{"record":{"id":"7d2cdf618d41a1ed","repo":"gastownhall/beads","slug":"metadata-replacement-cannot-combine-with-increment","errorCode":null,"errorMessage":"metadata replacement cannot combine with incremental edits","messagePattern":"metadata replacement cannot combine with incremental edits","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/uow/issue_operations.go","lineNumber":547,"sourceCode":"\tif err := storageissueops.ValidateUpdateRequest(request); err != nil {\n\t\treturn validationError(err)\n\t}\n\tif err := validateMetadataPatch(request.Patch.Metadata); err != nil {\n\t\treturn validationError(err)\n\t}\n\treturn nil\n}\n\nfunc validateCloseRequest(request publicops.CloseRequest) error {\n\tif request.Actor == \"\" || request.IssueID == \"\" {\n\t\treturn validationError(fmt.Errorf(\"close: actor and issue ID must not be empty\"))\n\t}\n\treturn nil\n}\n\nfunc validateMetadataPatch(metadata publicops.MetadataPatch) error {\n\tif metadata.Replace.Set && (metadata.Merge.Set || len(metadata.Set) > 0 || len(metadata.Unset) > 0) {\n\t\treturn fmt.Errorf(\"metadata replacement cannot combine with incremental edits\")\n\t}\n\tif metadata.Replace.Set && len(metadata.Replace.Value) > 0 && !json.Valid(metadata.Replace.Value) {\n\t\treturn fmt.Errorf(\"metadata replacement is not valid JSON\")\n\t}\n\tif metadata.Merge.Set {\n\t\tvar object map[string]json.RawMessage\n\t\tif len(metadata.Merge.Value) == 0 || json.Unmarshal(metadata.Merge.Value, &object) != nil || object == nil {\n\t\t\treturn fmt.Errorf(\"metadata merge must be a JSON object\")\n\t\t}\n\t}\n\tkeys := make([]string, 0, len(metadata.Set))\n\tfor key := range metadata.Set {\n\t\tkeys = append(keys, key)\n\t}\n\tsort.Strings(keys)\n\tfor _, key := range keys {\n\t\tif err := storage.ValidateMetadataKey(key); err != nil {\n\t\t\treturn err","sourceCodeStart":529,"sourceCodeEnd":565,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/uow/issue_operations.go#L529-L565","documentation":"validateMetadataPatch enforces that a metadata Replace (full replacement) is mutually exclusive with incremental edits (Merge, Set, Unset). Mixing them is ambiguous, so the patch is rejected as a plain error inside the validation path.","triggerScenarios":"Calling Update with request.Patch.Metadata where Replace.Set is true AND (Merge.Set is true, or len(Set) > 0, or len(Unset) > 0).","commonSituations":"Composing an update from multiple config sources where one sets Replace and another adds Set/Unset keys; merging user-supplied flags like --metadata-replace together with --set/--unset; copy-paste of patch-building code.","solutions":["Choose one strategy: either Replace alone or incremental edits (Merge/Set/Unset) alone","If you need replacement plus tweaks, compute the final object and use Replace only","If you need targeted changes, drop Replace.Set and use Set/Unset/Merge","Add a builder guard so patch constructors cannot set both"],"exampleFix":"// before\npatch.Metadata.Replace.Set = true\npatch.Metadata.Replace.Value = newJSON\npatch.Metadata.Set[\"priority\"] = json.RawMessage(`\"high\"`)\n// after\npatch.Metadata.Replace.Set = true\npatch.Metadata.Replace.Value = fullMetadataJSON // includes priority\n// (no Set/Unset/Merge alongside Replace)","handlingStrategy":"validation","validationCode":"func validMetadataPatch(p publicops.MetadataPatch) error {\n    if p.Replace.Set && (p.Merge.Set || len(p.Set) > 0 || len(p.Unset) > 0) {\n        return fmt.Errorf(\"replace is mutually exclusive with merge/set/unset\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"if err := uc.Update(ctx, req); err != nil {\n    if errors.Is(err, publicops.ErrValidation) && strings.Contains(err.Error(), \"cannot combine\") {\n        // rebuild patch with a single metadata strategy\n    }\n    return err\n}","preventionTips":["Pick one metadata-editing strategy per update (replace OR incremental)","Compose patch fields in a single builder function, not from multiple flag sources","Add a unit test asserting Replace+Set is rejected in your patch builder","Document the exclusivity rule where patch types are constructed"],"tags":["validation","metadata-patch","go","conflict"],"backgroundTag":"metadata-patch-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}