Skip to content

feat(spec): add next_month, next_quarter and next_30_days date-range presets - #30133

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-tg9eh0
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-tg9eh0

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Requested by Yibo Wang · project thread

Fixes #30076

Ruling-ref: 6631151570
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — git grep -n "last_90_days\|last_30_days" -- skills .claude/skills has 0 hits, and no skills/** line lists the preset vocabulary (the dashboards rule only uses this_quarter / this_year as examples). Nothing is owed to the paused skills lane.
Prior rulings read: date-range-presets,date_range_presets,next_month → 0 hits; none; thread: none; repo: objectstack-ai/objectstack (the card's own ruling 6631151570 is the scope; the tool reads rulings by first-line shape and did not classify it)

ADR-0087: not needed. Nothing is removed or narrowed; the 13 existing windows are byte-identical.

维护者速读

  • 改了什么:spec 的日期预设表新增三个往后看的预设:next_month(下月)、next_quarter(下季度)、next_30_days(未来 30 天),服务端 analytics-date-range 能解析它们,控制台 10 种语言都有标签。
  • 为什么改:卡片 [Decision] Relative dates look only backward: add 下月, 下季度 and 未来 N 天 for forecasting? #30076 的裁决 A(「其他全部同意」)。销售预测问的都是往后:下个月、下季度、未来 30 天成交什么;以前只能用会过期的自定义日期。
  • 风险与回滚:只加不改,原有 13 个窗口一字不动。下月、下季度的服务端终点没有现成宏,采用席位批准的“重锚定”:同一个解析器从下月第一天再算一次“下月第一天”,不新增宏 token,也不在 core 里做日期运算。回滚就是 revert。
  • 先看:packages/spec/src/data/date-range-presets.ts(表和顺序说明)、packages/core/src/utils/analytics-date-range.ts(重锚定),以及下面的测量表。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/date-range-presets.test.ts,然后 build spec 后 pnpm --filter @objectstack/core exec vitest run src/utils/analytics-date-range.test.ts(含上海、纽约 DST、查塔姆群岛行)。
  • 需要你做的:现在不用做什么,等 CI 和席位验收。

What changes

  • Spec (packages/spec/src/data/date-range-presets.ts): DATE_RANGE_PRESETS adds next_month, next_quarter, next_30_days, named after last_30_days's spelling. DATE_RANGE_PRESET_MACRO_WINDOWS adds their rows in the inclusive last-day convention (commit 80aef80), and the closed-window rule now covers the forward presets. Every row is pinned by extent in date-range-presets.test.ts (reference day 2026-07-15: August is 31 days, Q4 is 92, next_30_days is 31 days like last_30_days).
  • Display order (the filter bar reads the array in order): this_month, last_month, next_month, this_quarter, last_quarter, next_quarter, then last_7_days, last_30_days, last_90_days, next_30_days. Each forward preset sits in the family it extends, after the backward member, so all month windows are found in one place and the rolling group reads backward then forward. The existing 13 keep their relative order; no forward preset opens a group, because the backward windows are the ones most filters ask for.
  • next_30_days = today plus the 30 days after it (['{today}', '{30_days_from_now}']), the mirror of last_30_days (['{30_days_ago}', '{today}']). It opens on today because a row due today is still ahead of a forecast.
  • Server (packages/core/src/utils/analytics-date-range.ts): PRESET_WINDOW_TOKENS resolves the three half-open. A bound may now be { token, from }: resolve from, then resolve token again with the reference instant moved into that day (its noon) in the same zone. next_month ends at { token: 'next_month_start', from: 'next_month_start' }, i.e. the month after next; next_quarter likewise. The resolver stands at noon of the anchor day, not its first instant: where a zone springs forward at 00:00 on a period's first day (America/Asuncion, 2023-10-01) the day has no midnight, the day-start instant reads as the day before, and the window came out empty (measured: end === start). Seat ruling on this card thread: re-anchor, no new macro token (option A of the seat's two).
  • Readers that close over the vocabulary: the conformance kit (analytics-date-range-conformance.ts), analytics-date-range-closed-vocabulary.test.ts and lint validate-preset-comparands.ts iterate DATE_RANGE_PRESETS, so they take the three without edits. objectql temporal-comparand-door.ts does not iterate the list: it judges a comparand by the field's declared type, so a new preset name needs nothing there (its test passes unchanged). Only prose counts ("the 13 declared names", "each of the thirteen") were made count-free.
  • ui/ (seat-lifted, exactly these): the three labels in all 10 packs under ui/packages/i18n/src/locales/ (ar, de, en, es, fr, ja, ko, pt, ru, zh; zh-Hant has no pack yet, phase 2 of Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428, so none is created); the three English defaults in filter-builder.tsx's label table (measured: without them the 29115 test renders the raw keys dashboard.filters.range.next_month, 4 cases red); next_month_end and next_quarter_end in the client resolver ui/packages/core/src/utils/date-macros.ts (spec tokens the client did not resolve; without them the console lowers 下月 to ['2026-08-01', '{next_month_end}']); and the two pins that counted 13 (dashboard-filters.presetWindows-29116.test.ts, filter-builder-relative-dates-29115.test.tsx).
  • packages/verify (dashboard-window.ts, a reader of DATE_RANGE_PRESETS): its fixture rows were written only on days up to today, so the three forward windows selected no rows and 5 of 15 cases in dashboard-window.test.ts went red. A window whose last day is after today now also writes its own future edges; the days written for the 13 existing presets are unchanged (15/15 green).
  • Pins: packages/spec/src/ui/dashboard.test.ts counted 13 presets and 14 default ranges; now 16 and 17. The 29116 test's compareTo case runs on the 13 retired presets only, with a comment naming Dashboard compareTo on a forward date preset (下月, 下季度, 未来 30 天) compares the window with itself #30140.
  • Docs: content/docs/ui/dashboards.mdx preset table; stale "13 presets" / "never after today" prose in packages/verify/README.md and content/docs/deployment/cli.mdx; the preset list in ui/packages/plugin-dashboard/README.md; content/docs/references/** regenerated by gen:docs; the docs/qa/platform-checklist/areas/api-backend.json item api-backend.date-range-preset-matrix widened to 16 variants (revision 3, enumSource.expect: 16).

Measurement: preset → spec window → server window → resolves?

Reference: spec on 2026-07-15 (UTC days); server on now 2026-09-09T12:34:56.789Z unless noted. Server windows are [start, end) instants.

preset spec window (inclusive, $between) server half-open tokens server window resolves?
next_month ['{next_month_start}', '{next_month_end}'] → 2026-08-01 … 2026-08-31 next_month_start → next_month_start re-anchored on next_month_start 2026-10-01T00:00Z → 2026-11-01T00:00Z yes, by re-anchoring (no month_after_next_start token exists; PERIOD_RE stops at next_)
next_quarter ['{next_quarter_start}', '{next_quarter_end}'] → 2026-10-01 … 2026-12-31 next_quarter_start → next_quarter_start re-anchored on next_quarter_start 2026-10-01T00:00Z → 2027-01-01T00:00Z yes, by re-anchoring
next_30_days ['{today}', '{30_days_from_now}'] → 2026-07-15 … 2026-08-14 today → 31_days_from_now 2026-09-09T00:00Z → 2026-10-10T00:00Z yes, directly (parametric days resolve to a calendar day)

Timezone and DST rows pinned in analytics-date-range.test.ts (#30076 block):

preset now zone window why
next_month 2026-01-31T20:00Z Asia/Shanghai 2026-02-28T16:00Z → 2026-03-31T16:00Z the Shanghai day is already Feb 1, so next month is March
next_month 2026-01-31T20:00Z UTC 2026-02-01T00:00Z → 2026-03-01T00:00Z control
next_month 2026-10-15T12:00Z America/New_York 2026-11-01T04:00Z → 2026-12-01T05:00Z DST ends Nov 1: start EDT, end EST
next_quarter 2026-12-10T12:00Z America/New_York 2027-01-01T05:00Z → 2027-04-01T04:00Z year rollover, EST start, EDT end
next_30_days 2026-10-20T12:00Z America/New_York 2026-10-20T04:00Z → 2026-11-20T05:00Z window crosses DST end
next_month 2026-03-15T00:00Z Pacific/Chatham 2026-03-31T10:15Z → 2026-04-30T11:15Z +13:45 then +12:45, non-whole-hour
next_month 2023-09-15T12:00Z America/Asuncion 2023-10-01T03:00Z → 2023-11-01T03:00Z springs forward at 00:00 on Oct 1; a midnight anchor gave an empty window
next_quarter 2023-08-15T12:00Z America/Asuncion 2023-10-01T03:00Z → 2024-01-01T03:00Z the quarter opens on the gap day

The existing #17341 sweep (every preset, 8 instants × 4 zones) also holds each new end to "the day after the spec's inclusive last day", and the tiling test now includes this_month → next_month and this_quarter → next_quarter.

UI reader measurement

reader reads the spec table? result
ui/packages/core/src/utils/dashboard-filters.ts yes: imports DATE_RANGE_PRESETS and lowers through DATE_RANGE_PRESET_MACRO_WINDOWS takes the three with no edit
ui/packages/components/src/custom/filter-builder.tsx yes: DATE_RANGE_PRESETS.map(...) for options and label keys takes the three; its English defaults table needed the three rows
ui/packages/core/src/utils/date-macros.ts (client resolver) no: its own token table lacked next_month_end / next_quarter_end; added here. next_week_end and next_year_end are still missing: follow-up #30134
ui/packages/types/src/data-protocol.ts:595-610 no: a hand-kept union already lists next_month, next_quarter, next_30_days (plus undeclared next_year, next_7_days, next_90_days); not edited

No domain:ui follow-up is needed for the preset list itself: both readers derive from the spec.

Follow-ups

Verification

  • spec suite (os-verify-lock): 24,399 passed after the dashboard pin move (811 files); spec typecheck passed.
  • core suite: 133 files, 2,970 passed at f7e8b20; analytics-date-range.test.ts 73 at the round-1 head (two Asuncion rows added). core and lint typecheck passed (after a full packages/** build, 80/80 tasks).
  • Readers that iterate the vocabulary: driver-memory date-range conformance, DST, timezone and arity files (49 passed); service-analytics conformance and arity (42); verify dashboard-window.test.ts (15); lint validate-preset-comparands.test.ts (29); objectql engine-temporal-comparand-door.test.ts (14); driver-sql sql-driver-calendar-day-upper-bound.test.ts (18); service-analytics native-sql-temporal-comparand-decline.test.ts (8); verify typecheck passed.
  • ui (from the ui/ root): dashboard-filters.presetWindows-29116, filter-builder-relative-dates-29115, dashboard-range-preset-labels-4984, en-zh-key-parity: 4 files, 112 passed.
  • pnpm --filter @objectstack/spec check:generated: all 14 artifacts current.
  • Derived gates: dispatch-gates --commands listed 108; all 108 run, --ran reconciles 108/108, 0 NOT-MEASURED. 107 exit 0. pnpm check:platform-checklist exits 1 on one finding that is on main already: areas/platform-core.json anchors packages/metadata-protocol/src/protocol.ts#assertProtocolCompat, which origin/main's protocol.ts no longer declares (0 matches at base f68b47ca). This PR's own checklist item passes. check:generated crashed once on heap at 4 GB inside check:test-typecheck and passed at CI's 6 GB (NODE_OPTIONS=--max-old-space-size=6144).
  • Lint: eslint on every changed file, 0 errors.
  • Round 1 (contract review 6634808446): derivation grew by one family, pnpm check:cli-examples-parity, exit 0; the docs gates for the edited prose (check:doc-authoring, check:docs-transcript-drift, check:doc-anchors, check-affected-docs, check-drift-comment, check:published-readme-links, check:issue-citations, check:docs-spec-enumerations) exit 0; the ui 29116 test 51 passed.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 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: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzj9os3R42oiDRwN6HZ

…presets

The three forward presets join DATE_RANGE_PRESETS with their inclusive
last-day windows. Core resolves them half-open; the month and quarter ends
re-anchor next_*_start on the day it names, so no macro token is added.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzj9os3R42oiDRwN6HZ
Adds next_month, next_quarter and next_30_days labels to all ten locale
packs and the filter builder's defaults, teaches the client macro resolver
next_month_end and next_quarter_end, and moves the two preset pins.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzj9os3R42oiDRwN6HZ
The dashboard-window proof capped its fixture days at today, so the three
forward presets selected no fixture rows and every proof under them failed.
A window whose last day is after today now writes its own edges; the days
written for the other presets are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzj9os3R42oiDRwN6HZ
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/lint, @objectstack/spec, @objectstack/verify, touching 19 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/lint/src/validate-preset-comparands.ts, packages/spec/src/data/filter.zod.ts, packages/verify/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via this_month (literal, a string literal in DATE_RANGE_PRESETS))
  • content/docs/ui/dashboards.mdx (via DATE_RANGE_PRESETS (symbol, a top-level const object), last_month (literal, a string literal in DATE_RANGE_PRESETS), last_quarter (literal, a string literal in DATE_RANGE_PRESETS), next_30_days (literal, a string literal in DATE_RANGE_PRESETS), next_month (literal, a string literal in DATE_RANGE_PRESETS), next_quarter (literal, a string literal in DATE_RANGE_PRESETS), this_month (literal, a string literal in DATE_RANGE_PRESETS), this_quarter (literal, a string literal in DATE_RANGE_PRESETS))
  • content/docs/ui/reports.mdx (via this_month (literal, a string literal in DATE_RANGE_PRESETS), this_quarter (literal, a string literal in DATE_RANGE_PRESETS))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/lint/src/validate-preset-comparands.ts, packages/spec/src/data/filter.zod.ts, packages/verify/README.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 179 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 a9fb14bdeeb5e09fec341ead4e0fffd8d8ed00bb → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json a9fb14bdeeb5e09fec341ead4e0fffd8d8ed00bb

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head f7e8b20 · 2026-11-04T15:25Z

Full review of the first head at CONTRACT_REVIEW_TIER.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f7e8b2034a08adcb10e71b6f7f1d08873dd91ac4
Local-runs: none
Scope: the full diff (28 files) against merge-base f68b47c; card #30076 and its 5 comments; PR #30133 body and 2 issue comments (0 review comments); check-runs on the head; packages/spec/src/data/date-range-presets.ts, date-range-presets.test.ts, date-macros.zod.ts:108-266, ui/dashboard.zod.ts:1474, ui/report.zod.ts:704, data/analytics.zod.ts:739-842; packages/core/src/utils/analytics-date-range.ts, analytics-date-range.test.ts (NOW, SWEEP, #17341 and #30076 blocks), analytics-date-range-conformance.ts, filter-tokens.ts:269-520, datetime.ts:100-160; packages/lint/src/validate-preset-comparands.ts; packages/objectql/src/temporal-comparand-door.ts; packages/verify/src/dashboard-window.ts:86-766, packages/verify/README.md:40-170; ui/packages/core/src/utils/date-macros.ts, dashboard-filters.ts, compare-to.ts:55-140, tests/dashboard-filters.presetWindows-29116.test.ts; ui/packages/components/src/custom/filter-builder.tsx, filterRelativeDates.ts, tests/filter-builder-relative-dates-29115.test.tsx; ui/packages/plugin-dashboard/src/DashboardFilterBar.tsx, DatasetWidget.tsx:244-275, README.md:415; ui/packages/plugin-report/src/ReportDateRange.tsx; ui/packages/types/src/data-protocol.ts:590-610; the 10 locale packs; content/docs/ui/dashboards.mdx, content/docs/deployment/cli.mdx:2590-2606, content/docs/protocol/kernel/i18n-standard.mdx:140-185; .changeset/30076-forward-date-range-presets.md and .changeset/config.json; #29349 ruling F (comment 6580070604 and the 四棱(F) text).

① Derived judgments

  1. Extents hold. Spec rows (date-range-presets.ts:154-156) are inclusive: next_month = [{next_month_start},{next_month_end}], next_quarter = [{next_quarter_start},{next_quarter_end}], next_30_days = [{today},{30_days_from_now}], i.e. today through the 30th day ahead = 31 calendar days, the exact mirror of last_30_days (31 days, [{30_days_ago},{today}]); both pinned at 31 by prescribedDayCount (date-range-presets.test.ts:464,467) and the three rows are in EXPECTED_WINDOW_DAYS (:179-181) and TOKEN_DAY_ON_REFERENCE (:146-150). Core's half-open form (analytics-date-range.ts:168-170) covers the same days: next_30_days stops before 31_days_from_now (Sep 9 → Oct 10 excl. = 31 days, test :246); next_month/next_quarter stop at next_*_start re-anchored on the day next_*_start names (:329,:368-372). The analytics-date-range.ts says the spec's prescription ends are "one day earlier for the eight period presets" — PR #17338 makes it ten, and makes that sentence false #17341 sweep (test :396-409) iterates every DATE_RANGE_PRESETS member over 8 instants × 4 zones, including 2026-12-31T23:59Z and 2026-01-01 (December and Q4 rollovers), and holds each core end to nextUtcCalendarDay(resolveFilterToken(specEnd)) in the same zone, so the two conventions are tied by the one resolver, not by hand. Concrete rows pin UTC year-cross (:245), Shanghai day-already-February (:249), New York DST end/start (:252-254) and Chatham +13:45/+12:45 (:256). The core conformance kit iterates DATE_RANGE_PRESETS (analytics-date-range-conformance.ts:184), so service-analytics and driver-memory are judged on the three without edits.
  2. Re-anchoring zone: boundDayStart passes the same timezone to both tokenDayStartMs(from) and tokenDayStart(token, anchor) (analytics-date-range.ts:368-372); resolveFilterToken derives today via proxyDay(now, timezone) (filter-tokens.ts:269,414) and all arithmetic is UTC-proxy; no local-clock call anywhere. One hazard (should-fix, ② below): the anchor round-trips through an INSTANT, local midnight of the from day (zonedDateStartToUtcMs), and datetime.ts:146-150 documents that a wall clock the zone skips settles on the post-transition offset, "just before the gap", which reads as 23:xx of the PREVIOUS day locally. A zone whose spring-forward is 00:00 on the first day of a month or quarter then re-anchors on the last day of the prior period, next_month_start seen from there IS the anchor day, and the window is [start, start), empty. tz-db case: America/Asuncion, 2023-10-01 00:00 → 01:00 (first Sunday of October rule; Oct 1 2023 was a Sunday), so next_month from 2023-09-15 and next_quarter from 2023-07-15 in that zone resolve empty. No current zone has such a rule for future dates, and the sweep zones do not include one, so it is latent; the existing 13 presets render each bound once and never round-trip. Fix: re-anchor on the day's noon (zonedWallClockToUtcMs({…day, hour: 12}, tz)), as ui date-macros.ts:63 already does for the same reason, and add an Asuncion-2023 row.
  3. Readers. Every vocabulary reader on the head derives from the spec: spec analytics.zod.ts:739 (z.enum(DATE_RANGE_PRESETS)), dashboard.zod.ts:1474, report.zod.ts:704, filter.zod.ts, lint validate-preset-comparands.ts, core resolver/conformance, verify dashboard-window.ts:761, ui dashboard-filters.ts:25-26, filterRelativeDates.ts:92-100, filter-builder.tsx, DashboardFilterBar.tsx:285, ReportDateRange.tsx:218. No source reader holds a stale list or count. Stale prose remains in three hand-written places the PR did not touch: packages/verify/README.md:44 ("each of the 13 presets") and :167 ("never after today", now false since dashboard-window.ts:607-610 writes a forward window's future edges); content/docs/deployment/cli.mdx:2594 ("never after today") and :2600 ("each of the 13 presets"), a page the docs-drift check named; ui/packages/plugin-dashboard/README.md:415 ("today | this_week | … | last_90_days | custom", the list no longer ends at last_90_days). validate-preset-comparands.ts:72 "13 declared preset names" is inside a verbatim quote of the 2026-09-06 ruling and is rightly left. The objectql door (temporal-comparand-door.ts) imports only classifyFilterToken/isTextFilterOperator and reads no vocabulary; it refuses any string on a temporal field, so it is unaffected, but the PR body's claim that it "iterates DATE_RANGE_PRESETS" is wrong (⑤).
  4. UI edits are confined to readers that red: the three labels in all 10 packs (ar de en es fr ja ko pt ru zh; zh-Hant has no console pack yet, Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface #29428 phase 2, so the same-PR follow at i18n-standard.mdx:160-162 is met for every language the surface carries); the English defaults table in filter-builder.tsx:1217-1226 (the 29115 test renders raw keys without it); next_month_end/next_quarter_end in date-macros.ts:132,134, correct (endOfMonth(addMonths(startOfMonth(today),1)), endOfQuarter(addMonths(startOfQuarter(today),3))), pinned across the year rollover and a leap February (29116 test :119-127); and the two count/order pins. Labels are correct native wording and match each pack's existing register: zh 下月/下季度/未来 30 天 (the ruling's own words), ja 来月/来四半期/今後 30 日間, ko 다음 달/다음 분기/향후 30일, de Nächster Monat/Nächstes Quartal/Nächste 30 Tage, fr Le mois prochain/Le trimestre prochain/30 prochains jours, es El próximo mes/El próximo trimestre/Próximos 30 días, pt Próximo mês/Próximo trimestre/Próximos 30 dias, ru Следующий месяц/Следующий квартал/Следующие 30 дней, ar الشهر القادم/الربع القادم/الـ 30 يومًا القادمة. ui/packages/types/src/data-protocol.ts:595-610 is a hand union that already lists the three (plus undeclared next_year, next_7_days, next_90_days); handed back as an observation, correctly not edited.
  5. Inline compareTo (should-fix, filed not fixed): shiftFilterByCompareTo(previousPeriod) substitutes only current_*/today tokens (compare-to.ts:55-72), so a next_month window [{next_month_start},{next_month_end}] is its own "previous period" (delta 0), and compareToTrendLabelKey matches /month_start/ and labels it "vs last month" (compare-to.ts:131-134). The dataset path (DatasetWidget.tsx:247-256) shifts the resolved window server-side and is right. The 29116 test narrowed its compareTo case from every preset to RETIRED_PRESETS (:151) without pinning what a forward preset does there or filing it; the PR body's "No domain:ui follow-up is needed" is stated for the preset list only and is true of that, but this inline-KPI reader needs a domain:ui card and a line in the body.
  6. Verify: fixtureDays (dashboard-window.ts:602-614) adds the future edges only when end > today; the 13 existing presets' days are unchanged by construction (their ends are ≤ today). Windows are resolved on the caller's timezone (:756-764) and the family already declines a non-UTC caller (:1018), so the future-edge rows are UTC days like the rest.

② Semver level

Clause-② yes (widening): three new accepted values on DATE_RANGE_PRESETS, reaching dashboard.dateRange.defaultRange, the date global filter's defaultValue, report.defaultRange (report.zod.ts:704, via DATE_RANGE_DEFAULT_RANGES) and AnalyticsQuery.timeDimensions[].dateRange (analytics.zod.ts:739). Metadata-protocol: added is right: new values on authorable enums, no removal, no tightening. The changeset's seven levels cover every moved package: @objectstack/spec minor, @objectstack/core minor (new behaviour in the resolver), @objectstack/lint patch and @objectstack/verify patch (lint is prose only; verify changes fixture days, no API), @object-ui/core minor (two new client tokens), @object-ui/components minor, @object-ui/i18n minor. plugin-dashboard and plugin-report derive and are untouched. Nothing is narrowed: PRESET_WINDOW_TOKENS and PresetWindowBound are module-private (analytics-date-range.ts:329-334), the spec table's row type is unchanged, and the 13 existing rows are byte-identical. Check Changeset is green; check-changeset-no-major passed per the dev report.

③ Boundary flags

  • Check-runs at read time: 23 completed (all success or skipped, incl. Check Changeset, Spec property liveness, Type Check · source gates, ESLint, PR-body guards) and 20 still in_progress (Test Core 1-8, Build Core/Docs/UI Deps, the Lint lanes, Type Check workspace/consumer/debt, Live E2E UI). No failure yet; the verdict below is on the code, and the seat must re-read the runs before merge.
  • The PR body says pnpm check:platform-checklist exits 1 on a main-borne finding (platform-core.json anchor assertProtocolCompat); not this PR's, and the api-backend.json item moved to revision 3 / expect 16 correctly.
  • Out-of-scope findings in the dev report (missing client next_week_end/next_year_end; the hand union in ui types) are correctly left out of this PR.
  • The verify family proves forward windows only when the seed door accepts future-dated rows; the dev measured 15/15 green, and the api-backend.json knownGaps line now says a forward window needs future rows.

④ Design judgment

Re-anchoring is the right shape for the ruling: it names the month and quarter after next without a new macro token (PERIOD_RE stops at next_, filter-tokens.ts:371) and without calendar arithmetic in core, and the property test (test :264-277, "the end of next_X is the start of next_X as seen from inside next X") states exactly the invariant it relies on. Placing each forward preset after the backward member of its family keeps the 13 existing positions and reads as the ruling's three additions, not option B. The remaining defects are a latent DST-gap round trip in the re-anchor, stale prose in two READMEs and one docs page, a body claim about the objectql door, and an inline-compareTo reader that the test quietly stopped covering. None contradicts the ruling or the spec extents, so the verdict is PASS with should-fixes.

Should-fix 1: analytics-date-range.ts:368-372 re-anchors on local midnight; on a zone that skips 00:00 of a period's first day (America/Asuncion 2023-10-01) next_month/next_quarter resolve empty. Anchor on the day's noon and add that row.
Should-fix 2: update packages/verify/README.md:44,167, content/docs/deployment/cli.mdx:2594,2600 and ui/packages/plugin-dashboard/README.md:415 (13 presets; "never after today"; list ending at last_90_days).
Should-fix 3: PR body "objectql temporal-comparand-door.ts … iterate DATE_RANGE_PRESETS" is false; the door reads no vocabulary. Say it is unaffected because it is field-typed.
Should-fix 4: file a domain:ui card for compare-to.ts previousPeriod on forward windows (self-comparison, "vs last month" label) and name it in the PR body; restore or replace the compareTo coverage the 29116 test dropped for the three presets.

Implemented-by: claude/project-thread-tg9eh0
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: PASS

A zone that springs forward at 00:00 on a period's first day (America/
Asuncion, 2023-10-01) has no midnight, and the day-start instant read as
the day before, so next_month and next_quarter came out empty there.
Also drops stale preset counts from the verify docs and lists the forward
presets in the plugin-dashboard README.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzj9os3R42oiDRwN6HZ
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 186fb60 · 2026-11-04T15:42Z

Delta review of fix round 1 (the four should-fixes of review 6634808446) at CONTRACT_REVIEW_TIER.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review — delta (fix round 1)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 186fb6019ead8de9ede5fae9d73611f4e8d32bac
Local-runs: none
Scope: the delta git diff f7e8b203..186fb601 (6 files, one commit, merge-base still f68b47c); record 6634808446 in full; the proposed body /mnt/project-files/seat3-orders/30076-body-r1.md against the live PR body; packages/core/src/utils/analytics-date-range.ts:120-240 (PresetWindowBound, PRESET_WINDOW_TOKENS, tokenDayStartMs, HALF_DAY_MS, boundDayStart, resolveAnalyticsDateRangePreset), analytics-date-range.test.ts:225-290 (#30076 block), datetime.ts:95-175 (zonedDateStartToUtcMs, zonedWallClockToUtcMs gap/overlap rules), filter-tokens.ts:269,417,450 (proxyDay); packages/verify/README.md:40-44,165-169; content/docs/deployment/cli.mdx:2586-2600; ui/packages/plugin-dashboard/README.md:415; ui/packages/core/src/utils/tests/dashboard-filters.presetWindows-29116.test.ts:60-75,140-170; compare-to.ts (diff stat: untouched); .changeset/30076-forward-date-range-presets.md (unchanged); cards #30134 and #30140; check-runs on the head.

① Derived judgments

  1. Should-fix 1 (noon anchor): closed. analytics-date-range.ts:200-201 defines HALF_DAY_MS and :218 anchors at tokenDayStartMs(bound.from) + HALF_DAY_MS before the second resolveFilterToken pass; :205-213 states the hazard and why noon. Only the re-anchored ends (next_month, next_quarter, :169-170) take this path; every string bound, i.e. both bounds of the 13 existing presets and of next_30_days, still goes through tokenDayStart unchanged (:217), so no existing window moves.
  2. Can +12h leave the day? No, at both edges. The base the zone helper returns is local 00:00 on a normal day; +12h is 12:00 local (13:00 across a morning spring-forward, 11:00 across a morning fall-back), inside a 23-, 24- or 25-hour day. On the degenerate gap day the base is not the day's first instant but one gap-width BEFORE it (datetime.ts:146-150: a skipped wall clock settles just before the gap), so +12h lands 12h minus the gap into the day: for Asuncion 2023-10-01 that is 11h into a 23-hour day; even a 2-hour gap gives 10h into a 22-hour day. A fall-back at midnight makes 00:00 ambiguous and the helper takes the first occurrence (:151-153), which IS the day's first instant, and the day is 25h. Then proxyDay (filter-tokens.ts:269,417) reads the anchor's calendar parts in the same zone, so the second pass sees the anchor day as today. The one-line comment at :200 ("from a day's first instant") is slightly loose on the gap day, but :207-210 says the base reads as the day before, so the pair is correct together.
  3. The two Asuncion rows (test :247-248) are right. Paraguay 2023: -04 standard, DST -03 from the first Sunday of October (2023-10-01) at 00:00 local to the fourth Sunday of March 2024. So 2023-10-01 00:00 -04 = 04:00Z is skipped straight to 01:00 -03. next_month from 2023-09-15T12:00Z (08:00 -04, Sep 15): next_month_start = 2023-10-01; helper → 00:00 at the post-gap offset -03 = 03:00Z (reads 23:00 -04 Sep 30); +12h = 15:00Z = 12:00 -03 Oct 1; next_month_start seen from Oct 1 = 2023-11-01, rendered at 00:00 -03 = 2023-11-01T03:00Z. next_quarter from 2023-08-15T12:00Z: anchor the same 15:00Z, next_quarter_start from Oct 1 = 2024-01-01, still DST, 00:00 -03 = 2024-01-01T03:00Z. Both rows match. Before the fix, the anchor 03:00Z read as Sep 30, next_month_start from there was Oct 1 again, end === start, as the record predicted.
  4. The window's start instant 2023-10-01T03:00Z (23:00 -04 on Sep 30) is pre-existing zone-helper behaviour, not this PR's. It is zonedDateStartToUtcMs('2023-10-01', 'America/Asuncion') resolving a skipped midnight to the earlier candidate, documented and pinned in datetime.ts:140-150 and shared by every preset whose bound falls on that day (this_month or this_quarter on 2023-10-15 in Asuncion opens at the same 03:00Z). The test comment at :242-246 says exactly this. Whether the helper should land post-gap (Temporal compatible) is a datetime.ts question outside [Decision] Relative dates look only backward: add 下月, 下季度 and 未来 N 天 for forecasting? #30076 and I do not carry it as a finding; it costs a 1-hour sliver before the day at every midnight gap for every preset, and both bounds of this PR's windows are rendered through that one helper, so they stay consistent with the 13.
  5. Should-fix 2 (stale prose): closed. packages/verify/README.md:44 "under each preset", :168-169 "never after today, except a forward preset's own edges, which are future days"; content/docs/deployment/cli.mdx:2595 the same clause, :2600 "under each preset"; ui/packages/plugin-dashboard/README.md:415 lists next_month | next_quarter | next_30_days | custom. A head-wide grep for "13 presets"/"each of the 13"/"thirteen" finds only CHANGELOGs, releases and an unrelated FeedItemType sentence; nothing in packages/, ui/ or hand-written docs. cli.mdx:2595 is one long line; a wrapping nit only, and the body says the docs gates ran green on it.
  6. Should-fix 3 (body): closed in r1 with one leftover. The objectql line now reads "does not iterate the list: it judges a comparand by the field's declared type", matching temporal-comparand-door.ts as reviewed. Follow-ups section names Console date macros: {next_week_end} and {next_year_end} stay unresolved in the client #30134 (p3, next_week_end/next_year_end) and Dashboard compareTo on a forward date preset (下月, 下季度, 未来 30 天) compares the window with itself #30140 (p2, compareTo), both open and routed domain:ui; Dashboard compareTo on a forward date preset (下月, 下季度, 未来 30 天) compares the window with itself #30140 carries Blocked-by [Decision] Relative dates look only backward: add 下月, 下季度 and 未来 N 天 for forecasting? #30076. The Server bullet describes the noon anchor and the Asuncion measurement; the DST table has the two rows; the Docs bullet names the three prose files; counts are consistent (71 + 2 = 73 in analytics-date-range.test.ts). Leftover: the Server bullet's second sentence still says "with the reference instant at the start of that day in the same zone" and then contradicts itself two sentences later ("stands at noon of the anchor day, not its first instant"). Should-fix, prose only.
  7. Should-fix 4 (compareTo): closed as carried. 29116 test :69-71 names Dashboard compareTo on a forward date preset (下月, 下季度, 未来 30 天) compares the window with itself #30140 and says why the case runs on RETIRED_PRESETS only; compare-to.ts is byte-identical between the two heads; the body's Pins bullet and Follow-ups name it.
  8. Nothing new of substance. The delta is the constant, the two JSDoc edits (:144-146, :201-213), two test rows, one test comment and the prose. No vocabulary, schema, changeset or export moved; the changeset's seven levels are unchanged and still cover every published package the PR touches. Observation, no fix owed: the property test at :255-266 re-anchors on w.start (the day-start instant), so on a midnight-gap zone it would read the previous day and fail; it passes because SWEEP_ZONES has no such zone. The concrete Asuncion rows are the pin for that case, which is enough.

② Semver level

Unchanged from the full review: Clause-② yes (widening), Metadata-protocol: added. The noon anchor changes only which instant the resolver stands on while naming a day; both rendered bounds are still day-start instants of resolver-named days. No bound of any of the 13 existing presets can move (they never take the re-anchored path), and @objectstack/core is already minor in the changeset. No ADR-0087 trigger.

③ Boundary flags

  • Check-runs on 186fb60 at read time: 8 completed (Auto Label, Check Documentation Links, Check PR Size, PR-body guards, Spec property liveness, Governed Surface Queue Guard success; Schedule gate, 18.x harness, Packed-tarball skipped) and 15 in_progress (Type Check workspace/source/consumer/debt, all Lint lanes incl. ESLint and PM dispatch-gates, Flag docs affected, Check Changeset). No failure; the seat must re-read before merge.
  • The live PR body is still r0; r1 is the proposed file and must be posted. r1 adds Fixes #30076 and the follow-ups section the record asked for.
  • I did not run node to confirm the Asuncion offsets; the derivation above is from the tz-db rule (Paraguay first-Sunday-of-October DST start at 00:00, in force in 2023) and the helper's documented gap rule. The test rows are the measurement.

④ Design judgment

The fix is the smallest one that closes the hazard: it moves where the resolver stands, not what it answers, and keeps the "one resolver, no calendar arithmetic in core" line the full review approved. Anchoring at noon rather than the gap-aware zonedWallClockToUtcMs({hour:12}) is equivalent here (the only question is which local day proxyDay reads) and avoids a second helper call. Prose is now count-free where it must be, the body says true things about the objectql door and names both carriers. Four should-fixes closed; one prose leftover in the body.

Should-fix 1 (body r1, Server bullet): replace "with the reference instant at the start of that day in the same zone" with "with the reference instant moved into that day (its noon) in the same zone" so the bullet does not contradict its own next sentence.

Implemented-by: claude/project-thread-tg9eh0
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

维护者速读

  • 改了什么: 日期预设表新增三个往后看的预设:next_month(下月)、next_quarter(下季度)、next_30_days(未来 30 天)。
    • 服务端 analytics-date-range 能解析这三个预设。下月、下季度的终点用「重锚定」算出,不新增宏 token。
    • 控制台 10 个语言包都补了标签。客户端宏解析补上了 next_month_end 和 next_quarter_end。
  • 为什么改: [Decision] Relative dates look only backward: add 下月, 下季度 and 未来 N 天 for forecasting? #30076 的裁决 A。销售预测问的都是往后看的问题。以前只能用自定义日期,保存后会过期。
  • 风险与回滚: 低。原有 13 个窗口一点没动,只是放宽,没有拒收任何值。回滚就是还原本 PR。
  • 先看: packages/spec/src/data/date-range-presets.ts(表);packages/core/src/utils/analytics-date-range.ts(重锚定,从锚定日的正午算起)。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/data/date-range-presets.test.ts。
  • 需要你做的: 无,席位落地。

席位意见:fable 契约复审两轮,都是 PASS。首轮提了 4 个建议项:

  1. 零点夏令时跳变的时区里,窗口会算成空。改为从正午锚定后,这个问题已修复。
  2. 几处文档还写着「13 个预设」,已更新。
  3. PR 描述有一行写错,已更正。
  4. 「对比上期」在往后看的窗口上会拿窗口和它自己比,已开后续卡 Dashboard compareTo on a forward date preset (下月, 下季度, 未来 30 天) compares the window with itself #30140(ui,p2)。

UI 侧还缺两个宏,开了后续卡 #30134(ui,p3)。

席位核对 — ACCEPT · domain:spec#3 · head 186fb6019ead8de9ede5fae9d73611f4e8d32bac

  • Shape: 31 files, +332 −42, on origin/main f68b47ca. The PR changes:
    • the spec table and its windows (inclusive last day), plus pins;
    • core's half-open windows, with a { token, from } re-anchored bound for the two period ends, anchored at the anchor day's noon, and conformance rows (Shanghai, New York DST, Chatham, Asuncion 00:00 gap);
    • the verify dashboard-window fixture, which now writes future edges;
    • on the ui side, only the readers that red: count and order pins, preset labels in all 10 packs (same-PR follow, ruling F), two client macro ends, and the filter-builder English defaults;
    • docs and READMEs.
  • Premise measured: the dev measured that next_30_days resolves directly (31_days_from_now). next_month and next_quarter had no token for the exclusive end, because PERIOD_RE stops at next_. The seat ruled for re-anchoring and no new token.
  • Gates: the dev ran 109 derived gates. One was red, check:platform-checklist, on a main-borne anchor this PR does not touch. Check-runs on this head were still running when this was posted. Auto-merge waits on them, and the seat re-reads any red.
  • Contract review:
    • Full review, 6634808446 on f7e8b203: PASS, with four should-fixes.
    • Delta review, 6635074212 on this head: PASS. All four are closed, and the delta review's one body-wording note was fixed in the seat's body refresh.
  • Declarations: Clause-②: yes (widening), Metadata-protocol: added (new accepted values on an authorable enum). The changeset covers the 7 moved packages. ADR-0087 is not needed.
  • Accepted deviations:
    • The ui edits go beyond the order's "no ui edits" line. The seat lifted that line on the card thread for exactly the readers that red, because ui takes spec as workspace:^.
    • A window's start instant on a 00:00-gap day settles one hour before local midnight. That is pre-existing zonedDateStartToUtcMs behaviour shared by every preset, not this PR's.
  • Carried:
  • Landing: not governed (check-governed-merges --pr 30133: 0 of 31 paths, 374 lines). The hand-off is pr_ready plus automerge_enable, through the merge queue.

The os verify --dashboard-windows pin counted 13 probes and 13 held; with
the three forward presets the run reads 16. Both now follow the
vocabulary's length.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzj9os3R42oiDRwN6HZ
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

席位核对 — ACCEPT carried to head 5af3917a89fc44c756aef0dfb386824b19e323f3

  • Why a new head: Test Core (4/8) on 186fb601 was red, and the cause was this PR. The test packages/cli/test/verify-dashboard-windows.test.ts:164 pinned probes: 13 and held: 13, and the run read 16 and 16. The cli test was a reader the first sweep missed.
  • Delta (1 file, +9 −1): the summary now reads both counts from DATE_RANGE_PRESETS.length, imported from @objectstack/spec/data (cli already depends on @objectstack/spec). No product code moved. The contract reviews 6634808446 and 6635074212 stand, and the seat read this test-only delta itself.
  • Dev runs: that test passed 3 of 3 against freshly built core, verify and cli. A grep of every suite that runs os verify --dashboard-windows found no other preset count.
  • Landing: unchanged: not governed, auto-merge stays enabled, through the queue.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit aa57c76 Nov 4, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-tg9eh0 branch November 4, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Relative dates look only backward: add 下月, 下季度 and 未来 N 天 for forecasting?

1 participant