Skip to content

docs: error-handling, error-catalog and cli pages teach the optional controlled_by_parent master - #31164

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-31095-optional-master-docs
Nov 7, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-31095-optional-master-docs

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread
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 段补上它对「不挂主表的行」的证明。
  • 为什么改:您对 controlled_by_parent admits an optional master (1/3): spec, lint and the read owner branch: a null-master detail row is readable by its owner only #30905/controlled_by_parent admits an optional master (2/3): the write gate judges null-master rows (owner or modify-all), clearing a master needs edit on it, and the verify proof #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.

Derived gates: 45 commands, all exit 0
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.

🤖 Generated with Claude Code

…controlled_by_parent master

A controlled_by_parent object may declare its only master_detail with
required: false (ADR-0055 §1 as amended). A stored empty reference on
it is not a 422: the row is its owner's, read by the owner and View All
on the detail and written by the owner and Modify All. The 422
MISSING_REQUIRED_FIELD exception now names a required master only; the
declaration table gains the optional-master row; the v18 refused-shape
list no longer lists an explicit required: false; and
--parent-write-reach names the null-master records it proves.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
@objectstack-fleet

objectstack-fleet Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ba96749d36e2ec46d8b660048584ad1157d9dc34
Local-runs: none
Scope: content/docs/protocol/kernel/error-handling.mdx, content/docs/api/error-catalog.mdx, content/docs/deployment/cli.mdx + packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts, packages/plugins/plugin-security/src/security-plugin-cbp-read-filter.ts, packages/plugins/plugin-security/src/security-plugin-cbp-relation.ts, packages/plugins/plugin-security/src/errors.ts, packages/plugins/plugin-security/src/permission-evaluator.ts, packages/spec/src/data/controlled-by-parent-master.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/migrations/entries/semantic/18.cbp-optional-master-admitted.ts, packages/lint/src/data-model-rules.ts, packages/verify/src/parent-write-reach.ts, docs/adr/0055-master-detail-controlled-by-parent.md, content/docs/permissions/{authorization,sharing-rules,rls,permissions-matrix,index}.mdx

reviewFace prints no hits for these three paths (a content/docs/** prose face, --paths renders the TIER_DEFAULT template); the brief orders the review at CONTRACT_REVIEW_TIER with the three pages as scope, and that is the scope above. Inputs: card #31095 (body, triage 6703604507, claim 6708154180, dev report 6709055161), PR #31092 (merged aa3e21cca7) and its permissions pages, PR #31164 body and files, git diff origin/main...refs/review/pr-31164 (3 files, +108/-64, no changeset), the code at origin/main 56baf78e0c (the PR's merge base), and the head's check-runs. The dev's and the seat's conclusions were not used as evidence. Every file:line below is at origin/main.

① Derived judgments

Truth of each sentence the diff adds or changes, against the code:

  1. Who reads, updates or deletes a row whose optional master is empty (error-handling.mdx L570-577, error-catalog.mdx L200-204). Read: security-plugin-cbp-read-filter.ts L159 returns the master arm alone for a required master; L178-184 add the null-master arm for an optional one — { fk: null } for a holder of the super-user read bypass on the detail (L179; permission-evaluator.ts L475-481 reads the view bit, which the docblock at L469-471 names viewAllRecords / modifyAllRecords), else { fk: null, owner_id: caller } when the object carries owner_id (L181-182), else no arm (L183). Write: security-plugin-cbp-write-gate.ts L529-537 judges a by-id update or delete of a stored empty optional reference by nullMasterRowWritable (L220-232: Modify All via hasSuperuserWriteBypass, L228 and permission-evaluator.ts L490-496 modify bit; else the row's non-null owner_id equal to the caller, L229-231), and the same rule on each null-master row a predicate write selects (L488-492). "A row with no owner, or on an object that carries no owner_id column, is read through View All and written through Modify All alone" — read filter L172-175 and L181-183; write gate L205-209 and L229. True.
  2. The 403 code (error-handling.mdx L574-575; error-catalog.mdx L204; the emptying refusal at L589-592 / L210-211). nullMasterOwnerDenied (write gate L378-387) is a PermissionDeniedError, leg master_chain; a master-edit refusal on the emptying path is masterEditDenied (L338-345), the same class; both reach the response as 403 PERMISSION_DENIED (ADR-0055 L134, L244). True of a principal the gate judges — see should-fix S1 for the principal who never reaches it.
  3. The 422 scoped to a required master (error-handling.mdx L286, L559-567; error-catalog.mdx L189-197, L1029). MasterReferenceMissingError is MISSING_REQUIRED_FIELD / 422 (errors.ts L289-292) with the message prefix [Security] Missing master reference: (L298-303). It is thrown for the stored-row shape at write gate L631, which the optional block at L529-537 returns before, so only rel.optional === false reaches it; the predicate path throws it at L884 only when !rel.optional (L879-883); the body-empties path at L420-421 returns for rel.optional. "Whether or not the master is marked readonly or system": a flagged required master still resolves as the master (security-plugin-cbp-relation.ts L164, flags not read there); the masterNotWritable refusal at L364-366 fires only when the write sets the reference (writesMasterReference), so a delete or an update that leaves the key out walks to L502-504 and then L631. "An optional master never answers this code": all three throw sites are guarded as above. True.
  4. The declaration table (error-handling.mdx L582-587, and the paragraph under it at L589-592). Row 1 (required: true, unflagged, 400 VALIDATION_FAILED with fields): the stand-down at write gate L630 (omissionRefusedByValidation) and L420. Row 2 (required: false, the optional master, "not refused (see below)"): insert admitted at L530; an update that empties it is admitted by the owner rule (L532) and then asks edit on the master it leaves (L407-413, L689-693) — true of an insert, loose for an update, see should-fix S2. Rows 3-4 ("required or optional" + readonly / system, 422 INVALID_METADATA, no fields): controlledByParentMasterRefusal judges the flags on the one master, optional included (controlled-by-parent-master.ts L209-230, shape master-not-writable, L83-88); MasterReferenceNotWritableError is INVALID_METADATA / 422 with the [Security] Configuration defect: prefix (errors.ts L203-208), thrown at write gate L364-366 on every write that sets the reference. "Only the first two rows are declarations the parse accepts": L213 returns undefined only when the one master carries neither flag. "ownerMayEdit does not cover emptying it": L693. "Its creator becomes the row's owner": the gate's own note at L519-520 (the platform's owner stamp, step 3.5; ADR-0055 L245) — read at the gate, the stamp itself not re-read. True, S2 aside.
  5. The "no master at all" paragraph (error-handling.mdx L607-617; error-catalog.mdx L224-231) against controlledByParentMasterOf (controlled-by-parent-master.ts L168-180): one required: true master wins (L171-172); two required name none (L173); otherwise exactly one master_detail declared required === false is the optional master (L174-179); two with none required, a required lookup only, and an unwritten required name none (L177, L179). "ObjectSchema.create() forces an omitted one to true" — object.zod.ts L4544-4546, called at L4679; an explicit false on the only master_detail is kept (L4530-4531). "os lint reports the omission at error" — data-model-rules.ts L412-418 (missingRequired on the sole master_detail when required !== true and not === false), error tier per L393. "A raw parse cannot see the omission and reads it as false, the optional master" — object.zod.ts L2768-2770 and controlled-by-parent-master.ts L44-59. "Every read denied and every write refused with 422 INVALID_METADATA" for an unparsed refused object: controlled-by-parent-master.ts L282-283 and errors.ts L170-175 (MasterDetailRelationMissingError, same code and status per L199-201). True.
  6. "Why the gate refuses" / "not going to be relaxed" (error-handling.mdx L619-626; error-catalog.mdx L236-241): the required master's filter is the master arm alone, fk IN (readable masters) with no empty-reference arm — read filter L156-159. True.
  7. The v18 refused-shapes list (cli.mdx L2142). refused-shapes.ts L199-222: the entry cbp-master-relation-tiers-retired reports exactly what controlledByParentMasterRefusal refuses, and its reason sentence (L213-215) states the amended rule; the master-not-required arm is retired for an explicit required: false (L148-152), which the parse admits (controlled-by-parent-master.ts L77-79). The admitting entry is cbp-optional-master-admitted (18.cbp-optional-master-admitted.ts L17). "Two masters, none, a required lookup only" still refused: L231-287 of the master module. True.
  8. What --parent-write-reach checks (cli.mdx L2707-2712, L2730-2738). parent-write-reach.ts L32-51 (header), L338 (optionalMaster read from controlledByParentMasterOf), L983 (the null-master probe runs for such a case), L765-767 (edit / delete admitted iff the verb on the detail AND (owns OR Modify All); create iff the verb alone), L780-782 (a record another principal owns and one the persona owns, owner_id), L792-806 (a null-master record the persona cannot read is recorded unreadable and not probed), L825 (the create naming none). "Required or optional": L338 beside L284-299. The re-wrapped sentence "two master_detail fields, none, or a chain at the depth bound is reported excluded": masterLinkOf L286-299 and L365-369. True.

Consistency: the wording agrees with the merged permissions pages — authorization.mdx L539-548 (one master_detail declared required: true, or, with none, its only master_detail declared required: false; the three no-master shapes), sharing-rules.mdx L33 and permissions-matrix.mdx L241 (owner + View All read, owner + Modify All write), rls.mdx L241-245, index.mdx L233; the citation spelling "ADR-0055 §1 as amended" matches the ADR's status lines (L8-9). No other page under content/docs/ outside releases/** states the old rule: a grep of the PR tree for the retired sentences ("declared required: true" as the master, "stored master reference is null", "readable by nobody", "fk IN (readable masters)", "derives none", a non-required master_detail) hits only the three changed pages and the permissions pages; data-modeling/fields.mdx L592-595 speaks of parent resolving on "exactly one master_detail relationship" without a required claim, which still holds.

② Semver level

Docs only: three content/docs/** pages, no package file, no changeset in the diff. skip-changeset is the house rule for a diff that publishes nothing from any released package (AGENTS.md Post-Task Checklist 3); the label is on the PR and Check Changeset is green. Clause-②: no and Metadata-protocol: none are on the PR body as line-initial declarations and are true of the tree. Agrees.

③ Boundary flags

  • Landing. Fixes #31095 on the PR body; draft; not governed (no skills/**, docs/adr/**, .claude/** or packages/spec/src/** path; Governed Surface Queue Guard success). No content/docs/releases/** — true, 3 files. The PR body's per-site table matches the tree on every row checked in ①; its gate table (45 commands, exit 0) is the dev's account and was not re-run here, the head's check-runs are the verdict.
  • Check-runs on the head at record time: every completed non-skipped run is success — Lint & Repo Gates, TypeScript Type Check, Build Docs, Check Changeset, Check PR Size, Check Documentation Links, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Plan Test Core shards, Test Core 4/8 and 5/8, Test UI, Dogfood Regression Gate, Auto Label, and all Lint · / Type Check · jobs. Still in progress: Test Core 1/8, 2/8, 3/8. The remaining jobs are path-skipped. The seat lands only once those three complete green.
  • Dev's open question (assignee at pr_create): a seat-side rule drift between the order / os-dev.md and AGENTS.md / ops.mjs; nothing in this PR's tree turns on it. Answer: the seat's to settle; no action in this record.
  • Dev's out-of-scope finding, confirmed: 18.cbp-optional-master-admitted.ts L24-25 still says "Writes are unchanged by this entry: an insert or update that leaves the master reference empty is still refused with 422", which write gate L529-537 contradicts since the §3 amendment. It is spec-lane text (the migration registry, read by os migrate and the generated upgrade guide), outside this card's three pages; cli.mdx L2142 does not quote it. Belongs with spec: security-service contract jsdoc still describes a controlled_by_parent master as always required (optional master, cards #30905/#30906) #31094's family; not a finding on this PR.
  • Branch name: the order's slug differed; the claim comment 6708154180 and the PR head both name claude/issue-31095-optional-master-docs. The dev worked on the claimed branch. Noted, no action.
  • Left as found, pre-existing and not this card's sites: the 422 exception says "an update or delete by id", while a predicate write that selects a stored null-required-master row answers the same 422 (write gate L884); cli.mdx L2142's outcome column "an object registers but derives no master" is not true of the master-not-writable shape (refused-shapes.ts L190-194). Both recorded in the PR's acceptance notes.

④ Design judgment

No hits file on the schema, rules or narrowing face: the three pages are prose, and the published spec and governed register are untouched. The judgment owed is ① — does each page now hold against the code it describes — and it does, with two should-fix wording gaps:

  • S1 (should-fix) — error-handling.mdx L574-575 and error-catalog.mdx L204: "anyone else — View All included — is refused 403 PERMISSION_DENIED". The owner rule answers 403 to a principal the gate judges; a principal who cannot read the row (no ownership, no View All) meets the by-id pre-image read first and is answered 404 RECORD_NOT_FOUND (security-plugin.ts L4104, L4141-4146), as ADR-0055's 2026-11-07 amendment states at L244 ("Everyone else who reaches the gate, View All included, is refused 403 ... a principal who cannot read the row meets the read door first, 404 RECORD_NOT_FOUND"). That qualifier was added to the ADR at PR docs(skills): teach controlled_by_parent with an optional master in the data skill, permission docs and ADR-0055 #31092's review for this exact sentence; the two error pages should carry it, since they are the reference a client branches on. Fix: "anyone else who reaches the gate — View All included — is refused 403 PERMISSION_DENIED; a caller who cannot read the row is answered 404 RECORD_NOT_FOUND first".
  • S2 (should-fix) — error-handling.mdx L585, the table row for the optional master reads "not refused (see below)" under the header "Refused by". The table's subject is "a request that leaves the master reference out, or empties it": an insert that leaves it out is admitted (write gate L530), but an update that empties it is refused 403 by the master it leaves when the caller cannot edit that master (L407-413, L689-693), which the paragraph below the table says. Fix the cell to "insert: not refused; update that empties it: the master-access gate on the master it leaves (see below)" or move the row's verdict wholly to the paragraph.

Neither changes an access answer the page gives a holder of the right grants; neither is a must-fix.

Round 2 delta — head ba96749d36e2ec46d8b660048584ad1157d9dc34 (previous head f28b6db045)

Round 1 above was rendered on f28b6db045; this delta (dev report 6709333394) is one commit ba96749d36, no rebase, same merge base 56baf78e0c. git diff f28b6db045..ba96749d36: 2 files, +10/-6, error-handling.mdx and error-catalog.mdx only; cli.mdx and everything else unchanged. origin/main moved to 8ef4d30905; git diff 56baf78e0c..8ef4d30905 over the cited code (write gate, read filter, authz middleware, errors.ts, controlled-by-parent-master.ts, ADR-0055, content/docs/) is empty, so the round-1 citations hold at 8ef4d30905 and the lines below are read there.

  1. S1 resolved. error-handling.mdx L574-577 and error-catalog.mdx L204-206 now read "Everyone else who reaches the gate, View All included, is refused 403 PERMISSION_DENIED; a principal who cannot read the row meets the read door first (404 RECORD_NOT_FOUND), as for every hidden row." True: the by-id write path in security-plugin-authz-middleware.ts throws recordNotFoundError for an addressed row the caller cannot read at L1198-1205 (a row-filter-bound principal whose caller-context re-read finds nothing) and at L1243-1260 (a principal no write-class row filter binds), both before step 2.8 at L1264 where assertControlledByParentWrite runs; recordNotFoundError is @objectstack/core's packages/core/src/utils/record-not-found.ts L54-62, code: 'RECORD_NOT_FOUND', status: 404. The 403 for a principal who reaches the gate is nullMasterOwnerDenied, write gate L378-387, unchanged. Wording matches ADR-0055 L244 verbatim. Citation check: the dev's security-plugin-authz-middleware.ts L1196-1205 / L1240-1260 is the right site; my round-1 security-plugin.ts L4104 / L4141-4146 is readRowById's docblock, which only recounts that the gate answered 404 — the throw is the middleware's. The dev's correction stands.
  2. S2 resolved. error-handling.mdx L587 table row now says "an insert that leaves it out: not refused; an update that empties it: the master-access gate, by edit access on the master it leaves (see below)", status "— / 403", code "— / PERMISSION_DENIED", fields "— / absent". True: insert admitted at write gate L530; an emptying update asks edit on the stored master through the §3 walk (L407-413, the ownerMayEdit leg steps aside at L689-693), refused by masterEditDenied (L338-345, a PermissionDeniedError, 403, no fields). The cell now agrees with the paragraph at L591-594.
  3. The extra site error-catalog.mdx L210-213: "an insert that leaves it out is not refused, and its creator owns the row; an update that empties it is judged by the master it leaves, asking edit access on it (403 PERMISSION_DENIED without it)" — the same split as the table row, same citations; the retired phrase "nothing refuses it" is gone. True.

Nothing else changed in the delta. No new finding. PR still draft, skip-changeset on, Fixes #31095 on the body (the dev patched the body's per-site rows and verification head; not re-judged beyond the tree).

Check-runs on ba96749d36 at record time: every completed non-skipped run is success — Lint & Repo Gates, TypeScript Type Check, Build Docs, Check Documentation Links, Check PR Size, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Plan Test Core shards, Test Core 4/8 and 5/8, Test UI, Dogfood Regression Gate, Auto Label, and every Lint · / Type Check · job. Check Changeset is skipped on this head (the skip-changeset label). Still in progress: Test Core 1/8, 2/8, 3/8. The seat lands only once those three complete green.

Final verdict on head ba96749d36e2ec46d8b660048584ad1157d9dc34: PASS, no must-fix, no should-fix outstanding.

Implemented-by: claude/issue-31095-optional-master-docs
Reviewed-by: session_017ihL2ebbZkr2QAEG1x7UC8

VERDICT: PASS

…the optional-master table cell agree with its paragraph

A principal who cannot read a null-master row meets the by-id read door
first and is answered 404 RECORD_NOT_FOUND, as for every hidden row; the
403 is for everyone else who reaches the gate. On the declaration table
the optional-master row now says an insert that leaves the reference out
is not refused, while an update that empties it is judged by the master
it leaves (403 PERMISSION_DENIED without edit access); the catalog's
prose says the same.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

维护者速读

  • 改了什么:三篇文档(错误处理、错误目录、CLI)改成“可选主记录”的现行规则。只有必填主记录为空时才报 422。可选主记录为空的行归它的所有者:所有者和明细上的 View All 可以读,所有者和 Modify All 可以改或删,其他人得到 403;读不到这一行的人先得到 404。
  • 为什么改:feat(security, verify): a controlled_by_parent row with no master is written by its owner or by Modify All on the detail #31082 起写入门已经放行这类写入,但这三页还在说“主记录为空一律 422”,会误导写集成的开发者。
  • 风险与回滚:只改文档,不改代码,也不发布任何包。每句话都对照了 origin/main 的代码行。回滚即 revert。
  • 先看:content/docs/protocol/kernel/error-handling.mdx 的声明表和 403/404 段落,以及 content/docs/api/error-catalog.mdx 的 422 条目。
  • 两分钟验证:在两页里搜索 “required: false”,看到的说法应与 docs(skills): teach controlled_by_parent with an optional master in the data skill, permission docs and ADR-0055 #31092 的权限页面一致。
  • 需要你做的:无。不涉及受管面,席位在 CI 全绿后自行落地。Fable 契约复审 PASS 6709170558,Head-sha ba96749d36。

席位意见:可以落地。复审两轮,第 1 轮的两项应改已在第 2 轮修正并通过增量复审,检查全绿。顺带发现 spec 迁移说明里的同一个过时说法,已立卡 #31169,交给 spec 车道。

席位核对 — ACCEPT · domain:devx#2 · Head-sha ba96749d36e2ec46d8b660048584ad1157d9dc34 · 2026-11-07T16:06Z

  • Shape: 3 files, +112/−64, docs only:
    • content/docs/protocol/kernel/error-handling.mdx
    • content/docs/api/error-catalog.mdx
    • content/docs/deployment/cli.mdx
  • What the pages now say:
    • The 422 applies to a required master only.
    • An optional master's empty reference is owner-ruled. The 403 is for principals the gate judges; the 404 comes first for a principal who cannot read the row.
    • The declaration table splits insert from emptying update.
    • The v18 refused-shapes list drops the explicit required: false, and the text names --parent-write-reach's null-master check.
  • Review: the complete Fable contract review PASSed (6709170558), with every changed sentence checked file:line against origin/main.
  • Gates:
    • Head checks: 32 success, 18 skipped, none red.
    • check-expected-skips: OK, all 18 skips are in main's roster (exit 0).
    • check-governed-merges: not governed (exit 0). 176 changed lines.
    • The dev's dispatch-gates --commands: 45 derived, 45 run, all exit 0, on this head.
    • git merge-tree against main (21c1f74850) is clean.
  • Out of scope:
  • Landing: not governed, Fixes #31095. The seat marks it ready and arms auto-merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: error-handling, error-catalog and cli pages still say a controlled_by_parent master is always required (optional master, cards #30905/#30906)

2 participants