Skip to content

driver-sql + driver-memory: an epoch-millisecond NUMBER against a date field is read by neither driver's storage rule — where: { placed_on: { $gt: 1769940000000 } } returns 6 of 6 rows on SqlDriver and 0 on InMemoryDriver over REST #20203

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site, where a numeric temporal comparand meets a date column:

Finding class (a). reach: was measured at the public REST door (below).

The domain:engine execution seat 1 (session_01Bvd69VPa6puiNzzPUroDBx) filed this from its #20176 dev's report (os-dev-report 5854303341 on #20176, PR #20202), out-of-scope finding 2. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.

What happens

Measured by the #20176 dev on a real InMemoryDriver and a real SqlDriver (better-sqlite3 :memory:), schema-synced, over a 6-row object whose placed_on is a date field. The readings are identical at base cfe2387a3b and at PR #20202's head:

Door where: { placed_on: { $gt: 1769940000000 } } (a JS number, epoch ms)
REST POST /api/v1/data/:object/query, SqlDriver 6 rows: SQLite orders every INTEGER below every TEXT, so every stored date string is "greater"
REST, InMemoryDriver 0 rows: mingo never matches a number against text
engine.find, both drivers the same split
  • The two drivers disagree, and neither reads the number as a date. SqlDriver returns every row for a $gt bound, which is a silent wrong answer at a public door.
  • The same comparand on a datetime field IS read by the storage rule: epoch ms converts to the instant, and both drivers count 3.
  • The temporal-comparand door (assertTemporalComparandsInterpretable) judges only strings, by design, so a number reaches the drivers unjudged.

Suggested shape (⛔ not a ruling)

One of the two, decided once for every temporal face:

The rule lives in ONE place: temporalStorageForm in @objectstack/core once PR #20202 lands. Pin it on both drivers, through the engine and REST, with $gt / $lt / $eq / $in / $between, and with a datetime control.

Filing-gate answers

Dedupe words: epoch ms number date field where drivers disagree · date field numeric comparand sqlite integer text ordering · temporal comparand number date column

Activity

  1. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer: a third public-door answer, on live PostgreSQL. domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T09:14Z. ⛔ Not a claim.

    PR #20202's at-tier contract review (record 5854540769) measured this card's row on a private live PostgreSQL 16.13 as well. Over REST, where: { placed_on: { $gt: 1769940000000 } } on a date field answers DATABASE_ERROR / 500 ("Internal server error"). So one comparand now has three answers across three drivers: InMemoryDriver 0 rows, SQLite 6 rows, PostgreSQL 500. The readings are identical at PR #20202's base and head.

  2. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: an API a customer can call | api-backend.query-contract-matrix | P2

    Triage: first grade — bug · priority:p2 · domain:engine · area:api · pm:queue

    Triage: lands in packages/core/src/utils/temporal-storage-form.ts, the one storage-form rule that PR #20202 lifted into core (merged 2026-09-27T09:34Z), which both drivers now read (driver-sql temporalFilterValue, driver-memory coerceTemporalValue) ⇒ domain:engine. Rationale: an epoch-ms number against a date field is read by no storage rule. One REST request gets three answers: InMemoryDriver 0 rows, SQLite 6 of 6, PostgreSQL DATABASE_ERROR / 500 (pointer 5854552106). The same number on a datetime field is read as an instant on both drivers (3 = 3). It is a silent wrong answer, plus a 500, at a public door. It is the same family as #20176 (p2, just closed) ⇒ p2. reach: is measured over REST on three drivers.

    Triage seat (objectstack-wide, seat post #6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-27T10:11Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card and its pointer, #20176 and PR #20202 (both closed and merged), and #8690.

    The serial constraint is gone. PR #20202 merged, so the rule this card changes now lives in one place, temporalStorageForm. It is not folded into #20176: that card is closed, and its rows were the per-aggregation positions, not where.

    Direction. Read or refuse must be decided once for every temporal face, as the card says. On the texts already in force the answer is read:

    ⇒ Read an epoch-ms number on a date field as its UTC calendar day, in temporalStorageForm, the same rule for every caller. Stop valve: if ADR-0053 (the date / datetime semantics record) states that a date comparand must be a calendar-day string, refuse at the temporal-comparand door instead, cite the sentence, and report the choice here. ⛔ Not a third copy in a driver.

    Execution notes.

    1. Pin $gt / $lt / $eq / $in / $between on a date field with an epoch-ms number, on both drivers, through the engine and REST, with the datetime control.
    2. Record in ## Acceptance notes the PostgreSQL reading after the change (the 500 must be gone). Use the live cell PR fix(objectql,core): a per-aggregation filter and having read a temporal comparand by the column's storage rule — one rule in core, shared with both drivers' where #20202's review used, if it is reachable.
  3. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    on Sep 27, 2026
  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 22
    Session: session_01Bvd69VPa6puiNzzPUroDBx
    Account: os-sales (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20203-epoch-ms-date-comparand
    Worktree: objectstack-issue-20203
    Domain: domain:engine
    Seat: domain:engine#1
    File surface:

    • packages/core/src/utils/temporal-storage-form.ts: the date rule. Following triage 5854940483, an epoch-ms NUMBER is read as its UTC calendar day, the same rule for every caller. The stop valve: if ADR-0053 states that a date comparand must be a calendar-day string, refuse it at the temporal-comparand door instead, cite the sentence, and stop and report;
    • tests in packages/core, the two drivers and REST (test side only);
    • .changeset/20203-*.md.

    Stop on breach and explain in the report. ⛔ No third copy in a driver: driver-sql and driver-memory read temporalStorageForm since PR #20202. ⛔ Not packages/spec. ⛔ Not the datetime / time rules, which must stay byte-identical.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5854940483
    Serial constraints cleared: at 2026-09-27T10:45Z, the region predecessor PR #20202 (#20176) has landed as e5cf27d65f and is verified on main (landing record 5854697554). A census of the 12 open PRs finds none on temporal-storage-form.ts, sql-driver.ts or memory-temporal.ts. The seat runs batch:3 (the maintainer's ruling); this is the only card in flight.

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20203,
    "status": "done",
    "branch": "claude/issue-20203-epoch-ms-date-comparand",
    "pr": "#20224",
    "session": "session_01Bvd69VPa6puiNzzPUroDBx — mode:subagent; the id the claim 5855157485 names, and the one this container derives (CLAUDE_CODE_REMOTE_SESSION_ID cse_01Bvd69VPa6puiNzzPUroDBx → session_…)",
    "premise_still_valid": true,
    "summary": "temporalStorageForm (@objectstack/core) now reads a finite number on a date field as the UTC calendar day of new Date(value): the same conversion a Date takes, so a number and its Date always agree. NaN, ±Infinity and numbers past the Date range come back unchanged. driver-sql (toDateOnly / temporalFilterValue), driver-memory (coerceTemporalValue) and the engine's per-aggregation filter / having already call this rule, so there is no driver source change. One REST request that answered memory 0, SQLite 6 of 6 and PostgreSQL DATABASE_ERROR/500 now answers the date's own count on all three (e.g. $gt 1769940000000 → 1 row), and where = per-aggregation filter = having on every cell. Every datetime, time and non-number date cell is byte-identical base → head. Stop valve: ADR-0053 states no requirement that a date comparand be a calendar-day string, so the read direction was taken. Draft PR #20224; the card had assignee os-sales on arrival, and PR #20224 now carries the same assignee.",
    "tests": "Commits: the fix ba5c0de; tests 6cb41f4; changesets 4c8b92e; merge of main 16aedca; final 1c8c5ce (types the three new pins' query options, test files only). Pins: core temporal-storage-form.test.ts 72 passed (file unchanged since 4c8b92e); at 1c8c5ce driver-sql sql-driver-20203-epoch-ms-date-comparand.test.ts on SQLite + live PostgreSQL 16.13 (TZ=America/New_York, server Asia/Shanghai) 46 passed | 1 skipped (MySQL declared unprovisioned); driver-memory memory-20203-epoch-ms-date-comparand.test.ts 23 passed; rest data-query-epoch-ms-date-comparand.test.ts 18 passed. Whole suites at 16aedca, after merging main 4df101c and rebuilding the closure (vitest run, no --project, so both local and repo projects): core 57 files / 1488 passed; driver-memory 55 / 1351 passed; driver-sql with live PostgreSQL 197 passed, 3 skipped files / 3852 passed, 87 skipped (reporter: sqlite RAN, live postgres RAN, live mysql NOT RUN); objectql 319 / 5767 passed; rest (includes every query-route test) 201 / 3582 passed, 1 skipped. Typecheck: core at 16aedca, driver-memory / driver-sql / rest at 1c8c5ce, exit 0 each; every new/changed test file is in a compiled program by --listFiles (tsconfig.json for the drivers, tsconfig.test.json for core and rest); test-typecheck ledgers unchanged (core 4 held, rest 0). Ablation at 4c8b92e (fix committed first; ablation-replace wrap mode with EXIT/INT/TERM restore on the absolute path): anchor "const instant = typeof value === 'number' && Number.isFinite(value) ? new Date(value) : value;" x1 → x0, blob 61a6c0861a → 2a9bd5d997; core rebuilt; ablation-dist-preflight --absent exit 0 (marker gone from dist). Mutated: core 13 failed / 59 passed; memory 12 failed / 11 passed (the empty-object rows); driver-sql 35 failed / 11 passed / 1 skipped (SQLite empties green, PG empties red: the 500 came back); rest 17 failed / 1 passed (the min(datetime) control). Restored by the tool: blob == HEAD 61a6c0861a, git diff HEAD empty, whole-tree git status --porcelain empty; core rebuilt; preflight marker present exit 0; pins 72 / 23 / 46+1 skipped / 18 passed. Reverse-verification direction observed: turned red (the ordinary direction).",
    "mcp_calls": "0 — no MCP GitHub tool was called (reads were REST GETs; writes went through scripts/pm and the fleet-write relay)",
    "api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot], each carried by one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) = #20224, relay run 36317832633; (2) assign via scripts/pm/label-write.mjs → POST /repos//issues/20224/assignees [os-sales], relay run 36317881511, read back MATCHES; (3) this os-dev-report → POST /repos//issues/20203/comments via scripts/pm/post-stamped.mjs. Plus git push of the branch (not a REST write). No label was written by this run (the PR's documentation / size/l / tests / tooling labels were applied by the repo's labeler).",
    "open_questions": [
    {
    "question": "The date rule's Date conversion spells a year outside 1000..9999 unpadded (999-06-15, 10000-01-01, -1-01-01), so its text stops ordering as a YYYY-MM-DD day. This PR routes a number through that same conversion (H3: a number and its Date agree), so a REST number now reaches the spelling too. os-dev rule 3 says a published defect this round touches must be fixed; the dispatch ruling says every non-number date comparand stays byte-identical, and fixing the spelling moves Date cells. Which governs?",
    "options": [
    "A — leave this PR as is (number ≡ Date on the whole Date range); file the spelling as its own class (a) card, whose ruling may move Date cells deliberately (4-digit pad for years 0..999; a decided reading for years past 9999 and below 0, which have no YYYY-MM-DD spelling at all).",
    "B — pad the year to 4 digits for numbers only in this PR: the number path becomes right for years 0..999, but a number and its Date disagree there (breaks H3), and years past 9999 / below 0 stay unresolved.",
    "C — fix the shared conversion here (pad 0..999) and move the Date cells, overriding the dispatch ruling; still leaves years past 9999 / below 0 without a pinned form."
    ],
    "recommendation": "A. Real business need: epoch-ms numbers before year 1000 or after 9999 on a date field have no measured writer; the measured pull is the card's 2026 numbers, which this PR serves. Long-term soundness: one conversion for Date and number (no third dialect); the unpadded spelling is a Date-arm defect, and its right fix (including the out-of-range years with no YYYY-MM-DD form) is a decision, not a mechanical edit, so the bounded-fix exemption's "shape already pinned" condition fails. Keeping AI from writing wrong code: B would teach two spellings of one day depending on input type, the exact dialect the single rule removed. Startup focus: no new surface; the edge gets its own card with evidence."
    }
    ],
    "out_of_scope_findings": [
    "class: a · driver-mongodb src/mongodb-temporal.ts storageDateValue (its own copy of the date rule, outside this claim) returns a finite number unchanged: probe storageDateValue(1769940000000) → 1769940000000 while core temporalStorageForm(1769940000000, "date") → "2026-02-01" (a Date and an ISO string agree on both). On MongoDB a number against the YYYY-MM-DD text meets BSON type bracketing, the mechanism behind driver-memory's 0 at base; the live Mongo answer is NOT MEASURED (no server here). Fix shape: re-point driver-mongodb's date/time arms at core's rule, as PR #20202's acceptance note already suggested. · dedupe words: driver-mongodb epoch ms number date field · mongodb storageDateValue number unchanged · mongodb-temporal date rule copy",
    "class: a · the date rule's Date conversion (${y}-${m}-${d} from getUTCFullYear, in core temporal-storage-form.ts, formerly each driver's copy) spells a year outside 1000..9999 unpadded, so the text no longer orders or parses as a YYYY-MM-DD day. Measured at head, same six-row fixture, REST number and in-process Date agree: year 999 → memory/SQLite $gt 0 / $lt 6 (PG 6 / 0, correct); year 10000 → memory/SQLite 6 / 0 (PG 0 / 6, correct); year -1 → PG DATABASE_ERROR / 500. Reachable in-process by a Date before this PR, and by a REST number after it. See open_questions[0]. · dedupe words: date rule year below 1000 unpadded · calendar day year outside 4 digits text order · toDateOnly getUTCFullYear padding",
    "carrier: 承接者:无 · SqlDriver.toDateOnly's docstring still says "Anything else (and null/undefined) passes through unchanged", and a number no longer does. driver-sql source is outside the claim (test side only). Noted in PR #20224 Acceptance notes, not filed."
    ],
    "gates": {
    "derived_at": "1c8c5ce756",
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths); stderr line 1 names objectstack-ai/objectstack at 1c8c5ce; 62 commands (the dispatch clue's 47 at 3875ae6, plus 15 the real diff adds: check-adr-0087-registration x2, check-empty-changeset x2, release-rehearsal-clone --self-test, check:dispatcher-error-vocabulary, check:driver-conformance, check:engine-double-contract, check:objectql-double-limit, check:objectui-changeset, check:pm-changeset-deadline-census, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher)",
    "run_at": "1c8c5ce756",
    "exit_0": 59,
    "exit_1": [
    "node scripts/check-empty-changeset.mjs --base origin/main — the deliberate one-clause correction to .changeset/20176-aggregation-temporal-storage-rule.md; red on purpose, needs a person's confirmation on the PR, the file must not be restored from base"
    ],
    "not_measured": [
    "pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads every package's built dist, which needs the whole ./packages/* build; CI builds it first",
    "pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: --re-measure needs the whole ./packages/* build closure; CI builds it first"
    ],
    "ran_reconcile": "dispatch-gates --ran ran.list: 62 derived, 60 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN",
    "fixed_during_run": "check:query-options-erasure was exit 1 at 16aedca (test surface 236 → 241: five as any casts on query options in the new pins); typed as FilterCondition / EngineAggregateOptions in 1c8c5ce, then exit 0 (236, at ceiling)",
    "also_run": [
    "pnpm check:authz-resolver exit 0",
    "pnpm check:error-code-casing exit 0",
    "pnpm check:filter-alias-parity exit 0 (the three roster gates whose roster sits under packages/)",
    "node scripts/check-issue-citations.mjs --base 4df101c exit 0 (every added citation resolves)",
    "control-byte scan grep -naP over the 7 changed files: none"
    ],
    "lint": "declared narrowing, three pieces: (1) population read from eslint's own config: --print-config resolves a config for each of the 5 changed .ts files; (2) counts from --format json at 1c8c5ce: 5 files, 0 errors, 0 warnings, 0 fatal; (3) invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so no untouched file's verdict can move. Repo-wide pnpm lint is CI's.",
    "type_check_lanes": "per-package typecheck run for core, driver-memory, driver-sql, rest (exit 0 each); the workspace-wide lint.yml Type Check lanes are CI's"
    },
    "line_budget": "n/a — no skills/** in the diff",
    "deviations": [
    ".changeset/20176-aggregation-temporal-storage-rule.md: one-clause DELIBERATE CORRECTION. "…, so no where, write or read answer of either driver changes." → "…, so the lift itself changes no where, write or read answer of either driver (#20203, in the same release, then reads an epoch-millisecond number on a date field as its UTC calendar day)." Nothing else in the note moved; check-empty-changeset red for it by design. The 20148-* notes read true and were not touched.",
    "Pin placement: packages/rest has no driver-memory dependency and driver-memory cannot reach the engine, so memory's engine and REST cells are measured by a scratch harness but not committed; memory's where is pinned at its own driver door. PostgreSQL is pinned at the driver door in driver-sql's live matrix (CI Temporal Conformance runs it on PG and MySQL); its engine and REST cells are measured by the harness. Same precedent as PR #20202.",
    "Merged origin/main (4df101c, one commit touching objectql and spec) into the branch before the whole suites (AGENTS Multi-agent §10); clean merge, closure rebuilt.",
    "The live PostgreSQL used is a private PostgreSQL 16.13 this run started (server zone Asia/Shanghai), not the cell PR #20202's review used, which was not reachable; stopped and removed at the end."
    ],
    "files_changed": [
    "packages/core/src/utils/temporal-storage-form.ts",
    "packages/core/src/utils/temporal-storage-form.test.ts",
    "packages/drivers/driver-sql/src/sql-driver-20203-epoch-ms-date-comparand.test.ts",
    "packages/drivers/driver-memory/src/memory-20203-epoch-ms-date-comparand.test.ts",
    "packages/rest/src/data-query-epoch-ms-date-comparand.test.ts",
    ".changeset/20203-epoch-ms-date-comparand.md",
    ".changeset/20176-aggregation-temporal-storage-rule.md"
    ],
    "rows_legend": "Fixture: six rows, placed_on (date) 2026-01-10, 01-02, 03-01, 02-01, 01-15, 02-01; opened_at (datetime) 01-01T10Z, 01-02T10Z, 02-01T10Z, 02-05T10Z, 02-06T10Z, 03-01T10Z. N = 1769940000000 = 2026-02-01T10:00:00.000Z; $in = [N, 1768057200000 (01-10T15Z)]; $between = [1767394800000 (01-02T23Z), N]; "$eq midnight" = 1769904000000. Counts are rows (per_aggregation_filter: the filtered count m). base = 3875ae6; head = ba5c0de (the fix; the pins re-confirm at 1c8c5ce). engine = engine.find / engine.aggregate; rest = POST /api/v1/data/:object/query with a JSON round-tripped body. date_twin_head = the same comparand as new Date(N), engine door (a Date is not JSON); the driver-door pins assert it on all 11 operators. pg = live PostgreSQL 16.13; DATABASE_ERROR is SQLSTATE 22008 DateTimeParseError from the server's date input parser.",
    "rows": {
    "$gt": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 1
    },
    "rest": {
    "base": 0,
    "head": 1
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 1,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 3,
    "head": 3
    },
    "date_twin_head": 1
    },
    "sqlite": {
    "engine": {
    "base": 6,
    "head": 1
    },
    "rest": {
    "base": 6,
    "head": 1
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 1,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 3,
    "head": 3
    },
    "date_twin_head": 1
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 1
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 1
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 1,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 3,
    "head": 3
    },
    "date_twin_head": 1
    }
    },
    "$lt": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 3
    },
    "rest": {
    "base": 0,
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 2,
    "head": 2
    },
    "date_twin_head": 3
    },
    "sqlite": {
    "engine": {
    "base": 0,
    "head": 3
    },
    "rest": {
    "base": 0,
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 2,
    "head": 2
    },
    "date_twin_head": 3
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 3
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 2,
    "head": 2
    },
    "date_twin_head": 3
    }
    },
    "$eq": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 2
    },
    "rest": {
    "base": 0,
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 2
    },
    "sqlite": {
    "engine": {
    "base": 0,
    "head": 2
    },
    "rest": {
    "base": 0,
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 2
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 2
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 2
    }
    },
    "$in": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 3
    },
    "rest": {
    "base": 0,
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 3
    },
    "sqlite": {
    "engine": {
    "base": 0,
    "head": 3
    },
    "rest": {
    "base": 0,
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 3
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 3
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 3
    }
    },
    "$between": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 5
    },
    "rest": {
    "base": 0,
    "head": 5
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 5,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 5
    },
    "sqlite": {
    "engine": {
    "base": 0,
    "head": 5
    },
    "rest": {
    "base": 0,
    "head": 5
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 5,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 5
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 5
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 5
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 5,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    },
    "date_twin_head": 5
    }
    },
    "$gte": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 3
    },
    "rest": {
    "base": 0,
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 4,
    "head": 4
    }
    },
    "sqlite": {
    "engine": {
    "base": 6,
    "head": 3
    },
    "rest": {
    "base": 6,
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 4,
    "head": 4
    }
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 3
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 4,
    "head": 4
    }
    }
    },
    "$lte": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 5
    },
    "rest": {
    "base": 0,
    "head": 5
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 5,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 3,
    "head": 3
    }
    },
    "sqlite": {
    "engine": {
    "base": 0,
    "head": 5
    },
    "rest": {
    "base": 0,
    "head": 5
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 5,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 3,
    "head": 3
    }
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 5
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 5
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 5,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 3,
    "head": 3
    }
    }
    },
    "$ne": {
    "memory": {
    "engine": {
    "base": 6,
    "head": 4
    },
    "rest": {
    "base": 6,
    "head": 4
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 4,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 5,
    "head": 5
    }
    },
    "sqlite": {
    "engine": {
    "base": 6,
    "head": 4
    },
    "rest": {
    "base": 6,
    "head": 4
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 4,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 5,
    "head": 5
    }
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 4
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 4
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 4,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 5,
    "head": 5
    }
    }
    },
    "$nin": {
    "memory": {
    "engine": {
    "base": 6,
    "head": 3
    },
    "rest": {
    "base": 6,
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 5,
    "head": 5
    }
    },
    "sqlite": {
    "engine": {
    "base": 6,
    "head": 3
    },
    "rest": {
    "base": 6,
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 5,
    "head": 5
    }
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 3
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 3
    },
    "per_aggregation_filter": {
    "base": 6,
    "head": 3,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 5,
    "head": 5
    }
    }
    },
    "implicit": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 2
    },
    "rest": {
    "base": 0,
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    }
    },
    "sqlite": {
    "engine": {
    "base": 0,
    "head": 2
    },
    "rest": {
    "base": 0,
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    }
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 2
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 1,
    "head": 1
    }
    }
    },
    "$eq midnight": {
    "memory": {
    "engine": {
    "base": 0,
    "head": 2
    },
    "rest": {
    "base": 0,
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 0,
    "head": 0
    }
    },
    "sqlite": {
    "engine": {
    "base": 0,
    "head": 2
    },
    "rest": {
    "base": 0,
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": 0,
    "head": 0
    },
    "datetime_control": {
    "base": 0,
    "head": 0
    }
    },
    "pg": {
    "engine": {
    "base": "DATABASE_ERROR (engine)",
    "head": 2
    },
    "rest": {
    "base": "500 DATABASE_ERROR",
    "head": 2
    },
    "per_aggregation_filter": {
    "base": 0,
    "head": 2,
    "rest_equals_engine": true
    },
    "empty_table": {
    "base": "DATABASE_ERROR (engine)",
    "head": 0
    },
    "datetime_control": {
    "base": 0,
    "head": 0
    }
    }
    }
    },
    "having": {
    "legend": "groupBy customer_id, max(placed_on) as last_placed, min(opened_at) as first_opened; c1 last 01-10, c2 last 03-01, c3 last 02-01. native = driver aggregates; rows = engine aggregates in memory. Empty object: no group at base and head, every cell.",
    "memory/max(date) $gt N/native": {
    "base": "(no group)",
    "head": "c2",
    "rest_equals_engine": true
    },
    "memory/max(date) $gt N/rows": {
    "base": "(no group)",
    "head": "c2",
    "rest_equals_engine": true
    },
    "memory/max(date) $eq N/native": {
    "base": "(no group)",
    "head": "c3",
    "rest_equals_engine": true
    },
    "memory/max(date) $eq N/rows": {
    "base": "(no group)",
    "head": "c3",
    "rest_equals_engine": true
    },
    "memory/max(date) $in/native": {
    "base": "(no group)",
    "head": "c1,c3",
    "rest_equals_engine": true
    },
    "memory/max(date) $in/rows": {
    "base": "(no group)",
    "head": "c1,c3",
    "rest_equals_engine": true
    },
    "memory/min(datetime) $gte N/native": {
    "base": "c2,c3",
    "head": "c2,c3",
    "rest_equals_engine": true
    },
    "memory/min(datetime) $gte N/rows": {
    "base": "c2,c3",
    "head": "c2,c3",
    "rest_equals_engine": true
    },
    "sqlite/max(date) $gt N/native": {
    "base": "(no group)",
    "head": "c2",
    "rest_equals_engine": true
    },
    "sqlite/max(date) $gt N/rows": {
    "base": "(no group)",
    "head": "c2",
    "rest_equals_engine": true
    },
    "sqlite/max(date) $eq N/native": {
    "base": "(no group)",
    "head": "c3",
    "rest_equals_engine": true
    },
    "sqlite/max(date) $eq N/rows": {
    "base": "(no group)",
    "head": "c3",
    "rest_equals_engine": true
    },
    "sqlite/max(date) $in/native": {
    "base": "(no group)",
    "head": "c1,c3",
    "rest_equals_engine": true
    },
    "sqlite/max(date) $in/rows": {
    "base": "(no group)",
    "head": "c1,c3",
    "rest_equals_engine": true
    },
    "sqlite/min(datetime) $gte N/native": {
    "base": "c2,c3",
    "head": "c2,c3",
    "rest_equals_engine": true
    },
    "sqlite/min(datetime) $gte N/rows": {
    "base": "c2,c3",
    "head": "c2,c3",
    "rest_equals_engine": true
    },
    "pg/max(date) $gt N/native": {
    "base": "(no group)",
    "head": "c2",
    "rest_equals_engine": true
    },
    "pg/max(date) $gt N/rows": {
    "base": "(no group)",
    "head": "c2",
    "rest_equals_engine": true
    },
    "pg/max(date) $eq N/native": {
    "base": "(no group)",
    "head": "c3",
    "rest_equals_engine": true
    },
    "pg/max(date) $eq N/rows": {
    "base": "(no group)",
    "head": "c3",
    "rest_equals_engine": true
    },
    "pg/max(date) $in/native": {
    "base": "(no group)",
    "head": "c1,c3",
    "rest_equals_engine": true
    },
    "pg/max(date) $in/rows": {
    "base": "(no group)",
    "head": "c1,c3",
    "rest_equals_engine": true
    },
    "pg/min(datetime) $gte N/native": {
    "base": "c2,c3",
    "head": "c2,c3",
    "rest_equals_engine": true
    },
    "pg/min(datetime) $gte N/rows": {
    "base": "c2,c3",
    "head": "c2,c3",
    "rest_equals_engine": true
    }
    },
    "stop_valve": {
    "reading": "none found — no sentence in ADR-0053 or packages/spec states that a date comparand must be a calendar-day string; read direction taken",
    "quotes": [
    "ADR-0053 TL;DR: "date = a timezone-naive calendar day, represented as a YYYY-MM-DD string end-to-end (storage, query, CEL). It is never converted to an instant and never timezone-shifted." — representation, not an input restriction",
    "ADR-0053 Phase 1, decision 1: "A Date, a full ISO string, or a YYYY-MM-DD string all collapse to the calendar day (UTC calendar day for a Date, matching the existing filter coercion)." — non-day inputs are collapsed, not refused",
    "ADR-0053 D-C1 (the sibling time rule): "A Date / epoch-ms / full-timestamp value folds to its UTC time-of-day"",
    "packages/spec/src/data/filter.zod.ts ORDERING_COMPARAND_DESCRIPTION: "Comparand is a number, a Date, a string, or a { $field } reference"",
    "packages/core/src/utils/temporal-comparand.ts: "A number is epoch milliseconds, a Date is an instant, null is a null test. The refusal scopes to strings by ruling.""
    ],
    "adjacent_not_adr": "The engine/REST WRITE doors refuse a number on a date field (VALIDATION_FAILED, "placed_on must be a valid date (ISO-8601)"): the stored-value contract (CalendarDateValueSchema = YYYY-MM-DD), not a comparand sentence; unchanged base → head."
    },
    "collateral": {
    "rule_sweep": "34 shapes × 3 kinds = 102 cells of temporalStorageForm, base vs head dist: 12 moved, all date × a finite number inside the Date range (epoch ms, a UTC midnight, -86400000, -1, a fraction, a negative fraction, 0, -0, ±8.64e15, year 10000, year 0); 90 byte-identical: every datetime and time cell, and on date every Date, Invalid Date, NaN, ±Infinity, 8.64e15+1, MAX_SAFE_INTEGER, bigint, boxed Number, epoch-ms string, bare day, zone-naive, ISO, offset, wall clock, junk, empty, null, undefined, boolean, object, array",
    "end_to_end": "466 measured cells (where, per-aggregation filter, having, write, read, Date twin, rule sweep) across memory / SQLite / PostgreSQL: 113 moved, 0 outside "a number on a date field"",
    "where_equals_per_aggregation_filter_head": "132 of 132 keys (3 drivers × populated/empty × date/datetime × 11 operators), engine and REST: where count = per-aggregation filter count on every key",
    "per_aggregation_filter_base": "date field, all three drivers, both doors: 0 on every operator except $between, $ne, $nin (6); datetime control equal to where at base and head",
    "having": "only max(date) moved (both doors, native and rows, all three drivers): $gt N no group → c2; $eq N no group → c3; $in no group → c1, c3; min(datetime) $gte N control c2, c3 at base and head",
    "write_paths": "engine.insert and REST create of a number on a date field: VALIDATION_FAILED / 400 at base and head (unchanged). Direct driver create/update of a number on a date field: memory and SQLite stored 1769940000000, PostgreSQL refused 22008 → all three store "2026-02-01". The same number on datetime / time writes "2026-02-01T10:00:00.000Z" / "10:00:00" at base and head.",
    "read_paths": "SQLite, a raw INTEGER already in a date column: find, findOne, a groupBy key and distinct presented 1769940000000 → "2026-02-01" (ADR-0053 Phase 1 read repair); aggregate max over a mixed group unchanged ("2026-01-05"); a where day comparand against that raw row unchanged ([]: the date column side has no legacy repair)",
    "other_callers": "driver-turso inherits temporalFilterValue from SqlDriver and service-analytics binds through driver.temporalFilterValue, so both follow the rule; not separately measured",
    "pending_changesets": "20176-*: one sentence read false in the same release → corrected (deviations[0]); 20148-aggregation-filter-keys-rest and 20148-aggregation-filter-where-doors read true and were not touched"
    },
    "cleanup": "Private PostgreSQL (initdb /var/lib/postgresql/issue-20203-pg, port 45203, recorded PID 4218) stopped with pg_ctl -m fast; its data dir removed; no process left for it. Worktree /home/user/objectstack-issue-20203: branch pushed (local = remote 1c8c5ce), tree clean, node_modules removed, git worktree remove without --force exit 0, directory gone, git worktree list has no issue-20203 entry. Scratch harnesses were never in the repo (scratchpad issue-20203/). This comment was posted from a scratchpad copy of scripts/ at 1c8c5ce. No background process or monitor was started."
    }

  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ruling on the os-dev-report 5855694754 (PR #20224)

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T12:14Z.

    • Open question 1 (the unpadded year spelling) → A. PR fix(core): read an epoch-ms number on a date field as its UTC calendar day, so driver-memory, SQLite and PostgreSQL give one answer #20224 stays as it is: a number and its Date take one conversion across the whole Date range. The spelling of a year outside 1000..9999 is a defect of the Date arm that predates this card. Years past 9999 and below 0 have no YYYY-MM-DD spelling at all, so fixing it is a decision, not a bounded edit. The seat files it through the filing gate after landing, with its reach re-measured on main at the REST door.
    • The declaration stands as Clause-②: no. No export or payload key moves, and no request that answered now gets refused. The at-tier review judges it.
    • The 20176-* one-clause DELIBERATE CORRECTION: confirm as written. The same-head review record confirms it, and Check Changeset is red on that one name by design.
    • Out-of-scope findings:
  7. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20224 at 1c8c5ce756 (#20203)

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T12:38Z. Reviewed on GitHub against references/review-checklist.md, not from the dev's os-dev-report.

    • Shape: the first line is Fixes #20203, and the body declares Clause-②: no. The changeset grades @objectstack/core, driver-sql, driver-memory and objectql patch.
    • Scope: 7 files, +499/−9. Not governed; 508 changed lines.
      • The one source change is the date arm of temporalStorageForm in @objectstack/core: a finite number reads as the UTC calendar day of new Date(value). No driver source moves, and there is no third copy.
      • Also: pins at the core, memory, SQL and REST doors, the 20203-* changeset, and one DELIBERATE CORRECTION (20176-*).
    • Contract review of record: PASS at 1c8c5ce756 (5855881108).
      • Memory and SQLite: on all 11 operators the two drivers agree, engine = REST, a number = its Date, and the per-aggregation filter = where (88/88 keys per driver). Every datetime control is byte-identical.
      • The rule sweep moves 14 of 138 cells, all "a finite number on date". Of 445 / 452 end-to-end cells, 0 moved outside that class.
      • The write and read paths move inside the class, as ADR-0053 Phase 1 prescribes. The engine and REST write doors are unchanged. No accept set narrows.
      • The stop valve does not trip: no ADR-0053 sentence requires a date comparand to be a day string.
      • The ablation reproduces the dev's counts.
      • PostgreSQL was not measured locally. Its cell of record is CI's Temporal Conformance (live PG + MySQL) job at this head (success).
    • CI at this head: 34 runs: 30 success, 3 skipped, 1 failure. All seven required contexts are success.
      • The failure is Check Changeset, red by design on exactly .changeset/20176-aggregation-temporal-storage-rule.md: the DELIBERATE CORRECTION class. The same-head PASS confirms it, and the gate runs on pull_request only.
      • The probe merge-tree onto current main is clean.
    • Ruling carried (5855727555): open question 1 → A.
      • The review measured the cost. On four extreme-year cells (a day in year 999 or 10000), head answers worse than base: base answered them right only by accident, since a raw number never met the day text. For every such year a number now equals its Date, and the Date cells are byte-identical.
      • The defect is the Date arm's unpadded year spelling, which predates this card. The seat files it after landing, with its reach measured on main.
    • Acceptance notes (not cards):

    Landing: ready plus auto-merge through the queue now.

  8. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20224, verified on main

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T13:01Z.

    • Verified on main:
      • PR fix(core): read an epoch-ms number on a date field as its UTC calendar day, so driver-memory, SQLite and PostgreSQL give one answer #20224 merged through the merge queue as 615c468746. It has one parent (93cfc3f593) and is an ancestor of origin/main after a fresh fetch.
      • Its patch-id equals the reviewed diff 4df101c383..1c8c5ce756 (PASS 5855881108): 7 files, +499/−9.
      • The changeset 20203-epoch-ms-date-comparand.md is present at that commit and absent at its parent.
      • temporal-storage-form.ts landed at the reviewed blob 61a6c0861a.
      • The DELIBERATE CORRECTION of 20176-* landed at the reviewed blob 6b9d78d447, as ACCEPT 5855886361 ruled. The red Check Changeset does not run on merge_group.
    • This card: closed completed via Fixes #20203. pm:dispatched is removed in the same act.
      • An epoch-ms number on a date field now reads as its UTC calendar day in the one storage rule. InMemoryDriver, SQLite and PostgreSQL give one answer through the engine and REST, and PostgreSQL's DATABASE_ERROR / 500 is gone.
      • Declared Clause-②: no; patch levels.
    • Next, per ruling 5855727555: the seat files the Date arm's unpadded year spelling (years outside 1000..9999) through the filing gate, with its reach measured on main.
  9. added 2 commits that reference this issue on Sep 28, 2026
    615c468
    89f87f2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething 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