Skip to content

fix(plugin-security)!: under a controlled_by_parent declaration whose one master reference is readonly or system, every write that sets it is refused (ADR-0055 §1) - #25256

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-fi0g2a
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-fi0g2a

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25095

Clause-②: no (narrowing)
Metadata-protocol: none

⛔ Disclosure ruling 5323310017 (#9387) applies: this PR states the rule and its prescription only. No probe, posture, caller, request or served value is recorded here or in any comment; the measurements are held privately.

Route. Ruling 6345869077 on #25095 chose route B: refuse at the write gate. This PR implements that ruling; it supersedes the narrower insert-only route this draft first carried.

The rule

ADR-0055 §1 derives a controlled_by_parent detail's access from exactly one required master_detail that every write of a detail supplies. Since #25092 the parse refuses that master when it is declared readonly: true or system: true (controlledByParentMasterRefusal, shape master-not-writable). An object with that declaration still reaches the registry through a door that does not parse:

  • a raw registerObject or registerApp;
  • defineStack(…, { strict: false });
  • a stored object the boot registers under its [metadata_spec_invalid] diagnostic (the registry registers off-spec rows for availability).

On such an object the write gate let writes that set the master reference through, and a detail could end up outside the master its access derives from: readable by nobody, while the write reported success.

Now the write gate refuses every write that sets that reference, 422 INVALID_METADATA, before any master is asked and before anything is written:

  • every insert, single or batch, whether or not its body names a master;
  • every update whose body carries the reference key as an own property, whatever the value, null included (an upsert reaches the gate as one of the two).

The message is the parse's own refusal of the declaration, prefixed with the write it refused, so it carries the parse's prescription: remove the flag and declare the master reference as an ordinary required field, or give the object its own sharingModel. Both flags follow one rule, and every caller the gate judges gets the same answer.

Premise checked first (ruling item 7): on all three doors above, the object registers, resolveCbpRelation resolves its relation, and the flag is readable on the field def the relation came from.

What changed

  • packages/plugins/plugin-security/src/security-plugin.ts
    • CbpRelation.masterNotWritable: resolved in resolveCbpRelation from the same field def the relation came from, judged by controlledByParentMasterRefusal on that one def, so the flags and the sentence are spelled once, in the parse. The relation still resolves; the read leg, the cache and reportCbpMasterUnresolved are untouched. No second registry reader, and omissionRefusedByValidation is not widened.
    • assertControlledByParentWrite: right after the missing-relation leg, before any master is asked, writesMasterReference (insert always; update when the body carries the key) throws MasterReferenceNotWritableError. A delete, and an update that leaves the key alone, take the path they took before.
    • The two stale freeze-note paragraphs at the empty-master branch are rewritten to the current state (both ramp legs have landed, the parse refuses all three shapes, and no insert of theirs reaches that branch). The freeze itself stands.
  • packages/plugins/plugin-security/src/errors.ts: MasterReferenceNotWritableError beside MasterDetailRelationMissingError (INVALID_METADATA, 422; the error-code ledger already has this row for plugin-security). Not exported. The MasterReferenceMissingError docblock is corrected.
  • .changeset/25095-cbp-unwritable-master-write.md: patch, BREAKING, Clause-②: no (narrowing), Metadata-protocol: none, ADR-0087 not-required (already-registered cbp-master-relation-tiers-retired), as pnpm check:adr-0087-registration judges it.
  • content/docs/api/error-catalog.mdx and content/docs/protocol/kernel/error-handling.mdx: corrected, because this PR makes them false. They said a write that leaves out a readonly or system master reference answers 422 MISSING_REQUIRED_FIELD; such a write now answers 422 INVALID_METADATA (the two-item list above), and the 422 MISSING_REQUIRED_FIELD exception is narrowed to a by-id update or delete of a detail whose stored master reference is null. The flat example body moves to that case.

Unchanged, on purpose

  • Reads. The relation still resolves, so rows already stored keep the master their reads derive from.
  • A delete, and an update whose body leaves the key out.
  • System contexts (the bypass is unchanged), objects with an ordinary required master, and every object that does not declare controlled_by_parent.
  • No packages/spec, packages/objectql or lint change, and no read-side change.
  • The served member (checkControlledByParentWrite) asks a by-id update with no body, so it never reaches the new refusal, and its closed outcome vocabulary is untouched.

Tests

packages/plugins/plugin-security/src/controlled-by-parent-unwritable-master-write.test.ts (renamed from …-insert.test.ts, 83 cases). Real stack: ObjectQL over better-sqlite3, the real SecurityPlugin, the real sharing service and middleware, a member context from resolveAuthzContext + assembleExecutionContext, a platform admin's carried posture, and the engine's security source wired as a kernel wires it, so a detail read composes the engine's ReadableIds leaf over its master. The flagged declarations are registered raw through registerApp, for readonly, system and both, on an ordinary object and on a sys_-named one. For a member and for a platform admin:

  • a single create naming a master, a single create naming none, and a batch create each answer INVALID_METADATA / 422 with exactly the parse's message, and nothing is stored; the master walk is never entered;
  • a by-id update naming a master, and one setting it to null, answer the same, and the row keeps its master;
  • controls: a stored row is still read, an update that leaves the key out lands, a delete lands;
  • a detail declared as the three in-repo controlled_by_parent objects declare theirs (Field.masterDetail, required: true, through the parse) takes single and batch creates, a re-pointing update, a note update and a delete as before.

A system create lands on every flagged declaration, and each flagged declaration is one the parse refuses at fields as master-not-writable.

Moved pins, by the ruling: controlled-by-parent-sharing.test.ts's [#8688] flagged-insert cases now expect INVALID_METADATA with the parse's message (and its stale "AUTHORABLE" wording is corrected); controlled-by-parent-every-row-master.test.ts's cases on its system-flagged detail now expect INVALID_METADATA for every batch create and every emptying update, and its "the stored master is asked first" ordering pin moves to the validated detail, where it still holds.

Ablation and the gate list are reported in the card's os-dev-report.

Notes


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB

…olled_by_parent declaration whose one master reference is readonly or system (ADR-0055 §1)

The parse refuses that declaration, so an object carries it only through a
door that does not parse. There the write gate now refuses an insert that
names a master, with the parse's own message (422 INVALID_METADATA), before
any master is asked. Stored rows keep their master and their reads.

Claude-Session: https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 10 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))
  • content/docs/deployment/install-modes.mdx (via INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))
  • content/docs/permissions/authorization.mdx (via INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))
  • content/docs/permissions/field-level-security.mdx (via SecurityPlugin (symbol, a top-level class), statusCode (symbol, a field of class MasterReferenceNotWritableError))
  • content/docs/permissions/index.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/error-handling.mdx (via INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))
  • content/docs/protocol/kernel/plugin-spec.mdx (via statusCode (symbol, a field of class MasterReferenceNotWritableError))
  • content/docs/ui/forms.mdx (via SecurityPlugin (symbol, a top-level class))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))
  • content/docs/releases/v17/17-5.mdx (via INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))
  • content/docs/releases/v17/17-6.mdx (via INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))
  • content/docs/releases/v17/17-7.mdx (via INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))
  • content/docs/releases/v18/18-0.mdx (via SecurityPlugin (symbol, a top-level class), INVALID_METADATA (literal, a string literal in MasterReferenceNotWritableError))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 284e04810a309d5a7c8c3654b036f9ebf07a53d4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7af7fc8a34f75e5670fd8438f1f12bc5d514202d — the merge of head 5aea100c1554460a0b97e78c3006f312f7b74981 into base 284e04810a309d5a7c8c3654b036f9ebf07a53d4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7af7fc8a34f75e5670fd8438f1f12bc5d514202d && git checkout 7af7fc8a34f75e5670fd8438f1f12bc5d514202d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 284e04810a309d5a7c8c3654b036f9ebf07a53d4 5aea100c1554460a0b97e78c3006f312f7b74981 && git checkout -B drift-repro 284e04810a309d5a7c8c3654b036f9ebf07a53d4 && git merge --no-ff 5aea100c1554460a0b97e78c3006f312f7b74981

node scripts/docs-audit/affected-docs.mjs --json 284e04810a309d5a7c8c3654b036f9ebf07a53d4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284e04810a309d5a7c8c3654b036f9ebf07a53d4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…arent master reference declared readonly or system (ADR-0055 §1)

Ruling 6345869077 on #25095 (route B, refuse at the write gate). The
relation still resolves for such a declaration, so reads, the cache and
the unresolved report are unchanged and stored rows keep their master.
CbpRelation.masterNotWritable is now read from the same field def the
relation came from, judged by the parse's own predicate on that one def.

The gate refuses, before any master is asked and before anything is
written, every insert (single or batch, with or without a master in the
body) and every update whose body carries the reference key (any value,
null included), 422 INVALID_METADATA with the parse's message. A delete
and an update that leaves the key alone take the path they took before.
The same answer for every caller the gate judges; the system bypass is
unchanged.

Tests: the suite is renamed to controlled-by-parent-unwritable-master-write
and now covers a member and a platform admin, ordinary and sys_-named
objects, inserts with and without a master, batch inserts, updates
naming a master or null, and the controls (stored rows read, an update
without the key, a delete, a system create, the in-repo declaration
shape). The [#8688] flagged-insert pin in controlled-by-parent-sharing
and the unjudged-reference pins in controlled-by-parent-every-row-master
move to the new answer; the stored-master-first ordering pin moves to
the validated detail. The stale AUTHORABLE wording and the two stale
freeze-note paragraphs are corrected.

Claude-Session: https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(plugin-security)!: an insert naming a master under a controlled_by_parent declaration whose one master reference is readonly or system is refused (ADR-0055 §1) fix(plugin-security)!: under a controlled_by_parent declaration whose one master reference is readonly or system, every write that sets it is refused (ADR-0055 §1) Oct 22, 2026
…-fi0g2a

# Conflicts:
#	packages/plugins/plugin-security/src/controlled-by-parent-sharing.test.ts
#	packages/plugins/plugin-security/src/security-plugin.ts
…table-master suite after #25198

The detail read is now the engine's ReadableIds leaf over its master, so the
rig fills the engine's security-source slot as a kernel does. The read-access
sentence in the error docblock and the changeset names the security plugin,
not the write gate.

Claude-Session: https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l and removed size/m labels Oct 22, 2026
…answer INVALID_METADATA for a readonly or system master

This PR changes what the two flagged declarations answer, so the two pages
that published the old 422 MISSING_REQUIRED_FIELD rows are corrected here:
the stored-null-master shape keeps MISSING_REQUIRED_FIELD (its example now
shows that message), the flagged rows answer 422 INVALID_METADATA for every
write that sets the reference, and the stale "authorable today" paragraphs,
false since the parse refuses both declarations, are removed.

Claude-Session: https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5aea100c1554460a0b97e78c3006f312f7b74981
Local-runs: none
Scope: packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/errors.ts, packages/plugins/plugin-security/src/controlled-by-parent-unwritable-master-write.test.ts, packages/plugins/plugin-security/src/controlled-by-parent-sharing.test.ts, packages/plugins/plugin-security/src/controlled-by-parent-every-row-master.test.ts, .changeset/25095-cbp-unwritable-master-write.md, content/docs/api/error-catalog.mdx, content/docs/protocol/kernel/error-handling.mdx + packages/spec/src/data/controlled-by-parent-master.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/conversions/refused-shapes.ts, packages/spec/src/stack.zod.ts, packages/plugins/plugin-security/src/platform-ownership-policies.ts, packages/plugins/plugin-security/src/index.ts, packages/objectql/src/engine.ts, packages/objectql/src/registry.ts, packages/rest/src/import-prepare.ts, .github/workflows/pr-automation.yml

Face: Clause-②: no (narrowing) on the PR body and the changeset. Ruling of record: 6345869077 on #25095 (route B, refuse at the write gate). Inputs: the card and its ten comments, the PR body, file list and net diff against main, and the check-runs on the head. ⛔ Disclosure (#9387): abstract only; no probe, request, posture or served value is written here.

Gate verdicts. On the head, every completed check is success or skipped (Lint & Repo Gates, Build Core, Build Docs, Check Changeset including its ADR-0087 step, Governed Surface Queue Guard, Dogfood Regression Gate, the claim and duplicate guards); the Test Core shards and one Type Check lane were still in_progress at review time. The compare endpoint 0d2447846...5aea100c reports exactly two files, both doc pages, so the dev's "identical code" claim holds, and on 0d2447846 every check-run is success or skipped, Test Core, TypeScript Type Check and Lint & Repo Gates included. The docs-only delta moves one gate, Build Docs, and that one is green on the head itself. Those conclusions are the derived-gate verdicts this record leans on; nothing was built, run or re-run locally.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged against the ruling and the cited code at the head.

  1. The refusal, and where it sits — right. resolveCbpRelation carries one more bit on CbpRelation (masterNotWritable), read from the same field def the relation came from, judged by the spec's own controlledByParentMasterRefusal on a one-field map under the controlled_by_parent model and kept only when the shape is master-not-writable. The resolver's own predicate already guarantees that def is a required master_detail, so the predicate's one-master branch is the one reached and the flags are read === true exactly as the parse reads them. No second registry reader; cbpRelCache, the read derivation and reportCbpMasterUnresolved untouched; omissionRefusedByValidation byte-identical. In assertControlledByParentWrite the new branch is placed immediately after the !rel throw and before askBodyNamedMaster, before the batch walk, before the predicate-master walk, before readRowById and before the chain walk: so before any master is asked and before any store read, and the middleware runs it before the executor, so before any write. Right.
  2. Which writes it refuses — right, and every door. writesMasterReference is true for insert unconditionally (single body or array) and for update when the body, or any row of an array body, carries the reference key as an own property, null included; false for everything else. The step 2.8 guard hands the gate insert, update, delete, transfer, restore and purge. At the engine: a batch create is insert with an array (insertMany delegates to insert); a by-id update and a predicate or bulk update both reach the gate as update with the patch as opCtx.data, and the new branch precedes the single-id / predicate dispatch, so a predicate update carrying the key is refused whether or not it selects rows; the engine has no upsert verb (ENGINE_UPDATE_UPSERT_REMOVED), and the REST import's upsert write mode resolves per row to one of the two. The gate is called from exactly two places, the middleware (principal and delegator passes) and the served member. No path that sets the reference through the gate is left open. Right.
  3. What is kept — right. A delete and an update without the key return false from writesMasterReference and take the stored-row walk as before. A system context exits the middleware at its first branch (ahead of step 2.8) and the served member answers allow first; the new suite pins a system create landing. An ordinary required master yields undefined from the predicate, so the bit is null; a non-controlled_by_parent object returns before the resolver. Reads: the relation still resolves and only a field was added to the cached relation; the read leg is not in the diff. The transfer, restore and purge operations reach the gate and are not refused: they set no field from the caller's body, consistent with the ruling's two write shapes. Right.
  4. Envelope and class — right. MasterReferenceNotWritableError in errors.ts beside MasterDetailRelationMissingError: INVALID_METADATA, status and statusCode 422, message [Security] Configuration defect: ... followed by the spec predicate's sentence, so the prescription is spelled once in spec. The package index re-exports only PermissionDeniedError and its predicate from errors.ts, so the class is not a public export of @objectstack/plugin-security, as the body says. The error-code ledger's @objectstack/plugin-security block already carries INVALID_METADATA (the assertControlledByParentWrite answers a metadata defect and a missing row with the same 403 PERMISSION_DENIED "requires edit access to its master record" #7474 row), and the provenance gates ride in Lint & Repo Gates, green on both heads. Right.
  5. Served member — right, with one note. checkControlledByParentWrite builds a by-id update context with no data, so the new branch cannot fire from it and its closed outcome vocabulary is unchanged. controlledByParentWriteOutcomeOf has no mapping for the new class; from that member it is unreachable, and were a future caller to hand the gate a body, the error would rethrow unchanged rather than be swallowed. Right as shipped; noted for the lane.
  6. Caller classes — right within the ruling's scope. Every caller step 2.8 judges gets the same answer, delegator pass included. The pre-existing step 2.8 guard (a principal with no permission sets, or a context without a user id) skips the whole controlled-by-parent gate, as it did before this PR for every leg; the ruling's item 4 binds "every caller the gate judges", and this diff neither widens nor narrows that guard. Recorded by class only.
  7. Premise (ruling item 7) — holds at source. The resolver reads ql.getSchema, which reads the engine's SchemaRegistry; registerApp and registerObject write it directly; the registry's [metadata_spec_invalid] branch registers the item anyway after the failed validate; defineStack(…, { strict: false }) skips the parse and composes into the stack the engine registers. All three doors converge on the one entry the resolver reads, and the flag survives on the def because none of those doors rewrites it. The dev's premise report is consistent with the code.
  8. Freeze note and stale prose — right. The empty-master branch's two stale paragraphs are rewritten to the landed state; the if (operation === 'insert' && rel.omissionRefusedByValidation) return; stand-down and the MasterReferenceMissingError throw are unchanged, so the stored-row backstop stands.
  9. Docs — now true to the code. Both pages state: the remaining 422 MISSING_REQUIRED_FIELD is a by-id update or delete of a detail whose stored master reference is null, whatever the declaration (the stored-row leg still throws it, including under a flagged declaration when the update leaves the key out); the flat example body is the class's stored-row sentence verbatim; the two flagged rows of the table now read INVALID_METADATA; the message prefix [Security] Configuration defect: matches the class; the three non-parse doors are named; delete and key-less update answered as on any detail; the "authorable today" paragraphs are gone, true since PR feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit #25181. The HTTP-status tables on both pages are updated to match. Right.

② Semver level

③ Boundary flags

The dev's os-dev-report 6348208562 on #25095 lists open_questions: []. Its flags and deviations, and the ruling's unverified points, each answered:

  • Ruling item 3, ledger provenance comment: not needed; the existing plugin-security INVALID_METADATA row covers the class and the provenance gates are green. Answered.
  • Ruling item 3, served member vocabulary: untouched and unreachable from the member, as ①.5. Answered.
  • Ruling item 6, "say so in the test": the ruling asked the suite to state why the raw registration door alone pins the resolution (the three non-parse doors converge on the one registry entry the resolver reads). The dev report says the suite says so; the suite's docblock and boot comment name the raw door and do not carry that sentence. The pins themselves are as ruled, and the convergence holds at source (①.7), so no accept-set or pin is affected. Owed: one docblock sentence in the new suite, at the lane seat's ACCEPT; not a ground against this head.
  • Moved pins (ruling item 6): exactly the two files the ruling names. controlled-by-parent-sharing.test.ts: the two [#8688] flagged-insert cases move from MISSING_REQUIRED_FIELD to INVALID_METADATA with the parse's sentence, and the stale AUTHORABLE prose is corrected. controlled-by-parent-every-row-master.test.ts: on its one provenance-flagged detail, the batch-create and the emptying-update cases move to INVALID_METADATA, every stored-row null assertion and the key-less update control are kept, the held-master batch assertion is tightened to the whole envelope, and the "stored master is asked first" ordering pin moves to the validated detail, where the walk still runs, because the ruling's own ordering (refusal before any master is asked) makes it unprovable on the flagged one; the new suite pins the walk never being entered there. Nothing weakened.
  • Dev deviation, PR body patched twice through the relay: a process note outside the contract; the body read for this review is the current one. Noted.
  • Dev deviation, "What changed" missing the docs bullet: the current body lists both doc pages under "What changed". Resolved.
  • Out-of-scope finding, refused-shapes.ts sentence ("loses the save door" only): now incomplete, not false; the ruling routes it to the domain:spec seat and the pointer is on [PM seat] domain:spec · seat 2 — 🟢 os-bill #18549. Cross-lane, not this PR's. Noted.
  • Head gates still running at review time: Test Core and one Type Check lane on the head were in_progress; the identical-code head 0d2447846 is fully green and the compare confirms a docs-only delta, whose one moved gate (Build Docs) is green on the head. The queue re-runs the required set on the merged generation in any case. Not a ground.
  • Disclosure: the PR body, the tests and this record stay abstract; no served value is written.

Implemented-by: claude/project-thread-fi0g2a
Reviewed-by: session_016g7nYP9wdNx1GDa5XDr7Uy

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-22T18:58Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25095 (ruling 6345869077, route B; claim 6345497639; Ruling-ref 6348207514; card session branch claude/project-thread-fi0g2a) · reviewed head 5aea100c1554460a0b97e78c3006f312f7b74981

⛔ Disclosure (#9387): abstract. The PR body, the report 6348208562 and the contract review were read for probes, postures, requests or served values first; none carries one.

  • Shape: the write gate in plugin-security refuses, 422 INVALID_METADATA, every write that sets a controlled_by_parent master reference declared readonly or system (every insert; every update whose body carries the key), before any master is asked; the message is the parse's own controlledByParentMasterRefusal. 8 files, +727 / −151; no package export moves.
  • Ruling conformance: route B at the write gate; delete, key-less update, reads, system contexts, ordinary required masters and non-cbp objects unchanged; no spec / objectql / lint change; premise (item 7) measured first and held on all three non-parse doors.
  • Contract review: CONTRACT_REVIEW_TIER, isolated, record 6348410768 on this head — PASS, Local-runs: none, all 8 paths plus 10 cited files.
  • Declarations: fix(plugin-security)!, Clause-②: no (narrowing), Metadata-protocol: none, patch with the BREAKING banner, ADR-0087 not-required (already-registered cbp-master-relation-tiers-retired); Check Changeset green.
  • Pins and ablation (ruling item 6): the moved pins are exactly the ruling's two files; the ablation of the new branch turned 60 of the suite's 83 cases red and restored byte-identical.
  • Dogfood battery: full, on this head under the verify lock (289 files pass / 3 skipped; 2481 tests pass / 18 skipped; 0 failures).
  • Gates: dispatch-gates --ran 99 / 99 (29 from the two doc pages); plugin-security 4379 pass; every check-run on this head success or an expected skip.
  • Docs: error-catalog.mdx and error-handling.mdx are corrected in this PR, because the change made their rows false; the seat added the bullet to the body's What changed.
  • Accepted deviations: the harness-pinned branch (claimed); the PR body patched twice through the relay; one pr_request_reviewers relay write. Ruling item 6 also asked the new suite to say in a comment why the raw registration door alone pins the resolution. That sentence is missing, but the pins are as ruled and the reviewer verified the convergence at source. The sentence rides [maintainer] refactor(plugin-security): split src/security-plugin.ts (10,746 lines, +2,803 in 30 days) along its SecurityPlugin seams, one move-only slice per PR #25223, which is serial on this card and works in the same suite's neighbourhood.
  • Not blocking, noted: controlledByParentWriteOutcomeOf has no mapping for the new class, which is unreachable from the served member today. The pre-existing step 2.8 guard skips the whole gate for a context with no sets or no user, which is outside this ruling. refused-shapes.ts's "save door only" sentence is now incomplete; that belongs to the spec lane, which has a pointer on [PM seat] domain:spec · seat 2 — 🟢 os-bill #18549.
  • Landing: not governed (check-governed-merges --pr 25256: 0 of 8 paths, 878 lines). Ready + auto-merge through the relay.

Generated by Claude Code

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/l tests tooling

Projects

None yet

2 participants