Skip to content

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

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21293-single-series-measure-arity
Oct 2, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 as 5944795015): for pie, donut, funnel, treemap and sankey, 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. scatter and radar with a dimension are outside the ruling and parse as before.

What changed

  • packages/spec/src/ui/dashboard.zod.ts
    • New module-private SINGLE_SERIES_CHART_TYPES (:784): pie / donut / funnel / treemap / sankey, typed as const satisfies readonly ChartType[]. It is private, like the metric family's list. The check, its refusal text and the values doc string read it.
    • The check gains a second arm, the single-series arm. When dimensions is declared and non-empty, values has two or more members, and type is one of the five, it emits ONE custom issue at values. The issue names the widget id, the count and the type, and says the type "draws ONE series whatever its dimensions". It steers to type: 'table' (a column per measure), a bar-family type (one bar per measure in each category), or one widget per measure.
    • The dimensionless arm is unchanged, byte for byte, in both verdict and message. The arms are exclusive on dimensions, so a dimensionless pie still gets the dimensionless message, and only one issue.
    • The metric family still returns early (its sibling check owns it), and so do types outside ChartTypeSchema.
    • Renamed export: checkDashboardWidgetDimensionlessMeasureArity is now checkDashboardWidgetChartMeasureArity (:878, chained at :1461). The reason is in "The rename" below.
    • The values doc string now states the single-series rule. It reads the private constant.
  • ADR-0087 entry packages/spec/src/migrations/entries/semantic/18.dashboard-widget-single-series-multi-measure-refused.ts, plus the gen:migration-registry lap. 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).
  • 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 entry, amended in two sentences (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, and os migrate meta prints 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.
  • Tests
    • 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 .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.
  • Regenerated:
    • api-surface/ui.json and export-origins/ui.json: one row renamed in each, -1 / +1.
    • The values row of content/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 ...DimensionlessMeasureArity names 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:

  • At the .objectui-sha pin 89cad75d55702cc4f267bead5bf267de575d5842, git grep "DimensionlessMeasureArity\|MULTI_MEASURE_TYPES" returns 0 hits. The control, checkDashboardWidgetMetricMeasureArity in packages/types/src/zod/complex.zod.ts, returns 2. objectui's DashboardWidgetSchema mirror chains checkDashboardWidgetStageOrder and checkDashboardWidgetMetricMeasureArity only (complex.zod.ts:1304, and the attached row in spec-object-refinements-7715.test.ts:135).
  • On objectui origin/main 5988b6b53 the readings are the same: 0 hits, control 2.
  • In this repository the old name had no consumer outside packages/spec's own tests (git grep).
  • The spec object's check COUNT is unchanged at three, so objectui's census test sees no count change at the bump.

A rename is a removal plus an addition, so the changeset carries the import FROM → TO, and Clause-② is yes.

Measured before building (the dispatch's four hypotheses)

The branch base is 4b20c84748. It is newer than the dispatch's 1d0600bf66, and dashboard.zod.ts did not change between the two.

  • H1, holds. The check returned early on any non-empty dimensions (old :834), and DASHBOARD_WIDGET_MULTI_MEASURE_TYPES listed the eight multi-measure types. At the base I built dist and ran DashboardWidgetSchema.safeParse on { id, type, dataset, dimensions: ['stage'], values: ['revenue', 'cost'] }. It returned ACCEPT for pie, donut, funnel, treemap, sankey, scatter, radar, bar and table. One measure returned ACCEPT on all of them. The lit control, the same body plus bogusProp, returned REFUSE unrecognized_keys on every type.
  • H2, holds. objectui's renderer at the pin (packages/plugin-charts/src/AdvancedChartImpl.tsx) binds the first series on exactly these arms:
  • H3, partly falsified. There is no declared "order": build-migration-registry.ts derives 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 on main first, so this branch merged origin/main db3fee3dc9 through scripts/pm/os-regen-merge.sh (merge commit b3b3c516f9).
    • The registry text-merged clean.
    • gen:migration-registry on the merged tree rewrote it byte-identical: 346 semantic entries, git status empty.
    • All three sibling entries are present at HEAD. Quoted-exact-name git grep, HEAD vs origin/main: analytics-query-window-non-negative-integer 1/1, dashboard-widget-dimensionless-multi-measure-refused 1/1, and dashboard-widget-single-series-multi-measure-refused 1 at HEAD, 0 on main.
  • H4, census: no authored hit. I scanned brace-local literals carrying values: [...] over every tracked .ts/.tsx/.js/.mjs/.cjs/.json/.md/.mdx/.yml file.
    • objectstack at 4b20c84748: 496 literals, 33 on the seven types, 1 dimensioned multi-measure on the five. That one is packages/spec/src/ui/object-refinement-check-exports.test.ts:365, the fixture that pinned the old acceptance, triaged below.
    • Lit control: 9 dimensioned multi-measure literals on other types (table / combo widgets in examples/app-showcase, and others).
    • objectui at the pin: 480 literals, 13 on the seven, and 1 hit on the five. That hit is the prose example in .changeset/11417-dashboard-single-series-dropped-measure.md:10, not metadata.
    • examples/**, the showcase and CRM apps, and packages/platform-objects dashboards carry zero instances.
    • Dynamically built fixtures do not show up in a static scan. I inspected each downstream test that loops over chart types or builds widgets (lint validate-widget-bindings, runtime-gate, validate-dashboard-widget-options, and metadata-protocol's two donut fixtures). All carry one measure.

Doors after the change (built dist, one-shot probe, deleted afterwards, git status clean)

body (dimensions: ['stage']) DashboardSchema getMetadataTypeSchema('dashboard') ObjectStackDefinitionSchema defineStack
pie, 2 measures REFUSE widgets.0.values:custom REFUSE widgets.0.values:custom REFUSE dashboards.0.widgets.0.values:custom throws defineStack validation failed (1 issue), naming the widget
sankey, 2 measures REFUSE REFUSE REFUSE throws
pie, 1 measure (control) ACCEPT ACCEPT ACCEPT returns
radar, 2 measures (control) ACCEPT ACCEPT ACCEPT returns
bar, 2 measures (control) ACCEPT ACCEPT ACCEPT returns

I did not re-measure os validate or 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)

New pins ([#21293] block in dashboard.test.ts)

  • Each of the five, with a dimension and two measures: REFUSE, custom at values, message naming the type.
  • The control: one measure on each of the five with a dimension: ACCEPT.
  • Each of the five with TWO dimensions: REFUSE.
  • scatter / radar with a dimension and two measures: ACCEPT.
  • Every multi-measure type with a dimension and three measures: ACCEPT.
  • Taxonomy read off the door: with a dimension, the set of ChartTypeSchema types refused at two measures is exactly the metric family plus the five.
  • A dimensionless widget of each of the five keeps the dimensionless message, word for word and as ONE issue. The parse leg equals the direct call.
  • The metric family with a dimension keeps its own refusal, and the chart export stays silent on it.
  • Direct-call legs: a dimensioned pie is refused; a wider-enum type (custom) and a dimensioned scatter are not.
  • DashboardSchema.widgets[] travel, and the values doc string.
  • The old export name is absent from @objectstack/spec/ui. The new export is the lit control.

Verification

Every reading below is at HEAD b3b3c516f9 unless it names another commit.

Reverse verification, at committed 221df98d72, through scripts/ablation-replace.mjs:

  • The mutation turned the single-series arm's type guard into an unconditional return. The subject is imported from src by relative path, so no dist is involved.
  • The prediction was "turns red", and that is the direction observed.
anchor x1 -> x0 · marker x0 -> x1 · blob 9a8c2284f0c8 -> cfdc26ae3cbf (mutation landed)
RED   vitest exit=1   19 failed | 342 passed (361)   (dashboard.test.ts + object-refinement-check-exports.test.ts)
restored blob 9a8c2284f0c8 == HEAD blob, git diff HEAD empty; marker count after restore 0

The 19 red tests:

  • 14 in the new block: 5 refusals, 5 two-dimension refusals, the taxonomy pin, the message pin, the direct-call pin and the travel pin;
  • 5 parity rows in the exports catalogue, one per dimensioned single-series fixture.

Tests. Each is the package's own vitest run. Downstream packages were chosen by direction: the dependents of @objectstack/spec that import or parse the dashboard schemas (git grep). This is not the full ...@objectstack/spec sweep.

package result
@objectstack/spec (--project local) 600 files, 17666 passed, 1 todo, exit 0
@objectstack/spec typecheck (tsc + scripts + test layer) exit 0 (and at 221df98d72 before the merge). The test-layer ledger held at 52 files / 246 errors / 135 signatures; tsc -p tsconfig.test.json --listFiles lists both touched test files and the new entry.
@objectstack/lint 119 files, 5575 tests, exit 0
@objectstack/sdui-parser 13 files, 218 tests, exit 0
@objectstack/platform-objects 59 files, 949 tests, exit 0
@objectstack/metadata-protocol 201 passed, 3 skipped (204 files); 2983 passed, 19 skipped; exit 0
@objectstack/metadata 56 files, 836 tests, exit 0
@objectstack/service-analytics 168 files; 3794 passed, 89 skipped; exit 0
@objectstack/cli (--project unit) 247 files, 3538 tests, exit 0. The integration tier is declared to CI: no spawn entry or integration file is touched.
@objectstack/objectql (--project local) 364 files, 7360 tests, exit 0

Generated artefacts. pnpm --filter @objectstack/spec check:generated reports "All 15 generated artifacts are up to date" on the merged tree, after a rebuild. check:migration-registry reports registry.ts is current (346 semantic, 243 retired-key, 212 retired-def).

Gates.

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at b3b3c516f9 derives 115 commands. The 10 changed paths are measured against merge base db3fee3dc; 671 changed lines.
  • All 115 ran. --ran reconciliation: "115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)".
  • Six first answered PREREQUISITE NOT MET (exit 3) and were re-run to a real exit 0 after a workspace build:
    • check:doc-formula-expressions and check:doc-security-posture (lint / formula not built);
    • check:skill-examples (client-react not built);
    • check:docs-transcript-drift (lint not built);
    • check:dual-build-cjs-loads (86 packages had no dist);
    • check:lean-entry-closure (objectql not built).
  • Compared with the seat's dispatch-time list (109), the re-derivation adds six: the changeset-driven check-empty-changeset pair, release-rehearsal-clone --self-test, release-pending-publish --self-test, check:objectui-changeset and check:pm-changeset-deadline-census. All six ran.
  • Verdict lines:
    • 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 --diff on git diff origin/main...HEAD exits 4:

  • It reports five T2 tells, dashboard.zod.ts:785–:789. These are the members of the module-private SINGLE_SERIES_CHART_TYPES as const array. The array narrows the accept set, so these tells are false as widenings.
  • T3 does not fire, because the api-surface/ui.json hunk removes the old row beside the new one.
  • The export listing does gain a row: checkDashboardWidgetChartMeasureArity (function), the renamed export.
  • --declaration yes exits 0.

So the measured arm is Clause-②: yes (narrowing). The yes holds because a renamed check function is exported for objectui's .shape mirror 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):

  • I ran eslint --no-inline-config --format json over the 6 touched .ts files. The JSON reports 6 files, 0 errors and 0 warnings.
  • Population: eslint --print-config returns a rule set for each file, so none is ignored.
  • Invariance: parserOptions.project and projectService are null for every file, and eslint.config.mjs:327 states that it never enables type-aware linting. This diff therefore cannot move a verdict on any untouched file.

Deviations

Acceptance notes

None of these is filed. None is a reproducible defect, a contract violation or an authoring trap.

Downstream: objectui's mirror (not in this PR; objectstack-ai/objectui#11417 carries it)

@object-ui/types builds DashboardWidgetSchema from a .shape spread, so it runs only the checks it chains, and today it chains neither arm. At the pin bump that carries this change:

objectstack-ai/objectui#11417 remains open: this PR carries only the spec half.


Generated by Claude Code

claude added 3 commits October 2, 2026 13:07
…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>
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/analytics.mdx (via DashboardWidgetSchema (symbol, a top-level const))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via DashboardWidgetSchema (symbol, a top-level const))
  • content/docs/releases/v16.mdx (via DashboardWidgetSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 39a912ea73ddff7fc85ebb3379a8ce74ff1343f5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cbad8adc60ff74b4c794971a08e21735217af3a3 — the merge of head 4435664b26cb01f1d1d4b6c9424dc6e799bbf2fa into base 39a912ea73ddff7fc85ebb3379a8ce74ff1343f5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 39a912ea73ddff7fc85ebb3379a8ce74ff1343f5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b3b3c516f93786ae888815e6c2a8434abad3f3f1
Local-runs: none

Isolated contract review of PR #21425 for card #21293, branch claude/issue-21293-single-series-measure-arity, net diff origin/main...b3b3c516f9 (merge base db3fee3dc9): 10 files, +585/-86. Inputs: the card's body and its three comments (triage 5944795015, the claim 5952561725, the os-dev report 5954788000); objectui#11417's ruling 5943580519; the mother rule 11d28c17df (PR #21053, #20958) as a git object; the PR body, its file list and the diff; the head's check-runs; and objectui read-only at the .objectui-sha pin 89cad75d5570 and at its main e46ee770ba. Nothing was built, run or re-run.

① Derived judgments

  1. Accept set at DashboardWidgetSchema.values, every door that parses the spec schema. With dimensions declared and non-empty, two or more values, and type in pie / donut / funnel / treemap / sankey, the widget is refused: ONE custom issue at values naming the widget id, the count and the type (dashboard.zod.ts, the single-series arm inside checkDashboardWidgetChartMeasureArity at :878, chained once at :1461). Right — exactly the ruled narrowing, through 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 check (same function, one more arm, same attachment point) with no second rule. The five live in the module-private SINGLE_SERIES_CHART_TYPES (:784, as const satisfies readonly ChartType[]), which the check, its message and the values doc string read.
  2. scatter and radar with a dimension and two or more measures still parse: the single-series arm returns on any type outside the five before adding an issue. Right — outside the ruling, as the card and triage said. Named, not a defect of this PR: at the pin the renderer also binds series[0] on scatter (AdvancedChartImpl.tsx:1338, :2464) but says so on the chart, and radar maps every series (:2418). The ruling drew the line at the five; the diff keeps it there.
  3. 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 dimensionless refusal is byte-identical. The dimensionless ctx.addIssue block at the head equals the one on origin/main character for character (both extracted and compared). The arms are exclusive on dimensions — the single-series arm returns after its issue, and the dimensionless arm is reached only when dimensions is absent or [] — so a dimensionless widget of the five gets exactly one issue with the old text. The reordering of the early returns (metric family and ChartTypeSchema membership now precede the dimensions read) moves no dimensionless verdict, since every return is a pure predicate on the same inputs. Right.
  4. Unchanged answers, each read off the code: one measure on every type (a values of one member, or none, returns first); every DASHBOARD_WIDGET_MULTI_MEASURE_TYPES member with or without a dimension (each arm returns on them); the metric family (returns before either arm, so the sibling check's one issue stands alone); a type outside ChartTypeSchema (returns); values: [] (.min(1) untouched). Right.
  5. One input whose answer moves and that the ruling does not cover — direct calls only. Called as a function with a non-array dimensions (a string, say) on one of the five with two measures, the old export returned silently ("anything but absent-or-empty returns"); the new one refuses, because the single-series arm treats anything but absent-or-empty as declared. Unreachable through the spec door (the field's own invalid_type) and through objectui's mirror, which re-types only id and type (complex.zod.ts:1261–:1266 at the pin) and keeps the spec's dimensions. The direction matches the rule's own sentence ("one series whatever its dimensions"). Judged acceptable; named so it is not met later as an unannounced move.
  6. Public surface of @objectstack/spec/ui: checkDashboardWidgetDimensionlessMeasureArity removed, checkDashboardWidgetChartMeasureArity added (api-surface/ui.json and export-origins/ui.json, one row out and one in each; the spec object's .superRefine count stays 3). A removed export is breaking on its own, and the changeset says so (②). Necessary or a choice? A choice — the ruling ordered the arm, not the name — but one inside the claim's envelope (5952561725 listed the two listings "if an export moves" and conditioned Clause-② on an added row), and in the startup-stage shape: no alias, no re-export of the old name, no grace window, FROM → TO in the changeset. A name that says "dimensionless" on a check that now judges dimensioned widgets would misdirect the one consumer the export exists for, a mirror choosing what to chain. Right.
  7. Consumers of the old name, verified at the pin. git grep over objectui at 89cad75d5570 for DimensionlessMeasureArity and MULTI_MEASURE_TYPES: 0 files. Control checkDashboardWidgetMetricMeasureArity: the import at complex.zod.ts:23 and the chain at :1304 (the two code hits the dev counted), plus tests, changesets and comments. The same readings hold on objectui main e46ee770ba. The mirror chains checkDashboardWidgetStageOrder and checkDashboardWidgetMetricMeasureArity only (:1303–:1304); its census (spec-object-refinements-7715.test.ts:135) reads the spec object's check count, unchanged at 3, and names neither chart-arity name. In this repository the old name survives only as history (packages/spec/CHANGELOG.md, the two entries' prose), the changeset's FROM → TO row, one code comment, and the test that pins its absence from the barrel. The Console Pin Gate was skipped on this head by its path filter (packages/spec/src/** is not in the console filter), so the pin's safety against the rename rests on this grep, reproduced here, not on a CI build of this PR; Post-Task Checklist Add Changesets and GitHub Actions automation #4's pre-merge check is met. Right.
  8. Renderer premise, verified at the pin. AdvancedChartImpl.tsx: the pie / donut arm opens at :2087 and binds series[0]?.dataKey at :2095; funnel :2160 / :2161; treemap :2242 / :2243; sankey :2277 / :2278–:2279. None reads another series. objectui's own SINGLE_SERIES_CHART_FAMILIES (DatasetWidget.tsx:415) names the same five. Right.
  9. Registry. The new entry file 18.dashboard-widget-single-series-multi-measure-refused.ts matches its generated region in registry.ts line for line; it sits in step18 between dashboard-widget-metric-family-... and dashboard-widget-stage-order-non-funnel-refused, the id sort the builder derives ("Order is DERIVED, never declared"). 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's analytics-query-window-non-negative-integer is present once at the head and once on main; all three siblings are present; the merge commit b3b3c516f9 is the os-regen-merge.sh merge of main at db3fee3dc9. The amended 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 text in the region equals its entry file's. Consistent by reading; check:migration-registry runs under Lint & Repo Gates on the head (state below). No stale projection exists: spec-changes.json and docs/protocol-upgrade-guide.md are not in the tree on main, and the mother PR touched neither. Right.
  10. Tests and fixtures. The [#21293] block pins the five refusals, the one-measure control, the two-dimension refusals, scatter / radar acceptance, the multi-measure controls, the taxonomy read off the door (refused at two measures with a dimension = metric family plus the five), the message, the dimensionless message word for word as one issue with the parse leg equal to the direct call, the metric family's own single issue, the direct-call legs, DashboardSchema.widgets[] travel, the doc string, and the old name's absence from the barrel with the new export as the lit control. The [#17779] parse leg now runs on the ten types no check refuses, and its claim ("the metric check leaves non-metric types alone") is read off the metric export directly for all fifteen, so no claim was dropped. The catalogue row "two measures on a pie WITH a dimension — out of this rule" became five refusing rows plus accepting rows for scatter, radar and a one-measure pie. Every pin whose premise the ruling falsified moved to the refusal; none was deleted. Right.
  11. Doc string and the generated reference (dashboard.mdx, the values row in both tables): state the single-series rule and read the private constant. Right.

② Semver level

  • Clause-②: yes (narrowing) is right. (narrowing): the accept set at values shrinks — that is the change, and it is BREAKING, so the banner is owed. yes: the claim's own criterion was "yes if the diff adds any export-listing row", and the listing gains checkDashboardWidgetChartMeasureArity (function) while losing the old row (which is why T3 stays silent). The five T2 tells the dev reports on SINGLE_SERIES_CHART_TYPES are members of a module-private narrowing set: false as widenings, and the arm does not rest on them. The gate itself was not re-run here; the dev's reading is consistent with the diff.
  • Changeset .changeset/21293-single-series-multi-measure-refused.md: '@objectstack/spec': minor, a BREAKING banner, and exactly one marker, adr-0087: registered dashboard-widget-single-series-multi-measure-refused. Same shape as the mother rule's changeset (minor plus banner plus registered, under the launch-window convention check-changeset-no-major enforces). Check Changeset is green on the head. Right.
  • Every sentence checked against the diff and the pin. The export swap (one row out, one in, both listings) holds. The renderer binding on the four arms holds at the pin lines above. "Parsed before the change on all five, and on scatter / radar / bar / table" holds by the old code's early return on any non-empty dimensions. "Refused after at widgets[N].values; scatter and radar parse as before" holds. The FROM → TO table's four rows hold: the import row names the right subpath (@objectstack/spec/ui, per export-origins/ui.json) and the same (widget, ctx) signature. "ONE custom issue ... draws one series whatever its dimensions" matches the message text. "No first-party consumer chained the old name; the mirror chains checkDashboardWidgetStageOrder and checkDashboardWidgetMetricMeasureArity only, at the pin and on main" holds. The "Nothing else moves" list holds item by item (one measure; scatter / radar; the multi-measure set with or without a dimension; the dimensionless five word for word and one issue; the metric family; too_small; a type outside ChartTypeSchema). The census figures (496 / 33 / 1 here; 480 / 13 / 1 in objectui) are the dev's measurement and were not re-run; the one spec fixture they name is visible in the diff, and the objectui hit they name is prose in .changeset/11417-dashboard-single-series-dropped-measure.md at the pin.
  • Does the ADR-0087 disposition cover the removed name? Yes. The entry's surface names the export and its rename, and its replacement tells a mirror that chained the old name to import the new one. An export rename is a TypeScript-surface change whose channel is the consumer's compiler (ADR-0087 D8), so the changeset's FROM → TO row is the load-bearing prescription for it; the entry carries it as a rider on the metadata narrowing it registers. One marker is the gate's rule; one entry covering both facets is right.

③ Boundary flags

  1. The amended 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 entry, outside the claim's listed surface. Two sentences in 18.dashboard-widget-dimensionless-multi-measure-refused.ts that this diff made false — the export's name, and "the same seven types WITH a dimension keep accepting several measures exactly as before" — now name the new export (recording the old one) and point the five at the new entry; the registry region was regenerated, not hand-edited. Judged right. The repo's rules for registered entries: the entries README forbids hand edits inside the registry markers (respected) and derives the filename from the id so that two cards editing one entry collide in git (respected — main was merged through os-regen-merge.sh); it does not freeze an entry's prose. ADR-0087's "appended to, never rewritten" is sys_metadata_history, not entry text. Leaving the sentence would have os migrate meta print, in the same protocol-18 hop, that dimensioned pies "keep accepting" beside the entry that refuses them — a declared-but-not-delivered statement Prime Directive chore: version packages #10 forbids. The standing clause the dev followed exists: .claude/agents/os-dev.md:67 (a published defect this round's change makes false must be fixed) and :184–:185 (when the dispatch wording conflicts with that file, the file wins and the conflict is named in the report, never chosen silently). The claim's "stop on breach" was answered by naming the conflict in the report. Two sentences, one hop, no new rule: no scope expansion. Answered.
  2. Commit trailer. The two pushed non-merge commits (c1d4f3dd, 221df98d) end with the model-free trailer pair AGENTS.md prescribes (the session-URL line and the generic co-author line, neither naming a model); the merge commit b3b3c516f9 is os-regen-merge.sh's git merge --no-edit product and carries no trailer; no model identifier appears in the pushed history, the PR title or body, the changeset, the entries or the code. Answered; no deviation stands.
  3. T2 tells on a module-private as const array. Answered: they are not widenings, and the declaration does not rest on them. Escalated as a note for the adopting seat, not filed (this review files nothing): check-widening-tells reads every new bare member of an as const array on packages/spec/src/** as "the accept set gains a value", with a licence only for ledger-table rows; a future PR that adds a module-private NARROWING set with no export change would meet exit 4 under a truthful no (narrowing). That is a gate-imprecision question, not a defect in this diff.
  4. The module-private set beside objectui's SINGLE_SERIES_CHART_FAMILIES. Answered: acceptable. The ruling's "follows it by reference" is the check export (what the mirror chains), and the refusal prints the five, so the mirror never restates them to refuse correctly; objectui's set is renderer-derived and tied to the renderer by its own test (DatasetWidget.singleSeriesMeasures-11417.test.tsx). Two lists exist, in two repositories, kept honest from different ends. Noted for the seat: the mother rule exported its set so the mirror could import rather than restate it, whereas this PR followed the metric family's private precedent (SINGLE_MEASURE_WIDGET_TYPES); exporting later is a free widening (a T3 row, yes (widening)), so nothing is foreclosed.
  5. open_questions is empty; the two out_of_scope_findings are items 3 and 4 above. Of the dispatch's hypotheses, H3 "partly falsified" (no declared order; the builder sorts by id) is correct and changes nothing in the landing.

Check-runs on the head, read over REST at 2026-10-02T14:56Z after the last shard concluded: 35 check-runs, 33 success, 2 skipped, none failed, none still running. The skips are Console Pin Gate (its console path filter does not include packages/spec/src/**; see ①.7 for the pin reading that stands in for it) and Packed-tarball smoke (opt-in). Green on this head: Lint & Repo Gates (carrying check:migration-registry, check:api-surface, check-adr-0087-registration and the widening-tells gate), Check Changeset, Governed Surface Queue Guard, Spec property liveness, Build Core, all six Test Core shards, every Type Check leg, the three Dogfood Regression Gate shards, Dogfood Verify CLI and Temporal Conformance. These conclusions are the gate verdicts this record relies on; nothing was re-run locally. The PR touches no governed surface, so this record is the Clause-② contract review the claim called for, not a Tier S landing record.

Implemented-by: claude/issue-21293-single-series-measure-arity
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Regen-provenance: 5955132369 · b3b3c516f9 → 4435664b26 · git diff --name-only 39a912ea73 4435664b26 minus git diff --name-only db3fee3dc9 b3b3c516f9 → (empty)

domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5952561725 · 2026-10-02T15:15Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 15:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 15:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 32d5769 Oct 2, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21293-single-series-measure-arity branch October 2, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

2 participants