Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cbe8e24
fix(core)!: the start gate names what is missing
btravers Aug 20, 2026
f5b7d24
fix(core): the start gate's arms are asserted, and its spec says what…
btravers Aug 20, 2026
db7223d
fix(amqp,temporal): the composer gates print a sentence, not an expan…
btravers Aug 20, 2026
0081e05
fix(http): the undeclared-key gate names the rule, not 'never'
btravers Aug 21, 2026
ac12553
fix(http): the undeclared-key gate names the key, and the spec quotes…
btravers Aug 21, 2026
03d1924
docs: the kernel gate's diagnostic, and di's, as measured
btravers Aug 21, 2026
bab2a08
docs: the how-to pages say what each gate prints
btravers Aug 21, 2026
52c38e5
docs: the reference pages quote the gates' real diagnostics
btravers Aug 21, 2026
a77b4f4
docs: the example walkthroughs tell the two start-time refusals apart
btravers Aug 21, 2026
a85f635
docs: the package specs name the mechanism each gate uses
btravers Aug 21, 2026
905527c
docs: the example and starter comments name the mechanism, not the wr…
btravers Aug 21, 2026
7d861a6
docs: the READMEs say the arity gate carries the port, not prints it
btravers Aug 21, 2026
a46909b
docs: the arity gate's ports are spelled out, not hovered
btravers Aug 21, 2026
af5be64
docs: keep-a-port-private quotes a gate that was actually run
btravers Aug 21, 2026
e4bd996
docs: the uncovered key was re-measured on the real contracts
btravers Aug 21, 2026
9af980d
docs: record what each gate printed before and after
btravers Aug 21, 2026
f26a4c9
docs: the harness gate is measured, and it is the fourth mechanism
btravers Aug 21, 2026
e04a755
docs: the example fixtures name the marker, not the rest parameter
btravers Aug 21, 2026
ce14658
docs: the record claims only what the diff shows
btravers Aug 21, 2026
b4aa9a0
docs: a seventh site, and one shared technique instead of two copies
btravers Aug 21, 2026
06e90d3
docs: runMain's comment names the cast, not a tuple that is gone
btravers Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .changeset/authenticated-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ contract rather than detecting one that was forgotten.
`@btravstack/http` resolves the principal through a new `Authenticator` port —
`HttpAuthenticator<P>()([deps], { sync })`, an ordinary di provider, wired on
`HttpModule`'s `authenticator` option. A contract that marks nothing needs no
authenticator; a marked router whose root provides none is di's existing
`UNSATISFIED DEPENDENCIES` gate, and an authenticator minted on a different
authenticator; a marked router whose root provides none carries the port as an
unmet need `start` refuses, and an authenticator minted on a different
identity than the router is refused at `HttpModule`. A marked procedure whose
authenticator declines is answered `UNAUTHORIZED` before dispatch, with the
handler never running and no reason reaching the caller — `Unauthenticated`
Expand Down
21 changes: 21 additions & 0 deletions .changeset/gates-say-what-they-mean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@btravstack/core": minor
"@btravstack/amqp": minor
"@btravstack/temporal": minor
"@btravstack/http": minor
"@btravstack/testing": minor
---

The compile-time gates name what is missing. `start`'s markers rode a phantom
rest tuple, whose failure is an arity error — and arity errors never print
types, so `NO RUNTIME` never reached a reader and TypeScript's related info
pointed at the wrong fix. They ride the module parameter now.

`start`, `runMain` and `bootFixture` no longer take the trailing gate argument.
No production call site passed one; the documented hand-spelled bypass went
with it, so this is a signature change without a migration.

The same widening reached the composers: `AmqpHandlers`'s/`TemporalActivities`'s
`UNCOVERED HANDLERS`/`UNCOVERED ACTIVITIES` marker and `HttpRouter`'s
`UNDECLARED KEY` marker now say the rule in English and name the missing key,
where each used to end on a bare `"UNCOVERED HANDLERS"` or `never`.
34 changes: 23 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,12 +445,18 @@ type checker already verifies.
**`start`'s** gate (`order-api`, `order-temporal-worker`,
`order-amqp-worker` — its `NO RUNTIME` arm, since no starter's runtime
declares a `needs` any more; `order-api`'s also pins the `unit` halves) and
**di's** need on the starter's port (a composition importing `http()` /
the **unmet need** on the starter's port (a composition importing `http()` /
`temporal({ contract, workflows })` / `amqp({ contract })` without providing
the router / activities / handlers carries the starter's port as an unmet
need `start` refuses); the fourth,
`order-application`'s, pins **di's** `UNSATISFIED DEPENDENCIES` gate on
`Module.scoped`. They are different gates and easy to conflate. `start`'s
the router / activities / handlers carries the starter's port in `Needs`, and
`start`'s `module` parameter takes only `Scope | Env`, so it fails to assign —
measured: a `TS2345` ending on
`Type '"HttpRouter"' is not assignable to type '"@di/Scope"'`, which names the
port); the fourth, `order-application`'s, pins **di's**
`UNSATISFIED DEPENDENCIES` gate on `Module.scoped`, which is a rest-tuple
**arity** error printing `Expected 5 arguments, but got 2` and nothing else.
**Three** different mechanisms, easy to conflate — and only the first prints
its name. Do not call the second "di's `UNSATISFIED DEPENDENCIES` gate": an
earlier revision of this file did, and it is wrong in both halves. `start`'s
`UNSATISFIED RUNTIME NEEDS` arm is pinned only by `packages/core`'s own
`start.test-d.ts`, since every shipped runtime declares `needs: []`.
`examples/` is not the only place the gate is pinned by a **type test**:
Expand Down Expand Up @@ -670,9 +676,13 @@ AuditSlice, observability()], … })`),
the piece's own port id rather than on a record position — and
`AmqpHandlers(contract)([...])` / `TemporalActivities(contract)([...])`
compose them: every key the contract declares must be covered (an uncovered
one is refused at the call, against an `"UNCOVERED HANDLERS"` /
`"UNCOVERED ACTIVITIES"` marker that names the missing key too once the
array's length matches the marker tuple's own length of 2), and two slices
one is refused at the call, against an `"UNCOVERED HANDLERS — …"` /
`"UNCOVERED ACTIVITIES — …"` marker — at the **tail of the third line** of a
`TS2769`, past three hundred characters of the caller's own contract, which
is not shortenable from inside either package because the width is in the
type arguments rather than in a name; the missing key is named too once the
array's length matches the marker tuple's own length of 2, as a **separate**
diagnostic on the trailing element whose target is the bare key), and two slices
both discharged for one key are di's duplicate-provider defect at build —
the same exactness the keyed HTTP
form gets from the shape of the record it composes, reached here through the
Expand Down Expand Up @@ -735,9 +745,11 @@ CustomersSlice, observability()], exports: [Logger] })`** is the whole
`port` back off
`Serving.info`; binding, the drain and the trace-id policy are the
package's. Two gates keep the composition honest at compile time: a root
that forgets `http()` fails on arity (`NO RUNTIME`), and one that imports
it without providing `orderRouter` fails di's own gate at `start`, since the
starter's runtime provider depends on its router port.
that forgets `http()` is refused against
`"NO RUNTIME — the module exports no port declared over RuntimePort"`, the
sentence intersected onto `start`'s `module` parameter, and one that imports
it without providing `orderRouter` leaves `HttpRouterPort` in `Needs`, which
the same parameter refuses by assignability — not di's arity gate.
- **oRPC is pinned to an exact beta.** `@orpc/{client,contract,server}` sit at
`2.0.0-beta.28` in the catalog because oRPC v2's `latest` dist-tag is still
the **1.x** line, while `@unthrown/orpc` peers on `^2.0.0-beta`: an unpinned
Expand Down
17 changes: 14 additions & 3 deletions docs/examples/order-amqp-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,9 @@ an exchange, never a consumer.

## The gate

`needs-gate.test-d.ts` pins `NO RUNTIME`, and di's gate spelled with the
`amqp()` primitive — the sugar cannot leave the handlers out, which is what it
is for:
`needs-gate.test-d.ts` pins `NO RUNTIME — …`, and the unmet-need refusal
spelled with the `amqp()` primitive — the sugar cannot leave the handlers out,
which is what it is for:

```ts
const HandlerlessAmqp = Module("HandlerlessAmqp")({
Expand All @@ -320,6 +320,17 @@ const HandlerlessAmqp = Module("HandlerlessAmqp")({
const _missingHandlers = start(HandlerlessAmqp, options);
```

Two different diagnostics, worth telling apart. The first is `start`'s marker:
the module argument fails to match
`Module<…> & "NO RUNTIME — the module exports no port declared over RuntimePort"`,
and the sentence is the last line. The second is the `Needs` channel: the
handlers port is left outstanding and `start`'s `module` parameter takes only
`Scope | Env`, so what prints is
`Type 'HandlersInstanceOf<…>' is not assignable to type 'Env | Scope'` — wide,
because the contract expands, but ending on
`Type '"AmqpHandlers"' is not assignable to type '"@di/Scope"'`, which names the
port. Neither is di's `UNSATISFIED DEPENDENCIES` arity gate.

## Where to go next

- The other two deployments: [Order API (HTTP)](/examples/order-api),
Expand Down
25 changes: 16 additions & 9 deletions docs/examples/order-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,11 @@ directions of `start`'s own gate and di's, side by side:
const _missingRuntime = start(RuntimelessApi, options);
```

`RuntimelessApi` is the same list of slices without `http(...)`: `start`'s phantom
rest tuple becomes a required argument naming the absence, and the call fails
on arity. It provides `bearerAuthenticator` even so, deliberately: the contract
`RuntimelessApi` is the same list of slices without `http(...)`: `start`'s
phantom marker becomes the sentence
`"NO RUNTIME — the module exports no port declared over RuntimePort"`, and the
module argument fails to match its parameter type — the sentence is the error's
last line. It provides `bearerAuthenticator` even so, deliberately: the contract
marks `orders`, so a graph carrying the router without an authenticator has an
unmet need too, and an arm that could fail either way pins neither gate.

Expand All @@ -524,12 +526,17 @@ const RouterlessApi = Module("RouterlessApi")({
const _missingRouter = start(RouterlessApi, options);
```

This one is **di's** gate, not the kernel's: `http()`'s runtime provider
depends on the starter's own router port through di, so a composition that
imports the starter without providing the router carries an unmet need, and
`start` — which accepts only `Scope | Env` outstanding — refuses the module.
There is no `UNSATISFIED RUNTIME NEEDS` arm here, because the shipped runtime
declares no needs.
This one is the **`Needs` channel**, not the kernel's marker: `http()`'s runtime
provider depends on the starter's own router port through di, so a composition
that imports the starter without providing the router carries an unmet need, and
`start` — whose `module` parameter accepts only `Scope | Env` outstanding —
refuses it. What prints is that assignability failure, and it names the port:
`Type 'HttpRouterPort' is not assignable to type 'Env | Scope'`, down to
`Type '"HttpRouter"' is not assignable to type '"@di/Scope"'`. It is **not**
di's `UNSATISFIED DEPENDENCIES` arity gate, which guards `Module.build` and
`Module.scoped`; conflating the two is easy and the distinction is the point of
having both pinned here. There is no `UNSATISFIED RUNTIME NEEDS` arm, because
the shipped runtime declares no needs.

```ts
// @ts-expect-error — UNSATISFIED UNIT NEEDS: the module does not export Logger for RequestModule to read.
Expand Down
14 changes: 12 additions & 2 deletions docs/examples/order-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,14 +284,24 @@ activities or handlers that implement it.
`start`'s, and easy to conflate with it:

```ts
// Negative: nothing provides `OrderRepository`, so the gate becomes a required
// two-element tuple and the call is an arity error naming the unmet need.
// Negative: nothing provides `OrderRepository`, so di's rest parameter is a
// required two-element tuple the call does not pass.
// @ts-expect-error — UNSATISFIED DEPENDENCIES: no OrderRepository is provided.
const _unwiredOrders = Module.scoped(OrderApplicationModule, (ctx) =>
ctx.get(PlaceOrder).execute("o-1", 1),
);
```

What that prints is `error TS2554: Expected 5 arguments, but got 2.` and
nothing else — an arity error carries no type, so neither the
`UNSATISFIED DEPENDENCIES` label nor `OrderRepository` appears in it. Both are
in the rest parameter's type, and hand-spelling the phantom arguments is what
prints them: pass the label through as the fourth argument and the fifth reports
`Argument of type 'number' is not assignable to parameter of type
'Logger | OrderRepository'` — measured, this vertical's own two open needs.
`start`'s three arms are the deliberate contrast: they ride the `module`
parameter precisely so their sentence prints.

Each vertical's gate is pinned separately, which is the split showing up in
the type tests: a graph that provides `OrderRepository` still cannot scope
`CustomerApplicationModule`, and one that provides the customer repository
Expand Down
13 changes: 10 additions & 3 deletions docs/examples/order-temporal-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,22 @@ shutdown to escalate to. See

## The gate

`needs-gate.test-d.ts` pins `NO RUNTIME` (the graph without the starter fails
on arity) and di's gate spelled with the `temporal()` primitive, since the
sugar cannot leave the activities out at all:
`needs-gate.test-d.ts` pins `NO RUNTIME — …` (the graph without the starter
fails to match the sentence intersected onto `start`'s `module` parameter) and
the unmet-need refusal spelled with the `temporal()` primitive, since the sugar
cannot leave the activities out at all:

```ts
// @ts-expect-error — UNMET NEED: the module's needs channel carries the activities port, which nothing provides.
const _missingActivities = start(ActivitylessTemporal, options);
```

That second one is the `Needs` channel, not di's `UNSATISFIED DEPENDENCIES`
arity gate: `start`'s `module` parameter accepts only `Scope | Env`
outstanding, so the activities port fails to assign and the diagnostic ends on
`Type '"TemporalActivities"' is not assignable to type '"@di/Scope"'` — the
port named, after several lines of the contract expanding.

Dropping one slice's import while still providing the composed activities is
a different failure — the runtime `WiringDefect` the wiring rule above
describes — and is not something a compile-time gate can catch, so it is
Expand Down
Loading
Loading