Skip to content

Commit 7d0e7b5

Browse files
refactor(spec)!: retire dashboard.widgets[].responsive (#4876) (#4995)
* refactor(spec)!: retire dashboard.widgets[].responsive (#4876) 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 <noreply@anthropic.com> * chore(spec): regenerate authorable-surface after merging main (#4876) 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 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1cae606 commit 7d0e7b5

11 files changed

Lines changed: 262 additions & 8 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: retire `dashboard.widgets[].responsive` — the straggler of the #3896 inert-key sweep (#4876, ADR-0049)
6+
7+
`DashboardWidgetSchema.responsive` let an author declare per-breakpoint layout
8+
overrides on a dashboard widget — `breakpoint`, `hiddenOn`, `columns`, `order`
9+
and no renderer ever read them. The value parsed, validated, and then did
10+
nothing: `DashboardRenderer`, `DashboardEditor` and `plugin-designer` name
11+
`responsive` only in comments, and the one genuine per-breakpoint consumer in
12+
objectui (`useResponsiveConfig`) is fed by `page.components[].responsive`, never
13+
by a widget. Re-measured 2026-08-03 across both repos, plus zero authored
14+
instances anywhere in this repo's examples, apps and tests.
15+
16+
Four days earlier, #3896 retired the **literally same-named** `view.responsive`
17+
on exactly this evidence. This embed survived that sweep for a reason that is
18+
worth stating plainly, because it is not "we looked and it was live": the
19+
liveness ledger declares no `children` on `dashboard.widgets`, and the walk
20+
drills only one level through an explicit `children` — so **no widget-level key
21+
has ever been classified at all** (22 of them). The instrument had a hole, not
22+
the key a mandate. That gap is filed and fixed separately as **#4956**.
23+
24+
Leaving it would have shipped v17 with one word and two fates — `view.responsive`
25+
a `tsc` error, `dashboard.widgets[].responsive` silently accepted — which no
26+
author or authoring agent could be expected to explain, on a key that today
27+
accepts *any* content on both sides (objectui types it a documented `any`). That
28+
is precisely where AI-authored metadata errors hide and multiply.
29+
30+
FROM → TO:
31+
32+
| Removed | Replacement |
33+
| :--- | :--- |
34+
| `dashboard.widgets[].responsive` (key) | **none** — delete it; the grid reflows by `columns` + `gap` on the dashboard and the `layout` box on each widget |
35+
36+
**The shape is NOT removed — only this embed.** `ResponsiveConfigSchema` /
37+
`ResponsiveConfig` stay exported and stay live on `page.components[].responsive`,
38+
whose renderer genuinely reads them. Nothing that imports the shape breaks, and
39+
an author who needs breakpoint behaviour today has a real place to put it. This
40+
narrowness is deliberate: the maintainer's ruling covers the dashboard widget
41+
surface only.
42+
43+
The retirement kit:
44+
45+
- **Tombstone.** `retiredKey()` on the widget key. `DashboardWidgetSchema` *is*
46+
`.strict()`, so a plain delete would still be loud — but only as a generic
47+
"unrecognized key". The tombstone keeps the key declared so the rejection
48+
carries the **prescription**, and types the key `never` so authoring it fails
49+
`tsc` first. A pin asserts the message is the prescription and *not*
50+
`Unrecognized key`.
51+
- **ADR-0087 D2 conversion + D3 chain step**
52+
(`dashboard-widget-responsive-removed`, `retiredFromLoadPath`):
53+
`os migrate meta --from 16` deletes the key from author sources, and stored
54+
dashboards replay clean instead of meeting the tombstone at load. A lossless
55+
delete — the key never had an effect to lose. Kept as its own entry rather than
56+
folded into `dashboard-inert-keys-removed`, whose identity is the #3896 sweep:
57+
this removal rests on its own 2026-08-03 measurement and should say so in
58+
`spec-changes.json` and the upgrade guide.
59+
- **No liveness row is added**, matching `widgets[].performance` in the #3896
60+
sweep — a widget-level row would be an ORPHAN, not a classification, until
61+
#4956 lands the drill. The ledger `_note` records the removal and why the row
62+
is absent.
63+
- Baselines moved at KEY level only, as the shape's survival implies:
64+
`authorable-surface.json` gains `ui/DashboardWidget:responsive [RETIRED]`;
65+
`json-schema.manifest.json`, `api-surface.json` and
66+
`api-surface-signatures.json` are unchanged by construction — no def stopped
67+
being emitted and no export was removed.
68+
69+
No runtime behaviour changes — that impossibility is the reason for the removal.
70+
71+
**objectui shard:** the outcome is retirement, not the fallback clause, so
72+
objectui#3235's conditional pin-bump item is permanently de-listed; the `any`
73+
declaration on that repo's side can be cleaned on its own schedule.

content/docs/references/ui/dashboard.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Dashboard header action
110110
| **options** | `Record<string, any>` | optional | Widget specific configuration |
111111
| **filterBindings** | `Record<string, string \| 'false'>` | optional | Per-widget dashboard-filter bindings: filter name → this widget's field, or false to opt out |
112112
| **suppressWarnings** | `string[]` | optional | Build diagnostic rule ids suppressed on this widget |
113-
| **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 |
113+
| **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. |
114114
| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
115115

116116

content/docs/ui/dashboards.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ selects `dimensions` (X / group / split) and `values` (the measures to plot):
106106
| `colorVariant` | `enum` | optional | KPI/card accent color |
107107
| `compareTo` | `enum \| object` | optional | Period-over-period comparison window |
108108
| `options` | `object` | optional | Renderer extras **plus** the query keys below |
109-
| `responsive` | `object` | optional | Responsive behavior |
110109

111110
### Widget `options`
112111

docs/protocol-upgrade-guide.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ Finally it removes the script-body capability token 'crypto.hash' (#4391). Four
190190

191191
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.
192192

193+
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.
194+
193195
### Mechanical (applied for you)
194196

195197
| Conversion | Surface | Change | Load window |
@@ -214,6 +216,7 @@ It also removes `connector.rateLimitConfig` and its whole shape (#4911). This on
214216
| `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 |
215217
| `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 |
216218
| `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 |
219+
| `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 |
217220
| `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 |
218221
| `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 |
219222
| `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 |

packages/spec/authorable-surface.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7220,7 +7220,7 @@
72207220
"ui/DashboardWidget:options",
72217221
"ui/DashboardWidget:requiresObject",
72227222
"ui/DashboardWidget:requiresService",
7223-
"ui/DashboardWidget:responsive",
7223+
"ui/DashboardWidget:responsive [RETIRED]",
72247224
"ui/DashboardWidget:suppressWarnings",
72257225
"ui/DashboardWidget:title",
72267226
"ui/DashboardWidget:type",

packages/spec/liveness/dashboard.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"type": "dashboard",
3-
"_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.",
3+
"_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.",
44
"props": {
55
"name": {
66
"status": "live",

packages/spec/spec-changes.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@
188188
"conversionId": "dashboard-inert-keys-removed",
189189
"toMajor": 17
190190
},
191+
{
192+
"surface": "dashboard.widgets[].responsive",
193+
"to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected)",
194+
"conversionId": "dashboard-widget-responsive-removed",
195+
"toMajor": 17
196+
},
191197
{
192198
"surface": "agent.knowledge",
193199
"to": "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)",
@@ -824,6 +830,12 @@
824830
"conversionId": "dashboard-inert-keys-removed",
825831
"toMajor": 17
826832
},
833+
{
834+
"surface": "dashboard.widgets[].responsive",
835+
"to": "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected)",
836+
"conversionId": "dashboard-widget-responsive-removed",
837+
"toMajor": 17
838+
},
827839
{
828840
"surface": "agent.knowledge",
829841
"to": "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)",

packages/spec/src/conversions/registry.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2225,6 +2225,65 @@ const dashboardInertKeysRemoved: MetadataConversion = {
22252225
},
22262226
};
22272227

2228+
/**
2229+
* dashboard.widgets[].responsive (#4876) — the same-named `view.responsive`
2230+
* went in the #3896 close-out above; this one escaped that sweep through a
2231+
* liveness drill gap rather than on evidence (`dashboard.json` declares no
2232+
* `children` on `widgets`, so no widget-level key was ever classified — filed
2233+
* as #4956). Re-measured 2026-08-03: no objectui code reads
2234+
* `widget.responsive`, and zero authored instances exist repo-wide.
2235+
*
2236+
* Deliberately a SEPARATE entry rather than another key on
2237+
* `dashboard-inert-keys-removed`: that entry's identity is the #3896 sweep, and
2238+
* folding a differently-evidenced removal into it would misattribute this one
2239+
* in `spec-changes.json` and the upgrade guide — the two places an upgrading
2240+
* author actually reads. Both are `toMajor: 17`, so a stored dashboard carrying
2241+
* both keys is cleaned by both in one replay.
2242+
*
2243+
* Strips ONLY the widget embed. The shared `ResponsiveConfig` shape is
2244+
* untouched and still live on `page.components[].responsive`.
2245+
*/
2246+
const dashboardWidgetResponsiveRemoved: MetadataConversion = {
2247+
id: 'dashboard-widget-responsive-removed',
2248+
toMajor: 17,
2249+
retiredFromLoadPath: true,
2250+
surface: 'dashboard.widgets[].responsive',
2251+
summary: "dashboard widget key 'responsive' removed (#4876 — no renderer ever applied per-widget breakpoint overrides; page.components[].responsive is unaffected)",
2252+
apply(stack, emit) {
2253+
return mapCollection(stack, 'dashboards', (d, path) => {
2254+
const widgets = d.widgets;
2255+
if (!Array.isArray(widgets)) return d;
2256+
let touched = false;
2257+
const rebuilt = widgets.map((w, i) => {
2258+
if (!w || typeof w !== 'object' || Array.isArray(w)) return w;
2259+
const cleaned = stripKeys(w as Record<string, unknown>, ['responsive'], emit, `${path}.widgets[${i}]`);
2260+
if (cleaned !== w) touched = true;
2261+
return cleaned;
2262+
});
2263+
if (!touched) return d;
2264+
return { ...d, widgets: rebuilt };
2265+
});
2266+
},
2267+
fixture: {
2268+
before: {
2269+
dashboards: [{
2270+
name: 'ops_overview',
2271+
widgets: [{
2272+
id: 'w1', type: 'kpi', dataset: 'orders', values: ['total'],
2273+
responsive: { columns: { xs: 12, lg: 4 }, order: { xs: 2, lg: 1 }, hiddenOn: ['xs'] },
2274+
}],
2275+
}],
2276+
},
2277+
after: {
2278+
dashboards: [{
2279+
name: 'ops_overview',
2280+
widgets: [{ id: 'w1', type: 'kpi', dataset: 'orders', values: ['total'] }],
2281+
}],
2282+
},
2283+
expectedNotices: 1,
2284+
},
2285+
};
2286+
22282287
/**
22292288
* agent.knowledge — a grounding claim nothing enforced (the RAG path reads
22302289
* `sourceIds` from the LLM's tool-call arguments, never the agent record).
@@ -3667,6 +3726,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConv
36673726
flowInertKeysRemoved,
36683727
viewInertKeysRemoved,
36693728
dashboardInertKeysRemoved,
3729+
dashboardWidgetResponsiveRemoved,
36703730
agentKnowledgeRemoved,
36713731
skillTriggerPhrasesRemoved,
36723732
stackApiRequireAuthRemoved,

0 commit comments

Comments
 (0)