Skip to content

feat(spec)!: a report chart's seriesBy that cannot split (single-series type, or the xAxis dimension) is refused at authoring time - #28439

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

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.

Ablation, one per arm (src-resolved suite, so no build leg). Mutation through scripts/ablation-replace.mjs, restored to the HEAD blob each time.
  • 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.
Package runs
  • 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.
Derived gates (dispatch-gates.mjs --commands, 104 lines, each run with stdin closed, exit captured before any pipe)

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.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibwvHraeucX94Rvvzrk


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 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 ReportSchema (symbol, a top-level const))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-7.mdx (via ReportSchema (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
  • the SDK route bridge reached 60 of 212 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 — 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 630b4078daf45d41c2d0c19ece48661163dc6418 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 630b4078daf45d41c2d0c19ece48661163dc6418

⚠️ 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 630b4078daf45d41c2d0c19ece48661163dc6418 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 1707dd00edb420d45edcb08d6c842a5b9aa437a0 · 2026-10-31T16:13Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1707dd00edb420d45edcb08d6c842a5b9aa437a0
Local-runs: none
Scope: packages/spec/src/ui/report.zod.ts (checkReportChartSeriesSplit, wired into ReportSchema's non-joined superRefine arm; seriesBy docblock + describe), packages/spec/src/ui/dashboard.zod.ts (SINGLE_SERIES_CHART_TYPES const → export, docblock), packages/spec/src/ui/report.test.ts (new describe, 7 cases), generated api-surface/ui.json + export-origins/ui.json, content/docs/references/ui/report.mdx, .changeset/28424-report-chart-series-split.md. 7 files, +158/−9. Fixes #28424 (dispatch claim 6544971272, dev report 6545877106).
Inputs: card #28424 body + claim + dev report, the PR body, the PR diff at this head, git show/git grep on origin/main at the merge-base 5da3a8ce (no main commit since touches any of the 7 files), hotcrm checkout; check-runs (read 2026-10-31T16:38Z: all 50 latest-per-name check-runs completed, 39 success + 11 skipped, none failed, none running) — 50 checks. No build or test run here; the seat's numbers (spec 773 files / 22837 tests, lint 80/80, dogfood 361+3, ablation per arm, 104 derived gates) are taken from the dev report and not re-measured.

① Derived judgments

  • Acceptance set narrows, as declared. ReportSchema now refuses two value combinations that parsed at the base: chart.seriesBy present on a chart.type in SINGLE_SERIES_CHART_TYPES (pie/donut/funnel/treemap/sankey, verified on main dashboard.zod.ts L808-814), and chart.seriesBy === chart.xAxis. Both arms sit inside the existing else (non-joined) branch beside checkReportOrder, so a joined report keeps its one whole-chart refusal at chart and never reaches the new check (test "a joined report is untouched" pins it). Clause-②: yes (narrowing) and Metadata-protocol: narrowed on the PR body and the changeset are right. The changeset is @objectstack/spec: minor with feat(spec)!: and an ADR-0087 not-required (no-migration-prescription) marker; the marker's reason holds — no key moves, and a conversion cannot pick between "change the type" and "drop the split", so the refusal text names both fixes.
  • Public face: one new export. SINGLE_SERIES_CHART_TYPES goes from module-private to exported through export * from './dashboard.zod' (ui/index.ts L41); api-surface and export-origins carry the one new row each and nothing else moves. ReportChartSchema stays an unrefined ChartConfigSchema.extend(...), so its .shape/.extend readers (SpecReportChartSchema in ui/packages/types, chart.test.ts) are untouched — correct placement for a cross-field rule that needs r.name and the chart.seriesBy path.
  • One source holds inside the spec package. report.zod.ts imports the constant for both the check and the .describe() text; no second list is added. For the record: ui/packages/plugin-dashboard/src/DatasetWidget.tsx L507 already carries its own SINGLE_SERIES_CHART_FAMILIES set with the same five members — pre-existing, not this PR's, and the renderer keeps its refusal panel as the last line of defence by the card's out-of-scope line.
  • Refusal texts are actionable and exclusive. The single-series arm returns before the same-dimension arm, so a pie split by its own xAxis gets one issue, not two; it names the report, the type, the split, and prescribes bar/column/horizontal-bar or dropping seriesBy. The same-dimension arm says the split must name a different dimension than xAxis or be dropped.
  • Existing metadata: zero hits. git grep seriesBy on main over examples/, packages/qa/, ui/** non-test metadata and the hotcrm checkout returns no authored seriesBy outside tests, so no shipped report is refused by this narrowing (the seat's reading, confirmed here).
  • Rider S1 superseded. The seriesBy === xAxis lint warning named on Report chart draws the seriesBy split: query the second dimension and pivot it into grouped series #28206 is answered by this spec refusal; validateChartBindings (packages/lint) still owns whether seriesBy names a declared dimension, which the schema cannot see. The PR adds no lint rule — consistent with the card's "the dispatch decides which".

② Rulings cited

③ Narrowing / widening

  • Narrowing only, declared. No key added, removed or renamed; treemap/sankey fall under the arm by set membership even though only pie/donut/funnel are enumerated in tests — the "one source" test covers every ChartTypeSchema member, so the refused set is pinned to the constant, not to three names.

④ Behaviour / flags

  • Authoring-time only; the renderer's one-series refusal panel is unchanged. Studio's client-side .shape mirror still accepts both shapes until save/publish (PR acceptance note) — same position as the dashboard arm, noted not filed.
  • Docs: content/docs/references/ui/report.mdx is regenerated describe text only; skills prose for seriesBy is skills: teach report chart seriesBy in the objectstack-ui dashboards rule #28207, out of scope.

Implemented-by: claude/project-thread-xfdmx2
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz
VERDICT: PASS

@objectstack-fleet objectstack-fleet Bot removed the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

维护者速读

  • 改了什么: 报表图表声明 seriesBy(按第二个维度拆成多条系列)时,有两种写法现在在编写阶段就被拒绝:一是图表类型是单系列类型(pie / donut / funnel / treemap / sankey),二是 seriesBy 和 xAxis 写成了同一个维度。拒绝信息会告诉作者怎么改:换成柱状类图表,或者去掉 seriesBy。
  • 为什么改: spec: a report chart whose seriesBy cannot split (single-series type, or the same dimension as xAxis) is refused at authoring time, not on the page #28424。这两种写法以前都能通过校验,页面上却只画出一个报错框,或者每条系列只有一个点。用户打开报表才发现,作者在 os build 或 Studio 保存时什么都看不到。
  • 风险与回滚: 这是收窄。仓库里没有现有元数据用到这两种写法(开发已测,seriesBy 今天才合入)。单系列类型清单和仪表盘共用一份(SINGLE_SERIES_CHART_TYPES,只是改为导出)。回滚即 revert。
  • 先看: packages/spec/src/ui/report.zod.ts 中的 checkReportChartSeriesSplit。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/ui/report.test.ts。
  • 需要你做的: 无。不属于受管面,契约复审已 PASS(6546191274),由席位落地。
  • 席位意见: ACCEPT,可落地。

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · 2026-10-31T16:41Z

Head 1707dd00edb420d45edcb08d6c842a5b9aa437a0. Card #28424, claim 6544971272, os-dev-report 6545877106. The maintainer's ruling, verbatim: 「按 pm-dispatch 审过的pr你可以自主落地」.

  • Shape: a draft to main opened by the relay. It carries Fixes #28424, Clause-②: yes (narrowing) and Metadata-protocol: narrowed. The changeset is @objectstack/spec minor with an ADR-0087 not-required (no-migration-prescription) marker. ui seat 2's lint rider S1 (Report chart draws the seriesBy split: query the second dimension and pivot it into grouped series #28206, 6544899074) is superseded by the spec refusal, and the body says so.
  • Contract review: record 6546191274 on this head, PASS. It has no blocking finding and no should-fix.
  • Gates: all check-runs on this head are success or skipped, apart from a Check Changeset re-run that the label change started and that is still in progress. check-governed-merges --pr 28439 reports NOT governed, +158/−9.
  • Deviation noted: the first three wip commits carry no trailer pair. The squash merge writes one commit, and the branch is not rewritten.
  • Landing: pr_ready + automerge_enable in this act. The queue waits for the re-run.

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/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: a report chart whose seriesBy cannot split (single-series type, or the same dimension as xAxis) is refused at authoring time, not on the page

1 participant