{"record":{"id":"c63fda6da42efd43","repo":"gastownhall/beads","slug":"w-claim-next-does-not-take-a-projection","errorCode":null,"errorMessage":"%w: claim next does not take a projection","messagePattern":"%w: claim next does not take a projection","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/claim_next.go","lineNumber":42,"sourceCode":"func ValidateClaimNextRequest(request publicops.ClaimNextRequest) error {\n\tif request.Actor == \"\" {\n\t\treturn fmt.Errorf(\"%w: claim next requires an actor\", storage.ErrValidation)\n\t}\n\tif request.Filter.Limit != nil {\n\t\treturn fmt.Errorf(\"%w: claim next does not take a limit\", storage.ErrValidation)\n\t}\n\tif request.Filter.Offset != 0 {\n\t\treturn fmt.Errorf(\"%w: claim next does not take an offset\", storage.ErrValidation)\n\t}\n\t// Brief is refused for the reason Limit and Offset are, and the reason is\n\t// sharper here: a claim does not read its row through the page's query at\n\t// all. ExecuteClaimNext refetches the winning row whole (GetIssueInTx) and\n\t// hydrates its counts itself, so the projection has nothing to apply to,\n\t// and a claim that accepted the field would answer a MUTATING request with\n\t// a fully-hydrated row carrying IsLitePartial=false — the caller's only\n\t// signal that it did not get what it asked for, saying it did.\n\tif request.Filter.Brief {\n\t\treturn fmt.Errorf(\"%w: claim next does not take a projection\", storage.ErrValidation)\n\t}\n\treturn nil\n}\n\n// ExecuteClaimNext claims the first ready issue matching filter in tx, hydrates\n// it in that same transaction, and reports the durable tables changed.\n//\n// It takes the filter rather than the public request because this package\n// speaks WorkFilter: the request-to-filter translation is internal/workapi's,\n// and the same builder that answers Reader.Ready is what the accessor\n// implementations run before calling in here. That is what keeps the claim's\n// predicate and the listing's predicate the same predicate.\n//\n// A nil claim with a nil error is the ordinary empty-front outcome. It reports\n// no changed tables, so the caller's transaction commits nothing and no\n// history entry is recorded.\nfunc ExecuteClaimNext(ctx context.Context, tx *sql.Tx, actor string, filter types.WorkFilter) (publicops.ClaimNextResult, ChangedTables, error) {\n\tclaimed, err := ClaimReadyIssueInTx(ctx, tx, filter, actor)","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/claim_next.go#L24-L60","documentation":"ValidateClaimNextRequest rejects ClaimNextRequest.Filter.Brief=true. A claim does not read its row through the paged query at all: ExecuteClaimNext refetches the winning row whole and hydrates counts itself, so a 'brief' projection has nothing to apply to. Accepting it would return a fully-hydrated row with IsLitePartial=false, falsely signaling the caller got the projection it asked for.","triggerScenarios":"Calling ClaimNext with request.Filter.Brief set to true, usually by reusing a filter from a brief/lite ready listing.","commonSituations":"Code paths that request brief rows to save bandwidth for listings then reuse the same filter struct for claiming; CLI flags like --brief or a lite-mode toggle left enabled when claiming work.","solutions":["Set request.Filter.Brief = false before calling ClaimNext","Use a dedicated WorkFilter for claims, separate from listing filters","If the caller wants a light row, strip fields from the returned ClaimNextResult client-side instead of asking the storage layer to project"],"exampleFix":"// before\nreq.Filter = readyListFilter // Brief: true\nresult, err := store.ClaimNext(ctx, req)\n// after\nreq.Filter = readyListFilter\nreq.Filter.Brief = false // claims always return fully hydrated rows\nresult, err := store.ClaimNext(ctx, req)","handlingStrategy":"validation","validationCode":"if req.Filter.Brief {\n\treturn errors.New(\"ClaimNext always returns a fully hydrated row; Brief is not supported\")\n}","typeGuard":"func noProjection(f types.WorkFilter) bool { return !f.Brief }","tryCatchPattern":null,"preventionTips":["Keep claim filters separate from listing/projection filters","Strip fields from the returned result instead of requesting a projection","Reset Brief (and Limit/Offset) whenever converting a ready-list filter into a claim request"],"tags":["validation","claim-next","projection"],"backgroundTag":"claim-next-projection-not-allowed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}