{"record":{"id":"8f91793d77ee7f47","repo":"gastownhall/beads","slug":"unclaim-id-must-not-be-empty","errorCode":null,"errorMessage":"Unclaim: id must not be empty","messagePattern":"Unclaim: id must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/issue.go","lineNumber":1880,"sourceCode":"func (u *issueUseCaseImpl) GetStaleIssues(ctx context.Context, filter types.StaleFilter) ([]*types.Issue, error) {\n\tout, err := u.issueRepo.GetStaleIssues(ctx, filter)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"GetStaleIssues: %w\", err)\n\t}\n\treturn out, nil\n}\n\nfunc (u *issueUseCaseImpl) GetEpicsEligibleForClosure(ctx context.Context) ([]*types.EpicStatus, error) {\n\tout, err := u.issueRepo.GetEpicsEligibleForClosure(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"GetEpicsEligibleForClosure: %w\", err)\n\t}\n\treturn out, nil\n}\n\nfunc (u *issueUseCaseImpl) Unclaim(ctx context.Context, id, actor string, force bool) error {\n\tif id == \"\" {\n\t\treturn fmt.Errorf(\"Unclaim: id must not be empty\")\n\t}\n\tif err := u.issueRepo.UnclaimIssue(ctx, id, actor, force); err != nil {\n\t\treturn fmt.Errorf(\"Unclaim: %w\", err)\n\t}\n\treturn nil\n}\n\n// UnclaimIfAssignee is the compare-and-swap release: it clears the claim only\n// while the issue is still assigned to expectedAssignee, and otherwise returns\n// storage.ErrAssigneeMismatch having written nothing. It is the conditional\n// twin of Unclaim and runs the SAME transition (assignee cleared, status\n// reopened, started_at cleared, lease dropped, row_lock rewritten, \"unclaimed\"\n// event recorded) because both reach the one classic implementation in\n// issueops — which is what makes `bd unclaim --if-assignee` behave identically\n// on the proxied-server and embedded backends.\nfunc (u *issueUseCaseImpl) UnclaimIfAssignee(ctx context.Context, id, actor, expectedAssignee string) error {\n\tif id == \"\" {\n\t\treturn fmt.Errorf(\"UnclaimIfAssignee: id must not be empty\")","sourceCodeStart":1862,"sourceCodeEnd":1898,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/issue.go#L1862-L1898","documentation":"Validation error from Unclaim: the use case rejects the call because the issue id argument is an empty string. This is thrown before any repository interaction, so nothing was modified. It is a caller contract violation, not a storage failure.","triggerScenarios":"Calling Unclaim(ctx, \"\", actor, force) — passing an empty id, typically when a variable holding the issue key was never populated.","commonSituations":"Parsing a CLI arg or JSON field that is missing/empty and passing it straight to Unclaim; empty results from upstream lookups feeding the claim-release path.","solutions":["Ensure the issue id is resolved before calling Unclaim (check the source of the empty value)","Validate the id is non-empty in your caller before invoking","If the id comes from user input, surface a clear 'issue key required' message"],"exampleFix":"// before\nif err := uc.Unclaim(ctx, issueID, actor, false); err != nil { ... }\n// after\nif issueID == \"\" {\n\treturn fmt.Errorf(\"issue id required\")\n}\nif err := uc.Unclaim(ctx, issueID, actor, false); err != nil { ... }","handlingStrategy":"validation","validationCode":"if id == \"\" {\n\treturn fmt.Errorf(\"cannot unclaim: issue id is empty\")\n}","typeGuard":null,"tryCatchPattern":"if err := uc.Unclaim(ctx, id, actor, force); err != nil {\n\tif strings.Contains(err.Error(), \"id must not be empty\") {\n\t\treturn fmt.Errorf(\"caller bug: empty issue id\")\n\t}\n\treturn err\n}","preventionTips":["Validate issue ids at the boundary (CLI args, JSON fields) before they reach storage calls","Fail fast on empty lookups instead of threading empty strings downward","Use a small helper to assert non-empty keys before any uc.* call"],"tags":["validation","claim","input-validation"],"backgroundTag":"empty-required-argument","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}