{"record":{"id":"462d5e31794eb5d4","repo":"gastownhall/beads","slug":"w-v","errorCode":null,"errorMessage":"%w: %v","messagePattern":"%w: %v","errorType":"validation","errorClass":"issueops.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/metadata_cas.go","lineNumber":68,"sourceCode":"// 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\n// CanonicalMetadataValue returns raw's canonical encoding: the encoding two\n// JSON metadata values share exactly when issueops.MetadataCAS calls them\n// equal.\n//","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/metadata_cas.go#L50-L86","documentation":"PlanCompareAndSetKey validates the metadata key with ValidateMetadataKey and wraps any failure as issueops.ErrValidation with the underlying message. ValidateMetadataKey rejects invalid characters (including quotes and backslashes) and empty/oversized keys, since keys are embedded in JSON path expressions and must be safe. The wrapped %v carries the specific rule that was violated.","triggerScenarios":"Calling PlanCompareAndSetKey with in.Key containing illegal characters (quotes, backslashes, whitespace/control chars), an empty key, or a key exceeding the allowed length — e.g. a key read from user input without sanitization.","commonSituations":"Building keys dynamically from labels or filenames that contain spaces/quotes; copy-pasting keys with trailing whitespace; API consumers passing arbitrary user input straight through as a key.","solutions":["Call ValidateMetadataKey(key) yourself first and surface the specific rule violation to the user.","Sanitize the key: lowercase kebab-case, letters/digits/hyphens, no quotes or backslashes.","Trim whitespace and reject empty keys before constructing the request.","Check errors.Is(err, issueops.ErrValidation) to distinguish this from storage errors."],"exampleFix":"// before\nkey := \"my key \\\"quoted\\\"\" // illegal characters\nplan, err := PlanCompareAndSetKey(req) // ErrValidation\n// after\nkey := \"my-key\"\nif err := ValidateMetadataKey(key); err == nil {\n    plan, err = PlanCompareAndSetKey(req)\n}","handlingStrategy":"validation","validationCode":"if err := ValidateMetadataKey(req.Key); err != nil {\n    return fmt.Errorf(\"invalid metadata key %q: %v\", req.Key, err)\n} // run before PlanCompareAndSetKey","typeGuard":"func safeKey(k string) bool { return ValidateMetadataKey(k) == nil }","tryCatchPattern":"plan, err := PlanCompareAndSetKey(req)\nif err != nil {\n    if errors.Is(err, issueops.ErrValidation) {\n        return fmt.Errorf(\"compare-and-set rejected: %v\", err)\n    }\n    return err\n}","preventionTips":["Restrict keys to kebab-case alphanumerics; reject quotes, backslashes, and whitespace at the input boundary.","Trim and check non-empty before accepting user-supplied keys.","Re-run ValidateMetadataKey in tests for every key format your tooling can produce."],"tags":["validation","metadata","compare-and-set"],"backgroundTag":"invalid-metadata-key","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}