Filed by the domain:spec seat 2 PM (session_014EJ1ED8X4MMrT18BhVx4tx) from the #20600 dev's out-of-scope finding (report on #20600), which the at-tier contract review 5891983885 on PR #20643 confirmed from the code. This is not the last-day defect #20600 fixes, and PR #20643 does not change it.
What was measured
The dev measured this at the published MemoryAnalyticsService.query / generateSql export of @objectstack/driver-memory, built at PR #20643's head:
- Two rows,
created_at = 2026-07-28T10:00Z and 2026-07-27T10:00Z, and a cube where { created_at: { $lte: '2026-07-28' } }.
- With
created_at undeclared, both rows are answered, and the SQL echo compiles the half-open bound 2026-07-29. That is ADR-0053's whole-day rule for a bare-day upper bound.
- With
created_at declared datetime (through syncSchema), only the 07-27 row is answered. The echo compiles an inclusive bound at 2026-07-28T00:00:00.000Z, so the rest of the named day is dropped.
- On the same data,
find() with the same filter answers both rows.
Cause (read from the code at origin/main)
In packages/drivers/driver-memory/src/memory-analytics.ts:
comparandsFor (:1538, called at :879 and :1350) turns the comparand into storage form through filterComparandStorageForm → coerceTemporalValue → temporalStorageForm. A bare day becomes 2026-07-28T00:00:00.000Z there.
- Only after that does the
lte row ask nextUtcCalendarDay for the next day. That helper declines an instant, so the whole-day widening never happens.
ADR-0053 orders these two steps the other way. Its D-E addendum says "Ordering is load-bearing where D-D meets D-E: the calendar-day upper-bound rewrite ... runs on the bare-day STRING first; only the resulting bound is converted to the storage form. Converting first would hand nextUtcCalendarDay a Date, which it correctly refuses to widen". So on this face the whole-day rule never reaches a declared datetime field, whatever the day.
Reach
- The measured door is the published
@objectstack/driver-memory export of MemoryAnalyticsService.
- The review found no in-repo door that constructs
MemoryAnalyticsService, so no HTTP route reaches it at origin/main. An app or test that uses the export directly does.
- Nothing here was measured at an HTTP boot.
Scope for whoever takes it
- Make the memory analytics face apply the whole-day rule before storage-form conversion, as ADR-0053 orders. Pin it with the undeclared and declared-
datetime pair above, answering the same rows as find().
$between's maximum and a dateRange end on the same face are the likely siblings. Measure them in the same pass.
Dedupe words: memory-analytics lte declared datetime whole day · comparandsFor storage form before nextUtcCalendarDay · cube where $lte bare day datetime memory analytics.
Generated by Claude Code
Filed by the
domain:specseat 2 PM (session_014EJ1ED8X4MMrT18BhVx4tx) from the #20600 dev's out-of-scope finding (report on #20600), which the at-tier contract review5891983885on PR #20643 confirmed from the code. This is not the last-day defect #20600 fixes, and PR #20643 does not change it.What was measured
The dev measured this at the published
MemoryAnalyticsService.query/generateSqlexport of@objectstack/driver-memory, built at PR #20643's head:created_at=2026-07-28T10:00Zand2026-07-27T10:00Z, and a cubewhere { created_at: { $lte: '2026-07-28' } }.created_atundeclared, both rows are answered, and the SQL echo compiles the half-open bound2026-07-29. That is ADR-0053's whole-day rule for a bare-day upper bound.created_atdeclareddatetime(throughsyncSchema), only the07-27row is answered. The echo compiles an inclusive bound at2026-07-28T00:00:00.000Z, so the rest of the named day is dropped.find()with the same filter answers both rows.Cause (read from the code at
origin/main)In
packages/drivers/driver-memory/src/memory-analytics.ts:comparandsFor(:1538, called at:879and:1350) turns the comparand into storage form throughfilterComparandStorageForm→coerceTemporalValue→temporalStorageForm. A bare day becomes2026-07-28T00:00:00.000Zthere.lterow asknextUtcCalendarDayfor the next day. That helper declines an instant, so the whole-day widening never happens.ADR-0053 orders these two steps the other way. Its D-E addendum says "Ordering is load-bearing where D-D meets D-E: the calendar-day upper-bound rewrite ... runs on the bare-day STRING first; only the resulting bound is converted to the storage form. Converting first would hand
nextUtcCalendarDayaDate, which it correctly refuses to widen". So on this face the whole-day rule never reaches a declareddatetimefield, whatever the day.Reach
@objectstack/driver-memoryexport ofMemoryAnalyticsService.MemoryAnalyticsService, so no HTTP route reaches it atorigin/main. An app or test that uses the export directly does.Scope for whoever takes it
datetimepair above, answering the same rows asfind().$between's maximum and adateRangeend on the same face are the likely siblings. Measure them in the same pass.Dedupe words:
memory-analytics lte declared datetime whole day·comparandsFor storage form before nextUtcCalendarDay·cube where $lte bare day datetime memory analytics.Generated by Claude Code