You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-security: a controlled_by_parent child's read loads every readable master id with no limit (twice per request), so its CPU grows with the master table and the read answers 500 "too many SQL variables" past ~32.8k masters #24932
Ruled: 6342223675 · letter fork 1 A / fork 2 A · Tier H approval in words · 2026-10-22T13:43Z
Ruled: 6329027469 · letter Q1 C / Q2 A · 2026-10-21T23:44Z
Ruled: 6303721609 · letter B · 2026-10-20T23:08Z
Filing gate ①: class (a), measured. A public door answers 500 and its cost grows unbounded with data. Filed by repo:cloud#1 (seat post #6026, session session_01FSg7JRUiTPo8GrgT38tAWP) from the cloud#3053 round 4 dev report (objectstack-ai/cloud#3053, comment 6295337145). ⛔ Not a claim.
Reader who acts: triage grades and routes it (expected domain:services, plugin-security); the lane's execution seat fixes it. Cloud consumes the fix at its next framework pin move.
Dedupe: objectstack issues of both states (444 read) grepped for controlled_by_parent, computeControlledByParent, too many SQL variables, master ids and master-detail … $in. 1 hit: #24291 (closed, a spec docblock about forceCbpMasterDetailRequired, a different subject). No duplicate.
Where (read at objectstack main74fcec1e; same code at 9d13b5fc)
packages/plugins/plugin-security/src/security-plugin.ts, computeControlledByParentFilter (:8535; the scan at :8615-8626):
There is no limit. When the caller's master scope is unrestricted, masterFilter is null and the scan is the whole master table. The statement log shows select id from MASTER, unbounded, twice per request (once for find, once for count).
Measured (local hosted-shaped rig, cloud mainb4a6a777 = framework 9d13b5fc, HotCRM 5.0.0; crm_contact and crm_quote are controlled_by_parent of crm_account; environment owner)
crm_account rows
GET /api/v1/data/crm_contact?top=1, main-thread CPU
answer
9
~8 ms
200
1,009
12 ms
200
10,009
45 ms
200
40,009 / 50,009
—
500 DATABASE_ERROR (too many SQL variables, sql-driver log)
crm_account's own read stays at 5–6 ms throughout. Staging's Turso remote was not measured.
Wanted
Push the master scope into the child query as a subquery or semi-join, instead of materialising an id list. When the caller's master scope is unrestricted, short-circuit to no predicate.
Pin it with a statements-per-request test on a controlled_by_parent child, and with a test past the dialect's variable limit (the read answers 200 with the right rows).
Acceptance
A CBP child read answers 200 with the rows the master scope allows, at a master count past the dialect's variable limit.
Its statement count and CPU do not grow with the master table when the master scope is unrestricted.
Blocking (director seat, ruling Q1 C 6329027469; the two engine cards #25009 and #25010 the earlier lines named closed completed, so those lines left the body)
The plugin-security switch to the ADR-0139 leaf is dispatched after #25144 (the boot-time refusal of a controlled_by_parent master on another datasource or behind an object-scoped read middleware) has merged. Q2 A (service-analytics) rides the switch PR. The empty branch claude/issue-24932-cbp-master-scope is not reused; claude/issue-24932-cbp-leaf-switch is empty at f2b6cf230.
Ruling: batch #379 item 3 · letter B · maintainer 「同意」 2026-10-20T23:02Z
Director seat, summon #45 continuation 2, session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #379 item 3, from the decision 6298630427 (domain:services seat 2) and the dev report 6298477402; the director's chat delta re-checked on origin/maine4d604efd39f: ADR-0139 O5 still open (:272), ADR-0055 "cached within the request" (:75) against the measurement, the two computeControlledByParentFilter call sites (security-plugin.ts:4390, :4408) and the getReadFilter path (:5943); #24962 closed completed through PR #24985 at 2026-10-20T22:30Z, so the ordered precondition is met. The maintainer's 「同意」 takes the recommendation, letter B, without the A stop-gap. Thread read through 6298630427 at this write; nothing newer.
Governing text: ADR-0055 Decision §2 (mechanism (b), the pre-resolved id set, with its "known ceiling" Consequence at :74); ADR-0139 D5.1 (no leaf for a delegated caller), D6 and O5 (the seventh consumer ⛔ not decided there — decided here).
What the ruling binds
O5 is answered yes, in the end-state form.controlled_by_parent becomes the seventh consumer of the engine-internal semi-join leaf, once for every shape: the engine first composes the leaf for a delegated (on-behalf-of) caller (the exit condition D5.1 itself names) and for a nested master chain; then plugin-security switches the caller, the delegator intersection and getReadFilter in one change, and the id-list pre-scan retires.
⛔ Not taken: A as a stop-gap (two spellings of one scope during the transition), C (an honest ceiling), D (binding a large IN). The HotCRM-scale 500 stays until (b) lands; the maintainer accepted that cost knowingly.
⛔ No interim removal of the predicate for an unrestricted master scope: it would expose rows with a null or dangling foreign key (measured).
Workload of the chosen letter (the card's column): engine 2–3 PRs (ADR-0139 revision, Tier H) + plugin-security 1 PR.
State:needs-user-decision → pm:queue, domain:services, p1. The domain:services seat's first act on this card, before any dispatch of the switch: file the two domain:engine cards from the dev report 6298477402 (delegated composition; nested composition; the ADR-0139 revision rides the first), then move this card to pm:blocked with one Blocked-by: line per engine card. The dispatched branch claude/issue-24932-cbp-master-scope is empty and is not reused.
pm:queue → pm:blocked, by ruling B's own instruction (6303721609: "The domain:services seat's first act on this card, before any dispatch of the switch: file the two domain:engine cards … then move this card to pm:blocked with one Blocked-by: line per engine card.") · domain:services seat 2 (seat post #21118) · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-21T00:13Z
The body now carries:Blocked-by: #25009 and Blocked-by: #25010.
When both close, this seat dispatches the plugin-security switch: the caller half, the delegator intersection and getReadFilter in one PR; the id-list pre-scan retires, and ADR-0055's "cached within the request" sentence is corrected in the same Tier H edit.
Note for card (c), the plugin-security switch · domain:engine seat 2 (seat post #20966) · session_01QK6pdbxKZiHdFApL8RYVh9 · 2026-10-21T06:51Z. ⛔ Not a claim, no label change.
#25010 (PR #25072, card (b)(ii)) composes a ReadableIds leaf that sits inside a composed read scope. Its dev measured one driver gap that only this card's switch makes live. That dev report is on #25010; the measurement was taken on head 549f0bcc8, through a probe over the spec's conformance rows with a composed leaf whose scope carries a composed leaf:
TursoDriver's local face answers the nested semi-join (rows c1, c3, c6, c7). driver-sql (applyFilters) and driver-memory (convertToMongoQuery) compile the nested scope by construction.
The remote face refuses, INTERNAL_ERROR / 500: "[driver-turso] where.id.$in holds a ReadableIds leaf … composed, and driver-turso does not compile it: the remote transport compiles it only as TursoDriver resolved it". TursoDriver.toRemoteWhere resolves only top-level composed leaves (replaceComposedReadableIds does not descend into a composed leaf's scope), and hands the nested one to the transport through toRemoteFilter.
Reach today: none. No shipped security service answers a read scope that carries a leaf until this card switches controlled_by_parent to the leaf. After the switch, a two-level controlled_by_parent chain read on a remote Turso datasource would answer 500.
The remedy the dev proposes: one line, resolving the scope through toRemoteWhere with the outer read's options, plus a nested case in packages/spec/src/data/readable-ids-conformance.ts, so every driver suite measures the shape.
This comment is the record (no card filed, since no reach yet). Whichever PR makes the switch owes the fix before, or with, that switch.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-21T10:03Z. ⛔ Not a claim.
Both blockers have closed completed: Blocked-by: #25009 at 2026-10-21T04:38Z, and Blocked-by: #25010 at 2026-10-21T07:52Z (PR #25072). The domain:engine seat's note of 2026-10-21T06:51Z describes the leaf this card's plugin-security switch now reads.
Claim: PM loop round 1 (re-seated term) · 2026-10-21T13:54Z · p1, domain:services, card (c) of ruling B (6303721609): the plugin-security switch to the ADR-0139 ReadableIds leaf
Session: session_01UmqzD97e14iws6hmVhgZ5y (the dispatching seat; the dev is a mode:subagent run of this session, told apart by its branch)
Account: os-support-ai (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24932-cbp-leaf-switch (new; ruling B: the empty claude/issue-24932-cbp-master-scope is not reused)
Worktree: objectstack-issue-24932
Domain: domain:services
Seat: domain:services#2
File surface: the dev re-measures first; a breach stops the work and is explained in the report.
packages/plugins/plugin-security/src/**: computeControlledByParentFilter and its four call sites (the engine middleware's principal and delegator halves, getReadFilter, computeDelegatedReadFilter), and the plugin's tests.
packages/services/service-analytics/src/**: only if the raw-SQL door must accept the composed leaf that getReadFilter will now answer (read-scope-sql.ts / read-admission.ts).
packages/drivers/driver-turso/src/turso-driver.tstoRemoteWhere, one line, plus its test. This is a cross-lane surface (domain:engine). It enters through the ownerless-blocking-item channel: the engine seat's note 6309967905 on this card hands that fix to "whichever PR makes the switch … before, or with, that switch", and no card carries it.
docs/adr/0055-master-detail-controlled-by-parent.md:75, the "cached within the request" sentence corrected to what the code does, by ruling B ② (c). This is Tier H: the PR waits for the maintainer's approval.
{
"issue": 24932,
"status": "needs_decision",
"branch": "claude/issue-24932-cbp-leaf-switch",
"pr": null,
"session": "session_01UmqzD97e14iws6hmVhgZ5y — the dispatching seat's session (mode:subagent shares it; this run is told apart by its branch)",
"premise_still_valid": true,
"summary": "All seven premises were measured on the real stack (ObjectQL + SqlDriver on better-sqlite3 + the real SecurityPlugin and SharingService, origin/main f2b6cf230) before any edit, and two of them fork, so by the dispatch's stop conditions no switch was built: no tracked file was edited, the branch is empty at f2b6cf230, no PR. P3 forks: the declaration path admits a controlled_by_parent master of both classes the engine refuses by name (ADR-0139 D5.2 and D5.3, engine.ts:6550-6558 on main). ObjectSchema.parse admits a private master declared on another datasource together with its controlled_by_parent child, and a controlled_by_parent child whose master is sys_attachment (an object-scoped-middleware object); engine.registerApp admits a child whose master is bound explicitly to a second registered datasource and a child whose master carries an object-scoped read middleware; today's id list serves both (viewer xc1,xc2 and wc1,wc2; owner xc1 and wc1; delegated xc1 and wc1), and the leaf form answers INTERNAL_ERROR / 500 for every caller on both. No shipped producer: HotCRM e6d5d3e declares six controlled_by_parent objects (crm_contact, crm_quote, crm_contract, crm_quote_line_item, crm_opportunity_line_item, crm_event_attendee), every master a crm_* object on the one datasource with no object-scoped middleware; in-repo declarations outside tests: none. P5 forks: with the leaf as getReadFilter's answer, every analytics face answers READ_SCOPE_COMPILE_FAILED / 500 where today it answers the find path's rows — the native raw-SQL face ($in needs an array), the engine-bound face (assertReadScopeComparandsRunnable refuses the branded leaf as a non-list comparand before engine.aggregate could compose it) and the /analytics/sql echo. Compiling the leaf on the raw-SQL face itself needs the engine's composition and the driver's tenant wall (outside the surface); the in-surface design (the native strategy declines a leaf-carrying scope and routes to the engine face, as it declines a field-reference comparand by the 2026-08-12 ruling; the engine-bound guard admits the branded leaf with the engine's own option) changes the echo's answer for controlled_by_parent objects, so it is put to the seat rather than built. P1, P2, P4, P6 and P7 hold (readings below). The leaf form was measured through two held mutations of security-plugin.ts (scripts/ablation-replace.mjs), restored with blob == HEAD 0fe4494afce3 and git diff HEAD empty; it answers 1 statement per find and 2 per find+count with no master id scan, the same rows as today for the plain, restricted, delegated, nested-chain, null-FK, dangling-FK and child-only callers, and 200 with exactly bc1,bc2 (count 2) past 33,000 masters where today answers DATABASE_ERROR / 500. Two more doors differ under the leaf and are part of the switch's design, not a fork: a metadata cycle and a chain over the depth bound answer zero rows today (the plugin's own $in: [] doors) and INTERNAL_ERROR / 500 from the engine's cycle and depth refusals under the leaf; the switch keeps both doors as closed as today through a registry-only walk (no store work).",
"premises": {
"P1": "HOLDS. computeControlledByParentFilter (security-plugin.ts:8799 on f2b6cf230; the scan at :8906-8910) runs ql.find(rel.master, { where: masterFilter ?? {}, fields: ['id'], context: { isSystem: true } }) with no limit and answers { fk: { $in: ids } }. Measured: find+count on a controlled_by_parent child = 4 statements, 2 of them `select id from msr_account` (unrestricted scope: `select id from msr_account`; restricted: `… where owner_id = ?`); a delegated caller = 6 statements, 4 master id scans (two per half); a two-level chain = 6 statements, 4 pre-scans (grand, then mid with the grand ids bound). At 33,000 masters find and count both answer DATABASE_ERROR / 500, the driver log reading `too many SQL variables`.",
"P2": "HOLDS (measured under the leaf mutation, restored). A plain caller: unrestricted c1,c2 / restricted c1, one statement `select * from msr_contact where ((account in (select id from msr_account [where owner_id = ?])))`. A delegated caller with the delegator intersection: c1, one statement carrying both halves as two subqueries over the master. A leaf nested in a composed scope (two-level chain): l1 for the owner, l1,l2 for a view-all caller, one statement `… mid in (select id from msr_mid where grand in (select id from msr_grand where owner_id = ?))`. find=1, find+count=2, master id scans=0 for every shape.",
"P3": "FORKS — both classes are admitted. Spec: ObjectSchema.parse admits { sharingModel: private, datasource: 'other' } beside its controlled_by_parent child, and a controlled_by_parent child whose master_detail references sys_attachment. Engine: registerApp admits a child whose master is bound to a second registered datasource (resolveDatasourceBinding answers { datasource: 'other', via: 'explicit' } for the master and the default driver for the child) and a child whose master has an object-scoped read middleware (hasObjectMiddleware true). Today's id list serves rows on both (viewer xc1,xc2 and wc1,wc2; owner xc1 and wc1; delegated xc1 and wc1; the masters read directly x1,x2 / x1 and w1). Under the leaf both answer INTERNAL_ERROR / 500 by name: `… composes no such leaf over 'msr_xmaster', which is bound to datasource 'other' …` and `… over 'msr_mwmaster', whose rows an object-scoped read middleware narrows …` (engine.ts:6550 and :6554-6558). Shipped compositions: HotCRM e6d5d3e, 6 controlled_by_parent objects, every master a crm_* object on the single datasource, no middleware-gated master, no datasource or datasourceMapping declaration anywhere in its src; in-repo declarations outside test fixtures: none.",
"P4": "HOLDS. A caller holding read on the child and no object-level read on the master: today [] on the child (direct master read PERMISSION_DENIED / 403), the leaf form [] too (canReadObject false ⇒ $in: []). The plugin's own door (objectReadAdmission before the scan, #24962) and the leaf's door (canReadObject at ADR-0139 D2 step 1) close identically for a plain caller, and for a delegated caller the leaf's one canReadObject(delegated context) equals today's two halves (principal AND delegator).",
"P5": "FORKS. Today all three analytics faces answer the find path's rows (viewer c1,c2; owner c1; delegated c1; child-only []), the native face running the id list as a bound IN. Under the leaf: native face READ_SCOPE_COMPILE_FAILED / 500 `$in for \"account\" needs an array (fail-closed)` (read-scope-sql.ts compileOperator, :2102-2103); engine-bound face READ_SCOPE_COMPILE_FAILED / 500 from assertReadScopeComparandsRunnable (:1076-1079, assertListComparandShapes without admitReadableIds: `Operator $in on field account requires an ARRAY of values. Received object …`), before engine.aggregate could compose the leaf; the /analytics/sql echo refuses the same way; the child-only caller keeps [] on every face (the deny sentinel, unchanged). What the door needs: the engine-bound merges (objectql-strategy.ts withReadScope :900 and the FK attribute map :1422) need assertReadScopeComparandsRunnable to pass admitReadableIds: true (the option the engine's own ingress door passes, objectql/filter-comparand-shape.ts:123) — normalizeFilterComparandTypes skips a non-array $in by design, assertReadScopeCannotVacate reads no finding on it, and judgeFilter runs the engine's admitting pipeline — after which engine.aggregate composes the leaf at its boundary; the raw-SQL face (compileScopedFilterToSql) cannot compile a leaf without the engine's composition (canReadObject + getReadFilter per parent, recursive, the cycle and depth guards) and the driver's tenant wall for the master (driver-sql applyTenantScope with the outer options) — work outside the claimed surface — so the in-surface shape is the ruled precedent: NativeSQLStrategy.canHandle declines a scope carrying a leaf and the query routes to the engine face, and the dry-run echo refuses in its withheld envelope for controlled_by_parent objects where today it renders the bound list. Reported, not built.",
"P6": "HOLDS. docs/adr/0055-master-detail-controlled-by-parent.md:75 reads `One extra master-id query per controlled_by_parent object per request (cached within the request)`; the code has no request cache (find then count scan the master twice, measured in P1). :74 (`large-tenant scale … is a known limit — a future share-table/join mechanism would replace the id list`) is made false by the switch and would be corrected in the same edit.",
"P7": "HOLDS. Probe over the spec's conformance rows with a composed leaf whose scope carries a composed leaf (`record_id in composed(P, { id: { $in: composed(P, { owner: 'u1' }) } })`): TursoDriver local face answers c1,c3,c6,c7 (count 4; under tenantId org_a c1,c3,c7); the remote face refuses INTERNAL_ERROR / 500 `[driver-turso] where.id.$in holds a ReadableIds leaf … composed, and driver-turso does not compile it: the remote transport compiles it only as TursoDriver resolved it`. TursoDriver.toRemoteWhere (turso-driver.ts:3457-3466) resolves the leaf's scope through toRemoteFilter only, and core's replaceComposedReadableIds returns a composed leaf unentered (readable-ids-driver-refusal.ts:132), so the nested one reaches the transport unresolved. The one-line remedy (resolve `leaf.scope` through toRemoteWhere with the outer read's options) and the conformance row are ready to land with the switch."
},
"tests": "Measurement only, nothing committed; the worktree is clean (git status --porcelain empty, security-plugin.ts blob 0fe4494afce3 == HEAD). Harnesses (removed from the worktree, copies kept): /tmp/claude-0/-home-user/baba1a33-5813-5ac5-906a-64b704dc6695/scratchpad/issue-24932/dev/harness-main.test.ts.txt (rows, statements, the bind limit, the P3 pairs, the cycle and depth chains), harness-analytics.test.ts.txt (the three analytics faces over the same stack), harness-turso-probe.test.ts.txt (both Turso faces). Runs, every one through scripts/pm/os-verify-lock.sh: closure build `pnpm exec turbo run build --filter='@objectstack/plugin-security...' --filter='@objectstack/service-analytics...' --filter='@objectstack/driver-turso...' --filter='@objectstack/driver-memory...' --filter='@objectstack/driver-sqlite-wasm...' --concurrency=2` 22 tasks successful, VERDICT command-exit 0 (237 s); today's main harness VERDICT command-exit 0, Tests 4 passed (4) (measure-main2.txt); Turso probe Tests 2 passed (2) and analytics harness Tests 1 passed (1), sequenced with ';' under one acquisition (VERDICT batch-last-exit 0; each part's own exit recorded in its log: turso=0, analytics=0); the leaf leg (leaf-leg.sh under the lock, VERDICT command-exit 0): two held mutations of security-plugin.ts through scripts/ablation-replace.mjs — `ok mutation landed: anchor 1 → 0, blob 0fe4494afce3 → dca16daa31b7` (the import) and `anchor 1 → 0, blob dca16daa31b7 → fe26c47aa78f` (the scan replaced by the leaf answer; on disk: leaf-return count 1, scan-anchor count 0), main harness main=0 Tests 4 passed, analytics harness analytics=0 Tests 1 passed, then `ok restored: blob == HEAD (0fe4494afce3) and git diff HEAD is empty`. Both harness suites resolve @objectstack/objectql and @objectstack/driver-sql from src through the package's vitest alias, so no dist was on the mutation's resolution path. Not re-run: the landed suites of #25032 / #25072 (their pins stand on main). NOT MEASURED: Postgres, MySQL, a live Turso remote; a tenancy-enabled master.",
"gates": "No diff, so no family is owed. `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) in the worktree: exit 2, `this branch changes nothing against 'origin/main' (merge base f2b6cf230) — nothing to derive`. No PR, so no check-runs.",
"mcp_calls": "0 — no MCP GitHub tool was called; reads were `gh api` REST GETs (issue 24932 and its comments, issue 25010 and its comments, pulls 25032 / 25072 and their files, pull 24893 and its files).",
"api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/24932/comments (this report, via scripts/pm/post-stamped.mjs through the fleet-write relay as objectstack-fleet[bot]). Not a REST write: `git push -u origin claude/issue-24932-cbp-leaf-switch`, the empty-branch write probe (head f2b6cf230, one push). pr_create not used.",
"line_budget": "n/a — no file changed",
"deviations": [
"No PR and no code: needs_decision by the dispatch's stop conditions (P3 and P5 fork). The remote branch claude/issue-24932-cbp-leaf-switch stays empty at f2b6cf230; ⛔ not reused for the empty old branch's work either.",
"Three harness files were written into the worktree (packages/plugins/plugin-security/src/zz-measure-24932.test.ts, …/zz-measure-24932-analytics.test.ts, packages/drivers/driver-turso/src/zz-probe-24932-nested.test.ts), never committed, deleted before cleanup; copies in the scratchpad. The analytics harness imported service-analytics' src by a relative path from plugin-security — a harness-only spelling, never a committed test input.",
"P2 was measured by a two-anchor held mutation of security-plugin.ts (the scan replaced by the leaf answer) rather than by re-running the landed engine suites; both mutations were restored and the restore proven (blob == HEAD, git diff HEAD empty, trap-free script with an EXIT/INT/TERM restore on the absolute path).",
"Two harness runs were sequenced with ';' under one lock acquisition, so that acquisition's VERDICT is batch-last-exit; each part's exit code was captured into its own log before any pipe (turso=0, analytics=0, main=0).",
"The delegated caller was built as a plain context carrying onBehalfOf { userId } with a stored sys_user_position grant for the delegator, not through resolveAuthzContext / assembleExecutionContext; the landed #25032 suite covers the assembled shape."
],
"files_changed": [],
"open_questions": [
{
"question": "P3 — a controlled_by_parent master of a class the engine refuses by name (ADR-0139 D5.2: an object-scoped read middleware narrows its rows; D5.3: bound to another datasource) is admitted by the declaration path (spec parse, registerApp) and served by today's id list; after the switch such a child answers INTERNAL_ERROR / 500 for every caller. Measured: zero shipped producers (HotCRM 0 of 6, in-repo 0). Which answer does the switch give that shape?",
"options": [
"A — The engine's named refusal IS the answer for these two classes: the switch answers the leaf for every controlled_by_parent object and the engine refuses D5.2 / D5.3 masters loudly at read time with the remedy in the message (today's behaviour for the six consumers' D5 classes, chosen by ADR-0139 D5); ADR-0055's corrected Consequence names the two classes as the one limit that replaces the id-list ceiling. Business need: none measured — no shipped producer declares either shape, so no reader loses rows. Long-term: one spelling of one scope (the ruling's end state), the D5 posture already ruled (`refuse loudly, no silent fallback`). AI-error-proofing: a loud, named 500 beats an id list that works until the bind cliff; but the parse still admits a declaration the runtime refuses, so a boot-time or authoring-time refusal would be the honest end state (option C). Startup focus: no new mechanism, implementation-first for a zero-pull declared shape. Effort: the switch as dispatched plus two pins (one per class) and one ADR sentence.",
"B — Keep the id list for exactly these two classes, chosen up front inside plugin-security (ask the engine whether the master has an object-scoped middleware or another datasource binding; answer the bounded list for it, the leaf otherwise). Business need: none measured. Long-term: a second, permanent spelling of one scope — the drift getReadFilter exists to prevent and the shape ruling 6303721609 ③ refused for the transition; the bind cliff stays for that class. AI-error-proofing: neutral. Startup focus: larger — the datasource binding is not on the public IObjectQLEngine contract, so plugin-security would need a new engine door (engine lane). Effort: engine door + plugin-security routing + pins.",
"C — Refuse the declaration where it is made: a controlled_by_parent child whose master resolves to another datasource, or to an object an object-scoped read middleware gates, is refused at app registration / os validate with the remedy named. Business need: none measured; it protects authors from a shape that today works until the bind cliff and tomorrow 500s. Long-term: declared equals enforced, the cleanest end state; both facts are deployment facts (datasourceMapping, package default datasource, lifecycle routing, middleware registrations), so the refusal lives in the engine/runtime boot, not in the spec parse. AI-error-proofing: best — loud before any data exists. Startup focus: a new runtime refusal in another lane; defensible as closing a declared-but-unenforceable shape, not as a new capability. Effort: an engine/runtime card after the switch."
],
"recommendation": "A now, with C filed as the engine-lane follow-up only if the maintainer wants the refusal moved from read time to boot. Business need: zero producers measured, so no reader loses rows under A. Long-term: A keeps one spelling (the ruling's end state) and reuses the D5 refusal ADR-0139 already chose for exactly these classes; B reintroduces the two spellings the ruling refused. AI-error-proofing: A's refusal is loud and names the remedy; C would make it loud earlier, which is why it is the follow-up and not a reason to hold the switch. Startup focus: A adds nothing; B adds an engine door for a shape nobody declares."
},
{
"question": "P5 — with the leaf as getReadFilter's answer, every analytics face (native raw SQL, engine aggregate, the /analytics/sql echo) refuses a controlled_by_parent object's read scope READ_SCOPE_COMPILE_FAILED / 500 where today it answers the find path's rows. Compiling the leaf on the raw-SQL face needs the engine's composition and the driver's tenant wall (outside the claimed surface). May the switch carry the in-surface design, and with which answer for the echo?",
"options": [
"A — In service-analytics only: (i) the engine-bound merges (withReadScope and the FK attribute map) admit the branded uncomposed leaf at assertReadScopeComparandsRunnable by passing admitReadableIds: true — the option the engine's own ingress door passes — so engine.aggregate composes it at its boundary exactly as a find does; (ii) NativeSQLStrategy.canHandle declines a query whose base or joined object's read scope carries a ReadableIds leaf, routing it to the ObjectQL strategy — the content-based decline the 2026-08-12 maintainer ruling (Q1 = B, recorded at canHandle) accepted for a field-reference comparand (security rules exist in one place, no copy, no new StrategyContext hook) and the one hasObjectMiddleware already drives; (iii) the /analytics/sql dry-run echo of a controlled_by_parent object refuses in its withheld READ_SCOPE_COMPILE_FAILED envelope (it runs no composition and must not render one it did not run), the shape the cross-field ruling kept (`only the dry-run face refuses, one consistent loud answer`). Business need: HotCRM dashboards over its six controlled_by_parent objects keep every row through the engine face; the echo of those objects changes from rendered SQL to a loud refusal. Long-term: one owner of the composition (the engine); the native face keeps declining what only the engine can judge. AI-error-proofing: neutral (nothing authorable). Startup focus: smallest, ~40 lines plus pins. Cost: controlled_by_parent objects leave the native-SQL strategy for the engine aggregate (same rows; the performance class of the two faces on those objects is unmeasured), and the spec docblock's `the one caller that may pass admitReadableIds` sentence gains a second caller, the engine's mirror.",
"B — The raw-SQL face compiles the leaf itself: the engine exposes its composition as a contract door (compose the read scope of an object for a context, the chain, cycle and depth guards included) and service-analytics compiles the composed leaf as a semi-join with the master's tenant wall obtained from the driver. Business need: keeps controlled_by_parent objects on native SQL and the echo rendering. Long-term: a second compiler of the leaf beside driver-sql and the Turso remote transport, and a new engine contract member; against the 2026-08-12 ruling's direction for this strategy. AI-error-proofing: neutral. Startup focus: largest — engine, spec contract, driver-sql tenant-wall access, service-analytics; two lanes.",
"C — getReadFilter keeps answering the bound id list (only the middleware halves switch). Business need: none. Long-term: two spellings of one scope (ruling 6303721609 ③ refused it), the bind cliff stays on analytics, and the engine's nested composition would receive a list from getReadFilter(master) while the middleware writes a leaf — the drift this method exists to prevent. Not recommended."
],
"recommendation": "A, because it is inside the claimed surface, is the direction a prior maintainer ruling fixed for this strategy, keeps one composition owner and answers the find path's rows on every analytics query; the one visible change — the dry-run echo refusing for controlled_by_parent objects — is a loud answer on a debugging aid, not a row change, and is named in the PR as a deviation. If the maintainer wants the echo to keep rendering for these objects, that is B's work and a separate card."
}
],
"deferred_card_repricing": "#25106 is left unaffected in shape by this card: it still edits getReadFilter and computeDelegatedReadFilter, which the switch leaves as two of the four call sites of the derivation. After the switch those methods answer a leaf-carrying scope, so #25106's pins must read rows through the engine (a leaf composes only there) rather than match an in-memory filter — a heavier test rig, not a different fix. Its serial order after this card stands.",
"measurements_local": "All under /tmp/claude-0/-home-user/baba1a33-5813-5ac5-906a-64b704dc6695/scratchpad/issue-24932/dev/: base.txt (BASE=f2b6cf230); measure-main.txt and measure-main2.txt (today: rows per persona, statements, the bind limit, P3 admission readings — the second run adds the delegator's stored grant); measure-turso-main.txt (P7, both faces); measure-analytics-main.txt (P5 today, three faces); leaf-leg.sh and leaf-leg.txt (the two held mutations, on-disk counts, the restore proof); measure-leaf.txt (P2 / P3 / P4 under the leaf, rows and statements); measure-leaf-analytics.txt (P5 under the leaf); build-closure.txt; gates-derive.txt; the thread and PR bodies as issue.json, comments.json, c-*.md, pr-25032-body.md, pr-25072-body.md, c25010-*.md; harness copies harness-main.test.ts.txt, harness-analytics.test.ts.txt, harness-turso-probe.test.ts.txt.",
"out_of_scope_findings": [
"carrier: #24932 (this card's decision) · noted, not filed · Under the leaf, a metadata cycle (two controlled_by_parent objects mastering each other) and a chain past the depth bound answer INTERNAL_ERROR / 500 from the engine's own cycle and depth refusals (READABLE_IDS_MAX_CHAIN_DEPTH, engine.ts:6479-6498), where today's derivation answers zero rows with an error log naming the cycle or the bound (its $in: [] doors, controlled-by-parent-chain.test.ts pins both). The switch keeps both doors as closed as today through a registry-only walk of the master chain before answering the leaf (no store work), so the engine never meets either; stated here so the seat can object before it is built.",
"carrier: the switch PR (Tier H text) · noted, not filed · Beyond ADR-0055 :74-:75, three more sentences of that record describe the id list and go stale with the switch: the status line's `§2's chosen mechanism (b) … stand`, the amendment's `What both halves do now — Read` paragraph (:120, the master set resolved through computeControlledByParentFilter as a third layer) and its `Cost … paid per hop` sentence (:123). The dispatch allows no other ADR text; the seat decides whether the switch's Tier H edit may name them.",
"carrier: the switch PR · noted, not filed · packages/spec/src/data/readable-ids.ts:50 and filter-comparand-shape.ts:432-444 say the engine is `the one caller that may pass admitReadableIds`; option A for P5 makes the analytics read-scope mirror a second caller of the same door (judging the scope the engine will run). Spec docblock, outside this card's spec surface (the conformance row only).",
"carrier: none · noted, not filed · On a delegated read the leaf form composes the master's scope once per parent (the engine asks each parent once per read) but the middleware's two halves each write a leaf over the same master, so the one statement carries the same subquery twice (`account in (select …) and account in (select …)`, measured). Correct rows, one redundant subquery per delegated read; a later simplification of the delegator half, not this card's.",
"carrier: none · noted, not filed · driver-turso and driver-sqlite-wasm have no vitest alias for @objectstack/spec, so the nested conformance row reaches their suites only after a spec rebuild (scripts/ablation-dist-preflight.mjs before any verdict); driver-sql and driver-memory alias spec to src. A test-rig fact for the switch PR, not a defect."
]
}
[Decision] Sub-question of ruling B: two declarable master shapes that the leaf cannot serve, and the analytics raw-SQL door
Ruling-ref: 6303721609 (letter B stands; only the sub-question that its premise "once for every shape" falsifies is put here)
domain:services seat 2 (seat post #21118) · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-21T14:41Z. Evidence: the dev report 6318648840. It is measurement only, on the real stack (ObjectQL + SqlDriver/better-sqlite3 + SecurityPlugin + SharingService) at origin/mainf2b6cf230. No code was committed.
Release: session_01UmqzD97e14iws6hmVhgZ5y · Cause: the dispatch's stop conditions fired (P3 and P5 fork), and P3 sets an Accepted ADR rule against the ruling's text, which is the maintainer's to settle · Goes to: needs-user-decision; this write clears the assignee. Branch claude/issue-24932-cbp-leaf-switch is empty (at f2b6cf230, no commits). The measurements and harness copies are kept in the seat's scratchpad.
ADR-0139 D5 (Accepted), verbatim: "Each of the two remaining classes is recognised by the engine, which refuses to compose a leaf for it (loud, as D3's envelope), and routed by the consumer to today's probe before the leaf is written. ⛔ No silent fallback: the probe is chosen up front, never reached by catching a refusal." Also: "D5.3 … Recorded as a limit, not solved."
Ruling B (6303721609) ①: "once for every shape … and the id-list pre-scan retires". ③: "⛔ Not taken: A as a stop-gap (two spellings of one scope during the transition)".
The conflict: if D5 is followed as written, controlled_by_parent keeps the id list for these two classes, which falls short of ① "retires". If ① is followed, D5's routing sentence needs an amendment for the seventh consumer.
Ruling: batch #382 item 1 · letter Q1 C / Q2 A · maintainer 「同意」 2026-10-21T23:40Z
Director seat, summon #47, session_01TQvbE2ENCniJr1ffCzbPc6 (os-bill, writing through the relay). Presented in this session's chat as batch #382 item 1, from the domain:services seat 2 decision 6318800595 and the dev report 6318648840. The maintainer first asked 「24932 25133 帮我综合分析工作量和风险」; the director's effort-and-risk reading was given in chat; the maintainer answered 「同意」, taking the recommendation: Q1 C, Q2 A. Ruling B (6303721609) stands; this ruling settles the sub-question its premise "once for every shape" opened.
The director's delta, re-checked on origin/mainabc25a3536: the engine still refuses both classes by name (packages/objectql/src/engine.ts:6553, :6557); plugin-audit routes the same two classes up front through public contract members, so the dev report's "B needs a new engine door" was wrong and the seat's correction is confirmed (plugin-audit/src/parent-record-read-gate.ts:207-208); the raw-SQL face refuses a non-array $in (service-analytics/src/read-scope-sql.ts:2103). New reading: every object carrying an object-scoped read middleware on main is a platform system object (comment and reaction, activity, the audit ledger, mail message and association, attachment, position assignment, approval request — enumerated on #25144), so C forbids only "a business detail mastered by one of those", a narrow blast radius. Boot-time refusal precedents: #24806 (+394 / −50), #25007 (+483 / −6). Thread read to its end at this write: 6318800595 is the newest comment.
Governing text: ADR-0139 D5 (the engine refuses to compose a leaf for the two classes, loud as D3's envelope; "⛔ No silent fallback"); ruling B 6303721609 ① ("once for every shape … the id-list pre-scan retires") and ③ (no two spellings of one scope); ADR-0049 (enforce-or-remove); for Q2, ADR-0021 D-C (the raw-SQL read scope IS the find path's scope) and the 2026-08-12 ruling on #7598 (Q1 = B, recorded at NativeSQLStrategy.canHandle).
What the ruling binds
Q1 — letter C, refuse the declaration where the deployment binds it.
Not taken: A (a declaration that boots today and answers 500 at read tomorrow — the shape ADR-0049 exists to remove); B (a permanent second spelling for zero users, against ruling B ①).
Q2 — letter A, in service-analytics only.
5. The engine-bound merges (withReadScope, the FK attribute map) pass admitReadableIds: true so engine.aggregate composes the leaf at its boundary as a find does; NativeSQLStrategy.canHandle declines a query whose base or joined object's read scope carries a leaf and routes it to the ObjectQL strategy (the content-based decline the 2026-08-12 ruling accepted for a field-reference comparand); the /analytics/sql dry-run echo of a controlled_by_parent object refuses in its withheld READ_SCOPE_COMPILE_FAILED envelope. That echo change is the one visible change, on a debugging aid, named in the PR as a deviation.
6. Analytics admits the leaf only in the scope getReadFilter answers; a filter an author writes keeps today's judgement (pinned). The spec docblock's "the one caller that may pass admitReadableIds" sentence is corrected in the same PR.
7. Not taken: B (a second compiler of the leaf and a new engine contract member, against the 2026-08-12 direction); C (two spellings, refused by ruling B ③).
Execution parameters, confirmed as the seat proposed: the ADR-0055 sentences the switch falsifies (the status line, :74, :75, :120, :123) are corrected one by one in the same Tier H edit; a metadata cycle and a chain past the depth bound keep answering zero rows through a registry-only walk, never 500; the duplicated subquery on a delegated read is correct and left for a later simplification; #25106 stays serial after the switch.
Measurement conditions the director adds to the switch's dispatch (the risk the chat analysis named): the switch PR runs driver-sql's readable-ids conformance suite on live PostgreSQL, or the dev measures on PostgreSQL locally and records the reading; dashboard latency over the HotCRM fixture for its six controlled_by_parent objects is measured on the engine face against today's native face, and a material regression is a fork to report, not to absorb; the Turso remote nested-leaf fix (one line in toRemoteWhere plus the conformance row) lands with the switch; the cloud seat reads a two-level controlled_by_parent chain on staging (Turso remote) before moving its pin. Rollback of the switch is one revert; #25144's refusal stands on its own.
Effort of the chosen letters: Q1 C — one domain:engine PR (precedents above) ahead of the switch; the switch itself domain:services · 1 PR, M, with Q2 A's ~40 lines plus pins inside it (the card's column). The dev's three measurement harnesses are kept in the dispatching seat's scratchpad and become the pins. Confidence gap carried: the registration-order question, which #25144 measures first.
State:needs-user-decision → pm:blocked, Blocked-by: #25144 (the closed #25009 / #25010 lines leave the body). The assignee stays clear; domain:services seat 2 re-claims when #25144 closes. Branch claude/issue-24932-cbp-leaf-switch is empty at f2b6cf230; the next claim decides whether to reuse it.
What the boot refuses. A controlled_by_parent detail whose master is on another datasource (D5.3) or behind an object-scoped read middleware (D5.2). The refusal is INVALID_METADATA / 422, thrown from ObjectQLPlugin's kernel:bootstrapped handler.
One limit the switch's reader should carry. It is relevant to ruling ③'s dated ADR-0139 D5 note, which says the seventh consumer "needs no probe because the boot refuses both classes".
Boot only. The refusal judges the artifacts present at boot. An artifact registered after boot is admitted, through the manifest service or metadata:reloaded.
The HTTP install door is closed.POST /api/v1/packages cannot carry an object definition, so no public entry reaches the late path today.
Wording for the note. "Refuses at boot" is the accurate form. Once the switch writes the leaf on every read, engine seat 1 re-measures the late path and files a card if a public entry reaches it.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-22T05:54Z. ⛔ Not a claim.
Blocked-by: #25144 (the engine card that ruling Q1 C files: declaration-time refusal of the two master shapes). #25144 closed completed at 2026-10-22T04:59Z. Per the ruling (6329027469), the leaf switch now lands after C, together with Q2 A (the analytics raw-SQL door yields to the engine face). The domain:services seat resumes from its empty branch claude/issue-24932-cbp-leaf-switch.
Claim: PM loop round 1 · 2026-10-22T06:23Z · p1, domain:services, area:access · the plugin-security switch to the ADR-0139 ReadableIds leaf, with Q2 A in service-analytics, on ruling Q1 C / Q2 A (6329027469)
Session: session_01UnHcTADjeg3VwxF21QHj7W (the dispatching seat; the dev is a mode:subagent run of this session, told apart by its branch)
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24932-cbp-leaf-switch (reused: empty at f2b6cf230, which is on main; the ruling's State line leaves the choice to this claim)
Worktree: objectstack-issue-24932
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface (at origin/mainc5915b2bb); the dev re-measures first, and a breach stops the work and is explained in the report:
packages/plugins/plugin-security/src/**: computeControlledByParentFilter and its four call sites (the engine middleware's principal and delegator halves, getReadFilter, computeDelegatedReadFilter), answering the leaf for every controlled_by_parent object with no up-front routing to an id list. A metadata cycle and a chain past the depth bound keep answering zero rows through a registry-only walk, never 500 (ruling, execution parameters). The plugin's tests.
packages/services/service-analytics/src/**, Q2 A only (ruling ⑤ ⑥):
the engine-bound merges (withReadScope, the FK attribute map) pass admitReadableIds: true;
NativeSQLStrategy.canHandle declines a query whose base or joined object's read scope carries a leaf;
the /analytics/sql dry-run echo of a controlled_by_parent object refuses in its withheld READ_SCOPE_COMPILE_FAILED envelope, named in the PR as a deviation;
analytics admits the leaf only in the scope getReadFilter answers, and a filter an author writes keeps today's judgement (pinned).
packages/drivers/driver-turso/src/turso-driver.tstoRemoteWhere, one line, plus its test. Cross-lane (domain:engine): engine seat 2's note 6309967905 hands the fix to the switch PR, and the ruling's measurement conditions name it. Declared on [PM seat] domain:engine — ⏳ vacant #6367.
packages/spec/src/data/readable-ids-conformance.ts, one nested-leaf row, and packages/spec/src/data/filter-comparand-shape.ts, the docblock sentence naming "the one caller that may pass admitReadableIds" (ruling ⑥). Cross-lane (domain:spec): a conformance row and one prose sentence; no schema, accept-set or export change. Declared on [PM seat] domain:spec — 🟢 os-tesla · session_013odhA26dvzy7pQmevdTTvy #6017. ⛔ Nothing else in packages/spec.
docs/adr/0055-master-detail-controlled-by-parent.md: the sentences the switch falsifies (the status line, :74, :75, :120, :123), corrected one by one. docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md D5: the dated note for the seventh consumer, worded "refuses at boot" (engine seat 1's pointer 6333147990). This is Tier H: the PR waits for the maintainer's word.
.changeset/.
Container & model: M, mode:subagent, model: fable (CONTRACT_REVIEW_TIER as the ceiling, as the previous claim 6317834763 chose; reason: a security read-scope switch on every controlled_by_parent read, the delegator intersection and three analytics faces. dispatch-gates --tier on this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable")
Clause-②: no
Ruling-ref: 6329027469
Responsibility: plugin-security's computeControlledByParentFilter (this lane's code) materialises the master id list unbounded, answering 500 past the dialect's bind limit | the engine's ADR-0139 ReadableIds leaf (ruling B 6303721609), with #25144's boot refusal of the two master shapes the leaf cannot serve (ruling Q1 C 6329027469, landed as PR #25166) | every reader of a controlled_by_parent child, HotCRM's six among them; measured 500 at 40,009 masters (card body)
Thread-read: 6333845431
Serial constraints cleared:
No open PR touches security-plugin.ts, service-analytics, turso-driver.ts, readable-ids-conformance.ts, filter-comparand-shape.ts, ADR-0055 or ADR-0139. The open PRs' file lists were read this round.
The declaration reads the ruling: the switch respells one existing scope (an id list becomes the engine's semi-join leaf), widens no accept set, adds no public name, and must keep every row set (ruling B ④). The /analytics/sql echo's refusal for a controlled_by_parent object is the one visible change, which the ruling names as a deviation on a debugging aid. The contract-tier review judges whether it moves the arm. Either way the build owes the full dogfood battery, and a review at CONTRACT_REVIEW_TIER before the queue.
The ruling's measurement conditions ride the dispatch:
driver-sql's readable-ids conformance suite on live PostgreSQL, or a local PostgreSQL reading recorded;
dashboard latency over the HotCRM fixture's six objects, engine face against today's native face, where a material regression is a fork to report;
the Turso remote nested-leaf fix with the conformance row.
The cloud seat's staging read of a two-level chain is the cloud seat's, at its pin move. The previous dev's harnesses lived in another session's scratchpad and are not reachable here; the dev rebuilds them from 6318648840.
Pointer · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-22T09:12Z. ⛔ Not a claim, no label change.
Seat 1 is signing off (the maintainer's 「你的任务都处理完了吗?可以下班了」). Its pointer 6333147990 says engine seat 1 re-measures #25144's late path once this switch lands. That re-measure now passes to the domain:engine seat on duty when this card closes, seat 2 (#20966) or seat 1's next term.
What to measure: an artifact registered after boot, through the manifest service or metadata:reloaded, carrying a controlled_by_parent detail over an unservable master. Try the marketplace install route first.
What decides: file a card only if a public entry reaches it.
{
"issue": 24932,
"status": "done",
"branch": "claude/issue-24932-cbp-leaf-switch",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25198",
"session": "session_01UnHcTADjeg3VwxF21QHj7W — the dispatching seat's session (mode:subagent shares it; this run is told apart by its branch)",
"premise_still_valid": true,
"summary": "The plugin-security switch to the ADR-0139 ReadableIds leaf is built as ruling Q1 C / Q2 A binds it, on dispatch base 72a302a9d, final head 35c4c3951 (the PR opened at 2b988305d; one test-only commit after it: CI's Test Core shard 5 reddened on packages/core's repo pin row-serving-door-exposure, which reads any non-test source calling `.canReadObject(` as a row-serving door and found the new in-memory composer testkit; reproduced locally with `pnpm --filter @objectstack/core run test:repo` — 1 failed / 81 passed — and fixed by handing the composer the engine's two halves as injected predicates named for ADR-0139 D2 steps 1 and 5, so the testkit names no service member; test:repo then 6 files / 82 passed, the three rewired suites 3 files / 80 passed), draft PR #25198 (Tier H: docs/adr/0055 and 0139 edited as the ruling names, waits for the maintainer's word). computeControlledByParentFilter answers { masterFK: { $in: ReadableIds.of(master) } } for every controlled_by_parent object on a read verb at all four call sites, with no up-front routing to an id list; the cycle and depth guards are a registry-only walk made before the leaf is written, the cut equal to the engine's (a chain of eight derived objects composes into one statement with eight nested subqueries; the ninth and a cycle answer zero rows, 200); a write verb's predicate carries no leaf (ADR-0139 D1 — the predicate write is narrowed by the read door and judged per master as before, pinned). Q2 A in service-analytics: withReadScope, resolveFkAttr and the plugin's record-label pass admit the leaf, NativeSQLStrategy.canHandle declines a leaf-carrying scope, compileScopedFilterToSql refuses it by name in the withheld READ_SCOPE_COMPILE_FAILED envelope (the echo's refusal is the named deviation); the spec docblock sentence is corrected; the Turso remote face resolves a nested composed leaf and a nested conformance row runs in every driver suite. Measured on the real stack (SQLite and live PostgreSQL 16 stood up in the container): every caller shape keeps its row set, find = 1 statement and find + count = 2 with no master scan, 33,000 masters answer 200 with exactly the right rows (the id-list form: DATABASE_ERROR / 500 under ablation). TWO FORKS TO REPORT, neither absorbed silently: (1) a delegated caller's detail now follows its master's delegated read — explain-enforce-parity's row 'an agent on behalf of the org writer, a detail under someone else's master, update' moves 404 → 403 and the chain suite's hand-built two-human delegated row widens from line_us to line_us, line_jp — because the engine composes the master's leaf with getReadFilter(master)'s ADR-0090 D10 spelling (#25106: the principal's leg at the ceiling's DECLARED depth, 'org' when silent) where the id-list derivation used the ceiling's EFFECTIVE depth ('own'); the master itself was already readable to that caller (the sibling parent row p_theirs answered 403), so the detail now reads exactly the rows a delegated direct read of its master reads (ADR-0139 D5.1 as amended, ADR-0055 §2's equality) and no row is served the caller could not read on the master; recommendation: accept, see open_questions. (2) the two #21829 stand-down write shapes (a nested operation, the engine's referential field clear) carry the RLS write filter only, where before they also carried masterFK IN (readable master ids); engine-internal, not caller-addressable, NOT MEASURED at run time, named in the PR. Dashboard latency over the HotCRM fixture (hotcrm @ c8a7a7047, its eight controlled_by_parent objects, 10,000 masters per master object, 16 widgets) on SQLite: the engine face is 25% faster in total than today's native face over the id list (ratio 0.75; per-widget 0.34–1.18, the two above 1.0 inside their own spread), no material regression. Deviations: PR opened without merging the six main commits that landed since the base (no file overlap; the queue's merge-ref run validates); the PostgreSQL latency cell and the full dogfood battery are reported below as they stood at report time.",
"tests": "Every build/test under scripts/pm/os-verify-lock.sh (slot issue-24932-dev; shared box). Closure build `pnpm exec turbo run build --filter='@objectstack/plugin-security...' --filter='@objectstack/service-analytics...' --filter='@objectstack/driver-turso...' --filter='@objectstack/driver-sql...' --filter='@objectstack/driver-memory...' --filter='@objectstack/driver-sqlite-wasm...' --concurrency=2`: 22 tasks successful, VERDICT command-exit 0 (117 s). Batch 1 (spec rebuilt first, each part's exit captured before any pipe): plugin-security first run 89 failed in 13 files (one cause: ten real-stack rigs boot a bare ObjectQL with no security source in its slot, so every leaf composed $in: []; plus a fake-engine matcher reading a leaf and two of my own over-strict pins) → fixed; plugin-security typecheck 0; service-analytics `vitest run` 184 files / 4505 passed / 262 skipped, exit 0, typecheck 0; driver-turso readable-ids conformance 27 passed (nested row on both faces); driver-sql sqlite 18 passed / 2 skipped; driver-memory 15; driver-sqlite-wasm 11; spec readable-ids 48; driver-turso typecheck 0. Batch 2 (OS_TEST_POSTGRES_URL=postgres://postgres@127.0.0.1:54329/objectstack_test, a local PostgreSQL 16 started with initdb/pg_ctl as the postgres user): driver-sql readable-ids conformance `live postgres RAN`, 36 passed / 1 skipped; driver-mongodb refusing conformance 13 passed; the reworked plugin-security suites 12 failed in 3 files (my PG regex, a helper running a caller's scope under a system context, the delegated parity row) → fixed. Batch 4 on head 2b988305d: the three reworked suites with the PG cell 3 files / 231 passed (`on sqlite` and `on postgres`), then the FULL plugin-security suite 208 files / 4275 passed / 45 skipped, VERDICT command-exit 0 (held 933 s). ABLATIONS (scratchpad/ablate.sh; each leg: git restore --source=BASE for the subject, blob hash proven == BASE, on-disk marker counts printed, pins run, git checkout HEAD -- the files, blob proven == HEAD, git diff HEAD empty; trap restores on exit): security-plugin.ts at BASE (leaf-return count 1 → 0): ABLATED_RUN_EXIT=1, 30 pins red across the sqlite and postgres cells — every statements-per-request pin, the 33,000-master read (DATABASE_ERROR / 500), the leaf-shape pins, the engine's-cut pin, nested-compose's one-statement pin, the chain and sharing no-store-work pins; restored blob == HEAD 54b78ccf, tree clean. The four analytics files at BASE (admit count 2 → 0): 3 pins red (engine-bound face refuses, canHandle true and the raw seam called, the echo's compiler names no leaf); restored, clean. turso-driver.ts at BASE (nested-resolve count 1 → 0): 1 pin red (the nested row refuses on the remote face); restored, clean. Reverse-verification direction observed: red, as expected, on every leg. Latency harness (scratchpad/harness-latency.test.ts.txt, copied in for the run and removed, tree clean after): SQLite cell measured, the table is in the PR (today native 755.4 ms, after engine 568.4 ms, control engine+idlist 1031.5 ms, 16 widgets, identical counts); PostgreSQL cell (measured after the dogfood battery on 35c4c3951, same harness, same box): today native 1101.4 ms, after engine 510.0 ms, control engine+idlist 1432.1 ms over the 16 widgets (ratio 0.46), identical counts; per-widget ratios 0.20–1.60. TWO WIDGETS TRIP THE PER-WIDGET THRESHOLD I stated in the PR (ratio over 1.5): crm_contact count 32.7 → 50.5 ms (1.54) and crm_quote count 33.9 → 54.4 ms (1.60), both one-hop details of the private crm_account; their grouped twins are 1.19 and 0.38, every other widget is 0.20–0.68, and the absolute delta is 18–20 ms on a shared box whose own max spread for those cells is 40.7 / 44.4 (today) against 52.7 / 56.5 (after). The control cell shows the cost is the engine face's routing on a PG COUNT (engine+idlist 58.1 / 54.0 ms), which the leaf then recovers most of. My reading: not material — a 20 ms per-widget cost on two count widgets against a 54% total gain, and inside the box's noise band; reported here as the ruling asks rather than absorbed, see pr_body_additions. Shard-5 fix verified on 35c4c3951: `pnpm --filter @objectstack/core run test:repo` 6 files / 82 passed; the three rewired suites 3 files / 80 passed. NOT MEASURED: MySQL; a live Turso remote (the remote face was measured through the in-repo libsql stub, as the Turso suite measures it); a tenancy-walled master on PostgreSQL beyond the conformance row.",
"gates": "Local subset on the FINAL head 35c4c3951 (rerun after the last push): `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 102 commands (changed lines under the 3000 human-merge threshold); each run with its exit code captured before any pipe and recorded byte for byte (scratchpad/ran.list): 102 exit 0. `node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack`: ✓ 102 derived families accounted — 102 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN, RAN_VERDICT_EXIT=0. The earlier union on 2b988305d had two PREREQUISITE-NOT-MET families (`check:dual-build-cjs-loads`, `check:i18n`, both read every package's dist/); on the final head the dogfood closure build had filled those dists first and both ran exit 0. `node scripts/check-nul-bytes.mjs`: OK. Not derived and not run: check:platform-checklist (no exported symbol renamed or removed); check:dev-prereqs / check:skill-frame-freshness (not triggered). The spec families that read a generated surface (check:api-surface, check:docs) ran green with nothing regenerated. CI on the PR head 35c4c3951 (check-runs, all completed): success — Build Core, Lint & Repo Gates, TypeScript Type Check, Type Check · workspace / source gates / consumer gates / debt ledger, Test Core and Test Core (1/6)…(6/6), Governed Surface Queue Guard, Check Changeset, Check PR Size, Check Documentation Links, Dogfood Regression Gate, Spec property liveness, Auto Label, Flag docs affected by code changes, Lint · ESLint / CI, release & engine gates / PM dispatch-gates / docs & code guards / repo & tooling gates / scoped · query-options erasure / slot lookup & corpus / tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter; skipped (selector or opt-in) — 18.x upgrade harness (opt-in), Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix shard), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). On the PR's first head 2b988305d, Test Core (5/6) and the Test Core aggregate were red: packages/core's test:repo project, the repo pin row-serving-door-exposure, reproduced locally (1 failed / 81 passed) and fixed in 35c4c3951.",
"dogfood_battery": "35c4c3951 · `pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build` (exit 0) then `pnpm --filter @objectstack/dogfood test` (no OS_TEST_SHARD, no path) under the lock: os-verify-lock VERDICT command-exit 0 · held the lock 2867s (47m47s) · Test Files 287 passed | 3 skipped (290) · Tests 2395 passed | 18 skipped (2413) · no failure. A first battery run on 2b988305d died with the container's worker restart (DF_BUILD=0, the test never reached its summary) and is not cited.",
"mcp_calls": "1 — mcp__claude-code-remote__add_repo (read access to objectstack-ai/hotcrm, the latency fixture; it attached nothing and I cloned read-only; a session tool, not a GitHub MCP tool, and no write). No GitHub MCP tool was called; every GitHub read was a `gh api` REST GET (issue 24932, its ten comments and the two cross-lane declarations, pull 25198 and its head's check-runs).",
"api_writes": "2 — both through scripts/pm/ as objectstack-fleet[bot] via the fleet-write relay (--via dispatch): ① POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, assignees os-bill; relay request fw-20261022T091452Z-19a15f, run 39753937651 success, read-back 20432 bytes sent = stored; the relay's assignees leg is POST …/issues/25198/assignees inside the same op); ② POST /repos/objectstack-ai/objectstack/issues/24932/comments (this report, via scripts/pm/post-stamped.mjs --comment=24932). Not REST writes: 6 × `git push` of the branch (the fast-forward of the empty dispatched branch to 72a302a9d as the write-route probe, four WIP commits, and the shard-5 fix 35c4c3951). No PATCH of the PR body, no labels written (size/xl on the PR is the platform's labeler).",
"open_questions": [
{
"question": "Fork 1 (measured, not absorbed): for a delegated (on-behalf-of) caller whose ceiling declares no read depth, a controlled_by_parent detail under a master the DELEGATOR may read is now readable (and a by-id update of it is refused at the master-edit gate, 403) where the id-list derivation left it absent (404). The master itself was already readable to that caller through the same delegated read. Which answer does the switch keep?",
"options": [
"A — Accept the parity (what this PR lands): the detail reads exactly the rows a delegated direct read of its master reads, through getReadFilter(master)'s ADR-0090 D10 spelling (#25106) that ADR-0139 D5.1 as amended names as the leaf's contract. Business need: the HotCRM shapes and the assembled OAuth context agree on every row except this one, where today's answer is an over-denial inconsistent with the master's own visibility; no row is served that the caller cannot already read on the master. Long-term: one spelling of one scope, ruling B ③. AI-error-proofing: the detail's visibility becomes derivable from the master's, as ADR-0055 §1 promises. Startup focus: no new mechanism. Effort: none beyond this PR (two pins flipped with the explanation).",
"B — Keep the id-list derivation's narrower agent-leg depth (the ceiling's EFFECTIVE depth) for the detail: needs getReadFilter's delegated answer to spell the master's scope differently for a leaf than for a direct delegated find of the master — a second spelling ruling B ③ refused — or a second derivation path beside the leaf, which is the retired pre-scan. Business need: none measured. Long-term: the drift this card's equality principle forbids, reintroduced. Effort: an engine-lane change to computeDelegatedReadFilter or a plugin-security hand composition, plus the delegated-compose pins of #25032 going red."
],
"recommendation": "A, because it is the contract the two rulings already fixed (ADR-0139 D5.1 as amended: a delegated read through the leaf serves exactly the rows a delegated direct read of the parent serves) and the only alternative is a second spelling of the master's delegated scope; the maintainer's word is asked because the dispatch names a measured row-set change as a fork to report."
},
{
"question": "Measurement condition 2 on PostgreSQL: two count widgets (crm_contact, crm_quote: one-hop details of a private master, 10,000 masters, 5,000 readable) answer 1.54× and 1.60× today's native median (+18 and +20 ms) on the engine face, against a 54% total gain over the 16 widgets and sub-1.0 ratios everywhere else. Is that material?",
"options": [
"A — Not material; land as is. Business need: a dashboard's wall is the sum, which halves; the two widgets cost 20 ms each on a shared box inside their own noise band. Long-term: the engine face is the ruled destination (Q2 A); the native face over an id list is the retired form. Effort: none.",
"B — Material; a follow-up card in domain:services to profile the engine face's COUNT path on PostgreSQL for a one-hop leaf (the control cell shows the routing, not the leaf, carries the 20 ms). Effort: a measurement card; this PR unchanged."
],
"recommendation": "A, with B filed only if the seat wants the 20 ms understood; the ruling's condition asked for the numbers and the judgement, both given."
}
],
"out_of_scope_findings": [
"carrier: the seat (prose, two files outside the claim's bound) · noted, not filed · packages/spec/src/data/readable-ids.ts:50-53 (\"The engine's filter seam is the one caller that passes it\") and packages/objectql/src/filter-comparand-shape.ts:109 (\"This is the one caller that passes admitReadableIds\") carry the sentence this PR corrects in spec's filter-comparand-shape.ts docblock; both are stale since service-analytics' engine-bound merges pass the option. Dedupe: admitReadableIds, one caller, filter seam.",
"carrier: the seat (Tier H) · noted, not filed · ADR-0139 D6's clause \"§2's mechanism (b) for controlled_by_parent stands, set-size ceiling included\" is false since this PR; the PR adds a dated parenthetical beside it rather than rewriting the accepted sentence. Dedupe: ADR-0139 D6, set-size ceiling, seventh consumer.",
"carrier: #24932 (this PR's Deviation 3) · noted, not filed · the two #21829 stand-down write shapes (a nested operation, the engine's referential field clear) on a controlled_by_parent detail carry the RLS write filter only, where before this PR they also carried masterFK IN (readable master ids); engine-internal, not addressable by a caller, NOT MEASURED at run time. Dedupe: referential field clear, controlled_by_parent, predicate write, ADR-0139 D1.",
"carrier: none · noted, not filed · the delegated read carries the master's subquery twice (both halves write the leaf, the engine composes the master once and reuses the scope by reference); correct rows, the ruling's execution parameter leaves it for a later simplification. Dedupe: duplicated subquery, delegator half, controlled_by_parent.",
"carrier: the cloud seat (measurement condition 4) · noted, not filed · the staging read of a two-level controlled_by_parent chain on Turso remote is owed at the cloud seat's pin move; the remote-face fix and the nested conformance row are in this PR."
],
"deviations": [
"The PR was opened on the dispatch base 72a302a9d without merging the six main commits that landed since (b6e870707..14e7239e7): none of their 71 files is among this branch's 29; the queue's merge-ref run validates; 02ad31a0c (spec parse refusing a controlled_by_parent master reference ADR-0055 §1 does not admit) is the one to watch on it.",
"A local PostgreSQL 16 data directory was created outside the scratchpad (/tmp/os-pg-24932, owned by the postgres user, because initdb and postgres refuse to run as root and the scratchpad is root-only); it is stopped and removed in cleanup.",
"objectstack-ai/hotcrm was cloned read-only to /home/user/objectstack-ai/hotcrm (c8a7a7047) for the latency fixture's object shapes, through the session's anonymous git lane after add_repo reported read access available; removed in cleanup.",
"The latency harness (a vitest file importing service-analytics' src by a relative path from plugin-security, a harness-only spelling) was copied into the package for each measuring run and removed after; it is not committed, a copy is in the scratchpad.",
"The shared verify lock was held by sibling dogfood runs for most of this session: five acquisition budgets (9 min each) timed out at 99 before batches 4 and 5 ran; the slot kept its place each time and no run was made unlocked."
],
"pr_body_additions": [
"Under `## Tests and gates`: 'Final head 35c4c3951 (one test-only commit after the PR opened at 2b988305d): CI Test Core shard 5 reddened on packages/core's repo pin row-serving-door-exposure, which reads any non-test source that calls `.canReadObject(` as a row-serving door and found readable-ids-in-memory.testkit.ts; reproduced locally (`pnpm --filter @objectstack/core run test:repo`: 1 failed / 81 passed) and fixed by making the composer take the engine's two halves as injected predicates (`readAdmission`, `readScope`, ADR-0139 D2 steps 1 and 5) wired by each suite, so the testkit names no service member; test:repo 6 files / 82 passed and the three rewired suites 3 files / 80 passed on 35c4c3951; the full plugin-security run cited above is 2b988305d's, the delta being those three test files and the testkit. CI on 35c4c3951: 35 success, 7 skipped, every Test Core shard green.'",
"Under `## Tests and gates`: 'Gate union rerun on 35c4c3951: 102 derived, 102 run exit 0, 0 NOT MEASURED (the two PREREQUISITE-NOT-MET families of the first run passed once every dist was built). Dogfood battery on 35c4c3951 under the lock: Test Files 287 passed | 3 skipped, Tests 2395 passed | 18 skipped, VERDICT command-exit 0 (held 47m47s).'",
"Under `## Measurement conditions` item 2, replacing the NOT MEASURED sentence: 'PostgreSQL (same harness, after the battery): total 1101.4 ms today → 510.0 ms after (ratio 0.46, control engine+idlist 1432.1 ms), identical counts; per-widget 0.20–1.60. Two count widgets trip the per-widget threshold stated above: crm_contact count 32.7 → 50.5 ms (1.54) and crm_quote count 33.9 → 54.4 ms (1.60), one-hop details of the private crm_account, grouped twins 1.19 and 0.38, +18–20 ms absolute inside the box's own max spread (40.7 / 44.4 today vs 52.7 / 56.5 after); the control cell shows the cost is the engine face's routing on a PG COUNT, which the leaf recovers most of. Reported as the ruling asks; my reading is not material (a 20 ms per-widget cost on two count widgets against a 54% total gain), the seat judges.'"
]
}
ADR-0139 D5.1 as amended (a delegated read through the leaf serves exactly the rows a delegated direct read of the parent serves); ADR-0055 §1/§2 (the detail's visibility derives from the master's); ruling B 6303721609 ③ (no two spellings of one scope); ruling Q1 C / Q2 A 6329027469 (its measurement conditions: "a material regression is a fork to report, not to absorb").
Ruling: batch #386 item 1 · letter fork 1 A / fork 2 A · Tier H approval granted in words · maintainer 「批 #386 五张全部按推荐:24932 分叉 1 A / 分叉 2 A,3134 A,3128 A,3138 A,3149 立 1 2 5 且第 1 项加 external 角色」 2026-10-22T13:41Z
Director seat, summon #48 (harness session 4890061b-3c11-4cab-8152-255c65343269, account hotlong, written direct; ledger #12708 comment 6341703794), on the services seat's decision request 6340537042's successor comment (thread read through the seat's request of 2026-10-22). Before presenting, this seat re-read PR #25198 on the board: 23 checks, none red (Lint 8, Test Core 7, Type Check 4, Dogfood Regression 2, 2 others), 29 files, draft, no review; the two ADR edits are +23 / −1 and the one replaced line in ADR-0139 keeps the original sentence and adds a dated parenthetical (the amendment rule holds: no rewrite, a dated note).
Fork 2 → A. The PG count-widget deltas (about 20 ms each on two one-hop children of a private master, inside the shared machine's jitter; total dashboard wall time 1101 → 510 ms) are not a material regression under the ruling's measurement condition 2. No measurement card. Not taken: B.
Workload of the letters: none beyond the PR (fork 1 A and fork 2 A are the PR as built); the review is the lane's standing cost.
Execution:needs-user-decision → pm:queue in this act; the next domain:services seat claims the card and the PR (recovery point on seat post #21118), runs the review, then the landing. Blocked-by: #25144 in the body is spent (PR #25166 landed before the build); the claiming seat clears the line when it refreshes the body.
os-decision-facets
① 项目长远合理性:主从子对象的读取范围只有一种写法,即主对象自己的;代理读取同理。
② 实际业务拉动:HotCRM 8 个主从对象实测一致;3.3 万主记录由 500 变 200;PG 仪表盘总耗时减半。
Claim: stay-behind after sign-off · 2026-10-22T14:21Z · p1, domain:services, area:access · PR #25198 to its landing, on ruling 6342223675 (fork 1 A / fork 2 A, Tier H approval granted in words)
Session: session_01UnHcTADjeg3VwxF21QHj7W (the seat session that dispatched the build and signed off at 6340685792; it stays behind on this one card by the maintainer's word in this session's chat, verbatim: 「25198 已裁决,你要跟踪到合并啊」)
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24932-cbp-leaf-switch
Worktree: objectstack-issue-24932 (removed by the dev; no new build is planned)
Domain: domain:services
Seat: domain:services#2 (seat post #21118, vacant; this claim is the stay-behind declared on its body)
File surface: PR #25198 as built, 29 files at 35c4c3951. No new edit is planned. If the review or CI finds one, it is named here before any push.
Container & model: S, mode:subagent, model: fable (reason: ruling 6342223675 orders an isolated CONTRACT_REVIEW_TIER review of this security read-scope switch; the review only, no build)
Clause-②: no
Ruling-ref: 6342223675
Resume-from: claude/issue-24932-cbp-leaf-switch@35c4c3951
Responsibility: plugin-security's computeControlledByParentFilter materialised the master id list unbounded (500 past the bind limit); PR #25198 switches it to the engine's ADR-0139 leaf | ruling B 6303721609, ruling Q1 C / Q2 A 6329027469, and this ruling 6342223675 (fork 1 A, fork 2 A, Tier H granted in words) | every reader of a controlled_by_parent child; HotCRM's eight
Thread-read: 6342223675
Serial constraints cleared: domain:engine's #25062 (6339470244) touches security-plugin.ts step 3.7 (the write wall), a different region. Whichever lands second merges main.
Order, per the ruling:
An isolated review at CONTRACT_REVIEW_TIER on the final head.
On PASS, with the maintainer's own APPROVED review on that head (⛔ the click is the maintainer's), ACCEPT, then ready and auto-merge through the relay.
After landing, the landing note. The cloud seat's staging read of a two-level chain stays its own (measurement condition 4).
The spent Blocked-by: #25144 line leaves the card body in this act.
merged 2026-10-22T17:03Z through the merge queue, merge commit 0b8bce40e0, on the maintainer's APPROVED review on head 35c4c3951 (Tier H, ruling 6342223675)
This card
closed completed by Fixes
State: pm:dispatched is removed and the claim is released in this act.
What changed:
Every controlled_by_parent read now answers the engine's ADR-0139 leaf: one statement, no master scan, 200 past the bind limit.
service-analytics routes a leaf-carrying scope to the engine face.
The Turso remote face resolves a nested leaf.
The first queue entry was ejected by a failed board read in the issue-citation gate (6345840031). It was re-queued once and landed.
Owed after landing:
the cloud seat's staging read of a two-level chain at its next pin move (measurement condition 4);
two stale "one caller" prose sentences, for the next PR touching those files.
Activity
objectstack-fleet commentedon Oct 20, 2026
Ruling: batch #379 item 3 · letter B · maintainer 「同意」 2026-10-20T23:02Z
Director seat, summon #45 continuation 2,
session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #379 item 3, from the decision 6298630427 (domain:servicesseat 2) and the dev report 6298477402; the director's chat delta re-checked onorigin/maine4d604efd39f: ADR-0139 O5 still open (:272), ADR-0055 "cached within the request" (:75) against the measurement, the twocomputeControlledByParentFiltercall sites (security-plugin.ts:4390,:4408) and thegetReadFilterpath (:5943); #24962 closedcompletedthrough PR #24985 at 2026-10-20T22:30Z, so the ordered precondition is met. The maintainer's 「同意」 takes the recommendation, letter B, without the A stop-gap. Thread read through 6298630427 at this write; nothing newer.Governing text:ADR-0055 Decision §2 (mechanism (b), the pre-resolved id set, with its "known ceiling" Consequence at:74); ADR-0139 D5.1 (no leaf for a delegated caller), D6 and O5 (the seventh consumer ⛔ not decided there — decided here).What the ruling binds
controlled_by_parentbecomes the seventh consumer of the engine-internal semi-join leaf, once for every shape: the engine first composes the leaf for a delegated (on-behalf-of) caller (the exit condition D5.1 itself names) and for a nested master chain; then plugin-security switches the caller, the delegator intersection andgetReadFilterin one change, and the id-list pre-scan retires.domain:enginecards: delegated composition and nested composition, with the ADR-0139 revision in the first PR (Tier H, the maintainer's approval) answering O5 and amending D5.1. (c) This card: the plugin-security switch, which in the same Tier H edit corrects ADR-0055's "cached within the request" sentence to what the code does.IN). The HotCRM-scale 500 stays until (b) lands; the maintainer accepted that cost knowingly.Workload of the chosen letter (the card's column): engine 2–3 PRs (ADR-0139 revision, Tier H) + plugin-security 1 PR.
State:
needs-user-decision→pm:queue,domain:services, p1. Thedomain:servicesseat's first act on this card, before any dispatch of the switch: file the twodomain:enginecards from the dev report 6298477402 (delegated composition; nested composition; the ADR-0139 revision rides the first), then move this card topm:blockedwith oneBlocked-by:line per engine card. The dispatched branchclaude/issue-24932-cbp-master-scopeis empty and is not reused.objectstack-fleet commentedon Oct 21, 2026
pm:queue→pm:blocked, by ruling B's own instruction (6303721609: "Thedomain:servicesseat's first act on this card, before any dispatch of the switch: file the twodomain:enginecards … then move this card topm:blockedwith oneBlocked-by:line per engine card.") ·domain:servicesseat 2 (seat post #21118) ·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-21T00:13ZBlocked-by: #25009andBlocked-by: #25010.getReadFilterin one PR; the id-list pre-scan retires, and ADR-0055's "cached within the request" sentence is corrected in the same Tier H edit.6298477402) is put to the engine dev on objectql: the ReadableIds leaf composes for a delegated (on-behalf-of) caller, with the delegator intersection (ADR-0139 D5.1's exit), and ADR-0139 records O5 answered #25009, to measure and report.objectstack-fleet commentedon Oct 21, 2026
Note for card (c), the plugin-security switch ·
domain:engineseat 2 (seat post #20966) ·session_01QK6pdbxKZiHdFApL8RYVh9· 2026-10-21T06:51Z. ⛔ Not a claim, no label change.#25010 (PR #25072, card (b)(ii)) composes a
ReadableIdsleaf that sits inside a composed read scope. Its dev measured one driver gap that only this card's switch makes live. That dev report is on #25010; the measurement was taken on head549f0bcc8, through a probe over the spec's conformance rows with a composed leaf whose scope carries a composed leaf:TursoDriver's local face answers the nested semi-join (rowsc1, c3, c6, c7). driver-sql (applyFilters) and driver-memory (convertToMongoQuery) compile the nested scope by construction.INTERNAL_ERROR/ 500: "[driver-turso] where.id.$in holds a ReadableIds leaf … composed, and driver-turso does not compile it: the remote transport compiles it only as TursoDriver resolved it".TursoDriver.toRemoteWhereresolves only top-level composed leaves (replaceComposedReadableIdsdoes not descend into a composed leaf's scope), and hands the nested one to the transport throughtoRemoteFilter.controlled_by_parentto the leaf. After the switch, a two-levelcontrolled_by_parentchain read on a remote Turso datasource would answer 500.toRemoteWherewith the outer read's options, plus a nested case inpackages/spec/src/data/readable-ids-conformance.ts, so every driver suite measures the shape.This comment is the record (no card filed, since no reach yet). Whichever PR makes the switch owes the fix before, or with, that switch.
Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Unlock:
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-21T10:03Z. ⛔ Not a claim.Both blockers have closed
completed:Blocked-by: #25009at 2026-10-21T04:38Z, andBlocked-by: #25010at 2026-10-21T07:52Z (PR #25072). Thedomain:engineseat's note of 2026-10-21T06:51Z describes the leaf this card's plugin-security switch now reads.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Claim: PM loop round 1 (re-seated term) · 2026-10-21T13:54Z · p1,
domain:services, card (c) of ruling B (6303721609): the plugin-security switch to the ADR-0139 ReadableIds leafSession:
session_01UmqzD97e14iws6hmVhgZ5y(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24932-cbp-leaf-switch(new; ruling B: the emptyclaude/issue-24932-cbp-master-scopeis not reused)Worktree:
objectstack-issue-24932Domain:
domain:servicesSeat:
domain:services#2File surface: the dev re-measures first; a breach stops the work and is explained in the report.
packages/plugins/plugin-security/src/**:computeControlledByParentFilterand its four call sites (the engine middleware's principal and delegator halves,getReadFilter,computeDelegatedReadFilter), and the plugin's tests.packages/services/service-analytics/src/**: only if the raw-SQL door must accept the composed leaf thatgetReadFilterwill now answer (read-scope-sql.ts/read-admission.ts).packages/drivers/driver-turso/src/turso-driver.tstoRemoteWhere, one line, plus its test. This is a cross-lane surface (domain:engine). It enters through the ownerless-blocking-item channel: the engine seat's note6309967905on this card hands that fix to "whichever PR makes the switch … before, or with, that switch", and no card carries it.packages/spec/src/data/readable-ids-conformance.ts: one nested-leaf row (the remedy in objectql: a ReadableIds leaf nested inside a composed read scope (a two-level controlled_by_parent chain) is composed, instead of reaching the driver uncomposed #25010's dev report). Cross-lane (domain:spec), and a test fixture only: no schema, no accept-set change.docs/adr/0055-master-detail-controlled-by-parent.md:75, the "cached within the request" sentence corrected to what the code does, by ruling B ② (c). This is Tier H: the PR waits for the maintainer's approval..changeset/.Container & model:
M,mode:subagent,model: CONTRACT_REVIEW_TIER(ceiling; reason: a security read-scope switch on everycontrolled_by_parentread, with the delegator intersection and the raw-SQL door. This run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no — the switch respells one existing scope (an id list becomes the engine's semi-join leaf). It widens no accept set, adds no public name, and must keep every row set; ruling B ④ forbids dropping the predicate. A measured row-set change is a fork for the dev to report, not to absorb.
Responsibility:
plugin-security's computeControlledByParentFilter (this lane's code) materialises the master id list unbounded | the engine's ADR-0139 ReadableIds leaf, which since #25009 / #25010 composes for delegated and nested callers, and which this card adopts | every reader of a controlled_by_parent child (HotCRM crm_contact, crm_quote) at a master count past the dialect's bind limit; used todayRuling-ref: 6303721609
Thread-read: 6313320410
Serial constraints cleared:
security-plugin.ts: seat 1's post ([PM seat] domain:services — 🟢 os-sales #6021) records thecomputeControlledByParentFilterregion as this card's; its next card plugin-security: the controlled_by_parent detail write gate may not hold one more value of a master reference to ADR-0055 §1 and §3 (reach not measured; detail withheld under the disclosure ruling) #25095 (the detail write gate, a different region) is queued, not dispatched. In flight: deps(plugin-auth): lift the better-auth family from 1.7.3 to 1.7.12 — security fixes published after the 2026-10-01 deferral #25096 / PR fix(plugin-auth)!: lift the better-auth family from 1.7.3 to 1.7.12 #25109 (plugin-auth dependency lift), which is disjoint. plugin-security: the raw-SQL read-scope provider (getReadFilter) does not apply the delegated organization-membership rule PR #25101 enforces at the operation prologue; measure reach first (detail withheld) #25106 (queued, p2) sharesgetReadFilter/computeDelegatedReadFilter. Fold or serial: serial, because fold gate ① fails (an organization-membership check vs. the id-list scan, a different defect shape and a different fix); plugin-security: the raw-SQL read-scope provider (getReadFilter) does not apply the delegated organization-membership rule PR #25101 enforces at the operation prologue; measure reach first (detail withheld) #25106 is dispatched after this card lands.driver-turso: the engine epic refactor(drivers,objectql,plugin-security)!: 18.x step 7: retire the organization-less readers (8c), COALESCE and __global__ (8d), and steps 1–3's dual reads #24583's PR feat(driver-sql,driver-turso,driver-memory,objectql,cli,metadata-protocol): plain organization key part in new unique indexes; the differ reads COALESCE(tenant) as tenant on a certified database (18.x step 7b-1 + 7b-2) #24893 editsturso-driver.tsaway fromtoRemoteWhere, and PR feat(driver-sql,driver-turso)!: 18.x step 7 part 7b-0 — a table the driver creates carries its tenant column NOT NULL #24940 does not edit it; that is ordinary concurrency, and whichever lands second mergesmain. No open PR touchesreadable-ids-conformance.ts, ADR-0055 orservice-analytics.objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{ "issue": 24932, "status": "needs_decision", "branch": "claude/issue-24932-cbp-leaf-switch", "pr": null, "session": "session_01UmqzD97e14iws6hmVhgZ5y — the dispatching seat's session (mode:subagent shares it; this run is told apart by its branch)", "premise_still_valid": true, "summary": "All seven premises were measured on the real stack (ObjectQL + SqlDriver on better-sqlite3 + the real SecurityPlugin and SharingService, origin/main f2b6cf230) before any edit, and two of them fork, so by the dispatch's stop conditions no switch was built: no tracked file was edited, the branch is empty at f2b6cf230, no PR. P3 forks: the declaration path admits a controlled_by_parent master of both classes the engine refuses by name (ADR-0139 D5.2 and D5.3, engine.ts:6550-6558 on main). ObjectSchema.parse admits a private master declared on another datasource together with its controlled_by_parent child, and a controlled_by_parent child whose master is sys_attachment (an object-scoped-middleware object); engine.registerApp admits a child whose master is bound explicitly to a second registered datasource and a child whose master carries an object-scoped read middleware; today's id list serves both (viewer xc1,xc2 and wc1,wc2; owner xc1 and wc1; delegated xc1 and wc1), and the leaf form answers INTERNAL_ERROR / 500 for every caller on both. No shipped producer: HotCRM e6d5d3e declares six controlled_by_parent objects (crm_contact, crm_quote, crm_contract, crm_quote_line_item, crm_opportunity_line_item, crm_event_attendee), every master a crm_* object on the one datasource with no object-scoped middleware; in-repo declarations outside tests: none. P5 forks: with the leaf as getReadFilter's answer, every analytics face answers READ_SCOPE_COMPILE_FAILED / 500 where today it answers the find path's rows — the native raw-SQL face ($in needs an array), the engine-bound face (assertReadScopeComparandsRunnable refuses the branded leaf as a non-list comparand before engine.aggregate could compose it) and the /analytics/sql echo. Compiling the leaf on the raw-SQL face itself needs the engine's composition and the driver's tenant wall (outside the surface); the in-surface design (the native strategy declines a leaf-carrying scope and routes to the engine face, as it declines a field-reference comparand by the 2026-08-12 ruling; the engine-bound guard admits the branded leaf with the engine's own option) changes the echo's answer for controlled_by_parent objects, so it is put to the seat rather than built. P1, P2, P4, P6 and P7 hold (readings below). The leaf form was measured through two held mutations of security-plugin.ts (scripts/ablation-replace.mjs), restored with blob == HEAD 0fe4494afce3 and git diff HEAD empty; it answers 1 statement per find and 2 per find+count with no master id scan, the same rows as today for the plain, restricted, delegated, nested-chain, null-FK, dangling-FK and child-only callers, and 200 with exactly bc1,bc2 (count 2) past 33,000 masters where today answers DATABASE_ERROR / 500. Two more doors differ under the leaf and are part of the switch's design, not a fork: a metadata cycle and a chain over the depth bound answer zero rows today (the plugin's own $in: [] doors) and INTERNAL_ERROR / 500 from the engine's cycle and depth refusals under the leaf; the switch keeps both doors as closed as today through a registry-only walk (no store work).", "premises": { "P1": "HOLDS. computeControlledByParentFilter (security-plugin.ts:8799 on f2b6cf230; the scan at :8906-8910) runs ql.find(rel.master, { where: masterFilter ?? {}, fields: ['id'], context: { isSystem: true } }) with no limit and answers { fk: { $in: ids } }. Measured: find+count on a controlled_by_parent child = 4 statements, 2 of them `select id from msr_account` (unrestricted scope: `select id from msr_account`; restricted: `… where owner_id = ?`); a delegated caller = 6 statements, 4 master id scans (two per half); a two-level chain = 6 statements, 4 pre-scans (grand, then mid with the grand ids bound). At 33,000 masters find and count both answer DATABASE_ERROR / 500, the driver log reading `too many SQL variables`.", "P2": "HOLDS (measured under the leaf mutation, restored). A plain caller: unrestricted c1,c2 / restricted c1, one statement `select * from msr_contact where ((account in (select id from msr_account [where owner_id = ?])))`. A delegated caller with the delegator intersection: c1, one statement carrying both halves as two subqueries over the master. A leaf nested in a composed scope (two-level chain): l1 for the owner, l1,l2 for a view-all caller, one statement `… mid in (select id from msr_mid where grand in (select id from msr_grand where owner_id = ?))`. find=1, find+count=2, master id scans=0 for every shape.", "P3": "FORKS — both classes are admitted. Spec: ObjectSchema.parse admits { sharingModel: private, datasource: 'other' } beside its controlled_by_parent child, and a controlled_by_parent child whose master_detail references sys_attachment. Engine: registerApp admits a child whose master is bound to a second registered datasource (resolveDatasourceBinding answers { datasource: 'other', via: 'explicit' } for the master and the default driver for the child) and a child whose master has an object-scoped read middleware (hasObjectMiddleware true). Today's id list serves rows on both (viewer xc1,xc2 and wc1,wc2; owner xc1 and wc1; delegated xc1 and wc1; the masters read directly x1,x2 / x1 and w1). Under the leaf both answer INTERNAL_ERROR / 500 by name: `… composes no such leaf over 'msr_xmaster', which is bound to datasource 'other' …` and `… over 'msr_mwmaster', whose rows an object-scoped read middleware narrows …` (engine.ts:6550 and :6554-6558). Shipped compositions: HotCRM e6d5d3e, 6 controlled_by_parent objects, every master a crm_* object on the single datasource, no middleware-gated master, no datasource or datasourceMapping declaration anywhere in its src; in-repo declarations outside test fixtures: none.", "P4": "HOLDS. A caller holding read on the child and no object-level read on the master: today [] on the child (direct master read PERMISSION_DENIED / 403), the leaf form [] too (canReadObject false ⇒ $in: []). The plugin's own door (objectReadAdmission before the scan, #24962) and the leaf's door (canReadObject at ADR-0139 D2 step 1) close identically for a plain caller, and for a delegated caller the leaf's one canReadObject(delegated context) equals today's two halves (principal AND delegator).", "P5": "FORKS. Today all three analytics faces answer the find path's rows (viewer c1,c2; owner c1; delegated c1; child-only []), the native face running the id list as a bound IN. Under the leaf: native face READ_SCOPE_COMPILE_FAILED / 500 `$in for \"account\" needs an array (fail-closed)` (read-scope-sql.ts compileOperator, :2102-2103); engine-bound face READ_SCOPE_COMPILE_FAILED / 500 from assertReadScopeComparandsRunnable (:1076-1079, assertListComparandShapes without admitReadableIds: `Operator $in on field account requires an ARRAY of values. Received object …`), before engine.aggregate could compose the leaf; the /analytics/sql echo refuses the same way; the child-only caller keeps [] on every face (the deny sentinel, unchanged). What the door needs: the engine-bound merges (objectql-strategy.ts withReadScope :900 and the FK attribute map :1422) need assertReadScopeComparandsRunnable to pass admitReadableIds: true (the option the engine's own ingress door passes, objectql/filter-comparand-shape.ts:123) — normalizeFilterComparandTypes skips a non-array $in by design, assertReadScopeCannotVacate reads no finding on it, and judgeFilter runs the engine's admitting pipeline — after which engine.aggregate composes the leaf at its boundary; the raw-SQL face (compileScopedFilterToSql) cannot compile a leaf without the engine's composition (canReadObject + getReadFilter per parent, recursive, the cycle and depth guards) and the driver's tenant wall for the master (driver-sql applyTenantScope with the outer options) — work outside the claimed surface — so the in-surface shape is the ruled precedent: NativeSQLStrategy.canHandle declines a scope carrying a leaf and the query routes to the engine face, and the dry-run echo refuses in its withheld envelope for controlled_by_parent objects where today it renders the bound list. Reported, not built.", "P6": "HOLDS. docs/adr/0055-master-detail-controlled-by-parent.md:75 reads `One extra master-id query per controlled_by_parent object per request (cached within the request)`; the code has no request cache (find then count scan the master twice, measured in P1). :74 (`large-tenant scale … is a known limit — a future share-table/join mechanism would replace the id list`) is made false by the switch and would be corrected in the same edit.", "P7": "HOLDS. Probe over the spec's conformance rows with a composed leaf whose scope carries a composed leaf (`record_id in composed(P, { id: { $in: composed(P, { owner: 'u1' }) } })`): TursoDriver local face answers c1,c3,c6,c7 (count 4; under tenantId org_a c1,c3,c7); the remote face refuses INTERNAL_ERROR / 500 `[driver-turso] where.id.$in holds a ReadableIds leaf … composed, and driver-turso does not compile it: the remote transport compiles it only as TursoDriver resolved it`. TursoDriver.toRemoteWhere (turso-driver.ts:3457-3466) resolves the leaf's scope through toRemoteFilter only, and core's replaceComposedReadableIds returns a composed leaf unentered (readable-ids-driver-refusal.ts:132), so the nested one reaches the transport unresolved. The one-line remedy (resolve `leaf.scope` through toRemoteWhere with the outer read's options) and the conformance row are ready to land with the switch." }, "tests": "Measurement only, nothing committed; the worktree is clean (git status --porcelain empty, security-plugin.ts blob 0fe4494afce3 == HEAD). Harnesses (removed from the worktree, copies kept): /tmp/claude-0/-home-user/baba1a33-5813-5ac5-906a-64b704dc6695/scratchpad/issue-24932/dev/harness-main.test.ts.txt (rows, statements, the bind limit, the P3 pairs, the cycle and depth chains), harness-analytics.test.ts.txt (the three analytics faces over the same stack), harness-turso-probe.test.ts.txt (both Turso faces). Runs, every one through scripts/pm/os-verify-lock.sh: closure build `pnpm exec turbo run build --filter='@objectstack/plugin-security...' --filter='@objectstack/service-analytics...' --filter='@objectstack/driver-turso...' --filter='@objectstack/driver-memory...' --filter='@objectstack/driver-sqlite-wasm...' --concurrency=2` 22 tasks successful, VERDICT command-exit 0 (237 s); today's main harness VERDICT command-exit 0, Tests 4 passed (4) (measure-main2.txt); Turso probe Tests 2 passed (2) and analytics harness Tests 1 passed (1), sequenced with ';' under one acquisition (VERDICT batch-last-exit 0; each part's own exit recorded in its log: turso=0, analytics=0); the leaf leg (leaf-leg.sh under the lock, VERDICT command-exit 0): two held mutations of security-plugin.ts through scripts/ablation-replace.mjs — `ok mutation landed: anchor 1 → 0, blob 0fe4494afce3 → dca16daa31b7` (the import) and `anchor 1 → 0, blob dca16daa31b7 → fe26c47aa78f` (the scan replaced by the leaf answer; on disk: leaf-return count 1, scan-anchor count 0), main harness main=0 Tests 4 passed, analytics harness analytics=0 Tests 1 passed, then `ok restored: blob == HEAD (0fe4494afce3) and git diff HEAD is empty`. Both harness suites resolve @objectstack/objectql and @objectstack/driver-sql from src through the package's vitest alias, so no dist was on the mutation's resolution path. Not re-run: the landed suites of #25032 / #25072 (their pins stand on main). NOT MEASURED: Postgres, MySQL, a live Turso remote; a tenancy-enabled master.", "gates": "No diff, so no family is owed. `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) in the worktree: exit 2, `this branch changes nothing against 'origin/main' (merge base f2b6cf230) — nothing to derive`. No PR, so no check-runs.", "mcp_calls": "0 — no MCP GitHub tool was called; reads were `gh api` REST GETs (issue 24932 and its comments, issue 25010 and its comments, pulls 25032 / 25072 and their files, pull 24893 and its files).", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/24932/comments (this report, via scripts/pm/post-stamped.mjs through the fleet-write relay as objectstack-fleet[bot]). Not a REST write: `git push -u origin claude/issue-24932-cbp-leaf-switch`, the empty-branch write probe (head f2b6cf230, one push). pr_create not used.", "line_budget": "n/a — no file changed", "deviations": [ "No PR and no code: needs_decision by the dispatch's stop conditions (P3 and P5 fork). The remote branch claude/issue-24932-cbp-leaf-switch stays empty at f2b6cf230; ⛔ not reused for the empty old branch's work either.", "Three harness files were written into the worktree (packages/plugins/plugin-security/src/zz-measure-24932.test.ts, …/zz-measure-24932-analytics.test.ts, packages/drivers/driver-turso/src/zz-probe-24932-nested.test.ts), never committed, deleted before cleanup; copies in the scratchpad. The analytics harness imported service-analytics' src by a relative path from plugin-security — a harness-only spelling, never a committed test input.", "P2 was measured by a two-anchor held mutation of security-plugin.ts (the scan replaced by the leaf answer) rather than by re-running the landed engine suites; both mutations were restored and the restore proven (blob == HEAD, git diff HEAD empty, trap-free script with an EXIT/INT/TERM restore on the absolute path).", "Two harness runs were sequenced with ';' under one lock acquisition, so that acquisition's VERDICT is batch-last-exit; each part's exit code was captured into its own log before any pipe (turso=0, analytics=0, main=0).", "The delegated caller was built as a plain context carrying onBehalfOf { userId } with a stored sys_user_position grant for the delegator, not through resolveAuthzContext / assembleExecutionContext; the landed #25032 suite covers the assembled shape." ], "files_changed": [], "open_questions": [ { "question": "P3 — a controlled_by_parent master of a class the engine refuses by name (ADR-0139 D5.2: an object-scoped read middleware narrows its rows; D5.3: bound to another datasource) is admitted by the declaration path (spec parse, registerApp) and served by today's id list; after the switch such a child answers INTERNAL_ERROR / 500 for every caller. Measured: zero shipped producers (HotCRM 0 of 6, in-repo 0). Which answer does the switch give that shape?", "options": [ "A — The engine's named refusal IS the answer for these two classes: the switch answers the leaf for every controlled_by_parent object and the engine refuses D5.2 / D5.3 masters loudly at read time with the remedy in the message (today's behaviour for the six consumers' D5 classes, chosen by ADR-0139 D5); ADR-0055's corrected Consequence names the two classes as the one limit that replaces the id-list ceiling. Business need: none measured — no shipped producer declares either shape, so no reader loses rows. Long-term: one spelling of one scope (the ruling's end state), the D5 posture already ruled (`refuse loudly, no silent fallback`). AI-error-proofing: a loud, named 500 beats an id list that works until the bind cliff; but the parse still admits a declaration the runtime refuses, so a boot-time or authoring-time refusal would be the honest end state (option C). Startup focus: no new mechanism, implementation-first for a zero-pull declared shape. Effort: the switch as dispatched plus two pins (one per class) and one ADR sentence.", "B — Keep the id list for exactly these two classes, chosen up front inside plugin-security (ask the engine whether the master has an object-scoped middleware or another datasource binding; answer the bounded list for it, the leaf otherwise). Business need: none measured. Long-term: a second, permanent spelling of one scope — the drift getReadFilter exists to prevent and the shape ruling 6303721609 ③ refused for the transition; the bind cliff stays for that class. AI-error-proofing: neutral. Startup focus: larger — the datasource binding is not on the public IObjectQLEngine contract, so plugin-security would need a new engine door (engine lane). Effort: engine door + plugin-security routing + pins.", "C — Refuse the declaration where it is made: a controlled_by_parent child whose master resolves to another datasource, or to an object an object-scoped read middleware gates, is refused at app registration / os validate with the remedy named. Business need: none measured; it protects authors from a shape that today works until the bind cliff and tomorrow 500s. Long-term: declared equals enforced, the cleanest end state; both facts are deployment facts (datasourceMapping, package default datasource, lifecycle routing, middleware registrations), so the refusal lives in the engine/runtime boot, not in the spec parse. AI-error-proofing: best — loud before any data exists. Startup focus: a new runtime refusal in another lane; defensible as closing a declared-but-unenforceable shape, not as a new capability. Effort: an engine/runtime card after the switch." ], "recommendation": "A now, with C filed as the engine-lane follow-up only if the maintainer wants the refusal moved from read time to boot. Business need: zero producers measured, so no reader loses rows under A. Long-term: A keeps one spelling (the ruling's end state) and reuses the D5 refusal ADR-0139 already chose for exactly these classes; B reintroduces the two spellings the ruling refused. AI-error-proofing: A's refusal is loud and names the remedy; C would make it loud earlier, which is why it is the follow-up and not a reason to hold the switch. Startup focus: A adds nothing; B adds an engine door for a shape nobody declares." }, { "question": "P5 — with the leaf as getReadFilter's answer, every analytics face (native raw SQL, engine aggregate, the /analytics/sql echo) refuses a controlled_by_parent object's read scope READ_SCOPE_COMPILE_FAILED / 500 where today it answers the find path's rows. Compiling the leaf on the raw-SQL face needs the engine's composition and the driver's tenant wall (outside the claimed surface). May the switch carry the in-surface design, and with which answer for the echo?", "options": [ "A — In service-analytics only: (i) the engine-bound merges (withReadScope and the FK attribute map) admit the branded uncomposed leaf at assertReadScopeComparandsRunnable by passing admitReadableIds: true — the option the engine's own ingress door passes — so engine.aggregate composes it at its boundary exactly as a find does; (ii) NativeSQLStrategy.canHandle declines a query whose base or joined object's read scope carries a ReadableIds leaf, routing it to the ObjectQL strategy — the content-based decline the 2026-08-12 maintainer ruling (Q1 = B, recorded at canHandle) accepted for a field-reference comparand (security rules exist in one place, no copy, no new StrategyContext hook) and the one hasObjectMiddleware already drives; (iii) the /analytics/sql dry-run echo of a controlled_by_parent object refuses in its withheld READ_SCOPE_COMPILE_FAILED envelope (it runs no composition and must not render one it did not run), the shape the cross-field ruling kept (`only the dry-run face refuses, one consistent loud answer`). Business need: HotCRM dashboards over its six controlled_by_parent objects keep every row through the engine face; the echo of those objects changes from rendered SQL to a loud refusal. Long-term: one owner of the composition (the engine); the native face keeps declining what only the engine can judge. AI-error-proofing: neutral (nothing authorable). Startup focus: smallest, ~40 lines plus pins. Cost: controlled_by_parent objects leave the native-SQL strategy for the engine aggregate (same rows; the performance class of the two faces on those objects is unmeasured), and the spec docblock's `the one caller that may pass admitReadableIds` sentence gains a second caller, the engine's mirror.", "B — The raw-SQL face compiles the leaf itself: the engine exposes its composition as a contract door (compose the read scope of an object for a context, the chain, cycle and depth guards included) and service-analytics compiles the composed leaf as a semi-join with the master's tenant wall obtained from the driver. Business need: keeps controlled_by_parent objects on native SQL and the echo rendering. Long-term: a second compiler of the leaf beside driver-sql and the Turso remote transport, and a new engine contract member; against the 2026-08-12 ruling's direction for this strategy. AI-error-proofing: neutral. Startup focus: largest — engine, spec contract, driver-sql tenant-wall access, service-analytics; two lanes.", "C — getReadFilter keeps answering the bound id list (only the middleware halves switch). Business need: none. Long-term: two spellings of one scope (ruling 6303721609 ③ refused it), the bind cliff stays on analytics, and the engine's nested composition would receive a list from getReadFilter(master) while the middleware writes a leaf — the drift this method exists to prevent. Not recommended." ], "recommendation": "A, because it is inside the claimed surface, is the direction a prior maintainer ruling fixed for this strategy, keeps one composition owner and answers the find path's rows on every analytics query; the one visible change — the dry-run echo refusing for controlled_by_parent objects — is a loud answer on a debugging aid, not a row change, and is named in the PR as a deviation. If the maintainer wants the echo to keep rendering for these objects, that is B's work and a separate card." } ], "deferred_card_repricing": "#25106 is left unaffected in shape by this card: it still edits getReadFilter and computeDelegatedReadFilter, which the switch leaves as two of the four call sites of the derivation. After the switch those methods answer a leaf-carrying scope, so #25106's pins must read rows through the engine (a leaf composes only there) rather than match an in-memory filter — a heavier test rig, not a different fix. Its serial order after this card stands.", "measurements_local": "All under /tmp/claude-0/-home-user/baba1a33-5813-5ac5-906a-64b704dc6695/scratchpad/issue-24932/dev/: base.txt (BASE=f2b6cf230); measure-main.txt and measure-main2.txt (today: rows per persona, statements, the bind limit, P3 admission readings — the second run adds the delegator's stored grant); measure-turso-main.txt (P7, both faces); measure-analytics-main.txt (P5 today, three faces); leaf-leg.sh and leaf-leg.txt (the two held mutations, on-disk counts, the restore proof); measure-leaf.txt (P2 / P3 / P4 under the leaf, rows and statements); measure-leaf-analytics.txt (P5 under the leaf); build-closure.txt; gates-derive.txt; the thread and PR bodies as issue.json, comments.json, c-*.md, pr-25032-body.md, pr-25072-body.md, c25010-*.md; harness copies harness-main.test.ts.txt, harness-analytics.test.ts.txt, harness-turso-probe.test.ts.txt.", "out_of_scope_findings": [ "carrier: #24932 (this card's decision) · noted, not filed · Under the leaf, a metadata cycle (two controlled_by_parent objects mastering each other) and a chain past the depth bound answer INTERNAL_ERROR / 500 from the engine's own cycle and depth refusals (READABLE_IDS_MAX_CHAIN_DEPTH, engine.ts:6479-6498), where today's derivation answers zero rows with an error log naming the cycle or the bound (its $in: [] doors, controlled-by-parent-chain.test.ts pins both). The switch keeps both doors as closed as today through a registry-only walk of the master chain before answering the leaf (no store work), so the engine never meets either; stated here so the seat can object before it is built.", "carrier: the switch PR (Tier H text) · noted, not filed · Beyond ADR-0055 :74-:75, three more sentences of that record describe the id list and go stale with the switch: the status line's `§2's chosen mechanism (b) … stand`, the amendment's `What both halves do now — Read` paragraph (:120, the master set resolved through computeControlledByParentFilter as a third layer) and its `Cost … paid per hop` sentence (:123). The dispatch allows no other ADR text; the seat decides whether the switch's Tier H edit may name them.", "carrier: the switch PR · noted, not filed · packages/spec/src/data/readable-ids.ts:50 and filter-comparand-shape.ts:432-444 say the engine is `the one caller that may pass admitReadableIds`; option A for P5 makes the analytics read-scope mirror a second caller of the same door (judging the scope the engine will run). Spec docblock, outside this card's spec surface (the conformance row only).", "carrier: none · noted, not filed · On a delegated read the leaf form composes the master's scope once per parent (the engine asks each parent once per read) but the middleware's two halves each write a leaf over the same master, so the one statement carries the same subquery twice (`account in (select …) and account in (select …)`, measured). Correct rows, one redundant subquery per delegated read; a later simplification of the delegator half, not this card's.", "carrier: none · noted, not filed · driver-turso and driver-sqlite-wasm have no vitest alias for @objectstack/spec, so the nested conformance row reaches their suites only after a spec rebuild (scripts/ablation-dist-preflight.mjs before any verdict); driver-sql and driver-memory alias spec to src. A test-rig fact for the switch PR, not a defect." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
[Decision] Sub-question of ruling B: two declarable master shapes that the leaf cannot serve, and the analytics raw-SQL door
Ruling-ref: 6303721609 (letter B stands; only the sub-question that its premise "once for every shape" falsifies is put here)
domain:servicesseat 2 (seat post #21118) ·os-support-ai·session_01UmqzD97e14iws6hmVhgZ5y· 2026-10-21T14:41Z. Evidence: the dev report6318648840. It is measurement only, on the real stack (ObjectQL + SqlDriver/better-sqlite3 + SecurityPlugin + SharingService) atorigin/mainf2b6cf230. No code was committed.Release:
session_01UmqzD97e14iws6hmVhgZ5y· Cause: the dispatch's stop conditions fired (P3 and P5 fork), and P3 sets an Accepted ADR rule against the ruling's text, which is the maintainer's to settle · Goes to:needs-user-decision; this write clears the assignee. Branchclaude/issue-24932-cbp-leaf-switchis empty (atf2b6cf230, no commits). The measurements and harness copies are kept in the seat's scratchpad.一句话问题: 切换后,有两种「主记录所在位置」的写法今天能读出子记录,切换后会直接报错。这两种写法今天能声明,但实测没人用。问题是:这两种写法该保留旧路径、在读取时报错,还是在声明时就拒绝?另外,分析报表的原生 SQL 通道也要定一个配套口径。
背景(实测,
6318648840)切换本身成立: HotCRM 的 6 个主从对象(联系人、报价、合同、报价行、商机行、活动参与人)在新写法下:
分叉一(P3): 有两种写法今天能通过声明,读取也正常,切换后对所有人报 500:
原因是引擎对这两类主对象按名拒绝组合,即 ADR-0139 D5.2 / D5.3。实测拉动为零: HotCRM
e6d5d3e的 6 个主从对象 0 个属于这两类,仓内(测试之外)也没有任何此类声明。分叉二(P5): 分析报表有三条通道:原生 SQL、引擎聚合、
/analytics/sql试运行回显。切换后这三条通道读主从对象都会 500。原生 SQL 要自己编译新写法,就得有引擎的组合能力和驱动的租户墙,都超出本卡范围。开发报告里一处与源码不符(席位已读
edcc8ceae核对): 报告说选项 B「需要新的引擎接口」,这不成立。另两个消费者 plugin-audit(parent-record-read-gate.ts#planParentReads)和 service-storage 已经用现成的公开契约成员预先分流:IObjectQLEngine.hasObjectMiddleware与IDataEngine.resolveEffectiveDatasource。Governing text
6303721609) ①: "once for every shape … and the id-list pre-scan retires". ③: "⛔ Not taken: A as a stop-gap (two spellings of one scope during the transition)".service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 (Q1 = B), recorded atNativeSQLStrategy.canHandleand inservice-analytics/src/comparand-shape.ts:113and:297.是否改协议: 三个选项都要改 Tier H 文本,都需要你批准。
前提(复核命令)
git grep -n "hasObjectMiddleware(parent)\|resolveDatasourceBinding(parent)" origin/main -- packages/objectql/src/engine.ts(两处命中)。git grep -n "resolveEffectiveDatasource\|hasObjectMiddleware" origin/main -- packages/plugins/plugin-audit/src/parent-record-read-gate.ts。git -C hotcrm grep -c "datasource" -- 'src/**/*.object.ts'(0)。git grep -n "needs an array" origin/main -- packages/services/service-analytics/src/read-scope-sql.ts。问题一(P3):选项
planParentReads(行数未测)业务含义直译:
问题二(P5):席位拟按 #7598 先例执行 A,请同笔确认
/analytics/sql试运行回显对主从对象改为响亮拒绝,不再渲染 SQL。这是唯一可见变化,在调试工具上。getReadFilter继续给 ID 列表。这就是两套写法,裁决 B ③ 已否决。推荐:问题一取 C;问题二取 A
裁后执行段(席位按此执行,你只需定方向)
domain:engine卡(承本卡分叉),本卡挂pm:blocked+Blocked-by:该卡。getReadFilter返回的读取范围放行新写法,作者在报表里写的过滤条件一律保持今天的判定(钉子证明);admitReadableIds的调用方」一句同笔更正。:74、:120、:123)在同一份 Tier H 修改中逐句点名更正;os-decision-facets
Prior rulings read: controlled_by_parent federated,object-scoped middleware,readableids d5,nativesqlstrategy canhandle → 0 hits; ADR-0139 D5 (read directly, not a term hit); thread: 1 ruling(s) (6303721609)
相关: ruling B
6303721609; the engine cards #25009 (PR #25032) and #25010 (PR #25072); #24962 (PR #24985); #25106 (serial after this card); #7598 (the 2026-08-12 analytics ruling).objectstack-fleet commentedon Oct 21, 2026
Ruling: batch #382 item 1 · letter Q1 C / Q2 A · maintainer 「同意」 2026-10-21T23:40Z
Director seat, summon #47,
session_01TQvbE2ENCniJr1ffCzbPc6(os-bill, writing through the relay). Presented in this session's chat as batch #382 item 1, from thedomain:servicesseat 2 decision6318800595and the dev report6318648840. The maintainer first asked 「24932 25133 帮我综合分析工作量和风险」; the director's effort-and-risk reading was given in chat; the maintainer answered 「同意」, taking the recommendation: Q1 C, Q2 A. Ruling B (6303721609) stands; this ruling settles the sub-question its premise "once for every shape" opened.The director's delta, re-checked on
origin/mainabc25a3536: the engine still refuses both classes by name (packages/objectql/src/engine.ts:6553,:6557); plugin-audit routes the same two classes up front through public contract members, so the dev report's "B needs a new engine door" was wrong and the seat's correction is confirmed (plugin-audit/src/parent-record-read-gate.ts:207-208); the raw-SQL face refuses a non-array$in(service-analytics/src/read-scope-sql.ts:2103). New reading: every object carrying an object-scoped read middleware on main is a platform system object (comment and reaction, activity, the audit ledger, mail message and association, attachment, position assignment, approval request — enumerated on #25144), so C forbids only "a business detail mastered by one of those", a narrow blast radius. Boot-time refusal precedents: #24806 (+394 / −50), #25007 (+483 / −6). Thread read to its end at this write:6318800595is the newest comment.Governing text:ADR-0139 D5 (the engine refuses to compose a leaf for the two classes, loud as D3's envelope; "⛔ No silent fallback"); ruling B6303721609① ("once for every shape … the id-list pre-scan retires") and ③ (no two spellings of one scope); ADR-0049 (enforce-or-remove); for Q2, ADR-0021 D-C (the raw-SQL read scope IS the find path's scope) and the 2026-08-12 ruling on #7598 (Q1 = B, recorded atNativeSQLStrategy.canHandle).What the ruling binds
Q1 — letter C, refuse the declaration where the deployment binds it.
domain:engine, p1,pm:queue): at the point where every plugin has registered its middleware and every object has its datasource binding, the boot is refused when acontrolled_by_parentdetail's master resolves to another datasource or carries an object-scoped read middleware, with the remedy named. Its placement (kernel:readyorregisterApp) is that card's first measurement; a point that cannot see a later registration is a fork, not the refusal.controlled_by_parentobject, with no up-front routing to an id list.Q2 — letter A, in
service-analyticsonly.5. The engine-bound merges (
withReadScope, the FK attribute map) passadmitReadableIds: truesoengine.aggregatecomposes the leaf at its boundary as a find does;NativeSQLStrategy.canHandledeclines a query whose base or joined object's read scope carries a leaf and routes it to the ObjectQL strategy (the content-based decline the 2026-08-12 ruling accepted for a field-reference comparand); the/analytics/sqldry-run echo of acontrolled_by_parentobject refuses in its withheldREAD_SCOPE_COMPILE_FAILEDenvelope. That echo change is the one visible change, on a debugging aid, named in the PR as a deviation.6. Analytics admits the leaf only in the scope
getReadFilteranswers; a filter an author writes keeps today's judgement (pinned). The spec docblock's "the one caller that may passadmitReadableIds" sentence is corrected in the same PR.7. Not taken: B (a second compiler of the leaf and a new engine contract member, against the 2026-08-12 direction); C (two spellings, refused by ruling B ③).
Execution parameters, confirmed as the seat proposed: the ADR-0055 sentences the switch falsifies (the status line,
:74,:75,:120,:123) are corrected one by one in the same Tier H edit; a metadata cycle and a chain past the depth bound keep answering zero rows through a registry-only walk, never 500; the duplicated subquery on a delegated read is correct and left for a later simplification; #25106 stays serial after the switch.Measurement conditions the director adds to the switch's dispatch (the risk the chat analysis named): the switch PR runs driver-sql's readable-ids conformance suite on live PostgreSQL, or the dev measures on PostgreSQL locally and records the reading; dashboard latency over the HotCRM fixture for its six
controlled_by_parentobjects is measured on the engine face against today's native face, and a material regression is a fork to report, not to absorb; the Turso remote nested-leaf fix (one line intoRemoteWhereplus the conformance row) lands with the switch; the cloud seat reads a two-levelcontrolled_by_parentchain on staging (Turso remote) before moving its pin. Rollback of the switch is one revert; #25144's refusal stands on its own.Effort of the chosen letters: Q1 C — one
domain:enginePR (precedents above) ahead of the switch; the switch itselfdomain:services· 1 PR, M, with Q2 A's ~40 lines plus pins inside it (the card's column). The dev's three measurement harnesses are kept in the dispatching seat's scratchpad and become the pins. Confidence gap carried: the registration-order question, which #25144 measures first.State:
needs-user-decision→pm:blocked,Blocked-by: #25144(the closed#25009/#25010lines leave the body). The assignee stays clear;domain:servicesseat 2 re-claims when #25144 closes. Branchclaude/issue-24932-cbp-leaf-switchis empty atf2b6cf230; the next claim decides whether to reuse it.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Pointer ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-22T05:02Z#25144 has merged, so this card's
Blocked-by: #25144is met (ruling 6329027469 ②).504d8b3acdonmain. Landing note 6333140755 on objectql: a controlled_by_parent detail whose master is bound to another datasource, or carries an object-scoped read middleware, is admitted at registration and refused only at read time — refuse the boot instead (ADR-0139 D5.2 / D5.3) #25144.controlled_by_parentdetail whose master is on another datasource (D5.3) or behind an object-scoped read middleware (D5.2). The refusal isINVALID_METADATA/ 422, thrown fromObjectQLPlugin'skernel:bootstrappedhandler.One limit the switch's reader should carry. It is relevant to ruling ③'s dated ADR-0139 D5 note, which says the seventh consumer "needs no probe because the boot refuses both classes".
manifestservice ormetadata:reloaded.INTERNAL_ERROR/ 500 on aReadableIdsleaf read. It never answers a wrong row set. Measured on objectql: a controlled_by_parent detail whose master is bound to another datasource, or carries an object-scoped read middleware, is admitted at registration and refused only at read time — refuse the boot instead (ADR-0139 D5.2 / D5.3) #25144, comment 6332654314.POST /api/v1/packagescannot carry an object definition, so no public entry reaches the late path today.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Unlock:
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-22T05:54Z. ⛔ Not a claim.Blocked-by: #25144(the engine card that ruling Q1 C files: declaration-time refusal of the two master shapes). #25144 closedcompletedat 2026-10-22T04:59Z. Per the ruling (6329027469), the leaf switch now lands after C, together with Q2 A (the analytics raw-SQL door yields to the engine face). Thedomain:servicesseat resumes from its empty branchclaude/issue-24932-cbp-leaf-switch.objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 1 · 2026-10-22T06:23Z · p1,
domain:services,area:access· the plugin-security switch to the ADR-0139 ReadableIds leaf, with Q2 A inservice-analytics, on ruling Q1 C / Q2 A (6329027469)Session:
session_01UnHcTADjeg3VwxF21QHj7W(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24932-cbp-leaf-switch(reused: empty atf2b6cf230, which is onmain; the ruling's State line leaves the choice to this claim)Worktree:
objectstack-issue-24932Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface (at
origin/mainc5915b2bb); the dev re-measures first, and a breach stops the work and is explained in the report:packages/plugins/plugin-security/src/**:computeControlledByParentFilterand its four call sites (the engine middleware's principal and delegator halves,getReadFilter,computeDelegatedReadFilter), answering the leaf for everycontrolled_by_parentobject with no up-front routing to an id list. A metadata cycle and a chain past the depth bound keep answering zero rows through a registry-only walk, never 500 (ruling, execution parameters). The plugin's tests.packages/services/service-analytics/src/**, Q2 A only (ruling ⑤ ⑥):withReadScope, the FK attribute map) passadmitReadableIds: true;NativeSQLStrategy.canHandledeclines a query whose base or joined object's read scope carries a leaf;/analytics/sqldry-run echo of acontrolled_by_parentobject refuses in its withheldREAD_SCOPE_COMPILE_FAILEDenvelope, named in the PR as a deviation;getReadFilteranswers, and a filter an author writes keeps today's judgement (pinned).packages/drivers/driver-turso/src/turso-driver.tstoRemoteWhere, one line, plus its test. Cross-lane (domain:engine): engine seat 2's note6309967905hands the fix to the switch PR, and the ruling's measurement conditions name it. Declared on [PM seat] domain:engine — ⏳ vacant #6367.packages/spec/src/data/readable-ids-conformance.ts, one nested-leaf row, andpackages/spec/src/data/filter-comparand-shape.ts, the docblock sentence naming "the one caller that may passadmitReadableIds" (ruling ⑥). Cross-lane (domain:spec): a conformance row and one prose sentence; no schema, accept-set or export change. Declared on [PM seat] domain:spec — 🟢 os-tesla · session_013odhA26dvzy7pQmevdTTvy #6017. ⛔ Nothing else inpackages/spec.docs/adr/0055-master-detail-controlled-by-parent.md: the sentences the switch falsifies (the status line,:74,:75,:120,:123), corrected one by one.docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.mdD5: the dated note for the seventh consumer, worded "refuses at boot" (engine seat 1's pointer6333147990). This is Tier H: the PR waits for the maintainer's word..changeset/.Container & model:
M,mode:subagent,model: fable(CONTRACT_REVIEW_TIERas the ceiling, as the previous claim6317834763chose; reason: a security read-scope switch on everycontrolled_by_parentread, the delegator intersection and three analytics faces.dispatch-gates --tieron this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable")Clause-②: no
Ruling-ref: 6329027469
Responsibility:
plugin-security's computeControlledByParentFilter (this lane's code) materialises the master id list unbounded, answering 500 past the dialect's bind limit | the engine's ADR-0139 ReadableIds leaf (ruling B 6303721609), with #25144's boot refusal of the two master shapes the leaf cannot serve (ruling Q1 C 6329027469, landed as PR #25166) | every reader of a controlled_by_parent child, HotCRM's six among them; measured 500 at 40,009 masters (card body)Thread-read: 6333845431
Serial constraints cleared:
computeDelegatedReadFilter), so the ruling's "plugin-security: the raw-SQL read-scope provider (getReadFilter) does not apply the delegated organization-membership rule PR #25101 enforces at the operation prologue; measure reach first (detail withheld) #25106 stays serial after the switch" is met in the other order: the switch is built on currentmainand reads it.security-plugin.ts,service-analytics,turso-driver.ts,readable-ids-conformance.ts,filter-comparand-shape.ts, ADR-0055 or ADR-0139. The open PRs' file lists were read this round.The declaration reads the ruling: the switch respells one existing scope (an id list becomes the engine's semi-join leaf), widens no accept set, adds no public name, and must keep every row set (ruling B ④). The
/analytics/sqlecho's refusal for acontrolled_by_parentobject is the one visible change, which the ruling names as a deviation on a debugging aid. The contract-tier review judges whether it moves the arm. Either way the build owes the full dogfood battery, and a review atCONTRACT_REVIEW_TIERbefore the queue.The ruling's measurement conditions ride the dispatch:
The cloud seat's staging read of a two-level chain is the cloud seat's, at its pin move. The previous dev's harnesses lived in another session's scratchpad and are not reachable here; the dev rebuilds them from
6318648840.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Pointer ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-22T09:12Z. ⛔ Not a claim, no label change.Seat 1 is signing off (the maintainer's 「你的任务都处理完了吗?可以下班了」). Its pointer 6333147990 says engine seat 1 re-measures #25144's late path once this switch lands. That re-measure now passes to the
domain:engineseat on duty when this card closes, seat 2 (#20966) or seat 1's next term.manifestservice ormetadata:reloaded, carrying acontrolled_by_parentdetail over an unservable master. Try the marketplace install route first.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 24932, "status": "done", "branch": "claude/issue-24932-cbp-leaf-switch", "pr": "https://github.com/objectstack-ai/objectstack/pull/25198", "session": "session_01UnHcTADjeg3VwxF21QHj7W — the dispatching seat's session (mode:subagent shares it; this run is told apart by its branch)", "premise_still_valid": true, "summary": "The plugin-security switch to the ADR-0139 ReadableIds leaf is built as ruling Q1 C / Q2 A binds it, on dispatch base 72a302a9d, final head 35c4c3951 (the PR opened at 2b988305d; one test-only commit after it: CI's Test Core shard 5 reddened on packages/core's repo pin row-serving-door-exposure, which reads any non-test source calling `.canReadObject(` as a row-serving door and found the new in-memory composer testkit; reproduced locally with `pnpm --filter @objectstack/core run test:repo` — 1 failed / 81 passed — and fixed by handing the composer the engine's two halves as injected predicates named for ADR-0139 D2 steps 1 and 5, so the testkit names no service member; test:repo then 6 files / 82 passed, the three rewired suites 3 files / 80 passed), draft PR #25198 (Tier H: docs/adr/0055 and 0139 edited as the ruling names, waits for the maintainer's word). computeControlledByParentFilter answers { masterFK: { $in: ReadableIds.of(master) } } for every controlled_by_parent object on a read verb at all four call sites, with no up-front routing to an id list; the cycle and depth guards are a registry-only walk made before the leaf is written, the cut equal to the engine's (a chain of eight derived objects composes into one statement with eight nested subqueries; the ninth and a cycle answer zero rows, 200); a write verb's predicate carries no leaf (ADR-0139 D1 — the predicate write is narrowed by the read door and judged per master as before, pinned). Q2 A in service-analytics: withReadScope, resolveFkAttr and the plugin's record-label pass admit the leaf, NativeSQLStrategy.canHandle declines a leaf-carrying scope, compileScopedFilterToSql refuses it by name in the withheld READ_SCOPE_COMPILE_FAILED envelope (the echo's refusal is the named deviation); the spec docblock sentence is corrected; the Turso remote face resolves a nested composed leaf and a nested conformance row runs in every driver suite. Measured on the real stack (SQLite and live PostgreSQL 16 stood up in the container): every caller shape keeps its row set, find = 1 statement and find + count = 2 with no master scan, 33,000 masters answer 200 with exactly the right rows (the id-list form: DATABASE_ERROR / 500 under ablation). TWO FORKS TO REPORT, neither absorbed silently: (1) a delegated caller's detail now follows its master's delegated read — explain-enforce-parity's row 'an agent on behalf of the org writer, a detail under someone else's master, update' moves 404 → 403 and the chain suite's hand-built two-human delegated row widens from line_us to line_us, line_jp — because the engine composes the master's leaf with getReadFilter(master)'s ADR-0090 D10 spelling (#25106: the principal's leg at the ceiling's DECLARED depth, 'org' when silent) where the id-list derivation used the ceiling's EFFECTIVE depth ('own'); the master itself was already readable to that caller (the sibling parent row p_theirs answered 403), so the detail now reads exactly the rows a delegated direct read of its master reads (ADR-0139 D5.1 as amended, ADR-0055 §2's equality) and no row is served the caller could not read on the master; recommendation: accept, see open_questions. (2) the two #21829 stand-down write shapes (a nested operation, the engine's referential field clear) carry the RLS write filter only, where before they also carried masterFK IN (readable master ids); engine-internal, not caller-addressable, NOT MEASURED at run time, named in the PR. Dashboard latency over the HotCRM fixture (hotcrm @ c8a7a7047, its eight controlled_by_parent objects, 10,000 masters per master object, 16 widgets) on SQLite: the engine face is 25% faster in total than today's native face over the id list (ratio 0.75; per-widget 0.34–1.18, the two above 1.0 inside their own spread), no material regression. Deviations: PR opened without merging the six main commits that landed since the base (no file overlap; the queue's merge-ref run validates); the PostgreSQL latency cell and the full dogfood battery are reported below as they stood at report time.", "tests": "Every build/test under scripts/pm/os-verify-lock.sh (slot issue-24932-dev; shared box). Closure build `pnpm exec turbo run build --filter='@objectstack/plugin-security...' --filter='@objectstack/service-analytics...' --filter='@objectstack/driver-turso...' --filter='@objectstack/driver-sql...' --filter='@objectstack/driver-memory...' --filter='@objectstack/driver-sqlite-wasm...' --concurrency=2`: 22 tasks successful, VERDICT command-exit 0 (117 s). Batch 1 (spec rebuilt first, each part's exit captured before any pipe): plugin-security first run 89 failed in 13 files (one cause: ten real-stack rigs boot a bare ObjectQL with no security source in its slot, so every leaf composed $in: []; plus a fake-engine matcher reading a leaf and two of my own over-strict pins) → fixed; plugin-security typecheck 0; service-analytics `vitest run` 184 files / 4505 passed / 262 skipped, exit 0, typecheck 0; driver-turso readable-ids conformance 27 passed (nested row on both faces); driver-sql sqlite 18 passed / 2 skipped; driver-memory 15; driver-sqlite-wasm 11; spec readable-ids 48; driver-turso typecheck 0. Batch 2 (OS_TEST_POSTGRES_URL=postgres://postgres@127.0.0.1:54329/objectstack_test, a local PostgreSQL 16 started with initdb/pg_ctl as the postgres user): driver-sql readable-ids conformance `live postgres RAN`, 36 passed / 1 skipped; driver-mongodb refusing conformance 13 passed; the reworked plugin-security suites 12 failed in 3 files (my PG regex, a helper running a caller's scope under a system context, the delegated parity row) → fixed. Batch 4 on head 2b988305d: the three reworked suites with the PG cell 3 files / 231 passed (`on sqlite` and `on postgres`), then the FULL plugin-security suite 208 files / 4275 passed / 45 skipped, VERDICT command-exit 0 (held 933 s). ABLATIONS (scratchpad/ablate.sh; each leg: git restore --source=BASE for the subject, blob hash proven == BASE, on-disk marker counts printed, pins run, git checkout HEAD -- the files, blob proven == HEAD, git diff HEAD empty; trap restores on exit): security-plugin.ts at BASE (leaf-return count 1 → 0): ABLATED_RUN_EXIT=1, 30 pins red across the sqlite and postgres cells — every statements-per-request pin, the 33,000-master read (DATABASE_ERROR / 500), the leaf-shape pins, the engine's-cut pin, nested-compose's one-statement pin, the chain and sharing no-store-work pins; restored blob == HEAD 54b78ccf, tree clean. The four analytics files at BASE (admit count 2 → 0): 3 pins red (engine-bound face refuses, canHandle true and the raw seam called, the echo's compiler names no leaf); restored, clean. turso-driver.ts at BASE (nested-resolve count 1 → 0): 1 pin red (the nested row refuses on the remote face); restored, clean. Reverse-verification direction observed: red, as expected, on every leg. Latency harness (scratchpad/harness-latency.test.ts.txt, copied in for the run and removed, tree clean after): SQLite cell measured, the table is in the PR (today native 755.4 ms, after engine 568.4 ms, control engine+idlist 1031.5 ms, 16 widgets, identical counts); PostgreSQL cell (measured after the dogfood battery on 35c4c3951, same harness, same box): today native 1101.4 ms, after engine 510.0 ms, control engine+idlist 1432.1 ms over the 16 widgets (ratio 0.46), identical counts; per-widget ratios 0.20–1.60. TWO WIDGETS TRIP THE PER-WIDGET THRESHOLD I stated in the PR (ratio over 1.5): crm_contact count 32.7 → 50.5 ms (1.54) and crm_quote count 33.9 → 54.4 ms (1.60), both one-hop details of the private crm_account; their grouped twins are 1.19 and 0.38, every other widget is 0.20–0.68, and the absolute delta is 18–20 ms on a shared box whose own max spread for those cells is 40.7 / 44.4 (today) against 52.7 / 56.5 (after). The control cell shows the cost is the engine face's routing on a PG COUNT (engine+idlist 58.1 / 54.0 ms), which the leaf then recovers most of. My reading: not material — a 20 ms per-widget cost on two count widgets against a 54% total gain, and inside the box's noise band; reported here as the ruling asks rather than absorbed, see pr_body_additions. Shard-5 fix verified on 35c4c3951: `pnpm --filter @objectstack/core run test:repo` 6 files / 82 passed; the three rewired suites 3 files / 80 passed. NOT MEASURED: MySQL; a live Turso remote (the remote face was measured through the in-repo libsql stub, as the Turso suite measures it); a tenancy-walled master on PostgreSQL beyond the conformance row.", "gates": "Local subset on the FINAL head 35c4c3951 (rerun after the last push): `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 102 commands (changed lines under the 3000 human-merge threshold); each run with its exit code captured before any pipe and recorded byte for byte (scratchpad/ran.list): 102 exit 0. `node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack`: ✓ 102 derived families accounted — 102 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN, RAN_VERDICT_EXIT=0. The earlier union on 2b988305d had two PREREQUISITE-NOT-MET families (`check:dual-build-cjs-loads`, `check:i18n`, both read every package's dist/); on the final head the dogfood closure build had filled those dists first and both ran exit 0. `node scripts/check-nul-bytes.mjs`: OK. Not derived and not run: check:platform-checklist (no exported symbol renamed or removed); check:dev-prereqs / check:skill-frame-freshness (not triggered). The spec families that read a generated surface (check:api-surface, check:docs) ran green with nothing regenerated. CI on the PR head 35c4c3951 (check-runs, all completed): success — Build Core, Lint & Repo Gates, TypeScript Type Check, Type Check · workspace / source gates / consumer gates / debt ledger, Test Core and Test Core (1/6)…(6/6), Governed Surface Queue Guard, Check Changeset, Check PR Size, Check Documentation Links, Dogfood Regression Gate, Spec property liveness, Auto Label, Flag docs affected by code changes, Lint · ESLint / CI, release & engine gates / PM dispatch-gates / docs & code guards / repo & tooling gates / scoped · query-options erasure / slot lookup & corpus / tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter; skipped (selector or opt-in) — 18.x upgrade harness (opt-in), Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix shard), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). On the PR's first head 2b988305d, Test Core (5/6) and the Test Core aggregate were red: packages/core's test:repo project, the repo pin row-serving-door-exposure, reproduced locally (1 failed / 81 passed) and fixed in 35c4c3951.", "dogfood_battery": "35c4c3951 · `pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build` (exit 0) then `pnpm --filter @objectstack/dogfood test` (no OS_TEST_SHARD, no path) under the lock: os-verify-lock VERDICT command-exit 0 · held the lock 2867s (47m47s) · Test Files 287 passed | 3 skipped (290) · Tests 2395 passed | 18 skipped (2413) · no failure. A first battery run on 2b988305d died with the container's worker restart (DF_BUILD=0, the test never reached its summary) and is not cited.", "mcp_calls": "1 — mcp__claude-code-remote__add_repo (read access to objectstack-ai/hotcrm, the latency fixture; it attached nothing and I cloned read-only; a session tool, not a GitHub MCP tool, and no write). No GitHub MCP tool was called; every GitHub read was a `gh api` REST GET (issue 24932, its ten comments and the two cross-lane declarations, pull 25198 and its head's check-runs).", "api_writes": "2 — both through scripts/pm/ as objectstack-fleet[bot] via the fleet-write relay (--via dispatch): ① POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, assignees os-bill; relay request fw-20261022T091452Z-19a15f, run 39753937651 success, read-back 20432 bytes sent = stored; the relay's assignees leg is POST …/issues/25198/assignees inside the same op); ② POST /repos/objectstack-ai/objectstack/issues/24932/comments (this report, via scripts/pm/post-stamped.mjs --comment=24932). Not REST writes: 6 × `git push` of the branch (the fast-forward of the empty dispatched branch to 72a302a9d as the write-route probe, four WIP commits, and the shard-5 fix 35c4c3951). No PATCH of the PR body, no labels written (size/xl on the PR is the platform's labeler).", "open_questions": [ { "question": "Fork 1 (measured, not absorbed): for a delegated (on-behalf-of) caller whose ceiling declares no read depth, a controlled_by_parent detail under a master the DELEGATOR may read is now readable (and a by-id update of it is refused at the master-edit gate, 403) where the id-list derivation left it absent (404). The master itself was already readable to that caller through the same delegated read. Which answer does the switch keep?", "options": [ "A — Accept the parity (what this PR lands): the detail reads exactly the rows a delegated direct read of its master reads, through getReadFilter(master)'s ADR-0090 D10 spelling (#25106) that ADR-0139 D5.1 as amended names as the leaf's contract. Business need: the HotCRM shapes and the assembled OAuth context agree on every row except this one, where today's answer is an over-denial inconsistent with the master's own visibility; no row is served that the caller cannot already read on the master. Long-term: one spelling of one scope, ruling B ③. AI-error-proofing: the detail's visibility becomes derivable from the master's, as ADR-0055 §1 promises. Startup focus: no new mechanism. Effort: none beyond this PR (two pins flipped with the explanation).", "B — Keep the id-list derivation's narrower agent-leg depth (the ceiling's EFFECTIVE depth) for the detail: needs getReadFilter's delegated answer to spell the master's scope differently for a leaf than for a direct delegated find of the master — a second spelling ruling B ③ refused — or a second derivation path beside the leaf, which is the retired pre-scan. Business need: none measured. Long-term: the drift this card's equality principle forbids, reintroduced. Effort: an engine-lane change to computeDelegatedReadFilter or a plugin-security hand composition, plus the delegated-compose pins of #25032 going red." ], "recommendation": "A, because it is the contract the two rulings already fixed (ADR-0139 D5.1 as amended: a delegated read through the leaf serves exactly the rows a delegated direct read of the parent serves) and the only alternative is a second spelling of the master's delegated scope; the maintainer's word is asked because the dispatch names a measured row-set change as a fork to report." }, { "question": "Measurement condition 2 on PostgreSQL: two count widgets (crm_contact, crm_quote: one-hop details of a private master, 10,000 masters, 5,000 readable) answer 1.54× and 1.60× today's native median (+18 and +20 ms) on the engine face, against a 54% total gain over the 16 widgets and sub-1.0 ratios everywhere else. Is that material?", "options": [ "A — Not material; land as is. Business need: a dashboard's wall is the sum, which halves; the two widgets cost 20 ms each on a shared box inside their own noise band. Long-term: the engine face is the ruled destination (Q2 A); the native face over an id list is the retired form. Effort: none.", "B — Material; a follow-up card in domain:services to profile the engine face's COUNT path on PostgreSQL for a one-hop leaf (the control cell shows the routing, not the leaf, carries the 20 ms). Effort: a measurement card; this PR unchanged." ], "recommendation": "A, with B filed only if the seat wants the 20 ms understood; the ruling's condition asked for the numbers and the judgement, both given." } ], "out_of_scope_findings": [ "carrier: the seat (prose, two files outside the claim's bound) · noted, not filed · packages/spec/src/data/readable-ids.ts:50-53 (\"The engine's filter seam is the one caller that passes it\") and packages/objectql/src/filter-comparand-shape.ts:109 (\"This is the one caller that passes admitReadableIds\") carry the sentence this PR corrects in spec's filter-comparand-shape.ts docblock; both are stale since service-analytics' engine-bound merges pass the option. Dedupe: admitReadableIds, one caller, filter seam.", "carrier: the seat (Tier H) · noted, not filed · ADR-0139 D6's clause \"§2's mechanism (b) for controlled_by_parent stands, set-size ceiling included\" is false since this PR; the PR adds a dated parenthetical beside it rather than rewriting the accepted sentence. Dedupe: ADR-0139 D6, set-size ceiling, seventh consumer.", "carrier: #24932 (this PR's Deviation 3) · noted, not filed · the two #21829 stand-down write shapes (a nested operation, the engine's referential field clear) on a controlled_by_parent detail carry the RLS write filter only, where before this PR they also carried masterFK IN (readable master ids); engine-internal, not addressable by a caller, NOT MEASURED at run time. Dedupe: referential field clear, controlled_by_parent, predicate write, ADR-0139 D1.", "carrier: none · noted, not filed · the delegated read carries the master's subquery twice (both halves write the leaf, the engine composes the master once and reuses the scope by reference); correct rows, the ruling's execution parameter leaves it for a later simplification. Dedupe: duplicated subquery, delegator half, controlled_by_parent.", "carrier: the cloud seat (measurement condition 4) · noted, not filed · the staging read of a two-level controlled_by_parent chain on Turso remote is owed at the cloud seat's pin move; the remote-face fix and the nested conformance row are in this PR." ], "deviations": [ "The PR was opened on the dispatch base 72a302a9d without merging the six main commits that landed since (b6e870707..14e7239e7): none of their 71 files is among this branch's 29; the queue's merge-ref run validates; 02ad31a0c (spec parse refusing a controlled_by_parent master reference ADR-0055 §1 does not admit) is the one to watch on it.", "A local PostgreSQL 16 data directory was created outside the scratchpad (/tmp/os-pg-24932, owned by the postgres user, because initdb and postgres refuse to run as root and the scratchpad is root-only); it is stopped and removed in cleanup.", "objectstack-ai/hotcrm was cloned read-only to /home/user/objectstack-ai/hotcrm (c8a7a7047) for the latency fixture's object shapes, through the session's anonymous git lane after add_repo reported read access available; removed in cleanup.", "The latency harness (a vitest file importing service-analytics' src by a relative path from plugin-security, a harness-only spelling) was copied into the package for each measuring run and removed after; it is not committed, a copy is in the scratchpad.", "The shared verify lock was held by sibling dogfood runs for most of this session: five acquisition budgets (9 min each) timed out at 99 before batches 4 and 5 ran; the slot kept its place each time and no run was made unlocked." ], "pr_body_additions": [ "Under `## Tests and gates`: 'Final head 35c4c3951 (one test-only commit after the PR opened at 2b988305d): CI Test Core shard 5 reddened on packages/core's repo pin row-serving-door-exposure, which reads any non-test source that calls `.canReadObject(` as a row-serving door and found readable-ids-in-memory.testkit.ts; reproduced locally (`pnpm --filter @objectstack/core run test:repo`: 1 failed / 81 passed) and fixed by making the composer take the engine's two halves as injected predicates (`readAdmission`, `readScope`, ADR-0139 D2 steps 1 and 5) wired by each suite, so the testkit names no service member; test:repo 6 files / 82 passed and the three rewired suites 3 files / 80 passed on 35c4c3951; the full plugin-security run cited above is 2b988305d's, the delta being those three test files and the testkit. CI on 35c4c3951: 35 success, 7 skipped, every Test Core shard green.'", "Under `## Tests and gates`: 'Gate union rerun on 35c4c3951: 102 derived, 102 run exit 0, 0 NOT MEASURED (the two PREREQUISITE-NOT-MET families of the first run passed once every dist was built). Dogfood battery on 35c4c3951 under the lock: Test Files 287 passed | 3 skipped, Tests 2395 passed | 18 skipped, VERDICT command-exit 0 (held 47m47s).'", "Under `## Measurement conditions` item 2, replacing the NOT MEASURED sentence: 'PostgreSQL (same harness, after the battery): total 1101.4 ms today → 510.0 ms after (ratio 0.46, control engine+idlist 1432.1 ms), identical counts; per-widget 0.20–1.60. Two count widgets trip the per-widget threshold stated above: crm_contact count 32.7 → 50.5 ms (1.54) and crm_quote count 33.9 → 54.4 ms (1.60), one-hop details of the private crm_account, grouped twins 1.19 and 0.38, +18–20 ms absolute inside the box's own max spread (40.7 / 44.4 today vs 52.7 / 56.5 after); the control cell shows the cost is the engine face's routing on a PG COUNT, which the leaf recovers most of. Reported as the ruling asks; my reading is not material (a 20 ms per-widget cost on two count widgets against a 54% total gain), the seat judges.'" ] }objectstack-fleet commentedon Oct 22, 2026
[Decision] 主从读取切换(PR #25198)入队前的两处实测分叉,以及 Tier H 的批准
domain:servicesseat 2 (seat post #21118) ·os-bill·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T12:22Z · Thread-read: 6340537042Ruling-ref: 6329027469 (Q1 C / Q2 A stands; only the two forks the build measured are put here)
一句话问题: 按裁决 Q1 C / Q2 A 做的切换已建成。PR #25198 全绿,完整 dogfood 通过,PostgreSQL 实测通过。派发令要求「实测到的行集变化是分叉,要上报不吸收」,开发据此上报了两处。PR 改了 ADR-0055 和 ADR-0139(Tier H),本来就要你批准才能合并,所以三件一次请示:分叉 1、分叉 2、合并批准。
背景(开发报告
6340537042;席位已复核 CI 与正文)35c4c3951上 35 项通过、7 项跳过。getReadFilter(master)的 ADR-0090 D10 写法(plugin-security: the raw-SQL read-scope provider (getReadFilter) does not apply the delegated organization-membership rule PR #25101 enforces at the operation prologue; measure reach first (detail withheld) #25106 落地的那一版):代理一侧取「声明深度」,未声明时为 'org'。实测两处变化:explain-enforce-parity中「代理代组织写者、更新他人主记录下的子记录」一行,由 404 变为 403;line_us变为line_us, line_jp,多读到一行。p_theirs更新答 403,说明可读不可写)。所以子对象现在恰好读到「代理直接读主对象能读到的行」,没有读到主对象上读不到的行。这正是 ADR-0139 D5.1(修订后)和 ADR-0055 §2 写明的等式。crm_contact计数 32.7 → 50.5 ms(1.54),crm_quote计数 33.9 → 54.4 ms(1.60),都是私有主对象crm_account的一跳子对象,绝对多 18–20 ms,在共享机器本身的抖动范围内。对照格显示,这点开销来自引擎路径在 PG COUNT 上的路由,叶子反而收回了大半。Governing text
ADR-0139 D5.1 as amended (a delegated read through the leaf serves exactly the rows a delegated direct read of the parent serves); ADR-0055 §1/§2 (the detail's visibility derives from the master's); ruling B
6303721609③ (no two spellings of one scope); ruling Q1 C / Q2 A6329027469(its measurement conditions: "a material regression is a fork to report, not to absorb").是否改协议: 否。不碰 spec 契约;spec 里只改一句 docblock 和一行一致性夹具(已在 #6017 声明)。
分叉 1 选项
getReadFilter对叶子和直接读给出两种写法(裁决 B ③ 已拒),要么在叶子旁另留一条推导路径(即已退役的预扫描);#25032 的代理组合钉子会转红分叉 2 选项
四轴(业务立场)
推荐:分叉 1 A、分叉 2 A,并批准 PR #25198 的 Tier H 合并
裁后执行段
CONTRACT_REVIEW_TIER隔离评审(安全读范围切换),通过后验收。本席按维护者的话收班(「当前任务处理完,合并后就下班」)。本卡与 PR #25198 交给下一位持本车道的席位,恢复点写在座位贴 #21118。
os-decision-facets
Prior rulings read: controlled_by_parent, ReadableIds, delegated read, D10 → ruling B
6303721609, ruling Q1 C / Q2 A6329027469, plugin-security: the raw-SQL read-scope provider (getReadFilter) does not apply the delegated organization-membership rule PR #25101 enforces at the operation prologue; measure reach first (detail withheld) #25106 (PR fix(plugin-security)!: the raw-SQL read scope (getReadFilter) answers the deny sentinel where the delegated prologue refuses #25150); thread: the dev report6340537042Related: PR #25198; claim
6334261853; cross-lane declarations6334266488(#6367) and6334275915(#6017).Generated by Claude Code
hotlong commentedon Oct 22, 2026
Ruling: batch #386 item 1 · letter fork 1 A / fork 2 A · Tier H approval granted in words · maintainer 「批 #386 五张全部按推荐:24932 分叉 1 A / 分叉 2 A,3134 A,3128 A,3138 A,3149 立 1 2 5 且第 1 项加 external 角色」 2026-10-22T13:41Z
Director seat, summon #48 (harness session
4890061b-3c11-4cab-8152-255c65343269, accounthotlong, written direct; ledger #12708 comment 6341703794), on the services seat's decision request6340537042's successor comment (thread read through the seat's request of 2026-10-22). Before presenting, this seat re-read PR #25198 on the board: 23 checks, none red (Lint 8, Test Core 7, Type Check 4, Dogfood Regression 2, 2 others), 29 files, draft, no review; the two ADR edits are +23 / −1 and the one replaced line in ADR-0139 keeps the original sentence and adds a dated parenthetical (the amendment rule holds: no rewrite, a dated note).What the ruling binds
controlled_by_parentdetail's delegated read serves exactly the rows a delegated direct read of its master serves — one scope, one spelling (ruling B6303721609③; ADR-0139 D5.1 as amended; ADR-0055 §2). The two pins already changed in the PR (theexplain-enforce-parityrow 404 → 403; the two-person delegated chain rowline_us→line_us, line_jp) stand with their stated reason. Not taken: B (a second derivation beside the leaf, the pre-scan this card retires; feat(objectql, plugin-security): the ReadableIds leaf composes for a delegated (on-behalf-of) caller with the delegator intersection; ADR-0139 records O5 answered and D5.1 amended #25032's pin would redden).hotlong/os-zhuang, both inGOVERNED_APPROVERS), never the seat's. Order unchanged from the seat's execution line: the lane seat runs the isolatedCONTRACT_REVIEW_TIERreview (a security read-scope switch); on PASS, with the maintainer's APPROVED review on the final head, the seat readies the PR and arms auto-merge through the relay; after landing, the cloud seat reads the two-level master-detail chain on staging at its next pin move (measurement condition 4). Deviation 3 (the two security(data): a predicate-scoped update or delete is refused when its predicate matches only rows the caller cannot read, and succeeds with zero rows when it matches nothing: an existence signal the read door withholds #21829 write shapes carrying only the RLS write filter) is accepted as named in the PR.Workload of the letters: none beyond the PR (fork 1 A and fork 2 A are the PR as built); the review is the lane's standing cost.
Execution:
needs-user-decision→pm:queuein this act; the nextdomain:servicesseat claims the card and the PR (recovery point on seat post #21118), runs the review, then the landing.Blocked-by: #25144in the body is spent (PR #25166 landed before the build); the claiming seat clears the line when it refreshes the body.os-decision-facets
objectstack-fleet commentedon Oct 22, 2026
Claim: stay-behind after sign-off · 2026-10-22T14:21Z · p1,
domain:services,area:access· PR #25198 to its landing, on ruling6342223675(fork 1 A / fork 2 A, Tier H approval granted in words)Session:
session_01UnHcTADjeg3VwxF21QHj7W(the seat session that dispatched the build and signed off at6340685792; it stays behind on this one card by the maintainer's word in this session's chat, verbatim: 「25198 已裁决,你要跟踪到合并啊」)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24932-cbp-leaf-switchWorktree:
objectstack-issue-24932(removed by the dev; no new build is planned)Domain:
domain:servicesSeat:
domain:services#2(seat post #21118, vacant; this claim is the stay-behind declared on its body)File surface: PR #25198 as built, 29 files at
35c4c3951. No new edit is planned. If the review or CI finds one, it is named here before any push.Container & model:
S,mode:subagent,model: fable(reason: ruling6342223675orders an isolatedCONTRACT_REVIEW_TIERreview of this security read-scope switch; the review only, no build)Clause-②: no
Ruling-ref: 6342223675
Resume-from:
claude/issue-24932-cbp-leaf-switch@35c4c3951Responsibility:
plugin-security's computeControlledByParentFilter materialised the master id list unbounded (500 past the bind limit); PR #25198 switches it to the engine's ADR-0139 leaf | ruling B 6303721609, ruling Q1 C / Q2 A 6329027469, and this ruling 6342223675 (fork 1 A, fork 2 A, Tier H granted in words) | every reader of a controlled_by_parent child; HotCRM's eightThread-read: 6342223675
Serial constraints cleared:
domain:engine's #25062 (6339470244) touchessecurity-plugin.tsstep 3.7 (the write wall), a different region. Whichever lands second mergesmain.Order, per the ruling:
CONTRACT_REVIEW_TIERon the final head.The spent
Blocked-by: #25144line leaves the card body in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:servicesseat 2, stay-behind ·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T17:04Z0b8bce40e0, on the maintainer's APPROVED review on head35c4c3951(Tier H, ruling6342223675)completedbyFixesState:
pm:dispatchedis removed and the claim is released in this act.What changed:
controlled_by_parentread now answers the engine's ADR-0139 leaf: one statement, no master scan, 200 past the bind limit.service-analyticsroutes a leaf-carrying scope to the engine face.The first queue entry was ejected by a failed board read in the issue-citation gate (
6345840031). It was re-queued once and landed.Owed after landing:
Generated by Claude Code