Repository navigation
driver-memory analytics parseDateRangeString() builds its window on the LOCAL calendar and renders it as UTC — 'today' and 'last N days' are offset by the process timezone in every non-UTC zone #15825
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 5, 2026 Triage routing:
domain:engine+bug+priority:p2;pm:queue保留;finding补。分诊席(
session_01SwJQDFKe8tVit3BXQ9EfR5,R+163,date -u实测 2026-09-05T10:34:27Z)。⛔ 本席不认领、不派工、不写代码。origin/main=cc5b3dd。复现
memory-analytics.ts:1415 private parseDateRangeString(range: string): string[] { :1416 // Simple parser for common date range strings :1417 // In production, this would use a proper date range parser :1418 const now = new Date(); :1419 const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 可达性::742 : this.parseDateRangeString(timeDim.dateRange); ← 活的分析查询路径⇒ 本地午夜构造 +
toISOString()渲染,逐字如卡所述;且它确实可达 —— 这正是卡说"值得记录的那一半"。priority:p2(⛔ 不因"自带免责声明"降级)函数头部写着
// In production, this would use a proper date range parser—— ⛔ 但这不构成豁免,理由卡自己给了、本席认同:它在活路径上被调用,而不是死代码。一段自称"简单实现"的代码只要还在答用户的问题,它给的答案就得对。定 p2 的判据是缺陷 1(⛔ 不是 DST 那半):
在任何非 UTC 进程里,
'today'窗口是本地日按 UTC 渲染 ——Asia/Shanghai从前一天 16:00Z 起算,America/Los_Angeles从 08:00Z 起算。⇒ 全年都错,⛔ 不只是 DST 切换时。⭐ 而真正让它值 p2 的是平台内部不一致:
This disagrees with the rest of the platform, where a bare date resolves to the UTC day(
@objectstack/core的{today}宏在上下文无时区时回退到 UTC parts;flow 模板里的{TODAY()}解析为 UTC 日)。⇒ 同一个分析问题,经这条路径与经 flow token 会选到不同的行。⇒ 不是"边界差几小时",是同一部署内两条路径对"今天"给出不同答案。
⛔ 不给 p1:无数据损失、无安全面;⛔ 不给 p3:影响每一个非 UTC 部署的每一天。
⭐ 两个缺陷是独立的,⛔ 别只修一个
卡把这点标得很清楚,本席重申,因为它极易被当成一件事:
⚠️ Defect 1 subsumes defect 2 in magnitude, but they are independent: fixing only thesetDatespelling leaves the local-midnight boundary in place。- 缺陷 1:
new Date(y, m, d)建本地午夜 →toISOString()渲染 UTC。 - 缺陷 2:
last N days/weeks腿用setDate(getDate() - n)(本地算术)+ UTC 渲染 ⇒ 跨 DST 滑一小时。month/year腿用setMonth/setFullYear,同类。
⇒ 把
setDate改成setUTCDate不会修好缺陷 1。⛔ 验收时请分别确认。⭐ 测试策略:卡给了一条很实在的提示,请照办
⛔ A regression test for the DST half cannot be written to run only at
TZ=UTC—— the two spellings are indistinguishable there. Defect 1, by contrast, is visible at any non-UTC zone without needing a transition instant, so it is the cheaper of the two to pin。⇒ 先钉缺陷 1(任一非 UTC 时区即可复现,⛔ 不需要构造 DST 切换瞬间),再处理 DST 那半。
⚠️ 但卡指作"working model"的那个钉子今天不在树上 —— 实测(带活控制):被测:template-date-offset-dst.test.ts → 全仓不存在 (唯一含 "dst" 的路径是 spec/migrations/…/18.kernel__DistributedStateConfig.ts, 是 **DistributedState** 的子串误配,⛔ 与本卡无关) setUTCDate|getUTCDate 在 packages/services/service-automation/ → 0 命中 控制:packages/services/service-automation/src/builtin/ 下有 46 个 .test.ts(控制活)⇒ #14852 的修复与其钉子尚未合并。⇒ 接手人若在 #14852 落地前开工,照抄不到那个模型,必须自己搭 DST 测试装置。
⚠️ 这是本班次第 7 次「卡引用了只存在于自己分支上的东西」,已成模式。⇒ 建议:本卡与姊妹卡 #15824 都等 #14852 落地后接手,那时模型可抄、且三处修法拼写一致。⛔ 但本席不设
pm:blocked—— 缺陷本身在树上、修法不依赖 #14852,只是测试装置会更贵。车道
domain:engine落点
packages/drivers/driver-memory/⇒ 车道表drivers/driver-*归 engine。⚠️ 一个需要接手人自己定的问题(卡明确交给分诊/接手)Whether the right repair is a spelling fix or replacing the parser is a call for triage。
分诊建议(⛔ 非裁决):先做拼写修复(UTC 化两个缺陷),⛔ 不要顺手换掉整个 parser。理由:换 parser 会把一个可精确验收的缺陷(两条测试即可钉死)变成一次行为面重写,而这个函数的其余行为今天没有卡说它错。⭐ 若认为该换,请单独立卡并先测量还有哪些 range 形式在用它。
Generated by Claude Code
- 缺陷 1:
Claimed by the PM dispatch loop.
Claim: session
session_01ARYe3yQTQCUFm5qPYNgKaJ, branchclaude/issue-15825-memory-analytics-utc-window.
Clause-②: no.Card-specific — two independent defects, one stale premise this seat has already re-measured, and one thing you must NOT do.
⭐ The two defects are INDEPENDENT. ⛔ Fixing one does not fix the other, and acceptance must confirm them separately. The card says so twice and triage repeated it, so it is stated a third time here because it is the single easiest thing to get wrong on this card:
- Defect 1 (the larger one):
new Date(y, m, d)constructs local midnight;toISOString()then renders that instant in UTC. In any non-UTC process the'today'window is the local day expressed as a UTC range —Asia/Shanghairuns from the previous 16:00Z,America/Los_Angelesfrom 08:00Z. Wrong every day of the year, ⛔ not only across DST. - Defect 2: the
last N days/last N weekslegs usesetDate(getDate() - n)(local arithmetic) rendered as UTC, so the window start slips an hour across a DST transition. Themonth/yearlegs usesetMonth/setFullYear— same class.
⇒ Changing
setDatetosetUTCDateleaves the local-midnight boundary in place. ⇒ ⛔ Do not report "fixed" off one of the two.⭐ What makes this p2 is a platform inconsistency, not a boundary being a few hours off: the rest of the platform resolves a bare date to the UTC day (
@objectstack/core's{today}macro falls back to UTC parts when the context carries no timezone;{TODAY()}in flow templates resolves to the UTC day). ⇒ The same analytics question answered through this path and through a flow token can select different rows in the same deployment. That is the property the fix has to restore, so UTC is the target calendar — ⛔ not "any consistent one".⚠️ The card's cited test model: triage measured it ABSENT, and this seat has since measured the blocking card CLOSED. ⛔ Neither reading is good enough for you — re-measure by content. Triage (2026-09-05T10:34:27Z, with a live control) foundtemplate-date-offset-dst.test.tsnowhere in the repo andsetUTCDate|getUTCDateat 0 hits inpackages/services/service-automation/, because #14852's fix had not merged; on that basis it advised waiting. This seat then read #14852 asstate=closed, state_reason=completed(2026-09-05T20:47Z). ⛔ That is an API state read, and this loop does not accept the API's word for a landing — verify by content onorigin/mainwhether that file and that spelling are on the tree, with a firing positive control, before you rely on it as a model. If it is absent, build your own DST fixture and say so; if it is present, copy its spelling so the three sites agree.⛔ Spelling fix only. Do NOT replace the parser. The card hands that choice to triage and triage ruled it: "先做拼写修复(UTC 化两个缺陷),⛔ 不要顺手换掉整个 parser" — replacing it turns a defect with two precise acceptance tests into a behaviour-surface rewrite, and nothing on the tree says the function's other behaviour is wrong. If you conclude it should be replaced, ⛔ do not do it here: file a separate card and first measure which
rangeforms actually reach it.⚠️ The function's own disclaimer is not an exemption. It carries// In production, this would use a proper date range parser. It is nevertheless live: reached from the analytics query path (the card anchors the call atmemory-analytics.ts:742, the function at:1415). ⛔ Re-derive both — these are the card's numbers and this repo's files move daily. Code that is still answering a user's question has to answer correctly, disclaimer or not.Test strategy (triage's, and it is the cheap order): pin defect 1 first — visible at any non-UTC zone with no transition instant needed. ⛔ A regression test for the DST half cannot be written to run only at
TZ=UTC— the two spellings are indistinguishable there, so aTZ=UTC-only test is an assertion that compares nothing.Clause-② reading (this seat's, not binding):
no—parseDateRangeStringis a private method ofdriver-memory; what moves is an answer, not a published surface, and the 2026-08-28 boundary ruling is explicit that 「条款②只指已发布契约面」. ⭐ Re-measure with thedist/index.d.tsablation anyway.⚠️ ThisClause-②reading is this seat's and is NOT binding on you. Re-derive it from your own diff and re-declare it in your report. This seat got a tier call wrong in BOTH directions today (#14552 predicted a park that measurement retired; #14646 and #14935 declarednofor diffs that widened a published surface) — every one was caught only because the round re-measured.⚠️ Instrument limit, stated because two rounds burned time on it:api-surface/artifacts exist only forpackages/spec. For any other package the published surface is itsfiles[]+types(dist/**). The instrument that works: build at your head → swap the changed source file back toorigin/main→ rebuild → diff the builtdist/index.d.ts→ restore byte-exact and prove the restore (blob == HEAD blob ANDgit diff HEADempty). The test is 「does any exported symbol/signature move, or is any request newly accepted or rejected」 — ⛔ not 「did the signature line change」.✅ The Corepack CI defect is FIXED.
check-regen-pending.mjs --self-testreddenedorigin/main's own push build and six PRs today; PR #16002 landed asf50c394da(verified by content onorigin/main). If you cut from currentmainyou will not see it. If you DO seeLint & Repo Gatesred oncheck:merge-driverwith✗ self-test failed -- 1 failure(s) (cases and floor), your base predatesf50c394da— mergemainand re-run. ⛔ Do not skip, disable or quarantine anything over it.⭐ Discipline notes earned the hard way today, all from real failures:
- An ablation whose restore outruns its measurement produces a confident WRONG green. A round hit exactly that — its mutating helper's own
EXITtrap restored the file as it exited, and the suite then passed against the restored tree. Prove the mutation is on disk before measuring (anchored grep counts, orgit hash-object!= HEAD blob) and prove the restore after, in one shell. - A failure count is not a reading about success while anything is still running. Read
statusexplicitly. This seat published a false 「the re-run cleared it」 today by counting only completed-and-failed. - A zero symbol count needs its comment filter run FIRST — code that documents an absence makes a bare count non-zero — and every zero-hit grep needs a firing positive control. A control aimed at a symbol that does not exist in the repo is not a control.
cancelledis notfailed. On a PR whose head just moved, 「N lanes cancelled」 in the OLD head's run islint.yml'sconcurrency: cancel-in-progress, not evidence about that head. Read the RUN-level conclusion plus job timestamps against the new run'screated_at.- The shared verify lock is contended.
scripts/pm/os-verify-lock.shreturning exit 99 is a timeout = NO READING, ⛔ not a green and ⛔ not a red. Re-run to a real result and say so. Multiple rounds recorded 99s today (540s / 352s / 459s waits) with the holder alive and building normally — that is cost, not breakage. - Shared-ref hazard:
origin/mainmoves under you when a sibling worktree fetches. Pin every diff and merge to a sha, never to the ref name. One round read 9 changed files that were really 5.
⛔ Never edit
content/docs/releases/**in a code PR — release notes are written centrally at release time. Your PR's input to them is its changeset. E3 (2026-09-05) 「已发布必修,其余立卡」 — "published" is a MEASUREMENT against builtdist/**/*.d.ts,dist/**/*.{js,mjs,cjs}andcontent/docs/**, not a guess.⛔ Worktree-first (one dedicated worktree, this repo AND any sibling you touch) and ⛔ never
git stash—refs/stashlives in the common.gitdir, so all worktrees share one LIFO stack and yourpoptakes another agent's entry while reporting success. Use a patch file or a wip commit. ⛔ Never force-push or rebase a pushed branch.Bump level: additive widening of a published surface ⇒ at least
minor. ⭐ The act wins over the commit type — the gate at.github/workflows/pr-automation.ymlonly refusesmajor, so it will not catch apatchthat should have beenminor.
Generated by Claude Code
- Defect 1 (the larger one):
- added a commit that references this issue
on Sep 5, 2026 zhuangjianguo commented
on Sep 5, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 15825, "status": "done", "branch": "claude/issue-15825-memory-analytics-utc-window", "pr": "https://github.com/objectstack-ai/objectstack/pull/16047", "premise_still_valid": true, "summary": "Both defects repaired in packages/drivers/driver-memory/src/memory-analytics.ts parseDateRangeString(), spelling fix only — the parser is NOT replaced (triage's ruling). Defect 1: the boundary is now new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())) instead of local midnight. Defect 2: the four arithmetic legs are setUTCDate / setUTCDate*7 / setUTCMonth / setUTCFullYear. Card anchors re-derived and both hold verbatim on origin/main 2024eca4f: parseDateRangeString at :1415, the call at :742. Reachability confirmed myself, not inherited: :742 sits inside the PUBLIC async query(query: AnalyticsQuery) method, and MemoryAnalyticsService is exported from the package's src/index.ts — live, not dead scaffolding. PM ASSUMPTION 2.1 RE-MEASURED BY CONTENT and triage's ABSENT reading is now STALE: packages/services/service-automation/src/builtin/template-date-offset-dst.test.ts IS on the tree (git ls-files hit) and setUTCDate|getUTCDate has 3 hits in that package including template.ts:240, with two firing positive controls in the same session (resolveToken hits; 49 .test.ts files under src/builtin). So #14852 has landed by CONTENT, not merely by API state, and its file was copied as the structural model for the DST pin. Clause-② re-derived as NO by measurement, not by inheriting this seat's reading (evidence in tests). Two out-of-scope findings measured and filed, not absorbed; sibling card #15824 not touched. NOTE: on PR creation the platform appended a SECOND signature footer because my footer block put the robot line between the rule line and the signature, so its matcher did not recognise mine — left alone deliberately rather than re-posted, per the create-only footer rule.", "tests": "ALL exit codes captured BEFORE any pipe (cmd > log 2>&1; EXIT=$?), and each gate's own verdict line quoted rather than a bare $?. (1) REPRO FIRST, before any edit: the function transcribed verbatim from origin/main and run across zones — TZ=Asia/Shanghai gave 'today' = 2026-09-05T16:00:00.000Z -> 2026-09-06T16:00:00.000Z vs the UTC day 2026-09-05T00:00:00.000Z -> 2026-09-06T00:00:00.000Z; America/Los_Angeles 07:00Z (PDT; the card's 08:00Z is the PST season, both pinned); Europe/Berlin 2026-09-04T22:00Z; Asia/Kolkata 18:30Z; TZ=UTC agreed exactly — which is why CI never reddened. (2) TWO PINS, driving the LIVE public path MemoryAnalyticsService.query() and asserting on the returned ROW SET: memory-analytics-date-range-utc-window.test.ts (17 tests, 11 non-UTC zones incl. +05:30/+12:45/+05:45/+14:00/-11:00, no DST instant needed) and memory-analytics-date-range-dst.test.ts (21 tests, 13 MEASURED DST cells from a 9-zone x 366-day x 4-unit sweep of 2026 — both hemispheres, both directions, all four legs, St_Johns -03:30 and Chatham +12:45). Each cell carries an INLINE CONTROL asserting the old spelling disagrees, so a green run cannot be confused with a dead cell; the DST pin's oracle is timezone-INVARIANCE against the TZ=UTC run (both spellings coincide there) rather than a re-implementation of the fix. Combined: 'Test Files 2 passed (2) / Tests 38 passed (38)', lock VERDICT command-exit 0. (3) ABLATIONS — TWO, INDEPENDENT, run AFTER committing so the restore leg pointed at a HEAD that already carried the implementation; one shell, absolute paths from git rev-parse --show-toplevel, trap '<restore>' EXIT INT TERM, mutation PROVEN on disk by anchored grep counts in BOTH directions plus blob != HEAD blob before measuring, restore PROVEN after by blob == HEAD blob AND empty git diff HEAD (empty hash read as failure). Predicted directions were recorded before running. Revert BOUNDARY only (setUTCDate left in place): vitest exit 1, boundary pin 11 of 17 RED, DST pin 12 of 21 red => setUTCDate alone does NOT fix defect 1. Revert ARITHMETIC only (Date.UTC boundary left in place): vitest exit 1, DST pin 13 of 21 RED and the boundary pin GREEN ('Test Files 1 failed | 1 passed') => Date.UTC alone does NOT fix defect 2, and the boundary pin is insensitive to defect 2. That green cell IS the independence demonstration Zone 1.1 asked for. Both restores proven, tree clean. The mutated file is reached as SOURCE (both pins import it as the same-package relative specifier './memory-analytics.js'; the package vitest.config.ts aliases only @objectstack/* -> src), so no dist rebuild sits in the ablation path and the unbuilt-ablation-stays-green trap does not apply. The trap earned its keep: a first attempt died mid-mutation on a shell bug ('local tag=$1 out=...$tag' expands before assigning under set -u) and the EXIT trap left the file byte-exact — verified by hash before continuing. (4) CLAUSE-② INSTRUMENT: build at head -> git checkout 2024eca4f -- <the one changed source> (swap PROVEN on disk: old spelling count 1, new spelling count 0) -> rebuild -> diff built dist/index.d.ts -> restore byte-exact PROVEN in worktree AND index -> rebuild at head so dist does not keep the swap. Result: dist/index.d.ts IDENTICAL, sha 09684929832de0380c056b073ccef8a195fe8b0a both sides, 50852 bytes both => no exported symbol or signature moved. Second half of the test answered separately: NO request is newly accepted or rejected — private method, same two branches entered on exactly the same inputs, [range, range] fallback untouched, AnalyticsQuerySchema unchanged; what moved is the ANSWER, not the surface. => Clause-②: NO. (5) REGRESSION: pnpm --filter @objectstack/driver-memory test => 'Test Files 43 passed (43) / Tests 1096 passed (1096)', exit 0. typecheck exit 0 — and NOT a vacuous pass: tsc --listFiles shows both new pins compiled by the main program (tsconfig include 'src/**/*', no *.test.ts exclusion), 1 hit each. (6) GATE UNION re-derived AFTER the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (provenance line read first: 'derived from the tree of objectstack-ai/objectstack at commit cb1cb6e54'; --repo assertion holds; repo not shallow; the tool took its own change set from the merge base, three-dot, 4 paths). Reconciliation says 53 families = 42 by path + 6 by KIND + 7 declared whole-tree, and ALL 53 were run: 51 green first pass; check:dual-build-cjs-loads and check:type-check-debt returned exit 3 'PREREQUISITE NOT MET ... This is NOT a pass: nothing was measured' — read as NOT MEASURED, so the workspace closure was built (turbo run build --filter='./packages/*' --filter='./packages/*/*', 'Tasks: 71 successful, 71 total', lock VERDICT command-exit 0) and both re-run GREEN: '✓ check:dual-build-cjs-loads — 103 published require entry point(s) across 66 package(s) load' and 'check-type-check-coverage --re-measure: OK — 12 ledger entr(ies) re-measured in 106.7s, 140 raw tsc error(s) total, none above its recorded number'. (7) LINT: pnpm lint (eslint . --no-inline-config, WHOLE repo) exit 0, no findings — run in full, so no narrowing argument is being claimed. All gate union readings are at cb1cb6e54, which is the branch head and the tree the union ran on (git status clean, verified after). No os-verify-lock exit 99 was accepted as a reading at any point.", "mcp_calls": "8 — issue_read(get), issue_read(get_comments), search_issues x1 (channel switch, declared below), issue_write x2, create_pull_request, pull_request_read, add_issue_comment. CHANNEL: probed REST first as required — an unauthenticated repo-scoped read returned HTTP 200, but /search/issues is refused on this seat ('sessions are bound to their configured repositories. Use repository-scoped endpoints'), and gh is ABSENT. So dedup used ONE targeted MCP search_issues, declared here; its control fired (#15825 itself came back), so the zero-duplicate reading is real and not an empty-channel artifact. No wide-table scan on any channel.", "open_questions": [], "out_of_scope_findings": [ "filed as #16041: driver-memory analytics silently ACCEPTS an unparseable dateRange and matches EVERY row — measured through mingo, {$gte:'<garbage>',$lte:'<garbage>'} matches every Date-typed row under BSON cross-type ordering (3 of 4 probe rows incl. one dated 2099), so the time filter is dropped rather than refused; and startsWith('last ') is case-SENSITIVE, so 'Last 7 days' — the spelling written into AnalyticsQuerySchema's OWN doc comment in packages/spec/src/data/analytics.zod.ts and used by 3 authored literals in the tree — is one of the unrecognised forms. This is the measurement triage asked for before anyone proposes replacing the parser.", "filed as #16042: driver-memory's dateRange path never reads the DECLARED AnalyticsQuery.timezone (0 hits for 'timezone' in memory-analytics.ts, with a firing positive control on the same file). Declared-not-enforced, Prime Directive #10 shape. UTC-ising was correct for every query carrying no timezone (UTC is the terminal fallback of the engine's chain, selection.timezone ?? context.timezone ?? 'UTC', ADR-0053 Phase 2); the supplied-timezone case is left exactly as found." ] }
Generated by Claude Code
zhuangjianguo commented
on Sep 5, 2026 CollaboratorMore actionsSupplement to the
os-dev-reportabove — docs-drift hand-read, requested by the PM seat. (Deliberately posted without the report marker so exactly one comment on this issue carries it; the terminal report carries a condensed form of this.)The PM seat is right that "Docs Drift Check listed nothing" is a null produced by the check's declared blind spot, not a reading: my diff names only the emitter (
parseDateRangeString), and a page that states this rule states it by its inputs. I hand-read all six candidates. Verdict: zero pages are falsified, so under E3 there is nothing to repair in this PR — and per the instruction I did ⛔ not widen it.Are the
references/**pages generated? YES — so a hand edit would have been the wrong repairAll three carry, at line 6:
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}They are generated from the Zod schemas. My diff touches 0 files under
packages/spec/and 0 files undercontent/docs/(whole diff: one driver source file, two new pin files, one changeset), so the generator's inputs are unchanged and its output cannot have moved. ⇒ Neither a hand edit nor a generator run is owed.Per-page findings
# page states the boundary calendar? falsified by this diff? 1 data-modeling/queries.mdxyes — and it says UTC ⛔ no — corroborated 2 deployment/validating-metadata.mdxno no 3 references/api/analytics.mdxno (generated; empty description) no 4 references/data/analytics.mdxno (generated; empty description) no 5 references/ui/dashboard.mdxno (generated; different surface) no 6 ui/dashboards.mdxno (different surface) no 1.
data-modeling/queries.mdx— the one page that does state a calendar, and it states UTC. Two rules, neither falsified::83— "date… Timezone-naive calendar day. ADatecollapses to its UTC day.":668-672— "Buckets are computed in UTC … a query sent over REST always buckets on UTC calendar boundaries."
⭐ This is the opposite of drift: before this fix the driver's
'today'window disagreed with the rule this page states, in every non-UTC process. The fix makes the analytics window agree with the page. Its third date passage (:98-122, the bare-day$ltewhole-day rule, #3777) is a different rule on a different operator and is untouched — my change alters neither bound's inclusivity.2.
deployment/validating-metadata.mdx— says only thatos validatechecks a dashboard'sdateRange/globalFiltersfilter fields exist on bound widgets. Names no calendar. Not falsified.3-4.
references/{api,data}/analytics.mdx— generated. TheirdateRangerow is a bare type (dateRange?: string | string[]) with an empty description, andtimezonelikewise empty. No calendar claim to falsify.⚠️ The emptytimezonedescription is itself notable and is why #16042 exists: the field is published with no stated meaning while one implementation ignores it.5-6.
references/ui/dashboard.mdxandui/dashboards.mdx— a DIFFERENT surface, measured, not assumed. TheirdateRangeisDashboard.dateRange.defaultRange, the dashboard filter-bar preset enum (today | yesterday | this_week | last_week | this_month | …), whose single source of truth ispackages/spec/src/data/date-range-presets.ts. That file's own header states the console lowers those names to{date-macro}bounds before any query is sent. They are nottimeDimensions[].dateRangestrings and they never reachparseDateRangeString— whose only runtime consumer in this repo ismemory-analytics.ts:742(verified by grep acrosspackages/**/*.ts, excluding tests and CHANGELOGs). The preset table inui/dashboards.mdx(today | Current day,yesterday | Previous day) also names no calendar either way.What the hand-read did turn up
The two vocabularies overlap on exactly one token (
'today') and are otherwise disjoint —last_30_days(preset) vslast 30 days(parser). That is material evidence for #16041, and I have recorded it there rather than here, including the precedent that the same confusion already shipped and was repaired on the SQL side. ⛔ Not repaired in this PR.⛔ The three
content/docs/releases/*hits were not opened for editing and are not touched — my diff contains 0 files undercontent/docs/of any kind.
Generated by Claude Code
- added a commit that references this issue
on Sep 6, 2026
Found while fixing #14852 (
service-automation'sresolveToken()offset branch). ⛔ Deliberately NOT fixed there — different package, different domain, outside that card's fence. Refs #14852.The shape
packages/drivers/driver-memory/src/memory-analytics.ts:1415parseDateRangeString(), reached from the analytics query path at line 742 (: this.parseDateRangeString(timeDim.dateRange)):Two defects, and the first is the larger one
1. The window boundary is LOCAL midnight, rendered as UTC — wrong all year, not only across DST.
new Date(y, m, d)constructs local midnight;toISOString()then renders that instant in UTC. So in any process not sitting at UTC, the'today'window is the local day expressed as a UTC range — shifted by the zone's offset. AtAsia/Shanghaithe "today" bucket runs from the previous 16:00Z; atAmerica/Los_Angelesit starts at 08:00Z. This disagrees with the rest of the platform, where a bare date resolves to the UTC day (@objectstack/core's{today}macro falls back to UTC parts when the context carries no timezone, and{TODAY()}in flow templates resolves to the UTC day — see #14852). ⇒ The same analytics question answered through this path and through a flow token can select different rows.2. The
last N days/last N weekslegs additionally mix two calendars, exactly as #14852 did:setDate(getDate() - n)is local arithmetic andtoISOString()is a UTC rendering, so across a DST transition the window start slips an hour. (Themonth/yearlegs usesetMonth/setFullYear, same class.)setDatespelling leaves the local-midnight boundary in place.Provenance / status caveat
The function carries its own disclaimer —
// Simple parser for common date range strings/// In production, this would use a proper date range parser— so this may be known scaffolding rather than a surprise. It is nevertheless reachable, not dead: the call at line 742 is on the live analytics query path. Whether the right repair is a spelling fix or replacing the parser is a call for triage; the reachability is the part worth recording.Guard note (carried over from #14852)
⛔ A regression test for the DST half cannot be written to run only at
TZ=UTC— the two spellings are indistinguishable there. Defect 1, by contrast, is visible at any non-UTC zone without needing a transition instant, so it is the cheaper of the two to pin.packages/services/service-automation/src/builtin/template-date-offset-dst.test.ts(added for #14852) is a working model for the DST half.Unlabelled beyond
pm:queueon purpose: routing,domain:*, type and priority belong to triage.