@codefast/di 0.5.0 → 0.6.1

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 (266) hide show
  1. package/CHANGELOG.md +712 -0
  2. package/README.md +119 -30
  3. package/dist/ambient/active-container.d.ts +37 -0
  4. package/dist/ambient/active-container.d.ts.map +1 -0
  5. package/dist/ambient/active-container.js +41 -0
  6. package/dist/ambient/active-container.js.map +1 -0
  7. package/dist/container/binding-builders.d.ts +12 -8
  8. package/dist/container/binding-builders.d.ts.map +1 -1
  9. package/dist/container/binding-builders.js +62 -31
  10. package/dist/container/binding-builders.js.map +1 -1
  11. package/dist/container/container.d.ts +33 -19
  12. package/dist/container/container.d.ts.map +1 -1
  13. package/dist/container/container.js +281 -118
  14. package/dist/container/container.js.map +1 -1
  15. package/dist/{resolution → core}/binding-scope.d.ts +2 -2
  16. package/dist/core/binding-scope.d.ts.map +1 -0
  17. package/dist/core/binding-scope.js.map +1 -0
  18. package/dist/{binding.d.ts → core/binding.d.ts} +70 -15
  19. package/dist/core/binding.d.ts.map +1 -0
  20. package/dist/{binding.js → core/binding.js} +27 -12
  21. package/dist/core/binding.js.map +1 -0
  22. package/dist/core/constraint-requirement.d.ts +53 -0
  23. package/dist/core/constraint-requirement.d.ts.map +1 -0
  24. package/dist/core/constraint-requirement.js +62 -0
  25. package/dist/core/constraint-requirement.js.map +1 -0
  26. package/dist/{constructor-type.d.ts → core/constructor-type.d.ts} +1 -1
  27. package/dist/core/constructor-type.d.ts.map +1 -0
  28. package/dist/core/constructor-type.js.map +1 -0
  29. package/dist/{module.d.ts → core/module.d.ts} +5 -5
  30. package/dist/core/module.d.ts.map +1 -0
  31. package/dist/{module.js → core/module.js} +4 -4
  32. package/dist/core/module.js.map +1 -0
  33. package/dist/{registry.d.ts → core/registry.d.ts} +27 -7
  34. package/dist/core/registry.d.ts.map +1 -0
  35. package/dist/{registry.js → core/registry.js} +125 -52
  36. package/dist/core/registry.js.map +1 -0
  37. package/dist/core/tag.d.ts +80 -0
  38. package/dist/core/tag.d.ts.map +1 -0
  39. package/dist/core/tag.js +96 -0
  40. package/dist/core/tag.js.map +1 -0
  41. package/dist/{token.d.ts → core/token.d.ts} +2 -6
  42. package/dist/core/token.d.ts.map +1 -0
  43. package/dist/core/token.js +13 -0
  44. package/dist/core/token.js.map +1 -0
  45. package/dist/{types.d.ts → core/types.d.ts} +27 -21
  46. package/dist/core/types.d.ts.map +1 -0
  47. package/dist/core/types.js.map +1 -0
  48. package/dist/decorators/inject.d.ts +5 -49
  49. package/dist/decorators/inject.d.ts.map +1 -1
  50. package/dist/decorators/inject.js +34 -104
  51. package/dist/decorators/inject.js.map +1 -1
  52. package/dist/decorators/injectable.d.ts +25 -5
  53. package/dist/decorators/injectable.d.ts.map +1 -1
  54. package/dist/decorators/injectable.js +2 -3
  55. package/dist/decorators/injectable.js.map +1 -1
  56. package/dist/decorators/lifecycle-decorators.d.ts +4 -2
  57. package/dist/decorators/lifecycle-decorators.d.ts.map +1 -1
  58. package/dist/decorators/lifecycle-decorators.js +19 -25
  59. package/dist/decorators/lifecycle-decorators.js.map +1 -1
  60. package/dist/{resolution → errors}/diagnostics.d.ts +2 -0
  61. package/dist/errors/diagnostics.d.ts.map +1 -0
  62. package/dist/errors/diagnostics.js.map +1 -0
  63. package/dist/{errors.d.ts → errors/errors.d.ts} +89 -4
  64. package/dist/errors/errors.d.ts.map +1 -0
  65. package/dist/{errors.js → errors/errors.js} +129 -7
  66. package/dist/errors/errors.js.map +1 -0
  67. package/dist/index.d.ts +21 -14
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +15 -7
  70. package/dist/index.js.map +1 -1
  71. package/dist/injection/descriptor.d.ts +79 -0
  72. package/dist/injection/descriptor.d.ts.map +1 -0
  73. package/dist/injection/descriptor.js +118 -0
  74. package/dist/injection/descriptor.js.map +1 -0
  75. package/dist/{resolution → injection}/resolve-options.d.ts +21 -11
  76. package/dist/injection/resolve-options.d.ts.map +1 -0
  77. package/dist/injection/resolve-options.js +101 -0
  78. package/dist/injection/resolve-options.js.map +1 -0
  79. package/dist/introspection/dependency-graph.d.ts +16 -5
  80. package/dist/introspection/dependency-graph.d.ts.map +1 -1
  81. package/dist/introspection/dependency-graph.js +138 -57
  82. package/dist/introspection/dependency-graph.js.map +1 -1
  83. package/dist/introspection/graph-adapters/cytoscape.d.ts +6 -3
  84. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -1
  85. package/dist/introspection/graph-adapters/cytoscape.js +3 -0
  86. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -1
  87. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -1
  88. package/dist/introspection/graph-adapters/dot.js +12 -5
  89. package/dist/introspection/graph-adapters/dot.js.map +1 -1
  90. package/dist/introspection/graph-adapters/mermaid.d.ts +9 -0
  91. package/dist/introspection/graph-adapters/mermaid.d.ts.map +1 -0
  92. package/dist/introspection/graph-adapters/mermaid.js +47 -0
  93. package/dist/introspection/graph-adapters/mermaid.js.map +1 -0
  94. package/dist/introspection/graph-adapters/reactflow.d.ts +6 -3
  95. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -1
  96. package/dist/introspection/graph-adapters/reactflow.js +5 -1
  97. package/dist/introspection/graph-adapters/reactflow.js.map +1 -1
  98. package/dist/introspection/inspector.d.ts +4 -4
  99. package/dist/introspection/inspector.d.ts.map +1 -1
  100. package/dist/introspection/inspector.js +10 -6
  101. package/dist/introspection/inspector.js.map +1 -1
  102. package/dist/lifecycle/lifecycle-manager.d.ts +27 -0
  103. package/dist/lifecycle/lifecycle-manager.d.ts.map +1 -0
  104. package/dist/{resolution/lifecycle.js → lifecycle/lifecycle-manager.js} +39 -9
  105. package/dist/lifecycle/lifecycle-manager.js.map +1 -0
  106. package/dist/lifecycle/scope-manager.d.ts +48 -0
  107. package/dist/lifecycle/scope-manager.d.ts.map +1 -0
  108. package/dist/lifecycle/scope-manager.js +135 -0
  109. package/dist/lifecycle/scope-manager.js.map +1 -0
  110. package/dist/metadata/metadata-reader-token.d.ts +1 -1
  111. package/dist/metadata/metadata-reader-token.d.ts.map +1 -1
  112. package/dist/metadata/metadata-reader-token.js +1 -1
  113. package/dist/metadata/metadata-reader-token.js.map +1 -1
  114. package/dist/metadata/metadata-types.d.ts +9 -9
  115. package/dist/metadata/metadata-types.d.ts.map +1 -1
  116. package/dist/metadata/symbol-metadata-reader.d.ts +19 -6
  117. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
  118. package/dist/metadata/symbol-metadata-reader.js +89 -9
  119. package/dist/metadata/symbol-metadata-reader.js.map +1 -1
  120. package/dist/metadata/verifying-metadata-reader.d.ts +16 -0
  121. package/dist/metadata/verifying-metadata-reader.d.ts.map +1 -0
  122. package/dist/metadata/verifying-metadata-reader.js +32 -0
  123. package/dist/metadata/verifying-metadata-reader.js.map +1 -0
  124. package/dist/resolution/{activation-need.d.ts → cache/activation-need.d.ts} +5 -5
  125. package/dist/resolution/cache/activation-need.d.ts.map +1 -0
  126. package/dist/resolution/cache/activation-need.js.map +1 -0
  127. package/dist/resolution/{binding-lookup-cache.d.ts → cache/binding-lookup-cache.d.ts} +9 -5
  128. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -0
  129. package/dist/resolution/{binding-lookup-cache.js → cache/binding-lookup-cache.js} +55 -0
  130. package/dist/resolution/cache/binding-lookup-cache.js.map +1 -0
  131. package/dist/resolution/cache/class-introspector.d.ts +66 -0
  132. package/dist/resolution/cache/class-introspector.d.ts.map +1 -0
  133. package/dist/resolution/cache/class-introspector.js +194 -0
  134. package/dist/resolution/cache/class-introspector.js.map +1 -0
  135. package/dist/resolution/context.d.ts +84 -0
  136. package/dist/resolution/context.d.ts.map +1 -0
  137. package/dist/resolution/{environment.js → context.js} +54 -80
  138. package/dist/resolution/context.js.map +1 -0
  139. package/dist/resolution/path/resolution-path.d.ts +94 -0
  140. package/dist/resolution/path/resolution-path.d.ts.map +1 -0
  141. package/dist/resolution/path/resolution-path.js +117 -0
  142. package/dist/resolution/path/resolution-path.js.map +1 -0
  143. package/dist/resolution/{instantiation-plan.d.ts → plan/instantiation-plan.d.ts} +37 -7
  144. package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -0
  145. package/dist/resolution/plan/instantiation-plan.js +419 -0
  146. package/dist/resolution/plan/instantiation-plan.js.map +1 -0
  147. package/dist/resolution/resolver.d.ts +28 -28
  148. package/dist/resolution/resolver.d.ts.map +1 -1
  149. package/dist/resolution/resolver.js +581 -239
  150. package/dist/resolution/resolver.js.map +1 -1
  151. package/dist/resolution/select/binding-select.d.ts +34 -0
  152. package/dist/resolution/select/binding-select.d.ts.map +1 -0
  153. package/dist/resolution/select/binding-select.js +175 -0
  154. package/dist/resolution/select/binding-select.js.map +1 -0
  155. package/dist/resolution/{constraints.d.ts → select/constraints.d.ts} +13 -12
  156. package/dist/resolution/select/constraints.d.ts.map +1 -0
  157. package/dist/resolution/{constraints.js → select/constraints.js} +33 -22
  158. package/dist/resolution/select/constraints.js.map +1 -0
  159. package/package.json +160 -20
  160. package/src/ambient/active-container.ts +65 -0
  161. package/src/container/binding-builders.ts +79 -44
  162. package/src/container/container.ts +357 -170
  163. package/src/{resolution → core}/binding-scope.ts +2 -2
  164. package/src/{binding.ts → core/binding.ts} +101 -39
  165. package/src/core/constraint-requirement.ts +94 -0
  166. package/src/{constructor-type.ts → core/constructor-type.ts} +1 -1
  167. package/src/{module.ts → core/module.ts} +10 -10
  168. package/src/core/registry.ts +365 -0
  169. package/src/core/tag.ts +158 -0
  170. package/src/{token.ts → core/token.ts} +2 -14
  171. package/src/{types.ts → core/types.ts} +37 -34
  172. package/src/decorators/inject.ts +39 -175
  173. package/src/decorators/injectable.ts +40 -10
  174. package/src/decorators/lifecycle-decorators.ts +22 -34
  175. package/src/{resolution → errors}/diagnostics.ts +2 -0
  176. package/src/{errors.ts → errors/errors.ts} +152 -8
  177. package/src/index.ts +43 -15
  178. package/src/injection/descriptor.ts +206 -0
  179. package/src/injection/resolve-options.ts +144 -0
  180. package/src/introspection/dependency-graph.ts +223 -63
  181. package/src/introspection/graph-adapters/cytoscape.ts +9 -3
  182. package/src/introspection/graph-adapters/dot.ts +13 -5
  183. package/src/introspection/graph-adapters/mermaid.ts +61 -0
  184. package/src/introspection/graph-adapters/reactflow.ts +11 -4
  185. package/src/introspection/inspector.ts +16 -12
  186. package/src/{resolution/lifecycle.ts → lifecycle/lifecycle-manager.ts} +68 -32
  187. package/src/lifecycle/scope-manager.ts +156 -0
  188. package/src/metadata/metadata-reader-token.ts +2 -2
  189. package/src/metadata/metadata-types.ts +9 -9
  190. package/src/metadata/symbol-metadata-reader.ts +95 -18
  191. package/src/metadata/verifying-metadata-reader.ts +41 -0
  192. package/src/resolution/{activation-need.ts → cache/activation-need.ts} +8 -8
  193. package/src/resolution/{binding-lookup-cache.ts → cache/binding-lookup-cache.ts} +63 -5
  194. package/src/resolution/cache/class-introspector.ts +251 -0
  195. package/src/resolution/{environment.ts → context.ts} +84 -145
  196. package/src/resolution/path/resolution-path.ts +167 -0
  197. package/src/resolution/plan/instantiation-plan.ts +615 -0
  198. package/src/resolution/resolver.ts +663 -313
  199. package/src/resolution/select/binding-select.ts +210 -0
  200. package/src/resolution/select/constraints.ts +130 -0
  201. package/dist/binding.d.ts.map +0 -1
  202. package/dist/binding.js.map +0 -1
  203. package/dist/constructor-type.d.ts.map +0 -1
  204. package/dist/constructor-type.js.map +0 -1
  205. package/dist/errors.d.ts.map +0 -1
  206. package/dist/errors.js.map +0 -1
  207. package/dist/module.d.ts.map +0 -1
  208. package/dist/module.js.map +0 -1
  209. package/dist/registry.d.ts.map +0 -1
  210. package/dist/registry.js.map +0 -1
  211. package/dist/resolution/activation-need.d.ts.map +0 -1
  212. package/dist/resolution/activation-need.js.map +0 -1
  213. package/dist/resolution/binding-lookup-cache.d.ts.map +0 -1
  214. package/dist/resolution/binding-lookup-cache.js.map +0 -1
  215. package/dist/resolution/binding-scope.d.ts.map +0 -1
  216. package/dist/resolution/binding-scope.js.map +0 -1
  217. package/dist/resolution/binding-select.d.ts +0 -23
  218. package/dist/resolution/binding-select.d.ts.map +0 -1
  219. package/dist/resolution/binding-select.js +0 -121
  220. package/dist/resolution/binding-select.js.map +0 -1
  221. package/dist/resolution/class-introspector.d.ts +0 -27
  222. package/dist/resolution/class-introspector.d.ts.map +0 -1
  223. package/dist/resolution/class-introspector.js +0 -60
  224. package/dist/resolution/class-introspector.js.map +0 -1
  225. package/dist/resolution/constraints.d.ts.map +0 -1
  226. package/dist/resolution/constraints.js.map +0 -1
  227. package/dist/resolution/diagnostics.d.ts.map +0 -1
  228. package/dist/resolution/diagnostics.js.map +0 -1
  229. package/dist/resolution/environment.d.ts +0 -94
  230. package/dist/resolution/environment.d.ts.map +0 -1
  231. package/dist/resolution/environment.js.map +0 -1
  232. package/dist/resolution/instantiation-plan.d.ts.map +0 -1
  233. package/dist/resolution/instantiation-plan.js +0 -183
  234. package/dist/resolution/instantiation-plan.js.map +0 -1
  235. package/dist/resolution/lifecycle.d.ts +0 -23
  236. package/dist/resolution/lifecycle.d.ts.map +0 -1
  237. package/dist/resolution/lifecycle.js.map +0 -1
  238. package/dist/resolution/resolution-path.d.ts +0 -98
  239. package/dist/resolution/resolution-path.d.ts.map +0 -1
  240. package/dist/resolution/resolution-path.js +0 -98
  241. package/dist/resolution/resolution-path.js.map +0 -1
  242. package/dist/resolution/resolve-options.d.ts.map +0 -1
  243. package/dist/resolution/resolve-options.js +0 -56
  244. package/dist/resolution/resolve-options.js.map +0 -1
  245. package/dist/resolution/scope.d.ts +0 -28
  246. package/dist/resolution/scope.d.ts.map +0 -1
  247. package/dist/resolution/scope.js +0 -88
  248. package/dist/resolution/scope.js.map +0 -1
  249. package/dist/token.d.ts.map +0 -1
  250. package/dist/token.js +0 -22
  251. package/dist/token.js.map +0 -1
  252. package/dist/types.d.ts.map +0 -1
  253. package/dist/types.js.map +0 -1
  254. package/src/registry.ts +0 -282
  255. package/src/resolution/binding-select.ts +0 -154
  256. package/src/resolution/class-introspector.ts +0 -74
  257. package/src/resolution/constraints.ts +0 -121
  258. package/src/resolution/instantiation-plan.ts +0 -292
  259. package/src/resolution/resolution-path.ts +0 -156
  260. package/src/resolution/resolve-options.ts +0 -89
  261. package/src/resolution/scope.ts +0 -105
  262. /package/dist/{resolution → core}/binding-scope.js +0 -0
  263. /package/dist/{constructor-type.js → core/constructor-type.js} +0 -0
  264. /package/dist/{types.js → core/types.js} +0 -0
  265. /package/dist/{resolution → errors}/diagnostics.js +0 -0
  266. /package/dist/resolution/{activation-need.js → cache/activation-need.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,717 @@
1
1
  # @codefast/di
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#729](https://github.com/codefastlabs/codefast/pull/729) [`a8fff29`](https://github.com/codefastlabs/codefast/commit/a8fff29aac58b6e60595de35a613795087b055ab) Thanks [@thevuong](https://github.com/thevuong)! - perf(di): settle a single-tag dependency at compile time, the named settlement's rule on the tagged lane
8
+
9
+ A compiled plan settled a name-only dependency ahead of time and still escaped a tag-only one, though the tagged lane
10
+ now has everything the named rule needs: criteria are interned so an index hit is exact, and the chain-walk memo gives
11
+ the compiler a path-independent lookup to ask. A dependency carrying one tag and nothing else, whose candidate carries
12
+ no predicate and whose slot the request satisfies, now compiles to a plain dep thunk; a predicate, a second tag, a name,
13
+ a miss — anything whose selection could read the resolution path — escapes exactly as before, on both the sync and async
14
+ plan lanes.
15
+
16
+ `InstantiationPlanHost` gains `lookupPathIndependentTaggedEntry`, deliberately **optional** where the named twin's
17
+ arrival was a breaking change: a host that does not provide it stays a valid host, and a compiler given none simply
18
+ escapes the dependency, which is the pre-settlement behavior.
19
+
20
+ Measured paired against the previous build, six alternating passes: the new `slot-injected-tag-compiled` row — four
21
+ tagged constants injected into one class, mirroring the named pair — reads **4.33×** (every pass 4.13–4.61), going 5.76M
22
+ → 24.96M hz/op and landing exactly where `slot-injected-name-compiled` and the criteria-free `plan-deps-inlined` sit,
23
+ which is what "the criterion was the only reason it escaped" predicts. The interpreted twin and three controls hold
24
+ parity. Eight tests mirror the named settlement's pins — the baked answer tracks rebinds, a predicate keeps runtime
25
+ selection, an opaque factory's escape replays the tag — plus one holding a two-tag dependency on the runtime path.
26
+
27
+ - [#729](https://github.com/codefastlabs/codefast/pull/729) [`a60bcf8`](https://github.com/codefastlabs/codefast/commit/a60bcf80deeda3964b0bf2e8a6d30aeeb6dc39ab) Thanks [@thevuong](https://github.com/thevuong)! - fix(di): a `when()` chain no longer hides a helper's requirement from `validate()`
28
+
29
+ `.when(whenParentNamed("x")).when(other)` composes one closure out of two predicates, and the composition dropped the
30
+ unreachability requirement the name helper had recorded — the constraint stayed impossible to satisfy, `validate()` just
31
+ could not see it, a limit the previous changeset stated outright. SPEC's rule carries no such carve-out: `validate()`
32
+ throws `UnreachableConstraintError` when no binding declares the slot name a constraint waits for, composed or not.
33
+
34
+ The composition site now merges both sides' requirements onto the composite predicate, and `validate()` reads the full
35
+ list, so a requirement survives any number of `when()` narrowings and either side of the chain can contribute one.
36
+ `constraintRequirementOf` keeps its shape and answers the first recorded requirement; the plural
37
+ `constraintRequirementsOf` is the reader `validate()` uses. A container that previously validated clean can now throw —
38
+ that is the documented rule holding where it silently did not.
39
+
40
+ - [#729](https://github.com/codefastlabs/codefast/pull/729) [`0ee3290`](https://github.com/codefastlabs/codefast/commit/0ee329099359352ed8870d4f8bcfcbb8f2a55126) Thanks [@thevuong](https://github.com/thevuong)! - perf(di): lend a sync escape's seed stack instead of copying it per call
41
+
42
+ A compiled plan's escape re-enters the runtime resolver seeded with its ancestor frames, and minted a fresh
43
+ `[...frames]` on every call because the resolver pushes and pops on the array it is given. Every sync lane pops what it
44
+ pushes, so the owned array still holds exactly the seed when a call returns — the thunk now lends one array, the
45
+ resolver's own root-stack rule one level down. A dirty return (length not restored) drops the array and the next call
46
+ mints; re-entering the same thunk without a genuine cycle is impossible — every route back to the same plan node crosses
47
+ a binding that is still in flight — so the claimed branch is a one-compare defence rather than a hot case. The async
48
+ escape lane keeps copying: it lives across awaits, where "the call returned" and "the stack is free" are different
49
+ moments.
50
+
51
+ The win is bigger than removing one small allocation, and the mechanism is the context pool: a pooled resolution context
52
+ is reused only for the array pair it already holds, so a fresh array per escape forced a fresh context per escape — the
53
+ lent array keeps its identity across calls and the pool starts hitting. Measured paired against the previous build, six
54
+ alternating passes, all five escape rows positive in every pass: `plan-escape-factory-dep` **1.41×** (1.331–1.460),
55
+ `plan-escape-scoped-dep` **1.23×**, `plan-escape-optional-dep` **1.14×**, `plan-escape-hooked-dep` **1.12×**,
56
+ `plan-escape-multi-dep` **1.11×**; the no-escape `plan-deps-inlined` control holds parity and
57
+ `realistic-graph-resolve-root`, whose root plan escapes once for a singleton materialization, reads 1.03×. A new test
58
+ pins that a throwing escape leaves the thunk reusable.
59
+
60
+ - [#729](https://github.com/codefastlabs/codefast/pull/729) [`12186d6`](https://github.com/codefastlabs/codefast/commit/12186d698b57a491a7b99d63750ff83199772f35) Thanks [@thevuong](https://github.com/thevuong)! - perf(di): a one-entry cache in front of `TagKey.of()`, narrowing the inline-criterion gap
61
+
62
+ The tag-interning changeset recorded that an inline `.of(v)` stayed behind a hoisted criterion because every call reads
63
+ the intern map, and that the gap had widened rather than closed. An inline call site usually repeats one value, so
64
+ `of()` now keeps its last `(value, pair)` and answers a repeat before touching `internKeyFor` or the map. The check is
65
+ `Object.is` — the comparison the slot contract already defines — so ±0 stay distinct and `NaN` hits itself, with no
66
+ special-casing.
67
+
68
+ Measured paired against the previous build, six alternating passes on rows whose A/A floor is the suite's widest:
69
+ `slot-tag-shorthand-inline` **1.07×** (six of six passes positive, 1.040–1.124) and `slot-tag-array-inline` **1.07×**
70
+ (five of six), while both hoisted rows — which never call `.of()` in the loop — sit at parity, doubling as the proof the
71
+ source swap was live. Hoisted stays the fast spelling, as it must: it pays zero calls.
72
+
73
+ - [#729](https://github.com/codefastlabs/codefast/pull/729) [`aa76f4d`](https://github.com/codefastlabs/codefast/commit/aa76f4d0559004337f4c0a0aa89b434c26a78d3c) Thanks [@thevuong](https://github.com/thevuong)! - perf(di): memoize the single-tag chain walk, with a deferred map so a per-request child does not pay for it
74
+
75
+ A single-tag `resolve` was the one criteria lane that consulted each container's tag index on the way up on **every**
76
+ call — `defaultEntry` and `namedEntry` already memoized their walks, and ARCHITECTURE carried the question as open,
77
+ waiting on a fresh-vs-warm measurement. That measurement (RESULTS.md, 2026-08-16): the memoized name lane answered the
78
+ same parent-owned shape at ~2.1× the unmemoized tag lane, warm; the memo's per-container state cost ~5–8% at duty cycle
79
+ 1 and amortized before N=4.
80
+
81
+ `BindingLookupCache` gains `taggedEntry(token, tag)` — chain-versioned like `namedEntry`, `null` meaning "this shape
82
+ needs full selection", predicate- and alias-carrying hits declined so a `when()` is still evaluated on every resolve —
83
+ and `resolve()` gains the tagged twin of its name-only fast lane, dispatching just the shapes whose semantics involve no
84
+ resolution context (plain constants, cached singletons). The memo key is the criterion object itself: interning makes
85
+ identity the slot contract's own `Object.is`, so ±0 stay split and no indexed hit needs a value re-check.
86
+
87
+ The first cut mirrored `namedEntry` exactly and failed its own gate: `fresh-child-tag-n1` — one tagged resolve inside a
88
+ child that then dies — read 0.861 across six negative passes, and the whole cost was the inner-map allocation on a
89
+ container that never asks twice. So the first `(token, tag)` shape a cache generation sees is answered from the walk and
90
+ parked in a one-entry front; the map is not written until a second distinct shape appears, and an alternating pair
91
+ converges after one extra walk per key.
92
+
93
+ Measured paired against the previous build, six alternating passes, medians: `slot-tag-parent-owned` **2.78×** (every
94
+ pass 2.73–2.95), `slot-tag-shorthand-hoisted` **2.14×**, `tagged-binding-resolve` **1.98×** (a head-to-head row),
95
+ `fresh-child-tag-n4` **1.24×**, with the must-hold `fresh-child-tag-n1` at 0.98 (0.933–1.072, inside that row's floor)
96
+ and the name-lane and multi-tag controls at parity. Seven new tests pin the memo's invalidation and its refusals:
97
+ rebind/unbind in both request spellings, a parent rebind observed from a child, a predicate beside the tag, a late
98
+ container-level activation hook, and a warmed `+0` criterion refusing `-0`.
99
+
100
+ ## 0.6.0
101
+
102
+ ### Minor Changes
103
+
104
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`9182664`](https://github.com/codefastlabs/codefast/commit/91826641f284ebf8e7bfcdbdcb3aaf73f77381cb) Thanks [@thevuong](https://github.com/thevuong)! - perf(di): compile statically-visible graphs entering resolveAsync into async plans
105
+
106
+ A transient `class`/`resolved`/`resolved-async` binding resolved by `resolveAsync` at a true root (no open cascade) now
107
+ compiles once into a plan, like the sync lane has always done — the graph is declared up front, so nothing about it
108
+ needs per-level bookkeeping. A fully synchronous subtree executes without touching a promise; nodes that may yield one
109
+ await their dependencies together, exactly as the interpreted path does (promise-valued constants unwrap, siblings start
110
+ before the first rejection propagates). `dynamic-async` factories stay opaque and keep the cascade lane. Escapes replay
111
+ the async dispatch seeded with the plan's ancestors, so cycles, criteria and hooks behave as if nothing had compiled.
112
+ Diagnostics gain `compiledAsyncPlanCount`.
113
+
114
+ - [#690](https://github.com/codefastlabs/codefast/pull/690) [`ed1387c`](https://github.com/codefastlabs/codefast/commit/ed1387c7be719ece9a271993834dd23347b5bf6e) Thanks [@thevuong](https://github.com/thevuong)! - Fix `toResolved` bindings on the async path, and tighten the type surface.
115
+
116
+ `bind(T).toResolved(factory, deps)` threw `InternalError: resolved binding requires resolution context` from every async
117
+ entry point — `resolveAsync`, `resolveAllAsync`, `resolveOptionalAsync`, and any `toDynamicAsync` factory awaiting one.
118
+ `requiresResolutionContext()` answers only for the two factory kinds that are handed a context, so a `resolved` binding
119
+ legitimately arrives with none; the guard that rejected it never read the context it demanded. The sync lane never had
120
+ it.
121
+
122
+ Type-surface changes, all verified type-identical or strictly wider:
123
+
124
+ - Optional properties on the public option bags — `ResolveOptions`, `InjectOptions`, `InjectableOptions`, `GraphOptions`
125
+ — are now `?: T | undefined`. Under `exactOptionalPropertyTypes` the old `?: T` rejected a caller holding
126
+ `T | undefined`, which is the shape a real call site has.
127
+ - `BindingConstraint` is exported: the `(ctx: ConstraintContext) => boolean` that `when()` takes and every `when*`
128
+ helper returns now has a name instead of fourteen inline spellings.
129
+ - `ParamMetadata` and `InjectionDescriptor` extend `DependencySlot`, and the dependency-graph builder uses it directly,
130
+ so the one shape both dependency sources normalise to is enforced by the compiler rather than by four declarations
131
+ that happened to match.
132
+ - `PartialBinding` is derived from `Binding` by a distributive `Omit`, so a new binding kind cannot join one union and
133
+ miss the other.
134
+ - The inert `const` modifier is gone from type parameters inferred from a token rather than a literal; the four on
135
+ `toResolved`/`toResolvedAsync`, where `deps` is an array literal, stay.
136
+
137
+ - [#703](https://github.com/codefastlabs/codefast/pull/703) [`4ceccf2`](https://github.com/codefastlabs/codefast/commit/4ceccf2656ca626215093b85c4111ef8e195c1fd) Thanks [@thevuong](https://github.com/thevuong)! - A chain's `when()` calls now narrow rather than replace. SPEC defines a candidate as a binding that passes **all** of a
138
+ chain's `when(ctx)` predicates, and §5.4 describes a binding as carrying "one or several constraints combined"; the
139
+ chain's type says the same by returning `this`. Only the implementation disagreed, and it did so silently — `#reslot`
140
+ overwrote the predicate field, so the first condition was discarded and never called.
141
+
142
+ The consequence was worse than a binding being more permissive than written. Specificity prefers a binding that carries
143
+ a predicate, so a discarded first condition made the constrained binding beat the default one:
144
+
145
+ ```ts
146
+ container
147
+ .bind(Logger)
148
+ .to(ConsoleLogger)
149
+ .when((ctx) => ctx.parent !== undefined) // never consulted
150
+ .when((ctx) => ctx.parent?.scope === "singleton");
151
+ ```
152
+
153
+ Nothing reported it — not at bind time, not at resolve time, not from `validate()`. A service that should have received
154
+ the default logger received the constrained one instead.
155
+
156
+ `whenTagged()` already accumulated, which is what made this a trap rather than a quirk: two adjacent methods with the
157
+ same chaining syntax and the same `this` return type, one combining and one replacing.
158
+
159
+ Binding-level `onActivation()` and `onDeactivation()` keep replacing. That reading is pinned by a test and is reasonable
160
+ for reconfiguring a chain held in a variable, even though the chained spelling reads as combination and the
161
+ container-level hooks accumulate. Changing it is a separate decision from this one.
162
+
163
+ - [#708](https://github.com/codefastlabs/codefast/pull/708) [`957f438`](https://github.com/codefastlabs/codefast/commit/957f4385612d068162e82f134ec8d995e0819834) Thanks [@thevuong](https://github.com/thevuong)! - A compiled plan now settles a name-only dependency at compile time instead of escaping to the runtime for it. A
164
+ dependency escaped as soon as it carried any criterion, before anything tried to look it up — yet `whenNamed` writes the
165
+ binding's slot name rather than a predicate, so a name-only request is usually a plain hit in the registry's named
166
+ index, and that index is already memoized on the same registry version the plan cache is keyed on. Four named constants
167
+ injected into one class stop escaping and become four `() => value` thunks.
168
+
169
+ The row that measures exactly that shape, `slot-injected-name-compiled`, reads **4.06×** the previous build, paired and
170
+ alternating over twelve passes with every pass above 3.87×, while its interpreted twin and eleven control rows hold
171
+ parity. Allocation on the same shape falls from 1260 to 366 scavenges per 2M resolves — level with the criteria-free
172
+ plan of the same arity, so the compiled lane no longer allocates more than the interpreted path it exists to beat.
173
+
174
+ Selection is only baked in when it cannot depend on the resolution path: the candidate must carry no predicate and its
175
+ slot must match the request. A predicate reads the path, so it stays the runtime's to evaluate, and anything else — a
176
+ tag, a miss, an ambiguous name — escapes exactly as before.
177
+
178
+ `InstantiationPlanHost` gains `lookupPathIndependentNamedEntry`. **Breaking** for anything implementing that interface
179
+ directly, which the package exposes on the `./resolution/plan/instantiation-plan` subpath.
180
+
181
+ - [#704](https://github.com/codefastlabs/codefast/pull/704) [`44dd4a3`](https://github.com/codefastlabs/codefast/commit/44dd4a3cfa886fdf43debc708d6ede9505d71ea5) Thanks [@thevuong](https://github.com/thevuong)! - `toConstantValue(...).onDeactivation(...)` now runs, and `validate()` reports a hook that can never run.
182
+
183
+ SPEC §3.4 names `toConstantValue` as one of the two things deactivation applies to ("treat as singleton"), and
184
+ `ConstantBindingBuilder` offers `onDeactivation` in the type. The hook was never called.
185
+
186
+ What made it hard to spot is that it worked whenever an activation hook happened to sit beside it:
187
+
188
+ ```ts
189
+ container
190
+ .bind(Pool)
191
+ .toConstantValue(pool)
192
+ .onDeactivation((p) => p.end()); // silent
193
+ container
194
+ .bind(Pool)
195
+ .toConstantValue(pool)
196
+ .onActivation((_ctx, p) => p) // unrelated
197
+ .onDeactivation((p) => p.end()); // now it fires
198
+ ```
199
+
200
+ The resolver's plain-constant fast path returns `binding.value` with no pipeline, and it decides that on activation
201
+ alone — but the step it skips is what records the binding as a cached singleton, and that recording is what teardown
202
+ iterates. An activation hook disables the fast path, so the deactivation starts working for a reason that has nothing to
203
+ do with it. `dispose()`, `unbind()`, `unbindAll()` and module `unload()` were all affected.
204
+
205
+ Teardown now finds a constant through the registry instead of relying on that recording. A container that has never held
206
+ a constant skips the sweep, so teardown cost is unchanged where there is nothing to find.
207
+
208
+ **A constant deactivates whether or not anything resolved it.** A singleton only exists after its first resolve, so an
209
+ unresolved one has nothing to deactivate; a constant is the opposite — the value is handed in at bind time and exists
210
+ from then on. Making the hook depend on whether someone happened to resolve it would make teardown depend on resolution
211
+ order. If the value was activated, the hook receives the activated value rather than the bound one.
212
+
213
+ ### `validate()` reports an unreachable hook
214
+
215
+ Container-level hooks are keyed by token identity, so a class used only as an implementation target matches nothing:
216
+
217
+ ```ts
218
+ container.bind(LoggerToken).to(ConsoleLogger);
219
+ container.onDeactivation(ConsoleLogger, (l) => l.flush()); // keyed by the class — never runs
220
+ ```
221
+
222
+ `validate()` now throws `UnreachableLifecycleHookError` when a container-level hook's token is bound in neither this
223
+ container nor any ancestor. Registering a hook before its binding stays valid — the check runs at `validate()`, not at
224
+ registration — and a hook for a parent-owned token is accepted, since the child resolves through it.
225
+
226
+ ### Not changed
227
+
228
+ `transient`, `scoped` and `toAlias` bindings still have no deactivation, and this is not a gap:
229
+ `TransientBindingBuilder`, `ScopedBindingBuilder` and `AliasBindingBuilder` do not expose `onDeactivation` at all, so
230
+ the compiler rejects it. They appear to accept one only under a runtime that skips type-checking.
231
+
232
+ - [#706](https://github.com/codefastlabs/codefast/pull/706) [`8dfac73`](https://github.com/codefastlabs/codefast/commit/8dfac73fd4278c94bfe20f1554ce3f06c62445ac) Thanks [@thevuong](https://github.com/thevuong)! - Two constraints that could never hold are now reported instead of quietly resolving to the default binding.
233
+
234
+ `whenParentTaggedAll([])` reads as a requirement but matches every parent — "carries all of no criteria" is vacuously
235
+ true, so the constraint silently weakens to "has a parent at all", and specificity still ranks it above an unconstrained
236
+ binding. Both `…TaggedAll` helpers now throw `EmptyTagCriteriaError` at the call site. An empty list is what a filtered
237
+ array or an absent config produces, which is exactly when nobody is watching.
238
+
239
+ `whenParentNamed("typo")` waits on a bare string, so a misspelling produces a constraint nothing can satisfy, with no
240
+ error at bind time, at resolve time, or from `validate()`. The name helpers now record what they wait for on the
241
+ predicate itself, and `validate()` throws `UnreachableConstraintError` when no binding in the container or its ancestors
242
+ declares that slot name.
243
+
244
+ Neither touches resolution: the criteria check runs where the helper is called, and the name check runs inside
245
+ `validate()`. The requirement rides on the predicate under a symbol that resolution never reads.
246
+
247
+ One limit, stated because it is easy to assume otherwise: chaining `.when(whenParentNamed("x")).when(other)` composes a
248
+ new closure, and the requirement does not survive that. The constraint is still unreachable; `validate()` just cannot
249
+ see it.
250
+
251
+ - [#681](https://github.com/codefastlabs/codefast/pull/681) [`4a29f20`](https://github.com/codefastlabs/codefast/commit/4a29f2086dd7ad8e9d3a1e429470776478af668c) Thanks [@thevuong](https://github.com/thevuong)! - A custom `MetadataReader` can now actually reach resolution, and the ambient container is public API:
252
+
253
+ - **`Container.create({ metadataReader })`** — new `ContainerOptions`. A container hands its reader to the resolver it
254
+ builds in its constructor, so a `MetadataReaderToken` binding on that same container was always too late: resolution
255
+ kept the decorator reader and any undecorated class threw `MissingMetadataError`, while
256
+ `validate()`/`inspect()`/`generateDependencyGraph()` re-read the token and honoured it — the two halves disagreed. The
257
+ option is in place before the resolver exists and is inherited by children; the binding path still works in its one
258
+ working shape (bound on a parent, used from a child).
259
+ - **One container, one reader.** That asymmetry is gone rather than documented: a container now answers every question —
260
+ resolve, `validate()`, `inspect()`, `generateDependencyGraph()`, `unbind*` — with the reader its resolver was built
261
+ with, so introspection cannot describe a class differently from how it is instantiated. As a side effect those paths
262
+ no longer re-scan the registry for `MetadataReaderToken` on every call.
263
+ - **`runWithContainer` / `getActiveContainer` are exported from the root entry**, alongside the metadata pieces needed
264
+ to write a reader without reaching for subpaths: `defaultMetadataReader`, `SymbolMetadataReader`, and the
265
+ `ConstructorMetadata` / `LifecycleMetadata` / `ParamMetadata` types. `toMermaidGraph` joins the other graph adapters
266
+ on the barrel.
267
+ - **A `MetadataReader`'s answer is verified, not trusted.** The seam returned `ConstructorMetadata` by cast, so a
268
+ hand-written reader that forgot `params` produced a bare `TypeError` from the plan compiler — no `code`, no class name
269
+ — while `validate()` passed the same container because its cold path defended with `?? []`. New `InvalidMetadataError`
270
+ names the class and the defect. A supplied reader is wrapped once at container construction so resolve, `validate()`
271
+ and `generateDependencyGraph()` all see verified answers; the decorator reader writes the metadata it later reads, so
272
+ a container that supplies none is left on the path it always took.
273
+ - **Fix: `MissingContainerContextError` named the accessor where it meant the class.** Constructing a class with
274
+ `@inject` accessors outside a container context reported `Class 'clock' … container.resolve(clock)` instead of the
275
+ class it was told to name (SPEC §7.5 already specified the class). The error now carries
276
+ `className: string | undefined` and `accessorName: string | symbol` instead of a single flattened `targetName`, and
277
+ phrases itself accordingly — the word "Class" leaves the sentence when there is no class to name. **Breaking:**
278
+ `targetName` is gone from this error.
279
+ - New examples `18-ambient-container` and `19-custom-metadata-reader`; SPEC §6.1, §6.11, §7.4 and §7.5 updated.
280
+
281
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`4d472a6`](https://github.com/codefastlabs/codefast/commit/4d472a68b5629f2fba034dae95f302c5db5cb437) Thanks [@thevuong](https://github.com/thevuong)! - fix(di)!: keep a factory's ctx on its own resolution path across a nested top-level resolve
282
+
283
+ The depth-indexed sync context pool reused a pooled context by resetting it onto whatever array pair the next
284
+ acquisition carried. A nested `container.resolve()` inside a factory mints its own path arrays and reaches the same
285
+ depth as the frame still holding that pooled context, so the outer factory's `ctx` was silently re-pointed at the nested
286
+ resolve's arrays — `ctx.resolve` then evaluated `when()` predicates against an empty ancestor chain and selected the
287
+ wrong binding, and `ctx.graph` reported an empty path. A pooled context is now reused only for the array pair it already
288
+ holds; a mismatch mints a fresh context, and only the resolver's two stable pairs may claim a pool slot.
289
+
290
+ Breaking (type/API surface, no behavioral change for correct programs):
291
+
292
+ - `ConstraintContext.currentResolveOptions` is now `Readonly<ResolveOptions>` — the object was already frozen at
293
+ runtime, so a write through it always threw; it is now a compile error.
294
+ - `ScopeManager.hasScoped`/`getScoped` are removed — dead since `readScoped` replaced the two-read shape.
295
+ - Diagnostics getters renamed for role: `BindingRegistry.isBuilt` → `isNamedIndexBuilt`/`isTaggedIndexBuilt` (the tagged
296
+ index was previously unobservable), `ScopeManager.isBuilt` → `isScopedCacheBuilt`, `LifecycleManager.isBuilt` →
297
+ `isActivationTableBuilt`. `builtSubsystems` now also reports `registry.taggedIndex`.
298
+ - `TagKeyMask` is exported from the package root.
299
+
300
+ - [#680](https://github.com/codefastlabs/codefast/pull/680) [`c415c6b`](https://github.com/codefastlabs/codefast/commit/c415c6bd9466421419fd7d97445fb29f76257d95) Thanks [@thevuong](https://github.com/thevuong)! - `generateDependencyGraph` now tells the whole wiring story instead of an approximation of it:
301
+
302
+ - **Optional dependencies are visible.** A bound optional dependency's edge carries an `optional` label; an unbound one
303
+ now points at an `unbound:<token>` placeholder node (`kind`/`scope`: `"unbound"`) instead of silently disappearing —
304
+ "optional and absent" is no longer indistinguishable from "not a dependency".
305
+ - **Multi-bindings fan out.** An `injectAll(...)` dependency draws an edge to every binding of the token, not just the
306
+ first.
307
+ - **Class-constructor edges use slot labels.** A named or tagged constructor dependency is labeled `name:...`/`tag:...`
308
+ like resolved-factory deps always were, and edge targets are filtered with the same slot-matching rules resolution
309
+ uses (`matchesSlot`, SPEC §6.9) — an unnamed request no longer draws an edge to a named binding it could never
310
+ resolve.
311
+ - **`includeParent` connects across the chain.** A child binding whose dependency is satisfied by the parent now gets
312
+ its edge (own bindings still shadow the parent, mirroring resolution's upward walk).
313
+
314
+ `GraphNode["scope"]` widens from `BindingScope` to `BindingScope | "unbound"` for the placeholder nodes.
315
+
316
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`a4377ff`](https://github.com/codefastlabs/codefast/commit/a4377ff1a2afd5c83a865ce38a93a8573582ffb6) Thanks [@thevuong](https://github.com/thevuong)! - **Breaking:** removed `isToken()`. The guard tested for an object carrying a string `name`, which every `Token` has but
317
+ so does anything else — an `InjectionDescriptor` that named its slot passed it, as did any plain `{ name }` object. A
318
+ `Token` is a branded structural type with nothing to check at runtime, so the predicate could not be made sound; it
319
+ narrowed to `Token<unknown>` on evidence that did not support the claim. Nothing in the package used it.
320
+
321
+ Discriminating a declared dependency is what `isInjectionDescriptor()` is for, and it stays. Code that called
322
+ `isToken(x)` to tell a token from a class wants `typeof x === "function"` instead.
323
+
324
+ - [#690](https://github.com/codefastlabs/codefast/pull/690) [`f4b1aa6`](https://github.com/codefastlabs/codefast/commit/f4b1aa6335f535574eae5cf559b81a568f5a7a30) Thanks [@thevuong](https://github.com/thevuong)! - Report the failures that were being swallowed or mislabelled, and derive the types the build emits.
325
+
326
+ `@codefast/di`:
327
+
328
+ - `@inject`, `@postConstruct` and `@preDestroy` on a static member now throw `StaticMemberDecoratorError` instead of
329
+ `InternalError`. All three act on one instance, so this is caller misuse — and `InternalError` means the library
330
+ broke, which sent anyone catching it to file a bug against their own mistake. SPEC §10 already recorded that mistake
331
+ for predicate ambiguity.
332
+ - `AsyncResolutionError` names the token the caller asked for and the token whose factory is async, which is what SPEC
333
+ has always specified. Every throw site passed the same token twice, so the message read "Token 'X' requires async
334
+ resolution because 'X' in its dependency chain has an async factory"; a `resolve(App)` that fails on an async
335
+ `Database` now says so. `asyncSourceToken` defaults to `tokenName` for the case where the requested binding is itself
336
+ the source.
337
+ - A `MetadataReader` that names a `@postConstruct`/`@preDestroy` method the instance does not have raises
338
+ `InvalidMetadataError` instead of skipping the hook — a hook that silently never runs is the failure a caller cannot
339
+ see. `InvalidMetadataError`'s message no longer says "constructor", since it now covers both answers; the specifics
340
+ moved into `reason`.
341
+ - `MissingScopeContextError` from `ScopeManager` names its token instead of `"(unknown)"`, and the scoped read takes one
342
+ map lookup where it took two.
343
+ - `Token`, `Constructor` and `InjectionDescriptor` declare `out Value`, so the compiler checks the covariance the engine
344
+ already relied on.
345
+
346
+ Repo-wide: `isolatedDeclarations` is on for every package that emits declarations, so a public type can always be
347
+ written down from the source file alone. `allowJs` is gone from the shared base config — no package has JavaScript
348
+ sources. `@codefast/theme` and `@codefast/tracking` gained explicit annotations on four exported constants to satisfy
349
+ it; the emitted types are unchanged. `@codefast/ui` and `@codefast/benchmark-viewer` opt out for reasons recorded in
350
+ their configs.
351
+
352
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`50448de`](https://github.com/codefastlabs/codefast/commit/50448defd0c94bffe9b824afef46aa42d80114e2) Thanks [@thevuong](https://github.com/thevuong)! - perf(di)!: freeze slot tags where they are built so snapshots alias instead of copy
353
+
354
+ `BindingSnapshot.slot.tags` is now the binding's own frozen array rather than a fresh copy per snapshot per binding —
355
+ `lookupBindings()`/`inspect()` skip an allocation per binding, reclaiming the cost the defensive copy had added. The
356
+ array is frozen at its two construction sites (the default slot and the builder's re-tag), so the registry stays
357
+ uncorruptible.
358
+
359
+ Breaking: mutating a snapshot's `tags` array — already a type error against `ReadonlyArray` — now throws `TypeError` at
360
+ runtime instead of silently editing a private copy.
361
+
362
+ - [`08a5f2d`](https://github.com/codefastlabs/codefast/commit/08a5f2d6425960d7674b257196962009ab6279dd) Thanks [@thevuong](https://github.com/thevuong)! - Publish every module as an entry point again — the sole-consumer repo prefers full access over encapsulation. The 0.5.0
363
+ surface reduction (13 subpaths) is reverted: `resolution/*`, `registry`, `container/*`, `binding`, `constructor-type`,
364
+ and the `metadata` internals are entry points once more. Introspection modules keep the flat specifiers they have always
365
+ shipped under (`./inspector`, `./dependency-graph`, `./graph-adapters/*`).
366
+
367
+ - [#680](https://github.com/codefastlabs/codefast/pull/680) [`c415c6b`](https://github.com/codefastlabs/codefast/commit/c415c6bd9466421419fd7d97445fb29f76257d95) Thanks [@thevuong](https://github.com/thevuong)! - The dependency graph states its facts as fields instead of hiding them in a display string. `GraphEdge` gains
368
+ `optional: boolean` and `slotName?: string`, so a consumer reads what an edge means rather than parsing `label` (which
369
+ stays, as the string the adapters render). `GraphNode` gains `tokenKey`: two tokens that share a display name are now
370
+ distinguishable, and the same token keeps its key across graphs from the same process — enough to key a view by, which
371
+ `tokenName` never was.
372
+
373
+ `GraphNode["kind"]` is now `BindingKind | "unbound"` instead of a bare `string`, alongside the already-widened `scope`,
374
+ and the React Flow and Cytoscape adapters carry those same unions (plus `tokenKey`, `optional`, `slotName`) instead of
375
+ flattening them to `string` — a consumer can narrow on them now. SPEC.md now documents what the graph does and does not
376
+ represent: unbound optional placeholders, omitted required-but-unbound deps, `injectAll` fan-out, slot-filtered targets,
377
+ unevaluated predicates, and parent shadowing under `includeParent`.
378
+
379
+ - [#688](https://github.com/codefastlabs/codefast/pull/688) [`3112841`](https://github.com/codefastlabs/codefast/commit/31128417f8ac1212c2861df0e1270ba818324e31) Thanks [@thevuong](https://github.com/thevuong)! - `inject()`, `optional()` and `injectAll()` accept the single-tag shorthand. `container.resolve(Token, { tag: pair })`
380
+ has always been valid while `inject(Token, { tag: pair })` was a compile error, so the same request had two vocabularies
381
+ depending on whether you were asking a container or declaring a dependency — and the one a constructor dependency had to
382
+ use was the longer one.
383
+
384
+ `InjectOptions` gains `tag`, and that is the whole surface change. Nothing downstream learns a second spelling: the
385
+ shorthand is folded into `tags` where the descriptor is built, so `InjectionDescriptor`, `ParamMetadata`, the plan
386
+ compiler and the dependency graph keep seeing exactly one tag list. Passing both is a request for every pair across the
387
+ two, which is what the matcher already did with them.
388
+
389
+ While that path was open: an `@inject` accessor was rebuilding its resolve options on **every constructed instance**,
390
+ from the raw options rather than from the descriptor. It now derives them once, from the descriptor — so the accessor
391
+ honours the shorthand for free, and stops allocating per instance.
392
+
393
+ Measured against the previous build on the bind and boot rows, paired and alternating: parity everywhere, controls
394
+ clean.
395
+
396
+ - [#702](https://github.com/codefastlabs/codefast/pull/702) [`22a02b8`](https://github.com/codefastlabs/codefast/commit/22a02b8604e932550474297c8d86fed161385237) Thanks [@thevuong](https://github.com/thevuong)! - What a constructor and a factory are handed is now checked against what they declare.
397
+
398
+ `@injectable([...])` had no relation to the class it decorated. Deps in the wrong order compiled and injected the wrong
399
+ dependency; a deps array one short compiled and handed a parameter `undefined`; `injectAll()` handed an array to a
400
+ parameter declaring one value, and `optional()` handed `undefined` to one that did not admit it. Every case failed
401
+ silently — resolution succeeded, the object was built, and the wrong value was already inside it.
402
+
403
+ The decorator now infers its deps and requires the class to match:
404
+
405
+ ```ts
406
+ @injectable([ConfigToken, LoggerToken]) // Property 'log' is missing in type 'Config'
407
+ class Service {
408
+ constructor(
409
+ readonly logger: Logger,
410
+ readonly config: Config,
411
+ ) {}
412
+ }
413
+ ```
414
+
415
+ `@injectable()` and `@injectable([])` are unchanged — a separate overload keeps the no-dependency form as loose as it
416
+ was, for classes that inject through properties.
417
+
418
+ One mismatch still compiles: a deps array **longer** than the constructor, because a class taking fewer parameters
419
+ satisfies a constructor type taking more. The surplus dependency is resolved and discarded rather than misplaced, which
420
+ makes it the least harmful of the four.
421
+
422
+ Alongside it, a hand-written `InjectionDescriptor` no longer lies to a `toResolved` factory.
423
+ `{ token: Plugin, multi: true }` said `Plugin` and delivered `Array<Plugin>`; `optional: true` said `Plugin` and could
424
+ deliver `undefined`. `ResolvedDependencyValue` reads the flags before the descriptor's own type parameter, so both now
425
+ say what they do. `injectAll()` and `optional()` were already correct and are untouched.
426
+
427
+ The tightening found real looseness in `examples/17-extended-constraints` immediately: thirteen tokens were declared by
428
+ structural shape — `token<{ score(): string }>` — while the constructors receiving them declared the concrete class,
429
+ which has private members and is therefore a different type. Every one of those tokens is bound to exactly that class,
430
+ so they now say so.
431
+
432
+ - [#680](https://github.com/codefastlabs/codefast/pull/680) [`c415c6b`](https://github.com/codefastlabs/codefast/commit/c415c6bd9466421419fd7d97445fb29f76257d95) Thanks [@thevuong](https://github.com/thevuong)! - New `toMermaidGraph` adapter (`@codefast/di/graph-adapters/mermaid`): renders a container graph as Mermaid
433
+ `flowchart TD` source — viewable anywhere Mermaid renders (GitHub markdown, docs tooling, mermaid.live) with no extra
434
+ library. Parent-chain nodes and unbound-optional placeholders carry dashed `classDef`s; `toDotGraph` now also dashes
435
+ unbound placeholders, not just parent nodes.
436
+
437
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`def51b4`](https://github.com/codefastlabs/codefast/commit/def51b4dea15700b8ad7add488247f2d34147f41) Thanks [@thevuong](https://github.com/thevuong)! - perf(di)!: derive resolution-path names from frames instead of carrying a second array
438
+
439
+ Every hop used to push and pop two lockstep arrays — token names for error messages and frames for cycle detection. The
440
+ name array is gone: cycle guards, branch extension, escapes, contexts and the cascade carry only the frame stack, and
441
+ the names an error or `ctx.graph.resolutionPath` reports are derived from the frames at the moment they are asked for.
442
+ Hot lanes pay one push/pop per hop instead of two; only error paths pay the name materialization.
443
+
444
+ Breaking (internal-module surface; the root export is unchanged):
445
+
446
+ - `ResolverCallbacks` and the resolution-path helpers take only the frame stack — `enterResolutionPath(stack, frame)`,
447
+ `extendResolutionBranch(stack, depth, frame)`; `OwnedBranchPath` and `extendResolutionStackBranch` are gone
448
+ (`OwnedBranchStack` is the one brand).
449
+ - `DependencyResolver.rootPath` is gone; the lending protocol reads `rootStack` alone.
450
+ - `ConstraintContext.resolutionPath` is now derived per read from `resolutionStack` — contents are identical, but it is
451
+ no longer the same array object across reads.
452
+
453
+ - [#708](https://github.com/codefastlabs/codefast/pull/708) [`801c749`](https://github.com/codefastlabs/codefast/commit/801c7496ec0d7899c98d94bbbb9677005710f91b) Thanks [@thevuong](https://github.com/thevuong)! - A dependency slot that carries a name or a tag no longer rebuilds its `ResolveOptions` on every hop. The criteria are
454
+ fixed when the slot is declared, so the derived options are too; they are now built once and memoized on the slot
455
+ itself, which is sound to share across containers because they derive from the slot alone. A slot carrying no criterion
456
+ answers from its two fields without calling the builder at all, so the common shape never reaches the memo. A frozen
457
+ slot — which a custom `MetadataReader` may hand out — keeps rebuilding rather than throwing.
458
+
459
+ The memoized object is frozen, because sharing it has a consequence: a constraint predicate is handed it as
460
+ `currentResolveOptions`, and `ResolveOptions` declares mutable fields, so a write through that reference would rewrite
461
+ what the dependency asks for on every later resolve. Frozen, the attempt throws where it is made. Paired A/B over six
462
+ rows and twelve passes puts the freeze inside noise — the control that cannot be affected by it moved as much as the row
463
+ that can.
464
+
465
+ `resolveOptionsForSlot` and the `DependencySlot` type are exported, so the memoizing form is reachable and the slot it
466
+ takes has a name a consumer can write down.
467
+
468
+ This is an allocation change, not a throughput one, and the distinction is worth stating because only one lane ever
469
+ paid. A compiled plan already derives a criteria-carrying param's options at compile time and captures them in its
470
+ escape thunk; the interpreted path had no such moment, so it minted an options object per hop, per resolve. Counted as
471
+ scavenges per 2M resolves under a 1 MB young generation
472
+ (`pnpm --filter @codefast/benchmark-di-inversify instrument:alloc`), a four-named-dependency class whose plan is
473
+ declined went 870 → 442, landing exactly on the criteria-free control's 443, while the compiled lane sat at 1260 on both
474
+ builds. A paired benchmark A/B across all 65 rows reads flat — correctly, since none of them injected a
475
+ criteria-carrying dependency until two rows were added for the lane.
476
+
477
+ - [#690](https://github.com/codefastlabs/codefast/pull/690) [`3bcb204`](https://github.com/codefastlabs/codefast/commit/3bcb2041e6e154b5fbd3a55a75161a614ce96b77) Thanks [@thevuong](https://github.com/thevuong)! - `src/` is reorganised by dependency direction, temperature and lane. **Breaking for deep subpath imports only** — the
478
+ root entry `@codefast/di` and the four `@codefast/di/graph-adapters/*` specifiers are unchanged, and nothing else in
479
+ this repo imported a deep specifier.
480
+
481
+ - **`core/`** now holds the model — `token`, `types`, `constructor-type`, `binding`, `binding-scope`, `registry`,
482
+ `module` — instead of sitting loose beside `index.ts`, so the layering the architecture test enforces is visible in
483
+ the tree rather than only in prose.
484
+ - **`errors/`** separates the taxonomy from its diagnostics. The hot path imports error constructors and nothing else;
485
+ message building belongs behind the throw, which is what the measured cost of a deeper throw site already said.
486
+ - **`injection/`** is new, and it closes a real inversion: `core/binding.ts` and `metadata/metadata-types.ts` imported
487
+ `InjectionDescriptor` from `decorators/inject.ts` — the model depending on a decorator module, which passed the
488
+ layering test only because the imports are type-only. The descriptor, its normalisers and the two pure builders
489
+ (`optional`, `injectAll`) now live at the model layer; `decorators/inject.ts` keeps the one symbol that is actually a
490
+ decorator. `resolve-options` moves alongside, so `DependencySlot` and the descriptor it derives from are in one place.
491
+ - **`ambient/`** takes the module-global active container out of `resolution/environment.ts`, which had been carrying
492
+ three unrelated jobs. The remainder is renamed `resolution/context.ts`, which is what it is.
493
+ - **`lifecycle/`** promotes `LifecycleManager` and `ScopeManager` out of `resolution/`, and
494
+ **`resolution/{cache,path,plan,select}/`** groups the engine's collaborators by the lane each one serves.
495
+
496
+ Renamed specifiers: `./binding`, `./constructor-type`, `./module`, `./registry`, `./token`, `./types` → `./core/*`;
497
+ `./errors` → `./errors/errors`; `./resolution/binding-scope` → `./core/binding-scope`; `./resolution/diagnostics` →
498
+ `./errors/diagnostics`; `./resolution/lifecycle` → `./lifecycle/lifecycle-manager`; `./resolution/scope` →
499
+ `./lifecycle/scope-manager`; `./resolution/resolve-options` → `./injection/resolve-options`;
500
+ `./resolution/{activation-need,binding-lookup-cache,class-introspector}` → `./resolution/cache/*`;
501
+ `./resolution/resolution-path` → `./resolution/path/resolution-path`; `./resolution/instantiation-plan` →
502
+ `./resolution/plan/instantiation-plan`; `./resolution/{binding-select,constraints}` → `./resolution/select/*`;
503
+ `./resolution/environment` → `./resolution/context`.
504
+
505
+ No behaviour changes. Measured as a paired, alternating, per-scenario A/B against the pre-move build over thirteen
506
+ scenarios: every median inside the A/A control's own spread, and the one row that looked down at three passes
507
+ (`fan-out-tree-depth-3-breadth-4`, 0.966) came back at 0.993 over seven passes against an A/A median of 0.991 on the
508
+ same row.
509
+
510
+ `tests/unit/architecture.test.ts` gains a check that ARCHITECTURE.md's backticked `tests/…` citations point at files
511
+ that exist — the existing link check only saw `](src/…)` links, so a moved test file could invalidate a citation
512
+ silently.
513
+
514
+ Three re-declared types are now derived, which is structurally identical and breaks nothing: `buildResolutionFrame`'s
515
+ `slot` parameter is `ResolutionFrame["slot"]` rather than a hand-written shape that had lost both `readonly` modifiers,
516
+ inlined `BindingTag`'s definition and dropped its tuple labels; `injectionSlotToResolveOptions` takes
517
+ `Pick<DependencySlot, "name" | "tags">`; and the descriptor's `tags` no longer carries `any` from
518
+ `PropertyDescriptor.value` into a cast that looked checked.
519
+
520
+ - [#688](https://github.com/codefastlabs/codefast/pull/688) [`c3403a0`](https://github.com/codefastlabs/codefast/commit/c3403a037f2ab7a9e3cdab15d33c1be2eacadcb4) Thanks [@thevuong](https://github.com/thevuong)! - A `resolve` whose tags match several bindings now takes the one declaring the most tags, instead of throwing
521
+ `AmbiguousBindingError`.
522
+
523
+ Tags on a binding are its own conditions, not a filter the request must match exactly, so naming more tags satisfies
524
+ more bindings rather than fewer. Given `whenTagged(Fuel.of("petrol"))` and a specialisation
525
+ `whenTagged(Fuel.of("petrol")).whenTagged(Size.of("v8"))`, a request for `{fuel}` skipped the specialisation — it also
526
+ requires `size` — and a request for `{fuel, size}` satisfied both and was ambiguous. No request reached the
527
+ specialisation at all, so declaring one was pointless.
528
+
529
+ That is the dispatch model, the same one routing, media queries and overload resolution use, and every one of those
530
+ pairs it with a most-specific-wins rule for exactly this reason. This adds the rule that was missing: a candidate
531
+ declaring more tags than every other is the more specific match. `{fuel}` now resolves the general binding and
532
+ `{fuel, size}` the specialisation, which is what both the filter reading and the dispatch reading of tags predict.
533
+
534
+ Selection order is predicate first, then tag count, then throw. Predicate keeps its precedence because it is the older
535
+ rule and re-ordering would re-decide resolutions that already succeed; with this order, every call that resolved before
536
+ resolves to the same binding, and only calls that previously threw can now return. An equal tag count is still genuinely
537
+ ambiguous — `{fuel:petrol}` against `{size:v8}` with both tags requested has no more specific side — and `resolveAll` is
538
+ untouched, since specificity only applies where one binding must be chosen.
539
+
540
+ The new comparison sits on the branch that used to throw, so no successful resolve reaches it. Paired A/B over three
541
+ passes, alternating order, per-scenario isolation: the four rows whose requests reach candidate selection and two
542
+ controls all land in parity, 0.986×–1.040× against a control spread of 0.993×–1.018×.
543
+
544
+ - [#691](https://github.com/codefastlabs/codefast/pull/691) [`02ea054`](https://github.com/codefastlabs/codefast/commit/02ea0542e4c99b5cf0e59c70ac11673aff85dcee) Thanks [@thevuong](https://github.com/thevuong)! - Replace the `[string, unknown]` tag tuple with a `tag()` factory whose criteria are interned.
545
+
546
+ A tag key is declared once and mints its own criteria:
547
+
548
+ ```ts
549
+ const Region = tag<"eu" | "us">("region");
550
+ container.bind(Storage).to(S3).whenTagged(Region.of("eu"));
551
+ container.resolve(Storage, { tag: Region.of("eu") });
552
+ ```
553
+
554
+ The value type is now checked at both ends, so a bind site and a resolve site cannot drift apart — previously the key
555
+ was a bare string and the value was `unknown`, and a typo was a runtime `NoMatchingBindingError` rather than a compile
556
+ error. `whenTagged` takes the criterion instead of `(key, value)`, which makes it the same shape a request carries.
557
+
558
+ This is not a compatible change: `BindingTag` is an interned, branded object rather than a tuple, and nothing outside
559
+ `TagKey.of()` can construct one. `whenTagged`, `whenParentTagged`, `whenAnyAncestorTagged`, `whenParentTaggedAll`,
560
+ `whenAnyAncestorTaggedAll`, `ResolveOptions.tag/tags` and `InjectOptions.tag/tags` all take criteria now.
561
+
562
+ Interning is what pays for it, and it pays twice:
563
+
564
+ - **The registry indexes tagged bindings by the criterion**, not by key-then-value, which removes a hash level and —
565
+ because equal criteria are one object — makes the index exact. The value re-check that existed only to correct a
566
+ `Map`'s SameValueZero treatment of `±0` is gone; the intern cache splits those two under a private symbol instead, so
567
+ `Object.is` (SPEC §3.5) still holds.
568
+ - **The multi-tag lane prefilters on a key mask.** Each key carries a bit, each slot and request the OR of theirs, so a
569
+ slot whose keys the request does not cover is rejected by one AND and one compare before any criterion is read. Bits
570
+ wrap every 32 keys; a shared bit is a false positive identity then rejects, never a false negative.
571
+
572
+ Measured with `di:bench:isolate` against the previous build, `@codefast/di` hz/op:
573
+
574
+ | Row | Before | After |
575
+ | ------------------------------ | -----: | -----------: |
576
+ | `slot-tag-miss-optional` | 13.3M | 19.8M (+48%) |
577
+ | `slot-tag-shorthand-hoisted` | 38.9M | 49.8M (+28%) |
578
+ | `tagged-binding-resolve` | 38.1M | 48.4M (+27%) |
579
+ | `multi-tag-slot-resolve` | 10.9M | 13.8M (+27%) |
580
+ | `slot-tag-resolve-all` | 38.2M | 46.5M (+22%) |
581
+ | `multi-tag-constraint-resolve` | 6.9M | 8.1M (+18%) |
582
+
583
+ Five control rows the change does not touch moved between −3.3% and +3.0%, and the head-to-head aggregate held at 44
584
+ wins / 0 parity / 0 losses against inversify 8.2.3. Three new `mask-*` rows price the prefilter directly: reject-heavy,
585
+ admit-then-decide, and the shared-bit collision.
586
+
587
+ One thing interning did **not** buy: an inline `Region.of(v)` is still slower than a hoisted criterion (+2–3% against
588
+ +25–28%), because `.of()` reads the intern map on every call. The gap between the inline and hoisted rows widened rather
589
+ than closed.
590
+
591
+ ### Patch Changes
592
+
593
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`09e85b8`](https://github.com/codefastlabs/codefast/commit/09e85b87a80143d60c90240ea79de583c0f1ffb2) Thanks [@thevuong](https://github.com/thevuong)! - Fix a batch of correctness bugs found by a full engine audit:
594
+
595
+ - Subclass decorators no longer pollute the base class's metadata: defining a decorated subclass used to make the base
596
+ unresolvable and silently dropped the subclass's own hooks and accessors. Lifecycle and accessor metadata now
597
+ aggregate over the base chain (postConstruct base-first, preDestroy derived-first); constructor metadata stays opt-in
598
+ per class.
599
+ - A dependency cycle through an `@inject` accessor now throws `CircularDependencyError` instead of overflowing the
600
+ stack.
601
+ - A held fluent chain refined after later registry writes can no longer undo an `unbind` or destroy a newer binding; a
602
+ scope refinement evicts the instance cached under the old scope, and a re-slot no longer double-deactivates or re-runs
603
+ the factory of a cached `undefined` singleton.
604
+ - `resolveAll`/`resolveAllAsync` materialize a parent-owned singleton at the parent, so a child override no longer leaks
605
+ into the shared instance and `child.dispose()` no longer destroys it.
606
+ - Concurrent async resolves of one scoped binding construct one instance; a sync resolve during an in-flight async
607
+ singleton materialization refuses with `AsyncResolutionError` instead of silently double-constructing.
608
+ - Container-level activation hooks fire per the binding's owner, matching the SPEC, including through compiled plans.
609
+ - Teardown survives throwing hooks (every remaining hook still runs, failures are reported), drains in-flight async
610
+ materializations, deactivates dependents before dependencies, and a disposed parent no longer serves or
611
+ re-materializes singletons through live children. A module whose setup throws rolls back, so a retry load works.
612
+ - DOT/Mermaid graph adapters escape token names; `has()`/`hasOwn()` answer ambiguity with `true`; binding snapshots no
613
+ longer alias live registry state; `NoMatchingBindingError` survives bigint and circular tag values; the verifying
614
+ metadata reader checks lifecycle and accessor answers; `resolveOptional` evaluates `when()` predicates once
615
+ (measurably faster on the hit path).
616
+
617
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`7a103f0`](https://github.com/codefastlabs/codefast/commit/7a103f05b1306f9889f218753a26bc814fc05de3) Thanks [@thevuong](https://github.com/thevuong)! - Let a child container compile an instantiation plan for a transient class binding its parent owns. The plan compiler
618
+ refuses to decide until a runtime resolve has read the class's lifecycle metadata, but that discovery was recorded on
619
+ the owner's introspector while the compiler consulted the introspector of whichever resolver was doing the resolving.
620
+ Where those differ, the child's answer stayed unknown forever: it recompiled the plan on every single resolve and threw
621
+ the result away each time, falling back to the interpreted path. The resolving side now settles its own answer after
622
+ instantiating a class binding another container owns.
623
+
624
+ Measured against `benchmarks/di-inversify` (fast profile, isolated, best-of-3 per side): median 1.003×, geomean 1.005×
625
+ over 103 scenarios, with no reproducible regression on a path the change touches.
626
+
627
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`27020d1`](https://github.com/codefastlabs/codefast/commit/27020d159652ad71f7afe371e29cda1f17097739) Thanks [@thevuong](https://github.com/thevuong)! - perf(di): make token binding lists copy-on-write so selection drops its snapshot
628
+
629
+ The registry now replaces a token's binding list on `add`/`removeById` instead of splicing it in place, so a selection
630
+ walking the list while a `when()` predicate rebinds the token keeps its own pre-mutation array by construction. The
631
+ defensive per-selection copy in binding selection is gone — predicate-bearing `resolveAll` fan-outs no longer pay an
632
+ allocation per call. Observable behavior is unchanged: the mid-selection-rebind pin test passes as written.
633
+
634
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`7feb085`](https://github.com/codefastlabs/codefast/commit/7feb0853292d347d2ea0e7b57d044226e91ca349) Thanks [@thevuong](https://github.com/thevuong)! - Key the resolution-path cycle guard on binding identity instead of token display names. Two distinct tokens created with
635
+ the same name (say, two `token("Config")` from different modules) on one dependency chain used to throw a false
636
+ `CircularDependencyError` for a legitimately acyclic graph — on the sync lane, on the deep-path membership set, and on
637
+ the async branch lane alike. The guard now compares binding ids read off the resolution frame stack, which moves in
638
+ lockstep with the path; the display-name array is kept solely for the error message, so a real cycle still reports the
639
+ same readable chain.
640
+
641
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`e337290`](https://github.com/codefastlabs/codefast/commit/e3372904e67990ea7f5a91e4f7ae014326b7026a) Thanks [@thevuong](https://github.com/thevuong)! - Warm constants whose only activation handler is container-level during `initializeAsync()`. A `toConstantValue` binding
642
+ carrying a per-binding `onActivation` was already resolved and cached by the warm-up, but one whose hook was registered
643
+ through `container.onActivation(token, …)` was skipped — so the hook first ran on whichever request happened to resolve
644
+ the token, exactly the lazy-init latency `initializeAsync()` exists to remove. The skip now tests both hook channels,
645
+ matching how the resolver's own plain-constant fast path decides the same question.
646
+
647
+ Also tightens `isSyncModule()`, which read a brand field directly and so returned `undefined` rather than `false` for an
648
+ async module despite declaring a boolean type predicate, and corrects the `Promise` type assertion on the parent-owned
649
+ singleton lane of `resolveAsync`.
650
+
651
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`839efbb`](https://github.com/codefastlabs/codefast/commit/839efbb2318d80acca76b8a02846c30f2ca3306c) Thanks [@thevuong](https://github.com/thevuong)! - perf(di): index multi-tag slots by their first criterion for subset selection
652
+
653
+ A name-less multi-tag `resolve` over a wide variant set no longer scans the token's whole binding list: multi-tag slots
654
+ are bucketed under their first criterion, and since a matching slot's every tag is in the request, walking the request's
655
+ buckets (plus the single-tag index) finds each candidate exactly once — no dedup set, no extra per-resolve allocation.
656
+ The lane engages only past a size threshold on the token's list (under it the generic scan is cheaper) and serves
657
+ `resolve` only, so `resolveAll` keeps its result order. Selection semantics — subsets, specificity, predicates,
658
+ ambiguity — are unchanged.
659
+
660
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`5d5fe67`](https://github.com/codefastlabs/codefast/commit/5d5fe67d0d8480314fa6a45b40696a57856cf05f) Thanks [@thevuong](https://github.com/thevuong)! - Trim the `resolveAll` selection path so it fits V8's cumulative inlining budget. The chain from `resolveAll` down to the
661
+ per-candidate resolve summed to roughly 1040 bytes of bytecode against a 920-byte budget, so TurboFan stopped inlining
662
+ partway through it. Three changes cut about 100 of those bytes without changing behaviour: `filterBindings` takes the
663
+ slot-match decision from its caller instead of deriving it, the per-candidate predicate is read once rather than twice,
664
+ and building a non-root `ConstraintContext` moved to its own function so a selection inlines the test and not the object
665
+ literal.
666
+
667
+ Measured against `benchmarks/di-inversify` (default isolated profile, source-swapped, six alternating paired passes,
668
+ median per row): `production-event-bus-dispatch` 1.058×, `resolve-all-strategies-10` 1.045×,
669
+ `resolve-all-strategies-100` 1.034×. `constant-resolve` and `resolve-all-named-64` read slightly under parity, both on
670
+ rows fast enough that the suite's own guidance says not to read them alone; neither is on the path this touches.
671
+
672
+ - [#688](https://github.com/codefastlabs/codefast/pull/688) [`6dcb736`](https://github.com/codefastlabs/codefast/commit/6dcb736a561c527b14b1153a2a4b79d84d28ce79) Thanks [@thevuong](https://github.com/thevuong)! - `resolve(token, { tag: pair })` now reaches the registry's tagged index, which its own documentation had always claimed
673
+ it did. It never had: `singleTagOnlyOf` treated the presence of `tag` as a reason to give up on the fast lane, so the
674
+ shorthand — the form `README` reaches for and `ResolveOptions` advertised as the fast one — was the only spelling
675
+ excluded from it, and fell through to full candidate selection instead. Results were never wrong, only slower. Measured
676
+ paired against the previous build, alternating order, five passes, medians: **2.42×** on a single-tag resolve with the
677
+ pair hoisted and **2.38×** with it written inline, every pass agreeing; `{ tags: [pair] }` and the
678
+ `tagged-binding-resolve` row hold at parity, controls clean.
679
+
680
+ A request that carries a tag from both sources at once still declines the index, because two tags requested is not
681
+ something a one-tag index can answer without skipping the ambiguity check the full path would have run.
682
+
683
+ SPEC §3.5 now states the rule this fixes as normative — a fast path serving `tags: [pair]` must serve `tag: pair` — so
684
+ the two spellings cannot drift into different lanes again, and `tests/unit/resolution/tag-shorthand-parity.test.ts` pins
685
+ both the lane and the result equality across the value kinds `Object.is` and a `Map`'s SameValueZero disagree on.
686
+ `ResolveOptions.tag` and the README also stop implying the two forms differ in speed, and say what actually
687
+ distinguishes them: only `tags` expresses more than one tag, and only `tags` exists on `InjectOptions`.
688
+
689
+ - [#688](https://github.com/codefastlabs/codefast/pull/688) [`bbc111b`](https://github.com/codefastlabs/codefast/commit/bbc111b61611c1e62924503c6be713a96579dca8) Thanks [@thevuong](https://github.com/thevuong)! - `resolveAll(token, { tag })` reads the tagged index instead of scanning every binding under the token. `resolveAll` has
690
+ had a fast lane for a name-only request since the name index existed; the identical shape for a one-tag request was
691
+ missing, for a reason that had expired. `simpleTagOf` kept predicate-bearing bindings out of the tag index, justified by
692
+ the index being "read without a re-check" — which stopped being true when the `±0` fix gave every indexed hit a
693
+ re-check. With the premise gone the exclusion was vestigial, and it was the only thing keeping `resolveAll` off the
694
+ index.
695
+
696
+ Both lanes that read the index now evaluate the predicate on what they find, exactly as the name lane always has, so an
697
+ indexed hit whose `when()` refuses still cannot reach a caller.
698
+
699
+ Worth **1.72×** on a `resolveAll` over a tagged token, nine passes and every one positive, landing that row at the
700
+ throughput the equivalent name lane already had. Single-tag `resolve`, the name lanes, and the sync controls hold.
701
+
702
+ One row moves the other way and is recorded rather than explained: `resolveAll` with no options over a hundred
703
+ pure-predicate bindings measures **0.95×**, nine passes inside a one-percent spread. It has no causal path — that
704
+ request leaves candidate selection at its first test and never reaches the index or any new code — and the obvious
705
+ remedy, keeping the caller its original size, did not move it. It reads as a code-layout effect; the experiment that
706
+ would confirm that has not been run yet.
707
+
708
+ - [#727](https://github.com/codefastlabs/codefast/pull/727) [`bda71f7`](https://github.com/codefastlabs/codefast/commit/bda71f7a2d1121f1abc8d6e575d6779ef5085117) Thanks [@thevuong](https://github.com/thevuong)! - Stop a deep synchronous resolution from reporting a circular dependency that is not there. The membership set the
709
+ resolution path attaches past `RESOLUTION_SET_THRESHOLD` is seeded from the path, and the frames already on it are
710
+ handed no set to delete from on unwind — so the set outlived the resolve it was built for, on an array the resolver
711
+ reuses. A graph resolved on the interpreted path deeper than that threshold answered its first resolve and threw
712
+ `CircularDependencyError` on every later one, and a sibling branch below the attach depth threw within a single resolve.
713
+ The set is now dropped as soon as it stops mirroring the path, and rebuilt by the next frame that needs one.
714
+
3
715
  ## 0.5.0
4
716
 
5
717
  ### Minor Changes