Implements Phase 1 of bd-56 (Repair Commands & AI-Assisted Tooling): New commands: - bd repair-deps: Find and fix orphaned dependency references - bd detect-pollution: Detect test issues using pattern matching - bd validate: Comprehensive health check (orphans, duplicates, pollution) Features: - JSON output support for all commands - Safe deletion with backup for detect-pollution - Auto-fix support for orphaned dependencies - Direct storage access (requires BEADS_NO_DAEMON=1) Closes bd-56 (Phase 1 complete) Related: bd-103, bd-105, bd-106 Amp-Thread-ID: https://ampcode.com/threads/T-5822c6d2-d645-4043-9a8d-3c51ac93bbb7 Co-authored-by: Amp <amp@ampcode.com>
163 lines
4.3 KiB
Go
163 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
var repairDepsCmd = &cobra.Command{
|
|
Use: "repair-deps",
|
|
Short: "Find and fix orphaned dependency references",
|
|
Long: `Find issues that reference non-existent dependencies and optionally remove them.
|
|
|
|
This command scans all issues for dependency references (both blocks and related-to)
|
|
that point to issues that no longer exist in the database.
|
|
|
|
Example:
|
|
bd repair-deps # Show orphaned dependencies
|
|
bd repair-deps --fix # Remove orphaned references
|
|
bd repair-deps --json # Output in JSON format`,
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
// Check daemon mode - not supported yet (uses direct storage access)
|
|
if daemonClient != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: repair-deps command not yet supported in daemon mode\n")
|
|
fmt.Fprintf(os.Stderr, "Use: bd --no-daemon repair-deps\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
fix, _ := cmd.Flags().GetBool("fix")
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get all issues
|
|
allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error fetching issues: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Build ID existence map
|
|
existingIDs := make(map[string]bool)
|
|
for _, issue := range allIssues {
|
|
existingIDs[issue.ID] = true
|
|
}
|
|
|
|
// Find orphaned dependencies
|
|
type orphanedDep struct {
|
|
IssueID string
|
|
OrphanedID string
|
|
DepType string
|
|
}
|
|
|
|
var orphaned []orphanedDep
|
|
|
|
for _, issue := range allIssues {
|
|
// Check dependencies
|
|
for _, dep := range issue.Dependencies {
|
|
if !existingIDs[dep.DependsOnID] {
|
|
orphaned = append(orphaned, orphanedDep{
|
|
IssueID: issue.ID,
|
|
OrphanedID: dep.DependsOnID,
|
|
DepType: string(dep.Type),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Output results
|
|
if jsonOutput {
|
|
result := map[string]interface{}{
|
|
"orphaned_count": len(orphaned),
|
|
"fixed": fix,
|
|
"orphaned_deps": []map[string]interface{}{},
|
|
}
|
|
|
|
for _, o := range orphaned {
|
|
result["orphaned_deps"] = append(result["orphaned_deps"].([]map[string]interface{}), map[string]interface{}{
|
|
"issue_id": o.IssueID,
|
|
"orphaned_id": o.OrphanedID,
|
|
"dep_type": o.DepType,
|
|
})
|
|
}
|
|
|
|
outputJSON(result)
|
|
return
|
|
}
|
|
|
|
// Human-readable output
|
|
if len(orphaned) == 0 {
|
|
fmt.Println("No orphaned dependencies found!")
|
|
return
|
|
}
|
|
|
|
fmt.Printf("Found %d orphaned dependencies:\n\n", len(orphaned))
|
|
for _, o := range orphaned {
|
|
fmt.Printf(" %s: depends on %s (%s) - DELETED\n", o.IssueID, o.OrphanedID, o.DepType)
|
|
}
|
|
|
|
if !fix {
|
|
fmt.Printf("\nRun 'bd repair-deps --fix' to remove these references.\n")
|
|
return
|
|
}
|
|
|
|
// Fix orphaned dependencies
|
|
fmt.Printf("\nRemoving orphaned dependencies...\n")
|
|
|
|
// Group by issue for efficient updates
|
|
orphansByIssue := make(map[string][]string)
|
|
for _, o := range orphaned {
|
|
orphansByIssue[o.IssueID] = append(orphansByIssue[o.IssueID], o.OrphanedID)
|
|
}
|
|
|
|
fixed := 0
|
|
for issueID, orphanedIDs := range orphansByIssue {
|
|
// Get current issue to verify
|
|
issue, err := store.GetIssue(ctx, issueID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", issueID, err)
|
|
continue
|
|
}
|
|
|
|
// Collect orphaned dependency IDs to remove
|
|
orphanedSet := make(map[string]bool)
|
|
for _, orphanedID := range orphanedIDs {
|
|
orphanedSet[orphanedID] = true
|
|
}
|
|
|
|
// Build list of dependencies to keep
|
|
validDeps := []*types.Dependency{}
|
|
for _, dep := range issue.Dependencies {
|
|
if !orphanedSet[dep.DependsOnID] {
|
|
validDeps = append(validDeps, dep)
|
|
}
|
|
}
|
|
|
|
// Update via storage layer
|
|
// We need to remove each orphaned dependency individually
|
|
for _, orphanedID := range orphanedIDs {
|
|
if err := store.RemoveDependency(ctx, issueID, orphanedID, actor); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error removing %s from %s: %v\n", orphanedID, issueID, err)
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("✓ Removed %s from %s dependencies\n", orphanedID, issueID)
|
|
fixed++
|
|
}
|
|
}
|
|
|
|
// Schedule auto-flush
|
|
markDirtyAndScheduleFlush()
|
|
|
|
fmt.Printf("\nRepaired %d orphaned dependencies.\n", fixed)
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
repairDepsCmd.Flags().Bool("fix", false, "Remove orphaned dependency references")
|
|
rootCmd.AddCommand(repairDepsCmd)
|
|
}
|