Skip to content

refactor(spec)!: finish #5775's SDUI props count — 4 keys declared, page:tabs.type renamed to tabStyle (#6776) - #6868

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-6776-component-props-declarations
Aug 9, 2026
Merged

refactor(spec)!: finish #5775's SDUI props count — 4 keys declared, page:tabs.type renamed to tabStyle (#6776)#6868
os-zhuang merged 3 commits into
mainfrom
claude/issue-6776-component-props-declarations

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #6776

#5775 reconciled ComponentPropsMap with the renderers that serve it, then recorded that "the rest of the keys the renderers honour are declared". That sentence did not hold. Five more author-facing props are read by objectui's renderers and were undeclared here, so for each of them three platform authorities disagreed at once: objectui's published manifest and generated sdui-intrinsics.d.ts told an author (very often an AI author, ADR-0033) the key was legal, validateComponentProps (#5068) reported it undeclared, and the renderer honoured it anyway. That is #5435's shape, with the wrong half on the spec side this time — and these five are what block validateComponentProps's error upgrade alongside #5728.

Per-prop itemization

Prop Landing site Before After
page:header.recordChrome PageHeaderProps (component.zod.ts) undeclared; validateComponentProps reports component-props-unknown-key z.boolean().default(true)false drops the record chip for the bare heading a non-record page wants
page:header.showStar PageHeaderProps undeclared z.boolean().default(true) — the follow (favourite) star beside the record title
page:header.showCopyId PageHeaderProps undeclared z.boolean().default(true) — the copy-record-id button beside the record title
page:accordion.variant PageAccordionProps undeclared (schema had only items / allowMultiple / aria) z.enum(['flush','card']).default('flush')flush draws the divider, card leaves the border to each panel's content
page:tabs.tabStyle PageTabsProps declared as type, which no author can write in a flat or JSX carrier renamed to tabStyle; type becomes a retiredKey() tombstone + ADR-0087 D2 conversion page-tabs-type-to-tab-style

Route A for page:tabs, and why

Per the tier-3 ruling on the card. The concept was declared all along; the spelling was unauthorable, because a props key named type collides with the page component's own dispatch key. Three consequences, each re-measured on objectui origin/main b3439f4 before implementing (the card's baseline was c4c0ac8 — all three survived the drift):

Premise Reading at origin/main b3439f4 Verdict
containers.tsx:381 reads both spellings, spec's first const type: 'line' | 'card' | 'pill' = schema?.properties?.type || schema?.tabStyle || 'line'; HOLDS, line number unchanged
SchemaRenderer.tsx:251-270 refuses to hoist properties.type :253 comment "never let inner properties.type / properties.id shadow the outer component descriptor", naming tab visual style as the case; :264 if (k === 'type' || k === 'id') continue; HOLDS, inside the quoted range
sdui-parser BASE_PROPS includes 'type' validate.ts:20-30 set contains 'type'; :68 if (BASE_PROPS.has(key)) continue; runs before the unknown/typed checks HOLDS

So type is unauthorable in the flat and JSX carriers (where the node reads { type: 'page:tabs', ... } and type is the tag name), and unvalidated even in the nested properties object. tabStyle is what objectui's registry publishes as the designer input (containers.tsx:657) and what the renderer reads in every carrier. Converging on the spelling that works, rather than the one that declares well, is #5775's displayField to labelField again; one spelling rather than two is Prime Directive #12. Option B (declare tabStyle as an alias) was refused for the reason the card gives: the dialect that would survive is the one that silently fails to validate.

Corroboration the card did not have: objectui's registry inputs for all three components already publish exactly the five names this PR declares (tabStyle, variant, recordChrome, showStar, showCopyId). After this lands, spec and objectui's manifest agree on every one.

The conversion's reach — one shared-walk fix, and why it was not optional

mapPageComponents (the conversion layer's page walk) now visits pages[].slots.SLOT as well as pages[].regions[].components[]. Its own comment used to call region level "the whole surface", reasoning that everything else lives inside a free-form properties bag. PageSchema.slots is the counter-example: a closed map of seven named slots, each declared z.union([PageComponentSchema, z.array(PageComponentSchema)]) — as typed as any region component — and packages/lint's walkPageComponents has always visited both, so every conversion reached strictly less than the lint rule that judges its result.

#6776 is where that gap cost something. page:tabs is one of those slots, and all four in-repo authoring sites are slots.tabs on kind: 'slotted' record pages:

  • packages/platform-objects/src/pages/sys-organization.page.ts:51
  • packages/platform-objects/src/pages/sys-user.page.ts:139
  • packages/platform-objects/src/pages/sys-position.page.ts:49
  • examples/app-showcase/src/ui/pages/project-detail.page.ts:38

A region-only rewrite would have left os migrate meta --from 16 unable to touch the only shape this key is written in, while the tombstone's prescription promised it could — a false prescription is the ADR-0078 shape the tombstone exists to avoid. Every other page-component conversion gains the same reach, in the direction it already declares; their fixtures are region-based, so nothing else moves. All four sites are migrated to tabStyle in this PR.

One consequence worth stating plainly: because PageComponent.properties is an open bag, tsc does not catch properties.type at a slot authoring site. The tombstone's compile channel reaches a site only where PageTabsProps is the declared type; for slotted pages the audible channel is the parse (and validateComponentProps, pinned below).

Coupled factual correction in packages/lint (declared in the claim)

Comment-only, no rule behaviour changed, no other lint edits:

Tests

  • component.test.ts — defaults and value refusals for the four new keys; the console preview sample (recordChrome: false) parsed verbatim; tabStyle accepted for all three values; type refused by name with the prescription (/`type`.*removed.*`tabStyle`/s), not a bare .toThrow() — an undeclared key on this non-strict schema is stripped silently, and a throw-only assertion cannot tell the two apart.
  • conversions.test.ts — the discriminator (the entry keys on component.type while rewriting properties.type, so "which type" is the whole correctness question, pinned by asserting the node's own type is untouched); slot reach for both the single-component and array forms with their path spellings; #4923 both-keys, in both the agreeing (twin dropped) and disagreeing (both kept, no notice) directions; idempotence; and the acceptance face in both directions, with PageSchema green on both spellings because the open bag never judged the key at all.
  • validate-component-props.test.ts — the five shapes that used to warn now report nothing, and the retired type reports component-props-invalid carrying tabStyle rather than component-props-unknown-key carrying nothing.

Reverse verification

Deleting the retiredKey() tombstone while keeping the rename turns pnpm --filter @objectstack/spec check:authorable-surface red, naming the key:

❌ 1 authorable key(s) disappeared from the contract:
     - ui/PageTabsProps:type

   These schemas are NOT .strict(), so Zod silently STRIPS an unknown key: an author
   who keeps writing one gets a clean parse and a setting that never takes effect —
   no error, nothing to grep, nothing pointing at the changelog (#3733, ADR-0104).

Restoring the tombstone returns it to green (check:authorable-surface PASS).

Worth stating because the expected direction was predicted wrong before the run: the gate that fires is the disappeared-key check (a) — the silent-strip trip wire — not the RETIRED_KEYS_BY_MAJOR registration check (b2) that this PR's registry entry feeds. (b2) rejects an entry naming a key that is still LIVE; here the key is neither live nor tombstoned, so (a) catches it first and (b2) is never reached. The registry entry is still load-bearing in the other direction — it is what lets the [RETIRED] line exist in authorable-surface/ui.json at all — but it is not what makes the tombstone's removal audible.

Generated artifacts regenerated

authorable-surface/ui.json (four new keys, ui/PageTabsProps:tabStyle, and ui/PageTabsProps:type [RETIRED]), authorable-defaults/ui.json, spec-changes.json, docs/protocol-upgrade-guide.md, content/docs/references/ui/component.mdx. api-surface/ is unchanged, as expected for key-level narrowing on a def that still exists.

Follow-up for the objectui shard

Nothing to change in objectui source — its registry already publishes all five names. On the spec pin bump, the parity gate's exemption entries for these five keys expire and must be deleted (the gate asserts expiry), alongside the three element:record_picker exemptions #5775 already discharged.


Generated by Claude Code

…abs.type → tabStyle (#6776)

#5775 recorded that "the rest of the keys the renderers honour are declared".
Re-counting against objectui origin/main (b3439f4) found five more: for each,
objectui's published manifest called the key legal, validateComponentProps
(#5068) called it undeclared, and the renderer honoured it anyway — #5435's
shape with the wrong half on the spec side.

Declared (no behaviour change, the contract catching up):
  - page:header.recordChrome / .showStar / .showCopyId
  - page:accordion.variant

Renamed (tombstone + ADR-0087 D2 conversion + RETIRED_KEYS_BY_MAJOR[17]):
  - page:tabs.type → tabStyle. A props key named `type` collides with the page
    component's own dispatch key: SchemaRenderer refuses to hoist
    properties.type, sdui-parser's BASE_PROPS skips it before any validation,
    and in a flat/JSX carrier `type` is the tag name. Unauthorable in every
    carrier but the nested properties object, and unvalidated even there. Route
    A per the tier-3 ruling; all three objectui premises re-verified at
    origin/main before implementing.

Also: mapPageComponents now walks pages[].slots.<slot>, not only
regions[].components[]. PageSchema.slots is a closed map of seven declared
PageComponent slots and packages/lint's walkPageComponents has always visited
both. page:tabs IS one of those slots and all four in-repo sites are slots.tabs,
so region-only reach would have made the tombstone's `os migrate meta`
prescription a false promise.

Coupled factual correction (comment-only) in packages/lint: the falsified
"rest of the keys are declared" sentence in authoring-rules.ts, and this card
added to validateComponentProps' error-upgrade inventory alongside #5728.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 3:33am

Request Review

@github-actions github-actions Bot added the size/l label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/platform-objects, @objectstack/spec.

113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling labels Aug 9, 2026
claude added 2 commits August 9, 2026 02:21
…-to-tab-style (#6776)

The Check Changeset gate (#6148) requires a breaking changeset to answer the
ledger question in writing. This one's answer is 'registered': the conversion
entry, its D3 chain step and the RETIRED_KEYS_BY_MAJOR[17] key all land in this
PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
…ponent-props-declarations

# Conflicts:
#	packages/spec/src/migrations/registry.ts
@os-zhuang
os-zhuang marked this pull request as ready for review August 9, 2026 04:07
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit a933452 Aug 9, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6776-component-props-declarations branch August 9, 2026 04:22
os-zhuang pushed a commit that referenced this pull request Aug 9, 2026
`main` gained #6868 (`page-tabs-type-to-tab-style` + `ui/PageTabsProps:type`)
after this branch's last CI head. The merge itself was clean — git placed the
three siblings' entries at different offsets — but the generated artifacts are
driver-deferred and were regenerated from the merged tree rather than
text-merged.

All three protocol-17 siblings verified present after regeneration:
`notification-list-cursor-retired` (this PR), `action-descriptor-is-async-retired`
(#6862) and `page-tabs-type-to-tab-style` (#6868, a D2 conversion).

`authorable-surface/api.json` and `authorable-defaults/api.json` pick up
`api/ApiRoutes:email` and `api/MetadataEndpointsConfig:maskObjectFields` from
other PRs merged in the same window — this branch's copies were simply behind.

check:generated 10/10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sGk4SKHqGRgmmqUok1P8M
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/l tests tooling

Projects

None yet

2 participants