{"record":{"id":"97f52cae7ca952e2","repo":"gastownhall/beads","slug":"w-compare-and-set-requires-an-issue-id","errorCode":null,"errorMessage":"%w: compare-and-set requires an issue id","messagePattern":"%w: compare-and-set requires an issue id","errorType":"validation","errorClass":"issueops.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/metadata_cas.go","lineNumber":64,"sourceCode":"\t// request removes the key.\n\tValue *json.RawMessage\n}\n\n// PlanCompareAndSetKey validates a compare-and-set request and canonicalizes\n// its values. It is the whole of the role's request validation: every\n// implementation calls it before touching a substrate, so a refused request\n// costs no database work anywhere.\n//\n// It COPIES both raw values rather than aliasing the caller's, because the\n// canonical form is written into the plan and the request belongs to the caller\n// for the whole call.\nfunc PlanCompareAndSetKey(in issueops.CompareAndSetKeyRequest) (CompareAndSetKeyPlan, error) {\n\tif in.Actor == \"\" {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\n\t\t\t\"%w: compare-and-set requires an actor to attribute the swap to\", issueops.ErrValidation)\n\t}\n\tif in.IssueID == \"\" {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\n\t\t\t\"%w: compare-and-set requires an issue id\", issueops.ErrValidation)\n\t}\n\tif err := ValidateMetadataKey(in.Key); err != nil {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\"%w: %v\", issueops.ErrValidation, err)\n\t}\n\tplan := CompareAndSetKeyPlan{Actor: in.Actor, IssueID: in.IssueID, Key: in.Key}\n\tvar err error\n\tif plan.Expected, err = CanonicalMetadataPointer(in.Expected); err != nil {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\"%w: expected value for metadata key %q: %v\",\n\t\t\tissueops.ErrValidation, in.Key, err)\n\t}\n\tif plan.Value, err = CanonicalMetadataPointer(in.Value); err != nil {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\"%w: new value for metadata key %q: %v\",\n\t\t\tissueops.ErrValidation, in.Key, err)\n\t}\n\treturn plan, nil\n}\n","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/metadata_cas.go#L46-L82","documentation":"PlanCompareAndSetKey returns this wrapped issueops.ErrValidation error when in.IssueID is empty. The compare-and-set operation targets a specific issue's metadata key, so an issue id is mandatory for the plan. It is a pure input-validation error raised before any storage access.","triggerScenarios":"Calling PlanCompareAndSetKey with CompareAndSetKeyRequest.IssueID == \"\" — e.g. the caller resolved no issue from a search, or a variable holding the issue id was never assigned.","commonSituations":"A lookup step returned zero results and the empty string was passed through; scripts using an unset shell variable ($ID expands to empty); refactors that renamed the field and left it unset.","solutions":["Set in.IssueID to the target issue's id before calling (e.g. \"bd-42\").","Guard the caller: if issueID == \"\" return early with a clearer upstream message instead of reaching the planner.","In shell scripts, quote and check: : \"${ID:?issue id required}\".","Match errors.Is(err, issueops.ErrValidation) to render a user-friendly 'issue id required' error."],"exampleFix":"// before\nplan, err := PlanCompareAndSetKey(issueops.CompareAndSetKeyRequest{Actor: \"alice\", Key: \"status\"})\n// after\nplan, err := PlanCompareAndSetKey(issueops.CompareAndSetKeyRequest{Actor: \"alice\", IssueID: \"bd-42\", Key: \"status\"})","handlingStrategy":"validation","validationCode":"if req.IssueID == \"\" {\n    return errors.New(\"compare-and-set requires an issue id\")\n} // run before PlanCompareAndSetKey","typeGuard":null,"tryCatchPattern":"plan, err := PlanCompareAndSetKey(req)\nif err != nil {\n    if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), \"requires an issue id\") {\n        return fmt.Errorf(\"no issue selected: resolve an issue id first\")\n    }\n    return err\n}","preventionTips":["Fail fast in the caller when issue lookup returns zero results instead of passing \"\".","In shell scripts, use ${ID:?message} to catch unset variables.","Keep issue-id resolution and mutation planning in the same code path."],"tags":["validation","metadata","compare-and-set"],"backgroundTag":"validation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}