feat(service-storage): mountStorageRoutes — one host door for kernels with no http-server service - #16741
Conversation
…dispatch (#15169) The `mountStorageRoutes` fixture engine answered `delete` / `update` / `findOne` more loosely than `ObjectQL` does, which is how a dead route ships with its suite green (#4434 / #5619). Route the three verbs through the producer's own predicates (`assertEngineDeleteDispatch`, `assertEngineUpdateDispatch`, `assertEngineFindOnePredicate`) and register the three (file, verb) pairs in the gate's ledger via its `--write`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
`buildAuthSessionResolver` and `buildFileReadAuthorizer` were given an `export` keyword by the composition refactor and nothing imports them: measured as import EDGES (an `import`/`export … from` clause naming the symbol, multi-line aware), both are 0, against firing positive controls `findFileHolder` 4, `mountStorageRoutes` 2, `StorageServicePlugin` 15. `buildFileReadAuthorizer` IS the ADR-0104 D3 download-authorization gate, so the keyword is not free: dropping it makes "a consumer gets no handle on the gate" hold at the module level too, not only because `index.ts` declines to re-export and the package's `exports` map publishes `"."` alone. `composeStorageRoutes` and `toGateRegistry` keep their exports — `mount-storage-routes.ts` imports them, and they hand back a report of booleans, never a gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…orage-routes-public-seam
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 7 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 e45a571e88e040b8ccbfe13c472f8831f4cdf008 && git checkout e45a571e88e040b8ccbfe13c472f8831f4cdf008
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ed7243d52bbc1b6d00a3b621b0dcea4925df32b5 ae43bfa29cf06e2ff5faf7b15c553fe822d2a2bb && git checkout -B drift-repro ed7243d52bbc1b6d00a3b621b0dcea4925df32b5 && git merge --no-ff ae43bfa29cf06e2ff5faf7b15c553fe822d2a2bb
node scripts/docs-audit/affected-docs.mjs --json ed7243d52bbc1b6d00a3b621b0dcea4925df32b5
|
Contract review (
|
`mount-storage-routes.ts` matches the `*-routes.ts` discovery convention, so
`check:route-envelope` found it and refused it as NOT DECLARED — undeclared is
an error, never a default. It writes no response body of its own: it binds the
three package-internal seams and hands the surface to `registerStorageRoutes`,
so every `/api/v1/storage/*` body is still written by `storage-routes.ts`
through the shared sendOk/sendError pair. Declared `{ responses: 0, ok: 0,
err: 0 }` with a note in the neighbours' shape. No ratchet, no vendorWire, no
other entry touched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
…orage-routes-public-seam
Patch round — F1/F2 discharged (director seat)Head is now F1 — the change.
It was 7 conformant on F2 — reconciled gate line. Which of the two it was: the derivation did not include it. Re-derived from Also run, because The "## Gates" section of the PR body now carries this reading, and the shape sentence is corrected to name the triage seat's preference ( Not done, and why: F3/F4/F5 are advisory and were not in this dispatch's scope; the PR stays draft, unlabelled and un-merged. Recorded for the director seat rather than filed, since filing would widen this round: the derivation's blind spot above is general — Generated by Claude Code |
Contract review (
|
| # | Finding at 210d2b9 |
Status @ ae43bfa |
Evidence |
|---|---|---|---|
| F1 | check:route-envelope red — mount-storage-routes.ts undeclared in MODULES |
Discharged | a69736671 adds the entry; CI step Response-envelope guard on head: ✓ Route-envelope conformance — 11 route module(s) audited: 8 conformant, 2 ratcheted, 1 exempt (§2, §5) |
| F2 | PR-body gate ledger unreconcilable with CI | Discharged | ## Gates now states the family was RED on 210d2b9, explains why the 66-set structurally could not name it, and reports 78 derived / 73 run / 5 NOT-MEASURED exit-3 (§4) |
| F3 | No maintainer ruling; shape was triage-preferred | Discharged | 5579652641 ratifies the single-entry shape; PR body's shape sentence now cites 5571654797 + 5579652641 (§4) |
| F4 | Route-table equality asserted across a fake PluginContext and a real kernel |
Carried, advisory | Test file byte-identical to 210d2b9; unchanged, not owed by this card |
| F5 | Module-level export on package-internal seams |
Carried, informational | storage-service-plugin.ts byte-identical; unreachable via "." (§3) |
1. What moved between 210d2b9 and ae43bfa29
git diff $(git merge-base origin/main ae43bfa29)..ae43bfa29 — merge-base is ed7243d52 (= current origin/main base), 8 files: .changeset/storage-routes-host-mount.md · packages/services/service-storage/README.md · src/index.ts · src/mount-storage-routes.ts · src/mount-storage-routes.test.ts · src/storage-service-plugin.ts · scripts/check-route-envelope.mjs · scripts/engine-double-contract.pinned.json.
- Per-file
git diff --quiet 210d2b9 ae43bfa29 -- <f>: the changeset, README,index.ts,mount-storage-routes.ts,mount-storage-routes.test.tsandstorage-service-plugin.tsare byte-identical to210d2b9. ✅ scripts/check-route-envelope.mjsmoved:git diff --stat 210d2b9 a69736671= that one file, +12 / −0, nothing else — matching the patch comment's claim exactly. ✅scripts/engine-double-contract.pinned.jsonreads as "moved" between the two heads, but that is main-drift, not a PR change: the PR-side hunk (git diff <old-base> 210d2b9 -- <file>vsgit diff ed7243d52 ae43bfa29 -- <file>) is line-for-line identical (the same threemount-storage-routes.test.tsrows,delete/findOne/update,pinned: 1); main itself added 15 lines to that ledger between the two bases. ✅- The merge commit
ae43bfa29has parentsa69736671+ed7243d52;git merge-tree --write-tree a69736671 ed7243d52re-derives tree1155fffec…, equal toae43bfa29^{tree}— a clean, content-neutral merge carrying nothing of its own. ✅ - Governed paths (
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,content/docs/releases/**): absent. ✅
2. The MODULES entry
scripts/check-route-envelope.mjs:267 — 'packages/services/service-storage/src/mount-storage-routes.ts': { responses: 0, ok: 0, err: 0 }, preceded by a [#15169] note. Shape matches the neighbours exactly (share-link-routes.ts:255, storage-routes.ts:242, settings-routes.ts:243 all declare the same three-key zero literal); no ratchet, no vendorWire, no other MODULES row touched (the diff against origin/main is the 12-line insertion only). ✅
The zero is measured the way the gate measures it: the gate's counter (countResponseWrites, ~line 1549) counts <receiver>.json( call sites excluding req.json() request reads. grep -c '\.json(' on mount-storage-routes.ts = 0; broadened to .json( | .send( | .status( | sendOk | sendErr | ok( | err( on non-comment lines = 0. The door's only side effect on the HTTP surface is composeStorageRoutes(http, toGateRegistry(kernel), {...}) at line 130, which hands the surface to registerStorageRoutes; storage-routes.ts is declared at MODULES:242 with the same zero literal, its bodies going through the shared packages/types/src/response-envelope.ts pair (the gate's own line: "all bodies written by packages/types/src/response-envelope.ts (2 write sites, pinned)"). So the entry is structurally true, not measured-and-hoped. ✅
3. ADR-0104 D3 boundary — re-verified on ae43bfa
src/index.tsread in full: the only new exports aremountStorageRoutes(value) andMountStorageRoutesOptions/StorageRouteKernel/StorageRoutesMountReport(types). Grep over non-comment lines forbuildFileReadAuthorizer | buildAuthSessionResolver | findFileHolder | composeStorageRoutes | toGateRegistry: none (the names appear only in the explanatory comments). ✅package.jsonexports:"."only —import/require×types/default, no subpath, no wildcard. ✅MountStorageRoutesOptions(mount-storage-routes.ts) declares exactlybasePath,presignedTtl,sessionTtl,downloadTtl,logger. Non-comment occurrences ofresolveSession | authorizeFileRead | resolveFileHolderin that file: 0. ✅composeStorageRoutes(call sites on head (git grepoverpackages/+scripts/, declaration excluded):mount-storage-routes.ts:130andstorage-service-plugin.ts:441— exactly two, as the ruling states. ✅
4. PR body ## Gates and the shape sentence
- States in terms: "
check:route-envelopewas RED on210d2b9"; that the 66-set at210d2b9could not name it becausedispatch-gatesreaches the family only via the gate script or already-declaredMODULESpaths; that atae43bfathe family is matched via gate sourcepackages/services/service-storage/src/mount-storage-routes.ts, moving the derived total 66 → 78; 78 derived / 73 run / 5 NOT-MEASURED (exit 3,PREREQUISITE NOT MET) / 0 UNRUN,check:route-envelopeamong the 73 green, with the gate's verdict line quoted. Reconcilable with CI (§5). ✅ - Opening sentence: "in the single-entry shape the triage seat preferred (5571654797), ratified by the director seat on finding(service-storage): hosted tenant kernels cannot mount storage's REST routes — registerStorageRoutes needs buildAuthSessionResolver / buildFileReadAuthorizer / findFileHolder, all package-internal; export them so cloud can bridge storage the way it bridges settings #15169 (5579652641)" — both comment ids present, and the prior "the triage seat ruled" overstatement is gone. ✅
- The merge is described as a plain
git mergeofed7243d5, no rebase/force-push — consistent with the two-parent merge commit verified in §1. ✅
5. CI on ae43bfa29
get_check_runs: 36 check runs — 33 success · 0 failure · 0 in_progress · 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in) — all path/opt-in skips, none of them the envelope gate). mergeable_state: clean; still draft, still carrying needs:contract-review.
Lint & Repo Gates (job 101946849166): success, completed 05:46:03Z. Because #16754 (now in the merge) scopes some families of that job by path, I read the job log rather than trusting the job colour: the Run pnpm check:route-envelope step executed at 05:42:44Z (node scripts/check-route-envelope.mjs --self-test && node scripts/check-route-envelope.mjs) and printed ✓ check-route-envelope self-test passed then ✓ Route-envelope conformance — 11 route module(s) audited: 8 conformant, 2 ratcheted, 1 exempt. The step in lint.yml:2656 carries no if: path condition. The verdict is not conditional. ✅
6. The recorded derivation blind spot — does it deserve a card?
The patch agent recorded (not filed) that dispatch-gates cannot name check:route-envelope for a NEW *-routes.ts module until that module is declared in MODULES, i.e. the exact case the gate exists to catch is invisible to every dispatch brief, and proposed "declare the scan surface (the subtree spelling) beside the MODULES roster".
Checked against the tree: this is already ledgered, and the proposed remedy was already refused. scripts/pm/bare-root-worklist.mjs carries CENSUS_REFUSE_WIDE row 'check:route-envelope packages' — verdict REFUSE-WIDE, 2181 of 5837 tracked packages/ files opened (37.4%) at 2aa8456cf, lineage #14131 / #14325 / #14692 / #14695 — with the reasoning that the gate's walk(join(ROOT, 'packages')) population is the root, so a subtree declaration would be TRUE but would name this gate on every card touching a package. So the general finding is known and the subtree spelling is a closed door.
My reading: a card is warranted, but only in a narrower form than the one the patch agent named. What actually triggers this gate's error path is not "any file under packages/" but "a file matching *-routes.ts under packages/ that is not in MODULES" — a pattern-shaped hint, not a subtree. If dispatch-gates can express a pattern hint (or grow one), the width objection in the REFUSE-WIDE row does not apply to it, and the derivation would name the family for exactly the cards that create a new route module. That is a follow-up to #14695's lineage, not a rider on this PR, and not filed here.
Residual findings
R1 — F4 carried (advisory, unchanged). Route-table equality is still asserted across a fake PluginContext and a real kernel; gate parity on the plugin path rests on the shared composition (two call sites, verified). Expectation: none for this PR; a follow-up could mount the plugin on a real kernel with an http-server collector and drive the same 401/403 arms through the plugin's door.
R2 — F5 carried (informational, unchanged). composeStorageRoutes, toGateRegistry, StorageGateRegistry, StorageRoutesComposition remain module-level exports of storage-service-plugin.ts, unreachable through ".". Expectation: none; recorded so "not exported" continues to be read as "not on the published surface".
R3 — Derivation blind spot for new *-routes.ts modules (advisory, not filed). See §6. Expectation: a narrowly scoped card under the #14695 lineage proposing a pattern-shaped hint; no change to this PR.
R4 — PR body ## Tests paragraph reports the pre-merge local run (informational). The 38-file / 593-test figure was measured before ed7243d52 was merged in. Because the six package files are byte-identical to that head and CI's Test Core (1–6) and Type Check · jobs are green on ae43bfa, the claim is covered, not stale in substance. Expectation: none.
What still blocks landing
Nothing from this seat. The PR is draft and carries needs:contract-review; per the ruling, the director seat lands it (non-governed). This seat approves, merges, readies, labels or edits nothing.
Generated by Claude Code
Fixes #15169
Option A, in the single-entry shape the triage seat preferred (5571654797), ratified by the director seat on #15169 (5579652641); carried verbatim by the dispatch
5577865232: one new export,mountStorageRoutes, rather than the three package-internal gate builders the card named.The problem, restated from the measurement
StorageServicePluginmounts/api/v1/storage/*itself, atkernel:ready, on the kernel'shttp-serverservice. A hosted per-environment tenant kernel registers no such service, so that branch logs "no HTTP server available": the storage service,sys_file/sys_upload_session/sys_attachment, the lifecycle hooks and the reap guards are all present, and every/api/v1/storage/*request answers 404 — an app with an attachment field cannot upload. Settings already had a working host bridge becauseregisterSettingsRoutesand everything it needs are public. Storage could not be bridged the same way:registerStorageRoutesneeds three seams that are, deliberately, package-internal — the upload session resolver, the ADR-0104 D3 download authorizer, and the tombstone holder predicate.🔴 What this shape exposes to consumers — read this first
buildFileReadAuthorizeris the ADR-0104 D3 download-authorization gate, so the boundary that moves is a security boundary. Stated exhaustively.Added to the published surface (
packages/services/service-storage/src/index.ts):mountStorageRoutes(http, kernel, options?)MountStorageRoutesOptionsbasePath,presignedTtl,sessionTtl,downloadTtl,logger.StorageRouteKernelgetService, optionalgetServiceAsync, optionalgetKernel.StorageRoutesMountReportbasePathplus four booleans/strings —sessionResolver,downloadAuthorizer,tombstoneHolderResolver,metadataStore.The first parameter is
IHttpServer, the same first parameterregisterSettingsRoutes(http: IHttpServer, …)takes — the ruling asked for verbatim alignment with the bridge settings already proved, and this is it. Only the registration half (get/post/put) is called, so the host's own route-collecting shim satisfies it as readily as a real adapter.NOT added, and unreachable:
buildAuthSessionResolver,buildFileReadAuthorizer,findFileHolder, and the two package-internal seams this refactor introduced,composeStorageRoutesandtoGateRegistry(withStorageGateRegistry/StorageRoutesComposition).Three independent facts make that unreachability mechanical, not a convention:
exportsmap publishes"."alone — no subpath pattern — so no deep import reachesstorage-service-plugin.jsat all.dist/index.jshas 26 runtime exports:mountStorageRoutesPRESENT andregisterStorageRoutesPRESENT (two firing positive controls),buildFileReadAuthorizer/buildAuthSessionResolver/findFileHolder/composeStorageRoutes/toGateRegistryall absent.dist/index.d.tsnames 62 exports; the four new ones aremountStorageRoutes,MountStorageRoutesOptions,StorageRouteKernel,StorageRoutesMountReport, whileStorageGateRegistryandStorageRoutesComposition— the two package-internal types this refactor introduced — are absent from it.exportkeyword; nothing imported them, so this PR drops it (commit "the two gate builders stay module-private"). Measured as import EDGES, not as symbol mentions: animport/export … fromclause naming the symbol, multi-line aware —buildFileReadAuthorizer0,buildAuthSessionResolver0, against firing positive controls in the same run (findFileHolder4,mountStorageRoutes2,StorageServicePlugin15).What a consumer cannot do, and why: it cannot supply, replace or omit any of the three gates.
MountStorageRoutesOptionsnames none ofregisterStorageRoutes' three gate keys (resolveSession,authorizeFileRead,resolveFileHolder), and the door reads named fields off its options — the bag is never passed through — so a widened object smuggling one in is inert. Both directions are pinned: a type-level pin (three@ts-expect-errorliterals, compiled bytsconfig.test.json) and a runtime pin (a consumer "supplying" an always-allow authorizer, after which the anonymous caller is still 401 and nothing is minted).What it does control, stated honestly: the host chooses the kernel. The gates are built from that kernel's
authservice and data engine, so a host that hands over a kernel with a permissiveauthservice gets permissive authentication — exactly as the plugin's own mount does on such a kernel. What is not substitutable is the authorization logic: parent-record reachability, thefileAccessDelegatedispatch, the tenancy-posture read and its outage relay all stay inside the package, in one function, reached by one composition.How the one-definition property is preserved
The point of exporting is that the platform keeps one definition of the download gate — option C (a consumer re-implementing it) is what creates a second, divergent one, and it was refused on the cloud side and ratified in triage.
composeStorageRoutesis now the single placeresolveSession/authorizeFileRead/resolveFileHolderare wired, and it has exactly two callers: the plugin's ownkernel:readymount, andmountStorageRoutes. The plugin no longer builds the three itself — the deleted lines are the ones that used to. So the host's storage door and the plugin's are one code path by construction: they cannot drift apart, because there is nothing to drift.§2 of the new suite pins that as behaviour rather than as structure: the plugin's own
kernel:readymount and the host door register byte-for-byte the same route table, and the host door threads the kernel's async registry into the D3 authorizer's tenancy-posture read exactly as the plugin path does (an unreadable posture store answers the declared 503, never a verdict; the control with a readable posture reaches the 401).resolveFileHolderkeeps its own one-definition property from #10246: it is the reap guard'sfindFileHolder, handed over rather than re-derived, so the sweep that reaps and the download path that refuses cannot answer differently.Behaviour parity — nothing existing changes
registerStorageRoutesandStorageRoutesOptionsare untouched. The plugin mounts what it mounted before: the same base path, the same TTLs, the same three gates, the sameStorageMetadataStore.downloadTtlis threaded through the composition and the plugin passes none —StorageServicePluginOptionshas no such field — so it arrivesundefinedand takes the same?? 300default it took when it was not passed at all. The one thing removed is the plugin's unused privatestorefield, which nothing read. The door resolves the data engine the way the plugin'skernel:readybranch does — a syncgetService('objectql')inside a try/catch,nullon absence — so "no engine" means the same thing on both paths.The gate builders' parameter changed from
PluginContextto a normalised registry slice, produced bytoGateRegistry. That is the same three-way readingresolveAdmissionTenancyPostureused to perform inline (getServiceAsyncoff the value, else offgetKernel(), else absent), performed once so both mount paths share it — and it is what lets a host holding a bare kernel build the gates without impersonating a plugin context.Tests
Run under the container's shared verify lock, four steps,
VERDICT command-exit 0: dependency-closure build, package build,vitest run(38 files, 593 tests, all passed), andtypecheck(tsc --noEmit, the scripts project, andcheck:test-typecheck— which is what makes the three@ts-expect-errorpins below load-bearing rather than decorative). The new suite on its own, verbose: 13 passed, 0 failed.New suite
mount-storage-routes.test.ts, four sections, every fixture kernel a realObjectKernelso the registry classification the posture read depends on is the registry's own:AUTH_REQUIREDwith nothing minted; an admitted member whose parent record is unreachable is 403FILE_DOWNLOAD_DENIED, not 401. Same two directions on the upload gate, plus theowner_idstamp.getService-only accessor (LiteKernel shape) keeps the posture read quiet and the gate bound.storageservice throws naming the remedy; noauthand no engine mounts with the gates off, reports them off, warns once, and the report carries no function.The fixture engine's
delete/update/findOneroute through the producer's own dispatch predicates (assertEngineDeleteDispatch,assertEngineUpdateDispatch,assertEngineFindOnePredicate), with the three (file, verb) pairs registered inscripts/engine-double-contract.pinned.jsonby the gate's own--write— a double looser thanObjectQLis how a dead route ships with a green suite.Lint — a declared narrowing, not a skipped run
pnpm lintis a whole-repoeslint . --no-inline-config; CI owns that run. What is delivered here is the narrowed one, with the three readings that make a narrowing a measurement rather than an omission:ESLint.isPathIgnoredabout each rather than by guessing from the ignore patterns.--format json— 4 files (the diff's lintable ones; the README, the changeset and the JSON ledger are not ESLint's), 0 errors, 0 warnings, exit 0.eslint.config.mjs, and that file says of itself that it "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file, test or not", a claim measured there with a positive control. With no cross-file type program, nothing in this diff can move the verdict on a file it does not touch.Gates
dispatch-gates --changed --commands --repo objectstack-ai/objectstackon the merged tree atae43bfa, every printed command run and recorded byte-for-byte, then--ranto reconcile: 78 derived families, 73 run, 5 NOT-MEASURED, 0 UNRUN (exit 0). All 73 that were measured exited 0,check:route-envelopeamong them.check:route-envelopewas RED on210d2b9, and the earlier ledger on this line did not name it because the derivation at that tree did not contain it. Re-derived from210d2b9itself, that set is 66 commands andcheck:route-envelopeappears nowhere in it — not in the runnable list, not in the artifact-roster block, not among the too-wide-to-place families.dispatch-gatesreaches that family by two spellings only: its own gate scriptscripts/check-route-envelope.mjs, and the module paths already declared in the gate'sMODULEStable, each of which it reads as a gate source. A brand-new*-routes.tsis by construction in neither, so the family stayed silent for the one card that moved it. The earlier "66 derived, 66 run" was therefore an accurate account of a set that structurally could not name this gate — not a mis-reported result — and CI caught it becauseLint & Repo Gatesruns the family unconditionally. Declaringmount-storage-routes.tsinMODULESis also what closes the derivation gap: atae43bfathe family is matched via gate sourcepackages/services/service-storage/src/mount-storage-routes.ts, which is why the derived total moves 66 → 78.Gate verdict on the new head, quoted from the gate's own output:
✓ Route-envelope conformance — 11 route module(s) audited: 8 conformant, 2 ratcheted, 1 exempt(was 7 conformant), with✓ check-route-envelope self-test passedahead of it — thecheck:route-envelopescript runs its self-test first.The five NOT-MEASURED families each exit 3,
PREREQUISITE NOT MET, and each says in its own words that it measured nothing:check:dts-closure,check:dual-build-cjs-loads,check:sourcemap-no-sources-content(all three read built output; this worktree has only this package's dependency closure built),check:i18n(the workspace CLI is not built) andcheck:type-check-debt(--re-measureneeds the workspace closure built). Read as NOT MEASURED — not green, not red; CI builds everything and measures them there. They are recorded asNOT-MEASURED … :: reasonlines in the run record, which is why the reconciliation above separates 73 from 78 rather than claiming 78 measured.Also run, because
dispatch-gatesmarks their rosters as living in a directory one of these paths is in and warns that such silence is a fact about a list rather than a clearance:check-changeset-fixed,check-published-list-mirrors,check:authz-resolver,check:console-injection,check:error-code-casing,check:filter-alias-parity— all exit 0.check:published-readme-exportsis again the one that answersPREREQUISITE NOT MET(exit 3) for the same whole-repo-build reason. (check:engine-double-contractandcheck:i18n-stale-fillare in the derived 78 above and exited 0 there.)The derivation ran with
--repo objectstack-ai/objectstack, which it checked against this checkout'sorigin. It reported STALE TREE on the pre-merge tree, soorigin/mainwas merged (ed7243d5) before deriving; the merge is a plaingit merge, no rebase and no force-push.Changeset
minoron@objectstack/service-storage. Notskip-changeset: that label is for a diff that publishes nothing from a released package, and this one widens a released package's public surface. Notpatch: it is a new export, an addition rather than a fix. Not breaking, so no ADR-0087 disposition marker is owed.Clause-② evidence
Clause-②: yes— the deliverable is by definition a widening of the published surface (four new names inindex.ts, table above), which is limb one of the clause. The dispatch recorded it at claim time and hungneeds:contract-reviewon the card; this PR carries the same label, as the dual-carrier rule requires.验收备注
--writeproduced for them. Both are the gate's sanctioned remedy for a newly pinned double.origin/mainwas merged into this branch before the gate derivation, becausedispatch-gatesreported STALE TREE and named two gate scripts that had moved. The list re-derived identical afterwards.check:published-readme-exportsneeds every published package built and answersPREREQUISITE NOT MET(exit 3) on a partially built tree. That is a property of the gate's whole-repo prerequisite, not of this diff — CI builds everything and measures it there.🤖 Generated with Claude Code
https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
Generated by Claude Code
Generated by Claude Code