Skip to content

Commit 775e079

Browse files
feat(types)!: refuse both content channels on the E3 residual (nine object views, detail-view, email, password) and repair the shorthand Omit erasure (objectui#9256) (#10897)
Part of #9256 Clause-②: yes **Clause-② `yes`, as the claim declared:** twelve published arms stop accepting an authored `children`, and two published TypeScript faces stop erasing the members they inherit. Both narrow an accept set; a contract review is owed before landing. The E3 residual that the maintainer's ruling on objectui#8284 (comment `5861449497`, Q2 A) moved onto this card: keys with a published arm whose renderer reads **neither** content channel. It also carries the ruling's execution parameter 3, the `InputShorthandSchema` / `UiCalendarSchema` TypeScript `Omit` erasure. objectui#9256 stays open for the rest of family D. ## What changed - **Narrowed (12 keys):** `object-grid`, `object-form`, `object-kanban`, `object-map`, `object-tree`, `object-view`, `object-gantt`, `object-calendar`, `object-chart`, `detail-view`, `email`, `password`. - TypeScript face: `body?: never` and `children?: never`, each with a docblock naming what the node renders instead. - zod mirror: `body` and `children` are two `retirementTombstone` members fed ONE string from the new `neitherContentChannelGuidance` builder in `zod/tombstone.zod.ts`. Both stay MEMBERS, so `zod-mirror-parity`'s key sets stay equal. - `body` was already refused on these arms (by `BaseSchema`, objectui#6771), except on the `email` / `password` TypeScript face, which the `Omit` erased. It is restated because `BaseSchema`'s refusal names `children` as the remedy, and `children` is dead here too. - **`Omit` erasure repaired:** `InputShorthandSchema` and `UiCalendarSchema` now inherit through `OmitDeclared` (`form.ts`, not exported). It is a key-remapping mapped type: it drops exactly the named keys and keeps every other named member and the index signature. - `ui:calendar` gains no refusal of its own. Its face now re-exposes what `CalendarSchema` declares, so the published TypeScript face refuses `body`, `children` and a `mode` outside the declared enum, which the erased face accepted. Its mirror (a `.extend()` of that one) already refused them. - I measured the `ui:calendar` renderer anyway (`renderers/form/calendar.tsx`, typed `CalendarSchema`). It reads `defaultValue`, `mode` and `value`, and no channel. - **Held (3 keys): `br`, `hr`, `img`.** See "Held, and the question it leaves" below. ## Per-key measurement, taken on `origin/main` `0c50f1863` before any edit **Instrument.** The TypeScript compiler API, ⛔ not grep. It builds one program per workspace package from that package's own `tsconfig.json`: 40 programs over 1573 source files, on a BUILT tree (turbo build 42/42 first), with `@object-ui/types` resolving to `packages/types/dist`. It files every `.body` / `.children` read (property access, string element access, object destructuring) under the declared type of its receiver. - 547 channel reads were filed. - **Lit controls fire:** `ButtonSchema` 1, `DivSchema` 1, `CardSchema` 1, `ContainerSchema` 1 (each `children`). - **Receiver-reachability control:** the same program sees every narrowed declaration as a receiver of the keys its renderer reads. The zeros below are readings, not blindness. | key | registration(s) claiming it | registration hop → reader, and the type it reads the node as | `body` / `children` reads under that type | receiver reads (reachability) | arm, TS face: before → after | arm, zod face: before → after | disposition | |---|---|---|---|---|---|---|---| | `object-grid` | `plugin-grid:object-grid` (+ console `registerLazy` of the same plugin) | `ObjectGridRenderer` (`any`) → `ObjectGrid` · `ObjectGridSchema` | 0 / 0 | 158 reads, 38 keys | body refused (BaseSchema), children accepted → both refused | same → both refused by name | narrowed | | `object-form` | `plugin-form:object-form` | `ObjectFormRenderer` (`any`) → `ObjectForm` · `ObjectFormSchema` | 0 / 0 | 200 / 44 | same | same | narrowed | | `object-kanban` | `plugin-kanban:object-kanban` (+ lazy) | `ObjectKanbanRenderer` (`any`) → `ObjectKanban` · `ObjectKanbanSchema` | 0 / 0 | 67 / 13 | same | same | narrowed | | `object-map` | `plugin-map:object-map` (+ lazy) | `ObjectMapRenderer` (`any`) → `ObjectMap` · `ObjectMapSchema` | 0 / 0 | 30 / 16 | same | same | narrowed | | `object-tree` | `plugin-tree:object-tree` (+ lazy) | `ObjectTreeRenderer` (`any`) → `ObjectTree` · `ObjectTreeSchema` | 0 / 0 | 13 / 8 | same | same | narrowed | | `object-view` | `plugin-view:object-view` (and the `view` alias) | `ObjectViewRenderer` (`any`) → `ObjectView` · `ObjectViewSchema` | 0 / 0 | 112 / 19 | same | same | narrowed | | `object-gantt` | `plugin-gantt:object-gantt` (+ lazy) | `ObjectGanttRenderer` (`any`) → `ObjectGantt` · `ObjectGanttSchema` | 0 / 0 | 61 / 48 | same | same | narrowed | | `object-calendar` | `plugin-calendar:object-calendar` (+ lazy) | `ObjectCalendarRenderer` (`any`) → `ObjectCalendar` · `ObjectCalendarSchema` | 0 / 0 | 40 / 12 | same | same | narrowed | | `object-chart` | `plugin-charts:object-chart` (+ lazy) | `ObjectChartBlock` (`any`) → `ObjectChart` · `ObjectChartSchema` | 0 / 0 | 123 / 15 | same | same | narrowed | | `detail-view` | `plugin-detail:detail-view` | `DetailViewRenderer` (`any`) → `DetailView` · `DetailViewSchema` | 0 / 0 | 195 / 31 | same | same | narrowed | | `email` | `ui:email` (`field:email` stands down) | `any` wrapper spreads the node into `InputRenderer` · `InputSchema` | 0 / 0 | 23 / 17 | BOTH accepted (erased `Omit`) → both refused | body refused, children accepted → both refused by name | narrowed | | `password` | `ui:password` (`field:password` stands down) | same as `email` | 0 / 0 | 23 / 17 | same as `email` | same as `email` | narrowed | | `br` | `ui:br` | html-elements factory (`any`), void tag: passes `undefined` as the child | reads neither (the one `schema?.children` read is behind the void-tag guard) | n/a (`any`) | body refused, children accepted (family arm) → unchanged | same → unchanged | **held** | | `hr` | `ui:hr` | same | same | n/a | same | same | **held** | | `img` | `ui:img` | same | same | n/a | same | same | **held** | The residual count stays 15: no key reads `children` today. 12 are narrowed and 3 are held. **`any`-typed receivers, attributed rather than assumed.** - Every hop above is `any`-typed. Each hop body was read: none contains a channel read. - The any-typed reads in the eleven hosting files and packages were attributed one by one: - React `children` destructured off internal components' own props (`SchemaRenderer` passes no node child as a React child); - `document.body` and `err.body`; - `TreeNode.children` on the record hierarchy `ObjectTree` builds; - `FeedItem.body`; - `record:alert`'s own `props.body`, which is another registration. - The html factory's `schema?.children` read sits behind `isVoid ? undefined : …`. **Delegation hosts, checked for pass-through.** Every `SchemaRenderer` call in the ten object/detail packages was traced. The node it renders is built one of three ways: - key by key — `ObjectForm` / `ModalForm` / `DrawerForm` / `TabbedForm` / `SplitForm` / `WizardForm` all build their `form` node that way, and the `form` renderer DOES draw `children`, which is why this was checked; - from a sub-key — `DetailView`'s `header` / `footer` / tab `content`, `ObjectChart`'s drill-down table; - or from a generated `object-*` node — `ObjectView` assembles `object-kanban`, `object-calendar`, `object-gallery`, `object-timeline`, `object-gantt`, `object-map`, `object-tree` and `object-chart` from named keys, and every one of those reads neither channel (for gallery and timeline: `packages/plugin-list` and `packages/plugin-timeline` hold no node-channel read). No host spreads its own node into a node that reads `children`. **Generic traversers** (`validateChildren` in core, `sdui-parser` `validateTree`, the metadata-admin preview canvases, the vscode validator) walk children to validate or preview. They render nothing and are node-agnostic, as for every row family D already holds. **objectui#9910 cross-check.** None of the 12 narrowed registrations declares a `children` slot input: a grep over their registration files finds 0 hits, and the lit control is `html-elements.tsx` / `div.tsx`, which do. **Producers.** `pnpm census:body-dialect --keys` over the 15, plus `ui:calendar` and `input`, with `div` / `card` / `page` / `button` / `p` / `h1` as lit controls in the same pass: 8981 files, 2744 candidate nodes. - **0** nodes author `children` on any of the 15. - 1 node authors `body` on `ui:calendar`: it was the family-D TRIPWIRE test line (test bucket), moved by this change. - The controls read `div` 170 / `card` 183 / `page` 47 `children`. - Producers derived: none. Nothing was migrated. ## The `Omit` repair — measured on the BUILT d.ts A probe program under `packages/components` resolved `@object-ui/types` to `packages/types/dist/index.d.ts` (`--traceResolution`). It carried 15 `@ts-expect-error` pins (the 10 object/detail `children`, `email` `children`, `password` `body`, `email` `label: 42`, and the `ui:calendar` `children` and `mode: 'default'`), a lit control (`const n: number = 'x'`), and 12 channel-free controls. - **On this branch:** exit 0 — every directive used, every control compiles, the lit control fires. - **On the base dist:** exit 2 with 15 × TS2578, and the lit control is still used. - The emitted `dist/form.d.ts` declares `OmitDeclared` locally and both heritage clauses use it. ## Red on base, then green — one-shot, the restore proven by state The predictions were written to a file BEFORE the mutation. The mutation was `git checkout 0c50f18 -- THE-SEVEN-DECLARATION-FILES`, with every test file at HEAD. It was proven by 7/7 blob hashes equal to BASE plus anchor counts (`neitherContentChannel` 0, `OmitDeclared` 0). The run sat under `trap … EXIT INT TERM` with absolute paths. | reader | mutated (BASE arms) | restored (HEAD `14fd13c51`) | |---|---|---| | vitest, the new pin file | **RED** exit 1 — 73 failed / 90 passed; exactly the predicted 12 × 5 `children` families + 12 `body`-message rows + 1 nested row | **GREEN** 163 / 163 | | `tsc -p packages/types/tsconfig.test.json` | **RED** exit 2 — 18 × TS2578 and 3 × TS2322 (the `Eq` pins) in the new file, plus 3 in `zod-mirror-parity` naming the two re-collapsed pairs and the stale `KnownDrift` row; 0 elsewhere | **GREEN** exit 0 | | built-d.ts probe (types rebuilt; dist markers 0 → 29 / 6) | **RED** exit 2, 15 × TS2578 | **GREEN** exit 0 | The restore was proven by 7/7 blob hashes equal to HEAD, `git diff HEAD` empty and `git status` clean, both after the restore and at the end. ⚠️ **The `body` rows on the ten object/detail arms are GREEN on base, by design:** `BaseSchema` already refused `body` there. Only their MESSAGE changes, and the message rows are the ones that turn red. `?: never` is erased before vitest runs, so the TypeScript half is read by `tsc` alone. The vitest run cannot see it. ## Serial constraints - **Mapped at branch time (`0c50f1863`).** PRs objectui#10714, #10883 and #10884 were open then; all three have since merged, and PR objectui#10875 was the base. - `layout.ts` (TabItem) and `layout.zod.ts` (TabItemSchema): untouched, because `br` / `hr` / `img` are held and neither file is edited. - `content-channel-family-d-9256.test.ts`: the only edits are forced ones, clear of PR objectui#10714's hunks, which sit in the ListItem and Tabs sections: - The file's own `ui:calendar` TRIPWIRE said "when the collapse is repaired this line goes RED … turn it back into a `@ts-expect-error` pair". The repair fired it. - The pair moved to the new file. That file lost the tripwire line, its `uiCalendarBody` array entry (32 → 31) and the now-unused `UiCalendarSchema` type import. - No pin was added there. - `objectql.ts`: the insertion after `ObjectViewSchema`'s last member is clear of PR objectui#10883's `NamedListView` docblock edit. - `object-view-unmirrored-keys-7779.test.ts` (PR objectui#10884): untouched. - **Open PRs the branch-time map missed** (re-mapped by contract review `5863631182`): - PR objectui#10890 rewrote the same two `KnownDrift` header figures in `zod-mirror-parity.test.ts` and landed first (`c30c8dd4c`). This branch merged `main` at that commit (merge commit `31bb63bcb`, no rebase, no force-push) and recounted the figures from the merged table; see "Merge round" under Gates. - PRs objectui#10888, #10889 and #10892 are textually disjoint, and all three have since merged. `git merge-tree` of the merge head against `main` at `6a7f24e92`, which carries all of them, reports no conflict. - ⛔ `zod/index.zod.ts` and the `packages/cli` ratchet (objectui#10872) are untouched. ## Collateral that the repair and narrowing made true or false — each measured, none widened - **`zod-mirror-parity.test.ts`:** - The two `Omit` COLLAPSE rows (32 + 26 keys) leave `MirroredUndeclared`. Its figures move from 4 / 60 to 2 / 2, and the class note records the emptying. - Restoring the named members NARROWED those faces: a collapsed face accepted every key at `any`. So this is not the "declare the keys" remedy that ledger refuses. - The repaired shorthand face now carries `InputSchema`'s `onChange` runtime slot. It enters `KnownDrift` as inherited drift: one entry, one key. After the merge with PR objectui#10890 (which took `ai.zod.ts#AIFormAssistSchema` out, one entry and two keys), the table holds 49 entries / 88 keys, and both header sites say so. - The union-arm non-vacuity leg had no multi-arm entry left in this ledger. It now takes the same reader's arm-walking on `KnownDrift` and re-arms here when a multi-arm entry lands. - **`object-view-spec-parity.test.ts`:** `body` / `children` are redeclared envelope keys on `object-view` now. They are triaged into `SANCTIONED_LOCAL` as refusal-only surface, like `viewTabBar`, and the declared-surface figure moves 24 → 26. - **`CalendarSchema`'s texts on both faces said the TypeScript `Omit` "collapses … and carries no member".** That became false; they are corrected. - **Two comments** that spelled the old plain-`Omit` heritage (`index.ts`, `form-barrel-mirror-9406.test.ts`) are corrected. - **`scripts/check-handler-key-read-sites.mjs`** caught my first spelling. It was a computed spread (`...helper(…)`) that the gate cannot resolve, so it stopped judging all eleven arms and its `detail-view` `onTabChange` waiver read stale. The fix, the second commit, declares both members explicitly. Now 49 / 49 reads are judged and 0 are left unjudged. ## Held, and the question it leaves `br` / `hr` / `img` read neither channel, but their only published arm is `HtmlElementSchema`: ONE object over 38 tags, whose renderer draws `children` for the other 35 (`p`, `h1`, …). Refusing the key on three tags means splitting that family arm, or refining it per tag. Both change a published contract's shape. - The option the ruling adopted (option A in measurement `5859897857`) reads: "leaving the three void tags to the tier because their arm is the shared HtmlElementSchema family whose per-tag split was ruled out (objectui#9067)". - The parser tier already names them: the registration declares no `children` slot on a void tag, so `validateTree` reports `not-a-container`. - A HELD control in the new pin file records the shared-arm fact, so the hold is visible. That the brief listed them among the keys to narrow is recorded as a conflict in the report, ⛔ not resolved here by guessing. ## Gates (at HEAD `14fd13c51`, exit codes captured by redirect-then-capture; heavy runs through the shared verify lock) | gate | result | |---|---| | `@object-ui/types` build (tsc + vite + dist completeness) | exit 0 — 132 emitted files verified | | `@object-ui/types` `type-check` (`tsc --noEmit` + examples + `tsconfig.test.json`) | exit 0 | | `pnpm exec vitest run packages/types/` (parity, node-slot, arm, mirror, family-D and the new pins) | 259 files / 5670 tests, exit 0 | | `pnpm exec vitest run scripts/` | 177 passed + 2 skipped of 179 files / 5317 tests, exit 0 | | consumer type-check — every package naming a narrowed type (28 packages: the ten object plugins, app-shell, components, core, fields, cli, react, runner, console, schema-catalog, vscode-extension …) | exit 0, 28 × `type-check: Done` | | `pnpm --filter @object-ui/types lint` | 0 errors; 0 findings on any added line (eslint JSON over the 13 touched files) | | `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:doc-types` · `check:doc-snippets` · `check:doc-examples` · `check:skill-examples` · `check:doc-fences` · `check:doc-example-ids` (against the built types) | exit 0 each | | `check:handler-key-reads` · `check:component-surface-parity` · `check:prompt-keys` · `check:registry-bare-names` · `check:spec-symbols` · `check:readme-exports` · `pnpm check` (CLI self-check) | exit 0 each | | governed guard `--test` over the 14 paths | NOT GOVERNED | NOT MEASURED: - `check:sdui-registration-pins` exits 2 without a console build. The diff touches no registration and no `sideEffects` array. Left to CI. - The 4-way `pnpm test` shards beyond `packages/types` and `scripts/`, plus `test:dist` and E2E: left to CI. ### Merge round — head `31bb63bcb` (merges `main` at `c30c8dd4c`) The one conflict was the two `KnownDrift` header figures in `zod-mirror-parity.test.ts`. It was resolved by RECOUNTING the merged table with the test's own AST reader, not by picking a side: | tree | `KnownDrift` | |---|---| | base `0c50f1863` | 49 entries / 89 keys | | PR objectui#10890 removes `ai.zod.ts#AIFormAssistSchema` | −1 entry / −2 keys (48 / 87 on `c30c8dd4c`) | | this branch adds `form.zod.ts#InputShorthandSchema` | +1 entry / +1 key (50 / 90 on `14fd13c51`) | | merged `31bb63bcb` | **49 entries / 88 keys** | The population bullet and the KNOWN_DRIFT restatement both read 49 / 88, and the bullet keeps both histories in landing order. The other ledgers read the same as before: `MirroredUndeclared` 2 / 2, `UnmirroredDeclared` 12 / 84, `RuntimeOnlyDeclared` 3 / 7, `WiderThanDeclared` 6 / 7. The merge head differs from `main` by exactly this PR's 14 files. | gate at `31bb63bcb` | result | |---|---| | `zod-mirror-parity` + `content-channel-e3-residual-9256` | 2 files / 200 tests, exit 0 | | `@object-ui/types` build · `type-check` (incl. `tsconfig.test.json`) | exit 0 · exit 0 | | `pnpm exec vitest run packages/types/` (incl. PR objectui#10890's `ai-zod-arms-10859` / `ai-node-faces-agree-10874` and the family-D file PR objectui#10714 edited) | 261 files / 5696 tests, exit 0 | | `pnpm exec vitest run scripts/` | 177 passed + 2 skipped of 179 files / 5322 tests, exit 0 | | `check:control-bytes` · `check:new-line-citations` · `changeset:check` · `check-changeset-presence` · `check:changeset-claims` · `check:pending-changeset-literals` · `check:handler-key-reads` | exit 0 each | ## Acceptance notes — out of scope, not fixed here - **`input` reads neither channel as well.** `InputSchema`: 0 channel reads, 17 keys read; the registration declares no `children` slot. - It is this card's remaining family-D name, and it was not in this claim. - Its old hold-out ground ("a TS `Omit` carries nothing") is gone with this repair: a tombstone on `InputSchema` now propagates on both faces. - Carrier: the next objectui#9256 slice. - **`PartialSchema`'s docblock in `index.ts`** says every generic re-spelling of its `Omit` collapses for the same `keyof T` reason. `OmitDeclared` is a generic spelling that does not: this PR measures it on two faces. - Carrier: the next objectui#9256 slice, which touches the same `Omit` subject. objectui#6397, that alias's card, is closed. - Noted, not filed. - **The family-D test header** still records `InputSchema` as "family E, frozen" and `DetailViewSchema` as unattributable. Both readings are superseded by this change. - Carrier: the next objectui#9256 slice. - Noted, not edited: PR objectui#10714 held that file while this PR was written. It has since merged, so the next slice edits the header freely. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6a7f24e commit 775e079

14 files changed

Lines changed: 1051 additions & 151 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
**BREAKING (shipped as `minor` — see below):** twelve node types now refuse both
6+
content channels by name — `object-grid`, `object-form`, `object-kanban`,
7+
`object-map`, `object-tree`, `object-view`, `object-gantt`, `object-calendar`,
8+
`object-chart`, `detail-view`, `email` and `password`. None of their renderers
9+
reads `body` or `children`, so an authored child list on them rendered nothing:
10+
no error, no warning, no element. Both keys are now `?: never` on the TypeScript
11+
face and a by-name refusal on the zod mirror, each kept a member of the mirror
12+
shape.
13+
14+
What moves for an author:
15+
16+
- `children` on any of the twelve type-checked and parsed green before; it is now
17+
refused at authoring time and at `safeParse` time, at its own path.
18+
- `body` was already refused on the ten `object-*` / `detail-view` nodes, on both
19+
faces, and by the zod mirror on `email` / `password`. Its refusal message on all
20+
twelve now names what the node renders instead, where it used to point at
21+
`children`, which these nodes do not read either.
22+
- The TypeScript faces of `email` / `password` (`InputShorthandSchema`) and
23+
`ui:calendar` (`UiCalendarSchema`) now carry every member they inherit. Both were
24+
spelled as an `Omit` over an interface with an index signature, which erased
25+
every inherited member, so each face checked only the keys it wrote itself and
26+
accepted every other key at `any`. Now `body`, `children` and a wrongly typed
27+
inherited member — `label: 42` on `email`, a `mode` outside `single` /
28+
`multiple` / `range` on `ui:calendar` — no longer type-check there. An undeclared
29+
key still type-checks on both repaired faces: the index signature is kept.
30+
- The `body` / `children` refusal text on `calendar` and `ui:calendar` changes
31+
wording only, because it described the erased TypeScript face; their zod mirrors
32+
accept exactly what they accepted before.
33+
34+
No render behaviour changes: nothing read these keys, which is the whole reason
35+
they could be refused.
36+
37+
Migration: move the content to the channel the component renders, or drop it. On
38+
`detail-view` that is `header`, `footer` or a tab's `content`; for the others,
39+
place the content beside the node in a container that reads `children`.
40+
41+
`minor` rather than `major` because this repo's version policy forbids `major`
42+
in any changeset — one `fixed` group — and records `minor` plus an explicit
43+
breaking note as the spelling for a breaking change here.

‎packages/types/src/__tests__/content-channel-e3-residual-9256.test.ts‎

Lines changed: 333 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ import type { ViewSwitcherSchema } from '../views';
221221
import type { NLQuerySchema } from '../ai';
222222
import type { TextSchema, ImageSchema, IconSchema, TabsSchema } from '../layout';
223223
import type { AccordionSchema } from '../disclosure';
224-
import type { CalendarSchema, UiCalendarSchema } from '../form';
224+
import type { CalendarSchema } from '../form';
225225

226226
type Mirror = {
227227
safeParse: (v: unknown) => { success: boolean; error?: z.ZodError };
@@ -565,30 +565,20 @@ describe('objectui#9256 — the TypeScript face refuses both channels at the AUT
565565
const timelineBody: TimelineSchema = { type: 'timeline', body: CONTENT };
566566
// @ts-expect-error objectui#9256 — `timeline` reads neither channel on either of its two readers
567567
const timelineChildren: TimelineSchema = { type: 'timeline', children: CONTENT };
568-
// ⚠️ TRIPWIRE, and deliberately NOT a `@ts-expect-error`. `UiCalendarSchema`
569-
// is declared as `Omit<CalendarSchema, 'type'>`, and `BaseSchema` carries an
570-
// index signature — so `Omit` resolves through `Exclude<string, 'type'>` =
571-
// `string` and collapses every member into that signature. MEASURED with the
572-
// checker: `UiCalendarSchema['body']` answers `any`, and so does
573-
// `UiCalendarSchema['mode']`, which is a key this change never touched. That
574-
// face therefore carries NONE of `CalendarSchema`'s members, tombstones
575-
// included, and the line below compiles today. Its MIRROR does refuse the
576-
// key — the `ui:calendar` row above is the reading — so this is a measured
577-
// asymmetry between the two published faces, not a gap in the mirror.
578-
// When the collapse is repaired this line goes RED, which is the signal to
579-
// restate the two tombstones on `UiCalendarSchema` and turn it back into a
580-
// `@ts-expect-error` pair.
581-
const uiCalendarBody: UiCalendarSchema = { type: 'ui:calendar', body: CONTENT };
568+
// `ui:calendar`'s TypeScript face stood here as a TRIPWIRE: its heritage was a
569+
// plain `Omit` that collapsed into `BaseSchema`'s index signature, so this line
570+
// compiled. objectui#9256's E3 slice repaired the heritage, the tripwire fired as
571+
// designed, and the pair it asked for lives with that repair's other pins in
572+
// `content-channel-e3-residual-9256.test.ts`.
582573

583574
expect([
584575
separatorBody, separatorChildren, kbdBody, skeletonChildren, checkboxBody, paginationChildren,
585576
dialogChildren, collapsibleChildren, carouselBody, galleryBody, viewSwitcherChildren, reportBody,
586577
nlQueryChildren, pivotBody, chatbotChildren,
587578
textBody, textChildren, imageBody, imageChildren, iconBody, iconChildren,
588579
tabsBody, tabsChildren, accordionBody, accordionChildren, calendarBody, calendarChildren,
589-
uiCalendarBody,
590580
listBody, listChildren, timelineBody, timelineChildren,
591-
]).toHaveLength(32);
581+
]).toHaveLength(31);
592582
});
593583

594584
it('CONTROL — the same nodes WITHOUT a content channel compile (no `@ts-expect-error` here, and `tsc` is the reader)', () => {

‎packages/types/src/__tests__/form-barrel-mirror-9406.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ const EXCLUDED_BY_RULING_ELSEWHERE = ['SemanticElementSchema', 'HtmlElementSchem
269269

270270
describe('objectui#9406 -- the two names resolve from the root barrel', () => {
271271
it('`InputShorthandSchema` resolves from the root, with its narrowed `type`', () => {
272-
// `Omit<InputSchema, 'type' | 'inputType'>` re-narrowed: `type` is the two
272+
// `InputSchema` without `type` / `inputType`, re-narrowed: `type` is the two
273273
// shorthand literals, and `inputType` is unwritable here (objectui#8762).
274274
const type: Eq<ShorthandFromRoot['type'], 'email' | 'password'> = true;
275275
const inputType: Eq<ShorthandFromRoot['inputType'], undefined> = true;

‎packages/types/src/__tests__/object-view-spec-parity.test.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,13 @@ const SANCTIONED_LOCAL = new Set<string>([
329329
'showViewSwitcher',
330330
// Points at objectui's own named-view record, not a spec key.
331331
'defaultListView',
332+
// The two content channels, redeclared ONLY as named refusals (objectui#9256,
333+
// the E3 residual): `ObjectView` reads neither, so both are `?: never` /
334+
// `neitherContentChannel()` twins — declared so an authored child list is
335+
// refused by name instead of rendering nothing. The spec's view models no child
336+
// list on this node, so the refusal is objectui-only surface, as `viewTabBar`'s is.
337+
'body',
338+
'children',
332339
]);
333340

334341
// ── the suite ────────────────────────────────────────────────────────────────
@@ -385,10 +392,13 @@ describe('ObjectViewSchema declared-surface consistency (#2890 scope B)', () =>
385392
expect(zodOnly).toEqual([]);
386393
});
387394

388-
it('narrows exactly the two envelope keys the node means to narrow', () => {
389-
// Mechanically derived, so a THIRD redeclaration — the usual way a node
395+
it('narrows exactly the four envelope keys the node means to narrow', () => {
396+
// Mechanically derived, so a FIFTH redeclaration — the usual way a node
390397
// quietly re-types `data` or `name` out from under the envelope — fails here.
391-
expect([...redeclaredEnvelopeKeys].sort()).toEqual(['description', 'type']);
398+
// `body` / `children` joined `description` / `type` with objectui#9256 (the E3
399+
// residual): both are redeclared ONLY as named refusals, because the renderer
400+
// reads neither content channel.
401+
expect([...redeclaredEnvelopeKeys].sort()).toEqual(['body', 'children', 'description', 'type']);
392402
});
393403

394404
it('has exactly the known TS-only backlog — it may shrink, never grow', () => {
@@ -423,8 +433,13 @@ describe('ObjectViewSchema declared-surface consistency (#2890 scope B)', () =>
423433
// reference — the last TS-only key closed on the zod side. So 22
424434
// non-envelope keys, 22 + 2 = 24 declared, and the TS figure again does not
425435
// move.
436+
//
437+
// objectui#9256 (the E3 residual) redeclared two ENVELOPE keys, `body` and
438+
// `children`, as named refusals: 22 non-envelope keys still, 22 + 4 = 26
439+
// declared. The TS figure below does not move either — both keys are
440+
// `BaseSchema`'s, so the compiler attributes them to the envelope.
426441
expect(ouiZodKeys.filter((k) => !ENVELOPE.has(k))).toHaveLength(22);
427-
expect(ouiDeclaredKeys).toHaveLength(24);
442+
expect(ouiDeclaredKeys).toHaveLength(26);
428443
// The interface's own surface beyond the envelope. The audit counted 25
429444
// declared fields including the 3 whose names the envelope also owns
430445
// (`type`, `description`, `className`); 22 is that figure with those three

‎packages/types/src/__tests__/zod-mirror-parity.test.ts‎

Lines changed: 55 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,12 @@
154154
* a delta to this number; count the registry. Nothing asserts it against a written
155155
* one, so this line is prose and can rot; the pin that cannot is the one
156156
* comparing the two halves to each other.
157-
* - **48 entries** in `KnownDrift`, **87 keys** across them — 49 / 89 until
157+
* - **49 entries** in `KnownDrift`, **88 keys** across them — 48 / 87 until
158+
* objectui#9256 repaired the `Omit` erasure on `form.zod.ts#InputShorthandSchema`:
159+
* the face had declared nothing it inherited, so no key of it could drift, and it
160+
* now carries `InputSchema`'s members, `onChange` included. ONE entry is new with
161+
* one key, and it is `InputSchema`'s own runtime-slot drift inherited through the
162+
* repaired heritage — not drift the change introduced. It was 49 / 89 until
158163
* objectui#10874 settled the TypeScript face of the three `@object-ui/plugin-ai`
159164
* declarations per key: `onApplySuggestion` / `onRejectSuggestion` became `?: never`
160165
* (both faces now refuse them), so `ai.zod.ts#AIFormAssistSchema` measures clean and
@@ -674,7 +679,7 @@
674679
*
675680
* ## KNOWN_DRIFT is a ratchet, not a waiver
676681
*
677-
* 48 of the registered pairs carry TYPE drift TODAY (measured, not assumed). Each is
682+
* 49 of the registered pairs carry TYPE drift TODAY (measured, not assumed). Each is
678683
* pinned to its EXACT drifted key set, so the entry fails when new drift appears on
679684
* that mirror AND when the recorded drift is fixed — a stale entry cannot rot
680685
* quietly. Correcting them is not one change: the pairs below split into DISJOINT
@@ -2220,6 +2225,13 @@ interface KnownDrift {
22202225
'form.zod.ts#InputOTPSchema': 'onChange';
22212226
/** RUNTIME SLOT (objectui#6124): the `input` renderer calls `props.onChange(e.target.value)` after `SchemaRenderer`'s spread. */
22222227
'form.zod.ts#InputSchema': 'onChange';
2228+
/**
2229+
* Inherited drift: `InputSchema`'s runtime slot above, reaching this face since
2230+
* objectui#9256 repaired its `Omit` heritage (it declared nothing it inherited
2231+
* before, so nothing could drift). Both registrations spread their props into the
2232+
* same `input` renderer, so the slot is the same slot.
2233+
*/
2234+
'form.zod.ts#InputShorthandSchema': 'onChange';
22232235
/** RUNTIME SLOT (objectui#6124): the `select` renderer calls `props.onChange(matchOptionValue(…))` after `SchemaRenderer`'s spread. (This pair LEFT the ledger with objectui#5927's widenings and re-enters on a different key for a different reason.) */
22242236
'form.zod.ts#SelectSchema': 'onChange';
22252237
/** RUNTIME SLOT (objectui#6124): the `textarea` renderer calls `props.onChange(e.target.value)` after `SchemaRenderer`'s spread. */
@@ -3651,7 +3663,7 @@ export const assertionWiderLedgerRecordsEveryKey: never = 0 as unknown as WiderL
36513663
* Exact MIRRORED-BUT-UNDECLARED key set per pair — the seed of the direction
36523664
* objectui#9711 opened, measured on the tree this ledger landed on.
36533665
*
3654-
* `this ledger seeds **4 entries** carrying **60 keys**` — and ⛔ read that off the
3666+
* `this ledger seeds **2 entries** carrying **2 keys**` — and ⛔ read that off the
36553667
* census at the bottom of this file, not off this sentence: both figures are pinned
36563668
* to this ledger's own AST by 'the fourth direction is enumerated and sized at test
36573669
* time', so an entry or an arm added here moves them or the file reddens.
@@ -3669,6 +3681,16 @@ export const assertionWiderLedgerRecordsEveryKey: never = 0 as unknown as WiderL
36693681
* one" remedy the next section refuses: no key was hand-declared, the twin and the
36703682
* mirror were put on one derivation, and the ruling named the mechanism.
36713683
*
3684+
* ⭐ Then by two entries and 58 keys, when objectui#9256 executed parameter 3 of the
3685+
* ruling on objectui#8284 (Q2 A): the `Omit` COLLAPSE rows — the `email` / `password`
3686+
* and `ui:calendar` twins — left because their heritage clauses stopped collapsing
3687+
* into `BaseSchema`'s index signature. ⚠️ That is NOT the "declare the keys" remedy
3688+
* the next section refuses, and the direction is the reason: a collapsed face
3689+
* accepted EVERY key at `any`, so restoring the named members NARROWED those faces
3690+
* (`label: 42` and `body` stopped type-checking on `email`) rather than enlarging an
3691+
* accept set. No key was hand-declared; the members are inherited from the very
3692+
* declarations the mirrors `.omit()` / `.extend()`.
3693+
*
36723694
* ## ⛔ What this ledger is NOT
36733695
*
36743696
* ⛔ It is NOT a worklist to be drained by DECLARING the keys it names. Declaring a
@@ -3699,17 +3721,17 @@ export const assertionWiderLedgerRecordsEveryKey: never = 0 as unknown as WiderL
36993721
* interface restates only the subset objectui's renderers read. The remedy axis
37003722
* is the same one objectstack#4115 opened when it made the keys flow in at all,
37013723
* and it is a spec-alignment decision, not a typo.
3702-
* - **`Omit` COLLAPSE.** The declaration is `Omit< Base, K >` over an interface
3703-
* that carries `BaseSchema`'s index signature, so `Exclude< keyof Base, K >`
3704-
* resolves through `string` and the `Pick` collapses EVERY member into the index
3705-
* signature. That face has never declared the keys its docblock describes —
3706-
* measured, not assumed, and pinned independently of this file for the calendar
3707-
* twin. The mirror propagates because zod `.extend()` carries the shape. ⭐ This
3708-
* class is why the operator must not be "fixed" to read fewer keys: it is
3709-
* reporting a real property of the published declaration, loudly, which is
3710-
* precisely what a degenerate `DeclaredKeys< D >` should do. A vacuous reading
3711-
* here over-reports and is caught; the naive `keyof D` spelling under-reports to
3712-
* `never` and would never be caught at all.
3724+
* - **`Omit` COLLAPSE — EMPTIED by objectui#9256.** The declaration was
3725+
* `Omit< Base, K >` over an interface that carries `BaseSchema`'s index
3726+
* signature, so `Exclude< keyof Base, K >` resolved through `string` and the
3727+
* `Pick` collapsed EVERY member into the index signature; the mirror propagated
3728+
* because zod `.extend()` carries the shape. Both instances (the `email` /
3729+
* `password` and `ui:calendar` twins) were repaired on the DECLARATION side and
3730+
* their rows deleted. ⭐ The class is still why the operator must not be "fixed"
3731+
* to read fewer keys: it reported a real property of the published declaration,
3732+
* loudly, which is precisely what a degenerate `DeclaredKeys< D >` should do. A
3733+
* vacuous reading here over-reports and is caught; the naive `keyof D` spelling
3734+
* under-reports to `never` and would never be caught at all.
37133735
*/
37143736
interface MirroredUndeclared {
37153737
// `app.zod.ts#AppComponentSchema` HAD a row here — 20 keys of SPEC-OWNED INFLOW
@@ -3742,85 +3764,15 @@ interface MirroredUndeclared {
37423764
"complex.zod.ts#DashboardConfigSchema":
37433765
| "aria";
37443766

3745-
/**
3746-
* `Omit` COLLAPSE. The declaration is `Omit< InputSchema, 'type' | 'inputType' >`
3747-
* and `InputSchema` reaches `BaseSchema`'s index signature, so the `Omit` resolves
3748-
* through `Exclude< string, … >` = `string` and every member collapses into that
3749-
* signature. What survives as a DECLARED member is what this interface writes
3750-
* itself. Every other key the mirror states therefore reads as undeclared — which
3751-
* is a true statement about the published face, not an artefact: the checker
3752-
* answers `any` for each of them. ⛔ Do not "repair" this entry by editing the
3753-
* measurement; repairing the FACE is a declaration-side decision and a different
3754-
* card.
3755-
*/
3756-
"form.zod.ts#InputShorthandSchema":
3757-
| "label"
3758-
| "error"
3759-
| "description"
3760-
| "id"
3761-
| "name"
3762-
| "pattern"
3763-
| "placeholder"
3764-
| "className"
3765-
| "style"
3766-
| "data"
3767-
| "bind"
3768-
| "body"
3769-
| "children"
3770-
| "visible"
3771-
| "visibleWhen"
3772-
| "visibleOn"
3773-
| "hidden"
3774-
| "hiddenOn"
3775-
| "disabled"
3776-
| "disabledOn"
3777-
| "testId"
3778-
| "defaultValue"
3779-
| "ariaLabel"
3780-
| "required"
3781-
| "value"
3782-
| "min"
3783-
| "max"
3784-
| "step"
3785-
| "onChange"
3786-
| "wrapperClass"
3787-
| "readOnly"
3788-
| "maxLength";
3767+
// `form.zod.ts#InputShorthandSchema` and `form.zod.ts#UiCalendarSchema` HAD rows
3768+
// here — the whole `Omit` COLLAPSE class (32 and 26 keys): both TypeScript twins
3769+
// were spelled `Omit< Base, K >` over an interface carrying `BaseSchema`'s index
3770+
// signature, so each face declared only what it wrote itself. objectui#9256 (the E3
3771+
// residual the ruling on objectui#8284 moved there) repaired the FACES, not this
3772+
// measurement: both heritage clauses are now `OmitDeclared`, a key-remapping `Omit`
3773+
// that keeps the named members, and both pairs measure `never`. Re-adding either row
3774+
// without re-adding the collapse reddens the reconciliation below.
37893775

3790-
/**
3791-
* `Omit` COLLAPSE, the second instance and the one measured independently of this
3792-
* file: the declaration is `Omit< CalendarSchema, 'type' >`, the collapse is
3793-
* described in that declaration's own docblock, and both halves are pinned by the
3794-
* content-channel family-D twin test. This entry is what that reading looks like
3795-
* when a general operator meets it instead of a per-key tripwire.
3796-
*/
3797-
"form.zod.ts#UiCalendarSchema":
3798-
| "label"
3799-
| "description"
3800-
| "id"
3801-
| "name"
3802-
| "placeholder"
3803-
| "className"
3804-
| "style"
3805-
| "data"
3806-
| "bind"
3807-
| "body"
3808-
| "children"
3809-
| "visible"
3810-
| "visibleWhen"
3811-
| "visibleOn"
3812-
| "hidden"
3813-
| "hiddenOn"
3814-
| "disabled"
3815-
| "disabledOn"
3816-
| "testId"
3817-
| "defaultValue"
3818-
| "ariaLabel"
3819-
| "value"
3820-
| "onChange"
3821-
| "mode"
3822-
| "minDate"
3823-
| "maxDate";
38243776

38253777
// `layout.zod.ts#PageNodeSchema` HAD a row here — 10 keys (the envelope, `source`,
38263778
// `interfaceConfig`, `requires`) — deleted by objectui#9736 for the same reason:
@@ -5678,8 +5630,17 @@ outside the card that built this instrument.`)
56785630
for (const [entry, keys] of read) {
56795631
expect(keys.length, `MirroredUndeclared['${entry}'] read as having no keys`).toBeGreaterThan(0);
56805632
}
5681-
expect(keyTotal(), 'MirroredUndeclared: no entry read as a union of more than one literal — the reader is not walking union arms')
5682-
.toBeGreaterThan(read.size);
5633+
// The union-arm leg needs an entry with more than one arm to be a fact about
5634+
// THIS ledger. objectui#9256 emptied the last multi-arm entries here (the two
5635+
// `Omit` COLLAPSE rows), leaving single-key entries only — so, while that holds,
5636+
// the leg is taken on `KnownDrift`, which this SAME reader sizes above and which
5637+
// does hold multi-arm entries. It re-arms on this ledger the moment a multi-arm
5638+
// entry lands here; ⛔ it is never dropped, because a reader that stopped walking
5639+
// arms would still pass both legs above.
5640+
const armWitness = keyTotal() > read.size ? read : ledgerEntryMembers('KnownDrift');
5641+
const armKeys = Array.from(armWitness.values()).reduce((n, keys) => n + keys.length, 0);
5642+
expect(armKeys, 'no entry read as a union of more than one literal — the reader is not walking union arms')
5643+
.toBeGreaterThan(armWitness.size);
56835644
});
56845645

56855646
it('the seed spans BOTH mirror-construction styles — it is not an artefact of one', () => {

0 commit comments

Comments
 (0)