Uh oh!
There was an error while loading. Please reload this page.
refactor(dashboard): compareTo 收敛为 { kind, dimension? },并让 dataset 路径真的跑出对比 - #3612
Merged
Merged
Conversation
…ension? }
`CompareToConfig` was a three-branch union
(`'previousPeriod' | 'previousYear' | { offset }`). objectstack#5011 collapsed
`DashboardWidgetSchema.compareTo` onto the contract the analytics executor
already implements — `DatasetCompareTo`, a plain strict object
`{ kind: 'previousPeriod' | 'previousYear'; dimension?: string }` — so this
renderer now reads exactly that one shape.
- `shiftFilterByCompareTo` / `compareToTrendLabelKey` dispatch on `.kind`. The
`{ offset }` duration shift is deleted with the arm: `{ offset: '1y' }` IS
`kind: 'previousYear'`, while `'7d'` / `'1M'` have no faithful target and are
restated by the author on the widget's own `filter` plus
`kind: 'previousPeriod'` (semantic migration
`dashboard-widget-compareto-offset`). No trend label key is retired — the
offset arm resolved to `vsPreviousPeriod`, which survives as the
`previousPeriod` fallback.
- `DatasetWidget` no longer discards half of `compareTo`. It forwarded only the
object form because the two string arms had no meaning downstream; with one
shape there is nothing to discard, and a stale string is invalid metadata to
be rejected where it is authored, not reinterpreted here (AGENTS.md #0.1).
- `ObjectChart` / `ObjectMetricWidget` / `DashboardRenderer` needed no branch
table of their own: they only ever forwarded the value into the two core
helpers, so the single discriminator stays in one place. The cast that
bridged the old spec skew in `DashboardRenderer` is gone.
Why no union, kept in a comment: zod collapses a failed union into a bare
`Invalid input`, so the prescriptions written inside each arm never reached the
author (framework#5014). One strict object keeps the rejection specific.
Tests: the retired `offset` block is REPLACED, not re-spelled — it pinned the
duration shift itself, so re-spelling would have left it green by producing
nothing. What replaces it pins the key the new shape gained: `dimension` is the
EXECUTOR's, never read renderer-side. New inline-path coverage
(ObjectMetricWidget + ObjectChart) uses a QUARTER window so `previousYear` and
`previousPeriod` disagree about both window and label — with a year-scoped
filter a `previousYear` falling through to the previousPeriod arm would pass.
Refs #3337 (points 1-4, 6-8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt…on runs
The type convergence alone leaves the dataset path exactly as broken as before,
one error later: the executor shifts a `timeDimensions` entry carrying a
`dateRange`, and this widget only ever produced a `runtimeFilter`. A widget
whose window lives in its own `filter` (a date macro, or the dashboard's
date-range filter merged in by DashboardRenderer) forwarded a well-formed
`compareTo` and got "compareTo needs a dated window to shift" back.
So when — and only when — a comparison is asked for, the resolved filter's
bounded date windows are lowered into `selection.timeDimensions[].dateRange`:
- MOVED, not copied. The comparison pass re-runs with the SHIFTED
timeDimensions but the same `runtimeFilter`; a copy left behind would
intersect the shifted window with the current one and every
`<measure>__compare` column would come back empty.
- Only a bounded, closed `{ $gte, $lte }` pair of ISO dates qualifies. A
numeric range is not a window; `$gt`/`$lt` are exclusive and `dateRange` has
no exclusive bound; a half-open `{ $gte }` has no end to shift. Each is left
in the filter, which is what makes the executor say so out loud.
- Only CONJUNCTIVE positions are walked (top level and `$and`); a window inside
`$or` is not the window the query runs in. Two conjunctive windows on ONE
dimension are intersected — arithmetic on the AND, and it keeps the executor
from being handed the same dimension twice and calling it ambiguous.
- WHICH dimension is shifted is not decided here. Every window is lowered under
the name the author wrote; zero or two candidates is the executor's error to
raise, naming them. Guessing one renderer-side would trade a loud error for a
quietly wrong window — the failure class objectstack#5011 set out to end.
Second half, without which the acceptance criterion cannot be met: this widget
never rendered `<measure>__compare` at all, so even a successful comparison was
dropped on the floor — the same "silently no comparison" from the other
direction. KPI widgets now show the delta + window label (via the shared
`computeMetricDelta` and the same `dashboard.trend.*` keys the inline metric
card uses — no new key), tables gain a comparison column formatted as its base
measure, and charts gain a `variant: 'comparison'` overlay series matching
ObjectChart's own. The cross-tab is deliberately left out: spreading a
comparison across bucket × measure × window is a layout decision of its own.
Refs #3337 (point 5).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRtThe latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Aug 7, 2026
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 7, 2026 15:56
Uh oh!
There was an error while loading. Please reload this page.
akarma-synetal pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 10, 2026
…-tab (objectstack-ai#3614) (objectstack-ai#3757) The matrix branch of `DatasetWidget` (`type: 'pivot'` with two or more dimensions) was the one render path PR objectstack-ai#3612 left out. It lays out `bucket x measure` and never admitted the `<measure>__compare` columns the executor returns, so a pivot with a bounded date window and a `compareTo` ran a correct comparison query, received correct comparison data, and showed none of it — headers, cells and all three subtotals silent. That is objectui#3337's "silently no comparison", relocated to another widget family. Maintainer ruling 2026-08-07 chose form 2, in-cell stacking: - The comparison stacks INSIDE the cell — current value on top, comparison value and delta beneath in smaller type. The column structure stays `bucket x measure`: a comparison column would make it `bucket x measure x window`, doubling the width and adding a third header level on the widget family whose width is already the scarce resource. - rowTotalById / colTotalById / grandTotal stack it identically. A Total that alone showed no comparison reads as "this row has none" — a different, false statement. - `exportColumns` drops its `isMatrix` exception, so the cross-tab exports a flat `<measure>__compare` column per compared measure with bare numbers in the cells. Display stacks; export is data, and a spreadsheet can still compute on it (never "$120 $100 20%"). - One caption names the window for the whole table, from the same `dashboard.trend.*` vocabulary the KPI/flat-table paths use, and the delta comes from the same `computeMetricDelta` — a cross-tab cell and a dataset KPI over the same two windows agree on sign and rounding rather than each rolling their own percentage. A cross-tab has no per-window column header to qualify, which is exactly what stacking traded away. Presence is detected from the DATA via the existing `comparedValues`, not a new config flag, so nothing is opted into per path and a pivot the executor sent no comparison for renders byte-identically to before. Tests extend the package's own compareTo suite: stacked cells (rise / fall / flat / empty cell), all three subtotals, the flat CSV export asserted to carry no display formatting at all, plus three regression pins (no compareTo; a compareTo the executor answered without comparison columns; stray `__compare` columns the widget never asked to compare). Reverse verification, predicted before running: reverting the render change alone turns the two stacking tests red on the missing `matrix-cell-compare` and leaves the export test and all three pins green; reverting the export change alone turns only the export test red ("Status,Priority,Amount" vs the expected comparison column). Both matched. Fixesobjectstack-ai#3614 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#3337
上游 objectstack#5011 把
DashboardWidgetSchema.compareTo从三分支联合收敛为执行器早已实现的那份契约(DatasetCompareTo,普通 strict object)。本仓是消费侧适配。前提已实测:pin 的@objectstack/spec解析到 17.0.0-rc.5,node_modules里compareTo: z.ZodOptional<z.ZodObject<{ kind, dimension? }, $strict>>,DatasetCompareTo.dimension已是可选并注明由执行器决议。两个 commit,按「改型」与「唯一的功能改动」分开:
1.
refactor:类型收敛(正文 1-4、6-8 点)CompareToConfig→{ kind: 'previousPeriod' | 'previousYear'; dimension?: string };shiftFilterByCompareTo/compareToTrendLabelKey按.kind分派,{ offset }的时长位移随分支删除。vsPreviousPeriod,而它正是previousPeriod的兜底,六个dashboard.trend.*一个没少。本 PR 未触碰任何语言包文件。DatasetWidget删掉typeof widget.compareTo === 'object'的字符串丢弃 workaround 及那句已不成立的注释,widget.compareTo原样转发。遗留字符串在新形状下是非法元数据,应在作者/发布侧被拒,而不是在渲染器里改写成另一个查询(AGENTS.md #0.1)。ObjectChart/ObjectMetricWidget/DashboardRenderer:这三处本来就没有整值判别 —— 它们只把值转发进两个 core helper,唯一的判别式在 helper 里。所以是类型与注释跟随,外加删掉DashboardRenderer里那个用来兜跨仓 skew 的as any。这一点与 issue 正文的预期不同,如实说明。2.
feat:唯一的功能改动(正文第 5 点)只做改型的话,dataset 路径只是换个报错继续坏:执行器要位移的是带
dateRange的timeDimensions条目,而本组件只产出runtimeFilter。所以在且仅在请求对比时,把已解析的日期窗口降进selection.timeDimensions[].dateRange:runtimeFilter;留一份副本会让位移窗口与当前窗口取交集,每个__compare列都空。{ $gte, $lte }ISO 日期对。数值区间不是窗口;$gt/$lt是开区间而dateRange没有开区间上界;半开的{ $gte }没有可位移的终点。三者都留在 filter 里 —— 这正是让执行器把话说出口的方式。$and);$or里的窗口不是整条查询所处的窗口。同一维度上的两个合取窗口取交集(AND 上的算术),顺带避免把同一个维度名递给执行器两次再被判为歧义。验收里那句「真的渲染出对比列」需要的第二半:本组件此前根本不渲染
__compare列(表格columns = [...dimensions, ...values]、metric 只读values[0]),所以就算对比成功也会被丢掉 —— 同一个「静默无对比」的另一个方向。现在 KPI 显示 delta + 窗口标签(复用computeMetricDelta与内联 metric 卡片同一套dashboard.trend.*,不新增 key),表格多一列(按基础度量的 format 格式化,如revenue__compare显示$100),图表多一条variant: 'comparison'覆盖序列(与 ObjectChart 的 overlay 同款)。交叉表(pivot ≥2 维)有意不做:把对比铺进 桶 × 度量 × 窗口 是另一个布局决策,已另开 finding 记录。验证
--maxWorkers=2):packages/corepackages/plugin-dashboardpackages/plugin-charts→ 110 files / 1817 tests passed。type-check三包全绿;改动文件 eslint 0 error;check-control-bytesOK;changeset guard(无major)OK。origin/main,结果与预测逐条吻合:compare-to.test.ts红 3 条(previousYear 窗口 + 两条 label key):旧码用compareTo === 'previousYear'比字符串,{ kind: 'previousYear' }落进 previousPeriod 分支。DatasetWidget.compareTo.test.tsx红 16/20(旧码没有extractDateWindows、不下沉窗口、不渲染对比);绿的 4 条全是「无 compareTo 时不变」的反向对照。DatasetWidget.test.tsx第 8 点的反转断言,对旧码是绿的,不是红。派发单预设的 before-red 在这里不成立:旧守卫的判别式是typeof === 'object',而新形状的每一个取值都是 object。真正变化的是「字符串拼写不再由渲染器裁决」,它落回 schema 的具名拒绝(#5018 家族的倒置方向)。因此那条老断言是整条替换:原来钉的「字符串被丢弃」随着字符串形态一起消失,替换成「省略dimension时不得替作者补一个」。同理 core 测试里的offset块也是整条替换(re-spell 只会让它「因为什么都没产出」而绿),换成钉住dimension属于执行器、渲染器不读。越界说明(1 处,有意)
packages/plugin-dashboard/SKILL.md不在派发单的文件围栏内,但它是本包面向 AI 作者的授权文档,原文三个 JSON 示例写的正是已退役拼写("compareTo": "previousPeriod"/{ "offset": "7d" })—— 留着会让 AI 持续产出被 strict schema 拒绝的元数据。按 AGENTS.md #2(docs-driven)一并更新,并补上「窗口必须有界」这条新的作者约束。与在途 #3546-s2(语言包)、#3584(CONTRIBUTING.md)无文件相交。🤖 Generated with Claude Code
https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Generated by Claude Code