Summary
Two formula-authoring footguns let an AI write a reasonable-looking Field.formula that passes objectstack build but silently evaluates to null at runtime — the failure class our guardrails are meant to eliminate. Both were found during 9.5.x release-prep testing of example-crm (fixed there in #1927; this issue tracks the systemic prevention).
1. No double × int arithmetic overload (primary)
cel-js (@marcbachmann/cel-js) types a record field number as double and a bare integer literal as int, and has no mixed arithmetic overload:
record.amount / 100 → ERR no such overload: dyn<double> / int → null
record.amount * 2 → ERR no such overload: dyn<double> * int → null
record.amount * record.probability / 100.0 → OK (float literal)
record.amount >= 4 → OK (cel-js DOES have mixed *comparison* overloads)
So amount / 100, price * 2, total - fee (int literal) all silently null. Only float literals (100.0, 2.0) work. Integer literals in arithmetic are ubiquitous → high-frequency footgun.
Constraint: cel-js rejects registering operator overloads — env.registerFunction('_/_(double, int): double', …) → Invalid signature. So there is no trivial overload-registration fix.
Options
- (a) Build-time lint — in
validate-expressions.ts (ADR-0032), flag a number-typed field used as an arithmetic operand against an int literal; message: "use a float literal (e.g. / 100.0)". Cheapest, matches the advisory-warning guardrail strategy.
- (b) Engine AST promotion — in the CEL engine, walk the parsed AST and promote
int literals that are arithmetic operands to double. Must NOT touch int-typed function args (e.g. daysFromNow(30)). More robust (fixes silently), more invasive, needs tests.
- (a) and (b) are complementary.
2. Bare identifiers in formula expressions (secondary)
Formula scope exposes only {record, previous, input, os} (see buildScope, packages/formula/src/stdlib.ts) — no field flattening. So:
cel`status == "converted"` → null (bare `status`)
cel`record.status == "converted"` → OK
validate-expressions.ts doesn't flag bare identifiers that fail to resolve. A lint that requires record./input./os.-qualified references in formula expressions would catch it at build.
Acceptance
- An AI-authored
expected_revenue = amount * probability / 100 is either rejected at build with an actionable message, or compiles to a working double formula.
- Bare-identifier formula references are flagged at build.
Refs: #1927 (example-crm fixes), ADR-0032. Same "build-green / runtime-silent" family as #1877 / #1870 / #1876.
Summary
Two formula-authoring footguns let an AI write a reasonable-looking
Field.formulathat passesobjectstack buildbut silently evaluates tonullat runtime — the failure class our guardrails are meant to eliminate. Both were found during 9.5.x release-prep testing ofexample-crm(fixed there in #1927; this issue tracks the systemic prevention).1. No
double × intarithmetic overload (primary)cel-js (
@marcbachmann/cel-js) types a record field number asdoubleand a bare integer literal asint, and has no mixed arithmetic overload:So
amount / 100,price * 2,total - fee(int literal) all silently null. Only float literals (100.0,2.0) work. Integer literals in arithmetic are ubiquitous → high-frequency footgun.Constraint: cel-js rejects registering operator overloads —
env.registerFunction('_/_(double, int): double', …)→Invalid signature. So there is no trivial overload-registration fix.Options
validate-expressions.ts(ADR-0032), flag a number-typed field used as an arithmetic operand against anintliteral; message: "use a float literal (e.g./ 100.0)". Cheapest, matches the advisory-warning guardrail strategy.intliterals that are arithmetic operands todouble. Must NOT touchint-typed function args (e.g.daysFromNow(30)). More robust (fixes silently), more invasive, needs tests.2. Bare identifiers in formula expressions (secondary)
Formula scope exposes only
{record, previous, input, os}(seebuildScope,packages/formula/src/stdlib.ts) — no field flattening. So:validate-expressions.tsdoesn't flag bare identifiers that fail to resolve. A lint that requiresrecord./input./os.-qualified references in formula expressions would catch it at build.Acceptance
expected_revenue = amount * probability / 100is either rejected at build with an actionable message, or compiles to a workingdoubleformula.Refs: #1927 (example-crm fixes), ADR-0032. Same "build-green / runtime-silent" family as #1877 / #1870 / #1876.