{"record":{"id":"f63b0a857ccca52c","repo":"gastownhall/beads","slug":"a-ready-count-does-not-take-a-limit","errorCode":null,"errorMessage":"a ready count does not take a limit","messagePattern":"a ready count does not take a limit","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/ready.go","lineNumber":118,"sourceCode":"\t\treturn filter, fmt.Errorf(\"invalid sort policy '%s'. Valid values: hybrid, priority, oldest%.0w\", in.Sort, issueops.ErrValidation)\n\t}\n\treturn filter, nil\n}\n\n// BuildReadyCountFilter turns a ready request into the storage-level filter a\n// COUNT of the ready set runs against: BuildReadyFilter's filter with the page\n// removed, and the single definition of what `bd ready`'s published total means.\n//\n// It refuses a request carrying a page. issueops.ReadyCounter.CountReady\n// promises its answer equals len(Reader.Ready(r with Limit=0).Items), and a\n// Limit would make that \"how many of the first N\" while an Offset would\n// subtract the rows it skipped from the size of a set that still holds them.\n//\n// The zeroed limit is set on a LOCAL copy: a nil Limit means the shared ready\n// default at BuildReadyFilter, so an unlimited count has to say so explicitly.\nfunc BuildReadyCountFilter(in issueops.ReadyRequest) (types.WorkFilter, error) {\n\tif in.Limit != nil {\n\t\treturn types.WorkFilter{}, fmt.Errorf(\"%w: a ready count does not take a limit\", issueops.ErrValidation)\n\t}\n\tif in.Offset != 0 {\n\t\treturn types.WorkFilter{}, fmt.Errorf(\"%w: a ready count does not take an offset\", issueops.ErrValidation)\n\t}\n\tunlimited := 0\n\tcounted := in\n\tcounted.Limit = &unlimited\n\t// Brief is CARRIED, not refused and not cleared, which is the opposite of\n\t// what ClaimNext does with it. A count reads no field of any row, so the\n\t// projection cannot make the number wrong; and the count is not always\n\t// cheap enough for that to be the end of it. The unit-of-work seam has no\n\t// COUNT(*) over the ready predicate and sizes the set by running the\n\t// unbounded page and taking its length (uow/ready_counter.go), so clearing\n\t// the field here would hydrate every heavy column of the whole ready set to\n\t// answer `bd ready --brief`, which is the cost the projection exists to\n\t// avoid and larger than the page it was asked for. Carrying it also keeps\n\t// the count filter what this builder says it is: the listing's filter with\n\t// the PAGE removed, and nothing else removed.","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/ready.go#L100-L136","documentation":"BuildReadyCountFilter refuses a ReadyRequest that carries a Limit, because a COUNT of the ready set must be unbounded — any limit would make the count answer 'how many up to N' rather than the set size. The refusal wraps issueops.ErrValidation with the message as a %w suffix.","triggerScenarios":"Calling BuildReadyCountFilter with in.Limit != nil, e.g. a client reusing the same ReadyRequest it built for a paged `bd ready` call for the count endpoint.","commonSituations":"Shared request builders feeding both the list and count paths; an HTTP handler binding the page limit from the query string into the count request; copying a ReadyRequest struct wholesale without clearing Limit.","solutions":["Nil out Limit on the request before calling BuildReadyCountFilter","Build a separate ReadyRequest for counting with no Limit set","At the handler layer, ignore/reject limit params for the count route"],"exampleFix":"// before\nreq.Limit = &pageLimit\ncountFilter, err := BuildReadyCountFilter(req)\n// after\nreq.Limit = nil\ncountFilter, err := BuildReadyCountFilter(req)","handlingStrategy":"validation","validationCode":"if req.Limit != nil {\n\treturn errors.New(\"count requests must not set a limit\")\n}","typeGuard":null,"tryCatchPattern":"filter, err := BuildReadyCountFilter(req)\nif err != nil && errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), \"does not take a limit\") {\n\t// clear Limit and rebuild\n}","preventionTips":["Build count requests separately from paged listing requests","Clear Limit (set nil) when reusing a ReadyRequest for counting","Reject limit query params on count endpoints at the handler layer"],"tags":["validation","ready-queue","count","pagination"],"backgroundTag":"pagination-parameter-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}