You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(formula): declare SCOPE_ROOTS as dyn in the strict env so a bare reference behind a root name is judged
`firstUndeclaredReference` reads the ONE error cel-js's checker returns and acts
only on `Unknown variable: X`. The strict env it builds declared every
`SCOPE_ROOTS` member as `map`, while the permissive env `celEngine.compile`
type-checks in leaves the same names `dyn`. `map` carries no `==` / `<` / `+`
overload, so an ordinary comparison on a root — or on an object field or flow
variable sharing a root's name — compiled clean and then faulted `no such
overload` in the strict env only. That fault took the single error slot and
every undeclared reference behind it in the same source went unjudged: the
helper answered `null`, the same value that means "every reference is rooted",
and four published call sites read it as the second.
The two environments now agree about these names, so the class cannot arise
rather than being compensated for downstream. `dyn` is what the list's own
doc-comment already claimed the declaration was for (member access, arithmetic
and comparison on a root all deferring to runtime); `map` delivered only the
first of the three.
Measured on the twelve-row probe table the option was ruled against: 11 rows
flip, 17 of 17 negative controls stay clean, and the card's five-row regression
table is unchanged. The twelfth row is a CEL TYPE name, which CEL declares
itself and no declaration here can reach — its strict-env message is
byte-identical under a `map` and a `dyn` root declaration.
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>
fix(formula): the strict declaredness env declares `SCOPE_ROOTS` as `dyn`, so a bare reference behind a root name is no longer masked (#16412)
7
+
8
+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is renamed, retired or re-typed: no `packages/spec` key changes its name, its type or its optionality, no stored shape moves, and every view, form, flow and formula parses byte-identically to before. `objectstack migrate meta` therefore has nothing to rewrite, and this changeset carries no rewrite instructions. What narrows is the ACCEPT SET of published CHECKERS at build time: `validateExpression` and two `@objectstack/lint` rules now report a bare field reference they previously left unjudged, which is the same verdict each of them already returns for that identifier when it is written first in the same predicate. The sources that newly report are already broken at RUN time and were before this change: a bare identifier in a record-scoped site resolves to nothing, the expression evaluates to null and a visibility predicate falls open, which is #1928's class. The remedy is per-source and the diagnostic already names it in full, naming the identifier and the namespace it belongs under; there is no authored artifact and no stored representation for a migration to act on. -->
9
+
10
+
**BREAKING** in the accept-set sense — an accept-set narrowing on published
11
+
CHECKERS, in the same sense as a route that starts refusing a request it should
12
+
always have refused — landing in the launch window as `minor` on both packages (during the window the bump level is
13
+
not the carrier of breaking-ness; this paragraph and the disposition above
14
+
are). Nothing that was already reported stops being reported, and no source
15
+
that is correct starts being reported.
16
+
17
+
`firstUndeclaredReference` asks cel-js's checker for the first undeclared
18
+
identifier in a source. That checker returns exactly ONE error, and the helper
19
+
acts only on `Unknown variable: X`, so whenever the first error is of another
20
+
class every undeclared reference behind it in the same source went unjudged and
21
+
the helper answered `null` — which is also the value that means "every
22
+
reference is rooted". Four published call sites read that answer, and none of
23
+
them can tell the two readings apart.
24
+
25
+
The widest way to reach that state was a disagreement between two environments
26
+
in this package about the same names. The strict env declared every
27
+
`SCOPE_ROOTS` member (`data`, `config`, `record`, `result`, `item`, `event`,
28
+
`input`, `user`, …) as `map`, while the permissive env that `celEngine.compile`
29
+
type-checks in leaves them `dyn`. `map` has no `==`, `<` or `+` overload, so an
30
+
ordinary comparison on one of those names compiled clean and then faulted `no
31
+
such overload` in the strict env only — taking the single error slot and
32
+
silencing everything behind it. An author reaches it by naming an object field
33
+
or a flow variable after a namespace root and reading it bare, which on a
34
+
metadata-editing form is not even a coincidence: that layer binds the row under
35
+
edit as `data`.
36
+
37
+
The strict env now declares those roots `dyn`, which is what the list's own
38
+
doc-comment already claimed it was for — member access, arithmetic and
39
+
comparison on a root all deferring to runtime — and which `map` delivered only
40
+
the first of. The two environments agree about these names, so the class cannot
41
+
arise rather than being compensated for downstream.
42
+
43
+
What starts reporting, measured on each published surface:
44
+
45
+
-`@objectstack/formula``validateExpression` with `scope: 'record'` — a bare
46
+
reference behind a root name is the hard error it always was for the same
47
+
identifier written first (`ok` was `true` with zero errors; it is now `false`).
48
+
-`@objectstack/formula``validateExpression` with `scope: 'flattened'` — the
49
+
did-you-mean warning reaches a misspelled field behind a root name.
50
+
-`@objectstack/lint``visibility-bare-identifier` — a bare identifier behind a
51
+
root name in a `visibleWhen` predicate is a finding. Per that rule's own
52
+
message the console otherwise falls open and the element renders
53
+
unconditionally.
54
+
-`@objectstack/lint` flow-variable shadowing — a shadowed field read behind a
55
+
root name is warned. That rule's documented blind spot is now name-local, as
56
+
its wording always claimed: the colliding name itself is still not reported.
57
+
58
+
⛔ Two first-error classes are NOT closed by this, and both stay pinned. A CEL
59
+
TYPE name (`type`, `string`, `int`, …) is declared by CEL itself, so no
60
+
declaration this package makes can reach it; measured on the strict env, the
61
+
message for `type == 'grid'` is byte-identical under a `map` and a `dyn` root
62
+
declaration. And `has()` handed a non-select argument still faults its own
63
+
class, which `@objectstack/lint`'s visibility rule masks at its own call site
64
+
(#16118) and which nothing else masks.
65
+
66
+
The narrowing this helper is built on is unchanged: it still acts only on
67
+
`Unknown variable`, so `type(record.x) == string`, comprehension macros, guard
68
+
idioms, optional chaining and stdlib calls report nothing, and a widening of
69
+
that regex onto the overload message remains refused.
0 commit comments