Fix critical P0 database reinitialization bug (bd-130)

Fixes silent data loss when .beads/ directory removed and daemon auto-starts.

Root cause: checkGitForIssues() hardcoded 'issues.jsonl' but git tracks 'beads.jsonl'

Changes:
- Fix A (bd-131): checkGitForIssues() tries beads.jsonl first, then issues.jsonl
- Fix B (bd-132): Immediate export after import in bd init to prevent daemon race
- Fix C (bd-133): Safety check that fails loudly if import fails
- Fix D (bd-134): Daemon startup auto-import when DB empty but git has issues
- Tests (bd-135): Comprehensive integration test suite

Oracle-recommended improvements:
- Export to exact git-relative path (prevents path drift)
- filepath.ToSlash for Windows git compatibility
- 64MB scanner buffer for large JSONL lines
- Improved safety check messages (only suggest local file if exists)

All tests passing. No regressions.

Amp-Thread-ID: https://ampcode.com/threads/T-0e31dc6a-a0d9-46c6-87b2-cfdebe829a52
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-24 14:52:24 -07:00
parent 3b15b5b259
commit 14895bf97a
5 changed files with 515 additions and 24 deletions

View File

@@ -145,11 +145,56 @@ bd.db
fmt.Fprintf(os.Stderr, "Try manually: git show HEAD:%s | bd import -i /dev/stdin\n", jsonlPath)
}
// Non-fatal - continue with empty database
} else if !quiet {
fmt.Fprintf(os.Stderr, "✓ Successfully imported %d issues from git.\n\n", issueCount)
} else {
// CRITICAL: Immediately export to local JSONL to prevent daemon race condition
// The daemon might auto-start before the 5-second auto-flush debounce completes
// Write to exact git-relative path to prevent path drift
gitRoot := findGitRoot()
if gitRoot == "" {
if !quiet {
fmt.Fprintf(os.Stderr, "Warning: could not find git root for export\n")
}
} else {
absJSONL := filepath.Join(gitRoot, filepath.FromSlash(jsonlPath))
if err := os.MkdirAll(filepath.Dir(absJSONL), 0750); err != nil {
if !quiet {
fmt.Fprintf(os.Stderr, "Warning: failed to create export dir: %v\n", err)
}
} else if err := exportToJSONLWithStore(ctx, store, absJSONL); err != nil {
if !quiet {
fmt.Fprintf(os.Stderr, "Warning: failed to export after import: %v\n", err)
}
}
}
if !quiet {
fmt.Fprintf(os.Stderr, "✓ Successfully imported %d issues from git.\n\n", issueCount)
}
}
}
// Safety check: verify import succeeded and catch silent data loss
stats, err := store.GetStatistics(ctx)
if err == nil && stats.TotalIssues == 0 {
// DB empty after init - check if git has issues we failed to import
recheck, recheckPath := checkGitForIssues()
if recheck > 0 {
fmt.Fprintf(os.Stderr, "\n❌ ERROR: Database empty but git has %d issues!\n", recheck)
fmt.Fprintf(os.Stderr, "Auto-import failed. Manual recovery:\n")
fmt.Fprintf(os.Stderr, " git show HEAD:%s | bd import -i /dev/stdin\n", filepath.ToSlash(recheckPath))
// Only suggest local file import if file exists
gitRoot := findGitRoot()
if gitRoot != "" {
localFile := filepath.Join(gitRoot, filepath.FromSlash(recheckPath))
if _, err := os.Stat(localFile); err == nil {
fmt.Fprintf(os.Stderr, "Or:\n")
fmt.Fprintf(os.Stderr, " bd import -i %s\n", localFile)
}
}
_ = store.Close()
os.Exit(1)
}
}
if err := store.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", err)
}