From ecf9ff4eeb86527e29868fa7b33b485ca5962087 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 02:07:30 +0000 Subject: [PATCH 1/4] docs(skills): objectui teaches CEL for bare-string predicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since #27584 the console reads a bare-string predicate as CEL, the way the spec defines it and the server reads it. skills/objectui still taught the legacy JavaScript spellings (`===`, `!==`, a JS operator list counted as condition syntax), which now fault and hide the node. - Every bare-predicate example is CEL; the architecture overview says a bare condition string is CEL on @objectstack/formula, and the operator list says it is the `${…}` grammar only. - The Condition fields section gains a FROM → TO list measured on the console's evaluator: `===`/`!==`, `.includes`, `.length`, and a `data.*` key the scope may lack (`has()`; CEL faults on a missing key). - The row-predicate bullet says only a `${…}` condition still reads on the legacy engine. - Eval 2 now hands over a JS-syntax predicate and expects the CEL rewrite. - Paid down in the same file: the bindable-text-keys paragraph restated troubleshooting item 1. All three files stay under their ceilings. Fixes #27587 Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4d1Ffw7D64ARzogMukj --- skills/objectui/evals/schema-expressions.json | 7 ++-- skills/objectui/guides/schema-expressions.md | 40 ++++++++++--------- skills/objectui/rules/protocol.md | 4 +- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/skills/objectui/evals/schema-expressions.json b/skills/objectui/evals/schema-expressions.json index a249e16c61e..a51c778f920 100644 --- a/skills/objectui/evals/schema-expressions.json +++ b/skills/objectui/evals/schema-expressions.json @@ -20,12 +20,13 @@ }, { "id": 2, - "prompt": "I need to make a button visible only to admin users AND only when the record status is 'active'. Also, the button should show the record owner's name. Show me the correct schema.", - "expected_output": "Produces a schema with combined visible/hidden expression using && operator, and does not put the owner name in props.label — that envelope is evaluated and then discarded. The name is resolved in the host, or carried on a text node's content.", + "prompt": "My button should show only to admins AND only while the status is 'active', with the owner's name on it. I wrote \"visibleWhen\": \"data.role === 'admin' && data.status === 'active'\" and it never shows. Fix the schema.", + "expected_output": "Says a bare predicate is CEL, so === faults and hides the node; rewrites it with == and &&. The owner name goes on a text node's content, not props.label.", "files": [], "assertions": { "must_contain": [ - "visible", + "CEL", + "role == 'admin'", "&&", "${" ], diff --git a/skills/objectui/guides/schema-expressions.md b/skills/objectui/guides/schema-expressions.md index 74190d7c1d2..d54c5503a9f 100644 --- a/skills/objectui/guides/schema-expressions.md +++ b/skills/objectui/guides/schema-expressions.md @@ -6,10 +6,8 @@ Writing and debugging dynamic expressions in ObjectUI schemas. The expression sy ObjectUI uses a two-tier expression evaluator: -1. **Template expressions** — strings containing `${...}` placeholders: `"Hello ${user.name}"` -2. **Condition expressions** — raw boolean expressions without wrappers: `data.status === 'active'` - -Both are parsed by a **recursive-descent parser** (no `eval()`, no `new Function()`). This means expressions are CSP-safe and work under strict Content Security Policy headers. +1. **Template expressions** — strings containing `${...}` placeholders: `"Hello ${user.name}"`, parsed by a **recursive-descent parser** (no `eval()`, no `new Function()`), so CSP-safe. +2. **Condition expressions** — a bare string in a predicate key: `data.status == 'active'`. It is **CEL**, evaluated by `@objectstack/formula`, as on the server. Key files (for reference, not for editing): - `packages/core/src/evaluator/ExpressionEvaluator.ts` — main entry point @@ -55,11 +53,6 @@ this rule. The four cases that decide most schemas: { "type": "text", "content": "${data.customer.name}" } ``` -`title` / `label` / `value` / `description` are template-evaluated on the types -that declare them -- `statistic` (`label` / `value` / `description`), `card` -(`title` / `description`), `button` (`label`) -- and read raw everywhere else. -The set is closed and declared in `@objectstack/spec`. - ## Template expression syntax (`${}`) ### Basic property access @@ -79,6 +72,9 @@ ${!isLocked} → boolean negation ``` ### Supported operators (full list) + +The `${…}` grammar only. A bare condition string is CEL — see "Condition fields". + - Arithmetic: `+`, `-`, `*`, `/`, `%` - Comparison: `>`, `<`, `>=`, `<=`, `==`, `===`, `!=`, `!==` - Logical: `&&`, `||`, `!` @@ -237,7 +233,7 @@ name, so calls are case-insensitive. ### Syntax options The node-level condition keys are `visibleWhen` (show), `hidden` and -`disabled`. Each takes a boolean, a `${}` template, or a raw expression: +`disabled`. Each takes a boolean, a `${}` template, or a bare CEL string: ```jsonc @@ -251,9 +247,18 @@ The node-level condition keys are `visibleWhen` (show), `hidden` and ```jsonc -{ "hidden": "data.status !== 'active'" } // raw expression (no ${} needed) +{ "hidden": "data.status != 'active'" } // bare string = CEL (no ${} needed) ``` +A JS-only spelling in a bare string faults: a visibility gate hides the node, +`disabled` disables. FROM → TO: + +- `a === b` / `a !== b` → `a == b` / `a != b` +- `list.includes(x)` → `x in list`; `s.includes('x')` → `s.contains('x')` +- `list.length` → `size(list)` +- a `data.*` key the scope may lack (`data.x == 1`, `!data.x`, `?.`, `??`) → + `has(data.x) && data.x == 1`: CEL faults on a missing key + `visibleOn` / `hiddenOn` / `disabledOn` are retired and refused by name — rename to `visibleWhen` / `hidden` / `disabled`; the value is unchanged. @@ -292,10 +297,8 @@ rename to `visibleWhen` / `hidden` / `disabled`; the value is unchanged. ## Field-level conditional rules (`visibleWhen` / `readonlyWhen` / `requiredWhen`) These live on the **object's field metadata**, are written in CEL, and are -evaluated by `@objectstack/formula` -- a different tier from the node-level -conditions above, which are schema/widget-level and run on the recursive-descent -`SafeExpressionParser`. **How to author them is the `objectstack-data` skill's -job**, under "Conditional Field Rules": the predicate scope, the client-only vs +evaluated by `@objectstack/formula`. **How to author them is the +`objectstack-data` skill's job**, under "Conditional Field Rules": the predicate scope, the client-only vs client-and-server enforcement split, the invariant-vs-transition-gate choice, and the protocol-17 removal of the `conditionalRequired` alias are all there. Author them in the canonical tagged-template form that skill teaches @@ -332,9 +335,8 @@ Three renderer-side facts that live nowhere else: operator, value }` form and the `{ expression: "${…}" }` template form are retired, refused at the schema door by name with the respelling. A STORED legacy rule still renders through the shared evaluator (a compatibility read, - not an authoring form); a `condition` in legacy-only syntax (`${…}`, `===`, - `.includes()`) still routes to the old engine with a one-time deprecation - warning -- write CEL. + not an authoring form); only a `${…}` `condition` still routes to the old + engine. `===` or `.includes()` faults on CEL. Both warn once -- write CEL. - **`data.*` is the trap in a row predicate.** A row predicate binds ONE root, `record.*`; a bare `status` or `data.status` is retired there and faults on the runtime engine (`Unknown variable`), under each surface's existing error @@ -637,6 +639,6 @@ When an expression isn't working: 1. **Which key is it on, and does that type declare the key?** `content` and the predicate keys are evaluated and read on every type. `title` / `label` / `value` / `description` are evaluated **only on the types that declare them** — `statistic` (`label` / `value` / `description`), `card` (`title` / `description`), `button` (`label`) — and read raw everywhere else, including on a namespaced spelling such as `ui:statistic`. A `${...}` inside a `props` envelope is evaluated and then discarded. (A `properties` envelope is the one that is evaluated *and* hoisted onto the node — see [`rules/protocol.md`](../rules/protocol.md) for why that is recorded, not recommended.) 2. Is the `${}` syntax correct? Check for unmatched braces. 3. Is the data actually available in scope? Check what the host published on `PredicateScopeProvider` — ⛔ not `SchemaRendererProvider`'s `dataSource`, which publishes no expression root. -4. For conditions: `visibleWhen` / `hidden` / `disabled` take a raw expression or a `${}` template; the `On` spellings are refused. +4. For conditions: `visibleWhen` / `hidden` / `disabled` take a bare CEL string or a `${}` template. 5. Does the expression use a blocked pattern? Check for constructors, `eval`, `window`, etc. 6. Is type coercion causing issues? `${0 && "yes"}` returns `0`, not `false`. diff --git a/skills/objectui/rules/protocol.md b/skills/objectui/rules/protocol.md index 45c9f45cd36..1f4a601edb1 100644 --- a/skills/objectui/rules/protocol.md +++ b/skills/objectui/rules/protocol.md @@ -13,8 +13,8 @@ value must clear before it reaches the screen. | Field | Evaluation Type | Return Type | Example | |---|---|---|---| | `content` | Template (`${}`) | string | `"content": "Total: ${data.total}"` | -| `visibleWhen` | Condition | boolean | `"visibleWhen": "data.permissions.canView"` (raw expression, no `${}`) | -| `hidden` | Condition | boolean | `"hidden": "data.status === 'draft'"` | +| `visibleWhen` | Condition | boolean | `"visibleWhen": "data.permissions.canView"` (bare string = CEL) | +| `hidden` | Condition | boolean | `"hidden": "data.status == 'draft'"` | | `visible` | Condition | boolean | `"visible": "${data.isActive}"` | | `disabled` | Condition | boolean | `"disabled": "!data.hasPermission"` | | `title` / `label` / `value` / `description` | Template (`${}`) | Preserves original type | **Only on the component types the spec declares** — see "Rule: Bindable Text Keys" below. `"value": "${data.total}"` on a `statistic` renders the number. | From ad2c8b6754102cd1430c43a238ec27364aee5b64 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 02:09:15 +0000 Subject: [PATCH 2/4] chore(changeset): @objectstack/skills patch for the objectui CEL predicate teaching Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4d1Ffw7D64ARzogMukj --- .../27587-objectui-skill-teaches-cel-predicates.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/27587-objectui-skill-teaches-cel-predicates.md diff --git a/.changeset/27587-objectui-skill-teaches-cel-predicates.md b/.changeset/27587-objectui-skill-teaches-cel-predicates.md new file mode 100644 index 00000000000..1da68cc36cd --- /dev/null +++ b/.changeset/27587-objectui-skill-teaches-cel-predicates.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): objectui teaches CEL for a bare-string predicate, with a FROM → TO list for the JavaScript spellings + +Clause-②: no +Metadata-protocol: none + +- **Before.** `skills/objectui` taught bare predicates in the legacy JavaScript grammar: `"hidden": "data.status === 'draft'"` in `rules/protocol.md`, `data.status === 'active'` and `data.status !== 'active'` in `guides/schema-expressions.md`, and an operator list (`===`, `!==`, `.includes()`, `.length`) that read as condition syntax. Since #27584 the console reads a bare string in a predicate key as CEL, as the spec defines it and the server reads it, so an agent following the skill wrote a gate that faulted and hid the node. +- **Now.** Every bare-predicate example is CEL. The guide says a bare condition string is CEL on `@objectstack/formula` and that the operator list is the `${…}` template grammar only, and it gives a FROM → TO list measured on the console's evaluator: `===` / `!==` → `==` / `!=`; `list.includes(x)` → `x in list`, `s.includes('x')` → `s.contains('x')`; `list.length` → `size(list)`; a `data.*` key the scope may lack → `has(data.x) && …`, because CEL faults on a missing key. The row-predicate bullet says only a `${…}` condition still reads on the legacy engine. Eval 2 in `evals/schema-expressions.json` hands over a JS-syntax predicate and expects the CEL rewrite. +- No schema, key or behaviour changes. From e8ba3a8b984ea6c3ca01ef8169991201588a67a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 02:17:14 +0000 Subject: [PATCH 3/4] docs(skills): eval 2 asserts a CEL spelling the guide teaches Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4d1Ffw7D64ARzogMukj --- skills/objectui/evals/schema-expressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/objectui/evals/schema-expressions.json b/skills/objectui/evals/schema-expressions.json index a51c778f920..d998fab476b 100644 --- a/skills/objectui/evals/schema-expressions.json +++ b/skills/objectui/evals/schema-expressions.json @@ -26,7 +26,7 @@ "assertions": { "must_contain": [ "CEL", - "role == 'admin'", + "status == 'active'", "&&", "${" ], From 0376f2de88506f547342451a064d3f6c4f2ec3e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 02:50:51 +0000 Subject: [PATCH 4/4] =?UTF-8?q?docs(skills):=20objectui=20FROM=20=E2=86=92?= =?UTF-8?q?=20TO=20row=20gives=20the=20negated=20has()=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A key the scope may lack, read negated, is `!has(data.x) || !data.x`; `has(data.x) && !data.x` answers false when the key is absent. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4d1Ffw7D64ARzogMukj --- skills/objectui/guides/schema-expressions.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skills/objectui/guides/schema-expressions.md b/skills/objectui/guides/schema-expressions.md index d54c5503a9f..03088767824 100644 --- a/skills/objectui/guides/schema-expressions.md +++ b/skills/objectui/guides/schema-expressions.md @@ -257,7 +257,8 @@ A JS-only spelling in a bare string faults: a visibility gate hides the node, - `list.includes(x)` → `x in list`; `s.includes('x')` → `s.contains('x')` - `list.length` → `size(list)` - a `data.*` key the scope may lack (`data.x == 1`, `!data.x`, `?.`, `??`) → - `has(data.x) && data.x == 1`: CEL faults on a missing key + `has(data.x) && data.x == 1`, `!has(data.x) || !data.x`: CEL faults on a + missing key `visibleOn` / `hiddenOn` / `disabledOn` are retired and refused by name — rename to `visibleWhen` / `hidden` / `disabled`; the value is unchanged.