Skip to content

refactor(plugin-security): move the permission-set resolution and addressed-write readers out of security-plugin.ts (#25223 cut 8) - #25406

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 8 of the security-plugin.ts split: row 6 of the series plan, the permission-set resolution and the addressed-write readers. The claim is 6363282332. The row has two seams, so the bodies of nine private methods move unedited out of the class to two new modules in this one PR, as cut 6 split its bodies over two modules.

To packages/plugins/plugin-security/src/security-plugin-permission-sets.ts:

To packages/plugins/plugin-security/src/security-plugin-addressed-writes.ts:

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 2dfeb1a772, cut 7's merge. 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. Every caller still calls the methods. Seven members of the row stay whole:

  • getEffectiveObjectPermissions, resolveFallbackPermissionSets, declaresControlledByParent, callerCannotReadAddressedRow and getCallerPreImage: each body is no longer than its stub would be.
  • resolvePermissionSetsForContext, the memo. Its body retires a rejected entry with this.permissionSetMemo.delete(context). The tenant-audit census places that call by its receiver's declared type, a WeakMap the class constructs, and subtracts it as a non-engine call. Read through a host, the receiver has no type the census can read. A trial cut measured it: the census exits 1 ([untyped-receiver]) and asks for an UNTYPED_RECEIVERS row, in a ledger its own docblock declares shrink-only. So the memo stays.
  • describePositionGrants. Its body calls this.canReadObject(…), and packages/core/src/security/row-serving-door-exposure.pin.test.ts classifies every non-test source file that calls the read admission. Moved, the body would make the new module a caller the pin has no row for, and writing that row is a classification, not a move.

extractSingleId has no docblock of its own. Its docblock is the first of the two that sit above declaresControlledByParent's, and it stays there; the moved function's pointer comment says so.

Module-level declarations.

  • Two stay and gain export. The modules import both by value, as cuts 3, 5, 6 and 7 import theirs.
    • readUnlessRefused is shared: absentUnderCallerRead and unreadableUnderCallerRead ask it too.
    • POSITION_NAME_FOLD_EVENT: the reportNameFoldCollisions docblock, which stays, names it in place with a {@link}. Its own docblock says it is deliberately not exported from the package, and that stays true: index.ts exports only SecurityPlugin, and the published .d.ts does not change (below).
  • The modules also import four values that are already exported: isGuestEnvelope (cut 6), and PREDICATE_WRITE_READABLE_ID_CAP, predicateWriteReadableCapError and copyOfCallerPredicate (cut 3, which security-plugin-cbp-write-gate.ts imports from security-plugin.ts). Those stay where they are, so that module's import does not move.
  • The cut 6 .d.ts trap was checked before the claim. At cut 6, an export whose signature names another file's type moved a declaration in the published .d.ts. A local trial build with the whole cut in place ran first. With both new exports in place, the head dist/index.d.ts is byte-identical to base (below).
  • Import names only the moved bodies read leave security-plugin.ts, and the modules import them from the same specifiers:
    • MCP_AGENT_PERMISSION_SET_RESTRICTED from @objectstack/spec/ai (that whole import statement goes);
    • readDisabledCatalogNames and withoutOperationPrivateKeys from @objectstack/core;
    • resolveEngineDeleteDispatch and the type EngineDeleteDispatchInput from @objectstack/metadata-core.
      No staying docblock links to any of them.
  • Each value import is read only inside a function body, at call time, never while any of the three modules evaluates. index.ts still exports only SecurityPlugin, so the published surface does not move (measured below).

Readers. No reader outside the moved code reads the moved lines by path, so this PR changes only the three source files (see Readers below).

The published .d.ts does not change

Under the series bar (ruling 6348891160 item 1), measured on base 2dfeb1a772 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.
  • These are the same hashes as cuts 6 and 7 reported. Base and head here are both built against 2dfeb1a772's dependencies, rebuilt on that commit first.
  • readUnlessRefused and POSITION_NAME_FOLD_EVENT gain export in security-plugin.ts. A local trial build measured this before the claim, because at cut 6 an export whose signature names another file's type moved a declaration. Here the .d.ts did not change by a byte.
  • 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 1307 (private resolvePermissionSetsForContextUnmemoized;):
    • Made public (resolvePermissionSetsForContextUnmemoized;): raw 19c84435e20b905ee3f3d43803961fa4f0475b04f5747e4662731d5c50432b45, normalised cb6bcc7126d1a176815c99a2b9ad5a674d7b9f5f7b1c0dd43b47e39c5980f8a6.
    • Renamed (private resolvePermissionSetsForContextUnmemoizedRenamed;): raw bbfe2a4f35bc7a2f415852a2bb4ba3f35bd78391557b484e8638dee38b1b4039, normalised 677731d933ae7a9ddf2e4c65135853065b58506a08d33b62dab1ed2d1a9fb974.
    • Both differ from the real hashes, so the comparison does see a visibility change and a rename.
  • The nine moved methods are private, so the .d.ts carries each one's name and docblock only (lines 1261, 1307, 1326, 1448, 1542, 1574, 1600, 1601 and 1643 of both).
  • 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, as at cuts 6 and 7. The new exports of security-plugin.ts do not reach the entry.
  • The bundle JS bytes differ (index.js cf7a65c1 to 3a109ef0, index.mjs d4178511 to 1c38ee91), because code moved within them. One consequence of the removed @objectstack/spec/ai import: that import now sits in the new module's segment (index.mjs line 9232), so the bundle's first import of @objectstack/spec/ai is one it already had, at line 1046 (1047 at base), rather than line 664. @objectstack/spec declares "sideEffects": false, so no evaluation order a caller can observe moves.

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

What moved

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

method base lines module host members
permissionSetMemoKey (private) :6906-:6915 permission-sets none: called directly
resolvePermissionSetsForContextUnmemoized (private, async) :7003-:7085 permission-sets baselinePermissionSets, bootstrapPermissionSets, dbLoaderForContext, dropDeactivatedFoldedSets, logger, metadata, permissionEvaluator, reportNameFoldCollisions
dropDeactivatedFoldedSets (private, async) :7105-:7122 permission-sets ql
reportNameFoldCollisions (private) :7279-:7324 permission-sets logger, nameFoldWarned
addressedByIdWriteId (private) :7437-:7450 addressed-writes none: called directly
addressedPredicateWrite (private) :7486-:7497 addressed-writes none: called directly
readableRowsScopeForPredicateWrite (private, async) :7524-:7556 addressed-writes ql
extractSingleId (private) :7558-:7570 addressed-writes none: called directly
readRowById (private, async) :7613-:7630 addressed-writes ql

Each async stub drops async and returns the moved function's promise, as cuts 1, 3, 5, 6 and 7 did for their async methods. A body that reads no member is called directly, with no host, as cut 1's extractTargetField is. All nine methods are private, so the .d.ts carries each one's name and docblock only.

The shape (#23050 shape B, as cuts 1 to 7 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, …), or one direct call, marked [#25223 cut 8].
  • PermissionSetsHost and AddressedWritesHost are new. Each types every member as the class member itself (SecurityPlugin['m']). Each moved function types its this as the Pick of the host over exactly the members its body reads, and 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 resolution still asks the plugin's catalog loader, its folded-set drop and its fold report;
    • the memo, describePositionGrants and every other caller still reach the moved methods through the plugin. No test replaces a moved method on the instance today; one that did would still replace it for every caller.
      No moved body calls a function-valued field, so no receiver changes: permissionEvaluator, ql, metadata and logger are objects whose own methods the bodies call.
  • Fields follow the rule cut 1 set, on each field's own declaration.
    • baselinePermissionSets, bootstrapPermissionSets and nameFoldWarned are declared readonly and go by value, so the bodies read the plugin's own lists and its own warned set.
    • logger, metadata, permissionEvaluator and ql are not declared readonly, so they go as getters, and each read happens when the body makes it. start() assigns logger, metadata and ql after construction.
    • 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 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 217 lines is the base line with two leading spaces removed. Parameters, return types and async match. Three negative controls fail the check:
    • in resolvePermissionSetsForContextUnmemoized, if (!isAgent && context?.userId) changed to if (isAgent && context?.userId);
    • in readableRowsScopeForPredicateWrite, PREDICATE_WRITE_READABLE_ID_CAP + 1 changed to + 2;
    • in reportNameFoldCollisions, removedIn: 'next_major' changed to 'next_minor'.
  • Class side: each moved method keeps its docblock and signature, less async. Every other class member, 117 named members plus the constructor, is byte-identical to base. That includes the seven staying members above. The class has 127 members at base and at head.
  • Top level of security-plugin.ts: 97 of the 103 base top-level statements other than the class are byte-identical to base. The six that differ:
    • two are the base text with export before the declaration: readUnlessRefused, POSITION_NAME_FOLD_EVENT;
    • three imports lose the names listed above: @objectstack/core (twice) and @objectstack/metadata-core;
    • the @objectstack/spec/ai import is gone.
      The two new statements are the imports of the new modules.
  • git's own view (git diff --color-moved=plain --color-moved-ws=allow-indentation-change 2dfeb1a772 HEAD over the three source files): of the 227 deleted lines, git sees 218 as moved. The other 9 are the four signatures that lose async, the two declarations that gain export, and the three import lines that lose names (@objectstack/core twice, @objectstack/metadata-core). The deleted @objectstack/spec/ai import line shows as moved, because the permission-sets module imports it with the same text. Of the 578 added lines, 229 show as moved. The other 349 are the two modules' headers, imports, host interfaces and pointer comments, the nine stubs, the two new imports with their comments, and the edited lines above.
  • Size: 805 changed lines (+578 / -227) over 3 files, under the 3,000-line manual-merge line. security-plugin.ts goes from 9,394 to 9,248 lines; security-plugin-permission-sets.ts has 281 and security-plugin-addressed-writes.ts 216.

ADR anchors (ruling 6353073966 item 2)

  • node scripts/check-adr-symbol-anchors.mjs: exit 0 at base 2dfeb1a772 and exit 0 at head, both with 2384 anchors across 142 records resolve — 466 symbol (440 declaration, 26 literal), 1890 file-level, 28 cross-repo, 6 exempt, 18 continuation. CI runs this gate (lint.yml), but dispatch-gates --commands does not derive it for this change set ([finding] dispatch-gates derives the symbol-anchor sweeps only from their corpus paths, so a change that removes an anchored symbol from a TARGET file passes every local gate and reds CI #21697), so it is run by hand here.
  • pnpm check:adr-anchors: exit 0 at base and at head, both with 80 anchored files. The decision-letter count on the anchored surface goes from 2005 to 2002. That gate counts ADR-NNNN Dk citations in anchored files only, and neither new module has an entry:
    • 3 citations leave security-plugin.ts with the moved bodies, all in resolvePermissionSetsForContextUnmemoized: ADR-0138 D3, ADR-0090 D10 and ADR-0090 D5;
    • none arrives: the two new import comments, the module headers and the pointer comments cite no decision letter.
    • Each moved citation is still in the tree, in security-plugin-permission-sets.ts.
  • scripts/adr-anchors/ is unchanged. No paragraph of the security-plugin.ts entry (ADR-0055 §1, ADR-0106 D7, ADR-0123 D2, ADR-0138 D1) describes these bodies, so neither new module gets an entry, as the modules of cuts 1, 6 and 7 have none, and none is invented to hold the count. 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 2dfeb1a772 BEFORE the claim, with the whole cut applied as a local trial: no anchor changed class. Every anchor resolves at head in security-plugin.ts with its base class, and neither new module resolves any of them.

The table gives each anchor's class at base and 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 · :665 (top-level) declaration · :688 (top-level)
#SecurityPlugin ADR-0049:49, ADR-0138:248, ADR-0138:265 declaration · :1297 (class) declaration · :1320 (class)
#SecurityPlugin.getMetadataReadableFields ADR-0138:153 declaration · :6092 (method) declaration · :6115 (method)
#SecurityPlugin.resolvePermissionSetsForContextUnmemoized ADR-0138:427 declaration · :7003 (method) declaration · :7020 (method)
#assertControlledByParentWrite ADR-0055:58, ADR-0055:125 declaration · :8651 (method) declaration · :8505 (method)
#assertMasterRowEditable ADR-0055:125 declaration · :8777 (method) declaration · :8631 (method)
#collectRLSPolicies ADR-0105:93 declaration · :8831 (method) declaration · :8685 (method)
#composeWriteTenantWall ADR-0123:4 declaration · :7989 (method) declaration · :7843 (method)
#computeControlledByParentFilter ADR-0055:5, ADR-0055:123, ADR-0139:246 declaration · :8297 (method) declaration · :8151 (method)
#computeDelegatedReadFilter ADR-0139:223 declaration · :5970 (method) declaration · :5993 (method)
#computeRlsFilter ADR-0055:24 declaration · :7697 (method) declaration · :7551 (method)
#getReadFilter ADR-0096:181, ADR-0096:182, ADR-0096:183, ADR-0096:199, ADR-0139:222 declaration · :5848 (method) declaration · :5871 (method)
#hasPlatformAdminPosture ADR-0099:317 declaration · :7724 (method) declaration · :7578 (method)
#isPrincipalLessContext ADR-0138:152 declaration · :538 (top-level) declaration · :561 (top-level)
#manage_metadata ADR-0099:68 literal · :287 (top-level) literal · :310 (top-level)
#nav_permission_sets ADR-0126:406 literal · :1725 (init) literal · :1748 (init)
#rlsFilter ADR-0055:24 declaration · :4299 (start) declaration · :4322 (start)
#writeCheckPolicies ADR-0066:112 declaration · :1192 (top-level) declaration · :1215 (top-level)

No anchor gains or loses a resolving site; each line only shifts. ADR-0138:427 #SecurityPlugin.resolvePermissionSetsForContextUnmemoized resolves on the method, which stays with its docblock; its body moved. The two exports this cut adds (readUnlessRefused, POSITION_NAME_FOLD_EVENT) name no anchored symbol.

Readers

No reader outside the moved code reads the moved lines by path, so no gate, baseline, docs anchor or test moves with this cut. Each kind was read:

  • The tenant-audit census. The moved bodies hold no engine write: readRowById's ql.findOne and the predicate write's ql.find are reads. The one write-verb call in the row, the memo's permissionSetMemo.delete(…), stays in the class (above). node scripts/tenant-audit-census.mjs --json gives the same 238 write call sites at base and head, compared without line numbers and with the new modules mapped back to security-plugin.ts: 0 differences, the same totals, the same 174 non-engine subtractions with the same reasons, 0 unresolved, 0 unledgered. Only scannedSources moves, 721 to 723: the two modules. check-tenant-audit-census and its --self-test exit 0 at head.
  • The system-context census. The moved bodies hold no elevation read. node scripts/isystem-census.mjs --json gives the same 148 sites at base and head, compared the same way: 0 differences, and no site in either new module. The role counts are the same (read 153, declaration 38, key 438, other 9), and so are the 5 non-elevation reads. The scanned file count is 393 at both, because neither module mentions the flag. check-system-context-census and its --self-test exit 0 at base and at head with the same output.
  • packages/core/src/security/row-serving-door-exposure.pin.test.ts classifies every non-test caller of the read admission canReadObject. No moved body calls it: describePositionGrants, the one row 6 member that does, stays (above). operation-private-keys.pin.test.ts in the same directory finds every declaration of withoutOperationPrivateKeys and the key prefix; the moved body only calls the helper. Run on their own at head: 8 of 8 pass. Open PR test: scope the tree-wide pin scans to the extensions they judge #25389 changes those two pins' scan scope (an extension-scoped prefilter), not what either discriminates on, and touches none of this cut's files.
  • 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 (delegated-organization-membership.test.ts) counts resolveDelegatorContext( and delegatedOrganizationRefusal( per source file. The moved bodies call neither, so its list of three files is unchanged, and it passes at head.
  • Other readers that name the file in code. Besides the two pins above, the code that names security-plugin.ts by path is: the check-system-context-census.mjs row for the file (keyed on start, which this cut does not touch), the spec liveness ledger's file-level evidence in field.json, the file's scripts/adr-anchors/ entry, and the citation string in measure-position-name-fold-census.mjs. None reads text inside the moved bodies. The tree-wide scanners that read every source file are the gates and censuses below, each run at base and head.
  • The position-name fold's prose and its two scripts. Four texts name security-plugin.ts as where the fold happens: the lint.yml comment on the Permission-set resolution binds by POSITION NAME while sys_position_permission_set sits near-empty — confirm name-based resolution is the intended mechanism, not an accident the empty junction hides #13419 gate, the message text of check-position-name-fold-loaders.mjs, the comment in measure-position-name-fold-census.mjs, which quotes the request line, and the delegated-admin-gate.ts docblock, which quotes the fold expression. Each stays true by method: SecurityPlugin#resolvePermissionSetsForContextUnmemoized is still in that file, and its body is one call away. None of them is edited.
    • check-position-name-fold-loaders.mjs scans the tree only for references to the HotCRM artifact, which no moved body names. It and its --self-test exit 0 at head.
    • measure-position-name-fold-census.mjs scans plugin-security's sources for export const string constants, so it now also sees POSITION_NAME_FOLD_EVENT, whose value names no position or set. Run at base and at head, its output is byte-identical: it already refuses on its own failing self-test at base. CI does not run it.
  • 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.
  • Symbol anchors outside the ADRs. Every security-plugin.ts#… anchor anywhere in the repo was resolved at base and at head: 30 symbols, among them the 27 spec liveness, platform checklist, system-context.mdx and spec conversions/registry.ts anchors cuts 2 to 7 read. With symbolSegmentResolution, each resolves at head with its base class: 0 changed.
  • 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:
    • the behaviour tests of the resolution, the fold warning, the activation drop, the addressed-write read question and the store-fault probe (resolve-permission-sets-for-context.pin.test.ts, position-name-fold-warning.test.ts, orgless-position-name-fold.test.ts, permission-set-active.test.ts, explain-read-absent-every-write-verb.test.ts, vama-write-path-convergence.test.ts, store-fault-fail-closed.test.ts and the rest). Each drives the plugin; none calls or replaces a moved method directly;
    • comments and prose naming these methods: explain-engine.ts (addressedByIdWriteId in security-plugin.ts), security-plugin-cbp-write-gate.ts (whose header says readableRowsScopeForPredicateWrite in security-plugin.ts shares its three values; it still does, through this module), and the tests above.
  • Line citations: no security-plugin.ts:N citation in the repo points into the moved ranges (base :6906-:7630). The citations found are the list cuts 2 to 7 read (docs/launch-readiness.md :309, packages/objectql/src/engine.ts :1614, :1690, :1736, spec liveness :3281 and the rest, and the L949 of engine-summary-recompute-context.test.ts). 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 2dfeb1a772 (advisory): 0 pages. The derivation reads 29 anchors (22 symbol, 9 literal) across the one changed package, and no page names any of them. This PR edits no page.
  • Census gates, run on base 2dfeb1a772 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-system-context-census, 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.
    • check:adr-anchors: the same 80 anchored files; the decision-letter count goes from 2005 to 2002 (above), and the files carrying citations from 6631 to 6632, the permission-sets module.
    • Only scanned counts move, each by the two new files:
      • check-undeclared-dep-imports: 3307 to 3309 files, and 2853 to 2857 specifiers. The modules import @objectstack/spec/security, @objectstack/spec/ai, @objectstack/core and @objectstack/metadata-core, all declared dependencies.
      • check:org-identifier 3649 to 3651, check:page-declaration-shape 3656 to 3659, check:logger-receiver-detach 3623 to 3626, check:refd-timer-probe 9976 to 9978, check-plugin-teardown-shape 9377 to 9379, check-comment-mask-corpus 9982 to 9985, check:closed-ceremony-text 4548 to 4550, check:nul-bytes 11979 to 11981, check:gitlink-declared and check-closing-keyword-parity 11988 to 11990, check:error-status-conformance 3404 to 3406, and the spec package's check:duration-unit-keys 3312 to 3314. Where a count moves by 3, the third file is a local build output or generated file that exists only in the head worktree.
      • check:doc-authoring: 1426 to 1428 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, as at cuts 6 and 7. CI's checkout does not have them.
      • check-position-name-fold-loaders: 12343 to 12348 files, with the same 8 references (6 test, 2 declared instrument, 0 loader). It walks the working tree.
    • check:dts-closure and check:sourcemap-no-sources-content sweep built packages, and the head worktree has more of them built (76 and 72) than the base worktree (19). Both pass at both. check:dual-build-cjs-loads and check:i18n answer exit 3 at base, a missing-build prerequisite in the base worktree rather than a verdict, and exit 0 at head.
    • check-issue-citations judges a change set, so it has nothing to read at base. At head it judged 26 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, no census ledger, and no file outside the package.
  • Serial check, also in the claim: of the 13 open PRs at claim time, none touched security-plugin.ts, the new module names, explain-engine.ts, delegated-admin-gate.ts, the census scripts, the permissions docs pages or the plugin-security anchor entry.
  • Since the local runs, origin/main gained 1 commit (830a63a680, feat(analytics): carry a dataset dimension's nullLabel on its column descriptor, and name System Overview's null user bucket "System" #25402, analytics). It touches none of this cut's files. On a scratch merge of this head onto that main, check-adr-symbol-anchors, check:adr-anchors, check-platform-checklist, check-system-context-census and check-tenant-audit-census all exit 0; check:adr-anchors there counts 80 anchored files and 2002 decision-letter citations.
  • What is left of the plan's row 6 in security-plugin.ts after this cut: the seven members that stay whole (above) and the nine method shells. security-plugin.ts has 9,248 lines at head, 9,394 at base.

Gates

All gates ran at the head, the tree of 23b0616bae, on base 2dfeb1a772.

  • 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 2dfeb1a772 gives the same counts. VERDICT command-exit 0 on both.
  • 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: 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 2dfeb1a772 first.
  • row-serving-door-exposure.pin.test.ts and operation-private-keys.pin.test.ts (@objectstack/core): 8 of 8 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), pnpm check:query-options-erasure, 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: 52 commands. The --ran reconciliation shows 52 derived, 52 run, 0 NOT-MEASURED, 0 UNRUN. The zero is derived: all 52 recorded an exit code and none is 3. No docs family is derived, because this cut touches no page.

Per command (exit code), the derived first and the 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-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-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/spec run check:duration-unit-keys exit 0
  • pnpm check:closed-ceremony-text exit 0
  • pnpm check:cross-package-test-inputs exit 0
  • pnpm check:doc-authoring exit 0
  • pnpm check:driver-memory-census exit 0
  • pnpm check:dts-closure exit 0
  • pnpm check:dual-build-cjs-loads 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:nul-bytes exit 0
  • pnpm check:org-identifier exit 0
  • pnpm check:page-declaration-shape exit 0
  • pnpm check:published-files exit 0
  • pnpm check:refd-timer-probe 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:watch-hint-literal exit 0
  • node scripts/check-adr-symbol-anchors.mjs exit 0
  • pnpm check:adr-anchors exit 0
  • node scripts/check-platform-checklist.mjs exit 0
  • pnpm check:platform-checklist exit 0
  • node scripts/check-scripts-symbol-anchors.mjs exit 0
  • node scripts/check-scripts-symbol-anchors.mjs --self-test exit 0
  • pnpm check:query-options-erasure exit 0
  • node scripts/isystem-census.mjs --json exit 0
  • node scripts/tenant-audit-census.mjs --json exit 0

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa

…ressed-write readers out of security-plugin.ts

The bodies of resolvePermissionSetsForContextUnmemoized (the resolution
itself: the position fold, the human baseline or the agent ceiling, the
fallback), dropDeactivatedFoldedSets, reportNameFoldCollisions and
permissionSetMemoKey move unedited to security-plugin-permission-sets.ts.
The bodies of addressedByIdWriteId, addressedPredicateWrite,
readableRowsScopeForPredicateWrite, extractSingleId and readRowById move
unedited to security-plugin-addressed-writes.ts. Each takes the import
names 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; a body that reads no member is called directly. The memo, the
position-grant description and five thin members stay whole.
readUnlessRefused and POSITION_NAME_FOLD_EVENT stay and are exported and
imported by value. 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
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 11:47
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

29 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 2 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 830a63a6805ff03824b55ab50f91c4aeead59ee4 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 830a63a6805ff03824b55ab50f91c4aeead59ee4

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T12:34Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25223 cut 8, plan row 6 (claim 6363282332; os-dev-report 6364689268) · reviewed head 23b0616bae8c661ca7dfcf8b6f85630ea04b509a · merge base 2dfeb1a772

  • Shape: 9 private method bodies move unedited, in shape B as in cuts 1 to 7. 3 files, +578 / −227.
    • To security-plugin-permission-sets.ts: resolvePermissionSetsForContextUnmemoized, dropDeactivatedFoldedSets, reportNameFoldCollisions, permissionSetMemoKey.
    • To security-plugin-addressed-writes.ts: addressedByIdWriteId, addressedPredicateWrite, readableRowsScopeForPredicateWrite, extractSingleId, readRowById.
    • Stay whole: five thin members, resolvePermissionSetsForContext (the memo; a host read of permissionSetMemo.delete() made the census report an untyped receiver, and that ledger is shrink-only), and describePositionGrants (it calls canReadObject, and the core row-serving pin classifies every caller file). Both reasons are accepted.
    • readUnlessRefused and POSITION_NAME_FOLD_EVENT gain export in place. plugin-security's index.ts exports only { SecurityPlugin }, so neither leaks (.d.ts below).
  • .d.ts, ruling 6348891160 item 1, re-run by the seat in its own worktree. turbo run build --filter=@objectstack/plugin-security ran at 2dfeb1a772 and at this head. dist/index.d.ts and dist/index.d.mts are raw af5dee1bebafa0376a876d1e7b247c878ea42ac35c7336a6d3c12a7953143156 and normalised 431e7f5097ac1c220bc840188aef02105bc0b16e845e976dd562d02cc79be8bd at both. The raw diff is 0 lines.
    • The seat's own positive control uses head line 1305, private resolvePermissionSetsForContext, made public: raw 2b7e2df7bd028fa5… and normalised 913a803995da45d0… both moved.
    • Export lists: 66 on ESM and CJS, base = head (report).
  • Gates re-run by the seat, all exit 0 at base and at head:
    • check-adr-symbol-anchors: 2384 … 466 symbol at both.
    • check-platform-checklist
    • check-tenant-audit-census: 238 sites.
    • check-system-context-census: 148 sites.
    • check-adr-anchors
  • Decision-letter count, read by the seat: 2005 at base, 2002 at head. ADR-0138 D3, ADR-0090 D10 and ADR-0090 D5 go out with the resolution body into an unanchored module, and none comes in. Accepted disclosed, as for cuts 6 and 7, and no anchor entry is added. The series total on this card is now 4 + 10 + 3.
  • Tests and gates (report): 52 derived gates plus 9 by hand exit 0. Docs drift: 0 pages. CI on this head: 34 success and 12 expected skips, none failed or pending.
  • Mergeable: clean, and git merge-tree onto today's main (f0fdd45bbc, after refactor(plugin-auth): move AuthManager's database and auth secret inputs into auth-manager-database-secret.ts (#25224 cut 7) #25404) 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 25406: 805 lines). Ready + auto-merge through the relay. The card stays open and dispatched. Next is plan row 7 (read filters, field readers, object admission), claimed afresh on the merged main.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 12:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 12:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 25f88e8 Oct 23, 2026
47 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-3fzya4 branch October 23, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants