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
The root CLAUDE.md currently states the refusal flatly:
There is noDefect construction, no overrideProvider, no accumulation of runtimes, and no recoverFailure-style channel-moving helper. Swapping an adapter is composing a different module, which di already documents and the type checker already verifies.
Reopening it, because the cost is now measurable rather than theoretical.
What the refusal actually costs today: four hand-maintained copies of composition roots, all of them in examples/, which is the teaching surface.
"a parallel root rather than OrderAmqpWorker itself because nothing can be layered over a graph that already provides Logger" — order-amqp-worker/src/test-fixtures.ts:44
"A parallel root rather than OrderApi itself for the same reason … nothing can be layered over a graph that already provides" — order-api/src/test-fixtures.ts:100
And each must be kept in sync by hand. PR #49 proved the drift risk concretely: adding two slices to OrderAmqpWorker meant remembering to add them to the recording root too, and the fixture's TSDoc had to grow a paragraph explaining that it "mirror[s] OrderAmqpWorker's own root". A copy with no gate is the thing this repo's own documentation rule warns about, applied to composition instead of prose.
The Logger case is the sharpest form: observability() mints fresh providers per call and di dedupes by reference, so a second call is [di] two providers registered for port "LoggerConfig" (measured). There is no way to layer a recording sink over a real root — only to rebuild the root beside it.
It is also the missing half of #50. That issue asks whether a slice should see the root's providers (needs-bubble-up, today) or only what it imports (NestJS). The reason Nest can afford strict import-visibility is that its testing module has overrideProvider; recomposition is not its substitution mechanism. Here, recomposition is the mechanism, which is exactly why import-visibility would close the seam. Decide these two together or the second one is unanswerable.
Where it would live, and what stays true. Nest's overrideProvider is on Test.createTestingModule, not on the application. The same split works here: it belongs in @btravstack/testing, beside bootFixture and tapped, and production composition stays override-free — so the original argument ("swapping an adapter is composing a different module") survives intact for the thing it was actually about.
The part worth designing carefully is how much of the compile-time guarantee an override keeps. Overriding a port with a provider of the same service type is checkable, and should be a type error otherwise. What an override cannot check is whether the resulting graph still makes sense — which is the honest cost, and is the same cost tapped already accepts.
Acceptance
A decision recorded in packages/testing/CLAUDE.md and in the root CLAUDE.md's public-surface section, which currently states the refusal.
If adopted: the override is type-checked against the port's service, lives only in @btravstack/testing, and the four parallel roots in examples/ collapse to overrides on the real root.
If the refusal stands: the four parallel roots get a stated maintenance rule, since nothing currently fails when one drifts from the root it mirrors.
The root
CLAUDE.mdcurrently states the refusal flatly:Reopening it, because the cost is now measurable rather than theoretical.
What the refusal actually costs today: four hand-maintained copies of composition roots, all of them in
examples/, which is the teaching surface.examples/order-amqp-worker/src/test-fixtures.tsRecordingAmqpWorker— the wholeAmqpModulerootexamples/order-api/src/test-fixtures.tsexamples/order-temporal-worker/src/test-fixtures.tsEach says why, in its own words:
And each must be kept in sync by hand. PR #49 proved the drift risk concretely: adding two slices to
OrderAmqpWorkermeant remembering to add them to the recording root too, and the fixture's TSDoc had to grow a paragraph explaining that it "mirror[s]OrderAmqpWorker's own root". A copy with no gate is the thing this repo's own documentation rule warns about, applied to composition instead of prose.The
Loggercase is the sharpest form:observability()mints fresh providers per call and di dedupes by reference, so a second call is[di] two providers registered for port "LoggerConfig"(measured). There is no way to layer a recording sink over a real root — only to rebuild the root beside it.It is also the missing half of #50. That issue asks whether a slice should see the root's providers (needs-bubble-up, today) or only what it imports (NestJS). The reason Nest can afford strict import-visibility is that its testing module has
overrideProvider; recomposition is not its substitution mechanism. Here, recomposition is the mechanism, which is exactly why import-visibility would close the seam. Decide these two together or the second one is unanswerable.Where it would live, and what stays true. Nest's
overrideProvideris onTest.createTestingModule, not on the application. The same split works here: it belongs in@btravstack/testing, besidebootFixtureandtapped, and production composition stays override-free — so the original argument ("swapping an adapter is composing a different module") survives intact for the thing it was actually about.The part worth designing carefully is how much of the compile-time guarantee an override keeps. Overriding a port with a provider of the same service type is checkable, and should be a type error otherwise. What an override cannot check is whether the resulting graph still makes sense — which is the honest cost, and is the same cost
tappedalready accepts.Acceptance
packages/testing/CLAUDE.mdand in the rootCLAUDE.md's public-surface section, which currently states the refusal.@btravstack/testing, and the four parallel roots inexamples/collapse to overrides on the real root.