refactor: remove all deletions.jsonl code (bd-fom)
Complete removal of the legacy deletions.jsonl manifest system. Tombstones are now the sole deletion mechanism. Removed: - internal/deletions/ - entire package - cmd/bd/deleted.go - deleted command - cmd/bd/doctor/fix/deletions.go - HydrateDeletionsManifest - Tests for all removed functionality Cleaned: - cmd/bd/sync.go - removed sanitize, auto-compact - cmd/bd/delete.go - removed dual-writes - cmd/bd/doctor.go - removed checkDeletionsManifest - internal/importer/importer.go - removed deletions checks - internal/syncbranch/worktree.go - removed deletions merge - cmd/bd/integrity.go - updated validation (warn-only on decrease) Files removed: 12 Lines removed: ~7500 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/deletions"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// legacyDeletionRecordCmd represents a single deletion entry from the legacy deletions.jsonl manifest.
|
||||
// This is inlined here for migration purposes only - new code uses inline tombstones.
|
||||
type legacyDeletionRecordCmd struct {
|
||||
ID string `json:"id"` // Issue ID that was deleted
|
||||
Timestamp time.Time `json:"ts"` // When the deletion occurred
|
||||
Actor string `json:"by"` // Who performed the deletion
|
||||
Reason string `json:"reason,omitempty"` // Optional reason for deletion
|
||||
}
|
||||
|
||||
// loadLegacyDeletionsCmd reads the legacy deletions.jsonl manifest.
|
||||
// Returns a map of deletion records keyed by issue ID and any warnings.
|
||||
// This is inlined here for migration purposes only.
|
||||
func loadLegacyDeletionsCmd(path string) (map[string]legacyDeletionRecordCmd, []string, error) {
|
||||
records := make(map[string]legacyDeletionRecordCmd)
|
||||
var warnings []string
|
||||
|
||||
f, err := os.Open(path) // #nosec G304 - controlled path from caller
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return records, nil, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to open deletions file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 0, 1024), 1024*1024)
|
||||
|
||||
lineNum := 0
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var record legacyDeletionRecordCmd
|
||||
if err := json.Unmarshal([]byte(line), &record); err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("line %d: invalid JSON", lineNum))
|
||||
continue
|
||||
}
|
||||
if record.ID == "" {
|
||||
warnings = append(warnings, fmt.Sprintf("line %d: missing ID", lineNum))
|
||||
continue
|
||||
}
|
||||
records[record.ID] = record
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, nil, fmt.Errorf("error reading deletions file: %w", err)
|
||||
}
|
||||
|
||||
return records, warnings, nil
|
||||
}
|
||||
|
||||
var migrateTombstonesCmd = &cobra.Command{
|
||||
Use: "migrate-tombstones",
|
||||
Short: "Convert deletions.jsonl entries to inline tombstones",
|
||||
@@ -59,11 +115,11 @@ Examples:
|
||||
}
|
||||
|
||||
// Check paths
|
||||
deletionsPath := deletions.DefaultPath(beadsDir)
|
||||
deletionsPath := filepath.Join(beadsDir, "deletions.jsonl")
|
||||
issuesPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
// Load existing deletions
|
||||
loadResult, err := deletions.LoadDeletions(deletionsPath)
|
||||
records, warnings, err := loadLegacyDeletionsCmd(deletionsPath)
|
||||
if err != nil {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
@@ -76,13 +132,13 @@ Examples:
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(loadResult.Records) == 0 {
|
||||
if len(records) == 0 {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"status": "noop",
|
||||
"message": "No deletions to migrate",
|
||||
"status": "noop",
|
||||
"message": "No deletions to migrate",
|
||||
"migrated": 0,
|
||||
"skipped": 0,
|
||||
"skipped": 0,
|
||||
})
|
||||
} else {
|
||||
fmt.Println("No deletions.jsonl entries to migrate")
|
||||
@@ -91,7 +147,7 @@ Examples:
|
||||
}
|
||||
|
||||
// Print warnings from loading
|
||||
for _, warning := range loadResult.Warnings {
|
||||
for _, warning := range warnings {
|
||||
if !jsonOutput {
|
||||
color.Yellow("Warning: %s\n", warning)
|
||||
}
|
||||
@@ -132,9 +188,9 @@ Examples:
|
||||
}
|
||||
|
||||
// Determine which deletions need migration
|
||||
var toMigrate []deletions.DeletionRecord
|
||||
var toMigrate []legacyDeletionRecordCmd
|
||||
var skippedIDs []string
|
||||
for id, record := range loadResult.Records {
|
||||
for id, record := range records {
|
||||
if existingTombstones[id] {
|
||||
skippedIDs = append(skippedIDs, id)
|
||||
if verbose && !jsonOutput {
|
||||
@@ -148,10 +204,10 @@ Examples:
|
||||
if len(toMigrate) == 0 {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"status": "noop",
|
||||
"message": "All deletions already migrated to tombstones",
|
||||
"status": "noop",
|
||||
"message": "All deletions already migrated to tombstones",
|
||||
"migrated": 0,
|
||||
"skipped": len(skippedIDs),
|
||||
"skipped": len(skippedIDs),
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("All %d deletion(s) already have tombstones in issues.jsonl\n", len(skippedIDs))
|
||||
@@ -163,10 +219,10 @@ Examples:
|
||||
if dryRun {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"dry_run": true,
|
||||
"dry_run": true,
|
||||
"would_migrate": len(toMigrate),
|
||||
"skipped": len(skippedIDs),
|
||||
"total": len(loadResult.Records),
|
||||
"skipped": len(skippedIDs),
|
||||
"total": len(records),
|
||||
})
|
||||
} else {
|
||||
fmt.Println("Dry run mode - no changes will be made")
|
||||
@@ -208,12 +264,12 @@ Examples:
|
||||
encoder := json.NewEncoder(file)
|
||||
var migratedIDs []string
|
||||
for _, record := range toMigrate {
|
||||
tombstone := convertDeletionRecordToTombstone(record)
|
||||
tombstone := convertLegacyDeletionRecordToTombstone(record)
|
||||
if err := encoder.Encode(tombstone); err != nil {
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"error": "write_tombstone_failed",
|
||||
"message": err.Error(),
|
||||
"error": "write_tombstone_failed",
|
||||
"message": err.Error(),
|
||||
"issue_id": record.ID,
|
||||
})
|
||||
} else {
|
||||
@@ -241,11 +297,11 @@ Examples:
|
||||
// Success output
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"status": "success",
|
||||
"migrated": len(migratedIDs),
|
||||
"skipped": len(skippedIDs),
|
||||
"total": len(loadResult.Records),
|
||||
"archive": archivePath,
|
||||
"status": "success",
|
||||
"migrated": len(migratedIDs),
|
||||
"skipped": len(skippedIDs),
|
||||
"total": len(records),
|
||||
"archive": archivePath,
|
||||
"migrated_ids": migratedIDs,
|
||||
})
|
||||
} else {
|
||||
@@ -262,10 +318,8 @@ Examples:
|
||||
},
|
||||
}
|
||||
|
||||
// convertDeletionRecordToTombstone creates a tombstone issue from a deletion record.
|
||||
// This is similar to the importer's convertDeletionToTombstone but operates on
|
||||
// deletions.DeletionRecord directly.
|
||||
func convertDeletionRecordToTombstone(del deletions.DeletionRecord) *types.Issue {
|
||||
// convertLegacyDeletionRecordToTombstone creates a tombstone issue from a legacy deletion record.
|
||||
func convertLegacyDeletionRecordToTombstone(del legacyDeletionRecordCmd) *types.Issue {
|
||||
deletedAt := del.Timestamp
|
||||
return &types.Issue{
|
||||
ID: del.ID,
|
||||
|
||||
Reference in New Issue
Block a user