feat(deletions): add pruning and git history fallback

Implements two P1 tasks for the deletions manifest epic:

bd-v2x: Add deletions pruning to bd compact
- PruneDeletions function removes records older than retention period
- Default retention: 7 days (configurable via metadata.json)
- CLI --retention flag for override
- Atomic file rewrite prevents corruption
- Called automatically during all compact operations

bd-pnm: Add git history fallback for pruned deletions
- Catches deletions where manifest entry was pruned
- Uses git log -S to search for ID in JSONL history
- Batches multiple IDs for efficiency (git -G regex)
- Self-healing: backfills manifest on hit
- Conservative: keeps issue if git check fails (shallow clone)

Tests added for both features with edge cases covered.

🤖 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-25 12:41:29 -08:00
parent 1804a91787
commit 3f84ec3774
7 changed files with 587 additions and 22 deletions

View File

@@ -179,3 +179,54 @@ func WriteDeletions(path string, records []DeletionRecord) error {
func DefaultPath(beadsDir string) string {
return filepath.Join(beadsDir, "deletions.jsonl")
}
// DefaultRetentionDays is the default number of days to retain deletion records.
const DefaultRetentionDays = 7
// PruneResult contains the result of a prune operation.
type PruneResult struct {
KeptCount int
PrunedCount int
PrunedIDs []string
}
// PruneDeletions removes deletion records older than the specified retention period.
// Returns PruneResult with counts and IDs of pruned records.
// If the file doesn't exist or is empty, returns zero counts with no error.
func PruneDeletions(path string, retentionDays int) (*PruneResult, error) {
result := &PruneResult{
PrunedIDs: []string{},
}
loadResult, err := LoadDeletions(path)
if err != nil {
return nil, fmt.Errorf("failed to load deletions: %w", err)
}
if len(loadResult.Records) == 0 {
return result, nil
}
cutoff := time.Now().AddDate(0, 0, -retentionDays)
var kept []DeletionRecord
for _, record := range loadResult.Records {
if record.Timestamp.After(cutoff) || record.Timestamp.Equal(cutoff) {
kept = append(kept, record)
} else {
result.PrunedCount++
result.PrunedIDs = append(result.PrunedIDs, record.ID)
}
}
result.KeptCount = len(kept)
// Only rewrite if we actually pruned something
if result.PrunedCount > 0 {
if err := WriteDeletions(path, kept); err != nil {
return nil, fmt.Errorf("failed to write pruned deletions: %w", err)
}
}
return result, nil
}