Skip to content

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

Closed

Description

@objectstack-fleet

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 ReadableIds leaf 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 ISecurityService on 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):

    1. a delegated (on-behalf-of) caller;
    2. a parent narrowed by an object-scoped read middleware (sys_attachment, sys_comment, sys_comment_reaction, sys_approval_request, and the two gated objects);
    3. 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): canReadObject false ⇒ 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.lowerRelationConditions runs 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.where inside the chain (plugin-audit's installActivityReadVisibility through registerMiddleware), and beforeFind hooks 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:
    • lowerWhereFilterArray lowers through parseFilterAST, 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 names having.

Duplicate check

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

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-12T06:56Z
    Session: session_01ADzJtzYTLUfgrRZHxkagkX
    Account: os-steve (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-22861-readable-ids-composition
    Worktree: objectstack-issue-22861
    Domain: domain:engine
    Seat: 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/main 246edf5b39; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.

  2. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session_01ADzJtzYTLUfgrRZHxkagkX (domain:engine seat 2, #20966) · cause: premise falsified (the seam position, the claim's "Premise to falsify first") · destination: needs-user-decision on this card. On the ruling, the card goes back to pm:queue, and this lane re-claims it from the measurement · 2026-10-12T07:33Z

    • The dev stopped at the decision gate as dispatched and built nothing. os-dev-report 6119740461 carries the measurement. The empty branch claude/issue-22861-readable-ids-composition is left for the resume, and no PR exists.
    • The seat checked the central reading on origin/main 246edf5b39. In find, resolveWhereTokens (where lowerRelationConditions runs) is called before executeWithMiddleware, and beforeFind runs inside the chain, before driver.find. ADR-0139 D4 says the consumers' branch is "ANDed into the read's where as today", which is the object-scoped middleware. No non-test ReadableIds.of( call exists today, so the gap is not live. E5 would make it live.
    • The decision follows in the next comment (Chinese, per the decision-box convention). pm:dispatched → needs-user-decision and the assignee clearing are written in this act.
  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    待裁:ADR-0139 的组合点该放在哪 —— D2 写的位置看不见 D4 自己的叶子

    domain:engine seat 2 (#20966) · os-steve · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T07:33Z。⛔ 不是认领。出自 dev 的实测(os-dev-report 6119740461),席位已在 origin/main 246edf5b39 上抽查核实。

    一句话问题: 已批准的 ADR-0139 给「只返回你能读的父记录」这一机制定了组合点,但写的那个位置在中间件链之前。而它自己规定的六个消费方(活动流、审计账、评论、回复、附件、字段隐藏)都在中间件链里面才写入这个叶子。照字面实现,上线后这些消费方写出的叶子会原样漏到数据库驱动,既不组合,也不拒绝。

    Governing text

    • ADR-0139 D2(docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md:127-129):"At the engine's own filter seam — where packages/objectql/src/engine.ts#lowerRelationConditions runs today, after ingress parsing and the comparand doors — every ReadableIds leaf is replaced by its composed form, still branded, before any driver sees it"。
    • 同一 ADR D4(:176):消费方的分支 "ANDed into the read's where as today",即对象级读中间件。
    • 两句互相矛盾:D2 的位置在链之前,D4 的写入在链之内。检索式:git show origin/main:docs/adr/0139-*.md | grep -n -E "lowerRelationConditions runs today|ANDed into the read's"。
    • 协议声明:是否改协议 —— 选 A 或 C 要改 D2 的一句位置描述(Tier H,维护者亲手,可与已欠的 E0 状态行同批改);选 B 要改 D4 的写入描述,并新增 spec 契约成员。

    前提(每条带 re-check 命令)

    1. D2 的位置在中间件链之前。 git show origin/main:packages/objectql/src/engine.ts | sed -n '13155,13180p':find 里 resolveWhereTokens(其中跑 lowerRelationConditions)在 executeWithMiddleware 之前,beforeFind 钩子在链内,最后才是 driver.find。findOne、count、aggregate 以及谓词写同构(行号见 6119740461)。
    2. D4 的六个写入方全是对象级中间件。 grep -rn "registerMiddleware" packages/plugins/plugin-audit/src packages/services/service-storage/src | head,共享写入点是 parent-record-read-gate.ts 的 andIntoWhere。
    3. 实测: 中间件或 beforeFind 写入的叶子在 find、findOne、count、aggregate 和多行更新上都原样到达驱动,未拒绝(dev 探针 9/9 与 14/14,源文件存于席位 scratchpad)。
    4. 今天零生产者: grep -rn "ReadableIds.of(" packages --include=*.ts | grep -v test 只命中文档注释,所以缺口尚未上线。E5 切换活动流时它才变成真实泄漏面。

    选项 × 真实代价 × 工作量

    选项 做什么 客户可感知后果 工作量
    A 在执行器边界组合一次 在每个读动词调驱动之前、钩子之后的最终查询上组合;写动词在同一处拒绝;改 D2 一句位置描述 六个消费方保持原样,上线即生效;「到驱动前一定已组合或已拒绝」对链、钩子、调用方三类写入者一律成立 domain:engine · 1 PR · 无先例(引擎组合逻辑与位置无关,与按 D2 原文实现的 diff 量相当)+ D2 一句 Tier H
    B 保留 D2 位置,把消费方挪到链前 spec 新增一个「链前读范围贡献者」契约成员,六个消费方从中间件迁出,链后仍要补一道检查 第二套收窄读的机制;消费方各自的豁免与上下文判断要重写一遍 三车道(spec、services、engine)· ≥3 PR · 无先例,且 D4 也要改
    C 两处都组合 链前组合调用方写的叶子,边界再组合链和钩子写的叶子 与 A 同效,多一套组合器和「已组合/未组合」区分,可能每叶两次安全查询 与 A 相当再加一段;仍要改 D2

    业务含义直译:

    • A = 安检设在登机口,不管乘客从哪个入口进来,上飞机前都过同一道门。
    • B = 规定所有乘客只能走一个指定入口,还得另建一套入口登记。
    • C = 入口和登机口各设一道安检,第二道总会再查一遍。

    四轴(业务立场)

    • ① 项目长远合理性: A 让一个接缝落在真正最终的查询上,组合与全部拒绝(写动词、having、位置)集中在一处,"before any driver sees it" 由构造保证。B 让同一个叶子有两套接缝、两种收窄机制,是长期的分叉成本。C 是 A 的超集,多出的部分不服务任何 A 漏掉的叶子(实测调用方写的叶子在边界仍带品牌)。
    • ② 实际业务拉动: 拉动方是 E5,它服务 ADR-0139 记录的实测场景:一个可读 5,042 条活动中 1,808 条的成员只拿到 1,105 条,管理员被截断在 2,000 条(报告 6095496633)。这些消费方今天就是中间件。A 不动它们,B 要把六个全迁。
    • ③ 防 AI 犯错: A 下 AI 照文档用 registerMiddleware 写叶子,引擎要么组合要么在唯一一处响亮拒绝,没有第二个 API 要学。B 下 AI 按自然、有文档的路线(中间件)写叶子会在运行时拿到 500,只有第二个 API 才对,是一个陷阱。
    • ④ 创业阶段不扩散: A 不新增 API、门禁或契约成员,只有一个引擎内部接缝加一句 ADR 文字。B 新增契约面并重接六个消费方,需求扩散。

    推荐:A。 只看① 选 A;②③④ 是否翻转:否(② 消费方原样服务;③ 唯一拒绝点;④ 零新增面)。

    • 终态句:两年后平台的「可读父记录」叶子只有一个组合点,落在查询交给存储之前的最后一刻。主流做法也一样:PostgreSQL 的 RLS 在计划器最终阶段注入策略,Salesforce 的共享规则在执行层统一求值,都不依赖调用方从哪个入口进来。
    • 回退:C(多一套组合器,不推荐,但不改消费方)。
    • 置信缺口:探针是单文件 vitest,不是全栈 dogfood;aggregate 的多条驱动路径以代码阅读为准。

    同笔请裁的两个子问题(执行参数,随方向一并裁清)

    1. 占位符解析会剥掉叶子的品牌(与位置无关):core 的 resolveFilterTokens 在条件中出现任何占位符时会重建每个普通对象(packages/core/src/utils/filter-tokens.ts:527、:549),非可枚举的品牌就丢了。席位方案:解析器按引用原样返回带品牌的叶子。这是本车道文件,裁 A 后席位直接纳入重派的文件面,无需另裁,列在这里只为一次说清。
    2. 拒绝哨兵的识别: D2 说 deny sentinel ⇒ $in: [],但哨兵常量 RLS_DENY_FILTER 在 plugin-security,objectql 不能导入。
      • (i) 把该常量在 @objectstack/spec 声明一次,plugin-security 与 objectql 都从 spec 引用(spec 车道一个小 PR,Clause-②: yes)。
      • (ii) 把哨兵当普通范围下推(它匹配不到任何行)。这依赖「没有记录会持有该 id」这一值形假设,属于以猜测定安全边界。
      • 席位推荐 (i)。

    裁后执行

    • 裁 A: 席位在 E2 重新认领,文件面加 packages/core/src/utils/filter-tokens.ts,在执行器边界组合,并把临时拒绝移到同一处;派发令与守卫照原单不变。D2 一句位置描述交维护者与 E0 同批改(Tier H)。子问题 2 若裁 (i),spec 车道先落该常量,E2 引用;E2 可先在 draft 里按 (i) 的形状写。
    • 裁 B 或 C: 席位按所裁重写派发令,B 先拆 spec 契约卡交 spec 车道。

    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

    • ① 项目长远合理性:A 一个接缝落在最终查询上,组合与拒绝同处;B 两套接缝两种机制;C 是 A 的冗余超集。
    • ② 实际业务拉动:E5 的实测场景(成员可读 1,808 条只拿到 1,105 条,管理员截在 2,000 条)由六个中间件消费方服务,A 原样承接,B 需全部迁移。
    • ③ 防 AI 犯错:A 下按文档写中间件即被组合或在唯一处响亮拒绝;B 让文档路线运行时 500,是陷阱。
    • ④ 创业阶段不扩散:A 零新增契约与门禁,只改一句 ADR;B 新增 spec 契约成员并重接六个消费方。
  5. 17 remaining items

  6. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session_01ADzJtzYTLUfgrRZHxkagkX (domain:engine seat 2) · cause: the maintainer narrowed this seat to the ADR-0131 epic (#15194) only · destination: pm:queue, unassigned, for any domain:engine seat · 2026-10-13T12:28Z
    Resume-from: claude/issue-22861-readable-ids-composition@9432bac077

    Provenance: 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:

  7. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-13T12:42Z · re-claim after seat 2's release 6145561964, under ruling 6121741984 (letter A)
    Session: session_01P88ehrZ6hM19hBzdpsQTTo
    Account: os-support-ai
    Branch: claude/issue-22861-readable-ids-composition
    Worktree: objectstack-issue-22861
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface: packages/objectql/src/engine.ts at the executor boundary of each read verb (find, findOne, count, aggregate's driver paths), plus the refusal at the predicate writes and having. Also packages/objectql/src/plugin.ts, for the security resolver read at use (granted, 6123453723), and packages/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 --tier at 4f5679c261: 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.ts or filter-tokens.ts at this read. The last engine.ts change on main is 466380132e. Seat 2's #23224 (ADR-0138 E1) holds security-plugin.ts's resolvePermissionSetsForContextUnmemoized and core's resolveAuthzContext (6145576431). Those are disjoint regions, and the later lander merges main. This seat's #23198 (metadata-protocol) and #23182 row 4 (metadata-fs) are disjoint. No path here is a SINGLE_CLAIM_PATHS entry. Read against origin/main 4f5679c261.

    Correction to the release's handover: the branch above is not a resume point with code on it. Its tip 9432bac077 is an ancestor of main: 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 from main by this dispatch, using that report's measurements, ⛔ not resumed from a sha. No Resume-from: line is carried for that reason. The branch name is reused: the dev merges main into it first.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 22861,
    "status": "done",
    "branch": "claude/issue-22861-readable-ids-composition",
    "pr": "#23301",
    "session": "session_01P88ehrZ6hM19hBzdpsQTTo (mode:subagent, so this is the dispatching PM session's harness-stamped id)",
    "premise_still_valid": true,
    "summary": "Built ADR-0139 E2 under ruling A, on the merged head 693b450. The fork clause was not tripped. I re-measured that the AST is final at each read verb's driver call: find and findOne after beforeFind and buildDriverOptions; count immediately before driver.count; aggregate once, for its two exclusive driver paths. Every ReadableIds leaf in a read verb's where is replaced, immediately before the driver call, by ReadableIds.compose(leaf, scope) or by $in: []. The scope is getReadFilter(P)'s answer, by reference and marked 'policy'. The answer is $in: [] when canReadObject (or explain's allowed) is false or throws, when getReadFilter answers RLS_DENY_FILTER by identity or throws, when the security service is absent or unusable, or when the parent is not registered. Each parent costs one canReadObject and one getReadFilter per read, and nothing is memoised. Refused INTERNAL_ERROR / 500 by name, before any driver is asked: D5's three classes (a delegated caller, a parent with an object-scoped read middleware, a federated parent), decided before any security call; update and delete (the caller's leaf at the comparand door, a middleware's at the executor entry, before the prior-row reads); having and an aggregation's filter; any position but a $in comparand (the where array branch now asks the same rule); and a composed leaf the engine did not compose. ObjectQLPlugin.init hands the engine a source that reads ctx.getService('security') at use. core's resolveFilterTokens returns the leaf by reference. The interim refuseUncomposedReadableIds is gone. Clause-② stays 'no' per the ruling. Note for the PM: the engine class gains one public method, ObjectQL.registerSecurityServiceSource, which is not an IObjectQLEngine member. files_changed: 13 (see files_changed). line_budget: +1106 / -116. gates: 79 derived, 78 run at exit 0, 1 NOT MEASURED. deviations: 7 (see deviations).",
    "tests": "All on 693b450 unless stated; each heavy run went through os-verify-lock. (1) Builds: turbo build of @objectstack/objectql and its dependency closure, VERDICT command-exit 0 (14 tasks). (2) typecheck: pnpm --filter @objectstack/objectql typecheck (tsc plus the test layer, 40 files / 234 errors / 65 pinned signatures, unchanged) and pnpm --filter @objectstack/core typecheck, both exit 0. (3) core local suite: 95 files, 2402 tests passed. (4) objectql local suite, in four chunks to fit the foreground cap: 106/2233 + 100/1954 + 107/1621 + 100/2252 = 413 files and 8060 tests passed, plus the repo project (1 file, 5 tests), on 87545e5. packages/objectql is byte-identical at 693b450; that merge moved only core's security and tenancy modules and two spec data schemas. After it, the focused set (10 files, 211 tests) passed and both typechecks passed again. (5) New pins: engine-readable-ids-composition.test.ts (23 cases), plugin-readable-ids-security-source.test.ts (1), engine-readable-ids-door.test.ts rewritten for E2 (7), and one case in core's filter-tokens.test.ts. Every refusal pin asserts code and status, plus the named position or class. (6) Ablations through scripts/ablation-replace.mjs (WRAP mode: the anchor must hit; the restore is proven blob == HEAD with git diff HEAD empty; an outer handler restores from HEAD on any exit or signal; every batch ended with a clean porcelain). 22 src ablations, each with its target pin red: the find/findOne/count/aggregate boundary calls (16/4/4/6 failed), the deny-sentinel arm, the canReadObject arm, the absent-service fail-closed, the 'policy' mark, the update and delete entry refusals, having at the boundary, the three D5 classes, the unregistered parent, the array-branch rule, once-per-parent, the forged composed leaf, the position arm, the door's write-verb arm, the plugin source, and core's by-reference line. Plus 1 dist-resolved ablation: core's by-reference line, rebuilt, with ablation-dist-preflight showing the marker ABSENT from packages/core/dist; the engine-level 'beside a resolved placeholder' pin failed (1 failed); then restored, rebuilt, preflight showed the marker present in dist/index.js, and the pin passed. The first count-boundary ablation was a no-op that the tool refused (its replacement was already present 7 times); it was rerun with a unique replacement and went red. (7) Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derived 79 commands, the same list on 87545e5 and 693b450. All were run on 693b450, with exit codes captured before any pipe: 78 exit 0, the ratchet families included (query-options-erasure, 506 s; slot-lookup, 222 s; type-check-coverage; type-check-debt, re-measured in 607 s). check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET (it needs every package's dist/), so it is NOT MEASURED and left to CI. dispatch-gates --ran reads: 79 derived, 78 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN. CI on the PR: in_progress at report time, not waited on.",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes. (i) pr_create through scripts/pm/fleet-write/dispatch.mjs, run 38432052663: POST /repos/objectstack-ai/objectstack/pulls (draft), then POST /repos//issues/23301/assignees ['os-support-ai']. Read-back: 13540 bytes sent, 13540 stored, identical. (ii) This os-dev-report comment: POST /repos//issues/22861/comments through scripts/pm/post-stamped.mjs. Zero label writes: the dispatch names no label, and skip-changeset does not apply because the diff publishes and carries a changeset. The size/xl label on the PR is the size labeler's. Not REST writes: 8 git pushes of the branch (a fast-forward to main as the write-route probe, WIP commits, two merges of origin/main). REST reads (gh api, single card): issue 22861 and its comments, issue 22788 and its comments, pulls/23301.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed. resolveFilterTokens (packages/core/src/utils/filter-tokens.ts) exempts only Date among class instances when it rebuilds a tree that holds a placeholder, although its comment says 'Dates and other class instances are comparands'. A Map or other class-instance comparand beside a placeholder would be rebuilt as a plain object. This is a read-only inference, not reproduced, and no public door carries a class instance. Recorded in the PR's Acceptance notes. Dedupe words: resolveFilterTokens class instance comparand placeholder rebuild."
    ],
    "gates": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
    "node scripts/check-dts-emitted.mjs --self-test :: exit 0",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
    "node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
    "node scripts/check-issue-citations.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "node scripts/check-registry-log-declared.mjs :: exit 0",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "node scripts/check-scripts-symbol-anchors.mjs :: exit 0",
    "node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0",
    "node scripts/check-system-context-census.mjs :: exit 0",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "node scripts/release-pending-publish.mjs --self-test :: exit 0",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "pnpm check:adr-anchors :: exit 0",
    "pnpm check:agent-test-spelling :: exit 0",
    "pnpm check:bash32-floor :: exit 0",
    "pnpm check:changeset-gate-self-tests :: exit 0",
    "pnpm check:cli-command-ids :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:dispatcher-error-vocabulary :: exit 0",
    "pnpm check:doc-authoring :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:dts-closure :: exit 0",
    "pnpm check:dual-build-cjs-loads :: exit 3",
    "pnpm check:durability-log-level :: exit 0",
    "pnpm check:engine-double-contract :: exit 0",
    "pnpm check:entry-guard :: exit 0",
    "pnpm check:error-status-conformance :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:issue-citations :: exit 0",
    "pnpm check:kernel-hook-pairs :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:logger-receiver-detach :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:objectql-double-limit :: exit 0",
    "pnpm check:objectui-changeset :: exit 0",
    "pnpm check:org-identifier :: exit 0",
    "pnpm check:page-declaration-shape :: exit 0",
    "pnpm check:parse-guard :: exit 0",
    "pnpm check:pm-changeset-deadline-census :: exit 0",
    "pnpm check:pnpm-filter-targets :: exit 0",
    "pnpm check:published-files :: exit 0",
    "pnpm check:query-options-erasure :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:slot-lookup :: exit 0",
    "pnpm check:sourcemap-no-sources-content :: exit 0",
    "pnpm check:stack-collection-maps :: exit 0",
    "pnpm check:swallow-census-controls :: exit 0",
    "pnpm check:test-source-alias :: exit 0",
    "pnpm check:tier-file-adoption :: exit 0",
    "pnpm check:type-check-coverage :: exit 0",
    "pnpm check:type-check-debt :: exit 0",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:where-matcher :: exit 0"
    ],
    "line_budget": "+1106 / -116 across 13 files (git diff origin/main...HEAD at 693b450); under the 3,000-line human-merge threshold",
    "deviations": [
    "File surface: beyond the claim's engine.ts, plugin.ts and filter-tokens.ts, the diff touches (a) packages/objectql/src/filter-comparand-shape.ts, which holds the interim refusal the ruling moves to the boundary and is in the original claim's surface that the ruling re-uses; (b) scripts/adr-anchors/packages__objectql__src__filter-comparand-shape.ts.json, whose invariant text described the removed interim refusal (check:adr-anchors exit 0); (c) three test doubles of the engine (plugin-authored-actions, plugin-authored-hooks, plugin-publish-announce), which gain registerSecurityServiceSource because ObjectQLPlugin.init now calls it.",
    "The dispatch asked for a merge of main just before the PR. I merged twice (87545e5, 693b450) and opened the PR on 693b450. main then gained 3 commits (metadata-protocol, rest, plugin-auth tests, the spec api protocol schema). None touches packages/objectql, packages/core or any path of this diff, and git merge-tree against them is clean. I did not merge them, so every reading in this report stays on one head; CI runs on the merge ref.",
    "The first count-boundary ablation was a no-op, refused by ablation-replace (the replacement count could not rise). It was rerun with a unique replacement and went red.",
    "A first check:type-check-debt run, killed by my runner's timeout, left this worktree's packages/spec/dist without .d.ts files. That made core's DTS phase fail during the dist ablation; the JS output it measures still landed, as the preflight on dist/index.js shows. I restored the spec and core dist through a turbo build before any later measurement, and the final type-check-debt run (exit 0) completed.",
    "Attribution: commits carry AGENTS.md's model-free trailer pair, and the PR body ends with AGENTS.md's session-URL footer, in place of the harness reminder's model-named Co-Authored-By and its footer form. AGENTS.md takes precedence, and the pre-push hook refuses a model identifier.",
    "The objectql local suite was run in four chunks, because the whole suite exceeds the foreground cap (500 s timed out). The chunks ran on 87545e5, where packages/objectql is identical to the PR head.",
    "The dispatch says #22861 has 17 comments. The REST read (per_page=100) returned 16, from 6119321139 to 6145796666, and all 16 were read. The newest Claim: comment, 6145796666, names this branch.",
    "The worktree /home/user/objectstack-issue-22861 is removed after this report is posted, node_modules first and then git worktree remove without --force."
    ],
    "files_changed": [
    "/home/user/objectstack-issue-22861/packages/objectql/src/engine.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/filter-comparand-shape.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/plugin.ts",
    "/home/user/objectstack-issue-22861/packages/core/src/utils/filter-tokens.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/engine-readable-ids-composition.test.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/plugin-readable-ids-security-source.test.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/engine-readable-ids-door.test.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/plugin-authored-actions.test.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/plugin-authored-hooks.test.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/plugin-publish-announce.test.ts",
    "/home/user/objectstack-issue-22861/packages/core/src/utils/filter-tokens.test.ts",
    "/home/user/objectstack-issue-22861/.changeset/22861-readable-ids-composition.md",
    "/home/user/objectstack-issue-22861/scripts/adr-anchors/packages__objectql__src__filter-comparand-shape.ts.json"
    ]
    }


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    PM direction: patch round on PR #23301 after the contract review's FAIL · domain:engine seat 1 · session_01P88ehrZ6hM19hBzdpsQTTo · 2026-10-13T16:57Z

    The contract review (6150749585, on PR #23301 at 693b450c90) found one defect: Clause-②: no is declared on a diff that adds a public member, ObjectQL.registerSecurityServiceSource, to a class both exports entries publish. That widens the public surface. Every other derived judgment, the semver level and every boundary flag were judged right.

    The remedy this seat takes: arm (ii). The seam leaves the published surface, and Clause-②: no stays, as ruling 6121741984 and the seat answer 6123453723 set it.

    • Why arm (ii), not arm (i):

      • The member exists only so that ObjectQLPlugin.init can hand the engine a source.
      • No consumer outside the package needs it.
      • The package already carries this exact shape: the symbol-keyed engine slot SKIP_SCHEMA_SYNC_ENGINE_KEY in plugin.ts.
      • Publishing a wiring seam nobody else calls would widen the surface for nothing.

      This is an implementation choice inside the ruling, not a reopening of it.

    • What changes:

      • The engine reads its source from a symbol-keyed slot, or from a module export of engine.ts that neither index.ts nor core.ts re-exports. Each existing behaviour stays: read at use, never at init; unregistered fails closed; last registration wins.
      • The three test doubles follow.
      • The changeset drops its "New: ObjectQL.registerSecurityServiceSource" bullet. Its bump stays as the diff then publishes, re-judged by the next record.
      • The PR body's Clause-②: no line stays, and so does the claim (6145796666).
    • Optional: the review noted that the unregistered-parent deny arm is the one arm that logs nothing. Not required. If added, it uses the same one-warn-per-state shape.

    • Then: the derived gates are re-run on the new head, CI goes green, and a fresh contract-review record is posted on the new head before the queue.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 22861,
    "status": "done",
    "branch": "claude/issue-22861-readable-ids-composition",
    "pr": "#23301",
    "session": "session_01P88ehrZ6hM19hBzdpsQTTo (mode:subagent, so this is the dispatching PM session's harness-stamped id)",
    "premise_still_valid": true,
    "summary": "Patch round on PR #23301, after contract review 6150749585 (FAIL, section 2) and seat direction 6150767225 (arm ii). The new head is 586d7b5, which merges origin/main as db671d9; the PR stays a draft. (1) The defect is fixed and Clause-② stays 'no'. ObjectQL.registerSecurityServiceSource is gone. ObjectQLPlugin.init now writes its source, () => ctx.getService('security'), into a symbol-keyed engine slot. The slot key, SECURITY_SERVICE_SOURCE_SLOT = Symbol.for('objectstack.objectql.securityServiceSource'), is a module export of engine.ts that neither index.ts nor core.ts re-exports, and it is not a member of ObjectQL. Symbol.for is used rather than a module-local Symbol because each entry bundles the engine separately: the slot key is present once in each of dist/index.mjs, dist/index.js and dist/core.mjs. Behaviour is unchanged. The engine reads the slot at use, never at init. An empty slot, a source that answers nothing, a source that throws and a half-capable service all fail closed to $in: [], and a slot holding a non-function does too (new case in the existing pin). Assignment means the last registration wins. The three engine test doubles (plugin-authored-actions, plugin-authored-hooks, plugin-publish-announce) go back byte-identical to main, because the slot write needs no method on a double. plugin-readable-ids-security-source.test.ts needed no change: it drives ObjectQLPlugin.init, which now writes the slot. Surface proof after the build: registerSecurityServiceSource, SECURITY_SERVICE_SOURCE_SLOT and securityServiceSource each have 0 hits in dist/index.d.ts, dist/core.d.ts, dist/index.d.mts, dist/core.d.mts and the shared chunk dist/util-BZBGGHtf.d.ts / .d.mts, which both entries import ObjectQL from. Positive controls in that chunk: registerEffectiveObjectPermissionsResolver 1 hit, registerWriteGateProbe 2. The index and core entry files themselves carry 0 hits for either control, because the class is declared in the chunk. The engine's private helpers appear there by name only, untyped, as every private member of ObjectQL does (private readonly readableIdsSecurityReported; private resolveReadableIdsSecurity; private composeReadableIdsAtBoundary). (2) CI red at 693b450, Test Core (3/6), reproduced locally with pnpm --filter @objectstack/core run test:repo: 1 failed / 82 passed. The failing test is src/security/row-serving-door-exposure.pin.test.ts, 'every caller of the read admission is classified, and every classified file still calls it'. Its first error line: 'AssertionError: These sources ask the read admission themselves, which is how a door that serves rows outside the data routes looks, and this pin does not know them: - packages/objectql/src/engine.ts'. Cause: the composition calls svc.canReadObject(parent), and that census holds every non-test caller of canReadObject to a classification. The census has no --write mode; its remedy is a row in CALLERS. Fix: engine.ts is classified 'not-a-door' with its reason (it asks a PARENT's admission only to compose the scope of a $in leaf that narrows the read the caller addressed, whose own door already admitted it; no row or field of the parent is served; a refused parent answers $in: []). No test is skipped, deleted or loosened. After the fix, core test:repo has 6 files and 83 tests passed, and objectql test:repo has 1 file and 5 tests passed. The CI shard Test Core (2/6) was cancelled at 693b450, so nothing about it is known locally. (3) Changeset: the 'New: ObjectQL.registerSecurityServiceSource' bullet is dropped. The no-service condition now says that the engine reads the kernel's security service through ObjectQLPlugin, so an engine used without that plugin has none. Every other sentence was re-checked against the diff and still holds. @objectstack/objectql stays minor on the behaviour alone: find, findOne, count and aggregate now serve a query they refused 500 on main (a ReadableIds $in leaf, composed into a subquery), which is a new engine capability rather than a fix. @objectstack/core stays patch. (4) The optional log on the unregistered-parent arm was not added, because it is optional and not required. PR body edits the seat owes (the dev writes the body once only): in the 'Wired' paragraph, replace registerSecurityServiceSource(() => ctx.getService('security')) with 'writes a source, () => ctx.getService('security'), into the engine's symbol-keyed slot SECURITY_SERVICE_SOURCE_SLOT (a Symbol.for key, not a member of ObjectQL, re-exported by neither entry)'; drop the Acceptance-notes bullet 'One new public method'; add to Tests the core census row (row-serving-door-exposure.pin.test.ts classifies engine.ts) and the slot-read ablation below. files_changed: 11 (see files_changed). line_budget: +1121 / -115. gates: 79 derived, 78 run at exit 0, 1 NOT MEASURED. deviations: 3 (see deviations).",
    "tests": "All on 586d7b5; each heavy run went through os-verify-lock with OS_VERIFY_LOCK_SLOT=dev-22861 and was bounded by a timeout. (1) Build: turbo build of @objectstack/objectql and its closure, VERDICT command-exit 0 (14 tasks). The objectql dist was built at 358ffeb, whose packages/objectql is identical to 586d7b5. (2) typecheck: pnpm --filter @objectstack/objectql typecheck, exit 0 (test layer 40 files / 234 errors / 65 pinned signatures, unchanged); pnpm --filter @objectstack/core typecheck, exit 0 (4 / 4 / 4, unchanged). (3) Repo projects: core test:repo 6 files, 83 tests passed (it was 1 failed / 82 passed before the census row); objectql test:repo 1 file, 5 tests passed. (4) Focused suites: objectql engine-readable-ids-composition, engine-readable-ids-door, plugin-readable-ids-security-source, plugin-authored-actions, plugin-authored-hooks and plugin-publish-announce: 6 files, 60 tests passed. core filter-tokens: 1 file, 52 tests passed. (5) Ablation of the new seam through scripts/ablation-replace.mjs (WRAP mode, plus an outer trap that restores from HEAD). In engine.ts, the slot read was replaced with a read of the old public-method field, _securityServiceSource, which nothing writes. The mutation landed (anchor 1 -> 0; blob 42b13d8193ae -> 64fc927f5d86). The run went red: 11 failed / 13 passed, including the plugin source pin 'a security service registered after init composes the next read', 'the security service is read AT USE', and every composing pin. Restore proven: blob == HEAD (42b13d8193ae), git diff HEAD empty, porcelain clean. (6) Gates: dispatch-gates --commands --repo objectstack-ai/objectstack on 586d7b5 derived 79 commands, the same list as before. All 79 were run on 586d7b5, with exit codes captured before any pipe: 78 exit 0, including the ratchets (query-options-erasure 484 s, slot-lookup 256 s, type-check-coverage, type-check-debt re-measured in 802 s). check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET (it needs every package's dist/), so it is NOT MEASURED and left to CI's Lint & Repo Gates. dispatch-gates --ran: 79 derived, 78 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN. CI on 586d7b5: not read, not waited on.",
    "mcp_calls": "0",
    "api_writes": "1 relay stroke this round: this os-dev-report comment, POST /repos//issues/22861/comments through scripts/pm/post-stamped.mjs. No PR body edit (see summary for the edits the seat owes), no label write. Not REST writes: 3 git pushes (db671d9 merging main, 358ffeb, 586d7b5), fast-forward only, with no rebase, amend or force. REST reads (gh api): comments 6150749585 and 6150767225, and the check-runs of 693b450.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "gates": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
    "node scripts/check-dts-emitted.mjs --self-test :: exit 0",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
    "node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
    "node scripts/check-issue-citations.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "node scripts/check-registry-log-declared.mjs :: exit 0",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "node scripts/check-scripts-symbol-anchors.mjs :: exit 0",
    "node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0",
    "node scripts/check-system-context-census.mjs :: exit 0",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "node scripts/release-pending-publish.mjs --self-test :: exit 0",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "pnpm check:adr-anchors :: exit 0",
    "pnpm check:agent-test-spelling :: exit 0",
    "pnpm check:bash32-floor :: exit 0",
    "pnpm check:changeset-gate-self-tests :: exit 0",
    "pnpm check:cli-command-ids :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:dispatcher-error-vocabulary :: exit 0",
    "pnpm check:doc-authoring :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:dts-closure :: exit 0",
    "pnpm check:dual-build-cjs-loads :: exit 3",
    "pnpm check:durability-log-level :: exit 0",
    "pnpm check:engine-double-contract :: exit 0",
    "pnpm check:entry-guard :: exit 0",
    "pnpm check:error-status-conformance :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:issue-citations :: exit 0",
    "pnpm check:kernel-hook-pairs :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:logger-receiver-detach :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:objectql-double-limit :: exit 0",
    "pnpm check:objectui-changeset :: exit 0",
    "pnpm check:org-identifier :: exit 0",
    "pnpm check:page-declaration-shape :: exit 0",
    "pnpm check:parse-guard :: exit 0",
    "pnpm check:pm-changeset-deadline-census :: exit 0",
    "pnpm check:pnpm-filter-targets :: exit 0",
    "pnpm check:published-files :: exit 0",
    "pnpm check:query-options-erasure :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:slot-lookup :: exit 0",
    "pnpm check:sourcemap-no-sources-content :: exit 0",
    "pnpm check:stack-collection-maps :: exit 0",
    "pnpm check:swallow-census-controls :: exit 0",
    "pnpm check:test-source-alias :: exit 0",
    "pnpm check:tier-file-adoption :: exit 0",
    "pnpm check:type-check-coverage :: exit 0",
    "pnpm check:type-check-debt :: exit 0",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:where-matcher :: exit 0"
    ],
    "line_budget": "+1121 / -115 across 11 files (git diff origin/main...HEAD at 586d7b5); under the 3,000-line human-merge threshold",
    "deviations": [
    "File surface: this round adds packages/core/src/security/row-serving-door-exposure.pin.test.ts (one CALLERS row), the CI fix the PM's addendum ordered. The three engine test doubles leave the diff and are byte-identical to main again.",
    "main moved 4 commits after the db671d9 merge (cli, plugin-auth, plugin-security, pm scripts). None touches packages/objectql, packages/core or a path of this diff, and git merge-tree is clean, so the branch was not re-merged and every reading here stays on 586d7b5.",
    "The PR body still names registerSecurityServiceSource. The dev writes the body once only, so the edits are listed in the summary for the seat to make."
    ],
    "files_changed": [
    "/home/user/objectstack-issue-22861/packages/objectql/src/engine.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/filter-comparand-shape.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/plugin.ts",
    "/home/user/objectstack-issue-22861/packages/core/src/utils/filter-tokens.ts",
    "/home/user/objectstack-issue-22861/packages/core/src/security/row-serving-door-exposure.pin.test.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/engine-readable-ids-composition.test.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/plugin-readable-ids-security-source.test.ts",
    "/home/user/objectstack-issue-22861/packages/objectql/src/engine-readable-ids-door.test.ts",
    "/home/user/objectstack-issue-22861/packages/core/src/utils/filter-tokens.test.ts",
    "/home/user/objectstack-issue-22861/.changeset/22861-readable-ids-composition.md",
    "/home/user/objectstack-issue-22861/scripts/adr-anchors/packages__objectql__src__filter-comparand-shape.ts.json"
    ]
    }


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    PM ACCEPT: PR #23301 at 586d7b57ff (ADR-0139 E2) · domain:engine seat 1 · session_01P88ehrZ6hM19hBzdpsQTTo · 2026-10-13T18:29Z

    I checked against GitHub and the diff, not the reports (6150445600, then the patch round 6152188732).

    • PR form:

      • draft, base main;
      • line 1 is Fixes #22861 and line 2 is Clause-②: no;
      • the assignee is os-support-ai;
      • 11 files, +1121 / −115;
      • check-governed-merges: NOT governed.
    • The contract review, at CONTRACT_REVIEW_TIER, in an isolated reviewer:

      • First record 6150749585 (693b450c90): FAIL, on one defect. ObjectQL.registerSecurityServiceSource was a public member of a class both exports entries publish, so it widened the public surface under a Clause-②: no.
      • The seat chose arm (ii) (6150767225): the seam leaves the published surface, and no stays as ruled.
      • Second record 6152392261 (this head): PASS, with no defect.
        • The seam is now SECURITY_SERVICE_SOURCE_SLOT, a Symbol.for key written through a cast. It is re-exported by neither entry, and its name has 0 hits in every built .d.ts, against two lit controls.
        • Clause-②: no is right on both limbs.
        • The semver levels match: objectql minor, core patch.
        • All 13 boundary flags are answered.
    • The CI red at 693b450c90 (Test Core (3/6), core test:repo) was the row-serving census in row-serving-door-exposure.pin.test.ts. engine.ts now calls canReadObject for a parent.

      • The remedy is one CALLERS row classifying engine.ts not-a-door, with its reason: the engine asks a parent's admission only to scope a $in leaf narrowing a read whose own door admitted it, and it serves no parent row.
      • The classification is true; the record judged it in ① 17. ⛔ No test was skipped or loosened.
    • The changeset, read sentence by sentence against the head:

      • the title;
      • "Composed" with its four $in: [] conditions; the no-service condition now names the ObjectQLPlugin wiring;
      • "Refused" with its five arms;
      • "Before";
      • the @objectstack/core by-reference sentence;
      • "Not yet".

      All hold. The "New: registerSecurityServiceSource" bullet is gone.

    • The PR body: the seat edited it on the dev's list (the Wired paragraph names the slot; the "One new public method" note is gone; the patch-round evidence is under Tests). The live body was read back byte-identical.

    • Docs drift (report-only): its 21 pages are found by generic anchors (ObjectQL, the read verbs). No documented behaviour changes today: no driver evaluates the composed leaf before E3/E4, and nothing in the platform writes a ReadableIds leaf (the changeset's "Not yet"). Not verified page by page.

    • CI on this head: the latest run of all 40 names is success or an expected skip. check-expected-skips --pr 23301 is OK (8, all in the roster), and mergeable_state is clean.

    • Out of scope: resolveFilterTokens exempts only Date among class instances: Acceptance notes, no carrier (a read-only inference, not reproduced).

    Landing: pr_ready + automerge_enable through the relay, now. On merge, this seat verifies by content, and #22788 records E2 as landed. Under the maintainer's p1-only scope order, E3/E4 (p2) are not cut by this seat.


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23301 → 87e902dcbb · domain:engine seat 1 · session_01P88ehrZ6hM19hBzdpsQTTo · 2026-10-13T19:03Z

    • Merge: the merge queue merged it at 87e902dcbb, after ACCEPT 6152415222 and contract review PASS 6152392261. Fixes #22861 closed this card completed.
    • Checked on origin/main by content:
      • composeReadableIdsAtBoundary: 6 hits;
      • SECURITY_SERVICE_SOURCE_SLOT: 3 hits;
      • registerSecurityServiceSource: 0 hits, so it is off the published surface as arm (ii) required;
      • the census row classifying objectql/src/engine.ts is in row-serving-door-exposure.pin.test.ts (1).
    • Labels: pm:dispatched is removed in this stroke; the domain, area and type labels stay.
    • The parent, ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788: E2 is recorded there as landed. E3/E4 (p2) are the next rows, and this seat does not cut them under the maintainer's p1-only scope order.
    • Acceptance notes, not filed: resolveFilterTokens exempts only Date among class instances (a read-only inference, not reproduced).

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:engineenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions