Skip to content

feat(plugin-report): a summary report with two or more row levels draws per-group subtotals - #28376

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-csgfso
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-csgfso

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #27975

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:summary 报表的 rows 有两层及以上时,按外层分组画:每组一行组头(外层值只写一次)、组内明细行(外层列留空)、一行「小计」;三层时逐层嵌套。小计取服务端按前缀分组算出的值,浏览器不做加总。
  • 为什么改:之前多层汇总报表只要总计,外层值在每行重复、没有分组小计(如 HotCRM「按负责人和阶段的商机漏斗」)。triage 6539817671 裁定为默认行为、不新增键。
  • 风险与回滚:所有已有的两层以上 summary 报表下次渲染即出现小计行(已在 changeset 写明);单层 summary、tabular、matrix 不变;服务端不回前缀分组时退回原平铺表。回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-report/src/DatasetReportRenderer.tsx 的 DatasetReportTable(totals 请求与 lines 渲染)和 nestSummaryLines / spanDrillRanges;新测试 DatasetReportRenderer.summarySubtotals-27975.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-report/src/__tests__/DatasetReportRenderer.summarySubtotals-27975.test.tsx,预期 16 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

Renderer (ui/packages/plugin-report/src/DatasetReportRenderer.tsx, DatasetReportTable).

  • A summary table with two or more rows levels asks the dataset for every proper prefix of rows plus the grand total through totals.groupings: [[a], []] for [a, b], [[a], [a, b], []] for [a, b, c] (summarySubtotalPrefixes). The server already re-runs the selection per grouping, which is what the matrix path relies on.
  • When every prefix grouping is answered, the body is drawn as nested groups (nestSummaryLines): per outer group a header row naming the value once, its inner lines (the outer columns left blank), and a Subtotal row carrying the server's pre-aggregated row for that prefix. The subtotal lookup relabels the totals rows the same way the display rows are relabeled and matches by bucket id, the matrix's rule. Nothing is summed client-side.
  • Groups keep the order of their first row in the laid-out rows, so the declared-option layout and an authored order both hold; rows of one group that an order by a measure returns apart are drawn together under it.
  • Drill: an inner row drills by every dimension, as before. A subtotal row drills by the prefix only: groupKey and the raw-value filter take the prefix dimensions, and its ranges are the span of its group's ranges (spanDrillRanges): a prefix range passes through, an inner time bucket widens to the group's buckets (already narrowed to any report period by the server), and a key some row lacks (the empty bucket) is dropped.
  • The export snapshot writes the grouped table exactly as drawn.
  • Unchanged faces: a one-level summary (still [[]], grand-total footer only), tabular (no totals), a matrix with columns, and a matrix without columns (it still degrades to this table with the grand total only; withSubtotals is passed for summary alone, top level and joined blocks). An answer missing any prefix grouping (an older server) draws the plain table with the outer value repeated.

i18n. report.subtotal added to all ten packs, reusing each pack's existing masterDetail.subtotal wording.

Changeset. .changeset/27975-summary-report-subtotals.md, patch for @object-ui/plugin-report and @object-ui/i18n, with both declaration lines and the visible default change named.

Tests

  • New ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.summarySubtotals-27975.test.tsx, 16 cases: the totals request for two and three levels; the grouped body for two levels; the server subtotal placed where the rows would sum differently (an avg); three-level nesting; grouping under an interleaving authored order; a joined summary block; drill on a subtotal (prefix only), on an inner row (every dimension), over a time-bucketed inner dimension (span), and over a group holding the empty bucket (range dropped). Controls: older server without the prefix grouping, no totals at all, one-level summary, two-level tabular, matrix without columns.
  • DatasetReportRenderer.export-25873.test.tsx: one new case, the CSV of a two-level summary equals the drawn table cell for cell.
  • Ablation (one-off, no permanent file): node scripts/ablation-replace.mjs replaced withTotals && withSubtotals ? summarySubtotalPrefixes(rows) : [] with an empty list (anchor 1 → 0, blob dc35dc45 → 4b92969b). Predicted: every grouped-face case red, the controls green. Measured over the two files: 11 failed, 18 passed. Restore verified by the tool: blob back to dc35dc45, git diff HEAD empty.

Acceptance notes

  • Live measure (showcase, fresh stack, head before merge ddbba1e8). The showcase ships no two-level summary, so one was saved into the throwaway --fresh database through PUT /api/v1/meta/report/showcase_hours_by_status_priority (rows: [status, priority], values: [est_hours]); no examples/** edit. The Console dev server rendered it from this branch's source.
    • Before (the origin/main renderer swapped in, then restored byte-identical): request totals.groupings: [[]]; 8 rows, each repeating its status (Backlog · Low · 12.0, Backlog · Medium · 18.0, …), footer Total 244.0.
    • After: request totals.groupings: [["status"], []]; the server answered totals[0].dimensions: ["status"] with 5 rows (Backlog 30, Done 24, In Progress 100, In Review 54, To Do 36) and totals[1] the grand total 244. Drawn: Backlog header, Low 12.0, Medium 18.0, Subtotal 30.0, … Done header, Medium 24.0, Subtotal 24.0, Total 244.0, in the status field's declared order.
    • Clicking the Backlog subtotal opened the drill drawer titled Backlog with both Backlog tasks (Low and Medium).
  • Groups under an order by a measure are ordered by their first row, not by their subtotal. Ordering groups by subtotal would need the server to order the prefix grouping, which it drops for totals by design. Noted, not filed.
  • The subtotal label is the bare word Subtotal (report.subtotal), placed in the column of the level it closes; it does not repeat the group's value. Noted.
  • Assignee and reviewer are not set on this PR. The dispatch frame bars the dev from writing them; the seat owns that.
  • Serial constraint with feat(plugin-report): a report chart draws its seriesBy split #28305 (same file, chart path): a local git merge-tree of this branch against feat(plugin-report): a report chart draws its seriesBy split #28305's head 39eb423b is clean. feat(plugin-report): a report chart draws its seriesBy split #28305 is still open; this branch merges origin/main again after it lands.

Verification

All readings on head 91ccb5a5 (after merging origin/main) unless named.

Affected package tests and type-checks
  • pnpm --dir ui exec vitest run packages/plugin-report/ packages/i18n/ under os-verify-lock: Test Files 124 passed (124), Tests 1728 passed, 20 skipped. VERDICT command-exit 0.
  • pnpm --filter @object-ui/plugin-report --filter @object-ui/i18n run type-check (after building the plugin-report dependency closure with declarations): VERDICT command-exit 0.
  • eslint over the three changed source/test files: 0 errors; the 5 warnings are on lines this diff does not touch.
  • pnpm check:new-line-citations (ui): 0 new citations.
Derived gates (dispatch-gates --commands, reconciled with --ran)
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran on head ddbba1e8: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit code 0. node scripts/check-sdui-manifest.mjs and its --self-test: exit 0. pnpm --dir ui exec vitest run scripts/: Test Files 169 passed, 2 skipped; Tests 4901 passed, 2 skipped.
  • After the origin/main merge the derivation on 91ccb5a5 names the same 34 families. Re-run there: 32 of them, all exit 0. Not re-run on 91ccb5a5: pnpm check:type-check-debt and pnpm --dir ui exec vitest run scripts/ (both green on ddbba1e8; the merge brought no change to ui/scripts/ or to a type-check ledger).

Generated by Claude Code

… per-group subtotals

A summary report asks the server for every proper prefix of its rows
through totals.groupings, and draws each outer group as a header row, its
inner rows without the repeated outer value, and the server's subtotal row.
A one-level summary, tabular and matrix keep their face; an answer without
the prefix groupings renders the plain table.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibwTmv3iyLca6doQ7h7
…, drill and export

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibwTmv3iyLca6doQ7h7
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 91ccb5a5e6d8aed6d974430d5e21da99e7dbf132 · 2026-10-31T08:33Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 91ccb5a5e6d8aed6d974430d5e21da99e7dbf132
Local-runs: none
Scope: ui/packages/plugin-report/src/DatasetReportRenderer.tsx (summarySubtotalPrefixes, nestSummaryLines, spanDrillRanges, DatasetReportTable's totals request, grouped lines, subtotal lookup, drill and snapshot, the two withSubtotals call sites, the module docblock), the ten i18n packs (report.subtotal), the two test files, .changeset/27975-summary-report-subtotals.md

Inputs: card #27975 (body; the seat's retriage request 6539388814; triage 6539817671 ruling the default with no new key; the seat's claim 6540287709; the dev report 6540832419), PR #28376 (body with the live showcase measure, the file list of 14 files, the diff of 909 lines against main read whole, the five commits from the merge-base ebd312a), the 55 check-runs on this head (read 2026-10-31T08:37Z: all 55 runs completed, none failed, none cancelled, none in progress; success: Auto Label, Build & E2E UI, Build UI Deps, Check Changeset, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Test Core, Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Build Core, Build Docs, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Plan Test Core shards, Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test Core (matrix shard/8), UI Published Artifacts), and the tree on main for packages/spec/src/api/analytics.zod.ts (totals.groupings at :539 to :565: one grouping per marginal, [] the grand total, the executor reads groupings and nothing else), packages/spec/src/ui/report.zod.ts (summary "grouped by row", drilldown on summary and matrix), content/docs/ui/reports.mdx (the summary row and section) and skills/objectstack-ui/rules/dashboards.md :377 (reports drill). The head is mergeable and clean against today's main (merge-tree clean); the ten locale files moved on main since the merge-base in other keys, and PR #28305 (same renderer file, chart path) is still open with a clean merge-tree between the two heads.

① Derived judgments

  • The ruling is implemented as ruled - right. Triage 6539817671 made subtotals the default of a summary with two or more rows levels and added no key. The renderer asks totals.groupings for every proper prefix of rows plus [], the shape the spec describes and the matrix branch already sends, and draws per outer group a header, the inner rows with the outer columns blank, and the server's subtotal row looked up by bucket id over relabeled values, which is the matrix's rule; nothing is summed in the browser, so an avg or a distinct count stays right (the test places a server subtotal where a client sum would differ). Three levels nest by recursion over the group's own indices.
  • Every other face is unchanged - right. withSubtotals is passed for summary alone at both call sites (top level and joined blocks); a one-level summary has no proper prefix and keeps [[]]; tabular passes no totals; a matrix without columns degrades to this table with the grand total only; and when any prefix grouping is missing from the answer (an older server) grouped is false and the plain table draws, the matrix's degrade rule. Group order follows the first row's position in the laid-out rows, so the declared-option layout and an authored order hold.
  • Drill keeps its identity rule - right. A detail row drills by every dimension as before (dims defaults to rows); a subtotal drills by the prefix read off the group's first server row, with drillDims narrowed to the prefix and the ranges spanned over the members, a key some member lacks dropped. The live measure shows the Backlog subtotal opening both Backlog tasks.
  • Export writes what is drawn - right. The snapshot carries the header and subtotal lines in the same cells; the export test compares the CSV cell for cell.
  • Accept-set: nothing an author writes moves - right. No spec path, no key, no new export (the three helpers are module-private); Clause-②: no and Metadata-protocol: none are right. The seat's hand-off note to this lane said "Clause-② is yes"; the PR, the changeset and the claim say no, and no export or declared surface moves, so no is the measured answer and nothing is owed.
  • Pins hold both directions - right. Sixteen cases on the grouped face plus five controls, and the dev's one-off ablation (prefixes forced empty) reds eleven and keeps the controls green, restored byte-identical.
  • Check-runs as gate verdicts - read as above; every run on this head completed and none is red, cancelled or in progress at the read.

② Semver level

  • @object-ui/plugin-report patch, @object-ui/i18n patch - accepted. A non-protocol behaviour change ships patch; the changeset names the visible default change on existing two-level summaries, which is what a reader of the CHANGELOG needs.

③ Boundary flags

  • Serial overlap with feat(plugin-report): a report chart draws its seriesBy split #28305 (DatasetReportRenderer.tsx, chart path) - the merge-tree between the two heads is clean; whichever lands second merges main and, if no hand-written hunk is shared, owes no delta review for that alone.
  • Docs and skills - reports.mdx describes summary as rows grouped with measures aggregated per group, which stays true; no page said "grand total only", so no docs edit is owed.
  • Groups under an order by a measure keep their first row's position rather than sorting by subtotal; the server drops order on totals sub-queries by design. The dev noted it and did not file it; a reader who wants subtotal-ordered groups needs the server to order the prefix grouping, which is a server card if ever asked.
  • No name approval owed - no new key.
  • The seat lands - no Tier H path (no docs/adr, no skills/**, 909 lines), no human gate.

④ Design judgment

  • Tried: a group whose subtotal row is missing from an otherwise complete answer (the cells draw empty rather than wrong, and the face stays grouped); a rows dimension that is a time bucket at the outer level (its range passes through unchanged, as every member carries it); an empty-bucket member (its range is dropped and the drill widens to the group, which is the right side to err on). Reading the prefix groupings from the server instead of summing client-side is the ADR-0021 governance line kept; nothing to add.

Verdict reasoning. The retriage ruling is implemented in the shape the spec already describes for totals, every other report face is unchanged and pinned, the subtotals are the server's, drill and export follow the drawn table, the declarations and the patch level are right, and every check is green or skipped.

Implemented-by: claude/project-thread-csgfso
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 08:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN

席位核对

ACCEPT · seat domain:ui#1 · head 91ccb5a5 · 2026-10-31T08:41Z

席位意见:多层(两层及以上)的汇总报表现在按外层分组显示:每组先有一行组头,外层值只写一次,然后是组内明细,最后一行「小计」。小计由服务器按分组计算,浏览器不做加总,所以平均值、去重计数等也是正确的。点击小计行会打开该组的全部记录,导出的表格和屏幕上看到的一致。单层汇总、明细表和交叉表都不变;如果服务器没有返回分组小计,就退回原来的平铺表。这是维护者裁定的默认行为,没有新增配置项。

  1. Shape: Fixes #27975, draft, base main, 14 files, +646/−32. Changes:
    • DatasetReportRenderer.tsx:
      • summarySubtotalPrefixes builds the totals.groupings request, and DatasetReportTable sends it.
      • nestSummaryLines draws the grouped face.
      • spanDrillRanges drives the subtotal drill.
      • withSubtotals is passed for summary only, at the top-level site and the joined-block site.
    • report.subtotal in all ten packs, two test files and a changeset.
  2. Seat read: this follows triage 6539817671, which made subtotals the default and added no key. Clause-②: no is accepted: the three helpers are module-private, and no spec path or export moves. My hand-off note to the review lane said "yes". The PR, the changeset and the claim say no, and the review confirms no, so nothing is amended. The visible default change on existing two-level summaries is named in the changeset.
  3. Contract review: PASS 6541235372 on this head. ② is patch for @object-ui/plugin-report and @object-ui/i18n. ③ flags the overlap with open feat(plugin-report): a report chart draws its seriesBy split #28305 on DatasetReportRenderer.tsx; the merge-tree is clean, and whichever PR lands second merges main. Groups under an order by a measure keep their first row's position. That is noted only, and would be a server card if anyone asks for it.
  4. Gates / check-runs: on 91ccb5a5: 44 success, 11 skipped, 0 failure. mergeable_state clean.
  5. Landing: check-governed-merges --pr 28376 reports NOT governed, 678 changed lines ≤ 3000. The seat readies the PR and arms auto-merge, on the maintainer's 「按 pm-dispatch 审过的pr你可以自主落地」 (seat thread, 2026-10-30T14:19Z).

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 size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Summary report with two row levels: per-group subtotals, and no repeated outer value

1 participant