@objectstack/formula packages/formula/src/cel-engine.ts:185. Measured on origin/main at de75e407e, in a worktree of this repo, with packages/formula built.
The contract and the reading that breaks it
firstUndeclaredReference(source, knownFields) documents itself as "the first such bare reference, or null". It gets that answer from cel-js's checker, which reports one error, and it acts only on a message matching Unknown variable: X:
const result = env.parse(source).check?.();
if (result && result.valid === false) {
const m = /Unknown variable:\s*([A-Za-z_$][\w$]*)/.exec(result.error?.message ?? '');
if (m) return m[1];
}
return null;
So when the checker's FIRST error is of any other class, every undeclared reference behind it in the same source is invisible, and the helper returns null — "every reference is rooted" — for a source where none of that is true.
Measured
Probe: the strict env this helper builds (unlistedVariablesAreDyn: false, stdlib registered, SCOPE_ROOTS plus current_user / page), the raw check() verdict beside the helper's answer.
| source |
checker's first error |
firstUndeclaredReference |
status == "qualified" |
Unknown variable: status |
"status" |
has(status) && status == "qualified" |
has() invalid argument |
null |
has(status) && other == "x" |
has() invalid argument |
null |
other == "x" && has(status) |
Unknown variable: other |
"other" |
has(record.status) && status == "qualified" |
Unknown variable: status |
"status" |
Row 3 is the one that names the mechanism: the masked name is not the guarded one. The masking is positional — everything after the first non-Unknown variable error goes unjudged, whatever it is called — not name-keyed.
A bare has(x) is the trigger measured here because has() requires a select argument, but nothing in the mechanism is specific to has(): any first checker error of another class does the same.
Who reads this helper
packages/lint/src/validate-visibility-predicates.ts:780 — this consumer is repaired at its own call site by card 16118, by masking has(…) calls out of the source before the checker sees it. That repair is local to that rule; it does not touch this helper.
packages/lint/src/flow-variable-scope.ts:273 — loops over this helper to collect flow-variable references. Unmeasured against this shape.
packages/formula/src/validate.ts:688 and :713 — the record-scoped bare-ref error and the unknown-field check. Unmeasured against this shape.
⛔ Not measured here: whether either of those three surfaces can actually receive a source whose first checker error is of another class, and what an author writes to get there. That is the first thing a fix should establish, because it decides whether this is a live reachability gap on those surfaces or only a latent one.
Why it is not simply "fix it upstream"
The checker hands back one error. Reporting past it needs either a re-check loop over a neutralised source or a different checker entry — a design change to an oracle several rules share, and one that would change what those rules report. That is a decision, not a patch, which is why card 16118's repair stayed at its own call site and this is filed separately.
Surfaced by the work on #16118. Dedupe: all 678 open non-PR issues enumerated through the REST list endpoint and grepped locally for firstUndeclaredReference, cel-engine, flow-variable-scope, validate-expressions, Unknown variable, undeclared reference, masks / masking; nothing covers this. Unassigned.
Generated by Claude Code
@objectstack/formulapackages/formula/src/cel-engine.ts:185. Measured onorigin/mainatde75e407e, in a worktree of this repo, withpackages/formulabuilt.The contract and the reading that breaks it
firstUndeclaredReference(source, knownFields)documents itself as "the first such bare reference, ornull". It gets that answer from cel-js's checker, which reports one error, and it acts only on a message matchingUnknown variable: X:So when the checker's FIRST error is of any other class, every undeclared reference behind it in the same source is invisible, and the helper returns
null— "every reference is rooted" — for a source where none of that is true.Measured
Probe: the strict env this helper builds (
unlistedVariablesAreDyn: false, stdlib registered,SCOPE_ROOTSpluscurrent_user/page), the rawcheck()verdict beside the helper's answer.firstUndeclaredReferencestatus == "qualified"Unknown variable: status"status"has(status) && status == "qualified"has() invalid argumentnullhas(status) && other == "x"has() invalid argumentnullother == "x" && has(status)Unknown variable: other"other"has(record.status) && status == "qualified"Unknown variable: status"status"Row 3 is the one that names the mechanism: the masked name is not the guarded one. The masking is positional — everything after the first non-
Unknown variableerror goes unjudged, whatever it is called — not name-keyed.A bare
has(x)is the trigger measured here becausehas()requires a select argument, but nothing in the mechanism is specific tohas(): any first checker error of another class does the same.Who reads this helper
packages/lint/src/validate-visibility-predicates.ts:780— this consumer is repaired at its own call site by card 16118, by maskinghas(…)calls out of the source before the checker sees it. That repair is local to that rule; it does not touch this helper.packages/lint/src/flow-variable-scope.ts:273— loops over this helper to collect flow-variable references. Unmeasured against this shape.packages/formula/src/validate.ts:688and:713— therecord-scoped bare-ref error and the unknown-field check. Unmeasured against this shape.⛔ Not measured here: whether either of those three surfaces can actually receive a source whose first checker error is of another class, and what an author writes to get there. That is the first thing a fix should establish, because it decides whether this is a live reachability gap on those surfaces or only a latent one.
Why it is not simply "fix it upstream"
The checker hands back one error. Reporting past it needs either a re-check loop over a neutralised source or a different checker entry — a design change to an oracle several rules share, and one that would change what those rules report. That is a decision, not a patch, which is why card 16118's repair stayed at its own call site and this is filed separately.
Surfaced by the work on #16118. Dedupe: all 678 open non-PR issues enumerated through the REST list endpoint and grepped locally for
firstUndeclaredReference,cel-engine,flow-variable-scope,validate-expressions,Unknown variable,undeclared reference,masks/masking; nothing covers this. Unassigned.Generated by Claude Code