Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c90b2a9
refactor(components,types,sdui-parser,core): retire the `body` child-…
claude Sep 18, 2026
7a7af8f
docs(corpus,vscode-extension): migrate the `body` child-list corpus a…
claude Sep 18, 2026
d59bc42
refactor(types): retire `body` on the four per-component redeclaratio…
claude Sep 18, 2026
f6e4e5c
test(components): move the `body` render pins onto `children`, and pi…
claude Sep 18, 2026
963f127
chore(changeset): declare the `body` child-list retirement
claude Sep 18, 2026
22859c8
fix(types,cli,scripts,examples): close the six reds the retirement le…
claude Sep 18, 2026
9ea3436
docs(changeset): re-read the 82 pending claims this diff's files appe…
claude Sep 18, 2026
28c1843
docs(changeset,types): cite by content where this diff moved a line, …
claude Sep 18, 2026
9547e08
test(vscode-extension): invert the `body` half of the objectui#7181 d…
claude Sep 18, 2026
b81f5f8
fix(components,types,vscode-extension): finish the narrowing, and sto…
claude Sep 18, 2026
42a139a
fix(components): keep the three thin `page:*` arms, and record why th…
claude Sep 18, 2026
f4b1090
docs(components,app-shell,cli): make the prose true after the restore…
claude Sep 18, 2026
7903764
Merge remote-tracking branch 'origin/main' into claude/issue-6771-bod…
claude Sep 18, 2026
2e729bc
fix(vscode-extension,changeset): close the round-four prose and carri…
claude Sep 18, 2026
a45df66
Merge remote-tracking branch 'origin/main' into claude/issue-6771-bod…
claude Sep 18, 2026
25e0130
fix(sdui-parser,types,vscode-extension,changeset): pin the carve-out,…
claude Sep 18, 2026
28f3753
Merge remote-tracking branch 'origin/main' into claude/issue-6771-bod…
claude Sep 18, 2026
afbae12
docs(scripts,types,vscode-extension,changeset): past-tense the senten…
claude Sep 18, 2026
059a41c
Merge remote-tracking branch 'origin/main' into claude/issue-6771-bod…
claude Sep 18, 2026
0e0c478
merge: `main` into the `body` dialect retirement, and re-point B2 at …
claude Sep 18, 2026
2367508
test(vscode-extension): state the reach the ablation measured, not a …
claude Sep 19, 2026
f50fccc
test(scripts): the producer scan's B2 liveness claim meets the retire…
claude Sep 19, 2026
b37fefa
test(scripts): the eleventh copy of the spent step-4 premise
claude Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .changeset/3965-mint-box-neutral-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ gap with a three-clause contract, pinned in
2. authored `className` passes through **verbatim**;
3. **zero** injected classes.

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

Landed on both contract faces per the zod-mirror-parity pairing (objectui#6424
family form): `BoxSchema` interface in `@object-ui/types`, its zod mirror in
Expand Down
5 changes: 3 additions & 2 deletions .changeset/5632-sidebar-trigger-dom-passthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ The renderer forwarded its whole prop bag to `SidebarTrigger`, which spreads its
own rest onto the `<button>` it renders — so every authored SDUI key on the node
became an attribute. Fourteen of them, one more than the shape this target
derives from, because this registration also never destructured `schema`: every
other registration in `renderers/navigation/sidebar.tsx` names it (they need
`schema.body`), while this one renders no children and named only `className`,
other registration in `renderers/navigation/sidebar.tsx` names it (they need the
node's child list — spelled `schema.body` when this change landed, `schema.children`
since objectui#6771 retired that spelling), while this one renders no children and named only `className`,
so the node `SchemaRenderer` injects on every render rode the spread and landed
as `schema="[object Object]"`.

Expand Down
7 changes: 4 additions & 3 deletions .changeset/6764-container-declaration-census.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ they render (objectui#6764).

`renderers/layout/semantic.tsx` registers `aside`, `main`, `header`, `nav`,
`footer`, `section` and `article`, and `renderers/layout/aspect-ratio.tsx`
registers one more; all eight call
`renderChildren(schema.children || schema.body)` and none declared
`isContainer`. Nothing on the render path reads that flag, so children always
registers one more; all eight called
`renderChildren(schema.children || schema.body)` when this change landed — the `body`
arm has since been retired by objectui#6771 and they call
`renderChildren(schema.children)` — and none declared `isContainer`. Nothing on the render path reads that flag, so children always
rendered — what the omission did was make `validateTree` warn `not-a-container`
on a child list it then rendered, on the tier built to accept AI-authored pages.
A warning that lies trains authors to discount the true ones.
Expand Down
128 changes: 128 additions & 0 deletions .changeset/6771-retire-body-child-list-dialect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
'@object-ui/types': minor
'@object-ui/components': minor
'@object-ui/core': minor
'@object-ui/sdui-parser': minor
'@object-ui/cli': minor
'@object-ui/app-shell': minor
'object-ui': minor
---

**BREAKING (authoring surface): `body` is no longer a child-list key. Author `children`.**

`BaseSchema` declared two spellings of one concept — `body` and `children` — and left the
choice per component. **Thirteen** registrations read `body` and nothing else, so it was
their ONLY door — the twelve the ruling enumerated plus `tooltip`, which it did not, and
which is the count the committed instrument carries (`BODY_ONLY` plus
`BODY_ONLY_UNRULED` in `scripts/body-dialect-census.mjs`). `div`, `card`, `page`,
`button`, `aspect-ratio`, the seven sectioning tags and the safe-HTML tag factory 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 to accept AI-authored pages, where the diagnostic **is** the
contract (objectui#6771).

Ruled 2026-09-01: one concept, one spelling, and the spelling is `children`.

## What changed

- **Renderers.** `alert`, `badge`, `tooltip` and the `sidebar-*` family read `children`.
Every `children || body` fallback drops its `body` arm **except the four `page:*`
reads named below** — `div`, `card`, `button`, `aspect-ratio`, the sectioning tags,
the safe-HTML tag factory behind ~36 tags, `page`'s flat content list, and
`@object-ui/core`'s recursive `validateSchema`.
- **Item-level `body` is a DIFFERENT key and is untouched.** `list` draws each entry
as `item.content || renderChildren(item.body)` and `tabs` as
`item.content || item.body`, both filed under the ITEM type rather than the node,
and `tabs` still ships `body` inside its own `defaultProps`. ⛔ Neither is this
spelling and neither is refused here: the node-level retirement does not reach a
member of a declared `items` array. Retiring the item-level dialect is
objectui#9590's card, and the two named above are recorded on it.
- **What still reads `body` at NODE level, and why.** Four renderer reads, all `page:*`: `page:card`
(renderer and Studio canvas) and the three thin `page:section` / `page:footer` /
`page:sidebar` containers. ⛔ Authoring the key is refused on them as it is
everywhere else; only the READ survives, for documents already STORED under it.
`@objectstack/spec` states the ground on `PageContainerProps` itself: «The renderers
keep reading `body` as a back-compat fallback for stored documents; that fallback is
objectui's to retire on its own schedule, and it is not a second authorable
spelling.» ⚠️ What separates the two halves is a CONVERSION, not a ground: the spec's
registry carries `page-card-body-to-children` (`toMajor: 17`, surface
`page.component.page:card.body`, shipped `retiredFromLoadPath: true` in spec 17.0.0)
and carries none for the other three — so a stored row under their key has no
migration path at all. Nothing in this repository authors any of them, and the
committed census over those keys is how to see that rather than take it on trust:
`node scripts/body-dialect-census.mjs --keys page:section,page:footer,page:sidebar,page:card,card`.
⚠️ This sentence used to restate that table's answer instead of pointing at it, and
the very commit that edited the sentence moved the figure — a count copied into prose
is what AGENTS.md #9 forbids, so the instrument replaces the number. Read the table
for three things: `card` + `body` is a control that FIRES on the head that ships (so
a zero elsewhere is a reading, not a blind spot), every node still carrying `body`
under these keys falls in the `test` bucket, and `page:section` is a LIT zero — 6
nodes resolved, 0 `body`, 4 `children` — rather than a key nobody asked about.
Retiring the THREE THIN reads is objectui#9916,
which asks for the stored-corpus reading they turn on; `page:card`'s read is not
that card's either — it is objectui's own, grounded by objectstack#5775 /
ADR-0087 D2 rather than owned by it, and it ends when the stored rows have been
replayed through the conversion that already shipped for it.
- **The published type.** `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 (`CardSchema`, `AspectRatioSchema`, `PageNodeSchema`, `TooltipSchema`).
⚠️ Refused **by name**, not deleted: `BaseSchema` carries an index signature and the
mirror ends `.passthrough()`, so a deleted member would be accepted silently and
rendered by nothing — the exact silence this retirement ends.
- **The VS Code extension (`object-ui`) changes behaviour, not just teaching.** Its
preview no longer draws a `body`-spelled document — the node renders empty, the way
the runtime renders it — and its validator now emits a warning naming `children` at
the key's own position. That warning is skipped for node types that declare their
own `body` input (`record:alert`), so a declared translation-map `body` is not
reported as the retired spelling.
- **Two new `@object-ui/sdui-parser` exports.** `RETIRED_CHILD_LIST_KEY` (the retired
spelling, so a consumer names it once) and `checkRetiredBodyDialect` (the diagnostic
the tier substitutes for `unknown-prop` on that key).
- **The authoring tier.** `sdui-parser` answers an authored `body` with the replacement
named, instead of the bare "has no prop" every typo gets — and a `body` child list
under a non-container draws the same containment code the `children` spelling draws.
- **Corpus and teaching, in the same change.** 114 authored nodes across the docs site,
the schema catalog, five package READMEs and the VS Code extension's snippets, JSON
schema, syntax map, hovers and completions. The platform does not refuse a spelling it
still ships.

## Migrating

Rename the key. `{ "type": "card", "body": [...] }` becomes
`{ "type": "card", "children": [...] }`; both faces now name `children` in the refusal,
and `pnpm census:body-dialect` reports where the dialect still lives in a tree.

⚠️ **Four renderer reads are deliberately untouched, all in the `page:*` namespace** —
`page:card` and the three thin `page:section` / `page:footer` / `page:sidebar`
containers. They are read-only back-compat paths for STORED documents, not a second
authoring face: `@objectstack/spec`'s `PageContainerProps` says so itself — `children`
is canonical, `body` is deliberately not declared, and «the renderers keep reading
`body` as a back-compat fallback for stored documents; that fallback is objectui's to
retire on its own schedule».

⛔ **Authoring `body` on them is still refused**, on both published faces and at the
authoring tier, exactly as everywhere else. What survives is the READ.

⚠️ The two halves are not in the same state. `page:card` has a migration path — the
spec's conversions registry carries `page-card-body-to-children` (`toMajor: 17`,
surface `page.component.page:card.body`), which shipped with `retiredFromLoadPath: true`
in spec 17.0.0. The other three have **no conversion at all**, so a stored row under
their key has nowhere to be migrated to. ⇒ the two halves are tracked separately, and
the pointer is not one card: retiring the three thin reads is objectui#9916, whose
question is the stored corpus; retiring `page:card`'s read was **grounded by**
objectstack#5775 / ADR-0087 D2, whose conversion has already shipped and whose
remaining condition is replayed rows. ⚠️ Grounded by, not owned by — the distinction
is the point. objectstack#5775 is CLOSED and its body is a props-declaration audit
that carries no such step, so a reader sent there for the action finds none. The
action is objectui's, on the spec's own terms quoted above: «that fallback is
objectui's to retire on its own schedule.»

**Non-rendering readers keep their arm on the same rule**, and none of them renders
anything: while a renderer still reaches stored `body` content, a reader that must see
the SAME content keeps its arm, or the renderer draws what the reader cannot find.
Those are the two tab-subtree walkers in `renderers/layout/containers.tsx`,
`app-shell`'s `pageSchemaIntrospect` (`CONTAINER_KEYS`) and `PageBlockInspector`
(`STRUCTURAL_PROP_KEYS`, the inspector half of a stored `properties.body`), and the
CLI's `OBJECTUI_STRUCTURAL_KEYS` — a file-IDENTIFICATION marker, where keeping `body` is
what lets an old file still be recognised as an ObjectUI node and therefore refused,
instead of silently not judged.
10 changes: 6 additions & 4 deletions .changeset/6939-overlay-trigger-mirror.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ groups on that card, dispatched as its own PR per the ruling).
Both members demanded `children` and omitted keys the renderer reads first, so
`safeValidateSchema` refused two catalog entries that draw correctly:

- **`tooltip`** now declares `trigger`, and `content` / `body` as the two halves of
one read (`renderers/overlay/tooltip.tsx:28,31` — `renderChildren(schema.trigger)`
and `schema.content || renderChildren(schema.body)`). The registration's own
`inputs` list `trigger` / `content` / `body` and never `children`. `trigger`
- **`tooltip`** now declares `trigger`, and `content` plus the rich-content slot as
the two halves of one read (`renderers/overlay/tooltip.tsx` —
`renderChildren(schema.trigger)` and `schema.content || renderChildren(…)`). The
registration's own `inputs` list `trigger` / `content` / that slot. ⚠️ The slot was
spelled `body` when this change landed; objectui#6771 retired that spelling and it
is `children` now, on both the read and the published `inputs`. `trigger`
follows `HoverCardSchema` two entries below, which is the settled in-repo shape
for this slot.
- **`context-menu`** now declares `triggerClassName`, `contentClassName` and
Expand Down
10 changes: 6 additions & 4 deletions .changeset/7926-page-node-refuses-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ parse GREEN and render nothing. `PageNodeSchema` never declared the key and
through `BaseSchema`'s `.passthrough()`. Measured through the real
`SchemaRenderer`: a `page` node with `actions: [{type:'button',label:'Add
Product'}, …]` drew **0** buttons and the label appeared nowhere in the DOM,
while the SAME two buttons in `body` drew **2**. Until objectui#7933 the array
while the SAME two buttons in the page's child list drew **2**. Until objectui#7933 the array
also reached the wrapper element as `actions="[object Object],[object Object]"`.

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

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

```json
{
"type": "page",
"title": "Products",
"body": [
"children": [
{ "type": "flex", "justify": "end", "gap": 2, "children": [
{ "type": "button", "label": "Add Product", "variant": "default" }
] }
Expand Down
9 changes: 6 additions & 3 deletions .changeset/8284-content-channel-per-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ that names the channel to write instead:
| the renderer reads | components | now refused |
|---|---|---|
| `children` | `box`, `span`, `container`, `flex`, `stack`, `grid`, `scroll-area`, `form`, `toggle` | `body` |
| `body` | `alert`, `badge`, `tooltip` (which reads `content` first, `body` as its fallback) | `children` |
| `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 |

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

**Not narrowed here, and why.** Components whose renderer reads BOTH channels through a
**Not narrowed here, and why.** Components whose renderer read BOTH channels through a
live `children || body` fallback (`div`, `card`, `button`, `aspect-ratio`, the `page`
family), components that read neither, and components with no dedicated declaration
(`sidebar-*`, the `any`-typed registrations) keep both channels. Each is named on
(`sidebar-*`, the `any`-typed registrations) kept both channels at the time of this
change. ⚠️ objectui#6771 has since retired the `body` spelling outright, so those
fallbacks now read `children` alone — this paragraph records what THIS change did and
did not do, not the state of the tree at release. Each is named on
objectui#8284 with the specific measurement it still needs; acting on any of them from the
`packages/components`-only sweep would have been a guess.
9 changes: 5 additions & 4 deletions .changeset/8344-node-recursion-point-redirect.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Redirect the node recursion point from `BaseSchemaCore` to `AnyComponentSchema`
(objectui#8344) — a nested node is now judged by its OWN component schema.

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

| entry | nested off-spec node | bundle (raw / gzip) | fill in output |
| --- | --- | --: | --- |
Expand Down
4 changes: 2 additions & 2 deletions .changeset/8572-chatbot-body-retired.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ objectui validate <root chatbot with body: { model, temperature }>
exit 1 · Path: body · Code: invalid_type · message names `requestBody` <- SUBJECT
objectui validate <the same document spelled requestBody>
exit 0 · "Schema is valid!" <- LIT CONTROL
objectui validate <the same chatbot inside card.body[]>
objectui validate <the same chatbot inside the card's child list>
exit 1 · refused through the node union <- DEPTH
```

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

Expand Down
Loading
Loading