{"record":{"id":"1be72c355fecb27b","repo":"gastownhall/beads","slug":"s-must-be-a-string-got-t","errorCode":null,"errorMessage":"%s must be a string, got %T","messagePattern":"(.+?) must be a string, got %T","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":975,"sourceCode":"\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) {\n\tif !present {\n\t\treturn nil, nil\n\t}\n\tswitch v := value.(type) {","sourceCodeStart":957,"sourceCodeEnd":993,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L957-L993","documentation":"The value provided for the append-notes operation is not a string. OpAppendNotes appends text to the issue's existing notes, so its value must be a Go string; any other type is rejected with this type-error message that reports the actual type received.","triggerScenarios":"Passing a non-string (int, []string, fmt.Stringer, *string, etc.) as the OpAppendNotes value in an update map passed to ResolveMergeOps.","commonSituations":"Building updates with a generic map[string]interface{} where a helper inserts the wrong type, passing a *string instead of dereferencing it, or deserializing a JSON number/bool into the append field.","solutions":["Convert the value to a string before inserting: fmt.Sprintf, strconv, or explicit dereference of *string","Use a typed update builder that enforces string for append-notes","Check the reported %T in the message to see which type actually arrived"],"exampleFix":"// before\nvar note *string = getNote()\nupdates[issueops.OpAppendNotes] = note // *string, not string\n\n// after\nif note != nil {\n    updates[issueops.OpAppendNotes] = *note // plain string\n}","handlingStrategy":"type-guard","validationCode":"if v, ok := updates[issueops.OpAppendNotes]; ok {\n    if _, isStr := v.(string); !isStr {\n        return fmt.Errorf(\"OpAppendNotes must be a string, got %T\", v)\n    }\n}","typeGuard":"func isString(v any) bool { _, ok := v.(string); return ok }","tryCatchPattern":"if err := issueops.ResolveMergeOps(issue, updates, resolved); err != nil {\n    if strings.Contains(err.Error(), \"must be a string, got\") {\n        return fmt.Errorf(\"coerce the append-notes value to string before retrying: %w\", err)\n    }\n}","preventionTips":["Dereference *string values before inserting into the updates map","Use typed builders instead of map[string]interface{} for updates","Check the %T in the error message to identify the offending type"],"tags":["validation","notes","type-error"],"backgroundTag":"wrong-argument-type","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}