Skip to content

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

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-22678-semi-join-leaf-adr
Oct 11, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-22678-semi-join-leaf-adr

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22678
Clause-②: no

Tier H, draft only. docs/adr/** is a governed surface (Prime Directive #14): this PR stays a draft and lands only on the maintainer's APPROVED review. ⛔ No seat readies, queues or arms auto-merge on it. #22590 remains open (it is Blocked-by: this card and objectui#12081).

What this PR carries

One new record, ADR-0139 (docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md, Proposed), transcribing ruling 6097072172 on #22590 (batch #312 item 2, maintainer 「同意」, letter A as the end state with the ADR as its first deliverable), plus one status-line pointer each on ADR-0055 and ADR-0056. It declares and implements nothing: no code, no spec, no driver file. The execution cards are listed in the record and ⛔ not filed.

What the record decides, in the ruling's own scope and nothing wider:

  • D1 — one engine-internal filter leaf, ReadableIds ("the ids of object X this caller may read"), a branded comparand in the position a list of ids holds today ($in on the pointer field). No request can spell it: a JSON where cannot carry the brand, and the unbranded shape is already refused INVALID_FILTER / 400 by the comparand door. Losing the brand fails closed, never silently widens.
  • D2 — ObjectQL composes it per request from canReadObject + getReadFilter (both halves, fail closed, deny sentinel ⇒ $in: []), lowers the scope the way the parent's own read is lowered, marks it policy, and leaves the tenant wall to the driver.
  • D3 — driver-sql compiles it as a subquery inside applyTenantScope; sqlite-wasm and local Turso inherit; the Turso remote transport and driver-memory implement it; driver-mongodb refuses loudly until a card measures a pipeline form. The refusal is one ADR-0112 envelope (500 INTERNAL_ERROR, not an author error). ⛔ No DriverCapabilities bit returns.
  • D4 — the six consumers (activity stream, compliance ledger, withheld-update rule, comment threads, reactions, attachments) emit one leaf per registered parent object and no pre-scan; the withheld-update rule moves to a writer-stamped changed_fields column with an idempotent backfill through the migration door.
  • D5 — three classes stay on today's bounded fail-closed probe, recognised by the engine refusing to compose a leaf for them: a delegated (on-behalf-of) caller; a parent narrowed by an object-scoped read middleware; a federated parent — which includes every parent of sys_activity on a deployment that registers the ADR-0057 telemetry datasource (recorded as a limit).
  • D6 — ADR-0055 row (a) and ADR-0056's non-goal are reversed for this one leaf only; the RLS compiler gains no subquery form and ADR-0055 §2's mechanism (b) stands.

Verification

Premise checked on origin/main d8830c28 by symbol, not by the card's line numbers: querySubqueries and joins are retired keys in packages/spec/src/data/driver.zod.ts; PARENT_GATE_SCAN_LIMIT = 2_000 in parent-record-read-gate.ts; RELATION_FILTER_ID_CAP = 1000 in packages/objectql/src/relation-filter-lowering.ts (the card's engine.ts attribution for the cap's definition is stale — the engine imports it); ADR-0055's row (a) and ADR-0056's non-goal read as the ruling quotes them. The six consumers were located at function level; only the activity stream was measured (report 6095496633), the other five are source readings and the record says so per row.

Gates at head f87ef2345a (after two wording rounds answering contract review 6100682640), derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the branch's change set (19 commands, identical to the dispatch's list), each exit captured before any pipe, reconciled with --ran: 19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN. check:doc-formula-expressions first exited 3 (PREREQUISITE NOT MET: no @objectstack/formula / @objectstack/lint dist) and was re-run after turbo run build --filter=@objectstack/formula --filter=@objectstack/lint under the verify lock (VERDICT command-exit 0); the re-run exited 0. check-adr-symbol-anchors: 2306 anchors across 142 records resolve, 0 line anchors (every reference this record makes is now a full path#symbol anchor the gate judges). check-adr-links: 725 destinations resolve. No package test or typecheck is owed (no package touched); no ablation (nothing built). Control-byte self-scan of the three files: none.

Acceptance notes

  • ADR number 0139, not 0136. 0136 is a gap on main (no record, no branch, no history names it); the record takes max+1 so the registry stays monotone in time and cannot collide with whatever reserved the gap. The dispatch said "the next free number"; this is the deviation, declared.
  • 285 lines against a suggested 250. The six-consumer table, the seven-row execution plan the ruling requires, and the three fail-closed classes with their recognition rule are the overage; the References section was folded into the header and the Alternatives and Consequences compressed once already.
  • Two back-pointers landed as one line each: ADR-0055's status line (row (a) revisited for one leaf; §2 stands) and ADR-0056's non-goal bullet (the compiler gains no form).
  • Findings noted, not filed (⛔ no card from this seat): .claude/skills/pm-dispatch/references/compile-surfaces.md cites driver-memory/src/memory-matcher.ts (checkCondition), a file that no longer exists on main (the evaluator is mingo in memory-driver.ts) — a stale reference in an agent-read file, carrier: the next edit of that table. plugin-sharing's sharing-service.ts reads record-share grants at limit: 5000 with no warning (source reading, from report 6095496633; the record lists it as open item O4).

维护者速读(草稿)

改了什么: 新增一份 ADR(0139,状态 Proposed),把你 10 月 10 日「同意」的 A 方案写成正式决策:引擎内部加一个不可编写、请求里永不接受的过滤叶子「这个调用者能读的 X 对象 id」,由 ObjectQL 组合权限范围后下推成对父表的子查询;driver-sql 实现,其余驱动实现或响亮拒绝;六个树内消费者(动态、审计台账、撤回更新规则、评论、表情、附件)改用它,撤回更新规则改为写入时盖一列 changed_fields 并回填;委托上下文、自带插件读闸的父对象、跨数据源父对象三类保留今天的有界 fail-closed 探针。另外在 ADR-0055 与 ADR-0056 各加一行指针。不改任何代码、spec 或驱动;执行卡只列在 ADR 里,不立。

为什么改: 实测一位普通成员应看到 1,808 条动态,系统只给 1,105 条且「总数」也显示 1,105,管理员恰好截在 2,000;原因是六处读闸都靠一个 2,000 行的预扫描窗口,多组织部署下所有组织的行挤同一个窗口。ADR-0055 当年明确拒绝过子查询,按第 13 条基本指令,反转一个已记录的决定本身需要一份新 ADR,所以代码之前先交这份文档。

风险与代价(含回滚): 这是一个真实的能力扩张,收窄为一个引擎内部叶子;工程量按裁决估计约 3–5k 行、6–10 个 PR、横跨三条车道。已点名的限制:driver-mongodb 在量过之前对这六类读会响亮报错(500)而不是悄悄少给;注册了 telemetry 独立数据源的部署,动态表与父表不在同一驱动,这个叶子帮不上,仍靠过渡方案 C 的响亮 400;方言绑定参数上限只取了文档值,未实测。回滚:ADR 处于 Proposed,未批准即不生效;批准后任何一张执行卡都可单独不落地,文档改动本身可整体 revert。

席位意见:(留空)

你要做的: 在本 PR 上给出 APPROVED review(或在评论里指出要改的决策点),批准后由 domain:engine 席位按 ADR 列出的顺序立执行卡。


Generated by Claude Code

… object X this caller may read"

Records ruling 6097072172 on #22590 (letter A, ADR first): one non-authorable
filter leaf the engine composes from canReadObject + getReadFilter and pushes
down as a subquery inside the driver's tenant wall; driver-sql compiles it, the
other drivers compile or refuse loudly; six in-tree consumers named; the
withheld-update rule moves to a writer-stamped changed_fields column; three
classes stay on the bounded fail-closed probe. ADR-0055 row (a) and ADR-0056's
non-goal gain one status-line pointer each. Proposed; implements nothing.

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Oct 10, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 10, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b528a2e092c61631c8f031cc4819a44e58b2d48d
Local-runs: none

Inputs, and nothing else: card #22678 (body, claim 6100296545, dev report 6100561974); ruling 6097072172 on #22590 with that thread (15 comments; the dev report 6095496633, the decision request 6095537369 and the card-filing 6097212566 read in full); PR #22707's body, file list and net diff against main (origin/main = 61bea242, merge base d8830c28); the 35 check-runs on the head. Every tree claim below was read on origin/main with git show / git grep; nothing was built, run or re-run. The PR is a draft and docs/adr/** is Tier H: this record informs the maintainer's word and lifts nothing by itself.

① Derived judgments

What the diff implies. Three files, +277 / −1, all under docs/adr/: one new record (0139-engine-internal-semi-join-leaf-readable-parent-ids.md, 275 lines, Proposed) and one pointer line each on ADR-0055 and ADR-0056. No package path, no packages/spec, no driver, no .changeset/, no scripts/adr-anchors/ entry. Accept-set change: none. Public-surface change: none. Governed surface: docs/adr/** → Tier H; the PR is a draft; no seat merges, queues or arms it. main moved d8830c28 → 61bea242 since the dev's reading; the diff between those two commits touches none of the paths the record cites (empty over docs/adr and every cited package directory), so the dev's readings are current.

Scope against ruling 6097072172 — one engine-internal, non-authorable leaf; nothing wider. Each decision, named:

  • D1 (one branded $in comparand, unspellable by a request) — inside. The ruling's "one non-authorable filter leaf, never accepted from a request". Tree claims hold: FieldOperatorsSchema, VALID_AST_OPERATORS, parseFilterAST are in packages/spec/src/data/filter.zod.ts; assertListComparandShapes refuses a plain object at a comparand position with INVALID_FILTER (packages/objectql/src/filter-comparand-shape.ts, the predicate at its top and the code at L80); markFilterSubtreeProvenance keys on Symbol.for and carries 'policy' (filter-subtree-provenance.ts). No new operator, no authorable spelling. Right.
  • D2 (ObjectQL composes from canReadObject + getReadFilter, fail closed, tenant wall left to the driver) — inside. The contract reads as the record says: ISecurityService answers a DENY filter on any failure and forbids memoising across requests (packages/spec/src/contracts/security-service.ts, the header and the getReadFilter doc); RLS_DENY_FILTER exists; lowerRelationConditions is the engine seam; $in: [] is the FALSE identity the relation lowering documents (relation-filter-lowering.ts); the analytics door asks both halves over the base object plus every joined object (service-analytics/src/read-admission.ts). Right.
  • D3 (driver-sql compiles inside applyTenantScope; wasm and local Turso inherit; the remote transport compiles; memory implements; mongodb refuses; no capability bit) — inside the ruling's "driver-sql implements it, the other four drivers implement it or refuse loudly". The tree has exactly five driver packages; SqliteWasmDriver extends SqlDriver and TursoDriver extends SqlDriver are stated in-tree; remote-transport.ts carries its own buildWhereSQL compiler; memory-tenancy-guard.ts refuses multi-tenant boot; mongodb-filter.ts#translateFilter exists. The 17.0.0 retirement text for querySubqueries / joins is quoted verbatim from driver.zod.ts (L350–357). Right. One deliberate narrowing to name: the decision request's option-A text (6095537369) said the driver contract "re-declares what 17.0.0 retired"; the ruling itself requires no bit, and the record's compile-or-refuse shape satisfies "implement it or refuse loudly" under ADR-0049's reasoning. Inside scope; flagged in ③ for the maintainer's eye.
  • D4 (six consumers emit one leaf per parent object, no pre-scan; changed_fields column with a backfill) — inside: the ruling names the six and the column. All six gate symbols resolve; PARENT_GATE_SCAN_LIMIT = 2_000 (parent-record-read-gate.ts L68), the comment gate's READ_SCAN_LIMIT = 2_000 (comment-access-hooks.ts L197), the attachment gate's own READ_SCAN_LIMIT = 2_000 (attachment-access-hooks.ts L731); the pre-scan reads with { isSystem: true } and no tenant (parent-record-read-gate.ts L186–191); isWithheldOnlyUpdate judges metadata.old / metadata.new; sys_activity is managedBy: 'append-only'; migrateStoredMetadataBodyCopies, AUDIT_EXCLUDED_OBJECTS and diff exist where cited. Right — with one mis-attribution, (a) below.
  • D5 (three classes stay on the bounded probe: delegated caller, middleware-gated parent, federated parent) — inside: the ruling names exactly these three. Verified to the line: getReadFilter returns RLS_DENY_FILTER for an onBehalfOf context with the in-tree comment "the D10 delegator intersection is NOT implemented on this path … FAIL CLOSED" (security-plugin.ts, inside getReadFilter); the object-scoped read gates are registered exactly as listed — { object: 'sys_attachment' }, { object: COMMENT_OBJECT } / COMMENT_REACTION_OBJECT, { object } = sys_approval_request (request-read-gate.ts#bindVisibilityGate), { object: ACTIVITY_OBJECT }, { object: LEDGER_OBJECT }; resolveDatasourceBinding exists and ObjectQL.LIFECYCLE_DATASOURCE = 'telemetry'; ADR-0057 §3.6 is the datasource split. Right. Note, not a defect: the recognition rule as written ("registerMiddleware with an object") is wider than the list — hasObjectMiddleware does not distinguish read from write middleware, so sys_user_position and sys_permission_set (write-side registrations in security-plugin.ts) also fall in the class; that errs to the bound, the direction the record itself chooses.
  • D6 (ADR-0055 row (a) and ADR-0056's non-goal reversed for this one leaf; the compiler gains no form) — inside. The quoted texts are verbatim on origin/main: ADR-0055 L50 (row (a)), L73 (the known limit), ADR-0056 L169 (the non-goal); the alternatives table does sit in ADR-0055 §2 (L37–53). Right.
  • Measurements (5,042 rows; 1,808 readable; 1,105 served and total 1,105; administrator at exactly 2,000; the warning on every read; main 1b99388505) — match report 6095496633 exactly. Right.
  • Number 0139 — origin/main holds 0137 and 0138, no 0136, no 0139; max+1 is right.
  • The two pointers — ADR-0055's is a continuation of its status line; ADR-0056's is on its non-goal bullet (L169), not its status line.

Claims that do not hold as written — three, none moves a decision:

  • (a) L177–178 — #isWithheldOnlyUpdate is written as a continuation after objects/sys-activity.object.ts#sys_activity; the symbol is packages/plugins/plugin-audit/src/activity-field-redaction.ts#isWithheldOnlyUpdate (L228). The anchor grammar inherits a path only from an anchor earlier on the SAME line and silently drops a continuation that opens its line (scripts/symbol-anchors.mjs, if (!prior) continue;), so check-adr-symbol-anchors never judged it — the green gate is not evidence here. The same orphan spelling sits at L78 (#VALID_AST_OPERATORS), L79 (#parseFilterAST) and L205 (#LIFECYCLE_DATASOURCE); those three are correct by hand-read but equally unjudged. Wrong, and invisible to the gate.
  • (b) L24, L211 — "the four-form fail-closed RLS compiler" / "whose four forms … are unchanged". The compiler's own doc says its subset "is now broader than the historical four forms" (rls-compiler.ts#compileExpression); the wording is inherited from ADR-0055 L89. The load-bearing half — a subquery shape compiles to null and fails closed — holds. Stale descriptor.
  • (c) L215, L259–260 — "Each record carries one status-line pointer" / "the two status-line pointers it carries". ADR-0056's pointer is on its non-goal bullet, not its status line; the PR body describes the placement correctly, the record's own acceptance criterion does not. The claim is about the diff, and it is inexact.

② Semver level

The diff publishes nothing from any released package: three Markdown files under docs/adr/, no .changeset/ entry, no package path. The skip-changeset label is present and is the right declaration for this class (⛔ not a bug fix in a released package). Check Changeset: success. Body line 2 reads Clause-②: no with no arm, which is right for a diff with no accept-set and no export change — the leaf it decides is non-authorable and nothing of it lands here. Matches.

③ Boundary flags

  • Dev flags (report 6100561974): (1) number 0139, not 0136 — answered above, right; (2) 275 lines against a suggested 250 — answered: the overage is the six-consumer table, the seven-row execution plan the ruling requires and D5's three classes; nothing cuts without dropping a ruling-required item; (3) the commit-trailer pair — not this review's surface. open_questions: []. The three out_of_scope_findings are noted-not-filed, correctly under Prime Directive chore: version packages #10: a stale path in an agent-read table, a 5,000-row grant read with no warning (carried as the record's O4), and a card attribution — none is a reproducible defect, a contract violation or an authoring trap on its own.
  • The record's O1–O5: each is marked non-blocking and bound to an execution card; O3 (mongodb refuses until measured) and D5.3 (a telemetry-split deployment keeps letter C for the activity stream) are limits the maintainer should read as such — the 维护者速读 names both.
  • Escalated to the maintainer (Tier H): the narrowing in D3 (no DriverCapabilities bit returns, where the decision request's option-A text re-declared one) — inside the ruling's letter, a deliberate choice, theirs to confirm.
  • Check-runs on the head: all 35 concluded — 24 success, 11 skipped, 0 failure. Lint & Repo Gates, Type Check · workspace and Test Core (1/6) were in progress at the dev's report and have since concluded success. Governed Surface Queue Guard: success. Green is necessary; it carries no information on (a)–(c), which no gate reads.

Why FAIL, and what clears it. D1–D6 are inside the ruling's scope, the quotes and measurements are exact, and every tree claim that bears on a decision holds. What fails is the record as text to be approved: one mechanism pointer names the wrong file and the gate cannot see it (a), one descriptor is stale (b), and the record's own acceptance criterion describes a pointer the diff does not carry (c). A governed record goes to the maintainer correct, not with known errors for them to spot. The fix is three edits on the dev's branch — re-anchor L178 as packages/plugins/plugin-audit/src/activity-field-redaction.ts#isWithheldOnlyUpdate and spell L78, L79 and L205 as full path#symbol anchors so the gate judges them; drop "four-form" at L24 and L211 (or say "closed form set, historically four"); at L215 and L259–260 say "one pointer each: ADR-0055's status line, ADR-0056's non-goal bullet". The re-review on the fixed head is a wording check; nothing else in this record needs re-deriving.

Implemented-by: claude/issue-22678-semi-join-leaf-adr
Reviewed-by: session_01JfJfBUC3cQ6hhgm9MQK76T

VERDICT: FAIL

Read at 2026-10-10T18:16Z — the card and its two comments, the ruling and the #22590 thread, PR #22707's body, files and net diff against origin/main 61bea242, and the 35 check-runs on b528a2e092.

objectstack-fleet Bot and others added 3 commits October 10, 2026 18:22
(a) re-anchor isWithheldOnlyUpdate to activity-field-redaction.ts and spell
the VALID_AST_OPERATORS, parseFilterAST and LIFECYCLE_DATASOURCE references as
full path#symbol anchors so check-adr-symbol-anchors judges them; (b) replace
"four-form" with the compiler's own description (closed pushdown form set,
historically four, broader today); (c) the acceptance criterion and D6 name
the two pointers as the diff carries them: ADR-0055's status line, ADR-0056's
non-goal bullet.

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
…s full path#symbol anchors

A bare `#symbol` opening its line is skipped by the anchor grammar's
continuation rule, so check-adr-symbol-anchors never judged these nine:
canReadObject, getReadFilter (security-service.ts); parseFilterAST,
assertListComparandShapes, FieldOperatorsSchema, VALID_AST_OPERATORS (D1);
markFilterSubtreeProvenance (D2); applyFilterCondition, applyTenantScope (D3);
computeParentRecordFilter (D4, E3/E5). Anchor spelling and re-wrapping only;
no decision text changes.

Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f87ef2345a81297eee220c96b9048f7bedc57ed0
Local-runs: none

Re-review on the fixed head, after contract review 6100682640 (FAIL on b528a2e092, three wording items (a)–(c)) and the dev's two wording rounds (reports 6100837448, 6100992444). Inputs, and nothing else: card #22678 (body and all five comments: the claim 6100296545, the three dev reports, the seat's REWORK 6100692842); ruling 6097072172 on #22590 with that thread (15 comments, read in full); PR #22707's body, its one comment (the earlier record), its file list, its four commits and the net diff against main (merge base 9f159397fb; origin/main = c63028e5bf at this read); the check-runs on the head. Every tree claim below was read on origin/main with git show / git grep; nothing was built, run or re-run. The PR is a draft and docs/adr/** is Tier H: this record informs the maintainer's word and lifts nothing by itself. The whole record is judged on this head, not only the three items.

① Derived judgments

What the diff implies. Three files, +287 / −1, all under docs/adr/: the new record 0139-engine-internal-semi-join-leaf-readable-parent-ids.md (285 lines, Proposed) and one pointer line each on ADR-0055 (a continuation of its status line) and ADR-0056 (on its non-goal bullet). No package path, no packages/spec, no driver, no .changeset/, no scripts/adr-anchors/ entry (none is owed: the record implements nothing). Accept-set change: none. Public-surface change: none. Governed surface: docs/adr/**, Tier H; the PR is a draft; no seat merges, queues or arms it. main moved one commit since the merge base (c63028e5bf, metadata-protocol search exposure plus one plugin-approvals test); it touches no path the record cites, so every reading below is current.

The three items from 6100682640, each on this head:

  • (a) CLEARED. The isWithheldOnlyUpdate pointer (now L184) is packages/plugins/plugin-audit/src/activity-field-redaction.ts#isWithheldOnlyUpdate, and that symbol is declared at L228 of that file on origin/main. The three references the first record named as orphan continuations (VALID_AST_OPERATORS L79, parseFilterAST L80, LIFECYCLE_DATASOURCE L212) are full path#symbol anchors. Round 2 went further than asked and spelled the nine remaining line-opening continuations in full (D1, D2, D3, D4, Context, E3/E5). On this head the record carries 54 anchor spans: 49 full anchors and 5 same-line continuations (#LEDGER_AUDIT_CAPABILITY L66, #READ_SCAN_LIMIT L70, #joins L81, #VALID_AST_OPERATORS L113, #compileExpression L220), each with a full anchor earlier on its own line, which is the one continuation form the grammar in scripts/symbol-anchors.mjs inherits a path from. A scan for a backticked #symbol opening a line finds zero. All 46 distinct targets resolve on origin/main: 42 symbol anchors to a declaration in the named file (canReadObject is the optional method at security-service.ts L793, compileExpression the method at rls-compiler.ts L1125, sys_activity the name: at sys-activity.object.ts L20) and 4 file-level anchors to a tracked file. Nothing in the record is now outside what the anchor gate judges.
  • (b) CLEARED. "four-form" is gone at both sites. L24 reads "the fail-closed RLS compiler and its closed pushdown form set (historically four, broader today)"; D6 (L218) reads the same and adds "#compileExpression: a subquery shape compiles to null and fails closed". That is the compiler's own doc on origin/main (rls-compiler.ts L1112–1116: "the supported subset is now broader than the historical four forms … a non-pushdownable shape (subquery, …) → null → compileFilter fails closed"). ADR-0055's own L89 still says "four-form"; that is ADR-0055's text, untouched by this diff, and the record no longer repeats it.
  • (c) CLEARED. D6 (L224–225) reads "Each record carries one pointer to this one … ADR-0055's status line, ADR-0056's non-goal bullet"; the acceptance criterion (L269–270) reads "the one pointer each it carries: ADR-0055's status line … and ADR-0056's non-goal bullet". The diff carries exactly that: ADR-0055 gains a line continuing its **Status** paragraph (L4), ADR-0056's non-goal bullet (L169) gains a trailing clause, and nothing else in either file moves.

The two rounds changed nothing else. The b528a2e092→head diff on the record is anchor spelling and re-wrapping, the compiler descriptor, the #compileExpression citation, and the two pointer sentences; no decision sentence, measurement, quotation or table row moved. So D1–D6 are re-judged on this head with the first record's tree facts re-read, and each is named:

  • D1 (one branded $in comparand, unspellable by a request) — inside the ruling's "one non-authorable filter leaf, never accepted from a request". FieldOperatorsSchema, VALID_AST_OPERATORS, parseFilterAST are in packages/spec/src/data/filter.zod.ts; assertListComparandShapes is in packages/objectql/src/filter-comparand-shape.ts; markFilterSubtreeProvenance in filter-subtree-provenance.ts. No new operator, no authorable spelling. Right.
  • D2 (ObjectQL composes from canReadObject + getReadFilter, fail closed, tenant wall left to the driver) — inside. ISecurityService (L406), canReadObject (L793) and getReadFilter (L447) are the contract in packages/spec/src/contracts/security-service.ts; RLS_DENY_FILTER is exported at rls-compiler.ts L680; lowerRelationConditions is the engine seam; read-admission.ts exists in service-analytics. Right.
  • D3 (driver-sql compiles inside applyTenantScope; wasm and local Turso inherit; the remote transport compiles; memory implements; mongodb refuses; no capability bit) — inside "driver-sql implements it, the other four drivers implement it or refuse loudly". packages/drivers/ holds exactly five packages; SqliteWasmDriver extends SqlDriver (L67) and TursoDriver extends SqlDriver (L1586); RemoteTransport (L988), translateFilter (L916), memory-tenancy-guard.ts exist; querySubqueries (L350) and joins (L356) are retiredKey in driver.zod.ts with the quoted words. Right. The deliberate narrowing (no DriverCapabilities bit returns, where the decision request's option-A text re-declared one) stands as the first record read it: inside the ruling's letter, flagged in ③.
  • D4 (six consumers emit one leaf per parent object, no pre-scan; changed_fields column with a backfill) — inside: the ruling names the six and the column. PARENT_GATE_SCAN_LIMIT = 2_000 (parent-record-read-gate.ts L68, whose own doc says "the comment gate's bound", which is what the record's "reused" at L60 means), the comment gate's READ_SCAN_LIMIT = 2_000 (L197), the attachment gate's own READ_SCAN_LIMIT = 2_000 (L731); sys_activity is managedBy: 'append-only' (L25); AUDIT_EXCLUDED_OBJECTS (L253), diff (L383), migrateStoredMetadataBodyCopies (L370) are where cited. Right, and the mis-attribution from (a) is gone.
  • D5 (three classes stay on the bounded probe) — inside: the ruling names exactly these three. security-plugin.ts#getReadFilter (L6040) returns RLS_DENY_FILTER for an onBehalfOf context with the in-tree "D10 delegator intersection is not implemented" message (L6079–6087); the object-scoped registrations are as listed ({ object: 'sys_attachment' } L787 of attachment-access-hooks.ts, COMMENT_OBJECT / COMMENT_REACTION_OBJECT L808–809 of comment-access-hooks.ts, ACTIVITY_OBJECT L147, LEDGER_OBJECT L163); resolveDatasourceBinding (L10156) and LIFECYCLE_DATASOURCE = 'telemetry' (L10017) are in engine.ts; ADR-0057 §3.6 exists (L264). Right. The first record's note stands and is not a defect: the recognition rule as written is wider than the list and errs to the bound, the direction the record chooses.
  • D6 (ADR-0055 row (a) and ADR-0056's non-goal reversed for this one leaf; the compiler gains no form) — inside. Row (a) and the known limit are verbatim on origin/main ADR-0055 (L50, L73); the non-goal verbatim on ADR-0056 (L169). Right, and now exact about the pointers, (b) and (c) above.
  • Measurements (5,042 / 1,808 / 1,105 and total 1,105 / administrator at 2,000 / warning on every read / main 1b99388505) — match report 6095496633. Number 0139 — origin/main holds 0137 and 0138, no 0136, no 0139; max+1 is right. Cross-references — ADR-0052 §5 and ActivityPointer, ADR-0090 D10, ADR-0096 D5, ADR-0053 D-D1, ADR-0057 §3.6 all exist where cited.

No claim in the record is found wrong on this head.

② Semver level

The diff publishes nothing from any released package: three Markdown files under docs/adr/, no .changeset/ entry, no package path. The skip-changeset label is present and is the right declaration for this class (not a bug fix in a released package). Body line 1 Fixes #22678, line 2 Clause-②: no with no arm, right for a diff with no accept-set and no export change; the leaf the record decides is non-authorable and nothing of it lands here. Check Changeset reads skipped on this head, which is pr-automation.yml's own opt-out on that label, not a verdict; the diff itself is the evidence. Matches.

③ Boundary flags

  • Dev flags, all three rounds. Round 0 (6100561974): number 0139 not 0136, 285 lines against a suggested 250 (the six-consumer table, the seven-row plan the ruling requires and D5's three classes), the model-free trailer pair; the first two answered right above, the third not this review's surface. Round 1 (6100837448): nine orphan continuations left outside the three-item fence; four PR-body sentences gone stale. Round 2 (6100992444): the nine are spelled in full, and the body now names head f87ef2345a, 2306 anchors and 285 lines (the body's updated_at follows the push). Both answered. deviations: none in rounds 1 and 2; open_questions: [] in all three rounds, nothing to escalate from them. out_of_scope_findings: round 0's three (a stale path in compile-surfaces.md; sharing-service.ts grant reads at limit: 5000, L507 and L584, carried as the record's O4; a card attribution for RELATION_FILTER_ID_CAP, which is at relation-filter-lowering.ts L84 as the record says) are noted-not-filed, correctly under Prime Directive chore: version packages #10; round 1's one (the nine continuations) is discharged by round 2; round 2 reports none.
  • The record's O1–O5 are each non-blocking and bound to an execution card; O3 (mongodb refuses until measured) and D5.3 (a telemetry-split deployment keeps letter C for the activity stream) are limits for the maintainer to read as such, and the 维护者速读 names both.
  • Escalated to the maintainer (Tier H, unchanged from the first record): the D3 narrowing, no DriverCapabilities bit where the decision request's option-A text re-declared one, inside the ruling's letter, a deliberate choice, theirs to confirm or reverse on the approval.
  • Check-runs on the head, at this record's read: 38 runs, 26 success, 11 skipped, 0 failure, 1 in progress: Lint & Repo Gates (the job that carries check-adr-symbol-anchors, check:adr-anchors, check:doc-authoring, check:pm-governed-merges, check:pm-governed-prose, check:pm-prior-rulings). Not waited for. Concluded success: Governed Surface Queue Guard, Check Documentation Links (check-adr-links), Type Check · workspace, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Test Core 1/6–6/6, Dogfood Regression Gate, Check PR Size, the four card-and-claim guards. Skipped by path or label: Build Core, Build Docs, Console Pin Gate, Temporal Conformance, Check Changeset, the opt-in smoke. The anchor question Lint & Repo Gates will answer is the one ① answered by hand on origin/main, every span, and nothing in the diff reaches any other gate in that job; the seat that acts next reads its conclusion, and a red there reopens this record.

Why PASS. D1–D6 stay inside ruling 6097072172, the quoted rulings, measurements and ADR texts are exact, every tree claim holds on origin/main, and the three wording items that failed the first record are each cleared on this head with no decision text moved. The record now goes to the maintainer without a known error in it. What it needs next is not a seat act: the maintainer's APPROVED review on this PR (Tier H), after which the owning seat lands it and cuts E1–E7 in the record's order; the PR stays a draft until then.

Implemented-by: claude/issue-22678-semi-join-leaf-adr
Reviewed-by: session_01JfJfBUC3cQ6hhgm9MQK76T

VERDICT: PASS

Read at 2026-10-10T18:59Z — the card and its five comments, the ruling and the #22590 thread, PR #22707's body, comment, files and net diff against origin/main c63028e5bf, and the check-runs on f87ef2345a.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)· PR #22707 · ADR-0139 · head f87ef2345a

domain:engine 席 1(#6367)· session_01JfJfBUC3cQ6hhgm9MQK76T · 2026-10-10T19:06Z。席位对照自己读的 diff 校正了草稿,并填好席位意见。

改了什么: 新增 ADR-0139(状态 Proposed),把你 10 月 10 日「同意」的方案 A 写成正式决策:引擎内部加一个过滤叶子,含义是「这个调用者能读的 X 对象 id」。它不可编写,请求里永远拼不出来。ObjectQL 用 canReadObject 与 getReadFilter 组合读范围,下推成对父表的子查询;driver-sql 编译它,其余驱动编译或响亮拒绝。六个树内消费者(动态、审计台账、撤回更新规则、评论、表情、附件)改用它,撤回更新规则改为写入时盖一列 changed_fields 并回填。三类(委托上下文、自带插件读闸的父对象、跨数据源父对象)保留今天的有界探针。ADR-0055 与 ADR-0056 各加一行指针。本 PR 只有文档,不改代码、spec 或驱动;执行卡 E1–E7 批准后才立。

为什么改: 实测一位普通成员应看到 1,808 条动态,系统只给 1,105 条,「总数」也显示 1,105;管理员恰好被截在 2,000 条。原因是六处读闸都靠一个 2,000 行的预扫描窗口,多组织部署下所有组织的行挤同一个窗口。ADR-0055 当年明确拒绝过子查询,按基本指令第 13 条,反转一个已记录的决定需要新 ADR,所以文档先于代码。

风险与代价(含回滚): 这是真实的能力扩张,但收窄为一个内部叶子;工程量约 3–5k 行、6–10 个 PR,横跨三条车道。已点名的限制有三条。

  • driver-mongodb 在量过之前,这六类读会响亮报 500,而不是悄悄少给。
  • 注册了 telemetry 独立数据源的部署,动态表与父表不在同一驱动,这个叶子帮不上,仍靠过渡方案 C 的响亮 400。
  • 方言绑定参数上限只取了文档值,未实测。

回滚:未批准即不生效;批准后每张执行卡都可单独不落地,文档本身可整体 revert。

席位意见: 建议批准。决策都在你的裁决范围内,两轮契约复审(先 FAIL 于措辞,再 PASS)确认引文、读数与锚点都成立。需要你确认的只有一处:D3 选择不恢复驱动能力位。好处是 AI 与作者没有新的可写能力位,驱动要么编译、要么响亮拒绝;代价是 MongoDB 部署在实测前读这六类对象会报错,而不是悄悄少给。这符合「宁可响亮失败也不静默」的方向。若你更希望 MongoDB 先退回有界探针,在批准时说一句,席位会让执行卡 E4 照此做。

你要做的: 在 PR #22707 上给出 APPROVED review;要改的决策点写在本评论下即可。


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants