Skip to content

Commit a2e94c2

Browse files
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)
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](https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T)_ --------- Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 165587e commit a2e94c2

3 files changed

Lines changed: 287 additions & 1 deletion

‎docs/adr/0055-master-detail-controlled-by-parent.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ADR-0055: Master-detail "controlled by parent" permissions — derived access via pre-resolved master-id membership
22

33
**Status**: Accepted (2026-06-19) — implemented in this PR (P0–P2) · **Amended** (2026-09-07, #11082 / PR #11183 — the "Single-level only in v1" limit under Consequences and the "Transitive nested master-detail chains" **Non-goal** are both **reversed**: since `@objectstack/plugin-security` 17.3.0 (merged 2026-08-23) the read derivation and the write gate compose `controlled_by_parent` across a chain, bounded by a cycle guard and a cost ceiling that both fail **closed**. Nothing else in this record changes — §1–§4, the mechanism choice, the set-size limit and the share-table non-goal all stand. See **"Amendment (2026-09-07, #11082): transitive chains compose — the single-level non-goal is reversed"** at the end.)
4+
· **Revisited in part** (2026-10-10, [ADR-0139](./0139-engine-internal-semi-join-leaf-readable-parent-ids.md), Proposed) — §2's alternatives row (a) is reversed for ONE engine-internal, non-authorable leaf the engine composes from the compiler's output; the RLS compiler still has no subquery form, and §2's chosen mechanism (b), its `rlsMembership` IN-form and the set-size limit all stand.
45
**Deciders**: ObjectStack Protocol Architects
56
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove), [ADR-0054](./0054-runtime-proof-for-authorable-surface.md) (prove-it-runs)
67
**Surfaced by**: an audit of master-detail permission semantics — `OWDModel.controlled_by_parent` is **declared but unenforced** (zero runtime consumers; not reachable through the object's `sharingModel` enum; the RLS compiler is relationship-blind). This is *false compliance* (ADR-0049) and *unproven liveness* (ADR-0054).

‎docs/adr/0056-permission-model-landing-verification.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ The one-time table above rots. Make it a **living ledger**: extend the ADR-0054
166166

167167
- **Not** a permission-model rewrite — the Salesforce-shaped model stays; this is landing verification.
168168
- **Not** designing the compliance/encryption/masking subsystems (D8 only triages their *status*).
169-
- **Not** adding RLS-compiler subquery support (ADR-0055 constraint stands; widening uses pre-resolved `rlsMembership`).
169+
- **Not** adding RLS-compiler subquery support (ADR-0055 constraint stands; widening uses pre-resolved `rlsMembership`). · **Revisited in part** (2026-10-10, [ADR-0139](./0139-engine-internal-semi-join-leaf-readable-parent-ids.md), Proposed): the compiler gains no subquery form; ONE engine-internal, non-authorable leaf is composed by the engine from the compiler's output.
170170
- **Not** ServiceNow-style per-row ACL scripts, nor client-side enforcement (server is the boundary).
171171

172172
## Alternatives considered

0 commit comments

Comments
 (0)