Skip to content

docs(adr-0055): record the 2026-11-14 amendment, writeRequiresMasterRead — read on the master is enough to write, by opt-in - #33425

Merged
os-zhuang merged 1 commit into
mainfrom
claude/project-thread-mllmvl
Nov 14, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/project-thread-mllmvl

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #33373

domain:skills#1 · os-project-manager · session_01FEkQ5mjPb2TvhBS1yQezQF

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

One file, docs/adr/0055-master-detail-controlled-by-parent.md (+52 / −1; the −1 is the :6 banner line, kept verbatim with a ⚠️ note appended).

  • Status block. A new · **Amended** (2026-11-14, #33250 / PR #33369, ruling A+ 6880588685) line after the 2026-11-07 line, and a ⚠️ note at the end of the :6 ownerMayEdit banner (its "every other caller … keeps the master-edit rule" no longer holds on a detail that declares the new key).
  • §3 (:69). A third ⚠️ line under the rule: a detail that declares writeRequiresMasterRead is asked read on its master; the rule stands at its own hop for every detail that does not.
  • 2026-10-26 amendment (:172, :188). A ⚠️ paragraph after each "Option 2 … was not chosen" / "Option 2's door … stays closed" line, lifting them for opted-in objects only. The 2026-10-25 ruling's text is not rewritten.
  • 2026-11-07 amendment (:247). A ⚠️ line under "Setting a master … Emptying one asks edit on the stored master": on an opted-in detail both masters are asked read through the same walk; the null-master row's owner rule still runs first.
  • New section "Amendment (2026-11-14, controlled_by_parent: per-object opt-in so read on the master is enough to insert and update the detail under the detail's own grants (ownerMayEdit covers owner update only; HotCRM Order to Cash blocked) #33250): read on the master is enough to write, by opt-in": the ruling quoted verbatim; spec key, reader and both parse refusals; the gate's one swap point; every write shape that reaches it; what decides afterwards, with the ownership floors said in plain words (N3); on-behalf-of; the 404 / 403 answers and the served member's legs; the null-master row; the lint stand-down; the two seat calls for the veto window (contract review 6881744779 ① items 5 and 6); the precise invariant instead of "byte for byte" (N1); fail-closed boundaries; proof; what is unchanged.

Acceptance notes

Every claim in the new text was read from the code on origin/main (b4624440, which contains PR #33369 as df9fe946):

Claim Read from
key, reader, fail-closed === true packages/spec/src/data/object.zod.ts (writeRequiresMasterRead, controlledByParentWriteRequiresMasterRead)
two parse refusals, their paths and the fix text object.zod.ts#refuseControlledByParentWriteRequiresMasterReadMisplaced
relation carries the opt-in security-plugin-cbp-relation.ts#resolveCbpRelation, security-plugin.ts CbpRelation.writeRequiresMasterRead
one swap point; read hop ends the walk security-plugin-cbp-write-gate.ts#assertControlledByParentChainEditable (if (hopRel.writeRequiresMasterRead) { … break; })
read asked of the read door; object_permission / row_level_security; store fault propagates security-plugin-cbp-write-gate.ts#assertMasterRowReadable, readUnlessRefused
verb-blind gate; once per principal security-plugin-authz-middleware.ts step 2.8 (['insert','update','delete','transfer','restore','purge'], delegator pass)
by-id floor handed over; delegated keeps both floors security-plugin-authz-middleware.ts step 2.7 (masterGateCoversThisWrite = !delegatorSets), platform-ownership-policies.ts (OWNERSHIP_FLOOR_PREDICATE, masterGateCoversOperation)
predicate write keeps the floor; by-id admits a row the caller did not create controlled-by-parent-owner-may-edit.test.ts [#33250] "the opt-in" describe (by-id rows seeded by REP, predicate rows created by the caller)
404 for a by-id non-reader, 403 "requires read access" for insert / re-point [#33250] "without read on the master" describe (expectHiddenRow, expectMasterReadRefusal); PR #33369 Acceptance notes
null-master row keeps the owner rule security-plugin-cbp-write-gate.ts (stored null-master branch: nullMasterRowWritable, then askBodyNamedMaster)
chains, non-opted detail under opted master [#33250] "chains" describe; contract review 6881744779 ① item 5
explain prose only explain-engine.ts#chainAsksMasterRead, MASTER_DENIAL_LEG_DETAIL_WITH_READ
lint stand-down packages/lint/src/validate-security-posture.ts (masterReadSuffices)

Verification

Derived gates on head 5647b9f (21 of 21 exit 0)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 21 commands; each ran with its exit captured before any pipe. --ran: "21 derived famil(ies) accounted for — 21 run, 0 NOT-MEASURED". check:doc-formula-expressions ran after turbo run build --filter=@objectstack/formula --filter=@objectstack/lint under the lock (VERDICT command-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/ci/plan-test-core-shards.mjs --self-test :: exit 0
pnpm check:adr-anchors :: exit 0
pnpm check:adr-index :: exit 0
pnpm check:adr-symbol-anchors :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
Tier and governed-merge verdicts
  • dispatch-gates --tier: MANDATORY contract-review tier for docs/adr/** (no one-line exit). This card thread ran on the claim's default tier; named as a deviation in the report, and the seat's in-seat contract review at tier is the owed record.
  • check-governed-merges.mjs --branch claude/project-thread-mllmvl: exit 3, GOVERNED, landing tier H (人合), docs/adr/** ×1; 53 changed lines, under the 3000 threshold.
  • Changeset: skip-changeset (docs/adr/** ships in no package).

Generated by Claude Code

…ead — read on the master is enough to write, by opt-in

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 14, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5647b9f9800bb89aa4dc69d5162d8733fdc6ab7b
Local-runs: none
Scope: docs/adr/0055-master-detail-controlled-by-parent.md + none

Inputs read: card #33373 body and all three comments (N1/N3 addendum 6882016066, claim 6883503202, dev report 6883674498); PR #33425 body, its one-file list and the net diff against main at merge-base b4624440; the check-runs on the head. Cited, read and not reviewed: ruling record 6880588685 on #33250; contract review 6881744779 on PR #33369; PR #33369's body and Acceptance notes; on main (which carries #33369 as df9fe946): packages/spec/src/data/object.zod.ts, packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts, security-plugin-cbp-relation.ts, security-plugin-cbp-read-filter.ts, security-plugin-authz-middleware.ts (steps 2.7 and 2.8), platform-ownership-policies.ts, security-plugin.ts (platformFloorYieldsToObjectWriteModel), explain-engine.ts, packages/lint/src/validate-security-posture.ts, and the four test files the Proof paragraph names. The diff carries no implementing file, so + none.

Check-runs on the head, polled until none was pending: 53 total, 27 success, 26 skipped, 0 failed, 0 pending. Concluded green: Check Changeset, Check Documentation Links, Governed Surface Queue Guard, PR-body guards, Lint and Repo Gates and every Lint shard (including PM dispatch-gates and docs and code guards), Test Core and Test Core 1/8, Test UI, every Type Check job, Dogfood Regression Gate. The skipped jobs are the path-filtered build, UI, E2E and opt-in jobs, which a docs-only diff does not trigger.

① Derived judgments

The diff is one ADR file, +52 / -1. It publishes no schema, no export and no code, so no accept-set or public surface moves. What the diff implies is the record's own invariant, judged against main:

  1. The ADR moves from narrower-than-runtime to agreeing with the runtime. On main, ADR-0055's 26 Oct amendment still says "Option 2 ... was not chosen" and "Option 2's door ... stays closed". The runtime has served that door, by opt-in, since feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369. Right: this is the hazard the ADR's own chain amendment names, a reviewer "restoring" the narrower rule.
  2. History kept. The 25 Oct ruling paragraph (ruling of record 6398566164) and the "Option 2's door" paragraph are byte-identical on the head. Each gets a ⚠️ paragraph below it that lifts it for opted-in objects only and says the paragraph above "stays the record of" the 25 Oct ruling. The one modified line, the 26 Oct status banner, keeps its text whole and appends a ⚠️ note, as the 6 Nov banner line already does for 7 Nov. Right.
  3. The invariant replaces "byte for byte" (N1): "A write's answer moves only if its own object, or a controlled_by_parent master on its chain, declares writeRequiresMasterRead". The walk confirms it. hopRel.writeRequiresMasterRead is the only new branch. A hop's relation is resolved only for a controlled_by_parent object, and the walk stops at the first master that governs its own rows. chainAsksMasterRead in explain picks prose and never a verdict. Right, and stated as a necessary condition, which is what it is.

② Semver level

skip-changeset label present. docs/adr/** ships in no package, so nothing is published and no bump is owed. Clause-②: no on the PR body is correct: the diff adds or removes no authorable key, refusal or member. The widening it records shipped with Clause-②: yes (widening) on #33369 and was reviewed there (6881744779). Metadata-protocol: none is correct.

③ Boundary flags

  • Dev deviation, the card thread ran on the claim's default tier although dispatch-gates --tier reads MANDATORY contract-review tier for docs/adr/**: answered. This record is the at-tier review the deviation names.
  • Dev deviation, signature as a prose line rather than a signed HTML comment: no contract effect. The body on the head carries both the prose line and a trailing signed comment.
  • Dev deviation, origin/main moved from fc283d6d to b4624440 before the branch was cut: harmless. Both contain df9fe946, and the anchors the card names (the status banner, §3 rule, both "Option 2" lines, the set/empty-master line) were found at the same places on the head.
  • open_questions: none filed.
  • Out-of-scope findings (N2 gate-comment drift, N4 unpinned emptying and lifecycle verbs): the dev did not file them, and the ADR's Proof paragraph names N4. Escalated to the seat, see ④ nit 6.
  • Ruling coverage: ruling record 6880588685 (A+) rules insert, update and delete by opt-in with the default unchanged, and lifts the "Option 2 was not chosen" line of 6398566164 for opted-in objects. The amendment records exactly that, and names the four points the ruling left to the seat: the key name, the covered paths, the relation to ownerMayEdit and the lint arm. Contract review 6881744779 ① items 5 and 6 are recorded under "The two seat calls (veto window)" as that review asked.

④ Design judgment

docs/adr/0055-master-detail-controlled-by-parent.md: holds. I checked every factual sentence of the new section and of the five ⚠️ notes against main:

  • Reader. controlledByParentWriteRequiresMasterRead (object.zod.ts) answers === true only, does not read sharingModel, and is exported from @objectstack/spec/data (export * from './object.zod'; rows in api-surface/data.json and export-origins/data.json). The relation resolver (security-plugin-cbp-relation.ts), explain and the lint (masterReadSuffices) each call it. Matches "only the boolean true opts in", and "the gate, explain and the lint ask that one reader".
  • Two parse refusals. refuseControlledByParentWriteRequiresMasterReadMisplaced runs in the object superRefine. When sharingModel is not controlled_by_parent, it refuses at ['sharing','controlledByParent','writeRequiresMasterRead'] and returns. On a detail that also has ownerMayEdit: true, it refuses at [...,'ownerMayEdit'] with exactly "Delete ownerMayEdit; writeRequiresMasterRead already admits the owner." false is never refused. Matches.
  • Per-hop gate. assertControlledByParentChainEditable has if (hopRel.writeRequiresMasterRead) { await assertMasterRowReadable(...); break; } ahead of assertMasterRowEditable. assertMasterRowReadable is readUnlessRefused(readRowById(rel.master, masterId, context)) under the principal's own context. A refused read gives object_permission, no row gives row_level_security, and a store fault propagates. Insert, batch insert, by-id, predicate (controlledByParentPredicateWriteMasters), re-point and emptying (askBodyNamedMaster) all go through this walk. Matches "one swap point", "the walk ends there" and every shape listed under "What reaches the walk". The ⚠️ on the set/empty-master line holds: on a stored null-master row, nullMasterRowWritable runs before askBodyNamedMaster([]), and emptying asks the walk on the stored master, which reads on an opted-in detail.
  • Seat call on chains. A non-declaring hop runs assertMasterRowEditable on its master, and the next hop's relation decides the question one level up. So a non-opted detail under an opted controlled_by_parent master is written by a reader of the top master, with that master's update grant and authored policies still asked on its hop. Pinned: [#33250] chains (line under milestone). Matches.
  • Delegation. Step 2.8 calls assertControlledByParentWrite with the agent's sets and context, then again with the delegator's. Step 2.7 sets masterGateCoversThisWrite = !delegatorSets, so a delegated by-id write keeps both floors. A predicate write's floor never comes off, because platformFloorYieldsToObjectWriteModel needs the by-id vouch. These are the N3 floors, stated in plain words ("always subject to those floors"; "both able admit it, subject to the floors above"). N3 is met.
  • 404 and 403 answers. Step 2.7 throws recordNotFoundError through callerCannotReadAddressedRow before step 2.8 runs. The detail's read derives only from the master arm (security-plugin-cbp-read-filter.ts: View All on the detail opens only the null-master arm). So a by-id non-reader gets 404, and an insert or a re-point gets the gate's 403 with "requires read access to its master record". Pinned: expectHiddenRow, expectMasterReadRefusal. Matches feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369's Acceptance notes.
  • Lifecycle verbs. Step 2.8's list is ['insert','update','delete','transfer','restore','purge'], and the gate is verb-blind. masterGateCoversOperation maps the lifecycle verbs onto update and delete for the floor hand-over. Matches "Every verb". As stated, they are not pinned under the opt-in (N4).
  • Lint stand-down. if (detail.masterReadSuffices) continue; sits ahead of both pushers. Both pushers report SECURITY_CBP_OWNER_POLICY_WITHOUT_OPT_IN. Matches "both branches". No rule is added.
  • Against feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369's diff and Acceptance notes. Every behaviour feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369 lists (key, refusals, per-hop read, the covered shapes, on-behalf-of, explain, the lint, 404 versus 403, the predicate floor) is in the amendment with the same polarity. Nothing in the amendment goes beyond the shipped diff.

Attempts to break it: I looked for a sentence that states a narrower or wider rule than main. I found none that a consumer would derive a wrong invariant from. Findings, all nits:

  1. Nit, stale sentence not marked in place. The 7 Sep chain amendment, "What both halves do now", Write bullet, still says the walk runs "the same three master-edit legs ... on every hop until it reaches a master that governs its own rows". On an opted-in hop the walk asks read and stops. The new invariant paragraph re-dates only the "What is unchanged" lists, and that bullet is not one of them. §3's ⚠️ and the new status line both point to the amendment, so a careful reader is warned.
  2. Nit, 7 Nov status line. It says "setting a master asks edit on it, emptying one asks edit on the stored master" and got no ⚠️. Its body line did get one. The PR annotated the 26 Oct banner in exactly this way, so this is an inconsistency in its own pattern.
  3. Nit, "every refusal still says 'requires edit access'" (the invariant paragraph). The 7 Nov null-master owner refusal ("permitted only to the record's owner or a holder of Modify All") never said that. Read it as "every master-gate refusal".
  4. Nit, "verbatim". The ruling record's option text continues with "HotCRM 的设计授权全部兑现。", and the ADR omits it while calling the quote verbatim. The omitted sentence is rationale, not rule, and the card quoted the same cut.
  5. Nit, silence on a predicate write by a non-reader. It selects no row it cannot read, so it judges no master and writes nothing. That is the same as main on any detail. "What a refusal answers" covers by-id, insert and re-point only.
  6. Nit, carriers. N2 (gate comments that still say "asks EDIT" for an emptied or named master) and N4 (pins for emptying and lifecycle verbs) are code follow-ups and are not filed. Review 6881744779's observation also has no carrier line: the attachment and comment gates' fallback admits on allow alone when no explain is served. The seat files one follow-up card for the three. Nits 1 to 5 live in a governed hits file, so fixing them moves the head. Accept them as deviations, or fold them into that card.

No must-fix, no should-fix.

Implemented-by: claude/project-thread-mllmvl
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

Adopted by the seat 2026-11-14T20:33Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

维护者速读

席位核对

Verdict: ACCEPT, held for the maintainer · head 5647b9f9800bb89aa4dc69d5162d8733fdc6ab7b · card #33373 · report 6883674498 · contract review 6883816828 (PASS on this head, CONTRACT_REVIEW_TIER; Ruling-ref for the in-seat review: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb option A).

  • Shape: draft → main, Fixes #33373. docs/adr/0055-master-detail-controlled-by-parent.md only (+52 / -1). skip-changeset (nothing published). GOVERNED, Tier H (docs/adr/**). Clause-②: no; Metadata-protocol: none.
  • Ruling: Leehom 2026-11-14 A+ 「开关打开后,能读主记录的人按子对象自己的授权新增、修改、删除,默认不变。」 (record 6880588685 on controlled_by_parent: per-object opt-in so read on the master is enough to insert and update the detail under the detail's own grants (ownerMayEdit covers owner update only; HotCRM Order to Cash blocked) #33250), quoted; the 2026-10-25 ruling paragraphs are byte-identical, amended by notes, not rewritten.
  • N1 and N3 (comment 6882016066): both met — the precise invariant replaces "byte for byte", and the ownership floors are stated in plain words.
  • Gates: 21 derived, 21 exit 0, --ran 0 NOT MEASURED. Check-runs on the head: 53, 27 success, 26 skipped, none failing. Wording matches skills: teach sharing.controlledByParent.writeRequiresMasterRead in the controlled_by_parent authoring guidance #33374's skill text (checked between the two threads).
  • Accepted deviations (review nits, in the governed ADR, so not fixed on this head): (1) the 2025-09-07 chain amendment's Write bullet still says every hop runs the edit legs, unmarked; (2) the 2026-11-07 status line lacks the ⚠️ its body line has; (3) "every refusal still says 'requires edit access'" should read "every master-gate refusal"; (4) the ruling is called verbatim but omits its rationale sentence 「HotCRM 的设计授权全部兑现。」; (5) a predicate write by a non-reader (selects nothing, as on main) is not mentioned. Process: the thread ran at the default tier, met by this in-seat review.
  • Follow-up the seat files: one card carrying nits 1–5 plus review 6881744779's N2 (stale "asks EDIT" comments in the gate), N4 (no tests yet for emptying a master and the lifecycle verbs under the opt-in) and its observation on the attachment and comment gates' allow-only fallback.
  • Landing: Tier H. The seat does not flip this PR ready or enqueue it. It needs the maintainer's merge, or an authorised APPROVED review after which the seat lands it.

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 14, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 14, 2026 21:26
@os-zhuang
os-zhuang enabled auto-merge November 14, 2026 21:26
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit e2d6953 Nov 14, 2026
59 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-mllmvl branch November 14, 2026 21:48
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Correction: contract-review tier

The contract-review record(s) 6883816828 on this PR say Served-tier: CONTRACT_REVIEW_TIER, and the seat adopted them as "isolated sub-agent at CONTRACT_REVIEW_TIER". That is wrong. The seat launched the reviewing sub-agent without the tier, so it ran at the seat's own default tier, not at CONTRACT_REVIEW_TIER. The seat found this on its own audit. The review was still isolated and adversarial, and its findings stand as an off-tier review. They are not the at-tier record AGENTS.md and the in-seat review ruling require.

This PR has already merged. The seat re-runs the review at CONTRACT_REVIEW_TIER against the merged tree and files a card for any finding it turns up.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5647b9f9800bb89aa4dc69d5162d8733fdc6ab7b
Local-runs: none
Scope: docs/adr/0055-master-detail-controlled-by-parent.md + none

At-tier re-review of a merged PR (merge commit e2d69534b5, an ancestor of main at 6e23180f04a). It replaces the off-tier record 6883816828 as the record on this head; that record was not used as evidence. Inputs read: card #33373 body and all four comments (N1/N3 addendum 6882016066, claim 6883503202, dev report 6883674498, ACCEPT 6883846498); PR #33425 body, its one-file list and the net diff against the merge-base b4624440; the check-runs on the head. Read to judge, not reviewed, at b4624440 (carries PR #33369 as df9fe946): ruling record 6880588685 on #33250; packages/spec/src/data/object.zod.ts; packages/spec/src/contracts/security-service.ts; in packages/plugins/plugin-security/src/: security-plugin-cbp-write-gate.ts, security-plugin-cbp-relation.ts, security-plugin-authz-middleware.ts (steps 2.7 and 2.8), platform-ownership-policies.ts, security-plugin.ts (platformFloorYieldsToObjectWriteModel), explain-engine.ts, controlled-by-parent-owner-may-edit.test.ts; packages/lint/src/validate-security-posture.ts. The diff carries no implementing file, hence + none.

Check-runs on the head: none failed, none pending; every run concluded success or skipped. Green: Check Changeset, Check Documentation Links, Governed Surface Queue Guard, PR-body guards, Lint and Repo Gates with every Lint shard (docs and code guards, which carries the ADR anchor gates, and PM dispatch-gates included), every Type Check job, Test Core, Test UI, Dogfood Regression Gate. The skipped runs are path-filtered build, UI, E2E and opt-in jobs a docs-only diff does not trigger.

main since the merge: docs/adr/0055-master-detail-controlled-by-parent.md is byte-identical on main to this head (no commit touches it after e2d69534b5). Of the code the ADR cites, only security-plugin-authz-middleware.ts moved (a1e04022590, PR #33290, refusal user text by locale); the step 2.7 vouch, the read-door call and the step 2.8 verb list and delegator pass are unchanged by it, and the gate file is untouched. No sentence of the amendment is made stale by main.

① Derived judgments

One ADR file, +52 / -1. No schema, export, refusal or code is published, so no accept-set and no public surface moves. What the diff implies:

  1. The ADR stops stating a narrower rule than the runtime serves. Before it, the 2026-10-26 amendment said Option 2 "was not chosen" and its door "stays closed" while main served that door by opt-in. Right.
  2. History is kept. The diff's single removed line is the 2026-10-26 status banner, re-added whole with a ⚠️ note appended; the 2026-10-25 ruling paragraph and the "Option 2's door" paragraph are untouched context lines, each followed by a ⚠️ paragraph that lifts it for opted-in objects only. Right.
  3. The invariant is stated as a necessary condition ("a write's answer moves only if its own object, or a controlled_by_parent master on its chain, declares the key"), not as "byte for byte". Right: if (hopRel.writeRequiresMasterRead) in assertControlledByParentChainEditable is the only new verdict branch, a hop relation exists only for a controlled_by_parent object, and chainAsksMasterRead picks prose only. The ruling record's own gloss ("an object that does not declare it is byte-identical") is narrower than the shipped chain behaviour, and the ADR says so under the seat calls instead of repeating it. Right.
  4. The ⚠️ under the 2026-11-07 set/empty-master line reverses that amendment's guard ("emptying one asks edit on the stored master") for opted-in details. Right against the code: a by-id write reads the stored master and hands it to the walk, which asks read on an opted-in hop; a master reader who may re-point a row may equally detach it. It is recorded, and named as unpinned in Proof.

② Semver level

skip-changeset label present and Check Changeset green. docs/adr/** ships in no package: nothing is published, no bump is owed. Clause-②: no is correct, since the diff adds or removes no authorable key, refusal or member; the widening it records shipped under Clause-②: yes (widening) on PR #33369. Metadata-protocol: none is correct.

③ Boundary flags

  • Dev deviation, thread ran at the default tier although docs/adr/** is MANDATORY contract-review tier: answered by this record, which is the at-tier review. The seat's correction comment 6886594154 states the earlier record was off-tier.
  • Dev deviation, prose signature line instead of a signed HTML comment: no contract effect.
  • Dev deviation, branch cut from b4624440 instead of the order's fc283d6d: harmless, both carry df9fe946, and the five anchors the card names were found where it names them.
  • open_questions: none.
  • Dev out-of-scope findings (review 6881744779 N2, stale "asks EDIT" comments in the gate; N4, emptying and lifecycle verbs unpinned under the opt-in): confirmed against the tree. The gate comments at the askBodyNamedMaster definition and at the stored null-master branch still say EDIT; no [#33250] describe covers emptying or transfer / restore / purge. Not filed by the dev. Escalated: the seat files the follow-up card (finding 7).
  • Card N1 and N3 (comment 6882016066): both met. N1 by the invariant paragraph; N3 by the three floor bullets and "subject to the floors above".
  • Off-tier record 6883816828, finding by finding: nit 1 (2026-09-07 Write bullet) confirmed; nit 2 (2026-11-07 status line) confirmed; nit 3 ("every refusal") confirmed; nit 4 ("verbatim") confirmed against ruling record 6880588685; nit 5 (predicate write by a non-reader) confirmed; nit 6 (carriers) confirmed. None rejected. It raised no should-fix and no must-fix, so none is owed a fix on this head; none of its nits is fixed on this head, as expected (the head did not move). This review adds one nit it missed (finding 6).

④ Design judgment

docs/adr/0055-master-detail-controlled-by-parent.md: holds against the code it cites and against its first consumer, PR #33369's tree. Each sentence of the new section and of the six ⚠️ notes was read against b4624440:

  • Spec and reader. writeRequiresMasterRead: z.boolean().optional() sits beside ownerMayEdit in the closed block; controlledByParentWriteRequiresMasterRead answers === true only and is exported through export * from './object.zod'; the relation resolver, chainAsksMasterRead and the lint's masterReadSuffices each call it. Agrees.
  • Parse refusals. refuseControlledByParentWriteRequiresMasterReadMisplaced refuses at the writeRequiresMasterRead path off a controlled_by_parent object and returns, then refuses at the ownerMayEdit path with the quoted fix sentence, character for character. false returns at the first line. Agrees.
  • Gate. One swap, assertMasterRowReadable then break, ahead of assertMasterRowEditable. Single and batch insert, by-id, predicate masters, re-point and emptying all call the same walk. assertMasterRowReadable maps a refused read to object_permission and no row to row_level_security; readUnlessRefused lets a store fault propagate. Agrees, including "the walk ends there" and the fail-closed bullet that the hops below keep the cycle guard and depth bound.
  • Chains. A non-declaring hop runs the edit legs, then the next hop's relation decides; pinned by the [#33250] chains describe (a line under an opted-in milestone, admitted for a contract reader and refused "requires read access" for a non-reader). Agrees with the first seat call.
  • Floors and delegation. masterGateCoversThisWrite = !delegatorSets; platformFloorYieldsToObjectWriteModel needs that by-id vouch; step 2.8 calls the gate for the agent, then for the delegator under delegatorContext. Agrees with the three floor bullets and the on-behalf-of bullet.
  • Answers. callerCannotReadAddressedRow throws recordNotFoundError in step 2.7, before step 2.8; the gate's sentence is "requires read access to its master record". Pinned by expectHiddenRow and expectMasterReadRefusal. Agrees.
  • Verbs. Step 2.8's list is insert, update, delete, transfer, restore, purge, and the gate branches on no verb for the walk. Agrees with the second seat call.
  • Null-master row. nullMasterRowWritable runs before askBodyNamedMaster([]) on a stored null-master row, opted in or not. Agrees.
  • Lint. if (detail.masterReadSuffices) continue; precedes both pushers of security-controlled-by-parent-owner-policy-without-opt-in. Agrees.
  • Ruling. Record 6880588685 gives option A+, chosen 2026-11-14 at 16:21:29 UTC, and the four points left to the seat. PR feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369 merged 2026-11-14. Agrees.

Tried to break it by looking for a sentence a consumer would derive a wrong rule from, wider or narrower than the tree. None found in the new section's rules. Findings:

  1. Nit. The 2026-09-07 amendment's Write bullet is stale and unmarked. "What both halves do now" still says the walk runs "the same three master-edit legs ... on every hop until it reaches a master that governs its own rows". On an opted-in hop it asks read and stops. The invariant paragraph re-dates only the "What is unchanged" lists, and this bullet is not in one. Mitigated by the status line, the §3 ⚠️ and the code at the cited walk, so no reader acting on the ADR as a whole is misled; this is the strongest of the nits and the first to fix.
  2. Nit. The 2026-11-07 status line has no ⚠️. It says "setting a master asks edit on it, emptying one asks edit on the stored master"; its body line got the note, and the PR marked the 2026-10-26 status line on the same ground. The new status line sits directly under it.
  3. Nit. "every refusal still says 'requires edit access'" in the invariant paragraph. The null-master owner refusal ("permitted only to the record's owner or a holder of Modify All") never said that. It should read "every master-gate refusal of a master".
  4. Nit. "verbatim". The ruling record's option text has a second sentence, 「HotCRM 的设计授权全部兑现。」, which the ADR drops while calling the quote verbatim. It is rationale, not rule; the card quoted the same cut.
  5. Nit. A predicate write by a non-reader is not described. It selects no row it can read, judges no master and writes nothing, as on any detail on main. "What a refusal answers" covers by-id, insert and re-point only.
  6. Nit, new. "checkControlledByParentWrite answers deny for all of them". The member's signature is (object, recordId, context): it is asked about a stored record, so it answers the by-id cases (pinned: object_permission, row_level_security). An insert and a re-point body cannot be put to it. "For all of them" should read "for a stored row of such a caller".
  7. Nit, carriers. N2 and N4 of review 6881744779, and that review's observation on the attachment and comment gates' allow-only fallback, have no card. The PR is merged, so findings 1 to 6 cannot be fixed on this head: the seat files one follow-up card carrying 1 to 6 (prose, a governed path, its own Tier H PR) and one for the code follow-ups N2 and N4, or accepts 3 to 6 as deviations and cards the rest.

No must-fix, no should-fix. Nits alone end the round.

Implemented-by: claude/project-thread-mllmvl
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

Adopted by the seat 2026-11-15T00:51Z · isolated sub-agent at CONTRACT_REVIEW_TIER (served model verified by the seat) · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

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 pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/s 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.

ADR-0055: record the 2026-11-14 amendment, writeRequiresMasterRead (read on the master is enough to write, by opt-in)

3 participants