Repository navigation
refactor(runtime,mcp): the last two admission doors classify the tenancy rejection through the shared classifier - #17683
Conversation
…pies onto the shared classifier WIP checkpoint before the verification lap. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 32 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 ea00bbe0223cc3958d74bfd3b8c6f68a91744066 && git checkout ea00bbe0223cc3958d74bfd3b8c6f68a91744066
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1a25f4a8d7600ed0f9cea9dbe0a94135452d66ef 056cc4923b710ec6a4dbeed19b9593c56139ba27 && git checkout -B drift-repro 1a25f4a8d7600ed0f9cea9dbe0a94135452d66ef && git merge --no-ff 056cc4923b710ec6a4dbeed19b9593c56139ba27
node scripts/docs-audit/affected-docs.mjs --json 1a25f4a8d7600ed0f9cea9dbe0a94135452d66ef
|
|
ACCEPT — Arming waits on every check, not only the required ones. The file surface was honoured, not merely untrippedDeclared IN: Measured on the diff, ⛔ not taken on the PR body's word: 5 files ( ⭐ The fence was not just obeyed — it was made load-bearing and then proved soThe dispatch's sharpest ⛔ was that only the async kernel leg is in scope, because
And the #16013 binding constraint carried through verbatim in the docblock:
⭐ Then it proved the fence matters by violating it. Two ablations rather than one, each with on-disk mutation proof taken before the result was read:
A2 is the one worth naming: a fence is usually an instruction nobody tests. This one now has a test that fires only when it is crossed, which is the difference between a comment and a guard. Both restores proved by blob-hash equality to the HEAD blob plus an empty The census instruction, fifth reading — and no correction owedThe card's own load-bearing line was "Re-measure the population before starting", and this seat ran it a fourth time at 10:07Z. The dev ran a fifth, independently, from the symbols rather than any line number, and reports still exactly two foldable sites. ⇒ the fourth reading stands and ⛔ there is nothing to correct — which is the outcome that census was demanded for after moving three times historically. ⭐ It also declined to fold a fifth site nobody had named: Other evidence worth recording: the test layer was proved reached rather than assumed (
The docs-drift row — checked, and it is corroboration rather than driftThe advisory named one page precisely:
⇒ that is the same two-arm rule the shared classifier implements, described as behaviour rather than as internals. A behaviour-preserving fold cannot falsify it. ⭐ Not drift — the page and the new code agree. ⛔ No cards owed from this report — stated because the last two produced severalAll three of the dev's "noted, not filed" items dispose correctly on their own terms and ⛔ none needs a card:
Landing verification owed after the queue merges:
Generated by Claude Code |
…o the commits that decided them (objectstack-ai#20713) Part of objectstack-ai#20594 Clause-②: no ## What changed This is stage 7 of the `domain:cli` lane of the dead-citation sweep: `packages/mcp/src`. Every comment site there that cited a tracker number answering 404 now cites, in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), the commit in this repository's history that decided what the line describes, and keeps saying in its own words what that commit decided. PR objectstack-ai#20533 is the method, and stages 1 to 6 of this card (PR objectstack-ai#20624, PR objectstack-ai#20632, PR objectstack-ai#20656, PR objectstack-ai#20673, PR objectstack-ai#20689, PR objectstack-ai#20703) are the precedents. The card stays open for the lane's remaining packages, so this PR says `Part of`. That is **17 sites on 17 lines in 9 files, covering 9 numbers**, rewritten to **9 distinct commits**: - the census's **10 sites**, in `mcp-server-runtime.ts` (5), `plugin.ts` (3) and `stdio-data-bridge.ts` (2), 7 numbers; - **7 test-file comment sites** in 6 test files (the census defers `*.test.ts`; stages 1 to 6 took test comments too). One more line changed: `__tests__/plugin-execution-context.test.ts:7`, the second half of the `:6` sentence ("this face was not in that card's inventory" now reads "not in that commit's inventory", since the card it pointed back to is now named as a commit). Only comments changed: **18 lines out, 18 in**, and every touched file keeps its line count, so no line citation into these files moves. **No citation number is added**: over the 18 line pairs, added-minus-removed numbers is empty, and no PR number stands newly on any line. No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records any of these 9 decisions (a grep for the 9 numbers there reads 0 hits, with a control number from the same tree reading 2), so every anchor is a commit. **No changeset, and `skip-changeset`:** none of the rewritten comments reaches `dist` (measured below: base and head emit six byte-identical files, and a code-mutation control changes four of them). That is stage 5's case (PR objectstack-ai#20689), not stage 6's. ## Census: `packages/mcp`, before and after **Instrument.** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged, run under `with-fleet.sh --read` for the token. The count is its `allocated-but-absent` findings under `packages/mcp/`. Both runs enumerated the whole board. | reading | tree | board | whole-repo `allocated-but-absent` | `packages/mcp` sites | lines | numbers | files | |---|---|---|---|---|---|---|---| | before | base `e4e5222b7b`, run 2026-09-29T19:45:33Z to 19:50:37Z | enumerated, 186 pages, frontier objectstack-ai#20708, 18,535 numbers | 1,222 | **10** | 10 | 7 | 3 | | after | `459ff81088`, run 19:58:39Z to 20:02:47Z | enumerated, 186 pages, frontier objectstack-ai#20709, 18,536 numbers | 1,212 | **0** | 0 | 0 | 0 | The whole-repo drop of 10 is exactly these sites: a site-by-site diff of the two JSON outputs has 10 findings gone, all under `packages/mcp/src`, and none added. The other three tallies (`resolves` 32,968, `resolves-as-pull-request` 1,984, `cross-repo-unjudged` 994) are equal in both runs. `packages/mcp/src` is byte-identical at `459ff81088` and at the head. **Supplementary scan (test files included).** The gate's exported `extractCitations` and `classifyCitation` over all 43 `.ts` files under `src/`, with the board from the gate's own `probeBoard`: 365 citations and 21 dead before (src comments 10, test comments 7, src strings 0, test strings 4), 348 and 4 after (0, 0, 0, 4). Its before list of src comment sites is identical to the census's. The 4 left are test titles, the form-D stage (see Acceptance notes). ## Per-site table `git blame` at the base ties each line to the commit that wrote it, and each anchor was read in its message, changeset or diff, not only its subject. Where the pull request that landed an anchor still answers, its body's first line names the dead number, which is noted. | number | sites (base line) | anchor: what it decided | |---|---|---| | `objectstack-ai#13318` | `mcp-server-runtime.ts:272` | `3ec8646f1`: the bridged tools' `readOnlyHint` / `destructiveHint` come from what the definition declares, and a tool that declares nothing is served neither hint (omit-when-unsourced). The line blames to `c39369d12`, the `openWorldHint` sibling, whose changeset calls this the repair "that preceded it". The PR that landed `3ec8646f1` answers 404 too. | | `objectstack-ai#6724` | `mcp-server-runtime.ts:625`; `mcp-server-runtime.metadata-outage.test.ts:289` | `4f3d2322e`: corrects `diagnoseEmptyRead`'s falsified claim that `MetadataFacade.getObject` differs from `get('object', n)`, in the TSDoc and in the outage test's restatement of it. Both lines blame to it; PR objectstack-ai#6948, which landed it, names objectstack-ai#6724. | | `objectstack-ai#6745` | `mcp-server-runtime.ts:636` | `7a5ef0008`: adds `metadata-service-getobject-equivalence.test.ts`, pinning `getObject(n)` equal to `get('object', n)` across all three implementations. The line's "PR objectstack-ai#6839 for objectstack-ai#6745" named this commit's PR (answers 200), which stays beside the sha as a convenience link. The spec lane gave the number this anchor. | | `objectstack-ai#6723` | `mcp-server-runtime.ts:637`, `:652`; `mcp-server-runtime.metadata-outage.test.ts:293` | `8ad609c69`: declares on `IMetadataService.getObject` that it answers the same as `get('object', name)`. `objectstack-ai#6723` was the pull request that landed as this commit (its subject carries the number); `objectstack-ai#6505`, the issue beside it on `:637`, answers 200 and stays. The spec lane gave the number this anchor. | | `objectstack-ai#17114` | `plugin.ts:8`, `:67`; `stdio-tenancy-posture-api-key-matrix.test.ts:569` | `4af758d47`: the last two admission doors, this one included, classify the tenancy rejection through the shared `classifyAdmissionTenancyPosture`. All three lines blame to it; PR objectstack-ai#17683 names objectstack-ai#17114, and stage 1 gave the number this anchor. | | `objectstack-ai#6216` | `plugin.ts:126`; `__tests__/plugin-execution-context.test.ts:6` | `f586f1a89`: one `ExecutionContext` assembler for the dispatcher, REST and share-link sites. Both lines blame to `502dc6fe7`, which converged this stdio face afterwards and names that convergence as its precedent. Its file list touches no `packages/mcp` file, which is what `:7` ("not in that commit's inventory") says. Stages 1 and 2 and the spec lane gave the number this anchor. | | `objectstack-ai#8422` | `stdio-data-bridge.ts:85`, `:394`; `stdio-data-bridge.not-found.test.ts:4` | `4810dd628`: the stdio bridge's by-id write seams throw the shared `recordNotFoundError` envelope instead of a bare `Error`. All three lines blame to it; PR objectstack-ai#8507 names objectstack-ai#8422. | | `objectstack-ai#17568` | `mcp-record-id-key-mistake-refusal.test.ts:4` | `9c9e6d08f`: pins that a missing-`recordId` refusal also names the `id` the caller sent (test-only). The line blames to it; PR objectstack-ai#17650 names objectstack-ai#17568. | | `objectstack-ai#13486` | `mcp-tool-bridge-safety-annotations.test.ts:423` | `6193e576d`: pins the bridge's two hand-copied safety name sets in the direction the old pin could not see (the docblock's heading is that commit's subject). The line blames to it; PR objectstack-ai#13888 names objectstack-ai#13486. | **Anchor checks.** Every cited sha matches exactly one object (`git rev-parse --disambiguate`, count 1 for each of the 9), is a commit, has one parent, and is an ancestor of `main` (`merge-base --is-ancestor` against `5757463712`, exit 0 for all 9). The checkout is not shallow. The control leg `979ad9575` (2026-08-08, the parent of the oldest anchor `8ad609c69` of 2026-08-08) exits 0, and the negative control, this branch's own `459ff81088`, exits 1. Four anchors reuse the landed stages' (`f586f1a89`, `4af758d47`, `7a5ef0008`, `8ad609c69`), so each number carries one anchor across the tree; five are new (`3ec8646f1`, `4f3d2322e`, `4810dd628`, `9c9e6d08f`, `6193e576d`). **Numbers.** All 9 dropped numbers answer 404 by REST (re-probed 2026-09-29T19:54Z). The numbers kept on changed lines (`objectstack-ai#6839`, a pull request; `objectstack-ai#6505`, `objectstack-ai#15348`, `objectstack-ai#16013`, `objectstack-ai#4435`, `objectstack-ai#5138`, `objectstack-ai#7867`) answer 200. Four slash-joined groups stand in `packages/mcp/src`, whose later halves the citation grammar does not read (`objectstack-ai#4435/objectstack-ai#5138/objectstack-ai#7867` twice, `objectstack-ai#5138/objectstack-ai#5581`, `objectstack-ai#7728/objectstack-ai#7823`); every half answers 200, so none is dead. ## Mechanical guard: no code token moves **H2 holds on both readings: the parser leaf-token diff is empty, and the emitted `dist` is byte-identical.** **Token guard.** It compares the TypeScript parser's leaf tokens (TypeScript 6.0.3, JSDoc nodes excluded) of the 9 touched files at base `e4e5222b7b` and at `459ff81088`. Controls mutate the head text in memory only. - Real run: 21,192 base tokens, 0 differing (exit 0). - Comment-insertion control: 0 differing (exit 0). - Code-insertion control: all 9 files differ at token 0 (exit 1). - String control (the first character of the `'vitest'` import specifier in `plugin-execution-context.test.ts` flipped): exactly 1 differing `StringLiteral`, at token 15 of that file (exit 1). **Emitted `dist`.** `pnpm --filter @objectstack/mcp build` at the head, then at base (the base tree of `packages/mcp/src` restored in place under a trap-armed restore; an on-disk probe read `objectstack-ai#13318` 1 and `commit 3ec8646` 0 before that build; afterwards every touched blob equals its HEAD blob and `git diff HEAD` is empty), with the same dependency builds: - all six files (`index.cjs`, `index.cjs.map`, `index.d.cts`, `index.d.ts`, `index.js`, `index.js.map`) are **byte-identical** by sha256. The built files do carry docblocks (14 in `index.js`, 78 in `index.d.ts`); none of the rewritten ones is on an emitted declaration. - Code-mutation control (`scripts/ablation-replace.mjs`, anchor: the sync leg's typed `ctx.getService` call on `'tenancy'` in `plugin.ts`, hit 1 to 0, its argument renamed to a marker; blob restored to HEAD `0a1aaa7955`, `git diff HEAD` empty): `scripts/ablation-dist-preflight.mjs` found the marker in `index.cjs` and `index.js`, and `index.cjs`, `index.js` and both `.map` files differ from the head build. `dist` was then rebuilt, its six sha256 values equal the first head build, and the preflight in `--absent` mode reads the marker absent from all 6 files with a clean tree. A raw scan of the 9 changed files for control bytes finds none (a positive probe on a scratch file matched). ## Changeset **None, and `skip-changeset`.** `@objectstack/mcp`'s `files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the build above emits byte-identical `dist` at base and head, so this diff publishes nothing from any released package. Stage 5 (PR objectstack-ai#20689) measured the same and shipped the same; stage 6 (PR objectstack-ai#20703) measured the opposite and carried a `patch`. ## Gates (head `7a0f15de62`) This host has no `flock`, so `os-verify-lock.sh` ran in its declared unlocked mode. Its disclosure, verbatim, from each run at this head and from the four `dist` builds (at `459ff81088`, `packages/mcp/src` byte-identical to this head): ```text os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 25s · declare it in the PR body · pnpm --workspace-concurrency=2 --filter '@objectstack/mcp...' build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 116s (1m56s) · declare it in the PR body · pnpm turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 10s · declare it in the PR body · pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 4s · declare it in the PR body · pnpm --filter @objectstack/mcp typecheck os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 3s · declare it in the PR body · pnpm --filter @objectstack/mcp build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter @objectstack/mcp build ``` - **Build:** `@objectstack/mcp` with its closure (9 of 81 workspace projects), then the whole workspace, `turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 71 of 71 tasks, after the merge. The tree was clean after both. - **Tests:** `vitest run`: 32 files, 344 tests passed (every `*.test.ts` under `src/`), at the head and before the merge. - **Typecheck:** `pnpm --filter @objectstack/mcp typecheck` exits 0. `tsc --listFiles`: `tsconfig.json` compiles the 11 non-test `src` files, `tsconfig.test.json` all 43 including the 32 test files. `check:test-typecheck`: 6 files, 53 errors, 8 pinned signatures, held. - **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`) exits 0 at this head (2026-09-29T20:18:54Z to 20:19:23Z), and at `459ff81088` before the merge. - **Citation judging:** after merging `origin/main` (`9b384f63ae`), `node scripts/check-issue-citations.mjs --base 9b384f6` judges 5 citations on the changed lines of 3 files (the kept numbers `objectstack-ai#15348`, `objectstack-ai#16013`, `objectstack-ai#4435`, `objectstack-ai#6505`, and `objectstack-ai#6839` as a pull request) and exits 0: every one resolves. Against `origin/main` after it moved to `5757463712`, the same 5 citations, exit 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 53 families. All 53 exit 0, and `--ran` with the exit-coded record reads "53 derived, 53 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). Among them: `check:issue-citations`, `check:doc-authoring` (808 pinned sites, no growth), `check:nul-bytes` (9,331 files, no raw control bytes), `check:published-files`, `check:type-check-debt`. - **Artifact rosters:** 36 of the 39 non-self-test roster rows exit 0, including the three the derivation marks as keeping their roster under one of this diff's paths (`check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`). The other three need a pull request's context; they are run against this PR once it exists and reported on the card. The 18 self-test-only rows grade their checkers' fixtures and cannot judge this diff. ## Hypotheses (measured first) - **H0 holds.** At base `e4e5222b7b` the filtered census answers 10 sites on 10 lines, 7 numbers, in 3 files, as on the seat's `0be898499f`. The whole-repo count is 1,222. - **H1 holds.** After the rewrite, the filtered census answers 0 for `packages/mcp`. No site was left for an open PR (the file lists of all 8 open PRs were read at 20:08:05Z: only the Version Packages PR objectstack-ai#20639 touches `packages/mcp`, in `CHANGELOG.md` and `package.json`) or for an unfound anchor. - **H2 holds, on both readings.** The comment-stripped (parser-token) diff of all 9 touched files is empty with its controls firing, and the emitted `dist` is byte-identical at base and head with a code control that changes it. ## Acceptance notes - **Test titles, the form-D stage.** 4 dead numbers remain in test string literals in `packages/mcp/src` (`describe` titles, no assertion text): `objectstack-ai#17568` twice in `mcp-record-id-key-mistake-refusal.test.ts` (`:151`, `:315`), `objectstack-ai#8422` in `stdio-data-bridge.not-found.test.ts:99`, `objectstack-ai#17114` in `stdio-tenancy-posture-api-key-matrix.test.ts:592`. They stay on the card for its form-D stage; no string moved here. - **Outside `src/**`, a later stage of the card:** `packages/mcp/vitest.config.ts:18` cites `objectstack-ai#8651` (404). `packages/mcp/test-typecheck-debt.json:2` cites `objectstack-ai#13470` (404) inside its `_comment` field, which the file itself says is generated by `scripts/check-test-typecheck.mts`, so a fix there is at that producer, in the `scripts/**` lane, not a hand edit. The other citations in `packages/mcp` outside `src/**` (`CHANGELOG.md` excluded) answer 200. - **Card-word residue, cited nowhere.** A few docblocks still say "this card" or "the card" a paragraph away from the rewritten line (for example `stdio-data-bridge.not-found.test.ts:19`, `mcp-record-id-key-mistake-refusal.test.ts:19`, `stdio-tenancy-posture-api-key-matrix.test.ts:580`, `:584`). They cite no number, so they were left, as the landed stages left theirs; only the one same-sentence companion (`plugin-execution-context.test.ts:7`) was changed. - **The moving `origin/main`.** The branch merged `origin/main` once (`7a0f15de62`, merging `9b384f63ae`: `service-storage`, `platform-objects` and `plugin-audit`, nothing in `packages/mcp`). A later fetch advanced the shared ref to `5757463712`, one commit in `platform-objects` translations. There was no second merge; CI judges the merge ref. ## Deviations - **One companion line (`plugin-execution-context.test.ts:7`)** beyond the 17 sites, the second half of the `:6` sentence. - **Commit trailers** are AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`), and the pre-push trailer check passed on every push. The harness's attribution reminder asked for a model-named trailer and a different PR footer, and AGENTS.md overrides it. The merge commit carries git's default message. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17114
@objectstack/core'sclassifyAdmissionTenancyPostureis the ONE place the #13906 decision 1 option A classification lives. Two admission doors were still hand-writing it, out of the declared scope of the fold that extracted it. They now call it.The census, re-measured — still exactly two
The card's own load-bearing instruction, run a fifth time, on this branch's base
e1fdaf8b0(origin/mainat worktree creation):packages/runtime/src/security/resolve-execution-context.ts, inresolveExecutionContextpackages/mcp/src/plugin.ts,resolveStdioTenancyPosture— the async kernel legMeasured from the symbols, not from any line number —
git grepforeffectiveTenancyPosturecrossed withisServiceNotRegisteredError, both overpackages. Population: 2. That agrees with the PM seat's fourth reading, so there is no correction to report.Everything else the two greps turn up, and why none of it is a third copy:
packages/mcp/src/plugin.tssync leg — the deliberate sync fallback. ⛔ NOT folded; see the fence section below.packages/runtime/src/domains/keys.tsandpackages/runtime/src/domains/activation-gate.ts— reach the service throughdeps.resolveServiceOrLoud(...)and carry no not-registered arm at all. A different, deliberate shape;keys.tsrecords the reason verbatim.packages/rest/src/rest-api-plugin.ts(two sites) — the quiet-return-undefinedshape, disposition (b) in the parent card's amendment.packages/runtime/src/http-dispatcher.ts—classifyServiceanswering{outcome: 'never-registered'}; that is the upstream classifier the three sites above rely on, not a copy of this one.plugin-auth/plugin-sharing— privatethis.effectiveTenancyPosture()methods over an already-held service. No registry rejection in sight.What was extracted, and what deliberately was not
The extractable part is the classification, not the resolution. Each seam keeps its own guard and hands its own former accessor expression in as the thunk:
The helper never learns how a seam reaches the service — one that did would be wrong for one seam or grow a flag per seam, which is the copies with extra steps.
⛔ The fence, and it is now a measured arm rather than a comment
The stdio door's sync fallback is taken only on a
KernelBase-shaped host that has nogetServiceAsyncat all.ObjectKernelBase's sync accessor throws a plain[Kernel] Service 'tenancy' not found— unbranded, because it never reachesPluginLoader.getService. Routing it through the shared classification would mint a 503 outage out of a supported composition, so its barecatchremains that seam's recorded decision.packages/runtime/src/domains/{keys,activation-gate}.tsandpackages/rest/src/rest-api-plugin.tsare untouched for the reasons in the census table.Tests — the discriminating behaviour at each folded seam
Both seams already carried the never-registered-versus-registered-and-failing pins; both suites are green unchanged. What is added is the pin the wrong fold fails, one per seam, plus the fence arm:
packages/runtime/src/security/resolve-execution-context.test.ts— a lookup that throws synchronously (withinKernelServiceLookup's declaredPromise of any, or any) must classify exactly as its rejecting twin: branded ⇒ quiet and admitted; unbranded ⇒ 503SERVICE_UNAVAILABLEwithobject: 'tenancy'. Both error values are read out of a realObjectKerneland then re-raised, so neither arm is a hand-built brand at the seam under measurement.packages/mcp/src/stdio-tenancy-posture-api-key-matrix.test.ts— the same two arms through the stdio door's boot, plus⛔ FENCE: a host with NO getServiceAsync takes the SYNC leg and stays quiet on its UNBRANDED miss.Ablations, each with on-disk mutation proof taken BEFORE the result was read
A1 — the wrong fold at the runtime seam. Resolve the service first, hand the helper a settled value (
() => ablationSettled). Marker occurrences on disk 0 to 2, the folded shape 1 to 0, blob21dbf185tob1cbc2b9.A2 — the over-scope fold at the fenced sync leg. Route the mcp sync leg through the helper. Marker 0 to 1, sync-leg anchor 1 to 0, blob
673907e8to0e1da1ec.Exactly the fence arm, and nothing else. Both restore legs are proved by blob hash equal to the
HEADblob and an emptygit diff HEAD, not by an exit code.Verification
Every heavy run went through
scripts/pm/os-verify-lock.sh; each verdict below is the wrapper's ownVERDICT command-exitline, never a bare exit status.pnpm --filter '@objectstack/runtime^...' --filter '@objectstack/mcp^...' build --concurrency=2VERDICT command-exit 0pnpm --filter @objectstack/runtime --filter @objectstack/mcp typecheckVERDICT command-exit 0pnpm --filter @objectstack/mcp testVERDICT command-exit 0— 31 files, 333 tests passedpnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2VERDICT command-exit 0— 256 files, 3590 tests passedpnpm --filter @objectstack/runtime --filter @objectstack/mcp buildVERDICT command-exit 0Gate roster derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no hand-written path list), re-derived after the changeset existed: 59 commands, every one run with its output redirected and$?captured before any pipe.Reconciled with
--ranafter a freshgit fetch origin main(so the derivation is against the newest localorigin/main, and the change set is still the same 5 paths off merge basee1fdaf8b0, three-dot):The two NOT MEASURED, with their reasons —
exit 3isPREREQUISITE NOT MET, which is neither a pass nor a failure, and both want a whole-tree build that is CI's run, not this card's:pnpm check:dual-build-cjs-loads— "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured." 36 packages unbuilt in this worktree.pnpm check:type-check-debt— "--re-measure cannot run: 1 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk", and its own text forbids reading anything into it.pnpm lint— the FULL union, ⛔ never narrowed to these paths.scripts/pm/dispatch-gates.mjsdoes not name it, so this lane adds it by hand:Zero problems over the whole repository. Positive control beside the zero, because a silent linter and a clean tree read alike — the same binary, the same
--no-inline-config, scoped topackages/mcp/src/plugin.ts:The test layer is proved reached, not assumed.
tsc -p tsconfig.test.json --listFilesnames both edited test files in the compiled program (stdio-tenancy-posture-api-key-matrix.test.tsx1,security/resolve-execution-context.test.tsx9), andpnpm --filter @objectstack/{runtime,mcp} check:test-typecheckisOKon both — the shrink-only, identity-pinned ledgers held (27 files / 191 errors for runtime, 6 / 53 for mcp; nothing added).Changeset — owed, and here is the measurement
patchon@objectstack/runtimeand@objectstack/mcp, ⛔ notskip-changeset. Both publishfiles[]: ["dist"], and after a real build the new call is in the shippeddistof each — 2 files each, with a known-absent symbol scoring 0 as the negative control andisServiceNotRegisteredErrorscoring 4 inruntime/distas the lit positive control.@objectstack/mcp'sdistno longer mentionsisServiceNotRegisteredErrorat all. Something published moves, so a changeset is owed even though no behaviour and no export does.Clause-②: no— confirmed by measurement, not assumed:classifyAdmissionTenancyPostureis already exported frompackages/core/src/security/index.tsand already imported from 8 files outsidepackages/core. This adds a 9th and 10th consumer, not a surface. No accepted input, no new export, no authorable key.Acceptance notes
Noted, not filed — neither is a reproducible defect, a declared-contract violation, or an authoring trap, and both are cosmetic drift inside files a future reader of this seam will open anyway:
classifyAdmissionTenancyPosture's export atpackages/core/src/security/index.ts:119; it reads:117today, and the mcp site reads:90-:101against the card's:92-:99. Ordinary line drift on a read-only file, already flagged by the dispatch itself — recorded so the next reader anchors on symbols. Carrier: any later card folding onto this helper.packages/runtime/src/security/resolve-execution-context.ts's module docblock still narrates the classification inline ("throwsAuthzStoreUnavailableError(503) for the one class of fault ..."). Still accurate, now one indirection away from the code. Left as is rather than rewritten: the prose is right, and rewriting it would enlarge an S-sized diff. Carrier: the next change to that door's tenancy read.Authored in Claude Code session
session_01TSf4DV7ziu4V5j73e46b7c. The attribution block below is the platform's append on the edit channel, not a second footer of mine — the session-URL form this write sent was appended over rather than stored, so the durable reference is this sentence.Generated by Claude Code