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 by

View on GitHub (pinned to 71377f2769)

Solutions

  1. Change the request Direction to publicops.EdgeDirectionIn ("in") when filtering by status.
  2. Drop the Status field (leave it empty) if an outbound count is what you actually want.
  3. Split the query: do an outbound count without status, then filter results in application code after reading the far-end issues.
  4. 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

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


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