Repository navigation
Repair bare unquoted dates in resolve_query's decomposition output - #2186
Conversation
Measures what the decomposition sub-call actually emits before deciding whether to repair anything, per the issue's first acceptance criterion. Drives the real exec_resolve_query over a corpus of phrasings and captures each decomposition's raw text through the existing NODESPACE_PROMPT_DUMP hook, which already names resolve_query as a covered caller — so no new production instrumentation is needed and the measured prompt is the one production builds. Co-Authored-By: Claude <noreply@anthropic.com>
…2185) The issue asked, first, whether the decomposition sub-call actually emits malformed filter shapes before adding a repair for one. Measured it: 33 decompositions over a corpus that deliberately over-samples the shapes the four tool-argument repairs exist for. All four came back inert on this path — zero over-quoted keys, zero leaked special-token keys, zero deny_unknown_fields drops, zero scalar-valued 'in'. So the proposed scalar-'in' normalization is declined: nothing here emits that shape, and #2182's lesson was not to add a repair for a shape nothing emits. Two corrections to the issue's reasoning, both measured: - Its stated bound ('the decomposition prompt never instructs in') does not hold. The model emits 'in' anyway, 6 of 21 filters, on multi-value enum phrasings — always with a proper JSON array, so the conclusion survives but the reason given for it would not protect a future reader who widened the prompt's operator vocabulary. - The path's real defect is one the issue did not anticipate: a bare unquoted YYYY-MM-DD value ({"operator":"lt","value":2026-08-19}), emitted 3/3 reps for 'show me the overdue ones'. serde_json rejects the WHOLE object, exec_resolve_query falls back to an empty resolution, and the search degrades to a bare type listing — so a request that should resolve to one node returns multiple_matches over every node of the type. Well formed, and wrong. That failure is upstream of every repair: all four, and coerce_filter_value_to_field_type, operate on an already-parsed Value, and parsing is what failed. The issue's 'more uniform' alternative — routing this through repair_parsed_tool_arguments — would not have caught it either. Fixes it where it is reachable: quote_bare_date_literals repairs the raw text before from_str. Narrow and unambiguous — a bare YYYY-MM-DD is not valid JSON under any reading, the scan skips string literals, and longer digit runs are declined rather than sliced. Allocation-free for already-valid output. Verified against the real model: the same malformed emission now resolves to the one overdue invoice, 3/3 reps (corpus resolution 12/33 -> 15/33, entirely that request). Also logs the whole-resolution fallback, which was silent, and records the measured coverage boundary at the filter-parsing site. Closes #2185 Co-Authored-By: Claude <noreply@anthropic.com>
Pragmatic Code Review — PR #2186Review Type: Initial Review Review SummaryThis is a strong change, and the strength is in the part that is easiest to skip: it answered the issue's first acceptance criterion empirically before writing a line of repair, then declined the fix the issue proposed because the measurement did not support it. That is the exact discipline #2182 established, applied to its own successor. It also does the harder thing — it reports that the issue's stated reason for the bound was wrong (the model emits I independently re-derived the central risk rather than taking the doc comment's word for it. I also verified the substrate the harness rests on:
Requirements Check — Issue #2185 Acceptance Criteria
Is declining AC2 correct? Yes, and it is the right call for the right reason. The issue itself offered the negative-result exit, so the risk is that declining was a convenience. It was not:
On the defect actually found: the bare- Findings🟡 Important📁 The comment reads: "Reproduced rather than shared because the production helper is private; keeping it identical is what makes the classification describe production's behavior and not this test's." It has already drifted:
I verified the two are behaviorally equivalent on the cases that matter (prose wrapping, markdown fences, nested objects, But the comment is now load-bearing documentation that is false, which is worse than no comment: the next reader has an explicit assurance of identity that a
Principle: DRY, applied to invariants. A comment claiming an invariant that the code does not hold is a liability — it stops the next reader from checking. 🟢 Suggestions📁 The coverage-boundary comment states the four repairs came back inert across "33 decompositions." But The Principle: state a measurement's scope alongside its result; an unqualified denominator invites over-reading. 📁 The repair is correctly placed — pre-parse is the only place this shape is reachable, and the doc comment argues that well. My question is whether it should be the only move. The date case currently has only the backstop. And the prompt is the proximate cause — your own comment says so: it instructs the model to resolve to "a concrete YYYY-MM-DD value", and
Reasonable to defer. Worth a line in the comment noting the prompt-side option was considered and why it was not taken, so the next reader does not re-litigate it. Principle: fix causes where you can and symptoms where you must — the codebase already documents this exact pairing for the adjacent case. 📁 harness:404, 507 — This is the only
📁 harness:459, 469 — the dump file is fully re-read and re-parsed twice per rep.
Nitpicks
What This Gets RightWorth naming explicitly, because these are the parts most often skipped:
RecommendationAPPROVE. The measurement is sound, the decline is evidence-backed rather than convenient, the repair is correctly scoped and placed at the only layer where it is reachable, and I could not construct an input that makes it produce wrong-but-parseable output. Net-positive by a wide margin. Before merge: commit the three uncommitted tests (nit 3). The |
malibio
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
…og raw output Review findings from PR #2186. - 🟡 The harness duplicated production's extract_json_object and its comment claimed the copy was identical. It was only behaviorally equivalent (i32 vs usize depth, byte iteration vs char_indices) — false load-bearing documentation, and exactly the drift that comment predicted, arriving before merge. Removed the copy and made the production helper pub so the harness imports it. The measurement is only a claim about production if it extracts the slice production extracts. Re-ran the live corpus after the swap: identical tally, resolved 15/33 unchanged. - 🟢 The coverage-boundary comment cited 33 decompositions as though all were inspected. classify_filter only runs on decompositions that parsed, so the bare-date cases contributed no filters. Now states the honest denominator — 21 classifiable filters — and why it is narrower. - 🟢 The fallback warn! logged the post-repair slice. When it fires the repairs did not rescue the output, so the raw emission is the more diagnostic of the two — it names the shape a future repair would have to cover. Now logs the raw `text` instead. Also commits three boundary tests written while the review ran: UTF-8 safety around multibyte payloads (the scan indexes bytes but slices &str), a date at end-of-input with no trailing byte, and escaped quotes inside strings. Skipped, with reasons: - Prompt-side hint demanding quoted dates: additive follow-up rather than a fix to this defect, and out of scope here. The repair is the guarantee; a prompt hint is probabilistic. - O(n^2) dump re-read: noise against a 5GB model load, per the review itself. - set_var thread-safety: crate is edition 2021 so no unsafe applies, and the sole #[ignore]d test owns its binary. Co-Authored-By: Claude <noreply@anthropic.com>
Review addressed —
|
Closes #2185.
The issue's first acceptance criterion was to decide with evidence whether the
resolve_querydecomposition sub-call actually emits malformed filter shapes, and to record a negative result rather than add a repair for a shape nothing emits. Measured it first. The measurement changed the answer.What was measured
packages/agent/tests/live_resolve_query_decomposition_shapes.rs—#[ignore]d, loads the GGUF. Drives the realexec_resolve_queryover a corpus of 11 phrasings × 3 reps (33 decompositions, 21 emitted filters) against the locked E4B model, capturing each decomposition's raw text through the existingNODESPACE_PROMPT_DUMPhook.Two deliberate choices:
nlp-engine'schat/prompt_dump.rsalready namesresolve_queryas a covered caller and already dumps both prompt and raw response.The corpus over-samples the shapes the repairs exist for — multi-value enum phrasings, identifier-vs-update-target sentences, a comma-bearing stored value — rather than sampling typical traffic, so a null result means something.
Finding 1 — the predicted malformations are absent; the issue's stated reason is wrong
All four repairs measured inert on this path. The issue's proposed fix (normalize the scalar-
incase atcoerce_filter_value_to_field_type) is therefore declined — that is AC3's negative result, and adding the repair anyway would be exactly the mistake AC3 exists to prevent.But the issue's reason — "the decomposition prompt never instructs
in, so the shape is not reachable" — does not hold. The prompt doesn't instruct it, and the model emitsinanyway, 6 of 21 filters:{"property": "status", "operator": "in", "value": ["checked_out", "in_repair"]}Always a proper JSON array, never a comma-joined scalar. So the conclusion survives and the reason doesn't — worth correcting in place, because that reason is what a future reader would rely on when widening the prompt's operator vocabulary, and it would not protect them.
Finding 2 — a real, deterministic defect the issue did not anticipate
unparseable_output: 3is one request, 3/3 reps, byte-identical — "Show me the overdue ones":{"query": "", "filters": [{"type":"property","operator":"lt","property":"due_date","value":2026-08-19}]}A bare unquoted date.
serde_jsonreads2026as a number, then fails on-08-19, rejecting the whole object — not just that filter.exec_resolve_queryfalls back to{"query":"","filters":[]}and the search degrades to a bare type listing.Observed end to end: the request returned
multiple_matchesover both invoices, including the one that is not overdue, instead of resolving the single overdue one. Well-formed output, and wrong — the silent-narrowing hazard the issue flags, arriving through a wider door than the one it was watching.The prompt provokes it directly: it instructs resolving a relative date to "a concrete YYYY-MM-DD value", and unquoted
YYYY-MM-DDis what came back.This is upstream of every repair. All four, and
coerce_filter_value_to_field_type, operate on an already-parsedValue; parsing is what failed. The issue's "more uniform" alternative — routing this output throughrepair_parsed_tool_arguments— would not have caught it either, for the same reason. And the proposed seam can't reach it:coerce_filter_value_to_field_typeruns per-filter-item, and there are no filter items.The fix
quote_bare_date_literalsrepairs the raw text beforefrom_str, the only place this is reachable. Safe because the shape isn't expressible as valid JSON under any reading — a bare2026-08-19is never a legal JSON value, so nothing legitimate is rewritten:"query": "due 2026-08-19") are untouched2026-08-19-01) rather than slicing ten characters out and inventing a valueCow::Borrowed) for already-valid outputDeliberately not generalized to other unquoted scalars — a bare word could be a typo'd
true, an enum member, or a malformed number, and choosing between those is interpretation, not repair.Verification against the real model
Same malformed emission from the model, different outcome:
multiple_matchesover both invoicesresolved: true→ the overdue one, 3/3 repsThe +3 is exactly the three reps of that request; nothing else moved.
Also in this PR
Testing
exec_resolve_query, asserting on resolved identity — the pre-fix behavior returned a well-formedmultiple_matches, so only an identity assertion catches the regression; asserting the helper alone would pass with the call site unwiredbun run test:allclean: frontend 227 files / 4794 tests (matches baseline), browser 2/24, all Rust suites passbun run quality:fixclean, clippy-D warningspasses🤖 Generated with Claude Code
https://claude.ai/code/session_01TDtMcV2chVWTDbMFerp5Vs