feat(deps): detect/prevent child→parent dependency anti-pattern (bd-nim5)

This prevents a common mistake where users add dependencies from child
issues to their parent epics. This creates a deadlock:
- Child can't start (blocked by open parent)
- Parent can't close (children not done)

Changes:
- dep.go: Reject child→parent deps at creation time with clear error
- server_labels_deps_comments.go: Same check for daemon RPC
- doctor/validation.go: New check detects existing bad deps
- doctor/fix/validation.go: Auto-fix removes bad deps
- doctor.go: Wire up check and fix handler

🤖 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-24 13:03:13 -08:00
parent 566e4f5225
commit ce2b05356a
7 changed files with 379 additions and 0 deletions

View File

@@ -956,3 +956,86 @@ func TestMergeBidirectionalTrees_PreservesDepth(t *testing.T) {
}
}
}
// Tests for child→parent dependency detection (bd-nim5)
func TestIsChildOf(t *testing.T) {
tests := []struct {
name string
childID string
parentID string
want bool
}{
// Positive cases: should be detected as child
{
name: "direct child",
childID: "bd-abc.1",
parentID: "bd-abc",
want: true,
},
{
name: "grandchild",
childID: "bd-abc.1.2",
parentID: "bd-abc",
want: true,
},
{
name: "nested grandchild direct parent",
childID: "bd-abc.1.2",
parentID: "bd-abc.1",
want: true,
},
{
name: "deeply nested child",
childID: "bd-abc.1.2.3",
parentID: "bd-abc",
want: true,
},
// Negative cases: should NOT be detected as child
{
name: "same ID",
childID: "bd-abc",
parentID: "bd-abc",
want: false,
},
{
name: "not a child - unrelated IDs",
childID: "bd-xyz",
parentID: "bd-abc",
want: false,
},
{
name: "not a child - sibling",
childID: "bd-abc.2",
parentID: "bd-abc.1",
want: false,
},
{
name: "reversed - parent is not child of child",
childID: "bd-abc",
parentID: "bd-abc.1",
want: false,
},
{
name: "prefix but not hierarchical",
childID: "bd-abcd",
parentID: "bd-abc",
want: false,
},
{
name: "not hierarchical ID",
childID: "bd-abc",
parentID: "bd-xyz",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isChildOf(tt.childID, tt.parentID)
if got != tt.want {
t.Errorf("isChildOf(%q, %q) = %v, want %v", tt.childID, tt.parentID, got, tt.want)
}
})
}
}