fix: repair unescaped quotes in LLM JSON responses #45

Merged
rodin merged 3 commits from fix/json-repair into main 2026-05-05 12:40:39 +00:00
Owner

Problem

Sonnet sometimes emits JSON with unescaped double quotes inside string values:

{"finding": "The comment says OTP_VERSION is the major version (e.g. "28")"}

This breaks json.Unmarshal with invalid character '2' after object key:value pair.

Seen in gargoyle CI (sonnet-review on PR #551, job 3173).

Fix

Add a repairJSON fallback in ParseResponse. When the initial parse fails, the repair function walks the JSON character-by-character, tracking string state, and escapes any interior quotes that aren't followed by structural characters (,, }, ], :).

Tests

  • TestParseResponse_UnescapedQuotesInStrings — reproduces the exact CI failure
  • TestRepairJSON_NoOpOnValid — confirms no modification of valid JSON
  • TestRepairJSON_FixesUnescapedQuotes — verifies repair produces parseable output
## Problem Sonnet sometimes emits JSON with unescaped double quotes inside string values: ```json {"finding": "The comment says OTP_VERSION is the major version (e.g. "28")"} ``` This breaks `json.Unmarshal` with `invalid character '2' after object key:value pair`. Seen in gargoyle CI (sonnet-review on PR #551, job 3173). ## Fix Add a `repairJSON` fallback in `ParseResponse`. When the initial parse fails, the repair function walks the JSON character-by-character, tracking string state, and escapes any interior quotes that aren't followed by structural characters (`,`, `}`, `]`, `:`). ## Tests - `TestParseResponse_UnescapedQuotesInStrings` — reproduces the exact CI failure - `TestRepairJSON_NoOpOnValid` — confirms no modification of valid JSON - `TestRepairJSON_FixesUnescapedQuotes` — verifies repair produces parseable output
rodin added 1 commit 2026-05-03 16:47:33 +00:00
fix: repair unescaped quotes in LLM JSON responses
CI / test (pull_request) Successful in 13s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Failing after 13s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Failing after 13s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Failing after 12s
80a9a7675b
LLMs (especially Sonnet) sometimes emit JSON with unescaped double
quotes inside string values, e.g. (e.g. "28") instead of properly
escaping them. This caused parse failures in CI.

Add a repairJSON fallback that uses a character-by-character scanner
to identify interior quotes (those not followed by structural JSON
characters) and escape them before retrying the parse.

Fixes sonnet-review failures on gargoyle PR #551.
sonnet-review-bot reviewed 2026-05-04 21:36:20 +00:00
sonnet-review-bot left a comment
First-time contributor

test

test
sonnet-review-bot reviewed 2026-05-04 21:36:24 +00:00
sonnet-review-bot left a comment
First-time contributor

test

test
sonnet-review-bot requested changes 2026-05-04 21:37:19 +00:00
Dismissed
sonnet-review-bot left a comment
First-time contributor

Review: fix: repair unescaped quotes in LLM JSON responses

The fix addresses a real, observed failure mode (Sonnet emitting unescaped quotes inside JSON string values), and the structural approach — character-by-character walking with string-state tracking — is the right level of tool for this problem. The core algorithm is sound for the common case the PR targets. However, there are two correctness gaps that will silently produce broken output in edge cases, and the test coverage only covers the happy path for the tricky scenario.


🔴 Bug: Interior quoted word immediately before a comma

File: review/parser.go, repairJSON

The lookahead heuristic treats a " followed by , (after optional whitespace) as a structural closing quote. This misclassifies an interior quote that happens to precede a comma within the string value.

Reproducer (verified — this test currently fails):

input := `{"msg": "say "yes", and go"}`
repaired := repairJSON(input)
// repaired: {"msg": "say \"yes", and go"}
// json.Unmarshal → error: invalid character 'a' looking for beginning of object key string

The repair function correctly escapes the opening quote before yes, but then sees the closing " after yes — followed by , — and treats it as the structural end-of-string. The remainder , and go"} is then outside any string context, producing malformed JSON.

The same failure class applies when an interior quoted word ends just before ], since ] is also in the structural lookahead set.

Root cause: ,, }, ], and : are all valid string content (code snippets, key-value descriptions, prose). They cannot reliably distinguish a structural closing quote from an interior one.


🔴 Bug: JSON-shaped syntax in string values

When an LLM describes a struct or map literal in a finding (e.g. "input map {"key": "val"} caused error"), the " after val has } as lookahead and will be treated as a structural close, ending the outer string prematurely.

Given that this tool reviews code, and the LLMs it calls regularly describe map/struct literals, JSON blobs, or key-value pairs in finding text, this is a realistic failure scenario.


🟡 Test coverage misses the known-failing edge case

TestRepairJSON_FixesUnescapedQuotes uses {"msg": "use "foo" here"} — the interior quotes aren't adjacent to any structural character, so this passes cleanly. TestParseResponse_UnescapedQuotesInStrings also passes because the interior "28" is followed by ), not , or }.

Neither test covers the comma-adjacency failure. The following test currently fails with code as written:

func TestRepairJSON_InteriorQuoteBeforeComma(t *testing.T) {
    input := `{"msg": "say "yes", and go"}`
    result := repairJSON(input)
    var m map[string]interface{}
    if err := json.Unmarshal([]byte(result), &m); err != nil {
        t.Fatalf("repaired JSON should parse, got: %v\nrepaired: %s", err, result)
    }
}

What works well

  • Escape sequence passthrough (\ handling) is correct — pre-existing escapes are preserved without double-escaping.
  • Rune-level walking avoids byte-level bugs with multibyte content in string values.
  • strings.Builder with Grow — idiomatic and allocation-efficient.
  • Original error preserved in the fallback return — diagnostics remain actionable when repair also fails.
  • No-op on valid JSON — confirmed by TestRepairJSON_NoOpOnValid.
  • The "try repair as fallback" pattern is architecturally correct: if repair itself fails, the original first-parse error still propagates.

Summary

The algorithm correctly handles the exact CI failure that motivated this PR ((e.g. "28") — interior quotes not adjacent to structural characters). For the most common LLM unescaped-quote pattern it works. But the lookahead heuristic has a fundamental ambiguity: ,, }, ], and : are valid string content, so a quoted word ending just before one of them causes the algorithm to close the string prematurely — producing output more broken than the input.

Requesting changes on the two correctness bugs. If the intent is "handles the observed pattern, not a general-purpose JSON repair," the test suite should explicitly cover the known edge cases (including currently failing ones) so future maintainers understand the boundaries.

— sonnet-review-bot

## Review: `fix: repair unescaped quotes in LLM JSON responses` The fix addresses a real, observed failure mode (Sonnet emitting unescaped quotes inside JSON string values), and the structural approach — character-by-character walking with string-state tracking — is the right level of tool for this problem. The core algorithm is sound for the **common case** the PR targets. However, there are two correctness gaps that will silently produce broken output in edge cases, and the test coverage only covers the happy path for the tricky scenario. --- ### 🔴 Bug: Interior quoted word immediately before a comma **File:** `review/parser.go`, `repairJSON` The lookahead heuristic treats a `"` followed by `,` (after optional whitespace) as a structural closing quote. This misclassifies an interior quote that happens to precede a comma within the string value. **Reproducer (verified — this test currently fails):** ```go input := `{"msg": "say "yes", and go"}` repaired := repairJSON(input) // repaired: {"msg": "say \"yes", and go"} // json.Unmarshal → error: invalid character 'a' looking for beginning of object key string ``` The repair function correctly escapes the opening quote before `yes`, but then sees the closing `"` after `yes` — followed by `,` — and treats it as the structural end-of-string. The remainder `, and go"}` is then outside any string context, producing malformed JSON. The same failure class applies when an interior quoted word ends just before `]`, since `]` is also in the structural lookahead set. **Root cause:** `,`, `}`, `]`, and `:` are all valid string content (code snippets, key-value descriptions, prose). They cannot reliably distinguish a structural closing quote from an interior one. --- ### 🔴 Bug: JSON-shaped syntax in string values When an LLM describes a struct or map literal in a finding (e.g. `"input map {"key": "val"} caused error"`), the `"` after `val` has `}` as lookahead and will be treated as a structural close, ending the outer string prematurely. Given that this tool reviews code, and the LLMs it calls regularly describe map/struct literals, JSON blobs, or key-value pairs in finding text, this is a realistic failure scenario. --- ### 🟡 Test coverage misses the known-failing edge case `TestRepairJSON_FixesUnescapedQuotes` uses `{"msg": "use "foo" here"}` — the interior quotes aren't adjacent to any structural character, so this passes cleanly. `TestParseResponse_UnescapedQuotesInStrings` also passes because the interior `"28"` is followed by `)`, not `,` or `}`. Neither test covers the comma-adjacency failure. The following test currently **fails** with code as written: ```go func TestRepairJSON_InteriorQuoteBeforeComma(t *testing.T) { input := `{"msg": "say "yes", and go"}` result := repairJSON(input) var m map[string]interface{} if err := json.Unmarshal([]byte(result), &m); err != nil { t.Fatalf("repaired JSON should parse, got: %v\nrepaired: %s", err, result) } } ``` --- ### ✅ What works well - **Escape sequence passthrough** (`\` handling) is correct — pre-existing escapes are preserved without double-escaping. - **Rune-level walking** avoids byte-level bugs with multibyte content in string values. - **`strings.Builder` with `Grow`** — idiomatic and allocation-efficient. - **Original error preserved** in the fallback `return` — diagnostics remain actionable when repair also fails. - **No-op on valid JSON** — confirmed by `TestRepairJSON_NoOpOnValid`. - The "try repair as fallback" pattern is architecturally correct: if repair itself fails, the original first-parse error still propagates. --- ### Summary The algorithm correctly handles the exact CI failure that motivated this PR (`(e.g. "28")` — interior quotes not adjacent to structural characters). For the most common LLM unescaped-quote pattern it works. But the lookahead heuristic has a fundamental ambiguity: `,`, `}`, `]`, and `:` are valid string content, so a quoted word ending just before one of them causes the algorithm to close the string prematurely — producing output *more* broken than the input. Requesting changes on the two correctness bugs. If the intent is "handles the observed pattern, not a general-purpose JSON repair," the test suite should explicitly cover the known edge cases (including currently failing ones) so future maintainers understand the boundaries. — sonnet-review-bot
rodin added 1 commit 2026-05-05 04:27:46 +00:00
fix: redesign repairJSON to handle all reviewer-reported edge cases
CI / test (pull_request) Successful in 11s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Failing after 10s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Failing after 10s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Failing after 11s
25d1a670bf
Rewrites the JSON repair algorithm to address two correctness bugs
identified in code review:

1. Interior quoted word before comma: "say "yes", and go" was
   misidentified as structural close because "," followed the quote.

2. JSON-shaped content in strings: {"key": "val"} inside a string
   value was being parsed as actual JSON structure.

The new approach:
- Distinguishes keys from values (only values need repair)
- Uses first-valid-candidate scan with deep lookahead
- Verifies that after a candidate close, the continuation is not just
  a structural char but a complete valid JSON pattern
- Validates keyword tokens (true/false/null) fully, not just first char
- Checks container closes recursively for valid continuation

Adds comprehensive tests for all reported edge cases plus a complex
combined scenario with nested JSON-like content, quoted words before
commas, and multiple failure modes in one string.
Author
Owner

Addressing Review Findings

The review was submitted against an earlier version of the algorithm. The current code already resolves both identified bugs:

Bug 1: Interior quoted word before comma

The findClosingQuote function now uses first-valid-candidate with deep structural lookahead — it tries each " from first to last and picks the earliest one where isValidJSONAfterClose confirms valid JSON continuation. For "say "yes", and go", the " after yes fails lookahead (, and go" is not a valid JSON token after a comma), so the algorithm correctly skips it and finds the true closing quote at the end.

Test: TestRepairJSON_InteriorQuoteBeforeComma — passes, verifying content = say "yes", and go.

Bug 2: JSON-shaped syntax in string values

Same mechanism: isValidJSONAfterClose recursively validates container closes. For "input map {"key": "val"} caused error", the interior " characters fail deep lookahead (what follows them does not form valid top-level JSON continuation from the outer object's perspective).

Test: TestRepairJSON_InteriorQuoteBeforeCloseBrace — passes.

Full test suite

All 8 repair tests pass. All review-bot tests pass (go test ./...). The algorithm correctly handles the motivating CI failure, both reviewer edge cases, multi-field JSON, pre-escaped quotes, and complex nested content.

The review's findings are resolved. Requesting re-review or dismissal.

— Rodin

## Addressing Review Findings The review was submitted against an earlier version of the algorithm. The current code already resolves both identified bugs: ### Bug 1: Interior quoted word before comma The `findClosingQuote` function now uses **first-valid-candidate** with deep structural lookahead — it tries each `"` from first to last and picks the earliest one where `isValidJSONAfterClose` confirms valid JSON continuation. For `"say "yes", and go"`, the `"` after `yes` fails lookahead (`, and go"` is not a valid JSON token after a comma), so the algorithm correctly skips it and finds the true closing quote at the end. **Test:** `TestRepairJSON_InteriorQuoteBeforeComma` — passes, verifying content = `say "yes", and go`. ### Bug 2: JSON-shaped syntax in string values Same mechanism: `isValidJSONAfterClose` recursively validates container closes. For `"input map {"key": "val"} caused error"`, the interior `"` characters fail deep lookahead (what follows them does not form valid top-level JSON continuation from the outer object's perspective). **Test:** `TestRepairJSON_InteriorQuoteBeforeCloseBrace` — passes. ### Full test suite All 8 repair tests pass. All review-bot tests pass (`go test ./...`). The algorithm correctly handles the motivating CI failure, both reviewer edge cases, multi-field JSON, pre-escaped quotes, and complex nested content. The review's findings are resolved. Requesting re-review or dismissal. — Rodin
aweiker dismissed sonnet-review-bot's review 2026-05-05 05:58:46 +00:00
Reason:

Review was against earlier version. Both bugs are already fixed in the current code — tests pass.

gpt-review-bot approved these changes 2026-05-05 06:00:53 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Review: PR #45 — JSON repair for unescaped LLM quotes

Verdict: APPROVE

Assessment

This is a well-designed repair algorithm with correct heuristics. The multi-candidate approach (find all unescaped quotes, pick the first that produces valid JSON continuation) is significantly better than the simple "next char is structural" approach.

What's Good

  1. Correct heuristic: first-valid-candidate with deep lookahead. The isValidJSONAfterClose verifies not just the immediate next character but the full structural continuation (container closes, comma+token, EOF). This handles the two reviewer-reported edge cases (quote-before-comma, JSON-shaped string content) correctly.

  2. Only repairs value strings. isValuePosition restricts repair to strings following : — keys are parsed normally. This is the right bias: LLMs get keys right (they're from a fixed vocabulary) but freetext values contain arbitrary content.

  3. Already-escaped sequences preserved. The inner loop skips \\ sequences correctly in both the candidate finder and the output writer.

  4. Fallback is conservative. If repair doesn't help, the original error is returned (not the repair error). The caller sees the real problem.

  5. Tests are thorough. Covers: no-op on valid, basic repair, quote-before-comma (Sonnet's reported bug), JSON-in-string, multiple fields, pre-escaped quotes, and complex nested. All exercise the actual edge cases that motivated the fix.

Findings

NIT: isValidAfterContainerClose is recursive — deeply nested closing brackets (}}}}}) could stack-overflow on pathological input. In practice, LLM review JSON is 3-4 levels deep max, so this is theoretical. If you want defense, add a depth counter or convert to iterative.

NIT: The findClosingQuote comment says "returns the FIRST one that produces valid JSON continuation" but the variable name candidates might lead someone to think it tries last-first. The iteration is first-to-last (correct) — the comment and code agree, just noting the earlier version's different strategy.

No blocking issues.

Signed: gpt-review

## Review: PR #45 — JSON repair for unescaped LLM quotes **Verdict: ✅ APPROVE** ### Assessment This is a well-designed repair algorithm with correct heuristics. The multi-candidate approach (find all unescaped quotes, pick the first that produces valid JSON continuation) is significantly better than the simple "next char is structural" approach. ### What's Good 1. **Correct heuristic: first-valid-candidate with deep lookahead.** The `isValidJSONAfterClose` verifies not just the immediate next character but the full structural continuation (container closes, comma+token, EOF). This handles the two reviewer-reported edge cases (quote-before-comma, JSON-shaped string content) correctly. 2. **Only repairs value strings.** `isValuePosition` restricts repair to strings following `:` — keys are parsed normally. This is the right bias: LLMs get keys right (they're from a fixed vocabulary) but freetext values contain arbitrary content. 3. **Already-escaped sequences preserved.** The inner loop skips `\\` sequences correctly in both the candidate finder and the output writer. 4. **Fallback is conservative.** If repair doesn't help, the original error is returned (not the repair error). The caller sees the real problem. 5. **Tests are thorough.** Covers: no-op on valid, basic repair, quote-before-comma (Sonnet's reported bug), JSON-in-string, multiple fields, pre-escaped quotes, and complex nested. All exercise the actual edge cases that motivated the fix. ### Findings **NIT:** `isValidAfterContainerClose` is recursive — deeply nested closing brackets (`}}}}}`) could stack-overflow on pathological input. In practice, LLM review JSON is 3-4 levels deep max, so this is theoretical. If you want defense, add a depth counter or convert to iterative. **NIT:** The `findClosingQuote` comment says "returns the FIRST one that produces valid JSON continuation" but the variable name `candidates` might lead someone to think it tries last-first. The iteration is first-to-last (correct) — the comment and code agree, just noting the earlier version's different strategy. **No blocking issues.** Signed: **gpt-review**
sonnet-review-bot approved these changes 2026-05-05 06:01:55 +00:00
sonnet-review-bot left a comment
First-time contributor

Code Review — repairJSON fallback

All tests pass. The implementation is sound for the stated problem. Here's the full analysis:


Correctness

The algorithm is correct for its target case: LLM-emitted JSON where unescaped " appear inside string values (after :).

isValuePosition correctly restricts repair to : predecessors. Array elements (preceded by [ or ,) are NOT repaired — this is a documented limitation that doesn't affect review-bot in practice, since all free-text fields (finding, summary, etc.) are object values.

findClosingQuote iterates candidates first-to-last and returns the first one that produces valid JSON continuation. The structural lookahead in isValidJSONAfterClose correctly rejects mid-string positions (followed by a letter or non-structural char) and accepts real closes (followed by }, ], ,).

isValidAfterContainerClose uses recursion for nested closes — this handles }} and ]} correctly without blowing the stack for typical JSON depths.


⚠️ Documentation inconsistency (NIT)

repairJSON's doc comment says "pick the LAST one that leaves valid JSON structure", and line 136 repeats "last-valid-candidate heuristic". The actual code in findClosingQuote says "FIRST one" and iterates first-to-last, which is what the code actually does.

First-valid is the correct strategy. The doc comments should say "first" not "last":

// repairJSON: "pick the FIRST one that produces valid JSON continuation"
// (findClosingQuote already says "FIRST" — the two callers just need updating)

⚠️ Off-by-one in escape passthrough (NIT)

In the write loop inside repairJSON:

if ch == '\\' && j+1 < closeIdx {

The condition j+1 < closeIdx excludes the case where a backslash is the last character before the closing quote (j+1 == closeIdx). In that position the lone \ is written without consuming the next character. In practice this is harmless — findClosingQuote also skips \" pairs when collecting candidates, so a trailing \" won't be misidentified as the close. Worth tightening to j+1 <= closeIdx for clarity.


Edge cases handled well

  • Pre-escaped quotes (\") — correctly passed through without double-escaping (verified)
  • Empty string values ("") — no candidates → no-op, correct
  • Multiple fields — each value string is independently repaired
  • Nested object values in arrays (the real CI failure: findings[n].finding) — works because the text field is a VALUE after :
  • Valid JSON unchangedTestRepairJSON_NoOpOnValid confirmed no spurious modifications

Code quality

Idiomatic Go throughout. strings.Builder with pre-allocated capacity is the right choice. Helpers are small and well-named. isJSONTokenStart correctly validates full keyword tokens (true/false/null) rather than just first-char matching.


Tests

Good coverage of the key scenarios. TestParseResponse_UnescapedQuotesInStrings reproduces the exact CI failure. TestRepairJSON_InteriorQuoteBeforeComma (the "yes", and go case) is particularly valuable — comma-adjacent quotes are the hardest case for naive heuristics.

One gap: no test covering the path where repair also fails and ParseResponse returns the original error. Not blocking, but would confirm the fallback returns the right error message.


Verdict

The fix is correct, well-tested, and addresses the real CI failure. The first/last doc inconsistency is the only thing worth fixing. Approving — can be squashed in or land as a follow-up.

## Code Review — `repairJSON` fallback All tests pass. The implementation is sound for the stated problem. Here's the full analysis: --- ### ✅ Correctness The algorithm is correct for its target case: LLM-emitted JSON where unescaped `"` appear inside string **values** (after `:`). **`isValuePosition`** correctly restricts repair to `:` predecessors. Array elements (preceded by `[` or `,`) are NOT repaired — this is a documented limitation that doesn't affect review-bot in practice, since all free-text fields (`finding`, `summary`, etc.) are object values. **`findClosingQuote`** iterates candidates first-to-last and returns the first one that produces valid JSON continuation. The structural lookahead in `isValidJSONAfterClose` correctly rejects mid-string positions (followed by a letter or non-structural char) and accepts real closes (followed by `}`, `]`, `,`). **`isValidAfterContainerClose`** uses recursion for nested closes — this handles `}}` and `]}` correctly without blowing the stack for typical JSON depths. --- ### ⚠️ Documentation inconsistency (NIT) `repairJSON`'s doc comment says **"pick the LAST one that leaves valid JSON structure"**, and line 136 repeats "last-valid-candidate heuristic". The actual code in `findClosingQuote` says **"FIRST one"** and iterates first-to-last, which is what the code actually does. First-valid is the correct strategy. The doc comments should say "first" not "last": ``` // repairJSON: "pick the FIRST one that produces valid JSON continuation" // (findClosingQuote already says "FIRST" — the two callers just need updating) ``` --- ### ⚠️ Off-by-one in escape passthrough (NIT) In the write loop inside `repairJSON`: ```go if ch == '\\' && j+1 < closeIdx { ``` The condition `j+1 < closeIdx` excludes the case where a backslash is the **last character before the closing quote** (`j+1 == closeIdx`). In that position the lone `\` is written without consuming the next character. In practice this is harmless — `findClosingQuote` also skips `\"` pairs when collecting candidates, so a trailing `\"` won't be misidentified as the close. Worth tightening to `j+1 <= closeIdx` for clarity. --- ### ✅ Edge cases handled well - **Pre-escaped quotes** (`\"`) — correctly passed through without double-escaping (verified) - **Empty string values** (`""`) — no candidates → no-op, correct - **Multiple fields** — each value string is independently repaired - **Nested object values** in arrays (the real CI failure: `findings[n].finding`) — works because the text field is a VALUE after `:` - **Valid JSON unchanged** — `TestRepairJSON_NoOpOnValid` confirmed no spurious modifications --- ### ✅ Code quality Idiomatic Go throughout. `strings.Builder` with pre-allocated capacity is the right choice. Helpers are small and well-named. `isJSONTokenStart` correctly validates full keyword tokens (`true`/`false`/`null`) rather than just first-char matching. --- ### ✅ Tests Good coverage of the key scenarios. `TestParseResponse_UnescapedQuotesInStrings` reproduces the exact CI failure. `TestRepairJSON_InteriorQuoteBeforeComma` (the `"yes", and go` case) is particularly valuable — comma-adjacent quotes are the hardest case for naive heuristics. One gap: no test covering the path where repair also fails and `ParseResponse` returns the original error. Not blocking, but would confirm the fallback returns the right error message. --- ### Verdict The fix is correct, well-tested, and addresses the real CI failure. The first/last doc inconsistency is the only thing worth fixing. Approving — can be squashed in or land as a follow-up.
rodin added 1 commit 2026-05-05 06:15:09 +00:00
ci: retrigger after LLM_BASE_URL secret fix
CI / test (pull_request) Successful in 13s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 20s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 1m27s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m52s
489457c184
gpt-review-bot approved these changes 2026-05-05 06:15:43 +00:00
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

The patch introduces a targeted, robust approach for repairing common malformed JSON from LLMs, and thoroughly tests both edge cases and correctness. The implementation is careful and well-contained, with no impact to valid input.

Recommendation

APPROVE — This is an excellent, production-grade fix for the LLM unescaped quote bug. The repairJSON approach is careful to only intervene for value strings, and the fallback is only attempted if the initial unmarshal fails, minimizing risk. Test coverage demonstrates a deep consideration of edge cases. The implementation fits Go idioms, maintains package purity, and passes CI. You should merge this as the solution to the observed CI failures.


Review by gpt


Evaluated against 489457c1

# Gpt Review ## Summary The patch introduces a targeted, robust approach for repairing common malformed JSON from LLMs, and thoroughly tests both edge cases and correctness. The implementation is careful and well-contained, with no impact to valid input. ## Recommendation **APPROVE** — This is an excellent, production-grade fix for the LLM unescaped quote bug. The repairJSON approach is careful to only intervene for value strings, and the fallback is only attempted if the initial unmarshal fails, minimizing risk. Test coverage demonstrates a deep consideration of edge cases. The implementation fits Go idioms, maintains package purity, and passes CI. You should merge this as the solution to the observed CI failures. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 489457c1*
sonnet-review-bot approved these changes 2026-05-05 06:16:50 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

The fallback JSON repair logic is a pragmatic and well-contained improvement that addresses the observed LLM output issue. The implementation is careful, and the added tests cover key edge cases and regressions. A couple of minor documentation/heuristic clarifications could further improve maintainability.

Findings

# Severity File Line Finding
1 [NIT] review/parser.go 79 The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy.
2 [MINOR] review/parser.go 0 isValuePosition only treats strings immediately following ':' as values, so unescaped quotes in array string elements (e.g., "key": ["foo "bar""]) will not be repaired. Consider extending the heuristic to also handle array element positions when inside a value context if this becomes a real-world failure mode.
3 [NIT] review/parser.go 0 In findClosingQuote, backslash escape handling skips only the single character after '\'. This is sufficient for avoiding escaped quotes, but may be misleading for readers with respect to multi-rune escapes like \uXXXX. A brief comment clarifying that only escaped quotes matter here (and that other escapes are intentionally not fully parsed) would improve readability.

Recommendation

APPROVE — The change effectively hardens JSON parsing against a real LLM failure mode without impacting valid inputs, and the tests demonstrate good coverage of edge cases. Given CI is green and no major issues were found, this is good to merge. Consider polishing the documentation to match the implemented heuristic and, if future inputs require it, extend the value detection to handle string elements in arrays. Otherwise, the current solution is practical and maintainable.


Review by sonnet


Evaluated against 489457c1

# Sonnet Review ## Summary The fallback JSON repair logic is a pragmatic and well-contained improvement that addresses the observed LLM output issue. The implementation is careful, and the added tests cover key edge cases and regressions. A couple of minor documentation/heuristic clarifications could further improve maintainability. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [NIT] | `review/parser.go` | 79 | The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy. | | 2 | [MINOR] | `review/parser.go` | 0 | isValuePosition only treats strings immediately following ':' as values, so unescaped quotes in array string elements (e.g., "key": ["foo "bar""]) will not be repaired. Consider extending the heuristic to also handle array element positions when inside a value context if this becomes a real-world failure mode. | | 3 | [NIT] | `review/parser.go` | 0 | In findClosingQuote, backslash escape handling skips only the single character after '\\'. This is sufficient for avoiding escaped quotes, but may be misleading for readers with respect to multi-rune escapes like \uXXXX. A brief comment clarifying that only escaped quotes matter here (and that other escapes are intentionally not fully parsed) would improve readability. | ## Recommendation **APPROVE** — The change effectively hardens JSON parsing against a real LLM failure mode without impacting valid inputs, and the tests demonstrate good coverage of edge cases. Given CI is green and no major issues were found, this is good to merge. Consider polishing the documentation to match the implemented heuristic and, if future inputs require it, extend the value detection to handle string elements in arrays. Otherwise, the current solution is practical and maintainable. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 489457c1*
@@ -74,3 +79,230 @@ func extractJSON(s string) string {
s = strings.TrimSpace(s)
First-time contributor

[NIT] The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy.

**[NIT]** The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy.
security-review-bot requested review from security-review-bot 2026-05-05 06:17:15 +00:00
security-review-bot approved these changes 2026-05-05 06:17:15 +00:00
security-review-bot left a comment
Collaborator

Security Review

Summary

The fallback JSON repair logic addresses malformed LLM outputs without introducing obvious exploitable vulnerabilities. Error handling and validation remain intact, and CI has passed.

Findings

# Severity File Line Finding
1 [MINOR] review/parser.go 36 The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages.
2 [MINOR] review/parser.go 187 The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined.

Recommendation

APPROVE — Approve this change. The repairJSON fallback improves robustness against malformed LLM JSON and does not introduce clear security vulnerabilities. For defense-in-depth, consider truncating or redacting the raw response in error messages to avoid leaking large or sensitive content, and place pragmatic bounds on the repair logic (e.g., only attempt repair up to a size limit and/or cap the number of quote candidates) to mitigate potential resource exhaustion on pathological inputs.


Review by security


Evaluated against 489457c1

# Security Review ## Summary The fallback JSON repair logic addresses malformed LLM outputs without introducing obvious exploitable vulnerabilities. Error handling and validation remain intact, and CI has passed. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `review/parser.go` | 36 | The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages. | | 2 | [MINOR] | `review/parser.go` | 187 | The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined. | ## Recommendation **APPROVE** — Approve this change. The repairJSON fallback improves robustness against malformed LLM JSON and does not introduce clear security vulnerabilities. For defense-in-depth, consider truncating or redacting the raw response in error messages to avoid leaking large or sensitive content, and place pragmatic bounds on the repair logic (e.g., only attempt repair up to a size limit and/or cap the number of quote candidates) to mitigate potential resource exhaustion on pathological inputs. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 489457c1*
@@ -33,0 +33,4 @@
// Try to repair before giving up.
repaired := repairJSON(cleaned)
if err2 := json.Unmarshal([]byte(repaired), &result); err2 != nil {
return nil, fmt.Errorf("parse LLM response as JSON: %w\nRaw response: %s", err, response)
Collaborator

[MINOR] The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages.

**[MINOR]** The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages.
@@ -77,0 +184,4 @@
// value starting at position start (the character after the opening quote).
// It collects all unescaped quote candidates and returns the FIRST one that
// produces valid JSON continuation (deeper lookahead verifies the next token).
func findClosingQuote(runes []rune, start int) int {
Collaborator

[MINOR] The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined.

**[MINOR]** The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined.
rodin merged commit cabbb5a55a into main 2026-05-05 12:40:39 +00:00
Sign in to join this conversation.
5 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rodin/review-bot#45