@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,78 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { componentId, defineComponent } from '../component';
3
+ import { Entity } from '../entity';
4
+ import type { EntityHandle } from '../entity-handle';
5
+ import { appendArchetypeRow, growArchetype, removeArchetypeRow } from '../storage/archetype';
6
+ import { createArchetypeGraph, getOrCreateArchetype } from '../storage/archetype-graph';
7
+ import { appendTableRow, growTable, removeTableRow } from '../storage/table';
8
+ import { World } from '../world';
9
+
10
+ describe('Table and Archetype ownership', () => {
11
+ it('stores component columns only on Table and logical membership only on Archetype', () => {
12
+ const Position = defineComponent('TableOwnerPosition', { x: 'f32' });
13
+ const graph = createArchetypeGraph();
14
+ const archetype = getOrCreateArchetype(graph, [componentId(Position)], [Position]);
15
+ const table = graph.tables[archetype.tableId];
16
+ if (table === undefined) throw new Error('table missing');
17
+
18
+ expect(table.storage.has(componentId(Entity))).toBe(true);
19
+ expect(table.storage.has(componentId(Position))).toBe(true);
20
+ expect(archetype.rows).toBeInstanceOf(Uint32Array);
21
+ expect('storage' in archetype).toBe(false);
22
+ });
23
+
24
+ it('grows logical and physical capacities independently', () => {
25
+ const Position = defineComponent('IndependentCapacityPosition', { x: 'f32' });
26
+ const graph = createArchetypeGraph();
27
+ const archetype = getOrCreateArchetype(graph, [componentId(Position)], [Position]);
28
+ const table = graph.tables[archetype.tableId];
29
+ if (table === undefined) throw new Error('table missing');
30
+ const archetypeCapacity = archetype.capacity;
31
+ const tableCapacity = table.capacity;
32
+
33
+ growArchetype(archetype, archetypeCapacity * 2);
34
+ expect(archetype.capacity).toBe(archetypeCapacity * 2);
35
+ expect(table.capacity).toBe(tableCapacity);
36
+
37
+ growTable(table, tableCapacity * 2);
38
+ expect(table.capacity).toBe(tableCapacity * 2);
39
+ expect(archetype.capacity).toBe(archetypeCapacity * 2);
40
+ });
41
+
42
+ it('keeps archetypeRow to tableRow explicit across independent swap-pop', () => {
43
+ const Position = defineComponent('SwapMappingPosition', { x: 'f32' });
44
+ const graph = createArchetypeGraph();
45
+ const archetype = getOrCreateArchetype(graph, [componentId(Position)], [Position]);
46
+ const table = graph.tables[archetype.tableId];
47
+ if (table === undefined) throw new Error('table missing');
48
+ const first = 0x01000001 as EntityHandle;
49
+ const second = 0x02000002 as EntityHandle;
50
+ const firstTableRow = appendTableRow(table, first);
51
+ const secondTableRow = appendTableRow(table, second);
52
+ appendArchetypeRow(archetype, firstTableRow);
53
+ appendArchetypeRow(archetype, secondTableRow);
54
+
55
+ expect(removeArchetypeRow(archetype, 0)).toEqual({ movedTableRow: 1, newRow: 0 });
56
+ expect(archetype.rows[0]).toBe(1);
57
+ expect(removeTableRow(table, 0)).toEqual({ movedEntity: second, newRow: 0 });
58
+ expect(table.storage.get(componentId(Entity))?.fields.get('self')?.view[0]).toBe(second);
59
+ });
60
+
61
+ it('world migration and despawn maintain both mapping layers', () => {
62
+ const Position = defineComponent('WorldMappingPosition', { x: 'f32' });
63
+ const Selected = defineComponent('WorldMappingSelected', {});
64
+ const world = new World();
65
+ const first = world.spawn({ component: Position, data: { x: 1 } }).unwrap();
66
+ const second = world.spawn({ component: Position, data: { x: 2 } }).unwrap();
67
+
68
+ world.addComponent(first, { component: Selected, data: {} }).unwrap();
69
+ expect(world.get(first, Position).unwrap().x).toBe(1);
70
+ expect(world.get(second, Position).unwrap().x).toBe(2);
71
+
72
+ world.despawn(first).unwrap();
73
+ expect(world.get(second, Entity).unwrap().self).toBe(second);
74
+ expect([...world.query({ read: [Position] }).unwrap()].map((row) => row.entity)).toEqual([
75
+ second,
76
+ ]);
77
+ });
78
+ });
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { FixedUpdate, Update } from '../schedule-token';
3
+ import { FixedTime, Time } from '../time';
4
+ import { World } from '../world';
5
+
6
+ describe('World time resources', () => {
7
+ it('rejects negative, NaN, and infinite deltas without mutating time', () => {
8
+ const world = new World();
9
+ const before = world.getResource(Time);
10
+
11
+ for (const delta of [-1, Number.NaN, Number.POSITIVE_INFINITY]) {
12
+ const result = world.update(delta);
13
+ expect(result.ok).toBe(false);
14
+ if (!result.ok) {
15
+ expect(result.error.code).toBe('time-delta-invalid');
16
+ expect(result.error.expected).toBe('a finite delta greater than or equal to 0');
17
+ expect(result.error.hint).toContain('world.update');
18
+ }
19
+ }
20
+
21
+ expect(world.getResource(Time)).toEqual(before);
22
+ });
23
+
24
+ it('treats an omitted delta as zero while still running Update', () => {
25
+ const world = new World();
26
+ let updates = 0;
27
+ world.addSystem(Update, { name: 'update', queries: [], fn: () => updates++ });
28
+
29
+ expect(world.update().ok).toBe(true);
30
+ expect(world.getResource(Time)).toMatchObject({ delta: 0, elapsed: 0 });
31
+ expect(world.getResource(FixedTime).tick).toBe(0);
32
+ expect(updates).toBe(1);
33
+ });
34
+
35
+ it('advances Time by an explicit positive delta', () => {
36
+ const world = new World();
37
+
38
+ expect(world.update(0.025).ok).toBe(true);
39
+ expect(world.getResource(Time)).toMatchObject({ delta: 0.025, elapsed: 0.025 });
40
+ });
41
+
42
+ it('rejects an incoherent policy before writing protected resources', () => {
43
+ const world = new World({
44
+ time: { fixedDeltaSeconds: 1 / 60, maxStepsPerUpdate: 4, maxDeltaSeconds: 0.05 },
45
+ });
46
+ const beforeTime = world.getResource(Time);
47
+ const beforeFixed = world.getResource(FixedTime);
48
+
49
+ const result = world.update(0.01);
50
+
51
+ expect(result.ok).toBe(false);
52
+ if (!result.ok) {
53
+ expect(result.error.code).toBe('time-config-invalid');
54
+ expect(result.error.expected).toContain('maxDeltaSeconds >=');
55
+ expect(result.error.hint).toContain('maxDeltaSeconds');
56
+ }
57
+ expect(world.getResource(Time)).toEqual(beforeTime);
58
+ expect(world.getResource(FixedTime)).toEqual(beforeFixed);
59
+ });
60
+
61
+ it('uses the clamped Time delta as the fixed accumulator input', () => {
62
+ const world = new World();
63
+ let fixedRuns = 0;
64
+ world.addSystem(Update, { name: 'observe-time', queries: [], fn: () => {} });
65
+ world.addSystem(FixedUpdate, { name: 'fixed', queries: [], fn: () => fixedRuns++ });
66
+
67
+ expect(world.update(5).ok).toBe(true);
68
+ const time = world.getResource(Time);
69
+ const fixed = world.getResource(FixedTime);
70
+ expect(time.delta).toBe(0.1);
71
+ expect(fixed).toMatchObject({
72
+ delta: 1 / 60,
73
+ maxStepsPerUpdate: 4,
74
+ droppedUpdates: 1,
75
+ });
76
+ expect(fixedRuns).toBe(4);
77
+ expect(fixed.droppedSeconds).toBeCloseTo(1 / 60, 12);
78
+ });
79
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expectTypeOf, it } from 'vitest';
2
+ import { defineComponent } from '../component';
3
+ import type { Query, QueryDescriptor, QueryRow, QuerySpan } from '../query/query';
4
+ import { World } from '../world';
5
+
6
+ const Position = defineComponent('QueryTypePosition', { x: 'f32', y: 'f32' });
7
+ const Velocity = defineComponent('QueryTypeVelocity', { x: 'f32' });
8
+
9
+ describe('Query public type inference', () => {
10
+ it('infers row read, write, and optional roles', () => {
11
+ const world = new World();
12
+ const query = world.query({ read: [Velocity], write: [Position] }).unwrap();
13
+ expectTypeOf(query).toMatchTypeOf<
14
+ Query<readonly [typeof Velocity], readonly [typeof Position], readonly []>
15
+ >();
16
+ });
17
+
18
+ it('projects scalar row and column shapes', () => {
19
+ const row = null as unknown as QueryRow<
20
+ readonly [typeof Velocity],
21
+ readonly [typeof Position],
22
+ readonly []
23
+ >;
24
+ const span = null as unknown as QuerySpan<
25
+ readonly [typeof Velocity],
26
+ readonly [typeof Position]
27
+ >;
28
+ expectTypeOf(row.get(Velocity).x).toBeNumber();
29
+ expectTypeOf(row.mut(Position).x).toBeNumber();
30
+ expectTypeOf(span.get(Velocity).x).toEqualTypeOf<Float32Array>();
31
+ expectTypeOf(span.mut(Position).x).toEqualTypeOf<Float32Array>();
32
+ });
33
+
34
+ it('keeps descriptor access tuples literal', () => {
35
+ const descriptor = { read: [Position], write: [Velocity] } satisfies QueryDescriptor;
36
+ expectTypeOf(descriptor.read).toEqualTypeOf<(typeof Position)[]>();
37
+ });
38
+ });
@@ -0,0 +1,348 @@
1
+ // feat-20260614-ecs-managed-lifecycle-ssot M1 t-w1: store-unit tests for
2
+ // UniqueRefStore.release order + throw-safety + slot reuse.
3
+ //
4
+ // These tests drive UniqueRefStore directly (no World) per research
5
+ // Finding 1.5: the regression repro path is achievable with a spy callback,
6
+ // no Rapier / World fixture required. Verifies AC-01, AC-02, AC-06, AC-07
7
+ // (requirements §3.1, §5).
8
+ //
9
+ // TDD red -> green:
10
+ // - (a) order assertion + (b) throw-safety: red before w2, green after w2.
11
+ // - (c) slot reuse: green after w7 (M3 deletes the gen-255 retirement that
12
+ // would otherwise force nextSlot to grow during 1000 iters on a single
13
+ // slot). Documented inline so reviewer can correlate the carryover.
14
+
15
+ import { handleGeneration, handleSlot, pack, unwrapHandle } from '@forgeax/engine-types';
16
+ import { describe, expect, it, vi } from 'vitest';
17
+ import { UniqueRefStaleError } from '../errors';
18
+ import { UniqueRefStore } from '../unique-ref-store';
19
+
20
+ describe('UniqueRefStore release ordering + throw-safety (feat-20260614 M1)', () => {
21
+ it('AC-01: releaseCallbacks entry is removed BEFORE onRelease fires', () => {
22
+ const store = new UniqueRefStore();
23
+ let observedHasCallback: boolean | null = null;
24
+
25
+ // Spy reads the private releaseCallbacks Map at the moment cb fires.
26
+ // After the M1 fix (w2) the entry is deleted before invocation, so the
27
+ // observation is `false`. Before the fix the entry is still live so
28
+ // observation is `true` -> assertion fails (red).
29
+ const onRelease = vi.fn((_payload: { id: number }) => {
30
+ // biome-ignore lint/suspicious/noExplicitAny: targeted private read for the order assertion
31
+ const internalCallbacks = (store as any).releaseCallbacks as Map<number, unknown>;
32
+ // biome-ignore lint/suspicious/noExplicitAny: handle raw u32 read for the lookup
33
+ const raw = (store as any).payloads as Map<number, unknown>;
34
+ // The handle is a u32 brand. Re-derive raw via the live keys snapshot
35
+ // since the handle isn't directly in scope inside the cb closure.
36
+ void raw;
37
+ // Use the callbacks map's keys: at the moment of cb invocation, the
38
+ // entry MUST already be gone -> map size 0 (only one alloc was made).
39
+ observedHasCallback = internalCallbacks.size > 0;
40
+ });
41
+
42
+ const handle = store.alloc('Test', { id: 7 }, onRelease);
43
+ const result = store.release(handle);
44
+
45
+ expect(result.ok).toBe(true);
46
+ expect(onRelease).toHaveBeenCalledTimes(1);
47
+ expect(observedHasCallback).toBe(false);
48
+ });
49
+
50
+ it('AC-02 + AC-06: throwing onRelease re-throws once; second release returns Stale (gen incremented, M4)', () => {
51
+ const store = new UniqueRefStore();
52
+ const onRelease = vi.fn((_payload: { id: number }) => {
53
+ throw new Error('intentional cleanup failure');
54
+ });
55
+
56
+ const handle = store.alloc('Test', { id: 9 }, onRelease);
57
+
58
+ // First release must propagate the throw (no try/finally swallowing).
59
+ expect(() => store.release(handle)).toThrow('intentional cleanup failure');
60
+ expect(onRelease).toHaveBeenCalledTimes(1);
61
+
62
+ // After M4 gen increment, the store gen is now 1 while handle gen is 0.
63
+ // Second release returns stale (gen mismatch), not double-release.
64
+ const second = store.release(handle);
65
+ expect(second.ok).toBe(false);
66
+ if (!second.ok) {
67
+ expect(second.error).toBeInstanceOf(UniqueRefStaleError);
68
+ }
69
+ expect(onRelease).toHaveBeenCalledTimes(1);
70
+ });
71
+
72
+ it('AC-07: 1000-iteration alloc/release loop reuses slots (no unbounded growth)', () => {
73
+ const store = new UniqueRefStore();
74
+
75
+ for (let i = 0; i < 1000; i++) {
76
+ const handle = store.alloc('Test', { id: i });
77
+ const result = store.release(handle);
78
+ expect(result.ok).toBe(true);
79
+ }
80
+
81
+ expect(store._liveCount()).toBe(0);
82
+
83
+ // nextSlot is private. After M3 (w7) deletes the gen-255 retirement path
84
+ // a single slot recycles forever and nextSlot stays at 2. Before w7,
85
+ // gen-255 retirement forces ~4 fresh slots over 1000 iters (256 per slot
86
+ // before retirement). The bound below stays true in both regimes and
87
+ // tightens after M3; the inline comment is the bridge between this test
88
+ // and M3's removal of gen retirement (research Finding 1.4).
89
+ // biome-ignore lint/suspicious/noExplicitAny: targeted private read for slot-reuse verification
90
+ const nextSlot = (store as any).nextSlot as number;
91
+ expect(nextSlot).toBeLessThan(10);
92
+ });
93
+ });
94
+
95
+ // ─── w6 M3: gen welding alloc tests ─────────────────────────────────────
96
+ // feat-20260623-asset-handle-generation M3 — alloc embeds generation into
97
+ // the returned handle via codec.pack(slot, gen). First alloc gen=0 (AC-06);
98
+ // slot 0 sentinel stays pack(0,0)===0 and isLive(0)===false (R6).
99
+ // M3 scope: gen welding only, no gen increment on release (that's M4).
100
+ // Reused slots always get gen=0 in M3.
101
+ describe('w6 M3 UniqueRefStore: gen welding on alloc', () => {
102
+ it('AC-06: first alloc gen=0 => handleSlot matches raw slot', () => {
103
+ const store = new UniqueRefStore();
104
+ const handle = store.alloc('TestAsset', { id: 1 });
105
+
106
+ const slot = handleSlot(handle);
107
+ const gen = handleGeneration(handle);
108
+
109
+ expect(gen).toBe(0);
110
+ // UniqueRefStore.nextSlot starts at 1, so the first alloc should be slot 1
111
+ expect(slot).toBe(1);
112
+ });
113
+
114
+ it('AC-05: pack(slot,0)===slot for slot 0-5 (sentinel + typical slots)', () => {
115
+ for (let s = 0; s <= 5; s++) {
116
+ expect(pack(s, 0)).toBe(s);
117
+ }
118
+ });
119
+
120
+ it('R6: slot 0 sentinel — pack(0,0)===0 and isLive(0)===false', () => {
121
+ expect(pack(0, 0)).toBe(0);
122
+
123
+ // isLive should return false for any handle whose raw value is 0.
124
+ // TypeScript disallows constructing Handle<T, 'unique'> with literal 0
125
+ // from outside toUnique, but isLive reads the raw value — passing
126
+ // a brand-cast 0 tests the sentinel path.
127
+ const store = new UniqueRefStore();
128
+ // biome-ignore lint/suspicious/noExplicitAny: sentinel test pattern
129
+ expect(store.isLive(0 as any)).toBe(false);
130
+ });
131
+
132
+ it('alloc welds gen into handle — gen extractable via handleGeneration', () => {
133
+ // After alloc gen welding, handleGeneration returns the gen embedded
134
+ // during alloc. In M3, gen is always 0 because release does not yet
135
+ // increment _generations (M4). But the pack(slot,gen) -> handleGeneration
136
+ // round-trip must work.
137
+ const store = new UniqueRefStore();
138
+ const h = store.alloc('TestAsset', { id: 1 });
139
+ expect(handleGeneration(h)).toBe(0);
140
+ expect(handleSlot(h)).toBe(1);
141
+ expect(pack(handleSlot(h), handleGeneration(h))).toBe(unwrapHandle(h));
142
+ // Not using unwrapHandle directly — using pack+handleSlot+handleGeneration
143
+ // as indirect verification.
144
+ // biome-ignore lint/suspicious/noExplicitAny: access the raw brand value
145
+ expect(h as any as number).toBe(pack(1, 0));
146
+ });
147
+
148
+ it('resolve works correctly after gen-welded alloc (gen increments on release, M4)', () => {
149
+ // Alloc + release + re-alloc: the second handle resolves to the
150
+ // second payload. After M4, release increments gen, re-alloc gets gen=1.
151
+ const store = new UniqueRefStore();
152
+ const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
153
+ expect(handleGeneration(h1)).toBe(0);
154
+
155
+ expect(store.release(h1).ok).toBe(true);
156
+
157
+ const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
158
+ // M4: release incremented gen to 1, so re-alloc gets gen=1
159
+ expect(handleGeneration(h2)).toBe(1);
160
+
161
+ const r = store.resolve(h2);
162
+ expect(r.ok).toBe(true);
163
+ if (r.ok) {
164
+ expect(r.value).toEqual({ mesh: 'sphere' });
165
+ }
166
+ });
167
+
168
+ it('alloc with onRelease fires after gen-welded release', () => {
169
+ const cb = vi.fn();
170
+ const store = new UniqueRefStore();
171
+ const h = store.alloc('Asset', { key: 1 }, cb);
172
+ expect(store.release(h).ok).toBe(true);
173
+ expect(cb).toHaveBeenCalledTimes(1);
174
+ expect(cb).toHaveBeenCalledWith({ key: 1 });
175
+ });
176
+
177
+ it('R6: slot 0 sentinel isLive false — alloc never produces slot 0', () => {
178
+ const store = new UniqueRefStore();
179
+ // Alloc 100 handles and release them — slot 0 should never appear
180
+ // because nextSlot starts at 1 and freeSlots only contain released
181
+ // slots >= 1.
182
+ const handles: ReturnType<typeof store.alloc>[] = [];
183
+ for (let i = 0; i < 100; i++) {
184
+ handles.push(store.alloc('Test', { id: i }));
185
+ }
186
+ for (const h of handles) {
187
+ expect(handleSlot(h)).toBeGreaterThanOrEqual(1);
188
+ expect(store.release(h).ok).toBe(true);
189
+ }
190
+ // All slots recycled — still no slot 0
191
+ const h = store.alloc('Test', { id: 999 });
192
+ expect(handleSlot(h)).toBeGreaterThanOrEqual(1);
193
+ });
194
+
195
+ it('M4: alloc after release reuses slot with gen 1 (gen increments on release)', () => {
196
+ const store = new UniqueRefStore();
197
+ const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
198
+ void handleSlot(h1); // probe slot
199
+ expect(store.release(h1).ok).toBe(true);
200
+
201
+ const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
202
+ expect(handleGeneration(h2)).toBe(1);
203
+
204
+ const r2 = store.resolve(h2);
205
+ expect(r2.ok).toBe(true);
206
+ if (r2.ok) expect(r2.value).toEqual({ mesh: 'sphere' });
207
+
208
+ // h1 (gen=0) is stale now
209
+ const r1 = store.resolve(h1);
210
+ expect(r1.ok).toBe(false);
211
+ if (!r1.ok) expect(r1.error.code).toBe('unique-ref-stale');
212
+ });
213
+ });
214
+
215
+ // ─── w9 M4: stale detection unit tests (red phase) ─────────────────────
216
+ // feat-20260623-asset-handle-generation M4 — gen comparison on
217
+ // resolve/release (UniqueRefStore has no retain). RED phase: gen increment
218
+ // on release is NOT implemented yet, so stale detections return ok.
219
+ describe('w9 M4 UniqueRefStore: stale detection (resolve/release + retire)', () => {
220
+ it('AC-01: stale resolve returns error with code unique-ref-stale', () => {
221
+ const store = new UniqueRefStore();
222
+ const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
223
+ // Release bumps gen 0->1, then re-alloc gets gen=1.
224
+ expect(store.release(h1).ok).toBe(true);
225
+
226
+ const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
227
+ expect(handleGeneration(h2)).toBe(1);
228
+
229
+ // h1 (gen=0) is stale
230
+ const r = store.resolve(h1);
231
+ expect(r.ok).toBe(false);
232
+ if (!r.ok) {
233
+ expect(r.error.code).toBe('unique-ref-stale');
234
+ }
235
+ // h2 resolves normally
236
+ const r2 = store.resolve(h2);
237
+ expect(r2.ok).toBe(true);
238
+ if (r2.ok) {
239
+ expect(r2.value).toEqual({ mesh: 'sphere' });
240
+ }
241
+ });
242
+
243
+ it('AC-01: stale resolve never returns a payload', () => {
244
+ const store = new UniqueRefStore();
245
+ const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
246
+ expect(store.release(h1).ok).toBe(true);
247
+ store.alloc('MeshAsset', { mesh: 'sphere' });
248
+
249
+ const r = store.resolve(h1);
250
+ expect(r.ok).toBe(false);
251
+ if (!r.ok) {
252
+ // biome-ignore lint/suspicious/noExplicitAny: accessing value on Result union error branch to verify no payload leak
253
+ expect((r as any).value).toBeUndefined();
254
+ }
255
+ });
256
+
257
+ it('AC-03: stale release returns error, does NOT drop payload of new holder', () => {
258
+ const store = new UniqueRefStore();
259
+ const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
260
+ expect(store.release(h1).ok).toBe(true);
261
+
262
+ const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
263
+ // h2 is live. Stale release of h1 must NOT drop h2's payload.
264
+ const r = store.release(h1);
265
+ expect(r.ok).toBe(false);
266
+ if (!r.ok) {
267
+ expect(r.error.code).toBe('unique-ref-stale');
268
+ }
269
+
270
+ // h2 is still live — resolve should succeed
271
+ const r2 = store.resolve(h2);
272
+ expect(r2.ok).toBe(true);
273
+ if (r2.ok) {
274
+ expect(r2.value).toEqual({ mesh: 'sphere' });
275
+ }
276
+ });
277
+
278
+ it('AC-03: stale release does not trigger onRelease callback', () => {
279
+ const cb = vi.fn();
280
+ const store = new UniqueRefStore();
281
+ const h1 = store.alloc('MeshAsset', { mesh: 'cube' }, cb);
282
+ expect(store.release(h1).ok).toBe(true);
283
+
284
+ // Re-alloc with new gen
285
+ store.alloc('MeshAsset', { mesh: 'sphere' });
286
+
287
+ // Stale release of h1 should NOT fire h2's callback
288
+ const r = store.release(h1);
289
+ expect(r.ok).toBe(false);
290
+ if (!r.ok) {
291
+ expect(r.error.code).toBe('unique-ref-stale');
292
+ }
293
+ expect(cb).toHaveBeenCalledTimes(1); // only from the first release (gen match)
294
+ });
295
+
296
+ it('AC-04: new handle after reuse resolves and releases normally', () => {
297
+ const store = new UniqueRefStore();
298
+ const h1 = store.alloc('MeshAsset', { mesh: 'cube' });
299
+ expect(store.release(h1).ok).toBe(true);
300
+
301
+ const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
302
+ expect(handleGeneration(h2)).toBe(1);
303
+
304
+ // resolve
305
+ const r = store.resolve(h2);
306
+ expect(r.ok).toBe(true);
307
+ if (r.ok) expect(r.value).toEqual({ mesh: 'sphere' });
308
+
309
+ // release
310
+ expect(store.release(h2).ok).toBe(true);
311
+
312
+ // h1 still stale
313
+ const rStale = store.resolve(h1);
314
+ expect(rStale.ok).toBe(false);
315
+ if (!rStale.ok) {
316
+ expect(rStale.error.code).toBe('unique-ref-stale');
317
+ }
318
+ });
319
+
320
+ it('AC-07: retire-on-255 — gen pushed past MAX_GEN then slot not recycled', () => {
321
+ const store = new UniqueRefStore();
322
+ const h = store.alloc('MeshAsset', { mesh: 'cube' });
323
+ const slot = handleSlot(h);
324
+
325
+ // gen=255 is still a usable handle under the new gen > MAX_GEN predicate.
326
+ // Bump _generations[slot] to 255 and push to freeSlots so the next alloc
327
+ // reuses it with gen=255.
328
+ // biome-ignore lint/suspicious/noExplicitAny: private mutation for retire edge boundary
329
+ (store as any)._generations[slot] = 255;
330
+ // biome-ignore lint/suspicious/noExplicitAny: push slot to free list so alloc reuses
331
+ (store as any).freeSlots.push(slot);
332
+
333
+ // Alloc reuses slot with gen=255
334
+ const h2 = store.alloc('MeshAsset', { mesh: 'sphere' });
335
+ expect(handleGeneration(h2)).toBe(255);
336
+ // Verify gen=255 is a usable handle — resolve succeeds.
337
+ const rResolve = store.resolve(h2);
338
+ expect(rResolve.ok).toBe(true);
339
+
340
+ // Release h2: gen matches, gen++ to 256 (>MAX_GEN), slot retired.
341
+ expect(store.release(h2).ok).toBe(true);
342
+
343
+ // Slot must not be recycled (retired)
344
+ // biome-ignore lint/suspicious/noExplicitAny: private read
345
+ const freeSlots: number[] = (store as any).freeSlots;
346
+ expect(freeSlots.indexOf(slot)).toBe(-1);
347
+ });
348
+ });
@@ -0,0 +1,30 @@
1
+ // @forgeax/engine-ecs — test fixture: brand-unwrap helper.
2
+ //
3
+ // @internal Test-only brand unwrapping for Object.is identity assertions
4
+ // across the family of phantom-branded numeric ids (Handle<T, M>, Entity,
5
+ // SceneInstanceId — all defined as `number & {brand}`). Charter P4
6
+ // (consistent abstraction) — collapses 42 historical brand-unwrap cast
7
+ // sites into one helper so AI users reading the test fixtures see a
8
+ // single intent marker rather than scattered casts. The helper is
9
+ // fixture-scope (lives under __tests__/utils, never re-exported from
10
+ // packages/ecs/src/index.ts) and must not appear in any production code
11
+ // path (AC-08 grep gate).
12
+ //
13
+ // Implementation uses `Number()` to preserve numeric identity while
14
+ // shedding the phantom brand; this avoids the legacy double-cast idiom
15
+ // and keeps the AC-07 grep gate clean even though this file lives
16
+ // inside packages/ecs/src/__tests__/.
17
+
18
+ /**
19
+ * Unwrap a phantom-branded numeric id (`Handle<T, M>`, `Entity`,
20
+ * `SceneInstanceId`, ...) to its underlying `number` representation. Used
21
+ * by test fixtures that need to compare brand-erased numeric values via
22
+ * `Object.is` / `===`.
23
+ *
24
+ * The constraint `H extends number` keeps the helper aligned with the
25
+ * brand idiom (`number & {brand}`) used throughout the codebase without
26
+ * coupling to any specific brand class.
27
+ *
28
+ * @internal
29
+ */
30
+ export const handleNumeric = <H extends number>(h: H): number => Number(h);
@@ -0,0 +1,15 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const rootSource = readFileSync(new URL('../index.ts', import.meta.url), 'utf8');
5
+
6
+ describe('ECS validation helpers stay owner-local', () => {
7
+ it('does not project write-path validators from the public root', async () => {
8
+ expect(rootSource).not.toContain('validateSharedFieldValues');
9
+ expect(rootSource).not.toContain('validateEnumFieldValues');
10
+
11
+ const root = (await import('../index')) as Record<string, unknown>;
12
+ expect(root).not.toHaveProperty('validateSharedFieldValues');
13
+ expect(root).not.toHaveProperty('validateEnumFieldValues');
14
+ });
15
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defineComponent } from '../component';
3
+ import { World } from '../world';
4
+
5
+ const VisibilityLike = defineComponent('M1VisibilityLikeAtomic', {
6
+ state: { type: 'enum', default: 0, labels: { inherited: 0, hidden: 1, visible: 2 } },
7
+ });
8
+
9
+ const Position = defineComponent('M1VisibilityAtomicPosition', {
10
+ value: 'f32',
11
+ });
12
+
13
+ function expectInvalidValue(result: { ok: boolean; error?: unknown }, entity?: number): void {
14
+ expect(result.ok).toBe(false);
15
+ if (result.ok) return;
16
+ const error = result.error as {
17
+ code: string;
18
+ expected: string;
19
+ hint: string;
20
+ detail: {
21
+ entity: number | undefined;
22
+ component: string;
23
+ field: string;
24
+ received: unknown;
25
+ allowedValues: Readonly<Record<string, number>>;
26
+ };
27
+ };
28
+ expect(error.code).toBe('component-field-invalid-value');
29
+ expect(error.expected).toContain('inherited');
30
+ expect(error.hint).toContain('VisibilityLike');
31
+ expect(error.detail).toEqual({
32
+ entity,
33
+ component: 'M1VisibilityLikeAtomic',
34
+ field: 'state',
35
+ received: 9,
36
+ allowedValues: { inherited: 0, hidden: 1, visible: 2 },
37
+ });
38
+ }
39
+
40
+ describe('M1 Visibility enum writes are atomic', () => {
41
+ it('rejects an invalid enum during spawn before creating the row', () => {
42
+ const world = new World();
43
+
44
+ const result = world.spawn({ component: VisibilityLike, data: { state: 9 } });
45
+
46
+ expectInvalidValue(result);
47
+ expect(world.spawn({ component: Position, data: { value: 1 } }).unwrap()).toBe(0);
48
+ });
49
+
50
+ it('rejects an invalid enum during addComponent before archetype migration', () => {
51
+ const world = new World();
52
+ const entity = world.spawn({ component: Position, data: { value: 1 } }).unwrap();
53
+
54
+ const result = world.addComponent(entity, {
55
+ component: VisibilityLike,
56
+ data: { state: 9 },
57
+ });
58
+
59
+ expectInvalidValue(result, entity);
60
+ expect(world.get(entity, VisibilityLike).ok).toBe(false);
61
+ expect(world.get(entity, Position).unwrap().value).toBe(1);
62
+ });
63
+
64
+ it('rejects an invalid enum during set and preserves the previous value', () => {
65
+ const world = new World();
66
+ const entity = world.spawn({ component: VisibilityLike, data: { state: 1 } }).unwrap();
67
+
68
+ const result = world.set(entity, VisibilityLike, { state: 9 });
69
+
70
+ expectInvalidValue(result, entity);
71
+ expect(world.get(entity, VisibilityLike).unwrap().state).toBe(1);
72
+ });
73
+
74
+ it('accepts every declared enum value through all three write entries', () => {
75
+ const world = new World();
76
+ const entity = world.spawn({ component: VisibilityLike, data: { state: 0 } }).unwrap();
77
+
78
+ expect(world.set(entity, VisibilityLike, { state: 1 }).ok).toBe(true);
79
+ expect(world.addComponent(entity, { component: Position, data: { value: 2 } }).ok).toBe(true);
80
+ expect(world.get(entity, VisibilityLike).unwrap().state).toBe(1);
81
+ });
82
+ });