fix(formula): arm the CEL hydration retry off cel-js's structured code (#6679) - #7097
Conversation
#6679) `isNumericOverloadError` decided whether to run the ADR-0032 §1c hydration retry by testing `/no such overload/i` against `err.message` — the last message-text read in `cel-engine.ts` that armed behaviour after #6223 / PR #6677 closed the same hole in `classifyError`. It now reads `err instanceof EvaluationError && err.code === 'no_such_overload'`, the class-and-code rule `classifyCelFault` already follows one function below. The phrase was reachable from a native throw: the `matches()` stdlib binding is `new RegExp(String(re)).test(...)`, so an uncompilable pattern escapes cel-js unwrapped as a `SyntaxError` echoing the pattern verbatim, from the source or from a row. Measuring that for the fix found a case the filing expected might not exist: when hydration lets the expression short-circuit around the throwing call, the spurious retry succeeds and returns a value where the fault was right. record.s == "5.0" ? matches(record.name, "no such overload(") : false { s: "5.0", name: "x" } -> was: ok/false now: the regex fault record.s == "5.0" ? matches(record.name, "(") : false { s: "5.0", name: "x" } -> the regex fault (unchanged) Both directions pinned in `cel-overload-retry-trigger.test.ts`: a native throw carrying the phrase no longer arms the retry, and a genuine cel-js `no_such_overload` still does (#1530, #1534). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CkomWBsADMsq174GmqhxhC
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
Closes #6679.
What changed
packages/formula/src/cel-engine.ts—isNumericOverloadError, the predicate that decides whethercelEngine.evaluateruns the ADR-0032 §1c hydration retry, no longer reads the error's prose:This was the last
message-text read in the file that armed behaviour after #6223 / PR #6677 deleted the keyword table inclassifyError. The structured answer already lived one function below —classifyCelFaultdoeserr instanceof EvaluationErrorand readserr.code— so this is the same rule applied to the one place still exempt from it.What was measured
Against cel-js
8.0.0as installed, not against the card:EvaluationErrorerr.constructor.name === 'EvaluationError',instanceoftrue'no_such_overload'(raised fromlib/operators.js)matches()escapes cel-js unwrappedmatches(record.name, "no such overload(")→Invalid regular expression: /no such overload(/: Unterminated group— a nativeSyntaxError, no cel-js source highlightmatches(record.name, record.re)withreoff the record — same messageThe pinning against cel-js is itself a test, so an upstream re-code or re-class goes red here rather than silently disarming the retry.
The behaviour change, and a correction to the filing
The change is one-directional, as filed: a native throw whose message merely contains the phrase stops arming the retry. Genuine
no_such_overloadfaults are untouched.The filing recorded this as observation-class — "Today the consequence appears to be nil… What is unmeasured is whether any input exists where a spuriously-armed retry succeeds" — and triage graded it not
target:v17on that basis. Measuring it for the fix found such an input, so the pricing here is a fix rather than a tolerance removal:Evaluation 1 takes the
matches(...)branch and throws natively; the phrase armed the retry; hydration maderecord.sthe number5, so5 == "5.0"went false, the ternary took the other branch,matcheswas never called, and the retry returnedfalse. Two expressions differing only in whether a regex literal happens to contain the phrase disagreed about whether they fault at all. The same shape reproduces with&&and with a date field. This does not change the grading ask — the exposure still needs an expression that short-circuits around the throwing call — but the honest description is no longer "no user-visible symptom".Faults are otherwise unchanged: a native throw carries no cel-js contract, so it is still
runtime(#6223).Tests
New file
packages/formula/src/cel-overload-retry-trigger.test.ts, 7 pins, both directions. Arming is observed without an export by counting record reads — hydration walks the live scope withObject.entriesbefore re-evaluating, so an armed retry always re-reads the record.Reverse-verification — the pins were written before the fix, run against the unmodified file, with red/green predicted per case:
reads2, expected 1)reads2, expected 1)oktrue, expected false)EvaluationError/no_such_overloadNo deviation. All 7 green after the fix.
Scope
Deliberately not touched:
UNSOUND_OVERLOAD_RE(cel-engine.ts:632, consumed at:767offresult.error?.message). It is a different mechanism with its own docblock, parsing operands and an operator out of the text rather than testing for a phrase — triage's "whether:632deserves the same treatment is a separate measurement" still holds, and this PR does not make it.One finding measured while pinning this and filed separately rather than folded in:
hydrateOverloadStrings's docblock claims the retry "can never change a comparison that already evaluated cleanly". That is false independently of this card — on a genuineno_such_overloadfault, a sibling sub-comparison that evaluated cleanly can still be re-interpreted (record.n >= 4 && record.s == "5.0"with{ n: "7", s: "5.0" }returnsfalse, because hydration makesrecord.sthe number5). This PR neither causes nor fixes it.Gates
pnpm lint(ESLint) andpnpm typecheckclean.check:empty-changeset,check:changeset-gate-self-tests,check:adr-anchors,check:error-code-casing,check:nul-bytes,check:doc-authoring,check:published-files,check:objectui-changeset,check:docs-audit-scope,check:required-contextsall pass. Suites:@objectstack/formula568,@objectstack/objectql2786,@objectstack/lint1771,@objectstack/service-automation885 — all passing.Related
#6679 · #6677 / #6223 (same file, same defect family) · #1530 / #1534 (why the retry exists) · ADR-0032 §1c
Generated by Claude Code