Skip to content

objectql + REST: a per-aggregation filter (and having) compares a temporal comparand type-blind, not by the column's storage rule — an ISO instant on a date field counts 1 where the where twin counts 3 #20176

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site, the per-aggregation filter position and having:

  • packages/objectql/src/engine.ts: ObjectQL.aggregate's per-aggregation loop;
  • packages/objectql/src/having-filter.ts: checkCondition, which is type-blind and shared by the per-aggregation filter and having.

Finding class (a). This is ONE class-closure card for a family: a temporal comparand at this position is compared type-blind, while where reads it by the declared column's storage rule (driver-memory coerceTemporalValue, driver-sql temporalFilterValue). reach: was measured at the public REST door (below).

The domain:engine execution seat 1 (session_01Bvd69VPa6puiNzzPUroDBx) filed this from its #20148 dev's report (os-dev-report 5852943342 on #20148, PR #20174): out-of-scope finding 1, plus open question 2's residual. The seat folded those two into this card (amendment 5852957006). ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.

The enumeration (each one a row of the pin)

Measured by the #20148 dev on a real InMemoryDriver and a real SqlDriver (schema-synced; 6 rows in 3 groups), through the engine and through REST POST /api/v1/data/:object/query. Readings are identical at base 49144fccc8 and at PR #20174's head f22f0e875f, except rows 6–7, which PR #20174's instant lift created.

# Shape, in aggregations[i].filter (or having) Count now The same condition as a where reach:
1 { placed_on: { $gte: '2026-02-01T00:00:00.000Z' } } on a date field 1 3 public: REST, both drivers
2 { placed_on: { $eq: '2026-02-01T00:00:00.000Z' } } on a date field 0 2 public: REST
3 { opened_at: { $lte: '2026-02-01' } }, a bare day as the upper bound on a datetime 2 3 public: REST
4 $between with a bare day as the max, on a datetime 2 3 public: REST
5 an epoch-ms bound on a datetime 0 3 public: REST
6 a Date carrying a time of day on a date field ($gte / $lt / $eq) 1 / 5 / 0 3 / 3 / 2 in-process only (a Date is not JSON)
7 a Date on a time field 0 3 in-process only
8 having on max(date) with an ISO bound keeps c2 the date rule keeps c2, c3 public: REST

Control: a zone-naive datetime string counts 4, and so does the where twin.

Suggested shape (⛔ not a ruling)

Filing-gate answers

Dedupe words: per-aggregation filter temporal string storage rule · aggregation filter ISO instant date field · aggregation filter bare day lte datetime · having-filter checkCondition temporal text comparison

Activity

  1. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

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

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

    Triage: lands in packages/objectql/src/engine.ts (ObjectQL.aggregate, :16071 on origin/main 455dcc0, its per-aggregation loop) and packages/objectql/src/having-filter.ts (checkCondition, shared by the per-aggregation filter and having) ⇒ domain:engine. Rationale: at the public REST door (POST /api/v1/data/:object/query, both drivers), a temporal comparand in aggregations[i].filter or having is compared type-blind. The same condition as a where is read by the column's storage rule, so the counts differ (1 vs 3, 0 vs 2 …, rows 1–5 and 8 of the card's table). That is a silent wrong answer on the aggregation contract. It is the same family as #20148 (p2, in flight) ⇒ p2. reach: is measured (REST, both drivers).

    Triage seat (objectstack-wide, seat post #6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-27T06:24Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), #20148 and its dev report 5852943342, PR #20174 (open), and #20127 (closed).

    A closure card for its family. The eight rows are one mechanism: a comparand normalised by the where door but not at this position. So one enumeration pin covers the family, and ⛔ no per-row cards. It is not folded into #20148, which is in flight (assigned, PR #20174 open).

    Sequencing. PR #20174 edits the same loop and checkCondition. The dispatching seat's in-flight check sees that intersection on #20148's declared surface ⇒ this card goes serial after PR #20174. It is a region order, not a Blocked-by:, as the card says.

    Execution notes.

    1. Re-measure all eight rows on the main that carries PR fix(objectql,metadata-protocol)!: the per-aggregation filter takes where's remaining doors — a bad date, an addDays numeric pair, an undeclared { $field } and an unknown key are refused; a Date bound compares as an instant #20174, first. Rows 6–7 were created by that PR's instant lift, so their readings may move.
    2. Use ONE storage-form rule for a temporal comparand, shared by the drivers' where (coerceTemporalValue, temporalFilterValue) and this position. ⛔ No third copy. Lifting it into packages/core keeps the PR inside the lane. Lifting it into packages/spec makes it a cross-domain PR, which the claim comment then declares.
    3. For having, thread each aggregated column's temporal class into checkCondition, the way objectql having: a { $field } reference with addDays against a non-temporal aggregated column answers by epoch-ms coercion, where SQL push-down refuses the same pair on where — the aggregated row declares no temporal class to judge it by #20127's rule already derives it for addDays pairs.
    4. Pin: the eight rows on a populated and an empty object, through the engine and REST, on both drivers. The where twin is the control, plus the zone-naive datetime row (4 = 4).
  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    on Sep 27, 2026
  3. 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-20176-aggregation-temporal-storage-rule
    Worktree: objectstack-issue-20176
    Domain: domain:engine
    Seat: domain:engine#1
    File surface:

    Stop on breach and explain in the report. ⛔ Not packages/spec: lifting the rule into spec would make this a cross-domain PR. Stop and report if core cannot hold it. ⛔ Not the where path's answers. ⛔ Not the refusal doors PR #20174 added.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5853357535
    Serial constraints cleared: at 2026-09-27T06:47Z, the region predecessor PR #20174 (#20148) has landed as cfe2387a3b and is verified on main (landing record 5853465810). A census of the 12 open PRs finds none on engine.ts, having-filter.ts or in-memory-aggregation.ts. The seat runs batch:3 (the maintainer's ruling); in flight are this card and PR #20182 in the merge queue.

  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20176,
    "status": "done",
    "branch": "claude/issue-20176-aggregation-temporal-storage-rule",
    "pr": "#20202",
    "session": "session_01Bvd69VPa6puiNzzPUroDBx — mode:subagent, the id the claim 5853510397 names (the newest Claim: on the card names this branch; no second claim posted)",
    "premise_still_valid": true,
    "summary": "Draft PR #20202 (head b48f417 = fix 8e1c0cf + pins/changesets 9e5c309 + typed where-twin a76b115 + a merge of main at 560b724 touching none of these files). One storage rule, temporalStorageForm(value, kind), now lives in @objectstack/core (new export); driver-sql's canonicalUtcDatetime / toDateOnly / canonicalTimeOfDay bodies and driver-memory's coerceTemporalValue call it (the drivers' copies are gone; MySQL's literal spelling stays in driver-sql). In objectql, checkCondition takes the column's temporal kind and, when it has one, puts the row value and every comparand of $eq/$ne/orderings/$between/$in/$nin/implicit equality into the storage form, with ADR-0053 D-D's whole-day reading of a bare-day upper bound on a datetime; the per-aggregation filter gets the kind from the object's declared fields (declaredFieldClasses, via applyInMemoryAggregation's new optional fields argument) and having from #20127's aggregatedRowColumnClasses, threaded to both applyHaving doors. All eight rows and 46 added members now equal their where twin on every cell (engine and REST, both drivers, populated and empty); where, refusals and non-temporal cells are byte-identical. H1 held (rows 6-7 unmoved by PR #20174), H2 held for the lifted function (213/213 cells agree; one divergence found in the drivers' operator layer, reported), H3 held, H4 held, H5: the new core export and the optional applyInMemoryAggregation argument are a widening — changeset grades core and objectql minor; the claim's Clause-② no is copied verbatim into the PR body and raised as open question 1. Card assignee untouched (os-sales); PR assignee set to os-sales.",
    "tests": "(1) Whole suites at b48f417 after merging main and a full package build (turbo 71/71): objectql 'Test Files 317 passed (317) · Tests 5712 passed (5712)'; core '54 passed · 1410 passed'; driver-memory '54 passed · 1328 passed'; driver-sql 'Test Files 188 passed | 11 skipped (199) · Tests 3099 passed | 170 skipped'; REST aggregate files (aggregation-filter-temporal-storage-rule, aggregation-filter-where-doors, list-view-grouping-query-door, request-schema-gate.conformance, rest-server-canonical-query-ast) '5 passed · 109 passed | 1 skipped'; service-analytics '128 passed · 3017 passed'. At base+fix before the merge: objectql 316/5628, core 53/1358, driver-memory 53/1269, driver-sql 187 passed 11 skipped / 3089 passed. (2) Live PostgreSQL 16 (private cluster on :55432, server timezone Asia/Shanghai, process TZ=America/New_York, OS_TEST_POSTGRES_URL set): driver-sql whole suite 'Test Files 196 passed | 3 skipped (199) · Tests 3806 passed | 86 skipped', sql-driver-temporal-conformance 103 with its MySQL cell skipped; reporter 'this run exercised 2 of 3 dialects'. MySQL NOT MEASURED, reason: no server in this container. Server stopped (pg_ctl stop, recorded pid 9263 gone) and its data dir removed. (3) Skewed process zone America/New_York, Asia/Kolkata, Pacific/Chatham: objectql new pins + engine-aggregate-filter (122), driver-memory new + temporal conformance (191), core new (52) — green in all three. (4) New pins: objectql engine-aggregate-temporal-storage-rule.test.ts 84; rest aggregation-filter-temporal-storage-rule.test.ts 13; driver-memory memory-temporal-storage-form.test.ts 59; driver-sql sql-driver-temporal-storage-form.test.ts 10; core temporal-storage-form.test.ts 52. (5) Typecheck core / driver-memory / driver-sql / objectql / rest: exit 0 each; --listFiles puts every new test file in a program; objectql check:test-typecheck '40 file(s) / 234 error(s) / 65 pinned signature(s)' unchanged. (6) Reverse verification: fix committed first; ablation-replace (wrap mode, EXIT/INT/TERM trap, absolute path) replaced the one kind-threading site in matchesHaving ('row, temporalKindOf(classes?.get(key)))) return false' -> 'row, undefined)) return false'): anchor 1 -> 0, replacement 0 -> 1, blob 77400359cc -> 7a2f5b7f14; objectql rebuilt; ablation-dist-preflight --absent: marker absent from all 14 built files; new objectql pins 'Tests 58 failed | 26 passed (84)' (the 26: controls, unmoved positions, the #20148 Date lift on an undeclared column, kit cases whose canonical comparands compare correctly as text); new REST pins '11 failed | 2 passed (13)' (the 2 controls); the scratch harness in that state reproduced the base table 5024/5024. Restore: git checkout HEAD --, blob 77400359cc == HEAD, git diff HEAD empty; objectql rebuilt; preflight 'marker present in 4 built files', 'working tree clean against HEAD'; pins 84 and 13 passed. Direction observed: turn red (the usual one). (7) Lint, declared narrowing: eslint --no-inline-config --format json on the 13 changed .ts files -> 13 files, 0 errors, 0 warnings, 0 fatal; --print-config resolves a config for 13 of 13; eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rule), so no untouched file's verdict can move. (8) Scratch real-driver measurement (not committed; copy in the scratchpad at issue-20176/measure-harness.test.ts.txt): InMemoryDriver + SqlDriver (better-sqlite3 :memory:), schema-synced, engine.aggregate and POST /api/v1/data/:object/query (JSON round-trip), populated (6 rows, 3 groups) and empty, per-aggregation grouped + ungrouped, where twin on aggregate and find, having on both applyHaving doors, driver reads counted — 5120 cells at head vs 5024 at base (96 = 3 shapes added later, their base read in the reverse state): 4460 identical, 564 moved. (9) Control-byte scan (grep -naP) of the 15 changed files and the PR body: none.",
    "mcp_calls": "3 — all reads, no write tool: mcp__github__issue_read get #20176, get_comments #20176, get_comments #20148. Other reads were REST GETs (pulls?head=, pulls/20202, commits/b48f417154/check-runs, check-runs/{id}/annotations).",
    "api_writes": "3 — each through the fleet-write relay as objectstack-fleet[bot] (one repository_dispatch each): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #20202; body read back byte-identical, 13630 bytes, one session-URL footer); (2) label-write --issue 20202 --assign os-sales -> POST /repos//issues/20202/assignees (read back: assignee os-sales; labels documentation, size/xl, tests, tooling were set by the labeler, not this run); (3) this os-dev-report comment -> POST /repos//issues/20176/comments. Not REST: git push x5 (the first the empty-branch probe, the last the merge of main).",
    "open_questions": [
    {
    "question": "Clause-② declaration. The claim carries 'Clause-②: no' and the PR body carries it verbatim. The diff adds a new exported symbol to @objectstack/core (temporalStorageForm) and an optional fourth parameter to the published @objectstack/objectql applyInMemoryAggregation — by pr-automation.yml's WHICH LEVEL rule a purely additive widening of a published surface, which is why the changeset grades core and objectql minor. check-changeset-no-major and check-adr-0087-registration pass either way (the level axis stands down on 'no'). Should the PR body line read 'Clause-②: yes (widening)'?",
    "options": [
    "A — keep 'Clause-②: no' as claimed: the answers of accepted inputs move toward the where answer (a correction, the PR #20174 precedent), and the level is already minor",
    "B — change the PR body line to 'Clause-②: yes (widening)': the core export and the optional parameter enlarge the public surface, which is the question the line answers"
    ],
    "recommendation": "B, and it is the seat's write (the PR body is written once at pr_create). 实际业务需求: temporalStorageForm is a real new entry point two drivers and the engine import today, not a speculative surface. 项目长远合理性: the line's question (「本卡放宽接受集或扩大公开面吗」) is answered yes by a new index export, and declaring it lets the level axis hold the minor grade mechanically. 防 AI 写错: an honest yes keeps the next reader from inferring that a no-widening PR can add exports. 创业阶段不扩散: B adds nothing; it only states what the diff already does."
    },
    {
    "question": "The pending changeset .changeset/20148-aggregation-filter-where-doors.md said 'Two readings still differ from a where' (a Date with a time of day on a date field, a Date on a time field). This PR closes both in the same release, so the sentence would ship false. It carries a one-clause deliberate correction ('still differed ... in this change alone ... (#20176 closes both in the same release, ...)'). check-empty-changeset (Check Changeset in CI) is red on it by design until a person confirms. Confirm the correction?",
    "options": [
    "A — confirm the correction as written",
    "B — restore the note from base and accept a false sentence in the release (not recommended)",
    "C — reword the correction (the seat edits the file)"
    ],
    "recommendation": "A: the gate's own text names this class DELIBERATE CORRECTION and says restoring would republish a false sentence."
    }
    ],
    "out_of_scope_findings": [
    "class: a · driver-memory and driver-sql answer one where differently for a bare calendar day used as the upper bound of a TIME field. On a time field with rows at 00:00:00 and 09:00:00, where { t: { $lte: '2026-02-01' } } and { t: { $between: ['00:00', '2026-02-01'] } } return no row on InMemoryDriver (its $lte and $between arms apply nextUtcCalendarDay to every kind, then store it as '00:00:00' behind $lt) and the midnight row on SqlDriver (calendarDayExclusiveUpperBound is datetime-only, so it compares $lte '00:00:00'). Measured through engine.find on both drivers at cfe2387 and at head (unchanged by this PR). The comparand passes the temporal-comparand door (a bare day reads as an instant for a time field), so it is REST-reachable. Contract text: TEMPORAL_TIME_CASES in @objectstack/spec/data says 'the bare-day whole-day rule (D-D) must NOT reach it' for a time column. Seam: spec:TEMPORAL_TIME_CASES / calendar-day.ts (ADR-0053 D-D) → runtime:packages/drivers/driver-memory/src/memory-driver.ts normalizeFieldOperators $lte and $between arms · dedupe: driver-memory time field bare day lte widened · nextUtcCalendarDay time column memory driver · bare-day upper bound time field drivers disagree",
    "class: a · an epoch-millisecond NUMBER against a DATE field is read by neither driver's storage rule, and the two drivers answer differently: POST /api/v1/data/:object/query with where { placed_on: { $gt: 1769940000000 } } on a 6-row date column returns 0 rows on InMemoryDriver (mingo never matches a number against text) and 6 rows on SqlDriver (SQLite orders every INTEGER below every TEXT). Measured by the scratch harness at base and head, engine and REST (the per-aggregation position answers 0 before and after). The temporal-comparand door does not judge non-string comparands by design, so the value reaches the drivers. Seam: runtime:packages/core/src/utils/temporal-storage-form.ts date rule (a number is returned unchanged) → runtime:packages/drivers/driver-sql/src/sql-driver.ts coerceFilterValue / driver-memory coerceTemporalValue · dedupe: epoch ms number date field where drivers disagree · date field numeric comparand sqlite integer text ordering · temporal comparand number date column",
    "carrier: 承接者:无 · driver-mongodb's mongodb-temporal.ts keeps its own copy of the storage rule (BSON Date form for datetime); instants agree with the core rule; outside this claim, noted in the PR's Acceptance notes, not filed",
    "carrier: 承接者:无 · core temporal-comparand.ts predicates (readsAsInstant / readsAsCalendarDay / readsAsWallClock) restate the rule's regexes; could be derived from temporalStorageForm; noted in the PR's Acceptance notes, not filed",
    "carrier: 承接者:无 · ObjectQL.resolveNowDefault hand-spells the date/time forms of a Date (agrees with the rule for every Date); noted in the PR's Acceptance notes, not filed"
    ],
    "gates": {
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at b48f417 (no paths): 69 commands; 'none of the commit(s) this tree can SEE touched what this answer derives from' (origin/main 2 commits ahead at derivation). The clue list at cfe2387 had 56; the derivation adds check-adr-0087-registration (+ --self-test), check-empty-changeset (+ --self-test), release-rehearsal-clone --self-test, 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, and drops none.",
    "run": "69 of 69 run at b48f417, each exit captured before any pipe: 68 exit 0, 1 exit 1 — node scripts/check-empty-changeset.mjs --base origin/main, on .changeset/20148-aggregation-filter-where-doors.md (the deliberate correction, open question 2). Round 1 (at 9e5c309) had check:query-options-erasure red: a new test erased a driver query to any (test surface 236 -> 237); fixed in a76b115 by typing the where twin (236, at the ceiling). check:dual-build-cjs-loads and check:type-check-debt first answered 3 (PREREQUISITE NOT MET) and exit 0 after the full package build.",
    "reconciliation": "dispatch-gates --ran ran-final.list: '69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)'",
    "rosters_in_my_paths": "check:authz-resolver, check:error-code-casing, check:filter-alias-parity (the three rosters under packages/ the derivation marks), check:published-readme-exports, check:select-shard-packages: exit 0 each",
    "changeset_gates": "check-adr-0087-registration --base origin/main exit 0; check-changeset-no-major --base origin/main exit 0 (its clause-② level axis reads the PR body and was NOT APPLICABLE locally)",
    "issue_citations": "node scripts/check-issue-citations.mjs --base 560b724 (the merge base): 28 citations across 8 files, all resolve",
    "ci": "at report time on b48f417: 31 check runs — 10 success, 3 skipped, 17 in_progress, 1 failure: Check Changeset, whose only error annotation is the check-empty-changeset refusal on .changeset/20148-aggregation-filter-where-doors.md (expected; open question 2). Not waited on."
    },
    "line_budget": "n/a — the diff touches no skills/**",
    "deviations": [
    "File surface, engine.ts: the claim says 'the per-aggregation loop only'. The loop runs before filter tokens resolve, so a comparand normalised there would miss every {today}-style bound (it resolves to a bare day later). Normalisation therefore runs where the filter is evaluated (having-filter.ts checkCondition), and the engine.ts edit is threading only: one declaredFields read beside the existing having class read (havingColumnClasses, now kept in a const), passed to both applyHaving calls and, as the field map, to applyInMemoryAggregation. No other line of ObjectQL.aggregate moved.",
    "File surface, in-memory-aggregation.ts: the claim scopes it to threading having's classes; it threads the per-aggregation filter's class map instead (having's classes go to applyHaving directly from engine.ts). The published applyInMemoryAggregation gains an optional fourth parameter (fields); without it the function answers as before (pinned).",
    "File surface, having-filter.ts: besides checkCondition and its callers (matchesHaving, applyHaving, matchesAggregationFilter now carry an optional class map), it adds declaredFieldClasses (exported), temporalKindOf and wholeDayUpperBound (private), two imports (temporalStorageForm from core, nextUtcCalendarDay from spec/data) and header/doc paragraphs; instantsOf's doc now says where the walker is still type-blind.",
    "Driver re-pointing: driver-sql's canonicalUtcDatetime, toDateOnly and canonicalTimeOfDay bodies (which temporalFilterValue reaches through coerceFilterValue, and which the write and read paths also use) delegate to the core rule, rather than only temporalFilterValue — so the driver has one rule on every path. driver-memory's three private per-kind functions are deleted (only coerceTemporalValue used them). H2's 852 base->head cells are the byte-identity evidence. core's temporal-comparand.ts gets doc edits only (it named the deleted memory functions).",
    "Pending changeset .changeset/20148-aggregation-filter-where-doors.md: one-clause DELIBERATE CORRECTION of 'Two readings still differ from a where, because the per-row comparison holds no declaration:' to 'Two readings still differed from a where in this change alone, because the per-row comparison held no declaration (#20176 closes both in the same release, reading every temporal comparand at this position by the column's storage rule):'. The other pending notes (20099, 20122, 20127, 20148-keys-rest) were read and stay true. check-empty-changeset is red on it by design (open question 2).",
    "Changeset levels: @objectstack/core minor and @objectstack/objectql minor (a new index export, and an optional parameter on a published function — pr-automation.yml WHICH LEVEL), driver-sql and driver-memory patch. The PR body carries the claim's 'Clause-②: no' verbatim; the changeset carries no Clause-② line (not breaking, no ADR-0087 marker needed). See open question 1.",
    "The REST where twin on InMemoryDriver is measured (scratch harness) but not committed as a pin: packages/rest has no driver-memory dependency; that driver's where half of the card's rows is pinned in its own package (memory-temporal-storage-form.test.ts).",
    "The branch merged origin/main at 560b724 (7 commits, none on these files) before the final suite run and gate derivation, per AGENTS.md Multi-agent §10; lockfile unchanged, full package build refreshed.",
    "Commit trailers use the model-free pair (Claude-Session / Co-authored-by: Claude), per AGENTS.md; the harness's model-named attribution line was not used."
    ],
    "files_changed": [
    ".changeset/20148-aggregation-filter-where-doors.md",
    ".changeset/20176-aggregation-temporal-storage-rule.md",
    "packages/core/src/index.ts",
    "packages/core/src/utils/temporal-comparand.ts",
    "packages/core/src/utils/temporal-storage-form.test.ts",
    "packages/core/src/utils/temporal-storage-form.ts",
    "packages/drivers/driver-memory/src/memory-temporal-storage-form.test.ts",
    "packages/drivers/driver-memory/src/memory-temporal.ts",
    "packages/drivers/driver-sql/src/sql-driver-temporal-storage-form.test.ts",
    "packages/drivers/driver-sql/src/sql-driver.ts",
    "packages/objectql/src/engine-aggregate-temporal-storage-rule.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/having-filter.ts",
    "packages/objectql/src/in-memory-aggregation.ts",
    "packages/rest/src/aggregation-filter-temporal-storage-rule.test.ts"
    ],
    "rows_legend": "Per-aggregation cells: ungrouped, filter on the 2nd of two counts; the value is the filtered count out of 6. P = populated, E = empty; a single value means all 4 cells (engine and REST × InMemoryDriver and SqlDriver) agree, otherwise value(door-driver initials: e/r = engine/REST, m/s = memory/sql). Grouped cells moved identically. having rows list the kept groups (c1/c2/c3) on both applyHaving doors. Base cfe2387 (3 shapes added later — the two created_at rows and epoch-ms on date — read in the reverse-verification state, which reproduced the base table 5024/5024); head = the code of b48f417.",
    "rows": [
    "row 1 · ISO instant $gte on date — base P 1, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "row 2 · ISO instant $eq on date — base P 0, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "row 3 · bare day $lte on datetime — base P 2, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "row 4 · bare day $between max on datetime — base P 2, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "row 5 · epoch ms $gt on datetime — base P 0, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "row 6 · Date 10:00 $gte on date — base P 1, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "row 6 · Date 10:00 $lt on date — base P 5, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "row 6 · Date 10:00 $eq on date — base P 0, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "row 7 · Date $gt on time — base P 0, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "control · zone-naive $gt on datetime (control) — base P 4, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · ISO instant $ne on date — base P 6, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · ISO instant implicit eq on date — base P 0, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "added · ISO instant $in members on date — base P 0, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · ISO instant $nin members on date — base P 6, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · ISO instant $between endpoints on date — base P 3, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · ISO instant $lt on date — base P 5, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · ISO instant $gt on date — base P 3, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · bare day $lte on date (control) — base P 5, E 0 | head P 5, E 0 | where twin P 5, E 0",
    "added · bare day $gt on datetime — base P 4, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · bare day $eq on datetime — base P 0, E 0 | head P 0, E 0 | where twin P 0, E 0",
    "added · bare day $lt on datetime — base P 3, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · bare day $between both on datetime — base P 1, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "added · bare day $in on datetime — base P 1, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · epoch ms string $gt on datetime — base P 6, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · epoch ms $lte on datetime — base P 0, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · epoch ms $eq on datetime — base P 0, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · offset ISO $gte on datetime — base P 3, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · offset ISO $eq on datetime — base P 0, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · zone-naive T $eq on datetime — base P 0, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · ISO no-ms $eq on datetime — base P 0, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · ISO no-ms $ne on datetime — base P 6, E 0 | head P 5, E 0 | where twin P 5, E 0",
    "added · ISO no-ms $nin on datetime — base P 6, E 0 | head P 5, E 0 | where twin P 5, E 0",
    "added · ISO instant $gt on time — base P 0, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · ISO instant $eq on time — base P 0, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · short wall clock $eq on time — base P 0, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · short wall clock $lte on time — base P 2, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · short wall clock $in on time — base P 0, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "added · short wall clock $between on time — base P 2, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · ms wall clock $eq on time — base P 0, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · bare day $lte on time — base P 6, E 0 | head P 0, E 0 | where twin P 0, E 0",
    "added · Date implicit eq on time — base P 0, E 0 | head P 1, E 0 | where twin P 1, E 0",
    "added · Date $in on date — base P 0, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "added · Date $nin on date — base P 6, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · Date $between on date — base P 3, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · Date $lte midnight on datetime — base P 2, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "added · ISO on created_at — base P 6, E 0 | head P 6, E 0 | where twin P 6, E 0",
    "added · null $eq on date — base P 0, E 0 | head P 0, E 0 | where twin P 0, E 0",
    "added · null $ne on datetime — base P 6, E 0 | head P 6, E 0 | where twin P 6, E 0",
    "added · $exists on date — base P 6, E 0 | head P 6, E 0 | where twin P 6, E 0",
    "added · under $or ISO on date — base P 1, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · under $not ISO on date — base P 5, E 0 | head P 3, E 0 | where twin P 3, E 0",
    "added · under $and ISO on date — base P 0, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "added · multi-op $gt+$lte on datetime — base P 1, E 0 | head P 2, E 0 | where twin P 2, E 0",
    "added · $lte bare day + $lt ISO on datetime — base P 3, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · text field ISO (non-temporal) — base P 4, E 0 | head P 4, E 0 | where twin P 4, E 0",
    "added · number field epoch (non-temporal) — base P 0, E 0 | head P 0, E 0 | where twin P 0, E 0",
    "added · bare day today $lte on created_at — base P 0, E 0 | head P 6, E 0 | where twin P 6, E 0",
    "added · offset ISO today $gte on created_at — base P 6, E 0 | head P 6, E 0 | where twin P 6, E 0",
    "added · epoch ms $gt on date — base P 0, E 0 | head P 0, E 0 | where twin P 0(em rm) / 6(es rs), E 0",
    "row 8 family (#20174 residual) · having Date 10:00 $gte on max(date) — base P c2, E [] | head P c2,c3, E [] (both doors)",
    "row 8 · having ISO instant $gte on max(date) — base P c2, E [] | head P c2,c3, E [] (both doors)",
    "row 8 family · having ISO instant $eq on max(date) — base P [], E [] | head P c2, E [] (both doors)",
    "row 8 family · having ISO instant $lt on min(date) — base P c1,c2, E [] | head P c2, E [] (both doors)",
    "row 8 family · having bare day $lte on min(datetime) — base P c1, E [] | head P c1,c2, E [] (both doors)",
    "row 8 family · having bare day $between max on min(datetime) — base P c1, E [] | head P c1,c2, E [] (both doors)",
    "row 8 family · having epoch ms $gte on min(datetime) — base P [], E [] | head P c2,c3, E [] (both doors)",
    "row 8 family · having offset ISO $gte on min(datetime) — base P c3, E [] | head P c2,c3, E [] (both doors)",
    "row 8 family · having zone-naive $gt on min(datetime) (control) — base P c2,c3, E [] | head P c2,c3, E [] (both doors)",
    "row 8 family · having short wall clock $eq on max(time) — base P [], E [] | head P c3, E [] (both doors)",
    "row 8 family · having short wall clock $gte on max(time) — base P c2,c3, E [] | head P c2,c3, E [] (both doors)",
    "row 8 family · having ISO instant $gte on max(time) — base P [], E [] | head P c2,c3, E [] (both doors)",
    "row 8 family · having Date $gte on max(time) — base P [], E [] | head P c2,c3, E [] (both doors)",
    "row 8 family · having ISO $in on max(date) — base P [], E [] | head P c2,c3, E [] (both doors)",
    "row 8 family · having ISO $nin on max(date) — base P c1,c2,c3, E [] | head P c1,c3, E [] (both doors)",
    "row 8 family · having ISO $ne on max(date) — base P c1,c2,c3, E [] | head P c1,c3, E [] (both doors)",
    "row 8 family · having epoch ms on sum (numeric, control) — base P c2, E [] | head P c2, E [] (both doors)",
    "row 8 family · having groupBy date col ISO — base P 2026-03-01, E [] | head P 2026-02-01,2026-03-01, E [] (both doors)",
    "row 8 family · having groupBy datetime col bare day $lte — base P 2026-01-01T10:00:00.000Z,2026-01-02T10:00:00.000Z, E [] | head P 2026-01-01T10:00:00.000Z,2026-01-02T10:00:00.000Z,2026-02-01T10:00:00.000Z, E [] (both doors)",
    "row 8 family · having day bucket ISO — base P 2026-02-05,2026-02-06,2026-03-01, E [] | head P 2026-02-01,2026-02-05,2026-02-06,2026-03-01, E [] (both doors)",
    "row 8 family · having day bucket bare day $lte — base P 2026-01-01,2026-01-02,2026-02-01, E [] | head P 2026-01-01,2026-01-02,2026-02-01, E [] (both doors)"
    ],
    "drivers_agree": {
    "storage_form": "driver-memory coerceTemporalValue vs SqlDriver.temporalFilterValue (better-sqlite3, pg, mysql2 clients; field maps seeded, no connection) on 71 shapes x 3 kinds: memory = sqlite = pg on 213/213 cells; mysql differs on the 36 datetime cells only by its literal spelling ('2026-02-01 10:00:00.000' for '2026-02-01T10:00:00.000Z'), which stays in driver-sql. No disagreement, so the lift picked nothing.",
    "base_to_head": "all 852 cells (213 x 4 faces) byte-identical base -> head; the new driver pins hold coerceTemporalValue and temporalFilterValue equal to temporalStorageForm on every shape and client.",
    "operator_layer_divergence": "outside the lifted function: a bare day as the $lte / $between max of a TIME field — InMemoryDriver no row, SqlDriver the midnight row (fixture rows 00:00:00 and 09:00:00), unchanged by this PR. The engine position follows the spec (ADR-0053 D-D table and TEMPORAL_TIME_CASES: no whole-day reading on a time column) = driver-sql's answer. Out-of-scope finding 1."
    },
    "collateral": {
    "where": "every where twin (aggregate verb and find), engine and REST, both drivers, both populations: 2112 cells byte-identical base -> head",
    "refusals": "964 cells that are a refusal on either side (PR #20174's, #20122's, #20123's, #20127's refusals and the rest): byte-identical",
    "moved": "564 cells moved, all populated cells of 72 shapes, every one on a date/datetime/time column; each moved to its where twin (per-aggregation) or to the date/datetime/time rule's answer (having). Per-aggregation m == where n on 820 of 834 comparable cells at head (586 at base); the 14 left are pre-existing and not this rule: Date on number/text fields (drivers disagree with each other), driver-memory's { $field } where answering zero (#20148's out-of-scope finding 2), and epoch-ms on a date field (out-of-scope finding 2)",
    "non_temporal": "every per-aggregation and having cell whose column is not temporal is byte-identical (text, number, count/sum aliases, month-label groupBy)",
    "groupBy": "4 groupBy probes byte-identical",
    "row5_datetime": "PR #20174's Date-on-datetime shapes keep their head answers (the R5 group: 10 datetime shapes unchanged); its Date-on-date/time shapes moved to the where answer",
    "pins": "objectql whole suite (317 files) includes engine-aggregate-filter, having-filter, in-memory-aggregation and engine-aggregate-having tests, all green"
    },
    "cleanup": "worktree ../objectstack-issue-20176: branch fully pushed (remote head b48f417 = local), git status clean; node_modules removed, then git worktree remove without --force: exit 0, directory gone, git worktree list has no issue-20176 entry. Scratch harness files were never committed and were deleted from packages/runtime/src after each run; copies are in the scratchpad (issue-20176/). The private PostgreSQL server this run started was stopped and its data dir removed. This comment was posted from a scratchpad copy of scripts/ at b48f417."
    }

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat amendment to claim 5853510397

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T08:46Z.

    • Open question 1 (the declaration) → B. The claim's Clause-②: no was wrong.
    • Open question 2 (the 20148-* sentence) → A. Confirm the one-clause DELIBERATE CORRECTION as written. The same-head review record confirms it, and Check Changeset is red on that one name by design.
    • Deviations accepted for review, with one to measure:
      • The normalisation lives in checkCondition, where {today}-style tokens have resolved, not in the loop. engine.ts only threads the classes.
      • in-memory-aggregation.ts threads the per-aggregation filter's class map.
      • ⚠️ The driver re-pointing is wider than the claim's "temporalFilterValue only". driver-sql's canonicalUtcDatetime / toDateOnly / canonicalTimeOfDay bodies now delegate to the core rule, and they also serve the write and read paths. The review measures that stored and returned bytes are byte-identical base → head on every dialect the container reaches.
    • Out-of-scope findings:
      • Finding 1 (a bare day as the upper bound of a time field: driver-memory no row, driver-sql the midnight row) lands in driver-memory's operator layer, which is under the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 freeze as the maintainer applied it on 2026-09-23. It is recorded, not filed.
      • Finding 2 (an epoch-ms NUMBER against a date field: 0 rows on driver-memory and 6 on driver-sql, over REST) has a public-door wrong answer on driver-sql. The seat files it separately after a dedupe.
  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20202 at b48f417154 (#20176)

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

    • Shape: the first line is Fixes #20176. The body declares Clause-②: yes (widening), corrected by the seat (amendment 5854322020) and confirmed by the review. Levels: @objectstack/core and @objectstack/objectql minor; driver-sql and driver-memory patch.
    • Scope: 15 files, +1179/−227. Not governed.
      • temporalStorageForm is added to @objectstack/core. Both drivers' storage-form functions and checkCondition read it, and the drivers' copies are deleted.
      • Class threading goes through engine.ts, in-memory-aggregation.ts (an optional fourth parameter) and having-filter.ts.
      • Pins, the changeset, and one DELIBERATE CORRECTION (20148-*).
    • Contract review of record: PASS at b48f417154 (5854540769).
      • Rows 1–8 and the family sample equal their where twin on memory, SQLite and live PostgreSQL 16, through the engine and REST. Every moved having cell equals the column's storage-rule answer.
      • The write and read paths through the re-pointed driver functions are byte-identical base → head: 452 entries, covering stored bytes, returned values, bind values and comparands. The MySQL server was NOT MEASURED; the client-side spelling is identical.
      • Every where cell (2916) and every refusal (822) is byte-identical. applyInMemoryAggregation with three arguments is unchanged.
      • The ablation reproduces the dev's counts: 58/84 and 11/13.
    • CI at this head: 41 runs, 34 success, 5 skipped, 2 failed. All seven required contexts are success.
      • The 2 failures are both Check Changeset, red by design on exactly .changeset/20148-aggregation-filter-where-doors.md. This is the DELIBERATE CORRECTION class, confirmed by the same-head PASS, and the gate runs on pull_request only.
      • git merge-tree against current main is clean.
    • Acceptance notes (not cards):
      • PR-body nit: "No other line moved" (driver-sql) omits an import line and one JSDoc line.
      • driver-mongodb keeps its own storage rule, and temporal-comparand.ts restates the rule's predicates. carrier: none.
      • The review observed that on the PostgreSQL native aggregate path a count comes back as text ("6"), where the in-memory path answers a number. It is identical at base and head, so not this PR's. carrier: none; recorded for the driver lane.
    • Filed from this card: 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 (an epoch-ms number against a date field; the review adds a third public-door answer: live PostgreSQL returns DATABASE_ERROR / 500).

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

  7. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20202, verified on main

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T09:35Z.

  8. added a commit that references this issue on Sep 28, 2026
    e5cf27d
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