@forgeax/engine-app 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 (348) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +424 -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__/callback-deletion.unit.test.d.ts +2 -0
  7. package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
  9. package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
  11. package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
  13. package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/create-app-error-retention.integration.test.d.ts +2 -0
  15. package/dist/__tests__/create-app-error-retention.integration.test.d.ts.map +1 -0
  16. package/dist/__tests__/create-app-features.test.d.ts +2 -0
  17. package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
  18. package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
  19. package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
  20. package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
  21. package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
  22. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts +2 -0
  23. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
  25. package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
  26. package/dist/__tests__/create-app-stop.test.d.ts +2 -0
  27. package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
  28. package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
  29. package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/create-app.test-d.d.ts +2 -0
  31. package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/create-app.test.d.ts +2 -0
  33. package/dist/__tests__/create-app.test.d.ts.map +1 -0
  34. package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
  35. package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
  37. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
  39. package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
  41. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
  42. package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
  43. package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
  44. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
  45. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
  47. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
  49. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
  50. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
  51. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
  53. package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
  54. package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
  55. package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
  57. package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
  59. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
  61. package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
  62. package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
  63. package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
  65. package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
  66. package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
  67. package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
  69. package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
  70. package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
  71. package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
  72. package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
  73. package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
  74. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
  75. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
  77. package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
  79. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
  81. package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
  83. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
  84. package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
  85. package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
  87. package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
  88. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
  89. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
  91. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
  92. package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
  93. package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
  94. package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
  95. package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
  96. package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
  97. package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
  98. package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
  99. package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
  100. package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
  101. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
  102. package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
  103. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
  104. package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
  105. package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
  106. package/dist/__tests__/remote-integration.test.d.ts +2 -0
  107. package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
  108. package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
  109. package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
  110. package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
  111. package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
  112. package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
  113. package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
  114. package/dist/__tests__/surface-handoff.test.d.ts +2 -0
  115. package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
  116. package/dist/__tests__/types-m2.test-d.d.ts +2 -0
  117. package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
  118. package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
  119. package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
  120. package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
  121. package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
  122. package/dist/animation-asset-lookup.d.ts +7 -0
  123. package/dist/animation-asset-lookup.d.ts.map +1 -0
  124. package/dist/browser-frame-signal.d.ts +17 -0
  125. package/dist/browser-frame-signal.d.ts.map +1 -0
  126. package/dist/canvas-policy.d.ts +4 -0
  127. package/dist/canvas-policy.d.ts.map +1 -0
  128. package/dist/create-app.d.ts +78 -0
  129. package/dist/create-app.d.ts.map +1 -0
  130. package/dist/engine-worker-runtime.mjs +827 -0
  131. package/dist/engine-worker-runtime.mjs.map +1 -0
  132. package/dist/errors.d.ts +254 -0
  133. package/dist/errors.d.ts.map +1 -0
  134. package/dist/execution/attached-world-swap.d.ts +10 -0
  135. package/dist/execution/attached-world-swap.d.ts.map +1 -0
  136. package/dist/execution/bootstrap-entry.d.ts +30 -0
  137. package/dist/execution/bootstrap-entry.d.ts.map +1 -0
  138. package/dist/execution/bootstrap-url.d.ts +5 -0
  139. package/dist/execution/bootstrap-url.d.ts.map +1 -0
  140. package/dist/execution/capabilities.d.ts +5 -0
  141. package/dist/execution/capabilities.d.ts.map +1 -0
  142. package/dist/execution/control.d.ts +12 -0
  143. package/dist/execution/control.d.ts.map +1 -0
  144. package/dist/execution/engine-worker-runtime.d.ts +2 -0
  145. package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
  146. package/dist/execution/engine-worker.d.ts +24 -0
  147. package/dist/execution/engine-worker.d.ts.map +1 -0
  148. package/dist/execution/host-controller.d.ts +16 -0
  149. package/dist/execution/host-controller.d.ts.map +1 -0
  150. package/dist/execution/index.d.ts +10 -0
  151. package/dist/execution/index.d.ts.map +1 -0
  152. package/dist/execution/kernel-pool.d.ts +14 -0
  153. package/dist/execution/kernel-pool.d.ts.map +1 -0
  154. package/dist/execution/kernel-worker-runtime.d.ts +2 -0
  155. package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
  156. package/dist/execution/measurement.d.ts +7 -0
  157. package/dist/execution/measurement.d.ts.map +1 -0
  158. package/dist/execution/protocol.d.ts +87 -0
  159. package/dist/execution/protocol.d.ts.map +1 -0
  160. package/dist/execution/report.d.ts +5 -0
  161. package/dist/execution/report.d.ts.map +1 -0
  162. package/dist/execution/schema.d.ts +4 -0
  163. package/dist/execution/schema.d.ts.map +1 -0
  164. package/dist/execution/selector.d.ts +10 -0
  165. package/dist/execution/selector.d.ts.map +1 -0
  166. package/dist/execution/types.d.ts +123 -0
  167. package/dist/execution/types.d.ts.map +1 -0
  168. package/dist/fallback-camera.d.ts +8 -0
  169. package/dist/fallback-camera.d.ts.map +1 -0
  170. package/dist/game-context.d.ts +98 -0
  171. package/dist/game-context.d.ts.map +1 -0
  172. package/dist/index.d.ts +27 -0
  173. package/dist/index.d.ts.map +1 -0
  174. package/dist/index.mjs +3980 -0
  175. package/dist/index.mjs.map +1 -0
  176. package/dist/input-map-plugin.d.ts +5 -0
  177. package/dist/input-map-plugin.d.ts.map +1 -0
  178. package/dist/input-plugin.d.ts +4 -0
  179. package/dist/input-plugin.d.ts.map +1 -0
  180. package/dist/internal/assembled-engine-profile.d.ts +5 -0
  181. package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
  182. package/dist/internal/assets-world-plugin.d.ts +7 -0
  183. package/dist/internal/assets-world-plugin.d.ts.map +1 -0
  184. package/dist/internal/browser-remote-bridge.d.ts +24 -0
  185. package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
  186. package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
  187. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
  188. package/dist/internal/component-introspection.d.ts +24 -0
  189. package/dist/internal/component-introspection.d.ts.map +1 -0
  190. package/dist/internal/debug-draw.d.ts +9 -0
  191. package/dist/internal/debug-draw.d.ts.map +1 -0
  192. package/dist/internal/ecs-import.d.ts +6 -0
  193. package/dist/internal/ecs-import.d.ts.map +1 -0
  194. package/dist/internal/engine-profile-common.d.ts +9 -0
  195. package/dist/internal/engine-profile-common.d.ts.map +1 -0
  196. package/dist/internal/error-fanout.d.ts +66 -0
  197. package/dist/internal/error-fanout.d.ts.map +1 -0
  198. package/dist/internal/frame-loop.d.ts +38 -0
  199. package/dist/internal/frame-loop.d.ts.map +1 -0
  200. package/dist/internal/input-attach.d.ts +44 -0
  201. package/dist/internal/input-attach.d.ts.map +1 -0
  202. package/dist/internal/main-engine-profile.d.ts +19 -0
  203. package/dist/internal/main-engine-profile.d.ts.map +1 -0
  204. package/dist/internal/remote-serve-flag.d.ts +19 -0
  205. package/dist/internal/remote-serve-flag.d.ts.map +1 -0
  206. package/dist/internal/remote-server-plugin.d.ts +8 -0
  207. package/dist/internal/remote-server-plugin.d.ts.map +1 -0
  208. package/dist/internal/rhi-capture.d.ts +19 -0
  209. package/dist/internal/rhi-capture.d.ts.map +1 -0
  210. package/dist/internal/rhi-debug-flag.d.ts +18 -0
  211. package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
  212. package/dist/internal/worker-engine-profile.d.ts +15 -0
  213. package/dist/internal/worker-engine-profile.d.ts.map +1 -0
  214. package/dist/kernel-worker-runtime.mjs +69 -0
  215. package/dist/kernel-worker-runtime.mjs.map +1 -0
  216. package/dist/load-game-errors.d.ts +131 -0
  217. package/dist/load-game-errors.d.ts.map +1 -0
  218. package/dist/load-game.d.ts +34 -0
  219. package/dist/load-game.d.ts.map +1 -0
  220. package/dist/plugin-factories.d.ts +3 -0
  221. package/dist/plugin-factories.d.ts.map +1 -0
  222. package/dist/renderer-plugin.d.ts +25 -0
  223. package/dist/renderer-plugin.d.ts.map +1 -0
  224. package/dist/tool-preview/bootstrap.d.ts +113 -0
  225. package/dist/tool-preview/bootstrap.d.ts.map +1 -0
  226. package/dist/tool-preview/evidence.d.ts +20 -0
  227. package/dist/tool-preview/evidence.d.ts.map +1 -0
  228. package/dist/tool-preview/framing.d.ts +13 -0
  229. package/dist/tool-preview/framing.d.ts.map +1 -0
  230. package/dist/tool-preview/recipe.d.ts +54 -0
  231. package/dist/tool-preview/recipe.d.ts.map +1 -0
  232. package/dist/types.d.ts +404 -0
  233. package/dist/types.d.ts.map +1 -0
  234. package/package.json +85 -0
  235. package/schema/execution-report.schema.json +168 -0
  236. package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
  237. package/src/__tests__/callback-deletion.unit.test.ts +14 -0
  238. package/src/__tests__/callback-inference.test-d.ts +36 -0
  239. package/src/__tests__/component-introspection.unit.test.ts +56 -0
  240. package/src/__tests__/consumer-migration.unit.test.ts +34 -0
  241. package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
  242. package/src/__tests__/create-app-features.test.ts +51 -0
  243. package/src/__tests__/create-app-listener-sync.test.ts +381 -0
  244. package/src/__tests__/create-app-lock-provider.test.ts +212 -0
  245. package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
  246. package/src/__tests__/create-app-plugin.test.ts +217 -0
  247. package/src/__tests__/create-app-stop.test.ts +609 -0
  248. package/src/__tests__/create-app-time.unit.test.ts +26 -0
  249. package/src/__tests__/create-app.test-d.ts +83 -0
  250. package/src/__tests__/create-app.test.ts +287 -0
  251. package/src/__tests__/create-renderer-error.integration.test.ts +25 -0
  252. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +24 -0
  253. package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
  254. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +176 -0
  255. package/src/__tests__/draw-source-single-world-regression.test.ts +184 -0
  256. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +82 -0
  257. package/src/__tests__/engine-worker-world-swap.unit.test.ts +69 -0
  258. package/src/__tests__/errors-pointer-lock-failed.test.ts +265 -0
  259. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +79 -0
  260. package/src/__tests__/execution-error-detail.test-d.ts +13 -0
  261. package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
  262. package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
  263. package/src/__tests__/execution-frame-protocol.unit.test.ts +60 -0
  264. package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
  265. package/src/__tests__/execution-measurement.unit.test.ts +20 -0
  266. package/src/__tests__/execution-public-api.test-d.ts +14 -0
  267. package/src/__tests__/execution-public-api.unit.test.ts +9 -0
  268. package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
  269. package/src/__tests__/execution-report.contract.test.ts +50 -0
  270. package/src/__tests__/execution-report.types.test-d.ts +16 -0
  271. package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
  272. package/src/__tests__/execution-selector.unit.test.ts +72 -0
  273. package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
  274. package/src/__tests__/fallback-camera.unit.test.ts +32 -0
  275. package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
  276. package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
  277. package/src/__tests__/frame-loop-world-array.test.ts +450 -0
  278. package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
  279. package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
  280. package/src/__tests__/plugin-policy.integration.test.ts +15 -0
  281. package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
  282. package/src/__tests__/profiler-default-off.test.ts +53 -0
  283. package/src/__tests__/profiler-frame-token.test.ts +235 -0
  284. package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
  285. package/src/__tests__/profiler-phase-integration.test.ts +117 -0
  286. package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
  287. package/src/__tests__/remote-integration.test.ts +224 -0
  288. package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
  289. package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
  290. package/src/__tests__/rhi-capture.browser.test.ts +51 -0
  291. package/src/__tests__/surface-handoff.test.ts +89 -0
  292. package/src/__tests__/types-m2.test-d.ts +81 -0
  293. package/src/__tests__/visibility-remote.integration.test.ts +116 -0
  294. package/src/__tests__/worker-execution-stop.test.ts +173 -0
  295. package/src/animation-asset-lookup.ts +13 -0
  296. package/src/browser-frame-signal.ts +44 -0
  297. package/src/canvas-policy.ts +16 -0
  298. package/src/create-app.ts +1264 -0
  299. package/src/errors.ts +442 -0
  300. package/src/execution/attached-world-swap.ts +33 -0
  301. package/src/execution/bootstrap-entry.ts +122 -0
  302. package/src/execution/bootstrap-url.ts +25 -0
  303. package/src/execution/capabilities.ts +102 -0
  304. package/src/execution/control.ts +48 -0
  305. package/src/execution/engine-worker-runtime.ts +383 -0
  306. package/src/execution/engine-worker.ts +163 -0
  307. package/src/execution/host-controller.ts +460 -0
  308. package/src/execution/index.ts +44 -0
  309. package/src/execution/kernel-pool.ts +236 -0
  310. package/src/execution/kernel-worker-runtime.ts +103 -0
  311. package/src/execution/measurement.ts +39 -0
  312. package/src/execution/protocol.ts +134 -0
  313. package/src/execution/report.ts +66 -0
  314. package/src/execution/schema.ts +168 -0
  315. package/src/execution/selector.ts +98 -0
  316. package/src/execution/types.ts +163 -0
  317. package/src/fallback-camera.ts +20 -0
  318. package/src/game-context.ts +126 -0
  319. package/src/index.ts +190 -0
  320. package/src/input-map-plugin.ts +21 -0
  321. package/src/input-plugin.ts +26 -0
  322. package/src/internal/assembled-engine-profile.ts +14 -0
  323. package/src/internal/assets-world-plugin.ts +33 -0
  324. package/src/internal/browser-remote-bridge.ts +269 -0
  325. package/src/internal/browser-rhi-debug-runtime.ts +67 -0
  326. package/src/internal/component-introspection.ts +116 -0
  327. package/src/internal/debug-draw.ts +38 -0
  328. package/src/internal/ecs-import.ts +41 -0
  329. package/src/internal/engine-profile-common.ts +9 -0
  330. package/src/internal/error-fanout.ts +129 -0
  331. package/src/internal/frame-loop.ts +539 -0
  332. package/src/internal/input-attach.ts +104 -0
  333. package/src/internal/main-engine-profile.ts +84 -0
  334. package/src/internal/remote-serve-flag.ts +41 -0
  335. package/src/internal/remote-server-plugin.ts +16 -0
  336. package/src/internal/rhi-capture.ts +211 -0
  337. package/src/internal/rhi-debug-flag.ts +40 -0
  338. package/src/internal/worker-engine-profile.ts +47 -0
  339. package/src/load-game-errors.ts +211 -0
  340. package/src/load-game.ts +115 -0
  341. package/src/plugin-factories.ts +2 -0
  342. package/src/renderer-plugin.ts +96 -0
  343. package/src/tool-preview/bootstrap.ts +835 -0
  344. package/src/tool-preview/evidence.ts +59 -0
  345. package/src/tool-preview/framing.ts +52 -0
  346. package/src/tool-preview/recipe.ts +137 -0
  347. package/src/types.ts +450 -0
  348. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,83 @@
1
+ // create-app.test-d.ts -- compile-time assertions for w2 acceptanceCheck.
2
+ //
3
+ // Anchors:
4
+ // - createApp(canvas, opts?) returns
5
+ // Promise<Result<App, AppError | RhiError | EngineEnvironmentError>>
6
+ // per AC-01 (canvas thin wrapper widens the error union with
7
+ // EngineEnvironmentError; M2 plugin-system-unify D-7 widens it with
8
+ // AppError for Cordis activation failures).
9
+ // - createApp({ renderer, world }) returns
10
+ // Promise<Result<App, AppError | RhiError>>
11
+ // per AC-02 (assemble form excludes EngineEnvironmentError -- renderer
12
+ // host-managed).
13
+ // - 'tagName' in arg dispatch lands the right overload at the call site
14
+ // (HTMLCanvasElement -> canvas form; AppAssembleArgs -> assemble form).
15
+ //
16
+ // charter awareness:
17
+ // - P3 explicit failure: Result envelope shape is type-level (no message strings).
18
+ // - P4 consistent abstraction: both routes return the same App handle.
19
+
20
+ import type { World } from '@forgeax/engine-ecs';
21
+ import type { RenderError, Renderer, RendererOptions } from '@forgeax/engine-render';
22
+ import type { RhiError } from '@forgeax/engine-rhi/errors';
23
+ import { createRenderer, type EngineEnvironmentError } from '@forgeax/engine-runtime';
24
+ import type { Result } from '@forgeax/engine-types';
25
+ import { describe, expectTypeOf, it } from 'vitest';
26
+ import { type App, type AppError, createApp } from '../index';
27
+
28
+ describe('createApp double-SSOT entry signatures (w2 acceptanceCheck)', () => {
29
+ it('canvas form returns the App boundary error union', () => {
30
+ const canvas = null as unknown as HTMLCanvasElement;
31
+ const ret = createApp(canvas);
32
+ expectTypeOf(ret).toEqualTypeOf<
33
+ Promise<Result<App, AppError | RhiError | EngineEnvironmentError>>
34
+ >();
35
+ });
36
+
37
+ it('assemble form returns the App boundary error union', () => {
38
+ const renderer = null as unknown as Renderer;
39
+ const world = null as unknown as World;
40
+ const ret = createApp({ renderer, world });
41
+ expectTypeOf(ret).toEqualTypeOf<Promise<Result<App, AppError | RhiError>>>();
42
+ });
43
+
44
+ it('app.renderer / app.world are reference-equal types at the call site (AC-09)', async () => {
45
+ const renderer = null as unknown as Renderer;
46
+ const world = null as unknown as World;
47
+ const result = await createApp({ renderer, world });
48
+ if (result.ok) {
49
+ expectTypeOf(result.value.renderer).toEqualTypeOf<Renderer>();
50
+ expectTypeOf(result.value.world).toEqualTypeOf<World>();
51
+ }
52
+ });
53
+ });
54
+
55
+ describe('Runtime createRenderer Result boundary (AC-11)', () => {
56
+ it('requires callers to narrow the construction Result', () => {
57
+ const canvas = null as unknown as HTMLCanvasElement;
58
+ const expected = null as unknown as Promise<
59
+ Result<Renderer, EngineEnvironmentError | RenderError>
60
+ >;
61
+ expectTypeOf(createRenderer(canvas)).toEqualTypeOf(expected);
62
+ });
63
+ });
64
+
65
+ // feat-20260608-create-app-param-surface-trim / M1 / AC-02 + TASK-006:
66
+ // `RendererOptions.clearColor` was deleted in this feat. AI users that
67
+ // pass `{ clearColor: ... }` to `createRenderer` / `createApp` get a
68
+ // TS2353 excess-property error at compile time (no shim, no
69
+ // deprecation window per AGENTS.md §Change stance).
70
+ describe('RendererOptions.clearColor removal (AC-02 + TASK-006)', () => {
71
+ it('RendererOptions does not carry a `clearColor` field', () => {
72
+ // The keyof of RendererOptions must not include 'clearColor'.
73
+ type Keys = keyof RendererOptions;
74
+ type HasClearColor = 'clearColor' extends Keys ? true : false;
75
+ expectTypeOf<HasClearColor>().toEqualTypeOf<false>();
76
+ });
77
+
78
+ it('passing `clearColor` as an object literal is a compile-time error (TS2353)', () => {
79
+ // @ts-expect-error -- 'clearColor' is not assignable to RendererOptions
80
+ const opts: RendererOptions = { clearColor: [0, 0, 0, 1] };
81
+ void opts;
82
+ });
83
+ });
@@ -0,0 +1,287 @@
1
+ // M3 / w15: FORGEAX_ENGINE_RHI_DEBUG guard three-segment dual-source evaluation.
2
+ //
3
+ // The canvas-form guard in create-app.ts reads the flag from two sources with a
4
+ // precise precedence + short-circuit shape (plan-strategy D-4):
5
+ //
6
+ // (typeof import.meta !== 'undefined' && import.meta.env?.FORGEAX_ENGINE_RHI_DEBUG)
7
+ // ?? globalThis.process?.env?.FORGEAX_ENGINE_RHI_DEBUG
8
+ //
9
+ // That expression is extracted into the pure `resolveRhiDebugFlag` helper so the
10
+ // three deployment scenarios can be exercised without a real bundler / runtime:
11
+ // (a) browser: import.meta.env replaced by the vite define -> '1' wins
12
+ // (b) dawn-node: import.meta absent (modelled as undefined env) -> falls
13
+ // through to globalThis.process.env
14
+ // (c) unset: neither source carries '1' -> undefined -> guard does not fire
15
+ //
16
+ // C5 (plan-strategy): the `typeof import.meta !== 'undefined'` prefix must
17
+ // short-circuit before any `.env` access, because under dawn-node `import.meta`
18
+ // itself can be undefined; passing `undefined` for the import.meta.env arg is
19
+ // the unit-level model of that prefix evaluating to false.
20
+ //
21
+ // OOS-1/2: only the DevTools-trigger path is in scope; this test does not
22
+ // exercise external HTTP triggers or in-page buttons.
23
+
24
+ import { World } from '@forgeax/engine-ecs';
25
+ import { Camera, orthographic, perspective } from '@forgeax/engine-render';
26
+ import { describe, expect, it } from 'vitest';
27
+
28
+ import { syncCameraAspect, syncCanvasDrawingBuffer } from '../create-app';
29
+ import { resolveRhiDebugFlag } from '../internal/rhi-debug-flag';
30
+
31
+ describe('create-app.test.ts', () => {
32
+ describe('resolveRhiDebugFlag three-segment dual-source (w15 / D-4)', () => {
33
+ it('(a) browser: import.meta.env.FORGEAX_ENGINE_RHI_DEBUG === "1" wins', () => {
34
+ const flag = resolveRhiDebugFlag(
35
+ { FORGEAX_ENGINE_RHI_DEBUG: '1' },
36
+ { FORGEAX_ENGINE_RHI_DEBUG: undefined },
37
+ );
38
+ expect(flag).toBe('1');
39
+ });
40
+
41
+ it('(b) dawn-node: import.meta absent -> falls through to process.env', () => {
42
+ // `undefined` models `typeof import.meta === 'undefined'` short-circuiting
43
+ // the first operand to a falsy value, so the `??` chain consults process.env.
44
+ const flag = resolveRhiDebugFlag(undefined, { FORGEAX_ENGINE_RHI_DEBUG: '1' });
45
+ expect(flag).toBe('1');
46
+ });
47
+
48
+ it('(b2) import.meta.env present but flag unset -> still falls through to process.env', () => {
49
+ // `import.meta.env` exists (vite always injects it) but the key is absent;
50
+ // the first operand is `undefined`, so `??` consults process.env.
51
+ const flag = resolveRhiDebugFlag({}, { FORGEAX_ENGINE_RHI_DEBUG: '1' });
52
+ expect(flag).toBe('1');
53
+ });
54
+
55
+ it('(c) unset: neither source carries the flag -> undefined (guard does not fire)', () => {
56
+ expect(resolveRhiDebugFlag(undefined, undefined)).toBeUndefined();
57
+ expect(resolveRhiDebugFlag({}, {})).toBeUndefined();
58
+ expect(
59
+ resolveRhiDebugFlag(
60
+ { FORGEAX_ENGINE_RHI_DEBUG: undefined },
61
+ { FORGEAX_ENGINE_RHI_DEBUG: undefined },
62
+ ),
63
+ ).toBeUndefined();
64
+ });
65
+
66
+ it('import.meta.env wins over process.env when both set (precedence)', () => {
67
+ // Browser path takes precedence: the `??` only consults process.env when
68
+ // the first operand is null/undefined, and '1' is neither.
69
+ const flag = resolveRhiDebugFlag(
70
+ { FORGEAX_ENGINE_RHI_DEBUG: '1' },
71
+ { FORGEAX_ENGINE_RHI_DEBUG: '0' },
72
+ );
73
+ expect(flag).toBe('1');
74
+ });
75
+
76
+ it('a non-"1" value is returned verbatim (guard compares === "1" at call site)', () => {
77
+ // resolveRhiDebugFlag returns the raw flag; the create-app guard does the
78
+ // `=== '1'` comparison. A '0' value must therefore not be coerced to fire.
79
+ expect(resolveRhiDebugFlag({ FORGEAX_ENGINE_RHI_DEBUG: '0' }, undefined)).toBe('0');
80
+ });
81
+ });
82
+
83
+ describe('zero-injection when flag unset (c) (w15 / F-3)', () => {
84
+ it('globalThis.__forgeax is undefined before any flagged createApp runs', () => {
85
+ // With the flag unset (the default unit-test environment: no vite define,
86
+ // no process.env override), the canvas form never touches globalThis, so
87
+ // the capture entry point does not exist. A DevTools caller invoking it
88
+ // would hit a TypeError -- the explicit-failure contract (charter P3).
89
+ const g = globalThis as { __forgeax?: { captureFrame?: unknown } };
90
+ expect(g.__forgeax).toBeUndefined();
91
+ expect(() => {
92
+ // Reading .captureFrame off an undefined __forgeax throws synchronously.
93
+ (g.__forgeax as { captureFrame: (n: number) => unknown }).captureFrame(1);
94
+ }).toThrow(TypeError);
95
+ });
96
+ });
97
+
98
+ describe('syncCanvasDrawingBuffer host boundary', () => {
99
+ it('matches the physical drawing buffer to CSS size at the device pixel ratio', () => {
100
+ const canvas = {
101
+ clientWidth: 800,
102
+ clientHeight: 400,
103
+ width: 300,
104
+ height: 150,
105
+ style: { width: '800px', height: '400px' },
106
+ };
107
+ const before = globalThis.devicePixelRatio;
108
+ Object.defineProperty(globalThis, 'devicePixelRatio', { configurable: true, value: 2 });
109
+ try {
110
+ syncCanvasDrawingBuffer(canvas);
111
+ expect(canvas.width).toBe(1600);
112
+ expect(canvas.height).toBe(800);
113
+ } finally {
114
+ Object.defineProperty(globalThis, 'devicePixelRatio', {
115
+ configurable: true,
116
+ value: before,
117
+ });
118
+ }
119
+ });
120
+
121
+ it('leaves a zero-sized host untouched', () => {
122
+ const canvas = { clientWidth: 0, clientHeight: 400, width: 300, height: 150 };
123
+ syncCanvasDrawingBuffer(canvas);
124
+ expect(canvas.width).toBe(300);
125
+ expect(canvas.height).toBe(150);
126
+ });
127
+
128
+ it('leaves a headless canvas without CSS dimensions untouched', () => {
129
+ const canvas = { width: 320, height: 180 } as HTMLCanvasElement;
130
+ syncCanvasDrawingBuffer(canvas);
131
+ expect(canvas.width).toBe(320);
132
+ expect(canvas.height).toBe(180);
133
+ });
134
+
135
+ it('preserves an intrinsic canvas drawing buffer without CSS dimensions', () => {
136
+ let width = 256;
137
+ let height = 256;
138
+ const canvas = {
139
+ get clientWidth() {
140
+ return width;
141
+ },
142
+ get clientHeight() {
143
+ return height;
144
+ },
145
+ get width() {
146
+ return width;
147
+ },
148
+ set width(value: number) {
149
+ width = value;
150
+ },
151
+ get height() {
152
+ return height;
153
+ },
154
+ set height(value: number) {
155
+ height = value;
156
+ },
157
+ };
158
+ const before = globalThis.devicePixelRatio;
159
+ Object.defineProperty(globalThis, 'devicePixelRatio', { configurable: true, value: 2 });
160
+ try {
161
+ syncCanvasDrawingBuffer(canvas);
162
+ syncCanvasDrawingBuffer(canvas);
163
+ expect(width).toBe(256);
164
+ expect(height).toBe(256);
165
+ } finally {
166
+ Object.defineProperty(globalThis, 'devicePixelRatio', {
167
+ configurable: true,
168
+ value: before,
169
+ });
170
+ }
171
+ });
172
+
173
+ it('does not repeatedly scale a CSS-sized canvas when layout follows width writes', () => {
174
+ let width = 256;
175
+ let height = 256;
176
+ const canvas = {
177
+ clientWidth: 256,
178
+ clientHeight: 256,
179
+ style: { width: '256px', height: '256px' },
180
+ get width() {
181
+ return width;
182
+ },
183
+ set width(value: number) {
184
+ width = value;
185
+ canvas.clientWidth = value;
186
+ },
187
+ get height() {
188
+ return height;
189
+ },
190
+ set height(value: number) {
191
+ height = value;
192
+ canvas.clientHeight = value;
193
+ },
194
+ };
195
+ const before = globalThis.devicePixelRatio;
196
+ Object.defineProperty(globalThis, 'devicePixelRatio', { configurable: true, value: 2 });
197
+ try {
198
+ syncCanvasDrawingBuffer(canvas);
199
+ syncCanvasDrawingBuffer(canvas);
200
+ expect(width).toBe(512);
201
+ expect(height).toBe(512);
202
+ } finally {
203
+ Object.defineProperty(globalThis, 'devicePixelRatio', {
204
+ configurable: true,
205
+ value: before,
206
+ });
207
+ }
208
+ });
209
+ });
210
+
211
+ // feat-20260617 M3 / w10: aspect-sync sidecar (createApp path only).
212
+ // syncCameraAspect is the canvas-form per-frame Update system body
213
+ // closure calls with the live canvas width/height. It walks Camera entities via
214
+ // world.get (NOT the query bundle: a bool column read off the bundle returns a
215
+ // raw 0/1 number, so `!== 0` is always true -- the
216
+ // bool-field-compared-with-not-equal-zero-always-true trap; D-5/Finding 2). It
217
+ // writes canvas.width/height into Camera.aspect for perspective + autoAspect=true
218
+ // cameras only.
219
+ describe('syncCameraAspect sidecar (w10 / AC-07)', () => {
220
+ it('canvas resize -> perspective autoAspect=true camera aspect becomes w/h', () => {
221
+ const world = new World();
222
+ const cam = world
223
+ .spawn({ component: Camera, data: perspective({ fov: 1, aspect: 1 }) })
224
+ .unwrap();
225
+
226
+ syncCameraAspect(world, 800, 400);
227
+
228
+ const r = world.get(cam, Camera);
229
+ expect(r.ok).toBe(true);
230
+ if (!r.ok) return;
231
+ expect(r.value.aspect).toBeCloseTo(2, 5);
232
+ });
233
+
234
+ it('autoAspect=false camera is left untouched', () => {
235
+ const world = new World();
236
+ const cam = world
237
+ .spawn({
238
+ component: Camera,
239
+ data: perspective({ fov: 1, aspect: 1.234, autoAspect: false }),
240
+ })
241
+ .unwrap();
242
+
243
+ syncCameraAspect(world, 800, 400);
244
+
245
+ const r = world.get(cam, Camera);
246
+ expect(r.ok).toBe(true);
247
+ if (!r.ok) return;
248
+ expect(r.value.aspect).toBeCloseTo(1.234, 5);
249
+ });
250
+
251
+ it('canvas size 0 is skipped (no NaN / 0 written into aspect)', () => {
252
+ const world = new World();
253
+ const cam = world
254
+ .spawn({ component: Camera, data: perspective({ fov: 1, aspect: 1.5 }) })
255
+ .unwrap();
256
+
257
+ syncCameraAspect(world, 0, 0);
258
+
259
+ const r = world.get(cam, Camera);
260
+ expect(r.ok).toBe(true);
261
+ if (!r.ok) return;
262
+ expect(r.value.aspect).toBeCloseTo(1.5, 5);
263
+ expect(Number.isNaN(r.value.aspect)).toBe(false);
264
+ });
265
+
266
+ it('orthographic camera is left untouched', () => {
267
+ const world = new World();
268
+ const cam = world
269
+ .spawn({
270
+ component: Camera,
271
+ data: orthographic({ left: -1, right: 1, bottom: -1, top: 1 }),
272
+ })
273
+ .unwrap();
274
+ const before = world.get(cam, Camera);
275
+ expect(before.ok).toBe(true);
276
+ if (!before.ok) return;
277
+ const aspectBefore = before.value.aspect;
278
+
279
+ syncCameraAspect(world, 800, 400);
280
+
281
+ const r = world.get(cam, Camera);
282
+ expect(r.ok).toBe(true);
283
+ if (!r.ok) return;
284
+ expect(r.value.aspect).toBeCloseTo(aspectBefore, 5);
285
+ });
286
+ });
287
+ });
@@ -0,0 +1,25 @@
1
+ import { createRenderer, EngineEnvironmentError } from '@forgeax/engine-runtime';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createApp } from '../create-app';
4
+
5
+ describe('app renderer construction boundary', () => {
6
+ it('returns construction failures through the Runtime Result boundary', async () => {
7
+ const result = await createRenderer(null as never);
8
+ expect(result.ok).toBe(false);
9
+ });
10
+
11
+ it('returns EngineEnvironmentError through the canvas Result path', async () => {
12
+ const canvas = {
13
+ tagName: 'CANVAS',
14
+ isConnected: true,
15
+ width: 16,
16
+ height: 16,
17
+ getContext: () => null,
18
+ addEventListener: () => undefined,
19
+ removeEventListener: () => undefined,
20
+ } as unknown as HTMLCanvasElement;
21
+ const result = await createApp(canvas, { rhi: undefined });
22
+ expect(result.ok).toBe(false);
23
+ if (!result.ok) expect(result.error).toBeInstanceOf(EngineEnvironmentError);
24
+ });
25
+ });
@@ -0,0 +1,24 @@
1
+ import type { Renderer } from '@forgeax/engine-render';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('app-facing renderer lifecycle', () => {
5
+ it('consumes the three lifecycle Result boundaries without casts', async () => {
6
+ const events: string[] = [];
7
+ const renderer = {
8
+ attach: () => ({ ok: false as const, error: {} }),
9
+ draw: () => ({ ok: false as const, error: {} }),
10
+ observe: async () => ({ ok: false as const, error: {} }),
11
+ releaseSurface: () => ({ ok: true as const, value: undefined }),
12
+ restoreSurface: () => ({ ok: true as const, value: undefined }),
13
+ recover: async () => ({ ok: true as const, value: undefined }),
14
+ dispose: () => events.push('dispose'),
15
+ onError: () => () => undefined,
16
+ onLost: () => () => undefined,
17
+ } as unknown as Renderer;
18
+ expect(renderer.attach({} as never).ok).toBe(false);
19
+ expect(renderer.draw({} as never).ok).toBe(false);
20
+ renderer.dispose();
21
+ renderer.dispose();
22
+ expect(events).toEqual(['dispose', 'dispose']);
23
+ });
24
+ });
@@ -0,0 +1,61 @@
1
+ // @perf-budget-skip: intentional documentation subprocess gate.
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ const repoRoot = resolve(fileURLToPath(new URL('../../../..', import.meta.url)));
10
+ const docsGate = resolve(repoRoot, 'scripts/forgeax/check-token-first-docs.mjs');
11
+
12
+ function runDocsGate(): { readonly status: number; readonly output: string } {
13
+ try {
14
+ const output = execFileSync(process.execPath, [docsGate], {
15
+ cwd: repoRoot,
16
+ encoding: 'utf8',
17
+ stdio: ['ignore', 'pipe', 'pipe'],
18
+ });
19
+ return { status: 0, output };
20
+ } catch (error: unknown) {
21
+ const result = error as {
22
+ readonly status?: number;
23
+ readonly stdout?: string;
24
+ readonly stderr?: string;
25
+ };
26
+ return {
27
+ status: result.status ?? 1,
28
+ output: `${result.stdout ?? ''}${result.stderr ?? ''}`,
29
+ };
30
+ }
31
+ }
32
+
33
+ describe('M5 active-documentation token-first gate', () => {
34
+ it('keeps the engine-owned active documentation surface present', () => {
35
+ for (const path of [
36
+ 'packages/ecs/README.md',
37
+ 'packages/app/README.md',
38
+ 'packages/audio-webaudio/README.md',
39
+ 'packages/input/README.md',
40
+ 'packages/runtime/README.md',
41
+ 'packages/state/README.md',
42
+ 'skills/forgeax-engine-ecs/SKILL.md',
43
+ 'skills/forgeax-engine-app/SKILL.md',
44
+ 'skills/forgeax-engine-audio/SKILL.md',
45
+ 'skills/forgeax-engine-state/SKILL.md',
46
+ 'apps/hello/sprite-atlas/README.md',
47
+ 'apps/learn-render/1.getting-started/5.transformations/README.md',
48
+ 'apps/learn-render/1.getting-started/7.camera/README.md',
49
+ 'apps/learn-render/2.lighting/3.materials/README.md',
50
+ 'packages/ecs/CHANGELOG.md',
51
+ ]) {
52
+ expect(existsSync(resolve(repoRoot, path)), path).toBe(true);
53
+ }
54
+ });
55
+
56
+ it('accepts only token-first registration and explicit-delta update examples', () => {
57
+ const result = runDocsGate();
58
+ expect(result.status, result.output).toBe(0);
59
+ expect(result.output).toContain('Active-documentation token-first gate passed');
60
+ });
61
+ });
@@ -0,0 +1,176 @@
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 { createRenderReadLease } from '@forgeax/engine-ecs/projection';
38
+ import type { Renderer } from '@forgeax/engine-render';
39
+ import { registerPropagateTransforms, Transform } from '@forgeax/engine-scene';
40
+ import { describe, expect, it } from 'vitest';
41
+ import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
42
+
43
+ // Distinguishing translation for the injected world's entity. Column-major mat4
44
+ // carries translation at indices [12] (x), [13] (y), [14] (z).
45
+ const INJECTED_TX = 5;
46
+ const INJECTED_TY = -3;
47
+ const INJECTED_TZ = 7;
48
+
49
+ function makeSyncScheduler() {
50
+ let pending: ((t: number) => void) | null = null;
51
+ let clock = 0;
52
+ const raf = (cb: (t: number) => void): number => {
53
+ pending = cb;
54
+ return 1;
55
+ };
56
+ const caf = (): void => {
57
+ pending = null;
58
+ };
59
+ const now = (): number => {
60
+ clock += 16;
61
+ return clock;
62
+ };
63
+ const pump = (frames: number): void => {
64
+ for (let i = 0; i < frames; i++) {
65
+ const cb = pending;
66
+ pending = null;
67
+ if (cb === null) break;
68
+ cb(clock);
69
+ }
70
+ };
71
+ return { raf, caf, now, pump };
72
+ }
73
+
74
+ type DrawSourceCallback = () =>
75
+ | { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
76
+ | undefined;
77
+
78
+ type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
79
+ drawSource?: DrawSourceCallback;
80
+ };
81
+
82
+ const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
83
+ opts: FrameLoopOptionsWithDrawSource,
84
+ ) => ReturnType<typeof createFrameLoop>;
85
+
86
+ describe('drawSource injected worlds are updated same-frame (w9, stale-matrix regression)', () => {
87
+ it('renderer reads a freshly-propagated Transform.world for the injected world at draw time', () => {
88
+ // The loop's OWN world — the single-world identity path. Never carries the
89
+ // injected entity, so it cannot mask a stale read on the injected world.
90
+ const loopWorld = new World();
91
+
92
+ // The injected world: register propagateTransforms (the sole Transform.world
93
+ // writer) and spawn an entity with a distinguishing non-identity translation.
94
+ // We deliberately do NOT call injectedWorld.update() here — the frame-loop
95
+ // must run it for the translation to reach Transform.world.
96
+ const injectedWorld = new World();
97
+ registerPropagateTransforms(injectedWorld);
98
+ const spawned = injectedWorld
99
+ .spawn({
100
+ component: Transform,
101
+ data: {
102
+ pos: [INJECTED_TX, INJECTED_TY, INJECTED_TZ],
103
+ quat: [0, 0, 0, 1],
104
+ scale: [1, 1, 1],
105
+ },
106
+ })
107
+ .unwrap();
108
+ const entity = spawned as unknown as EntityHandle;
109
+
110
+ // Sanity: BEFORE any frame, Transform.world is still the identity default
111
+ // (translation columns are 0). This proves the assertion below measures the
112
+ // frame-loop's update, not a pre-propagated value.
113
+ const before = injectedWorld.get(entity, Transform);
114
+ expect(before.ok).toBe(true);
115
+ if (before.ok) {
116
+ const w = before.value.world as unknown as ArrayLike<number>;
117
+ expect(w[12]).toBe(0);
118
+ expect(w[13]).toBe(0);
119
+ expect(w[14]).toBe(0);
120
+ }
121
+
122
+ // Spy renderer: at draw time, read the injected world's Transform.world and
123
+ // record the translation columns. This is the exact read the real extract
124
+ // stage performs (Transform.world is the extract SSOT).
125
+ const observed: Array<[number, number, number]> = [];
126
+ const renderer = {
127
+ attach: (world: World) => ({ ok: true, value: createRenderReadLease(world) }),
128
+ draw(): { ok: true; value: undefined } {
129
+ const r = injectedWorld.get(entity, Transform);
130
+ if (r.ok) {
131
+ const m = r.value.world as unknown as ArrayLike<number>;
132
+ observed.push([m[12] as number, m[13] as number, m[14] as number]);
133
+ }
134
+ return { ok: true, value: undefined };
135
+ },
136
+ onError(): () => void {
137
+ return () => {
138
+ // no-op
139
+ };
140
+ },
141
+ dispose(): void {
142
+ // no-op
143
+ },
144
+ } as unknown as Renderer;
145
+
146
+ const drawSource: DrawSourceCallback = () => ({
147
+ worlds: [injectedWorld],
148
+ cameraOwner: 0,
149
+ resourceOwner: 0,
150
+ });
151
+
152
+ const { raf, caf, now, pump } = makeSyncScheduler();
153
+ const loop = createFrameLoopWithDrawSource({
154
+ world: loopWorld,
155
+ renderer,
156
+ now,
157
+ raf,
158
+ caf,
159
+ drawSource,
160
+ });
161
+
162
+ expect(loop.start().ok).toBe(true);
163
+ pump(2);
164
+ loop.stop();
165
+
166
+ // At least one frame observed the injected world, and every observation saw
167
+ // the FRESH translation — the frame-loop ran injectedWorld.update() before
168
+ // draw. A draw-only frame-loop would observe [0, 0, 0] (identity) and fail.
169
+ expect(observed.length).toBeGreaterThanOrEqual(1);
170
+ for (const [x, y, z] of observed) {
171
+ expect(x).toBeCloseTo(INJECTED_TX);
172
+ expect(y).toBeCloseTo(INJECTED_TY);
173
+ expect(z).toBeCloseTo(INJECTED_TZ);
174
+ }
175
+ });
176
+ });