Add circular dependency detection to bd doctor
- Added checkDependencyCycles() function using recursive CTE - Integrated as Check #10 in runDiagnostics() - Reports error status with count and fix suggestion if cycles found - Updated doctor command documentation - Fixes bd-3e3b
This commit is contained in:
102
cmd/bd/doctor.go
102
cmd/bd/doctor.go
@@ -56,6 +56,7 @@ This command checks:
|
|||||||
- Daemon health (version mismatches, stale processes)
|
- Daemon health (version mismatches, stale processes)
|
||||||
- Database-JSONL sync status
|
- Database-JSONL sync status
|
||||||
- File permissions
|
- File permissions
|
||||||
|
- Circular dependencies
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bd doctor # Check current directory
|
bd doctor # Check current directory
|
||||||
@@ -165,6 +166,13 @@ func runDiagnostics(path string) doctorResult {
|
|||||||
result.OverallOK = false
|
result.OverallOK = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check 10: Dependency cycles
|
||||||
|
cycleCheck := checkDependencyCycles(path)
|
||||||
|
result.Checks = append(result.Checks, cycleCheck)
|
||||||
|
if cycleCheck.Status == statusError || cycleCheck.Status == statusWarning {
|
||||||
|
result.OverallOK = false
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,6 +863,100 @@ func checkPermissions(path string) doctorCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkDependencyCycles(path string) doctorCheck {
|
||||||
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||||
|
|
||||||
|
// If no database, skip this check
|
||||||
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Dependency Cycles",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: "N/A (no database)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database to check for cycles
|
||||||
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Dependency Cycles",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: "Unable to open database",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Query for cycles using simplified SQL
|
||||||
|
query := `
|
||||||
|
WITH RECURSIVE paths AS (
|
||||||
|
SELECT
|
||||||
|
issue_id,
|
||||||
|
depends_on_id,
|
||||||
|
issue_id as start_id,
|
||||||
|
issue_id || '→' || depends_on_id as path,
|
||||||
|
0 as depth
|
||||||
|
FROM dependencies
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
d.issue_id,
|
||||||
|
d.depends_on_id,
|
||||||
|
p.start_id,
|
||||||
|
p.path || '→' || d.depends_on_id,
|
||||||
|
p.depth + 1
|
||||||
|
FROM dependencies d
|
||||||
|
JOIN paths p ON d.issue_id = p.depends_on_id
|
||||||
|
WHERE p.depth < 100
|
||||||
|
AND p.path NOT LIKE '%' || d.depends_on_id || '→%'
|
||||||
|
)
|
||||||
|
SELECT DISTINCT start_id
|
||||||
|
FROM paths
|
||||||
|
WHERE depends_on_id = start_id`
|
||||||
|
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Dependency Cycles",
|
||||||
|
Status: statusWarning,
|
||||||
|
Message: "Unable to check for cycles",
|
||||||
|
Detail: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
cycleCount := 0
|
||||||
|
var firstCycle string
|
||||||
|
for rows.Next() {
|
||||||
|
var startID string
|
||||||
|
if err := rows.Scan(&startID); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cycleCount++
|
||||||
|
if cycleCount == 1 {
|
||||||
|
firstCycle = startID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cycleCount == 0 {
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Dependency Cycles",
|
||||||
|
Status: statusOK,
|
||||||
|
Message: "No circular dependencies detected",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doctorCheck{
|
||||||
|
Name: "Dependency Cycles",
|
||||||
|
Status: statusError,
|
||||||
|
Message: fmt.Sprintf("Found %d circular dependency cycle(s)", cycleCount),
|
||||||
|
Detail: fmt.Sprintf("First cycle involves: %s", firstCycle),
|
||||||
|
Fix: "Run 'bd dep cycles' to see full cycle paths, then 'bd dep remove' to break cycles",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
doctorCmd.Flags().Bool("json", false, "Output JSON format")
|
doctorCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
rootCmd.AddCommand(doctorCmd)
|
rootCmd.AddCommand(doctorCmd)
|
||||||
|
|||||||
Reference in New Issue
Block a user