Skip to content

fix(plugin-dashboard): a dimensionless table renders a row of every measure and a dimensionless chart one mark per measure, as the spec states (objectui#11261) - #11278

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11261-dimensionless-measures-render
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11261-dimensionless-measures-render

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11261
Clause-②: no

What this changes

DatasetWidget classified every widget that declares no dimensions as a one-number tile (isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0). So the card's { type: 'table', dataset: 'sales', values: ['revenue', 'cost', 'margin'] } rendered revenue alone. Every door accepts that widget, and the spec's own metric-family arity refusal sends authors there. Its ADR-0087 entry dashboard-widget-metric-family-multi-measure-refused (replacement, read in the installed @objectstack/spec 17.5.0) says: "type: 'table' renders a row of measures, and the chart families (bar / line / area / combo) render one mark per measure".

Per triage 5918660676 (the renderer delivers the stated behaviour; no spec narrowing):

  • Only a metric-family type is a tile by type. A widget with no dimension stays a tile, unless it carries two or more measures on a type the spec names. Those are table / pivot, or a widget whose family (CHART_TYPE_MAP) is bar / line / area / combo (MEASURE_AXIS_CHART_FAMILIES).
  • table / pivot: the existing flat-table branch renders the one row the dimensionless query answers, with every measure as a column. No totals footer is requested or drawn, because that row already IS the [] grand-total grouping.
  • bar / line / area / combo: the single row is transposed into one chart row per declared measure, in declaration order. The category is the measure's headerLabel (the widget's one measure-label channel). The value sits under one series labelled with the existing dashboard.total key, so no new string is minted. A compareTo the widget asks for draws as one overlay series beside each measure. The dimensioned overlay path is byte-identical (same data keys and labels).
  • One measure, no dimension: unchanged. It takes the metric branch on every type and renders the tile, byte-identical. This is pinned for table, pivot, bar, line, area and combo.
  • Types the spec's text does not name (pie, donut, funnel, scatter, column, horizontal-bar, radar, treemap, sankey): measured and unchanged. A dimensionless one with two measures is still a tile. It renders values[0], and objectui#8894's warning still names the dropped measure. This goes back as an open question in the report, and no display semantics are invented for these types.
  • objectui#8894's warning code is untouched. It follows isMetric, so it no longer fires for the widgets that now render every measure. One example in DatasetWidget.unrenderedMeasures-8894.test.tsx moved. Its SUBJECT "covers the dimensionless arm" used a dimensionless bar, which is no longer a tile, so it now uses a pie, which still is. Its assertions are unchanged.

Pins

DatasetWidget.dimensionlessMeasures-11261.test.tsx draws real Recharts marks through the registry, using the DatasetWidget.chartConfigMarks-9203 harness.

Row Kind
the card's table (and pivot) over revenue, cost, margin renders one row with all three, no footer, no warning SUBJECT
a two-measure dimensionless bar draws two .recharts-bar-rectangle, with category ticks Revenue, Cost SUBJECT
the same for combo; line / area plot across the two measure labels SUBJECT
a comparison on a dimensionless bar draws 4 marks (2 + 2 overlay) SUBJECT
a two-measure metric tile with no dimension renders the tile markup and still warns CONTROL
a ONE-measure dimensionless table / pivot / bar / line / area / combo renders the tile markup CONTROL
a dimensionless table / bar queries exactly { dimensions: [], measures: values } (no totals) CONTROL
nine unnamed types stay a tile and warn (measured; the open question) CONTROL

PR objectui#8887's two pins stay green: the drop on a metric-type tile, and the no-sub-caption byte-identity (DatasetWidget.subCaption.test.tsx).

Ablation (restore the old isMetric)

The fix was committed first. node ../objectstack/scripts/ablation-replace.mjs then ran in WRAP mode (the anchor must hit once, the write is verified on disk, and a trap restores it). It replaced the new isMetric line with the old one and ran the new file, the objectui#8894 file and the sub-caption file.

  • The mutation landed: anchor 1 -> 0, blob 9640acd2abcc -> 4bf0c16ab58b.
  • 7 failed / 38 passed: exactly the 7 SUBJECT rows went red. All 18 CONTROL rows, all 8 objectui#8894 rows and all 12 sub-caption rows stayed green.
  • Restored: blob == HEAD 9640acd2abcc, and git diff HEAD is empty (re-checked independently).

Gates (at 7d54b7cfb)

  • pnpm exec vitest run packages/plugin-dashboard/src/: 155 files passed, 1420 passed / 6 skipped. At base f61dab169 it was 154 files, 1395 / 6; the difference is the new file's 25.
  • pnpm --filter @object-ui/plugin-dashboard type-check, with the closure built first by turbo: exit 0. tsc -p tsconfig.test.json --listFiles includes both touched test files.
  • pnpm exec eslint on the 3 changed source and test files: 0 errors. The 11 warnings in DatasetWidget.tsx are the same 11, under the same rules, as at base. The new file has 0 errors and 0 warnings.
  • node scripts/check-changeset-presence.mjs: exit 0. node scripts/check-control-bytes.mjs: exit 0. pnpm check:new-line-citations: 0 new citations.
  • The changeset is markdown, so node scripts/markdown-test-inputs.mjs --audit ran (exit 0), and so did pnpm exec vitest run scripts/__tests__/ (177 files passed, 2 skipped; 5371 passed).
  • Consumers: the four app-shell files that render DatasetWidget, plus PR objectui#11257's two door pins (types, app-shell): 6 files, 84 passed.

Acceptance notes

  • Census: every authored multi-measure dashboard widget in objectstack's examples declares a dimension (git grep over its origin/main at 75519e1c0). objectui's examples/ and apps/ author none. So no shipped example changes rendering.
  • Docs: no doc or README stated the old "no dimensions is a tile" rule (grep over content/docs, the package README and skills/), so none became false. The changeset carries the user-facing description.
  • Observation, not measured through a door, and not filed (no carrier): chartType = CHART_TYPE_MAP[widgetType] ?? 'bar' sends a widget with no type and a dimension to a bar chart, while the spec defaults type to metric. A spec-parsed document always carries a type, so this reaches only an unparsed client draft. This change reads CHART_TYPE_MAP without that default, so it adds no path through it.

Generated by Claude Code

…easure and a dimensionless chart one mark per measure (objectui#11261)

DatasetWidget classified every widget with no dimensions as a one-number
tile, so a dimensionless table or bar/line/area/combo chart with two or more
measures rendered values[0] and dropped the rest, although every door accepts
it and the spec's metric-family arity refusal sends authors there for
"several numbers in ONE widget".

Only a metric-family type is now a tile by type. With no dimension and two or
more measures, a table/pivot renders its one row of measures, and a
bar/line/area/combo chart plots one mark per measure with the measures'
labels on the category axis (the single row transposed under one series
labelled with the existing dashboard.total key). One measure and no dimension
stays a tile on every type, and every type the spec's text does not name
keeps its tile too.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
…measure rendering (objectui#11261)

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/7125-dashboard-empty-state-keys-retired.md

  • names DatasetWidget.tsx → packages/plugin-dashboard/src/DatasetWidget.tsx — edited by this change

    Not touched: table.noRows ('No rows to display') and engine.form.noRows (packages/app-shell/src/views/metadata-admin/i18n.ts, read at widgets.tsx) — two different, same-named keys in different namespaces. Nor the comments in WidgetEmptyState.tsx, DatasetWidget.tsx, ObjectDataTable.tsx and PivotTable.tsx that record WHY three widgets with three strings became one shared empty state; the packs' own comment keeps that rationale and now names the retirement instead of a row that is gone.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with e420df310 (merge-base with origin/main): 3 file(s) changed outside .changeset/, read against 1850 pending declaration(s) that publish a body (2462 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3583.9 KB 3607.4 KB
Main entry chunk (gzip) 150.0 KB 350 KB
Entry file index-CzAG-yET.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 570.09KB 136.40KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.86KB 63.53KB
fields (index.js) 261.11KB 66.26KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 41.23KB 11.53KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.27KB 37.26KB
plugin-designer (index.js) 216.32KB 44.56KB
plugin-detail (index.js) 244.72KB 64.45KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.61KB 44.59KB
plugin-gantt (index.js) 173.03KB 43.07KB
plugin-grid (index.js) 231.28KB 63.53KB
plugin-kanban (index.js) 48.62KB 15.21KB
plugin-list (index.js) 116.42KB 28.89KB
plugin-map (index.js) 23.50KB 7.82KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.05KB 9.67KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7d54b7cfbe5042569dac72c80409314b34e0dc0a
Local-runs: none

Inputs read, and nothing else: card #11261 (body and all 3 comments: triage 5918660676, claim 5919807834, os-dev-report 5920760284); PR #11278 (body, 4-file list, the three-dot diff against main; merge base f61dab169, 2 commits ahead, 8 behind); the head's check-runs with filter=latest; objectui#8894's retriage answer 5918678067 as context for item 5. Files read at the head for context (REST contents, no checkout): DatasetWidget.tsx, the three test files this diff names (dimensionlessMeasures-11261, unrenderedMeasures-8894, subCaption), packages/react/src/hooks/useDatasetDimensionLabels.ts, packages/plugin-dashboard/package.json, and the six dashboard docs (content/docs/blocks/dashboard.mdx, fields/widget-props.mdx, guide/dashboard-filters.md, plugins/plugin-dashboard.mdx, the package README and SKILL). The stale local clone at /home/user/objectui (0eb9f36ac, which does not contain the merge base) was consulted for its ref only and nothing was read from it. Binding direction: triage 5918660676 items 1–5.

Check-runs on the head (the gate verdicts): 43 latest runs, all completed at 22:36Z after a 4-minute foreground wait: 40 success, 3 skipped (Test (coverage), the coverage shard matrix, dependabot), 0 failures. Every derived family is green: Test (8 shards, dist pins), Type Check, Lint, Build & E2E, Changeset Declaration / Bump Policy / Claim Re-read / Fixed Group Check / Overwrite Report, Governed Surface Queue Guard, Control Byte Scan, Line Citation Gate, Spec Main Shape Gate, Inert vi.mock Specifier Check, the doc checks and the skill checks. The dev's local gate numbers (155 files / 1420 passed, type-check 0, eslint 0 errors, changeset-presence 0, control-bytes 0, citations 0, scripts suite 5371) are the dev's claims; the check-runs confirm each family at this head.

① Derived judgments

Item 1 — the classification (DatasetWidget.tsx:493-494). RIGHT. rendersEveryMeasureWithoutDimension = values.length above 1 AND (isTable OR MEASURE_AXIS_CHART_FAMILIES.has(CHART_TYPE_MAP[widgetType] ?? '')), and isMetric = METRIC_TYPES.has(type) OR (dimensions.length === 0 AND NOT that). Read against CHART_TYPE_MAP at the head, the types that leave the tile branch when dimensionless with 2+ measures are exactly: table, pivot (by isTable), and the CHART_TYPE_MAP members that resolve to the four named families: bar, grouped-bar, stacked-bar, bi-polar-bar (family bar); line, spline, step-line (family line); area, stacked-area (family area); combo. Not wider: column maps to column and horizontal-bar to horizontal-bar in CHART_TYPE_MAP, neither is in the set, so both still take the tile branch, as do pie / donut / funnel / pyramid / scatter / bubble / radar / treemap / sankey, an unknown type, and a typeless widget (CHART_TYPE_MAP[''] is undefined, so the ?? '' misses the set; the chartType line's ?? 'bar' default is not consulted here, which is right). METRIC_TYPES is untouched, so the metric family is a tile by type, with or without dimensions. Coverage note, no action: the six family aliases (grouped-bar, stacked-bar, bi-polar-bar, spline, step-line, stacked-area) leave the tile through the same expression and are pinned by no row; the four canonical members are.

Item 1 — the transposition (DatasetWidget.tsx:1712-1728). RIGHT. measuresOnAxis = dimensions.length === 0 is reachable only past the isMetric and isTable returns, so only a bar / line / area / combo family with 2+ measures reaches it (a dimensionless widget that is not one of those was returned as a tile above). It builds one row per declared measure in declaration order: category headerLabel(m) under the private key __measure, value state.rows[0]?.[m] under __value — the same rows[0] the tile reads, the [] grouping the dimensionless query answers. The single series is { dataKey: '__value', label: tt('dashboard.total', 'Total') }: dashboard.total is the key the flat table's footer already reads at line 1554, so no locale key is minted, and the label is the grouping the row is. The two private keys cannot collide with a measure name because a transposed row carries only those keys. Overlay: a compared measure's measure__compare value rides under __value__compare; comparedSeries on this path is the one transposed series (when any measure compared), and the overlay takes compareColumn('__value') with the primary's mark and axis. The dimensioned path is byte-identical: comparedSeries = comparedValues.map(m to { dataKey: m, label: headerLabel(m) }) feeds the same dataKey / label pair the old inline map produced. Pinned: bar and combo draw 2 .recharts-bar-rectangle with ticks Revenue, Cost; line / area plot across the two labels; the compare row draws 4 marks. Observations, no action: authored chartConfig.series entries keyed by a measure name bind to nothing on this path (the one derived series is keyed __value), which is what they bound to on the tile they replaced, so nothing regresses; and canDrill is false here (drillDims is empty and no drillRanges come back for a dimensionless query), so no drill semantics are added on the transposed chart or the one-row table.

Item 1 — the flat-table branch. RIGHT. wantsFlatTotals = isTable AND NOT isMatrix AND dimensions.length above 0 AND limit == null is a context line (unchanged at the head), so a dimensionless table / pivot asks the executor for no totals grouping and draws no footer; isMatrix needs 2 dimensions, so pivot takes the flat table. One row, its td cells the measures, its th cells the measure headers. Right for a row that IS the grand total: a [] grouping under a [] selection would print the same row twice, and the colSpan={dimensions.length} footer cell would be a zero span; the footer's comment was re-grounded on wantsFlatTotals rather than on "a zero-dimension widget returns above", which is now the true reason. Pinned for table and pivot: one tbody row 510000 / 120000 / 390000, headers revenue / cost / margin, no dataset-table-total-row, no objectui#8894 warning.

Item 2 — one measure and no dimension. RIGHT. With values.length of 1 the new conjunct is false and isMetric collapses to METRIC_TYPES.has(type) OR dimensions.length === 0, the old expression, for every type. Pinned byte-identical (REVENUE_TILE) for table, pivot, bar, line, area, combo.

Item 4 — the query. RIGHT. queryDataset(dataset, { dimensions, measures: values, ... }) is untouched; totalsGroupings is undefined for the dimensionless table (above); compareTo forwards for a bar family exactly as it did for the tile it was (chartTypeIgnoresCompareTo('bar') is false). Pinned: a dimensionless table and bar each query exactly { dimensions: [], measures: values }. useDatasetDimensionMeta is now enabled for these widgets, but it returns before any read when dims.length === 0 (useDatasetDimensionLabels.ts:134), so no object-schema read is added.

Item 4 — the ablation leg, against what the new file pins. CONSISTENT. The new file holds 25 rows: 7 SUBJECT (table + pivot via it.each, bar, combo, line + area via it.each, the compare overlay) and 18 CONTROL (the metric tile, six one-measure types, two query rows, nine unnamed types). Each SUBJECT asserts something the old isMetric makes false (a tile has no tbody row and draws no Recharts mark), and each CONTROL is green on both sides by construction. The dev's WRAP-mode ablation reports 7 failed / 38 passed over 45 rows: 25 + the 8894 file's 8 + the sub-caption file's 12 = 45, and 18 + 8 + 12 = 38, so the totals and the red set are consistent with the files at this head. PR objectui#8887's two pins (subCaption.test.tsx lines 107 and 178) are both on type: 'metric', which METRIC_TYPES keeps a tile, so they stay green through this change; the head's Test shards confirm. The ablation itself is the dev's run and was not re-run here.

Item 3 — the nine CONTROL rows for the unnamed types. RIGHT. Each asserts the measured state (the REVENUE_TILE markup and the warning naming "cost") under a title reading measured, open question, with a comment that the maintainer's answer is EXPECTED to change the row. They pin behaviour and assert no semantics; the display semantics are returned as Q1, as item 3 ordered.

No accept set or public type moves. RIGHT. The diff touches DatasetWidget.tsx (module-private const additions, no export change), one new test, one changed test example, and one changeset; no schema, no packages/types, no door. Governed Surface Queue Guard and the changeset checks are green. The header docblock and the inline comments now state the new rule; no other in-file comment states the old one (line 519's "a pie widget with no dimensions renders as a metric tile" is still true).

② Semver level

.changeset/11261-dimensionless-measures-render.md: '@object-ui/plugin-dashboard': patch (the package's name at the head, version 17.6.0). RIGHT. The diff delivers a rendering the installed spec already states for widgets every door already accepts, and moves no API surface: patch. The changeset text names the spec sentence, the two arms, the unchanged one-measure case, the nine unnamed types and the unchanged query, which is what the diff publishes. Changeset Declaration, Changeset Bump Policy and Changeset Claim Re-read are green.

Clause-②: no. RIGHT, and consistent: the claim 5919807834 says Clause-②: no, the PR body's second line says Clause-②: no, and the diff moves no schema, published type or accepted set.

③ Boundary flags

Deviation, the objectui#8894 file: one SUBJECT ("covers the dimensionless arm") moved from a dimensionless bar to a dimensionless pie, with its assertion (measureWarning() contains "cost") unchanged and the warning code (DatasetWidget.tsx:766-776) untouched by any hunk. The file's intent survives: the row exists to prove the signal is keyed off the drop and not off the type name, and a pie is a non-metric type that is still a tile on that arm at this head; the title was narrowed from "any widget type" to "a non-metric widget type", which is what is now true. The file's header still describes the metric tile it is about ("must not start rendering them"), and its three CONTROL rows, including the byte-identity of a three-measure metric tile, are green at the head. Item 5 holds.

Q1 (the nine unnamed types) and Q2 (the dispatch's item 5 premise that the warning would afterwards fire only for metric-family tiles, measured false). Neither is answered on this card, and that is the ordered outcome: item 3 says a claim that finds them dropping measures "returns an open question" and invents no semantics. Carrying both to objectui#8894 at ACCEPT with pm:retriage, rather than holding this PR, is RIGHT: holding would tie the delivery of the spec-named types to a decision triage reserved for itself, and 8894 is the carrier because its plan step C in 5918678067 ("narrow it to the metric family, arm 2 only, after objectui#11261 lands") rests on the premise Q2 measures false. At this head the warning still fires, truly, for a dimensionless multi-measure pie / donut / funnel / scatter / column / horizontal-bar / radar / treemap / sankey (the moved SUBJECT and the nine CONTROL rows pin it), so a narrowing to the metric family would re-silence nine accepted types; Q2 is the same decision as Q1 and belongs beside it. Of the retriage answer's next-claim pins, "a dimensionless table shows no warning" is delivered here. The PR must not claim the warning is now metric-only, and it does not: the body says it "no longer fires for the widgets that now render every measure", the changeset does not mention the warning, and no comment in the diff says metric-only.

The out-of-scope observation (a typeless widget with a dimension routes to bar through chartType's ?? 'bar', reachable only by an unparsed draft because the spec defaults type to metric): recorded in the report and the PR's acceptance notes, not filed. Right for an observation measured through no public door; and this diff reads CHART_TYPE_MAP without that default, so it adds no path through it (verified at line 493).

The PR body at this head, sentence by sentence: the base-state description, the door and spec statements, the classification, the two rendering arms, the one-measure case, the nine-type list, the warning statement, the moved example, the pin table, the gates, the census line and the docs line are true or, for the local gate numbers and the objectstack census, the dev's claims outside this input set that nothing here contradicts. The docs line was checked: none of the four dashboard docs, the README or the SKILL at the head states the old "no dimensions is a tile" rule. One sentence is not true as written: in "Ablation", "all 18 CONTROL rows, all 9 objectui#8894 rows and all 11 sub-caption rows stayed green" — at this head DatasetWidget.unrenderedMeasures-8894.test.tsx holds 8 rows (five SUBJECT, three CONTROL; the diff removes none) and DatasetWidget.subCaption.test.tsx holds 12 (1 + 5 + 1 + 1 + 2 + 1 + 1). One row is attributed to the wrong file; the total (38 green of 45) and the red set (exactly the 7 SUBJECT rows) are unaffected, and both files are green on the head's shards. Named, not blocking.

Implemented-by: claude/issue-11261-dimensionless-measures-render
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 22:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit b4333ab Sep 30, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11261-dimensionless-measures-render branch September 30, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants