Uh oh!
There was an error while loading. Please reload this page.
fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322
Conversation
…ative-SQL merge sites The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty family to a constant-TRUE predicate — the echo rendered, and the native strategy actually executed, a whole-table WHERE for a scope execute() refused. Call the same guard at both merge sites, after the compiler so its own #13571 refusals keep their messages; compileScopedFilterToSql itself (the ruled #13571 residue included) is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
📓 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 — 8 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 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57
|
os-sales
commented
Sep 2, 2026
Landing provenance ( Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13926
What this closes
#13640 guarded the ObjectQL ENGINE merges (
withReadScope,resolveFkAttr) withassertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consumecompileScopedFilterToSql. That left one read scope with two answers on one strategy:ObjectQLStrategy.execute()REFUSED the$not-over-$in: []family whileObjectQLStrategy.generateSql()— the/analytics/sqlecho — still COMPILED it to a predicate admitting every row, andNativeSQLStrategy.applyReadScopebuilt a real, executed WHERE from the same compiler.The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining
getReadScopemerge sites. Zero compiler change (see "What did NOT move").Measured, not read from the compiler (the card asked)
The card flagged its row consequence as derived from the emitted predicate.
read-scope-vacancy-three-faces.test.tsmeasures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:{ owner: { $nin: [] } }{ $not: { owner: { $in: [] } } }{ $not: { owner: [] } }{ $not: { owner: { $in: [], $ne: 'u_other' } } }{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }{ $not: { $not: { owner: { $nin: [] } } } }Post-fix, all six spellings refuse on all three faces in the module's one envelope —
READ_SCOPE_COMPILE_FAILED/ 500 — asserted bycodeplusstatusin every refusal case, never a bare throw assertion.The PM's question: latent execution leak, or echo truthfulness?
Both, split by route.
NativeSQLStrategy.execute()runsgenerateSql()'s output throughctx.executeRawSql— real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repogetReadScopeproducer (the spec contract inpackages/spec/src/contracts/analytics-service.tsexists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only:execute()refuses atwithReadScopebefore the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.Also verified while counting doors: the third
getReadScopereader in the package,NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), andanalytics-service.ts'scallCtxwrapper only pre-resolves scopes into the sameStrategyContextthe strategies consume. The card's count of exactly two unguarded merge sites holds.Where the guard stands — AFTER the compiler, deliberately
At both new sites the guard runs after
compileScopedFilterToSqlreturns. The shapes the compiler already refuses ($nin: []at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes —read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.What did NOT move (controls)
compileScopedFilterToSqlitself: zero code diff (doc header updated to record the new call sites). The read-scope-sql's emptied-membership folds are polarity-dependent at the lowering site itself:$in: []folds to1 = 0one arm from$not, and$nin: []folds to1 = 1(constant TRUE) #13571 verdict's residue —$notover$in: []still compiling to constant TRUE at that lowering — stays exactly where the verdict left it; the polarity-aware compiler design remains the ruled-first follow-up. The new suite pins the compiler still compiling all three leaking spellings, labelled IMMOBILITY CONTROLS, not contracts (fix(analytics): refuse a non-binding read scope before the ObjectQL engine #13924's precedent). A direct out-of-package consumer of the public export still receives the compiled constant.$in: []zero-rows reduction and the fix(plugin-security): make the RLS emptied-membership deny guard polarity-aware #13570 RLS composite (emptied membership beside an own-rows grant): asserted unchanged on ALL THREE faces (the own row still flows; the$andcomposite still denies with zero rows and no refusal).objectql-read-scope-vacancy-refusal.test.tswere amended to name the new call sites; its assertions are byte-identical).Ablation (direction predicted before the run)
Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms
srcdirectly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code:git checkout HEAD --with absolute paths under an EXIT/INT/TERM trap, thengit diff HEADempty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.Verification (all on c8ac3e7, the head of this branch)
pnpm --filter @objectstack/service-analytics test— Test Files 87 passed (87), Tests 1871 passed (1871)pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles— zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the editsnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines;check-test-completenessexits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor redpnpm lint(repo-wideeslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needednode scripts/check-nul-bytes.mjsOK over 7815 filespatchfor@objectstack/service-analytics— no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Generated by Claude Code