gastownhall/beads · error

ErrSelfDependency

ErrSelfDependency

Error message

%w: %s

What it means

A dependency-add batch item tried to make an issue depend on itself, which is forbidden. The error is wrapped in publicops.ErrSelfDependency and again in an ItemError carrying the batch index and source key so callers can attribute the failure to one item.

Source

Thrown at internal/storage/uow/batch_applier.go:385

// WHETHER THE EDGE WAS NEW IS READ BEFORE THE WRITE, because this backend's
// insert does not report it: BulkAddDepsResult echoes the request. A pre-read
// per edge is bounded by the request's hundred-item cap and is the only way
// ItemResult.Changed can mean the same thing on all three legs — an idempotent
// re-add of the same pair with the same type changed nothing.
func (r *uowApplyRun) applyDepAdd(ctx context.Context, index int, item *publicops.DepAddItem) error {
	source, err := r.resolve(item.Source, index, "source")
	if err != nil {
		return err
	}
	target, err := r.resolve(item.Target, index, "target")
	if err != nil {
		return err
	}
	itemErr := func(err error) error {
		return &publicops.ItemError{Index: index, Kind: publicops.ItemDepAdd, Key: item.Source.Key, IssueID: source, Err: err}
	}
	if source == target {
		return itemErr(fmt.Errorf("%w: %s", publicops.ErrSelfDependency, source))
	}
	sourceWisp, sourceMine := r.planes[source]
	targetWisp, targetMine := r.planes[target]
	if sourceMine && targetMine && sourceWisp != targetWisp {
		return itemErr(storageissueops.CrossPlaneBatchEdgeError(source, target))
	}
	existed, err := r.edgeExists(ctx, source, target, item.Type)
	if err != nil {
		return itemErr(err)
	}
	dep := &types.Dependency{
		IssueID:     source,
		DependsOnID: target,
		Type:        item.Type,
		Metadata:    item.Metadata,
	}
	if _, err := r.uw.DependencyUseCase().AddDependencies(ctx, []*types.Dependency{dep}, r.plan.Actor, domain.BulkAddDepsOpts{
		SkipPerEdgeCycleCheck: r.plan.SkipPerEdgeCycleCheck,

View on GitHub (pinned to 71377f2769)

Solutions

  1. Fix the batch/CLI input so source and target differ
  2. Pre-filter items where Source.Key == DependsOn.Key before submitting the batch
  3. Assert source != target where the dependency list is generated programmatically

Example fix

// before
if source == target { return itemErr(...) } // rejected at apply time
// after (caller-side)
for _, it := range items {
    if it.DepAdd.Source.Key == it.DepAdd.DependsOn.Key { continue } // skip self-dep
}
Defensive patterns

Strategy: validation

Validate before calling

for i, it := range items {
    if it.DepAdd != nil && it.DepAdd.Source.Key == it.DepAdd.DependsOn.Key {
        return fmt.Errorf("item %d: self-dependency on %s", i, it.DepAdd.Source.Key)
    }
}

Try / catch

err := applyBatch(ctx, batch)
var itemErr *publicops.ItemError
if errors.As(err, &itemErr) && errors.Is(itemErr.Err, publicops.ErrSelfDependency) {
    log.Printf("batch item %d (%s): self-dependency", itemErr.Index, itemErr.Key)
}

Prevention

When it happens

Trigger: applyDepAdd receives a DepAdd item where item.Source.Key resolves to the same issue id as the target (source == target). E.g. 'bd dep add bd-1 bd-1' or a programmatic batch with identical source and target keys.

Common situations: Template-generated batch scripts where a variable accidentally equals the source id; loops that include the seed issue in its own dependency list; copy-paste of the same issue id into both fields.

Related errors


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