Repository navigation
Commit 75b2169
feat(spec): ComponentPropsMap rows for action:button/group/menu/icon and element:definition-list/repeater (#20420)
Fixes #20371
Clause-②: yes
## What this does
`ComponentPropsMap` gains six rows for the curated objectui public
blocks that had none: `action:button`, `action:group`, `action:menu`,
`action:icon`, `element:definition-list`, `element:repeater`. Each row
is a `strictObject` from birth, and each key set is measured from the
renderer's read points in objectui, not transcribed from
`UIActionSchema`, from the registrations' `inputs`, or from this
package's object-metadata `ActionSchema` (triage execution note 1).
Before this change the six failed in two ways:
- The four `action:*` types sit outside every namespace the
`PageComponentType` enum populates, so the props gate skipped them. Any
key inside `properties` parsed, was stored, and was ignored by the
renderer.
- The two `element:*` types sit inside the reserved `element:` namespace
with no enum member and no row, so `component-type-unknown` refused the
whole node (severity error), although objectui registers, publishes and
offers both.
A row closes both. `component-type-vocabulary.ts` derives the known set
from `Object.keys(ComponentPropsMap)`, so the two `element:*` types join
the `element:` vocabulary through their rows. This is the
`element:metadata_viewer` shape: no enum member and no string-arm ledger
entry (the vocabulary test forbids a ledger entry for a type the map
declares). The three-part evidence the ledger comment asks for
(registration, publication, authorship) is written on the map rows, with
pin citations.
Findings stay at the props gate's existing warning tier.
`PageComponentSchema` parsing is unchanged, because the open `type` arm
already admitted all six.
## Read points: measured at the pin (`.objectui-sha` `dd3f7e1be356`)
First measured at `f8a9d0fb0596`. After #20436 moved the pin, every read
point was re-derived at `dd3f7e1be356` (2026-09-28) and re-anchored
together with the sha.
Per-key citations are in `component.zod.ts` section 4b and in each
schema's docblock. The decisions the measurement made:
| Row | Declared keys | Measured, not assumed |
|:---|:---|:---|
| `action:button` | `name`, `label`, `icon`, `actionType`, `variant`,
`size`, `visible`, `disabled` + 21 keys forwarded to the runner
(`params`, `target`, `openIn`, `endpoint`, `method`, `bodyExtra`,
`bodyShape`, `operation`, `patch`, `confirmText`, `successMessage`,
`errorMessage`, `refreshAfter`, `undoable`, `recordIdField`,
`locations`, `toast`, `resultDialog`, `onSuccess`, `description`,
`objectName`) | `name` is **optional**, because the renderer reads
`schema.name ?? schema.label` (`action-button.tsx:119`). `variant`
accepts `primary` and `size` accepts `md` because the renderer maps both
(`:137-138`). `type` is refused with a rename to `actionType`. `enabled`
(the legacy fallback) and `autoTrigger` (a host transport flag, "NOT
persisted metadata") are refused with a prescription. |
| `action:icon` | same as the button, minus `size`, `undoable`,
`recordIdField` | `:107` pins the icon size, so there is no `size`.
`undoable` and `recordIdField` are not in its forward (`:134-196`). |
| `action:group` | `actions`, `display`, `location`, `label`, `icon`,
`variant`, `size`, `visible` | `actions` is a **list** of action objects
(`:248`); the registration publishes `type: 'object'`. There is no
group-level `name`: the registration publishes it (`:415`), nothing
reads it, and it is refused with a prescription. `size` takes the
primitive's four values: `md` is mapped only on the dropdown trigger
(`:357`) and reaches the Button primitive unmapped in the default inline
mode (`:398`, `:91`). |
| `action:menu` | `actions`, `label`, `icon`, `variant`, `size`,
`visible` | The trigger `variant` and `size` go to the Button primitive
unmapped (`:230-231`), so there is no `primary` and no `md`. |
| `element:definition-list` | `items` (strict `{ term, description? }`),
`columns`, `inline` | `columns` is the **number** `1 \| 2`, because the
renderer compares `=== 2` (`data-list.tsx:49`). The registration's enum
publishes the strings `'1'`/`'2'`, and the string `'2'` is refused with
a prescription. `items` is optional: absent and empty both render "No
details". `term` is required. |
| `element:repeater` | `object` (required), `titleField`, `fields`,
`filter`, `sort`, `limit`, `emptyText`, `divided` | `filter` and `sort`
use the family's one orthography, `ViewFilterRule[]` and `SortItem[]`.
Both reach the query: `ObjectStackAdapter.find` lowers rule arrays and
serializes sort items. `fields` takes a name or `{ field }`; the `label`
that the TS type advertises is never rendered and is refused. |
The value posture follows #7751. A key the renderer interprets itself
gets a value schema. A key it only forwards to the action runner gets
the scalar that `ActionDef` declares for it, or `z.unknown()` where
`ActionDef` uses a spec-derived block.
**`objectName`, carried by the new pin.** At `dd3f7e1be356`,
`action:button` and `action:icon` forward `objectName` to the runner
(`action-button.tsx:307`, `action-icon.tsx:195`), and the console
dispatches to that object instead of the page object. Both rows declare
`objectName` as the `ActionDef` string scalar. `action:group` and
`action:menu` forward it per member (`action-group.tsx:323`,
`action-menu.tsx:313`), so it rides each member object and the container
rows gain no key. The same pin reads static values from
`properties.params` (objectui#10289, `static-params.ts:91-101`). On a
page node that is the row itself, so `params` keeps its meaning: an
array is the input list and an object is the static values. Its value
schema is unchanged.
## Surface beyond the claim, and why
The claim lists `component.zod.ts`, tests in `src/ui/`, generator output
and `.changeset/`. Three gates required three more files, two in
`packages/spec` and one in `packages/qa/dogfood`. All three edits follow
from the new rows, and none was stopped on:
- `dropped-refinements.baseline.json`: `ElementRepeaterProps` publishes
`ViewFilterRuleSchema`, whose refinement the JSON Schema projection
drops. The build refuses to publish until the site is declared. I added
the entry the build printed, plus the header totals its test holds
(211→212 schemas, 609→610 sites).
- `type-alias-convention.pin.test.ts`: `gen:docs` requires a type alias
for every documented schema, and `check:spec-parsed-alias` requires an
isomorphic alias to be pinned. `ElementDefinitionListProps` is the only
isomorphic one of the six, so it gets one pin. The count is 780→781
after the merges with #19920's 786→783 and main's connector-retirement
783→780 (both intents stacked). The other five declare `XParsed`.
- `packages/qa/dogfood/test/expression-conformance.ledger.ts`:
gate-forced by the `Dogfood Regression Gate`
(`expression-conformance.test.ts`, ADR-0060 checkLedger). The six
`visible` / `disabled` predicate positions the new action rows declare
needed a classification. It has three rows, split by fault face as the
objectui renderers and `SchemaRenderer`'s node gate compose at the pin:
button/menu `visible` fail-closed, icon/group `visible` fail-soft-log,
button/icon `disabled` fail-closed. Round 4 re-anchored the three rows
at `dd3f7e1be356`; their fault faces are unchanged, because the
evaluators are code-identical across the pin hop.
`component-type-vocabulary.ts` was **not** edited. Its
`KNOWN_COMPONENT_TYPES` docblock lists the string-arm rows "exactly"
(`element:metadata_viewer`, the plugin widgets, `object-*`), and that
list no longer covers the six new rows. It is noted below, not fixed
here.
`element:repeater`'s `filter` is the bare
`z.array(ViewFilterRuleSchema)` door that `record:related_list`
declares, not a `ruleArrayFilterError` door. That prescription speaks to
a door that used to take the record form, and wiring it would pull the
repeater into the reach of the stored-row conversion
`page-component-filter-record-to-rule-array` (`conversions/registry.ts`,
whose test holds the two equal). That registry is outside this card.
## Premise checks (order zone 2)
- **A1 holds.** At `ab6fb027`, `git grep` for each quoted type in
`packages/spec/src`: the four `action:*` hit only
`action.zod.ts:983-986` (the `Action.component` enum) plus
`expression-bindable-text-keys.test.ts:126`. The two `element:*` types
hit 0. Control: `'element:text'` hits `component.zod.ts:4609` (the row).
- **A2.** Measured at the pin. The objectui checkout here is shallow and
does not contain the pin, so the pin was fetched into a scratch clone.
The two differences from `origin/main` are listed above. The pin was not
bumped.
- **A3.** The #7751 / #8691 / #8744 method is followed. The three-part
evidence is on the two `element:*` rows.
- **A4.** `node scripts/check-sdui-manifest.mjs`: see the gate table.
The manifest was not touched.
- **A5.** In this repo, **zero** authored nodes of the six types:
pattern
`['"]action:(button|group|menu|icon)['"]|['"]element:(definition-list|repeater)['"]`
plus the `type:` YAML form, excluding `node_modules`/`dist`. The hits
are `sdui.manifest.json`, `action.zod.ts`'s `component` enum, three
generated reference pages (a type union) and one test. Control with the
same shape: `'element:text'` gives 105 occurrences in 29 files,
including `examples/**` and `platform-objects`. No shipped metadata is
refused by the new rows.
## Through the lint door
A one-off probe (not a permanent test) runs `validateComponentTypes` and
`validateComponentProps` from `packages/lint/src` against the built
spec, using one stack with a planted typo on each row plus
`element:repeatr` as a control:
- **After:** `component-type-unknown` fires only on `element:repeatr`.
`component-props-unknown-key` (warning) fires on
`action:button.typo_key`, on `action:group.name`, and on
`element:repeater.fields.0.label`, which the lone union arm unpacks.
- **Before**, emulated in process by deleting the six rows and the two
known types from the same module instance the rules read:
`component-type-unknown` fires on both `element:*` types and on
`element:repeatr`, and the props gate reports **nothing**.
## Tests
The readings below are at HEAD `5e50899a4` (after merging `origin/main`
at `3cf644938`, the pin bump):
- `pnpm --filter @objectstack/spec test` (the `local` project): **565
files, 16684 passed**, 1 todo, exit 0. This includes the new
`src/ui/component-action-element-rows-20371.test.ts` (45 tests):
- key sets asserted whole;
- one objectui-sourced accepted example per type;
- an unknown-key refusal on every row;
- each measured decision above;
- vocabulary admission, with the `element:repeatr` control.
- `pnpm --filter @objectstack/spec test:repo`: **37 files, 684 passed**,
exit 0.
- `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit`,
`check:scripts-typecheck`, `check:test-typecheck`): exit 0.
- Consumers, downstream of spec (not a full `...@objectstack/spec`
sweep: the three the order names):
- `pnpm --filter @objectstack/lint test`: **115 files, 5331 passed**,
exit 0.
- `pnpm --filter @objectstack/metadata-core test`: **16 files, 289
passed**, exit 0.
- `packages/qa/dogfood` `test/expression-conformance.test.ts`: **7
passed**, exit 0.
- `@objectstack/cli` unit layer (`exec vitest run --project unit`; not
re-run this round, this is the round-1 reading): **2579 passed**, 29
skipped, 0 assertion failures. 52 files are **NOT MEASURED**: they fail
to load on `MODULE_NOT_FOUND` for workspace dependencies not built here
(`@objectstack/plugin-email`, `create-objectstack`,
`@objectstack/verify`, `@objectstack/cloud-connection`, the cli's own
`dist`). The integration layer is declared to CI, since the diff touches
no spawn entry.
- Builds (upstream direction):
- `@objectstack/spec` itself;
- lint's upstream (`--filter @objectstack/formula --filter
@objectstack/sdui-parser --filter @objectstack/lint`);
- client-react's upstream (`--filter "@objectstack/client-react..."
--filter "!@objectstack/spec"`) for `check:skill-examples`.
- eslint, a proven narrowing rather than the repo-wide `pnpm lint` (that
one is CI's):
- `eslint --no-inline-config --format json` over the diff's three TS
files (the only lintable files in it; the rest are JSON, MD and MDX,
which the config's `files` globs do not select): **3 files, 0 errors, 0
warnings**.
- `--print-config` resolves a config for each of the three files.
- `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, as its own comment near line 327 states), so
this diff cannot move any untouched file's verdict.
- Before the merge, the spec suite had one failure: the
dropped-refinements header totals. `7bd546c1` fixed it.
## Gates
The derived union at `5e50899a4` is 108 commands (`node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, with no paths, off the merge base). It is a
superset of the 72-line dispatch list, adding 36 families for the
changeset, the docs and the pin test. Every exit code was written to
disk before its output was read.
- **107 exit 0.** These include `check:type-check-debt` (to a verdict
this time: 4 ledger entries re-measured, none above its recorded
number), `check:dts-closure`, `check:generated`, `check:api-surface`,
`check:authorable-surface`, `check:docs`, `check:strictness-ledger`,
`check:objectui-pin-citations`, `check:spec-parsed-alias`,
`check:yaml-examples`, `check:liveness`, `check:issue-citations`,
`check:nul-bytes` and `check:skill-examples` (after building the client
closure).
- **NOT MEASURED: `pnpm check:dual-build-cjs-loads`.** It exited 3
(PREREQUISITE NOT MET) because it reads every workspace package's
`dist`, which needs a whole-repo build. This diff changes only
`@objectstack/spec`'s build output.
- `dispatch-gates --ran`: "108 derived famil(ies) accounted for — 107
run, 1 NOT-MEASURED".
- `node scripts/check-sdui-manifest.mjs`: exit 0. The manifest is
untouched by this branch and recorded at pin `dd3f7e1be356` (A4: no
lockstep gate moved).
- `check:objectui-pin-citations`: exit 0 (49 asserting citations match
`dd3f7e1be`, 61 historical). `--verify-anchors` against a `dd3f7e1be`
clone: exit 0.
## Acceptance notes (not filed; the seat decides)
- **objectui producer side** (carrier: the objectui#10872 follow-up the
seat files at ACCEPT; its `registry-inputs-spec-parity` gate will
surface each of these on the spec bump):
- the `action:group` registration publishes `name`, which nothing reads,
and a `size` enum with `md`, which inline mode does not map;
- the `element:definition-list` registration's `columns` enum is the
strings `'1'`/`'2'` (the designer writes numbers);
- `element:repeater`'s TS type and registration description advertise
`fields[].label`, which is never rendered.
- **objectui renderer, read-only inference, not reproduced** (carrier:
none):
- `action:menu` spreads `...rest` onto its trigger after
`disabled={loading}`, so `SchemaRenderer`'s `disabled: undefined` can
override the in-flight disable (the objectui#9131 shape it fixed on
button/icon);
- `action:menu` and inline `action:group` spread hoisted props
(`actions`, `label`, …) raw onto DOM elements.
- **`component-type-vocabulary.ts`'s `KNOWN_COMPONENT_TYPES` docblock**
enumerates the string-arm rows as "exactly" `element:metadata_viewer`,
the plugin widgets and `object-*`; the six new rows are not in that
list. This is prose drift and not edited, because the file is outside
the claimed surface (carrier: the next edit of that file).
- **`element:repeater.filter`** is a bare rule-array door. Wiring it to
`ruleArrayFilterError` means adding `element:repeater` to
`RULE_ARRAY_FILTER_BLOCK_TYPES` in `conversions/registry.ts` in the same
change (carrier: none).
## Changeset
`@objectstack/spec` `minor`: six new public rows, two types admitted to
the `element:` vocabulary, and nothing that a declared row accepted is
refused.
## Downstream
objectui#10872 can now arm the six by reference. When it bumps
`@objectstack/spec`, its `registry-inputs-spec-parity` gate will judge
the six in both directions:
- The forward direction flags `name` on `action:group` and the
`'1'`/`'2'` string enum on `columns`.
- The reverse direction lists the read-but-unpublished keys: 22 on
`action:button`, 20 on `action:icon`, `location`/`visible` on the group,
and `size`/`visible` on the menu.
That reconciliation belongs to objectui.
The `Surface beyond the claim` section was amended by the `domain:spec`
seat 1 (`session_01B3TqpoQbTAfG7G74GMDWNW`) after the patch round, from
the dev's delta `5868569396`.
Round 4 (after #20436 moved `.objectui-sha` to `dd3f7e1be356`): the
read-points heading, the table anchors, the `objectName` paragraph,
Tests, Gates, Acceptance notes and Downstream were amended by the same
seat from the dev’s delta (report `5873527479`).
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5b674f5 commit 75b2169
14 files changed
Lines changed: 1394 additions & 20 deletions
File tree
- .changeset
- content/docs/references
- ui
- docs/audits
- packages
- qa/dogfood/test
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- src
- ui
| 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 | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
363 | 363 | | |
364 | 364 | | |
365 | 365 | | |
366 | | - | |
| 366 | + | |
367 | 367 | | |
368 | 368 | | |
369 | 369 | | |
| |||
374 | 374 | | |
375 | 375 | | |
376 | 376 | | |
377 | | - | |
| 377 | + | |
378 | 378 | | |
379 | 379 | | |
380 | 380 | | |
| |||
0 commit comments