{"record":{"id":"ebd1ad9f63ff652f","repo":"JuliusBrussee/caveman","slug":"note-exceeds-4096-bytes-learn-outcomes","errorCode":null,"errorMessage":"note exceeds 4096 bytes","messagePattern":"note exceeds 4096 bytes","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/internal/store/learn_outcomes.go","lineNumber":83,"sourceCode":"\t// A scan run after the user's edit is post-fix evidence. Prefer newest report\n\t// snapshot strictly older than this plan's scan; use live plan only when no\n\t// older snapshot contains the sink. Missing live sinks may use snapshot only.\n\tsink, beforeSource := beforeSinkForAppliedFix(plan, sinkID, liveSink, appliedAtTime)\n\tif sink == nil {\n\t\treturn AppliedFixRecord{}, fmt.Errorf(\"sink %q not found in current learn plan\", sinkID)\n\t}\n\tfixKind = strings.TrimSpace(fixKind)\n\tif fixKind == \"\" {\n\t\tfixKind = defaultFixKind(*sink)\n\t}\n\tif fixKind == \"\" {\n\t\treturn AppliedFixRecord{}, fmt.Errorf(\"sink %q has no measurable fix kind; pass --fix-kind explicitly\", sinkID)\n\t}\n\tif len(fixKind) > 128 {\n\t\treturn AppliedFixRecord{}, fmt.Errorf(\"fix kind exceeds 128 bytes\")\n\t}\n\tif len(note) > 4096 {\n\t\treturn AppliedFixRecord{}, fmt.Errorf(\"note exceeds 4096 bytes\")\n\t}\n\tappliedAt := appliedAtTime.Format(time.RFC3339)\n\tbeforeEvidence := make(map[string]any, len(sink.Evidence)+1)\n\tfor key, value := range sink.Evidence {\n\t\tbeforeEvidence[key] = value\n\t}\n\tbeforeEvidence[\"before_source\"] = beforeSource\n\tevidence, err := json.Marshal(beforeEvidence)\n\tif err != nil {\n\t\treturn AppliedFixRecord{}, fmt.Errorf(\"encode before evidence: %w\", err)\n\t}\n\t// Fingerprint the edited artifact as it stands NOW — which at\n\t// `caveman learn applied` time is immediately after the approved edit. This\n\t// is the provenance anchor: a later scan re-hashes the same path and can\n\t// state whether the change we proposed is still the change that is there.\n\ttarget := fingerprintFixTarget(*sink)\n\ttargetJSON := \"\"\n\tif target != nil {","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/proxy/internal/store/learn_outcomes.go#L65-L101","documentation":"The optional free-form note attached to an applied-fix record is persisted in SQLite and echoed in reports. RecordAppliedFix caps it at 4096 bytes to bound row size. Any note longer than that is rejected outright.","triggerScenarios":"Calling RecordAppliedFix with a note argument longer than 4096 bytes.","commonSituations":"Pasting long analysis text or a full diff into --note; scripting tools that write logs as the note.","solutions":["Truncate the note to 4096 bytes before calling RecordAppliedFix","Move long explanations into a separate file and reference it in a short note","Validate note length in the CLI layer with a friendly message"],"exampleFix":"// before\nrec, err := store.RecordAppliedFix(ctx, sink, fixKind, longNote)\n// after\nif len(longNote) > 4096 {\n    longNote = longNote[:4096]\n}\nrec, err := store.RecordAppliedFix(ctx, sink, fixKind, longNote)","handlingStrategy":"validation","validationCode":"if len(note) > 4096 { note = note[:4096] } // or reject with a clear message","typeGuard":null,"tryCatchPattern":"if _, err := store.RecordAppliedFix(ctx, sink, kind, note); err != nil { if strings.Contains(err.Error(), \"note exceeds 4096\") { note = note[:4096]; retry } }","preventionTips":["Truncate notes defensively before calling the store","Keep long analysis in files and reference them from short notes","Enforce a smaller internal note limit (e.g. 2000 chars) in your tooling"],"tags":["validation","input-length","learn"],"backgroundTag":"value-out-of-range","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}