Repository navigation
Commit bc4c963
fix(driver-memory): a preset dateRange on a date field counts the reference zone's calendar days, from UTC westward too (#27341)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgKURKuRGF1ewEVVZKx7U1i4)_
Fixes #27272
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:内存驱动的分析查询里,预设日期范围(如“本季度”)落在声明为 `date`
的字段上时,改为按参考时区的日历日计数;`datetime` 字段仍按时刻比较。这条窗口规则从 service-analytics 挪到
`@objectstack/core`,两边共用同一份。
- 为什么改:从 UTC 往西(洛杉矶,连 UTC 本身也是),“本季度”会漏掉 7 月 1 日、多算 10 月 1
日,报表数字静默出错;上海只是碰巧算对。
- 风险与回滚:只影响“预设范围 × 已声明为 date 的字段”,在内存驱动上;显式起止日期、datetime
字段、未声明类型的字段行为不变,service-analytics 行为不变(只是改为调用 core)。回滚即还原本 PR。
- 先看:`packages/core/src/utils/analytics-date-range.ts`
末尾新增的两个导出;`packages/drivers/driver-memory/src/memory-analytics.ts` 的
`parseDateRangeString`;`packages/services/service-analytics/src/dataset-executor.ts`
删掉的旧副本。
- 两分钟验证:`pnpm --filter @objectstack/driver-memory exec vitest run
src/memory-analytics-date-range-date-field.test.ts`,预期 7
条全过,其中洛杉矶、UTC、上海的 this_quarter 都只列 7/1 和 9/30。
- 需要你做的:无,席位落地。
## What changes
**The rule has one home.** ADR-0053's window rule (a `date` column
compares the calendar days the period covers in the reference zone; a
`datetime` column compares instants) was spelled once, in
`service-analytics/src/dataset-executor.ts` (`dateRangeWindowBounds`
over the private `inclusiveCalendarDayWindow`, PR #27271).
`@objectstack/driver-memory` depends only on `core`, `spec` and `types`,
so it cannot import that. Both functions move, unchanged in behaviour,
into `@objectstack/core`'s `utils/analytics-date-range.ts`, beside
`resolveAnalyticsDateRangeString`, whose header already says the
lowering lives there because these two faces cannot import each other.
`dataset-executor.ts` imports both from core and re-exports
`dateRangeWindowBounds`, so the four service-analytics importers keep
their import line. Nothing in `packages/spec` moves.
**The memory face uses it.** `MemoryAnalyticsService` asks the driver
whether the time dimension's field is declared `date`. If it is, a
preset resolves through `dateRangeWindowBounds(window, timezone, true)`
to the inclusive day pair, and the existing bound construction reads
that pair exactly as it reads a caller's bare-day `[a, b]` (`$gte` first
day, `$lt` the next day after the last). Every other field keeps the
instant window it had.
**One driver accessor.** The declared kind lives in `InMemoryDriver`'s
private `temporalFields` (filled by `syncSchema`), so the driver gains
`isDeclaredDateField(object, field)`, the same pattern as
`filterComparandStorageForm`. An object never declared answers `false`:
the driver does not guess types from values.
**Surface widening.** The order's file surface was
`memory-analytics.ts`, its test, the rule's home and
`dataset-executor.ts`. It grew by
`packages/drivers/driver-memory/src/memory-driver.ts` (the accessor
only) and `packages/core/src/utils/analytics-date-range.ts` (+ its
test), on the seat's call (option A; option B, inferring the kind by
probing a comparand's stored shape, was refused as a type guess from a
value). `Clause-②: yes (widening)`: `@objectstack/core` gains the
exports `dateRangeWindowBounds` and `inclusiveCalendarDayWindow`, and
`InMemoryDriver` gains the public method `isDeclaredDateField`. Nothing
is removed or narrowed. One changeset, `patch` for `core`,
`driver-memory` and `service-analytics`.
## Tests
- New
`packages/drivers/driver-memory/src/memory-analytics-date-range-date-field.test.ts`
(7): the card's rows (06-30, 07-01, 09-30, 10-01), clock frozen at
2026-09-09, declared via `syncSchema`. `this_quarter` in
`America/Los_Angeles`, `UTC` and `Asia/Shanghai` selects 07-01 and
09-30; Los Angeles `this_month` selects 09-30 and not 10-01; an explicit
`['2026-07-01', '2026-09-30']` is unchanged. A declared `datetime` field
keeps the zone-midnight instant pair (both edges pinned one second
either side). An undeclared field keeps the instant pair.
- `packages/core/src/utils/analytics-date-range.test.ts` (+5): the rule
itself, three zones, the non-date arm and the explicit arm.
<details><summary>Red before the fix (origin/main ebfadb0)</summary>
```
× America/Los_Angeles: this_quarter selects Jul 1 through Sep 30
× UTC: this_quarter selects Jul 1 through Sep 30
× Los Angeles: this_month on Sep 9 selects Sep 30 and not Oct 1
AssertionError: expected [ '2026-09-30', '2026-10-01' ] to deeply equal [ '2026-07-01', '2026-09-30' ]
Tests 3 failed | 4 passed (7)
```
Shanghai and the datetime and undeclared pins were green before and
after. UTC is red too, not only zones west of it: the bare day
`2026-07-01` sorts before `2026-07-01T00:00:00.000Z` as text.
</details>
<details><summary>Ablation (fix committed first, then the face's date
branch disabled through scripts/ablation-replace.mjs)</summary>
```
ablation-replace: ok mutation landed: anchor 1 -> 0, blob a0d8cde -> c3b278652e1c
× America/Los_Angeles: this_quarter selects Jul 1 through Sep 30
× UTC: this_quarter selects Jul 1 through Sep 30
× Los Angeles: this_month on Sep 9 selects Sep 30 and not Oct 1
Tests 3 failed | 4 passed (7)
ablation-replace: blob after restore a0d8cde
```
A first attempt used a marker that threw a ReferenceError (red for the
wrong reason) and is discarded; the run above is the one that counts.
`git diff HEAD` was empty after the restore.
</details>
<details><summary>Package suites (HEAD 4ddf1d4)</summary>
```
@objectstack/core test + typecheck: Test Files 124 passed, Tests 2808 passed
@objectstack/driver-memory test + typecheck: Test Files 74 passed, Tests 1767 passed
@objectstack/service-analytics test + typecheck: Test Files 195 passed, Tests 4705 passed | 282 skipped
eslint --no-inline-config on the six touched source files: exit 0
```
</details>
## Acceptance notes
- **Deviation, for triage to rule on if it disagrees: an undeclared
field keeps the instant window.** The card measured the undeclared case
wrong as well. This PR follows `dateRangeWindowBounds`' own contract
from #27172 / PR #27271, "a column whose type the caller cannot name
answers `false` and keeps the instants", and this driver's rule that it
does not guess types from values (`memory-temporal.ts`). So a bare day
stored in a field the driver was never told is `date` is still compared
to the preset's instants as text. Making it count calendar days would
need the face to read the type from the stored value, which is the
option the seat refused.
- The face's `$or` over both stored forms is unchanged; on a declared
`date` field the `Date` arm reads the day pair as UTC midnights, as it
does for a caller's bare-day window.
- No behaviour change in service-analytics: its four call sites resolve
the same function, now from core.
- The branch carries two early WIP commits (`7cd672d6`, `46faf53d`)
committed without the session trailer pair; the final commit `4ddf1d4a`
carries it. The branch name is not `claude/issue-*`, so it is not
force-pushed and history is not rewritten.
## Verification
Local subset, measured at `4ddf1d4a`: `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 64 families from the 7 changed
paths; each ran from the worktree with its exit code captured before any
pipe.
<details><summary>dispatch-gates --ran: 64 derived, 63 run, 1
NOT-MEASURED, 0 UNRUN</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-conformance :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:kernel-hook-pairs :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
- `pnpm check:dual-build-cjs-loads` exits 3 (PREREQUISITE NOT MET: it
needs every workspace package built, and this worktree built only the
three packages' closure). NOT MEASURED locally; CI runs it.
- `node scripts/check-plugin-teardown-shape.mjs --self-test` first
exited 3 on the shallow clone; after `git fetch origin 621a487` it
exits 0 (48 cases).
</details>
Every other family CI runs is read from this PR's check-runs, not re-run
locally.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CbomWvWL9K2F1XBS6p)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 79d5dac commit bc4c963
7 files changed
Lines changed: 313 additions & 85 deletions
File tree
- .changeset
- packages
- core/src/utils
- drivers/driver-memory/src
- services/service-analytics/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
45 | 46 | | |
46 | 47 | | |
47 | 48 | | |
| |||
369 | 370 | | |
370 | 371 | | |
371 | 372 | | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | | - | |
| 86 | + | |
| 87 | + | |
87 | 88 | | |
88 | 89 | | |
89 | 90 | | |
| |||
280 | 281 | | |
281 | 282 | | |
282 | 283 | | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
33 | 37 | | |
34 | 38 | | |
35 | 39 | | |
| |||
1196 | 1200 | | |
1197 | 1201 | | |
1198 | 1202 | | |
1199 | | - | |
| 1203 | + | |
| 1204 | + | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
1200 | 1208 | | |
1201 | 1209 | | |
1202 | 1210 | | |
| |||
2243 | 2251 | | |
2244 | 2252 | | |
2245 | 2253 | | |
| 2254 | + | |
| 2255 | + | |
| 2256 | + | |
2246 | 2257 | | |
2247 | 2258 | | |
2248 | 2259 | | |
2249 | 2260 | | |
2250 | 2261 | | |
2251 | | - | |
| 2262 | + | |
2252 | 2263 | | |
| 2264 | + | |
| 2265 | + | |
| 2266 | + | |
| 2267 | + | |
| 2268 | + | |
| 2269 | + | |
| 2270 | + | |
| 2271 | + | |
| 2272 | + | |
| 2273 | + | |
2253 | 2274 | | |
2254 | 2275 | | |
2255 | 2276 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2359 | 2359 | | |
2360 | 2360 | | |
2361 | 2361 | | |
| 2362 | + | |
| 2363 | + | |
| 2364 | + | |
| 2365 | + | |
| 2366 | + | |
| 2367 | + | |
| 2368 | + | |
| 2369 | + | |
| 2370 | + | |
| 2371 | + | |
| 2372 | + | |
| 2373 | + | |
| 2374 | + | |
| 2375 | + | |
2362 | 2376 | | |
2363 | 2377 | | |
2364 | 2378 | | |
| |||
0 commit comments