{"record":{"id":"906cddc1d1cd050a","repo":"gastownhall/beads","slug":"cannot-combine-a-metadata-replacement-with-increme","errorCode":null,"errorMessage":"cannot combine a metadata replacement with incremental metadata edits","messagePattern":"cannot combine a metadata replacement with incremental metadata edits","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":880,"sourceCode":"\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// resolveMetadataMergeOps folds OpMergeMetadata/OpSetMetadata/OpUnsetMetadata\n// into a concrete \"metadata\" value on resolved, using oldIssue.Metadata (read in\n// the same mutation transaction) as the base. It is a no-op when no metadata\n// operation keys are present.\nfunc resolveMetadataMergeOps(oldIssue *types.Issue, updates, resolved map[string]interface{}) error {\n\t_, hasMerge := updates[OpMergeMetadata]\n\t_, hasSet := updates[OpSetMetadata]\n\t_, hasUnset := updates[OpUnsetMetadata]\n\tif !hasMerge && !hasSet && !hasUnset {\n\t\treturn nil\n\t}\n\tif _, direct := resolved[\"metadata\"]; direct {\n\t\treturn fmt.Errorf(\"cannot combine a metadata replacement with incremental metadata edits\")\n\t}\n\n\tcurrent := oldIssue.Metadata\n\tif hasMerge {\n\t\tnormalized, err := storage.NormalizeMetadataValue(updates[OpMergeMetadata])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid %s: %w\", OpMergeMetadata, err)\n\t\t}\n\t\tmerged, err := storage.MergeMetadataJSON(current, json.RawMessage(normalized))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"metadata merge failed: %w\", err)\n\t\t}\n\t\tcurrent = merged\n\t}\n\tif hasSet || hasUnset {\n\t\tunset, err := mergeOpStrings(OpUnsetMetadata, updates[OpUnsetMetadata], hasUnset)\n\t\tif err != nil {\n\t\t\treturn err","sourceCodeStart":862,"sourceCodeEnd":898,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L862-L898","documentation":"The update payload supplied both a direct replacement of the metadata field and incremental metadata operations (merge/set/unset). The library cannot apply both semantics in one update, so it rejects the mutation before touching the database.","triggerScenarios":"Calling ResolveMergeOps (via an issue update) with updates containing OpSetMetadata, OpMergeMetadata, or OpUnsetMetadata while the resolved map already contains a direct \"metadata\" value.","commonSituations":"Building an update struct where both Metadata and MetadataOps/Merge fields are populated by different code paths (e.g. a CLI flag plus a default merge), or double-applying metadata in a middleware layer.","solutions":["Remove the direct metadata replacement and keep only merge/set/unset ops","Or drop the incremental ops and supply the full metadata object as a replacement","If combining is intended, perform two sequential update calls"],"exampleFix":"// before\nupdates := map[string]any{\n  issueops.OpSetMetadata:   map[string]any{\"a\": 1},\n  issueops.OpMergeMetadata: map[string]any{\"b\": 2},\n}\n\n// after\nupdates := map[string]any{\n  issueops.OpMergeMetadata: map[string]any{\"b\": 2}, // incremental only\n}","handlingStrategy":"validation","validationCode":"func checkNoConflict(updates map[string]any, resolved map[string]any) error {\n    _, direct := resolved[\"metadata\"]\n    _, hasOp := updates[issueops.OpSetMetadata]\n    hasOp = hasOp || updates[issueops.OpMergeMetadata] != nil || updates[issueops.OpUnsetMetadata] != nil\n    if direct && hasOp {\n        return errors.New(\"remove either the metadata replacement or the merge/set/unset ops\")\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"if err := issueops.ResolveMergeOps(issue, updates, resolved); err != nil {\n    if strings.Contains(err.Error(), \"cannot combine a metadata replacement\") {\n        // strip incremental ops and retry with replacement only\n    }\n}","preventionTips":["Pick one metadata mutation style (replacement OR ops) per update call","Centralize metadata update construction in one helper","Audit middleware that injects metadata ops into caller-built updates"],"tags":["validation","metadata","api-misuse"],"backgroundTag":"conflicting-update-fields","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}