Commit 11d28c1
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
- .changeset
- content/docs/references/ui
- packages/spec
- api-surface
- export-origins
- src
- migrations
- entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
| 80 | + | |
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| |||
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
185 | | - | |
| 185 | + | |
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
| 120 | + | |
120 | 121 | | |
121 | 122 | | |
122 | 123 | | |
| |||
476 | 477 | | |
477 | 478 | | |
478 | 479 | | |
| 480 | + | |
479 | 481 | | |
480 | 482 | | |
481 | 483 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
| 117 | + | |
117 | 118 | | |
118 | 119 | | |
119 | 120 | | |
| |||
462 | 463 | | |
463 | 464 | | |
464 | 465 | | |
| 466 | + | |
465 | 467 | | |
466 | 468 | | |
467 | 469 | | |
| |||
Lines changed: 82 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
0 commit comments