fix: the compile-time gates say what they mean - #82
Merged
Conversation
A phantom rest tuple fails as an arity error, and arity errors never print
types — so NO RUNTIME never reached a reader, and tsc's related info pointed
at the wrong fix ("an argument for 'options' was not provided"). The marker
rides an intersection on the module parameter now, where it prints in full.
… is true The three sentences were unasserted after the rest tuple went: @ts-expect-error accepts any error, so an arm could be swapped silently. One expectTypeOf per arm pins the sentence itself. order-temporal-worker's NO RUNTIME fixture owed Logger as well as a runtime, so tsc elaborated the other failure; observability() in its imports leaves the runtime as the only thing missing and the sentence prints. packages/core/CLAUDE.md described a rest tuple failing on arity and claimed parity with di's gate for a hand-spelled bypass that no longer exists. The hatch survives as an ordinary cast; di's gate is still a rest tuple, so the two are no longer the same shape.
…sion
The marker prints LAST: TypeScript names the source type first, and the
source is the piece the caller wrote — di's Provider over the contract,
several hundred characters wide and outside these packages to name. Measured
three alias shapes against tsc; an indexed access into a mapped type loses its
name in the source position where a plain reference and an intersection with
{} keep theirs, but PieceOf is the TARGET, so no shape applied to it moves the
first line. Applying the surviving shape to the real PieceOf produced a
byte-identical diagnostic.
So the remedy available here is the literal itself: a longer string costs
nothing to print, and a reader who reaches the end of the line now reads an
instruction instead of a label. The six documents quoting the old literal move
in the same commit.
The keyed `HttpRouter(contract)(controllers)` form typed a key the contract
does not declare as `never`, so the whole complaint printed as
`Type 'Minted<…>' is not assignable to type 'never'` — the least actionable
type tsc can print, and it named neither the key nor the rule. It now types
as the sentence "UNDECLARED KEY — the contract declares no fragment under
this key", so the message ends on the rule in English.
The trade is real and recorded: `never` made the intersection reduce, so the
target printed in one short token; a string literal does not reduce, so the
middle line carries the expanded `Minted` shape. That is the shape four other
gates in controller.test-d.ts already print, and the line a reader lands on is
the last one.
All five gates still fire against both the plain and the marked contract, and
_ComposedNeedsAreDeclared still holds: on a valid composition the mapped type
is {}, so the sentence is reachable only on the failing call.
di's module gate was measured and left alone — the wrapper it would have taken
came back byte-identical, and TypeScript's own third line already names the
unhandled member.
… the real type Review follow-up on 0081e05. The spec still quoted the parameter type as ': never', eight lines above the prose that described the sentence — a file stating the old type and the new rule in the same breath. Fixed. Review then asked me to record 'neither string names the offending key' as a carried limitation, on my own claim that the key was unreachable from the value position. Before writing that into a file the documentation task will quote, I measured it: the claim is false. The mapped type is keyed by K, so ${K & string} puts the key in the sentence, and the gate now ends on ... is not assignable to type '"UNDECLARED KEY — the contract declares no fragment under billing"' Free: same line count, same intersection width, all five gates still firing against both the plain and the marked contract, _ComposedNeedsAreDeclared still holding. A symbol key collapses K & string to never and with it the template, which is the pre-existing terse behaviour rather than a regression. Taken rather than carried, because carrying it would have put a measured-false statement into the spec — the failure this whole pass is about.
Ten passages told a reader to hover the call to see which port di's arity
gate is missing. Nobody observed a tooltip — TS 7.0.2 ships no JS language
service and the native binary answers no LSP initialize — so the instruction
was the same unmeasured claim about compiler output this branch exists to
remove.
What is measured is the technique that prints them. The rest parameter is
`[error: "UNSATISFIED DEPENDENCIES", missing: N]`, and a value neither slot
accepts turns the arity error into an assignability one that answers a slot
at a time: the label first, then the port. The order is load-bearing and now
stated — a reader who passes two junk arguments sees only the label.
Module.build(Resourceful, n, n)
Argument of type 'number' is not assignable to parameter of type
'"UNSATISFIED DEPENDENCIES"'.
Module.build(Resourceful, "UNSATISFIED DEPENDENCIES", n)
Argument of type 'number' is not assignable to parameter of type 'Scope'.
Module.scoped(OrderApplicationModule, use, {}, "UNSATISFIED DEPENDENCIES", n)
Argument of type 'number' is not assignable to parameter of type
'Logger | OrderRepository'.
The last is `examples/order-application`'s own two open needs, which is what
the example's four passages now quote. Hover survives in three of the ten
places, demoted to what it is: the language service reads the same parameter
type, so it would be expected to show the same ports — inferred, never
observed here.
`compile-time-wiring.md`'s "Hovering a large module shows real channel
unions" goes the same way, to the half that a 400-character diagnostic
already proves: the unions are real, and anything that prints one prints it
at full width.
`needs-gate.test-d.ts` is comment text only — the directive has not moved,
and the gate still fires.
The page never passes its module to `start`, so `Type 'Pool' is not assignable to type 'Env | Scope'` was a plausible string nobody had seen. The pattern is measured five times over on the starters' own gates, so the claim now cites one of those — the captured `Type '"HttpRouter"' is not assignable to type '"@di/Scope"'` — instead of inventing the page's own port into a diagnostic.
`'"orderAudit"'` and `'"fulfillOrder"'` were the examples' keys substituted
into a shape measured on a purpose-built 3-key scratch contract that actually
printed `'"c"'`. Both re-captured against the shipped contracts, with a
2-element array covering one key twice so TypeScript lines the array up
against the marker tuple positionally:
AmqpHandlers(orderContract)([orderNotifications, orderNotifications])
… is not assignable to type '"orderAudit"'.
TemporalActivities(orderContract)([chargeOrder, chargeOrder])
… is not assignable to type '"fulfillOrder"'.
The strings were right; nothing had confirmed it. Each now says which
contract it was measured against, so the next reader does not have to.
`Boot` lost its trailing gate argument in this branch; its reference page still published the old signature. It now matches `boot-fixture.ts`. `tapped` is still a conditional rest tuple, so `NOT EXPORTED` never reaches a reader — measured, `Expected 4 arguments, but got 2.` and nothing else, with the label and the port answering one hand-spelled slot at a time. The page states that the way di's arity gate already states it. That makes four mechanisms, not three. The record counts the three a composing application meets and names the harness's fourth; the testing reference says the reciprocal.
`start.ts` ships the gate intersected onto `module`. The reasoning each comment gives survives — a generic `X` leaves `StartGate<X>` unresolved, so pinning is still required — only the mechanism clause was stale. Comment text only; no code and no directive moved.
The changeset said no call site passed the gate argument; `start.test-d.ts` passed it three times, as the documented hand-spelled bypass, before this branch deleted those calls. Two pages read "is an / surfaces as \`UNSATISFIED DEPENDENCIES\` error", which reads as the printed message. They link the gate the way the swept pages do.
test-an-application.md claimed the tap gate's NOT EXPORTED label reaches a reader at the call site; it does not — only the arity error does. Rewritten to link the measured account, matching how the two sites in ce14658 were corrected. testing.md's own account re-taught di's hand-spelling technique in ~15 lines duplicating entry-points.md#the-gate. Kept the three captured tsc strings and the gate's own one-slot-at-a-time ordering, cut the technique prose to one link.
There was a problem hiding this comment.
Pull request overview
This PR updates the kernel and starter type-level “gates” so TypeScript errors surface actionable, human-readable diagnostics at the call site (instead of arity-only failures), and propagates the clarified gate semantics through package docs and examples. It also tightens several compile-time marker diagnostics (AMQP/Temporal “uncovered” composer markers and HTTP keyed-router “undeclared key” errors) to better explain what’s wrong and (where possible) name the offending key.
Changes:
- Replace
start/runMain/Boot’s trailing phantom rest-tuple gate with aStartGatemarker intersected onto themoduleparameter, so the gate’s sentence prints in TS errors. - Widen composer/keyed-router marker literals (AMQP/Temporal/HTTP) to produce more legible diagnostics and better key naming.
- Align docs, examples, and type-tests to the corrected “what prints” behavior across the different gate mechanisms.
Reviewed changes
Copilot reviewed 62 out of 62 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/testing/src/boot-fixture.ts | Update Boot signature to use module & StartGate (marker intersection) rather than a phantom rest arg. |
| packages/temporal/src/temporal-module.ts | Widen “UNCOVERED ACTIVITIES” marker to a descriptive sentence for clearer TS diagnostics. |
| packages/temporal/CLAUDE.md | Document updated uncovered-activities diagnostic wording and unmet-need vs arity-gate distinctions. |
| packages/http/src/orpc.ts | Improve keyed HttpRouter exactness error by replacing never with a message that names the undeclared key. |
| packages/http/src/auth.test-d.ts | Update type-test commentary and expectations to match the new gate mechanisms/diagnostics. |
| packages/http/CLAUDE.md | Update package spec/docs around which mechanism rejects which wiring mistake and what prints. |
| packages/di/README.md | Minor wording adjustment in README around compile-time gates. |
| packages/di/CLAUDE.md | Clarify what di’s arity-gate prints and how to surface missing ports (phantom-arg technique). |
| packages/core/src/start.ts | Implement StartGate as an intersected marker on module; remove trailing rest-tuple gate arg. |
| packages/core/src/start.test-d.ts | Pin the exact StartGate sentence arms via expectTypeOf, since @ts-expect-error doesn’t assert message text. |
| packages/core/src/run-main.ts | Update runMain signature to mirror start’s intersected StartGate marker. |
| packages/core/CLAUDE.md | Document the new marker-based gate and the resulting diagnostics and bypass story. |
| packages/amqp/src/handler.test-d.ts | Adjust commentary about when uncovered keys are named vs marker-only diagnostics. |
| packages/amqp/src/amqp-runtime.ts | Widen “UNCOVERED HANDLERS” marker to a descriptive sentence for clearer TS diagnostics. |
| packages/amqp/CLAUDE.md | Document updated uncovered-handlers diagnostic wording and unmet-need vs arity-gate distinctions. |
| examples/README.md | Update narrative about which gate mechanism prints what (start marker vs Needs vs di arity gate). |
| examples/order-temporal-worker/src/test-fixtures.ts | Update fixture commentary to reflect marker-on-module gate proof visibility. |
| examples/order-temporal-worker/src/needs-gate.test-d.ts | Adjust needs-gate test to match new start marker behavior; add observability() to isolate the intended negative. |
| examples/order-application/src/needs-gate.test-d.ts | Update explanation of di’s arity-gate printing behavior and how to surface missing ports. |
| examples/order-application/src/module.ts | Update doc comment explaining that the gate “carries” missing ports even if the arity error doesn’t print them. |
| examples/order-application/README.md | Update README to reflect arity-only printing and how to reveal missing ports. |
| examples/order-api/src/test-fixtures.ts | Update fixture commentary to reflect marker-on-module gate proof visibility. |
| examples/order-api/src/needs-gate.test-d.ts | Update needs-gate explanations to distinguish StartGate marker from Needs-channel refusal and from di’s arity gate. |
| examples/order-api/src/module.ts | Update module doc comment explaining “Needs channel emptied” vs kernel marker satisfied. |
| examples/order-amqp-worker/src/test-fixtures.ts | Update fixture commentary to reflect marker-on-module gate proof visibility. |
| examples/order-amqp-worker/src/needs-gate.test-d.ts | Update needs-gate explanations to match new start marker behavior. |
| examples/hexagonal-order-api/README.md | Clarify di arity-gate printed output and the “spell phantom args” diagnostic technique. |
| docs/reference/testing.md | Update Boot signature docs; add explanation of the tap gate as an arity error and what it prints. |
| docs/reference/temporal.md | Update uncovered-activities marker text and clarify where/why the marker appears in TS diagnostics. |
| docs/reference/http.md | Update keyed-router exactness diagnostics and authenticator unmet-need explanation (Needs vs di arity gate). |
| docs/reference/glossary.md | Update “gate” definition to reflect marker-vs-rest-tuple mechanisms and what each prints. |
| docs/reference/di/modules.md | Clarify Needs channel description and link to entry-point gate docs. |
| docs/reference/di/entry-points.md | Document measured arity-only diagnostics and the phantom-arg technique to surface missing ports. |
| docs/reference/core/start.md | Update start signature docs and StartGate explanation to reflect marker intersection and measured output. |
| docs/reference/core/runtime.md | Update wording to match new StartGate sentence for runtime-needs failures. |
| docs/reference/core/exit-codes.md | Update runMain signature docs and describe marker-based gate printing. |
| docs/reference/amqp.md | Update uncovered-handlers marker text and clarify where/why the marker appears in TS diagnostics. |
| docs/index.md | Adjust homepage copy to avoid claiming “arity error” for all compile-time failures. |
| docs/how-to/write-a-runtime.md | Update examples to show the new printed StartGate sentences instead of arity-based labels. |
| docs/how-to/test-an-application.md | Link to the new tap-gate explanation and clarify its behavior. |
| docs/how-to/swap-an-adapter.md | Update the “wrong pairing” example to show the arity-only message and how to surface the missing port. |
| docs/how-to/split-a-worker-into-slices.md | Update uncovered-marker wording and clarify where the marker appears in TS diagnostics. |
| docs/how-to/split-a-router-into-controllers.md | Update explanation of authenticator omission failure (Needs-channel refusal at start). |
| docs/how-to/serve-orpc-over-http.md | Update explanation of the three compile-time checks with the new marker/Needs behavior. |
| docs/how-to/run-a-temporal-worker.md | Update explanation of NO RUNTIME and unmet-need failures to match marker/Needs behavior. |
| docs/how-to/protect-a-procedure.md | Update explanation of authenticator omission (Needs-channel refusal) and what prints. |
| docs/how-to/open-a-per-request-scope.md | Update UNSATISFIED UNIT NEEDS explanation to match marker/printed sentence behavior. |
| docs/how-to/manage-a-resource.md | Update di arity-gate example to show what prints and how to surface missing ports. |
| docs/how-to/keep-a-port-private.md | Clarify di arity-gate output vs start unmet-need output that names the port. |
| docs/explanation/why-start.md | Update explanation to match that the compile-time line produces named diagnostics (not necessarily arity errors). |
| docs/explanation/starters.md | Clarify unmet-need refusal printing (Needs-channel) vs di arity gate. |
| docs/explanation/scopes-and-resources.md | Update reference to the di entry-point gate link and wording. |
| docs/explanation/one-process-one-runtime.md | Update “NO RUNTIME” description to match marker-based refusal wording. |
| docs/explanation/design-decisions.md | Update gate design-decision section to reflect marker-on-module gate and its bypass story. |
| docs/explanation/compile-time-wiring.md | Comprehensive rewrite to distinguish gate mechanisms, document measured output, and record the corrected mapping. |
| docs/examples/order-temporal-worker.md | Update example narrative to match new NO RUNTIME and unmet-need behaviors/diagnostics. |
| docs/examples/order-application.md | Update example narrative to show arity-only di gate output and how to surface missing ports. |
| docs/examples/order-api.md | Update example narrative to match new marker-based NO RUNTIME and Needs-channel refusal printing. |
| docs/examples/order-amqp-worker.md | Update example narrative to match new marker-based NO RUNTIME and Needs-channel refusal printing. |
| CLAUDE.md | Update root guidance to distinguish start marker, Needs-channel refusal, and di’s arity gate; update uncovered-marker wording. |
| .changeset/gates-say-what-they-mean.md | Changeset describing the gate diagnostic improvements and signature change. |
| .changeset/authenticated-contracts.md | Changeset text updated to reflect authenticator omission as unmet-need refusal (not di arity gate). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The rest tuple went with cbe8e24; the forwarding comment still called the discharge a tuple. bootFixture's equivalent already said cast.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #66.
Every gate in this repo worked. The documentation described diagnostics they
never printed — and the sweep to fix that kept catching the same defect inside
itself, three review rounds running. Every claim here was measured by stripping
an
@ts-expect-errorand readingtsc's output; the raw before/after is in thenew The record section of
docs/explanation/compile-time-wiring.md.What changed in the code
start's gate now prints its sentence. The threeStartGatearms rode aphantom rest tuple, whose failure is an arity error — and an arity error never
prints a type, so
NO RUNTIMEnever reached a reader and TypeScript's relatedinformation pointed at the wrong fix ("an argument for 'options' was not
provided"). The marker is intersected onto the
moduleparameter now, and aparameter type is something TypeScript prints:
start,runMainandbootFixturelose their trailing gate argument. Noproduction call site passed one — it existed to be omitted.
The composer gates say the rule in English.
@btravstack/amqpand@btravstack/temporal'sUNCOVERED HANDLERS/UNCOVERED ACTIVITIESmarkers,and
@btravstack/http'sUNDECLARED KEY, replaced a barenevertarget. Thehttp one goes further than planned: the mapped type is keyed by
K, so atemplate literal puts the offending key in the sentence —
"UNDECLARED KEY — the contract declares no fragment under billing".Three findings that outgrew the plan
1. There were three gate mechanisms, not two. An unmet need at
startisnot di's
UNSATISFIED DEPENDENCIESgate and never was. Measured, it isplain assignability of the
Needschannel againststart'sModule<X, E, Scope | Env>parameter — and it names the offending port twice,which makes it the best diagnostic in the repo. Thirteen places called it
di's gate, including the root
CLAUDE.mdand a shipped changeset.2. Then there were four. The whole-branch review found
tappedin@btravstack/testingstill a conditional rest tuple — the exact shape thisbranch proved prints nothing but arity.
docs/reference/testing.mdwaspublishing both a
Bootsignature this branch had deleted and aNOT EXPORTEDlabel no reader ever sees. Measured:
Expected 4 arguments, but got 2.3. The phantom arguments answer one slot at a time.
Module.build(m, n, n)prints only the
"UNSATISFIED DEPENDENCIES"label mismatch; the label must bepassed through before the compiler will name
Scope. Without that stated, areader following the technique reads the first mismatch as the technique
failing.
The documentation
44 claims corrected across 45 files — ten
CLAUDE.mdfiles, the docs site, thepackage READMEs, the example workspaces and the
@ts-expect-errorcommentsthemselves. Ten sites told readers to hover for the missing ports; nobody
had ever observed that tooltip, and it could not be verified here (TS 7.0.2
ships no JS language service). Three survive, relabelled as explicit inference;
the rest now state the measured fact — spelling the phantom arguments by hand
surfaces the ports.
Not fixed, deliberately
Root
CLAUDE.md:457says "Three different mechanisms". Its own sentencescopes that to the four
needs-gatefiles, so it is not wrong — but it is theone line a reader could take as the census, now that the census is four. Worth
a decision rather than a drive-by edit.
Gate
format --check·lint·typecheck31/31 (zero unused directives) ·knip·
test29/29 ·build10/10 · docs build. 87@ts-expect-errordirectivesbefore and after, none added, removed, or now guarding a different call.