gastownhall/beads · warning · domain.ErrSelfDependency

%w: %s cannot depend on itself

Error message

%w: %s cannot depend on itself

What it means

Self-dependencies (an issue depending on itself) are rejected because they're meaningless and would pollute the dependency graph. The validator compares IssueID and DependsOnID per edge and fails with domain.ErrSelfDependency (wrapped) plus the offending issue ID. This is distinct from storage.ErrValidation so callers can distinguish graph-rule violations from generic request validation.

Source

Thrown at internal/storage/issueops/dependency_editor.go:44

// workspace-configurable set (see the Dep* constants), so refusing an unlisted
// type would refuse a workspace's own.
func ValidateAddDependenciesRequest(request publicops.AddDependenciesRequest) error {
	if request.Actor == "" {
		return fmt.Errorf("%w: add dependencies requires an actor", storage.ErrValidation)
	}
	if len(request.Edges) == 0 {
		return fmt.Errorf("%w: add dependencies requires at least one edge", storage.ErrValidation)
	}
	for i, edge := range request.Edges {
		if edge.IssueID == "" || edge.DependsOnID == "" {
			return fmt.Errorf("%w: add dependencies edge %d requires both endpoints", storage.ErrValidation, i)
		}
		if !edge.Type.IsValid() {
			return fmt.Errorf("%w: add dependencies edge %d requires a dependency type (max %d chars)",
				storage.ErrValidation, i, types.MaxDependencyTypeLen)
		}
		if edge.IssueID == edge.DependsOnID {
			return fmt.Errorf("%w: %s cannot depend on itself", domain.ErrSelfDependency, edge.IssueID)
		}
	}
	return nil
}

// ValidateRemoveDependencyRequest applies the request rules every
// DependencyEditor implementation shares for a removal.
func ValidateRemoveDependencyRequest(request publicops.RemoveDependencyRequest) error {
	if request.Actor == "" {
		return fmt.Errorf("%w: remove dependency requires an actor", storage.ErrValidation)
	}
	if request.IssueID == "" || request.DependsOnID == "" {
		return fmt.Errorf("%w: remove dependency requires both endpoints", storage.ErrValidation)
	}
	return nil
}

// AddDependenciesCommitMessage is the history entry an edge assertion records.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Filter edges where IssueID == DependsOnID before building the request.
  2. Match errors.Is(err, domain.ErrSelfDependency) to detect this case programmatically.
  3. Fix the upstream edge-generation logic so source issues are excluded from their own dependency lists.
  4. Verify ID normalization (case/whitespace) isn't accidentally making two IDs equal.

Example fix

// before
edges := buildEdges(issueID, mentionedIDs) // may include issueID itself
// after
edges := buildEdges(issueID, mentionedIDs)
edges = slices.DeleteFunc(edges, func(e publicops.DependencyEdge) bool {
    return e.IssueID == e.DependsOnID
})
Defensive patterns

Strategy: validation

Validate before calling

req.Edges = slices.DeleteFunc(req.Edges, func(e publicops.DependencyEdge) bool {
    return e.IssueID == e.DependsOnID
})

Type guard

func isSelfDependency(e publicops.DependencyEdge) bool {
    return e.IssueID == e.DependsOnID
}

Try / catch

err := store.AddDependencies(ctx, req)
if errors.Is(err, domain.ErrSelfDependency) {
    // skip the self-edge; not a data-integrity problem
}

Prevention

When it happens

Trigger: Calling AddDependencies with an edge where IssueID == DependsOnID — e.g. generated edges from data where both endpoints resolved to the same issue, or CLI arguments where the user passed the same ID twice.

Common situations: Bulk imports where a normalized/uppercased ID matched itself; scripts computing edges from text mentions that included the source issue; user typos entering the same bd-xxx twice.

Related errors


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