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:
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user