@forgeax/engine-app 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +418 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
  5. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/app-rebuild.test.d.ts +2 -0
  7. package/dist/__tests__/app-rebuild.test.d.ts.map +1 -0
  8. package/dist/__tests__/asset-registry-types.test-d.d.ts +2 -0
  9. package/dist/__tests__/asset-registry-types.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +2 -0
  11. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +1 -0
  12. package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
  13. package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
  15. package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
  17. package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
  19. package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/create-app-features.test.d.ts +2 -0
  21. package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
  22. package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
  23. package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
  24. package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
  25. package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
  26. package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
  27. package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
  28. package/dist/__tests__/create-app-stop.test.d.ts +2 -0
  29. package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
  30. package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
  31. package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/create-app.test-d.d.ts +2 -0
  33. package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
  34. package/dist/__tests__/create-app.test.d.ts +2 -0
  35. package/dist/__tests__/create-app.test.d.ts.map +1 -0
  36. package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
  37. package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
  39. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
  40. package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
  41. package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +2 -0
  43. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +1 -0
  44. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
  45. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
  46. package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
  47. package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
  48. package/dist/__tests__/ecs-import.unit.test.d.ts +2 -0
  49. package/dist/__tests__/ecs-import.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
  51. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
  53. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
  55. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
  56. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
  57. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
  59. package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
  60. package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
  61. package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
  63. package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
  65. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
  67. package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
  68. package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
  69. package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
  71. package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
  72. package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
  73. package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
  75. package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
  76. package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
  77. package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
  78. package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
  79. package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
  80. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
  81. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
  83. package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
  85. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/execution-world-health-owner.test-d.d.ts +2 -0
  87. package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +1 -0
  88. package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
  89. package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
  91. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
  92. package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
  93. package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
  94. package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
  95. package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
  96. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
  97. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
  99. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
  100. package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
  101. package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
  102. package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
  103. package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
  104. package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
  105. package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
  106. package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
  107. package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
  108. package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
  109. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
  110. package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
  111. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
  112. package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
  113. package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
  114. package/dist/__tests__/remote-integration.test.d.ts +2 -0
  115. package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
  116. package/dist/__tests__/remote-serve-flag.test.d.ts +2 -0
  117. package/dist/__tests__/remote-serve-flag.test.d.ts.map +1 -0
  118. package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
  119. package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
  120. package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
  121. package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
  122. package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
  123. package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
  124. package/dist/__tests__/surface-handoff.test.d.ts +2 -0
  125. package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
  126. package/dist/__tests__/types-m2.test-d.d.ts +2 -0
  127. package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
  128. package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
  129. package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
  130. package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
  131. package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
  132. package/dist/animation-asset-lookup.d.ts +8 -0
  133. package/dist/animation-asset-lookup.d.ts.map +1 -0
  134. package/dist/assets-runtime-assembly.d.ts +19 -0
  135. package/dist/assets-runtime-assembly.d.ts.map +1 -0
  136. package/dist/canvas-policy.d.ts +4 -0
  137. package/dist/canvas-policy.d.ts.map +1 -0
  138. package/dist/create-app.d.ts +57 -0
  139. package/dist/create-app.d.ts.map +1 -0
  140. package/dist/engine-worker-runtime.mjs +884 -0
  141. package/dist/engine-worker-runtime.mjs.map +1 -0
  142. package/dist/errors.d.ts +263 -0
  143. package/dist/errors.d.ts.map +1 -0
  144. package/dist/execution/attached-world-swap.d.ts +21 -0
  145. package/dist/execution/attached-world-swap.d.ts.map +1 -0
  146. package/dist/execution/bootstrap-entry.d.ts +30 -0
  147. package/dist/execution/bootstrap-entry.d.ts.map +1 -0
  148. package/dist/execution/capabilities.d.ts +5 -0
  149. package/dist/execution/capabilities.d.ts.map +1 -0
  150. package/dist/execution/control.d.ts +14 -0
  151. package/dist/execution/control.d.ts.map +1 -0
  152. package/dist/execution/engine-worker-runtime.d.ts +2 -0
  153. package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
  154. package/dist/execution/engine-worker.d.ts +23 -0
  155. package/dist/execution/engine-worker.d.ts.map +1 -0
  156. package/dist/execution/host-controller.d.ts +16 -0
  157. package/dist/execution/host-controller.d.ts.map +1 -0
  158. package/dist/execution/index.d.ts +10 -0
  159. package/dist/execution/index.d.ts.map +1 -0
  160. package/dist/execution/kernel-pool.d.ts +14 -0
  161. package/dist/execution/kernel-pool.d.ts.map +1 -0
  162. package/dist/execution/kernel-worker-runtime.d.ts +2 -0
  163. package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
  164. package/dist/execution/measurement.d.ts +7 -0
  165. package/dist/execution/measurement.d.ts.map +1 -0
  166. package/dist/execution/protocol.d.ts +84 -0
  167. package/dist/execution/protocol.d.ts.map +1 -0
  168. package/dist/execution/report.d.ts +5 -0
  169. package/dist/execution/report.d.ts.map +1 -0
  170. package/dist/execution/schema.d.ts +4 -0
  171. package/dist/execution/schema.d.ts.map +1 -0
  172. package/dist/execution/selector.d.ts +10 -0
  173. package/dist/execution/selector.d.ts.map +1 -0
  174. package/dist/execution/types.d.ts +107 -0
  175. package/dist/execution/types.d.ts.map +1 -0
  176. package/dist/fallback-camera.d.ts +8 -0
  177. package/dist/fallback-camera.d.ts.map +1 -0
  178. package/dist/game-context.d.ts +98 -0
  179. package/dist/game-context.d.ts.map +1 -0
  180. package/dist/index.d.ts +24 -0
  181. package/dist/index.d.ts.map +1 -0
  182. package/dist/index.mjs +3985 -0
  183. package/dist/index.mjs.map +1 -0
  184. package/dist/input-map-plugin.d.ts +5 -0
  185. package/dist/input-map-plugin.d.ts.map +1 -0
  186. package/dist/input-plugin.d.ts +4 -0
  187. package/dist/input-plugin.d.ts.map +1 -0
  188. package/dist/internal/assembled-engine-profile.d.ts +5 -0
  189. package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
  190. package/dist/internal/assets-world-plugin.d.ts +21 -0
  191. package/dist/internal/assets-world-plugin.d.ts.map +1 -0
  192. package/dist/internal/browser-remote-bridge.d.ts +25 -0
  193. package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
  194. package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
  195. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
  196. package/dist/internal/component-introspection.d.ts +24 -0
  197. package/dist/internal/component-introspection.d.ts.map +1 -0
  198. package/dist/internal/ecs-import.d.ts +6 -0
  199. package/dist/internal/ecs-import.d.ts.map +1 -0
  200. package/dist/internal/engine-profile-common.d.ts +9 -0
  201. package/dist/internal/engine-profile-common.d.ts.map +1 -0
  202. package/dist/internal/error-fanout.d.ts +66 -0
  203. package/dist/internal/error-fanout.d.ts.map +1 -0
  204. package/dist/internal/frame-loop.d.ts +35 -0
  205. package/dist/internal/frame-loop.d.ts.map +1 -0
  206. package/dist/internal/input-attach.d.ts +44 -0
  207. package/dist/internal/input-attach.d.ts.map +1 -0
  208. package/dist/internal/main-engine-profile.d.ts +15 -0
  209. package/dist/internal/main-engine-profile.d.ts.map +1 -0
  210. package/dist/internal/remote-serve-flag.d.ts +23 -0
  211. package/dist/internal/remote-serve-flag.d.ts.map +1 -0
  212. package/dist/internal/remote-server-plugin.d.ts +8 -0
  213. package/dist/internal/remote-server-plugin.d.ts.map +1 -0
  214. package/dist/internal/rhi-capture.d.ts +19 -0
  215. package/dist/internal/rhi-capture.d.ts.map +1 -0
  216. package/dist/internal/rhi-debug-flag.d.ts +18 -0
  217. package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
  218. package/dist/internal/worker-engine-profile.d.ts +13 -0
  219. package/dist/internal/worker-engine-profile.d.ts.map +1 -0
  220. package/dist/kernel-worker-runtime.mjs +69 -0
  221. package/dist/kernel-worker-runtime.mjs.map +1 -0
  222. package/dist/load-game-errors.d.ts +131 -0
  223. package/dist/load-game-errors.d.ts.map +1 -0
  224. package/dist/load-game.d.ts +34 -0
  225. package/dist/load-game.d.ts.map +1 -0
  226. package/dist/plugin-factories.d.ts +3 -0
  227. package/dist/plugin-factories.d.ts.map +1 -0
  228. package/dist/tool-preview/bootstrap.d.ts +113 -0
  229. package/dist/tool-preview/bootstrap.d.ts.map +1 -0
  230. package/dist/tool-preview/evidence.d.ts +20 -0
  231. package/dist/tool-preview/evidence.d.ts.map +1 -0
  232. package/dist/tool-preview/framing.d.ts +13 -0
  233. package/dist/tool-preview/framing.d.ts.map +1 -0
  234. package/dist/tool-preview/recipe.d.ts +54 -0
  235. package/dist/tool-preview/recipe.d.ts.map +1 -0
  236. package/dist/types.d.ts +444 -0
  237. package/dist/types.d.ts.map +1 -0
  238. package/package.json +86 -0
  239. package/schema/execution-report.schema.json +168 -0
  240. package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
  241. package/src/__tests__/app-rebuild.test.ts +31 -0
  242. package/src/__tests__/asset-registry-types.test-d.ts +16 -0
  243. package/src/__tests__/assets-runtime-assembly.integration.test.ts +68 -0
  244. package/src/__tests__/callback-deletion.unit.test.ts +14 -0
  245. package/src/__tests__/callback-inference.test-d.ts +32 -0
  246. package/src/__tests__/component-introspection.unit.test.ts +56 -0
  247. package/src/__tests__/consumer-migration.unit.test.ts +21 -0
  248. package/src/__tests__/create-app-features.test.ts +40 -0
  249. package/src/__tests__/create-app-listener-sync.test.ts +377 -0
  250. package/src/__tests__/create-app-lock-provider.test.ts +212 -0
  251. package/src/__tests__/create-app-plugin.test.ts +217 -0
  252. package/src/__tests__/create-app-stop.test.ts +617 -0
  253. package/src/__tests__/create-app-time.unit.test.ts +26 -0
  254. package/src/__tests__/create-app.test-d.ts +82 -0
  255. package/src/__tests__/create-app.test.ts +346 -0
  256. package/src/__tests__/create-renderer-error.integration.test.ts +24 -0
  257. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +19 -0
  258. package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
  259. package/src/__tests__/draw-source-headless-full-chain.test.ts +226 -0
  260. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +182 -0
  261. package/src/__tests__/draw-source-single-world-regression.test.ts +185 -0
  262. package/src/__tests__/ecs-import.unit.test.ts +40 -0
  263. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +74 -0
  264. package/src/__tests__/engine-worker-world-swap.unit.test.ts +76 -0
  265. package/src/__tests__/errors-pointer-lock-failed.test.ts +268 -0
  266. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +80 -0
  267. package/src/__tests__/execution-error-detail.test-d.ts +13 -0
  268. package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
  269. package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
  270. package/src/__tests__/execution-frame-protocol.unit.test.ts +61 -0
  271. package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
  272. package/src/__tests__/execution-measurement.unit.test.ts +20 -0
  273. package/src/__tests__/execution-public-api.test-d.ts +10 -0
  274. package/src/__tests__/execution-public-api.unit.test.ts +9 -0
  275. package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
  276. package/src/__tests__/execution-report.contract.test.ts +50 -0
  277. package/src/__tests__/execution-report.types.test-d.ts +16 -0
  278. package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
  279. package/src/__tests__/execution-selector.unit.test.ts +72 -0
  280. package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
  281. package/src/__tests__/execution-world-health-owner.test-d.ts +9 -0
  282. package/src/__tests__/fallback-camera.unit.test.ts +32 -0
  283. package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
  284. package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
  285. package/src/__tests__/frame-loop-world-array.test.ts +441 -0
  286. package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
  287. package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
  288. package/src/__tests__/plugin-policy.integration.test.ts +15 -0
  289. package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
  290. package/src/__tests__/profiler-default-off.test.ts +53 -0
  291. package/src/__tests__/profiler-frame-token.test.ts +248 -0
  292. package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
  293. package/src/__tests__/profiler-phase-integration.test.ts +129 -0
  294. package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
  295. package/src/__tests__/remote-integration.test.ts +224 -0
  296. package/src/__tests__/remote-serve-flag.test.ts +24 -0
  297. package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
  298. package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
  299. package/src/__tests__/rhi-capture.browser.test.ts +51 -0
  300. package/src/__tests__/surface-handoff.test.ts +92 -0
  301. package/src/__tests__/types-m2.test-d.ts +81 -0
  302. package/src/__tests__/visibility-remote.integration.test.ts +119 -0
  303. package/src/__tests__/worker-execution-stop.test.ts +116 -0
  304. package/src/animation-asset-lookup.ts +13 -0
  305. package/src/assets-runtime-assembly.ts +70 -0
  306. package/src/canvas-policy.ts +16 -0
  307. package/src/create-app.ts +1351 -0
  308. package/src/errors.ts +454 -0
  309. package/src/execution/attached-world-swap.ts +67 -0
  310. package/src/execution/bootstrap-entry.ts +122 -0
  311. package/src/execution/capabilities.ts +102 -0
  312. package/src/execution/control.ts +52 -0
  313. package/src/execution/engine-worker-runtime.ts +400 -0
  314. package/src/execution/engine-worker.ts +161 -0
  315. package/src/execution/host-controller.ts +454 -0
  316. package/src/execution/index.ts +43 -0
  317. package/src/execution/kernel-pool.ts +236 -0
  318. package/src/execution/kernel-worker-runtime.ts +103 -0
  319. package/src/execution/measurement.ts +39 -0
  320. package/src/execution/protocol.ts +131 -0
  321. package/src/execution/report.ts +66 -0
  322. package/src/execution/schema.ts +168 -0
  323. package/src/execution/selector.ts +98 -0
  324. package/src/execution/types.ts +146 -0
  325. package/src/fallback-camera.ts +20 -0
  326. package/src/game-context.ts +126 -0
  327. package/src/index.ts +176 -0
  328. package/src/input-map-plugin.ts +21 -0
  329. package/src/input-plugin.ts +26 -0
  330. package/src/internal/assembled-engine-profile.ts +22 -0
  331. package/src/internal/assets-world-plugin.ts +79 -0
  332. package/src/internal/browser-remote-bridge.ts +267 -0
  333. package/src/internal/browser-rhi-debug-runtime.ts +67 -0
  334. package/src/internal/component-introspection.ts +116 -0
  335. package/src/internal/ecs-import.ts +41 -0
  336. package/src/internal/engine-profile-common.ts +10 -0
  337. package/src/internal/error-fanout.ts +129 -0
  338. package/src/internal/frame-loop.ts +525 -0
  339. package/src/internal/input-attach.ts +104 -0
  340. package/src/internal/main-engine-profile.ts +80 -0
  341. package/src/internal/remote-serve-flag.ts +50 -0
  342. package/src/internal/remote-server-plugin.ts +16 -0
  343. package/src/internal/rhi-capture.ts +211 -0
  344. package/src/internal/rhi-debug-flag.ts +40 -0
  345. package/src/internal/worker-engine-profile.ts +47 -0
  346. package/src/load-game-errors.ts +211 -0
  347. package/src/load-game.ts +115 -0
  348. package/src/plugin-factories.ts +2 -0
  349. package/src/tool-preview/bootstrap.ts +848 -0
  350. package/src/tool-preview/evidence.ts +59 -0
  351. package/src/tool-preview/framing.ts +52 -0
  352. package/src/tool-preview/recipe.ts +137 -0
  353. package/src/types.ts +505 -0
  354. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,50 @@
1
+ // @forgeax/engine-app/internal/remote-serve-flag -- FORGEAX_ENGINE_REMOTE_SERVE
2
+ // dual-source resolution (plan-strategy secondary D-7).
3
+ //
4
+ // The remote eval server is auto-started in dev mode on Node hosts (vite's
5
+ // import.meta.env.DEV) and production builds skip it. Browsers never start this
6
+ // server: it owns a Node WebSocket listener and the browser has a separate
7
+ // opt-in bridge. Headless/dawn-node can opt in via the
8
+ // FORGEAX_ENGINE_REMOTE_SERVE=1 env var.
9
+ //
10
+ // Dual sources (mirrors rhi-debug-flag.ts pattern):
11
+ // browser (vite): import.meta.env.DEV is statically replaced by vite
12
+ // (true in dev server, false in production build), but the browser guard
13
+ // below keeps the Node-only server import out of the page.
14
+ // dawn-node : import.meta may be undefined; fallback to
15
+ // globalThis.process.env.FORGEAX_ENGINE_REMOTE_SERVE === '1'.
16
+ //
17
+ // The SSOT precedence: import.meta.env.DEV wins when available;
18
+ // process.env is the headless opt-in path.
19
+
20
+ /** Minimal structural view of an env bag carrying the serve flag. */
21
+ interface RemoteServeEnv {
22
+ readonly FORGEAX_ENGINE_REMOTE_SERVE?: string | undefined;
23
+ }
24
+
25
+ /**
26
+ * Resolve whether the remote eval server should be started.
27
+ *
28
+ * Returns `true` when:
29
+ * - a Node host is in vite dev mode (import.meta.env.DEV === true), OR
30
+ * - headless/dawn-node with FORGEAX_ENGINE_REMOTE_SERVE=1 opt-in
31
+ *
32
+ * Returns `false` in browsers, production builds, and headless hosts without
33
+ * explicit opt-in. The browser path must not even request the Node-only
34
+ * `@forgeax/engine-remote/server` module: Vite would otherwise discover it
35
+ * during the first Play boot and refresh the live dependency graph.
36
+ *
37
+ * @param isDev flag from import.meta.env.DEV (undefined when import.meta absent)
38
+ * @param processEnv globalThis.process?.env when present, else undefined
39
+ * @param isBrowser true for the DOM canvas host; false for Node/headless hosts
40
+ */
41
+ export function resolveRemoteServeFlag(
42
+ isDev: boolean | undefined,
43
+ processEnv: RemoteServeEnv | undefined,
44
+ isBrowser = false,
45
+ ): boolean {
46
+ if (isBrowser) return false;
47
+ if (isDev === true) return true;
48
+ if (processEnv?.FORGEAX_ENGINE_REMOTE_SERVE === '1') return true;
49
+ return false;
50
+ }
@@ -0,0 +1,16 @@
1
+ import type { Plugin } from '@forgeax/engine-plugin';
2
+
3
+ export interface RemoteServerHandle {
4
+ readonly port: number;
5
+ close(): Promise<void>;
6
+ }
7
+
8
+ /** Own a Node remote server with the App Fiber while App still exposes its handle. */
9
+ export function remoteServerPlugin(handle: RemoteServerHandle): Plugin {
10
+ return {
11
+ name: 'remote-server',
12
+ apply(ctx) {
13
+ ctx.effect(() => async () => handle.close(), 'remote/server');
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,211 @@
1
+ import type { RhiBackendInstrumentation } from '@forgeax/engine-render/internal';
2
+ import { RhiError } from '@forgeax/engine-rhi';
3
+ import type {
4
+ CaptureFrameOptions,
5
+ EncodedTape,
6
+ RecorderAttachment,
7
+ RhiDebugError,
8
+ } from '@forgeax/engine-rhi-debug';
9
+ import { createRhiDebugError } from '@forgeax/engine-rhi-debug';
10
+ import { err, ok, type Result } from '@forgeax/engine-types';
11
+ import type { FrameLoopHandle } from './frame-loop';
12
+
13
+ export interface RhiTapeArtifactRef {
14
+ readonly kind: 'rhi-tape';
15
+ readonly digest: string;
16
+ readonly bytes: Uint8Array;
17
+ }
18
+
19
+ export interface RhiCapture {
20
+ captureFrame(options?: CaptureFrameOptions): Promise<Result<RhiTapeArtifactRef, RhiDebugError>>;
21
+ }
22
+
23
+ type CaptureResult = Result<EncodedTape, RhiDebugError>;
24
+ type CaptureFrameDriver = Pick<FrameLoopHandle, 'getState' | 'pause' | 'resume' | 'stepFrame'>;
25
+
26
+ const captureDrivers = new WeakMap<RhiCapture, CaptureFrameDriver>();
27
+
28
+ export function createRhiCapture(attachment: RecorderAttachment): RhiCapture {
29
+ let activeCapture: Promise<Result<RhiTapeArtifactRef, RhiDebugError>> | undefined;
30
+ const capture: RhiCapture = {
31
+ captureFrame(options) {
32
+ const driver = captureDrivers.get(capture);
33
+ if (driver === undefined) return captureAttachment(attachment, options);
34
+ if (activeCapture !== undefined) {
35
+ return Promise.resolve(
36
+ err(
37
+ createRhiDebugError('capture-busy', {
38
+ stage: 'capture',
39
+ cause: 'another App capture transaction is active',
40
+ }),
41
+ ),
42
+ );
43
+ }
44
+ const request = captureWithAppFrame(attachment, driver, options);
45
+ activeCapture = request;
46
+ void request.then(
47
+ () => {
48
+ if (activeCapture === request) activeCapture = undefined;
49
+ },
50
+ () => {
51
+ if (activeCapture === request) activeCapture = undefined;
52
+ },
53
+ );
54
+ return request;
55
+ },
56
+ };
57
+ return capture;
58
+ }
59
+
60
+ /** Bind the App-owned frame authority to its host capture capability. */
61
+ export function bindRhiCaptureFrameDriver(capture: RhiCapture, driver: CaptureFrameDriver): void {
62
+ captureDrivers.set(capture, driver);
63
+ }
64
+
65
+ async function captureAttachment(
66
+ attachment: RecorderAttachment,
67
+ options: CaptureFrameOptions | undefined,
68
+ ): Promise<Result<RhiTapeArtifactRef, RhiDebugError>> {
69
+ const result = await attachment.captureFrame(options);
70
+ if (!result.ok) return result;
71
+ return ok(toArtifact(result.value));
72
+ }
73
+
74
+ async function captureWithAppFrame(
75
+ attachment: RecorderAttachment,
76
+ driver: CaptureFrameDriver,
77
+ options: CaptureFrameOptions | undefined,
78
+ ): Promise<Result<RhiTapeArtifactRef, RhiDebugError>> {
79
+ const state = driver.getState();
80
+ if (state !== 'running' && state !== 'paused') {
81
+ return err(
82
+ createRhiDebugError('capture-unavailable', {
83
+ stage: 'capture',
84
+ cause: `App capture requires a running or paused frame loop, received '${state}'`,
85
+ }),
86
+ );
87
+ }
88
+ if (options?.signal?.aborted) return captureAttachment(attachment, options);
89
+
90
+ const resumeAfter = state === 'running';
91
+ if (resumeAfter) {
92
+ const paused = driver.pause();
93
+ if (!paused.ok) {
94
+ return err(
95
+ createRhiDebugError('capture-unavailable', {
96
+ stage: 'capture',
97
+ cause: `App capture could not pause the frame loop: ${describeFailure(paused.error)}`,
98
+ }),
99
+ );
100
+ }
101
+ }
102
+
103
+ const controller = new AbortController();
104
+ const abortFromUser = (): void => controller.abort();
105
+ options?.signal?.addEventListener('abort', abortFromUser, { once: true });
106
+ let captureResult: Promise<CaptureResult>;
107
+ try {
108
+ captureResult = attachment.captureFrame({
109
+ ...(options ?? {}),
110
+ signal: controller.signal,
111
+ });
112
+ } catch (cause: unknown) {
113
+ options?.signal?.removeEventListener('abort', abortFromUser);
114
+ if (resumeAfter && driver.getState() === 'paused') driver.resume();
115
+ return err(
116
+ createRhiDebugError('capture-unavailable', {
117
+ stage: 'capture',
118
+ cause: `App capture could not arm the recorder: ${describeFailure(cause)}`,
119
+ }),
120
+ );
121
+ }
122
+
123
+ let result: CaptureResult | undefined;
124
+ let transactionError: RhiDebugError | undefined;
125
+ try {
126
+ const snapshot = await attachment.frameBoundary();
127
+ if (!snapshot.ok) {
128
+ result = await captureResult;
129
+ } else if (options?.signal?.aborted) {
130
+ controller.abort();
131
+ result = await captureResult;
132
+ } else {
133
+ const stepped = driver.stepFrame(0);
134
+ if (!stepped.ok) {
135
+ transactionError = createRhiDebugError('capture-unavailable', {
136
+ stage: 'capture',
137
+ cause: `App capture frame failed: ${describeFailure(stepped.error)}`,
138
+ });
139
+ controller.abort();
140
+ result = await captureResult;
141
+ } else {
142
+ result = await captureResult;
143
+ }
144
+ }
145
+ } catch (cause: unknown) {
146
+ transactionError = createRhiDebugError('capture-unavailable', {
147
+ stage: 'capture',
148
+ cause: `App capture transaction failed: ${describeFailure(cause)}`,
149
+ });
150
+ controller.abort();
151
+ result = await captureResult;
152
+ } finally {
153
+ options?.signal?.removeEventListener('abort', abortFromUser);
154
+ if (resumeAfter && driver.getState() === 'paused') {
155
+ const resumed = driver.resume();
156
+ if (!resumed.ok && transactionError === undefined) {
157
+ transactionError = createRhiDebugError('capture-unavailable', {
158
+ stage: 'capture',
159
+ cause: `App capture could not resume the frame loop: ${describeFailure(resumed.error)}`,
160
+ });
161
+ }
162
+ }
163
+ }
164
+
165
+ if (transactionError !== undefined) return err(transactionError);
166
+ if (result === undefined) result = await captureResult;
167
+ if (!result.ok) return result;
168
+ return ok(toArtifact(result.value));
169
+ }
170
+
171
+ function describeFailure(cause: unknown): string {
172
+ if (typeof cause === 'string') return cause;
173
+ if (cause instanceof Error) return cause.message;
174
+ if (typeof cause === 'object' && cause !== null && 'hint' in cause) {
175
+ const hint = (cause as { hint?: unknown }).hint;
176
+ if (typeof hint === 'string') return hint;
177
+ }
178
+ return String(cause);
179
+ }
180
+
181
+ export function createRhiInstrumentation(
182
+ attachment: RecorderAttachment,
183
+ ): RhiBackendInstrumentation {
184
+ return {
185
+ resolveSurfaceDevice(device) {
186
+ const resolved = attachment.backend.unwrapDeviceForSurface(device);
187
+ if (resolved.ok) return resolved;
188
+ return err(
189
+ new RhiError({
190
+ code: 'rhi-not-available',
191
+ expected: 'the recorder can resolve the wrapped surface device',
192
+ hint: resolved.error.hint,
193
+ }),
194
+ );
195
+ },
196
+ onFrameBoundary() {
197
+ void attachment.frameBoundary();
198
+ },
199
+ onDeviceLost() {
200
+ attachment.deviceLost();
201
+ },
202
+ };
203
+ }
204
+
205
+ function toArtifact(encoded: EncodedTape): RhiTapeArtifactRef {
206
+ return {
207
+ kind: 'rhi-tape',
208
+ digest: encoded.digest,
209
+ bytes: encoded.bytes,
210
+ };
211
+ }
@@ -0,0 +1,40 @@
1
+ // @forgeax/engine-app/internal/rhi-debug-flag -- FORGEAX_ENGINE_RHI_DEBUG
2
+ // three-segment dual-source resolution (plan-strategy D-4).
3
+ //
4
+ // The canvas form reads the flag from two independent sources with a precise
5
+ // precedence + short-circuit shape so the same build serves both deployment
6
+ // targets:
7
+ //
8
+ // browser (vite): `import.meta.env.FORGEAX_ENGINE_RHI_DEBUG` is statically
9
+ // replaced by the vite-plugin-rhi-debug `define` hook.
10
+ // dawn-node : `import.meta` may be undefined and there is no vite define,
11
+ // so the flag arrives through `globalThis.process.env`.
12
+ //
13
+ // Extracting the resolution into this pure helper lets it be unit-tested across
14
+ // the three scenarios (browser '1' / dawn-node process.env / unset) without a
15
+ // real bundler or runtime. The call site keeps the literal `typeof import.meta
16
+ // !== 'undefined'` prefix (C5) -- modelled here by passing `undefined` for the
17
+ // `importMetaEnv` argument when `import.meta` is absent.
18
+
19
+ /** Minimal structural view of an env bag carrying the flag. */
20
+ interface RhiDebugEnv {
21
+ readonly FORGEAX_ENGINE_RHI_DEBUG?: string | undefined;
22
+ }
23
+
24
+ /**
25
+ * Resolve the raw FORGEAX_ENGINE_RHI_DEBUG flag from the two sources.
26
+ *
27
+ * Precedence: `import.meta.env` wins; `globalThis.process.env` is the fallback.
28
+ * Returns the raw string (or undefined); the caller compares `=== '1'`.
29
+ *
30
+ * @param importMetaEnv `import.meta.env` when `import.meta` is defined,
31
+ * else `undefined` (models the `typeof import.meta !== 'undefined'` prefix
32
+ * short-circuiting to a falsy first operand).
33
+ * @param processEnv `globalThis.process?.env` when present, else `undefined`.
34
+ */
35
+ export function resolveRhiDebugFlag(
36
+ importMetaEnv: RhiDebugEnv | undefined,
37
+ processEnv: RhiDebugEnv | undefined,
38
+ ): string | undefined {
39
+ return importMetaEnv?.FORGEAX_ENGINE_RHI_DEBUG ?? processEnv?.FORGEAX_ENGINE_RHI_DEBUG;
40
+ }
@@ -0,0 +1,47 @@
1
+ import {
2
+ type AnimationPayloadLookup,
3
+ animationPayloadsPlugin,
4
+ animationRuntimePlugin,
5
+ } from '@forgeax/engine-animation';
6
+ import { type AudioBackend, audioBackendPlugin } from '@forgeax/engine-audio';
7
+ import { type InputBackend, inputBackendPlugin } from '@forgeax/engine-input';
8
+ import type { Plugin } from '@forgeax/engine-plugin';
9
+ import { ownedRendererPlugin, renderComponentsPlugin } from '@forgeax/engine-render';
10
+ import { scenePlugin } from '@forgeax/engine-scene';
11
+ import { statePlugin } from '@forgeax/engine-state';
12
+
13
+ import { inputPlugin } from '../input-plugin';
14
+ import {
15
+ assetRegistryPlugin,
16
+ assetRegistryWorldPlugin,
17
+ assetsWorldPlugin,
18
+ rendererAssetsPlugin,
19
+ } from './assets-world-plugin';
20
+ import type { EngineProfileBase } from './engine-profile-common';
21
+
22
+ export interface WorkerEngineProfileOptions extends EngineProfileBase {
23
+ readonly animationPayloads: AnimationPayloadLookup;
24
+ readonly input: InputBackend;
25
+ readonly audio: AudioBackend;
26
+ }
27
+
28
+ /** Static Engine Worker selection; Host-only acquisition stays outside this realm. */
29
+ export function workerEngineProfile(options: WorkerEngineProfileOptions): Plugin[] {
30
+ return [
31
+ ownedRendererPlugin(options.renderer),
32
+ renderComponentsPlugin(),
33
+ rendererAssetsPlugin(),
34
+ ...(options.assetAssembly === undefined
35
+ ? []
36
+ : [assetRegistryPlugin(options.assetAssembly), assetRegistryWorldPlugin()]),
37
+ assetsWorldPlugin(),
38
+ inputBackendPlugin(options.input),
39
+ audioBackendPlugin(options.audio),
40
+ scenePlugin(),
41
+ animationPayloadsPlugin(options.animationPayloads),
42
+ animationRuntimePlugin(),
43
+ statePlugin(),
44
+ inputPlugin(),
45
+ ...(options.extensions ?? []),
46
+ ];
47
+ }
@@ -0,0 +1,211 @@
1
+ // @forgeax/engine-app -- LoadGameError class + 3-code closed union +
2
+ // LOAD_GAME_ERROR_HINTS / LOAD_GAME_EXPECTED tables.
3
+ //
4
+ // Isomorphic to AppError in errors.ts: class extends Error + 4-field
5
+ // surface (.code / .expected / .hint / .detail) + discriminated detail
6
+ // per code; the co-located policy-owner proof locks the derived views.
7
+ //
8
+ // Shape:
9
+ // - LoadGameErrorCode = closed union 3 members (charter P4 closed-union
10
+ // exhaustive switch needs no default fallback; tsc strict mode guards
11
+ // completeness; AGENTS.md "Errors are structured").
12
+ // Members:
13
+ // - 'module-not-found'
14
+ // - 'invalid-format'
15
+ // - 'import-failed'
16
+ //
17
+ // - LoadGameError class = 4-field surface (.code / .expected / .hint /
18
+ // .detail) byte-for-byte parallel to AppError. AI users walk .code /
19
+ // .detail by property access (charter P3 explicit failure: never parse
20
+ // the message string).
21
+ //
22
+ // - LoadGameError is exposed as a discriminated union (variant per code)
23
+ // so AI users get .detail narrowing for free after
24
+ // `if (err.code === '...')`:
25
+ //
26
+ // if (err.code === 'module-not-found') {
27
+ // console.warn('game not found:', err.detail.slug);
28
+ // }
29
+ //
30
+ // - LOAD_GAME_ERROR_HINTS / LOAD_GAME_EXPECTED are 3-key Records keyed
31
+ // by LoadGameErrorCode; the private policy owner below is the single
32
+ // source for both projections.
33
+ //
34
+ // Related: requirements AC-08 (LoadGameError structured error 3 codes +
35
+ // hint + detail); plan-strategy D-3 (reuse codebase structured error
36
+ // pattern); charter P3 (explicit failure).
37
+
38
+ /**
39
+ * Closed LoadGameErrorCode union (3 members).
40
+ *
41
+ * | code | trigger |
42
+ * |:--|:--|
43
+ * | `'module-not-found'` | resolver throws an error distinguishable as module-not-found (e.g. "Cannot find module"). Detail carries `slug` so AI users can surface which game path failed. |
44
+ * | `'invalid-format'` | resolver returns a module without a valid native Cordis default export. Detail carries `exportKeys` so AI users can inspect the module shape. |
45
+ * | `'import-failed'` | resolver throws a generic Error (network error, build error, etc.) not distinguishable as module-not-found. Detail carries the original `cause` Error so AI users can chain narrow. |
46
+ *
47
+ * Plan-strategy D-3 locks the count at 3.
48
+ */
49
+ export type LoadGameErrorCode = keyof typeof loadGameErrorPolicy;
50
+
51
+ /**
52
+ * Detail variant for the `'module-not-found'` arm.
53
+ *
54
+ * `slug` carries the game identifier that the resolver failed to locate
55
+ * so AI users can surface exactly which game could not be loaded.
56
+ */
57
+ export interface LoadGameDetailModuleNotFound {
58
+ readonly slug: string;
59
+ }
60
+
61
+ /**
62
+ * Detail variant for the `'invalid-format'` arm.
63
+ *
64
+ * `exportKeys` carries the keys of the module object returned by the
65
+ * resolver. AI users inspect these to understand why the native default
66
+ * plugin export was rejected.
67
+ */
68
+ export interface LoadGameDetailInvalidFormat {
69
+ readonly exportKeys: string[];
70
+ }
71
+
72
+ /**
73
+ * Detail variant for the `'import-failed'` arm.
74
+ *
75
+ * `cause` carries the original thrown value verbatim so AI users can
76
+ * chain narrow (e.g. `cause instanceof TypeError` for network errors)
77
+ * without losing structure.
78
+ */
79
+ export interface LoadGameDetailImportFailed {
80
+ readonly cause: unknown;
81
+ }
82
+
83
+ /**
84
+ * Conditional resolver from `LoadGameErrorCode` to its detail payload type.
85
+ */
86
+ export type LoadGameErrorDetailFor<C extends LoadGameErrorCode> = C extends 'module-not-found'
87
+ ? LoadGameDetailModuleNotFound
88
+ : C extends 'invalid-format'
89
+ ? LoadGameDetailInvalidFormat
90
+ : LoadGameDetailImportFailed;
91
+
92
+ /**
93
+ * Tagged union of `.detail` payloads carried by structured LoadGameError.
94
+ */
95
+ export type LoadGameErrorDetail = LoadGameErrorDetailFor<LoadGameErrorCode>;
96
+
97
+ class LoadGameErrorClass extends Error {
98
+ readonly code: LoadGameErrorCode;
99
+ readonly expected: string;
100
+ readonly hint: string;
101
+ readonly detail: LoadGameErrorDetail;
102
+
103
+ constructor(args: {
104
+ code: LoadGameErrorCode;
105
+ expected: string;
106
+ hint: string;
107
+ detail: LoadGameErrorDetail;
108
+ }) {
109
+ super(`[LoadGameError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);
110
+ this.name = 'LoadGameError';
111
+ this.code = args.code;
112
+ this.expected = args.expected;
113
+ this.hint = args.hint;
114
+ this.detail = args.detail;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Variant intersection: a `LoadGameErrorClass` instance whose `code` literal
120
+ * narrows to `C` and whose `detail` narrows to `LoadGameErrorDetailFor<C>`.
121
+ */
122
+ type LoadGameErrorVariant<C extends LoadGameErrorCode> = LoadGameErrorClass & {
123
+ readonly code: C;
124
+ readonly detail: LoadGameErrorDetailFor<C>;
125
+ };
126
+
127
+ /**
128
+ * Public LoadGameError type -- discriminated union of the 3 variants.
129
+ *
130
+ * AI-user form (charter P3 explicit failure):
131
+ *
132
+ * ```ts
133
+ * function recover(err: LoadGameError): string {
134
+ * switch (err.code) {
135
+ * case 'module-not-found': return `game not found: ${err.detail.slug}`;
136
+ * case 'invalid-format': return `bad shape, exports: ${err.detail.exportKeys.join(', ')}`;
137
+ * case 'import-failed': return err.detail.cause instanceof Error ? err.detail.cause.message : 'unknown';
138
+ * }
139
+ * }
140
+ * ```
141
+ */
142
+ export type LoadGameError = {
143
+ [C in LoadGameErrorCode]: LoadGameErrorVariant<C>;
144
+ }[LoadGameErrorCode];
145
+
146
+ interface LoadGameErrorConstructor {
147
+ new <C extends LoadGameErrorCode>(args: {
148
+ code: C;
149
+ expected: string;
150
+ hint: string;
151
+ detail: LoadGameErrorDetailFor<C>;
152
+ }): LoadGameErrorVariant<C>;
153
+ readonly prototype: LoadGameErrorClass;
154
+ }
155
+
156
+ /**
157
+ * LoadGameError constructor -- `new LoadGameError({ code, expected, hint, detail })`.
158
+ */
159
+ export const LoadGameError: LoadGameErrorConstructor =
160
+ LoadGameErrorClass as unknown as LoadGameErrorConstructor;
161
+
162
+ /**
163
+ * `expected` table -- the engine-side invariant that was violated when each
164
+ * code surfaces. AI users read this as the L2 detail (charter F2 priority
165
+ * text); `.hint` carries the recovery action.
166
+ *
167
+ * 3 keys; the policy-owner proof locks the exact count, order, and values.
168
+ */
169
+ type LoadGameErrorPolicy = { readonly expected: string; readonly hint: string };
170
+
171
+ const loadGameErrorPolicy = {
172
+ 'module-not-found': {
173
+ expected:
174
+ 'resolver should return a module with a native Cordis default export for the given slug',
175
+ hint: 'verify the game slug matches an existing template directory; check the resolver import path for typos',
176
+ },
177
+ 'invalid-format': {
178
+ expected: 'resolved module must default-export a Cordis function, class, or object plugin',
179
+ hint: 'default-export one plugin and let the Host mount it into App.pluginContext',
180
+ },
181
+ 'import-failed': {
182
+ expected:
183
+ 'resolver should complete without throwing; import path, network, and build errors are forwarded here',
184
+ hint: 'inspect detail.cause for the original error (network failure, build error, dynamic import timeout, etc.)',
185
+ },
186
+ } satisfies Record<string, LoadGameErrorPolicy>;
187
+
188
+ export const LOAD_GAME_EXPECTED: Readonly<Record<LoadGameErrorCode, string>> = Object.fromEntries(
189
+ Object.entries(loadGameErrorPolicy).map(([code, policy]) => [code, policy.expected]),
190
+ ) as Readonly<Record<LoadGameErrorCode, string>>;
191
+
192
+ /**
193
+ * `hint` table -- actionable recovery guidance per code (charter P3).
194
+ *
195
+ * 3 keys; the policy-owner proof locks the exact count, order, and values.
196
+ */
197
+ export const LOAD_GAME_ERROR_HINTS: Readonly<Record<LoadGameErrorCode, string>> =
198
+ Object.fromEntries(
199
+ Object.entries(loadGameErrorPolicy).map(([code, policy]) => [code, policy.hint]),
200
+ ) as Readonly<Record<LoadGameErrorCode, string>>;
201
+
202
+ /**
203
+ * Type guard for narrowing unknown errors to LoadGameError.
204
+ *
205
+ * AI users who catch mixed error types can call `if (isLoadGameError(e))`
206
+ * before walking `.code`. Uses instanceof against the internal class
207
+ * (the public constructor delegates to LoadGameErrorClass).
208
+ */
209
+ export function isLoadGameError(err: unknown): err is LoadGameError {
210
+ return err instanceof LoadGameErrorClass;
211
+ }
@@ -0,0 +1,115 @@
1
+ // @forgeax/engine-app -- loadGame(slug, resolver) -> Result<Plugin, LoadGameError>
2
+ //
3
+ // Pure-function load helper that validates a dynamically-imported game
4
+ // template module. The resolver is an injection point so loadGame is
5
+ // independent of Vite / bundler specifics.
6
+ //
7
+ // Shape:
8
+ // 1. Call resolver(slug). If resolver throws, distinguish module-not-found
9
+ // (slug in detail) from import-failed (cause in detail).
10
+ // 2. Validate the module's `default` export as a Cordis plugin.
11
+ // 3. On success, return that plugin without creating a parallel entry lifecycle.
12
+ //
13
+ // Constraints from upstream:
14
+ // - requirements D-3: loadGame does NOT depend on Vite specifics
15
+ // - requirements boundary-case table: module-not-found -> slug in detail;
16
+ // invalid-format -> exportKeys; import-failed -> cause
17
+ // - plan-strategy D-3: LoadGameError reuses codebase structured error pattern
18
+ //
19
+ // Charter awareness:
20
+ // - P3 explicit failure: all 3 error paths return structured Result.err
21
+ // - F1 context-limited: the function is one screen (no hidden state)
22
+
23
+ import type { Plugin } from '@forgeax/engine-plugin';
24
+ import { err, ok, type Result } from '@forgeax/engine-types';
25
+
26
+ import { LOAD_GAME_ERROR_HINTS, LOAD_GAME_EXPECTED, LoadGameError } from './load-game-errors';
27
+
28
+ /**
29
+ * The shape of a module that the resolver returns.
30
+ */
31
+ interface GameEntryModule {
32
+ readonly default?: unknown;
33
+ readonly [key: string]: unknown;
34
+ }
35
+
36
+ /**
37
+ * Resolver function signature: receives a slug (the game identifier),
38
+ * returns a Promise that resolves to a module object.
39
+ *
40
+ * The host (apps/preview/) injects this as a dynamic import proxy,
41
+ * e.g. `(slug) => import(\`../../templates/\${slug}/src/main.ts\`)`.
42
+ * loadGame does not hardcode any import path.
43
+ */
44
+ export type GamePluginResolver = (slug: string) => Promise<GameEntryModule>;
45
+
46
+ function isPlugin(value: unknown): value is Plugin {
47
+ return (
48
+ typeof value === 'function' ||
49
+ (typeof value === 'object' &&
50
+ value !== null &&
51
+ 'apply' in value &&
52
+ typeof value.apply === 'function')
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Load and validate a game template module.
58
+ *
59
+ * Returns `Result.ok<Plugin>` when the resolver returns a module
60
+ * whose default export is a native Cordis plugin. Returns `Result.err<LoadGameError>`
61
+ * with one of 3 error codes on failure.
62
+ *
63
+ * @param slug - The game identifier (e.g. 'game-default'). Passed
64
+ * through to the resolver and carried in the 'module-not-found' detail.
65
+ * @param resolver - Async function that imports/fetches the game module.
66
+ * The host owns all path resolution logic.
67
+ */
68
+ export async function loadGame(
69
+ slug: string,
70
+ resolver: GamePluginResolver,
71
+ ): Promise<Result<Plugin, LoadGameError>> {
72
+ let module: GameEntryModule;
73
+ try {
74
+ module = await resolver(slug);
75
+ } catch (thrown: unknown) {
76
+ // Distinguish module-not-found from generic import failure.
77
+ // The heuristic: if the thrown error contains the slug in its message,
78
+ // treat it as module-not-found (the resolver signaled the specific
79
+ // module was not found, e.g. Vite "Failed to load module" with the
80
+ // slug path in the message). Otherwise treat it as import-failed
81
+ // (generic network error, eval error, etc.).
82
+ if (thrown instanceof Error && thrown.message.includes(slug)) {
83
+ return err(
84
+ new LoadGameError({
85
+ code: 'module-not-found',
86
+ expected: LOAD_GAME_EXPECTED['module-not-found'],
87
+ hint: LOAD_GAME_ERROR_HINTS['module-not-found'],
88
+ detail: { slug },
89
+ }),
90
+ );
91
+ }
92
+ return err(
93
+ new LoadGameError({
94
+ code: 'import-failed',
95
+ expected: LOAD_GAME_EXPECTED['import-failed'],
96
+ hint: LOAD_GAME_ERROR_HINTS['import-failed'],
97
+ detail: { cause: thrown },
98
+ }),
99
+ );
100
+ }
101
+
102
+ if (!isPlugin(module.default)) {
103
+ const exportKeys = Object.keys(module);
104
+ return err(
105
+ new LoadGameError({
106
+ code: 'invalid-format',
107
+ expected: LOAD_GAME_EXPECTED['invalid-format'],
108
+ hint: LOAD_GAME_ERROR_HINTS['invalid-format'],
109
+ detail: { exportKeys },
110
+ }),
111
+ );
112
+ }
113
+
114
+ return ok(module.default);
115
+ }