Skip to content

feat(spec)!: a dimensionless dashboard widget declares two or more measures only on a type that renders them — refuse pie / donut / funnel / scatter / radar / treemap / sankey at values (objectui#8894 ruling D) - #21053

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20958-dimensionless-multi-measure-refused
Oct 1, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20958
Clause-②: yes (narrowing)

Applies maintainer ruling D's principle (objectui#8894, 5643392537: 「协议不正确的应该先修改协议」) to the chart types: a dashboard widget with no dimension may declare two or more measures only on a type that renders them. The metric-family narrowing (09e16a574) was the ruling's first application; this is the same principle on pie, donut, funnel, scatter, radar, treemap and sankey. The direction was not re-opened here.

What changed

  • DASHBOARD_WIDGET_MULTI_MEASURE_TYPES (new export, packages/spec/src/ui/dashboard.zod.ts) is the one list: table, pivot, bar, column, horizontal-bar, line, area, combo, typed as const satisfies readonly ChartType[]. The check, its refusal text and the values doc string all read it. No second literal of the set exists in code (a test pins one export const declaration).
  • checkDashboardWidgetDimensionlessMeasureArity (new export) is an object-level check chained onto DashboardWidgetSchema by identifier, right after checkDashboardWidgetMetricMeasureArity. It refuses when dimensions is absent or [], values has two or more members, and type is a declared ChartTypeSchema member outside the multi-measure set and outside the metric family. It emits ONE custom issue at values naming the widget id, the count and the type, lists the multi-measure set (read from the constant), and steers to type: 'table' (a row of measures), a bar-family type (bar / column / horizontal-bar), or one widget per measure.
  • The metric family is excluded on purpose. checkDashboardWidgetMetricMeasureArity already refuses a second measure on metric / kpi / gauge / solid-gauge / bullet (and a typeless widget, which defaults to metric) at any dimensionality. Firing here too would turn that existing refusal from one issue into two. So every type outside the multi-measure set is refused dimensionless by exactly one of the two checks, and the metric refusal is byte-identical (pinned: same message as the metric export's direct call, one issue).
  • A type the spec does not declare is not judged by the export. Through this door it is unreachable (zod's invalid_value on type aborts object-level checks). objectui's mirror, however, re-points type at a wider enum (list, custom, component widget types) with no default, so the export returns on anything outside ChartTypeSchema.options. It refuses exactly what the spec's door refuses.
  • ADR-0087 semantic entry packages/spec/src/migrations/entries/semantic/18.dashboard-widget-dimensionless-multi-measure-refused.ts, plus the gen:migration-registry lap. Nothing was hand-edited between the markers. Its acceptance criteria name the doors as measured (below), state that the TODO cannot name per-document measures, and list what is and is not refused.
  • Changeset .changeset/20958-dimensionless-multi-measure-refused.md: minor, BREAKING banner, the ADR-0087 marker registered dashboard-widget-dimensionless-multi-measure-refused, the Clause-② line, and a FROM → TO table.
  • Regenerated: api-surface/ui.json and export-origins/ui.json (+2 exports each, 0 removed), and the values row of content/docs/references/ui/dashboard.mdx.

Measured before building (the dispatch's three hypotheses)

Premise at the base 05be35259 (built dist, DashboardWidgetSchema.safeParse): a widget with no dimensions and values: ['a', 'b'] returned ACCEPT on every one of the seven types. The metric family returned REFUSE values:custom at every dimensionality. bogusProp on the same widget returned REFUSE unrecognized_keys, which is the lit control.

H1, attachment points. In this repo the metric check is attached at exactly one place: the .superRefine chain of DashboardWidgetSchema. DashboardSchema.widgets, ObjectStackDefinitionSchema.dashboards and the dashboard metadata-type schema all reach it through that schema (git grep over packages/**: no other construction of the widget schema, no .shape mirror in this repo). The new check is chained at the same point. The "editor face" the precedent entry names is objectui's .shape mirror, which lives outside this repo (see Downstream).

H2, every door by the schema alone. The subject is a dimensionless two-measure widget. The controls are the same body as table / bar, and a pie carrying one dimension.

door subject (pie / donut / sankey / radar) controls
DashboardSchema, getMetadataTypeSchema('dashboard') REFUSE widgets.0.values:custom ACCEPT
ObjectStackDefinitionSchema REFUSE dashboards.0.widgets.0.values:custom ACCEPT
defineStack throws defineStack validation failed (1 issue), naming the widget returns
os validate (a config built by defineStack) exit 1 at the same defineStack issue get past the schema step; exit 1 later on an unrelated docs-namespace rule of the probe app
metadata save path (saveMetaItem, ACTIVE and DRAFT) 422 INVALID_METADATA at widgets.0.values, 0 rows persisted ACTIVE save succeeds, 1 row

No consumer-side copy exists. The probes were one-shot: a scratch config and a scratch test file, both deleted, and git status was clean afterwards.

H3, fixtures and stored rows. I ran a census at 05be35259 over every tracked .ts / .tsx / .js / .json / .md / .mdx. 469 brace-local literals carry values: [...]. 23 of them are on one of the seven types, and every one has one dimension and one measure. That leaves 0 dimensionless multi-measure widgets on the seven. Lit control: the same scan classifies 40 dimensionless multi-measure literals of other kinds (reports, metric-family test fixtures, dataset selections). An objectui checkout at 1263e40 gives 8 seven-type literals and 0 dimensionless multi-measure. No fixture needed triage, and the stop condition did not fire.

Verification

All of the following ran at e1494c720, after merging origin/main 9b0de7de7 through scripts/pm/os-regen-merge.sh. On that merge, api-surface/ui.json and export-origins/ui.json were regenerated from the merged tree and now hold main's new exports plus this branch's two.

Reverse verification ran at the committed 1a0e75a6d through scripts/ablation-replace.mjs. It deleted only the .superRefine(checkDashboardWidgetDimensionlessMeasureArity) attachment, so the function stayed declared. The subject is imported from src through a relative path, so no dist was involved.

anchor x1 -> x0 · marker x0 -> x1 · blob b33ae8c4bd64 -> 209e3d201d54 (mutation landed)
RED   vitest exit=1   27 failed | 274 passed (301)
restored blob b33ae8c4bd64 == HEAD blob, git diff HEAD empty
GREEN full spec suite at 1a0e75a6d after the restore: 584 files, 17284 passed, exit 0

The 27 red tests: the 7 refusals, the 7 explicit-[] refusals, the message, travel and attachment pins, and in the exports catalogue 8 parity rows plus the count and bijection legs. A first attempt with the replacement ); was refused by the tool itself, because that string is a substring of the anchor and its count could not rise. The mutation was restored, and the attempt is reported here rather than counted.

Tests (each is the package's own vitest run against the rebuilt closure; the filter direction is per package, not the full ...@objectstack/spec sweep):

package result at e1494c720
@objectstack/spec (--project local) 584 files, 17290 passed, 1 todo, exit 0
@objectstack/spec typecheck (tsc + scripts + test layer) exit 0; test-layer ledger held at 52 files, 248 errors, 136 signatures
@objectstack/metadata-protocol 195 passed, 3 skipped (198 files); 2896 tests passed, 19 skipped; exit 0
@objectstack/lint 117 files, 5446 tests, exit 0
@objectstack/service-analytics 150 files, 3458 tests, exit 0
@objectstack/sdui-parser 13 files, 224 tests, exit 0
@objectstack/platform-objects 59 files, 948 tests, exit 0
@objectstack/objectql (--project local) 348 files, 6821 tests, exit 0
@objectstack/metadata 56 files, 836 tests, exit 0
@objectstack/cli (--project unit; the integration tier is declared to CI because no spawn entry or integration file is touched) 239 files, 3391 tests, exit 0

Before the merge, the spec suite and the metadata-protocol, lint, sdui-parser and platform-objects suites were also green at 1a0e75a6d. The 3 skipped metadata-protocol files were skipped on both runs.

Generated artefacts: pnpm --filter @objectstack/spec check:generated reports "All 15 generated artifacts are up to date". check:api-surface reports "public API surface + factory signatures unchanged" after regeneration.

Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at e1494c720 derives 113 commands, and all 113 ran with exit 0. --ran reconciliation: "113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero, all 113 recorded an exit code and none of them is 3)". Three of them first answered something other than a verdict, and each was re-run to a real exit 0:

  • check:skill-examples (exit 3, client-react not built)
  • check:dual-build-cjs-loads (exit 3, eight packages had no dist)
  • check:query-options-erasure (killed by my own 240-second per-command cap; 278 s uncapped)

The list includes check-adr-0087-registration --base origin/main, which reads [BREAKING+bang+clause-②-narrowing] registered dashboard-widget-dimensionless-multi-measure-refused. It also includes check-changeset-no-major, check-empty-changeset, check:nul-bytes, check:pm-widening-tells, check:spec-docblock-symbol-anchors, check:issue-citations, check:doc-authoring and check:widget-option-census.

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 5 touched .ts files. The JSON reports 5 files, 0 errors and 0 warnings, at e1494c720.
  • Population: eslint --print-config returns a rule set for each touched file, so none is ignored.
  • Invariance: eslint.config.mjs enables no type-aware linting. parserOptions.project and projectService are null for these files, and the config's own comment states it never enables them. This diff therefore cannot move a verdict on any untouched file.

Acceptance notes

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

  • Whether the seven types render several measures WITH a dimension is unmeasured here and outside the card. The rule refuses only the dimensionless shape; the entry and the docblock say so rather than imply it. Carrier: none.
  • No STEP18_RATIONALE fragment was added. This follows the precedent (09e16a574 added none). The step currently holds 52 fragments against 254 major-18 semantic entries (this one included), and fragments accompany retirements that carry conversions. The dispatch's hot-file note describes the fragment mechanism; nobody else's fragment was touched.
  • Two new exports, not one. The card names one exported constant. The check itself is exported too, because object-refinement-check-exports.test.ts pins one export per custom check the schema carries, and objectui's mirror chains the check by name. Clause-②: yes (narrowing) covers both.
  • scripts/ablation-replace.mjs refuses a replacement that is a substring of its anchor, because the replacement count cannot rise. This is conservative and correct; the remedy is a distinctive marker. Carrier: none.

Downstream: objectui's mirror (not in this PR)

@object-ui/types builds DashboardWidgetSchema from a .shape spread, so it runs only the checks it chains. Until it chains this one, its editor keeps accepting a dimensionless two-measure pie, and the author meets the refusal at publish. The entry says so. For the coordination child:

  • Import checkDashboardWidgetDimensionlessMeasureArity (and, for any UI that lists the set, DASHBOARD_WIDGET_MULTI_MEASURE_TYPES) from @objectstack/spec/ui.
  • Chain it in packages/types/src/zod/complex.zod.ts after the existing .superRefine(checkDashboardWidgetMetricMeasureArity).
  • Add it to the attached list of the DashboardWidgetSchema (complex.zod.ts) row in packages/types/src/__tests__/spec-object-refinements-7715.test.ts. That census reads the spec object's check count, so it reddens by row name at the spec bump until this lands.
  • Refusal shape: ONE issue with code: 'custom' and path: ['values']. The message starts "Widget ID declares N measures with no dimensions on type: 'T'", where ID is the widget id, N is the count and T is the authored type.

Generated by Claude Code

claude added 7 commits October 1, 2026 00:43
… widget whose type declares no multi-measure rendering

DASHBOARD_WIDGET_MULTI_MEASURE_TYPES is the one list (table, pivot, bar,
column, horizontal-bar, line, area, combo); checkDashboardWidgetDimensionlessMeasureArity
refuses the seven remaining non-metric chart types at values when dimensions
is absent or empty, naming the widget id, the count and the type.

Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU
Co-authored-by: Claude <noreply@anthropic.com>
…R-0087 semantic entry

Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU
Co-authored-by: Claude <noreply@anthropic.com>
…NG, ADR-0087 registered

Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU
Co-authored-by: Claude <noreply@anthropic.com>
… reference for the two new exports

Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU
Co-authored-by: Claude <noreply@anthropic.com>
…tree — main's exports and this branch's two

Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 4 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 — 137 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 d1f8ce8658b937d3685df62b04e2e478430b8e83 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 45a8676ff86f5095d47cc092b6a09c04cc56d6a6 — the merge of head e1494c7206fcd7d16d5446fbb98dce842e58dff3 into base d1f8ce8658b937d3685df62b04e2e478430b8e83, 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 45a8676ff86f5095d47cc092b6a09c04cc56d6a6 && git checkout 45a8676ff86f5095d47cc092b6a09c04cc56d6a6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d1f8ce8658b937d3685df62b04e2e478430b8e83 e1494c7206fcd7d16d5446fbb98dce842e58dff3 && git checkout -B drift-repro d1f8ce8658b937d3685df62b04e2e478430b8e83 && git merge --no-ff e1494c7206fcd7d16d5446fbb98dce842e58dff3

node scripts/docs-audit/affected-docs.mjs --json d1f8ce8658b937d3685df62b04e2e478430b8e83

⚠️ 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 d1f8ce8658b937d3685df62b04e2e478430b8e83 → 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: e1494c7206fcd7d16d5446fbb98dce842e58dff3
Local-runs: none

Inputs read: card #20958 (body, the claim comment and the os-dev-report), PR #21053 (body, the nine-file list, the net diff against main), the files at the head through git show (reading only), and the check-runs on the head. Nothing was built, run or re-run.

① Derived judgments

Accept-set changes the diff implies, each judged:

  1. Narrowing at DashboardWidgetSchema.values — a widget whose dimensions is absent or an empty array, whose values carries two or more members, and whose type is pie, donut, funnel, scatter, radar, treemap or sankey is refused with ONE custom issue at values, naming the widget id, the count and the authored type. Right. This is the card's scope item 1 exactly, and every card pin is covered by a test: the seven refused (also with an explicit dimensions: []), the same seven accepted with one dimension or with one measure, and every member of the multi-measure set accepted dimensionless with three measures (the control).
  2. The metric family (metric / kpi / gauge / solid-gauge / bullet, and a typeless widget, which defaults to metric) is stepped over by the new check. The card's prose says "outside the multi-measure set", which read literally would include the family; the card's pin "the metric family's existing refusal is unchanged" decides it. Right: firing on the family would turn one issue into two on the same values, and the diff pins the family's refusal byte-identical to the metric export's direct call. The taxonomy test pins seven plus five plus the eight-member set equal to ChartTypeSchema.options; I counted the enum at the head: twenty members, so the partition is exhaustive and a new chart type reddens that test rather than slipping past both checks.
  3. New public export DASHBOARD_WIDGET_MULTI_MEASURE_TYPES (table, pivot, bar, column, horizontal-bar, line, area, combo, as const satisfies readonly ChartType[]). Right: the card's "one exported constant". All eight are declared ChartTypeSchema members, ChartType is exported from chart.zod.ts at the head, and ui/index.ts ships dashboard.zod wholesale, so the barrel carries it. The check, the refusal text and the values doc string all read it. At the head the only other literal of the set in the repo is the expectation in dashboard.test.ts that pins the constant's content, which is a test reading, not a second list any code consults. The card's "no second list" holds.
  4. New public export checkDashboardWidgetDimensionlessMeasureArity (arity 2, chained by identifier after checkDashboardWidgetMetricMeasureArity). The card names only the constant as an export. Right anyway: the card's "Then" section requires objectui's mirror to re-attach the check as it did for the metric family, which needs the function exported; object-refinement-check-exports.test.ts pins one export per custom check on the schema; and the metric-family precedent commit 09e16a574 exported its check for the same reason. The changeset names both exports on its Clause-② line.
  5. Export behaviour off the spec's door. The export returns on a type outside ChartTypeSchema.options and on a typeless widget (the default is a family member). Right: through the door the default is applied before object-level checks (ChartTypeSchema.default('metric') at the head) and an unknown type aborts on invalid_value, so the export refuses exactly what the door refuses when objectui chains it onto its wider, default-less enum. dimensions is z.array(z.string()).optional(), so a non-array dimensions is the field's own invalid_type through the door and the check's early return there is unreachable, which is harmless.
  6. The values doc string gains "with no dimensions, two or more only on" followed by the constant joined with slashes; the two DashboardWidget rows of content/docs/references/ui/dashboard.mdx carry the same text. Right: a shipped doc string change consistent with the rule, regenerated rather than hand-edited.
  7. ADR-0087 semantic entry 18.dashboard-widget-dimensionless-multi-measure-refused.ts, plus the registry lap. Right: the registry hunk sits inside the os-generated semantic:18 region (the markers at the head wrap it), sorted by id between dashboard-widget-chart-config-structure-refused and dashboard-widget-metric-family-multi-measure-refused, and the entry's surface, replacement, reason and acceptance criteria describe the same rule the check implements (the seven, absent-or-empty dimensions, two or more values, one custom issue at values, the family stepped over, the type refusal alone on an unknown type).
  8. No STEP18_RATIONALE fragment. Right: the registry's own docblock reads "one fragment per retirement"; this entry is a semantic narrowing with no conversion, and the precedent commit 09e16a574 added none either (its registry diff is one hunk in the semantic region and touches no fragment line).
  9. api-surface/ui.json and export-origins/ui.json gain exactly the two symbols above and remove nothing. Right, and consistent with the barrel.
  10. No consumer fixture moves. The card's census and the dev's census both read zero authored dimensionless multi-measure widgets on the seven; the file list holds none. Right.
  11. File surface. The nine files are the claim's surface and nothing else: the schema file, its two tests, one semantic entry plus the regenerated registry, the two regenerated spec artefacts, the regenerated reference page, one changeset. Right.

One non-blocking reading: the refusal message hardcodes "bar / column / horizontal-bar" as the bar-family steer. That is the card's scope item 2 (steer to table or a bar-family type), not a restatement of the multi-measure set, which the same message prints from the constant.

② Semver level

  • Changeset .changeset/20958-dimensionless-multi-measure-refused.md: "@objectstack/spec": minor, a feat(spec)!: summary, a BREAKING banner, an adr-0087 marker reading registered dashboard-widget-dimensionless-multi-measure-refused, and a Clause-②: yes (narrowing) line followed by prose. Matches what the diff publishes: the surface gains two exports (the minor floor) and the accept set narrows (breaking-ness carried by the banner and the ADR-0087 disposition under the launch-window convention, where check-changeset-no-major refuses major). The shape is the metric-family precedent's changeset 17779-dashboard-metric-family-single-measure.md line for line.
  • The Clause-② parser (scripts/pm/clause2-line.mjs) reads the value as the first token after the key and the arm as the first word inside the parenthetical, so the trailing prose on the changeset's line does not unseat narrowing.
  • Clause-②: yes (narrowing) on the PR body, on the changeset and on the card's claim; all three agree.
  • Check Changeset concluded success on the head.

③ Boundary flags

Dev deviations (the os-dev-report lists four), each answered:

  1. Two exports, not one. Answered right in ①.4; no escalation.
  2. No STEP18_RATIONALE fragment despite the dispatch's hot-file note. Answered right in ①.8: fragments accompany retirements with conversions, the precedent added none, and the step18 rationale merge test asks only that the fragments present stay sorted; no escalation.
  3. Metric family excluded though the card says "outside the multi-measure set". Answered right in ①.2: the card's own pin forbids a second issue on the family; every type outside the set is still refused dimensionless by exactly one check; no escalation.
  4. origin/main moved after the merge lap and was not chased. The PR reads mergeable at the time of this review; the seat re-reads the base and every check before landing. Not a contract matter; no action.

open_questions: none listed, none found.

Out-of-scope findings (two, carrier none): whether the seven types render several measures WITH a dimension is outside the card, and the entry states it as unanswered rather than implying it is covered — agreed, no escalation; the ablation tool refusing a replacement that is a substring of its anchor is a tooling observation — agreed, nothing owed here.

Check-runs on the head, read at 2026-10-01T03:46Z: 32 runs, 15 concluded success (Auto Label, Build Core, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · source gates, filter), 2 concluded skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), and 15 still in progress at the read (Test Core shards 1 to 6, Dogfood Regression Gate shards 1 to 3, Dogfood Verify CLI, Lint & Repo Gates, Temporal Conformance (live PG + MySQL), Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace). No run concluded failure. A run still in progress is not a FAIL; the seat re-reads every check before landing.

Implemented-by: claude/issue-20958-dimensionless-multi-measure-refused
Reviewed-by: session_017VaLJnYwhPsanVCe9dMCJU

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 04:02
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 04:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 11d28c1 Oct 1, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20958-dimensionless-multi-measure-refused branch October 1, 2026 05:17
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