gastownhall/beads · error

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

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.

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)

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.