Found while working on #16042 (the AnalyticsQuery.timezone repair in the same function). ⛔ Deliberately NOT fixed there — that card's fence was WHICH calendar the window is anchored to; this is the window's upper-bound OPERATOR, a pre-existing question in both calendars. Refs #16042, #15825.
The shape
packages/drivers/driver-memory/src/memory-analytics.ts:760 widens a dateRange end with nextUtcCalendarDay(end):
const nextDay = nextUtcCalendarDay(end);
const stringBounds = nextDay != null
? { $gte: start, $lt: nextDay } // bare `YYYY-MM-DD` end -> half-open, correct
: { $gte: start, $lte: end }; // anything else -> INCLUSIVE
nextUtcCalendarDay widens only a bare YYYY-MM-DD string and returns null for a full ISO timestamp — that is its documented contract (packages/spec/src/data/calendar-day.ts:42, pinned by calendar-day.test.ts:29-31). So every end that is a timestamp rather than a bare day falls to the $lte branch and the window becomes closed at both ends.
Measured through the public entry, against the BUILT package
Explicit array bounds, so no relative-token resolution is involved:
rows: 2026-09-06T00:00:00.000Z, 2026-09-06T12:00:00.000Z, 2026-09-07T00:00:00.000Z
query: timeDimensions: [{ dimension: 'events.createdAt',
dateRange: ['2026-09-06T00:00:00.000Z','2026-09-07T00:00:00.000Z'] }]
selected: ALL THREE, including 2026-09-07T00:00:00.000Z
⇒ the instant that begins the NEXT day is counted inside the window that ends there.
Why it matters beyond an explicit array
parseDateRangeString('today') returns exactly such a pair — the day's start instant and the next day's start instant. So dateRange: 'today' counts a row stamped at precisely 00:00:00.000 of tomorrow. Two adjacent day windows therefore overlap at one instant, and a row landing on it is counted in both. It is a one-instant error, but it is a silent double-count, and a CURRENT_TIMESTAMP-written row truncated to the second is not as improbable as a millisecond-precision one.
This is calendar-independent: it was true when the window was the UTC day and it is still true now that a supplied timezone moves the window (#16042), because the operator choice never depended on which calendar produced the bounds.
The decision it needs
The comment at the call site states the intent — "Both spellings are half-open on a bare-day end (#4042; the SQL twin is #3777)" — so half-open is clearly the design for the bare-day case. The question is whether a full-timestamp end should be half-open too:
- Half-open everywhere (
$lt on any end): makes 'today' exactly one day and removes the overlap; but it silently narrows an explicit dateRange: [a, b] that a caller wrote meaning "inclusive of b", which is the reading $lte gives today.
- Keep
$lte for an explicit timestamp end, make the relative tokens emit a bare-day or an exclusive end: fixes the token path without changing what an explicit array means.
⇒ Contract-first, so filing rather than patching. ⚠️ Related but distinct from #16041, which is about an unparseable dateRange matching every row; this one is about a parsed window's boundary.
Unlabelled on purpose: routing, domain:*, type and priority belong to triage.
Generated by Claude Code
Found while working on #16042 (the
AnalyticsQuery.timezonerepair in the same function). ⛔ Deliberately NOT fixed there — that card's fence was WHICH calendar the window is anchored to; this is the window's upper-bound OPERATOR, a pre-existing question in both calendars. Refs #16042, #15825.The shape
packages/drivers/driver-memory/src/memory-analytics.ts:760widens adateRangeend withnextUtcCalendarDay(end):nextUtcCalendarDaywidens only a bareYYYY-MM-DDstring and returnsnullfor a full ISO timestamp — that is its documented contract (packages/spec/src/data/calendar-day.ts:42, pinned bycalendar-day.test.ts:29-31). So every end that is a timestamp rather than a bare day falls to the$ltebranch and the window becomes closed at both ends.Measured through the public entry, against the BUILT package
Explicit array bounds, so no relative-token resolution is involved:
⇒ the instant that begins the NEXT day is counted inside the window that ends there.
Why it matters beyond an explicit array
parseDateRangeString('today')returns exactly such a pair — the day's start instant and the next day's start instant. SodateRange: 'today'counts a row stamped at precisely 00:00:00.000 of tomorrow. Two adjacent day windows therefore overlap at one instant, and a row landing on it is counted in both. It is a one-instant error, but it is a silent double-count, and aCURRENT_TIMESTAMP-written row truncated to the second is not as improbable as a millisecond-precision one.This is calendar-independent: it was true when the window was the UTC day and it is still true now that a supplied
timezonemoves the window (#16042), because the operator choice never depended on which calendar produced the bounds.The decision it needs
The comment at the call site states the intent — "Both spellings are half-open on a bare-day end (#4042; the SQL twin is #3777)" — so half-open is clearly the design for the bare-day case. The question is whether a full-timestamp end should be half-open too:
$lton any end): makes'today'exactly one day and removes the overlap; but it silently narrows an explicitdateRange: [a, b]that a caller wrote meaning "inclusive of b", which is the reading$ltegives today.$ltefor an explicit timestamp end, make the relative tokens emit a bare-day or an exclusive end: fixes the token path without changing what an explicit array means.⇒ Contract-first, so filing rather than patching.⚠️ Related but distinct from #16041, which is about an unparseable
dateRangematching every row; this one is about a parsed window's boundary.Unlabelled on purpose: routing,
domain:*, type and priority belong to triage.Generated by Claude Code