gastownhall/beads · error · storage.ErrValidation
%w: count edges status %q needs direction %q: an outbound ed
Error message
%w: count edges status %q needs direction %q: an outbound edge's far end may be a row this database does not hold
What it means
ValidateEdgeCountRequest rejects an EdgeCountRequest that sets a Status filter with a Direction other than the inbound direction ("in"). Status narrowing is only defined for inbound counts because an outbound edge's far end (depends_on_id) may point at an issue this database does not hold, so the join to the issues/wisps status tables cannot be answered reliably. The error wraps storage.ErrValidation, so callers can detect it with errors.Is.
Source
Thrown at internal/storage/issueops/edge_counts.go:46
//
// THE ORDER IS PART OF THE CONTRACT. The direction is checked FIRST, so an
// empty request is a refusal about the direction rather than an empty answer:
// EdgeCountRequest{} names no anchors, and answering it with no anchors would
// let a caller that forgot the direction get a plausible response forever. The
// per-entry checks that follow tell a caller's mistake from a legitimately
// empty answer, exactly as ValidateEdgeReadRequest's do.
func ValidateEdgeCountRequest(request publicops.EdgeCountRequest) error {
switch request.Direction {
case publicops.EdgeDirectionIn, publicops.EdgeDirectionOut:
case "":
return fmt.Errorf("%w: count edges requires a direction (%q or %q)",
storage.ErrValidation, publicops.EdgeDirectionOut, publicops.EdgeDirectionIn)
default:
return fmt.Errorf("%w: count edges direction %q is not %q or %q",
storage.ErrValidation, request.Direction, publicops.EdgeDirectionOut, publicops.EdgeDirectionIn)
}
if request.Status != "" && request.Direction != publicops.EdgeDirectionIn {
return fmt.Errorf("%w: count edges status %q needs direction %q: an outbound edge's far end may be a row this database does not hold",
storage.ErrValidation, request.Status, publicops.EdgeDirectionIn)
}
for i, id := range request.IDs {
if id == "" {
return fmt.Errorf("%w: count edges id %d is empty", storage.ErrValidation, i)
}
}
for i, depType := range request.Types {
if !depType.IsValid() {
return fmt.Errorf("%w: count edges type %d is not a usable dependency type (non-empty, max %d chars)",
storage.ErrValidation, i, types.MaxDependencyTypeLen)
}
}
return nil
}
// FinishEdgeCount assembles the per-anchor answer from the two things every
// implementation reads: which anchors exist, and the edge tallies keyed byView on GitHub (pinned to 71377f2769)
Solutions
- Change the request Direction to publicops.EdgeDirectionIn ("in") when filtering by status.
- Drop the Status field (leave it empty) if an outbound count is what you actually want.
- Split the query: do an outbound count without status, then filter results in application code after reading the far-end issues.
- If the status filter was set programmatically, guard the builder so it only emits Status when Direction == EdgeDirectionIn.
Example fix
// before
req := publicops.EdgeCountRequest{IDs: []string{"bd-1"}, Direction: "out", Status: "open"}
// after
req := publicops.EdgeCountRequest{IDs: []string{"bd-1"}, Direction: "in", Status: "open"} Defensive patterns
Strategy: validation
Validate before calling
func validEdgeCountRequest(req publicops.EdgeCountRequest) bool {
if req.Status == "" { return true }
return req.Direction == publicops.EdgeDirectionIn
} Try / catch
if err := ValidateEdgeCountRequest(req); err != nil {
if errors.Is(err, storage.ErrValidation) { /* fix request, don't retry */ }
} Prevention
- Only set Status on inbound ("in") edge counts
- Centralize request construction so Direction/Status pairing is enforced in one place
- Run ValidateEdgeCountRequest yourself before issuing the call
- Add a unit test mirroring TestValidateEdgeCountRequestChecksTheDirectionBeforeAnythingElse
When it happens
Trigger: Calling CountEdges (via ExecuteEdgeCount) with an EdgeCountRequest{Status: "open", Direction: "out", IDs: [...]}, or with Direction left to any value other than EdgeDirectionIn while Status is non-empty.
Common situations: A CLI or API caller who wants 'open blockers of X' reuses the same filter struct for both directions and sends Direction "out" by mistake; code copied from an outbound-count example that then had a status filter added; version drift where a new Status field is populated by a shared request builder.
Related errors
- remove dep: sourceID and dependsOnID must not be empty
- %w: %s cannot depend on itself
- %w: count edges id %d is empty
- %w: count edges type %d is not a usable dependency type (non
- no store is open for this workspace
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/82335f1120e28939.
Report an issue: GitHub.