Skip to content

refactor(plugin-security): move SecurityPlugin's controlled-by-parent write gate into security-plugin-cbp-write-gate.ts (#25223 cut 3) - #25324

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-3fzya4
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-3fzya4

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Part of #25223
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6353073966

Cut 3 of the security-plugin.ts split. The claim is 6354388643. The bodies of five async methods move unedited to the new module packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts:

  • checkControlledByParentWrite
  • assertControlledByParentWrite
  • assertControlledByParentChainEditable
  • controlledByParentPredicateWriteMasters
  • assertMasterRowEditable

Together they are the ADR-0055 §3 controlled-by-parent write gate the write path runs at step 2.8, and the check the security service serves. The three module-level declarations only those bodies read move with them: MASTER_EDIT_DENIAL_LEG, controlledByParentWriteOutcomeOf and writesMasterReference. So do the four error classes only those bodies construct or test.

It is move-only, and the card stays open for the later cuts. Branch: claude/project-thread-3fzya4, the harness-pinned branch for this session, restarted from origin/main at 21a25dd9f1 (cut 2's merge). It names no issue, so the claim comment is the identity record.

Five exports, said up front. security-plugin.ts adds the export keyword to five module-level declarations, and the new module imports them by value. Each line is otherwise byte-identical to base, and index.ts re-exports only SecurityPlugin, so none reaches the package's surface (the export tables below are unchanged). They stay in security-plugin.ts for these reasons:

  • CBP_MAX_CHAIN_DEPTH: ADR-0055:135 anchors it by symbol in that file (ruling 6353073966 item 5), and controlledByParentChainRefusal there walks it too.
  • requiredCapsForOperation: packages/spec/liveness/object.json anchors it by symbol in that file, and that file belongs to the spec lane. The CRUD gate reads it too.
  • PREDICATE_WRITE_READABLE_ID_CAP, predicateWriteReadableCapError and copyOfCallerPredicate: readableRowsScopeForPredicateWrite, which stays, reads all three. Moving them would leave a value import running the other way.

Each value is read only inside a function body, at call time, never while either module evaluates. So the two-way import is safe under ESM, and the published bundle holds one module. The plugin's 4403 tests run on the source modules, cycle included, and pass.

These stay in security-plugin.ts byte for byte, by ruling item 4, until docs PR #25316 (card #25314) lands: computeControlledByParentFilter, controlledByParentChainRefusal and composeWriteTenantWall.

The published .d.ts does not change

Under the series bar (ruling 6348891160 item 1), measured on base 21a25dd9f1 and on the head (the tree of this PR's commit):

  • Raw sha256 of dist/index.d.ts and dist/index.d.mts, base and head, all four the same: 803f5bfb7da0b00f8cd8d8f25172463d8dfb07d93fbdd8a9c6f4d4b7435e429a. The raw diff is 0 lines, so no union order moved either.
  • Normalised sha256, all four the same: b6b5c1b7fa6fa188f6e2f7e4cca3816a246b570f20d708643d116012aa86fa77.
  • The normaliser, run from packages/plugins/plugin-security (it sorts the members of each string-literal union and changes nothing else):
node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]*"(?:\s*\|\s*"[^"\n]*")+/g,r=>r.split(/\s*\|\s*/).sort().join(" | ")))' dist/index.d.ts | sha256sum
  • Positive controls on a copy of the head dist/index.d.ts, line 2308 (private assertControlledByParentWrite;):
    • Made public (assertControlledByParentWrite;): raw ea8bebdf8cd6c0e974393bf00a9434fce8ec2ba1c3cb816549fd203c39cb90c7, normalised 1a405f50b386e1209593962447896d000b736456d5dcaa64ab8f9921522053bb.
    • Renamed (private assertControlledByParentWriteRenamed;): raw 7a055168bef89d872b54102a60b735a6f12e7bf1af7f367ea67531e10b56654e, normalised 5f730b82bac2ce457a64717cd57973fa75fcf06d0aee180f1b5cb86c8e4d83ab.
    • Both differ from the real hashes, so the comparison does see a visibility change and a rename.
  • Export tables: the ESM and CJS bundles each export the same 63 names at base and head. The lists were loaded at runtime, not parsed. The sorted name list hashes f607fd765d6b277e08b3b46ccb16d1e2d133ec55658ffe40155aef25e1bf54f0 on all four, the same as at cut 2.
  • The bundle JS bytes differ (index.js 6d2a398f to aece9708, index.mjs 953c5d9a to 38b8f218), because code moved within them.

No changeset: skip-changeset, per ruling 6348891160 item 4.

What moved

Base line numbers are each member's signature at 21a25dd9f1. The last column lists the members the body reads, which is exactly what its host carries.

method base lines host members
checkControlledByParentWrite :9363-:9390 assertControlledByParentWrite, declaresControlledByParent, resolveOperationPrincipals
assertControlledByParentWrite :9525-:9780 assertControlledByParentChainEditable, controlledByParentPredicateWriteMasters, declaresControlledByParent, extractSingleId, readRowById, resolveCbpRelation
assertControlledByParentChainEditable :9804-:9899 assertMasterRowEditable, declaresControlledByParent, readRowById, resolveCbpRelation
controlledByParentPredicateWriteMasters :9937-:9986 ql
assertMasterRowEditable :10003-:10230 checkAuthoredRowWrite, collectRLSPolicies, computeRlsFilter, declaresControlledByParent, delegatorOwnWriteFloorOptions, getObjectSecurityMeta, permissionEvaluator, ql, resolveSharingCanEdit, resolveSharingWriteVerdict

All five methods are async, and every moved function is async too.

Module-level declarations that move, each with its docblock and verbatim: MASTER_EDIT_DENIAL_LEG (base :659, 11 lines with its docblock), controlledByParentWriteOutcomeOf (:673, 24 lines) and writesMasterReference (:740, 15 lines). No code left in security-plugin.ts reads them.

Imports: MasterDetailRelationMissingError, DetailRecordNotFoundError, MasterReferenceMissingError and MasterReferenceNotWritableError leave the errors.js import of security-plugin.ts. No code there reads them any more. The module imports them, plus the names its bodies share with code that stays (withoutOperationPrivateKeys, the metadata-core engine dispatch resolvers, PermissionSet, the two contract types and isPlatformOwnershipFloorPolicy). Those stay imported in security-plugin.ts too.

The shape (#23050 shape B, as cuts 1 and 2 applied it)

  • The class keeps each private method with its name, signature, docblock and position, because tsup emits private members with their JSDoc into the .d.ts. Each body is now one host literal plus fn.call(host, …), marked [#25223 cut 3].
  • CbpWriteGateHost types every member as the class member itself (SecurityPlugin['m']). Each moved function types its this as the Pick of the host over exactly the members its body reads, and each stub annotates its literal with the same Pick. There is no cast, and no member becomes public or protected.
  • The module imports SecurityPlugin type-only. CbpRelation keeps its one declaration in security-plugin.ts, and the module names it through the class's own signature, as cut 2's module does (an alias over the non-null return type of SecurityPlugin['resolveCbpRelation']).
  • Methods go in as late-bound closures that call this.m(...a) on the plugin at call time. A spy or test double set on the plugin is still the one the gate reaches. The bodies that call one another (the check and the gate, the gate and its chain walk, each leg and the master-row verdict) still do so through the plugin. No moved body calls a function-valued field, so no receiver changes.
  • Fields: ql and permissionEvaluator go as getters, because neither is declared readonly. start() assigns ql after construction, and security-plugin.test.ts replaces permissionEvaluator on a constructed plugin. The host declares both readonly, so a moved body cannot assign a member through it; the typecheck passes, so none does.
  • Async: every stub drops async and returns the moved function's own promise. Building the host reads no field and runs no getter, and calling an async function never throws synchronously, so a caller awaits a promise that settles exactly as the old method's did.
  • Nothing re-exports the module, so the package entry does not move.
  • The class docblocks keep their {@link} tags to the four error classes, byte for byte. Those names are no longer imported in security-plugin.ts, so an editor shows the links unresolved there. No gate resolves docblock links, and the .d.ts is unchanged.

Proof of move-only

  • Bodies, by AST token: for all 5 methods, the moved function's body has the same TypeScript token sequence as the base method body, and each of its 617 lines is the base line with two leading spaces removed. Parameters, return types and async match. A negative control, if (context?.isSystem) changed to if (!context?.isSystem), fails the check.
  • Class side: each method keeps its docblock and signature, with async dropped. The three moved declarations are verbatim in the module and absent from security-plugin.ts. Each of the five exported values is its base text with export added and nothing else. CbpRelation, computeControlledByParentFilter, controlledByParentChainRefusal and composeWriteTenantWall are byte-identical to base.
  • git's own view (git diff --color-moved=plain --color-moved-ws=allow-indentation-change 21a25dd9f1 HEAD over the two source files): 673 of 683 deleted lines and 709 of 923 added lines are detected as moved.
    • Deleted, not moved (10): the five declaration lines that gain export, and the five private async signature lines that lose async.
    • Added, not moved: the replacements of those ten lines, the stub bodies, and the import of the new module with its comment. In the module: the license header, the module docblock, the imports, the CbpRelation alias, CbpWriteGateHost, one pointer docblock per function and the rewritten signature heads. The rest are lines git does not pair with a moved block, such as blanks and closing braces.
  • Size: 1613 changed lines (+930 / -683, 3 files), under the 3000-line human-merge threshold. security-plugin.ts goes from 10,823 to 10,208 lines.

ADR anchors (ruling 6353073966 item 2)

  • node scripts/check-adr-symbol-anchors.mjs: exit 0 at base 21a25dd9f1 and exit 0 at head, both with 2383 anchors across 142 records resolve — 467 symbol (440 declaration, 27 literal), 1888 file-level, 28 cross-repo, 6 exempt, 18 continuation. CI runs this gate (lint.yml), but dispatch-gates --commands does not derive it for this change set ([finding] dispatch-gates derives the symbol-anchor sweeps only from their corpus paths, so a change that removes an anchored symbol from a TARGET file passes every local gate and reds CI #21697), so it is run by hand here.
  • pnpm check:adr-anchors: exit 0, 77 anchored files (76 at base, plus the new entry). The new entry is scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin-cbp-write-gate.ts.json. It carries ADR-0055 and the §1 invariant paragraph of the security-plugin.ts entry, verbatim, as cut 2's entry does. Its "writes throw MasterDetailRelationMissingError" half is the gate's relation leg, which now lives here. The moved comments also cite ADR-0066 and ADR-0090. Like the security-plugin.ts entry, which lists four of the many ADRs that file cites, the new entry does not list them, and the gate passes. The security-plugin.ts entry is unchanged, because the methods, their docblocks and the read half remain in that file. docs/adr/** is not touched.
  • The anchor map covers every ADR symbol anchor naming security-plugin.ts: 28 anchors, 20 symbols, 10 ADRs. It was measured with extractAnchors and symbolSegmentResolution from scripts/symbol-anchors.mjs, which is measurement, not a new gate. It was read on 21a25dd9f1 BEFORE the cut, with the five ranges blanked: no anchor changed class. Every anchor resolves at head in security-plugin.ts with its base class.

The table gives each anchor's class at base and head, the code lines it resolves on, and the member each line sits in. The member is shown after the line number. Every anchor stays in security-plugin.ts.

anchor cited at base class · lines (member) head class · lines (member)
#CBP_MAX_CHAIN_DEPTH ADR-0055:135 declaration · :635 (top-level) declaration · :643 (top-level, now exported)
#SecurityPlugin ADR-0049:49, ADR-0138:248, ADR-0138:265 declaration · :1377 (class) declaration · :1332 (class)
#SecurityPlugin.getMetadataReadableFields ADR-0138:153 declaration · :2526 (start), :6119 (method) declaration · :2481, :6074
#SecurityPlugin.resolvePermissionSetsForContextUnmemoized ADR-0138:427 declaration · :7030 (method) declaration · :6985 (method)
#assertControlledByParentWrite ADR-0055:125 declaration · :9525 (method) declaration · :9465 (method shell)
#assertMasterRowEditable ADR-0055:125 declaration · :10003 (method) declaration · :9591 (method shell)
#collectRLSPolicies ADR-0105:93 declaration · :10260 (method) declaration · :9645 (method)
#computeControlledByParentFilter ADR-0055:5, ADR-0055:123, ADR-0139:246 declaration · :9071 (method) declaration · :9026 (method)
#computeDelegatedReadFilter ADR-0139:223 declaration · :5997 (method) declaration · :5952 (method)
#computeRlsFilter ADR-0055:24 declaration · :5249 (explainAccessForCaller), :7652 (method) declaration · :5204, :7607
#controlled_by_parent ADR-0055:58 literal · :9080 (computeControlledByParentFilter), :9146 (controlledByParentChainRefusal) literal · :9035, :9101
#getReadFilter ADR-0096:181, :182, :183, :199, ADR-0139:222 declaration · :2289 (start), :5875 (method) declaration · :2244, :5830
#hasPlatformAdminPosture ADR-0099:317 declaration · :7679 (method) declaration · :7634 (method)
#isPrincipalLessContext ADR-0138:152 declaration · :489 (top-level) declaration · :497 (top-level)
#manage_metadata ADR-0099:68 literal · :238 (PLATFORM_ADMIN_ONLY_CAPABILITIES) literal · :246
#nav_permission_sets ADR-0126:363 literal · :1782 (init) literal · :1737
#refuseNoActiveOrganization ADR-0123:4 declaration · :8492 (local in composeWriteTenantWall) declaration · :8447
#rlsFilter ADR-0055:24 declaration · :4352 (local in start) declaration · :4307
#sharingModel ADR-0055:26 declaration · :9079 (local in computeControlledByParentFilter) declaration · :9034
#writeCheckPolicies ADR-0066:112 declaration · :1272 (top-level) declaration · :1227 (top-level)

No resolving site moves. The two anchors that name a moved method resolve on its declaration, which stays as a shell. #collectRLSPolicies and #computeRlsFilter are read by assertMasterRowEditable, but they resolve on their own declarations, which do not move.

At head, four of these names appear on extra lines inside the stubs: assertControlledByParentWrite, assertMasterRowEditable, collectRLSPolicies and computeRlsFilter. Each is a key of a host literal (a line-start member key, so declaration-class) and a quoted member of the stub's Pick list (a literal). The class each anchor resolves with is the same at head as at base.

The three anchors ruling item 4 holds for #25316 (ADR-0055:26 #sharingModel, ADR-0055:58 #controlled_by_parent and ADR-0123:4 #refuseNoActiveOrganization) still resolve only inside computeControlledByParentFilter, controlledByParentChainRefusal and composeWriteTenantWall. This cut does not touch those bodies.

Readers

One docs anchor reads a moved line by path: row 9b of content/docs/permissions/system-context.mdx, re-pointed below. No test, script, gate or baseline does. (Corrected by the seat; the report 6355294898 carries the same correction.)

  • The census page, re-pointed in this PR. check-system-context-census exits 1 on the cut without it. The served check's elevation read, if (context?.isSystem) return { outcome: 'allow' };, is the first line of checkControlledByParentWrite's body, so it moved with the body. The census then finds that read in the new module with no row pointing at it (file-without-a-row). The page's row 9b still names security-plugin.ts#checkControlledByParentWrite, where the census no longer sees a read (anchor-is-not-a-read-site).
    • The fix is the second commit, d87954919b. Row 9b of content/docs/permissions/system-context.mdx now anchors packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts#checkControlledByParentWrite. That is a one-line change, and the row's prose and every other row are unchanged. The page is hand-written and not a governed surface. The claim's surface covers it: a docs anchor that reads a moved line by path moves in the same PR.
    • Base 21a25dd9f1 and head, compared with node scripts/isystem-census.mjs --json: the same 148 elevation read sites, the same symbols, receivers and text, once the one moved site's file is mapped back to security-plugin.ts. nonElevationReads, roleCounts and staleLedgerRows are identical. The gate then exits 0 at head with the same summary as base, except that the sites now span 70 files instead of 69. Its --self-test exits 0 too.
  • The other symbol-anchor gates all exit 0: check-scripts-symbol-anchors, check-spec-docblock-symbol-anchors, pnpm --filter @objectstack/spec check:liveness, and check-platform-checklist --changed-from origin/main. The last of these judged 6 anchors and lost 0. packages/spec/liveness/object.json anchors security-plugin.ts#requiredCapsForOperation, which stays.
  • Line citations: no security-plugin.ts:N citation in the repo points into a moved range. The moved ranges at 21a25dd9f1 are :649-:684, :732-:746 and the five method bodies between :9363 and :10230.
    • Head line numbers shift by +12 for base :120-:167 (the new import), by +8 for base :172-:648 (four error names left the errors.js import), by -29 for base :686-:731 and by -45 for base :747-:9362.
    • The citations :132, :303, :309, :1614, :1690, :1736, :3215, :3281, :3519 and :4344 all land in those shifted ranges. They are the same list cut 2 read, in CHANGELOG.md, docs/launch-readiness.md, docs/design/predicate-compilation-convergence.md, packages/objectql/src/engine.ts and packages/spec/liveness/object.json. Cut 2's report found every one of them already pointing at an unrelated line, so this cut does not make any of them wrong.
  • Docs Drift Check, re-derived with node scripts/docs-audit/affected-docs.mjs --json 21a25dd9f1 (advisory): 4 pages. system-context.mdx is the page re-pointed above. authorization.mdx, explain.mdx and rls.mdx are reached only through the string literal 'not_applicable' inside checkControlledByParentWrite. The literal moved unchanged and its behaviour is the same, so there is nothing to edit on those pages.
  • Census gates, run on base 21a25dd9f1 and head and compared:
    • Identical output: check-platform-object-tenancy-census, check-position-name-fold-loaders, check-registry-log-declared, check-rest-log-spy-declared, check-tenant-audit-census (237 write call sites at both), check:cross-package-test-inputs, check:driver-memory-census and check:query-options-erasure. The last of these reports the same counts at both, and security-plugin.ts is not a baseline key. The moved bodies carry no erasure site the baseline counts.
    • Only the scanned counts move, by the one new file:
      • check-undeclared-dep-imports: 3288 to 3289 files, and 2827 to 2831 specifiers. The 4 are the new module's package imports (@objectstack/core, @objectstack/spec/security, @objectstack/metadata-core and @objectstack/spec/contracts), all declared dependencies.
      • check:doc-authoring: 1411 to 1412 sources.
      • check:org-identifier: 3630 to 3631 files.
      • check:logger-receiver-detach: 3604 to 3606 files. The second extra file is apps/docs/.next/types/validator.ts, a gitignored local build output of the docs app, as at cut 2. It is not in the tree CI checks out.
    • check-system-context-census: compared above.
    • check-issue-citations judges a change set, so it has nothing to read at base. At head it judged 40 citations across 2 files, and all resolve.

Acceptance notes

Gates

All gates ran at the head, the tree of d87954919b, on base 21a25dd9f1. origin/main has not moved since that base.

  • pnpm --filter @objectstack/plugin-security test (vitest run --maxWorkers=3, under the verify lock): Test Files 210 passed (210), Tests 4403 passed, 45 skipped (4448). Base 21a25dd9f1 gives the same counts. VERDICT command-exit 0 on both. This ran on the cut commit c3e6ac12cd; the second commit changes only the docs page.
  • pnpm --filter @objectstack/plugin-security typecheck: exit 0. It runs tsc --noEmit, the scripts tsconfig, and check:test-typecheck.
  • eslint --no-inline-config over the two touched source files: exit 0.
  • pnpm --filter @objectstack/plugin-security build: exit 0. It produced the .d.ts and bundles measured above.
  • node scripts/check-adr-symbol-anchors.mjs exit 0, pnpm check:query-options-erasure exit 0, plus the three symbol-anchor gates under Readers. None of these is derived for this change set; each was run by hand.
  • dispatch-gates --commands: 92 commands on the 4 changed paths (62 for the cut alone; the docs page adds 30 docs-family gates). The --ran reconciliation shows 92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN. The zero is derived: all 92 recorded an exit code and none is 3.
  • Two docs-family gates first refused on a prerequisite, not a finding: pnpm --filter @objectstack/spec run check:docs exited 1 because the local packages/spec/json-schema tree was older than packages/spec/src, and pnpm --filter @objectstack/spec run check:skill-examples exited 3 because the local spec dist was stale. After pnpm --filter @objectstack/spec build (which left the tree clean), both exit 0. The list below records those re-runs.

Per command (exit code):

  • node scripts/check-ci-filter-parity.mjs 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-adoption.mjs exit 0
  • node scripts/check-comment-mask-adoption.mjs --self-test exit 0
  • node scripts/check-comment-mask-corpus.mjs exit 0
  • node scripts/check-doc-frontmatter.mjs exit 0
  • node scripts/check-doc-frontmatter.mjs --self-test 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-dts-emitted.mjs --self-test exit 0
  • node scripts/check-issue-citations.mjs exit 0
  • node scripts/check-keyed-text-bounds.mjs exit 0
  • node scripts/check-keyed-text-bounds.mjs --self-test exit 0
  • node scripts/check-platform-object-tenancy-census.mjs exit 0
  • node scripts/check-platform-object-tenancy-census.mjs --self-test exit 0
  • node scripts/check-plugin-teardown-shape.mjs exit 0
  • node scripts/check-plugin-teardown-shape.mjs --self-test exit 0
  • node scripts/check-position-name-fold-loaders.mjs exit 0
  • node scripts/check-position-name-fold-loaders.mjs --self-test exit 0
  • node scripts/check-registry-log-declared.mjs exit 0
  • node scripts/check-registry-log-declared.mjs --self-test exit 0
  • node scripts/check-rest-log-spy-declared.mjs exit 0
  • node scripts/check-rest-log-spy-declared.mjs --self-test exit 0
  • node scripts/check-scripts-symbol-anchors.mjs exit 0
  • node scripts/check-scripts-symbol-anchors.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/check-system-context-census.mjs exit 0
  • node scripts/check-system-context-census.mjs --self-test exit 0
  • node scripts/check-tenant-audit-census.mjs exit 0
  • node scripts/check-tenant-audit-census.mjs --self-test exit 0
  • node scripts/check-undeclared-dep-imports.mjs exit 0
  • node scripts/check-undeclared-dep-imports.mjs --self-test exit 0
  • node scripts/docs-audit/check-affected-docs.mjs exit 0
  • node scripts/docs-audit/check-drift-comment.mjs 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:duration-unit-keys 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:adr-anchors exit 0
  • pnpm check:agent-test-spelling exit 0
  • pnpm check:bash32-floor exit 0
  • pnpm check:cli-command-ids 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: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:dts-closure exit 0
  • pnpm check:dual-build-cjs-loads exit 0
  • pnpm check:entry-guard exit 0
  • pnpm check:error-status-conformance exit 0
  • pnpm check:gitlink-declared exit 0
  • pnpm check:i18n exit 0
  • pnpm check:i18n-stale-fill exit 0
  • pnpm check:issue-citations exit 0
  • pnpm check:lean-entry-closure exit 0
  • pnpm check:logger-receiver-detach exit 0
  • pnpm check:merge-driver exit 0
  • pnpm check:nul-bytes exit 0
  • pnpm check:org-identifier exit 0
  • pnpm check:page-declaration-shape exit 0
  • pnpm check:parse-guard exit 0
  • pnpm check:pnpm-filter-targets exit 0
  • pnpm check:pnpm-flag-spellings exit 0
  • pnpm check:published-files 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:slot-lookup exit 0
  • pnpm check:sourcemap-no-sources-content exit 0
  • pnpm check:test-source-alias exit 0
  • pnpm check:tier-file-adoption exit 0
  • pnpm check:vendor-version-stamps exit 0
  • pnpm check:watch-hint-literal exit 0
  • pnpm check:query-options-erasure exit 0
  • node scripts/check-adr-symbol-anchors.mjs exit 0
  • node scripts/check-spec-docblock-symbol-anchors.mjs exit 0
  • pnpm --filter @objectstack/spec check:liveness exit 0
  • node scripts/check-platform-checklist.mjs --changed-from origin/main exit 0

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa

… write gate into security-plugin-cbp-write-gate.ts

Cut 3 of the security-plugin.ts split, move-only. The bodies of
checkControlledByParentWrite, assertControlledByParentWrite,
assertControlledByParentChainEditable, controlledByParentPredicateWriteMasters
and assertMasterRowEditable move unedited to a new module beside it, with
MASTER_EDIT_DENIAL_LEG, controlledByParentWriteOutcomeOf, writesMasterReference
and the error imports only they read. Each method keeps its name, signature,
docblock and position and hands its body one Pick-typed host (ql and
permissionEvaluator as getters, methods as late-bound closures); the stubs
drop async and return the moved function's promise.

Five module-level values the bodies read stay in security-plugin.ts and are
exported for the module's call-time import: CBP_MAX_CHAIN_DEPTH (ADR-0055
symbol anchor), requiredCapsForOperation (spec liveness anchor), and the
three predicate-write helpers readableRowsScopeForPredicateWrite shares.
index.ts re-exports only SecurityPlugin, so the package surface is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa
…-detail write check at the module its body moved to

The served check's elevation read (`context?.isSystem`, the write path's own
first exit) now lives in `security-plugin-cbp-write-gate.ts`, inside
`checkControlledByParentWrite`. Row 9b's anchor follows it; the row's prose,
and every other row, is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 23, 2026
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 01:11
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx (via not_applicable (literal, a string literal in checkControlledByParentWrite))
  • content/docs/permissions/explain.mdx (via not_applicable (literal, a string literal in checkControlledByParentWrite))
  • content/docs/permissions/rls.mdx (via not_applicable (literal, a string literal in checkControlledByParentWrite))
  • content/docs/permissions/system-context.mdx (via checkControlledByParentWrite (symbol, a method of class SecurityPlugin; a top-level function))
What this run could not see
  • 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 7b047b4483cbdc5423a879be5d604e09986678f6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 34a34a959934e0ef69cfac76bd18202c7a787a43 — the merge of head d87954919b0a3720ee318253b45263ea826e9733 into base 7b047b4483cbdc5423a879be5d604e09986678f6, 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 34a34a959934e0ef69cfac76bd18202c7a787a43 && git checkout 34a34a959934e0ef69cfac76bd18202c7a787a43
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7b047b4483cbdc5423a879be5d604e09986678f6 d87954919b0a3720ee318253b45263ea826e9733 && git checkout -B drift-repro 7b047b4483cbdc5423a879be5d604e09986678f6 && git merge --no-ff d87954919b0a3720ee318253b45263ea826e9733

node scripts/docs-audit/affected-docs.mjs --json 7b047b4483cbdc5423a879be5d604e09986678f6

⚠️ 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 7b047b4483cbdc5423a879be5d604e09986678f6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T01:42Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25223 cut 3 (claim 6354388643, Ruling-ref: 6353073966; os-dev-report 6355294898; card session branch claude/project-thread-3fzya4) · reviewed head d87954919b0a3720ee318253b45263ea826e9733 · merge base 21a25dd9f1

  • Shape: the bodies of the five async methods of the ADR-0055 §3 controlled-by-parent write gate move into security-plugin-cbp-write-gate.ts. Those methods are checkControlledByParentWrite, assertControlledByParentWrite, assertControlledByParentChainEditable, controlledByParentPredicateWriteMasters and assertMasterRowEditable. With them go the three module-level declarations only they read and the four error-class imports only they use. The class keeps each method shell (shape B, ql / permissionEvaluator as getters). Per ruling item 4, computeControlledByParentFilter, controlledByParentChainRefusal and composeWriteTenantWall are untouched; they wait for docs/adr: three security-plugin.ts symbol anchors (ADR-0055:26, ADR-0055:58, ADR-0123:4) resolve only on tokens inside bodies the #25223 split moves; repoint them to method declarations the class keeps #25314 (PR docs(adr): repoint three security-plugin.ts symbol anchors to declarations the class keeps #25316). 4 files, +931 / −684.
  • Declared up front (claim and body), read by the seat: security-plugin.ts adds export to five module-level values the new module imports by value. CBP_MAX_CHAIN_DEPTH is ADR-anchored and stays by ruling item 5. requiredCapsForOperation is anchored by spec liveness/object.json. The other three are shared with readableRowsScopeForPredicateWrite, which stays. Each line is the base line plus export, read only at call time. index.ts re-exports only SecurityPlugin, so the published surface does not move. The .d.ts and export-list readings below confirm it.
  • Move proof (report): 5 of 5 bodies are AST-token identical with a failing negative control, 617 lines. --color-moved leaves 10 deleted lines unmoved: the five export additions and the five shells that drop async.
  • .d.ts, ruling 6348891160 item 1, re-run by the seat in its own worktree. turbo run build --filter=@objectstack/plugin-security ran at 21a25dd9f1 and at this head, and the seat ran the normaliser one-liner from the PR body. dist/index.d.ts and dist/index.d.mts are raw 803f5bfb7da0b00f8cd8d8f25172463d8dfb07d93fbdd8a9c6f4d4b7435e429a and normalised b6b5c1b7fa6fa188f6e2f7e4cca3816a246b570f20d708643d116012aa86fa77, the same at base and head. The raw diff is 0 lines. Seat's own positive control, on a different member from the report's: head line 2174 private checkControlledByParentWrite made public gives raw a75f793fbe851a79… and normalised 42e3ffa23f3de985…, both moved. The report's control, line 2308 assertControlledByParentWrite, also moved both hashes. Export lists: 63 names in ESM and CJS, base equals head (report).
  • ADR anchor reading, ruling item 2, re-run by the seat: check-adr-symbol-anchors exits 0 at base and head, both printing 2383 anchors … 467 symbol (440 declaration, 27 literal). The report's pre-cut map shows no anchor resolving only inside a moved body. #assertControlledByParentWrite and #assertMasterRowEditable (ADR-0055:125) resolve on the kept shells.
  • Docs reader: the served check's isSystem read moved with its body. Row 9b of content/docs/permissions/system-context.mdx is re-pointed to security-plugin-cbp-write-gate.ts#checkControlledByParentWrite (second commit, one line). Seat re-run on this head: check-system-context-census exits 0 (148 elevation read sites). The report's base/head isystem-census --json matches all 148 sites. The seat corrected the body's Readers sentence, which had said no docs anchor reads a moved line, through the relay, and appended the required footer.
  • Tests and gates (report): plugin-security 210 files, 4403 pass / 45 skipped, the same as base. dispatch-gates --ran 92 / 92, plus the hand-run anchor, liveness and erasure gates. CI on this head: 40 success and 14 expected skips, none failed or pending, mergeable clean.
  • Declarations: refactor(plugin-security), Clause-②: no, Metadata-protocol: none, skip-changeset. No contract-review record or dogfood battery is owed. Out-of-scope findings: none.
  • Landing: not governed (check-governed-merges --pr 25324: 1615 lines, under 3000). Ready + auto-merge through the relay. The card stays open and dispatched. The next cut is claimed afresh on the merged file.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 01:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 01:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 3e51491 Oct 23, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-3fzya4 branch October 23, 2026 02:14
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/xl 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.

2 participants