Repository navigation
[finding] driver-memory analytics: a cube where $lte bare day on a declared datetime field drops the rest of that day — comparandsFor converts to storage form before the whole-day rule runs #20661
Description
Activity
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsPath: records and reports — a date bound means the same day on every face | 缺项 (memory analytics converts a bare-day
$ltecomparand to storage form before the whole-day rule, so on a declareddatetimefield the rest of the named day is dropped) | P2Triage: first grade —
bug·priority:p2·domain:engine·area:reports·pm:queue. Direction: ADR-0053's order (the whole-day rewrite on the bare-day string first, then storage form), with the siblings measured in the same pass. Serial after PR #20643Triage: lands in
packages/drivers/driver-memory/src/memory-analytics.ts(comparandsFor,:1538) ⇒domain:engine.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T16:04Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2.
- It is silent wrong data (NORTH-STAR rule 1): the measured cube answers one row where
find()on the same filter answers two. - But no in-repo door constructs
MemoryAnalyticsService. It is reached through the published@objectstack/driver-memoryexport only, so the reach is narrow. - If a door is found, it regrades to p1 on the report.
Direction.
comparandsForapplies the calendar-day upper-bound rewrite (nextUtcCalendarDay) to the bare-day string first, and converts only the resulting bound to storage form. That is exactly the "ordering is load-bearing" sentence of ADR-0053's D-E addendum. ⛔ Don't teachnextUtcCalendarDayto widen an instant: it correctly refuses one.- Siblings, same pass:
$between's maximum and adateRangeend on this face. Fix each one found under the same rule. - Pins: the card's undeclared / declared-
datetimepair, both answering the same rows asfind(), plus one pin per sibling found. - Serial after PR fix(spec,drivers): a datetime $lte or $between maximum on 9999-12-31 includes the whole last supported day (#20600) #20643 ([finding] nextUtcCalendarDay('9999-12-31') answers '10000-01-01', so on SQLite a datetime $lte '9999-12-31' or a $between maximum on that day answers no rows #20600,
os-tesla), which fixes the9999-12-31maximum on the same bound path.
- It is silent wrong data (NORTH-STAR rule 1): the measured cube answers one row where
- addedarea:reportsBusiness reporting — dashboards, reports, the numbers a manager readsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 29, 2026 objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsClaim: PM loop round 24
Session:session_01DEvba2nBuD4tWzfq8r8NFY
Account:os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20661-memory-analytics-whole-day-first
Worktree:objectstack-issue-20661
Domain:domain:engine
Seat:domain:engine#1
File surface (triage 5893953164):packages/drivers/driver-memory/src/memory-analytics.ts: thelterows of both predicate tables (mingo and the SQL echo), andcomparandsForor the builder context they read. The calendar-day upper-bound rewrite runs on the bare-day STRING first, and only the resulting bound is converted to storage form, as ADR-0053 D-E3 orders. ⛔nextUtcCalendarDayis not taught to widen an instant.- the siblings, measured in the same pass:
$between's maximum and adateRangeend on this face. Each one found is fixed under the same rule; one not reachable on this face is reported with the measurement. - tests in
packages/drivers/driver-memory/src/: the card's undeclared / declared-datetimepair, both answering the same rows asfind(), and one pin per sibling found. .changeset/20661-*.md(@objectstack/driver-memory,patch).
Stop on breach and explain in the report. ⛔ Not
packages/speccalendar-day.ts, and ⛔ not the other drivers' bound paths.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5893953164
Serial constraints cleared: read at 2026-09-29T19:52Z againstorigin/maine4e5222b7.- Triage's "serial after PR fix(spec,drivers): a datetime $lte or $between maximum on 9999-12-31 includes the whole last supported day (#20600) #20643" is cleared: it merged as
1a75e39d4(the last touch ofmemory-analytics.ts). - None of the 9 open PRs' file lists touches
packages/drivers/driver-memory/src/.
- added 3 commits that reference this issue
on Sep 29, 2026 objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20661, "status": "done", "branch": "claude/issue-20661-memory-analytics-whole-day-first", "pr": "https://github.com/objectstack-ai/objectstack/pull/20714", "session": "session_01DEvba2nBuD4tWzfq8r8NFY", "premise_still_valid": true, "summary": "Reproduced at the built @objectstack/driver-memory export on origin/main fa0a4b6610. With created_at declared datetime, a cube where { created_at: { $lte: '2026-07-28' } } answered r27 only, and the echo compiled created_at <= '2026-07-28T00:00:00.000Z'. find() answered r27 and r28. The undeclared field answered both rows with < '2026-07-29'. Fix in memory-analytics.ts: a new lteUpperBound(authored, comparand, storageForm) widens the AUTHORED bare day and converts only the widened bound through the same per-member storage-form conversion (comparandsFor became storageFormFor, still one home, driver.filterComparandStorageForm). Both lte rows (mingo and SQL echo) only render that one decision (ADR-0053 D-E3). After the fix, the declared datetime field answers r27 and r28, and the echo compiles created_at < '2026-07-29T00:00:00.000Z'. Hypotheses: H1 held (lines 273/468/1555/886/1367 at fa0a4b6610). H2 held ($between refused INVALID_FILTER/400 on both exits, no row added, refusal pinned on the date shape). H3 held (dateRange end widens the raw end; declared datetime ['2026-07-01','2026-07-28'] answers r27,r28 = find(); pinned, not changed). H4 held (declared date answers and echo made_on < '2026-07-29' byte-identical before and after; pinned). H5 held (the #20600 pins stay green; the declared-datetime last day was also broken by this order, answering c26,none against find()'s c26,mid,none with <= '9999-12-31T00:00:00.000Z'; it now compiles IS NOT NULL and is pinned). Reach unchanged: git grep 'new MemoryAnalyticsService(' finds 0 non-test hits against 31 in tests (control); service-analytics plugin.ts:245 only names a dev-plugin fallback in a comment. Other faces, by reading only: native-sql-strategy.ts:1244, objectql-strategy.ts:548, preview-evaluator lteBound and mongodb-filter.ts:1165 all widen before converting (or never convert), so none has this order. The harness attribution reminder asked for a model-named Co-Authored-By trailer; commits carry the AGENTS.md model-free pair instead, and the PR footer uses the AGENTS.md session-URL form. PR CI was in_progress at report time (31 check runs: 11 success, 3 skipped, 17 in_progress).", "tests": "At HEAD 8cae2ae398. (1) pnpm --filter @objectstack/driver-memory test: Test Files 63 passed (63), Tests 1456 passed (1456). (2) pnpm --filter @objectstack/driver-memory typecheck (tsc --noEmit && tsc --noEmit -p tsconfig.typecheck.json): exit 0; tsc --listFiles on the first program lists the new test file. (3) New file src/memory-analytics-20661-lte-whole-day-first.test.ts, 7 cases: the card's undeclared/declared-datetime pair, full-timestamp instant control, declared date control, declared-datetime last day and the day before, dateRange sibling, $between refusal envelope (code INVALID_FILTER, status 400) on both exits. Each rows case asserts cube rows == find() and == a literal list, plus the echo WHERE. (4) Ablation: fix committed first (cb155edf6c). The subject is imported relatively from src, so no dist is on the path and no build or dist preflight applies. scripts/ablation-replace.mjs under a trap restoring HEAD replaced 'const nextDay = nextUtcCalendarDay(authored);' with '...(comparand);' (the old order): anchor x1 to x0, replacement x0 to x1, blob 014497594cc6 to a41c37fac411. vitest over the new file plus memory-driver-calendar-day-upper-bound.test.ts: Tests 2 failed | 15 passed. The failures were the declared-datetime pair ('expected [ r27 ] to deeply equal [ r27, r28 ]') and the declared last day ('expected [ c26, prev ] ...'); every control stayed green, the expected direction. Restore: blob after restore == HEAD blob 014497594cc6, git diff HEAD empty. (5) Gates: node scripts/pm/dispatch-gates.mjs --commands derived 60 commands; all 60 exit 0. check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first exited 3 (PREREQUISITE NOT MET) and were re-run green after pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' (71/71 tasks). --ran: '60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN'. (6) Narrowed lint at 8cae2ae398: eslint --no-inline-config --format json over the 2 touched .ts files: 2 files, 0 errors, 0 warnings. Population: eslint --print-config resolves a config for both files (6 and 5 rules). Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project, header at :327), so untouched files' verdicts cannot move.", "mcp_calls": "0", "api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write, executed as objectstack-fleet[bot]): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, run 36626431281, PR #20714 draft; (2) label-write --assign os-support-ai, POST /repos/objectstack-ai/objectstack/issues/20714/assignees, run 36626529459, read back MATCHES; (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/20661/comments via post-stamped.mjs. Plus git push, which is not REST: the empty branch first, then 3 commits.", "open_questions": [], "out_of_scope_findings": [], "gates": "dispatch-gates --commands at 8cae2ae398: 60 derived, 60 run, all exit 0; --ran reconciliation 60/60, 0 NOT-MEASURED, 0 UNRUN. Plus pnpm --filter @objectstack/driver-memory test (1456/1456) and typecheck (exit 0).", "line_budget": "changed lines 290 (+271 / -19) over 3 files, against the 5000 human-merge threshold: under. No governed surface is touched.", "deviations": [ "Branch not merged forward from origin/main. It is 2 commits behind (9b384f63ae service-storage docs, 5757463712 platform-objects translations); neither touches driver-memory, spec or core. CI's merge ref covers them.", "Three gates first exited 3 (PREREQUISITE NOT MET, no built closure). A whole-closure turbo build was run under the verify lock (5m07s held) and the three were re-run green; none is recorded as NOT MEASURED.", "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. The AGENTS.md model-free trailer pair and session-URL footer were used instead." ], "files_changed": [ ".changeset/20661-memory-analytics-lte-whole-day-first.md", "packages/drivers/driver-memory/src/memory-analytics-20661-lte-whole-day-first.test.ts", "packages/drivers/driver-memory/src/memory-analytics.ts" ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsACCEPT — PR #20714 @
8cae2ae39domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T20:40Z. The seat is the reviewer of record. Everything below was read on GitHub and onorigin/main, not taken from the report.- Shape: a draft against
main. The first line isFixes #20661, and no other closing keyword touches another card.Clause-②: nois in the body and in the changeset. The PR assignee isos-support-ai. - Scope: 3 files, +271/-19, all inside the claimed surface:
memory-analytics.ts, a new test file beside it, and.changeset/20661-*.md(@objectstack/driver-memorypatch). No governed surface is touched. - The fix:
lteUpperBoundwidens the AUTHORED bare day, and only the widened bound goes to storage form (ADR-0053 D-E3).- The mingo
$matchand thegenerateSqlecho both render that one decision. comparandsForbecamestorageFormFor, which is still the one conversion point.nextUtcCalendarDayis untouched.
- Measured first: before the fix, the declared-
datetimepair answered one row wherefind()answered two. After it, the answers equalfind(), and a literal id list is asserted as well. Triage's siblings:$betweenis refusedINVALID_FILTER/ 400 on this face, and the refusal is pinned;- the
dateRangeend already widened first, so it is pinned and not changed; - the declared-
datetime9999-12-31bound had the same order defect and now asks only for a value, asfind()does. It is pinned.
- Controls unchanged: an undeclared field, a declared
datefield, and a full timestamp orDatecomparand. - Contract review: at-tier record 5898421456 on this head, PASS (read-only,
Local-runs: none). Every flag is answered and nothing is escalated. - Reach: there is no non-test
new MemoryAnalyticsService(, so triage's p2 stands. - Out-of-scope findings: none. 0 filed.
Landing: once every check on this head concludes green (12 were
in_progressat the review's read), the seat flips it ready and arms auto-merge.
Generated by Claude Code
- Shape: a draft against
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsLanded — PR #20714 as
d3f88faf3domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T21:02Z.- Verified on
main:d3f88faf3is a squash with one parent (575746371) and an ancestor oforigin/main.lteUpperBoundappears inpackages/drivers/driver-memory/src/memory-analytics.tsat the squash (7 hits) and not at its parent (0). - Route: ready and auto-merge through the relay at the reviewed head
8cae2ae39;added_to_merge_queueat 20:45Z, then merged by the queue at 21:01Z. - Card: closed
completedby the PR'sFixes #20661;pm:dispatchedis removed in this act. The lane's closed set since the previous landing reads [finding] driver-memory analytics: a cubewhere$ltebare day on a declareddatetimefield drops the rest of that day —comparandsForconverts to storage form before the whole-day rule runs #20661 alone.
Generated by Claude Code
- Verified on
- added 3 commits that reference this issue
on Oct 7, 2026
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