Repository navigation
Commit f5b6b3f
fix(lint)!: page/view visibleWhen refuses a has() argument that is not a field selection (#25313)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg52sU6eSQpjfDKQEaQYrvSX)_
Fixes #25281
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Executes ruling `6351260886` on #25281, letter A. Draft until the
maintainer ratifies that ruling; the seat runs the contract review and
the landing.
## What
CEL defines `has()` in one form, `has(e.f)`, with `f` a field name after
a dot. The engine refuses any other argument as `has() invalid argument`
on every evaluation, whatever the record holds. The page and view
`visibleWhen` door (`validateVisibilityPredicates`, so `os validate`,
`os build`, `os lint`, `defineStack` and the page and view save doors)
did not judge the argument, so `has(status)` and `has(record.items[0])`
published clean and the console drew the guarded element for every user.
- **`@objectstack/lint`**: a new gating arm,
`visibility-predicate-has-argument` (`error`), in the existing family,
at the same site and path as its siblings. It fires on a `has()` call
whose single argument is not a `.` field selection, reports the first
such call per predicate, quotes the argument and the engine's wording,
and gives one hint line with the spelling the definition admits:
- a bare name: `has(record.status)` or `'status' in record`, with the
root the layer binds (`data` on a metadata-editing form); a bare root
(`has(record)`) is told `has(record.FIELD)`, never `record.record`;
- an index as the last step: `size(record.items) > 0`, or `'k' in e`
when the index is a string key;
- `e.?f`: `has(e.f)`; anything else: the one form, with the layer root.
No null-guard advice is repeated (`validate-null-guards` owns it). `os
explain visibility-predicate-has-argument` carries the reasoning.
- **The oracle (mechanism (b) of the ruling)**: the canonical AST this
rule already walks for the #16118 mask. A `has` call with exactly one
argument whose node is not `.` is refused. After `rewriteIndexedHas`
(#25267) the engine refuses exactly those arguments, and the suite pins
the equivalence against `celEngine.compile` in both directions: 22
refused shapes and 10 accepted shapes. Mechanism (b) was chosen over (a)
because the engine reports one error per source: in `bogusFn(1) &&
has(status)` a compile-based helper would see only the unknown function,
while the AST walk sees the `has()` call too.
- **The #16118 mask stays load-bearing.** `has(status)` alone yields ONE
finding (this one). `has(status) && status == 'x'` keeps its
`visibility-bare-identifier` finding on the unguarded `status`, beside
the new one.
- **`@objectstack/spec`, ADR-0087 kit**: the D3 semantic entry
`visibility-predicate-has-argument-refused` (step 18) and a
`REFUSED_STORED_SHAPES` detector. `os migrate meta --stored` lists a
stored page component, form section or section field carrying the shape
under that id. No D2 conversion: the test the author meant is not
recorded. `packages/spec` cannot parse CEL, so the detector reads the
source through a package-internal token reading
(`conversions/cel-has-argument.ts`). A lint-side test
(`validate-visibility-predicates.stored-listing.test.ts`) holds the
listing, the page save door and `celEngine.compile` to one set over 29
predicates.
- **`@objectstack/formula`**: comment only. The STILL OPEN note above
`firstUndeclaredReference` records that the page/view door now reports
the argument. The first-error masking it describes is unchanged.
## Pins rewritten as part of the ruling (not silently)
- The module note: the rule list (five gating rules), §Function
existence point 2 ("narrows by exactly TWO axes"), the CEL-TYPE
blind-spot bullet ("Function existence and the `has()` argument shape
excepted"), and a new §`has()` argument shape that quotes the ruling and
states what it does not change.
- `validate-visibility-predicates.test.ts`: row 3 (`has(status)` alone),
"several bare `has()` arguments" and the comprehension row flip from
`[]` to the one `has()` finding, with no bare-identifier finding. The
"#13594 narrows this pin by exactly one axis" block becomes two axes and
adds `has(status)` beside `totallyBogusFn(1,2)`.
## Unchanged (#13594's scope discipline, verbatim)
`has(e.f)` for any `e`, including the indexed and call receivers #25267
lowers; every `dyn` predicate; `type == 'grid'`, `type(record.x) ==
string`, `upper(1, 2)`, `1 + 'a'` (pinned: no `has()` finding); a `has(`
inside a string literal; a member call `x.has(y)`; `has()` and `has(a,
b)`, which the engine refuses under another message; the render-side
fail-open (ADR-0137 D3).
## Measured before the first edit
- HotCRM producers: **0** at `4166f21` (shallow clone). The 550 `has(`
occurrences hold no bare-name and no index-last argument in a CEL
string. The only hits are comment prose (`(!has(x) || isBlank(x))` in
two flow docblocks, one test comment).
- This repository: **0** at the base `5801a4f`. Every `has(NAME)` /
`has(e[i])` hit is TypeScript `Map`/`Set` `.has(`, a code comment,
CHANGELOG or docs prose.
- Base `5801a4f`: `has(status)` and `has(record.items[0])` on a form
field draw no finding from `validateVisibilityPredicates`, and
`celEngine.compile` answers `has() invalid argument` on both. The suite
at the base pinned that silence (rows 898–926).
## Tests
Run on this branch, each under `scripts/pm/os-verify-lock.sh`:
- Before the merge of `main` (head `ce74b8efc`): `@objectstack/lint` 154
files / 7164 tests, `@objectstack/formula` 52 / 1376,
`@objectstack/spec` 710 / 21577 (1 todo), all passing; the three
packages type-check.
- After merging `main` (#25299, head `7f0e0e6fe`): `@objectstack/spec`
`src/conversions` + `src/migrations` 26 files / 1036 tests,
`@objectstack/lint` 154 / 7164, both passing; spec and lint type-check.
The merge kept both refused-shape describes; the new detector reads the
stack alone and does not use the context parameter #25279 added.
- New suites: `validate-visibility-predicates.test.ts` ›
`visibility-predicate-has-argument (#25281)` (acceptance pair, engine
equivalence both ways, prescriptions, mask, first-only, boundaries,
carriers); `validate-visibility-predicates.stored-listing.test.ts`
(engine, page door and stored listing over 18 refused + 11 accepted
predicates); `conversions/cel-has-argument.test.ts`; the `#25281`
describe in `refused-shapes.test.ts`; two rows and a view-door assertion
in `runtime-gate.page-visibility-predicate-writes.test.ts`.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 92 families, the same list before
and after the merge. On head `7f0e0e6fe` all 92 were run and each exited
0; `--ran` reconciliation: "92 derived famil(ies) accounted for — 92
run, 0 NOT-MEASURED". This includes `check-empty-changeset`,
`check-adr-0087-registration` and `check-changeset-no-major`. (On the
pre-merge head `check:dual-build-cjs-loads` exited 3 for a missing dist;
with the dists built it exits 0.)
## Dogfood battery
`pnpm exec turbo run build --filter='@objectstack/dogfood^...' && pnpm
--filter @objectstack/dogfood test` on head `7f0e0e6fe`: 289 files
passed, 3 skipped (292); 2491 tests passed, 18 skipped (2509). `VERDICT
command-exit 0`.
## Acceptance notes
- The detector in `packages/spec` reads tokens, not the engine's AST. A
predicate that is also broken CEL can be listed there while the door
names its syntax fault first. Both send the owner to the same predicate.
- Not addressed here: on the object, flow, action, sharing-rule and hook
surfaces, `validateExpression` already refuses both shapes
(`invalid-cel`, `has() invalid argument`). Its prescription there is the
generic "predicates are bare CEL", not a `has()` spelling. Noted, not
filed: the verdict is right and the surface is outside this card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5f4a3f0 commit f5b6b3f
13 files changed
Lines changed: 1088 additions & 29 deletions
File tree
- .changeset
- packages
- formula/src
- lint/src
- spec/src
- conversions
- migrations/entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
404 | 404 | | |
405 | 405 | | |
406 | 406 | | |
407 | | - | |
408 | | - | |
409 | | - | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
410 | 414 | | |
411 | 415 | | |
412 | 416 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
267 | 267 | | |
268 | 268 | | |
269 | 269 | | |
| 270 | + | |
270 | 271 | | |
271 | 272 | | |
272 | 273 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
853 | 853 | | |
854 | 854 | | |
855 | 855 | | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
856 | 873 | | |
857 | 874 | | |
858 | 875 | | |
| |||
4772 | 4789 | | |
4773 | 4790 | | |
4774 | 4791 | | |
| 4792 | + | |
4775 | 4793 | | |
4776 | 4794 | | |
4777 | 4795 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
46 | 47 | | |
47 | 48 | | |
48 | 49 | | |
| |||
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
| 63 | + | |
62 | 64 | | |
63 | 65 | | |
64 | 66 | | |
| |||
90 | 92 | | |
91 | 93 | | |
92 | 94 | | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
93 | 98 | | |
94 | 99 | | |
95 | 100 | | |
| |||
198 | 203 | | |
199 | 204 | | |
200 | 205 | | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
201 | 210 | | |
202 | 211 | | |
203 | 212 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
0 commit comments