feat(spec)!: a pie / donut / funnel / treemap / sankey widget takes one measure with a dimension too — refuse two or more at values, and rename the check to checkDashboardWidgetChartMeasureArity (#21293) - #21425
Conversation
…a dimension too (WIP) Extends the dimensionless measure-arity rule to the dimensioned arm for pie / donut / funnel / treemap / sankey, and renames the check export to checkDashboardWidgetChartMeasureArity. Generated artefacts follow. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…rigins and the dashboard reference Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…ngle-series-measure-arity
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cbad8adc60ff74b4c794971a08e21735217af3a3 && git checkout cbad8adc60ff74b4c794971a08e21735217af3a3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 39a912ea73ddff7fc85ebb3379a8ce74ff1343f5 4435664b26cb01f1d1d4b6c9424dc6e799bbf2fa && git checkout -B drift-repro 39a912ea73ddff7fc85ebb3379a8ce74ff1343f5 && git merge --no-ff 4435664b26cb01f1d1d4b6c9424dc6e799bbf2fa
node scripts/docs-audit/affected-docs.mjs --json 39a912ea73ddff7fc85ebb3379a8ce74ff1343f5
|
Contract reviewServed-tier: Isolated contract review of PR #21425 for card #21293, branch ① Derived judgments
② Semver level
③ Boundary flags
Check-runs on the head, read over REST at 2026-10-02T14:56Z after the last shard concluded: 35 check-runs, 33 Implemented-by: VERDICT: PASS |
…ngle-series-measure-arity
|
Regen-provenance: 5955132369 ·
|
Fixes #21293
Clause-②: yes (narrowing)
Extends #20958's per-type measure-arity rule to the dimensioned arm, as triage ruled on objectstack-ai/objectui#11417 (comment
5943580519, carried onto this card as5944795015): forpie,donut,funnel,treemapandsankey, two or more measures are refused whatever the dimension. It uses #20958's mechanism and message shape: the same check, one more arm, and one ADR-0087 semantic entry in #20958's shape. No second rule is added.scatterandradarwith a dimension are outside the ruling and parse as before.What changed
packages/spec/src/ui/dashboard.zod.tsSINGLE_SERIES_CHART_TYPES(:784):pie/donut/funnel/treemap/sankey, typedas const satisfies readonly ChartType[]. It is private, like the metric family's list. The check, its refusal text and thevaluesdoc string read it.dimensionsis declared and non-empty,valueshas two or more members, andtypeis one of the five, it emits ONEcustomissue atvalues. The issue names the widgetid, the count and the type, and says the type "draws ONE series whatever itsdimensions". It steers totype: 'table'(a column per measure), a bar-family type (one bar per measure in each category), or one widget per measure.dimensions, so a dimensionlesspiestill gets the dimensionless message, and only one issue.ChartTypeSchema.checkDashboardWidgetDimensionlessMeasureArityis nowcheckDashboardWidgetChartMeasureArity(:878, chained at:1461). The reason is in "The rename" below.valuesdoc string now states the single-series rule. It reads the private constant.packages/spec/src/migrations/entries/semantic/18.dashboard-widget-single-series-multi-measure-refused.ts, plus thegen:migration-registrylap. Nothing between the markers was hand-edited. 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. It also records the export rename (FROM → TO).18.dashboard-widget-dimensionless-multi-measure-refused.ts). It named the old export, and it said "the same seven types WITH a dimension keep accepting several measures exactly as before". This change makes both false, andos migrate metaprints both entries in the same hop. They now name the new export (noting the old name) and point the five at this entry. This file is outside the claim's listed file surface; see Deviations.dashboard.test.ts: a new[#21293]block, plus the fixture triage below.object-refinement-check-exports.test.ts: the parity catalogue row is renamed, and its fixtures are triaged..changeset/21293-single-series-multi-measure-refused.md:minor, a BREAKING banner, the(narrowing)arm, and exactly one ADR-0087 marker (registered dashboard-widget-single-series-multi-measure-refused). It carries a FROM → TO table that includes the export rename.api-surface/ui.jsonandexport-origins/ui.json: one row renamed in each,-1 / +1.valuesrow ofcontent/docs/references/ui/dashboard.mdx.src/migrations/registry.ts.The rename, and what objectui's mirror chains today
The check now judges widgets WITH a dimension, so
...DimensionlessMeasureAritynames a boundary that no longer exists. A mirror deciding which export to chain reasons from that name first. This is the cheapest moment there will ever be to change it, because nothing chains it yet:.objectui-shapin89cad75d55702cc4f267bead5bf267de575d5842,git grep "DimensionlessMeasureArity\|MULTI_MEASURE_TYPES"returns 0 hits. The control,checkDashboardWidgetMetricMeasureArityinpackages/types/src/zod/complex.zod.ts, returns 2. objectui'sDashboardWidgetSchemamirror chainscheckDashboardWidgetStageOrderandcheckDashboardWidgetMetricMeasureArityonly (complex.zod.ts:1304, and theattachedrow inspec-object-refinements-7715.test.ts:135).origin/main5988b6b53the readings are the same: 0 hits, control 2.packages/spec's own tests (git grep).A rename is a removal plus an addition, so the changeset carries the import FROM → TO, and
Clause-②isyes.Measured before building (the dispatch's four hypotheses)
The branch base is
4b20c84748. It is newer than the dispatch's1d0600bf66, anddashboard.zod.tsdid not change between the two.dimensions(old:834), andDASHBOARD_WIDGET_MULTI_MEASURE_TYPESlisted the eight multi-measure types. At the base I builtdistand ranDashboardWidgetSchema.safeParseon{ id, type, dataset, dimensions: ['stage'], values: ['revenue', 'cost'] }. It returned ACCEPT forpie,donut,funnel,treemap,sankey,scatter,radar,barandtable. One measure returned ACCEPT on all of them. The lit control, the same body plusbogusProp, returned REFUSEunrecognized_keyson every type.packages/plugin-charts/src/AdvancedChartImpl.tsx) binds the first series on exactly these arms:pie/donutat:2095(const pieDataKey = series[0]?.dataKey || 'value');funnelat:2161,treemapat:2243andsankeyat:2278(const dataKey = series[0]?.dataKey || 'value').radarmaps every series (:2406onward,series.map(...)).DatasetWidget.tsxat the pin already names the same five asSINGLE_SERIES_CHART_FAMILIES(:387–:413), which is the feat(spec): ToolExecutionContext.surfaceContext — what the user is currently discussing (cloud#1610) #11417 diagnostic. It saysscatter"refuses a second series out loud".build-migration-registry.tsderives the order by sorting entry ids ("Order is DERIVED, never declared"). The serial constraint did hold. PR fix(analytics)!: a query window outside the non-negative integers is refused at the door, and an offset with no limit runs on SQLite #21399 (analytics-query-window-non-negative-integer) landed onmainfirst, so this branch mergedorigin/maindb3fee3dc9throughscripts/pm/os-regen-merge.sh(merge commitb3b3c516f9).gen:migration-registryon the merged tree rewrote it byte-identical: 346 semantic entries,git statusempty.git grep, HEAD vsorigin/main:analytics-query-window-non-negative-integer1/1,dashboard-widget-dimensionless-multi-measure-refused1/1, anddashboard-widget-single-series-multi-measure-refused1 at HEAD, 0 on main.values: [...]over every tracked.ts/.tsx/.js/.mjs/.cjs/.json/.md/.mdx/.ymlfile.4b20c84748: 496 literals, 33 on the seven types, 1 dimensioned multi-measure on the five. That one ispackages/spec/src/ui/object-refinement-check-exports.test.ts:365, the fixture that pinned the old acceptance, triaged below.table/combowidgets inexamples/app-showcase, and others)..changeset/11417-dashboard-single-series-dropped-measure.md:10, not metadata.examples/**, the showcase and CRM apps, andpackages/platform-objectsdashboards carry zero instances.lintvalidate-widget-bindings,runtime-gate,validate-dashboard-widget-options, andmetadata-protocol's two donut fixtures). All carry one measure.Doors after the change (built
dist, one-shot probe, deleted afterwards,git statusclean)dimensions: ['stage'])DashboardSchemagetMetadataTypeSchema('dashboard')ObjectStackDefinitionSchemadefineStackpie, 2 measureswidgets.0.values:customwidgets.0.values:customdashboards.0.widgets.0.values:customdefineStack validation failed (1 issue), naming the widgetsankey, 2 measurespie, 1 measure (control)radar, 2 measures (control)bar, 2 measures (control)I did not re-measure
os validateor the metadata save path. The entry does not claim them. They parse through the same schema at the same attachment point as #20958, which measured both.Fixture triage (pins whose premise this change makes false)
object-refinement-check-exports.test.ts. The row "two measures on apieWITH a dimension — out of this rule" (refusesAt: []) is this arm's subject now. It becomes five refusing rows, one per single-series type. Accepting rows are added forscatter/radarwith a dimension and for a one-measure dimensionedpie.dashboard.test.ts, the spec(ui): refuse two or more measures on a dimensionless dashboard widget whose type declares no multi-measure rendering (pie, donut, funnel, scatter, radar, treemap, sankey): ruling D's principle, objectui#8894 Q1 #20958 block. "accepts%swith ONE dimension and two measures" iterated all seven types. It keepsscatterandradar, and the five move to the new block as refusals.dashboard.test.ts, the spec: a metric-family dashboard widget (metric/kpi/gauge/solid-gauge/bullet) declares exactly one measure — narrowDashboardWidgetSchema.valuesfor the tile family (objectui#8894 ruling D) #17779 block. "leaves%s— every NON-metric type — accepting three measures" ran onWIDGET_BASE, which carries a dimension. Its own claim is that the METRIC check leaves non-metric types alone, so that claim is now read off the metric export directly for all 15 types. The parse leg keeps the 10 types no check refuses.dimensions: []refusals, the message pin and the metric-family one-issue pin.New pins (
[#21293]block indashboard.test.ts)customatvalues, message naming the type.scatter/radarwith a dimension and two measures: ACCEPT.ChartTypeSchematypes refused at two measures is exactly the metric family plus the five.pieis refused; a wider-enum type (custom) and a dimensionedscatterare not.DashboardSchema.widgets[]travel, and thevaluesdoc string.@objectstack/spec/ui. The new export is the lit control.Verification
Every reading below is at HEAD
b3b3c516f9unless it names another commit.Reverse verification, at committed
221df98d72, throughscripts/ablation-replace.mjs:return. The subject is imported fromsrcby relative path, so nodistis involved.The 19 red tests:
Tests. Each is the package's own vitest run. Downstream packages were chosen by direction: the dependents of
@objectstack/specthat import or parse the dashboard schemas (git grep). This is not the full...@objectstack/specsweep.@objectstack/spec(--project local)@objectstack/spectypecheck (tsc + scripts + test layer)221df98d72before the merge). The test-layer ledger held at 52 files / 246 errors / 135 signatures;tsc -p tsconfig.test.json --listFileslists both touched test files and the new entry.@objectstack/lint@objectstack/sdui-parser@objectstack/platform-objects@objectstack/metadata-protocol@objectstack/metadata@objectstack/service-analytics@objectstack/cli(--project unit)@objectstack/objectql(--project local)Generated artefacts.
pnpm --filter @objectstack/spec check:generatedreports "All 15 generated artifacts are up to date" on the merged tree, after a rebuild.check:migration-registryreportsregistry.ts is current (346 semantic, 243 retired-key, 212 retired-def).Gates.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsatb3b3c516f9derives 115 commands. The 10 changed paths are measured against merge basedb3fee3dc; 671 changed lines.--ranreconciliation: "115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)".PREREQUISITE NOT MET(exit 3) and were re-run to a real exit 0 after a workspace build:check:doc-formula-expressionsandcheck:doc-security-posture(lint/formulanot built);check:skill-examples(client-reactnot built);check:docs-transcript-drift(lintnot built);check:dual-build-cjs-loads(86 packages had nodist);check:lean-entry-closure(objectqlnot built).check-empty-changesetpair,release-rehearsal-clone --self-test,release-pending-publish --self-test,check:objectui-changesetandcheck:pm-changeset-deadline-census. All six ran.check-adr-0087-registration --base origin/main:[BREAKING+bang+clause-②-narrowing] registered dashboard-widget-single-series-multi-measure-refused (new here: dashboard-widget-single-series-multi-measure-refused).check:api-surface: "public API surface + factory signatures unchanged ✓", against the regenerated snapshot.check:doc-authoring: no internal issue-id in the 17250 customer-facing strings. The refusal text carries none.check:nul-bytes: OK.Clause-②, measured.node scripts/pm/check-widening-tells.mjs --declaration no --diffongit diff origin/main...HEADexits 4:dashboard.zod.ts:785–:789. These are the members of the module-privateSINGLE_SERIES_CHART_TYPESas constarray. The array narrows the accept set, so these tells are false as widenings.api-surface/ui.jsonhunk removes the old row beside the new one.checkDashboardWidgetChartMeasureArity (function), the renamed export.--declaration yesexits 0.So the measured arm is
Clause-②: yes (narrowing). Theyesholds because a renamed check function is exported for objectui's.shapemirror to chain, which is the dispatch's own example. The(narrowing)holds because the accept set shrinks.Lint, a measured narrowing rather than the repo-wide run (that run belongs to CI):
eslint --no-inline-config --format jsonover the 6 touched.tsfiles. The JSON reports 6 files, 0 errors and 0 warnings.eslint --print-configreturns a rule set for each file, so none is ignored.parserOptions.projectandprojectServiceare null for every file, andeslint.config.mjs:327states that it never enables type-aware linting. This diff therefore cannot move a verdict on any untouched file.Deviations
os migrate metaprints both entries in one hop, so leaving them would print a contradiction. The standing dev definition requires fixing released text that a change makes false. The claim's "stop on breach" reads the other way, so the conflict is named here rather than settled silently. The edit is two sentences, and the registry region is regenerated, not hand-edited.Acceptance notes
None of these is filed. None is a reproducible defect, a contract violation or an authoring trap.
check-widening-tellsT2 fires on a module-privateas constset whose members narrow. The declaration isyesfor the export row anyway, so the tells decide nothing here. The matcher was not touched. The precedent (SINGLE_MEASURE_WIDGET_TYPES) predates the gate. Carrier: none.SINGLE_SERIES_CHART_FAMILIES, "Kept here, once, because the renderer has no declaration of it to read"). No import of a spec list is measured anywhere. If objectui's mirror or diagnostic later wants it, exporting it is a free widening. Carrier: none.STEP18_RATIONALEfragment, following spec(ui): refuse two or more measures on a dimensionless dashboard widget whose type declares no multi-measure rendering (pie, donut, funnel, scatter, radar, treemap, sankey): ruling D's principle, objectui#8894 Q1 #20958's precedent.Downstream: objectui's mirror (not in this PR; objectstack-ai/objectui#11417 carries it)
@object-ui/typesbuildsDashboardWidgetSchemafrom a.shapespread, so it runs only the checks it chains, and today it chains neither arm. At the pin bump that carries this change:checkDashboardWidgetChartMeasureArityfrom@objectstack/spec/ui.packages/types/src/zod/complex.zod.tsafter.superRefine(checkDashboardWidgetMetricMeasureArity).attachedlist of theDashboardWidgetSchema (complex.zod.ts)row inspec-object-refinements-7715.test.ts.customissue atpath: ['values']. With a dimension, the message starts "WidgetIDdeclares N measures ontype: 'T', a single-series type (...) that draws ONE series whatever itsdimensions". Without one, it keeps spec(ui): refuse two or more measures on a dimensionless dashboard widget whose type declares no multi-measure rendering (pie, donut, funnel, scatter, radar, treemap, sankey): ruling D's principle, objectui#8894 Q1 #20958's "declares N measures with nodimensions" text.objectstack-ai/objectui#11417 remains open: this PR carries only the spec half.
Generated by Claude Code