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..69b1253b51 100644 --- a/.changeset/9256-content-channel-family-d.md +++ b/.changeset/9256-content-channel-family-d.md @@ -39,6 +39,25 @@ 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` +(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-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. diff --git a/.changeset/9256-input-content-channels.md b/.changeset/9256-input-content-channels.md new file mode 100644 index 0000000000..9adb7b752c --- /dev/null +++ b/.changeset/9256-input-content-channels.md @@ -0,0 +1,36 @@ +--- +'@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, 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: + +- `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/__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/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 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 * 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), });