Repository navigation
fix(metadata,objectql,driver-memory): route queryHistory through the lowering, lower type-blind without a field map, then delete driver-memory F3 (#5930 step 4, group 1b) - #20925
Conversation
In driver mode DatabaseLoader.queryHistory hands its filter straight to IDataDriver.find / count, which passes no seam. ADR-0053 D-D1 item 5 (as amended) says such a caller "gets the comparison it wrote" once a face's whole-day copy is deleted, so until = 'YYYY-MM-DD' would run as <= midnight and drop every version recorded later that day (measured on driver-memory with its copy deleted: 2 rows to 0). The loader now runs the shared lowerFilterCondition itself in driver mode, typed by the history object it syncs (recorded_at is Field.datetime; every other column lowers byte-identical, item 7). Engine mode is left to the engine's own seam. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
… field map declaredDatetimeLowering read "no field map" as "no datetime column" and left the whole-day rule to each driver's own copy. ADR-0053 D-D1 item 5 (as amended) retires those copies, and item 7 says what a seam that cannot read the declared type does: "applies the rewrite type-blind". So the absent-map branch now omits the reader and the whole-day rules apply to every column of such an object. An object with a field map keeps the typed scope byte-identical. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
InMemoryDriver's four copies of the bare-day upper-bound rule are deleted: the $lte and $between arms of the FilterCondition translator and the <= and between arms of the AST-node translator. Every seam hands the driver the lowered filter (ADR-0053 D-D1 items 5 and 9, as amended), so it compiles the comparison it is handed. Its direct-call suites now hand it what a typed seam hands it (lowerFilterCondition with the declared-datetime reader) and keep their expected rows. New pins: one cell per deleted site for a direct call that passed no seam, and the item-7 convergence on a registered object's text and undeclared columns (SqlDriver's answer), declared in the changeset. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check6 anchor(s) derived from 3 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 31 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 cb9db8ee178bc21771887b21387e98247412e6cd && git checkout cb9db8ee178bc21771887b21387e98247412e6cd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1571aedce565068688eeb2c2c8443901c5680de9 86ccdc099e3464076c7a90d6d9ca177e05b857f1 && git checkout -B drift-repro 1571aedce565068688eeb2c2c8443901c5680de9 && git merge --no-ff 86ccdc099e3464076c7a90d6d9ca177e05b857f1
node scripts/docs-audit/affected-docs.mjs --json 1571aedce565068688eeb2c2c8443901c5680de9 |
Contract reviewServed-tier: Head confirmed on the PR at the moment of reading (open, draft, base Check-runs on this head at the moment of reading (not waited on): 23 ① Derived judgmentsCommit 1 (
Commit 2 (
Commit 3 (
② Semver level
Review faces, sentence by sentence:
③ Boundary flags
Implemented-by: VERDICT: FAIL FAIL items:
Generated by Claude Code |
The driver-memory bullet said every seam hands InMemoryDriver the lowered filter. That is false of a row-level security using filter, which the security middleware ANDs into the where after the engine's seam and which only the RLS compile seam lowers, for the columns its field guard types as datetime. With no guard (the object's fields unresolvable) a bare-day upper bound in such a policy now reaches the driver as written, where F3 used to widen it. The bullet now says so, and that it holds until #20822 group 2 makes that seam type-blind. Patch levels and Clause-② unchanged. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta review after record 5919688563 (FAIL 1 on Check-runs on this head at the moment of reading (not waited on): 38 runs, 18 ① Derived judgmentsThe corrected
The
② Semver levelUnchanged from what 5919688563 judged right, and re-read at this head: ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #20822
Clause-②: no
#5930 step 4, group 1b: F3 (
driver-memory's query path). This follows the seat's answer B (5915193659 on #20822), read from ADR-0053 D-D1 items 5, 7 and 10 as amended: first route the direct caller, then make the engine seam type-blind when it has no field map, then delete. It is one PR in three ordered commits onmainat4d0b9cd542:0bd0e6d4f7fix(metadata)DatabaseLoader.queryHistoryin driver mode runslowerFilterConditionon its ownwhere. The reader is typed by the history object the loader syncs (recorded_atisField.datetime). The loader becomes a seam (item 5).5317b5aa22fix(objectql)declaredDatetimeLowering's absent-map branch drops the reader, so an object with no field map is lowered type-blind (item 7). An object with a field map keeps the typed scope byte-identical.e15606bae2refactor(driver-memory)lowerFilterConditionwith the declared-datetime reader. New pins cover item 5 (one cell per deleted site) and item 7's convergence.86ccdc099edocs(changeset)driver-memorybullet names the RLS no-guard path (review 5919688563, FAIL 1), and no longer says that every seam hands the driver a lowered filter. Changeset text only.13 files against
4d0b9cd542(+662 / -102 ate15606bae2; commit 4 changes one changeset line). The changeset is.changeset/20822-f3-route-then-delete.md:patchfor@objectstack/metadata,@objectstack/objectqland@objectstack/driver-memory, with the (b) convergence stated.The answers that move, named
These were measured through the real engine (
ObjectQLdist,engine.find) onSqlDriver(driver-sqlite-wasm) andInMemoryDriver. The table was synced throughdriver.syncSchema. The object was either registered in the engine with its field map ("registered") or not registered ("unregistered").r1=2026-07-28T00:00:00.000Z,r2=…T12:00:00.000Z,r3=…T23:59:59.999Z,r4=2026-07-29T00:00:00.000Z. The same instant is written intoat(datetime),txt(text) andextra(not declared; memory only).d(date) holds the calendar day.{ col: { $lte: '2026-07-28' } }.mainwith commit 1 only; c2 = after commit 2; c3 = after commit 3.at(datetime)d(date)txt(text, ISO)txt(text, ISO)extra(undeclared)at/dtxttxt/extraat$lte '9999-12-31'at$betweenthe dayCommit 2 moves one answer, a widening, on
SqlDriver. Take an unregistered object's non-datetime column that holds ISO instant text. A bare-day$lteon it now keeps the whole day (none becomes r1,r2,r3). That is item 7's reading for a seam that cannot read the declared type: "applies the rewrite type-blind". The dispatch expected thatSqlDriver's answer for an unregistered object would not move yet, because its F1 copy still exists (H2). That holds fordatetimeanddatecolumns only. F1 covers the columns the driver itself knows asdatetime, and nothing else.Commit 3 moves the (b) cells, both narrowings on
driver-memory, ontoSqlDriver's answer. On a registered object:textcolumn holding ISO text;The typed seam leaves both byte-identical, and the deleted copy used to widen them. The seat's answer calls this item 7's scope ("it is not a decision"). It is declared in the changeset. An unregistered object does not narrow on memory, because commit 2 now lowers it at the seam.
Neither move is a narrowing beyond what item 7 names, so nothing stopped.
Commit 1:
queryHistorybecomes a seam (H1: held)These were measured with a scratch probe over the built
distof@objectstack/metadata,driver-memoryanddriver-sqlite-wasm. The mode is driver mode (new DatabaseLoader({ driver })), with two saves on one day anduntil/since= that day:untilsince = untiluntilsince = untilmain(F3 present)Other direct driver callers in
packages/metadata. The other one isutils/history-cleanup.ts(recorded_at: { $lt: cutoffISO }, twice). That is an instant$lt, which no rule widens, so it is unaffected. The other_find/_countfilters in the loader are equality only. No other temporal bound was found.H4. Group 2 (
driver-sqlF1) meets the samequeryHistorycaller. Commit 1 lowers it for every driver, so group 2 has no caller left to route inpackages/metadata. §A below is the answer group 2 must keep once F1 is gone.Pin:
database-loader-20822-history-whole-day.test.ts. It fakesDateonly.wherethe driver'sfind/countreceive (recorded_at: { $lt: next day }, lower bound kept, instantuntiland other columns byte-identical).§B is driver-agnostic. It is the half that goes red when the loader stops lowering.
Not pinned on memory inside
@objectstack/metadata. A new test consumer of@objectstack/driver-memoryneeds a maintainer ruling (scripts/driver-memory-census.ledger.json,RULED_CEILING = 2). So the memory half is covered in two other ways:driver-memory's own pin of how it answers the lowered and the unlowered filter;Engine mode is untouched: the engine's
whereseam lowers it, typed by the registered history object.Commit 2: an object with no field map is lowered type-blind (H2: held for datetime and date, falsified for text)
The only change is
if (fields === null || typeof fields !== 'object') return {};. The typed branch is unchanged. The control in the new pin (engine-20822-no-field-map-type-blind-lowering.test.ts) and the existingengine-shared-filter-lowering-seam.test.tsstay green. The new pin coversfind,findOne,count,aggregate'swhereand the judge on an unregistered object.havinghas its own aggregated-row reader (F8, group 3), which is untouched.Commit 3: F3 deleted (H3: held)
Deleted:
$lteand$betweenarms of the FilterCondition translator;betweenarms of the AST-node translator ({ type: 'comparison' }, which no seam emits; only direct callers reach it).nextUtcCalendarDay/isUnboundedAboveare no longer imported bymemory-driver.ts. The clobber-class table inassembleLoweredWrites' docblock loses the$lt/$newriters the rewrite added. No driver-local guard is kept.F3's typed reader is the engine's
declaredDatetimeLowering. It is typed when the object has a field map, and type-blind without one (commit 2). The driver's ownsyncSchematemporal index is not consulted by any seam.The 43 direct-call tests are the same 43 that went red with the deletion alone:
Each now hands
find()what a typed seam hands it:lowerFilterConditionwith a reader over the fixture's own declared field map. In the 20661 file, theundeclaredreading is lowered type-blind, which is commit 2's reading. 0expect(lines changed in the six routed files.New pin:
memory-driver-20822-comparison-as-written.test.ts.datetimecolumn a bare day takes its storage form, the midnight instant, so$ltekeeps the midnight row.Ablations: each one committed first, restored and proven by blob hash, re-run at the final head
e15606bae2Every mutation went through
scripts/ablation-replace.mjs(anchor must hit, blob verified, restored blob equal to HEAD,git diff HEADempty).SqlDriver's F1 copy$ltearm restored (with its import)$betweenarm restoredbetweenarm restoredIn every commit-3 row, the red cells are the matching cells of the new pin and nothing else. Each restored copy is idempotent on lowered input (item 9).
The H1 dist counterfactual (the memory 2 / 2 to 0 / 0 row above) ran through
ablation-dist-preflight.mjsfor the restore leg: marker absent from all 30 built files and the tree clean. The mutate leg's arrival indistis shown by the probe's answer moving.Tests, gates and lint, all at
e15606bae2@objectstack/driver-memoryvitest: 66 files / 1424 passed.@objectstack/metadatavitest: 56 files / 836 passed.@objectstack/objectqlvitest--project local: 348 files / 6807 passed.--project repo: 1 / 5 passed.typecheck: exit 0. That covers objectql'scheck:test-typecheck(OK, 234 errors / 65 signatures held in the ledger). driver-memory'stsconfig.jsonprogram lists all 66 test files.node scripts/pm/dispatch-gates.mjs --commands(merge base4d0b9cd54) derived 66 families. 66 run, all exit 0. The--ranverdict: "66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero …)". This includes:check:driver-conformance: "OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt".check:driver-memory-census: "OK — every declaration is ledgered …".check:dual-build-cjs-loadsandcheck:type-check-debt, after a whole-workspace build.check:query-options-erasure: back at 236 test sites. My first draft added one{ where } as any, and it is now typed.eslint --no-inline-config --format jsonover the 12 changed.tsfiles gave 12 files, 0 errors and 0 warnings. Each file resolves under--print-config.eslint.config.mjsenables no type-aware linting ("noparserOptions.project, no typed@typescript-eslintrules"), so no untouched file's verdict can move. The fullpnpm lintis CI's.Acceptance notes
carrier:#5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 2 (driver-sqlF1), which removes the next copy standing behind this reading.plugin-securityrls-compiler.tsrlsLoweringreads an absent guard as "no datetime column". The guard is absent whengetObjectFieldNamescannot resolve the object. This is the same population, and the same reading, that commit 2 changed on the engine.{ signed_on: { $lte: '2026-01-05' } }.usingpolicy with a bare-day upper bound, on an object whose declared fields the security plugin cannot resolve, reachesdriver-memoryas written, where the deleted copy used to widen it. (Acheckclause reachesmatchesFilterCondition, not this driver, so it does not move here.) The changeset'sdriver-memorybullet names this path (86ccdc099e, after contract review 5919688563), and the seat's answer 5918373748 (A) carries therlsLoweringtwin into group 2.driver-memorypins because of the census ledger (above).--force-with-leaseand all five conditions met: first to fold two WIP commits into commit 3, then ontomain4d0b9cd542after fix(objectql,spec)!: a groupBy on a multi-value field and a count_distinct on a JSON-stored field are refused INVALID_FIELD / 400 at the engine aggregate door, on every driver (#20808) #20911 landed inengine.ts. No merge commit remains.domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 group 2 (F1, F2), group 3 (F6, F7, F8), and the stale matcher pointers the last group PR corrects.Generated by Claude Code