Fix bd-khnb: Prevent auto-import from resurrecting deleted issues

Replace mtime-based staleness detection with content-based (SHA256 hash) to prevent
git operations from resurrecting deleted issues.

**Problem:**
Auto-import used file modification time to detect if JSONL was "newer" than database.
Git operations (checkout, merge, pull) restore old files with recent mtimes, causing
auto-import to load stale data over current database state, resurrecting deleted issues.

**Solution:**
- Added computeJSONLHash() to compute SHA256 of JSONL content
- Added hasJSONLChanged() with two-tier check:
  1. Fast-path: Check mtime first (99% of checks are instant)
  2. Slow-path: Compute hash only if mtime changed (catches git operations)
- Store metadata: last_import_hash, last_import_mtime, last_import_time
- Updated auto-import in daemon_sync.go to use content-based check
- Updated validatePreExport to use content-based check (bd-xwo)
- Graceful degradation: metadata failures are non-fatal warnings

**Changes:**
- cmd/bd/integrity.go: Add computeJSONLHash(), hasJSONLChanged()
- cmd/bd/integrity_test.go: Add comprehensive tests for new functions
- cmd/bd/import.go: Update metadata after import
- cmd/bd/sync.go: Use hasJSONLChanged() instead of isJSONLNewer()
- cmd/bd/daemon_sync.go: Use hasJSONLChanged() in auto-import

**Testing:**
- Unit tests pass (TestHasJSONLChanged with 7 scenarios)
- Integration test passes (test_bd_khnb_fix.sh)
- Verified git resurrection scenario prevented

Fixes: bd-khnb
Related: bd-3bg, bd-xwo, bd-39o, bd-56p, bd-m8t, bd-rfj, bd-t5o

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-20 21:49:52 -05:00
parent 57253f93a3
commit 0020eb490c
5 changed files with 480 additions and 42 deletions

View File

@@ -126,14 +126,17 @@ Use --merge to merge the sync branch back to main branch.`,
if dryRun {
fmt.Println("→ [DRY RUN] Would export pending changes to JSONL")
} else {
// Smart conflict resolution: if JSONL is newer, auto-import first
if isJSONLNewer(jsonlPath) {
fmt.Println("→ JSONL is newer than database, importing first...")
if err := importFromJSONL(ctx, jsonlPath, renameOnImport); err != nil {
fmt.Fprintf(os.Stderr, "Error auto-importing: %v\n", err)
os.Exit(1)
// Smart conflict resolution: if JSONL content changed, auto-import first
// Use content-based check (not mtime) to avoid git resurrection bug (bd-khnb)
if err := ensureStoreActive(); err == nil && store != nil {
if hasJSONLChanged(ctx, store, jsonlPath) {
fmt.Println("→ JSONL content changed, importing first...")
if err := importFromJSONL(ctx, jsonlPath, renameOnImport); err != nil {
fmt.Fprintf(os.Stderr, "Error auto-importing: %v\n", err)
os.Exit(1)
}
fmt.Println("✓ Auto-import complete")
}
fmt.Println("✓ Auto-import complete")
}
// Pre-export integrity checks
@@ -600,6 +603,30 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error {
// Clear auto-flush state
clearAutoFlushState()
// Update last_import_hash metadata to enable content-based staleness detection (bd-khnb fix)
// After export, database and JSONL are in sync, so update hash to prevent unnecessary auto-import
if currentHash, err := computeJSONLHash(jsonlPath); err == nil {
if err := store.SetMetadata(ctx, "last_import_hash", currentHash); err != nil {
// Non-fatal warning: Metadata update failures are intentionally non-fatal to prevent blocking
// successful exports. System degrades gracefully to mtime-based staleness detection if metadata
// is unavailable. This ensures export operations always succeed even if metadata storage fails.
fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_hash: %v\n", err)
}
exportTime := time.Now().Format(time.RFC3339)
if err := store.SetMetadata(ctx, "last_import_time", exportTime); err != nil {
// Non-fatal warning (see above comment about graceful degradation)
fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_time: %v\n", err)
}
// Store mtime for fast-path optimization in hasJSONLChanged (bd-3bg)
if jsonlInfo, statErr := os.Stat(jsonlPath); statErr == nil {
mtimeStr := fmt.Sprintf("%d", jsonlInfo.ModTime().Unix())
if err := store.SetMetadata(ctx, "last_import_mtime", mtimeStr); err != nil {
// Non-fatal warning (see above comment about graceful degradation)
fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_mtime: %v\n", err)
}
}
}
// Update database mtime to be >= JSONL mtime (fixes #278, #301, #321)
// This prevents validatePreExport from incorrectly blocking on next export
beadsDir := filepath.Dir(jsonlPath)