{"record":{"id":"72392feccd8c5ea9","repo":"gastownhall/beads","slug":"w-close-batch-item-d-requires-an-issue-id","errorCode":null,"errorMessage":"%w: close batch item %d requires an issue ID","messagePattern":"%w: close batch item (.+?) requires an issue ID","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/storage/issueops/close_batch.go","lineNumber":27,"sourceCode":"\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\n// batch that skipped a mistyped id must not claim it in the log.\n//\n// LANDED is Changed, not \"no error\": an idempotent re-close persisted nothing,\n// so naming it would put an id in `bd dolt log` under a commit that did not","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close_batch.go#L9-L45","documentation":"ValidateCloseBatchRequest rejects any batch item whose IssueID is empty, wrapping storage.ErrValidation and identifying the offending item index (0-based) in the message. Every item in a batch close must name a target issue; a blank ID can never resolve and would corrupt per-item result mapping, so it is rejected before storage work. Match with errors.Is(err, storage.ErrValidation).","triggerScenarios":"Building CloseBatchRequest items where an element has IssueID == \"\" — e.g. mapping over records whose ID field was never populated, or partially deserialized JSON items missing the id key.","commonSituations":"Parsing CLI/CSV input where one row lacks an ID; joining issue lists where the ID column name mismatched; items copied from another type with a different ID field name; truncated file imports.","solutions":["Validate each item's IssueID before constructing the request and report the bad record to the user (the error message already gives the index).","Check item construction/mapping code — ensure the correct ID field is copied into IssueID for every element.","Match errors.Is(err, storage.ErrValidation), parse the index if needed, and drop or fix the offending item.","Harden import/parsing paths to fail on rows with missing IDs at read time."],"exampleFix":"// before\nitems := make([]publicops.CloseBatchItem, 0, len(rows))\nfor _, r := range rows {\n    items = append(items, publicops.CloseBatchItem{IssueID: r.ID}) // r.ID may be \"\"\n}\n// after\nfor i, r := range rows {\n    if r.ID == \"\" {\n        return fmt.Errorf(\"row %d has no issue ID\", i)\n    }\n    items = append(items, publicops.CloseBatchItem{IssueID: r.ID})\n}","handlingStrategy":"validation","validationCode":"for i, item := range items {\n    if item.IssueID == \"\" {\n        return fmt.Errorf(\"item %d is missing an issue ID\", i)\n    }\n}","typeGuard":"func validBatchItems(items []publicops.CloseBatchItem) bool {\n    for _, it := range items {\n        if it.IssueID == \"\" {\n            return false\n        }\n    }\n    return true\n}","tryCatchPattern":"if err := ValidateCloseBatchRequest(req); err != nil {\n    if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), \"requires an issue ID\") {\n        var idx int\n        fmt.Sscanf(err.Error(), \"close batch item %d\", &idx)\n        return fmt.Errorf(\"batch item %d has no issue ID; fix the input source\", idx)\n    }\n    return err\n}","preventionTips":["Validate every item at construction time, not just at the storage boundary.","Fail fast in parsers/imports when a row lacks an ID.","Use a helper to build CloseBatchItem that rejects empty IDs.","Map the correct source ID field when converting between types."],"tags":["validation","batch-close","missing-id","api-misuse"],"backgroundTag":"missing-issue-id","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}