Skip to content

feat(plugin-report): a report chart draws its seriesBy split - #28305

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

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-suz06i

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #28206

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:报表图表写了 chart.seriesBy 后真的按第二个维度拆分系列了(例如每个负责人一根柱,按阶段分段);台账里 chart.seriesBy 从 planned 改为 live;顺手改掉了 lint 说明里因此变假的两句话。
  • 为什么改:seriesBy 已能写、发布时也会校验,但图表只查 [xAxis] × [yAxis],拆分从来没画出来;HotCRM 的「按负责人和阶段的商机漏斗」只能退回矩阵表。
  • 风险与回滚:只有写了 seriesBy 的报表图表行为改变;没写的图表查询和输出逐字节不变(有对照用例钉住)。回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-report/src/DatasetReportRenderer.tsx 的 DatasetReportChart(查询维度、系列排序、系列标签三处)、新测试 DatasetReportRenderer.chartSeriesBy-28206.test.tsx、packages/spec/liveness/report.json 的 seriesBy 行。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-report/src/__tests__/DatasetReportRenderer.chartSeriesBy-28206.test.tsx,预期 4 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

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

  • Reads chart.seriesBy. When it is set on a series chart, the chart's own dataset query is [xAxis, seriesBy] × [yAxis] instead of [xAxis] × [yAxis], and the rows go to buildChartSeries with both dimensions. That takes the helper's existing pivot branch (the one a two-dimension dashboard widget already takes): one row per x bucket, one series per split value, null group included. chart-series.ts is unchanged.
  • The split dimension joins the chart's dimension-metadata read, so its values are relabeled to their (localized) option labels, and the series are listed in that dimension's declared option order with the empty group last. This is the same derivation DatasetWidget runs for its second dimension (seriesOrder), and it uses the same opt-out the categories use: a report order that reaches the chart's selection keeps the server's order.
  • Series labels: with a split, every series is a dimension value, so the measure's display name (objectui#4020) is no longer stamped onto a series. A series keeps the value it plots, unless a chart.series[] entry names that value (the spec's reading of series[].name when seriesBy is set). Then that entry's presentation lands on it through mergeAuthoredSeries, and its label resolves through pickLocalized in the UI language rather than first-string-wins.
  • Without seriesBy, the query, the rows and the series are what they were.
  • The file's own prose that said the chart queries only chart.xAxis × chart.yAxis now names the split.

Liveness (packages/spec/liveness/report.json).

  • chart.children.seriesBy: planned → live, verifiedAt 2026-10-31, the renderer and the new test as evidence. authorWarn / authorHint are dropped (a live row may not carry them), so os validate stops printing "nothing draws it yet" for the key.
  • S2: the chart row's note dropped its stale sentence about a groupBy sub-key (ChartConfigSchema declares no groupBy) and now says the chart also groups by seriesBy when it is set.
  • packages/spec/liveness/state-counts/report.md regenerated (gen:liveness-counts): report live 39 → 40, planned 1 → 0.

Prose this change made false, corrected (no rule, severity or finding change).

  • packages/lint/src/validate-chart-bindings.ts, module header: the report-charts bullet quoted the renderer as "queries only chart.xAxis × chart.yAxis", and the chart-axis-not-selected section quoted "the selection is exactly one dimension × one measure ... returns ONE series". Both now quote the current renderer text and say what happens with a split.
  • Same file, ChartBinding.ownSelection doc comment: "queries chart.xAxis × chart.yAxis alone and derives exactly ONE series" now names the split.
  • packages/lint/src/rule-explanations.ts, chart-axis-not-selected explanation (os explain): "the embedded chart queries exactly one dimension × one measure and derives ONE series" now says it queries its chart.xAxis dimension (and its chart.seriesBy split, when set) × that one measure and, without a split, derives one series.

Changeset: .changeset/28206-report-chart-series-by-renderer.md, patch for @object-ui/plugin-report, @objectstack/spec (the published liveness ledger) and @objectstack/lint (the os explain text), with both declaration lines.

Tests

New ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.chartSeriesBy-28206.test.tsx, four cases over HotCRM's opportunity_funnel_owner_stage shape (a bar per owner, split by stage; the table groups by owner alone, so the only selection naming stage is the chart's):

  1. the chart's query is { dimensions: ['owner', 'stage'], measures: ['total_amount'] }, and the chart gets xAxisKey: 'owner' and one row per owner carrying each stage's amount (the null stage under (None));
  2. the series are in the stage field's declared order (Prospecting, Proposal, Negotiation, Closed Won), the null group last, each keeping its own column;
  3. a series[] entry naming the value Proposal lands its colour on that series and its { en, zh } label resolves to the zh limb on a zh console; the other series are untouched;
  4. control: the same chart without seriesBy queries owner alone and draws one total_amount series labelled Total Amount, rows passed through.

Ablation (one-off, no permanent file): node scripts/ablation-replace.mjs replaced the split arm of the chart query ((seriesBy ? [xAxis, seriesBy] : [xAxis]) : []; → [xAxis] : [];, anchor 1 → 0, blob 280c006b → 983b4e78). Predicted before the run: cases 1–3 red, the control green. Measured: 3 failed, 1 passed. Restore verified: blob back to 280c006b and git diff HEAD empty.

Acceptance notes

  • S1 is not in this PR. The seat dropped it per triage 6528521739 (spec lane). Measured location for the hand-over: validateChartBindings lives in packages/lint/src/validate-chart-bindings.ts (@objectstack/lint), not in packages/spec. The warning wanted is seriesBy === xAxis on a report chart.
  • A single-series chart family with seriesBy. A report pie / donut / funnel that sets seriesBy now hands the chart several series, which the chart component draws as its single-series refusal notice rather than a split; before this PR it drew one series and ignored the key. The spec does not refuse seriesBy on those types today. Noted for the spec lane beside S1, not filed.
  • What series[].name matches with a split. It matches the series key, which is the split value as the rows carry it: the server-resolved display label (ADR-0021), not the stored option value. For a select split, an entry naming the stored value (proposal) lands on nothing, as an unknown name does today. Noted, not filed.

Verification

All readings on head 39eb423b unless named.

Affected package tests and type-checks
  • pnpm --dir ui exec vitest run packages/plugin-report/ under os-verify-lock: Test Files 31 passed (31), Tests 282 passed, 7 skipped. VERDICT command-exit 0.
  • pnpm --filter @object-ui/plugin-report run type-check (after building its dependency closure with declarations): VERDICT command-exit 0.
  • pnpm --filter @objectstack/lint test: Test Files 181 passed (181), Tests 7679 passed. pnpm --filter @objectstack/lint typecheck: VERDICT command-exit 0.
  • eslint over the four changed source/test files: 0 errors; the 5 warnings are pre-existing lines outside this diff.
Derived gates (dispatch-gates --commands, reconciled with --ran)

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran → 71 derived families accounted for: 70 run, all exit 0, 1 NOT MEASURED.

  • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 (PREREQUISITE NOT MET: reads built dist/ of every package; not built locally). Declared to CI.
  • check:liveness first went red on the stale state-counts/report.md shard; regenerated with gen:liveness-counts and re-run green.
  • node scripts/check-plugin-teardown-shape.mjs --self-test first exited 3 on the shallow clone (its pinned fixture commit was missing); green after fetching that commit.
  • node scripts/check-sdui-manifest.mjs and its --self-test: exit 0.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/liveness/report.json, packages/spec/liveness/state-counts/report.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/liveness/report.json, packages/spec/liveness/state-counts/report.md) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 168 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 53749673ba7a0005d09e8a79544da10d5faa10bc → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 53749673ba7a0005d09e8a79544da10d5faa10bc

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 39eb423b391b593f8cd4d981f126ca60af308758 · 2026-10-31T04:14Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 39eb423b391b593f8cd4d981f126ca60af308758
Local-runs: none

Inputs: card #28206 (body and all four comments: triage 6528521739 with its one-file cross-domain exception and the S1 ruling, the unblock 6530264156, the seat's claim 6538373765, the dev report 6538795928), PR #28305 (body, the file list of 7 files, the diff of 596 lines against main read whole, the commits from merge-base 73b7270), the 69 check-runs on this head (read 2026-10-31T04:37Z: all 69 runs completed, none failed, none cancelled, none in progress; success: Build & E2E UI, Build Core, Build UI Deps, Check Changeset, Dogfood Regression Gate, Flag docs affected by code changes, 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), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), 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), Auto Label, Build Docs, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), UI Published Artifacts), and the tree at this head for mergeAuthoredSeries (ui/packages/core/src/utils/chart-presentation.ts:232, a derived.map that keeps order and length), scopeOrder and declaredOptionPosition (DatasetReportRenderer.tsx:396 and :451) and useDatasetDimensionMeta (useDatasetDimensionLabels.ts:169, relabel follows the dimensions list in order). The head is mergeable and clean against today's main (merge-tree clean); the one file main moved since the merge-base that is also in this diff, packages/lint/src/rule-explanations.ts, merges without conflict.

① Derived judgments

  • The card's effect, as ordered - right. DatasetReportChart reads chart.seriesBy as a string and, on a series plan, widens its own query to [xAxis, seriesBy] × [yAxis]; buildChartSeries is handed queryDimensions, so with a split it takes the pivot branch it already runs for a two-dimension dashboard widget (one row per x bucket, one column per split value, the null group under the translated (None)), and without one it takes the single-dimension branch it took before. Pinned: the query the source receives, the pivoted rows, the xAxisKey, and the CONTROL (no seriesBy: one owner query, one total_amount series labelled from the field, rows passed through). chart-series.ts is untouched.
  • Series order and labels - right. The split joins chartDimensions, so the dimension-metadata read covers it and relabel[1] is the split; its localized options give seriesOrder, the pivoted series are sorted by declaredOptionPosition with the null label last, and the same scopeOrder opt-out the categories use applies (a report order that reaches the chart's selection keeps the server's order). With a split the measure's display name is stamped on no series; each keeps its (relabeled) value, and a series[] entry naming that value lands its presentation through mergeAuthoredSeries and its label through pickLocalized. The authoredSeries[i] / derivedSeries[i] pairing is safe because mergeAuthoredSeries maps the derived list in place.
  • The cross-domain edits are the triage exception and its S2 rider, plus prose the change made false - right. packages/spec/liveness/report.json: chart.children.seriesBy planned to live with the renderer and the new test as evidence, authorWarn / authorHint dropped; the chart row's note drops its stale groupBy sentence (S2) and names the split. state-counts/report.md is the generated shard (live 39 to 40, planned 1 to 0, classified 41) that check:liveness requires after the flip. packages/lint changes are comment and os explain prose only: the header quote is re-synced to the renderer's new sentence, and no rule, severity or finding moves.
  • S1 stays with the spec lane, as triage ruled - right. The body records the measured location (packages/lint/src/validate-chart-bindings.ts, @objectstack/lint, not packages/spec) for the hand-over.
  • Accept-set: nothing an author writes moves - right. No spec schema changes; Clause-②: no and Metadata-protocol: none hold (no new export; the ui change is inside one component).
  • 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, @objectstack/spec, @objectstack/lint all patch; Clause-②: no; Metadata-protocol: none - accepted. The renderer changes behaviour only when a key that was declared-but-inert is present; the spec entry covers the published liveness ledger and the lint entry covers explanation text. Patch is the most either could be.

③ Boundary flags

  • Two follow-ups for the spec lane, named in the body and not filed - (a) rider S1, the seriesBy === xAxis warning in validateChartBindings; (b) a single-series family (pie / donut / funnel) with seriesBy now hands the chart several series, which draws the chart's single-series refusal notice where it drew one series before; the spec does not refuse the key on those types. Both are spec-lane decisions outside this card's surface. The seat routes them with S1.
  • series[].name matches the series key, which is the server-resolved display label, not the stored option value - noted in the body; it is the same rule mergeAuthoredSeries applies everywhere, so not a defect of this diff.
  • No name approval owed - the key was ruled with Report chart: split a series by a second dataset dimension #27974 and PR feat(spec): a report chart can split its bars by a second dataset dimension (chart.seriesBy) #28191.

Verdict reasoning. The diff draws the key where the card said, by handing the existing pivot both dimensions rather than re-deriving a split; order and labels follow the derivations the dashboard widget already uses; a chart without the key is byte-for-byte what it was, pinned by the CONTROL and an ablation shown three red / one green. The ledger flip and its generated shard are the triage exception; the lint edits are prose. Semver is patch everywhere. The two open questions are spec-lane follow-ups the body names.

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

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibxgfM8p8t3fo6y9eFK

维护者速读

  • 改了什么:报表图表声明了 seriesBy 时,按第二个维度拆分成分组柱(比如「按负责人一根柱,再按阶段拆分」),之前这个键声明了却不画。
  • 为什么改:HotCRM 的商机漏斗按负责人×阶段只能退回矩阵表(Report chart draws the seriesBy split: query the second dimension and pivot it into grouped series #28206)。
  • 风险与回滚:只在设置了 seriesBy 时查询多带一个维度,没设的图表不变;回滚 = revert 本 PR。
  • 先看:DatasetReportRenderer.tsx 的 DatasetReportChart 查询与 buildChartSeries 透传。
  • 两分钟验证:打开一个带 seriesBy 的柱状报表,看到每个 X 下按 Y 的分组柱,图例按选项顺序。
  • 需要你做的:无;本席按 pm-dispatch 复核后落地。
  • 席位意见:同意。S1(seriesBy === xAxis 警告)按分诊留给 spec 车道,本 PR 不带。

席位核对

ACCEPT · head 39eb423b391b593f8cd4d981f126ca60af308758 · 2026-10-31T06:52Z

  1. 形态: 7 files, +360/−52; Fixes #28206; 0 governed paths, under 3000 lines (check-governed-merges --pr 28305).
  2. 前提实测: renderer queried only [xAxis] × [yAxis] on main; liveness row chart.children.seriesBy was planned.
  3. 门禁 check-runs: 72 on the head, 58 success + 14 skipped, 0 failing; mergeable_state clean.
  4. 契约复审: PASS 6539092307 on this head.
  5. 范围外去向: S1 (packages/lint/src/validate-chart-bindings.ts warning) and the pie/donut/funnel + seriesBy refusal finding go to the spec lane from this seat. Made-false lint prose corrected in-diff (listed in the body). Skills-docs: not owed (Metadata-protocol: none).
  6. 落地条款: seat runs pr_ready; auto-merge waits for the maintainer's word in the seat thread.

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/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Report chart draws the seriesBy split: query the second dimension and pivot it into grouped series

1 participant