{"record":{"id":"d061556b0bb74ee0","repo":"gastownhall/beads","slug":"w-cannot-combine-a-notes-replacement-with-s","errorCode":null,"errorMessage":"%w: cannot combine a notes replacement with %s","messagePattern":"%w: cannot combine a notes replacement with (.+?)","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":971,"sourceCode":"\t\tdelete(data, key)\n\t}\n\tresult, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal metadata: %w\", err)\n\t}\n\treturn json.RawMessage(result), nil\n}\n\n// resolveNotesAppendOp folds OpAppendNotes into a concrete \"notes\" value on\n// resolved, appending to oldIssue.Notes (read in the same mutation transaction).\n// It is a no-op when the append op is absent.\nfunc resolveNotesAppendOp(oldIssue *types.Issue, updates, resolved map[string]interface{}) error {\n\traw, ok := updates[OpAppendNotes]\n\tif !ok {\n\t\treturn nil\n\t}\n\tif _, direct := resolved[\"notes\"]; direct {\n\t\treturn fmt.Errorf(\"%w: cannot combine a notes replacement with %s\", storage.ErrValidation, OpAppendNotes)\n\t}\n\ttext, ok := raw.(string)\n\tif !ok {\n\t\treturn fmt.Errorf(\"%s must be a string, got %T\", OpAppendNotes, raw)\n\t}\n\tcombined := oldIssue.Notes\n\tif combined != \"\" {\n\t\tcombined += \"\\n\"\n\t}\n\tcombined += text\n\tresolved[\"notes\"] = combined\n\treturn nil\n}\n\n// mergeOpStrings coerces a merge-operation value to []string. Accepts\n// []interface{} of strings as well, so operation maps survive a JSON\n// round-trip (e.g. daemon transports).\nfunc mergeOpStrings(op string, value interface{}, present bool) ([]string, error) {","sourceCodeStart":953,"sourceCodeEnd":989,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L953-L989","documentation":"An update attempted to replace the notes field directly while also appending to notes via OpAppendNotes. These operations conflict (the replacement would discard the appended text), so the library rejects the update with a validation error (wrapping storage.ErrValidation).","triggerScenarios":"Calling ResolveMergeOps with OpAppendNotes present in updates while the resolved map already contains a direct \"notes\" value.","commonSituations":"A code path that sets Notes explicitly while another layer (CLI command, webhook handler) adds an append op in the same update payload.","solutions":["Remove the direct notes replacement and keep only the append op","Or drop OpAppendNotes and include the appended text directly in the replacement string","Split into two sequential update calls if both semantics are required"],"exampleFix":"// before\nresolved[\"notes\"] = \"reset\"\nupdates[issueops.OpAppendNotes] = \"new entry\"\n\n// after\nupdates[issueops.OpAppendNotes] = \"new entry\" // append only, no direct notes set","handlingStrategy":"validation","validationCode":"if _, hasAppend := updates[issueops.OpAppendNotes]; hasAppend {\n    if _, direct := resolved[\"notes\"]; direct {\n        return errors.New(\"drop either the notes replacement or the append op\")\n    }\n}","typeGuard":null,"tryCatchPattern":"err := issueops.ResolveMergeOps(issue, updates, resolved)\nif err != nil {\n    var vErr error = storage.ErrValidation\n    if errors.Is(err, vErr) && strings.Contains(err.Error(), \"notes replacement\") {\n        delete(resolved, \"notes\") // retry with append only\n        err = issueops.ResolveMergeOps(issue, updates, resolved)\n    }\n}","preventionTips":["Never populate both resolved[\"notes\"] and updates[OpAppendNotes]","Route all notes mutations through one helper","Use append ops for additive logging; use replacement only for full rewrites"],"tags":["validation","notes","api-misuse"],"backgroundTag":"conflicting-update-fields","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}