Implement cycle detection and prevention improvements

- Add diagnostic warnings when cycles detected after dep add (bd-309)
- Add semantic validation for parent-child dependency direction (bd-308)
- Document cycle handling behavior in code, README, and DESIGN (bd-310)

Changes:
- cmd/bd/dep.go: Add DetectCycles() call and warning after dep add
- internal/storage/sqlite/dependencies.go: Add parent-child direction validation and comprehensive cycle prevention comments
- internal/storage/sqlite/dependencies_test.go: Add TestParentChildValidation
- README.md: Add dependency types and cycle prevention section with examples
- DESIGN.md: Add detailed cycle prevention design rationale and trade-offs
This commit is contained in:
Steve Yegge
2025-10-16 13:17:32 -07:00
parent 6753024eb0
commit 1e32041fe6
6 changed files with 531 additions and 385 deletions

View File

@@ -366,12 +366,56 @@ type Dependency struct {
type DependencyType string
const (
DepBlocks DependencyType = "blocks" // hard blocker
DepRelated DependencyType = "related" // soft relationship
DepParentChild DependencyType = "parent-child" // epic/subtask
DepBlocks DependencyType = "blocks" // hard blocker
DepRelated DependencyType = "related" // soft relationship
DepParentChild DependencyType = "parent-child" // epic/subtask
DepDiscoveredFrom DependencyType = "discovered-from" // discovered during work
)
```
#### Cycle Prevention Design
**Goal**: Maintain a directed acyclic graph (DAG) across all dependency types.
**Rationale**: Cycles create three major problems:
1. **Ready Work Calculation**: Issues in a cycle appear blocked by each other, hiding them from `bd ready` even though there's no clear blocking reason. Example: A depends on B, B depends on A → neither appears as ready work.
2. **Semantic Confusion**: Circular dependencies are conceptually problematic. If A depends on B and B depends on A (directly or through other issues), which should be done first? The answer is ambiguous.
3. **Traversal Complexity**: Operations like `bd dep tree`, blocking propagation, and hierarchy display rely on DAG structure. Cycles require special handling (cycle detection, path marking) or risk infinite loops.
**Implementation Strategy**:
- **Prevention over Detection**: We prevent cycles at insertion time in `AddDependency`, so cycles can never exist in the database.
- **Cross-Type Checking**: We check ALL dependency types, not just `blocks`. Cross-type cycles (e.g., A blocks B, B parent-child A) are just as problematic as single-type cycles.
- **Recursive CTE**: SQLite recursive common table expression traverses from `DependsOnID` to check if `IssueID` is reachable. If yes, adding the edge would complete a cycle.
- **Depth Limit**: Traversal limited to 100 levels to prevent excessive query cost and handle edge cases.
- **Transaction Safety**: Cycle check happens in transaction before INSERT, so no partial state on rejection.
**Performance Considerations**:
- Cycle check runs on every `AddDependency` call (not skippable)
- Indexed on `dependencies.issue_id` for efficient traversal
- Cost grows with dependency graph depth, not total issue count
- Typical case (small trees): <10ms overhead
- Pathological case (deep chains): O(depth × branching) but limited by depth=100
**User Experience**:
- Clear error messages: "cannot add dependency: would create a cycle (bd-3 → bd-1 → bd-2 → bd-3)"
- After successful addition, we run `DetectCycles()` and warn if any cycles exist elsewhere
- `bd dep cycles` command for manual cycle detection and diagnosis
**Trade-offs**:
- ✅ Prevents semantic confusion and broken ready work calculation
- ✅ Keeps code simple (no cycle handling in traversals)
- ⚠️ Small performance overhead on every dependency addition
- ⚠️ Cannot represent certain real-world patterns (mutual blockers must be modeled differently)
**Alternative Considered**: Allow cycles and handle during traversal with cycle detection and path tracking. Rejected because it adds complexity everywhere dependencies are used and doesn't solve the semantic ambiguity problem.
### Labels
```go