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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
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