Skip to content

feat(spec): close the 31 SDUI component-props shapes against unknown keys (#4001 batch A) - #8008

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-4001-batch-a-component-strict
Aug 12, 2026
Merged

feat(spec): close the 31 SDUI component-props shapes against unknown keys (#4001 batch A)#8008
os-zhuang merged 6 commits into
mainfrom
claude/issue-4001-batch-a-component-strict

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Part of #4001 — the campaign card stays open as the program anchor.

ui/component.zod.ts, all 31 object sites, stripstrict. It was the single largest remaining chunk of the campaign's authorable surface, and the third batch to touch this file: 批 17 measured it as no gate (nothing parsed these schemas at all), #5068 wired the parse, and this closes the shapes behind it.

What actually changes for an author

#5068 already reported these keys. It did so by walking a strip-mode object and reconstructing what the parse would have dropped. Now the parse rejects the key itself. Same rule id (component-props-unknown-key), same warning tier — and three things a reconstruction could not give:

  1. Curated prescriptions per surface (aliases / guidance live on the shape, not in the walker).
  2. A rejection that holds for every caller of these schemas, not only inside the gate that walks them.
  3. Union arms — see the wiring note below.

Per-schema decision table

Every one of the 31 sites is closed; the column that carries judgement is why the closure is safe there, and what curated table came with it.

#SitePostureBasis
1emptyProps(type) — the 7 no-prop types (app:launcher, nav:menu, nav:breadcrumb, global:search, global:notifications, user:profile, element:divider)strictA factory, not one shared EmptyProps const: an empty shape has no candidate keys, so the distance fallback can say nothing and the surface name is the entire diagnostic. Seven types share the shape; none may share a name. Still one strictObject( AST site. Corpus authors {} on 9 element:divider nodes — clean.
2PageContainerProps (page:section / page:footer / page:sidebar)strict + guidancebodychildren. The renderers keep reading body as a back-compat fallback for stored documents (#5775) — objectui's to retire on its own schedule, never a second authorable spelling (PD #12). Five edits, so the suggester cannot reach it.
3PageHeaderPropsstrict + alias, +2 declareddescriptionsubtitle (see the ADR-0087 note below). Declares maxVisible / mobileMaxVisible.
4PageTabsPropsstrict, +1 declaredDeclares alwaysShowStrip.
5PageTabsProps.items[]strict + aliaskeyvalue. Two measured producers: objectui's Studio designer publishes key (previews/block-config.ts), and #5776 recorded the showcase doing the same. The renderer reads neither — containers.tsx:566 takes it.value, falling back to tab-<idx> — so an authored key silently yields tab tokens that move when the item list changes.
6PageCardPropsstrictvisible was the header's named live specimen; the wrong-layer set answers it (below). Corpus already clean.
7RecordDetailsPropsstrict, +2 declaredDeclares inlineEdit / showHeader.
8RecordDetailsProps.sections[]strictNested block — strictness does not recurse. Corpus authors {name,label,columns,fields} only.
9RecordRelatedListPropsstrictCorpus authors 9 keys, all declared.
10…sort[] (union arm)strict + aliasesdirectionorder (the same alias view.zod.ts:1333 already ships for its own sort rows), namefield (the divergence validate-page-field-bindings.ts's fieldRefsFrom exists to handle).
11–12…add, …add.pickerstrict + aliasobjectNameobject: both are real keys on the same component, one level apart.
13RecordHighlightsFieldobject armstrict + aliasfieldname (this arm keys its field name; columns/sort/filter key theirs field). Follows #5583's ChartGroupBySchema precedent for closing a union arm.
14–22RecordHighlightsProps, RecordActivityProps, RecordChatterProps, RecordPathProps (+ stages[], alias namevalue), PageAccordionProps (+ items[], guidance below), AIChatWindowProps, ai:suggestionstrictMechanical; corpus clean on each.
23–31the nine Element*PropsSchema (text, number, image, metadata_viewer, button, filter, form, record_picker, text_input)strictlazySchema(() => strictObject(…)) — the wrapper is unchanged, so the ADR-0089 D3a toJSONSchema/Proxy hazard the campaign flagged is not touched.

Cross-cutting: every top-level props schema carries the COMPONENT_LEVEL_GUIDANCE sets — a visibility pattern (visible, visibleWhen, visibleIf, hiddenWhen, …) answered with "move it up to the node's own visibleWhen, ADR-0089", and an enumerated node-key list (id, events, style, className, responsiveStyles, dataSource, responsive) answered with "this belongs on the component NODE". The carrier hoists properties onto the node before rendering, which is exactly what makes the confusion durable: the inner spelling appears to work for a renderer read, and is parsed by nothing that decides anything.

The five newly DECLARED keys — judgement, not mechanics

The dispatch asked for declare-vs-guidance per key, measured. The measurement was to enumerate by the renderer's read pattern (the #6276 lesson), not by any previous ruling's key list — a sweep of objectui@d8d0d66:

KeyRead pointWhy declared
page:header.maxVisible / .mobileMaxVisiblecontainers.tsx:1358schema?.maxVisible ?? schema?.properties?.maxVisible, ?? 3 / ?? 1The renderer's own comment says both are "overridable on the page:header"
page:tabs.alwaysShowStripcontainers.tsx:637schema?.properties?.alwaysShowStrip === trueIts comment invites authors verbatim: "Authors who want the strip even at length 1 can pass properties.alwaysShowStrip: true"
record:details.inlineEdit / .showHeaderrecord-details.tsx:234/257schema.inlineEdit ?? true, schema.showHeader ?? false"Authors can still force-disable with inlineEdit: false"

This is the #5611 / #5775 / #6276 rule applied a fourth time (the delivered, authorized shape is the contract). Closing the shapes around them would have converted an invited affordance into a rejection.

All five are optional with no schema default, deliberately: 3 / 1 / false / true / false are the renderer's fallbacks, and a schema default would materialize them on every parsed component — turning "the author said nothing" into "the author asked for the default", which is a different fact and the one a later liveness audit reads. Same reasoning the record picker's limit docblock already records for its own 50.

The divergence pointing the other way → #7973

The same sweep found three Studio-designer inputs no renderer reads: page:accordion.title, page:accordion.items[].value (the renderer overwrites it with panel-<index>), and page:header.icon (retired here at #6946). Those are producer-side defects in the sibling repo, filed as #7973 — not keys to declare. The accordion item's value carries a guidance entry saying the renderer derives it, not a rename: one component over, page:tabs's value really is read, and collapsing the two would be the wrong answer on both.

Union arms — the Zod-4 collapse (constraint 4)

zod-issue-format.ts already unpacked collapsed invalid_union arms for the human message (#5583), so the surface name and the rename did reach the author. What was missing was the routing: a strict arm's rejection was filed as component-props-invalid with the generic value hint, while the identical fact on a plain object was component-props-unknown-key.

validate-component-props.ts now unpacks a lone arm's unrecognized_keys back onto the unknown-key rule (unrecognizedKeysFromUnionArm). The one-arm condition is the correctness of it: an arm rejecting a key does not mean the author meant that arm — fields: ['status'] is a perfectly good record:highlights entry through the string arm — so the rename is claimed only when every other arm rejected the value for a different reason and exactly one arm got far enough to judge keys. Both directions pinned in validate-component-props.test.ts.

Before → after, same input:

[component-props-invalid] …properties.fields.0 fields.0: Invalid input (received …) — no accepted form matched: (1) … (2) …
[component-props-unknown-key] …properties.fields.0.labell `labell` is not a prop `record:highlights` declares … Did you mean `labell` → `label`?

Probe evidence — #5000 means "validate clean" is empty evidence

objectstack validate never parses through PageSchema, so the corpus was judged by direct .parse() against the built @objectstack/spec artifact, over: the three example apps' dist/objectstack.json build artifacts (what objectstack build actually emits), the same three from source through defineStack, and the three published platform pages.

beforeafter
page-component nodes walked (packages/lint's own walkPageComponents)426426
registered-type props bags judged244244
undeclared keys authored00
refusals22 — the same two
#5068 gate findings on the corpus00
negative control: injected key caught by direct .parse()0 / 244244 / 244
negative control: caught by the gate244244

The two refusals are one pre-existing case, reported twice (artifact + source): the showcase's element:record_picker binding its object through the sibling dataSource rather than the flat object shorthand — which the gate explicitly suppresses (suppliedByDataSource). Not a new refusal, and unchanged by this PR.

The negative control is the load-bearing row. Before the conversion the direct-parse half was blind on every one of the 12 registered types the corpus exercises — which is the defect this batch fixes, measured rather than asserted. (Its denominator was fixed mid-run: it initially counted nodes of a registered type that carry no properties bag at all, reading element:divider as 6/9 "partly blind" when three nodes simply had no bag.)

The docs half of the cost evidence — and the one hit it found

#3746 established that a strictness batch owes a scan of what the docs teach, not only what the corpus authors. Every content/docs/**/*.mdx and skills/**/*.md code block carrying a registered component type was parsed and its props judged against the closed schemas.

It was not zero. content/docs/ui/pages.mdx's "Complete Example" — an os:check block, so an example the docs gate compiles and an AI author copies — taught record:highlights.properties.actions. RecordHighlightsProps has never declared it and objectui's record-highlights.tsx reads schema.fields and nothing else, so under strip it was dropped in silence: the example parsed clean, rendered without the actions, and no layer disagreed. That is exactly how a doc teaches a dead key for this long. Fixed in ef3afdb9c — the actions move to record:quick_actions, which is the component that renders them and which the same page already lists two sections up.

After the fix: 392 mdx + 2299 markdown files, 8 registered-type props bags, 0 teaching a key these schemas reject. check:skill-examples — 209 prose examples type-check.

Reverse verification — four sabotages, predicted direction

SabotagePredictedObserved
revert PageCardProps to z.objectflip pin red + ledger drift✅ 2 tests red; check:strictness-ledger demands the worklist row back
drop the union-arm unpacking in lintthe two arm tests red, spec suite unaffected✅ both red
delete the declared maxVisiblecheck:authorable-surface red — a deleted key must prove itself"1 authorable key(s) disappeared from the contract"
delete the keyvalue aliasthe prescription pin red✅ red

Gates

@objectstack/spec381 files / 10037 tests ✅ · typecheck (src + scripts + test layer) ✅ · @objectstack/lint 71 / 1932 ✅ · check:generated13/13 ✅ (check:docs, check:api-surface, check:authorable-surface, check:strictness-ledger, check:liveness among them) · check:skill-examples ✅ · check:merge-driver ✅ · check:nul-bytes ✅ · check:spec-parsed-alias ✅ · check:adr-anchors ✅ · check:i18n ✅.

Blast radius: turbo run typecheck --filter='...@objectstack/spec'124 / 124 tasks ✅ (the example apps rebuild inside it). Consumer suites metadata / metadata-core / metadata-protocol / sdui-parser / platform-objects / cli60 / 60 tasks ✅.

Generated artifacts were regenerated, never hand-edited: gen:strictness-ledger (ui/ strip 37 → 6; global authorable strip 40 → 9; component.zod.ts 31 → 0), gen:liveness-counts (no movement), gen:schema (authorable-surface: +5 keys, 0 deletions, so the .base.json anchor is untouched), gen:docs. The regenerated component.mdx diff is exactly the five new rows — the #3746 trap-1 description swap did not happen, checked rather than assumed. The ledger's hand-written half: the ui/ worklist row is deleted (the file is closed) and the closure recorded on the file-level triage row.

Deliberately NOT in this PR

The carrier stays z.record(z.string(), z.unknown()) — direction B remains declined by the 2026-08-05 ruling, unregistered record:*-style types are still skipped, not rejected, and component.test.ts's two carrier assertions stay green on purpose. The storage path still parses no props (#4463's fourth wall). The gate is still warning level. And per the dispatch: FieldWidgetPropsSchema, BaseNavItemSchema, and the pinned wire arms elsewhere are untouched.

Changeset: @objectstack/specminor, @objectstack/lint patch.

…keys (#4001 batch A)
`ComponentPropsMap` — the declared shape of every `page:*`, `record:*`,
`element:*`, `nav:*` and `ai:*` node a page can carry — stripped unknown keys
in silence. All 31 object sites in `ui/component.zod.ts` are `strictObject` now,
including the two union arms (`RecordHighlightsField`'s object arm and
`record:related_list`'s sort entry) and every nested block, since strictness
does not recurse.
#5068 already REPORTED these keys, by walking a strip-mode object and
reconstructing what the parse would have dropped. Now the parse rejects the key
itself — same rule id, same warning tier, and three things the reconstruction
could not give: curated per-surface prescriptions, a rejection that holds for
every caller rather than only inside the gate, and coverage of union arms.
Five renderer-honoured props are DECLARED rather than rejected, on the rule this
file has applied three times before (#5611/#5775/#6276 — the delivered,
authorized shape is the contract): `page:header` `maxVisible`/`mobileMaxVisible`,
`page:tabs.alwaysShowStrip`, `record:details` `inlineEdit`/`showHeader`. Each is
read by objectui through `schema?.X ?? schema?.properties?.X` with its own
comment inviting authors. All optional with no schema default — the defaults are
the renderer's, and declaring them would turn an unset key into an authored one.
Curated tables, each backed by a producer measured in the wild rather than
guessed: a tab item's `key` → `value` (objectui's Studio designer publishes
`key`; the renderer reads `it.value`), a header's `description` → `subtitle`
(the rename its own ADR-0087 conversion performs — the one path that had no
diagnostic at all), a container's `body` → `children`, and a wrong-layer family
for keys that belong on the component NODE.
`@objectstack/lint` gains one piece of wiring: zod 4 collapses union-arm
failures into a single `invalid_union`, so a lone arm's `unrecognized_keys` is
unpacked back onto the unknown-key rule id — and deliberately is not when two
arms could both have been meant.
Deliberately unchanged: the carrier is still `z.record(z.string(), z.unknown())`
(direction B stays declined, unregistered `record:*` types are still skipped),
the storage path still parses no props (#4463), and the gate is still warning
level.
Verified with direct build-artifact `.parse()` probes over the example corpus —
`objectstack validate` never parses through `PageSchema` (#5000), so "the
examples validate clean" would have been no evidence. 244 registered props bags
across the three example apps' build artifacts, the same three from source, and
the three published platform pages: zero undeclared keys, zero new refusals. The
probe's negative control moved from 0/244 injections caught to 244/244.
Filed out of scope: #7973 (objectui's Studio designer publishes three page
component inputs no renderer reads).
Part of #4001
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 1:09pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec.

106 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 packages/lint, @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/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/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/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • 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/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.

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.

…r rendered it
Found by the #3746-style corpus scan this batch owes: every `content/docs/**`
and `skills/**` code block carrying a registered component type was parsed and
its props judged against the now-closed `ComponentPropsMap`. One hit, in the
"Complete Example" of the pages guide — an `os:check` block, so it is an example
the docs gate compiles and an AI author copies.
`RecordHighlightsProps` has never declared `actions`, and objectui's
`record-highlights.tsx` reads `schema.fields` and nothing else. Under strip the
key was dropped in silence, which is precisely why a doc could teach it for this
long: the example parsed clean and rendered without the actions, and no layer
disagreed. With the shape closed it is a rejection, so the doc had to be right
before the batch could land.
The actions move to `record:quick_actions`, which is the component that renders
them (`actionNames`, per objectui's own designer config) and which the same page
already lists two sections up.
Scan after the fix: 392 mdx + 2299 markdown files, 8 registered-type props bags,
0 teaching a key these schemas reject. `check:skill-examples` — 209 prose
examples type-check.
Part of #4001
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx
@os-zhuang
os-zhuang marked this pull request as ready for review August 12, 2026 12:43
@os-zhuang
os-zhuang added this pull request to the merge queueAug 12, 2026
…s-regen driver's second half
`authorable-surface/ui.json` carries `merge=os-regen`, so a merge resolves it by
taking a side and leaving it REGEN-PENDING rather than by textually splicing two
generated files. That is the driver working as designed; the half that must
follow is the regeneration, and it is not optional here — measured on the merged
tree before pushing, `check:authorable-surface` fails:
❌ authorable-surface/ is out of date (1 key(s) not recorded).
+ ui/GlobalFilter:object
The missing key is main's own, from #7892 (`GlobalFilterSchema` gains an optional
`object` for i18n label resolution). Nothing of this branch's is involved: the
regenerated diff is exactly that one line, and batch A's five declared keys were
already recorded.
Worth stating because the failure mode is quiet: `pnpm build` REWRITES this
artifact, so a `check:generated` run that happens to follow a build reports all
13 green against the file the build just fixed. Only `check:authorable-surface`
on its own, against the merged bytes, shows the drift — which is how this was
caught before the merge queue caught it.
Merged tree verified: spec 381 files / 10091 tests, lint 71 / 1932,
check:generated 13/13, check:strictness-ledger current (438 sites / 149 strip /
9 authorable — unmoved by the merge). The ledger counts and liveness counts were
regenerated too and came back byte-identical.
Part of #4001
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to a conflict with the base branch Aug 12, 2026
…ged with batch B
Second turn of the os-regen relay. `#7985` (batch B — the memory driver's five
persistence sub-shapes) and `#7972` landed while this branch was in the queue, so
the shared counts artifact met two batches that each decremented it correctly and
independently.
That is the exact arithmetic #5107 built this artifact to prevent: the rows do
not overlap, git merges them without complaint, and the SUBTOTAL — which overlaps
nothing — merges clean and wrong. The artifact carries `merge=os-regen` so the
merge defers rather than splices, and the only correct resolution is to recompute
from the merged tree. Regenerated, never hand-edited:
still-open (strip) 149 → 144 (batch B's five)
files carrying at least one 26 → 25
authorable — forced scope 9 → 4
data/ strip 107 → 102, data/ strict 57 → 62
total strict 283 → 288
Neither batch's own numbers moved; the combined ones did. `check:strictness-ledger`
agrees with the merged tree in both directions (25 open files / 144 strip sites,
no closed file still carrying a worklist row — batch B's `driver/memory.zod.ts`
row left with their side of the prose).
Merged tree verified: spec 382 files / 10109 tests, lint 71 / 1932,
check:generated 13/13.
Part of #4001
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Queue ejection resolved — ready to re-queue (I have not re-queued it myself; landing is the spec lane's to drive).

The queue removed this for MERGE_CONFLICT. Two rounds of the os-regen relay tail, both resolved by regenerating from the merged tree, never by hand-resolving a number:

  1. 3ede555ceauthorable-surface/ui.json. The merge left it regen-pending (that is merge=os-regen working as designed, not a broken merge), so check:authorable-surface on the merged bytes failed with + ui/GlobalFilter:object — main's own key from spec: GlobalFilterSchema gains optional object for i18n label resolution (#7804) #7892, not anything of this branch's. The regenerated diff is exactly that one line.
  2. 5705acb24 — the strictness-ledger counts, after spec: close memory driver's persistence sub-shapes against unknown keys (#4001 batch B) #7985 (batch B) and feat(spec): refuse the confirmText + params pair on ActionSchema, scoped by schema boundary #7972 landed. This is the arithmetic strictness 台账「数字/散文分家」:计数与表头转生成物走 os-regen,Class 判定与依据保持手写 —— 终结「干净合并两边都错」(单日 7 例) #5107 built the artifact to prevent: two batches each decrement the same subtotal by their own correct delta, the rows do not overlap, and the subtotal merges clean and wrong. Recomputed: still-open strip 149 → 144, files 26 → 25, authorable forced-scope 9 → 4, data/ strip 107 → 102. Neither batch's own numbers moved; the combined ones did.

One trap worth leaving in the thread, because it makes a green run meaningless: pnpm build rewrites authorable-surface/, so a check:generated that happens to run after a build reports 13/13 against the file the build just fixed. Only check:authorable-surface on its own, against the merged bytes, shows the drift. That is how (1) was caught here rather than in the queue.

Merged tree at origin/main333a374cc (now an ancestor of the head): spec 382 files / 10109 tests, lint 71 / 1932, check:generated13/13, check:strictness-ledger agrees in both directions (25 open files / 144 strip sites, no closed file still carrying a worklist row). The batch's own diff is unchanged by either merge.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Re-queue receipt (spec seat outgoing session, per the queue-rebuild-collateral prescription): this PR was removed_from_merge_queue at 12:59:03Z with no merged event. Signature read before re-queueing — the third (non-red) signature: this PR has ZERO merge_group runs of its own, while its batch mates' runs are all success (pr-7985 and pr-7972 queue branches: CI / Lint & Type Check / Spec Liveness / Console Pin Freshness / ADR Merge Approval all green at 12:43Z; both merged at ~12:58–59Z). No check ever ran and failed for this PR — the queue dropped it while rebuilding as the two ahead landed. Not a flake re-run, not a diff problem. Re-queueing once; a second ejection goes to the queue steward per discipline, not another self re-queue.


Generated by Claude Code

@os-zhuang
os-zhuang enabled auto-merge August 12, 2026 13:16
@os-zhuang
os-zhuang added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit 9d1d9c7Aug 12, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4001-batch-a-component-strict branch August 12, 2026 13:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uisize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude