Repository navigation
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 ·
|
…string / boolean, refused in both analytics strategies in the spec's words (objectstack-ai#21000) (objectstack-ai#21452) Fixes objectstack-ai#21000 Clause-②: no (narrowing) Dispatched by the claim `5955932074` (PM loop round 1, `domain:spec` seat 1, session `session_01UtnxvdiN376GF3sgXwAw4d`), on triage's answer B `5952826307`: the enum retirement only. The row wildcard boundary is objectstack-ai#21409's (landed as `b79301000c`, merged here). ## What this does **`@objectstack/spec`: `AggregationMetricType` loses `number`, `string` and `boolean`** (ADR-0049 enforce-or-remove, grade `5923362062`). - They declared "a custom SQL expression returning a number / string / boolean": the measure's `sql` was the whole computation. Since ruling D on objectstack-ai#20943 (`5d5e679873`), a cube member's `sql` is a column reference, so the three had nothing left to compute. - The enum is declared through `enumWithRetiredValues` (`shared/retired-key.ts`), the house value-level mechanism. The six aggregates (`count`, `sum`, `avg`, `min`, `max`, `count_distinct`) are the whole vocabulary. - An authored retired type fails `tsc`, because it is gone from the type. It is refused at parse with a named prescription, at the enum, at a metric's `type` and at a cube's `measures.METRIC.type`. - The prescription names the aggregate that fits: `sum`, `avg`, `min` or `max` over the column; `count` over `'*'` or over a column; or `count_distinct`. A per-row value becomes a stored or formula field of the object that the measure aggregates. A value derived from measures is `derived: { op, of }` on an ADR-0021 dataset. - A value the enum never declared keeps zod's own message. - The prescriptions are module-private, so the export surface does not grow. - **ADR-0087:** D3 entry `cube-metric-expression-types-retired` (`migrations/entries/semantic/`), with a step-18 rationale fragment (order 62). `registry.ts` was regenerated by `gen:migration-registry` after each merge, never edited by hand. - There is no D2 conversion, by design: the column alone does not say which aggregate the author meant, and a stored cube is refused, never rewritten. - There is no `RETIRED_KEYS_BY_MAJOR` row, because no key left the shape. - **Liveness:** the `analytics_cube` row `measures.type` stays `live`, re-verified 2026-10-02. The narrowing is recorded, and the evidence now names `aggregateOfMeasure` instead of the partition. - **Generated:** only `content/docs/references/data/analytics.mdx` moved, losing the three values from the enum list and from both `type` cells. - `api-surface`, `authorable-surface`, `json-schema.manifest` and `api-surface-signatures` are byte-identical, as the playbook predicts for an enum-value narrowing. - `spec-changes.json` and the upgrade guide stay at protocol 17, and both checks are green. - The module header gained an `@module data/analytics` marker. Without it, moving the imports below the header dropped the page's opening paragraph: `lib/file-description.ts` rule 3 does not select a block inside the import list without the marker. The page's description is byte-identical to `main`. - The neighbouring `CUBE_MEMBER_SQL` docblock no longer says that the ObjectQL path refuses the partition, in the present tense. **`@objectstack/service-analytics`: the `EXPRESSION_METRIC_TYPES` partition is deleted.** It is replaced by ONE verdict both strategies call, `aggregateOfMeasure` (`strategies/native-sql-strategy.ts`). - `aggregateOfMeasure` admits a type this runtime lowers (the `AGGREGATE_SQL` keys, pinned EQUAL to the enum's options). It refuses everything else with the SPEC's own words, `AggregationMetricType.safeParse(type)`, so the runtime keeps no list of metric types, retired or otherwise. - `NativeSQLStrategy#resolveMeasureSql` asks it before anything is lowered. The verbatim emit is gone, and so is the unrecognised-type throw it replaced. - `ObjectQLStrategy#resolveMeasureAggregation` asks it at the one resolver both doors call. The `INVALID_FIELD` arm for the partition is gone. - Comments that named the partition or the three types are updated: `plugin.ts` (the bridge's comment, its docblock, and its runtime message, which said "a custom-SQL measure is refused earlier"), `preview-evaluator.ts`, `analytics-service.ts`, `cube-measure-field-type-door.ts` and `dataset-refusal.ts`. **`@objectstack/lint` (test and comment only).** objectstack-ai#21435 landed between my merges with a pin asserting that the three types sit outside the aggregate table. That is false after this retirement, so the assertion now reads `[]`. The skip-5 `silent` case is kept. No changeset is needed: a comment and a test, nothing in the published output changes. ## Clause-②, measured `node scripts/pm/check-widening-tells.mjs --declaration no --diff` (the merge-base diff against `b79301000c`) exits 0, with no widening tell. Three key lines are reported as a stated silence. They are the retired-member prescription entries `number:` / `string:` / `boolean:` in the `enumWithRetiredValues` map, which are refusals, not accept-set members. No export-listing row was added (`check:api-surface` green, byte-identical), so the line is `Clause-②: no (narrowing)`. Both changesets are BREAKING, with `!`, a **BREAKING** banner, the `(narrowing)` arm, exactly one ADR-0087 marker (`registered cube-metric-expression-types-retired`) and `minor`. ## Census (examples, packages, platform objects, objectui) The instrument is an AST walk over every object-literal member of a `measures:` record. It covered 7,654 `.ts`/`.js` files under `examples/**` and `packages/**` at `4ec505761d`, platform objects included. - 321 measure entries in total. Lit control: `count` 164, `sum` 71. - Retired-type entries: 7. All are deliberate refusal fixtures in the `service-analytics` tests, built without the parse. - Zero hits in `examples/**`, in non-test `packages/**`, in `skills/**` and in `content/docs/**` (one cube example there, `count` / `sum`). - `examples/app-showcase/src/data/analytics/showcase.cube.ts`: 3 measures (`count`, `sum`, `avg`). Its three `type: 'string'` lines (48, 53, 63) are **dimensions**. `DimensionType` is a separate enum, unchanged, and pinned in the new test file. - JSON fixtures carrying record-form `measures`: zero. - objectui at the `.objectui-sha` pin `89cad75d55`: 0 mentions of `AggregationMetricType`, and 0 record-form measure entries over 528 files that mention `measures`. Control: `clientValidation.ts` names `CubeSchema`. The Console Pin Gate is not at risk: no export left. ## Premise check (zone 2) 1. **Holds.** On `68c5ab7eba`, `AggregationMetricType` (`data/analytics.zod.ts:27`) listed the three, and `MetricSchema.type` used it. Measured through `AnalyticsService` with a column `sql`: - the raw-SQL path SERVED the column unaggregated: `SELECT status AS "status", amount AS "m" FROM "orders" GROUP BY status`; - the ObjectQL path refused the measure `INVALID_FIELD` / 400. 2. `api/analytics.zod.ts:231`: the `/analytics/meta` member's describe ("Aggregation type for a measure (`AggregationMetricType`)") is not made false by the retirement, so it is not edited. 3. That `type` is a separate `z.string()` field, deliberately not the enum, because the projection copies the value verbatim. Measured: it is not the enum. ## What a stored cube carrying a retired type meets (fail closed, never stood down) **Pinned in `cube-metric-expression-types-retirement.test.ts`:** - the artifact boot door (`ObjectStackDefinitionSchema`, the parse `MetadataPlugin` runs a built artifact through) refuses it at `analyticsCubes.0.measures.m.type` with the prescription; - `defineStack` refuses it with `STACK_SCHEMA_INVALID` / 422; - `defineCube` and the `analytics_cube` write door (`getMetadataTypeSchema('analytics_cube')`, what `PUT /api/v1/meta/analytics_cube/NAME` validates) refuse it too; - the rehydration seam (`applyConversionsToStoredItem`) replays NOTHING over it. Control: the same row's retired sub-day granularity IS rewritten, so the seam is live. The stored row reaches the parse as stored, and the parse refuses it. **Measured through the real dispatcher routes** (a temporary `packages/runtime` probe, not committed), for a cube a host registers in-process WITHOUT the parse: - `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql`, on both strategies: `500`, with `error.message` carrying the spec's prescription verbatim. Nothing executed. - `sum` control: `200`. - `GET /api/v1/analytics/meta`: `200`, listing the measure with `type: "number"` as registered (see the Acceptance notes). ## Merges (serial constraints) - `0d182f0549` merged `main` at `3a6d92f78b`, bringing PR objectstack-ai#21424 (objectstack-ai#21376, the NativeSQL filter-compile region) and PR objectstack-ai#21425 (objectstack-ai#21293, its registry entry). Clean. - `587d9d4b63` merged `main` at `d7d5b4f96a`. One hand conflict, in the `objectql-strategy.ts` import from `native-sql-strategy.js`: objectstack-ai#21440 added `windowClauseSql`, and both are kept. - `4ec505761d` merged `main` at `b79301000c`, bringing PR objectstack-ai#21431 (objectstack-ai#21409, the count-only boundary). One hand conflict, in the `analytics.zod.ts` imports: `enumWithRetiredValues` and objectstack-ai#21409's `ANALYTICS_COLUMN_PATH` / `rowWildcardOutsideCount` / `rowWildcardOutsideCountRefusal`, both kept. - Every merge went through `scripts/pm/os-regen-merge.sh`. Its step 4 was run each time, and `gen:migration-registry` afterwards wrote no diff. - **Registry reading at `4ec505761d`:** 349 semantic, 244 retired-key and 212 retired-def entries. The siblings' ids `analytics-row-wildcard-outside-count-refused` and `dashboard-widget-single-series-multi-measure-refused` appear at the same counts as on `main` (1 each). `cube-metric-expression-types-retired` appears twice: the semantic entry and its step-18 rationale fragment. - **Ledger reading at `4ec505761d`:** the `measures.sql` and `dimensions.sql` notes carry objectstack-ai#21409's count-only wording and no longer name this card. `measures.type` carries this retirement's own wording. ## Tests (head `4ec505761d`) - `@objectstack/spec`: - `vitest --project local`: 602 files, 17737 passed, 1 todo; - `--project repo`: 43 of 49 files, 705 passed (the other six are NOT MEASURED, below); - `typecheck` (`tsc`, scripts, test layer): OK. The new `@ts-expect-error` (a typed `Metric` with `type: 'number'`) sits in the compiled test program. - `@objectstack/service-analytics`: 170 files, 3846 passed, 126 skipped. `typecheck` OK. - The reverse verification happened on the way: a fixture typed `Cube` with `type: 'number'` failed `tsc` with TS2322 against the rebuilt `.d.ts` until it was cast. - `@objectstack/lint`: 119 files, 5592 passed. `typecheck` OK. ## Ablations Both run from the committed tree through `scripts/ablation-replace.mjs`. In each, the anchor hit once and the blob changed; the restore was proved by blob equal to `HEAD` and an empty `git diff HEAD`. Both subjects resolve from `src`, so no rebuild was needed. - **The runtime verdict admits every string** (`aggregateOfMeasure`'s table check removed): 28 failed, 12 passed, over `metric-type-coverage`, `measure-expression-both-strategies` and `measure-expression-sql`. - Every refusal case went red: both strategies, both doors, and the drift case. - The admitted-aggregate, cross-object-twin and coverage-equality cases stayed green, which is the predicted direction. - **The spec's `number` prescription is unmapped:** 8 failed, 17 passed in `cube-metric-expression-types-retirement.test.ts`. Every `number` door pin went red; the `string` / `boolean` pins and the controls stayed green. ## Gates `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `4ec505761d` derived 117 families. All 117 were run, each exit code recorded, and `--ran` answered "117 derived, 117 run, 0 NOT-MEASURED (a DERIVED zero)". Among them: - `check-adr-0087-registration` ("2 declared-breaking changeset(s), each carrying an ADR-0087 disposition"); - `check-changeset-no-major`; - `check:generated` ("All 15 generated artifacts are up to date"); - `check:liveness`, `check:doc-authoring`, `check:nul-bytes` and `check:skill-examples` (after building the client closure); - `check:type-check-debt` (325 s); - `check:dual-build-cjs-loads` (after a whole-repo build, 71 of 72 tasks cached). **NOT MEASURED** - `@objectstack/spec` repo-project files `build-schemas-check-mode`, `dist-freshness`, `dist-freshness-adoption`, `publish-smoke-boot-failure`, `publish-smoke-port-collision` and `schema-tree-freshness`. Reason: they drive whole builds and exercise build tooling this diff does not touch. - `@objectstack/cli` integration tier, declared to CI. ## Acceptance notes - **`GET /analytics/meta` still lists a host-registered unparsed cube's measure with its retired `type`**, while the query doors refuse it. This is pre-existing for any enum-invalid type (`median` read the same at base). It is reachable only by a host that registers a cube literal without `CubeSchema`, because every parsing door refuses it first. Not filed; carrier: none. - **A stored `analytics_cube` row** read at `GET /api/v1/meta/analytics_cube/NAME` comes back as stored. Stored rows keep being read (the runtime gate's D4 asymmetry), and no conversion rewrites it (pinned at the seam). - Re-saving it is refused with the prescription (the write door pin). - The analytics registry has no metadata read path (objectstack-ai#20965's measurement), so such a row reaches no query. - The HTTP read itself was NOT MEASURED through the route. - **The ObjectQL envelope for the three moves from `INVALID_FIELD` / 400 to the undeclared-500 tier.** The message is readable and carries the prescription. This is the tier `dataset-refusal.ts` assigns to a cube that never met the parse, and the changeset says so. - A never-declared type (`median`) on the ObjectQL path is now refused at the resolver in the same tier. Before, it was forwarded to `executeAggregate`: the auto-bridge refused it, a host's own executor received it, and `/analytics/sql` echoed `MEDIAN(amount)`. - `aggregate-bridge-function-vocabulary.test.ts` therefore pins both seams. The bridge is driven directly through the service's strategy context, because no cube path reaches it with a non-aggregate method any more. - **The `measures.sql` ledger note** (objectstack-ai#20943's, re-pointed by objectstack-ai#21409) was made false by this diff, as at-tier record `5959409102` ruled. It was corrected in patch round 2 (`4278601b82`): both runtime expression branches are gone (the gate's stand-down with objectstack-ai#20965, the raw-SQL verbatim emit here). What remains for a cube that reaches the service without meeting the parse is `qualifyAndRegisterJoin` passing a non-column `sql` through inside the aggregate, measured through `generateSql`. - **The prescriptions say "removed … in @objectstack/spec 17.7.0"**, assuming the next release is a minor (the label is 17.6.0, which is published). If the next release is cut as a major, those runtime strings need the new number. - **Files beyond the claim's list**, all comments or tests that named the partition or the types, or that the merges made false: - `service-analytics`: `analytics-service.ts`, `cube-measure-field-type-door.ts`, `dataset-refusal.ts`, and the tests `aggregate-bridge-function-vocabulary`, `caller-member-column-reference-gate`, `cube-authored-format-granularity`, `field-read-admission-gate` and `unlisted-refusal-envelope`; - `lint`: `validate-dataset-measure-aggregates` (source comment and test). --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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