fix(runtime): the /keys mint and install-wide activation gates classify a tenancy resolution failure instead of reading it as "no wall" - #16385
Conversation
…ctivation-write gates Both gates derived the effective tenancy posture through `DomainHandlerDeps.resolveService`, the dispatcher's capability PROBE, whose fallback chain absorbs every rejection at every step and answers `undefined`. So a `tenancy` service that was registered and FAILED to build arrived at both gates as the same value a deployment that never registered one produces, and both gates read that as "there is no wall": `POST /keys` minted an organization-less key it would otherwise refuse, and an organization administrator's install-wide activation write was served instead of refused. Measured on the pre-fix tree with a real `ObjectKernel` whose `tenancy` is registered through a throwing factory: the mint door answered 201 with one row written and the raw secret echoed once, and the activation door answered 200 with `setActionActive` called. The identity step already reads this fact through the classified lookup (`resolveServiceOrLoud`, #13906 decision 1 option A). That made one failure answer 503 at the identity step and admit at these two gates in the same deployment, so "what is this deployment's state on the wall question" had two answers at once. The gates now read the same classification: never registered stays quiet and behaves exactly as before, every other resolution failure is re-raised as `AuthzStoreUnavailableError` (503 `SERVICE_UNAVAILABLE`). `resolveService` keeps its probe contract for every other name and every other domain; the classified read is a second, opted-into deps facility, so no gate that was not named here changes behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…failure Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…nancy-posture-loud-resolve
…nd honour the caller's bound `check:engine-double-contract` and `check:objectql-double-limit` both read the new fixture. The `update`/`delete` verbs were copied from a sibling and no door under test reaches either, so they were coverage nobody was getting and would have owed the retained ledger two rows for pins that can never fire; they are removed rather than pinned, and the ledger is untouched (744 rows held, none added). The `find` double now applies the caller's `limit` by presence and after the filter, so it cannot answer a page the producer would not have returned. `check:system-context-census` line rot from the gate edit is repaired by the gate's own `--fix`: two anchors in the elevation-read page move with the lines they cite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…nancy-posture-loud-resolve
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 58f0526cce4dcf83fddedbaf4d201105fcfbbce5 && git checkout 58f0526cce4dcf83fddedbaf4d201105fcfbbce5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4998efa71773154561c471075f4ef12566ecc455 0795550f3574c35bd100ac3dbffdee4b522fb717 && git checkout -B drift-repro 4998efa71773154561c471075f4ef12566ecc455 && git merge --no-ff 0795550f3574c35bd100ac3dbffdee4b522fb717
node scripts/docs-audit/affected-docs.mjs --json 4998efa71773154561c471075f4ef12566ecc455
|
|
VERDICT: CHANGES REQUIRED Independent adversarial contract review of head Clause ② — the headline. The declaration should be
|
Contract review ADOPTED — CHANGES REQUIRED, at tier, verbatim. Carriers now hung; round 2 dispatched
✅ Tier verification138 harness-stamped ⭐ Clause ② is
|
probe (healthy scoped, posture: 'isolated') |
this PR | merge base | should be |
|---|---|---|---|
| activation gate, tenant org admin | 503 | 200 + row written | §5 403 |
| activation gate, PLATFORM_ADMIN operator | 503 | 200 (correct) | 200 |
⇒ ⭐ The PR converts one wrong admit and one correct admit into outages, and reintroduces the very identity-step/gate split it exists to remove.
⭐ This is exactly the attack the dispatch brief asked for — "are any of them ordinary conditions that now turn a working door into a 503? Construct one if you can." It was constructed. An over-refusal probe that only tries the obvious cases would have missed it, because the missing argument is invisible unless the registration is scoped.
The rest
- F2 (medium) —
refuseUngrantedActivationWriteis also called fromdomains/automation.ts:1050(POST /automation/:name/toggle). Awaited, so nothing is unhandled, but the PR's own claims ("two pins, one per gate"; "no gate not named above changes behaviour") leave that door unnamed and unpinned. - F3 (low, but lands in
mainverbatim) — commit8d189afclaims a failure "answer[ed] 503 at the identity step and admit[ted] at these two gates … two answers at once". Driven:dispatch('POST','/keys')throws 503 at the identity step and never reaches either gate. The collapse is a door-body fact. ⭐ The PR body says this correctly; the commit message does not — and the commit message is what entersmain.
⭐ What the review attacked and could NOT break
Pins 6/6 at head and exactly 4 failed | 2 passed pre-fix; the ablation replicated independently (M1 → 2 failed | 4 passed, M2 → 4 failed | 2 passed); no option-C creep; the removed fixture verbs genuinely unreached with no ledger growth; check:system-context-census --fix not a baseline move; the anchors; the full runtime suite at 238 files / 3352 tests. "Every other sentence in the commits, PR body and changeset checked; apart from findings 2–4 none is false."
⛔ Nothing flipped ready, enqueued, or armed. Round 2 carries F1 + F4 (declaration and level together) + F2 + F3.
Generated by Claude Code
…nancy-posture-loud-resolve
…quest's own scope The classified read this branch gave `refuseUngrantedActivationWrite` dropped the scope id: `deps.resolveServiceOrLoud(context, 'tenancy')`, where the mint gate in `./keys.ts` and the identity step in `../http-dispatcher.ts` both pass `context.environmentId`. A `tenancy` registered `ServiceLifecycle.SCOPED` and resolved without a scope id rejects UNBRANDED — `Scope ID required for scoped service 'tenancy'` — which the classified lookup then re-raises, correctly. So the gate answered 503 on a service that was never unwell, and it was the gate's own omission that produced the fault it reported. Driven at the previous head with a HEALTHY scoped `tenancy` reporting `isolated` and `environmentId = 'platform'`, entering at each door body: actions/_activation tenant org admin threw 503 (cause "Scope ID required…") actions/_activation PLATFORM_ADMIN threw 503 automation/:name/toggle both callers threw 503 keys mint org-less answered 400 (correct: keys passed the scope) So the branch converted one wrong admit AND one correct admit into outages: on the merge base the tenant org admin was wrongly served 200 with the row written (the collapse this card is about) and the platform OPERATOR was correctly served 200 — and the operator is the one authority ADR-0126 §5 says this install-wide switch belongs to. It also re-created the split this branch exists to remove: the identity step, which resolves with the scope, read that same deployment as healthy while the gate called it an outage. With `context.environmentId` passed, the same probe answers 403 PERMISSION_DENIED for the tenant org admin (no row written) and 200 for the operator (row written) on all three doors, and every throwing-factory leg still answers 503 with nothing written. Predicted in writing before the run; all fifteen probe legs matched. The pins grow a third wiring — `scoped-healthy`, a real `ServiceLifecycle.SCOPED` factory that SUCCEEDS — because a pin file about outages that never registers a HEALTHY service cannot tell "loud on a broken service" from "loud on everything". The operator leg is the load-bearing one: every other caller here is one a refusal is a correct answer for, so only the caller whose correct answer is 200 can catch a gate that manufactured an outage. The automation toggle door is pinned here too — see the following commit, which corrects what the earlier message said about it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ct two sentences in 8d189af Commit 8d189af on this branch says, of the classified read: "That made one failure answer 503 at the identity step and admit at these two gates in the same deployment, so 'what is this deployment's state on the wall question' had two answers at once." That is overstated for a WIRE caller and it would land in `main` verbatim, because this repository squashes. Driven at this head, `dispatch('POST', '/keys', …)` and `dispatch('POST', '/actions/_activation/…')` with a `tenancy` factory that throws both answer 503 SERVICE_UNAVAILABLE with `context.executionContext` never set, nothing minted and no activation row written: the identity step raises first and the request never reaches either gate. Since PR #15909 the collapse at these two gates is a DOOR-BODY fact — an embedder calling `handleKeys` / `handleActions` directly, which is exactly where this branch's pins enter — not a second answer a wire caller could observe for that class. The pin section of the PR body already said this correctly; the commit message did not. The severity argument is unchanged and does not rest on the overstatement: one deployment still holds two different readings of its own wall question, and the door-body reading is the permissive one. The same commit also says: "the classified read is a second, opted-into deps facility, so no gate that was not named here changes behaviour." True of `resolveService`'s other callers, false about the gate it edited: `refuseUngrantedActivationWrite` is ONE body with TWO doors — `./actions.ts` (`POST /actions/_activation/:object/:action`) and `./automation.ts` (`POST /automation/:name/toggle`, through `refuseUngrantedFlowActivationWrite`) — so the toggle door inherited the new throw exit while going unnamed and unpinned. Both `await` the call, so nothing was ever unhandled; what was missing was the statement and the coverage. Both doors are now named in the gate's own doc block and pinned together in `tenancy-posture-outage-gates.test.ts`, which is where a claim about "the gate" can be checked against both halves of the surface. `DomainHandlerDeps.resolveServiceOrLoud` also gains the caller rule the scope-id defect earned: pass the scope you hold, because a rejection out of the classified read must describe the SERVICE and never the call site's own omission. Under the plain probe that omission was invisible; under this one it is a 503 for every caller of the door. The census docs line moves with the anchors this edit shifted, by the gate's own `--fix`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ed interface
The changeset graded `@objectstack/runtime` `patch` and argued it in one line:
"Patch rather than minor: no accept set widens, and a declared guard returns to
enforced." That reasoning answers a different question than the level asks.
`DomainHandlerDeps` is a PUBLISHED type — `packages/runtime/src/index.ts`
re-exports it and the package's `exports["."]` resolves to the emitted
`dist/index.d.ts`, which carries the interface and the new REQUIRED member
`resolveServiceOrLoud(…)`. A required member added to an exported interface is a
new symbol on the published surface: every external `const deps:
DomainHandlerDeps = {…}` stops compiling. This repository's own history grades
that exact shape as minor — three entries under 17.0.0 / Minor Changes in
packages/runtime/CHANGELOG.md say `DomainHandlerDeps` grows two lazily-read
members, grows the shared facilities the body needs, and grows
`getRequestKernelService` and `routeNotFound`. Nothing distinguishes this growth
from those three.
The earlier `patch` rested on an in-repo grep — one constructor, no implementer
outside `packages/runtime` — which is evidence of presence and never evidence of
absence. The mechanical floor exists so that likelihood is not the thing being
argued.
So: level `minor`, and the clause-② declaration in the PR body moves to `yes`
with it — the LEVEL AXIS check makes `yes` plus `patch` a self-contradiction, and
the two belong to one judgement. The justification sentence for the level no
longer used is removed rather than left standing.
The changeset text also picks up what round 2 measured: the activation gate is
one body behind TWO routes, so three routes change, not two, and every gate now
reads the posture in the request's own environment scope.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
|
VERDICT: PASS Independent adversarial delta review of round 2, head 1. The F1 fix — driven, and attacked with input classes round 2 did not nameA 120-leg matrix (6 Round 2's 15-leg table reproduces exactly (
Attacks on the fix, all at head, each compared with the merge base:
Wire-reachability of the throwing-factory class (commit 2. The third door on the MERGE BASE — was it broken before, or by this branch?Driven, not read. On 3. Clause ② — did it land mechanically?
4. The commit stream into
|
Delta review ADOPTED — PASS, at tier, verbatim. Carriers stripped; CI being re-read in the final label state
✅ Tier verification123 harness-stamped ⭐ The question this seat put to the reviewer had a real answer, and it is worse than the round-1 review knewThe brief asked: what did the toggle door do on the MERGE BASE? — because "a door left unpinned" and "a door this branch broke" are different severities. Driven on
⇒ At round 1 this branch had turned the operator's correct 200 into a manufactured outage on the toggle door as well — a regression on a third, unpinned route, not merely an undocumented one. Round 2 repaired and pinned it; the reviewer's words: "nothing remains." ⭐ Neither the card, nor triage, nor the round-1 review had ever driven that door. It took asking "and what did it do before?" to find that the branch had broken something that worked. The F1 fix survived an attack round 2 did not runA 120-leg matrix (6 tenancy wirings × 4
The NO-OP account was verified, not takenRound 2 reported its first negative control was inert because the gate reads HEAD, not the working tree. The reviewer reproduced both halves — working-tree Findings — three low, none blocking, all recorded
Landing positionCarriers stripped from both sides together and read back ( Next: re-read every check in the final label state, then ready and enqueue. Generated by Claude Code |
Fixes #15900
Clause-②: yes
Two dispatcher domain gates derived the effective tenancy posture through
DomainHandlerDeps.resolveService, which is the dispatcher's capability probe: every step of its fallback chain absorbs every rejection and answersundefined. So atenancyservice that was registered and failed to build arrived at both gates as the same value a deployment that never registered one produces, and both read that as "there is no wall".Round 2 — the contract review found a regression this branch had introduced, and it is repaired here
The at-tier review drove a healthy
tenancyregisteredServiceLifecycle.SCOPEDand found that the new classified read at the activation gate dropped the scope id —deps.resolveServiceOrLoud(context, 'tenancy')wherekeys.tsand the identity step both passcontext.environmentId. A scoped registration resolved without a scope id rejects UNBRANDED (Scope ID required for scoped service 'tenancy'), the classified lookup re-raised it correctly, and the gate answered 503 on a service that was never unwell — an outage the gate manufactured from its own omission.Re-driven here at the previous head, entering at each door body, healthy scoped
tenancyreportingisolated,environmentId = 'platform':POST /keysmint, org-less callerPERMISSION_DENIED, no row writtenPLATFORM_ADMINoperatorPERMISSION_DENIED, no writePLATFORM_ADMINoperatorSo the branch had converted one wrong admit and one correct admit into outages, and the caller it locked out is the platform operator — the one authority ADR-0126 §5 says this install-wide switch belongs to. It also re-created the very split this branch exists to remove: the identity step, which resolves with the scope, read that deployment as healthy while the gate called it an outage. Every direction and count above was written down before the run and matched.
Two more review findings are answered in the same round:
refuseUngrantedActivationWriteis one body called fromdomains/actions.ts:156and fromdomains/automation.ts:1050(POST /automation/:name/toggle, throughrefuseUngrantedFlowActivationWrite). Bothawaitit, so nothing was ever unhandled — but the toggle door inherited the new throw exit while going unnamed and unpinned. It is now named in the gate's own doc block and pinned beside the other two doors, and the sentences that said otherwise are corrected in the commit that carries the change.8d189afwas overstated and would have landed inmainverbatim. Corrected in a later commit that quotes it; the measurement is under Verification below.Clause ② is
yes, and the changeset isminorDomainHandlerDepsis a published type:packages/runtime/src/index.tsre-exports it, the package'sexports["."]resolves todist/index.d.ts, and the emitted declaration carries the interface with the new required memberresolveServiceOrLoud(…). A required member added to an exported interface is a new symbol on the published surface — every externalconst deps: DomainHandlerDeps = {…}stops compiling. This repository has graded that exact shape three times already, all minor, all under## 17.0.0/### Minor Changesinpackages/runtime/CHANGELOG.md(lines 7491, 7572, 7587: "DomainHandlerDepsgrows two lazily-read members", "grows the shared facilities the body needs", "growsgetRequestKernelService… androuteNotFound").Round 1 declared
nofrom an in-repo grep — one constructor, no implementer outsidepackages/runtime— which is evidence of presence and never evidence of absence; the mechanical floor exists precisely so likelihood is not the thing argued. So the declaration isyeson both carriers, the changeset is'@objectstack/runtime': minor, and the "Patch rather than minor" sentence that justified the level no longer in use is gone rather than left standing.The conformance limb stays
noby the repo's own written negative boundary: a runtime permission/security behaviour change belongs to the manual floor's security class, not to clause ②.Nobody had run this. Now somebody has.
The card says of itself "source reading, not an end-to-end run", and triage added that its line-level check must not be inherited as proof that a broken
tenancyservice really lets these doors through. So this branch drove it first, against the pre-fix tree, with a realObjectKernelwhosetenancyis registered through a throwing factory:The premise holds, exactly as written:
POST /keysminted an organization-less key and echoed the raw secret once, where a walled posture refuses one; and a tenant organization administrator's install-wide activation write was served and the row written, where ADR-0126 §5 requires the platform operator.Why this is
securityand not tidinessThe identity step has read this same fact through the classified lookup since PR #15909. So one deployment holds two different readings of its own wall question at the same moment: 503 at the identity step, and admission at these door bodies. Stated precisely, because the reach matters and the review measured it: for the throwing-factory class a WIRE request dies at the identity step and never reaches these gates, so the permissive half is reachable by an embedder calling
handleKeys/handleActions/handleAutomationdirectly — which is exactly where the pins enter. The inconsistency is still worse than either behaviour alone, because there is no longer a single answer to what the deployment's posture even is.The change
Ruled on the card 2026-09-06T13:51Z (director seat, decision batch #55), maintainer's reply verbatim and untranslated: 「同意」 — option A, the narrow two-site fix.
The gates now read the posture through a classified lookup, and the two facts are told apart by the registry's own brand (#13905), never by message text:
AuthzStoreUnavailableError, so the door answers 503SERVICE_UNAVAILABLE. The posture is an authorization INPUT that was never read, so admission was never decided — 「A posture that could not be READ is not a posture that is ABSENT.」 ([finding] two morecomputeExecCtxseams read "failed" and "not wired" as one value, and both feed authorization inputs — tenancy posture and the ADR-0069 auth gate #13906 decision 1 option A). Nothing is minted and nothing is permitted.tenancyis resolved rather than reported as an outage.Two ruled call sites, three routes, because the activation gate is one body behind two doors. Sites, re-derived on this head:
packages/runtime/src/domains/keys.ts:135:140(const walled = …)packages/runtime/src/domains/activation-gate.ts:217:222, operator test at:228packages/runtime/src/domains/actions.ts:156packages/runtime/src/domains/automation.ts:1050refuseUngrantedFlowActivationWrite(:532)packages/runtime/src/domain-handler-registry.ts:172resolveServiceOrLouddeps memberpackages/runtime/src/http-dispatcher.ts:408What is deliberately NOT here
Option C — rerouting
'tenancy'through the loud lookup for every domain — is explicitly not ruled, and the card and triage both say why: it would change every gate that reads the name in one stroke, and nobody has enumerated those gates.resolveServicekeeps its probe contract for every other name and every other domain. The classified read is a second, opted-into deps facility that a gate calls at one call site at a time, so no gate outside the three routes named above changes behaviour. The boundary — and the round-2 rule that a caller must pass the scope it holds, so a rejection describes the SERVICE and never the call site's own omission — is written on the interface member itself.keys.tscarried a comment documenting "an ABSENT service means we cannot tell, and the honest answer at MINT time is to mint" as a deliberate choice. That choice is kept and is now scoped at the site to the class it was always true of — never-registered — so the next reader does not re-derive the collapsed reading.Anchor rot, reported
Triage read
activation-gate.ts:142/:146. On this branch's base (a87163c89b9) the real sites were:161/:165— the file had moved by 19 lines before any work started.keys.ts:114was exact. Every anchor in this PR was re-derived, not inherited, in both rounds.Verification
Gate union derived mechanically, twice, from the delivered diff (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, plain and--commands) — never from a hand-written list, and never from a hand-made file list either: the script takes its own change set from the merge base.PREREQUISITE NOT MET/ "the package is not built" (check:skill-examples,check:dual-build-cjs-loads,check:type-check-debt); rather than record the non-measurement, the whole package closure was built (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71 tasks) and all three then ran green.check-partof-closing-keywordandcheck-single-claim-pathsprintNOT WIREDwith no PR context (the first was then driven locally with this body and this branch's commit list — see below), andcheck:react-declaration-parityneeds objectui'ssdui.manifest.json, which this repository does not contain.check:system-context-censusreddened twice on line rot from these edits and was repaired both times by the gate's own--fix(the one docs line in the diff); the census population is unchanged at 105 sites / 140 anchors.check-partof-closing-keyworddriven locally withPR_BODY= this body andPR_COMMITS_FILE= this branch's nine commit messages: clean on both rules — no commit carries a card-relation trailer, and the body's first line is the single declaration.check-changeset-no-major --eventdriven against the LIVE label set (needs:contract-reviewincluded) and this declaration: LEVEL AXIS green onyes+minor. It is a measurement, not a formality — the same run against apatchlevel (committed in a throwaway worktree, because this gate reads HEAD and a working-tree edit is invisible to it) answers exit 1: "This PR declares clause-② YES and grades a package it grewpatch."pnpm --filter @objectstack/runtime test— 238 files / 3360 tests, all passing (3352 before, +8 new pins).pnpm --filter @objectstack/runtime typecheckgreen, includingcheck:test-typecheck. The test layer is proven to COVER the pin file rather than assumed to:tsc -p tsconfig.test.json --listFileslists it (1 hit), and a plain run shows 191 pre-existing debt errors with zero in it.pnpm lint— the whole repo,eslint . --no-inline-config, exit 0. Not a narrowed run, so no narrowing evidence is owed.Every reading above was taken on the final head:
0795550f357. Round 2 was driven in sessionsession_01D47qPfEWVPmhguWgBZCi5N.The pins, and why they enter at the door body
14 pins across three doors —
POST /keys, the actions activation write, and the automation toggle — over three wirings oftenancy: never registered (the control), registered through a factory that throws (the class this card is about), and registeredServiceLifecycle.SCOPEDand healthy (the class round 2 added). Without the never-registered control a pin cannot show the two classes were separated, only that the door got louder about both; without the scoped-healthy leg it cannot tell "loud on a broken service" from "loud on everything", which is exactly the defect the review found.The operator leg is the load-bearing one there: every other caller in the file is one a refusal is a correct answer for, so a gate that answered "no" to everything would still satisfy them. The
PLATFORM_ADMINoperator is the only caller whose correct answer is200.They call
handleKeys/handleActions/handleAutomationdirectly rather thandispatch(), deliberately. On thedispatch()path the identity step runs first and already answers 503 for the throwing-factory fault, so a pin routed through it would go green on PR #15909's change with these gates left exactly as they were — a phantom check. Each pin asserts the ADR-0112 envelope (statusandcode, plus the outage brand or the §5PERMISSION_DENIED), that the dispatcher's own error exit renders the outage as a 503 on the door, and — the load-bearing half — that nothing was minted and no activation row was written.The fixture builds a real
ObjectKernelrather than a hand-rolled brand:serviceNotRegisteredErroris package-internal to@objectstack/core, so a double's own brand would be this file's opinion of the classification instead of the classification. In this treetenancyis registered as an instance byplugin-auth, so both the failed-to-build and the scoped classes had to be CONSTRUCTED — a realregisterServiceFactory, resolved through the realPluginLoader.getService.The new fixture reddened two gates, and both were real
check:engine-double-contractandcheck:objectql-double-limitboth read the new test file. Theupdate/deleteverbs on the doubles had been copied from a sibling fixture and no door under test reaches either, so they were coverage nobody was getting and would have owed the retained ledger two rows for pins that can never fire. They were removed rather than pinned — the ledger is untouched (744 rows held, none added; the limit baseline verified "no files added"). Thefinddouble applies the caller'slimitby presence and after the filter, so it cannot answer a page the producer would not have returned. Round 2's automation double follows the same rule:getFlowandtoggleFlow, nothing this door does not reach.Ablation — predicted first, in both rounds
Round 1 mutated
packages/core/src/service-not-registered.ts, the file that owns the classification the pins restate:Round 2 adds two legs against the enlarged file (14 tests):
PluginLoader's SCOPED guard so a scoped service can never resolve WITH a scope idEvery mutation was confirmed on disk by blob hash plus grep counts of the removed and injected text, and every restore the same way — hash equality against the HEAD blob and an empty
git diff HEAD— under atrap … EXIT INT TERMwith absolute paths, andgit checkout HEAD -- ABSOLUTE_PATHnever a bare checkout.