From 87520eab457f8a304b0a96662ca8039b7ef5b94a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:37:51 +0000 Subject: [PATCH 1/4] feat(types,components)!: retire four zero-read or third-spelling keys, mirror two chatbot keys, file eighteen runtime slots by name (objectui#6152, round 4) - LabelSchema.content: retired on both faces (a third spelling of the label text); the label renderer's third read is dropped. - ReportComponentSchema.chartConfig / reportType and DetailViewSchema.autoDiscoverRelated: re-measured zero-read, retired on both faces; specReportToPresentation stops writing reportType. - ChatbotSchema.requestBody mirrored through the shared pick; ChatbotFloatingSchema.floatingConfig mirrored through a module-private FloatingChatbotConfig restatement that carries the triggerIcon tombstone. - DataTableSchema (14 code-produced keys) and DetailViewSchema (activities, comments, history, recordNavigation) filed in RuntimeOnlyDeclared by name, each with its reason on RuntimeOnlyNamedAllowList. - UnmirroredDeclared 9 / 44 -> 6 / 20; RuntimeOnlyDeclared 4 / 9 -> 4 / 27. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude --- .../src/__tests__/form-renderers.test.tsx | 15 +- .../components/src/renderers/form/label.tsx | 4 +- ...-registration-authoring-faces-7655.test.ts | 30 +- ...ating-chatbot-trigger-icon-retired.test.ts | 104 +++--- .../types/src/__tests__/spec-report.test.ts | 13 +- .../src/__tests__/zod-mirror-parity.test.ts | 306 ++++++++++++------ packages/types/src/complex.ts | 15 +- packages/types/src/form.ts | 30 +- packages/types/src/reports.ts | 42 ++- packages/types/src/spec-report.ts | 17 +- packages/types/src/views.ts | 21 +- packages/types/src/zod/complex.zod.ts | 85 +++-- packages/types/src/zod/form.zod.ts | 12 +- packages/types/src/zod/reports.zod.ts | 15 + packages/types/src/zod/views.zod.ts | 19 +- 15 files changed, 508 insertions(+), 220 deletions(-) diff --git a/packages/components/src/__tests__/form-renderers.test.tsx b/packages/components/src/__tests__/form-renderers.test.tsx index bcbfa4a0bd..58353e83c6 100644 --- a/packages/components/src/__tests__/form-renderers.test.tsx +++ b/packages/components/src/__tests__/form-renderers.test.tsx @@ -359,13 +359,26 @@ describe('Form Renderers - Display Issue Detection', () => { it('should render label element', () => { const { container } = renderComponent({ type: 'label', - content: 'Form Label', + text: 'Form Label', }); const label = container.querySelector('label'); expect(label).toBeTruthy(); expect(label?.textContent).toContain('Form Label'); }); + + it('no longer reads the retired `content` spelling (objectui#6152)', () => { + // `content` was the renderer's third read, after `text` and `label`. Both faces of + // `@object-ui/types` retire it, so a node carrying only `content` renders no text. + const { container } = renderComponent({ + type: 'label', + content: 'Retired Spelling', + }); + + const label = container.querySelector('label'); + expect(label).toBeTruthy(); + expect(label?.textContent ?? '').not.toContain('Retired Spelling'); + }); }); describe('Field locators (ADR-0054 C4)', () => { diff --git a/packages/components/src/renderers/form/label.tsx b/packages/components/src/renderers/form/label.tsx index f3b0816073..f3da2ff64e 100644 --- a/packages/components/src/renderers/form/label.tsx +++ b/packages/components/src/renderers/form/label.tsx @@ -27,7 +27,9 @@ ComponentRegistry.register('label', // Apply designer props {...{ 'data-obj-id': dataObjId, 'data-obj-type': dataObjType, style }} > - {schema.text || schema.label || schema.content} + {/* `text`, then `label`. A third spelling, `content`, is retired on both + faces of `@object-ui/types` and is no longer read (objectui#6152). */} + {schema.text || schema.label} ); }, 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 4f5047341a..31d73aaaae 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 @@ -47,9 +47,9 @@ * `?: 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`. + * parses green with any value, as a tripwire; objectui#6152 round 4 kept the + * refusal TypeScript-only under that ruling, so no arm is owed. 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 * @@ -376,8 +376,9 @@ describe('`ChatbotEnhancedSchema` (zod) validates what the face declares', () => }); it('mirrors `requestBody` under the key the renderer reads, and refuses a non-object', () => { - // `ChatbotSchema`'s twin mirrors this under `body`, colliding with the base - // children slot; the two new twins do not copy that collision. + // `ChatbotSchema`'s twin used to mirror this under `body`, colliding with the + // base children slot; objectui#8572 retired that arm, and since objectui#6152 + // round 4 all three twins share one `requestBody` arm through the pick. expect(ChatbotEnhancedZod.shape.requestBody).toBeDefined(); const result = ChatbotEnhancedZod.safeParse({ ...node, requestBody: 'tenant=acme' }); expect(result.success).toBe(false); @@ -395,7 +396,7 @@ describe('`ChatbotEnhancedSchema` (zod) validates what the face declares', () => }); }); -describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and leaves the two shared floating keys unmirrored', () => { +describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and leaves `displayMode` unmirrored', () => { const node = { type: 'chatbot-floating', messages: [{ id: '1', role: 'user', content: 'hi' }], @@ -419,7 +420,7 @@ describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and le } }); - 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)', () => { + it('TRIPWIRE — `displayMode` stays unmirrored here as on `ChatbotSchema`: any value still parses green (retired by objectui#7654; the refusal stays TypeScript-only under that ruling, objectui#6152 round 4)', () => { // 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 @@ -446,11 +447,14 @@ describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and le expect(ChatbotFloatingZod.safeParse({ ...node, enableMarkdown: 'anything-at-all' }).success).toBe(false); }); - it('`floatingConfig` has no mirror here either — the objectui#6152 axis is not widened into', () => { - expect((ChatbotFloatingZod.shape as Record).floatingConfig).toBeUndefined(); - // Rides through unvalidated, wrong shape and all — byte for byte the - // outcome on `ChatbotSchema`'s twin, which has no arm for it either. - expect(ChatbotFloatingZod.safeParse({ ...node, floatingConfig: { panelHeight: '520px' } }).success).toBe(true); + 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. + 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(); }); it('`onClear` / `onError` / `onSend` are refused by name here too', () => { @@ -465,7 +469,7 @@ describe('`ChatbotFloatingSchema` (zod) validates what the face declares, and le describe('the census is structural: picked off `ChatbotSchema`, never copied', () => { const shared: readonly ChatbotSharedKey[] = [ 'messages', 'placeholder', 'api', 'conversationId', 'systemPrompt', 'model', 'streamingEnabled', - 'headers', 'onError', 'showTimestamp', 'userAvatarUrl', 'userAvatarFallback', + 'headers', 'requestBody', 'onError', 'showTimestamp', 'userAvatarUrl', 'userAvatarFallback', 'assistantAvatarUrl', 'assistantAvatarFallback', 'autoResponse', 'autoResponseText', 'autoResponseDelay', 'onSend', ]; 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 c9ac1af398..37aec0d8a6 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 @@ -49,6 +49,13 @@ * 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. + * + * ⚠️ AMENDED by objectui#6152 round 4: "type-level only" now holds on a `chatbot` + * node alone. That round minted the `FloatingChatbotConfig` mirror on the + * `chatbot-floating` twin — the registration that reads `floatingConfig` — with + * 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. */ import { describe, it, expect } from 'vitest'; @@ -128,44 +135,29 @@ describe('the `triggerIcon` tombstone makes authoring a `tsc` error', () => { }); }); -/* ── the runtime channel: DELIBERATELY unchanged, and a tripwire if that ends ─ */ +/* ── the runtime channel: unchanged on `chatbot`, refused on `chatbot-floating` since objectui#6152 round 4 ─ */ // Both faces declare `floatingConfig` — `ChatbotSchema` always did, and // objectui#7655 declared it on `ChatbotFloatingSchema`, the face of the one -// registration that reads it — 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 on a `%s` node, and that is deliberate (objectui#7654)', (type, twin) => { +// 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)', () => { const node = { - type, + type: 'chatbot', messages: [{ id: 'm1', role: 'user' as const, content: 'hi' }], }; - it(`a ${type} node carrying \`floatingConfig.triggerIcon\` still parses GREEN`, () => { - // `FloatingChatbotConfig` has NO zod mirror: `floatingConfig` sits in the - // `UnmirroredDeclared` ledger (`zod-mirror-parity.test.ts`, under both - // `complex.zod.ts#ChatbotSchema` and, since objectui#7655, - // `complex.zod.ts#ChatbotFloatingSchema`), and `BaseSchema` is - // `.passthrough()`, so the whole object rides through unvalidated. This was - // green before the tombstone and is green after it — the retirement changed - // the TypeScript face only, and this pins that it changed no parse outcome. - // - // ⚠️ TRIPWIRE: if objectui#6152 ever mints a `FloatingChatbotConfigSchema` - // and wires it onto these twins as the `floatingConfig` arm, the assertion - // that fires is the SHAPE PIN at the foot of this block — the one reading - // `shape.floatingConfig` — and NOT this line. Measured on objectui#7678's - // base, arm injected on both twins and restored under a trap: a - // house-style non-strict `z.object` mirror reds the shape pin ONLY (2 - // failures, one per twin) and leaves this parse-green line GREEN, because - // a non-strict object accepts `triggerIcon` and strips it, so `success` - // stays `true`; a `z.strictObject` mirror reds both (4 failures). Either - // shape trips the file — that is the intended signal, not a nuisance: - // whoever lands the mirror must add the `retirementTombstone()` half for - // `triggerIcon` at the same time, and flip these controls rather than - // delete them into a vacuum. - const result = twin.safeParse({ + 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. + const result = ChatbotSchema.safeParse({ ...node, floatingConfig: { title: 'Chat', triggerIcon: 'Sparkles' }, }); @@ -173,7 +165,7 @@ describe.each([ }); it('a live `floatingConfig` parses green too — the non-vacuity control', () => { - const result = twin.safeParse({ + const result = ChatbotSchema.safeParse({ ...node, floatingConfig: { title: 'Chat', triggerSize: 56 }, }); @@ -181,16 +173,50 @@ describe.each([ }); it('the mirror really has no `floatingConfig` key at all', () => { - // The load-bearing fact behind everything above, asserted rather than - // assumed: a key the mirror declares would appear in its shape. - // - // ⚠️ This is also the assertion the objectui#6152 TRIPWIRE fires through: - // under a house-style non-strict mirror it reds HERE and nowhere else in - // this file. The measurement is recorded at that comment, above. - const shape = (twin as unknown as { shape: Record }).shape; + // 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. + 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.messages).toBeDefined(); }); }); + +describe('a `chatbot-floating` node REFUSES `floatingConfig.triggerIcon` at runtime (objectui#6152 round 4 minted the mirror)', () => { + const node = { + type: 'chatbot-floating', + messages: [{ id: 'm1', role: 'user' as const, content: 'hi' }], + }; + + it('`floatingConfig.triggerIcon` is refused at its own path, and the refusal says the key was never read', () => { + // The flipped tripwire: this line used to assert `success: true`. + const result = ChatbotFloatingSchema.safeParse({ + ...node, + floatingConfig: { title: 'Chat', triggerIcon: 'Sparkles' }, + }); + expect(result.success).toBe(false); + const issues = (result.error?.issues ?? []).filter((i) => i.path.join('.') === 'floatingConfig.triggerIcon'); + expect(issues.length, JSON.stringify(result.error?.issues)).toBe(1); + expect(issues[0].code).toBe('invalid_type'); + expect(issues[0].message).toContain('objectui#7654'); + }); + + it('a live `floatingConfig` parses green — the non-vacuity control', () => { + const result = ChatbotFloatingSchema.safeParse({ + ...node, + floatingConfig: { title: 'Chat', triggerSize: 56 }, + }); + expect(result.success).toBe(true); + }); + + it('the mirror declares `floatingConfig`, and its `triggerIcon` is a MEMBER tombstone, not an undeclared key', () => { + const shape = (ChatbotFloatingSchema as unknown as { shape: Record }).shape; + expect(shape.floatingConfig).toBeDefined(); + const inner = (shape.floatingConfig as { unwrap: () => { shape: Record } }).unwrap().shape; + expect(inner.triggerIcon).toBeDefined(); + // Lit control: a live member sits beside it. + expect(inner.triggerSize).toBeDefined(); + }); +}); diff --git a/packages/types/src/__tests__/spec-report.test.ts b/packages/types/src/__tests__/spec-report.test.ts index 4783eca275..0ead2093b1 100644 --- a/packages/types/src/__tests__/spec-report.test.ts +++ b/packages/types/src/__tests__/spec-report.test.ts @@ -79,7 +79,10 @@ describe('SpecReport bridge', () => { expect(legacy.fields?.[0].name).toBe('email'); }); - it('collapses joined report type to tabular for legacy renderer', () => { + it('writes no `reportType` — the key is retired and nothing read it (objectui#6152)', () => { + // The adapter used to write the spec `type` as `reportType` (collapsing `joined` + // to `tabular`). No reader existed, so the key was retired on both faces of + // `ReportComponentSchema` and this write was removed with it. const report = SpecReport.create({ name: 'joined_demo', label: 'Joined', @@ -87,10 +90,12 @@ describe('SpecReport bridge', () => { blocks: [{ name: 'b1', label: 'Block 1', dataset: 'account_ds', values: ['name'] }], } as never); const legacy = specReportToPresentation(report); - expect(legacy.reportType).toBe('tabular'); + expect('reportType' in legacy).toBe(false); + // Lit control: the same conversion still writes the keys the viewer reads. + expect(legacy.title).toBe('Joined'); }); - it('preserves matrix report type and maps rows to groupBy', () => { + it('maps a matrix report\'s rows to groupBy', () => { const report = SpecReport.create({ name: 'matrix_demo', label: 'Matrix', @@ -100,7 +105,7 @@ describe('SpecReport bridge', () => { values: ['amount_sum'], }); const legacy = specReportToPresentation(report); - expect(legacy.reportType).toBe('matrix'); + expect('reportType' in legacy).toBe(false); expect(legacy.groupBy?.[0].field).toBe('region'); }); diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 4b7a9a5540..f137d3bdbc 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -364,7 +364,19 @@ * 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`. - * - **9 entries** in `UnmirroredDeclared`, **44 keys** across them — 12 / 62 until + * - **6 entries** in `UnmirroredDeclared`, **20 keys** across them — 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 + * the three chatbot faces spell it once) and `ChatbotFloatingSchema.floatingConfig` + * (a restatement of `FloatingChatbotConfig` carrying the `triggerIcon` tombstone's + * runtime half). FOUR were RETIRED on both faces, each a tombstone and so a mirrored + * member: `LabelSchema.content` (a third spelling of the label text, the entry's only + * key), `ReportComponentSchema`'s `chartConfig` and `reportType` (the entry's whole + * content) and `DetailViewSchema.autoDiscoverRelated`, all three re-measured ZERO-READ. + * EIGHTEEN were RECLASSIFIED BY NAME into `RuntimeOnlyDeclared` — see that bullet + * below: fourteen of `DataTableSchema`'s keys and four of `DetailViewSchema`'s, the + * latter emptying that entry with the retirement beside them; 12 / 62 until * objectui#6152 round 3 closed THREE entries and shrank two more, eighteen keys by * two different routes that must not be read as one. SIXTEEN were MIRRORED, each * measured READ by a type-checker census: `PaginationSchema.currentPage` (the entry's @@ -439,7 +451,14 @@ * 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`, **9 keys** across them — 3 / 7 + * - **4 entries** in `RuntimeOnlyDeclared`, **27 keys** across them — 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` + * keys (host-fetched runtime data, and `recordNavigation`'s required callback). + * ⚠️ The OTHER side of `UnmirroredDeclared` losing the same eighteen — one + * reclassification seen from both sides, ⛔ not new debt, and nothing mirrored by it. + * The entry count held; it was 3 / 7 * until objectui#6152 round 3 filed `objectql.zod.ts#ObjectFormSchema`'s `open` and * `submitHandler` here: a NEW entry, and the first keys this ledger holds that are * NOT callback-shaped — admitted by NAME through `RuntimeOnlyNamedAllowList`, each @@ -481,8 +500,11 @@ * 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. - * **2 of the 4** are a subset of the **9** pairs above, so - * the union of the two unmirrored ledgers is **11** pairs, being **9** from `UnmirroredDeclared` plus **2** recorded ONLY in `RuntimeOnlyDeclared`. + * **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`. + * ⭐ 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. * ⭐ objectui#6152 round 3 reopened the difference, by the route predicted below: * two pairs became runtime-only and nothing else. `ObjectFormSchema`'s whole * unmirrored debt was its two runtime slots, now filed by name; `FormSchema`'s @@ -498,11 +520,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 first `2` (a - * quantity of its own — how many entries the two unmirrored ledgers share), the - * `11` (the union) and the second `2` (the pairs recorded only as runtime-only), - * and three RESTATEMENTS of counts already stated above — the `4` and both `9`s. - * (This list was amended by objectui#6152 round 3, which reshaped the sentence.) + * ⚠️ Every live figure in these two sentences is pinned: the `1` (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.) * 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 @@ -2516,7 +2539,7 @@ interface KnownDrift { * * objectui#6058 seeded this ledger at **121 keys**, and — on ONE line, because the * pin below reads this sentence off disk — - * **44 keys** is what this ledger records today. + * **20 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; @@ -2543,7 +2566,14 @@ interface KnownDrift { * `RuntimeOnlyDeclared` by NAME and, in the same change, MIRRORED sixteen keys across * four other entries (`PaginationSchema`, `FormSchema`, `ReportComponentSchema`, * `DetailViewSchema`), each measured READ by a type-checker census — two routes, and - * the two must not be added up as one repair. + * the two must not be added up as one repair. Then objectui#6152 round 4 moved + * twenty-four keys by three routes: two MIRRORED (`ChatbotSchema.requestBody`, + * `ChatbotFloatingSchema.floatingConfig`), four RETIRED on both faces + * (`LabelSchema.content`, `ReportComponentSchema`'s `chartConfig` / `reportType`, + * `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. * * ## The decomposition of "121" — a reading at NAMED REVISIONS, so it cannot rot * @@ -2594,7 +2624,8 @@ interface KnownDrift { * * ⚠️ Today's figure is NOT 85. Two keys entered this ledger AFTER the 121 — * `ChatbotFloatingSchema`'s `displayMode` and `floatingConfig`, seeded by objectui#7655 - * — so the survivors plus those two are what the live figure above counts. + * — and one of them is still here (objectui#6152 round 4 mirrored `floatingConfig`), so + * the survivors plus `displayMode` are what the live figure above counts. * ⛔ Do not update the 85 when the ledger moves: it is a reading at `ed7178bf3` and * it stays one. The live figure is the pinned one, and it is the ONLY figure in this * docstring that moves. @@ -2706,7 +2737,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 (6 entries, 30 keys)** — plain omissions from a hand-written mirror. + * - **LOCAL (3 entries, 6 keys)** — plain omissions from a hand-written mirror. + * 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; + * `ChatbotSchema` and `ChatbotFloatingSchema` each had one key MIRRORED, and + * `DataTableSchema` had fourteen RECLASSIFIED by name. No entry crossed between the + * halves, so the SPEC-DERIVED half did not move. * It was 8 / 46 until objectui#6152 round 3 MIRRORED sixteen keys here, each * measured READ: `PaginationSchema.currentPage` and all eight of `FormSchema`'s * layout keys, each its entry's whole content, so two entries left; and @@ -2773,10 +2810,14 @@ interface KnownDrift { * by two routes: it RECLASSIFIED `ObjectFormSchema`'s last two keys into * `RuntimeOnlyDeclared` by name, so the SPEC-DERIVED half lost that entry, and it * MIRRORED sixteen LOCAL keys, emptying `PaginationSchema`'s and `FormSchema`'s entries - * and shrinking `ReportComponentSchema`'s and `DetailViewSchema`'s. The seeded pair is - * no longer among them, and the ledger now totals — on ONE line, because the pin below + * and shrinking `ReportComponentSchema`'s and `DetailViewSchema`'s. objectui#6152 round 4 + * then moved only the LOCAL half, by three routes: it RETIRED `LabelSchema.content`, + * `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 * reads this sentence off disk — - * **9 entries / 44 keys** — 3 / 14 spec-derived, 6 / 30 local. + * **6 entries / 20 keys** — 3 / 14 spec-derived, 3 / 6 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` @@ -2812,28 +2853,45 @@ interface KnownDrift { */ 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. `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. + * LOCAL. It was three until objectui#6152 round 4 MIRRORED `requestBody`, READ by all + * three chatbot registrations (`body: schema.requestBody`), through the shared pick in + * `complex.zod.ts`, so the three faces spell it once. (This note used to open by + * 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: + * + * - `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. */ - 'complex.zod.ts#ChatbotSchema': 'displayMode' | 'floatingConfig' | 'requestBody'; + 'complex.zod.ts#ChatbotSchema': 'displayMode' | 'floatingConfig'; /** * LOCAL — a pair born ledgered (objectui#7655) with the two keys the floating - * face declares alongside `ChatbotSchema`, in the same state the entry above - * 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 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. + * face declares alongside `ChatbotSchema`. It was two until objectui#6152 round 4 + * MIRRORED `floatingConfig`, READ by this registration alone + * (`floatingConfig={schema.floatingConfig}`), through a module-private restatement + * of `FloatingChatbotConfig` whose six live members `FloatingChatbot` destructures; + * that restatement carries the `triggerIcon` tombstone's runtime half, as the + * objectui#7654 tripwire asked (`floating-chatbot-trigger-icon-retired.test.ts`). + * + * `displayMode` is RETIRED (objectui#7654, maintainer ruling B, 2026-09-05): a + * `?: never` tombstone on this face and on `ChatbotSchema` alike, unmirrored on + * both twins BY DECISION — round 4 kept the refusal TypeScript-only under that + * ruling, for the stored documents the entry above names. ⛔ Not a waiver: every + * OTHER key this pair declares is mirrored, and a second key here reddens the pair + * like growth on any other entry. */ - 'complex.zod.ts#ChatbotFloatingSchema': 'displayMode' | 'floatingConfig'; + 'complex.zod.ts#ChatbotFloatingSchema': 'displayMode'; // `complex.zod.ts#DashboardComponentSchema` recorded `title` here (SPEC-DERIVED) // until objectui#7623 RETIRED the declaration — the objectui#7129 route, not a // mirror edit: the spec's strict `DashboardSchema` refuses a root `title` outright, @@ -2855,15 +2913,29 @@ interface UnmirroredDeclared { // 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, and still the largest single entry at 17. It was 29: the twelve `on*` keys - * are in `RuntimeOnlyDeclared` below (objectui#6152). `rowActions` is in - * `KnownDrift` above — the mirror does declare that one, disjointly. + * 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': - | 'disableInnerScroll' | 'editable' | 'manualPagination' | 'manualSearch' | 'manualSorting' - | 'page' | 'rowActionDefs' | 'rowClassName' - | 'rowCount' | 'rowStyle' | 'search' | 'selectionResetKey' | 'selectionStyle' - | 'showAddRow' | 'showSelectionCount' | 'singleClickEdit' | 'sort'; + 'data-display.zod.ts#DataTableSchema': 'editable' | 'selectionStyle' | 'singleClickEdit'; /** * 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 @@ -2889,17 +2961,14 @@ interface UnmirroredDeclared { // refuses by name) and records nothing here. ⭐ That is why this move shrank the // entry count and the key total together where objectui#7762's did not: one key // off a one-key entry empties it, one key off a fifteen-key entry does not. - /** - * LOCAL. ⚠️ Not a plain omission, measured by objectui#6152 round 3 and left here - * on purpose: `content` is the THIRD spelling of the label's text. The `label` - * renderer reads `schema.text || schema.label || schema.content`, the mirror already - * declares `text` and `label`, and the interface calls both `label` and `content` - * "legacy". Mirroring it would sanction a third spelling of one fact (AGENTS.md - * #0.1), so its route is a canonical-spelling ruling — the one `PaginationSchema`'s - * `currentPage` got — ⛔ not a mirror edit. Authored: once, in a components test; the - * catalog writes `label` (and `text`), never `content`. - */ - 'form.zod.ts#LabelSchema': 'content'; + // `form.zod.ts#LabelSchema` recorded `content` here (LOCAL) — the entry's ONLY key — + // until objectui#6152 round 4 RETIRED it on both faces (the seat's answer B to round + // 3's open question): `content` was the THIRD spelling of the label's text, which the + // `label` renderer read last (`schema.text || schema.label || schema.content`). It is + // `?: never` on the interface and a `retirementTombstone()` on the mirror now, and the + // renderer's third read is dropped. A tombstone is a mirrored member, so the entry + // went with its one key. No document authored it (once, in a components test, which + // now authors `text`). Which of `text` / `label` is canonical was ⛔ not decided there. // `navigation.zod.ts#PaginationSchema` recorded `currentPage` here (LOCAL) — the // entry's ONLY key — until objectui#6152 round 3 MIRRORED it, on the ruling that // made it the canonical spelling (the seat's answer to round 1's open question, @@ -2957,49 +3026,26 @@ interface UnmirroredDeclared { | 'grouping' | 'navigation' | 'operations' | 'reorderableColumns' | 'resizableColumns' | 'rowColor' | 'rowHeight' | 'singleClickEdit'; - /** - * LOCAL. It was three until objectui#6152 round 3 MIRRORED `conditionalFormatting`, - * which `ReportViewer` reads per cell off the report a `report-viewer` node carries. - * - * The two left are NOT mirroring debt in waiting, and ⛔ mirroring them would - * repair nothing: round 3's type-checker census found ZERO reads of either off this - * type, and an authored census over every tracked JSON file and Markdown JSON fence - * found no document writing either. `reportType` has one in-code producer — the - * spec-report converter in `../spec-report.ts` writes it — and no reader; - * `chartConfig` has neither. They are NARROWING candidates under objectui#6170's - * family rule (the exported type follows the measured authored+read set), which - * owes the runtime inertness probe AGENTS.md asks for before a key is called inert; - * round 3 reported them and ⛔ did not narrow. - */ - 'reports.zod.ts#ReportComponentSchema': 'chartConfig' | 'reportType'; - /** - * LOCAL. It was 11 until objectui#6152 round 3 MIRRORED the six CONFIGURATION keys - * `DetailView` reads — `primaryField`, `summaryFields`, `autoTabs`, `defaultTab`, - * `sectionGroups`, `highlightFields` — and 14 before that: the three `on*` keys are - * in `RuntimeOnlyDeclared` below (objectui#6152), where the 2026-07 audit had already - * ruled them. - * - * The five left are NOT the same fact, and round 3 left each on a stated route - * rather than mirroring it: - * - * - `activities`, `comments`, `history` — READ by `DetailView`, but what they - * carry is RUNTIME DATA a host fetches (activity rows, comment rows, audit-log - * entries with a `loading` flag), not configuration: no document authors any of - * them, and the one producer outside tests (the record page's own `history`) - * builds it in code. Whether an authored copy is a supported dialect is the - * round-1 "class (b)" classification question, open on objectui#6152. - * - `recordNavigation` — READ, and its declared value carries a REQUIRED callback - * (`onNavigate: (recordId) => void`), so no JSON document can satisfy it and a - * mirror could only relax or refuse that member. ⛔ A callback is never mirrored; - * the key is handler-shaped without an `on*` spelling, the `submitHandler` case, - * so it needs a ruling to move, ⛔ not a refiling. - * - `autoDiscoverRelated` — ZERO reads (type-checker census, and no untyped read) - * and zero authored: a NARROWING candidate under objectui#6170's family rule, - * reported by round 3 and ⛔ not narrowed. - */ - 'views.zod.ts#DetailViewSchema': - | 'activities' | 'autoDiscoverRelated' | 'comments' - | 'history' | 'recordNavigation'; + // `reports.zod.ts#ReportComponentSchema` recorded three keys here (LOCAL). objectui#6152 + // round 3 MIRRORED `conditionalFormatting`, which `ReportViewer` reads per cell, and + // round 4 RETIRED the other two on both faces under ADR-0049 enforce-or-remove: + // `chartConfig` and `reportType`, re-measured ZERO-READ by a type-checker census (no + // typed read, no untyped read of either name off a report) with no authored document. + // Each is `?: never` on the interface and a `retirementTombstone()` on the mirror, and + // `reportType`'s one in-code producer — `specReportToPresentation` in `../spec-report.ts`, + // whose output nothing read it from — stopped writing it in the same change. Tombstones + // are mirrored members, so the entry went. + // `views.zod.ts#DetailViewSchema` recorded fourteen keys here (LOCAL). The three `on*` + // keys went to `RuntimeOnlyDeclared` at objectui#6152's reclassification; round 3 + // MIRRORED the six configuration keys `DetailView` reads (`primaryField`, + // `summaryFields`, `autoTabs`, `defaultTab`, `sectionGroups`, `highlightFields`); and + // round 4 emptied the entry by two routes that must not be read as one. It MOVED + // `activities`, `comments`, `history` and `recordNavigation` to `RuntimeOnlyDeclared` + // below BY NAME (the seat's answer A), each with its reason on + // `RuntimeOnlyNamedAllowList` — host-fetched runtime data, and a value carrying a + // required callback — ⛔ none mirrored. And it RETIRED `autoDiscoverRelated` on both + // faces under ADR-0049 enforce-or-remove, re-measured ZERO-READ by a type-checker + // census with no authored document and no producer; a tombstone is a mirrored member. } /* ── The runtime-only / non-authorable ledger (objectui#6152) ─────────────── */ @@ -3015,7 +3061,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 **44 keys** today. + * `UnmirroredDeclared` records **20 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 @@ -3154,11 +3200,24 @@ interface RuntimeOnlyDeclared { * spellings, different signatures. objectui#6175 wired persistence WITHOUT * retiring anything, because retiring either spelling is a declared-surface change * and that ruling is still OPEN. Nothing about this entry's membership changed. + * + * ⭐ GREW by fourteen NON-callback keys with objectui#6152 round 4, each filed BY + * NAME with its reason on `RuntimeOnlyNamedAllowList` below, and each come from + * `UnmirroredDeclared` above, whose `DataTableSchema` entry kept three. ⛔ None was + * mirrored and no declaration moved: one reclassification seen from both sides. They + * are what a HOST sets in code on the `data-table` node it builds — `ObjectGrid`'s + * in-code node for twelve, `RelatedList`'s for `rowActionDefs` (and `manualSorting` / + * `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. */ 'data-display.zod.ts#DataTableSchema': | 'onColumnReorder' | 'onPageChange' | 'onPageSizeChange' - | 'onSearchChange' | 'onSortChange'; + | 'onSearchChange' | 'onSortChange' + | 'disableInnerScroll' | 'manualPagination' | 'manualSearch' | 'manualSorting' + | 'page' | 'rowActionDefs' | 'rowClassName' | 'rowCount' | 'rowStyle' + | 'search' | 'selectionResetKey' | 'showAddRow' | 'showSelectionCount' | '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 @@ -3244,8 +3303,20 @@ interface RuntimeOnlyDeclared { * were the two keys whose twin on `crud.zod.ts#DetailSchema` already refused * them. This is also the pair where the audit wrote "a props bag wearing a * schema's clothes". + * + * ⭐ GREW by four NON-callback keys with objectui#6152 round 4 (the seat's answer A + * to round 3's open question), each filed BY NAME with its reason on + * `RuntimeOnlyNamedAllowList` below: `activities`, `comments` and `history` carry + * host-fetched RUNTIME DATA, and `recordNavigation`'s value carries a REQUIRED + * callback. All four came from `UnmirroredDeclared` above, whose `DetailViewSchema` + * entry they emptied together with `autoDiscoverRelated`'s retirement — so this pair + * is now recorded ONLY here, one of the pairs the cross-ledger sentence in the file + * header counts. ⛔ None was mirrored and no declaration moved. objectui#4650's + * two-layer split stays the end state. */ - 'views.zod.ts#DetailViewSchema': 'onTabChange'; + 'views.zod.ts#DetailViewSchema': + | 'onTabChange' + | 'activities' | 'comments' | 'history' | 'recordNavigation'; } /** @@ -3337,6 +3408,43 @@ interface RuntimeOnlyNamedAllowList { */ submitHandler: 'a function slot the form calls instead of the data source write, supplied in code by MasterDetailForm; objectui#6182 rules handlers runtime-only'; }; + /** + * objectui#6152 round 4 — `DataTableSchema`'s code-produced keys (the round-3 class + * the seat named for keys a host produces in code and no document authors). Each was + * measured READ by `data-table` and PRODUCED only in code by a type-checker census, + * 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. + */ + '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'; + 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'; + page: 'controlled current page for manualPagination: set in code by ObjectGrid from its own paging state; authored in no document'; + rowActionDefs: 'host row actions: set in code by RelatedList and rendered only beside its onRowActionDef slot; authored in no document'; + rowClassName: 'a function slot the table calls per row, supplied in code by ObjectGrid from rowColor; JSON has no function value'; + rowCount: 'server match total for manualPagination: set in code by ObjectGrid from its query result; authored in no document'; + rowStyle: 'a function slot the table calls per row, supplied in code by ObjectGrid from conditionalFormatting; JSON has no function value'; + search: 'controlled search term for manualSearch: set in code by ObjectGrid from its own search state; authored in no document'; + 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'; + sort: 'controlled sort for manualSorting: set in code by ObjectGrid and RelatedList from their own sort state; authored in no document'; + }; + /** + * objectui#6152 round 4 — the seat's answer A to round 3's open question: runtime data + * a host fetches, and a value carrying a required callback. Each is READ by + * `DetailView` and no document authors any of them; the one in-tree producer outside + * tests is `RecordDetailView`'s `history`, built in code. objectui#4650's split (an + * authored-metadata type beside a renderer-props type) stays the end state. + */ + 'views.zod.ts#DetailViewSchema': { + activities: 'runtime data a host fetches (activity rows) and passes in code; authored in no document, and no in-tree producer writes it'; + comments: 'runtime data a host fetches (comment rows), read beside the onAddComment slot; authored in no document, and no in-tree producer writes it'; + history: 'host-fetched runtime data: audit-log entries with a loading flag, built in code by RecordDetailView; authored in no document'; + recordNavigation: 'runtime result-set state whose onNavigate member is a REQUIRED function, so no JSON document can supply it'; + }; } /** The allow-listed key names for one pair — `never` for a pair with no row. */ diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index 7fd518e5c2..3ce0d40614 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -1576,7 +1576,10 @@ export interface ChatbotSchema extends BaseSchema { * 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. + * `retirementTombstone()` half at that time. objectui#6152 round 4 took the + * question under that ruling and kept the refusal TypeScript-only: stored + * designer documents carry `displayMode: 'floating'`, so no arm is minted + * and the key stays unmirrored on both twins by decision, not as debt. * * @deprecated Not part of this contract — the value was inert. The node * `type` selects the presentation. @@ -2011,6 +2014,16 @@ export interface FloatingChatbotConfig { * it, so `triggerIcon`'s refusal is TYPE-LEVEL ONLY. Runtime parse behaviour * is unchanged. * + * ⚠️ AMENDED by objectui#6152 round 4, which minted that mirror. The + * `chatbot-floating` twin now declares `floatingConfig` through a + * module-private restatement of this interface in `zod/complex.zod.ts`, and + * that restatement carries the `retirementTombstone()` half for this key, so + * on a `chatbot-floating` node `floatingConfig.triggerIcon` is refused at + * runtime too. `ChatbotSchema.floatingConfig` stays unmirrored — the `chatbot` + * registration never reads it — so on a `chatbot` node the refusal is still + * type-level only. The paragraph above is the reading this retirement landed + * on. + * * ## 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/form.ts b/packages/types/src/form.ts index 36b5d95d9a..ca7a83d122 100644 --- a/packages/types/src/form.ts +++ b/packages/types/src/form.ts @@ -2284,9 +2284,25 @@ export interface LabelSchema extends BaseSchema { */ label?: string; /** - * Legacy content property + * RETIRED (objectui#6152 round 4, ADR-0049) — a THIRD spelling of the label's + * text, beside {@link LabelSchema.text} and {@link LabelSchema.label}. + * + * The `label` renderer read it last, as `schema.text || schema.label || + * schema.content`, and that read is dropped in the same change. No document + * authored it: the authored census over every tracked JSON file, Markdown JSON + * fence and `type: 'label'` object literal found it once, in a components test, + * against `label` 48 times and `text` 5 times. Honouring three spellings keeps + * one fact writable three ways (AGENTS.md #0.1), so it is retired at once, with + * no alias window. Which of `text` / `label` is canonical is a separate question + * and is not decided here. + * + * `?: never` rather than deleted: this interface carries `BaseSchema`'s index + * signature, so a deleted member would type-check silently, while a tombstone + * makes presence a `tsc` error, and the zod twin refuses the key by name. + * + * @deprecated RETIRED (objectui#6152) — write the text as `text` (or `label`). */ - content?: string; + content?: never; /** * HTML for attribute */ @@ -2301,8 +2317,9 @@ export interface LabelSchema extends BaseSchema { * `packages/plugin-chatbot/src/renderer.tsx` is the kind of prefix hit grep * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: - * `content`, `label`, `text` (in - * `packages/components/src/renderers/form/label.tsx`). + * `label`, `text` (in + * `packages/components/src/renderers/form/label.tsx`; its third read, + * `content`, is retired by objectui#6152 round 4). * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here @@ -2327,8 +2344,9 @@ export interface LabelSchema extends BaseSchema { * `packages/plugin-chatbot/src/renderer.tsx` is the kind of prefix hit grep * scores. Every read is filed under the TYPE of the object it is read from; * this declaration carries none. What the renderer DOES read off this node: - * `content`, `label`, `text` (in - * `packages/components/src/renderers/form/label.tsx`). + * `label`, `text` (in + * `packages/components/src/renderers/form/label.tsx`; its third read, + * `content`, is retired by objectui#6152 round 4). * * Before objectui#9256 tombstoned them here, `body` and `children` were both * inherited-and-optional from {@link BaseSchema} — so authoring either here diff --git a/packages/types/src/reports.ts b/packages/types/src/reports.ts index f8a149fd69..92108b793d 100644 --- a/packages/types/src/reports.ts +++ b/packages/types/src/reports.ts @@ -348,7 +348,9 @@ export interface ReportExportConfig { * * The mirror was missing `joined`, so a spec-valid joined report did not * type-check against `ReportComponentSchema.reportType`. Derived rather than restated so - * a report format the spec adds cannot go missing here. + * a report format the spec adds cannot go missing here. (`ReportComponentSchema.reportType` + * itself is retired by objectui#6152 round 4 — nothing read it; this alias stays, as the + * spec's report-type vocabulary.) */ export type ReportType = z.infer; @@ -369,9 +371,23 @@ export interface ReportComponentSchema extends BaseSchema { description?: string; /** - * Report type (tabular, summary, matrix) + * RETIRED (objectui#6152 round 4, ADR-0049 enforce-or-remove) — declared as the + * report's layout type, and read by nothing. + * + * A type-checker census over every package's sources found ZERO reads of this + * member (and no untyped read of the name), and no document authored it. Its one + * producer was in code — `specReportToPresentation` in `./spec-report.ts` wrote it + * onto the presentation it hands `report-viewer`, which never looked at it — and + * that write is removed in the same change. A spec report's own `type` is what + * selects a dataset-bound report's layout; this presentation key selected nothing. + * + * `?: never` rather than deleted: this interface carries `BaseSchema`'s index + * signature, so a deleted member would type-check silently, while a tombstone + * makes presence a `tsc` error, and the zod twin refuses the key by name. + * + * @deprecated RETIRED (objectui#6152) — nothing reads it. Delete the key. */ - reportType?: ReportType; + reportType?: never; /** * Data source configuration — RETIRED (objectui#6121, maintainer ruling of @@ -500,13 +516,21 @@ export interface ReportComponentSchema extends BaseSchema { }>; /** - * Chart configuration (visual chart editor output) + * RETIRED (objectui#6152 round 4, ADR-0049 enforce-or-remove) — declared as + * "visual chart editor output", and read and written by nothing. + * + * A type-checker census over every package's sources found ZERO reads of this + * member on a report (the `chartConfig` a dashboard widget carries is a different + * key on a different type), no in-code producer, and no document authoring it, + * so an authored value configured nothing. + * + * `?: never` rather than deleted: this interface carries `BaseSchema`'s index + * signature, so a deleted member would type-check silently, while a tombstone + * makes presence a `tsc` error, and the zod twin refuses the key by name. + * + * @deprecated RETIRED (objectui#6152) — nothing reads it. Delete the key. */ - chartConfig?: { - chartType?: string; - xAxisField?: string; - yAxisFields?: string[]; - }; + chartConfig?: never; /** * REFUSED BY NAME (objectui#9256, ADR-0049) — `report` reads NEITHER * content channel; see `children` below for the measurement. diff --git a/packages/types/src/spec-report.ts b/packages/types/src/spec-report.ts index 512e5b333f..90f14b9fac 100644 --- a/packages/types/src/spec-report.ts +++ b/packages/types/src/spec-report.ts @@ -168,7 +168,8 @@ export interface LegacyReportPresentationLike { type: 'report'; title?: string; description?: string; - reportType?: 'tabular' | 'summary' | 'matrix'; + // No `reportType`: `ReportComponentSchema.reportType` is retired (objectui#6152 + // round 4) — nothing read it, and this adapter was its only writer. fields?: Array<{ name: string; label?: string; @@ -207,15 +208,10 @@ function resolveLabel(label: unknown, fallback: string): string { * aggregation / sort / granularity for the dataset-aware renderer to resolve. */ export function specReportToPresentation(report: SpecReport): LegacyReportPresentationLike { - const reportType = (report.type ?? 'tabular') as SpecReportTypeName; - - // The legacy schema only knows tabular/summary/matrix; collapse `joined` to - // `tabular` so it at least renders something. A real `joined` renderer is a - // separate milestone. - const legacyReportType: 'tabular' | 'summary' | 'matrix' = - reportType === 'joined' || reportType === 'tabular' - ? 'tabular' - : reportType; + // The spec report's `type` is deliberately NOT carried over. This adapter used to + // write it as `reportType` (collapsing `joined` to `tabular`), and nothing on the + // presentation path ever read it, so the key was retired on both faces of + // `ReportComponentSchema` and this write went with it (objectui#6152 round 4). // `values` are measure names defined in the dataset; the report only refers // to them. Label / aggregation are resolved downstream from the dataset. @@ -235,7 +231,6 @@ export function specReportToPresentation(report: SpecReport): LegacyReportPresen description: report.description ? resolveLabel(report.description, '') : undefined, - reportType: legacyReportType, fields, groupBy: groupBy.length > 0 ? groupBy : undefined, }; diff --git a/packages/types/src/views.ts b/packages/types/src/views.ts index 1ddb9a0f8b..067bd103e1 100644 --- a/packages/types/src/views.ts +++ b/packages/types/src/views.ts @@ -931,12 +931,23 @@ export interface DetailViewSchema extends BaseSchema { emptyText?: string; }; /** - * When true, auto-discover related lists from objectSchema reference fields - * (lookup, master_detail) when no explicit `related` is provided. - * Requires a DataSource with getObjectSchema. - * @default false + * RETIRED (objectui#6152 round 4, ADR-0049 enforce-or-remove) — declared as + * "auto-discover related lists from reference fields", and read by nothing. + * + * A type-checker census over every package's sources found ZERO reads of this + * member (and no untyped read of the name), and the authored census found no + * document and no in-code producer writing it. `DetailView` never discovered + * related lists from it; the related-list capability is the protocol-governed + * `record:related_list` block. A key nothing honours is retired, not mirrored. + * + * `?: never` rather than deleted: this interface carries `BaseSchema`'s index + * signature, so a deleted member would type-check silently, while a tombstone + * makes presence a `tsc` error, and the zod twin refuses the key by name. + * + * @deprecated RETIRED (objectui#6152) — nothing reads it. Author a + * `record:related_list` block for a related list. */ - autoDiscoverRelated?: boolean; + autoDiscoverRelated?: never; /** * When true, automatically generate Details/Related/Activity tabs * when no explicit `tabs` are configured. Sections go into the Details tab, diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index da1964cf89..0f9c5f5c6e 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -891,6 +891,12 @@ export const ChatbotSchema = BaseSchema.extend({ model: z.string().optional().describe('AI model identifier'), streamingEnabled: z.boolean().optional().describe('Enable streaming responses'), headers: z.record(z.string(), z.string()).optional().describe('Additional API headers'), + // objectui#6152 round 4 — declared on the interface all along and READ by all three + // registrations (`body: schema.requestBody`, forwarded to the chat runtime), but + // mirrored only on the two twins below. Declared HERE so the shared pick carries it + // and the three faces spell it once; `body` below points authors at this key. + requestBody: z.record(z.string(), z.unknown()).optional() + .describe('Additional body parameters sent with each API request (forwarded to the chat runtime as its `body` option)'), body: retirementTombstone( 'REFUSED (objectui#8572, ADR-0049) — `body` is the CONTENT slot on every other component, and ' + '`chatbot` reads NEITHER content channel; this arm restated it as the chat API body params, the one ' @@ -944,12 +950,12 @@ export const ChatbotSchema = BaseSchema.extend({ * Not exported: it is a census, not a mirror, and the parity census in * `__tests__/zod-mirror-parity.test.ts` registers `export const`s only. * - * `requestBody` is deliberately NOT in this pick, and cannot be: the two twins - * below mirror the key the renderer actually reads, while `ChatbotSchema` above - * does not declare it at all — it rides through `.passthrough()` there, the - * state `__tests__/zod-mirror-parity.test.ts` records under - * `UnmirroredDeclared`. Picking a key this shape does not hold would pick - * nothing. + * `requestBody` is in this pick since objectui#6152 round 4. Before it, + * `ChatbotSchema` above did not declare the key at all — it rode through + * `.passthrough()` there, recorded under `UnmirroredDeclared` in + * `__tests__/zod-mirror-parity.test.ts` — so each twin below carried its own + * copy of the arm. Now `ChatbotSchema` declares it and all three faces share + * this one spelling, as `../complex.ts`'s `ChatbotSharedKey` always said. * * ⚠️ This note used to end "ruling on `ChatbotSchema`'s own `body` arm is a * separate question and is not decided here". It has been decided: ruling A on @@ -967,6 +973,7 @@ const ChatbotSharedMirrorShape = ChatbotSchema.pick({ model: true, streamingEnabled: true, headers: true, + requestBody: true, onError: true, showTimestamp: true, userAvatarUrl: true, @@ -980,9 +987,6 @@ const ChatbotSharedMirrorShape = ChatbotSchema.pick({ }).shape; /** The arms `chatbot-enhanced` and `chatbot-floating` share beyond the pick above. */ -const chatbotRequestBodyArm = () => - z.record(z.string(), z.unknown()).optional() - .describe('Additional body parameters sent with each API request (forwarded to the chat runtime as its `body` option)'); const chatbotEnableMarkdownArm = () => z.boolean().optional().describe('Render assistant messages as markdown (default true)'); const chatbotEnableFileUploadArm = () => @@ -1000,7 +1004,6 @@ const chatbotOnClearArm = () => export const ChatbotEnhancedSchema = BaseSchema.extend({ type: z.literal('chatbot-enhanced'), ...ChatbotSharedMirrorShape, - requestBody: chatbotRequestBodyArm(), maxToolRoundtrips: ChatbotSchema.shape.maxToolRoundtrips, maxHeight: ChatbotSchema.shape.maxHeight, processVisibility: ChatbotSchema.shape.processVisibility, @@ -1041,39 +1044,67 @@ export const ChatbotEnhancedSchema = BaseSchema.extend({ ), }); +/** + * objectui#6152 round 4 — `../complex.ts`'s `FloatingChatbotConfig`, restated + * member for member: the trigger and panel geometry `FloatingChatbot` destructures + * (`position`, `defaultOpen`, `panelWidth`, `panelHeight`, `title`, `triggerSize`), + * each measured READ by a type-checker census. Module-private: a nested shape of + * the `chatbot-floating` arm below, not a registered pair — the parity ratchet + * compares it through that arm's `floatingConfig` member. + * + * `triggerIcon` is the RUNTIME half of objectui#7654's tombstone, which that card + * left type-level only because this mirror did not exist; its tripwire + * (`../__tests__/floating-chatbot-trigger-icon-retired.test.ts`) asked whoever + * minted the mirror to add it here, at the same time. + */ +const FloatingChatbotConfigSchema = z.object({ + position: z.enum(['bottom-right', 'bottom-left']).optional() + .describe('Corner the trigger button sits in (default bottom-right)'), + defaultOpen: z.boolean().optional().describe('Open the panel on mount (default false)'), + panelWidth: z.number().optional().describe('Panel width in pixels (default 400)'), + panelHeight: z.number().optional().describe('Panel height in pixels (default 520)'), + title: z.string().optional().describe('Panel header title (default "Chat")'), + triggerIcon: retirementTombstone( + 'RETIRED (objectui#7654, ADR-0049) — never read: the floating trigger renders a fixed icon and takes ' + + 'no icon prop, so no authored value changes it. Delete the key.', + ), + triggerSize: z.number().optional().describe('Trigger button size in pixels (default 56)'), +}); + /** * Chatbot Floating Schema - `chatbot-floating` component (objectui#7655). * - * Zod twin of `../complex.ts`'s `ChatbotFloatingSchema`. Two of that - * declaration's keys are deliberately NOT mirrored, and the parity ledger - * records both under `UnmirroredDeclared` for this pair — exactly as it - * records the same two keys for `ChatbotSchema`, which declares them too: + * 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: * - * - `floatingConfig` — `FloatingChatbotConfig` has no Zod mirror at all; - * 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` — RETIRED by objectui#7654 (maintainer ruling B, * 2026-09-05): the node `type` is the one selector of presentation. The * 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 + * designer control and seed removed. The refusal stays TypeScript-only: + * stored designer documents carry `displayMode: 'floating'`, and + * objectui#6152 round 4 kept that under the ruling, so this twin has no + * arm 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. + * It rides through `BaseSchema`'s `.passthrough()` unvalidated, byte for + * byte as it does on `ChatbotSchema`'s twin. + * + * `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. */ export const ChatbotFloatingSchema = BaseSchema.extend({ type: z.literal('chatbot-floating'), ...ChatbotSharedMirrorShape, - requestBody: chatbotRequestBodyArm(), maxToolRoundtrips: ChatbotSchema.shape.maxToolRoundtrips, enableMarkdown: chatbotEnableMarkdownArm(), enableFileUpload: chatbotEnableFileUploadArm(), onClear: chatbotOnClearArm(), + floatingConfig: FloatingChatbotConfigSchema.optional() + .describe('Trigger button and panel geometry for the floating chat'), // Declared HERE rather than inherited, because objectui#6771's retirement // of `BaseSchema.body` refuses the key with a message naming `children` — // which is the right remedy on every node except this family, where diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts index 1301034f97..99964ce8b5 100644 --- a/packages/types/src/zod/form.zod.ts +++ b/packages/types/src/zod/form.zod.ts @@ -754,20 +754,28 @@ export const LabelSchema = BaseSchema.extend({ type: z.literal('label'), text: z.string().optional().describe('Label text'), label: z.string().optional().describe('Label text (alternative)'), + // objectui#6152 round 4 — a THIRD spelling of the label's text, retired on both faces + // (the TypeScript twin is `content?: never`) and dropped from the renderer's read. + // A tombstone, not a deletion: `BaseSchema` is `.passthrough()`, so a deleted arm + // would KEEP an authored value in silence. + content: retirementTombstone( + 'RETIRED (objectui#6152, ADR-0049) — `content` was a third spelling of the label text, and the ' + + '`label` renderer no longer reads it. Write the text as `text` (or `label`) instead.', + ), htmlFor: z.string().optional().describe('Associated input ID'), body: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `label` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `content`, `label`, `text`.', + + 'What it renders instead: `label`, `text`.', ), children: retirementTombstone( 'REFUSED (objectui#9256, ADR-0049) — `label` reads NEITHER content channel: measured with the ' + 'TypeScript type checker across all 24 registering packages, no renderer read consumes `body` or ' + '`children` for this node, and `SchemaRenderer` strips both out of the props bag it spreads. An ' + 'authored value therefore rendered NOTHING — no render-time error or warning and no element; only the parser tier\'s `not-a-container` warning (objectui#9910) noticed it. ' - + 'What it renders instead: `content`, `label`, `text`.', + + 'What it renders instead: `label`, `text`.', ), }); diff --git a/packages/types/src/zod/reports.zod.ts b/packages/types/src/zod/reports.zod.ts index 6e8babd278..754da3c0b6 100644 --- a/packages/types/src/zod/reports.zod.ts +++ b/packages/types/src/zod/reports.zod.ts @@ -197,6 +197,21 @@ export const ReportComponentSchema = BaseSchema.extend({ backgroundColor: z.string().optional().describe('Cell background colour when the rule matches'), textColor: z.string().optional().describe('Cell text colour when the rule matches'), })).optional().describe('Conditional formatting rules, evaluated per cell; the first matching rule styles it'), + // objectui#6152 round 4 — two keys the interface declared and NOTHING read (a + // type-checker census over every package's sources, no untyped read, no authored + // document). Retired on both faces under ADR-0049 enforce-or-remove; tombstones + // rather than deletions because `BaseSchema` is `.passthrough()`, so a deleted arm + // would KEEP an authored value in silence. `reportType`'s one in-code producer, the + // spec-report converter in `../spec-report.ts`, stops writing it in the same change. + reportType: retirementTombstone( + 'RETIRED (objectui#6152, ADR-0049) — nothing ever read `reportType` off a report: the presentation ' + + 'renderer draws the same sections whatever it says. Delete the key; a dataset-bound report\'s layout ' + + 'is the spec report\'s own `type`.', + ), + chartConfig: retirementTombstone( + 'RETIRED (objectui#6152, ADR-0049) — nothing ever read `chartConfig` off a report, so an authored ' + + 'value configured nothing. Delete the key.', + ), // objectui#9256 (family-D re-measure): the renderer reads NEITHER content channel, so both are // refused by name here as on the TypeScript twin, each kept a MEMBER. body: retirementTombstone(REPORT_NEITHER_CHANNEL), diff --git a/packages/types/src/zod/views.zod.ts b/packages/types/src/zod/views.zod.ts index 8622dadfef..2a4cbf7a31 100644 --- a/packages/types/src/zod/views.zod.ts +++ b/packages/types/src/zod/views.zod.ts @@ -266,7 +266,10 @@ export const DetailViewSchema = BaseSchema.extend({ * ⚠️ NOT the nested `recordNavigation.onNavigate`, which is a DIFFERENT key * at a different path with a different signature — `(recordId) => void`, the * prev/next result-set walker. This refusal is about the MEMBER; the nested - * one is untouched on both faces and stays authorable where it lives. + * one is untouched on both faces. It was never authorable where it lives: its + * value is a REQUIRED function, which no JSON document can supply, so + * objectui#6152 round 4 filed `recordNavigation` as a runtime slot by name + * (`RuntimeOnlyNamedAllowList` in `../__tests__/zod-mirror-parity.test.ts`). */ onNavigate: handlerKeyRefusal('onNavigate', 'runtime-slot', 'SPA navigation callback'), /** @@ -282,7 +285,9 @@ export const DetailViewSchema = BaseSchema.extend({ * * ⚠️ `comments` is deliberately NOT declared here, for the reason its twin * records: it is not a handler key, declaring it is an accept-set decision of - * its own, and this card's rows are the handler keys. + * its own, and this card's rows are the handler keys. objectui#6152 round 4 + * made that decision: `comments` (with `activities` and `history`) is runtime + * data a host fetches, filed by name as runtime-only, so it stays unmirrored. */ onAddComment: handlerKeyRefusal('onAddComment', 'runtime-slot', 'New comment callback'), showEdit: z.boolean().optional().describe('Show edit button'), @@ -308,6 +313,16 @@ export const DetailViewSchema = BaseSchema.extend({ .describe('Groups of sections, each rendered under a collapsible header'), highlightFields: z.array(DetailViewHighlightFieldSchema).optional() .describe('Key fields shown prominently in a highlight banner below the header'), + // objectui#6152 round 4 — declared on the interface and read by NOTHING (a + // type-checker census over every package's sources, no untyped read, no authored + // document, no in-code producer). Retired on both faces under ADR-0049 + // enforce-or-remove; a tombstone rather than a deletion because `BaseSchema` is + // `.passthrough()`, so a deleted arm would KEEP an authored value in silence. + autoDiscoverRelated: retirementTombstone( + 'RETIRED (objectui#6152, ADR-0049) — nothing ever read `autoDiscoverRelated`: `detail-view` does not ' + + 'discover related lists from reference fields. Delete the key, and author a `record:related_list` ' + + 'block for each related list the record page should show.', + ), /** * The DETAIL-VIEW RELATED-LIST REFUSAL (objectui#7997) — `related` retires * from `DetailViewSchema` on BOTH faces under ADR-0049 enforce-or-remove From 66ad6c9bc3b982f8cbde7190fd9ef29f5cb182c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:37:52 +0000 Subject: [PATCH 2/4] docs(pagination): the with-item-count example and the docs interface block stop authoring pageSize / totalItems, which neither face declares (objectui#6152, round 4) Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude --- content/docs/components/basic/pagination.mdx | 7 ++++--- .../components-basic-pagination/with-item-count.json | 4 +--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/content/docs/components/basic/pagination.mdx b/content/docs/components/basic/pagination.mdx index 61a01c7785..5fff012ee1 100644 --- a/content/docs/components/basic/pagination.mdx +++ b/content/docs/components/basic/pagination.mdx @@ -9,10 +9,13 @@ The Pagination component allows users to navigate through pages of data. -## With Page Size +## On a Later Page +`pagination` has no page-size or item-count keys: the page state it reads is +`currentPage` and `totalPages`. + ## Schema ```plaintext @@ -20,8 +23,6 @@ interface PaginationSchema { type: 'pagination'; currentPage: number; // Current page (1-based) totalPages: number; // Total number of pages - pageSize?: number; // Items per page - totalItems?: number; // Total number of items // Events // RUNTIME SLOT (objectui#6124): a React host supplies it; JSON refuses it diff --git a/examples/schema-catalog/src/schemas/components-basic-pagination/with-item-count.json b/examples/schema-catalog/src/schemas/components-basic-pagination/with-item-count.json index 576542cfb3..86a36691f9 100644 --- a/examples/schema-catalog/src/schemas/components-basic-pagination/with-item-count.json +++ b/examples/schema-catalog/src/schemas/components-basic-pagination/with-item-count.json @@ -1,7 +1,5 @@ { "type": "pagination", "currentPage": 3, - "totalPages": 20, - "pageSize": 10, - "totalItems": 200 + "totalPages": 20 } From 2c7452bee4939a910bc4704e99a95e10da288ce8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:41:34 +0000 Subject: [PATCH 3/4] test(types): pin round 4's retirements and mirrors on every face (objectui#6152, round 4) Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude --- .../unmirrored-declared-round4-6152.test.ts | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 packages/types/src/__tests__/unmirrored-declared-round4-6152.test.ts diff --git a/packages/types/src/__tests__/unmirrored-declared-round4-6152.test.ts b/packages/types/src/__tests__/unmirrored-declared-round4-6152.test.ts new file mode 100644 index 0000000000..1b165bd9ed --- /dev/null +++ b/packages/types/src/__tests__/unmirrored-declared-round4-6152.test.ts @@ -0,0 +1,183 @@ +/** + * 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 4 — what this round did to the declared-but-unmirrored keys + * it closed by RETIREMENT and by MIRRORING. (The eighteen keys it filed as runtime + * slots by name moved between two test ledgers only; `zod-mirror-parity.test.ts` + * reconciles them, and no published face changed for them.) + * + * ## Retired on both faces + * + * Four keys the TypeScript face declared and the zod mirror had never heard of: + * + * - `label.content` — a THIRD spelling of the label text, after `text` and + * `label`; the renderer's third read is dropped in the same change; + * - `report.chartConfig`, `report.reportType`, `detail-view.autoDiscoverRelated` + * — each re-measured ZERO-READ by a type-checker census, with no authored + * document (`reportType`'s one in-code producer stops writing it). + * + * 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()`. + * + * ## Mirrored + * + * - `chatbot.requestBody` — read by all three chatbot registrations, and until + * now refused by the strict face on a `chatbot` node although `tsc` accepted it; + * - `chatbot-floating.floatingConfig` — read by that registration alone, now + * judged member by member (its `triggerIcon` tombstone is pinned in + * `floating-chatbot-trigger-icon-retired.test.ts`). + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import type { LabelSchema as TsLabelSchema } from '../form'; +import type { ReportComponentSchema as TsReportComponentSchema } from '../reports'; +import type { DetailViewSchema as TsDetailViewSchema } from '../views'; +import { LabelSchema as LabelMirror } from '../zod/form.zod.js'; +import { ReportComponentSchema as ReportMirror } from '../zod/reports.zod.js'; +import { DetailViewSchema as DetailViewMirror } from '../zod/views.zod.js'; +import { ChatbotSchema as ChatbotMirror, ChatbotFloatingSchema as ChatbotFloatingMirror } from '../zod/complex.zod.js'; +import { AnyComponentSchema, StrictAnyComponentSchema } from '../zod/index.zod.js'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(HERE, '..', '..', '..', '..'); + +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 }; + +const RETIRED: Retired[] = [ + { name: 'label', key: 'content', value: 'Email', base: { type: 'label', text: 'Email' }, mirror: LabelMirror as unknown as Mirror }, + { + name: 'report', + key: 'chartConfig', + value: { chartType: 'bar', xAxisField: 'stage', yAxisFields: ['amount'] }, + base: { type: 'report', title: 'Pipeline' }, + mirror: ReportMirror as unknown as Mirror, + }, + { name: 'report', key: 'reportType', value: 'summary', base: { type: 'report', title: 'Pipeline' }, mirror: ReportMirror as unknown as Mirror }, + { + name: 'detail-view', + key: 'autoDiscoverRelated', + value: true, + base: { type: 'detail-view', objectName: 'account' }, + mirror: DetailViewMirror as unknown as Mirror, + }, +]; + +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 4 — keys RETIRED on both faces', () => { + it.each(RETIRED)('CONTROL: the minimal `$name` document parses on both faces', ({ base }) => { + expect(AnyComponentSchema.safeParse(base).success).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`), not an unrecognised key', ({ key, value, base, mirror }) => { + 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']); + }); + + it('the `label.content` refusal names the live spelling `text`', () => { + const parsed = LabelMirror.safeParse({ type: 'label', content: 'Email' }); + const atKey = (parsed.error?.issues ?? []).filter((issue) => issue.path[0] === 'content'); + expect(atKey.some((issue) => issue.message.includes('`text`')), JSON.stringify(atKey)).toBe(true); + }); + + 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 `content` is RETIRED (objectui#6152) — write `text` + const label: TsLabelSchema = { type: 'label', content: 'Email' }; + // @ts-expect-error `chartConfig` is RETIRED (objectui#6152) — nothing read it + const chart: TsReportComponentSchema = { type: 'report', chartConfig: { chartType: 'bar' } }; + // @ts-expect-error `reportType` is RETIRED (objectui#6152) — nothing read it + const kind: TsReportComponentSchema = { type: 'report', reportType: 'summary' }; + // @ts-expect-error `autoDiscoverRelated` is RETIRED (objectui#6152) — nothing read it + const detail: TsDetailViewSchema = { type: 'detail-view', autoDiscoverRelated: true }; + const live: TsLabelSchema = { type: 'label', text: 'Email', label: 'Email' }; + expect([label.type, chart.type, kind.type, detail.type, live.text]).toEqual(['label', 'report', 'report', 'detail-view', 'Email']); + }); +}); + +describe('objectui#6152 round 4 — `chatbot.requestBody` and `chatbot-floating.floatingConfig` are MIRRORED', () => { + const messages = [{ id: 'm1', role: 'user', content: 'hi' }]; + const ROWS = [ + { + name: 'chatbot', + key: 'requestBody', + mirror: ChatbotMirror as unknown as Mirror, + base: { type: 'chatbot', messages }, + valid: { tenant: 'acme', temperature: 0.2 }, + wrong: 'tenant=acme', + wrongPath: 'requestBody', + }, + { + name: 'chatbot-floating', + key: 'floatingConfig', + mirror: ChatbotFloatingMirror as unknown as Mirror, + base: { type: 'chatbot-floating', messages }, + valid: { position: 'bottom-left', defaultOpen: true, panelWidth: 420, panelHeight: 560, title: 'Support', triggerSize: 48 }, + wrong: { position: 'top-left' }, + wrongPath: 'floatingConfig.position', + }, + ] as const; + + it.each(ROWS)('`$name`.`$key` is a member of the mirror', ({ key, mirror }) => { + expect(key in mirror.shape).toBe(true); + }); + + it.each(ROWS)('an authored `$name`.`$key` parses on the strict face and the tolerant face', ({ key, base, valid }) => { + const doc = { ...base, [key]: valid }; + 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); + }); + + it.each(ROWS)('a wrong-typed `$name`.`$key` is refused at its path on the tolerant face', ({ key, base, wrong, wrongPath }) => { + const parsed = AnyComponentSchema.safeParse({ ...base, [key]: wrong }); + expect(parsed.success).toBe(false); + const paths = (parsed.error?.issues ?? []).map((issue) => issue.path.join('.')); + expect(paths, JSON.stringify(parsed.error?.issues)).toContain(wrongPath); + }); +}); + +describe('objectui#6152 round 4 — the pagination catalog document authors only what `pagination` declares', () => { + it('`with-item-count` parses on the strict face, and no longer carries `pageSize` / `totalItems`', () => { + const doc = JSON.parse(readFileSync(join(REPO_ROOT, 'examples/schema-catalog/src/schemas/components-basic-pagination/with-item-count.json'), 'utf8')); + // Non-vacuity: this is the document the finding named, and it is a pagination node. + expect(doc).toMatchObject({ type: 'pagination', currentPage: 3, totalPages: 20 }); + expect('pageSize' in doc || 'totalItems' in doc).toBe(false); + const strict = StrictAnyComponentSchema.safeParse(doc); + expect(strict.success, JSON.stringify(strict.error?.issues)).toBe(true); + }); +}); From 4c272dca5fddfcece0173a7472cdf3a05b4d6bb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:42:17 +0000 Subject: [PATCH 4/4] chore(changeset): round 4's release note, and dated append-only corrections to four pending changesets it made false (objectui#6152, round 4) Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude --- .changeset/6152-unmirrored-round4.md | 50 +++++++++++++++++++ .changeset/6639-objectgrid-title-mirrored.md | 2 + ...floating-chatbot-trigger-icon-tombstone.md | 2 + ...55-chatbot-registration-authoring-faces.md | 2 + .../9447-detail-view-handler-key-refusal.md | 2 + 5 files changed, 58 insertions(+) create mode 100644 .changeset/6152-unmirrored-round4.md diff --git a/.changeset/6152-unmirrored-round4.md b/.changeset/6152-unmirrored-round4.md new file mode 100644 index 0000000000..1211f1daf7 --- /dev/null +++ b/.changeset/6152-unmirrored-round4.md @@ -0,0 +1,50 @@ +--- +'@object-ui/types': minor +'@object-ui/components': minor +--- + +feat(types): four declared keys nothing honoured are retired on both faces, and two chatbot keys gain their zod mirror (objectui#6152, round 4) + +**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`. None of them is +honoured, so each is retired at once, with no alias window: + +- `label`: `content`, a third spelling of the label text beside `text` and `label`. The `label` + renderer in `@object-ui/components` read it last (`text`, then `label`, then `content`) and no + longer reads it, so a node that carries only `content` renders no text. No document in this + repository authored it. Write `text` (or `label`). +- `report`: `chartConfig` and `reportType`. Nothing read either key off a report, so an authored + value configured nothing. `specReportToPresentation` no longer writes `reportType` onto the + presentation it returns, and `LegacyReportPresentationLike` no longer declares it. +- `detail-view`: `autoDiscoverRelated`. Nothing read it: `detail-view` does not discover related + lists from reference fields. Author a `record:related_list` block for each related list. + +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 + tolerant validator used to keep the value without examining it. + +Delete the key from any document or literal that carries it. + +**Mirrored.** Two chatbot keys the published types declare, and the renderers read, gain their +zod arm: + +- `chatbot`: `requestBody`, the chat API's extra body params. All three chatbot nodes now share + one `requestBody` arm. The strict authoring face used to refuse `requestBody` on a `chatbot` + node, although the type invites it and the retirement of `body` points authors at it. +- `chatbot-floating`: `floatingConfig`, judged member by member (`position`, `defaultOpen`, + `panelWidth`, `panelHeight`, `title`, `triggerSize`). Its retired `triggerIcon` member is now + refused at runtime too, on this node. On a `chatbot` node `floatingConfig` stays unvalidated, + because the `chatbot` renderer never reads it. + +A wrong-typed value at either key, for example a string `requestBody` or a `floatingConfig` +`position` of `'top-left'`, is now refused at its path, where it used to be kept unexamined +(breaking for invalid documents). + +`@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. + +`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. diff --git a/.changeset/6639-objectgrid-title-mirrored.md b/.changeset/6639-objectgrid-title-mirrored.md index c83f1073a3..92f071011a 100644 --- a/.changeset/6639-objectgrid-title-mirrored.md +++ b/.changeset/6639-objectgrid-title-mirrored.md @@ -15,3 +15,5 @@ admitting any `title` unexamined, and it now enforces the declared `string`. `zod-mirror-parity.test.ts`'s `UnmirroredDeclared` ledger records the key as worked off — the ledger's first shrink by repair (97 + 1 mirrored + 23 reclassified is what the seeded "121" now means). + +**Correction, 2026-09-30 (objectui#6152, round 4).** The last sentence above says "97 + 1 mirrored + 23 reclassified" is what the seeded "121" now means. That was a reading when this change was written, and it no longer is: later changes mirrored, retired and reclassified further keys, and the survivor count it names went stale before objectui#6152 round 3. No live decomposition of "121" is kept anywhere, because the ledgers record no per-key provenance. The dated readings, and the live total, are in `zod-mirror-parity.test.ts`, whose pins derive them. The mirroring of `title` this entry describes is unaffected. diff --git a/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md b/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md index 9b382d360c..61c04d32b9 100644 --- a/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md +++ b/.changeset/7654-floating-chatbot-trigger-icon-tombstone.md @@ -68,3 +68,5 @@ That file also pins the runtime half as a **tripwire**: it asserts that a node c `FloatingChatbotConfigSchema`, it goes red — the intended signal that whoever lands the mirror must add the `retirementTombstone()` half at the same time and flip the control 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. diff --git a/.changeset/7655-chatbot-registration-authoring-faces.md b/.changeset/7655-chatbot-registration-authoring-faces.md index f62592c637..476320e685 100644 --- a/.changeset/7655-chatbot-registration-authoring-faces.md +++ b/.changeset/7655-chatbot-registration-authoring-faces.md @@ -116,3 +116,5 @@ registration reads it, so it leaves `ChatbotSharedKey`, and `ChatbotSchema` decl instead as a `?: never` member that the other two faces pick by name, refused by name on the zod twins. The "twenty keys" above is kept as the reading of this change; the 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. diff --git a/.changeset/9447-detail-view-handler-key-refusal.md b/.changeset/9447-detail-view-handler-key-refusal.md index 46e5136cc8..5e8b86d155 100644 --- a/.changeset/9447-detail-view-handler-key-refusal.md +++ b/.changeset/9447-detail-view-handler-key-refusal.md @@ -47,3 +47,5 @@ behaviour as a node type (`{ "type": "toast", ... }`, an `action:button` node). different path with a different signature (`(recordId) => void`), which stays authorable on both faces; and `onTabChange`, whose disposition is still open on the handler-key parent card and which this change deliberately does not touch. + +**Correction, 2026-09-30 (objectui#6152, round 4).** The last paragraph above says the nested `recordNavigation.onNavigate` "stays authorable on both faces". It was never authorable in a JSON document: its value is a required function, and the zod mirror has never declared `recordNavigation`. objectui#6152 round 4 recorded `recordNavigation` as a runtime slot a host sets in code. No published face changed with that, and the refusals this entry describes are unaffected.