Skip to content

[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

@objectstack-fleet

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:

  1. 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.
  2. 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

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: records and reports — a date bound means the same day on every face | 缺项 (memory analytics converts a bare-day $lte comparand to storage form before the whole-day rule, so on a declared datetime field the rest of the named day is dropped) | P2

    Triage: 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 #20643

    Triage: 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-memory export only, so the reach is narrow.
    • If a door is found, it regrades to p1 on the report.

    Direction.

  2. added
    area:reportsBusiness reporting — dashboards, reports, the numbers a manager reads
    bugSomething isn't working
    and removed on Sep 29, 2026
  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 24
    Session: session_01DEvba2nBuD4tWzfq8r8NFY
    Account: os-support-ai (the seat's linked user as GET /user answers 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: the lte rows of both predicate tables (mingo and the SQL echo), and comparandsFor or 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. ⛔ nextUtcCalendarDay is not taught to widen an instant.
    • the siblings, measured in the same pass: $between's maximum and a dateRange end 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-datetime pair, both answering the same rows as find(), and one pin per sibling found.
    • .changeset/20661-*.md (@objectstack/driver-memory, patch).

    Stop on breach and explain in the report. ⛔ Not packages/spec calendar-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 against origin/main e4e5222b7.

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20714 @ 8cae2ae39

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-29T20:40Z. The seat is the reviewer of record. Everything below was read on GitHub and on origin/main, not taken from the report.

    • Shape: a draft against main. The first line is Fixes #20661, and no other closing keyword touches another card. Clause-②: no is in the body and in the changeset. The PR assignee is os-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-memory patch). No governed surface is touched.
    • The fix:
      • lteUpperBound widens the AUTHORED bare day, and only the widened bound goes to storage form (ADR-0053 D-E3).
      • The mingo $match and the generateSql echo both render that one decision.
      • comparandsFor became storageFormFor, which is still the one conversion point.
      • nextUtcCalendarDay is untouched.
    • Measured first: before the fix, the declared-datetime pair answered one row where find() answered two. After it, the answers equal find(), and a literal id list is asserted as well. Triage's siblings:
      • $between is refused INVALID_FILTER / 400 on this face, and the refusal is pinned;
      • the dateRange end already widened first, so it is pinned and not changed;
      • the declared-datetime 9999-12-31 bound had the same order defect and now asks only for a value, as find() does. It is pinned.
    • Controls unchanged: an undeclared field, a declared date field, and a full timestamp or Date comparand.
    • 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_progress at the review's read), the seat flips it ready and arms auto-merge.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #20714 as d3f88faf3

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-29T21:02Z.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions