Repository navigation
Commit c76cdeb
docs(adr): ADR-0147 — membership-scoped RLS lowers to an engine-internal org-member semi-join leaf; the visible set stays exactly what it is (Proposed) (#29501)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi93GrVeZda4ZCMrastvdgi1C)_
Fixes #29360
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:新增 ADR-0147(Proposed),决定 `id in current_user.org_user_ids` 这类成员范围
RLS 不再每次请求预读整个组织成员列表,而是下推为数据库子查询;另给 ADR-0139 D1/D3 与 ADR-0057 D3
各加一条带日期的指针注记。
- 为什么改:#29281 修完后成员列表已精确,但大组织每个请求都要多读一次成员表,且成员数超过数据库绑定参数上限(SQLite 约 4
万、Postgres 65,535)时 `sys_user` 读取直接 500。
- 风险与回滚:纯文档,不改代码、不改 schema;可见集合由
D-0「同集合不变式」锁死,任何会改变"谁能看到谁"的方案都列为已否决。回滚即撤销本 PR。
- 先看:`docs/adr/0147-…md` 的 D-0(不变式)、D-d(不编译的驱动走精确数组兜底,绝不拒绝)、D-e(读取方清单)。
- 两分钟验证:`node scripts/check-adr-symbol-anchors.mjs && node
scripts/check-adr-links.mjs`,预期两行 ✅,ADR-0147 的每个代码锚点都能解析。
- 需要你做的:批准本 PR(Tier H,含对 ADR-0139「只此一个叶子」「无能力位」两条禁令在本叶子上的修订),命名已由席位定为
`OrgMemberIds`(驱动编译声明的名字由 spec 卡定),无需你另行确认。
## What changes
- **New:
`docs/adr/0147-membership-scoped-rls-lowers-to-an-engine-internal-org-member-semi-join-leaf.md`,
Status `Proposed`.** It decides D-a to D-g as #29360 lists them (the
#29281 design, os-dev-report `6577634380`; seat ruling `6577690137` item
4), plus **D-0, the same-set invariant** triage `6580364238` asked for
as a decision of its own: the visible set is `S(org, caller) = {caller}
∪ {non-blank sys_member.user_id of the active org}`, `{caller}` with no
active org, and every lowering answers exactly S on every driver,
posture, polarity and size.
- **D-a** keeps the spelling. `FIELD in current_user.org_user_ids`
lowers to a branded sibling of `ReadableIds` (proposed name
`OrgMemberIds`; the S1 spec card names the driver declaration; naming is
the lane seat's call), carrying the resolver's own `(organizationId,
callerId)` anchor. A context that carries an array still lowers the
array.
- **D-b** fixes the inner read to today's read: active-org equality,
system authority, no tenant wall (in group posture the wall would widen
S), no validity, status or ban filter (each narrows S), and the blank-id
guard (which also keeps `!(… in …)` NULL-safe).
- **D-c** ORs the anchor's caller in, never the compiling context's
`userId` (the delegated explainer carries the principal's list under the
delegator's id).
- **D-a / D-d revisit two ADR-0139 prohibitions for this one leaf**:
D1's "No second leaf" (kept in spirit: the second meaning gets its own
brand) and D3's "No `DriverCapabilities` bit returns" (a consulted
declaration, because a refusal is excluded here).
- **D-d**: compile, or **the floor**, the exact completion-read array of
PR #29356, chosen up front and never a refusal. That covers MongoDB,
OData, federated objects, the in-process judges and the analytics
native-SQL read-scope compiler (`service-analytics`
`compileScopedFilterToSql`). The floor substitution descends into
composed `ReadableIds` scopes. The explainer publishes the array as
`readFilter`, today's wire shape.
- **D-e** is the reader census re-measured on `cafe8ee6`, corrected from
the card: the current-user endpoint and the annotation pins read
nothing; the automation, sharing and transport carriers and the presence
probes were missing. It says which sites move to the anchor and which
keep the array.
- **D-f** records `rlsMembership` leaves as out of scope, with the
reason.
- **D-g** is the acceptance: PR #29356's two pins byte-unchanged, plus
group-posture, NULL-`user_id` negation, caller-arm, no-active-org,
delegation, floor-parity, analytics, nested and explain-`readFilter`
pins.
- **Execution plan**: S1 spec card → V1 lowering and floor → V2 SQL
family and memory → V3 request path → V4 MongoDB (optional).
-
**`docs/adr/0139-engine-internal-semi-join-leaf-readable-parent-ids.md`**:
two dated, append-only pointer notes, under D1's "No second leaf" and
under D3's capability-bit bullet. `ReadableIds` keeps both as written,
and the Status line is untouched.
-
**`docs/adr/0057-erp-authorization-core-business-units-and-scope-depth.md`**:
one dated amendment note under D3's "mirroring the `org_user_ids` cap"
bullet, and one inline `· **Amended**` entry on its Status line pointing
at it (after the ADR-0131 entry; the index still keys on `Accepted`).
The cap no longer exists after PR #29356. D3's own bound requirement is
unchanged.
- **`docs/adr/README.md`**: regenerated by `node scripts/adr-index.mjs`
(one row and the counts).
No code, no schema, no test. `skip-changeset`: `docs/adr/**` publishes
nothing.
## Tests
None: an ADR-only diff. No forced test edits.
## Acceptance notes
- **Round 2** answers contract review `6583043471` (FAIL on `f3a117fb`):
both must-fixes and should-fixes a, b, c and e land in the records.
Round 3 applies the seat's two rulings on round 2: should-fix d
(ADR-0057-erp's Status line gains its inline `Amended` entry), and
naming is the seat's call, so O1 carries no maintainer gate.
- **ADR number.** 0147 is the next free number. The highest on `main`
`cafe8ee6` is 0146, and none of the 43 open PRs adds a `docs/adr/0*.md`
(each head fetched, `git diff --name-status origin/main HEAD --
docs/adr`, zero `A` rows). 0136 and 0140 are unused gaps; they are not
taken, since numbers are never reassigned.
- **Out of scope, carried:** the
`packages/plugins/plugin-auth/src/admin-set-user-manager.ts` comment
quotes ADR-0057 D3's "`org_user_ids` cap". The quotation stays accurate
as a quotation, and correcting the comment is a server-lane code edit,
so V3 carries it (ADR-0147 O6).
- **New public names**: the leaf is `OrgMemberIds`; S1 names the
driver's compile declaration. Naming is the lane seat's call (ADR-0147
O1).
- **Seat's mechanism assumptions, measured:** the spec sites are
`rls.zod.ts`'s module header ("Subquery-shaped needs are
pre-resolved…"), its "does not lower" paragraph and "Dynamic
membership", and `execution-context.zod.ts`'s `org_user_ids` and
`rlsMembership` docs. That matches the card; the line numbers have
drifted. Generalising `ReadableIds` would make S depend on the caller's
grants on `sys_member`, so the sibling leaf is chosen (Alternatives).
## Verification
<details><summary>Derived gates on `723e27a6`: 21 of 21 exit 0, --ran
reconciled</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 21 commands (4 paths, +412 / -4).
The lint closure was built first (at `d55c14e4`; `formula` and `lint`
are untouched since), because `check:doc-formula-expressions` reads
`@objectstack/formula` and `@objectstack/lint` dist: `os-verify-lock.sh
-c 'pnpm exec turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint --concurrency=2'`, VERDICT command-exit 0.
Every command ran with stdin from `/dev/null`, its exit code captured
before any pipe:
| exit | command |
|:--|:--|
| 0 | `node scripts/check-adr-links.mjs` |
| 0 | `node scripts/check-adr-links.mjs --self-test` |
| 0 | `node scripts/check-closing-keyword-parity.mjs` |
| 0 | `node scripts/check-closing-keyword-parity.mjs --self-test` |
| 0 | `node scripts/check-comment-mask-corpus.mjs` |
| 0 | `node scripts/ci/plan-test-core-shards.mjs --self-test` |
| 0 | `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` |
| 0 | `pnpm check:adr-anchors` |
| 0 | `pnpm check:adr-index` |
| 0 | `pnpm check:adr-symbol-anchors` |
| 0 | `pnpm check:ci-filter-parity` |
| 0 | `pnpm check:closed-ceremony-text` |
| 0 | `pnpm check:cross-package-test-inputs` |
| 0 | `pnpm check:doc-authoring` |
| 0 | `pnpm check:driver-memory-census` |
| 0 | `pnpm check:gitlink-declared` |
| 0 | `pnpm check:nul-bytes` |
| 0 | `pnpm check:pm-governed-merges` |
| 0 | `pnpm check:pm-prior-rulings` |
| 0 | `pnpm check:refd-timer-probe` |
| 0 | `pnpm check:watch-hint-literal` |
`node scripts/pm/dispatch-gates.mjs --ran FILE --repo
objectstack-ai/objectstack` (exit 0): "21 derived famil(ies) accounted
for — 21 run, 0 NOT-MEASURED (a DERIVED zero — all 21 recorded an exit
code and none of them is 3)".
`check-adr-symbol-anchors`: 3037 anchors across 149 records resolve.
`check-adr-links`: 979 relative destinations resolve. `adr-index
--report`: README current.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(n/a: no schema)
- [x] 2. Every name I added uses the casing for its kind.
(`OrgMemberIds`, PascalCase like `ReadableIds`)
- [x] 3. No formatter ran; `pnpm lint` passes. (no lintable file in the
diff)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset. (publishes
nothing: `skip-changeset`)
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
maintainer-facing exception the lane prescribes).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
**Governed surface (`docs/adr/**`, Tier H): this PR stays DRAFT and
waits for a `GOVERNED_APPROVERS` approval.**
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01ADHkmSNasUm3e1dfbV5aaD
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmSNasUm3e1dfbV5aaD)_
<!-- signed: domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut
-->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 85ea224 commit c76cdeb
4 files changed
Lines changed: 412 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
163 | 171 | | |
164 | 172 | | |
165 | 173 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
138 | 143 | | |
139 | 144 | | |
140 | 145 | | |
| |||
178 | 183 | | |
179 | 184 | | |
180 | 185 | | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
181 | 192 | | |
182 | 193 | | |
183 | 194 | | |
| |||
0 commit comments