@forgeax/engine-app 0.1.2

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 (354) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +418 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
  5. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/app-rebuild.test.d.ts +2 -0
  7. package/dist/__tests__/app-rebuild.test.d.ts.map +1 -0
  8. package/dist/__tests__/asset-registry-types.test-d.d.ts +2 -0
  9. package/dist/__tests__/asset-registry-types.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +2 -0
  11. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +1 -0
  12. package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
  13. package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
  15. package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
  17. package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
  19. package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/create-app-features.test.d.ts +2 -0
  21. package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
  22. package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
  23. package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
  24. package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
  25. package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
  26. package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
  27. package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
  28. package/dist/__tests__/create-app-stop.test.d.ts +2 -0
  29. package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
  30. package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
  31. package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/create-app.test-d.d.ts +2 -0
  33. package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
  34. package/dist/__tests__/create-app.test.d.ts +2 -0
  35. package/dist/__tests__/create-app.test.d.ts.map +1 -0
  36. package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
  37. package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
  39. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
  40. package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
  41. package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +2 -0
  43. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +1 -0
  44. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
  45. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
  46. package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
  47. package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
  48. package/dist/__tests__/ecs-import.unit.test.d.ts +2 -0
  49. package/dist/__tests__/ecs-import.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
  51. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
  53. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
  55. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
  56. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
  57. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
  59. package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
  60. package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
  61. package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
  63. package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
  65. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
  67. package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
  68. package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
  69. package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
  71. package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
  72. package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
  73. package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
  75. package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
  76. package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
  77. package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
  78. package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
  79. package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
  80. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
  81. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
  83. package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
  85. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/execution-world-health-owner.test-d.d.ts +2 -0
  87. package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +1 -0
  88. package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
  89. package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
  91. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
  92. package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
  93. package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
  94. package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
  95. package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
  96. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
  97. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
  99. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
  100. package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
  101. package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
  102. package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
  103. package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
  104. package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
  105. package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
  106. package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
  107. package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
  108. package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
  109. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
  110. package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
  111. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
  112. package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
  113. package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
  114. package/dist/__tests__/remote-integration.test.d.ts +2 -0
  115. package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
  116. package/dist/__tests__/remote-serve-flag.test.d.ts +2 -0
  117. package/dist/__tests__/remote-serve-flag.test.d.ts.map +1 -0
  118. package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
  119. package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
  120. package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
  121. package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
  122. package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
  123. package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
  124. package/dist/__tests__/surface-handoff.test.d.ts +2 -0
  125. package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
  126. package/dist/__tests__/types-m2.test-d.d.ts +2 -0
  127. package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
  128. package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
  129. package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
  130. package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
  131. package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
  132. package/dist/animation-asset-lookup.d.ts +8 -0
  133. package/dist/animation-asset-lookup.d.ts.map +1 -0
  134. package/dist/assets-runtime-assembly.d.ts +19 -0
  135. package/dist/assets-runtime-assembly.d.ts.map +1 -0
  136. package/dist/canvas-policy.d.ts +4 -0
  137. package/dist/canvas-policy.d.ts.map +1 -0
  138. package/dist/create-app.d.ts +57 -0
  139. package/dist/create-app.d.ts.map +1 -0
  140. package/dist/engine-worker-runtime.mjs +884 -0
  141. package/dist/engine-worker-runtime.mjs.map +1 -0
  142. package/dist/errors.d.ts +263 -0
  143. package/dist/errors.d.ts.map +1 -0
  144. package/dist/execution/attached-world-swap.d.ts +21 -0
  145. package/dist/execution/attached-world-swap.d.ts.map +1 -0
  146. package/dist/execution/bootstrap-entry.d.ts +30 -0
  147. package/dist/execution/bootstrap-entry.d.ts.map +1 -0
  148. package/dist/execution/capabilities.d.ts +5 -0
  149. package/dist/execution/capabilities.d.ts.map +1 -0
  150. package/dist/execution/control.d.ts +14 -0
  151. package/dist/execution/control.d.ts.map +1 -0
  152. package/dist/execution/engine-worker-runtime.d.ts +2 -0
  153. package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
  154. package/dist/execution/engine-worker.d.ts +23 -0
  155. package/dist/execution/engine-worker.d.ts.map +1 -0
  156. package/dist/execution/host-controller.d.ts +16 -0
  157. package/dist/execution/host-controller.d.ts.map +1 -0
  158. package/dist/execution/index.d.ts +10 -0
  159. package/dist/execution/index.d.ts.map +1 -0
  160. package/dist/execution/kernel-pool.d.ts +14 -0
  161. package/dist/execution/kernel-pool.d.ts.map +1 -0
  162. package/dist/execution/kernel-worker-runtime.d.ts +2 -0
  163. package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
  164. package/dist/execution/measurement.d.ts +7 -0
  165. package/dist/execution/measurement.d.ts.map +1 -0
  166. package/dist/execution/protocol.d.ts +84 -0
  167. package/dist/execution/protocol.d.ts.map +1 -0
  168. package/dist/execution/report.d.ts +5 -0
  169. package/dist/execution/report.d.ts.map +1 -0
  170. package/dist/execution/schema.d.ts +4 -0
  171. package/dist/execution/schema.d.ts.map +1 -0
  172. package/dist/execution/selector.d.ts +10 -0
  173. package/dist/execution/selector.d.ts.map +1 -0
  174. package/dist/execution/types.d.ts +107 -0
  175. package/dist/execution/types.d.ts.map +1 -0
  176. package/dist/fallback-camera.d.ts +8 -0
  177. package/dist/fallback-camera.d.ts.map +1 -0
  178. package/dist/game-context.d.ts +98 -0
  179. package/dist/game-context.d.ts.map +1 -0
  180. package/dist/index.d.ts +24 -0
  181. package/dist/index.d.ts.map +1 -0
  182. package/dist/index.mjs +3985 -0
  183. package/dist/index.mjs.map +1 -0
  184. package/dist/input-map-plugin.d.ts +5 -0
  185. package/dist/input-map-plugin.d.ts.map +1 -0
  186. package/dist/input-plugin.d.ts +4 -0
  187. package/dist/input-plugin.d.ts.map +1 -0
  188. package/dist/internal/assembled-engine-profile.d.ts +5 -0
  189. package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
  190. package/dist/internal/assets-world-plugin.d.ts +21 -0
  191. package/dist/internal/assets-world-plugin.d.ts.map +1 -0
  192. package/dist/internal/browser-remote-bridge.d.ts +25 -0
  193. package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
  194. package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
  195. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
  196. package/dist/internal/component-introspection.d.ts +24 -0
  197. package/dist/internal/component-introspection.d.ts.map +1 -0
  198. package/dist/internal/ecs-import.d.ts +6 -0
  199. package/dist/internal/ecs-import.d.ts.map +1 -0
  200. package/dist/internal/engine-profile-common.d.ts +9 -0
  201. package/dist/internal/engine-profile-common.d.ts.map +1 -0
  202. package/dist/internal/error-fanout.d.ts +66 -0
  203. package/dist/internal/error-fanout.d.ts.map +1 -0
  204. package/dist/internal/frame-loop.d.ts +35 -0
  205. package/dist/internal/frame-loop.d.ts.map +1 -0
  206. package/dist/internal/input-attach.d.ts +44 -0
  207. package/dist/internal/input-attach.d.ts.map +1 -0
  208. package/dist/internal/main-engine-profile.d.ts +15 -0
  209. package/dist/internal/main-engine-profile.d.ts.map +1 -0
  210. package/dist/internal/remote-serve-flag.d.ts +23 -0
  211. package/dist/internal/remote-serve-flag.d.ts.map +1 -0
  212. package/dist/internal/remote-server-plugin.d.ts +8 -0
  213. package/dist/internal/remote-server-plugin.d.ts.map +1 -0
  214. package/dist/internal/rhi-capture.d.ts +19 -0
  215. package/dist/internal/rhi-capture.d.ts.map +1 -0
  216. package/dist/internal/rhi-debug-flag.d.ts +18 -0
  217. package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
  218. package/dist/internal/worker-engine-profile.d.ts +13 -0
  219. package/dist/internal/worker-engine-profile.d.ts.map +1 -0
  220. package/dist/kernel-worker-runtime.mjs +69 -0
  221. package/dist/kernel-worker-runtime.mjs.map +1 -0
  222. package/dist/load-game-errors.d.ts +131 -0
  223. package/dist/load-game-errors.d.ts.map +1 -0
  224. package/dist/load-game.d.ts +34 -0
  225. package/dist/load-game.d.ts.map +1 -0
  226. package/dist/plugin-factories.d.ts +3 -0
  227. package/dist/plugin-factories.d.ts.map +1 -0
  228. package/dist/tool-preview/bootstrap.d.ts +113 -0
  229. package/dist/tool-preview/bootstrap.d.ts.map +1 -0
  230. package/dist/tool-preview/evidence.d.ts +20 -0
  231. package/dist/tool-preview/evidence.d.ts.map +1 -0
  232. package/dist/tool-preview/framing.d.ts +13 -0
  233. package/dist/tool-preview/framing.d.ts.map +1 -0
  234. package/dist/tool-preview/recipe.d.ts +54 -0
  235. package/dist/tool-preview/recipe.d.ts.map +1 -0
  236. package/dist/types.d.ts +444 -0
  237. package/dist/types.d.ts.map +1 -0
  238. package/package.json +86 -0
  239. package/schema/execution-report.schema.json +168 -0
  240. package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
  241. package/src/__tests__/app-rebuild.test.ts +31 -0
  242. package/src/__tests__/asset-registry-types.test-d.ts +16 -0
  243. package/src/__tests__/assets-runtime-assembly.integration.test.ts +68 -0
  244. package/src/__tests__/callback-deletion.unit.test.ts +14 -0
  245. package/src/__tests__/callback-inference.test-d.ts +32 -0
  246. package/src/__tests__/component-introspection.unit.test.ts +56 -0
  247. package/src/__tests__/consumer-migration.unit.test.ts +21 -0
  248. package/src/__tests__/create-app-features.test.ts +40 -0
  249. package/src/__tests__/create-app-listener-sync.test.ts +377 -0
  250. package/src/__tests__/create-app-lock-provider.test.ts +212 -0
  251. package/src/__tests__/create-app-plugin.test.ts +217 -0
  252. package/src/__tests__/create-app-stop.test.ts +617 -0
  253. package/src/__tests__/create-app-time.unit.test.ts +26 -0
  254. package/src/__tests__/create-app.test-d.ts +82 -0
  255. package/src/__tests__/create-app.test.ts +346 -0
  256. package/src/__tests__/create-renderer-error.integration.test.ts +24 -0
  257. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +19 -0
  258. package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
  259. package/src/__tests__/draw-source-headless-full-chain.test.ts +226 -0
  260. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +182 -0
  261. package/src/__tests__/draw-source-single-world-regression.test.ts +185 -0
  262. package/src/__tests__/ecs-import.unit.test.ts +40 -0
  263. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +74 -0
  264. package/src/__tests__/engine-worker-world-swap.unit.test.ts +76 -0
  265. package/src/__tests__/errors-pointer-lock-failed.test.ts +268 -0
  266. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +80 -0
  267. package/src/__tests__/execution-error-detail.test-d.ts +13 -0
  268. package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
  269. package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
  270. package/src/__tests__/execution-frame-protocol.unit.test.ts +61 -0
  271. package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
  272. package/src/__tests__/execution-measurement.unit.test.ts +20 -0
  273. package/src/__tests__/execution-public-api.test-d.ts +10 -0
  274. package/src/__tests__/execution-public-api.unit.test.ts +9 -0
  275. package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
  276. package/src/__tests__/execution-report.contract.test.ts +50 -0
  277. package/src/__tests__/execution-report.types.test-d.ts +16 -0
  278. package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
  279. package/src/__tests__/execution-selector.unit.test.ts +72 -0
  280. package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
  281. package/src/__tests__/execution-world-health-owner.test-d.ts +9 -0
  282. package/src/__tests__/fallback-camera.unit.test.ts +32 -0
  283. package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
  284. package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
  285. package/src/__tests__/frame-loop-world-array.test.ts +441 -0
  286. package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
  287. package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
  288. package/src/__tests__/plugin-policy.integration.test.ts +15 -0
  289. package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
  290. package/src/__tests__/profiler-default-off.test.ts +53 -0
  291. package/src/__tests__/profiler-frame-token.test.ts +248 -0
  292. package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
  293. package/src/__tests__/profiler-phase-integration.test.ts +129 -0
  294. package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
  295. package/src/__tests__/remote-integration.test.ts +224 -0
  296. package/src/__tests__/remote-serve-flag.test.ts +24 -0
  297. package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
  298. package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
  299. package/src/__tests__/rhi-capture.browser.test.ts +51 -0
  300. package/src/__tests__/surface-handoff.test.ts +92 -0
  301. package/src/__tests__/types-m2.test-d.ts +81 -0
  302. package/src/__tests__/visibility-remote.integration.test.ts +119 -0
  303. package/src/__tests__/worker-execution-stop.test.ts +116 -0
  304. package/src/animation-asset-lookup.ts +13 -0
  305. package/src/assets-runtime-assembly.ts +70 -0
  306. package/src/canvas-policy.ts +16 -0
  307. package/src/create-app.ts +1351 -0
  308. package/src/errors.ts +454 -0
  309. package/src/execution/attached-world-swap.ts +67 -0
  310. package/src/execution/bootstrap-entry.ts +122 -0
  311. package/src/execution/capabilities.ts +102 -0
  312. package/src/execution/control.ts +52 -0
  313. package/src/execution/engine-worker-runtime.ts +400 -0
  314. package/src/execution/engine-worker.ts +161 -0
  315. package/src/execution/host-controller.ts +454 -0
  316. package/src/execution/index.ts +43 -0
  317. package/src/execution/kernel-pool.ts +236 -0
  318. package/src/execution/kernel-worker-runtime.ts +103 -0
  319. package/src/execution/measurement.ts +39 -0
  320. package/src/execution/protocol.ts +131 -0
  321. package/src/execution/report.ts +66 -0
  322. package/src/execution/schema.ts +168 -0
  323. package/src/execution/selector.ts +98 -0
  324. package/src/execution/types.ts +146 -0
  325. package/src/fallback-camera.ts +20 -0
  326. package/src/game-context.ts +126 -0
  327. package/src/index.ts +176 -0
  328. package/src/input-map-plugin.ts +21 -0
  329. package/src/input-plugin.ts +26 -0
  330. package/src/internal/assembled-engine-profile.ts +22 -0
  331. package/src/internal/assets-world-plugin.ts +79 -0
  332. package/src/internal/browser-remote-bridge.ts +267 -0
  333. package/src/internal/browser-rhi-debug-runtime.ts +67 -0
  334. package/src/internal/component-introspection.ts +116 -0
  335. package/src/internal/ecs-import.ts +41 -0
  336. package/src/internal/engine-profile-common.ts +10 -0
  337. package/src/internal/error-fanout.ts +129 -0
  338. package/src/internal/frame-loop.ts +525 -0
  339. package/src/internal/input-attach.ts +104 -0
  340. package/src/internal/main-engine-profile.ts +80 -0
  341. package/src/internal/remote-serve-flag.ts +50 -0
  342. package/src/internal/remote-server-plugin.ts +16 -0
  343. package/src/internal/rhi-capture.ts +211 -0
  344. package/src/internal/rhi-debug-flag.ts +40 -0
  345. package/src/internal/worker-engine-profile.ts +47 -0
  346. package/src/load-game-errors.ts +211 -0
  347. package/src/load-game.ts +115 -0
  348. package/src/plugin-factories.ts +2 -0
  349. package/src/tool-preview/bootstrap.ts +848 -0
  350. package/src/tool-preview/evidence.ts +59 -0
  351. package/src/tool-preview/framing.ts +52 -0
  352. package/src/tool-preview/recipe.ts +137 -0
  353. package/src/types.ts +505 -0
  354. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,226 @@
1
+ // feat-20260709-editor-world-partition-editorworld-super-composite / M2 / w10
2
+ // (RED — impl lands in w12). Full-chain consumer test on the rhi-null headless
3
+ // backend: the drawSource seam drives a REAL Renderer (createRenderer +
4
+ // RhiNull) end-to-end — update -> extract -> record — with no GPU.
5
+ //
6
+ // Unlike w8 (spy renderer, call-shape) and w9 (spy renderer, stale-matrix), this
7
+ // test exercises the actual runtime render pipeline through the seam. The camera
8
+ // + light + visible mesh live ONLY in the injected world; the frame-loop's own
9
+ // world is empty. So a non-zero draw count proves three things at once:
10
+ // 1. drawSource is consumed (else the empty loop world is drawn -> 0 draws),
11
+ // 2. the injected world was world.update(1 / 60).unwrap()'d (propagate ran; extract finds the
12
+ // camera + resolved Transform.world),
13
+ // 3. the full URP record path fired headlessly (bookkeeper counts draws).
14
+ //
15
+ // rhi-null needs no GPU: navigator.gpu is stubbed undefined so createRenderer
16
+ // selects the injected RhiNull instance, and the RhiNullDevice bookkeeper counts
17
+ // draw / bind-group / pipeline records. File suffix `.test.ts` runs in the node
18
+ // vitest project (no dawn / no browser).
19
+ //
20
+ // The frame-loop is driven directly (createFrameLoop + injected now/raf/caf)
21
+ // rather than through createApp, because createApp's assemble form does not (yet
22
+ // — that is w11) forward drawSource; w12 threads it into the loop. Driving the
23
+ // loop directly keeps this test focused on the seam's runtime contract. The
24
+ // drawSource option is reached through a typed alias before w12 widens the type.
25
+ //
26
+ // Anchors:
27
+ // plan-strategy §2 D-3 (drawSource pull -> update injected worlds -> draw)
28
+ // research F1 (frame-loop is the update -> draw driver; the seam insertion pt)
29
+ // feat-20260623 rhi-null-command-flow.unit.test.ts (headless full-chain
30
+ // precedent: createRenderer + RhiNull + bookkeeper counters)
31
+
32
+ import { World } from '@forgeax/engine-ecs';
33
+ import { createPrimitiveMesh } from '@forgeax/engine-geometry';
34
+ import {
35
+ Camera,
36
+ DirectionalLight,
37
+ MeshFilter,
38
+ MeshRenderer,
39
+ type Renderer,
40
+ } from '@forgeax/engine-render';
41
+ import type { RhiNullDevice } from '@forgeax/engine-rhi-null';
42
+ import { rhi } from '@forgeax/engine-rhi-null';
43
+ import { createRenderer } from '@forgeax/engine-runtime';
44
+ import { registerPropagateTransforms, Transform } from '@forgeax/engine-scene';
45
+
46
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
47
+ import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
48
+
49
+ function makeStubCanvas(w = 800, h = 600): HTMLCanvasElement {
50
+ return {
51
+ width: w,
52
+ height: h,
53
+ getContext(_kind: string): unknown {
54
+ return null;
55
+ },
56
+ addEventListener: () => undefined,
57
+ removeEventListener: () => undefined,
58
+ } as unknown as HTMLCanvasElement;
59
+ }
60
+
61
+ function buildManifestDataUrl(): string {
62
+ const manifest = {
63
+ schemaVersion: '1.0.0',
64
+ entries: [
65
+ { hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
66
+ { hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
67
+ {
68
+ hash: 'tonemap0',
69
+ wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
70
+ glsl: '',
71
+ bindings: '',
72
+ },
73
+ ],
74
+ };
75
+ return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
76
+ }
77
+
78
+ function identityTransform(
79
+ overrides: Partial<Record<string, number[]>> = {},
80
+ ): Record<string, number[]> {
81
+ return {
82
+ pos: [0, 0, 0],
83
+ quat: [0, 0, 0, 1],
84
+ scale: [1, 1, 1],
85
+ ...overrides,
86
+ };
87
+ }
88
+
89
+ // The injected world carries the FULL renderable set (camera + light + visible
90
+ // mesh). It also registers propagateTransforms so the frame-loop's world.update
91
+ // resolves Transform.world before extract reads it.
92
+ function makeInjectedWorld(): World {
93
+ const world = new World();
94
+ const cube = createPrimitiveMesh('cube').unwrap();
95
+ const cubeHandle = world.internSharedRef('MeshAsset', cube);
96
+ registerPropagateTransforms(world);
97
+ world.spawn(
98
+ { component: Transform, data: identityTransform({ pos: [0, 0, 5] }) },
99
+ { component: Camera, data: { fov: 60, near: 0.1, far: 1000, tonemap: 4 } },
100
+ );
101
+ world.spawn(
102
+ { component: Transform, data: identityTransform() },
103
+ {
104
+ component: DirectionalLight,
105
+ data: { direction: [0.5, -1, -0.5], castShadow: true, intensity: 1 },
106
+ },
107
+ );
108
+ world.spawn(
109
+ { component: Transform, data: identityTransform() },
110
+ { component: MeshFilter, data: { assetHandle: cubeHandle } },
111
+ { component: MeshRenderer, data: {} },
112
+ );
113
+ return world;
114
+ }
115
+
116
+ function makeSyncScheduler() {
117
+ let pending: ((t: number) => void) | null = null;
118
+ let clock = 0;
119
+ const raf = (cb: (t: number) => void): number => {
120
+ pending = cb;
121
+ return 1;
122
+ };
123
+ const caf = (): void => {
124
+ pending = null;
125
+ };
126
+ const now = (): number => {
127
+ clock += 16;
128
+ return clock;
129
+ };
130
+ const pump = (frames: number): void => {
131
+ for (let i = 0; i < frames; i++) {
132
+ const cb = pending;
133
+ pending = null;
134
+ if (cb === null) break;
135
+ cb(clock);
136
+ }
137
+ };
138
+ return { raf, caf, now, pump };
139
+ }
140
+
141
+ type DrawSourceCallback = () =>
142
+ | { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
143
+ | undefined;
144
+
145
+ type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
146
+ drawSource?: DrawSourceCallback;
147
+ };
148
+
149
+ const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
150
+ opts: FrameLoopOptionsWithDrawSource,
151
+ ) => ReturnType<typeof createFrameLoop>;
152
+
153
+ // The RhiNull instance is a structural stand-in for a real RhiInstance in this
154
+ // headless test; cast the options bag to RendererOptions so the direct
155
+ // createRenderer import (strict RendererOptions) accepts the injected backend.
156
+ const rhiOptions = {
157
+ rhi: rhi as unknown as import('@forgeax/engine-render').RendererOptions['rhi'],
158
+ } as import('@forgeax/engine-render').RendererOptions;
159
+
160
+ let renderer: Renderer | null = null;
161
+
162
+ beforeEach(() => {
163
+ vi.stubGlobal('navigator', { gpu: undefined });
164
+ });
165
+
166
+ afterEach(() => {
167
+ vi.unstubAllGlobals();
168
+ if (renderer) {
169
+ try {
170
+ renderer.dispose();
171
+ } catch {
172
+ // ignore
173
+ }
174
+ renderer = null;
175
+ }
176
+ });
177
+
178
+ describe('drawSource full chain on rhi-null headless (w10)', () => {
179
+ it('drives update -> extract -> record for an injected world with no GPU', async () => {
180
+ const canvas = makeStubCanvas();
181
+ renderer = await createRenderer(canvas, rhiOptions, {
182
+ shaderManifestUrl: buildManifestDataUrl(),
183
+ });
184
+ await renderer.ready;
185
+
186
+ const rhiNullDevice = renderer.device as unknown as RhiNullDevice;
187
+
188
+ // The frame-loop's own world is EMPTY — it has no camera / mesh, so it
189
+ // records zero draws on its own. Only the injected world is renderable.
190
+ const loopWorld = new World();
191
+ const injectedWorld = makeInjectedWorld();
192
+
193
+ const drawSource: DrawSourceCallback = () => ({
194
+ worlds: [injectedWorld],
195
+ cameraOwner: 0,
196
+ resourceOwner: 0,
197
+ });
198
+
199
+ const { raf, caf, now, pump } = makeSyncScheduler();
200
+ const loop = createFrameLoopWithDrawSource({
201
+ world: loopWorld,
202
+ renderer: renderer as Renderer,
203
+ now,
204
+ raf,
205
+ caf,
206
+ drawSource,
207
+ });
208
+
209
+ // Reset per-frame counters immediately before the observed frame.
210
+ rhiNullDevice.totalDrawCount = 0;
211
+ rhiNullDevice.totalBindGroupCount = 0;
212
+ rhiNullDevice.framePassNames = [];
213
+
214
+ expect(loop.start().ok).toBe(true);
215
+ pump(1);
216
+ loop.stop();
217
+
218
+ // The full URP record path fired for the INJECTED world: a non-zero draw
219
+ // count is only reachable if drawSource was consumed AND the injected world
220
+ // was updated (camera + resolved Transform.world present for extract).
221
+ expect(rhiNullDevice.totalDrawCount).toBeGreaterThanOrEqual(1);
222
+ expect(rhiNullDevice.totalBindGroupCount).toBeGreaterThanOrEqual(1);
223
+ expect(renderer.perFramePassNames.length).toBeGreaterThan(0);
224
+ expect(renderer.perFramePassNames).toContain('main');
225
+ });
226
+ });
@@ -0,0 +1,182 @@
1
+ // feat-20260709-editor-world-partition-editorworld-super-composite / M2 / w9
2
+ // (RED — impl lands in w12). Contract test: a world injected through drawSource
3
+ // MUST be world.update(1 / 60).unwrap()'d by the frame-loop in the SAME frame it is drawn, so
4
+ // the renderer reads a freshly-propagated Transform.world mat4 — never a stale
5
+ // one.
6
+ //
7
+ // Why this matters (Strategist D-3, not covered by research): the renderer's
8
+ // extract stage reads the DERIVED `Transform.world` column, whose sole writer is
9
+ // the propagateTransforms system (plugin-factories.ts / systems/propagate-
10
+ // transforms.ts). That writer only runs inside `world.update(1 / 60).unwrap()`. If drawSource
11
+ // merely fed injected worlds into `renderer.draw(worlds, ...)` WITHOUT the
12
+ // frame-loop running `world.update(1 / 60).unwrap()` on them first, the renderer would read the
13
+ // injected world's Transform.world at its previous (or default identity) value —
14
+ // a stale-matrix bug that renders the injected world one frame behind (or at the
15
+ // origin on the first frame).
16
+ //
17
+ // This test spawns an entity in the injected world with a DISTINGUISHING
18
+ // non-identity translation and never calls world.update(1 / 60).unwrap() on that world itself.
19
+ // The spy renderer reads `Transform.world` at draw time. A frame-loop that
20
+ // updates injected worlds surfaces the translation in the mat4's column-major
21
+ // [12]/[13]/[14] slots; a draw-only frame-loop surfaces the identity default
22
+ // (0/0/0), which fails the assertion. So the test has genuine discriminating
23
+ // power over the stale-matrix regression.
24
+ //
25
+ // Driven through createFrameLoop directly with injected now / raf / caf seams,
26
+ // mirroring frame-loop-world-array.test.ts. The drawSource option is reached
27
+ // through a typed alias before w12 widens the real FrameLoopOptions type.
28
+ //
29
+ // Anchors:
30
+ // plan-strategy §2 D-3 (frame-loop MUST update injected worlds — transform
31
+ // propagation — before draw; not draw-only)
32
+ // research F1 (frame-loop is the world.update -> renderer.draw driver)
33
+ // AGENTS.md invariant 6 (Transform.world is the round-trip fidelity seam)
34
+
35
+ import type { EntityHandle } from '@forgeax/engine-ecs';
36
+ import { World } from '@forgeax/engine-ecs';
37
+ import type { Renderer } from '@forgeax/engine-render';
38
+ import { registerPropagateTransforms, Transform } from '@forgeax/engine-scene';
39
+ import { describe, expect, it } from 'vitest';
40
+ import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
41
+
42
+ // Distinguishing translation for the injected world's entity. Column-major mat4
43
+ // carries translation at indices [12] (x), [13] (y), [14] (z).
44
+ const INJECTED_TX = 5;
45
+ const INJECTED_TY = -3;
46
+ const INJECTED_TZ = 7;
47
+
48
+ function makeSyncScheduler() {
49
+ let pending: ((t: number) => void) | null = null;
50
+ let clock = 0;
51
+ const raf = (cb: (t: number) => void): number => {
52
+ pending = cb;
53
+ return 1;
54
+ };
55
+ const caf = (): void => {
56
+ pending = null;
57
+ };
58
+ const now = (): number => {
59
+ clock += 16;
60
+ return clock;
61
+ };
62
+ const pump = (frames: number): void => {
63
+ for (let i = 0; i < frames; i++) {
64
+ const cb = pending;
65
+ pending = null;
66
+ if (cb === null) break;
67
+ cb(clock);
68
+ }
69
+ };
70
+ return { raf, caf, now, pump };
71
+ }
72
+
73
+ type DrawSourceCallback = () =>
74
+ | { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
75
+ | undefined;
76
+
77
+ type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
78
+ drawSource?: DrawSourceCallback;
79
+ };
80
+
81
+ const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
82
+ opts: FrameLoopOptionsWithDrawSource,
83
+ ) => ReturnType<typeof createFrameLoop>;
84
+
85
+ describe('drawSource injected worlds are updated same-frame (w9, stale-matrix regression)', () => {
86
+ it('renderer reads a freshly-propagated Transform.world for the injected world at draw time', () => {
87
+ // The loop's OWN world — the single-world identity path. Never carries the
88
+ // injected entity, so it cannot mask a stale read on the injected world.
89
+ const loopWorld = new World();
90
+
91
+ // The injected world: register propagateTransforms (the sole Transform.world
92
+ // writer) and spawn an entity with a distinguishing non-identity translation.
93
+ // We deliberately do NOT call injectedWorld.update() here — the frame-loop
94
+ // must run it for the translation to reach Transform.world.
95
+ const injectedWorld = new World();
96
+ registerPropagateTransforms(injectedWorld);
97
+ const spawned = injectedWorld
98
+ .spawn({
99
+ component: Transform,
100
+ data: {
101
+ pos: [INJECTED_TX, INJECTED_TY, INJECTED_TZ],
102
+ quat: [0, 0, 0, 1],
103
+ scale: [1, 1, 1],
104
+ },
105
+ })
106
+ .unwrap();
107
+ const entity = spawned as unknown as EntityHandle;
108
+
109
+ // Sanity: BEFORE any frame, Transform.world is still the identity default
110
+ // (translation columns are 0). This proves the assertion below measures the
111
+ // frame-loop's update, not a pre-propagated value.
112
+ const before = injectedWorld.get(entity, Transform);
113
+ expect(before.ok).toBe(true);
114
+ if (before.ok) {
115
+ const w = before.value.world as unknown as ArrayLike<number>;
116
+ expect(w[12]).toBe(0);
117
+ expect(w[13]).toBe(0);
118
+ expect(w[14]).toBe(0);
119
+ }
120
+
121
+ // Spy renderer: at draw time, read the injected world's Transform.world and
122
+ // record the translation columns. This is the exact read the real extract
123
+ // stage performs (Transform.world is the extract SSOT).
124
+ const observed: Array<[number, number, number]> = [];
125
+ const renderer = {
126
+ backend: 'webgpu' as const,
127
+ ready: Promise.resolve({ ok: true, value: undefined }),
128
+ attachWorld: () => ({ ok: true, value: undefined }),
129
+ detachWorld: () => {},
130
+ draw(worlds: readonly World[]): { ok: true; value: undefined } {
131
+ // worlds is what drawSource returned; find the injected world in it.
132
+ const iw = worlds.find((w) => w === injectedWorld);
133
+ if (iw !== undefined) {
134
+ const r = iw.get(entity, Transform);
135
+ if (r.ok) {
136
+ const m = r.value.world as unknown as ArrayLike<number>;
137
+ observed.push([m[12] as number, m[13] as number, m[14] as number]);
138
+ }
139
+ }
140
+ return { ok: true, value: undefined };
141
+ },
142
+ onError(): () => void {
143
+ return () => {
144
+ // no-op
145
+ };
146
+ },
147
+ dispose(): void {
148
+ // no-op
149
+ },
150
+ } as unknown as Renderer;
151
+
152
+ const drawSource: DrawSourceCallback = () => ({
153
+ worlds: [injectedWorld],
154
+ cameraOwner: 0,
155
+ resourceOwner: 0,
156
+ });
157
+
158
+ const { raf, caf, now, pump } = makeSyncScheduler();
159
+ const loop = createFrameLoopWithDrawSource({
160
+ world: loopWorld,
161
+ renderer,
162
+ now,
163
+ raf,
164
+ caf,
165
+ drawSource,
166
+ });
167
+
168
+ expect(loop.start().ok).toBe(true);
169
+ pump(2);
170
+ loop.stop();
171
+
172
+ // At least one frame observed the injected world, and every observation saw
173
+ // the FRESH translation — the frame-loop ran injectedWorld.update() before
174
+ // draw. A draw-only frame-loop would observe [0, 0, 0] (identity) and fail.
175
+ expect(observed.length).toBeGreaterThanOrEqual(1);
176
+ for (const [x, y, z] of observed) {
177
+ expect(x).toBeCloseTo(INJECTED_TX);
178
+ expect(y).toBeCloseTo(INJECTED_TY);
179
+ expect(z).toBeCloseTo(INJECTED_TZ);
180
+ }
181
+ });
182
+ });
@@ -0,0 +1,185 @@
1
+ // feat-20260709-editor-world-partition-editorworld-super-composite / M2 / w8
2
+ // (RED — impl lands in w12). Contract test: the drawSource injection seam MUST
3
+ // degrade to the existing single-world path byte-identically when the host does
4
+ // not opt in.
5
+ //
6
+ // Two non-opt-in shapes must both reproduce the legacy call
7
+ // `renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })` (feat-20260708 M3 / AC-03 wrapping):
8
+ //
9
+ // 1. `drawSource` absent entirely — the createApp/frame-loop caller passes no
10
+ // drawSource (the single-world AI-user default).
11
+ // 2. `drawSource` present but returning `undefined` on the frame — a host
12
+ // that wired the seam but, this frame, has nothing multi-world to inject
13
+ // (e.g. editor with no partitioned viewport active).
14
+ //
15
+ // Both cases MUST call `renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })` with the loop's
16
+ // own single world and owner 0. This pins the AC-03 regression guarantee across
17
+ // the new seam: adding drawSource never perturbs the single-world path.
18
+ //
19
+ // This test drives createFrameLoop directly (injected now / raf / caf seams)
20
+ // with a spy renderer, mirroring frame-loop-world-array.test.ts. The drawSource
21
+ // option is reached through a typed alias so the intent reads clearly before w12
22
+ // widens the real FrameLoopOptions type (test-first RED window).
23
+ //
24
+ // Anchors:
25
+ // plan-strategy §2 D-3 (drawSource pull callback; absent / undefined -> single
26
+ // world path byte-identical to draw([world], { cameraOwner: 0, resourceOwner: 0 }))
27
+ // research F1 (frame-loop hardcodes renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 }) — the
28
+ // seam insertion point)
29
+
30
+ import { World } from '@forgeax/engine-ecs';
31
+ import type { Renderer } from '@forgeax/engine-render';
32
+ import { describe, expect, it } from 'vitest';
33
+ import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
34
+
35
+ interface DrawCall {
36
+ readonly worlds: unknown;
37
+ readonly options: unknown;
38
+ }
39
+
40
+ function makeSpyRenderer(): { renderer: Renderer; calls: DrawCall[] } {
41
+ const calls: DrawCall[] = [];
42
+ const renderer = {
43
+ backend: 'webgpu' as const,
44
+ ready: Promise.resolve({ ok: true, value: undefined }),
45
+ attachWorld: () => ({ ok: true, value: undefined }),
46
+ detachWorld: () => {},
47
+ draw(worlds: unknown, options: unknown): { ok: true; value: undefined } {
48
+ calls.push({ worlds, options });
49
+ return { ok: true, value: undefined };
50
+ },
51
+ onError(): () => void {
52
+ return () => {
53
+ // no-op unsubscribe
54
+ };
55
+ },
56
+ dispose(): void {
57
+ // no-op
58
+ },
59
+ } as unknown as Renderer;
60
+ return { renderer, calls };
61
+ }
62
+
63
+ /**
64
+ * Synchronous rAF driver: fire the scheduled tick exactly `frames` times.
65
+ * Mirrors frame-loop-world-array.test.ts so both single-world contract tests
66
+ * step the loop identically.
67
+ */
68
+ function makeSyncScheduler() {
69
+ let pending: ((t: number) => void) | null = null;
70
+ let clock = 0;
71
+ const raf = (cb: (t: number) => void): number => {
72
+ pending = cb;
73
+ return 1;
74
+ };
75
+ const caf = (): void => {
76
+ pending = null;
77
+ };
78
+ const now = (): number => {
79
+ clock += 16;
80
+ return clock;
81
+ };
82
+ const pump = (frames: number): void => {
83
+ for (let i = 0; i < frames; i++) {
84
+ const cb = pending;
85
+ pending = null;
86
+ if (cb === null) break;
87
+ cb(clock);
88
+ }
89
+ };
90
+ return { raf, caf, now, pump };
91
+ }
92
+
93
+ // The drawSource pull callback shape w11/w12 will introduce on FrameLoopOptions.
94
+ // Declared locally so this test states the contract independent of the (not-yet
95
+ // widened) source type. `undefined` return = "no multi-world injection this
96
+ // frame" (degrade to single world).
97
+ type DrawSourceCallback = () =>
98
+ | { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
99
+ | undefined;
100
+
101
+ type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
102
+ drawSource?: DrawSourceCallback;
103
+ };
104
+
105
+ const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
106
+ opts: FrameLoopOptionsWithDrawSource,
107
+ ) => ReturnType<typeof createFrameLoop>;
108
+
109
+ describe('drawSource seam degrades to single-world path (w8, AC-03 regression)', () => {
110
+ it('no drawSource: draws the primary World with explicit camera and resource owners', () => {
111
+ const world = new World();
112
+ const { renderer, calls } = makeSpyRenderer();
113
+ const { raf, caf, now, pump } = makeSyncScheduler();
114
+
115
+ // No drawSource field at all — the single-world AI-user default.
116
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
117
+ expect(loop.start().ok).toBe(true);
118
+ pump(3);
119
+
120
+ expect(calls.length).toBeGreaterThanOrEqual(3);
121
+ for (const call of calls) {
122
+ expect(Array.isArray(call.worlds)).toBe(true);
123
+ const arr = call.worlds as unknown[];
124
+ expect(arr.length).toBe(1);
125
+ expect(arr[0]).toBe(world);
126
+ expect(call.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
127
+ }
128
+
129
+ loop.stop();
130
+ });
131
+
132
+ it('drawSource returns undefined: still draws [world] with { cameraOwner: 0, resourceOwner: 0 } (no-inject frame)', () => {
133
+ const world = new World();
134
+ const { renderer, calls } = makeSpyRenderer();
135
+ const { raf, caf, now, pump } = makeSyncScheduler();
136
+
137
+ let pulls = 0;
138
+ const drawSource: DrawSourceCallback = () => {
139
+ pulls++;
140
+ // Host wired the seam but has nothing multi-world to inject this frame.
141
+ return undefined;
142
+ };
143
+
144
+ const loop = createFrameLoopWithDrawSource({ world, renderer, now, raf, caf, drawSource });
145
+ expect(loop.start().ok).toBe(true);
146
+ pump(3);
147
+
148
+ // The seam was consulted (pull callback fired) but the resulting draw is
149
+ // identical to the primary-World call.
150
+ expect(pulls).toBeGreaterThanOrEqual(3);
151
+ expect(calls.length).toBeGreaterThanOrEqual(3);
152
+ for (const call of calls) {
153
+ expect(Array.isArray(call.worlds)).toBe(true);
154
+ const arr = call.worlds as unknown[];
155
+ expect(arr.length).toBe(1);
156
+ expect(arr[0]).toBe(world);
157
+ expect(call.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
158
+ }
159
+
160
+ loop.stop();
161
+ });
162
+
163
+ it('setDrawSource switches routing without creating a second frame loop', () => {
164
+ const world = new World();
165
+ const overlay = new World();
166
+ const { renderer, calls } = makeSpyRenderer();
167
+ const { raf, caf, now, pump } = makeSyncScheduler();
168
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
169
+
170
+ expect(loop.start().ok).toBe(true);
171
+ pump(1);
172
+ loop.setDrawSource(() => ({ worlds: [world, overlay], cameraOwner: 0, resourceOwner: 0 }));
173
+ pump(1);
174
+ loop.setDrawSource(undefined);
175
+ pump(1);
176
+
177
+ expect(calls).toHaveLength(3);
178
+ expect(calls[0]?.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
179
+ expect(calls[1]?.worlds).toEqual([world, overlay]);
180
+ expect(calls[1]?.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
181
+ expect(calls[2]?.worlds).toEqual([world]);
182
+ expect(calls[2]?.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
183
+ loop.stop();
184
+ });
185
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ createEcsImportModule,
4
+ ECS_PUBLIC_SYMBOLS,
5
+ projectEcsPublicModule,
6
+ } from '../internal/ecs-import';
7
+
8
+ describe('host-owned ECS import projection', () => {
9
+ it('keeps the explicit six-symbol surface and drops implementation exports', () => {
10
+ expect(ECS_PUBLIC_SYMBOLS).toEqual([
11
+ 'World',
12
+ 'Entity',
13
+ 'Update',
14
+ 'FixedUpdate',
15
+ 'Time',
16
+ 'FixedTime',
17
+ ]);
18
+ expect(
19
+ projectEcsPublicModule({
20
+ World: 'World',
21
+ Entity: 'Entity',
22
+ defineComponent: 'implementation-only',
23
+ }),
24
+ ).toEqual({ World: 'World', Entity: 'Entity' });
25
+ });
26
+
27
+ it('projects only the ECS specifier and preserves other host modules', async () => {
28
+ const calls: string[] = [];
29
+ const importModule = createEcsImportModule(async (specifier) => {
30
+ calls.push(specifier);
31
+ return specifier === '@forgeax/engine-ecs'
32
+ ? { World: 'World', defineComponent: 'implementation-only' }
33
+ : { value: 'other-module' };
34
+ });
35
+
36
+ await expect(importModule('@forgeax/engine-ecs')).resolves.toEqual({ World: 'World' });
37
+ await expect(importModule('virtual:host-module')).resolves.toEqual({ value: 'other-module' });
38
+ expect(calls).toEqual(['@forgeax/engine-ecs', 'virtual:host-module']);
39
+ });
40
+ });