Repository navigation
Commit f6fb83f
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
Lines changed: 27 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
119 | 126 | | |
120 | 127 | | |
121 | 128 | | |
| |||
301 | 308 | | |
302 | 309 | | |
303 | 310 | | |
304 | | - | |
305 | | - | |
306 | | - | |
307 | | - | |
308 | | - | |
309 | | - | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
314 | | - | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
315 | 327 | | |
316 | 328 | | |
317 | 329 | | |
| |||
405 | 417 | | |
406 | 418 | | |
407 | 419 | | |
408 | | - | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
409 | 424 | | |
410 | 425 | | |
411 | 426 | | |
| |||
0 commit comments