feat: Enhance bd doctor with force repair and source selection

Add comprehensive database corruption recovery capabilities to bd doctor.

## Changes

### New Command Flags

- --force: Force repair mode that bypasses database validation
- --source: Choose source of truth (auto/jsonl/db) for recovery

### Enhanced Error Classification

Improved CheckDatabaseIntegrity() to detect and classify:
- Database locked errors (suggests killing processes, removing locks)
- Invalid SQLite files (suggests JSONL recovery with exact commands)
- Migration/validation failures (orphaned dependencies, etc.)
- Generic database errors (context-aware suggestions)

Each error type provides:
- Specific diagnosis
- Step-by-step recovery instructions
- Appropriate command examples with new flags

### Force Recovery Implementation

New DatabaseCorruptionRecoveryWithOptions() function:
- Bypasses database validation when --force is used
- Supports explicit source of truth selection
- Auto-detects best recovery path when source=auto
- Comprehensive rollback on failure
- Uses --force --no-git-history in import during force mode

### Integration

Updated fix orchestration to pass force and source flags to recovery.

## Usage Examples

```bash
# Unopenable database with validation errors
bd doctor --fix --force --source=jsonl

# Choose specific source of truth
bd doctor --fix --source=jsonl  # Trust JSONL
bd doctor --fix --source=db     # Trust database
bd doctor --fix --source=auto   # Auto-detect (default)

# Force recovery with auto-detection
bd doctor --fix --force
```

## Problem Solved

Before: When database had validation errors (orphaned dependencies,
foreign key violations), all bd commands failed in a catch-22 situation.
Could not open database to fix database. Users had to manually delete
database files and reinit.

After: bd doctor --fix --force detects unopenable databases, provides
clear recovery steps, and forces rebuild from JSONL even when database
validation fails.

## Backward Compatibility

- All new flags are optional with safe defaults
- --source defaults to 'auto' (existing behavior)
- --force is opt-in only
- Existing bd doctor behavior unchanged when flags not used
- DatabaseCorruptionRecovery() still exists for compatibility

Fixes: bd-pgza
This commit is contained in:
kraitsura
2025-12-29 22:59:48 -08:00
parent aa759c06aa
commit 602c59eb48
4 changed files with 202 additions and 11 deletions

View File

@@ -249,28 +249,66 @@ func CheckDatabaseIntegrity(path string) DoctorCheck {
// Open database in read-only mode for integrity check
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
if err != nil {
// Classify the error type for better recovery guidance
errMsg := err.Error()
var errorType string
var recoverySteps string
// Check if JSONL recovery is possible
jsonlCount, _, jsonlErr := CountJSONLIssues(filepath.Join(beadsDir, "issues.jsonl"))
if jsonlErr != nil {
jsonlCount, _, jsonlErr = CountJSONLIssues(filepath.Join(beadsDir, "beads.jsonl"))
}
if jsonlErr == nil && jsonlCount > 0 {
return DoctorCheck{
Name: "Database Integrity",
Status: StatusError,
Message: fmt.Sprintf("Failed to open database (JSONL has %d issues for recovery)", jsonlCount),
Detail: err.Error(),
Fix: "Run 'bd doctor --fix' to recover from JSONL backup",
// Classify error type
if strings.Contains(errMsg, "database is locked") {
errorType = "Database is locked"
recoverySteps = "1. Check for running bd processes: ps aux | grep bd\n" +
"2. Kill any stale processes\n" +
"3. Remove stale locks: rm .beads/beads.db-shm .beads/beads.db-wal .beads/daemon.lock\n" +
"4. Retry: bd doctor --fix"
} else if strings.Contains(errMsg, "not a database") || strings.Contains(errMsg, "file is not a database") {
errorType = "File is not a valid SQLite database"
if jsonlErr == nil && jsonlCount > 0 {
recoverySteps = fmt.Sprintf("Database file is corrupted beyond repair.\n\n"+
"Recovery steps:\n"+
"1. Backup corrupt database: mv .beads/beads.db .beads/beads.db.broken\n"+
"2. Rebuild from JSONL (%d issues): bd doctor --fix --force --source=jsonl\n"+
"3. Verify: bd stats", jsonlCount)
} else {
recoverySteps = "Database file is corrupted and no JSONL backup found.\n" +
"Manual recovery required:\n" +
"1. Restore from git: git checkout HEAD -- .beads/issues.jsonl\n" +
"2. Rebuild database: bd doctor --fix --force"
}
} else if strings.Contains(errMsg, "migration") || strings.Contains(errMsg, "validation failed") {
errorType = "Database migration or validation failed"
if jsonlErr == nil && jsonlCount > 0 {
recoverySteps = fmt.Sprintf("Database has validation errors (possibly orphaned dependencies).\n\n"+
"Recovery steps:\n"+
"1. Backup database: mv .beads/beads.db .beads/beads.db.broken\n"+
"2. Rebuild from JSONL (%d issues): bd doctor --fix --force --source=jsonl\n"+
"3. Verify: bd stats\n\n"+
"Alternative: bd doctor --fix --force (attempts to repair in-place)", jsonlCount)
} else {
recoverySteps = "Database validation failed and no JSONL backup available.\n" +
"Try: bd doctor --fix --force"
}
} else {
errorType = "Failed to open database"
if jsonlErr == nil && jsonlCount > 0 {
recoverySteps = fmt.Sprintf("Run 'bd doctor --fix --source=jsonl' to rebuild from JSONL (%d issues)", jsonlCount)
} else {
recoverySteps = "Run 'bd doctor --fix --force' to attempt recovery"
}
}
return DoctorCheck{
Name: "Database Integrity",
Status: StatusError,
Message: "Failed to open database for integrity check",
Detail: err.Error(),
Fix: "Run 'bd doctor --fix' to back up the corrupt DB and rebuild from JSONL (if available), or restore from backup",
Message: errorType,
Detail: fmt.Sprintf("%s\n\nError: %s", recoverySteps, errMsg),
Fix: "See recovery steps above",
}
}
defer db.Close()