Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions .changeset/9256-e3-residual-content-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@object-ui/types': minor
---

**BREAKING (shipped as `minor` — see below):** twelve node types now refuse both
content channels by name — `object-grid`, `object-form`, `object-kanban`,
`object-map`, `object-tree`, `object-view`, `object-gantt`, `object-calendar`,
`object-chart`, `detail-view`, `email` and `password`. None of their renderers
reads `body` or `children`, so an authored child list on them rendered nothing:
no error, no warning, no element. Both keys are now `?: never` on the TypeScript
face and a by-name refusal on the zod mirror, each kept a member of the mirror
shape.

What moves for an author:

- `children` on any of the twelve type-checked and parsed green before; it is now
refused at authoring time and at `safeParse` time, at its own path.
- `body` was already refused on the ten `object-*` / `detail-view` nodes, on both
faces, and by the zod mirror on `email` / `password`. Its refusal message on all
twelve now names what the node renders instead, where it used to point at
`children`, which these nodes do not read either.
- The TypeScript faces of `email` / `password` (`InputShorthandSchema`) and
`ui:calendar` (`UiCalendarSchema`) now carry every member they inherit. Both were
spelled as an `Omit` over an interface with an index signature, which erased
every inherited member, so each face checked only the keys it wrote itself and
accepted every other key at `any`. Now `body`, `children` and a wrongly typed
inherited member — `label: 42` on `email`, a `mode` outside `single` /
`multiple` / `range` on `ui:calendar` — no longer type-check there. An undeclared
key still type-checks on both repaired faces: the index signature is kept.
- The `body` / `children` refusal text on `calendar` and `ui:calendar` changes
wording only, because it described the erased TypeScript face; their zod mirrors
accept exactly what they accepted before.

No render behaviour changes: nothing read these keys, which is the whole reason
they could be refused.

Migration: move the content to the channel the component renders, or drop it. On
`detail-view` that is `header`, `footer` or a tab's `content`; for the others,
place the content beside the node in a container that reads `children`.

`minor` rather than `major` because this repo's version policy forbids `major`
in any changeset — one `fixed` group — and records `minor` plus an explicit
breaking note as the spelling for a breaking change here.
333 changes: 333 additions & 0 deletions packages/types/src/__tests__/content-channel-e3-residual-9256.test.ts

Large diffs are not rendered by default.

24 changes: 7 additions & 17 deletions packages/types/src/__tests__/content-channel-family-d-9256.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ import type { ViewSwitcherSchema } from '../views';
import type { NLQuerySchema } from '../ai';
import type { TextSchema, ImageSchema, IconSchema, TabsSchema } from '../layout';
import type { AccordionSchema } from '../disclosure';
import type { CalendarSchema, UiCalendarSchema } from '../form';
import type { CalendarSchema } from '../form';

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

expect([
separatorBody, separatorChildren, kbdBody, skeletonChildren, checkboxBody, paginationChildren,
dialogChildren, collapsibleChildren, carouselBody, galleryBody, viewSwitcherChildren, reportBody,
nlQueryChildren, pivotBody, chatbotChildren,
textBody, textChildren, imageBody, imageChildren, iconBody, iconChildren,
tabsBody, tabsChildren, accordionBody, accordionChildren, calendarBody, calendarChildren,
uiCalendarBody,
listBody, listChildren, timelineBody, timelineChildren,
]).toHaveLength(32);
]).toHaveLength(31);
});

it('CONTROL — the same nodes WITHOUT a content channel compile (no `@ts-expect-error` here, and `tsc` is the reader)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ const EXCLUDED_BY_RULING_ELSEWHERE = ['SemanticElementSchema', 'HtmlElementSchem

describe('objectui#9406 -- the two names resolve from the root barrel', () => {
it('`InputShorthandSchema` resolves from the root, with its narrowed `type`', () => {
// `Omit<InputSchema, 'type' | 'inputType'>` re-narrowed: `type` is the two
// `InputSchema` without `type` / `inputType`, re-narrowed: `type` is the two
// shorthand literals, and `inputType` is unwritable here (objectui#8762).
const type: Eq<ShorthandFromRoot['type'], 'email' | 'password'> = true;
const inputType: Eq<ShorthandFromRoot['inputType'], undefined> = true;
Expand Down
23 changes: 19 additions & 4 deletions packages/types/src/__tests__/object-view-spec-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,13 @@ const SANCTIONED_LOCAL = new Set<string>([
'showViewSwitcher',
// Points at objectui's own named-view record, not a spec key.
'defaultListView',
// The two content channels, redeclared ONLY as named refusals (objectui#9256,
// the E3 residual): `ObjectView` reads neither, so both are `?: never` /
// `neitherContentChannel()` twins — declared so an authored child list is
// refused by name instead of rendering nothing. The spec's view models no child
// list on this node, so the refusal is objectui-only surface, as `viewTabBar`'s is.
'body',
'children',
]);

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

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

it('has exactly the known TS-only backlog — it may shrink, never grow', () => {
Expand Down Expand Up @@ -423,8 +433,13 @@ describe('ObjectViewSchema declared-surface consistency (#2890 scope B)', () =>
// reference — the last TS-only key closed on the zod side. So 22
// non-envelope keys, 22 + 2 = 24 declared, and the TS figure again does not
// move.
//
// objectui#9256 (the E3 residual) redeclared two ENVELOPE keys, `body` and
// `children`, as named refusals: 22 non-envelope keys still, 22 + 4 = 26
// declared. The TS figure below does not move either — both keys are
// `BaseSchema`'s, so the compiler attributes them to the envelope.
expect(ouiZodKeys.filter((k) => !ENVELOPE.has(k))).toHaveLength(22);
expect(ouiDeclaredKeys).toHaveLength(24);
expect(ouiDeclaredKeys).toHaveLength(26);
// The interface's own surface beyond the envelope. The audit counted 25
// declared fields including the 3 whose names the envelope also owns
// (`type`, `description`, `className`); 22 is that figure with those three
Expand Down
149 changes: 55 additions & 94 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@
* a delta to this number; count the registry. Nothing asserts it against a written
* one, so this line is prose and can rot; the pin that cannot is the one
* comparing the two halves to each other.
* - **48 entries** in `KnownDrift`, **87 keys** across them — 49 / 89 until
* - **49 entries** in `KnownDrift`, **88 keys** across them — 48 / 87 until
* objectui#9256 repaired the `Omit` erasure on `form.zod.ts#InputShorthandSchema`:
* the face had declared nothing it inherited, so no key of it could drift, and it
* now carries `InputSchema`'s members, `onChange` included. ONE entry is new with
* one key, and it is `InputSchema`'s own runtime-slot drift inherited through the
* repaired heritage — not drift the change introduced. It was 49 / 89 until
* objectui#10874 settled the TypeScript face of the three `@object-ui/plugin-ai`
* declarations per key: `onApplySuggestion` / `onRejectSuggestion` became `?: never`
* (both faces now refuse them), so `ai.zod.ts#AIFormAssistSchema` measures clean and
Expand Down Expand Up @@ -674,7 +679,7 @@
*
* ## KNOWN_DRIFT is a ratchet, not a waiver
*
* 48 of the registered pairs carry TYPE drift TODAY (measured, not assumed). Each is
* 49 of the registered pairs carry TYPE drift TODAY (measured, not assumed). Each is
* pinned to its EXACT drifted key set, so the entry fails when new drift appears on
* that mirror AND when the recorded drift is fixed — a stale entry cannot rot
* quietly. Correcting them is not one change: the pairs below split into DISJOINT
Expand Down Expand Up @@ -2220,6 +2225,13 @@ interface KnownDrift {
'form.zod.ts#InputOTPSchema': 'onChange';
/** RUNTIME SLOT (objectui#6124): the `input` renderer calls `props.onChange(e.target.value)` after `SchemaRenderer`'s spread. */
'form.zod.ts#InputSchema': 'onChange';
/**
* Inherited drift: `InputSchema`'s runtime slot above, reaching this face since
* objectui#9256 repaired its `Omit` heritage (it declared nothing it inherited
* before, so nothing could drift). Both registrations spread their props into the
* same `input` renderer, so the slot is the same slot.
*/
'form.zod.ts#InputShorthandSchema': 'onChange';
/** 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.) */
'form.zod.ts#SelectSchema': 'onChange';
/** RUNTIME SLOT (objectui#6124): the `textarea` renderer calls `props.onChange(e.target.value)` after `SchemaRenderer`'s spread. */
Expand Down Expand Up @@ -3651,7 +3663,7 @@ export const assertionWiderLedgerRecordsEveryKey: never = 0 as unknown as WiderL
* Exact MIRRORED-BUT-UNDECLARED key set per pair — the seed of the direction
* objectui#9711 opened, measured on the tree this ledger landed on.
*
* `this ledger seeds **4 entries** carrying **60 keys**` — and ⛔ read that off the
* `this ledger seeds **2 entries** carrying **2 keys**` — and ⛔ read that off the
* census at the bottom of this file, not off this sentence: both figures are pinned
* to this ledger's own AST by 'the fourth direction is enumerated and sized at test
* time', so an entry or an arm added here moves them or the file reddens.
Expand All @@ -3669,6 +3681,16 @@ export const assertionWiderLedgerRecordsEveryKey: never = 0 as unknown as WiderL
* one" remedy the next section refuses: no key was hand-declared, the twin and the
* mirror were put on one derivation, and the ruling named the mechanism.
*
* ⭐ Then by two entries and 58 keys, when objectui#9256 executed parameter 3 of the
* ruling on objectui#8284 (Q2 A): the `Omit` COLLAPSE rows — the `email` / `password`
* and `ui:calendar` twins — left because their heritage clauses stopped collapsing
* into `BaseSchema`'s index signature. ⚠️ That is NOT the "declare the keys" remedy
* the next section refuses, and the direction is the reason: a collapsed face
* accepted EVERY key at `any`, so restoring the named members NARROWED those faces
* (`label: 42` and `body` stopped type-checking on `email`) rather than enlarging an
* accept set. No key was hand-declared; the members are inherited from the very
* declarations the mirrors `.omit()` / `.extend()`.
*
* ## ⛔ What this ledger is NOT
*
* ⛔ It is NOT a worklist to be drained by DECLARING the keys it names. Declaring a
Expand Down Expand Up @@ -3699,17 +3721,17 @@ export const assertionWiderLedgerRecordsEveryKey: never = 0 as unknown as WiderL
* interface restates only the subset objectui's renderers read. The remedy axis
* is the same one objectstack#4115 opened when it made the keys flow in at all,
* and it is a spec-alignment decision, not a typo.
* - **`Omit` COLLAPSE.** The declaration is `Omit< Base, K >` over an interface
* that carries `BaseSchema`'s index signature, so `Exclude< keyof Base, K >`
* resolves through `string` and the `Pick` collapses EVERY member into the index
* signature. That face has never declared the keys its docblock describes —
* measured, not assumed, and pinned independently of this file for the calendar
* twin. The mirror propagates because zod `.extend()` carries the shape. ⭐ This
* class is why the operator must not be "fixed" to read fewer keys: it is
* reporting a real property of the published declaration, loudly, which is
* precisely what a degenerate `DeclaredKeys< D >` should do. A vacuous reading
* here over-reports and is caught; the naive `keyof D` spelling under-reports to
* `never` and would never be caught at all.
* - **`Omit` COLLAPSE — EMPTIED by objectui#9256.** The declaration was
* `Omit< Base, K >` over an interface that carries `BaseSchema`'s index
* signature, so `Exclude< keyof Base, K >` resolved through `string` and the
* `Pick` collapsed EVERY member into the index signature; the mirror propagated
* because zod `.extend()` carries the shape. Both instances (the `email` /
* `password` and `ui:calendar` twins) were repaired on the DECLARATION side and
* their rows deleted. ⭐ The class is still why the operator must not be "fixed"
* to read fewer keys: it reported a real property of the published declaration,
* loudly, which is precisely what a degenerate `DeclaredKeys< D >` should do. A
* vacuous reading here over-reports and is caught; the naive `keyof D` spelling
* under-reports to `never` and would never be caught at all.
*/
interface MirroredUndeclared {
// `app.zod.ts#AppComponentSchema` HAD a row here — 20 keys of SPEC-OWNED INFLOW
Expand Down Expand Up @@ -3742,85 +3764,15 @@ interface MirroredUndeclared {
"complex.zod.ts#DashboardConfigSchema":
| "aria";

/**
* `Omit` COLLAPSE. The declaration is `Omit< InputSchema, 'type' | 'inputType' >`
* and `InputSchema` reaches `BaseSchema`'s index signature, so the `Omit` resolves
* through `Exclude< string, … >` = `string` and every member collapses into that
* signature. What survives as a DECLARED member is what this interface writes
* itself. Every other key the mirror states therefore reads as undeclared — which
* is a true statement about the published face, not an artefact: the checker
* answers `any` for each of them. ⛔ Do not "repair" this entry by editing the
* measurement; repairing the FACE is a declaration-side decision and a different
* card.
*/
"form.zod.ts#InputShorthandSchema":
| "label"
| "error"
| "description"
| "id"
| "name"
| "pattern"
| "placeholder"
| "className"
| "style"
| "data"
| "bind"
| "body"
| "children"
| "visible"
| "visibleWhen"
| "visibleOn"
| "hidden"
| "hiddenOn"
| "disabled"
| "disabledOn"
| "testId"
| "defaultValue"
| "ariaLabel"
| "required"
| "value"
| "min"
| "max"
| "step"
| "onChange"
| "wrapperClass"
| "readOnly"
| "maxLength";
// `form.zod.ts#InputShorthandSchema` and `form.zod.ts#UiCalendarSchema` HAD rows
// here — the whole `Omit` COLLAPSE class (32 and 26 keys): both TypeScript twins
// were spelled `Omit< Base, K >` over an interface carrying `BaseSchema`'s index
// signature, so each face declared only what it wrote itself. objectui#9256 (the E3
// residual the ruling on objectui#8284 moved there) repaired the FACES, not this
// measurement: both heritage clauses are now `OmitDeclared`, a key-remapping `Omit`
// that keeps the named members, and both pairs measure `never`. Re-adding either row
// without re-adding the collapse reddens the reconciliation below.

/**
* `Omit` COLLAPSE, the second instance and the one measured independently of this
* file: the declaration is `Omit< CalendarSchema, 'type' >`, the collapse is
* described in that declaration's own docblock, and both halves are pinned by the
* content-channel family-D twin test. This entry is what that reading looks like
* when a general operator meets it instead of a per-key tripwire.
*/
"form.zod.ts#UiCalendarSchema":
| "label"
| "description"
| "id"
| "name"
| "placeholder"
| "className"
| "style"
| "data"
| "bind"
| "body"
| "children"
| "visible"
| "visibleWhen"
| "visibleOn"
| "hidden"
| "hiddenOn"
| "disabled"
| "disabledOn"
| "testId"
| "defaultValue"
| "ariaLabel"
| "value"
| "onChange"
| "mode"
| "minDate"
| "maxDate";

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

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