@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,1421 @@
1
+ // @forgeax/engine-ecs — world-component-access: component storage and access.
2
+ //
3
+ // This module owns component rows, managed storage, array operations, archetype
4
+ // migration, and the relationship callbacks that mutate component storage. World
5
+ // remains the typed facade and supplies one narrow per-World state capability.
6
+
7
+ import { err, isRetiredSlot, ok, type Result, unwrapHandle } from '@forgeax/engine-types';
8
+ import type { BufferPool } from './buffer-pool';
9
+ import {
10
+ bufferFieldByteLength,
11
+ type Component,
12
+ type ComponentSchema,
13
+ componentId,
14
+ componentSchema,
15
+ type InputShapeOf,
16
+ isEntityField,
17
+ isManagedBufferField,
18
+ isManagedField,
19
+ type ManagedArrayElementType,
20
+ type ManagedArrayElementValue,
21
+ type ShapeOf,
22
+ TYPE_METADATA,
23
+ } from './component';
24
+ import { fillComponentDefaults, validateComponentDataKeys } from './component-default-fallback';
25
+ import { componentDefinition } from './component-schema';
26
+ import { validateManagedArrayValues, validateSharedFieldValues } from './component-value-validate';
27
+ import { Entity as EntityComponent } from './entity';
28
+ import {
29
+ ENTITY_MAX_INDEX,
30
+ ENTITY_NULL_RAW,
31
+ type EntityHandle,
32
+ encodeEntity,
33
+ entityGeneration,
34
+ entityIndex,
35
+ } from './entity-handle';
36
+ import {
37
+ ComponentAlreadyPresentError,
38
+ ComponentNotPresentError,
39
+ EntityIndexOverflowError,
40
+ FixedSizeMismatchError,
41
+ ManagedBufferOutOfBoundsError,
42
+ RelationshipSelfCycleError,
43
+ RelationshipTargetReadonlyError,
44
+ RemoveEssentialComponentError,
45
+ StaleEntityError,
46
+ validateEnumFieldValues,
47
+ validateNumericFieldValues,
48
+ } from './errors';
49
+ import {
50
+ isRelationshipTarget,
51
+ RelationshipIndex,
52
+ relationshipMirror,
53
+ relationshipRole,
54
+ } from './relationship-index';
55
+ import type { SharedRefStore } from './shared-ref-store';
56
+ import { type Archetype, appendArchetypeRow } from './storage/archetype';
57
+ import {
58
+ type ArchetypeGraph,
59
+ getAddEdge,
60
+ getOrCreateArchetype,
61
+ getRemoveEdge,
62
+ getTable,
63
+ } from './storage/archetype-graph';
64
+ import { removeSparseTag } from './storage/change-detection';
65
+ import { arrayCountColumnName, type FieldView, normalizeBufferWrite } from './storage/column';
66
+ import { appendTableRow, type Table } from './storage/table';
67
+ import type { UniqueRefStore } from './unique-ref-store';
68
+ import type { ComponentData, EcsError, EntityRecord } from './world';
69
+ import { ComponentStorage } from './world-component-storage';
70
+
71
+ type ErrorContext = { readonly systemName: string };
72
+
73
+ type ArrayFieldsOf<S extends ComponentSchema> = {
74
+ [K in keyof S]: S[K] extends
75
+ | `array<${ManagedArrayElementType}>`
76
+ | `array<${ManagedArrayElementType}, ${number}>`
77
+ ? K
78
+ : never;
79
+ }[keyof S];
80
+
81
+ type ArrayFieldElementValue<
82
+ S extends ComponentSchema,
83
+ K extends keyof S,
84
+ > = S[K] extends `array<${infer Elem extends ManagedArrayElementType}>`
85
+ ? ManagedArrayElementValue<Elem>
86
+ : S[K] extends `array<${infer Elem extends ManagedArrayElementType}, ${number}>`
87
+ ? ManagedArrayElementValue<Elem>
88
+ : never;
89
+
90
+ function relationshipPayloadWrites(data: Readonly<Record<string, unknown>>): boolean {
91
+ return Object.values(data).some((value) => {
92
+ if (Array.isArray(value)) return value.length > 0;
93
+ if (ArrayBuffer.isView(value)) return value.byteLength > 0;
94
+ return true;
95
+ });
96
+ }
97
+
98
+ export interface ComponentAccessState {
99
+ readonly graph: ArchetypeGraph;
100
+ readonly records: EntityRecord[];
101
+ readonly freeIndices: number[];
102
+ readonly bufferPool: BufferPool;
103
+ readonly uniqueRefs: UniqueRefStore;
104
+ readonly sharedRefs: SharedRefStore;
105
+ readonly relationshipIndexes: Map<number, RelationshipIndex>;
106
+ readonly markComponentAdded: (entity: EntityHandle, componentId: number) => void;
107
+ readonly markComponentsAdded: (entity: EntityHandle, componentIds: readonly number[]) => void;
108
+ readonly markComponentChanged: (entity: EntityHandle, componentId: number) => void;
109
+ readonly removeComponentChange: (entity: EntityHandle, componentId: number) => void;
110
+ readonly markStructureChanged: () => void;
111
+ routeError(err: unknown, ctx: ErrorContext): void;
112
+ }
113
+
114
+ export class WorldComponentAccess {
115
+ private readonly storage: ComponentStorage;
116
+
117
+ constructor(private readonly state: ComponentAccessState) {
118
+ this.storage = new ComponentStorage(state);
119
+ }
120
+
121
+ private get graph(): ArchetypeGraph {
122
+ return this.state.graph;
123
+ }
124
+
125
+ private get records(): EntityRecord[] {
126
+ return this.state.records;
127
+ }
128
+
129
+ private table(archetype: Archetype): Table {
130
+ return getTable(this.graph, archetype.tableId);
131
+ }
132
+
133
+ private tableRow(record: EntityRecord): number {
134
+ return this.graph.archetypes[record.archetypeId]?.rows[record.archetypeRow] ?? -1;
135
+ }
136
+
137
+ private get freeIndices(): number[] {
138
+ return this.state.freeIndices;
139
+ }
140
+
141
+ private get bufferPool(): BufferPool {
142
+ return this.state.bufferPool;
143
+ }
144
+
145
+ private get uniqueRefs(): UniqueRefStore {
146
+ return this.state.uniqueRefs;
147
+ }
148
+
149
+ private routeError(err: unknown, ctx: ErrorContext): void {
150
+ this.state.routeError(err, ctx);
151
+ }
152
+
153
+ private relationshipIndex(component: Component): RelationshipIndex | undefined {
154
+ if (relationshipRole(component)?.kind !== 'source') return undefined;
155
+ let index = this.state.relationshipIndexes.get(componentId(component));
156
+ if (index === undefined) {
157
+ index = new RelationshipIndex();
158
+ this.state.relationshipIndexes.set(componentId(component), index);
159
+ }
160
+ return index;
161
+ }
162
+
163
+ /** Read the World-owned materialized target array; never consults a shadow list. */
164
+ relationshipTargetEntries(source: Component, target: EntityHandle): readonly EntityHandle[] {
165
+ const role = relationshipRole(source);
166
+ if (role?.kind !== 'source') return [];
167
+ const mirror = relationshipMirror(source);
168
+ if (mirror === undefined) return [];
169
+ const result = this.get(target, mirror);
170
+ if (!result.ok) return [];
171
+ const entries = (result.value as Record<string, unknown>)[role.targetField];
172
+ return entries !== undefined && typeof entries === 'object' ? (entries as EntityHandle[]) : [];
173
+ }
174
+
175
+ private markComponentAdded(entity: EntityHandle, component: Component): void {
176
+ this.state.markComponentAdded(entity, componentId(component));
177
+ }
178
+
179
+ private markComponentChanged(entity: EntityHandle, component: Component): void {
180
+ this.state.markComponentChanged(entity, componentId(component));
181
+ }
182
+
183
+ private markStructureChanged(): void {
184
+ this.state.markStructureChanged();
185
+ }
186
+
187
+ relationshipTargetEntity(
188
+ component: Component,
189
+ value: Record<string, unknown>,
190
+ ): EntityHandle | null {
191
+ for (const [fieldName, fieldType] of Object.entries(componentSchema(component))) {
192
+ if (isEntityField(fieldType)) {
193
+ const raw = value[fieldName];
194
+ if (raw === null || raw === undefined) return null;
195
+ const asNum = raw as number;
196
+ if (asNum === ENTITY_NULL_RAW) return null;
197
+ return asNum as EntityHandle;
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+
203
+ private preflightComponentFieldValues(
204
+ holder: EntityHandle | null,
205
+ componentData: ComponentData,
206
+ ): Result<void, EcsError> {
207
+ const data = componentData.data as Record<string, unknown>;
208
+ const arrayError = validateManagedArrayValues(componentData.component, data);
209
+ if (arrayError !== null) return err(arrayError as unknown as EcsError);
210
+ const sharedError = validateSharedFieldValues(componentData.component, data);
211
+ if (sharedError !== null) return err(sharedError as unknown as EcsError);
212
+ const numericError = validateNumericFieldValues(
213
+ componentData.component,
214
+ data,
215
+ holder === null ? undefined : (holder as number),
216
+ );
217
+ if (numericError !== null) return err(numericError as unknown as EcsError);
218
+ return ok(undefined);
219
+ }
220
+
221
+ /**
222
+ * Validate one structural component payload without touching archetypes,
223
+ * columns, relationship mirrors, epochs, or managed-reference stores.
224
+ * CommandBuffer uses this same owner-level gate as the direct World facade;
225
+ * the optional pending set lets a batch refer to an entity reserved earlier
226
+ * in that batch without mistaking it for a stale live handle.
227
+ */
228
+ preflightComponentData(
229
+ holder: EntityHandle | null,
230
+ componentData: ComponentData,
231
+ pendingEntities?: ReadonlySet<number>,
232
+ unavailableEntities?: ReadonlySet<number>,
233
+ ): Result<void, EcsError> {
234
+ const data = componentData.data as Record<string, unknown>;
235
+ const keyError = validateComponentDataKeys(componentData.component, data);
236
+ if (keyError !== null) return err(keyError as unknown as EcsError);
237
+ const valuePreflight = this.preflightComponentFieldValues(holder, componentData);
238
+ if (!valuePreflight.ok) return valuePreflight;
239
+ if (isRelationshipTarget(componentData.component) && relationshipPayloadWrites(data)) {
240
+ return err(new RelationshipTargetReadonlyError(componentData.component.name, 'command'));
241
+ }
242
+
243
+ const filled = fillComponentDefaults(componentData.component, data);
244
+ const enumError = validateEnumFieldValues(
245
+ componentData.component,
246
+ filled,
247
+ holder === null ? undefined : (holder as number),
248
+ );
249
+ if (enumError !== null) return err(enumError as unknown as EcsError);
250
+
251
+ const role = relationshipRole(componentData.component as Component);
252
+ if (role?.kind !== 'source') return ok(undefined);
253
+ const target = this.relationshipTargetEntity(componentData.component as Component, filled);
254
+ if (target === null) return ok(undefined);
255
+
256
+ const targetRaw = target as unknown as number;
257
+ if (unavailableEntities?.has(targetRaw) === true) {
258
+ const targetRecord = this.records[entityIndex(target)];
259
+ return err(
260
+ new StaleEntityError(target as number, entityIndex(target), entityGeneration(target), {
261
+ operation: 'relationship-insert',
262
+ component: componentData.component.name,
263
+ expectedGeneration: entityGeneration(target),
264
+ actualGeneration: targetRecord?.generation ?? -1,
265
+ }),
266
+ );
267
+ }
268
+ const targetIsPending = pendingEntities?.has(targetRaw) === true;
269
+ const targetRecord = this.records[entityIndex(target)];
270
+ const actualGeneration = targetRecord?.generation ?? -1;
271
+ const targetLive = this.recordIsLive(targetRecord, entityGeneration(target));
272
+ const holderIsPending =
273
+ holder === null || pendingEntities?.has(holder as unknown as number) === true;
274
+ if (!targetIsPending && !targetLive && !holderIsPending) {
275
+ return err(
276
+ new StaleEntityError(target as number, entityIndex(target), entityGeneration(target), {
277
+ operation: 'relationship-insert',
278
+ component: componentData.component.name,
279
+ expectedGeneration: entityGeneration(target),
280
+ actualGeneration,
281
+ }),
282
+ );
283
+ }
284
+
285
+ // A pending holder has no row to walk yet. Once materialized, its target
286
+ // is still checked by the same source-side relationship callback.
287
+ if (holder === null || pendingEntities?.has(holder as unknown as number) === true) {
288
+ return ok(undefined);
289
+ }
290
+ const roleAllowsSelf = role?.kind === 'source' && role.allowSelf;
291
+ if (holder === target && !roleAllowsSelf) {
292
+ return err(
293
+ new RelationshipSelfCycleError(
294
+ componentData.component.name,
295
+ holder as number,
296
+ target as number,
297
+ ),
298
+ );
299
+ }
300
+
301
+ const cycleHit =
302
+ holder === target && roleAllowsSelf
303
+ ? null
304
+ : this.relationshipCycleHit(componentData.component as Component, target, holder);
305
+ if (cycleHit !== null) {
306
+ return err(
307
+ new RelationshipSelfCycleError(
308
+ componentData.component.name,
309
+ holder as number,
310
+ cycleHit as number,
311
+ ),
312
+ );
313
+ }
314
+ return ok(undefined);
315
+ }
316
+
317
+ private relationshipCycleHit(
318
+ holderComponent: Component,
319
+ start: EntityHandle,
320
+ holder: EntityHandle,
321
+ ): EntityHandle | null {
322
+ const visited = new Set<number>();
323
+ let current = start;
324
+ while (true) {
325
+ if (current === holder) return current;
326
+ const raw = current as unknown as number;
327
+ if (visited.has(raw)) return null;
328
+ visited.add(raw);
329
+ const record = this.records[entityIndex(current)];
330
+ if (!this.recordIsLive(record, entityGeneration(current))) return null;
331
+ const archetype = this.graph.archetypes[record.archetypeId];
332
+ if (
333
+ !archetype?.components.some(
334
+ (candidate) => componentId(candidate) === componentId(holderComponent),
335
+ )
336
+ ) {
337
+ return null;
338
+ }
339
+ const value = this.readRow(archetype, holderComponent, this.tableRow(record)) as Record<
340
+ string,
341
+ unknown
342
+ >;
343
+ const next = this.relationshipTargetEntity(holderComponent, value);
344
+ if (next === null) return null;
345
+ current = next;
346
+ }
347
+ }
348
+
349
+ /** Prepare the target side before a source archetype mutation commits. */
350
+ private prepareRelationshipInsert(
351
+ component: Component,
352
+ value: Record<string, unknown>,
353
+ ): Result<void, EcsError> {
354
+ const role = relationshipRole(component);
355
+ if (role?.kind !== 'source') return ok(undefined);
356
+ const target = this.relationshipTargetEntity(component, value);
357
+ if (target === null) return ok(undefined);
358
+ const mirror = relationshipMirror(component);
359
+ if (mirror === undefined) return ok(undefined);
360
+ const targetRec = this.records[entityIndex(target)];
361
+ const actualGeneration = targetRec?.generation ?? -1;
362
+ if (!this.recordIsLive(targetRec, entityGeneration(target))) {
363
+ return err(
364
+ new StaleEntityError(target as number, entityIndex(target), entityGeneration(target), {
365
+ operation: 'relationship-insert',
366
+ component: component.name,
367
+ expectedGeneration: entityGeneration(target),
368
+ actualGeneration,
369
+ }),
370
+ );
371
+ }
372
+ const targetArch = this.graph.archetypes[targetRec.archetypeId];
373
+ const hasMirror =
374
+ targetArch?.components.some((candidate) => componentId(candidate) === componentId(mirror)) ??
375
+ false;
376
+ if (!hasMirror) {
377
+ const added = this._addComponentCore(
378
+ target,
379
+ { component: mirror, data: {} as Partial<ShapeOf<ComponentSchema>> },
380
+ true,
381
+ );
382
+ if (!added.ok) return added;
383
+ }
384
+ const length = this.relationshipTargetEntries(component, target).length;
385
+ return this.ensureArrayCapacity(target, mirror, role.targetField as never, length + 1);
386
+ }
387
+
388
+ /** Append `holder` to the materialized target list. */
389
+ relationshipOnInsert(
390
+ holder: EntityHandle,
391
+ component: Component,
392
+ value: Record<string, unknown>,
393
+ ): Result<void, EcsError> {
394
+ const role = relationshipRole(component);
395
+ if (role?.kind !== 'source') return ok(undefined);
396
+ const target = this.relationshipTargetEntity(component, value);
397
+ if (target === null) return ok(undefined);
398
+ const mirror = relationshipMirror(component);
399
+ /* istanbul ignore next -- defineComponent relationship validation guarantees mirror exists */
400
+ if (mirror === undefined) return ok(undefined);
401
+
402
+ const prepared = this.prepareRelationshipInsert(component, value);
403
+ if (!prepared.ok) {
404
+ // A dangling source edge is still useful state: hierarchy/animation
405
+ // projections report the missing target. The target mirror cannot be
406
+ // updated, but insertion itself remains atomic and successful.
407
+ if (prepared.error.code === 'stale-entity') return ok(undefined);
408
+ return prepared;
409
+ }
410
+
411
+ // Lazy-create the mirror component on the target when absent (D-3c).
412
+ const targetSlot = entityIndex(target);
413
+ const targetRec = this.records[targetSlot];
414
+ if (!this.recordIsLive(targetRec, entityGeneration(target))) return ok(undefined);
415
+ const targetArch = this.graph.archetypes[targetRec.archetypeId];
416
+ const mirrorLocalId = componentId(mirror);
417
+ const hasMirror =
418
+ targetArch?.components.some((component) => componentId(component) === mirrorLocalId) ?? false;
419
+ if (!hasMirror) {
420
+ const added = this._addComponentCore(
421
+ target,
422
+ {
423
+ component: mirror,
424
+ data: {} as Partial<ShapeOf<ComponentSchema>>,
425
+ },
426
+ true,
427
+ );
428
+ if (!added.ok) return added;
429
+ }
430
+ const targetEntries = this.relationshipTargetEntries(component, target);
431
+ const slot = targetEntries.length;
432
+ const mirrored = this.appendArrayElement(
433
+ target,
434
+ mirror as Component<string, ComponentSchema>,
435
+ role.targetField as never,
436
+ holder as never,
437
+ );
438
+ if (!mirrored.ok) return mirrored;
439
+ this.relationshipIndex(component)?.attach(holder, target, slot);
440
+ return ok(undefined);
441
+ }
442
+
443
+ /** Remove `holder` from the materialized target list. */
444
+ relationshipOnRemove(
445
+ holder: EntityHandle,
446
+ component: Component,
447
+ oldValue: Record<string, unknown>,
448
+ ): Result<void, EcsError> {
449
+ const role = relationshipRole(component);
450
+ if (role?.kind !== 'source') return ok(undefined);
451
+ const target = this.relationshipTargetEntity(component, oldValue);
452
+ if (target === null) return ok(undefined);
453
+ const mirror = relationshipMirror(component);
454
+ /* istanbul ignore next -- defineComponent relationship validation guarantees mirror exists */
455
+ if (mirror === undefined) return ok(undefined);
456
+ const targetSlot = entityIndex(target);
457
+ const targetRec = this.records[targetSlot];
458
+ if (!this.recordIsLive(targetRec, entityGeneration(target))) return ok(undefined);
459
+
460
+ const index = this.relationshipIndex(component);
461
+ if (index === undefined) return ok(undefined);
462
+ const slot = index.slotOf(holder);
463
+ if (slot === undefined || index.targetOf(holder) !== target) return ok(undefined);
464
+ const mirrored = this.removeArrayElementAt(
465
+ target,
466
+ mirror as Component<string, ComponentSchema>,
467
+ role.targetField as never,
468
+ slot,
469
+ );
470
+ if (!mirrored.ok) return mirrored;
471
+ index.detach(holder);
472
+ if (mirrored.value !== undefined) index.updateSlot(mirrored.value, target, slot);
473
+ return ok(undefined);
474
+ }
475
+
476
+ /**
477
+ * Read component data from an entity.
478
+ *
479
+ * **Transient view contract (feat-20260602):** for fixed-capacity
480
+ * `array<T,N>` and `buffer<N>` fields, the returned `TypedArray` (and any
481
+ * subarray of it) aliases the archetype column buffer directly. The view is
482
+ * valid only until the next structural change (`spawn` / `despawn` /
483
+ * `addComponent` / `removeComponent`). Holding a view across a structural
484
+ * change is undefined behaviour -- the backing `ArrayBuffer` is detached on
485
+ * column growth, and swap-remove at the same row index points to the wrong
486
+ * entity. **Re-fetch `world.get(e, C)` on every access.** See
487
+ * `packages/ecs/README.md` Transient view contract section.
488
+ *
489
+ * @returns `Result<ShapeOf<S>, EcsError>` —
490
+ * `ok(ShapeOf<S>)` on success;
491
+ * `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
492
+ * `err(ComponentNotPresentError)` (`.code = 'component-not-present'`) if
493
+ * the entity does not have the component (a never-present component on
494
+ * this entity degrades to the same `component-not-present` path — there is
495
+ * no separate "not registered" failure; components are global at
496
+ * `defineComponent` time).
497
+ *
498
+ * @example
499
+ * ```ts
500
+ * const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
501
+ * const world = new World();
502
+ * const e = world.spawn({ component: Position, data: { x: 1, y: 2 } }).unwrap();
503
+ * const r = world.get(e, Position);
504
+ * if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
505
+ * const pos = r.value;
506
+ * ```
507
+ */
508
+ get<S extends ComponentSchema>(
509
+ entity: EntityHandle,
510
+ component: Component<string, S>,
511
+ ): Result<ShapeOf<S>, EcsError> {
512
+ const record = this.lookupAlive(entity, 'get', component.name);
513
+ if (!record.ok) return record;
514
+
515
+ const rec = record.value;
516
+ const arch = this.graph.archetypes[rec.archetypeId];
517
+ /* istanbul ignore next -- defensive: alive record always has valid archetypeId */
518
+ if (!arch) {
519
+ return err(
520
+ new StaleEntityError(entity as number, entityIndex(entity), entityGeneration(entity), {
521
+ operation: 'get',
522
+ component: component.name,
523
+ expectedGeneration: entityGeneration(entity),
524
+ actualGeneration: rec.generation,
525
+ }),
526
+ );
527
+ }
528
+
529
+ // Check if this archetype has the component (using World-local ID).
530
+ const localId = componentId(component);
531
+ if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
532
+ return err(new ComponentNotPresentError(entity as number, component.name));
533
+ }
534
+
535
+ return ok(this.storage.readRow(arch, component, this.tableRow(rec)));
536
+ }
537
+
538
+ /**
539
+ * Column-level zero-copy view of an `array<T, N>` / `array<T>` field.
540
+ *
541
+ * Resolves the live byte region for `(entity, component, fieldName)`
542
+ * directly at the column level and returns the element-typed TypedArray
543
+ * aliasing it (`view.buffer` is the SSOT byte region; mutations route
544
+ * through `world.set`). Unlike `get`, this does NOT build the
545
+ * `{}` whole-component object nor walk every schema field. Per-frame
546
+ * consumers that need one column (the resolved world mat4) take this path to
547
+ * avoid the `get` overhead (1 `{}` alloc + N-field readRow walk).
548
+ *
549
+ * Fixed `array<T,N>` columns (feat-20260602) store their elements inline, so
550
+ * the view aliases the archetype column buffer directly (no BufferPool
551
+ * indirection); variable `array<T>` columns still alias the BufferPool slot.
552
+ * The returned view's element type follows the schema element type
553
+ * (`array<entity,N>` -> `Uint32Array`, `array<f32,N>` -> `Float32Array`,
554
+ * etc.) -- the prior f32-only early-return gate is removed.
555
+ *
556
+ * **Transient view contract:** the returned `TypedArray` aliases the column
557
+ * buffer and is valid only until the next structural change (`spawn` /
558
+ * `despawn` / `addComponent` / `removeComponent`). Column growth
559
+ * (`growColumn`) detaches the old `ArrayBuffer` via `transfer()`; a
560
+ * swap-remove at the same row index leaves the view pointing to the wrong
561
+ * entity. **Callers must re-fetch `_getArrayView` on every access** and must
562
+ * not hold the view across any operation that may cause archetype migration.
563
+ * All existing per-frame consumers (`propagateTransforms` / `render-extract`
564
+ * / `pick`) already conform -- they fetch the view inside a single pass with
565
+ * no intervening structural changes.
566
+ *
567
+ * Returns `undefined` when the entity is dead, the component is absent, the
568
+ * field does not exist, or the field is not an `array<...>` column.
569
+ *
570
+ * @internal Engine-internal fast path; AI users read the typed view through
571
+ * `world.get(e, Transform).world`. The accessor is the zero-materialization
572
+ * route the propagate kernel and render walk use.
573
+ */
574
+ _getArrayView(
575
+ entity: EntityHandle,
576
+ component: Component,
577
+ fieldName: string,
578
+ ): FieldView | undefined {
579
+ const record = this.lookupAlive(entity, '_getArrayView', component.name);
580
+ if (!record.ok) return undefined;
581
+
582
+ const rec = record.value;
583
+ const arch = this.graph.archetypes[rec.archetypeId];
584
+ if (!arch) return undefined;
585
+ return this.storage.readArrayView(arch, component, this.tableRow(rec), fieldName);
586
+ }
587
+
588
+ /**
589
+ * Write (partial) component data to an entity.
590
+ *
591
+ * @returns `Result<void, EcsError>` —
592
+ * `ok(void)` on success;
593
+ * `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
594
+ * `err(ComponentNotPresentError)` (`.code = 'component-not-present'`) if
595
+ * entity does not have the component (F-02: no longer silently ignores).
596
+ *
597
+ * @example
598
+ * ```ts
599
+ * const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
600
+ * const world = new World();
601
+ * const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
602
+ * const r = world.set(e, Position, { x: 10 });
603
+ * if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
604
+ * r.unwrap();
605
+ * ```
606
+ */
607
+ set<S extends ComponentSchema>(
608
+ entity: EntityHandle,
609
+ component: Component<string, S>,
610
+ value: Partial<InputShapeOf<S>>,
611
+ markChanged = true,
612
+ ): Result<void, EcsError> {
613
+ const record = this.lookupAlive(entity, 'set', component.name);
614
+ if (!record.ok) return record;
615
+
616
+ const rec = record.value;
617
+ const arch = this.graph.archetypes[rec.archetypeId];
618
+ /* istanbul ignore next -- defensive: alive record always has valid archetypeId */
619
+ if (!arch) {
620
+ return err(
621
+ new StaleEntityError(entity as number, entityIndex(entity), entityGeneration(entity), {
622
+ operation: 'set',
623
+ component: component.name,
624
+ expectedGeneration: entityGeneration(entity),
625
+ actualGeneration: rec.generation,
626
+ }),
627
+ );
628
+ }
629
+ const localId = componentId(component);
630
+ if (!arch.components.some((candidate) => componentId(candidate) === localId)) {
631
+ // F-02: set on missing component returns err instead of silent ignore
632
+ return err(new ComponentNotPresentError(entity as number, component.name));
633
+ }
634
+ const valuePreflight = this.preflightComponentFieldValues(entity, {
635
+ component,
636
+ data: value,
637
+ });
638
+ if (!valuePreflight.ok) return valuePreflight;
639
+ const currentValue = this.storage.readRow(arch, component, this.tableRow(rec)) as Record<
640
+ string,
641
+ unknown
642
+ >;
643
+ const enumError = validateEnumFieldValues(
644
+ component,
645
+ { ...currentValue, ...(value as Record<string, unknown>) },
646
+ entity as number,
647
+ );
648
+ if (enumError !== null) return err(enumError as unknown as EcsError);
649
+ if (component.storage === 'sparse') {
650
+ if (markChanged) this.markComponentChanged(entity, component);
651
+ return ok(undefined);
652
+ }
653
+ const fieldCols = this.table(arch).storage.get(localId)?.fields;
654
+ if (fieldCols === undefined) {
655
+ throw new Error(`Table storage for ${component.name} does not exist.`);
656
+ }
657
+ for (const fieldName of Object.keys(value)) {
658
+ const col = fieldCols.get(fieldName);
659
+ if (!col) {
660
+ continue;
661
+ }
662
+ const fieldType = (componentSchema(component) as Record<string, string>)[fieldName] ?? '';
663
+ // M1/M2 release loop (set path): release the prior managed value
664
+ // BEFORE writing the new one. Single SSOT helper `releaseManagedFieldOnRow`
665
+ // (feat-20260614 D-2) covers every managed-field family (`ref<T>` /
666
+ // `string` / `buffer` / variable `array<T>`); it self-skips fields that
667
+ // do not match `isManagedField` here, but for set-ref/string we already
668
+ // gated on it so the call is hot. Zeroes the column when applicable.
669
+ if (isManagedField(fieldType)) {
670
+ this.storage.releaseManagedFieldOnRow(arch, component, this.tableRow(rec), fieldName);
671
+ }
672
+ const raw = (value as Record<string, unknown>)[fieldName];
673
+ if (fieldType === 'bool') {
674
+ col.view[this.tableRow(rec)] = raw ? 1 : 0;
675
+ } else if (isEntityField(fieldType)) {
676
+ // M3 entity field overwrite: encode null as ENTITY_NULL_RAW;
677
+ // otherwise store the Entity bit pattern (slot+gen).
678
+ col.view[this.tableRow(rec)] =
679
+ raw === null || raw === undefined ? ENTITY_NULL_RAW : (raw as number);
680
+ } else if (isManagedBufferField(fieldType)) {
681
+ // M2 set path: collapsed-vocab keyword family `'buffer'` (variable) +
682
+ // `'buffer<N>'` (fixed). The two shapes diverge here:
683
+ // - `buffer<N>` — schema-declared byteLength is fixed; raw must be a
684
+ // `Uint8Array` whose `byteLength === N`. Mismatched payloads route
685
+ // `FixedSizeMismatchError` via Result.err so AI users observe an
686
+ // explicit failure instead of silent truncation (verify round 1
687
+ // B1 fix; charter P3 — explicit failure > silent acceptance).
688
+ // - `'buffer'` — variable capacity; release the prior slot then
689
+ // alloc a fresh one sized to the new payload's byteLength (mirrors
690
+ // the `array<T>` set path's release-then-alloc D-5 ordering).
691
+ // raw is normalized from any AllowSharedBufferSource view to a
692
+ // Uint8Array over its bytes (feat-20260621 V2 / AC-A4). Non-buffer
693
+ // raw (a forced cast feeding e.g. a number) normalizes to null and
694
+ // is treated as a no-op (column slot stays unchanged).
695
+ const isFixedBuffer = fieldType !== 'buffer';
696
+ const bytes = normalizeBufferWrite(raw);
697
+ if (bytes !== null) {
698
+ if (isFixedBuffer) {
699
+ // feat-20260602: fixed `buffer<N>` lives inline as a stride-N u8
700
+ // column (arity = N bytes). Write the payload straight into the
701
+ // row window -- no BufferPool slot.
702
+ const expected = bufferFieldByteLength(fieldType);
703
+ if (bytes.byteLength !== expected) {
704
+ return err(new FixedSizeMismatchError(fieldName, expected, bytes.byteLength));
705
+ }
706
+ const arity = col.arity;
707
+ (col.view as Uint8Array).set(bytes.subarray(0, arity), this.tableRow(rec) * arity);
708
+ } else {
709
+ // Variable `'buffer'` set: release prior slot via SSOT helper
710
+ // (feat-20260614 D-2) then alloc fresh sized to the new payload
711
+ // (verify round 1 B2 fix path). The helper zeroes the column on
712
+ // release; sentinel slot id 0 is a no-op.
713
+ this.storage.releaseManagedFieldOnRow(arch, component, this.tableRow(rec), fieldName);
714
+ const allocR = this.bufferPool.alloc(bytes.byteLength);
715
+ if (!allocR.ok) {
716
+ const ctx: ErrorContext = {
717
+ systemName: `World.set (${component.name}.${fieldName})`,
718
+ };
719
+ this.routeError(allocR.error, ctx);
720
+ col.view[this.tableRow(rec)] = 0;
721
+ continue;
722
+ }
723
+ const slot = allocR.value;
724
+ slot.view.set(bytes);
725
+ col.view[this.tableRow(rec)] = slot.id;
726
+ }
727
+ }
728
+ } else if (fieldType === 'string') {
729
+ // M1 string-field set path (AC-05 path 3): the prior handle was
730
+ // already released by the unified `isManagedField` pre-write block
731
+ // above (D-R3) -- here we just alloc the new handle and store the
732
+ // u32. Mirrors the array<T> release-then-alloc pattern (D-5) so AI
733
+ // users observe the UniqueRefStore _liveCount net-zero invariant
734
+ // on field overwrite. Missing / non-string raw -> '' fallback
735
+ // (AC-06).
736
+ const text = typeof raw === 'string' ? raw : '';
737
+ const handle = this.uniqueRefs.alloc<'String'>('String', text);
738
+ col.view[this.tableRow(rec)] = unwrapHandle(handle);
739
+ } else {
740
+ const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
741
+ if (arrayMeta !== undefined) {
742
+ // M1 set path for array<T> / array<T,N> fields (feat-20260614 D-3
743
+ // calling convention). The set semantics mirror spawn: release the
744
+ // prior slot via the SSOT helper, then alloc a fresh one sized to
745
+ // the new value, copy bytes verbatim, store slot id (+ count for
746
+ // variable). Fixed `array<T,N>` is inline — the helper short-
747
+ // circuits and writeArrayField writes directly into the row's
748
+ // stride window with no pool traffic.
749
+ this.storage.releaseManagedFieldOnRow(arch, component, this.tableRow(rec), fieldName);
750
+ this.storage.writeArrayField(
751
+ arch,
752
+ component,
753
+ this.tableRow(rec),
754
+ fieldName,
755
+ fieldType,
756
+ arrayMeta,
757
+ raw,
758
+ );
759
+ } else {
760
+ // The pre-write `releaseManagedFieldOnRow` block above already
761
+ // released the prior `'shared<T>'` rc via SharedRefStore.release;
762
+ // here we retain the new value so net rc delta is +1 / 0 / -1 per
763
+ // M4 invariant (set: -1+1=0; spawn: 0+1=+1; despawn: -1).
764
+ col.view[this.tableRow(rec)] = raw as number;
765
+ if (fieldType.startsWith('shared<') && (raw as number) !== 0) {
766
+ this.storage.retainSharedScalarHandle(raw as number, component.name, fieldName);
767
+ }
768
+ }
769
+ }
770
+ }
771
+ if (markChanged) this.markComponentChanged(entity, component);
772
+ return ok(undefined);
773
+ }
774
+
775
+ // ──────────────────────────────────────────────────────────────────────────
776
+ // Internal relationship array maintenance. Public array mutation is always
777
+ // expressed as one `world.set` payload; these helpers only implement the
778
+ // engine-owned target projection and backpointer swap-remove path.
779
+ //
780
+ // Append/remove are engine-owned relationship maintenance only.
781
+ //
782
+ // The `fieldName` parameter is typed `ArrayFieldsOf<S>` so cross-shape
783
+ // access (entity / buffer / string / scalar field names) is rejected at
784
+ // compile time -- AI users see a TS error well before any runtime path.
785
+ // ──────────────────────────────────────────────────────────────────────────
786
+
787
+ /**
788
+ * Append `value` to the variable `array<T>` field `fieldName` on `entity`.
789
+ *
790
+ * BufferPool grow is amortized O(1) via the size-class freelist (research
791
+ * Finding 5). Relationship target arrays grow byte-wise.
792
+ *
793
+ * @returns `Result<void, EcsError>` with the normal stale/component errors.
794
+ *
795
+ * The helper is called only by relationship synchronization.
796
+ */
797
+ private appendArrayElement<S extends ComponentSchema, K extends ArrayFieldsOf<S>>(
798
+ entity: EntityHandle,
799
+ component: Component<string, S>,
800
+ fieldName: K,
801
+ value: ArrayFieldElementValue<S, K>,
802
+ ): Result<void, EcsError> {
803
+ const record = this.lookupAlive(entity, 'relationship-append', component.name);
804
+ if (!record.ok) return record;
805
+ const rec = record.value;
806
+ const arch = this.graph.archetypes[rec.archetypeId];
807
+ /* istanbul ignore next -- alive record always has a valid archetype */
808
+ if (!arch) {
809
+ return err(
810
+ new StaleEntityError(entity as number, entityIndex(entity), entityGeneration(entity), {
811
+ operation: 'relationship-append',
812
+ component: component.name,
813
+ expectedGeneration: entityGeneration(entity),
814
+ actualGeneration: rec.generation,
815
+ }),
816
+ );
817
+ }
818
+ const localId = componentId(component);
819
+ const fieldCols = this.table(arch).storage.get(localId)?.fields;
820
+ if (!fieldCols) {
821
+ return err(new ComponentNotPresentError(entity as number, component.name));
822
+ }
823
+ const fieldNameStr = fieldName as string;
824
+ const col = fieldCols.get(fieldNameStr);
825
+ /* istanbul ignore next -- ArrayFieldsOf filter ensures the column exists */
826
+ if (!col) return err(new ComponentNotPresentError(entity as number, component.name));
827
+ const arrayMeta = componentDefinition(component).fields[fieldNameStr]?.arrayMeta;
828
+ /* istanbul ignore next -- ArrayFieldsOf filter guarantees array<*> */
829
+ if (arrayMeta === undefined) {
830
+ return err(new ComponentNotPresentError(entity as number, component.name));
831
+ }
832
+ const meta = TYPE_METADATA[arrayMeta.elementType];
833
+ /* istanbul ignore next -- arrayMeta.elementType is guaranteed in TYPE_METADATA */
834
+ if (!meta) return err(new ComponentNotPresentError(entity as number, component.name));
835
+ // biome-ignore lint/style/noNonNullAssertion: ManagedArrayElementType always scalar -> byteSize present
836
+ const elementBytes = meta.byteSize!;
837
+ const slotId = col.view[this.tableRow(rec)] as number;
838
+
839
+ const countCol = fieldCols.get(arrayCountColumnName(fieldNameStr));
840
+ /* istanbul ignore next -- variable arrays always allocate the count column */
841
+ if (countCol === undefined) {
842
+ return err(new ComponentNotPresentError(entity as number, component.name));
843
+ }
844
+ const count = countCol.view[this.tableRow(rec)] as number;
845
+ const newCount = count + 1;
846
+ const newByteLength = newCount * elementBytes;
847
+
848
+ let liveSlotId = slotId;
849
+ if (liveSlotId === 0) {
850
+ // Empty/unallocated slot — alloc fresh.
851
+ const allocR = this.bufferPool.alloc(newByteLength);
852
+ if (!allocR.ok) return err(allocR.error);
853
+ liveSlotId = allocR.value.id;
854
+ col.view[this.tableRow(rec)] = liveSlotId;
855
+ } else {
856
+ // A previously-allocated slot may have drained below its high-water
857
+ // mark: swap-remove (`_removeArrayElementByValue`) and `pop` only lower
858
+ // the count column, never shrink the managed buffer. When the refilled
859
+ // length still fits inside the slot's current logical length, reuse the
860
+ // buffer in place -- routing through `grow` would hit the (correct, but
861
+ // here irrelevant) shrink-not-supported guard and strand the field
862
+ // (e.g. `Children.entities` never repopulating after a full drain).
863
+ if (newByteLength > this.bufferPool.view(liveSlotId).byteLength) {
864
+ const growR = this.bufferPool.grow(liveSlotId, newByteLength);
865
+ if (!growR.ok) return err(growR.error);
866
+ }
867
+ }
868
+ const liveBytes = this.bufferPool.view(liveSlotId);
869
+ // Reinterpret the slot bytes as the element-typed view and write at the
870
+ // tail index. Entity values are stored as their u32 bit pattern.
871
+ this.storage.writeArrayElementAt(liveBytes, count, arrayMeta.elementType, value as number);
872
+ countCol.view[this.tableRow(rec)] = newCount;
873
+ this.markComponentChanged(entity, component);
874
+ return ok(undefined);
875
+ }
876
+
877
+ private ensureArrayCapacity<S extends ComponentSchema, K extends ArrayFieldsOf<S>>(
878
+ entity: EntityHandle,
879
+ component: Component<string, S>,
880
+ fieldName: K,
881
+ minimum: number,
882
+ ): Result<void, EcsError> {
883
+ const record = this.lookupAlive(entity, 'relationship-capacity', component.name);
884
+ if (!record.ok) return record;
885
+ const rec = record.value;
886
+ const arch = this.graph.archetypes[rec.archetypeId];
887
+ if (!arch) {
888
+ return err(
889
+ new StaleEntityError(entity as number, entityIndex(entity), entityGeneration(entity), {
890
+ operation: 'relationship-capacity',
891
+ component: component.name,
892
+ expectedGeneration: entityGeneration(entity),
893
+ actualGeneration: rec.generation,
894
+ }),
895
+ );
896
+ }
897
+ const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
898
+ if (!fieldCols) return err(new ComponentNotPresentError(entity as number, component.name));
899
+ const fieldNameStr = fieldName as string;
900
+ const col = fieldCols.get(fieldNameStr);
901
+ if (!col) return err(new ComponentNotPresentError(entity as number, component.name));
902
+ const arrayMeta = componentDefinition(component).fields[fieldNameStr]?.arrayMeta;
903
+ if (arrayMeta === undefined) {
904
+ return err(new ComponentNotPresentError(entity as number, component.name));
905
+ }
906
+ const meta = TYPE_METADATA[arrayMeta.elementType];
907
+ if (!meta?.byteSize) {
908
+ return err(new ComponentNotPresentError(entity as number, component.name));
909
+ }
910
+ const maximum = Math.floor(262_144 / meta.byteSize);
911
+ if (!Number.isSafeInteger(minimum) || minimum < 0 || minimum > maximum) {
912
+ return err(new ManagedBufferOutOfBoundsError(minimum, maximum));
913
+ }
914
+
915
+ const byteLength = minimum * meta.byteSize;
916
+ const slotId = col.view[this.tableRow(rec)] as number;
917
+ if (slotId === 0) {
918
+ if (minimum === 0) return ok(undefined);
919
+ const allocated = this.bufferPool.alloc(byteLength);
920
+ if (!allocated.ok) return allocated;
921
+ col.view[this.tableRow(rec)] = allocated.value.id;
922
+ return ok(undefined);
923
+ }
924
+ if (this.bufferPool.view(slotId).byteLength >= byteLength) return ok(undefined);
925
+ const grown = this.bufferPool.grow(slotId, byteLength);
926
+ return grown.ok ? ok(undefined) : grown;
927
+ }
928
+
929
+ /**
930
+ * Remove one variable-array element at a known slot. Relationship holders
931
+ * supply the slot from their backpointer, so this is O(1) and never scans
932
+ * the materialized target array.
933
+ */
934
+ private removeArrayElementAt(
935
+ entity: EntityHandle,
936
+ component: Component<string, ComponentSchema>,
937
+ fieldName: string,
938
+ slot: number,
939
+ ): Result<EntityHandle | undefined, EcsError> {
940
+ const record = this.lookupAlive(entity, 'removeArrayElementAt', component.name);
941
+ if (!record.ok) return record;
942
+ const rec = record.value;
943
+ const arch = this.graph.archetypes[rec.archetypeId];
944
+ if (!arch) return err(new ComponentNotPresentError(entity as number, component.name));
945
+ const fieldCols = this.table(arch).storage.get(componentId(component))?.fields;
946
+ if (!fieldCols) return err(new ComponentNotPresentError(entity as number, component.name));
947
+ const col = fieldCols.get(fieldName);
948
+ const arrayMeta = componentDefinition(component).fields[fieldName]?.arrayMeta;
949
+ const countCol = fieldCols.get(arrayCountColumnName(fieldName));
950
+ if (!col || !arrayMeta || arrayMeta.length !== undefined || !countCol) {
951
+ return err(new ComponentNotPresentError(entity as number, component.name));
952
+ }
953
+ const row = this.tableRow(rec);
954
+ const count = countCol.view[row] as number;
955
+ if (slot < 0 || slot >= count) return ok(undefined);
956
+ const slotId = col.view[row] as number;
957
+ if (slotId === 0) return ok(undefined);
958
+ const liveBytes = this.bufferPool.view(slotId);
959
+ const last = count - 1;
960
+ const moved =
961
+ slot === last
962
+ ? undefined
963
+ : (this.storage.readArrayElementAt(liveBytes, last, arrayMeta.elementType) as EntityHandle);
964
+ if (slot !== last) {
965
+ this.storage.writeArrayElementAt(liveBytes, slot, arrayMeta.elementType, moved as number);
966
+ }
967
+ countCol.view[row] = last;
968
+ this.markComponentChanged(entity, component);
969
+ return ok(moved);
970
+ }
971
+
972
+ // ──────────────────────────────────────────────────────────────────────────
973
+ // addComponent / removeComponent (archetype migration via edges, AC-07)
974
+ // ──────────────────────────────────────────────────────────────────────────
975
+
976
+ /**
977
+ * Add a component to an existing entity, triggering archetype migration.
978
+ *
979
+ * @returns `Result<void, EcsError>` —
980
+ * `ok(void)` on success;
981
+ * `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
982
+ * `err(ComponentAlreadyPresentError)` (`.code = 'component-already-present'`)
983
+ * if entity already has the component (E-03).
984
+ *
985
+ * @example
986
+ * ```ts
987
+ * const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
988
+ * const Velocity = defineComponent('Velocity', { dx: 'f32', dy: 'f32' });
989
+ * const world = new World();
990
+ * const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
991
+ * const r = world.addComponent(e, { component: Velocity, data: { dx: 1, dy: 0 } });
992
+ * if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
993
+ * r.unwrap();
994
+ * ```
995
+ */
996
+ addComponent<S extends ComponentSchema>(
997
+ entity: EntityHandle,
998
+ componentData: ComponentData<S>,
999
+ ): Result<void, EcsError> {
1000
+ return this._addComponentCore(entity, componentData, false);
1001
+ }
1002
+
1003
+ /**
1004
+ * Core implementation of `addComponent` with reentry guard.
1005
+ *
1006
+ * @param internal — `true` when called from relationship maintenance
1007
+ * (lazy mirror create or exclusive reparent).
1008
+ * @internal
1009
+ */
1010
+ _addComponentCore<S extends ComponentSchema>(
1011
+ entity: EntityHandle,
1012
+ componentData: ComponentData<S>,
1013
+ internal: boolean,
1014
+ ): Result<void, EcsError> {
1015
+ const record = this.lookupAlive(entity, 'addComponent', componentData.component.name);
1016
+ if (!record.ok) return record;
1017
+
1018
+ const rec = record.value;
1019
+ const srcArch = this.graph.archetypes[rec.archetypeId];
1020
+ /* istanbul ignore next -- defensive: alive record always has valid archetypeId */
1021
+ if (!srcArch) {
1022
+ return err(
1023
+ new StaleEntityError(entity as number, entityIndex(entity), entityGeneration(entity), {
1024
+ operation: 'addComponent',
1025
+ component: componentData.component.name,
1026
+ expectedGeneration: entityGeneration(entity),
1027
+ actualGeneration: rec.generation,
1028
+ }),
1029
+ );
1030
+ }
1031
+
1032
+ const preflight = this.preflightComponentData(entity, componentData);
1033
+ if (!preflight.ok) return preflight;
1034
+
1035
+ // bug-20260615: unknown-key fail-fast BEFORE archetype mutation so a
1036
+ // typo aborts cleanly without partial state (mirrors _spawnCore).
1037
+ const keyErr = validateComponentDataKeys(
1038
+ componentData.component,
1039
+ componentData.data as Record<string, unknown>,
1040
+ );
1041
+ if (keyErr !== null) {
1042
+ return err(keyErr as unknown as EcsError);
1043
+ }
1044
+ const arrayErr = validateManagedArrayValues(
1045
+ componentData.component,
1046
+ componentData.data as Record<string, unknown>,
1047
+ );
1048
+ if (arrayErr !== null) {
1049
+ return err(arrayErr as unknown as EcsError);
1050
+ }
1051
+ // feat-20260713 M2 / w9: P3 shared-field value gate (see _spawnCore). Runs
1052
+ // before archetype mutation so a mis-bound GUID aborts cleanly.
1053
+ const sharedErr = validateSharedFieldValues(
1054
+ componentData.component,
1055
+ componentData.data as Record<string, unknown>,
1056
+ );
1057
+ if (sharedErr !== null) {
1058
+ return err(sharedErr as unknown as EcsError);
1059
+ }
1060
+ const filled = fillComponentDefaults(
1061
+ componentData.component,
1062
+ componentData.data as Record<string, unknown>,
1063
+ );
1064
+ const enumErr = validateEnumFieldValues(componentData.component, filled, entity as number);
1065
+ if (enumErr !== null) {
1066
+ return err(enumErr as unknown as EcsError);
1067
+ }
1068
+
1069
+ // Check if entity already has this component (using World-local ID).
1070
+ const localId = componentId(componentData.component);
1071
+ if (srcArch.components.some((candidate) => componentId(candidate) === localId)) {
1072
+ // M2 exclusive relationship: re-adding the holder with a (possibly new)
1073
+ // target auto-reparents instead of failing (AC-12). Prune the old side
1074
+ // first (removeComponent prunes the old target), then fall through to
1075
+ // the normal add (which appends the new target). The two steps keep the
1076
+ // materialized target list consistent (AC-13);
1077
+ // removeComponent + addComponent each touch the mirror exactly once and
1078
+ // the mirror component carries no relationship of its own, so there is
1079
+ // no recursion. Reparent only fires for top-level user calls
1080
+ // (!internal); engine-internal lazy create / append
1081
+ // never re-add an existing relationship component.
1082
+ const role = relationshipRole(componentData.component as Component);
1083
+ if (role?.kind === 'source' && role.exclusive && !internal) {
1084
+ const prepared = this.prepareRelationshipInsert(
1085
+ componentData.component as Component,
1086
+ filled as Record<string, unknown>,
1087
+ );
1088
+ if (!prepared.ok) return prepared;
1089
+ const removeR = this._removeComponentCore(
1090
+ entity,
1091
+ componentData.component as Component,
1092
+ false,
1093
+ );
1094
+ if (!removeR.ok) return removeR;
1095
+ return this._addComponentCore(entity, componentData, false);
1096
+ }
1097
+ return err(new ComponentAlreadyPresentError(entity as number, componentData.component.name));
1098
+ }
1099
+
1100
+ if (!internal && relationshipRole(componentData.component as Component)?.kind === 'source') {
1101
+ const prepared = this.prepareRelationshipInsert(
1102
+ componentData.component as Component,
1103
+ filled as Record<string, unknown>,
1104
+ );
1105
+ if (!prepared.ok) return prepared;
1106
+ }
1107
+
1108
+ // Get target archetype via edge cache.
1109
+ const targetArch = getAddEdge(
1110
+ this.graph,
1111
+ srcArch,
1112
+ localId,
1113
+ componentData.component as Component,
1114
+ );
1115
+
1116
+ if (componentData.component.storage === 'sparse') {
1117
+ this.storage.moveEntityArchetype(rec, srcArch, targetArch);
1118
+ } else {
1119
+ this.storage.migrateEntity(rec, srcArch, targetArch);
1120
+ }
1121
+
1122
+ // Write the new component's data. Apply layer-2 + layer-3 silent
1123
+ // fallback so addComponent shares the SAME default-resolution path
1124
+ // as spawn / SceneAsset.instantiate (feat-20260517 / M2 / AC-04
1125
+ // research §F4 auto-symmetry; ComponentData<S>['data'] is the
1126
+ // physical bridge).
1127
+ if (componentData.component.storage === 'table') {
1128
+ this.storage.writeRow(
1129
+ targetArch,
1130
+ componentData.component,
1131
+ this.tableRow(rec),
1132
+ filled as ShapeOf<S>,
1133
+ );
1134
+ }
1135
+ this.markComponentAdded(entity, componentData.component as Component);
1136
+ // Relationship sync: append to the materialized target list.
1137
+ if (!internal && relationshipRole(componentData.component as Component)?.kind === 'source') {
1138
+ const relationshipResult = this.relationshipOnInsert(
1139
+ entity,
1140
+ componentData.component as Component,
1141
+ filled as Record<string, unknown>,
1142
+ );
1143
+ if (!relationshipResult.ok) return relationshipResult;
1144
+ }
1145
+
1146
+ this.markStructureChanged();
1147
+ return ok(undefined);
1148
+ }
1149
+
1150
+ /**
1151
+ * Remove a component from an existing entity, triggering archetype migration.
1152
+ *
1153
+ * @returns `Result<void, EcsError>` —
1154
+ * `ok(void)` on success;
1155
+ * `err(StaleEntityError)` (`.code = 'stale-entity'`) if entity is dead;
1156
+ * `err(ComponentNotPresentError)` (`.code = 'component-not-present'`)
1157
+ * if entity doesn't have the component (E-04).
1158
+ *
1159
+ * @example
1160
+ * ```ts
1161
+ * const Position = defineComponent('Position', { x: 'f32', y: 'f32' });
1162
+ * const world = new World();
1163
+ * const e = world.spawn({ component: Position, data: { x: 0, y: 0 } }).unwrap();
1164
+ * const r = world.removeComponent(e, Position);
1165
+ * if (!r.ok) { return; } // r.error.code === 'stale-entity' on dead handle
1166
+ * r.unwrap();
1167
+ * ```
1168
+ */
1169
+ removeComponent<S extends ComponentSchema>(
1170
+ entity: EntityHandle,
1171
+ component: Component<string, S>,
1172
+ ): Result<void, EcsError> {
1173
+ return this._removeComponentCore(entity, component, false);
1174
+ }
1175
+
1176
+ /**
1177
+ * Core implementation of `removeComponent` with reentry guard.
1178
+ *
1179
+ * @param internal — `true` when called from relationship maintenance
1180
+ * (exclusive reparent).
1181
+ * @internal
1182
+ */
1183
+ _removeComponentCore<S extends ComponentSchema>(
1184
+ entity: EntityHandle,
1185
+ component: Component<string, S>,
1186
+ internal: boolean,
1187
+ ): Result<void, EcsError> {
1188
+ // Essential-component hard reject (feat-20260602 / plan-strategy D-3): the
1189
+ // id=0 `Entity` component is carried by every archetype unconditionally (it
1190
+ // is the row's own packed handle) and cannot be removed. Reject before any
1191
+ // liveness lookup so the rejection is structural, not entity-state-dependent.
1192
+ if (componentId(component) === componentId(EntityComponent)) {
1193
+ return err(new RemoveEssentialComponentError(component.name));
1194
+ }
1195
+
1196
+ const record = this.lookupAlive(entity, 'removeComponent', component.name);
1197
+ if (!record.ok) return record;
1198
+
1199
+ const rec = record.value;
1200
+ const srcArch = this.graph.archetypes[rec.archetypeId];
1201
+ /* istanbul ignore next -- defensive: alive record always has valid archetypeId */
1202
+ if (!srcArch) {
1203
+ return err(
1204
+ new StaleEntityError(entity as number, entityIndex(entity), entityGeneration(entity), {
1205
+ operation: 'removeComponent',
1206
+ component: component.name,
1207
+ expectedGeneration: entityGeneration(entity),
1208
+ actualGeneration: rec.generation,
1209
+ }),
1210
+ );
1211
+ }
1212
+
1213
+ // Check if entity has this component (using World-local ID).
1214
+ const localId = componentId(component);
1215
+ if (!srcArch.components.some((candidate) => componentId(candidate) === localId)) {
1216
+ return err(new ComponentNotPresentError(entity as number, component.name));
1217
+ }
1218
+
1219
+ // Capture the old relationship value before column removal so the
1220
+ // materialized target list can be pruned.
1221
+ const role = relationshipRole(component as Component);
1222
+ const needsOldValue = role?.kind === 'source' && !internal;
1223
+ if (needsOldValue) {
1224
+ const oldValue = this.storage.readRow(
1225
+ srcArch,
1226
+ component as Component,
1227
+ this.tableRow(rec),
1228
+ ) as Record<string, unknown>;
1229
+ // Relationship sync: prune the holder from the target's materialized list.
1230
+ if (role?.kind === 'source' && !internal) {
1231
+ const relation = this.relationshipOnRemove(entity, component as Component, oldValue);
1232
+ if (!relation.ok) return relation;
1233
+ }
1234
+ }
1235
+
1236
+ // M1 release loop (removeComponent path): release every `ref<T>` field
1237
+ // on the component being removed before migration drops the row.
1238
+ if (component.storage === 'table') {
1239
+ this.storage.releaseManagedRefsOnRow(srcArch, component as Component, this.tableRow(rec));
1240
+ }
1241
+
1242
+ // Get target archetype via edge cache.
1243
+ const targetArch = getRemoveEdge(this.graph, srcArch, localId);
1244
+
1245
+ if (component.storage === 'sparse') {
1246
+ this.storage.moveEntityArchetype(rec, srcArch, targetArch);
1247
+ const set = this.graph.sparseTags.get(componentId(component));
1248
+ if (set !== undefined) removeSparseTag(set, entity);
1249
+ } else {
1250
+ this.storage.migrateEntity(rec, srcArch, targetArch);
1251
+ }
1252
+ this.state.removeComponentChange(entity, componentId(component));
1253
+ this.markStructureChanged();
1254
+ return ok(undefined);
1255
+ }
1256
+
1257
+ // ──────────────────────────────────────────────────────────────────────────
1258
+ // Internal — deferred command support (CommandBuffer interface)
1259
+ // ──────────────────────────────────────────────────────────────────────────
1260
+
1261
+ /**
1262
+ * @internal Allocate a pending entity for deferred spawn.
1263
+ * Returns an Entity handle. The entity is "pending" because
1264
+ * archetypeId === -1 (set by allocateIndex); no separate flag needed.
1265
+ */
1266
+ _allocatePendingEntity(): EntityHandle {
1267
+ const indexSlot = this.allocateIndex();
1268
+ // biome-ignore lint/style/noNonNullAssertion: allocateIndex guarantees a valid slot with an initialized record
1269
+ return encodeEntity(indexSlot, this.records[indexSlot]!.generation);
1270
+ }
1271
+
1272
+ /**
1273
+ * Return a deferred-spawn reservation to the free-list without publishing a
1274
+ * row or advancing an epoch. CommandBuffer.abort is the sole caller; a
1275
+ * materialized entity is intentionally left untouched so an unexpected
1276
+ * post-write failure poisons the World instead of attempting an unsafe undo.
1277
+ */
1278
+ _cancelPendingEntity(entity: EntityHandle): void {
1279
+ const slot = entityIndex(entity);
1280
+ const record = this.records[slot];
1281
+ if (record === undefined || record.generation !== entityGeneration(entity)) return;
1282
+ if (record.archetypeId !== -1 || record.archetypeRow !== -1) return;
1283
+ record.generation += 1;
1284
+ if (!isRetiredSlot(record.generation)) this.freeIndices.push(slot);
1285
+ }
1286
+
1287
+ /**
1288
+ * @internal Materialize a pending entity: actually place it into an archetype.
1289
+ * Idempotent: a record with archetypeId !== -1 is already materialized.
1290
+ */
1291
+ _materializePendingEntity(
1292
+ entity: EntityHandle,
1293
+ componentDatas: ComponentData[],
1294
+ ): Result<void, EcsError> {
1295
+ const slot = entityIndex(entity);
1296
+ const record = this.records[slot];
1297
+ if (!record || record.archetypeId !== -1) return ok(undefined);
1298
+
1299
+ // Find or create target archetype (using World-local IDs).
1300
+ const componentIds = componentDatas.map((cd) => componentId(cd.component));
1301
+ const components = componentDatas.map((cd) => cd.component);
1302
+ const arch = getOrCreateArchetype(this.graph, componentIds, components);
1303
+
1304
+ // Append entity row.
1305
+ const table = this.table(arch);
1306
+ const tableRow = appendTableRow(table, entity);
1307
+ const archetypeRow = appendArchetypeRow(arch, tableRow);
1308
+ record.archetypeId = arch.id;
1309
+ record.archetypeRow = archetypeRow;
1310
+
1311
+ // Write initial data. Apply layer-2 + layer-3 silent fallback so
1312
+ // deferred-spawn (Commands.spawn) shares the SAME default-resolution
1313
+ // path as the synchronous `world.spawn` / `addComponent` /
1314
+ // SceneAsset.instantiate (feat-20260517 / M2 / AC-04 + AC-09).
1315
+ this.state.markComponentsAdded(entity, [
1316
+ componentId(EntityComponent),
1317
+ ...componentDatas.map((cd) => componentId(cd.component)),
1318
+ ]);
1319
+ for (const cd of componentDatas) {
1320
+ const filled = fillComponentDefaults(cd.component, cd.data as Record<string, unknown>);
1321
+ this.storage.writeRow(arch, cd.component, tableRow, filled as ShapeOf<ComponentSchema>);
1322
+ }
1323
+
1324
+ // Essential id=0 `Entity` column write (feat-20260602 / plan-strategy D-3),
1325
+ // mirroring the synchronous `spawn` path: the deferred handle was minted at
1326
+ // `_allocatePendingEntity` time and is passed in here.
1327
+ this.storage.writeEntitySelf(arch, tableRow, entity);
1328
+
1329
+ // Publish relationship targets after all rows are written.
1330
+ for (const cd of componentDatas) {
1331
+ if (relationshipRole(cd.component as Component)?.kind === 'source') {
1332
+ const filled = fillComponentDefaults(cd.component, cd.data as Record<string, unknown>);
1333
+ const relationshipResult = this.relationshipOnInsert(
1334
+ entity,
1335
+ cd.component as Component,
1336
+ filled as Record<string, unknown>,
1337
+ );
1338
+ if (!relationshipResult.ok) {
1339
+ return relationshipResult;
1340
+ }
1341
+ }
1342
+ }
1343
+ this.markStructureChanged();
1344
+ return ok(undefined);
1345
+ }
1346
+
1347
+ // ──────────────────────────────────────────────────────────────────────────
1348
+ // Internal — entity index allocation
1349
+ // ──────────────────────────────────────────────────────────────────────────
1350
+
1351
+ allocateIndex(): number {
1352
+ const recycled = this.freeIndices.pop();
1353
+ if (recycled !== undefined) {
1354
+ return recycled;
1355
+ }
1356
+ const slot = this.records.length;
1357
+ if (slot > ENTITY_MAX_INDEX) {
1358
+ throw new EntityIndexOverflowError(slot);
1359
+ }
1360
+ this.records.push({ generation: 0, archetypeId: -1, archetypeRow: -1 });
1361
+ return slot;
1362
+ }
1363
+
1364
+ /**
1365
+ * Single liveness predicate (feat-20260602 / plan-strategy D-4): a slot is
1366
+ * live for a given handle generation iff the record exists, its generation
1367
+ * still matches the handle (despawn bumps generation, so a stale or recycled
1368
+ * handle fails here), and the slot is materialized into an archetype
1369
+ * (archetypeId !== -1). Replaces the former `record.alive && record.generation
1370
+ * === gen` conjunction and the intermediate `!record.pending` clause.
1371
+ */
1372
+ recordIsLive(record: EntityRecord | undefined, gen: number): record is EntityRecord {
1373
+ return record !== undefined && record.generation === gen && record.archetypeId !== -1;
1374
+ }
1375
+
1376
+ lookupAlive(
1377
+ entity: EntityHandle,
1378
+ operation: string,
1379
+ component?: string,
1380
+ ): Result<EntityRecord, EcsError> {
1381
+ const slot = entityIndex(entity);
1382
+ const gen = entityGeneration(entity);
1383
+ const record = this.records[slot];
1384
+ if (!this.recordIsLive(record, gen)) {
1385
+ return err(
1386
+ new StaleEntityError(entity as number, slot, gen, {
1387
+ operation,
1388
+ ...(component !== undefined ? { component } : {}),
1389
+ expectedGeneration: gen,
1390
+ actualGeneration: this.records[slot]?.generation ?? -1,
1391
+ }),
1392
+ );
1393
+ }
1394
+ return ok(record);
1395
+ }
1396
+
1397
+ readRow<S extends ComponentSchema>(
1398
+ arch: Archetype,
1399
+ component: Component<string, S>,
1400
+ row: number,
1401
+ ): ShapeOf<S> {
1402
+ return this.storage.readRow(arch, component, row);
1403
+ }
1404
+
1405
+ writeEntitySelf(arch: Archetype, row: number, handle: EntityHandle): void {
1406
+ this.storage.writeEntitySelf(arch, row, handle);
1407
+ }
1408
+
1409
+ writeRow<S extends ComponentSchema>(
1410
+ arch: Archetype,
1411
+ component: Component<string, S>,
1412
+ row: number,
1413
+ value: ShapeOf<S>,
1414
+ ): void {
1415
+ this.storage.writeRow(arch, component, row, value);
1416
+ }
1417
+
1418
+ releaseManagedRefsOnRow(arch: Archetype, component: Component, row: number): void {
1419
+ this.storage.releaseManagedRefsOnRow(arch, component, row);
1420
+ }
1421
+ }