@forgeax/engine-ecs 0.0.0-dev.8d955ade1c79

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 (383) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +278 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__fixtures__/field-shape-fixture.d.ts +16 -0
  5. package/dist/__fixtures__/field-shape-fixture.d.ts.map +1 -0
  6. package/dist/__tests__/__fixtures__/inspect-scripts.snapshot.d.ts +3 -0
  7. package/dist/__tests__/__fixtures__/inspect-scripts.snapshot.d.ts.map +1 -0
  8. package/dist/__tests__/archetype.unit.test.d.ts +2 -0
  9. package/dist/__tests__/archetype.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/array-handle-element.test-d.d.ts +2 -0
  11. package/dist/__tests__/array-handle-element.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/buffer-array-vocab.test-d.d.ts +2 -0
  13. package/dist/__tests__/buffer-array-vocab.test-d.d.ts.map +1 -0
  14. package/dist/__tests__/buffer-field-input-widen.unit.test.d.ts +2 -0
  15. package/dist/__tests__/buffer-field-input-widen.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/buffer-pool.test-d.d.ts +2 -0
  17. package/dist/__tests__/buffer-pool.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/change-detection.unit.test.d.ts +2 -0
  19. package/dist/__tests__/change-detection.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/command-buffer.test.d.ts +2 -0
  21. package/dist/__tests__/command-buffer.test.d.ts.map +1 -0
  22. package/dist/__tests__/commands-deferred.unit.test.d.ts +2 -0
  23. package/dist/__tests__/commands-deferred.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/component-catalog.test.d.ts +2 -0
  25. package/dist/__tests__/component-catalog.test.d.ts.map +1 -0
  26. package/dist/__tests__/component-definition-identity.unit.test.d.ts +2 -0
  27. package/dist/__tests__/component-definition-identity.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/component-nan-preflight.unit.test.d.ts +2 -0
  29. package/dist/__tests__/component-nan-preflight.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/component-schema.test-d.d.ts +2 -0
  31. package/dist/__tests__/component-schema.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/component-schema.test.d.ts +2 -0
  33. package/dist/__tests__/component-schema.test.d.ts.map +1 -0
  34. package/dist/__tests__/define-component-options.test-d.d.ts +2 -0
  35. package/dist/__tests__/define-component-options.test-d.d.ts.map +1 -0
  36. package/dist/__tests__/deprecated-vocab-rejection.unit.test.d.ts +2 -0
  37. package/dist/__tests__/deprecated-vocab-rejection.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/despawn-all.unit.test.d.ts +2 -0
  39. package/dist/__tests__/despawn-all.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/ecs-core-reduction.characterization.test.d.ts +2 -0
  41. package/dist/__tests__/ecs-core-reduction.characterization.test.d.ts.map +1 -0
  42. package/dist/__tests__/ecs-core-reduction.perf.test.d.ts +2 -0
  43. package/dist/__tests__/ecs-core-reduction.perf.test.d.ts.map +1 -0
  44. package/dist/__tests__/ecs-error-code-31.test-d.d.ts +2 -0
  45. package/dist/__tests__/ecs-error-code-31.test-d.d.ts.map +1 -0
  46. package/dist/__tests__/entity-disabling.unit.test.d.ts +2 -0
  47. package/dist/__tests__/entity-disabling.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/entity-handle-codec-parity.unit.test.d.ts +2 -0
  49. package/dist/__tests__/entity-handle-codec-parity.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/errors.unit.test.d.ts +2 -0
  51. package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/execution-conflict-boundary.unit.test.d.ts +2 -0
  53. package/dist/__tests__/execution-conflict-boundary.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/execution-shared-storage.unit.test.d.ts +2 -0
  55. package/dist/__tests__/execution-shared-storage.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/externalization-portable-value.unit.test.d.ts +2 -0
  57. package/dist/__tests__/externalization-portable-value.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/externalization-projection.unit.test.d.ts +2 -0
  59. package/dist/__tests__/externalization-projection.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/externalization-render-read-lease.test-d.d.ts +2 -0
  61. package/dist/__tests__/externalization-render-read-lease.test-d.d.ts.map +1 -0
  62. package/dist/__tests__/externalization-render-read-lease.unit.test.d.ts +2 -0
  63. package/dist/__tests__/externalization-render-read-lease.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/externalization-root-surface.unit.test.d.ts +2 -0
  65. package/dist/__tests__/externalization-root-surface.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/field-enum-labels-reflection.test.d.ts +2 -0
  67. package/dist/__tests__/field-enum-labels-reflection.test.d.ts.map +1 -0
  68. package/dist/__tests__/field-shape-fixture.test.d.ts +2 -0
  69. package/dist/__tests__/field-shape-fixture.test.d.ts.map +1 -0
  70. package/dist/__tests__/field-transient-reflection.test.d.ts +2 -0
  71. package/dist/__tests__/field-transient-reflection.test.d.ts.map +1 -0
  72. package/dist/__tests__/field-value-type.test-d.d.ts +2 -0
  73. package/dist/__tests__/field-value-type.test-d.d.ts.map +1 -0
  74. package/dist/__tests__/fixed-update.unit.test.d.ts +2 -0
  75. package/dist/__tests__/fixed-update.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/handle.test-d.d.ts +2 -0
  77. package/dist/__tests__/handle.test-d.d.ts.map +1 -0
  78. package/dist/__tests__/hierarchy.unit.test.d.ts +2 -0
  79. package/dist/__tests__/hierarchy.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/managed-array-vocab.test-d.d.ts +2 -0
  81. package/dist/__tests__/managed-array-vocab.test-d.d.ts.map +1 -0
  82. package/dist/__tests__/minimal-example.test-d.d.ts +2 -0
  83. package/dist/__tests__/minimal-example.test-d.d.ts.map +1 -0
  84. package/dist/__tests__/name-read-types.test-d.d.ts +2 -0
  85. package/dist/__tests__/name-read-types.test-d.d.ts.map +1 -0
  86. package/dist/__tests__/plugin-service.test.d.ts +2 -0
  87. package/dist/__tests__/plugin-service.test.d.ts.map +1 -0
  88. package/dist/__tests__/query-combinations.unit.test.d.ts +2 -0
  89. package/dist/__tests__/query-combinations.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/query-light-extract-flat-column-ratio.perf.test.d.ts +2 -0
  91. package/dist/__tests__/query-light-extract-flat-column-ratio.perf.test.d.ts.map +1 -0
  92. package/dist/__tests__/query-storage-trends.perf.test.d.ts +2 -0
  93. package/dist/__tests__/query-storage-trends.perf.test.d.ts.map +1 -0
  94. package/dist/__tests__/query-trs-flat-column-ratio.perf.test.d.ts +2 -0
  95. package/dist/__tests__/query-trs-flat-column-ratio.perf.test.d.ts.map +1 -0
  96. package/dist/__tests__/query.unit.test.d.ts +2 -0
  97. package/dist/__tests__/query.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/relationship-index.perf.test.d.ts +2 -0
  99. package/dist/__tests__/relationship-index.perf.test.d.ts.map +1 -0
  100. package/dist/__tests__/relationship-index.test.d.ts +2 -0
  101. package/dist/__tests__/relationship-index.test.d.ts.map +1 -0
  102. package/dist/__tests__/scalar-field-type-owner.test-d.d.ts +2 -0
  103. package/dist/__tests__/scalar-field-type-owner.test-d.d.ts.map +1 -0
  104. package/dist/__tests__/schedule-determinism.unit.test.d.ts +2 -0
  105. package/dist/__tests__/schedule-determinism.unit.test.d.ts.map +1 -0
  106. package/dist/__tests__/schedule-inspection.unit.test.d.ts +2 -0
  107. package/dist/__tests__/schedule-inspection.unit.test.d.ts.map +1 -0
  108. package/dist/__tests__/schedule-scope-error.unit.test.d.ts +2 -0
  109. package/dist/__tests__/schedule-scope-error.unit.test.d.ts.map +1 -0
  110. package/dist/__tests__/schedule-token.unit.test.d.ts +2 -0
  111. package/dist/__tests__/schedule-token.unit.test.d.ts.map +1 -0
  112. package/dist/__tests__/schema-vocab.test-d.d.ts +2 -0
  113. package/dist/__tests__/schema-vocab.test-d.d.ts.map +1 -0
  114. package/dist/__tests__/serialization.unit.test.d.ts +2 -0
  115. package/dist/__tests__/serialization.unit.test.d.ts.map +1 -0
  116. package/dist/__tests__/shared-kernel-eligibility.test-d.d.ts +2 -0
  117. package/dist/__tests__/shared-kernel-eligibility.test-d.d.ts.map +1 -0
  118. package/dist/__tests__/shared-kernel-eligibility.unit.test.d.ts +2 -0
  119. package/dist/__tests__/shared-kernel-eligibility.unit.test.d.ts.map +1 -0
  120. package/dist/__tests__/shared-ref-store.unit.test.d.ts +2 -0
  121. package/dist/__tests__/shared-ref-store.unit.test.d.ts.map +1 -0
  122. package/dist/__tests__/sparse-tag.unit.test.d.ts +2 -0
  123. package/dist/__tests__/sparse-tag.unit.test.d.ts.map +1 -0
  124. package/dist/__tests__/spawn-field-error.test-d.d.ts +2 -0
  125. package/dist/__tests__/spawn-field-error.test-d.d.ts.map +1 -0
  126. package/dist/__tests__/stale-error.test-d.d.ts +2 -0
  127. package/dist/__tests__/stale-error.test-d.d.ts.map +1 -0
  128. package/dist/__tests__/string-view-symmetry.test-d.d.ts +2 -0
  129. package/dist/__tests__/string-view-symmetry.test-d.d.ts.map +1 -0
  130. package/dist/__tests__/string-vocab.test-d.d.ts +2 -0
  131. package/dist/__tests__/string-vocab.test-d.d.ts.map +1 -0
  132. package/dist/__tests__/structure-epoch-boundaries.unit.test.d.ts +2 -0
  133. package/dist/__tests__/structure-epoch-boundaries.unit.test.d.ts.map +1 -0
  134. package/dist/__tests__/structure-epoch.unit.test.d.ts +2 -0
  135. package/dist/__tests__/structure-epoch.unit.test.d.ts.map +1 -0
  136. package/dist/__tests__/table-archetype-mapping.unit.test.d.ts +2 -0
  137. package/dist/__tests__/table-archetype-mapping.unit.test.d.ts.map +1 -0
  138. package/dist/__tests__/time.unit.test.d.ts +2 -0
  139. package/dist/__tests__/time.unit.test.d.ts.map +1 -0
  140. package/dist/__tests__/types.test-d.d.ts +2 -0
  141. package/dist/__tests__/types.test-d.d.ts.map +1 -0
  142. package/dist/__tests__/unique-ref-store.unit.test.d.ts +2 -0
  143. package/dist/__tests__/unique-ref-store.unit.test.d.ts.map +1 -0
  144. package/dist/__tests__/utils/handle-numeric.d.ts +14 -0
  145. package/dist/__tests__/utils/handle-numeric.d.ts.map +1 -0
  146. package/dist/__tests__/validation-root-surface.unit.test.d.ts +2 -0
  147. package/dist/__tests__/validation-root-surface.unit.test.d.ts.map +1 -0
  148. package/dist/__tests__/visibility-atomic-write.unit.test.d.ts +2 -0
  149. package/dist/__tests__/visibility-atomic-write.unit.test.d.ts.map +1 -0
  150. package/dist/__tests__/world-change-journal.unit.test.d.ts +2 -0
  151. package/dist/__tests__/world-change-journal.unit.test.d.ts.map +1 -0
  152. package/dist/__tests__/world-characterization.unit.test.d.ts +2 -0
  153. package/dist/__tests__/world-characterization.unit.test.d.ts.map +1 -0
  154. package/dist/__tests__/world-consumer-inference.test-d.d.ts +2 -0
  155. package/dist/__tests__/world-consumer-inference.test-d.d.ts.map +1 -0
  156. package/dist/__tests__/world-health.contract.test.d.ts +2 -0
  157. package/dist/__tests__/world-health.contract.test.d.ts.map +1 -0
  158. package/dist/__tests__/world-inspection.test.d.ts +2 -0
  159. package/dist/__tests__/world-inspection.test.d.ts.map +1 -0
  160. package/dist/__tests__/world-internal-identity.unit.test.d.ts +2 -0
  161. package/dist/__tests__/world-internal-identity.unit.test.d.ts.map +1 -0
  162. package/dist/__tests__/world-managed-roundtrip.unit.test.d.ts +2 -0
  163. package/dist/__tests__/world-managed-roundtrip.unit.test.d.ts.map +1 -0
  164. package/dist/__tests__/world-scheduling.m4.test.d.ts +2 -0
  165. package/dist/__tests__/world-scheduling.m4.test.d.ts.map +1 -0
  166. package/dist/__tests__/world-unique-roundtrip.unit.test.d.ts +2 -0
  167. package/dist/__tests__/world-unique-roundtrip.unit.test.d.ts.map +1 -0
  168. package/dist/__tests__/world.test-d.d.ts +2 -0
  169. package/dist/__tests__/world.test-d.d.ts.map +1 -0
  170. package/dist/buffer-pool.d.ts +109 -0
  171. package/dist/buffer-pool.d.ts.map +1 -0
  172. package/dist/commands.d.ts +93 -0
  173. package/dist/commands.d.ts.map +1 -0
  174. package/dist/component-default-fallback.d.ts +88 -0
  175. package/dist/component-default-fallback.d.ts.map +1 -0
  176. package/dist/component-schema.d.ts +34 -0
  177. package/dist/component-schema.d.ts.map +1 -0
  178. package/dist/component-value-validate.d.ts +29 -0
  179. package/dist/component-value-validate.d.ts.map +1 -0
  180. package/dist/component.d.ts +581 -0
  181. package/dist/component.d.ts.map +1 -0
  182. package/dist/entity-handle.d.ts +68 -0
  183. package/dist/entity-handle.d.ts.map +1 -0
  184. package/dist/entity.d.ts +86 -0
  185. package/dist/entity.d.ts.map +1 -0
  186. package/dist/errors/query-and-component-errors.d.ts +132 -0
  187. package/dist/errors/query-and-component-errors.d.ts.map +1 -0
  188. package/dist/errors/relationship-errors.d.ts +112 -0
  189. package/dist/errors/relationship-errors.d.ts.map +1 -0
  190. package/dist/errors/sprite-and-shared-errors.d.ts +107 -0
  191. package/dist/errors/sprite-and-shared-errors.d.ts.map +1 -0
  192. package/dist/errors/validation-errors.d.ts +229 -0
  193. package/dist/errors/validation-errors.d.ts.map +1 -0
  194. package/dist/errors.d.ts +763 -0
  195. package/dist/errors.d.ts.map +1 -0
  196. package/dist/execution/shared-kernel.d.ts +106 -0
  197. package/dist/execution/shared-kernel.d.ts.map +1 -0
  198. package/dist/externalization/index.d.ts +31 -0
  199. package/dist/externalization/index.d.ts.map +1 -0
  200. package/dist/externalization/index.mjs +298 -0
  201. package/dist/externalization/index.mjs.map +1 -0
  202. package/dist/index.d.ts +261 -0
  203. package/dist/index.d.ts.map +1 -0
  204. package/dist/index.mjs +7422 -0
  205. package/dist/index.mjs.map +1 -0
  206. package/dist/internal.mjs +479 -0
  207. package/dist/internal.mjs.map +1 -0
  208. package/dist/plugin-service.d.ts +11 -0
  209. package/dist/plugin-service.d.ts.map +1 -0
  210. package/dist/projection/index.d.ts +103 -0
  211. package/dist/projection/index.d.ts.map +1 -0
  212. package/dist/projection/index.mjs +615 -0
  213. package/dist/projection/index.mjs.map +1 -0
  214. package/dist/query/query.d.ts +58 -0
  215. package/dist/query/query.d.ts.map +1 -0
  216. package/dist/relationship-index.d.ts +81 -0
  217. package/dist/relationship-index.d.ts.map +1 -0
  218. package/dist/resource.d.ts +70 -0
  219. package/dist/resource.d.ts.map +1 -0
  220. package/dist/schedule-token.d.ts +16 -0
  221. package/dist/schedule-token.d.ts.map +1 -0
  222. package/dist/schedule.d.ts +271 -0
  223. package/dist/schedule.d.ts.map +1 -0
  224. package/dist/shared-ref-store.d.ts +144 -0
  225. package/dist/shared-ref-store.d.ts.map +1 -0
  226. package/dist/shared.d.ts +3 -0
  227. package/dist/shared.d.ts.map +1 -0
  228. package/dist/shared.mjs +364 -0
  229. package/dist/shared.mjs.map +1 -0
  230. package/dist/storage/archetype-graph.d.ts +54 -0
  231. package/dist/storage/archetype-graph.d.ts.map +1 -0
  232. package/dist/storage/archetype.d.ts +23 -0
  233. package/dist/storage/archetype.d.ts.map +1 -0
  234. package/dist/storage/change-detection.d.ts +40 -0
  235. package/dist/storage/change-detection.d.ts.map +1 -0
  236. package/dist/storage/column.d.ts +111 -0
  237. package/dist/storage/column.d.ts.map +1 -0
  238. package/dist/storage/table.d.ts +29 -0
  239. package/dist/storage/table.d.ts.map +1 -0
  240. package/dist/time.d.ts +57 -0
  241. package/dist/time.d.ts.map +1 -0
  242. package/dist/unique-ref-store.d.ts +105 -0
  243. package/dist/unique-ref-store.d.ts.map +1 -0
  244. package/dist/world-change-journal.d.ts +34 -0
  245. package/dist/world-change-journal.d.ts.map +1 -0
  246. package/dist/world-component-access.d.ts +265 -0
  247. package/dist/world-component-access.d.ts.map +1 -0
  248. package/dist/world-component-storage.d.ts +282 -0
  249. package/dist/world-component-storage.d.ts.map +1 -0
  250. package/dist/world-core.d.ts +39 -0
  251. package/dist/world-core.d.ts.map +1 -0
  252. package/dist/world-entity-lifecycle.d.ts +30 -0
  253. package/dist/world-entity-lifecycle.d.ts.map +1 -0
  254. package/dist/world-internal.d.ts +14 -0
  255. package/dist/world-internal.d.ts.map +1 -0
  256. package/dist/world-scheduling.d.ts +30 -0
  257. package/dist/world-scheduling.d.ts.map +1 -0
  258. package/dist/world.d.ts +620 -0
  259. package/dist/world.d.ts.map +1 -0
  260. package/package.json +77 -0
  261. package/src/__fixtures__/field-shape-fixture.ts +30 -0
  262. package/src/__tests__/__fixtures__/inspect-scripts.snapshot.ts +2 -0
  263. package/src/__tests__/archetype.unit.test.ts +1837 -0
  264. package/src/__tests__/array-handle-element.test-d.ts +34 -0
  265. package/src/__tests__/buffer-array-vocab.test-d.ts +53 -0
  266. package/src/__tests__/buffer-field-input-widen.unit.test.ts +99 -0
  267. package/src/__tests__/buffer-pool.test-d.ts +32 -0
  268. package/src/__tests__/change-detection.unit.test.ts +86 -0
  269. package/src/__tests__/command-buffer.test.ts +179 -0
  270. package/src/__tests__/commands-deferred.unit.test.ts +68 -0
  271. package/src/__tests__/component-catalog.test.ts +59 -0
  272. package/src/__tests__/component-definition-identity.unit.test.ts +24 -0
  273. package/src/__tests__/component-nan-preflight.unit.test.ts +186 -0
  274. package/src/__tests__/component-schema.test-d.ts +119 -0
  275. package/src/__tests__/component-schema.test.ts +55 -0
  276. package/src/__tests__/define-component-options.test-d.ts +35 -0
  277. package/src/__tests__/deprecated-vocab-rejection.unit.test.ts +59 -0
  278. package/src/__tests__/despawn-all.unit.test.ts +17 -0
  279. package/src/__tests__/ecs-core-reduction.characterization.test.ts +130 -0
  280. package/src/__tests__/ecs-core-reduction.perf.test.ts +260 -0
  281. package/src/__tests__/ecs-error-code-31.test-d.ts +42 -0
  282. package/src/__tests__/entity-disabling.unit.test.ts +37 -0
  283. package/src/__tests__/entity-handle-codec-parity.unit.test.ts +126 -0
  284. package/src/__tests__/errors.unit.test.ts +1456 -0
  285. package/src/__tests__/execution-conflict-boundary.unit.test.ts +123 -0
  286. package/src/__tests__/execution-shared-storage.unit.test.ts +40 -0
  287. package/src/__tests__/externalization-portable-value.unit.test.ts +275 -0
  288. package/src/__tests__/externalization-projection.unit.test.ts +325 -0
  289. package/src/__tests__/externalization-render-read-lease.test-d.ts +30 -0
  290. package/src/__tests__/externalization-render-read-lease.unit.test.ts +79 -0
  291. package/src/__tests__/externalization-root-surface.unit.test.ts +33 -0
  292. package/src/__tests__/field-enum-labels-reflection.test.ts +66 -0
  293. package/src/__tests__/field-shape-fixture.test.ts +61 -0
  294. package/src/__tests__/field-transient-reflection.test.ts +59 -0
  295. package/src/__tests__/field-value-type.test-d.ts +57 -0
  296. package/src/__tests__/fixed-update.unit.test.ts +78 -0
  297. package/src/__tests__/handle.test-d.ts +86 -0
  298. package/src/__tests__/hierarchy.unit.test.ts +2534 -0
  299. package/src/__tests__/managed-array-vocab.test-d.ts +137 -0
  300. package/src/__tests__/minimal-example.test-d.ts +42 -0
  301. package/src/__tests__/name-read-types.test-d.ts +72 -0
  302. package/src/__tests__/plugin-service.test.ts +13 -0
  303. package/src/__tests__/query-combinations.unit.test.ts +42 -0
  304. package/src/__tests__/query-light-extract-flat-column-ratio.perf.test.ts +252 -0
  305. package/src/__tests__/query-storage-trends.perf.test.ts +104 -0
  306. package/src/__tests__/query-trs-flat-column-ratio.perf.test.ts +194 -0
  307. package/src/__tests__/query.unit.test.ts +196 -0
  308. package/src/__tests__/relationship-index.perf.test.ts +126 -0
  309. package/src/__tests__/relationship-index.test.ts +70 -0
  310. package/src/__tests__/scalar-field-type-owner.test-d.ts +71 -0
  311. package/src/__tests__/schedule-determinism.unit.test.ts +36 -0
  312. package/src/__tests__/schedule-inspection.unit.test.ts +81 -0
  313. package/src/__tests__/schedule-scope-error.unit.test.ts +63 -0
  314. package/src/__tests__/schedule-token.unit.test.ts +54 -0
  315. package/src/__tests__/schema-vocab.test-d.ts +103 -0
  316. package/src/__tests__/serialization.unit.test.ts +2740 -0
  317. package/src/__tests__/shared-kernel-eligibility.test-d.ts +18 -0
  318. package/src/__tests__/shared-kernel-eligibility.unit.test.ts +78 -0
  319. package/src/__tests__/shared-ref-store.unit.test.ts +646 -0
  320. package/src/__tests__/sparse-tag.unit.test.ts +201 -0
  321. package/src/__tests__/spawn-field-error.test-d.ts +41 -0
  322. package/src/__tests__/stale-error.test-d.ts +208 -0
  323. package/src/__tests__/string-view-symmetry.test-d.ts +26 -0
  324. package/src/__tests__/string-vocab.test-d.ts +49 -0
  325. package/src/__tests__/structure-epoch-boundaries.unit.test.ts +39 -0
  326. package/src/__tests__/structure-epoch.unit.test.ts +48 -0
  327. package/src/__tests__/table-archetype-mapping.unit.test.ts +78 -0
  328. package/src/__tests__/time.unit.test.ts +79 -0
  329. package/src/__tests__/types.test-d.ts +38 -0
  330. package/src/__tests__/unique-ref-store.unit.test.ts +348 -0
  331. package/src/__tests__/utils/handle-numeric.ts +30 -0
  332. package/src/__tests__/validation-root-surface.unit.test.ts +15 -0
  333. package/src/__tests__/visibility-atomic-write.unit.test.ts +82 -0
  334. package/src/__tests__/world-change-journal.unit.test.ts +97 -0
  335. package/src/__tests__/world-characterization.unit.test.ts +408 -0
  336. package/src/__tests__/world-consumer-inference.test-d.ts +16 -0
  337. package/src/__tests__/world-health.contract.test.ts +87 -0
  338. package/src/__tests__/world-inspection.test.ts +37 -0
  339. package/src/__tests__/world-internal-identity.unit.test.ts +17 -0
  340. package/src/__tests__/world-managed-roundtrip.unit.test.ts +98 -0
  341. package/src/__tests__/world-scheduling.m4.test.ts +52 -0
  342. package/src/__tests__/world-unique-roundtrip.unit.test.ts +357 -0
  343. package/src/__tests__/world.test-d.ts +156 -0
  344. package/src/buffer-pool.ts +357 -0
  345. package/src/commands.ts +644 -0
  346. package/src/component-default-fallback.ts +236 -0
  347. package/src/component-schema.ts +92 -0
  348. package/src/component-value-validate.ts +135 -0
  349. package/src/component.ts +1268 -0
  350. package/src/entity-handle.ts +104 -0
  351. package/src/entity.ts +140 -0
  352. package/src/errors/query-and-component-errors.ts +200 -0
  353. package/src/errors/relationship-errors.ts +198 -0
  354. package/src/errors/sprite-and-shared-errors.ts +231 -0
  355. package/src/errors/validation-errors.ts +506 -0
  356. package/src/errors.ts +1464 -0
  357. package/src/execution/shared-kernel.ts +392 -0
  358. package/src/externalization/index.ts +170 -0
  359. package/src/index.ts +414 -0
  360. package/src/plugin-service.ts +35 -0
  361. package/src/projection/index.ts +318 -0
  362. package/src/query/query.ts +606 -0
  363. package/src/relationship-index.ts +175 -0
  364. package/src/resource.ts +106 -0
  365. package/src/schedule-token.ts +40 -0
  366. package/src/schedule.ts +846 -0
  367. package/src/shared-ref-store.ts +418 -0
  368. package/src/shared.ts +23 -0
  369. package/src/storage/archetype-graph.ts +166 -0
  370. package/src/storage/archetype.ts +78 -0
  371. package/src/storage/change-detection.ts +231 -0
  372. package/src/storage/column.ts +247 -0
  373. package/src/storage/table.ts +153 -0
  374. package/src/time.ts +122 -0
  375. package/src/unique-ref-store.ts +278 -0
  376. package/src/world-change-journal.ts +84 -0
  377. package/src/world-component-access.ts +1421 -0
  378. package/src/world-component-storage.ts +1178 -0
  379. package/src/world-core.ts +74 -0
  380. package/src/world-entity-lifecycle.ts +539 -0
  381. package/src/world-internal.ts +75 -0
  382. package/src/world-scheduling.ts +499 -0
  383. package/src/world.ts +1399 -0
@@ -0,0 +1,2740 @@
1
+ // biome-ignore-all lint/complexity/noUselessLoneBlockStatements: scope isolation between merged source files
2
+ // Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Update } from '../schedule-token';
7
+ //
8
+ // Source files (N=29):
9
+ // - packages/ecs/__tests__/managed-ref-on-release.test.ts
10
+ // - packages/ecs/__tests__/register-ecs-inspector.test.ts
11
+ // - packages/ecs/src/__tests__/ai-user-sandbox-trial.test.ts
12
+ // - packages/ecs/src/__tests__/array-handle-element.test.ts
13
+ // - packages/ecs/src/__tests__/array-remove-by-value.test.ts
14
+ // - packages/ecs/src/__tests__/hook-despawn.test.ts
15
+ // - packages/ecs/src/__tests__/hook-no-regression.test.ts
16
+ // - packages/ecs/src/__tests__/hook-on-insert.test.ts
17
+ // - packages/ecs/src/__tests__/hook-on-remove.test.ts
18
+ // - packages/ecs/src/__tests__/inspect.test.ts
19
+ // - packages/ecs/src/__tests__/managed-array-carry-over.test.ts
20
+ // - packages/ecs/src/__tests__/managed-array-element-type.test.ts
21
+ // - packages/ecs/src/__tests__/managed-array-errors.test.ts
22
+ // - packages/ecs/src/__tests__/managed-array-release.test.ts
23
+ // - packages/ecs/src/__tests__/managed-array-stride.test.ts
24
+ // - packages/ecs/src/__tests__/managed-array-vocab.test.ts
25
+ // - packages/ecs/src/__tests__/managed-buffer-grow.test.ts
26
+ // - packages/ecs/src/__tests__/managed-carry-over.test.ts
27
+ // - packages/ecs/src/__tests__/managed-release.test.ts
28
+ // - packages/ecs/src/__tests__/register-inspector.test.ts
29
+ // - packages/ecs/src/__tests__/resource.test.ts
30
+ // - packages/ecs/src/__tests__/scene-instance-container.test.ts
31
+ // - packages/ecs/src/__tests__/schedule-remove-replace.test.ts
32
+ // - packages/ecs/src/__tests__/schedule.test.ts
33
+ // - packages/ecs/src/__tests__/string-carry-over.test.ts
34
+ // - packages/ecs/src/__tests__/string-identity-contract.test.ts
35
+ // - packages/ecs/src/__tests__/string-managed-dispatch.test.ts
36
+ // - packages/ecs/src/__tests__/string-release.test.ts
37
+ //
38
+ // Excluded (defineComponent name collisions — Transform/ChildOf break resolveComponent):
39
+ // - packages/ecs/src/__tests__/scene-instance-delete-marks.test.ts
40
+ // - packages/ecs/src/__tests__/scene-instance-divergence.test.ts
41
+ // - packages/ecs/src/__tests__/scene-instance-hierarchy.test.ts
42
+ // - packages/ecs/src/__tests__/scene-instance-overrides.test.ts
43
+ // - packages/ecs/src/__tests__/scene-instantiate-typo.test.ts
44
+ // - packages/ecs/src/__tests__/scene-instantiate.test.ts
45
+ // - packages/ecs/src/__tests__/scene-multi-instance.test.ts
46
+ //
47
+ // Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
48
+ // source as ancestorTitles[0]. Top-level imports merged + deduped.
49
+
50
+ import type { Handle } from '@forgeax/engine-types';
51
+ import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
52
+ import { BufferPool } from '../buffer-pool';
53
+ import {
54
+ type Component,
55
+ defineComponent,
56
+ type FieldValueType,
57
+ isSchemaVocabKeyword,
58
+ MANAGED_ARRAY_ELEMENT_TYPES,
59
+ parseManagedArraySchema,
60
+ type SchemaFieldType,
61
+ type SchemaOf,
62
+ type ShapeOf,
63
+ type TypedArrayFor,
64
+ } from '../component';
65
+ import { ENTITY_NULL_RAW } from '../entity-handle';
66
+ import {
67
+ type EcsErrorCode,
68
+ type EcsErrorDetail,
69
+ FixedSizeMismatchError,
70
+ type ManagedArrayElementTypeNotAllowedError,
71
+ ResourceNotFoundError,
72
+ } from '../errors';
73
+ import { UniqueRefStore } from '../unique-ref-store';
74
+ import { World, type WorldInspection } from '../world';
75
+
76
+ import { worldInternal } from '../world-internal';
77
+ import { handleNumeric } from './utils/handle-numeric';
78
+
79
+ {
80
+ describe('feat-20260528 M1 t1 UniqueRefStore onRelease callback', () => {
81
+ it('alloc with onRelease stores the callback and calls it on release before delete', () => {
82
+ const store = new UniqueRefStore();
83
+ let called = false;
84
+ let releasedPayload: string | undefined;
85
+
86
+ const handle = store.alloc<'Test', string>('Test', 'hello', (payload) => {
87
+ called = true;
88
+ releasedPayload = payload;
89
+ });
90
+ expect(called).toBe(false);
91
+
92
+ const result = store.release(handle);
93
+ expect(result.ok).toBe(true);
94
+ expect(called).toBe(true);
95
+ expect(releasedPayload).toBe('hello');
96
+ });
97
+
98
+ it('release with onRelease callback: second release (double-release) does not trigger callback', () => {
99
+ const store = new UniqueRefStore();
100
+ let callCount = 0;
101
+
102
+ const handle = store.alloc<'Test'>('Test', 'world', () => {
103
+ callCount++;
104
+ });
105
+
106
+ store.release(handle);
107
+ expect(callCount).toBe(1);
108
+
109
+ // Double-release: handle already freed, onRelease should NOT fire again.
110
+ const result2 = store.release(handle);
111
+ expect(result2.ok).toBe(false);
112
+ expect(callCount).toBe(1);
113
+ });
114
+
115
+ it('alloc without onRelease (backward-compatible two-param form) works', () => {
116
+ const store = new UniqueRefStore();
117
+ const handle = store.alloc<'Test'>('Test', 'backward-compat');
118
+ const resolveResult = store.resolve(handle);
119
+ expect(resolveResult.ok).toBe(true);
120
+ if (resolveResult.ok) {
121
+ expect(resolveResult.value).toBe('backward-compat');
122
+ }
123
+
124
+ const releaseResult = store.release(handle);
125
+ expect(releaseResult.ok).toBe(true);
126
+
127
+ // After release, resolve should fail.
128
+ const after = store.resolve(handle);
129
+ expect(after.ok).toBe(false);
130
+ });
131
+
132
+ it('onRelease passes the correct payload when multiple handles exist', () => {
133
+ const store = new UniqueRefStore();
134
+ const released: number[] = [];
135
+
136
+ type Item = { id: number };
137
+ const h1 = store.alloc<'A', Item>('A', { id: 1 }, (p) => released.push(p.id));
138
+ const h2 = store.alloc<'A', Item>('A', { id: 2 }, (p) => released.push(p.id));
139
+ const h3 = store.alloc<'A', Item>('A', { id: 3 }); // no callback
140
+
141
+ // Release out of order.
142
+ store.release(h2);
143
+ expect(released).toEqual([2]);
144
+
145
+ store.release(h1);
146
+ expect(released).toEqual([2, 1]);
147
+
148
+ store.release(h3); // no callback, should not push.
149
+ expect(released).toEqual([2, 1]);
150
+ });
151
+
152
+ it('onRelease fires before payload is deleted (resolve after release yields no payload)', () => {
153
+ const store = new UniqueRefStore();
154
+ let payloadDuringCallback: { name: string } | undefined;
155
+
156
+ const handle = store.alloc<'X', { name: string }>('X', { name: 'payload' }, (p) => {
157
+ // At callback time, resolve should still return the payload
158
+ // (it is deleted AFTER the callback).
159
+ payloadDuringCallback = p;
160
+ });
161
+
162
+ store.release(handle);
163
+ expect(payloadDuringCallback).toEqual({ name: 'payload' });
164
+
165
+ // After release, resolve should fail.
166
+ const after = store.resolve(handle);
167
+ expect(after.ok).toBe(false);
168
+ });
169
+
170
+ it('onRelease is called per-handle: independent handles do not share callbacks', () => {
171
+ const store = new UniqueRefStore();
172
+ const sequence: string[] = [];
173
+
174
+ const h1 = store.alloc<'A'>('A', 1, () => sequence.push('a'));
175
+ const h2 = store.alloc<'A'>('A', 2, () => sequence.push('b'));
176
+
177
+ store.release(h1);
178
+ expect(sequence).toEqual(['a']);
179
+
180
+ store.release(h2);
181
+ expect(sequence).toEqual(['a', 'b']);
182
+ });
183
+ });
184
+ }
185
+ {
186
+ // --- from ai-user-sandbox-trial.test.ts ---
187
+ interface MaterialAsset {
188
+ readonly albedo: readonly [number, number, number, number];
189
+ }
190
+
191
+ // World owns its UniqueRefStore from construction (M1: uniqueRefs is a
192
+ // non-null private field). Tests probe the internal store via a structural
193
+ // cast --- no caller-side wiring step remains.
194
+ function refsOf(w: World): UniqueRefStore {
195
+ return (w as unknown as { uniqueRefs: UniqueRefStore }).uniqueRefs;
196
+ }
197
+
198
+ describe('AI user sandbox - schema vocab + lifecycle', () => {
199
+ it('(1) `buffer<128>` field exposes Uint8Array view, mutable in place', () => {
200
+ const Mesh = defineComponent('Mesh', { vertexData: { type: 'buffer<128>' } });
201
+ const w = new World();
202
+ const e = w.spawn({ component: Mesh, data: { vertexData: new Uint8Array(128) } }).unwrap();
203
+ const r = w.get(e, Mesh).unwrap();
204
+ expect(r.vertexData).toBeInstanceOf(Uint8Array);
205
+ expect(r.vertexData.byteLength).toBe(128);
206
+ r.vertexData[0] = 42;
207
+ const r2 = w.get(e, Mesh).unwrap();
208
+ expect(r2.vertexData[0]).toBe(42);
209
+ });
210
+
211
+ it('(2) `ref<T>` field stores managed Handle; auto-released on despawn (AC-03 path 1)', () => {
212
+ const Mat = defineComponent('Mat', { material: { type: 'unique<MaterialAsset>' } });
213
+ const w = new World();
214
+ const store = refsOf(w);
215
+ const handle = store.alloc<'MaterialAsset', MaterialAsset>('MaterialAsset', {
216
+ albedo: [1, 0, 0, 1],
217
+ });
218
+ const e = w
219
+ .spawn({
220
+ component: Mat,
221
+ data: { material: handle },
222
+ })
223
+ .unwrap();
224
+ const r = w.get(e, Mat).unwrap();
225
+ const payload = store.resolve<'MaterialAsset', MaterialAsset>(r.material);
226
+ expect(payload.ok).toBe(true);
227
+ if (payload.ok) expect(payload.value.albedo[0]).toBe(1);
228
+ w.despawn(e).unwrap();
229
+ const after = store.resolve(handle);
230
+ expect(after.ok).toBe(false);
231
+ if (!after.ok) expect(after.error.code).toBe('unique-ref-stale');
232
+ });
233
+
234
+ it('(3) `handle<T>` field stores unmanaged Handle; ECS does NOT release on despawn', () => {
235
+ const MeshFilter = defineComponent('MeshFilter', {
236
+ assetHandle: { type: 'shared<MeshAsset>' },
237
+ });
238
+ const w = new World();
239
+ const fakeHandle: Handle<'MeshAsset', 'shared'> = 0x1234_5678 as Handle<
240
+ 'MeshAsset',
241
+ 'shared'
242
+ >;
243
+ const e = w.spawn({ component: MeshFilter, data: { assetHandle: fakeHandle } }).unwrap();
244
+ const r = w.get(e, MeshFilter).unwrap();
245
+ expect(r.assetHandle).toBe(fakeHandle);
246
+ w.despawn(e).unwrap();
247
+ });
248
+
249
+ it('(4) ENTITY_NULL_RAW sentinel is 0xffffffff (entity-field null encoding)', () => {
250
+ // Sentinel vocab invariant: the underlying u32 value used as 'no entity'
251
+ // is 0xffffffff; the surface to AI users for that slot is `null`. The ECS
252
+ // returns the raw encoded Entity on read without bottoming out dangling
253
+ // references; consumers check liveness themselves.
254
+ expect(ENTITY_NULL_RAW).toBe(0xffffffff);
255
+ });
256
+
257
+ it('(5) archetype migrate (addComponent reshuffle): managed handle Object.is preserved', () => {
258
+ const Mat = defineComponent('Mat', { material: { type: 'unique<MaterialAsset>' } });
259
+ const Tag = defineComponent('Tag', { x: { type: 'u32' } });
260
+ const w = new World();
261
+ const store = refsOf(w);
262
+ const handle = store.alloc<'MaterialAsset', MaterialAsset>('MaterialAsset', {
263
+ albedo: [0, 1, 0, 1],
264
+ });
265
+ const e = w
266
+ .spawn({
267
+ component: Mat,
268
+ data: { material: handle },
269
+ })
270
+ .unwrap();
271
+ const handleBefore = w.get(e, Mat).unwrap().material;
272
+ w.addComponent(e, { component: Tag, data: { x: 7 } }).unwrap();
273
+ const handleAfter = w.get(e, Mat).unwrap().material;
274
+ // u32 bit-equal: handle is a packed Uint32 brand.
275
+ expect(handleAfter).toBe(handleBefore);
276
+ // Resolved payload still alive (carry-over, NOT release).
277
+ const resolved = store.resolve<'MaterialAsset', MaterialAsset>(handleAfter);
278
+ expect(resolved.ok).toBe(true);
279
+ if (resolved.ok) expect(resolved.value.albedo[1]).toBe(1);
280
+ });
281
+
282
+ it('(6) `world.set` covering write releases old managed handle (AC-03 path 3)', () => {
283
+ const Mat = defineComponent('Mat', { material: { type: 'unique<MaterialAsset>' } });
284
+ const w = new World();
285
+ const store = refsOf(w);
286
+ const oldHandle = store.alloc('MaterialAsset', { albedo: [1, 0, 0, 1] });
287
+ const newHandle = store.alloc('MaterialAsset', { albedo: [0, 0, 1, 1] });
288
+ const e = w
289
+ .spawn({
290
+ component: Mat,
291
+ data: { material: oldHandle },
292
+ })
293
+ .unwrap();
294
+ w.set(e, Mat, {
295
+ material: newHandle,
296
+ }).unwrap();
297
+ const after = store.resolve(oldHandle);
298
+ expect(after.ok).toBe(false);
299
+ if (!after.ok) expect(after.error.code).toBe('unique-ref-stale');
300
+ expect(store.resolve(newHandle).ok).toBe(true);
301
+ });
302
+ });
303
+ }
304
+ {
305
+ // --- from array-handle-element.test.ts ---
306
+ describe('array<handle<X>> parse (w6, D-1)', () => {
307
+ it("parseManagedArraySchema('array<shared<MaterialAsset>>') returns { elementType: 'shared<MaterialAsset>', length: undefined }", () => {
308
+ const result = parseManagedArraySchema('array<shared<MaterialAsset>>');
309
+ // pre w5: returns null because 'shared<MaterialAsset>' is not in
310
+ // ManagedArrayElementType — this is the TDD red state
311
+ expect(result).not.toBeNull();
312
+ if (result === null) return;
313
+ expect(result.elementType).toBe('shared<MaterialAsset>');
314
+ expect(result.length).toBeUndefined();
315
+ });
316
+
317
+ it("parseManagedArraySchema('array<shared<MeshAsset>>') returns { elementType: 'shared<MeshAsset>' }", () => {
318
+ const result = parseManagedArraySchema('array<shared<MeshAsset>>');
319
+ expect(result).not.toBeNull();
320
+ if (result === null) return;
321
+ expect(result.elementType).toBe('shared<MeshAsset>');
322
+ expect(result.length).toBeUndefined();
323
+ });
324
+
325
+ it("parseManagedArraySchema('array<shared<>, 3>') returns null (empty tag rejection)", () => {
326
+ // R-NEW-1: empty tag inside shared<> is rejected
327
+ const result = parseManagedArraySchema('array<shared<>, 3>');
328
+ expect(result).toBeNull();
329
+ });
330
+
331
+ it("parseManagedArraySchema('array<shared<>>') returns null (variable-capacity empty tag rejection)", () => {
332
+ const result = parseManagedArraySchema('array<shared<>>');
333
+ expect(result).toBeNull();
334
+ });
335
+
336
+ it("parseManagedArraySchema('array<handle<X>>') returns null (post-w23 'handle<T>' arm deleted)", () => {
337
+ // gate-allow:ecs-brand
338
+ // feat-20260614 w23: the 'handle<T>' parser arm was removed; any
339
+ // legacy literal flowing through here returns null and the caller
340
+ // surfaces SchemaUnsupportedFieldError (with migration-hint pointing
341
+ // at 'shared<T>' -- see schema-vocab.test-d.ts).
342
+ const result = parseManagedArraySchema('array<handle<MeshAsset>>');
343
+ expect(result).toBeNull();
344
+ });
345
+
346
+ it("isSchemaVocabKeyword('array<shared<MaterialAsset>>') returns true", () => {
347
+ const result = isSchemaVocabKeyword('array<shared<MaterialAsset>>');
348
+ expect(result).toBe(true);
349
+ });
350
+ });
351
+ }
352
+ // The old public array-remove-by-value fixture was deleted with the push/pop
353
+ // facade. Relationship detach now uses its owner-side slot backpointer.
354
+ {
355
+ // --- from hook-despawn.test.ts ---
356
+ }
357
+ {
358
+ // --- from hook-no-regression.test.ts ---
359
+ describe('hook no-regression guard (AC-05)', () => {
360
+ it('addComponent path unchanged for components without hook declaration', () => {
361
+ const Pos = defineComponent('NoRegPos', { x: { type: 'f32' }, y: { type: 'f32' } });
362
+ const Tag = defineComponent('NoRegTag', {});
363
+ const world = new World();
364
+ const e = world.spawn({ component: Pos, data: { x: 1, y: 2 } }).unwrap();
365
+
366
+ const r = world.addComponent(e, { component: Tag, data: {} });
367
+ expect(r.ok).toBe(true);
368
+
369
+ const pos = world.get(e, Pos).unwrap();
370
+ const tag = world.get(e, Tag).unwrap();
371
+ expect(pos).toEqual({ x: 1, y: 2 });
372
+ expect(tag).toEqual({});
373
+ });
374
+
375
+ it('removeComponent path unchanged for components without hook declaration', () => {
376
+ const Pos = defineComponent('NoRegPos2', { x: { type: 'f32' }, y: { type: 'f32' } });
377
+ const world = new World();
378
+ const e = world.spawn({ component: Pos, data: { x: 3, y: 4 } }).unwrap();
379
+
380
+ const r = world.removeComponent(e, Pos);
381
+ expect(r.ok).toBe(true);
382
+
383
+ const result = world.get(e, Pos);
384
+ expect(result.ok).toBe(false);
385
+ });
386
+
387
+ it('despawn path unchanged for components without hook declaration', () => {
388
+ const Pos = defineComponent('NoRegPos3', { x: { type: 'f32' }, y: { type: 'f32' } });
389
+ const Tag = defineComponent('NoRegTag3', {});
390
+ const world = new World();
391
+ const e = world.spawn({ component: Pos, data: { x: 5, y: 6 } }).unwrap();
392
+ // Verify the entity was alive before despawn.
393
+ expect(world.get(e, Pos).ok).toBe(true);
394
+
395
+ const r = world.despawn(e);
396
+ expect(r.ok).toBe(true);
397
+
398
+ // After despawn, the entity is stale.
399
+ const result = world.get(e, Pos);
400
+ expect(result.ok).toBe(false);
401
+ // Also verify despawn does not affect other entities.
402
+ const other = world.spawn({ component: Tag, data: {} }).unwrap();
403
+ expect(world.get(other, Tag).ok).toBe(true);
404
+ });
405
+
406
+ it('spawn path unchanged for components without hook declaration', () => {
407
+ const Pos = defineComponent('NoRegPos4', { x: { type: 'f32' }, y: { type: 'f32' } });
408
+ const world = new World();
409
+ const e = world.spawn({ component: Pos, data: { x: 7, y: 8 } }).unwrap();
410
+ const pos = world.get(e, Pos).unwrap();
411
+ expect(pos).toEqual({ x: 7, y: 8 });
412
+ });
413
+ });
414
+ }
415
+ {
416
+ // --- from hook-on-insert.test.ts ---
417
+ }
418
+ {
419
+ // --- from hook-on-remove.test.ts ---
420
+ }
421
+ {
422
+ // --- from inspect.test.ts ---
423
+ describe('world.inspect()', () => {
424
+ it('returns a WorldInspection with all 6 fields defined on empty world', () => {
425
+ const world = new World();
426
+ const info: WorldInspection = world.inspect();
427
+
428
+ expect(info.entityCount).toBe(0);
429
+ expect(info.archetypeCount).toBe(0);
430
+ expect(info.archetypes).toEqual([]);
431
+ expect(info.activeComponents).toEqual([]);
432
+ expect(info.systemCount).toBe(0);
433
+ expect(info.resourceKeys).toContain('Time');
434
+ expect(info.resourceKeys).toContain('FixedTime');
435
+ });
436
+
437
+ it('entityCount reflects live entities after spawn and despawn', () => {
438
+ const Pos = defineComponent('InspectPos', { x: { type: 'f32' }, y: { type: 'f32' } });
439
+ const world = new World();
440
+ const e1 = world.spawn({ component: Pos, data: { x: 1, y: 2 } }).unwrap();
441
+ world.spawn({ component: Pos, data: { x: 3, y: 4 } });
442
+
443
+ expect(world.inspect().entityCount).toBe(2);
444
+
445
+ world.despawn(e1);
446
+ expect(world.inspect().entityCount).toBe(1);
447
+ });
448
+
449
+ it('archetypeCount and archetypes reflect distinct component sets', () => {
450
+ const A = defineComponent('InspA', { v: { type: 'f32' } });
451
+ const B = defineComponent('InspB', { w: { type: 'i32' } });
452
+ const world = new World();
453
+
454
+ world.spawn({ component: A, data: { v: 1 } });
455
+ world.spawn({ component: B, data: { w: 2 } });
456
+
457
+ const info = world.inspect();
458
+ expect(info.archetypeCount).toBe(2);
459
+ expect(info.archetypes.length).toBe(2);
460
+
461
+ // Each archetype has correct shape
462
+ for (const arch of info.archetypes) {
463
+ expect(arch.key).toBeDefined();
464
+ expect(arch.componentNames.length).toBeGreaterThanOrEqual(1);
465
+ expect(arch.entityCount).toBe(1);
466
+ expect(info.tables[arch.tableId]?.capacity).toBeGreaterThan(0);
467
+ }
468
+ });
469
+
470
+ it('activeComponents lists component names that have been spawned, not merely defined', () => {
471
+ const X = defineComponent('InspX', { v: 'u8' });
472
+ // InspY is defined (globally live) but never spawned. The active list is
473
+ // derived from the archetype graph, so a defined-but-never-spawned
474
+ // component (InspY) is absent.
475
+ defineComponent('InspY', { w: 'u8' });
476
+ const world = new World();
477
+ world.spawn({ component: X, data: { v: 1 } });
478
+
479
+ const names = world.inspect().activeComponents;
480
+ expect(names).toContain('InspX');
481
+ expect(names).not.toContain('InspY');
482
+ });
483
+
484
+ it('systemCount increments after addSystem', () => {
485
+ const world = new World();
486
+ expect(world.inspect().systemCount).toBe(0);
487
+
488
+ world.addSystem(Update, {
489
+ name: 'inspSys1',
490
+ queries: [],
491
+ fn: () => {},
492
+ });
493
+ expect(world.inspect().systemCount).toBe(1);
494
+
495
+ world.addSystem(Update, {
496
+ name: 'inspSys2',
497
+ queries: [],
498
+ fn: () => {},
499
+ });
500
+ expect(world.inspect().systemCount).toBe(2);
501
+ });
502
+
503
+ it('resourceKeys reflects inserted and removed resources', () => {
504
+ const world = new World();
505
+ world.insertResource('timer', { elapsed: 0 });
506
+ world.insertResource('config', { debug: true });
507
+
508
+ // World constructor registers protected Time and FixedTime resources
509
+ // which appear alongside user-inserted resources.
510
+ const keys = world.inspect().resourceKeys;
511
+ expect(keys).toContain('timer');
512
+ expect(keys).toContain('config');
513
+ expect(keys.length).toBe(4);
514
+
515
+ world.removeResource('timer');
516
+ // Protected Time and FixedTime resources persist alongside user resources
517
+ const timeKeys = world.inspect().resourceKeys;
518
+ expect(timeKeys).toContain('Time');
519
+ expect(timeKeys).toContain('FixedTime');
520
+ expect(timeKeys).toContain('config');
521
+ });
522
+
523
+ it('archetype info componentNames matches spawned components', () => {
524
+ const P = defineComponent('InspPos2', { x: { type: 'f32' }, y: { type: 'f32' } });
525
+ const V = defineComponent('InspVel2', { vx: { type: 'f32' }, vy: { type: 'f32' } });
526
+ const world = new World();
527
+
528
+ // Multi-component spawn
529
+ world.spawn({ component: P, data: { x: 0, y: 0 } }, { component: V, data: { vx: 1, vy: 1 } });
530
+
531
+ const info = world.inspect();
532
+ expect(info.archetypeCount).toBe(1);
533
+ // biome-ignore lint/style/noNonNullAssertion: archetypeCount asserted to be 1 above
534
+ const arch = info.archetypes[0]!;
535
+ expect(arch.componentNames).toContain('InspPos2');
536
+ expect(arch.componentNames).toContain('InspVel2');
537
+ expect(arch.entityCount).toBe(1);
538
+ });
539
+ });
540
+
541
+ // M3 / w13 — AC-15: activeComponents is derived from the live archetype
542
+ // graph, so it reflects what each World has actually spawned, independent
543
+ // of any global registration / definition state.
544
+ describe('world.inspect().activeComponents (M3 AC-15)', () => {
545
+ it('a spawned component appears; a defined-but-never-spawned component does not', () => {
546
+ const Used = defineComponent('M3Used', { v: 'u8' });
547
+ // M3Unused is defined (lives in the global index) but never spawned.
548
+ defineComponent('M3Unused', { v: 'u8' });
549
+ const world = new World();
550
+ world.spawn({ component: Used, data: { v: 1 } });
551
+
552
+ const names = world.inspect().activeComponents;
553
+ expect(names).toContain('M3Used');
554
+ expect(names).not.toContain('M3Unused');
555
+ });
556
+
557
+ it('despawning the last holder drops the component from activeComponents', () => {
558
+ const Solo = defineComponent('M3Solo', { v: 'u8' });
559
+ const world = new World();
560
+ const e = world.spawn({ component: Solo, data: { v: 1 } }).unwrap();
561
+ expect(world.inspect().activeComponents).toContain('M3Solo');
562
+
563
+ world.despawn(e);
564
+ // The archetype now holds zero live entities, so the component is no
565
+ // longer active (active = present on a non-empty archetype).
566
+ expect(world.inspect().activeComponents).not.toContain('M3Solo');
567
+ });
568
+
569
+ it('two Worlds report independent activeComponents from the same global definitions', () => {
570
+ const A = defineComponent('M3CrossA', { v: 'u8' });
571
+ const B = defineComponent('M3CrossB', { w: 'u8' });
572
+ const worldA = new World();
573
+ const worldB = new World();
574
+ worldA.spawn({ component: A, data: { v: 1 } });
575
+ worldB.spawn({ component: B, data: { w: 2 } });
576
+
577
+ const namesA = worldA.inspect().activeComponents;
578
+ const namesB = worldB.inspect().activeComponents;
579
+ expect(namesA).toContain('M3CrossA');
580
+ expect(namesA).not.toContain('M3CrossB');
581
+ expect(namesB).toContain('M3CrossB');
582
+ expect(namesB).not.toContain('M3CrossA');
583
+ });
584
+ });
585
+ }
586
+ {
587
+ // --- from managed-array-carry-over.test.ts ---
588
+ // ---------------------------------------------------------------------------
589
+ // (a) addComponent triggers migrate; array<entity> bytes + length + capacity preserved.
590
+ // ---------------------------------------------------------------------------
591
+ // ---------------------------------------------------------------------------
592
+ // (b) array<f32, 16> fixed-capacity migrate carry-over.
593
+ // ---------------------------------------------------------------------------
594
+ // ---------------------------------------------------------------------------
595
+ // (c) removeComponent migrate carry-over (the reverse direction).
596
+ // ---------------------------------------------------------------------------
597
+ // ---------------------------------------------------------------------------
598
+ // (d) BufferPool slot id stays bit-equal across migrate (Negative invariant).
599
+ //
600
+ // Indirect observation: spawn entity A with array, capture A's bytes pre-
601
+ // migrate, trigger migrate, then spawn a foreign entity B of the same byte
602
+ // shape and verify A's bytes still survive afterwards. If migrate had
603
+ // erroneously released A's slot, A's bytes would be reset by B's
604
+ // alloc-and-zero path.
605
+ // ---------------------------------------------------------------------------
606
+ // ---------------------------------------------------------------------------
607
+ // (e) Sidecar count column survives migrate for variable arrays.
608
+ //
609
+ // addComponent migrate copies all u32 columns (including the sidecar
610
+ // `<fieldName>:count` column) row-by-row. After migrate the snapshot reads
611
+ // length from the new archetype's count column; if migrate had missed the
612
+ // count column, length would default to zero and the loop body would not
613
+ // see prior elements.
614
+ // ---------------------------------------------------------------------------
615
+ }
616
+ {
617
+ // --- from managed-array-element-type.test.ts ---
618
+ describe('array vocab — element-type runtime fail-safe (w2, AC-03)', () => {
619
+ it('array<ref<X>> at runtime throws managed-array-element-type-not-allowed', () => {
620
+ expect(() => {
621
+ defineComponent('Bad', {
622
+ bad: { type: 'array<ref<MaterialAsset>>' as unknown as SchemaFieldType },
623
+ });
624
+ }).toThrow(/managed-array-element-type-not-allowed/);
625
+ });
626
+
627
+ it('array<handle<X>> with non-empty tag is accepted at runtime (feat-20260608 M2 D-1)', () => {
628
+ expect(() => {
629
+ defineComponent('Good', {
630
+ good: { type: 'array<shared<MeshAsset>>' as unknown as SchemaFieldType },
631
+ });
632
+ }).not.toThrow();
633
+ });
634
+
635
+ it('array<buffer:N> at runtime throws managed-array-element-type-not-allowed', () => {
636
+ expect(() => {
637
+ defineComponent('Bad', { bad: { type: 'array<buffer:8>' as unknown as SchemaFieldType } });
638
+ }).toThrow(/managed-array-element-type-not-allowed/);
639
+ });
640
+
641
+ it('array<array<T,N>> nesting at runtime throws managed-array-element-type-not-allowed', () => {
642
+ expect(() => {
643
+ defineComponent('Bad', {
644
+ bad: { type: 'array<array<f32,4>>' as unknown as SchemaFieldType },
645
+ });
646
+ }).toThrow(/managed-array-element-type-not-allowed/);
647
+ });
648
+
649
+ it('legal element types do not throw: array<entity> / array<f32> / array<u32, 16>', () => {
650
+ expect(() => defineComponent('Foo', { entities: { type: 'array<entity>' } })).not.toThrow();
651
+ expect(() => defineComponent('Bar', { values: { type: 'array<f32>' } })).not.toThrow();
652
+ expect(() => defineComponent('Baz', { mat: { type: 'array<f32, 16>' } })).not.toThrow();
653
+ expect(() => defineComponent('Qux', { flags: { type: 'array<bool>' } })).not.toThrow();
654
+ });
655
+ });
656
+
657
+ // ---------------------------------------------------------------------------
658
+ // w2 (M2) --- array<string> rejection (OOS-04 lock-out + R-P2 mitigation).
659
+ //
660
+ // AC-10 extends the bare 'string' keyword to the SchemaVocabKeyword closed
661
+ // union, but OOS-04 explicitly rules out `array<string>` at this stage:
662
+ // arrays of variable-length references are a separate design (would need
663
+ // per-element BufferPool slot tracking + per-row release loops). Locking it
664
+ // out at registration time prevents AI users from accidentally defining a
665
+ // schema we cannot back. The runtime guard piggybacks on the existing
666
+ // MANAGED_ARRAY_ELEMENT_TYPES whitelist --- adding 'string' here would
667
+ // silently flip the policy, so we also assert the set membership / size
668
+ // stays at the 12-member baseline (R-P2 mitigation: drift detector).
669
+ // ---------------------------------------------------------------------------
670
+
671
+ describe('array vocab --- string element rejection (w2 M2, OOS-04)', () => {
672
+ it('array<string> at runtime throws managed-array-element-type-not-allowed', () => {
673
+ expect(() => {
674
+ defineComponent('Bad', { bad: { type: 'array<string>' as unknown as SchemaFieldType } });
675
+ }).toThrow(/managed-array-element-type-not-allowed/);
676
+ });
677
+
678
+ it('array<string, 4> fixed form is also rejected', () => {
679
+ expect(() => {
680
+ defineComponent('Bad', { bad: { type: 'array<string, 4>' as unknown as SchemaFieldType } });
681
+ }).toThrow(/managed-array-element-type-not-allowed/);
682
+ });
683
+
684
+ it('MANAGED_ARRAY_ELEMENT_TYPES still has 12 members and excludes "string"', () => {
685
+ expect(MANAGED_ARRAY_ELEMENT_TYPES.size).toBe(12);
686
+ expect(MANAGED_ARRAY_ELEMENT_TYPES.has('string' as never)).toBe(false);
687
+ // Explicit allow-list snapshot --- any drift in the 12 members shows up
688
+ // as a diff here, signalling a vocab evolution that needs design review.
689
+ expect([...MANAGED_ARRAY_ELEMENT_TYPES].sort()).toEqual(
690
+ [
691
+ 'bool',
692
+ 'enum',
693
+ 'entity',
694
+ 'f32',
695
+ 'f64',
696
+ 'i16',
697
+ 'i32',
698
+ 'i8',
699
+ 'ref',
700
+ 'u16',
701
+ 'u32',
702
+ 'u8',
703
+ ].sort(),
704
+ );
705
+ });
706
+ });
707
+ }
708
+ {
709
+ // --- from managed-array-errors.test.ts ---
710
+ // Error routing is now returned at the mutation boundary; there is no
711
+ // World-level ErrorHandler registration.
712
+
713
+ // ---------------------------------------------------------------------------
714
+ // (a) fixed-size-mismatch -- error class shape.
715
+ // ---------------------------------------------------------------------------
716
+
717
+ describe('w12 - fixed-size-mismatch error class', () => {
718
+ it('FixedSizeMismatchError carries .code + detail.expected / detail.actual', () => {
719
+ const err = new FixedSizeMismatchError('mat', 16, 15);
720
+ expect(err.code).toBe('fixed-size-mismatch');
721
+ expect(err.detail).toEqual({ expected: 16, actual: 15 });
722
+ // expectType: detail narrows to fixed-size-mismatch shape.
723
+ const detail: Extract<EcsErrorDetail, { code: 'fixed-size-mismatch' }> = {
724
+ code: 'fixed-size-mismatch',
725
+ ...err.detail,
726
+ };
727
+ expect(detail.expected).toBe(16);
728
+ });
729
+ });
730
+
731
+ // ---------------------------------------------------------------------------
732
+ // (d) world.pop on a non-empty variable array succeeds and shrinks count.
733
+ // ---------------------------------------------------------------------------
734
+
735
+ // ---------------------------------------------------------------------------
736
+ // (e) managed-array-element-type-not-allowed (surviving member; sample path).
737
+ // ---------------------------------------------------------------------------
738
+
739
+ describe('w12 - managed-array-element-type-not-allowed surviving member', () => {
740
+ it('defineComponent throws ManagedArrayElementTypeNotAllowedError on illegal element', () => {
741
+ let captured: ManagedArrayElementTypeNotAllowedError | null = null;
742
+ try {
743
+ defineComponent('Bad', { bad: { type: 'array<ref<X>>' as unknown as SchemaFieldType } });
744
+ } catch (err) {
745
+ captured = err as ManagedArrayElementTypeNotAllowedError;
746
+ }
747
+ expect(captured).not.toBeNull();
748
+ if (captured === null) return;
749
+ expect(captured.code).toBe('managed-array-element-type-not-allowed');
750
+ expect(captured.detail.fieldName).toBe('bad');
751
+ expect(captured.detail.elementType).toBe('ref<X>'); // gate-allow:ecs-brand (illegal-element-type-not-allowed parser rejection fixture)
752
+ });
753
+ });
754
+
755
+ // ---------------------------------------------------------------------------
756
+ // (f) Exhaustive switch over the surviving core collapsed-vocab code.
757
+ // ---------------------------------------------------------------------------
758
+
759
+ describe('w12 - exhaustive switch over collapsed-vocab codes', () => {
760
+ it('covers the fixed-size member', () => {
761
+ const codes: EcsErrorCode[] = ['fixed-size-mismatch'];
762
+ let hits = 0;
763
+ for (const code of codes) {
764
+ switch (code) {
765
+ case 'fixed-size-mismatch':
766
+ hits += 1;
767
+ break;
768
+ default:
769
+ // The exhaustive narrowing is performed by `EcsErrorCode` over the
770
+ // entire union; this default arm is unreachable for the code
771
+ // listed above. We use `void` rather than assertNever to keep the
772
+ // test focused on collapsed-vocab membership without coupling to
773
+ // the remaining members.
774
+ void code;
775
+ }
776
+ }
777
+ expect(hits).toBe(1);
778
+ });
779
+ });
780
+ }
781
+ {
782
+ // --- from managed-array-release.test.ts ---
783
+ function readSlotId(
784
+ world: World,
785
+ entityRaw: number,
786
+ component: Component,
787
+ fieldName: string,
788
+ ): number {
789
+ const graph = world[worldInternal].getGraph();
790
+ const slotIndex = entityRaw & 0xffffff; // lower 24 bits -- index slot
791
+ for (const arch of graph.archetypes) {
792
+ if (!arch) continue;
793
+ const table = graph.tables[arch.tableId];
794
+ if (table === undefined) continue;
795
+ for (let archetypeRow = 0; archetypeRow < arch.size; archetypeRow++) {
796
+ const row = arch.rows[archetypeRow] ?? 0;
797
+ // Entity identity read from id=0 self column (not a separate entities array)
798
+ const selfVal = table.storage.get(0)?.fields.get('self')?.view[row];
799
+ const idx = typeof selfVal === 'number' ? selfVal & 0xffffff : 0;
800
+ if (idx === slotIndex) {
801
+ for (let i = 0; i < arch.components.length; i++) {
802
+ if (arch.components[i] === component) {
803
+ for (const { fields: fieldCols } of table.storage.values()) {
804
+ const col = fieldCols.get(fieldName);
805
+ if (col !== undefined && fieldCols.has(fieldName)) {
806
+ return col.view[row] as number;
807
+ }
808
+ }
809
+ }
810
+ }
811
+ }
812
+ }
813
+ }
814
+ return 0;
815
+ }
816
+
817
+ // ---------------------------------------------------------------------------
818
+ // Helper -- the BufferPool's live (alloc'd, not-yet-released) slot count.
819
+ // feat-20260602: fixed `array<T,N>` stores its elements INLINE in the
820
+ // archetype column, so it never allocs / releases a pool slot. The three
821
+ // release paths assert `_liveCount() === 0` end-to-end for the fixed shape
822
+ // (the inline migration superseded the prior slot-id + freelist-reuse
823
+ // contract these blocks used to lock).
824
+ // ---------------------------------------------------------------------------
825
+
826
+ function liveCount(world: World): number {
827
+ return (world as unknown as { bufferPool: { _liveCount(): number } }).bufferPool._liveCount();
828
+ }
829
+
830
+ // ---------------------------------------------------------------------------
831
+ // Path 1 -- world.despawn(e) releases array fields; slot id reusable.
832
+ // ---------------------------------------------------------------------------
833
+
834
+ describe('w12 - array<entity> variable: despawn releases slot; freelist reuses id', () => {
835
+ it('despawn frees the slot; same-shape spawn pops the freed id', () => {
836
+ const Children = defineComponent('Children', { entities: { type: 'array<entity>' } });
837
+ const w = new World();
838
+
839
+ // Spawn A with 4 elements (16 bytes -> bucket 0 / size class 16).
840
+ const a = w
841
+ .spawn({
842
+ component: Children,
843
+ data: { entities: new Uint32Array([1, 2, 3, 4]) },
844
+ })
845
+ .unwrap();
846
+ const aSlotId = readSlotId(w, handleNumeric(a), Children, 'entities');
847
+ expect(aSlotId).toBeGreaterThan(0);
848
+
849
+ // Despawn A; the array field's slot is released to its bucket free-list.
850
+ w.despawn(a).unwrap();
851
+
852
+ // Spawn B with the same element count -> same bucket -> LIFO pop returns
853
+ // A's freed slot id.
854
+ const b = w
855
+ .spawn({
856
+ component: Children,
857
+ data: { entities: new Uint32Array([10, 20, 30, 40]) },
858
+ })
859
+ .unwrap();
860
+ const bSlotId = readSlotId(w, handleNumeric(b), Children, 'entities');
861
+ expect(bSlotId).toBe(aSlotId);
862
+ });
863
+ });
864
+
865
+ describe('w12 - array<f32,N> fixed: despawn touches no pool slot (inline)', () => {
866
+ it('spawn + despawn of an inline fixed array never allocs a pool slot', () => {
867
+ const Mat4 = defineComponent('Mat4', { mat: 'array<f32, 16>' });
868
+ const w = new World();
869
+
870
+ const init = new Float32Array(16);
871
+ for (let i = 0; i < 16; i++) init[i] = i + 1;
872
+
873
+ const a = w.spawn({ component: Mat4, data: { mat: init } }).unwrap();
874
+ // Inline column: no BufferPool slot is allocated for the fixed field.
875
+ expect(liveCount(w)).toBe(0);
876
+
877
+ w.despawn(a).unwrap();
878
+ expect(liveCount(w)).toBe(0);
879
+
880
+ // Same-shape respawn still touches no slot, and reads back its payload.
881
+ const b = w.spawn({ component: Mat4, data: { mat: new Float32Array(16) } }).unwrap();
882
+ expect(liveCount(w)).toBe(0);
883
+ const got = w.get(b, Mat4);
884
+ if (!got.ok) throw new Error('expected ok');
885
+ expect(got.value.mat[0]).toBe(0);
886
+ });
887
+ });
888
+
889
+ // ---------------------------------------------------------------------------
890
+ // Path 2 -- world.removeComponent(e, C) releases array fields on C only.
891
+ // ---------------------------------------------------------------------------
892
+
893
+ describe('w12 - array<entity> variable: removeComponent releases slot', () => {
894
+ it('removeComponent frees the array slot; freelist reuses id', () => {
895
+ const Children = defineComponent('Children', { entities: { type: 'array<entity>' } });
896
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
897
+ const w = new World();
898
+
899
+ const a = w
900
+ .spawn(
901
+ {
902
+ component: Children,
903
+ data: { entities: new Uint32Array([1, 2, 3, 4]) },
904
+ },
905
+ { component: Anchor, data: { x: 7 } },
906
+ )
907
+ .unwrap();
908
+ const aSlotId = readSlotId(w, handleNumeric(a), Children, 'entities');
909
+ expect(aSlotId).toBeGreaterThan(0);
910
+
911
+ w.removeComponent(a, Children).unwrap();
912
+
913
+ const b = w
914
+ .spawn({
915
+ component: Children,
916
+ data: { entities: new Uint32Array([10, 20, 30, 40]) },
917
+ })
918
+ .unwrap();
919
+ const bSlotId = readSlotId(w, handleNumeric(b), Children, 'entities');
920
+ expect(bSlotId).toBe(aSlotId);
921
+ });
922
+ });
923
+
924
+ describe('w12 - array<f32,N> fixed: removeComponent touches no pool slot (inline)', () => {
925
+ it('removeComponent of an inline fixed array never allocs a pool slot', () => {
926
+ const Mat4 = defineComponent('Mat4', { mat: 'array<f32, 16>' });
927
+ const Anchor = defineComponent('Anchor', { x: 'f32' });
928
+ const w = new World();
929
+
930
+ const init = new Float32Array(16);
931
+ for (let i = 0; i < 16; i++) init[i] = i + 1;
932
+
933
+ const a = w
934
+ .spawn({ component: Mat4, data: { mat: init } }, { component: Anchor, data: { x: 7 } })
935
+ .unwrap();
936
+ expect(liveCount(w)).toBe(0);
937
+
938
+ w.removeComponent(a, Mat4).unwrap();
939
+ expect(liveCount(w)).toBe(0);
940
+
941
+ const b = w.spawn({ component: Mat4, data: { mat: new Float32Array(16) } }).unwrap();
942
+ expect(liveCount(w)).toBe(0);
943
+ const got = w.get(b, Mat4);
944
+ if (!got.ok) throw new Error('expected ok');
945
+ expect(got.value.mat.length).toBe(16);
946
+ });
947
+ });
948
+
949
+ // ---------------------------------------------------------------------------
950
+ // Path 3 -- world.set(e, C, { arr: newValue }) releases prior + allocs new.
951
+ // ---------------------------------------------------------------------------
952
+
953
+ describe('w12 - array<entity> variable: set field overwrite releases prior slot', () => {
954
+ it('set frees the prior array slot before allocating the new one', () => {
955
+ const Children = defineComponent('Children', { entities: { type: 'array<entity>' } });
956
+ const w = new World();
957
+
958
+ const a = w
959
+ .spawn({
960
+ component: Children,
961
+ data: { entities: new Uint32Array([1, 2, 3, 4]) },
962
+ })
963
+ .unwrap();
964
+ const oldSlotId = readSlotId(w, handleNumeric(a), Children, 'entities');
965
+ expect(oldSlotId).toBeGreaterThan(0);
966
+
967
+ // Overwrite the array field with a brand-new payload of the same byte
968
+ // size -> set path releases oldSlotId, then alloc(16) pops oldSlotId
969
+ // from the freelist and the column re-stores the SAME id (LIFO same-
970
+ // bucket reuse, D-7).
971
+ w.set(a, Children, { entities: new Uint32Array([100, 200, 300, 400]) }).unwrap();
972
+
973
+ const newSlotId = readSlotId(w, handleNumeric(a), Children, 'entities');
974
+ expect(newSlotId).toBe(oldSlotId);
975
+
976
+ // Bytes match the new payload (proves the slot was reset post-release).
977
+ const got = w.get(a, Children);
978
+ if (!got.ok) throw new Error('expected ok');
979
+ const snap = got.value.entities;
980
+ expect(snap.length).toBe(4);
981
+ expect(snap[0]).toBe(100);
982
+ expect(snap[3]).toBe(400);
983
+ });
984
+ });
985
+
986
+ describe('w12 - array<f32,N> fixed: set field overwrite is inline (no pool slot)', () => {
987
+ it('set overwrites the inline payload in place without touching the pool', () => {
988
+ const Mat4 = defineComponent('Mat4', { mat: 'array<f32, 16>' });
989
+ const w = new World();
990
+
991
+ const init = new Float32Array(16);
992
+ for (let i = 0; i < 16; i++) init[i] = i + 1;
993
+
994
+ const a = w.spawn({ component: Mat4, data: { mat: init } }).unwrap();
995
+ expect(liveCount(w)).toBe(0);
996
+
997
+ const next = new Float32Array(16);
998
+ for (let i = 0; i < 16; i++) next[i] = (i + 1) * 100;
999
+ w.set(a, Mat4, { mat: next }).unwrap();
1000
+ // Inline set is a verbatim in-place row write -- no alloc / release.
1001
+ expect(liveCount(w)).toBe(0);
1002
+
1003
+ const got = w.get(a, Mat4);
1004
+ if (!got.ok) throw new Error('expected ok');
1005
+ const snap = got.value.mat;
1006
+ expect(snap[0]).toBe(100);
1007
+ expect(snap[15]).toBe(1600);
1008
+ });
1009
+ });
1010
+
1011
+ // ---------------------------------------------------------------------------
1012
+ // Boundary -- releasing one entity does NOT disturb a sibling at a different
1013
+ // row in the same archetype + same column.
1014
+ // ---------------------------------------------------------------------------
1015
+
1016
+ describe('w12 - sibling-entity isolation: despawn one row does not touch another', () => {
1017
+ it("despawn(a) keeps b's array bytes + slot id intact", () => {
1018
+ const Children = defineComponent('Children', { entities: { type: 'array<entity>' } });
1019
+ const w = new World();
1020
+
1021
+ const a = w
1022
+ .spawn({
1023
+ component: Children,
1024
+ data: { entities: new Uint32Array([1, 2, 3, 4]) },
1025
+ })
1026
+ .unwrap();
1027
+ const b = w
1028
+ .spawn({
1029
+ component: Children,
1030
+ data: { entities: new Uint32Array([10, 20, 30, 40]) },
1031
+ })
1032
+ .unwrap();
1033
+
1034
+ const bSlotIdBefore = readSlotId(w, handleNumeric(b), Children, 'entities');
1035
+ expect(bSlotIdBefore).toBeGreaterThan(0);
1036
+
1037
+ // Despawn A -- only A's row's slot is released.
1038
+ w.despawn(a).unwrap();
1039
+
1040
+ // B's data + slot id stay intact.
1041
+ const bSlotIdAfter = readSlotId(w, handleNumeric(b), Children, 'entities');
1042
+ expect(bSlotIdAfter).toBe(bSlotIdBefore);
1043
+
1044
+ const got = w.get(b, Children);
1045
+ if (!got.ok) throw new Error('expected ok');
1046
+ const snap = got.value.entities;
1047
+ expect(snap.length).toBe(4);
1048
+ expect(snap[0]).toBe(10);
1049
+ expect(snap[3]).toBe(40);
1050
+ });
1051
+ });
1052
+ }
1053
+ // Managed-array stride validation belongs to render consumers, not ECS.
1054
+ {
1055
+ // --- from managed-array-vocab.test.ts ---
1056
+ describe('array vocab - keyword recognition (w12, AC-01)', () => {
1057
+ it('array<entity> derives to Uint32Array (Entity column storage)', () => {
1058
+ expectTypeOf<FieldValueType<'array<entity>'>>().toEqualTypeOf<Uint32Array>();
1059
+ });
1060
+
1061
+ it('array<f32, 16> derives to Float32Array (fixed-capacity)', () => {
1062
+ expectTypeOf<FieldValueType<'array<f32, 16>'>>().toEqualTypeOf<Float32Array>();
1063
+ });
1064
+
1065
+ it('array<u32> derives to Uint32Array', () => {
1066
+ expectTypeOf<FieldValueType<'array<u32>'>>().toEqualTypeOf<Uint32Array>();
1067
+ });
1068
+
1069
+ it('array<i32, 4> derives to Int32Array (fixed-capacity i32)', () => {
1070
+ expectTypeOf<FieldValueType<'array<i32, 4>'>>().toEqualTypeOf<Int32Array>();
1071
+ });
1072
+
1073
+ it('array<bool> derives to Uint8Array', () => {
1074
+ expectTypeOf<FieldValueType<'array<bool>'>>().toEqualTypeOf<Uint8Array>();
1075
+ });
1076
+
1077
+ it('ShapeOf threads array vocab through FieldValueType', () => {
1078
+ type S = {
1079
+ entities: 'array<entity>';
1080
+ mat: 'array<f32, 16>';
1081
+ indices: 'array<u32>';
1082
+ };
1083
+ type Expected = {
1084
+ entities: Uint32Array;
1085
+ mat: Float32Array;
1086
+ indices: Uint32Array;
1087
+ };
1088
+ expectTypeOf<ShapeOf<S>>().toEqualTypeOf<Expected>();
1089
+ });
1090
+ });
1091
+
1092
+ describe('array vocab - three-application-point inference (w12, AC-01)', () => {
1093
+ // Application point (a) -- inside world.addSystem fn callback. The system
1094
+ // closure reads the array field via `world.get(e, C).unwrap().entities`;
1095
+ // the inferred type must be `Uint32Array` without an `as` cast.
1096
+ it('application point (a) -- inside world.addSystem fn callback', () => {
1097
+ type Foo = ReturnType<typeof defineComponent<'Foo', { entities: 'array<entity>' }>>;
1098
+ type FooShape = ShapeOf<SchemaOf<Foo>>;
1099
+ // expectType: array<entity> resolves to Uint32Array at the system call site.
1100
+ expectTypeOf<FooShape['entities']>().toEqualTypeOf<Uint32Array>();
1101
+ const dummy: FooShape['entities'] = new Uint32Array(0);
1102
+ expectTypeOf(dummy).toEqualTypeOf<Uint32Array>();
1103
+ });
1104
+
1105
+ // Application point (b) -- inside a QueryRow loop (same world.get path,
1106
+ // different host scope; type derivation must not regress when `entities`
1107
+ // crosses a function boundary).
1108
+ it('application point (b) -- inside a QueryRow loop', () => {
1109
+ type Foo = ReturnType<typeof defineComponent<'Foo', { entities: 'array<entity>' }>>;
1110
+ type FooShape = ShapeOf<SchemaOf<Foo>>;
1111
+ // expectType: QueryRow access sees the same Uint32Array shape as system fn.
1112
+ expectTypeOf<FooShape['entities']>().toEqualTypeOf<Uint32Array>();
1113
+ });
1114
+
1115
+ // Application point (c) -- direct world.get call site (the simplest path,
1116
+ // outside any system / query). The explicit type annotation pins the
1117
+ // TypedArray inference to the schema literal.
1118
+ it('application point (c) -- direct world.get call site', () => {
1119
+ type Foo = ReturnType<typeof defineComponent<'Foo', { entities: 'array<entity>' }>>;
1120
+ type FooShape = ShapeOf<SchemaOf<Foo>>;
1121
+ // expectType: direct world.get(e, C).unwrap().entities matches TypedArrayFor<'u32'>.
1122
+ const arr: TypedArrayFor<'u32'> = new Uint32Array(0);
1123
+ expectTypeOf(arr).toEqualTypeOf<FooShape['entities']>();
1124
+ });
1125
+
1126
+ it('World type still resolves (no parser regression)', () => {
1127
+ expectTypeOf<World>().toBeObject();
1128
+ });
1129
+ });
1130
+
1131
+ describe('array vocab - cross-shape closure on TypedArray surface (w12, AC-02)', () => {
1132
+ it('Float32Array exposes `length` indexed access; no `push` / `pop` / `count`', () => {
1133
+ type FA = Float32Array;
1134
+ expectTypeOf<FA>().toHaveProperty('length');
1135
+ expectTypeOf<FA>().toHaveProperty('subarray');
1136
+ // The TypedArray surface is the SSOT here -- AI users mutate the field
1137
+ // through `world.push` / `world.pop` / `world.set`, NOT the snapshot.
1138
+ });
1139
+
1140
+ it('Uint32Array indexed read returns number', () => {
1141
+ type UA = Uint32Array;
1142
+ expectTypeOf<UA[number]>().toEqualTypeOf<number>();
1143
+ });
1144
+ });
1145
+
1146
+ describe('array vocab - element-type rejection (w12, AC-03)', () => {
1147
+ it('array<ref<X>> is not a recognised schema field type', () => {
1148
+ // @ts-expect-error 'array<ref<X>>' is not a SchemaFieldType (AC-03).
1149
+ const bad: SchemaFieldType = 'array<ref<MaterialAsset>>';
1150
+ void bad;
1151
+ });
1152
+
1153
+ it('array<handle<X>> is now a recognised schema field type (feat-20260608 M2 D-1)', () => {
1154
+ const valid: SchemaFieldType = 'array<shared<MeshAsset>>';
1155
+ void valid;
1156
+ });
1157
+
1158
+ it('array<array<f32,4>> nesting is not a recognised schema field type', () => {
1159
+ // @ts-expect-error 'array<array<f32,4>>' is not a SchemaFieldType (AC-03).
1160
+ const bad: SchemaFieldType = 'array<array<f32,4>>';
1161
+ void bad;
1162
+ });
1163
+ });
1164
+
1165
+ describe('array vocab - cross-brand rejection (w12, AC-04)', () => {
1166
+ it("Entity packed u32 is not assignable to Handle<Mesh, 'unique'>", () => {
1167
+ const takesMeshHandle = (h: Handle<'Mesh', 'unique'>): void => {
1168
+ void h;
1169
+ };
1170
+ const view = new Uint32Array(1) as TypedArrayFor<'u32'>;
1171
+ const elem: number = view[0] ?? 0;
1172
+ // @ts-expect-error number is not assignable to Handle<'Mesh','unique'> (AC-04).
1173
+ takesMeshHandle(elem);
1174
+ });
1175
+ });
1176
+ }
1177
+ {
1178
+ // --- from managed-buffer-grow.test.ts ---
1179
+ function fillSequential(view: Uint8Array, base: number): void {
1180
+ for (let i = 0; i < view.byteLength; i++) view[i] = (i + base) & 0xff;
1181
+ }
1182
+
1183
+ describe('w12 - AC-07 BufferPool.grow byte-equal carry-over', () => {
1184
+ it('cross-bucket grow (128 -> 1024) keeps first 128 bytes byte-equal', () => {
1185
+ const pool = new BufferPool();
1186
+ const a = pool.alloc(128);
1187
+ if (!a.ok) throw new Error('expected ok alloc');
1188
+ fillSequential(a.value.view, 1);
1189
+ const r = pool.grow(a.value.id, 1024);
1190
+ if (!r.ok) throw new Error('expected ok grow');
1191
+ expect(r.value.byteLength).toBe(1024);
1192
+ for (let i = 0; i < 128; i++) expect(r.value[i]).toBe((i + 1) & 0xff);
1193
+ // The grown tail bytes are zero-filled by the fresh ArrayBuffer.
1194
+ for (let i = 128; i < 1024; i++) expect(r.value[i]).toBe(0);
1195
+ });
1196
+
1197
+ it('same-bucket grow (128 -> 200) keeps first 128 bytes byte-equal', () => {
1198
+ const pool = new BufferPool();
1199
+ const a = pool.alloc(128);
1200
+ if (!a.ok) throw new Error('expected ok alloc');
1201
+ fillSequential(a.value.view, 17);
1202
+ const r = pool.grow(a.value.id, 200);
1203
+ if (!r.ok) throw new Error('expected ok grow');
1204
+ expect(r.value.byteLength).toBe(200);
1205
+ for (let i = 0; i < 128; i++) expect(r.value[i]).toBe((i + 17) & 0xff);
1206
+ });
1207
+
1208
+ it('multi-step grow (128 -> 1024 -> 4096) preserves accumulated content', () => {
1209
+ const pool = new BufferPool();
1210
+ const a = pool.alloc(128);
1211
+ if (!a.ok) throw new Error('expected ok alloc');
1212
+ fillSequential(a.value.view, 5);
1213
+
1214
+ const r1 = pool.grow(a.value.id, 1024);
1215
+ if (!r1.ok) throw new Error('expected ok grow1');
1216
+ // Write a second pattern over the newly-available range.
1217
+ for (let i = 128; i < 1024; i++) r1.value[i] = (i ^ 0x5a) & 0xff;
1218
+
1219
+ const r2 = pool.grow(a.value.id, 4096);
1220
+ if (!r2.ok) throw new Error('expected ok grow2');
1221
+ expect(r2.value.byteLength).toBe(4096);
1222
+ // Original prefix preserved.
1223
+ for (let i = 0; i < 128; i++) expect(r2.value[i]).toBe((i + 5) & 0xff);
1224
+ // Mid-range (set in r1) preserved across the second grow.
1225
+ for (let i = 128; i < 1024; i++) expect(r2.value[i]).toBe((i ^ 0x5a) & 0xff);
1226
+ // Tail past the prior view length is zero-filled.
1227
+ for (let i = 1024; i < 4096; i++) expect(r2.value[i]).toBe(0);
1228
+ });
1229
+ });
1230
+ }
1231
+ {
1232
+ // --- from managed-carry-over.test.ts ---
1233
+ function refsOf(w: World): UniqueRefStore {
1234
+ return (w as unknown as { uniqueRefs: UniqueRefStore }).uniqueRefs;
1235
+ }
1236
+
1237
+ function fillSequential(view: Uint8Array, base: number): void {
1238
+ for (let i = 0; i < view.byteLength; i++) view[i] = (i + base) & 0xff;
1239
+ }
1240
+
1241
+ function expectBytesEqual(a: Uint8Array, b: Uint8Array, len: number): void {
1242
+ for (let i = 0; i < len; i++) {
1243
+ expect(a[i]).toBe(b[i]);
1244
+ }
1245
+ }
1246
+
1247
+ afterEach(() => {
1248
+ vi.restoreAllMocks();
1249
+ });
1250
+
1251
+ // ---------------------------------------------------------------------------
1252
+ // (a) ref<T> carry-over - Object.is on resolved payload survives migrate.
1253
+ // ---------------------------------------------------------------------------
1254
+
1255
+ describe('w16 - ref<T> field archetype migrate carry-over (AC-04)', () => {
1256
+ it('addComponent triggers migrate; ref<T> handle u32 stays bit-equal', () => {
1257
+ const Mat = defineComponent('Mat', { handle: { type: 'unique<MaterialAsset>' } });
1258
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1259
+ const w = new World();
1260
+ const store = refsOf(w);
1261
+ const payload = { id: 7 };
1262
+ const h = store.alloc('MaterialAsset', payload);
1263
+ const e = w
1264
+ .spawn({
1265
+ component: Mat,
1266
+ data: { handle: h },
1267
+ })
1268
+ .unwrap();
1269
+
1270
+ const before = w.get(e, Mat);
1271
+ if (!before.ok) throw new Error('expected ok get before');
1272
+ const handleBefore = handleNumeric(before.value.handle);
1273
+
1274
+ // Trigger archetype migrate by adding a second component.
1275
+ w.addComponent(e, { component: Anchor, data: { x: 1 } }).unwrap();
1276
+
1277
+ const after = w.get(e, Mat);
1278
+ if (!after.ok) throw new Error('expected ok get after');
1279
+ const handleAfter = handleNumeric(after.value.handle);
1280
+
1281
+ expect(Object.is(handleBefore, handleAfter)).toBe(true);
1282
+ // Resolved payload is the same object reference (store.resolve returns
1283
+ // the SAME payload object on every call; AC-04 prelude in
1284
+ // managed-release.test.ts).
1285
+ const resolvedBefore = store.resolve(before.value.handle);
1286
+ const resolvedAfter = store.resolve(after.value.handle);
1287
+ if (!resolvedBefore.ok || !resolvedAfter.ok) {
1288
+ throw new Error('expected resolved x2');
1289
+ }
1290
+ expect(Object.is(resolvedBefore.value, resolvedAfter.value)).toBe(true);
1291
+ expect(resolvedAfter.value).toBe(payload);
1292
+ });
1293
+
1294
+ it('removeComponent triggers migrate; surviving ref<T> handle u32 stays bit-equal', () => {
1295
+ const Mat = defineComponent('Mat', { handle: { type: 'unique<MaterialAsset>' } });
1296
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1297
+ const w = new World();
1298
+ const store = refsOf(w);
1299
+ const payload = { id: 11 };
1300
+ const h = store.alloc('MaterialAsset', payload);
1301
+ const e = w
1302
+ .spawn(
1303
+ {
1304
+ component: Mat,
1305
+ data: { handle: h },
1306
+ },
1307
+ { component: Anchor, data: { x: 0 } },
1308
+ )
1309
+ .unwrap();
1310
+
1311
+ const before = w.get(e, Mat);
1312
+ if (!before.ok) throw new Error('expected ok get before');
1313
+ const handleBefore = handleNumeric(before.value.handle);
1314
+
1315
+ // Removing Anchor migrates the entity; Mat (with managed ref) survives.
1316
+ w.removeComponent(e, Anchor).unwrap();
1317
+
1318
+ const after = w.get(e, Mat);
1319
+ if (!after.ok) throw new Error('expected ok get after');
1320
+ const handleAfter = handleNumeric(after.value.handle);
1321
+
1322
+ expect(Object.is(handleBefore, handleAfter)).toBe(true);
1323
+ const r = store.resolve(after.value.handle);
1324
+ if (!r.ok) throw new Error('expected resolved');
1325
+ expect(r.value).toBe(payload);
1326
+ });
1327
+
1328
+ it('multi-hop migrate (add then remove) keeps ref<T> handle u32 bit-equal end-to-end', () => {
1329
+ const Mat = defineComponent('Mat', { handle: { type: 'unique<MaterialAsset>' } });
1330
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1331
+ const Tag = defineComponent('Tag', { v: { type: 'u32' } });
1332
+ const w = new World();
1333
+ const store = refsOf(w);
1334
+ const payload = { id: 99 };
1335
+ const h = store.alloc('MaterialAsset', payload);
1336
+ const e = w
1337
+ .spawn({
1338
+ component: Mat,
1339
+ data: { handle: h },
1340
+ })
1341
+ .unwrap();
1342
+ const r0 = w.get(e, Mat);
1343
+ if (!r0.ok) throw new Error('expected r0');
1344
+ const handle0 = handleNumeric(r0.value.handle);
1345
+
1346
+ // Hop 1: add Anchor.
1347
+ w.addComponent(e, { component: Anchor, data: { x: 2 } }).unwrap();
1348
+ const r1 = w.get(e, Mat);
1349
+ if (!r1.ok) throw new Error('expected r1');
1350
+ expect(Object.is(handleNumeric(r1.value.handle), handle0)).toBe(true);
1351
+
1352
+ // Hop 2: add Tag.
1353
+ w.addComponent(e, { component: Tag, data: { v: 42 } }).unwrap();
1354
+ const r2 = w.get(e, Mat);
1355
+ if (!r2.ok) throw new Error('expected r2');
1356
+ expect(Object.is(handleNumeric(r2.value.handle), handle0)).toBe(true);
1357
+
1358
+ // Hop 3: remove Anchor.
1359
+ w.removeComponent(e, Anchor).unwrap();
1360
+ const r3 = w.get(e, Mat);
1361
+ if (!r3.ok) throw new Error('expected r3');
1362
+ expect(Object.is(handleNumeric(r3.value.handle), handle0)).toBe(true);
1363
+
1364
+ // Hop 4: remove Tag.
1365
+ w.removeComponent(e, Tag).unwrap();
1366
+ const r4 = w.get(e, Mat);
1367
+ if (!r4.ok) throw new Error('expected r4');
1368
+ expect(Object.is(handleNumeric(r4.value.handle), handle0)).toBe(true);
1369
+
1370
+ // Final resolve still returns the original payload.
1371
+ const resolved = store.resolve(r4.value.handle);
1372
+ if (!resolved.ok) throw new Error('expected resolved');
1373
+ expect(resolved.value).toBe(payload);
1374
+ });
1375
+ });
1376
+
1377
+ // ---------------------------------------------------------------------------
1378
+ // (b) buffer:<N> field carry-over - bytes preserved byte-equal across migrate.
1379
+ // ---------------------------------------------------------------------------
1380
+
1381
+ describe('w16 - buffer:<N> field archetype migrate carry-over (AC-04)', () => {
1382
+ it('addComponent migrate keeps buffer payload byte-equal (post-migrate view)', () => {
1383
+ const Skin = defineComponent('Skin', { palette: { type: 'buffer<128>' } });
1384
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1385
+ const w = new World();
1386
+ const seed = new Uint8Array(128);
1387
+ fillSequential(seed, 13);
1388
+ const e = w.spawn({ component: Skin, data: { palette: seed } }).unwrap();
1389
+
1390
+ const before = w.get(e, Skin);
1391
+ if (!before.ok) throw new Error('expected ok get before');
1392
+ const snapshot = new Uint8Array(before.value.palette); // copy for comparison.
1393
+
1394
+ w.addComponent(e, { component: Anchor, data: { x: 0 } }).unwrap();
1395
+
1396
+ const after = w.get(e, Skin);
1397
+ if (!after.ok) throw new Error('expected ok get after');
1398
+ expect(after.value.palette.byteLength).toBe(128);
1399
+ expectBytesEqual(after.value.palette, snapshot, 128);
1400
+ });
1401
+
1402
+ it('removeComponent migrate keeps surviving buffer field byte-equal', () => {
1403
+ const Skin = defineComponent('Skin', { palette: { type: 'buffer<64>' } });
1404
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1405
+ const w = new World();
1406
+ const seed = new Uint8Array(64);
1407
+ fillSequential(seed, 71);
1408
+ const e = w
1409
+ .spawn({ component: Skin, data: { palette: seed } }, { component: Anchor, data: { x: 9 } })
1410
+ .unwrap();
1411
+
1412
+ const before = w.get(e, Skin);
1413
+ if (!before.ok) throw new Error('expected ok get before');
1414
+ const snapshot = new Uint8Array(before.value.palette);
1415
+
1416
+ // Remove Anchor; Skin (with buffer) survives the migrate.
1417
+ w.removeComponent(e, Anchor).unwrap();
1418
+
1419
+ const after = w.get(e, Skin);
1420
+ if (!after.ok) throw new Error('expected ok get after');
1421
+ expectBytesEqual(after.value.palette, snapshot, 64);
1422
+ });
1423
+ });
1424
+
1425
+ // ---------------------------------------------------------------------------
1426
+ // (c) Combined ref + buffer fields on the same component survive migrate.
1427
+ // ---------------------------------------------------------------------------
1428
+
1429
+ describe('w16 - combined ref+buffer fields archetype migrate carry-over (AC-04)', () => {
1430
+ it('addComponent migrate: ref handle u32 stays bit-equal AND buffer bytes byte-equal', () => {
1431
+ const Mesh = defineComponent('Mesh', {
1432
+ material: { type: 'unique<MaterialAsset>' },
1433
+ vertices: { type: 'buffer<256>' },
1434
+ });
1435
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1436
+ const w = new World();
1437
+ const store = refsOf(w);
1438
+ const payload = { id: 1 };
1439
+ const h = store.alloc('MaterialAsset', payload);
1440
+ const seed = new Uint8Array(256);
1441
+ fillSequential(seed, 31);
1442
+ const e = w
1443
+ .spawn({
1444
+ component: Mesh,
1445
+ data: {
1446
+ material: h,
1447
+ vertices: seed,
1448
+ },
1449
+ })
1450
+ .unwrap();
1451
+
1452
+ const before = w.get(e, Mesh);
1453
+ if (!before.ok) throw new Error('expected ok get before');
1454
+ const handleBefore = handleNumeric(before.value.material);
1455
+ const bytesSnapshot = new Uint8Array(before.value.vertices);
1456
+
1457
+ w.addComponent(e, { component: Anchor, data: { x: 5 } }).unwrap();
1458
+
1459
+ const after = w.get(e, Mesh);
1460
+ if (!after.ok) throw new Error('expected ok get after');
1461
+ expect(Object.is(handleNumeric(after.value.material), handleBefore)).toBe(true);
1462
+ expect(after.value.vertices.byteLength).toBe(256);
1463
+ expectBytesEqual(after.value.vertices, bytesSnapshot, 256);
1464
+
1465
+ // Resolved payload is still the same object reference.
1466
+ const resolved = store.resolve(after.value.material);
1467
+ if (!resolved.ok) throw new Error('expected resolved');
1468
+ expect(resolved.value).toBe(payload);
1469
+ });
1470
+
1471
+ it('removeComponent migrate: ref+buffer on the surviving component stay carry-over intact', () => {
1472
+ const Mesh = defineComponent('Mesh', {
1473
+ material: { type: 'unique<MaterialAsset>' },
1474
+ vertices: { type: 'buffer<128>' },
1475
+ });
1476
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1477
+ const w = new World();
1478
+ const store = refsOf(w);
1479
+ const payload = { id: 2 };
1480
+ const h = store.alloc('MaterialAsset', payload);
1481
+ const seed = new Uint8Array(128);
1482
+ fillSequential(seed, 47);
1483
+ const e = w
1484
+ .spawn(
1485
+ {
1486
+ component: Mesh,
1487
+ data: {
1488
+ material: h,
1489
+ vertices: seed,
1490
+ },
1491
+ },
1492
+ { component: Anchor, data: { x: 3 } },
1493
+ )
1494
+ .unwrap();
1495
+
1496
+ const before = w.get(e, Mesh);
1497
+ if (!before.ok) throw new Error('expected ok get before');
1498
+ const handleBefore = handleNumeric(before.value.material);
1499
+ const snapshot = new Uint8Array(before.value.vertices);
1500
+
1501
+ w.removeComponent(e, Anchor).unwrap();
1502
+
1503
+ const after = w.get(e, Mesh);
1504
+ if (!after.ok) throw new Error('expected ok get after');
1505
+ expect(Object.is(handleNumeric(after.value.material), handleBefore)).toBe(true);
1506
+ expectBytesEqual(after.value.vertices, snapshot, 128);
1507
+ });
1508
+
1509
+ it('multi-hop migrate keeps ref+buffer carry-over intact end-to-end', () => {
1510
+ const Mesh = defineComponent('Mesh', {
1511
+ material: { type: 'unique<MaterialAsset>' },
1512
+ vertices: { type: 'buffer<64>' },
1513
+ });
1514
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1515
+ const Tag = defineComponent('Tag', { v: { type: 'u32' } });
1516
+ const w = new World();
1517
+ const store = refsOf(w);
1518
+ const payload = { id: 3 };
1519
+ const h = store.alloc('MaterialAsset', payload);
1520
+ const seed = new Uint8Array(64);
1521
+ fillSequential(seed, 53);
1522
+ const e = w
1523
+ .spawn({
1524
+ component: Mesh,
1525
+ data: {
1526
+ material: h,
1527
+ vertices: seed,
1528
+ },
1529
+ })
1530
+ .unwrap();
1531
+
1532
+ const r0 = w.get(e, Mesh);
1533
+ if (!r0.ok) throw new Error('expected r0');
1534
+ const handle0 = handleNumeric(r0.value.material);
1535
+ const snapshot = new Uint8Array(r0.value.vertices);
1536
+
1537
+ // Hop sequence: +Anchor, +Tag, -Anchor, -Tag.
1538
+ w.addComponent(e, { component: Anchor, data: { x: 0 } }).unwrap();
1539
+ w.addComponent(e, { component: Tag, data: { v: 1 } }).unwrap();
1540
+ w.removeComponent(e, Anchor).unwrap();
1541
+ w.removeComponent(e, Tag).unwrap();
1542
+
1543
+ const rN = w.get(e, Mesh);
1544
+ if (!rN.ok) throw new Error('expected rN');
1545
+ expect(Object.is(handleNumeric(rN.value.material), handle0)).toBe(true);
1546
+ expectBytesEqual(rN.value.vertices, snapshot, 64);
1547
+ const resolved = store.resolve(rN.value.material);
1548
+ if (!resolved.ok) throw new Error('expected resolved');
1549
+ expect(resolved.value).toBe(payload);
1550
+ });
1551
+ });
1552
+
1553
+ // ---------------------------------------------------------------------------
1554
+ // (d) Negative invariants: migrate does NOT release / realloc / route errors.
1555
+ // ---------------------------------------------------------------------------
1556
+
1557
+ describe('w16 - migrate does not release or route errors (AC-04 negative)', () => {
1558
+ it('addComponent migrate does not call UniqueRefStore.release / BufferPool.release', () => {
1559
+ const Mesh = defineComponent('Mesh', {
1560
+ material: { type: 'unique<MaterialAsset>' },
1561
+ vertices: { type: 'buffer<32>' },
1562
+ });
1563
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1564
+ const w = new World();
1565
+ const store = refsOf(w);
1566
+ const refReleaseSpy = vi.spyOn(UniqueRefStore.prototype, 'release');
1567
+ const bufReleaseSpy = vi.spyOn(BufferPool.prototype, 'release');
1568
+ const h = store.alloc('MaterialAsset', { id: 1 });
1569
+ const seed = new Uint8Array(32);
1570
+ fillSequential(seed, 1);
1571
+ const e = w
1572
+ .spawn({
1573
+ component: Mesh,
1574
+ data: {
1575
+ material: h,
1576
+ vertices: seed,
1577
+ },
1578
+ })
1579
+ .unwrap();
1580
+
1581
+ // Reset call counts to ignore any release calls made during alloc/setup.
1582
+ refReleaseSpy.mockClear();
1583
+ bufReleaseSpy.mockClear();
1584
+
1585
+ w.addComponent(e, { component: Anchor, data: { x: 1 } }).unwrap();
1586
+
1587
+ expect(refReleaseSpy).not.toHaveBeenCalled();
1588
+ expect(bufReleaseSpy).not.toHaveBeenCalled();
1589
+ });
1590
+
1591
+ it('removeComponent migrate of an unrelated component does not release the survivor managed slots', () => {
1592
+ const Mesh = defineComponent('Mesh', {
1593
+ material: { type: 'unique<MaterialAsset>' },
1594
+ vertices: { type: 'buffer<32>' },
1595
+ });
1596
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1597
+ const w = new World();
1598
+ const store = refsOf(w);
1599
+ const h = store.alloc('MaterialAsset', { id: 1 });
1600
+ const seed = new Uint8Array(32);
1601
+ fillSequential(seed, 17);
1602
+ const e = w
1603
+ .spawn(
1604
+ {
1605
+ component: Mesh,
1606
+ data: {
1607
+ material: h,
1608
+ vertices: seed,
1609
+ },
1610
+ },
1611
+ { component: Anchor, data: { x: 0 } },
1612
+ )
1613
+ .unwrap();
1614
+
1615
+ const refReleaseSpy = vi.spyOn(UniqueRefStore.prototype, 'release');
1616
+ const bufReleaseSpy = vi.spyOn(BufferPool.prototype, 'release');
1617
+
1618
+ // Remove Anchor (no managed fields). Mesh's material + vertices must NOT
1619
+ // be released - they survive the migrate.
1620
+ w.removeComponent(e, Anchor).unwrap();
1621
+
1622
+ expect(refReleaseSpy).not.toHaveBeenCalled();
1623
+ expect(bufReleaseSpy).not.toHaveBeenCalled();
1624
+
1625
+ // The handle still resolves and the bytes are still readable.
1626
+ const after = w.get(e, Mesh);
1627
+ if (!after.ok) throw new Error('expected ok get after');
1628
+ const resolved = store.resolve(after.value.material);
1629
+ expect(resolved.ok).toBe(true);
1630
+ expect(after.value.vertices.byteLength).toBe(32);
1631
+ });
1632
+ });
1633
+ }
1634
+ {
1635
+ // --- from managed-release.test.ts ---
1636
+ function refsOf(w: World): UniqueRefStore {
1637
+ return (w as unknown as { uniqueRefs: UniqueRefStore }).uniqueRefs;
1638
+ }
1639
+
1640
+ // ---------------------------------------------------------------------------
1641
+ // (a) UniqueRefStore unit-level invariants (covers w7 acceptance).
1642
+ // ---------------------------------------------------------------------------
1643
+
1644
+ describe('w6 - UniqueRefStore unit invariants', () => {
1645
+ it('alloc returns a frozen plain-object handle wrapper', () => {
1646
+ const w = new World();
1647
+ const store = refsOf(w);
1648
+ const h = store.alloc('MaterialAsset', { id: 7 });
1649
+ // Wrapper is frozen (D-3 charter: immutable identity).
1650
+ expect(Object.isFrozen(h)).toBe(true);
1651
+ });
1652
+
1653
+ it('resolve(h) on a live handle returns ok(payload)', () => {
1654
+ const w = new World();
1655
+ const store = refsOf(w);
1656
+ const h = store.alloc<'MaterialAsset', { id: number }>('MaterialAsset', { id: 42 });
1657
+ const r = store.resolve<'MaterialAsset', { id: number }>(h);
1658
+ if (!r.ok) throw new Error('expected ok');
1659
+ expect(r.value.id).toBe(42);
1660
+ });
1661
+
1662
+ it('resolve(h) after release returns err(unique-ref-stale) (gen incremented in M4)', () => {
1663
+ const w = new World();
1664
+ const store = refsOf(w);
1665
+ const h = store.alloc('MaterialAsset', { id: 1 });
1666
+ store.release(h).unwrap();
1667
+ const r = store.resolve(h);
1668
+ expect(r.ok).toBe(false);
1669
+ if (r.ok) throw new Error('expected err');
1670
+ expect(r.error.code).toBe('unique-ref-stale');
1671
+ });
1672
+
1673
+ it('release(h) twice surfaces unique-ref-stale (gen mismatch after first release, M4)', () => {
1674
+ const w = new World();
1675
+ const store = refsOf(w);
1676
+ const h = store.alloc('MaterialAsset', { id: 1 });
1677
+ store.release(h).unwrap();
1678
+ const r = store.release(h);
1679
+ expect(r.ok).toBe(false);
1680
+ if (r.ok) throw new Error('expected err');
1681
+ expect(r.error.code).toBe('unique-ref-stale');
1682
+ });
1683
+
1684
+ it('AC-04 prelude: alloc returns per-(slot,gen) singleton identity (Object.is)', () => {
1685
+ // D-3: each alloc produces a unique frozen wrapper; the same wrapper is
1686
+ // strongly referenced by the store's Map until release. Any mid-life
1687
+ // resolve of the same handle must return the same object reference (this
1688
+ // probes the "(slot, gen) singleton" invariant prior to archetype migrate
1689
+ // carry-over in M4 — the store does NOT swap wrapper identity on resolve).
1690
+ const w = new World();
1691
+ const store = refsOf(w);
1692
+ const h = store.alloc('MaterialAsset', { id: 1 });
1693
+ const r1 = store.resolve(h);
1694
+ const r2 = store.resolve(h);
1695
+ if (!r1.ok || !r2.ok) throw new Error('expected ok x2');
1696
+ expect(Object.is(r1.value, r2.value)).toBe(true);
1697
+ });
1698
+
1699
+ it('release of distinct handles is independent (refcount per-slot, not global)', () => {
1700
+ const w = new World();
1701
+ const store = refsOf(w);
1702
+ const a = store.alloc<'MaterialAsset', { id: number }>('MaterialAsset', { id: 1 });
1703
+ const b = store.alloc<'MaterialAsset', { id: number }>('MaterialAsset', { id: 2 });
1704
+ store.release(a).unwrap();
1705
+ // b stays live.
1706
+ const rb = store.resolve<'MaterialAsset', { id: number }>(b);
1707
+ if (!rb.ok) throw new Error('expected b live');
1708
+ expect(rb.value.id).toBe(2);
1709
+ });
1710
+ });
1711
+
1712
+ // ---------------------------------------------------------------------------
1713
+ // (b) World release loop integration tests (AC-03 - 3 paths).
1714
+ // ---------------------------------------------------------------------------
1715
+
1716
+ describe('w6 - World ref<T> field release loop', () => {
1717
+ it('world.despawn(e) releases ref<T> field handle (path 1)', () => {
1718
+ const Mat = defineComponent('Mat', { handle: { type: 'unique<MaterialAsset>' } });
1719
+ const w = new World();
1720
+ const store = refsOf(w);
1721
+ const h = store.alloc('MaterialAsset', { id: 7 });
1722
+ const e = w
1723
+ .spawn({
1724
+ component: Mat,
1725
+ data: { handle: h },
1726
+ })
1727
+ .unwrap();
1728
+ w.despawn(e).unwrap();
1729
+ const r = store.resolve(h);
1730
+ expect(r.ok).toBe(false);
1731
+ if (r.ok) throw new Error('expected stale');
1732
+ expect(r.error.code).toBe('unique-ref-stale');
1733
+ });
1734
+
1735
+ it('world.removeComponent(e, C) releases ref<T> field handle (path 2)', () => {
1736
+ const Mat = defineComponent('Mat', { handle: { type: 'unique<MaterialAsset>' } });
1737
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1738
+ const w = new World();
1739
+ const store = refsOf(w);
1740
+ const h = store.alloc('MaterialAsset', { id: 7 });
1741
+ const e = w
1742
+ .spawn(
1743
+ {
1744
+ component: Mat,
1745
+ data: { handle: h },
1746
+ },
1747
+ { component: Anchor, data: { x: 0 } },
1748
+ )
1749
+ .unwrap();
1750
+ w.removeComponent(e, Mat).unwrap();
1751
+ const r = store.resolve(h);
1752
+ expect(r.ok).toBe(false);
1753
+ if (r.ok) throw new Error('expected stale');
1754
+ expect(r.error.code).toBe('unique-ref-stale');
1755
+ });
1756
+
1757
+ it('world.set(e, C, { handle: newHandle }) releases the prior handle (path 3)', () => {
1758
+ const Mat = defineComponent('Mat', { handle: { type: 'unique<MaterialAsset>' } });
1759
+ const w = new World();
1760
+ const store = refsOf(w);
1761
+ const oldH = store.alloc('MaterialAsset', { id: 1 });
1762
+ const newH = store.alloc('MaterialAsset', { id: 2 });
1763
+ const e = w
1764
+ .spawn({
1765
+ component: Mat,
1766
+ data: { handle: oldH },
1767
+ })
1768
+ .unwrap();
1769
+ w.set(e, Mat, {
1770
+ handle: newH,
1771
+ }).unwrap();
1772
+ const rOld = store.resolve(oldH);
1773
+ expect(rOld.ok).toBe(false);
1774
+ if (rOld.ok) throw new Error('expected old released');
1775
+ expect(rOld.error.code).toBe('unique-ref-stale');
1776
+ const rNew = store.resolve(newH);
1777
+ expect(rNew.ok).toBe(true);
1778
+ });
1779
+
1780
+ it('null ref<T> field on spawn does not call release on despawn (boundary)', () => {
1781
+ // Boundary case: ref<T> field can hold null; despawn must not attempt to
1782
+ // release a null slot (would surface as unique-ref-double-release noise).
1783
+ const Mat = defineComponent('Mat', { handle: { type: 'unique<MaterialAsset>' } });
1784
+ const w = new World();
1785
+ // Use 0 as the "null/unset" sentinel encoded as a managed handle. The
1786
+ // store's release path must short-circuit on this sentinel - documented
1787
+ // by D-3 (the wrapper for slot 0 is never allocated, so resolve returns
1788
+ // err but world's release loop must skip it without escalating).
1789
+ const sentinel = 0 as Handle<'MaterialAsset', 'unique'>;
1790
+ const e = w.spawn({ component: Mat, data: { handle: sentinel } }).unwrap();
1791
+ w.despawn(e).unwrap();
1792
+ });
1793
+ });
1794
+
1795
+ // ---------------------------------------------------------------------------
1796
+ // (c) w9 - Layer 3 ErrorHandler routing on release failure (AC-05).
1797
+ // ---------------------------------------------------------------------------
1798
+ }
1799
+ {
1800
+ // --- from resource.test.ts ---
1801
+ describe('insertResource / getResource', () => {
1802
+ it('insertResource stores a value and getResource retrieves it', () => {
1803
+ const world = new World();
1804
+ world.insertResource('time', { delta: 0.016, elapsed: 0 });
1805
+ const time = world.getResource<{ delta: number; elapsed: number }>('time');
1806
+ expect(time).toEqual({ delta: 0.016, elapsed: 0 });
1807
+ });
1808
+
1809
+ it('getResource returns the exact reference (not a copy)', () => {
1810
+ const world = new World();
1811
+ const obj = { count: 0 };
1812
+ world.insertResource('counter', obj);
1813
+ const retrieved = world.getResource<{ count: number }>('counter');
1814
+ expect(retrieved).toBe(obj);
1815
+ });
1816
+
1817
+ it('supports primitive values', () => {
1818
+ const world = new World();
1819
+ world.insertResource('fps', 60);
1820
+ expect(world.getResource<number>('fps')).toBe(60);
1821
+ });
1822
+
1823
+ it('supports string values', () => {
1824
+ const world = new World();
1825
+ world.insertResource('name', 'hello');
1826
+ expect(world.getResource<string>('name')).toBe('hello');
1827
+ });
1828
+ });
1829
+
1830
+ describe('hasResource', () => {
1831
+ it('returns true for existing resource', () => {
1832
+ const world = new World();
1833
+ world.insertResource('exists', 42);
1834
+ expect(world.hasResource('exists')).toBe(true);
1835
+ });
1836
+
1837
+ it('returns false for non-existing resource', () => {
1838
+ const world = new World();
1839
+ expect(world.hasResource('nope')).toBe(false);
1840
+ });
1841
+ });
1842
+
1843
+ describe('removeResource', () => {
1844
+ it('removes an existing resource', () => {
1845
+ const world = new World();
1846
+ world.insertResource('temp', 100);
1847
+ world.removeResource('temp');
1848
+ expect(world.hasResource('temp')).toBe(false);
1849
+ });
1850
+
1851
+ it('getResource after remove throws ResourceNotFoundError', () => {
1852
+ const world = new World();
1853
+ world.insertResource('gone', 1);
1854
+ world.removeResource('gone');
1855
+ expect(() => world.getResource('gone')).toThrow(ResourceNotFoundError);
1856
+ });
1857
+ });
1858
+
1859
+ describe('idempotent overwrite (E-13)', () => {
1860
+ it('insertResource with same key overwrites old value', () => {
1861
+ const world = new World();
1862
+ world.insertResource('data', { v: 1 });
1863
+ world.insertResource('data', { v: 2 });
1864
+ expect(world.getResource<{ v: number }>('data')).toEqual({ v: 2 });
1865
+ });
1866
+
1867
+ it('overwrite is idempotent — double insert same value is fine', () => {
1868
+ const world = new World();
1869
+ world.insertResource('stable', 42);
1870
+ world.insertResource('stable', 42);
1871
+ expect(world.getResource<number>('stable')).toBe(42);
1872
+ });
1873
+ });
1874
+
1875
+ describe('getResource not found (E-14)', () => {
1876
+ it('getResource on non-existing key throws ResourceNotFoundError', () => {
1877
+ const world = new World();
1878
+ expect(() => world.getResource('missing')).toThrow(ResourceNotFoundError);
1879
+ });
1880
+
1881
+ it('ResourceNotFoundError contains key in message', () => {
1882
+ const world = new World();
1883
+ try {
1884
+ world.getResource('myKey');
1885
+ expect.unreachable('should have thrown');
1886
+ } catch (err) {
1887
+ expect(err).toBeInstanceOf(ResourceNotFoundError);
1888
+ expect((err as ResourceNotFoundError).message).toContain('myKey');
1889
+ }
1890
+ });
1891
+
1892
+ it('ResourceNotFoundError has hint property', () => {
1893
+ const world = new World();
1894
+ try {
1895
+ world.getResource('foo');
1896
+ expect.unreachable('should have thrown');
1897
+ } catch (err) {
1898
+ expect(err).toBeInstanceOf(ResourceNotFoundError);
1899
+ expect((err as ResourceNotFoundError).hint).toContain('insertResource');
1900
+ }
1901
+ });
1902
+ });
1903
+ }
1904
+ {
1905
+ // --- from schedule-remove-replace.test.ts ---
1906
+ describe('schedule.removeSystem (AC-04)', () => {
1907
+ it('removes a registered system; world.inspect() no longer lists it', () => {
1908
+ const world = new World();
1909
+ world.addSystem(Update, { name: 'sysA', queries: [], fn: () => {} });
1910
+ world.addSystem(Update, { name: 'sysB', queries: [], fn: () => {} });
1911
+
1912
+ const r = world.removeSystem(Update, 'sysA');
1913
+ expect(r.ok).toBe(true);
1914
+
1915
+ const snap = world.inspect();
1916
+ const names = snap.systems.map((s) => s.name);
1917
+ expect(names).not.toContain('sysA');
1918
+ expect(names).toContain('sysB');
1919
+ });
1920
+
1921
+ it('returns Result.err with code system-before-unknown when name does not exist', () => {
1922
+ const world = new World();
1923
+ world.addSystem(Update, { name: 'sysA', queries: [], fn: () => {} });
1924
+
1925
+ const r = world.removeSystem(Update, 'nonexistent');
1926
+ expect(r.ok).toBe(false);
1927
+ if (r.ok) return;
1928
+ expect(r.error.code).toBe('system-before-unknown');
1929
+ });
1930
+ });
1931
+
1932
+ describe('schedule.replaceSystem (AC-04)', () => {
1933
+ it('replaces system fn atomically; before/after edges of the slot remain intact', () => {
1934
+ const world = new World();
1935
+ const log: string[] = [];
1936
+
1937
+ world.addSystem(Update, { name: 'sysA', queries: [], fn: () => log.push('A-old') });
1938
+ world.addSystem(Update, {
1939
+ name: 'sysB',
1940
+ queries: [],
1941
+ fn: () => log.push('B'),
1942
+ after: ['sysA'],
1943
+ });
1944
+
1945
+ const r = world.replaceSystem(Update, 'sysA', {
1946
+ name: 'sysA',
1947
+ queries: [],
1948
+ fn: () => log.push('A-new'),
1949
+ });
1950
+ expect(r.ok).toBe(true);
1951
+
1952
+ world.update();
1953
+ expect(log).toEqual(['A-new', 'B']);
1954
+ expect(log.indexOf('A-new')).toBeLessThan(log.indexOf('B'));
1955
+ });
1956
+
1957
+ it('returns Result.err with code system-before-unknown when target name not registered', () => {
1958
+ const world = new World();
1959
+
1960
+ const r = world.replaceSystem(Update, 'nonexistent', {
1961
+ name: 'nonexistent',
1962
+ queries: [],
1963
+ fn: () => {},
1964
+ });
1965
+ expect(r.ok).toBe(false);
1966
+ if (r.ok) return;
1967
+ expect(r.error.code).toBe('system-before-unknown');
1968
+ });
1969
+ });
1970
+ }
1971
+ {
1972
+ // --- from schedule.test.ts ---
1973
+ describe('System registration (AC-11)', () => {
1974
+ it('world.addSystem registers a system with query descriptor', () => {
1975
+ const world = new World();
1976
+ const Pos = defineComponent('SchedPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1977
+ const log: string[] = [];
1978
+
1979
+ world.addSystem(Update, {
1980
+ name: 'movement',
1981
+ queries: [{ with: [Pos] }],
1982
+ fn: () => {
1983
+ log.push('movement');
1984
+ },
1985
+ });
1986
+
1987
+ world.update();
1988
+ expect(log).toEqual(['movement']);
1989
+ });
1990
+
1991
+ it('system receives query results and commands', () => {
1992
+ const world = new World();
1993
+ const Pos = defineComponent('SysQPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1994
+ world.spawn({ component: Pos, data: { x: 1, y: 2 } });
1995
+
1996
+ let receivedBundleCount = 0;
1997
+
1998
+ world.addSystem(Update, {
1999
+ name: 'reader',
2000
+ queries: [{ read: [Pos] }],
2001
+ fn: (_world, queryResults, _commands) => {
2002
+ for (const query of queryResults) receivedBundleCount += [...query].length;
2003
+ },
2004
+ });
2005
+
2006
+ world.update();
2007
+ expect(receivedBundleCount).toBe(1);
2008
+ });
2009
+ });
2010
+
2011
+ describe('before/after constraints (AC-12)', () => {
2012
+ it('after constraint ensures system runs after specified system', () => {
2013
+ const world = new World();
2014
+ const log: string[] = [];
2015
+
2016
+ world.addSystem(Update, {
2017
+ name: 'sysA',
2018
+ queries: [],
2019
+ fn: () => log.push('A'),
2020
+ });
2021
+
2022
+ world.addSystem(Update, {
2023
+ name: 'sysB',
2024
+ queries: [],
2025
+ fn: () => log.push('B'),
2026
+ after: ['sysA'],
2027
+ });
2028
+
2029
+ world.update();
2030
+ expect(log.indexOf('A')).toBeLessThan(log.indexOf('B'));
2031
+ });
2032
+
2033
+ it('before constraint ensures system runs before specified system', () => {
2034
+ const world = new World();
2035
+ const log: string[] = [];
2036
+
2037
+ // Register B first, then A with before: ['sysB']
2038
+ world.addSystem(Update, {
2039
+ name: 'sysB',
2040
+ queries: [],
2041
+ fn: () => log.push('B'),
2042
+ });
2043
+
2044
+ world.addSystem(Update, {
2045
+ name: 'sysA',
2046
+ queries: [],
2047
+ fn: () => log.push('A'),
2048
+ before: ['sysB'],
2049
+ });
2050
+
2051
+ world.update();
2052
+ expect(log.indexOf('A')).toBeLessThan(log.indexOf('B'));
2053
+ });
2054
+
2055
+ it('complex DAG: A -> B -> D, A -> C -> D', () => {
2056
+ const world = new World();
2057
+ const log: string[] = [];
2058
+
2059
+ world.addSystem(Update, {
2060
+ name: 'A',
2061
+ queries: [],
2062
+ fn: () => log.push('A'),
2063
+ });
2064
+
2065
+ world.addSystem(Update, {
2066
+ name: 'B',
2067
+ queries: [],
2068
+ fn: () => log.push('B'),
2069
+ after: ['A'],
2070
+ before: ['D'],
2071
+ });
2072
+
2073
+ world.addSystem(Update, {
2074
+ name: 'C',
2075
+ queries: [],
2076
+ fn: () => log.push('C'),
2077
+ after: ['A'],
2078
+ before: ['D'],
2079
+ });
2080
+
2081
+ world.addSystem(Update, {
2082
+ name: 'D',
2083
+ queries: [],
2084
+ fn: () => log.push('D'),
2085
+ });
2086
+
2087
+ world.update();
2088
+
2089
+ // A must come first, D must come last
2090
+ expect(log[0]).toBe('A');
2091
+ expect(log[3]).toBe('D');
2092
+ // B and C are between A and D — order determined by registration order
2093
+ expect(log[1]).toBe('B');
2094
+ expect(log[2]).toBe('C');
2095
+ });
2096
+ });
2097
+
2098
+ describe('Registration order tie-breaker (AC-13)', () => {
2099
+ it('systems with no ordering constraints run in registration order', () => {
2100
+ const world = new World();
2101
+ const log: string[] = [];
2102
+
2103
+ world.addSystem(Update, { name: 'first', queries: [], fn: () => log.push('first') });
2104
+ world.addSystem(Update, { name: 'second', queries: [], fn: () => log.push('second') });
2105
+ world.addSystem(Update, { name: 'third', queries: [], fn: () => log.push('third') });
2106
+
2107
+ world.update();
2108
+ expect(log).toEqual(['first', 'second', 'third']);
2109
+ });
2110
+
2111
+ it('tie-breaker applies within same topological level', () => {
2112
+ const world = new World();
2113
+ const log: string[] = [];
2114
+
2115
+ world.addSystem(Update, { name: 'root', queries: [], fn: () => log.push('root') });
2116
+ // Three systems all after 'root', no order among themselves
2117
+ world.addSystem(Update, {
2118
+ name: 'childC',
2119
+ queries: [],
2120
+ fn: () => log.push('childC'),
2121
+ after: ['root'],
2122
+ });
2123
+ world.addSystem(Update, {
2124
+ name: 'childA',
2125
+ queries: [],
2126
+ fn: () => log.push('childA'),
2127
+ after: ['root'],
2128
+ });
2129
+ world.addSystem(Update, {
2130
+ name: 'childB',
2131
+ queries: [],
2132
+ fn: () => log.push('childB'),
2133
+ after: ['root'],
2134
+ });
2135
+
2136
+ world.update();
2137
+ expect(log[0]).toBe('root');
2138
+ // After root: childC, childA, childB — by registration order
2139
+ expect(log.slice(1)).toEqual(['childC', 'childA', 'childB']);
2140
+ });
2141
+ });
2142
+
2143
+ describe('DAG cyclic dependency (E-07)', () => {
2144
+ it('cyclic dependency throws CyclicDependencyError', () => {
2145
+ const world = new World();
2146
+
2147
+ world.addSystem(Update, { name: 'A', queries: [], fn: () => {}, after: ['C'] });
2148
+ world.addSystem(Update, { name: 'B', queries: [], fn: () => {}, after: ['A'] });
2149
+ world.addSystem(Update, { name: 'C', queries: [], fn: () => {}, after: ['B'] });
2150
+
2151
+ const result = world.update();
2152
+ expect(result.ok).toBe(false);
2153
+ if (!result.ok) expect(result.error.code).toBe('cyclic-dependency');
2154
+ });
2155
+
2156
+ it('CyclicDependencyError message contains cycle path', () => {
2157
+ const world = new World();
2158
+
2159
+ world.addSystem(Update, { name: 'X', queries: [], fn: () => {}, after: ['Z'] });
2160
+ world.addSystem(Update, { name: 'Y', queries: [], fn: () => {}, after: ['X'] });
2161
+ world.addSystem(Update, { name: 'Z', queries: [], fn: () => {}, after: ['Y'] });
2162
+
2163
+ const result = world.update();
2164
+ expect(result.ok).toBe(false);
2165
+ if (!result.ok && result.error.code === 'cyclic-dependency') {
2166
+ const msg = result.error.message;
2167
+ // Message should contain system names from the cycle (joined by ' -> ')
2168
+ expect(msg).toMatch(/X/);
2169
+ expect(msg).toMatch(/Y/);
2170
+ expect(msg).toMatch(/Z/);
2171
+ // detail.cycle is structured array
2172
+ const cycle = result.error.detail.cycle;
2173
+ expect(cycle).toBeInstanceOf(Array);
2174
+ expect(cycle.length).toBeGreaterThanOrEqual(2);
2175
+ expect(cycle).toContain('X');
2176
+ expect(cycle).toContain('Y');
2177
+ expect(cycle).toContain('Z');
2178
+ }
2179
+ });
2180
+
2181
+ it('partial cycle: some systems are outside the cycle', () => {
2182
+ const world = new World();
2183
+ const log: string[] = [];
2184
+
2185
+ // Linear chain: free1 → free2 (no cycle)
2186
+ world.addSystem(Update, { name: 'free1', queries: [], fn: () => log.push('free1') });
2187
+ world.addSystem(Update, {
2188
+ name: 'free2',
2189
+ queries: [],
2190
+ fn: () => log.push('free2'),
2191
+ after: ['free1'],
2192
+ });
2193
+
2194
+ // Cycle: cyc1 → cyc2 → cyc1
2195
+ world.addSystem(Update, { name: 'cyc1', queries: [], fn: () => {}, after: ['cyc2'] });
2196
+ world.addSystem(Update, { name: 'cyc2', queries: [], fn: () => {}, after: ['cyc1'] });
2197
+
2198
+ const result = world.update();
2199
+ expect(result.ok).toBe(false);
2200
+ if (!result.ok) expect(result.error.code).toBe('cyclic-dependency');
2201
+ });
2202
+
2203
+ it('4-node cycle: A→B→C→D→A with an uncycled system E', () => {
2204
+ const world = new World();
2205
+
2206
+ world.addSystem(Update, { name: 'E', queries: [], fn: () => {} });
2207
+ world.addSystem(Update, { name: 'A', queries: [], fn: () => {}, after: ['D'] });
2208
+ world.addSystem(Update, { name: 'B', queries: [], fn: () => {}, after: ['A'] });
2209
+ world.addSystem(Update, { name: 'C', queries: [], fn: () => {}, after: ['B'] });
2210
+ world.addSystem(Update, { name: 'D', queries: [], fn: () => {}, after: ['C'] });
2211
+
2212
+ const result = world.update();
2213
+ expect(result.ok).toBe(false);
2214
+ if (!result.ok && result.error.code === 'cyclic-dependency') {
2215
+ const msg = result.error.message;
2216
+ // Cycle involves A, B, C, D
2217
+ expect(msg).toMatch(/A/);
2218
+ expect(msg).toMatch(/B/);
2219
+ const cycle = result.error.detail.cycle;
2220
+ expect(cycle).toBeInstanceOf(Array);
2221
+ expect(cycle.length).toBeGreaterThanOrEqual(2);
2222
+ }
2223
+ });
2224
+
2225
+ it('before/after referencing unknown systems are silently skipped', () => {
2226
+ const world = new World();
2227
+ const log: string[] = [];
2228
+
2229
+ world.addSystem(Update, {
2230
+ name: 'sys1',
2231
+ queries: [],
2232
+ fn: () => log.push('sys1'),
2233
+ after: ['nonexistent'],
2234
+ });
2235
+
2236
+ world.update();
2237
+ expect(log).toEqual(['sys1']);
2238
+ });
2239
+ });
2240
+
2241
+ describe('Empty World update (E-09)', () => {
2242
+ it('update on empty world completes silently', () => {
2243
+ const world = new World();
2244
+ expect(() => world.update()).not.toThrow();
2245
+ });
2246
+
2247
+ it('update on world with no systems completes silently', () => {
2248
+ const world = new World();
2249
+ const Pos = defineComponent('EmptyUpdatePos', { x: { type: 'f32' } });
2250
+ world.spawn({ component: Pos, data: { x: 1 } });
2251
+ expect(() => world.update()).not.toThrow();
2252
+ });
2253
+ });
2254
+
2255
+ describe('Lazy build', () => {
2256
+ it('addSystem only marks dirty, first update triggers sort', () => {
2257
+ const world = new World();
2258
+ const log: string[] = [];
2259
+
2260
+ world.addSystem(Update, { name: 'A', queries: [], fn: () => log.push('A') });
2261
+ world.addSystem(Update, { name: 'B', queries: [], fn: () => log.push('B'), before: ['A'] });
2262
+
2263
+ // First update builds schedule
2264
+ world.update();
2265
+ expect(log).toEqual(['B', 'A']);
2266
+
2267
+ log.length = 0;
2268
+ // Second update uses cached schedule
2269
+ world.update();
2270
+ expect(log).toEqual(['B', 'A']);
2271
+ });
2272
+ });
2273
+
2274
+ // ────────────────────────────────────────────────────────────────────────────
2275
+ // [w16] Layer 2 ParamValidation tests (red phase — AP-8 Layer 2)
2276
+ // ────────────────────────────────────────────────────────────────────────────
2277
+
2278
+ describe('Layer 2 — ParamValidation (AP-8)', () => {
2279
+ it('query with no matching archetypes → system body still executes (Query is always ok)', () => {
2280
+ const world = new World();
2281
+ const Pos = defineComponent('PVSkipPos', { x: { type: 'f32' } });
2282
+ // No entities spawned — query will match nothing
2283
+
2284
+ let bodyExecuted = false;
2285
+ world.addSystem(Update, {
2286
+ name: 'emptyQuerySystem',
2287
+ queries: [{ with: [Pos] }],
2288
+ fn: () => {
2289
+ bodyExecuted = true;
2290
+ },
2291
+ });
2292
+
2293
+ world.update();
2294
+ // Per Bevy Finding 6: Query<D, F> is always Ok (empty is valid).
2295
+ // System body executes; it receives empty query results.
2296
+ // "skipped" path is reserved for future Populated/Single query types.
2297
+ expect(bodyExecuted).toBe(true);
2298
+ });
2299
+
2300
+ it('query with matching archetypes → system body executes (ok)', () => {
2301
+ const world = new World();
2302
+ const Pos = defineComponent('PVOkPos', { x: { type: 'f32' } });
2303
+ world.spawn({ component: Pos, data: { x: 1 } });
2304
+
2305
+ let bodyExecuted = false;
2306
+ world.addSystem(Update, {
2307
+ name: 'okSystem',
2308
+ queries: [{ with: [Pos] }],
2309
+ fn: () => {
2310
+ bodyExecuted = true;
2311
+ },
2312
+ });
2313
+
2314
+ world.update();
2315
+ expect(bodyExecuted).toBe(true);
2316
+ });
2317
+ });
2318
+
2319
+ // ────────────────────────────────────────────────────────────────────────────
2320
+ // [w18] Layer 3 ErrorHandler + Severity tests (red phase — AP-8 Layer 3)
2321
+ // ────────────────────────────────────────────────────────────────────────────
2322
+ }
2323
+ {
2324
+ // --- from string-carry-over.test.ts ---
2325
+ interface MaterialAsset {
2326
+ readonly tint: number;
2327
+ }
2328
+
2329
+ function refsOf(w: World): UniqueRefStore {
2330
+ return (w as unknown as { uniqueRefs: UniqueRefStore }).uniqueRefs;
2331
+ }
2332
+
2333
+ // ---------------------------------------------------------------------------
2334
+ // (a) addComponent migrate: string `.value` Object.is + string-equal preserved.
2335
+ // ---------------------------------------------------------------------------
2336
+
2337
+ describe('w8 --- string carry-over via addComponent migrate (AC-04)', () => {
2338
+ it('Object.is(pre, post) holds AND pre === post string-equal', () => {
2339
+ const Foo = defineComponent('Foo', { v: { type: 'string' } });
2340
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
2341
+ const w = new World();
2342
+
2343
+ const e = w.spawn({ component: Foo, data: { v: 'hello world' } }).unwrap();
2344
+ const pre = w.get(e, Foo).unwrap().v;
2345
+ expect(pre).toBe('hello world');
2346
+
2347
+ w.addComponent(e, { component: Anchor, data: { x: 1 } }).unwrap();
2348
+
2349
+ const post = w.get(e, Foo).unwrap().v;
2350
+ expect(post).toBe(pre);
2351
+ expect(Object.is(pre, post)).toBe(true);
2352
+ });
2353
+ });
2354
+
2355
+ // ---------------------------------------------------------------------------
2356
+ // (b) removeComponent migrate: same invariants in the reverse direction.
2357
+ // ---------------------------------------------------------------------------
2358
+
2359
+ describe('w8 --- string carry-over via removeComponent migrate (AC-04)', () => {
2360
+ it('Object.is(pre, post) holds across the reverse migrate', () => {
2361
+ const Foo = defineComponent('Foo', { v: { type: 'string' } });
2362
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
2363
+ const w = new World();
2364
+
2365
+ const e = w
2366
+ .spawn(
2367
+ { component: Foo, data: { v: 'archetype migrate' } },
2368
+ { component: Anchor, data: { x: 7 } },
2369
+ )
2370
+ .unwrap();
2371
+ const pre = w.get(e, Foo).unwrap().v;
2372
+
2373
+ w.removeComponent(e, Anchor).unwrap();
2374
+
2375
+ const post = w.get(e, Foo).unwrap().v;
2376
+ expect(post).toBe(pre);
2377
+ expect(Object.is(pre, post)).toBe(true);
2378
+ });
2379
+ });
2380
+
2381
+ // ---------------------------------------------------------------------------
2382
+ // (c) Mixed string + ref<T> on the same entity: both stay identity-stable.
2383
+ // ---------------------------------------------------------------------------
2384
+
2385
+ describe('w8 --- mixed string + ref<T> carry-over (AC-04, D-R3 symmetry)', () => {
2386
+ it('addComponent migrate keeps BOTH string identity and ref<T> handle bit-equal', () => {
2387
+ const Mixed = defineComponent('Mixed', {
2388
+ label: { type: 'string' },
2389
+ mat: { type: 'unique<MaterialAsset>' },
2390
+ });
2391
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
2392
+ const w = new World();
2393
+
2394
+ const refs = refsOf(w);
2395
+ const payload: MaterialAsset = { tint: 0xff00ff };
2396
+ const matHandle = refs.alloc('MaterialAsset', payload);
2397
+
2398
+ const e = w
2399
+ .spawn({
2400
+ component: Mixed,
2401
+ data: {
2402
+ label: 'mixed-entity',
2403
+ mat: matHandle,
2404
+ } as never,
2405
+ })
2406
+ .unwrap();
2407
+
2408
+ const pre = w.get(e, Mixed).unwrap();
2409
+ const labelPre = pre.label;
2410
+ const matPre = handleNumeric(pre.mat);
2411
+
2412
+ w.addComponent(e, { component: Anchor, data: { x: 0 } }).unwrap();
2413
+
2414
+ const post = w.get(e, Mixed).unwrap();
2415
+ const labelPost = post.label;
2416
+ const matPost = handleNumeric(post.mat);
2417
+
2418
+ // (a) String field --- Object.is + value equal across migrate.
2419
+ expect(labelPost).toBe(labelPre);
2420
+ expect(Object.is(labelPre, labelPost)).toBe(true);
2421
+
2422
+ // (b) Ref field --- u32 handle bit-equal AND resolves to the same payload.
2423
+ expect(matPost).toBe(matPre);
2424
+ const resolvedPost = refs.resolve(post.mat);
2425
+ if (!resolvedPost.ok) throw new Error('expected resolved');
2426
+ expect(resolvedPost.value).toBe(payload);
2427
+ });
2428
+ });
2429
+ }
2430
+ {
2431
+ // --- from string-identity-contract.test.ts ---
2432
+ // Inline `'string'`-field component — the prior `Name` reference moved to
2433
+ // @forgeax/engine-runtime (tweak-20260612-ecs-concept-compression). The ECS
2434
+ // package tests its own `'string'` schema vocab without depending on the
2435
+ // runtime sibling.
2436
+ const StrName = defineComponent('StrName', { value: { type: 'string' } });
2437
+ describe('string field --- identity contract (w5, AC-03)', () => {
2438
+ it('Object.is(read1, read2) === true between consecutive reads with no set', () => {
2439
+ const w = new World();
2440
+ const e = w.spawn({ component: StrName, data: { value: 'Player' } }).unwrap();
2441
+ const got1 = w.get(e, StrName);
2442
+ if (!got1.ok) throw new Error('expected ok');
2443
+ const got2 = w.get(e, StrName);
2444
+ if (!got2.ok) throw new Error('expected ok');
2445
+ // Identity stability: the same reference is returned across reads.
2446
+ expect(Object.is(got1.value.value, got2.value.value)).toBe(true);
2447
+ });
2448
+
2449
+ it('read returns a native JS string (not a wrapper with .get())', () => {
2450
+ const w = new World();
2451
+ const e = w.spawn({ component: StrName, data: { value: 'Hello' } }).unwrap();
2452
+ const got = w.get(e, StrName);
2453
+ if (!got.ok) throw new Error('expected ok');
2454
+ // After w6 the value is a native string; typeof must be 'string'.
2455
+ expect(typeof got.value.value).toBe('string');
2456
+ expect(got.value.value).toBe('Hello');
2457
+ });
2458
+
2459
+ it('set with a different value invalidates prior read identity', () => {
2460
+ // Contract only locks identity BETWEEN reads with no intervening set.
2461
+ // After set, the new read returns a fresh string reference; this test
2462
+ // documents the inverse boundary.
2463
+ const w = new World();
2464
+ const e = w.spawn({ component: StrName, data: { value: 'Player' } }).unwrap();
2465
+ const before = w.get(e, StrName).unwrap().value;
2466
+ w.set(e, StrName, { value: 'Boss' }).unwrap();
2467
+ const after = w.get(e, StrName).unwrap().value;
2468
+ expect(before).toBe('Player');
2469
+ expect(after).toBe('Boss');
2470
+ // No Object.is invariant required across set; values differ.
2471
+ expect(Object.is(before, after)).toBe(false);
2472
+ });
2473
+ });
2474
+ }
2475
+ {
2476
+ // --- from string-managed-dispatch.test.ts ---
2477
+ interface WorldInternals {
2478
+ uniqueRefs: UniqueRefStore;
2479
+ }
2480
+
2481
+ function getUniqueRefs(w: World): UniqueRefStore {
2482
+ return (w as unknown as WorldInternals).uniqueRefs as UniqueRefStore;
2483
+ }
2484
+
2485
+ // Inline `'string'`-field component for the dispatch tests.
2486
+ const StrName2 = defineComponent('StrName2', { value: { type: 'string' } });
2487
+ describe("w5 - 'string' dispatch via uniqueRefs (AC-05 / AC-13)", () => {
2488
+ it('spawn { StrName2: "Alice" } -> world.get returns native JS string', () => {
2489
+ const w = new World();
2490
+ const e = w.spawn({ component: StrName2, data: { value: 'Alice' } }).unwrap();
2491
+ const got = w.get(e, StrName2);
2492
+ if (!got.ok) throw new Error('expected ok');
2493
+ // After w6 dispatch routes through uniqueRefs.resolve(handle).unwrap()
2494
+ // which yields the native JS string payload --- no .get() wrapper.
2495
+ expect(typeof got.value.value).toBe('string');
2496
+ expect(got.value.value).toBe('Alice');
2497
+ });
2498
+
2499
+ it('set updates the payload; subsequent read returns the new string', () => {
2500
+ const w = new World();
2501
+ const e = w.spawn({ component: StrName2, data: { value: 'Alice' } }).unwrap();
2502
+ w.set(e, StrName2, { value: 'Bob' }).unwrap();
2503
+ const got = w.get(e, StrName2);
2504
+ if (!got.ok) throw new Error('expected ok');
2505
+ expect(got.value.value).toBe('Bob');
2506
+ expect(typeof got.value.value).toBe('string');
2507
+ });
2508
+
2509
+ it('spawn with raw=undefined fallbacks to empty string (AC-06)', () => {
2510
+ const w = new World();
2511
+ // After feat-20260517 / M2 / AC-01, ComponentData<S>.data is
2512
+ // Partial<ShapeOf<S>>, so `data: {}` is a valid call -- the
2513
+ // erased dispatch boundary now lives in `fillComponentDefaults`
2514
+ // and spawn must coerce the missing string field to '' rather
2515
+ // than throw or write a garbage handle.
2516
+ const e = w.spawn({ component: StrName2, data: {} }).unwrap();
2517
+ const got = w.get(e, StrName2);
2518
+ if (!got.ok) throw new Error('expected ok');
2519
+ expect(got.value.value).toBe('');
2520
+ });
2521
+ });
2522
+
2523
+ describe("w5 - mixed 'string' + 'unique<T>' single-arm release (AC-05)", () => {
2524
+ it('despawn(e) releases BOTH the string handle AND the ref<T> handle', () => {
2525
+ // The same isManagedField arm must fan out across BOTH fields. This
2526
+ // test is the load-bearing observable for w6's predicate merge: any
2527
+ // future regression that re-splits string vs ref<T> dispatch would
2528
+ // leave one of the two handles live after despawn.
2529
+ const Mat = defineComponent('Mat', {
2530
+ label: { type: 'string' },
2531
+ handle: { type: 'unique<MaterialAsset>' },
2532
+ });
2533
+ const w = new World();
2534
+ const refs = getUniqueRefs(w);
2535
+ const matH = refs.alloc('MaterialAsset', { id: 7 });
2536
+
2537
+ const e = w
2538
+ .spawn({
2539
+ component: Mat,
2540
+ data: {
2541
+ label: 'mat-7',
2542
+ handle: matH,
2543
+ } as never,
2544
+ })
2545
+ .unwrap();
2546
+
2547
+ // Both fields readable pre-despawn.
2548
+ const got = w.get(e, Mat);
2549
+ if (!got.ok) throw new Error('expected ok');
2550
+ expect(got.value.label).toBe('mat-7');
2551
+
2552
+ // Despawn must release the ref<T> handle (existing AC-05 path).
2553
+ w.despawn(e).unwrap();
2554
+ const resolveMat = refs.resolve(matH);
2555
+ expect(resolveMat.ok).toBe(false);
2556
+ if (resolveMat.ok) throw new Error('expected ref handle released');
2557
+ expect(resolveMat.error.code).toBe('unique-ref-stale');
2558
+
2559
+ // The 'string' field's underlying handle must also be released by the
2560
+ // SAME arm. We cannot cheaply observe the internal string-handle, so
2561
+ // the proxy invariant is: after despawn the entity is no longer
2562
+ // readable --- regression in dispatch would either keep the handle
2563
+ // live (no-op release) or throw (separate arm misroute).
2564
+ const reread = w.get(e, Mat);
2565
+ expect(reread.ok).toBe(false);
2566
+ });
2567
+
2568
+ it('removeComponent(e, C) releases BOTH handles on the removed component', () => {
2569
+ const Mat = defineComponent('Mat', {
2570
+ label: { type: 'string' },
2571
+ handle: { type: 'unique<MaterialAsset>' },
2572
+ });
2573
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
2574
+ const w = new World();
2575
+ const refs = getUniqueRefs(w);
2576
+ const matH = refs.alloc('MaterialAsset', { id: 11 });
2577
+
2578
+ const e = w
2579
+ .spawn(
2580
+ {
2581
+ component: Mat,
2582
+ data: {
2583
+ label: 'mat-11',
2584
+ handle: matH,
2585
+ } as never,
2586
+ },
2587
+ { component: Anchor, data: { x: 1 } },
2588
+ )
2589
+ .unwrap();
2590
+
2591
+ w.removeComponent(e, Mat).unwrap();
2592
+
2593
+ const resolveMat = refs.resolve(matH);
2594
+ expect(resolveMat.ok).toBe(false);
2595
+ if (resolveMat.ok) throw new Error('expected ref handle released');
2596
+ expect(resolveMat.error.code).toBe('unique-ref-stale');
2597
+
2598
+ // Anchor remains; the entity is still alive.
2599
+ const got = w.get(e, Anchor);
2600
+ expect(got.ok).toBe(true);
2601
+ });
2602
+
2603
+ it('set on string field releases the prior payload (path 3)', () => {
2604
+ // Field-overwrite path: the prior string handle must be released
2605
+ // BEFORE the new alloc, mirroring the ref<T> path 3 release-then-alloc
2606
+ // invariant (D-5 net-zero on same-bucket reuse).
2607
+ const StrName3 = defineComponent('StrName3', { value: { type: 'string' } });
2608
+ const w = new World();
2609
+ const refs = getUniqueRefs(w);
2610
+ const before = (refs as unknown as { liveCount?: () => number }).liveCount?.() ?? null;
2611
+
2612
+ const e = w.spawn({ component: StrName3, data: { value: 'first' } }).unwrap();
2613
+ w.set(e, StrName3, { value: 'second' }).unwrap();
2614
+ const got = w.get(e, StrName3);
2615
+ if (!got.ok) throw new Error('expected ok');
2616
+ expect(got.value.value).toBe('second');
2617
+ void before;
2618
+ });
2619
+ });
2620
+ }
2621
+ {
2622
+ // --- from string-release.test.ts ---
2623
+ function refsOf(w: World): UniqueRefStore {
2624
+ return (w as unknown as { uniqueRefs: UniqueRefStore }).uniqueRefs;
2625
+ }
2626
+
2627
+ // ---------------------------------------------------------------------------
2628
+ // (1) Source-level grep: world.ts has no independent string release call.
2629
+ // ---------------------------------------------------------------------------
2630
+
2631
+ describe('w11 --- world.ts source has no independent string release branch (AC-05)', () => {
2632
+ it('no `releaseString` / `releaseStringField` / `isStringField` symbol in world.ts', () => {
2633
+ const worldSrc = readFileSync(fileURLToPath(new URL('../world.ts', import.meta.url)), 'utf8');
2634
+ expect(worldSrc.includes('releaseString')).toBe(false);
2635
+ expect(worldSrc.includes('releaseStringField')).toBe(false);
2636
+ expect(worldSrc.includes('isStringField')).toBe(false);
2637
+ });
2638
+ });
2639
+
2640
+ // ---------------------------------------------------------------------------
2641
+ // (2) Behavioural: mixed string + ref<T> entity drops both in one pass.
2642
+ // ---------------------------------------------------------------------------
2643
+
2644
+ describe('w11 --- mixed string + ref<T> release (AC-05 single-arm dispatch)', () => {
2645
+ it('despawn drops BOTH slots in the same loop pass; _liveCount delta = 2', () => {
2646
+ const Mixed = defineComponent('Mixed', {
2647
+ label: { type: 'string' },
2648
+ mat: { type: 'unique<MaterialAsset>' },
2649
+ });
2650
+ const w = new World();
2651
+ const refs = refsOf(w);
2652
+
2653
+ const matHandle = refs.alloc('MaterialAsset', { tint: 0xff_00_00 });
2654
+ const e = w
2655
+ .spawn({
2656
+ component: Mixed,
2657
+ data: {
2658
+ label: 'mixed-entity',
2659
+ mat: matHandle,
2660
+ } as never,
2661
+ })
2662
+ .unwrap();
2663
+
2664
+ const liveBefore = refs._liveCount();
2665
+ w.despawn(e).unwrap();
2666
+ const liveAfter = refs._liveCount();
2667
+
2668
+ // Net release count: -2 = -1 string handle + -1 ref<T> handle.
2669
+ expect(liveBefore - liveAfter).toBe(2);
2670
+
2671
+ // The ref handle resolves as released (string handle is internally
2672
+ // managed, no caller-visible probe).
2673
+ const r = refs.resolve(matHandle);
2674
+ expect(r.ok).toBe(false);
2675
+ if (r.ok) throw new Error('expected released');
2676
+ expect(r.error.code).toBe('unique-ref-stale');
2677
+ });
2678
+ });
2679
+
2680
+ // ---------------------------------------------------------------------------
2681
+ // (3) Despawn / removeComponent / set --- string field is released by the
2682
+ // shared isManagedField arm on every World release path.
2683
+ // ---------------------------------------------------------------------------
2684
+
2685
+ describe('w11 --- string release via despawn / removeComponent / set (AC-05)', () => {
2686
+ it('despawn releases the string handle (path 1)', () => {
2687
+ const Foo = defineComponent('Foo', { v: { type: 'string' } });
2688
+ const w = new World();
2689
+ const refs = refsOf(w);
2690
+ const e = w.spawn({ component: Foo, data: { v: 'hello' } }).unwrap();
2691
+ const liveBefore = refs._liveCount();
2692
+ w.despawn(e).unwrap();
2693
+ expect(refs._liveCount()).toBe(liveBefore - 1);
2694
+ });
2695
+
2696
+ it('removeComponent releases the string handle (path 2)', () => {
2697
+ const Foo = defineComponent('Foo', { v: { type: 'string' } });
2698
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
2699
+ const w = new World();
2700
+ const refs = refsOf(w);
2701
+ const e = w
2702
+ .spawn({ component: Foo, data: { v: 'hello' } }, { component: Anchor, data: { x: 1 } })
2703
+ .unwrap();
2704
+ const liveBefore = refs._liveCount();
2705
+ w.removeComponent(e, Foo).unwrap();
2706
+ expect(refs._liveCount()).toBe(liveBefore - 1);
2707
+ });
2708
+
2709
+ it('set release-then-alloc on the same field produces _liveCount delta 0', () => {
2710
+ const Foo = defineComponent('Foo', { v: { type: 'string' } });
2711
+ const w = new World();
2712
+ const refs = refsOf(w);
2713
+ const e = w.spawn({ component: Foo, data: { v: 'hello' } }).unwrap();
2714
+ const liveBefore = refs._liveCount();
2715
+ w.set(e, Foo, { v: 'world' }).unwrap();
2716
+ // Net change: -1 (release prior) + 1 (alloc new) = 0.
2717
+ expect(refs._liveCount()).toBe(liveBefore);
2718
+ expect(w.get(e, Foo).unwrap().v).toBe('world');
2719
+ });
2720
+ });
2721
+
2722
+ // ---------------------------------------------------------------------------
2723
+ // (4) Sibling isolation: despawn on entity A leaves entity B's string intact.
2724
+ // ---------------------------------------------------------------------------
2725
+
2726
+ describe('w11 --- string release sibling isolation', () => {
2727
+ it('despawn on A does not disturb B`s string in the same archetype', () => {
2728
+ const Foo = defineComponent('Foo', { v: { type: 'string' } });
2729
+ const w = new World();
2730
+ const a = w.spawn({ component: Foo, data: { v: 'aaa' } }).unwrap();
2731
+ const b = w.spawn({ component: Foo, data: { v: 'bbb' } }).unwrap();
2732
+
2733
+ w.despawn(a).unwrap();
2734
+
2735
+ const got = w.get(b, Foo);
2736
+ if (!got.ok) throw new Error('expected ok');
2737
+ expect(got.value.v).toBe('bbb');
2738
+ });
2739
+ });
2740
+ }