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
- Skip the call entirely when there are no edges (no-op is fine) instead of invoking AddDependencies.
- Check that edge collection upstream actually produced edges (log the count before the call).
- 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
- Guard call sites with a len(Edges) > 0 check.
- Log edge counts in batch pipelines so silent emptying is visible.
- Only call AddDependencies when there is actual work.
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
- no store is open for this workspace
- no absolute native user directory is available
- ExternalDoltConfig: set either Socket OR (Host, Port), not b
- ExternalDoltConfig: must set Socket or (Host, Port)
- ExternalDoltConfig: Host requires Port
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/ae6f4a25960f3c25.
Report an issue: GitHub.