From d0303afc427954c12674a18e3e8c675a1aa78682 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 06:03:49 +0000 Subject: [PATCH 1/4] feat(types)!: refuse both content channels on `input` (objectui#9256, family D input slice) The `input` renderer reads neither `body` nor `children` (compiler-API read-site sweep on a built tree: no channel read filed under InputSchema, lit controls firing, InputSchema seen as the receiver of every key the renderer reads). Both keys are now `?: never` on the TypeScript face and a by-name retirementTombstone refusal on the zod mirror, fed one neitherContentChannelGuidance string, each kept a member. InputShorthandSchema now inherits the pair through OmitDeclared and keeps its own restated pair, which carries the message naming email / password; its docblocks say so. The family-D test header's superseded remarks and PartialSchema's docblock claim about generic Omit re-spellings are corrected (comments only). New pin file content-channel-input-9256.test.ts; one @object-ui/types minor changeset with a breaking note. Part of objectui#9256. Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk Co-authored-by: Claude --- .changeset/9256-input-content-channels.md | 35 ++++ .../content-channel-family-d-9256.test.ts | 36 +++- .../content-channel-input-9256.test.ts | 191 ++++++++++++++++++ packages/types/src/form.ts | 60 +++++- packages/types/src/index.ts | 23 ++- packages/types/src/zod/form.zod.ts | 21 ++ 6 files changed, 347 insertions(+), 19 deletions(-) create mode 100644 .changeset/9256-input-content-channels.md create mode 100644 packages/types/src/__tests__/content-channel-input-9256.test.ts diff --git a/.changeset/9256-input-content-channels.md b/.changeset/9256-input-content-channels.md new file mode 100644 index 0000000000..5b3c612770 --- /dev/null +++ b/.changeset/9256-input-content-channels.md @@ -0,0 +1,35 @@ +--- +'@object-ui/types': minor +--- + +**BREAKING (shipped as `minor` — see below):** the `input` node type now refuses +both content channels by name. Its renderer reads neither `body` nor `children`, +so an authored child list on it rendered nothing: no error, no warning, no +element. Both keys are now `?: never` on the TypeScript face (`InputSchema`) and +a by-name refusal on the zod mirror, each kept a member of the mirror shape. + +What moves for an author: + +- `children` on `input` type-checked and parsed green before; it is now refused + at authoring time and at `safeParse` time, at its own path. +- `body` on `input` was already refused on both faces, by `BaseSchema`. Its + refusal message now names what `input` renders instead, where it used to + point at `children`, which `input` does not read either. +- `email` / `password` (`InputShorthandSchema`) accept and refuse exactly what + they did before, with the same message. They already refused both channels + with their own pair, which names those two nodes. They now also inherit + `input`'s pair: on the TypeScript face it is the same `?: never`, and on the + zod mirror their own members override it, so the message an author reads + there does not change. + +No render behaviour changes: nothing read these keys, which is the whole reason +they could be refused. + +Migration: `input` renders one field from its own keys, so there is no channel +to move the content to. Put label or help text in `label`, `placeholder` or +`description`; place anything else beside the `input` in a container that reads +`children`, or drop it. + +`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. diff --git a/packages/types/src/__tests__/content-channel-family-d-9256.test.ts b/packages/types/src/__tests__/content-channel-family-d-9256.test.ts index acc8e31f94..6d35496990 100644 --- a/packages/types/src/__tests__/content-channel-family-d-9256.test.ts +++ b/packages/types/src/__tests__/content-channel-family-d-9256.test.ts @@ -47,9 +47,12 @@ * `icon`, `tabs` and `accordion` each have ONE claimant of the bare key * because every rival passes `skipFallback: true`, while `calendar`'s * several claimants name ONE full type. `ui:calendar` rides along on - * `calendar`'s tombstone by declaration (`Omit` on the TS face, `.extend()` - * on the mirror) and is pinned as its own row so the propagation is - * measured and not assumed. + * `calendar`'s tombstone by declaration (`.extend()` on the mirror, and on + * the TS face `OmitDeclared` since objectui#9256's E3 slice — the plain + * `Omit` it replaced collapsed into `BaseSchema`'s index signature and + * carried no member, the tombstone included). Its mirror half is pinned as + * its own row here; its TS half lives in + * `content-channel-e3-residual-9256.test.ts`. * - ⭐ `list` and `timeline` are narrowed HERE, by objectui#9256 slice 3. They * were held for a SERIAL constraint and ⛔ never for a verdict: both declare * in `data-display.ts`, which another slice was editing. That constraint is @@ -85,14 +88,23 @@ * to tombstone at all — it does not extend `BaseSchema` and carries only * `bodyExtra` / `bodyShape`, the prefix keys grep confuses for the channel. * A LIVE CONTROL below keeps `button` parsing both channels. - * - `InputSchema`: `InputShorthandSchema` is declared as an `Omit` of it, so a - * tombstone here would propagate onto the `email` / `password` shorthand - * face, whose registrations are `any`-typed — family E, frozen. + * - `InputSchema` is narrowed, but NOT in this file. It was held out here while + * `InputShorthandSchema` inherited from it through a plain `Omit`, which + * collapsed into `BaseSchema`'s index signature: a tombstone on `input` + * reached neither the `email` nor the `password` face. objectui#9256's E3 + * slice re-spelled that heritage as `OmitDeclared` and narrowed the two + * shorthand faces (`content-channel-e3-residual-9256.test.ts`); its `input` + * slice then narrowed `input` itself, measured to read neither channel + * (`content-channel-input-9256.test.ts`). * - `AppComponentSchema`: it declares the `app` type literal, and `app` is * served by `PageNodeSchema`, which reads BOTH channels — family C, with the * maintainer. - * - `DetailViewSchema`: its own literal is `detail-view`, and that name is - * registered with an `any`-typed renderer whose reads are unattributable. + * - `DetailViewSchema` is narrowed, but NOT in this file. The `detail-view` + * registration's `any`-typed hop hands the node to `DetailView`, typed + * `DetailViewSchema`, whose `SchemaRenderer` calls render sub-keys (`header`, + * `footer`, a tab's `content`) and never the node's own channel. The E3 slice + * attributed those reads directly and narrowed it there + * (`content-channel-e3-residual-9256.test.ts`). * - the `body` channel of the three chatbot faces: the parity ledger already * records that key as "two different meanings of one key — a naming * collision to rule on". Their `children` channel is narrowed here, and the @@ -317,9 +329,11 @@ const ROWS: ReadonlyArray { success: boolean; error?: z.ZodError }; + shape: Record; +}; + +const input = InputMirror as unknown as Mirror; +const shorthand = InputShorthandMirror as unknown as Mirror; +const CHANNELS = ['body', 'children'] as const; +const CONTENT = [{ type: 'text', content: 'measured' }]; +const issues = (m: Mirror, doc: unknown) => { + const r = m.safeParse(doc); + return r.success ? null : r.error!.issues.map((i) => ({ code: i.code, path: i.path.join('.'), message: i.message })); +}; + +/* ── (a) both channels are REFUSED BY NAME, at the key's own path ─────────── */ + +describe('objectui#9256 input slice — both content channels are refused where the renderer reads neither', () => { + it.each(CHANNELS)('input.%s is refused at that key\'s own path', (key) => { + const found = issues(input, { type: 'input', [key]: CONTENT }); + expect(found, `input.${key} parsed green — the tombstone is not installed`).not.toBeNull(); + expect(found!.some((i) => i.path === key && i.code === 'invalid_type')).toBe(true); + }); + + it.each(CHANNELS)('input.%s — the message names the channel, the node, the card, and what it renders instead', (key) => { + const issue = issues(input, { type: 'input', [key]: CONTENT })!.find((i) => i.path === key)!; + expect(issue.message).toContain(`\`${key}\``); + expect(issue.message).toContain('`input`'); + expect(issue.message).toContain('objectui#9256'); + expect(issue.message).toContain('NEITHER content channel'); + expect(issue.message).toContain('What it renders instead: '); + // ⛔ Not `BaseSchema`'s generic `body` refusal, which names `children` as + // the remedy — on `input` `children` is refused as well. + expect(issue.message).not.toContain('Did you mean'); + }); + + it.each(CHANNELS)('input.%s — ONE string feeds both author-facing channels: the issue message IS the `.describe()` metadata', (key) => { + const issue = issues(input, { type: 'input', [key]: CONTENT })!.find((i) => i.path === key)!; + expect(input.shape[key]?.description).toBe(issue.message); + }); + + it.each(CHANNELS)('input.%s — the refusal is about the KEY, not a value domain: every value is refused', (key) => { + for (const value of [CONTENT, 'text', 42, null, {}, []]) { + expect(issues(input, { type: 'input', [key]: value })?.some((i) => i.path === key)).toBe(true); + } + }); + + it.each(CHANNELS)('input.%s — the refusal reaches the node through `AnyComponentSchema`, at the root and nested', (key) => { + expect(AnyComponentSchema.safeParse({ type: 'input', label: 'Name' }).success).toBe(true); + expect(AnyComponentSchema.safeParse({ type: 'input', label: 'Name', [key]: CONTENT }).success).toBe(false); + // Nested inside a container that DOES read `children` — the outer node is + // legal, so the refusal can only be the inner `input`'s. + expect(AnyComponentSchema.safeParse({ type: 'div', children: [{ type: 'input' }] }).success).toBe(true); + expect(AnyComponentSchema.safeParse({ type: 'div', children: [{ type: 'input', [key]: CONTENT }] }).success).toBe(false); + }); +}); + +/* ── (b) CONTROLS — nothing that parsed before stops parsing ──────────────── */ + +describe('objectui#9256 input slice — CONTROLS', () => { + it('`input` still parses with the keys its renderer reads', () => { + expect(issues(input, { type: 'input' })).toBeNull(); + expect(issues(input, { + type: 'input', + name: 'email', + label: 'Email', + placeholder: 'you@example.com', + inputType: 'email', + required: true, + description: 'We never share it', + className: 'p-4', + wrapperClass: 'w-full', + })).toBeNull(); + }); + + it.each(CHANNELS)('input.%s — the tombstone is a MEMBER of the mirror shape, so the parity ratchet\'s key sets stay equal', (key) => { + expect(Object.keys(input.shape)).toContain(key); + }); + + it.each(['email', 'password'] as const)('`%s` reads the message that names ITS node — the shorthand\'s own pair overrides the inherited one', (type) => { + // The shorthand arm is `InputSchema.omit({ type, inputType }).extend({ … })`, + // so it inherits the `input` pair above and restates it. The restatement + // refuses nothing new; what it changes is the subject the author is told + // about. If it is deleted, this goes red rather than the message quietly + // starting to talk about `input`. + for (const key of CHANNELS) { + const issue = issues(shorthand, { type, [key]: CONTENT })!.find((i) => i.path === key)!; + expect(issue.code).toBe('invalid_type'); + expect(issue.message).toContain('`email` / `password`'); + expect(issue.message).not.toContain('`input` reads NEITHER'); + } + }); + + it('family C still reads a content channel — `div` takes `children`', () => { + // The distinction this card rests on: a reader keeps its channel, a + // non-reader loses both. Measured in the same run as the rows above. + expect(AnyComponentSchema.safeParse({ type: 'div', children: CONTENT }).success).toBe(true); + }); +}); + +/* ── (c) the TypeScript face — ⚠️ READ BY `tsc`, NOT BY VITEST ────────────── */ + +type Eq = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; + +describe('objectui#9256 input slice — the TypeScript face refuses both channels at the AUTHORING site', () => { + // The refused lines spread a base that the CONTROL test below proves + // compiles, so the ONLY difference an `@ts-expect-error` can be answering is + // the channel written beside it. + const base = { type: 'input', label: 'Name' } satisfies InputSchema; + + it('the `@ts-expect-error` lines in this block are the assertion; vitest only proves they are reachable', () => { + // @ts-expect-error objectui#9256 — `input` reads neither channel + const inputBody: InputSchema = { ...base, body: CONTENT }; + // @ts-expect-error objectui#9256 — `input` reads neither channel + const inputChildren: InputSchema = { ...base, children: CONTENT }; + expect([inputBody, inputChildren]).toHaveLength(2); + }); + + it('the two faces agree: the shorthand\'s restated pair is the SAME type as the one it inherits', () => { + // `tsc` is the reader. A restatement that drifted from `InputSchema`'s + // pair would make these `false` and fail to compile. + const body: Eq = true; + const children: Eq = true; + expect([body, children]).toEqual([true, true]); + }); + + it('CONTROL — the same node WITHOUT a content channel compiles (no `@ts-expect-error` here, and `tsc` is the reader)', () => { + const ok = [ + base, + { type: 'input', label: 'Email', inputType: 'email', required: true } satisfies InputSchema, + { type: 'email', label: 'Email' } satisfies InputShorthandSchema, + ]; + expect(ok).toHaveLength(3); + }); +}); diff --git a/packages/types/src/form.ts b/packages/types/src/form.ts index 613662d043..5b8b972701 100644 --- a/packages/types/src/form.ts +++ b/packages/types/src/form.ts @@ -151,6 +151,52 @@ export interface InputSchema extends BaseSchema { * Pattern for validation */ pattern?: string; + /** + * REFUSED BY NAME (objectui#9256, ADR-0049) — `input` reads NEITHER content + * channel; see `children` below for the measurement. + * + * {@link BaseSchema} already refuses `body` (objectui#6771), but that refusal + * names `children` as the remedy, and `input` does not read `children` + * either. It is restated here so the refusal names what `input` renders + * instead. + * + * @deprecated Not a channel `input` reads — nothing renders it. + */ + body?: never; + /** + * REFUSED BY NAME (objectui#9256, ADR-0049) — `input` reads NEITHER content + * channel, so an authored child list here rendered NOTHING: no error, no + * warning, no element. + * + * MEASURED with the TypeScript TYPE CHECKER and not with grep, over one + * program per workspace package plus the apps and the examples, on a BUILT + * tree reporting zero unresolved-module diagnostics — an unbuilt tree answers + * `any`, and `any` reads as NEITHER. Every `body` / `children` read is filed + * under the TYPE of the object it is read from; this declaration carries + * none, while the same instrument sees it as the receiver of every key the + * renderer reads: `defaultValue`, `description`, `error`, `id`, `inputType`, + * `label`, `max`, `maxLength`, `min`, `name`, `pattern`, `placeholder`, + * `readOnly`, `required`, `step`, `value`, `wrapperClass` (in + * `packages/components/src/renderers/form/input.tsx`). `ui:input` is the only + * registration claiming the bare `input` key, and it hands the node straight + * to that renderer. `SchemaRenderer` strips both keys out of the props bag it + * spreads, the renderer forwards only the form-control DOM keys to the native + * element, and the registration declares no `children` slot (objectui#9910). + * + * ⭐ WHY THIS NAME WAS HELD OUT, AND WHAT CHANGED. {@link InputShorthandSchema} + * inherits from this interface, and its heritage used to be a plain `Omit`, + * which collapsed into `BaseSchema`'s index signature — so a tombstone here + * reached neither the `email` nor the `password` face. objectui#9256's E3 + * slice re-spelled that heritage as the index-signature-safe `OmitDeclared`, + * after which this pair reaches both shorthand faces on the TypeScript side; + * the shorthand restates it with guidance naming its own node. + * + * What it renders instead: one input field — `label`, `placeholder`, + * `inputType`, `value` / `defaultValue`, `description`, `error`, `required`. + * + * @deprecated Not a channel `input` reads — nothing renders it. + */ + children?: never; } /** @@ -2549,10 +2595,13 @@ export interface InputShorthandSchema extends OmitDeclared = Extract; * position) — this is its generic mapped-type-alias position. * * ⭐ SEQUENCING (objectui#6397 triage, 2026-08-25) — this declaration is - * deliberately left AS WRITTEN. It is not repairable in place: `T` is generic, - * so there is no literal key list to `Pick` the way objectui#6269 could for its - * two concrete schemas, and every generic re-spelling collapses for the same - * `keyof T` reason. It is also not removable here — dropping a published export + * deliberately left AS WRITTEN. The triage also held that it was not + * repairable in place: `T` is generic, so there is no literal key list to + * `Pick` the way objectui#6269 could for its two concrete schemas, and it read + * every generic re-spelling as collapsing for the same `keyof T` reason. + * + * ⚠️ That last reading is FALSE, and objectui#9256 is the counter-example. A + * key-remapping mapped type (`{ [P in keyof T as P extends K ? never : P]: … }`) + * iterates the named members and the index signature separately, so it drops + * exactly `K` and keeps every other named member — generic in `T` and all. + * `OmitDeclared` in `./form.ts` is that spelling; the E3 slice of objectui#9256 + * re-spelled `InputShorthandSchema` and `UiCalendarSchema` with it, and + * `__tests__/content-channel-e3-residual-9256.test.ts` pins that both faces + * carry their inherited members again. So the obstacle to a repair here is not + * the type system. Re-spelling this alias would NARROW what every + * instantiation accepts (a declared member would stop accepting any value at + * `any`), a contract change that is not made in a comment correction, and + * objectui#6397 — the card that triaged it — is closed. + * + * It is also not removable here — dropping a published export * of `@object-ui/types` is a breaking removal of published capability and sits * on the human floor. Once objectui#5155 removes the root index signature, * `keyof T` resolves to the literal member union again and this alias starts diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts index d78c187c47..af9a6f48ea 100644 --- a/packages/types/src/zod/form.zod.ts +++ b/packages/types/src/zod/form.zod.ts @@ -218,6 +218,18 @@ export const ButtonSchema = BaseSchema.extend({ children: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional(), }); +/** + * objectui#9256 (`input` slice): ONE refusal string for both content channels + * of {@link InputSchema}. The `email` / `password` arm below + * ({@link InputShorthandSchema}) inherits this pair through its `.omit()` and + * overrides it with its own string, which names that node and its route. + */ +const INPUT_NEITHER_CHANNEL = neitherContentChannelGuidance( + 'input', + '`ui:input` is the only registration claiming the bare key, and it hands the node to the `input` renderer, which reads it as `InputSchema`', + 'one input field — `label`, `placeholder`, `inputType`, `value` / `defaultValue`, `description`, `error`, `required`', +); + /** * Input Schema - Text input component */ @@ -251,6 +263,11 @@ export const InputSchema = BaseSchema.extend({ step: z.number().optional().describe('Step value (for number type)'), maxLength: z.number().optional().describe('Maximum length'), pattern: z.string().optional().describe('Validation pattern'), + // objectui#9256 (`input` slice): the renderer reads NEITHER content channel, so both are + // refused by name here as on the TypeScript twin, each kept a MEMBER. `body` was already + // refused by `BaseSchema` (objectui#6771), whose message names `children` as the remedy. + body: retirementTombstone(INPUT_NEITHER_CHANNEL), + children: retirementTombstone(INPUT_NEITHER_CHANNEL), }); /** @@ -1102,6 +1119,10 @@ export const InputShorthandSchema = InputSchema.omit({ type: true, inputType: tr // // objectui#9256 (E3 residual, ruling Q2 A on objectui#8284): the renderer reads NEITHER content // channel, so both are refused by name here as on the TypeScript twin, each kept a MEMBER. + // ⚠️ Since the `input` slice, `.omit()` above already carries {@link InputSchema}'s own + // tombstone pair of the same shape, so these two refuse nothing new. They are kept because + // they override that pair's message with one that names `email` / `password` and the wrapper + // route, which is what an author of these two nodes reads. body: retirementTombstone(INPUT_SHORTHAND_NEITHER_CHANNEL), children: retirementTombstone(INPUT_SHORTHAND_NEITHER_CHANNEL), }); From 72dba894ab46ab843d3fbd97ca2f2b96f793e1fe Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 06:09:17 +0000 Subject: [PATCH 2/4] docs(types): the PartialSchema pin header carries the same false re-spelling claim; correct it (objectui#9256) partial-schema-collapse-pin.test.ts's header named the key-remapping spelling itself as collapsing. It does not: the same spelling instantiated at ObjectGridSchema / ButtonSchema keeps the named members (one-shot probe against the built d.ts, reported on the PR). Comment only; no assertion in that file moves. Also tightens the PartialSchema docblock wording. Part of objectui#9256. Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk Co-authored-by: Claude --- .../__tests__/partial-schema-collapse-pin.test.ts | 15 ++++++++++++--- packages/types/src/index.ts | 12 ++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/types/src/__tests__/partial-schema-collapse-pin.test.ts b/packages/types/src/__tests__/partial-schema-collapse-pin.test.ts index 3e955cf695..d432e34502 100644 --- a/packages/types/src/__tests__/partial-schema-collapse-pin.test.ts +++ b/packages/types/src/__tests__/partial-schema-collapse-pin.test.ts @@ -47,11 +47,20 @@ * - Retiring the alias is a REMOVAL OF A PUBLISHED EXPORT of * `@object-ui/types` — a breaking removal of published capability, which * sits on the human floor. Not a dev's call, and not this file's business. - * - Repairing it in place is not available: `T` is generic, so there is no - * literal key list to `Pick` (the escape #6269 used for its two concrete - * schemas), and every generic re-spelling — + * - Repairing it in place was read as not available: `T` is generic, so there + * is no literal key list to `Pick` (the escape #6269 used for its two + * concrete schemas), and the triage held that every generic re-spelling — * `{ [K in keyof T as K extends 'type' ? never : K]?: T[K] }` included — * collapses for the same `keyof T` reason. + * ⚠️ That last reading is FALSE. A + * key-remapping mapped type iterates the named members and the index + * signature separately, so it keeps the named members; `OmitDeclared` in + * `../form.ts` is that spelling, and objectui#9256's E3 slice repaired two + * published faces with it (pinned in + * `content-channel-e3-residual-9256.test.ts`). `PartialSchema`'s own + * docblock in `../index.ts` records why the alias is still left as + * written; this file's assertions are about the alias as written and do + * not move. * - Once objectui#5155 removes the root index signature, the alias starts * working AS WRITTEN with no edit at all, and the removal question * dissolves. diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f49436a982..524ce3bf71 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1017,12 +1017,12 @@ export type SchemaByType = Extract; * exactly `K` and keeps every other named member — generic in `T` and all. * `OmitDeclared` in `./form.ts` is that spelling; the E3 slice of objectui#9256 * re-spelled `InputShorthandSchema` and `UiCalendarSchema` with it, and - * `__tests__/content-channel-e3-residual-9256.test.ts` pins that both faces - * carry their inherited members again. So the obstacle to a repair here is not - * the type system. Re-spelling this alias would NARROW what every - * instantiation accepts (a declared member would stop accepting any value at - * `any`), a contract change that is not made in a comment correction, and - * objectui#6397 — the card that triaged it — is closed. + * `__tests__/content-channel-e3-residual-9256.test.ts` pins inherited members + * on both faces. So the obstacle to a repair here is not the type system. + * Re-spelling this alias would NARROW what every instantiation accepts (a + * declared member would stop accepting a value of the wrong type), which is a + * contract change and is not made in a comment correction; objectui#6397, the + * card that triaged it, is closed. * * It is also not removable here — dropping a published export * of `@object-ui/types` is a breaking removal of published capability and sits From 6eb3397b4d6f0fab4e4dd9fdd1e4614ed1a2cc11 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 07:13:50 +0000 Subject: [PATCH 3/4] docs(changeset): date-note three pending entries this card made false, and word the input entry's render claim exactly (objectui#9256) Prose only; every frontmatter block byte-identical. - 9256-input-content-channels: "no error, no warning, no element" was true of the render path only; the parser tier's not-a-container warning (objectui#9910) fired for an input carrying children (measured through validateTree and compile on the live registry). - 6397-partial-schema-collapse-pin: dated note, the key-remapping re-spelling does not collapse (28 / 62 on this release's tree, 27 / 61 at that entry's own commit under the same TypeScript, 1 shipped). - 9256-content-channel-family-d: dated note naming the hold-outs later slices of objectui#9256 narrowed in this release. - 8499-node-slot-registered-arms: dated note, objectui#8762 declared inputType on the shorthand arm as a by-name refusal. Part of objectui#9256. Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk Co-authored-by: Claude --- .changeset/6397-partial-schema-collapse-pin.md | 15 +++++++++++++++ .changeset/8499-node-slot-registered-arms.md | 7 +++++++ .changeset/9256-content-channel-family-d.md | 10 ++++++++++ .changeset/9256-input-content-channels.md | 5 +++-- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.changeset/6397-partial-schema-collapse-pin.md b/.changeset/6397-partial-schema-collapse-pin.md index 244bf72b19..06ebfbee34 100644 --- a/.changeset/6397-partial-schema-collapse-pin.md +++ b/.changeset/6397-partial-schema-collapse-pin.md @@ -37,6 +37,21 @@ Once #5155 removes the root index signature, the alias starts working as written with no edit at all. What this ships is the removal of the one impermissible state — *declared, published, collapsed, and unpinned*. +⚠️ **Dated note, 2026-09-28 — a generic re-spelling of this `Omit` does not collapse — +objectui#9256.** Later in this same release objectui#9256 declared a key-remapping +`OmitDeclared` (`{ [P in keyof T as P extends K ? never : P]: T[P] }`, in +`packages/types/src/form.ts`) and re-spelled the heritage of `InputShorthandSchema` and +`UiCalendarSchema` with it. Such a mapped type iterates the named members and the index +signature separately, so it drops exactly `K`, keeps every other named member with its +modifiers, and keeps the index signature. The same spelling of this alias declares 28 +properties at `ButtonSchema` and 62 at `ObjectGridSchema` in this release, where the +shipped alias declares 1 at each; at this change's own commit, under the same TypeScript, +it declares 27 and 61, the source counts in the table above. So "every generic re-spelling +collapses" was not true when it was written. The alias is still left as written: +re-spelling it would narrow what every instantiation accepts, a contract change and not a +pin's business. The `PartialSchema` docblock and `partial-schema-collapse-pin.test.ts` +carry the correction. The rest of this entry is kept as the reading of this change. + No runtime code, no type declaration and no accepted value changes; a consumer's `PartialSchema` means exactly what it meant before. Declared `patch` rather than as a no-release so the doc-comment warning actually reaches the published diff --git a/.changeset/8499-node-slot-registered-arms.md b/.changeset/8499-node-slot-registered-arms.md index 0434fec402..9995637e92 100644 --- a/.changeset/8499-node-slot-registered-arms.md +++ b/.changeset/8499-node-slot-registered-arms.md @@ -44,6 +44,13 @@ from 107 to 154: `renderers/form/input.tsx` registers onto the `input` renderer with `inputType` pinned. `inputType` is deliberately NOT declared on this arm: the wrapper spreads its own value last, so an authored one is overwritten. + ⚠️ **Dated note, 2026-09-28 — `inputType` is now declared on this arm, as a refusal — + objectui#8762.** Later in this same release the arm declares `inputType` on both faces + and refuses it by name (`?: never` on the TypeScript face, a `retirementTombstone` on + the zod mirror, at path `inputType`), with guidance pointing at + `{ "type": "input", "inputType": "email" }`. So "`inputType` is deliberately NOT declared + on this arm" no longer holds; the reason does, since the wrapper still spreads its own + value last. The rest of this entry is kept as the reading of this change. - `UiCalendarSchema` (`zod/form.zod.ts`) — `ui:calendar`, the date-picker primitive `renderers/form/calendar.tsx` registers under exactly that key (`skipFallback`, because bare `calendar` belongs to the plugin-calendar view). diff --git a/.changeset/9256-content-channel-family-d.md b/.changeset/9256-content-channel-family-d.md index 82e74f8144..251b4a7081 100644 --- a/.changeset/9256-content-channel-family-d.md +++ b/.changeset/9256-content-channel-family-d.md @@ -39,6 +39,16 @@ twins are unchanged and go on inheriting `body` as the content slot, and the rea held the key out — it was a naming collision to rule on, not a family-D read-site verdict — is the part that stayed true. +⚠️ **Dated note, 2026-09-28 — most of the hold-outs above have since been narrowed — +objectui#9256.** Later in this same release, under the same card: `text`, `image`, `icon`, +`tabs`, `accordion` and `calendar`, six of the nine names, once `check:registry-bare-names` +(objectui#9264) measured one owner for each; `list` and `timeline`; `DetailViewSchema` and +the `email` / `password` shorthand faces (`InputShorthandSchema`), whose TypeScript heritage +was re-spelled from a collapsing `Omit` to `OmitDeclared`; and `InputSchema` itself. Each +now refuses both channels on both faces, and each is its own entry in this release. +`button` and `AppComponentSchema` still accept `children`. The rest of this entry is kept as +the reading of this change. + Four published documents in the schema catalog and three component reference pages were authoring `children` on `dialog`, `drawer`, `popover` and `collapsible`, all of which read `content`. They rendered empty boxes and are corrected here. diff --git a/.changeset/9256-input-content-channels.md b/.changeset/9256-input-content-channels.md index 5b3c612770..9adb7b752c 100644 --- a/.changeset/9256-input-content-channels.md +++ b/.changeset/9256-input-content-channels.md @@ -4,8 +4,9 @@ **BREAKING (shipped as `minor` — see below):** the `input` node type now refuses both content channels by name. Its renderer reads neither `body` nor `children`, -so an authored child list on it rendered nothing: no error, no warning, no -element. Both keys are now `?: never` on the TypeScript face (`InputSchema`) and +so an authored child list on it rendered nothing, with no render-time error or +warning and no element; only the parser tier's `not-a-container` warning +(objectui#9910) noticed it. Both keys are now `?: never` on the TypeScript face (`InputSchema`) and a by-name refusal on the zod mirror, each kept a member of the mirror shape. What moves for an author: From 876f6d7eab170592343169409aaf9fd272db62e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 07:38:53 +0000 Subject: [PATCH 4/4] docs(changeset): the chatbot twins no longer take body, and the E3 entry's render claim worded exactly (objectui#9256) Prose only; both frontmatter blocks byte-identical. - 9256-content-channel-family-d: dated note after the AMENDED paragraph. objectui#6771 (pending in this release) retired body on BaseSchema, so chatbot-enhanced / chatbot-floating refuse body on both faces (TS through BaseSchema's body?: never; each mirror by name, naming requestBody), which the paragraph's "go on inheriting body" no longer describes. - 9256-e3-residual-content-channels: "no error, no warning, no element" becomes the wording this slice's own entry uses. not-a-container was measured through validateTree on the live registry for all twelve keys (every plugin registration loaded; none declares a children input). Part of objectui#9256. Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk Co-authored-by: Claude --- .changeset/9256-content-channel-family-d.md | 9 +++++++++ .changeset/9256-e3-residual-content-channels.md | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.changeset/9256-content-channel-family-d.md b/.changeset/9256-content-channel-family-d.md index 251b4a7081..69b1253b51 100644 --- a/.changeset/9256-content-channel-family-d.md +++ b/.changeset/9256-content-channel-family-d.md @@ -39,6 +39,15 @@ twins are unchanged and go on inheriting `body` as the content slot, and the rea held the key out — it was a naming collision to rule on, not a family-D read-site verdict — is the part that stayed true. +⚠️ **Dated note, 2026-09-28 — the two twins no longer take `body` — objectui#6771.** Later +in this same release objectui#6771 retired `body` as a child-list spelling on `BaseSchema` +itself, so the `body` slot `chatbot-enhanced` and `chatbot-floating` inherited is gone. Both +now refuse `body` on both faces: the TypeScript face through `BaseSchema`'s `body?: never`, +and each mirror by name, with a message pointing at `requestBody`, the key the registration +forwards to the chat runtime. So "The two twins are unchanged and go on inheriting `body` as +the content slot" no longer holds. The paragraph above is kept as the reading of that +amendment. + ⚠️ **Dated note, 2026-09-28 — most of the hold-outs above have since been narrowed — objectui#9256.** Later in this same release, under the same card: `text`, `image`, `icon`, `tabs`, `accordion` and `calendar`, six of the nine names, once `check:registry-bare-names` diff --git a/.changeset/9256-e3-residual-content-channels.md b/.changeset/9256-e3-residual-content-channels.md index e8e1565d8b..2d53f08327 100644 --- a/.changeset/9256-e3-residual-content-channels.md +++ b/.changeset/9256-e3-residual-content-channels.md @@ -6,8 +6,9 @@ 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 +reads `body` or `children`, so an authored child list on them rendered nothing, +with no render-time error or warning and no element; only the parser tier's +`not-a-container` warning (objectui#9910) noticed it. 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.