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.
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
}
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__/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);
+ });
+});
diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts
index d267bc690f..be10f66b94 100644
--- a/packages/types/src/__tests__/zod-mirror-parity.test.ts
+++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts
@@ -372,7 +372,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
@@ -447,7 +459,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
@@ -489,8 +508,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
@@ -506,11 +528,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
@@ -2536,7 +2559,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;
@@ -2563,7 +2586,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
*
@@ -2614,7 +2644,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.
@@ -2726,7 +2757,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
@@ -2793,10 +2830,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`
@@ -2832,28 +2873,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,
@@ -2875,15 +2933,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
@@ -2909,17 +2981,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,
@@ -2977,49 +3046,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) ─────────────── */
@@ -3035,7 +3081,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
@@ -3174,11 +3220,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
@@ -3264,8 +3323,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';
}
/**
@@ -3357,6 +3428,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