{"record":{"id":"7e88db3b5bf9fa62","repo":"gastownhall/beads","slug":"w-close-batch-requires-at-least-one-item","errorCode":null,"errorMessage":"%w: close batch requires at least one item","messagePattern":"%w: close batch requires at least one item","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/storage/issueops/close_batch.go","lineNumber":23,"sourceCode":"\t\"database/sql\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/steveyegge/beads/internal/storage\"\n\t\"github.com/steveyegge/beads/internal/types\"\n\tpublicops \"github.com/steveyegge/beads/issueops\"\n)\n\n// ValidateCloseBatchRequest applies the request rules every BatchCloser\n// implementation shares, so a rule is a contract rather than one backend's\n// habit. It rejects the request outright; a per-item refusal is a result, not\n// a validation failure, and never reaches here.\nfunc ValidateCloseBatchRequest(request publicops.CloseBatchRequest) error {\n\tif request.Actor == \"\" {\n\t\treturn fmt.Errorf(\"%w: close batch requires an actor\", storage.ErrValidation)\n\t}\n\tif len(request.Items) == 0 {\n\t\treturn fmt.Errorf(\"%w: close batch requires at least one item\", storage.ErrValidation)\n\t}\n\tfor i, item := range request.Items {\n\t\tif item.IssueID == \"\" {\n\t\t\treturn fmt.Errorf(\"%w: close batch item %d requires an issue ID\", storage.ErrValidation, i)\n\t\t}\n\t}\n\tif request.ClaimNext != nil {\n\t\tif err := ValidateClaimNextRequest(publicops.ClaimNextRequest{Actor: request.Actor, Filter: *request.ClaimNext}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// CloseBatchCommitMessage is the history entry a batch records. It is the\n// only spelling — the request carries no label to override it, and could not\n// compose this one, because it names what LANDED rather than what was asked for,\n// which is why it is composed from the result and not from the request: a","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close_batch.go#L5-L41","documentation":"ValidateCloseBatchRequest rejects a CloseBatchRequest with zero items, wrapping storage.ErrValidation. A batch close with no items is a no-op misuse rather than something a backend should silently succeed on, so the shared contract rejects it up front. Callers should match with errors.Is(err, storage.ErrValidation).","triggerScenarios":"Passing CloseBatchRequest with a nil or empty Items slice to any BatchCloser implementation that invokes ValidateCloseBatchRequest.","commonSituations":"Upstream filtering (e.g. filtering a list of issues by status) accidentally removed every candidate before batching; loops that append to the wrong slice; JSON payloads where items was omitted or empty; tests with unpopulated fixtures.","solutions":["Guard before calling: check len(request.Items) > 0 and skip or report 'nothing to close' instead of issuing the call.","Match errors.Is(err, storage.ErrValidation) and return a friendly 'no issues selected' message.","Fix upstream selection logic that produced an empty candidate list unexpectedly.","In API layers, return 400-style validation output before touching storage."],"exampleFix":"// before\nerr := closer.CloseBatch(ctx, publicops.CloseBatchRequest{Actor: actor, Items: nil})\n// after\nif len(items) == 0 {\n    return fmt.Errorf(\"no issues to close\")\n}\nerr := closer.CloseBatch(ctx, publicops.CloseBatchRequest{Actor: actor, Items: items})","handlingStrategy":"validation","validationCode":"if len(items) == 0 {\n    return fmt.Errorf(\"no issues selected for batch close\")\n}","typeGuard":"func hasBatchItems(req publicops.CloseBatchRequest) bool {\n    return len(req.Items) > 0\n}","tryCatchPattern":"if err := ValidateCloseBatchRequest(req); err != nil {\n    if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), \"at least one item\") {\n        return ErrNothingToClose // typed sentinel for callers\n    }\n    return err\n}","preventionTips":["Short-circuit empty candidate lists before issuing a batch call.","Log when upstream filtering reduces the item set to zero — usually a filter bug.","Provide an explicit 'nothing to do' UX path instead of an error surprise.","Cover the empty-input case in tests."],"tags":["validation","batch-close","empty-input","api-misuse"],"backgroundTag":"empty-batch-validation","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}