From a05506ae34860cc93426e27a182e908bd36e3b4f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 11:40:13 +0000 Subject: [PATCH 1/3] feat(spec): retire page.components[].responsive (ADR-0049 D2) and repair the four redirect texts (#11027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last carrier of the ResponsiveConfig layout block measured dead across both repos (zero callers of useResponsiveConfig / ResponsiveProtocol), so the key is a retiredKey() tombstone and the orphaned value schemas (ResponsiveConfigSchema, the two breakpoint maps, BreakpointName) leave with it. Every shipped text that prescribed the key as the live alternative — the #4876 widget tombstone, the generated references, the upgrade-guide prose, the protocol-17 migration rationale, the conversion summary, and the dashboard liveness note — is corrected to point at responsiveStyles (ADR-0065). ADR-0087: retired-key + retired-def entries under protocol 18 and the page-component-responsive-removed conversion, wired into the step-18 chain. Co-Authored-By: Claude --- .../page-component-responsive-removed.md | 106 ++++++ content/docs/protocol/objectui/layout-dsl.mdx | 23 +- content/docs/references/index.mdx | 10 +- content/docs/references/ui/dashboard.mdx | 2 +- content/docs/references/ui/page.mdx | 2 +- content/docs/references/ui/responsive.mdx | 72 +--- content/docs/ui/pages.mdx | 2 +- ...07-unknown-key-strictness-ledger.counts.md | 14 +- docs/protocol-upgrade-guide.md | 4 +- packages/spec/api-surface/ui.json | 7 - packages/spec/authorable-surface/ui.json | 18 +- packages/spec/export-origins/ui.json | 7 - packages/spec/json-schema.manifest/ui.json | 4 - packages/spec/liveness/dashboard.json | 4 +- packages/spec/liveness/page.json | 4 +- packages/spec/spec-changes.json | 4 +- packages/spec/src/conversions/registry.ts | 105 +++++- .../18.ui__BreakpointColumnMap.ts | 8 + .../retired-defs/18.ui__BreakpointName.ts | 11 + .../retired-defs/18.ui__BreakpointOrderMap.ts | 8 + .../retired-defs/18.ui__ResponsiveConfig.ts | 14 + .../18.ui__PageComponent__responsive.ts | 28 ++ packages/spec/src/migrations/registry.ts | 89 ++++- .../src/type-alias-convention.pin.test.ts | 21 +- packages/spec/src/ui/dashboard.test.ts | 39 +-- packages/spec/src/ui/dashboard.zod.ts | 34 +- .../ui/interaction-config-retirement.test.ts | 7 +- packages/spec/src/ui/page.test.ts | 49 ++- packages/spec/src/ui/page.zod.ts | 31 +- packages/spec/src/ui/responsive.test.ts | 233 ++----------- packages/spec/src/ui/responsive.zod.ts | 313 ++++-------------- .../src/ui/widget-i18n-retirement.test.ts | 3 +- skills/objectstack-ui/references/_index.md | 2 +- 33 files changed, 622 insertions(+), 656 deletions(-) create mode 100644 .changeset/page-component-responsive-removed.md create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__BreakpointColumnMap.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__BreakpointName.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__BreakpointOrderMap.ts create mode 100644 packages/spec/src/migrations/entries/retired-defs/18.ui__ResponsiveConfig.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.ui__PageComponent__responsive.ts diff --git a/.changeset/page-component-responsive-removed.md b/.changeset/page-component-responsive-removed.md new file mode 100644 index 0000000000..e25f43fa76 --- /dev/null +++ b/.changeset/page-component-responsive-removed.md @@ -0,0 +1,106 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire `page.components[].responsive` and the `ResponsiveConfig` layout vocabulary; repair every shipped text that prescribed it (#11027, ADR-0049 D2) + + + +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the migration prescription is +registered under protocol major 18, where `os migrate meta` users will look). + +`page.components[].responsive` was the LAST carrier of the per-breakpoint +LAYOUT block (`ResponsiveConfig`: grid columns / visibility / display order on +the Tailwind `xs…2xl` axis) — and the destination the +`dashboard.widgets[].responsive` tombstone (#4876) prescribed verbatim as the +live alternative ("which objectui `useResponsiveConfig` really does read"). +Measured across objectstack + objectui with the tsc-probe methodology +(positive and negative controls; objectui `3b147a367`, objectstack +`8d21f7a76`): that claim was false. objectui's two complete, published +implementations of the contract — `useResponsiveConfig` (`@object-ui/mobile`) +and `ResponsiveProtocol` (`@object-ui/core`) — had ZERO callers, nothing in +either repo read `.responsive` off a page component, objectui's own +`BaseSchema` node interface never declared the key, and zero authored +instances exist. An author following the shipped prescription moved an inert +key to an inert key and was told it now works. Identical disposition to +`view.responsive` (#3896) and `dashboard.widgets[].responsive` (#4876) on +identical evidence. + +**What is refused:** an authored `responsive` on a page component. The key is +a `retiredKey()` tombstone, so authoring it is a `tsc` error and a parse error +carrying the prescription. + +**What leaves with it:** `ResponsiveConfigSchema` / `ResponsiveConfig`, +`BreakpointColumnMapSchema` / `BreakpointColumnMap`, +`BreakpointOrderMapSchema` / `BreakpointOrderMap`, and the `BreakpointName` +enum — no other authorable carrier existed, and an exported value schema with +no consumer reads as a capability (#3950; the `PerformanceConfigSchema` +precedent). Importing any of them is TS2305 from this release. + +**What stays:** `responsiveStyles` (ADR-0065, `ResponsiveStylesSchema` / +`StyleMapSchema`) — the per-breakpoint channel objectui really compiles to +id-scoped CSS — is untouched and is what every repaired text now points at. + +The redirect repair (the reason this ships as one change): four author-facing +surfaces shipped the false redirect and are corrected together — the #4876 +tombstone prescription (`dashboard.zod.ts`), the generated widget reference +page, the protocol upgrade guide prose, and the protocol-17 migration +rationale that `os migrate meta --from 16` prints. The `dashboard.json` +liveness note and the #4876 conversion summary carried the same claim and are +corrected too; `responsive.zod.ts`'s `hidden → hiddenOn` alias curation, which +justified itself by `useResponsiveConfig`'s return shape, leaves with the +schema that hosted it. + +The retirement kit: + +- `retiredKey()` tombstone at the schema (`packages/spec/src/ui/page.zod.ts`), + prescription pointing at `responsiveStyles` with the CSS translations for + `columns` / `hiddenOn` / `order` +- ADR-0087 registration: retired-key entry `ui/PageComponent:responsive`, four + retired-def entries (`ui/ResponsiveConfig`, `ui/BreakpointColumnMap`, + `ui/BreakpointOrderMap`, `ui/BreakpointName`), and the D2 conversion + `page-component-responsive-removed` (protocol 18), wired into the step-18 + chain — `os migrate meta --from 17` strips the key from authored pages at + every component position, region, slot, or nested container (pure lossless + delete; it never had an effect to lose) +- pin tests (`page.test.ts` — refusal pin asserts the prescription; a positive + pin parses a component without the key and asserts `responsiveStyles` + survives; `dashboard.test.ts`'s #4876 pins now assert the corrected + prescription instead of the false redirect) +- `ResponsiveStylesSchema` guidance for `columns` / `hiddenOn` / `order` now + names the retirement and the CSS that IS applied, instead of prescribing the + dead sibling key +- generated baselines/docs follow the schema (authorable surface, JSON-schema + manifest, api-surface, export-origins, spec-changes, upgrade guide, + reference docs, skill references) +- objectui's two dead consumer implementations are the other half of this + measurement and are queued under objectui#4773 (this package's texts no + longer point authors at them) + +## FROM → TO + +```ts +// before — parsed green; no renderer ever applied any of it +{ + type: 'element:text', + id: 'kpi_label', + responsive: { + columns: { xs: 12, lg: 4 }, + order: { xs: 2, lg: 1 }, + hiddenOn: ['xs'], + }, +} + +// after — express per-breakpoint behaviour as scoped CSS (ADR-0065), which +// objectui compiles and applies (desktop-first buckets) +{ + type: 'element:text', + id: 'kpi_label', + responsiveStyles: { + large: { gridColumn: 'span 4', order: '1' }, + small: { gridColumn: 'span 12', order: '2' }, + xsmall: { display: 'none' }, + }, +} +``` diff --git a/content/docs/protocol/objectui/layout-dsl.mdx b/content/docs/protocol/objectui/layout-dsl.mdx index 73642ff923..0b2e0c53fc 100644 --- a/content/docs/protocol/objectui/layout-dsl.mdx +++ b/content/docs/protocol/objectui/layout-dsl.mdx @@ -791,22 +791,6 @@ interface FormField { } ``` -### Responsive Columns - -{/* os:check */} -```typescript -// Source: packages/spec/src/ui/responsive.zod.ts (BreakpointColumnMapSchema) -// Grid columns (1-12) per named breakpoint. All entries optional. -interface ResponsiveColumns { - xs?: number; // ~480px - sm?: number; // ~640px - md?: number; // ~768px - lg?: number; // ~1024px - xl?: number; // ~1280px - '2xl'?: number; // ~1536px -} -``` - ### Visibility Rule Visibility is a single **CEL expression** string, not a structured rule object. @@ -844,8 +828,9 @@ error names the offending key and, when it looks like a visibility predicate, points you back at `visibleWhen`. Breakpoint-based show/hide is handled separately via the component's -`responsive.hiddenOn` array (e.g. `hiddenOn: ['xs', 'sm']`), see -`packages/spec/src/ui/responsive.zod.ts`. +`responsiveStyles` scoped CSS (ADR-0065), e.g. `xsmall: { display: 'none' }` — +see `packages/spec/src/ui/responsive.zod.ts`. (The former `responsive.hiddenOn` +layout key was retired in v17.x; no renderer ever applied it.) #### Position-gated visibility @@ -1032,4 +1017,4 @@ block. See - [Page Reference](/docs/references/ui/page) - Page, region, and component schemas - [View Reference](/docs/references/ui/view) - Form, list, kanban, and calendar views -- [Responsive Reference](/docs/references/ui/responsive) - Breakpoint layout and scoped responsive styles +- [Responsive Reference](/docs/references/ui/responsive) - Per-breakpoint scoped responsive styles (ADR-0065) diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 5f74ebfba9..5c963ac4f4 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1586 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1582 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 8 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 36 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 156 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **199** | **1586** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 152 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **199** | **1582** | 14 protocol modules | --- @@ -365,7 +365,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 156 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 152 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -384,7 +384,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`notification.zod.ts`](/docs/references/ui/notification) | `NotificationPosition`, `NotificationSeverity`, `NotificationType` | | [`page.zod.ts`](/docs/references/ui/page) | `ElementDataSource`, `InterfacePageConfig`, `Page`, `PageComponent`, `PageComponentType`, `PageRegion`, `PageType`, `PageVariable` | | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | -| [`responsive.zod.ts`](/docs/references/ui/responsive) | `BreakpointColumnMap`, `BreakpointName`, `BreakpointOrderMap`, `ResponsiveConfig`, `ResponsiveStyles`, `StyleMap` | +| [`responsive.zod.ts`](/docs/references/ui/responsive) | `ResponsiveStyles`, `StyleMap` | | [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` | | [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `FormButtonConfig`, `FormField`, `FormFieldPublicPicker`, `FormSection`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index d4dc5f3969..ffb11d642c 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -110,7 +110,7 @@ Dashboard header action | **options** | `{ dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; sortBy?: string; sortOrder?: Enum<'asc' \| 'desc'>; limit?: integer; … } & Record` | optional | Widget specific configuration | | **filterBindings** | `Record` | optional | Per-widget dashboard-filter bindings: filter name → this widget's field, or false to opt out | | **suppressWarnings** | `string[]` | optional | Build diagnostic rule ids suppressed on this widget | -| **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, which objectui `useResponsiveConfig` really does read; move the layout there if you need breakpoint behaviour today. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. This message used to point at `page.components[].responsive` as the live home of the shared `ResponsiveConfig` shape; that key was measured equally unread and removed with the shape in #11027. For breakpoint behaviour that IS applied, use `responsiveStyles` on a page component (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **aria** | `never` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 D2) — no renderer ever applied it, so ARIA attributes declared on a widget silently did not reach the DOM: the key promised accessibility compliance it did not deliver. This is the same removal the dashboard-level `aria` got in 17.0.0 (#3896). Delete the key. The dashboard renderer emits its own `aria-*` attributes for the widget grid; author a `title` (and `description`) on the widget instead — those ARE what the renderer labels the card with. The shared `AriaProps` shape is NOT gone: it stays live on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | ### Allowed Values: `DashboardWidget.type` diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 09e5b6967e..4baab160bb 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -116,7 +116,7 @@ Interface-level page configuration (Airtable parity) | **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.`. The shipping renderer additionally mounts `app`, `features`, `os.user` and binds `data` to the data-source ADAPTER here — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). ⚠️ `data` is surface-dependent: on a `page:tabs` item `visibleWhen` it is the record ROW instead. e.g. "page.selectedProjectId != ''" | | **visibility** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. | | **dataSource** | `{ object: string; view?: string; filter?: any; sort?: object[]; … }` | optional | Per-element data binding for multi-object pages | -| **responsive** | `{ breakpoint?: Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>; hiddenOn?: Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>[]; columns?: object; order?: object }` | optional | Responsive layout configuration | +| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (#11027, ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | | **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | diff --git a/content/docs/references/ui/responsive.mdx b/content/docs/references/ui/responsive.mdx index 156c84049c..5431b66c56 100644 --- a/content/docs/references/ui/responsive.mdx +++ b/content/docs/references/ui/responsive.mdx @@ -12,79 +12,13 @@ description: Responsive protocol schemas ## TypeScript Usage ```typescript -import { BreakpointColumnMapSchema, BreakpointName, BreakpointOrderMapSchema, ResponsiveConfigSchema, ResponsiveStylesSchema, StyleMapSchema } from '@objectstack/spec/ui'; -import type { BreakpointColumnMap, BreakpointName, BreakpointOrderMap, ResponsiveConfig, ResponsiveStyles, StyleMap } from '@objectstack/spec/ui'; +import { ResponsiveStylesSchema, StyleMapSchema } from '@objectstack/spec/ui'; +import type { ResponsiveStyles, StyleMap } from '@objectstack/spec/ui'; // Validate data -const result = BreakpointColumnMapSchema.parse(data); +const result = ResponsiveStylesSchema.parse(data); ``` ---- - -## BreakpointColumnMap - -Grid columns per breakpoint (1-12) - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **xs** | `number` | optional | | -| **sm** | `number` | optional | | -| **md** | `number` | optional | | -| **lg** | `number` | optional | | -| **xl** | `number` | optional | | -| **2xl** | `number` | optional | | - - ---- - -## BreakpointName - -### Allowed Values - -* `xs` -* `sm` -* `md` -* `lg` -* `xl` -* `2xl` - - ---- - -## BreakpointOrderMap - -Display order per breakpoint - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **xs** | `number` | optional | | -| **sm** | `number` | optional | | -| **md** | `number` | optional | | -| **lg** | `number` | optional | | -| **xl** | `number` | optional | | -| **2xl** | `number` | optional | | - - ---- - -## ResponsiveConfig - -Responsive layout configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **breakpoint** | `Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>` | optional | Minimum breakpoint for visibility | -| **hiddenOn** | `Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>[]` | optional | Hide on these breakpoints | -| **columns** | `{ xs?: number; sm?: number; md?: number; lg?: number; … }` | optional | Grid columns per breakpoint | -| **order** | `{ xs?: number; sm?: number; md?: number; lg?: number; … }` | optional | Display order per breakpoint | - - --- ## ResponsiveStyles diff --git a/content/docs/ui/pages.mdx b/content/docs/ui/pages.mdx index c7d8a136d3..47cadf0fc4 100644 --- a/content/docs/ui/pages.mdx +++ b/content/docs/ui/pages.mdx @@ -188,7 +188,7 @@ The `type` field is a union of the standard `PageComponentType` enum and any cus - **AI:** `ai:chat_window`, `ai:suggestion` - **Elements:** `element:text`, `element:number`, `element:image`, `element:divider`, `element:button`, `element:record_picker`, `element:text_input` (`element:filter` and `element:form` were retired in v17.x — no renderer ever shipped for either. List surfaces own their filtering via a view's `userFilters` quick-filter bar or the list toolbar's filter builder; for forms use the object-bound `object-form` block, which is rendered and designer-publishable) -Components may also carry `dataSource` (per-element object binding for multi-object pages), `responsive`, and `aria` configuration. Custom string types are also accepted for project-specific widgets. +Components may also carry `dataSource` (per-element object binding for multi-object pages), `responsiveStyles` (per-breakpoint scoped CSS, ADR-0065), and `aria` configuration. Custom string types are also accepted for project-specific widgets. (The former `responsive` layout block was retired in v17.x — no renderer ever applied it; see the upgrade guide.) ## Variables diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index 0b75db4377..81f80c1aa1 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -21,7 +21,7 @@ regenerate. | Measure | Value | |---|---| | Triaged directories | 5 | -| Object sites in them | 439 | +| Object sites in them | 436 | | Still-open (strip) sites | 124 | | Files carrying at least one | 22 | @@ -44,12 +44,12 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `ui/` | 171 | 159 | 5 | 0 | 7 | +| `ui/` | 168 | 156 | 5 | 0 | 7 | | `data/` | 156 | 74 | 1 | 0 | 81 | | `automation/` | 65 | 42 | 0 | 0 | 23 | | `security/` | 20 | 7 | 0 | 0 | 13 | | `studio/` | 27 | 27 | 0 | 0 | 0 | -| **total** | **439** | **309** | **6** | **0** | **124** | +| **total** | **436** | **306** | **6** | **0** | **124** | ## File-level triage — site counts @@ -72,11 +72,11 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `i18n.zod.ts` | 1 | | `page.zod.ts` | 7 | | `report.zod.ts` | 3 | -| `responsive.zod.ts` | 4 | +| `responsive.zod.ts` | 1 | | `sharing.zod.ts` | 1 | | `view.zod.ts` | 57 | | `widget.zod.ts` | 1 | -| **total** | **171** | +| **total** | **168** | ### `data/` — sites @@ -155,7 +155,7 @@ over it is here. ### `ui/` — open -**7 strip of 171**, in 4 file(s). +**7 strip of 168**, in 4 file(s). | File | Strip | Sites | |---|---|---| @@ -163,7 +163,7 @@ over it is here. | `app.zod.ts` | 1 | 18 | | `view.zod.ts` | 4 | 57 | | `widget.zod.ts` | 1 | 1 | -| **total** | **7** | **171** | +| **total** | **7** | **168** | | Bucket | Sites | |---|---| diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 819b3bb060..702e15ee78 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -194,7 +194,7 @@ Finally it removes the script-body capability token 'crypto.hash' (#4391). Four It also removes `connector.rateLimitConfig` and its whole shape (#4911). This one is not "declared but unread" — it is declared but UNIMPLEMENTED, one step worse. The only token bucket the platform owns (runtime `security/rate-limit.ts`) is INBOUND: the dispatcher calls `consume(key)` on a request fingerprint and answers 429. Nothing anywhere throttles the calls a connector makes OUT, and no provider — `connector-rest`, `connector-openapi`, `connector-mcp`, `connector-slack` — reads the key or has a seam that could. So `strategy`, `maxRequests`, `windowSeconds`, `burstCapacity`, `respectUpstreamLimits` and `rateLimitHeaders` parsed cleanly and capped nothing, on a surface where the author believed they had bounded their spend against a third party's quota. `ConnectorRateLimitConfig` and the `RateLimitStrategy` enum it embedded had no other consumer and are removed with the key, so importing either is TS2305 in v17 — the #4834 shape, and the same implementation-first ruling: the vocabulary comes back WITH the engine, in one change. It is deliberately NOT converted to `shared` `RateLimitConfig`, which limits the calls others make to US; #4684 split their names for precisely this confusion, and rewriting an outbound cap into an inbound one would throttle the wrong direction. Delete the key and rate-limit where the calls are actually made — the connector provider or upstream gateway. -Last, it removes `dashboard.widgets[].responsive` (#4876) — the straggler of the #3896 sweep above, which retired the literally same-named `view.responsive` on the same evidence four days earlier. Re-measured before removal: no objectui code reads `widget.responsive` (DashboardRenderer, DashboardEditor and plugin-designer name it only in comments), and there are zero authored instances repo-wide, so the conversion is expected to be a no-op on every real source — it exists so that a stored dashboard carrying the key is cleaned deterministically rather than meeting the tombstone at load. What kept it alive was not evidence but a hole in the instrument: the liveness ledger declares no `children` on `dashboard.widgets`, and the walk only drills one level through an explicit `children`, so no widget-level key has ever been classified at all (filed as #4956, fixed separately). The removal is deliberately narrow — it takes the widget EMBED, not the shape. `ResponsiveConfig` stays exported and stays live on `page.components[].responsive`, which objectui `useResponsiveConfig` genuinely reads, so no import breaks and authors who need breakpoint behaviour today have somewhere real to put it. Per-widget responsive layout returns if and when a renderer implements it. +Last, it removes `dashboard.widgets[].responsive` (#4876) — the straggler of the #3896 sweep above, which retired the literally same-named `view.responsive` on the same evidence four days earlier. Re-measured before removal: no objectui code reads `widget.responsive` (DashboardRenderer, DashboardEditor and plugin-designer name it only in comments), and there are zero authored instances repo-wide, so the conversion is expected to be a no-op on every real source — it exists so that a stored dashboard carrying the key is cleaned deterministically rather than meeting the tombstone at load. What kept it alive was not evidence but a hole in the instrument: the liveness ledger declares no `children` on `dashboard.widgets`, and the walk only drills one level through an explicit `children`, so no widget-level key has ever been classified at all (filed as #4956, fixed separately). The removal was deliberately narrow — it took the widget EMBED, not the shape, which at the time was believed live on `page.components[].responsive` via objectui `useResponsiveConfig`. CORRECTED at #11027 (2026-08): that belief measured false — the hook had zero callers, nothing read the page key either — so protocol 18 retires `page.components[].responsive` and the `ResponsiveConfig` shape with it (see the `page-component-responsive-removed` step-18 entry). Authors who need breakpoint behaviour use `responsiveStyles` (ADR-0065), the channel objectui really compiles. Per-widget responsive layout returns if and when a renderer implements it. Finally it CONVERGES `dashboard.widgets[].compareTo` (#5011) — the one entry in this step that is not a removal but a vocabulary merge, and the one whose defect was worst-shaped. The widget declared three arms with confident TSDoc; the analytics executor implements one contract, `DatasetSelection.compareTo` = `{ kind, dimension? }`, which has no `offset` in it. On the ADR-0021 dataset path the two string arms were DROPPED by the renderer (a comparison silently absent from a widget whose author asked for one) and `{ offset }` was forwarded into that contract with no dimension, so the executor threw `compareTo requires a timeDimension "undefined"` and errored the whole widget. All three arms worked on the legacy inline chart path. Same key, two fates — and the failing one was the path the spec itself calls canonical, which is why this ranks above an ordinary declared-but-unread key: the documentation was actively teaching a shape that crashes. The widget now declares the executor's own words, so `declared = enforced` holds by construction with no second vocabulary left to drift. `dimension` is optional and resolved by the EXECUTOR (one dated time dimension → that one; zero or several → a loud error naming the candidates), which is a producer-side resolution rule, not the consumer-side tolerance PD #12 forbids. The bare strings and `{ offset: '1y' }` replay mechanically; every other `{ offset }` duration is a semantic TODO below, because `previousPeriod` shifts by the resolved window's own length and rewriting `7d` into it would change which rows the comparison counts. The converged slot is also union-free, which is not cosmetic: zod collapses a failed union into one bare `Invalid input` and #5014 showed that curated guidance inside a union arm never reaches the author at all. @@ -268,7 +268,7 @@ Finally it removes the 'pdf' member of `view.exportOptions` formats (#8010, main | `view-list-passthrough-keys-removed` | `view.list.striped / view.list.bordered / view.list.virtualScroll` | view list keys removed (#7176): 'striped'/'bordered'/'virtualScroll' — every measured reader copied the key forward and none applied it (pass-through-only; ADR-0049 enforce-or-remove) | retired — `migrate meta` only | | `view-export-options-pdf-removed` | `view.list.exportOptions / view.listViews.*.exportOptions` | list-view export format 'pdf' removed (#8010 — PDF export was declined as #1301 NOT_PLANNED; ObjectGrid dropped the declared format from the menu with only a runtime console.warn) | retired — `migrate meta` only | | `dashboard-inert-keys-removed` | `dashboard.aria / dashboard.performance / dashboard.widgets[].performance` | dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them) | retired — `migrate meta` only | -| `dashboard-widget-responsive-removed` | `dashboard.widgets[].responsive` | dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected) | retired — `migrate meta` only | +| `dashboard-widget-responsive-removed` | `dashboard.widgets[].responsive` | dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was itself retired at protocol 18, #11027) | retired — `migrate meta` only | | `dashboard-widget-action-aria-removed` | `dashboard.widgets[].actionUrl / dashboard.widgets[].actionType / dashboard.widgets[].actionIcon / dashboard.widgets[].aria` | dashboard widget keys 'actionUrl'/'actionType'/'actionIcon' and 'aria' removed (#5010 — no renderer ever drew a per-widget action button, and widget ARIA attributes never reached the DOM; use header.actions[] and the widget title/description) | retired — `migrate meta` only | | `dashboard-widget-compareto-converged` | `dashboard.widgets[].compareTo` | dashboard widget 'compareTo' converged on the executor's { kind, dimension? } contract (#5011 — the bare strings and { offset: '1y' } rewrite mechanically; other { offset } durations have no faithful target and are reported, not guessed) | retired — `migrate meta` only | | `agent-knowledge-removed` | `agent.knowledge` | agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level) | retired — `migrate meta` only | diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index 73a48b4051..ff737a7990 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -47,11 +47,6 @@ "AssembledViewArtifactParsed (type)", "AssembledViewArtifactSchema (const)", "AssembledViewPartition (interface)", - "BreakpointColumnMap (type)", - "BreakpointColumnMapSchema (const)", - "BreakpointName (type)", - "BreakpointOrderMap (type)", - "BreakpointOrderMapSchema (const)", "BulkActionDef (type)", "BulkActionDefParsed (type)", "BulkActionDefSchema (const)", @@ -316,8 +311,6 @@ "ReportSortSchema (const)", "ReportType (type)", "ResolvedActionParam (interface)", - "ResponsiveConfig (type)", - "ResponsiveConfigSchema (const)", "ResponsiveStyles (type)", "ResponsiveStylesSchema (const)", "RowColorConfig (type)", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index b804473bed..8335d00c04 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -139,18 +139,6 @@ "ui/AriaProps:ariaDescribedBy", "ui/AriaProps:ariaLabel", "ui/AriaProps:role", - "ui/BreakpointColumnMap:2xl", - "ui/BreakpointColumnMap:lg", - "ui/BreakpointColumnMap:md", - "ui/BreakpointColumnMap:sm", - "ui/BreakpointColumnMap:xl", - "ui/BreakpointColumnMap:xs", - "ui/BreakpointOrderMap:2xl", - "ui/BreakpointOrderMap:lg", - "ui/BreakpointOrderMap:md", - "ui/BreakpointOrderMap:sm", - "ui/BreakpointOrderMap:xl", - "ui/BreakpointOrderMap:xs", "ui/BulkActionDef:batchSize", "ui/BulkActionDef:confirmLabel", "ui/BulkActionDef:confirmText", @@ -915,7 +903,7 @@ "ui/PageComponent:id", "ui/PageComponent:label", "ui/PageComponent:properties", - "ui/PageComponent:responsive", + "ui/PageComponent:responsive [RETIRED]", "ui/PageComponent:responsiveStyles", "ui/PageComponent:style", "ui/PageComponent:type", @@ -1082,10 +1070,6 @@ "ui/ReportNavItem:visible", "ui/ReportSort:by", "ui/ReportSort:direction", - "ui/ResponsiveConfig:breakpoint", - "ui/ResponsiveConfig:columns", - "ui/ResponsiveConfig:hiddenOn", - "ui/ResponsiveConfig:order", "ui/ResponsiveStyles:large", "ui/ResponsiveStyles:medium", "ui/ResponsiveStyles:small", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index df6d7b3150..70dd21043a 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -47,11 +47,6 @@ "AssembledViewArtifactParsed": "src/ui/assembled-views.zod.ts#AssembledViewArtifactParsed (type)", "AssembledViewArtifactSchema": "src/ui/assembled-views.zod.ts#AssembledViewArtifactSchema (const)", "AssembledViewPartition": "src/ui/assembled-views.zod.ts#AssembledViewPartition (interface)", - "BreakpointColumnMap": "src/ui/responsive.zod.ts#BreakpointColumnMap (type)", - "BreakpointColumnMapSchema": "src/ui/responsive.zod.ts#BreakpointColumnMapSchema (const)", - "BreakpointName": "src/ui/responsive.zod.ts#BreakpointName (type)", - "BreakpointOrderMap": "src/ui/responsive.zod.ts#BreakpointOrderMap (type)", - "BreakpointOrderMapSchema": "src/ui/responsive.zod.ts#BreakpointOrderMapSchema (const)", "BulkActionDef": "src/ui/bulk-action.zod.ts#BulkActionDef (type)", "BulkActionDefParsed": "src/ui/bulk-action.zod.ts#BulkActionDefParsed (type)", "BulkActionDefSchema": "src/ui/bulk-action.zod.ts#BulkActionDefSchema (const)", @@ -316,8 +311,6 @@ "ReportSortSchema": "src/ui/report.zod.ts#ReportSortSchema (const)", "ReportType": "src/ui/report.zod.ts#ReportType (type)", "ResolvedActionParam": "src/ui/action-params.zod.ts#ResolvedActionParam (interface)", - "ResponsiveConfig": "src/ui/responsive.zod.ts#ResponsiveConfig (type)", - "ResponsiveConfigSchema": "src/ui/responsive.zod.ts#ResponsiveConfigSchema (const)", "ResponsiveStyles": "src/ui/responsive.zod.ts#ResponsiveStyles (type)", "ResponsiveStylesSchema": "src/ui/responsive.zod.ts#ResponsiveStylesSchema (const)", "RowColorConfig": "src/ui/view.zod.ts#RowColorConfig (type)", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index 6174a2db03..2359646dad 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -16,9 +16,6 @@ "ui/AppContextSelector", "ui/AppearanceConfig", "ui/AriaProps", - "ui/BreakpointColumnMap", - "ui/BreakpointName", - "ui/BreakpointOrderMap", "ui/BulkActionDef", "ui/BulkActionExecution", "ui/BulkActionOperation", @@ -132,7 +129,6 @@ "ui/ReportNavItem", "ui/ReportSort", "ui/ReportType", - "ui/ResponsiveConfig", "ui/ResponsiveStyles", "ui/RowColorConfig", "ui/RowHeight", diff --git a/packages/spec/liveness/dashboard.json b/packages/spec/liveness/dashboard.json index ebbdb73589..5d02d881a5 100644 --- a/packages/spec/liveness/dashboard.json +++ b/packages/spec/liveness/dashboard.json @@ -142,8 +142,8 @@ }, "responsive": { "status": "dead", - "verifiedAt": "2026-08-03", - "note": "The key this drill exists for. Nothing in either repo ever read `widget.responsive` — DashboardRenderer / DashboardEditor / plugin-designer name it only in comments, and objectui's one real per-breakpoint consumer (useResponsiveConfig) is fed by `page.components[].responsive`. Its literal namesake `view.list.responsive` was judged dead and removed in the 2026-07-30 #3896 sweep precisely because `view` IS drilled through `children`; this one was never asked, and survived on that silence alone. Retired 2026-08-03 via #4876 / PR #4995 (retiredKey tombstone + the protocol-17 `dashboard-widget-responsive-removed` conversion). The row stays because the tombstone keeps the key in the walked shape — the rls.priority precedent — and no authorWarn is needed: authoring it is now a tsc error and a parse error." + "verifiedAt": "2026-08-24", + "note": "The key this drill exists for. Nothing in either repo ever read `widget.responsive` — DashboardRenderer / DashboardEditor / plugin-designer name it only in comments. Its literal namesake `view.list.responsive` was judged dead and removed in the 2026-07-30 #3896 sweep precisely because `view` IS drilled through `children`; this one was never asked, and survived on that silence alone. Retired 2026-08-03 via #4876 / PR #4995 (retiredKey tombstone + the protocol-17 `dashboard-widget-responsive-removed` conversion). CORRECTED 2026-08-24 (#11027): this note used to call `useResponsiveConfig` the 'one real per-breakpoint consumer', fed by `page.components[].responsive` — measured false: that hook (and `ResponsiveProtocol`, the second implementation) had zero callers, so the page key was retired too (protocol-18 `page-component-responsive-removed`) and the tombstone here no longer prescribes it. The row stays because the tombstone keeps the key in the walked shape — the rls.priority precedent — and no authorWarn is needed: authoring it is now a tsc error and a parse error." }, "aria": { "status": "dead", diff --git a/packages/spec/liveness/page.json b/packages/spec/liveness/page.json index e3780e0992..ac07f5d10e 100644 --- a/packages/spec/liveness/page.json +++ b/packages/spec/liveness/page.json @@ -1,6 +1,6 @@ { "type": "page", - "_note": "PageSchema (UI). Renderers live in objectui; objectui paths are cited with the `objectui:` realm marker (in `evidence` or `note` prose) and counted, not resolved against this repo (#5623). Seeded from the Studio page-design dogfood (framework#2254/#2261/#2265). Containers (variables/regions/interfaceConfig/slots/aria) classified at top level — one drill level, no divergent sub-statuses. The removed roadmap page types (record_review/blank) and their config fields (recordReview/blankLayout) were hard-removed — no longer authorable.", + "_note": "PageSchema (UI). Renderers live in objectui; objectui paths are cited with the `objectui:` realm marker (in `evidence` or `note` prose) and counted, not resolved against this repo (#5623). Seeded from the Studio page-design dogfood (framework#2254/#2261/#2265). Containers (variables/regions/interfaceConfig/slots/aria) classified at top level — one drill level, no divergent sub-statuses. The removed roadmap page types (record_review/blank) and their config fields (recordReview/blankLayout) were hard-removed — no longer authorable. RETIREMENT RECORD (#11027, 2026-08-24): `regions[].components[].responsive` is a retiredKey tombstone (ADR-0049 D2) — measured dead across both repos (zero callers of either objectui consumer implementation); the shared `ResponsiveConfigSchema` left with it. The walk drills one level only, so no component-level key has a row here (`page/regions` stays RECORDED in undrilled-containers.baseline.json — #4956's page-side instance, still open for the other component keys); the checkable record of THIS retirement is the authorable-surface `ui/PageComponent:responsive [RETIRED]` row, RETIRED_KEYS_BY_MAJOR[18], and the `page-component-responsive-removed` conversion.", "props": { "source": { "status": "live", @@ -50,7 +50,7 @@ "verifiedAt": "2026-08-10", "evidenceScope": "cross-repo", "evidence": "objectui: packages/components/src/renderers/layout/page.tsx:290 branches on schema.regions, :197-204 resolves the named slots (header/sidebar/main/aside/footer) and appends the remainder below main at :211, and :157 flattens their components for the JSX-source path — measured objectui @11c1e71e", - "note": "region → component tree rendering (header/main/sidebar/footer). RE-CITED 2026-08-10 (#7133/#7142): same split-citation pruning as `template` — at objectui @11c1e71e the containers.tsx half names `regions` only in a comment (:798, 'page:section — thin wrapper used inside regions'). Verdict unchanged: LIVE." + "note": "region → component tree rendering (header/main/sidebar/footer). RE-CITED 2026-08-10 (#7133/#7142): same split-citation pruning as `template` — at objectui @11c1e71e the containers.tsx half names `regions` only in a comment (:798, 'page:section — thin wrapper used inside regions'). Verdict unchanged: LIVE. Component-level liveness stays unclassified (undrilled container, #4956); one component key carries a verdict OUTSIDE this file: `components[].responsive` is a retiredKey tombstone (#11027, ADR-0049 D2 — dead on a two-repo measurement; see the file _note)." }, "isDefault": { "status": "live", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 1ed39ba252..2940b480fc 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -202,7 +202,7 @@ }, { "surface": "dashboard.widgets[].responsive", - "to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected)", + "to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was itself retired at protocol 18, #11027)", "conversionId": "dashboard-widget-responsive-removed", "toMajor": 17 }, @@ -1350,7 +1350,7 @@ }, { "surface": "dashboard.widgets[].responsive", - "to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected)", + "to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was itself retired at protocol 18, #11027)", "conversionId": "dashboard-widget-responsive-removed", "toMajor": 17 }, diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index d6208afc08..21edf8944b 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -2508,15 +2508,17 @@ const dashboardInertKeysRemoved: MetadataConversion = { * author actually reads. Both are `toMajor: 17`, so a stored dashboard carrying * both keys is cleaned by both in one replay. * - * Strips ONLY the widget embed. The shared `ResponsiveConfig` shape is - * untouched and still live on `page.components[].responsive`. + * Strips ONLY the widget embed. At the time the shared `ResponsiveConfig` + * shape was believed to stay live on `page.components[].responsive`; #11027 + * measured that carrier equally unread and retired it and the shape at + * protocol 18 (`page-component-responsive-removed` below). */ const dashboardWidgetResponsiveRemoved: MetadataConversion = { id: 'dashboard-widget-responsive-removed', toMajor: 17, retiredFromLoadPath: true, surface: 'dashboard.widgets[].responsive', - summary: "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected)", + summary: "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; the page.components[].responsive key this entry once deferred to was itself retired at protocol 18, #11027)", apply(stack, emit) { return mapCollection(stack, 'dashboards', (d, path) => { const widgets = d.widgets; @@ -7803,6 +7805,102 @@ const mappingLookupParamsRemoved: MetadataConversion = { }, }; +/** + * page.components[].responsive (#11027, ADR-0049 D2) — the LAST carrier of the + * `ResponsiveConfig` layout block, and the destination the + * `dashboard-widget-responsive-removed` tombstone (#4876) prescribed as the + * live alternative. Measured across objectstack + objectui (tsc-probe + * methodology, positive and negative controls): objectui's two implementations + * of the contract (`useResponsiveConfig`, `ResponsiveProtocol`) had zero + * callers, nothing read `.responsive` off a page component, and objectui's own + * node interface never declared the key — so the prescribed migration moved an + * inert key to an inert key. Zero authored instances exist in either repo, so + * this is expected to be a no-op on every real source; it exists so a stored + * page carrying the key is cleaned deterministically rather than meeting the + * tombstone at load. The shape (`ResponsiveConfigSchema` + its breakpoint + * maps + `BreakpointName`) leaves with the key — `RETIRED_DEFS_BY_MAJOR[18]`. + * + * Like its widget sibling it survived the earlier sweeps through an instrument + * gap, not on evidence: `page/regions` is an undrilled container + * (`undrilled-containers.baseline.json`), so no component-level key has ever + * been classified (#4956's page-side instance). + * + * The reach is every position a component can be authored in + * ({@link mapPageComponents}): `regions[].components[]`, `slots.`, and + * the containers a component nests under its `properties`. + */ +const pageComponentResponsiveRemoved: MetadataConversion = { + id: 'page-component-responsive-removed', + toMajor: 18, + retiredFromLoadPath: true, + surface: 'page.components[].responsive', + summary: + "page component key 'responsive' removed (#11027 — no renderer ever applied per-component " + + 'breakpoint layout overrides, and the shared ResponsiveConfig shape leaves with its last ' + + 'carrier; use responsiveStyles (ADR-0065) for breakpoint behaviour that IS applied)', + apply(stack, emit) { + return mapPageComponents(stack, (component, path) => + stripKeys(component, ['responsive'], emit, path)); + }, + fixture: { + before: { + pages: [{ + name: 'ops_home', + regions: [{ + name: 'main', + components: [ + { + type: 'element:text', + id: 't1', + responsive: { columns: { xs: 12, lg: 4 }, order: { xs: 2, lg: 1 }, hiddenOn: ['xs'] }, + // Control: the LIVE per-breakpoint channel is untouched. + responsiveStyles: { small: { padding: 'var(--space-4)' } }, + }, + // The nested position (#6775's lesson): a component inside a + // card's container is still a component. + { + type: 'page:card', + id: 'c1', + properties: { + children: [ + { type: 'element:divider', id: 'd1', responsive: { hiddenOn: ['xs'] } }, + ], + }, + }, + ], + }], + }], + }, + after: { + pages: [{ + name: 'ops_home', + regions: [{ + name: 'main', + components: [ + { + type: 'element:text', + id: 't1', + responsiveStyles: { small: { padding: 'var(--space-4)' } }, + }, + { + type: 'page:card', + id: 'c1', + properties: { + children: [ + { type: 'element:divider', id: 'd1' }, + ], + }, + }, + ], + }], + }], + }, + // Two notices: one per stripped key site (the region-level component and + // the nested one). + expectedNotices: 2, + }, +}; + export const CONVERSIONS_BY_MAJOR: Readonly> = { 11: [flowNodeHttpRename, pageKindJsxToHtml, flowNodeFilterAlias, objectCompactLayoutRename], 13: [stackRolesToPositions, owdLegacyReadAliases, sharingRecipientRoleToPosition], @@ -7886,6 +7984,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly> // `element-input-target-variable-removed` (a page component IS a stack // collection member, unlike the `kernel/Manifest:loading` family). 'ui/ElementTextInputProps:targetVariable', + // #11027 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-22, ruled B: + // retire + repair the redirect texts in the same change). The LAST carrier of + // the `ResponsiveConfig` layout block, and the destination the + // `dashboard.widgets[].responsive` tombstone (#4876) prescribed as the live + // alternative. Measured across objectstack + objectui with the tsc-probe + // methodology (positive and negative controls): objectui's two implementations + // of the contract (`useResponsiveConfig` in `@object-ui/mobile`, + // `ResponsiveProtocol` in `@object-ui/core`) had ZERO callers, nothing read + // `.responsive` off a page component, and objectui's own node interface never + // declared the key — so an author following the shipped prescription moved an + // inert key to an inert key and was told it now works. Same retirement the + // family already took on identical evidence: `view.responsive` (#3896), + // `dashboard.widgets[].responsive` (#4876). It survived the sweeps through an + // instrument gap, not on evidence: `page/regions` is an undrilled container, + // so no component-level key has ever been classified (#4956's page-side + // instance). The shape leaves with its last carrier — see + // `RETIRED_DEFS_BY_MAJOR[18]` (`ui/ResponsiveConfig` and its breakpoint maps). + // The live per-breakpoint channel on the same component is `responsiveStyles` + // (ADR-0065). + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstone ships on the 17.x line (launch-window convention: accept-set + // narrowings ride minor releases) and the prescription lives at the major + // boundary where `migrate meta` users look (the #8495 / PR #8666 precedent). + // Sources are rewritten by the D2 conversion `page-component-responsive-removed`. + 'ui/PageComponent:responsive', // #10054 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-21, executing // the 2026-08-20 census verdict). `icon` on the object arm of // `RecordHighlightsField` was declared, described (`Icon name (lucide icon @@ -7231,11 +7275,44 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly> // exported value schema with no consumer reads as a capability). See // `18.ui__Theme.ts` for the retirement record and the ruling. 'ui/BorderRadius', + // #11027 — `ui/BreakpointColumnMap` (breakpoint → grid column count, 1-12) + // left with `ui/ResponsiveConfig`: its ONLY consumer was the retired + // `ResponsiveConfigSchema.columns` (the #3950 rule — an exported value schema + // with no consumer reads as a capability). See `18.ui__ResponsiveConfig.ts` + // for the retirement record and the measurement. + 'ui/BreakpointColumnMap', + // #11027 — `ui/BreakpointName` (the Tailwind-style `xs…2xl` breakpoint-name + // enum) left with `ui/ResponsiveConfig`: its only consumers were the retired + // `ResponsiveConfigSchema` (`breakpoint`, `hiddenOn`) and the two retired + // breakpoint maps' key sets (the #3950 rule — an exported value schema with + // no consumer reads as a capability). The surviving `ResponsiveStyles` + // vocabulary is the ADR-0065 max-width buckets (`large`/`medium`/`small`/ + // `xsmall`), a different axis by design. See `18.ui__ResponsiveConfig.ts` for + // the retirement record and the measurement. + 'ui/BreakpointName', + // #11027 — `ui/BreakpointOrderMap` (breakpoint → display order) left with + // `ui/ResponsiveConfig`: its ONLY consumer was the retired + // `ResponsiveConfigSchema.order` (the #3950 rule — an exported value schema + // with no consumer reads as a capability). See `18.ui__ResponsiveConfig.ts` + // for the retirement record and the measurement. + 'ui/BreakpointOrderMap', // #10485 — `ui/ColorPalette` (the colour palette sub-block) left with `ui/Theme`: // its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an // exported value schema with no consumer reads as a capability). See // `18.ui__Theme.ts` for the retirement record and the ruling. 'ui/ColorPalette', + // #11027 — `ui/ResponsiveConfig` (the per-breakpoint LAYOUT block: grid + // columns / visibility / display order on the Tailwind `xs…2xl` axis). Its + // last authorable carrier, `page.components[].responsive`, is tombstoned in + // this same major (ADR-0049 D2; the widget embed went in #4876, the view + // embed in #3896), and an exported value schema with no consumer reads as a + // capability (#3950 rule — the `PerformanceConfigSchema` precedent from the + // very same file). Measured before removal: zero callers of either objectui + // consumer implementation, zero authored instances in either repo. The live + // per-breakpoint channel is `responsiveStyles` (ADR-0065, `ResponsiveStyles`), + // which stays. The Tailwind-style layout vocabulary returns if and when a + // renderer implements it — in one change, with the engine (the #4834 rule). + 'ui/ResponsiveConfig', // #10485 — `ui/Shadow` (the shadow scale sub-block) left with `ui/Theme`: // its ONLY consumer was the retired `ThemeSchema` (the #3950 rule — an // exported value schema with no consumer reads as a capability). See diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 06a3c017cd..a4564cc7f4 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -267,7 +267,7 @@ import type * as M170 from './ui/component.zod.js'; import type * as M183 from './api/sortability.zod.js'; // --------------------------------------------------------------------------- -// 837 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 833 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -1337,8 +1337,9 @@ export type Iso694 = Assert, export type Iso695 = Assert, z.infer< typeof M164.JoinedReportBlockSchema > >>; // ui/responsive.zod.ts -export type Iso696 = Assert, z.infer< typeof M165.BreakpointName > >>; -export type Iso697 = Assert, z.infer< typeof M165.ResponsiveConfigSchema > >>; +// (Iso696 `BreakpointName` / Iso697 `ResponsiveConfigSchema` removed with +// their schemas — #11027's ADR-0049 retirement of the responsive layout +// vocabulary. The Iso numbers are positional and stay vacant.) export type Iso698 = Assert, z.infer< typeof M165.StyleMapSchema > >>; export type Iso699 = Assert, z.infer< typeof M165.ResponsiveStylesSchema > >>; @@ -1581,8 +1582,8 @@ export type Iso822 = Assert, z.infe export type Iso823 = Assert, z.infer< typeof M164.ReportType > >>; // ui/responsive.zod.ts -export type Iso824 = Assert, z.infer< typeof M165.BreakpointColumnMapSchema > >>; -export type Iso825 = Assert, z.infer< typeof M165.BreakpointOrderMapSchema > >>; +// (Iso824 `BreakpointColumnMapSchema` / Iso825 `BreakpointOrderMapSchema` +// removed with their schemas — #11027, see the Iso696/Iso697 note above.) // ui/view.zod.ts export type Iso826 = Assert, z.infer< typeof M167.CalendarConfigSchema > >>; @@ -1667,7 +1668,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 837 isomorphic pins', () => { + it('still declares all 833 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -1950,9 +1951,15 @@ describe('ADR-0122 type-alias convention', () => { // coincide and ADR-0122 gives it a pin rather than an `XParsed`. Its id is // `Iso856`, the next free one — the ids are claims about pins, not // positions. + // + // 837 -> 833 is #11027's ADR-0049 retirement of the responsive layout + // vocabulary (`BreakpointName`, `ResponsiveConfigSchema`, + // `BreakpointColumnMapSchema`, `BreakpointOrderMapSchema` — Iso696/697/ + // 824/825): the four schemas no longer exist, so there is nothing left to + // exempt. -4 retired, +0 of my own; the Iso numbers stay vacant. const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert { expect(message).toMatch(/removed in @objectstack\/spec 17\.0\.0/); expect(message).toMatch(/#4876/); expect(message).toMatch(/Delete the key/); - // It must point at the surviving home for the capability, or an author who - // really wants breakpoints reads this as "responsive layout is gone". - expect(message).toMatch(/page\.components\[\]\.responsive/); + // The redirect correction (#11027): this message used to prescribe + // `page.components[].responsive` as the live home of the shared shape; + // that key measured equally unread and is retired too, so the message + // must (a) say so rather than silently dropping the old promise — an + // author who followed it needs the correction — and (b) point at the + // channel that IS applied. + expect(message).toMatch(/page\.components\[\]\.responsive[\s\S]*#11027/); + expect(message).toMatch(/responsiveStyles/); // `.strict()` on this schema would answer a DELETED key with a generic // unrecognized-key error. The tombstone is what makes it a prescription — // if this ever regresses to the strict path, this assertion is the tripwire. @@ -561,26 +566,22 @@ describe('[#4876] DashboardWidgetSchema — retired `responsive`', () => { expect(w.dataset).toBe('orders'); }); - // ── CONTROL: the shared shape is NOT retired, only this embed ────────────── - it('CONTROL: `ResponsiveConfigSchema` is still exported and still parses', async () => { - const ui = await import('./index'); - expect(ui.ResponsiveConfigSchema).toBeTruthy(); - const cfg = ui.ResponsiveConfigSchema.parse({ - columns: { xs: 12, lg: 4 }, order: { xs: 2, lg: 1 }, hiddenOn: ['xs'], - }); - expect(cfg.columns).toEqual({ xs: 12, lg: 4 }); - expect(cfg.hiddenOn).toEqual(['xs']); - }); - - it('CONTROL: `page.components[].responsive` parses exactly as before', async () => { + // ── The former CONTROLs, inverted by #11027 ──────────────────────────────── + // Until #11027 two controls here asserted that `ResponsiveConfigSchema` + // stayed exported and that `page.components[].responsive` round-tripped — + // the "only this embed goes" contract of #4876. The #11027 measurement + // falsified the premise those controls encoded (nothing ever read the page + // key either), so the shape left with its last carrier and the page key is + // a tombstone of its own, pinned in `page.test.ts` + // ("[#11027] PageComponentSchema — retired `responsive`"). The surviving + // control is the channel the corrected prescriptions point at: + it('CONTROL: `responsiveStyles` on a page component still parses (ADR-0065)', async () => { const { PageComponentSchema } = await import('./page.zod'); const c = PageComponentSchema.parse({ type: 'page:sidebar', properties: {}, - responsive: { columns: { xs: 12, lg: 4 }, order: { xs: 2, lg: 1 }, hiddenOn: ['xs'] }, + responsiveStyles: { small: { display: 'none' } }, }); - // Round-trips untouched — the page embed is a live consumer path - // (objectui `useResponsiveConfig`), not a second casualty of this removal. - expect(c.responsive).toEqual({ columns: { xs: 12, lg: 4 }, order: { xs: 2, lg: 1 }, hiddenOn: ['xs'] }); + expect(c.responsiveStyles).toEqual({ small: { display: 'none' } }); }); }); diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 61200e21b9..36edf17211 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -20,9 +20,10 @@ import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; // tombstone below. import { I18nLabelSchema } from './i18n.zod'; // `ResponsiveConfigSchema` is no longer imported here: `widgets[].responsive` -// was retired (#4876). The shape itself is NOT removed — it stays live on -// `page.components[].responsive` (`page.zod.ts`), whose renderer really does -// read it (objectui `useResponsiveConfig`). See the tombstone below. +// was retired (#4876). At the time the shape survived on +// `page.components[].responsive`; #11027 then measured THAT carrier dead too +// (zero callers of either objectui implementation) and retired the key and the +// shape whole. See the tombstone below. /** * Color variant for dashboard widgets (e.g., KPI cards). @@ -585,21 +586,28 @@ export const DashboardWidgetSchema = lazySchema(() => strictObject({ // `responsive` REMOVED (#4876): authorable and inert, exactly like the // same-named `view.responsive` retired four days earlier (#3896 close-out). // No objectui code read `widget.responsive` — DashboardRenderer / - // DashboardEditor / plugin-designer mention it only in comments, and the one - // real per-breakpoint consumer (`useResponsiveConfig`) is fed by - // `page.components[].responsive`, not by a widget. It escaped the #3896 sweep - // through a liveness-ledger drill gap, not on evidence: `dashboard.json` - // declares no `children` on `widgets`, so no widget-level key has ever been - // classified (#4956). The shared `ResponsiveConfigSchema` survives untouched - // via `page.zod.ts` — only this embed goes. + // DashboardEditor / plugin-designer mention it only in comments. It escaped + // the #3896 sweep through a liveness-ledger drill gap, not on evidence: + // `dashboard.json` declared no `children` on `widgets`, so no widget-level + // key had ever been classified (#4956). + // + // CORRECTED (#11027): this tombstone used to redirect authors to + // `page.components[].responsive` as the live home of the shared + // `ResponsiveConfig` shape, on the belief that objectui `useResponsiveConfig` + // read it. Measured 2026-08 across both repos, that hook (and the second + // implementation, `ResponsiveProtocol`) had ZERO callers — the prescribed + // destination applied no breakpoint behaviour either. The page key and the + // shape are retired too (`page.zod.ts` tombstone, ADR-0049 D2); the live + // per-breakpoint channel is `responsiveStyles` (ADR-0065). responsive: retiredKey( '`dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — ' + 'no renderer ever read it, so per-widget breakpoint overrides were never applied: the value ' + 'parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules ' + '(`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. ' + - 'The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, ' + - 'which objectui `useResponsiveConfig` really does read; move the layout there if you need ' + - 'breakpoint behaviour today. ' + + 'This message used to point at `page.components[].responsive` as the live home of the shared ' + + '`ResponsiveConfig` shape; that key was measured equally unread and removed with the shape ' + + 'in #11027. For breakpoint behaviour that IS applied, use `responsiveStyles` on a page ' + + 'component (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render. ' + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', ), diff --git a/packages/spec/src/ui/interaction-config-retirement.test.ts b/packages/spec/src/ui/interaction-config-retirement.test.ts index 47ac6defe2..e68cce7792 100644 --- a/packages/spec/src/ui/interaction-config-retirement.test.ts +++ b/packages/spec/src/ui/interaction-config-retirement.test.ts @@ -104,8 +104,10 @@ describe('[#4988] ui/ interaction config family retirement', () => { * Names that must SURVIVE on `./ui`, one per neighbouring concern a * too-wide sweep would plausibly take: * - * - `ResponsiveConfigSchema` — batch 13's sixth file, measured REACHABLE and - * tightened instead of retired. The single most likely over-deletion. + * - (`ResponsiveConfigSchema` stood here — batch 13's sixth file, measured + * graph-REACHABLE and tightened — until #11027 measured its consumers dead + * and retired the shape with its last carrier, ADR-0049. A survivor list + * entry follows its subject out.) * - `AriaPropsSchema` / `I18nLabelSchema` — the two shapes the five retired * modules imported; deleting a consumer must not take its dependency. * - `NotificationTypeSchema` — `ui/notification.zod.ts` kept its presentation @@ -117,7 +119,6 @@ describe('[#4988] ui/ interaction config family retirement', () => { * vocabularies would have hung off had they ever had a carrier. */ const MUST_SURVIVE = [ - 'ResponsiveConfigSchema', 'AriaPropsSchema', 'I18nLabelSchema', 'NotificationTypeSchema', diff --git a/packages/spec/src/ui/page.test.ts b/packages/spec/src/ui/page.test.ts index da30315a49..911a8eece0 100644 --- a/packages/spec/src/ui/page.test.ts +++ b/packages/spec/src/ui/page.test.ts @@ -445,14 +445,53 @@ describe('Page ARIA Integration', () => { }); }); -describe('Page Responsive Integration', () => { - it('should accept component with responsive config', () => { - const result = PageComponentSchema.parse({ +// --------------------------------------------------------------------------- +// [#11027] `page.components[].responsive` is RETIRED (ADR-0049 D2) +// +// RUNTIME assertions, the dashboard.test.ts #4876 pattern: the tombstone's +// `tsc` channel is proved by the build of the packages that author pages, not +// by this file. This key was the destination the #4876 widget tombstone +// prescribed as live; measured 2026-08, neither objectui consumer +// implementation had a single caller, so the whole `ResponsiveConfig` layout +// vocabulary left with its last carrier. +// --------------------------------------------------------------------------- +describe('[#11027] PageComponentSchema — retired `responsive`', () => { + it('REJECTS an authored `responsive` with the prescription (not "unrecognized key")', () => { + let message = ''; + try { + PageComponentSchema.parse({ + type: 'page:sidebar', + properties: {}, + responsive: { hiddenOn: ['xs', 'sm'] }, + }); + } catch (e) { message = String((e as Error).message); } + + // The prescription in the four parts an upgrading author needs: the + // fully-qualified key, the version, the issue, and the fix. + expect(message).toMatch(/page\.components\[\]\.responsive/); + expect(message).toMatch(/removed in @objectstack\/spec 17/); + expect(message).toMatch(/#11027/); + expect(message).toMatch(/Delete the key/); + // It must point at the per-breakpoint channel that IS applied, or an + // author who really wants breakpoints reads this as "responsive is gone". + expect(message).toMatch(/responsiveStyles/); + // The house migrate sentence, spanning lines (`s` flag). + expect(message).toMatch(/os migrate meta --from 17/s); + // `.strict()` on this schema would answer a DELETED key with a generic + // unrecognized-key error. The tombstone is what makes it a prescription. + expect(message).not.toMatch(/Unrecognized key/); + }); + + it('still accepts a component with no `responsive` (the retirement strips nothing else)', () => { + const c = PageComponentSchema.parse({ type: 'page:sidebar', properties: {}, - responsive: { hiddenOn: ['xs', 'sm'] }, + responsiveStyles: { xsmall: { display: 'none' } }, }); - expect(result.responsive?.hiddenOn).toEqual(['xs', 'sm']); + expect(c).not.toHaveProperty('responsive'); + // CONTROL: the LIVE per-breakpoint channel on the same component (ADR-0065) + // is not a second casualty of this removal. + expect(c.responsiveStyles?.xsmall?.display).toBe('none'); }); }); diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index 0adac316af..8f4e9da3c6 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -8,7 +8,8 @@ import { VISIBILITY_ONLY_STRICT_OPTIONS } from '../shared/editability-boundary'; import { SortItemSchema } from '../shared/enums.zod'; import { FilterConditionSchema } from '../data/filter.zod'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; -import { ResponsiveConfigSchema, ResponsiveStylesSchema } from './responsive.zod'; +import { ResponsiveStylesSchema } from './responsive.zod'; +import { retiredKey } from '../shared/retired-key'; import { UserActionsConfigSchema, AppearanceConfigSchema, @@ -234,8 +235,32 @@ export const PageComponentSchema = lazySchema(() => strictObject({ /** Per-element data binding, overrides page-level object context */ dataSource: ElementDataSourceSchema.optional().describe('Per-element data binding for multi-object pages'), - /** Responsive layout overrides per breakpoint */ - responsive: ResponsiveConfigSchema.optional().describe('Responsive layout configuration'), + // `responsive` REMOVED (#11027, ADR-0049 D2): authorable and inert, exactly + // like `view.responsive` (#3896) and `dashboard.widgets[].responsive` + // (#4876) before it — and it was the key the #4876 tombstone redirected + // authors TO. Measured across objectstack + objectui (tsc-probe methodology + // with positive and negative controls): objectui's two implementations of + // the contract (`useResponsiveConfig`, `ResponsiveProtocol`) had zero + // callers, nothing read `.responsive` off a page component, and objectui's + // own node interface never declared the key — so the "live destination" the + // family's tombstones prescribed applied no breakpoint behaviour at all. + // It escaped classification through the same instrument gap as the widget + // key (#4956's page-side instance): `page/regions` is an undrilled + // container, so no component-level key has ever been asked. The live + // per-breakpoint channel on this same component is `responsiveStyles` + // (ADR-0065, sixteen lines up), which objectui really does compile. + // `ResponsiveConfigSchema` and its breakpoint maps had no other authorable + // carrier and are removed with the key (`responsive.zod.ts`, the + // PerformanceConfigSchema precedent). + responsive: retiredKey( + '`page.components[].responsive` was removed in @objectstack/spec 17 (#11027, ADR-0049 D2) — ' + + 'no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) ' + + 'parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS ' + + 'applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled ' + + "to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to " + + 'hide a component on the narrowest screens. ' + + 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.', + ), /** ARIA accessibility attributes */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), diff --git a/packages/spec/src/ui/responsive.test.ts b/packages/spec/src/ui/responsive.test.ts index 772163dbfd..4576e95a51 100644 --- a/packages/spec/src/ui/responsive.test.ts +++ b/packages/spec/src/ui/responsive.test.ts @@ -1,110 +1,26 @@ import { describe, it, expect } from 'vitest'; import { - ResponsiveConfigSchema, ResponsiveStylesSchema, StyleMapSchema, - BreakpointColumnMapSchema, - BreakpointOrderMapSchema, - BreakpointName, - type ResponsiveConfig, } from './responsive.zod'; import { PageComponentSchema } from './page.zod'; -describe('BreakpointName', () => { - it('should accept all valid breakpoint names', () => { - const names = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const; - - names.forEach(name => { - expect(() => BreakpointName.parse(name)).not.toThrow(); - }); - }); - - it('should reject invalid breakpoint names', () => { - expect(() => BreakpointName.parse('xxl')).toThrow(); - expect(() => BreakpointName.parse('mobile')).toThrow(); - expect(() => BreakpointName.parse('')).toThrow(); - }); -}); - -describe('ResponsiveConfigSchema', () => { - it('should accept empty config', () => { - expect(() => ResponsiveConfigSchema.parse({})).not.toThrow(); - }); - - it('should accept config with breakpoint visibility', () => { - const config: ResponsiveConfig = { - breakpoint: 'md', - }; - - const result = ResponsiveConfigSchema.parse(config); - expect(result.breakpoint).toBe('md'); - }); - - it('should accept config with hiddenOn breakpoints', () => { - const config: ResponsiveConfig = { - hiddenOn: ['xs', 'sm'], - }; - - const result = ResponsiveConfigSchema.parse(config); - expect(result.hiddenOn).toEqual(['xs', 'sm']); - }); - - it('should accept config with column mapping', () => { - const config: ResponsiveConfig = { - columns: { xs: 12, sm: 6, md: 4, lg: 3 }, - }; - - const result = ResponsiveConfigSchema.parse(config); - expect(result.columns?.xs).toBe(12); - expect(result.columns?.lg).toBe(3); - }); - - it('should reject columns outside 1-12 range', () => { - expect(() => ResponsiveConfigSchema.parse({ - columns: { xs: 0 }, - })).toThrow(); - - expect(() => ResponsiveConfigSchema.parse({ - columns: { xs: 13 }, - })).toThrow(); - }); - - it('should accept config with display order', () => { - const config: ResponsiveConfig = { - order: { xs: 2, lg: 1 }, - }; - - const result = ResponsiveConfigSchema.parse(config); - expect(result.order?.xs).toBe(2); - expect(result.order?.lg).toBe(1); - }); - - it('should accept full responsive config', () => { - const config: ResponsiveConfig = { - breakpoint: 'sm', - hiddenOn: ['xs'], - columns: { xs: 12, sm: 6, md: 4, lg: 3, xl: 2 }, - order: { xs: 3, lg: 1 }, - }; - - expect(() => ResponsiveConfigSchema.parse(config)).not.toThrow(); - }); -}); - -// PerformanceConfigSchema tests removed with the schema (#3896 close-out): -// every carrier of a `performance` block was authorable and inert. -// -// The `type PerformanceConfig` import that survived beside them until #4001 -// batch 13 did not: it named an export removed with the schema, so this file -// had been importing a type that does not exist. It compiled only because -// nothing here used it. +// BreakpointName / ResponsiveConfigSchema / BreakpointColumnMapSchema / +// BreakpointOrderMapSchema tests removed with the schemas (#11027, ADR-0049 +// D2): every authorable carrier of the `responsive` layout block was inert — +// `view.responsive` (#3896), `dashboard.widgets[].responsive` (#4876), and +// finally `page.components[].responsive`, whose two objectui consumer +// implementations had zero callers. The retirement pins live in +// `page.test.ts` ("[#11027] PageComponentSchema — retired `responsive`"); +// the #4001 批 13 curation measurements those suites kept executable are +// preserved on the surviving shape below where they still have a subject. // --------------------------------------------------------------------------- -// #4001 batch 13 (ADR-0078). This file carries TWO breakpoint vocabularies and -// they sit sixteen lines apart on the same page component — `responsiveStyles` -// (large/medium/small/xsmall, ADR-0065) and `responsive` (columns/order keyed -// xs…2xl). Crossing them is the mistake this file invites, and until now every -// crossing was silent. +// #4001 batch 13 (ADR-0078). This file used to carry TWO breakpoint +// vocabularies sixteen lines apart on the same page component; since #11027 +// only `responsiveStyles` (large/medium/small/xsmall, ADR-0065) survives, and +// the curation's job is to catch the vocabularies authors still carry in — +// the Tailwind ramp, and the knobs of the retired `responsive` block. // --------------------------------------------------------------------------- describe('unknown keys are rejected, not stripped (#4001 batch 13)', () => { const unknownKeyIssue = (schema: { safeParse: (v: unknown) => any }, value: unknown) => { @@ -120,9 +36,10 @@ describe('unknown keys are rejected, not stripped (#4001 batch 13)', () => { }); it('maps the Tailwind ramp onto the max-width bucket that contains it', () => { - // The sibling vocabulary: `BreakpointName`, declared in this same file and - // used by `responsive` on the same component. Edit distance cannot get - // from `lg` to `large`, so only a written-down alias answers it. + // The vocabulary the retired sibling `responsive` block was keyed by + // (#11027), and the one authors carry in from Tailwind itself. Edit + // distance cannot get from `lg` to `large`, so only a written-down + // alias answers it. const cases: Array<[string, string]> = [ ['xs', 'xsmall'], ['sm', 'small'], ['md', 'medium'], ['lg', 'large'], ['xl', 'large'], ['2xl', 'large'], @@ -135,11 +52,15 @@ describe('unknown keys are rejected, not stripped (#4001 batch 13)', () => { } }); - it('points a `responsive` key at the sibling key, not at a bucket', () => { + it('answers a retired `responsive` knob with its CSS translation', () => { + // `columns` / `hiddenOn` / `order` were the knobs of the retired layout + // block (#11027). An author who lands one here is migrating off it, so + // each entry names the removal and the CSS that IS applied. for (const key of ['columns', 'hiddenOn', 'order']) { const message = unknownKeyIssue(ResponsiveStylesSchema, { [key]: {} })!.message; - expect(message, `\`${key}\` should be sent one level out`).toContain('sibling key'); - expect(message).toContain('`responsive:'); + expect(message, `\`${key}\` should name the retirement`).toContain('retired `responsive` layout block'); + expect(message).toContain('#11027'); + expect(message).toContain('per-breakpoint CSS'); } }); @@ -155,94 +76,7 @@ describe('unknown keys are rejected, not stripped (#4001 batch 13)', () => { }); }); - describe('ResponsiveConfigSchema', () => { - it('rejects an undeclared key instead of dropping it', () => { - expect(unknownKeyIssue(ResponsiveConfigSchema, { colums: { lg: 4 } })!.message) - .toContain('`colums` → `columns`'); - }); - - it('answers a bare breakpoint name with the knob-first shape', () => { - // The legacy breakpoint-keyed form. Not hypothetical: it is the `before` - // fixture of the `view-inert-keys-removed` conversion, from the - // `view.responsive` retired in 17 (#3896). - const message = unknownKeyIssue(ResponsiveConfigSchema, { sm: {} })!.message; - expect(message).toContain('BREAKPOINT NAME'); - expect(message).toContain("columns: { sm: 6 }"); - }); - - it('names each offending breakpoint separately, not once for all of them', () => { - // `guidance` emits one bullet VERBATIM per key, so a shared prescription - // string prints the same paragraph N times (the batch 10 `join` / - // `joinGateway` lesson). Each name carries its own text instead. - const message = unknownKeyIssue(ResponsiveConfigSchema, { sm: {}, lg: {} })!.message; - expect(message).toContain("columns: { sm: 6 }"); - expect(message).toContain("columns: { lg: 6 }"); - }); - - it('sends a `responsiveStyles` bucket back to the sibling key', () => { - for (const bucket of ['large', 'medium', 'small', 'xsmall']) { - expect(unknownKeyIssue(ResponsiveConfigSchema, { [bucket]: {} })!.message) - .toContain('`responsiveStyles`'); - } - expect(unknownKeyIssue(ResponsiveConfigSchema, { responsiveStyles: {} })!.message) - .toContain('one level'); - }); - - it('reaches `hiddenOn` from both wrong spellings', () => { - // `hidden` is objectui's RESOLVED spelling (`useResponsiveConfig` returns - // `{ hidden, columns, order, breakpoint }`) — a different WORD, which no - // edit distance can reach, so it keeps its entry in the alias table. - expect(unknownKeyIssue(ResponsiveConfigSchema, { hidden: true })!.message) - .toContain('`hidden` → `hiddenOn`'); - // `hideOn` is the SAME word and had an alias entry of its own until #4990, - // because the fallback charged the author for `hiddenOn`'s capital O: - // `hideOn` scored 3 against a budget of 2 and returned nothing, while the - // all-lowercase `hiddenon` scored 1 and resolved. #4990 folds case on both - // sides, so the alias was retired and this now rides the fallback alone. - // These two assertions ARE batch 13's measurement, kept executable — the - // first fails if the general fix regresses, the second is the comparison - // that made the old behaviour indefensible. - expect(unknownKeyIssue(ResponsiveConfigSchema, { hideOn: ['xs'] })!.message) - .toContain('`hideOn` → `hiddenOn`'); - expect(unknownKeyIssue(ResponsiveConfigSchema, { hiddenon: ['xs'] })!.message) - .toContain('`hiddenon` → `hiddenOn`'); - }); - }); - - describe('the per-breakpoint maps', () => { - it('maps the styling vocabulary back onto the Tailwind ramp', () => { - for (const schema of [BreakpointColumnMapSchema, BreakpointOrderMapSchema]) { - for (const [wrote, meant] of [['large', 'lg'], ['medium', 'md'], ['small', 'sm'], ['xsmall', 'xs']]) { - expect(unknownKeyIssue(schema, { [wrote]: 4 })!.message) - .toContain(`\`${wrote}\` → \`${meant}\``); - } - } - }); - - it('answers the recorded `xxl` near-miss with `2xl`', () => { - // Pinned as an invalid breakpoint name by this file's own test since - // before #4001 ("should reject invalid breakpoint names", above). - expect(unknownKeyIssue(BreakpointColumnMapSchema, { xxl: 4 })!.message) - .toContain('`xxl` → `2xl`'); - }); - - it('was the worst of the three, because half the map survived', () => { - // Measured on `main` before this change: - // ResponsiveConfigSchema.parse({ columns: { large: 4, lg: 3 } }) - // → { columns: { lg: 3 } } - // The node did lay out — at the wrong width, on the breakpoints the - // author never named. A total loss is at least visible. - const result = ResponsiveConfigSchema.safeParse({ columns: { large: 4, lg: 3 } }); - expect(result.success).toBe(false); - }); - - it('still accepts every declared breakpoint, including the quoted one', () => { - expect(BreakpointColumnMapSchema.parse({ xs: 12, sm: 6, md: 4, lg: 3, xl: 2, '2xl': 1 })['2xl']).toBe(1); - expect(BreakpointOrderMapSchema.parse({ xs: 3, '2xl': 1 })['2xl']).toBe(1); - }); - }); - - // THE SEAM THIS CHANGE EXISTS FOR. `PageComponentSchema` has been `.strict()` + // THE SEAM #4001 批 13 EXISTED FOR. `PageComponentSchema` has been `.strict()` // since ADR-0089 D3a, and that never reached these blocks — a strict shell // over strip-mode children is not a closed surface, it is a closed surface's // silhouette. @@ -253,27 +87,24 @@ describe('unknown keys are rejected, not stripped (#4001 batch 13)', () => { it('keeps parsing exactly what it parsed before — pinned', () => { // The showcase authors `responsiveStyles` on ~40 nodes in this shape - // (`examples/app-showcase/src/ui/pages/*.page.ts`); `page.test.ts` authors - // `responsive: { hiddenOn: [...] }`. Both must be untouched by this change. + // (`examples/app-showcase/src/ui/pages/*.page.ts`). It must be untouched + // by the #11027 retirement of its sibling. const parsed = PageComponentSchema.parse(component({ responsiveStyles: { large: { fontSize: '40px', fontWeight: '700' }, small: { fontSize: '30px' }, }, - responsive: { hiddenOn: ['xs', 'sm'], columns: { xs: 12, lg: 4 }, order: { lg: 1 } }, })); expect(parsed.responsiveStyles?.large?.fontSize).toBe('40px'); - expect(parsed.responsive?.hiddenOn).toEqual(['xs', 'sm']); - expect(parsed.responsive?.columns?.lg).toBe(4); + expect(parsed.responsiveStyles?.small?.fontSize).toBe('30px'); }); it('no longer accepts a component whose styling silently evaporates', () => { - // Before this change the SAME input parsed clean and returned - // `{ responsiveStyles: {}, responsive: {} }` — every styling and layout - // instruction the author wrote, gone, reported valid. + // Before #4001 批 13 the SAME input parsed clean and returned + // `{ responsiveStyles: {} }` — every styling instruction the author + // wrote, gone, reported valid. const result = PageComponentSchema.safeParse(component({ responsiveStyles: { lg: { fontSize: '40px' } }, - responsive: { colums: { lg: 4 }, hideOn: ['xs'] }, })); expect(result.success).toBe(false); }); diff --git a/packages/spec/src/ui/responsive.zod.ts b/packages/spec/src/ui/responsive.zod.ts index ad6c4a4347..1dc3df6386 100644 --- a/packages/spec/src/ui/responsive.zod.ts +++ b/packages/spec/src/ui/responsive.zod.ts @@ -6,247 +6,57 @@ import { lazySchema } from '../shared/lazy-schema'; import { strictObject } from '../shared/strict-object'; // ───────────────────────────────────────────────────────────────────────────── -// WHY THIS FILE IS STRICT (#4001 批 13, ADR-0078) — engineering rationale; the -// author-facing text is the JSDoc on each schema, which is what the generated -// reference page renders. +// THIS FILE USED TO CARRY TWO BREAKPOINT VOCABULARIES; ONE SURVIVES (#11027). // -// THIS FILE CARRIES TWO BREAKPOINT VOCABULARIES, AND THEY SIT SIXTEEN LINES -// APART ON THE SAME PAGE COMPONENT. `PageComponentSchema` declares both: +// Until protocol 18 this file declared both channels `PageComponentSchema` +// offered sixteen lines apart: // // responsiveStyles: ResponsiveStylesSchema // large | medium | small | xsmall // responsive: ResponsiveConfigSchema // columns/order keyed xs … 2xl // // The first is desktop-first max-width buckets (ADR-0065, mirroring Builder.io's -// SDK); the second is the Tailwind-style `BreakpointName` ramp declared at the -// top of this file. Both are correct, neither is a typo of the other, and edit -// distance cannot bridge `lg` → `large`. So the curation below is not decoration -// — crossing the two vocabularies is THE mistake this file invites, and until -// #4001 批 13 every crossing was silent. +// SDK) and is LIVE: objectui's `compileScopedStyles` (`@object-ui/core`, +// `styling/scoped-styles.ts`) compiles exactly `large`/`medium`/`small`/`xsmall` +// into id-scoped CSS, and the showcase authors it on ~40 nodes. // -// Measured on `main` before the change (`ResponsiveStylesSchema.parse`): +// The second — the Tailwind-style `xs…2xl` layout config — was retired whole +// (#11027, ADR-0049 D2). Measured across objectstack + objectui with the +// tsc-probe methodology (positive and negative controls): objectui's two +// implementations of the contract (`useResponsiveConfig` in `@object-ui/mobile`, +// `ResponsiveProtocol` in `@object-ui/core`) had ZERO callers, nothing read +// `.responsive` off a page component, and objectui's own node interface never +// declared the key — so `page.components[].responsive`, the destination the +// `dashboard.widgets[].responsive` tombstone (#4876) prescribed as "live", +// applied exactly as much breakpoint behaviour as the key it retired: none. +// `ResponsiveConfigSchema`, `BreakpointColumnMapSchema`, +// `BreakpointOrderMapSchema` and the `BreakpointName` enum had no other +// authorable carrier and left with the key (the PerformanceConfigSchema +// precedent below — an exported schema with no consumer is read as a +// capability, #3950). The removal record is the `retiredKey()` tombstone on +// `page.zod.ts`, the protocol-18 `page-component-responsive-removed` +// conversion, and `RETIRED_DEFS_BY_MAJOR[18]`. // -// { lg: { fontSize: '40px' } } → {} -// { desktop: { fontSize: '40px' } } → {} -// { columns: { large: 4, lg: 3 } } → { columns: { lg: 3 } } -// -// The third one is the worst of the three: half the author's map survived, so -// the node did lay out — at the wrong width, on the breakpoints they did not -// name. And none of it was caught upstream, because THE OUTER GATE DOES NOT -// RECURSE. `PageComponentSchema` has been `.strict()` since ADR-0089 D3a, and it -// still accepted this whole component: -// -// PageComponentSchema.parse({ -// type: 'element:text', id: 't1', -// responsiveStyles: { lg: { fontSize: '40px' } }, -// responsive: { colums: { lg: 4 }, hideOn: ['xs'] }, -// }) -// → { …, responsiveStyles: {}, responsive: {} } -// -// Every styling and layout instruction the author wrote, gone, reported valid. -// That is the campaign's thesis in one parse: a strict shell over strip-mode -// blocks is not a closed surface, it is a closed surface's silhouette. -// -// WHERE THIS BINDS, MEASURED — because a tightening must not claim reach it does -// not have. The door is `getMetadataTypeSchema('page')`, i.e. the registry read -// by `MetadataManager.validate`, `GET /api/v1/meta` and the Studio page form: -// against it, the crossed vocabulary and the legacy breakpoint-keyed shape are -// both rejected, and a clean page still parses. It does NOT bind in -// `objectstack build` / `validate` — that path never parses page metadata at -// all, which a control proves rather than a guess: a key `PageComponentSchema` -// has rejected since ADR-0089 D3a passes both commands and lands in the built -// artifact. That gap predates this change and is filed as #5000; it is recorded -// here so nobody reads the campaign's usual "three example apps validate" line -// as evidence for this surface. -// -// LIVENESS IS NOT WHAT THIS CHANGE CLAIMS. `responsiveStyles` is read — -// objectui's `compileScopedStyles` (`@object-ui/core`, `styling/scoped-styles.ts`) -// compiles exactly `large`/`medium`/`small`/`xsmall` into id-scoped CSS, and the -// showcase authors it on ~40 nodes. `ResponsiveConfigSchema`'s own liveness is a -// separate, open question tracked outside #4001 — closing a shape says which -// keys it declares, never that a renderer reads them. +// The #4001 批 13 strictness engineering (silent strip-mode blocks under a +// strict shell, `lg` → `large` vocabulary crossings) is preserved on the +// surviving shape below; the batch's measurements live in +// `docs/audits/2026-07-unknown-key-strictness-ledger.md` (`responsive.zod.ts` +// row) and the #4001 semantic entry. // ───────────────────────────────────────────────────────────────────────────── -/** - * Breakpoint Name Enum - * Standard Tailwind-style breakpoint names (xs–2xl). - */ -export const BreakpointName = z.enum(['xs', 'sm', 'md', 'lg', 'xl', '2xl']); - -export type BreakpointName = z.input; - -/** - * Aliases for the two per-breakpoint MAPS (`columns` / `order`), which are keyed - * by {@link BreakpointName}. - * - * Anchored to a named sibling rather than to edit distance: the four targets are - * the key set of {@link ResponsiveStylesSchema}, the OTHER breakpoint vocabulary - * on the same page component. `large` → `lg` is a vocabulary crossing, not a - * misspelling, so only a written-down entry can answer it. - * - * `xxl` is the one entry that is not a crossing — it is the recorded near-miss - * for `2xl`, pinned as rejected by this file's own test since before #4001 - * (`responsive.test.ts`, "should reject invalid breakpoint names"). - */ -const BREAKPOINT_MAP_ALIASES = { - large: 'lg', - medium: 'md', - small: 'sm', - xsmall: 'xs', - xxl: '2xl', -} as const; - -const BREAKPOINT_MAP_HISTORY = - 'Until #4001 批 13 a breakpoint name this map does not declare was dropped silently — ' + - 'the surviving half of the map still laid the node out, at the wrong width, on the ' + - 'breakpoints the author never named.'; - -/** - * Breakpoint Column Map Schema - * Maps breakpoint names to grid column counts (1-12). - * All entries are optional — only specified breakpoints are configured. - */ -export const BreakpointColumnMapSchema = lazySchema(() => strictObject( - { - surface: 'this per-breakpoint column map', - history: BREAKPOINT_MAP_HISTORY, - aliases: BREAKPOINT_MAP_ALIASES, - }, - { - xs: z.number().min(1).max(12).optional(), - sm: z.number().min(1).max(12).optional(), - md: z.number().min(1).max(12).optional(), - lg: z.number().min(1).max(12).optional(), - xl: z.number().min(1).max(12).optional(), - '2xl': z.number().min(1).max(12).optional(), - }, -).describe('Grid columns per breakpoint (1-12)')); -export type BreakpointColumnMap = z.input; - -/** - * Breakpoint Order Map Schema - * Maps breakpoint names to display order numbers. - * All entries are optional — only specified breakpoints are configured. - */ -export const BreakpointOrderMapSchema = lazySchema(() => strictObject( - { - surface: 'this per-breakpoint order map', - history: BREAKPOINT_MAP_HISTORY, - aliases: BREAKPOINT_MAP_ALIASES, - }, - { - xs: z.number().optional(), - sm: z.number().optional(), - md: z.number().optional(), - lg: z.number().optional(), - xl: z.number().optional(), - '2xl': z.number().optional(), - }, -).describe('Display order per breakpoint')); -export type BreakpointOrderMap = z.input; - -/** - * A bare breakpoint name written at the `responsive` LEVEL rather than inside - * one of its maps — the legacy breakpoint-keyed shape. - * - * This is not hypothetical: the retired `view.responsive` was authored that way, - * and the conversion registry still carries `responsive: { sm: {} }` as the - * `before` fixture of `view-inert-keys-removed`. A rename cannot answer it — - * three different keys are plausible targets — so each name gets its own - * prescription. Per-key rather than one shared string, because `guidance` emits - * one bullet VERBATIM per offending key and a shared string prints the same - * paragraph N times (the 批 10 `join`/`joinGateway` lesson). - */ -const BREAKPOINT_AT_TOP_LEVEL = Object.fromEntries( - (['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const).map((bp) => [ - bp, - `\`${bp}\` is a BREAKPOINT NAME, not a key on this block — the breakpoint-keyed ` + - `shape (\`responsive: { ${bp}: … }\`) belonged to \`view.responsive\`, retired in 17 ` + - `(#3896). Name the knob first, the breakpoint second: \`columns: { ${bp}: 6 }\`, ` + - `\`order: { ${bp}: 2 }\`, or \`hiddenOn: ['${bp}']\`.`, - ]), -); - -/** - * A key from the SIBLING vocabulary — `responsiveStyles`' max-width buckets — - * written on the layout block instead. The fix is the sibling key on the same - * component, so this is a wrong-layer pointer rather than a rename. - */ -const STYLE_BUCKET_AT_LAYOUT_LEVEL = Object.fromEntries( - (['large', 'medium', 'small', 'xsmall'] as const).map((bucket) => [ - bucket, - `\`${bucket}\` is a \`responsiveStyles\` bucket, not a \`responsive\` key — this block ` + - `configures LAYOUT (grid columns / visibility / order) on the \`xs\`…\`2xl\` axis. For ` + - `per-breakpoint CSS write the sibling key on this component: ` + - `\`responsiveStyles: { ${bucket}: { … } }\` (ADR-0065).`, - ]), -); - -/** - * Responsive Configuration Schema - * - * Provides responsive layout configuration for UI components. - * Maps breakpoint names to layout behavior (columns, visibility, order). - * - * @example - * ```typescript - * const config: ResponsiveConfig = { - * columns: { xs: 12, sm: 6, lg: 4 }, - * hiddenOn: ['xs'], - * order: { xs: 2, lg: 1 }, - * }; - * ``` +/* + * REMOVED — `BreakpointName`, `BreakpointColumnMapSchema` / + * `BreakpointColumnMap`, `BreakpointOrderMapSchema` / `BreakpointOrderMap`, + * `ResponsiveConfigSchema` / `ResponsiveConfig` (#11027, ADR-0049 D2). They + * typed the `responsive` layout block whose only remaining authorable carrier + * was `page.components[].responsive` (its widget sibling was retired in + * #4876, `view.responsive` in #3896): every carrier was authorable and inert + * — no renderer or runtime ever applied a per-breakpoint column count, + * display order, or visibility from it. An exported schema with no consumer + * is read as a capability by whoever finds it (#3950 precedent; exactly how + * `PerformanceConfigSchema` left in the #3896 close-out). The Tailwind-style + * `xs…2xl` layout vocabulary returns if and when a renderer implements it — + * in one change, with the engine (the #4834 rule). */ -export const ResponsiveConfigSchema = lazySchema(() => strictObject( - { - surface: 'this responsive layout configuration', - history: - 'Until #4001 批 13 these were dropped silently — and the component around them ' + - 'parsed clean, because `PageComponentSchema` is strict only at its own level.', - aliases: { - // objectui's `useResponsiveConfig` resolves this block and returns - // `{ hidden, columns, order, breakpoint }` (`@object-ui/mobile`, - // `useResponsiveConfig.ts`). `hidden` is that RESULT's spelling of the - // authored `hiddenOn`, which is where the wrong word comes from. - hidden: 'hiddenOn', - // `hideOn` USED to need an entry here. It is not a different word — it is - // the same word, and the distance fallback could not reach it only - // because of #4990: the fallback lowercased the INPUT but not the - // CANDIDATES, so `hiddenOn`'s capital O cost an extra edit and `hideOn` - // scored 3 against a budget of 2, while the all-lowercase `hiddenon` - // scored 1 and resolved fine. #4990 fixed that at the source by folding - // case on both sides, so this per-case workaround is retired: `hideOn` - // now reaches `hiddenOn` on distance alone. The measurement that - // justified the entry is preserved as an assertion in `responsive.test.ts` - // ("reaches `hiddenOn` from both wrong spellings") rather than as a - // comment, so it fails if the general fix ever regresses. - }, - guidance: { - ...BREAKPOINT_AT_TOP_LEVEL, - ...STYLE_BUCKET_AT_LAYOUT_LEVEL, - responsiveStyles: - '`responsiveStyles` is a SIBLING key on the component, not a key inside ' + - '`responsive`. Move it out one level: `{ …component, responsive: { … }, ' + - 'responsiveStyles: { large: { … } } }` (ADR-0065).', - }, - }, - { - /** Minimum breakpoint for visibility */ - breakpoint: BreakpointName.optional() - .describe('Minimum breakpoint for visibility'), - - /** Hide on specific breakpoints */ - hiddenOn: z.array(BreakpointName).optional() - .describe('Hide on these breakpoints'), - - /** Grid columns per breakpoint (1-12 column grid) */ - columns: BreakpointColumnMapSchema.optional().describe('Grid columns per breakpoint'), - - /** Display order per breakpoint */ - order: BreakpointOrderMapSchema.optional().describe('Display order per breakpoint'), - }, -).describe('Responsive layout configuration')); - -export type ResponsiveConfig = z.input; /** * Style Map Schema (ADR-0065) @@ -280,9 +90,11 @@ export type StyleMap = z.input; * collision-free, responsive-correct. Desktop-first: `large` is the * unconditional base; `medium`/`small`/`xsmall` are `max-width` overrides. * - * Distinct from {@link ResponsiveConfigSchema}, which configures *layout* (grid - * columns / visibility / order) on the Tailwind `xs..2xl` axis. This styles a - * node's own box; that arranges a node within a grid. + * Since #11027 this is the ONLY per-breakpoint channel on a page component: the + * sibling `responsive` layout block (grid columns / visibility / order on a + * Tailwind `xs..2xl` axis) was authorable and inert, and was retired under + * ADR-0049 D2. Express layout per breakpoint as CSS here instead — see the + * `guidance` entries below for the per-knob translations. */ export const ResponsiveStylesSchema = lazySchema(() => strictObject( { @@ -292,12 +104,13 @@ export const ResponsiveStylesSchema = lazySchema(() => strictObject( 'and since the whole block is optional, a node whose every style was written ' + 'under the wrong vocabulary rendered completely unstyled and parsed clean.', aliases: { - // The Tailwind ramp is `BreakpointName`, declared at the top of this file - // and used by the SIBLING `responsive` key on the same page component. - // Mapped onto the max-width bucket that contains each one: objectui's - // `STYLE_BREAKPOINTS` cuts at medium ≤991px, small ≤640px, xsmall ≤479px - // (`@object-ui/core`, `styling/scoped-styles.ts`), and `large` is the - // unconditional base, so everything above `md` lands there. + // The Tailwind ramp (`xs`…`2xl`) is the vocabulary the retired sibling + // `responsive` block was keyed by (#11027), and the one authors carry in + // from Tailwind itself. Mapped onto the max-width bucket that contains + // each name: objectui's `STYLE_BREAKPOINTS` cuts at medium ≤991px, + // small ≤640px, xsmall ≤479px (`@object-ui/core`, + // `styling/scoped-styles.ts`), and `large` is the unconditional base, so + // everything above `md` lands there. xs: 'xsmall', sm: 'small', md: 'medium', @@ -306,18 +119,22 @@ export const ResponsiveStylesSchema = lazySchema(() => strictObject( '2xl': 'large', }, guidance: { + // The three knobs of the RETIRED `responsive` layout block (#11027, + // ADR-0049 D2 — no renderer ever applied them). An author who lands one + // here is usually migrating off that block, so each entry carries the + // CSS translation that IS applied. columns: - '`columns` is a `responsive` key, not a `responsiveStyles` bucket — this block ' + - 'holds per-breakpoint CSS. For a grid column count write the sibling key on this ' + - 'component: `responsive: { columns: { lg: 4 } }`.', + '`columns` was a key of the retired `responsive` layout block (#11027 — no renderer ' + + 'ever applied it). This block holds per-breakpoint CSS. Express a column span as CSS ' + + "on the bucket where it should apply, e.g. `medium: { gridColumn: 'span 6' }`.", hiddenOn: - '`hiddenOn` is a `responsive` key, not a `responsiveStyles` bucket. Write the ' + - "sibling key on this component: `responsive: { hiddenOn: ['xs'] }` — or express it " + - 'as CSS here with `xsmall: { display: \'none\' }`.', + '`hiddenOn` was a key of the retired `responsive` layout block (#11027 — no renderer ' + + 'ever applied it). This block holds per-breakpoint CSS. Hide a component per ' + + "breakpoint with CSS, e.g. `xsmall: { display: 'none' }`.", order: - '`order` is a `responsive` key, not a `responsiveStyles` bucket. Write the sibling ' + - 'key on this component: `responsive: { order: { lg: 1 } }` — or express it as CSS ' + - "here with `large: { order: '1' }`.", + '`order` was a key of the retired `responsive` layout block (#11027 — no renderer ' + + 'ever applied it). This block holds per-breakpoint CSS. Set display order per ' + + "breakpoint with CSS, e.g. `small: { order: '1' }`.", }, }, { diff --git a/packages/spec/src/ui/widget-i18n-retirement.test.ts b/packages/spec/src/ui/widget-i18n-retirement.test.ts index efdb12cb13..3f17653d6f 100644 --- a/packages/spec/src/ui/widget-i18n-retirement.test.ts +++ b/packages/spec/src/ui/widget-i18n-retirement.test.ts @@ -105,7 +105,8 @@ describe('[#5055] ui/ widget + i18n family retirement', () => { 'AriaPropsSchema', 'I18nLabelSchema', // Neighbours a too-wide `ui/` sweep would plausibly take. - 'ResponsiveConfigSchema', + // (`ResponsiveConfigSchema` stood here until #11027 retired the responsive + // layout surface whole — a survivor list entry follows its subject out.) 'NotificationTypeSchema', 'SharingConfigSchema', // (`ThemeSchema` stood here until #10485 retired the theme surface whole.) diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 01f52e6a46..73d2513d4c 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -40,7 +40,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and - `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. -- `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Breakpoint Name Enum +- `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Style Map Schema (ADR-0065) - `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol ## How to read these From 7125cdc6e85cbdb38ace31c5c588868b15618691 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 12:43:39 +0000 Subject: [PATCH 2/3] =?UTF-8?q?revert(skills):=20drop=20the=20generated=20?= =?UTF-8?q?=5Findex.md=20hunk=20from=20this=20PR=20=E2=80=94=20skills/**?= =?UTF-8?q?=20is=20a=20governed=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One governed-path hit forks the whole PR to human-merge-only (AGENTS.md Prime Directive #14: a mixed diff is not a proportion question). The regenerated skill-reference line moves to its own PR, which lands only after this retirement — until then check:skill-refs reads the committed line as stale against this branch's sources by design. Part of #11027 Co-Authored-By: Claude --- skills/objectstack-ui/references/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 73d2513d4c..01f52e6a46 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -40,7 +40,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and - `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. -- `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Style Map Schema (ADR-0065) +- `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Breakpoint Name Enum - `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol ## How to read these From d9bb37b2828dcbff2f6f62ebdbda514a42667add Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 13:21:25 +0000 Subject: [PATCH 3/3] =?UTF-8?q?revert(skills):=20restore=20the=20regenerat?= =?UTF-8?q?ed=20=5Findex.md=20line=20=E2=80=94=20PM=20withdrew=20the=20gov?= =?UTF-8?q?erned-surface=20split?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check:skill-refs byte-compares the committed index against a fresh render of this tree's own sources, so the generator-owned line must ride the source PR: split out, it left this branch (and would have left main, between the two landings) red on that gate. Regenerated with gen:skill-refs, verified in sync. Part of #11027 Co-Authored-By: Claude --- skills/objectstack-ui/references/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 01f52e6a46..73d2513d4c 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -40,7 +40,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and - `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. -- `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Breakpoint Name Enum +- `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Style Map Schema (ADR-0065) - `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol ## How to read these