Merge branch 'main' of github.com:steveyegge/beads
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)
|
||||
- Database-JSONL sync status
|
||||
- File permissions
|
||||
- Circular dependencies
|
||||
|
||||
Examples:
|
||||
bd doctor # Check current directory
|
||||
@@ -165,6 +166,13 @@ func runDiagnostics(path string) doctorResult {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
doctorCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
rootCmd.AddCommand(doctorCmd)
|
||||
|
||||
Reference in New Issue
Block a user