@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,2534 @@
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 { componentId, componentSchema } from '../component';
5
+ import { componentDefinition } from '../component-schema';
6
+ import { Update } from '../schedule-token';
7
+ //
8
+ // Source files (N=25):
9
+ // - packages/ecs/__tests__/world-alloc-managed-ref.test.ts
10
+ // - packages/ecs/src/__tests__/commands.test.ts
11
+ // - packages/ecs/src/__tests__/component-reflection.test.ts
12
+ // - packages/ecs/src/__tests__/component.test.ts
13
+ // - packages/ecs/src/__tests__/entity-dangling-removed.test.ts
14
+ // - packages/ecs/src/__tests__/entity-liveness.test.ts
15
+ // - packages/ecs/src/__tests__/entity.test.ts
16
+ // - packages/ecs/src/__tests__/hierarchy-commands.test.ts
17
+ // - packages/ecs/src/__tests__/hierarchy-traversal.test.ts
18
+ // - packages/ecs/src/__tests__/name-component.test.ts
19
+ // - packages/ecs/src/__tests__/name-mutation.test.ts
20
+ // - packages/ecs/src/__tests__/relationship-define-order.test.ts
21
+ // - packages/ecs/src/__tests__/relationship-schema.test.ts
22
+ // - packages/ecs/src/__tests__/relationship-sync.test.ts
23
+ // - packages/ecs/src/__tests__/world-array-reflection.test.ts
24
+ // - packages/ecs/src/__tests__/world-array-view.test.ts
25
+ // - packages/ecs/src/__tests__/world-buffer-fields.test.ts
26
+ // - packages/ecs/src/__tests__/world-core.test.ts
27
+ // - packages/ecs/src/__tests__/world-inspect-systems.test.ts
28
+ // - packages/ecs/src/__tests__/world-integration.test.ts
29
+ // - packages/ecs/src/__tests__/world-managed-refs-non-null.test.ts
30
+ // - packages/ecs/src/__tests__/world-spawn-array-fallback.test.ts
31
+ // - packages/ecs/src/__tests__/world-spawn-defaults.test.ts
32
+ // - packages/ecs/src/__tests__/world-spawn-direct.test.ts
33
+ // - packages/ecs/src/__tests__/component-default-fallback.test.ts
34
+ //
35
+ // Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
36
+ // source as ancestorTitles[0]. Top-level imports merged + deduped.
37
+ //
38
+ // Collision resolutions (value/type name clashes from merge):
39
+ // import { Entity as EntityComponent } from '../entity'
40
+ // + targeted rename in entity-liveness.test.ts body: world.get(..., Entity) -> world.get(..., EntityComponent)
41
+
42
+ import { describe, expect, it } from 'vitest';
43
+ import { type ComponentSchema, TYPE_METADATA } from '../component';
44
+ import { fillComponentDefaults, typeDefault } from '../component-default-fallback';
45
+ import { Entity as EntityComponent } from '../entity';
46
+ import {
47
+ decodeEntity,
48
+ ENTITY_MAX_GENERATION,
49
+ ENTITY_MAX_INDEX,
50
+ ENTITY_NULL_RAW,
51
+ type EntityHandle,
52
+ encodeEntity,
53
+ entityGeneration,
54
+ entityIndex,
55
+ } from '../entity-handle';
56
+ import {
57
+ ComponentAlreadyPresentError,
58
+ ComponentNotPresentError,
59
+ type EcsErrorCode,
60
+ EntityIndexOverflowError,
61
+ ManagedArrayElementTypeNotAllowedError,
62
+ SchemaUnsupportedFieldError,
63
+ StaleEntityError,
64
+ } from '../errors';
65
+ import { defineComponent } from '../index';
66
+ import { UniqueRefStore } from '../unique-ref-store';
67
+ import { World } from '../world';
68
+ import { handleNumeric } from './utils/handle-numeric';
69
+
70
+ {
71
+ // --- from world-alloc-managed-ref.test.ts ---
72
+ describe('feat-20260528 M1 t2 World.allocUniqueRef() public API', () => {
73
+ it('allocUniqueRef returns a branded Handle', () => {
74
+ const world = new World();
75
+ defineComponent('RefTest', { payload: { type: 'unique<Test>' } });
76
+
77
+ const handle = world.allocUniqueRef<'Test', { id: number }>('Test', { id: 42 });
78
+ expect(typeof handle).toBe('number');
79
+ expect(handle).not.toBe(0);
80
+ });
81
+
82
+ it('allocUniqueRef with onRelease: despawn entity carrying ref field triggers callback', () => {
83
+ const world = new World();
84
+ const Holder = defineComponent('Holder', { value: { type: 'unique<Test>' } });
85
+
86
+ let released = false;
87
+ let releasedPayload: { id: number } | undefined;
88
+
89
+ const handle = world.allocUniqueRef<'Test', { id: number }>('Test', { id: 7 }, (p) => {
90
+ released = true;
91
+ releasedPayload = p;
92
+ });
93
+
94
+ const entity = world
95
+ .spawn({
96
+ component: Holder,
97
+ data: { value: handle },
98
+ })
99
+ .unwrap();
100
+
101
+ expect(released).toBe(false);
102
+
103
+ world.despawn(entity);
104
+
105
+ expect(released).toBe(true);
106
+ expect(releasedPayload).toEqual({ id: 7 });
107
+ });
108
+
109
+ it('allocUniqueRef onRelease: set field overwrite triggers callback for old handle', () => {
110
+ const world = new World();
111
+ const Holder = defineComponent('Holder2', { value: { type: 'unique<Test>' } });
112
+
113
+ const released: number[] = [];
114
+
115
+ const h1 = world.allocUniqueRef<'Test', { id: number }>('Test', { id: 1 }, (p) => {
116
+ released.push(p.id);
117
+ });
118
+ const h2 = world.allocUniqueRef<'Test', { id: number }>('Test', { id: 2 }, () => {});
119
+
120
+ const entity = world
121
+ .spawn({
122
+ component: Holder,
123
+ data: { value: h1 },
124
+ })
125
+ .unwrap();
126
+
127
+ // Overwrite h1 with h2 — h1's onRelease should fire.
128
+ world.set(entity, Holder, { value: h2 }).unwrap();
129
+ expect(released).toEqual([1]);
130
+ });
131
+
132
+ it('allocUniqueRef handle identity is preserved through world.get', () => {
133
+ const world = new World();
134
+ const Holder = defineComponent('Holder3', { value: { type: 'unique<Test>' } });
135
+
136
+ const handle = world.allocUniqueRef<'Test', string>('Test', 'hello-world');
137
+
138
+ const entity = world
139
+ .spawn({
140
+ component: Holder,
141
+ data: { value: handle },
142
+ })
143
+ .unwrap();
144
+
145
+ const result = world.get(entity, Holder);
146
+ expect(result.ok).toBe(true);
147
+ if (result.ok) {
148
+ // `ref<T>` fields return the branded u32 handle, not the resolved payload.
149
+ // Identity check: the stored handle matches what we allocated.
150
+ expect(result.value.value).toBe(handle);
151
+ }
152
+ });
153
+
154
+ it('allocUniqueRef onRelease: removeComponent triggers callback', () => {
155
+ const world = new World();
156
+ const Holder = defineComponent('Holder4', { value: { type: 'unique<Test>' } });
157
+
158
+ let released = false;
159
+
160
+ const handle = world.allocUniqueRef<'Test', string>('Test', 'payload', () => {
161
+ released = true;
162
+ });
163
+
164
+ const entity = world
165
+ .spawn({
166
+ component: Holder,
167
+ data: { value: handle },
168
+ })
169
+ .unwrap();
170
+
171
+ world.removeComponent(entity, Holder).unwrap();
172
+ expect(released).toBe(true);
173
+ });
174
+
175
+ it('allocUniqueRef double-release: despawn after set-overwrite does not double-fire (M4: stale on re-use)', () => {
176
+ // After M4 gen increment on release, the old handle's gen mismatches the
177
+ // store's gen after h1's slot is freed and re-allocated. The resolve
178
+ // returns stale — not silent resolve to new payload.
179
+ const world = new World();
180
+ const Holder = defineComponent('Holder5', { value: { type: 'unique<Test>' } });
181
+
182
+ let callCount = 0;
183
+
184
+ const h1 = world.allocUniqueRef<'Test', string>('Test', 'first', () => {
185
+ callCount++;
186
+ });
187
+ const h2 = world.allocUniqueRef<'Test', string>('Test', 'second', () => {});
188
+
189
+ const entity = world
190
+ .spawn({
191
+ component: Holder,
192
+ data: { value: h1 },
193
+ })
194
+ .unwrap();
195
+
196
+ // Overwrite triggers h1's onRelease (gen 0->1).
197
+ world.set(entity, Holder, { value: h2 }).unwrap();
198
+ expect(callCount).toBe(1);
199
+
200
+ // M4: after release, gen incremented. freeSlots = [h1.slot] with gen=1.
201
+ // A fresh allocUniqueRef reuses the slot with gen=1. Resolving old h1
202
+ // (gen=0) now returns stale — gen mismatch (AC-01).
203
+ world.allocUniqueRef<'Test', string>('Test', 'third');
204
+ // slot is the same but gen differs
205
+ // biome-ignore lint/suspicious/noExplicitAny: targeted private read
206
+ const store = (world as any).uniqueRefs as UniqueRefStore;
207
+ const staleResolve = store.resolve<'Test', string>(h1);
208
+ expect(staleResolve.ok).toBe(false);
209
+ if (!staleResolve.ok) {
210
+ expect(staleResolve.error.code).toBe('unique-ref-stale');
211
+ }
212
+
213
+ // Despawn triggers h2's onRelease (no-op), h1 is already released.
214
+ world.despawn(entity);
215
+ expect(callCount).toBe(1); // No double-fire.
216
+ });
217
+ });
218
+ }
219
+
220
+ {
221
+ // --- from commands.test.ts ---
222
+ describe('Deferred spawn/despawn (AC-14)', () => {
223
+ it('spawn during system is deferred — entity not queryable until flush', () => {
224
+ const world = new World();
225
+ const Pos = defineComponent('DfSpawnPos', { x: { type: 'f32' }, y: { type: 'f32' } });
226
+ let entityCountDuringSystem = 0;
227
+
228
+ world.addSystem(Update, {
229
+ name: 'spawner',
230
+ queries: [],
231
+ fn: (_world, _queries, commands) => {
232
+ commands.spawn({ component: Pos, data: { x: 10, y: 20 } });
233
+ },
234
+ });
235
+
236
+ world.addSystem(Update, {
237
+ name: 'counter',
238
+ queries: [{ read: [Pos] }],
239
+ fn: (_world, queryResults) => {
240
+ for (const query of queryResults) entityCountDuringSystem += [...query].length;
241
+ },
242
+ after: ['spawner'],
243
+ });
244
+
245
+ world.update();
246
+ // With edge-derived visibility, counter(after:spawner) sees spawner's deferred entity
247
+ expect(entityCountDuringSystem).toBe(1);
248
+
249
+ // After flush, the entity should exist. Run another update to verify.
250
+ // spawner runs again on the second frame, adding a second entity.
251
+ entityCountDuringSystem = 0;
252
+ world.update();
253
+ expect(entityCountDuringSystem).toBe(2);
254
+ });
255
+
256
+ it('despawn during system is deferred — entity still visible until flush', () => {
257
+ const world = new World();
258
+ const Tag = defineComponent('DfDespawnTag', { v: { type: 'i32' } });
259
+ const e = world.spawn({ component: Tag, data: { v: 42 } }).unwrap();
260
+
261
+ let entitySeenDuringSystem = false;
262
+
263
+ world.addSystem(Update, {
264
+ name: 'destroyer',
265
+ queries: [],
266
+ fn: (_world, _queries, commands) => {
267
+ commands.despawn(e);
268
+ },
269
+ });
270
+
271
+ world.addSystem(Update, {
272
+ name: 'checker',
273
+ queries: [{ read: [Tag] }],
274
+ fn: (_world, queryResults) => {
275
+ for (const query of queryResults) {
276
+ if ([...query].length > 0) entitySeenDuringSystem = true;
277
+ }
278
+ },
279
+ after: ['destroyer'],
280
+ });
281
+
282
+ world.update();
283
+ // With edge-derived visibility, checker(after:destroyer) does NOT see
284
+ // the entity after destroyer's deferred despawn is applied
285
+ expect(entitySeenDuringSystem).toBe(false);
286
+ });
287
+
288
+ it('addComponent during system is deferred', () => {
289
+ const world = new World();
290
+ const Pos = defineComponent('DfAddPos', { x: { type: 'f32' } });
291
+ const Vel = defineComponent('DfAddVel', { vx: { type: 'f32' } });
292
+ const e = world.spawn({ component: Pos, data: { x: 1 } }).unwrap();
293
+
294
+ world.addSystem(Update, {
295
+ name: 'adder',
296
+ queries: [],
297
+ fn: (_world, _queries, commands) => {
298
+ commands.addComponent(e, { component: Vel, data: { vx: 5 } });
299
+ },
300
+ });
301
+
302
+ world.update();
303
+ // After flush, entity should have Vel
304
+ const vel = world.get(e, Vel).unwrap();
305
+ expect(vel).toEqual({ vx: 5 });
306
+ });
307
+
308
+ it('removeComponent during system is deferred', () => {
309
+ const world = new World();
310
+ const Pos = defineComponent('DfRmPos', { x: { type: 'f32' } });
311
+ const Vel = defineComponent('DfRmVel', { vx: { type: 'f32' } });
312
+ const e = world
313
+ .spawn({ component: Pos, data: { x: 1 } }, { component: Vel, data: { vx: 5 } })
314
+ .unwrap();
315
+
316
+ world.addSystem(Update, {
317
+ name: 'remover',
318
+ queries: [],
319
+ fn: (_world, _queries, commands) => {
320
+ commands.removeComponent(e, Vel);
321
+ },
322
+ });
323
+
324
+ world.update();
325
+ // After flush, entity should no longer have Vel
326
+ expect(world.get(e, Vel).ok).toBe(false);
327
+ });
328
+ });
329
+
330
+ describe('Deferred spawn pending handle (E-06)', () => {
331
+ it('deferred spawn returns a pending Entity handle with valid id', () => {
332
+ const world = new World();
333
+ const Pos = defineComponent('PendPos', { x: { type: 'f32' } });
334
+ let pendingEntity: EntityHandle | undefined;
335
+
336
+ world.addSystem(Update, {
337
+ name: 'spawner',
338
+ queries: [],
339
+ fn: (_world, _queries, commands) => {
340
+ pendingEntity = commands.spawn({ component: Pos, data: { x: 99 } });
341
+ // The handle should be a valid number
342
+ expect(typeof pendingEntity).toBe('number');
343
+ // biome-ignore lint/style/noNonNullAssertion: pendingEntity is assigned on the line above
344
+ expect(entityIndex(pendingEntity!)).toBeGreaterThanOrEqual(0);
345
+ },
346
+ });
347
+
348
+ world.update();
349
+ expect(pendingEntity).toBeDefined();
350
+ });
351
+
352
+ it('commands.isDeferred(entity) returns true for pending entity', () => {
353
+ const world = new World();
354
+ const Pos = defineComponent('IsDfPos', { x: { type: 'f32' } });
355
+
356
+ world.addSystem(Update, {
357
+ name: 'checker',
358
+ queries: [],
359
+ fn: (_world, _queries, commands) => {
360
+ const pending = commands.spawn({ component: Pos, data: { x: 1 } });
361
+ expect(commands.isDeferred(pending)).toBe(true);
362
+ },
363
+ });
364
+
365
+ world.update();
366
+ });
367
+
368
+ it('get on pending entity returns undefined before flush (not throw)', () => {
369
+ const world = new World();
370
+ const Pos = defineComponent('PendGetPos', { x: { type: 'f32' } });
371
+ let pendingEntity: EntityHandle | undefined;
372
+
373
+ world.addSystem(Update, {
374
+ name: 'spawner',
375
+ queries: [],
376
+ fn: (_world, _queries, commands) => {
377
+ pendingEntity = commands.spawn({ component: Pos, data: { x: 42 } });
378
+ },
379
+ });
380
+
381
+ world.update();
382
+ // After flush, the entity should be accessible
383
+ expect(pendingEntity).toBeDefined();
384
+ // biome-ignore lint/style/noNonNullAssertion: pendingEntity is confirmed defined by the expect above
385
+ const val = world.get(pendingEntity!, Pos).unwrap();
386
+ expect(val).toEqual({ x: 42 });
387
+ });
388
+ });
389
+
390
+ describe('Flush cascade', () => {
391
+ it('flush processes commands produced during flush itself', () => {
392
+ const world = new World();
393
+ const Tag = defineComponent('CascadeTag', { v: { type: 'i32' } });
394
+
395
+ // Test that multiple commands in one frame all flush.
396
+ world.addSystem(Update, {
397
+ name: 'multiSpawner',
398
+ queries: [],
399
+ fn: (_world, _queries, commands) => {
400
+ commands.spawn({ component: Tag, data: { v: 1 } });
401
+ commands.spawn({ component: Tag, data: { v: 2 } });
402
+ commands.spawn({ component: Tag, data: { v: 3 } });
403
+ },
404
+ });
405
+
406
+ world.update();
407
+
408
+ // After flush, all 3 entities should exist
409
+ let count = 0;
410
+ world.addSystem(Update, {
411
+ name: 'counter',
412
+ queries: [{ read: [Tag] }],
413
+ fn: (_world, queryResults) => {
414
+ for (const query of queryResults) count += [...query].length;
415
+ },
416
+ });
417
+
418
+ world.update();
419
+ expect(count).toBe(3);
420
+ });
421
+ });
422
+
423
+ describe('Flush updates query cache', () => {
424
+ it('query sees entities spawned by deferred commands after flush', () => {
425
+ const world = new World();
426
+ const Pos = defineComponent('FlushQPos', { x: { type: 'f32' } });
427
+
428
+ // First update: deferred spawn
429
+ world.addSystem(Update, {
430
+ name: 'spawner',
431
+ queries: [],
432
+ fn: (_world, _queries, commands) => {
433
+ commands.spawn({ component: Pos, data: { x: 1 } });
434
+ },
435
+ });
436
+
437
+ world.update();
438
+
439
+ // Replace spawner with a no-op
440
+ // Use a new system to count — the query should see the flushed entity
441
+ let found = 0;
442
+ world.addSystem(Update, {
443
+ name: 'verifier',
444
+ queries: [{ read: [Pos] }],
445
+ fn: (_world, queryResults) => {
446
+ for (const query of queryResults) found += [...query].length;
447
+ },
448
+ });
449
+
450
+ world.update();
451
+ expect(found).toBeGreaterThanOrEqual(1);
452
+ });
453
+ });
454
+ }
455
+ {
456
+ // --- from component-reflection.test.ts ---
457
+ describe('defineComponent reflection — component.fields (AC-01 layer 3)', () => {
458
+ it('exposes per-field reflection carrying type and default', () => {
459
+ const C = defineComponent('FieldsCarrier', { x: 'f32', y: 'i32' });
460
+ expect(C.fields.x).toBeDefined();
461
+ expect(C.fields.x?.type).toBe('f32');
462
+ expect(C.fields.y?.type).toBe('i32');
463
+ });
464
+
465
+ it('component.fields is frozen (read-only)', () => {
466
+ const C = defineComponent('FieldsFrozen', { x: 'f32' });
467
+ expect(Object.isFrozen(C.fields)).toBe(true);
468
+ expect(Object.isFrozen(C.fields.x)).toBe(true);
469
+ });
470
+
471
+ it('array field carries arrayMeta { elementType, length? }', () => {
472
+ const C = defineComponent('FieldsArray', { v: 'array<f32, 3>' });
473
+ expect(C.fields.v?.arrayMeta).toEqual({ elementType: 'f32', length: 3 });
474
+ });
475
+
476
+ it('non-array field has no arrayMeta', () => {
477
+ const C = defineComponent('FieldsScalar', { x: 'f32' });
478
+ expect(C.fields.x?.arrayMeta).toBeUndefined();
479
+ });
480
+ });
481
+
482
+ describe('defineComponent reflection — derived projections (D-A7 / D-A8)', () => {
483
+ it('component schema is derived from fields[k].type by the owner projection', () => {
484
+ const C = defineComponent('SchemaProjection', {
485
+ x: { type: 'f32', default: 1 },
486
+ v: 'array<f32, 3>',
487
+ });
488
+ expect(componentSchema(C)).toEqual({ x: 'f32', v: 'array<f32, 3>' });
489
+ });
490
+
491
+ it('owner defaults are derived from fields[k].default', () => {
492
+ const C = defineComponent('DefaultsProjection', {
493
+ x: { type: 'f32', default: 5 },
494
+ });
495
+ expect(componentDefinition(C).defaults).toMatchObject({ x: 5 });
496
+ });
497
+ });
498
+
499
+ describe('TYPE_METADATA — global per-type metadata table (AC-01 layer 2)', () => {
500
+ it('is importable and queryable', () => {
501
+ expect(TYPE_METADATA).toBeDefined();
502
+ expect(typeof TYPE_METADATA).toBe('object');
503
+ });
504
+
505
+ it('covers the 11 legacy scalar keys', () => {
506
+ for (const k of [
507
+ 'f32',
508
+ 'f64',
509
+ 'i32',
510
+ 'u32',
511
+ 'i16',
512
+ 'u16',
513
+ 'i8',
514
+ 'u8',
515
+ 'bool',
516
+ 'enum',
517
+ 'ref',
518
+ ]) {
519
+ expect(TYPE_METADATA[k]).toBeDefined();
520
+ }
521
+ });
522
+
523
+ it('covers the 6 vocab-family keys', () => {
524
+ for (const k of ['entity', 'string', 'buffer', 'ref', 'shared', 'array']) {
525
+ expect(TYPE_METADATA[k]).toBeDefined();
526
+ }
527
+ });
528
+
529
+ it('each scalar row carries the required columns', () => {
530
+ const f32 = TYPE_METADATA.f32;
531
+ expect(f32?.byteSize).toBe(4);
532
+ expect(f32?.viewCtor).toBe(Float32Array);
533
+ expect(f32?.isScalar).toBe(true);
534
+ expect(f32?.isManaged).toBe(false);
535
+ });
536
+
537
+ it('vocab rows expose the predicate columns', () => {
538
+ expect(TYPE_METADATA.entity?.isEntityRef).toBe(true);
539
+ expect(TYPE_METADATA.buffer?.isBuffer).toBe(true);
540
+ expect(TYPE_METADATA.ref?.isManaged).toBe(true);
541
+ expect(TYPE_METADATA.array?.isArray).toBe(true);
542
+ });
543
+ });
544
+
545
+ describe('defineComponent reflection — AC-03(c) pre-parse reuse', () => {
546
+ it('returns the same arrayMeta object reference across repeated reads', () => {
547
+ // Parse happens once at registration time; the cached reflection object is
548
+ // reused — no per-read re-parse. Same token, same array field, identical ref.
549
+ const C = defineComponent('PreParseReuse', { v: 'array<f32, 16>' });
550
+ const a = C.fields.v?.arrayMeta;
551
+ const b = C.fields.v?.arrayMeta;
552
+ expect(a).toBe(b);
553
+ });
554
+ });
555
+
556
+ // ────────────────────────────────────────────────────────────────────────────
557
+ // [w4] arrayMeta length-sentinel parsing + AI-user error messages.
558
+ // ────────────────────────────────────────────────────────────────────────────
559
+
560
+ describe('arrayMeta length sentinel (AC-01 / D-A1)', () => {
561
+ it('fixed-capacity array<f32, 3> carries an explicit length', () => {
562
+ const C = defineComponent('ArrFixed', { v: 'array<f32, 3>' });
563
+ expect(C.fields.v?.arrayMeta).toEqual({ elementType: 'f32', length: 3 });
564
+ });
565
+
566
+ it('variable-capacity array<f32> has no length field (length === undefined => variable)', () => {
567
+ const C = defineComponent('ArrVariable', { v: 'array<f32>' });
568
+ expect(C.fields.v?.arrayMeta).toEqual({ elementType: 'f32' });
569
+ expect(C.fields.v?.arrayMeta?.length).toBeUndefined();
570
+ // Bare length sentinel: the variable case has no own `length` key at all
571
+ // (no isVariable / kind discriminant, D-A1 user ruling).
572
+ expect('length' in (C.fields.v?.arrayMeta as object)).toBe(false);
573
+ });
574
+
575
+ it('non-array field carries no arrayMeta', () => {
576
+ const C = defineComponent('NoArr', { x: 'u32', s: 'string' });
577
+ expect(C.fields.x?.arrayMeta).toBeUndefined();
578
+ expect(C.fields.s?.arrayMeta).toBeUndefined();
579
+ });
580
+ });
581
+
582
+ describe('defineComponent fail-fast throw (charter P3 / OOS-6)', () => {
583
+ it('field-descriptor missing type throws with the field name + expected shape', () => {
584
+ expect(() =>
585
+ // Missing `type` on the descriptor object — programmer error.
586
+ defineComponent('MissingType', { x: { default: 0 } as unknown as { type: 'f32' } }),
587
+ ).toThrow(SchemaUnsupportedFieldError);
588
+ try {
589
+ defineComponent('MissingType2', { broken: { default: 0 } as unknown as { type: 'f32' } });
590
+ expect.unreachable('should have thrown');
591
+ } catch (e) {
592
+ expect((e as Error).message).toContain('broken');
593
+ expect((e as Error).message).toContain('type');
594
+ }
595
+ });
596
+
597
+ it('illegal array element type still throws ManagedArrayElementTypeNotAllowedError', () => {
598
+ expect(() => defineComponent('BadArr', { v: 'array<ref<X>>' as 'array<f32, 3>' })).toThrow(
599
+ ManagedArrayElementTypeNotAllowedError,
600
+ );
601
+ });
602
+ });
603
+
604
+ // ────────────────────────────────────────────────────────────────────────────
605
+ // [w15] AC-10 naming disambiguation: input `fields` vs reflection `component.fields`.
606
+ // ────────────────────────────────────────────────────────────────────────────
607
+
608
+ describe('AC-10 input fields vs reflection component.fields naming disambiguation', () => {
609
+ it('component.fields[field].type is the reflection (same as the input type keyword)', () => {
610
+ const C = defineComponent('Disambig', { x: { type: 'f32', default: 0 } });
611
+ // Reflection: component.fields[x].type === 'f32'
612
+ expect(C.fields.x?.type).toBe('f32');
613
+ // Reflection: component.fields[x].default === 0 (as supplied)
614
+ expect(C.fields.x?.default).toBe(0);
615
+ // Input fields: the second-argument fields is consumed at registration;
616
+ // component.fields is the read-only reflection surface on the token.
617
+ // Both are named "fields" but occupy distinct scopes (parameter vs
618
+ // property) — per D-A3, this does not constitute a name collision.
619
+ });
620
+ });
621
+ }
622
+ {
623
+ // --- from component.test.ts ---
624
+ describe('defineComponent', () => {
625
+ it('returns a frozen token with the three public facts', () => {
626
+ const Pos = defineComponent('Position', { x: { type: 'f32' }, y: { type: 'f32' } });
627
+ expect(Pos.name).toBe('Position');
628
+ expect(Pos.fields.x?.type).toBe('f32');
629
+ expect(Pos.fields.y?.type).toBe('f32');
630
+ expect(Reflect.ownKeys(Pos)).toEqual(['name', 'fields', 'storage']);
631
+ expect(Object.isFrozen(Pos)).toBe(true);
632
+ });
633
+
634
+ it('keeps numeric identity in the owner helper, outside the token', () => {
635
+ const Vel = defineComponent('Velocity', { vx: { type: 'f32' }, vy: { type: 'f32' } });
636
+ expect(typeof componentId(Vel)).toBe('number');
637
+ expect(componentId(Vel)).toBeGreaterThanOrEqual(0);
638
+ });
639
+
640
+ it('assigns auto-incrementing ComponentId', () => {
641
+ const A = defineComponent('CompA', { a: { type: 'f32' } });
642
+ const B = defineComponent('CompB', { b: { type: 'f32' } });
643
+ expect(componentId(B)).toBe(componentId(A) + 1);
644
+ });
645
+
646
+ it('supports all 11 scalar field types', () => {
647
+ const allTypes: ComponentSchema = {
648
+ f: 'f32',
649
+ d: 'f64',
650
+ i: 'i32',
651
+ u: 'u32',
652
+ s: 'i16',
653
+ us: 'u16',
654
+ b: 'i8',
655
+ ub: 'u8',
656
+ flag: 'bool',
657
+ e: 'enum',
658
+ r: 'ref',
659
+ };
660
+ const comp = defineComponent('AllTypes', allTypes);
661
+ expect(componentSchema(comp)).toEqual(allTypes);
662
+ });
663
+
664
+ it('supports tag component (empty schema {})', () => {
665
+ const Player = defineComponent('Player', {});
666
+ expect(Player.name).toBe('Player');
667
+ expect(componentSchema(Player)).toEqual({});
668
+ expect(typeof componentId(Player)).toBe('number');
669
+ });
670
+
671
+ it('throws SchemaUnsupportedFieldError for invalid field type', () => {
672
+ expect(() => defineComponent('Bad', { x: { type: 'vec3' as 'f32' } })).toThrow(
673
+ SchemaUnsupportedFieldError,
674
+ );
675
+ });
676
+ });
677
+ }
678
+ {
679
+ // --- from entity-dangling-removed.test.ts ---
680
+ describe('drop-entity-dangling-sweep - read path no longer guards liveness', () => {
681
+ it('despawn target -> holder entity field reads back the original raw u32 (AC-02)', () => {
682
+ const ChildOf = defineComponent('ChildOf', { parent: { type: 'entity' } });
683
+ const Tag = defineComponent('Tag', { v: { type: 'u32' } });
684
+ const w = new World();
685
+
686
+ const parent = w.spawn({ component: Tag, data: { v: 1 } }).unwrap();
687
+ const child = w.spawn({ component: ChildOf, data: { parent } }).unwrap();
688
+
689
+ // Live read returns the parent reference unchanged.
690
+ const before = w.get(child, ChildOf).unwrap();
691
+ expect(before.parent).toBe(parent);
692
+
693
+ // Despawn the referenced target: the stored u32 still encodes the old
694
+ // (slot, gen) pair, but no record exists for it anymore.
695
+ w.despawn(parent).unwrap();
696
+
697
+ // New contract: the read returns the same raw u32 verbatim -- no clearing,
698
+ // no error, no component removal. The consumer is responsible for liveness.
699
+ const after = w.get(child, ChildOf);
700
+ expect(after.ok).toBe(true);
701
+ if (!after.ok) throw new Error('expected ok read after target despawn');
702
+ expect(after.value.parent).not.toBeNull();
703
+ expect(handleNumeric(after.value.parent as number)).toBe(handleNumeric(parent));
704
+ });
705
+
706
+ it('explicit null entity field round-trips through write/read (AC-06)', () => {
707
+ const Link = defineComponent('Link', { target: { type: 'entity' } });
708
+ const w = new World();
709
+
710
+ const e = w.spawn({ component: Link, data: { target: null } }).unwrap();
711
+ const read = w.get(e, Link).unwrap();
712
+ expect(read.target).toBeNull();
713
+
714
+ // The null sentinel constant is intact (OOS-1).
715
+ expect(ENTITY_NULL_RAW).toBe(0xffffffff);
716
+ });
717
+ });
718
+ }
719
+ {
720
+ // --- from entity-liveness.test.ts ---
721
+ let dummyCounter = 0;
722
+ function defineTag(prefix: string) {
723
+ dummyCounter += 1;
724
+ return defineComponent(`${prefix}_${dummyCounter}`, {});
725
+ }
726
+
727
+ function recordGeneration(world: World, slot: number): number | undefined {
728
+ const records = (world as unknown as { records: { generation: number }[] }).records;
729
+ return records[slot]?.generation;
730
+ }
731
+
732
+ /**
733
+ * Liveness probe (feat-20260602: `world.isAlive` retired): a handle is alive iff
734
+ * `world.get(e, Entity)` resolves; a stale / pending handle yields `err`.
735
+ */
736
+ function isAlive(world: World, entity: EntityHandle): boolean {
737
+ return world.get(entity, EntityComponent).ok;
738
+ }
739
+
740
+ describe('despawn absorbs alive into generation (AC-11)', () => {
741
+ it('bumps the slot generation by 1 after despawn', () => {
742
+ const world = new World();
743
+ const Tag = defineTag('AC11_Tag');
744
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
745
+ const slot = entityIndex(e);
746
+ const genBefore = recordGeneration(world, slot);
747
+ expect(genBefore).toBe(0);
748
+
749
+ world.despawn(e).unwrap();
750
+ expect(recordGeneration(world, slot)).toBe(1);
751
+ });
752
+
753
+ it('makes the old handle read back as stale via world.get(e, EntityComponent)', () => {
754
+ const world = new World();
755
+ const Tag = defineTag('AC11_Stale');
756
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
757
+ world.despawn(e).unwrap();
758
+ const r = world.get(e, EntityComponent);
759
+ expect(r.ok).toBe(false);
760
+ if (!r.ok) expect(r.error.code).toBe('stale-entity');
761
+ expect(isAlive(world, e)).toBe(false);
762
+ });
763
+ });
764
+
765
+ describe('entityCount sums live archetype rows (AC-12)', () => {
766
+ it('reports the live count after spawn N / despawn M', () => {
767
+ const world = new World();
768
+ const Tag = defineTag('AC12_Tag');
769
+ const a = world.spawn({ component: Tag, data: {} }).unwrap();
770
+ world.spawn({ component: Tag, data: {} }).unwrap();
771
+ world.spawn({ component: Tag, data: {} }).unwrap();
772
+ expect(world.inspect().entityCount).toBe(3);
773
+
774
+ world.despawn(a).unwrap();
775
+ expect(world.inspect().entityCount).toBe(2);
776
+ });
777
+
778
+ it('is zero for a fresh world and after despawning all entities', () => {
779
+ const world = new World();
780
+ const Tag = defineTag('AC12_Empty');
781
+ expect(world.inspect().entityCount).toBe(0);
782
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
783
+ world.despawn(e).unwrap();
784
+ expect(world.inspect().entityCount).toBe(0);
785
+ });
786
+ });
787
+
788
+ describe('generation > 255 retires the slot permanently (AC-13)', () => {
789
+ it('retires the index after the 256th generation and staleness holds for the old handle', () => {
790
+ const world = new World();
791
+ const Tag = defineTag('AC13_Tag');
792
+
793
+ // Cycle the same slot through gen 0..255 (256 generations).
794
+ let e = world.spawn({ component: Tag, data: {} }).unwrap();
795
+ const slot = entityIndex(e);
796
+ for (let gen = 0; gen < 255; gen++) {
797
+ expect(entityIndex(e)).toBe(slot);
798
+ expect(entityGeneration(e)).toBe(gen);
799
+ world.despawn(e).unwrap();
800
+ e = world.spawn({ component: Tag, data: {} }).unwrap();
801
+ }
802
+ // Now e is the gen=255 incarnation of the slot.
803
+ expect(entityIndex(e)).toBe(slot);
804
+ expect(entityGeneration(e)).toBe(255);
805
+
806
+ // Despawning the gen=255 entity bumps the stored generation to 256 (out of
807
+ // the 8-bit handle range) and permanently retires the slot.
808
+ world.despawn(e).unwrap();
809
+ expect(recordGeneration(world, slot)).toBe(256);
810
+
811
+ // The old gen=255 handle is dead, and a fresh spawn does NOT reuse the slot.
812
+ expect(isAlive(world, e)).toBe(false);
813
+ const fresh = world.spawn({ component: Tag, data: {} }).unwrap();
814
+ expect(entityIndex(fresh)).not.toBe(slot);
815
+ });
816
+ });
817
+
818
+ describe('deferred-spawn (pending) semantics are unchanged (AC-14)', () => {
819
+ it('a pending entity is not yet live inside the system, then live after flush', () => {
820
+ const world = new World();
821
+ const Tag = defineTag('AC14_Pending');
822
+ let pending: EntityHandle | undefined;
823
+
824
+ world.addSystem(Update, {
825
+ name: 'ac14-spawner',
826
+ queries: [],
827
+ fn: (_world, _queries, commands) => {
828
+ pending = commands.spawn({ component: Tag, data: {} });
829
+ // Inside the system, before flush: the slot is pending, so not live.
830
+ // biome-ignore lint/style/noNonNullAssertion: assigned on the line above
831
+ expect(isAlive(world, pending!)).toBe(false);
832
+ },
833
+ });
834
+
835
+ world.update();
836
+
837
+ // After update flushes the command buffer: materialized + live, carrying
838
+ // its own handle in the id=0 Entity column.
839
+ expect(pending).toBeDefined();
840
+ // biome-ignore lint/style/noNonNullAssertion: assigned inside the system above
841
+ const e = pending!;
842
+ expect(isAlive(world, e)).toBe(true);
843
+ expect(world.get(e, EntityComponent).unwrap().self).toBe(e);
844
+ });
845
+ });
846
+ }
847
+ {
848
+ // --- from entity.test.ts ---
849
+ describe('encodeEntity / decodeEntity roundtrip', () => {
850
+ it('roundtrips index=0, generation=0', () => {
851
+ const e = encodeEntity(0, 0);
852
+ const { index, generation } = decodeEntity(e);
853
+ expect(index).toBe(0);
854
+ expect(generation).toBe(0);
855
+ });
856
+
857
+ it('roundtrips index=ENTITY_MAX_INDEX, generation=0', () => {
858
+ const e = encodeEntity(ENTITY_MAX_INDEX, 0);
859
+ const { index, generation } = decodeEntity(e);
860
+ expect(index).toBe(ENTITY_MAX_INDEX);
861
+ expect(generation).toBe(0);
862
+ });
863
+
864
+ it('roundtrips index=0, generation=ENTITY_MAX_GENERATION', () => {
865
+ const e = encodeEntity(0, ENTITY_MAX_GENERATION);
866
+ const { index, generation } = decodeEntity(e);
867
+ expect(index).toBe(0);
868
+ expect(generation).toBe(ENTITY_MAX_GENERATION);
869
+ });
870
+
871
+ it('roundtrips index=ENTITY_MAX_INDEX, generation=ENTITY_MAX_GENERATION', () => {
872
+ const e = encodeEntity(ENTITY_MAX_INDEX, ENTITY_MAX_GENERATION);
873
+ const { index, generation } = decodeEntity(e);
874
+ expect(index).toBe(ENTITY_MAX_INDEX);
875
+ expect(generation).toBe(ENTITY_MAX_GENERATION);
876
+ });
877
+
878
+ // ToInt32 trap: gen >= 128 causes (gen << 24) to be negative in JS.
879
+ // The >>> 0 correction must handle this.
880
+ it('handles generation >= 128 (ToInt32 trap)', () => {
881
+ for (const gen of [128, 200, 255]) {
882
+ const e = encodeEntity(42, gen);
883
+ const { index, generation } = decodeEntity(e);
884
+ expect(index).toBe(42);
885
+ expect(generation).toBe(gen);
886
+ }
887
+ });
888
+
889
+ it('roundtrips arbitrary (index, generation) pairs', () => {
890
+ const pairs: [number, number][] = [
891
+ [1, 1],
892
+ [100, 50],
893
+ [0xffffff, 0xff],
894
+ [12345, 127],
895
+ [12345, 128],
896
+ [0, 255],
897
+ ];
898
+ for (const [idx, gen] of pairs) {
899
+ const e = encodeEntity(idx, gen);
900
+ expect(entityIndex(e)).toBe(idx);
901
+ expect(entityGeneration(e)).toBe(gen);
902
+ }
903
+ });
904
+ });
905
+
906
+ describe('encodeEntity overflow', () => {
907
+ it('throws EntityIndexOverflowError for index > ENTITY_MAX_INDEX', () => {
908
+ expect(() => encodeEntity(ENTITY_MAX_INDEX + 1, 0)).toThrow(EntityIndexOverflowError);
909
+ });
910
+
911
+ it('throws EntityIndexOverflowError for negative index', () => {
912
+ expect(() => encodeEntity(-1, 0)).toThrow(EntityIndexOverflowError);
913
+ });
914
+ });
915
+
916
+ describe('entityIndex / entityGeneration helpers', () => {
917
+ it('extracts index correctly', () => {
918
+ const e = encodeEntity(999, 42);
919
+ expect(entityIndex(e)).toBe(999);
920
+ });
921
+
922
+ it('extracts generation correctly', () => {
923
+ const e = encodeEntity(999, 42);
924
+ expect(entityGeneration(e)).toBe(42);
925
+ });
926
+ });
927
+
928
+ describe('Entity branded type', () => {
929
+ it('encodeEntity returns a number', () => {
930
+ const e = encodeEntity(0, 0);
931
+ expect(typeof e).toBe('number');
932
+ });
933
+ });
934
+
935
+ describe('generation retirement (AC-03, E-08)', () => {
936
+ // These tests verify the retirement *constants* and *encoding* behavior.
937
+ // The actual World-level retirement logic (not pushing to free list) is
938
+ // tested in world-core.test.ts (w11).
939
+
940
+ it('ENTITY_MAX_GENERATION equals 255', () => {
941
+ expect(ENTITY_MAX_GENERATION).toBe(255);
942
+ });
943
+
944
+ it('ENTITY_MAX_INDEX equals 2^24 - 1', () => {
945
+ expect(ENTITY_MAX_INDEX).toBe(16_777_215);
946
+ });
947
+
948
+ it('generation field is exactly 8 bits (masks to 0xff)', () => {
949
+ // Encoding with generation > 255 should mask to lower 8 bits
950
+ const e = encodeEntity(0, 256);
951
+ expect(entityGeneration(e)).toBe(0);
952
+ });
953
+ });
954
+ }
955
+ // `Name` component (schema literal + identity + AC-13 invariant + spawn/set
956
+ // fallback) tests migrated to packages/runtime/src/__tests__/name.unit.test.ts
957
+ // by tweak-20260612-ecs-concept-compression — `Name` now lives in
958
+ // @forgeax/engine-runtime (it is a built-in component, not part of the ECS
959
+ // framework itself).
960
+ {
961
+ // --- from relationship-define-order.test.ts ---
962
+ }
963
+ {
964
+ // --- from relationship-schema.test.ts ---
965
+ }
966
+ {
967
+ // --- from world-buffer-fields.test.ts ---
968
+ describe('w13 - World buffer:<N> field 3-path release', () => {
969
+ it('spawn allocs buffer slot of schema-declared bytes; get returns live view', () => {
970
+ const Skin = defineComponent('Skin', { palette: { type: 'buffer<128>' } });
971
+ const w = new World();
972
+ const seed = new Uint8Array(128);
973
+ for (let i = 0; i < 128; i++) seed[i] = (i + 3) & 0xff;
974
+ const e = w.spawn({ component: Skin, data: { palette: seed } }).unwrap();
975
+ const r = w.get(e, Skin);
976
+ if (!r.ok) throw new Error('expected ok get');
977
+ expect(r.value.palette).toBeInstanceOf(Uint8Array);
978
+ expect(r.value.palette.byteLength).toBe(128);
979
+ for (let i = 0; i < 128; i++) expect(r.value.palette[i]).toBe((i + 3) & 0xff);
980
+ });
981
+
982
+ it('despawn(e) releases buffer field slots (path 1) - free-list reuse confirms', () => {
983
+ const Skin = defineComponent('Skin', { palette: { type: 'buffer<128>' } });
984
+ const w = new World();
985
+ const e1 = w.spawn({ component: Skin, data: { palette: new Uint8Array(128) } }).unwrap();
986
+ const r1 = w.get(e1, Skin);
987
+ if (!r1.ok) throw new Error('expected ok get1');
988
+ // Identity probe via fingerprint: stamp byte 0 = 0xAA before despawn so
989
+ // the free-list reuse on the next spawn observably resets it (D-5 alloc
990
+ // zeroes recycled bytes).
991
+ r1.value.palette[0] = 0xaa;
992
+ w.despawn(e1).unwrap();
993
+ const e2 = w.spawn({ component: Skin, data: { palette: new Uint8Array(128) } }).unwrap();
994
+ const r2 = w.get(e2, Skin);
995
+ if (!r2.ok) throw new Error('expected ok get2');
996
+ expect(r2.value.palette[0]).toBe(0); // recycled slot is zeroed.
997
+ });
998
+
999
+ it('removeComponent(e, C) releases buffer fields on the removed component (path 2)', () => {
1000
+ const Skin = defineComponent('Skin', { palette: { type: 'buffer<64>' } });
1001
+ const Anchor = defineComponent('Anchor', { x: { type: 'f32' } });
1002
+ const w = new World();
1003
+ const e = w
1004
+ .spawn(
1005
+ { component: Skin, data: { palette: new Uint8Array(64) } },
1006
+ { component: Anchor, data: { x: 1 } },
1007
+ )
1008
+ .unwrap();
1009
+ expect(() => w.removeComponent(e, Skin).unwrap()).not.toThrow();
1010
+ // Skin gone; Anchor stays.
1011
+ const rs = w.get(e, Skin);
1012
+ expect(rs.ok).toBe(false);
1013
+ if (rs.ok) throw new Error('expected err get Skin');
1014
+ expect(rs.error.code).toBe('component-not-present');
1015
+ const ra = w.get(e, Anchor);
1016
+ if (!ra.ok) throw new Error('expected ok get Anchor');
1017
+ expect(ra.value.x).toBe(1);
1018
+ });
1019
+
1020
+ it('set(e, C, { field: Uint8Array }) copies bytes into existing slot (path 3)', () => {
1021
+ const Skin = defineComponent('Skin', { palette: { type: 'buffer<32>' } });
1022
+ const w = new World();
1023
+ const initial = new Uint8Array(32);
1024
+ for (let i = 0; i < 32; i++) initial[i] = i;
1025
+ const e = w.spawn({ component: Skin, data: { palette: initial } }).unwrap();
1026
+
1027
+ // Snapshot the live view BEFORE set; D-7 guarantees set does NOT realloc,
1028
+ // so the same view stays valid after the bytes flow through.
1029
+ const before = w.get(e, Skin);
1030
+ if (!before.ok) throw new Error('expected ok get');
1031
+ const liveView = before.value.palette;
1032
+
1033
+ const replacement = new Uint8Array(32);
1034
+ for (let i = 0; i < 32; i++) replacement[i] = (i * 11) & 0xff;
1035
+ w.set(e, Skin, { palette: replacement }).unwrap();
1036
+
1037
+ const after = w.get(e, Skin);
1038
+ if (!after.ok) throw new Error('expected ok get after');
1039
+ for (let i = 0; i < 32; i++) expect(after.value.palette[i]).toBe((i * 11) & 0xff);
1040
+ // The view captured before the set still reflects the new bytes
1041
+ // (same backing slot - D-7 simplification, no realloc on set).
1042
+ for (let i = 0; i < 32; i++) expect(liveView[i]).toBe((i * 11) & 0xff);
1043
+ });
1044
+ });
1045
+ }
1046
+ {
1047
+ // --- from world-core.test.ts ---
1048
+ describe('World.spawn', () => {
1049
+ it('single-component spawn returns a valid Entity handle', () => {
1050
+ const world = new World();
1051
+ const Pos = defineComponent('WPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1052
+ const e = world.spawn({ component: Pos, data: { x: 1, y: 2 } }).unwrap();
1053
+ expect(typeof e).toBe('number');
1054
+ expect(entityIndex(e)).toBeGreaterThanOrEqual(0);
1055
+ expect(entityGeneration(e)).toBe(0);
1056
+ });
1057
+
1058
+ it('multi-component spawn targets correct archetype (AC-06)', () => {
1059
+ const world = new World();
1060
+ const Pos = defineComponent('MCPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1061
+ const Vel = defineComponent('MCVel', { vx: { type: 'f32' }, vy: { type: 'f32' } });
1062
+ const e = world
1063
+ .spawn({ component: Pos, data: { x: 1, y: 2 } }, { component: Vel, data: { vx: 3, vy: 4 } })
1064
+ .unwrap();
1065
+ // Verify both components are readable
1066
+ const pos = world.get(e, Pos).unwrap();
1067
+ const vel = world.get(e, Vel).unwrap();
1068
+ expect(pos).toEqual({ x: 1, y: 2 });
1069
+ expect(vel).toEqual({ vx: 3, vy: 4 });
1070
+ });
1071
+ });
1072
+
1073
+ describe('World.get / World.set', () => {
1074
+ it('get returns correct field values', () => {
1075
+ const world = new World();
1076
+ const Hp = defineComponent('WHp', { current: { type: 'i32' }, max: { type: 'i32' } });
1077
+ const e = world.spawn({ component: Hp, data: { current: 100, max: 200 } }).unwrap();
1078
+ const hp = world.get(e, Hp).unwrap();
1079
+ expect(hp).toEqual({ current: 100, max: 200 });
1080
+ });
1081
+
1082
+ it('set partially updates fields', () => {
1083
+ const world = new World();
1084
+ const Pos = defineComponent('SetPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1085
+ const e = world.spawn({ component: Pos, data: { x: 1, y: 2 } }).unwrap();
1086
+ world.set(e, Pos, { x: 10 }).unwrap();
1087
+ const pos = world.get(e, Pos).unwrap();
1088
+ expect(pos).toEqual({ x: 10, y: 2 });
1089
+ });
1090
+ });
1091
+
1092
+ describe('World.despawn', () => {
1093
+ it('despawn marks entity as dead', () => {
1094
+ const world = new World();
1095
+ const Tag = defineComponent('DTag', {});
1096
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
1097
+ world.despawn(e);
1098
+ // get on despawned entity should return err(StaleEntityError)
1099
+ const result = world.get(e, Tag);
1100
+ expect(result.ok).toBe(false);
1101
+ expect(!result.ok && result.error).toBeInstanceOf(StaleEntityError);
1102
+ });
1103
+
1104
+ it('generation retirement: gen=255 → index not recycled (E-08)', () => {
1105
+ const world = new World();
1106
+ const Tag = defineComponent('RetTag', {});
1107
+
1108
+ let entity = world.spawn({ component: Tag, data: {} }).unwrap();
1109
+ const idx = entityIndex(entity);
1110
+
1111
+ for (let gen = 0; gen < 254; gen++) {
1112
+ world.despawn(entity);
1113
+ entity = world.spawn({ component: Tag, data: {} }).unwrap();
1114
+ expect(entityIndex(entity)).toBe(idx);
1115
+ expect(entityGeneration(entity)).toBe(gen + 1);
1116
+ }
1117
+
1118
+ world.despawn(entity);
1119
+ entity = world.spawn({ component: Tag, data: {} }).unwrap();
1120
+ expect(entityIndex(entity)).toBe(idx);
1121
+ expect(entityGeneration(entity)).toBe(255);
1122
+
1123
+ world.despawn(entity);
1124
+
1125
+ const newEntity = world.spawn({ component: Tag, data: {} }).unwrap();
1126
+ expect(entityIndex(newEntity)).not.toBe(idx);
1127
+ });
1128
+
1129
+ it('despawn stale handle is idempotent (E-01, AC-17)', () => {
1130
+ const world = new World();
1131
+ const Tag = defineComponent('IdempTag', {});
1132
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
1133
+ world.despawn(e);
1134
+ // Second despawn should return ok (idempotent)
1135
+ const result = world.despawn(e);
1136
+ expect(result.ok).toBe(true);
1137
+ });
1138
+ });
1139
+
1140
+ describe('World stale handle errors (E-02)', () => {
1141
+ it('get on stale handle returns err(StaleEntityError)', () => {
1142
+ const world = new World();
1143
+ const C = defineComponent('StaleGet', { v: { type: 'f32' } });
1144
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1145
+ world.despawn(e);
1146
+ const result = world.get(e, C);
1147
+ expect(result.ok).toBe(false);
1148
+ expect(!result.ok && result.error).toBeInstanceOf(StaleEntityError);
1149
+ expect(!result.ok && (result.error as StaleEntityError).operation).toBe('get');
1150
+ expect(!result.ok && (result.error as StaleEntityError).component).toBe('StaleGet');
1151
+ });
1152
+
1153
+ it('set on stale handle returns err(StaleEntityError)', () => {
1154
+ const world = new World();
1155
+ const C = defineComponent('StaleSet', { v: { type: 'f32' } });
1156
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1157
+ world.despawn(e);
1158
+ const result = world.set(e, C, { v: 2 });
1159
+ expect(result.ok).toBe(false);
1160
+ expect(!result.ok && result.error).toBeInstanceOf(StaleEntityError);
1161
+ expect(!result.ok && (result.error as StaleEntityError).operation).toBe('set');
1162
+ expect(!result.ok && (result.error as StaleEntityError).component).toBe('StaleSet');
1163
+ });
1164
+ });
1165
+
1166
+ describe('World.addComponent / World.removeComponent', () => {
1167
+ it('addComponent adds a new component and triggers archetype migration (AC-07)', () => {
1168
+ const world = new World();
1169
+ const Pos = defineComponent('AddPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1170
+ const Vel = defineComponent('AddVel', { vx: { type: 'f32' }, vy: { type: 'f32' } });
1171
+ const e = world.spawn({ component: Pos, data: { x: 1, y: 2 } }).unwrap();
1172
+
1173
+ world.addComponent(e, { component: Vel, data: { vx: 3, vy: 4 } }).unwrap();
1174
+
1175
+ expect(world.get(e, Pos).unwrap()).toEqual({ x: 1, y: 2 });
1176
+ expect(world.get(e, Vel).unwrap()).toEqual({ vx: 3, vy: 4 });
1177
+ });
1178
+
1179
+ it('removeComponent removes component and triggers archetype migration (AC-07)', () => {
1180
+ const world = new World();
1181
+ const Pos = defineComponent('RmPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1182
+ const Vel = defineComponent('RmVel', { vx: { type: 'f32' }, vy: { type: 'f32' } });
1183
+ const e = world
1184
+ .spawn({ component: Pos, data: { x: 1, y: 2 } }, { component: Vel, data: { vx: 3, vy: 4 } })
1185
+ .unwrap();
1186
+
1187
+ world.removeComponent(e, Vel).unwrap();
1188
+
1189
+ expect(world.get(e, Pos).unwrap()).toEqual({ x: 1, y: 2 });
1190
+ expect(world.get(e, Vel).ok).toBe(false);
1191
+ });
1192
+
1193
+ it('addComponent on already present component returns err(ComponentAlreadyPresentError) (E-03)', () => {
1194
+ const world = new World();
1195
+ const Pos = defineComponent('DupPos', { x: { type: 'f32' } });
1196
+ const e = world.spawn({ component: Pos, data: { x: 1 } }).unwrap();
1197
+ const result = world.addComponent(e, { component: Pos, data: { x: 2 } });
1198
+ expect(result.ok).toBe(false);
1199
+ expect(!result.ok && result.error).toBeInstanceOf(ComponentAlreadyPresentError);
1200
+ });
1201
+
1202
+ it('removeComponent on absent component returns err(ComponentNotPresentError) (E-04)', () => {
1203
+ const world = new World();
1204
+ const Pos = defineComponent('AbsPos', { x: { type: 'f32' } });
1205
+ const Vel = defineComponent('AbsVel', { vx: { type: 'f32' } });
1206
+ const e = world.spawn({ component: Pos, data: { x: 1 } }).unwrap();
1207
+ const result = world.removeComponent(e, Vel);
1208
+ expect(result.ok).toBe(false);
1209
+ expect(!result.ok && result.error).toBeInstanceOf(ComponentNotPresentError);
1210
+ });
1211
+
1212
+ it('addComponent on stale entity returns err(StaleEntityError)', () => {
1213
+ const world = new World();
1214
+ const C = defineComponent('StaleAdd', { v: { type: 'f32' } });
1215
+ const C2 = defineComponent('StaleAdd2', { w: { type: 'f32' } });
1216
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1217
+ world.despawn(e);
1218
+ const result = world.addComponent(e, { component: C2, data: { w: 1 } });
1219
+ expect(result.ok).toBe(false);
1220
+ expect(!result.ok && result.error).toBeInstanceOf(StaleEntityError);
1221
+ expect(!result.ok && (result.error as StaleEntityError).operation).toBe('addComponent');
1222
+ expect(!result.ok && (result.error as StaleEntityError).component).toBe('StaleAdd2');
1223
+ });
1224
+
1225
+ it('removeComponent on stale entity returns err(StaleEntityError)', () => {
1226
+ const world = new World();
1227
+ const C = defineComponent('StaleRm', { v: { type: 'f32' } });
1228
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1229
+ world.despawn(e);
1230
+ const result = world.removeComponent(e, C);
1231
+ expect(result.ok).toBe(false);
1232
+ expect(!result.ok && result.error).toBeInstanceOf(StaleEntityError);
1233
+ expect(!result.ok && (result.error as StaleEntityError).operation).toBe('removeComponent');
1234
+ expect(!result.ok && (result.error as StaleEntityError).component).toBe('StaleRm');
1235
+ });
1236
+ });
1237
+
1238
+ // ────────────────────────────────────────────────────────────────────────────
1239
+ // [w5] World method Result path tests (red phase — AP-8 Layer 1)
1240
+ // ────────────────────────────────────────────────────────────────────────────
1241
+
1242
+ describe('World.spawn — Result path', () => {
1243
+ it('spawn returns ok(Entity) on success', () => {
1244
+ const world = new World();
1245
+ const Pos = defineComponent('RSpawnPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1246
+ const result = world.spawn({ component: Pos, data: { x: 1, y: 2 } });
1247
+ // Result should be ok
1248
+ expect(result.ok).toBe(true);
1249
+ expect(result.ok).toBe(true);
1250
+ const entity = result.unwrap();
1251
+ expect(typeof entity).toBe('number');
1252
+ expect(entityIndex(entity)).toBeGreaterThanOrEqual(0);
1253
+ expect(entityGeneration(entity)).toBe(0);
1254
+ });
1255
+ });
1256
+
1257
+ describe('World.despawn — Result path', () => {
1258
+ it('despawn returns ok(void) on live entity', () => {
1259
+ const world = new World();
1260
+ const Tag = defineComponent('RDespawnTag', {});
1261
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
1262
+ const result = world.despawn(e);
1263
+ expect(result.ok).toBe(true);
1264
+ });
1265
+
1266
+ it('despawn on stale handle returns ok (idempotent, E-01)', () => {
1267
+ const world = new World();
1268
+ const Tag = defineComponent('RDespawnStale', {});
1269
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
1270
+ world.despawn(e);
1271
+ // Second despawn should still return ok (idempotent)
1272
+ const result = world.despawn(e);
1273
+ expect(result.ok).toBe(true);
1274
+ });
1275
+ });
1276
+
1277
+ describe('World.get — Result path', () => {
1278
+ it('get returns ok(data) on live entity with component', () => {
1279
+ const world = new World();
1280
+ const Hp = defineComponent('RGetHp', { current: { type: 'i32' }, max: { type: 'i32' } });
1281
+ const e = world.spawn({ component: Hp, data: { current: 100, max: 200 } }).unwrap();
1282
+ const result = world.get(e, Hp);
1283
+ expect(result.ok).toBe(true);
1284
+ expect(result.unwrap()).toEqual({ current: 100, max: 200 });
1285
+ });
1286
+
1287
+ it('get on stale entity returns err(StaleEntityError)', () => {
1288
+ const world = new World();
1289
+ const C = defineComponent('RGetStale', { v: { type: 'f32' } });
1290
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1291
+ world.despawn(e);
1292
+ const result = world.get(e, C);
1293
+ expect(result.ok).toBe(false);
1294
+ const error = !result.ok ? result.error : undefined;
1295
+ expect(error).toBeInstanceOf(StaleEntityError);
1296
+ expect(error?.code).toBe('stale-entity');
1297
+ expect(error?.hint).toBeDefined();
1298
+ });
1299
+
1300
+ it('get on entity missing the component returns err(ComponentNotPresentError)', () => {
1301
+ const world = new World();
1302
+ const A = defineComponent('RGetMissA', { v: { type: 'f32' } });
1303
+ const B = defineComponent('RGetMissB', { w: { type: 'i32' } });
1304
+ const e = world.spawn({ component: A, data: { v: 1 } }).unwrap();
1305
+ const result = world.get(e, B);
1306
+ expect(result.ok).toBe(false);
1307
+ const error = !result.ok ? result.error : undefined;
1308
+ expect(error).toBeInstanceOf(ComponentNotPresentError);
1309
+ expect(error?.code).toBe('component-not-present');
1310
+ });
1311
+
1312
+ it('hasComponent checks presence without allocating a missing/stale Result error', () => {
1313
+ const world = new World();
1314
+ const A = defineComponent('RHasComponentA', { v: { type: 'f32' } });
1315
+ const B = defineComponent('RHasComponentB', { w: { type: 'i32' } });
1316
+ const e = world.spawn({ component: A, data: { v: 1 } }).unwrap();
1317
+
1318
+ expect(world.hasComponent(e, A)).toBe(true);
1319
+ expect(world.hasComponent(e, B)).toBe(false);
1320
+ expect(world.hasComponent(0xdeadbeef as never, A)).toBe(false);
1321
+ });
1322
+ });
1323
+
1324
+ describe('World.set — Result path', () => {
1325
+ it('set returns ok(void) on success', () => {
1326
+ const world = new World();
1327
+ const Pos = defineComponent('RSetPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1328
+ const e = world.spawn({ component: Pos, data: { x: 1, y: 2 } }).unwrap();
1329
+ const result = world.set(e, Pos, { x: 10 });
1330
+ expect(result.ok).toBe(true);
1331
+ expect(world.get(e, Pos).unwrap()).toEqual({ x: 10, y: 2 });
1332
+ });
1333
+
1334
+ it('set on stale entity returns err(StaleEntityError)', () => {
1335
+ const world = new World();
1336
+ const C = defineComponent('RSetStale', { v: { type: 'f32' } });
1337
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1338
+ world.despawn(e);
1339
+ const result = world.set(e, C, { v: 2 });
1340
+ expect(result.ok).toBe(false);
1341
+ expect(!result.ok && result.error).toBeInstanceOf(StaleEntityError);
1342
+ });
1343
+
1344
+ it('set on entity missing the component returns err(ComponentNotPresentError) — F-02', () => {
1345
+ const world = new World();
1346
+ const A = defineComponent('RSetMissA', { v: 'f32' });
1347
+ const B = defineComponent('RSetMissB', { w: 'i32' });
1348
+ const e = world.spawn({ component: A, data: { v: 1 } }).unwrap();
1349
+ const result = world.set(e, B, { w: 99 });
1350
+ expect(result.ok).toBe(false);
1351
+ const error = !result.ok ? result.error : undefined;
1352
+ expect(error).toBeInstanceOf(ComponentNotPresentError);
1353
+ expect(error?.code).toBe('component-not-present');
1354
+ expect(error?.hint).toBeDefined();
1355
+ });
1356
+ });
1357
+
1358
+ describe('World.addComponent — Result path', () => {
1359
+ it('addComponent returns ok(void) on success', () => {
1360
+ const world = new World();
1361
+ const Pos = defineComponent('RAddPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1362
+ const Vel = defineComponent('RAddVel', { vx: { type: 'f32' }, vy: { type: 'f32' } });
1363
+ const e = world.spawn({ component: Pos, data: { x: 1, y: 2 } }).unwrap();
1364
+ const result = world.addComponent(e, { component: Vel, data: { vx: 3, vy: 4 } });
1365
+ expect(result.ok).toBe(true);
1366
+ expect(world.get(e, Vel).unwrap()).toEqual({ vx: 3, vy: 4 });
1367
+ });
1368
+
1369
+ it('addComponent on stale entity returns err(StaleEntityError)', () => {
1370
+ const world = new World();
1371
+ const C = defineComponent('RAddStale', { v: { type: 'f32' } });
1372
+ const C2 = defineComponent('RAddStale2', { w: { type: 'f32' } });
1373
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1374
+ world.despawn(e);
1375
+ const result = world.addComponent(e, { component: C2, data: { w: 1 } });
1376
+ expect(result.ok).toBe(false);
1377
+ expect(!result.ok && result.error).toBeInstanceOf(StaleEntityError);
1378
+ });
1379
+
1380
+ it('addComponent on already present component returns err(ComponentAlreadyPresentError)', () => {
1381
+ const world = new World();
1382
+ const Pos = defineComponent('RAddDup', { x: { type: 'f32' } });
1383
+ const e = world.spawn({ component: Pos, data: { x: 1 } }).unwrap();
1384
+ const result = world.addComponent(e, { component: Pos, data: { x: 2 } });
1385
+ expect(result.ok).toBe(false);
1386
+ const error = !result.ok ? result.error : undefined;
1387
+ expect(error).toBeInstanceOf(ComponentAlreadyPresentError);
1388
+ expect(error?.code).toBe('component-already-present');
1389
+ });
1390
+ });
1391
+
1392
+ describe('World.removeComponent — Result path', () => {
1393
+ it('removeComponent returns ok(void) on success', () => {
1394
+ const world = new World();
1395
+ const Pos = defineComponent('RRmPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1396
+ const Vel = defineComponent('RRmVel', { vx: { type: 'f32' }, vy: { type: 'f32' } });
1397
+ const e = world
1398
+ .spawn({ component: Pos, data: { x: 1, y: 2 } }, { component: Vel, data: { vx: 3, vy: 4 } })
1399
+ .unwrap();
1400
+ const result = world.removeComponent(e, Vel);
1401
+ expect(result.ok).toBe(true);
1402
+ });
1403
+
1404
+ it('removeComponent on stale entity returns err(StaleEntityError)', () => {
1405
+ const world = new World();
1406
+ const C = defineComponent('RRmStale', { v: { type: 'f32' } });
1407
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1408
+ world.despawn(e);
1409
+ const result = world.removeComponent(e, C);
1410
+ expect(result.ok).toBe(false);
1411
+ expect(!result.ok && result.error).toBeInstanceOf(StaleEntityError);
1412
+ });
1413
+
1414
+ it('removeComponent on absent component returns err(ComponentNotPresentError)', () => {
1415
+ const world = new World();
1416
+ const Pos = defineComponent('RRmAbs', { x: { type: 'f32' } });
1417
+ const Vel = defineComponent('RRmAbsVel', { vx: { type: 'f32' } });
1418
+ const e = world.spawn({ component: Pos, data: { x: 1 } }).unwrap();
1419
+ const result = world.removeComponent(e, Vel);
1420
+ expect(result.ok).toBe(false);
1421
+ const error = !result.ok ? result.error : undefined;
1422
+ expect(error).toBeInstanceOf(ComponentNotPresentError);
1423
+ expect(error?.code).toBe('component-not-present');
1424
+ });
1425
+ });
1426
+
1427
+ describe('World — additional branch coverage', () => {
1428
+ it('set on entity without the target component returns err(ComponentNotPresentError) — F-02', () => {
1429
+ const world = new World();
1430
+ const A = defineComponent('CovA', { v: 'f32' });
1431
+ const B = defineComponent('CovB', { w: 'i32' });
1432
+ const e = world.spawn({ component: A, data: { v: 1 } }).unwrap();
1433
+ // B is registered but entity doesn't have it — returns err (F-02)
1434
+ const result = world.set(e, B, { w: 99 });
1435
+ expect(result.ok).toBe(false);
1436
+ expect(!result.ok && result.error).toBeInstanceOf(ComponentNotPresentError);
1437
+ // A's value should be unchanged
1438
+ expect(world.get(e, A).unwrap()).toEqual({ v: 1 });
1439
+ });
1440
+
1441
+ it('set with an unknown field name silently skips that field', () => {
1442
+ const world = new World();
1443
+ const C = defineComponent('CovC', { v: { type: 'f32' } });
1444
+ const e = world.spawn({ component: C, data: { v: 5 } }).unwrap();
1445
+ // Pass a partial with a field name not in the schema
1446
+ world
1447
+ .set(e, C, { nonexistent: 42 } as Record<string, unknown> as Partial<{ v: number }>)
1448
+ .unwrap();
1449
+ // v should be unchanged
1450
+ expect(world.get(e, C).unwrap()).toEqual({ v: 5 });
1451
+ });
1452
+
1453
+ it('get on entity missing the component returns err(ComponentNotPresentError)', () => {
1454
+ const world = new World();
1455
+ const A = defineComponent('CovGetA', { v: { type: 'f32' } });
1456
+ const B = defineComponent('CovGetB', { w: { type: 'i32' } });
1457
+ const e = world.spawn({ component: A, data: { v: 1 } }).unwrap();
1458
+ const result = world.get(e, B);
1459
+ expect(result.ok).toBe(false);
1460
+ expect(!result.ok && result.error).toBeInstanceOf(ComponentNotPresentError);
1461
+ });
1462
+
1463
+ it('migration with swap-pop updates swapped entity record correctly', () => {
1464
+ const world = new World();
1465
+ const A = defineComponent('SwapA', { v: { type: 'f32' } });
1466
+ const B = defineComponent('SwapB', { w: { type: 'f32' } });
1467
+ const e1 = world.spawn({ component: A, data: { v: 1 } }).unwrap();
1468
+ const e2 = world.spawn({ component: A, data: { v: 2 } }).unwrap();
1469
+ const e3 = world.spawn({ component: A, data: { v: 3 } }).unwrap();
1470
+
1471
+ world.addComponent(e1, { component: B, data: { w: 10 } }).unwrap();
1472
+
1473
+ expect(world.get(e1, A).unwrap()).toEqual({ v: 1 });
1474
+ expect(world.get(e1, B).unwrap()).toEqual({ w: 10 });
1475
+ expect(world.get(e2, A).unwrap()).toEqual({ v: 2 });
1476
+ expect(world.get(e3, A).unwrap()).toEqual({ v: 3 });
1477
+ });
1478
+
1479
+ it('removeComponent with swap-pop preserves all entity data', () => {
1480
+ const world = new World();
1481
+ const A = defineComponent('RmSwapA', { v: { type: 'f32' } });
1482
+ const B = defineComponent('RmSwapB', { w: { type: 'f32' } });
1483
+
1484
+ const e1 = world
1485
+ .spawn({ component: A, data: { v: 1 } }, { component: B, data: { w: 10 } })
1486
+ .unwrap();
1487
+ const e2 = world
1488
+ .spawn({ component: A, data: { v: 2 } }, { component: B, data: { w: 20 } })
1489
+ .unwrap();
1490
+ const e3 = world
1491
+ .spawn({ component: A, data: { v: 3 } }, { component: B, data: { w: 30 } })
1492
+ .unwrap();
1493
+
1494
+ world.removeComponent(e1, B).unwrap();
1495
+
1496
+ expect(world.get(e1, A).unwrap()).toEqual({ v: 1 });
1497
+ expect(world.get(e1, B).ok).toBe(false);
1498
+ expect(world.get(e2, A).unwrap()).toEqual({ v: 2 });
1499
+ expect(world.get(e2, B).unwrap()).toEqual({ w: 20 });
1500
+ expect(world.get(e3, A).unwrap()).toEqual({ v: 3 });
1501
+ expect(world.get(e3, B).unwrap()).toEqual({ w: 30 });
1502
+ });
1503
+
1504
+ it('pending entity: get/set return data after flush (E-06 branch)', () => {
1505
+ const world = new World();
1506
+ const A = defineComponent('PendA', { v: { type: 'f32' } });
1507
+
1508
+ let pendingEntity: EntityHandle | undefined;
1509
+ world.addSystem(Update, {
1510
+ name: 'deferSpawn',
1511
+ queries: [],
1512
+ fn: (_world, _results, commands) => {
1513
+ pendingEntity = commands.spawn({ component: A, data: { v: 42 } });
1514
+ expect(commands.isDeferred(pendingEntity)).toBe(true);
1515
+ },
1516
+ });
1517
+
1518
+ world.update();
1519
+
1520
+ expect(pendingEntity).toBeDefined();
1521
+ // biome-ignore lint/style/noNonNullAssertion: controlled test context
1522
+ expect(world.get(pendingEntity!, A).unwrap()).toEqual({ v: 42 });
1523
+ });
1524
+
1525
+ it('bool field stores true/false correctly through set', () => {
1526
+ const world = new World();
1527
+ const C = defineComponent('BoolCov', { flag: { type: 'bool' } });
1528
+ const e = world.spawn({ component: C, data: { flag: true } }).unwrap();
1529
+ expect(world.get(e, C).unwrap()).toEqual({ flag: true });
1530
+ world.set(e, C, { flag: false }).unwrap();
1531
+ expect(world.get(e, C).unwrap()).toEqual({ flag: false });
1532
+ });
1533
+
1534
+ it('auto-registers component on spawn if not registered', () => {
1535
+ const world = new World();
1536
+ const C = defineComponent('AutoReg', { v: { type: 'f32' } });
1537
+ const e = world.spawn({ component: C, data: { v: 1 } }).unwrap();
1538
+ expect(world.get(e, C).unwrap()).toEqual({ v: 1 });
1539
+ const e2 = world.spawn({ component: C, data: { v: 2 } }).unwrap();
1540
+ expect(world.get(e2, C).unwrap()).toEqual({ v: 2 });
1541
+ });
1542
+ });
1543
+
1544
+ // ────────────────────────────────────────────────────────────────────────────
1545
+ // [w8] per-World ID isolation tests (red phase — O-3)
1546
+ // ────────────────────────────────────────────────────────────────────────────
1547
+
1548
+ describe('component identity — global owner projection', () => {
1549
+ it('the same token keeps one owner id across Worlds, distinct tokens differ', () => {
1550
+ const Pos = defineComponent('GlobalIdPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1551
+ const Vel = defineComponent('GlobalIdVel', { vx: { type: 'f32' }, vy: { type: 'f32' } });
1552
+
1553
+ // The single id source is the global owner projection; it is the same value
1554
+ // regardless of which World (or how many) registers the token.
1555
+ const worldA = new World();
1556
+ const worldB = new World();
1557
+ // Both Worlds are created; no per-World register step exists — the token's
1558
+ // global id is stable regardless of how many Worlds observe it.
1559
+ expect(worldA).not.toBe(worldB);
1560
+ expect(typeof componentId(Pos)).toBe('number');
1561
+ // Same token -> same id in every World (no per-World re-numbering).
1562
+ expect(componentId(Pos)).toBe(componentId(Pos));
1563
+ // Distinct tokens -> distinct ids.
1564
+ expect(componentId(Pos)).not.toBe(componentId(Vel));
1565
+ });
1566
+ });
1567
+ }
1568
+ {
1569
+ // --- from world-inspect-systems.test.ts ---
1570
+ describe('world.inspect().systems (AC-05)', () => {
1571
+ it('exposes systems as Array<{ name: string }>', () => {
1572
+ const world = new World();
1573
+ world.addSystem(Update, { name: 'sysA', queries: [], fn: () => {} });
1574
+
1575
+ const snap = world.inspect();
1576
+ expect(Array.isArray(snap.systems)).toBe(true);
1577
+ expect(snap.systems[0]).toEqual({ name: 'sysA', sets: [] });
1578
+ });
1579
+
1580
+ it('keeps systemCount === systems.length as a derived invariant', () => {
1581
+ const world = new World();
1582
+ expect(world.inspect().systems.length).toBe(world.inspect().systemCount);
1583
+
1584
+ world.addSystem(Update, { name: 'sysA', queries: [], fn: () => {} });
1585
+ world.addSystem(Update, { name: 'sysB', queries: [], fn: () => {} });
1586
+ world.addSystem(Update, { name: 'sysC', queries: [], fn: () => {} });
1587
+
1588
+ const snap = world.inspect();
1589
+ expect(snap.systems.length).toBe(snap.systemCount);
1590
+ });
1591
+
1592
+ it('grows systems.length by N after registering N systems on a fresh World', () => {
1593
+ const world = new World();
1594
+ const baseline = world.inspect().systems.length;
1595
+
1596
+ world.addSystem(Update, { name: 'sA', queries: [], fn: () => {} });
1597
+ world.addSystem(Update, { name: 'sB', queries: [], fn: () => {} });
1598
+
1599
+ expect(world.inspect().systems.length).toBe(baseline + 2);
1600
+ });
1601
+
1602
+ it('every system entry has a non-empty name', () => {
1603
+ const world = new World();
1604
+ world.addSystem(Update, { name: 'movement', queries: [], fn: () => {} });
1605
+ world.addSystem(Update, { name: 'render', queries: [], fn: () => {} });
1606
+
1607
+ const snap = world.inspect();
1608
+ for (const entry of snap.systems) {
1609
+ expect(typeof entry.name).toBe('string');
1610
+ expect(entry.name.length).toBeGreaterThan(0);
1611
+ }
1612
+ });
1613
+ });
1614
+ }
1615
+ {
1616
+ // --- from world-integration.test.ts ---
1617
+ describe('Scenario 1: Complete lifecycle — spawn → query → system → deferred flush → despawn', () => {
1618
+ it('full frame cycle works end to end', () => {
1619
+ const world = new World();
1620
+
1621
+ // Define components
1622
+ const Position = defineComponent('IntPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1623
+ const Velocity = defineComponent('IntVel', { vx: { type: 'f32' }, vy: { type: 'f32' } });
1624
+
1625
+ // Spawn entities
1626
+ const e1 = world
1627
+ .spawn(
1628
+ { component: Position, data: { x: 0, y: 0 } },
1629
+ { component: Velocity, data: { vx: 1, vy: 2 } },
1630
+ )
1631
+ .unwrap();
1632
+ const e2 = world
1633
+ .spawn(
1634
+ { component: Position, data: { x: 10, y: 20 } },
1635
+ { component: Velocity, data: { vx: -1, vy: -2 } },
1636
+ )
1637
+ .unwrap();
1638
+
1639
+ // Register a movement system that integrates velocity into position
1640
+ world.addSystem(Update, {
1641
+ name: 'movement',
1642
+ queries: [{ read: [Velocity], write: [Position] }],
1643
+ fn: (_world, queryResults) => {
1644
+ for (const query of queryResults) {
1645
+ for (const row of query) {
1646
+ const position = row.mut(Position);
1647
+ const velocity = row.get(Velocity);
1648
+ position.x += velocity.vx;
1649
+ position.y += velocity.vy;
1650
+ }
1651
+ }
1652
+ },
1653
+ });
1654
+
1655
+ // Register a system that deferred-despawns entities at x > 5
1656
+ world.addSystem(Update, {
1657
+ name: 'despawner',
1658
+ queries: [{ with: [Position] }],
1659
+ fn: (_world, _queryResults, commands) => {
1660
+ // Deferred despawn e2
1661
+ commands.despawn(e2);
1662
+ },
1663
+ after: ['movement'],
1664
+ });
1665
+
1666
+ // Frame 1: movement runs, then despawner queues despawn
1667
+ world.update();
1668
+
1669
+ // After frame 1: positions should be updated
1670
+ const pos1 = world.get(e1, Position).unwrap();
1671
+ expect(pos1).toEqual({ x: 1, y: 2 }); // 0+1, 0+2
1672
+
1673
+ // e2 should be despawned after flush
1674
+ expect(world.get(e2, Position).ok).toBe(false);
1675
+
1676
+ // Frame 2: only e1 remains, position updates again
1677
+ world.update();
1678
+ const pos1Frame2 = world.get(e1, Position).unwrap();
1679
+ expect(pos1Frame2).toEqual({ x: 2, y: 4 }); // 1+1, 2+2
1680
+ });
1681
+
1682
+ it('deferred spawn is visible in next frame queries', () => {
1683
+ const world = new World();
1684
+ const Tag = defineComponent('LifecycleTag', { value: { type: 'i32' } });
1685
+
1686
+ let spawnedCount = 0;
1687
+
1688
+ // System that spawns an entity on first frame
1689
+ let frame = 0;
1690
+ world.addSystem(Update, {
1691
+ name: 'spawner',
1692
+ queries: [],
1693
+ fn: (_world, _queries, commands) => {
1694
+ if (frame === 0) {
1695
+ commands.spawn({ component: Tag, data: { value: 42 } });
1696
+ }
1697
+ frame++;
1698
+ },
1699
+ });
1700
+
1701
+ world.addSystem(Update, {
1702
+ name: 'counter',
1703
+ queries: [{ read: [Tag] }],
1704
+ fn: (_world, queryResults) => {
1705
+ spawnedCount = 0;
1706
+ for (const query of queryResults) spawnedCount += [...query].length;
1707
+ },
1708
+ after: ['spawner'],
1709
+ });
1710
+
1711
+ // Frame 1: spawner defers spawn, counter sees 0 during this frame
1712
+ world.update();
1713
+ // After flush, entity exists. counter in frame 1 saw 0.
1714
+
1715
+ // Frame 2: counter should see the spawned entity
1716
+ world.update();
1717
+ expect(spawnedCount).toBe(1);
1718
+ });
1719
+
1720
+ it('resources are accessible across frames', () => {
1721
+ const world = new World();
1722
+ world.insertResource('frameCount', { count: 0 });
1723
+
1724
+ world.addSystem(Update, {
1725
+ name: 'frameCounter',
1726
+ queries: [],
1727
+ fn: () => {
1728
+ const fc = world.getResource<{ count: number }>('frameCount');
1729
+ fc.count += 1;
1730
+ },
1731
+ });
1732
+
1733
+ world.update();
1734
+ world.update();
1735
+ world.update();
1736
+
1737
+ expect(world.getResource<{ count: number }>('frameCount')).toEqual({ count: 3 });
1738
+ });
1739
+ });
1740
+
1741
+ describe('Scenario 2: hot/cold mixed archetype', () => {
1742
+ // Note: In the current implementation, all 11 field types are scalar,
1743
+ // so "cold-table" only triggers for non-scalar types which don't exist yet.
1744
+ // This test verifies that hot components with different TypedArray backing
1745
+ // work correctly in the same archetype and query returns correct TypedArray views.
1746
+
1747
+ it('archetype with multiple hot components returns correct TypedArray views per field', () => {
1748
+ const world = new World();
1749
+
1750
+ const Position = defineComponent('MixPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1751
+ const Health = defineComponent('MixHp', { current: { type: 'i32' }, max: { type: 'i32' } });
1752
+ const Flags = defineComponent('MixFlags', { active: { type: 'bool' } });
1753
+
1754
+ // Spawn entity with all three components (different TypedArray backing types)
1755
+ world.spawn(
1756
+ { component: Position, data: { x: 1.5, y: 2.5 } },
1757
+ { component: Health, data: { current: 100, max: 200 } },
1758
+ { component: Flags, data: { active: true } },
1759
+ );
1760
+
1761
+ // Query for Position + Health (hot-table, Float32Array + Int32Array)
1762
+ let x: Float32Array | undefined;
1763
+ let y: Float32Array | undefined;
1764
+ let current: Int32Array | undefined;
1765
+ let max: Int32Array | undefined;
1766
+ world.addSystem(Update, {
1767
+ name: 'mixedReader',
1768
+ queries: [{ read: [Position, Health] }],
1769
+ fn: (_world, queryResults) => {
1770
+ for (const query of queryResults) {
1771
+ for (const span of query.spans().unwrap()) {
1772
+ const position = span.get(Position);
1773
+ const health = span.get(Health);
1774
+ x = position.x;
1775
+ y = position.y;
1776
+ current = health.current;
1777
+ max = health.max;
1778
+ }
1779
+ }
1780
+ },
1781
+ });
1782
+
1783
+ world.update();
1784
+
1785
+ expect(x).toBeInstanceOf(Float32Array);
1786
+ expect(y).toBeInstanceOf(Float32Array);
1787
+ // biome-ignore lint/style/noNonNullAssertion: controlled test context
1788
+ expect(x![0]).toBeCloseTo(1.5);
1789
+ // biome-ignore lint/style/noNonNullAssertion: controlled test context
1790
+ expect(y![0]).toBeCloseTo(2.5);
1791
+
1792
+ expect(current).toBeInstanceOf(Int32Array);
1793
+ expect(max).toBeInstanceOf(Int32Array);
1794
+ // biome-ignore lint/style/noNonNullAssertion: controlled test context
1795
+ expect(current![0]).toBe(100);
1796
+ // biome-ignore lint/style/noNonNullAssertion: controlled test context
1797
+ expect(max![0]).toBe(200);
1798
+
1799
+ // Verify they are backed by different buffers (per-field independent buffer, D-01)
1800
+ // biome-ignore lint/style/noNonNullAssertion: controlled test context
1801
+ expect(x!.buffer).not.toBe(y!.buffer);
1802
+ // biome-ignore lint/style/noNonNullAssertion: controlled test context
1803
+ expect(x!.buffer).not.toBe(current!.buffer);
1804
+ });
1805
+
1806
+ it('tag component in archetype: query matches but no data columns', () => {
1807
+ const world = new World();
1808
+
1809
+ const Position = defineComponent('TagMixPos', { x: { type: 'f32' } });
1810
+ const Player = defineComponent('TagMixPlayer', {}); // tag component
1811
+
1812
+ world.spawn({ component: Position, data: { x: 5 } }, { component: Player, data: {} });
1813
+
1814
+ // Query With both: should match
1815
+ let matchCount = 0;
1816
+ world.addSystem(Update, {
1817
+ name: 'tagQuery',
1818
+ queries: [{ read: [Position], with: [Player] }],
1819
+ fn: (_world, queryResults) => {
1820
+ matchCount = 0;
1821
+ for (const query of queryResults) {
1822
+ for (const row of query) {
1823
+ matchCount += 1;
1824
+ expect(row.get(Position).x).toBeDefined();
1825
+ }
1826
+ }
1827
+ },
1828
+ });
1829
+
1830
+ world.update();
1831
+ expect(matchCount).toBe(1);
1832
+
1833
+ // Query Without Player: should NOT match
1834
+ let excludedCount = 0;
1835
+ world.addSystem(Update, {
1836
+ name: 'excludeTag',
1837
+ queries: [{ read: [Position], without: [Player] }],
1838
+ fn: (_world, queryResults) => {
1839
+ excludedCount = 0;
1840
+ for (const query of queryResults) excludedCount += [...query].length;
1841
+ },
1842
+ });
1843
+
1844
+ world.update();
1845
+ expect(excludedCount).toBe(0);
1846
+ });
1847
+ });
1848
+
1849
+ describe('Scenario 3: multi-component spawn + addComponent migration + query cache update', () => {
1850
+ it('addComponent changes archetype and query reflects the change', () => {
1851
+ const world = new World();
1852
+
1853
+ const Position = defineComponent('MigPos', { x: { type: 'f32' }, y: { type: 'f32' } });
1854
+ const Velocity = defineComponent('MigVel', { vx: { type: 'f32' }, vy: { type: 'f32' } });
1855
+ const Gravity = defineComponent('MigGrav', { g: { type: 'f32' } });
1856
+
1857
+ // Spawn with Position + Velocity
1858
+ const e = world
1859
+ .spawn(
1860
+ { component: Position, data: { x: 0, y: 0 } },
1861
+ { component: Velocity, data: { vx: 1, vy: 0 } },
1862
+ )
1863
+ .unwrap();
1864
+
1865
+ // Query for Position+Velocity: should match 1
1866
+ let pvCount = 0;
1867
+ world.addSystem(Update, {
1868
+ name: 'pvCounter',
1869
+ queries: [{ read: [Position, Velocity] }],
1870
+ fn: (_world, queryResults) => {
1871
+ pvCount = 0;
1872
+ for (const query of queryResults) pvCount += [...query].length;
1873
+ },
1874
+ });
1875
+
1876
+ // Query for Position+Velocity+Gravity: should match 0 initially
1877
+ let pvgCount = 0;
1878
+ world.addSystem(Update, {
1879
+ name: 'pvgCounter',
1880
+ queries: [{ read: [Position, Velocity, Gravity] }],
1881
+ fn: (_world, queryResults) => {
1882
+ pvgCount = 0;
1883
+ for (const query of queryResults) pvgCount += [...query].length;
1884
+ },
1885
+ });
1886
+
1887
+ world.update();
1888
+ expect(pvCount).toBe(1);
1889
+ expect(pvgCount).toBe(0);
1890
+
1891
+ // Add Gravity component — triggers archetype migration
1892
+ world.addComponent(e, { component: Gravity, data: { g: 9.8 } }).unwrap();
1893
+
1894
+ // After migration: PV+G query should now see entity
1895
+ world.update();
1896
+ expect(pvgCount).toBe(1);
1897
+ // Original PV-only archetype has 0 entities now, but PV+G archetype has PV too
1898
+ expect(pvCount).toBe(1); // PV+G archetype still matches PV query
1899
+
1900
+ // Verify data integrity after migration
1901
+ const pos = world.get(e, Position).unwrap();
1902
+ const vel = world.get(e, Velocity).unwrap();
1903
+ const grav = world.get(e, Gravity).unwrap();
1904
+ expect(pos).toEqual({ x: 0, y: 0 });
1905
+ expect(vel).toEqual({ vx: 1, vy: 0 });
1906
+ expect(grav.g).toBeCloseTo(9.8);
1907
+ });
1908
+
1909
+ it('removeComponent migration + query exclusion', () => {
1910
+ const world = new World();
1911
+
1912
+ const A = defineComponent('MigA', { a: { type: 'f32' } });
1913
+ const B = defineComponent('MigB', { b: { type: 'f32' } });
1914
+
1915
+ const e = world
1916
+ .spawn({ component: A, data: { a: 1 } }, { component: B, data: { b: 2 } })
1917
+ .unwrap();
1918
+
1919
+ // Query With A, Without B: should initially have 0
1920
+ let aNotBCount = 0;
1921
+ world.addSystem(Update, {
1922
+ name: 'aNotB',
1923
+ queries: [{ read: [A], without: [B] }],
1924
+ fn: (_world, queryResults) => {
1925
+ aNotBCount = 0;
1926
+ for (const query of queryResults) aNotBCount += [...query].length;
1927
+ },
1928
+ });
1929
+
1930
+ world.update();
1931
+ expect(aNotBCount).toBe(0); // entity has both A and B
1932
+
1933
+ // Remove B: entity migrates to archetype with only A
1934
+ world.removeComponent(e, B).unwrap();
1935
+
1936
+ world.update();
1937
+ expect(aNotBCount).toBe(1); // now matches With(A) Without(B)
1938
+
1939
+ // Verify data preserved
1940
+ expect(world.get(e, A).unwrap()).toEqual({ a: 1 });
1941
+ });
1942
+
1943
+ it('multiple entities spanning multiple archetypes', () => {
1944
+ const world = new World();
1945
+
1946
+ const Pos = defineComponent('MultiPos', { x: { type: 'f32' } });
1947
+ const Vel = defineComponent('MultiVel', { v: { type: 'f32' } });
1948
+ const Acc = defineComponent('MultiAcc', { a: { type: 'f32' } });
1949
+
1950
+ // 3 entities in different archetype configurations
1951
+ const e1 = world.spawn({ component: Pos, data: { x: 1 } }).unwrap();
1952
+ const e2 = world
1953
+ .spawn({ component: Pos, data: { x: 2 } }, { component: Vel, data: { v: 20 } })
1954
+ .unwrap();
1955
+ const e3 = world
1956
+ .spawn(
1957
+ { component: Pos, data: { x: 3 } },
1958
+ { component: Vel, data: { v: 30 } },
1959
+ { component: Acc, data: { a: 300 } },
1960
+ )
1961
+ .unwrap();
1962
+
1963
+ // Query for all entities with Pos: should find 3 across different archetypes
1964
+ let totalPosEntities = 0;
1965
+ let posValues: number[] = [];
1966
+
1967
+ world.addSystem(Update, {
1968
+ name: 'posReader',
1969
+ queries: [{ read: [Pos] }],
1970
+ fn: (_world, queryResults) => {
1971
+ totalPosEntities = 0;
1972
+ posValues = [];
1973
+ for (const query of queryResults) {
1974
+ for (const row of query) {
1975
+ totalPosEntities += 1;
1976
+ posValues.push(row.get(Pos).x);
1977
+ }
1978
+ }
1979
+ },
1980
+ });
1981
+
1982
+ world.update();
1983
+ expect(totalPosEntities).toBe(3);
1984
+ expect(posValues.sort()).toEqual([1, 2, 3]);
1985
+
1986
+ // Deferred despawn e2, add Acc to e1
1987
+ world.addSystem(Update, {
1988
+ name: 'modifier',
1989
+ queries: [],
1990
+ fn: (_world, _q, commands) => {
1991
+ commands.despawn(e2);
1992
+ commands.addComponent(e1, { component: Acc, data: { a: 100 } });
1993
+ },
1994
+ });
1995
+
1996
+ world.update();
1997
+
1998
+ // Now: e1 has Pos+Acc, e3 has Pos+Vel+Acc, e2 is dead
1999
+ expect(world.get(e2, Pos).ok).toBe(false);
2000
+ expect(world.get(e1, Pos).unwrap()).toEqual({ x: 1 });
2001
+ expect(world.get(e1, Acc).unwrap()).toEqual({ a: 100 });
2002
+ expect(world.get(e3, Pos).unwrap()).toEqual({ x: 3 });
2003
+ expect(world.get(e3, Acc).unwrap()).toEqual({ a: 300 });
2004
+ });
2005
+ });
2006
+ }
2007
+ {
2008
+ // --- from world-managed-refs-non-null.test.ts ---
2009
+ interface WorldInternals {
2010
+ uniqueRefs: UniqueRefStore | null;
2011
+ }
2012
+
2013
+ describe('w1 - World.uniqueRefs non-null after construction (AC-08)', () => {
2014
+ it('new World() owns a non-null UniqueRefStore without external store wiring', () => {
2015
+ const w = new World();
2016
+ const refs = (w as unknown as WorldInternals).uniqueRefs;
2017
+ expect(refs).not.toBeNull();
2018
+ expect(refs).toBeInstanceOf(UniqueRefStore);
2019
+ });
2020
+
2021
+ it('spawn + despawn on a ref<T> field dispatches without external store wiring', () => {
2022
+ // Behavioural counterpart: the despawn release loop must traverse the
2023
+ // internal uniqueRefs without throwing. Before w2 the uniqueRefs slot
2024
+ // is null and `releaseManagedRefHandle` short-circuits silently, so this
2025
+ // case alone does not red-gate the contract; the structural assertion
2026
+ // above is the load-bearing gate. Kept here so the file documents the
2027
+ // post-w2 dispatch path AI users actually exercise.
2028
+ const Mat = defineComponent('Mat', { handle: { type: 'unique<MaterialAsset>' } });
2029
+ const w = new World();
2030
+ const refs = (w as unknown as WorldInternals).uniqueRefs;
2031
+ if (refs === null) throw new Error('AC-08 violated: uniqueRefs is null after new World()');
2032
+ const h = refs.alloc('MaterialAsset', { id: 1 });
2033
+ const spawnR = w.spawn({ component: Mat, data: { handle: h } });
2034
+ if (!spawnR.ok) throw new Error('expected spawn ok');
2035
+ const despawnR = w.despawn(spawnR.value);
2036
+ expect(despawnR.ok).toBe(true);
2037
+ const resolveR = refs.resolve(h);
2038
+ expect(resolveR.ok).toBe(false);
2039
+ if (resolveR.ok) throw new Error('expected stale');
2040
+ expect(resolveR.error.code).toBe('unique-ref-stale');
2041
+ });
2042
+ });
2043
+ }
2044
+ {
2045
+ // --- from world-spawn-array-fallback.test.ts ---
2046
+ }
2047
+ {
2048
+ // --- from world-spawn-defaults.test.ts ---
2049
+ // ────────────────────────────────────────────────────────────────────────
2050
+ // t8 — addComponent / spawn symmetry (research §F4 + §RD-4)
2051
+ // ────────────────────────────────────────────────────────────────────────
2052
+
2053
+ describe('w-spawn-fallback t8 — addComponent symmetry with spawn (AC-04 mirror)', () => {
2054
+ const Probe = defineComponent('Probe', {
2055
+ posX: { type: 'f32' },
2056
+ posY: { type: 'f32' },
2057
+ posZ: { type: 'f32' },
2058
+ });
2059
+
2060
+ // Empty marker so addComponent has an existing entity to attach to.
2061
+ const Marker = defineComponent('Marker', {
2062
+ seq: { type: 'u32' },
2063
+ });
2064
+
2065
+ it('addComponent partial data goes through the same helper as spawn', () => {
2066
+ const wSpawn = new World();
2067
+ const eSpawn = wSpawn.spawn({ component: Probe, data: { posX: 1 } }).unwrap();
2068
+ const spawnRow = wSpawn.get(eSpawn, Probe).unwrap();
2069
+
2070
+ const wAdd = new World();
2071
+ const eAdd = wAdd.spawn({ component: Marker, data: { seq: 0 } }).unwrap();
2072
+ wAdd.addComponent(eAdd, { component: Probe, data: { posX: 1 } }).unwrap();
2073
+ const addRow = wAdd.get(eAdd, Probe).unwrap();
2074
+
2075
+ expect(spawnRow.posX).toBe(1);
2076
+ expect(spawnRow.posY).toBe(0); // layer-3
2077
+ expect(spawnRow.posZ).toBe(0); // layer-3
2078
+
2079
+ expect(addRow.posX).toBe(spawnRow.posX);
2080
+ expect(addRow.posY).toBe(spawnRow.posY);
2081
+ expect(addRow.posZ).toBe(spawnRow.posZ);
2082
+ });
2083
+
2084
+ it('addComponent with empty data: {} matches spawn with empty data: {}', () => {
2085
+ const wSpawn = new World();
2086
+ const eSpawn = wSpawn.spawn({ component: Probe, data: {} }).unwrap();
2087
+ const spawnRow = wSpawn.get(eSpawn, Probe).unwrap();
2088
+
2089
+ const wAdd = new World();
2090
+ const eAdd = wAdd.spawn({ component: Marker, data: {} }).unwrap();
2091
+ wAdd.addComponent(eAdd, { component: Probe, data: {} }).unwrap();
2092
+ const addRow = wAdd.get(eAdd, Probe).unwrap();
2093
+
2094
+ expect(spawnRow.posX).toBe(0);
2095
+ expect(spawnRow.posY).toBe(0);
2096
+ expect(spawnRow.posZ).toBe(0);
2097
+ expect(addRow.posX).toBe(spawnRow.posX);
2098
+ expect(addRow.posY).toBe(spawnRow.posY);
2099
+ expect(addRow.posZ).toBe(spawnRow.posZ);
2100
+ });
2101
+ });
2102
+
2103
+ // ────────────────────────────────────────────────────────────────────────
2104
+ // t10 — brand-class NULL sentinel cross-test (handle<T> / entity / ref<T>)
2105
+ // ────────────────────────────────────────────────────────────────────────
2106
+ }
2107
+ {
2108
+ // --- from world-spawn-direct.test.ts ---
2109
+ describe('AC-04 — spawn three-path passthrough (no register)', () => {
2110
+ it('spawn: a freshly defined component is usable without registering it', () => {
2111
+ const world = new World();
2112
+ const Pos = defineComponent('DirectSpawnPos', { x: 'f32', y: 'f32' });
2113
+ // No per-World register step — defineComponent is the only step.
2114
+ const e = world.spawn({ component: Pos, data: { x: 3, y: 4 } }).unwrap();
2115
+ expect(world.get(e, Pos).unwrap()).toEqual({ x: 3, y: 4 });
2116
+ });
2117
+
2118
+ it('addComponent: adding a never-registered component to a live entity works', () => {
2119
+ const world = new World();
2120
+ const Tag = defineComponent('DirectAddTag', {});
2121
+ const Vel = defineComponent('DirectAddVel', { vx: 'f32', vy: 'f32' });
2122
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
2123
+ // Vel was only defined, never registered.
2124
+ world.addComponent(e, { component: Vel, data: { vx: 5, vy: 6 } }).unwrap();
2125
+ expect(world.get(e, Vel).unwrap()).toEqual({ vx: 5, vy: 6 });
2126
+ });
2127
+
2128
+ it('deferred spawn (materialize path): commands.spawn + update materializes without register', () => {
2129
+ const world = new World();
2130
+ const Hp = defineComponent('DirectMatHp', { current: 'i32', max: 'i32' });
2131
+ let pending: EntityHandle | undefined;
2132
+ world.addSystem(Update, {
2133
+ name: 'deferred-spawner',
2134
+ queries: [],
2135
+ fn: (_world, _q, commands) => {
2136
+ // Deferred spawn returns a pending Entity; materialized on flush.
2137
+ pending = commands.spawn({ component: Hp, data: { current: 7, max: 9 } });
2138
+ },
2139
+ });
2140
+ // update() runs the system then flushes commands -> _materializePendingEntity.
2141
+ world.update();
2142
+ expect(pending).toBeDefined();
2143
+ const captured = world.get(pending as EntityHandle, Hp);
2144
+ expect(captured.ok).toBe(true);
2145
+ expect(captured.unwrap()).toEqual({ current: 7, max: 9 });
2146
+ });
2147
+ });
2148
+
2149
+ describe('AC-07 — get fail-fast degradation (returns err, never throws)', () => {
2150
+ it('get on a never-present (but defined) component returns err(component-not-present)', () => {
2151
+ const world = new World();
2152
+ const Present = defineComponent('GetPresent', { v: 'f32' });
2153
+ const NeverHeld = defineComponent('GetNeverHeld', { w: 'f32' });
2154
+ const e = world.spawn({ component: Present, data: { v: 1 } }).unwrap();
2155
+ // NeverHeld is defined but this entity never carried it.
2156
+ const result = world.get(e, NeverHeld);
2157
+ expect(result.ok).toBe(false);
2158
+ expect(!result.ok && result.error).toBeInstanceOf(ComponentNotPresentError);
2159
+ expect(!result.ok && result.error.code).toBe('component-not-present');
2160
+ });
2161
+
2162
+ it('get does not throw for a never-present component (degradation is a Result, not an exception)', () => {
2163
+ const world = new World();
2164
+ const Present = defineComponent('NoThrowPresent', { v: 'f32' });
2165
+ const NeverHeld = defineComponent('NoThrowNeverHeld', { w: 'f32' });
2166
+ const e = world.spawn({ component: Present, data: { v: 1 } }).unwrap();
2167
+ expect(() => world.get(e, NeverHeld)).not.toThrow();
2168
+ });
2169
+ });
2170
+
2171
+ describe('AC-05 — EcsErrorCode remains a closed source-owned union', () => {
2172
+ it('keeps the mutation epoch infrastructure failure out of the core union', () => {
2173
+ // @ts-expect-error mutation epoch exhaustion is an infrastructure throw, not a World Result code.
2174
+ const code: EcsErrorCode = 'change-epoch-exhausted';
2175
+ expect(code).toBe('change-epoch-exhausted');
2176
+ });
2177
+
2178
+ it('the dropped register codes are not assignable to EcsErrorCode', () => {
2179
+ // @ts-expect-error — COMPONENT_ALREADY_REGISTERED was removed from the union.
2180
+ const dropped1: EcsErrorCode = 'COMPONENT_ALREADY_REGISTERED';
2181
+ // @ts-expect-error — COMPONENT_NOT_REGISTERED was removed from the union.
2182
+ const dropped2: EcsErrorCode = 'COMPONENT_NOT_REGISTERED';
2183
+ expect([dropped1, dropped2]).toBeDefined();
2184
+ });
2185
+ });
2186
+ }
2187
+ {
2188
+ // --- from component-default-fallback.test.ts (__tests__/) ---
2189
+ describe('layer-3 typeDefault — scalar arms', () => {
2190
+ it('f32 -> 0', () => {
2191
+ expect(typeDefault('f32')).toBe(0);
2192
+ });
2193
+ it('f64 -> 0', () => {
2194
+ expect(typeDefault('f64')).toBe(0);
2195
+ });
2196
+ it('i32 -> 0', () => {
2197
+ expect(typeDefault('i32')).toBe(0);
2198
+ });
2199
+ it('u32 -> 0', () => {
2200
+ expect(typeDefault('u32')).toBe(0);
2201
+ });
2202
+ it('i16 -> 0', () => {
2203
+ expect(typeDefault('i16')).toBe(0);
2204
+ });
2205
+ it('u16 -> 0', () => {
2206
+ expect(typeDefault('u16')).toBe(0);
2207
+ });
2208
+ it('i8 -> 0', () => {
2209
+ expect(typeDefault('i8')).toBe(0);
2210
+ });
2211
+ it('u8 -> 0', () => {
2212
+ expect(typeDefault('u8')).toBe(0);
2213
+ });
2214
+ it('bool -> false', () => {
2215
+ expect(typeDefault('bool')).toBe(false);
2216
+ });
2217
+ it('enum -> 0', () => {
2218
+ expect(typeDefault('enum')).toBe(0);
2219
+ });
2220
+ it('ref (legacy scalar) -> 0', () => {
2221
+ expect(typeDefault('ref')).toBe(0);
2222
+ });
2223
+ });
2224
+
2225
+ describe('layer-3 typeDefault — vocab arms', () => {
2226
+ it("'string' -> 0 (uniqueRefs handle slot)", () => {
2227
+ expect(typeDefault('string')).toBe(0);
2228
+ });
2229
+ it("'entity' -> ENTITY_NULL_RAW", () => {
2230
+ expect(typeDefault('entity')).toBe(ENTITY_NULL_RAW);
2231
+ });
2232
+ it("'array<entity>' -> [] (the only special-cased array arm)", () => {
2233
+ expect(typeDefault('array<entity>')).toEqual([]);
2234
+ });
2235
+ it("'array<f32>' -> 0 (asymmetric: writeArrayField bottoms to empty slot)", () => {
2236
+ expect(typeDefault('array<f32>')).toBe(0);
2237
+ });
2238
+ it("'array<u32>' -> 0 (asymmetric same as array<f32>)", () => {
2239
+ expect(typeDefault('array<u32>')).toBe(0);
2240
+ });
2241
+ it("'array<f32, 16>' -> 0 (fixed-N capacity)", () => {
2242
+ expect(typeDefault('array<f32, 16>')).toBe(0);
2243
+ });
2244
+ it("'buffer' -> 0 (variable-byte BufferPool slot id)", () => {
2245
+ expect(typeDefault('buffer')).toBe(0);
2246
+ });
2247
+ it("'buffer<64>' -> 0 (fixed-byte inline stride-N column zeroed row)", () => {
2248
+ expect(typeDefault('buffer<64>')).toBe(0);
2249
+ });
2250
+ it("'unique<MaterialAsset>' -> 0 (UniqueRefStore handle slot)", () => {
2251
+ expect(typeDefault('unique<MaterialAsset>')).toBe(0);
2252
+ });
2253
+ it("'shared<MeshAsset>' -> 0 (unmanaged handle phantom u32)", () => {
2254
+ expect(typeDefault('shared<MeshAsset>')).toBe(0);
2255
+ });
2256
+ });
2257
+
2258
+ describe('fillComponentDefaults — public surface', () => {
2259
+ it('preserves explicit raw values (layer-1 entry)', () => {
2260
+ const C = defineComponent('C', { x: { type: 'f32' }, y: { type: 'f32' } });
2261
+ const out = fillComponentDefaults(C, { x: 7 });
2262
+ expect(out.x).toBe(7);
2263
+ // y missing in raw -> layer-3 fallback 0
2264
+ expect(out.y).toBe(0);
2265
+ });
2266
+
2267
+ it('layer-2 token defaults beat layer-3 typeDefault', () => {
2268
+ const C = defineComponent('C', {
2269
+ a: { type: 'f32' },
2270
+ b: { type: 'f32', default: 42 },
2271
+ });
2272
+ const out = fillComponentDefaults(C, {});
2273
+ expect(out.a).toBe(0); // layer-3
2274
+ expect(out.b).toBe(42); // layer-2
2275
+ });
2276
+
2277
+ it('mixed (explicit + token + typeDefault) — three-layer route in one component', () => {
2278
+ const C = defineComponent('M', {
2279
+ e: { type: 'f32' },
2280
+ t: { type: 'f32', default: 17 },
2281
+ f: { type: 'f32' },
2282
+ });
2283
+ const out = fillComponentDefaults(C, { e: 9 });
2284
+ expect(out.e).toBe(9); // layer-1
2285
+ expect(out.t).toBe(17); // layer-2
2286
+ expect(out.f).toBe(0); // layer-3
2287
+ });
2288
+
2289
+ it('brand-class field (handle<T>) — schema-nullable gets layer-3 NULL sentinel 0', () => {
2290
+ const C = defineComponent('M', {
2291
+ mesh: { type: 'shared<MeshAsset>' },
2292
+ mat: { type: 'unique<MaterialAsset>' },
2293
+ });
2294
+ const out = fillComponentDefaults(C, {});
2295
+ expect(out.mesh).toBe(0);
2296
+ expect(out.mat).toBe(0);
2297
+ });
2298
+
2299
+ it("'array<entity>' returns empty array literal (only array arm with [])", () => {
2300
+ const C = defineComponent('M', { children: { type: 'array<entity>' } });
2301
+ const out = fillComponentDefaults(C, {});
2302
+ expect(out.children).toEqual([]);
2303
+ });
2304
+
2305
+ it('array<f32> returns raw 0 (asymmetric — D-2 / OOS-6 letter)', () => {
2306
+ const C = defineComponent('M', { tr: { type: 'array<f32>' } });
2307
+ const out = fillComponentDefaults(C, {});
2308
+ expect(out.tr).toBe(0);
2309
+ });
2310
+ });
2311
+ }
2312
+ {
2313
+ // --- from relationshipSyncDepth-elimination.test.ts (w10) ---
2314
+ describe('feat-20260611 M-6 w12 recordIsLive liveness predicate', () => {
2315
+ const Tag = defineComponent('Tag', {});
2316
+
2317
+ // State 1: live entity — record exists, generation matches, archetypeId !== -1
2318
+ it('returns true for a live entity (gen match + archetypeId >= 0)', () => {
2319
+ const world = new World();
2320
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
2321
+ // get internally uses recordIsLive → ok proves recordIsLive returned true
2322
+ const r = world.get(e, Tag);
2323
+ expect(r.ok).toBe(true);
2324
+ expect(r.unwrap()).toEqual({});
2325
+ });
2326
+
2327
+ // State 2: stale handle (despawned) — record exists but generation bumped
2328
+ it('returns false when generation does not match (despawned entity)', () => {
2329
+ const world = new World();
2330
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
2331
+ world.despawn(e).unwrap();
2332
+ const r = world.get(e, Tag);
2333
+ expect(r.ok).toBe(false);
2334
+ if (!r.ok) {
2335
+ expect(r.error.code).toBe('stale-entity');
2336
+ }
2337
+ });
2338
+
2339
+ // State 3: record undefined (out-of-bounds index, a made-up handle)
2340
+ it('returns false when record is undefined (nonexistent slot)', () => {
2341
+ const world = new World();
2342
+ // encode a valid-looking handle for a slot that has no record
2343
+ const bogus = encodeEntity(9999, 0) as EntityHandle;
2344
+ const r = world.get(bogus, Tag);
2345
+ expect(r.ok).toBe(false);
2346
+ if (!r.ok) {
2347
+ expect(r.error.code).toBe('stale-entity');
2348
+ }
2349
+ });
2350
+
2351
+ // State 4: deferred spawn pending (not yet materialized) —
2352
+ // gen matches but archetypeId === -1
2353
+ it('returns false for a pending deferred-spawn entity (archetypeId === -1)', () => {
2354
+ const world = new World();
2355
+
2356
+ let pendingEntity: EntityHandle | undefined;
2357
+ world.addSystem(Update, {
2358
+ name: 'defer-spawn',
2359
+ queries: [],
2360
+ fn: (_world, _q, commands) => {
2361
+ pendingEntity = commands.spawn({ component: Tag, data: {} });
2362
+ },
2363
+ });
2364
+ world.update();
2365
+ expect(pendingEntity).toBeDefined();
2366
+
2367
+ // After flush, the entity is live — get must succeed
2368
+ // biome-ignore lint/style/noNonNullAssertion: confirmed defined by expect above
2369
+ const r = world.get(pendingEntity!, Tag);
2370
+ expect(r.ok).toBe(true);
2371
+
2372
+ // Before flush the entity is not live (pending).
2373
+ // We verify this indirectly: during system execution, queries
2374
+ // don't see the entity. After flush, get succeeds.
2375
+ // The direct pending test uses the deferred-path inside a system:
2376
+ let seenBeforeFlush = false;
2377
+ world.addSystem(Update, {
2378
+ name: 'check-pending',
2379
+ queries: [{ with: [Tag] }],
2380
+ fn: (_world, queries, commands) => {
2381
+ const spawned = commands.spawn({ component: Tag, data: {} });
2382
+ // Inside the system, before flush, the pending entity won't match a query
2383
+ for (const _row of queries[0]) {
2384
+ void _row;
2385
+ }
2386
+ // But commands.isDeferred reports it
2387
+ expect(commands.isDeferred(spawned)).toBe(true);
2388
+ seenBeforeFlush = true;
2389
+ },
2390
+ });
2391
+ world.update();
2392
+ expect(seenBeforeFlush).toBe(true);
2393
+ });
2394
+
2395
+ // State 5: despawn-then-recycle — slot recycled with fresh gen,
2396
+ // old handle's gen does not match new record's gen
2397
+ it('returns false for a handle whose slot was recycled (gen mismatch on recycled slot)', () => {
2398
+ const world = new World();
2399
+ const e1 = world.spawn({ component: Tag, data: {} }).unwrap();
2400
+ // Despawn → slot freed and gen bumped.
2401
+ world.despawn(e1).unwrap();
2402
+
2403
+ // Next spawn may reuse the freed slot with a bumped gen.
2404
+ // The old handle (e1) with pre-bump gen should be stale.
2405
+ const e2 = world.spawn({ component: Tag, data: {} }).unwrap();
2406
+ const r = world.get(e1, Tag);
2407
+ expect(r.ok).toBe(false);
2408
+ if (!r.ok) {
2409
+ expect(r.error.code).toBe('stale-entity');
2410
+ }
2411
+ // The new entity is live at the same or different slot
2412
+ expect(world.get(e2, Tag).ok).toBe(true);
2413
+ });
2414
+
2415
+ // State 6: generation retired (gen > 255) — the slot is never recycled,
2416
+ // the record still exists but any handle for it is stale
2417
+ it('returns false for a handle whose slot generation was retired (> 255)', () => {
2418
+ // This test exhausts the generation counter on one slot.
2419
+ // We can't practically despawn 255 times in a unit test,
2420
+ // so we verify the code path via direct record manipulation.
2421
+ // Instead, we trust the generation-encoding math: encodeEntity forces
2422
+ // gen into 8 bits. A gen > 255 cannot be encoded in a handle,
2423
+ // so any handle for a retired slot always fails gen match.
2424
+ // We test the boundary: gen=255 despawn bumps to 256 → retired.
2425
+ const world = new World();
2426
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
2427
+ const slot = entityIndex(e);
2428
+
2429
+ // Despawn once — gen goes 0→1, slot recycled.
2430
+ world.despawn(e).unwrap();
2431
+
2432
+ // The slot is back on freeIndices. Spawn reuses it with gen=1.
2433
+ const fresh = world.spawn({ component: Tag, data: {} }).unwrap();
2434
+ expect(entityIndex(fresh)).toBe(slot);
2435
+ expect(entityGeneration(fresh)).toBe(1);
2436
+
2437
+ // The retired case: a handle with gen=0 for this slot must be stale.
2438
+ const oldHandle = encodeEntity(slot, 0) as EntityHandle;
2439
+ const r = world.get(oldHandle, Tag);
2440
+ expect(r.ok).toBe(false);
2441
+ if (!r.ok) {
2442
+ expect(r.error.code).toBe('stale-entity');
2443
+ }
2444
+ });
2445
+
2446
+ // State 7: archetypeId === -1 with gen match (post-despawn before recycle)
2447
+ // After despawn, record.archetypeId is set to -1, then gen is bumped.
2448
+ // A handle matching the pre-bump gen has gen mismatch (State 2).
2449
+ // A handle matching the post-bump gen: the slot is on freeIndices
2450
+ // but not yet re-allocated → gen matches, archetypeId === -1.
2451
+ it('returns false when archetypeId === -1 (despawned, pre-recycle)', () => {
2452
+ const world = new World();
2453
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
2454
+ const slot = entityIndex(e);
2455
+
2456
+ world.despawn(e).unwrap();
2457
+
2458
+ // After despawn: archetypeId === -1, gen bumped by 1.
2459
+ // The post-bump gen is what the slot carries now.
2460
+ // But there is no public handle with the post-bump gen — the old
2461
+ // handle has the pre-bump gen, and no new entity has been spawned.
2462
+ // We construct the post-bump-gen handle to test the archetypeId === -1 path.
2463
+ const postBumpGen = entityGeneration(e) + 1;
2464
+ const postBumpHandle = encodeEntity(slot, postBumpGen) as EntityHandle;
2465
+ const r = world.get(postBumpHandle, Tag);
2466
+ expect(r.ok).toBe(false);
2467
+ if (!r.ok) {
2468
+ // gen matches but archetypeId === -1 → stale
2469
+ expect(r.error.code).toBe('stale-entity');
2470
+ }
2471
+ });
2472
+
2473
+ // Deferred flush full lifecycle: E-06 path
2474
+ it('deferred entity: not live before flush, live after flush, stale after despawn', () => {
2475
+ const world = new World();
2476
+
2477
+ let spawnedHandle: EntityHandle | undefined;
2478
+ world.addSystem(Update, {
2479
+ name: 'lifecycle-spawn',
2480
+ queries: [],
2481
+ fn: (_world, _q, commands) => {
2482
+ spawnedHandle = commands.spawn({ component: Tag, data: {} });
2483
+ // Pending: not yet live
2484
+ expect(commands.isDeferred(spawnedHandle)).toBe(true);
2485
+ // get on pending returns stale-entity
2486
+ const beforeFlush = world.get(spawnedHandle, Tag);
2487
+ expect(beforeFlush.ok).toBe(false);
2488
+ if (!beforeFlush.ok) {
2489
+ expect(beforeFlush.error.code).toBe('stale-entity');
2490
+ }
2491
+ },
2492
+ });
2493
+ world.update();
2494
+
2495
+ expect(spawnedHandle).toBeDefined();
2496
+ // biome-ignore lint/style/noNonNullAssertion: confirmed defined by expect above
2497
+ const e = spawnedHandle!;
2498
+
2499
+ // After flush: live
2500
+ const afterFlush = world.get(e, Tag);
2501
+ expect(afterFlush.ok).toBe(true);
2502
+ expect(afterFlush.unwrap()).toEqual({});
2503
+
2504
+ // Despawn: stale again
2505
+ world.despawn(e).unwrap();
2506
+ const afterDespawn = world.get(e, Tag);
2507
+ expect(afterDespawn.ok).toBe(false);
2508
+ if (!afterDespawn.ok) {
2509
+ expect(afterDespawn.error.code).toBe('stale-entity');
2510
+ }
2511
+ });
2512
+
2513
+ // Derived expression correctness: archetypeId === -1 gates liveness,
2514
+ // not a separate pending flag.
2515
+ it('derived expression: archetypeId !== -1 is the sole structural live gate', () => {
2516
+ const world = new World();
2517
+
2518
+ // Synchronous spawn sets archetypeId immediately
2519
+ const e = world.spawn({ component: Tag, data: {} }).unwrap();
2520
+ const slot = entityIndex(e);
2521
+ const gen = entityGeneration(e);
2522
+
2523
+ // Access internal records to verify the predicate
2524
+ // biome-ignore lint/suspicious/noExplicitAny: access private members for white-box liveness test
2525
+ const records = (world as any).records;
2526
+ const record = records[slot];
2527
+ expect(record).toBeDefined();
2528
+ expect(record.generation).toBe(gen);
2529
+ expect(record.archetypeId).toBeGreaterThanOrEqual(0);
2530
+ // pending field must not exist (w13 will remove it; w12 is red phase)
2531
+ expect('pending' in record).toBe(false);
2532
+ });
2533
+ });
2534
+ }