fix(plugin-security,driver-sql,driver-turso): lower type-blind at the RLS seam without a guard, then delete the F1/F2 whole-day and NOT-rewrite copies (#5930 step 4, group 2) - #20988
Conversation
… guard carries no type set ADR-0053 D-D1 item 7, as amended: a seam that cannot read the declared field type applies the whole-day rewrite type-blind. rlsLowering read an absent guard (the security plugin could not resolve the object's fields) or a guard without a datetime set as "no column is datetime", which left a bare-day upper bound to the drivers' own copies of the rule. Those copies are deleted in the next commits, and a using filter is composed into the query after the engine's where seam, so nothing else lowers it. rlsLowering now hands the lowering no reader in that case. The typed branch is unchanged. The existing pin "a guard with no types reads no column as datetime" is rewritten to the new reading, and a new pin file covers the no-guard compile for both clauses and a using policy through SecurityPlugin.getReadFilter handed to SqlDriver.find. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…seams' lowering answers the bound ADR-0053 D-D1 items 5 and 9, as amended. The shared lowering (lowerFilterCondition, @objectstack/spec/data) widens a bare-day upper bound once, at the seams, so every seamed read hands these drivers $lt the next day and no $between on a declared datetime. The faces' own copies were idempotent on that input and are deleted: - driver-sql F1: calendarDayExclusiveUpperBound, calendarDayUpperBoundRewrite and calendarDayBetweenRewrite (protected methods on the exported class) and their two call blocks in the emitter, on the plain and legacy-normalised column paths; - driver-turso F2: toRemoteFilter's whole-day arms (toRemoteUpperBound and the $lte / $between-max rewrites). The two-bound $between split the remote transport needs is kept, structural only. The transport's $between refusal text no longer credits the split with the rule. A caller that passes no seam gets the comparison it wrote (item 5). The direct-call temporal suites (driver-sql, driver-sqlite-wasm, turso local and remote) are routed through lowerFilterCondition with each fixture's declared-datetime reader; the expected rows are unchanged. The probe matrix that called the deleted methods now pins the lowered bound's physical form per dialect. New pins cover one cell per deleted branch on both drivers, with the lowered control. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
… the seams' lowering totalises the operand ADR-0053 D-D1 items 5 and 9, as amended. The shared lowering's rule 3 makes every leaf of a $not operand total in the direction its operator answers for a row with no value (#5146), once, at the seams. The faces' own copies were idempotent on that input and are deleted: - driver-sql F1: nullSafeNegationOperand and its polarity tables (nullValueSatisfiesOperator, operatorIsNullTotal, nullGuardForFieldSpec); the $not branch negates the operand it is handed; - driver-turso F2: RemoteTransport's copy of the same (the third hand copy of the ruling). applyNullSafeNegative / nullSafeNegative, the emitters' own NULL-safe spelling of $ne, $nin and $notContains, are not copies of the $not rewrite and stay. A caller that passes no seam gets SQL's three-valued NOT (item 5); both Turso faces still agree. A refusal raised inside a $not now resolves against the caller's own provenance marks, since the operand is no longer a rewritten copy. Not deleted (H3 stop): assertDefinedComparands, the undefined-comparand refusal, in both faces. Deleting driver-sql's moves a direct caller from INVALID_FILTER/400 to an answer on five positions; the transport's is held to driver-sql's wording by the local/remote parity suite. The $not direct-call suites are routed through lowerFilterCondition, including two service-analytics find() faces that stand for the engine; refusal pins stay direct calls. New pins cover the direct $not on both drivers, with the lowered control. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…gin-security patch for the deleted face copies Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 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 84631a24bdb4300f780eb9213369aa004f73fb58 && git checkout 84631a24bdb4300f780eb9213369aa004f73fb58
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2f2fa11d756f665a4c06160480c1dce15b9d67a4 0e8378cf54e4a7809cd6388774a227e6b25a2a07 && git checkout -B drift-repro 2f2fa11d756f665a4c06160480c1dce15b9d67a4 && git merge --no-ff 0e8378cf54e4a7809cd6388774a227e6b25a2a07
node scripts/docs-audit/affected-docs.mjs --json 2f2fa11d756f665a4c06160480c1dce15b9d67a4
|
…ed methods, registered in the ADR-0087 ledger The changeset grades the removal of SqlDriver's calendarDayExclusiveUpperBound, calendarDayUpperBoundRewrite and calendarDayBetweenRewrite as a narrowing: driver-sql moves to minor under the launch-window convention, with a BREAKING paragraph and its FROM -> TO line to lowerFilterCondition from @objectstack/spec/data. It states plainly which two sentences of the unreleased shared-lowering changeset it supersedes. The protocol-18 step of the ledger gains the semantic entry driver-sql-calendar-day-methods-removed, and the changeset carries the matching registered disposition. registry.ts is regenerated by gen:migration-registry. gen:spec-changes and gen:upgrade-guide were re-run and left their outputs byte-identical: both stop at the current protocol, 17. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…lowering in words, not by tracker number The printed guidance of every semantic entry must carry no tracker id: an author reads it in the terminal, and a number sends them to a page that can be deleted. The entry's reason cited the lowering and the removal by tracker number. It now names the shared filter lowering in @objectstack/spec/data (lowerFilterCondition) and says the driver's copy was deleted, with no number. registry.ts is regenerated by gen:migration-registry. gen:spec-changes and gen:upgrade-guide were re-run and left their outputs byte-identical: both stop at the current protocol, 17. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
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): 42 runs, 37 ① Derived judgmentsCommit 1 ( Commit 2 ( Commit 3 ( Routing, verified from the diff. Across the 38 modified suites every changed assertion wraps its filter in Two measured stops — both right under D4 (b) and the card's acceptance rule.
H4, spot-checked from Docs — the eight hand-written pages the drift check names, read on ② Semver level
The ledger entry Release text. The Supersedes paragraph quotes both now-false sentences of Review faces, sentence by sentence. Changeset: every sentence of the title, BREAKING, FROM → TO, Supersedes and the five bullets is true against the head's code (the ③ Boundary flags
Implemented-by: VERDICT: FAIL FAIL items:
Generated by Claude Code |
…lowering's, not the driver's The page said that on a datetime column "the driver compiles" a bare-day upper bound half-open. That was the copy this PR deletes from SqlDriver and TursoDriver. The page now says that the shared filter lowering (lowerFilterCondition from @objectstack/spec/data, ADR-0053 D-D1 as amended), run by the engine's where seam and the RLS compile seam, rewrites the bound half-open before any driver sees it. A seam that cannot read the declared types applies the rewrite to every column. A filter handed directly to SqlDriver or a driver built on it is compared as written. The page's other statements are unchanged. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta review on record 5923464846 ( Check-runs on this head at the moment of reading (not waited on): 39 runs, 22 ① Derived judgmentsThe new paragraph (page lines 569 to 581), sentence by sentence, against the code at this head.
The scoping of the direct-call sentence — right, and necessary. The dispatch's unscoped clause ("a filter handed to a driver directly is compared as written") would be false at this head: Other sentences on the page, read at this head for drift from the edit. None became false. Three are near the paragraph and were read closely: the section opener (lines 531 to 534, "the driver puts the comparand into the same canonical form the column is stored in ( Everything 5923464846 judged on commits 1 to 6 carries over unchanged, because the code is byte-identical: commit 1 ( ② Semver levelUnchanged from what 5923464846 judged right, and verified unchanged: A docs-only commit needs no changeset change — right. A changeset describes what a released package publishes (Post-Task Checklist step 3). ③ Boundary flags
FAIL item 1 of 5923464846 is cleared: the page no longer states the whole-day rule as the driver's own compile, and each sentence it states instead is true against the code at this head. Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #20822
Clause-②: no (narrowing)
#5930 step 4, group 2:
driver-sqlF1,driver-tursoremote F2, and theplugin-securityrlsLoweringabsent-guard twin (the seat's answer 5918373748, A). Ruled by 5902355785 (D4 (b)) and ADR-0053 D-D1 items 5, 7 and 9 as amended. The order follows group 1b's pattern: make the seam type-blind first, then delete. Seven commits onmainat5f6b63a6fd:e20e17e929fix(plugin-security)rlsLoweringhands the lowering no reader when the guard carries nodatetimeset (no guard, or a guard built without types). The RLS compile seam is now type-blind there (item 7). The typed branch is byte-identical. The pin "a guard with no types reads no column as datetime" is rewritten to the new reading. A new pin file covers the no-guard compile for both clauses, and ausingpolicy throughSecurityPlugin.getReadFilterhanded toSqlDriver.find.a9b1f79282refactor(driver-sql, driver-turso)calendarDayExclusiveUpperBound,calendarDayUpperBoundRewriteandcalendarDayBetweenRewrite, plus their two call blocks in the emitter. F2:toRemoteFilter's whole-day arms (toRemoteUpperBoundand the$lte/$between-max rewrites). The direct-call temporal suites are routed throughlowerFilterConditionwith each fixture's declared-datetime reader. New item-5 pins on both drivers.d5277f98e6refactor(driver-sql, driver-turso)$notcopies: F1nullSafeNegationOperandand its three polarity tables, and F2RemoteTransport's copy of the same. The$notdirect-call suites are routed, including twoservice-analyticsfind()faces that stand for the engine. Refusal pins stay direct calls. New pins on both drivers.4658e72939docs(changeset).changeset/20822-driver-sql-turso-copies.md, first draft.6ec47a8770docs(changeset, spec)driver-sqlminor,Clause-②: no (narrowing), a BREAKING paragraph with a FROM → TO line, both step-2 sentences superseded, and@objectstack/specpatchfor the ledger entry18.driver-sql-calendar-day-methods-removed.tswith the regeneratedregistry.ts. The other three packages staypatch.d8b8f9095afix(spec)migrate-meta-engine-guidancetest was red on commit 5 for this.0e8378cf54docs(objectql)content/docs/protocol/objectql/query-syntax.mdxstates the whole-day rule as the shared lowering's (the engine'swhereseam and the RLS compile seam), and a direct call toSqlDriveror a driver built on it as compared as written (contract review 5923464846, FAIL 1).49 files, +1501 / -977 against
5f6b63a6fdat0e8378cf54, under the 5000-line human-merge threshold. No governed path.The answers that move, named
All rows were measured on SQLite (better-sqlite3
:memory:, and the libsql stub for Turso remote).wherepositions,aggregations[i].filter, RLS compile seam with a typed guardusing/checkcompiled with no guard, or with a guard without adatetimesetrecord.signed_onbefore-or-on'2026-01-05'{ signed_on: { $lte: '2026-01-05' } }, as written{ signed_on: { $lt: '2026-01-06' } }; SQLite keeps every row of the day (pin §B);InMemoryDriverkeeps the whole day tooSqlDriver/SqliteWasmDriver/TursoDrivercall (both faces),datetimecolumn$ltea bare day$betweenwith a bare-day max$lte '9999-12-31'$notover=, an ordering operator,$in,$contains, a$orNOT: that row is not returned$notover$ne/$nin/$notContains, and$ne/$nin/$notContainswithout a$notapplyNullSafeNegative/nullSafeNegative, #5298); they are not copies of the$notrewriteRemoteTransportcall with an author-marked filter$notoperand (for example$or: [null])$notNothing moved beyond item 5 or item 7, so no site stopped on that clause.
Two stops, measured (H3, and the
$betweensplit)assertDefinedComparandsis kept in both faces.undefinedcomparand onfind,findOne,count,update,delete,aggregations[i].filter,havingand$notpositions (engine-comparand-type-door.test.ts,engine-aggregate-filter.test.ts,engine-aggregate-having-comparand-shape.test.ts, all green). The RLS compile seam refuses throughnormalizeFilterComparandTypesinjudgeCompiledComparands.sql-driver-undefined-comparand-refusal.test.ts, 22 positions):INVALID_FILTER/ 400 to an answer:$contains,$notContains,$startsWithand$endsWithwithundefined, andundefinedbeside the TRUE identity{}. The LIKE family bindsString(undefined).DATABASE_ERROR.INVALID_FILTER/ 400, throughserializeComparand's unbindable-comparand refusal. No answer moved. Butturso-local-remote-null-parity.test.ts, which the dispatch names as a suite to keep green, holds both faces to driver-sql's one sentence (drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050,{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240): 13 parity cases (the drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 block), 14 refusal-seam rows (13undefinedComparandrows plus the closed-class enumeration) and 20undefined-refusal pins went red. F2's copy is kept with F1's. Its comment now says why.toRemoteFilterkeeps its structural$betweensplit. The remote transport has no$betweenarm, and it refuses an unsplit range. The typed lowering leaves a$betweenon a non-datetimecolumn whole. So removing the split moves seamed answers from rows to a refusal. Measured by ablation (S1): 6 red. They are the numeric$betweencontrol, twoField.time$betweenconformance cells,$notover a numeric$between, and the two$betweenitem-5 cells. Only the whole-day half of those arms is deleted.Hypotheses
75519e1c0a, the first base.sql-driver.tsis unchanged by the rebase onto5f6b63a6fd.NullGuard5064,nullValueSatisfiesOperator5077,operatorIsNullTotal5141,nullGuardForFieldSpec5180,nullSafeNegationOperand5265;$notbranch at 16943;assertDefinedComparandsis at 4707, called at 4929 and 16588;calendarDayExclusiveUpperBoundis at 15476,calendarDayUpperBoundRewriteat 15498 andcalendarDayBetweenRewriteat 15521, called at 17036 and 17053.toRemoteFilteris at 2567 (the dispatch said about 2566);$betweenarm is at 2610 and the$ltearm at 2638;toRemoteUpperBoundis at 2685, and it callscalendarDayUpperBoundRewriteat 2691;remote-transport.tshas its quartet at 379 to 614, called at 3116, andassertDefinedComparandsat 4364, called at 2961.$betweensplit is the exception above: it is structural, not a copy.src(driver.find/count/aggregate/updateMany/deleteMany/distinct,getDriverForObject,_find/_count):metadataDatabaseLoaderin driver mode builds equality filters only (baseFilter,nextEventSeq).queryHistoryis already lowered (group 1b), and its §A pin stays green on SQLite with F1 gone.metadatahistory-cleanupusesrecorded_at $ltan instant, andtype $nin.objectqlLifecycleService's archive pass useshot.find/cold.deleteMany, throughgetDriverForObject/datasource. Its filters are$ltan ISO instant, anorganization_idequality, and$or: [organization_id $nin, organization_id null].secret-reference-union/secret orphansuse{ type: 'datasource' }or nowhere.metadata-protocol's migrations run raw SQL.$lte, a$between, a$notor anundefinedcomparand.$ltan instant is never widened.$ninoutside a$notkeeps the emitter's own NULL-safe form.rlsLowering(undefined)and a guard withoutdatetimeboth return{}. The existing pin flipped as predicted. The new pin goes through ausingpolicy: SQLite keeps the whole day. The memory half is the filter shape (driver-agnostic) plusdriver-memory's own §C pin of a type-blind lowered filter. A plugin-security test that imports@objectstack/driver-memoryis an arrival outside the driver-memory census ledger.Temporal Conformance (live PG + MySQL)in CI. Remote Turso was measured against the libsql stub only.For the review
packages/specis touched for the ledger only: one semantic entry insrc/migrations/entries/semantic/and the regeneratedregistry.ts.spec-changesand the upgrade guide stay byte-identical, because the generators stop at protocol 17. It is a declared cross-lane surface (the seat's answer 5922490848).SqlDriver's published class surface shrinks.calendarDayExclusiveUpperBound,calendarDayUpperBoundRewriteandcalendarDayBetweenRewritewereprotectedmembers of the exported class, so they are declared indist/index.d.ts. Measured against the published.d.ts(tsc 6.0.3): a call gets TS2339, and anoverridere-declaration gets TS4113. A plain re-declaration still compiles, but the driver never calls it. The replacement islowerFilterConditionfrom@objectstack/spec/data. The changeset carries it as BREAKING (minor), with the ADR-0087 ledger entrydriver-sql-calendar-day-methods-removed.TursoDriverwas the one caller, and it is updated. The test probe subclass is rewritten.objectui/cloudat their pins: NOT MEASURED (no sibling checkout in this container).One sentence of this release's step-2 changeset is superseded.
.changeset/5930-shared-filter-lowering.mdsays "A guard without that set treats no column asdatetime" and "Each driver keeps its own copy of these rules". The new changeset has a Supersedes paragraph naming both sentences. The step-2 file is not edited here, because the foreign-changeset rule incheck-empty-changeset(finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR'sminorchangeset and every gate stayed green #17712) forbids it.Expected values changed in three places; all other edits wrap arguments.
rls-shared-lowering-seam.test.ts.$notrefusal-provenance flip inremote-transport-not-operator.test.ts(e).sql-driver-calendar-day-upper-bound.test.ts's probe matrix. It called the deleted methods. It now pins the lowered bound's physical form per dialect, with the same values as before:2026-07-29T00:00:00.000Z, and2026-07-29 00:00:00.000on MySQL. The calendar arithmetic and the scope rows are the lowering's (spec'sfilter-lowering.test.ts).Every other changed
expect(line wraps its filter inseamed(...), and its expected value is unchanged.Verification at head
4658e72939(driver code; unchanged since. The patch round's gates atd8b8f9095a: 94 derived, 94 run, every one exit 0, under a stale-tree warning for 5 filesmainchanged outside this diff)Suites, each with
vitest run --maxWorkers=2. Package baselines are from75519e1c0a.driver-sqldriver-tursodriver-sqlite-wasmplugin-securityservice-analyticsmetadataobjectql,--project localobjectql,--project reporestThe typechecks for
driver-sql,driver-turso,driver-sqlite-wasm,plugin-security(includingcheck:test-typecheck) andservice-analyticsall exit 0.driver-sql's tsc program lists all 214 of its test files.Ablations. Each restores the deleted copy on the committed head through
scripts/ablation-replace.mjs. Each is restored with blob equal to HEAD andgit diff HEADempty.$notrewrite$notrewrite$betweensplit removedA3 rebuilt
driver-sql, andablation-dist-preflightshowed the marker present indist/. The restore leg rebuilt it again, and--absentshowed the marker gone from all 6 built files with the tree clean.Gates.
node scripts/pm/dispatch-gates.mjs --commandsderives 67 families: 46 paths against merge base5f6b63a6f, with no stale-tree warning after the rebase.check:driver-conformance(OK, 50 covered cells, 0 DEBT),check:driver-memory-census,check:engine-double-contract,check:query-options-erasure,check:nul-bytes,check:dual-build-cjs-loadsandcheck:i18n. The last two first exited 3 (PREREQUISITE NOT MET) and were rerun after a full workspace build (72 tasks).dispatch-gates --ran: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.Lint (narrowed).
eslint --no-inline-config --format jsonover the 45 changed.tsfiles: 45 files, 0 errors, 0 warnings.--print-config.eslint.config.mjsnever enables type-aware linting (noparserOptions.project), so no untouched file's verdict can move.pnpm lintis CI's.Acceptance notes
driver-memory:objectqlhaving-filter.tsaround line 1365, andspecfilter.zod.tsaround lines 434, 734 and 750, still namecalendarDayUpperBoundRewrite/calendarDayBetweenRewrite.service-analyticsread-scope-shared-lowering-seam.test.tshas a case titled "RLS using, as written (a guard without types)". Its rows still hold, but the RLS seam no longer emits that shape.undefinedrefusal. The seat answered A (5922273550): it stays in both faces as a door, not a copy of the lowering's meaning, because without it 5 of F1's 22 direct-call positions move from 400 to an answer.applyNullSafeNegative(F1) andnullSafeNegative(F2) are not in the design's census quartet. They are the emitters' own NULL-safe spelling of$ne/$nin/$notContains, and they stay.Generated by Claude Code