gastownhall/beads · warning · storage.ErrValidation

%w: add dependencies requires at least one edge

Error message

%w: add dependencies requires at least one edge

What it means

ValidateAddDependenciesRequest rejects requests whose Edges slice is empty, since a no-op dependency write would silently succeed otherwise. The guard is wrapped with storage.ErrValidation for errors.Is() matching. It's raised before touching the database.

Source

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

// DependencyEditor implementation shares. It lives here rather than in each of
// them because a rule enforced on one backend and not the other is not a
// contract.
//
// The self-dependency refusal is here, ahead of the per-edge cycle probe and
// for EVERY edge type, for the reason the domain path states: a blocking
// self-edge otherwise trips the cycle check and reports the wrong refusal, and
// SkipPerEdgeCycleCheck would skip it entirely.
//
// The type check is that there IS a type — non-empty, within the column's
// length. It is deliberately not a membership test: the vocabulary is an open,
// 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.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Skip the call entirely when there are no edges (no-op is fine) instead of invoking AddDependencies.
  2. Check that edge collection upstream actually produced edges (log the count before the call).
  3. Match with errors.Is(err, storage.ErrValidation) to distinguish from storage errors.

Example fix

// before
_ = store.AddDependencies(ctx, req) // req.Edges may be empty
// after
if len(req.Edges) == 0 {
    return nil // nothing to do
}
_ = store.AddDependencies(ctx, req)
Defensive patterns

Strategy: validation

Validate before calling

if len(req.Edges) == 0 {
    return nil // no-op: skip the call entirely
}

Type guard

func hasEdges(req publicops.AddDependenciesRequest) bool { return len(req.Edges) > 0 }

Try / catch

err := store.AddDependencies(ctx, req)
if errors.Is(err, storage.ErrValidation) && len(req.Edges) == 0 {
    return nil // treat as intentional no-op
}

Prevention

When it happens

Trigger: Calling AddDependencies with Edges == nil or len(Edges) == 0 — e.g. an upstream batch builder filtered out all edges but the caller still invoked the API.

Common situations: Batch pipelines where a filter/dedupe step emptied the edge list; CLI flag parsing that produced no edges; automation generating zero changes and calling the API unconditionally.

Related errors


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