Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .changeset/dashboard-widget-responsive-removed.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion content/docs/references/ui/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Dashboard header action
| **options** | `Record<string, any>` | optional | Widget specific configuration |
| **filterBindings** | `Record<string, string \| 'false'>` | 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 |


Expand Down
1 change: 0 additions & 1 deletion content/docs/ui/dashboards.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
3 changes: 3 additions & 0 deletions docs/protocol-upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/authorable-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -7220,7 +7220,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",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/liveness/dashboard.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
12 changes: 12 additions & 0 deletions packages/spec/spec-changes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -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)",
Expand Down
60 changes: 60 additions & 0 deletions packages/spec/src/conversions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>, ['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).
Expand Down Expand Up @@ -3667,6 +3726,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConv
flowInertKeysRemoved,
viewInertKeysRemoved,
dashboardInertKeysRemoved,
dashboardWidgetResponsiveRemoved,
agentKnowledgeRemoved,
skillTriggerPhrasesRemoved,
stackApiRequireAuthRemoved,
Expand Down
Loading
Loading