Skip to content

refactor(plugin-security): move the rest of the tenant write wall and the context prologue out of security-plugin.ts (#25223 cut 6) - #25396

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

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-3fzya4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

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

Cut 6 of the security-plugin.ts split: the rest of step 3.7's Layer 0 tenant write wall, and the engine middleware's context prologue. The claim is 6358893564, amended by 6359105931 before the push (below). The bodies of four async methods move unedited out of the class:

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 bdf762c9bd, which carries cut 5's merge be318db654. It names no issue, so the claim comment is the identity record.

What stays in the class. Each moved method keeps its name, signature, docblock and position, less async. Three thin adapters of the wall stay whole: computeWriteTenantCheckFilter, computeWriteTenantWall and raiseWriteWallRefusal. Each body is shorter than its stub would be. The middleware, canWriteObject and every other caller still call the methods.

Module-level declarations.

  • Two move to the wall module, as they stood, docblocks included: callerHasOrganizationScope and carriesActiveOrganization. Only the moved bodies read them: the pre-test reads the first, and the first and cut 5's body read the second. carriesActiveOrganization loses the export cut 5 gave it, so the wall module no longer imports anything back from security-plugin.ts for it.
  • Two move to the prologue module: principalLessDenial, the ADR-0096 D5 refusal only the prologue throws, docblock included, and the @objectstack/core import of GuestOrganizationUnresolvedError and resolveGuestOrganization, comment included, because only the guest refusal reads it. principalLessDenial gains export there.
  • Three stay and gain export. Each module imports what it reads by value, as cuts 3 and 5 import theirs.
    • isPrincipalLessContext: ADR-0138:152 anchors it in security-plugin.ts, and the probes and the row scope read it there.
    • isGuestEnvelope: the middleware's owner-write step and the set resolution read it.
    • isTenantWallDenial: tenant-layer.ts names it as living in security-plugin.ts.
  • isPrincipalLessContext's docblock names the refusal with {@link principalLessDenial}. security-plugin.ts imports that name type-only from the prologue module, so the link still resolves. A type-only import emits nothing and has no runtime effect; typecheck and lint pass with it.
  • Why the refusal moves rather than staying exported (the claim amendment). The claim had principalLessDenial staying in security-plugin.ts with export. That was measured on the first head build: its return type makes the file's declarations import errors.ts, and the published dist/index.d.ts then declares PermissionDeniedError and isPermissionDeniedError at the top instead of after the class. That is a raw diff of 102 lines, one 51-line block moved with its text unchanged, and ruling 6348891160 admits union-order swaps only. With the refusal moved, the head .d.ts is byte-identical to base.
  • Each value import is read only inside a function body, at call time, never while either module evaluates. index.ts still exports only SecurityPlugin, so the published surface does not move (measured below).
  • WriteTenantWall, WriteWallRefusal and WriteWallRowVerdict keep their one declaration. The method signatures, raiseWriteWallRefusal and start() read them. The wall module names all three through the class's own signatures, as cuts 2 to 5 name theirs.
  • The postureUsesUnionScope name leaves the @objectstack/spec/security import of security-plugin.ts, because no code left there reads it. The wall module already imports it.

Two readers outside the moved code follow the bodies.

  • content/docs/permissions/system-context.mdx, row 6c. Its anchor is security-plugin.ts#writeWallVerdict, the census site of the body's context.isSystem read. check-system-context-census keys each elevation read by file and enclosing symbol, so with the body moved and the anchor unchanged the census finds a file with no row. The anchor now names security-plugin-write-tenant-wall.ts#writeWallVerdict. Only the path changed. The census lists the same sites at base and head, with the one site's file mapped back (below).
  • packages/plugins/plugin-security/src/delegated-organization-membership.test.ts, the plugin-security: delegation probes and diagnostics (canReadObject / canWriteObject / canExport, field mask, explain) answer for an on-behalf-of context the operation prologue now refuses (PR #25101) #25107 enumeration pin. It counts, per source file, the delegator resolutions and the organization-refusal asks, and lists the files that resolve one. The prologue's one resolution and its one ask now sit in the new module. The expected list gains security-plugin-operation-principals.ts, and the title says three files instead of two. The rule it pins, asks equal resolutions in every file, is unchanged, and it holds at head: the new module counts 1 and 1, security-plugin.ts 7 and 7.

The published .d.ts does not change

Under the series bar (ruling 6348891160 item 1), measured on base bdf762c9bd 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: af5dee1bebafa0376a876d1e7b247c878ea42ac35c7336a6d3c12a7953143156. The raw diff is 0 lines, so no union order moved either.
  • Normalised sha256, all four the same: 431e7f5097ac1c220bc840188aef02105bc0b16e845e976dd562d02cc79be8bd.
  • The hashes differ from cut 5's report because origin/main moved between cut 5's base and this one. Base and head here are both built against bdf762c9bd's dependencies, rebuilt on that commit first.
  • The first head build of this cut did not meet the bar, and the claim amendment 6359105931 records why. With principalLessDenial kept in security-plugin.ts and exported, the head .d.ts declared PermissionDeniedError and isPermissionDeniedError at the top instead of after the class: a raw diff of 102 lines, one 51-line block moved with its text unchanged. The refusal moved to the prologue module instead, and the head above is the tree after that change.
  • 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 1813 (private organizationWallRefusal;):
    • Made public (organizationWallRefusal;): raw 648873d54d4f3adf15cdbdd199ae7fa25bc46a30230249cebfe5e5bb812c29d1, normalised 847afa0caa5b4756a02a9e7833a0a878783a41187ac45e2e6aea5600a2b35fcf.
    • Renamed (private organizationWallRefusalRenamed;): raw 7a1e24b3874a47fb0a2a54f368917c6db961f2fcde858a11145e55cf7c9e423d, normalised f76ae5f05b7ca709d90f94a15277906f2862240f3b923c61707dcd703a632bec.
    • Both differ from the real hashes, so the comparison does see a visibility change and a rename. The first head build above is a third, unplanned control: the comparison saw one declaration move.
  • writeWallVerdict is public, so the .d.ts carries its full signature. It is the same line at base and head (line 1874 of both): the four parameters object, operation, context and rows, and the promise of one verdict per row or null.
  • Export tables: the ESM and CJS bundles each export the same 66 names at base and head. The lists were loaded at runtime (require and import() of the built bundles), not parsed. The sorted name list hashes 83576430f83cf60cecbcc5c0e80214f16c626a8e66f700294fc6a44361423116 on all four. The count differs from cut 5's 63 because origin/main added exports between the two bases; it is the same at this base and head. The new exports of security-plugin.ts (isGuestEnvelope, isPrincipalLessContext, isTenantWallDenial) do not reach the entry.
  • The bundle JS bytes differ (index.js fe2eebf9 to 77ef1113, index.mjs 254fc57b to f604cc07), because code moved within them.

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

What moved

The base line numbers are each method's signature and closing brace at bdf762c9bd. The last column lists the members the body reads, which is exactly what its host carries.

method to base lines host members
organizationWallRefusal (private, async) wall :8471-:8493 computeWriteTenantCheckFilter, orgScopingEnabled, tenancyPosture
writeWallVerdict (public, async) wall :8580-:8612 composeWriteTenantWall, resolveOperationPrincipals
resolveOperationPrincipals (private, async) prologue :8953-:9012 logger, ql, refuseGuestWithoutOrganization, resolvePermissionSetsForContext, tenancyPosture
refuseGuestWithoutOrganization (private, async) prologue :9038-:9062 guestOrganizationRefusalWarned, logger, ql, tenancyPosture

Each stub drops async and returns the moved function's promise, as cuts 1, 3 and 5 did for their async methods. The .d.ts emits no async, so this does not reach it. That includes writeWallVerdict, the one public method here, whose full signature the .d.ts carries.

The shape (#23050 shape B, as cuts 1 to 5 applied it)

  • The class keeps each method with its name, signature, docblock and position, because tsup emits members with their JSDoc into the .d.ts. Each body is now one host literal plus fn.call(host, …), marked [#25223 cut 6].
  • WriteTenantWallHost gains the members the two new wall bodies read (composeWriteTenantWall, resolveOperationPrincipals), and OperationPrincipalsHost is new. Each types every member as the class member itself (SecurityPlugin['m']). Each moved function types its this as the Pick of its host over exactly the members its body reads, and the stub annotates its literal with the same Pick. There is no cast, and no member becomes public or protected.
  • 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 body reaches:
    • the pre-test still asks the read side's Layer 0 through computeWriteTenantCheckFilter;
    • the preview still runs the prologue and the wall through the plugin;
    • the prologue still asks the plugin's refuseGuestWithoutOrganization and resolvePermissionSetsForContext.
      No moved body calls a function-valued field, so no receiver changes.
  • Fields follow the rule cut 1 set, on each field's own declaration.
    • guestOrganizationRefusalWarned is declared readonly and goes by value, so the guest refusal records each reason it warned for in the plugin's own set, once per reason as before.
    • ql, logger and tenancyPosture are not readonly (the plugin assigns each after construction), and orgScopingEnabled is the class's own getter. All four go as getters, so each read happens when the body makes it.
    • The host declares every field readonly, so a moved body cannot assign a member through it; the typecheck passes, so none does.
  • Nothing re-exports either module, so the package entry does not move.

Proof of move-only

  • Bodies, by AST token: each moved function's body has the same TypeScript token sequence as the base method body, and each of its 117 lines is the base line with two leading spaces removed. Parameters, the return type and async match. Two negative controls fail the check:
    • in the prologue module, the delegation branch's guard on the resolved set count changed from greater than zero to at least zero;
    • in the wall module, context.isSystem || !context.userId changed to context.isSystem && !context.userId in the preview's first guard.
  • Cut 5's function: composeWriteTenantWall in the wall module is byte-identical to base, its pointer docblock included.
  • Class side: each moved method keeps its docblock and signature, less async. Every other class member, 122 named members plus the constructor, is byte-identical to base. That includes the three adapters, canWriteObject, checkControlledByParentWrite, resolvePermissionSetsForContext and start(). The class has 127 members at base and at head.
  • Top level of security-plugin.ts: 96 of the 105 base top-level statements other than the class are byte-identical to base, WriteTenantWall, WriteWallRefusal and WriteWallRowVerdict included. The nine that differ:
    • three are the base text with export before function: isGuestEnvelope, isPrincipalLessContext, isTenantWallDenial;
    • four moved, with their text checked in the module: callerHasOrganizationScope (identical), carriesActiveOrganization (identical less the export cut 5 added), principalLessDenial (identical plus export ) and the @objectstack/core import (identical, comment included);
    • the @objectstack/spec/security import, which loses postureUsesUnionScope;
    • cut 5's import of the wall module, which now names cut 6's two functions too. The one new statement is the import of the prologue module, with the type-only principalLessDenial.
  • git's own view (git diff --color-moved=plain --color-moved-ws=allow-indentation-change bdf762c9bd HEAD over the three source files): 183 of 240 deleted lines and 211 of 448 added lines are detected as moved.
    • Deleted, not moved (57). In security-plugin.ts (14): cut 5's import of the wall module with its comment (4 lines), the postureUsesUnionScope, import line, the five declaration lines that come back changed (isGuestEnvelope, isPrincipalLessContext and isTenantWallDenial with export; principalLessDenial with export, in the prologue module; carriesActiveOrganization without export, in the wall module) and the four method signature lines, which come back without async. In the wall module (43): cut 5's module docblock and its import of carriesActiveOrganization, rewritten for two cuts, and two lines of the host docblock, which now speak of several functions.
    • Added, not moved: those lines' replacements, the four stubs and the two import blocks with their comments. In the wall module: the new docblock and imports, the third type alias and the two new host members. In the prologue module: the license header, the docblock, the imports, OperationPrincipalsHost, the pointer docblocks and the rewritten signature heads. The rest are lines git does not pair with a moved block, such as blanks and closing braces.
  • Size: 694 changed lines (+451 / -243, 5 files), under the 3000-line human-merge threshold. security-plugin.ts goes from 9,992 to 9,863 lines, and the wall module from 437 to 567. The prologue module is 207 lines.

ADR anchors (ruling 6353073966 item 2)

  • node scripts/check-adr-symbol-anchors.mjs: exit 0 at base bdf762c9bd and exit 0 at head, both with 2383 anchors across 142 records resolve — 466 symbol (440 declaration, 26 literal), 1889 file-level, 28 cross-repo, 6 exempt, 18 continuation, the same counts as cut 5. 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 at base and at head, 79 anchored files at both (no entry added or changed). One count moves, and it is disclosed here rather than answered with a new anchor entry. The decision-letter citations on the anchored surface go from 2014 to 2010. Seven ADR-NNNN Dk citations move with the prologue's text into the prologue module, which has no anchor entry: ADR-0090 D10 four times in the delegation branch, ADR-0138 D4 twice (the moved import comment and the guest refusal's warning) and ADR-0096 D5 once (principalLessDenial's docblock). The two new import comments in security-plugin.ts add three. Every citation still resolves as a citation: the total goes from 52091 to 52100, across 6619 to 6620 files. Writing an invariant paragraph for the prologue module would be new prose about the prologue, outside a move-only cut. [maintainer] refactor(plugin-auth): split src/auth-manager.ts (10,338 lines, +1,867 in 30 days) along its AuthManager seams, one move-only slice per PR #25224 cut 5 disclosed the same kind of shift the same way. The wall module is anchored, so the six citations that move into it (the pre-test's comment and the two lifted docblocks) stay on the anchored surface.
  • scripts/adr-anchors/ is unchanged. The wall module's entry (cut 5) carries ADR-0123 and the ADR-0123 D2 paragraph, which describes the no-active-organization pre-test; organizationWallRefusal is that pre-test, and it now lives in the module the entry names. No paragraph of the security-plugin.ts entry (ADR-0055, ADR-0106, ADR-0123 D2, ADR-0138 D1) describes the prologue or the guest refusal, so the prologue module gets no entry, as cut 1's module has none. The security-plugin.ts entry is unchanged. docs/adr/** is not touched.
  • The anchor map covers every ADR symbol anchor naming security-plugin.ts: 27 anchors, 18 symbols. It was measured with extractAnchors and symbolSegmentResolution from scripts/symbol-anchors.mjs, which is measurement, not a new gate. It was read on bdf762c9bd BEFORE the cut, with the four bodies 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 line it resolves on, and the member that line sits in. Every anchor stays in security-plugin.ts.

anchor cited at base class · line (member) head class · line
#CBP_MAX_CHAIN_DEPTH ADR-0055:135 declaration · :663 (top-level) declaration · :658
#SecurityPlugin ADR-0049:49, ADR-0138:248, ADR-0138:265 declaration · :1340 (class) declaration · :1290
#SecurityPlugin.getMetadataReadableFields ADR-0138:153 declaration · :2516 (start), :6135 (method) declaration · :2466, :6085
#SecurityPlugin.resolvePermissionSetsForContextUnmemoized ADR-0138:427 declaration · :7046 (method) declaration · :6996
#assertControlledByParentWrite ADR-0055:58, ADR-0055:125 declaration · :9249 (method shell) declaration · :9120
#assertMasterRowEditable ADR-0055:125 declaration · :9375 (method shell) declaration · :9246
#collectRLSPolicies ADR-0105:93 declaration · :9429 (method) declaration · :9300
#composeWriteTenantWall ADR-0123:4 declaration · :8513 (method shell) declaration · :8458 (method shell), and :8533 (see below)
#computeControlledByParentFilter ADR-0055:5, ADR-0055:123, ADR-0139:246 declaration · :8841 (method shell) declaration · :8766
#computeDelegatedReadFilter ADR-0139:223 declaration · :6013 (method) declaration · :5963
#computeRlsFilter ADR-0055:24 declaration · :5265 (explainAccessForCaller), :7740 (method) declaration · :5215, :7690
#getReadFilter ADR-0096:181, :182, :183, :199, ADR-0139:222 declaration · :2275 (start), :5891 (method) declaration · :2225, :5841
#hasPlatformAdminPosture ADR-0099:317 declaration · :7767 (method) declaration · :7717
#isPrincipalLessContext ADR-0138:152 declaration · :517 (top-level) declaration · :531 (top-level, now export function)
#manage_metadata ADR-0099:68 literal · :266 (PLATFORM_ADMIN_ONLY_CAPABILITIES) literal · :280
#nav_permission_sets ADR-0126:363 literal · :1768 (init) literal · :1718
#rlsFilter ADR-0055:24 declaration · :4342 (local in start) declaration · :4292
#writeCheckPolicies ADR-0066:112 declaration · :1235 (top-level) declaration · :1185

No resolving site moves out of the file. One anchor gains a site, and its class does not change. ADR-0123:4 #composeWriteTenantWall still resolves on the method declaration, which stays as a shell. At head the name also starts a line in writeWallVerdict's stub, as its host-literal key, the closure that calls the method on the plugin (:8533), and appears quoted in that stub's Pick (:8532). Cut 3's stub added the same kind of site for #assertControlledByParentWrite (checkControlledByParentWrite's stub, :8980 here). The anchor is judged by its class, which is declaration at base and at head. ADR-0138:152 #isPrincipalLessContext resolves on the declaration, which stays and gains export.

Readers

Two readers of the moved lines exist outside the moved code, the census row and the enumeration pin above, and both are re-pointed in this PR. No other test, script, gate, baseline or docs anchor reads the moved lines by path.

  • The system-context census. check-system-context-census exits 0 at base and at head. Both print 148 elevation read sites in 22 packages … the page cites 140 symbol(s) against 140 required, over 163 anchors. The only difference is the file count, 70 to 71, because the one read site of writeWallVerdict now sits in the wall module. node scripts/isystem-census.mjs --json gives the same 148 sites at base and head, compared without line numbers and with the two new modules mapped back to security-plugin.ts: 0 differences. The role counts are the same (read 153, declaration 38, key 438, other 9), and only the corpus and scanned file counts move, by one.
  • The tenant-audit census. The moved bodies hold no engine write: the prologue reads through resolveDelegatorContext and resolveGuestOrganization, which take ql as an argument. node scripts/tenant-audit-census.mjs --json gives the same 237 write call sites at base and head, compared the same way: 0 differences, the same totals, 0 unresolved, 0 unledgered. Only scannedSources moves, 715 to 716, plus the measured-at ref. check-tenant-audit-census prints the same line at both.
  • The plugin-security: delegation probes and diagnostics (canReadObject / canWriteObject / canExport, field mask, explain) answer for an on-behalf-of context the operation prologue now refuses (PR #25101) #25107 enumeration pin is described above. It is part of the plugin-security suite, which passes at head with the same counts as base.
  • packages/core/src/security/row-serving-door-exposure.pin.test.ts classifies every caller of the read admission. The moved bodies call no admission, so the new module needs no row. Run on its own: 3 of 3 pass.
  • The platform checklist. check-platform-checklist exits 0 at base and head with the same output. No checklist citation names a moved symbol or a local inside a moved body.
  • Readers by name, not by path. These were read and none needs a change:
    • packages/core/src/security/guest-organization.ts names SecurityPlugin.resolveOperationPrincipals, and explain-engine.ts and delegated-organization-membership.test.ts name resolveOperationPrincipals. The method still exists under that name.
    • tenant-layer.ts names isTenantWallDenial in security-plugin.ts, and explain-engine.ts names isTenantWallDenial. It stays there.
    • write-wall-verdict-equivalence.test.ts calls plugin.writeWallVerdict(…), and packages/objectql names it in comments. The public method still exists with the same signature.
    • Docblocks in security-plugin.ts name {@link resolveOperationPrincipals}, {@link composeWriteTenantWall} and {@link canWriteObject}. The methods still exist.
  • Line citations: no security-plugin.ts:N citation in the repo points into the moved ranges (base :8471-:8493, :8580-:8612, :8953-:9012, :9038-:9062) or at the moved module-level declarations. The citations found are the list cuts 2 to 5 read (docs/launch-readiness.md :309, packages/objectql/src/engine.ts :1614, :1690, :1736, spec liveness :3281 and the rest). Each already pointed at an unrelated line before cut 2, so this cut does not make any of them wrong.
  • Docs Drift Check, re-derived with node scripts/docs-audit/affected-docs.mjs --json bdf762c9bd (advisory): 1 page, content/docs/permissions/system-context.mdx, through writeWallVerdict. That is the census row this PR re-points.
  • Census gates, run on base bdf762c9bd and head and compared:
    • Identical output: check-adr-symbol-anchors, check-platform-checklist, check-platform-object-tenancy-census, check-registry-log-declared, check-rest-log-spy-declared, check-tenant-audit-census, check:cross-package-test-inputs, check:driver-memory-census and check:query-options-erasure. The last reports the same counts at both, and security-plugin.ts is not a baseline key.
    • Only scanned counts move:
      • check-system-context-census: 70 to 71 files, as above.
      • check-undeclared-dep-imports: 3301 to 3302 files, and 2843 to 2844 specifiers. The prologue module imports @objectstack/spec/security and @objectstack/core, and security-plugin.ts drops the one @objectstack/core import that moved. Both are declared dependencies.
      • check:org-identifier: 3643 to 3644 files.
      • check:doc-authoring: 1420 to 1421 parsed sources. Its clean-file count goes from 424 to 427 because the head worktree holds three gitignored generated pages that the base worktree does not (content/docs/protocol-upgrade/17.md, 18.md and index.md). CI's checkout does not have them.
      • check:logger-receiver-detach: 3617 to 3619 files: the new module, and one local build output, as at cuts 2 to 5.
      • check-position-name-fold-loaders: 12331 to 12335 files, with the same 8 references (6 test, 2 declared instrument, 0 loader). It walks the working tree: the new module plus local build outputs that exist only in the head worktree.
    • check-issue-citations judges a change set, so it has nothing to read at base. At head it judged 13 citations across 3 files, and all resolve.

Acceptance notes

  • Nothing outside this cut is touched: no other section of the class, no behaviour, no docs/adr/**, no scripts/adr-anchors/ entry. The one file outside the package is the census page, and only its row 6c path changes.
  • Serial check, also in the claim: of the 25 open PRs at claim time, none touched security-plugin.ts, the wall module, system-context.mdx, the delegated-organization test or the plugin-security anchor entries.
  • Since the local runs, origin/main gained 15 commits (to b22ab8d6fb). None touches this cut's files. On a scratch merge of this head onto that main, check-adr-symbol-anchors, check:adr-anchors, check-platform-checklist and check-system-context-census all exit 0. check:adr-anchors there counts 80 anchored files and 2013 decision-letter citations, against 80 and 2017 on that main alone (main added an anchored file), so the shift this cut makes is the same four.
  • What is left of the tenant write wall in security-plugin.ts after this cut: the three adapters (computeWriteTenantCheckFilter, computeWriteTenantWall, raiseWriteWallRefusal), the four method shells, the three type declarations and the middleware's step 3.7.

Gates

All gates ran at the head, the tree of 0177b21a24, on base bdf762c9bd.

  • pnpm --filter @objectstack/plugin-security test (vitest run --maxWorkers=3, under the verify lock): Test Files 214 passed (214), Tests 4465 passed, 45 skipped (4510). Base bdf762c9bd gives the same counts. VERDICT command-exit 0 on both. The first head commit (before the claim amendment) gave the same counts too.
  • 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 three touched source files and the touched test: exit 0.
  • pnpm --filter @objectstack/plugin-security build: exit 0. It produced the .d.ts and bundles measured above. The package's dependencies were rebuilt on bdf762c9bd first.
  • packages/core/src/security/row-serving-door-exposure.pin.test.ts: 3 of 3 pass.
  • node scripts/check-adr-symbol-anchors.mjs, pnpm check:adr-anchors, pnpm check:platform-checklist (and its script), check-scripts-symbol-anchors (and --self-test), and the two census --json reads. None of these is derived for this change set; each was run by hand, and each exit is listed below.
  • dispatch-gates --commands: 88 commands. The census page adds the docs families, as it did at cut 3. The --ran reconciliation shows 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN. The zero is derived: all 88 recorded an exit code and none is 3.

Per command (exit code), the 88 derived first and the 8 hand-run after:

  • 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-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: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:engine-double-contract 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:objectql-double-limit exit 0
  • pnpm check:org-identifier exit 0
  • pnpm check:page-declaration-shape exit 0
  • pnpm check:published-files exit 0
  • pnpm check:published-readme-links exit 0
  • pnpm check:query-options-erasure 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:type-check-coverage exit 0
  • pnpm check:type-check-debt exit 0
  • pnpm check:vendor-version-stamps exit 0
  • pnpm check:watch-hint-literal exit 0
  • pnpm check:where-matcher exit 0
  • node scripts/check-adr-symbol-anchors.mjs exit 0
  • node scripts/check-platform-checklist.mjs exit 0
  • node scripts/check-scripts-symbol-anchors.mjs exit 0
  • node scripts/check-scripts-symbol-anchors.mjs --self-test exit 0
  • node scripts/isystem-census.mjs --json exit 0
  • node scripts/tenant-audit-census.mjs --json exit 0
  • pnpm check:adr-anchors exit 0
  • pnpm check:platform-checklist exit 0

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa

… the context prologue out of security-plugin.ts

The bodies of organizationWallRefusal (the ADR-0123 D2 scope pre-test) and
writeWallVerdict (the wall's per-row verdict for the write preview) join
composeWriteTenantWall in security-plugin-write-tenant-wall.ts, with the
two module-level declarations only they read, callerHasOrganizationScope
and carriesActiveOrganization. The bodies of resolveOperationPrincipals
(the middleware's context prologue: ADR-0096 D5, ADR-0090 D10) and
refuseGuestWithoutOrganization (ADR-0138 D4) move unedited to a new module,
security-plugin-operation-principals.ts, with the two things only they
read: the strict-mode refusal principalLessDenial and the @objectstack/core
import of the guest refusal. Each method keeps its name, signature,
docblock and position and hands its body one host literal of exactly the
members it reads. isPrincipalLessContext, isGuestEnvelope and
isTenantWallDenial stay and are exported and imported by value;
principalLessDenial is imported back type-only for the docblock link that
names it. The system-context census row 6c anchor and the per-file
delegator-resolution pin follow the moved bodies. Move-only: no verdict
changes.

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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/system-context.mdx (via writeWallVerdict (symbol, a method of class SecurityPlugin; a top-level function), writeWallVerdict (literal, a string literal in WriteWallRowVerdict))
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 b22ab8d6fb3462bff05142efc6b6cc040c95f646 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json b22ab8d6fb3462bff05142efc6b6cc040c95f646

⚠️ 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 b22ab8d6fb3462bff05142efc6b6cc040c95f646 → 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-23T07:56Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25223 cut 6 (claim 6358893564, amended before the push by 6359105931; Ruling-ref: 6353073966; os-dev-report 6360202554; card session branch claude/project-thread-3fzya4) · reviewed head 0177b21a24872691eb7853eac63dfa0c6039678f · merge base bdf762c9bd

  • Shape: four async method bodies move unedited, in shape B as in cuts 1 to 5. 5 files, +451 / −243.
    • organizationWallRefusal and writeWallVerdict (public) join cut 5 in security-plugin-write-tenant-wall.ts.
    • resolveOperationPrincipals and refuseGuestWithoutOrganization go to the new security-plugin-operation-principals.ts.
    • Of the module-level declarations, callerHasOrganizationScope and carriesActiveOrganization move to the wall module. carriesActiveOrganization drops the export cut 5 gave it. principalLessDenial moves to the prologue module.
  • The amendment, read by the seat: the claim had principalLessDenial staying in security-plugin.ts and exported. Its PermissionDeniedError return type moved the errors block in the published .d.ts (102 raw diff lines). That is not a union swap, so ruling 6348891160 item 1 does not admit it. The card thread moved the helper instead, and security-plugin.ts imports it type-only so the docblock link still resolves. The seat agrees with this fix: the bar holds on this head (next item).
  • .d.ts, ruling 6348891160 item 1, re-run by the seat in its own worktree. turbo run build --filter=@objectstack/plugin-security ran at bdf762c9bd and at this head. dist/index.d.ts and dist/index.d.mts are raw af5dee1bebafa0376a876d1e7b247c878ea42ac35c7336a6d3c12a7953143156 and normalised 431e7f5097ac1c220bc840188aef02105bc0b16e845e976dd562d02cc79be8bd, the same at base and head. The raw diff is 0 lines.
    • Seat's positive control: head line 2065, private computeControlledByParentFilter, made public. Raw db0766b422337b6f… and normalised 6b80ff0ea75b06a7… both moved.
  • Readers re-pointed in this PR, read by the seat:
  • ADR anchors, re-run by the seat: check-adr-symbol-anchors exits 0 at both, 2383 … 466 symbol. check-adr-anchors exits 0 at both, and its decision-letter count reads 2014 at base and 2010 at head. Seven citations move into the unanchored prologue module, and three come in through import comments on the anchored side (as the claim discloses). The prologue module gets no anchor entry, for the same reason as [maintainer] refactor(plugin-auth): split src/auth-manager.ts (10,338 lines, +1,867 in 30 days) along its AuthManager seams, one move-only slice per PR #25224 cut 5 (6360020819): an entry would need new invariant text.
  • Tests and gates (report): plugin-security 214 files, 4465 pass / 45 skipped, the same as base. The token identity has two negative controls. dispatch-gates --ran 88 / 88, plus 8 run by hand. CI on this head: 36 success and 11 expected skips, none failed or pending. Mergeable clean, and git merge-tree onto today's main is clean.
  • Body: the attribution header, both footers and the os-sales reviewer are present.
  • 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 25396: 694 lines). Ready + auto-merge through the relay. The card stays open and dispatched. The next cut is the RLS filter layers.

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 skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants