{"record":{"id":"6b4309e9267507e8","repo":"gastownhall/beads","slug":"w-compare-and-set-requires-an-actor-to-attribute","errorCode":null,"errorMessage":"%w: compare-and-set requires an actor to attribute the swap to","messagePattern":"%w: compare-and-set requires an actor to attribute the swap to","errorType":"validation","errorClass":"issueops.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/metadata_cas.go","lineNumber":60,"sourceCode":"\t// Expected is the canonical encoding of the value the key must hold, or nil\n\t// when the request requires the key to be ABSENT.\n\tExpected *json.RawMessage\n\t// Value is the canonical encoding of the value to store, or nil when the\n\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)","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/metadata_cas.go#L42-L78","documentation":"PlanCompareAndSetKey validates its CompareAndSetKeyRequest and fails fast with the sentinel issueops.ErrValidation when in.Actor is empty. A compare-and-set swap must be attributed to an actor for audit purposes, so the library refuses to plan the operation without one. Callers should match on errors.Is(err, issueops.ErrValidation) to distinguish it from I/O failures.","triggerScenarios":"Calling PlanCompareAndSetKey with a CompareAndSetKeyRequest whose Actor field is \"\" — e.g. building the request programmatically without the current user, or a CLI context where the actor was not resolved before planning.","commonSituations":"Automation/CI running without a configured user identity; forgetting to pass --actor or equivalent on the CLI; constructing the request in tests or scripts with only key/value fields filled.","solutions":["Populate in.Actor with the issuing identity (username, agent id, or email) before calling PlanCompareAndSetKey.","On the CLI, supply the actor flag or ensure the environment provides an identity.","Use errors.Is(err, issueops.ErrValidation) to detect and surface a clear 'actor required' message to users.","If identity is genuinely unavailable, decide on an explicit service account name rather than an empty string."],"exampleFix":"// before\nplan, err := PlanCompareAndSetKey(issueops.CompareAndSetKeyRequest{IssueID: \"bd-1\", Key: \"status\"})\n// after\nplan, err := PlanCompareAndSetKey(issueops.CompareAndSetKeyRequest{Actor: \"alice\", IssueID: \"bd-1\", Key: \"status\"})","handlingStrategy":"validation","validationCode":"if req.Actor == \"\" {\n    return errors.New(\"compare-and-set requires an actor; set Actor to the issuing identity\")\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 actor\") {\n        return fmt.Errorf(\"actor required for compare-and-set: configure identity\")\n    }\n    return err\n}","preventionTips":["Resolve the acting identity (user, agent, service account) before planning mutations.","Never construct CompareAndSetKeyRequest with a zero-value Actor.","In CI, configure an explicit bot identity rather than relying on ambient auth."],"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"}