@oasys/oecs 0.4.0 → 0.5.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 (273) hide show
  1. package/CHANGELOG.md +636 -0
  2. package/README.md +47 -32
  3. package/dist/core/ecs/access_check.d.cts +77 -0
  4. package/dist/core/ecs/access_check.d.ts +8 -8
  5. package/dist/core/ecs/access_check.d.ts.map +1 -1
  6. package/dist/core/ecs/archetype.d.cts +486 -0
  7. package/dist/core/ecs/archetype.d.ts +25 -13
  8. package/dist/core/ecs/archetype.d.ts.map +1 -1
  9. package/dist/core/ecs/archetype_graph.d.cts +96 -0
  10. package/dist/core/ecs/archetype_graph.d.ts +96 -0
  11. package/dist/core/ecs/archetype_graph.d.ts.map +1 -0
  12. package/dist/core/ecs/builtin_relations.d.cts +43 -0
  13. package/dist/core/ecs/builtin_relations.d.ts +8 -8
  14. package/dist/core/ecs/builtin_relations.d.ts.map +1 -1
  15. package/dist/core/ecs/command_log.d.cts +116 -0
  16. package/dist/core/ecs/command_log.d.ts +13 -10
  17. package/dist/core/ecs/command_log.d.ts.map +1 -1
  18. package/dist/core/ecs/component.d.cts +201 -0
  19. package/dist/core/ecs/component.d.ts +100 -5
  20. package/dist/core/ecs/component.d.ts.map +1 -1
  21. package/dist/core/ecs/compute_backend.d.cts +37 -0
  22. package/dist/core/ecs/compute_backend.d.ts +2 -2
  23. package/dist/core/ecs/debug_names.d.cts +7 -0
  24. package/dist/core/ecs/debug_names.d.ts +7 -0
  25. package/dist/core/ecs/debug_names.d.ts.map +1 -0
  26. package/dist/core/ecs/deferred_commands.d.cts +55 -0
  27. package/dist/core/ecs/deferred_commands.d.ts +55 -0
  28. package/dist/core/ecs/deferred_commands.d.ts.map +1 -0
  29. package/dist/core/ecs/dispatch_trace.d.cts +93 -0
  30. package/dist/core/ecs/dispatch_trace.d.ts +3 -3
  31. package/dist/core/ecs/ecs.d.cts +580 -0
  32. package/dist/core/ecs/ecs.d.ts +340 -293
  33. package/dist/core/ecs/ecs.d.ts.map +1 -1
  34. package/dist/core/ecs/ecs_memory.d.cts +179 -0
  35. package/dist/core/ecs/ecs_memory.d.ts +1 -1
  36. package/dist/core/ecs/entity.d.cts +34 -0
  37. package/dist/core/ecs/entity.d.ts +8 -2
  38. package/dist/core/ecs/entity.d.ts.map +1 -1
  39. package/dist/core/ecs/entity_allocator.d.cts +59 -0
  40. package/dist/core/ecs/entity_allocator.d.ts +59 -0
  41. package/dist/core/ecs/entity_allocator.d.ts.map +1 -0
  42. package/dist/core/ecs/event.d.cts +79 -0
  43. package/dist/core/ecs/event.d.ts +36 -11
  44. package/dist/core/ecs/event.d.ts.map +1 -1
  45. package/dist/core/ecs/event_registry.d.cts +22 -0
  46. package/dist/core/ecs/event_registry.d.ts +22 -0
  47. package/dist/core/ecs/event_registry.d.ts.map +1 -0
  48. package/dist/core/ecs/facades.d.cts +158 -0
  49. package/dist/core/ecs/facades.d.ts +158 -0
  50. package/dist/core/ecs/facades.d.ts.map +1 -0
  51. package/dist/core/ecs/frame_stepper.d.cts +58 -0
  52. package/dist/core/ecs/frame_stepper.d.ts +58 -0
  53. package/dist/core/ecs/frame_stepper.d.ts.map +1 -0
  54. package/dist/core/ecs/frame_trace.d.cts +133 -0
  55. package/dist/core/ecs/frame_trace.d.ts +5 -5
  56. package/dist/core/ecs/frame_trace.d.ts.map +1 -1
  57. package/dist/core/ecs/host_commands.d.cts +284 -0
  58. package/dist/core/ecs/host_commands.d.ts +77 -45
  59. package/dist/core/ecs/host_commands.d.ts.map +1 -1
  60. package/dist/core/ecs/index.d.cts +46 -0
  61. package/dist/core/ecs/index.d.ts +45 -40
  62. package/dist/core/ecs/index.d.ts.map +1 -1
  63. package/dist/core/ecs/observer.d.cts +173 -0
  64. package/dist/core/ecs/observer.d.ts +44 -13
  65. package/dist/core/ecs/observer.d.ts.map +1 -1
  66. package/dist/core/ecs/query.d.cts +734 -0
  67. package/dist/core/ecs/query.d.ts +267 -90
  68. package/dist/core/ecs/query.d.ts.map +1 -1
  69. package/dist/core/ecs/ref.d.cts +31 -0
  70. package/dist/core/ecs/ref.d.ts +3 -3
  71. package/dist/core/ecs/ref.d.ts.map +1 -1
  72. package/dist/core/ecs/relation.d.cts +243 -0
  73. package/dist/core/ecs/relation.d.ts +26 -14
  74. package/dist/core/ecs/relation.d.ts.map +1 -1
  75. package/dist/core/ecs/relation_service.d.cts +235 -0
  76. package/dist/core/ecs/relation_service.d.ts +235 -0
  77. package/dist/core/ecs/relation_service.d.ts.map +1 -0
  78. package/dist/core/ecs/resource.d.cts +53 -0
  79. package/dist/core/ecs/resource.d.ts +25 -5
  80. package/dist/core/ecs/resource.d.ts.map +1 -1
  81. package/dist/core/ecs/resource_registry.d.cts +20 -0
  82. package/dist/core/ecs/resource_registry.d.ts +20 -0
  83. package/dist/core/ecs/resource_registry.d.ts.map +1 -0
  84. package/dist/core/ecs/resume.d.cts +85 -0
  85. package/dist/core/ecs/resume.d.ts +6 -6
  86. package/dist/core/ecs/resume.d.ts.map +1 -1
  87. package/dist/core/ecs/run_condition.d.cts +81 -0
  88. package/dist/core/ecs/run_condition.d.ts +16 -10
  89. package/dist/core/ecs/run_condition.d.ts.map +1 -1
  90. package/dist/core/ecs/schedule.d.cts +142 -0
  91. package/dist/core/ecs/schedule.d.ts +15 -6
  92. package/dist/core/ecs/schedule.d.ts.map +1 -1
  93. package/dist/core/ecs/snapshot_service.d.cts +94 -0
  94. package/dist/core/ecs/snapshot_service.d.ts +94 -0
  95. package/dist/core/ecs/snapshot_service.d.ts.map +1 -0
  96. package/dist/core/ecs/sparse_store.d.cts +110 -0
  97. package/dist/core/ecs/sparse_store.d.ts +5 -2
  98. package/dist/core/ecs/sparse_store.d.ts.map +1 -1
  99. package/dist/core/ecs/store.d.cts +937 -0
  100. package/dist/core/ecs/store.d.ts +144 -356
  101. package/dist/core/ecs/store.d.ts.map +1 -1
  102. package/dist/core/ecs/store_layout_listener.d.cts +23 -0
  103. package/dist/core/ecs/system.d.cts +250 -0
  104. package/dist/core/ecs/system.d.ts +138 -22
  105. package/dist/core/ecs/system.d.ts.map +1 -1
  106. package/dist/{utils/arrays.d.ts → core/ecs/utils/arrays.d.cts} +1 -0
  107. package/dist/core/ecs/utils/error.d.cts +61 -0
  108. package/dist/core/ecs/utils/error.d.ts +12 -2
  109. package/dist/core/ecs/utils/error.d.ts.map +1 -1
  110. package/dist/core/store/action_ring.d.cts +136 -0
  111. package/dist/core/store/allocator.d.cts +238 -0
  112. package/dist/core/store/allocator.d.ts.map +1 -1
  113. package/dist/core/store/buffer_backed_column.d.cts +69 -0
  114. package/dist/core/store/buffer_backed_column.d.ts +2 -2
  115. package/dist/core/store/column_store.d.cts +273 -0
  116. package/dist/core/store/column_store.d.ts +18 -10
  117. package/dist/core/store/column_store.d.ts.map +1 -1
  118. package/dist/core/store/command_dispatch.d.cts +52 -0
  119. package/dist/core/store/command_ring.d.cts +107 -0
  120. package/dist/core/store/descriptor.d.cts +80 -0
  121. package/dist/core/store/descriptor.d.ts +1 -1
  122. package/dist/core/store/descriptor.d.ts.map +1 -1
  123. package/dist/core/store/entity_index.d.cts +108 -0
  124. package/dist/core/store/event_ring.d.cts +95 -0
  125. package/dist/core/store/extend.d.cts +39 -0
  126. package/dist/core/store/extend.d.ts +3 -73
  127. package/dist/core/store/extend.d.ts.map +1 -1
  128. package/dist/core/store/grow.d.cts +34 -0
  129. package/dist/core/store/grow.d.ts +4 -9
  130. package/dist/core/store/grow.d.ts.map +1 -1
  131. package/dist/core/store/header.d.cts +64 -0
  132. package/dist/core/store/header.d.ts +1 -1
  133. package/dist/core/store/header.d.ts.map +1 -1
  134. package/dist/core/store/index.d.cts +17 -0
  135. package/dist/core/store/index.d.ts +16 -15
  136. package/dist/core/store/index.d.ts.map +1 -1
  137. package/dist/core/store/layout_ops.d.cts +152 -0
  138. package/dist/core/store/layout_ops.d.ts +152 -0
  139. package/dist/core/store/layout_ops.d.ts.map +1 -0
  140. package/dist/core/store/region_table.d.cts +74 -0
  141. package/dist/core/store/region_table.d.ts +1 -1
  142. package/dist/core/store/region_table.d.ts.map +1 -1
  143. package/dist/core/store/snapshot.d.cts +43 -0
  144. package/dist/core/store/snapshot.d.ts +2 -2
  145. package/dist/core/store/state_hash.d.cts +38 -0
  146. package/dist/core/store/state_hash.d.ts +1 -1
  147. package/dist/core/store/store_regions.d.cts +38 -0
  148. package/dist/core/store/store_regions.d.ts +1 -1
  149. package/dist/core/store/vendored_abi/abi.d.ts +43 -0
  150. package/dist/core/store/{__generated__ → vendored_abi}/abi.d.ts.map +1 -1
  151. package/dist/dev_flag.d.cts +2 -0
  152. package/dist/dev_flag.d.ts +2 -0
  153. package/dist/dev_flag.d.ts.map +1 -0
  154. package/dist/extensions/editor/editor.d.cts +172 -0
  155. package/dist/extensions/editor/editor.d.ts +65 -42
  156. package/dist/extensions/editor/editor.d.ts.map +1 -1
  157. package/dist/extensions/editor/field_handle.d.cts +40 -0
  158. package/dist/extensions/editor/field_handle.d.ts +10 -5
  159. package/dist/extensions/editor/field_handle.d.ts.map +1 -1
  160. package/dist/extensions/editor/index.cjs +1 -1
  161. package/dist/extensions/editor/index.d.cts +21 -0
  162. package/dist/extensions/editor/index.d.ts +2 -2
  163. package/dist/extensions/editor/index.js +130 -100
  164. package/dist/extensions/reactive/ecs_sync.d.cts +214 -0
  165. package/dist/extensions/reactive/ecs_sync.d.ts +26 -22
  166. package/dist/extensions/reactive/ecs_sync.d.ts.map +1 -1
  167. package/dist/extensions/reactive/index.cjs +1 -1
  168. package/dist/extensions/reactive/index.d.cts +23 -0
  169. package/dist/extensions/reactive/index.d.ts +1 -1
  170. package/dist/extensions/reactive/index.js +106 -116
  171. package/dist/extensions/solid/index.cjs +1 -1
  172. package/dist/extensions/solid/index.d.cts +6 -0
  173. package/dist/extensions/solid/index.d.ts +2 -2
  174. package/dist/extensions/solid/index.js +8 -4
  175. package/dist/extensions/solid/kernel_solid.d.cts +45 -0
  176. package/dist/extensions/solid/kernel_solid.d.ts +6 -3
  177. package/dist/extensions/solid/kernel_solid.d.ts.map +1 -1
  178. package/dist/host_commands-BI8pEmjH.js +2326 -0
  179. package/dist/host_commands-CxhpzMx9.cjs +1 -0
  180. package/dist/index.cjs +1 -2
  181. package/dist/index.d.cts +66 -0
  182. package/dist/index.d.ts +50 -1
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.js +4489 -5898
  185. package/dist/internal.cjs +2 -0
  186. package/dist/internal.d.cts +20 -0
  187. package/dist/internal.d.ts +20 -0
  188. package/dist/internal.d.ts.map +1 -0
  189. package/dist/internal.js +141 -0
  190. package/dist/primitives.cjs +1 -1
  191. package/dist/primitives.d.cts +18 -0
  192. package/dist/primitives.d.ts +6 -6
  193. package/dist/primitives.js +24 -23
  194. package/dist/reactive/array.d.ts +24 -0
  195. package/dist/reactive/array.d.ts.map +1 -0
  196. package/dist/reactive/index.cjs +1 -0
  197. package/dist/reactive/index.d.cts +11 -0
  198. package/dist/reactive/index.d.ts +11 -0
  199. package/dist/reactive/index.d.ts.map +1 -0
  200. package/dist/reactive/index.js +18 -0
  201. package/dist/reactive/interop.d.cts +19 -0
  202. package/dist/{core/reactive → reactive}/interop.d.ts +1 -1
  203. package/dist/reactive/interop.d.ts.map +1 -0
  204. package/dist/reactive/kernel.d.cts +73 -0
  205. package/dist/{core/reactive → reactive}/kernel.d.ts +5 -3
  206. package/dist/reactive/kernel.d.ts.map +1 -0
  207. package/dist/reactive/map.d.ts +16 -0
  208. package/dist/reactive/map.d.ts.map +1 -0
  209. package/dist/reactive/shallow.d.cts +11 -0
  210. package/dist/reactive/shallow.d.ts +11 -0
  211. package/dist/reactive/shallow.d.ts.map +1 -0
  212. package/dist/reactive/struct.d.cts +10 -0
  213. package/dist/{core/reactive → reactive}/struct.d.ts +1 -1
  214. package/dist/reactive/struct.d.ts.map +1 -0
  215. package/dist/shallow-DQ8aG51_.cjs +1 -0
  216. package/dist/shallow-DfnAm9vG.js +150 -0
  217. package/dist/shared-BU1Cd40h.js +103 -0
  218. package/dist/shared-BymrGTyR.cjs +1 -0
  219. package/dist/shared.cjs +1 -1
  220. package/dist/shared.d.cts +22 -0
  221. package/dist/shared.d.ts +2 -2
  222. package/dist/shared.js +1 -1
  223. package/dist/topological_sort-BDvEyb9W.cjs +1 -0
  224. package/dist/topological_sort-DK6EjpWa.js +135 -0
  225. package/dist/type_primitives/assertions.d.cts +32 -0
  226. package/dist/type_primitives/assertions.d.ts +13 -1
  227. package/dist/type_primitives/assertions.d.ts.map +1 -1
  228. package/dist/type_primitives/binary_heap/binary_heap.d.cts +37 -0
  229. package/dist/type_primitives/bitset/bitset.d.cts +54 -0
  230. package/dist/type_primitives/brand.d.cts +23 -0
  231. package/dist/type_primitives/error.d.cts +16 -0
  232. package/dist/type_primitives/error.d.ts +4 -3
  233. package/dist/type_primitives/error.d.ts.map +1 -1
  234. package/dist/type_primitives/index.d.cts +13 -0
  235. package/dist/type_primitives/index.d.ts +9 -9
  236. package/dist/type_primitives/sparse_map/sparse_map.d.cts +30 -0
  237. package/dist/type_primitives/sparse_set/sparse_set.d.cts +28 -0
  238. package/dist/type_primitives/topological_sort/topological_sort.d.cts +29 -0
  239. package/dist/type_primitives/typed_arrays/typed_arrays.d.cts +127 -0
  240. package/dist/typed_arrays-BcXcnzB4.js +258 -0
  241. package/dist/typed_arrays-D3NmwtCG.cjs +1 -0
  242. package/dist/utils/error.d.cts +6 -0
  243. package/dist/version.d.cts +10 -0
  244. package/dist/version.d.ts +10 -0
  245. package/dist/version.d.ts.map +1 -0
  246. package/package.json +102 -19
  247. package/dist/array-CxbPyiHO.cjs +0 -1
  248. package/dist/array-uFR7Q8fU.js +0 -132
  249. package/dist/core/reactive/array.d.ts.map +0 -1
  250. package/dist/core/reactive/index.cjs +0 -1
  251. package/dist/core/reactive/index.d.ts +0 -10
  252. package/dist/core/reactive/index.d.ts.map +0 -1
  253. package/dist/core/reactive/index.js +0 -17
  254. package/dist/core/reactive/interop.d.ts.map +0 -1
  255. package/dist/core/reactive/kernel.d.ts.map +0 -1
  256. package/dist/core/reactive/map.d.ts.map +0 -1
  257. package/dist/core/reactive/struct.d.ts.map +0 -1
  258. package/dist/log/console_sink.d.ts +0 -4
  259. package/dist/log/console_sink.d.ts.map +0 -1
  260. package/dist/log/index.d.ts +0 -3
  261. package/dist/log/index.d.ts.map +0 -1
  262. package/dist/log/logger.d.ts +0 -27
  263. package/dist/log/logger.d.ts.map +0 -1
  264. package/dist/shared-BXSZnxx4.cjs +0 -1
  265. package/dist/shared-C678TAPY.js +0 -99
  266. package/dist/topological_sort-DlRpSrxu.js +0 -391
  267. package/dist/topological_sort-WAT-VHb-.cjs +0 -1
  268. package/dist/utils/arrays.d.ts.map +0 -1
  269. package/dist/utils/constants.d.ts.map +0 -1
  270. /package/dist/{utils/constants.d.ts → core/ecs/utils/constants.d.cts} +0 -0
  271. /package/dist/core/store/{__generated__/abi.d.ts → vendored_abi/abi.d.cts} +0 -0
  272. /package/dist/{core/reactive/array.d.ts → reactive/array.d.cts} +0 -0
  273. /package/dist/{core/reactive/map.d.ts → reactive/map.d.cts} +0 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,636 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.5.1] — 2026-07-06
9
+
10
+ ### Changed (breaking) — one attach grammar
11
+
12
+ `addComponents` and `template` now take the same callable-bundle varargs as `spawnBundle`
13
+ and `ctx.commands.spawn` / `add`, replacing the `{ def, values }[]` entry-object array — one
14
+ grammar across every authoring surface:
15
+
16
+ ```ts
17
+ // before
18
+ ecs.addComponents(e, [{ def: Pos, values: { x, y } }, { def: Vel, values: { vx } }]);
19
+ const Bullet = ecs.template([{ def: Pos, values: { x: 0, y: 0 } }]);
20
+ // after
21
+ ecs.addComponents(e, Pos({ x, y }), Vel({ vx }));
22
+ const Bullet = ecs.template(Pos({ x: 0, y: 0 }));
23
+ ```
24
+
25
+ To migrate: drop the array brackets, wrap a valued entry in its def's call
26
+ (`{ def: X, values: V }` → `X(V)`), and leave a bare entry bare (`{ def: X }` → `X`).
27
+
28
+ - Each item is schema-checked against its **own** def via the `StrictBundles` mapped tuple
29
+ (`{ [K in keyof Items]: … }`) — a misspelled or cross-component field, including a
30
+ hand-written raw `{ def, values }` literal, is a compile error. `spawnBundle` gains this
31
+ per-item checking (it previously had none).
32
+ - `ctx.commands.spawn` / `ctx.commands.add` now schema-check their bundle values in
33
+ declared-access systems as well (the `DeclaredBundleOrDef` type distributes over the
34
+ declared add set); a permissive / `exclusive` context stays loose, as before.
35
+ - The `TemplateEntry` / `TemplateEntries` public types are removed (they encoded the retired
36
+ entry-object grammar). The host command seam (`HostCommandQueue.spawn` — the record/replay
37
+ and editor-undo transport) deliberately keeps its entry-object + complete-values shape.
38
+
39
+ ### Changed — API vocabulary consistency
40
+
41
+ Cheap alignments from a public-API vocabulary audit that followed the grammar unification:
42
+
43
+ - `removeComponents(e, ...defs)` is now varargs, mirroring `addComponents` (was
44
+ `removeComponents(e, defs[])`).
45
+ - `HostCommandQueue.pending()` is now a `pending` getter (matching every other count accessor).
46
+ - `ReadonlyEntityIdArray` → `ReadonlyEntityIDArray` (acronym casing, matching `EntityID`).
47
+ - The entity-id parameter is now uniformly `entityId` across the core surface (ECS lifecycle,
48
+ host-command queue, `ObserverFn`); the `HostCommand` wire-format field stays `eid`.
49
+ - Source-compatible widenings: `ecs.despawn`, `ecs.removeSystem`, and the `HostCommandQueue`
50
+ mutators now return `this` for chaining.
51
+
52
+ The `ref` / `refRead` argument order was reviewed and **deliberately kept** def-first
53
+ (`ctx.ref(def, entityId)`): these are the outside-iteration members of the `cols.mut` /
54
+ `cols.read` column-cursor family, so def-first is the cursor convention, not an inconsistency
55
+ to fix — flipping it would align with `getField` while breaking alignment with `cols.mut`.
56
+ Documented as such (`refs.md`, `queries.md`) rather than flipped.
57
+
58
+ ### Changed (breaking) — host-write-seam verb grammar
59
+
60
+ The host-write-seam handles are namespaced command buffers, so they drop the component noun
61
+ to match `ctx.commands.add` / `remove` — and their own already-bare
62
+ `spawn`/`despawn`/`disable`/`enable`/`setField`:
63
+
64
+ - `HostCommandQueue.addComponent` / `removeComponent` → `add` / `remove`.
65
+ - `Editor` and `TransactionBuilder` `.addComponent` / `.removeComponent` → `add` / `remove`
66
+ (the two surfaces are designed to match, so they move together).
67
+ - The editor extension's entity-id parameters and its `FieldReader` type now read `entityId`,
68
+ completing the core's `eid` → `entityId` pass.
69
+
70
+ The wire-format `kind` discriminants (`"add_component"` / `"remove_component"`), the ring
71
+ codecs, and the `HostCommand` record's `eid` field are unchanged — transport vocabulary.
72
+
73
+ ### Changed (breaking) — `ctx.getResource`
74
+
75
+ The in-system resource getter is now `ctx.getResource(key)` (was the verb-less `ctx.resource(key)`),
76
+ matching its flat-surface siblings `setResource` / `removeResource` / `hasResource` and the
77
+ `getField` / `setField` / `hasComponent` convention. The rule is now explicit: the flat `ctx`
78
+ surface verbs every accessor; the grouped `ecs.resources` facade drops the noun (`get` / `set` /
79
+ `remove` / `has`) because its receiver already names it. `ConditionContext` (run-condition
80
+ predicates) moves in lockstep.
81
+
82
+ ### Fixed
83
+
84
+ - The immediate host spawn family (`spawn` / `spawnBundle` / `spawnMany`) now throws in DEV
85
+ when called from inside a system body — redirecting to `ctx.commands.spawn` — like every
86
+ other immediate host structural mutator. Previously it was silently unguarded (the archetype
87
+ iteration guard does not cover the append path), a live mid-iteration footgun; its guard
88
+ docstring's "one rule for every host mutator" claim is now true.
89
+ - Added explicit public `QueryCache` cache-map type annotations so JSR publish passes
90
+ slow-type validation and can generate package declarations cleanly.
91
+
92
+ ## [0.5.0] — 2026-07-06
93
+
94
+ ### Changed (breaking) — lifecycle & naming unification
95
+
96
+ One vocabulary across host, commands, and access declarations; the receiver now implies the
97
+ timing (host = immediate, `ctx.commands` = deferred). Hard renames, no deprecation aliases —
98
+ see [docs/MIGRATION-0.4-to-0.5.md](docs/MIGRATION-0.4-to-0.5.md) for the complete
99
+ rename/removal map:
100
+
101
+ | 0.4 | 0.5 |
102
+ | --- | --- |
103
+ | `ecs.createEntity()` / `ecs.createEntity(template, overrides?)` | `ecs.spawn()` / `ecs.spawn(template, overrides?)` |
104
+ | `ecs.createEntities(template, count)` | `ecs.spawnMany(template, count, overrides?)` |
105
+ | `ecs.destroyEntity(e)` *(deferred)* | `ecs.despawn(e)` — **now immediate** |
106
+ | `ctx.createEntity()` | `ctx.commands.spawn()` |
107
+ | `ctx.destroyEntity(e)` | `ctx.commands.despawn(e)` |
108
+ | `ctx.addComponent(e, def, values?)` | `ctx.commands.add(e, def, values)` or `ctx.commands.add(e, def({ … }))` |
109
+ | `ctx.removeComponent(e, def)` | `ctx.commands.remove(e, def)` |
110
+ | `ctx.disable(e)` / `ctx.enable(e)` | `ctx.commands.disable(e)` / `ctx.commands.enable(e)` |
111
+ | `sourcesOf(def, tgt)` | `sourcesOf(tgt, def)` — matches `targetOf` / `targetsOf` |
112
+ | `query.count()` | `query.entityCount` (getter, beside `archetypeCount`) |
113
+ | `WorldRestoreError` / `WORLD_SNAPSHOT_VERSION` | `ECSRestoreError` / `ECS_SNAPSHOT_VERSION` |
114
+
115
+ - **Host `despawn` is immediate** — `ecs.despawn(e); ecs.isAlive(e)` is `false` on the next
116
+ line, closing the audit's M1 finding (host `addComponent` immediate but destroy buffered).
117
+ **Observer note:** like every immediate op, host `despawn` fires no *structural* observers —
118
+ `onRemove` no longer sees host-despawned entities (it did at 0.4, when host destroy was
119
+ deferred). Observer-driven consumers, including the `reactive-sync` map bridges, only see
120
+ despawns that go through `ctx.commands.despawn` or the host-command seam. (`onSet` is
121
+ receiver-blind — derived change detection sees host `setField` writes as always.)
122
+ - **Every immediate host structural mutator throws in dev when called from inside a system
123
+ body** — `despawn`, `addComponent`/`addComponents`, `removeComponent`/`removeComponents`,
124
+ `batchAddComponent`/`batchRemoveComponent`, `disable`/`enable` — each error pointing at its
125
+ `ctx.commands` equivalent. Mid-system these ops can move rows a running query is walking and
126
+ are invisible to observers; previously only `despawn` was guarded wholesale (the others were
127
+ caught only when they touched the archetype being iterated). Cross-world host mutation from
128
+ another world's system (#785) is unaffected — the guard is scoped to the mutated world.
129
+ - **The bare deferred duplicates on `ctx` are removed** — `ctx.addComponent`,
130
+ `ctx.removeComponent`, `ctx.disable`, `ctx.enable` join the already-removed
131
+ `ctx.createEntity` / `ctx.destroyEntity`. `ctx.commands` is now the *only* deferred surface,
132
+ completing the receiver-implies-timing rule with zero exceptions. `ctx.commands.add` gains
133
+ the explicit complete-values shape (`ctx.commands.add(e, Pos, { x: 0, y: 0 })`) the removed
134
+ `ctx.addComponent` carried, so compile-checked complete attaches survive the move.
135
+ `ctx.isDisabled` stays (immediate read), as do the immediate sparse/relation ops.
136
+ - **`sourcesOf` canonicalized to `(entity, def)`** on `ecs.relations` and `SystemContext` —
137
+ it was the one arg-order outlier on the relation surface (M3).
138
+ - **The package root is now a curated, explicit export list** — `export *` no longer flattens the
139
+ whole core barrel, so future barrel additions cannot silently widen the public API. A checked-in
140
+ public-API snapshot test makes any surface change an explicit diff in review.
141
+ - **Internal/tooling symbols moved to `@oasys/oecs/internal`** (explicitly **unstable — no semver
142
+ guarantees**): the packed-EntityID codec (`createEntityId`, `getEntityGeneration`, `MAX_INDEX`,
143
+ `MAX_GENERATION`, `MAX_LIVE_GENERATION`, `RETIRED_GENERATION`, `MAX_ENTITY_ID`), the SAB
144
+ command-ring transport (`HostCommandDispatcher`, `ring*Codec`, `HOST_COMMAND_PAYLOAD_BYTES`),
145
+ memory-sizing internals (`resolveECSMemory`, `DEFAULT_ECS_CAP_BYTES`, `BUDGET_*`), and the
146
+ dev-mode singletons (`accessCheck`, `dispatchTrace`). `getEntityIndex` stays at the root.
147
+
148
+ ### Added
149
+
150
+ - **`addComponent` bundle overload** — `ecs.addComponent(e, Pos({ x: 1 }))` accepts a bundle
151
+ with the usual zero-fill semantics (M2); the explicit `(e, def, values)` form stays
152
+ complete-values, so a typo'd or missing field is still a compile error.
153
+ - **`spawnMany` typed template + shared overrides** — bulk spawn takes the same typed
154
+ `Template<Defs>` as `spawn` plus one optional `TemplateOverrides<Defs>` object applied to
155
+ every row (contiguous batches use one `fill` per overridden column).
156
+ - **JSDoc `@example` on the core surface** — `registerComponent`, `spawn`, `addComponent`,
157
+ `query`, `registerSystem`, `startup`, `update`, `ctx.emit` / `ctx.read`,
158
+ `events.register`, `resources.register` now carry hover-visible examples (M23).
159
+ - **Component debug names** — `registerComponent(schema, { name: "Pos" })` (and the sparse
160
+ sibling) records a diagnostic label, so access-violation and liveness errors read
161
+ `'Pos' (component 5)` instead of leaving you to count registration order
162
+ (`ComponentRegisterOptions`).
163
+ - **Total probes + `tryGetField`** — `hasComponent` / `hasSparse` / `relations.has` now return
164
+ `false` for a dead entity instead of dev-throwing (a "has" probe is exactly the call made to
165
+ avoid dead entities); `ecs.tryGetField(e, def, field)` returns `undefined` for a dead entity or
166
+ missing component, and `ctx.tryGetField` mirrors it inside systems (declared-read checked).
167
+ - **Plural host mutators chain** — `addComponents`, `removeComponents`, `batchAddComponent`,
168
+ `batchRemoveComponent` return `this` (previously `void`), matching their singular siblings.
169
+ - **`Query.firstEntity()` / `Query.singleEntity()`** — singleton reads (player, camera) without a
170
+ hand-rolled `forEach` + capture; `singleEntity` dev-throws `QUERY_NOT_SINGLETON` on 0 or >1.
171
+ - **Host-side `ecs.refRead(def, e)`** — whole-component read-only view, parity with
172
+ `ctx.refRead`.
173
+ - **Run-condition combinators** — `not()` / `allOf()` / `anyOf()`, merging the operands' declared
174
+ read surfaces.
175
+ - **Editor change notification** — `editor.onChange(cb)` (fires on commit/undo/redo/clear) plus
176
+ `canUndo` / `canRedo` getters; no more per-frame `depths()` polling.
177
+ - **`using` support** — `ObserverHandle` implements `Symbol.dispose`.
178
+ - **Write-seam lifecycle** — `uninstallHostCommandSeam(world, queue)`,
179
+ `HostCommandQueue.clear()`, `HostCommandDispatcher.off(opCode)`,
180
+ `HostCommandRecorder.snapshotLog()` (stable deep copy).
181
+ - **`VERSION`** export and a `"./package.json"` export; `engines: { node: ">=20" }` and a README
182
+ runtime note (resizable `ArrayBuffer`).
183
+ - Root re-exports so failure modes are nameable without extra entry points:
184
+ `StoreRestoreError`, `SabUnavailableError`, `TypedArrayTag`; `/reactive` now exports `Eq` and
185
+ `shallow` (moved from `/reactive-sync`, which re-exports for compat); `signal()` gains the
186
+ zero-arg Solid-parity overload; `SingletonSyncOptions.eq`.
187
+ - **`FrameStepper`** — optional host-side driver over the authoritative `ecs.update(dt)`:
188
+ `play()`/`pause()`/`toggle()` on `requestAnimationFrame` (injectable `requestFrame`/`cancelFrame`
189
+ for tests and non-browser hosts), explicit `step()`/`stepFrames()` for debuggers, editors, and
190
+ rollback playback, and a `maxDt` clamp (default 0.25 s) so a resumed background tab doesn't feed
191
+ the whole suspension into the accumulator as one delta. Validation throws `INVALID_FRAME_STEP`.
192
+ - **`ObserverConfig.name`** — diagnostic label surfaced as the frame trace's
193
+ `observer_fired.observer` field (the role a system's `name` plays); observe-only, never affects
194
+ `stateHash` or dispatch order. Unnamed observers fall back to `observer(<component debug name>)`
195
+ when the component was registered with a name, else `observer(<cid>)`.
196
+ - **`ECSOptions.onWarn`** — injectable sink for dev-mode engine diagnostics (currently the
197
+ schedule's dropped-ordering-edge warning and the `ECSOptions` unknown-key warning),
198
+ defaulting to `console.warn`. Replaces the internal `src/log` singleton, which is deleted.
199
+ - **Editor `fieldHandle` `read` thunk is optional** — defaults to `Editor.committedField`.
200
+
201
+ ### Fixed
202
+
203
+ - **Host iteration guard (`STRUCTURAL_DURING_ITERATION`)** — with host `despawn` now immediate,
204
+ a host-side `forEach`/`eachChunk` callback that despawned (or transitioned/toggled) an entity of
205
+ the archetype it was visiting silently skipped entities via the row swap-remove. Row-removing
206
+ ops on an archetype a live dense walk is standing in now throw in dev, *before* any mutation
207
+ lands (the transition path checks ahead of the destination append, so no dual-residency
208
+ half-state). Collect ids during the walk and mutate after it. Mutating archetypes the walk is
209
+ *not* currently visiting stays legal — the #431 fresh-snapshot machinery still covers those.
210
+ - **Cross-world despawn false positive** — `worldB.despawn(e)` from inside world A's system no
211
+ longer trips the in-system despawn guard (the accessCheck span is process-global; the guard now
212
+ also requires *this* world to be mid-schedule). Driving a second world from a system (#785)
213
+ mutates it host-style, which is safe — B is not iterating. Unnamed systems in the guard message
214
+ now render as `system_<id>` instead of `'?'`.
215
+ - **Frame trace records every deferred command (ADR-0030)** — the removed bare `ctx.*` deferred
216
+ forms bypassed the `commandQueued` trace hook, so host-command-seam adds/removes/toggles (and
217
+ any system using the bare forms) were invisible to an attached `FrameTraceSink` while their
218
+ spawns/despawns were visible. With `ctx.commands` as the only deferred surface every queued
219
+ command is traced, and `ctx.commands.spawn` now also traces each bundle attach it queues
220
+ (previously only the spawn itself).
221
+ - **Stale deferred-attach docs** — `host_commands.ts` / the host-write-seam page claimed the
222
+ deferred add path does not zero-fill omitted fields (NaN readback); every attach path
223
+ zero-fills since #716 (`writeFields`'s `?? 0`). The complete-values requirement on
224
+ `SpawnEntry` is documented as what it is — explicit intent in a reified, replayable record —
225
+ and the observer docs now scope "immediate ops fire no observers" to *structural* observers
226
+ (`onSet` is derived change detection and sees host `setField` writes).
227
+ - **`ecs.refRead` / `ctx.ref` / `ctx.refRead` on a missing component or tag def** — threw a raw
228
+ `TypeError` from the ref internals; now a dev `ECSError` (`COMPONENT_NOT_REGISTERED`) naming the
229
+ op and component, matching `getField`. Host `refRead`'s docstring now states the single-
230
+ expression lifetime rule (any immediate structural mutation can row-swap under a held ref).
231
+ - **Editor: aborted transactions no longer poison undo** — `transaction(tx => …)` staged its
232
+ `setField` shadow writes into the editor's shared map at build time, so a build callback that
233
+ threw left phantom pending values behind and seeded the *next* edit's undo inverse with a value
234
+ the world never held. Staging is now transaction-local and merges only on commit.
235
+ - **Editor: `pendingField` self-resolves for dead slots** — a shadow entry for a despawned entity
236
+ (or removed component) echoed its stale value forever and leaked; the reconcile-on-read now
237
+ prunes it and returns `undefined`.
238
+ - **JSR/Deno consumers no longer break on the `__DEV__` global** — shipped source now reads a
239
+ guarded `DEV` flag (`src/dev_flag.ts`) that constant-folds in the npm bundle and defaults to
240
+ dev-on for raw-source consumers (`globalThis.__DEV__ = false` opts out).
241
+ - **Error experience** — every `ENTITY_NOT_ALIVE` names the operation and decodes the packed id
242
+ (index + generation, with context); system access violations use the new `ACCESS_UNDECLARED`
243
+ category instead of overloading `*_NOT_REGISTERED`; resource/event "not registered" messages
244
+ name the key and hint the registration call; messages no longer reference pre-0.4 snake_case
245
+ option names or private tracker issue numbers.
246
+ - **Packaging** — per-entry `.d.cts` and explicit-extension declaration specifiers
247
+ (`attw --pack` fully green: node10/node16/bundler across all eight entry points, was
248
+ masquerading + resolution errors); `typesVersions` for `moduleResolution: node10` subpaths; npm
249
+ tarball ships `CHANGELOG.md`; `@internal` editor internals no longer leak into published types.
250
+ - **Type-level closures** — `EventShape<S>` homomorphic bound (interface-declared event schemas
251
+ now accepted); `RelationOptions` is a union so `{ exclusive: true, multi: true }` is a compile
252
+ error; `ResourceKey`'s phantom is a unique symbol (no `.__phantom` in autocomplete);
253
+ `pairsOf` / `sourcesOfAny` return readonly tuples; `SystemConfig.fn` optional when
254
+ `backendHandle` is present.
255
+ - Dev-mode diagnostics: ownerless `computed()` / `onCleanup()` warn (kernel); ECSOptions warns on
256
+ unknown keys; `runIfResourceEq` warns on object-valued `expected` (reference-identity `===`);
257
+ `runEveryNTicks` validation throws `ECSError` (`INVALID_RUN_CONDITION`).
258
+ - **Docs standardized on the `ecs` receiver** — README, GETTING_STARTED, BEST_PRACTICES, the
259
+ api reference, and every in-source JSDoc example now spell `const ecs = new ECS()`
260
+ (M22; with the `World*` names renamed to `ECS*`, "world" survives only as prose). The
261
+ host-write-seam docs now explain *why* `queue.spawn` takes complete-value `spawnEntry`s
262
+ rather than zero-filling bundles (M4: commands are a reified, replayable record — complete
263
+ values are explicit intent legible to replay, not a correctness need; the deferred add path
264
+ zero-fills omitted fields since #716).
265
+ - **JSR publish no longer ships `__tests__` helper files** (`casing_codemod.ts`,
266
+ `test_helpers.ts` — including a `node:fs` import subject to JSR type-checking).
267
+
268
+ ### Changed (breaking) — type-level & facade surface
269
+
270
+ - **Compile-time typestate across the system, query, relation, and key seams.** The config-form
271
+ `registerSystem` now infers your access declarations as literal types and hands `fn`/`onAdded` a
272
+ `SystemContext<DeclaredAccess<…>>` narrowed to exactly the declared surface — undeclared access
273
+ is a compile error naming the missing declaration, with the dev-mode runtime check remaining as
274
+ the backstop for dynamic values. Query columns are typed by the query's terms
275
+ (`ChunkColumns<Defs>` / `ArchetypeView<Defs>`; `.and(...)` extends the term set), relation
276
+ handles carry their cardinality (`RelationDef<"exclusive">` vs `RelationDef<"multi">` — the
277
+ exclusive-only traversal surfaces reject a multi handle at compile time), and
278
+ `ResourceKey`/`EventKey`/`EventDef` are invariant so a key can no longer widen through
279
+ `unknown`. A checked-in type battery (`typing_assertions.ts`) pins every rule.
280
+ - **Grouped facades: `ecs.relations`, `ecs.events`, `ecs.resources`, `ecs.snapshots`.** Cohesive
281
+ secondary surfaces move off the flat namespace onto narrow typed facades —
282
+ `ecs.relations.add(child, ChildOf, parent)`, `ecs.events.emit(Damage, {...})`,
283
+ `ecs.resources.get(Time)`, `ecs.snapshots.capture()`. The facades mirror the typestate
284
+ surface exactly (cardinality-stamped `relations.register`, exclusive-only traversal). Hot-path
285
+ API (component ops, queries, spawn/destroy, sparse ops) stays flat by design. Facade classes
286
+ are exported type-only; the runtime export list is unchanged.
287
+ - **Value arguments are schema-checked at compile time across every attach seam.** Tag defs
288
+ reject value objects (`Frozen({ x: 1 })` no longer compiles — tags carry no data);
289
+ `addComponents` takes schema-checked entries (`TemplateEntries<Defs>`), so a misspelled or
290
+ cross-component field key is a compile error instead of a silent zero-fill; host-seam
291
+ `queue.spawn` entries (`SpawnEntries<Defs>`) are checked complete against each def's own
292
+ schema (`ValuesArg` / `CompleteFieldValues` exported); and `events.register` requires the
293
+ field list to cover the event schema (`EventFieldsCover`) — a partial list silently dropped
294
+ columns and read back `undefined` at runtime. Smaller closures in the same vein: `observe`
295
+ accepts any `ComponentHandle`, `NoInfer` pins key-typed value params (`events.emit`,
296
+ resources), and reactive-sync's `JoinReader.field` is constrained to the join's component
297
+ set.
298
+
299
+ ### Removed (breaking)
300
+
301
+ - The 29 flat forms the new facades replace (`registerRelation`/`addRelation`/`targetOf`/…,
302
+ `registerEvent`/`registerSignal`/`emit`/`read`, `registerResource`/`resource`/`setResource`/
303
+ `removeResource`/`hasResource`, `snapshot`/`restoreInto`/`snapshotSparse`/`restoreSparse`/
304
+ `stateHash`/`deterministic`, `relationCount`/`compactRelations`). Each maps 1:1 onto its
305
+ grouped replacement — `ecs.relations.add(...)`, `ecs.events.emit(...)`, `ecs.resources.get(...)`,
306
+ `ecs.snapshots.capture()` (was `snapshot()`) / `ecs.snapshots.restore(...)` (was
307
+ `restoreInto(...)`), `ecs.relations.count` (was `relationCount`), `ecs.relations.compact()`
308
+ (was `compactRelations()`). System-side `ctx.*` and all `Store`-level methods are unchanged.
309
+
310
+ ### Changed (internal)
311
+
312
+ - **`Store` decomposed into seven focused collaborators** (RelationService, EventRegistry +
313
+ ResourceRegistry, EntityAllocator, DeferredCommandBuffer, SnapshotService, ArchetypeGraph) with
314
+ `Store` as the coordinator; the hot-path extractions were A/B-benchmarked against
315
+ identical-code controls with no regression. The `ECS` facade's pure delegations now live in a
316
+ marker-delimited pass-through band whose logic-free invariant is enforced by an AST guard test.
317
+ - Typed per-consumer host seams (`ObserverHost`, `QueryHost`) replace underscore-convention
318
+ reach-through on `Store`; `QueryCache` now owns all 12 query-resolution cache maps.
319
+ - Store layer consolidation: one strategy-parameterized factory behind
320
+ `growableSabAllocator` / `heapArraybufferAllocator`; a typed `isColumnStoreInternal` guard
321
+ replaces six structural casts; grow/extend's ~200 duplicated lines moved to a shared
322
+ `layout_ops.ts` (bit-identical layouts pinned by a golden differential test across the
323
+ full allocator matrix).
324
+ - `core/reactive` moved to `src/reactive` (the published `./reactive` subpath is unchanged);
325
+ `__generated__/abi.ts` renamed to `vendored_abi/abi.ts` (it is a hand-maintained snapshot,
326
+ not generated output).
327
+ - Deleted orphaned duplicate `src/utils/{arrays,constants}.ts`; renamed the custom `TypeError`
328
+ (shadowed the ECMAScript global) to `AssertionError`; retired the 246-line casing codemod +
329
+ guard test (the 0.4 rename has converged).
330
+
331
+ ## [0.4.0] — 2026-06-24
332
+
333
+ Major release. oecs is **re-derived from the upstream oasys engine ECS** — its modern descendant — and
334
+ gains whole subsystems while staying pure-TS and zero-dependency by default. The public API moves to
335
+ the engine's surface, so **every consumer touches breaking changes** — chiefly a global
336
+ `snake_case` → `camelCase` rename. See [docs/MIGRATION-0.3-to-0.4.md](docs/MIGRATION-0.3-to-0.4.md).
337
+
338
+ ### Changed (breaking)
339
+
340
+ - **The entire public API is now `camelCase`.** Every method, property, parameter, and field renamed
341
+ from `snake_case` (`create_entity` → `createEntity`, `add_component` → `addComponent`, `get_field` →
342
+ `getField`, `is_alive` → `isAlive`, `register_system` → `registerSystem`, …). Types and handles stay
343
+ PascalCase and SCREAMING_SNAKE constants are unchanged. A `vitest` casing guard prevents regressions.
344
+ - **Renamed query/context verbs.** `QueryBuilder.every` → `with`; `query.not` → `without`;
345
+ `query.any_of` → `anyOf`; `query.for_each` → `forEach`; `archetype.get_column` → `getColumnRead`;
346
+ `event_key` / `signal_key` / `resource_key` → `eventKey` / `signalKey` / `resourceKey`;
347
+ `is_ecs_error` → `isEcsError`; `destroy_entity_deferred` → `destroyEntity` (still deferred).
348
+ - **Ref mutability flipped on the unsuffixed name.** `ctx.ref` is now the **mutable** default (was
349
+ read-only in 0.3); the read-only variant is `ctx.refRead` (was `ctx.ref_mut` for the mutable one).
350
+ Same rule for columns: mutable `getColumn` (internal) vs read-only `getColumnRead`.
351
+ - **`WorldOptions` → `ECSOptions`; `fixed_timestep` → `fixedTimestep`.**
352
+ - **`initial_capacity` removed** — replaced by the `memory` surface (`memory: { budget }` /
353
+ `{ maxBytes }` / `{ columnCapacity }` pin / `{ shared }` / `{ wasm }` / `{ allocator }`). Passing the
354
+ old option keys throws at construction, pointing at `memory`.
355
+ - **Component-touching systems must declare `reads` / `writes`.** A new `__DEV__` access checker
356
+ (tree-shaken from production) validates every column / ref / field / resource access against a
357
+ system's declared surface. The bare `(ctx, dt)` and `(q, ctx, dt)` + query-builder `registerSystem`
358
+ overloads declare no access, so a system that touches ECS data through them throws in dev — move it
359
+ to the config form (`registerSystem({ reads, writes, fn })`). `exclusive: true` systems bypass the
360
+ checker. A registration-time lint (`QUERY_ACCESS_UNDECLARED`) additionally checks any declared
361
+ `queries ⊆ reads ∪ writes`.
362
+ - **`removeComponents` takes an array, not varargs** (`removeComponents(e, [A, B])`);
363
+ `batchAddComponent` / `batchRemoveComponent` key on `ArchetypeID` instead of an `Archetype` object.
364
+ - **Event schema shape.** `eventKey`'s type parameter is now a field → value-type record
365
+ (`eventKey<{ target: EntityID; amount: number }>("Damage")`) rather than a tuple of field names, so
366
+ branded fields round-trip through `emit` / `read`. `registerEvent(key, [...fieldNames])` unchanged
367
+ otherwise.
368
+
369
+ ### Added
370
+
371
+ - **Two storage profiles over one backing-neutral `ColumnStore`.** Default is pure-TS **heap** (a plain
372
+ resizable `ArrayBuffer`) — no `SharedArrayBuffer`, no cross-origin isolation. Opt-in
373
+ `@oasys/oecs/shared` (`memory: { shared: {} }`) uses a `SharedArrayBuffer` for worker offload / a WASM
374
+ compute backend. Same code path; identical state hash.
375
+ - **Determinism** (opt-in `deterministic: true`): a state hash over column bytes + `snapshot()` /
376
+ `restoreInto()` (and `snapshotSparse` / `restoreSparse`), **backing-agnostic** — a heap world and a
377
+ shared world with identical history agree. `WorldRestoreError` / `SparseRestoreError` fail closed
378
+ before overwriting live backing.
379
+ - **Observers** — `world.observe(def, { onAdd, onRemove, onSet, onDisable, onEnable })`, structural +
380
+ per-entity.
381
+ - **Relations** — `(relation, target)` pairs, `ChildOf` / `IsA` presets (`registerChildOf` /
382
+ `registerIsA`), `(R,*)` / `(*,T)` wildcard queries (`withRelation`, `forEachRelatedTo`,
383
+ `ANY_RELATION`), hierarchy queries (`query.hierarchy`), traversal (`ancestorsOf` / `rootOf` /
384
+ `cascadeOf`), and on-delete cleanup policies.
385
+ - **Sparse component storage** (`registerSparseComponent` / `addSparse` / `query.withSparse`),
386
+ **run conditions / system sets** (`systemSet` + `configureSet`; `runIfResourceEq` / `runEveryNTicks`
387
+ / `runIfAnyMatch`), **entity enable/disable** (row-partitioned; `disable` / `enable` /
388
+ `includeDisabled`), and **templates** (`world.template([...])` + `createEntity(template, overrides)`
389
+ / `createEntities(template, count)` for zero-transition spawns).
390
+ - **Typed host→ECS write seam** — `installHostCommandSeam(world)` + `applyHostCommand` + a
391
+ `HostCommandQueue` drained by a blessed `exclusive` apply system; a cross-thread ring transport
392
+ (`HostCommandDispatcher`); record/replay (`HostCommandRecorder`, `replayCommandLog`,
393
+ `serializeCommandLog`); and an undo/redo + field-handle layer at `@oasys/oecs/editor`.
394
+ - **Frame trace** — `world.setTrace(sink)` + `FrameTraceRecorder` emit a structured per-frame event
395
+ stream (`__DEV__`-gated). **Compute backend seam** — `world.attachBackend(backend)` runs a system's
396
+ body on a compiled backend instead of its TS closure.
397
+ - **Reactive UI seam (optional):** zero-dependency kernel at `@oasys/oecs/reactive`; ECS→reactive
398
+ bridge at `@oasys/oecs/reactive-sync` (publish-only-dirty, O(changed)); SolidJS adapter at
399
+ `@oasys/oecs/solid` with `solid-js` as an **optional** peer dependency.
400
+ - **`memory` sizing surface** on the constructor: `budget` (by expected `entities`) / `maxBytes` /
401
+ `columnCapacity` / `shared` / `wasm` / `allocator` arms; `resolveECSMemory(...)` exported to inspect
402
+ what an intent resolves to.
403
+ - **Hot-path iteration ergonomics:**
404
+ - **`query.eachChunk((cols, count) => …)`** — the mutable per-archetype iterator. `cols.mut(def)` /
405
+ `cols.read(def)` resolve a whole component's field columns at once into a destructurable group
406
+ (`const { x, y } = cols.mut(Pos)`), stamping the change tick once inside `mut` and handing back
407
+ `count` (= `entityCount`). The only mutable column accessor reachable through iteration (the
408
+ `ArchetypeView` from `forEach` stays read-only). Honours `includeDisabled()`; dense-only like `forEach`.
409
+ - **`ctx.commands`** — a Bevy-`Commands`-style facade namespacing the **deferred** structural ops
410
+ (`spawn` / `add` / `remove` / `despawn` / `disable` / `enable`), unambiguously deferred vs the
411
+ immediate `world.addComponent`.
412
+ - **Callable bundles** — `bundle(def, values)` pairs a def with field values (omitted fields
413
+ zero-fill); `world.spawnBundle(...)` (immediate) and `ctx.commands.spawn` / `.add` (deferred)
414
+ accept a `bundle(...)` or a bare def (tag / all-zero), unifying the attach shapes.
415
+ - **`ctx.updateField` / `ctx.markChanged`**, and optional-component queries (`query.optional(...)` +
416
+ `getOptionalColumnRead`).
417
+ - **Composable change-detection queries** — `query.changed(...)` returns a `ChangedQuery` that now
418
+ mirrors the dense query verbs (`and` / `without` / `anyOf` / `optional`), so
419
+ `q.changed(Pos).without(Dead)` works (refining *after* `changed()`, previously a dead end).
420
+ - **New public exports** — entity-ID codec (`createEntityId` / `getEntityIndex` / `getEntityGeneration`
421
+ + `MAX_*` bounds) for snapshot/replication decode; the error taxonomy (`ECSError`, `ECS_ERROR`,
422
+ `isEcsError`) for catch-and-branch; and `@oasys/oecs/primitives` (`BitSet`, `SparseSet`, `SparseMap`,
423
+ growable typed arrays, `BinaryHeap`, `topologicalSort`).
424
+
425
+ ### Packaging
426
+
427
+ - **Multi-entry build** → `dist/` emits ESM + CJS + `.d.ts` for every subpath (`.`, `/primitives`,
428
+ `/shared`, `/reactive`, `/reactive-sync`, `/editor`, `/solid`); `sideEffects:false` + tree-shaking
429
+ keep core consumers from pulling SAB / Solid. `solid-js` is an optional peer dependency. `jsr.json`
430
+ exports updated.
431
+
432
+ ## [0.3.3] — 2026-04-30
433
+
434
+ Release-process and packaging hygiene. No runtime changes.
435
+
436
+ ### Changed
437
+
438
+ - **JSR bundle slimmed.** `.github/` and `docs/` are now excluded from the published JSR package. Consumers download less; build/CI artefacts stay on GitHub.
439
+ - **Tag-driven publish workflow.** `.github/workflows/publish.yml` now triggers on `v*` tag pushes instead of every push to `main`, and creates a GitHub Release alongside the JSR publish. Cuts a release by tagging.
440
+
441
+ ## [0.3.2] — 2026-04-30
442
+
443
+ Documentation-only release. No runtime changes.
444
+
445
+ ### Added
446
+
447
+ - **Module overview on `src/index.ts`.** A `@module` block now renders as the JSR Overview tab.
448
+ - **JSDoc on the full public surface.** `ECS` and its public methods, `Query` / `QueryBuilder` / `SystemContext` / `ChangedQuery`, all type aliases and interfaces, the event/resource key minters, and the `SCHEDULE` phases are now documented in-source.
449
+ - **`@internal` tags on internal-but-public TS members** (e.g. `_resolve_query`, `Query._include`, `SystemContext.store`) so JSR hides them from the rendered docs.
450
+
451
+ ## [0.3.1] — 2026-04-23
452
+
453
+ Performance-only patch release. Two targeted allocation-elimination changes on hot paths; no API changes; full 466-test suite unchanged.
454
+
455
+ ### Performance
456
+
457
+ - **Cache multi-component transition maps on `Archetype`.** `add_components` / `remove_components` on already-populated entities previously allocated a fresh `Int16Array` per call via `build_transition_map`. A per-archetype `batch_transition_maps: Map<ArchetypeID, Int16Array>` now caches the map on first use. Single-component paths unchanged. Measured: **+12–15%** throughput on `add_components` (already-populated) at 10k / 100k / 1M; **−35–42%** peak heap and **−49–61%** peak RSS on the same workload. ([#9](https://github.com/oasys-works/oecs/pull/9))
458
+ - **Per-Query composition cache for single-component composition shapes.** `q.and(X)`, `q.not(X)`, `q.any_of(X)`, and `q.changed(X)` previously allocated a BitSet copy, a defs slice (and, for `.changed`, a new `ChangedQuery`) on every call, even though the resolver already cached the resulting `Query` object. Single-component calls now short-circuit through a per-parent-`Query` Map and skip the allocation path entirely. Multi-component compositions fall through unchanged. Measured: **~6×** throughput on a 4-shape compose loop at 10k / 100k / 1M; **−40–56%** peak heap and **essentially zero RSS growth** during the workload. ([#10](https://github.com/oasys-works/oecs/pull/10))
459
+
460
+ ## [0.3.0] — 2026-04-21
461
+
462
+ A substantial release focused on change detection, stricter component-access
463
+ typing, and a simpler key-based API for events and resources. Several public
464
+ entry points change shape; see the migration notes under *Breaking changes*.
465
+
466
+ ### Added
467
+
468
+ #### Change detection
469
+
470
+ - Frame-based tick counter on the world. `ECS` now holds a `_tick` that
471
+ advances once per `update()`. Systems can see it via `ctx.world_tick`,
472
+ and each `SystemContext` receives `last_run_tick` — the tick at which that
473
+ system last executed.
474
+ - Per-component change ticks on archetypes. Each archetype tracks
475
+ `_changed_tick[component_id]` — the tick at which any entity in that
476
+ archetype last had the component mutated. Maintained automatically by
477
+ `write_fields`, `write_fields_positional`, `copy_shared_from`,
478
+ `move_entity_from`, and `bulk_move_all_from`, all of which now accept a
479
+ `tick` parameter.
480
+ - `ChangedQuery<Defs>` — a new query variant, produced by `query.changed(...)`,
481
+ that restricts iteration to archetypes whose tracked components were
482
+ modified after `last_run_tick`. Validates at construction that the named
483
+ components are part of the parent query's include set.
484
+
485
+ #### Readonly component views
486
+
487
+ - `ReadonlyColumn<T>` and `ReadonlyUint32Array` — compile-time readonly views
488
+ of typed-array columns. Returned by `archetype.get_column()` and the new
489
+ `archetype.entity_ids` getter. Prevents accidental indexed writes at the
490
+ type level; zero runtime cost.
491
+ - `ReadonlyComponentRef<S>` — readonly variant of `ComponentRef`. Returned by
492
+ `query.ref(...)`. Use it when you only need to read component fields.
493
+ - `archetype.get_column_mut(def, field, tick)` — explicit mutable column
494
+ accessor. Writes through `get_column_mut` update `_changed_tick`.
495
+ - `query.ref_mut(...)` — mutable sibling of `ref()`. Returns a `ComponentRef`
496
+ and records the component as changed for the current tick.
497
+
498
+ #### Key-based Event API
499
+
500
+ - `EventKey<F>` — symbol-typed key that carries the event's field schema as
501
+ a phantom type.
502
+ - `event_key<F>(name)` / `signal_key(name)` — factories for module-scope
503
+ event keys. `signal_key` is a convenience wrapper for zero-field events.
504
+
505
+ #### Key-based Resource API
506
+
507
+ - `ResourceKey<T>` — symbol-typed key carrying the resource's value type as
508
+ a phantom type.
509
+ - `resource_key<T>(name)` — factory for module-scope resource keys.
510
+ - `world.has_resource(key)` — existence check.
511
+ - Resources are now plain key→value storage. `world.resource(key)` returns
512
+ the stored `T` directly.
513
+
514
+ #### Errors
515
+
516
+ - New `ECS_ERROR` categories: `RESOURCE_ALREADY_REGISTERED`,
517
+ `EVENT_ALREADY_REGISTERED`, `EVENT_NOT_REGISTERED`.
518
+ - New `TYPE_ERROR` category: `ASSERTION_FAIL_NON_NULLABLE`, emitted by the
519
+ new `assert_non_null` helper.
520
+
521
+ #### Assertions
522
+
523
+ - `assert_non_null<T>(value, message?)` in `type_primitives/assertions` —
524
+ dev-only (`__DEV__` guarded) assertion that narrows `T` to `NonNullable<T>`
525
+ and throws a `TypeError` with contextual info on failure.
526
+
527
+ #### New primitives
528
+
529
+ - `BinaryHeap<T>` in `type_primitives/binary_heap` — generic array-backed
530
+ heap with a user-supplied comparator. `push`, `pop`, `peek`, `clear`,
531
+ `size`. O(log n) push/pop, O(1) peek.
532
+ - `topological_sort<T>(nodes, edges, tiebreaker, node_name?)` in
533
+ `type_primitives/topological_sort` — Kahn's algorithm with a
534
+ `BinaryHeap`-backed ready queue for deterministic tie-breaking. Throws
535
+ `TypeError` on cycles; the schedule layer re-wraps as
536
+ `ECSError(CIRCULAR_SYSTEM_DEPENDENCY)`.
537
+
538
+ #### Public exports
539
+
540
+ - `SystemFn`, `ReadonlyComponentRef`, `ChangedQuery`, `ReadonlyColumn`,
541
+ `ReadonlyUint32Array`, `EventKey`, `event_key`, `signal_key`,
542
+ `ResourceKey`, `resource_key` are now part of the package surface.
543
+
544
+ ### Changed
545
+
546
+ - Query iteration is callback-based. `Query` no longer implements
547
+ `[Symbol.iterator]`. Iterate with `query.for_each((archetype) => { ... })`.
548
+ - `world.register_event`, `world.register_signal`, and `world.register_resource`
549
+ return `void` and take an `EventKey` / `ResourceKey` as their first argument.
550
+ - `world.emit`, `world.read`, `world.resource`, and `world.set_resource`
551
+ accept keys instead of definition objects. `world.resource(key)` returns
552
+ the typed value `T` directly rather than a field-reader wrapper.
553
+ - Schedule execution methods take a tick. `run_startup(label, tick)`,
554
+ `run_update(label, tick)`, and `run_fixed_update(label, tick)` require
555
+ the current frame tick. `ECS.update()` wires this automatically.
556
+ - System ordering now uses the shared `topological_sort` primitive. Observable
557
+ behavior is unchanged: `before`/`after` constraints respected,
558
+ `insertion_order` remains the tie-breaker, cycles surface as
559
+ `ECSError(CIRCULAR_SYSTEM_DEPENDENCY)`.
560
+ - Store/query wiring. The store keeps a reference to each active `Query` via
561
+ `update_query_ref` and calls `mark_non_empty_dirty` only when structural
562
+ changes occur, avoiding spurious query rebuilds on stable frames.
563
+ - Bit-manipulation and hash constants (`BITS_PER_WORD`, `BITS_PER_WORD_SHIFT`,
564
+ `BITS_PER_WORD_MASK`, `FNV_OFFSET_BASIS`, `FNV_PRIME`) are exported from
565
+ `type_primitives/bitset` rather than `utils/constants`.
566
+ - Growable-array defaults (`DEFAULT_INITIAL_CAPACITY`, `GROWTH_FACTOR`) are
567
+ exported from `type_primitives/typed_arrays`.
568
+
569
+ ### Fixed
570
+
571
+ - Query dirty propagation. `flush_destroyed` now marks affected queries dirty
572
+ so subsequent iteration sees the correct archetype set. `flush_structural`
573
+ skips dirty marking when no changes occurred.
574
+ - `set_field` on the world goes through `get_column_mut` with the current
575
+ tick, so mutations via the high-level API are visible to `ChangedQuery`.
576
+
577
+ ### Removed
578
+
579
+ - `ResourceChannel`, `ResourceDef<F>`, `ResourceReader<F>`, `ResourceID`,
580
+ `as_resource_id`, and the `__resource_schema` marker symbol — the entire
581
+ SoA column-based resource storage layer. Resources are now key→value.
582
+ - `RESOURCE_ROW` constant — unused.
583
+ - `EventDef<F>` — replaced by `EventKey<F>`.
584
+
585
+ ### Breaking changes
586
+
587
+ 1. **Event definitions.** Define a key at module scope, then register and use it:
588
+ ```ts
589
+ // before
590
+ const damage = world.register_event({ amount: "u32" } as const);
591
+ world.emit(damage, { amount: 5 });
592
+
593
+ // after
594
+ const DAMAGE = event_key<{ amount: "u32" }>("damage");
595
+ world.register_event(DAMAGE, { amount: "u32" } as const);
596
+ world.emit(DAMAGE, { amount: 5 });
597
+ ```
598
+
599
+ 2. **Resource registration / access.**
600
+ ```ts
601
+ // before
602
+ const clock = world.register_resource({ ms: "u32" } as const, { ms: 0 });
603
+ const ms = world.resource(clock).ms;
604
+
605
+ // after
606
+ const CLOCK = resource_key<{ ms: number }>("clock");
607
+ world.register_resource(CLOCK, { ms: 0 });
608
+ const ms = world.resource(CLOCK).ms;
609
+ ```
610
+ `world.resource()` returns the stored value directly; the reader wrapper
611
+ and the SoA column storage are gone.
612
+
613
+ 3. **Query iteration.**
614
+ ```ts
615
+ // before
616
+ for (const arch of query) { ... }
617
+
618
+ // after
619
+ query.for_each((arch) => { ... });
620
+ ```
621
+
622
+ 4. **Mutable vs readonly refs.** `query.ref(...)` now returns
623
+ `ReadonlyComponentRef`. Switch to `query.ref_mut(...)` when writing —
624
+ this is also what enables change detection for that component.
625
+
626
+ 5. **Archetype column access.** `archetype.get_column(...)` returns a
627
+ `ReadonlyColumn`. Use `archetype.get_column_mut(def, field, tick)` for
628
+ direct writes. Most callers should use `query.ref_mut` and won't notice.
629
+
630
+ 6. **Schedule driver signatures.** If you drive the scheduler directly
631
+ (bypassing `ECS.update()`), `run_startup`, `run_update`, and
632
+ `run_fixed_update` now require a `tick: number` argument.
633
+
634
+ ## [0.2.1] and earlier
635
+
636
+ Prior releases — see git history.