Skip to content

feat(plugin-security)!: the security readers read the catalog and the activation ledger (ADR-0131 cutover stage 2a) - #22751

Merged
objectstack-fleet[bot] merged 54 commits into
mainfrom
claude/issue-15204-s2a-security-readers
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 54 commits into
mainfrom
claude/issue-15204-s2a-security-readers

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15204
Clause-②: yes

Stacked on stage 1. The base is claude/issue-15204-s1-position-permission-sets (PR #22723). GitHub retargets this PR to main when stage 1 merges, because merged head branches are deleted. Stage 1's branch was merged in at d0cab9f8 (its patch round 1 and its main merges); the diff below is against that head.

Size: 2,694 changed lines (+1,189 / −1,505, 35 files, against d0cab9f8), under the 3,000 line. Test deletions are included.

Stage 2a of the ADR-0131 cutover batch (stage plan 6094501866 row 2, split by claim amendment 6101897723). The plugin-security readers move onto the security catalog and the activation ledger. Stages 2b (plugin-auth, plugin-sharing) and 2c (the activation door for permission and position) are separate PRs. #15204 remains open after this merges.

What changes

resolvePermissionSets' row fallback goes (security-plugin.ts).

The delegated-administration gate (delegated-admin-gate.ts, ADR-0090 D12). This answers stage 1's Q1.

  • setsBoundToPosition reads the position's definition permissionSets and each set's adminScope from the catalog. The junction rows are not read. So the D12 containment check and the assignable-positions listing judge what an assignment actually grants, a binding declared only on the definition included.
  • A name the definition carries that the catalog does not hold is still judged: it must be on the delegate's allowlist. It starts granting as soon as a set of that name is authored.
  • A catalog read failure propagates and refuses the delegate's write. Before, a failure returned [], which approved the allowlist check.
  • positionIsDelegatable reads the definition's delegatable. describeDelegableScope lists the catalog's positions (anchors excluded), so a registry-only position is offered.
  • resolveOwnPosition is deleted: the catalog is one per deployment (feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 Q4 = A).
  • A direct grant's set is the catalog's definition of the name the grant carries. That was readGrantSetRows. A re-pointed grant reads its permission_set_id to a name (the grant id lookup), and then the catalog body.
  • Kept for stage 8-pre: the write gates on the sys_position_permission_set and sys_permission_set tables themselves (assertBindingWrite, assertSetAuthoring, positionById, loadSetRowById). They govern writes to tables that still exist; they are not readers of authority.

The explainer (explain-engine.ts).

  • "deactivated" is read through core's readDisabledCatalogNames, the resolver's own ledger read, so explain and enforce agree.
  • A grant's set is the catalog definition. A name the catalog does not hold is reported neither as expired nor as deactivated.
  • readGrantSetRows and isRowActive are no longer imported.

readGrantSetRows (grant-permission-set-name.ts) has no reader left and is deleted. What remains in the file is the name-stamping hook pair, which derives permission_set from permission_set_id, plus grantSetNameOf. That half goes with the id column (stage 8 or C7b).

The position-assignment refusal (position-catalog-refusal.ts).

Q (a): one holder of a position name per deployment (position-write-through.ts). Measured on this base:

  • S10 (feat(plugin-security)!: under single, a Setup position create or rename into a name a package or a built-in holds answers the metadata door's refusal (C2 stage S10) #22582) refuses a Setup create or rename into a name a package or a built-in holds.
  • The registry's one-holder rule refuses a second package.
  • The metadata door holds one definition per name.
  • The gap: under single, a Setup create, or a rename into a name, that an environment definition already held (another organization's Setup row, a metadata-door save, an app stack's declaration) went through. The write-through then overwrote that definition with the new row's label and description and an empty permissionSets.
  • The fix: that gap is now refused 409 UNIQUE_VIOLATION, the wire answer a second row of the name in one organization already gets, and the row write is undone. The error is a new in-package PositionNameConflictError, which stamps the same registered constants as PermissionSetNameConflictError.
  • The module is deleted in stage 7b. This executes the Q (a) ruling ("the write door refuses a second holder") on the door that exists until then.

@objectstack/core.

  • readDisabledCatalogNames is exported, so the explainer and the plugin share the resolver's one ledger read.
  • The catalog read's module doc and the index comment said deactivation lives on the catalog row, which stage 1 already changed. They now name the ledger. This is a comment-only correction.

Equivalence

  • grant-readers-by-name.golden.test.ts: the recorded golden is unchanged in all three postures. Its member now holds a set declared in the catalog and switched off in the ledger, instead of a row with active: false.
  • explain-enforce-parity: green, 211 tests together with explain-engine.
  • The delegated-admin pins (delegatable, adminScope containment, anchors, holdings per organization) are green on catalog-bound harnesses.
  • The resolver's resolve-authz-context.batch-equivalence.golden.json is untouched: core's resolver is unchanged apart from the export.

Tests

The fake engines and harnesses that bound no catalog now declare one. Most use the plugin-security testkit (bindCatalogFromTables), which now also carries adminScope and delegatable; the rest register items on the real registry. Deactivation fixtures use the ledger.

Deleted, because their premise retired with the per-organization catalog:

  • delegated-admin-gate-position-organization.test.ts: positions resolved per organization row. Its rule is replaced by delegated-admin-gate-catalog.test.ts and a rewritten block in delegated-admin-gate.test.ts.
  • The row-loader block of engine-find-bare-array.pin.test.ts.
  • The fix(security,sharing): materialize the RBAC catalog per organization #11121 per-organization row block of resolve-permission-sets-for-context.pin.test.ts.

Rewritten: permission-set-active.test.ts (the ledger is the switch, and the row is not read). REVERSED pin in orgless-position-name-fold.test.ts: an organization's row-only set resolves for nobody, its own active members included (#15196 Q3 = A).

Two dogfood fixtures authored their position or set as a row, and this PR is what stops rows from counting. They now declare it through the metadata door (PUT /meta/position/NAME, PUT /meta/permission/NAME):

  • delegation-of-duty: the gate read the row's delegatable.
  • showcase-scope-depth-fallback: the fallback set is requested by name, so the row loader used to serve it.

The /me/apps and FLS dogfood probes failed for the same reason on stage 1's own head; stage 1's patch round 1 moved them, and they are not touched here.

scripts/objectql-double-limit.baseline.json loses the permission-set-active.test.ts entry: the rewritten file's doubles are clean (ratchet down).

Ablations (scripts/ablation-replace.mjs, each restored byte-identical with git diff HEAD empty):

  1. Delete the dropDeactivatedFoldedSets call: permission-set-active 2 failed / 5 passed.
  2. Make setsBoundToPosition ignore permissionSets: delegated-admin-gate-catalog 3 failed / 5 passed.
  3. Remove the Q (a) insert refusal: position-write-through 3 failed / 48 passed.

Acceptance notes

  • Release-cut condition (6101876814) is unchanged by this PR. Setup's Deactivate still writes the row, and nothing writes the ledger for permission or position until stage 2c.
  • auto-org-admin-grant.ts reads sys_permission_set only to find the row id a grant's permission_set_id foreign key must name (required: true on that column), and the ids a revoke must reach. That is the grant id lookup that order correction 4 assigns to stage 8 or C7b, not an authority read. It is left as is.
  • security-plugin.ts still imports resolveOwnOrganizationRow and seedCtx, but only for bindBaselineToEveryone (retired by 6a, "only delete").
  • PermissionEvaluator.resolvePermissionSets reads the metadata list first and the catalog loader third. A name that both the metadata service and an ADR-0005 registry overlay answer takes the metadata body, while the resolver's catalog read takes the overlay. No such pair was measured in the repository's fixtures. This is recorded for stage 8, which retires the bootstrap list.
  • GRANTS_CACHE_WATCHED_OBJECTS still watches the three retired tables (stage 1's note; harmless over-invalidation).

Verification

All runs are local. Heavy runs ran under os-verify-lock, and each exit code was captured before any pipe.

On the final head 6d6c6543 (stage 1 at d0cab9f8):

  • plugin-security: 198 files, 4,129 passed / 45 skipped.
  • plugin-security typecheck green (test layer included).
  • The four dogfood files below: 23 passed.

On 4c9f967be (stage 1 at bf6bdf07; the merge since brought only stage 1's main merges):

  • core typecheck green.
  • mcp: 390 passed.
  • metadata-protocol: 28,101 passed.
  • dogfood: 239 files, 1,925 passed.

Before the first stage 1 merge (5a960f8ae, stage 1 at 83b54d84), these suites were green: plugin-auth, rest, runtime, organizations, service-automation, verify, cli (unit), plugin-approvals, hono, example-crm, example-showcase and cloud-connection. The core golden (resolve-authz-context.batch-equivalence + security-catalog) was also green, 50 passed.

Gates, on 6d6c6543: dispatch-gates --commands derived 134 families, and --ran with recorded exit codes reports 134 run, 0 NOT-MEASURED. One family exits 1: check:platform-checklist, with 4 ABSENT SYMBOL anchors in metadata-protocol/src/protocol.ts and service-storage/src/attachment-access-hooks.ts. Stage 1 measured the identical findings on main, and this diff touches neither file.

Fixed while sweeping, each re-run green:

  • check:undeclared-dep-imports: an @objectstack/objectql import, a devDependency here, replaced by the in-package error.
  • check:objectql-double-limit and check:where-matcher: the test doubles.

The teardown-shape self-test needed its pinned fixture commit, fetched by sha (git fetch origin 621a4876…). check:skill-examples and check:dual-build-cjs-loads needed their packages built first. All three were re-run green.

The repo-wide pnpm lint was not run locally.


Generated by Claude Code

claude added 30 commits October 10, 2026 08:31
…d bodies from the catalog

Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
…declared at kernel:ready

Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
…ledger regenerated

Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
…ssionSets through the write-through

Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
…activation ledger

The resolver honours ADR-0049 deactivation through sys_metadata_activation
(types position and permission) and no longer reads the catalog row's active
column. convertDeactivatedCatalogRows turns existing active=false rows into
ledger rows for the upgrade ceremony; it is not wired into boot. A composition
whose registry has no ledger object issues no ledger read.

The admin-standing surface now derives from the ledger; the last-admin guard
refuses a ledger write that switches admin_full_access off. The name-fold
warning names permissionSets as the governed remedy.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
The resolver reads deactivation from sys_metadata_activation; the install
fixtures' grant maps answer it empty, as they answer the other authz tables.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…-position-permission-sets

# Conflicts:
#	packages/plugins/plugin-security/src/security-plugin.ts
The merged capability declarations are told apart by _packageId; the test
registry now stamps it as the engine registry does.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…Sets; the ledger-guard test double holds the caller's bound

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
… zh-CN, ja-JP and es-ES

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…tion ledger (stage 2a, WIP)

Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN
Co-authored-by: Claude <noreply@anthropic.com>
…per-organization position pins retire

Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN
Co-authored-by: Claude <noreply@anthropic.com>
…h deactivation through the ledger

Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN
Co-authored-by: Claude <noreply@anthropic.com>
…ermission-sets' into claude/issue-15204-s2a-security-readers
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/plugin-security, touching 44 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/core/src/security/index.ts, packages/core/src/security/security-catalog.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/core/src/security/index.ts, packages/core/src/security/security-catalog.ts) — pages documenting those are invisible to this run
  • 5 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 — 35 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 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27

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

…on-assignment refusal name the ledger and the security catalog

Claude-Session: https://claude.ai/code/session_01VHtcG1CjvZzd7U6F9Yg8oN
Co-authored-by: Claude <noreply@anthropic.com>
…a-security-readers

# Conflicts:
#	packages/core/src/security/resolve-authz-context.ts
#	packages/core/src/security/security-catalog.ts
#	packages/plugins/plugin-security/src/__tests__/security-catalog.testkit.ts
#	packages/plugins/plugin-security/src/explain-engine.test.ts
#	packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts
#	packages/plugins/plugin-security/src/security-plugin.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d5af7e19aaad61e1c154c1c977d237b9c06fc1bc
Local-runs: none

Contract-tier review of PR #22751 (stage 2a of the #15204 cutover batch, Clause-②: yes), rendered at 2026-10-11T02:22Z from the card's body and all 53 comments, the PR body, its 37-file list and its net diff against main (bfc15d27, stage 1's squash, the base GitHub retargeted the PR to), and the check-runs on the head once they had all settled: 35 runs, 33 success, 2 roster skips (Console Pin Gate, Packed-tarball smoke), none failing; Check Changeset, Check PR Size, Lint & Repo Gates, every Test Core and Dogfood Regression Gate shard, and the four Type Check runs are among the 33. The net diff is the one first read on 6e99c2bf (35 files) plus the dev's docs commit 738026f6 (two pages under content/docs/permissions/) and the merge of main (d5af7e19); a line-by-line comparison of the two net diffs shows the two docs files and nothing else, so the report's verification on 6d6c6543 / 4ffaf170 (one changeset line and the two docs pages away) carries to this head. Size against main: 37 files, +1,192 / −1,507, 2,699 changed lines, under the 3,000 line (Check PR Size green). Nothing was built, run or re-run; the few facts the diff implies but does not show were read from the head tree over the contents API and are named where they decide a judgment.

① Derived judgments

  1. sys_user_position.position, the assignment door (position-catalog-refusal.ts) — right. The predicate moves from "a sys_position row the writer's context reaches" to "the security catalog resolves the name" (securityCatalogReaderOf(ql).resolve('position', name)), and takes no organization. Widening: a position declared only in a package, a built-in, or one saved through the metadata door is accepted (it was 400). Narrowing: a name only a row carries is refused with the same envelope a dangling name gets (400 VALIDATION_FAILED, reference_not_found), so the verdict is still no existence oracle. A deactivated position is still accepted (plugin-security: the write path accepts a sys_user_position row whose position names no sys_position catalog row — 201 with nothing resolvable (RE-CUT: the originally reported resolution-path defect is disproved, see the 2026-09-09 measurement) #16712's predicate; the refusal reads no ledger). The fail-open stance is unchanged in kind (no reader bound, or a read that throws: warn, the write proceeds). The id-spelling hint still reads the row by id under the writer's own context and names it only when the catalog resolves that name. Pinned: builtin-positions.boot (row-only refused, stack-declared accepted, in both postures), position-catalog-refusal (registry-only accepted; namesWithoutCatalogRow answers the same with and without an organization; the placeholder-shaped name case kept on a registry item). The published statement of this door, system-context.mdx row 23b, is rewritten on this head to the new predicate (the catalog "takes no organization, so a name only a sys_position row carries — any organization's — is refused like one nobody carries"); the row's anchor still resolves. Right.

  2. The plugin's third set source (security-plugin.ts) — the direction is right by inspection; its pin is gone and the diff says otherwise. FAIL reason. The per-organization sys_permission_set row loader (dbLoaderFor, callerOrganizationId, permissionSetPageOrRefuse) is replaced by a catalog loader that returns the catalog's definition whole; environment-level, so one loader serves every caller. Narrowing: a row-only set's object and field permissions stop applying (the resolver already granted nothing through it, feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 Q3 = A; the REVERSED pin in orgless-position-name-fold says so). The loader has no catch, createSecurityCatalogReader.resolve throws AuthzStoreUnavailableError for a read that did not answer (unreadable() in core's security-catalog.ts), and PermissionEvaluator.resolvePermissionSets catches one frame up (permission-evaluator.ts:551: the unresolved sets grant nothing, the [security][observability] resolvePermissionSets swallows dbLoader failures silently — custom permission sets vanish with no log #2565 db lookup failed warn fires). That is the right direction. But the only pins of it were block 7 of engine-find-bare-array.pin.test.ts (five cases: a thrown read propagates, a non-array and a non-row page are refused, the request stays fail-closed through the evaluator and the warn re-arms), and this diff deletes them; the rewritten header says the fault direction "is pinned on the catalog loader in security-readers-catalog.test.ts", and no file of that name exists on this head (contents API 404; the head tree holds none). permission-evaluator.test.ts holds no loader case, and the only throwing-catalog case in this diff is the delegated-admin gate's (①.4), not the loader's. So the enforcement-plane fault direction of the third source lands unpinned, with a comment that names a pin that is not there, which is the "declared, not delivered" shape the deleted block existed to close.

  3. Deactivation at the plugin's resolution (dropDeactivatedFoldedSets) — right, with one residual recorded. The row loader's isRowActive over every row it loaded becomes one readDisabledCatalogNames read, issued only for POSITION names folded into the request that resolved as sets and were judged by neither context.permissions nor the baseline. Since stage 1 the resolver reads the same ledger (§6b) before building permissions, so this path and enforcement cannot disagree, and a request with no collision issues no read. Pinned by permission-set-active (the reachability case; the 0/1 shape; another type's ledger row switches nothing; a row's active: false is not read; no ledger read for a resolver-judged name or for a fold-free request) and ablation 1. Residual: a hand-built context handed to resolvePermissionSetsForContext with a ledger-off name in permissions now resolves it, where the old loader dropped it on the row flag; the production path's permissions is the resolver's output, so no wire door widens. It belongs to seat Q3's carrier (stage 8, the three-source order).

  4. The delegated-administration gate (ADR-0090 D12; stage 1's Q1 → A executed here) — right. setsBoundToPosition reads the definition's permissionSets and each set's adminScope from the catalog, never the junction; a name the definition carries that the catalog does not hold is still judged (it must be allowlisted, so this refuses more, never less); a catalog read failure now propagates and refuses the delegate's write, where [] approved it before (narrowing, in the safe direction). positionIsDelegatable reads the definition's delegatable, fail closed on none or unreadable. describeDelegableScope lists the catalog's positions (anchors excluded; a registry-only position is offered; a position whose binding cannot be read is not; no catalog, nothing). resolveOwnPosition is deleted (one catalog per deployment, feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 Q4 = A). A direct grant's set is the catalog's definition of the grant's name (catalogSet, null on failure, which refuses a delegate as unresolved; a tenant administrator is not judged); a re-pointed grant reads its id to a row name (the grant id lookup, order correction 4's, kept) and then the catalog body. The write gates on the junction and set tables (assertBindingWrite, assertSetAuthoring, positionById, loadSetRowById) stay: they govern writes to tables that still exist and read no authority. Pinned by the new delegated-admin-gate-catalog (eight cases, the unreadable-catalog refusal included), the rewritten block in delegated-admin-gate.test.ts, the holding-organization suite on a bound catalog, and ablation 2. Deleting delegated-admin-gate-position-organization.test.ts is right: it pinned a per-organization row read that no longer exists.

  5. The explainer (explain-engine.ts) — right. "deactivated" is read through core's readDisabledCatalogNames for held positions and for the sets of window-active grants, so explain and enforce read one switch; a grant naming a set the catalog does not hold is reported neither expired nor deactivated. isRowActive and readGrantSetRows leave the module. Pinned in explain-engine.test.ts (ledger-off reported whatever the rows say; row-off with no ledger row in effect and silent; a catalog-less name silent) and the parity suite the report names. The published pointer in authorization.mdx now names the sys_metadata_activation row and says the explainer reads the resolver's ledger (the docs commit). Right.

  6. Deleted and added internals — no public-surface change. readGrantSetRows and GrantSetRowEngine (grant-permission-set-name.ts), PermissionSetReadUnansweredError (removed) and PositionNameConflictError (added) in errors.ts: the plugin's index.ts on this head exports only PermissionDeniedError and isPermissionDeniedError from errors.js, DelegatedAdminGate and isTenantAdmin, explainAccess and buildContextForUser from the touched modules, and the package's exports map is the single . entry. The errors.test.ts FLOOR roster is updated to match. Right.

  7. sys_position under single (ruling Q (a), position-write-through.ts) — right. A Setup create, or a rename into a name, that the catalog already resolves to a definition no package or built-in holds is refused 409 UNIQUE_VIOLATION (PositionNameConflictError, stamping the constants PermissionSetNameConflictError stamps, so no second spelling of the code), and the row write is undone; a catalog that cannot be read refuses, rows undone. It runs inside the write-through's existing stand-downs (isSystem, a walled posture, no metadata door, lines 428–434 on the head), so it is single-only and never touches a system or seeder write; S10's 403 NOT_OVERRIDABLE for a package or built-in name is asked first. This executes "the write door refuses a second holder" on the door that exists until 7b; the module is not on batch Release version 0.4.0 #310 item 4's list (seat Q4 → A). Pinned by five cases (two organizations, the metadata-door holder, the rename, a control, the unreadable catalog) and ablation 3.

  8. @objectstack/core — right. readDisabledCatalogNames is exported (an addition to the published surface, judged at ②); the security-catalog.ts module doc and the index comment now name the ledger as the switch, which stage 1 made true. Comment-only otherwise.

  9. Test and fixture surface — right. __tests__/security-catalog.testkit.ts carries admin_scope and the row's delegatable onto the definition (test-only). The two dogfood fixtures declare their position and set through PUT /meta/position/NAME and PUT /meta/permission/NAME as the platform administrator, today's authoring: [Decision] 切换后谁能在 Setup 里新建/编辑岗位和权限集:保持今天的权限(持 manage_metadata 者),还是放开给组织管理员(安全边界) #22621 → A is untouched. scripts/objectql-double-limit.baseline.json ratchets down one entry.

  10. The dispatch's exclusions (6101897723) hold. No transition piece: the row fallback, the junction read and resolveOwnPosition are gone, and the one row read kept by name is order correction 4's grant id lookup. No edit to a retired module: none of the seven seeders, permission-set-projection.ts or per-organization-catalog.ts is in the file list, and security-plugin.ts's edits are the loader hunk and resolvePermissionSetsForContextUnmemoized, not stage 3's kernel:bootstrapped region (the single-writer-path check is green). No change to who writes the catalog. Nothing under content/docs/releases/; the two docs edits sit under content/docs/permissions/.

  11. Published pages — right on this head, one sentence left to its carrier. The docs commit rewrote the two statements this diff had falsified (system-context.mdx row 23b, ①.1; the explainer's pointer in authorization.mdx, ①.5). The other permissions pages read on the head (positions.mdx, permission-sets.mdx, delegated-administration.mdx, explain.mdx) state no row read this diff retires. One sentence remains loose: authorization.mdx's closing paragraph on deactivation says the write gates that judge "a delegated administrator's blast radius keep reading every row, deactivated included"; after ①.4 that gate reads the catalog's definitions, and the ledger is still not consulted there, so the sentence's point (deactivated included) holds while its noun does not. Prose only; it goes with the stage that next touches the page.

② Semver level

.changeset/15204-s2a-security-readers.md: @objectstack/core: minor is right for an additive export (readDisabledCatalogNames) with comment-only changes beside it. @objectstack/plugin-security: minor, BREAKING (enforcement stops reading sys_permission_set rows; a row-only set or position confers nothing; a 409 at the Setup door), under the launch-window convention stage 1 shipped at the same level; check-changeset-no-major is green on the head. FROM → TO pairs are present for each of the five behaviour changes (set resolution, the D12 gate, the explainer, the assignment door, the one-holder refusal) and for the core export. The ADR-0087 marker reads not-required (already-registered position-permission-sets-declared) with its sentence: the disposition seat Q2 → A ruled; Check Changeset is green on this head (its red on the superseded 6e99c2bf came from the retarget, not the file). Packages touched with no changeset are right: packages/qa/dogfood is private QA tooling, scripts/ is repo tooling, and content/docs/ ships no package. No shipped TSDoc the diff touches contradicts the changeset.

Clause-②: yes — right. The sys_user_position door's accept set widens (a registry-only position is accepted), which is the widening the dispatch pre-declared for 2a; each narrowing arm (a row-only name refused; a row-only set's permissions no longer applied; a delegate's write refused on an unreadable catalog; 409 at sys_position) is declared FROM → TO. The line sits on the PR body's second line and in the changeset.

③ Boundary flags

Dev flags (os-dev-report 6103972024), each answered on the card by the seat (6103991518) inside a ruling the card already carries:

  • Q1, the stacked PR → A: kept; GitHub has since retargeted it to main on stage 1's merge (6104351995), and the merge round is on this head. Answered.
  • Q2, the ADR-0087 disposition → A, spelled already-registered: the marker on this head; the gate is green. Answered.
  • Q3, the evaluator's three-source order → A, carrier stage 8. Answered; ①.3's residual rides the same carrier.
  • Q4, the Q (a) refusal in position-write-through.ts → A: not on batch Release version 0.4.0 #310 item 4's list, retires with 7b. Answered; ①.7 agrees.

Deviations: the core export and comment corrections (answered by the changeset and ①.8); the dogfood fixtures and the baseline ratchet (①.9); the PR base (Q1). premise_parts_not_valid: auto-org-admin-grant.ts is left as is, its reads being the grant id lookup order correction 4 assigns to stage 8 or C7b, with the walled auto-grant's missing-row consequence carried to 6c / 8 by the seat; Q (a) was mostly held already and the environment-holder gap is what was built (①.7). Answered.

Out-of-scope findings, each with a named carrier the seat accepted: check:platform-checklist red on main (already reported, not in this diff); the walled auto-grant target (stage 6c / 8); the core/row-active.ts comment (stage 2b, which its ACCEPT 6104535364 has since moved to 2c's merge round).

Items the landing comment (6104351995) owes this PR's merge round: the docs commit is on this head; the size addendum is the dev's to post (the size is stated above from the PR's own counts).

Escalated by this record:

  • E1, the verdict's reason, to a patch round on this PR: pin the third source's fault direction on the catalog loader (a read that throws propagates out of the loader; the evaluator keeps the request fail-closed and the db lookup failed warn fires), in the file the engine-find-bare-array.pin.test.ts header names, or correct that header to the pin's real home. The ablation discipline the report applied to three other claims applies here too.
  • E2, no action here: the authorization.mdx sentence at ①.11 goes with the stage that next touches the page.
  • The release-cut condition (6101876814, extended in 6102862135 and restated in 6104351995) is unchanged by this PR: nothing in the diff writes sys_metadata_activation for permission or position outside test fixtures; 2c carries the door.

Implemented-by: session_01VHtcG1CjvZzd7U6F9Yg8oN
Reviewed-by: session_01Rerax7QTjKMPCUZxQUtPFR

VERDICT: FAIL

One reason, ①.2 / E1: the enforcement-plane fault direction of the plugin's third set source lands unpinned, and the diff's own comment names a pin that does not exist on this head. Every other judgment holds; a PASS record is owed on the head that restores the pin.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

os-dev-report addendum (stage 2a, PR #22751): the merge round after stage 1 landed · session_01VHtcG1CjvZzd7U6F9Yg8oN

Head: d5af7e19aa. It merges origin/main at bfc15d275b (stage 1's squash, #22723) through scripts/pm/os-regen-merge.sh, with no rebase and no force-push. Its parents are 738026f662, the docs commit (pushed in this round), and bfc15d275b.

  • Six non-generated files conflicted. Each was double-counted: stage 1 appears once as its own commits, already in this branch, and once as the squash on main. They are core/security/resolve-authz-context.ts, core/security/security-catalog.ts, and in plugin-security __tests__/security-catalog.testkit.ts, explain-engine.test.ts, grant-readers-by-name.golden.test.ts and security-plugin.ts.
  • main's copy of each was measured byte-identical to stage 1's tip 23d3545e, which this branch already builds on: git diff 23d3545e64 origin/main is empty for all six. So the branch side was taken, and every resolved file was checked to equal HEAD's blob. Nothing from main is lost.
  • The script's rerun took main's side of the generated paths, which left nothing to commit.
  • The diff against main contains only this stage's 37 files.

Size against the new merge base: 2,699 changed lines, +1,192 / −1,507, 37 files. That is the 35 files measured before, plus the two docs pages below. Generated files: none. This is the size of record.

ADR-0087 marker: kept in the gate's own form, not-required (already-registered position-permission-sets-declared) …. The entry is now on main.

Docs edits (commit 738026f662). I ran node scripts/docs-audit/affected-docs.mjs against stage 1's tip. It lists 32 pages; 11 are content/docs/releases/**, which are release-owned and not edited here. The remaining 21 pages were read:

  • api/error-catalog
  • concepts/metadata-lifecycle
  • data-modeling/drivers
  • data-modeling/objects
  • deployment/environment-variables
  • deployment/validating-metadata
  • permissions/authentication
  • permissions/authorization
  • permissions/delegated-administration
  • permissions/explain
  • permissions/field-level-security
  • permissions/index
  • permissions/permission-metadata
  • permissions/permission-sets
  • permissions/permissions-matrix
  • permissions/positions
  • permissions/sharing-rules
  • permissions/system-context
  • plugins/packages
  • protocol/backward-compatibility
  • ui/forms

Two sentences were false, and both are fixed:

  1. permissions/authorization.mdx, the explain-engine paragraph. "…with no pointer to the catalogue row somebody switched off" now reads "…with no pointer to the sys_metadata_activation row somebody switched off — the explainer reads the same ledger the resolver does, so explain and enforce agree." Nothing else in the paragraph changed.
  2. permissions/system-context.mdx, census row 23b. It said the assignment refusal judges the sys_position rows the writer's organization context reaches. It now says the refusal judges the security catalog (the environment registry, one per deployment, taking no organization), and that a name only a sys_position row carries is refused like one nobody carries. The anchor is unchanged.

The four pages named for re-reading were checked again against main's post-stage-1 text, and each still reads true:

  • authorization: the delegatable: true opt-in now lives on the definition; the "write gates … keep reading every row" paragraph is about assignment rows and anchor bindings, which the gate still reads unfiltered.
  • delegated-administration: the allowlist rule, the describeDelegableScope section and the "every set it distributes" rule describe behaviour, not a store.
  • positions and permission-sets: they already say a junction row grants nothing.

Gates on d5af7e19aa. Each exit code was captured before any pipe. dispatch-gates --commands derived 108 families against main; --ran with the recorded codes reports 108 derived, 108 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.

  • check:platform-checklist is green now that it runs against main.
  • check:skill-examples and check:dual-build-cjs-loads first refused for missing builds (exit 3). After building their 8 packages, both exited 0.
  • pnpm --filter @objectstack/spec check:generated: exit 0.
  • check:doc-authoring, check:doc-anchors, check:system-context-census (118 of 118 symbols) and check:nul-bytes, all exit 0, were run on the docs commit before the merge.

Suites on d5af7e19aa (all under os-verify-lock):

  • core: typecheck exit 0; src/security, 31 files, 775 passed.
  • plugin-security: typecheck exit 0 (test layer included); 198 files, 4,129 passed / 45 skipped.
  • dogfood: 239 files, 1,925 passed / 9 skipped.

The PR stays a draft.


Generated by Claude Code

…closed warn path; the warn names the catalog

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

Copy link
Copy Markdown
Contributor Author

os-dev-report addendum 2 (stage 2a, PR #22751): contract-review patch round 1, for record 6104579158 ①.2 / E1 · session_01VHtcG1CjvZzd7U6F9Yg8oN

Head: 07a471675c. It adds two commits on d5af7e19aa:

  • 4dc0454ef4: the pin, the warn text, and the docs sentence.
  • 07a471675c: the engine-double ledger entry.

Both commits were reworded before their first push, so the pre-push hook would accept them. No published commit was rewritten, and the pushed tree is byte-identical to the one the runs below measured.

E1, the catalog loader's pin, is restored (option 1). It is the new file packages/plugins/plugin-security/src/security-readers-catalog.test.ts. The header comment in engine-find-bare-array.pin.test.ts names this file, and the file now exists. It has 8 tests, which keep block 7's intent against the post-2a loader:

  • the catalog loader — what it answers:
    • "a set the catalog holds comes back WHOLE, and no sys_permission_set row is read"
    • "a name the catalog does not hold answers nothing — an EMPTY answer is a real answer"
    • "a set only a sys_permission_set ROW carries resolves nothing, and the row is never read"
    • "a name asked twice is answered once"
    • "an engine with no catalog bound answers nothing, never a guess"
  • a catalog read that did not answer:

The warn now names the catalog. PermissionEvaluator's warn read "sys_permission_set db lookup failed", which points an operator at a store the loader no longer reads. It now reads "permission-set catalog lookup failed — unresolved sets grant nothing this request". Its existing assertion in security-plugin.test.ts follows, and the evaluator's two stale doc lines about the loader querying sys_permission_set are corrected (comments only).

Ablation (scripts/ablation-replace.mjs). I made the loader swallow its read failure: catalog.resolve('permission', name).catch(() => undefined). Then security-readers-catalog.test.ts reports 2 failed / 6 passed: the propagation pin and the fail-closed + warn pin. Restore check: blob == HEAD, git diff HEAD empty.

Docs sentence (content/docs/permissions/authorization.mdx, the last paragraph of the deactivation section).

  • Was: "…the write gates that judge audience-anchor bindings and a delegated administrator's blast radius keep reading every row, deactivated included: dropping rows there would make…"
  • Now: "…the write gates that judge audience-anchor bindings and a delegated administrator's blast radius do not consult the activation ledger: they judge every binding and assignment in front of them — including the permission sets a position's definition names — whether or not the ledger switched it off, because skipping a switched-off one would make a refused binding permitted and a delegate's boundary narrower, which is the opposite of switching access off."

Runs. All suites ran under os-verify-lock, with exit codes captured before any pipe.

  • plugin-security typecheck: exit 0.
  • plugin-security suite: 199 files, 4,137 passed / 45 skipped. That is the earlier 198 files and 4,129 tests, plus the new file's 8.
  • Derived gates against main: --ran with the recorded codes reports 108 derived, 108 run, 0 NOT-MEASURED, all exit 0.
  • The sweep's one red was check:engine-double-contract: the new pin's findOne double was not yet in the pinned ledger. I ran the gate's own --write, which added one entry to scripts/engine-double-contract.pinned.json, and the re-run exited 0.
  • check:doc-authoring, check:doc-anchors and check:nul-bytes: all exit 0 on the final tree.

Size against the merge base: 2,902 changed lines, +1,384 / −1,518, 41 files. That is the 37 files before, plus the new pin, permission-evaluator.ts, security-plugin.test.ts and the ledger JSON. The ledger JSON's +5 is the only line written by a generator.

The PR stays a draft.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 047a8771388ac7e45b7d151d5fb53df959c7bf45
Local-runs: none

Contract-tier review of PR #22751 (stage 2a of the #15204 cutover batch, Clause-②: yes), rendered at 2026-10-11T03:33Z. It supersedes record 6104579158 (FAIL on d5af7e19, one reason: the third set source's fault direction unpinned, with a comment naming a pin that did not exist). Inputs: the card's body and all 60 comments, the PR body (unchanged since the first record), its 41-file list, its net diff against main (base 149294c0, which carries stage 1, stage 2b e7e6544a and stage 3), the dev's addenda 6104601971 and 6104794019, and the check-runs on this head once they had all settled: 35 runs, 33 success, 2 roster skips (Console Pin Gate, Packed-tarball smoke), none failing; Check Changeset, Check PR Size, Lint & Repo Gates, every Test Core and Dogfood Regression Gate shard, the four Type Check runs and Temporal Conformance are among the 33 (the cancelled type-check lanes on the superseded 07a47167 are not runs on this head). The addendum names 07a47167; the head is one commit further, the merge of origin/main the landing record 6104351995 asked for, and GitHub's test-merge commit for this head (b36770da) holds the same authorization.mdx as the head, so the sentences judged below are the ones that land. Since this head was pushed, main has also taken stage 6b-1b (7aa8d51c, 6104947165); GitHub still reads the PR mergeable and clean, and the queue's merge is the seat's landing step, outside this record. Size against main: 41 files, +1,384 / −1,518, 2,902 changed lines, under the 3,000 line. Nothing was built, run or re-run; the facts the diff implies but does not show were read from the head tree over the contents API.

What changed since the FAIL record. A line-by-line comparison of this head's net diff with the one judged on d5af7e19 shows four files added to it and one page edited further, and no other content change (the remaining differences are hunk offsets in security-plugin.ts, where main's stage 3 added lines above the loader hunk):

① Derived judgments

Judgments 1 and 3 to 11 of 6104579158 carry over unchanged: the delta touches none of their code, and the pages they read are the same on this head except where named below. Restated here only where the delta moves them.

  1. The plugin's third set source (security-plugin.ts) — right, and the E1 reason is closed. The pin the FAIL asked for exists on this head, in the file the engine-find-bare-array.pin.test.ts header names. It boots the real SecurityPlugin over a double engine whose registry answers or throws, takes the private catalogSetLoader, and holds the loader's three answers apart: a catalog-held set comes back whole with no sys_permission_set read (rowReads stays 0); a name the catalog does not hold, a row-only set, and an engine with no catalog bound each answer [], with the row never read; a name asked twice is answered once; a registry read that throws leaves the loader as AuthzStoreUnavailableError, asserted by instance and by core's exported code and status constants, never as "no such set"; and through PermissionEvaluator.resolvePermissionSets the same failure resolves [] with exactly one warn naming the catalog and the unresolved name, beside a control where the catalog answers and nothing warns. That is block 7's intent on the post-cutover read (the non-array and non-row page cases have no page to apply to). The report's ablation (the loader made to swallow its read failure) turns exactly the two fail-closed cases red, 2 failed / 6 passed, and the comment now points at a file that is there. The enforcement-plane fault direction is pinned, and the diff says where. Right.

  2. The [security][observability] resolvePermissionSets swallows dbLoader failures silently — custom permission sets vanish with no log #2565 warn text (permission-evaluator.ts) — right; not a contract surface, no changeset line owed. [security] sys_permission_set db lookup failed — unresolved sets grant nothing this request becomes [security] permission-set catalog lookup failed — unresolved sets grant nothing this request. It is a logger line, not an error envelope, an export or a wire shape; its meta (unresolved, error) is unchanged; no permissions page quotes it, and the repository's own assertion on it (security-plugin.test.ts) is updated in the same commit. The old text pointed an operator at a table the loader no longer reads, so the change is a correction the cutover owed. The two evaluator doc comments now say the loader is the plugin's catalog loader; comments only. A changeset sentence would be harmless but is not required, and the changeset is unchanged on this head.

  3. The published page (authorization.mdx), read on this head where 2a's and 2b's edits sit in one tree — right. The head-versus-main diff of the page is exactly 2a's two edits: the explainer pointer (6104579158 ①.5) and the closing sentence, which now says the audience-anchor and delegated-administration write gates "do not consult the activation ledger: they judge every binding and assignment in front of them — including the permission sets a position's definition names — whether or not the ledger switched it off". That is what the code does after ①.4 (the D12 gate reads definition permissionSets and adminScope from the catalog and never the ledger; the anchor gate judges the junction write it is handed). 2b's paragraphs in the same section ("Absent is ACTIVE": the last-administrator guard reads the catalog and the ledger and no row; the break-glass switch-off refused only while it would leave no administrator) describe other readers and do not contradict these two sentences; the section reads as one account of the ledger. GitHub's test-merge copy equals the head's, so this is the text that lands. The prose carrier noted in 6104579158 ①.11 / E2 is thereby closed.

  4. The pinned-ledger entry (scripts/engine-double-contract.pinned.json) — right. One entry, { file: security-readers-catalog.test.ts, verb: findOne, pinned: 1 }, in the ledger's own shape beside its neighbours: the new double declares findOne through assertEngineFindOnePredicate, and the gate's --write recorded it (the report's one red in the sweep, green on re-run). These five lines are the only generator-written lines in the PR.

② Semver level

Unchanged from 6104579158, and the changeset file is byte-identical on this head: @objectstack/core: minor (an additive export, readDisabledCatalogNames), @objectstack/plugin-security: minor BREAKING under the launch-window convention stage 1 shipped at the same level, FROM → TO pairs for each behaviour change, the ADR-0087 marker not-required (already-registered position-permission-sets-declared). The delta adds a test file, a log-line correction (①.12, no line owed), comment fixes, a generated ledger entry and a docs sentence: none moves the level and none adds a published surface. check-changeset-no-major and Check Changeset are among the green runs on this head. Packages touched with no changeset remain right: packages/qa/dogfood (private QA), scripts/ and content/docs/.

Clause-②: yes — right, as before: the sys_user_position door's accept set widens (a registry-only position is accepted), the dispatch pre-declared that widening for 2a, and each narrowing arm (a row-only name refused; a row-only set's permissions no longer applied; a delegate's write refused on an unreadable catalog; 409 at sys_position) is declared FROM → TO. The line sits on the PR body's second line and in the changeset.

③ Boundary flags

The dev's four open_questions (6103972024) stand answered by the seat (6103991518), as recorded in 6104579158; the deviations, the premise_parts_not_valid items and the three out-of-scope findings keep their carriers (the core/row-active.ts comment now with 2c's merge round, per 6104535364). Nothing in addendum 6104794019 raises a new question: it answers E1 by option 1 (restore the pin), names the warn correction, the ledger entry and the docs sentence, and reports the suite at 199 files, 4,137 passed / 45 skipped, 108 of 108 derived gates run against main, the ablation, and the size.

Closed by this head: E1 (①.2) and E2 (①.13). Open carriers unchanged: ①.3's residual with seat Q3 (stage 8). The release-cut condition (6102862135, as 6104692060 extends it for objectui's spec pin) is unchanged by this PR: nothing in the diff writes sys_metadata_activation for permission or position outside test fixtures; 2c carries the door.

Implemented-by: session_01VHtcG1CjvZzd7U6F9Yg8oN
Reviewed-by: session_01Rerax7QTjKMPCUZxQUtPFR

VERDICT: PASS

The one reason of 6104579158 is closed on this head, every other judgment of that record holds, and the four delta items are each judged right above.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 03:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

os-dev-report addendum 3 (stage 2a, PR #22751): the main merge after 2b · session_01VHtcG1CjvZzd7U6F9Yg8oN

Head: 047a877138. It merges origin/main at 149294c02c through scripts/pm/os-regen-merge.sh, with no rebase. That brings in 2b (#22745, e7e6544ab9) and stage 3 (#22671). The merge was clean: no conflicts, and no generated path to restore.

The patch-round-1 content (07a471675c, addendum 2) is unchanged: the restored catalog-loader pin security-readers-catalog.test.ts, the warn text, and the authorization.mdx sentence.

authorization.mdx read against 2b's rewritten "Grant lifecycle: the active switch" section. The two stages agree, and no further text change was made:

  • 2b's paragraph says the last-administrator guard reads the catalogue for admin_full_access and the ledger, never a row's active.
  • This stage's explain sentence names the sys_metadata_activation row the explainer reads, which is the same ledger the resolver reads.
  • This stage's write-gate sentence says those gates do not consult the ledger and judge every binding and assignment in front of them.

Runs on 047a877138. Each ran under os-verify-lock, with exit codes captured before any pipe.

  • Build closure: exit 0.
  • plugin-security typecheck: exit 0.
  • plugin-security suite: 200 files, 4,150 passed / 45 skipped. That count includes stage 3's tests.
  • plugin-auth, 2b's package: 136 files, 2,769 passed / 10 skipped.
  • dogfood, which stage 3 also touches through the boot: 241 files, 1,954 passed / 9 skipped.
  • Derived gates against main: --ran with the recorded codes reports 108 derived, 108 run, 0 NOT-MEASURED, all exit 0.

Size against the merge base: 2,902 changed lines, +1,384 / −1,518, 41 files. Unchanged by this merge.

PR state: #22751 reads draft: false now. I did not flip it, so I have left it as I found it.


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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants