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
From #108's review discussion (uniform DX across the three transports), and a prerequisite to merging it.
The asymmetry
The root CLAUDE.md claims one shape — a piece is a provider; a slice is a module; a modulith is several slice modules in one root — one shape, all three transports — and the two workers deliver it: AmqpHandler(contract, key) and TemporalWorkflowActivities(contract, key) mint the piece's port from the contract key, so AmqpHandlers(contract)([...]) / TemporalActivities(contract)([...]) compose an anonymous array, exact through the port ids against an UNCOVERED … marker. Adding a slice is: create the directory, run pnpm generate. Zero root edits.
HTTP is the odd one out. api.HttpController(name, fragment) takes a fragment value, not a key — the piece does not know which contract key it implements — so the composing form is a keyed record the root hand-writes: api.HttpRouter(contract)({ orders: ordersController, … }). The edit is compiler-forced (the record is exact), so HTTP never had the WiringDefect class; but the authoring flow is create, generate, then edit the root, and the teaching surface now shows two flows for one concept. #108's slice-tree generator makes the gap visible: workers consume a generated pieces array; HTTP needs named re-exports plus a hand edit.
The package's own spec already contains the observation that closes it: authenticatorPort(scheme) mints Port(`HttpAuthenticator:${scheme}`) — described in packages/http/CLAUDE.md as "the move AmqpHandler(contract, key) makes". This issue is that move, applied to the controller itself.
Proposed shape
// piece: minted by contract key, symmetric with AmqpHandler(orderContract, "orderNotifications")exportconstpiece=api.HttpController(contract,"orders")({place: PlaceOrder,find: FindOrder,logger: Logger},{sync: ... },);// root: an array, symmetric with AmqpHandlers(orderContract)(pieces)exportconstorderRouter=api.HttpRouter(contract)(pieces);
The port id carries the key (HttpController:orders-style), as HttpAuthenticator:<scheme> already does; the bespoke display name ("OrdersController") goes the way the workers' went.
The fragment's type is derived, not passed: Inherit<C[K], RequirementsOf<C>> — the exact computation the keyed record form's ControllerFor already performs, so a root-marked contract's requirements reach the piece at mint time and context.principal types as today.
Coverage: every contract key must be covered by exactly one piece in the array — the workers' UNCOVERED … marker pattern, including the named-missing-key trailing diagnostic once lengths match; two pieces claiming one key are di's duplicate-provider defect at build.
Runtime: the array reassembles into the record by port id, then the existing routerOf walk is unchanged.
With this, #108's generator loses its controller mode entirely: controller.ts pieces join the ordinary pieces array, slices.gen.ts has one shape in all three apps, and adding a slice is create the directory, run generate — everywhere.
Open questions the design must answer
The do-not-break property (controller.test-d.ts's fifth gate): a slice lifts out of the modulith with its piece unchanged. A key-minted piece is keyed against the root contract, so the lifted root plausibly becomes api.HttpRouter({ orders: contract.orders })([piece]) — the fragment wrapped in a one-key record, the piece untouched. The property must survive in some spelling, pinned by the same gate, or this issue is rejected on that ground.
Fate of the keyed record form. The repo prefers THE one way. Does HttpRouter(contract)({...}) survive as a second composing form, or does the array replace it? (The (deps, arm) bare form stays either way — it is the primitive.) Note HttpRouter currently disambiguates its three forms by arity plus whether sync holds a function; an array argument is trivially distinguishable, but four forms is an argument for retiring one.
Scopes and marked fragments ride Inherit/Effective unchanged in principle — auth.test-d.ts's arms must all still fire, both directions, against key-minted pieces.
api.HttpController(contract, key) mints the piece from the key; api.HttpRouter(contract)([pieces]) composes with worker-grade exactness (uncovered key refused and named; duplicate key a build defect).
The lifted-slice property survives with the piece unchanged, pinned where it is pinned today.
examples/order-api reads identically to the two workers: same slice files, same generated slices.gen.ts shape, zero root edits to add a slice.
From #108's review discussion (uniform DX across the three transports), and a prerequisite to merging it.
The asymmetry
The root
CLAUDE.mdclaims one shape — a piece is a provider; a slice is a module; a modulith is several slice modules in one root — one shape, all three transports — and the two workers deliver it:AmqpHandler(contract, key)andTemporalWorkflowActivities(contract, key)mint the piece's port from the contract key, soAmqpHandlers(contract)([...])/TemporalActivities(contract)([...])compose an anonymous array, exact through the port ids against anUNCOVERED …marker. Adding a slice is: create the directory, runpnpm generate. Zero root edits.HTTP is the odd one out.
api.HttpController(name, fragment)takes a fragment value, not a key — the piece does not know which contract key it implements — so the composing form is a keyed record the root hand-writes:api.HttpRouter(contract)({ orders: ordersController, … }). The edit is compiler-forced (the record is exact), so HTTP never had theWiringDefectclass; but the authoring flow is create, generate, then edit the root, and the teaching surface now shows two flows for one concept. #108's slice-tree generator makes the gap visible: workers consume a generatedpiecesarray; HTTP needs named re-exports plus a hand edit.The package's own spec already contains the observation that closes it:
authenticatorPort(scheme)mintsPort(`HttpAuthenticator:${scheme}`)— described inpackages/http/CLAUDE.mdas "the moveAmqpHandler(contract, key)makes". This issue is that move, applied to the controller itself.Proposed shape
HttpController:orders-style), asHttpAuthenticator:<scheme>already does; the bespoke display name ("OrdersController") goes the way the workers' went.Inherit<C[K], RequirementsOf<C>>— the exact computation the keyed record form'sControllerForalready performs, so a root-marked contract's requirements reach the piece at mint time andcontext.principaltypes as today.UNCOVERED …marker pattern, including the named-missing-key trailing diagnostic once lengths match; two pieces claiming one key are di's duplicate-provider defect at build.routerOfwalk is unchanged.With this, #108's generator loses its controller mode entirely:
controller.tspieces join the ordinarypiecesarray,slices.gen.tshas one shape in all three apps, and adding a slice is create the directory, run generate — everywhere.Open questions the design must answer
controller.test-d.ts's fifth gate): a slice lifts out of the modulith with its piece unchanged. A key-minted piece is keyed against the root contract, so the lifted root plausibly becomesapi.HttpRouter({ orders: contract.orders })([piece])— the fragment wrapped in a one-key record, the piece untouched. The property must survive in some spelling, pinned by the same gate, or this issue is rejected on that ground.HttpRouter(contract)({...})survive as a second composing form, or does the array replace it? (The(deps, arm)bare form stays either way — it is the primitive.) NoteHttpRoutercurrently disambiguates its three forms by arity plus whethersyncholds a function; an array argument is trivially distinguishable, but four forms is an argument for retiring one.Inherit/Effectiveunchanged in principle —auth.test-d.ts's arms must all still fire, both directions, against key-minted pieces.examples/order-api, both split how-tos,docs/reference/http.md, the fivecontroller.test-d.tsgates re-pinned against the array form, and feat: the slice tree is generated — slices.gen.ts, fixed slice/piece exports, drift as a spec #108 rebased to drop controller mode.Acceptance
api.HttpController(contract, key)mints the piece from the key;api.HttpRouter(contract)([pieces])composes with worker-grade exactness (uncovered key refused and named; duplicate key a build defect).examples/order-apireads identically to the two workers: same slice files, same generatedslices.gen.tsshape, zero root edits to add a slice.