Repository navigation
fix(driver-memory): a cube $lte on a bare day keeps the whole day on a declared datetime field (#20661) - #20714
Conversation
…ore storage form (#20661) The mingo and SQL-echo lte rows asked nextUtcCalendarDay about the comparand already in storage form, which on a declared datetime field is the midnight instant the helper refuses to widen. Both rows now render one lteUpperBound decision taken from the authored value, and only the widened bound is converted (ADR-0053 D-E3). Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…nd declared datetime (#20661) Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
#20661) Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 199fc9bab269f329a34294f9f11a6a1fb664700d && git checkout 199fc9bab269f329a34294f9f11a6a1fb664700d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57574637129bebb4a6868c465be7e1b80eed1954 8cae2ae3980972016b27ab33b3c6f59cf3197f8e && git checkout -B drift-repro 57574637129bebb4a6868c465be7e1b80eed1954 && git merge --no-ff 8cae2ae3980972016b27ab33b3c6f59cf3197f8e
node scripts/docs-audit/affected-docs.mjs --json 57574637129bebb4a6868c465be7e1b80eed1954
|
Contract reviewServed-tier: Inputs: card #20661 (body; triage 5893953164; claim 5897541483; os-dev-report 5898224404), PR #20714 (body, file list, net diff against ① Derived judgmentsFiles:
② Semver level
Clause-②: Shipped prose, sentence by sentence against the diff and
③ Boundary flags
Check-runs on Implemented-by: VERDICT: PASS Generated by Claude Code |
…ommits that decided them (objectstack-ai#20717) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the seventh stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/plugins/plugin-approvals/src/**` and nothing else. By the seat's census at the claim (`5897866351`), it is the largest package in the lane that no in-flight work holds. Later stages cover the other packages, so this PR says `Part of` and the card stays open. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), by the method of stages 1 to 6 (PR objectstack-ai#20609 as `422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as `9b384f63a`). That is **41 sites on 38 lines in 13 files, covering 14 numbers**: - 24 census sites (every census site this package has); - 15 sites in test comments, which the census defers; - 2 sites the gate's citation grammar cannot see, found by a raw scan (see Acceptance notes): the second number of `objectstack-ai#8287/objectstack-ai#8778` (`approval-node.test.ts:462`) and 「the option objectstack-ai#8710 rejected」 (`approval-service.ts:2329`), which the gate reads as an option ordinal. Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and says in its own words what was decided: **13 distinct shas**. No number in this package has an ADR or ruling record of its own in the repository (a grep of `docs/adr/` for all 14 finds none, and a grep of the rest of `docs/` finds only an audit that names `objectstack-ai#11311` as evidence), so every anchor is a commit, per ruling C's order. No number was dropped. Only comments changed. Every touched source file keeps its line count (41 lines out, 41 in, over 13 files), so no line citation into these files moves. 3 of those 41 lines hold no dead citation: 1 reflow line and 2 lost-referent lines, listed under Wordings below. No code token moves (see the guard below). **No citation number is added.** Every tracker number on an added line was already on the line it replaces: `objectstack-ai#8613` (`approval-service.ts:2295`, `:2363`, `approval-service.test.ts:751`), `objectstack-ai#8287` (`sys-approval-request.object.ts:138`, `approval-node.test.ts:462`), `objectstack-ai#10101` (`backfill-platform-row-organizations.ts:9`) and `objectstack-ai#12069` (`translations/index.ts:26`). Each answers 200. Over the whole diff, added minus removed is 0 or negative for every number, and no number is new to the diff. No PR number stands on an added line; the one `PR #N` spelling in scope (`backfill-platform-row-organizations.ts:9`) became its squash commit. Five dead sites are left on purpose, all of them test strings (see the list below). One more file: a `patch` changeset for `@objectstack/plugin-approvals`, because the rewritten docblocks and inline comments ship (see Changeset below). ## Census: `plugin-approvals`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/plugins/plugin-approvals/`. Each run counts as a reading only because its board frontier equals the newest issue number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | plugin-approvals sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `575746371`, run 2026-09-29T20:15:05Z to 20:18:28Z | enumerated, 186 pages, frontier objectstack-ai#20709 (newest objectstack-ai#20709 before and after), 18,536 numbers | 1,195 | **24** | 22 | 6 | 10 | | after | head `e698d2393`, run 20:28:39Z to 20:31:58Z | enumerated, 186 pages, frontier objectstack-ai#20716 (newest objectstack-ai#20714 before, objectstack-ai#20716 after), 18,543 numbers | 1,171 | **0** | 0 | 0 | 0 | The before count matches the seat's census at the claim and A1 (24 sites). The whole-repo drop is 24, exactly this diff's census sites. The `resolves` tally is 32,971 in both runs, and `resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did not move either. The after run was taken on `e698d2393`; the head `708244c2b` adds only the changeset. No run was truncated or discarded: both enumerations read 186 pages at the newest frontier. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `namesThisRepository` over every `.ts` file under `plugin-approvals/src` (76 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it `allocated-but-absent`, and alive when that census judged it on this board anywhere (its `--list` extraction) and did not report it. The 18 numbers the census never saw, because they stand only in test files or strings here, were read one by one on the issues endpoint: 14 answer 200, and `objectstack-ai#8863`, `objectstack-ai#11081`, `objectstack-ai#11286` and `objectstack-ai#11308` answer 404. | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `575746371` | 981 | **44** | 24 | 15 | 0 | 5 | | after, `e698d2393` | 942 | **5** | 0 | 0 | 0 | 5 | Its src-comment column equals the census's 24, which is the control on the second instrument. The 902 live citations and the 32 cross-repo citations are the same in both readings, and the drop of 39 citations is exactly the rewritten sites the gate grammar sees. Three extracted tokens are not citations and stay unjudged in both readings: `&objectstack-ai#39;` (an HTML entity) and two CSS colours, all in `action-link-pages.ts` string literals. A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) finds 46 dead occurrences before and 5 after; the 2 it sees beyond the gate are the two gate-invisible sites above, and its residue equals the gate's residue site for site. ## Per-number table Sites and files count every dead occurrence in scope at the base (comments and strings, tests included). `rewritten / left` counts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject, and `git blame` at the base puts every rewritten line in its anchor commit or in a later commit that descends from it (`merge-base --is-ancestor` exit 0 for each pair). | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#16709` | 10/2 | 8/2 | `8c7cca1ce`: the three residues of the stranded-inspection contract review. Item 2 (the PM ruling of 2026-09-08) keeps a row whose third read threw in the report as the undifferentiated `failed`; item 3 moves `refineFailedRunState` inside the `try`, so a malformed host verdict costs only its own row. Its message numbers the items, which is why the lines keep 「item 2」 and 「item 3」. New to the sweep | | `objectstack-ai#8710` | 6/2 | 6/0 | `04d03c3a0`: a deactivated `sys_position` confers no sharing-rule shares, filtered at the sharing call site and never inside the addressing primitive. Its message quotes the 2026-08-15 ruling verbatim, the same sentence the quoted blocks here carry, and its diff writes the 「a name with no row is untouched」 fallback that `approval-service.ts:2318` quotes. Stage 2's anchor, and `plugin-sharing/src/position-graph.ts:42` already reads 「objectstack-ai#8613 / commit 04d03c3」 | | `objectstack-ai#6523` | 4/3 | 4/0 | `aa4b90d9a`: the 36 enforcement signatures, `IApprovalService` among them, converged onto the full `ExecutionContext`. Its subject names it. Stages 2 and 6 and the spec stage's anchor | | `objectstack-ai#6206` | 3/3 | 3/0 | `aa4b90d9a`: the same commit, whose body applies 「the objectstack-ai#6206 ruling default (converge on the full envelope, keep no per-site subset contracts)」. Written as the full-envelope ruling, the form stages 2 and 6 used | | `objectstack-ai#8778` | 4/4 | 4/0 | `7901b2dd2`: the stamp-only `tenancy.organizationField`, Option A of the maintainer's ruling, declared on `sys_api_key` as `active_organization_id`. The spec, `plugin-security` and `service-storage` stages' anchor | | `objectstack-ai#11081` | 5/1 | 5/0 | `c28e4cfae`: the two SqlDriver-backed fixtures of `objectstack-ai#11081` stop muting their kernel and pin the expected read-refusal noise with the runtime's shared capture. Its diff writes all five `[objectstack-ai#11081]` tags. New to the sweep | | `objectstack-ai#11286` | 5/1 | 2/3 | `b019891cd`: the contract test that pins the two `managerIsProvablyOutsideOrg` screens to equal verdicts. Its subject names it. New to the sweep | | `objectstack-ai#11674` | 2/1 | 2/0 | `1cba33f16`: the seed loader warns at load time when a seed defers a required column, and the ordering constraint is documented at the four pointer-pair sites, this object among them. Stage 2's anchor for the same paragraph | | `objectstack-ai#12493` | 2/2 | 2/0 | `aa5994e17`: the Operation Message Catalog gains `approval_recall_not_submitter` (and `record_write_denied`) ahead of their emitters. Its diff names `objectstack-ai#12493` throughout. Stage 2's anchor | | `objectstack-ai#8707` | 1/1 | 1/0 | `1408fe385`: audit rows are stamped from the record's own organization, which its message says the maintainer's ruling on `objectstack-ai#8287` requires; the line keeps 「honouring objectstack-ai#8287's ruling」. New to the sweep | | `objectstack-ai#8863` | 1/1 | 1/0 | `d200b016b`: the two negative pins that assert the unfiltered position expansion on the approvals side. Its body names `objectstack-ai#8863`. New to the sweep | | `objectstack-ai#11308` | 1/1 | 1/0 | `5a916c4d4`: the one-off platform-row organization backfill, dry run and write, which its body calls the `objectstack-ai#11308` sweep. New to the sweep | | `objectstack-ai#11311` | 1/1 | 1/0 | `1272f0a6b`: the squash commit of the pull request that was `objectstack-ai#11311` (its subject carries the number), which moved the resolver to `metadata-core` and made the approval and automation-run writers stamp the subject's organization. New to the sweep | | `objectstack-ai#11671` | 1/1 | 1/0 | `09b4f4e4e`: the source-hashes provenance companion. The identical `translations/index.ts` line in `service-messaging`, `plugin-sharing` and `plugin-security` already cites it | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each of the 13), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13; the history is complete, `--is-shallow-repository` false, 15,129 commits). Each of the 14 numbers answers 404 on the issues endpoint, read one by one; `objectstack-ai#11311` answers 404 on the pulls endpoint too. ## Wordings to check - **The full-envelope ruling, `approval-node.ts:29`, `approval-service.ts:52-53` and `exec-context-annotation.pin.ts:7-8`.** 「since objectstack-ai#6523 (the objectstack-ai#6206 ruling …)」 became 「since commit aa4b90d (the full-envelope ruling …)」, word for word the form `plugin-sharing`'s landed `sharing-service.ts:20` and `exec-context-annotation.pin.ts:7` use. `approval-service.ts:53` is 1 reflow line. - **The ruling's record, `approval-service.ts:2295` and `approval-service.test.ts:751`.** 「Maintainer ruling, 2026-08-15 (objectstack-ai#8710, inheriting objectstack-ai#8613), verbatim:」 became 「… (commit 04d03c3, inheriting objectstack-ai#8613), verbatim:」. The quotation under it is the ruling itself and is untouched; `04d03c3a0`'s message carries the same sentence. - **`approval-service.ts:2329`.** 「that is the option objectstack-ai#8710 rejected」 became 「that is the option the ruling (commit 04d03c3) rejected」. - **The test heading, `approval-service.test.ts:749`.** 「the objectstack-ai#8710 carve-out, asserted on THIS side (objectstack-ai#8863)」 became 「the commit 04d03c3 carve-out, asserted on THIS side (commit d200b01)」: the carve-out's record, and the commit that asserted it here. - **A PR number, `backfill-platform-row-organizations.ts:9`.** 「objectstack-ai#10101 (landed as PR objectstack-ai#11311)」 became 「objectstack-ai#10101 (landed as commit 1272f0a)」, the pull request's squash commit. - **Item numbers, `approval-service.ts:4893`, `:4906` and `stranded-request-inspection.test.ts:123`.** 「[objectstack-ai#16709 item 3]」 became 「[commit 8c7cca1, item 3]」, and likewise for item 2, beside its 「PM ruling, 2026-09-08」, which `8c7cca1ce`'s message records under 「Item 2」. - **Lost referents, 2 lines with no dead site** (every file keeps its line count): `backfill-platform-row-organizations.test.ts:17` 「the one thing this card must not do」 became 「the one thing this sweep must not do」, and `manager-org-screen-parity.contract.test.ts:61` 「the very decision this card is fenced out of」 became 「the very decision this pin is fenced out of」. Each 「this card」 pointed at the number the same comment block opened with, which is now a commit; `b019891cd`'s message says the pin 「PINS the duplication, it does not remove it」. ## The 5 sites left - **Test strings, 5 sites**, left as stages 1 to 6 left theirs: - `describe` / `it` titles: `manager-org-screen-parity.contract.test.ts:232` (`objectstack-ai#11286`), `stranded-request-inspection.test.ts:519` and `:642` (`objectstack-ai#16709`); - a test double's thrown message and an assertion message: `manager-org-screen-parity.contract.test.ts:107` and `:294` (`objectstack-ai#11286`). - There is no operator string, generated header or quoted ruling carrying a dead number in this package. The generated `*.source-hashes.generated.ts` headers already cite `09b4f4e4e` and are untouched. The two verbatim quotations of the 2026-08-15 ruling carry no number and are untouched. ## Mechanical guard: no code token moves The guard compares the TypeScript parser's leaf nodes, with comments as trivia and JSDoc nodes never visited, base `575746371` against head. Template literals are therefore read in context. It ran over all 13 touched `.ts` files. - Real run: 36,204 base leaf tokens, **0 files with a token change** (exit 0). - Comment control in `sys-approval-request.object.ts` (「who a row is ABOUT」 to 「whom a row is ABOUT」): 0 files changed, as expected (exit 0). - Positive control, a code token added in `sys-approval-request.object.ts` (`referenceVia: 'object_name',` given a trailing `as const`): DIFFER (exit 1). - Positive control, one digit changed inside a kept test title (`stranded-request-inspection.test.ts:642`): DIFFER (exit 1). Every mutation went through `scripts/ablation-replace.mjs`, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (`6cb56301a334`, `757ad45900ac`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/plugin-approvals` (`.changeset/20596-plugin-approvals-provenance-anchors.md`) is included. Its body is stage 6's, word for word, with the package name changed. Measured on the built package (A3): `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After the build (a cache miss for this package, so `dist` is this head's source), the rewritten comments reach `dist`: `8c7cca1ce` 4 times and `04d03c3a0` 4 times in each of `dist/index.d.ts` and `index.d.mts`; `04d03c3a0` 4 times, `1cba33f16` twice, and `8c7cca1ce`, `7901b2dd2` and `1408fe385` once each in each of `index.js` and `index.mjs`. Positive controls: the unchanged line 「A step routing to nobody is」, in the same docblock as the shipped rewrite at `approval-service.ts:2295`, is found once in each of the four files, and the unchanged line 「itself stays unwalled (`tenancy.enabled: false`)」 beside the shipped rewrite at `sys-approval-request.object.ts:144` once in each JS file. A never-written negative phrase appears nowhere in `dist`. None of the 14 dead numbers is left anywhere in `dist`. ## Gates (head `708244c2b`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` (self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 5 citations across 6 files, and all 5 resolve (`objectstack-ai#8613` twice, `objectstack-ai#8287`, `objectstack-ai#10101`, `objectstack-ai#12069`), each already on the line it replaces. - **Doc authoring:** `pnpm check:doc-authoring` exits 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `708244c2b` derived 64 commands: all 57 derived at dispatch, plus `check:dispatcher-error-vocabulary`, `check:engine-double-contract`, `check:objectql-double-limit`, `check:query-options-erasure`, `check:type-check-coverage`, `check:type-check-debt` and `check:where-matcher`. Each ran with its exit code captured before any pipe, and all 64 exit 0. `--ran`, fed each command with its exit code, reports 64 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*` and `./packages/*/*` ran first under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an unbuilt workspace. - **Roster families the derivation lists outside its commands** (their rosters sit in directories this diff touches): `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit 0. - **Tests and typecheck, under the verify lock:** - `pnpm --filter @objectstack/plugin-approvals test`: 51 files pass and 791 tests pass. That is every test file in the package, the 7 touched ones included. - `pnpm --filter @objectstack/plugin-approvals typecheck` exits 0 (`tsc` on `tsconfig.json`, the scripts program, and the test layer on `tsconfig.test.json`, held at its ledger of 8 files, 324 errors and 27 pinned signatures). `--listFiles`: the `tsconfig.json` program holds the 25 non-test files under `src/`, the 6 touched ones included; the `tsconfig.test.json` program holds all 76 files under `src/`, the 51 test files and all 13 touched files included. - **Lint, as a proven narrowing:** `eslint --no-inline-config --format json` over the 13 touched `.ts` files gives 13 files, 0 errors and 0 warnings. All 13 are in eslint's own population (`isPathIgnored` is false for each; a `dist` file, as the control, is ignored). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own lines 327-328 state), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the 14 changed files for control bytes finds none. ## Acceptance notes - **The gate-invisible spellings, grepped as the claim asked.** `CITATION_RE` refuses a hyphen after the digits and a `/` before the `#` (objectstack-ai#20636). In this package there is one `#N-word` spelling, 「objectstack-ai#3266-era」 (`record-reader-visibility.test.ts:342`), and two `#A/#B` spellings, `objectstack-ai#8287/objectstack-ai#8778` (`approval-node.test.ts:462`) and `objectstack-ai#8543/objectstack-ai#8580` (`approval-vocabularies.test.ts:66`): the claim's 3, 1 and 2. `objectstack-ai#3266`, `objectstack-ai#8287`, `objectstack-ai#8543` and `objectstack-ai#8580` answer 200; the second number `objectstack-ai#8778` is dead, so that one line is rewritten here. - **A third spelling the gate cannot see, found by the raw scan.** `NON_CITATION_HEADS` excuses any `#N` after the word 「option」 as an option ordinal, so 「the option objectstack-ai#8710 rejected」 (`approval-service.ts:2329`) was never extracted: a dead number there would pass the diff gate at exit 0 and never enter a census count. It is rewritten here. Across the gate's declared surfaces at the base, the only other `option #N` with three or more digits is `packages/objectql/src/validation/rule-validator.ts:2202` (`option objectstack-ai#14088`), which answers 200. Same family as objectstack-ai#20636; noted for its closeout, not a card of its own. - **A retired key name in this package's prose, not changed here.** `tenancy.organizationField` left the authorable surface in `502f179cc`, and limb 0 of the shared resolver now reads `PLATFORM_STAMP_ORGANIZATION_COLUMNS` in `metadata-core`, keyed by object name. Comments in this package still name the retired key as what limb 0 reads (`sys-approval-request.object.ts:143`, the line above a rewrite; `backfill-platform-row-organizations.ts:35`, `approval-node.test.ts:463`, `approval-service.ts:2707`, `backfill-platform-row-organizations.test.ts:50`), and two test fixtures still declare it on a stub `sys_api_key` (`approval-node.test.ts:467`, `backfill-platform-row-organizations.test.ts:54`), where it is inert because the resolver keys by name. The anchor `7901b2dd2` is right for the key those lines name, and nothing is wrong at runtime. Correcting the prose would reach past the dead citations, and the fixtures are code tokens, so none of it is changed here. - **The census instrument did not truncate in this stage.** Both enumerations read 186 pages at the newest frontier. - **Anchors the next stages can reuse**, each checked here: `objectstack-ai#16709` → `8c7cca1ce`; `objectstack-ai#11081` → `c28e4cfae`; `objectstack-ai#11286` → `b019891cd`; `objectstack-ai#11308` → `5a916c4d4`; `objectstack-ai#11311` → `1272f0a6b`; `objectstack-ai#8707` → `1408fe385`; `objectstack-ai#8863` → `d200b016b`. - **Base.** The branch is on `main` at `575746371`, which is still `main` at 20:56Z (read into a private ref), so there was no merge. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20661
Clause-②: no
The memory analytics (cube) face now applies the whole-day rule for a bare-day
$lteBEFORE it converts the bound to the field's storage form, as ADR-0053 D-E3 orders. On a declareddatetimefield,where { created_at: { $lte: '2026-07-28' } }now answers the same rows asfind(), and the SQL echo shows the bound that runs.The change
packages/drivers/driver-memory/src/memory-analytics.ts:lteUpperBound(authored, comparand, storageForm). It asksnextUtcCalendarDayabout the AUTHORED value and returns one of three decisions:before(a bare day: the next day's midnight, converted to storage form, exclusive),unbounded(9999-12-31, which has no next day), orthrough(anything else: the authored comparand in storage form, inclusive).lterows, the mingo$matchrow (CUBE_OPERATOR_TO_MONGO_PREDICATE) and the SQL echo row (CUBE_OPERATOR_TO_SQL_PREDICATE), now only render that one decision. They used to callnextUtcCalendarDay(comparands[0]), andcomparandsis already in storage form. On a declareddatetimefield that value is the instant2026-07-28T00:00:00.000Z, which the helper correctly refuses to widen.comparandsForbecamestorageFormFor(cube, member), which returns the per-member conversion (stilldriver.filterComparandStorageForm). Each exit maps its comparands through it and passes the same function to its builder asstorageForm, for the one bound a builder derives instead of receiving.comparandsis still computed for every operator, so every conversion (and any refusal it raises) happens exactly as before.nextUtcCalendarDay(packages/speccalendar-day.ts) is untouched. The driver's own filter path (memory-driver.ts) was already in this order (nextUtcCalendarDay(val), thenstore(nextDay)) and is untouched.The landing site is the one the claim expected. The producer is the emitter, not
filterComparandStorageForm: ADR-0053 D-D1 keeps the storage-form conversion operator-blind and puts the operator-sensitive rewrite on the emitters.Before and after, measured at the published export
Built
@objectstack/driver-memorydist/index.mjs(MemoryAnalyticsService,InMemoryDriver). Before =origin/mainatfa0a4b6610(this branch's base;e4e5222b7plus one commit that does not touch this package). After = this branch. Rowsr28created_at: 2026-07-28T10:00:00.000Zandr272026-07-27T10:00:00.000Z. "Declared" meanssyncSchemawithcreated_at: datetimeandmade_on: date.wherefind()query()beforequery()aftercreated_atundeclared$lte '2026-07-28'created_at < '2026-07-29'created_atdeclareddatetime$lte '2026-07-28'created_at <= '2026-07-28T00:00:00.000Z'created_at < '2026-07-29T00:00:00.000Z'made_ondeclareddate(control)$lte '2026-07-28'made_on < '2026-07-29'created_atdeclareddatetime$lteaDateat2026-07-28T00:00Z(control)created_at <= '2026-07-28T00:00:00.000Z'created_atdeclareddatetime$lte '9999-12-31', rowsc262026-07-15T14:00Z,mid9999-12-31T10:00Z,nonecreated_at <= '9999-12-31T00:00:00.000Z'created_at IS NOT NULLcreated_atundeclared$lte '9999-12-31', same rowscreated_at IS NOT NULLIn the last-day rows,
nonewas written withcreated_at: null, and the driver'screatestamps itscreated_atdefault in its place, so that row has a value. The pinned test uses no such row.Dispatch hypotheses
fa0a4b6610, bothlterows callednextUtcCalendarDay(comparands[0])(:273,:468).comparandscame fromcomparandsFor(:1555, called at:886and:1367), and the builder context also carriedraw. The fix widens fromraw[0]and converts only the widened bound, through the same conversion the comparands take. It is one function that both rows render, so the two exits cannot drift.$betweenis not inMONGO_TO_CUBE_OPERATOR. Measured on both exits with a declareddatetimefield and a bare-day maximum:INVALID_FILTER/ 400 ("declared by the Filter Protocol but cannot be compiled by driver-memory's analytics (cube) face"). No row added. The new test pins that refusal on the date shape, so widening the table to take$betweenturns it red and points atlteUpperBound.timeDimensions[].dateRangeend widens the rawendstring before it builds the string andDatebounds (:1005), so it already follows D-E3. On a declareddatetimefield,['2026-07-01', '2026-07-28']answers r27, r28 before and after, the same asfind()with$gte/$lte. Pinned, not changed.datefield the storage form of a bare day is that bare day, and the answer and echo are byte-identical before and after (table above). Pinned.isUnboundedAbovepath still answers "has a value". Its existing pins inmemory-driver-calendar-day-upper-bound.test.ts(undeclared field) are green. On a DECLAREDdatetimefield the last day was also broken by this card's order (<= '9999-12-31T00:00:00.000Z', droppingmid). It now answersIS NOT NULL/$ne: nulland is pinned.Verification
Tests: new
packages/drivers/driver-memory/src/memory-analytics-20661-lte-whole-day-first.test.ts, 7 cases. Every case compares the cube's rows withfind()and with a literal id list, and checks the echo'sWHERE:the card's pair, undeclared and declared
datetime;a full-timestamp
$lte(stays inclusive, in storage form);the declared
datecontrol;the declared-
datetimelast day and the day before;the
dateRangesibling;the
$betweenrefusal on both exits, asserting the envelopecodeandstatus.pnpm --filter @objectstack/driver-memory test: 63 files, 1456 tests passed.pnpm --filter @objectstack/driver-memory typecheck(tsc --noEmit && tsc --noEmit -p tsconfig.typecheck.json): exit 0.--listFileson the first program lists the new test file.Reverse verification. The fix was committed first (HEAD
cb155edf6c). The subject is loaded fromsrcthrough the test's relative import, so nodistis involved. The mutation went throughscripts/ablation-replace.mjsunder atraprestoringHEAD: the anchornextUtcCalendarDay(authored)was replaced withnextUtcCalendarDay(comparand), which is the old order. On disk the anchor went 1 to 0, the replacement 0 to 1, and the blob014497594cc6becamea41c37fac411. Run over the new file andmemory-driver-calendar-day-upper-bound.test.ts: 2 failed, 15 passed. The two failures were the declared-datetimepair (expected [ 'r27' ] to deeply equal [ 'r27', 'r28' ]) and the declared last day (expected [ 'c26', 'prev' ] …). The undeclared,date, instant,dateRange,$betweenand #20600 cases stayed green, which is the expected direction. Restore: the blob equals theHEADblob014497594cc6andgit diff HEADis empty.Gates, at
8cae2ae398:node scripts/pm/dispatch-gates.mjs --commands: 60 commands, all exit 0.check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debtfirst exited 3 (PREREQUISITE NOT MET, no built closure). They were re-run green afterpnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'(71 of 71 tasks).--ranreconciliation: "60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN".Lint, narrowed and not the repo-wide run:
eslint --no-inline-config --format jsonover the 2 touched.tsfiles: 2 files, 0 errors, 0 warnings.--print-configresolves a config for each, with 6 and 5 rules.eslint.config.mjsenables no type-aware linting (noparserOptions.project; its own header says so at:327), so this diff cannot move the verdict for any untouched file.The
Clause-②: noline above is the claim's.Acceptance notes
MemoryAnalyticsService:git grep 'new MemoryAnalyticsService('finds 0 non-test hits against 31 in test files (the control).service-analyticsplugin.ts:245mentions it in a comment as a registered fallback "from dev-plugin", but no code in this repo registers it.service-analyticsnative-sql-strategy.ts:1244widensvalues[0]and then coercesnextDay.objectql-strategy.ts:548widens the raw$ltebound.lteBoundis type-blind and converts nothing.driver-mongodbmongodb-filter.ts:1165widensvalueand then storesnextDay.origin/mainby 2 commits. They were9b384f63ae(service-storage docs) and5757463712(platform-objects translations), with no path indriver-memory,specorcore. The branch was not merged forward; CI's merge ref covers them.Generated by Claude Code