gastownhall/beads · error

blocking dependency -> conflicts with parent-child hierarchy

Error message

blocking dependency %s -> %s conflicts with parent-child hierarchy

What it means

During legacy SQLite migration, dependencies are validated against the parent-child hierarchy. A blocking dependency between two issues that are also related as parent/child (in either direction) is rejected because it creates contradictory scheduling semantics. The migration aborts rather than importing an inconsistent graph.

Solutions

  1. Open the legacy SQLite DB and delete or re-point the offending blocking dependency so it links non-hierarchical issues (find it with a query on the dependencies table matching the two IDs in the message).
  2. Convert the parent-child relationship to the dependency instead: if the intent was ordering, keep only the blocking dep between siblings and restructure the hierarchy.
  3. Re-run the migration after fixing the dependency rows.

Example fix

// before (legacy DB)
// issue mol-1 parent of mol-2; dependency mol-1 blocks mol-2
DELETE FROM dependencies WHERE issue_id='mol-1' AND depends_on_id='mol-2';
// after: dependency exists only between sibling issues, hierarchy kept intact
Defensive patterns

Strategy: validation

Validate before calling

for _, dep := range blocking {
    if types.ExtractPrefix(dep.IssueID) == types.ExtractPrefix(dep.DependsOnID) &&
        (reachable(hierarchy, dep.IssueID, dep.DependsOnID) ||
            reachable(hierarchy, dep.DependsOnID, dep.IssueID)) {
        return fmt.Errorf("fix dependency %s -> %s before migration", dep.IssueID, dep.DependsOnID)
    }
}

Prevention

When it happens

Trigger: Migrating a legacy SQLite database that contains a blocking dependency (dep.IssueID -> dep.DependsOnID) where both endpoints share the same prefix and one is reachable from the other via parent-child edges (reachable(hierarchy, ...) in either direction).

Common situations: Users manually created blocking deps between a parent and its child (or child and parent) in an old beads database; older versions allowed this but current code forbids it; import data from third-party tooling that doesn't distinguish hierarchy from blocking edges.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/663a0097b266c2db. Report an issue: GitHub.

Appendix: source

Thrown at internal/migration/legacysqlite/reader.go:1013

			}
			switch dep.Type {
			case types.DepBlocks, types.DepConditionalBlocks:
				blocking = append(blocking, dep)
				scheduling[dep.IssueID] = append(scheduling[dep.IssueID], dep.DependsOnID)
			case types.DepParentChild:
				hierarchy[dep.IssueID] = append(hierarchy[dep.IssueID], dep.DependsOnID)
				scheduling[dep.IssueID] = append(scheduling[dep.IssueID], dep.DependsOnID)
			}
		}
	}
	if hasDirectedCycle(scheduling) {
		return fmt.Errorf("legacy SQLite dependency graph has a scheduling cycle")
	}
	for _, dep := range blocking {
		if types.ExtractPrefix(dep.IssueID) == types.ExtractPrefix(dep.DependsOnID) &&
			(reachable(hierarchy, dep.IssueID, dep.DependsOnID) ||
				reachable(hierarchy, dep.DependsOnID, dep.IssueID)) {
			return fmt.Errorf("blocking dependency %s -> %s conflicts with parent-child hierarchy", dep.IssueID, dep.DependsOnID)
		}
	}
	return nil
}

func hasDirectedCycle(graph map[string][]string) bool {
	indegree := make(map[string]int, len(graph))
	for from, targets := range graph {
		if _, ok := indegree[from]; !ok {
			indegree[from] = 0
		}
		for _, target := range targets {
			indegree[target]++
		}
	}
	queue := make([]string, 0, len(indegree))
	for id, degree := range indegree {
		if degree == 0 {

View on GitHub (pinned to 71377f2769)