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
objectstack-fleet[bot] merged 7 commits into
Conversation
… widget whose type declares no multi-measure rendering DASHBOARD_WIDGET_MULTI_MEASURE_TYPES is the one list (table, pivot, bar, column, horizontal-bar, line, area, combo); checkDashboardWidgetDimensionlessMeasureArity refuses the seven remaining non-metric chart types at values when dimensions is absent or empty, naming the widget id, the count and the type. Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…R-0087 semantic entry Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…NG, ADR-0087 registered Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
… reference for the two new exports Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…mensionless-multi-measure-refused
…tree — main's exports and this branch's two Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
📓 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 — 137 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 45a8676ff86f5095d47cc092b6a09c04cc56d6a6 && git checkout 45a8676ff86f5095d47cc092b6a09c04cc56d6a6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d1f8ce8658b937d3685df62b04e2e478430b8e83 e1494c7206fcd7d16d5446fbb98dce842e58dff3 && git checkout -B drift-repro d1f8ce8658b937d3685df62b04e2e478430b8e83 && git merge --no-ff e1494c7206fcd7d16d5446fbb98dce842e58dff3
node scripts/docs-audit/affected-docs.mjs --json d1f8ce8658b937d3685df62b04e2e478430b8e83
|
Contract reviewServed-tier: Inputs read: card #20958 (body, the claim comment and the os-dev-report), PR #21053 (body, the nine-file list, the net diff against ① Derived judgmentsAccept-set changes the diff implies, each judged:
One non-blocking reading: the refusal message hardcodes " ② Semver level
③ Boundary flagsDev deviations (the os-dev-report lists four), each answered:
Out-of-scope findings (two, carrier none): whether the seven types render several measures WITH a dimension is outside the card, and the entry states it as unanswered rather than implying it is covered — agreed, no escalation; the ablation tool refusing a replacement that is a substring of its anchor is a tooling observation — agreed, nothing owed here. Check-runs on the head, read at 2026-10-01T03:46Z: 32 runs, 15 concluded success ( Implemented-by: VERDICT: PASS Generated by Claude Code |
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 onpie,donut,funnel,scatter,radar,treemapandsankey. 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, typedas const satisfies readonly ChartType[]. The check, its refusal text and thevaluesdoc string all read it. No second literal of the set exists in code (a test pins oneexport constdeclaration).checkDashboardWidgetDimensionlessMeasureArity(new export) is an object-level check chained ontoDashboardWidgetSchemaby identifier, right aftercheckDashboardWidgetMetricMeasureArity. It refuses whendimensionsis absent or[],valueshas two or more members, andtypeis a declaredChartTypeSchemamember outside the multi-measure set and outside the metric family. It emits ONEcustomissue atvaluesnaming the widgetid, the count and the type, lists the multi-measure set (read from the constant), and steers totype: 'table'(a row of measures), a bar-family type (bar/column/horizontal-bar), or one widget per measure.checkDashboardWidgetMetricMeasureArityalready refuses a second measure onmetric/kpi/gauge/solid-gauge/bullet(and a typeless widget, which defaults tometric) 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).invalid_valueontypeaborts object-level checks). objectui's mirror, however, re-pointstypeat a wider enum (list,custom, component widget types) with no default, so the export returns on anything outsideChartTypeSchema.options. It refuses exactly what the spec's door refuses.packages/spec/src/migrations/entries/semantic/18.dashboard-widget-dimensionless-multi-measure-refused.ts, plus thegen:migration-registrylap. 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/20958-dimensionless-multi-measure-refused.md:minor, BREAKING banner, the ADR-0087 markerregistered dashboard-widget-dimensionless-multi-measure-refused, theClause-②line, and a FROM → TO table.api-surface/ui.jsonandexport-origins/ui.json(+2 exports each, 0 removed), and thevaluesrow ofcontent/docs/references/ui/dashboard.mdx.Measured before building (the dispatch's three hypotheses)
Premise at the base
05be35259(builtdist,DashboardWidgetSchema.safeParse): a widget with no dimensions andvalues: ['a', 'b']returned ACCEPT on every one of the seven types. The metric family returned REFUSEvalues:customat every dimensionality.bogusPropon the same widget returned REFUSEunrecognized_keys, which is the lit control.H1, attachment points. In this repo the metric check is attached at exactly one place: the
.superRefinechain ofDashboardWidgetSchema.DashboardSchema.widgets,ObjectStackDefinitionSchema.dashboardsand thedashboardmetadata-type schema all reach it through that schema (git grepoverpackages/**: no other construction of the widget schema, no.shapemirror in this repo). The new check is chained at the same point. The "editor face" the precedent entry names is objectui's.shapemirror, 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 apiecarrying one dimension.DashboardSchema,getMetadataTypeSchema('dashboard')widgets.0.values:customObjectStackDefinitionSchemadashboards.0.widgets.0.values:customdefineStackdefineStack validation failed (1 issue), naming the widgetos validate(a config built bydefineStack)defineStackissuesaveMetaItem, ACTIVE and DRAFT)422 INVALID_METADATAatwidgets.0.values, 0 rows persistedNo consumer-side copy exists. The probes were one-shot: a scratch config and a scratch test file, both deleted, and
git statuswas clean afterwards.H3, fixtures and stored rows. I ran a census at
05be35259over every tracked.ts/.tsx/.js/.json/.md/.mdx. 469 brace-local literals carryvalues: [...]. 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 at1263e40gives 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 mergingorigin/main9b0de7de7throughscripts/pm/os-regen-merge.sh. On that merge,api-surface/ui.jsonandexport-origins/ui.jsonwere regenerated from the merged tree and now hold main's new exports plus this branch's two.Reverse verification ran at the committed
1a0e75a6dthroughscripts/ablation-replace.mjs. It deleted only the.superRefine(checkDashboardWidgetDimensionlessMeasureArity)attachment, so the function stayed declared. The subject is imported fromsrcthrough a relative path, so nodistwas involved.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/specsweep):e1494c720@objectstack/spec(--project local)@objectstack/spectypecheck (tsc + scripts + test layer)@objectstack/metadata-protocol@objectstack/lint@objectstack/service-analytics@objectstack/sdui-parser@objectstack/platform-objects@objectstack/objectql(--project local)@objectstack/metadata@objectstack/cli(--project unit; the integration tier is declared to CI because no spawn entry or integration file is touched)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:generatedreports "All 15 generated artifacts are up to date".check:api-surfacereports "public API surface + factory signatures unchanged" after regeneration.Gates:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackate1494c720derives 113 commands, and all 113 ran with exit 0.--ranreconciliation: "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-reactnot built)check:dual-build-cjs-loads(exit 3, eight packages had nodist)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 includescheck-changeset-no-major,check-empty-changeset,check:nul-bytes,check:pm-widening-tells,check:spec-docblock-symbol-anchors,check:issue-citations,check:doc-authoringandcheck:widget-option-census.Lint, a measured narrowing rather than the repo-wide run (that run belongs to CI):
eslint --no-inline-config --format jsonover the 5 touched.tsfiles. The JSON reports 5 files, 0 errors and 0 warnings, ate1494c720.eslint --print-configreturns a rule set for each touched file, so none is ignored.eslint.config.mjsenables no type-aware linting.parserOptions.projectandprojectServiceare 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.
STEP18_RATIONALEfragment was added. This follows the precedent (09e16a574added 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.object-refinement-check-exports.test.tspins one export percustomcheck the schema carries, and objectui's mirror chains the check by name.Clause-②: yes (narrowing)covers both.scripts/ablation-replace.mjsrefuses 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/typesbuildsDashboardWidgetSchemafrom a.shapespread, so it runs only the checks it chains. Until it chains this one, its editor keeps accepting a dimensionless two-measurepie, and the author meets the refusal at publish. The entry says so. For the coordination child:checkDashboardWidgetDimensionlessMeasureArity(and, for any UI that lists the set,DASHBOARD_WIDGET_MULTI_MEASURE_TYPES) from@objectstack/spec/ui.packages/types/src/zod/complex.zod.tsafter the existing.superRefine(checkDashboardWidgetMetricMeasureArity).attachedlist of theDashboardWidgetSchema (complex.zod.ts)row inpackages/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.code: 'custom'andpath: ['values']. The message starts "WidgetIDdeclares N measures with nodimensionsontype: 'T'", where ID is the widget id, N is the count and T is the authored type.Generated by Claude Code