Skip to content

Commit 3ac9125

Browse files
committed
fix(spec): re-measure the six #20371 rows at objectui dd3f7e1be and declare objectName
The objectui pin moved from f8a9d0fb0596 to dd3f7e1be356. Every read point the action:* and element:* rows cite was re-derived at the new pin and re-anchored; the asserting citations now name dd3f7e1be. The new pin forwards `objectName` to the action runner from action:button and action:icon, so both rows declare it (the ActionDef string scalar, per the #7751 value posture). action:group and action:menu forward it per member, so the container rows gain no key. `params` keeps its meaning on the page path (array = input list, object = static values via properties.params). The dogfood expression-conformance rows for these keys are re-anchored at the same pin; their fault policies are unchanged across the hop. Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude <noreply@anthropic.com>
1 parent c628e1d commit 3ac9125

4 files changed

Lines changed: 129 additions & 86 deletions

File tree

‎.changeset/20371-component-props-action-element-rows.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ What changes at the authoring doors (`os validate` / `os build` / `os lint`):
1515

1616
Measured decisions worth knowing when you author these blocks:
1717

18-
- **`action:button` / `action:icon`** — `name` is optional (the renderer reads `name ?? label`). The executor is `actionType`; `type` inside `properties` is refused with a rename to `actionType` (on a page component `type` is the component itself). `visible` / `disabled` take a boolean, a CEL string or a `{ dialect, source }` envelope. The legacy `enabled` fallback and the host-only `autoTrigger` flag are refused with a prescription. `action:icon` reads no `size`.
19-
- **`action:group` / `action:menu`** — `actions` is a LIST of action objects (a member's executor is its own `type`); a bare list of action names is refused. `action:group` reads no group-level `name`, so it is refused with a prescription. `variant` / `size` take the Button primitive's vocabulary; `primary` and `md` are accepted only on `action:button` (and `primary` on `action:icon`), where the renderer maps them.
18+
- **`action:button` / `action:icon`** — `name` is optional (the renderer reads `name ?? label`). The executor is `actionType`; `type` inside `properties` is refused with a rename to `actionType` (on a page component `type` is the component itself). `visible` / `disabled` take a boolean, a CEL string or a `{ dialect, source }` envelope. The legacy `enabled` fallback and the host-only `autoTrigger` flag are refused with a prescription. `action:icon` reads no `size`. `objectName` names the object the action acts on (forwarded to the runner; omitted, the action acts on the page's object).
19+
- **`action:group` / `action:menu`** — `actions` is a LIST of action objects (a member's executor is its own `type`); a bare list of action names is refused. A member's `objectName` rides the member object; the containers themselves read no `objectName`. `action:group` reads no group-level `name`, so it is refused with a prescription. `variant` / `size` take the Button primitive's vocabulary; `primary` and `md` are accepted only on `action:button` (and `primary` on `action:icon`), where the renderer maps them.
2020
- **`element:definition-list`** — `items` of strict `{ term, description? }`; `columns` is the NUMBER `1` or `2` (the string `'2'` renders one column and is refused).
2121
- **`element:repeater`** — `object` is required; `filter` / `sort` take the family's one orthography (`ViewFilterRule[]`, `SortItem[]`); `fields` takes a field name or `{ field }` (an unrendered `label` is refused).

‎packages/qa/dogfood/test/expression-conformance.ledger.ts‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,13 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
398398
// two evaluation legs; the row states what the COMPOSITION does to a predicate
399399
// that faults. Split three ways because the faces differ, per renderer — the
400400
// collision rule `cel-action-visible` / `cel-action-disabled` were split on.
401-
// Read (not run) at the `.objectui-sha` pin `f8a9d0fb0596`: the
402-
// renderers are `components/src/renderers/action/action-{button,menu,icon,group}.tsx`,
401+
// Read (not run) at the `.objectui-sha` pin `dd3f7e1be356` (re-read there
402+
// 2026-09-28; first read at `f8a9d0fb0596`, and across that hop the node-gate
403+
// evaluators and `useCondition` are code-identical, so every fault face below
404+
// stands and only line numbers moved): the renderers are `components/src/renderers/action/action-{button,menu,icon,group}.tsx`,
403405
// `useCondition` is `react/src/hooks/useExpression.ts:200-244`, the node gate's
404406
// two legs are `react/src/SchemaRenderer.tsx` `evaluateVisibilityPredicate`
405-
// (:1048) and `evaluateEnablementPredicate` (:1138, :1772-1784). Tracker anchors
407+
// (:1157) and `evaluateEnablementPredicate` (:1247, :1879-1891). Tracker anchors
406408
// kept out of the strings below (`check:doc-authoring`): the node gate's fault
407409
// report is objectui#6038 (visibility legs) and objectui#6445 (enablement legs);
408410
// the empty-`disabled` fix `cel-action-disabled` cites is objectui#3842.
@@ -411,7 +413,7 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
411413
summary: '`action:button` / `action:menu` page-block visibility (props `visible`) — the block is not rendered when the predicate is FALSE',
412414
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-closed',
413415
enforcement:
414-
'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:menu"]`, the one door that reaches `properties.visible`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value, page scope — a fault there answers `evaluateCondition`\'s fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData, { throwOnError: true })` (action-button.tsx:116-119, action-menu.tsx:219-222) against the host-bound row — a fault returns `false` and warns once per predicate (useExpression.ts:215-238), and the block returns null (action-button.tsx:298, action-menu.tsx:293). The legs AND, so a faulting predicate HIDES the block: fail-CLOSED, the face `cel-action-visible` records for a registered action',
416+
'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:menu"]`, the one door that reaches `properties.visible`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value, page scope — a fault there answers `evaluateCondition`\'s fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData, { throwOnError: true })` (action-button.tsx:117-120, action-menu.tsx:224-227) against the host-bound row — a fault returns `false` and warns once per predicate (useExpression.ts:215-238), and the block returns null (action-button.tsx:335, action-menu.tsx:322). The legs AND, so a faulting predicate HIDES the block: fail-CLOSED, the face `cel-action-visible` records for a registered action',
415417
covers: [
416418
'ui/component.zod.ts:ActionButtonPropsSchema.visible',
417419
'ui/component.zod.ts:ActionMenuPropsSchema.visible',
@@ -423,24 +425,24 @@ export const EXPRESSION_SURFACE: ExprSurface[] = [
423425
summary: '`action:icon` / `action:group` page-block visibility (props `visible`) — the block is not rendered when the predicate is FALSE',
424426
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log',
425427
enforcement:
426-
'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:icon" | "action:group"]`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value — a fault answers the fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData)` WITHOUT `throwOnError` (action-icon.tsx:96, action-group.tsx:233) — a fault answers `evaluateCondition`\'s fail-soft `true` as well (ExpressionEvaluator.ts:387-408), and action-icon.tsx:197-199 names that policy "fail-soft, unlike `action:button`\'s fail-closed one". Both legs show, so a faulting predicate leaves the block RENDERED, and the node gate is the leg that logs it: fail-SOFT-LOG',
428+
'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:icon" | "action:group"]`. EVALUATOR, two legs: (1) SchemaRenderer node gate `evaluateVisibilityPredicate` on the hoisted value — a fault answers the fail-soft `true` (shown) and is REPORTED in every build; (2) the renderer\'s own `useCondition(toPredicateInput(schema.visible), recordData)` WITHOUT `throwOnError` (action-icon.tsx:97, action-group.tsx:238) — a fault answers `evaluateCondition`\'s fail-soft `true` as well (ExpressionEvaluator.ts:387-408), and action-icon.tsx:219-221 names that policy "fail-soft, unlike `action:button`\'s fail-closed one". Both legs show, so a faulting predicate leaves the block RENDERED, and the node gate is the leg that logs it: fail-SOFT-LOG',
427429
covers: [
428430
'ui/component.zod.ts:ActionIconPropsSchema.visible',
429431
'ui/component.zod.ts:ActionGroupPropsSchema.visible',
430432
],
431-
note: 'The opposite fault face to `cel-action-block-visible-closed` on the same key, which is why the two do not share a row. The `-log` half rests on the node gate\'s report; the renderer leg itself is silent for a bare string and warns only on the `{dialect:"cel"}` route. A literal `false` on `action:group` is honoured by the node gate, not by the renderer, whose `:306` check is a truthiness test. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.',
433+
note: 'The opposite fault face to `cel-action-block-visible-closed` on the same key, which is why the two do not share a row. The `-log` half rests on the node gate\'s report; the renderer leg itself is silent for a bare string and warns only on the `{dialect:"cel"}` route. A literal `false` on `action:group` is honoured by the node gate, not by the renderer, whose `:343` check is a truthiness test. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.',
432434
},
433435
{
434436
id: 'cel-action-block-disabled',
435437
summary: '`action:button` / `action:icon` page-block disabling (props `disabled`) — the button stays on screen and cannot be pressed while the predicate is TRUE',
436438
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-closed',
437439
enforcement:
438-
'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:icon"]`. EVALUATOR, two legs, both un-negated: (1) SchemaRenderer node gate `evaluateEnablementPredicate` on the hoisted `disabled` (SchemaRenderer.tsx:1772-1784), forwarded as the `disabled` prop — a fault answers `evaluateCondition`\'s fail-soft `true`, which on this leg means GREYED OUT, and is REPORTED in every build (SchemaRenderer.tsx:1138-1165); (2) the renderer\'s own `useCondition(toPredicateInput(schema.disabled), recordData)` (action-button.tsx:129 + :333-339, action-icon.tsx:101 + :235-241), the same `true` on a fault. The legs OR, so a faulting predicate leaves the button DISABLED: the action is refused, fail-CLOSED',
440+
'BUILD-TIME GATE, measured here: lint/page-envelope-audit.ts door 3 parses `ComponentPropsMap["action:button" | "action:icon"]`. EVALUATOR, two legs, both un-negated: (1) SchemaRenderer node gate `evaluateEnablementPredicate` on the hoisted `disabled` (SchemaRenderer.tsx:1879-1891), forwarded as the `disabled` prop — a fault answers `evaluateCondition`\'s fail-soft `true`, which on this leg means GREYED OUT, and is REPORTED in every build (SchemaRenderer.tsx:1247-1274); (2) the renderer\'s own `useCondition(toPredicateInput(schema.disabled), recordData)` (action-button.tsx:130 + :370-376, action-icon.tsx:102 + :257-263), the same `true` on a fault. The legs OR, so a faulting predicate leaves the button DISABLED: the action is refused, fail-CLOSED',
439441
covers: [
440442
'ui/component.zod.ts:ActionButtonPropsSchema.disabled',
441443
'ui/component.zod.ts:ActionIconPropsSchema.disabled',
442444
],
443-
note: 'Deliberately NOT `fail-soft-log` like `cel-action-disabled`: the renderers measured here answer a faulting `disabled` with `true`, and on an un-negated enablement leg that `true` greys the control out — SchemaRenderer.tsx:1149-1156 states exactly that asymmetry ("on the negated visibility legs that means SHOWN, here it means GREYED OUT"). The objectui fix `cel-action-disabled` cites is about an EMPTY `disabled: \'\'`, which `hasDeclaredVisibilityGate` / `hasDeclaredPredicate` now treat as no gate, not about a faulting one. ⚠️ Scope consequence worth knowing: the node-gate leg evaluates at PAGE scope, so a row-scoped `record.*` predicate that does not resolve there faults and greys the button out whatever the row says. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.',
445+
note: 'Deliberately NOT `fail-soft-log` like `cel-action-disabled`: the renderers measured here answer a faulting `disabled` with `true`, and on an un-negated enablement leg that `true` greys the control out — SchemaRenderer.tsx:1258-1265 states exactly that asymmetry ("on the negated visibility legs that means SHOWN, here it means GREYED OUT"). The objectui fix `cel-action-disabled` cites is about an EMPTY `disabled: \'\'`, which `hasDeclaredVisibilityGate` / `hasDeclaredPredicate` now treat as no gate, not about a faulting one. ⚠️ Scope consequence worth knowing: the node-gate leg evaluates at PAGE scope, so a row-scoped `record.*` predicate that does not resolve there faults and greys the button out whatever the row says. Same bare-string limit as `cel-action-block-visible-closed`. ⛔ NOT MEASURED HERE: the renderers were read at the pin, not run.',
444446
},
445447
{
446448
id: 'cel-flow',

‎packages/spec/src/ui/component-action-element-rows-20371.test.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ describe('the six rows exist and are the exported schemas (#20371)', () => {
7979
describe('key sets, asserted whole — measured from the renderers\' read points', () => {
8080
// Forwarded to the action runner by `action:button` / `action:icon`
8181
// (`execute({ ...forwarded })`). `undoable` / `recordIdField` are forwarded
82-
// by the button only.
82+
// by the button only. `objectName` joined the forward with the pin that
83+
// carries it; the four renderers forward it, the two containers per member.
8384
const FORWARDED = [
8485
'params', 'description', 'target', 'openIn', 'endpoint', 'method', 'bodyExtra', 'bodyShape',
8586
'operation', 'patch', 'confirmText', 'successMessage', 'errorMessage', 'refreshAfter',
86-
'locations', 'toast', 'resultDialog', 'onSuccess',
87+
'locations', 'toast', 'resultDialog', 'onSuccess', 'objectName',
8788
];
8889

8990
it('action:button', () => {
@@ -304,6 +305,20 @@ describe('what the measurement decided, pinned', () => {
304305
expect(limit.map((i) => [i.code, i.path.join('.')])).toEqual([['too_small', 'limit']]);
305306
});
306307

308+
it('`objectName` is declared where the renderer forwards it — on the action, never on a container', () => {
309+
for (const schema of [ActionButtonPropsSchema, ActionIconPropsSchema]) {
310+
expect(schema.parse({ label: 'Close child', objectName: 'task' })).toEqual({ label: 'Close child', objectName: 'task' });
311+
}
312+
// `action:group` / `action:menu` forward each MEMBER's `objectName`: it
313+
// rides the member object, which this row does not judge ...
314+
for (const schema of [ActionGroupPropsSchema, ActionMenuPropsSchema]) {
315+
const member = { name: 'close', label: 'Close', type: 'script', objectName: 'task' };
316+
expect(schema.safeParse({ actions: [member] }).success).toBe(true);
317+
// ... and a container-level one is read by nothing.
318+
expect(unknownKeyIssue(schema.safeParse({ objectName: 'task' })).keys).toEqual(['objectName']);
319+
}
320+
});
321+
307322
it('repeater: the `object-*` family\'s `objectName` is refused and renamed to `object`', () => {
308323
const issue = unknownKeyIssue(ElementRepeaterPropsSchema.safeParse({ object: 'task', objectName: 'task' }));
309324
expect(issue.keys).toEqual(['objectName']);

0 commit comments

Comments
 (0)