test(core,runtime): authorization ObjectQL doubles enforce limit - #11521
Merged
Conversation
The in-memory `ql` doubles on the authorization-resolution path matched `where` and handed back every matched row, dropping `opts.limit`. A double that ignores the bound cannot tell a read bounded at 200 from the same read bounded at 1000, or from one carrying no bound at all — so raising a limit, lowering it, or folding two reads that carry different ones was green by construction, and the production symptom is a silently truncated result set rather than an error. Nine doubles across four files now enforce the bound the way the real read path does: AFTER the filter, and by PRESENCE rather than truthiness so `limit: 0` returns nothing rather than the whole table — the door `driver-memory` already carries for itself as `query.limit !== undefined`. Four new cases pin the instrument itself, so the blindness cannot come back unnoticed and deleting the bound fails something. They include the worked example from issue #10978: two `sys_member` reads at 200 and 1000, where folding them truncates `org_user_ids`, an RLS input. Measurement over the whole population, reported on the card: 49 limit-blind query-honouring doubles across 43 files, all 49 reached at runtime and 44 handed a real bound (values 1 … 10000). Teaching all 49 to honour the bound broke 0 of 1062 tests — the class was unobservable, not wrong. No shipped behaviour changes; test instruments only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. |
This was referenced Aug 24, 2026
os-steve
marked this pull request as ready for review
August 24, 2026 02:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #10978
Part of, not a closing keyword — the dispatch asked for a closingkeyword, but the census below changed the shape of the work: this PR converts 9 of the
49 limit-blind doubles. Closing the card would retire the remaining 40 silently. The
card should stay open and be closed by hand once the PM has triaged the remainder.
What the card asked, and what the census found
The card proposed "teach the shared doubles to honour
limit(a one-lineslice)". Thedispatch flagged the word shared as an unmeasured guess. It is: there is no shared
helper. The doubles are N hand-rolled copies.
Measured on this tree (
919f3f6e):findimplementations in test fileswhere(query-honouring doubles)limitOnly one exported helper exists anywhere —
makeRecordingQl, the probe #10825 had tobuild for itself, which is precisely the rediscovery cost the card is about.
The measurement — "let whatever breaks be the measurement"
Two instrumented runs over the 43 files, each a trap-guarded mutation restored on every
exit path, each with the mutation proven on disk before the run was believed.
Run 1 — receipts (behaviour-neutral). Records what each double is actually handed.
Without this, "nothing broke" is uninterpretable: it cannot tell no test was vacuous
from no limit ever arrives.
1, 2, 3, 4, 5, 6, 7, 10, 12, 20, 40, 50, 100, 200, 500, 1000, 5000, 10000.Run 2 — all 49 doubles bounded. Every one taught to honour the bound at once:
So the answer the card said "may well be zero today" is zero. The class was
unobservable, not wrong — no test in the tree is currently vacuous because of
limit-blindness, and nothing here is a suite being repaired.
What actually changed
Nine doubles across four files — the authorization-resolution seam, where the card's
worked example lives and where a dropped bound is a privilege bug rather than a cosmetic
one (
org_user_idsis an RLS input):packages/core/src/security/resolve-authz-context.test.ts(2 doubles)packages/core/src/security/admin-standing-surface.test.tspackages/core/src/security/api-key.test.tspackages/runtime/src/security/resolve-execution-context.test.ts(5 doubles)Semantics were taken from the real read path, not invented —
driver-memoryappliesfilter, sort, offset, then limit:
whereexcludes:a double that is silently wrong rather than merely unbounded.
limit: 0means "return no records" and0is falsy,so
opts.limit ? …answers a request for nothing with the whole table. That is ameasured door in this repo, which
driver-memorycarries asquery.limit !== undefined.matches production here rather than diverging from it.
Four cases pin the instrument itself, including the card's worked example (two
sys_memberreads at 200 and 1000). Without them the bound is unverified and deleting it fails nothing.
Reverse verification (predicted direction stated first: red). Ablating the bound to the
identity, from the committed state, with the anchor proven present before (1) and absent
after (0): 4 failed, 63 passed — exactly the four instrument cases, nothing else. No
rebuild is involved and none is owed: the ablated subject is the test file's own file-local
function, which vitest resolves from source, not through a package
exportsintodist/.What is deliberately NOT here
check:where-matcher-family gate. Ruled out for this round: [finding] The shared workspace-enumerator consolidation is blocked on dispatch-gates following first-party imports — today it would DELETE the population declarations it is meant to centralise #11190 is in flightmeasuring
dispatch-gates' discovered-family universe, and adding a family concurrentlywould corrupt its baseline. The measurement above is also the input that decision wants —
0 vacuous tests, but 44 live limit-carrying doubles.
resolveUserAuthzGrantsand every read's actual limit areuntouched. Test instruments only, so no changeset (
skip-changeset).once — the maximum-conflict shape against every parallel dev — for zero broken tests and
no shipped-behaviour risk. Left on the open card for the PM to schedule.
Verification
All at the final commit
919f3f6e; each gate quoted by its own printed verdict line, withexit codes captured before any pipe.
pnpm --filter @objectstack/core --filter @objectstack/runtime test— core 936 passed(936), runtime 2724 passed (2724), 0 failures.
pnpm --filter @objectstack/runtime typecheck— clean (script name echoed, so not azero-match).
@objectstack/coredeclares notypecheckscript; it is one of the 13DEBT-ledger packages, confirmed by the gate rather than assumed.
check:where-matcher— "288 matcher(s) discovered, 288 answer the combinator batterycorrectly or refuse it loudly (176 refuse)."
check:engine-double-contract— "OK — 390 pinned, 133 in the DEBT ledger, 2 exempt."check:type-check-debt— "32 ledger entr(ies) re-measured in 515.3s, 1897 raw tscerror(s) total, none above its recorded number." (First attempt refused with 27 deps
unbuilt; that refusal was treated as NOT MEASURED, the closure was built, and it was
re-run.)
check:cross-package-test-inputs,check:kernel-hook-pairs,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check:query-options-erasure,check:type-check-coverage,check:nul-bytes,check-ci-filter-parity,check-plugin-teardown-shape,check-affected-docs— all OK.Declared narrowing. The two instrumented runs covered the 43 files carrying the doubles
rather than all ten packages' suites. The narrowing is sound by construction, not by
sampling: every edit is inside a test file, and each patched
findis a file-localfunction that nothing imports, so no behaviour outside its own file can change. The two
packages this PR actually edits were then run in full anyway (3660 tests). The gate set was
derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, whichcomputes its own change set (4 paths vs merge base
63da5867e) and confirmed the repoassertion against this checkout's remote.
Generated by Claude Code