From bf73bf21673abf4b3dc1dc13f0f82fb87830bfa5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 18:20:28 +0000 Subject: [PATCH 1/2] refactor(spec)!: retire dashboard.widgets[].responsive (#4876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0049 enforce-or-remove, mirroring #3896's handling of the identical `view.responsive`: authorable, parsed, and never read by any renderer. Re-measured 2026-08-03 — no objectui code reads `widget.responsive` (DashboardRenderer/DashboardEditor/plugin-designer mention it in comments only; the real per-breakpoint consumer `useResponsiveConfig` is fed by `page.components[].responsive`), and zero authored instances repo-wide. It survived the #3896 sweep through a liveness drill gap, not evidence: `dashboard.json` declares no `children` on `widgets`, so no widget-level key has ever been classified (#4956, separate). - `retiredKey()` tombstone carrying the migration prescription; the key stays declared so `.strict()` answers with the prescription rather than a generic "unrecognized key" - D2 conversion `dashboard-widget-responsive-removed` (retiredFromLoadPath) + D3 chain step at major 17; own entry, not folded into the #3896 one - shared `ResponsiveConfigSchema` and its `page.zod.ts` embed untouched — pinned as a control - baselines: authorable-surface KEY-level only (`[RETIRED]`); manifest / api-surface / api-surface-signatures unchanged by construction Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 Co-Authored-By: Claude Opus 5 --- .../dashboard-widget-responsive-removed.md | 73 +++++++++++++++++++ content/docs/references/ui/dashboard.mdx | 2 +- content/docs/ui/dashboards.mdx | 1 - docs/protocol-upgrade-guide.md | 3 + packages/spec/authorable-surface.json | 2 +- packages/spec/liveness/dashboard.json | 2 +- packages/spec/spec-changes.json | 12 +++ packages/spec/src/conversions/registry.ts | 60 +++++++++++++++ packages/spec/src/migrations/registry.ts | 18 ++++- packages/spec/src/ui/dashboard.test.ts | 70 ++++++++++++++++++ packages/spec/src/ui/dashboard.zod.ts | 27 ++++++- 11 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 .changeset/dashboard-widget-responsive-removed.md diff --git a/.changeset/dashboard-widget-responsive-removed.md b/.changeset/dashboard-widget-responsive-removed.md new file mode 100644 index 0000000000..308047f4e9 --- /dev/null +++ b/.changeset/dashboard-widget-responsive-removed.md @@ -0,0 +1,73 @@ +--- +"@objectstack/spec": major +--- + +refactor(spec)!: retire `dashboard.widgets[].responsive` — the straggler of the #3896 inert-key sweep (#4876, ADR-0049) + +`DashboardWidgetSchema.responsive` let an author declare per-breakpoint layout +overrides on a dashboard widget — `breakpoint`, `hiddenOn`, `columns`, `order` — +and no renderer ever read them. The value parsed, validated, and then did +nothing: `DashboardRenderer`, `DashboardEditor` and `plugin-designer` name +`responsive` only in comments, and the one genuine per-breakpoint consumer in +objectui (`useResponsiveConfig`) is fed by `page.components[].responsive`, never +by a widget. Re-measured 2026-08-03 across both repos, plus zero authored +instances anywhere in this repo's examples, apps and tests. + +Four days earlier, #3896 retired the **literally same-named** `view.responsive` +on exactly this evidence. This embed survived that sweep for a reason that is +worth stating plainly, because it is not "we looked and it was live": the +liveness ledger declares no `children` on `dashboard.widgets`, and the walk +drills only one level through an explicit `children` — so **no widget-level key +has ever been classified at all** (22 of them). The instrument had a hole, not +the key a mandate. That gap is filed and fixed separately as **#4956**. + +Leaving it would have shipped v17 with one word and two fates — `view.responsive` +a `tsc` error, `dashboard.widgets[].responsive` silently accepted — which no +author or authoring agent could be expected to explain, on a key that today +accepts *any* content on both sides (objectui types it a documented `any`). That +is precisely where AI-authored metadata errors hide and multiply. + +FROM → TO: + +| Removed | Replacement | +| :--- | :--- | +| `dashboard.widgets[].responsive` (key) | **none** — delete it; the grid reflows by `columns` + `gap` on the dashboard and the `layout` box on each widget | + +**The shape is NOT removed — only this embed.** `ResponsiveConfigSchema` / +`ResponsiveConfig` stay exported and stay live on `page.components[].responsive`, +whose renderer genuinely reads them. Nothing that imports the shape breaks, and +an author who needs breakpoint behaviour today has a real place to put it. This +narrowness is deliberate: the maintainer's ruling covers the dashboard widget +surface only. + +The retirement kit: + +- **Tombstone.** `retiredKey()` on the widget key. `DashboardWidgetSchema` *is* + `.strict()`, so a plain delete would still be loud — but only as a generic + "unrecognized key". The tombstone keeps the key declared so the rejection + carries the **prescription**, and types the key `never` so authoring it fails + `tsc` first. A pin asserts the message is the prescription and *not* + `Unrecognized key`. +- **ADR-0087 D2 conversion + D3 chain step** + (`dashboard-widget-responsive-removed`, `retiredFromLoadPath`): + `os migrate meta --from 16` deletes the key from author sources, and stored + dashboards replay clean instead of meeting the tombstone at load. A lossless + delete — the key never had an effect to lose. Kept as its own entry rather than + folded into `dashboard-inert-keys-removed`, whose identity is the #3896 sweep: + this removal rests on its own 2026-08-03 measurement and should say so in + `spec-changes.json` and the upgrade guide. +- **No liveness row is added**, matching `widgets[].performance` in the #3896 + sweep — a widget-level row would be an ORPHAN, not a classification, until + #4956 lands the drill. The ledger `_note` records the removal and why the row + is absent. +- Baselines moved at KEY level only, as the shape's survival implies: + `authorable-surface.json` gains `ui/DashboardWidget:responsive [RETIRED]`; + `json-schema.manifest.json`, `api-surface.json` and + `api-surface-signatures.json` are unchanged by construction — no def stopped + being emitted and no export was removed. + +No runtime behaviour changes — that impossibility is the reason for the removal. + +**objectui shard:** the outcome is retirement, not the fallback clause, so +objectui#3235's conditional pin-bump item is permanently de-listed; the `any` +declaration on that repo's side can be cleaned on its own schedule. diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index a0b5c44dc1..19c4f7a5d4 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -110,7 +110,7 @@ Dashboard header action | **options** | `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** | `{ 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** | `any` | 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 rewrite it automatically. | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | diff --git a/content/docs/ui/dashboards.mdx b/content/docs/ui/dashboards.mdx index b366399bee..35fdc69139 100644 --- a/content/docs/ui/dashboards.mdx +++ b/content/docs/ui/dashboards.mdx @@ -106,7 +106,6 @@ selects `dimensions` (X / group / split) and `values` (the measures to plot): | `colorVariant` | `enum` | optional | KPI/card accent color | | `compareTo` | `enum \| object` | optional | Period-over-period comparison window | | `options` | `object` | optional | Renderer extras **plus** the query keys below | -| `responsive` | `object` | optional | Responsive behavior | ### Widget `options` diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 3f368fc411..518dcc4033 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -190,6 +190,8 @@ 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. + ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | @@ -214,6 +216,7 @@ It also removes `connector.rateLimitConfig` and its whole shape (#4911). This on | `flow-inert-keys-removed` | `flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId` | flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle) | retired — `migrate meta` only | | `view-inert-keys-removed` | `view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria` | view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live) | 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 | | `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 | | `skill-trigger-phrases-removed` | `skill.triggerPhrases` | skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection) | retired — `migrate meta` only | | `stack-api-require-auth-removed` | `stack.api.requireAuth` | stack key 'api.requireAuth' removed — anonymous access is always denied; publish public surfaces by declaration (#3963) | retired — `migrate meta` only | diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 64a1191624..8d51af1f4b 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -7204,7 +7204,7 @@ "ui/DashboardWidget:options", "ui/DashboardWidget:requiresObject", "ui/DashboardWidget:requiresService", - "ui/DashboardWidget:responsive", + "ui/DashboardWidget:responsive [RETIRED]", "ui/DashboardWidget:suppressWarnings", "ui/DashboardWidget:title", "ui/DashboardWidget:type", diff --git a/packages/spec/liveness/dashboard.json b/packages/spec/liveness/dashboard.json index 1333e6ce58..c539786c85 100644 --- a/packages/spec/liveness/dashboard.json +++ b/packages/spec/liveness/dashboard.json @@ -1,6 +1,6 @@ { "type": "dashboard", - "_note": "DashboardSchema (UI, ADR-0021 dataset-bound). Live path: objectui DashboardView → DashboardRenderer → DatasetWidget. Seeded from docs/audits/2026-06-dashboardschema-property-liveness.md and re-verified against objectui HEAD — several audit-era findings are superseded: the ADR-0021 widget migration shipped (Studio WidgetConfigPanel + DashboardRenderer on dataset/dimensions/values, framework#3251; DashboardWidgetSchema is now `.strict()`); `globalFilters`/`dateRange` are LIVE (dashboard-level filters, framework#2501); the `title`↔`label` drift is fixed (renderer falls back to `label`, objectui#2806); the undeclared widget props were reconciled (#1894). objectui paths cited as prose in `note` (not `evidence`). Framework provenance/lock fields auto-classify live (ADR-0010). Widget-level props are classified in the DashboardWidgetSchema subtree, not drilled here. 2026-07-30 (#3896 close-out sweep): the dead authoring keys were REMOVED — tombstoned at the schema with prescriptions (retiredKey) and stripped by the protocol-17 close-out conversions; entries deleted per the #3715 precedent.", + "_note": "DashboardSchema (UI, ADR-0021 dataset-bound). Live path: objectui DashboardView → DashboardRenderer → DatasetWidget. Seeded from docs/audits/2026-06-dashboardschema-property-liveness.md and re-verified against objectui HEAD — several audit-era findings are superseded: the ADR-0021 widget migration shipped (Studio WidgetConfigPanel + DashboardRenderer on dataset/dimensions/values, framework#3251; DashboardWidgetSchema is now `.strict()`); `globalFilters`/`dateRange` are LIVE (dashboard-level filters, framework#2501); the `title`↔`label` drift is fixed (renderer falls back to `label`, objectui#2806); the undeclared widget props were reconciled (#1894). objectui paths cited as prose in `note` (not `evidence`). Framework provenance/lock fields auto-classify live (ADR-0010). Widget-level props are classified in the DashboardWidgetSchema subtree, not drilled here. 2026-07-30 (#3896 close-out sweep): the dead authoring keys were REMOVED — tombstoned at the schema with prescriptions (retiredKey) and stripped by the protocol-17 close-out conversions; entries deleted per the #3715 precedent. 2026-08-03 (#4876): `widgets[].responsive` REMOVED — tombstoned (retiredKey) and stripped by the protocol-17 `dashboard-widget-responsive-removed` conversion. It carries NO row here, deliberately, exactly like `widgets[].performance` in the #3896 sweep: the walk drills only one level through an explicit `children`, and `widgets` declares none, so a widget-level row would be an ORPHAN rather than a classification. That gap — not evidence of liveness — is why this key outlived the sweep; it is filed as #4956 and fixed there, and closing it is what will finally bring the 22 widget-level keys under the ratchet.", "props": { "name": { "status": "live", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 12afec70d3..09745be9a4 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -188,6 +188,12 @@ "conversionId": "dashboard-inert-keys-removed", "toMajor": 17 }, + { + "surface": "dashboard.widgets[].responsive", + "to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected)", + "conversionId": "dashboard-widget-responsive-removed", + "toMajor": 17 + }, { "surface": "agent.knowledge", "to": "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", @@ -824,6 +830,12 @@ "conversionId": "dashboard-inert-keys-removed", "toMajor": 17 }, + { + "surface": "dashboard.widgets[].responsive", + "to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected)", + "conversionId": "dashboard-widget-responsive-removed", + "toMajor": 17 + }, { "surface": "agent.knowledge", "to": "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index bcd1d5d300..0b4f8231e1 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -2225,6 +2225,65 @@ const dashboardInertKeysRemoved: MetadataConversion = { }, }; +/** + * dashboard.widgets[].responsive (#4876) — the same-named `view.responsive` + * went in the #3896 close-out above; this one escaped that sweep through a + * liveness drill gap rather than on evidence (`dashboard.json` declares no + * `children` on `widgets`, so no widget-level key was ever classified — filed + * as #4956). Re-measured 2026-08-03: no objectui code reads + * `widget.responsive`, and zero authored instances exist repo-wide. + * + * Deliberately a SEPARATE entry rather than another key on + * `dashboard-inert-keys-removed`: that entry's identity is the #3896 sweep, and + * folding a differently-evidenced removal into it would misattribute this one + * in `spec-changes.json` and the upgrade guide — the two places an upgrading + * 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`. + */ +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)", + apply(stack, emit) { + return mapCollection(stack, 'dashboards', (d, path) => { + const widgets = d.widgets; + if (!Array.isArray(widgets)) return d; + let touched = false; + const rebuilt = widgets.map((w, i) => { + if (!w || typeof w !== 'object' || Array.isArray(w)) return w; + const cleaned = stripKeys(w as Record, ['responsive'], emit, `${path}.widgets[${i}]`); + if (cleaned !== w) touched = true; + return cleaned; + }); + if (!touched) return d; + return { ...d, widgets: rebuilt }; + }); + }, + fixture: { + before: { + dashboards: [{ + name: 'ops_overview', + widgets: [{ + id: 'w1', type: 'kpi', dataset: 'orders', values: ['total'], + responsive: { columns: { xs: 12, lg: 4 }, order: { xs: 2, lg: 1 }, hiddenOn: ['xs'] }, + }], + }], + }, + after: { + dashboards: [{ + name: 'ops_overview', + widgets: [{ id: 'w1', type: 'kpi', dataset: 'orders', values: ['total'] }], + }], + }, + expectedNotices: 1, + }, +}; + /** * agent.knowledge — a grounding claim nothing enforced (the RAG path reads * `sourceIds` from the LLM's tool-call arguments, never the agent record). @@ -3667,6 +3726,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly { })).toThrow(); }); }); + +// ============================================================================ +// [#4876] `widgets[].responsive` is RETIRED — mirrors #3896's `view.responsive` +// ============================================================================ +// +// RUNTIME assertions, deliberately. #4642 established that a compile-time pin in +// `packages/spec` is a no-op: `tsconfig.json` excludes `**/*.test.ts` and +// `vitest.config.ts` never enables `typecheck`, so an `Assert< Equal< … > >` +// here would be dead text. The tombstone's `tsc` channel is proved by the build +// of the packages that author dashboards, not by this file. +// +// The pair below is the whole contract of this retirement: the widget embed +// REJECTS with the prescription, and the shared shape it used to reference is +// untouched everywhere else. Splitting one shared schema's two embeds is +// exactly the change that silently over-reaches, so the control is not optional. +describe('[#4876] DashboardWidgetSchema — retired `responsive`', () => { + const widget = { id: 'orders_kpi', type: 'metric', dataset: 'orders', values: ['total'] }; + + it('REJECTS an authored `responsive` with the prescription (not "unrecognized key")', () => { + let message = ''; + try { + DashboardWidgetSchema.parse({ + ...widget, + responsive: { columns: { xs: 12, lg: 4 }, hiddenOn: ['xs'] }, + }); + } catch (e) { message = String((e as Error).message); } + + // The prescription itself, in the four parts an upgrading author needs: + // the fully-qualified key, the version, the issue, and the fix. + expect(message).toMatch(/dashboard\.widgets\[\]\.responsive/); + 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/); + // `.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. + expect(message).not.toMatch(/Unrecognized key/); + }); + + it('still accepts a widget with no `responsive` (the retirement strips nothing else)', () => { + const w = DashboardWidgetSchema.parse(widget); + expect(w).not.toHaveProperty('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 () => { + 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'] }, + }); + // 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'] }); + }); +}); diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index d626cb74ec..4558f88581 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -10,7 +10,10 @@ import { ChartTypeSchema, ChartConfigSchema } from './chart.zod'; import { ActionType } from './action.zod'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; -import { ResponsiveConfigSchema } from './responsive.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. /** * Color variant for dashboard widgets (e.g., KPI cards). @@ -347,8 +350,26 @@ export const DashboardWidgetSchema = lazySchema(() => z.object({ */ suppressWarnings: z.array(z.string()).optional().describe('Build diagnostic rule ids suppressed on this widget'), - /** Responsive layout overrides per breakpoint */ - responsive: ResponsiveConfigSchema.optional().describe('Responsive layout configuration'), + // `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. + 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. ' + + 'Run `os migrate meta --from 16` to rewrite it automatically.', + ), /** ARIA accessibility attributes */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), From 96062fcc0433ff932b95109f464f48a7400b712c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 19:02:20 +0000 Subject: [PATCH 2/2] chore(spec): regenerate authorable-surface after merging main (#4876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge of origin/main resolved `authorable-surface.json` to this branch's side, silently dropping the 16 rows #4974 (#4001 批 11) added for `automation/Webhook` and `integration/WebhookConfig` — main's own committed baseline had them. Regenerating via `gen:schema` (the gate's own proof path, never a hand edit) restores both sides: the sibling's webhook rows and this branch's `ui/DashboardWidget:responsive [RETIRED]`. Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 Co-Authored-By: Claude Opus 5 --- packages/spec/authorable-surface.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 8d51af1f4b..902015d532 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -2494,6 +2494,13 @@ "automation/WaitResumePayload:signalName", "automation/WaitResumePayload:variables", "automation/WaitResumePayload:webhookPayload", + "automation/Webhook:_lock", + "automation/Webhook:_lockDocsUrl", + "automation/Webhook:_lockReason", + "automation/Webhook:_lockSource", + "automation/Webhook:_packageId", + "automation/Webhook:_packageVersion", + "automation/Webhook:_provenance", "automation/Webhook:description", "automation/Webhook:headers", "automation/Webhook:isActive", @@ -2501,6 +2508,7 @@ "automation/Webhook:method", "automation/Webhook:name", "automation/Webhook:object", + "automation/Webhook:protection", "automation/Webhook:secret", "automation/Webhook:timeoutMs", "automation/Webhook:triggers", @@ -4169,6 +4177,13 @@ "integration/RetryConfig:retryOnNetworkError", "integration/RetryConfig:retryableStatusCodes", "integration/RetryConfig:strategy", + "integration/WebhookConfig:_lock", + "integration/WebhookConfig:_lockDocsUrl", + "integration/WebhookConfig:_lockReason", + "integration/WebhookConfig:_lockSource", + "integration/WebhookConfig:_packageId", + "integration/WebhookConfig:_packageVersion", + "integration/WebhookConfig:_provenance", "integration/WebhookConfig:description", "integration/WebhookConfig:events", "integration/WebhookConfig:headers", @@ -4177,6 +4192,7 @@ "integration/WebhookConfig:method", "integration/WebhookConfig:name", "integration/WebhookConfig:object", + "integration/WebhookConfig:protection", "integration/WebhookConfig:secret", "integration/WebhookConfig:signatureAlgorithm", "integration/WebhookConfig:timeoutMs",