Skip to content

refactor(plugin-security): move the everyone anchor, the explanation, the sharing verdicts and the authored-row write verdict out of security-plugin.ts (#25223 cut 10) - #25516

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 10 of the security-plugin.ts split covers row 8 of the series plan: the everyone anchor, the explanation, the sharing verdicts and the authored-row write verdict. The claim is 6369634056. The row has four seams, so this one PR moves the bodies of ten methods out of the class, unedited, into four new modules. Cut 9 split its row the same way, over three.

To packages/plugins/plugin-security/src/security-plugin-everyone-anchor.ts:

To packages/plugins/plugin-security/src/security-plugin-explain-access.ts:

To packages/plugins/plugin-security/src/security-plugin-sharing-verdicts.ts:

To packages/plugins/plugin-security/src/security-plugin-authored-row-write.ts:

The change is move-only, and the card stays open for start(). The branch is claude/project-thread-3fzya4, the harness-pinned branch for this session. It was restarted from origin/main at 05cc3ee953, which contains cut 9's merge dd124e9466. The branch name 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. Every caller still reaches the methods through the plugin:

  • the security service binding (explain, checkAuthoredRowWrite);
  • init() and kernel:ready;
  • the middleware;
  • the read scope;
  • the master gate.

Two members of the row stay whole, everyoneBaseline (6 body lines) and destroy (3). Neither body is longer than its stub would be: everyoneBaseline's stub would also be 6 lines.

Module-level declarations.

  • AUTHORED_ROW_WRITE_PROBE_CONTEXT is read only by checkAuthoredRowWrite's body. It moves with its docblock, unedited and still unexported, as cuts 1 to 4 and 9 moved declarations that only their bodies read. Its docblock's {@link SecurityPlugin.checkAuthoredRowWrite} resolves through the module's type import of the class.

  • absentUnderCallerRead and unreadableUnderCallerRead stay in security-plugin.ts, and each gains export and nothing else. The explanation reads both. absentUnderCallerRead is shared with the write path's step 2.7, which stays. unreadableUnderCallerRead is defined and documented beside it. Its docblock links to absentUnderCallerRead, and cut 8's security-plugin-addressed-writes.ts header names both as living in security-plugin.ts.

  • requiredCapsForOperation is already exported. The explanation imports it by value, as the object-admission module does.

  • Import names that only the moved bodies read leave security-plugin.ts. The modules import them from the same specifiers:

    • explainAccessForAsker and buildContextForUser (./explain-engine.js);
    • the whole ./builtin-positions.js import (declareEveryoneBaseline, registerBuiltinPositions);
    • readAcceptedEveryoneSets (./suggested-audience-bindings.js);
    • readDeclaredCapabilityContext (./declared-capability-context.js);
    • vetOrganizationClaim (@objectstack/core);
    • the type ExplainOperation (@objectstack/spec/security).

    No staying docblock links to any of them. Two staying docblocks name readAcceptedEveryoneSets and vetOrganizationClaim, in backticks only.

  • The unexported interface RlsFilterOptions stays where every gate reads it. The explanation and the sharing verdicts name it through the class's own signature, as the type of computeLayeredRlsFilter's fifth parameter less undefined, so the moved text names it exactly as before. Cut 9's modules name ObjectSecurityMeta the same way.

  • Each value import is read only inside a function body, at call time, and never while a module evaluates. index.ts still exports only what it did, so the published surface does not move (measured below).

Two readers outside the moved code read the moved lines by path. Both are re-pointed in this PR, as cut 9 re-pointed the same two.

  • content/docs/permissions/system-context.mdx, row 9. check-system-context-census keys each elevation read by file and enclosing symbol, and explainAccessForCaller's callerContext?.isSystem read now sits in the explain module.
    • A trial cut measured the page left untouched. The census reports 2 problems: the new file has no row, and the old anchor is no longer a read site.
    • The anchor keeps its symbol and changes only its path, from security-plugin.ts#explainAccessForCaller to security-plugin-explain-access.ts#explainAccessForCaller. No row's text changes.
  • 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. The explanation resolves a delegator and asks the organization check beside it. So the expected list gains security-plugin-explain-access.ts, and the title says seven files instead of six.
    • The rule it pins is unchanged: asks equal resolutions in every file. It holds at head: the explain module counts 1 and 1, and security-plugin.ts counts 1 and 1 (start(); it counted 2 and 2 at base).

No ADR anchor entry is added. No paragraph of the security-plugin.ts entry describes these bodies (see ADR anchors below).

One receiver changes, disclosed. resolveKernelService is a function-valued field. The explanation and three of the sharing verdicts call it through the host, so its this is now the host, not the plugin. Two facts keep every call answering as it did:

  • the only function ever assigned to the field is the arrow function start() assigns, which returns ctx.getService(name) and answers undefined when that throws, and it reads no this;
  • no test or other file assigns the field.

Cut 9's report said that no body it moved calls a function-valued field. This cut's bodies do, so it is stated here.

The published .d.ts does not change

Measured under the series bar (ruling 6348891160 item 1), on base 05cc3ee953 and on head 38392cb1b7. Each was a full turbo run build in its own worktree. A package build of the same trees before the claim gave the same bytes.

  • Raw sha256 of dist/index.d.ts and dist/index.d.mts is the same for all four files, base and head: 7d67966ce82e00a1531807b3b0008f16c489fc2c3199f9ad67078302eb2803da. The raw diff is 0 lines, so no union order moved either. These are the same bytes cut 9 measured, so neither cut 9's merge nor main's later commits moved this package's .d.ts.
  • Normalised sha256 is the same for all four: b53e9025f794e20debd2245afed5ceb1a0ad48109a2a21cc19232a562ccd9cb4.
  • security-plugin.ts gains two exports, absentUnderCallerRead and unreadableUnderCallerRead. Each takes one read callback and answers a boolean promise, so its signature names no type from another module, so the cut 6 trap does not arise. index.ts re-exports neither.
  • The normaliser, run from packages/plugins/plugin-security, 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, each a one-line edit on a copy of the head dist/index.d.ts:

    • line 557 (private resolveSharingReadFilter;) made public: raw 8c8fad488058d08a54e23b5410b539b7eecad71a0e749b0133a61280e146bdde, normalised f02f50957a57e31fd943e00fa0ef74113a2f0efdc4af8e3ee5b963c7e932a3c6;
    • line 447 renamed (private declareEveryoneRenamed;): raw 7af56294d477cdbc07b22567c87a110bbc2c36d37724cc47ed3652dc085a8ab4, normalised 44d50a2b10e166373f11f31ef5e046c1a05ab9f5613e9d6b762c03c7ca15ec88;
    • line 745, the public checkAuthoredRowWrite signature, with context?: any changed to context?: unknown: raw b75e990785a61245092adf8414acba79b6fe27f330ebd7555e9a808642802556, normalised 6b7331a568193fda94c1e45eabe85f5704d61c6ef99639bb197b7f11510b81b6.

    All three differ from the real hashes, so the comparison does see a visibility change, a rename and a change to a public signature.

  • The two public methods carry their full signatures and docblocks in the .d.ts: explainAccessForCaller (line 525) and checkAuthoredRowWrite (745). The eight private methods carry only their name and docblock: declareEveryone (447), declareBuiltinPositions (483), and the six sharing verdicts (557 to 676). Those lines are the same in both files.

  • Export tables: the ESM and CJS bundles each export the same 66 names at base and at head. The lists were loaded at runtime, with require and import() of the built bundles, not parsed. The sorted name list hashes to 83576430f83cf60cecbcc5c0e80214f16c626a8e66f700294fc6a44361423116 for all four, as at cuts 6 to 9.

  • The bundle JS bytes differ, because code moved within them: index.js goes from dfa83a54 to 78d8f0eb, and index.mjs from 18c4d919 to 5d290484. Every value the new modules import comes from a file inside this package or from a specifier security-plugin.ts already imports. So the set of external specifiers each bundle loads is the same at base and head: 16 distinct require specifiers in index.js and 19 distinct from specifiers in index.mjs.

  • Bundle order, disclosed: builtin-positions.ts, manifest.ts and objects/sys-audience-binding-suggestion.object.ts are now emitted just ahead of the four modules, where before they sat between security-plugin.ts's chunks. None of them imports security-plugin.ts, and their top level only declares constants.

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

What moved

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

method base lines module host members
declareEveryone (private, async) :4912-:4946 everyone-anchor deploymentAcceptance, epoch, everyoneBaseline, metadata, ql
declareBuiltinPositions (private) :4994-:5013 everyone-anchor everyoneBaseline
explainAccessForCaller (public, async) :5061-:5373 explain-access baselinePermissionSets, checkControlledByParentWrite, computeLayeredRlsFilter, computeReadPartialMaskRules, computeRlsFilter, delegatedAdminGate, foldFieldRequiredPermissions, getObjectSecurityMeta, permissionEvaluator, ql, readRowById, resolveKernelService, resolvePermissionSetsForContext, resolvePreImageFloorDrop, resolveSharingReadFilter, resolveWriteScopeForSharing, tenancyPosture
resolveSharingReadFilter (private, async) :5401-:5449 sharing-verdicts getObjectSecurityMeta, permissionEvaluator, resolveKernelService, resolvePermissionSetsForContext
resolveSharingCanEdit (private, async) :5468-:5493 sharing-verdicts logger, resolveKernelService, resolveWriteScopeForSharing
resolveWriteScopeForSharing (private, async) :5513-:5531 sharing-verdicts getObjectSecurityMeta, permissionEvaluator, resolvePermissionSetsForContext
resolveSharingWriteVerdict (private, async) :5563-:5593 sharing-verdicts logger, resolveKernelService, resolveWriteScopeForSharing
resolvePreImageFloorDrop (private, async) :5621-:5642 sharing-verdicts collectRLSPolicies, resolveSharingWriteVerdict
delegatorOwnWriteFloorOptions (private, async) :5667-:5684 sharing-verdicts resolvePreImageFloorDrop
checkAuthoredRowWrite (public, async) :5754-:5838 authored-row-write collectRLSPolicies, computeLayeredRlsFilter, logger, ql, resolvePermissionSetsForContext

Each async stub drops async and returns the moved function's promise, as cuts 1 and 3 to 9 did for their async methods. declareBuiltinPositions was synchronous, and its stub returns its function's void. Two of the ten are public, so the .d.ts carries their full signatures. A body move does not touch a signature, and those lines do not change (above).

The shape (#23050 shape B, as cuts 1 to 9 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 10].

  • EveryoneAnchorHost, ExplainAccessHost, SharingVerdictsHost and AuthoredRowWriteHost are 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. So a spy or test double set on the plugin is still the one the body reaches:

    • the everyone declarations still ask the plugin's baseline and deployment acceptance;
    • the explanation still asks the plugin's set resolution, RLS filters, sharing read filter, write-depth stash, floor decision, controlled-by-parent check, field folds, posture reader and by-id read;
    • the sharing verdicts still ask the plugin's set resolution, posture reader and RLS policy collection, and they still ask each other through the plugin;
    • the authored-row verdict still asks the plugin's set resolution, policy collection and layered RLS filter.

    No test replaces a moved method on the plugin instance today. One that did would still replace it for every caller.

  • Fields follow the rule cut 1 set, judged on each field's own declaration:

    • baselinePermissionSets is declared readonly, so it goes by value.
    • None of the other eight fields the bodies read is declared readonly: delegatedAdminGate, epoch, logger, metadata, permissionEvaluator, ql, resolveKernelService and tenancyPosture. So they go as getters, and each read happens when the body makes it. start() assigns all of them except permissionEvaluator after construction, and tests replace permissionEvaluator on a constructed plugin.
    • delegatedAdminGate, epoch, logger, permissionEvaluator and ql are objects whose own methods the bodies call. resolveKernelService is the one function-valued field (disclosed above).
    • The hosts declare every field readonly, so a moved body cannot assign a member through them. The typecheck passes, so none does.
  • Nothing re-exports any of the modules, 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. Each of the 549 body lines is the base line with two leading spaces removed. Parameters, return types and async match.

    • The explanation's body holds five multi-line JSDoc text tokens, the docblocks of its inner closures. Their continuation lines are re-indented one level, like every other body line. So the comparison dedents those JSDoc tokens one level, and only those. A string or template token that spans lines is compared byte for byte. The explanation's 1,731 tokens are then equal.
    • Four negative controls fail the check:
      • in explainAccessForCaller, callerContext?.isSystem === true changed to === false;
      • in resolveSharingCanEdit, the fail-closed return false; of the probe's catch changed to return true;;
      • in checkAuthoredRowWrite, the non-write abstention return 'abstain'; changed to return 'admit';;
      • in one of the explanation's inner JSDoc blocks, "The two contexts" changed to "The three contexts". This control shows that the JSDoc dedent does not hide a change of words.
  • Class side. Each moved method keeps its docblock and signature, less async. Every other class member is byte-identical to base: 116 named members plus the constructor, including everyoneBaseline and destroy. The class has 127 members at base and at head.

  • Top level of security-plugin.ts. Of the 106 top-level statements at base other than the class, 97 are byte-identical at head. The other nine:

    • five imports each lose the names listed above, and the ./builtin-positions.js import is gone;
    • AUTHORED_ROW_WRITE_PROBE_CONTEXT is gone from the file. The authored-row module holds the same statement and docblock, byte for byte;
    • absentUnderCallerRead and unreadableUnderCallerRead are the base text with export added.

    The four new statements are the imports of the new modules.

  • The two re-pointed readers. The page diff is one line, the anchor's path. The pin diff is the title's count and one added list entry.

  • git's own view. This is git diff --color-moved=plain --color-moved-ws=allow-indentation-change 05cc3ee953 HEAD over the five source files. Of the 596 deleted lines, git sees 581 as moved. The other 15 are the nine signatures that lose async, four of the import lines that change, and the two functions that gain export. Of the 1,184 added lines, 679 show as moved: 669 in the four modules, and 10 in security-plugin.ts whose text git also finds among the deleted lines (nine stub }; lines and the host line baselinePermissionSets: this.baselinePermissionSets,). The rest are the modules' headers, imports, host interfaces, type aliases and function signatures, the ten stubs, and the four new imports with their comments.

  • Size. 1,785 changed lines (+1,187 / -598) over 7 files, under the 3,000-line manual-merge line.

    • security-plugin.ts goes from 8,852 to 8,406 lines.
    • security-plugin-everyone-anchor.ts has 140 lines, security-plugin-explain-access.ts 433, security-plugin-sharing-verdicts.ts 277 and security-plugin-authored-row-write.ts 184.
    • The other two files are the page (+1 / -1) and the pin (+2 / -1).

ADR anchors (ruling 6353073966 item 2)

  • node scripts/check-adr-symbol-anchors.mjs exits 0 and gives byte-identical output at base 05cc3ee953 and at head. dispatch-gates --commands does not derive it for this change set, so it was run by hand.

  • pnpm check:adr-anchors exits 0 at base and at head, with 81 anchored files at both ends.

  • No new entry. No paragraph of the security-plugin.ts entry describes a moved body:

    • ADR-0055 §1 is the controlled-by-parent relation (cuts 2 to 4);
    • ADR-0106 D7 is the projection asymmetry (cut 9's field-readers module carries it);
    • ADR-0123 D2 is the write wall's no-organization refusal (cut 5);
    • ADR-0138 D1 is the seed-ownership claim.

    So no module of this cut gets an entry, as the modules of cuts 1 and 6 to 8 have none.

  • The decision-letter count on the anchored surface goes from 1978 to 1969. That gate counts ADR-NNNN Dk citations in each anchored file and in each entry's own invariant text. Nine citations leave the anchored surface with the moved bodies:

    • explainAccessForCaller 7: ADR-0090 D6, ADR-0090 D10, ADR-0090 D12, ADR-0105 D3 twice, ADR-0111 D3, ADR-0111 D5;
    • resolveSharingReadFilter 1: ADR-0057 D1;
    • checkAuthoredRowWrite 1: ADR-0058 D4.

    The other bodies, the moved constant's docblock, the four module headers and the four new import comments cite none.

    resolveSharingReadFilter's depth parameter comment also cites ADR-0090 D10. The parameter list belongs to the signature, which stays in the class, so that letter stays on the anchored surface. The move copies each signature unedited, so the function's parameter list carries it too.

    Measured directly: security-plugin.ts holds 258 such citations at base and 249 at head, and the 9 that leave are exactly these.

  • Every moved citation is still in the tree, in the module its body moved to. Citations that resolve tree-wide go from 52223 across 6651 files at base to 52224 across 6654 at head. Three of the modules cite an ADR. The one added citation is the depth parameter comment, which both signatures now carry.

  • docs/adr/** is not touched, and no anchor token is planted.

  • The anchor map covers every ADR symbol anchor that names security-plugin.ts: 27 anchors over 18 symbols.

    • It was measured with extractAnchors and symbolSegmentResolution from scripts/symbol-anchors.mjs. That is measurement, not a new gate.
    • It was read on 05cc3ee953 BEFORE the claim, with the whole cut applied as a local trial, and no anchor changed class.
    • No ADR anchors a row 8 member or the moved constant. Every anchor resolves at head in security-plugin.ts, with its base class.

The table gives each anchor's class at base and at head, and the line its own declaration resolves on. Every anchor stays in security-plugin.ts.

anchor cited at base class · line (member) head class · line (member)
#CBP_MAX_CHAIN_DEPTH ADR-0055:135 declaration · :712 (top-level) declaration · :744 (top-level)
#SecurityPlugin ADR-0049:49, ADR-0138:248, ADR-0138:265 declaration · :1344 (class) declaration · :1346 (class)
#SecurityPlugin.getMetadataReadableFields ADR-0138:153 declaration · :6030 (method) declaration · :5584 (method)
#SecurityPlugin.resolvePermissionSetsForContextUnmemoized ADR-0138:427 declaration · :6624 (method) declaration · :6178 (method)
#assertControlledByParentWrite ADR-0055:58, ADR-0055:125 declaration · :8109 (method) declaration · :7663 (method)
#assertMasterRowEditable ADR-0055:125 declaration · :8235 (method) declaration · :7789 (method)
#collectRLSPolicies ADR-0105:93 declaration · :8289 (method) declaration · :7843 (method)
#composeWriteTenantWall ADR-0123:4 declaration · :7447 (method) declaration · :7001 (method)
#computeControlledByParentFilter ADR-0055:5, ADR-0055:123, ADR-0139:246 declaration · :7755 (method) declaration · :7309 (method)
#computeDelegatedReadFilter ADR-0139:223 declaration · :5949 (method) declaration · :5503 (method)
#computeRlsFilter ADR-0055:24 declaration · :7155 (method) declaration · :6709 (method)
#getReadFilter ADR-0096:181, ADR-0096:182, ADR-0096:183, ADR-0096:199, ADR-0139:222 declaration · :5895 (method) declaration · :5449 (method)
#hasPlatformAdminPosture ADR-0099:317 declaration · :7182 (method) declaration · :6736 (method)
#isPrincipalLessContext ADR-0138:152 declaration · :585 (top-level) declaration · :617 (top-level)
#manage_metadata ADR-0099:68 literal · :338 (top-level) literal · :370 (top-level)
#nav_permission_sets ADR-0126:406 literal · :1772 (init) literal · :1774 (init)
#rlsFilter ADR-0055:24 declaration · :4346 (start) declaration · :4348 (start)
#writeCheckPolicies ADR-0066:112 declaration · :1239 (top-level) declaration · :1241 (top-level)

Readers

Two readers outside the moved code read the moved lines by path: the system-context page and the #25107 enumeration pin. Both are re-pointed in this PR (above). No other gate, baseline, docs anchor or test reads the moved lines by path. Each kind was read:

  • The system-context census.
    • node scripts/isystem-census.mjs --json gives the same 148 sites at base and at head, 0 differences. The comparison ignores line numbers and maps the four modules back to security-plugin.ts.
    • One head site is in the modules: explainAccessForCaller's callerContext?.isSystem read, which the page's re-pointed anchor names. The moved bodies' isSystem: true keys and the probe constant's key are counted the same way at both ends.
    • The role counts are the same (read 153, declaration 38, key 438, other 9), and so are the 5 non-elevation reads and the 22 packages. The files holding a site go from 74 to 75, and the scanned files from 397 to 399, the two modules that name isSystem.
    • check-system-context-census and its --self-test exit 0 at base and at head. The totals are the same (148 sites, 127 symbols, 140 cited against 140 required, 163 anchors), and files go from 74 to 75. With the page left untouched, it reports 2 problems (claim).
  • The tenant-audit census. No moved body holds an engine write. The explanation and the authored-row verdict read through ql.findOne and ql.find, and the everyone declaration hands ql to the derivation's readers. node scripts/tenant-audit-census.mjs --json gives the same 238 write call sites at base and at head, compared the same way: 0 differences, none in the modules, and the same 175 non-engine calls. Only scannedSources moves, from 730 to 734, the four modules. check-tenant-audit-census exits 0 at base and at head with the same output, and tenant-audit-census.mdx is not touched.
  • 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 and operation-private-keys.pin.test.ts. No moved body names the read admission or the operation-private keys. Run on their own at head: 8 of 8 pass.
  • The platform checklist. access-security.json:2032 cites security-plugin.ts#declareEveryone (the everyone derivation). The anchor resolves on the method, which stays with its docblock. check-platform-checklist (the script and pnpm check:platform-checklist) exits 0 at base and at head with the same output.
  • Symbol anchors outside the ADRs. Every path-qualified plugin-security/src/security-plugin*.ts#… anchor outside docs/adr/ was resolved in its own file at base and at head: 32 at both, 0 unresolved. At head, 22 point into security-plugin.ts, 1 into the explain module (the re-pointed row 9), and 9 into the modules of cuts 3, 5 and 9, as at base.
  • The non-ADR corpora. The non-ADR corpora that cuts 2 to 9 read are spec liveness, the platform checklist, system-context.mdx and spec conversions/registry.ts. They hold 22 anchors over 13 symbols that name security-plugin.ts. With symbolSegmentResolution, each resolves at head with its base class: 0 changed. check-scripts-symbol-anchors and its --self-test, and pnpm --filter @objectstack/spec check:liveness, give the same output at base and at head.
  • Readers by name, not by path. These were read, and none needs a change, because each method still exists in the class under the same name and signature:
    • rest-server.ts:5953, which names SecurityPlugin.explainAccessForCaller in prose;
    • the behaviour tests of the explanation, the sharing verdicts, the authored-row verdict and the everyone anchor in this package. Each drives the plugin, and none calls or replaces a moved method directly;
    • the page's row text and the docblocks that stay in the class, which name these methods and stay true by method.
  • Links in the moved text. These are unedited. Where one named a class member from inside a body or a signature, the function's copy still names that member as text. Examples are {@link computeDelegatedReadFilter} in resolveSharingReadFilter's depth comment, and the explanation's inner links to resolvePreImageFloorDrop and checkControlledByParentWrite. The class keeps the docblocks those links belong to.
  • Line citations. No security-plugin.ts:N citation in the repo points into the moved ranges (base :760 to :788 and :4912 to :5838).

Gates

All gates ran against base 05cc3ee953 and on head 38392cb1b7, each in its own worktree after a full turbo run build there (both exit 0).

  • pnpm --filter @objectstack/plugin-security test (vitest run): Test Files 214 passed (214); Tests 4465 passed, 45 skipped (4510). Base 05cc3ee953 gives the same counts.

  • pnpm --filter @objectstack/plugin-security typecheck: exit 0 at head and at base. It runs tsc --noEmit, the scripts tsconfig, and check:test-typecheck.

  • eslint over the six touched source and test files: exit 0.

  • pnpm --filter @objectstack/plugin-security build: exit 0. It produced the .d.ts and the bundles measured above.

  • row-serving-door-exposure.pin.test.ts and operation-private-keys.pin.test.ts (@objectstack/core): 8 of 8 pass.

  • dispatch-gates --commands: 88 commands. The --ran reconciliation shows 88 derived, 88 run, 0 NOT-MEASURED and 0 UNRUN. The zero is derived: all 88 recorded an exit code, and none is 3.

  • Nine commands are not derived for this change set, and the reconciliation names them as recorded outside the derivation. Each was run by hand:

    • node scripts/check-adr-symbol-anchors.mjs;
    • pnpm check:adr-anchors;
    • check-platform-checklist, both the script and pnpm check:platform-checklist;
    • check-scripts-symbol-anchors and its --self-test;
    • the two census --json reads;
    • pnpm --filter @objectstack/spec check:liveness.

    pnpm check:query-options-erasure is derived for this change set this time.

  • Seven more were run at base and at head. Six keep a roster in a directory this change touches, so dispatch-gates marks their silence as no evidence: check-published-list-mirrors, check:authz-resolver, check:console-injection, check:error-code-casing, check:filter-alias-parity and check:published-readme-exports. The seventh is the package typecheck. All seven exit 0 at both.

  • Base against head, all 97 commands. All 97 (the 88 derived and the 9 hand-run) exit 0 at head and at base. 72 give identical output at both, once the worktree path and timings are masked. The other 25, each read line by line:

    • check:adr-anchors, check-system-context-census and the two census --json reads move as described above.
    • check-issue-citations judges a change set, so it has nothing to read at base. At head it judged 42 citations across 5 files, and all resolve.
    • Counts that move by exactly the four modules:
      • check-closing-keyword-parity 12029 to 12033 tracked files;
      • check:gitlink-declared 12029 to 12033 index entries;
      • check:nul-bytes 12020 to 12024;
      • check-comment-mask-corpus 10005 to 10009;
      • check-plugin-teardown-shape 9397 to 9401 sources;
      • check-position-name-fold-loaders 12370 to 12374 files, with the same 8 references;
      • check-undeclared-dep-imports 3316 to 3320 files;
      • the spec package's check:duration-unit-keys 3321 to 3325;
      • check:closed-ceremony-text 4573 to 4577;
      • check:doc-authoring 1434 to 1438 parsed sources;
      • check:engine-double-contract 3625 to 3629 production files;
      • check:error-status-conformance 3413 to 3417;
      • check:logger-receiver-detach 3633 to 3637;
      • check:org-identifier 3658 to 3662;
      • check:page-declaration-shape 3666 to 3670;
      • check:refd-timer-probe 9998 to 10002;
      • check:type-check-coverage and check:type-check-debt 3542 to 3546 walked sources;
      • check:vendor-version-stamps 10397 to 10401.
    • check-undeclared-dep-imports also reads 2865 to 2872 @objectstack/* specifiers. Those are the seven the modules import, all declared dependencies. security-plugin.ts keeps every specifier it had.
    • check:doc-authoring also reads 104377 to 104528 strings, the strings of the new headers and stubs.
    • check:skill-identifier-liveness reads 139798 to 139805 implementation word tokens. Those are the seven words new to the tree: the four host names, the stubs' two getter locals readEpoch and readResolveKernelService, and the word "abstentions" in the authored-row module's header. No citation changes.

Per command (exit code at head, then at base), the derived first and the hand-run after:

  • node scripts/check-ci-filter-parity.mjs exit 0 (base 0)
  • node scripts/check-closing-keyword-parity.mjs exit 0 (base 0)
  • node scripts/check-closing-keyword-parity.mjs --self-test exit 0 (base 0)
  • node scripts/check-comment-mask-adoption.mjs exit 0 (base 0)
  • node scripts/check-comment-mask-adoption.mjs --self-test exit 0 (base 0)
  • node scripts/check-comment-mask-corpus.mjs exit 0 (base 0)
  • node scripts/check-doc-frontmatter.mjs exit 0 (base 0)
  • node scripts/check-doc-frontmatter.mjs --self-test exit 0 (base 0)
  • node scripts/check-doc-route-spelling.mjs --advisory exit 0 (base 0)
  • node scripts/check-doc-route-spelling.mjs --self-test exit 0 (base 0)
  • node scripts/check-docs-section-name.mjs exit 0 (base 0)
  • node scripts/check-docs-section-name.mjs --self-test exit 0 (base 0)
  • node scripts/check-dts-emitted.mjs --self-test exit 0 (base 0)
  • node scripts/check-issue-citations.mjs exit 0 (base 0)
  • node scripts/check-keyed-text-bounds.mjs exit 0 (base 0)
  • node scripts/check-keyed-text-bounds.mjs --self-test exit 0 (base 0)
  • node scripts/check-platform-object-tenancy-census.mjs exit 0 (base 0)
  • node scripts/check-platform-object-tenancy-census.mjs --self-test exit 0 (base 0)
  • node scripts/check-plugin-teardown-shape.mjs exit 0 (base 0)
  • node scripts/check-plugin-teardown-shape.mjs --self-test exit 0 (base 0)
  • node scripts/check-position-name-fold-loaders.mjs exit 0 (base 0)
  • node scripts/check-position-name-fold-loaders.mjs --self-test exit 0 (base 0)
  • node scripts/check-registry-log-declared.mjs exit 0 (base 0)
  • node scripts/check-registry-log-declared.mjs --self-test exit 0 (base 0)
  • node scripts/check-rest-log-spy-declared.mjs exit 0 (base 0)
  • node scripts/check-rest-log-spy-declared.mjs --self-test exit 0 (base 0)
  • node scripts/check-section-landing-index.mjs exit 0 (base 0)
  • node scripts/check-section-landing-index.mjs --self-test exit 0 (base 0)
  • node scripts/check-system-context-census.mjs exit 0 (base 0)
  • node scripts/check-system-context-census.mjs --self-test exit 0 (base 0)
  • node scripts/check-tenant-audit-census.mjs exit 0 (base 0)
  • node scripts/check-tenant-audit-census.mjs --self-test exit 0 (base 0)
  • node scripts/check-undeclared-dep-imports.mjs exit 0 (base 0)
  • node scripts/check-undeclared-dep-imports.mjs --self-test exit 0 (base 0)
  • node scripts/docs-audit/check-affected-docs.mjs exit 0 (base 0)
  • node scripts/docs-audit/check-drift-comment.mjs exit 0 (base 0)
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions exit 0 (base 0)
  • pnpm --filter @objectstack/lint run check:doc-security-posture exit 0 (base 0)
  • pnpm --filter @objectstack/spec run check:docs exit 0 (base 0)
  • pnpm --filter @objectstack/spec run check:duration-unit-keys exit 0 (base 0)
  • pnpm --filter @objectstack/spec run check:empty-state exit 0 (base 0)
  • pnpm --filter @objectstack/spec run check:liveness exit 0 (base 0)
  • pnpm --filter @objectstack/spec run check:skill-examples exit 0 (base 0)
  • pnpm --filter @objectstack/spec run check:strictness-ledger exit 0 (base 0)
  • pnpm --filter @objectstack/spec run check:variant-docs exit 0 (base 0)
  • pnpm --filter @objectstack/spec run check:yaml-examples exit 0 (base 0)
  • pnpm check:closed-ceremony-text exit 0 (base 0)
  • pnpm check:corpus-claim-drift exit 0 (base 0)
  • pnpm check:cross-package-test-inputs exit 0 (base 0)
  • pnpm check:doc-anchors exit 0 (base 0)
  • pnpm check:doc-authoring exit 0 (base 0)
  • pnpm check:docs-audit-scope exit 0 (base 0)
  • pnpm check:docs-redirects exit 0 (base 0)
  • pnpm check:docs-single-h1 exit 0 (base 0)
  • pnpm check:docs-spec-enumerations exit 0 (base 0)
  • pnpm check:docs-transcript-drift exit 0 (base 0)
  • pnpm check:driver-memory-census exit 0 (base 0)
  • pnpm check:dts-closure exit 0 (base 0)
  • pnpm check:dual-build-cjs-loads exit 0 (base 0)
  • pnpm check:engine-double-contract exit 0 (base 0)
  • pnpm check:error-status-conformance exit 0 (base 0)
  • pnpm check:gitlink-declared exit 0 (base 0)
  • pnpm check:i18n exit 0 (base 0)
  • pnpm check:i18n-stale-fill exit 0 (base 0)
  • pnpm check:issue-citations exit 0 (base 0)
  • pnpm check:lean-entry-closure exit 0 (base 0)
  • pnpm check:logger-receiver-detach exit 0 (base 0)
  • pnpm check:merge-driver exit 0 (base 0)
  • pnpm check:nul-bytes exit 0 (base 0)
  • pnpm check:objectql-double-limit exit 0 (base 0)
  • pnpm check:org-identifier exit 0 (base 0)
  • pnpm check:page-declaration-shape exit 0 (base 0)
  • pnpm check:published-files exit 0 (base 0)
  • pnpm check:published-readme-links exit 0 (base 0)
  • pnpm check:query-options-erasure exit 0 (base 0)
  • pnpm check:react-page-adapter-contract exit 0 (base 0)
  • pnpm check:refd-timer-probe exit 0 (base 0)
  • pnpm check:role-word exit 0 (base 0)
  • pnpm check:skill-identifier-liveness exit 0 (base 0)
  • pnpm check:slot-lookup exit 0 (base 0)
  • pnpm check:sourcemap-no-sources-content exit 0 (base 0)
  • pnpm check:test-source-alias exit 0 (base 0)
  • pnpm check:tier-file-adoption exit 0 (base 0)
  • pnpm check:type-check-coverage exit 0 (base 0)
  • pnpm check:type-check-debt exit 0 (base 0)
  • pnpm check:vendor-version-stamps exit 0 (base 0)
  • pnpm check:watch-hint-literal exit 0 (base 0)
  • pnpm check:where-matcher exit 0 (base 0)
  • node scripts/check-adr-symbol-anchors.mjs exit 0 (base 0)
  • pnpm check:adr-anchors exit 0 (base 0)
  • node scripts/check-platform-checklist.mjs exit 0 (base 0)
  • pnpm check:platform-checklist exit 0 (base 0)
  • node scripts/check-scripts-symbol-anchors.mjs exit 0 (base 0)
  • node scripts/check-scripts-symbol-anchors.mjs --self-test exit 0 (base 0)
  • node scripts/isystem-census.mjs --json exit 0 (base 0)
  • node scripts/tenant-audit-census.mjs --json exit 0 (base 0)
  • pnpm --filter @objectstack/spec check:liveness exit 0 (base 0)

Acceptance notes


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa

… the sharing verdicts and the authored-row write verdict out of security-plugin.ts

The bodies of declareEveryone and declareBuiltinPositions (the everyone
anchor's definition and the built-in positions) move unedited to
security-plugin-everyone-anchor.ts, with the four import names only they
read. The body of explainAccessForCaller moves unedited to
security-plugin-explain-access.ts, with the import names only it reads;
the two caller-read classifications it shares with the write path stay in
security-plugin.ts and are exported. The bodies of the six sharing
verdicts (resolveSharingReadFilter, resolveSharingCanEdit,
resolveWriteScopeForSharing, resolveSharingWriteVerdict,
resolvePreImageFloorDrop, delegatorOwnWriteFloorOptions) move unedited to
security-plugin-sharing-verdicts.ts. The body of checkAuthoredRowWrite
moves unedited to security-plugin-authored-row-write.ts, with the probe
scope constant only it reads. Each method keeps its name, signature,
docblock and position and hands its body one host literal of exactly the
members it reads. everyoneBaseline and destroy stay whole. The two
readers that read the moved lines by path are re-pointed: the
system-context page's explain anchor names the new module, and the
delegator enumeration pin lists it. 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 github-actions Bot added size/xl documentation Improvements or additions to documentation tests labels Oct 23, 2026
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 17:36
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/administrator-guide.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/permissions/authorization.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/permissions/capabilities.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/permissions/delegated-administration.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/permissions/explain.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/permissions/permission-metadata.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/permissions/permission-sets.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/permissions/permissions-matrix.mdx (via computeLayeredRlsFilter (symbol, a field of interface AuthoredRowWriteHost; a field of interface ExplainAccessHost), computeLayeredRlsFilter (literal, a string literal in RlsFilterOptions; a string literal in checkAuthoredRowWrite; a string literal in computeLayeredRlsFilter; a string literal in explainAccessForCaller))
  • content/docs/permissions/rls.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/permissions/sharing-rules.mdx (via computeLayeredRlsFilter (symbol, a field of interface AuthoredRowWriteHost; a field of interface ExplainAccessHost), computeLayeredRlsFilter (literal, a string literal in RlsFilterOptions; a string literal in checkAuthoredRowWrite; a string literal in computeLayeredRlsFilter; a string literal in explainAccessForCaller))
  • content/docs/permissions/system-context.mdx (via checkControlledByParentWrite (symbol, a field of interface ExplainAccessHost), explainAccessForCaller (symbol, a method of class SecurityPlugin; a top-level function), checkControlledByParentWrite (literal, a string literal in checkControlledByParentWrite; a string literal in explainAccessForCaller), manage_users (literal, a string literal in explainAccessForCaller))

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

  • content/docs/releases/v13.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/releases/v15.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/releases/v16.mdx (via manage_users (literal, a string literal in explainAccessForCaller))
  • content/docs/releases/v18/18-0.mdx (via /api/v1/meta/position (route, a path literal in declareBuiltinPositions))

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
  • 3 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 — 17 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 5188e7f18fea8d336d5f4a0f0a48c6af67dfc243 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 5188e7f18fea8d336d5f4a0f0a48c6af67dfc243

⚠️ 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 5188e7f18fea8d336d5f4a0f0a48c6af67dfc243 → 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-23T18:37Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25223 cut 10, plan row 8 (claim 6369634056; os-dev-report 6371818390) · reviewed head 38392cb1b7 · merge base 05cc3ee953


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 18:38
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 18:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 82f5a11 Oct 23, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-3fzya4 branch October 23, 2026 19:10
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 tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants