You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
client.packages declares envelopes two mounted surfaces do not emit: get diverges between them, and install/enable/disable declare { package } where the only serving surface sends the bare row #12034
Measured at c804f0ca5 while implementing #11925 (which deliberately did not bind these four methods for
exactly this reason). Out of that card's scope — it binds erased anys, and each of these is a shape claim
that is wrong independently of the erasure.
GET /api/v1/packages and GET /api/v1/packages/:id are each served by two implementations. The REST
registrar is mounted only when a package service is registered and, in its own words, the direct-mount
routes "shadow live dispatcher twins" (packages/rest/src/direct-mount-composition.ts). Both surfaces are
real deployments, and both route ledgers map the same client method onto both routes.
1. GET /packages/:id — the two surfaces emit different envelopes
unwrapResponse strips exactly one { success, data } envelope (packages/client/src/index.ts:5106), so the
post-unwrap value is InstalledPackage on one surface and { package: InstalledPackage & { source } } on the
other. client.packages.get declares { package: any } — right on REST, wrong on the dispatcher. There is no
single true type, which is why #11925 left it unbound rather than shipping a false narrowing.
Both ledgers agree the one client method covers both routes:
packages.get [runtime] GET /packages/:id responseSchema=None
packages.get [rest ] GET /api/v1/packages/:id responseSchema=None
2. install / enable / disable — a declared envelope no surface emits
These three have no REST twin (the REST registrar mounts only POST /packages/publish, GET /packages, GET /packages/:id, DELETE /packages/:id). The only surface serving them is the dispatcher, and it sends
the bare row:
client method
route
handler
emitted data
client declares
packages.install
POST /packages
domains/packages.ts:315
success(pkg)
{ package: any; message?: string }
packages.enable
PATCH /packages/:id/enable
domains/packages.ts:331
success(pkg)
{ package: any; message?: string }
packages.disable
PATCH /packages/:id/disable
domains/packages.ts:350
success(pkg)
{ package: any; message?: string }
So (await client.packages.enable(id)).package compiles today (the member is any) and is undefined at
runtime; the correct read is the row itself. The declaration is not merely erased, it is false, and the any inside it is what keeps the falsehood invisible.
Why this is worth its own card rather than a rider
Correcting these is a response-shape decision, not an annotation fix, and it forks:
Fix the SDK to match the producer — cheap, but for get it can only be right about one surface.
Fix the producers to agree — the dispatcher and REST twins converge on one envelope; the SDK then has a
single true type to bind. This is the contract-first direction and the only one that makes get bindable.
Either way it needs a clause-② narrowing analysis of its own: today's any members mean any consumer read
compiles, so tightening these breaks callers at compile time. In-repo callers are zero for get/enable/ disable and two for install; the published SDK's external consumers are the real surface.
Related: #11925 (the erasure card that measured this), #11942 (the docs page whose fences no compiler reads).
Activity