Skip to content

fix(formula): hydrate string-serialized numeric fields in CEL comparisons (#1534)#1536

Merged
os-zhuang merged 2 commits into
mainfrom
claude/gallant-goldberg-Kl8mk
Jun 2, 2026
Merged

fix(formula): hydrate string-serialized numeric fields in CEL comparisons (#1534)#1536
os-zhuang merged 2 commits into
mainfrom
claude/gallant-goldberg-Kl8mk

Conversation

@os-zhuang
Copy link
Copy Markdown
Contributor

Summary

Fixes #1534. Record-change flow decision / edge conditions that compare a numeric field against a literal (record.rating >= 4, record.amount > 100000) faulted at runtime whenever the field's value serialized as a stringField.rating(allowHalf)"5.0", Field.currency(scale)"250000.00", Field.percent likewise. Strict CEL raised no such overload: dyn >= int, so the comparison could never be satisfied and the flow silently dead-ended at the decision node (no edge matched, success: true, nothing downstream ran).

Root cause

The bound field value is the raw, string-serialized row value, while the build-time validator (ADR-0032 §1a) and schema treat the field as numeric — a build-vs-runtime mismatch. cel-js is strict: string <op> number has no overload.

Fix

The shared @objectstack/formula CEL engine (cel-engine.ts) now retries an evaluation once with purely-numeric strings hydrated to numbers — but only after a no such overload fault. Key safety property:

  • A comparison that already type-checks (e.g. record.zip == "02134", string == string) never enters the retry path, so it is never re-interpreted.
  • The retry only ever rescues an evaluation that already faulted — it can never change a currently-passing result.
  • A genuinely non-numeric operand (e.g. record.rating >= 4 where rating is "high") still faults loudly; if the retry can't type-check, the original error is surfaced (ADR-0032 §1c/§1d — fail loudly, never swallow).

Because both projection paths the issue calls out route through ExpressionEngine.evaluateservice-automation's evaluateCondition and objectql's applyFormulaPlan — fixing the shared engine resolves them consistently, as requested in the issue's relationship to #1530.

The "silent dead-end" half is already covered on main: evaluateCondition throws an attributed error on a non-ok CEL result (the #1491 fix), which execute() records as a loud failed run. With this change the common numeric-string case no longer faults at all.

Tests

  • packages/formula/src/cel-engine.test.ts — new numeric-string field hydration suite: rating "5.0" >= 4, currency "250000.00" > 100000, percent, previous.* transition gates, compound predicates, unmet compares returning false (not a fault), genuine string equality left untouched, and non-numeric strings still faulting.
  • packages/services/service-automation/src/engine.test.ts — end-to-end: a record-change flow routes through a decision node's record.rating >= 4 edge (rating "5.0") to the hot-lead branch instead of dead-ending.

All formula (82) and automation (98) tests pass.

Scope note

The arithmetic case (record.amount + 100 on a hydrated number) hits a separate cel-js limitation — double + int literal has no overload even for genuine numbers — and is out of scope for this comparison-focused issue.

https://claude.ai/code/session_01FjtHqsTWWPThCP6jsVdvPa


Generated by Claude Code

…sons (#1534)

Numeric fields that serialize as strings (Field.rating -> "5.0",
Field.currency -> "250000.00", Field.percent) made predicates like
`record.rating >= 4` fault under strict CEL (`no such overload: dyn >= int`).
In automation flow decision/edge conditions this silently dead-ended the run
(no edge matched); in objectql applyFormulaPlan it swallowed to null.

The shared CEL engine now retries an evaluation once with purely-numeric
strings hydrated to numbers, but only after a `no such overload` fault — so a
comparison that already type-checks is never re-interpreted, and a genuinely
non-numeric operand still faults loudly. Because both the automation condition
path and the objectql formula path route through ExpressionEngine.evaluate,
both are fixed consistently.

https://claude.ai/code/session_01FjtHqsTWWPThCP6jsVdvPa
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 2, 2026 10:01am

Request Review

Comment thread packages/formula/src/cel-engine.ts Fixed
CodeQL flagged polynomial backtracking in NUMERIC_STRING_RE: the
`\d+\.?\d*` form degrades to adjacent unbounded quantifiers (`\d+\d*`)
when the dot is absent, backtracking on long digit runs. Anchor the
fractional part as a single optional `(?:\.\d*)?` group — same matched
language, no hazard.

https://claude.ai/code/session_01FjtHqsTWWPThCP6jsVdvPa
@os-zhuang os-zhuang marked this pull request as ready for review June 2, 2026 10:10
@os-zhuang os-zhuang merged commit 825ab06 into main Jun 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

3 participants