diff --git a/.changeset/10859-console-phase-2c-stubs.md b/.changeset/10859-console-phase-2c-stubs.md new file mode 100644 index 0000000000..56641a95b9 --- /dev/null +++ b/.changeset/10859-console-phase-2c-stubs.md @@ -0,0 +1,13 @@ +--- +'@object-ui/console': minor +--- + +chore(console)!: drop the lazy `pie-chart`, `donut-chart` and `radar-chart` stubs (objectui#10859, batch 8 phase 2c) + +**BREAKING (authoring):** the console no longer registers lazy stubs for `pie-chart`, `donut-chart` or `radar-chart` in `register-plugins.ts`, nor for `pie-chart` in the dev-only preview gallery. `@object-ui/plugin-charts` retired those keys. `chart`, `object-chart`, `bar-chart` and `chart:bar` stay lazily registered. + +Migration: + +- `pie-chart` / `donut-chart` / `radar-chart` → `{ "type": "chart", "chartType": "pie" | "donut" | "radar" }`. + +**Clause-②: yes**, released as `minor` with this banner. diff --git a/.changeset/10859-known-types-phase-2c.md b/.changeset/10859-known-types-phase-2c.md new file mode 100644 index 0000000000..f994e60f2f --- /dev/null +++ b/.changeset/10859-known-types-phase-2c.md @@ -0,0 +1,16 @@ +--- +'@object-ui/cli': minor +--- + +chore(cli)!: the generated known-types list drops the four node type keys objectui#10859 batch 8 phase 2c retired + +**BREAKING (authoring):** `KNOWN_SCHEMA_TYPES` (regenerated from the registration calls) no longer lists `pie-chart`, `donut-chart`, `radar-chart` or `page-header`, nor their namespaced twins (`plugin-charts:pie-chart`, `plugin-charts:donut-chart`, `plugin-charts:radar-chart`, `layout:page-header`, `protocol-placeholder:page-header`). `objectui check` now reports a root document of one of those types as an unknown type, and `objectui validate` keeps refusing it at `type`, as it already did. + +Migration: + +- `pie-chart` / `donut-chart` / `radar-chart` → `chart` with `chartType: "pie"` / `"donut"` / `"radar"`; +- `page-header` → `page:header`, with `title` / `subtitle` / `actions` in `properties`. + +The registered-types ratchet (`REFUSED_AT_TYPE`) falls from 24 to 20, and its namespaced twin from 379 to 374. + +**Clause-②: yes**, released as `minor` with this banner. diff --git a/.changeset/10859-page-header-placeholder.md b/.changeset/10859-page-header-placeholder.md new file mode 100644 index 0000000000..438f62d93d --- /dev/null +++ b/.changeset/10859-page-header-placeholder.md @@ -0,0 +1,13 @@ +--- +'@object-ui/components': minor +--- + +refactor(components)!: drop `page-header` from the opt-in protocol placeholders (objectui#10859, batch 8 phase 2c) + +**BREAKING (authoring):** `PROTOCOL_COMPONENTS` no longer lists `page-header`, so a host that calls `registerPlaceholders()` no longer registers a placeholder under `protocol-placeholder:page-header` or the bare `page-header`. The key was never a protocol page block (the spec's `PageComponentType` member is `page:header`), and `@object-ui/layout` retired its registration in the same release. `page:header` stays in the list. + +Migration: + +- `{ "type": "page-header", "title": T, "subtitle": S }` → `{ "type": "page:header", "properties": { "title": T, "subtitle": S } }`. + +**Clause-②: yes** — a registration leaves the runtime (narrowing), released as `minor` with this banner. diff --git a/.changeset/10859-retire-chart-family-aliases.md b/.changeset/10859-retire-chart-family-aliases.md new file mode 100644 index 0000000000..3f631ad5be --- /dev/null +++ b/.changeset/10859-retire-chart-family-aliases.md @@ -0,0 +1,17 @@ +--- +'@object-ui/plugin-charts': minor +--- + +refactor(plugin-charts)!: retire the `pie-chart`, `donut-chart` and `radar-chart` node type keys; the families are `chart` + `chartType` (objectui#10859, batch 8 phase 2c) + +**BREAKING (authoring):** the plugin no longer registers `pie-chart`, `donut-chart` or `radar-chart` (and with them `plugin-charts:pie-chart`, `plugin-charts:donut-chart` and `plugin-charts:radar-chart`), and their `CHART_TYPE_KEYWORD_FAMILIES` rows are gone. `objectui validate` already refused each at `type`. Their one producer, this package's `examples/chart-examples.ts`, now authors `{ type: 'chart', chartType: … }`, which `objectui validate` accepts on both faces. A node still authored with one of the three types now renders the "Unknown component type" panel. + +Migration: + +- `{ "type": "pie-chart", … }` → `{ "type": "chart", "chartType": "pie", … }`, with the same keys; +- `{ "type": "donut-chart", … }` → `{ "type": "chart", "chartType": "donut", … }`, with the same keys; +- `{ "type": "radar-chart", … }` → `{ "type": "chart", "chartType": "radar", … }`, with the same keys. + +A legacy report section whose `chart.type` says one of the three now reaches the generic `chart` renderer with no family derived from that type; give it the matching `chartType`. `chart:bar` is the one family keyword this package still registers. + +**Clause-②: yes** — registrations leave the runtime (narrowing), released as `minor` with this banner. diff --git a/.changeset/10859-retire-page-header-layout.md b/.changeset/10859-retire-page-header-layout.md new file mode 100644 index 0000000000..dc5070ef78 --- /dev/null +++ b/.changeset/10859-retire-page-header-layout.md @@ -0,0 +1,18 @@ +--- +'@object-ui/layout': minor +--- + +refactor(layout)!: retire the `page-header` node type key; the header node is `page:header` (objectui#10859, batch 8 phase 2c) + +**BREAKING (authoring):** `registerLayout()` no longer registers `PageHeader` under `page-header` (and with it `layout:page-header`). `objectui validate` already refused a `page-header` node at `type`, and nothing in this repository authored or emitted it. objectstack's `page-header-subtitle-alias` conversion renames the key `description` → `subtitle` on both header spellings and leaves the type to objectui. A `page-header` node now renders the "Unknown component type" panel. `PageHeader` stays a named export for JSX composition. + +Migration, measured against `objectui validate` (`page:header` carries its props in `properties`): + +- `{ "type": "page-header", "title": T, "subtitle": S }` → `{ "type": "page:header", "properties": { "title": T, "subtitle": S } }`; +- `actions` (action ids) → `properties.actions`, the same id array; +- `icon` has no `page:header` spelling: the contract refuses `properties.icon` by name (an ADR-0087 D2 tombstone). Drop it, or compose `PageHeader` in JSX, which still takes `icon`; +- `children` (the right-hand slot) has no `page:header` spelling: the contract refuses a node-level `children` by name, and `properties.children` as an unrecognized key. Put the buttons in `properties.actions` as action ids, or compose `PageHeader` in JSX, which still renders its `children`. + +`registerLayout()` now registers four keys: `layout:page:card`, `responsive-grid`, `navigation-renderer` and `app-schema-renderer`. + +**Clause-②: yes** — a registration leaves the runtime (narrowing), released as `minor` with this banner. diff --git a/.changeset/10859-retire-scatter-chart.md b/.changeset/10859-retire-scatter-chart.md index 0f78105244..5b725b7c41 100644 --- a/.changeset/10859-retire-scatter-chart.md +++ b/.changeset/10859-retire-scatter-chart.md @@ -15,3 +15,6 @@ A legacy report section whose `chart.type` says `scatter-chart` now reaches the `pie-chart`, `donut-chart` and `radar-chart` were ruled the same way but stay registered: this package's `examples/chart-examples.ts` still authors them, so they wait on their own decision. **Clause-②: yes** — a registration leaves the runtime (narrowing), released as `minor` with this banner. + +⚠️ **Dated note, 2026-10-02 — the three chart aliases are retired too — objectui#10859.** +The decision this entry said `pie-chart`, `donut-chart` and `radar-chart` wait on is made (the seat's fork ruling on objectui#10859): later in this same release, phase 2c moved `examples/chart-examples.ts` to `{ type: 'chart', chartType: 'pie' | 'donut' | 'radar' }` and then unregistered all three keys, with their `plugin-charts:` twins and `CHART_TYPE_KEYWORD_FAMILIES` rows. The rest of this entry is kept as the reading of this change. diff --git a/.changeset/11044-base-props-one-list.md b/.changeset/11044-base-props-one-list.md index d64069c61a..ab92bbe5b0 100644 --- a/.changeset/11044-base-props-one-list.md +++ b/.changeset/11044-base-props-one-list.md @@ -13,3 +13,9 @@ Three changes to what is accepted come with it: - **The cost is named.** On a type that does not declare one of the six keys, the parser tier no longer reports it, whether or not the renderer reads it. For example, `description` on `page-header` is the retired alias of `subtitle` (objectui#3226), and it used to draw `unknown-prop`. It now draws nothing. `SDUI_BASE_PROPS` and its `SduiBaseProp` / `SduiBasePropScope` types are new exports. Each entry names a `BaseSchema` member, the scope it is a base prop in (`'every-node'` or `'where-undeclared'`), and its type in the generated JSX surface. + +⚠️ **Dated note, 2026-10-02 — the `page-header` example no longer applies — objectui#10859.** +Later in this same release objectui#10859 batch 8 (phase 2c) unregistered `page-header`, so a +`page-header` node now draws `unknown-component` before any prop check runs. The cost this entry names +is unchanged for every type that is still registered. The rest of this entry is kept as the reading of +this change. diff --git a/.changeset/7401-chart-family-from-type.md b/.changeset/7401-chart-family-from-type.md index 3ba8ae20a7..a7074780be 100644 --- a/.changeset/7401-chart-family-from-type.md +++ b/.changeset/7401-chart-family-from-type.md @@ -17,3 +17,9 @@ Later in this same release objectui#10859 batch 8 (phase 2b) unregistered `scatt `plugin-charts:scatter-chart`, and its `CHART_TYPE_KEYWORD_FAMILIES` row went with it; the scatter family is authored as `{ type: 'chart', chartType: 'scatter' }`. `pie-chart`, `donut-chart` and `radar-chart` stay registered. The rest of this entry is kept as the reading of this change. + +⚠️ **Dated note, 2026-10-02 — `pie-chart`, `donut-chart` and `radar-chart` are retired too — objectui#10859.** +Later in this same release objectui#10859 batch 8 (phase 2c) unregistered the three keys, with their +`plugin-charts:` twins and `CHART_TYPE_KEYWORD_FAMILIES` rows, after moving `examples/chart-examples.ts` +to `{ type: 'chart', chartType: 'pie' | 'donut' | 'radar' }`. `chart:bar` is the one family keyword left. +The rest of this entry is kept as the reading of this change. diff --git a/.changeset/9910-children-slot-declare-and-pin.md b/.changeset/9910-children-slot-declare-and-pin.md index bff68e5c11..f021b56039 100644 --- a/.changeset/9910-children-slot-declare-and-pin.md +++ b/.changeset/9910-children-slot-declare-and-pin.md @@ -43,3 +43,8 @@ provider-scoped and portal renderers), `scripts/container-declaration-baseline.j is at zero, and a registration that is a layout container but renders no authored list (`page:tabs`, `page:accordion`, `responsive-grid`) now draws the TRUE `not-a-container` the flag used to silence. + +⚠️ **Dated note, 2026-10-02 — `page-header` is retired — objectui#10859.** +Later in this same release objectui#10859 batch 8 (phase 2c) unregistered `@object-ui/layout`'s +`page-header` (and `layout:page-header`), so its `children` slot declaration left with it; authors write +`page:header`. The rest of this entry is kept as the reading of this change. diff --git a/.changeset/app-shell-page-header-subtitle-4761.md b/.changeset/app-shell-page-header-subtitle-4761.md index 01333ab79c..f1a27197a1 100644 --- a/.changeset/app-shell-page-header-subtitle-4761.md +++ b/.changeset/app-shell-page-header-subtitle-4761.md @@ -44,3 +44,9 @@ pin the card asked for. It asserts the subtitle on the DOM a reader gets (a nothing and is rejected by the compiler, and — the assertion that actually goes red if either side drifts again — that both packages' `PageHeaderComponentProps` declare `subtitle`. + +⚠️ **Dated note, 2026-10-02 — the `page-header` node is retired — objectui#10859.** +Later in this same release objectui#10859 batch 8 (phase 2c) unregistered the `page-header` node key. +`@object-ui/layout`'s `PageHeader` stays an exported component (still spelling its secondary line +`subtitle`), and the authored header node is `page:header`. The rest of this entry is kept as the reading +of this change. diff --git a/apps/console/src/__tests__/unfulfilled-chart-stubs-8760.test.tsx b/apps/console/src/__tests__/unfulfilled-chart-stubs-8760.test.tsx index 3028080076..b07cc8677b 100644 --- a/apps/console/src/__tests__/unfulfilled-chart-stubs-8760.test.tsx +++ b/apps/console/src/__tests__/unfulfilled-chart-stubs-8760.test.tsx @@ -55,8 +55,9 @@ * fails HERE, on the first render nobody has done yet. * 2. THE RETIREMENT, as a render outcome. An authored node of a retired type * now paints the loud OBJUI-001 refusal instead of the eternal skeleton. - * 3. THE CONTROL. `pie-chart` and `bar-chart` — fulfilled variants in the - * same sweep — are unchanged end to end: same registry identity, same + * 3. THE CONTROL. `chart:bar` and `bar-chart` — fulfilled variants in the + * same sweep (`chart:bar` since objectui#10859 batch 8 retired the + * `pie-chart` control) — are unchanged end to end: same registry identity, same * drawn output, no alert and no placeholder. Each is asserted * individually; neither reads zero on both sides of the change. * @@ -85,7 +86,7 @@ import { SchemaRenderer } from '@object-ui/react'; // boundary, and paying that cost at import time puts it outside every // test/hook timeout. The specifier is character-identical to the one // `register-plugins.ts` hands `registerLazy`, so ESM hands the loader the very -// same module instance. It cannot mask the defect it guards against: the four +// same module instance. It cannot mask the defect it guards against: the seven // retired keys are ones this module never registers under ANY load order. import '@object-ui/plugin-charts'; import '../register-plugins'; @@ -94,12 +95,15 @@ const HERE = dirname(fileURLToPath(import.meta.url)); const REGISTER_PLUGINS = join(HERE, '..', 'register-plugins.ts'); /** - * The three spellings objectui#8760 retired, plus `scatter-chart`, which - * objectui#10859 batch 8 (phase 2b) retired with its registration — the same - * two halves must hold for it: no stub left in this app, no key in the - * registry, and a loud refusal for an authored node. + * The three spellings objectui#8760 retired, plus the four objectui#10859 + * batch 8 retired with their registrations — `scatter-chart` (phase 2b) and + * `pie-chart`, `donut-chart`, `radar-chart` (phase 2c). The same two halves + * must hold for each: no stub left in this app, no key in the registry, and a + * loud refusal for an authored node. */ -const RETIRED = ['line-chart', 'area-chart', 'advanced-chart', 'scatter-chart'] as const; +const RETIRED = [ + 'line-chart', 'area-chart', 'advanced-chart', 'scatter-chart', 'pie-chart', 'donut-chart', 'radar-chart', +] as const; /** * The chart stub list, read from the source it is declared in — never a copy. @@ -140,8 +144,8 @@ describe('objectui#8760 — every chart stub this app registers is fulfilled', ( // Non-vacuity for every assertion below: a reader that has stopped reading // must throw or come back empty, never yield a silently passing zero. const list = chartStubList(); - expect(list.length, 'the stub-list read went vacuous').toBeGreaterThanOrEqual(5); - expect(list, 'the reader is looking at the chart loop').toContain('pie-chart'); + expect(list.length, 'the stub-list read went vacuous').toBeGreaterThanOrEqual(3); + expect(list, 'the reader is looking at the chart loop').toContain('chart:bar'); for (const retired of RETIRED) expect(list).not.toContain(retired); }); @@ -158,8 +162,9 @@ describe('objectui#8760 — every chart stub this app registers is fulfilled', ( unfulfilled, 'a lazy stub resolves to nothing — the key renders `Loading …` forever', ).toEqual([]); - // 6 since objectui#10859 batch 8 took `scatter-chart` out of the list. - expect(list).toHaveLength(6); + // 3 since objectui#10859 batch 8 took `scatter-chart` (phase 2b) and + // `pie-chart` / `donut-chart` / `radar-chart` (phase 2c) out of the list. + expect(list).toHaveLength(3); }, 20000); }); @@ -188,7 +193,7 @@ describe('objectui#8760 — CONTROL: the fulfilled variants are unmoved, each on // a DIFFERENT renderer, and each is non-zero on both sides of the change, so // neither can be the "reads 0 either way" kind that proves nothing. it.each([ - ['pie-chart', 'ChartRenderer'], + ['chart:bar', 'ChartRenderer'], ['bar-chart', 'ChartBarRenderer'], ])('`%s` still resolves to `%s`', async (type, renderer) => { await ComponentRegistry.loadLazy(type); @@ -198,7 +203,7 @@ describe('objectui#8760 — CONTROL: the fulfilled variants are unmoved, each on expect(ComponentRegistry.getKnownTypes()).toContain(type); }); - it.each(['pie-chart', 'bar-chart'])('`%s` still draws — no alert, no placeholder', async (type) => { + it.each(['chart:bar', 'bar-chart'])('`%s` still draws — no alert, no placeholder', async (type) => { await ComponentRegistry.loadLazy(type); const shot = paint(type, { chartType: 'pie', data: ROWS, xAxisKey: 'm', dataKey: 'v', series: [{ dataKey: 'v' }] }); expect(shot.alerts, `${type} started refusing — the control moved`).toBe(0); diff --git a/apps/console/src/preview-gallery.tsx b/apps/console/src/preview-gallery.tsx index 2607cfade5..7c7d0ab5ec 100644 --- a/apps/console/src/preview-gallery.tsx +++ b/apps/console/src/preview-gallery.tsx @@ -52,8 +52,10 @@ for (const variant of ['metric', 'metric-card']) { // `register-plugins.ts` alone would have left both keys blessed by // `check:doc-types` from THIS file, so the retirement would have changed // nothing an author can observe. Chart families are reached as -// `{ "type": "chart", "chartType": "line" | "area" }`. -for (const variant of ['chart', 'bar-chart', 'pie-chart']) { +// `{ "type": "chart", "chartType": "line" | "area" }`. `pie-chart` left this list +// with its registration (objectui#10859 batch 8, phase 2c), for the same reason: +// author `{ "type": "chart", "chartType": "pie" }`. +for (const variant of ['chart', 'bar-chart']) { ComponentRegistry.registerLazy(variant, () => import('@object-ui/plugin-charts'), { namespace: 'plugin-charts', category: 'chart', diff --git a/apps/console/src/register-plugins.ts b/apps/console/src/register-plugins.ts index aa9cfbb43a..04f46406de 100644 --- a/apps/console/src/register-plugins.ts +++ b/apps/console/src/register-plugins.ts @@ -112,10 +112,11 @@ ComponentRegistry.registerLazy('chart', () => import('@object-ui/plugin-charts') // in `packages/plugin-charts/src`: `unfulfilled-chart-stubs-8760.test.ts` // drives this very list through the real loader and fails on the first key // that resolves to nothing. -// ⛔ `scatter-chart` is RETIRED from this list with its registration -// (objectui#10859 batch 8, phase 2b) — scatter is `{ "type": "chart", -// "chartType": "scatter" }`. -for (const variant of ['object-chart', 'bar-chart', 'pie-chart', 'donut-chart', 'radar-chart', 'chart:bar']) { +// ⛔ `scatter-chart` (objectui#10859 batch 8, phase 2b) and `pie-chart`, +// `donut-chart`, `radar-chart` (phase 2c) are RETIRED from this list with their +// registrations — those families are `{ "type": "chart", "chartType": "scatter" +// | "pie" | "donut" | "radar" }`. +for (const variant of ['object-chart', 'bar-chart', 'chart:bar']) { ComponentRegistry.registerLazy(variant, () => import('@object-ui/plugin-charts'), { namespace: 'plugin-charts', category: 'chart', diff --git a/apps/site/app/components/InteractiveDemo.tsx b/apps/site/app/components/InteractiveDemo.tsx index b55e341f80..b62df6293e 100644 --- a/apps/site/app/components/InteractiveDemo.tsx +++ b/apps/site/app/components/InteractiveDemo.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react'; import { SchemaRenderer, SchemaRendererContext, toRenderableSchema } from '@object-ui/react'; import { SidebarProvider } from '@object-ui/components'; -// Registers `page-header` & friends — see the module header (objectui#3787). +// Registers `@object-ui/layout`'s blocks — see the module header (objectui#3787). import './registerLayoutBlocks'; import { galleryDataSource } from './galleryDataSource'; import type { SchemaNode } from '@object-ui/core'; diff --git a/apps/site/app/components/LiveSplitDemo.tsx b/apps/site/app/components/LiveSplitDemo.tsx index cfc49b1730..d3fe57f885 100644 --- a/apps/site/app/components/LiveSplitDemo.tsx +++ b/apps/site/app/components/LiveSplitDemo.tsx @@ -24,7 +24,7 @@ import { getExample, type Example, } from '@object-ui/example-schema-catalog'; -// Registers `page-header` & friends — see the module header (objectui#3787). +// Registers `@object-ui/layout`'s blocks — see the module header (objectui#3787). import './registerLayoutBlocks'; const PRESET_IDS = [ diff --git a/apps/site/app/components/SchemaThumbnail.tsx b/apps/site/app/components/SchemaThumbnail.tsx index c297128f92..3835a809bb 100644 --- a/apps/site/app/components/SchemaThumbnail.tsx +++ b/apps/site/app/components/SchemaThumbnail.tsx @@ -19,7 +19,7 @@ import React, { import { SchemaRenderer, SchemaRendererContext, toRenderableSchema } from '@object-ui/react'; import { SidebarProvider } from '@object-ui/components'; import type { SchemaNode } from '@object-ui/core'; -// Registers `page-header` & friends — see the module header (objectui#3787). +// Registers `@object-ui/layout`'s blocks — see the module header (objectui#3787). // Named directly, not reached through the module below: `scripts/__tests__/ // site-playground-layout-registration-3904.test.ts` discovers every // `SchemaRenderer` host and requires it to import THIS module, and a host that diff --git a/apps/site/app/components/registerLayoutBlocks.ts b/apps/site/app/components/registerLayoutBlocks.ts index 7b3472eb0b..4efbd1e058 100644 --- a/apps/site/app/components/registerLayoutBlocks.ts +++ b/apps/site/app/components/registerLayoutBlocks.ts @@ -4,8 +4,11 @@ */ /** - * Registers `@object-ui/layout`'s blocks (`page-header`, `app-shell`, - * `sidebar-nav`, …) for the docs site's schema renderers. + * Registers `@object-ui/layout`'s blocks (`responsive-grid`, + * `navigation-renderer`, `app-schema-renderer`, `layout:page:card`) for the docs + * site's schema renderers. (The `page-header` alias this module was first added + * for was retired by objectui#10859 batch 8; authors write `page:header`, which + * `@object-ui/components` registers.) * * Why this exists (objectui#3787): the site renders catalog examples through * `SchemaRenderer`, and `ComponentRegistry` only knows a type once the package diff --git a/apps/site/app/playground/page.tsx b/apps/site/app/playground/page.tsx index f027cdad67..bfdc28a8ae 100644 --- a/apps/site/app/playground/page.tsx +++ b/apps/site/app/playground/page.tsx @@ -5,11 +5,11 @@ import { SchemaRenderer, toRenderableSchema } from '@object-ui/react'; import type { SchemaNode } from '@object-ui/core'; import dynamic from 'next/dynamic'; import { ObjectUIProvider } from '@/app/components/ObjectUIProvider'; -// Registers `page-header` & friends — see the module header (objectui#3787). +// Registers `@object-ui/layout`'s blocks — see the module header (objectui#3787). // The Playground renders hand-typed schemas, so it needs the same registration // as the three catalog hosts: without it, typing `page-header` here produced the // red OBJUI-001 "Unknown component type" panel for a component the rest of the -// docs site renders fine (objectui#3904). +// docs site rendered fine (objectui#3904). import '@/app/components/registerLayoutBlocks'; import { ChevronLeft, ChevronRight, Monitor, Smartphone, Tablet } from 'lucide-react'; diff --git a/content/docs/guide/layout.md b/content/docs/guide/layout.md index fe71f5e77d..bdb99cfee8 100644 --- a/content/docs/guide/layout.md +++ b/content/docs/guide/layout.md @@ -358,19 +358,19 @@ does not render. > ignores it. A trail of links is a `breadcrumb` node — see > [Breadcrumbs for Deep Navigation](#2-breadcrumbs-for-deep-navigation). -> **`page-header` is an alias, not the author key.** `page-header` (and its namespaced form -> `layout:page-header`) is still accepted: a node written that way renders. It resolves to a -> different renderer, the `PageHeader` component in `@object-ui/layout`, and it has no -> `ComponentPropsMap` row, so props written under it are never checked against -> `PageHeaderProps`. Author `page:header`. +> **`page-header` is retired; author `page:header`.** `page-header` (and its namespaced form +> `layout:page-header`) is no longer registered (objectui#10859): a node written that way +> renders the "Unknown component type" panel, and `objectui validate` refuses it at `type`. +> Write `{ "type": "page:header", "properties": { "title": "…", "subtitle": "…" } }`; the +> secondary line is `subtitle`. ## SidebarNav Component The `SidebarNav` provides a collapsible navigation sidebar with menu items. **`SidebarNav` is a React component, and `sidebar-nav` is not a component key at all.** -`registerLayout()` (`packages/layout/src/index.ts`) registers five keys — `page-header`, -`page:card`, `responsive-grid`, `navigation-renderer` and `app-schema-renderer` — and +`registerLayout()` (`packages/layout/src/index.ts`) registers four keys — `page:card`, +`responsive-grid`, `navigation-renderer` and `app-schema-renderer` — and nothing in this repo registers `sidebar-nav`. What a `{ "type": "sidebar-nav" }` node actually does is measured under [There is no `sidebar-nav` node](#there-is-no-sidebar-nav-node) below. Compose the nav in diff --git a/content/docs/layout/page-header.mdx b/content/docs/layout/page-header.mdx index b8e4157f42..d2ebcd3115 100644 --- a/content/docs/layout/page-header.mdx +++ b/content/docs/layout/page-header.mdx @@ -282,13 +282,19 @@ the display locale) renders `aria-label`, `ariaDescribedBy` renders `aria-descri - **Actions**: up to `maxVisible` inline buttons on desktop and `mobileMaxVisible` on a mobile viewport; the rest fold into the `⋯` menu. -## The `page-header` alias +## The retired `page-header` key -`page-header` (and its namespaced form `layout:page-header`) is still accepted: a node -written that way renders. It is an alias, not the author key. It resolves to a different -renderer, the `PageHeader` component in `@object-ui/layout`, and it has no -`ComponentPropsMap` row, so props written under it are never checked against -`PageHeaderProps`. Author `page:header`. +`page-header` (and its namespaced form `layout:page-header`) is no longer registered +(objectui#10859). A node written that way renders the "Unknown component type" panel, and +`objectui validate` refuses it at `type`. Write the same header as `page:header`, with its +secondary line as `subtitle`: + +```json +{ "type": "page:header", "properties": { "title": "Accounts", "subtitle": "All active accounts" } } +``` + +The `PageHeader` React component in `@object-ui/layout` is still exported for hosts that +compose it in JSX. ## Related diff --git a/examples/schema-catalog/test/pageheader-with-actions.test.tsx b/examples/schema-catalog/test/pageheader-with-actions.test.tsx index 992ae8b446..63fa0a2c63 100644 --- a/examples/schema-catalog/test/pageheader-with-actions.test.tsx +++ b/examples/schema-catalog/test/pageheader-with-actions.test.tsx @@ -11,8 +11,9 @@ * it. * * ⚠️ The file keeps its old name on purpose. Tests in `packages/layout` cite it - * BY PATH for the alias-registration and manifest pins at the bottom (the - * `diagnose()` helper lives here), so renaming it would silently falsify those + * BY PATH for the layout-registration and manifest pins at the bottom (the + * `diagnose()` helper lives here; the `page-header` alias they once pinned is + * retired, objectui#10859), so renaming it would silently falsify those * pointers. The demo it pins was renamed instead: it carries no actions any * more, for the reason the render pin below states. * @@ -244,130 +245,69 @@ describe('the `page:header` registration declares every key the demo writes (obj }); /** - * ── The `page-header` ALIAS registration, below this line ───────────────────── + * ── `@object-ui/layout`'s registrations, below this line ────────────────────── * * Everything from here down is about `@object-ui/layout`'s registrations, not - * about the docs demo. The alias stays registered (retiring it is not - * objectui#3906's job), and `packages/layout` tests point here by path for the - * manifest half of these facts. Until objectui#3906 these pins read the docs - * demo; they now read `ALIAS_FIXTURE`, which is that former demo JSON verbatim, - * so what they measure has not moved. + * about the docs demo. Until objectui#10859 batch 8 (phase 2c) this section + * pinned the `page-header` ALIAS: that a children-bearing alias node validated + * clean of `not-a-container` (#3900), and that its declaration face matched what + * its renderer read (#3972: `icon`, `actions`, the `description` cost of + * objectui#11044), read off `ALIAS_FIXTURE`, the former demo JSON. That phase + * retired the alias, so the alias half is now the retirement pin, and the + * `navigation-renderer` halves, which never depended on it, stay as they were. */ -const ALIAS_FIXTURE = { +const RETIRED_ALIAS_NODE = { type: 'page-header', title: 'Users', subtitle: 'Manage your team members and permissions', - icon: 'users', - children: [ - { type: 'button', label: 'Export', variant: 'outline' }, - { type: 'button', label: 'Add User' }, - ], + children: [{ type: 'button', label: 'Add User' }], }; -/** - * The validator agrees with the alias's render (#3900). - * - * Two directions, and BOTH are load-bearing. Asserting only the first would - * leave the suite green if someone silenced the containment check outright, or - * flipped `isContainer` on by default — the test would then be measuring - * nothing. The second case is the control that keeps the first one meaningful. - */ -describe('the `page-header` alias validates clean of `not-a-container` (#3900)', () => { - const CONTAINMENT = 'not-a-container'; - - it('reports no `not-a-container` for an alias node with children', () => { - const diagnostics = diagnose(ALIAS_FIXTURE); - - // Reachability BEFORE the absence — an empty result proves nothing if the - // containment branch never ran. Two ways it silently would not: - // `validateTree` reports `unknown-component` and returns before the - // containment check when the tag is missing from the manifest, and the - // branch is guarded by `node.children?.length`, so a fixture that lost its - // children would satisfy the assertion below for the wrong reason. - expect(diagnostics.filter((d) => d.code === 'unknown-component')).toEqual([]); - expect(ALIAS_FIXTURE.children.length).toBeGreaterThan(0); - - // Filtered by code, not asserted against an empty diagnostic list: this test - // owns the CONTAINMENT fact only, so a future diagnostic on another key - // belongs to that key's pin, not here. - expect(diagnostics.filter((d) => d.code === CONTAINMENT)).toEqual([]); +describe('the retired `page-header` alias is unknown to the manifest (objectui#10859 batch 8)', () => { + it('draws `unknown-component`, and the canonical `page:header` does not', () => { + // Lit control first: the key authors write still resolves in the same manifest. + expect( + diagnose({ type: 'page:header', title: 'Users' }).filter((d) => d.code === 'unknown-component'), + ).toEqual([]); + expect(diagnose(RETIRED_ALIAS_NODE).map((d) => d.code)).toContain('unknown-component'); }); +}); - it('still reports `not-a-container` for a component that genuinely takes none', () => { - // The control. `navigation-renderer` (registered in the SAME file as the - // #3900 change) is driven entirely by its `items` prop and never reads - // `schema.children`, so children under it ARE an authoring mistake and the - // author must still hear about it. If this component ever legitimately - // becomes a container, this assertion goes red — move the control to - // another childless registration rather than deleting it. - // `items: []` keeps the planted defect — children under a childless - // component — the only thing wrong with this node: an array-valued `items` - // validates clean (#3972) and omitting it draws `missing-required-prop` - // (#3987), both pinned below. +/** + * The containment control (#3900), kept without the alias it used to stand + * beside: `navigation-renderer` (registered in `@object-ui/layout`) is driven + * entirely by its `items` prop and never reads `schema.children`, so children + * under it ARE an authoring mistake and the author must still hear about it. + */ +describe('a layout component that takes no child list still reports `not-a-container` (#3900)', () => { + it('reports `not-a-container` for children under `navigation-renderer`', () => { + // If this component ever legitimately becomes a container, this assertion + // goes red — move the control to another childless registration rather than + // deleting it. `items: []` keeps the planted defect — children under a + // childless component — the only thing wrong with this node: an + // array-valued `items` validates clean (#3972) and omitting it draws + // `missing-required-prop` (#3987), both pinned below. const codes = diagnose({ type: 'navigation-renderer', items: [], children: [{ type: 'button', label: 'Nope' }], }).map((d) => d.code); - expect(codes).toContain(CONTAINMENT); + expect(codes).toContain('not-a-container'); }); }); /** - * The prop face of the same agreement (#3972). + * The prop face of the same agreement (#3972), through the same manifest. * * `registerLayout()`'s `inputs` lists are what `sdui-parser` validates a node's - * top-level props against, so a key the renderer reads and `inputs` omits comes - * back as `unknown-prop`, and a key declared with the wrong `type` comes back as - * `type-mismatch` — on CORRECT authoring, both times. Two keys were in that - * state: `page-header.icon` (the alias's renderer-own key, objectui#3829) and - * `navigation-renderer.items` (declared `object`, actually `NavigationItem[]`). - * - * Every assertion here comes in pairs, positive then control, because "no - * diagnostic" is exactly what a silenced check also looks like. + * top-level props against, so a key declared with the wrong `type` comes back as + * `type-mismatch` on CORRECT authoring. `navigation-renderer.items` was declared + * `object` while the renderer reads `NavigationItem[]`. */ -describe('the alias declaration face matches what the renderers read (#3972)', () => { +describe('the layout declaration face matches what the renderer reads (#3972)', () => { const codesFor = (schema: unknown): string[] => diagnose(schema).map((d) => d.code); - it('draws no `unknown-prop` on the alias fixture', () => { - // Reachability first: the fixture must still WRITE `icon`, otherwise the - // absence below is satisfied by a fixture that stopped exercising the key. - expect(ALIAS_FIXTURE.icon).toBe('users'); - expect(diagnose(ALIAS_FIXTURE).filter((d) => d.code === 'unknown-component')).toEqual([]); - - expect(diagnose(ALIAS_FIXTURE).filter((d) => d.code === 'unknown-prop')).toEqual([]); - }); - - it('still draws `unknown-prop` for a near-miss the declaration does not carry', () => { - // The control: `subTitle` is a misspelling of the declared `subtitle`. If this - // goes green, the check stopped running, and the assertion above is then - // measuring nothing. - expect(codesFor({ ...ALIAS_FIXTURE, subTitle: 'x' })).toContain('unknown-prop'); - }); - - it('draws nothing for `description`, the retired alias of `subtitle` — the named cost of objectui#11044', () => { - // `description` is the legacy alias of `subtitle`, which objectui#3226 removed - // from `inputs` on purpose so the registry stops teaching a second dialect, and - // which objectui#3789 removed from the renderer. It used to draw `unknown-prop` - // here. objectui#11044 (triage ruling) made it, as a `BaseSchema` member some - // registrations declare, a base prop wherever a type declares no input of that - // name, so the parser tier no longer reports it on `page-header`. Pinned so that - // restoring the warning is a deliberate change rather than a silent one. - const schema = { ...ALIAS_FIXTURE, description: 'x' }; - expect(diagnose(schema).filter((d) => d.message.includes('"description"'))).toEqual([]); - }); - - it('accepts an `actions` array on the alias, and reports a non-array', () => { - // `actions` is declared `array` on the alias, matching the canonical - // `page:header`, so the array form validates clean… - expect(codesFor({ type: 'page-header', title: 'Users', actions: ['export'] })).toEqual([]); - // …and a non-array is reported instead of silently accepted. - expect(codesFor({ type: 'page-header', title: 'Users', actions: { export: true } })).toContain( - 'type-mismatch', - ); - }); - it('accepts an array-valued `navigation-renderer.items`, and reports an object', () => { expect(codesFor({ type: 'navigation-renderer', items: [] })).toEqual([]); expect( @@ -444,10 +384,11 @@ describe('the validator reports the node shape that is guaranteed to crash (#398 expect(codes).not.toContain(REQUIRED); // …and the same for a second component in the same registration file, so the - // control is not one prop's accident: `page-header` renders with only a - // title, and every key it declares is optional. - expect(diagnose({ type: 'page-header', title: 'Users' }).map((d) => d.code)).not.toContain( - REQUIRED, - ); + // control is not one prop's accident: `responsive-grid` renders with no + // props at all, and every key it declares is optional. (This was the + // `page-header` alias until objectui#10859 batch 8 retired it.) + const grid = diagnose({ type: 'responsive-grid' }).map((d) => d.code); + expect(grid).not.toContain('unknown-component'); + expect(grid).not.toContain(REQUIRED); }); }); diff --git a/packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx b/packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx index f2ea9a8e9e..b716dd74f3 100644 --- a/packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx +++ b/packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx @@ -33,13 +33,13 @@ * * | package | targets | targets leaking | leaked attributes | * |------------------|---------|-----------------|-------------------| - * | plugin-charts | 8 | 0 | 0 | + * | plugin-charts | 5 | 0 | 0 | * | plugin-calendar | 3 | 0 | 0 | * | plugin-chatbot | 3 | 0 | 0 | * | plugin-dashboard | 7 | 2 | 7 / 9 | * | components | 160 | 90 | 12 .. 15 | * - * **92 of 181 targets leak.** The `components` row is objectui#5574 and is + * **92 of 178 targets leak.** The `components` row is objectui#5574 and is * covered in its own section below; the two `plugin-dashboard` rows are the * older tail. Both are in {@link LEAK_LEDGER}: * `plugin-dashboard:metric` and `plugin-dashboard:metric-card`, the open tail @@ -505,7 +505,10 @@ const CHART_SERIES = [{ dataKey: 'sales' }, { dataKey: 'revenue' }]; * * Since objectui#10859 batch 8 (phase 2b) retired the `scatter-chart` key, the * scatter target is `plugin-charts:chart` with `chartType: 'scatter'` — the - * spelling that draws — and the history below is about the retired key. + * spelling that draws — and the history below is about the retired key. Phase + * 2c retired `pie-chart`, `donut-chart` and `radar-chart` too, so their three + * targets left with their registrations (this sweep has one target per + * registered type). * * ⭐ Until objectui#7401 this target was NOT SWEPT AT ALL. Its registration * declared its family as `defaultProps: { chartType: 'scatter' }`, nothing on @@ -540,7 +543,7 @@ const SCATTER_SERIES = [{ dataKey: 'y' }]; /** * The two OBJECT-BOUND chart targets (`plugin-charts:object-chart`, * `view:chart`). They stay object-bound — `objectName` is what makes them a - * different registry path from the six inline chart targets above, which reach + * different registry path from the three inline chart targets above, which reach * `ObjectChart` through `ObjectChartBlock` and its `ElementDataSourceGate`. * * `data` / `series` are authored on TOP of that binding (`data` is a declared @@ -858,9 +861,6 @@ const TARGETS: Readonly> = { // the scatter arm swept (see SCATTER_DATA). { type: 'plugin-charts:chart', schemaExtras: { chartType: 'scatter', data: SCATTER_DATA, xAxisKey: 'x', series: SCATTER_SERIES }, ready: '[data-slot="chart"]' }, { type: 'plugin-charts:chart:bar', schemaExtras: { data: CHART_DATA, series: CHART_SERIES }, ready: '[data-slot="chart"]' }, - { type: 'plugin-charts:pie-chart', schemaExtras: { data: CHART_DATA, series: CHART_SERIES }, ready: '[data-slot="chart"]' }, - { type: 'plugin-charts:donut-chart', schemaExtras: { data: CHART_DATA, series: CHART_SERIES }, ready: '[data-slot="chart"]' }, - { type: 'plugin-charts:radar-chart', schemaExtras: { data: CHART_DATA, series: CHART_SERIES }, ready: '[data-slot="chart"]' }, { type: 'plugin-charts:object-chart', schemaExtras: OBJECT_CHART_EXTRAS, ready: '[data-slot="chart"]' }, { type: 'view:chart', schemaExtras: OBJECT_CHART_EXTRAS, ready: '[data-slot="chart"]' }, ], @@ -1828,7 +1828,7 @@ const DOCBLOCK_COUNTS = { /** Attributes leaked by the shape with the most members. */ commonestShapeAttributes: 14, /** Every target this sweep renders, all five packages. */ - allTargets: 181, + allTargets: 178, /** Every ledgered row, `plugin-dashboard`'s open tail included. */ allLedgered: 92, } as const; diff --git a/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts b/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts index 150060c054..69facfc728 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts @@ -395,10 +395,10 @@ describe('page palette ↔ spec PageComponentType coverage', () => { * to drop the value silently — now rejects the whole node BY NAME. A retirement * that leaves its producer standing makes the failure worse, not better. * - * The `layout:page-header` ALIAS keeps its own `icon` input, deliberately: that - * is a different renderer with a real read point, and it is guarded separately - * in `packages/layout/src/__tests__/page-header-authorable-keys.test.tsx`. The - * two are opposite read facts about two renderers, not an inconsistency. + * The `layout:page-header` ALIAS kept its own `icon` input, deliberately: that + * was a different renderer with a real read point. objectui#10859 batch 8 + * retired the alias, so `page:header` is the only header node, and the + * contract refuses its `icon` by name (the ADR-0087 D2 tombstone). */ describe('page:header `icon` — the designer field retired with the spec key (#3829)', () => { const fieldNames = () => BLOCK_CONFIG['page:header'].map((f) => f.name); diff --git a/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts b/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts index 88a212896b..f75eba11c0 100644 --- a/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts +++ b/packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts @@ -72,10 +72,11 @@ import { validate } from '../commands/validate.js'; * (iii) keys RETIRED rather than armed — the 28 bare field-widget fallbacks * and the `tree` / `view` aliases, see `RETIRED_BARE_KEYS_10859_BATCH_8`; * batch 8 phase 2b: minus twelve more RETIRED keys, see - * `RETIRED_BARE_KEYS_10859_BATCH_8_PHASE_2B`). LOWER it when a batch arms or - * retires more keys; never raise it. + * `RETIRED_BARE_KEYS_10859_BATCH_8_PHASE_2B`; batch 8 phase 2c: minus four + * more, see `UNREGISTERED_10859_BATCH_8_PHASE_2C`). LOWER it when a batch arms + * or retires more keys; never raise it. * - * The 24 that remain are named in `STILL_REFUSED_10859`, and the row below + * The 20 that remain are named in `STILL_REFUSED_10859`, and the row below * pins the refused set to exactly that list, so this comment cannot drift from * the measurement: * @@ -84,19 +85,24 @@ import { validate } from '../commands/validate.js'; * `spec-report` and `app-schema-renderer` (objectui#11440, which arms them), * plus `navigation-renderer` and `responsive-grid` (objectui#11441, the * maintainer's decision card); - * - fourteen phase 2b FORKS, ruled for retirement but kept registered because - * the premise gate failed on them, and reported on objectui#10859 with the - * evidence: `pie-chart`, `donut-chart` and `radar-chart` (a producer: - * `@object-ui/plugin-charts`' `examples/chart-examples.ts`, which - * objectui#7401 pins), `page-header` (objectstack's `page-header-subtitle-alias` - * conversion and its fixtures author it), and the ten `sidebar-*` primitives - * (with `sidebar-provider` gone, the armed `sidebar` mounts only under a - * host-supplied provider, so the ruled teaching rewrite has no target). + * - the ten `sidebar-*` primitives, phase 2b's third fork. The seat's fork + * ruling (`5948252391`) chose A, conditionally: the armed `sidebar` supplies + * its own `SidebarProvider` when no provider is above it, the teaching is + * rewritten to that node, and the ten retire in phase 2d. * - * The ruling's target after phase 2b was 10; the difference is the fourteen - * forks, each waiting on its own decision. + * Phase 2b's other forks are closed by the same ruling and retired in phase 2c: + * `pie-chart`, `donut-chart` and `radar-chart` once + * `@object-ui/plugin-charts`' `examples/chart-examples.ts`, their one producer, + * moved to `{ type: 'chart', chartType }`; and `page-header`, whose phase-2b + * reason was a misreading. objectstack's `page-header-subtitle-alias` + * conversion renames the KEY `description` → `subtitle` on both header + * spellings. Its docblock says it does not rewrite the type, and that the type + * registration is objectui's to retire on its own schedule; its fixtures are + * test data, not producers. + * + * The ruling's target is 10 after phase 2d; objectui#11440 then takes it to 2. */ -const REFUSED_AT_TYPE = 24; +const REFUSED_AT_TYPE = 20; /** * The head's refused count over the NAMESPACED keys (objectui#10872 batch 1: @@ -109,10 +115,14 @@ const REFUSED_AT_TYPE = 24; * ten namespaced twins of the keys it unregistered — `plugin-charts:scatter-chart`, * `plugin-dashboard:dashboard-grid`, `plugin-form:form-analytics`, * `plugin-grid:import-wizard`, `view:shared-view-link`, the four - * `plugin-designer:` keys and `plugin-detail:related-list`). LOWER it when a - * batch arms or retires more keys; never raise it. + * `plugin-designer:` keys and `plugin-detail:related-list`; objectui#10859 + * batch 8 phase 2c: minus the five namespaced twins of the four keys it + * unregistered — `plugin-charts:pie-chart`, `plugin-charts:donut-chart`, + * `plugin-charts:radar-chart`, `layout:page-header` and + * `protocol-placeholder:page-header`). LOWER it when a batch arms or retires + * more keys; never raise it. */ -const NAMESPACED_REFUSED_AT_TYPE = 379; +const NAMESPACED_REFUSED_AT_TYPE = 374; /** The bare registry keys — the population the card measured. */ const BARE_KEYS = KNOWN_SCHEMA_TYPES.filter((key) => !key.includes(':')); @@ -184,15 +194,28 @@ const RETIRED_BARE_KEYS_10859_BATCH_8_PHASE_2B = [ ]; /** - * Every bare key still refused at `type` after objectui#10859 batch 8 phase 2b + * The four bare keys objectui#10859 batch 8 phase 2c RETIRED by unregistration + * (the seat's fork ruling `5948252391`), each mapped to the namespaced twins + * that went with it. `page-header` had two: `@object-ui/layout`'s registration + * and the opt-in `PROTOCOL_COMPONENTS` placeholder in `@object-ui/components`. + */ +const UNREGISTERED_10859_BATCH_8_PHASE_2C = { + 'pie-chart': ['plugin-charts:pie-chart'], + 'donut-chart': ['plugin-charts:donut-chart'], + 'radar-chart': ['plugin-charts:radar-chart'], + 'page-header': ['layout:page-header', 'protocol-placeholder:page-header'], +} as const; + +/** + * Every bare key still refused at `type` after objectui#10859 batch 8 phase 2c * — the `REFUSED_AT_TYPE` docblock says why each is still here. Alphabetical. */ const STILL_REFUSED_10859 = [ - 'app-schema-renderer', 'detail-section', 'donut-chart', 'embeddable-form', 'home', - 'navigation-renderer', 'object-pivot', 'page-header', 'pie-chart', 'radar-chart', - 'record', 'responsive-grid', 'sidebar-content', 'sidebar-footer', 'sidebar-group', - 'sidebar-header', 'sidebar-inset', 'sidebar-menu', 'sidebar-menu-button', - 'sidebar-menu-item', 'sidebar-provider', 'sidebar-trigger', 'spec-report', 'utility', + 'app-schema-renderer', 'detail-section', 'embeddable-form', 'home', + 'navigation-renderer', 'object-pivot', 'record', 'responsive-grid', + 'sidebar-content', 'sidebar-footer', 'sidebar-group', 'sidebar-header', + 'sidebar-inset', 'sidebar-menu', 'sidebar-menu-button', 'sidebar-menu-item', + 'sidebar-provider', 'sidebar-trigger', 'spec-report', 'utility', ] as const; /** Is `type` unclaimed by every arm of the validator's root union? */ @@ -308,6 +331,21 @@ describe('registered component types refused at `type` — a ratchet (objectui#1 expect(NAMESPACED_KEYS).toContain('record:related_list'); }); + it('counts the four keys batch 8 phase 2c retired as gone from the registry (objectui#10859 batch 8)', () => { + expect(Object.keys(UNREGISTERED_10859_BATCH_8_PHASE_2C)).toHaveLength(4); + for (const [key, twins] of Object.entries(UNREGISTERED_10859_BATCH_8_PHASE_2C)) { + // Retired, not armed: out of the population, still refused at `type`. + expect(BARE_KEYS, key).not.toContain(key); + expect(refusedAtType(key), key).toBe(true); + for (const twin of twins) expect(NAMESPACED_KEYS, twin).not.toContain(twin); + } + // Lit controls: the spellings the retirements point authors to. + expect(BARE_KEYS).toContain('chart'); + expect(refusedAtType('chart')).toBe(false); + expect(NAMESPACED_KEYS).toContain('page:header'); + expect(refusedAtType('page:header')).toBe(false); + }); + it('the refused set is exactly the named remainder — the pin comment cannot drift (objectui#10859 batch 8)', () => { const refused = BARE_KEYS.filter(refusedAtType).sort(); expect(refused).toEqual([...STILL_REFUSED_10859]); diff --git a/packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts b/packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts index c7aadab8a5..780a472532 100644 --- a/packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts +++ b/packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts @@ -33,7 +33,8 @@ * * Silence is the pre-repair reading, so a test that only asserts warnings could * pass against a command that warns about everything. Two fulfilled variants - * from the SAME stub sweep — `bar-chart` and `pie-chart` — must stay silent, + * from the SAME stub sweep — `bar-chart` and `chart:bar` (the `pie-chart` + * control until objectui#10859 batch 8 retired that key) — must stay silent, * and so must the spelling the corrected documentation now teaches, * `{ "type": "chart", "chartType": "line" }`. Each is checked on its own: all * three are non-zero readings (a real registered key, judged), not an absence @@ -118,7 +119,7 @@ describe('objectui#8760 — `objectui check` names the retired chart keys', () = }); describe('objectui#8760 — CONTROL: what still passes, each verified on its own', () => { - it.each(['bar-chart', 'pie-chart'])( + it.each(['bar-chart', 'chart:bar'])( '`%s` — a fulfilled variant from the same sweep — stays silent', async (type) => { // Individually non-zero on both sides: each is a key the charts plugin diff --git a/packages/cli/src/utils/known-schema-types.ts b/packages/cli/src/utils/known-schema-types.ts index fa94132ef6..f9514f0c69 100644 --- a/packages/cli/src/utils/known-schema-types.ts +++ b/packages/cli/src/utils/known-schema-types.ts @@ -101,7 +101,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'dialog', 'div', 'dl', - 'donut-chart', 'drawer', 'dropdown-menu', 'dt', @@ -206,7 +205,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'label', 'layout:app-schema-renderer', 'layout:navigation-renderer', - 'layout:page-header', 'layout:page:card', 'layout:responsive-grid', 'li', @@ -245,7 +243,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'p', 'page', 'page-designer', - 'page-header', 'page:accordion', 'page:card', 'page:footer', @@ -255,17 +252,13 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'page:tabs', 'pagination', 'password', - 'pie-chart', 'pivot', 'plugin-calendar:calendar-view', 'plugin-calendar:object-calendar', 'plugin-charts:bar-chart', 'plugin-charts:chart', 'plugin-charts:chart:bar', - 'plugin-charts:donut-chart', 'plugin-charts:object-chart', - 'plugin-charts:pie-chart', - 'plugin-charts:radar-chart', 'plugin-chatbot:chatbot', 'plugin-chatbot:chatbot-enhanced', 'plugin-chatbot:chatbot-floating', @@ -353,7 +346,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'protocol-placeholder:global:search', 'protocol-placeholder:nav:breadcrumb', 'protocol-placeholder:nav:menu', - 'protocol-placeholder:page-header', 'protocol-placeholder:page:accordion', 'protocol-placeholder:page:card', 'protocol-placeholder:page:footer', @@ -392,7 +384,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'protocol-placeholder:widget:table', 'protocol-placeholder:widget:text', 'q', - 'radar-chart', 'radio-group', 'record', 'record:activity', diff --git a/packages/components/src/renderers/placeholders.tsx b/packages/components/src/renderers/placeholders.tsx index 98ca24bb0b..4c56f29214 100644 --- a/packages/components/src/renderers/placeholders.tsx +++ b/packages/components/src/renderers/placeholders.tsx @@ -79,7 +79,11 @@ const PROTOCOL_COMPONENTS = [ 'field:json', 'field:address', 'field:location', // 12. Page Components - 'page:header', 'page-header', // Added headers + // `page-header` left with its `@object-ui/layout` registration (objectui#10859 + // batch 8, phase 2c): it was never a protocol page block — the spec's + // `PageComponentType` member is `page:header` — and a placeholder here would + // have kept the retired bare key registered in every host that opts in. + 'page:header', 'page:footer', 'page:tabs', 'page:accordion', 'page:card', 'page:sidebar', 'record:details', 'record:highlights', 'record:related_list', 'record:activity', 'record:chatter', 'record:path', diff --git a/packages/layout/README.md b/packages/layout/README.md index cd39170bb8..ff1df6f652 100644 --- a/packages/layout/README.md +++ b/packages/layout/README.md @@ -23,7 +23,7 @@ pnpm add @object-ui/layout ## Registration -Importing this package registers its component keys (`page-header`, `page:card`, +Importing this package registers its component keys (`page:card`, `responsive-grid`, `navigation-renderer`, `app-schema-renderer`) on the `ComponentRegistry` as a module load side effect, so the side-effect-only import is enough: @@ -39,6 +39,10 @@ used to say `"sideEffects": false`, and a bundler honouring it dropped the registration silently). `registerLayout()` is also exported for hosts that prefer to register explicitly. +The `page-header` key is retired (objectui#10859): author `page:header`, which +`@object-ui/components` registers, with the secondary line as `subtitle`. `PageHeader` +stays an export for JSX composition. + ## Components ### AppShell diff --git a/packages/layout/src/__tests__/app-shell-docs-nav-example.test.ts b/packages/layout/src/__tests__/app-shell-docs-nav-example.test.ts index 6cc1163d4c..e06d1f5ff0 100644 --- a/packages/layout/src/__tests__/app-shell-docs-nav-example.test.ts +++ b/packages/layout/src/__tests__/app-shell-docs-nav-example.test.ts @@ -65,8 +65,9 @@ * * Scope: this file scans `content/docs/layout/app-shell.mdx` and nothing else. * The `icon:`-as-string rejection in particular must NOT be widened to the docs - * tree — `page-header` really does declare `icon` as a string (an icon NAME), - * so this is a fact about THIS component's prop on THIS page. Pinning the whole + * tree — `navigation-renderer` really does take `icon` as a string (an icon + * NAME it resolves through `resolveIcon`), so this is a fact about THIS + * component's prop on THIS page. Pinning the whole * docs tree to source is objectui#3786's problem, not this one. */ @@ -327,7 +328,7 @@ describe("app-shell.mdx's SidebarNav examples spell only real keys (objectui#479 'unknown lowercase tag and renders nothing (objectui#4793). Import the Lucide', 'component and pass it.', '', - 'Scope note: `page-header` DOES declare `icon` as a string (an icon name), so this', + 'Scope note: `navigation-renderer` DOES take `icon` as a string (an icon name), so this', 'assertion is deliberately confined to SidebarNav fences on this one page.', ].join('\n'), ).toBe(false); diff --git a/packages/layout/src/__tests__/containment-declared-slot-9910.test.tsx b/packages/layout/src/__tests__/containment-declared-slot-9910.test.tsx index cddf57642b..18ddb6ff40 100644 --- a/packages/layout/src/__tests__/containment-declared-slot-9910.test.tsx +++ b/packages/layout/src/__tests__/containment-declared-slot-9910.test.tsx @@ -13,15 +13,17 @@ * * `packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx` * holds the same contract over the components registry and cannot see this - * package (components does not import layout). Five registrations live here: - * `page-header` renders `schema.children` into its right-hand slot; + * package (components does not import layout). Four registrations live here: * `responsive-grid`, `page:card` (the `layout:`-only thin card) and * `app-schema-renderer` render a React `children` PROP, which `SchemaRenderer` * never fills from a node (it strips `children` before spreading), and - * `navigation-renderer` reads `items`. So exactly one declares the slot, and - * the other four keep drawing `not-a-container` on an authored list — the flag - * three of them carry (`isContainer: true`, layout containment) silences - * nothing, which is the ruling's hard line and is pinned here as the control. + * `navigation-renderer` reads `items`. So none declares the slot, and all four + * keep drawing `not-a-container` on an authored list — the flag three of them + * carry (`isContainer: true`, layout containment) silences nothing, which is + * the ruling's hard line and is pinned here as the control. (The fifth, the + * `page-header` alias, rendered `schema.children` into its right-hand slot and + * declared it; objectui#10859 batch 8 retired that key, so the declaring row + * left with it.) * * The predicate is executed, not read off the source: render through the real * `SchemaRenderer` with one authored child and ask whether it reached the DOM @@ -82,17 +84,16 @@ const declaresChildren = (type: string): boolean => describe('the layout registrations declare the `children` slot exactly where they render it (objectui#9910)', () => { it('has the population it claims', () => { // Anti-vacuity: the census below iterates this set, so an empty or - // mis-filtered set would pass on nothing. The five registrations, in the + // mis-filtered set would pass on nothing. The four registrations, in the // namespaced spelling every one of them has. const keys = layoutKeys(); expect(keys).toEqual(expect.arrayContaining([ - 'layout:page-header', 'layout:page:card', 'layout:responsive-grid', 'layout:navigation-renderer', 'layout:app-schema-renderer', ])); - expect(keys.length).toBeGreaterThanOrEqual(5); + expect(keys.length).toBeGreaterThanOrEqual(4); }); it('declared ⇔ rendered, and the diagnostic follows the declaration', async () => { @@ -109,13 +110,10 @@ describe('the layout registrations declare the `children` slot exactly where the expect(mismatched, 'a layout registration renders children without the slot, or declares a slot it never renders').toEqual([]); }, 60_000); - it('`page-header` is the one that renders the list, and it declares the slot on both keys', async () => { - expect(await rendersChildren('page-header')).toBe(true); - for (const key of ['page-header', 'layout:page-header']) { - const slot = (ComponentRegistry.getMeta(key)?.inputs ?? []).find((i) => i.name === CHILD_LIST_KEY); - expect(slot?.type, `\`${key}\` declares no \`children\` slot`).toBe('slot'); - expect(diagnose(withChildren(key)).filter((d) => d.code === CONTAINMENT)).toEqual([]); - } + it('no layout registration declares the slot since `page-header` retired (objectui#10859 batch 8)', () => { + // The retired alias was the one slot declarer; the census above would pass + // over an empty slot population, so the zero is stated here on purpose. + expect(layoutKeys().filter(declaresChildren)).toEqual([]); }); it('⛔ the layout flag silences nothing: flagged registrations without the slot keep the TRUE warning', async () => { diff --git a/packages/layout/src/__tests__/guide-layout-sidebar-nav-doc.test.ts b/packages/layout/src/__tests__/guide-layout-sidebar-nav-doc.test.ts index 1d0b756b76..0951003d63 100644 --- a/packages/layout/src/__tests__/guide-layout-sidebar-nav-doc.test.ts +++ b/packages/layout/src/__tests__/guide-layout-sidebar-nav-doc.test.ts @@ -18,9 +18,10 @@ * (`content/docs/layout/app-shell.mdx`, pinned by `app-shell-docs-nav-example.test.ts`), * on a THIRD surface — and this one went one level further than either. Those * two misspelled the keys of a real React component. This page taught a JSON - * NODE TYPE that does not exist: `registerLayout()` registers `page-header`, - * `page:card`, `responsive-grid`, `navigation-renderer` and - * `app-schema-renderer`, and nothing in the repo registers `sidebar-nav`. + * NODE TYPE that does not exist: `registerLayout()` registers `page:card`, + * `responsive-grid`, `navigation-renderer` and `app-schema-renderer` (the + * `page-header` alias was a fifth until objectui#10859 batch 8 retired it), and + * nothing in the repo registers `sidebar-nav`. * (`app-shell` was a sixth key until objectui#4841 deregistered it — for the * same reason, one step less far along: it resolved to a component that a JSON * document could never fill. `sidebar-nav` never resolved at all, which is why @@ -75,8 +76,8 @@ * section: the app-shell examples compose it, and `### 5. Sidebar Organization` * under Best Practices was a THIRD block of the same defect, rewritten with this * change. The quoted-`icon` ban in particular must stay scoped that way — - * `page-header` declares `icon` as a string (an icon NAME), and so does - * `navigation-renderer`, whose `resolveIcon` looks names up on purpose. A + * `navigation-renderer` takes `icon` as a string (an icon NAME), whose + * `resolveIcon` looks names up on purpose. A * page-wide ban on `icon: '…'` would be wrong. * * `packages/layout/README.md` belongs to `readme-sidebar-nav-example.test.ts` @@ -495,7 +496,7 @@ describe("the guide's SidebarNav examples pass only real props (objectui#4840)", '(SidebarNav.tsx:60 and :109) — a string reaches React as an unknown lowercase tag and', 'renders nothing (objectui#4840, objectui#3999). Import the Lucide component and pass it.', '', - 'Scope note: `page-header` and `navigation-renderer` DO take an icon NAME as a string,', + 'Scope note: `navigation-renderer` DOES take an icon NAME as a string,', "so this assertion is deliberately confined to fences that mention SidebarNav.", ].join('\n'), ).toBe(false); diff --git a/packages/layout/src/__tests__/page-header-authorable-keys.test.tsx b/packages/layout/src/__tests__/page-header-authorable-keys.test.tsx index 7301f8eedd..116fc7833f 100644 --- a/packages/layout/src/__tests__/page-header-authorable-keys.test.tsx +++ b/packages/layout/src/__tests__/page-header-authorable-keys.test.tsx @@ -7,376 +7,59 @@ */ /** - * The `page-header` alias declares only keys `@objectstack/spec` declares - * (objectui#3226). + * The `page-header` node key is RETIRED (objectui#10859 batch 8, phase 2c), and + * the `PageHeader` component it served stays an export. * - * The legacy kebab key `page-header` and the canonical protocol key - * `page:header` (in `@object-ui/components`) render the same concept, but this - * one used to DECLARE a different authorable key for the secondary line: - * `description`, where the spec's `PageHeaderProps` — and therefore - * `page:header` — declares `subtitle`. That is not a tolerated legacy spelling, - * it is a second dialect published on the declaration surface: `inputs` is what - * the designer offers as fields and what the framework's - * `check:react-declaration-parity` diffs against the spec schemas, so an author - * (especially an AI one) was being TOLD `description` was legal. Metadata that - * took the offer renders a subtitle under `page-header` and silently loses it - * under `page:header` — the same JSON, two results, which is the failure mode - * one contract exists to prevent. + * Until that phase this file pinned the `page-header` alias registration's + * declared surface: that it declared only keys `@objectstack/spec`'s + * `PageHeaderProps` accepts (objectui#3226), judged with + * `@object-ui/test-support`'s tombstone probe (objectui#3809 / objectui#3829, + * which kept the renderer-read `icon` input on a named carve-out); that it + * declared the `children` slot it renders (objectui#3900 / objectui#9910); and + * that it declared `actions` / `icon` with the types the renderer reads + * (objectui#3972). With the registration gone there is no declared surface left + * to judge, so those describes went with it. `page:header`, the key authors + * write, is judged by the repo-wide parity gate in `apps/console`. * - * The cross-check below is deliberately derived from the spec's own shape - * rather than a hand-written allowlist: a future input added here that the spec - * does not declare fails for the same reason `description` did, without anyone - * having to remember this issue. + * What stays pinned here: + * - the retirement itself — `registerLayout()` publishes neither spelling, and + * the lit control is a layout key it still publishes under both + * (`responsive-grid`), the far end the docs site's registrar relies on + * (`scripts/__tests__/site-playground-layout-registration-3904.test.ts`); + * - the rendered half of objectui#3789, on the exported component: a lone + * `description` draws nothing, and `subtitle` wins when both are passed. + * That is a fact about `PageHeader`, which hosts still compose in JSX. * - * SEQUENCING — SETTLED (objectui#3789). This file used to end with two tests - * pinning the runtime `subtitle ?? description` read, because the alias existed - * precisely for out-of-repo consumer schemas: "no in-repo author writes - * `description`" (true, verified) said nothing about whether anyone does, and - * dropping the read while metadata still carried the key would have deleted an - * external page's second line while its title kept rendering — the least - * reportable failure there is. The gate was a measurement, not a date: every - * spec-valid position a `page-header` node can occupy had to be shown to pass - * through a loader that performs the ADR-0087 D2 rewrite - * `page-header-subtitle-alias` (`description` → `subtitle`). It did not, twice: - * the conversion walker reached only `regions[].components[]` - * (objectstack#6775). After objectstack#6776 (slots) and PR #7034 (containers - * nested to any depth), all seven positions convert — re-measured against the - * spec build this repo consumes — so the read and its two pins were deleted in - * the same change. The measurement itself did not go away with them: it is a - * standing pin in `page-header-subtitle-conversion-coverage.test.ts`, which goes - * red if that reach ever narrows again. - * - * TOMBSTONES — objectui#3829. "Derived from the spec's own shape" was doing less - * work than it read as. `Object.keys(shape)` reports a key whether the spec - * ACCEPTS it or REJECTS it by name: an ADR-0087 D2 retirement replaces the - * member with `z.never()` and leaves the entry in place. So when - * @objectstack/spec 17.0.0 retired `PageHeaderProps.icon` (objectstack#6946 / - * PR objectstack#7115), every assertion here that consulted the derived key set - * went on passing for `icon` — describing a spec key that no longer exists. The - * derivation now skips tombstones, and the two facts the old reading conflated - * are stated separately: `actions` is declared because the spec owns it, `icon` - * is declared because THIS renderer draws it (the canonical `page:header` never - * did, which is why upstream retired the key at all). The carve-out is a named, - * issue-backed, self-clearing list rather than a silent pass — see - * `RENDERER_OWN_DECLARED`. - * - * THE JUDGE IS SHARED NOW — objectui#3809, and this file's own note asked for it - * ("the criterion here is deliberately the same one objectui#3809 converges on - * repo-wide; when that lands, this local helper is what it replaces"). The local - * `z.never()` probe is gone, replaced by `@object-ui/test-support`'s. Three - * things change with it, none of them a loosening: - * - * - the same criterion now serves the repo-wide parity gate - * (`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`), where - * its absence was a live FALSE GREEN in one direction and a FALSE RED in the - * other. This file had the fix and could not lend it — that is what a copied - * judgement costs; - * - recognition gained a second channel (the `[REMOVED]` marker `retiredKey()` - * stamps on the description), OR-ed with the structural one, so a Zod - * internals rework can no longer silently turn the probe permissive; - * - the probe is calibrated once, centrally, against what the contract's own - * `safeParse` actually rejects — `packages/test-support/src/__tests__/ - * spec-tombstones.test.ts`. The local non-vacuity test below is KEPT anyway: - * it pins this file's own subject (`icon` retired, `title` live), which no - * amount of central calibration can state. + * The absence keys are literals on purpose: `scripts/__tests__/ + * unit-registry-absence-collision.test.ts` resolves a registry-absence key + * statically and pins the sites it cannot. */ import { describe, it, expect, beforeAll } from 'vitest'; import { render, screen } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; -import { CHILD_LIST_KEY } from '@object-ui/sdui-parser'; -import { PageHeaderProps as SpecPageHeaderProps } from '@objectstack/spec/ui'; -import { - authorableShapeKeys, - isShapeKeyTombstoned, - listedShapeKeys, - shapeMemberTypeName, -} from '@object-ui/test-support'; import { registerLayout, PageHeader } from '../index'; -/** Every key `PageHeaderProps` still LISTS — ADR-0087 tombstones included. */ -const specDeclaredKeys = new Set(listedShapeKeys(SpecPageHeaderProps)); - -/** One `.shape` member's type, unwrapped past `.optional()`. */ -const shapeMemberType = (key: string): string | undefined => - shapeMemberTypeName(SpecPageHeaderProps, key); - -/** - * Is this key an ADR-0087 D2 tombstone — still listed, but rejected by name with - * a migration message? - * - * The distinction is the whole reason this file changed in objectui#3829. A D2 - * retirement does NOT delete the key from the shape; it REPLACES the member with - * `z.never()`. So `Object.keys(shape)` keeps answering "yes, declared" for a key - * the spec refuses — and every assertion below that derived its truth from raw - * `Object.keys` was therefore FALSE GREEN for `icon` from the moment - * @objectstack/spec 17.0.0 retired `PageHeaderProps.icon` (objectstack#6946 / - * PR objectstack#7115). - */ -const isTombstoned = (key: string): boolean => isShapeKeyTombstoned(SpecPageHeaderProps, key); - -/** Authorable keys of the spec node this renderer serves — tombstones excluded. */ -const specKeys = new Set(authorableShapeKeys(SpecPageHeaderProps)); - -/** - * Keys this ALIAS declares on a renderer-read fact the spec no longer carries. - * - * Exactly one entry, and it is a ruling rather than a shortcut: objectui#3829 - * measured `page:header.icon` after upstream retired it and found the two - * renderers disagree. The CANONICAL `page:header` never read the key — which is - * why the spec retired it, and why the metadata-admin designer's icon field for - * that block was removed in the same change. This alias renderer DOES read it - * (`PageHeader.tsx:123`, drawn at `:231-233`), `content/docs/layout/ - * page-header.mdx` publishes the prop, and the docs page's only live demo writes - * `"icon": "users"`. Withdrawing the input would delete a working capability and - * break that demo, so route A was taken: keep the input, and stop pretending the - * spec is what licenses it. - * - * The residual risk is named rather than hidden: `icon` now renders under - * `page-header` and is rejected by name under `page:header`, which is one key - * with two outcomes — the failure mode a single contract exists to prevent. It - * is accepted only because this alias is the legacy registration and the - * capability is real; if the canonical renderer ever grows the same drawing, the - * right move is to un-retire the key upstream, not to widen this list. - */ -const RENDERER_OWN_DECLARED: Record = { - icon: 'Retired upstream as an ADR-0087 D2 tombstone by objectstack#6946 / PR objectstack#7115 because the CANONICAL page:header renderer never read it. This alias renderer reads and draws it (PageHeader.tsx:123, :231-233), the docs page publishes it and its live demo writes it, so objectui#3829 ruled the input stays — declared on the renderer read, not on spec parity.', -}; - -/** - * Keys licensed by the JSON PROTOCOL rather than by `PageHeaderProps`. - * - * Exactly one: `children`, the protocol's base child-list key - * (`BaseSchema.children`; `BASE_PROPS` in `sdui-parser/src/validate.ts`). It - * is not a member of any per-block props schema — the spec declares it only - * on the four `page:*` containers whose whole surface is the slot — and since - * objectui#9910 a renderer that puts `schema.children` on the page declares - * `{ name: 'children', type: 'slot' }` in `inputs`, because that input is the - * ONE thing the tier's `not-a-container` reads. `PageHeader` renders the list - * into its right-hand slot, so it declares the input; the dialect guard below - * must not read that as a second dialect, and this set says so by name. Read - * from the parser's own export so the two cannot drift. - */ -const PROTOCOL_LICENSED = new Set([CHILD_LIST_KEY]); - -const declaredInputs = (type: string, namespace?: string) => { - const config = ComponentRegistry.getConfig(type, namespace); - if (!config) throw new Error(`"${namespace ? `${namespace}:${type}` : type}" is not registered`); - return config.inputs ?? []; -}; - -const declaredInputNames = (type: string, namespace?: string): string[] => - declaredInputs(type, namespace).map((input) => input.name); - beforeAll(() => { registerLayout(); }); -describe('the `page-header` registration declares the spec key, not a dialect', () => { - it('is registered under the bare key and its namespace', () => { - expect(ComponentRegistry.getConfig('page-header')).toBeTruthy(); - expect(ComponentRegistry.getConfig('page-header', 'layout')).toBeTruthy(); - }); - - // The one assertion this issue is about: whatever else changes, the - // declaration surface must never advertise `description` again. - it.each([ - ['page-header', undefined], - ['page-header', 'layout'], - ])('does not advertise `description` on %s (namespace: %s)', (type, namespace) => { - expect(declaredInputNames(type, namespace)).not.toContain('description'); +describe('the `page-header` registration is retired (objectui#10859 batch 8)', () => { + it('publishes neither the bare key nor its `layout` namespace', () => { + // Lit control first: a key `registerLayout()` still publishes, both spellings. + expect(ComponentRegistry.getConfig('responsive-grid')).toBeTruthy(); + expect(ComponentRegistry.getConfig('responsive-grid', 'layout')).toBeTruthy(); + expect(ComponentRegistry.getConfig('page-header')).toBeUndefined(); + expect(ComponentRegistry.getConfig('page-header', 'layout')).toBeUndefined(); + expect(ComponentRegistry.has('layout:page-header')).toBe(false); }); - it('declares `subtitle` — the spec key for the secondary line', () => { - expect(declaredInputNames('page-header')).toContain('subtitle'); - expect(specKeys.has('subtitle')).toBe(true); - // …and the spec has no `description` at all, which is the whole reason the - // old declaration was wrong rather than merely redundant. - expect(specKeys.has('description')).toBe(false); + it('keeps `PageHeader` as an export a host composes directly', () => { + render(); + expect(screen.getByText('Accounts')).toBeTruthy(); + expect(screen.getByText('All active accounts')).toBeTruthy(); }); - - it('the tombstone probe really reads the spec — not `undefined` for everything', () => { - // Guards `isTombstoned`, not the subject. A Zod-internals change that made - // `shapeMemberType` return `undefined` everywhere would silently restore the - // raw `Object.keys` behaviour this file exists to stop, and every assertion - // below would go green describing nothing. Both directions are pinned: a - // known tombstone reads `never`, a known live key does not. - expect(specDeclaredKeys.size).toBeGreaterThan(0); - expect(shapeMemberType('title')).toBeTruthy(); - expect(isTombstoned('title')).toBe(false); - expect(isTombstoned('icon')).toBe(true); - // THE PREMISE, stated locally (objectui#3809): a D2 retirement KEEPS the - // member. Everything above narrows a set that only needs narrowing while - // that is true — if upstream ever retires by deleting the key, `icon` drops - // out of the listed set, this line reds, and the right response is to remove - // the narrowing rather than to keep filtering nothing. - expect(specDeclaredKeys.has('icon')).toBe(true); - // …and the narrowing is not a no-op, which is the third way this could rot. - expect(specKeys.size).toBeLessThan(specDeclaredKeys.size); - }); - - it('declares nothing `@objectstack/spec` accepts, beyond the declared renderer-own keys', () => { - // objectui#3226's original assertion, on the narrowed key set. `specKeys` - // used to be raw `Object.keys(shape)`, which counted a retired `icon` as a - // spec key and let this test pass for the wrong reason. - const offSpec = declaredInputNames('page-header') - .filter((name) => !specKeys.has(name)) - .filter((name) => !(name in RENDERER_OWN_DECLARED)) - .filter((name) => !PROTOCOL_LICENSED.has(name)); - expect(offSpec).toEqual([]); - }); - - it('the protocol carve-out is exactly the child-list key, and it is not a spec key here', () => { - // What keeps `PROTOCOL_LICENSED` from becoming a second dialect door: it - // holds the one key the protocol owns, that key is spelled the way the - // parser spells it, and `PageHeaderProps` really does not carry it (if the - // spec ever declares `children` on this block, the carve-out is dead and - // this line says so). - expect([...PROTOCOL_LICENSED]).toEqual(['children']); - expect(specDeclaredKeys.has(CHILD_LIST_KEY)).toBe(false); - }); - - it('every renderer-own declaration is licensed by a tombstone and says why', () => { - // What keeps `RENDERER_OWN_DECLARED` from becoming the dialect door - // objectui#3226 closed. An entry is legal only for a key the spec ONCE - // declared and has since retired — a key the spec never had is `showBack`, - // and that one is pinned as NOT declared further down. Each clause is - // self-clearing: un-retire the key upstream and the tombstone assertion - // reds, drop the input and the first one does. - for (const [name, reason] of Object.entries(RENDERER_OWN_DECLARED)) { - expect(declaredInputNames('page-header'), `${name} is allowed but not declared`).toContain( - name, - ); - expect(specDeclaredKeys.has(name), `${name} was never a spec key — that is a dialect`).toBe( - true, - ); - expect(isTombstoned(name), `${name} is live spec; it needs no carve-out`).toBe(true); - expect(/#\d+/.test(reason), `${name} needs a tracking issue`).toBe(true); - } - }); -}); - -describe('the `page-header` registration declares the child slot it renders (objectui#3900 / objectui#9910)', () => { - // Same principle as the `inputs` narrowing above, other direction: the - // declaration face must not DENY a surface the component serves either. - // `PageHeader.tsx` deliberately renders `schema.children` into the - // right-hand slot, `content/docs/layout/page-header.mdx` publishes that slot's - // precedence, and the docs page's only live demo is exactly that shape — while - // the registration once declared nothing about it, so `sdui-parser`'s - // `not-a-container` diagnostic fired on it (objectui#3900). Nothing on the - // render path reads the declaration, so the omission broke no rendering; it - // made the validator tell authors (AI authors especially) that a documented, - // demo-verified schema was invalid, which is how the true `not-a-container` - // reports lose their credibility. - // - // objectui#3900 paid that with `isContainer`. objectui#9910 moved the - // declaration the tier reads to the `children` slot input (the flag means - // LAYOUT containment only, objectui#6804) — so the slot is what this block - // pins now, and the flag is pinned beside it for its own reason: the - // react-page JSX scope skips layout containers, and a page header is one. - // - // Not an extension of the spec's authoring surface: `children` is a base - // property of every node in objectui's JSON protocol (`BASE_PROPS` in - // `sdui-parser/src/validate.ts`), not a key of `PageHeaderProps` — which is - // why the dialect guard above licenses it through `PROTOCOL_LICENSED`. - // - // The end-to-end half of this pin — the real demo JSON through the manifest - // the app actually builds, plus the control proving the diagnostic still fires - // for a genuinely childless component — lives in - // `examples/schema-catalog/test/pageheader-with-actions.test.tsx`, next to the - // fixture it validates. - it.each([ - ['page-header', undefined], - ['page-header', 'layout'], - ])('declares the `children` slot on %s (namespace: %s)', (type, namespace) => { - const slot = declaredInputs(type, namespace).find((i) => i.name === CHILD_LIST_KEY); - expect(slot, `${type} declares no \`children\` input`).toBeTruthy(); - expect(slot?.type).toBe('slot'); - }); - - it.each([ - ['page-header', undefined], - ['page-header', 'layout'], - ])('marks %s (namespace: %s) as a LAYOUT container', (type, namespace) => { - expect(ComponentRegistry.getConfig(type, namespace)?.isContainer).toBe(true); - }); -}); - -describe('the `page-header` registration declares the keys the renderer reads (objectui#3972)', () => { - // The `declares nothing @objectstack/spec accepts` test above is ONE-WAY: it - // catches a declared key the spec rejects, and can never catch a spec key the - // renderer honours while `inputs` omits it. That omission is not a documentation - // gap — `sdui-parser/src/validate.ts:70-77` reports `unknown-prop` for any - // top-level key not in `inputs`, so the manifest gate warned authors off keys - // this component renders. `icon` was live on the repo's own documented demo. - // - // Both keys pass the renderer-read × `ManifestInputType` halves of the audit; - // the omissions below pin the audit's negative results, which is what keeps - // this from becoming "copy the spec's shape into `inputs`". - // - // They no longer share a THIRD face, which is why the two are split rather - // than kept in one `it.each`. `actions` is a live spec key and is declared on - // spec parity; `icon` is an ADR-0087 tombstone and is declared on the renderer - // read alone (objectui#3829). One assertion cannot state both facts, and the - // version that tried — `expect(specKeys.has(name)).toBe(true)` for both — was - // green only because the retired member was still listed in the raw shape. - const declaresWithType = (name: string, type: string) => { - for (const namespace of [undefined, 'layout']) { - const input = declaredInputs('page-header', namespace).find((i) => i.name === name); - expect(input, `page-header (namespace: ${namespace}) does not declare \`${name}\``).toBeTruthy(); - expect(input?.type).toBe(type); - } - }; - - it('declares `actions` with type `array`, on both registration keys', () => { - declaresWithType('actions', 'array'); - // …and it is legal to declare because the spec owns the key. If the spec - // ever drops it, this line fails here rather than as a mystery parity red. - expect(specKeys.has('actions')).toBe(true); - }); - - it('declares `icon` with type `string` on both keys — on the renderer read, not spec parity', () => { - // The assertion this issue moved. What is pinned is the ALIAS INPUT ITSELF: - // the input exists, on both registration keys, spelled `string` because the - // authorable form is an icon name. That is the fact the docs page and its - // live demo depend on, and it is unaffected by the spec key's retirement. - declaresWithType('icon', 'string'); - // What DID change is the licence. `icon` is no longer a key the spec - // accepts, so the old `expect(specKeys.has('icon')).toBe(true)` is now - // asserted in reverse — and the two lines under it are why that is a - // retirement rather than a deletion, which is exactly what the raw - // `Object.keys` reading could not tell apart. - expect(specKeys.has('icon')).toBe(false); - expect(specDeclaredKeys.has('icon')).toBe(true); - expect(isTombstoned('icon')).toBe(true); - // The carve-out is stated, not implied: `RENDERER_OWN_DECLARED` is what the - // dialect guard above consults, and its own discipline test licenses this. - expect(Object.keys(RENDERER_OWN_DECLARED)).toContain('icon'); - }); - - it('does not declare `breadcrumb` — a spec key this renderer never reads', () => { - // The other direction of the same audit, and the reason it is not "declare - // every spec key": `PageHeader.tsx` has no `breadcrumb` read point at all - // (the word occurs only in a comment and an `aria-label`). Declaring it would - // publish configuration the component silently drops — objectui#3829's defect, - // which is the exact mistake this file's positive assertions could invite. - expect(specKeys.has('breadcrumb')).toBe(true); - expect(declaredInputNames('page-header')).not.toContain('breadcrumb'); - }); - - // `description` used to ride in this list, on the same "read by the renderer, - // not a spec key" footing. objectui#3789 removed the read, so it no longer - // belongs here — and nothing is lost by dropping it, because the assertion - // that the DECLARATION never re-advertises it is stated outright above - // ("does not advertise `description` on %s"), which is the guard that matters. - it.each(['showBack', 'action'])( - 'does not declare `%s` — read by the renderer, but no spec key exists', - (name) => { - expect(specKeys.has(name)).toBe(false); - expect(declaredInputNames('page-header')).not.toContain(name); - }, - ); }); describe('the retired `description` alias is not read at runtime (objectui#3789)', () => { diff --git a/packages/layout/src/__tests__/page-node-renderer-stays-deleted.test.ts b/packages/layout/src/__tests__/page-node-renderer-stays-deleted.test.ts index a374763c32..2d9b87d972 100644 --- a/packages/layout/src/__tests__/page-node-renderer-stays-deleted.test.ts +++ b/packages/layout/src/__tests__/page-node-renderer-stays-deleted.test.ts @@ -72,7 +72,8 @@ describe('the layout page-node renderer stays deleted (objectui#3223)', () => { it('does not register the `page` key — that owner is @object-ui/components', () => { const barrel = readFileSync(join(SRC, 'index.ts'), 'utf8'); - // `page:card` / `page-header` are this package's own keys and stay. + // `page:card` is this package's own key and stays (the `page-header` alias + // was retired by objectui#10859 batch 8). expect(barrel).not.toMatch(/ComponentRegistry\.register\(\s*['"]page['"]/); }); diff --git a/packages/layout/src/__tests__/readme-sidebar-nav-example.test.ts b/packages/layout/src/__tests__/readme-sidebar-nav-example.test.ts index 2a8d94d926..ddb991c3fe 100644 --- a/packages/layout/src/__tests__/readme-sidebar-nav-example.test.ts +++ b/packages/layout/src/__tests__/readme-sidebar-nav-example.test.ts @@ -47,10 +47,11 @@ * keys accept are half 1's job. * * The scan in half 3's last test is deliberately narrow: it re-reads the fenced - * blocks that mention `SidebarNav` and rejects a quoted `icon:`. `page-header` - * really does take an icon NAME as a string (`src/index.ts`, `type: 'string'`), - * so this must never become a README-wide ban on `icon: '…'` — it is a fact - * about THIS component's prop, scoped to THIS component's examples. + * blocks that mention `SidebarNav` and rejects a quoted `icon:`. + * `navigation-renderer` really does take an icon NAME as a string (its + * `resolveIcon`), so this must never become a README-wide ban on `icon: '…'` — + * it is a fact about THIS component's prop, scoped to THIS component's + * examples. * * Not covered, on purpose: `content/docs/layout/sidebar-nav.mdx` already spelled * these examples correctly, and pinning the whole docs tree to source is @@ -270,7 +271,7 @@ describe("the README's SidebarNav props tables name exactly the real keys (objec 'unknown lowercase tag and renders nothing (objectui#3999). Import the Lucide', 'component and pass it.', '', - 'Scope note: `page-header` DOES declare `icon` as a string (an icon name), so this', + 'Scope note: `navigation-renderer` DOES take `icon` as a string (an icon name), so this', 'assertion is deliberately confined to fences that mention SidebarNav.', ].join('\n'), ).toBe(false); diff --git a/packages/layout/src/index.ts b/packages/layout/src/index.ts index 2bbed2b54c..122cf0133a 100644 --- a/packages/layout/src/index.ts +++ b/packages/layout/src/index.ts @@ -4,7 +4,6 @@ */ import { ComponentRegistry } from '@object-ui/core'; -import { PageHeader } from './PageHeader'; import { PageCard } from './PageCard'; import { ResponsiveGrid } from './ResponsiveGrid'; import { NavigationRenderer } from './NavigationRenderer'; @@ -19,138 +18,43 @@ export * from './NavigationRenderer'; export * from './AppSchemaRenderer'; export function registerLayout() { - // Legacy `page-header` alias. Kept for any consumer schemas that still - // reference the kebab-cased ID; the canonical renderer lives in - // `@object-ui/components` under the protocol-compliant `page:header` - // namespace. We intentionally do NOT re-register `page:header` here — - // doing so would (depending on package load order) clobber the - // record-aware renderer in components with this thinner one. - // - // `inputs` declares the AUTHORABLE surface, and it must name the same keys - // the spec does — the designer and the framework's - // `check:react-declaration-parity` read this list and treat everything in it - // as a legal input (objectui#3226). `@objectstack/spec/ui`'s - // `PageHeaderProps` declares `subtitle`; it has no `description`. This list - // used to declare `description`, so the alias did not merely tolerate a - // legacy spelling — it ADVERTISED a second dialect for the one concept - // `page:header` calls `subtitle`, and metadata authored against it renders - // a subtitle here and nothing at all under the canonical key. - // - // The runtime `subtitle ?? description` fallback in `PageHeader.tsx` is gone - // too (objectui#3789). It was kept for out-of-repo consumer schemas — "no - // in-repo author writes `description`" was never evidence that nobody does — - // and it retired only once the ADR-0087 D2 conversion entry - // `page-header-subtitle-alias` (`description` → `subtitle` at load time, in - // the framework repo) was MEASURED to reach every spec-valid header position: - // regions, `slots.*` (objectstack#6776) and containers nested to any depth, - // `properties.children` / `items[].children` / `body` / `footer` - // (objectstack#6775, PR objectstack-ai/objectstack#7034). That measurement is now a standing pin — - // `__tests__/page-header-subtitle-conversion-coverage.test.ts` — so a spec - // build whose conversion reach narrows again goes red HERE, where the missing - // fallback would otherwise turn into a silently dropped second line. - // - // `isContainer: true` is the other half of the same principle, and it is NOT - // an extension of the spec's authoring surface (objectui#3900). `children` is - // a base property of EVERY node in objectui's JSON protocol — `BASE_PROPS` in - // `sdui-parser/src/validate.ts` lists it beside `type`/`id`/`className` — not - // a key of the spec's `PageHeaderProps`. So this flag answers the protocol - // question "does this node accept a child list?", and for this component the - // answer has always been yes: `PageHeader.tsx` deliberately re-introduces - // `schema.children` into the right-hand slot (that is how `record:quick_actions` - // nests under `page:header.children`), `content/docs/layout/page-header.mdx` - // publishes the slot's precedence as contract, and the docs page's only live - // demo is exactly that shape. - // - // Leaving the flag off did not make children illegal — nothing on the render - // path reads `isContainer` — it made the VALIDATOR lie: `sdui-parser`'s - // `not-a-container` diagnostic fired on the documented, demo-verified, - // correctly-rendering write-up. A warning that lies is worse than a missing - // one, because it trains authors (AI authors especially) to discount the true - // `not-a-container` reports on components that really are childless. - // - // `icon` and `actions` are the same lie on two more keys, found by auditing - // this list against the renderer's actual read points (objectui#3972). Each - // declared key below is aligned on the faces that have to agree, and the - // audit's negative results are as load-bearing as its positive ones: - // - // RENDERER READS IT × A `ManifestInputType` CAN SPELL IT - // - `title` / `subtitle` — read at `PageHeader.tsx:121/122`; both are live - // spec keys of `PageHeaderProps`, which is the objectui#3226 narrowing - // above. - // - `icon` — read at `PageHeader.tsx:123`, rendered at `:231-233` (a - // string goes through `LazyIcon`, a node renders as-is). This one stands - // on the RENDERER-READ fact ALONE, and the change of footing is - // deliberate rather than an oversight: `PageHeaderProps.icon` was retired - // upstream as an ADR-0087 D2 tombstone in @objectstack/spec 17.0.0 - // (objectstack#6946 / PR objectstack#7115) because the CANONICAL - // `page:header` renderer never read it. THIS renderer does — it draws the - // icon beside the title — `content/docs/layout/page-header.mdx` publishes - // the prop, and the docs page's only live demo - // (`layout-page-header/pageheader-with-actions`) writes `"icon": "users"`, - // so withdrawing the input would delete a working capability and make the - // manifest gate report `unknown-prop` on the repo's own documented - // example. `type: 'string'` because the AUTHORABLE spelling is an icon - // name; the `React.ReactNode` form is a programmatic prop, not JSON. - // objectui#3829 ruled it stays on exactly this footing. What keeps the - // claim honest — and stops the retired spec key from reading as parity — - // is `__tests__/page-header-authorable-keys.test.tsx`, which skips - // tombstones when it derives the spec's key set. - // - `actions` — read at `:125`, resolved at `:199-203` and delegated to - // `record:quick_actions`; `PageHeaderProps.actions` is a LIVE spec key (an - // array of action ids), and the canonical `page:header` already publishes - // it as `type: 'array'` (`components/.../containers.tsx:1692`). Spelled - // identically here on purpose — one concept, one key, one type. - // - // NOT declared, deliberately, and each for its own reason: - // - `breadcrumb` — spec declares it, this renderer has NO read point (the - // word appears only in a comment and an `aria-label`). Declaring it would - // be objectui#3829's defect in reverse: an authoring surface the platform - // silently drops. (`page:header.icon` WAS that same case on the CANONICAL - // renderer — which is why it sits in `UNPUBLISHED_EXEMPTIONS` in - // `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`, and - // why the spec retired the key there while `icon` stays declared HERE: - // different renderers, opposite read facts.) - // - `showBack` / `action` — this renderer reads them, the spec has no such - // keys. Declaring one would publish a second dialect, which is the whole - // point of the objectui#3226 narrowing above. (`description` used to sit - // in this line for the same reason; objectui#3789 removed the read, so it - // is no longer a key this renderer honours in any spelling.) - // - `aria` — spec declares it; omitted for the reason every block omits it - // (accessibility escape hatch, not a layout choice). - ComponentRegistry.register('page-header', PageHeader, { - namespace: 'layout', - label: 'Page Header', - category: 'Layout', - // LAYOUT containment (objectui#6804 / objectui#9910 Q2). It no longer - // carries the "renders a child list" fact — the `children` slot input - // below does, and it is the one thing `sdui-parser`'s `not-a-container` - // reads (objectui#9910 Q1; objectui#3900 is the card that first paid for - // the flag being wrong here). - isContainer: true, - inputs: [ - { name: 'title', type: 'string' }, - { name: 'subtitle', type: 'string' }, - { name: 'icon', type: 'string', description: 'Lucide icon name' }, - { - name: 'actions', - type: 'array', - description: 'ActionDef list (or action ids) rendered through record:quick_actions', - }, - // The right-hand slot `PageHeader` renders `schema.children` into - // (`content/docs/layout/page-header.mdx` publishes its precedence). - // `children` is the protocol's base child-list key, not a - // `PageHeaderProps` member, so this line is licensed by the protocol - // rather than by spec parity — `__tests__/page-header-authorable-keys` - // states that carve-out by name. - { name: 'children', type: 'slot', description: 'Right-hand slot content; a JSX `children` prop wins over it' }, - ], - }); + // ⛔ The `page-header` node type key is RETIRED (objectui#10859 batch 8, + // phase 2c, the seat's fork ruling on that card, by the objectui#10393 / + // objectui#8760 route). The page header authors write is `page:header`, + // which `@object-ui/components` registers and the spec's `PageHeaderProps` + // declares (its secondary line is `subtitle`). `PageHeader` itself stays a + // named export of this package. + // + // What was here: a registration of `PageHeader` under the kebab key + // `page-header` in the `layout` namespace, so it stored `layout:page-header` + // and the bare `page-header` fallback, with `isContainer: true` and five + // declared inputs (`title`, `subtitle`, `icon`, `actions`, the `children` + // slot). (Described rather than quoted, for the reason the `app-shell` note + // below gives: the pins that read this file take a verbatim call in a comment + // for a live registration.) No `@object-ui/types` arm claims the key, so + // `objectui validate` refused a `page-header` node at `type` while the + // registry mounted it. + // + // Why unregistering is the whole retirement: no source, doc, example or + // catalog document in this repository authors the node, and nothing emits + // it. objectstack's ADR-0087 conversion `page-header-subtitle-alias` renames + // the KEY `description` → `subtitle` on both header spellings; its own + // docblock says it does not rewrite the type, and that this registration "is + // objectui's to retire, on its own schedule". Its fixtures are test data, not + // producers. The opt-in `PROTOCOL_COMPONENTS` placeholder entry for the same + // spelling (`@object-ui/components`) went in the same change, so no host can + // bring the bare key back. The `subtitle ?? description` fallback this alias + // once carried was already gone (objectui#3789); the conversion-reach pin in + // `__tests__/page-header-subtitle-conversion-coverage.test.ts` stays, because + // it guards `page:header`. // Page Card — register ONLY as `layout:page:card`. `skipFallback` keeps this // thin div from clobbering the bare `page:card` key, which belongs to the // record-aware PageCardRenderer in @object-ui/components (it renders // title/body/children; this one only renders React children and would - // otherwise leak schema props onto the DOM). Same rationale as `page-header`. + // otherwise leak schema props onto the DOM). The retired `page-header` alias + // above made the same choice the other way round, by never claiming + // `page:header`. ComponentRegistry.register('page:card', PageCard, { namespace: 'layout', skipFallback: true, diff --git a/packages/plugin-charts/examples/chart-examples.ts b/packages/plugin-charts/examples/chart-examples.ts index b017d92d33..379f40c5d2 100644 --- a/packages/plugin-charts/examples/chart-examples.ts +++ b/packages/plugin-charts/examples/chart-examples.ts @@ -7,13 +7,19 @@ */ /** - * Example: Using all new chart types - * - * This example demonstrates the new Pie, Donut, Radar, and Scatter chart types + * Example: the pie, donut and radar chart families + * + * Each example is the generic `chart` node with `chartType` naming the family, + * the spelling that draws (objectui#8760). Until objectui#10859 batch 8 (phase + * 2c) they authored the `pie-chart` / `donut-chart` / `radar-chart` keys, which + * that phase retired once these examples, their last producer, had moved. + * `__tests__/chart-family-from-type-7401.test.tsx` renders each one and asserts + * it draws its family and no bar (objectui#7401, ruling item 3). */ export const pieChartExample = { - type: 'pie-chart', + type: 'chart', + chartType: 'pie', data: [ { name: 'Chrome', value: 65 }, { name: 'Firefox', value: 20 }, @@ -31,7 +37,8 @@ export const pieChartExample = { }; export const donutChartExample = { - type: 'donut-chart', + type: 'chart', + chartType: 'donut', data: [ { category: 'Electronics', revenue: 45000 }, { category: 'Clothing', revenue: 32000 }, @@ -43,7 +50,8 @@ export const donutChartExample = { }; export const radarChartExample = { - type: 'radar-chart', + type: 'chart', + chartType: 'radar', data: [ { skill: 'React', score: 90 }, { skill: 'TypeScript', score: 85 }, diff --git a/packages/plugin-charts/src/__tests__/chart-family-from-type-7401.test.tsx b/packages/plugin-charts/src/__tests__/chart-family-from-type-7401.test.tsx index 29f2154998..ba06f79fa9 100644 --- a/packages/plugin-charts/src/__tests__/chart-family-from-type-7401.test.tsx +++ b/packages/plugin-charts/src/__tests__/chart-family-from-type-7401.test.tsx @@ -32,6 +32,13 @@ * two scatter rows of the table above are the card's historical measurement; * the scatter cases below pin the retirement and the spelling that draws. * + * ⚠️ Since phase 2c, `pie-chart`, `donut-chart` and `radar-chart` are RETIRED + * the same way, after `examples/chart-examples.ts` (their last producer) moved + * to `{ type: 'chart', chartType: 'pie' | 'donut' | 'radar' }`. Of the keywords + * the card fixed, `chart:bar` alone stays registered, so the "renders as + * itself" rows now run on it and on the spelling that draws, and the retired + * keys are pinned absent under both spellings with no family derived. + * * ## Why these render through the REAL SDUI path * * The defect lived between the registry and the renderer, so a unit test on @@ -144,22 +151,29 @@ const refusal = (c: HTMLElement) => describe('objectui#7401 — a chart-type registration renders as itself', () => { /** - * Both spellings, because the SDUI path reaches one registration by either - * and the card measured BOTH drawing a bar. A fix that handled only the - * namespaced key would leave `type: 'pie-chart'` — the spelling the docs and - * `examples/chart-examples.ts` actually use — still broken. + * Both spellings of the surviving keyword, because the SDUI path reaches one + * registration by either and the card measured BOTH drawing the default bar. + * `chart:bar` names the bar family, so this row pins the derivation through + * the registry; the next row pins the families the retired keywords named, + * through the spelling that draws them. */ it.each([ - ['pie-chart', 'pie'], - ['plugin-charts:pie-chart', 'pie'], - ['donut-chart', 'pie'], - ['plugin-charts:donut-chart', 'pie'], - ['radar-chart', 'radar'], - ['plugin-charts:radar-chart', 'radar'], - ])('%s draws %s marks and no bars', async (type, family) => { + ['chart:bar'], + ['plugin-charts:chart:bar'], + ])('%s draws bar marks through the derived family', async (type) => { const container = await renderSchema({ type, data: ROWS, xAxisKey: 'xm', series: ONE_SERIES }); - expect(marks(container, family), `${type} drew no ${family}`).toBeGreaterThan(0); - expect(marks(container, 'bar'), `${type} still drew bars`).toBe(0); + expect(marks(container, 'bar'), `${type} drew no bar`).toBeGreaterThan(0); + expect(refusal(container)).toBeNull(); + }); + + it.each([ + ['pie', 'pie'], + ['donut', 'pie'], + ['radar', 'radar'], + ])('chart with chartType %s draws %s marks and no bars', async (chartType, family) => { + const container = await renderSchema({ type: 'chart', chartType, data: ROWS, xAxisKey: 'xm', series: ONE_SERIES }); + expect(marks(container, family), `chartType ${chartType} drew no ${family}`).toBeGreaterThan(0); + expect(marks(container, 'bar'), `chartType ${chartType} still drew bars`).toBe(0); expect(refusal(container)).toBeNull(); }); @@ -176,11 +190,11 @@ describe('objectui#7401 — a chart-type registration renders as itself', () => * that carries it. Both halves are needed: the row above proves the marks * reach the DOM, this proves which of the two families they were drawn as. */ - it('donut-chart and pie-chart resolve to DIFFERENT families', () => { - expect(normalizeChartSchema({ type: 'donut-chart' }).chartType).toBe('donut'); - expect(normalizeChartSchema({ type: 'plugin-charts:donut-chart' }).chartType).toBe('donut'); - expect(normalizeChartSchema({ type: 'pie-chart' }).chartType).toBe('pie'); - expect(normalizeChartSchema({ type: 'plugin-charts:pie-chart' }).chartType).toBe('pie'); + it('chartType donut and chartType pie resolve to DIFFERENT families', () => { + expect(normalizeChartSchema({ type: 'chart', chartType: 'donut' }).chartType).toBe('donut'); + expect(normalizeChartSchema({ type: 'plugin-charts:chart', chartType: 'donut' }).chartType).toBe('donut'); + expect(normalizeChartSchema({ type: 'chart', chartType: 'pie' }).chartType).toBe('pie'); + expect(normalizeChartSchema({ type: 'plugin-charts:chart', chartType: 'pie' }).chartType).toBe('pie'); }); /** @@ -206,14 +220,28 @@ describe('objectui#7401 — a chart-type registration renders as itself', () => }); it('the retired scatter-chart key is unregistered under both spellings and derives no family', () => { - // Lit control first: a sibling alias this package still registers. - expect(ComponentRegistry.get('radar-chart')).toBe(ChartRenderer); + // Lit control first: the keyword this package still registers. + expect(ComponentRegistry.get('chart:bar')).toBe(ChartRenderer); expect(ComponentRegistry.get('scatter-chart')).toBeUndefined(); expect(ComponentRegistry.get('plugin-charts:scatter-chart')).toBeUndefined(); expect(familyFromComponentType('scatter-chart')).toBeUndefined(); expect(familyFromComponentType('plugin-charts:scatter-chart')).toBeUndefined(); }); + it('the retired pie-chart, donut-chart and radar-chart keys are unregistered under both spellings and derive no family', () => { + // Lit control first, as above. Literal keys, one per assertion. + expect(ComponentRegistry.get('plugin-charts:chart:bar')).toBe(ChartRenderer); + expect(ComponentRegistry.get('pie-chart')).toBeUndefined(); + expect(ComponentRegistry.get('plugin-charts:pie-chart')).toBeUndefined(); + expect(ComponentRegistry.get('donut-chart')).toBeUndefined(); + expect(ComponentRegistry.get('plugin-charts:donut-chart')).toBeUndefined(); + expect(ComponentRegistry.get('radar-chart')).toBeUndefined(); + expect(ComponentRegistry.get('plugin-charts:radar-chart')).toBeUndefined(); + expect(familyFromComponentType('pie-chart')).toBeUndefined(); + expect(familyFromComponentType('donut-chart')).toBeUndefined(); + expect(familyFromComponentType('radar-chart')).toBeUndefined(); + }); + /** * The card's CONTROL row, unchanged: the generic `chart` type with an * explicit `chartType` already reached the family it named, and an explicit @@ -227,11 +255,12 @@ describe('objectui#7401 — a chart-type registration renders as itself', () => }); it('an explicit chartType wins over the type-derived family', async () => { + // `chart:bar` derives `bar`; the explicit `line` still wins. const container = await renderSchema({ - type: 'pie-chart', chartType: 'line', data: ROWS, xAxisKey: 'xm', series: ONE_SERIES, + type: 'chart:bar', chartType: 'line', data: ROWS, xAxisKey: 'xm', series: ONE_SERIES, }); expect(marks(container, 'line')).toBeGreaterThan(0); - expect(marks(container, 'pie')).toBe(0); + expect(marks(container, 'bar')).toBe(0); }); }); @@ -239,13 +268,21 @@ describe('objectui#7401 — a chart-type registration renders as itself', () => * Ruling item 3 — `packages/plugin-charts/examples/chart-examples.ts` is the * in-repo victim the card named (`type: 'pie-chart'`, drawing a bar). It is * confirmed here rather than argued: the example object itself is rendered. + * + * objectui#10859 batch 8 (phase 2c, the seat's fork ruling) moved the three + * examples to the spelling that draws — `{ type: 'chart', chartType }` — before + * retiring the keys they used to author. The intent of this pin survives the + * move: each example still draws the family it names and no bar. The spelling + * is pinned too, so an example cannot drift back onto a retired key. */ describe('objectui#7401 — the in-repo examples draw what they say', () => { it.each([ - ['pieChartExample', pieChartExample, 'pie'], - ['donutChartExample', donutChartExample, 'pie'], - ['radarChartExample', radarChartExample, 'radar'], - ])('%s draws %s', async (_name, example, family) => { + ['pieChartExample', 'pie', 'pie', pieChartExample], + ['donutChartExample', 'donut', 'pie', donutChartExample], + ['radarChartExample', 'radar', 'radar', radarChartExample], + ])('%s (chartType %s) draws %s marks and no bars', async (_name, chartType, family, example) => { + expect((example as Record).type).toBe('chart'); + expect((example as Record).chartType).toBe(chartType); const container = await renderSchema(example as Record); expect(marks(container, family)).toBeGreaterThan(0); expect(marks(container, 'bar')).toBe(0); diff --git a/packages/plugin-charts/src/index.tsx b/packages/plugin-charts/src/index.tsx index ea5580c1f4..ee578d8101 100644 --- a/packages/plugin-charts/src/index.tsx +++ b/packages/plugin-charts/src/index.tsx @@ -138,9 +138,9 @@ ComponentRegistry.register( ); /** - * ⭐ The chart-family registrations below (`chart:bar`, `pie-chart`, - * `donut-chart`, `radar-chart`; `scatter-chart` was retired by objectui#10859 - * batch 8) declare their family in ONE + * ⭐ The chart-family registration below (`chart:bar`; `pie-chart`, + * `donut-chart`, `radar-chart` and `scatter-chart` were retired by + * objectui#10859 batch 8) declares its family in ONE * place, and it is not here: `CHART_TYPE_KEYWORD_FAMILIES` in * `normalizeChartSchema.ts`, which `ChartRenderer` resolves through on every * render. @@ -177,35 +177,34 @@ ComponentRegistry.register( } ); -ComponentRegistry.register( - 'pie-chart', - ChartRenderer, - { - namespace: 'plugin-charts', - label: 'Pie Chart', - category: 'plugin' - } -); - -ComponentRegistry.register( - 'donut-chart', - ChartRenderer, - { - namespace: 'plugin-charts', - label: 'Donut Chart', - category: 'plugin' - } -); - -ComponentRegistry.register( - 'radar-chart', - ChartRenderer, - { - namespace: 'plugin-charts', - label: 'Radar Chart', - category: 'plugin' - } -); +/** + * ⛔ The `pie-chart`, `donut-chart` and `radar-chart` node type keys are + * RETIRED (objectui#10859 batch 8, phase 2c, the seat's fork ruling on that + * card, by the objectui#10393 / objectui#8760 route). Each family is reached as + * `{ type: 'chart', chartType: 'pie' | 'donut' | 'radar' }`, the spelling that + * draws ("chartType is the spelling that draws", objectui#8760). + * + * ## What was here, and why it went + * + * Three registrations of the generic `ChartRenderer`, one per key, each in the + * `plugin-charts` namespace, so each stored `plugin-charts:KEY` and the bare + * `KEY` fallback; their families came from `CHART_TYPE_KEYWORD_FAMILIES` rows + * (objectui#7401), which went in the same change. No `@object-ui/types` arm + * claims any of the three, so `objectui validate` refused such a node at + * `type` while the registry mounted it. + * + * ## Why unregistering is the whole retirement + * + * Phase 2b found one producer and reported the three as a fork: + * `examples/chart-examples.ts` in this package authored all three, pinned by + * objectui#7401's "the in-repo examples draw what they say". The fork ruling + * moved those examples to `chart` + `chartType` first; the 7401 pin still + * asserts each draws its family and no bar. Re-measured for phase 2c after that + * move: 0 producers in source, docs, examples, the catalog or objectstack, and + * 0 runtime emission. The console's lazy stubs went in the same change, so no + * table or host keeps a key alive. The pie, donut and radar FAMILIES are + * untouched. + */ /** * ⛔ The `scatter-chart` node type key is RETIRED (objectui#10859 batch 8, @@ -216,8 +215,10 @@ ComponentRegistry.register( * * ## What was here, and why it went * - * `ComponentRegistry.register('scatter-chart', ChartRenderer, { namespace: - * 'plugin-charts', ... })` — a second key on the generic renderer, which stored + * A registration of the generic `ChartRenderer` under `scatter-chart` in the + * `plugin-charts` namespace (described, not quoted: source readers such as + * `@object-ui/types`' node-slot test take a quoted call in a comment for a live + * one) — a second key on the generic renderer, which stored * both `plugin-charts:scatter-chart` and the bare `scatter-chart` fallback, and * whose family came from its `CHART_TYPE_KEYWORD_FAMILIES` row (objectui#7401). * No `@object-ui/types` arm claims it, so `objectui validate` refused a node @@ -231,9 +232,8 @@ ComponentRegistry.register( * (`apps/console/src/register-plugins.ts`) went in the same change, so no table * or host keeps the key alive. The `scatter` FAMILY is untouched. * - * ⚠️ `pie-chart`, `donut-chart` and `radar-chart` above were ruled the same way - * and stay registered: `examples/chart-examples.ts` in this package still - * authors all three, and objectui#7401's "the in-repo examples draw what they - * say" pins that file. That is a producer, so phase 2b reported them as a fork - * instead of retiring them. + * `pie-chart`, `donut-chart` and `radar-chart` were ruled the same way. Phase + * 2b kept them registered as a fork, because `examples/chart-examples.ts` still + * authored them; phase 2c retired them once those examples had moved (the note + * above). */ diff --git a/packages/plugin-charts/src/normalizeChartSchema.ts b/packages/plugin-charts/src/normalizeChartSchema.ts index c2354eb1ee..4e6be5159e 100644 --- a/packages/plugin-charts/src/normalizeChartSchema.ts +++ b/packages/plugin-charts/src/normalizeChartSchema.ts @@ -127,9 +127,15 @@ const CHART_TYPES = new Set([ * `CHART_TYPES` answers "is this bare `type` a chart FAMILY" — `'pie'`, * `'scatter'`, the spec's own vocabulary. These are something else: they are * SDUI component discriminators, the keys `index.tsx` hands - * `ComponentRegistry.register`. `type: 'pie-chart'` does not name a family in + * `ComponentRegistry.register`. `type: 'chart:bar'` does not name a family in * any spec; it names a registration whose whole purpose is to draw one. * + * Since objectui#10859 batch 8 the table holds that one row: the + * `pie-chart`, `donut-chart`, `radar-chart` (phase 2c) and `scatter-chart` + * (phase 2b) keys were retired with their rows, and those families are reached + * as `{ type: 'chart', chartType: … }`. The history below is why the table + * exists at all. + * * ## Why the derivation exists at all (objectui#7401) * * Each of these registrations declared its family as @@ -163,19 +169,18 @@ const CHART_TYPES = new Set([ */ export const CHART_TYPE_KEYWORD_FAMILIES: ReadonlyMap = new Map([ ['chart:bar', 'bar'], - ['pie-chart', 'pie'], - ['donut-chart', 'donut'], - ['radar-chart', 'radar'], - // ⛔ `scatter-chart` is RETIRED (objectui#10859 batch 8, phase 2b): its - // registration left `index.tsx` in the same change, and objectui#7401's - // pin fails on a row with no registration. Scatter is drawn by - // `{ type: 'chart', chartType: 'scatter' }`. + // ⛔ `pie-chart`, `donut-chart`, `radar-chart` (objectui#10859 batch 8, + // phase 2c) and `scatter-chart` (phase 2b) are RETIRED: each registration + // left `index.tsx` with its row, and objectui#7401's pin fails on a row with + // no registration. Those families are drawn by + // `{ type: 'chart', chartType: 'pie' | 'donut' | 'radar' | 'scatter' }`. ]); /** * The namespace `index.tsx` registers every keyword above under. The SDUI path - * reaches a registration by either spelling — bare (`pie-chart`) or namespaced - * (`plugin-charts:pie-chart`) — and objectui#7401 measured BOTH drawing a bar, + * reaches a registration by either spelling — bare (`chart:bar`) or namespaced + * (`plugin-charts:chart:bar`; objectui#7401 measured the retired `pie-chart` / + * `plugin-charts:pie-chart` pair) — and objectui#7401 measured BOTH drawing a bar, * so both are resolved here. */ const PLUGIN_CHARTS_NAMESPACE = 'plugin-charts:'; @@ -601,9 +606,9 @@ export function normalizeChartSchema( // `chartType` (internal) → `specType` (an author `type` rescued from the // envelope collision) → `type` (only when it is unambiguously a chart family // and not a component discriminator). - // A registered chart-type keyword (`pie-chart`) is read LAST, after both + // A registered chart-type keyword (`chart:bar`) is read LAST, after both // explicit spellings: an author who writes `chartType: 'line'` on a - // `pie-chart` node still gets a line, exactly as objectui#7401's ruling + // `chart:bar` node still gets a line, exactly as objectui#7401's ruling // requires, and `plugin-charts:chart` with an explicit `chartType` — the // card's control row — is untouched. const rawType = str(schema.type); diff --git a/packages/test-support/README.md b/packages/test-support/README.md index df6778ef84..258b46122f 100644 --- a/packages/test-support/README.md +++ b/packages/test-support/README.md @@ -54,8 +54,7 @@ code imports — nothing in `src/` of a released package may import this. `Object.keys(schema.shape)` cannot answer, because a retired key stays in the shape (objectui#3809). Consumed by `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` (both parity - directions), `packages/layout/src/__tests__/page-header-authorable-keys.test.tsx`, - `packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts` + directions), `packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts` and `packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts` (the last two converged off local structural-only copies by objectui#4947). No copy of the judgement is left in-tree: gates import this module, they do diff --git a/packages/test-support/src/__tests__/spec-tombstones.test.ts b/packages/test-support/src/__tests__/spec-tombstones.test.ts index 39f4fa7e8e..62a641bd10 100644 --- a/packages/test-support/src/__tests__/spec-tombstones.test.ts +++ b/packages/test-support/src/__tests__/spec-tombstones.test.ts @@ -29,9 +29,10 @@ * * The real half is deliberately DERIVED, with no key names pinned here. The * consuming gates pin their own key sets (`apps/console/src/__tests__/ - * registry-inputs-spec-parity.test.ts` and `packages/layout/src/__tests__/ + * registry-inputs-spec-parity.test.ts`, and until objectui#10859 batch 8 + * retired the alias it judged, `packages/layout/src/__tests__/ * page-header-authorable-keys.test.tsx`), each against the surface it judges; - * a third copy of those names in this file would only add a place to forget. + * another copy of those names in this file would only add a place to forget. */ import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts b/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts index d079dbb1cf..d372b66e49 100644 --- a/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts +++ b/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts @@ -272,13 +272,15 @@ describe('objectui#8499 — `line-chart` stays unarmed, and the reason stays che ].map((m) => m[1]); // Non-vacuity: the reader must actually find the console's registrations. expect(stubbed.length, 'the console stub read went vacuous').toBeGreaterThanOrEqual(20); - expect(stubbed, 'the reader is looking at the chart stubs').toContain('pie-chart'); + // `chart:bar` since objectui#10859 batch 8 retired the `pie-chart` stub. + expect(stubbed, 'the reader is looking at the chart stubs').toContain('chart:bar'); const pluginSource = read(CHARTS_PLUGIN); const registered = [...pluginSource.matchAll(/register\(\s*\n?\s*'([^']+)'/g)].map((m) => m[1]); // Non-vacuity: the reader must actually find this module's registrations. - expect(registered.length, 'the registration read went vacuous').toBeGreaterThanOrEqual(6); - expect(registered).toContain('pie-chart'); + // Four since objectui#10859 batch 8 retired four chart-family keys. + expect(registered.length, 'the registration read went vacuous').toBeGreaterThanOrEqual(4); + expect(registered).toContain('chart:bar'); for (const type of RETIRED_8760) { expect(stubbed, `${type} is stubbed in the console again`).not.toContain(type); diff --git a/scripts/__tests__/side-effects-declaration-consistency.test.ts b/scripts/__tests__/side-effects-declaration-consistency.test.ts index 10293dc491..f83ae32876 100644 --- a/scripts/__tests__/side-effects-declaration-consistency.test.ts +++ b/scripts/__tests__/side-effects-declaration-consistency.test.ts @@ -1519,7 +1519,8 @@ describe('objectui#3899 specimen: @object-ui/layout registers through the source 'Bundling `import "@object-ui/layout";` through the workspace src alias dropped the component ' + 'registrations. This is objectui#3899 exactly: a bundler took the manifest at its word. Check ' + 'that `sideEffects` names "./src/index.ts".', - ).toContain('page-header'); + // `responsive-grid` since objectui#10859 batch 8 retired `page-header`. + ).toContain('responsive-grid'); // Every key the barrel registers, so losing all but one cannot pass on the // strength of that one. Read out of the source rather than listed here: @@ -1586,7 +1587,7 @@ describe('objectui#3899 specimen: @object-ui/layout registers through the source code, 'A copy of this package declaring `sideEffects: false` kept its registrations, so the source-alias ' + 'probe above is not measuring the manifest at all.', - ).not.toContain('page-header'); + ).not.toContain('responsive-grid'); } finally { rmSync(path.resolve(mirrored, '..'), { recursive: true, force: true }); } diff --git a/scripts/__tests__/site-playground-layout-registration-3904.test.ts b/scripts/__tests__/site-playground-layout-registration-3904.test.ts index 96d1a33114..7dacb6ab73 100644 --- a/scripts/__tests__/site-playground-layout-registration-3904.test.ts +++ b/scripts/__tests__/site-playground-layout-registration-3904.test.ts @@ -14,11 +14,13 @@ import { fileURLToPath } from 'node:url'; * `SchemaThumbnail`, `LiveSplitDemo`). The Playground renders HAND-TYPED schemas * and was outside that acceptance surface, so it kept resolving `page-header` to * nothing and painting the red OBJUI-001 "Unknown component type" panel — for a - * component the rest of the site renders fine. The placeholder net does not catch - * it either: `page-header` lives in the opt-in `PROTOCOL_COMPONENTS` of + * component the rest of the site rendered fine. The placeholder net did not catch + * it either: `page-header` then lived in the opt-in `PROTOCOL_COMPONENTS` of * `packages/components/src/renderers/placeholders.tsx`, not in the eagerly * registered `PALETTE_PLACEHOLDER_BLOCKS`, and `registerPlaceholders()` is only - * called by `apps/console`. + * called by `apps/console`. (objectui#10859 batch 8 later retired `page-header` + * itself; the registrar still carries `@object-ui/layout`'s other blocks, so the + * defect class this file pins is unchanged.) * * This is pinned by DISCOVERY, not by a hardcoded list of four: the bug was a host * that nobody remembered to add. A fifth host added tomorrow is caught the same @@ -108,7 +110,7 @@ describe('objectui#3904 — every apps/site SchemaRenderer host registers the la expect( imports.test(source), `${host} renders but never imports ${REGISTRAR}, so types owned by ` + - '@object-ui/layout (page-header, app-shell, sidebar-nav) resolve to nothing and render ' + + '@object-ui/layout (responsive-grid, navigation-renderer, app-schema-renderer) resolve to nothing and render ' + 'the red OBJUI-001 panel. Add: import ' + "'@/app/components/registerLayoutBlocks';" ).toBe(true); @@ -120,11 +122,12 @@ describe('objectui#3904 — the registrar the hosts import actually registers th * The middle link of the chain. Without it the suite above is satisfied by an * import of a module that registers nothing. * - * The far end — `registerLayout()` really putting `page-header` into the + * The far end — `registerLayout()` really putting its blocks into the * `ComponentRegistry` — is already pinned next to the implementation, in - * `packages/layout/src/__tests__/page-header-authorable-keys.test.tsx` - * ("is registered under the bare key and its namespace"), so it is deliberately - * not restated here: that file owns it, and #3899 is working that package. + * `packages/layout/src/__tests__/page-header-authorable-keys.test.tsx`, whose + * lit control is `responsive-grid` under both spellings (the `page-header` + * key this file first cited was retired by objectui#10859 batch 8), so it is + * deliberately not restated here: that file owns it. */ const registrar = fs.readFileSync( path.join(SITE_APP_DIR, 'components/registerLayoutBlocks.ts'),