From 095636b79a2c855624fe4d9fffd44fdd4d408597 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 21:22:22 +0000 Subject: [PATCH 1/2] fix(types): retire the header-bar keys nothing reads or authors (nav, left, center, height) ADR-0049 enforce-or-remove under the objectui#7759 ruling (rule 2, D1-(ii)): the key is objectui-own, the spec is silent, and the read site is the truth. A runtime probe through the real SchemaRenderer drew the header identical to its absence for every value; no in-tree document authors any of the four. Both faces now carry the tombstone (?: never beside retirementTombstone). title, logo, right and sticky are also unread but have in-tree authors and are left for the seat to decide. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S --- .../10387-header-bar-unread-keys-retired.md | 26 ++++++ .../docs/components/navigation/header-bar.mdx | 11 +++ .../header-bar-unread-keys-10387.test.ts | 88 +++++++++++++++++++ packages/types/src/navigation.ts | 43 +++++++-- packages/types/src/zod/navigation.zod.ts | 32 ++++++- 5 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 .changeset/10387-header-bar-unread-keys-retired.md create mode 100644 packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts diff --git a/.changeset/10387-header-bar-unread-keys-retired.md b/.changeset/10387-header-bar-unread-keys-retired.md new file mode 100644 index 0000000000..ee1a7605bb --- /dev/null +++ b/.changeset/10387-header-bar-unread-keys-retired.md @@ -0,0 +1,26 @@ +--- +'@object-ui/types': minor +--- + +fix(types): retire the four `header-bar` keys nothing reads or authors (objectui#10387) + +`HeaderBarSchema.nav`, `left`, `center` and `height` are retired on both faces +(ADR-0049). None of them is in the spec, and the `header-bar` renderer reads only +`actions`, `crumbs`, `rightContent` and `search`. Rendered through the real +`SchemaRenderer`, a header with any of these keys was identical to one without +them. No in-tree app, example or doc authored any of the four. + +- The zod mirror (`HeaderBarSchema` in `@object-ui/types/zod`) now refuses each + key by name. The refusal says what the renderer reads instead. +- The TypeScript declaration types each key `never`. + +Breaking, but only for documents the renderer already ignored. A `header-bar` +that carries `nav`, `left`, `center` or `height` now fails validation and no +longer type-checks. To migrate, delete the key: + +- `nav`: put links in `crumbs`, or use a `navigation-menu` / `sidebar` node. +- `left` / `center`: put custom content in `rightContent` or `actions`. +- `height`: remove it. The header's height is fixed by the renderer. + +`title`, `logo`, `right` and `sticky` are also unread, but in-tree documents +author them. They are unchanged here. diff --git a/content/docs/components/navigation/header-bar.mdx b/content/docs/components/navigation/header-bar.mdx index 397663f7f9..9310c46087 100644 --- a/content/docs/components/navigation/header-bar.mdx +++ b/content/docs/components/navigation/header-bar.mdx @@ -53,6 +53,17 @@ interface HeaderBarSchema { } ``` +### Retired keys + +`nav`, `left`, `center` and `height` are retired on this node (objectui#10387, +alongside `variant` from objectui#10286). The renderer never read any of them, so an +authored value drew the same header as leaving it out. They now fail validation +by name, and the TypeScript declaration types each one `never`. Delete the key: + +- `nav` — put links in `crumbs`, or use a `navigation-menu` / `sidebar` node. +- `left` / `center` — put custom content in `rightContent` or `actions`. +- `height` — the header's height is fixed (`h-14`, `sm:h-16`). + ## Examples ### Application Header diff --git a/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts b/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts new file mode 100644 index 0000000000..8c3fb8075a --- /dev/null +++ b/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts @@ -0,0 +1,88 @@ +/** + * 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#10387 — the `header-bar` keys that have no read site and no in-tree + * author retire from both faces (ADR-0049 enforce-or-remove; objectui#7759 ruling + * rule 2 and D1-(ii): the key is objectui-own, the spec is silent, and the read + * site is the truth). + * + * The four retired here are `nav`, `left`, `center` and `height`. A one-off + * probe through the real `SchemaRenderer` and the real registry drew the header + * byte-identical to its absence for every value tried, with `rightContent` as + * the lit control that did change the markup. The other unread keys the card + * names (`title`, `logo`, `right`, `sticky`) have in-tree authors and are NOT + * touched by this card; the pin below keeps them parsing so this file cannot be + * read as having retired them. + * + * `BaseSchema` is `.passthrough()`: an UNDECLARED key parses green unexamined. So + * every refusal here has a lit control on the same document, an unknown key that + * must stay green, which shows the refusal is the declared key's own verdict. + */ + +import { describe, it, expect } from 'vitest'; +import { HeaderBarSchema } from '../zod/navigation.zod.js'; +import type { HeaderBarSchema as HeaderBarSchemaType } from '../navigation.js'; + +const UNKNOWN_KEY = 'zzzNotAKeyAnySurfaceDeclares10387'; +const NODE = { type: 'header-bar' as const, crumbs: [{ label: 'Home' }] }; +const TEXT = { type: 'text', content: 'x' }; + +/** Values each face used to admit for the key, one per former arm. */ +const FORMER_VALUES: Record<'nav' | 'left' | 'center' | 'height', unknown[]> = { + nav: [[{ label: 'Docs', href: '/docs' }], []], + left: [TEXT, [TEXT], 'plain text'], + center: [TEXT, [TEXT], 'plain text'], + height: ['64px', 64], +}; + +function refusedPaths(result: { success: boolean; error?: { issues: { path: PropertyKey[] }[] } }): string[] { + return result.success ? [] : (result.error?.issues ?? []).map((i) => i.path.map(String).join('.')); +} + +describe('header-bar unread keys retire on both faces (objectui#10387)', () => { + for (const [key, values] of Object.entries(FORMER_VALUES)) { + it(`\`${key}\` is refused BY NAME for every value a former arm admitted`, () => { + for (const value of values) { + const result = HeaderBarSchema.safeParse({ ...NODE, [key]: value }); + expect(refusedPaths(result), `${key}: ${JSON.stringify(value)}`).toEqual([key]); + } + }); + + it(`\`${key}\`'s refusal says why and names what the renderer reads instead`, () => { + const result = HeaderBarSchema.safeParse({ ...NODE, [key]: values[0] }); + expect(result.success).toBe(false); + const issue = result.success ? undefined : result.error.issues[0]; + expect(issue?.code).toBe('invalid_type'); + expect(issue?.message.startsWith('REFUSED (objectui#10387, ADR-0049)')).toBe(true); + for (const read of ['actions', 'crumbs', 'rightContent', 'search']) expect(issue?.message).toContain(`\`${read}\``); + }); + } + + it('lit control: the same document without the keys, and with an undeclared key, parses', () => { + expect(HeaderBarSchema.safeParse(NODE).success).toBe(true); + expect(HeaderBarSchema.safeParse({ ...NODE, [UNKNOWN_KEY]: 'x' }).success).toBe(true); + }); + + it('the keys with in-tree authors are untouched by this card and still parse', () => { + const doc = { ...NODE, title: 'App', logo: TEXT, right: [TEXT], sticky: true }; + expect(HeaderBarSchema.safeParse(doc).success).toBe(true); + }); + + it('the declaration refuses them too (compile-time)', () => { + // @ts-expect-error — `nav` is `never` on the TS face. + const a: HeaderBarSchemaType = { type: 'header-bar', nav: [{ label: 'Docs', href: '/docs' }] }; + // @ts-expect-error — `left` is `never` on the TS face. + const b: HeaderBarSchemaType = { type: 'header-bar', left: 'x' }; + // @ts-expect-error — `center` is `never` on the TS face. + const c: HeaderBarSchemaType = { type: 'header-bar', center: 'x' }; + // @ts-expect-error — `height` is `never` on the TS face. + const d: HeaderBarSchemaType = { type: 'header-bar', height: 64 }; + expect([a, b, c, d].map((n) => n.type)).toEqual(['header-bar', 'header-bar', 'header-bar', 'header-bar']); + }); +}); diff --git a/packages/types/src/navigation.ts b/packages/types/src/navigation.ts index 64c0efd3ab..ce27bc1dec 100644 --- a/packages/types/src/navigation.ts +++ b/packages/types/src/navigation.ts @@ -65,9 +65,18 @@ export interface HeaderBarSchema extends BaseSchema { */ logo?: string; /** - * Navigation links + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `nav`. + * + * Not in `@objectstack/spec`, so the objectui#7759 ruling makes the read + * site the truth, and there is none: the renderer's one function reads + * `actions`, `crumbs`, `rightContent` and `search` off `schema` and takes no + * spread props. Through the real `SchemaRenderer` an authored link list drew + * the header byte-identical to its absence. No in-tree document authored it. + * For links, use `crumbs` here, or a `navigation-menu` / `sidebar` node. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - nav?: NavLink[]; + nav?: never; /** * Breadcrumb items */ @@ -85,13 +94,25 @@ export interface HeaderBarSchema extends BaseSchema { */ rightContent?: SchemaNode; /** - * Left side content + * RETIRED (objectui#10387, ADR-0049) — `header-bar` has no left slot. + * + * Not in `@objectstack/spec`; the renderer reads only `actions`, `crumbs`, + * `rightContent` and `search` and takes no spread props, so an authored node + * rendered nothing. No in-tree document authored it. For custom content use + * `rightContent` or `actions`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - left?: SchemaNode | SchemaNode[]; + left?: never; /** - * Center content + * RETIRED (objectui#10387, ADR-0049) — `header-bar` has no center slot. + * + * Same reading as `left`: no read site, no spec declaration, no in-tree + * author. For custom content use `rightContent` or `actions`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - center?: SchemaNode | SchemaNode[]; + center?: never; /** * Right side content */ @@ -102,9 +123,15 @@ export interface HeaderBarSchema extends BaseSchema { */ sticky?: boolean; /** - * Header height + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `height`. + * + * The header's height is fixed by the renderer's own classes (`h-14`, and + * `sm:h-16` from the `sm` breakpoint); no read of this key exists, and no + * in-tree document authored it. Not in `@objectstack/spec`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - height?: string | number; + height?: never; /** * RETIRED (objectui#10286, ADR-0049) — `header-bar` reads no `variant`. * diff --git a/packages/types/src/zod/navigation.zod.ts b/packages/types/src/zod/navigation.zod.ts index 787760e537..3ed2655fdb 100644 --- a/packages/types/src/zod/navigation.zod.ts +++ b/packages/types/src/zod/navigation.zod.ts @@ -64,7 +64,13 @@ export const HeaderBarSchema = BaseSchema.extend({ type: z.literal('header-bar'), title: z.string().optional().describe('Header title'), logo: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Logo content'), - nav: z.array(NavLinkSchema).optional().describe('Navigation links'), + nav: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `nav`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored link list rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For links use `crumbs`, or a `navigation-menu` / `sidebar` node.', + ), crumbs: z.array(BreadcrumbItemSchema).optional().describe('Breadcrumb items'), search: z.object({ enabled: z.boolean().describe('Whether search is enabled'), @@ -73,11 +79,29 @@ export const HeaderBarSchema = BaseSchema.extend({ }).optional().describe('Search configuration'), actions: z.array(SchemaNodeSchema).optional().describe('Right-side action slots'), rightContent: SchemaNodeSchema.optional().describe('Custom right content area'), - left: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Left content'), - center: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Center content'), + left: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `left`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored node rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For custom content use `rightContent` or `actions`.', + ), + center: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `center`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored node rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For custom content use `rightContent` or `actions`.', + ), right: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Right content'), sticky: z.boolean().optional().describe('Whether header is sticky'), - height: z.union([z.string(), z.number()]).optional().describe('Header height'), + height: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `height`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so every value drew the same fixed-height header — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. The height is fixed by the renderer (`h-14` / `sm:h-16`).', + ), variant: retirementTombstone( 'REFUSED (objectui#10286, ADR-0049) — `header-bar` reads no `variant`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' From 968cd76988f6efecfe293f694e32049e3d52183c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 21:44:52 +0000 Subject: [PATCH 2/2] fix(types): retire title, logo, right and sticky on header-bar too; migrate their authors PM decision A on the open question: the four keys have no read site (runtime probe) and the spec is silent, so they retire like nav/left/center/height. Their only authors were docs and a type-check example; those now use crumbs and actions. The two components tests drop their inert title filler. Retiring logo empties the HeaderBarSchema WIDER entry and its WIDER_ARMS row; the header figures are re-derived (7 / 10 / 10, split 5 / 5 / 0 / 0). The ledger's arm-enumeration non-vacuity check now reads a fixed union control slot (SidebarSchema.content), because the ledger no longer holds a union. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S --- .../10387-header-bar-unread-keys-retired.md | 28 +++++++----- .../docs/components/navigation/header-bar.mdx | 15 ++++--- content/docs/guide/objectos-integration.mdx | 8 +--- ...r-bar-right-content-numeric-falsy.test.tsx | 6 +-- .../node-slot-numeric-falsy.test.tsx | 2 +- packages/types/README.md | 2 +- packages/types/examples/dashboard.ts | 6 +-- .../header-bar-unread-keys-10387.test.ts | 36 ++++++++++----- .../src/__tests__/zod-mirror-parity.test.ts | 34 +++++++------- packages/types/src/navigation.ts | 45 +++++++++++++++---- packages/types/src/zod/navigation.zod.ts | 32 +++++++++++-- 11 files changed, 144 insertions(+), 70 deletions(-) diff --git a/.changeset/10387-header-bar-unread-keys-retired.md b/.changeset/10387-header-bar-unread-keys-retired.md index ee1a7605bb..0ce59da5dc 100644 --- a/.changeset/10387-header-bar-unread-keys-retired.md +++ b/.changeset/10387-header-bar-unread-keys-retired.md @@ -2,25 +2,33 @@ '@object-ui/types': minor --- -fix(types): retire the four `header-bar` keys nothing reads or authors (objectui#10387) +fix(types): retire the eight `header-bar` keys the renderer never read (objectui#10387) -`HeaderBarSchema.nav`, `left`, `center` and `height` are retired on both faces -(ADR-0049). None of them is in the spec, and the `header-bar` renderer reads only -`actions`, `crumbs`, `rightContent` and `search`. Rendered through the real -`SchemaRenderer`, a header with any of these keys was identical to one without -them. No in-tree app, example or doc authored any of the four. +`HeaderBarSchema.title`, `logo`, `nav`, `left`, `center`, `right`, `sticky` and +`height` are retired on both faces (ADR-0049). None of them is in the spec, and +the `header-bar` renderer reads only `actions`, `crumbs`, `rightContent` and +`search`. Rendered through the real `SchemaRenderer`, a header with any of these +keys was identical to one without them. - The zod mirror (`HeaderBarSchema` in `@object-ui/types/zod`) now refuses each key by name. The refusal says what the renderer reads instead. - The TypeScript declaration types each key `never`. +- `logo` also stops disagreeing with itself: the declaration said an image URL + string and the mirror a node or node list. Neither rendered, and both are + refused now. Breaking, but only for documents the renderer already ignored. A `header-bar` -that carries `nav`, `left`, `center` or `height` now fails validation and no -longer type-checks. To migrate, delete the key: +that carries any of the eight keys now fails validation and no longer +type-checks. To migrate, delete the key: +- `title`: name the current page with the last entry of `crumbs`. +- `logo`: put brand content in `rightContent` or `actions`. - `nav`: put links in `crumbs`, or use a `navigation-menu` / `sidebar` node. - `left` / `center`: put custom content in `rightContent` or `actions`. +- `right`: use `actions` (a node list) or `rightContent` (one node). +- `sticky`: make the parent layout sticky; the header itself never was. - `height`: remove it. The header's height is fixed by the renderer. -`title`, `logo`, `right` and `sticky` are also unread, but in-tree documents -author them. They are unchanged here. +The in-tree examples that used these keys (`packages/types/examples/dashboard.ts`, +the `@object-ui/types` README and the ObjectOS integration guide) now use +`crumbs` and `actions`. diff --git a/content/docs/components/navigation/header-bar.mdx b/content/docs/components/navigation/header-bar.mdx index 9310c46087..29d8df7593 100644 --- a/content/docs/components/navigation/header-bar.mdx +++ b/content/docs/components/navigation/header-bar.mdx @@ -55,13 +55,18 @@ interface HeaderBarSchema { ### Retired keys -`nav`, `left`, `center` and `height` are retired on this node (objectui#10387, -alongside `variant` from objectui#10286). The renderer never read any of them, so an -authored value drew the same header as leaving it out. They now fail validation -by name, and the TypeScript declaration types each one `never`. Delete the key: - +`title`, `logo`, `nav`, `left`, `center`, `right`, `sticky` and `height` are retired +on this node (objectui#10387, alongside `variant` from objectui#10286). The renderer +never read any of them, so an authored value drew the same header as leaving it out. +They now fail validation by name, and the TypeScript declaration types each one +`never`. Delete the key: + +- `title` — name the current page with the last entry of `crumbs`. +- `logo` — put brand content in `rightContent` or `actions`. - `nav` — put links in `crumbs`, or use a `navigation-menu` / `sidebar` node. - `left` / `center` — put custom content in `rightContent` or `actions`. +- `right` — use `actions` (a node list) or `rightContent` (one node). +- `sticky` — make the parent layout sticky; the header itself never was. - `height` — the header's height is fixed (`h-14`, `sm:h-16`). ## Examples diff --git a/content/docs/guide/objectos-integration.mdx b/content/docs/guide/objectos-integration.mdx index d87efe9247..2b8bc93fa3 100644 --- a/content/docs/guide/objectos-integration.mdx +++ b/content/docs/guide/objectos-integration.mdx @@ -118,13 +118,7 @@ export function App() { template: 'header-sidebar-main', header: { type: 'header-bar', - title: 'My App', - navigation: { - items: [ - { label: 'Dashboard', path: '/', icon: 'home' }, - { label: 'Contacts', path: '/contacts', icon: 'users' } - ] - } + crumbs: [{ label: 'My App' }] }, sidebar: { type: 'navigation-menu', diff --git a/packages/components/src/__tests__/header-bar-right-content-numeric-falsy.test.tsx b/packages/components/src/__tests__/header-bar-right-content-numeric-falsy.test.tsx index 4e395d0e0d..42bbfb03f1 100644 --- a/packages/components/src/__tests__/header-bar-right-content-numeric-falsy.test.tsx +++ b/packages/components/src/__tests__/header-bar-right-content-numeric-falsy.test.tsx @@ -81,7 +81,7 @@ function renderRightContent(rightContent: unknown) { const C = ComponentRegistry.get('header-bar') as React.ComponentType; const { container } = render( - + , ); const header = container.querySelector('header'); @@ -94,7 +94,7 @@ function renderBaseline() { const C = ComponentRegistry.get('header-bar') as React.ComponentType; const { container } = render( - + , ); const header = container.querySelector('header'); @@ -128,7 +128,7 @@ describe('ui:header-bar rightContent numeric-falsy leak (objectui#9033)', () => // slot stopped admitting numbers and the rows below stopped being about // anything — ⛔ that is a declaration change, not a licence to delete them // (objectui#7105: node slots relax the RENDERER). - const parsed = HeaderBarSchemaZod.safeParse({ type: 'header-bar', title: 'H', rightContent: 0 }); + const parsed = HeaderBarSchemaZod.safeParse({ type: 'header-bar', rightContent: 0 }); expect(parsed.success).toBe(true); }); }); diff --git a/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx b/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx index 91f29a677f..0c6c1fa0ea 100644 --- a/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx +++ b/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx @@ -272,7 +272,7 @@ describe('SchemaNode slots refuse numeric-falsy authored values (objectui#9162)' function renderHeaderBar(value: unknown): string { const C = ComponentRegistry.get('header-bar', 'ui') as React.ComponentType; if (!C) throw new Error('no renderer registered for ui:header-bar'); - const schema: Record = { type: 'header-bar', title: 'H' }; + const schema: Record = { type: 'header-bar' }; if (value !== OMIT) schema.rightContent = value; render( diff --git a/packages/types/README.md b/packages/types/README.md index 55d4f12e43..0468add616 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -325,7 +325,7 @@ const page: FlexSchema = { type: 'flex', direction: 'col', children: [ - { type: 'header-bar', title: 'My App' }, + { type: 'header-bar', crumbs: [{ label: 'My App' }] }, { type: 'flex', direction: 'row', diff --git a/packages/types/examples/dashboard.ts b/packages/types/examples/dashboard.ts index f940d5a479..a85b1be42c 100644 --- a/packages/types/examples/dashboard.ts +++ b/packages/types/examples/dashboard.ts @@ -24,10 +24,8 @@ export const dashboardSchema: FlexSchema = { // Header { type: 'header-bar', - title: 'Object UI Dashboard', - logo: '/logo.svg', - sticky: true, - right: [ + crumbs: [{ label: 'Object UI Dashboard' }], + actions: [ { type: 'button', label: 'Profile', diff --git a/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts b/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts index 8c3fb8075a..d5e590089e 100644 --- a/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts +++ b/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts @@ -12,13 +12,13 @@ * rule 2 and D1-(ii): the key is objectui-own, the spec is silent, and the read * site is the truth). * - * The four retired here are `nav`, `left`, `center` and `height`. A one-off - * probe through the real `SchemaRenderer` and the real registry drew the header - * byte-identical to its absence for every value tried, with `rightContent` as - * the lit control that did change the markup. The other unread keys the card - * names (`title`, `logo`, `right`, `sticky`) have in-tree authors and are NOT - * touched by this card; the pin below keeps them parsing so this file cannot be - * read as having retired them. + * All eight keys the card names retire: `title`, `logo`, `nav`, `left`, + * `center`, `right`, `sticky` and `height`. A one-off probe through the real + * `SchemaRenderer` and the real registry drew the header byte-identical to its + * absence for every value tried, with `rightContent` as the lit control that did + * change the markup. Four of them (`title`, `logo`, `right`, `sticky`) had + * documentation and type-check-example authors and no read site; the PM seat + * ruled they retire too, and those authors migrated to `crumbs` / `actions`. * * `BaseSchema` is `.passthrough()`: an UNDECLARED key parses green unexamined. So * every refusal here has a lit control on the same document, an unknown key that @@ -34,10 +34,16 @@ const NODE = { type: 'header-bar' as const, crumbs: [{ label: 'Home' }] }; const TEXT = { type: 'text', content: 'x' }; /** Values each face used to admit for the key, one per former arm. */ -const FORMER_VALUES: Record<'nav' | 'left' | 'center' | 'height', unknown[]> = { +const FORMER_VALUES: Record<'title' | 'logo' | 'nav' | 'left' | 'center' | 'right' | 'sticky' | 'height', unknown[]> = { + // `logo` is the key whose two faces disagreed: the declaration said a URL string, + // the mirror a node or node list. Every arm either face admitted is refused now. + title: ['My App'], + logo: ['/logo.svg', TEXT, [TEXT]], nav: [[{ label: 'Docs', href: '/docs' }], []], left: [TEXT, [TEXT], 'plain text'], center: [TEXT, [TEXT], 'plain text'], + right: [TEXT, [TEXT]], + sticky: [true, false], height: ['64px', 64], }; @@ -69,12 +75,20 @@ describe('header-bar unread keys retire on both faces (objectui#10387)', () => { expect(HeaderBarSchema.safeParse({ ...NODE, [UNKNOWN_KEY]: 'x' }).success).toBe(true); }); - it('the keys with in-tree authors are untouched by this card and still parse', () => { - const doc = { ...NODE, title: 'App', logo: TEXT, right: [TEXT], sticky: true }; + it('the keys the renderer DOES read still parse (what the refusals point at)', () => { + const doc = { ...NODE, search: { enabled: true }, actions: [TEXT], rightContent: TEXT }; expect(HeaderBarSchema.safeParse(doc).success).toBe(true); }); it('the declaration refuses them too (compile-time)', () => { + // @ts-expect-error — `title` is `never` on the TS face. + const e: HeaderBarSchemaType = { type: 'header-bar', title: 'My App' }; + // @ts-expect-error — `logo` is `never` on the TS face. + const f: HeaderBarSchemaType = { type: 'header-bar', logo: '/logo.svg' }; + // @ts-expect-error — `right` is `never` on the TS face. + const g: HeaderBarSchemaType = { type: 'header-bar', right: [] }; + // @ts-expect-error — `sticky` is `never` on the TS face. + const h: HeaderBarSchemaType = { type: 'header-bar', sticky: true }; // @ts-expect-error — `nav` is `never` on the TS face. const a: HeaderBarSchemaType = { type: 'header-bar', nav: [{ label: 'Docs', href: '/docs' }] }; // @ts-expect-error — `left` is `never` on the TS face. @@ -83,6 +97,6 @@ describe('header-bar unread keys retire on both faces (objectui#10387)', () => { const c: HeaderBarSchemaType = { type: 'header-bar', center: 'x' }; // @ts-expect-error — `height` is `never` on the TS face. const d: HeaderBarSchemaType = { type: 'header-bar', height: 64 }; - expect([a, b, c, d].map((n) => n.type)).toEqual(['header-bar', 'header-bar', 'header-bar', 'header-bar']); + expect([a, b, c, d, e, f, g, h].every((n) => n.type === 'header-bar')).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 9421e537f6..cf64f7ba8a 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -427,8 +427,12 @@ * spelled "six" rots exactly as fast as one spelled `6`, it is just harder to * point a regex at. ⛔ Do not spell a live figure out again, and ⛔ do not * restate one without checking that the pin's spelling still reaches it. - * - **8 entries** in `WiderThanDeclared`, **11 keys** across them, and **12 arms** - * under those keys — split **5** SCHEMA-NODE, **6** CONCRETE, **0** MIXED, **1** unions. + * - **7 entries** in `WiderThanDeclared`, **10 keys** across them, and **10 arms** + * under those keys — split **5** SCHEMA-NODE, **5** CONCRETE, **0** MIXED, **0** unions. + * It read 8 / 11 / 12 — 5 / 6 / 0 / 1 — until objectui#10387 RETIRED + * `navigation.zod.ts#HeaderBarSchema::logo` on both faces: the `header-bar` renderer + * reads no `logo`, so the entry, its one key and BOTH its arms left together. It was + * the ledger's last two-arm key, which is why `unions` reached zero. * It read 11 / 16 / 18 — 5 / 11 / 0 / 2 — until objectui#10286 closed five CONCRETE * keys of objectui#7759's groups C and D: `complex.zod.ts#FilterFieldSchema::operators` * and `complex.zod.ts#FilterBuilderSchema::fields` (both faces now state the spec's @@ -3202,18 +3206,12 @@ interface WiderThanDeclared { * narrow. */ 'layout.zod.ts#PageNodeSchema': 'slots'; - /** - * CONCRETE. (`variant` LEFT under objectui#10286 — retired on both faces, see the - * note where its `KnownDrift` entry stood.) `logo` ENTERED under objectui#7760: the mirror is - * `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` — the single-or-list - * spelling objectui#7069 called systematic — and the declaration states `logo?: - * string`. Both arms read now, and both are wider than a bare string, so an author - * may write a node here, `safeParse` returns green and `tsc` refuses it. ⛔ That card - * did not create the divergence: it made it MEASURABLE. `Unconstrained` had been - * excluding the key, because the face read `unknown` at the top level and one array - * deep. - */ - 'navigation.zod.ts#HeaderBarSchema': 'logo'; + // `navigation.zod.ts#HeaderBarSchema` recorded `logo` here (CONCRETE, two arms; ENTERED + // under objectui#7760): the mirror spelled it single-or-list `SchemaNode` and the + // declaration a bare `string`. The `header-bar` renderer reads no `logo` at all — through + // the real `SchemaRenderer` a URL, a node and a node list each drew the header + // byte-identical to its absence — and the spec does not declare the key, so objectui#10387 + // RETIRED it on both faces (`?: never` beside a `retirementTombstone`). The entry is GONE. // `overlay.zod.ts#TooltipSchema` recorded `content` here (CONCRETE; ENTERED under // objectui#7760): the mirror spelled it single-or-list and the declaration the single arm. // The renderer places `schema.content` RAW in a React child position, so the list arm parsed @@ -3350,7 +3348,6 @@ const WIDER_ARMS: Readonly< Record< string, readonly WiderArmClass[] > > = { 'complex.zod.ts#DashboardComponentSchema::dateRange': ['CONCRETE'], 'form.zod.ts#FormSchema::layout': ['CONCRETE'], 'layout.zod.ts#PageNodeSchema::slots': ['SCHEMA-NODE'], - 'navigation.zod.ts#HeaderBarSchema::logo': ['CONCRETE', 'CONCRETE'], 'views.zod.ts#DetailViewFieldSchema::options': ['CONCRETE'], 'views.zod.ts#DetailViewSchema::fields': ['SCHEMA-NODE'], 'views.zod.ts#DetailViewSchema::sections': ['SCHEMA-NODE'], @@ -5275,7 +5272,14 @@ describe('the WIDER ledger is judged per ARM, not per key (objectui#8252)', () = // below would pass while measuring nothing; one that walked into object shapes // would report many arms everywhere and the ledger would be rewritten to match // an instrument rather than the mirrors. Neither is visible from the arity pin. + // + // The union direction reads a FIXED control slot beside the ledger's own rows: + // objectui#10387 retired the ledger's last two-arm key (`HeaderBarSchema::logo`), + // and a non-vacuity check that needs the LEDGER to hold a union would have gone + // red on a shrinking ledger rather than on a broken unwrapper. `SidebarSchema`'s + // `content` is the single-or-list spelling, two arms, outside the ledger. const measured = widerArmRows().map(({ pair, key }) => measureMirrorArms(pair, key)?.length); + measured.push(measureMirrorArms('navigation.zod.ts#SidebarSchema', 'content')?.length); expect(measured.filter((n) => n === 1).length).toBeGreaterThan(0); expect(measured.filter((n) => n !== undefined && n > 1).length).toBeGreaterThan(0); }); diff --git a/packages/types/src/navigation.ts b/packages/types/src/navigation.ts index ce27bc1dec..9bdc37dc41 100644 --- a/packages/types/src/navigation.ts +++ b/packages/types/src/navigation.ts @@ -57,13 +57,29 @@ export interface NavLink { export interface HeaderBarSchema extends BaseSchema { type: 'header-bar'; /** - * Header title/brand + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `title`. + * + * Not in `@objectstack/spec`, so the objectui#7759 ruling makes the read + * site the truth, and there is none: the renderer's one function reads + * `actions`, `crumbs`, `rightContent` and `search` off `schema` and takes no + * spread props. Through the real `SchemaRenderer` an authored title drew the + * header byte-identical to its absence. For the current page name, use the + * last entry of `crumbs`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - title?: string; + title?: never; /** - * Brand logo image URL + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `logo`. + * + * No read site and no spec declaration. The two faces had also drifted + * apart: this declaration said an image URL `string`, the zod mirror a node + * or node array, and neither ever rendered. For brand content use + * `rightContent` or `actions`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - logo?: string; + logo?: never; /** * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `nav`. * @@ -114,14 +130,25 @@ export interface HeaderBarSchema extends BaseSchema { */ center?: never; /** - * Right side content + * RETIRED (objectui#10387, ADR-0049) — `header-bar` has no `right` slot. + * + * No read site and no spec declaration; the right side of the header is + * `actions` (a node list) and `rightContent` (one node), both read. An + * authored `right` rendered nothing. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - right?: SchemaNode | SchemaNode[]; + right?: never; /** - * Whether header is sticky - * @default true + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `sticky`. + * + * No read site and no spec declaration: `true` and `false` both drew the + * same, non-sticky header. To pin it, give the parent layout the sticky + * positioning. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - sticky?: boolean; + sticky?: never; /** * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `height`. * diff --git a/packages/types/src/zod/navigation.zod.ts b/packages/types/src/zod/navigation.zod.ts index 3ed2655fdb..385141979a 100644 --- a/packages/types/src/zod/navigation.zod.ts +++ b/packages/types/src/zod/navigation.zod.ts @@ -62,8 +62,20 @@ export const BreadcrumbItemSchema = z.object({ */ export const HeaderBarSchema = BaseSchema.extend({ type: z.literal('header-bar'), - title: z.string().optional().describe('Header title'), - logo: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Logo content'), + title: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `title`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored title rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For the current page name use the last entry of `crumbs`.', + ), + logo: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `logo`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored logo (URL or node) rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For brand content use `rightContent` or `actions`.', + ), nav: retirementTombstone( 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `nav`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' @@ -93,8 +105,20 @@ export const HeaderBarSchema = BaseSchema.extend({ + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + '`rightContent`, `search`. For custom content use `rightContent` or `actions`.', ), - right: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Right content'), - sticky: z.boolean().optional().describe('Whether header is sticky'), + right: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `right`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored node rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. The right side is `actions` (a node list) and `rightContent` (one node).', + ), + sticky: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `sticky`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so true and false drew the same non-sticky header — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. To pin the header, make its parent layout sticky.', + ), height: retirementTombstone( 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `height`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and '