Skip to content

feat(di)!: an import's needs travel without being re-declared - #87

Merged
btravers merged 4 commits into
mainfrom
feat/needs-travel-through-imports
Aug 21, 2026
Merged

feat(di)!: an import's needs travel without being re-declared#87
btravers merged 4 commits into
mainfrom
feat/needs-travel-through-imports

Conversation

@btravers

Copy link
Copy Markdown
Contributor

Follow-on to #86, which shipped the needs gate one notch too strict.

The change

NeedsGate now reads a module's own providers alone. A port one of them
depends on and nothing here satisfies still has to be named — so a provider can
never silently receive a service from whoever composed the module, which is
what #50 was about. What is dropped is the re-declaration at every hop.

An import's own unmet needs are already published in its type, at the imports
entry a reader is looking at, and start still refuses a root that has not
discharged them. Restating them bought nothing but lines.

Measured

12 of the 22 declarations in the repo were pure propagation. What is left is
what actually reads something:

Declares Because
DatabaseModule [Env] reads DATABASE_URL
observability() [Env], each starter [Env] binds its own configuration
AuditSlice, NotificationsSlice, OrdersSlice [Logger] their handler / controller writes a line
OrderApplicationModule [OrderRepository, Logger] its interactors read them
// reads DATABASE_URL — declares it
const DatabaseModule = Module("Database")({
  needs: [Env],
  provides: [databaseConfig, orderDatabaseProvider],
  exports: [OrderDatabase],
});

// only imports it — declares nothing
export const OrderPersistenceModule = Module("OrderPersistence")({
  imports: [DatabaseModule],
  provides: [orderRepositoryProvider, outboxProvider],
  exports: [OrderRepository, Outbox],
});

That is ConfigModule.forFeature's shape reached without a global — which is
the point Env made: six declarations in order-api before, one of them a
module that reads an environment variable.

order-amqp-worker's root keeps needs: [Env], and that is the rule
working rather than an exception: it provides relayConfig itself, so Env is
its own provider's need and not one inherited from the slices below.

What moved back

Three needs-gate.test-d.ts negatives return to start: a starter's port is
owed by an import, so di's declaration gate has nothing to say about it.
order-application's gate test gains one that separates the two — a module
whose OWN providers read the repositories, declaring nothing — so both
mechanisms stay pinned side by side.

Unmet (the Needs channel) is unchanged and stays deliberately wider than
what the gate asks a module to declare: everything outstanding, however it got
there.

Gate

All six green from the root: format --check, lint, typecheck (31/31),
knip, test (30/30), build (10/10). The changeset from #86 is rewritten
rather than added to, since neither has been released.

NeedsGate now reads a module's OWN providers alone. A port one of them depends
on and nothing here satisfies still has to be named in needs — so a provider can
never silently receive a service from whoever composed the module, which is what
#50 was about. What is dropped is the re-declaration at every hop: an import's
own unmet needs are already published in its type, at the imports entry a reader
is looking at, and start still refuses a root that has not discharged them.

Measured on this repo: 12 of 22 declarations were pure propagation. Dropping
them leaves exactly the modules that read the port — DatabaseModule says
needs: [Env] because it reads DATABASE_URL, and the persistence modules and
slices that import it say nothing. That is ConfigModule.forFeature's shape,
reached without a global.

The Needs CHANNEL is unchanged and stays wider than what the gate asks a module
to declare: everything outstanding, however it got there.
Twelve declarations go: the three roots, both persistence modules, and the four
slices that only inherited what they named. What is left is what actually reads
something — DatabaseModule and observability() and each starter for Env,
AuditSlice and NotificationsSlice and OrdersSlice and the two stand-in services
for Logger, the application modules for their repositories.

order-amqp-worker's root keeps needs: [Env], and the reason is the rule working:
it provides relayConfig itself, so Env is its own provider's need rather than
one inherited from the slices below.

Three needs-gate negatives move back to start — the starter's port is owed by an
import — and order-application's gains one that separates the two gates: a
module whose OWN providers read the repositories, declaring nothing.
The rule in packages/di/CLAUDE.md, modules-and-privacy, compile-time-wiring and
reference/di/modules, the needs note on the three starter sugars, the slices
bullet and the gate inventory in the root CLAUDE.md, and 38 samples that were
declaring what they only inherited.

The two example pages describing a starter-port omission as di's declaration
gate go back to describing it as start's needs channel, which is what it is
again: the port is owed by an import.
Copilot AI lite review requested due to automatic review settings August 21, 2026 19:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refines @btravstack/di’s NeedsGate so it only enforces declarations for ports required by a module’s own providers, while allowing an imported module’s unmet needs to propagate outward without being re-declared at every hop. This reduces noisy needs: [Env] boilerplate across the repo while preserving the invariant that no provider can silently consume a service supplied only by an ancestor.

Changes:

  • Update NeedsGate to check only unmet dependencies from the module’s own provides list (not imported modules’ needs).
  • Remove now-redundant needs: [Env] declarations throughout starters, examples, and fixtures; update gate/type tests accordingly.
  • Refresh documentation and changeset text to describe the new “needs travel through imports” model.

Reviewed changes

Copilot reviewed 59 out of 59 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Removes redundant Env import/needs from root example.
packages/temporal/src/test-fixtures.ts Drops propagated needs: [Env] in Temporal test apps.
packages/temporal/README.md Updates worked example to omit redundant needs: [Env].
packages/temporal/CLAUDE.md Clarifies needs applies to root’s own providers only.
packages/observability/src/test-fixtures.ts Removes propagated needs: [Env] from fixture modules.
packages/observability/README.md Updates example root to omit redundant needs: [Env].
packages/http/src/test-fixtures.ts Removes propagated needs: [Env] from HTTP fixtures.
packages/http/README.md Updates worked examples to omit redundant needs: [Env].
packages/http/CLAUDE.md Aligns docs with “imports’ needs travel” rule.
packages/di/src/module.ts Implements the narrower NeedsGate and updates docs.
packages/di/src/module.test-d.ts Adds a type test proving import needs propagate.
packages/di/CLAUDE.md Updates “Module visibility” spec to new gate behavior.
packages/core/src/test-fixtures.ts Adds rationale comment; keeps needs: [Env] where required.
packages/config/README.md Fixes sample import to include Env where used.
packages/amqp/src/test-fixtures.ts Drops propagated needs: [Env] in AMQP fixtures.
packages/amqp/src/amqp-runtime.test-d.ts Moves negative from declaration gate to start gate.
packages/amqp/CLAUDE.md Updates needs explanation to “own providers only”.
examples/order-temporal-worker/src/test-fixtures.ts Removes propagated needs: [Env] in test roots.
examples/order-temporal-worker/src/slices/fulfillment/module.ts Removes propagated needs from slice module.
examples/order-temporal-worker/src/slices/billing/module.ts Removes propagated needs from slice module.
examples/order-temporal-worker/src/needs-gate.test-d.ts Updates negatives to reflect new gate division.
examples/order-temporal-worker/src/module.ts Removes redundant needs: [Env] from worker root.
examples/order-temporal-worker/README.md Updates snippet to omit redundant needs: [Env].
examples/order-infrastructure/src/module.ts Keeps Env declaration at DatabaseModule; removes propagation.
examples/order-infrastructure/README.md Removes redundant needs: [Env] from importers.
examples/order-application/src/needs-gate.test-d.ts Adjusts negatives to use Module.scoped vs declaration gate.
examples/order-api/src/test-fixtures.ts Drops propagated needs: [Env] in stub APIs.
examples/order-api/src/slices/orders/module.ts Narrows slice needs to Logger only.
examples/order-api/src/slices/customers/module.ts Removes propagated needs: [Env] from customers slice.
examples/order-api/src/needs-gate.test-d.ts Updates negative to fail at start for starter port needs.
examples/order-api/src/module.ts Removes redundant needs: [Env] from API root.
examples/order-api/README.md Updates snippets for new “needs travel” model.
examples/order-amqp-worker/src/test-fixtures.ts Keeps needs: [Env] where root provides env-bound config.
examples/order-amqp-worker/src/needs-gate.test-d.ts Updates negatives to reflect kernel vs di gate split.
examples/order-amqp-worker/src/module.ts Clarifies why this root still declares Env.
docs/tutorial/second-runtime.md Removes redundant Env/needs in tutorial root.
docs/tutorial/getting-started.md Removes redundant Env/needs in tutorial root.
docs/reference/temporal.md Updates reference snippet to omit redundant needs: [Env].
docs/reference/observability.md Updates reference snippet to omit redundant needs: [Env].
docs/reference/http.md Updates reference snippet to omit redundant needs: [Env].
docs/reference/di/modules.md Updates needs semantics and declaration-gate explanation.
docs/reference/config.md Fixes sample import to include Env where used.
docs/index.md Removes redundant Env/needs from landing page sample.
docs/how-to/test-an-application.md Removes redundant needs: [Env] from testing recipe.
docs/how-to/split-a-worker-into-slices.md Updates slice examples to omit propagated needs.
docs/how-to/split-a-router-into-controllers.md Updates slice snippet to omit propagated Env need.
docs/how-to/serve-orpc-over-http.md Removes redundant Env/needs from composition-root step.
docs/how-to/run-a-temporal-worker.md Removes redundant Env/needs from worker root examples.
docs/how-to/protect-a-procedure.md Removes redundant needs: [Env] from protected root snippet.
docs/how-to/open-a-per-request-scope.md Removes redundant needs: [Env] from example module.
docs/how-to/log-and-correlate.md Removes redundant Env/needs from logging recipes.
docs/explanation/starters.md Updates starter example to omit redundant needs: [Env].
docs/explanation/modules-and-privacy.md Updates explanation to reflect new propagation model.
docs/explanation/compile-time-wiring.md Updates narrative to “own providers” + import propagation.
docs/examples/order-temporal-worker.md Updates example snippets to omit redundant needs: [Env].
docs/examples/order-api.md Updates example snippets and gate explanation for new model.
docs/examples/order-amqp-worker.md Updates negative example to fail at start via Needs channel.
CLAUDE.md Updates repo-wide guidance on which gate catches starter-port needs.
.changeset/declared-module-needs.md Updates release note text to match narrowed gate semantics.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/reference/di/modules.md Outdated
Comment thread examples/order-api/README.md
Comment thread docs/examples/order-api.md
Comment thread docs/how-to/split-a-router-into-controllers.md
`needs` was described as not making the port available inside the module, which
reads as "a provider cannot use it" — the opposite of what it is for. A provider
may depend on a declared need and is handed whatever an ancestor supplies; what
declaring does not do is provide the port locally, so it stays outside
`Available` and is still not exportable.

The three `OrdersSlice` samples still said the environment and the logger were
"both named here" while the code beside them named only `Logger`: the sweep
rewrote the value and left the comment. They now say which is which and why.
@btravers
btravers merged commit 8ceb70d into main Aug 21, 2026
13 checks passed
@btravers
btravers deleted the feat/needs-travel-through-imports branch August 21, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants