Skip to content

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

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20822-g1b-f3-route-then-delete
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20822-g1b-f3-route-then-delete

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

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 on main at 4d0b9cd542:

# Commit What it does
1 0bd0e6d4f7 fix(metadata) DatabaseLoader.queryHistory in driver mode runs lowerFilterCondition on its own where. The reader is typed by the history object the loader syncs (recorded_at is Field.datetime). The loader becomes a seam (item 5).
2 5317b5aa22 fix(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.
3 e15606bae2 refactor(driver-memory) F3's four whole-day sites are deleted. The 43 direct-call tests are routed through lowerFilterCondition with the declared-datetime reader. New pins cover item 5 (one cell per deleted site) and item 7's convergence.
4 86ccdc099e docs(changeset) The driver-memory bullet 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 at e15606bae2; commit 4 changes one changeset line). The changeset is .changeset/20822-f3-route-then-delete.md: patch for @objectstack/metadata, @objectstack/objectql and @objectstack/driver-memory, with the (b) convergence stated.

The answers that move, named

These were measured through the real engine (ObjectQL dist, engine.find) on SqlDriver (driver-sqlite-wasm) and InMemoryDriver. The table was synced through driver.syncSchema. The object was either registered in the engine with its field map ("registered") or not registered ("unregistered").

  • Rows: 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 into at (datetime), txt (text) and extra (not declared; memory only). d (date) holds the calendar day.
  • Filter: { col: { $lte: '2026-07-28' } }.
  • Columns: BASE = main with commit 1 only; c2 = after commit 2; c3 = after commit 3.
Object · column Driver BASE c2 c3
registered · at (datetime) both r1,r2,r3 r1,r2,r3 r1,r2,r3
registered · d (date) both r1,r2,r3 r1,r2,r3 r1,r2,r3
registered · txt (text, ISO) sqlite none none none
registered · txt (text, ISO) memory r1,r2,r3 r1,r2,r3 none
registered · extra (undeclared) memory r1,r2,r3 r1,r2,r3 none
unregistered · at / d both r1,r2,r3 r1,r2,r3 r1,r2,r3
unregistered · txt sqlite none r1,r2,r3 r1,r2,r3
unregistered · txt / extra memory r1,r2,r3 r1,r2,r3 r1,r2,r3
any · at $lte '9999-12-31' both r1..r4 r1..r4 r1..r4
any · at $between the day both r1,r2,r3 r1,r2,r3 r1,r2,r3
  • Commit 2 moves one answer, a widening, on SqlDriver. Take an unregistered object's non-datetime column that holds ISO instant text. A bare-day $lte on 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 that SqlDriver's answer for an unregistered object would not move yet, because its F1 copy still exists (H2). That holds for datetime and date columns only. F1 covers the columns the driver itself knows as datetime, and nothing else.

  • Commit 3 moves the (b) cells, both narrowings on driver-memory, onto SqlDriver's answer. On a registered object:

    • a declared text column holding ISO text;
    • a column the object does not declare.

    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: queryHistory becomes a seam (H1: held)

These were measured with a scratch probe over the built dist of @objectstack/metadata, driver-memory and driver-sqlite-wasm. The mode is driver mode (new DatabaseLoader({ driver })), with two saves on one day and until / since = that day:

State memory until memory since = until sqlite until sqlite since = until
main (F3 present) 2 / 2 2 / 2 2 / 2 2 / 2
all three commits 2 / 2 2 / 2 2 / 2 2 / 2
F3 deleted, loader lowering removed (dist ablation) 0 / 0 0 / 0 2 / 2 (F1 still present) 2 / 2
  • Other direct driver callers in packages/metadata. The other one is utils/history-cleanup.ts (recorded_at: { $lt: cutoffISO }, twice). That is an instant $lt, which no rule widens, so it is unaffected. The other _find / _count filters in the loader are equality only. No other temporal bound was found.

  • H4. Group 2 (driver-sql F1) meets the same queryHistory caller. Commit 1 lowers it for every driver, so group 2 has no caller left to route in packages/metadata. §A below is the answer group 2 must keep once F1 is gone.

  • Pin: database-loader-20822-history-whole-day.test.ts. It fakes Date only.

    • §A: the rows on real SQLite in driver mode.
    • §B: the where the driver's find / count receive (recorded_at: { $lt: next day }, lower bound kept, instant until and 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-memory needs a maintainer ruling (scripts/driver-memory-census.ledger.json, RULED_CEILING = 2). So the memory half is covered in two other ways:

    • §B (what every driver receives), plus driver-memory's own pin of how it answers the lowered and the unlowered filter;
    • the dist measurement in the table above.
  • Engine mode is untouched: the engine's where seam 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 existing engine-shared-filter-lowering-seam.test.ts stay green. The new pin covers find, findOne, count, aggregate's where and the judge on an unregistered object. having has its own aggregated-row reader (F8, group 3), which is untouched.

Commit 3: F3 deleted (H3: held)

  • Deleted:

    • the $lte and $between arms of the FilterCondition translator;
    • the less-or-equal and between arms of the AST-node translator ({ type: 'comparison' }, which no seam emits; only direct callers reach it).

    nextUtcCalendarDay / isUnboundedAbove are no longer imported by memory-driver.ts. The clobber-class table in assembleLoweredWrites' docblock loses the $lt / $ne writers 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 own syncSchema temporal index is not consulted by any seam.

  • The 43 direct-call tests are the same 43 that went red with the deletion alone:

    Suite Tests
    temporal-conformance 25
    calendar-day-upper-bound 5
    analytics-20661 5
    datetime-storage 4
    temporal-storage-form 2
    shared-lowering-door 2

    Each now hands find() what a typed seam hands it: lowerFilterCondition with a reader over the fixture's own declared field map. In the 20661 file, the undeclared reading is lowered type-blind, which is commit 2's reading. 0 expect( lines changed in the six routed files.

  • New pin: memory-driver-20822-comparison-as-written.test.ts.

    • §A (item 5): a direct call gets the comparison it wrote. There is one cell per deleted site. On a datetime column a bare day takes its storage form, the midnight instant, so $lte keeps the midnight row.
    • §B (item 7): the registered-object convergence cells.
    • §C: the type-blind reading.

Ablations: each one committed first, restored and proven by blob hash, re-run at the final head e15606bae2

Every mutation went through scripts/ablation-replace.mjs (anchor must hit, blob verified, restored blob equal to HEAD, git diff HEAD empty).

Commit Mutation Red Green
1 loader lowering removed 2 of 8 (§B's two bare-day cells) §A stays green through SqlDriver's F1 copy
2 absent-map branch removed 4 of 19 (the four unregistered lowering cells) the control, the instant and the judge cells
3 $lte arm restored (with its import) 4 of 1424 the other 1420
3 $between arm restored 2 of 1424 the other 1422
3 AST less-or-equal arm restored 1 of 1424 the other 1423
3 AST between arm restored 1 of 1424 the other 1423

In 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.mjs for the restore leg: marker absent from all 30 built files and the tree clean. The mutate leg's arrival in dist is shown by the probe's answer moving.

Tests, gates and lint, all at e15606bae2

  • @objectstack/driver-memory vitest: 66 files / 1424 passed.
  • @objectstack/metadata vitest: 56 files / 836 passed.
  • @objectstack/objectql vitest --project local: 348 files / 6807 passed. --project repo: 1 / 5 passed.
  • The three packages' typecheck: exit 0. That covers objectql's check:test-typecheck (OK, 234 errors / 65 signatures held in the ledger). driver-memory's tsconfig.json program lists all 66 test files.
  • node scripts/pm/dispatch-gates.mjs --commands (merge base 4d0b9cd54) derived 66 families. 66 run, all exit 0. The --ran verdict: "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-loads and check: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.
  • Lint, narrowed. eslint --no-inline-config --format json over the 12 changed .ts files gave 12 files, 0 errors and 0 warnings. Each file resolves under --print-config. eslint.config.mjs enables no type-aware linting ("no parserOptions.project, no typed @typescript-eslint rules"), so no untouched file's verdict can move. The full pnpm lint is CI's.

Acceptance notes


Generated by Claude Code

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>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

6 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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 31 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1571aedce565068688eeb2c2c8443901c5680de9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cb9db8ee178bc21771887b21387e98247412e6cd — the merge of head 86ccdc099e3464076c7a90d6d9ca177e05b857f1 into base 1571aedce565068688eeb2c2c8443901c5680de9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e15606bae222670556ae385e4764ecab66a3608b
Local-runs: none

Head confirmed on the PR at the moment of reading (open, draft, base main). Merge base 4d0b9cd542; three single-parent commits 0bd0e6d4f7 → 5317b5aa22 → e15606bae2, no merge commit; PR #20895 (8fec76a2b) is an ancestor of the merge base. Net diff against main: 13 files, +662 / −102, the PR's file list exactly; no governed path.

Check-runs on this head at the moment of reading (not waited on): 23 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke), 6 in_progress (Test Core 1, 3, 5 and 6 of 6; Type Check · workspace; Lint & Repo Gates), 0 failed. The in-progress ones are gate verdicts still owed; nothing below re-runs them.

① Derived judgments

Commit 1 (0bd0e6d4f7, @objectstack/metadata) — right.

  • queryHistory lowers only in driver mode: where = this.engine ? historyFilter : lowerFilterCondition(historyFilter, HISTORY_FILTER_LOWERING); _find / _count route to engine.find / engine.count in engine mode, whose where seam lowers once. Not lowered twice — and item 9 makes a second pass a no-op anyway.
  • The reader (hasOwnProperty on SysMetadataHistoryObject.fields and type === 'datetime') is the engine's typed branch verbatim, and SqlDriver fills datetimeFields by type === 'datetime' (sql-driver.ts). recorded_at is Field.datetime in metadata-core, and ensureHistorySchema syncs that object. Right.
  • Other direct temporal callers in packages/metadata non-test source: utils/history-cleanup.ts recorded_at: { $lt: cutoffISO } (twice) is the only one. lowerBounds in filter-lowering.ts acts on $lte / $between alone, so $lt an instant is untouched. Claim true.
  • Accept set: unchanged (the same queryHistory options). Public surface: none (HISTORY_FILTER_LOWERING is module-private). The pin imports driver-sqlite-wasm, already a devDependency of metadata; §B spies on the driver's find / count and is driver-agnostic.

Commit 2 (5317b5aa22, @objectstack/objectql) — right.

  • The code change is one line, if (fields === null || typeof fields !== 'object') return {};; the typed return { isDatetimeColumn } after it is byte-identical. lowerFilterCondition with no isDatetimeColumn is the type-blind reading (filter-lowering.ts skips the reader guard when it is absent). That is item 7's second half applied to the population the old branch read as "no datetime column".
  • Reach: every position that calls declaredDatetimeLowering — find, findOne, count, update, delete, aggregate's where and aggregations[i].filter, the judge, and the nested-relation reads. having keeps its own aggregated-row reader, untouched. The changeset's verb list is right.
  • Accept set: unchanged — the lowering never refuses, and the judge gains no verdict (pinned). The one answer that moves on SqlDriver (unregistered object, non-datetime column holding ISO text, bare-day $lte: none → the whole day) is a widening and is exactly item 7's type-blind reading; datetime / date columns do not move because F1's copy is idempotent on lowered input (item 9). The changeset states it truly. No public surface change.

Commit 3 (e15606bae2, @objectstack/driver-memory) — right on the deletion, the routing and the item-5 / item-7 narrowing; one path unnamed (see ②).

  • All four sites are gone; memory-driver.ts no longer imports nextUtcCalendarDay / isUnboundedAbove and keeps no guard. The only nextUtcCalendarDay left in the package is memory-analytics.ts's dateRange window arm (item 8, kept on purpose by refactor(driver-memory): retire the reference matcher and the cube face's own whole-day bound (#5930 step 4, group 1: F4 + F5; F3 stopped) #20895). No non-test source in objectql or spec/data emits the { type: 'comparison' } AST node, so its two arms were direct-caller only.
  • The 43 routed tests, verified from the diff: in the six suites 0 changed lines contain expect( / toEqual / toBe / toHaveLength, no it( or describe( was removed, and every edit is an import, a shared-fixture hoist, or a seamed(...) / lowered(...) wrapper whose reader is the fixture's own declared field map. No assertion weakened, no expected value edited to fit. The count 43 is the dev's run; the routed sites are consistent with it.
  • The (b) narrowing on memory (registered object: declared text holding ISO text, and an undeclared column — r1,r2,r3 → none) is what the typed seam leaves alone and what SqlDriver answers: item 7's scope, and item 5's direct-caller rule for the AST spelling. Unregistered objects do not narrow on memory because commit 2 now lowers them at the seam. Both are pinned (memory-driver-20822-comparison-as-written.test.ts §B / §C), and the pin's cell layout matches the dev's per-site ablation counts (4 / 2 / 1 / 1).
  • Accept set: unchanged (malformedBetweenError and the shape gate stay; nothing new is refused or admitted). Public surface: InMemoryDriver's exports are unchanged. The changeset gives a direct find() caller the one-line way back — lower with lowerFilterCondition first — and names the subpath (@objectstack/spec/data) in the same file.

② Semver level

.changeset/20822-f3-route-then-delete.md: patch for @objectstack/metadata, @objectstack/objectql, @objectstack/driver-memory; Clause-②: no, also the PR body's second line. All three are released (17.5.0, not private). Right. Neither answer move changes what an author can write (no spec key, option or operator is added, removed or refused) or what a public export accepts (no export added or removed; the metadata and objectql changes are module-private; driver-memory drops two imports, not exports). The commit-2 widening and the commit-3 narrowing are answer changes ruled by ADR-0053 D-D1 items 5 and 7, not accept-set arms, so no (widening) / (narrowing) arm applies and patch is the level. Check Changeset is green.

Review faces, sentence by sentence:

  • PR body: "three ordered commits on main at 4d0b9cd542" true; "13 files, +662 / -102" true; the deletion list true; "nextUtcCalendarDay / isUnboundedAbove are no longer imported by memory-driver.ts" true; "No driver-local guard is kept" true; "The only change is if (fields === null …) return {};" true for code (docblocks moved too); "0 expect( lines changed" true; "Engine mode is untouched" true; "having … untouched" true; "The driver's own syncSchema temporal index is not consulted by any seam" true. The H1 to H4 rows are consistent with the diff. The measured-rows tables are the dev's runs, not re-run here; the pins' structure agrees with them.
  • Changeset, metadata bullet: every sentence true. objectql bullet: every sentence true. driver-memory bullet: the deletion, the two convergences and the item-5 sentence true. "Every seam hands it the lowered filter" is true of the mechanism and false of the effect on one seam: the RLS compile seam runs lowerFilterCondition with rlsLowering(fieldGuard), whose reader answers "no column" when the guard is absent (getObjectFieldNames returned null — a schema not loadable, which its own comment says can be a boot-time state), so a using policy's bare-day upper bound arrives at this driver unlowered. The security middleware composes that filter into ast.where after the engine's where seam has run (item 5's "composed onto the query after its seam"), so nothing lowers it later. On main this driver widened it; on this head it compiles it as written. That path is narrower than main on InMemoryDriver, and the changeset does not name it — the FAIL item below. (The check output reaches matchesFilterCondition, whose own copy F7 still holds, so it does not move here.)

③ Boundary flags

  • Open question 5918327806 / seat answer 5918373748 (A: carry the rlsLowering twin into group 2). The carry is a routing choice and is fine as one: item 7 does want that seam type-blind, and group 2 is where a SQL driver first meets it. What carrying leaves in THIS PR: one path worse than main on memory (above), disclosed in the PR body's Acceptance notes but not in the changeset that ships as CHANGELOG. FAIL — the remedy is one clause in the driver-memory bullet, for example: "A filter a policy composes after the engine's seam, on an object whose RLS field guard cannot be resolved, reaches this driver as written too until #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 makes that seam type-blind." Landing the one-line twin here with its pin is the other acceptable fix. Nothing else on this head needs to change.
  • Census ledger / the memory half of H1's pin. Answered by the diff: the metadata pin imports driver-sqlite-wasm (already a devDependency), never driver-memory, so the census gate (RULED_CEILING) is not touched; §B pins the where every driver receives and goes red when the loader stops lowering; driver-memory's own pin holds how it answers the lowered and the unlowered filter. Not a gap.
  • Two --force-with-lease re-stacks before the PR opened. The branch is claude/issue-*, its three commits carry one author / committer identity, no PR (so no reviewer or approval) existed yet, and the result is linear on 4d0b9cd542 with no merge commit. Conditions ② and ⑤ are the dev's statement and cannot be read from the tree; nothing contradicts them. Not a defect.
  • check:query-options-erasure red then fixed (236). The new pin carries no as any, and the six routed suites have the same as any count as on main. The gate's own verdict is inside Lint & Repo Gates, in progress at reading. Answered from the diff.
  • H1 counterfactual: the mutate-leg preflight line cut from the captured tail. A reporting gap, not a diff gap: the in-tree evidence is §B, driver-agnostic, which the dev ablated red (2 of 8). Noted; no change owed.
  • Dispatch H2 as written ("SqlDriver's unregistered answer does not move yet") was falsified for non-datetime columns. The dev reported it; it is the commit-2 widening judged right above, and the dispatch's stop clause ("beyond item 7's reading") did not fire because the move is item 7's reading. Right.

Implemented-by: claude/issue-20822-g1b-f3-route-then-delete
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: FAIL

FAIL items:

  1. The changeset does not name the one path this head narrows against main that neither of its two declared convergences covers: an RLS using policy's bare-day upper bound on an object whose field guard is absent, composed after the engine's seam, reaching InMemoryDriver as written (the carried rlsLowering twin). One clause in the driver-memory bullet, or the one-line twin with its pin, clears it.

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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 86ccdc099e3464076c7a90d6d9ca177e05b857f1
Local-runs: none

Delta review after record 5919688563 (FAIL 1 on e15606bae2). Head confirmed on the PR at the moment of reading (open, draft, base main, head repo is the base repo, 4 commits). Commit 4 86ccdc099e has the single parent e15606bae2; the chain from the merge base 4d0b9cd542 is still linear, no merge commit. Delta e15606bae2..86ccdc099e: exactly one file, .changeset/20822-f3-route-then-delete.md, one line replaced (the @objectstack/driver-memory bullet); no code, no test, no governed path. Net diff against the merge base: 13 files, +662 / -102, the same set and counts the PR reports and the API returns at this head.

Check-runs on this head at the moment of reading (not waited on): 38 runs, 18 success, 5 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke, one Auto Label, one Check PR Size), 15 in_progress (Build Core; Dogfood Regression Gate 1 to 3 of 3; Lint and Repo Gates; Temporal Conformance; Test Core 1 to 6 of 6; Type Check consumer gates, debt ledger and workspace), 0 failed. Check Changeset and Governed Surface Queue Guard are success. Some names appear twice (two suites on the head); no pair disagrees. The in-progress ones are gate verdicts still owed; nothing here re-runs them.

① Derived judgments

The corrected driver-memory bullet, sentence by sentence against the head's code — every path stated truly.

  • Engine-seamed read ("A read through the engine hands it a where the engine's seam has already lowered, so on that path a declared datetime column keeps the whole named day, and a declared date column answers as before"): true. engine.ts runs resolveWhereTokens(... declaredDatetimeLowering(schema), 'where', ...) before executeWithMiddleware on find (:11674 then :11676), findOne (:11947 then :11949) and aggregate (:17159 then :17197), and withResolvedWhere before the middleware on bulk update / delete (:13570, :16239). With a field map the reader types datetime only, so a date column is left as written (the measured table's d row is unchanged across all three columns); without one the seam is type-blind (commit 2). "On that path" is the right scope, since the next sentence names the path it does not cover.
  • The RLS using filter ("not lowered by the engine's seam: the security middleware ANDs it into the query's where after that seam has run, and only the RLS compile seam lowers it, rewriting just the columns its field guard declares datetime"): true. security-plugin.ts composes extra into opCtx.ast.where inside its middleware (:3766, { $and: [opCtx.ast.where, ...extra] }), which the engine enters only after :11674 has lowered the caller's where; the executor then hands opCtx.ast to driver.find (:11690) with no further lowering, and beforeFind hooks lower nothing. extra comes from computeLayeredRlsFilter, whose Layer 1 is rlsCompiler.compileFilter(compilable, context, 'using', guard) (:7269 to :7274); rls-compiler.ts lowers every kept policy filter through judgeCompiledComparands(outcome.filter, rlsLowering(fieldGuard)) (:690) which calls the shared lowerFilterCondition (:320); rlsLowering (:342 to :345) reads fieldGuard?.datetime, a set loadObjectFieldNames fills from the same declaration by type === 'datetime' (:8961, :8968). The delegator's RLS under ADR-0090 D10 goes through the same compile, and the controlled-by-parent filter carries no bound, so "only the RLS compile seam" holds.
  • The two (b) convergences: the sentence is byte-identical to the one 5919688563 judged true; carried over.
  • The one no-guard narrowing and when it ends ("an RLS using policy with a bare-day upper bound, on an object whose declared fields the security plugin cannot resolve, is compiled with no field guard, so the RLS compile seam reads no column as datetime and the bound reaches this driver as written, where this driver used to widen it to the whole day; that holds until #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 makes the RLS compile seam type-blind when it has no guard"): true, each clause. getObjectFieldNames (:8882) returns null when neither ql.getSchema nor metadata.get('object', ...) yields a field map (:8916 to :8919; its own comment: a null may mean the schema is not registered yet at boot), and :7272 then passes undefined as the guard. rlsLowering(undefined) returns an isDatetimeColumn reader that answers false for every column, and filter-lowering.ts lowerBounds returns the spec untouched when a reader is present and answers false (:290, if (options.isDatetimeColumn && !options.isDatetimeColumn(field)) return none;) — the opposite of the absent reader, which is type-blind. So the bound arrives at InMemoryDriver as written. At the merge base memory-driver.ts widened it (the $lte and $between arms at :1417 to :1418 and :1492 to :1493, the AST arms at :1742 to :1743 and :1759 to :1760, all calling nextUtcCalendarDay); at this head the file has no such site and no such import. The end named is the seat's answer 5918373748 (A): group 2 carries the one-line twin, rlsLowering returning {} for an absent guard, with a pin. The sentence says exactly that and no more.
  • The item-5 direct-caller sentence: byte-identical to the one 5919688563 judged true; carried over.
  • The deleted sentence ("Every seam hands it the lowered filter ...") was the FAIL 1 sentence; it is gone and nothing else in the bullet restates it.

The check claim and the rest of the review faces.

  • PR body, Acceptance notes: "A check clause reaches matchesFilterCondition, not this driver, so it does not move here" — true. computeWriteCheckFilter compiles with the 'check' clause (:7530 to :7535, the same guard shape) and the result is judged in-process against the post-image, checkParts.every(...) over matchesFilterCondition(image, f, checkFieldOptions) (:3226); no driver receives it. The changeset names the using filter only, which is the one that reaches the driver; the two texts agree. The earlier check in that note (the one 5919798956 flagged) is gone from the live body.
  • PR body, commits table row 4 ("names the RLS no-guard path ... no longer says that every seam hands the driver a lowered filter. Changeset text only"): true of the delta diff.
  • PR body, "13 files against 4d0b9cd542 (+662 / -102 at e15606bae2; commit 4 changes one changeset line)": true, and the counts are the same at 86ccdc099e — the changeset is new against the merge base, so rewriting one line in it moves no count.
  • PR body, "the changeset's driver-memory bullet names this path (86ccdc099e, after contract review 5919688563), and the seat's answer 5918373748 (A) carries the rlsLowering twin into group 2": true; matches the seat's answer as read.
  • Changeset title line ("applied at the seams only") and the metadata and objectql bullets: unchanged text, unchanged truth; the RLS compile seam is one of the seams the title names.
  • One sentence became ambiguous, not false: the PR body still opens with "It is one PR in three ordered commits on main at 4d0b9cd542" above a four-row table. The three are the answer-B steps and row 4 says it is text only, so the reader is not misled about the code; noted in ③, no change owed for the verdict.

② Semver level

Unchanged from what 5919688563 judged right, and re-read at this head: '@objectstack/metadata': patch, '@objectstack/objectql': patch, '@objectstack/driver-memory': patch; Clause-②: no in the changeset (line 9, outside the delta hunk) and as the PR body's second line. The delta is changeset prose: it adds, removes and refuses nothing an author can write and touches no public export, so no (widening) / (narrowing) arm arises from it. Check Changeset is success on this head. Right.

③ Boundary flags

  • Scope of the delta. One file, one line, the FAIL 1 remedy taken as the changeset clause (the seat's A carries the code twin into group 2). Every other item of 5919688563 carries over unchanged: commits 1 to 3 judged right (the loader seam, the type-blind absent-map branch, the four deletions and the 43 routed tests with no assertion edited), the accept set and public surface unchanged, the census-ledger answer, the two --force-with-lease re-stacks, the check:query-options-erasure reading, the H1 preflight reporting gap, and the H2 falsification judged as item 7's reading. None of them is touched by this delta and none is re-judged here.
  • The PR body edit. It is the seat's own write (the delta report 5919798956 named the edit as owed to the seat, the dev did not PATCH); the head did not move with it and the API reports the same head, file count and line counts. Nothing to ask about, nothing to correct.
  • "Three ordered commits" over a four-row table. Ambiguous as noted in ①; a one-word body edit ("three ordered code commits and one text commit") would close it. Not a contract defect: the body is a review face, not text that ships.
  • A stale doc comment in plugin-security, outside this card. rls-compiler.ts RlsFieldGuard.datetime (:131 to :135) still says an absent guard leaves "the rule to each face's own copy, as it was, never applied type-blind to a column no driver widens" — after this head InMemoryDriver has no copy, so that sentence is now stale for one driver. It is code text in a file this card does not touch, and group 2's claim names rls-compiler.ts's absent-guard branch as a declared surface (5918373748), where the comment is corrected with the twin. Noted; no change owed here.
  • Check-runs. All required contexts but Governed Surface Queue Guard were in_progress at reading; their conclusions are the gate verdicts, recorded as read, not waited on. The delta report's 19 changeset-derived gate commands at this head are the dev's run, not re-run here.

Implemented-by: claude/issue-20822-g1b-f3-route-then-delete
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants