@codefast/di 0.5.0-canary.8 → 0.5.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 (91) hide show
  1. package/CHANGELOG.md +387 -0
  2. package/README.md +3 -1
  3. package/dist/binding.d.ts +41 -22
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +11 -0
  6. package/dist/binding.js.map +1 -1
  7. package/dist/container/binding-builders.d.ts.map +1 -1
  8. package/dist/container/binding-builders.js +8 -5
  9. package/dist/container/binding-builders.js.map +1 -1
  10. package/dist/container/container.d.ts.map +1 -1
  11. package/dist/container/container.js +67 -110
  12. package/dist/container/container.js.map +1 -1
  13. package/dist/errors.d.ts +10 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +13 -0
  16. package/dist/errors.js.map +1 -1
  17. package/dist/index.d.ts +1 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/introspection/inspector.d.ts +0 -1
  22. package/dist/introspection/inspector.d.ts.map +1 -1
  23. package/dist/introspection/inspector.js +2 -7
  24. package/dist/introspection/inspector.js.map +1 -1
  25. package/dist/registry.d.ts.map +1 -1
  26. package/dist/registry.js +46 -33
  27. package/dist/registry.js.map +1 -1
  28. package/dist/resolution/activation-need.d.ts +4 -2
  29. package/dist/resolution/activation-need.d.ts.map +1 -1
  30. package/dist/resolution/activation-need.js +15 -11
  31. package/dist/resolution/activation-need.js.map +1 -1
  32. package/dist/resolution/binding-lookup-cache.d.ts +1 -1
  33. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -1
  34. package/dist/resolution/binding-lookup-cache.js +18 -2
  35. package/dist/resolution/binding-lookup-cache.js.map +1 -1
  36. package/dist/resolution/binding-scope.d.ts +5 -2
  37. package/dist/resolution/binding-scope.d.ts.map +1 -1
  38. package/dist/resolution/binding-scope.js +6 -17
  39. package/dist/resolution/binding-scope.js.map +1 -1
  40. package/dist/resolution/binding-select.d.ts +8 -1
  41. package/dist/resolution/binding-select.d.ts.map +1 -1
  42. package/dist/resolution/binding-select.js +13 -32
  43. package/dist/resolution/binding-select.js.map +1 -1
  44. package/dist/resolution/diagnostics.d.ts +2 -2
  45. package/dist/resolution/diagnostics.d.ts.map +1 -1
  46. package/dist/resolution/environment.d.ts +47 -21
  47. package/dist/resolution/environment.d.ts.map +1 -1
  48. package/dist/resolution/environment.js +134 -31
  49. package/dist/resolution/environment.js.map +1 -1
  50. package/dist/resolution/instantiation-plan.d.ts.map +1 -1
  51. package/dist/resolution/instantiation-plan.js +1 -1
  52. package/dist/resolution/instantiation-plan.js.map +1 -1
  53. package/dist/resolution/lifecycle.d.ts.map +1 -1
  54. package/dist/resolution/lifecycle.js +46 -53
  55. package/dist/resolution/lifecycle.js.map +1 -1
  56. package/dist/resolution/resolution-path.d.ts +82 -6
  57. package/dist/resolution/resolution-path.d.ts.map +1 -1
  58. package/dist/resolution/resolution-path.js +66 -8
  59. package/dist/resolution/resolution-path.js.map +1 -1
  60. package/dist/resolution/resolve-options.d.ts +41 -4
  61. package/dist/resolution/resolve-options.d.ts.map +1 -1
  62. package/dist/resolution/resolve-options.js +25 -1
  63. package/dist/resolution/resolve-options.js.map +1 -1
  64. package/dist/resolution/resolver.d.ts +29 -11
  65. package/dist/resolution/resolver.d.ts.map +1 -1
  66. package/dist/resolution/resolver.js +417 -564
  67. package/dist/resolution/resolver.js.map +1 -1
  68. package/dist/resolution/scope.d.ts +4 -0
  69. package/dist/resolution/scope.d.ts.map +1 -1
  70. package/dist/resolution/scope.js +8 -0
  71. package/dist/resolution/scope.js.map +1 -1
  72. package/package.json +5 -11
  73. package/src/binding.ts +46 -23
  74. package/src/container/binding-builders.ts +8 -5
  75. package/src/container/container.ts +72 -119
  76. package/src/errors.ts +17 -0
  77. package/src/index.ts +3 -1
  78. package/src/introspection/inspector.ts +2 -8
  79. package/src/registry.ts +52 -35
  80. package/src/resolution/activation-need.ts +18 -14
  81. package/src/resolution/binding-lookup-cache.ts +18 -2
  82. package/src/resolution/binding-scope.ts +6 -17
  83. package/src/resolution/binding-select.ts +14 -35
  84. package/src/resolution/diagnostics.ts +2 -2
  85. package/src/resolution/environment.ts +181 -35
  86. package/src/resolution/instantiation-plan.ts +5 -5
  87. package/src/resolution/lifecycle.ts +55 -53
  88. package/src/resolution/resolution-path.ts +119 -25
  89. package/src/resolution/resolve-options.ts +51 -4
  90. package/src/resolution/resolver.ts +582 -785
  91. package/src/resolution/scope.ts +10 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,392 @@
1
1
  # @codefast/di
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Detect async cycles from the synchronous factory cascade instead of a settle-scoped path, and escape to a per-branch path only where the cascade cannot see.
8
+
9
+ A factory asks for its dependencies from its **synchronous prefix** — `async ctx => await ctx.resolveAsync(dep)` calls `resolveAsync` before it awaits anything — so an eight-level chain is built inside one synchronous cascade before any of it settles, and the chain of who-is-resolving-whom at the moment of a request is the call stack itself. While that cascade is open the resolver's own arrays are the ancestor chain, pushed on factory-enter and popped when the factory returns its promise rather than when that promise settles. Two cascades cannot interleave, so `binding.inFlight` is exact path membership for async too, exactly as it already was for sync. Every level shares one context; nothing is allocated per level and no level observes its own settlement.
10
+
11
+ This fixes a false `CircularDependencyError`. A diamond — `A` awaiting `B` and `C` in parallel, both needing `D` — rejected with `Circular dependency detected: a → b → d → c → d`, a path in which `b → d → c` is not a dependency edge at all. `D`'s flag is now clear by the time the second sibling asks for it.
12
+
13
+ A request made from a continuation, after an await, has its ancestors on no call stack. It arrives with the cascade empty — an exact test, since a continuation never runs inside one — and escapes to a branch lane whose path is append-only: a level appends while its branch still owns the next slot and copies its own prefix once a sibling has claimed it. Anything the cascade lane does not serve escapes the same way, seeded with a snapshot of the ancestors reached so far, and a subtree that has left the cascade stays off it. A cycle formed entirely from post-await edges is still reported, one level in from the true root, because the ancestors before the first escape were never written down; `resolver-async.test.ts` pins that message.
14
+
15
+ Measured with `BENCH_ISOLATE=1 BENCH_FULL=1`, libraries interleaved with rotating order, 3 trials: against inversify 8.2.3 the suite goes from **42 / 0 / 1** to **43 / 0 / 0** — the async chain row this library had always lost now reads **1.60×** where it read 0.75×, and the async group's geomean goes **1.13× to 1.58×**. Per-level overhead against a floor of eight plain awaited async functions falls from 48.3 ns to **19.5 ns** with the collector idle and **21.3 ns** with a full GC forced every 100 samples — the lane is now within ~7 ns of a build carrying no cycle bookkeeping at all, and it is GC-insensitive again.
16
+
17
+ A paired A/B of the two builds, five passes alternating which side ran first, holds all seventeen measured sync rows at parity (0.99–1.08× medians, no row negative across every pass), including `circular-dependency-3`, which shares the `binding.inFlight` flag the cascade now uses. That A/B is also what caught a regression the suite reported as a win: a materialized async singleton did not match the cascade lane and escaped, snapshotting both cascade arrays on every resolve, for **0.81×** of the previous build across all five passes. The cascade entry now answers a plain constant and a cached singleton itself.
18
+
19
+ `ARCHITECTURE.md` records the two shapes tried before this one, including the one that fixed the same bug and measured worse, and why the sync lane's compiled-plan answer does not port to async.
20
+
21
+ `ResolutionDiagnostics` no longer carries `asyncContextPoolSize`, since there is no async context pool to report.
22
+
23
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`0093b99`](https://github.com/codefastlabs/codefast/commit/0093b99ed711ad037b0e98e7343dee89786d328b) Thanks [@thevuong](https://github.com/thevuong)! - Build a container's rarely-used collaborators on first use instead of in its constructor: the inspector, the module ref/binding tables, the scope's in-flight and scoped caches, the registry's named and tagged slot indexes, and the class introspector's metadata caches. A container that only binds and resolves — the common case, and every per-request child container — no longer allocates eleven `Map`s it never reads.
24
+
25
+ A fresh `Container.create()` retains 2.7 KB instead of 4.8 KB (**43% lighter**), and `parent.createChild()` the same, measured by retention against a forced collection. Every deferred collaborator behaves identically whether or not something touched it first, which `tests/unit/container/deferred-subsystems.test.ts` pins by exercising each one as the first thing a fresh container does.
26
+
27
+ It is a throughput win too, on the paths that actually build containers: `Container.create()` is **1.80×** faster (230 ns → 127 ns) and a per-request child container plus a resolve through it — `createChild()` + resolve, the shape a web app runs once per request — is **1.31×** faster. Measured by an interleaved A/B with both builds loaded into one process, 13 trials in alternating order, against a control scenario that resolves from a pre-built container and so cannot benefit; the control sat at 0.997–1.009, and median and best-of agreed on both figures.
28
+
29
+ **Breaking:** `ScopeManager.getAllScoped()` is removed from the `./resolution/scope` subpath. Deferring the scoped cache raised the question of what a bulk reader returns when the cache was never allocated, and this reader had no callers anywhere in the package — so it is gone rather than carrying an empty-map fallback for nobody. `getAllSingletons()` is unaffected; its cache is still eager.
30
+
31
+ What it does **not** do is close the `realistic-graph-cold-resolve` loss against tsyringe, and the arithmetic says why: 103 ns off container construction is 2.5% of that row's 4.06 µs iteration, so the row moves ~1.5% — measured, and inside the noise floor. That row's gap is GC-attributable (the two libraries are at mutator parity; di only loses once a forced collection is in the loop), so a 13.7% allocation cut was never going to carry it.
32
+
33
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - **Breaking:** `effectiveBindingScope` is no longer exported from the package root. It read a `Binding`,
34
+ which `package.json#exports` deliberately withholds, and no public API ever handed one out — so it was
35
+ exported and impossible to call. Read a binding's scope from `BindingSnapshot.scope`
36
+ (`container.lookupBindings()` / `container.inspect()`) or from `GraphNode.scope`
37
+ (`container.generateDependencyGraph()`), both of which have always carried it.
38
+
39
+ `bindingSlotToResolveOptions` now takes its slot structurally, so the slot on a public
40
+ `BindingSnapshot` — where `name` is an optional property rather than a required one holding
41
+ `undefined` — is accepted. Passing a `BindingSlot`-shaped literal keeps working.
42
+
43
+ A type test now asserts each exported function is callable with values a consumer can actually obtain
44
+ from the package's own exports, which is what neither of these satisfied.
45
+
46
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`de80bad`](https://github.com/codefastlabs/codefast/commit/de80bad63f14afda1bd64a6d247852b24aac8e16) Thanks [@thevuong](https://github.com/thevuong)! - Publish an intentional export surface: 13 subpaths instead of 36. The engine's collaborators — `resolution/*`, `registry`, `container/*`, `binding`, `constructor-type`, and the `metadata` internals — are no longer entry points. They carry the invariants documented in ARCHITECTURE.md, and publishing them meant every internal refactor was technically a breaking change. Everything a consumer needs stays reachable from the root export, which already re-exports the builder interfaces, `Constructor`, `MetadataReader`, `effectiveBindingScope`, and the resolve-options helpers.
47
+
48
+ **This also repairs a silent break.** The surface was generated from `dist/`, so reorganising `src/` into `container/`, `resolution/` and `introspection/` renamed twelve already-published entry points — `./inspector` → `./introspection/inspector`, `./dependency-graph` → `./introspection/dependency-graph`, `./graph-adapters/*` → `./introspection/graph-adapters/*`, `./container` → `./container/container`, and the whole flat `./resolver`/`./scope`/`./lifecycle`/`./environment`/`./constraints`/`./binding-select`/`./binding-scope`/`./resolve-options` set — with no changeset saying so. The consumer-facing ones (`./inspector`, `./dependency-graph`, `./graph-adapters/*`) are back at the specifiers they shipped under; `examples/tanstack-start` imports two of them and would have broken on its next upgrade.
49
+
50
+ **Breaking:** the internal subpaths listed above are gone. Import from the package root instead.
51
+
52
+ - [`4f7a188`](https://github.com/codefastlabs/codefast/commit/4f7a188a5f4a281882606f11ed660aecb9844753) Thanks [@thevuong](https://github.com/thevuong)! - Rename the `hint` resolve parameter to `options` throughout — "hint" implied optional guidance the container may ignore, but the value is a hard selection criterion (`resolve` throws `NoMatchingBindingError` when nothing matches), so the name misstated its role. Positional call sites are unaffected; the one breaking surface is `NoMatchingBindingError.hint`, now `NoMatchingBindingError.options`.
53
+
54
+ - [`ad11507`](https://github.com/codefastlabs/codefast/commit/ad115077e23eaed845abd1f093f32d57f2445a36) Thanks [@thevuong](https://github.com/thevuong)! - Reorganize the source tree into subsystem folders — `container/` (container + the extracted fluent binding builders), `resolution/` (resolver, scope, lifecycle, environment, selection/constraints, and the extracted cycle-guard module), and `introspection/` (inspector, dependency graph, and the graph adapters). The root entry keeps exporting everything and now also exports the graph adapters (`toDotGraph`, `toCytoscapeGraph`, `toReactFlowGraph` and their types), so `import { toReactFlowGraph } from "@codefast/di"` is the preferred path.
55
+
56
+ Breaking (0.x minor): the `@codefast/di/graph-adapters/*` subpaths are removed — import the adapters from the root entry or from `@codefast/di/introspection/graph-adapters/*`. Deep subpaths of other moved modules follow the new folders (e.g. `@codefast/di/resolver` → `@codefast/di/resolution/resolver`).
57
+
58
+ - [`6a25788`](https://github.com/codefastlabs/codefast/commit/6a25788320c73074c3ae0bb06cf7a70b7800c953) Thanks [@thevuong](https://github.com/thevuong)! - Resolver performance overhaul — the head-to-head benchmark vs InversifyJS 8 now shows 38/38 comparable scenarios won (median 1.82×, isolated mode), up from 7 losing rows. Four techniques, no public-API changes:
59
+
60
+ - **Chain-versioned lookup memo** — `BindingRegistry` gains a monotonic mutation version; resolvers memoize options-less `token → {binding, owner}` lookups across the parent chain with alias hops folded to the terminal binding. Resolving a root binding from a depth-2 child (or through `toAlias`) is now as fast as resolving it locally.
61
+ - **Compiled resolution plans** — a transient class binding whose dependency subgraph is pure static (class/constant/cached-singleton deps, no activation hooks or `postConstruct`) compiles once into a nested-constructor closure, cycle-checked at compile time. Anything dynamic keeps the runtime cycle guard, so error semantics are unchanged.
62
+ - **Uniform binding hidden class** — `Registry.add` rebuilds every binding with one fixed field superset so mixed binding kinds no longer turn the resolver's hot property reads megamorphic (~30% throughput loss in processes exercising several kinds).
63
+ - **Leaner async transient path** — cleanup runs as a FIFO side listener on the factory promise instead of a derived-promise chain (one less promise and one less microtask hop per level), and activated transient dynamic bindings get a dedicated lane that fetches container hooks once. Behavior note: an unawaited _failing_ `resolveAsync` no longer surfaces as an `unhandledRejection`; await (or `.catch`) the returned promise.
64
+
65
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`4ba70d1`](https://github.com/codefastlabs/codefast/commit/4ba70d1724e19580ee93ee392e413c23e669f310) Thanks [@thevuong](https://github.com/thevuong)! - Keep a singleton's instance on its binding instead of in a per-container table. A binding belongs to exactly one container, so its singleton slot is per-binding — which turns every cached-singleton read from a keyed `Map` lookup into a field read, on the most common resolve shape there is. The scope manager keeps only a lazily-created list of the bindings that have materialized, so disposal and `inspect()` can still enumerate them, and the singleton `Map` is gone entirely.
66
+
67
+ In the suite, `realistic-graph-resolve-root` — a transient controller over eight cached singletons — went from 10.66M to **12.19M** hz/op, and the `realistic` group geomean from 2.24× to **2.54×** of InversifyJS. That row carries a 2.9% IQR, so it is one of the numbers here worth reading precisely. `singleton-class-1-dep` and cold container build both moved up as well, on rows whose IQR is too wide to attribute confidently.
68
+
69
+ An interleaved A/B against the previous build, both in one process with a control that cannot benefit, put the same row between 1.09× and 1.40× across four runs — never slower, median and best-of agreeing inside each run, but with a spread that depends on what else the process had run. The suite's figure is the one to cite; the A/B established the direction. A trap worth recording: the control first read 0.88×, which was an artifact of timing an 11 ns call one at a time — batched the way the harness does it, the same control reads 1.02× with a 4% spread.
70
+
71
+ **Breaking:** `ScopeManager`'s singleton API takes a `Binding` rather than a `BindingIdentifier`, and `hasSingleton`/`getSingleton`/`peekSingleton`/`setSingleton(id, …)`/`getAllSingletons` are replaced by `setSingleton(binding, …)`, `deleteSingleton(binding)` and `cachedSingletons()`. The `SINGLETON_MISS` sentinel is gone; `NO_INSTANCE` on the binding replaces it. None of this is a published entry point any more, so it is internal — but a fork reaching into `./resolution/scope` would notice. `InstantiationPlanDependencyEntry` also drops its `ownerScope` field, which a compiled thunk no longer needs.
72
+
73
+ - [#643](https://github.com/codefastlabs/codefast/pull/643) [`14c3a98`](https://github.com/codefastlabs/codefast/commit/14c3a98a98ae6221df447a94afe14b2e4a147c90) Thanks [@thevuong](https://github.com/thevuong)! - `container.validate()` now reports a captive dependency when a singleton depends on a **transient or scoped `toDynamic` / `toDynamicAsync` binding**. Previously any dynamic terminal was classified opaque and its declared scope went unchecked, so the most common form of the bug — a singleton capturing one instance of something bound transient — passed validation silently.
74
+
75
+ A factory's _body_ remains opaque: `validate()` still does not descend into it, so whatever the factory resolves internally is not reported. Only the declared scope of the dependency edge is judged, which is the part the container actually knows.
76
+
77
+ **Breaking:** a container that wires a singleton to a transient or scoped dynamic binding now throws `ScopeViolationError` from `validate()` where it previously passed. Either widen the dependency's scope, or inject a factory instead of the value if a fresh instance per use is intended.
78
+
79
+ ### Patch Changes
80
+
81
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Make resolving through a container-level `onActivation` hook as cheap as resolving without one.
82
+
83
+ A transient factory binding that carries activation hooks now takes the same `O(1)` `binding.inFlight`
84
+ cycle guard as the unhooked lane — the argument for that guard never mentioned hooks, since a hook
85
+ runs on the call stack the factory did — and `LifecycleManager` keeps a one-entry token→hooks cache
86
+ in front of its map, because a resolve loop asks about the same token every iteration. Together they
87
+ halve what the hook lane costs over the plain one. A hook that re-resolves its own token still
88
+ reports `CircularDependencyError`, and the flag is still released on every exit path.
89
+
90
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Fix three defects found by an audit of the resolution engine's memoization:
91
+
92
+ - A `.onActivation()` hook added to a chain **after** its binding's first resolve was silently skipped on every lane that consults the activation-need memo (named resolves and nested dependency resolves) while the default-slot dynamic lane honored it. The memo now reads the binding's own hook fresh on every call, so all lanes give one answer.
93
+ - The activation-need memo is keyed by binding id and was only invalidated by the lifecycle version, so a long-running container that rebinds in a loop grew it without bound (~60 B per rebind). The memo is now also stamped with the registry version, evicting entries whose binding ids a rebind has retired.
94
+ - A `scoped` instance cached in a child container survived `unbind`/`unbindAll`/module unload — the drain released singletons only. Scoped entries are now released with their binding (no deactivation, per SPEC §5.2), and resolution diagnostics expose a `scopedInstanceCount` so the release is pinned structurally.
95
+
96
+ A paired A/B against the previous build over six activation- and dispatch-sensitive rows (three passes, alternating order) held every row within noise of parity.
97
+
98
+ - [`2def688`](https://github.com/codefastlabs/codefast/commit/2def688e305eebe7e14af4ae163beec13582aad5) Thanks [@thevuong](https://github.com/thevuong)! - Fix a crash on cyclic aliases: `bind(a).toAlias(b)` + `bind(b).toAlias(a)` previously recursed until `RangeError: Maximum call stack size exceeded` on both `resolve` and `resolveAsync`. Alias following is now an iterative loop with exact revisit detection — a genuine cycle throws `CircularDependencyError` naming the alias chain, and legitimately long alias chains resolve with no arbitrary hop cap.
99
+
100
+ Also splits the magic `32` that served two unrelated roles: the transient-dynamic fast lanes keep their own `DEEP_LANE_THRESHOLD = 32` (a shared-context/pool design point), while the cycle-scan Set attachment moves to a measured `RESOLUTION_SET_THRESHOLD = 128` — benchmarking showed `Array.includes` beats the Set's has/add/delete churn up to at least depth 96, so mid-depth graphs now skip the Set entirely.
101
+
102
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`d27b76f`](https://github.com/codefastlabs/codefast/commit/d27b76fb14200ae5226ec2a05b77d44ab91b016c) Thanks [@thevuong](https://github.com/thevuong)! - Thread an async chain's resolution context through the call and pool it, instead of parking chain identity on the resolver. `ctx.resolveAsync()` now hands the callee the context it used, so an inner level reuses it when the owner matches — which removes the resolver's path-identity heuristic, its shared settle callback and its active-level counter, and makes two concurrent chains correct by construction rather than by a fallback branch.
103
+
104
+ The contexts are pooled, and that is load-bearing rather than an allocation micro-optimization: a per-chain context survives its chain's microtask hops, so under a collecting profile a freshly allocated one is promoted out of the nursery and then collected the expensive way. An ablation that allocated per chain cost **2.5×** on `dynamic-async-chain-8` under a forced GC every 100 samples, which is the reason for the shape.
105
+
106
+ It does **not** close that row. An earlier draft of this changeset claimed it went from 0.98× to 1.18× of InversifyJS; that figure came from a probe running both library builds in one process, which this harness's README warns is worth ~30% on async chains, and from a 3-trial suite run on a loaded machine. At 5 trials on a quiet machine the row is **0.87×** with a 0.6% / 0.3% IQR — among the tightest numbers in the suite. The mechanism above is real; the win over inversify was not.
107
+
108
+ A competing hypothesis was tested and rejected: a forced full GC costs the two libraries the same (1.35 ms / 9.76 MB live for di, 1.41 ms / 9.89 MB for inversify), so the cost was never the collection but what di re-established afterwards.
109
+
110
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Stop allocating a throwaway `Map` on every named resolve. The named-lookup memo upserted with
111
+ `getOrInsert(token, new Map())`, whose fallback JavaScript evaluates eagerly — so every call built a
112
+ `Map` for the hit that immediately discarded it. It now uses `Map.prototype.getOrInsertComputed` with a
113
+ module-level factory, which allocates nothing on a hit and no closure per call: **~1.72×** on
114
+ `named-constant-get`, measured paired against the previous build with the order alternated.
115
+
116
+ The bind-time upserts keep the eager form deliberately — a bind is usually a token's first, so the
117
+ fallback is usually the value stored, and the computed form measured slower there.
118
+
119
+ `@codefast/di` now calls `Map.prototype.getOrInsertComputed` as well as `getOrInsert`; both ship in Node
120
+ 26+, which the package already required.
121
+
122
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Put a one-entry cache in front of `BindingLookupCache`'s options-less token map. Two shapes reach that map and neither can use the registry's direct index: an **alias**, whose terminal binding the index cannot name, and a token owned by a **parent container**, whose entry has to carry the owner. Both are then resolved in a loop over the same token, so the map lookup they repeat deserves an inline cache — the rule this package already applies to `LifecycleManager.activationHandlersFor()`.
123
+
124
+ Paired A/B against the previous build, seven passes alternating which side ran first, medians: `to-alias-redirect` **1.16×** (every pass 1.15–1.18) and `child-depth-2-resolve` **1.23×** (every pass 1.22–1.27), which were the two thinnest wins in the suite outside the lifecycle rows. `rebind-hot-swap` — the row that invalidates the cache on every iteration, so the only place a front cache could be pure overhead — reads 1.17×, after a five-pass run had put it at 0.88× on mixed signs; the tighter run is the one to believe.
125
+
126
+ In the interleaved isolated suite `to-alias-redirect` reads **1.53×** of inversify 8.2.3, up from 1.33×, which is what the paired ratio predicts. `child-depth-2-resolve` reads **1.14×** there against 1.36× before — that row carries both of the report's instability markers (above 30M ops/s, and a per-trial IQR over 5%), its own throughput went _up_, and seven paired passes put it at 1.22–1.27×, so the paired number is the one that describes this change. The suite's aggregate moved from 42/0/1 to 42/1/0 at a slightly lower median on rows this change cannot reach, which is run-to-run drift rather than an effect.
127
+
128
+ `null` is a real answer from that map, meaning "this token's shape needs the full selection path", so absence is tracked by the token slot rather than by the entry, and a registry-version change clears the slot along with the map.
129
+
130
+ Folding alias hops into `registry.getFastDefault()` instead was considered and rejected as unsound: that index is a bare own-registry `Map.get` returning a binding, while an alias's terminal may live in a parent container and its invalidation depends on the whole chain's summed version, neither of which the registry can see.
131
+
132
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Remove a type parameter the resolver could never honour. Fifteen private methods took `Binding<Value>`
133
+ and returned `Value`, but every caller supplied `Value` through an unchecked `as Binding<Value>` — so the
134
+ generic documented an intent the compiler never verified. The internal lanes now take the erased
135
+ `Binding` and return `unknown`, and the eight public resolve entry points each cast once, where the
136
+ caller's token is the claim being made. Seventeen casts fewer in the resolver.
137
+
138
+ What made that possible: the binding kinds declare their lifecycle hooks as methods rather than
139
+ function-typed properties, so their parameters compare bivariantly and `Binding<Value>` stays assignable
140
+ to `Binding`. The public `ActivationHandler` and `DeactivationHandler` are unchanged and still checked
141
+ strictly, so a handler you write is verified exactly as before.
142
+
143
+ No behaviour change: the emitted JavaScript is identical apart from one line break.
144
+
145
+ - [`19199af`](https://github.com/codefastlabs/codefast/commit/19199af174d8971081d1849a36fd9df05c8541ae) Thanks [@thevuong](https://github.com/thevuong)! - Fix binding-registration order sensitivity: the fluent builder chain commits eagerly, so `bind(x).toDynamic(f).when(p)` (or `.whenNamed(...)` / `.whenTagged(...)`) momentarily registered a default-slot binding whose last-wins commit silently displaced an existing default binding of the same token — and the displaced binding was never restored once the chain narrowed to a predicate or a named/tagged slot. Registering a default binding before a constrained one on the same token therefore lost the default. The commit chain now remembers what an intermediate commit displaced and restores it when the chain settles on a non-conflicting shape; a chain that genuinely ends on the same default slot still replaces the previous default (last-wins unchanged).
146
+
147
+ Binding selection also gains a most-specific-wins rule: when both a default binding and exactly one predicate-carrying binding match, the predicate wins (it is a deliberate specialization) instead of throwing `AmbiguousBindingError` — so "default plus `when(...)` override" now works as naturally intended. Two matching predicates remain ambiguous and still throw.
148
+
149
+ - [#643](https://github.com/codefastlabs/codefast/pull/643) [`14c3a98`](https://github.com/codefastlabs/codefast/commit/14c3a98a98ae6221df447a94afe14b2e4a147c90) Thanks [@thevuong](https://github.com/thevuong)! - Unify sync transient-dynamic cycle detection on a per-binding `inFlight` flag, replacing the shallow lane's `O(depth)` `resolutionPath.includes()` scan and the deep lane's `Map` of generation marks. Sync resolution runs on a single call stack, so a binding marked on factory-enter and cleared on factory-exit is exactly path membership — detection becomes an `O(1)` field read with no hashing, no string scan, and no side table to allocate or grow. The async lane keeps its own per-path check, since async chains can interleave.
150
+
151
+ Transient-dynamic chains that previously lost now win across the whole depth range (a 32-deep chain went from ~0.55× to ~1.8× of InversifyJS), deep chains widen their lead, and cold container builds get cheaper because there is no per-resolver cycle-tracking structure to allocate. It also fixes a latent correctness bug: because the deep lane now clears a binding's mark when its factory returns, a deep (past-threshold) transient dependency resolved twice via separate sub-branches (a diamond, not a cycle) no longer throws a false `CircularDependencyError`.
152
+
153
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`864d213`](https://github.com/codefastlabs/codefast/commit/864d213a4253346dae5799ebba06fc2726e933d2) Thanks [@thevuong](https://github.com/thevuong)! - Fold the fluent chain's registry committer into the chain itself. `bind()` now allocates a `BindingEntry` that carries only the `to*()` calls, and `to*()` a `BindingChain` that commits to the registry directly — one object per bind less than the entry/chain/committer trio, and one `Map` lookup less per binding registered by a module.
154
+
155
+ The two classes now share a `BindingRegistration` describing where the chain registers, built once per container rather than once per `bind()`. Threading that instead of a loose `(registry, moduleBindingIds, moduleRef)` triple makes the module invariant type-enforced — the id list is present exactly when the chain belongs to a module load — which removes both non-null assertions from the commit path, and drops the constructors from 4 and 5 positional parameters to 2 and 3.
156
+
157
+ `BindingCommitter` is gone and `BindingEntry`'s constructor now takes `(token, registration)`. Neither is a published entry point any more, so this is internal.
158
+
159
+ This is a simplification, **not** a throughput win: removing only the committer measured no change above noise. The chain stays two objects because `tests/unit/container/bind-to-builder-order.test.ts` requires `bind()`'s result to lack `when*()` at runtime — that is the test's own guarantee, stricter than SPEC §2.4, which only claims compiler enforcement. The measured ceiling for removing every builder object is ~19% on `realistic-graph-cold-resolve` under a forced GC — recorded in ARCHITECTURE so the lead is not re-tried blind.
160
+
161
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Collapse the resolver's duplicated logic onto one rule per question, and fix the two places where a
162
+ second copy had drifted.
163
+
164
+ - `resolveAll(token, { name })` now evaluates a `when()` predicate on a named binding, as `resolve`
165
+ always did. The name index answers the slot; the predicate is a further constraint, and the
166
+ fast lane was returning a candidate `resolve` refuses.
167
+ - Refining a binding's scope after its first resolve (`bind(T).toDynamic(f)` … later `.singleton()`)
168
+ now reports the new scope to `when()` predicates that read `ctx.parent.scope`. The resolution
169
+ frame is memoized on the binding and derives from `scope`, so the refinement has to drop it.
170
+
171
+ Internally: slot matching, name-only requests, and the alias walk each exist once; a class's
172
+ constructor params and a `toResolved` factory's descriptors resolve through one routine per lane;
173
+ `scope` is declared by every binding kind, so the engine reads it as a plain field. No public API
174
+ changed. Resolution throughput is unchanged or better across the benchmark suite — `resolveOptional`
175
+ ~1.5×, transient class and `toResolved` construction ~1.35–1.44×, constants and named lookups
176
+ ~1.3×, with the deep/wide graph rows at parity.
177
+
178
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`d27b76f`](https://github.com/codefastlabs/codefast/commit/d27b76fb14200ae5226ec2a05b77d44ab91b016c) Thanks [@thevuong](https://github.com/thevuong)! - Compile instantiation plans around dependencies the compiler cannot see through, instead of refusing to compile the graph at all. A factory, a scoped binding, an activation hook, a class past the depth limit, or a multi/optional/named parameter now compiles to an _escape_ — a re-entry into the runtime resolver seeded with exactly the ancestors the interpreted path would have pushed at that point, dispatched through exactly the resolve the interpreter would have called. Cycle detection, constraint contexts and error paths are therefore identical to never having compiled, and only the opaque dependency pays the runtime price while its siblings and ancestors stay compiled.
179
+
180
+ Previously a single `toDynamic` dependency anywhere in a class graph dropped the whole graph to the interpreted path — a 13.9× cliff on a shape applications write constantly (a factory-provided config injected into a class tree). That graph is now ~2× faster, and the first-materialization path of a singleton dependency inside a plan gained cycle detection it did not have.
181
+
182
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`d27b76f`](https://github.com/codefastlabs/codefast/commit/d27b76fb14200ae5226ec2a05b77d44ab91b016c) Thanks [@thevuong](https://github.com/thevuong)! - Split the resolver's self-contained caches into named collaborators — `BindingLookupCache` (the chain-versioned options-less lookup memo), `ClassIntrospector` (per-class metadata, `@postConstruct` discovery, accessor injection, instantiation) and `ActivationNeedCache` (per-binding activation need, versioned on the lifecycle manager). The engine class keeps the sync and async pipelines, which genuinely need the same private state on every hop, and `ARCHITECTURE.md` now records the layering, the invariants each hot path depends on, and the rule that separates a legitimate threshold (choosing an implementation) from the kind that was removed (choosing a semantics).
183
+
184
+ New subpaths `@codefast/di/resolution/{activation-need,binding-lookup-cache,class-introspector}`; `@codefast/di/resolution/class-plan` is now `@codefast/di/resolution/instantiation-plan`, correcting an export map that had been stale since the module was renamed.
185
+
186
+ - [`f9aeeb0`](https://github.com/codefastlabs/codefast/commit/f9aeeb04a271877e47a7fbbfc6d62ae0fe1ad955) Thanks [@thevuong](https://github.com/thevuong)! - Extend the compiled-plan and memoization coverage: `toResolved(...)` transient bindings with pure-static explicit deps now compile into factory-call plans (same refusal rules and sync-only check as class plans), and name-only resolves gain a chain-versioned memo that fast-paths constants and cached singletons — predicates, aliases, and anything context-dependent keep the full selection path. Measured: `named-constant-get` ~21M → ~30M hz/op, `to-resolved-3-deps` ~39M → ~52M hz/op.
187
+
188
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Stop minting two arrays per top-level sync resolve, and stop a pooled resolution context re-storing pointers it already holds.
189
+
190
+ `--prof` over the four thinnest rows put the largest di-attributed cost in a place none of this package's notes mention: `#acquireSyncResolutionContext` and `DefaultResolutionContext.reset()` together take **22%** of ticks on `fan-out-tree-depth-3-breadth-4` and **16%** on `scale-deep-transient-chain-512`, and `reset()` alone takes **10%** on `container-level-activation-hook`. The reason `reset()` is not free is that a pooled context outlives enough resolves to sit in old space, so each of its five field writes is a pointer store with a write barrier — and three of the five write the same resolver and the same two arrays every time.
191
+
192
+ Except they did not, because `container.resolve()` handed every call a fresh `[]` pair. So both halves are needed together: a resolver now keeps one sync `rootPath`/`rootStack` pair, lent to a top-level resolve when `rootStack.length === 0` and otherwise replaced by a fresh pair, and `reset()` compares before storing. Every sync lane pops what it pushes, so an empty stack is an exact "nobody holds this"; a nested `container.resolve()` from inside a factory still starts from an empty path, and if a resolve ever left the pair dirty the only consequence is that later resolves mint their own.
193
+
194
+ Paired A/B against this commit's parent, six passes alternating which side ran first: `constant-resolve` **1.70×**, `container-level-activation-hook` **1.67×**, `realistic-graph-resolve-root` **1.34×**, `fan-out-tree-depth-3-breadth-4` **1.28×**, `scale-deep-transient-chain-512` **1.21×**, `scale-mid-transient-chain-32` 1.16×, `singleton-class-1-dep` 1.13×, `to-alias-redirect` 1.09×, and `dynamic-async-chain-8` 0.99× as the untouched control.
195
+
196
+ `transient-class-1-dep` reads **0.91×**, negative in all six passes, and the mechanism is the same one that wins the other rows: a fresh array is in new space, so pushing a frame onto it needs no write barrier, while the shared pair is in old space and every push pays one. That row pushes a frame and does nothing else, so it is the one shape where the barrier costs more than the two allocations saved. Kept because it is one row at −9% against five between +21% and +70%.
197
+
198
+ `tests/unit/resolution/in-flight-invariants.test.ts` pins the lending rule in both directions — a nested root resolve gets its own pair, and a throwing resolve hands the pair back — and both were checked by breaking the guard.
199
+
200
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - `toSelf()` on a token that is not a class now throws `SelfBindingRequiresClassError` instead of a bare
201
+ `Error`, so it is catchable as a `DiError` like every other failure this package raises, carries a
202
+ `code` and the token name, and is documented in SPEC.
203
+
204
+ It was the one throw site outside the error taxonomy, and the architecture test could not see it —
205
+ that test only read `export class …Error` declarations. It now also fails on any `throw new Error(…)`
206
+ under `src/`, and on an error class the root barrel forgets to export.
207
+
208
+ - [#643](https://github.com/codefastlabs/codefast/pull/643) [`14c3a98`](https://github.com/codefastlabs/codefast/commit/14c3a98a98ae6221df447a94afe14b2e4a147c90) Thanks [@thevuong](https://github.com/thevuong)! - Collapse the async transient-dynamic resolver into a single lane and retune the cycle-set threshold.
209
+
210
+ The async lane used to split at depth 32 into a fast path (linear path scan, shared context, no stack frames) and a slow path (forced Set, fresh context per level, stack frames, extra microtask hop) — so context identity, `ctx.graph` contents, and promise shape all changed silently at that depth. Both are now one lane whose cycle guard goes through `enterResolutionPath`, the only mechanism that stays correct when chains interleave (`Promise.all`) and which adapts on its own: a linear scan while the path is short, an attached Set past `RESOLUTION_SET_THRESHOLD`. `DEEP_LANE_THRESHOLD` is gone from the package entirely.
211
+
212
+ `RESOLUTION_SET_THRESHOLD` drops from 128 to 32 on fresh measurements — at 128 an async chain costs 1275 / 3641 / 9645 / 26082 ns at depth 16 / 32 / 64 / 128 versus 1202 / 3285 / 7735 / 16837 at 32, so the old value was the worse choice at every depth measured.
213
+
214
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`a720c62`](https://github.com/codefastlabs/codefast/commit/a720c6297d041ffd2d0bba2e6146af894007a367) Thanks [@thevuong](https://github.com/thevuong)! - Collapse the fluent binding chain into one object. A single `BindingChain` is now the `BindToBuilder` that `bind()` returns and the kind-specific builder that `to*()` returns, so a `bind()` allocates one builder instead of two.
215
+
216
+ The `to*()`-before-`when*()` ordering stays enforced, as a type-level guarantee — which is what SPEC §2.4 actually claims. `bind()` is typed `BindToBuilder`, so a refinement before `to*()` does not compile; `tests/types/container-api.test.ts` pins that. For a caller without types, or one who casts past them, every refinement now throws the new **`ChainNotRegisteredError`** naming the token and pointing at `to*()`, rather than silently doing nothing. `whenDefault()` asserts registration for that reason alone, since it otherwise has nothing to do.
217
+
218
+ The previous revision kept two objects because a unit test asserted the refinement methods were _absent from the object_ `bind()` returns — a stricter reading than the spec, and one that pinned an implementation detail. That test now asserts the contract instead: every refinement throws before `to*()`, nothing is registered when it does, and the chain still works normally afterwards.
219
+
220
+ This is an API simplification, **not** a throughput win: going from four builder objects per bind to three measured no change above noise, and a fluent API cannot go below one, so the ~19% ceiling recorded in ARCHITECTURE for removing all of them is unreachable rather than pending.
221
+
222
+ - [#646](https://github.com/codefastlabs/codefast/pull/646) [`1241f82`](https://github.com/codefastlabs/codefast/commit/1241f82bdb40613667c781111f2ce20409ddfd89) Thanks [@thevuong](https://github.com/thevuong)! - Register a fluent binding chain once instead of once per refinement. `bind(T).toDynamic(f).singleton()` used to insert a binding, remove it, and insert a replacement — two registry mutations, two version bumps, and a full index churn per binding. The chain now registers on its `to*()` call and refines that same registered object in place; only `when*()` re-slots, and it re-registers under the chain's original id, so `id()` stays valid for the whole chain instead of the intermediate ids being dead. Binding construction also funnels through a single `createBinding()` literal, which is what guarantees the one V8 hidden class the resolver's hot property reads depend on — so the registry stores what it is handed rather than re-copying it.
223
+
224
+ Cold container build (build, bind 10 nodes, resolve the root) went from the suite's only loss to a win against every competitor: 0.76× → 3.0× of InversifyJS, 0.43× → 1.9× of Awilix, 0.22× → 1.07× of tsyringe.
225
+
226
+ The builder's `CommitFn` type is replaced by a `BindingCommitter` interface (`commit` plus `refine`, the latter for in-place refinements the registry indexes do not care about), and `createBinding` / `refinableFields` are new exports from `@codefast/di/binding`.
227
+
228
+ - [#643](https://github.com/codefastlabs/codefast/pull/643) [`14c3a98`](https://github.com/codefastlabs/codefast/commit/14c3a98a98ae6221df447a94afe14b2e4a147c90) Thanks [@thevuong](https://github.com/thevuong)! - Collapse the sync transient-dynamic resolver into a single lane and memoize each binding's resolution frame on the binding itself.
229
+
230
+ The separate deep lane existed to escape an `O(depth)` `resolutionPath.includes()` cycle scan past ~32 levels. With cycle detection now an `O(1)` `binding.inFlight` mark there is nothing to escape, so the depth split, its shared-context bookkeeping, its reentrancy fallback, and the per-resolver frame `Map` are all gone — the smaller function also inlines better. Frames derive only from immutable binding fields, so caching one per binding replaces a `Map` lookup per hop and a `Map` insert per binding per container.
231
+
232
+ Faster at every chain depth measured (8 → 512), e.g. a 32-deep transient chain improved ~39% and cold container build ~62%, which turns the cold-build result against Awilix from a loss into a win in the default benchmark profile.
233
+
234
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Fix a tag request answering differently depending on how it was spelled. `resolve(T, { tags: [["n", -0]] })`
235
+ matched a binding tagged `["n", 0]` while `resolve(T, { tag: ["n", -0] })` threw `NoMatchingBindingError`
236
+ and `resolveAll` returned `[]` — three answers to one question.
237
+
238
+ The registry indexes tagged bindings in a `Map`, so it answers by SameValueZero, while tag values compare
239
+ by `Object.is` as SPEC §3.5 requires; the two differ on `+0` versus `-0`. The fast path now re-checks the
240
+ index's answer, and only where the index can be wrong — a request whose tag value is not zero was already
241
+ exact. `NaN` was never affected: both rules treat it as equal to itself.
242
+
243
+ - [#643](https://github.com/codefastlabs/codefast/pull/643) [`14c3a98`](https://github.com/codefastlabs/codefast/commit/14c3a98a98ae6221df447a94afe14b2e4a147c90) Thanks [@thevuong](https://github.com/thevuong)! - `toResolved()` and `toResolvedAsync()` now accept injection descriptors — `inject()`, `optional()` and `injectAll()` — in their dependency list, matching what `@injectable([...])` already allowed and what the builder already did at runtime (it normalizes every entry through `normalizeToDescriptor`). Previously the public signature only admitted bare tokens and constructors, so an optional or multi dependency needed a cast even though resolution handled it correctly.
244
+
245
+ Factory arguments are typed from the descriptor: a bare token gives `Value`, `optional(token)` gives `Value | undefined`, and `injectAll(token)` gives `Array<Value>`. Widening only — existing bare-token call sites are unaffected.
246
+
247
+ - [#643](https://github.com/codefastlabs/codefast/pull/643) [`14c3a98`](https://github.com/codefastlabs/codefast/commit/14c3a98a98ae6221df447a94afe14b2e4a147c90) Thanks [@thevuong](https://github.com/thevuong)! - `unbind(token)` now drops the token's bindings in a single registry pass instead of removing them one id at a time. The previous path re-scanned and re-indexed the token's binding list once per binding — quadratic in the number of slots bound to that token — and bumped the registry version once per removal, invalidating resolver lookup caches repeatedly. Behaviour is unchanged, including deactivation of cached singletons.
248
+
249
+ - [#676](https://github.com/codefastlabs/codefast/pull/676) [`641e233`](https://github.com/codefastlabs/codefast/commit/641e2338d77fb61be2ca585a5986f34cf32ec746) Thanks [@thevuong](https://github.com/thevuong)! - Collapse the `types` and `default` lanes of `package.json#imports` from fallback arrays to single strings.
250
+
251
+ Node resolves an imports array by taking the first candidate it can parse, without checking that the file exists and without falling through — a specifier whose first candidate is missing throws `ERR_MODULE_NOT_FOUND` rather than trying the second. `./dist/*/index.js` and `./dist/*/index.d.ts` could therefore never be reached, so they read as a safety net that does not exist. The `source` lane keeps its extension candidates, which only `tsc` and Vite read and both probe.
252
+
253
+ ## 0.5.0-canary.9
254
+
255
+ ### Minor Changes
256
+
257
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Detect async cycles from the synchronous factory cascade instead of a settle-scoped path, and escape to a per-branch path only where the cascade cannot see.
258
+
259
+ A factory asks for its dependencies from its **synchronous prefix** — `async ctx => await ctx.resolveAsync(dep)` calls `resolveAsync` before it awaits anything — so an eight-level chain is built inside one synchronous cascade before any of it settles, and the chain of who-is-resolving-whom at the moment of a request is the call stack itself. While that cascade is open the resolver's own arrays are the ancestor chain, pushed on factory-enter and popped when the factory returns its promise rather than when that promise settles. Two cascades cannot interleave, so `binding.inFlight` is exact path membership for async too, exactly as it already was for sync. Every level shares one context; nothing is allocated per level and no level observes its own settlement.
260
+
261
+ This fixes a false `CircularDependencyError`. A diamond — `A` awaiting `B` and `C` in parallel, both needing `D` — rejected with `Circular dependency detected: a → b → d → c → d`, a path in which `b → d → c` is not a dependency edge at all. `D`'s flag is now clear by the time the second sibling asks for it.
262
+
263
+ A request made from a continuation, after an await, has its ancestors on no call stack. It arrives with the cascade empty — an exact test, since a continuation never runs inside one — and escapes to a branch lane whose path is append-only: a level appends while its branch still owns the next slot and copies its own prefix once a sibling has claimed it. Anything the cascade lane does not serve escapes the same way, seeded with a snapshot of the ancestors reached so far, and a subtree that has left the cascade stays off it. A cycle formed entirely from post-await edges is still reported, one level in from the true root, because the ancestors before the first escape were never written down; `resolver-async.test.ts` pins that message.
264
+
265
+ Measured with `BENCH_ISOLATE=1 BENCH_FULL=1`, libraries interleaved with rotating order, 3 trials: against inversify 8.2.3 the suite goes from **42 / 0 / 1** to **43 / 0 / 0** — the async chain row this library had always lost now reads **1.60×** where it read 0.75×, and the async group's geomean goes **1.13× to 1.58×**. Per-level overhead against a floor of eight plain awaited async functions falls from 48.3 ns to **19.5 ns** with the collector idle and **21.3 ns** with a full GC forced every 100 samples — the lane is now within ~7 ns of a build carrying no cycle bookkeeping at all, and it is GC-insensitive again.
266
+
267
+ A paired A/B of the two builds, five passes alternating which side ran first, holds all seventeen measured sync rows at parity (0.99–1.08× medians, no row negative across every pass), including `circular-dependency-3`, which shares the `binding.inFlight` flag the cascade now uses. That A/B is also what caught a regression the suite reported as a win: a materialized async singleton did not match the cascade lane and escaped, snapshotting both cascade arrays on every resolve, for **0.81×** of the previous build across all five passes. The cascade entry now answers a plain constant and a cached singleton itself.
268
+
269
+ `ARCHITECTURE.md` records the two shapes tried before this one, including the one that fixed the same bug and measured worse, and why the sync lane's compiled-plan answer does not port to async.
270
+
271
+ `ResolutionDiagnostics` no longer carries `asyncContextPoolSize`, since there is no async context pool to report.
272
+
273
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - **Breaking:** `effectiveBindingScope` is no longer exported from the package root. It read a `Binding`,
274
+ which `package.json#exports` deliberately withholds, and no public API ever handed one out — so it was
275
+ exported and impossible to call. Read a binding's scope from `BindingSnapshot.scope`
276
+ (`container.lookupBindings()` / `container.inspect()`) or from `GraphNode.scope`
277
+ (`container.generateDependencyGraph()`), both of which have always carried it.
278
+
279
+ `bindingSlotToResolveOptions` now takes its slot structurally, so the slot on a public
280
+ `BindingSnapshot` — where `name` is an optional property rather than a required one holding
281
+ `undefined` — is accepted. Passing a `BindingSlot`-shaped literal keeps working.
282
+
283
+ A type test now asserts each exported function is callable with values a consumer can actually obtain
284
+ from the package's own exports, which is what neither of these satisfied.
285
+
286
+ ### Patch Changes
287
+
288
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Make resolving through a container-level `onActivation` hook as cheap as resolving without one.
289
+
290
+ A transient factory binding that carries activation hooks now takes the same `O(1)` `binding.inFlight`
291
+ cycle guard as the unhooked lane — the argument for that guard never mentioned hooks, since a hook
292
+ runs on the call stack the factory did — and `LifecycleManager` keeps a one-entry token→hooks cache
293
+ in front of its map, because a resolve loop asks about the same token every iteration. Together they
294
+ halve what the hook lane costs over the plain one. A hook that re-resolves its own token still
295
+ reports `CircularDependencyError`, and the flag is still released on every exit path.
296
+
297
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Fix three defects found by an audit of the resolution engine's memoization:
298
+
299
+ - A `.onActivation()` hook added to a chain **after** its binding's first resolve was silently skipped on every lane that consults the activation-need memo (named resolves and nested dependency resolves) while the default-slot dynamic lane honored it. The memo now reads the binding's own hook fresh on every call, so all lanes give one answer.
300
+ - The activation-need memo is keyed by binding id and was only invalidated by the lifecycle version, so a long-running container that rebinds in a loop grew it without bound (~60 B per rebind). The memo is now also stamped with the registry version, evicting entries whose binding ids a rebind has retired.
301
+ - A `scoped` instance cached in a child container survived `unbind`/`unbindAll`/module unload — the drain released singletons only. Scoped entries are now released with their binding (no deactivation, per SPEC §5.2), and resolution diagnostics expose a `scopedInstanceCount` so the release is pinned structurally.
302
+
303
+ A paired A/B against the previous build over six activation- and dispatch-sensitive rows (three passes, alternating order) held every row within noise of parity.
304
+
305
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Stop allocating a throwaway `Map` on every named resolve. The named-lookup memo upserted with
306
+ `getOrInsert(token, new Map())`, whose fallback JavaScript evaluates eagerly — so every call built a
307
+ `Map` for the hit that immediately discarded it. It now uses `Map.prototype.getOrInsertComputed` with a
308
+ module-level factory, which allocates nothing on a hit and no closure per call: **~1.72×** on
309
+ `named-constant-get`, measured paired against the previous build with the order alternated.
310
+
311
+ The bind-time upserts keep the eager form deliberately — a bind is usually a token's first, so the
312
+ fallback is usually the value stored, and the computed form measured slower there.
313
+
314
+ `@codefast/di` now calls `Map.prototype.getOrInsertComputed` as well as `getOrInsert`; both ship in Node
315
+ 26+, which the package already required.
316
+
317
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Put a one-entry cache in front of `BindingLookupCache`'s options-less token map. Two shapes reach that map and neither can use the registry's direct index: an **alias**, whose terminal binding the index cannot name, and a token owned by a **parent container**, whose entry has to carry the owner. Both are then resolved in a loop over the same token, so the map lookup they repeat deserves an inline cache — the rule this package already applies to `LifecycleManager.activationHandlersFor()`.
318
+
319
+ Paired A/B against the previous build, seven passes alternating which side ran first, medians: `to-alias-redirect` **1.16×** (every pass 1.15–1.18) and `child-depth-2-resolve` **1.23×** (every pass 1.22–1.27), which were the two thinnest wins in the suite outside the lifecycle rows. `rebind-hot-swap` — the row that invalidates the cache on every iteration, so the only place a front cache could be pure overhead — reads 1.17×, after a five-pass run had put it at 0.88× on mixed signs; the tighter run is the one to believe.
320
+
321
+ In the interleaved isolated suite `to-alias-redirect` reads **1.53×** of inversify 8.2.3, up from 1.33×, which is what the paired ratio predicts. `child-depth-2-resolve` reads **1.14×** there against 1.36× before — that row carries both of the report's instability markers (above 30M ops/s, and a per-trial IQR over 5%), its own throughput went _up_, and seven paired passes put it at 1.22–1.27×, so the paired number is the one that describes this change. The suite's aggregate moved from 42/0/1 to 42/1/0 at a slightly lower median on rows this change cannot reach, which is run-to-run drift rather than an effect.
322
+
323
+ `null` is a real answer from that map, meaning "this token's shape needs the full selection path", so absence is tracked by the token slot rather than by the entry, and a registry-version change clears the slot along with the map.
324
+
325
+ Folding alias hops into `registry.getFastDefault()` instead was considered and rejected as unsound: that index is a bare own-registry `Map.get` returning a binding, while an alias's terminal may live in a parent container and its invalidation depends on the whole chain's summed version, neither of which the registry can see.
326
+
327
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Remove a type parameter the resolver could never honour. Fifteen private methods took `Binding<Value>`
328
+ and returned `Value`, but every caller supplied `Value` through an unchecked `as Binding<Value>` — so the
329
+ generic documented an intent the compiler never verified. The internal lanes now take the erased
330
+ `Binding` and return `unknown`, and the eight public resolve entry points each cast once, where the
331
+ caller's token is the claim being made. Seventeen casts fewer in the resolver.
332
+
333
+ What made that possible: the binding kinds declare their lifecycle hooks as methods rather than
334
+ function-typed properties, so their parameters compare bivariantly and `Binding<Value>` stays assignable
335
+ to `Binding`. The public `ActivationHandler` and `DeactivationHandler` are unchanged and still checked
336
+ strictly, so a handler you write is verified exactly as before.
337
+
338
+ No behaviour change: the emitted JavaScript is identical apart from one line break.
339
+
340
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Collapse the resolver's duplicated logic onto one rule per question, and fix the two places where a
341
+ second copy had drifted.
342
+
343
+ - `resolveAll(token, { name })` now evaluates a `when()` predicate on a named binding, as `resolve`
344
+ always did. The name index answers the slot; the predicate is a further constraint, and the
345
+ fast lane was returning a candidate `resolve` refuses.
346
+ - Refining a binding's scope after its first resolve (`bind(T).toDynamic(f)` … later `.singleton()`)
347
+ now reports the new scope to `when()` predicates that read `ctx.parent.scope`. The resolution
348
+ frame is memoized on the binding and derives from `scope`, so the refinement has to drop it.
349
+
350
+ Internally: slot matching, name-only requests, and the alias walk each exist once; a class's
351
+ constructor params and a `toResolved` factory's descriptors resolve through one routine per lane;
352
+ `scope` is declared by every binding kind, so the engine reads it as a plain field. No public API
353
+ changed. Resolution throughput is unchanged or better across the benchmark suite — `resolveOptional`
354
+ ~1.5×, transient class and `toResolved` construction ~1.35–1.44×, constants and named lookups
355
+ ~1.3×, with the deep/wide graph rows at parity.
356
+
357
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Stop minting two arrays per top-level sync resolve, and stop a pooled resolution context re-storing pointers it already holds.
358
+
359
+ `--prof` over the four thinnest rows put the largest di-attributed cost in a place none of this package's notes mention: `#acquireSyncResolutionContext` and `DefaultResolutionContext.reset()` together take **22%** of ticks on `fan-out-tree-depth-3-breadth-4` and **16%** on `scale-deep-transient-chain-512`, and `reset()` alone takes **10%** on `container-level-activation-hook`. The reason `reset()` is not free is that a pooled context outlives enough resolves to sit in old space, so each of its five field writes is a pointer store with a write barrier — and three of the five write the same resolver and the same two arrays every time.
360
+
361
+ Except they did not, because `container.resolve()` handed every call a fresh `[]` pair. So both halves are needed together: a resolver now keeps one sync `rootPath`/`rootStack` pair, lent to a top-level resolve when `rootStack.length === 0` and otherwise replaced by a fresh pair, and `reset()` compares before storing. Every sync lane pops what it pushes, so an empty stack is an exact "nobody holds this"; a nested `container.resolve()` from inside a factory still starts from an empty path, and if a resolve ever left the pair dirty the only consequence is that later resolves mint their own.
362
+
363
+ Paired A/B against this commit's parent, six passes alternating which side ran first: `constant-resolve` **1.70×**, `container-level-activation-hook` **1.67×**, `realistic-graph-resolve-root` **1.34×**, `fan-out-tree-depth-3-breadth-4` **1.28×**, `scale-deep-transient-chain-512` **1.21×**, `scale-mid-transient-chain-32` 1.16×, `singleton-class-1-dep` 1.13×, `to-alias-redirect` 1.09×, and `dynamic-async-chain-8` 0.99× as the untouched control.
364
+
365
+ `transient-class-1-dep` reads **0.91×**, negative in all six passes, and the mechanism is the same one that wins the other rows: a fresh array is in new space, so pushing a frame onto it needs no write barrier, while the shared pair is in old space and every push pays one. That row pushes a frame and does nothing else, so it is the one shape where the barrier costs more than the two allocations saved. Kept because it is one row at −9% against five between +21% and +70%.
366
+
367
+ `tests/unit/resolution/in-flight-invariants.test.ts` pins the lending rule in both directions — a nested root resolve gets its own pair, and a throwing resolve hands the pair back — and both were checked by breaking the guard.
368
+
369
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - `toSelf()` on a token that is not a class now throws `SelfBindingRequiresClassError` instead of a bare
370
+ `Error`, so it is catchable as a `DiError` like every other failure this package raises, carries a
371
+ `code` and the token name, and is documented in SPEC.
372
+
373
+ It was the one throw site outside the error taxonomy, and the architecture test could not see it —
374
+ that test only read `export class …Error` declarations. It now also fails on any `throw new Error(…)`
375
+ under `src/`, and on an error class the root barrel forgets to export.
376
+
377
+ - [#677](https://github.com/codefastlabs/codefast/pull/677) [`7fd9ba8`](https://github.com/codefastlabs/codefast/commit/7fd9ba82426493bee6ffd11a512920103a644842) Thanks [@thevuong](https://github.com/thevuong)! - Fix a tag request answering differently depending on how it was spelled. `resolve(T, { tags: [["n", -0]] })`
378
+ matched a binding tagged `["n", 0]` while `resolve(T, { tag: ["n", -0] })` threw `NoMatchingBindingError`
379
+ and `resolveAll` returned `[]` — three answers to one question.
380
+
381
+ The registry indexes tagged bindings in a `Map`, so it answers by SameValueZero, while tag values compare
382
+ by `Object.is` as SPEC §3.5 requires; the two differ on `+0` versus `-0`. The fast path now re-checks the
383
+ index's answer, and only where the index can be wrong — a request whose tag value is not zero was already
384
+ exact. `NaN` was never affected: both rules treat it as equal to itself.
385
+
386
+ - [#676](https://github.com/codefastlabs/codefast/pull/676) [`641e233`](https://github.com/codefastlabs/codefast/commit/641e2338d77fb61be2ca585a5986f34cf32ec746) Thanks [@thevuong](https://github.com/thevuong)! - Collapse the `types` and `default` lanes of `package.json#imports` from fallback arrays to single strings.
387
+
388
+ Node resolves an imports array by taking the first candidate it can parse, without checking that the file exists and without falling through — a specifier whose first candidate is missing throws `ERR_MODULE_NOT_FOUND` rather than trying the second. `./dist/*/index.js` and `./dist/*/index.d.ts` could therefore never be reached, so they read as a safety net that does not exist. The `source` lane keeps its extension candidates, which only `tsc` and Vite read and both probe.
389
+
3
390
  ## 0.5.0-canary.8
4
391
 
5
392
  ### Minor Changes
package/README.md CHANGED
@@ -16,7 +16,9 @@ Lightweight, type-safe dependency injection for modern TypeScript — built on T
16
16
 
17
17
  ## Requirements
18
18
 
19
- - **Node.js 26 or later — required.** The container uses the native `Map.prototype.getOrInsert` (ES2025), which ships in Node 26+. On older Node versions the package throws at runtime.
19
+ - **Node.js 26 or later — required.** The container uses the native `Map.prototype.getOrInsert` and
20
+ `Map.prototype.getOrInsertComputed` (ES2025), which ship in Node 26+. On older Node versions the
21
+ package throws at runtime.
20
22
  - **TypeScript 5.2+** with native Stage 3 decorators — leave `experimentalDecorators` **off** (it is off by default).
21
23
 
22
24
  ## Installation
package/dist/binding.d.ts CHANGED
@@ -32,12 +32,14 @@ export declare function bindingSlotToString(slot: BindingSlot): string;
32
32
  interface BindingBase<Value> {
33
33
  readonly id: BindingIdentifier;
34
34
  /**
35
- * True while this binding's factory is on the sync resolution stack — the sync lane's cycle check.
35
+ * True while this binding's factory is executing on the current synchronous call stack.
36
36
  *
37
- * @remarks Resolver-owned bookkeeping; callers never set it. See `ARCHITECTURE.md` for why the
38
- * sync and async lanes detect cycles differently.
37
+ * @remarks Both cycle guards that can use an `O(1)` flag read this — the sync transient-dynamic
38
+ * lane and the async cascade lane — because synchronous code does not interleave, so the flag *is*
39
+ * exact path membership. Not optional: `createBinding` always sets it, and a field that may be
40
+ * absent is a field that can cost the shared hidden class. Resolver-owned; callers never set it.
39
41
  */
40
- inFlight?: boolean | undefined;
42
+ inFlight: boolean;
41
43
  /**
42
44
  * Memoized resolution frame for this binding. Its contents derive only from immutable binding
43
45
  * fields, so it is computed once on first resolve and reused instead of a per-resolver Map
@@ -58,67 +60,68 @@ interface BindingBase<Value> {
58
60
  readonly predicate?: ((ctx: ConstraintContext) => boolean) | undefined;
59
61
  }
60
62
  type BindingBaseKeys = keyof BindingBase<unknown>;
63
+ /**
64
+ * The lifecycle hooks every kind but `alias` may carry.
65
+ *
66
+ * @remarks Declared as **methods**, not function-typed properties, so their parameters compare
67
+ * bivariantly and `Binding<Value>` stays assignable to `Binding`. The engine erases the value type at
68
+ * every lane boundary regardless; the public `ActivationHandler` / `DeactivationHandler` keep strict
69
+ * checking, which is where a user's handler is actually verified. Not `readonly`: a fluent chain
70
+ * refines both in place — see {@link RefinableBindingFields}.
71
+ */
72
+ interface BindingLifecycleHooks<Value> {
73
+ onActivation?(ctx: ResolutionContext, instance: Value): Value | Promise<Value>;
74
+ onDeactivation?(instance: Value): void | Promise<void>;
75
+ }
61
76
  /**
62
77
  * @since 0.3.16-canary.0
63
78
  */
64
- export interface ClassBinding<Value> extends BindingBase<Value> {
79
+ export interface ClassBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
65
80
  readonly kind: "class";
66
81
  readonly target: Constructor<Value>;
67
82
  readonly scope: BindingScope;
68
- readonly onActivation?: ActivationHandler<Value> | undefined;
69
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
70
83
  }
71
84
  /**
72
85
  * @since 0.3.16-canary.0
73
86
  */
74
- export interface DynamicBinding<Value> extends BindingBase<Value> {
87
+ export interface DynamicBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
75
88
  readonly kind: "dynamic";
76
89
  readonly factory: (ctx: ResolutionContext) => Value;
77
90
  readonly scope: BindingScope;
78
- readonly onActivation?: ActivationHandler<Value> | undefined;
79
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
80
91
  }
81
92
  /**
82
93
  * @since 0.3.16-canary.0
83
94
  */
84
- export interface DynamicAsyncBinding<Value> extends BindingBase<Value> {
95
+ export interface DynamicAsyncBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
85
96
  readonly kind: "dynamic-async";
86
97
  readonly factory: (ctx: ResolutionContext) => Promise<Value>;
87
98
  readonly scope: BindingScope;
88
- readonly onActivation?: ActivationHandler<Value> | undefined;
89
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
90
99
  }
91
100
  /**
92
101
  * @since 0.3.16-canary.0
93
102
  */
94
- export interface ResolvedBinding<Value> extends BindingBase<Value> {
103
+ export interface ResolvedBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
95
104
  readonly kind: "resolved";
96
105
  readonly factory: (...args: Array<unknown>) => Value;
97
106
  readonly deps: ReadonlyArray<InjectionDescriptor>;
98
107
  readonly scope: BindingScope;
99
- readonly onActivation?: ActivationHandler<Value> | undefined;
100
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
101
108
  }
102
109
  /**
103
110
  * @since 0.3.16-canary.0
104
111
  */
105
- export interface ResolvedAsyncBinding<Value> extends BindingBase<Value> {
112
+ export interface ResolvedAsyncBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
106
113
  readonly kind: "resolved-async";
107
114
  readonly factory: (...args: Array<unknown>) => Promise<Value>;
108
115
  readonly deps: ReadonlyArray<InjectionDescriptor>;
109
116
  readonly scope: BindingScope;
110
- readonly onActivation?: ActivationHandler<Value> | undefined;
111
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
112
117
  }
113
118
  /**
114
119
  * @since 0.3.16-canary.0
115
120
  */
116
- export interface ConstantBinding<Value> extends BindingBase<Value> {
121
+ export interface ConstantBinding<Value> extends BindingBase<Value>, BindingLifecycleHooks<Value> {
117
122
  readonly kind: "constant";
118
123
  readonly value: Value;
119
124
  readonly scope: "singleton";
120
- readonly onActivation?: ActivationHandler<Value> | undefined;
121
- readonly onDeactivation?: DeactivationHandler<Value> | undefined;
122
125
  }
123
126
  /**
124
127
  * @since 0.3.16-canary.0
@@ -126,6 +129,13 @@ export interface ConstantBinding<Value> extends BindingBase<Value> {
126
129
  export interface AliasBinding<Value> extends BindingBase<Value> {
127
130
  readonly kind: "alias";
128
131
  readonly target: Token<Value> | Constructor<Value>;
132
+ /**
133
+ * Always `transient` — an alias defers scoping to the binding it points at.
134
+ *
135
+ * @remarks Declared so `scope` is present on every kind, which is what lets the engine read it
136
+ * as a plain field instead of testing for the one kind that lacks it.
137
+ */
138
+ readonly scope: "transient";
129
139
  }
130
140
  /**
131
141
  * @since 0.3.16-canary.0
@@ -172,6 +182,15 @@ export interface RefinableBindingFields<Value> {
172
182
  * @since 0.5.0-canary.8
173
183
  */
174
184
  export declare function refinableFields<Value>(binding: Binding<Value>): RefinableBindingFields<Value>;
185
+ /**
186
+ * Drops the memoized resolution frame, for a refinement that changes what the frame reports.
187
+ *
188
+ * @remarks `scope` is the only field a chain writes in place that the frame derives from — a
189
+ * re-slot builds a fresh binding, whose frame starts empty anyway.
190
+ *
191
+ * @since 0.5.0-canary.9
192
+ */
193
+ export declare function clearBindingFrame<Value>(binding: Binding<Value>): void;
175
194
  /**
176
195
  * Common slot-constraint + id methods shared by all concrete binding builders.
177
196
  *