Skip to content

feat(spec): ComponentPropsMap rows for action:button/group/menu/icon and element:definition-list/repeater - #20420

Merged
objectstack-fleet[bot] merged 17 commits into
mainfrom
claude/issue-20371-component-props-action-element-rows
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 17 commits into
mainfrom
claude/issue-20371-component-props-action-element-rows

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

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 [finding] four more exported spec types resolve to unknown while their TSDoc promises a shape — ViewMetadataParsed, InlineAction, AssembledViewArtifact, JoinedReportBlock (the #19871 class, other sites) #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)

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).

…and element:definition-list/repeater

Six curated objectui public blocks had no row: the props gate skipped the
four action:* types and component-type-unknown refused the two element:*
lists. Each row is strict from birth, with its key set measured from the
renderer read points at the objectui pin.

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
… dropped filter refinement

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
…s for the six rows

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
…esolved citation

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
…authorable-surface, export-origins, declaration-map)

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
…ction-element-rows

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
…nts on the merged tree

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 28 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/declaration-map/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via actionType (literal, a string literal in ACTION_NODE_ALIASES))
  • content/docs/deployment/validating-metadata.mdx (via actionType (literal, a string literal in ACTION_NODE_ALIASES))
  • content/docs/protocol/objectui/layout-dsl.mdx (via ComponentPropsMap (symbol, a top-level const object))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via actionType (literal, a string literal in ACTION_NODE_ALIASES))
  • content/docs/releases/v17/17-0.mdx (via actionType (literal, a string literal in ACTION_NODE_ALIASES))
  • content/docs/releases/v17/17-1.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-3.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-5.mdx (via autoTrigger (symbol, a field of const object ACTION_NODE_GUIDANCE))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/declaration-map/ui.json, …) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3cf64493899458632f87e661fff1b130bd3a8273 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eac80982bafa158af1fff732c3b7505b54162e85 — the merge of head 5e50899a481dce659bc12ad2576b4fc534cd3893 into base 3cf64493899458632f87e661fff1b130bd3a8273, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin eac80982bafa158af1fff732c3b7505b54162e85 && git checkout eac80982bafa158af1fff732c3b7505b54162e85
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3cf64493899458632f87e661fff1b130bd3a8273 5e50899a481dce659bc12ad2576b4fc534cd3893 && git checkout -B drift-repro 3cf64493899458632f87e661fff1b130bd3a8273 && git merge --no-ff 5e50899a481dce659bc12ad2576b4fc534cd3893

node scripts/docs-audit/affected-docs.mjs --json 3cf64493899458632f87e661fff1b130bd3a8273

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3cf64493899458632f87e661fff1b130bd3a8273 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ession surfaces

Three ADR-0060 conformance rows for the six predicate positions the new
ComponentPropsMap rows declare, split by the fault face the objectui
renderers and SchemaRenderer's node gate compose to at the pin.

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 29fd4591e4c58735a265b1a19df759acd05471d5
Local-runs: none

Inputs: card #20371 (body, all five comments; triage notes 5863783308 binding), PR #20420 (body, 14-file list, net diff against main), the check-runs on the head (polled until none was in progress), origin/main files by git show, and objectui source at the pin the rows cite (.objectui-sha = f8a9d0fb0596, fetched and read, never built or run). The seat's ACCEPT (5868610882) was treated as a claim and re-tested. The head did not move during the review.

① Derived judgments

Every accept-set and public-surface change the diff implies, each tested against the pin:

  1. Vocabulary admission of element:definition-list / element:repeater — right. KNOWN_COMPONENT_TYPES is Object.keys(ComponentPropsMap) plus the enum plus the string-arm ledger (component-type-vocabulary.ts:96-100), so the two rows admit both types with no enum member and no STRING_ARM_REGISTERED_TYPES entry, the element:metadata_viewer shape; the vocabulary test forbids a ledger entry for a type the map declares. The three-part evidence holds at the pin: registration components/src/renderers/basic/data-list.tsx:75 and :184 (namespace element); publication core/src/registry/public-blocks.ts:109-110, and the tracked sdui.manifest.json on main carries both; authorship app-shell/.../previews/block-types.ts:129-130 (palette) and previews/block-config.ts:282-316 (inspectors). The element:repeatr control stays refused: the namespace was not opened, two members were named.
  2. The four action:* rows add dispatch, not vocabulary — right. action: is in no enum member, so hasReservedComponentNamespace stays false and the rows only give the props gate a schema to dispatch on. PageComponentSchema parsing is unchanged (open type arm; the test pins it). Findings land at the props gate's existing severity: 'warning' (packages/lint/src/validate-component-props.ts), so no previously accepted node is refused at any door.
  3. action:button, 28 keys — right. Each read point re-checked at the pin: name :118 (schema.name ?? schema.label) and :188, so optional is the measured state; label :346/:192; icon :133; actionType :187; variant/size :136-137 map primary and md to default, which is why those two values are accepted only here; visible/disabled :116-119/:129 with the gates at :298/:333-339; the twenty forwarded keys are exactly the forwarded: ActionDef literal at :177-271 plus the params payload at :153-155. Value posture matches the runner's ActionDef (core/src/actions/ActionRunner.ts): openIn is 'self' | 'new-tab' (:316), refreshAfter/undoable boolean (:270/:272), method/endpoint/target/confirmText/successMessage/errorMessage string, locations the spec's own, and the spec-derived blocks (bodyExtra, bodyShape, operation, patch, onSuccess, resultDialog) z.unknown(). Refusals are measured too: type aliased to actionType (the objectui#7415 rename the forward at :177-187 documents), enabled (:130/:336, legacy fallback) and autoTrigger (:291; auto-trigger.ts:18-19 says it is not persisted metadata) refused with prescriptions, className a node key (:306), objectName not read anywhere in the four renderers at the pin (grep: test files only), so deferring it to the pin bump is correct. One nit, not a wrong accept: toast is left z.unknown() where ActionDef:264 types a small concrete object; a later ratchet, as section 4b already says of the forwarded blocks.
  4. action:icon, 25 keys — right. size is fixed to the primitive's icon at :106 and refused with a prescription; undoable/recordIdField are absent from the forward at :119-174; label :137/:243/:252/:258/:264, description :138/:264; visible :96 + :207, disabled :101 + :236; variant :105 maps primary, default ghost. The 18-key forwarded set in the test equals the literal at :132-173.
  5. action:group, 8 keys — right. actions is read as a list (:243, schema.actions || []) although the registration publishes type: 'object' (:381); location feeds actionRendersAt (:244; types/src/ui-action.ts:85-98 takes an ActionLocation and passes every member when it is undefined); display :309; label/icon are dropdown-only (:328/:313); variant has no primary map at group level (:319, and :360 to :88 maps only a member's own value); size maps md on the dropdown trigger only (:320) and reaches the Button primitive raw in inline mode (:361 to :89), so declaring the primitive's four sizes is the read; visible :233 + :306. No group-level name is read anywhere in action-group.tsx or action-menu.tsx (grep: zero schema.name hits; the only name reads are member action.name keys), while the registration publishes it (:378), so refusing it with a prescription is right. Option A on the dev's open question is the measured answer (③.1).
  6. action:menu, 6 keys — right. actions :299; label :341 and :350-351; icon :224; variant/size handed to the primitive unmapped (:225-226), so no primary/md; visible :219-222 with throwOnError and the gate at :293. The registration (:381-390) publishes neither size nor visible.
  7. actions members on group/menu as z.array(z.record(z.string(), z.unknown())) — right as the list shape, with the member left unjudged. The row is strict; the member is not a page component (the containers draw and run it themselves) and judging its keys would mean transcribing UIActionSchema, which triage forbade. A bare string is refused (the record:quick_actions name-list confusion), which the test pins. The member key set is a later ratchet, named here so nobody reads the row as having measured it.
  8. element:definition-list, 3 keys — right, and isomorphic. items optional (:48 guards with Array.isArray, :51-53 renders "No details" for absent and empty alike); item strict { term, description? } (:66/:68, toText), with label/value aliased to term/description (the objectui#8279 pair block-config.ts:288-300 records); columns the number literal 1 | 2 because :49 compares === 2, the designer writes a number (block-config.ts:306), and the registration's string enum (:82) is refused with a prescription that names the collapse to one column; inline :63. No default, transform, catch or pipe anywhere, so the alias pin is the correct ADR-0122 disposition.
  9. element:repeater, 8 keys — right. object required (:122 never queries without it; registration :190 agrees); titleField :170-171; fields a name or { field } (:116, :175), with label refused because RepeaterColumn.label (:93) is never rendered; filter ViewFilterRule[] reaches $filter (:131) and ObjectStackAdapter.find lowers the object-form array through translateFilterArray (data-objectstack/src/index.ts:4793-4804); sort SortItem[] reaches $orderby (:132) and serializeOrderBy (:772-777) serializes { field, order } items; limit positive int (:133); emptyText :160; divided :165. The Studio field-list control writes string[] (inspectors/PageBlockInspector.tsx:86), so designer-built repeaters pass the row. The aliases are a subset of ElementDataSourceSchema's (page.zod.ts:197-203) plus objectName. The bare z.array(ViewFilterRuleSchema) door (the record:related_list :1270 shape) rather than ruleArrayFilterError is right: RULE_ARRAY_FILTER_BLOCK_TYPES (conversions/registry.ts:10741) is the object-* family whose filter had a record-form past; this door never did.
  10. Strict from birth — right. All six rows and both nested items are strictObject; the strictness ledger counts move ui/ 180 to 188 sites, strict 170 to 178, component.zod.ts 48 to 56, which is exactly six rows plus two items.
  11. dropped-refinements.baseline.json entry — right. ui/ElementRepeaterProps at filter.element is the same site shape every ViewFilterRuleSchema door in the map carries; totals 211 to 212 schemas and 609 to 610 sites are one schema, one site.
  12. Alias pin — right. Iso_ui_component__ElementDefinitionListPropsSchema is the only isomorphic row of the six: the four action:* rows carry EvaluatedExpressionInputSchema (bare string normalized to the envelope) and the repeater carries ViewFilterRuleSchema (operator normalized), and each of those five declares XParsed. Count 780 to 781 was re-derived from the merged file after the two stacked merges; check:spec-parsed-alias is inside the green Lint & Repo Gates.
  13. The three ADR-0060 ledger rows — all three classes right at the pin. visible and disabled are node-gate chain keys (SchemaRenderer.tsx:249, :313), so every surface has two legs, as the rows say.
  • cel-action-block-visible-closed (button/menu): useCondition(..., { throwOnError: true }) at action-button.tsx:116-119 / action-menu.tsx:219-222 returns false on a throw and warns once (react/src/hooks/useExpression.ts:215-238); the renderers return null (:298 / :293). The node gate answers fail-soft true (SchemaRenderer.tsx:1133) and reports. The legs AND, so a faulting predicate hides the block: fail-closed.
  • cel-action-block-visible-soft (icon/group): useCondition without the option (action-icon.tsx:96, action-group.tsx:233) reaches evaluateCondition's catch, which calls onFault and returns true (core/src/evaluator/ExpressionEvaluator.ts:387-408); both legs show and only the node gate's report logs it: fail-soft-log. action-icon.tsx:197-198 names its own policy in those words.
  • cel-action-block-disabled (button/icon): the renderer leg returns true on a fault (:129 / :101) and the gate ORs it in (:333-339 / :235-241); the node gate's evaluateEnablementPredicate (:1772-1784) also answers true, which on this leg is greyed out (:1149-1156), and reaches the renderer as hostDisabled. The legs OR, so the action is refused: fail-closed. The covers keys name exactly the six positions the discovery found (the previous head's red, this head's green Dogfood Regression Gate (3/3)).
  1. Generated artefacts — additive. api-surface/export-origins +17 (6 schemas, 6 author types, 5 XParsed), json-schema.manifest +6, declaration-map +12, references index 1516 to 1522 and component.mdx +173; check:generated / check:api-surface / check:authorable-surface are inside the green Lint & Repo Gates.
  2. Tests — right shape. Key sets asserted whole per row; one objectui-sourced accepted specimen per type (the action:button node is objectui AGENTS.md:98 verbatim); unknown-key refusal per row; every measured decision above pinned; vocabulary admission with the typo control and the action: unreserved control.

② Semver level

@objectstack/spec minor with Clause-②: yes (no (widening)/(narrowing) arm, which the rule allows) matches the diff. What publishes: six new exported schemas and eleven type aliases from @objectstack/spec, six new ComponentPropsMap rows, two new members of the element: vocabulary. Nothing declared is retired or renamed, so no migration text and no ADR-0087 marker is owed. The accept set widens (two types refused as component-type-unknown are accepted; four types previously skipped by the props gate are judged at warning tier); the PR's A5 sweep found zero authored nodes of the six types in this repo, and I found none in content/docs outside the generated reference (the actions.mdx:215 hit is Action.component, a different declaration). The only other touched package, @objectstack/dogfood, is private: true and the change is a test ledger. @objectstack/lint's door behaviour moves only through its spec dependency; no lint source changed. Check Changeset is green. The PR body carries Clause-②: yes on its second line. Not a governed surface: the file list touches none of docs/adr/**, docs/NORTH-STAR.md, .claude/**, skills/**, AGENTS.md, CLAUDE.md; Governed Surface Queue Guard is green.

③ Boundary flags

  1. open_questions[0] — name on action:group / action:menu: answered, option A is right. Neither renderer reads schema.name at the pin (grep over both files: zero hits; action.name on members only, action-group.tsx:336/:358, action-menu.tsx:322/:362). Triage note 1 ("measure each row from the renderer's read points") and ADR-0049 (declared means enforced) both decide A. The group refuses it with a prescription because its registration publishes it (:378); the menu's registration does not (:381-390), so the generic unknown-key refusal there is enough.
  2. cel-action-block-disabled fail-closed against the existing cel-action-disabled fail-soft-log: answered right, and one part escalated. The split is correct: different declaration (ActionButtonPropsSchema.disabled vs actionObject.disabled), different path (page-component renderer plus node gate vs the registered-action surfaces), and the measured face at the pin is closed. Leaving the old row untouched is correct under the claim's surface. Escalated, not fixed here: the old row's face rests on a console CHANGELOG line about the empty-disabled fix, which visibility-gate.ts:21-29 confirms was about '' and empty envelopes, not faulting predicates; the member leaves of action:group/action:menu evaluate a registered action's disabled through the same useCondition without throwOnError (action-group.tsx:84/:167, action-menu.tsx:106), which greys out on a fault. So cel-action-disabled may be mis-measured for the same reason this PR's row is closed. Read-only inference, not reproduced. Carrier: a follow-up card on the dogfood ledger, filed by the seat, not this PR.
  3. Docs drift, content/docs/protocol/objectui/layout-dsl.mdx:736-742: answered, no edit needed. The callout is generic ("for the platform's own types the authoring rules dispatch ComponentPropsMap and reject a misspelled prop; a custom.* type has no entry there"); it names no type and no row, and the page mentions none of the six types. The claim becomes more true with six more rows. Its "reject" wording against the gate's warning tier predates this diff and is not this card's.
  4. Out-of-surface files: all four accepted as gate-forced companions, each minimal and correctly formed. dropped-refinements.baseline.json (build refuses to publish an undeclared dropped site; the entry is the build's printed form; hand-edited by that file's own design, so outside the claim's "generated" letter but inside its intent); type-alias-convention.pin.test.ts (forced by check:spec-parsed-alias, one pin, count re-derived); packages/qa/dogfood/test/expression-conformance.ledger.ts (forced by the Dogfood gate's checkLedger; classes verified in ①.13; tracker ids kept in // comments for check:doc-authoring); docs/audits/...counts.md (generated, counts consistent). The dev reported each as a deviation rather than stopping, and the seat amended the PR body with the third; that is the right handling for a companion a gate forces.
  5. KNOWN_COMPONENT_TYPES docblock (component-type-vocabulary.ts:81-84) — escalated. It still enumerates the string-arm rows "exactly" as element:metadata_viewer, the plugin widgets and object-*; the map now exceeds that list by six. The derivation is code and stays correct; only the prose lags, in a file the claim excluded. Carrier: the next edit of that file (a one-line docs-only change; the objectui follow-up's spec bump is the natural moment).
  6. objectName on the four action rows — answered, right not to declare. Not read at the pin; objectui origin/main forwards it, so it joins with the pin bump that carries that read.
  7. element:repeater.filter as a bare rule-array door — answered, right (①.9); wiring ruleArrayFilterError would require adding the type to RULE_ARRAY_FILTER_BLOCK_TYPES in the same change, which is a conversions decision outside this card.
  8. objectui-side findings — verified at the pin, carried to the objectui follow-up the seat files at landing (Blocked-by: #20371). Registration publishes an unread name and an unmapped md on action:group (:378, :395); columns enum is strings (data-list.tsx:82); fields[].label advertised and unrendered (:93, :192); action:menu spreads ...rest after disabled={loading} (action-menu.tsx:340-342), the objectui#9131 shape, which no carrier names yet: the seat should add it to that follow-up.

CI on this head: 46 check-runs, none in progress at render time: 39 success, 7 skipped (opt-in and label jobs), 0 failures. The previous head's red (Dogfood Regression Gate (3/3)) is green on this head, and mergeable_state reads clean.

Implemented-by: claude/issue-20371-component-props-action-element-rows
Reviewed-by: session_01B3TqpoQbTAfG7G74GMDWNW

VERDICT: PASS


Generated by Claude Code

… merged tree (keeps the docs title rule)

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Regen-provenance: 5868826831 · 29fd4591e4c58735a265b1a19df759acd05471d5 → 28681fc5baabf169602c2de284b3ab99ebeeba1d · the PR's net diff of hand-written files, old head against its merge base vs new head against its merge base → (empty)

domain:spec seat 1 (session_01B3TqpoQbTAfG7G74GMDWNW) · 2026-09-28T12:19Z. The at-tier record 5868826831 (PASS at 29fd4591) carries over one pure-regeneration hop.

What the hop is: the dev's base-merge round 5869671220. bash scripts/pm/os-regen-merge.sh merged main e4d3f2ca6 (#20401, the title rule for the generated reference pages) as ded6f2066, and 28681fc5 regenerated the docs. No source file changed.

What the seat measured on the committed trees:

  • The merge bases are 50e273fd for the old head and e4d3f2ca for the new.
  • git diff --name-only BASE HEAD lists the same 14 files at both heads.
  • For each hand-written file, the added and removed lines hash identically at both heads (old base..old head against new base..new head): component.zod.ts 23b7188e, the row test e1d28ca8, the dogfood ledger 5b3fcccb, dropped-refinements.baseline.json 85944912, the changeset 4af65152. The pin test's hunk still reads 780→781.
  • The rest of the files that moved between the two heads are main's own movement plus the regenerated references. ui/component.mdx keeps docs(spec): generated reference pages follow the docs title rule, sidebar labels kept via navTitle #20401's title / navTitle and adds the six sections.

This line is a pointer; the queue guard re-runs the content test itself.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Pulled from the merge queue by the domain:spec seat 1 (session_01B3TqpoQbTAfG7G74GMDWNW) at 2026-09-28T14:03Z: converted to draft and auto-merge disabled.

Signature. The merge group pr-20420-3cf644938 sits on PR #20436, which is ahead in the queue and moves .objectui-sha from f8a9d0fb0 to dd3f7e1be. In that group Type Check · source gates failed (job 108955578884) on check:objectui-pin-citations: 2 spec source(s) assert an objectui pin this repo does NOT build against, at packages/spec/src/ui/component.zod.ts:2686 and :5371, both citing f8a9d0fb0. The same signature failed the group behind it (pr-20414, job 108955940928), because that group carries this PR.

Why this is not a flake, and whose it is. It is a semantic conflict with a PR ahead in the queue, and it is deterministic. Every merge of this head after PR #20436 fails the same way. The rows' read points were measured at the old pin, so a citation refresh alone would be the "confident lie" the gate names.

Fix (patch round, after PR #20436 lands).

  • Merge origin/main through scripts/pm/os-regen-merge.sh.
  • Re-measure every read point the six rows cite at dd3f7e1be, and move anchors and sha together.
  • Apply the PR's own note on objectName: it becomes a row addition if the new pin carries that forward.
  • Re-verify, then take a fresh at-tier record on the new head before re-queueing.

Pulling it early lets PR #20414 and PR #20423 regroup without it.


Generated by Claude Code

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 28, 2026
…eclare 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>
… objectName

Output of `pnpm --filter @objectstack/spec check:generated --fix` after
action:button and action:icon gained `objectName`.

Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5e50899a481dce659bc12ad2576b4fc534cd3893
Local-runs: none

Inputs: card #20371 (body and all seven comments, the round-4 report 5873527479 included), PR #20420 (body, 14-file list, net diff against its merge base 3cf644938, the patch round's own diff abf69011a..5e50899a4, the earlier record 5868826831, the Regen-provenance line and the audit comment 5871523278, all read as claims and re-tested), the check-runs on this head (polled until none was in progress), origin/main files by git show, and objectui in a scratch clone read with git show and never built or run: dd3f7e1be356 for every anchor below, and f8a9d0fb0596 only to diff the evaluators across the hop. The head did not move during the review; origin/main's .objectui-sha reads dd3f7e1be356 at review time; the PR is mergeable: clean, draft, auto-merge off.

① Derived judgments

Every public-surface change the diff implies, re-tested at the new pin:

  1. Re-anchoring is a re-measurement, not a sha rewrite — right. The patch round's own diff touches 174 lines of component.zod.ts, 18 of the ledger, 19 of the row test and 4 of the changeset, and every moved anchor I opened lands on the line it claims. Spot-checked per row at dd3f7e1be356: button :119 (schema.name ?? schema.label), :130, :134, :137-138, :176-179, :211-212, :216, :307, :335, :370-376, :383, :395-416; icon :97, :102, :106-107, :130-133, :147-148, :152-153, :195, :229, :257-263, :265, :274, :280-286; group :81-134, :91, :166-204, :238, :248-249, :274-280, :323, :343, :346, :350, :356-357, :365, :397-398, :415, :418, :432; menu :80, :108-147, :224-227, :229-231, :253-259, :313, :322, :328, :349-356, :369-371, :379-380, :410-419; static-params.ts:91-101, :142-148, :172-183; data-list.tsx:42-49, :51-53, :63-68, :75, :82, :93, :97-107, :119-120, :128, :143-146, :152-155, :181, :186, :191-196, :205, :213; public-blocks.ts:117-122; block-types.ts:136-137; block-config.ts:283-317 (:307 the number control); ActionRunner.ts:406; data-objectstack/src/index.ts:4782-4793 and :760-786; ui/button.tsx:19-35; auto-trigger.ts:18-19; objectui AGENTS.md:98. The two asserting citations (section 4b and the map rows) read .objectui-sha = dd3f7e1be, the first measurement is kept in the historical spelling, and check:objectui-pin-citations runs green inside Lint & Repo Gates on this head.

  2. objectName on action:button and action:icon, and on nothing else — right. The hop's renderer diff is exactly what the report says: the button and icon forward literals gained objectName: (schema as any).objectName (action-button.tsx:307, action-icon.tsx:195), and the two container handleExecutes gained objectName: (action as any).objectName off the MEMBER (action-group.tsx:323, action-menu.tsx:313). A grep of both container files finds no schema.objectName read at all, so a container-level key would be read by nothing, and declaring it would ship the declared-but-unenforced key ADR-0049 forbids. The row test pins all three halves (accepted on button/icon, accepted on a member, refused at container level). The value is z.string().optional(), the scalar ActionDef types it as (SpecActionInput['objectName'], ActionRunner.ts:406) and the shape every sibling row's objectName carries; the describe restates the renderer's own comment at :295-306 (dispatch target action.objectName, falling back to the page object). The regenerated authorable-surface/ui.json and component.mdx gain exactly the two lines.

  3. params keeps z.unknown() and its meaning — right. At the new pin readStaticParamValues (static-params.ts:91-101) reads the static values off properties.params and warns on a node-level object that is not the hoisted copy (:97-98). On a page node this row IS properties, so an array is still the input list (actionParams, :177-178 / :131-132) and an object is still the static values; the docblock now says so and the describe stays true. Nothing this row judges changed shape.

  4. element:repeater.filter through useResolvedFilter — right, no schema change. data-list.tsx:119-120 resolves context tokens in the rule array before $filter (:152); the value is still ViewFilterRule[], and a rule's string value already admits a token. The bare z.array(ViewFilterRuleSchema) door is the record:related_list (:1282) and picker (:1318) shape, not a ruleArrayFilterError door, and page-component-filter-record-to-rule-array.test.ts:500-503 derives RULE_ARRAY_FILTER_BLOCK_TYPES from the rows that refuse a record with that prescription, so the repeater correctly stays out of the conversion's reach and that test holds.

  5. The six key sets and value schemas otherwise — right and unchanged at the new pin. Button 29 keys (8 interpreted plus 21 forwarded: the literal :211-307 and the params payload); icon 26 (no size, :107 fixes icon; no undoable / recordIdField, absent from :147-195); group 8 (actions a list at :248 against the registration's type: 'object' at :418; no group-level name, published at :415 and read nowhere; size the primitive's four because inline mode hands it raw at :398 and :91 maps only a member's own md); menu 6 (variant / size unmapped at :230-231); definition-list 3 (columns === 2 at :49 against the string enum at :82; items optional, :48 / :51-53; item strict { term, description? }, :66 / :68); repeater 8 (object required, :143-146; fields a name or { field }, :128 / :196, label at :93 / :213 never rendered). Button primitive vocabulary confirmed at ui/button.tsx:19-35 (six variants, four sizes). openIn is 'self' | 'new-tab' (ActionDef:316), refreshAfter / undoable boolean (:270 / :272). Same nit as the earlier record: toast is z.unknown() where ActionDef:264 types a small object; a later ratchet, as section 4b says.

  6. Vocabulary admission of element:definition-list / element:repeater — right. KNOWN_COMPONENT_TYPES is the enum plus Object.keys(ComponentPropsMap) plus the string-arm ledger (component-type-vocabulary.ts:96-100), so the rows admit both with no enum member and no ledger entry. The three-part evidence holds at the new pin: registration data-list.tsx:75 / :205 (namespace element), publication public-blocks.ts:117-118 and the tracked sdui.manifest.json on main carrying all six, authorship block-types.ts:136-137 and block-config.ts:283-317. The element:repeatr control stays refused. The four action:* rows add dispatch, not vocabulary (action: is in no enum member; the test pins hasReservedComponentNamespace false).

  7. Dropped-refinements baseline — right. ui/ElementRepeaterProps at filter.element, 211 to 212 schemas, 609 to 610 sites: one schema, one site, unchanged by the merge.

  8. Type-alias pin — right. ElementDefinitionListPropsSchema is the only isomorphic row (a strict item of z.string() and z.unknown(), z.literal([1, 2]), z.boolean(), no default, transform, catch or pipe); the other five carry EvaluatedExpressionInputSchema or ViewFilterRuleSchema and declare XParsed. Count 780 to 781, re-derived from the merged file; check:spec-parsed-alias is inside the green Lint & Repo Gates, and tsc proves the pin.

  9. The three ADR-0060 ledger rows and their fault faces — right, and the "code-identical across the hop" claim is verified. I diffed the two pins: evaluateVisibilityPredicate (old :1048-1140 against new :1157-1249), evaluateEnablementPredicate (old :1138-1200 against new :1247-1309) and isDisabled (old :1772-1784 against new :1879-1891) are byte-identical; useExpression.ts differs in comments only; ExpressionEvaluator.ts is unchanged. At the new pin: button/menu visible use throwOnError: true (:117-120, :224-227), which returns false on a throw (useExpression.ts:215-238) and the block returns null (:335, :322), so the legs AND and the face is fail-closed; icon/group visible use useCondition bare (:97, :238), which reaches the evaluateCondition catch and answers true (ExpressionEvaluator.ts:387-408), and action-icon.tsx:219-221 names the policy, so fail-soft-log; button/icon disabled answer true on a fault (:130 / :102), the gate ORs it in (:370-376 / :257-263) and the node gate greys out (:1258-1265), so fail-closed. covers names the six positions; Dogfood Regression Gate (3/3) is green.

  10. Generated artefacts — additive against the merge base. api-surface / export-origins +17 (6 schemas, 6 author types, 5 XParsed), json-schema.manifest +6, declaration-map +12, authorable-surface +80 / 0 removed, references 1516 to 1522 (UI 159 to 165), strictness counts +8 sites (six rows plus two nested items: ui/ 180 to 188, component.zod.ts 48 to 56). check:generated, check:api-surface, check:authorable-surface, check:docs and check:strictness-ledger are inside the green Lint & Repo Gates.

  11. Tests — right shape. 45 tests: key sets asserted whole per row (objectName in FORWARDED), one objectui-sourced specimen per type (the button node is objectui AGENTS.md:98 verbatim), an unknown-key refusal per row, each measured decision pinned including the new objectName pin, and vocabulary admission with both controls.

② Semver level

@objectstack/spec minor with Clause-②: yes and no arm. Read with clause2-line.mjs's definition, yes takes at least minor, and the line is present in the changeset, on the PR body's third line and in the claim (5864633609). What widens: six new public rows and their exported schemas and types, two members of the element: vocabulary, and objectName on two rows (an optional key on rows that do not exist on main, so a widening of the accept set and of the authorable surface, a narrowing nowhere). Nothing declared is retired or renamed, so no migration text and no ADR-0087 marker is owed. The only other touched package, @objectstack/dogfood, is private: true. Not a governed surface (none of the 14 paths is a register row); Governed Surface Queue Guard and Check Changeset are green.

③ Boundary flags

  1. Deviation A — objectName on two rows rather than the four the order named: answered, the two-row reading is right. The order's condition was "if the pin carries it"; what the pin carries on the containers is a per-member forward (①.2). Declaring it on the container would be read by nothing, and the containers' registrations do not publish it, so the generic unknown-key refusal (pinned) is proportionate; a prescription like the group's name one is not owed because no producer writes it there.

  2. Deviation B — no quoted first lines added, ASSERTED_ANCHOR_FLOOR (7) unchanged: acceptable on this head, escalated. The gate's own design makes coverage "a ratchet, not a migration" and refuses a bulk fill, so an absent quote is not a gate breach. But section 4b now carries roughly a hundred anchors with zero machine-checkable content assertions, which is exactly the class that cost this PR a merge-queue ejection and a patch round, and the dev re-read every anchor by hand, the one moment the gate's docblock names for adding quotes. On this head my own re-read (①.1) is the compensating control. Carrier: a follow-up on the six rows' anchors (a quoted first line on the load-bearing anchors, and the floor raised by that number), filed by the seat or folded into the next pin bump; not this PR.

  3. The out-of-scope finding (readActionEntryParamValues returns an object params as the api payload until 18, static-params.ts:179-180): answered, no carrier owed here. It concerns a MEMBER's params, which the container rows deliberately do not judge (①.5), and it is objectui's own documented window (static-params.ts:162-166). Nothing in these rows should change.

  4. Earlier acceptance notes still open, each re-read at the new pin:

    • KNOWN_COMPONENT_TYPES docblock (component-type-vocabulary.ts:81-84 on main) still says "exactly" element:metadata_viewer, the plugin widgets and object-*; the map exceeds it by six. Prose drift in a file the claim excluded; the derivation is code and correct. Carrier: the next edit of that file. Escalated.
    • action:menu spreads ...rest after disabled={loading} (action-menu.tsx:369-371), and inline action:group spreads ...rest onto its wrapping div (:390): both still present at dd3f7e1be356. Read-only inference; no carrier names it yet. Escalated: the seat should add it to the objectui follow-up it files at landing.
    • cel-action-disabled may be mis-measured (the earlier record's ③.2): the member leaves still evaluate a registered action's disabled through useCondition without throwOnError (action-group.tsx:86 / :169, action-menu.tsx:108) and grey out on a fault, so that row's fail-soft-log face rests on the empty-disabled fix, not on a faulting predicate. Unchanged by this PR and correctly left alone under the claim's surface. Carrier: a follow-up card on the dogfood ledger, filed by the seat. Escalated.
    • element:repeater.filter as a bare rule-array door: answered right (①.4).
    • Docs drift, layout-dsl.mdx:736-742: the callout is generic (it names no type and no row) and becomes more true with six more rows; no edit owed.
    • toast as z.unknown(): a later ratchet, not a wrong accept.
  5. objectui producer-side notes carried by the objectui#10872 follow-up — verified at the new pin, still true, carried. The action:group registration publishes an unread name (:415) and a size enum with md (:432) that inline mode does not map (:398, :91); the element:definition-list registration's columns enum is the strings '1' / '2' (data-list.tsx:82) against the number compare (:49); element:repeater's type (:93) and registration description (:213) advertise fields[].label, never rendered (:194-196). Its registry-inputs-spec-parity gate will surface each on the spec bump. Carrier: the objectui follow-up the seat files at ACCEPT, Blocked-by: #20371.

  6. Fixes #20371 — right. The card asks for six measured rows (name optional where it is read, actions a list, rows strict from birth, the two element: types admitted on three-part evidence, no mirror of UIActionSchema or the object-metadata Action); every item is delivered on this head, and the cross-repo arming is triage note 3's separate carrier. The card this PR closes must claim this branch is green.

  7. NOT MEASURED — pnpm check:dual-build-cjs-loads (exit 3, needs a whole-repo build): covered. ci.yml:2116 runs it inside Build Core, which is green on this head. The CLI unit layer's 52 unloadable files (a round-1 reading, not re-run) are covered by the six green Test Core shards.

CI on this head: 42 check-runs, none in progress after re-polling (Check Changeset finished success): 38 success, 4 skipped (Auto Label first run, Check PR Size first run, Console Pin Gate, the opt-in tarball smoke), 0 failures. Type Check · source gates, the job that ejected the previous head on check:objectui-pin-citations, is green.

Implemented-by: claude/issue-20371-component-props-action-element-rows
Reviewed-by: session_01B3TqpoQbTAfG7G74GMDWNW

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 16:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 75b2169 Sep 28, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20371-component-props-action-element-rows branch September 28, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/xl tests tooling

Projects

None yet

2 participants