gastownhall/beads · error

dependency %s -> %s is a self-dependency

Error message

dependency %s -> %s is a self-dependency

What it means

After loading all issues, the reader validates the dependency graph and rejects any dependency whose issue and depends-on IDs are the same. Self-dependencies are meaningless in bd's graph and would poison cycle/hierarchy checks, so migration aborts with the offending pair.

Source

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

		}
		identity := commentIdentity{issueID: issueID, author: author, text: text, createdAt: created}
		if priorID, exists := seenComments[identity]; exists {
			return fmt.Errorf("legacy SQLite comments %d and %d share current import identity", priorID, id)
		}
		seenComments[identity] = id
		issue.Comments = append(issue.Comments, &types.Comment{ID: strconv.FormatInt(id, 10), IssueID: issueID, Author: author, Text: text, CreatedAt: created})
	}
	return comments.Err()
}

func validateDependencyGraph(issues []*types.Issue) error {
	scheduling := make(map[string][]string)
	hierarchy := make(map[string][]string)
	var blocking []*types.Dependency
	for _, issue := range issues {
		for _, dep := range issue.Dependencies {
			if dep.IssueID == dep.DependsOnID {
				return fmt.Errorf("dependency %s -> %s is a self-dependency", dep.IssueID, dep.DependsOnID)
			}
			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)) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Delete self-dependency rows in the legacy DB: DELETE FROM dependencies WHERE issue_id = depends_on_id
  2. Audit for other bad rows at the same time: SELECT * FROM dependencies WHERE issue_id = depends_on_id
  3. Re-run migration; recreate any intended link correctly (a self link has no valid replacement)

Example fix

-- before
-- row: bd-1 -> bd-1
DELETE FROM dependencies WHERE issue_id = depends_on_id;
-- after: no self-dependencies remain
Defensive patterns

Strategy: validation

Validate before calling

for (const d of legacyDeps) {
  if (d.issue_id === d.depends_on_id)
    throw new Error(`self-dependency ${d.issue_id} -> ${d.depends_on_id}`);
}

Type guard

function hasNoSelfDeps(deps) {
  return deps.every(d => d.issue_id !== d.depends_on_id);
}

Try / catch

try { migrateLegacySQLite(dbPath) } catch (e) {
  if (e.message.includes('is a self-dependency')) {
    deleteSelfDependencies(dbPath); // WHERE issue_id = depends_on_id
    retry();
  } else throw e;
}

Prevention

When it happens

Trigger: Legacy SQLite dependency row where issue_id equals depends_on_id (id -> id); detected during the per-issue sweep over issue.Dependencies.

Common situations: Buggy legacy tooling that let users or scripts depend an issue on itself; manual SQL inserts; restore bugs that rewrote one side of a dependency to the same ID.

Related errors


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