Skip to content

fix(core): compareTo on an already-past date range compares with the period before it, and previousYear from 29 February stays in February - #30246

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-6kk1x6
Nov 5, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #30197

维护者速读

  • 改了什么:指标块开启「与上一周期对比」时,「昨天」「上周」「上月」「上季度」「去年」不再拿窗口和自己比,改为对比再往前一期(前天、上上周、上上月……);「最近 7/30/90 天」改为对比之前等长的那段日子;「与去年同期对比」在 2 月 29 日不再跑到 3 月。这些对比的文字统一为「较上期」。
  • 为什么改:fix(core): compareTo on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself #30196 的验收备注发现的老问题:后向预设的对比涨跌永远是 0%,最近 N 天的对比窗口与本期重叠,闰日的去年同期错月。
  • 风险与回滚:只改 compare-to.ts 一个文件的逻辑,不动规范、不动服务端。「今天」「本周/本月/本季度/今年」和三个前向预设的环比窗口逐字节不变;变化的只有上述后向预设,以及 2 月 29 日那天的去年同期。回滚即 revert。
  • 先看:① compare-to.ts 的 lastPeriodBefore(规范没有「前两期」的宏,窗口是算出来的);② 同文件 rollingPair 的后向分支(N+1 天整体后移);③ previousYear 的日期夹取。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/core/src/utils/__tests__/dashboard-filters.presetWindows-29116.test.ts,预期 16 个预设全部通过,每个预设的上一期都在本期开始前一天结束。
  • 需要你做的:无,席位落地。

Clause-②: no
Metadata-protocol: none

What changes

  • @object-ui/core shiftFilterByCompareTo, kind: 'previousPeriod'.
    • {yesterday} and last_week_* / last_month_* / last_quarter_* / last_year_* were never substituted, so the yesterday, last_week, last_month, last_quarter and last_year presets compared the window with itself. Now {yesterday} becomes {2_days_ago}, and each last_* token becomes the period before last.
    • How "two periods back" is spelled, measured. DATE_MACRO_TOKENS (packages/spec/src/data/date-macros.zod.ts) stops at last_*, and the parameterised grammar ({N_weeks_ago}, {N_months_ago}) counts from today, not from a period's first day, so no declared token names the month before last. lastPeriodBefore therefore computes the day: it resolves the last_* START through resolveDateMacros (the display zone, like every other token), moves it back one week / month / quarter / year with UTC calendar arithmetic, and takes the day before that last_* start as the end. The result is an ISO day, which resolveDateMacros passes through. No token is invented.
    • Rolling windows. rollingPair (was forwardRollingPair) now also matches {N_days_ago} with {today}. That window is N + 1 days (last_7_days at 2026-07-15 is 07-08..07-15, measured), and moves back by N + 1: {(2N+1)_days_ago} … {(N+1)_days_ago}. It used to become {N_days_ago} … {yesterday}, N days overlapping the current window. The forward branch now writes {1_day_ago} instead of {yesterday}, because {yesterday} is now itself moved; the resolved window is unchanged.
    • All substitutions still happen in ONE pass (substituteTokens now takes a replacer), so today → yesterday is never carried on to 2_days_ago.
  • kind: 'previousYear' built new Date(y - 1, 1, 29), which is 1 March, so every window seen from 2028-02-29 was re-resolved at 2027-03-01 (this_month compared with March 2027). The day is clamped to the shifted month's length, 28 February.
  • compareToTrendLabelKey: a {yesterday} or last_* token, and a backward rolling window, read vsPreviousPeriod (they read vsYesterday, vsLastWeek, vsLastMonth, vsLastQuarter, vsLastYear). No existing dashboard.trend.* key names "the month before last", and vsPreviousPeriod ("vs previous period", 较上期) is exact for each, so no new key, no locale or fallback change. The check runs first, since last_month_start also contains month_start.
  • Docblock correction in NEXT_TO_CURRENT_TOKENS (my fix(core): compareTo on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself #30196 text): it said next_week_* / next_year_* have no _end in the macro vocabulary. The spec declares next_week_end and next_year_end; it is this package's resolver (date-macros.ts) that lacks them. The comment now says so; the mapping is unchanged.

Why

#30197, filed from #30196's Acceptance notes. Governing text: the compare-to.ts docblock, "kind: 'previousPeriod' — the equal-length window immediately before the current one", and previousYear = "same week / month / quarter, one year earlier". No spec change, no new compareTo kind, no executor change.

Tests

  • dashboard-filters.presetWindows-29116.test.ts: the compareTo case pins all 16 presets at 2026-07-15 (both kinds and the label); the five last_* / yesterday rows and the three last_N_days rows move, the eight others keep fix(core): compareTo on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself #30196's values. New for every preset on all six reference dates: the previous window ends the day before the current window starts, with the same day count for a day, a week or a rolling window, and from the 1st for a month, quarter or year. New for every preset: previousYear from 2028-02-29 equals resolving the filter at 2027-02-28.
  • compare-to.test.ts: yesterday, last week, last month (also from 2026-03-31, February then January), last quarter and last year across a year boundary, {7_days_ago}…{today} and a $between {30_days_ago}…{today}, a mixed last_month_start…current_month_end window moved bound by bound, the leap-day previousYear case, and the labels (a lone {today} still reads vsYesterday).
  • ObjectMetricWidget.compareTo.pastPresets-30197.test.tsx (rendered, the registered object-metric block, the fix(core): compareTo on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself #30196 harness): last_month (上月), yesterday and last_7_days each show 20% against exactly the expected previous window, under "vs previous period", and not the stale label.

Acceptance notes

  • CONTROL, measured outside the tests. The tsx probe of 16 presets × 8 reference dates × 2 kinds plus labels, main ff12aed3 against this branch. Unchanged: this_quarter, this_year, next_quarter in full; today, this_week, this_month, next_month, next_30_days in every row except previousYear at 2028-02-29 (the leap-day fix; e.g. this_month 2027-03-01..03-31 → 2027-02-01..02-28). Changed as intended: yesterday, last_week, last_month, last_quarter, last_year, last_7_days, last_30_days, last_90_days (previousPeriod rows and label, plus their 2028-02-29 previousYear row where it moves).
  • Inline and executor now agree on the rolling windows. last_30_days at 2026-07-15: this branch 2026-05-15..06-14; the executor (shiftRange, measured on Dashboard compareTo on a forward date preset (下月, 下季度, 未来 30 天) compares the window with itself #30140) 2026-05-15..06-14. On calendar presets the two still differ by design: the executor counts days, the inline path steps whole periods.
  • Out of scope, unchanged: the client resolver lacks next_week_end / next_year_end, which the spec declares (see the docblock correction above).

Verification

Suites, type-check, lint, ablations
  • Under os-verify-lock: pnpm --dir ui exec vitest run --maxWorkers=3 packages/core/ exit 0 (232 files, 4,321 passed, 27 skipped); packages/plugin-dashboard/ exit 0 (202 files, 1,928 passed, 6 skipped).
  • pnpm run type-check in core and plugin-dashboard: exit 0.
  • npx eslint on the touched files: 0 errors (warnings only, no-explicit-any, as in the surrounding code).
  • Ablations (scripts/ablation-replace.mjs, each red then restored to HEAD):
    • past tokens (lastPeriodBefore not called): 12 failed, covering the five past-token pins, the five 29116 rows, and the rendered last_month and yesterday rows;
    • backward rolling (ROLLING_DAYS_RE limited to from_now): 6 failed (the rolling pin, the label pin, the three last_N_days rows, the rendered last_7_days row);
    • leap day (clamp removed): 12 failed (the leap pin and the 11 presets whose 2028-02-29 previousYear window moves);
    • label (past-token check removed): 6 failed.
  • Derived gates (dispatch-gates --commands, 35): see the report on Inline compareTo previousPeriod: backward presets (上月, 上季度, 昨天, 上周, 去年) compare a window with itself, last_N_days compares N+1 days with N #30197.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; lint passes on the touched files.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 section excepted).

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzijmF7yUSk5ga9jRkh

… just before it, not with itself

previousPeriod moved only current_* / today tokens, so a dashboard window of
next_month or next_quarter was compared with itself (a 0 change every time)
and next_30_days moved only its start. A forward window now steps back one
period: next_month_* to this month, next_quarter_* to this quarter, and a
{today} .. {N_days_from_now} pair to the N + 1 days ending yesterday. The
trend label names that window (vsThisMonth, vsThisQuarter, vsPreviousPeriod);
the two new keys are in all ten locale packs. The 13 backward presets keep
their windows and labels.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzijmF7yUSk5ga9jRkh
…period before it, and previousYear from 29 February stays in February

previousPeriod never moved {yesterday} or the last_* tokens, so yesterday,
last week, last month, last quarter and last year compared the window with
itself, and {N_days_ago} .. {today} moved only its end. The past tokens now
compare with the period before them (computed from the resolved last_*
start, since no token names two periods back; {yesterday} becomes
{2_days_ago}), and a backward rolling window moves back by its own N + 1
days. previousYear clamps the shifted day to the month's length, so a
29 February window compares with February, not March. These comparisons
are labelled vsPreviousPeriod. today, this_* and the forward presets are
unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzijmF7yUSk5ga9jRkh
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-support-ai · session_01WGufzfE4V9fzZgJLrLDmMC

维护者速读(席位终稿)

  • 改了什么:仪表板上直接绑对象的 KPI 块和对象图表,选「昨天 / 上周 / 上月 / 上季度 / 去年」或「最近 N 天」再开「环比」时,现在跟它前面一段等长的时间比(上月比前一个月,最近 7 天比再往前的 7 天),不再自己跟自己比。另外,在 2 月 29 日选「同比」时,比的是去年 2 月,不再跑到 3 月。
  • 为什么改:这些往回看的预设原来环比永远是 0%,「最近 N 天」比的两段长度也不一样;是 Dashboard compareTo on a forward date preset (下月, 下季度, 未来 30 天) compares the window with itself #30140 复核时发现、立卡 Inline compareTo previousPeriod: backward presets (上月, 上季度, 昨天, 上周, 去年) compare a window with itself, last_N_days compares N+1 days with N #30197 的同一处问题。
  • 风险与回滚:低。只改 compare-to.ts 一个函数;「本期 / 未来」的预设结果不变(16 个预设 × 8 个日期探针,主干与分支逐条对照)。没有对应宏的「前一期」由日历算出具体日期。不加翻译键,这些比较统一显示「较上一周期」。回滚即 revert 本 PR。
  • 先看:ui/packages/core/src/utils/compare-to.ts 的 lastPeriodBefore 与 rollingPair。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.compareTo.pastPresets-30197.test.tsx。
  • 需要你做的:无。不是受管面,CI 全绿,席位直接落地。
  • 席位意见:ACCEPT。

席位核对 · ACCEPT · head 0dd85e008576e016891e91882b98abdf43bc7161 · 2026-11-05T03:30Z

  1. Shape: draft, base main, branch restarted from main after fix(core): compareTo on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself #30196 and merged main again (no force-push). Fixes #30197. One changeset: @object-ui/core patch, Clause-②: no / Metadata-protocol: none. 5 files, +331/−74. check-governed-merges --pr 30246: NOT governed. No spec key, no executor change.
  2. Read by the seat: the rolling pair now covers {N_days_ago} … {today} (→ {(2N+1)_days_ago} … {(N+1)_days_ago}, N + 1 days, equal length) beside the forward pair; the forward pair now writes {1_day_ago} instead of {yesterday} because yesterday is itself moved (date-macros.ts param grammar days? accepts the singular; Dashboard compareTo on a forward date preset (下月, 下季度, 未来 30 天) compares the window with itself #30140 pins unchanged). lastPeriodBefore resolves last_*_start in the display zone, then moves it back one calendar period (end = the day before last_*_start); {yesterday} → declared {2_days_ago}. Substitution is a single pass, so nothing moves twice. previousYear clamps the day to the shifted month (29 Feb → 28 Feb). Label: past windows read vsPreviousPeriod (checked before the month_start patterns they contain).
  3. Accepted deviations: no new trend key, so no locale or fallback edits (the Claim listed them); vsPreviousPeriod is exact. Worktree reused (objectstack-issue-30140). The fix(core): compareTo on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself #30196 docblock about next_week_end / next_year_end corrected: the spec declares them, the ui resolver lacks them — that gap is already Console date macros: {next_week_end} and {next_year_end} stay unresolved in the client #30134.
  4. Evidence: CONTROL probe (16 presets × 8 dates × 2 kinds, main vs branch): only the eight past presets and the leap-day previousYear row change. All four fix halves (past tokens, backward rolling, leap day, label) red under ablation, restored. core and plugin-dashboard suites + type-checks green; dispatch-gates 35/35 exit 0 with --ran reconciled. CI on this head: 44 success, 11 skipped.
  5. Out of scope: none new (next_week_end / next_year_end resolver gap → Console date macros: {next_week_end} and {next_year_end} stay unresolved in the client #30134). Skills-docs: n/a.
  6. Landing: ordinary queue, now: relay pr_ready + automerge_enable, follow to MERGED, landing record on Inline compareTo previousPeriod: backward presets (上月, 上季度, 昨天, 上周, 去年) compare a window with itself, last_N_days compares N+1 days with N #30197.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 03:31
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 03:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 41962336936 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (16/16) — 失败步骤: Run the ui tests (shard 16/16)

    × a read-only package: overlay types can be customized, never 可写; the create-only and closed types are read-only 96ms
    × a read-only package: the view list header reads 可定制, the page list header 只读 11ms
    × typeStateWord: the mapping in every context 6ms
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > All metadata tiles: one state word, in the package context (objectstack#29705) > a read-only pack
      ↳ 失败原因: AssertionError: expected '可定制' to be '只读' // Object.is equality
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > the list header draws the tile word (objectstack#29705) > a read-only package: the view list head
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/DirectoryPage.stateVocabulary-29705.test.tsx > the state vocabulary census (objectstack#29705) > typeStateWord: the mapping in every context
      ↳ 失败原因: AssertionError: expected [ Array(4) ] to deeply equal [ 'customizable', 'readOnly', …(2) ]
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 4 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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 tests tooling

Projects

None yet

1 participant