Skip to content

Commit 09ae32e

Browse files
os-steveclaude
andauthored
feat(lint): judge docs-corpus *When predicates as CEL, where the layer is determinable (#11652)
`os:check` type-checks a CEL string as `string`, so a `visibleWhen` calling a function that exists nowhere (`user.hasRole('admin')`) type-checked perfectly — and a field-level `visibleWhen` fault is fail-OPEN, so the copied example shows the element to everyone. Extends `check-doc-formula-expressions` with a third scan surface rather than minting a second opinion about one contract. The verdict is imported whole: `validateExpression` (syntax, unknown-function, bare reference) plus the newly-extracted `fieldRuleRootIssue` (the closed-root rule) — the same two the metadata walk applies to the same slot. Admission is structural and schema-backed, never keyed on the key: a `Field.*()` factory call, or a raw field def carrying `type:` inside an object-literal `fields:` MAP (`ObjectSchema.fields` is `z.record`; `FormFieldSchema` and `ScreenFieldConfigSchema` are `z.array`). A site whose layer is not determinable is skipped, and the skip list is printed and counted on every run — including green ones — because a gate that skips in silence is the false-green this surface exists to prevent, one level up. `fieldRuleRootIssue` / `FIELD_RULE_BOUND_ROOTS` were a closure inside `validateStackExpressions`; hoisted and exported so both callers stand on the rule rather than a dialect of it. Behaviour unchanged — 2271 lint tests pass. Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 781cc0b commit 09ae32e

4 files changed

Lines changed: 967 additions & 295 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
Judge `visibleWhen` / `readonlyWhen` / `requiredWhen` examples in the docs corpus
6+
as CEL — where the enclosing structure says which layer they are about
7+
8+
`{/* os:check */}` blocks are type-checked by `tsc --noEmit`, and every CEL
9+
string is the same type as every other CEL string, so
10+
`visibleWhen: "record.status != 'closed' && user.hasRole('admin')"` type-checked
11+
perfectly. `hasRole` is a CEL function that exists nowhere — it is in no stdlib
12+
registry and on no contract — so the predicate faults at runtime, and a
13+
field-level `visibleWhen` fault is fail-**open**: `resolveFieldRuleState`
14+
evaluates visibility with `fallback: true`, so the element the author wrote the
15+
predicate to hide is shown to everyone who copies the page. That is not a
16+
hypothetical shape — a shipped doc taught it (#11034 fixed the instance).
17+
18+
`check:doc-formula-expressions` gains this as a third scan surface rather than a
19+
second gate, because two gates with opinions about one contract is the thing
20+
Prime Directive #12 exists to prevent. The verdict is imported whole: syntax, the
21+
unknown-function catch and the bare-reference rule come from
22+
`@objectstack/formula`'s `validateExpression`, and the closed-root rule comes
23+
from `fieldRuleRootIssue` — the same two the metadata walk applies to the same
24+
slot, in the same order, in the same words.
25+
26+
**The layer is decided first, and a layer that cannot be decided is skipped and
27+
printed.** `visibleWhen` is one key spelling several unrelated contracts, and the
28+
binding root really does differ: an object field binds `record` + `previous`
29+
(+ `parent`), a per-option predicate binds `record` plus the host predicate scope
30+
including `current_user`, a page component binds the user roots and `app`, and a
31+
flow-screen field **flattens its own field names to top level**. A gate keyed on
32+
the key alone would have gone red on
33+
`content/docs/automation/flows.mdx`'s correct `visibleWhen:
34+
'createOpportunity == true'` and on `content/docs/ui/pages.mdx`'s correct
35+
`'sales_manager' in current_user.positions` — and a gate whose reds are wrong is
36+
worse than no gate, because it teaches people to add ignores.
37+
38+
So admission is structural and schema-backed, never keyed on the key: a
39+
`Field.*({ … })` factory call, or a raw field definition carrying `type:` inside
40+
an object-literal `fields:` **map**. The map-versus-array test is the load-bearing
41+
half and it is read off the schemas — `ObjectSchema.fields` is
42+
`z.record(name, FieldSchema)` while `FormFieldSchema` and `ScreenFieldConfigSchema`
43+
are both `z.array(…)`, so a `fields:` map is the object-field layer and nothing
44+
else, and a `fields:` array is exactly the case that cannot be told apart.
45+
46+
**The skip list is printed and counted on every run, including green ones.** A
47+
gate that skips in silence is the same false-green one level up, so the summary
48+
names every skipped site and why. Measured on the corpus as it stands: 23
49+
text-level `*When:` occurrences, of which 13 are admitted and judged, 7 are
50+
listed as skipped, and 3 are the ADR quoting `field.zod.ts`'s schema
51+
(`visibleWhen: ExpressionInputSchema.optional()`) rather than authoring a
52+
predicate. Three of those seven were invisible to an AST-only walk — a bare
53+
`visibleWhen: "…"` line at statement position is a labelled statement, not a
54+
property — so a text-level tripwire reconciles the two counts and any site the
55+
parser never surfaced is listed rather than dropped.
56+
57+
`@objectstack/lint` newly exports `fieldRuleRootIssue` and
58+
`FIELD_RULE_BOUND_ROOTS`. The field-rule root decision was a closure inside
59+
`validateStackExpressions` — correct while it had one caller, and exactly how a
60+
second caller comes to own a dialect of a rule instead of the rule. Behaviour is
61+
unchanged: the metadata walk now calls the extracted function and its 2271 tests
62+
pass untouched.

0 commit comments

Comments
 (0)