{"record":{"id":"82335f1120e28939","repo":"gastownhall/beads","slug":"w-count-edges-status-q-needs-direction-q-an-o","errorCode":null,"errorMessage":"%w: count edges status %q needs direction %q: an outbound edge's far end may be a row this database does not hold","messagePattern":"%w: count edges status %q needs direction %q: an outbound edge's far end may be a row this database does not hold","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/edge_counts.go","lineNumber":46,"sourceCode":"//\n// THE ORDER IS PART OF THE CONTRACT. The direction is checked FIRST, so an\n// empty request is a refusal about the direction rather than an empty answer:\n// EdgeCountRequest{} names no anchors, and answering it with no anchors would\n// let a caller that forgot the direction get a plausible response forever. The\n// per-entry checks that follow tell a caller's mistake from a legitimately\n// empty answer, exactly as ValidateEdgeReadRequest's do.\nfunc ValidateEdgeCountRequest(request publicops.EdgeCountRequest) error {\n\tswitch request.Direction {\n\tcase publicops.EdgeDirectionIn, publicops.EdgeDirectionOut:\n\tcase \"\":\n\t\treturn fmt.Errorf(\"%w: count edges requires a direction (%q or %q)\",\n\t\t\tstorage.ErrValidation, publicops.EdgeDirectionOut, publicops.EdgeDirectionIn)\n\tdefault:\n\t\treturn fmt.Errorf(\"%w: count edges direction %q is not %q or %q\",\n\t\t\tstorage.ErrValidation, request.Direction, publicops.EdgeDirectionOut, publicops.EdgeDirectionIn)\n\t}\n\tif request.Status != \"\" && request.Direction != publicops.EdgeDirectionIn {\n\t\treturn 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\",\n\t\t\tstorage.ErrValidation, request.Status, publicops.EdgeDirectionIn)\n\t}\n\tfor i, id := range request.IDs {\n\t\tif id == \"\" {\n\t\t\treturn fmt.Errorf(\"%w: count edges id %d is empty\", storage.ErrValidation, i)\n\t\t}\n\t}\n\tfor i, depType := range request.Types {\n\t\tif !depType.IsValid() {\n\t\t\treturn fmt.Errorf(\"%w: count edges type %d is not a usable dependency type (non-empty, max %d chars)\",\n\t\t\t\tstorage.ErrValidation, i, types.MaxDependencyTypeLen)\n\t\t}\n\t}\n\treturn nil\n}\n\n// FinishEdgeCount assembles the per-anchor answer from the two things every\n// implementation reads: which anchors exist, and the edge tallies keyed by","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/edge_counts.go#L28-L64","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nreq := publicops.EdgeCountRequest{IDs: []string{\"bd-1\"}, Direction: \"out\", Status: \"open\"}\n// after\nreq := publicops.EdgeCountRequest{IDs: []string{\"bd-1\"}, Direction: \"in\", Status: \"open\"}","handlingStrategy":"validation","validationCode":"func validEdgeCountRequest(req publicops.EdgeCountRequest) bool {\n\tif req.Status == \"\" { return true }\n\treturn req.Direction == publicops.EdgeDirectionIn\n}","typeGuard":null,"tryCatchPattern":"if err := ValidateEdgeCountRequest(req); err != nil {\n\tif errors.Is(err, storage.ErrValidation) { /* fix request, don't retry */ }\n}","preventionTips":["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"],"tags":["validation","dependency-graph","edge-count","request-direction"],"backgroundTag":"invalid-request-validation","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}