Improve bd doctor JSONL checks to focus on real problems

Previously bd doctor warned about using beads.jsonl vs issues.jsonl, but
users should be free to configure any name they want. The real problems are:
1. Having multiple JSONL files (sync/merge conflicts)
2. Configuration not matching reality

Changes:
- Rewrote CheckLegacyJSONLFilename to scan for ALL .jsonl files
- Now filters out merge artifacts (backup, .orig, .bak, etc.)
- Warns only when multiple real JSONL files exist
- Added CheckDatabaseConfig to detect when configured paths do not match
  what actually exists on disk
- Updated tests to verify backup files are ignored
- Added test cases for custom JSONL filenames

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-21 23:44:26 -08:00
parent c4c5c8063a
commit 87ee3a674e
4 changed files with 164 additions and 35 deletions

File diff suppressed because one or more lines are too long

View File

@@ -275,21 +275,28 @@ func runDiagnostics(path string) doctorResult {
result.OverallOK = false
}
// Check 6: Legacy JSONL filename (issues.jsonl vs beads.jsonl)
// Check 6: Multiple JSONL files (excluding merge artifacts)
jsonlCheck := convertDoctorCheck(doctor.CheckLegacyJSONLFilename(path))
result.Checks = append(result.Checks, jsonlCheck)
if jsonlCheck.Status == statusWarning || jsonlCheck.Status == statusError {
result.OverallOK = false
}
// Check 7: Daemon health
// Check 7: Database/JSONL configuration mismatch
configCheck := convertDoctorCheck(doctor.CheckDatabaseConfig(path))
result.Checks = append(result.Checks, configCheck)
if configCheck.Status == statusWarning || configCheck.Status == statusError {
result.OverallOK = false
}
// Check 8: Daemon health
daemonCheck := checkDaemonStatus(path)
result.Checks = append(result.Checks, daemonCheck)
if daemonCheck.Status == statusWarning || daemonCheck.Status == statusError {
result.OverallOK = false
}
// Check 8: Database-JSONL sync
// Check 9: Database-JSONL sync
syncCheck := checkDatabaseJSONLSync(path)
result.Checks = append(result.Checks, syncCheck)
if syncCheck.Status == statusWarning || syncCheck.Status == statusError {

View File

@@ -5,6 +5,8 @@ import (
"os"
"path/filepath"
"strings"
"github.com/steveyegge/beads/internal/configfile"
)
// CheckLegacyBeadsSlashCommands detects old /beads:* slash commands in documentation
@@ -103,25 +105,47 @@ func CheckAgentDocumentation(repoPath string) DoctorCheck {
}
}
// CheckLegacyJSONLFilename detects if project is using non-standard beads.jsonl
// instead of the canonical issues.jsonl filename.
// CheckLegacyJSONLFilename detects if there are multiple JSONL files,
// which can cause sync/merge issues. Ignores merge artifacts and backups.
func CheckLegacyJSONLFilename(repoPath string) DoctorCheck {
beadsDir := filepath.Join(repoPath, ".beads")
var jsonlFiles []string
hasBeadsJSON := false
for _, name := range []string{"issues.jsonl", "beads.jsonl"} {
jsonlPath := filepath.Join(beadsDir, name)
if _, err := os.Stat(jsonlPath); err == nil {
jsonlFiles = append(jsonlFiles, name)
if name == "beads.jsonl" {
hasBeadsJSON = true
}
// Find all .jsonl files
entries, err := os.ReadDir(beadsDir)
if err != nil {
return DoctorCheck{
Name: "JSONL Files",
Status: "ok",
Message: "No .beads directory found",
}
}
if len(jsonlFiles) == 0 {
var realJSONLFiles []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
// Must end with .jsonl
if !strings.HasSuffix(name, ".jsonl") {
continue
}
// Skip merge artifacts and backups
lowerName := strings.ToLower(name)
if strings.Contains(lowerName, "backup") ||
strings.Contains(lowerName, ".orig") ||
strings.Contains(lowerName, ".bak") ||
strings.Contains(lowerName, "~") ||
strings.HasPrefix(lowerName, "backup_") {
continue
}
realJSONLFiles = append(realJSONLFiles, name)
}
if len(realJSONLFiles) == 0 {
return DoctorCheck{
Name: "JSONL Files",
Status: "ok",
@@ -129,28 +153,108 @@ func CheckLegacyJSONLFilename(repoPath string) DoctorCheck {
}
}
if len(jsonlFiles) == 1 {
// Single JSONL file - check if it's the non-standard name
if hasBeadsJSON {
return DoctorCheck{
Name: "JSONL Files",
Status: "warning",
Message: "Using non-standard JSONL filename: beads.jsonl",
Fix: "Run 'git mv .beads/beads.jsonl .beads/issues.jsonl' to use canonical name",
}
}
if len(realJSONLFiles) == 1 {
return DoctorCheck{
Name: "JSONL Files",
Status: "ok",
Message: fmt.Sprintf("Using %s", jsonlFiles[0]),
Message: fmt.Sprintf("Using %s", realJSONLFiles[0]),
}
}
// Multiple JSONL files found
// Multiple JSONL files found - this is a problem!
return DoctorCheck{
Name: "JSONL Files",
Status: "warning",
Message: fmt.Sprintf("Multiple JSONL files found: %s", strings.Join(jsonlFiles, ", ")),
Fix: "Run 'git rm .beads/beads.jsonl' to standardize on issues.jsonl (canonical name)",
Message: fmt.Sprintf("Multiple JSONL files found: %s", strings.Join(realJSONLFiles, ", ")),
Detail: "Having multiple JSONL files can cause sync and merge conflicts.\n" +
" Only one JSONL file should be used per repository.",
Fix: "Determine which file is current and remove the others:\n" +
" 1. Check 'bd stats' to see which file is being used\n" +
" 2. Verify with 'git log .beads/*.jsonl' to see commit history\n" +
" 3. Remove the unused file(s): git rm .beads/<unused>.jsonl\n" +
" 4. Commit the change",
}
}
// CheckDatabaseConfig verifies that the configured database and JSONL paths
// match what actually exists on disk.
func CheckDatabaseConfig(repoPath string) DoctorCheck {
beadsDir := filepath.Join(repoPath, ".beads")
// Load config
cfg, err := configfile.Load(beadsDir)
if err != nil || cfg == nil {
// No config or error reading - use defaults
return DoctorCheck{
Name: "Database Config",
Status: "ok",
Message: "Using default configuration",
}
}
var issues []string
// Check if configured database exists
if cfg.Database != "" {
dbPath := cfg.DatabasePath(beadsDir)
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
// Check if other .db files exist
entries, _ := os.ReadDir(beadsDir)
var otherDBs []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".db") {
otherDBs = append(otherDBs, entry.Name())
}
}
if len(otherDBs) > 0 {
issues = append(issues, fmt.Sprintf("Configured database '%s' not found, but found: %s",
cfg.Database, strings.Join(otherDBs, ", ")))
}
}
}
// Check if configured JSONL exists
if cfg.JSONLExport != "" {
jsonlPath := cfg.JSONLPath(beadsDir)
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
// Check if other .jsonl files exist
entries, _ := os.ReadDir(beadsDir)
var otherJSONLs []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".jsonl") {
name := entry.Name()
// Skip backups
lowerName := strings.ToLower(name)
if !strings.Contains(lowerName, "backup") &&
!strings.Contains(lowerName, ".orig") &&
!strings.Contains(lowerName, ".bak") {
otherJSONLs = append(otherJSONLs, name)
}
}
}
if len(otherJSONLs) > 0 {
issues = append(issues, fmt.Sprintf("Configured JSONL '%s' not found, but found: %s",
cfg.JSONLExport, strings.Join(otherJSONLs, ", ")))
}
}
}
if len(issues) == 0 {
return DoctorCheck{
Name: "Database Config",
Status: "ok",
Message: "Configuration matches existing files",
}
}
return DoctorCheck{
Name: "Database Config",
Status: "warning",
Message: "Configuration mismatch detected",
Detail: strings.Join(issues, "\n "),
Fix: "Update configuration in .beads/metadata.json:\n" +
" 1. Check which files are actually being used\n" +
" 2. Update metadata.json to match the actual filenames\n" +
" 3. Or rename the files to match the configuration",
}
}

View File

@@ -190,20 +190,38 @@ func TestCheckLegacyJSONLFilename(t *testing.T) {
expectWarning: false,
},
{
name: "canonical issues.jsonl",
name: "single issues.jsonl",
files: []string{"issues.jsonl"},
expectedStatus: "ok",
expectWarning: false,
},
{
name: "non-standard beads.jsonl",
name: "single beads.jsonl is ok",
files: []string{"beads.jsonl"},
expectedStatus: "ok",
expectWarning: false,
},
{
name: "custom name is ok",
files: []string{"my-issues.jsonl"},
expectedStatus: "ok",
expectWarning: false,
},
{
name: "multiple JSONL files warning",
files: []string{"beads.jsonl", "issues.jsonl"},
expectedStatus: "warning",
expectWarning: true,
},
{
name: "both files present",
files: []string{"beads.jsonl", "issues.jsonl"},
name: "backup files ignored",
files: []string{"issues.jsonl", "issues.jsonl.backup", "BACKUP_issues.jsonl"},
expectedStatus: "ok",
expectWarning: false,
},
{
name: "multiple real files with backups",
files: []string{"issues.jsonl", "beads.jsonl", "issues.jsonl.backup"},
expectedStatus: "warning",
expectWarning: true,
},