Extended bd-tvus fix to all doctor check functions that access .beads/ directory. In Gas Town multi-clone setups, crew/polecat clones use .beads/redirect files to point to the shared mayor/rig beads directory. Doctor checks now use resolveBeadsDir() to follow these redirects: - daemon.go: CheckDaemonStatus - git.go: CheckSyncBranchConfig, FindOrphanedIssues, CheckOrphanedIssues - installation.go: CheckMultipleDatabases, CheckPermissions - integrity.go: CheckIDFormat, CheckDependencyCycles, CheckTombstones, CheckDeletionsManifest, CheckRepoFingerprint - jsonl_integrity.go: CheckJSONLIntegrity - legacy.go: CheckFreshClone - maintenance.go: CheckStaleClosedIssues, CheckExpiredTombstones, CheckCompactionCandidates - perf.go: RunPerformanceDiagnostics, CollectPlatformInfo - validation.go: CheckMergeArtifacts, CheckOrphanedDependencies, CheckDuplicateIssues, CheckTestPollution, CheckChildParentDependencies, CheckGitConflicts - version.go: CheckMetadataVersionTracking Intentionally NOT changed (check local files): - CheckInstallation: checks local .beads/ exists - CheckUntrackedBeadsFiles: checks git tracking of local files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
package doctor
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/steveyegge/beads/internal/beads"
|
|
"github.com/steveyegge/beads/internal/configfile"
|
|
"github.com/steveyegge/beads/internal/utils"
|
|
)
|
|
|
|
func CheckJSONLIntegrity(path string) DoctorCheck {
|
|
// Follow redirect to resolve actual beads directory (bd-tvus fix)
|
|
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
|
|
|
// Resolve JSONL path.
|
|
jsonlPath := ""
|
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil {
|
|
if cfg.JSONLExport != "" && !isSystemJSONLFilename(cfg.JSONLExport) {
|
|
p := cfg.JSONLPath(beadsDir)
|
|
if _, err := os.Stat(p); err == nil {
|
|
jsonlPath = p
|
|
}
|
|
}
|
|
}
|
|
if jsonlPath == "" {
|
|
// Fall back to a best-effort discovery within .beads/.
|
|
p := utils.FindJSONLInDir(beadsDir)
|
|
if _, err := os.Stat(p); err == nil {
|
|
jsonlPath = p
|
|
}
|
|
}
|
|
if jsonlPath == "" {
|
|
return DoctorCheck{Name: "JSONL Integrity", Status: StatusOK, Message: "N/A (no JSONL file)"}
|
|
}
|
|
|
|
// Best-effort scan for malformed lines.
|
|
f, err := os.Open(jsonlPath) // #nosec G304 -- jsonlPath is within the workspace
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "JSONL Integrity",
|
|
Status: StatusWarning,
|
|
Message: "Unable to read JSONL file",
|
|
Detail: err.Error(),
|
|
}
|
|
}
|
|
defer f.Close()
|
|
|
|
var malformed int
|
|
var examples []string
|
|
scanner := bufio.NewScanner(f)
|
|
lineNo := 0
|
|
for scanner.Scan() {
|
|
lineNo++
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var v struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.Unmarshal([]byte(line), &v); err != nil || v.ID == "" {
|
|
malformed++
|
|
if len(examples) < 5 {
|
|
if err != nil {
|
|
examples = append(examples, fmt.Sprintf("line %d: %v", lineNo, err))
|
|
} else {
|
|
examples = append(examples, fmt.Sprintf("line %d: missing id", lineNo))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return DoctorCheck{
|
|
Name: "JSONL Integrity",
|
|
Status: StatusWarning,
|
|
Message: "Unable to scan JSONL file",
|
|
Detail: err.Error(),
|
|
}
|
|
}
|
|
if malformed == 0 {
|
|
return DoctorCheck{
|
|
Name: "JSONL Integrity",
|
|
Status: StatusOK,
|
|
Message: fmt.Sprintf("%s looks valid", filepath.Base(jsonlPath)),
|
|
}
|
|
}
|
|
|
|
// If we have a database, we can auto-repair by re-exporting from DB.
|
|
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
|
dbPath = cfg.DatabasePath(beadsDir)
|
|
}
|
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
return DoctorCheck{
|
|
Name: "JSONL Integrity",
|
|
Status: StatusError,
|
|
Message: fmt.Sprintf("%s has %d malformed line(s)", filepath.Base(jsonlPath), malformed),
|
|
Detail: strings.Join(examples, "\n"),
|
|
Fix: "Restore the JSONL file from git or from a backup (no database available for auto-repair).",
|
|
}
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "JSONL Integrity",
|
|
Status: StatusError,
|
|
Message: fmt.Sprintf("%s has %d malformed line(s)", filepath.Base(jsonlPath), malformed),
|
|
Detail: strings.Join(examples, "\n"),
|
|
Fix: "Run 'bd doctor --fix' to back up the JSONL and regenerate it from the database.",
|
|
}
|
|
}
|
|
|
|
func isSystemJSONLFilename(name string) bool {
|
|
switch name {
|
|
case "deletions.jsonl", "interactions.jsonl", "molecules.jsonl":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|