Repository navigation
Commit 13c6d8f
docs: error-handling, error-catalog and cli pages teach the optional controlled_by_parent master (#31164)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiNjyvCdXYygFqntMNR7S2dW)_
Fixes #31095
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:错误处理页、错误目录页和 CLI 页不再说「`controlled_by_parent` 子对象的主表引用存空一律
422」。现在写明:只有必填主表(`required: true`)的存储空引用才回 `422
MISSING_REQUIRED_FIELD`;可选主表(唯一的 `master_detail` 写 `required:
false`)的空引用不是错误,这行归属主——属主和子对象「查看全部」可读,属主和「修改全部」可改删,其他人 403。CLI 页的 v18
升级清单不再把显式 `required: false` 列为被拒形态,`os verify --parent-write-reach`
段补上它对「不挂主表的行」的证明。
- **为什么改**:您对 #30905/#30906
的裁决「A」已经在代码里落地(读、写两半都已合并),权限文档页也已改好;这三页参考文档还在教旧规则,读者按它处理 422 会判断错。
- **风险与回滚**:只改三个 `content/docs` 页面,不发布任何包,无运行时影响;回滚即 revert。
- **先看**:`content/docs/protocol/kernel/error-handling.mdx` 的
`MISSING_REQUIRED_FIELD` 小节(新增「可选主表」段和声明表新行),其次
`content/docs/api/error-catalog.mdx` 同名小节。
- **两分钟验证**:`pnpm check:doc-anchors && pnpm check:doc-authoring`,预期两者
exit 0;再打开 error-handling 页的 `MISSING_REQUIRED_FIELD` 小节,应看到声明表里
`required: false` 一行写着「not refused」。
- **需要你做的**:无,席位落地。
## What changes
Three `content/docs` pages taught a `controlled_by_parent` master that
is always required. Each site was re-read against the code on `main` and
reworded; the wording follows the permissions pages
(`content/docs/permissions/authorization.mdx`, `sharing-rules.mdx`,
`rls.mdx`, `permissions-matrix.mdx`): "its only `master_detail` declared
`required: false`, an optional master (ADR-0055 §1 as amended)", "read
by its owner and by View All", "written by its owner and by Modify All".
| Page · site | Was | Now | Backed by |
|:--|:--|:--|:--|
| `protocol/kernel/error-handling.mdx` status table (422 row) | a stored
null master reference answers 422 | a stored null reference to a
**required** master; an optional master's empty reference is not an
error | `security-plugin-cbp-write-gate.ts`: the `masterId == null &&
rel.optional` block returns before `MasterReferenceMissingError`;
`controlledByParentPredicateWriteMasters` throws it only when
`!rel.optional` |
| same page, `MISSING_REQUIRED_FIELD` exception | 422 "whatever the
declaration" | 422 only where the master is `required: true` (flagged or
not); new paragraph: an optional master's empty row is its owner's, read
by owner + View All, updated/deleted by owner + Modify All; everyone
else who reaches the gate `403 PERMISSION_DENIED`, a principal who
cannot read the row `404 RECORD_NOT_FOUND` first (round 2, S1;
error-catalog says the same) | write gate `nullMasterRowWritable` +
`nullMasterOwnerDenied` (`PermissionDeniedError`); the read door's 404
for a hidden addressed row: `security-plugin-authz-middleware.ts`
`recordNotFoundError` after `callerCannotReadAddressedRow`; read filter
`computeControlledByParentFilter` `nullMasterArm`
(`security-plugin-cbp-read-filter.ts`) |
| same page, declaration table | three rows, all `required`; "Only the
first row is a declaration the parse accepts" | adds `required: false`
(optional master): an insert that leaves it out is not refused, an
update that empties it is judged by the master it leaves (`403
PERMISSION_DENIED` without edit access) (round 2, S2; error-catalog
prose says the same); flagged rows read "required or optional"; "Only
the first two rows"; insert naming no master admitted, emptying asks
edit on the master it leaves, `ownerMayEdit` does not cover it | write
gate: `if (operation === 'insert') return;`, `askBodyNamedMaster`
(`rel.optional` return), the `ownerMayEdit` leg's `!(rel.optional &&
bodyEmptiesMasterReference(...))`; `rel.masterNotWritable` refusal ahead
of the optional block; `controlledByParentMasterRefusal` judges
`master-not-writable` on the optional master too
(`controlled-by-parent-master.ts`) |
| same page, "no master at all" paragraph | master = exactly one
`master_detail` declared `required: true`; a `master_detail` without
`required` derives none | master = the required one, or with none the
only `master_detail` declared `required: false`; no master: two
`master_detail` none required, two required, required `lookup` only; an
omitted `required` is forced to `true` by `ObjectSchema.create()` and
reported by `os lint` at `error`, while a raw parse reads it as `false`
| `controlledByParentMasterOf` (`controlled-by-parent-master.ts`);
`object.zod.ts` `refuseControlledByParentWithoutOneMaster` docblock (the
amendment note on the omitted `required`) and
`forceCbpMasterDetailRequired` |
| same page, "Why the gate refuses" | `fk IN (readable masters)` can
never match | the same, said of a required master's filter, which has no
empty-reference arm | read filter: `if (!rel.optional) return
masterArm;` |
| `api/error-catalog.mdx` `MISSING_REQUIRED_FIELD` Fix line, callout,
"no master" paragraph, "not relaxed" paragraph | same four claims as
above | same rewording, condensed | same sources |
| same page, status table (422 row) | stored null master reference |
stored null reference to a required master; optional master's empty
reference is not an error | same |
| `deployment/cli.mdx` v18 "Not rewritten" table | lists "an explicit
`required: false`" as a refused stored shape | it is the optional
master, admitted by `cbp-optional-master-admitted` and not listed |
`refused-shapes.ts` `controlledByParentWithoutOneMaster` (judges with
`controlledByParentMasterRefusal`);
`migrations/entries/semantic/18.cbp-optional-master-admitted.ts` |
| same page, `--parent-write-reach` paragraph | "exactly one
`master_detail` field names its master" | "required or optional"; for an
optional master the run also proves null-master records: edit/delete =
verb on the detail and (owner or Modify All), create naming none =
create on the detail; an unreadable one is not probed |
`packages/verify/src/parent-write-reach.ts` module header and
`nullMasterReach`; `deriveParentWriteReachCases` marks `optionalMaster`
from `controlledByParentMasterOf` |
No `content/docs/releases/**` page is touched.
## Tests
Docs only: no code, no test. The derived docs gates below are the
verification.
## Acceptance notes
- `skip-changeset`: the diff touches only `content/docs/**`, which no
package's `files[]` ships.
- Left as found (not this card's wording): the CLI v18 table's
`controlled_by_parent` parenthetical does not name the
`master-not-writable` shape (a `readonly` / `system` master), which
`refused-shapes.ts` lists too; the row's "an object registers but
derives no master" is not true of that shape (it keeps its master and
loses the save door). The 422 exception names "an update or delete by
id"; a predicate write that selects such a row answers the same 422
(`controlledByParentPredicateWriteMasters`).
## Verification
Gate run on head `ba96749d36` (round 2; round 1 ran the same 45 on
`f28b6db045`, after merging `origin/main`); every command
`dispatch-gates --commands` derives for this diff, exit codes captured
before any pipe.
<details><summary>Derived gates: 45 commands, all exit 0</summary>
```text
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-examples-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
```
`--ran` reconciliation: `45 derived, 45 run, 0 NOT-MEASURED, 0 UNRUN` —
all 45 recorded an exit code, none of them 3 (`dispatch-gates --ran`,
exit 0). Five gates first refused with exit 3 for a missing build
(`@objectstack/lint`, `@objectstack/spec` schema,
`@objectstack/client-react` declarations); the closures were built and
every gate was rerun; round 2 reran all 45 on `ba96749d36`, all exit 0.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- signed: domain:devx#2 · marchtian ·
session_017ihL2ebbZkr2QAEG1x7UC8 -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent eb68b98 commit 13c6d8f
3 files changed
Lines changed: 112 additions & 64 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
181 | 181 | | |
182 | 182 | | |
183 | 183 | | |
184 | | - | |
| 184 | + | |
185 | 185 | | |
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
195 | | - | |
196 | | - | |
197 | | - | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
198 | 207 | | |
199 | 208 | | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
205 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
206 | 218 | | |
207 | 219 | | |
208 | 220 | | |
| |||
211 | 223 | | |
212 | 224 | | |
213 | 225 | | |
214 | | - | |
215 | | - | |
216 | | - | |
217 | | - | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
218 | 234 | | |
219 | 235 | | |
220 | 236 | | |
221 | 237 | | |
222 | 238 | | |
223 | 239 | | |
224 | | - | |
225 | | - | |
226 | | - | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
227 | 244 | | |
228 | 245 | | |
229 | 246 | | |
| |||
1011 | 1028 | | |
1012 | 1029 | | |
1013 | 1030 | | |
1014 | | - | |
| 1031 | + | |
1015 | 1032 | | |
1016 | 1033 | | |
1017 | 1034 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2139 | 2139 | | |
2140 | 2140 | | |
2141 | 2141 | | |
2142 | | - | |
| 2142 | + | |
2143 | 2143 | | |
2144 | 2144 | | |
2145 | 2145 | | |
| |||
2706 | 2706 | | |
2707 | 2707 | | |
2708 | 2708 | | |
2709 | | - | |
2710 | | - | |
2711 | | - | |
2712 | | - | |
| 2709 | + | |
| 2710 | + | |
| 2711 | + | |
| 2712 | + | |
| 2713 | + | |
2713 | 2714 | | |
2714 | 2715 | | |
2715 | 2716 | | |
| |||
2726 | 2727 | | |
2727 | 2728 | | |
2728 | 2729 | | |
2729 | | - | |
2730 | | - | |
2731 | | - | |
| 2730 | + | |
| 2731 | + | |
| 2732 | + | |
| 2733 | + | |
| 2734 | + | |
| 2735 | + | |
| 2736 | + | |
| 2737 | + | |
| 2738 | + | |
2732 | 2739 | | |
2733 | 2740 | | |
2734 | 2741 | | |
| |||
0 commit comments