Skip to content

Commit 2acd8e1

Browse files
os-salesclaude
andauthored
refactor(types,components,sdui-parser): retire the body child-list dialect — children is the one spelling (objectui#6771) (#9895)
Fixes #6771 > ## ⚠️ CORRECTION BLOCK — what this body gets wrong at head `b37fefa017cb7ff09456fc5de19ae45d93ebfc18` > > Re-stamped by the `domain:ui` seat at 2026-09-19T00:18Z, replacing the block written at 2026-09-18T19:49Z for head `25e0130eb…`. The head has moved **three times** since (`059a41c65…` → `f50fccc2f…` → this one), so that block's own corrected figures had themselves gone stale — which is the failure mode it existed to prevent, now measured on itself. > > ⛔ **This block is NOT claimed to be exhaustive.** The block it replaces said it was 「exhaustive by construction」 and a review falsified that with one unlisted sentence. A list of what a reader has checked is honest; a guarantee that nothing else is wrong is ⛔ not one this seat can make about a 106-path diff. > > **Contract-review records on this PR: five**, each fetched by id with its `issue_url` read at 2026-09-19T00:18Z — `5732059417` (head `9547e08ee`), `5733750066` (`f4b109034`), `5734518119` (`2e729bc6f`), `5735358922` (`25e0130eb`), `5736698949` (`059a41c65`). All five are **FAIL**. ⚠️ The fifth's **verdict is VOIDED** — `5736705386` records why: the reviewer ran below `CONTRACT_REVIEW_TIER` because the seat spawned it without passing the tier. ⭐ Its **measurements were not withdrawn**; only its authority to issue a verdict was. A sixth review, at tier and verified at spawn, is running against this head. > > **Figures in this body that are stale or false at this head.** ⚠️ Where a number moved, this block names the **instrument** rather than printing a replacement — a count copied into prose is what `AGENTS.md` #9 forbids, and re-copying one here would only restart the clock on the next head: > - Verification's `221 / 5009` and `282 / 2743` — bound to earlier heads. Run the affected-package suites under the verify lock; ⛔ do not read these. > - Verification's 「81 paths」, and the previous block's replacement figure 「**102**」 — **both stale**. The real path surface, read by this seat from `GET /pulls/9895/files` across all pages at 2026-09-19T00:18Z, is **106**, ⛔ not governed (0 of `docs/adr/**`, `.claude/**`, `skills/**`, `AGENTS.md`, `CLAUDE.md`), with a present-path control firing and a fabricated-path control reading absent. > - 「159 nodes carrying `body` … before, 38 after」 — bound to an earlier head. `pnpm census:body-dialect` is the instrument and it ships in this PR; ⛔ read it, not this sentence. > - The previous block's 「the instrument reads **27** on this head」 — that was a reading of `25e0130eb…`, ⛔ not of this head, and it is not re-taken here. > - 「About twenty per-component docblocks」 — measured at **137 across 12 files**, which is why it has its own card, objectui#9933. > - 「Twelve registrations read `body` and nothing else」 — thirteen, agreeing with the changeset and with this body's own next paragraph. > > ⚠️ **Most of these are this seat's own**, not the dev's: the block this replaces was the seat's, the 「26」/「27」 census figure was the seat's, and the voided review was the seat's error in spawning. The dev of round 7 ⛔ did not touch this body — the standing clause says the implementer writes it once, and this round's dispatch did not name it as an owed item, so carrying it is the seat's job. ⚠️ Which of the two rules governs when a dispatch *does* name it is with the maintainer as objectui#9976. > > **Not corrected here, and named so it is not lost:** the shipped changeset says the `page:card` retirement 「is objectui's」 without naming **objectui#9961**, the card this seat filed for it after this head was cut. ⛔ Not a falsified sentence — an uncited carrier, and the seat's call whether the CHANGELOG paragraph should carry it. > > ⛔ Nothing below this block was edited in place. Clause-②: yes ⭐ **Resolved by the seat on 2026-09-18, exactly the way the note below asks:** the declaration of record is this card's claim comment, which re-declares `yes`, and `needs:contract-review` is hung on both carriers. The `no` that stood on this line was the **dispatch's** score — ⛔ the note below is unchanged and was right to refuse to resolve it silently. ⚠️ **The ruling this executes says otherwise for one of its five steps, and I am not choosing between them silently.** The dispatch scored this card `no` on the ground that it NARROWS an accepted authoring set. The 2026-09-01 ruling scores its own **step 3** — retiring `body` from the published type — as **`Clause-② yes at dispatch`**, and triage's 2026-09-17 answer bound the `CONTRACT_REVIEW_TIER` quota to that step alone. Today's release record (comment `5730313581`) argued only the producer-side condition, which binds steps 2 / 4 / 5. ⇒ **step 3's recorded condition was not addressed by the release that unblocked this card**, and whether it is live today is a seat reading, not mine. I have drafted the whole ruling; the `needs:contract-review` label is the seat's to attach and I have neither attached nor waited for it. --- ## What this is The maintainer ruling of 2026-09-01 (「同意。」, option B) retires the `body` child-list dialect: one concept, one spelling, and the spelling is `children`. Step 1 (the liveness census) landed as #7184. **This is steps 2 through 5.** `BaseSchema` declared two spellings and left the choice per component. Twelve registrations read `body` and nothing else, so it was their only door; the wider set read `children || body` and took either. The authoring tier only ever knew `children` — so an author writing the one spelling that resolved got `unknown-prop`, the same warning a typo draws, on the tier built for AI authors where the diagnostic **is** the contract. ## The five steps, and where each one landed | step | state | |---|---| | 1 — census | landed earlier (#7184); re-run here as the verification instrument | | 2 — converge the `body`-only registrations; fallback readers drop the arm in the same change | this PR | | 3 — retire `body` from the published type | this PR ⚠️ see the Clause-② note above | | 4 — the tier teaches `children` only | this PR | | 5 — corpus and teaching migrate in the same commit | this PR | **Step 2.** `alert`, `badge`, `tooltip` and the ten `sidebar-*` reads converge on `children`. Every `children || body` fallback drops its `body` arm: `basic/div.tsx`, `layout/card.tsx`, `layout/semantic.tsx`, `layout/aspect-ratio.tsx`, `form/button.tsx` (named in the ruling's own cross-card boundary paragraph), `layout/page.tsx`'s three reads, `@object-ui/core`'s recursive `validateSchema`, the two container walkers that enumerated child channels, and both VS Code extension readers — whose own comments deferred the arm to this card by number. **Step 3.** `BaseSchema.body` is `never` on the TypeScript face and an alias refusal naming `children` on the Zod mirror, as are the four per-component redeclarations. **Refused by name, not deleted** — `BaseSchema` carries `[key: string]: any` and the mirror ends `.passthrough()`, so a deleted member is accepted silently and rendered by nothing, which is the exact silence this retirement ends. **Step 4.** A new `sdui-parser/src/body-dialect.ts` answers an authored `body` with the replacement NAMED, and it REPLACES the diagnostic the prop walk would emit rather than stacking on it — the shape `checkKanbanQuickAdd` next door is written against. A `body` child list under a non-container draws the same containment code the `children` spelling draws, which is the ruling's own sentence. **Step 5.** 114 authored nodes moved across the docs site, the schema catalog, five package READMEs and the extension's snippets / JSON schema / syntax map / hovers / completions, plus the prose that taught the key. ## ⚠️ A contrary fact, measured while implementing step 2 — pinned, not worked around Converging the twelve gives them a `children` read, so #6808's runtime predicate calls them containers. #6804's ruling anticipated this and wrote the answer in advance, verbatim: a census-pin exception **with its ground**, ⛔ not `isContainer`. That is what landed — ten reasoned entries in `container-declaration-baseline.json`'s `excluded`, and its exact-equality pin moved with them. **But the exception is no longer free, and the cost is this card's own defect one key over:** | | renders | draws `not-a-container` | |---|---|---| | before | `body` | `children` | | after | `children` | `children` | `validateTree`'s containment branch reads `node.children?.length`, so the false warning that used to sit on a key these registrations did not read now sits on the only key they do. Per the standing clause I executed the ruling literally, made no promote and no revert in this PR, and **inverted the pin into a refusal** that states the cost (`container-declaration-ratchet.test.tsx`, the converged-readers block). ⇒ **The question for whoever reads this:** #6804's ground — `children` here is a *label / content fallback*, not layout containment — transfers cleanly to `badge`, `alert`, `button` and `tooltip`. It does not obviously transfer to the `sidebar-*` family, whose child lists ARE layout containment and which are NOT in the public tier, so the react-page cost that justified `button`'s exclusion does not apply to them either. That ruling was made before the convergence existed. ⛔ Not decided here. ## Two readers deliberately left alone, each with its ground - **`page:card`** still reads `schema?.body ?? schema?.children`. That key is `PageCardProps.body` under a **different** retirement (objectstack#5775, ADR-0087 D2), and its own comment records a sequencing precondition — the load-time `body` to `children` conversion — that deleting the read before would blank stored documents silently. - **`items[]`-level `body`** on `list` (and the same shape on `tabs` items and `dashboard` widgets) is objectui#9590's card, fenced off this one. ⛔ Untouched, in either direction. ## Verification - `pnpm census:body-dialect`, the instrument step 1 committed for exactly this re-reading: **159 nodes carrying `body` on the retirement's keys before, 38 after** — and all 38 are test fixtures, the scanner's own planted fixtures, or two pre-existing changesets this branch does not own. - `packages/types` + `packages/sdui-parser`: **221 files, 5009 tests, all passing.** - `packages/components`: **282 files, 2743 tests, all passing.** - Changeset gates green: presence (1 declared), no-major, no-overwrite. Control bytes green. Governed-surface guard: NOT GOVERNED, 81 paths checked. ## Files outside the dispatched surface, declared rather than widened silently The dispatch named `packages/components/src/renderers/`, `packages/sdui-parser/src/` and `packages/types/src/base.ts`. The ruling's steps 2, 3 and 5 reach further and each of these is named for that reason: the rest of `packages/types/src` (the four redeclarations, their mirrors, the pins that move with them), `packages/core/src/validation/schema-validator.ts` (a generic `children || body` reader), `packages/vscode-extension/**` (two readers that deferred to this card, plus the teaching surfaces), `scripts/container-declaration-baseline.json` (#6804's ordered exception), `content/docs/**`, `examples/schema-catalog/**` and five READMEs (step 5). ## Acceptance notes Noted while measuring, ⛔ not filed and ⛔ not repaired here: - `dashboard` widgets author a per-widget `body` (`content/docs/api/schema-reference.md`, `plugin-dashboard/README.md`'s vocabulary note) that no renderer reads — same item-level family as objectui#9590. - About twenty per-component `body?: never` docblocks still justify themselves with "`body` is inherited-and-optional from `BaseSchema`, whose own docblock admits …". The refusals are correct and stay word-for-word; only that one premise sentence is now stale. - Two pre-existing changesets carry `body` migration recipes that will teach the retired spelling when they reach a CHANGELOG. Not edited — they belong to other cards and `check-changeset-overwrite` reports a branch that modifies them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 509f8ed commit 2acd8e1

106 files changed

Lines changed: 2487 additions & 818 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/3965-mint-box-neutral-container.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ gap with a three-clause contract, pinned in
1919
2. authored `className` passes through **verbatim**;
2020
3. **zero** injected classes.
2121

22-
Deliberately unlike `div`, `box` reads `children` only — never `schema.body`.
23-
The `div` renderer's `children || body` fallback is what made a mechanical
24-
`div`→X swap silently drop content on `body`-authoring nodes while the element
25-
count stayed unchanged; content moves into `children` at migration time (the
26-
objectui#6771 B-ruling direction).
22+
Deliberately unlike `div` AT THE TIME, `box` reads `children` only — never
23+
`schema.body`. The `div` renderer's `children || body` fallback is what made a
24+
mechanical `div`→X swap silently drop content on `body`-authoring nodes while the
25+
element count stayed unchanged. ⚠️ objectui#6771 has since executed that B-ruling and
26+
retired `body` across the protocol, so `div` reads `children` only too and `box` is no
27+
longer the exception this paragraph mints it as.
2728

2829
Landed on both contract faces per the zod-mirror-parity pairing (objectui#6424
2930
family form): `BoxSchema` interface in `@object-ui/types`, its zod mirror in

‎.changeset/5632-sidebar-trigger-dom-passthrough.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ The renderer forwarded its whole prop bag to `SidebarTrigger`, which spreads its
99
own rest onto the `<button>` it renders — so every authored SDUI key on the node
1010
became an attribute. Fourteen of them, one more than the shape this target
1111
derives from, because this registration also never destructured `schema`: every
12-
other registration in `renderers/navigation/sidebar.tsx` names it (they need
13-
`schema.body`), while this one renders no children and named only `className`,
12+
other registration in `renderers/navigation/sidebar.tsx` names it (they need the
13+
node's child list — spelled `schema.body` when this change landed, `schema.children`
14+
since objectui#6771 retired that spelling), while this one renders no children and named only `className`,
1415
so the node `SchemaRenderer` injects on every render rode the spread and landed
1516
as `schema="[object Object]"`.
1617

‎.changeset/6764-container-declaration-census.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ they render (objectui#6764).
77

88
`renderers/layout/semantic.tsx` registers `aside`, `main`, `header`, `nav`,
99
`footer`, `section` and `article`, and `renderers/layout/aspect-ratio.tsx`
10-
registers one more; all eight call
11-
`renderChildren(schema.children || schema.body)` and none declared
12-
`isContainer`. Nothing on the render path reads that flag, so children always
10+
registers one more; all eight called
11+
`renderChildren(schema.children || schema.body)` when this change landed — the `body`
12+
arm has since been retired by objectui#6771 and they call
13+
`renderChildren(schema.children)` — and none declared `isContainer`. Nothing on the render path reads that flag, so children always
1314
rendered — what the omission did was make `validateTree` warn `not-a-container`
1415
on a child list it then rendered, on the tier built to accept AI-authored pages.
1516
A warning that lies trains authors to discount the true ones.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/components': minor
4+
'@object-ui/core': minor
5+
'@object-ui/sdui-parser': minor
6+
'@object-ui/cli': minor
7+
'@object-ui/app-shell': minor
8+
'object-ui': minor
9+
---
10+
11+
**BREAKING (authoring surface): `body` is no longer a child-list key. Author `children`.**
12+
13+
`BaseSchema` declared two spellings of one concept — `body` and `children` — and left the
14+
choice per component. **Thirteen** registrations read `body` and nothing else, so it was
15+
their ONLY door — the twelve the ruling enumerated plus `tooltip`, which it did not, and
16+
which is the count the committed instrument carries (`BODY_ONLY` plus
17+
`BODY_ONLY_UNRULED` in `scripts/body-dialect-census.mjs`). `div`, `card`, `page`,
18+
`button`, `aspect-ratio`, the seven sectioning tags and the safe-HTML tag factory read
19+
`children || body` and took either. The authoring tier only ever knew `children`, so
20+
an author writing the one spelling that resolved got `unknown-prop` — the same warning a
21+
typo draws, on the tier built to accept AI-authored pages, where the diagnostic **is** the
22+
contract (objectui#6771).
23+
24+
Ruled 2026-09-01: one concept, one spelling, and the spelling is `children`.
25+
26+
## What changed
27+
28+
- **Renderers.** `alert`, `badge`, `tooltip` and the `sidebar-*` family read `children`.
29+
Every `children || body` fallback drops its `body` arm **except the four `page:*`
30+
reads named below** — `div`, `card`, `button`, `aspect-ratio`, the sectioning tags,
31+
the safe-HTML tag factory behind ~36 tags, `page`'s flat content list, and
32+
`@object-ui/core`'s recursive `validateSchema`.
33+
- **Item-level `body` is a DIFFERENT key and is untouched.** `list` draws each entry
34+
as `item.content || renderChildren(item.body)` and `tabs` as
35+
`item.content || item.body`, both filed under the ITEM type rather than the node,
36+
and `tabs` still ships `body` inside its own `defaultProps`. ⛔ Neither is this
37+
spelling and neither is refused here: the node-level retirement does not reach a
38+
member of a declared `items` array. Retiring the item-level dialect is
39+
objectui#9590's card, and the two named above are recorded on it.
40+
- **What still reads `body` at NODE level, and why.** Four renderer reads, all `page:*`: `page:card`
41+
(renderer and Studio canvas) and the three thin `page:section` / `page:footer` /
42+
`page:sidebar` containers. ⛔ Authoring the key is refused on them as it is
43+
everywhere else; only the READ survives, for documents already STORED under it.
44+
`@objectstack/spec` states the ground on `PageContainerProps` itself: «The renderers
45+
keep reading `body` as a back-compat fallback for stored documents; that fallback is
46+
objectui's to retire on its own schedule, and it is not a second authorable
47+
spelling.» ⚠️ What separates the two halves is a CONVERSION, not a ground: the spec's
48+
registry carries `page-card-body-to-children` (`toMajor: 17`, surface
49+
`page.component.page:card.body`, shipped `retiredFromLoadPath: true` in spec 17.0.0)
50+
and carries none for the other three — so a stored row under their key has no
51+
migration path at all. Nothing in this repository authors any of them, and the
52+
committed census over those keys is how to see that rather than take it on trust:
53+
`node scripts/body-dialect-census.mjs --keys page:section,page:footer,page:sidebar,page:card,card`.
54+
⚠️ This sentence used to restate that table's answer instead of pointing at it, and
55+
the very commit that edited the sentence moved the figure — a count copied into prose
56+
is what AGENTS.md #9 forbids, so the instrument replaces the number. Read the table
57+
for three things: `card` + `body` is a control that FIRES on the head that ships (so
58+
a zero elsewhere is a reading, not a blind spot), every node still carrying `body`
59+
under these keys falls in the `test` bucket, and `page:section` is a LIT zero — 6
60+
nodes resolved, 0 `body`, 4 `children` — rather than a key nobody asked about.
61+
Retiring the THREE THIN reads is objectui#9916,
62+
which asks for the stored-corpus reading they turn on; `page:card`'s read is not
63+
that card's either — it is objectui's own, grounded by objectstack#5775 /
64+
ADR-0087 D2 rather than owned by it, and it ends when the stored rows have been
65+
replayed through the conversion that already shipped for it.
66+
- **The published type.** `BaseSchema.body` is `never` on the TypeScript face and an
67+
alias refusal naming `children` on the Zod mirror, as are the four per-component
68+
redeclarations (`CardSchema`, `AspectRatioSchema`, `PageNodeSchema`, `TooltipSchema`).
69+
⚠️ Refused **by name**, not deleted: `BaseSchema` carries an index signature and the
70+
mirror ends `.passthrough()`, so a deleted member would be accepted silently and
71+
rendered by nothing — the exact silence this retirement ends.
72+
- **The VS Code extension (`object-ui`) changes behaviour, not just teaching.** Its
73+
preview no longer draws a `body`-spelled document — the node renders empty, the way
74+
the runtime renders it — and its validator now emits a warning naming `children` at
75+
the key's own position. That warning is skipped for node types that declare their
76+
own `body` input (`record:alert`), so a declared translation-map `body` is not
77+
reported as the retired spelling.
78+
- **Two new `@object-ui/sdui-parser` exports.** `RETIRED_CHILD_LIST_KEY` (the retired
79+
spelling, so a consumer names it once) and `checkRetiredBodyDialect` (the diagnostic
80+
the tier substitutes for `unknown-prop` on that key).
81+
- **The authoring tier.** `sdui-parser` answers an authored `body` with the replacement
82+
named, instead of the bare "has no prop" every typo gets — and a `body` child list
83+
under a non-container draws the same containment code the `children` spelling draws.
84+
- **Corpus and teaching, in the same change.** 114 authored nodes across the docs site,
85+
the schema catalog, five package READMEs and the VS Code extension's snippets, JSON
86+
schema, syntax map, hovers and completions. The platform does not refuse a spelling it
87+
still ships.
88+
89+
## Migrating
90+
91+
Rename the key. `{ "type": "card", "body": [...] }` becomes
92+
`{ "type": "card", "children": [...] }`; both faces now name `children` in the refusal,
93+
and `pnpm census:body-dialect` reports where the dialect still lives in a tree.
94+
95+
⚠️ **Four renderer reads are deliberately untouched, all in the `page:*` namespace** —
96+
`page:card` and the three thin `page:section` / `page:footer` / `page:sidebar`
97+
containers. They are read-only back-compat paths for STORED documents, not a second
98+
authoring face: `@objectstack/spec`'s `PageContainerProps` says so itself — `children`
99+
is canonical, `body` is deliberately not declared, and «the renderers keep reading
100+
`body` as a back-compat fallback for stored documents; that fallback is objectui's to
101+
retire on its own schedule».
102+
103+
⛔ **Authoring `body` on them is still refused**, on both published faces and at the
104+
authoring tier, exactly as everywhere else. What survives is the READ.
105+
106+
⚠️ The two halves are not in the same state. `page:card` has a migration path — the
107+
spec's conversions registry carries `page-card-body-to-children` (`toMajor: 17`,
108+
surface `page.component.page:card.body`), which shipped with `retiredFromLoadPath: true`
109+
in spec 17.0.0. The other three have **no conversion at all**, so a stored row under
110+
their key has nowhere to be migrated to. ⇒ the two halves are tracked separately, and
111+
the pointer is not one card: retiring the three thin reads is objectui#9916, whose
112+
question is the stored corpus; retiring `page:card`'s read was **grounded by**
113+
objectstack#5775 / ADR-0087 D2, whose conversion has already shipped and whose
114+
remaining condition is replayed rows. ⚠️ Grounded by, not owned by — the distinction
115+
is the point. objectstack#5775 is CLOSED and its body is a props-declaration audit
116+
that carries no such step, so a reader sent there for the action finds none. The
117+
action is objectui's, on the spec's own terms quoted above: «that fallback is
118+
objectui's to retire on its own schedule.»
119+
120+
**Non-rendering readers keep their arm on the same rule**, and none of them renders
121+
anything: while a renderer still reaches stored `body` content, a reader that must see
122+
the SAME content keeps its arm, or the renderer draws what the reader cannot find.
123+
Those are the two tab-subtree walkers in `renderers/layout/containers.tsx`,
124+
`app-shell`'s `pageSchemaIntrospect` (`CONTAINER_KEYS`) and `PageBlockInspector`
125+
(`STRUCTURAL_PROP_KEYS`, the inspector half of a stored `properties.body`), and the
126+
CLI's `OBJECTUI_STRUCTURAL_KEYS` — a file-IDENTIFICATION marker, where keeping `body` is
127+
what lets an old file still be recognised as an ObjectUI node and therefore refused,
128+
instead of silently not judged.

‎.changeset/6939-overlay-trigger-mirror.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ groups on that card, dispatched as its own PR per the ruling).
1010
Both members demanded `children` and omitted keys the renderer reads first, so
1111
`safeValidateSchema` refused two catalog entries that draw correctly:
1212

13-
- **`tooltip`** now declares `trigger`, and `content` / `body` as the two halves of
14-
one read (`renderers/overlay/tooltip.tsx:28,31` — `renderChildren(schema.trigger)`
15-
and `schema.content || renderChildren(schema.body)`). The registration's own
16-
`inputs` list `trigger` / `content` / `body` and never `children`. `trigger`
13+
- **`tooltip`** now declares `trigger`, and `content` plus the rich-content slot as
14+
the two halves of one read (`renderers/overlay/tooltip.tsx` —
15+
`renderChildren(schema.trigger)` and `schema.content || renderChildren(…)`). The
16+
registration's own `inputs` list `trigger` / `content` / that slot. ⚠️ The slot was
17+
spelled `body` when this change landed; objectui#6771 retired that spelling and it
18+
is `children` now, on both the read and the published `inputs`. `trigger`
1719
follows `HoverCardSchema` two entries below, which is the settled in-repo shape
1820
for this slot.
1921
- **`context-menu`** now declares `triggerClassName`, `contentClassName` and

‎.changeset/7926-page-node-refuses-actions.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ parse GREEN and render nothing. `PageNodeSchema` never declared the key and
1313
through `BaseSchema`'s `.passthrough()`. Measured through the real
1414
`SchemaRenderer`: a `page` node with `actions: [{type:'button',label:'Add
1515
Product'}, …]` drew **0** buttons and the label appeared nowhere in the DOM,
16-
while the SAME two buttons in `body` drew **2**. Until objectui#7933 the array
16+
while the SAME two buttons in the page's child list drew **2**. Until objectui#7933 the array
1717
also reached the wrapper element as `actions="[object Object],[object Object]"`.
1818

1919
`PageNodeSchema` now declares `actions` as an ADR-0049 refusal arm, so the same
@@ -26,14 +26,16 @@ the alert-dialog fixtures), and the authorable action FORM was already ruled on
2626
2026-08-25 for objectui#6497 / #6182: the declarative action object. Growing a
2727
reader here would have minted a fourth `actions` shape.
2828

29-
**Migration.** Put the buttons in `body` as nodes — a `button`, or an
30-
`action:button` with a declared `actionType`:
29+
**Migration.** Put the buttons in the page's child list as nodes — a `button`, or an
30+
`action:button` with a declared `actionType`. ⚠️ That list was spelled `body` when this
31+
change landed; objectui#6771 retired the spelling, and the recipe below is written in
32+
the one that is left so a reader copying it gets a document that parses:
3133

3234
```json
3335
{
3436
"type": "page",
3537
"title": "Products",
36-
"body": [
38+
"children": [
3739
{ "type": "flex", "justify": "end", "gap": 2, "children": [
3840
{ "type": "button", "label": "Add Product", "variant": "default" }
3941
] }

‎.changeset/8284-content-channel-per-component.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ that names the channel to write instead:
2525
| the renderer reads | components | now refused |
2626
|---|---|---|
2727
| `children` | `box`, `span`, `container`, `flex`, `stack`, `grid`, `scroll-area`, `form`, `toggle` | `body` |
28-
| `body` | `alert`, `badge`, `tooltip` (which reads `content` first, `body` as its fallback) | `children` |
28+
| `body` (at the time of this change; objectui#6771 has since retired that spelling and these three read `children`) | `alert`, `badge`, `tooltip` (which reads `content` first, the child list as its fallback) | the other channel |
2929

3030
Which channel each renderer reads was measured with the TypeScript type checker over
3131
every `ComponentRegistry.register(...)` call in `packages/components` — a read site is a
@@ -43,9 +43,12 @@ this change. If your own metadata authors the refused channel on one of these tw
4343
types, the component was already drawing nothing there; rename the key to the one in the
4444
table.
4545

46-
**Not narrowed here, and why.** Components whose renderer reads BOTH channels through a
46+
**Not narrowed here, and why.** Components whose renderer read BOTH channels through a
4747
live `children || body` fallback (`div`, `card`, `button`, `aspect-ratio`, the `page`
4848
family), components that read neither, and components with no dedicated declaration
49-
(`sidebar-*`, the `any`-typed registrations) keep both channels. Each is named on
49+
(`sidebar-*`, the `any`-typed registrations) kept both channels at the time of this
50+
change. ⚠️ objectui#6771 has since retired the `body` spelling outright, so those
51+
fallbacks now read `children` alone — this paragraph records what THIS change did and
52+
did not do, not the state of the tree at release. Each is named on
5053
objectui#8284 with the specific measurement it still needs; acting on any of them from the
5154
`packages/components`-only sweep would have been a guess.

‎.changeset/8344-node-recursion-point-redirect.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Redirect the node recursion point from `BaseSchemaCore` to `AnyComponentSchema`
66
(objectui#8344) — a nested node is now judged by its OWN component schema.
77

88
**Behaviour change, deliberately, at every depth below the root.** Every child slot
9-
(`body`, `children`, and every per-component redeclaration of them) is
9+
(`body`, `children`, and every per-component redeclaration of them — objectui#6771 has
10+
since retired `body`, leaving `children` and its redeclarations) is
1011
`z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, and `SchemaNodeSchema`'s
1112
component arm was `BaseSchemaCore` — the ~21 base keys and nothing type-specific. So
1213
per-type enforcement was ROOT-ONLY, for every component type: objectui#7869 measured
@@ -56,8 +57,8 @@ a child by its own schema would therefore have ADMITTED, at every child slot, a
5657
the base arm refused. ⛔ That widening is eliminated rather than declared: the arm the
5758
recursion point installs carries a check that a nested `chatbot` node's `body` still fits
5859
the node slot. Measured, corpus-valid chatbot seed plus `body: { model, temperature }`:
59-
accepted at the root before and after; inside `card.body[]` and `div.children[]` refused
60-
before and refused now. ⇒ the redirect narrows at all 109 redeclarations and widens at
60+
accepted at the root before and after; inside the card's and the div's child lists
61+
refused before and refused now. ⇒ the redirect narrows at all 109 redeclarations and widens at
6162
none. The published `ChatbotSchema` is untouched — whether its own `body` should carry the
6263
chat API's params is a separate question, recorded on objectui#8572 and deliberately not
6364
decided here.
@@ -100,7 +101,7 @@ the `./zod` barrel under Node or vitest, `@object-ui/cli`'s `check` / `validate`
100101
Measured on the published `dist/zod` face of this head (Vite 8.2.1 lib build, `es`,
101102
esbuild-minified, `zod` 4.4.3 and `@objectstack/spec` external, so the figures are this
102103
package's own bytes; nested off-spec node = `{ type: 'icon', icon: 'check', size: 'huge' }`
103-
inside `card.body[]`, parsed through `CardSchema`):
104+
inside the card's child list, parsed through `CardSchema`):
104105

105106
| entry | nested off-spec node | bundle (raw / gzip) | fill in output |
106107
| --- | --- | --: | --- |

‎.changeset/8572-chatbot-body-retired.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ objectui validate <root chatbot with body: { model, temperature }>
4949
exit 1 · Path: body · Code: invalid_type · message names `requestBody` <- SUBJECT
5050
objectui validate <the same document spelled requestBody>
5151
exit 0 · "Schema is valid!" <- LIT CONTROL
52-
objectui validate <the same chatbot inside card.body[]>
52+
objectui validate <the same chatbot inside the card's child list>
5353
exit 1 · refused through the node union <- DEPTH
5454
```
5555

@@ -62,7 +62,7 @@ keys and recursion and no per-component key at all. Measured on the same documen
6262
chatbot + body: { model: 'gpt-4' } console.warn "schema.children.type: type is required"
6363
chatbot + requestBody (no warning) <- LIT CONTROL
6464
chatbot, neither key (no warning) <- LIT CONTROL
65-
card + body: [{ label: 'x' }] console.warn "schema.children[0].type: type is required"
65+
card + <child list> [{ label: 'x' }] console.warn "schema.children[0].type: type is required"
6666
<- FIRING CONTROL
6767
```
6868

0 commit comments

Comments
 (0)