Skip to content

Commit f6fb83f

Browse files
feat(types)!: refuse the content channels on the nineteen public-block arms and metric-card (objectui#9256) (#11020)
Fixes #9256 Clause-②: yes **Clause-② `yes`, as the claim declared:** nineteen published zod arms and one TypeScript face plus its private zod twin stop accepting an authored `children`. Each renderer reads neither content channel off the node, so the key rendered nothing; it is now refused by name. A published accept set narrows, so a contract review is owed before landing. This is the slice ACCEPT `5873860373`, release `5874185838` and correction `5874664390` on objectui#9256 carried: the nineteen public-block arms, `metric-card`, and two text repairs. It says `Fixes` because a re-run of the card's instrument on this branch finds no OPEN family-D registration left (see "Why `Fixes`" below). ## What changed - **Eighteen zod-only arms**, each given a `children` tombstone and `body` restated with the neither-channel guidance, both kept MEMBERS (`retirementTombstone`, one `neitherContentChannelGuidance` string per arm): - in `zod/public-blocks.zod.ts`: `page:header`, `page:tabs`, `page:accordion`, `record:details`, `record:highlights`, `record:related_list`, `record:path`, `record:activity`, `record:discussion`, `record:history`, `record:quick_actions`, `record:reference_rail`, `element:text`, `element:number`, `element:button`, `element:divider`; - in `zod/objectql.zod.ts`: `object-metric`, `object-master-detail-form`. - **`record:alert`** (the nineteenth arm): `children` only, with its own string. Its renderer reads a key named `body` as the message TEXT, so the builder's sentence "no renderer read consumes `body` or `children`" would be false there; `body` is left to `BaseSchema`. - **`page:tabs` / `page:accordion`:** the NODE's channels only. Each renders the `children` of every ITEM in its `items` bag member; that item-level key is the spec row's and stays live (pinned). - **`metric-card`:** `children?: never` on the TypeScript face `DashboardWidgetSlotComponentSchema`, with `body?: never` restated beside it (it was already `never` through `BaseSchema`; the restatement carries the docblock that says what the card renders), and both members on the private slot arm in `zod/complex.zod.ts`. Its message is its own string, not the builder's: the builder says the parser tier's `not-a-container` warning noticed the key, and in a widget slot it does not, because that tier walks `children`, never `widgets`. - **No TypeScript face exists for the nineteen arms**, measured: no declaration in `packages/types` (source or built `.d.ts`) carries any of the nineteen literals. `@object-ui/plugin-form`'s `MasterDetailFormSchema` is the type of `MasterDetailForm`'s `schema` prop, has no index signature and no `children` member, and is the renderer's post-hoist reading, not an authoring face. - **Text repairs:** the comment above the chatbot rows in `content-channel-family-d-9256.test.ts` (it said `body` is ACCEPTED on `chatbot-enhanced` / `chatbot-floating`), the same stale sentence in that file's header, and the name of its LIVE CONTROL test, which said the family "still accepts `body`" while its body asserts the refusal. And the `CodeEditorSchema.children` docblock in `form.ts`, which said six keys go to Monaco "and nothing else": it now names `onChange` too. Text only. - Docblocks kept true: the `public-blocks.zod.ts` module docblock (a new "content channels" section; the flat-key paragraph no longer says `children` is judged by the base type on every arm), the two `objectql.zod.ts` arm docblocks, the slot arm's docblock, the `zod/README.md` sections, and the parity census's exclusion reasons for these arms in `zod-mirror-parity.test.ts`. - **New pin** `packages/types/src/__tests__/content-channel-public-blocks-9256.test.ts` (230 tests) and **one changeset** `.changeset/9256-public-blocks-content-channels.md`: `@object-ui/types` `minor` with an explicit BREAKING note and a migration line, the family's spelling under the no-major rule. ## Re-derivation on `origin/main` `5d689c3f6` (before any edit) **Instrument.** The re-measure's compiler-API walk, re-run: TypeScript 6.0.3, one program per `tsconfig.json` (44 programs: every workspace package, `apps/console`, the examples), 2022 non-test source files, on a BUILT tree (`turbo run build`, 42 / 42), 0 unresolved-module diagnostics. It files every `.body` / `.children` read (property access, string element access, destructuring) under its receiver's declared type with the enclosing function, and this run also records every whole-node spread (`...schema`, `...(schema as any)`, `...bound`, `...node`). Readings: 237 registration sites, 525 channel reads, 4 `children || body` pairs, 27 whole-node spreads. The set of channel reads (file, key, enclosing function) is identical to the re-measure's run on `1ac8cb627`. | key | registration → renderer | reads of the NODE's `children` / `body` | whole-node spread | |---|---|---|---| | `record:details`, `record:highlights`, `record:related_list`, `record:path`, `record:activity`, `record:history`, `record:quick_actions`, `record:reference_rail` | `@object-ui/plugin-detail`, `record` namespace, `skipFallback` → the matching `Record…Renderer` | none; each reads its named keys (`record:activity` through a `read(key)` helper called with literal feed keys only) | none | | `record:discussion` | same package → `RecordChatterRenderer` (shared with `record:chatter`) | none | yes: the node is copied into the panel config; that config is read for `position`, `width`, `collapsible`, `defaultCollapsed` and `feed` only | | `record:alert` | same package → `RecordAlertRenderer` | no `children`; `body` is read as the message TEXT | yes: `readProps` merges the node with `properties`; the result is read for `severity`, `title`, `body`, `icon`, `action`, `dismissible`, `dismissKey`, `visible` | | `page:header` | `@object-ui/components`, `page` namespace → `PageHeaderRenderer` (`any`-typed) | none | none | | `page:tabs`, `page:accordion` | same → `PageTabsRenderer` / `PageAccordionRenderer` (`any`-typed) | none at node level; each renders `item.children` (the tab strip's count badge also walks item descendants to count, and renders nothing) | none | | `element:text`, `element:number`, `element:button`, `element:divider` | same, `element` namespace → `Element…Renderer` (`any`-typed) | none: they read the props bag (`readProps`: `props` and `properties`) and `className`; `element:number` also its `dataSource` binding. The one `body` hit in that file is the `VARIANT_CLASS.body` lookup | none | | `object-metric` | `plugin-dashboard:object-metric` → `ObjectMetricBlock` → `ObjectMetricWidget` | none (the widget destructures named props) | none | | `object-master-detail-form` | `plugin-form:object-master-detail-form` → `MasterDetailFormRenderer` → `MasterDetailForm` | none (it builds its parent `object-form` node key by key) | none | | `metric-card` | `plugin-dashboard:metric-card` → `MetricCard`; `DashboardRenderer` hands a widget to `SchemaRenderer` as its own keys | none (named props; the rest is forwarded to its `Card` as DOM attributes) | none | `SchemaRenderer` destructures `children` and `body` out of the props bag it spreads, so a channel reaches a component only through `schema.*` or a whole-node spread, and the two spreads above end in named-key readers. No registration of the twenty declares a `children` slot input (objectui#9910), so the parser tier's `not-a-container` warning fires for each of them where that tier walks (it does not walk `widgets`). **Faces before the change (built dist, `AnyComponentSchema` parsed behaviourally):** all nineteen arms parse `{ type }` and `{ type, children }` green and refuse `{ type, body }` with `BaseSchema`'s "Did you mean `body` → `children`?" message; `metric-card` in a widget slot parses with `children` and type-checks with it. **Producers**, with lit controls in the same pass: `pnpm census:body-dialect --keys` over the twenty keys plus `div`, `card`, `page`, `page:card`, whole repository (9144 files): no node authors `children` on any of the twenty; `body` appears only as `record:alert`'s text prop, three times, all in tests. Controls: `children` on `div` 179, `card` 183, `page` 54. The same census over the sibling objectstack checkout (9497 files; a stale local checkout, so a supplementary reading): no `children` or `body` on any of the twenty; its only lit control is `page` with 3 `children`. Nothing needed migrating. ## How a `metric-card` refusal surfaces (measured before choosing the pin) `DashboardComponentSchema.widgets` is `z.union([slot arm, strict DashboardWidgetSchema])`, and the strict schema's `type` enum also admits `metric-card`. A widget the slot arm refuses therefore falls through to the strict schema, which refuses the same key as `unrecognized_keys`. The author gets ONE `invalid_union` at `widgets.0` ("Invalid input"), and the slot arm's by-name message sits in its `errors`, at the arm-relative path `children`. `objectui validate` prints it as `[arm 1/2]`, beside `[arm 2/2] Unrecognized keys: "value", "children"` (measured through the built CLI, before and after). So the pin asserts the refusal INSIDE the union's `errors`, plus the strict arm's `unrecognized_keys`, rather than as a top-level issue, and the union is not restructured. The TypeScript face refuses the key at the authoring site (`@ts-expect-error`). ## Red on base, then green Predictions were written to a file before any mutation. Each leg went through `ablation-replace.mjs` against committed HEAD `42d3ea57e` (anchor count and blob hash proven on disk, restore proven by blob equal to HEAD and an empty `git diff HEAD`; tree clean after all four). The pin imports the arms by relative path, so vitest and `tsc` read source and no leg needed a rebuild. | leg | vitest (new pin) | `tsc -p packages/types/tsconfig.test.json` | |---|---|---| | A1: delete `RecordDetailsBlockSchema`'s `children` member | **RED** 6 failed / 230: the five `record:details.children` rows and the nested-in-`page` case; the member row stays green, as predicted, because `BaseSchema` declares the key | GREEN (zod-only arm), as predicted | | A2: delete `DashboardWidgetSlotComponentSchema`'s `children?: never` | GREEN 230 / 230, NOT MEASURED by vitest (types are erased) | **RED**: exactly 1 × TS2578 at the `metric-card` pin | | A3: delete the slot arm's `children` member | **RED** exactly 1: the `metric-card` `children` case | GREEN, as predicted | | A4: delete `RecordAlertBlockSchema`'s `children` member | **RED** exactly 1: the `record:alert` own-message case | GREEN, as predicted | **Consumer-side reverse validation.** A probe inside `packages/plugin-dashboard`, compiled with that package's options, resolves `@object-ui/types` to `packages/types/dist/complex.d.ts`: `children` on a `DashboardWidgetSlotComponentSchema`, `body` on one, and `children` on a `metric-card` inside `DashboardComponentSchema.widgets` give exactly 3 × TS2322; the two controls without a channel compile. Probe deleted, tree clean. ## Gates (HEAD `42d3ea57e`; heavy runs through the shared verify lock; exit codes by redirect-then-capture) | gate | result | |---|---| | `@object-ui/types` `type-check` + `vitest run packages/types/` | exit 0; 274 files / 6359 tests | | downstream `type-check`: every package downstream of `@object-ui/types` with the script, except `@object-ui/site` and the repo root (38 packages, three batches) | 38 × `type-check: Done`, exit 0 | | `vitest run` `packages/cli/`, `packages/sdui-parser/`, and the six other test files that parse one of the twenty types through the zod face (console `public-contract` and `registry-inputs-spec-parity`, two app-shell block-config pins, `MasterDetailForm.i18nLabels`, `ObjectTree.schemaTyped-8655`) | 45 files / 820 tests, exit 0; the cli ratchet (`registered-types-validate-ratchet-10859`) green and unedited | | `vitest run scripts/` | 177 passed + 2 skipped of 179 files / 5350 tests, exit 0 | | `vitest run examples/schema-catalog/` | 34 files / 2198 tests, exit 0 | | console `vite build`, then `check:sdui-registration-pins` | exit 0 | | `check:eager-closure` | exit 1, shared with `main` — see Bundle Analysis | | eslint over the 8 touched TS files (eslint's own JSON: 8 entries) | 0 errors, 0 findings on an added line; the config enables no type-aware rule, so no untouched file's verdict can move | | `check:control-bytes` · `check:new-line-citations` (0 new) · `changeset:check` · `check-changeset-presence` · `check:changeset-claims` · `check:pending-changeset-literals` | exit 0 each | | `check:component-surface-parity` · `check:registry-bare-names` · `check:handler-key-reads` · `check:spec-symbols` · `check:readme-exports` · `check:element-data-source-declaration` · `check:prompt-keys` · `check:test-path-roots` · `pnpm check` | exit 0 each | | `check:doc-types` · `check:doc-snippets` · `check:doc-examples` · `check:skill-examples` · `check:doc-fences` · `check:doc-example-ids` | exit 0 each | | governed guard `--test` over the 10 paths | NOT GOVERNED (lit control `AGENTS.md`: exit 3) | **Bundle Analysis: measured, and the red is `main`'s.** Same box, same install, two console builds that differ only in `packages/types/src` (the five source files at `5d689c3f6`, then at HEAD; the console aliases `@object-ui/types` to source): eager closure 3,179,055 vs 3,179,056 gzip bytes, 10,758,424 raw bytes in both, 330 eager chunks in both. The one eager file that differs is the entry `index` chunk, same raw bytes, +1 gzip byte: it names the lazy `types-zod` chunk by content hash. Both builds are over the 3,179,000-byte ceiling, and `main`'s own `Bundle Analysis` on `5d689c3f6` concluded `failure` (objectui#10996). The new `metric-card` and `objectql.zod.ts` strings land only in the lazy `types-zod` chunk, which is not in the eager-closure report; the `public-blocks.zod.ts` strings are in no emitted chunk. The TypeScript members emit no JavaScript. No eager import is added. NOT MEASURED, left to CI: the `pnpm test` shards beyond the suites above (app-shell, components, core, the plugins and the rest), `@object-ui/site`'s type-check, `test:dist`, E2E, and the repo-wide `pnpm lint`. ## Why `Fixes` The card's instrument, re-run on this branch after the change: the published faces read from the built `packages/types` dist (the TypeScript `.d.ts` with the compiler API; the zod mirror behaviourally, with lit controls `div` accepts `children`, `div` refuses `body`, `dialog` refuses `children`, an unknown `type` is refused), joined to the enumerator's claims and to the read-site walk above. The population is unchanged from the re-measure: 189 published literals that are registry keys, same set. Face changes against the re-measure: exactly the twenty in this PR, each now refusing `children` on every face that carries it. `body` is accepted on no published face of any of the 189. The 68 literals whose face still accepts `children` are all readers or disposed: 42 html / semantic tags whose factory reads `schema.children`, 19 typed family A / C readers with a `children` read filed under their declared type, the four `page:` containers with a live `children || body` fallback (out of this card), and the three void tags disposed on the card. No OPEN family-D registration remains. ## Serial constraints Read at branch time and again before opening: `origin/main` has not moved since `5d689c3f6`. `git merge-tree --write-tree` of this head is clean against each open PR that touches a file here: objectui#11006 (docblocks in `base.ts`, `data-display.ts`, `zod/form.zod.ts`, `zod/objectql.zod.ts` at `SPEC_EXPORT_OPTIONS_OBJECT_SHAPE`; this PR edits none of those sites), objectui#10930 (the `GlobalFilterSchema` docblock in `zod/complex.zod.ts`, below the slot arm this PR edits), and objectui#10990 (`zod-mirror-parity.test.ts` and `zod/README.md`, different sites). objectui#10872's flat-props batch on the same arms is unclaimed; by ACCEPT `5873860373` whichever lands second runs serial behind the other. No rebase, no force-push. ## Acceptance notes — out of scope, not fixed here - `record:alert`'s flat `body` is still refused with `BaseSchema`'s message, which names `children` as the remedy, while the renderer reads that key as its message text. Carrier: objectui#10872's flat-props batch (unchanged by this PR; the new `children` message points at `properties`). - A `metric-card` node inside the legacy `{ id, component, layout }` envelope still parses with `children`: `DashboardWidgetSchema.component` is plain `BaseSchema`, deliberately (the objectui#8344 note on that member), so it accepts `children` on any node type, not only `metric-card`. It is not a face that carries the `metric-card` literal, so it is not a row of the card's instrument. Carrier: none. - `StrictAnyComponentSchema` refuses every `metric-card` widget that carries `value` (its derived strict slot arm closes the passthrough whose members are the card's registry inputs). Measured on both sides of this PR: at `5d689c3f6` the strict slot arm already reports `value` as unrecognized, and at this head a card carrying only `type` and `value` is refused. The strict face has no non-test consumer in this repository. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 24d3e65 commit f6fb83f

10 files changed

Lines changed: 740 additions & 46 deletions
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
**BREAKING (shipped as `minor` — see below):** nineteen ADR-0080 public-block
6+
arms and the dashboard widget slot's `metric-card` node now refuse an authored
7+
`children` by name. None of these renderers reads the node's child list, so the
8+
key rendered nothing, with no render-time error or warning and no element
9+
(objectui#9256).
10+
11+
- `page:header`, `page:tabs`, `page:accordion`, `record:details`,
12+
`record:highlights`, `record:related_list`, `record:path`, `record:activity`,
13+
`record:discussion`, `record:history`, `record:quick_actions`,
14+
`record:reference_rail`, `element:text`, `element:number`, `element:button`,
15+
`element:divider`, `object-metric` and `object-master-detail-form`: `children`
16+
and `body` are declared as by-name refusals on the zod arm, each kept a member.
17+
This package has no TypeScript declaration of these nodes, so the zod face is
18+
the only one that changes.
19+
- `record:alert`: `children` only. Its `body` is the alert's message text, not a
20+
content channel, and is left as it was.
21+
- `page:tabs` and `page:accordion`: the node's own `children` only. Each item's
22+
`children` in `items` stays live — that is what these blocks render.
23+
- `metric-card` in a dashboard's `widgets`: `children?: never` and `body?: never`
24+
on the TypeScript face (`DashboardWidgetSlotComponentSchema`), and both keys
25+
refused by name on its zod twin. That twin is the first arm of the widget
26+
slot's union, so the refusal reaches the author inside the union's
27+
`invalid_union` issue at the widget's path, beside the strict widget schema's
28+
`unrecognized_keys`; `objectui validate` prints it as one arm of two.
29+
30+
What moves for an author:
31+
32+
- `children` on any of these nodes parsed green (and, on `metric-card`, also
33+
type-checked). It is now refused at `safeParse` time at its own path — on
34+
`metric-card`, under the widget's `invalid_union` — and on
35+
`DashboardWidgetSlotComponentSchema` at authoring time.
36+
- `body` was already refused on all of them, by `BaseSchema`. Everywhere except
37+
`record:alert`, the zod refusal message now names what the node renders
38+
instead, where it used to point at `children`, which these nodes do not read
39+
either.
40+
41+
No render behaviour changes: nothing read these keys, which is the whole reason
42+
they could be refused.
43+
44+
Migration: each of these nodes renders from its own keys, so there is no channel
45+
to move the content to. Put it in the key the node does render
46+
(`element:text`'s `properties.content`, the item-level `children` of a
47+
`page:tabs` or `page:accordion` item, `record:alert`'s `properties.body`), place
48+
it beside the node in a container that reads `children` (`page:section`,
49+
`page:card`), or drop it.
50+
51+
Also, text only: `CodeEditorSchema.children`'s docblock now names `onChange`
52+
among the keys `CodeEditorRenderer` forwards to Monaco.
53+
54+
`minor` rather than `major` because this repo's version policy forbids `major`
55+
in any changeset — one `fixed` group — and records `minor` plus an explicit
56+
breaking note as the spelling for a breaking change here.

‎packages/types/src/__tests__/content-channel-family-d-9256.test.ts‎

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@
116116
* RE-POINTED at a twin rather than inverted; what this card asserts — that it
117117
* narrowed `children` on the chatbot faces and left their `body` alone — is
118118
* unmoved.
119+
* ⚠️ AMENDED AGAIN (objectui#9256, public-block slice): the hold-out on the
120+
* two TWIN faces has ended too, and not by a decision on this card —
121+
* objectui#6771 retired `body` on `BaseSchema`, so each twin now declares
122+
* the same neither-channel tombstone on `body` that its `children` carries,
123+
* pointing at `requestBody`. `body` is REFUSED on all three chatbot faces
124+
* today; the two controls below assert that refusal (the mirror control)
125+
* and its compile-time twin, and neither proves `body` parses anywhere.
119126
*
120127
* ## ⚠️ Half of this file is a COMPILE-TIME assertion and vitest CANNOT read it
121128
*
@@ -301,17 +308,22 @@ const ROWS: ReadonlyArray<readonly [
301308
['calendar-view', CalendarViewMirror as unknown as Mirror, ['body', 'children'], {}],
302309
// ⚠️ ONE-SIDED ROW, and ⛔ not a two-sided reading (objectui#9659, carrying a
303310
// contract-review residual on objectui#9639). The three rows below list `children`
304-
// only, and on the two TWIN faces that still means what it always meant: `children`
305-
// dead, `body` held out and LIVE, with the same-face LIVE CONTROL below proving it.
306-
// On the PLAIN `chatbot` face it no longer does. Ruling A on objectui#8572 retired
307-
// `ChatbotSchema.body` as an ADR-0049 tombstone for a DIFFERENT reason than this card's
308-
// — a naming collision, not a dead content channel — and objectui#9639 landed it, so
309-
// the plain face refuses BOTH channels today. Measured: `body` is ACCEPTED on
310-
// `chatbot-enhanced` and `chatbot-floating`, REFUSED on `chatbot`.
311-
// ⇒ the absence of `body` from the plain row means "not this card's to assert", ⛔ not
312-
// "still live here", and objectui#9639 had to re-point both controls below at a twin
313-
// precisely because no same-face control is available any more. The `body` half of the
314-
// plain face is pinned by `node-recursion-point-8344.test.ts`, which owns objectui#8572.
311+
// only. That no longer means `body` is live on any of them: `body` is REFUSED on all
312+
// three faces today, for two different reasons.
313+
// - PLAIN `chatbot`: ruling A on objectui#8572 retired `ChatbotSchema.body` as an
314+
// ADR-0049 tombstone for a DIFFERENT reason than this card's — a naming collision,
315+
// not a dead content channel — and objectui#9639 landed it.
316+
// - The TWINS `chatbot-enhanced` / `chatbot-floating`: objectui#6771 retired `body`
317+
// on `BaseSchema`, so each twin declares the same neither-channel tombstone on
318+
// `body` that its `children` carries, pointing at `requestBody`. The LIVE CONTROL
319+
// below asserts that refusal on `chatbot-enhanced`.
320+
// Re-measured in the objectui#9256 public-block slice: `body` is REFUSED on
321+
// `chatbot`, `chatbot-enhanced` and `chatbot-floating` alike. (This comment used to
322+
// say `body` was ACCEPTED on the two twins; that stopped being true when the twins
323+
// gained their `body` tombstone, and the control below already asserted the refusal.)
324+
// ⇒ the absence of `body` from these rows means "not this card's row to assert", ⛔ not
325+
// "still live here". The `body` half of the plain face is pinned by
326+
// `node-recursion-point-8344.test.ts`, which owns objectui#8572.
315327
// ⛔ Do not add `'body'` to the plain row to "fix" this: the message that row's
316328
// assertions read is objectui#9256's, and the plain face's tombstone carries
317329
// objectui#8572's instead — the row would go red on a true statement.
@@ -405,7 +417,10 @@ describe('objectui#9256 — CONTROLS: the node itself, and the held-out channel,
405417
expect(Object.keys(mirror.shape)).toContain(key);
406418
});
407419

408-
it('LIVE CONTROL — the chatbot family still accepts `body`, the channel held out of this card', () => {
420+
// Named for what it asserts since the objectui#9256 public-block slice: the
421+
// name used to say the family "still accepts `body`", which the body below has
422+
// not asserted since the twins gained their `body` tombstone.
423+
it('LIVE CONTROL — the chatbot twins REFUSE `body` too, pointing the author at `requestBody`', () => {
409424
// The held-out channel, still held out — RE-POINTED, not inverted, by
410425
// objectui#8572. This control was aimed at `ChatbotSchema`, whose own `body`
411426
// the parity ledger recorded as "two different meanings of one key — a naming

0 commit comments

Comments
 (0)