From b8244f051bce1a5d052430c0a0b1adff9b5e6778 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 13:28:54 +0000 Subject: [PATCH 1/2] feat(types)!: retire ChatbotSchema.displayMode as an ADR-0049 tombstone; drop the Display Mode control and seed (objectui#7654) `displayMode?: 'inline' | 'floating'` was declared on `ChatbotSchema` and on `ChatbotFloatingSchema`, offered as a "Display Mode" control in the `chatbot-floating` registration's `inputs`, seeded as `'floating'` by that registration's `defaultProps` -- and read by nothing. The node `type` is the one selector of presentation; this key was a second spelling of that choice. Maintainer ruling B (2026-09-05, decision batch #44): retire it. Both declarations become `displayMode?: never` with the JSDoc pointing at `type` as the replacement and citing discriminator prong 2 (advertised at CHANGELOG.md:578, 3.3.0). The designer control and the `defaultProps` seed are removed; the restatement of the control is the tombstone plus the changeset. Runtime validation of the key is unchanged -- zero before and after: it has no Zod arm on either twin and `BaseSchema` is `.passthrough()`, so stored documents the designer wrote parse exactly as they did. No `retirementTombstone()` half is minted (that is the objectui#6152 axis); the new test pins both twins' shapes as the tripwire that fires when the mirror is minted. Measured on this member (tsc, `FloatingChatbotConfig` lit as the no-index- signature control): declared, a wrong value is TS2322 and presence is clean; DELETED, every shape is silent and the member reads as `any`; TOMBSTONED, presence with any value is TS2322 in every shape. On a `BaseSchema` carrier the routes are loud-vs-silent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .../7654-chatbot-display-mode-tombstone.md | 99 ++++++++ content/docs/plugins/plugin-chatbot.mdx | 38 ++- packages/plugin-chatbot/src/renderer.tsx | 17 +- .../chatbot-display-mode-retired.test.ts | 223 ++++++++++++++++++ ...-registration-authoring-faces-7655.test.ts | 72 +++--- .../src/__tests__/zod-mirror-parity.test.ts | 16 +- packages/types/src/complex.ts | 113 +++++++-- packages/types/src/zod/complex.zod.ts | 15 +- 8 files changed, 510 insertions(+), 83 deletions(-) create mode 100644 .changeset/7654-chatbot-display-mode-tombstone.md create mode 100644 packages/types/src/__tests__/chatbot-display-mode-retired.test.ts diff --git a/.changeset/7654-chatbot-display-mode-tombstone.md b/.changeset/7654-chatbot-display-mode-tombstone.md new file mode 100644 index 000000000..9bc8fcba5 --- /dev/null +++ b/.changeset/7654-chatbot-display-mode-tombstone.md @@ -0,0 +1,99 @@ +--- +'@object-ui/types': minor +'@object-ui/plugin-chatbot': patch +--- + +Retire `ChatbotSchema.displayMode` — and its copy on `ChatbotFloatingSchema` — as an +ADR-0049 retirement tombstone, and remove the `chatbot-floating` registration's +"Display Mode" designer control and its `defaultProps.displayMode: 'floating'` seed +(objectui#7654, maintainer ruling B of 2026-09-05, director decision batch #44). + +⚠️ **BREAKING for anyone authoring `displayMode` against a chatbot face in TypeScript.** +Ships as `minor` per the launch-window convention: objectui's `major` is a cross-repo pin +to `@objectstack`'s so that "same major means compatible" holds across the two repos +(`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes ship as +`minor` with the break named where it lands — this entry is the channel that carries it. + +## What was retired, and why + +The node `type` — `chatbot-floating` versus `chatbot` / `chatbot-enhanced` — is the one +selector of presentation. `displayMode` (`'inline' | 'floating'`) was a second spelling +of that same choice, and no renderer has ever read it: `chatbot-floating` renders the +trigger and panel unconditionally, and `chatbot` never looked at the key, so +`displayMode: 'floating'` on a `chatbot` node produced no trigger and `'inline'` on a +`chatbot-floating` node changed nothing. It was nevertheless declared on both faces, +painted as a **Display Mode** control in the designer's property panel, and written as +`'floating'` into every node the designer created — two surfaces teaching a switch that +did not exist. + +Re-measured on this branch's base rather than inherited from the card: a whole-repo +`git grep` census over tracked files, build output excluded, returned the declarations, +the doc comments and parity-ledger entries beside them, one historical CHANGELOG line and +two unrelated `displayMode` props on `GridField` / `MasterDetailForm` — no read. The same +pass over `floatingConfig`, a key that IS read, returned 79 lines, so the instrument was +not blind. + +FROM → TO: + +- `ChatbotSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, an + ADR-0049 retirement tombstone whose comment points at `type` as the replacement. +- `ChatbotFloatingSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, + the same tombstone. objectui#7655 declared the key on the floating face with + `ChatbotSchema`'s own lines precisely so this retirement would find it on both faces; + leaving the copy typed would have kept the published face teaching the switch. +- `chatbot-floating` `inputs`: the **Display Mode** control is removed. +- `chatbot-floating` `defaultProps`: `displayMode: 'floating'` is no longer written into + designer-created nodes. + +A control is restated, never deleted into a vacuum (objectui#7070): the restatement of +the removed control is the tombstone's guidance plus this note. + +**Migration.** Delete `displayMode` from any TypeScript literal typed as `ChatbotSchema` +or `ChatbotFloatingSchema`; the presentation you wanted is already chosen by `type` — +`'chatbot-floating'` for the trigger-and-panel, `'chatbot'` / `'chatbot-enhanced'` for +inline. **No JSON document needs editing** — see the next section. + +## Stored documents: runtime validation of this key is unchanged — zero before, zero after + +`displayMode` has never had a Zod arm — it sits in the `UnmirroredDeclared` ledger for +both `complex.zod.ts#ChatbotSchema` and `#ChatbotFloatingSchema`, and `BaseSchema` is +`.passthrough()` — so a stored document carrying `displayMode: 'floating'` (every node +the designer ever created) parses green before this change and parses green after it, +and the value is dropped at render time exactly as it always was. + +That is deliberate, and it is why this tombstone has **no `retirementTombstone()` +half**: minting a mirror arm to refuse the key would be the declared-but-unmirrored axis +(objectui#6152), a different defect, and a parse outcome the ruling did not ask for. +`packages/types/src/__tests__/chatbot-display-mode-retired.test.ts` pins both twins' +shapes as a **tripwire** — the same shape objectui#7669 gave `triggerIcon` — so that if +objectui#6152 ever mints an arm for `displayMode`, the pin goes red and whoever lands the +mirror adds the `retirementTombstone()` half at that time, flipping the control rather +than deleting it. + +## Why a tombstone and not a deletion — measured on this carrier + +`ChatbotSchema` extends `BaseSchema`, which carries a `[key: string]: any` index +signature, and on such a carrier deleting an optional member is **silent in every value +shape**: the index signature defeats both excess-property checking and the weak-type +check. Measured on this member with `tsc -p tsconfig.test.json`, a no-index-signature +control carrier (`FloatingChatbotConfig`) lit in the same run: + +| route | fresh `'floating'` | fresh `'bogus'` | widened `'floating'` | +|---|---|---|---| +| declared (before) | clean | `TS2322` | clean | +| deleted | clean | **clean** | clean | +| tombstoned (after) | `TS2322` | `TS2322` | `TS2322` | + +Deleted, the member reads as `any` and even a wrong-typed value goes quiet. Tombstoned, +**presence with any value** is a compile error — a channel deletion cannot produce on +this carrier at all. On a `BaseSchema` carrier the two routes are loud-vs-silent, not +louder-vs-quieter (the discriminator's carrier branch as corrected on objectui#7678). +Prong 2 of that discriminator licenses the tombstone: the key was advertised in the +3.3.0 release record (`CHANGELOG.md:578`) and its published comment taught it as the +presentation switch. The deleted row is pinned in the test file as a live control — an +undeclared key that rides both shapes with no directive — so the contrast cannot rot. + +**Accept-set change, stated plainly for reviewers:** on the TypeScript face, a write of +`displayMode` against either chatbot face used to compile and now does not. On the +runtime face nothing changes at all. On the designer face, one control disappears from +the `chatbot-floating` property panel and new nodes no longer carry the key. diff --git a/content/docs/plugins/plugin-chatbot.mdx b/content/docs/plugins/plugin-chatbot.mdx index 7a1e8ca2d..bf852d455 100644 --- a/content/docs/plugins/plugin-chatbot.mdx +++ b/content/docs/plugins/plugin-chatbot.mdx @@ -160,10 +160,10 @@ author against. That is why the scope is spelled out here as well as in the types. The table below is that shared chat surface. `chatbot-floating` declares -seven more keys of its own - `displayMode` and six `floatingConfig` entries - -which no row below carries and which the other two registrations have no -trigger or panel to apply. They are documented in their own table after this -one, under **`chatbot-floating` panel and trigger keys**. +six more keys of its own - the six `floatingConfig` entries - which no row +below carries and which the other two registrations have no trigger or panel +to apply. They are documented in their own table after this one, under +**`chatbot-floating` panel and trigger keys**. | Property | Type | Default | Description | |----------|------|---------|-------------| @@ -195,19 +195,33 @@ one, under **`chatbot-floating` panel and trigger keys**. ### `chatbot-floating` panel and trigger keys -The seven keys below are declared in the `chatbot-floating` registration's own +The six keys below are declared in the `chatbot-floating` registration's own `inputs` (`packages/plugin-chatbot/src/renderer.tsx`). They configure the floating action button and the panel it opens; the `chatbot` and -`chatbot-enhanced` registrations render neither and ignore them. `displayMode` -and `floatingConfig` are declared on `ChatbotSchema` and on -`ChatbotFloatingSchema` alike (objectui#7655 declared the floating face with the -same two members; `ChatbotSchema` kept its own), so authoring them on an inline -node type-checks and parses - and is dropped at render time, because the -`chatbot` node never read either. +`chatbot-enhanced` registrations render neither and ignore them. `floatingConfig` +is declared on `ChatbotSchema` and on `ChatbotFloatingSchema` alike +(objectui#7655 declared the floating face with the same member; `ChatbotSchema` +kept its own), so authoring it on an inline node type-checks and parses - and +is dropped at render time, because the `chatbot` node never read it. + +**There is no `displayMode` key.** The presentation is selected by the node's +own `type`: author a `chatbot-floating` node for the trigger-and-panel +presentation, and a `chatbot` or `chatbot-enhanced` node for an inline one. +`displayMode` (`'inline' | 'floating'`) used to be declared on both faces, +offered as a **Display Mode** control in the designer and seeded as +`'floating'` into every node the designer created - and read by nothing: it was +a second spelling of the choice `type` already makes, so `'inline'` on a +`chatbot-floating` node changed nothing and `'floating'` on a `chatbot` node +produced no trigger. objectui#7654 retired it (maintainer ruling, 2026-09-05): +the declaration is a `never` tombstone on `ChatbotSchema` and +`ChatbotFloatingSchema`, so writing the key against either face is now a +compile error, and the designer control and default are gone. Stored documents +that still carry the key parse exactly as they did - it never had a Zod arm and +the twins are passthrough - and the value is ignored at render time, as it +always was. | Property | Type | Default | Description | |----------|------|---------|-------------| -| `displayMode` | `'inline' \| 'floating'` | `'floating'` | **Declared and offered in the designer, but read by nothing.** The node's own `type` selects the presentation: a `chatbot-floating` node renders the trigger and panel unconditionally, and authoring `'inline'` here does not make it inline - author a `chatbot` node for that. The registration declares it with `defaultValue: 'floating'` and writes the same value into its `defaultProps`, so nodes created in the designer carry it. objectui#7654 ruled it retired (maintainer, 2026-09-05): the declaration becomes a `never` tombstone and the designer control and default are removed in that card's own change; until that lands the key is carried exactly as described here | | `floatingConfig.position` | `'bottom-right' \| 'bottom-left'` | `'bottom-right'` | Corner the trigger sits in; the panel is anchored to the same side | | `floatingConfig.defaultOpen` | boolean | `false` | Whether the panel is already open when the node mounts | | `floatingConfig.panelWidth` | number | `400` | Panel width in pixels, applied from the `sm` breakpoint up - below it the panel is full-bleed. Snapped to a step, see below | diff --git a/packages/plugin-chatbot/src/renderer.tsx b/packages/plugin-chatbot/src/renderer.tsx index 3dbd2634c..8529f2cbc 100644 --- a/packages/plugin-chatbot/src/renderer.tsx +++ b/packages/plugin-chatbot/src/renderer.tsx @@ -273,7 +273,9 @@ ComponentRegistry.register('chatbot-enhanced', // anonymous `ChatbotSchema & { ... }` intersection local to this file; // every key that intersection carried was read-site-censused before being // declared on `ChatbotEnhancedSchema`, and the two `ChatbotSchema` keys this - // registration never read (`displayMode`, `floatingConfig`) are not on it. + // registration never read (`floatingConfig`, and `displayMode` — since + // retired as a `?: never` tombstone on both faces, objectui#7654) are not on + // it. // `surface` (objectui#6687, maintainer ruling 2026-08-29) is declared there // too; the plugin's own `ChatbotSurface` alias is pinned equal to it in // `__tests__`, so the union has one contract, not two dialects @@ -494,8 +496,18 @@ ComponentRegistry.register('chatbot-floating', { namespace: 'plugin-chatbot', label: 'Chatbot (Floating)', + // `displayMode` is NOT offered here and NOT seeded below (objectui#7654, + // maintainer ruling B, 2026-09-05). The control painted a "Display Mode" + // switch this registration never read — the node `type` is the one + // selector of presentation, and `` below renders + // unconditionally — while `defaultProps` wrote `'floating'` into every + // designer-created node. The control is restated, not deleted into a + // vacuum (objectui#7070): the restatement is the `?: never` tombstone on + // `ChatbotSchema` / `ChatbotFloatingSchema` in `@object-ui/types` and the + // release note. Stored documents carrying the key are unaffected — it has + // no Zod arm and `BaseSchema` is `.passthrough()`, so they parse exactly + // as before, and nothing here ever read the value. inputs: [ - { name: 'displayMode', type: 'string', label: 'Display Mode', defaultValue: 'floating', description: 'Set to "floating" for FAB widget' }, { name: 'floatingConfig.position', type: 'string', label: 'FAB Position', defaultValue: 'bottom-right', description: 'bottom-right or bottom-left' }, { name: 'floatingConfig.defaultOpen', type: 'boolean', label: 'Default Open', defaultValue: false }, { name: 'floatingConfig.panelWidth', type: 'number', label: 'Panel Width', defaultValue: 400 }, @@ -517,7 +529,6 @@ ComponentRegistry.register('chatbot-floating', { name: 'className', type: 'string', label: 'CSS Class' }, ], defaultProps: { - displayMode: 'floating', floatingConfig: { position: 'bottom-right', defaultOpen: false, diff --git a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts new file mode 100644 index 000000000..97d76b996 --- /dev/null +++ b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts @@ -0,0 +1,223 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `ChatbotSchema.displayMode` — and its copy on `ChatbotFloatingSchema` — is + * an ADR-0049 RETIREMENT TOMBSTONE (objectui#7654, maintainer ruling B, + * 2026-09-05), and its refusal is TYPE-LEVEL ONLY. Both halves are pinned here. + * + * ## What was measured + * + * `displayMode?: 'inline' | 'floating'` was declared on both chatbot faces, + * offered as a "Display Mode" control in the `chatbot-floating` registration's + * `inputs`, seeded as `'floating'` by that registration's `defaultProps` — and + * read by nothing. `chatbot-floating` renders `` + * unconditionally and `chatbot` never looked at the key: the node `type` is + * the one selector of presentation, and this key was a second spelling of it. + * Whole-repo `git grep` census, tracked files, build output excluded: the + * declarations, the doc comments and ledger entries beside them, one historical + * CHANGELOG line and two unrelated `displayMode` props on `GridField` / + * `MasterDetailForm`; the same pass over `floatingConfig`, a key that IS read, + * returned 79 lines, so the instrument was not blind. The control and the seed + * are gone in the same change; the restatement of that control is the + * tombstone plus the release note (objectui#7070). + * + * ## Why a tombstone, and what it buys on THIS carrier + * + * `ChatbotSchema` extends `BaseSchema`, which carries `[key: string]: any`. + * On such a carrier deleting an optional member is SILENT in every value shape + * — the index signature defeats both excess-property checking and the + * weak-type check — so the usual "a fresh literal would at least trip TS2353" + * comfort does not exist here. Measured on this member with + * `tsc -p tsconfig.test.json`, `FloatingChatbotConfig` (no index signature) + * lit as the control carrier in the same run: + * + * | route | fresh `'floating'` | fresh `'bogus'` | widened `'floating'` | undeclared key | + * |------------|--------------------|-----------------|----------------------|----------------| + * | declared | clean | TS2322 | clean | clean | + * | DELETED | clean | **clean** | clean | clean | + * | TOMBSTONED | TS2322 | TS2322 | TS2322 | clean | + * | control | — | — | — | TS2353 / TS2559| + * + * Deleted, the member reads as `any` and even a wrong-typed value goes quiet. + * Tombstoned, PRESENCE with any value is a compile error — a channel deletion + * cannot produce on this carrier at all. The routes are loud-vs-silent, not + * louder-vs-quieter, which is the discriminator's carrier branch as corrected + * on objectui#7678. Prong 2 licenses the tombstone: the key was advertised in + * the 3.3.0 release record (`CHANGELOG.md:578`) and its comment taught it as + * the presentation switch. + * + * The `@ts-expect-error` directives are REAL enforcement: this package + * type-checks its tests through `tsconfig.test.json`, so re-widening the + * declaration fails the build on the unused directive. A green `vitest` run is + * NOT evidence about them — type assertions are erased before it runs. The + * "deleted" row is pinned LIVE below, as an undeclared key that carries no + * directive, so the contrast cannot rot into prose. + */ + +import { describe, it, expect } from 'vitest'; +import type { + ChatMessage, + ChatbotFloatingSchema as TsChatbotFloatingSchema, + ChatbotSchema as TsChatbotSchema, + FloatingChatbotConfig, +} from '../complex'; +import { ChatbotFloatingSchema, ChatbotSchema } from '../zod/complex.zod'; + +/* ── type-level pins: the `tsc` channel ──────────────────────────────────── */ + +/** Invariant equality — `extends` both ways would accept a narrowing. */ +type Equal = + (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Expect = T; + +/** + * What the member READS as under the tombstone: `undefined`, not `never` — + * `?: never` without `exactOptionalPropertyTypes` is `never | undefined`, + * which collapses. `Equal` still tells that apart from the `any` a deletion + * leaves behind (measured: deleted, `Equal<…, any>` is the row that goes + * green), so this pin reddens on a deletion as well as on a re-widening. + */ +export type assertionDisplayModeReadsAsTombstone = [ + Expect>, + Expect>, +]; + +const messages: ChatMessage[] = [{ id: 'm1', role: 'user', content: 'hi' }]; + +describe('the `displayMode` tombstone makes authoring a `tsc` error on both faces', () => { + it('refuses the value that used to be VALID, in a fresh literal — presence is the error, not the value', () => { + const onChatbot: TsChatbotSchema = { + type: 'chatbot', + messages, + // @ts-expect-error `displayMode` is a retirement tombstone (objectui#7654) — select the presentation with `type` + displayMode: 'floating', + }; + const onFloating: TsChatbotFloatingSchema = { + type: 'chatbot-floating', + messages, + // @ts-expect-error `displayMode` is a retirement tombstone (objectui#7654) — a `chatbot-floating` node IS the floating presentation + displayMode: 'floating', + }; + expect(onChatbot.type).toBe('chatbot'); + expect(onFloating.type).toBe('chatbot-floating'); + }); + + it('refuses a wrong-typed value too — the row that went SILENT under deletion', () => { + const onChatbot: TsChatbotSchema = { + type: 'chatbot', + messages, + // @ts-expect-error `displayMode` is a retirement tombstone (objectui#7654) + displayMode: 'bogus', + }; + expect(onChatbot.type).toBe('chatbot'); + }); + + it('refuses it through a WIDENED value — the shape no excess-property check ever reaches', () => { + const rawChatbot = { type: 'chatbot' as const, messages, displayMode: 'floating' as const }; + // @ts-expect-error `displayMode` is a retirement tombstone (objectui#7654), reached through a widened value + const onChatbot: TsChatbotSchema = rawChatbot; + const rawFloating = { type: 'chatbot-floating' as const, messages, displayMode: 'floating' as const }; + // @ts-expect-error `displayMode` is a retirement tombstone (objectui#7654), reached through a widened value + const onFloating: TsChatbotFloatingSchema = rawFloating; + expect(onChatbot.type).toBe('chatbot'); + expect(onFloating.type).toBe('chatbot-floating'); + }); + + it('keeps the LIVE keys writable — the non-vacuity control', () => { + // Without this, a change that broke either face would satisfy every + // assertion above by accident. `floatingConfig` is the key the floating + // registration actually reads; `processVisibility` is a typed union + // `ChatbotSchema` still declares. + const onFloating: TsChatbotFloatingSchema = { + type: 'chatbot-floating', + messages, + floatingConfig: { position: 'bottom-left', defaultOpen: true, title: 'Chat' }, + }; + const onChatbot: TsChatbotSchema = { + type: 'chatbot', + messages, + processVisibility: 'debug', + }; + expect(onFloating.floatingConfig?.title).toBe('Chat'); + expect(onChatbot.processVisibility).toBe('debug'); + }); + + it('an UNDECLARED key rides a fresh literal AND a widened value on this carrier — the DELETED row, live', () => { + // No directive on purpose: this is where `displayMode` would sit had it + // been deleted instead of tombstoned. `BaseSchema`'s `[key: string]: any` + // absorbs the key at any value in every shape, so a deletion here is not + // "quieter" than a tombstone — it produces no diagnostic at all. + const fresh: TsChatbotSchema = { type: 'chatbot', messages, bogusUndeclared: 1 }; + const raw = { type: 'chatbot' as const, messages, bogusUndeclared: 1 }; + const widened: TsChatbotSchema = raw; + expect(fresh.type).toBe('chatbot'); + expect(widened.type).toBe('chatbot'); + }); + + it('…and the same undeclared key IS refused on a carrier without an index signature — the instrument control', () => { + // `FloatingChatbotConfig` has no index signature, so the compiler's two + // ordinary guards are visible here: excess-property checking on a fresh + // literal (TS2353) and the weak-type check on a lone-key widened value + // (TS2559). Their firing proves the silence above is the index signature, + // not a blind run. + const fresh: FloatingChatbotConfig = { + title: 'Chat', + // @ts-expect-error TS2353 — `FloatingChatbotConfig` has no index signature, so a fresh undeclared key is refused + bogusUndeclared: 1, + }; + const loneKey = { bogusUndeclared: 1 }; + // @ts-expect-error TS2559 — the weak-type check fires on a lone-key widened value without an index signature + const widened: FloatingChatbotConfig = loneKey; + expect(fresh.title).toBe('Chat'); + expect(widened).toBeDefined(); + }); +}); + +/* ── the runtime channel: DELIBERATELY unchanged, and a tripwire if that ends ─ */ + +// Both faces declared `displayMode` and NEITHER twin has an arm for it, so the +// tripwire parses both nodes, each through its own twin. +describe.each([ + ['chatbot', ChatbotSchema], + ['chatbot-floating', ChatbotFloatingSchema], +] as const)('there is NO zod refusal of `displayMode` on a `%s` node, and that is deliberate (objectui#7654)', (type, twin) => { + const node = { type, messages }; + + it(`a ${type} node carrying \`displayMode: 'floating'\` — what every designer-created node carried — still parses GREEN`, () => { + // Runtime validation of this key is ZERO before and after the retirement: + // `displayMode` sits in the `UnmirroredDeclared` ledger for both twins + // (`zod-mirror-parity.test.ts`) and `BaseSchema` is `.passthrough()`, so a + // stored document the designer wrote parses exactly as it did before the + // ruling and the value is dropped at render time, as it always was. + expect(twin.safeParse({ ...node, displayMode: 'floating' }).success).toBe(true); + expect(twin.safeParse({ ...node, displayMode: 'anything-at-all' }).success).toBe(true); + }); + + it('the twin really has no `displayMode` arm — the SHAPE pin, which is the tripwire that fires', () => { + // ⚠️ TRIPWIRE: if objectui#6152 ever mints a house-style (non-strict) arm + // for `displayMode`, THIS assertion goes red — the parse-green line above + // stays green for a non-strict arm and only reddens for a `.strict()` + // mirror (objectui#7678 item 2 measured both shapes). Red here is the + // intended signal, not a nuisance: whoever lands the mirror adds the + // `retirementTombstone()` half for `displayMode` at the same time, and + // flips this control rather than deleting it into a vacuum. + const shape = (twin as unknown as { shape: Record }).shape; + expect(shape.displayMode).toBeUndefined(); + // Lit control: a key the twin DOES declare is in its shape, so the reading + // above is a measurement and not an empty object. + expect(shape.messages).toBeDefined(); + }); + + it('a live key on the same twin is validated — the non-vacuity control for `.passthrough()`', () => { + // `messages` is an arm on both twins: a wrong value is refused, so the + // green readings above are the absence of an arm, not a twin that accepts + // everything. + expect(twin.safeParse({ ...node, messages: 'not-an-array' }).success).toBe(false); + }); +}); diff --git a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts index 3448d2205..f947cba2a 100644 --- a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts +++ b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts @@ -37,18 +37,18 @@ * its `@ts-expect-error` pins. The `it` blocks are the RUNTIME channel: the Zod * twins' accept sets. * - * ## `displayMode` and `floatingConfig` live on BOTH faces; neither is decided here + * ## `displayMode` and `floatingConfig` live on BOTH faces; `displayMode` is a tombstone on both * - * `ChatbotSchema` keeps both members exactly as it had them (declarations - * verbatim), and `ChatbotFloatingSchema` declares the same two — the designer - * control and the `defaultProps` seed for `displayMode` are the - * `chatbot-floating` registration's. `displayMode` is RULED RETIRED: - * objectui#7654, maintainer ruling B (2026-09-05) — `?: never` tombstone on - * `ChatbotSchema`, control and seed removed — and that retirement executes in - * #7654's own PR, which must find the member on both faces as ruled. So this - * card carries the key untouched, and the runtime pin below asserts it STILL - * parses green with any value, as a tripwire: that PR flips the pin - * deliberately, with the ruling in hand, rather than have it change under it. + * `ChatbotSchema` keeps `floatingConfig` exactly as it had it, and + * `ChatbotFloatingSchema` declares the same member. `displayMode` was carried + * the same way — declared verbatim on both faces, untouched by this card — + * until objectui#7654 RETIRED it (maintainer ruling B, 2026-09-05): it is a + * `?: never` tombstone on both faces now, the designer control and the + * `defaultProps` seed in the `chatbot-floating` registration are gone, and the + * key stays UNMIRRORED on both twins. The runtime pin below still asserts it + * parses green with any value, as a tripwire for the moment objectui#6152 + * mints an arm; the tombstone's own pins are in + * `chatbot-display-mode-retired.test.ts`. * * ## The census counts NAMED reads; the floating registration has a second channel * @@ -147,8 +147,10 @@ export type assertionFloatingDeclaresWhatItReads = Expect< /** * `chatbot` keeps its WHOLE face — the six legacy keys (`loading` … `height`), - * the `onSendMessage` tombstone, and `displayMode` / `floatingConfig` — exactly - * where they were. This card declared faces; it retired and moved nothing. + * the `onSendMessage` and (since objectui#7654) `displayMode` tombstones, and + * `floatingConfig` — exactly where they were. A `?: never` member is still a + * declared key, so the census does not move when a key is tombstoned. This + * card declared faces; it retired and moved nothing. */ export type assertionChatbotKeepsItsWholeFace = Expect< Equal< @@ -164,10 +166,13 @@ export type assertionChatbotKeepsItsWholeFace = Expect< * here — it would read as `any` through `BaseSchema`'s index signature, wrong * values would compile, and the objectui#7669 `triggerIcon` tombstone would lose * its reach on `chatbot` nodes (all three measured on #7655's first cut, which - * moved the keys). `Equal` is what catches the `any`. + * moved the keys). `Equal` is what catches the `any`. `displayMode` reads as + * `undefined` since objectui#7654 tombstoned it (`?: never` without + * `exactOptionalPropertyTypes` is `never | undefined`, which collapses) — a + * reading `Equal` still tells apart from the `any` a deletion would leave. */ export type assertionFloatingKeysStayTypedOnChatbot = [ - Expect>, + Expect>, Expect>, ]; @@ -187,7 +192,8 @@ export type assertionSharedKeysAreOneDeclaration = [ export type assertionFloatingKeysHaveOneTypeOnBothFaces = [ Expect>, Expect>, - Expect>, + // Both faces carry the objectui#7654 tombstone, so both read `undefined`. + Expect>, Expect>, ]; @@ -242,7 +248,6 @@ describe('the two faces annotate the nodes their registrations render (objectui# type: 'chatbot-floating', messages: baseMessages, floatingConfig: { position: 'bottom-left', defaultOpen: true, panelHeight: 520, title: 'Support' }, - displayMode: 'floating', enableMarkdown: false, onClear: () => undefined, }; @@ -281,13 +286,16 @@ describe('the two faces annotate the nodes their registrations render (objectui# // @ts-expect-error `panelHeight` is a number of pixels, not a CSS length floatingConfig: { panelHeight: '520px' }, }; - // …and on `ChatbotSchema` too, which still declares both keys: a wrong - // `displayMode` is refused there, not swallowed as `any`. + // …and on `ChatbotSchema` too: a wrong value on a key it declares as a + // union is refused there, not swallowed as `any`. (`displayMode` used to + // be this pin's key; it is a tombstone since objectui#7654, and a tombstone + // refuses PRESENCE, which is a different pin — see + // `chatbot-display-mode-retired.test.ts`.) const chatbot: ChatbotSchema = { type: 'chatbot', messages: baseMessages, - // @ts-expect-error `displayMode` is the typed union on `ChatbotSchema`, unchanged - displayMode: 'bogus', + // @ts-expect-error `processVisibility` is the typed union on `ChatbotSchema` + processVisibility: 'bogus', }; expect(enhanced.type).toBe('chatbot-enhanced'); expect(floating.type).toBe('chatbot-floating'); @@ -367,7 +375,6 @@ describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and le const result = ChatbotFloatingZod.safeParse({ ...node, floatingConfig: { position: 'bottom-left', defaultOpen: true, panelWidth: 400, panelHeight: 520, title: 'Support', triggerSize: 56 }, - displayMode: 'floating', enableMarkdown: true, enableFileUpload: true, requestBody: { tenant: 'acme' }, @@ -382,15 +389,18 @@ describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and le } }); - it('TRIPWIRE — `displayMode` is unmirrored here as on `ChatbotSchema`: any value still parses green (objectui#7654 ruled it retired; its own PR flips this)', () => { - // Declared on the face with the same `'inline' | 'floating'` type - // `ChatbotSchema` carries, deliberately NOT given a mirror arm: it is - // declared-but-unmirrored on both faces. A value the declaration would - // refuse rides through `.passthrough()` here, as it does on `ChatbotSchema`'s - // twin. objectui#7654 RULED the key retired (maintainer ruling B, - // 2026-09-05); that card's own PR lands the tombstone and flips this pin - // with the ruling in hand — if it goes red any other way, someone mirrored, - // retired or wired the key silently. + it('TRIPWIRE — `displayMode` stays unmirrored here as on `ChatbotSchema`: any value still parses green (retired by objectui#7654; the `retirementTombstone()` half is owed when objectui#6152 mints an arm)', () => { + // objectui#7654 retired the key (maintainer ruling B, 2026-09-05): a + // `?: never` tombstone on both TypeScript faces, designer control and seed + // removed. The RUNTIME face was deliberately left alone — the key has no + // mirror arm on either twin and `BaseSchema` is `.passthrough()`, so a + // stored document carrying it parses exactly as it did before the ruling. + // The SHAPE pin is the assertion that fires if objectui#6152 mints a + // house-style (non-strict) arm for the key; the parse-green line after it + // only fires for a `.strict()` mirror (objectui#7678 item 2). Red here is + // the signal to add the `retirementTombstone()` half at the same time, not + // to delete the pin. The tombstone's own pins are in + // `chatbot-display-mode-retired.test.ts`. expect((ChatbotFloatingZod.shape as Record).displayMode).toBeUndefined(); expect(ChatbotFloatingZod.safeParse({ ...node, displayMode: 'anything-at-all' }).success).toBe(true); // Lit control on the same instrument: a key the twin DOES declare is in its diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 2addbb038..a14cc579c 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -1095,7 +1095,10 @@ interface UnmirroredDeclared { /** * LOCAL. `body` sits in `KnownDrift` above for an unrelated reason (a naming * collision on a key both sides declare); these three the mirror has simply never - * heard of. + * heard of. `displayMode` is a `?: never` tombstone since objectui#7654 (maintainer + * ruling B, 2026-09-05) and stays listed: the TypeScript half is the tombstone, the + * mirror half (`retirementTombstone()`) is owed when objectui#6152 mints the arm, and + * `chatbot-display-mode-retired.test.ts` pins this twin's shape as the tripwire. */ 'complex.zod.ts#ChatbotSchema': 'displayMode' | 'floatingConfig' | 'requestBody'; /** @@ -1104,12 +1107,11 @@ interface UnmirroredDeclared { * records them. `floatingConfig` has no `FloatingChatbotConfig` mirror at all — * minting one is objectui#6152's axis, and the `triggerIcon` tombstone's tripwire * (objectui#7654, `floating-chatbot-trigger-icon-retired.test.ts`) watches for - * it. `displayMode` is RULED RETIRED (objectui#7654, maintainer ruling B, - * 2026-09-05) and the retirement executes in that card's own PR: the TypeScript - * half is the `?: never` tombstone, and the mirror half (`retirementTombstone()`) - * is owed when objectui#6152 mints the arm — until then the key stays unmirrored - * here and on `ChatbotSchema` alike, and what that PR does to these two entries - * is its own to record. ⛔ Not a waiver: every OTHER key this pair declares is + * it. `displayMode` is RETIRED (objectui#7654, maintainer ruling B, 2026-09-05): + * a `?: never` tombstone on this face and on `ChatbotSchema` alike, still + * unmirrored on both twins — the mirror half (`retirementTombstone()`) is owed + * when objectui#6152 mints the arm, and until then the key stays in both entries; + * that PR moved neither. ⛔ Not a waiver: every OTHER key this pair declares is * mirrored, and a third key here reddens the pair like growth on any other entry. */ 'complex.zod.ts#ChatbotFloatingSchema': 'displayMode' | 'floatingConfig'; diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index fd59df102..09dd72cdd 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -876,14 +876,83 @@ export interface ChatbotSchema extends BaseSchema { */ onSend?: (content: string, messages: ChatMessage[]) => void; - // --- Floating / FAB display mode --- + // --- Floating / FAB configuration --- /** - * Display mode for the chatbot. - * - `'inline'` (default): Embedded in the page flow. - * - `'floating'`: Rendered as a floating action button (FAB) that opens a panel overlay. + * ADR-0049 RETIREMENT TOMBSTONE — `displayMode` (objectui#7654, maintainer + * ruling B, 2026-09-05). Write the node `type` instead: `'chatbot-floating'` + * for the trigger-and-panel presentation, `'chatbot'` / `'chatbot-enhanced'` + * for an inline one. The node's `type` is the one selector of presentation; + * this key was a second spelling of that choice that no renderer has ever + * read. + * + * What was measured, on the retiring PR's base: `displayMode` was declared + * here and on {@link ChatbotFloatingSchema}, offered as a "Display Mode" + * control in the `chatbot-floating` registration's `inputs`, and seeded as + * `'floating'` by that registration's `defaultProps` — and read by nothing. + * `chatbot-floating` renders `` unconditionally and + * `chatbot` never looked at the key, so `'floating'` on a `chatbot` node + * produced no trigger and `'inline'` on a `chatbot-floating` node changed + * nothing. A whole-repo `git grep` census over tracked files, build output + * excluded, returned those sites, the doc comments and ledger entries beside + * them, one historical CHANGELOG line and two unrelated `displayMode` props + * (`GridField`, `MasterDetailForm`); the same pass over `floatingConfig`, a + * key that IS read, returned 79 lines, so the instrument was not blind. The + * control and the seed are removed in the same change; the restatement of + * that control is this tombstone plus the release note (objectui#7070: a + * control is restated, never deleted into a vacuum). + * + * ## Why a tombstone — discriminator prong 2 — and why it is loud-vs-silent here + * + * A `?: never` tombstone is available only on a surviving carrier + * (`ChatbotSchema` survives) and is used when either prong of this package's + * discriminator holds: it steers authors to a named live replacement KEY, + * or it keeps loud a key the docs taught as working. Prong 2 holds: the key + * was advertised in the 3.3.0 release record (`CHANGELOG.md:578`, "Extended + * `ChatbotSchema` with `displayMode` (`'inline' | 'floating'`) …") and its + * published comment told authors it selected the presentation. Prong 1 is + * not met by the letter — the replacement is the discriminant `type`, not a + * new key — which is why the guidance above names `type`. + * + * On a carrier extending {@link BaseSchema} — every component schema in this + * package — deleting an optional member is SILENT in every value shape, + * because the `[key: string]: any` index signature defeats both + * excess-property checking and the weak-type check. Measured on THIS member + * with `tsc -p tsconfig.test.json`, a no-index-signature control carrier + * (`FloatingChatbotConfig`) lit in the same run (TS2353 on a fresh undeclared + * key, TS2559 on a lone-key widened value): + * + * | route | fresh `'floating'` | fresh `'bogus'` | widened `'floating'` | + * |------------|--------------------|-----------------|----------------------| + * | declared | clean | TS2322 | clean | + * | DELETED | clean | clean | clean | + * | TOMBSTONED | TS2322 | TS2322 | TS2322 | + * + * Deleted, the member reads as `any` through the index signature and even a + * wrong-typed value goes quiet. Tombstoned, PRESENCE with any value is a + * compile error — a channel deletion cannot produce on this carrier at all: + * on a `BaseSchema` carrier the two routes are loud-vs-silent, not + * louder-vs-quieter. Pinned, the deleted row included as a live control, in + * `__tests__/chatbot-display-mode-retired.test.ts`. + * + * ## Runtime: unchanged, deliberately — zero validation before and after + * + * There is NO `retirementTombstone()` half. `displayMode` has never had a + * Zod arm: it sits in the `UnmirroredDeclared` ledger for both + * `complex.zod.ts#ChatbotSchema` and `#ChatbotFloatingSchema` + * (`__tests__/zod-mirror-parity.test.ts`), and `BaseSchema` is + * `.passthrough()`, so a stored document carrying `displayMode: 'floating'` + * — every node the designer ever created — parsed green before this change + * and parses green after it, and the value is dropped at render time as it + * always was. Minting an arm to refuse it would be the declared-but- + * unmirrored axis (objectui#6152); the retirement test pins both twins' + * shapes as a tripwire so that whoever mints the mirror adds the + * `retirementTombstone()` half at that time. + * + * @deprecated Not part of this contract — the value was inert. The node + * `type` selects the presentation. */ - displayMode?: 'inline' | 'floating'; + displayMode?: never; /** * Configuration for the floating action button and the panel it opens — @@ -1025,8 +1094,9 @@ export interface ChatbotEnhancedSchema * - `enableMarkdown`, `enableFileUpload` and the `onClear` runtime slot, * forwarded into the panel's ``; * - `floatingConfig`, the trigger and panel geometry - * ({@link FloatingChatbotConfig}), and `displayMode` — both ALSO declared - * on {@link ChatbotSchema}, unchanged there; see each member's comment. + * ({@link FloatingChatbotConfig}) — ALSO declared on {@link ChatbotSchema}, + * unchanged there — and `displayMode`, a `?: never` tombstone on both + * faces since objectui#7654; see each member's comment. * * NOT declared, on purpose: `maxHeight` (the panel pins its inner chat to * `100%` of `floatingConfig.panelHeight` AFTER any forwarded value, so an @@ -1075,22 +1145,19 @@ export interface ChatbotFloatingSchema */ onClear?: () => void; /** - * Display mode for the chatbot. - * - `'inline'` (default): Embedded in the page flow. - * - `'floating'`: Rendered as a floating action button (FAB) that opens a panel overlay. - * - * ⚠️ RULED RETIRED — objectui#7654, maintainer ruling B (2026-09-05): the - * node's own `type` is the one selector of presentation, and this key is a - * second spelling of that choice that no renderer has ever read (measured - * there and re-measured here: declared, offered as a designer control in the - * `chatbot-floating` registration's `inputs`, seeded by its `defaultProps`, - * read by nothing). The retirement — `?: never` tombstone, control and seed - * removed — executes in that card's own PR. objectui#7655 declared the key - * here with the same three lines {@link ChatbotSchema} still carries, so - * that PR finds the member on both faces exactly as ruled; nothing was - * retired, tombstoned, mirrored or made live here. - */ - displayMode?: 'inline' | 'floating'; + * ADR-0049 RETIREMENT TOMBSTONE — the same `displayMode` retirement as + * {@link ChatbotSchema.displayMode} (objectui#7654, maintainer ruling B, + * 2026-09-05); the rationale, the measurements and the runtime note live + * there, once. Declared here as well because objectui#7655 put the member on + * this face with `ChatbotSchema`'s own three lines so the retirement would + * find it on both faces — and because this is the face of the one + * registration that offered the control: a `chatbot-floating` node IS the + * floating presentation, so there is nothing left for this key to select. + * `type: 'chatbot-floating'` is the whole spelling. + * @deprecated Not part of this contract — the value was inert. The node + * `type` selects the presentation. + */ + displayMode?: never; /** * Configuration for the floating action button and the panel it opens — * read by `chatbot-floating` alone and forwarded to ``. diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index b1cd5ac00..f692b3c8c 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -493,14 +493,15 @@ export const ChatbotEnhancedSchema = BaseSchema.extend({ * minting one is the declared-but-unmirrored axis (objectui#6152), a * different defect from the one this pair closes, and the axis the * `triggerIcon` tombstone's tripwire watches (objectui#7654). - * - `displayMode` — RULED RETIRED by objectui#7654 (maintainer ruling B, + * - `displayMode` — RETIRED by objectui#7654 (maintainer ruling B, * 2026-09-05): the node `type` is the one selector of presentation. The - * retirement executes in that card's own PR — `?: never` tombstone on the - * TypeScript faces, designer control and seed removed — and, per the - * ruling, the mirror half (`retirementTombstone()`) is owed at the moment - * objectui#6152 mints an arm for it, not before. Until then a mirror arm - * here would be a parse outcome that ruling did not ask for, so this twin - * has none. + * TypeScript half landed there — `?: never` tombstone on both faces, + * designer control and seed removed — and, per the ruling, the mirror + * half (`retirementTombstone()`) is owed at the moment objectui#6152 mints + * an arm for it, not before: a mirror arm here today would be a parse + * outcome the ruling did not ask for, so this twin has none and a stored + * document carrying the key parses exactly as it did + * (`chatbot-display-mode-retired.test.ts` pins the shape as the tripwire). * * Both ride through `BaseSchema`'s `.passthrough()` unvalidated, byte for byte * as they do on `ChatbotSchema`'s twin. From 6b254df2547253970268a3a99a6533b72fbbdaea Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 15:12:03 +0000 Subject: [PATCH 2/2] docs(types): name the manifest/validator accept-set face, and correct the tripwire prose (objectui#7654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text-only land-prep on the retirement PR. No runtime, type or manifest behaviour changes: every hunk is changeset prose, a test title string, or a comment block. 1. Clause-② / changeset — the accept-set list named the TypeScript, runtime and designer faces but not the manifest one. Re-measured on both sides of this change, with the `chatbot-floating` registration restored from `origin/main` for the LIVE leg (mutation and restore proven by blob hash): `inputs` 20 -> 19, `defaultProps` 9 keys -> 8; `validateTree` on a stored node carrying `displayMode` goes 0 -> 1 diagnostic, code `unknown-prop`, severity `warning`; the props interface `generateDts` derives from the same `inputs` goes 20 -> 19 members. Author-time only: `compile()` still returns a tree carrying the value on both legs, and the page renderer filters diagnostics to `severity === 'error'` before failing a page. 2. The shape-pin comment and the 7655 TRIPWIRE comment claimed the parse-green line stays green for a non-strict mirror arm, citing objectui#7678 item 2. That transposition is false: item 2 measured object-mirror shapes, and `displayMode` is a scalar union. Minting the arm a scalar union takes — an optional `z.enum` over 'inline' | 'floating' — on `ChatbotSchema`'s twin reddens BOTH: `tsc` with exactly one error at zod-mirror-parity.test.ts(1586,14) and vitest with exactly two failures, the shape pin and the parse-green line. The prose now says what was measured. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .../7654-chatbot-display-mode-tombstone.md | 32 ++++++++++++++++--- .../chatbot-display-mode-retired.test.ts | 26 ++++++++++----- ...-registration-authoring-faces-7655.test.ts | 17 +++++++--- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/.changeset/7654-chatbot-display-mode-tombstone.md b/.changeset/7654-chatbot-display-mode-tombstone.md index 9bc8fcba5..5c8f73d54 100644 --- a/.changeset/7654-chatbot-display-mode-tombstone.md +++ b/.changeset/7654-chatbot-display-mode-tombstone.md @@ -93,7 +93,31 @@ Prong 2 of that discriminator licenses the tombstone: the key was advertised in presentation switch. The deleted row is pinned in the test file as a live control — an undeclared key that rides both shapes with no directive — so the contrast cannot rot. -**Accept-set change, stated plainly for reviewers:** on the TypeScript face, a write of -`displayMode` against either chatbot face used to compile and now does not. On the -runtime face nothing changes at all. On the designer face, one control disappears from -the `chatbot-floating` property panel and new nodes no longer carry the key. +## Accept-set change, one line per face + +- **TypeScript.** A write of `displayMode` against either chatbot face used to compile + and now does not. +- **Runtime (Zod / `safeValidateSchema`).** Nothing changes at all — a stored document + carrying the key parses green before and after, and keeps the value. +- **Designer.** The **Display Mode** control disappears from the `chatbot-floating` + property panel, and newly created nodes no longer carry the key. +- **Manifest, author-time validator, and generated JSX props.** The `chatbot-floating` + registration's `inputs` go from 20 entries to 19 and its `defaultProps` from 9 keys to + 8, so the manifest projected from them no longer lists the prop. Measured on both sides + of this change: `validateTree` on a stored `chatbot-floating` node carrying + `displayMode` goes from **0 diagnostics to exactly 1** — code `unknown-prop`, severity + **`warning`**, message `` ` has no prop "displayMode"` `` — which is + what the JSX/HTML authoring tier reports through `compile()`. In the same pair of runs + the props interface `generateDts` derives from those same `inputs` drops from 20 members + to 19, losing its `displayMode?: string` line, so a `.tsx` page written against those + generated intrinsics no longer type-checks the attribute. + + **This is author-time only: no stored document stops parsing and nothing at render + moves.** The value survives compilation — `compile()` returns a tree still carrying + `displayMode: 'floating'`, byte-for-byte the same keys before and after — and a + `warning` never blocks a page, because the page renderer filters the diagnostics to + `severity === 'error'` before deciding whether to fail. Two neighbouring instruments are + untouched and worth naming so the scope is not read wider than it is: `os validate` runs + `safeValidateSchema`, the Zod path, and is silent on this key before and after; and the + build-time `sdui-intrinsics.d.ts` artifact is generated from the PUBLIC tier, which does + not contain `chatbot-floating` on either side of this change. diff --git a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts index 97d76b996..a237f2597 100644 --- a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts +++ b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts @@ -199,14 +199,24 @@ describe.each([ expect(twin.safeParse({ ...node, displayMode: 'anything-at-all' }).success).toBe(true); }); - it('the twin really has no `displayMode` arm — the SHAPE pin, which is the tripwire that fires', () => { - // ⚠️ TRIPWIRE: if objectui#6152 ever mints a house-style (non-strict) arm - // for `displayMode`, THIS assertion goes red — the parse-green line above - // stays green for a non-strict arm and only reddens for a `.strict()` - // mirror (objectui#7678 item 2 measured both shapes). Red here is the - // intended signal, not a nuisance: whoever lands the mirror adds the - // `retirementTombstone()` half for `displayMode` at the same time, and - // flips this control rather than deleting it into a vacuum. + it('the twin really has no `displayMode` arm — the SHAPE pin, one of the TWO assertions a minted arm reddens', () => { + // ⚠️ TRIPWIRE: if objectui#6152 ever mints an arm for `displayMode`, this + // assertion goes red — and so does the parse-green line above. That was + // MEASURED on this pair, not transposed from another one. Minting the arm a + // scalar union takes on `ChatbotSchema`'s twin — an optional `z.enum` over + // `'inline' | 'floating'` — reddened BOTH instruments in the same tree: + // `tsc -p packages/types/tsconfig.test.json` with exactly one error, at + // `zod-mirror-parity.test.ts(1586,14)`, and `vitest` with exactly two + // failures — this shape pin AND the parse-green line, which asserts that + // `'anything-at-all'` parses, and an enum arm refuses it. So it does NOT + // take a `.strict()` mirror to redden the parse line here: the tripwire + // fires harder than "shape pin only". (The strict-vs-non-strict split is + // real for OBJECT-mirror shapes, which is what objectui#7678 item 2 + // measured; `displayMode` is a scalar union and that reading does not + // transpose onto it.) Red is the intended signal, not a nuisance: whoever + // lands the mirror adds the `retirementTombstone()` half for `displayMode` + // at the same time, and flips this control rather than deleting it into a + // vacuum. const shape = (twin as unknown as { shape: Record }).shape; expect(shape.displayMode).toBeUndefined(); // Lit control: a key the twin DOES declare is in its shape, so the reading diff --git a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts index f1ff48475..9b5d684c7 100644 --- a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts +++ b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts @@ -397,11 +397,18 @@ describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and le // removed. The RUNTIME face was deliberately left alone — the key has no // mirror arm on either twin and `BaseSchema` is `.passthrough()`, so a // stored document carrying it parses exactly as it did before the ruling. - // The SHAPE pin is the assertion that fires if objectui#6152 mints a - // house-style (non-strict) arm for the key; the parse-green line after it - // only fires for a `.strict()` mirror (objectui#7678 item 2). Red here is - // the signal to add the `retirementTombstone()` half at the same time, not - // to delete the pin. The tombstone's own pins are in + // BOTH assertions below are tripwires, not just the shape pin. Measured on + // `ChatbotSchema`'s twin, which carries the key on the same terms this one + // does: minting the arm a scalar union takes — an optional `z.enum` over + // `'inline' | 'floating'` — reddened `tsc -p` on this package's + // `tsconfig.test.json` with exactly one error, at + // `zod-mirror-parity.test.ts(1586,14)`, AND `vitest` with exactly two + // failures: the shape pin and the parse-green line, since an enum arm + // refuses `'anything-at-all'`. It does NOT take a `.strict()` mirror. (The + // strict-vs-non-strict split objectui#7678 item 2 records was measured on + // OBJECT-mirror shapes; transposing it onto this scalar union is wrong.) + // Red here is the signal to add the `retirementTombstone()` half at the + // same time, not to delete the pin. The tombstone's own pins are in // `chatbot-display-mode-retired.test.ts`. expect((ChatbotFloatingZod.shape as Record).displayMode).toBeUndefined(); expect(ChatbotFloatingZod.safeParse({ ...node, displayMode: 'anything-at-all' }).success).toBe(true);