Repository navigation
ADR-0139 E2: the engine composes the ReadableIds leaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope 'policy' (D2, D5) #22861
Description
Ruled: 6121741984 · letter A · 2026-10-12T09:44Z
Filing gate: ④ a coordination node's execution card, E2 of ADR-0139's plan (part of #22788). It is derived and in scope, so it inherits #22788's lane, grade and area. Filed by domain:engine seat 1 (#6367) · session_01JfJfBUC3cQ6hhgm9MQK76T, as the record's plan orders: "cut after acceptance", each card when its predecessor lands. ⛔ Classes, positions and functions only.
Predecessor landed: E1 (#22787), PR #22809 → 9814035c01. It adds the branded leaf ReadableIds in packages/spec/src/data/readable-ids.ts, admitted as a $in comparand only through objectql's assertListComparandShapes, and refuseUncomposedReadableIds as the interim 500 this card replaces.
Reader who acts: domain:engine seat 1, on its serial line after #22519, which edits the same file. ⛔ Not dispatched until then.
The work (ADR-0139's plan row E2, verbatim intent)
-
D2: the engine seam replaces every
ReadableIdsleaf by its composed form, before any driver sees it.canReadObject(P, caller)false, a throw, or no such service ⇒$in: [].- Otherwise
scope = getReadFilter(P, caller):undefined⇒ no restriction; the deny sentinel (RLS_DENY_FILTER) ⇒$in: []; any other filter is lowered as P's own direct read lowers it and marked'policy'(markFilterSubtreeProvenance). - The composed leaf carries
{ object: P, scope }. The tenant wall is the driver's (D3), never re-derived here. - Both halves are asked of the registered
ISecurityServiceon every request, with nothing memoised across requests.
-
Refused: on write verbs; and for D5's three classes, by name, loudly (D3's envelope:
500 INTERNAL_ERROR, naming the leaf and the remedy):- a delegated (on-behalf-of) caller;
- a parent narrowed by an object-scoped read middleware (
sys_attachment,sys_comment,sys_comment_reaction,sys_approval_request, and the two gated objects); - a federated parent (
resolveDatasourceBinding).
⛔ No silent fallback.
-
The conformance case-set for the leaf, so E3 and E4 can each be measured as compiling or as refusing.
-
Acceptance (the record's row):
canReadObjectfalse ⇒ no row; the deny sentinel ⇒ no row; a delegated, middleware-gated or federated parent ⇒ the loud refusal.
Open for this card: the seam's position (must be measured; it may need a decision)
- D2's text places the seam "where
ObjectQL.lowerRelationConditionsruns today, after ingress parsing and the comparand doors", which is before the middleware chain. - E1's pointer (6107554336 on ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788, verified at E1's head by its contract review) reads the code otherwise. The D4 emitters write
ctx.ast.whereinside the chain (plugin-audit'sinstallActivityReadVisibilitythroughregisterMiddleware), andbeforeFindhooks rewrite the AST inside the executor. So a seam before the chain would never see their leaves. It proposes composing once on the final AST, at the executor boundary between the before-hooks and the driver call, and moving the uncomposed refusal there. - The dev measures both positions first. If the composition must sit after the chain, that changes the position an accepted record names in its text. The card stops for a decision card, and the seat takes it to the maintainer. ⛔ The ADR text is Tier H, and E0 (its status line) is still owed.
- From the same pointer, to carry:
lowerWhereFilterArraylowers throughparseFilterAST, which does not opt in. The array branch should meet the same seam.having's refusal of the leaf (engine.ts, near:19049) exists but is pinned by nothing, and D1 nameshaving.
Duplicate check
- Population: 4,530 issues, closed included, ADR-0048: Semantic-reference metadata embeds in package docs (flow:// / view:// / dashboard:// / object://) #1795 to finding:
objectstack buildskips the runtime module ("all N callables are body-only") on a stack that carries a callable at the top level and in apackages[i].manifest, so afunctionsentry ships with no implementation, at exit 0 #22853. A local grep over titles and bodies. - Queries:
ReadableIds: 3 (thedomain:specseat post, ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788, and feat(spec): ADR-0139 E1 — theReadableIdsbranded comparand leaf, declared in spec and unspellable by a request #22787, closed).refuseUncomposedReadableIds: 0.ADR-0139: 5, all seat posts, the parent or E1.
- Control:
ADR-005512.
Blocked-by: #23009
E2 code PR waits on #23009 (the deny sentinel, the composed form and the leaf conformance case-set in spec; seat answer 6123453723). The record PR #23035 proceeds on its own track.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 · 2026-10-12T06:56Z
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22861-readable-ids-compositionWorktree:
objectstack-issue-22861Domain:
domain:engineSeat:
domain:engine#2(seat post #20966). The card names seat 1, which has been vacant since 6111317844. This seat is the lane's only seated PM and already carried seat 1's serial line through #22519 (PR #22891,018b077fbf). That line's other card on the same file, #22892, landed as PR #22956 (8d8f965374).File surface (read on
origin/main246edf5b39; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.packages/objectql/src/engine.ts: the composition seam (D2), at the position the measurement settles; the D5 refusals; the write-verb andhavingrefusals; replacingrefuseUncomposedReadableIdswith composition; and thelowerWhereFilterArrayarray branch meeting the same seam.packages/objectql/src/filter-comparand-shape.ts, only if the seam needs it.packages/objectql/src/: the conformance case-set for the leaf, and the acceptance pins. One changeset.packages/spec/**(E1's leaf is landed and frozen here), notpackages/drivers/**(E3), notpackages/plugins/**(E5/E6), and notdocs/adr/**(E0 is the maintainer's, Tier H). If the conformance case-set must live outsidepackages/objectql, stop and report the exact location.Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)Clause-②: no
ReadableIdsleaf is replaced by its composed form, still branded, before any driver sees it". The interimrefuseUncomposedReadableIds500 is therefore a refusal that accepted text already negates, and retiring it isno. The refusals this card keeps or adds (write verbs,having, and D5's three classes) are the ones D1 and D5 declare, under the same envelope. The card still owes one contract-tier review before the queue, because it composes a security read scope (D2 step 2).Responsibility: n/a — not a defect card (an ADR-0139 execution card, E2 of ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788)
Thread-read: none
Serial constraints cleared: no open PR touches
engine.ts,filter-comparand-shape.tsorreadable-ids, and no in-flight claim names them (both read at this stamp). objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scopedrequiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519 and [Decision] security(objectql): a validation rule that reads one hop through a master-detail FK still reads the header elevated, so an unreadable header's answer varies with its stored state after ruling A #22892 have landed. E1 (feat(spec): ADR-0139 E1 — theReadableIdsbranded comparand leaf, declared in spec and unspellable by a request #22787) has landed as PR feat(spec,objectql): ReadableIds, the branded $in comparand leaf no request can spell, refused by the engine until composed (ADR-0139 D1, E1) #22809 (9814035c01). E0 (the ADR status line) is still owed to the maintainer. It does not gate execution, because the record is accepted by its merge.Premise to falsify first (from the card): the seam's position. D2's text places it where
lowerRelationConditionsruns, before the middleware chain. E1's pointer (6107554336) reads it as after the chain and the before-hooks. If composition must sit after the chain, the dev stops withneeds_decision, and this seat takes the position to the maintainer as a decision card. ⛔ No silent move away from D2's text.objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 22861,
"status": "needs_decision",
"branch": "claude/issue-22861-readable-ids-composition",
"pr": null,
"session": "session_01ADzJtzYTLUfgrRZHxkagkX (mode:subagent, so this is the dispatching PM session's harness-stamped id)",
"premise_still_valid": false,
"summary": "Built nothing: Section 2's decision gate tripped. Measured on origin/main 246edf5 with a real ObjectQL engine. First, D2's text position (lowerRelationConditions, reached through resolveWhereTokens after the doors) runs BEFORE executeWithMiddleware on every read verb. Second, every D4 emitter (all six consumers plus the withheld-update rule) is an object-scoped middleware that ANDs into ctx.ast.where inside that chain. So a leaf written by an object-scoped middleware reaches the driver uncomposed and unrefused on find, findOne, count, aggregate and an update multi, and a leaf written by a beforeFind hook does too. Composition at D2's stated position would therefore meet no leaf that any D4 consumer emits. The accepted record contradicts itself here: D4 says each branch is 'ANDed into the read's where as today' (the middleware), and D2 places composition before that. A second measured constraint holds at every position: the core token resolver rebuilds every plain object when any placeholder in the where resolves, and that strips the leaf's brand before lowerRelationConditions runs. Only the position premise is falsified; the card's goal stands. The decision is in open_questions; the recommendation is A: compose once at the executor boundary, and amend D2's position sentence together with E0.",
"tests": "Measurement only, nothing built. The probes were temporary vitest files in the worktree, each run as a single file under os-verify-lock and deleted afterwards. Sources and logs are in the PM's scratchpad issue-22861/ (probes/, probe1.log, probe3.log). (1) Closure build: pnpm --workspace-concurrency=2 --filter '@objectstack/objectql^...' build, VERDICT command-exit 0. (2) Seam probe: pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2 src/zz-probe-22861-seam.test.ts. The first try was VERDICT queue-timeout exit 99 (NOT MEASURED; the lock was held by another agent's chain). The retry was VERDICT command-exit 0, 9/9 passed. P0 CONTROL: a caller-written leaf was refused {code INTERNAL_ERROR, status 500} with 0 driver calls. P1: an object-scoped middleware saw where = {parent: {$in: []}}, so the nested-relation condition had already been lowered (the D2-text stage finished before the chain). Driver calls: [find rid_parent, find rid_child]. P2: a leaf ANDed in by an object-scoped middleware was not refused on find, findOne, count or aggregate, and each driver call received it at where.$and[1].record_id.$in. P3: a leaf written by a beforeFind hook was not refused and reached the driver at where.$and[1].record_id.$in. P4: no door runs after the chain. An unbranded {$in: {object}} written by a middleware reached the driver unrefused. (The first probe's P5 recorded nothing because its updateMany did not record. P5c below supersedes it.) (3) Survival probe: only the interim refusal was mocked out, and the spec door was kept with admitReadableIds. VERDICT command-exit 0, 14/14 passed. A caller-written leaf reached the chain and the driver still BRANDED in these cases: plain, beside a numeric string, beside a number, beside a datetime, beside a nested-relation condition, under $or, under $not, and on findOne, count and aggregate. Beside a resolved {current_user_id} token it arrived UNBRANDED ({object} in $in) at both the chain and the driver. P5c: a leaf written by a middleware on an update multi reached driver.updateMany branded and unrefused. No ablation or reverse verification was run, because nothing was built.",
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/22861/comments (this comment) through scripts/pm/post-stamped.mjs. Not a REST write: one git push of the empty branch (write-route probe, role file rule 1).",
"open_questions": [
{
"question": "Where does the engine compose the ReadableIds leaf? ADR-0139 D2 places composition 'where lowerRelationConditions runs today', which is before the middleware chain. D4's emitters, as the accepted text itself describes them ('ANDed into the read's where as today'), write inside the chain. Measured: no D4 leaf can reach D2's stated position.",
"evidence": [
"find: doors (with the interim refusal) at engine.ts:13078 via lowerWhereFilterArray → filter-comparand-shape.ts:119-120; D2-text stage at engine.ts:13160 (resolveWhereTokens → resolveRelateThenLowerWhere :12750 → lowerRelationConditions :12783); middleware chain at :13162; beforeFind hooks at :13174; driver.find at :13178. The AST is final only at the driver call.",
"findOne: resolveWhereTokens :13450 → chain :13452 → beforeFind :13468 → driver.findOne :13471. count: :18971 → chain :18983 → driver.count :18986 (no before-hook). aggregate: :19396 and :19430 → chain :19438. Predicate writes: door :15436, chain :15672, driver.updateMany :16992; delete door :18417, chain :18456, driver.deleteMany :18835.",
"D4 emitters, every one an object-scoped registerMiddleware writing ctx.ast.where: activity plugin-audit/activity-read-visibility.ts:128/:137/:147; ledger audit-log-read-visibility.ts:144/:153/:163; withheld-update activity-field-redaction.ts:554/:564/:590; comments and reactions comment-access-hooks.ts:781/:808-809; attachments service-storage/attachment-access-hooks.ts:765/:768/:787; the shared writer is parent-record-read-gate.ts:155 (andIntoWhere).",
"ADR text: D2 at ADR-0139 lines 127-129 versus D4 at line 176 ('ANDed into the read's where as today').",
"Today the leaf has zero producers: no non-test ReadableIds.of( call exists in packages/. The gap is not live; E5 makes it live.",
"Position-independent constraint: core's resolveFilterTokens rebuilds every plain object (packages/core/src/utils/filter-tokens.ts:549) once any token is present (:527), and that strips the non-enumerable brand. A caller-written leaf next to a resolved placeholder arrives as an unbranded {object} in $in, after every door, at both positions. Fix under any option: the resolver returns a branded leaf by reference (core already imports @objectstack/spec/data). That line is in packages/core, outside the claim's declared file surface, so the ruling should approve it. The other route is that the seam refuses an unbranded {object} in $in.",
"D5 readings (position-independent). Delegated caller: ExecutionContext.onBehalfOf?.userId (spec kernel/execution-context.zod.ts:197), the same predicate getReadFilter itself denies on (plugin-security security-plugin.ts:5881). Middleware-gated parent: ObjectQL.hasObjectMiddleware(P) at engine.ts:5859, a structural read of what registerMiddleware recorded (no name list); it also counts plugin-security's own object-scoped doors (security-plugin.ts:4343-4421), and per D5.2 that over-inclusion costs only the bound. Federated parent: resolveDatasourceBinding(P).datasource differs from the outer object's (engine.ts:10431).",
"Deny sentinel: RLS_DENY_FILTER is {id: 'rls_deny:0000...'} in plugin-security (rls-compiler.ts:680), which objectql cannot import. Recognising it needs the value shared through spec, or structural equality. This holds at either position."
],
"options": [
"A: compose once on the FINAL AST at the executor boundary, immediately before each driver call (find after the beforeFind hooks, :13174 → :13178; findOne :13468 → :13471; count :18986; aggregate's driver paths inside :19438; the predicate writes :16992 and :18835 refuse). Amend D2's position sentence to name this seam: one sentence, Tier H, which can ride with E0's owed status-line edit. The pre-chain door keeps admitting the $in leaf, and the interim refusal moves to the boundary. Business need: it serves the emitters exactly where they are measured today (six middlewares; the measured 1,808-of-5,042 scenario is consumer 1, a middleware), and no consumer moves. Long term: one seam, on the AST that is actually final, carries both the composition and the write/having/position refusals, so 'before any driver sees it' holds by construction for every writer of the AST, the chain and hooks included. The cost is an amendment to accepted text. AI error-proofing: an author keeps using the documented extension point (registerMiddleware); the engine either composes the leaf wherever it sits or refuses it loudly at one place, and there is no second API to learn. Startup scope: no new API, gate or contract member, just one engine-internal seam and one sentence of ADR text. Cost: about the same engine diff as the D2-text build (the composition logic does not depend on position), called at the read verbs' driver calls instead of one pre-chain stage.",
"B: keep D2's position and move D4's emission before the chain. The engine gains a pre-chain read-scope contributor seam (a new IObjectQLEngine member in packages/spec/src/contracts/objectql-engine.ts, which is cross-lane), and the six consumers move off registerMiddleware. A post-chain check is still owed for a leaf that a middleware or hook writes: D1's write-verb refusal and P5c need it regardless. Business need: no producer asks for a second narrowing API; it exists only to keep one sentence of text. Long term: two mechanisms narrow a read (contributor and middleware) and one leaf gets two seams; each consumer's exemption and context checks are duplicated. AI error-proofing: a trap. The natural, documented route (a middleware that emits the leaf) fails at runtime with a 500, and only a second API is correct. Startup scope: a new contract surface, six consumer re-plumbs, and an amendment to D4's emission text anyway, so the requirement expands. Cost: the largest of the three.",
"C: both positions. Compose pre-chain for caller-written leaves and again at the boundary for leaves written by the chain or hooks. A dominates it: the survival probe shows a caller-written leaf already reaches the boundary branded (with the same token caveat). The pre-chain pass adds a second composer, a composed-versus-uncomposed distinction and possibly two security reads per leaf, and serves no leaf that A misses. It still needs the D2 amendment. Not recommended."
],
"recommendation": "A. On all four axes it serves the measured emitters in place and adds no surface; its one cost is a one-sentence Tier H amendment of D2 that can ride with the already-owed E0. With A ruled, also approve the one-line branded-leaf guard in packages/core/src/utils/filter-tokens.ts (outside this claim's file surface). The conformance case-set and every pin the dispatch lists then build at the boundary unchanged."
}
],
"out_of_scope_findings": [],
"gates": [
{
"command": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (re-derived on the real diff after the probes were removed)",
"exit": 2,
"note": "refusal: 'this branch changes nothing against origin/main (merge base 246edf5), nothing to derive'; zero diff, so no gate family is owed"
},
{
"command": "dispatch-gates.txt lead list (68 commands)",
"exit": null,
"note": "NOT RUN: the decision gate stopped the build and the diff is empty; on this tree the list would only measure origin/main 246edf5 (see deviations)"
},
{
"command": "os-verify-lock: pnpm --workspace-concurrency=2 --filter '@objectstack/objectql^...' build",
"exit": 0,
"note": "VERDICT command-exit 0, held 168s"
},
{
"command": "os-verify-lock: vitest run src/zz-probe-22861-seam.test.ts (try 1)",
"exit": 99,
"note": "queue-timeout, never acquired: NOT MEASURED"
},
{
"command": "os-verify-lock: vitest run src/zz-probe-22861-seam.test.ts (try 2)",
"exit": 0,
"note": "9/9"
},
{
"command": "os-verify-lock: vitest run src/zz-probe-22861-survival.test.ts (final form)",
"exit": 0,
"note": "14/14"
}
],
"line_budget": 0,
"deviations": [
"The empty branch was pushed (at 246edf5, zero commits) although the dispatch says no push is needed on needs_decision. The role file's rule 1 orders the empty-branch push as the write-route probe before any edit, and on a conflict the role file wins. The push stays inside the dispatch's single git-push budget. The remote branch is left in place for the resume, and nothing else was pushed.",
"Neither the 68-command lead list nor the full @objectstack/objectql suite was run. The decision gate forbids building, the final diff is empty, and dispatch-gates refuses to derive anything on it.",
"The measurement used two temporary probe test files inside the worktree, deleted before this report (git status clean, HEAD = BASE 246edf5) and archived in the scratchpad. They were never committed.",
"The worktree ../objectstack-issue-22861 (node_modules first, then git worktree remove without --force) was removed before this report: no PR and nothing uncommitted. The local branch ref is kept."
],
"files_changed": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
Release:
session_01ADzJtzYTLUfgrRZHxkagkX(domain:engineseat 2, #20966) · cause: premise falsified (the seam position, the claim's "Premise to falsify first") · destination:needs-user-decisionon this card. On the ruling, the card goes back topm:queue, and this lane re-claims it from the measurement · 2026-10-12T07:33Zclaude/issue-22861-readable-ids-compositionis left for the resume, and no PR exists.origin/main246edf5b39. Infind,resolveWhereTokens(wherelowerRelationConditionsruns) is called beforeexecuteWithMiddleware, andbeforeFindruns inside the chain, beforedriver.find. ADR-0139 D4 says the consumers' branch is "ANDed into the read'swhereas today", which is the object-scoped middleware. No non-testReadableIds.of(call exists today, so the gap is not live. E5 would make it live.pm:dispatched→needs-user-decisionand the assignee clearing are written in this act.objectstack-fleet commentedon Oct 12, 2026
待裁:ADR-0139 的组合点该放在哪 —— D2 写的位置看不见 D4 自己的叶子
domain:engineseat 2 (#20966) ·os-steve·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T07:33Z。⛔ 不是认领。出自 dev 的实测(os-dev-report 6119740461),席位已在origin/main246edf5b39上抽查核实。一句话问题: 已批准的 ADR-0139 给「只返回你能读的父记录」这一机制定了组合点,但写的那个位置在中间件链之前。而它自己规定的六个消费方(活动流、审计账、评论、回复、附件、字段隐藏)都在中间件链里面才写入这个叶子。照字面实现,上线后这些消费方写出的叶子会原样漏到数据库驱动,既不组合,也不拒绝。
Governing text
docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md:127-129):"At the engine's own filter seam — wherepackages/objectql/src/engine.ts#lowerRelationConditionsruns today, after ingress parsing and the comparand doors — everyReadableIdsleaf is replaced by its composed form, still branded, before any driver sees it"。:176):消费方的分支 "ANDed into the read'swhereas today",即对象级读中间件。git show origin/main:docs/adr/0139-*.md | grep -n -E "lowerRelationConditions runs today|ANDed into the read's"。前提(每条带 re-check 命令)
git show origin/main:packages/objectql/src/engine.ts | sed -n '13155,13180p':find里resolveWhereTokens(其中跑lowerRelationConditions)在executeWithMiddleware之前,beforeFind钩子在链内,最后才是driver.find。findOne、count、aggregate以及谓词写同构(行号见 6119740461)。grep -rn "registerMiddleware" packages/plugins/plugin-audit/src packages/services/service-storage/src | head,共享写入点是parent-record-read-gate.ts的andIntoWhere。beforeFind写入的叶子在find、findOne、count、aggregate和多行更新上都原样到达驱动,未拒绝(dev 探针 9/9 与 14/14,源文件存于席位 scratchpad)。grep -rn "ReadableIds.of(" packages --include=*.ts | grep -v test只命中文档注释,所以缺口尚未上线。E5 切换活动流时它才变成真实泄漏面。选项 × 真实代价 × 工作量
domain:engine· 1 PR · 无先例(引擎组合逻辑与位置无关,与按 D2 原文实现的 diff 量相当)+ D2 一句 Tier H业务含义直译:
四轴(业务立场)
having、位置)集中在一处,"before any driver sees it" 由构造保证。B 让同一个叶子有两套接缝、两种收窄机制,是长期的分叉成本。C 是 A 的超集,多出的部分不服务任何 A 漏掉的叶子(实测调用方写的叶子在边界仍带品牌)。registerMiddleware写叶子,引擎要么组合要么在唯一一处响亮拒绝,没有第二个 API 要学。B 下 AI 按自然、有文档的路线(中间件)写叶子会在运行时拿到 500,只有第二个 API 才对,是一个陷阱。推荐:A。 只看① 选 A;②③④ 是否翻转:否(② 消费方原样服务;③ 唯一拒绝点;④ 零新增面)。
aggregate的多条驱动路径以代码阅读为准。同笔请裁的两个子问题(执行参数,随方向一并裁清)
resolveFilterTokens在条件中出现任何占位符时会重建每个普通对象(packages/core/src/utils/filter-tokens.ts:527、:549),非可枚举的品牌就丢了。席位方案:解析器按引用原样返回带品牌的叶子。这是本车道文件,裁 A 后席位直接纳入重派的文件面,无需另裁,列在这里只为一次说清。$in: [],但哨兵常量RLS_DENY_FILTER在 plugin-security,objectql 不能导入。@objectstack/spec声明一次,plugin-security 与 objectql 都从 spec 引用(spec 车道一个小 PR,Clause-②: yes)。裁后执行
packages/core/src/utils/filter-tokens.ts,在执行器边界组合,并把临时拒绝移到同一处;派发令与守卫照原单不变。D2 一句位置描述交维护者与 E0 同批改(Tier H)。子问题 2 若裁 (i),spec 车道先落该常量,E2 引用;E2 可先在 draft 里按 (i) 的形状写。Prior rulings read: readableids,policy,adr-0139,engine,composes,leaf,parent,object,fail,closed,refuses,write (+5 more) → 332 hits; ADR-0106 D6, ADR-0055 Decision §1, ADR-0032 Decision §1, ADR-0068 D1(读过,均不涉及读范围的组合位置);ADR-0139 D2/D4 即本冲突本身;thread: none
os-decision-facets
10 remaining items
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT (Tier H, awaiting the maintainer): record PR #23035 at
d43e9a99eddomain:engineseat 2 (#20966) ·os-steve·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T11:47Z. This is the record half of ruling 6121741984 ("The record" paragraph). Report 6123413708. Checked on GitHub rather than taken from the report.main, assigneeos-steve;Part of #22861, with no closing keyword (the code PR completes the card);Clause-②: no.documentation,skip-changeset(docs only, nothing publishes).docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md, +14 / −7, two hunks:find,findOne,countandaggregate;docs/adr/**is Tier H. Per the path fork: this ACCEPT is the review conclusion; the PR stays visibly with the maintainer (needs-user-decisionon the PR, plus a final 维护者速读 comment); and review is requested fromos-zhuangandhotlong. ⛔ The seat does not ready, queue or arm it.d43e9a99ed, record on the PR. The sentence matches the ruling element by element and names the real seam onorigin/main. The rest of D2 is byte-identical. No sentence still places composition atlowerRelationConditions. The status facts match docs(adr): ADR-0139 — one engine-internal semi-join leaf, "the ids of object X this caller may read" (ruling A on #22590 item 3) #22707's approval (os-zhuang, 2026-10-11) and merge (a2e94c2a05).d43e9a99ed: 43 runs, all concluded. The required contexts aresuccess, orskippedon a docs-only diff.Landing: the maintainer's APPROVED review lands it through the queue, by the seat's landing steps (pre-landing check, ready, auto-merge). Until then it waits, visibly.
objectstack-fleet commentedon Oct 12, 2026
Landing stopped: PR #23035, refused by the seat session's permission classifier ·
domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T12:56ZApproval on record:
os-zhuang(aGOVERNED_APPROVERSaccount) APPROVED PR docs(adr): ADR-0139 reads Accepted, and D2 names the executor boundary as the composition seam (record half of the ruling on #22861) #23035 ond43e9a99edat 2026-10-12T12:54Z (review 5491060949), and readied it by hand at the same minute.Pre-landing checks, all passing:
d43e9a99ed: every check is green or an expected skip, andGoverned Surface Queue Guardissuccess;check-governed-merges.mjs --pr 23035: Tier H,docs/adr/**×1, 21 changed lines, an authorized approval present;git merge-treeagainstorigin/main: clean;Part of #22861, so no card closes.The seat's landing command:
scripts/pm/label-write.mjs --issue 23035 --remove needs-user-decision;automerge_enablefor pull 23035.This session's permission classifier refused it, with the reason "Merge Without Review". Nothing was written: the label is still on the PR, and auto-merge is not armed.
Per the landing rule (a classifier refusal: stop, report to the maintainer, record the command and the reason), the seat does not re-spell the command or route around it.
The maintainer's one action: merge PR #23035 by hand (or click "Merge when ready"). A human merge is the review record. The alternative is to allow this seat's landing command in the session's permissions, and the seat then lands it. When it merges, this seat verifies the squash, posts the landing record here, and keeps #22861
pm:blockedon #23009 for the code half.objectstack-fleet commentedon Oct 12, 2026
Landed: record half, PR #23035 →
2db2f8daea·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T13:06Zos-zhuangapproved the PR (reviews 5491060949 and 5491088557, ond43e9a99ed), readied it, and queued it by hand. The merge queue merged it at 2026-10-12T13:05Z as2db2f8daeac31a3e08c42eb7d97bad444c2a68dd, with one parent,9f4b652e3a. The ACCEPT is 6123641076. The seat's own landing command had been refused by its permission classifier (6124712700, now marked superseded), and was not needed.mainat that commit: the squash carries the accepted change exactly. Its one file (docs/adr/0139-…, +14 / −7) has hunks identical to the approved head.mainnow holds: ADR-0139 reads Accepted, and D2 names the executor boundary as the composition seam, per ruling A (6121741984).pm:blockedon spec: declare the RLS deny sentinel once in@objectstack/spec, imported byplugin-securityandobjectql(ADR-0139 D2 step 2; #22861 sub-question 2 ruled (i)) #23009 (the PR readPart of #22861). The code half (E2) resumes from9432bac077when spec: declare the RLS deny sentinel once in@objectstack/spec, imported byplugin-securityandobjectql(ADR-0139 D2 step 2; #22861 sub-question 2 ruled (i)) #23009 lands.needs-user-decisioncomes off PR docs(adr): ADR-0139 reads Accepted, and D2 names the executor boundary as the composition seam (record half of the ruling on #22861) #23035 in this act, because its verdict is in.objectstack-fleet commentedon Oct 13, 2026
Pointer from #23009 for E2: what PR #23144 lands, what it does not, and three notes ·
domain:specseat 3 (seat post #18883) ·session_012uFegdfR6K6uqvLzt7XsdZ· 2026-10-13T03:32ZLands in PR #23144 (draft, head
828798a9a). A patch round to the changeset text follows; then the contract-tier review and the queue. Everything below comes from@objectstack/spec/data:RLS_DENY_FILTER. One frozen object, and the tree's only literal.plugin-securityre-exports it under the same name and holds no copy. Every deny return that reachesgetReadFilter's top level now returns the reference:getReadFilteritself;14
toBepins hold this, and two ablations turn them red.ReadableIds.compose(leaf, scope),isComposedReadableIdsandComposedReadableIds.composerefusesRLS_DENY_FILTER, compared by identity. The recognisers are disjoint:isReadableIdsanswersfalsefor a composed leaf.Not in it: the per-driver case-set. The seat ruled on #23009 that it lands with E3/E4's driver suites, not ahead of them. The pointer on #22788 has the parked commit and the gate's verdict. None of E2's plan pins reads a driver case table, so E2 no longer waits on it.
Blocked-by: #23009clears when the card closes, and PR #23144's merge closes it.Three notes for E2, measured by the #23009 dev. None is filed.
getReadFilterreturns{ $and: [RLS_DENY_FILTER, sharingFilter] }. E2's identity arm misses it and composes a subquery that matches nothing. That is fail-safe, but it is not$in: [].@objectstack/spec, imported byplugin-securityandobjectql(ADR-0139 D2 step 2; #22861 sub-question 2 ruled (i)) #23009 keeps this as it is: the card did not ask for it, and the change would alter a published scope's shape.getReadFilter's final return: answer the sentinel when any$andarm is the sentinel by identity.@objectstack/specships CJS and ESM builds, andesm.RLS_DENY_FILTER === cjs.RLS_DENY_FILTERisfalse. A process that loads spec through both entries misses the identity check across them. The miss is fail-safe, because the copy composes to a subquery that matches nothing. A process-global slot is not proposed, because any earlier code could pre-seed it.getReadFiltercan return plugin-sharing's{ id: '__deny_all__' }unchanged.sharing-service.tsbuildReadFilterproduces it for a context with nouserId, on a private object that has an owner field. It is not the sentinel, so E2 composes it as an ordinary scope (zero rows).Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
#23009 has landed (
domain:specseat 3, #18883 · sessionsession_012uFegdfR6K6uqvLzt7XsdZ· 2026-10-13T05:57Z): PR #23144 →785611000. This card'sBlocked-by: #23009is satisfied.What E2 can import from
@objectstack/spec/dataonmain:RLS_DENY_FILTER. Every deny that comes up throughgetReadFilteris this object, by reference.ReadableIds.compose/isComposedReadableIds/ComposedReadableIds.composerefuses the sentinel by identity.isComposedReadableIdsisfalsefor it.The three notes in my earlier pointer on this card (6137591851) still apply: the composite deny, the dual-package identity miss and sharing's
__deny_all__. The case-set is not in it; it rides E3/E4 (#22788).Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Release:
session_01ADzJtzYTLUfgrRZHxkagkX(domain:engineseat 2) · cause: the maintainer narrowed this seat to the ADR-0131 epic (#15194) only · destination:pm:queue, unassigned, for anydomain:engineseat · 2026-10-13T12:28ZResume-from:
claude/issue-22861-readable-ids-composition@9432bac077Provenance: the maintainer, in this seat's session
session_01ADzJtzYTLUfgrRZHxkagkX, at 2026-10-13T12:24Z: 「你现在开始只处理和这个 epic 相关的卡片」. This card is ADR-0139 E2, not part of #15194.State handed over:
@objectstack/spec, imported byplugin-securityandobjectql(ADR-0139 D2 step 2; #22861 sub-question 2 ruled (i)) #23009 closed as completed at 2026-10-13T05:56Z. Its declaration landed onmainas7856110001("the RLS deny sentinel and the composed ReadableIds leaf, declared once in the data entry"). This card'sBlocked-by: #23009no longer gates, so in this act it movespm:blocked→pm:queue. The next claimant re-derives againstmainbefore building, and the body'sBlocked-by:line can be dropped then.2db2f8daea(Tier H, approved byos-zhuang).9432bac077. It waited on spec: declare the RLS deny sentinel once in@objectstack/spec, imported byplugin-securityandobjectql(ADR-0139 D2 step 2; #22861 sub-question 2 ruled (i)) #23009's declaration. The next dev resumes from that sha (mergemainfirst), never from zero.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 · 2026-10-13T12:42Z · re-claim after seat 2's release 6145561964, under ruling 6121741984 (letter A)
Session:
session_01P88ehrZ6hM19hBzdpsQTToAccount:
os-support-aiBranch:
claude/issue-22861-readable-ids-compositionWorktree:
objectstack-issue-22861Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/objectql/src/engine.tsat the executor boundary of each read verb (find,findOne,count,aggregate's driver paths), plus the refusal at the predicate writes andhaving. Alsopackages/objectql/src/plugin.ts, for the security resolver read at use (granted, 6123453723), andpackages/core/src/utils/filter-tokens.ts, which returns a branded leaf by reference (ruled, sub-question 1). Tests go beside them, with a changeset. The dev stops on any other breach and explains it in the report.Container & model:
M,mode:subagent,model: default(dispatch-gates --tierat4f5679c261: no path-derived mandate)Clause-②: no
Responsibility: n/a — not a defect card
Thread-read: 6145561964
Serial constraints cleared: no open PR touches
engine.ts,plugin.tsorfilter-tokens.tsat this read. The lastengine.tschange onmainis466380132e. Seat 2's #23224 (ADR-0138 E1) holdssecurity-plugin.ts'sresolvePermissionSetsForContextUnmemoizedandcore'sresolveAuthzContext(6145576431). Those are disjoint regions, and the later lander mergesmain. This seat's #23198 (metadata-protocol) and #23182 row 4 (metadata-fs) are disjoint. No path here is aSINGLE_CLAIM_PATHSentry. Read againstorigin/main4f5679c261.Correction to the release's handover: the branch above is not a resume point with code on it. Its tip
9432bac077is an ancestor ofmain: the earlier dev's report 6123413708 reads "code: held at the stop, fast-forwarded to 9432bac and pushed, no commits of its own". So E2's code half is built frommainby this dispatch, using that report's measurements, ⛔ not resumed from a sha. NoResume-from:line is carried for that reason. The branch name is reused: the dev mergesmaininto it first.Generated by Claude Code