Repository navigation
docs(spec): the cbp-optional-master-admitted entry states the current write rule, and the security-service contract names ownerMayEdit - #31210
Conversation
… write rule for an empty optional master The entry's replacement and its step-18 upgrade-guide paragraph still said a write leaving an optional controlled_by_parent master empty is refused with 422. The write gate now admits it by the owner rule: the owner or Modify All on the detail writes such a row, everyone else is refused 403 PERMISSION_DENIED, and only an empty required master reference still answers 422 MISSING_REQUIRED_FIELD. Text only; id, surface and reason unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t
…r leg of the controlled_by_parent write check The allow bullet of ControlledByParentWriteOutcome and the docblock of ISecurityService.checkControlledByParentWrite read as if every by-id update of a controlled_by_parent record needed edit access to its master. A detail declaring sharing.controlledByParent.ownerMayEdit: true lets the record's owner, not on behalf of another user, update it by id with read access to the master, unless the body empties an optional master (ADR-0055 section 3 as amended). Both docblocks now say so. The changeset covers this and the migration-entry text. JSDoc only. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 171 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fc08f7a1613a91ddf717941c6e9348dde19a7de3 && git checkout fc08f7a1613a91ddf717941c6e9348dde19a7de3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 66bf5b4a019c62f50ed1824b35fe6ba345cf51e3 082e8bf80665184fce72aa9120aaea14e3b9bcb5 && git checkout -B drift-repro 66bf5b4a019c62f50ed1824b35fe6ba345cf51e3 && git merge --no-ff 082e8bf80665184fce72aa9120aaea14e3b9bcb5
node scripts/docs-audit/affected-docs.mjs --json 66bf5b4a019c62f50ed1824b35fe6ba345cf51e3
|
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract-review-claim: session_01FEkQ5mw7HFYn9o4LEGMQdK · head d3adb0d · 2026-11-07T17:45Z |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Inputs: card #31169 (body; comments 6709432110, 6710037976, 6710851706, 6710876574), card #31180 (body; comments 6710232004, 6710378726), PR #31210 body and file list, the net diff Check-runs on the head at review time: 18 completed success (Auto Label; Check Changeset; Check PR Size; Dogfood Regression Gate; Flag docs affected by code changes; Governed Surface Queue Guard; Lint · CI, release & engine gates; Lint · ESLint; Lint · PM dispatch-gates; Lint · docs & code guards; Lint · repo & tooling gates; Lint · scoped · query-options erasure; Lint · scoped · tooling self-tests; PR-body guards; Plan Test Core shards; Spec property liveness; Type Check · source gates; filter), 8 skipped (opt-in and path-filtered jobs), 15 still in progress (Build Core; Build UI Deps; Lint · scoped · slot lookup and corpus; Live E2E UI; Test Core 1/8 to 8/8; Type Check · consumer gates; Type Check · debt ledger; Type Check · workspace). Not waited for. The gate families that judge the ① Derived judgments
② Semver level
③ Boundary flagsRead off os-dev-report 6710851706 on #31169, the PR body's Acceptance notes, and the two claims.
④ Design judgmentEvery sentence the new text states about the write gate was read against packages/spec/src/contracts/security-service.ts (card #31180). The
The
packages/spec/src/migrations/entries/semantic/18.cbp-optional-master-admitted.ts (card #31169).
packages/spec/src/migrations/registry.ts.template (STEP18 fragment No must-fix. One should-fix (S1), one escalation (③ item 2). Implemented-by: VERDICT: PASS |
…rsede the pending read-side changeset's write line The checkControlledByParentWrite docblock said the owner leg walks no chain; it walks none for the master the record names, while a body that re-points the row under another master still asks edit on that master (askBodyNamedMaster). The spec changeset now names the stale "Writes are unchanged ... 422" sentence of the pending 30905 changeset as superseded by the write rule. Text only. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Inputs: the one-commit delta ① Derived judgmentsThis is a delta review over PASS 6710982325 (head d3adb0d): everything that record judged on the three ② Semver levelUnchanged: ③ Boundary flags
④ Design judgmentpackages/spec/src/contracts/security-service.ts,
.changeset/31169-cbp-optional-master-entry-write-text.md. The added sentence: "This supersedes the sentence "Writes are unchanged: an insert or update that leaves the master reference empty is still refused with 422" in the
packages/spec/src/migrations/entries/semantic/18.cbp-optional-master-admitted.ts and packages/spec/src/migrations/registry.ts.template. Byte-identical to d3adb0d; the judgment of record 6710982325 stands: every sentence holds against the gate, no finding. No must-fix, no should-fix. Both findings of record 6710982325 are closed by this head. Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK 维护者速读
席位核对Verdict: ACCEPT · head
Ruling-ref: the maintainer, 2026-11-07T16:00Z: 「你有 fable ,车道契约复审可以自己做」; 2026-11-07T16:59Z: 「你应该使用 pm-dispatch 技能审核和落地pr」; standing landing authorisation 2026-10-25T11:35Z: 「按照 pm-dispatch 规则复核过的都授权」. |
Requested by Leehom · project thread
Fixes #31169
Fixes #31180
Clause-②: no
Metadata-protocol: none
维护者速读
cbp-optional-master-admitted这一条的说明文字(replacement与文件头注释),以及升级指南里同一条的那段叙述:不再说「主表引用为空的写入一律 422」,改为现行规则——可选主表为空的行由属主或该子对象「修改全部」写,其他人(含「查看全部」)403;不挂主表的新建放行;只有必填主表为空仍是 422。② 安全服务契约(security-service.ts)的allow一条和checkControlledByParentWrite说明补上「属主可编辑」开关(ownerMayEdit):开了它,记录属主本人按 id 更新、非代办、不清空可选主表时,只要能读主记录即可。os migrate meta和据此生成的发布说明会告诉作者错误的答案;契约注释则从没写过controlled_by_parent: the owner of a detail row cannot update their own row when they only read the master — no equivalent of the "activity owner always has full access" rule (hotclm obligations today, HotCRM tasks next) #25760 裁决的属主开关,读起来像「一律要主记录编辑权」。@objectstack/specpatch 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, themasterId == null && rel.optionalbranch ofassertControlledByParentWrite, and its403 PERMISSION_DENIEDrefusalnullMasterOwnerDenied):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 refused403 PERMISSION_DENIED. A row with no owner, or on an object without anowner_idcolumn, 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 with422 MISSING_REQUIRED_FIELD. The wording followscontent/docs/protocol/kernel/error-handling.mdxandcontent/docs/api/error-catalog.mdx(PR docs: error-handling, error-catalog and cli pages teach the optional controlled_by_parent master #31164) and the permissions pages of PR docs(skills): teach controlled_by_parent with an optional master in the data skill, permission docs and ADR-0055 #31092, so the four texts agree. The replacement string carries no tracker number (check:doc-authoring).packages/spec/src/migrations/registry.ts.template, the step-18 rationale fragmentcbp-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 samecheck:upgrade-guide/check:spec-changesfamily). The claim's file surface does not name this file; the seat is asked to amend it.ownerMayEditopt-in, so itsallowbullet and "requires EDIT access to its master" are incomplete for an opted-in controlled_by_parent detail #31180 (folded, its own commit):packages/spec/src/contracts/security-service.ts, JSDoc only. Theallowbullet ofControlledByParentWriteOutcomeand theISecurityService.checkControlledByParentWritedocblock now name thesharing.controlledByParent.ownerMayEdit: trueopt-in, citing ADR-0055 §3 as amended 2026-10-26. Conditions measured from the owner leg inassertControlledByParentWrite(security-plugin-cbp-write-gate.ts, theoperation === 'update' && detailRow !== null && rel.ownerMayEdit && …branch): a by-id update; not on behalf of another user (opCtx.context.onBehalfOf.userIdabsent); the storedowner_idis the caller'suserId; 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 answersallow..changeset/31169-cbp-optional-master-entry-write-text.md: one'@objectstack/spec': patchcovering both cards. The registry ships in the package (dist,spec-changes.json,protocol-upgrade-guide.mdare infiles[]) and so does the contract's JSDoc (indist).Unchanged: the entry's
id,surface,reasonandacceptanceCriteria; 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 bypackages/plugins/plugin-security/src/controlled-by-parent-optional-master.test.ts(PR #31082). The regenerated (gitignored)packages/spec/src/migrations/registry.tscarries 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.mdstates 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 bydispatch-gates --commands, 77 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3: it needs every package'sdist, CI's).--ranreconciles 78 accounted, 0 unrun.pnpm --filter @objectstack/spec run typecheckexit 0;vitest run src/migrations/ src/contracts/57 files / 673 tests passed. Details in the card'sos-dev-report; CI runs the full set.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5odNVgHTd2MQbQCp5t