Split from #3587, same as #3587 was split from #3563. Tranches 1–2 are complete: the dispatcher ledger (27→0, #3569…#3579) and the REST ledger (mismatch 2→0 gap 43→0, #3609…#3633) both rest at zero with CI-pinned ratchets. One surface remains un-audited — the ledger header (packages/rest/src/rest-route-ledger.ts) records it as the known NOT-COVERED note:
services that autonomously mount routes on the host IHttpServer — service-storage (storage-routes.ts, the SDK's whole storage surface) and service-i18n.
The inventory (13 routes)
packages/services/service-storage/src/storage-routes.ts — 10 mounts:
| Route |
Purpose |
POST {base}/upload/presigned |
presigned-URL issue (the SDK storage.upload path) |
POST {base}/upload/complete |
post-upload finalize |
POST {base}/upload/chunked |
chunked-upload session create |
PUT {base}/upload/chunked/:uploadId/chunk/:chunkIndex |
chunk write |
POST {base}/upload/chunked/:uploadId/complete |
chunked finalize |
GET {base}/upload/chunked/:uploadId/progress |
chunked progress |
GET {base}/files/:fileId/url |
download-URL resolve (the SDK storage.download path) |
GET {base}/files/:fileId |
file metadata |
PUT {base}/_local/raw/:token |
local-driver loopback write (likely server-only) |
GET {base}/_local/raw/:token |
local-driver loopback read (likely server-only) |
packages/services/service-i18n/src/i18n-service-plugin.ts — 3 mounts (behind the registerRoutes option):
| Route |
Purpose |
GET {base}/locales |
available locales |
GET {base}/translations/:locale |
translation bundle |
GET {base}/labels/:object/:locale |
object field labels |
Client side today: storage (2 methods) and i18n (3 methods) namespaces exist and are exercised in production — but no ledger row anywhere backs them, so they are exactly the pre-#3563 posture: expressed, working, unguarded.
Method — tranche 1/2 recipe, one new seam
- These registrars bypass both
RouteManager and the REST server entirely, so neither existing enumeration sees them. Add the enumeration seam the way tranche 2 did for direct-mounts: a conformance test registers the routes against a capturing mock IHttpServer and diffs the captured list against the ledger.
- Ledger location: these services are independent packages; a per-package mini-ledger next to each registrar (verified by that package's own tests) avoids cross-package edges — the tranche-1 lesson (
no runtime→client package edge) applies verbatim.
- Client half: extend
rest-route-ledger-coverage.test.ts's pattern — every row naming a client method must resolve on ObjectStackClient.
- Expected dispositions: the 2
_local/raw loopbacks are server-only (driver-internal token URLs); the chunked-upload family needs a triage — the SDK currently speaks only the presigned path, so chunked is either a real gap (close it) or documented server-only for host apps.
Acceptance
- Every autonomously-mounted route has a reviewed disposition in a CI-guarded ledger.
- The
storage / i18n SDK namespaces are backed by ledger rows (closing the last "expressed but unguarded" surface).
- With all three surfaces ledgered, the strict reverse guard becomes possible: a client method whose target route exists on no surface can be flagged mechanically — file that as the capstone if it doesn't fall out naturally.
Refs: packages/rest/src/rest-route-ledger.ts header (NOT COVERED note); #3563 (tranche 1), #3587 (tranche 2, closed at ratchet 0).
Split from #3587, same as #3587 was split from #3563. Tranches 1–2 are complete: the dispatcher ledger (27→0, #3569…#3579) and the REST ledger (mismatch 2→0 gap 43→0, #3609…#3633) both rest at zero with CI-pinned ratchets. One surface remains un-audited — the ledger header (
packages/rest/src/rest-route-ledger.ts) records it as the known NOT-COVERED note:The inventory (13 routes)
packages/services/service-storage/src/storage-routes.ts— 10 mounts:POST {base}/upload/presignedstorage.uploadpath)POST {base}/upload/completePOST {base}/upload/chunkedPUT {base}/upload/chunked/:uploadId/chunk/:chunkIndexPOST {base}/upload/chunked/:uploadId/completeGET {base}/upload/chunked/:uploadId/progressGET {base}/files/:fileId/urlstorage.downloadpath)GET {base}/files/:fileIdPUT {base}/_local/raw/:tokenserver-only)GET {base}/_local/raw/:tokenserver-only)packages/services/service-i18n/src/i18n-service-plugin.ts— 3 mounts (behind theregisterRoutesoption):GET {base}/localesGET {base}/translations/:localeGET {base}/labels/:object/:localeClient side today:
storage(2 methods) andi18n(3 methods) namespaces exist and are exercised in production — but no ledger row anywhere backs them, so they are exactly the pre-#3563 posture: expressed, working, unguarded.Method — tranche 1/2 recipe, one new seam
RouteManagerand the REST server entirely, so neither existing enumeration sees them. Add the enumeration seam the way tranche 2 did for direct-mounts: a conformance test registers the routes against a capturing mockIHttpServerand diffs the captured list against the ledger.no runtime→client package edge) applies verbatim.rest-route-ledger-coverage.test.ts's pattern — every row naming a client method must resolve onObjectStackClient._local/rawloopbacks areserver-only(driver-internal token URLs); the chunked-upload family needs a triage — the SDK currently speaks only the presigned path, so chunked is either a realgap(close it) or documentedserver-onlyfor host apps.Acceptance
storage/i18nSDK namespaces are backed by ledger rows (closing the last "expressed but unguarded" surface).Refs:
packages/rest/src/rest-route-ledger.tsheader (NOT COVERED note); #3563 (tranche 1), #3587 (tranche 2, closed at ratchet 0).