Repository navigation
Commit 2b752f1
docs(spec): the cbp-optional-master-admitted entry states the current write rule, and the security-service contract names ownerMayEdit (#31210)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XGML1ZArFc2ffDxPnNbY42U)_
Fixes #31169
Fixes #31180
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:两处纯文字。① 迁移登记里 `cbp-optional-master-admitted`
这一条的说明文字(`replacement` 与文件头注释),以及升级指南里同一条的那段叙述:不再说「主表引用为空的写入一律
422」,改为现行规则——可选主表为空的行由属主或该子对象「修改全部」写,其他人(含「查看全部」)403;不挂主表的新建放行;只有必填主表为空仍是
422。② 安全服务契约(`security-service.ts`)的 `allow` 一条和
`checkControlledByParentWrite` 说明补上「属主可编辑」开关(`ownerMayEdit`):开了它,记录属主本人按
id 更新、非代办、不清空可选主表时,只要能读主记录即可。
- **为什么改**:PR #31082 已按您对 #30905 的裁决「A」放开写门,这条迁移说明还停在读侧落地时的旧句子,`os
migrate meta` 和据此生成的发布说明会告诉作者错误的答案;契约注释则从没写过 #25760
裁决的属主开关,读起来像「一律要主记录编辑权」。
- **风险与回滚**:纯文字,加一个 `@objectstack/spec` patch
changeset;`id`、`surface`、`reason`、`acceptanceCriteria`、schema
与运行时一概不动。回滚即 revert。
-
**先看**:`packages/spec/src/migrations/entries/semantic/18.cbp-optional-master-admitted.ts`
的 `replacement`;`packages/spec/src/contracts/security-service.ts` 的
`allow` 一条。
- **两分钟验证**:`git diff origin/main --
packages/spec/src/`,预期只有这条迁移的说明字符串、头注释、模板里同一段叙述和两段 JSDoc 变化。
- **需要你做的**:无,席位落地。
## What changes
Release text only, measured against the write gate PR #31082 landed
(`037371559b`,
`packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts`:
`nullMasterRowWritable`, the `masterId == null && rel.optional` branch
of `assertControlledByParentWrite`, and its `403 PERMISSION_DENIED`
refusal `nullMasterOwnerDenied`):
1.
`packages/spec/src/migrations/entries/semantic/18.cbp-optional-master-admitted.ts`,
`replacement`: the sentence "Writes are unchanged by this entry: an
insert or update that leaves the master reference empty is still refused
with 422" is replaced by the current rule. An insert that names no
master is admitted and its creator owns the row; its owner and a holder
of Modify All on the detail update or delete a row whose master
reference is empty, and everyone else who reaches the write gate, View
All included, is refused `403 PERMISSION_DENIED`. A row with no owner,
or on an object without an `owner_id` column, is written through Modify
All alone. An update that empties a named master asks edit access on the
master it leaves. A required master is unchanged: a stored empty
required master reference is still refused with `422
MISSING_REQUIRED_FIELD`. The wording follows
`content/docs/protocol/kernel/error-handling.mdx` and
`content/docs/api/error-catalog.mdx` (PR #31164) and the permissions
pages of PR #31092, so the four texts agree. The replacement string
carries no tracker number (`check:doc-authoring`).
2. Same file, header comment: a paragraph naming the write side,
ADR-0055 §3 as amended, the same ruling A, citing PR #31082 beside the
read-side citation.
3. `packages/spec/src/migrations/registry.ts.template`, the step-18
rationale fragment `cbp-optional-master-admitted` (the upgrade-guide
paragraph whose D3 record is this entry): "Writes are unchanged: an
empty master reference is still refused with 422" now reads that a
detail row naming no master is written by its owner and by Modify All on
the detail and refused with 403 for everyone else, and only an empty
required master reference is still refused with 422. Fixed in place
under os-dev rule 3's bounded exemption (same stale sentence about the
same entry, mechanical, no other claim on the paragraph, the same
`check:upgrade-guide` / `check:spec-changes` family). The claim's file
surface does not name this file; the seat is asked to amend it.
4. **#31180 (folded, its own commit):**
`packages/spec/src/contracts/security-service.ts`, JSDoc only. The
`allow` bullet of `ControlledByParentWriteOutcome` and the
`ISecurityService.checkControlledByParentWrite` docblock now name the
`sharing.controlledByParent.ownerMayEdit: true` opt-in, citing ADR-0055
§3 as amended 2026-10-26. Conditions measured from the owner leg in
`assertControlledByParentWrite` (`security-plugin-cbp-write-gate.ts`,
the `operation === 'update' && detailRow !== null && rel.ownerMayEdit &&
…` branch): a by-id update; not on behalf of another user
(`opCtx.context.onBehalfOf.userId` absent); the stored `owner_id` is the
caller's `userId`; the body does not empty an optional master; and the
caller can read the master row through the read door
(`readUnlessRefused(readRowById(rel.master, masterId, context))`). When
it holds, the gate returns before the chain walk, so no master is asked
for edit. The served member builds a body-less by-id update with the
caller's context, so it takes the same leg and answers `allow`.
5. `.changeset/31169-cbp-optional-master-entry-write-text.md`: one
`'@objectstack/spec': patch` covering both cards. The registry ships in
the package (`dist`, `spec-changes.json`, `protocol-upgrade-guide.md`
are in `files[]`) and so does the contract's JSDoc (in `dist`).
Unchanged: the entry's `id`, `surface`, `reason` and
`acceptanceCriteria`; every type, export and runtime answer.
## Tests
No behaviour change, so no test is added and none is ablated. The opt-in
leg the JSDoc describes is pinned by
`packages/plugins/plugin-security/src/controlled-by-parent-owner-may-edit.test.ts`.
`grep -rn "still refused with 422" packages/` and a search for the
template's phrasing found no pin or snapshot quoting the old sentences.
The behaviour the new text describes is pinned by
`packages/plugins/plugin-security/src/controlled-by-parent-optional-master.test.ts`
(PR #31082). The regenerated (gitignored)
`packages/spec/src/migrations/registry.ts` carries the new text and no
"Writes are unchanged" sentence.
## Acceptance notes
- `.changeset/30905-cbp-optional-master.md` (pending, from the read-side
PR) still ends its plugin-security bullet with "Writes are unchanged: an
insert or update that leaves the master reference empty is still refused
with 422." It was true of that PR, and
`.changeset/30906-cbp-null-master-write-gate.md` states the write
change, so both will compile into the same release's notes. Editing a
landed changeset is outside this card; noted for the seat, not changed
here.
## Verification
Local gates on the final head `d3adb0d1`: 78 derived by `dispatch-gates
--commands`, 77 exit 0, 1 NOT MEASURED (`check:dual-build-cjs-loads`,
exit 3: it needs every package's `dist`, CI's). `--ran` reconciles 78
accounted, 0 unrun. `pnpm --filter @objectstack/spec run typecheck` exit
0; `vitest run src/migrations/ src/contracts/` 57 files / 673 tests
passed. Details in the card's `os-dev-report`; CI runs the full set.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6f017e2 commit 2b752f1
4 files changed
Lines changed: 53 additions & 8 deletions
File tree
- .changeset
- packages/spec/src
- contracts
- migrations
- entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
405 | 405 | | |
406 | 406 | | |
407 | 407 | | |
408 | | - | |
409 | | - | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
410 | 416 | | |
411 | 417 | | |
412 | 418 | | |
| |||
1086 | 1092 | | |
1087 | 1093 | | |
1088 | 1094 | | |
1089 | | - | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
1090 | 1107 | | |
1091 | 1108 | | |
1092 | 1109 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
16 | 23 | | |
17 | 24 | | |
18 | 25 | | |
| |||
21 | 28 | | |
22 | 29 | | |
23 | 30 | | |
24 | | - | |
25 | | - | |
26 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
27 | 40 | | |
28 | 41 | | |
29 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1327 | 1327 | | |
1328 | 1328 | | |
1329 | 1329 | | |
1330 | | - | |
1331 | | - | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
1332 | 1334 | | |
1333 | 1335 | | |
1334 | 1336 | | |
| |||
0 commit comments