Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .changeset/6152-unmirrored-round4.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions .changeset/6639-objectgrid-title-mirrored.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions .changeset/7654-floating-chatbot-trigger-icon-tombstone.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions .changeset/7655-chatbot-registration-authoring-faces.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 2 additions & 0 deletions .changeset/9447-detail-view-handler-key-refusal.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 4 additions & 3 deletions content/docs/components/basic/pagination.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ The Pagination component allows users to navigate through pages of data.

<SchemaExample id="components-basic-pagination/basic-pagination" />

## With Page Size
## On a Later Page

<SchemaExample id="components-basic-pagination/with-item-count" />

`pagination` has no page-size or item-count keys: the page state it reads is
`currentPage` and `totalPages`.

## Schema

```plaintext
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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"type": "pagination",
"currentPage": 3,
"totalPages": 20,
"pageSize": 10,
"totalItems": 200
"totalPages": 20
}
15 changes: 14 additions & 1 deletion packages/components/src/__tests__/form-renderers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/renderers/form/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
</Label>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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);
Expand All @@ -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' }],
Expand All @@ -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
Expand All @@ -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<string, unknown>).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<string, unknown>).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<string, unknown>).floatingConfig).toBeUndefined();
});

it('`onClear` / `onError` / `onSend` are refused by name here too', () => {
Expand All @@ -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',
];
Expand Down
Loading
Loading