fix: resolve P2 sync noise and cleanup issues

- bd-6pni: Auto-filter tombstoned issues with mismatched prefixes during
  import instead of failing. Tombstones from contributor PRs with different
  test prefixes are pollution and safe to ignore.

- bd-ffr9: Stop recreating deletions.jsonl after tombstone migration.
  Added IsTombstoneMigrationComplete() check to all code paths that write
  to the legacy deletions manifest.

- bd-admx: Fix perpetual "JSONL file hash mismatch" warning. Now clears
  both export_hashes AND jsonl_file_hash when mismatch detected, so the
  warning doesn't repeat.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-16 00:55:43 -08:00
parent 88ccce884c
commit 2c86404d65
9 changed files with 341 additions and 30 deletions

View File

@@ -401,11 +401,15 @@ func validateJSONLIntegrity(ctx context.Context, jsonlPath string) (bool, error)
jsonlData, err := os.ReadFile(jsonlPath)
if err != nil {
if os.IsNotExist(err) {
// JSONL doesn't exist but we have a stored hash - clear export_hashes
// JSONL doesn't exist but we have a stored hash - clear export_hashes and jsonl_file_hash
fmt.Fprintf(os.Stderr, "⚠️ WARNING: JSONL file missing but export_hashes exist. Clearing export_hashes.\n")
if err := store.ClearAllExportHashes(ctx); err != nil {
return false, fmt.Errorf("failed to clear export_hashes: %w", err)
}
// Also clear jsonl_file_hash to prevent perpetual mismatch warnings (bd-admx)
if err := store.SetJSONLFileHash(ctx, ""); err != nil {
return false, fmt.Errorf("failed to clear jsonl_file_hash: %w", err)
}
return true, nil // Signal full export needed
}
return false, fmt.Errorf("failed to read JSONL file: %w", err)
@@ -421,11 +425,15 @@ func validateJSONLIntegrity(ctx context.Context, jsonlPath string) (bool, error)
fmt.Fprintf(os.Stderr, "⚠️ WARNING: JSONL file hash mismatch detected (bd-160)\n")
fmt.Fprintf(os.Stderr, " This indicates JSONL and export_hashes are out of sync.\n")
fmt.Fprintf(os.Stderr, " Clearing export_hashes to force full re-export.\n")
// Clear export_hashes to force full re-export
if err := store.ClearAllExportHashes(ctx); err != nil {
return false, fmt.Errorf("failed to clear export_hashes: %w", err)
}
// Also clear jsonl_file_hash to prevent perpetual mismatch warnings (bd-admx)
if err := store.SetJSONLFileHash(ctx, ""); err != nil {
return false, fmt.Errorf("failed to clear jsonl_file_hash: %w", err)
}
return true, nil // Signal full export needed
}

View File

@@ -685,7 +685,14 @@ func getDeletionsPath() string {
// recordDeletion appends a deletion record to the deletions manifest.
// This MUST be called BEFORE deleting from the database to ensure
// deletion records are never lost.
// After tombstone migration (bd-ffr9), this is a no-op since inline tombstones
// are used instead of deletions.jsonl.
func recordDeletion(id, deleteActor, reason string) error {
// bd-ffr9: Skip writing to deletions.jsonl if tombstone migration is complete
beadsDir := filepath.Dir(dbPath)
if deletions.IsTombstoneMigrationComplete(beadsDir) {
return nil
}
record := deletions.DeletionRecord{
ID: id,
Timestamp: time.Now().UTC(),
@@ -698,7 +705,14 @@ func recordDeletion(id, deleteActor, reason string) error {
// recordDeletions appends multiple deletion records to the deletions manifest.
// This MUST be called BEFORE deleting from the database to ensure
// deletion records are never lost.
// After tombstone migration (bd-ffr9), this is a no-op since inline tombstones
// are used instead of deletions.jsonl.
func recordDeletions(ids []string, deleteActor, reason string) error {
// bd-ffr9: Skip writing to deletions.jsonl if tombstone migration is complete
beadsDir := filepath.Dir(dbPath)
if deletions.IsTombstoneMigrationComplete(beadsDir) {
return nil
}
path := getDeletionsPath()
for _, id := range ids {
record := deletions.DeletionRecord{

View File

@@ -62,6 +62,39 @@ func TestRecordDeletion(t *testing.T) {
}
}
// TestRecordDeletion_SkipsAfterMigration tests that recordDeletion is a no-op after tombstone migration (bd-ffr9)
func TestRecordDeletion_SkipsAfterMigration(t *testing.T) {
tmpDir := t.TempDir()
// Set up dbPath so getDeletionsPath() works
oldDbPath := dbPath
dbPath = filepath.Join(tmpDir, "beads.db")
defer func() { dbPath = oldDbPath }()
// Create the .beads directory
if err := os.MkdirAll(tmpDir, 0750); err != nil {
t.Fatalf("failed to create directory: %v", err)
}
// Create the .migrated marker file to indicate tombstone migration is complete
migratedPath := filepath.Join(tmpDir, "deletions.jsonl.migrated")
if err := os.WriteFile(migratedPath, []byte("{}"), 0644); err != nil {
t.Fatalf("failed to create migrated marker: %v", err)
}
// Test recordDeletion - should be a no-op
err := recordDeletion("test-abc", "test-user", "test reason")
if err != nil {
t.Fatalf("recordDeletion failed: %v", err)
}
// Verify deletions.jsonl was NOT created
deletionsPath := getDeletionsPath()
if _, err := os.Stat(deletionsPath); !os.IsNotExist(err) {
t.Error("deletions.jsonl should not be created after tombstone migration")
}
}
// TestRecordDeletions tests that recordDeletions creates multiple deletion manifest entries
func TestRecordDeletions(t *testing.T) {
tmpDir := t.TempDir()

View File

@@ -16,12 +16,21 @@ import (
// HydrateDeletionsManifest populates deletions.jsonl from git history.
// It finds all issue IDs that were ever in the JSONL but are no longer present,
// and adds them to the deletions manifest.
// Note (bd-ffr9): After tombstone migration, this is a no-op since inline tombstones
// are used instead of deletions.jsonl.
func HydrateDeletionsManifest(path string) error {
if err := validateBeadsWorkspace(path); err != nil {
return err
}
beadsDir := filepath.Join(path, ".beads")
// bd-ffr9: Skip hydrating deletions.jsonl if tombstone migration is complete
if deletions.IsTombstoneMigrationComplete(beadsDir) {
fmt.Println(" Tombstone migration complete - skipping deletions.jsonl hydration")
return nil
}
// bd-6xd: issues.jsonl is the canonical filename
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")

View File

@@ -100,12 +100,12 @@ func TestJSONLIntegrityValidation(t *testing.T) {
if err := os.WriteFile(jsonlPath, []byte(`{"id":"bd-1","title":"Modified"}`+"\n"), 0644); err != nil {
t.Fatalf("failed to modify JSONL: %v", err)
}
// Add an export hash to verify it gets cleared
if err := testStore.SetExportHash(ctx, "bd-1", "dummy-hash"); err != nil {
t.Fatalf("failed to set export hash: %v", err)
}
// Validate should detect mismatch and clear export_hashes
needsFullExport, err := validateJSONLIntegrity(ctx, jsonlPath)
if err != nil {
@@ -114,7 +114,7 @@ func TestJSONLIntegrityValidation(t *testing.T) {
if !needsFullExport {
t.Fatalf("expected needsFullExport=true after clearing export_hashes")
}
// Verify export_hashes were cleared
hash, err := testStore.GetExportHash(ctx, "bd-1")
if err != nil {
@@ -123,6 +123,15 @@ func TestJSONLIntegrityValidation(t *testing.T) {
if hash != "" {
t.Fatalf("expected export hash to be cleared, got %q", hash)
}
// Verify jsonl_file_hash was also cleared (bd-admx fix)
fileHash, err := testStore.GetJSONLFileHash(ctx)
if err != nil {
t.Fatalf("failed to get JSONL file hash: %v", err)
}
if fileHash != "" {
t.Fatalf("expected jsonl_file_hash to be cleared to prevent perpetual warnings, got %q", fileHash)
}
})
// Test 3: Missing JSONL file
@@ -131,17 +140,17 @@ func TestJSONLIntegrityValidation(t *testing.T) {
if err := testStore.SetJSONLFileHash(ctx, "some-hash"); err != nil {
t.Fatalf("failed to set JSONL file hash: %v", err)
}
// Add an export hash
if err := testStore.SetExportHash(ctx, "bd-1", "dummy-hash"); err != nil {
t.Fatalf("failed to set export hash: %v", err)
}
// Remove JSONL file
if err := os.Remove(jsonlPath); err != nil {
t.Fatalf("failed to remove JSONL: %v", err)
}
// Validate should detect missing file and clear export_hashes
needsFullExport, err := validateJSONLIntegrity(ctx, jsonlPath)
if err != nil {
@@ -150,7 +159,7 @@ func TestJSONLIntegrityValidation(t *testing.T) {
if !needsFullExport {
t.Fatalf("expected needsFullExport=true after clearing export_hashes")
}
// Verify export_hashes were cleared
hash, err := testStore.GetExportHash(ctx, "bd-1")
if err != nil {
@@ -159,6 +168,15 @@ func TestJSONLIntegrityValidation(t *testing.T) {
if hash != "" {
t.Fatalf("expected export hash to be cleared, got %q", hash)
}
// Verify jsonl_file_hash was also cleared (bd-admx fix)
fileHash, err := testStore.GetJSONLFileHash(ctx)
if err != nil {
t.Fatalf("failed to get JSONL file hash: %v", err)
}
if fileHash != "" {
t.Fatalf("expected jsonl_file_hash to be cleared to prevent perpetual warnings, got %q", fileHash)
}
})
}