Skip to content

Commit 11d28c1

Browse files
feat(spec)!: a dimensionless dashboard widget declares two or more measures only on a type that renders them — refuse pie / donut / funnel / scatter / radar / treemap / sankey at values (objectui#8894 ruling D) (#21053)
Fixes #20958 Clause-②: yes (narrowing) Applies maintainer ruling D's principle (objectui#8894, `5643392537`: 「协议不正确的应该先修改协议」) to the chart types: a dashboard widget with **no dimension** may declare two or more measures only on a type that renders them. The metric-family narrowing (`09e16a574`) was the ruling's first application; this is the same principle on `pie`, `donut`, `funnel`, `scatter`, `radar`, `treemap` and `sankey`. The direction was not re-opened here. ## What changed - **`DASHBOARD_WIDGET_MULTI_MEASURE_TYPES`** (new export, `packages/spec/src/ui/dashboard.zod.ts`) is the one list: `table`, `pivot`, `bar`, `column`, `horizontal-bar`, `line`, `area`, `combo`, typed `as const satisfies readonly ChartType[]`. The check, its refusal text and the `values` doc string all read it. No second literal of the set exists in code (a test pins one `export const` declaration). - **`checkDashboardWidgetDimensionlessMeasureArity`** (new export) is an object-level check chained onto `DashboardWidgetSchema` by identifier, right after `checkDashboardWidgetMetricMeasureArity`. It refuses when `dimensions` is absent or `[]`, `values` has two or more members, and `type` is a declared `ChartTypeSchema` member outside the multi-measure set and outside the metric family. It emits ONE `custom` issue at `values` naming the widget `id`, the count and the type, lists the multi-measure set (read from the constant), and steers to `type: 'table'` (a row of measures), a bar-family type (`bar` / `column` / `horizontal-bar`), or one widget per measure. - **The metric family is excluded on purpose.** `checkDashboardWidgetMetricMeasureArity` already refuses a second measure on `metric` / `kpi` / `gauge` / `solid-gauge` / `bullet` (and a typeless widget, which defaults to `metric`) at any dimensionality. Firing here too would turn that existing refusal from one issue into two. So every type outside the multi-measure set is refused dimensionless by exactly one of the two checks, and the metric refusal is byte-identical (pinned: same message as the metric export's direct call, one issue). - **A type the spec does not declare is not judged by the export.** Through this door it is unreachable (zod's `invalid_value` on `type` aborts object-level checks). objectui's mirror, however, re-points `type` at a wider enum (`list`, `custom`, component widget types) with no default, so the export returns on anything outside `ChartTypeSchema.options`. It refuses exactly what the spec's door refuses. - **ADR-0087 semantic entry** `packages/spec/src/migrations/entries/semantic/18.dashboard-widget-dimensionless-multi-measure-refused.ts`, plus the `gen:migration-registry` lap. Nothing was hand-edited between the markers. Its acceptance criteria name the doors as measured (below), state that the TODO cannot name per-document measures, and list what is and is not refused. - **Changeset** `.changeset/20958-dimensionless-multi-measure-refused.md`: `minor`, **BREAKING** banner, the ADR-0087 marker `registered dashboard-widget-dimensionless-multi-measure-refused`, the `Clause-②` line, and a FROM → TO table. - **Regenerated:** `api-surface/ui.json` and `export-origins/ui.json` (+2 exports each, 0 removed), and the `values` row of `content/docs/references/ui/dashboard.mdx`. ## Measured before building (the dispatch's three hypotheses) **Premise at the base `05be35259`** (built `dist`, `DashboardWidgetSchema.safeParse`): a widget with no dimensions and `values: ['a', 'b']` returned ACCEPT on every one of the seven types. The metric family returned REFUSE `values:custom` at every dimensionality. `bogusProp` on the same widget returned REFUSE `unrecognized_keys`, which is the lit control. **H1, attachment points.** In this repo the metric check is attached at exactly one place: the `.superRefine` chain of `DashboardWidgetSchema`. `DashboardSchema.widgets`, `ObjectStackDefinitionSchema.dashboards` and the `dashboard` metadata-type schema all reach it through that schema (`git grep` over `packages/**`: no other construction of the widget schema, no `.shape` mirror in this repo). The new check is chained at the same point. The "editor face" the precedent entry names is objectui's `.shape` mirror, which lives outside this repo (see Downstream). **H2, every door by the schema alone.** The subject is a dimensionless two-measure widget. The controls are the same body as `table` / `bar`, and a `pie` carrying one dimension. | door | subject (pie / donut / sankey / radar) | controls | |---|---|---| | `DashboardSchema`, `getMetadataTypeSchema('dashboard')` | REFUSE `widgets.0.values:custom` | ACCEPT | | `ObjectStackDefinitionSchema` | REFUSE `dashboards.0.widgets.0.values:custom` | ACCEPT | | `defineStack` | throws `defineStack validation failed (1 issue)`, naming the widget | returns | | `os validate` (a config built by `defineStack`) | exit 1 at the same `defineStack` issue | get past the schema step; exit 1 later on an unrelated docs-namespace rule of the probe app | | metadata save path (`saveMetaItem`, ACTIVE and DRAFT) | `422 INVALID_METADATA` at `widgets.0.values`, 0 rows persisted | ACTIVE save succeeds, 1 row | No consumer-side copy exists. The probes were one-shot: a scratch config and a scratch test file, both deleted, and `git status` was clean afterwards. **H3, fixtures and stored rows.** I ran a census at `05be35259` over every tracked `.ts` / `.tsx` / `.js` / `.json` / `.md` / `.mdx`. 469 brace-local literals carry `values: [...]`. 23 of them are on one of the seven types, and every one has one dimension and one measure. That leaves 0 dimensionless multi-measure widgets on the seven. Lit control: the same scan classifies 40 dimensionless multi-measure literals of other kinds (reports, metric-family test fixtures, dataset selections). An objectui checkout at `1263e40` gives 8 seven-type literals and 0 dimensionless multi-measure. No fixture needed triage, and the stop condition did not fire. ## Verification All of the following ran at `e1494c720`, after merging `origin/main` `9b0de7de7` through `scripts/pm/os-regen-merge.sh`. On that merge, `api-surface/ui.json` and `export-origins/ui.json` were regenerated from the merged tree and now hold main's new exports plus this branch's two. **Reverse verification** ran at the committed `1a0e75a6d` through `scripts/ablation-replace.mjs`. It deleted only the `.superRefine(checkDashboardWidgetDimensionlessMeasureArity)` attachment, so the function stayed declared. The subject is imported from `src` through a relative path, so no `dist` was involved. ``` anchor x1 -> x0 · marker x0 -> x1 · blob b33ae8c -> 209e3d201d54 (mutation landed) RED vitest exit=1 27 failed | 274 passed (301) restored blob b33ae8c == HEAD blob, git diff HEAD empty GREEN full spec suite at 1a0e75a after the restore: 584 files, 17284 passed, exit 0 ``` The 27 red tests: the 7 refusals, the 7 explicit-`[]` refusals, the message, travel and attachment pins, and in the exports catalogue 8 parity rows plus the count and bijection legs. A first attempt with the replacement `);` was refused by the tool itself, because that string is a substring of the anchor and its count could not rise. The mutation was restored, and the attempt is reported here rather than counted. **Tests** (each is the package's own vitest run against the rebuilt closure; the filter direction is per package, not the full `...@objectstack/spec` sweep): | package | result at `e1494c720` | |---|---| | `@objectstack/spec` (`--project local`) | 584 files, 17290 passed, 1 todo, exit 0 | | `@objectstack/spec` typecheck (tsc + scripts + test layer) | exit 0; test-layer ledger held at 52 files, 248 errors, 136 signatures | | `@objectstack/metadata-protocol` | 195 passed, 3 skipped (198 files); 2896 tests passed, 19 skipped; exit 0 | | `@objectstack/lint` | 117 files, 5446 tests, exit 0 | | `@objectstack/service-analytics` | 150 files, 3458 tests, exit 0 | | `@objectstack/sdui-parser` | 13 files, 224 tests, exit 0 | | `@objectstack/platform-objects` | 59 files, 948 tests, exit 0 | | `@objectstack/objectql` (`--project local`) | 348 files, 6821 tests, exit 0 | | `@objectstack/metadata` | 56 files, 836 tests, exit 0 | | `@objectstack/cli` (`--project unit`; the integration tier is declared to CI because no spawn entry or integration file is touched) | 239 files, 3391 tests, exit 0 | Before the merge, the spec suite and the metadata-protocol, lint, sdui-parser and platform-objects suites were also green at `1a0e75a6d`. The 3 skipped metadata-protocol files were skipped on both runs. **Generated artefacts:** `pnpm --filter @objectstack/spec check:generated` reports "All 15 generated artifacts are up to date". `check:api-surface` reports "public API surface + factory signatures unchanged" after regeneration. **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `e1494c720` derives 113 commands, and all 113 ran with exit 0. `--ran` reconciliation: "113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero, all 113 recorded an exit code and none of them is 3)". Three of them first answered something other than a verdict, and each was re-run to a real exit 0: - `check:skill-examples` (exit 3, `client-react` not built) - `check:dual-build-cjs-loads` (exit 3, eight packages had no `dist`) - `check:query-options-erasure` (killed by my own 240-second per-command cap; 278 s uncapped) The list includes `check-adr-0087-registration --base origin/main`, which reads `[BREAKING+bang+clause-②-narrowing] registered dashboard-widget-dimensionless-multi-measure-refused`. It also includes `check-changeset-no-major`, `check-empty-changeset`, `check:nul-bytes`, `check:pm-widening-tells`, `check:spec-docblock-symbol-anchors`, `check:issue-citations`, `check:doc-authoring` and `check:widget-option-census`. **Lint, a measured narrowing rather than the repo-wide run** (that run belongs to CI): - I ran `eslint --no-inline-config --format json` over the 5 touched `.ts` files. The JSON reports 5 files, 0 errors and 0 warnings, at `e1494c720`. - Population: `eslint --print-config` returns a rule set for each touched file, so none is ignored. - Invariance: `eslint.config.mjs` enables no type-aware linting. `parserOptions.project` and `projectService` are null for these files, and the config's own comment states it never enables them. This diff therefore cannot move a verdict on any untouched file. ## Acceptance notes None of these is filed. None is a reproducible defect, a contract violation or an authoring trap. - **Whether the seven types render several measures WITH a dimension** is unmeasured here and outside the card. The rule refuses only the dimensionless shape; the entry and the docblock say so rather than imply it. Carrier: none. - **No `STEP18_RATIONALE` fragment** was added. This follows the precedent (`09e16a574` added none). The step currently holds 52 fragments against 254 major-18 semantic entries (this one included), and fragments accompany retirements that carry conversions. The dispatch's hot-file note describes the fragment mechanism; nobody else's fragment was touched. - **Two new exports, not one.** The card names one exported constant. The check itself is exported too, because `object-refinement-check-exports.test.ts` pins one export per `custom` check the schema carries, and objectui's mirror chains the check by name. `Clause-②: yes (narrowing)` covers both. - **`scripts/ablation-replace.mjs`** refuses a replacement that is a substring of its anchor, because the replacement count cannot rise. This is conservative and correct; the remedy is a distinctive marker. Carrier: none. ## Downstream: objectui's mirror (not in this PR) `@object-ui/types` builds `DashboardWidgetSchema` from a `.shape` spread, so it runs only the checks it chains. Until it chains this one, its editor keeps accepting a dimensionless two-measure `pie`, and the author meets the refusal at publish. The entry says so. For the coordination child: - Import `checkDashboardWidgetDimensionlessMeasureArity` (and, for any UI that lists the set, `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES`) from `@objectstack/spec/ui`. - Chain it in `packages/types/src/zod/complex.zod.ts` after the existing `.superRefine(checkDashboardWidgetMetricMeasureArity)`. - Add it to the `attached` list of the `DashboardWidgetSchema (complex.zod.ts)` row in `packages/types/src/__tests__/spec-object-refinements-7715.test.ts`. That census reads the spec object's check count, so it reddens by row name at the spec bump until this lands. - Refusal shape: ONE issue with `code: 'custom'` and `path: ['values']`. The message starts "Widget `ID` declares N measures with no `dimensions` on `type: 'T'`", where ID is the widget id, N is the count and T is the authored type. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a9d36d5 commit 11d28c1

9 files changed

Lines changed: 645 additions & 8 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: a dashboard widget with no dimension declares two or more measures only on a type that renders them — `pie` / `donut` / `funnel` / `scatter` / `radar` / `treemap` / `sankey` are refused at `values` (#20958; objectui#8894 ruling D's principle)
6+
7+
Clause-②: yes (narrowing) — the accept set NARROWS (that is the change), and the published surface GAINS two exports: the one constant the rule reads, `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES`, and the check itself, `checkDashboardWidgetDimensionlessMeasureArity`, exported so objectui's `.shape` mirror can chain it.
8+
9+
<!-- adr-0087: registered dashboard-widget-dimensionless-multi-measure-refused -->
10+
11+
**BREAKING** accept-set narrowing at `dashboard.widgets[].values`, shipped as
12+
`minor` under this repo's launch-window convention for breaking changes
13+
(`check-changeset-no-major` refuses `major` while the window is open, so
14+
breaking-ness is carried by this banner and by the ADR-0087 disposition above,
15+
never by the bump level). The prescription is registered under protocol major 18
16+
as `dashboard-widget-dimensionless-multi-measure-refused`.
17+
18+
**What was wrong.** Outside the metric family, `DashboardWidgetSchema.values`
19+
(`z.array(z.string()).min(1)`) had no upper bound. Measured on this tree before
20+
the change: `{ type: 'pie', dataset: 'sales', values: ['a', 'b'] }` with no
21+
`dimensions` parsed through `DashboardWidgetSchema`, and so did `donut`,
22+
`funnel`, `scatter`, `radar`, `treemap` and `sankey` — while `bogusProp` on the
23+
same widget was refused by name, the lit control. After it, the same body is
24+
refused at `defineStack`, at `os validate` (which loads through `defineStack`),
25+
and on the metadata save path (`422 INVALID_METADATA`, active and draft). With
26+
nothing to split by, those seven types draw `values[0]`: every measure after it
27+
is queried and dropped on the floor by the renderer. The maintainer's ruling D
28+
(「协议不正确的应该先修改协议」) fixes the protocol where it admits measures a
29+
widget type cannot render; the metric-family narrowing was its first
30+
application, and this is the same principle on the chart types.
31+
32+
### Write instead
33+
34+
| wrote | write instead |
35+
|---|---|
36+
| `{ id: 'mix', type: 'pie', dataset: 'sales', values: ['amount_sum', 'count'] }` (no `dimensions`) | `{ id: 'mix', type: 'table', dataset: 'sales', values: ['amount_sum', 'count'] }` — a row of measures |
37+
| the same, wanting a chart | `type: 'bar'` (or `column` / `horizontal-bar`) — one bar per measure |
38+
| the same, wanting the pie | `{ id: 'mix', type: 'pie', …, values: ['amount_sum'] }` **and** `{ id: 'mix_count', type: 'pie', …, values: ['count'] }` — one widget per measure, each with its own `id` (and `layout`, if you pin positions) |
39+
40+
No conversion does this for you: whether a dimensionless two-measure pie meant a
41+
table, a bar chart or two pies is an authoring choice. The refusal lands at
42+
`widgets[N].values` as ONE `custom` issue naming the widget's `id`, the number
43+
of measures and the authored `type`, and it lists the types that do render
44+
several measures on a dimensionless widget, read from
45+
`DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` (`table`, `pivot`, `bar`, `column`,
46+
`horizontal-bar`, `line`, `area`, `combo`). That constant is the one list — the
47+
check, the refusal text and the `values` doc string read it, and objectui's
48+
mirror is to import it rather than restate it. A type that later gains a
49+
declared multi-measure rendering joins it with no migration.
50+
51+
**Nothing else moves.** The seven types WITH a dimension, and with one measure,
52+
parse exactly as before; every type in the multi-measure set keeps accepting
53+
any number of measures with no dimension; the metric family's refusal is
54+
unchanged and still ONE issue (this check steps aside for `metric` / `kpi` /
55+
`gauge` / `solid-gauge` / `bullet` and for a typeless widget, which resolves to
56+
`metric`); an empty `values` keeps its `too_small`; a `type` outside
57+
`ChartTypeSchema` reports the type refusal alone. Census at the branch point
58+
(`05be35259`), every tracked `.ts` / `.tsx` / `.js` / `.json` / `.md` / `.mdx`:
59+
23 widget literals on the seven types, every one with one dimension and one
60+
measure, and 0 dimensionless multi-measure widgets on them; the same scan over
61+
an objectui checkout (`1263e40`) reads 0 as well.

‎content/docs/references/ui/dashboard.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const result = DashboardSchema.parse(data);
7777
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`) |
7878
| **dataset** | `string` | ✅ | Dataset name to bind (ADR-0021) |
7979
| **dimensions** | `string[]` | optional | Dimension names — X/group/split |
80-
| **values** | `string[]` | ✅ | Measure names — Y (at least one; exactly one on the metric/kpi/gauge/solid-gauge/bullet family) |
80+
| **values** | `string[]` | ✅ | Measure names — Y (at least one; exactly one on the metric/kpi/gauge/solid-gauge/bullet family; with no dimensions, two or more only on table/pivot/bar/column/horizontal-bar/line/area/combo) |
8181
| **layout** | `{ x: number; y: number; w: number; h: number }` | optional | Grid layout position (auto-flowed when omitted) |
8282
| **options** | `{ dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; sortBy?: string; sortOrder?: Enum<'asc' \| 'desc'>; limit?: integer; … } & Record<string, any>` | optional | Widget specific configuration |
8383
| **filterBindings** | `Record<string, string \| false>` | optional | Per-widget dashboard-filter bindings: filter name → this widget's field, or false to opt out |
@@ -182,7 +182,7 @@ Dashboard header action
182182
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`) |
183183
| **dataset** | `string` | ✅ | Dataset name to bind (ADR-0021) |
184184
| **dimensions** | `string[]` | optional | Dimension names — X/group/split |
185-
| **values** | `string[]` | ✅ | Measure names — Y (at least one; exactly one on the metric/kpi/gauge/solid-gauge/bullet family) |
185+
| **values** | `string[]` | ✅ | Measure names — Y (at least one; exactly one on the metric/kpi/gauge/solid-gauge/bullet family; with no dimensions, two or more only on table/pivot/bar/column/horizontal-bar/line/area/combo) |
186186
| **layout** | `{ x: number; y: number; w: number; h: number }` | optional | Grid layout position (auto-flowed when omitted) |
187187
| **options** | `{ dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; sortBy?: string; sortOrder?: Enum<'asc' \| 'desc'>; limit?: integer; … } & Record<string, any>` | optional | Widget specific configuration |
188188
| **filterBindings** | `Record<string, string \| false>` | optional | Per-widget dashboard-filter bindings: filter name → this widget's field, or false to opt out |

‎packages/spec/api-surface/ui.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
"ComponentProps (type)",
118118
"ComponentPropsInput (type)",
119119
"ComponentPropsMap (const)",
120+
"DASHBOARD_WIDGET_MULTI_MEASURE_TYPES (const)",
120121
"DATE_RANGE_DEFAULT_RANGES (const)",
121122
"DATE_RANGE_PRESETS (const)",
122123
"Dashboard (const)",
@@ -476,6 +477,7 @@
476477
"chartAggregateCategoryKey (function)",
477478
"chartAggregateResultKeys (function)",
478479
"chartAggregateValueKey (function)",
480+
"checkDashboardWidgetDimensionlessMeasureArity (function)",
479481
"checkDashboardWidgetMetricMeasureArity (function)",
480482
"checkDashboardWidgetStageOrder (function)",
481483
"checkGlobalFilterDateDefaultValue (function)",

‎packages/spec/export-origins/ui.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"ComponentProps": "src/ui/component.zod.ts#ComponentProps (type)",
115115
"ComponentPropsInput": "src/ui/component.zod.ts#ComponentPropsInput (type)",
116116
"ComponentPropsMap": "src/ui/component.zod.ts#ComponentPropsMap (const)",
117+
"DASHBOARD_WIDGET_MULTI_MEASURE_TYPES": "src/ui/dashboard.zod.ts#DASHBOARD_WIDGET_MULTI_MEASURE_TYPES (const)",
117118
"DATE_RANGE_DEFAULT_RANGES": "src/ui/dashboard.zod.ts#DATE_RANGE_DEFAULT_RANGES (const)",
118119
"DATE_RANGE_PRESETS": "src/data/date-range-presets.ts#DATE_RANGE_PRESETS (const)",
119120
"Dashboard": "src/ui/dashboard.zod.ts#Dashboard (type)",
@@ -462,6 +463,7 @@
462463
"chartAggregateCategoryKey": "src/ui/chart-aggregate.ts#chartAggregateCategoryKey (function)",
463464
"chartAggregateResultKeys": "src/ui/chart-aggregate.ts#chartAggregateResultKeys (function)",
464465
"chartAggregateValueKey": "src/ui/chart-aggregate.ts#chartAggregateValueKey (function)",
466+
"checkDashboardWidgetDimensionlessMeasureArity": "src/ui/dashboard.zod.ts#checkDashboardWidgetDimensionlessMeasureArity (function)",
465467
"checkDashboardWidgetMetricMeasureArity": "src/ui/dashboard.zod.ts#checkDashboardWidgetMetricMeasureArity (function)",
466468
"checkDashboardWidgetStageOrder": "src/ui/dashboard.zod.ts#checkDashboardWidgetStageOrder (function)",
467469
"checkGlobalFilterDateDefaultValue": "src/ui/dashboard.zod.ts#checkGlobalFilterDateDefaultValue (function)",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
export const entry: SemanticMigration = {
6+
id: 'dashboard-widget-dimensionless-multi-measure-refused',
7+
surface: 'dashboard widget measure arity WITHOUT a dimension — `dashboard.widgets[].values` '
8+
+ '(`DashboardWidgetSchema.values`) on a widget whose `dimensions` is absent or empty and whose '
9+
+ '`type` is one of the seven chart types that declare no rendering for several measures: '
10+
+ '`pie` / `donut` / `funnel` / `scatter` / `radar` / `treemap` / `sankey`',
11+
replacement: 'Pick a visual that renders several measures, or split the widget. With no '
12+
+ 'dimension, `type: \'table\'` renders a row of measures and a bar-family type (`bar` / '
13+
+ '`column` / `horizontal-bar`) renders one bar per measure; both keep the unbounded `values` '
14+
+ 'they have always had. The full set of types that render several measures on a dimensionless '
15+
+ 'widget is the exported constant `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` — at this release '
16+
+ '`table`, `pivot`, `bar`, `column`, `horizontal-bar`, `line`, `area` and `combo` — and the '
17+
+ 'refusal prints it from that constant. Or keep the type and give each measure its OWN widget: a '
18+
+ 'new `id`, the same `dataset`, that one measure in `values`, and its own `layout` if the '
19+
+ 'dashboard pins grid positions. ⛔ The migration does not do this for you and no conversion '
20+
+ 'could: whether a dimensionless two-measure pie meant a table, a bar chart or two pies is an '
21+
+ 'authoring choice, and N widgets need N ids and N boxes on a 12-column grid, which is a LAYOUT '
22+
+ 'decision about a dashboard the registry has never seen.',
23+
reason:
24+
'Maintainer ruling D, on objectui\'s finding that a widget silently drops every measure after '
25+
+ 'the first, applying the maintainer\'s standing rule 「协议不正确的应该先修改协议」 — the '
26+
+ 'protocol is fixed where it admits measures a widget type cannot render. Its first application '
27+
+ 'bounded the metric FAMILY to one measure (`dashboard-widget-metric-family-multi-measure-refused`); '
28+
+ 'this entry applies the same principle to the chart types. Measured in objectui by the dev who '
29+
+ 'delivered the multi-measure renderings for `table` / `pivot` and the bar, line, area and combo '
30+
+ 'families: the other seven `ChartTypeSchema` members, given no dimension and two or more '
31+
+ 'measures, render `values[0]` and drop the rest — the dataset query selects and computes every '
32+
+ 'measure, and all but the first are thrown away. Every door accepted the document, because '
33+
+ '`values` is `z.array(z.string()).min(1)` with no upper bound outside the metric family. That '
34+
+ 'is the declared≠delivered shape ADR-0049 exists to end. The census before the change found '
35+
+ 'zero authored dimensionless multi-measure widgets of any type in the platform\'s examples or in '
36+
+ 'objectui\'s example apps, so this ships at once with no deprecation window: there is no window '
37+
+ 'in which a queried-and-discarded measure does anything. Relaxing later is free and needs no '
38+
+ 'second migration — if one of the seven gains a declared multi-measure rendering (a radar of '
39+
+ 'measures, a funnel of measure stages) it joins the constant, while leaving the key unbounded '
40+
+ 'costs an author a widget that silently drops what they declared. ⛔ This change does not '
41+
+ 'invent those renderings.',
42+
acceptanceCriteria:
43+
'⚠️ WHICH DOOR: the refusal is the spec\'s, and it reaches every door that parses the spec '
44+
+ 'schema — measured on `defineStack`, which throws naming the widget; on `os validate`, which '
45+
+ 'loads the configuration through `defineStack` and fails there with that same issue; on the '
46+
+ 'stack schema and the `dashboard` metadata-type schema; and on the metadata save path, where '
47+
+ 'an ACTIVE save and a DRAFT save of such a dashboard both answer `422 INVALID_METADATA` at '
48+
+ '`widgets[N].values` and persist nothing. It is NOT refused by objectui\'s client-side '
49+
+ 'authoring door until that door chains the new export: `@object-ui/types` builds its '
50+
+ '`DashboardWidgetSchema` from a `.shape` spread of the spec\'s, which carries the FIELDS and '
51+
+ 'drops every object-level check, so its editor keeps accepting a dimensionless two-measure '
52+
+ '`pie` and the author meets the refusal at publish. ⇒ Do not read a green editor as a clean '
53+
+ 'dashboard; re-parse through the spec. '
54+
+ '⚠️ AND THE TODO CANNOT NAME YOUR MEASURES: a `SemanticMigration` is static prose emitted once '
55+
+ 'per hop, with no per-document interpolation and no filtering by whether the stack carries the '
56+
+ 'shape, so `os migrate meta` prints THIS paragraph, not a list of your widgets. The refusal is '
57+
+ 'what names them, per widget, on the re-parse — drive the fix off `os validate`, not off the '
58+
+ 'migrate output. '
59+
+ 'WHAT IS REFUSED, exactly: ONE `custom` issue at `widgets[N].values`, naming the widget\'s '
60+
+ '`id`, the number of measures and the authored `type`, when `dimensions` is absent or an empty '
61+
+ 'array, `values` carries two or more measures, and `type` is `pie`, `donut`, `funnel`, '
62+
+ '`scatter`, `radar`, `treemap` or `sankey`. The check is the exported '
63+
+ '`checkDashboardWidgetDimensionlessMeasureArity`, and the set it reads is the exported '
64+
+ '`DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` — one list, which the check, the refusal text and the '
65+
+ '`values` doc string all read. '
66+
+ 'WHAT IS NOT, so this is not read as complete: the same seven types WITH a dimension keep '
67+
+ 'accepting several measures exactly as before (whether that shape renders them all is a '
68+
+ 'separate question this entry does not answer); one measure parses on every type; every type in '
69+
+ '`DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` keeps accepting any number of measures with no '
70+
+ 'dimension; the metric family (`metric` / `kpi` / `gauge` / `solid-gauge` / `bullet`, and a '
71+
+ 'widget that declares no `type`, which resolves to `metric`) keeps its OWN refusal, unchanged '
72+
+ 'and still ONE issue — that check refuses a second measure at any dimensionality, and this one '
73+
+ 'steps aside for the family rather than adding a second issue on the same `values`; an EMPTY '
74+
+ '`values` keeps the field\'s own `too_small`; a `type` outside `ChartTypeSchema` reports the '
75+
+ 'TYPE refusal alone (zod treats that `invalid_value` as aborting and skips object-level checks), '
76+
+ 'and called directly on a wider type enum the export judges only the types the spec declares; '
77+
+ 'and whether each measure EXISTS in the bound dataset is still unreachable from this schema. '
78+
+ 'VERIFY by re-parsing each dashboard: a dashboard that had one dimensionless two-measure `pie` '
79+
+ 'should end with a `table` or bar-family widget carrying both measures, or with two widgets of '
80+
+ 'one measure each — check the rendered grid afterwards, because the second measure is a number '
81+
+ 'the dashboard was ALREADY paying to compute and had never shown.',
82+
};

0 commit comments

Comments
 (0)