@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,1268 @@
1
+ // @forgeax/engine-ecs — Component schema + opaque token.
2
+ //
3
+ // `defineComponent(name, fields, options?)` returns a frozen token carrying
4
+ // only the three runtime facts needed by callers:
5
+ // - `.name`: component name string
6
+ // - `.fields`: frozen field descriptors (the schema SSOT)
7
+ // - `.storage`: table or sparse placement
8
+ // Numeric identity, flat schema projections, and default maps live in the ECS
9
+ // owner tables below rather than on the public token.
10
+ //
11
+ // ComponentId is used by archetype storage, bitmask matching, and edges cache.
12
+
13
+ import { err, type Handle, ok, type Result } from '@forgeax/engine-types';
14
+ import {
15
+ assertComponentStorage,
16
+ deepFreeze,
17
+ registerComponentDefinition,
18
+ } from './component-schema';
19
+ import type { EntityHandle } from './entity-handle';
20
+ import {
21
+ ManagedArrayElementTypeNotAllowedError,
22
+ SchemaUnsupportedFieldError,
23
+ SparseStorageRequiresTagError,
24
+ } from './errors';
25
+ import type { ManagedColumnReader } from './storage/column';
26
+
27
+ // The internal package subpath reuses this owner module so the source budget
28
+ // does not grow a second forwarding module. Root exports remain curated in
29
+ // index.ts; this re-export is only reached through `@forgeax/engine-ecs/internal`.
30
+ export { componentDefinition } from './component-schema';
31
+
32
+ // ────────────────────────────────────────────────────────────────────────────
33
+ // Field types — schema vocab keywords (AC-01).
34
+ //
35
+ // Two-tier vocabulary:
36
+ //
37
+ // 1. Legacy scalar set: 11 keywords backed by TypedArray storage. Concrete
38
+ // byte-sizes + TypedArray constructors are internal constants consumed by
39
+ // `scalarRow()` to build TYPE_METADATA rows (see M4 §FIELD_SIZE_BYTES / VIEW_CTORS).
40
+ //
41
+ // 2. Schema-vocab keywords: 7 template-literal patterns expressing
42
+ // ECS-managed types whose storage is owned by separate subsystems:
43
+ // * `buffer:<bytes>` — fixed-byte managed Uint8Array, stored by BufferPool
44
+ // * `ref<T>` — managed Handle<T,'unique'>, released by UniqueRefStore
45
+ // * `shared<T>` — rc-tracked Handle<T,'shared'>, lifecycle owned by SharedRefStore
46
+ // * `entity` — Entity reference (Entity | null)
47
+ // * `string` — utf-8 string payload, allocated as a managed handle via UniqueRefStore
48
+ // * `array<T,N>` — fixed-capacity typed view; elements inline in stride-N column (feat-20260602)
49
+ // * `array<T>` — variable-capacity typed view over BufferPool slot bytes
50
+ //
51
+ // The retired `array<entity>` predecessor (closed out by this feat) is no
52
+ // longer a valid schema field type — the union has narrowed it out.
53
+ // ────────────────────────────────────────────────────────────────────────────
54
+
55
+ /** Bytes per element for each scalar field type. */
56
+ const FIELD_SIZE_BYTES = {
57
+ f32: 4,
58
+ f64: 8,
59
+ i32: 4,
60
+ u32: 4,
61
+ i16: 2,
62
+ u16: 2,
63
+ i8: 1,
64
+ u8: 1,
65
+ bool: 1,
66
+ enum: 4,
67
+ ref: 4,
68
+ } as const;
69
+
70
+ /** Numeric scalar field types backed by TypedArray storage (legacy tier). */
71
+ export type ScalarFieldType = keyof typeof FIELD_SIZE_BYTES;
72
+
73
+ /**
74
+ * Legal element-type whitelist for the `array<T,N>` / `array<T>` vocab
75
+ * keywords (AC-03). T must be a scalar field type, `entity`, or a
76
+ * `shared\<X\>` template with a non-empty tag; reference / buffer / nested
77
+ * array element types are forbidden (OOS-08 / OOS-03).
78
+ *
79
+ * feat-20260614 M5 / w23: the historical `handle\<X\>` element family was
80
+ * deleted in favor of `shared\<X\>` (rc-tracked, lifecycle owned by
81
+ * SharedRefStore). The `MANAGED_ARRAY_ELEMENT_TYPES` Set remains
82
+ * static-scalar + entity only (D-8); dynamic `shared\<X\>` templates are
83
+ * validated by `isValidArrayElementType` at parse time.
84
+ *
85
+ * Legal: every member of `ScalarFieldType` plus `entity` plus
86
+ * `shared\<X\>` (non-empty tag). The `ref` legacy scalar keyword (a u32
87
+ * column placeholder) is in the whitelist; the parametric `unique<T>` /
88
+ * `shared<T>` scalars are rejected as array element types by AC-03.
89
+ */
90
+ export type ManagedArrayElementType = ScalarFieldType | 'entity' | `shared<${string}>`;
91
+
92
+ /**
93
+ * Schema-vocab keywords beyond the legacy scalar tier (AC-01).
94
+ *
95
+ * Each pattern is a template-literal type so a literal schema like
96
+ * `{ mat: 'unique<MaterialAsset>' }` types the value as
97
+ * `Handle<'MaterialAsset','unique'>` end-to-end. Runtime acceptance is
98
+ * gated by the internal `isSchemaVocabKeyword` check — the SSOT for parser fail-fast.
99
+ *
100
+ * The legacy `'buffer:<N>'` literal is retired one-cut by
101
+ * feat-20260515-buffer-array-vocab-collapse w4: replaced by the
102
+ * angle-bracket generic shapes `'buffer'` (variable byte slot) and
103
+ * `'buffer<N>'` (fixed byte slot). With `'array<T>'` / `'array<T, N>'` they
104
+ * form a 4-keyword closed surface across two orthogonal axes (element-type
105
+ * x capacity contract).
106
+ */
107
+ export type SchemaVocabKeyword =
108
+ | 'string'
109
+ | 'buffer'
110
+ | `buffer<${number}>`
111
+ | `unique<${string}>`
112
+ | `shared<${string}>`
113
+ | 'entity'
114
+ | `array<${ManagedArrayElementType}, ${number}>`
115
+ | `array<${ManagedArrayElementType}>`;
116
+
117
+ /**
118
+ * Closed union of every keyword `defineComponent` accepts for a schema field.
119
+ * Combines the legacy scalar tier with the schema-vocab tier.
120
+ *
121
+ * `ComponentSchema` is keyed against this union; `defineComponent` rejects
122
+ * any field value not satisfying it (compile-time) or matching it
123
+ * (runtime).
124
+ */
125
+ export type SchemaFieldType = ScalarFieldType | SchemaVocabKeyword;
126
+
127
+ /**
128
+ * Producer-owned semantic shape tags for authoring/schema consumers.
129
+ *
130
+ * The ECS storage vocabulary remains the source of truth for bytes and
131
+ * runtime values. These tags capture the semantic shape that storage alone
132
+ * cannot express (for example an optional entity reference or a nested
133
+ * unique payload). The tag is deliberately closed so downstream consumers
134
+ * can exhaustively handle the representative field-shape vocabulary without
135
+ * creating a second component registry.
136
+ */
137
+ export type FieldShapeKind =
138
+ | 'scalar'
139
+ | 'boolean'
140
+ | 'enum'
141
+ | 'vector'
142
+ | 'quaternion'
143
+ | 'optional'
144
+ | 'nested'
145
+ | 'array'
146
+ | 'asset-ref';
147
+
148
+ /**
149
+ * Normalize any field-type keyword to its TYPE_METADATA key.
150
+ *
151
+ * The 11 legacy scalars round-trip their own key. The 6 vocab families normalize
152
+ * their parametric shapes to the family key:
153
+ * - `unique<T>` / `shared<T>` — strip `<T>` → `'ref'` / `'shared'`
154
+ * - `buffer<N>` — strip `<N>` → `'buffer'`
155
+ * - `array<T>` / `array<T,N>` — strip `<...>` → `'array'`
156
+ * - `entity` / `string` / `buffer` are identity.
157
+ *
158
+ * Returns `null` for an unrecognised keyword so callers can skip column
159
+ * allocation (same semantics as the retired `storageFieldType`).
160
+ */
161
+ export function fieldTypeToMetaKey(fieldType: string): string | null {
162
+ if (fieldType === 'entity' || fieldType === 'string' || fieldType === 'buffer') {
163
+ return fieldType;
164
+ }
165
+ if (fieldType.startsWith('unique<') && fieldType.endsWith('>')) return 'ref';
166
+ if (fieldType.startsWith('shared<') && fieldType.endsWith('>')) return 'shared';
167
+ if (fieldType.startsWith('buffer<') && fieldType.endsWith('>')) return 'buffer';
168
+ if (fieldType.startsWith('array<') && fieldType.endsWith('>')) return 'array';
169
+ // Legacy scalar — the 11 types are keys in TYPE_METADATA.
170
+ if (TYPE_METADATA[fieldType] !== undefined) return fieldType;
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * `true` when the schema field type is a managed-store slot - i.e. should be
176
+ * routed through `UniqueRefStore` (or `SharedRefStore` for `'shared<T>'`)
177
+ * for alloc / resolve / release. Derived from TYPE_METADATA[].isManaged
178
+ * column (feat-20260611-ecs-storage-naming-ssot D-3).
179
+ *
180
+ * Naming note (D-6 whitelist): `managed = ECS-tracked`. The prefix here is
181
+ * about column-side lifecycle ownership (the ECS releases the slot on
182
+ * despawn / overwrite), not the retired `'managed' | 'unmanaged'` Handle
183
+ * brand. Both `'unique<T>'` and `'shared<T>'` schema fields satisfy
184
+ * `isManagedField` because both are ECS-tracked; the dispatcher in
185
+ * `releaseManagedFieldOnRow` picks the right store per field type.
186
+ */
187
+ export function isManagedField(fieldType: string): boolean {
188
+ return TYPE_METADATA[fieldTypeToMetaKey(fieldType) ?? '']?.isManaged ?? false;
189
+ }
190
+
191
+ /**
192
+ * `true` when the schema field type is a managed-buffer slot - i.e. should be
193
+ * released by the M2 BufferPool release loop. Derived from
194
+ * TYPE_METADATA[].isBuffer column (feat-20260611-ecs-storage-naming-ssot D-3/D-4).
195
+ *
196
+ * D-4 semantic widening accepted: `buffer<abc>` resolves to metaKey 'buffer'
197
+ * (isBuffer=true) while the old regex-based impl rejected the non-integer N.
198
+ * This is a dead path — `defineComponent` rejects `buffer<abc>` via
199
+ * `isSchemaVocabKeyword` before the predicate fires.
200
+ *
201
+ * Naming note (D-6 whitelist): `managed = ECS-tracked`. Same semantic as
202
+ * `isManagedField` — the variable `'buffer'` keyword is one whose
203
+ * BufferPool slot the ECS releases at despawn / overwrite time.
204
+ */
205
+ export function isManagedBufferField(fieldType: string): boolean {
206
+ return TYPE_METADATA[fieldTypeToMetaKey(fieldType) ?? '']?.isBuffer ?? false;
207
+ }
208
+
209
+ /**
210
+ * `true` when the schema field type is the single-entity reference keyword
211
+ * `'entity'`. Derived from TYPE_METADATA[].isEntityRef column
212
+ * (feat-20260611-ecs-storage-naming-ssot D-3).
213
+ */
214
+ export function isEntityField(fieldType: string): boolean {
215
+ return TYPE_METADATA[fieldTypeToMetaKey(fieldType) ?? '']?.isEntityRef ?? false;
216
+ }
217
+
218
+ /**
219
+ * `true` when the schema field type is an `array<T,N>` / `array<T>` vocab
220
+ * keyword. Derived from TYPE_METADATA[].isArray column
221
+ * (feat-20260611-ecs-storage-naming-ssot D-3).
222
+ *
223
+ * Naming note (D-6 whitelist): `managed = ECS-tracked`. Variable
224
+ * `array<T>` storage routes through BufferPool (slot lifecycle owned by
225
+ * the ECS); fixed `array<T,N>` is inline stride-N and has no separate
226
+ * slot to release, but both share this predicate as they share the
227
+ * `'array'` meta key.
228
+ */
229
+ export function isManagedArrayField(fieldType: string): boolean {
230
+ return TYPE_METADATA[fieldTypeToMetaKey(fieldType) ?? '']?.isArray ?? false;
231
+ }
232
+
233
+ /**
234
+ * Set of legal element types for the `array<T,N>` / `array<T>` keywords
235
+ * (AC-03). Runtime mirror of `ManagedArrayElementType`.
236
+ *
237
+ * Naming note (D-6 whitelist): `MANAGED_ARRAY_ELEMENT_TYPES` keeps the
238
+ * `MANAGED` prefix because `managed = ECS-tracked` here — the Set is the
239
+ * static-whitelist arm of `isValidArrayElementType`, which gates which
240
+ * element types the ECS array dispatch knows how to retain / release.
241
+ * The `'shared<X>'` template family rides the `startsWith('shared<')`
242
+ * special case (D-8) rather than living in this Set.
243
+ */
244
+ export const MANAGED_ARRAY_ELEMENT_TYPES: ReadonlySet<ManagedArrayElementType> =
245
+ new Set<ManagedArrayElementType>([
246
+ 'f32',
247
+ 'f64',
248
+ 'i32',
249
+ 'u32',
250
+ 'i16',
251
+ 'u16',
252
+ 'i8',
253
+ 'u8',
254
+ 'bool',
255
+ 'enum',
256
+ 'ref',
257
+ 'entity',
258
+ ]);
259
+
260
+ /**
261
+ * Return `true` when `elementType` is a legal array element type
262
+ * (static-whitelist scalar | entity, or a `shared\<X\>` template with a
263
+ * non-empty tag). The empty-tag form `shared\<\>` is rejected
264
+ * (plan-strategy §2 D-1 / R-NEW-1).
265
+ *
266
+ * @internal
267
+ */
268
+ function isValidArrayElementType(elementType: string): elementType is ManagedArrayElementType {
269
+ if (MANAGED_ARRAY_ELEMENT_TYPES.has(elementType as ManagedArrayElementType)) return true;
270
+ // feat-20260614 D-8: `shared<X>` is a legal element-type via the
271
+ // startsWith special case; `MANAGED_ARRAY_ELEMENT_TYPES` Set deliberately
272
+ // does NOT carry a `'shared'` entry (D-8 keeps the static-whitelist Set
273
+ // free of the new family; runtime validation through the special case
274
+ // here pairs with the independent `'shared'` TYPE_METADATA row that
275
+ // routes element retain/release semantics in M4).
276
+ if (elementType.startsWith('shared<') && elementType.endsWith('>') && elementType.length > 9)
277
+ return true;
278
+ return false;
279
+ }
280
+
281
+ /**
282
+ * Parse an `array<T,N>` / `array<T>` schema string into its element type and
283
+ * optional fixed length. Returns `null` if the string is not a managed-array
284
+ * keyword or its element type is not in the whitelist (AC-03 runtime
285
+ * fail-safe).
286
+ *
287
+ * Examples:
288
+ * parseManagedArraySchema('array<entity>') => { elementType: 'entity', length: undefined }
289
+ * parseManagedArraySchema('array<f32, 16>') => { elementType: 'f32', length: 16 }
290
+ * parseManagedArraySchema('array<shared<MaterialAsset>>') => { elementType: 'shared<MaterialAsset>', length: undefined }
291
+ * parseManagedArraySchema('array<shared<>>') => null (empty tag rejection)
292
+ * parseManagedArraySchema('array<unique<X>>') => null (illegal element)
293
+ * parseManagedArraySchema('array<array<f32,4>>') => null (nested rejected)
294
+ *
295
+ * Naming note (D-6 whitelist): `parseManagedArraySchema` keeps the
296
+ * `Managed` infix because `managed = ECS-tracked` — every legal output
297
+ * shape this parser returns is one whose lifecycle the ECS knows how to
298
+ * retain / release on overwrite, despawn, or archetype migration.
299
+ */
300
+ export function parseManagedArraySchema(
301
+ fieldType: string,
302
+ ): { readonly elementType: ManagedArrayElementType; readonly length: number | undefined } | null {
303
+ if (!fieldType.startsWith('array<') || !fieldType.endsWith('>')) return null;
304
+ const inner = fieldType.slice(6, -1);
305
+ const commaIdx = inner.indexOf(',');
306
+ if (commaIdx === -1) {
307
+ // Variable-capacity: inner must be a bare element-type keyword or
308
+ // handle<X> template.
309
+ if (!isValidArrayElementType(inner)) return null;
310
+ return { elementType: inner as ManagedArrayElementType, length: undefined };
311
+ }
312
+ // Fixed-capacity: split at first comma; element-type before, integer length
313
+ // after. Reject any further '<' / ':' / ',' to keep the form unambiguous.
314
+ const head = inner.slice(0, commaIdx).trim();
315
+ const tail = inner.slice(commaIdx + 1).trim();
316
+ if (!isValidArrayElementType(head)) return null;
317
+ if (!/^[1-9]\d*$/.test(tail)) return null;
318
+ return { elementType: head as ManagedArrayElementType, length: Number.parseInt(tail, 10) };
319
+ }
320
+
321
+ /**
322
+ * Parse the byte count out of a `buffer<N>` schema keyword. Returns NaN if
323
+ * the input does not match the keyword pattern - callers that already gated
324
+ * via `isManagedBufferField` get a guaranteed-positive integer for the
325
+ * fixed-byte form. The bare `'buffer'` keyword (variable byte capacity)
326
+ * returns NaN and callers must check `fieldType === 'buffer'` separately.
327
+ */
328
+ export function bufferFieldByteLength(fieldType: string): number {
329
+ if (!fieldType.startsWith('buffer<') || !fieldType.endsWith('>')) return Number.NaN;
330
+ const tail = fieldType.slice(7, -1);
331
+ if (!/^[1-9]\d*$/.test(tail)) return Number.NaN;
332
+ return Number.parseInt(tail, 10);
333
+ }
334
+
335
+ /**
336
+ * Runtime check for a schema-vocab keyword (the tier-2 surface).
337
+ *
338
+ * Pure-function regex match — kept off the hot path; only invoked by
339
+ * `defineComponent` once per field at registration time. The match patterns
340
+ * are the runtime mirror of `SchemaVocabKeyword` template literals.
341
+ *
342
+ * - `'string'` is exact-match (bare literal, no `<>`).
343
+ * - `'buffer'` is exact-match (variable-byte capacity).
344
+ * - `buffer<N>` requires `N` to be a positive base-10 integer (`/^[1-9]\d*$/`).
345
+ * Forms like `buffer<abc>` / `buffer<0>` / `buffer<>` are rejected.
346
+ * - `unique<T>` / `shared<T>` require a non-empty target tag (`/^\w+$/`).
347
+ * - `entity` is exact-match.
348
+ * - `array<T,N>` / `array<T>` accept only the whitelist element types
349
+ * (`MANAGED_ARRAY_ELEMENT_TYPES`); illegal inner types fall through and
350
+ * the caller surfaces `managed-array-element-type-not-allowed`.
351
+ */
352
+ export function isSchemaVocabKeyword(s: string): s is SchemaVocabKeyword {
353
+ if (s === 'string') return true;
354
+ if (s === 'entity') return true;
355
+ if (s === 'buffer') return true;
356
+ if (s.startsWith('buffer<') && s.endsWith('>')) {
357
+ const tail = s.slice(7, -1);
358
+ return /^[1-9]\d*$/.test(tail);
359
+ }
360
+ if (s.startsWith('unique<') && s.endsWith('>')) {
361
+ return /^\w+$/.test(s.slice(7, -1));
362
+ }
363
+ if (s.startsWith('shared<') && s.endsWith('>')) {
364
+ return /^\w+$/.test(s.slice(7, -1));
365
+ }
366
+ if (s.startsWith('array<') && s.endsWith('>')) {
367
+ return parseManagedArraySchema(s) !== null;
368
+ }
369
+ return false;
370
+ }
371
+
372
+ /**
373
+ * JS value-shape per managed-array element type. `entity` maps to `Entity`
374
+ * (branded number), every scalar maps to `number` (bool is stored as a 0/1
375
+ * byte and read back as 0 or 1).
376
+ */
377
+ export type ManagedArrayElementValue<T extends ManagedArrayElementType> = T extends 'entity'
378
+ ? EntityHandle
379
+ : number;
380
+
381
+ /**
382
+ * Maps each field-type keyword to the JS value type read/written by it.
383
+ *
384
+ * Tier-1 (legacy scalars) widens to `boolean | number`; tier-2 (schema-vocab)
385
+ * resolves to the corresponding handle / entity / buffer / array / string
386
+ * shape via the `infer T` template-literal extraction pattern. Conditional
387
+ * types resolve top-down --- the `'string'` arm sits BEFORE the array<...> /
388
+ * `buffer<N>` arms so the precise literal wins template-literal resolution
389
+ * (R-P5: prevents `'string'` from being shadowed by a wider template-literal
390
+ * pattern). The fixed-capacity `array<T,N>` arm matches before the
391
+ * variable-capacity `array<T>` arm by the same rule.
392
+ *
393
+ * The 4 buffer/array keywords (`'buffer'` / `'buffer<N>'` / `'array<T>'` /
394
+ * `'array<T, N>'`) all resolve directly to a concrete TypedArray (or
395
+ * Uint8Array for the byte-only buffer family). The materialised value is a
396
+ * read-only snapshot: for fixed `buffer<N>` / `array<T,N>` it aliases the
397
+ * inline column buffer (feat-20260602); for variable `buffer` / `array<T>`
398
+ * it aliases the BufferPool slot bytes (plan-strategy §2.2 D-R3 contract).
399
+ * Mutation flows through `world.set` / `world.push` / `world.pop`
400
+ * not direct assignment to the returned TypedArray.
401
+ *
402
+ * The `'string'` arm resolves to a native JS `string` (D-R1 / AC-13): the
403
+ * dispatch routes the column u32 through `UniqueRefStore.resolve(handle)`
404
+ * which returns the immutable string payload by reference.
405
+ */
406
+ export type FieldValueType<T extends SchemaFieldType> = T extends 'bool'
407
+ ? boolean
408
+ : T extends 'entity'
409
+ ? EntityHandle | null
410
+ : T extends 'string'
411
+ ? string
412
+ : T extends 'buffer'
413
+ ? Uint8Array
414
+ : T extends `buffer<${number}>`
415
+ ? Uint8Array
416
+ : T extends `array<shared<${infer Target}>, ${number}>`
417
+ ? readonly Handle<Target, 'shared'>[]
418
+ : T extends `array<shared<${infer Target}>>`
419
+ ? readonly Handle<Target, 'shared'>[]
420
+ : T extends `array<${infer Elem extends ManagedArrayElementType}, ${number}>`
421
+ ? TypedArrayFor<Elem extends 'entity' ? 'u32' : Elem>
422
+ : T extends `array<${infer Elem extends ManagedArrayElementType}>`
423
+ ? TypedArrayFor<Elem extends 'entity' ? 'u32' : Elem>
424
+ : T extends `unique<${infer Target}>`
425
+ ? Handle<Target, 'unique'>
426
+ : T extends `shared<${infer Target}>`
427
+ ? Handle<Target, 'shared'>
428
+ : T extends ScalarFieldType
429
+ ? number
430
+ : never;
431
+
432
+ /**
433
+ * Input-side counterpart of {@link FieldValueType} for write paths
434
+ * (`world.spawn` / `world.addComponent` / `world.set`).
435
+ *
436
+ * Asymmetric on `array<scalar, N>` / `array<scalar>` ONLY: the read side
437
+ * surfaces zero-copy `Float32Array` / `Uint32Array` / etc views; the write
438
+ * side ALSO accepts `readonly number[]` because writeArrayField copies bytes
439
+ * verbatim from either shape (TypedArray subarray() OR per-element pack via
440
+ * DataView). Plain literals like `times: [0.5]` reach the same code path
441
+ * with no Float32Array wrapper boilerplate at the call site, and short
442
+ * prefixes pad the row tail with zero (writeArrayField D-3 contract).
443
+ *
444
+ * Asymmetric on `buffer` / `buffer<N>`: the read side returns `Uint8Array`,
445
+ * but the write side accepts any `AllowSharedBufferSource` (Float32Array /
446
+ * ArrayBuffer / Uint8Array / any TypedArray). The ECS buffer-write ingestion
447
+ * point (`World.writeRow` / `World.set`) normalizes any view to `Uint8Array`
448
+ * over its raw bytes before storing (feat-20260621 V2 / AC-A4). This lets AI
449
+ * users write typed param payloads directly, e.g.
450
+ * `world.set(e, PostProcessParams, { data: Float32Array.of(exposure,0,0,0) })`,
451
+ * without manual byte-reinterpret boilerplate at the call site.
452
+ *
453
+ * Every other arm matches FieldValueType verbatim (no widening): handles
454
+ * are already arrays-of-handle, scalars stay number, etc.
455
+ */
456
+ export type FieldInputType<T extends SchemaFieldType> = T extends 'bool'
457
+ ? boolean
458
+ : T extends 'entity'
459
+ ? EntityHandle | null
460
+ : T extends 'string'
461
+ ? string
462
+ : T extends 'buffer'
463
+ ? AllowSharedBufferSource
464
+ : T extends `buffer<${number}>`
465
+ ? AllowSharedBufferSource
466
+ : T extends `array<shared<${infer Target}>, ${number}>`
467
+ ? readonly Handle<Target, 'shared'>[]
468
+ : T extends `array<shared<${infer Target}>>`
469
+ ? readonly Handle<Target, 'shared'>[]
470
+ : T extends `array<${infer Elem extends ManagedArrayElementType}, ${number}>`
471
+ ? TypedArrayFor<Elem extends 'entity' ? 'u32' : Elem> | readonly number[]
472
+ : T extends `array<${infer Elem extends ManagedArrayElementType}>`
473
+ ? TypedArrayFor<Elem extends 'entity' ? 'u32' : Elem> | readonly number[]
474
+ : T extends `unique<${infer Target}>`
475
+ ? Handle<Target, 'unique'>
476
+ : T extends `shared<${infer Target}>`
477
+ ? Handle<Target, 'shared'>
478
+ : T extends ScalarFieldType
479
+ ? number
480
+ : never;
481
+
482
+ /**
483
+ * Maps a SchemaFieldType to its zero-copy query-column view type.
484
+ *
485
+ * Three storage shapes share the keyword space:
486
+ *
487
+ * 1. Scalar / fixed-inline columns -- the column buffer is the data, written
488
+ * in place. The bundle entry is a concrete writable TypedArray of the
489
+ * correct ctor (`f32` -> `Float32Array`, `'buffer<N>'` -> `Uint8Array`,
490
+ * `'array<T,N>'` -> the T-typed array). Direct index assignment is
491
+ * fine -- the column owns the bytes.
492
+ *
493
+ * 2. `shared\<X\>` (rc-tracked AssetRegistry reference) -- the column carries
494
+ * a u32 handle id; SharedRefStore owns the rc lifecycle. The bundle
495
+ * entry is a `ManagedColumnReader<T>` (D-4 / D-7) -- read-only, walk
496
+ * via `.get(i)`. Consumers route through `assets.get(handle)` to
497
+ * materialise the asset payload.
498
+ *
499
+ * 3. The 4 managed-vocab keywords -- `'string'` / `` `ref<T>` `` / variable
500
+ * `'buffer'` / variable `` `array<T>` `` -- the column carries a u32 slot
501
+ * id; the payload lives in `UniqueRefStore` / `BufferPool`. The bundle
502
+ * entry is a `ManagedColumnReader<T>` (D-4 / D-7) -- read-only by
503
+ * construction, no index signature. Mutation MUST flow through the
504
+ * public dispatch (`world.set` / `world.push` / `world.allocUniqueRef`).
505
+ *
506
+ * The `extends SchemaFieldType` upper bound matches `ComponentSchema[K]`
507
+ * so query bundle types do not have to pre-filter.
508
+ */
509
+ export type TypedArrayFor<T extends SchemaFieldType> = T extends 'f32'
510
+ ? Float32Array
511
+ : T extends 'f64'
512
+ ? Float64Array
513
+ : T extends 'i32'
514
+ ? Int32Array
515
+ : T extends 'u32' | 'enum' | 'ref' | 'entity'
516
+ ? Uint32Array
517
+ : T extends 'i16'
518
+ ? Int16Array
519
+ : T extends 'u16'
520
+ ? Uint16Array
521
+ : T extends 'i8'
522
+ ? Int8Array
523
+ : T extends 'u8' | 'bool'
524
+ ? Uint8Array
525
+ : T extends 'string'
526
+ ? ManagedColumnReader<'string'>
527
+ : T extends `unique<${string}>`
528
+ ? ManagedColumnReader<T>
529
+ : T extends `shared<${string}>`
530
+ ? ManagedColumnReader<T>
531
+ : T extends 'buffer'
532
+ ? ManagedColumnReader<'buffer'>
533
+ : T extends `buffer<${number}>`
534
+ ? Uint8Array
535
+ : T extends `array<${infer Elem extends ManagedArrayElementType}, ${number}>`
536
+ ? TypedArrayFor<
537
+ Elem extends 'entity' | `shared<${string}>` ? 'u32' : Elem
538
+ >
539
+ : T extends `array<${string}>`
540
+ ? ManagedColumnReader<T>
541
+ : never;
542
+
543
+ /**
544
+ * Relationship metadata (feat-20260531 M2 / plan-strategy D-5). Declares this
545
+ * component as the holder side of a Bevy-style bidirectional relationship: the
546
+ * holder carries a single `entity` field (the target), and the engine mirrors
547
+ * the reverse reference into `mirror`.`field` (an `array<entity>` on the target
548
+ * entity) at add / remove / despawn time.
549
+ *
550
+ * - `mirror` — the mirror component's string NAME (not a type reference, so
551
+ * `engine-ecs` never imports the mirror component type; AC-29). The mirror
552
+ * component is a derived runtime view rebuilt by the relationship owner, so
553
+ * it MUST declare `transient: true` — otherwise scene collect serializes it
554
+ * and `instantiateScene` double-writes (serialized copy + owner rebuild).
555
+ * - `field` — the `array<entity>` field on the mirror component that holds the
556
+ * reverse list. Validated to be exactly `'array<entity>'` at `defineComponent` time.
557
+ * - `exclusive` — when `true`, re-adding the holder component with a new target
558
+ * auto-reparents (clears the old mirror entry, then appends the new one)
559
+ * instead of returning `ComponentAlreadyPresentError` (AC-12).
560
+ * - `linkedSpawn` — when `true`, despawning the target recursively despawns the
561
+ * holders in its mirror list. Default `false` (D-1): despawn only prunes the
562
+ * mirror entry, the holder entity survives.
563
+ */
564
+ /** A schema is a record of field-name → field-type keyword. */
565
+ export type ComponentSchema = Record<string, SchemaFieldType>;
566
+
567
+ /** Derive the JS value-shape from a schema (read side; zero-copy views). */
568
+ export type ShapeOf<S extends ComponentSchema> = {
569
+ [K in keyof S]: FieldValueType<S[K]>;
570
+ };
571
+
572
+ /**
573
+ * Derive the input-side value-shape from a schema (write side; widens
574
+ * `array<scalar>` to also accept `readonly number[]` plus the strict
575
+ * TypedArray view). Used by `world.spawn` / `world.addComponent` /
576
+ * `world.set` `data` so AI users can write `times: [0.5]` instead of the
577
+ * `new Float32Array([0.5])` boilerplate. writeArrayField walks both shapes
578
+ * via the same byte-copy path so runtime semantics are identical.
579
+ */
580
+ export type InputShapeOf<S extends ComponentSchema> = {
581
+ [K in keyof S]: FieldInputType<S[K]>;
582
+ };
583
+
584
+ // ────────────────────────────────────────────────────────────────────────────
585
+ // ComponentId
586
+ // ────────────────────────────────────────────────────────────────────────────
587
+
588
+ /**
589
+ * Numeric identity is an ECS-owner fact, not component authoring data. Keep it
590
+ * out of the token's own enumerable surface so reflection sees only
591
+ * `name`/`fields`/`storage`.
592
+ */
593
+ /** Component owner identity shared by independently bundled ECS entry points. */
594
+ const COMPONENT_OWNER_REGISTRY = Symbol.for('forgeax.ecs.componentOwnerRegistry');
595
+ interface ComponentOwnerRegistry {
596
+ nextId: number;
597
+ readonly ids: WeakMap<object, ComponentId>;
598
+ readonly schemas: WeakMap<object, Readonly<Record<string, SchemaFieldType>>>;
599
+ }
600
+ const ownerSymbols = globalThis as typeof globalThis & { [key: symbol]: unknown };
601
+ const ownerRegistry =
602
+ (ownerSymbols[COMPONENT_OWNER_REGISTRY] as ComponentOwnerRegistry | undefined) ??
603
+ (() => {
604
+ const registry: ComponentOwnerRegistry = {
605
+ nextId: 1,
606
+ ids: new WeakMap<object, ComponentId>(),
607
+ schemas: new WeakMap<object, Readonly<Record<string, SchemaFieldType>>>(),
608
+ };
609
+ ownerSymbols[COMPONENT_OWNER_REGISTRY] = registry;
610
+ return registry;
611
+ })();
612
+
613
+ let entityDefinitionSeen = false;
614
+ let componentDefinedBeforeEntity = false;
615
+
616
+ /** @internal Barrel-only check for the id=0 Entity import-order invariant. */
617
+ export function isComponentDefinitionOrderValid(): boolean {
618
+ return !componentDefinedBeforeEntity;
619
+ }
620
+
621
+ /** @internal Read the owner-assigned identity for storage/archetype code. */
622
+ export function componentId(component: Component): ComponentId {
623
+ const id = ownerRegistry.ids.get(component);
624
+ if (id === undefined) throw new Error(`Component identity missing for '${component.name}'.`);
625
+ return id;
626
+ }
627
+
628
+ /** @internal Derive the flat type map from the fields SSOT. */
629
+ export function componentSchema<const C extends Component>(component: C): Readonly<SchemaOf<C>> {
630
+ const schema = ownerRegistry.schemas.get(component);
631
+ if (schema === undefined) throw new Error(`Component schema missing for '${component.name}'.`);
632
+ return schema as Readonly<SchemaOf<C>>;
633
+ }
634
+
635
+ /** Numeric identifier for a component type, used by bitmask matching and archetype edges. */
636
+ export type ComponentId = number;
637
+ export type ComponentStorage = 'table' | 'sparse';
638
+
639
+ // ────────────────────────────────────────────────────────────────────────────
640
+ // Token
641
+ // ────────────────────────────────────────────────────────────────────────────
642
+
643
+ declare const __componentBrand: unique symbol;
644
+
645
+ /**
646
+ * Opaque component token. Carries the component name `N` as a string-literal
647
+ * type parameter (lifted from the `defineComponent` call site via `<const N>`)
648
+ * and the schema-shape `S` as a phantom brand so `world.get(e, Comp)` can
649
+ * return `Result<ShapeOf<S>, EcsError>` precisely.
650
+ *
651
+ * The `N` parameter defaults to `string` to keep existing single-parameter
652
+ * `Component<S>` annotations source-compatible. When inferred from a
653
+ * `defineComponent('Position', ...)` call, `N` is the literal `'Position'`,
654
+ * which lets query row/span mapped types resolve `{ [K in N]: ... }` to a
655
+ * concrete keyed object instead of a degraded index signature (KD-1).
656
+ */
657
+ export interface Component<N extends string = string, S extends ComponentSchema = ComponentSchema> {
658
+ readonly name: N;
659
+ /** The one schema projection: type, default, and enum labels per field. */
660
+ readonly fields: Readonly<Record<keyof S & string, FieldReflection>>;
661
+ readonly storage: ComponentStorage;
662
+ readonly [__componentBrand]: ShapeOf<S>;
663
+ }
664
+
665
+ // ────────────────────────────────────────────────────────────────────────────
666
+ // TypedArray constructors — internal; consumed by `scalarRow()` to build
667
+ // TYPE_METADATA rows (feat-20260602 M4, w12).
668
+ // ────────────────────────────────────────────────────────────────────────────
669
+
670
+ /** TypedArray constructor for each scalar field type. */
671
+ const VIEW_CTORS: Readonly<
672
+ Record<
673
+ ScalarFieldType,
674
+ | Float32ArrayConstructor
675
+ | Float64ArrayConstructor
676
+ | Int32ArrayConstructor
677
+ | Uint32ArrayConstructor
678
+ | Int16ArrayConstructor
679
+ | Uint16ArrayConstructor
680
+ | Int8ArrayConstructor
681
+ | Uint8ArrayConstructor
682
+ >
683
+ > = {
684
+ f32: Float32Array,
685
+ f64: Float64Array,
686
+ i32: Int32Array,
687
+ u32: Uint32Array,
688
+ i16: Int16Array,
689
+ u16: Uint16Array,
690
+ i8: Int8Array,
691
+ u8: Uint8Array,
692
+ bool: Uint8Array,
693
+ enum: Uint32Array,
694
+ ref: Uint32Array,
695
+ };
696
+
697
+ // ────────────────────────────────────────────────────────────────────────────
698
+ // TYPE_METADATA — global per-type metadata table (feat-20260602 M1 / D-A6)
699
+ //
700
+ // Converges the 12 scattered type-intrinsic structures (3 tables + 9
701
+ // predicate / tool functions) into a single per-type authoritative table.
702
+ // Exports FIELD_SIZE_BYTES / VIEW_CTORS / isSchemaVocabKeyword /
703
+ // managedArrayElementBytes / SUPPORTED_FIELD_TYPES / storageFieldType were
704
+ // deleted M4 (w12); internal FIELD_SIZE_BYTES + VIEW_CTORS constants remain as
705
+ // build inputs for scalarRow(). All former consumers now read TYPE_METADATA:
706
+ // storage routing via fieldTypeToMetaKey() + TYPE_METADATA[key].storage,
707
+ // scalar checks via TYPE_METADATA[key]?.isScalar.
708
+ //
709
+ // Mixed key granularity (D-5): the 11 scalars are keyed by their concrete
710
+ // type (`f32` ... `ref`); the 6 vocab families are keyed by family (`entity`
711
+ // / `string` / `buffer` / `ref` / `handle` / `array`). The `array` row's T/N
712
+ // parameters are NOT table columns — they are parsed per-field into
713
+ // `arrayMeta` (see FieldDescriptor below). The vocab `ref` family row and
714
+ // the scalar `ref` row share the `'ref'` key intentionally: the scalar is a
715
+ // u32 column placeholder and the vocab `ref<T>` form maps to the same
716
+ // managed-ref storage, so one row carries both (isScalar + isManaged both
717
+ // true). tweak-20260612-ecs-concept-compression dropped redundant columns:
718
+ // `isVocabKeyword` (zero production consumers), the per-vocab managed-
719
+ // ref predicate column (100% duplicate of `isManaged`), and the YAGNI
720
+ // `fixedByteLength` placeholder;
721
+ // `isLegacyScalar` was renamed `isScalar` (the "legacy" prefix labelled the
722
+ // historical M2-introduction tense; the 11 scalars are first-class).
723
+ // ────────────────────────────────────────────────────────────────────────────
724
+
725
+ /**
726
+ * One row of the global type-metadata table. Carries the type-intrinsic
727
+ * properties a field type has regardless of which component declares it.
728
+ *
729
+ * - `byteSize` — element byte width for the column-storage scalar; `undefined`
730
+ * for families whose storage byte size is not a fixed per-type constant
731
+ * (variable buffer / array slot ids are u32-stored, surfaced via `storage`).
732
+ * - `viewCtor` — TypedArray constructor for the column storage; `undefined`
733
+ * for families without a direct TypedArray column.
734
+ * - `storage` — the column-storage scalar type this field routes to (every
735
+ * vocab family stores a u32 slot id / handle).
736
+ * - `isScalar` — member of the 11 concrete scalar types
737
+ * (`f32`/`f64`/`i32`/`u32`/`i16`/`u16`/`i8`/`u8`/`bool`/`enum`/`ref`).
738
+ * - `isManaged` — routed through `UniqueRefStore` (string / ref<T>).
739
+ * - `isBuffer` — a `buffer` / `buffer<N>` managed-byte slot.
740
+ * - `isEntityRef` — the single-entity `entity` reference keyword.
741
+ * - `isArray` — an `array<T>` / `array<T,N>` keyword.
742
+ */
743
+ export interface TypeMetadataRow {
744
+ readonly byteSize: number | undefined;
745
+ readonly viewCtor:
746
+ | Float32ArrayConstructor
747
+ | Float64ArrayConstructor
748
+ | Int32ArrayConstructor
749
+ | Uint32ArrayConstructor
750
+ | Int16ArrayConstructor
751
+ | Uint16ArrayConstructor
752
+ | Int8ArrayConstructor
753
+ | Uint8ArrayConstructor
754
+ | undefined;
755
+ readonly storage: ScalarFieldType;
756
+ readonly isScalar: boolean;
757
+ readonly isManaged: boolean;
758
+ readonly isBuffer: boolean;
759
+ readonly isEntityRef: boolean;
760
+ readonly isArray: boolean;
761
+ }
762
+
763
+ /** Build a scalar row from the concrete scalar type. */
764
+ function scalarRow(t: ScalarFieldType): TypeMetadataRow {
765
+ return {
766
+ byteSize: FIELD_SIZE_BYTES[t],
767
+ viewCtor: VIEW_CTORS[t],
768
+ storage: t,
769
+ isScalar: true,
770
+ // The scalar `ref` shares its key with the vocab `ref<T>` family; mark
771
+ // it as managed so the single row covers both.
772
+ isManaged: t === 'ref',
773
+ isBuffer: false,
774
+ isEntityRef: false,
775
+ isArray: false,
776
+ };
777
+ }
778
+
779
+ /**
780
+ * Global per-type metadata table. Keyed by concrete scalar type (11) plus
781
+ * vocab family (6 — `entity` / `string` / `buffer` / `ref` / `handle` /
782
+ * `array`). The `ref` key is shared by the legacy scalar and the vocab family
783
+ * (see header). Every vocab family stores a u32 slot id / handle.
784
+ *
785
+ * Built once at module load; frozen so downstream consumers (column.ts /
786
+ * archetype.ts / world.ts, migrated M2) read a stable single source.
787
+ */
788
+ export const TYPE_METADATA: Readonly<Record<string, TypeMetadataRow>> = Object.freeze({
789
+ f32: scalarRow('f32'),
790
+ f64: scalarRow('f64'),
791
+ i32: scalarRow('i32'),
792
+ u32: scalarRow('u32'),
793
+ i16: scalarRow('i16'),
794
+ u16: scalarRow('u16'),
795
+ i8: scalarRow('i8'),
796
+ u8: scalarRow('u8'),
797
+ bool: scalarRow('bool'),
798
+ enum: scalarRow('enum'),
799
+ ref: scalarRow('ref'),
800
+ entity: {
801
+ byteSize: 4,
802
+ viewCtor: Uint32Array,
803
+ storage: 'u32',
804
+ isScalar: false,
805
+ isManaged: false,
806
+ isBuffer: false,
807
+ isEntityRef: true,
808
+ isArray: false,
809
+ },
810
+ string: {
811
+ byteSize: 4,
812
+ viewCtor: Uint32Array,
813
+ storage: 'u32',
814
+ isScalar: false,
815
+ isManaged: true,
816
+ isBuffer: false,
817
+ isEntityRef: false,
818
+ isArray: false,
819
+ },
820
+ buffer: {
821
+ byteSize: 4,
822
+ viewCtor: Uint32Array,
823
+ storage: 'u32',
824
+ isScalar: false,
825
+ isManaged: false,
826
+ isBuffer: true,
827
+ isEntityRef: false,
828
+ isArray: false,
829
+ },
830
+ // feat-20260614-ecs-shared-component-and-unique-rename M3 (plan-strategy
831
+ // D-3): independent `'shared'` row, NOT a reuse of the `'ref'` (post-M2:
832
+ // `'unique<T>'` family) row. `isManaged: true` so write-barrier dispatch
833
+ // routes shared<T> fields through release on despawn / removeComponent /
834
+ // set-overwrite, but the M4 sub-dispatch in releaseManagedFieldOnRow will
835
+ // separate shared (rc--) from unique (direct slot drop) using the
836
+ // fieldType.startsWith('shared<') predicate. Keeping the meta key
837
+ // independent preserves the "meta key = release semantics" invariant
838
+ // (architecture-principles.md #1 SSOT).
839
+ shared: {
840
+ byteSize: 4,
841
+ viewCtor: Uint32Array,
842
+ storage: 'u32',
843
+ isScalar: false,
844
+ isManaged: true,
845
+ isBuffer: false,
846
+ isEntityRef: false,
847
+ isArray: false,
848
+ },
849
+ array: {
850
+ byteSize: 4,
851
+ viewCtor: Uint32Array,
852
+ storage: 'u32',
853
+ isScalar: false,
854
+ isManaged: false,
855
+ isBuffer: false,
856
+ isEntityRef: false,
857
+ isArray: true,
858
+ },
859
+ });
860
+
861
+ // ────────────────────────────────────────────────────────────────────────────
862
+ // FieldDescriptor — input field-descriptor object + per-field reflection
863
+ // (feat-20260602 M1 / D-A1 / D-A3)
864
+ // ────────────────────────────────────────────────────────────────────────────
865
+
866
+ /**
867
+ * Pre-parsed `array<T>` / `array<T,N>` reflection. Bare length sentinel
868
+ * (D-A1 user ruling): `length` present => fixed-capacity, `length === undefined`
869
+ * => variable-capacity. No `isVariable` / `kind` field — both are losslessly
870
+ * derivable from `length` presence (architecture-principles.md #2 Derive). This
871
+ * is exactly the existing `parseManagedArraySchema` return shape (zero shape
872
+ * change).
873
+ */
874
+ export interface ArrayMeta {
875
+ readonly elementType: ManagedArrayElementType;
876
+ readonly length?: number;
877
+ }
878
+
879
+ /**
880
+ * Input field-descriptor object (D-A3). The second `defineComponent` argument
881
+ * may declare each field either as a bare type keyword (legacy flat form,
882
+ * still accepted through M2; migrated repo-wide in M3) or as a descriptor
883
+ * object aggregating `type` + `default` + semantic `shape` + field-level
884
+ * `meta`.
885
+ *
886
+ * - `type` — the schema field-type keyword (a parametrized string such as
887
+ * `'array<f32,3>'` / `'unique<MaterialAsset>'` is used verbatim, D-A2).
888
+ * - `default` — layer-2 default value; retained in the field reflection row.
889
+ * - `shape` — producer-owned semantic shape tag for schema consumers; it does
890
+ * not change ECS storage or runtime value semantics.
891
+ * - `meta` — field-level open namespace; aggregated into `component.meta`. The
892
+ * infra gives no key special meaning (open map, OOS-1).
893
+ * - `transient` — when `true`, scene collect skips this field (D-5). Same word,
894
+ * same meaning as the component-level `transient` flag, with granularity sunk
895
+ * to the field level: a field that is derived/reconstructable (e.g. a resolved
896
+ * world mat4) is excluded from serialization while its component's persisted
897
+ * fields still round-trip. Absent (the common case) means the field is
898
+ * serialized.
899
+ * - `labels` — for an `enum` field ONLY: the label→numeric-value map (e.g.
900
+ * `{ static: 0, dynamic: 1, kinematic: 2 }`). An `enum` field stores a bare
901
+ * `u32` variant index; the human-readable names historically lived in a
902
+ * SEPARATE per-package const map (`RigidBodyTypeValue`) + comment table, so no
903
+ * schema consumer could read them and the two could drift. Declaring `labels`
904
+ * attaches that map to the field itself (SSOT-adjacent): it is aggregated into
905
+ * `component.fields[field].labels` and surfaced by reflection consumers (the
906
+ * editor's `describeComponent`, inspector UIs, validation hints) so a
907
+ * docs-only user learns the legal variants + their integers from the schema
908
+ * alone. Pass the EXISTING `*Value` const map here (Derive, don't Duplicate —
909
+ * one object, two consumers). Absent for non-enum fields / enums that opt out.
910
+ */
911
+ export interface FieldDescriptor<T extends SchemaFieldType = SchemaFieldType> {
912
+ readonly type: T;
913
+ readonly default?: FieldValueType<T>;
914
+ /** Semantic authoring shape; storage still follows `type`. */
915
+ readonly shape?: FieldShapeKind;
916
+ readonly meta?: Readonly<Record<string, unknown>>;
917
+ readonly transient?: boolean;
918
+ readonly labels?: Readonly<Record<string, number>>;
919
+ }
920
+
921
+ /**
922
+ * Per-field reflection produced at registration time and read off
923
+ * `component.fields[fieldName]` (D-A3). Carries the pre-parsed facts: the
924
+ * field `type`, its `default` (if any), semantic `shape` (if declared), — for
925
+ * `array<...>` fields only — the pre-parsed `arrayMeta` (parse happens once at
926
+ * registration, AC-03c), and the field-level `transient` flag (D-5) when
927
+ * declared.
928
+ *
929
+ * `transient` mirrors the component-level `Component.transient` (same word,
930
+ * same meaning): scene collect skips a `transient` field just as it skips a
931
+ * `transient` component. Granularity is sunk to the field level so a component
932
+ * can persist most of its fields while excluding a derived/reconstructable one
933
+ * (e.g. `Transform.world`). Absent means the field participates in
934
+ * serialization.
935
+ */
936
+ export interface FieldReflection {
937
+ readonly type: SchemaFieldType;
938
+ readonly default?: unknown;
939
+ /** Producer-declared semantic shape, when storage type alone is insufficient. */
940
+ readonly shape?: FieldShapeKind;
941
+ readonly arrayMeta?: ArrayMeta;
942
+ readonly transient?: boolean;
943
+ /**
944
+ * For an `enum` field: the label→numeric-value map declared on the field
945
+ * descriptor (see `FieldDescriptor.labels`). Lets a schema consumer resolve a
946
+ * variant name ↔ its stored `u32` index without a separate const map. Absent
947
+ * for non-enum fields / enums that did not declare labels.
948
+ */
949
+ readonly labels?: Readonly<Record<string, number>>;
950
+ }
951
+
952
+ /**
953
+ * One input field-spec value: either the bare type keyword (legacy flat form)
954
+ * or a field-descriptor object. Accepting both keeps the ~44 flat-string
955
+ * call-sites + ~55 test files green through M1/M2 while the field-descriptor
956
+ * form is migrated in repo-wide in M3 (D-A7 / D-A8 shrink the migration
957
+ * surface to the input side only).
958
+ */
959
+ export type FieldSpec<T extends SchemaFieldType = SchemaFieldType> = T | FieldDescriptor<T>;
960
+
961
+ /** An input field-spec map: field-name -> bare keyword | field-descriptor. */
962
+ export type FieldsInput = Record<string, FieldSpec>;
963
+
964
+ /**
965
+ * Project an input field-spec map down to its flat `ComponentSchema` shape
966
+ * (field-name -> type keyword). A bare-keyword spec maps to itself (identity,
967
+ * so existing flat-string call-sites infer exactly as before); a descriptor
968
+ * spec maps to its `type`. This keeps `Component<N, SchemaOf<F>>` driving every
969
+ * downstream type (ShapeOf / query row/span projection / TypedArrayFor) unchanged.
970
+ */
971
+ export type SchemaOf<F extends FieldsInput | Component> =
972
+ F extends Component<string, infer S>
973
+ ? S
974
+ : F extends FieldsInput
975
+ ? {
976
+ [K in keyof F]: F[K] extends FieldDescriptor<infer T>
977
+ ? T
978
+ : F[K] extends SchemaFieldType
979
+ ? F[K]
980
+ : never;
981
+ }
982
+ : never;
983
+
984
+ // ────────────────────────────────────────────────────────────────────────────
985
+ // defineComponent
986
+ // ────────────────────────────────────────────────────────────────────────────
987
+
988
+ /** Optional configuration for `defineComponent` (w4, M3 consumer; w21 layer-2 defaults). */
989
+ export interface DefineComponentOptions {
990
+ readonly storage?: ComponentStorage;
991
+ /**
992
+ * When `true`, the component is skipped by scene collect
993
+ * (rootsToSceneAsset). The component stays in archetype columns and
994
+ * participates normally in queries / world.get at runtime.
995
+ *
996
+ * Default: `false`. Mirror targets of relationship components should
997
+ * declare `transient: true` so they are not serialized (their state is
998
+ * rebuilt by the mirror hook after instantiateScene).
999
+ */
1000
+ readonly transient?: boolean;
1001
+ /**
1002
+ * Component-level open metadata namespace. Entries are copied into
1003
+ * `Component.meta` at registration; the ECS core assigns no meaning to any
1004
+ * key. Component-level entries win over field-level entries with the same
1005
+ * key, and consumers may extend the mutable map after registration.
1006
+ */
1007
+ readonly meta?: Readonly<Record<string, unknown>>;
1008
+ }
1009
+
1010
+ /**
1011
+ * Extract the bare field-type keyword from a field-spec (bare keyword | field-
1012
+ * descriptor object), fail-fast if a descriptor object is missing its `type`.
1013
+ * The throw carries the field name + expected shape (charter P3 / OOS-6: this
1014
+ * is a programmer error caught at registration time, no new EcsErrorCode).
1015
+ */
1016
+ function fieldSpecType(fieldName: string, spec: FieldSpec): SchemaFieldType {
1017
+ if (typeof spec === 'string') return spec as SchemaFieldType;
1018
+ const t = (spec as FieldDescriptor).type;
1019
+ if (typeof t !== 'string') {
1020
+ throw new SchemaUnsupportedFieldError(
1021
+ fieldName,
1022
+ `<field-descriptor missing 'type'> (expected { type, default?, meta? })`,
1023
+ );
1024
+ }
1025
+ return t as SchemaFieldType;
1026
+ }
1027
+
1028
+ /**
1029
+ * Define a component. Returns a frozen opaque token with exactly three runtime
1030
+ * facts: `.name`, `.fields`, and `.storage`. Numeric identity, flat schema,
1031
+ * and defaults are owner projections held outside the token.
1032
+ *
1033
+ * The second argument accepts each field either as a bare type keyword
1034
+ * ('f32', 'array<entity>', ...) or as a field-descriptor object
1035
+ * `{ type, default?, meta? }` (D-A3). The single `FieldsInput` overload
1036
+ * handles both forms — bare keywords are identity through `SchemaOf<F>`.
1037
+ *
1038
+ * The `<const N>` modifier lifts `name` to its string-literal type so the
1039
+ * returned `Component<N, SchemaOf<S>>` drives precise key-based mapped types
1040
+ * downstream (for example QueryRow and QuerySpan projections). At runtime `name` is a plain
1041
+ * string.
1042
+ *
1043
+ * Schema-field validation accepts both the legacy scalar tier
1044
+ * (`ScalarFieldType`, 11 keywords) and the schema-vocab tier
1045
+ * (`SchemaVocabKeyword`, 8 patterns including `array<T,N>` / `array<T>` /
1046
+ * `buffer` / `buffer<N>`). Mismatched values raise
1047
+ * `SchemaUnsupportedFieldError`. Illegal `array<...>` element types
1048
+ * (e.g. `array<ref<X>>`) raise `ManagedArrayElementTypeNotAllowedError`
1049
+ * (AC-03 runtime fail-safe).
1050
+ *
1051
+ * Relationship roles are declared through `defineRelationship`; component
1052
+ * definitions contain only schema vocabulary and no mirror metadata.
1053
+ *
1054
+ * @throws SchemaUnsupportedFieldError for any field type not in the supported
1055
+ * set, or a field-descriptor object missing its `type`.
1056
+ * @throws ManagedArrayElementTypeNotAllowedError when an `array<...>`
1057
+ * keyword carries an illegal element type.
1058
+ * @throws RelationshipMirrorComponentNotRegisteredError when
1059
+ * `relationship.mirror` names a component not yet defined.
1060
+ * @throws RelationshipMirrorFieldTypeMismatchError when the mirror's
1061
+ * `relationship.field` is missing or not typed `'array<entity>'`.
1062
+ */
1063
+ // Single signature post-M4 (w12) — bare-keyword field specs are valid
1064
+ // FieldSpec<T> values (identity through SchemaOf<F>), so flat-string schemas
1065
+ // work without a separate overload. tweak-20260612-ecs-concept-compression
1066
+ // dropped the redundant byte-identical overload declaration.
1067
+ export function defineComponent<const N extends string, const S extends FieldsInput>(
1068
+ name: N,
1069
+ fields: S,
1070
+ options?: DefineComponentOptions,
1071
+ ): Component<N, SchemaOf<S>> {
1072
+ const storage = options?.storage ?? 'table';
1073
+ assertComponentStorage(storage);
1074
+ if (storage === 'sparse' && Object.keys(fields).length !== 0) {
1075
+ throw new SparseStorageRequiresTagError(name);
1076
+ }
1077
+ const schema: Record<string, SchemaFieldType> = {};
1078
+ const reflectedFields: Record<string, FieldReflection> = {};
1079
+ const collectedMeta: Record<string, unknown> = {};
1080
+ const collectedDefaults: Record<string, unknown> = {};
1081
+
1082
+ for (const fieldName of Object.keys(fields)) {
1083
+ const spec = fields[fieldName] as FieldSpec;
1084
+ const fieldType = fieldSpecType(fieldName, spec);
1085
+
1086
+ // Validate the field type — same fail-fast as before, now over the
1087
+ // normalized keyword.
1088
+ let arrayMeta: ArrayMeta | undefined;
1089
+ if (TYPE_METADATA[fieldType]?.isScalar === true) {
1090
+ // legacy scalar — ok
1091
+ } else if (fieldType === 'string') {
1092
+ // string vocab — ok
1093
+ } else if (fieldType.startsWith('array<') && fieldType.endsWith('>')) {
1094
+ const parsed = parseManagedArraySchema(fieldType);
1095
+ if (parsed === null) {
1096
+ const elementType = fieldType.slice(6, -1);
1097
+ throw new ManagedArrayElementTypeNotAllowedError(fieldName, elementType);
1098
+ }
1099
+ // Pre-parse once at registration (AC-03c): array fields cache arrayMeta.
1100
+ // Bare length sentinel {elementType, length?} (D-A1): drop `length` when
1101
+ // variable so the row is byte-identical to the parse return shape.
1102
+ arrayMeta = deepFreeze(
1103
+ parsed.length === undefined
1104
+ ? { elementType: parsed.elementType }
1105
+ : { elementType: parsed.elementType, length: parsed.length },
1106
+ );
1107
+ } else if (!isSchemaVocabKeyword(fieldType)) {
1108
+ throw new SchemaUnsupportedFieldError(fieldName, fieldType);
1109
+ }
1110
+
1111
+ schema[fieldName] = fieldType;
1112
+
1113
+ // Per-field reflection row — only attach arrayMeta / default when present
1114
+ // (exactOptionalPropertyTypes: never set an explicit `undefined`).
1115
+ const row: {
1116
+ type: string;
1117
+ default?: unknown;
1118
+ shape?: FieldShapeKind;
1119
+ arrayMeta?: ArrayMeta;
1120
+ transient?: boolean;
1121
+ labels?: Readonly<Record<string, number>>;
1122
+ } = {
1123
+ type: fieldType,
1124
+ };
1125
+ if (typeof spec !== 'string') {
1126
+ const desc = spec as FieldDescriptor;
1127
+ if ('default' in desc) {
1128
+ row.default = desc.default;
1129
+ collectedDefaults[fieldName] = desc.default;
1130
+ }
1131
+ if (desc.shape !== undefined) row.shape = desc.shape;
1132
+ if (desc.meta !== undefined) {
1133
+ Object.assign(collectedMeta, desc.meta);
1134
+ }
1135
+ // exactOptionalPropertyTypes: only attach `transient` when declared.
1136
+ if (desc.transient !== undefined) row.transient = desc.transient;
1137
+ // enum label→value map (see FieldDescriptor.labels). Frozen so the
1138
+ // reflected row exposes a stable read-only map. Only enum fields declare it.
1139
+ if (desc.labels !== undefined) row.labels = deepFreeze({ ...desc.labels });
1140
+ }
1141
+ if (arrayMeta !== undefined) row.arrayMeta = arrayMeta;
1142
+ reflectedFields[fieldName] = Object.freeze(row) as FieldReflection;
1143
+ }
1144
+
1145
+ if (name === 'Entity') {
1146
+ entityDefinitionSeen = true;
1147
+ } else if (!entityDefinitionSeen) {
1148
+ componentDefinedBeforeEntity = true;
1149
+ }
1150
+ const id = name === 'Entity' ? 0 : ownerRegistry.nextId++;
1151
+
1152
+ // Derived defaults projection (D-A8): pure from `fields[k].default`.
1153
+ // No longer merged with a removed `options.defaults` input — strict single-entry
1154
+ // means the only way to set a layer-2 default is through the field descriptor.
1155
+ const frozenDefaults =
1156
+ Object.keys(collectedDefaults).length === 0
1157
+ ? undefined
1158
+ : (deepFreeze(collectedDefaults) as Readonly<Partial<ShapeOf<SchemaOf<S>>>>);
1159
+
1160
+ // Keep the component token immutable while leaving its open metadata map
1161
+ // extensible for higher-level consumers after registration.
1162
+ if (options?.meta !== undefined) {
1163
+ Object.assign(collectedMeta, options.meta);
1164
+ }
1165
+ const frozenSchema = deepFreeze(schema);
1166
+ const frozenFields = deepFreeze(reflectedFields);
1167
+ const meta = collectedMeta;
1168
+ const token = Object.freeze({ name, fields: frozenFields, storage }) as Component<N, SchemaOf<S>>;
1169
+ ownerRegistry.ids.set(token, id);
1170
+ ownerRegistry.schemas.set(token, frozenSchema);
1171
+ registerComponentDefinition(token, {
1172
+ fields: frozenFields,
1173
+ defaults: frozenDefaults,
1174
+ policy: { transient: options?.transient ?? false, meta },
1175
+ });
1176
+ return Object.freeze(token);
1177
+ }
1178
+
1179
+ export class ComponentInUseError extends Error {
1180
+ override readonly name = 'ComponentInUseError';
1181
+ readonly code = 'component-in-use' as const;
1182
+ readonly expected = 'the component to have no live entity or scheduled-system references';
1183
+ readonly hint =
1184
+ 'Remove owning systems and component values before disposing the registration lease.';
1185
+ readonly detail: { readonly componentName: string };
1186
+
1187
+ constructor(componentName: string) {
1188
+ super(`Component ${componentName} is still in use.`);
1189
+ this.detail = { componentName };
1190
+ }
1191
+ }
1192
+
1193
+ export class ComponentNameConflictError extends Error {
1194
+ override readonly name = 'ComponentNameConflictError';
1195
+ readonly code = 'component-name-conflict' as const;
1196
+ readonly expected = 'one component token per name in a World';
1197
+ readonly hint =
1198
+ 'Use the token already registered in this World or choose a distinct component name.';
1199
+ readonly detail: { readonly componentName: string };
1200
+
1201
+ constructor(componentName: string) {
1202
+ super(`Component ${componentName} is already registered with a different token.`);
1203
+ this.detail = { componentName };
1204
+ }
1205
+ }
1206
+
1207
+ export type ComponentCatalogError = ComponentInUseError | ComponentNameConflictError;
1208
+
1209
+ export interface ComponentLease {
1210
+ readonly component: Component;
1211
+ dispose(): Result<void, ComponentInUseError>;
1212
+ }
1213
+
1214
+ interface ComponentRegistration {
1215
+ readonly component: Component;
1216
+ owners: number;
1217
+ }
1218
+
1219
+ /** World-local discovery and ownership boundary for plugin-installed component vocabulary. */
1220
+ export class ComponentCatalog {
1221
+ private readonly registrations = new Map<string, ComponentRegistration>();
1222
+
1223
+ constructor(private readonly inUse: (component: Component) => boolean) {}
1224
+
1225
+ register(component: Component): Result<ComponentLease, ComponentNameConflictError> {
1226
+ const current = this.registrations.get(component.name);
1227
+ if (current !== undefined && current.component !== component) {
1228
+ return err(new ComponentNameConflictError(component.name));
1229
+ }
1230
+ if (current === undefined) {
1231
+ this.registrations.set(component.name, { component, owners: 1 });
1232
+ } else {
1233
+ current.owners += 1;
1234
+ }
1235
+
1236
+ let active = true;
1237
+ return ok({
1238
+ component,
1239
+ dispose: () => {
1240
+ if (!active) return ok(undefined);
1241
+ const registration = this.registrations.get(component.name);
1242
+ if (registration === undefined || registration.component !== component) {
1243
+ active = false;
1244
+ return ok(undefined);
1245
+ }
1246
+ if (registration.owners > 1) {
1247
+ registration.owners -= 1;
1248
+ active = false;
1249
+ return ok(undefined);
1250
+ }
1251
+ if (this.inUse(component)) return err(new ComponentInUseError(component.name));
1252
+ this.registrations.delete(component.name);
1253
+ active = false;
1254
+ return ok(undefined);
1255
+ },
1256
+ });
1257
+ }
1258
+
1259
+ resolve(name: string): Component | undefined {
1260
+ return this.registrations.get(name)?.component;
1261
+ }
1262
+
1263
+ entries(): ReadonlyMap<string, Component> {
1264
+ return new Map(
1265
+ [...this.registrations].map(([name, registration]) => [name, registration.component]),
1266
+ );
1267
+ }
1268
+ }