@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,184 @@
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 { createRenderReadLease } from '@forgeax/engine-ecs/projection';
32
+ import type { Renderer, RenderFrameInput } from '@forgeax/engine-render';
33
+ import { describe, expect, it } from 'vitest';
34
+ import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
35
+
36
+ interface DrawCall {
37
+ readonly request: RenderFrameInput;
38
+ }
39
+
40
+ function makeSpyRenderer(): { renderer: Renderer; calls: DrawCall[] } {
41
+ const calls: DrawCall[] = [];
42
+ const renderer = {
43
+ attach: (world: World) => ({ ok: true, value: createRenderReadLease(world) }),
44
+ draw(request: RenderFrameInput): { ok: true; value: undefined } {
45
+ calls.push({ request });
46
+ return { ok: true, value: undefined };
47
+ },
48
+ onError(): () => void {
49
+ return () => {
50
+ // no-op unsubscribe
51
+ };
52
+ },
53
+ dispose(): void {
54
+ // no-op
55
+ },
56
+ } as unknown as Renderer;
57
+ return { renderer, calls };
58
+ }
59
+
60
+ /**
61
+ * Synchronous rAF driver: fire the scheduled tick exactly `frames` times.
62
+ * Mirrors frame-loop-world-array.test.ts so both single-world contract tests
63
+ * step the loop identically.
64
+ */
65
+ function makeSyncScheduler() {
66
+ let pending: ((t: number) => void) | null = null;
67
+ let clock = 0;
68
+ const raf = (cb: (t: number) => void): number => {
69
+ pending = cb;
70
+ return 1;
71
+ };
72
+ const caf = (): void => {
73
+ pending = null;
74
+ };
75
+ const now = (): number => {
76
+ clock += 16;
77
+ return clock;
78
+ };
79
+ const pump = (frames: number): void => {
80
+ for (let i = 0; i < frames; i++) {
81
+ const cb = pending;
82
+ pending = null;
83
+ if (cb === null) break;
84
+ cb(clock);
85
+ }
86
+ };
87
+ return { raf, caf, now, pump };
88
+ }
89
+
90
+ // The drawSource pull callback shape w11/w12 will introduce on FrameLoopOptions.
91
+ // Declared locally so this test states the contract independent of the (not-yet
92
+ // widened) source type. `undefined` return = "no multi-world injection this
93
+ // frame" (degrade to single world).
94
+ type DrawSourceCallback = () =>
95
+ | { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
96
+ | undefined;
97
+
98
+ type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
99
+ drawSource?: DrawSourceCallback;
100
+ };
101
+
102
+ const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
103
+ opts: FrameLoopOptionsWithDrawSource,
104
+ ) => ReturnType<typeof createFrameLoop>;
105
+
106
+ describe('drawSource seam degrades to single-world path (w8, AC-03 regression)', () => {
107
+ it('no drawSource: draws the primary World with explicit camera and resource owners', () => {
108
+ const world = new World();
109
+ const { renderer, calls } = makeSpyRenderer();
110
+ const { raf, caf, now, pump } = makeSyncScheduler();
111
+
112
+ // No drawSource field at all — the single-world AI-user default.
113
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
114
+ expect(loop.start().ok).toBe(true);
115
+ pump(3);
116
+
117
+ expect(calls.length).toBeGreaterThanOrEqual(3);
118
+ for (const call of calls) {
119
+ expect(call.request.leases).toHaveLength(1);
120
+ expect(call.request.leases[0]?.worldIdentity).toBe(world.identity);
121
+ expect(call.request.camera.lease).toBe(call.request.leases[0]);
122
+ expect(call.request.environment.lease).toBe(call.request.leases[0]);
123
+ }
124
+
125
+ loop.stop();
126
+ });
127
+
128
+ it('drawSource returns undefined: still draws [world] with { cameraOwner: 0, resourceOwner: 0 } (no-inject frame)', () => {
129
+ const world = new World();
130
+ const { renderer, calls } = makeSpyRenderer();
131
+ const { raf, caf, now, pump } = makeSyncScheduler();
132
+
133
+ let pulls = 0;
134
+ const drawSource: DrawSourceCallback = () => {
135
+ pulls++;
136
+ // Host wired the seam but has nothing multi-world to inject this frame.
137
+ return undefined;
138
+ };
139
+
140
+ const loop = createFrameLoopWithDrawSource({ world, renderer, now, raf, caf, drawSource });
141
+ expect(loop.start().ok).toBe(true);
142
+ pump(3);
143
+
144
+ // The seam was consulted (pull callback fired) but the resulting draw is
145
+ // identical to the primary-World call.
146
+ expect(pulls).toBeGreaterThanOrEqual(3);
147
+ expect(calls.length).toBeGreaterThanOrEqual(3);
148
+ for (const call of calls) {
149
+ expect(call.request.leases).toHaveLength(1);
150
+ expect(call.request.leases[0]?.worldIdentity).toBe(world.identity);
151
+ expect(call.request.camera.lease).toBe(call.request.leases[0]);
152
+ expect(call.request.environment.lease).toBe(call.request.leases[0]);
153
+ }
154
+
155
+ loop.stop();
156
+ });
157
+
158
+ it('setDrawSource switches routing without creating a second frame loop', () => {
159
+ const world = new World();
160
+ const overlay = new World();
161
+ const { renderer, calls } = makeSpyRenderer();
162
+ const { raf, caf, now, pump } = makeSyncScheduler();
163
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
164
+
165
+ expect(loop.start().ok).toBe(true);
166
+ pump(1);
167
+ loop.setDrawSource(() => ({ worlds: [world, overlay], cameraOwner: 0, resourceOwner: 0 }));
168
+ pump(1);
169
+ loop.setDrawSource(undefined);
170
+ pump(1);
171
+
172
+ expect(calls).toHaveLength(3);
173
+ expect(calls[0]?.request.leases).toHaveLength(1);
174
+ expect(calls[0]?.request.leases[0]?.worldIdentity).toBe(world.identity);
175
+ expect(calls[1]?.request.leases).toHaveLength(2);
176
+ expect(calls[1]?.request.leases[0]?.worldIdentity).toBe(world.identity);
177
+ expect(calls[1]?.request.leases[1]?.worldIdentity).toBe(overlay.identity);
178
+ expect(calls[1]?.request.camera.lease).toBe(calls[1]?.request.leases[0]);
179
+ expect(calls[1]?.request.environment.lease).toBe(calls[1]?.request.leases[0]);
180
+ expect(calls[2]?.request.leases).toHaveLength(1);
181
+ expect(calls[2]?.request.leases[0]?.worldIdentity).toBe(world.identity);
182
+ loop.stop();
183
+ });
184
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { startEngineWorker } from '../execution/engine-worker';
3
+ import type { EngineToHostMessage, HostToEngineMessage } from '../execution/protocol';
4
+
5
+ class FakeWorker {
6
+ onmessage: ((event: MessageEvent<EngineToHostMessage>) => void) | null = null;
7
+ onerror: ((event: ErrorEvent) => void) | null = null;
8
+ readonly posts: Array<{
9
+ readonly message: HostToEngineMessage;
10
+ readonly transfer: readonly Transferable[];
11
+ }> = [];
12
+ readonly terminate = vi.fn();
13
+
14
+ postMessage(message: HostToEngineMessage, transfer: Transferable[] = []): void {
15
+ this.posts.push({ message, transfer });
16
+ if (message.kind !== 'init') return;
17
+ queueMicrotask(() => {
18
+ this.onmessage?.({
19
+ data: {
20
+ kind: 'ready',
21
+ worldIdentity: 'world-test',
22
+ realm: 'worker',
23
+ workerWebGpu: true,
24
+ },
25
+ } as MessageEvent<EngineToHostMessage>);
26
+ });
27
+ }
28
+ }
29
+
30
+ describe('Engine Worker thick bootstrap channel', () => {
31
+ it('transfers bootstrap data and the realm MessagePort with the canvas', async () => {
32
+ const worker = new FakeWorker();
33
+ const offscreen = {} as OffscreenCanvas;
34
+ const transferControlToOffscreen = vi.fn(() => offscreen);
35
+ const channel = new MessageChannel();
36
+ const started = await startEngineWorker({
37
+ canvas: { transferControlToOffscreen } as unknown as HTMLCanvasElement,
38
+ bootstrapUrl: 'https://example.test/bootstrap.js',
39
+ bootstrapData: { gameId: 'sample' },
40
+ bootstrapPort: channel.port2,
41
+ assetCatalog: {
42
+ url: '/__pack/scopes/sample/7/catalog.json',
43
+ expectedScope: { scopeId: 'sample', generation: 7 },
44
+ },
45
+ timeoutMs: 100,
46
+ tier: 'engine-worker',
47
+ workerFactory: () => worker as unknown as Worker,
48
+ });
49
+
50
+ expect(started.ok).toBe(true);
51
+ expect(transferControlToOffscreen).toHaveBeenCalledOnce();
52
+ expect(worker.posts[0]?.message).toMatchObject({
53
+ kind: 'init',
54
+ bootstrapData: { gameId: 'sample' },
55
+ bootstrapPort: channel.port2,
56
+ assetCatalog: {
57
+ url: '/__pack/scopes/sample/7/catalog.json',
58
+ expectedScope: { scopeId: 'sample', generation: 7 },
59
+ },
60
+ });
61
+ expect(worker.posts[0]?.transfer).toEqual([offscreen, channel.port2]);
62
+ started.ok && started.value.dispose();
63
+ channel.port1.close();
64
+ });
65
+
66
+ it('rejects non-cloneable data before Worker creation or canvas transfer', async () => {
67
+ const workerFactory = vi.fn();
68
+ const transferControlToOffscreen = vi.fn();
69
+ const started = await startEngineWorker({
70
+ canvas: { transferControlToOffscreen } as unknown as HTMLCanvasElement,
71
+ bootstrapUrl: 'https://example.test/bootstrap.js',
72
+ bootstrapData: { callback: (() => {}) as never },
73
+ timeoutMs: 100,
74
+ tier: 'engine-worker',
75
+ workerFactory,
76
+ });
77
+
78
+ expect(started.ok).toBe(false);
79
+ expect(workerFactory).not.toHaveBeenCalled();
80
+ expect(transferControlToOffscreen).not.toHaveBeenCalled();
81
+ });
82
+ });
@@ -0,0 +1,69 @@
1
+ import { World } from '@forgeax/engine-ecs';
2
+ import type { Renderer, RenderWorldLease } from '@forgeax/engine-render';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { commitAttachedWorld, SerializedRebuildQueue } from '../execution/attached-world-swap';
5
+
6
+ function rendererOwner(): Pick<Renderer, 'attach'> {
7
+ const lease = {
8
+ worldIdentity: {},
9
+ generation: 0,
10
+ readChanges: vi.fn(),
11
+ querySpans: vi.fn(),
12
+ inspectCursor: vi.fn(),
13
+ dispose: vi.fn(),
14
+ } as unknown as RenderWorldLease;
15
+ return {
16
+ attach: vi.fn(() => ({ ok: true as const, value: lease })),
17
+ };
18
+ }
19
+
20
+ describe('worker attached World swap', () => {
21
+ it('serializes concurrent rebuild requests', async () => {
22
+ const queue = new SerializedRebuildQueue();
23
+ const trace: string[] = [];
24
+ let releaseFirst: (() => void) | undefined;
25
+ const first = queue.enqueue(async () => {
26
+ trace.push('first:start');
27
+ await new Promise<void>((resolve) => {
28
+ releaseFirst = resolve;
29
+ });
30
+ trace.push('first:end');
31
+ });
32
+ const second = queue.enqueue(async () => {
33
+ trace.push('second:start');
34
+ trace.push('second:end');
35
+ });
36
+
37
+ await Promise.resolve();
38
+ expect(trace).toEqual(['first:start']);
39
+ releaseFirst?.();
40
+ await Promise.all([first, second]);
41
+ expect(trace).toEqual(['first:start', 'first:end', 'second:start', 'second:end']);
42
+ });
43
+
44
+ it('publishes the candidate before detaching the previous World', async () => {
45
+ const renderer = rendererOwner();
46
+ const next = new World();
47
+
48
+ await expect(commitAttachedWorld(renderer, next, async () => true)).resolves.toBe(true);
49
+ expect(renderer.attach).toHaveBeenCalledWith(next);
50
+ });
51
+
52
+ it('detaches a candidate whose bootstrap fails and keeps the previous World', async () => {
53
+ const renderer = rendererOwner();
54
+ const next = new World();
55
+
56
+ await expect(commitAttachedWorld(renderer, next, async () => false)).resolves.toBe(false);
57
+ });
58
+
59
+ it('detaches a candidate whose bootstrap throws', async () => {
60
+ const renderer = rendererOwner();
61
+ const next = new World();
62
+
63
+ await expect(
64
+ commitAttachedWorld(renderer, next, async () => {
65
+ throw new Error('bootstrap failed');
66
+ }),
67
+ ).rejects.toThrow('bootstrap failed');
68
+ });
69
+ });
@@ -0,0 +1,265 @@
1
+ // errors-pointer-lock-failed.test.ts -- M2 w10: AppErrorCode closed union
2
+ // (6 members) + 'app-pointer-lock-failed' detail shape + exhaustive switch
3
+ // + APP_EXPECTED / APP_ERROR_HINTS bidirectional symmetry.
4
+ //
5
+ // TDD red phase: this test is written BEFORE the 'app-pointer-lock-failed'
6
+ // member lands in errors.ts (w12). It will fail typecheck / assertions until
7
+ // w12 expands the union and adds the detail variant.
8
+ //
9
+ // charter awareness:
10
+ // P3 explicit failure -- every assertion produces a clear diagnostic
11
+ // P4 closed-union exhaustive switch -- zero default branch, tsc guards completeness
12
+
13
+ import { describe, expect, it } from 'vitest';
14
+ import {
15
+ APP_ERROR_HINTS,
16
+ APP_EXPECTED,
17
+ AppError,
18
+ type AppErrorCode,
19
+ type AppErrorDetail,
20
+ type AppErrorDetailFor,
21
+ isAppError,
22
+ } from '../errors';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // 1. AppErrorCode closed union
26
+ // ---------------------------------------------------------------------------
27
+
28
+ describe('AppErrorCode closed union', () => {
29
+ it('has every source-owned member', () => {
30
+ // Verify the full set of members. When 'app-pointer-lock-failed' is added
31
+ // in w12, this set will be complete and the exhaustive-switch check below
32
+ // will pass tsc with no default branch.
33
+ const allCodes: AppErrorCode[] = [
34
+ 'app-not-started',
35
+ 'app-already-running',
36
+ 'app-canvas-detached',
37
+ 'app-frame-step-invalid',
38
+ 'app-system-update-failed',
39
+ 'app-pointer-lock-failed',
40
+ 'app-plugin-activation-failed',
41
+ 'app-execution-tier-unavailable',
42
+ 'app-execution-bootstrap-failed',
43
+ 'app-execution-deadline-exceeded',
44
+ 'app-execution-kernel-failed',
45
+ 'app-execution-stale-world',
46
+ 'app-execution-rebuild-failed',
47
+ ];
48
+
49
+ expect(allCodes).toHaveLength(13);
50
+
51
+ // Each member must be unique.
52
+ const unique = new Set(allCodes);
53
+ expect(unique.size).toBe(13);
54
+ });
55
+
56
+ it('exhaustive switch over AppErrorCode has zero default branch', () => {
57
+ // This function MUST compile with no default. If a member is missing from
58
+ // the union, tsc will flag the switch as non-exhaustive. If a member is
59
+ // removed from the union but left here, tsc will flag the dead case.
60
+ // The test assertion itself is trivially true -- the test is the
61
+ // compile-time guarantee.
62
+ function exhaust(code: AppErrorCode): string {
63
+ switch (code) {
64
+ case 'app-not-started':
65
+ return 'not started';
66
+ case 'app-already-running':
67
+ return 'already running';
68
+ case 'app-canvas-detached':
69
+ return 'canvas detached';
70
+ case 'app-frame-step-invalid':
71
+ return 'frame step invalid';
72
+ case 'app-system-update-failed':
73
+ return 'system update failed';
74
+ case 'app-pointer-lock-failed':
75
+ return 'pointer lock failed';
76
+ case 'app-plugin-activation-failed':
77
+ return 'plugin activation failed';
78
+ case 'app-execution-tier-unavailable':
79
+ case 'app-execution-bootstrap-failed':
80
+ case 'app-execution-deadline-exceeded':
81
+ case 'app-execution-kernel-failed':
82
+ case 'app-execution-stale-world':
83
+ case 'app-execution-rebuild-failed':
84
+ return 'execution';
85
+ }
86
+ }
87
+
88
+ // Verify the function is callable for representative codes.
89
+ expect(exhaust('app-not-started')).toBe('not started');
90
+ expect(exhaust('app-already-running')).toBe('already running');
91
+ expect(exhaust('app-canvas-detached')).toBe('canvas detached');
92
+ expect(exhaust('app-system-update-failed')).toBe('system update failed');
93
+ expect(exhaust('app-pointer-lock-failed')).toBe('pointer lock failed');
94
+ });
95
+ });
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // 2. 'app-pointer-lock-failed' detail shape
99
+ // ---------------------------------------------------------------------------
100
+
101
+ describe("'app-pointer-lock-failed' detail shape", () => {
102
+ it('has .code === "app-pointer-lock-failed"', () => {
103
+ const err = new AppError({
104
+ code: 'app-pointer-lock-failed',
105
+ expected: 'pointer-lock request to succeed',
106
+ hint: 'remain in unlocked state; next trusted click will retry',
107
+ detail: { path: 'w3c', cause: new Error('test') },
108
+ });
109
+ expect(err.code).toBe('app-pointer-lock-failed');
110
+ });
111
+
112
+ it('detail.path narrows to "w3c" | "provider"', () => {
113
+ const w3cErr = new AppError({
114
+ code: 'app-pointer-lock-failed',
115
+ expected: 'pointer-lock request to succeed',
116
+ hint: 'remain in unlocked state; next trusted click will retry',
117
+ detail: { path: 'w3c', cause: new DOMException('test', 'NotAllowedError') },
118
+ });
119
+ expect(w3cErr.detail.path).toBe('w3c');
120
+
121
+ const provErr = new AppError({
122
+ code: 'app-pointer-lock-failed',
123
+ expected: 'pointer-lock request to succeed',
124
+ hint: 'remain in unlocked state; next trusted click will retry',
125
+ detail: { path: 'provider', cause: new Error('provider reject') },
126
+ });
127
+ expect(provErr.detail.path).toBe('provider');
128
+ });
129
+
130
+ it('detail.cause carries the original rejection value verbatim', () => {
131
+ const cause = new Error('W3C lock rejected');
132
+ const err = new AppError({
133
+ code: 'app-pointer-lock-failed',
134
+ expected: 'pointer-lock request to succeed',
135
+ hint: 'remain in unlocked state; next trusted click will retry',
136
+ detail: { path: 'w3c', cause },
137
+ });
138
+ expect(err.detail.cause).toBe(cause);
139
+ });
140
+
141
+ it('isAppError returns true for pointer-lock-failed errors', () => {
142
+ const err = new AppError({
143
+ code: 'app-pointer-lock-failed',
144
+ expected: 'pointer-lock request to succeed',
145
+ hint: 'remain in unlocked state; next trusted click will retry',
146
+ detail: { path: 'w3c', cause: new Error('test') },
147
+ });
148
+ expect(isAppError(err)).toBe(true);
149
+ });
150
+
151
+ it('message carries path and cause summary', () => {
152
+ const err = new AppError({
153
+ code: 'app-pointer-lock-failed',
154
+ expected: 'pointer-lock request to succeed',
155
+ hint: 'remain in unlocked state; next trusted click will retry',
156
+ detail: { path: 'w3c', cause: new Error('NotAllowedError') },
157
+ });
158
+ expect(err.message).toContain('app-pointer-lock-failed');
159
+ expect(err.message).toContain('NotAllowedError');
160
+ });
161
+
162
+ it('narrows via discriminated union: if (err.code === "app-pointer-lock-failed")', () => {
163
+ const err = new AppError({
164
+ code: 'app-pointer-lock-failed',
165
+ expected: 'pointer-lock request to succeed',
166
+ hint: 'remain in unlocked state; next trusted click will retry',
167
+ detail: { path: 'w3c', cause: new Error('test') },
168
+ }) as AppError;
169
+
170
+ if (err.code === 'app-pointer-lock-failed') {
171
+ // detail should narrow to { path: 'w3c' | 'provider'; cause: unknown }
172
+ const { path, cause } = err.detail;
173
+ expect(path).toBe('w3c');
174
+ expect(cause).toBeInstanceOf(Error);
175
+ } else {
176
+ // Should not reach here for this test.
177
+ expect.unreachable('expected code to be app-pointer-lock-failed');
178
+ }
179
+ });
180
+ });
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // 3. APP_EXPECTED / APP_ERROR_HINTS bidirectional symmetry (6 keys)
184
+ // ---------------------------------------------------------------------------
185
+
186
+ describe('APP_EXPECTED / APP_ERROR_HINTS bidirectional symmetry', () => {
187
+ it('APP_EXPECTED has one key per AppErrorCode member', () => {
188
+ const keys = Object.keys(APP_EXPECTED);
189
+ expect(keys).toHaveLength(13);
190
+ });
191
+
192
+ it('APP_ERROR_HINTS has one key per AppErrorCode member', () => {
193
+ const keys = Object.keys(APP_ERROR_HINTS);
194
+ expect(keys).toHaveLength(13);
195
+ });
196
+
197
+ it('APP_EXPECTED and APP_ERROR_HINTS have the same key set', () => {
198
+ const expectedKeys = Object.keys(APP_EXPECTED).sort();
199
+ const hintKeys = Object.keys(APP_ERROR_HINTS).sort();
200
+ expect(expectedKeys).toEqual(hintKeys);
201
+ });
202
+
203
+ it('every APP_EXPECTED entry is non-empty', () => {
204
+ for (const [key, value] of Object.entries(APP_EXPECTED)) {
205
+ expect(value, `APP_EXPECTED[${key}] must be non-empty`).toBeTruthy();
206
+ expect(value.length, `APP_EXPECTED[${key}] must be non-empty`).toBeGreaterThan(0);
207
+ }
208
+ });
209
+
210
+ it('every APP_ERROR_HINTS entry is non-empty', () => {
211
+ for (const [key, value] of Object.entries(APP_ERROR_HINTS)) {
212
+ expect(value, `APP_ERROR_HINTS[${key}] must be non-empty`).toBeTruthy();
213
+ expect(value.length, `APP_ERROR_HINTS[${key}] must be non-empty`).toBeGreaterThan(0);
214
+ }
215
+ });
216
+
217
+ it('app-pointer-lock-failed has expected and hint entries', () => {
218
+ expect(APP_EXPECTED).toHaveProperty('app-pointer-lock-failed');
219
+ expect(APP_ERROR_HINTS).toHaveProperty('app-pointer-lock-failed');
220
+ expect(APP_EXPECTED['app-pointer-lock-failed'].length).toBeGreaterThan(0);
221
+ expect(APP_ERROR_HINTS['app-pointer-lock-failed'].length).toBeGreaterThan(0);
222
+ });
223
+ });
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // 4. AppErrorDetail union includes the new variant
227
+ // ---------------------------------------------------------------------------
228
+
229
+ describe('AppErrorDetail union', () => {
230
+ it('accepts the pointer-lock-failed detail variant', () => {
231
+ const detail: AppErrorDetail = {
232
+ path: 'w3c',
233
+ cause: new Error('test'),
234
+ };
235
+ expect(detail.path).toBe('w3c');
236
+ expect(detail.cause).toBeInstanceOf(Error);
237
+ });
238
+
239
+ it('accepts the provider path variant', () => {
240
+ const detail: AppErrorDetail = {
241
+ path: 'provider',
242
+ cause: 'provider string rejection',
243
+ };
244
+ // cause is unknown, so string is valid.
245
+ expect(detail.path).toBe('provider');
246
+ expect(typeof detail.cause).toBe('string');
247
+ });
248
+ });
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // 5. AppErrorDetailFor conditional type narrows correctly
252
+ // ---------------------------------------------------------------------------
253
+
254
+ describe('AppErrorDetailFor conditional type', () => {
255
+ it('narrows "app-pointer-lock-failed" to { path, cause }', () => {
256
+ // This is a compile-time assertion: if AppErrorDetailFor<'app-pointer-lock-failed'>
257
+ // does not resolve to { path: 'w3c' | 'provider'; cause: unknown }, tsc will fail.
258
+ const detail: AppErrorDetailFor<'app-pointer-lock-failed'> = {
259
+ path: 'w3c',
260
+ cause: new Error('test'),
261
+ };
262
+ expect(detail.path).toBe('w3c');
263
+ expect(detail.cause).toBeInstanceOf(Error);
264
+ });
265
+ });
@@ -0,0 +1,79 @@
1
+ import { createWorldContext, World } from '@forgeax/engine-ecs';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import {
4
+ executionBootstrapHostPlugin,
5
+ loadBootstrapEntry,
6
+ prepareBootstrapEntry,
7
+ validateExecutionBootstrapData,
8
+ } from '../execution';
9
+
10
+ function moduleUrl(source: string): string {
11
+ return `data:text/javascript,${encodeURIComponent(source)}`;
12
+ }
13
+
14
+ describe('execution bootstrap isolation', () => {
15
+ it('activates realm-local bootstrap plugins and disposes their effects', async () => {
16
+ const url = moduleUrl(`export default function(data){return {plugins:[{
17
+ name:'test-bootstrap',inject:['world','executionBootstrapHost'],apply(ctx){
18
+ ctx.world.insertResource('bootstrapped',data.value);
19
+ ctx.effect(()=>()=>ctx.world.insertResource('cleaned',true),'test/cleanup');
20
+ ctx.executionBootstrapHost.setPointerLockAllowed(false);
21
+ }
22
+ }]}}`);
23
+ const prepared = await prepareBootstrapEntry(url, { value: true });
24
+ expect(prepared.ok).toBe(true);
25
+ if (!prepared.ok) return;
26
+ const world = new World();
27
+ const pointerLock = vi.fn();
28
+ const close = vi.fn();
29
+ const context = await createWorldContext(world, [
30
+ executionBootstrapHostPlugin({
31
+ port: { close } as unknown as MessagePort,
32
+ setPointerLockAllowed: pointerLock,
33
+ }),
34
+ ...(prepared.value.plugins ?? []),
35
+ ]);
36
+ expect(world.getResource('bootstrapped')).toBe(true);
37
+ expect(pointerLock).toHaveBeenCalledWith(false);
38
+ await context.fiber.dispose();
39
+ expect(world.getResource('cleaned')).toBe(true);
40
+ expect(close).toHaveBeenCalledOnce();
41
+ });
42
+
43
+ it('keeps schedule identity in the realm-local World', async () => {
44
+ const url = moduleUrl(`export default function(){return {plugins:[{
45
+ name:'module-system',inject:['world'],apply(ctx){
46
+ const system={name:'module-system',queries:[],fn(){}};
47
+ ctx.world.addSystem(ctx.world.scheduleToken('Update'),system).unwrap();
48
+ ctx.effect(()=>()=>ctx.world.removeSystem(ctx.world.scheduleToken('Update'),system.name),'test/system');
49
+ }
50
+ }]}}`);
51
+ const prepared = await prepareBootstrapEntry(url, undefined);
52
+ expect(prepared.ok).toBe(true);
53
+ if (!prepared.ok) return;
54
+ const world = new World();
55
+ const context = await createWorldContext(world, prepared.value.plugins ?? []);
56
+ expect(world.inspect().scheduleSystemCount(world.scheduleToken('Update'))).toBe(1);
57
+ await context.fiber.dispose();
58
+ expect(world.inspect().scheduleSystemCount(world.scheduleToken('Update'))).toBe(0);
59
+ });
60
+
61
+ it('rejects a missing default export structurally', async () => {
62
+ const result = await loadBootstrapEntry('data:text/javascript,export const value=1');
63
+ expect(result.ok).toBe(false);
64
+ if (!result.ok && result.error.code === 'app-execution-bootstrap-failed') {
65
+ expect(result.error.detail.phase).toBe('export');
66
+ }
67
+ });
68
+
69
+ it('rejects non-cloneable bootstrap data before canvas transfer', () => {
70
+ const result = validateExecutionBootstrapData(
71
+ { callback: (() => {}) as never },
72
+ 'https://example.test/bootstrap.js',
73
+ );
74
+ expect(result.ok).toBe(false);
75
+ if (!result.ok && result.error.code === 'app-execution-bootstrap-failed') {
76
+ expect(result.error.detail.phase).toBe('data');
77
+ }
78
+ });
79
+ });