{"record":{"id":"56efc83686765bd7","repo":"JuliusBrussee/caveman","slug":"json-splice-d-replacements-for-d-candidates","errorCode":null,"errorMessage":"json splice: %d replacements for %d candidates","messagePattern":"json splice: (.+?) replacements for (.+?) candidates","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/providers/jsonsplice/jsonsplice.go","lineNumber":105,"sourceCode":"\t}\n\tvar out string\n\tif json.Unmarshal(body[span.Start:span.End], &out) != nil {\n\t\treturn \"\", false\n\t}\n\treturn out, true\n}\n\nfunc StringField(body []byte, object Span, name string) (string, bool) {\n\tspan, ok := Field(body, object, name)\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\treturn String(body, span)\n}\n\nfunc Replace(body []byte, candidates []Candidate, replacements [][]byte) ([]byte, error) {\n\tif len(candidates) != len(replacements) {\n\t\treturn nil, fmt.Errorf(\"json splice: %d replacements for %d candidates\", len(replacements), len(candidates))\n\t}\n\tvar out []byte\n\tlast := 0\n\tfor i, candidate := range candidates {\n\t\tif candidate.Start < last || candidate.End > len(body) || candidate.Start >= candidate.End {\n\t\t\treturn nil, fmt.Errorf(\"json splice: invalid range\")\n\t\t}\n\t\treplacement := replacements[i]\n\t\tif replacement == nil || bytes.Equal(replacement, candidate.Original) {\n\t\t\tcontinue\n\t\t}\n\t\tquoted, err := quote(string(replacement))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif out == nil {\n\t\t\tout = make([]byte, 0, len(body)-len(candidate.Original)+len(replacement))\n\t\t}","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/proxy/providers/jsonsplice/jsonsplice.go#L87-L123","documentation":"jsonsplice.Replace splices candidate string replacements into a JSON body byte-for-byte and requires a 1:1 mapping between candidates and replacement values. If the lengths differ it refuses to proceed because it cannot determine which candidate each replacement belongs to.","triggerScenarios":"Calling Replace(body, candidates, replacements) where len(replacements) != len(candidates), typically after filtering candidates (skipping unchanged strings) without filtering replacements in parallel.","commonSituations":"OpenAI-style compression passes that drop candidates equal to their original but forget to drop matching replacements; off-by-one when building replacement slices; nil-vs-empty slice handling when no strings changed.","solutions":["Build candidates and replacements in a single loop so their lengths always match","Filter both slices together (e.g. pair them in a temp slice of structs) instead of independently","When everything is unchanged, pass empty (not nil-mismatched) slices or skip the call","Add an assertion/log before the call when lengths diverge to find the filtering bug"],"exampleFix":"// before: independent filtering\nvar cands []Candidate; var reps [][]byte\nfor _, c := range all { if !bytes.Equal(c.Rep, c.Original) { cands = append(cands, c) } }\nreps = allReps\nout, err := Replace(body, cands, reps)\n// after: paired filtering\ntype pair struct{ c Candidate; r []byte }\nvar kept []pair\nfor _, p := range pairs { if !bytes.Equal(p.r, p.c.Original) { kept = append(kept, p) } }\ncands := make([]Candidate, len(kept)); reps := make([][]byte, len(kept))\nfor i, p := range kept { cands[i], reps[i] = p.c, p.r }\nout, err := Replace(body, cands, reps)","handlingStrategy":"validation","validationCode":"if len(candidates) != len(replacements) { return fmt.Errorf(\"candidate/replacement mismatch: %d vs %d\", len(candidates), len(replacements)) }","typeGuard":null,"tryCatchPattern":"out, err := jsonsplice.Replace(body, cands, reps)\nif err != nil {\n    if strings.Contains(err.Error(), \"replacements for\") {\n        log.Printf(\"splice count mismatch: cands=%d reps=%d\", len(cands), len(reps))\n    }\n    return nil, err\n}","preventionTips":["Always construct candidates and replacements in the same loop","Filter with paired structs, then split into the two slices","Return the original body when nothing changed instead of calling Replace"],"tags":["json","go","byte-splice","argument-count"],"backgroundTag":"invalid-argument-value","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"}