You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 16e7a80
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec)!: a report chart's seriesBy that cannot split (single-series type, or the xAxis dimension) is refused at authoring time (#28439)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoVMTxdBoChiHe1Sf5J2Xcv5)_Fixes#28424
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
domain:spec#1 · os-tesla · session_01L9qibwvHraeucX94Rvvzrk
## 维护者速读
- 改了什么:报表图表的 `seriesBy` 拆分在两种拆不开的写法下,发布前就被 spec
拒收——饼图/环形图/漏斗/树图/桑基这类单系列图,和 `seriesBy` 与 `xAxis` 同一维度。
- 为什么改:这两种写法今天能保存、能发布,业务用户打开报表才看到报错框或每条系列只有一个点的“坏图”;作者应在 `os
build`/Studio 保存/`os lint` 时就听到,而且仪表盘早已对同类写法拒收。
- 风险与回滚:收窄,已存的这两种报表会在下次保存/校验时被拒,拒收文案点名改法(换柱状族或去掉
`seriesBy`);仓内示例与夹具零命中。回滚 = 撤这个 PR。
- 先看:`packages/spec/src/ui/report.zod.ts` 的
`checkReportChartSeriesSplit`;`packages/spec/src/ui/dashboard.zod.ts`
只是把 `SINGLE_SERIES_CHART_TYPES` 导出;`packages/spec/src/ui/report.test.ts`
末尾新 describe。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/ui/report.test.ts -t "cannot split"`,预期 7 个用例全绿。
- 需要你做的:无,席位落地。
## What changes
`ReportSchema`'s refinement now calls `checkReportChartSeriesSplit` on
every non-`joined` report. It refuses `chart.seriesBy` with one `custom`
issue at `chart.seriesBy`, in two arms:
1. **Single-series arm.** `chart.type` is in `SINGLE_SERIES_CHART_TYPES`
(`pie` / `donut` / `funnel` / `treemap` / `sankey`). The message names
the report, the type and the split, and says what to write instead: a
bar-family type (`bar` / `column` / `horizontal-bar`) for one bar per
split value, or drop `seriesBy`.
2. **Same-dimension arm.** `seriesBy === xAxis`. The message says
`seriesBy` must name a different dimension than `xAxis`, or be dropped.
The arms are exclusive. On a single-series type the first one answers,
since dropping `seriesBy` fixes both.
**One source for the type set.** `SINGLE_SERIES_CHART_TYPES` in
`dashboard.zod.ts` changes from a module-private `const` to an export,
and its docblock now names the report as a reader. `report.zod.ts`
imports it, and so does the `seriesBy` `.describe()`. No second list
exists.
**Where the check sits.** It runs in the report-level `superRefine`, not
on `ReportChartSchema`. That gives the `chart.seriesBy` path and the
report's name, and it leaves `ReportChartSchema` an unrefined
`ChartConfigSchema.extend(...)`. Its downstream `.extend` / `.shape`
readers (`ui/packages/types` `SpecReportChartSchema`, `chart.test.ts`)
are untouched.
**Rider S1 superseded.** S1 named a lint warning in
`packages/lint/src/validate-chart-bindings.ts` for `seriesBy === xAxis`.
This spec refusal supersedes it, so no lint rule is added.
`validateChartBindings` still owns whether `seriesBy` names a dimension
the dataset declares, which this schema cannot see.
Generated: `api-surface/ui.json` and `export-origins/ui.json` (the new
export), plus `content/docs/references/ui/report.mdx` (the `.describe()`
text).
## Tests
A new describe block, *ReportSchema refuses a chart seriesBy that cannot
split*, in `packages/spec/src/ui/report.test.ts`, 7 cases:
- `pie`, `donut` and `funnel` with `seriesBy` are refused at
`chart.seriesBy` as `custom`. Each message names the report, `type:
'TYPE'`, the split and the bar-family prescription.
- `bar` with `seriesBy === xAxis` is refused, and its message says the
split must name a different dimension.
- Controls: `bar` and `line` with a distinct `seriesBy` parse and keep
it. `pie` without `seriesBy` parses.
- A joined report carrying a `pie` chart split by its own `xAxis` gets
exactly one issue, at `chart` (the existing joined refusal), and none at
`chart.seriesBy`.
- One source: across every `ChartTypeSchema` member, the set this
refusal fires on equals `SINGLE_SERIES_CHART_TYPES`.
Lint fixtures: every `seriesBy` fixture in
`validate-chart-bindings.test.ts` is `bar` with a distinct split, so
none refuses now. No fixture changed. The suite stays green (80/80).
## Acceptance notes
- **Existing metadata.** `git grep seriesBy` over `examples/**`,
`packages/qa/**` and `ui/**`, plus the hotcrm checkout: every hit is a
`bar` with a distinct split. Zero hits on either refused shape, at
5da3a8c.
- **objectui's client-side authoring door.** The Studio report editor
runs its own `.shape` mirror, so it keeps accepting these two shapes
until it chains a check. The author then meets the refusal at save or
publish, the same position the dashboard arm is in. Noted, not filed.
- **Commit trailers.** The three `wip(...)` commits at the base of the
branch carry no trailer pair. The branch is not `claude/issue-*`, so it
is shared under AGENTS.md §3 and was not rewritten. The squash merge
drops them.
## Verification
All readings are at head `1707dd00` unless noted.
<details><summary>Ablation, one per arm (src-resolved suite, so no build
leg). Mutation through <code>scripts/ablation-replace.mjs</code>,
restored to the HEAD blob each time.</summary>
- Single-series arm, anchor `if (typeof type === 'string' &&
(SINGLE_SERIES_CHART_TYPES …).includes(type)) {` replaced with `if
(false) {`. Anchor count went 1 → 0, blob `4c78418e` → `ca603b9e`.
Result: 4 failed (pie, donut, funnel, one-source) and 3 passed.
Restored, blob equals HEAD.
- Same-dimension arm, anchor `if (seriesBy === xAxis) {` replaced with
`if (false) {`. Anchor count went 1 → 0, blob → `b4975efc`. Result: 1
failed (same dimension) and 6 passed. Restored, blob equals HEAD.
</details>
<details><summary>Package runs</summary>
- `pnpm --filter @objectstack/spec build` (declarations on): VERDICT
command-exit 0.
- `pnpm --filter @objectstack/spec check:generated`: 3 stale before
`--fix` (api-surface, export-origins, docs). After `--fix`, all 14 are
up to date.
- `pnpm --filter @objectstack/spec typecheck` + `pnpm --filter
@objectstack/lint exec vitest run src/validate-chart-bindings.test.ts`:
VERDICT command-exit 0, 80/80.
- `pnpm --filter @objectstack/spec test`: VERDICT command-exit 0, Test
Files 773 passed (773), Tests 22837 passed + 1 todo.
- `pnpm --filter @objectstack/dogfood test` (owed once for the
`(narrowing)` arm, at 1707dd0, closure built first): VERDICT
command-exit 0, Test Files 361 passed + 3 skipped (364), Tests 2917
passed + 28 skipped.
</details>
<details><summary>Derived gates (<code>dispatch-gates.mjs
--commands</code>, 104 lines, each run with stdin closed, exit captured
before any pipe)</summary>
Prerequisite build first: `turbo run build --filter='./packages/**'
--filter='@objectstack/dogfood^...'` with declarations (VERDICT
command-exit 0). Then all 104 lines ran, and every one is `:: exit 0`.
The `check:entry-nameability` log prints its standing NOT MEASURED line
for `api-assembled` / `qa`, which have no callable export; that line is
unrelated to this diff.
`dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`: "104
derived famil(ies) accounted for — 104 run, 0 NOT-MEASURED (a DERIVED
zero — all 104 recorded an exit code and none of them is 3)", exit 0.
The record is at
`/mnt/project-files/spec-seat-1/r2026-10-31/out/28424-gates-ran-1707dd00.list`
in the seat's project folder. Every other family is CI's: see this PR's
check-runs.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L9qibwvHraeucX94Rvvzrk
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibwvHraeucX94Rvvzrk)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec)!: a report chart's `seriesBy` that cannot split is refused at authoring time — on a single-series chart type, or naming the same dimension as `xAxis`
6
+
7
+
Clause-②: yes (narrowing)
8
+
Metadata-protocol: narrowed
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: two combinations of existing values that no renderer could draw are now refused. A conversion cannot know whether the author meant a bar chart or one series, so there is nothing for a migration to rewrite; the refusal text names both fixes. -->
11
+
12
+
**BREAKING** for two report chart shapes, both of which parsed before and then failed on the page. `ReportSchema` (non-`joined` reports; a joined report's `chart` is refused whole, as before) now refuses, at `chart.seriesBy`:
13
+
14
+
-**`seriesBy` on a single-series type** — `pie`, `donut`, `funnel`, `treemap` or `sankey`. The split derives one series per value of the `seriesBy` dimension and these types draw one series, so the page showed a refusal panel instead of the chart. The refusal names the report, the type and the split. Fix: a bar-family type (`bar` / `column` / `horizontal-bar`) for one bar per split value in each `xAxis` category, or drop `seriesBy` to keep the single-series chart. This is the report spelling of the rule dashboards already apply to a dimensioned widget with several measures on the same types.
15
+
-**`seriesBy` naming the same dimension as `xAxis`** — every series held exactly one point. Fix: name a different dimension than `xAxis`, or drop `seriesBy`.
16
+
17
+
`SINGLE_SERIES_CHART_TYPES` is now exported from `@objectstack/spec/ui` (the list `checkDashboardWidgetChartMeasureArity` reads); the report refusal reads the same constant, so the two doors cannot drift apart.
| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `<ObjectChart>` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
107
-
|**seriesBy**|`string`| optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension |
107
+
|**seriesBy**|`string`| optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension. Refused on a single-series type (pie/donut/funnel/treemap/sankey) and when it names the xAxis dimension|
108
108
109
109
### Nested Shape: `Report.dateRange`
110
110
@@ -168,7 +168,7 @@ const result = JoinedReportBlockSchema.parse(data);
168
168
|**annotations**|`{ type?: Enum<'line' \| 'region'>; axis?: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]`| optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }`|
| **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `<ObjectChart>` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
171
-
|**seriesBy**|`string`| optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension |
171
+
|**seriesBy**|`string`| optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension. Refused on a single-series type (pie/donut/funnel/treemap/sankey) and when it names the xAxis dimension|
0 commit comments