@codefast/di 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +94 -17
  3. package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
  4. package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
  5. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
  6. package/dist/container/binding-chain.js +254 -0
  7. package/dist/container/container.d.ts +11 -2
  8. package/dist/container/container.js +293 -73
  9. package/dist/core/binding-builders.d.ts +114 -0
  10. package/dist/core/binding-declaration.d.ts +127 -0
  11. package/dist/core/binding-declaration.js +187 -0
  12. package/dist/core/binding.d.ts +62 -138
  13. package/dist/core/binding.js +54 -17
  14. package/dist/core/module.d.ts +8 -5
  15. package/dist/core/module.js +17 -3
  16. package/dist/core/registry.d.ts +17 -28
  17. package/dist/core/registry.js +92 -133
  18. package/dist/core/state-epoch.d.ts +18 -1
  19. package/dist/core/state-epoch.js +17 -0
  20. package/dist/core/tag.js +1 -1
  21. package/dist/decorators/inject.js +6 -5
  22. package/dist/decorators/injectable.js +4 -2
  23. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
  24. package/dist/decorators/metadata-record.d.ts +9 -0
  25. package/dist/decorators/metadata-record.js +20 -0
  26. package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
  27. package/dist/{errors/errors.js → errors.js} +101 -17
  28. package/dist/index.d.ts +14 -10
  29. package/dist/index.js +7 -6
  30. package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
  31. package/dist/injection/descriptor.d.ts +1 -1
  32. package/dist/injection/descriptor.js +3 -7
  33. package/dist/introspection/dependency-graph.d.ts +8 -3
  34. package/dist/introspection/dependency-graph.js +48 -26
  35. package/dist/introspection/explanation.d.ts +101 -0
  36. package/dist/introspection/explanation.js +151 -0
  37. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  38. package/dist/introspection/inspector.d.ts +9 -2
  39. package/dist/introspection/inspector.js +30 -28
  40. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  41. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
  42. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  43. package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
  44. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  45. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  46. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  47. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  48. package/dist/metadata/types.js +1 -0
  49. package/dist/metadata/verify.d.ts +43 -0
  50. package/dist/metadata/verify.js +133 -0
  51. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +5 -4
  52. package/dist/metadata/verifying-reader.js +53 -0
  53. package/dist/resolution/async-fan-out.d.ts +12 -0
  54. package/dist/resolution/async-fan-out.js +26 -0
  55. package/dist/resolution/cache/activation-need.d.ts +1 -2
  56. package/dist/resolution/cache/activation-need.js +16 -19
  57. package/dist/resolution/cache/class-introspector.d.ts +38 -36
  58. package/dist/resolution/cache/class-introspector.js +79 -159
  59. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
  60. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
  61. package/dist/resolution/context.d.ts +16 -24
  62. package/dist/resolution/context.js +47 -56
  63. package/dist/resolution/path.d.ts +128 -0
  64. package/dist/resolution/path.js +167 -0
  65. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
  66. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
  67. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  68. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
  69. package/dist/resolution/resolver.d.ts +11 -12
  70. package/dist/resolution/resolver.js +573 -356
  71. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  72. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  73. package/dist/resolution/select/constraints.js +1 -1
  74. package/package.json +87 -67
  75. package/dist/container/binding-builders.js +0 -305
  76. package/dist/core/binding-scope.d.ts +0 -12
  77. package/dist/core/binding-scope.js +0 -12
  78. package/dist/metadata/verifying-metadata-reader.js +0 -31
  79. package/dist/resolution/path/resolution-path.d.ts +0 -93
  80. package/dist/resolution/path/resolution-path.js +0 -116
  81. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  82. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  83. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  84. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  85. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  86. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  87. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  88. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,381 @@
1
1
  # @codefast/di
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1010](https://github.com/codefastlabs/codefast/pull/1010) **Breaking:** `resolveAsync`, `resolveOptionalAsync` and `resolveAllAsync` on a disposed container — or on a child of
8
+ one — now return a promise rejected with `DisposedContainerError` instead of throwing it, and `resolveAllAsync(token)`
9
+ likewise rejects when a `when()` predicate throws or an alias cycle is followed, so every async method reports its
10
+ failure through the promise it returns. Code that awaits these calls, or chains `.catch()`, behaves as before or better;
11
+ only a caller that wrapped the call itself in `try`/`catch` without awaiting it no longer sees the error there, and
12
+ should await it instead.
13
+
14
+ - [#995](https://github.com/codefastlabs/codefast/pull/995) Add `container.explain(token, options)`, which answers why a request selects the binding it does without instantiating
15
+ anything. It reads the container chain the way `resolve` does and decides by the same rules, so its answer is the
16
+ engine's own. It reports each registry it read with every candidate and its verdict (`eligible`, `slot-mismatch`,
17
+ `predicate-refused`, `collection-member`), the rule that decided (`sole-candidate`, `sole-predicate`, `most-criteria`,
18
+ `default-alias`, `ambiguous`), the binding the request ends on after every alias, and an `outcome` naming the error
19
+ `resolve` would throw when it ends on none. `when()` predicates run as they would in `resolve`, and `options.ancestors`
20
+ explains a request made from inside other resolutions, so a predicate that reads the parent sees the one it would.
21
+
22
+ - [#999](https://github.com/codefastlabs/codefast/pull/999) **Breaking:** a binding's slot is declared before its strategy —
23
+ `bind(Logger).whenNamed("file").to(FileLogger).singleton()`. `when()`, `whenNamed()`, `whenTagged()`, `whenDefault()`
24
+ and `many()` now come before `to*()` and are refused after it with `ChainAlreadyRegisteredError`, so a binding registers
25
+ once, in its final slot, and no later step moves it, displaces something else or brings back what it displaced. Scope
26
+ and lifecycle hooks stay after `to*()`. To migrate, move each chain's slot steps in front of its `to*()` call.
27
+
28
+ The registry no longer parks the bindings a re-slot displaced or restores them, so a bind with a name or a tag costs a
29
+ quarter to two fifths less. `BindingBuilder`, `ConstantBindingBuilder` and `AliasBindingBuilder` lose their `Names` type
30
+ parameter, since the slot is settled before they exist.
31
+
32
+ - [#1005](https://github.com/codefastlabs/codefast/pull/1005) **Breaking for deep subpath imports only** — the root entry `@codefast/di` is unchanged. `src/` now follows three rules,
33
+ and the published subpaths follow `src/` with no exception: a directory names a family of two or more modules and a lone
34
+ module sits flat at its layer; a directory carries the topic and a file its role, so no file repeats its directory's
35
+ name; and the exports map is the source tree, so the `strip` that kept the introspection modules at flat specifiers is
36
+ gone.
37
+
38
+ Moves: the `MetadataReader` verification leaves `resolution/cache/class-introspector` for `metadata/verify` — it was the
39
+ one value import pointing up from `metadata/` into `resolution/`; the `RESOLUTION_DIAGNOSTICS` channel leaves `errors/`
40
+ for `introspection/diagnostics`; the fluent chain's contract leaves `core/binding` for `core/binding-builders`, and the
41
+ class implementing it is `container/binding-chain`; `effectiveBindingScope` folds into `core/binding`.
42
+
43
+ Renamed specifiers: `./errors/errors` → `./errors`; `./errors/diagnostics` → `./introspection/diagnostics`;
44
+ `./ambient/active-container` → `./ambient-container`; `./container/binding-builders` → `./container/binding-chain`;
45
+ `./core/binding-scope` → gone (`effectiveBindingScope` is in `./core/binding`); `./injection/resolve-options` →
46
+ `./injection/dependency-slot`;
47
+ `./metadata/{metadata-keys,metadata-types,metadata-reader-token,symbol-metadata-reader,verifying-metadata-reader}` →
48
+ `./metadata/{keys,types,reader-token,symbol-reader,verifying-reader}`; `./lifecycle/{lifecycle-manager,scope-manager}` →
49
+ `./lifecycle/{hooks,scopes}`; `./decorators/{lifecycle-decorators,decorator-metadata}` →
50
+ `./decorators/{lifecycle,metadata-record}`; `./resolution/path/resolution-path` → `./resolution/path`;
51
+ `./resolution/plan/{instantiation-plan,plan-codegen}` → `./resolution/plan/{compiler,codegen}`;
52
+ `./resolution/select/binding-select` → `./resolution/select/candidates`; `./resolution/cache/binding-lookup-cache` →
53
+ `./resolution/cache/lookup`; `./{inspector,explanation,dependency-graph,graph-adapters/*}` →
54
+ `./introspection/{inspector,explanation,dependency-graph,graph-adapters/*}`. New: `./core/binding-builders`,
55
+ `./metadata/verify`.
56
+
57
+ `@codefast/di-testing` follows the one specifier it imports, `metadata/verifying-reader`.
58
+
59
+ ### Patch Changes
60
+
61
+ - [#996](https://github.com/codefastlabs/codefast/pull/996) A request with criteria that matches no slot is fast again when the container holds no alias: `resolveOptional()` of a
62
+ missing tagged slot no longer probes for a default-slot alias to forward to in a registry that never held one.
63
+
64
+ - [#1009](https://github.com/codefastlabs/codefast/pull/1009) `resolveAsync` no longer rejects with `AsyncResolutionError` when a later sibling reads a singleton or scoped binding
65
+ synchronously — a `toDynamic` factory's `ctx.resolve`, an `@inject` accessor, a top-level `resolve` — while the async
66
+ lane is still materialising it from an earlier sibling. A level of the async lane now yields only where a dependency or
67
+ hook really awaits, so an instance built without yielding is cached before the next sibling starts, and only a
68
+ materialisation that is genuinely pending is published in flight.
69
+
70
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A scope refinement costs less at bind time. `singleton()`, `transient()` and `scoped()` now do nothing when the scope
71
+ does not change, and release only what exists when it does, so `bind(T).to(X).singleton()` straight after registration
72
+ no longer calls into the scope manager.
73
+
74
+ - [#997](https://github.com/codefastlabs/codefast/pull/997) Every read on a child container is cheaper. The child's check that no ancestor was disposed is now one epoch compare
75
+ that V8 inlines into each entry point, the walk up the chain runs only after some container was disposed, and a new
76
+ child starts confirmed live, so a per-request child skips the walk on its first read.
77
+
78
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) Resolving a class binding for the first time is cheaper. The metadata cache now keeps one record per class, which a cold
79
+ resolve looks up once instead of reading a separate map for each fact, and a class with no constructor parameters is
80
+ built without allocating an empty argument list. This mostly speeds up containers that bind and resolve many class
81
+ singletons at startup.
82
+
83
+ - [#1001](https://github.com/codefastlabs/codefast/pull/1001) The first resolve of a class binding is cheaper. The class's parameter list, and the check that a subclass is not
84
+ dropping its base's declared dependencies, are settled once per class instead of once per instantiation, and the class
85
+ asked about last is answered without a map lookup, so a hundred singletons of one class bind and materialize about a
86
+ fifth faster.
87
+
88
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A root-level `resolveAll()` read in a loop over the same token, as an event bus does for every event, is cheaper: the
89
+ lookup cache keeps the last collection in front of its map, so a repeat read skips the map lookup.
90
+
91
+ - [#997](https://github.com/codefastlabs/codefast/pull/997) A container that reads a collection once no longer builds a value list it never reads again. A root-level `resolveAll`
92
+ keeps its stable value list from the second read on, the way an instantiation plan compiles on repeat, so a fresh
93
+ container per request pays only for the members it resolves.
94
+
95
+ - [#996](https://github.com/codefastlabs/codefast/pull/996) A resolve that carries options on a root container gets back much of the speed 0.11.0 took from it. The check that
96
+ refuses a disposed chain grew in that release enough that V8 stopped inlining the lookup behind that entry point; a live
97
+ root now pays two field reads for it, and only a child reads the dispose epoch.
98
+
99
+ - [#998](https://github.com/codefastlabs/codefast/pull/998) A root container's `resolve(token, options)` no longer pays for the check a child makes that no ancestor was disposed.
100
+ Each container is created with the `resolve` of its role, so the root's never carries the child's path and the rule that
101
+ a disposed ancestor refuses its descendants stays as it was.
102
+
103
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A scoped binding whose scope changes stops handing children the instance they cached before the change. A binding
104
+ registered on a parent, resolved from a child, then refined `scoped()` → `transient()` → `scoped()` gave that child its
105
+ old scoped instance back, because only the registering container's cache was cleared. Every container now files a scoped
106
+ instance under a key the binding replaces when it leaves `scoped`, so the cached instance is never found again.
107
+
108
+ - [#1003](https://github.com/codefastlabs/codefast/pull/1003) A teardown reads a class's `@preDestroy` methods once for a run of bindings of that class, instead of once per binding,
109
+ and names the token only when a hook fails.
110
+
111
+ - [#1002](https://github.com/codefastlabs/codefast/pull/1002) A rebind that owes no teardown allocates nothing for the binding it displaces, and a collection read skips its scan for
112
+ dangling aliases in a chain that never held one.
113
+
114
+ ## 0.11.0
115
+
116
+ ### Minor Changes
117
+
118
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) A default-slot alias is a transparent pointer: a request whose criteria no slot of the alias's own token matches is
119
+ forwarded, criteria and all, to its target — `resolve(AbstractLogger, { name: "file" })` reaches `Logger`'s `"file"`
120
+ binding. An exact slot on the alias's token still wins, the nearest container still answers first, and `has` agrees;
121
+ `resolveAll` keeps filtering by the alias token's own slots.
122
+
123
+ - [#896](https://github.com/codefastlabs/codefast/pull/896) Retire the async cascade lane: every `resolveAsync` now runs on the branch lane, so a factory's context answers from its
124
+ own ancestors before and after an `await`. A `whenParentIs` binding requested after an `await` is now selected as the
125
+ sync lane selects it, `ctx.graph.resolutionPath` names the level's own ancestors, and a cycle formed entirely from
126
+ post-`await` edges is named from the true root. A transient class or factory root with a statically visible graph is
127
+ answered by the compiled async plan. `AsyncCascadeContext` and `resolveAsyncFromCascade` are removed from
128
+ `resolution/context`; `ResolutionDiagnostics.builtSubsystems` reports `resolver.asyncRootLevel` once the async lane has
129
+ been entered.
130
+
131
+ - [#937](https://github.com/codefastlabs/codefast/pull/937) Add declared modules: `Module.fromBindings(name, [binding(key, definition), …])` (also `SyncModule.fromBindings`) builds
132
+ a `SyncModule` from a list, each `binding()` stating one binding with the fluent chain's own vocabulary — `to`,
133
+ `toSelf: true`, `toConstantValue`, `toDynamic`, `toDynamicAsync`, `toResolved` / `toResolvedAsync` with `deps`,
134
+ `toAlias`, then `whenNamed`, `whenTagged` (one criterion or several), `when`, `many: true`, `scope` and the lifecycle
135
+ hooks. A definition is typed exactly as the chain is — the key alone decides the value type and the slot names, and the
136
+ compiler rejects a second strategy, a scope on a constant or an alias, a hook on an alias, `onDeactivation` off a
137
+ singleton, and a slot on a collection member. `binding()` checks the same rules at runtime for plain JavaScript and
138
+ throws the new `InvalidBindingDeclarationError` (`INVALID_BINDING_DECLARATION`, with `tokenName` and `reason`), or
139
+ `ManyBindingSlotError` / `SelfBindingRequiresClassError` as the chain does.
140
+
141
+ A declared module loads, unloads, ref-counts and imports exactly as the equivalent `Module.create` setup would, and
142
+ registers each binding already in its final shape, so a binding with a slot, a scope or a hook skips the chain steps a
143
+ fluent setup pays on every load. Nothing a resolve reads changes. `SyncModule`'s `MODULE_SETUP` member now holds either
144
+ the setup callback or the declarations.
145
+
146
+ - [#925](https://github.com/codefastlabs/codefast/pull/925) A constant that a plain last-wins `bind()` displaces now still runs its `onDeactivation` hook at `dispose()`. A
147
+ displaced constant left the registry — so the dispose sweep, which walks the registry, never reached it — and, carrying
148
+ no cached instance, it was absent from the singleton cache too, falling between the two. The container now records a
149
+ displaced constant that still owes a deactivation and drains it at dispose; a refinement that restores the binding (a
150
+ `when*()` that moves the winner off the slot) takes it back out of that set, so nothing deactivates twice. The
151
+ park-and-restore behaviour of `bind()` is otherwise unchanged.
152
+
153
+ - [#931](https://github.com/codefastlabs/codefast/pull/931) Disposing a container now reaches its descendants. A child whose parent (or any ancestor) has been disposed is itself
154
+ disposed: `isDisposed` reads `true`, and every `resolve`, `has` and mutation is refused with `DisposedContainerError`,
155
+ including the child's own bindings — previously a child kept building transients and constants from a torn-down chain,
156
+ and `child.has` and `child.resolve` disagreed. The guard keeps the resolve path cheap: a root still reads only its own
157
+ `#disposed` field, and a child adds one call-free compare of a dedicated dispose-epoch cell against the epoch at which
158
+ its chain was last confirmed live — the ancestor walk runs only after some container in the process is disposed. The
159
+ dispose epoch is separate from the state epoch, so a per-request child's dispose does not invalidate the chain-version
160
+ memo that keeps deep resolves cheap, and `createChild()` is untouched. `resolveAsync` refuses a disposed chain with a
161
+ synchronous throw at the entry, matching how it already guarded a self-disposed container, and `dispose()` stays ungated
162
+ so an `await using` child of a disposed ancestor still tears down cleanly.
163
+
164
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) `generateDependencyGraph({ includeParent: true })` follows resolution's own walk up the chain: every ancestor's bindings
165
+ join the graph, a single dependency connects to the nearest container holding a binding its slot matches — a child
166
+ binding the request cannot select no longer hides the parent's — and an `injectAll` fans out across the whole chain.
167
+ `buildDependencyGraph` takes the ancestor registries, nearest first, in place of the parent registry.
168
+
169
+ - [#921](https://github.com/codefastlabs/codefast/pull/921) A subclass that inherits declared constructor dependencies but declares none of its own — the natural
170
+ `class Derived extends Base {}` with an implicit constructor — is now rejected with `MissingMetadataError` instead of
171
+ being built with `undefined` arguments. Constructor metadata is opt-in per class, so an implicit constructor previously
172
+ slipped through the `target.length === 0` check and produced a silently-broken instance. The error names the base and
173
+ how many dependencies it declares. A subclass whose own `@injectable([])` declares zero deps is unaffected — it is built
174
+ with zero arguments, as declared.
175
+
176
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) Decorators report a runtime without `Symbol.metadata` as `MissingDecoratorMetadataError` at the declaration, naming the
177
+ decorator and the fix (`(Symbol as { metadata?: symbol }).metadata ??= Symbol.for("Symbol.metadata")` in a module
178
+ imported first), instead of failing with a bare `TypeError` on the first metadata write. TypeScript compiles
179
+ `context.metadata` to `undefined` on such a runtime; the library does not install the symbol itself, since the package
180
+ declares no side effects.
181
+
182
+ - [#926](https://github.com/codefastlabs/codefast/pull/926) Module `unload` is now symmetric with `import`, and `unbindAll` resets module bookkeeping. An `import` inside a module's
183
+ setup incremented the imported module's ref-count but nothing ever decremented it, so `unload(A)` left a module `A`
184
+ imported behind bound forever — the container now records each module's imports and, when a module's own ref-count
185
+ reaches 0, releases the imports it took, unbinding an imported module only when its ref-count reaches 0. And
186
+ `unbindAll()` / `unbindAllAsync()` now clear the module ref-count, binding-id and import tables after deactivation, so a
187
+ module loaded before `unbindAll` can be `load()`ed again instead of being silently skipped as already-loaded.
188
+
189
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) A sync `rebind()` whose old binding owes an async deactivation now fails exactly like a sync `unbind()` whatever shape
190
+ the token has: `rebind()` itself throws `AsyncDeactivationError`, the old bindings are removed, and nothing is
191
+ committed. A lone default binding used to let `rebind()` pass and then throw from `.to*()` after the replacement was
192
+ already registered.
193
+
194
+ - [#929](https://github.com/codefastlabs/codefast/pull/929) `runWithContainer` is now typed as synchronous-only. The ambient container lives in a module-level variable restored in
195
+ a `finally`, so it lasts only the callback's synchronous run — it does not survive an `await`. A callback returning a
196
+ `Promise` was silently accepted and lost the context; its return type is now
197
+ `Result extends Promise<unknown> ? never : Result`, so an async callback resolves to `never`.
198
+ `MissingContainerContextError` now says the context does not survive an `await`, and the TSDoc, README and SPEC state
199
+ the sync-only constraint. The engine internals are unchanged — they only ever ran synchronous callbacks.
200
+
201
+ - [#894](https://github.com/codefastlabs/codefast/pull/894) Four size thresholds are gone, each replaced by the algorithm it approximated: alias chains fold exactly to any length
202
+ (`ALIAS_HOP_LIMIT` is removed from `resolution/cache/binding-lookup-cache`); a generated plan is one statement per node
203
+ and inlines to any depth; every synchronous lane checks a cycle by the binding's in-flight flag at any depth
204
+ (`RESOLUTION_SET_THRESHOLD` and `enterResolutionPath` are removed from `resolution/path/resolution-path`); a
205
+ multi-criterion request is selected by one scan at any binding count. `PLAN_CODEGEN_THRESHOLD` is 1024, the measured
206
+ break-even of generating a plan against running its closure.
207
+
208
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) `UnreachableLifecycleHookError` carries a `reason` — `"unbound"` or `"no-deactivatable-binding"` — and an
209
+ `onDeactivation` hook on a token whose every binding is transient or scoped is reported as that, rather than as a token
210
+ nothing is bound to.
211
+
212
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) `validate()` checks an `optional()` dependency whenever it is bound, so a singleton capturing a bound transient or
213
+ scoped dependency through `optional()` is a `ScopeViolationError`; an optional dependency that is not bound still
214
+ imposes nothing.
215
+
216
+ - [#976](https://github.com/codefastlabs/codefast/pull/976) `engines.node` is now `>=24.0.0`, up from `>=22.12.0`, and Node 22 is no longer supported. Node 24.0.0 is the first
217
+ release with explicit resource management built in (`using`, `await using`, `DisposableStack`, `AsyncDisposableStack`,
218
+ `SuppressedError`) and all of ES2025, so the packages use both as the platform ships them instead of shimming them for
219
+ an older line, and the CI matrix runs the unit suite on 24.0.0 itself. Move to Node 24, or stay on the current minor
220
+ while a deployment still runs Node 22.
221
+
222
+ ### Patch Changes
223
+
224
+ - [#977](https://github.com/codefastlabs/codefast/pull/977) The README states the browser floor: Chrome and Edge 136, Firefox 136, and Safari 18.4 or later, the first releases that
225
+ ship every ES2025 builtin. `@codefast/di`'s README also says what a browser program without explicit resource management
226
+ does: it keeps `skipLibCheck` on and calls `dispose()` instead of `await using`.
227
+
228
+ - [#922](https://github.com/codefastlabs/codefast/pull/922) A lifecycle hook that returns a rejecting `Promise` when reached from a synchronous lane no longer crashes the process.
229
+ `runActivationSync` and `runDeactivationSync` call each hook before they can tell it is async, so on discovering a
230
+ returned `Promise` they now adopt its rejection (a no-op `.catch`) before throwing `AsyncActivationError` /
231
+ `AsyncDeactivationError` — a rejecting `@postConstruct`, `onActivation`, `onDeactivation` or `@preDestroy` can no longer
232
+ become an unhandled rejection that ends the process. The hook has still run; retry on `resolveAsync` / `unbindAsync` to
233
+ await it properly.
234
+
235
+ - [#896](https://github.com/codefastlabs/codefast/pull/896) An async level with several dependencies, and a `resolveAllAsync` collection, report the first failing dependency in
236
+ declaration order — the order the sync lanes report — instead of whichever rejection happened to settle first. Every
237
+ dependency still starts before anything is reported. A node with one dependency awaits it directly, with no fan-out to
238
+ settle.
239
+
240
+ - [#924](https://github.com/codefastlabs/codefast/pull/924) `child.has(token)` no longer throws when a parent container is disposed but the child is not. `has` recursed through the
241
+ parent's **public** `has`, which asserts the container is live, so a live child that could still `resolve` a
242
+ parent-owned token threw `DisposedContainerError` from `has` alone — breaking the `if (c.has(token)) c.resolve(token)`
243
+ guard on a healthy container. It now recurses through an internal `#hasInChain` that reads the parent's registry
244
+ directly, keeping the disposed-guard on the public entry point of the container being called.
245
+
246
+ - [#894](https://github.com/codefastlabs/codefast/pull/894) The cold path allocates only what it uses: a container builds its lookup memo, class introspector, context pools and
247
+ lone map on first use; a root's plan is compiled on the request that repeats it, so a container that resolves a root
248
+ once never compiles; a binding's activation need is stamped on the binding instead of memoized in a per-resolver map
249
+ (`ResolutionDiagnostics.builtSubsystems` no longer lists `resolver.activationNeedMemo`); teardown clears instances
250
+ without splicing the singleton list or pairing each binding; a rebind of a lone token is one registration whose
251
+ displaced binding is deactivated on the spot; a fresh registration is one probe and one write; a chain's own `.many()`
252
+ re-slots without probing the registry; each error class names itself with a literal.
253
+
254
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) A reader passed through `ContainerOptions.metadataReader` now outranks a `MetadataReaderToken` binding for that
255
+ container's children too, at every depth, as it already did for the container itself; a token binding still reaches the
256
+ children of a container given no reader.
257
+
258
+ - [#920](https://github.com/codefastlabs/codefast/pull/920) `resolveOptional`, `resolveOptionalAsync`, `resolveAll` and `resolveAllAsync` no longer throw when they reach an alias
259
+ whose chain ends at a token nothing matches. An alias is a transparent pointer, so a dangling chain is the same miss the
260
+ target itself would be: the optional lanes return `undefined`, and a dangling alias member is skipped from a collection
261
+ rather than failing the whole fan-out. A required `resolve` / `resolveAsync` still throws, and an alias **cycle** still
262
+ throws `CircularDependencyError` — a cycle has no absent reading.
263
+
264
+ - [#976](https://github.com/codefastlabs/codefast/pull/976) The README states what a program needs for the declarations' disposal members: the explicit resource management types,
265
+ which no numbered `lib` declares before ES2027. `@types/node` 24 or later loads them, and so does `ESNext.Disposable` in
266
+ `lib`. Without either, TypeScript 7 fails inside `container.d.ts` with TS2550 under `skipLibCheck: false`, and at
267
+ `await using` with TS2318. `@codefast/di-testing` drops a `/// <reference lib="esnext.disposable" />` that TypeScript 7
268
+ stripped from its emitted declarations anyway, and takes the lib from its `tsconfig.json` instead.
269
+
270
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) A dependency slot declaring `tags: []` states no criterion, as a binding slot already did:
271
+ `injectionSlotToResolveOptions` answers `undefined` for it, and such a dependency no longer escapes a compiled plan.
272
+
273
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) A `MetadataReader` passed through `ContainerOptions` is asked about a class once for the life of the reader: its answers
274
+ are memoized per class, and every container handed the same reader shares one verifying wrapper. Lifecycle metadata used
275
+ to be read on every activation, and each root container asked again.
276
+
277
+ - [#894](https://github.com/codefastlabs/codefast/pull/894) The React Flow graph export lays its nodes out on a square grid derived from the node count rather than a fixed five
278
+ columns.
279
+
280
+ - [#934](https://github.com/codefastlabs/codefast/pull/934) `MissingMetadataError` for a subclass that inherits declared constructor dependencies no longer advises giving it an
281
+ explicit constructor, which is rejected the same way. It points at `@injectable([...deps])` — `@injectable([])` for a
282
+ class that takes none — or a `toDynamic()`/`toResolved()` binding.
283
+
284
+ - [#894](https://github.com/codefastlabs/codefast/pull/894) Every resolution lane now answers a graph identically. A per-request child that misses a token its parent's bindings all
285
+ declined reports `NoMatchingBindingError` as the parent does, not `TokenNotBoundError`; a sibling on the async
286
+ interpreted lane is selected against its own path rather than the earlier sibling's frame; a dynamic factory that
287
+ resolves its own token from its synchronous prefix is reported as a cycle before it runs a second time.
288
+
289
+ - [#905](https://github.com/codefastlabs/codefast/pull/905) The chain-versioned lookup memo answers a repeated token from a method small enough for its hot callers to inline: the
290
+ hit is the whole of `defaultEntry`, and everything that fills the memo is the miss. An alias resolve, a parent-owned
291
+ resolve from a child and every other lookup that reaches the memo pays one inlined compare where it paid a call.
292
+
293
+ - [#938](https://github.com/codefastlabs/codefast/pull/938) A constant with an `onDeactivation` hook that a bind inside a module load displaces now runs that hook at `dispose()`,
294
+ as it already did when a plain `container.bind()` displaced it. A module's chains registered without the container's
295
+ displacement bookkeeping, so the displaced constant left the registry untracked and nothing ever tore it down; this held
296
+ for `Module.create` setups and declared modules (`Module.fromBindings`) alike. Unloading the displacing module still
297
+ does not restore the displaced binding — its deactivation stays owed to `dispose()`.
298
+
299
+ - [#918](https://github.com/codefastlabs/codefast/pull/918) A `{ name, tag }` request now selects the same binding as its `{ tags: [slotName.of(n), tag] }` spelling, closing two
300
+ selection bugs. A name that has been interned nowhere in the process no longer makes the request a miss: the request's
301
+ criteria can still be a superset of a slot's, so an index miss falls through to the scan and the parent walk instead of
302
+ returning early — the answer no longer depends on whether unrelated code has ever called `whenNamed` with that string.
303
+ And the name-plus-tag fast lane now declines to full selection whenever the token carries any `when()` predicate
304
+ candidate, so the more-specific rule's predicate step is honoured on this lane as on every other.
305
+
306
+ - [#896](https://github.com/codefastlabs/codefast/pull/896) Every binding predicate now reads one `ConstraintContext` shape: the shared root context, the context a selection builds
307
+ over a live path, an async level's prefix and the inspector's probe are all `DefaultConstraintContext`, exported from
308
+ `resolution/context`. A predicate's call site stays monomorphic, and the per-selection object literal with its eager
309
+ `ancestors` slice is gone — `ancestors` is now sliced on first read, as `ctx.graph` already did.
310
+
311
+ - [#916](https://github.com/codefastlabs/codefast/pull/916) `LEARNING.md` is removed, and with it the Learning page the docs site rendered for this package. What a shape is and
312
+ what it guarantees stays in `ARCHITECTURE.md`, the behavioural contract in `SPEC.md`, and usage in `README.md`, whose
313
+ document list no longer points at the removed file.
314
+
315
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) `resolveAll` returns a token's bindings in registration order however a chain refines its own binding. Binding a default
316
+ and then a named, tagged, member or predicate binding used to list the later one first, because the refinement re-added
317
+ what it had displaced behind it; each binding now keeps the place its first registration gave it.
318
+
319
+ - [#902](https://github.com/codefastlabs/codefast/pull/902) A transient factory root resolved with `resolveAsync()` is handed one resolution context per binding, built on the first
320
+ resolve and reused by every later one — its path is its own frame alone and the request carries no options, so the
321
+ context is a function of the binding. The root allocated an array and a context per resolve; it allocates nothing now,
322
+ and a concurrent root reads the same, correct, path after an `await`.
323
+
324
+ - [#919](https://github.com/codefastlabs/codefast/pull/919) A `NoMatchingBindingError`'s "Available slots" diagnostic no longer throws when a bound slot carries a tag value that
325
+ cannot be stringified — a bigint, a null-prototype object, or one whose `toString` throws. `bindingSlotToString` (and
326
+ the dependency-graph edge label) now render such a value as `<unprintable>` through a shared `stringifyTagValue` guard,
327
+ so the real `NoMatchingBindingError` surfaces instead of a masking `TypeError`, matching the guard the request-side
328
+ diagnostic already had.
329
+
330
+ - [#932](https://github.com/codefastlabs/codefast/pull/932) `SPEC.md` carries the behavioural contract and nothing else. The source-tree listing, the copied export barrel, the
331
+ build configuration and the roadmap of already-shipped work are gone — a reader can now rename any file under `src/`
332
+ without contradicting the spec. `Public API` states the surface as a rule instead of a second copy of `index.ts`, which
333
+ had already drifted from it, and `Scope and requirements` states the runtime, module format and TypeScript floor a
334
+ consumer must bring. New `DECISIONS.md` takes the background and the whole InversifyJS v8 comparison, `README.md` takes
335
+ the testing patterns with the `MetadataReader` example corrected to `Container.create({ metadataReader })`, and
336
+ `CONTRIBUTING.md` takes the packaging and decorator-toolchain notes.
337
+
338
+ Checking the spec against the source turned up four public names it never described: `BindingSnapshot.isMany`, the
339
+ `DependencySlot` that both `InjectionDescriptor` and `ParamMetadata` extend, and `bindingSlotToResolveOptions`, which
340
+ turns a snapshot's slot back into the `ResolveOptions` that selects it. All three are specified now, and the claim that
341
+ `toAlias()` returns the one builder with no type parameter is corrected — it has no _value_ type parameter, but it does
342
+ carry the token's slot names.
343
+
344
+ - [#927](https://github.com/codefastlabs/codefast/pull/927) `@postConstruct` / `@preDestroy` on a symbol-keyed method now fail at the declaration with the new
345
+ `SymbolKeyedLifecycleError`, instead of a misleading `InvalidMetadataError` at resolve that blamed a `MetadataReader`
346
+ the caller never configured. The lifecycle reader keys methods by their string name, so a symbol-keyed method could
347
+ never be found again; the decorator now rejects it up front, where the mistake is.
348
+
349
+ - [#957](https://github.com/codefastlabs/codefast/pull/957) `MissingDecoratorMetadataError` suggests installing `Symbol.metadata` as
350
+ `(Symbol as { metadata?: symbol }).metadata ??= Symbol.for("Symbol.metadata")`, which type-checks in a `.ts` file; the
351
+ bare `Symbol.metadata ??= …` it quoted failed with TS2540, since TypeScript declares the symbol `readonly`.
352
+
353
+ - [#933](https://github.com/codefastlabs/codefast/pull/933) A rejected async `onActivation` on a transient `toDynamic` binding — per-binding or container-level — no longer surfaces
354
+ as an unhandled rejection: the sync resolve still throws `AsyncActivationError`, and the hook's promise is adopted as it
355
+ already was on every other lane.
356
+
357
+ - [#965](https://github.com/codefastlabs/codefast/pull/965) The documented TypeScript floor is now 7, up from 5.9: TypeScript 7 is the one compiler that type-checks the package and
358
+ emits its published declarations, so it is the floor they support. `README.md`, `SPEC.md` and `DECISIONS.md` state it;
359
+ no code or declaration changed.
360
+
361
+ - [#940](https://github.com/codefastlabs/codefast/pull/940) `unbind(id)` and `unbindAsync(id)` now tear down a binding that a later last-wins bind displaced: its cached singleton
362
+ is deactivated, and a displaced constant runs its `onDeactivation`, at that call instead of waiting for `dispose()`. The
363
+ id lookup asked only the registry, which a displaced binding had already left, so the call did nothing. The unbind also
364
+ invalidates what the displacing chain holds parked, so a later `when*()` refinement of that chain can no longer restore
365
+ the unbound binding.
366
+
367
+ - [#939](https://github.com/codefastlabs/codefast/pull/939) Unloading a module now tears down a binding it registered that a later last-wins bind displaced: a displaced cached
368
+ singleton is deactivated, and a displaced constant runs its `onDeactivation`, at that `unload()`/`unloadAsync()` instead
369
+ of waiting for `dispose()`. Unload looked each of the module's bindings up in the registry, which a displaced binding
370
+ had already left, so it was skipped. The unload also invalidates what the displacing chain holds parked, so a later
371
+ `when*()` refinement of that chain can no longer restore a binding of the unloaded module.
372
+
373
+ - [#923](https://github.com/codefastlabs/codefast/pull/923) `validate()` now reports a container-level `onDeactivation` hook that can never run. The builder type blocks
374
+ `onDeactivation` on `scoped`/`transient` bindings, but `container.onDeactivation(token, handler)` takes any token with
375
+ no such gate, so a hook keyed to a token whose every binding is scoped or transient used to pass validation silently.
376
+ `validate()` now raises `UnreachableLifecycleHookError` for it, reusing the mechanism that already catches a hook on an
377
+ unbound token. A token that also has a singleton or constant binding, and any `onActivation` hook, stay valid.
378
+
3
379
  ## 0.10.1
4
380
 
5
381
  ### Patch Changes
package/README.md CHANGED
@@ -35,9 +35,14 @@ its keep as the dependency graph grows.
35
35
  pnpm add @codefast/di
36
36
  ```
37
37
 
38
- `@codefast/di` requires Node.js 22.12 or later and TypeScript 5.9 or later, with native Stage 3 decorators. Leave
39
- `experimentalDecorators` off — it's off by default. The package is published on 0.x and versioned on its own track:
40
- breaking changes ship as minor versions, so pin the minor version when you need stability.
38
+ `@codefast/di` runs on Node.js 24 or later and in Chrome and Edge 136, Firefox 136, or Safari 18.4 or later
39
+ ([support policy](../../SUPPORT.md#browsers)). It requires TypeScript 7 or later, with native Stage 3 decorators. Leave
40
+ `experimentalDecorators` off — it's off by default. Its declarations use explicit resource management, which no numbered
41
+ `lib` declares yet, so your program needs those types: `@types/node` 24 or later loads them, and any other program adds
42
+ `ESNext.Disposable` to `lib` once its runtime ships explicit resource management. A browser program that targets Safari,
43
+ which has not shipped it, keeps `skipLibCheck` on and calls `dispose()` instead of `await using`. The package is
44
+ published on 0.x and versioned on its own track: breaking changes ship as minor versions, so pin the minor version when
45
+ you need stability.
41
46
 
42
47
  ## Quick start
43
48
 
@@ -164,8 +169,8 @@ constraints is its slot — think of a slot as a labelled variant of the token.
164
169
  `{ name }`:
165
170
 
166
171
  ```ts
167
- container.bind(LoggerToken).toConstantValue(consoleLogger).whenNamed("console");
168
- container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
172
+ container.bind(LoggerToken).whenNamed("console").toConstantValue(consoleLogger);
173
+ container.bind(LoggerToken).whenNamed("file").toConstantValue(fileLogger);
169
174
 
170
175
  container.resolve(LoggerToken, { name: "file" }); // → fileLogger
171
176
  ```
@@ -175,7 +180,7 @@ Declare the names on the token and they become checked, completable literals at
175
180
  ```ts
176
181
  const LoggerToken = token<Logger, "console" | "file">("app:Logger");
177
182
 
178
- container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
183
+ container.bind(LoggerToken).whenNamed("file").toConstantValue(fileLogger);
179
184
  container.resolve(LoggerToken, { name: "file" }); // { name: "fiel" } is a compile error
180
185
  ```
181
186
 
@@ -188,7 +193,7 @@ import { tag } from "@codefast/di";
188
193
 
189
194
  const Provider = tag<"s3" | "gcs">("app:provider");
190
195
 
191
- container.bind(StorageToken).to(S3Storage).whenTagged(Provider.of("s3"));
196
+ container.bind(StorageToken).whenTagged(Provider.of("s3")).to(S3Storage);
192
197
  container.resolve(StorageToken, { tag: Provider.of("s3") }); // → S3Storage
193
198
  ```
194
199
 
@@ -225,7 +230,7 @@ predicates ship from the root entry:
225
230
  | `whenAnyAncestorTaggedAll(criteria)` | some ancestor's slot carries all criteria in the array |
226
231
 
227
232
  For the exact matching and most-specific-wins rules, see
228
- [`SPEC.md` → Slots and last-wins](./SPEC.md#slots-and-last-wins--the-exact-definition).
233
+ [the specification → Slots and last-wins](./spec/binding.md#slots-and-last-wins--the-exact-definition).
229
234
 
230
235
  ## Decorators
231
236
 
@@ -282,7 +287,8 @@ const byHand = runWithContainer(container, () => new Controller());
282
287
 
283
288
  An accessor resolves from the container that's constructing the instance. When something else owns the `new` — a router,
284
289
  an ORM, a test helper — open that context with `runWithContainer`. Without one, the accessor throws
285
- `MissingContainerContextError`. `getActiveContainer()` reads the open context.
290
+ `MissingContainerContextError`. `getActiveContainer()` reads the open context. `runWithContainer` is **synchronous
291
+ only**: the context does not survive an `await`, so a callback returning a `Promise` resolves to `never`.
286
292
 
287
293
  ### Auto-registration
288
294
 
@@ -361,6 +367,18 @@ exits. Synchronous `using` isn't supported — `onDeactivation` may be async —
361
367
  token's bindings; and `container.generateDependencyGraph()` returns a JSON graph. Adapters render that graph for common
362
368
  viewers: `toDotGraph`, `toMermaidGraph`, `toCytoscapeGraph`, and `toReactFlowGraph`.
363
369
 
370
+ `container.explain(token, options)` answers why a request selects the binding it does, without instantiating anything:
371
+ every candidate each registry offered and what selection made of it, the rule that decided, and the binding it ends on.
372
+ It evaluates `when()` predicates the way `resolve` does, and `options.ancestors` explains a request made from inside
373
+ other resolutions, so a predicate that reads the parent sees the one it would:
374
+
375
+ ```ts
376
+ const explanation = container.explain(Logger, { ancestors: [Settlement] });
377
+
378
+ explanation.selected?.id; // the binding `resolve` would answer with
379
+ explanation.steps[0]?.rule; // "sole-predicate": the only candidate with a `when()` beat the rest
380
+ ```
381
+
364
382
  ## Modules
365
383
 
366
384
  A module is a reusable, stateless bundle of related bindings. Group them once, then load them into any container.
@@ -384,6 +402,62 @@ const container = Container.fromModules(AppModule);
384
402
  `Container.fromModulesAsync` or `container.loadAsync`. Modules are ref-counted: loading one twice counts once, and
385
403
  `unload` removes its bindings only when the count reaches zero.
386
404
 
405
+ A module with no logic of its own can be written as a list instead. Each `binding()` states one binding with the fluent
406
+ chain's own vocabulary, is typed exactly as the chain is, and is checked where it is written:
407
+
408
+ ```ts
409
+ import { binding, Module } from "@codefast/di";
410
+
411
+ const InfrastructureModule = Module.fromBindings("app:Infra", [
412
+ binding(LoggerToken, { toConstantValue: console }),
413
+ binding(UserRepository, { toSelf: true, scope: "singleton" }),
414
+ binding(CacheToken, { toDynamic: () => new Map(), whenNamed: "session", scope: "scoped" }),
415
+ ]);
416
+ ```
417
+
418
+ It loads exactly as the equivalent `Module.create` would, and never slower: each binding registers in its final shape,
419
+ so one with a slot, a scope or a hook skips the chain steps a fluent setup pays on every load. A declared module mixes
420
+ into a fluent one with `api.import(...)`.
421
+
422
+ ## Testing
423
+
424
+ Give every test its own container and dispose it afterwards. A container shared across tests carries cached singletons
425
+ from one into the next.
426
+
427
+ ```ts
428
+ let container: Container;
429
+
430
+ beforeEach(() => {
431
+ container = Container.create();
432
+ });
433
+
434
+ afterEach(async () => {
435
+ await container.dispose();
436
+ });
437
+ ```
438
+
439
+ To swap one binding out of a module you already loaded, bind it at a **child** container — the child has no binding of
440
+ its own yet, and resolution prefers the child over its parent:
441
+
442
+ ```ts
443
+ const testContainer = Container.fromModules(AppModule).createChild();
444
+ testContainer.bind(DbToken).toConstantValue(fakeDatabase);
445
+ ```
446
+
447
+ `rebind()` is for replacing a binding that already exists in the **same** container — it throws
448
+ `RebindUnboundTokenError` when the token is only bound at a parent, so it is the wrong tool for the override above.
449
+
450
+ To construct a class the container was never told about — plain JavaScript, generated code, a class you cannot decorate
451
+ — supply the metadata yourself. Pass the reader to `Container.create`: a reader bound through `MetadataReaderToken` on
452
+ the container you are about to use is read too late, because the resolver was built with the container.
453
+
454
+ ```ts
455
+ const container = Container.create({ metadataReader: customReader });
456
+ ```
457
+
458
+ For unit tests where every collaborator should be a mock, [`@codefast/di-testing`](../di-testing/README.md) builds the
459
+ mocks from the class's own declared dependencies, through a real container.
460
+
387
461
  ## Errors
388
462
 
389
463
  Every error extends `DiError` and carries a stable `code`, so you can branch on the code rather than the message:
@@ -402,18 +476,21 @@ Every error extends `DiError` and carries a stable `code`, so you can branch on
402
476
  | `DisposedContainerError` | `DISPOSED_CONTAINER` | A disposed container is used |
403
477
 
404
478
  The full taxonomy — including `MissingMetadataError`, `InvalidMetadataError`, `RebindUnboundTokenError`,
405
- `AsyncModuleLoadError`, and the rest — is exported from the root entry and from `@codefast/di/errors/errors`.
479
+ `AsyncModuleLoadError`, `InvalidBindingDeclarationError`, and the rest — is exported from the root entry and from
480
+ `@codefast/di/errors`.
406
481
 
407
482
  ## Subpath exports
408
483
 
409
484
  Prefer the root entry: it re-exports the whole public API. Reach for a subpath only to trim a bundle, or to pull in
410
485
  something the root doesn't surface.
411
486
 
412
- Every module is also published as a subpath that mirrors the source layout: the model under `@codefast/di/core/*`
413
- (`core/token`, `core/tag`, `core/module`, …), errors under `errors/*`, the runtime under `container/*`, `injection/*`,
414
- `lifecycle/*`, and `resolution/*` (for example `@codefast/di/resolution/select/constraints`), and decorators and
415
- metadata under `decorators/*` and `metadata/*`. Introspection ships at flat specifiers: `@codefast/di/inspector`,
416
- `@codefast/di/dependency-graph`, and `@codefast/di/graph-adapters/{dot,mermaid,cytoscape,reactflow}`.
487
+ Every module is also published as a subpath that is its source path, with no exception: the model under
488
+ `@codefast/di/core/*` (`core/token`, `core/tag`, `core/module`, …), the error taxonomy at `@codefast/di/errors`, the
489
+ runtime under `container/*`, `injection/*`, `lifecycle/*`, and `resolution/*` (for example
490
+ `@codefast/di/resolution/select/constraints`), decorators and metadata under `decorators/*` and `metadata/*`, and
491
+ introspection under `introspection/*`: `@codefast/di/introspection/inspector`, `@codefast/di/introspection/explanation`,
492
+ `@codefast/di/introspection/dependency-graph`, and
493
+ `@codefast/di/introspection/graph-adapters/{dot,mermaid,cytoscape,reactflow}`.
417
494
 
418
495
  ## Benchmarks
419
496
 
@@ -424,9 +501,9 @@ alongside the method that produced them. Run it yourself rather than taking any
424
501
  ## Documentation
425
502
 
426
503
  - [Rendered docs on codefastlabs.com](https://codefastlabs.com/docs/di)
427
- - [`SPEC.md`](./SPEC.md) — the behavioural contract: public API, semantics, and errors.
504
+ - [`spec/`](./spec/README.md) — the behavioural contract: public API, semantics, and errors, one document per API area.
428
505
  - [`ARCHITECTURE.md`](./ARCHITECTURE.md) — the internal shape and the invariants the hot paths depend on.
429
- - [`LEARNING.md`](./LEARNING.md) — a guided tour of the techniques the engine applies, pointing at the code.
506
+ - [`DECISIONS.md`](./DECISIONS.md) — why the API looks like this, and what it did not take from InversifyJS v8.
430
507
  - [`examples/`](./examples/README.md) — runnable examples from basic tokens to a multi-file Ports & Adapters app.
431
508
  - [`CONTRIBUTING.md`](./CONTRIBUTING.md) — the package workflow: build, exports mirror, tests, and the perf guard.
432
509
  - [`CHANGELOG.md`](./CHANGELOG.md) — release history.
@@ -16,11 +16,16 @@ export interface AmbientResolution {
16
16
  resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
17
17
  }
18
18
  /**
19
- * Runs a callback with the given container installed as the ambient one and returns its result.
19
+ * Runs a synchronous callback with the given container installed as the ambient one and returns its result.
20
+ *
21
+ * @remarks The context lives in a module-level variable restored in a `finally`, so it lasts only the
22
+ * synchronous run of `fn` — it does not survive an `await`. A callback returning a `Promise` is a
23
+ * mistake the return type flags: it resolves to `never`, and the ambient context would already be torn
24
+ * down by the time the promise settled.
20
25
  *
21
26
  * @since 0.3.16-canary.0
22
27
  */
23
- export declare function runWithContainer<Result>(container: Container, fn: () => Result): Result;
28
+ export declare function runWithContainer<Result>(container: Container, fn: () => Result): Result extends Promise<unknown> ? never : Result;
24
29
  /**
25
30
  * Engine-internal variant of {@link runWithContainer} that also installs a path-continuing resolver.
26
31
  *
@@ -1,7 +1,12 @@
1
1
  let activeContainer;
2
2
  let activeResolution;
3
3
  /**
4
- * Runs a callback with the given container installed as the ambient one and returns its result.
4
+ * Runs a synchronous callback with the given container installed as the ambient one and returns its result.
5
+ *
6
+ * @remarks The context lives in a module-level variable restored in a `finally`, so it lasts only the
7
+ * synchronous run of `fn` — it does not survive an `await`. A callback returning a `Promise` is a
8
+ * mistake the return type flags: it resolves to `never`, and the ambient context would already be torn
9
+ * down by the time the promise settled.
5
10
  *
6
11
  * @since 0.3.16-canary.0
7
12
  */