From f7dea354d8086794ffe148d47b5d1b025ef59240 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 30 Nov 2025 21:21:47 -0800 Subject: [PATCH] fix(import): add warning when issues are skipped due to deletions manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When importing JSONL that contains issues in the deletions manifest, import now: - Filters out deleted issues before import - Prints per-issue warning with deletion details (date, actor) - Shows count of skipped issues in summary - Suggests --ignore-deletions flag to force import The new --ignore-deletions flag allows importing issues that are in the deletions manifest, useful for recovering accidentally deleted issues. Fixes bd-4zy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .beads/issues.jsonl | 1 + cmd/bd/import.go | 13 +++++++++++++ cmd/bd/import_shared.go | 6 ++++++ internal/importer/importer.go | 26 ++++++++++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index e69de29b..dfba33f9 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -0,0 +1 @@ +{"id":"bd-4zy","title":"Import silently skips issues in deletions manifest","description":"**Problem**: When JSONL has valid issues but the deletions manifest lists them as deleted, bd import returns '0 created, 0 updated' with NO warning. This is confusing because issues exist in JSONL but aren't imported.\n\n**Expected behavior**: \n- At minimum: warn that N issues were skipped due to deletions manifest\n- Better: offer to restore them or show what's being skipped\n\n**Requirements**:\n1. Add warning message when issues are skipped due to deletions\n2. Show count and optionally list IDs of skipped issues\n3. Consider --force flag to import anyway (ignoring deletions)\n\n**Files to check**: cmd/bd/import.go, internal/importer/","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-30T21:14:47.146103-08:00","updated_at":"2025-11-30T21:21:33.428328-08:00","closed_at":"2025-11-30T21:21:33.428328-08:00","close_reason":"Implemented filtering of deleted issues during import with warning message and --ignore-deletions flag"} diff --git a/cmd/bd/import.go b/cmd/bd/import.go index 40722178..79d9af72 100644 --- a/cmd/bd/import.go +++ b/cmd/bd/import.go @@ -96,6 +96,7 @@ NOTE: Import requires direct database access and does not work with daemon mode. orphanHandling, _ := cmd.Flags().GetString("orphan-handling") force, _ := cmd.Flags().GetBool("force") noGitHistory, _ := cmd.Flags().GetBool("no-git-history") + ignoreDeletions, _ := cmd.Flags().GetBool("ignore-deletions") // Check if stdin is being used interactively (not piped) if input == "" && term.IsTerminal(int(os.Stdin.Fd())) { @@ -255,6 +256,7 @@ NOTE: Import requires direct database access and does not work with daemon mode. ClearDuplicateExternalRefs: clearDuplicateExternalRefs, OrphanHandling: orphanHandling, NoGitHistory: noGitHistory, + IgnoreDeletions: ignoreDeletions, } result, err := importIssuesCore(ctx, dbPath, store, allIssues, opts) @@ -402,8 +404,18 @@ NOTE: Import requires direct database access and does not work with daemon mode. if len(result.IDMapping) > 0 { fmt.Fprintf(os.Stderr, ", %d issues remapped", len(result.IDMapping)) } + if result.SkippedDeleted > 0 { + fmt.Fprintf(os.Stderr, ", %d skipped (deleted)", result.SkippedDeleted) + } fmt.Fprintf(os.Stderr, "\n") + // Print skipped deleted issues summary if any (bd-4zy) + if result.SkippedDeleted > 0 { + fmt.Fprintf(os.Stderr, "\n⚠️ Skipped %d issue(s) found in deletions manifest\n", result.SkippedDeleted) + fmt.Fprintf(os.Stderr, " These issues were previously deleted and will not be resurrected.\n") + fmt.Fprintf(os.Stderr, " Use --ignore-deletions to force import anyway.\n") + } + // Print skipped dependencies summary if any if len(result.SkippedDependencies) > 0 { fmt.Fprintf(os.Stderr, "\n⚠️ Warning: Skipped %d dependencies due to missing references:\n", len(result.SkippedDependencies)) @@ -759,6 +771,7 @@ func init() { importCmd.Flags().String("orphan-handling", "", "How to handle missing parent issues: strict/resurrect/skip/allow (default: use config or 'allow')") importCmd.Flags().Bool("force", false, "Force metadata update even when database is already in sync with JSONL") importCmd.Flags().Bool("no-git-history", false, "Skip git history backfill for deletions (use during JSONL filename migrations)") + importCmd.Flags().Bool("ignore-deletions", false, "Import issues even if they're in the deletions manifest") importCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output import statistics in JSON format") rootCmd.AddCommand(importCmd) } diff --git a/cmd/bd/import_shared.go b/cmd/bd/import_shared.go index 35a113d1..63c6e1d3 100644 --- a/cmd/bd/import_shared.go +++ b/cmd/bd/import_shared.go @@ -166,6 +166,7 @@ type ImportOptions struct { ClearDuplicateExternalRefs bool // Clear duplicate external_ref values instead of erroring OrphanHandling string // Orphan handling mode: strict/resurrect/skip/allow (empty = use config) NoGitHistory bool // Skip git history backfill for deletions (prevents spurious deletion during JSONL migrations) + IgnoreDeletions bool // Import issues even if they're in the deletions manifest } // ImportResult contains statistics about the import operation @@ -183,6 +184,8 @@ type ImportResult struct { SkippedDependencies []string // Dependencies skipped due to FK constraint violations Purged int // Issues purged from DB (found in deletions manifest) PurgedIDs []string // IDs that were purged + SkippedDeleted int // Issues skipped because they're in deletions manifest + SkippedDeletedIDs []string // IDs that were skipped due to deletions manifest } // importIssuesCore handles the core import logic used by both manual and auto-import. @@ -223,6 +226,7 @@ func importIssuesCore(ctx context.Context, dbPath string, store storage.Storage, ClearDuplicateExternalRefs: opts.ClearDuplicateExternalRefs, OrphanHandling: importer.OrphanHandling(orphanHandling), NoGitHistory: opts.NoGitHistory, + IgnoreDeletions: opts.IgnoreDeletions, } // Delegate to the importer package @@ -246,6 +250,8 @@ func importIssuesCore(ctx context.Context, dbPath string, store storage.Storage, SkippedDependencies: result.SkippedDependencies, Purged: result.Purged, PurgedIDs: result.PurgedIDs, + SkippedDeleted: result.SkippedDeleted, + SkippedDeletedIDs: result.SkippedDeletedIDs, }, nil } diff --git a/internal/importer/importer.go b/internal/importer/importer.go index cd6bb500..c40c50dd 100644 --- a/internal/importer/importer.go +++ b/internal/importer/importer.go @@ -43,6 +43,7 @@ type Options struct { OrphanHandling OrphanHandling // How to handle missing parent issues (default: allow) ClearDuplicateExternalRefs bool // Clear duplicate external_ref values instead of erroring NoGitHistory bool // Skip git history backfill for deletions (prevents spurious deletion during JSONL migrations) + IgnoreDeletions bool // Import issues even if they're in the deletions manifest } // Result contains statistics about the import operation @@ -60,6 +61,8 @@ type Result struct { SkippedDependencies []string // Dependencies skipped due to FK constraint violations Purged int // Issues purged from DB (found in deletions manifest) PurgedIDs []string // IDs that were purged + SkippedDeleted int // Issues skipped because they're in deletions manifest + SkippedDeletedIDs []string // IDs that were skipped due to deletions manifest } // ImportIssues handles the core import logic used by both manual and auto-import. @@ -114,6 +117,29 @@ func ImportIssues(ctx context.Context, dbPath string, store storage.Storage, iss opts.OrphanHandling = sqliteStore.GetOrphanHandling(ctx) } + // Filter out issues that are in the deletions manifest (bd-4zy) + // Unless IgnoreDeletions is set, skip importing deleted issues + if !opts.IgnoreDeletions && dbPath != "" { + beadsDir := filepath.Dir(dbPath) + deletionsPath := deletions.DefaultPath(beadsDir) + loadResult, err := deletions.LoadDeletions(deletionsPath) + if err == nil && len(loadResult.Records) > 0 { + var filteredIssues []*types.Issue + for _, issue := range issues { + if del, found := loadResult.Records[issue.ID]; found { + // Issue is in deletions manifest - skip it + result.SkippedDeleted++ + result.SkippedDeletedIDs = append(result.SkippedDeletedIDs, issue.ID) + fmt.Fprintf(os.Stderr, "Skipping %s (in deletions manifest: deleted %s by %s)\n", + issue.ID, del.Timestamp.Format("2006-01-02"), del.Actor) + } else { + filteredIssues = append(filteredIssues, issue) + } + } + issues = filteredIssues + } + } + // Check and handle prefix mismatches if err := handlePrefixMismatch(ctx, sqliteStore, issues, opts, result); err != nil { return result, err