diff --git a/.changeset/6152-unmirrored-round4.md b/.changeset/6152-unmirrored-round4.md index 1211f1daf7..fa4646da5b 100644 --- a/.changeset/6152-unmirrored-round4.md +++ b/.changeset/6152-unmirrored-round4.md @@ -48,3 +48,5 @@ minor bump, per the repository's version policy. `displayMode` on the two chatbot types is unchanged: its refusal stays TypeScript-only, so stored designer documents that carry `displayMode: 'floating'` parse exactly as before. + +**Correction, 2026-10-01 (objectui#6152, round 5).** The "Mirrored" section above says that on a `chatbot` node `floatingConfig` stays unvalidated. That was true when this change was written, and it no longer is: objectui#6152 round 5 retired `floatingConfig` on the `chatbot` type, so writing it on a `chatbot` node is now a `tsc` error and a parse error that names `chatbot-floating`. The `chatbot-floating` node keeps its `floatingConfig`, judged member by member, as this entry says. diff --git a/.changeset/6152-unmirrored-round5.md b/.changeset/6152-unmirrored-round5.md new file mode 100644 index 0000000000..c3225e9c63 --- /dev/null +++ b/.changeset/6152-unmirrored-round5.md @@ -0,0 +1,42 @@ +--- +'@object-ui/types': minor +'@object-ui/components': minor +--- + +feat(types)!: retire `data-table`'s `selectionStyle` and `chatbot`'s `floatingConfig` on both faces, and stop teaching `data-table`'s inline-edit flags as authored keys (objectui#6152, round 5) + +**Retired (breaking).** Each key below was declared on a published TypeScript type in +`@object-ui/types` and unknown to its zod mirror in `@object-ui/types/zod`, so the tolerant +validator kept an authored value without examining it. Each is retired at once, with no alias +window: + +- `data-table`: `selectionStyle` (`'always' | 'hover'`). The `data-table` renderer in + `@object-ui/components` honoured `'hover'` by hiding each row's selection checkbox until the + row was hovered, but nothing in this repository or in `objectstack` authored or produced the + key. The hover-only branch is removed: a selectable table always shows its row checkboxes, + which is what `'always'` and an unset key already did. Delete the key; `selectable` alone + turns selection on. +- `chatbot`: `floatingConfig`. The `chatbot` renderer never read it, so a value on a `chatbot` + node configured nothing. The trigger and panel it describes belong to the floating + presentation: author `type: 'chatbot-floating'` with `floatingConfig`. That node's + `floatingConfig` is unchanged on both faces, and it is still validated member by member. + +For each retired key: + +- the TypeScript member is now `?: never`, so writing it is a `tsc` error; +- the zod mirror refuses it by name at the key, on the tolerant validator (`AnyComponentSchema`, + `safeValidateSchema`) and on the strict authoring face (`StrictAnyComponentSchema`) alike. The + strict face used to refuse it as an unknown key with no guidance; the refusal now says what to + write instead. + +Delete the key from any document or literal that carries it. + +**Docs: `data-table`'s inline-edit flags are host-paired.** `editable` and `singleClickEdit` +stay declared on `DataTableSchema`, unchanged, but the `data-table` page no longer teaches them +as keys a document sets. The table only stages an edit, and saving it needs callbacks a host +supplies in code. `object-grid` sets both keys on the table it builds and supplies that save +path; a document that wants inline editing authors an `object-grid`. Their doc comments say +the same. + +`@object-ui/types` and `@object-ui/components` are in the fixed release group, so this ships as a +minor bump, per the repository's version policy. diff --git a/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md b/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md index 61c04d32b9..e32df6dc52 100644 --- a/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md +++ b/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md @@ -70,3 +70,5 @@ mirror must add the `retirementTombstone()` half at the same time and flip the c rather than delete it into a vacuum. **Correction, 2026-09-30 (objectui#6152, round 4).** The sections above say `FloatingChatbotConfig` has no zod mirror, so the `triggerIcon` refusal is type-level only and the runtime face does not change. That was true when this change was written, and it is now true on a `chatbot` node only. objectui#6152 round 4 minted the mirror on the `chatbot-floating` node, the one whose renderer reads `floatingConfig`, together with the `retirementTombstone()` half this entry asked for. So a `chatbot-floating` node that carries `floatingConfig.triggerIcon` is now refused at that path. The tripwire test was flipped for that node, not deleted. + +**Correction, 2026-10-01 (objectui#6152, round 5).** The correction above says the `triggerIcon` refusal is still type-level only on a `chatbot` node. That no longer holds: objectui#6152 round 5 retired `floatingConfig` itself on the `chatbot` type, on both faces, so a `chatbot` node refuses the whole key, `triggerIcon` with it, at compile time and at parse time. The only node that still takes `floatingConfig` is `chatbot-floating`, where `triggerIcon` is refused on both faces as described above. diff --git a/.changeset/7655-chatbot-registration-authoring-faces.md b/.changeset/7655-chatbot-registration-authoring-faces.md index 476320e685..4d8d89a879 100644 --- a/.changeset/7655-chatbot-registration-authoring-faces.md +++ b/.changeset/7655-chatbot-registration-authoring-faces.md @@ -118,3 +118,5 @@ the zod twins. The "twenty keys" above is kept as the reading of this change; th objectui#5605 retirement entry states what the three faces declare now. **Correction, 2026-09-30 (objectui#6152, round 4).** The "Zod twins" section above says the floating twin leaves `floatingConfig` unmirrored because no `FloatingChatbotConfig` mirror exists. That was true when this change was written, and it no longer is: objectui#6152 round 4 minted that mirror and declared `floatingConfig` on the `chatbot-floating` twin, judged member by member. The same round declared `requestBody` on `ChatbotSchema`'s twin, so all three chatbot twins now share one `requestBody` arm. `displayMode` stays unmirrored on both twins, as this entry says. + +**Correction, 2026-10-01 (objectui#6152, round 5).** The section above says `ChatbotSchema` keeps `floatingConfig` as a typed member, so the `triggerIcon` tombstone reaches `chatbot` nodes. That is no longer true: objectui#6152 round 5 retired `ChatbotSchema.floatingConfig` on both faces, because the `chatbot` registration never read it. The member is a `?: never` tombstone on the TypeScript face and a named refusal on the zod twin, so a `chatbot` node refuses the whole key. `ChatbotFloatingSchema.floatingConfig` is unchanged; it never inherited the base member. diff --git a/content/docs/components/complex/data-table.mdx b/content/docs/components/complex/data-table.mdx index 05c3ca6f37..b3f8df8243 100644 --- a/content/docs/components/complex/data-table.mdx +++ b/content/docs/components/complex/data-table.mdx @@ -45,19 +45,6 @@ interface DataTableSchema { resizableColumns?: boolean; // Allow column resizing (default: true) reorderableColumns?: boolean; // Allow column reordering (default: true) - // Inline editing - editable?: boolean; // Enable inline cell editing (default: false) - singleClickEdit?: boolean; // Enter edit mode on single click (default: false) - renderCellEditor?: (ctx: { // Host-supplied editor widget; null -> built-in input - column: any; - row: any; // the persisted record - pendingRow: any; // row + this row's staged, unsaved edits (#7188) - value: any; - stage: (v: any) => void; - commit: (v?: any) => void; - cancel: () => void; - }) => ReactNode; - // Styling className?: string; // Tailwind CSS classes on the table wrapper cellClassName?: string; // Tailwind CSS classes on the utility cells only @@ -111,15 +98,41 @@ Drop the per-column half and the data cells stay at the table primitive's defaul ## Inline editing -With `editable: true` a cell enters edit mode on double-click (or on single click -with `singleClickEdit: true`) and the table renders one of its built-in editors — -text, number, date — chosen from the column's `type`. +Inline editing is a **host-paired** capability of this table, not a flag a document +sets. The table only *stages* an edit; saving it is the job of callbacks that a host +supplies in code (`onRowSave`, `onCellChange`, `onBatchSave`), and a JSON document +cannot supply a function. So the keys that switch editing on — `editable`, and +`singleClickEdit` for entering edit mode on the first click instead of a +double-click — are set by the host on the `data-table` node it builds, together with +that save path. `object-grid` is that host: it sets both keys on its table and +passes its own save path, which writes through the data source. + +**Do not author `editable` or `singleClickEdit` on a `data-table` node.** Without a +host save path the edits are staged in the table and nothing persists them. To edit +records inline from a document, author an `object-grid` instead — see +[Inline Editing](/docs/plugins/plugin-grid#inline-editing) in the grid plugin. + +When a host has switched editing on, a cell enters edit mode on double-click (or on +a single click) and the table renders one of its built-in editors — text, number, +date — chosen from the column's `type`. + +The host can also supply `renderCellEditor`, a function the table calls first for +every cell it is about to edit; it returns a node to use, or `null` to fall through +to the built-in editor for that column. This is how `object-grid` gives a `select` +or `lookup` cell the same dedicated control the form uses, without the component +layer having to re-implement it. Its context is: -`renderCellEditor` lets the host supply a widget instead. The table calls it first -for every cell it is about to edit; return a node to use it, or `null` to fall -through to the built-in editor for that column. This is how `object-grid` gives a -`select` or `lookup` cell the same dedicated control the form uses, without the -component layer having to re-implement it. +```plaintext +renderCellEditor?: (ctx: { + column: any; + row: any; // the persisted record + pendingRow: any; // row + this row's staged, unsaved edits (#7188) + value: any; + stage: (v: any) => void; + commit: (v?: any) => void; + cancel: () => void; +}) => ReactNode; +``` The returned node is wrapped by the table so it inherits the exit-edit affordances the built-in editors have: Enter commits from a single-line input, diff --git a/content/docs/plugins/plugin-chatbot.mdx b/content/docs/plugins/plugin-chatbot.mdx index f909130812..c4bcfaf140 100644 --- a/content/docs/plugins/plugin-chatbot.mdx +++ b/content/docs/plugins/plugin-chatbot.mdx @@ -230,10 +230,12 @@ 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. `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. +is declared on `ChatbotFloatingSchema` only. It used to be declared on +`ChatbotSchema` as well, so authoring it on a `chatbot` node type-checked and +parsed, and was then dropped at render time, because the `chatbot` node never +read it. objectui#6152 retired it there: writing `floatingConfig` on a `chatbot` +node is now a compile error **and** a parse error that names `chatbot-floating`, +the node type that reads it. `ChatbotEnhancedSchema` never declared 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 diff --git a/packages/components/src/renderers/complex/data-table.tsx b/packages/components/src/renderers/complex/data-table.tsx index 7792eb51bf..187904e0cf 100644 --- a/packages/components/src/renderers/complex/data-table.tsx +++ b/packages/components/src/renderers/complex/data-table.tsx @@ -756,7 +756,6 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { reorderableColumns = true, editable = false, singleClickEdit = false, - selectionStyle = 'always', rowClassName, rowStyle, className, @@ -2405,20 +2404,13 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { }} > {selectable && ( - 0 && "sticky left-0 z-10 bg-background", selectionStyle === 'hover' && "relative")}> - {selectionStyle === 'hover' ? ( -
- handleSelectRow(rowId, checked as boolean)} - /> -
- ) : ( - handleSelectRow(rowId, checked as boolean)} - /> - )} + 0 && "sticky left-0 z-10 bg-background")}> + {/* Always visible: the hover-only `selectionStyle` was retired + (objectui#6152 round 5) — nothing authored or produced it. */} + handleSelectRow(rowId, checked as boolean)} + /> )} {showRowNumbers && ( 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 31d73aaaae..72a4da6280 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 @@ -40,8 +40,11 @@ * * ## `displayMode` and `floatingConfig` live on BOTH faces; `displayMode` is a tombstone on both * - * `ChatbotSchema` keeps `floatingConfig` exactly as it had it, and - * `ChatbotFloatingSchema` declares the same member. `displayMode` was carried + * `ChatbotSchema` kept `floatingConfig` exactly as it had it, and + * `ChatbotFloatingSchema` declares the same member. (objectui#6152 round 5 has + * since RETIRED `ChatbotSchema.floatingConfig` on both faces — the `chatbot` + * registration never read it — so the member is live on the floating face only; + * the pins below read that.) `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 @@ -163,9 +166,10 @@ export type assertionFloatingDeclaresWhatItReads = Expect< /** * `chatbot` keeps its WHOLE face — the six legacy keys (`loading` … `height`), * 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. + * `floatingConfig` (a tombstone too since objectui#6152 round 5) — 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< @@ -177,19 +181,20 @@ export type assertionChatbotKeepsItsWholeFace = Expect< >; /** - * The two floating keys stay TYPED on `ChatbotSchema`. Read off the member, - * not the key set: a member that fell off the declaration would not go missing - * 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`. `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. + * The two floating keys stay DECLARED on `ChatbotSchema` — as tombstones now. Read + * off the member, not the key set: a member that fell off the declaration would not + * go missing here — it would read as `any` through `BaseSchema`'s index signature, + * any value would compile on a `chatbot` node (measured on #7655's first cut, which + * moved the keys off this face by DELETION). `Equal` is what catches the `any`. + * `displayMode` reads as `undefined` since objectui#7654 tombstoned it, and + * `floatingConfig` since objectui#6152 round 5 RETIRED it on this face (the `chatbot` + * registration never read 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>, ]; /* ── One declaration per shared key: a pick, not a copy ──────────────────── */ @@ -216,11 +221,15 @@ export type assertionMaxToolRoundtripsIsATombstoneOnEveryFace = [ Expect>, ]; -/* ── `displayMode` / `floatingConfig`: one type, both faces ─────────────── */ +/* ── `displayMode`: one type on both faces; `floatingConfig`: live on the floating face only ── */ +/** + * `floatingConfig` had one type on both faces until objectui#6152 round 5 retired the + * `chatbot` face's copy; the floating face never inherited it (its `Pick` leaves it + * out) and keeps its own declaration. The second row is the half that must NOT move. + */ export type assertionFloatingKeysHaveOneTypeOnBothFaces = [ Expect>, - Expect>, // Both faces carry the objectui#7654 tombstone, so both read `undefined`. Expect>, Expect>, @@ -448,13 +457,19 @@ describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and le }); it('`floatingConfig` is mirrored here since objectui#6152 round 4, and a wrong member value is refused at its path', () => { - // This registration is the one that reads the key; `ChatbotSchema`'s twin, - // whose registration never does, still has no arm for it. + // This registration is the one that reads the key. `ChatbotSchema`'s twin, + // whose registration never does, had no arm for it until objectui#6152 round 5 + // RETIRED it there — a tombstone that refuses ANY value, the valid one this + // twin accepts included. Pinned on both twins so neither half can move alone. expect((ChatbotFloatingZod.shape as Record).floatingConfig).toBeDefined(); const wrong = ChatbotFloatingZod.safeParse({ ...node, floatingConfig: { panelHeight: '520px' } }); expect(wrong.success).toBe(false); expect(wrong.error?.issues.some((i) => i.path.join('.') === 'floatingConfig.panelHeight')).toBe(true); - expect((ChatbotZod.shape as Record).floatingConfig).toBeUndefined(); + const valid = { panelHeight: 520, title: 'Support' }; + expect(ChatbotFloatingZod.safeParse({ ...node, floatingConfig: valid }).success).toBe(true); + const onChatbot = ChatbotZod.safeParse({ ...node, type: 'chatbot', floatingConfig: valid }); + expect(onChatbot.success).toBe(false); + expect(onChatbot.error?.issues.map((i) => [i.path.join('.'), i.code])).toEqual([['floatingConfig', 'invalid_type']]); }); it('`onClear` / `onError` / `onSend` are refused by name here too', () => { diff --git a/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts b/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts index 37aec0d8a6..d26f889a29 100644 --- a/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts +++ b/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts @@ -56,6 +56,15 @@ * the `retirementTombstone()` half this file's tripwire asked for, so a * `chatbot-floating` node refuses `triggerIcon` at runtime as well. The runtime * section below pins the two twins apart. + * + * ⚠️ AMENDED again by objectui#6152 round 5, which RETIRED + * `ChatbotSchema.floatingConfig` on both faces (zero reads on a `chatbot` node). + * The `chatbot` half of this file was the TRIPWIRE for that member's absence from + * the mirror; it is flipped, not deleted: a `chatbot` node now refuses the WHOLE + * `floatingConfig` key, a live config included, at compile time and at parse + * time. "Type-level only" therefore holds on no node any more — the one face that + * still declares `FloatingChatbotConfig` is `chatbot-floating`, where both halves + * of the `triggerIcon` refusal hold. */ import { describe, it, expect } from 'vitest'; @@ -100,28 +109,38 @@ describe('the `triggerIcon` tombstone makes authoring a `tsc` error', () => { expect(config.triggerSize).toBe(56); }); - it('reaches a `chatbot` node AND a `chatbot-floating` node through their faces — the tombstone bites wherever `floatingConfig` is declared', () => { - // Both faces declare `floatingConfig` (`ChatbotSchema` always did; - // objectui#7655 declared it on `ChatbotFloatingSchema` too). The pins above - // sit on `FloatingChatbotConfig` directly, so a face that LOST the member - // would not turn them red — the member would read as `any` through - // `BaseSchema`'s index signature and this literal would compile clean. That - // is exactly what #7655's contract review measured on its first cut, which - // moved the key off `ChatbotSchema`; pinned on the nodes since. - const onChatbot: TsChatbotSchema = { - type: 'chatbot', - messages: [], - // @ts-expect-error `triggerIcon` is a retirement tombstone (objectui#7654), reached through `ChatbotSchema.floatingConfig` - floatingConfig: { title: 'Chat', triggerIcon: 'Sparkles' }, - }; + it('reaches a `chatbot-floating` node through its face; on a `chatbot` node the WHOLE `floatingConfig` key is refused since objectui#6152 round 5', () => { + // The pins above sit on `FloatingChatbotConfig` directly, so a face that LOST + // the member would not turn them red — the member would read as `any` through + // `BaseSchema`'s index signature and the literal would compile clean. That is + // what #7655's contract review measured on its first cut, which DELETED the key + // off `ChatbotSchema`; pinned on the nodes since. const onFloating: TsChatbotFloatingSchema = { type: 'chatbot-floating', messages: [], // @ts-expect-error `triggerIcon` is a retirement tombstone (objectui#7654), reached through `ChatbotFloatingSchema.floatingConfig` floatingConfig: { title: 'Chat', triggerIcon: 'Sparkles' }, }; + // Lit control: a LIVE config type-checks on the floating face, so the + // directive above is about `triggerIcon` and not about the key. + const liveOnFloating: TsChatbotFloatingSchema = { + type: 'chatbot-floating', + messages: [], + floatingConfig: { title: 'Chat', triggerSize: 56 }, + }; + // The flipped tripwire, TS half: `ChatbotSchema.floatingConfig` is a `?: never` + // tombstone (objectui#6152 round 5), so even the LIVE config the line above + // accepts is refused on a `chatbot` node — the tombstone was not deleted into + // the index signature, where this literal would compile clean. + const onChatbot: TsChatbotSchema = { + type: 'chatbot', + messages: [], + // @ts-expect-error `floatingConfig` is RETIRED on `chatbot` (objectui#6152 round 5) — author `type: 'chatbot-floating'` + floatingConfig: { title: 'Chat', triggerSize: 56 }, + }; expect(onChatbot.type).toBe('chatbot'); expect(onFloating.type).toBe('chatbot-floating'); + expect(liveOnFloating.floatingConfig?.triggerSize).toBe(56); }); it('a key the interface never declared still rides the widened path — the DELETED row', () => { @@ -135,51 +154,58 @@ describe('the `triggerIcon` tombstone makes authoring a `tsc` error', () => { }); }); -/* ── the runtime channel: unchanged on `chatbot`, refused on `chatbot-floating` since objectui#6152 round 4 ─ */ +/* ── the runtime channel: refused on BOTH nodes — `triggerIcon` on `chatbot-floating`, the whole key on `chatbot` ─ */ -// Both faces declare `floatingConfig` — `ChatbotSchema` always did, and +// Both faces declared `floatingConfig` — `ChatbotSchema` always had, and // objectui#7655 declared it on `ChatbotFloatingSchema`, the face of the one // registration that reads it. Until objectui#6152 round 4 NEITHER twin had an arm // for it, and this file's runtime half pinned that on both, as a TRIPWIRE: whoever // minted a `FloatingChatbotConfig` mirror had to add the `retirementTombstone()` // half for `triggerIcon` in the same change, and flip these controls rather than // delete them into a vacuum. Round 4 minted it on the `chatbot-floating` twin only -// (that registration reads the key; `chatbot`'s never does), so the two twins are -// now pinned apart. -describe('there is NO zod refusal on a `chatbot` node, and that is deliberate (objectui#7654)', () => { +// (that registration reads the key; `chatbot`'s never does), and round 5 RETIRED +// the key on the `chatbot` twin. Both flips are below; neither twin's control was +// deleted. +describe('a `chatbot` node REFUSES the whole `floatingConfig` key at runtime (objectui#6152 round 5 retired it there)', () => { const node = { type: 'chatbot', messages: [{ id: 'm1', role: 'user' as const, content: 'hi' }], }; - it('a chatbot node carrying `floatingConfig.triggerIcon` still parses GREEN', () => { - // `ChatbotSchema.floatingConfig` stays in the `UnmirroredDeclared` ledger - // (`zod-mirror-parity.test.ts`): the `chatbot` registration never reads it, so - // objectui#6152 round 4 did not mirror it. `BaseSchema` is `.passthrough()`, - // so the whole object rides through unvalidated on this twin. + it('a chatbot node carrying `floatingConfig.triggerIcon` is refused at `floatingConfig` itself', () => { + // The flipped tripwire: this line asserted `success: true` until round 5, + // when `ChatbotSchema.floatingConfig` rode through `.passthrough()` unvalidated. const result = ChatbotSchema.safeParse({ ...node, floatingConfig: { title: 'Chat', triggerIcon: 'Sparkles' }, }); - expect(result.success).toBe(true); + expect(result.success).toBe(false); + const issues = (result.error?.issues ?? []).map((i) => [i.path.join('.'), i.code]); + expect(issues, JSON.stringify(result.error?.issues)).toEqual([['floatingConfig', 'invalid_type']]); }); - it('a live `floatingConfig` parses green too — the non-vacuity control', () => { + it('a LIVE `floatingConfig` is refused too — the tombstone refuses the key, not a member — and the refusal names the node that reads it', () => { + // The flipped non-vacuity control: until round 5 this parsed green. const result = ChatbotSchema.safeParse({ ...node, floatingConfig: { title: 'Chat', triggerSize: 56 }, }); - expect(result.success).toBe(true); + expect(result.success).toBe(false); + const issues = (result.error?.issues ?? []).filter((i) => i.path.join('.') === 'floatingConfig'); + expect(issues.length, JSON.stringify(result.error?.issues)).toBe(1); + expect(issues[0].message).toContain('chatbot-floating'); }); - it('the mirror really has no `floatingConfig` key at all', () => { - // The load-bearing fact behind the two lines above, asserted rather than - // assumed: a key the mirror declares would appear in its shape. This is the - // assertion the TRIPWIRE fires through if this twin ever gains the arm. + it('a chatbot node WITHOUT `floatingConfig` still parses green — the control', () => { + expect(ChatbotSchema.safeParse(node).success).toBe(true); + }); + + it('the mirror declares `floatingConfig` as a MEMBER tombstone, not an undeclared key', () => { + // The load-bearing fact behind the refusals above, asserted rather than + // assumed. Until round 5 this read `undefined`: the TRIPWIRE fired here. const shape = (ChatbotSchema as unknown as { shape: Record }).shape; - expect(shape.floatingConfig).toBeUndefined(); - // Lit control: a key the mirror DOES declare is present, so the reading - // above is a measurement and not an empty object. + expect(shape.floatingConfig).toBeDefined(); + // Lit control: a key the mirror declares as a live arm sits beside it. expect(shape.messages).toBeDefined(); }); }); diff --git a/packages/types/src/__tests__/unmirrored-declared-round5-6152.test.ts b/packages/types/src/__tests__/unmirrored-declared-round5-6152.test.ts new file mode 100644 index 0000000000..2d073a512f --- /dev/null +++ b/packages/types/src/__tests__/unmirrored-declared-round5-6152.test.ts @@ -0,0 +1,140 @@ +/** + * 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. + * + * objectui#6152 round 5 — the two declared-but-unmirrored keys this round closed by + * RETIREMENT. (The two it filed as runtime slots by name, `data-table`'s `editable` + * and `singleClickEdit`, moved between two test ledgers only; their declarations did + * not move, which the last block pins, and `zod-mirror-parity.test.ts` reconciles them.) + * + * ## Retired on both faces + * + * - `data-table.selectionStyle` — read by `data-table` (a hover-only checkbox style), + * but authored and produced nowhere, so the hover branch is dropped and selection + * checkboxes are always visible; + * - `chatbot.floatingConfig` — ZERO reads on a `chatbot` node (only the + * `chatbot-floating` registration reads it). `ChatbotFloatingSchema` never inherited + * the base member and keeps its own declaration, on both faces. + * + * Each is `?: never` on the interface and a `retirementTombstone()` on the mirror, so an + * authored value is refused BY NAME at the key on every face instead of being kept + * unexamined by `.passthrough()` (or, on the strict face, refused as an unknown key with + * no guidance). + */ +import { describe, it, expect } from 'vitest'; + +import type { DataTableSchema as TsDataTableSchema } from '../data-display'; +import type { ChatbotSchema as TsChatbotSchema, ChatbotFloatingSchema as TsChatbotFloatingSchema } from '../complex'; +import { DataTableSchema as DataTableMirror } from '../zod/data-display.zod.js'; +import { ChatbotSchema as ChatbotMirror } from '../zod/complex.zod.js'; +import { AnyComponentSchema, StrictAnyComponentSchema } from '../zod/index.zod.js'; + +type Issue = { code: string; path: PropertyKey[]; message: string }; +type Parse = (v: unknown) => { success: boolean; error?: { issues: Issue[] } }; +type Mirror = { shape: Record; safeParse: Parse }; + +type Retired = { + name: string; + key: string; + value: unknown; + base: Record; + mirror: Mirror; + /** A word the refusal must carry: the spelling or node to write instead. */ + instead: string; +}; + +const messages = [{ id: 'm1', role: 'user', content: 'hi' }]; + +const RETIRED: Retired[] = [ + { + name: 'data-table', + key: 'selectionStyle', + value: 'hover', + base: { type: 'data-table', selectable: true, columns: [{ header: 'Name', accessorKey: 'name' }], data: [{ name: 'Ada' }] }, + mirror: DataTableMirror as unknown as Mirror, + instead: '`selectable`', + }, + { + name: 'chatbot', + key: 'floatingConfig', + value: { position: 'bottom-left', title: 'Support', triggerSize: 48 }, + base: { type: 'chatbot', messages }, + mirror: ChatbotMirror as unknown as Mirror, + instead: 'chatbot-floating', + }, +]; + +const FACES: ReadonlyArray Parse]> = [ + ['the mirror', (row) => (v) => row.mirror.safeParse(v)], + ['the tolerant face', () => (v) => AnyComponentSchema.safeParse(v) as ReturnType], + ['the strict face', () => (v) => StrictAnyComponentSchema.safeParse(v) as ReturnType], +]; + +const RETIRED_ON_FACES = RETIRED.flatMap((row) => FACES.map(([face, parse]) => ({ ...row, face, parse: parse(row) }))); + +describe('objectui#6152 round 5 — keys RETIRED on both faces', () => { + it.each(RETIRED)('CONTROL: the minimal `$name` document parses on both faces', ({ base }) => { + const tolerant = AnyComponentSchema.safeParse(base); + expect(tolerant.success, JSON.stringify(tolerant.error?.issues)).toBe(true); + const strict = StrictAnyComponentSchema.safeParse(base); + expect(strict.success, JSON.stringify(strict.error?.issues)).toBe(true); + }); + + it.each(RETIRED)('`$name`.`$key` stays a MEMBER of the mirror, so an authored value is refused by name rather than kept', ({ key, mirror }) => { + expect(key in mirror.shape).toBe(true); + }); + + it.each(RETIRED_ON_FACES)('an authored `$name`.`$key` is refused at the key on $face', ({ key, value, base, parse }) => { + const parsed = parse({ ...base, [key]: value }); + expect(parsed.success).toBe(false); + const atKey = (parsed.error?.issues ?? []).filter((issue) => issue.path[0] === key); + expect(atKey.length, JSON.stringify(parsed.error?.issues)).toBeGreaterThan(0); + }); + + it.each(RETIRED)('the `$name`.`$key` refusal is the tombstone kind (`invalid_type`) and names what to write instead', ({ key, value, base, mirror, instead }) => { + const parsed = mirror.safeParse({ ...base, [key]: value }); + const atKey = (parsed.error?.issues ?? []).filter((issue) => issue.path[0] === key); + expect(atKey.map((issue) => issue.code)).toEqual(['invalid_type']); + expect(atKey[0]?.message, JSON.stringify(atKey)).toContain(instead); + }); + + it('each is a `tsc` error on its interface, while a live neighbour type-checks', () => { + // The directives are real enforcement: this package type-checks its tests + // (`tsconfig.test.json`), so re-declaring a key fails on the unused directive. + // @ts-expect-error `selectionStyle` is RETIRED (objectui#6152) — checkboxes are always visible + const table: TsDataTableSchema = { type: 'data-table', columns: [], data: [], selectionStyle: 'hover' }; + // @ts-expect-error `floatingConfig` is RETIRED on `chatbot` (objectui#6152) — author `chatbot-floating` + const chat: TsChatbotSchema = { type: 'chatbot', messages: [], floatingConfig: { title: 'Support' } }; + const liveTable: TsDataTableSchema = { type: 'data-table', columns: [], data: [], selectable: true }; + const liveFloating: TsChatbotFloatingSchema = { type: 'chatbot-floating', messages: [], floatingConfig: { title: 'Support' } }; + expect([table.type, chat.type, liveTable.selectable, liveFloating.floatingConfig?.title]).toEqual(['data-table', 'chatbot', true, 'Support']); + }); +}); + +describe('objectui#6152 round 5 — the `chatbot-floating` face keeps its own `floatingConfig`', () => { + it('an authored `chatbot-floating`.`floatingConfig` parses on the strict face and the tolerant face', () => { + const doc = { type: 'chatbot-floating', messages, floatingConfig: { position: 'bottom-left', title: 'Support', triggerSize: 48 } }; + const strict = StrictAnyComponentSchema.safeParse(doc); + expect(strict.success, JSON.stringify(strict.error?.issues)).toBe(true); + const tolerant = AnyComponentSchema.safeParse(doc); + expect(tolerant.success, JSON.stringify(tolerant.error?.issues)).toBe(true); + }); +}); + +/* + * `editable` / `singleClickEdit` were filed by NAME as runtime slots (the seat's answer + * B): the declarations did NOT move. Read off the member, so a deletion — which would + * read as `any` through `BaseSchema`'s index signature — or a tombstone — which would + * read as `undefined` — is red here. + */ +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Expect = T; + +export type assertionInlineEditFlagsKeepTheirDeclarations = [ + Expect>, + Expect>, + Expect>, +]; diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 24c287508e..158275ee01 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -372,7 +372,18 @@ * and 37 / 53 until objectui#7344 swept the string / `z.any()` handler mirrors: * `DetailSchema` and `DetailViewSchema` entered (one `onBack` each) and * `CalendarViewSchema` grew by `onEventClick`. - * - **6 entries** in `UnmirroredDeclared`, **20 keys** across them — 9 / 44 until + * - **5 entries** in `UnmirroredDeclared`, **16 keys** across them — 6 / 20 until + * objectui#6152 round 5 closed ONE entry and shrank another, four keys by TWO routes + * that must not be read as one. TWO were RETIRED on both faces under ADR-0049 + * enforce-or-remove, each a tombstone and so a mirrored member: + * `DataTableSchema.selectionStyle` (read by `data-table` but authored and produced + * nowhere, its hover branch dropped) and `ChatbotSchema.floatingConfig` (ZERO reads on + * a `chatbot` node; `ChatbotFloatingSchema` keeps its own). TWO were RECLASSIFIED BY + * NAME into `RuntimeOnlyDeclared` — see that bullet below: `DataTableSchema`'s + * `editable` and `singleClickEdit`, host-paired flags `ObjectGrid` sets in code, which + * emptied that entry with the retirement beside them. ⚠️ The LOCAL half did NOT reach + * zero: `displayMode` stays on both chatbot twins BY DECISION (objectui#7654 ruling B, + * its refusal TypeScript-only) — see those two entries; 9 / 44 until * objectui#6152 round 4 closed THREE entries and shrank three more, twenty-four keys * by THREE routes that must not be read as one. TWO were MIRRORED, each measured READ * by a type-checker census: `ChatbotSchema.requestBody` (through the shared pick, so @@ -459,7 +470,13 @@ * seeded long after the 121). It is ⛔ not replaced with a fresh digit, for the * reason above. The full statement is on that ledger, which owns it — read it * there, and ⛔ do not copy it back. - * - **4 entries** in `RuntimeOnlyDeclared`, **27 keys** across them — 4 / 9 + * - **4 entries** in `RuntimeOnlyDeclared`, **29 keys** across them — 4 / 27 + * until objectui#6152 round 5 filed TWO more non-callback keys here BY NAME, each + * with its reason on `RuntimeOnlyNamedAllowList`, on the `DataTableSchema` entry that + * already existed: `editable` and `singleClickEdit`, the inline-edit flags `ObjectGrid` + * sets in code beside the save path it supplies. ⚠️ The OTHER side of + * `UnmirroredDeclared` losing the same two — ⛔ not new debt, and nothing mirrored by + * it. The entry count held; it was 4 / 9 * until objectui#6152 round 4 filed EIGHTEEN non-callback keys here BY NAME, each * with its reason on `RuntimeOnlyNamedAllowList`, on two entries that already * existed: fourteen code-produced `DataTableSchema` keys and four `DetailViewSchema` @@ -508,8 +525,12 @@ * two. ⭐ The direction is the one this ledger is meant to drain in: a * runtime-only key leaves by being declared on the mirror, never by being * quietly refiled. - * **1 of the 4** are a subset of the **6** pairs above, so - * the union of the two unmirrored ledgers is **9** pairs, being **6** from `UnmirroredDeclared` plus **3** recorded ONLY in `RuntimeOnlyDeclared`. + * **0 of the 4** are a subset of the **5** pairs above, so + * the union of the two unmirrored ledgers is **9** pairs, being **5** from `UnmirroredDeclared` plus **4** recorded ONLY in `RuntimeOnlyDeclared`. + * ⭐ objectui#6152 round 5 moved the difference from 3 to 4, and the shared count to + * zero: `DataTableSchema` — the last pair in BOTH ledgers — left `UnmirroredDeclared` + * whole (two keys filed here by name, one retired) while keeping its entry here, so + * the union held at `9`. It read `1 of the 4`, `6` and `3` until then. * ⭐ objectui#6152 round 4 moved the difference from 2 to 3: `DetailViewSchema` left * `UnmirroredDeclared` whole (four keys filed here by name, one retired) while * keeping its entry here. It read `2 of the 4`, a union of `11`, until then. @@ -528,12 +549,12 @@ * drained it by MIRRORING the key as a named refusal. ⛔ The difference figure * is not recoverable by editing this sentence — it is a fact about the ledgers, * and it returns only when some pair is again runtime-only and nothing else. - * ⚠️ Every live figure in these two sentences is pinned: the `1` (a quantity + * ⚠️ Every live figure in these two sentences is pinned: the `0` (a quantity * of its own — how many entries the two unmirrored ledgers share), the `9` (the - * union) and the `3` (the pairs recorded only as runtime-only), and three - * RESTATEMENTS of counts already stated above — the `4` and both `6`s. (This list - * was amended by objectui#6152 round 3, which reshaped the sentence, and re-read - * by round 4, which moved only its digits.) + * union) and the `4` after `plus` (the pairs recorded only as runtime-only), and + * three RESTATEMENTS of counts already stated above — the `4` in `of the 4` and + * both `5`s. (This list was amended by objectui#6152 round 3, which reshaped the + * sentence, and re-read by rounds 4 and 5, which moved only its digits.) * They were spelled as English WORDS * until objectui#8222, which is why no instrument had ever read them: a figure * spelled "six" rots exactly as fast as one spelled `6`, it is just harder to @@ -2559,7 +2580,7 @@ interface KnownDrift { * * objectui#6058 seeded this ledger at **121 keys**, and — on ONE line, because the * pin below reads this sentence off disk — - * **20 keys** is what this ledger records today. + * **16 keys** is what this ledger records today. * The movements between the two are different facts. objectui#6152 measured the 23 * callback-shaped (`on*`) keys and ruled that mirroring is the wrong remedy for every * one of them; @@ -2593,7 +2614,10 @@ interface KnownDrift { * `DetailViewSchema.autoDiscoverRelated`), and eighteen RECLASSIFIED into * `RuntimeOnlyDeclared` by NAME (fourteen of `DataTableSchema`'s, four of * `DetailViewSchema`'s) — the largest single move since the seed, and mostly NOT a - * repair. + * repair. Then objectui#6152 round 5 moved four keys by two routes: two RETIRED on both + * faces (`DataTableSchema.selectionStyle`, `ChatbotSchema.floatingConfig`) and two + * RECLASSIFIED into `RuntimeOnlyDeclared` by NAME (`DataTableSchema`'s `editable` and + * `singleClickEdit`) — ⛔ none of the four a repair, and none mirrored. * * ## The decomposition of "121" — a reading at NAMED REVISIONS, so it cannot rot * @@ -2757,7 +2781,13 @@ interface KnownDrift { * spec schema does not model, which is objectui#2231's unification question and * NOT a local mirror edit. They are marked, not exempted: exempting them in the * instrument would re-blind exactly the pairs objectui#5927 leaned on hardest. - * - **LOCAL (3 entries, 6 keys)** — plain omissions from a hand-written mirror. + * - **LOCAL (2 entries, 2 keys)** — omissions from a hand-written mirror. ⚠️ The two + * left are NOT plain omissions: both are `displayMode`, on the two chatbot twins, a + * tombstone whose refusal stays TypeScript-only BY DECISION (objectui#7654 ruling B, + * kept by objectui#6152 round 4) — so this half does not drain to zero by mirroring. + * It was 3 / 6 until objectui#6152 round 5 closed `DataTableSchema`'s entry (two keys + * RECLASSIFIED by name, one RETIRED) and shrank `ChatbotSchema`'s (`floatingConfig` + * RETIRED). No entry crossed between the halves, so the SPEC-DERIVED half did not move. * It was 6 / 30 until objectui#6152 round 4 closed THREE entries here and shrank * three more: `LabelSchema` (its one key RETIRED), `ReportComponentSchema` (both keys * RETIRED) and `DetailViewSchema` (four keys RECLASSIFIED by name, one RETIRED) left; @@ -2835,9 +2865,14 @@ interface KnownDrift { * `ReportComponentSchema`'s last two keys and `DetailViewSchema.autoDiscoverRelated`, * RECLASSIFIED eighteen keys of `DataTableSchema` and `DetailViewSchema` into * `RuntimeOnlyDeclared` by name, and MIRRORED one key on each chatbot face — three - * entries emptied. The seeded pair is no longer among them, and the ledger now totals — on ONE line, because the pin below + * entries emptied. objectui#6152 round 5 then moved only the LOCAL half again, by two + * routes: it RETIRED `DataTableSchema.selectionStyle` and `ChatbotSchema.floatingConfig` + * on both faces and RECLASSIFIED `DataTableSchema`'s `editable` and `singleClickEdit` + * into `RuntimeOnlyDeclared` by name — one entry emptied, and what the LOCAL half keeps + * is `displayMode` on the two chatbot twins, by decision rather than as debt. The seeded + * pair is no longer among them, and the ledger now totals — on ONE line, because the pin below * reads this sentence off disk — - * **6 entries / 20 keys** — 3 / 14 spec-derived, 3 / 6 local. + * **5 entries / 16 keys** — 3 / 14 spec-derived, 2 / 2 local. * * ⛔ The four split figures above and this totals line are PINNED: 'objectui#7279' * at the bottom of this file derives every one of them from the `UnmirroredDeclared` @@ -2879,22 +2914,25 @@ interface UnmirroredDeclared { * saying `body` sat in `KnownDrift` for a naming collision; objectui#8572 retired that * arm and the `KnownDrift` row, as that entry above records.) * - * The two left are NOT mirroring debt in waiting: + * It was two until objectui#6152 round 5 RETIRED `floatingConfig` on both faces (the + * seat's answer A to round 4's open question): ZERO reads on this node by a + * type-checker census and a runtime probe, since the `chatbot` registration never + * reads it and only `chatbot-floating`'s does. It is `?: never` on the interface and a + * `retirementTombstone()` on this mirror now — a tombstone is a mirrored member — and + * `ChatbotFloatingSchema` never inherited it (it picks the shared keys and declares + * its own `floatingConfig`), so the entry below did not move. The `chatbot` half of + * `floating-chatbot-trigger-icon-retired.test.ts` was the tripwire for that absence, + * and it now pins the refusal instead. * - * - `displayMode` is a `?: never` tombstone since objectui#7654 (maintainer ruling B, - * 2026-09-05), and its refusal stays TypeScript-ONLY under that ruling: stored - * designer documents carry `displayMode: 'floating'`, and a zod arm would refuse - * them. objectui#6152 round 4 took the question under that ruling and minted no - * arm, so the key stays here by DECISION, ⛔ not as a mirror half owed. - * `chatbot-display-mode-retired.test.ts` pins this twin's shape as the tripwire. - * - `floatingConfig` — ZERO reads on this node (round 4's type-checker census): the - * `chatbot` registration never reads it, only `chatbot-floating`'s does, and that - * twin mirrors it now (see the entry below). Mirroring it here would bless a key - * nothing honours on this node, so it is a NARROWING candidate under ADR-0049 - * enforce-or-remove, reported by round 4 and ⛔ not narrowed. The `chatbot` half of - * `floating-chatbot-trigger-icon-retired.test.ts` pins its absence as a tripwire. + * The one left is NOT mirroring debt in waiting: `displayMode` is a `?: never` + * tombstone since objectui#7654 (maintainer ruling B, 2026-09-05), and its refusal + * stays TypeScript-ONLY under that ruling: stored designer documents carry + * `displayMode: 'floating'`, and a zod arm would refuse them. objectui#6152 round 4 + * took the question under that ruling and minted no arm, so the key stays here by + * DECISION, ⛔ not as a mirror half owed. `chatbot-display-mode-retired.test.ts` pins + * this twin's shape as the tripwire. */ - 'complex.zod.ts#ChatbotSchema': 'displayMode' | 'floatingConfig'; + 'complex.zod.ts#ChatbotSchema': 'displayMode'; /** * LOCAL — a pair born ledgered (objectui#7655) with the two keys the floating * face declares alongside `ChatbotSchema`. It was two until objectui#6152 round 4 @@ -2932,30 +2970,27 @@ interface UnmirroredDeclared { // entry and `ObjectDataTableSchema`'s below went in the same change) — not a // reclassification and not a retirement: the key is still declared, still authorable, // and is now enforced. The pair holds no entry in either unmirrored ledger. - /** - * LOCAL. It was 17 until objectui#6152 round 4 measured every key (a type-checker - * census over every package's sources for reads and in-code producers, and an - * authored census over every tracked JSON file, Markdown JSON fence and - * `type: 'data-table'` object literal) and moved FOURTEEN to `RuntimeOnlyDeclared` - * below BY NAME, each with its reason on `RuntimeOnlyNamedAllowList`: keys a host - * sets in code (`ObjectGrid`'s in-code `data-table` node, or `RelatedList`'s) and no - * document authors — ⛔ none mirrored, no declaration moved. It was 29 before that: - * the twelve `on*` keys are in `RuntimeOnlyDeclared` below (objectui#6152). - * `rowActions` is in `KnownDrift` above — the mirror does declare that one, disjointly. - * - * The three left fit NONE of round 4's classes, so the round stopped at each and - * reported it rather than guess: - * - * - `editable`, `singleClickEdit` — READ by `data-table`, produced in code by - * `ObjectGrid`, and authored by no document; but the published docs page for - * this node (`content/docs/components/complex/data-table.mdx`) lists both in its - * interface block and teaches `editable: true` in prose. Filing them runtime-only - * would contradict that page, and mirroring them would rest on no authored - * document, so the route is a ruling. - * - `selectionStyle` — READ by `data-table`, and neither authored nor produced - * anywhere: no document and no in-code producer writes it. - */ - 'data-display.zod.ts#DataTableSchema': 'editable' | 'selectionStyle' | 'singleClickEdit'; + // `data-display.zod.ts#DataTableSchema` recorded keys here (LOCAL) from objectui#6058's + // seeding until objectui#6152 round 5 emptied the entry by two routes that must not be + // read as one (the seat's answers 1 and 2 to round 4's open questions): + // + // - `editable` and `singleClickEdit` were MOVED to `RuntimeOnlyDeclared` below BY + // NAME, each with its reason on `RuntimeOnlyNamedAllowList` — round 4's class rule + // for a key a host produces in code (`ObjectGrid`, beside the save path it + // supplies) and no document authors. ⛔ Neither was mirrored and neither + // declaration moved; the docs page that taught them on an authored node + // (`content/docs/components/complex/data-table.mdx`) was corrected in the same + // change, which is what had stopped round 4 at them. + // - `selectionStyle` was RETIRED on both faces under ADR-0049 enforce-or-remove: + // read by `data-table`, but authored and produced nowhere (re-measured in this + // repository and in `objectstack`), so its hover-only branch was dropped and the key + // is `?: never` on the interface and a `retirementTombstone()` on the mirror. A + // tombstone is a mirrored member. + // + // It was three keys after round 4, which moved FOURTEEN host-produced keys to + // `RuntimeOnlyDeclared` by name; 17 before that, and 29 before the twelve `on*` keys + // went there (objectui#6152). `rowActions` is in `KnownDrift` above — the mirror does + // declare that one, disjointly. The pair now records nothing here. /** * SPEC-DERIVED by MEMBERSHIP since objectui#11070, LOCAL before it: that card declared * nine field-metadata members by reference to the spec's `FieldSchema`, so @@ -3081,7 +3116,7 @@ interface UnmirroredDeclared { * * `UnmirroredDeclared` above was seeded at **121 keys** by objectui#6058, and — on * ONE line, because the pin below reads this sentence off disk — - * `UnmirroredDeclared` records **20 keys** today. + * `UnmirroredDeclared` records **16 keys** today. * These 23 moved here whole. Keys have since left that ledger by MIRRORING and by * RETIREMENT, but the move recorded HERE is neither and repaired nothing. ⛔ Nothing * was mirrored by it, no declaration was removed, no defect was repaired and nothing was @@ -3230,14 +3265,22 @@ interface RuntimeOnlyDeclared { * `sort` as well) — and no document authors any of them: the server-mode controlled * state that pairs with the four `on*` slots above, two FUNCTION slots * (`rowClassName`, `rowStyle`), and host-composition flags. + * + * ⭐ GREW by two more NON-callback keys with objectui#6152 round 5 (the seat's answer + * B to round 4's first open question, under that round's own class rule): `editable` + * and `singleClickEdit`, the inline-edit flags `ObjectGrid` sets in code beside the + * save path it supplies (`onRowSave` / `onBatchSave`), each filed BY NAME with its + * reason on `RuntimeOnlyNamedAllowList` below. Both came from `UnmirroredDeclared` + * above, emptying its `DataTableSchema` entry together with `selectionStyle`'s + * retirement. ⛔ Neither was mirrored and no declaration moved. */ 'data-display.zod.ts#DataTableSchema': | 'onColumnReorder' | 'onPageChange' | 'onPageSizeChange' | 'onSearchChange' | 'onSortChange' - | 'disableInnerScroll' | 'manualPagination' | 'manualSearch' | 'manualSorting' + | 'disableInnerScroll' | 'editable' | 'manualPagination' | 'manualSearch' | 'manualSorting' | 'page' | 'rowActionDefs' | 'rowClassName' | 'rowCount' | 'rowStyle' - | 'search' | 'selectionResetKey' | 'showAddRow' | 'showSelectionCount' | 'sort'; + | 'search' | 'selectionResetKey' | 'showAddRow' | 'showSelectionCount' | 'singleClickEdit' | 'sort'; /** * 1 of `FormSchema`'s former 9. OVERSIGHT group — `onSubmit`, `onChange` and * `onCancel` are mirrored beside it. Read at `renderers/form/form.tsx:997`, by @@ -3435,9 +3478,16 @@ interface RuntimeOnlyNamedAllowList { * and found in no document by an authored census over every tracked JSON file, * Markdown JSON fence and `type: 'data-table'` object literal. ⛔ Not a waiver: the * comparison still reconciles each key, and a row whose key leaves the ledger is red. + * + * objectui#6152 round 5 added `editable` and `singleClickEdit` under the same class + * rule (the seat's answer B), re-measured the same way on that round's base. What + * had stopped round 4 at them was a docs page teaching both on an authored node; the + * same change corrected that page (`content/docs/components/complex/data-table.mdx`), + * because an authored `editable: true` stages edits that no callback persists. */ 'data-display.zod.ts#DataTableSchema': { disableInnerScroll: 'host-composition flag: set in code by ObjectGrid on each grouped sub-table so all groups share one scroll container; authored in no document'; + editable: 'host-paired flag: set in code by ObjectGrid beside the onRowSave / onBatchSave save path it supplies; an authored value stages edits that nothing persists, and no document authors it'; manualPagination: 'host-driven server paging: set in code by ObjectGrid with rowCount, page and the onPageChange slot; authored in no document'; manualSearch: 'host-driven server search: set in code by ObjectGrid with search and the onSearchChange slot; authored in no document'; manualSorting: 'host-driven server sort: set in code by ObjectGrid and RelatedList with sort and the onSortChange slot; authored in no document'; @@ -3450,6 +3500,7 @@ interface RuntimeOnlyNamedAllowList { selectionResetKey: 'imperative reset token: ObjectGrid bumps it in code to clear the checkboxes after a bulk action; authored in no document'; showAddRow: 'host-paired flag: set in code by ObjectGrid from create permission, beside the onAddRecord slot the row calls; authored in no document'; showSelectionCount: 'host-composition flag: set false in code by ObjectGrid, whose BulkActionBar shows the selection instead; authored in no document'; + singleClickEdit: 'host-paired flag: set in code by ObjectGrid with editable, choosing how a cell enters inline edit; authored in no document'; sort: 'controlled sort for manualSorting: set in code by ObjectGrid and RelatedList from their own sort state; authored in no document'; }; /** @@ -5429,13 +5480,34 @@ the two is stated ONCE, in this file's header.`) // reads that difference off the header and derives it from these same two // ledgers, so no direction needs asserting here, and the next drain moves a digit // rather than this leg. ⛔ Do not restore a direction leg of either sign. The two - // non-vacuity legs below are the ones both shapes relied on, and they stay. + // non-vacuity legs below are the ones both shapes relied on. + // + // ⭐ objectui#6152 round 5 made the second of them FALSE as a fact about the + // ledgers: `DataTableSchema`, the last pair in both, left `UnmirroredDeclared` + // whole, so the two unmirrored ledgers ARE disjoint now and the shared figure in + // the header reads `0`. Asserting a non-empty intersection here would have forced a + // key back onto a ledger to satisfy a test — route 1 again. What that leg guarded + // is kept, measured another way: an intersection of ZERO is only a reading if the + // entry reader spells one pair the same way in every ledger it reads — a reader + // that normalised names differently per ledger would return 0 for every + // intersection and the `0` would pass blind. So the lit control is an intersection + // that is NOT empty, read by the same reader: `KnownDrift` shares pairs with each + // of the two unmirrored ledgers (`ChatbotSchema` with one, `DataTableSchema` with + // the other, on this change's tree). ⛔ If a later drain empties BOTH of those + // intersections too, pick another non-empty one — never delete the control. const unmirrored = new Set(ledgerEntryKeys('UnmirroredDeclared')); const runtimeOnly = ledgerEntryKeys('RuntimeOnlyDeclared'); + const knownDrift = new Set(ledgerEntryKeys('KnownDrift')); expect(runtimeOnly.length, 'RuntimeOnlyDeclared read as EMPTY — the subset and union figures are then vacuous') .toBeGreaterThan(0); - expect(runtimeOnly.filter((pair) => unmirrored.has(pair)).length, 'the two unmirrored ledgers read as DISJOINT') - .toBeGreaterThan(0); + expect( + [...unmirrored].filter((pair) => knownDrift.has(pair)).length, + 'UnmirroredDeclared and KnownDrift read as DISJOINT — the entry reader may not spell one pair the same way in two ledgers, so a 0 intersection would be blind', + ).toBeGreaterThan(0); + expect( + runtimeOnly.filter((pair) => knownDrift.has(pair)).length, + 'RuntimeOnlyDeclared and KnownDrift read as DISJOINT — the entry reader may not spell one pair the same way in two ledgers, so a 0 intersection would be blind', + ).toBeGreaterThan(0); }); }); diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index 8719e4d7ad..48160e4156 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -1587,10 +1587,39 @@ export interface ChatbotSchema extends BaseSchema { displayMode?: never; /** - * Configuration for the floating action button and the panel it opens — - * read by `chatbot-floating` alone and forwarded to ``. - */ - floatingConfig?: FloatingChatbotConfig; + * ADR-0049 RETIREMENT TOMBSTONE — `floatingConfig` on a `chatbot` node + * (objectui#6152 round 5, the seat's answer A). Author a `chatbot-floating` + * node instead: {@link ChatbotFloatingSchema.floatingConfig} is the one + * declaration of the trigger and panel geometry, and that registration is the + * one that reads it. + * + * What was measured, on the retiring change's base (a reading at that + * revision, ⛔ not a live count): a type-checker census over every package + * found no read of this member and no producer outside one test; a runtime + * probe through the real `SchemaRenderer` and registry rendered a `chatbot` + * node byte-for-byte the same with and without a full `floatingConfig` + * (a `placeholder` control moved the DOM, and the same config on a + * `chatbot-floating` node drew its title); and no document authored it on a + * `chatbot` node. The `chatbot` registration forwards only DOM-safe props, + * so the value never reached ``, which has no trigger or panel to + * apply it to. + * + * Retired from THIS face only. {@link ChatbotFloatingSchema} does not + * inherit it: that face extends `Pick` and declares its own `floatingConfig`, and its zod + * twin spreads the shared pick and mirrors the member itself — so no + * re-declaration was needed, and the floating face is untouched. + * + * A `?: never` tombstone and ⛔ not a deletion, by the discriminator stated + * on {@link ChatbotSchema.displayMode} above (cited, not restated): a deleted + * member on this {@link BaseSchema} carrier reads as `any` through the index + * signature, and this member's published comment advertised the floating + * configuration on the `chatbot` face. The zod twin refuses it by name. + * + * @deprecated Not a key `chatbot` reads — author `type: 'chatbot-floating'` + * with `floatingConfig`. + */ + floatingConfig?: never; /** * REFUSED BY NAME (objectui#8572, ADR-0049) — the chat API's body params are * authored as `requestBody`, and `body` on this node means what it means on @@ -1841,8 +1870,10 @@ export interface ChatbotEnhancedSchema * - `enableMarkdown`, `enableFileUpload` and the `onClear` runtime slot, * forwarded into the panel's ``; * - `floatingConfig`, the trigger and panel geometry - * ({@link FloatingChatbotConfig}) — ALSO declared on {@link ChatbotSchema}, - * unchanged there — and `displayMode`, a `?: never` tombstone on both + * ({@link FloatingChatbotConfig}) — declared on THIS face alone since + * objectui#6152 round 5 retired the `chatbot` face's copy, which that + * registration never read (this face never inherited it: the `Pick` above + * leaves it out) — 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 @@ -1908,6 +1939,8 @@ export interface ChatbotFloatingSchema /** * Configuration for the floating action button and the panel it opens — * read by `chatbot-floating` alone and forwarded to ``. + * The one declaration of this geometry: {@link ChatbotSchema.floatingConfig} + * is a retirement tombstone since objectui#6152 round 5. */ floatingConfig?: FloatingChatbotConfig; /** @@ -2024,6 +2057,13 @@ export interface FloatingChatbotConfig { * type-level only. The paragraph above is the reading this retirement landed * on. * + * ⚠️ AMENDED again by objectui#6152 round 5, which RETIRED + * `ChatbotSchema.floatingConfig` on both faces. A `chatbot` node now refuses + * the whole `floatingConfig` key — `triggerIcon` with it — at compile time and + * at parse time, so "type-level only" no longer holds anywhere: the one face + * that still declares this interface is `chatbot-floating`, where both halves + * of this refusal hold. + * * ## Why a tombstone and not a deletion, with only the `tsc` channel available * * The usual argument for `?: never` over deletion is about the mirror (an diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index b21a9b18cf..6326ec8e93 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -1267,12 +1267,30 @@ export interface DataTableSchema extends BaseSchema { */ selectable?: boolean | 'single' | 'multiple'; /** - * Selection checkbox display style - * - 'always': Checkboxes are always visible - * - 'hover': Checkboxes only appear on row hover - * @default 'always' - */ - selectionStyle?: 'always' | 'hover'; + * RETIRED (objectui#6152 round 5, ADR-0049) — a selectable table always shows + * its row checkboxes; there is no hover-only style. Delete the key: `selectable` + * alone turns selection on. + * + * It was `'always' | 'hover'`, and `data-table` honoured `'hover'` (the checkbox + * cell faded in on row hover) — but nothing wrote it. Measured on the retiring + * change's base, a reading at that revision and ⛔ not a live count: no document + * in this repository or in `objectstack` authors it (every tracked + * JSON file, Markdown JSON fence and `type: 'data-table'` object literal), and a + * type-checker census over every package found its one destructuring read and + * no producer. A published capability with no author and no producer is + * retired, ⛔ not kept for its sunk cost, and the hover branch went with it. + * + * A `?: never` tombstone and ⛔ not a deletion, by the retire-vs-remove + * discriminator stated on `ChatbotSchema` in `complex.ts` (cited, not restated + * here): this member's published comment taught `'hover'` as a working style, + * and on a {@link BaseSchema} carrier a deleted member reads as `any` through + * the index signature, so a stale `'hover'` would type-check in silence. The + * zod twin refuses the key by name. + * + * @deprecated Not part of this contract — selection checkboxes are always + * visible. + */ + selectionStyle?: never; /** * Whether to render the built-in "N selected" count in the table toolbar. * Set false when an outer container (e.g. ObjectGrid's BulkActionBar) already @@ -1388,12 +1406,23 @@ export interface DataTableSchema extends BaseSchema { /** * Enable inline cell editing * When true, cells become editable on double-click or Enter key + * + * HOST-PAIRED, not an authored flag (objectui#6152 round 5): the table only + * STAGES an edit; persisting it is the job of the `onRowSave` / `onCellChange` + * / `onBatchSave` slots, which a host supplies in code — `ObjectGrid` sets this + * key on the `data-table` node it builds, together with that save path. A JSON + * document can supply no function, so an authored `editable: true` stages edits + * that nothing saves. To edit records inline from a document, author an + * `object-grid`, which carries the save path itself. * @default false */ editable?: boolean; /** * Enable single-click editing mode * When true with editable, clicking a cell enters edit mode (instead of double-click) + * + * HOST-PAIRED with {@link DataTableSchema.editable} (objectui#6152 round 5): + * `ObjectGrid` sets it in code; no document authors it. * @default false */ singleClickEdit?: boolean; diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index 0f9c5f5c6e..a07a733d5d 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -915,6 +915,18 @@ export const ChatbotSchema = BaseSchema.extend({ + 'any request was sent. Cap tool loops on the agent instead — `planning.maxIterations` (default 10). ' + 'Delete the key.', ), + // objectui#6152 round 5, ADR-0049 — declared on the interface and never + // mirrored here, so `.passthrough()` KEPT an authored value that the `chatbot` + // registration never reads. Retired on both faces; the census and the route + // are on the TS twin's `ChatbotSchema.floatingConfig` member (`../complex.ts`). + // ⛔ Not in `ChatbotSharedMirrorShape` below, so the `chatbot-floating` twin's + // own `floatingConfig` arm is untouched. + floatingConfig: retirementTombstone( + 'RETIRED (objectui#6152, ADR-0049) — never read on a `chatbot` node: the trigger and the panel this ' + + 'key configures belong to the floating presentation, which a `chatbot` node does not render. Author ' + + '`type: "chatbot-floating"` with `floatingConfig` instead — that is the registration that reads it — or ' + + 'delete the key.', + ), onError: handlerKeyRefusal('onError', 'runtime-slot', 'Error callback'), // --- Local display + legacy auto-response fields (objectui#6169) --- // Mirrors the TS declaration added at ../complex.ts in lockstep, so these @@ -1077,7 +1089,8 @@ const FloatingChatbotConfigSchema = z.object({ * Zod twin of `../complex.ts`'s `ChatbotFloatingSchema`. One of that * declaration's keys is deliberately NOT mirrored, and the parity ledger * records it under `UnmirroredDeclared` for this pair — exactly as it records - * the same key for `ChatbotSchema`, which declares it too: + * the same key for `ChatbotSchema`, which declares it too (the only key the two + * twins' ledger entries still hold since objectui#6152 round 5): * * - `displayMode` — RETIRED by objectui#7654 (maintainer ruling B, * 2026-09-05): the node `type` is the one selector of presentation. The @@ -1093,8 +1106,11 @@ const FloatingChatbotConfigSchema = z.object({ * `floatingConfig` was the second such key until objectui#6152 round 4 minted * `FloatingChatbotConfigSchema` above and declared it here: this registration is * the one that reads it (`floatingConfig={schema.floatingConfig}`). On - * `ChatbotSchema`'s twin it stays unmirrored — the `chatbot` registration never - * reads it. + * `ChatbotSchema`'s twin it stayed unmirrored — the `chatbot` registration never + * reads it — until objectui#6152 round 5 RETIRED it there, a + * `retirementTombstone()` arm on that twin; this twin does not spread that arm + * (the shared pick leaves it out), so its own arm above is the only + * `floatingConfig` any chatbot twin accepts. */ export const ChatbotFloatingSchema = BaseSchema.extend({ type: z.literal('chatbot-floating'), diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index 931c5ba19d..271026475a 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -412,6 +412,15 @@ export const DataTableSchema = BaseSchema.extend({ pageSizeOptions: z.array(z.number()).optional().describe('Options for the rows-per-page selector (defaults to 5/10/20/50/100).'), searchable: z.boolean().optional().describe('Enable search'), selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Enable row selection — `true`/`multiple` = multi-select, `single` = replace-on-select with no select-all'), + // objectui#6152 round 5, ADR-0049 — declared on the interface and never + // mirrored, so `.passthrough()` KEPT an authored value unexamined; it is + // retired on both faces now (the census and the route are on the + // `DataTableSchema.selectionStyle` member in `../data-display.ts`). + selectionStyle: retirementTombstone( + 'RETIRED (objectui#6152, ADR-0049) — a selectable table always shows its row checkboxes, and there is ' + + 'no hover-only style: nothing authored or produced this key, so the hover branch was dropped. Delete the ' + + 'key; `selectable` alone turns selection on.', + ), sortable: z.boolean().optional().describe('Enable sorting'), exportable: z.boolean().optional().describe('Enable data export'), rowActions: z.boolean().optional().describe('Show the row actions column (edit/delete) — mirrors the boolean the renderer truthiness-tests'),