@forgeax/engine-assets-runtime 0.1.4 → 0.1.7

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 (311) hide show
  1. package/README.md +453 -175
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/aabb.unit.test.d.ts +2 -0
  4. package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
  6. package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
  8. package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
  9. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
  10. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
  12. package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
  13. package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
  14. package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
  15. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
  16. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
  18. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
  19. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
  20. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
  21. package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
  22. package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
  23. package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
  24. package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
  25. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
  26. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
  27. package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
  28. package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
  29. package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
  30. package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
  31. package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
  32. package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
  33. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
  34. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
  35. package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
  36. package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
  37. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
  38. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
  39. package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
  40. package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
  41. package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
  42. package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
  43. package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
  44. package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
  45. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
  46. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
  47. package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
  48. package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
  49. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
  50. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
  51. package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
  52. package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
  53. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
  54. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
  55. package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
  56. package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
  57. package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
  58. package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
  59. package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
  60. package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
  61. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
  62. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
  63. package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
  64. package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
  65. package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
  66. package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
  67. package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
  68. package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
  69. package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
  70. package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
  71. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  72. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  73. package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
  74. package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
  75. package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
  76. package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
  77. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
  78. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
  79. package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
  80. package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
  81. package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
  82. package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
  83. package/dist/__tests__/plugin.test.d.ts +2 -0
  84. package/dist/__tests__/plugin.test.d.ts.map +1 -0
  85. package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
  86. package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
  87. package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
  88. package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
  89. package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
  90. package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
  91. package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
  92. package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
  93. package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
  94. package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
  95. package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
  96. package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
  97. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
  98. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
  99. package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
  100. package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
  101. package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
  102. package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
  103. package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
  104. package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
  105. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
  106. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
  107. package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
  108. package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
  109. package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
  110. package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
  111. package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
  112. package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
  113. package/dist/aabb.d.ts +3 -0
  114. package/dist/aabb.d.ts.map +1 -0
  115. package/dist/asset-registry.d.ts +554 -0
  116. package/dist/asset-registry.d.ts.map +1 -0
  117. package/dist/builtin-asset-registry.d.ts +22 -0
  118. package/dist/builtin-asset-registry.d.ts.map +1 -0
  119. package/dist/catalog-source.d.ts +2 -1
  120. package/dist/catalog-source.d.ts.map +1 -1
  121. package/dist/decode-image-bytes.d.ts +57 -0
  122. package/dist/decode-image-bytes.d.ts.map +1 -0
  123. package/dist/dynamic-texture-store.d.ts +89 -0
  124. package/dist/dynamic-texture-store.d.ts.map +1 -0
  125. package/dist/errors/asset.d.ts +201 -0
  126. package/dist/errors/asset.d.ts.map +1 -0
  127. package/dist/handles.d.ts +143 -0
  128. package/dist/handles.d.ts.map +1 -0
  129. package/dist/image-error.d.ts +6 -0
  130. package/dist/image-error.d.ts.map +1 -0
  131. package/dist/index.d.ts +32 -3
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.mjs +7233 -114
  134. package/dist/index.mjs.map +1 -1
  135. package/dist/internal.d.ts +1 -1
  136. package/dist/internal.d.ts.map +1 -1
  137. package/dist/loader-registry.d.ts +63 -0
  138. package/dist/loader-registry.d.ts.map +1 -0
  139. package/dist/loaders/inline-pack.d.ts +72 -0
  140. package/dist/loaders/inline-pack.d.ts.map +1 -0
  141. package/dist/loaders/mesh-bin.d.ts +21 -0
  142. package/dist/loaders/mesh-bin.d.ts.map +1 -0
  143. package/dist/loaders/pack-artifact.d.ts +20 -0
  144. package/dist/loaders/pack-artifact.d.ts.map +1 -0
  145. package/dist/material/dependency-graph.d.ts +9 -0
  146. package/dist/material/dependency-graph.d.ts.map +1 -0
  147. package/dist/material/generation-cache.d.ts +24 -0
  148. package/dist/material/generation-cache.d.ts.map +1 -0
  149. package/dist/material/inspection.d.ts +24 -0
  150. package/dist/material/inspection.d.ts.map +1 -0
  151. package/dist/material/loader.d.ts +59 -0
  152. package/dist/material/loader.d.ts.map +1 -0
  153. package/dist/material/runtime-shader.d.ts +14 -0
  154. package/dist/material/runtime-shader.d.ts.map +1 -0
  155. package/dist/mesh-bin.d.ts +3 -0
  156. package/dist/mesh-bin.d.ts.map +1 -0
  157. package/dist/mipmap-generator.d.ts +132 -0
  158. package/dist/mipmap-generator.d.ts.map +1 -0
  159. package/dist/payload-validate.d.ts +60 -0
  160. package/dist/payload-validate.d.ts.map +1 -0
  161. package/dist/plugin.d.ts +16 -0
  162. package/dist/plugin.d.ts.map +1 -0
  163. package/dist/registry/artifact-io.d.ts +16 -0
  164. package/dist/registry/artifact-io.d.ts.map +1 -0
  165. package/dist/registry/asset-evidence.d.ts +13 -0
  166. package/dist/registry/asset-evidence.d.ts.map +1 -0
  167. package/dist/registry/catalog-state.d.ts +47 -0
  168. package/dist/registry/catalog-state.d.ts.map +1 -0
  169. package/dist/registry/catalog.d.ts +48 -0
  170. package/dist/registry/catalog.d.ts.map +1 -0
  171. package/dist/registry/instantiate.d.ts +153 -0
  172. package/dist/registry/instantiate.d.ts.map +1 -0
  173. package/dist/registry/load-by-guid.d.ts +247 -0
  174. package/dist/registry/load-by-guid.d.ts.map +1 -0
  175. package/dist/registry/load-state.d.ts +26 -0
  176. package/dist/registry/load-state.d.ts.map +1 -0
  177. package/dist/registry/runtime-projection.d.ts +16 -0
  178. package/dist/registry/runtime-projection.d.ts.map +1 -0
  179. package/dist/registry/scene-publication-fence.d.ts +38 -0
  180. package/dist/registry/scene-publication-fence.d.ts.map +1 -0
  181. package/dist/registry/validate-material.d.ts +77 -0
  182. package/dist/registry/validate-material.d.ts.map +1 -0
  183. package/dist/resolve-asset-handle.d.ts +43 -0
  184. package/dist/resolve-asset-handle.d.ts.map +1 -0
  185. package/dist/resolve-tileset-runtime.d.ts +23 -0
  186. package/dist/resolve-tileset-runtime.d.ts.map +1 -0
  187. package/dist/scene-handle-fields.d.ts +88 -0
  188. package/dist/scene-handle-fields.d.ts.map +1 -0
  189. package/dist/scene-payload.d.ts +15 -0
  190. package/dist/scene-payload.d.ts.map +1 -0
  191. package/dist/wire-default-loaders.d.ts +29 -0
  192. package/dist/wire-default-loaders.d.ts.map +1 -0
  193. package/package.json +22 -9
  194. package/src/__tests__/aabb.unit.test.ts +97 -0
  195. package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
  196. package/src/__tests__/artifact-io.unit.test.ts +127 -0
  197. package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
  198. package/src/__tests__/asset-evidence.unit.test.ts +32 -0
  199. package/src/__tests__/{asset-kind.test.ts → asset-kind.unit.test.ts} +1 -1
  200. package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
  201. package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
  202. package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
  203. package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
  204. package/src/__tests__/asset-registry.unit.test.ts +511 -0
  205. package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
  206. package/src/__tests__/catalog-replica.unit.test.ts +196 -0
  207. package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
  208. package/src/__tests__/catalog-source.unit.test.ts +126 -0
  209. package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
  210. package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
  211. package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
  212. package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
  213. package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
  214. package/src/__tests__/decode-image-bytes.test.ts +90 -0
  215. package/src/__tests__/decode-image-mime-owner.test.ts +2 -5
  216. package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
  217. package/src/__tests__/errors-asset.unit.test.ts +92 -0
  218. package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
  219. package/src/__tests__/instantiate-context.unit.test.ts +65 -0
  220. package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
  221. package/src/__tests__/instantiate.unit.test.ts +684 -0
  222. package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
  223. package/src/__tests__/loader-registry.unit.test.ts +101 -0
  224. package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
  225. package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
  226. package/src/__tests__/material-load-consumer.test-d.ts +42 -0
  227. package/src/__tests__/material-ready.integration.test.ts +508 -0
  228. package/src/__tests__/material-stale-generation.unit.test.ts +147 -0
  229. package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
  230. package/src/__tests__/mesh-bin.unit.test.ts +152 -0
  231. package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
  232. package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
  233. package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
  234. package/src/__tests__/payload-validate.unit.test.ts +285 -0
  235. package/src/__tests__/plugin.test.ts +55 -0
  236. package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
  237. package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
  238. package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
  239. package/src/__tests__/registry-load-state.unit.test.ts +35 -0
  240. package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
  241. package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
  242. package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
  243. package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
  244. package/src/__tests__/scene-payload.unit.test.ts +151 -0
  245. package/src/__tests__/scene-publication-fence.test.ts +129 -0
  246. package/src/__tests__/scriptable-pack-public-surface.test-d.ts +27 -0
  247. package/src/__tests__/static-rig.integration.test.ts +8 -0
  248. package/src/__tests__/validate-material.unit.test.ts +359 -0
  249. package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
  250. package/src/aabb.ts +107 -0
  251. package/src/asset-registry.ts +2045 -0
  252. package/src/builtin-asset-registry.ts +164 -0
  253. package/src/catalog-source.ts +14 -70
  254. package/src/decode-image-bytes.ts +126 -0
  255. package/src/dynamic-texture-store.ts +234 -0
  256. package/src/errors/asset.ts +347 -0
  257. package/src/handles.ts +186 -0
  258. package/src/image-error.ts +45 -0
  259. package/src/index.ts +183 -16
  260. package/src/internal.ts +1 -0
  261. package/src/loader-registry.ts +157 -0
  262. package/src/loaders/inline-pack.ts +837 -0
  263. package/src/loaders/mesh-bin.ts +304 -0
  264. package/src/loaders/pack-artifact.ts +665 -0
  265. package/src/material/dependency-graph.ts +46 -0
  266. package/src/material/generation-cache.ts +177 -0
  267. package/src/material/inspection.ts +46 -0
  268. package/src/material/loader.ts +326 -0
  269. package/src/material/runtime-shader.ts +133 -0
  270. package/src/mesh-bin.ts +5 -0
  271. package/src/mipmap-generator.ts +450 -0
  272. package/src/payload-validate.ts +573 -0
  273. package/src/plugin.ts +48 -0
  274. package/src/registry/artifact-io.ts +259 -0
  275. package/src/registry/asset-evidence.ts +51 -0
  276. package/src/registry/catalog-state.ts +409 -0
  277. package/src/registry/catalog.ts +369 -0
  278. package/src/registry/instantiate.ts +1056 -0
  279. package/src/registry/load-by-guid.ts +1657 -0
  280. package/src/registry/load-state.ts +115 -0
  281. package/src/registry/runtime-projection.ts +63 -0
  282. package/src/registry/scene-publication-fence.ts +256 -0
  283. package/src/registry/validate-material.ts +385 -0
  284. package/src/resolve-asset-handle.ts +147 -0
  285. package/src/resolve-tileset-runtime.ts +72 -0
  286. package/src/scene-handle-fields.ts +221 -0
  287. package/src/scene-payload.ts +207 -0
  288. package/src/wire-default-loaders.ts +79 -0
  289. package/dist/__tests__/asset-graph-red.integration.test.d.ts +0 -2
  290. package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +0 -1
  291. package/dist/__tests__/asset-kind.test.d.ts +0 -2
  292. package/dist/__tests__/asset-kind.test.d.ts.map +0 -1
  293. package/dist/__tests__/asset-registry-core.integration.test.d.ts +0 -2
  294. package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +0 -1
  295. package/dist/__tests__/asset-registry-public-api.test-d.d.ts +0 -2
  296. package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +0 -1
  297. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +0 -2
  298. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +0 -1
  299. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +0 -2
  300. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +0 -1
  301. package/dist/__tests__/catalog-session-red.unit.test.d.ts +0 -2
  302. package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +0 -1
  303. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +0 -2
  304. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +0 -1
  305. package/src/__tests__/asset-graph-red.integration.test.ts +0 -113
  306. package/src/__tests__/asset-registry-core.integration.test.ts +0 -161
  307. package/src/__tests__/asset-registry-public-api.test-d.ts +0 -31
  308. package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +0 -79
  309. package/src/__tests__/asset-runtime-snapshot.unit.test.ts +0 -23
  310. package/src/__tests__/catalog-session-red.unit.test.ts +0 -276
  311. package/src/__tests__/registry-lifecycle-red.integration.test.ts +0 -80
@@ -0,0 +1,1056 @@
1
+ // @forgeax/engine-assets-runtime -- scene instantiate collaboration module
2
+ // (feat-20260705-runtime-tier2-decomposition M1 / w6, D-4 + D-1). Free functions
3
+ // taking the AssetRegistry instance as first param; logic byte-preserved from the
4
+ // class body (this. -> registry.). Hosts the SkinJointResolver + PostSpawnHook
5
+ // hook-contract types relocated from scene-instances/post-spawn-resolve-joints.ts
6
+ // (D-1); w9 wires PostSpawnHook into the AssetRegistry constructor.
7
+
8
+ import type { EcsError, EntityHandle, World } from '@forgeax/engine-ecs';
9
+ import type { PackError } from '@forgeax/engine-pack/errors';
10
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
11
+ import { err, ok, type Result } from '@forgeax/engine-rhi';
12
+ import {
13
+ worldDespawnScene,
14
+ worldInstantiateScene,
15
+ worldInstantiateSceneFlat,
16
+ worldSetSceneAssetResolver,
17
+ } from '@forgeax/engine-scene';
18
+ import {
19
+ type Asset,
20
+ type AssetEnvelope,
21
+ AssetError,
22
+ type CatalogEntry,
23
+ type Handle,
24
+ type MountOverride,
25
+ PACK_ERROR_HINTS,
26
+ type SceneAsset,
27
+ type SceneInstanceMount,
28
+ type SkeletonAsset,
29
+ type SkinAsset,
30
+ type TagOf,
31
+ unwrapHandle,
32
+ } from '@forgeax/engine-types';
33
+ import type { AssetRegistry } from '../asset-registry';
34
+ import { resolveAssetHandle } from '../resolve-asset-handle';
35
+ import {
36
+ extractMountOverrideHandleGuids,
37
+ extractSceneEntityHandleGuids,
38
+ } from '../scene-handle-fields';
39
+ import {
40
+ compareScenePublicationFences,
41
+ parseScenePublicationFence,
42
+ type ScenePublicationFence,
43
+ type ScenePublicationFenceError,
44
+ scenePublicationFenceFromCatalog,
45
+ } from './scene-publication-fence';
46
+
47
+ /**
48
+ * Resolver contract consumed by {@link postSpawnResolveJoints} (D-1: relocated
49
+ * here from scene-instances/post-spawn-resolve-joints.ts so the hook contract
50
+ * travels with the instantiate cluster into @forgeax/engine-assets-runtime).
51
+ */
52
+ export interface SkinJointResolver {
53
+ resolveSkinAsset(skeletonHandleRaw: number): SkinAsset | undefined;
54
+ }
55
+
56
+ function catalogSourceGuidForFence(
57
+ registry: AssetRegistry,
58
+ expected: ScenePublicationFence,
59
+ ): string | undefined {
60
+ const entries = catalogEntriesForFence(registry);
61
+ for (const entry of entries) {
62
+ if (entry.kind !== 'scene') continue;
63
+ const current = scenePublicationFenceFromCatalog(entries, entry.guid);
64
+ if (current.ok && compareScenePublicationFences(expected, current.value).ok) {
65
+ return entry.guid;
66
+ }
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ /** Merge the live Edit Catalog and the Play pack-index projection for fences. */
72
+ function catalogEntriesForFence(registry: AssetRegistry): readonly CatalogEntry[] {
73
+ const byGuid = new Map<string, CatalogEntry>();
74
+ for (const entry of registry.catalogSnapshot()?.entries ?? []) {
75
+ byGuid.set(entry.guid.toLowerCase(), entry);
76
+ }
77
+ for (const [guid, record] of registry.packIndexCache ?? []) {
78
+ if (record.sourcePath === undefined) continue;
79
+ const candidate = { ...record, guid, sourcePath: record.sourcePath } as CatalogEntry;
80
+ const current = byGuid.get(guid.toLowerCase());
81
+ if (
82
+ current === undefined ||
83
+ (current.publication === undefined && candidate.publication !== undefined)
84
+ ) {
85
+ byGuid.set(guid.toLowerCase(), candidate);
86
+ }
87
+ }
88
+ return [...byGuid.values()];
89
+ }
90
+
91
+ /**
92
+ * Post-spawn hook contract (D-1). A hook runs after instantiate spawns the
93
+ * scene subtree; the shipped implementation is runtime's `postSpawnResolveJoints`
94
+ * (auto-wire Skin.joints). Injected at the sole production assembly point
95
+ * (createRenderer) in w9/w10; when absent, instantiate skips joint wiring.
96
+ */
97
+ export type PostSpawnHook = (
98
+ world: World,
99
+ resolver: SkinJointResolver,
100
+ root: EntityHandle,
101
+ ) => { ok: true } | { ok: false; error: unknown };
102
+
103
+ /**
104
+ * Remove the entities and producer grants created by one instantiate call.
105
+ *
106
+ * GUID fields use World interning, so an interned handle is released only when
107
+ * its producer grant is the last live reference. A failed scene has no live
108
+ * holders after `despawnScene`; a handle retained by an unrelated sibling is
109
+ * therefore left untouched. Fresh scene and nested-scene alloc grants are
110
+ * always released after their spawned holders are gone.
111
+ */
112
+ function rollbackSpawn(
113
+ world: World,
114
+ roots: readonly EntityHandle[],
115
+ allocHandles: readonly number[],
116
+ internedHandles: readonly number[],
117
+ sharedRefBaseline: number,
118
+ ): void {
119
+ for (const root of new Set(roots)) {
120
+ void worldDespawnScene(world, root);
121
+ }
122
+
123
+ for (const raw of new Set(allocHandles)) {
124
+ const handle = raw as never;
125
+ if (world.sharedRefs.refcount(handle) > 0) void world.sharedRefs.release(handle);
126
+ }
127
+
128
+ for (const raw of new Set(internedHandles)) {
129
+ if (world.sharedRefs._liveCount() <= sharedRefBaseline) break;
130
+ const handle = raw as never;
131
+ if (world.sharedRefs.refcount(handle) === 1) void world.sharedRefs.release(handle);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Materialise a `SceneAsset` into an existing `World` and return the
137
+ * synthetic root `Entity` (feat-20260514 w31 sugar wrapper; AC-03 +
138
+ * requirements §IN-3; M3: returns Entity not SceneInstanceId).
139
+ *
140
+ * Before spawning, handle-type component fields (e.g. `assetHandle`,
141
+ * `material`, `skeleton`) containing GUID strings are resolved to fresh
142
+ * user-tier `Handle` numbers via `world.allocSharedRef` (feat-20260614 M8
143
+ * D-19 instantiate-time GUID->handle mint; supersedes the pre-D-17
144
+ * `resolveGuid` map). GUIDs that fail to parse or are not catalogued return
145
+ * `AssetError(code='asset-not-found')` with a hint containing the GUID,
146
+ * node localId, and field name.
147
+ *
148
+ * Errors propagate verbatim through the closed
149
+ * `AssetError | PackError | EcsError` union so AI users that already
150
+ * narrow `loadByGuid<SceneAsset>` results reuse the same `switch
151
+ * (err.code)` exhaustively (charter proposition 3 machine-readable
152
+ * union; plan-strategy §3.3 closed-union transparency).
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * const sceneRes = await engine.assets.loadByGuid<SceneAsset>(roomGuid); // payload (D-17)
157
+ * if (!sceneRes.ok) return;
158
+ * const handle = world.allocSharedRef('SceneAsset', sceneRes.value); // mint column handle
159
+ * const r = engine.assets.instantiate(handle, world);
160
+ * if (!r.ok) {
161
+ * switch (r.error.code) {
162
+ * case 'asset-not-found':
163
+ * case 'pack-cyclic-reference':
164
+ * // ... AssetErrorCode | PackErrorCode | EcsErrorCode exhaustive
165
+ * }
166
+ * }
167
+ * ```
168
+ */
169
+ export function instantiate<T extends SceneAsset>(
170
+ registry: AssetRegistry,
171
+ handle: Handle<TagOf<T>, 'shared'>,
172
+ world: World,
173
+ parent?: EntityHandle,
174
+ expectedPublication?: ScenePublicationFence,
175
+ ): Result<EntityHandle, AssetError | PackError | EcsError | ScenePublicationFenceError> {
176
+ const sharedRefBaseline = world.sharedRefs._liveCount();
177
+ const allocHandles: number[] = [];
178
+ const internedHandles: number[] = [];
179
+ // feat-20260614 M8 (D-15 / D-17): resolve the SceneAsset payload from the
180
+ // handle through the two-tier `resolveAssetHandle` (builtin / user-tier
181
+ // world.sharedRefs) -- the registry holds no handle->payload map. Scene
182
+ // GUID-type component fields are then resolved to fresh user-tier handles
183
+ // via `world.allocSharedRef` (instantiate-time GUID->handle mint). When the
184
+ // handle does not resolve to a scene payload, fall through to the ecs-only
185
+ // path (an externally-resolved SceneAssetResolver handle).
186
+ let instantiateResult: Result<EntityHandle, AssetError | PackError | EcsError>;
187
+ const sceneRes0 = resolveAssetHandle<SceneAsset>(
188
+ world,
189
+ handle as unknown as Handle<string, 'shared'>,
190
+ );
191
+ const sceneAsset = sceneRes0.ok ? sceneRes0.value : undefined;
192
+ if (sceneAsset !== undefined && sceneAsset.kind !== 'scene') {
193
+ return err(
194
+ new AssetError({
195
+ code: 'asset-invalid-value',
196
+ expected: 'instantiate handle resolves to a SceneAsset',
197
+ hint: `resolved asset kind was ${sceneAsset.kind}`,
198
+ }),
199
+ );
200
+ }
201
+ if (sceneAsset !== undefined && sceneAsset.kind === 'scene') {
202
+ // feat-20260622 M3 / w8: find the scene's GUID key in the catalog
203
+ // so _resolveSceneGuids can reverse-decode from envelope.refs edges.
204
+ const sceneGuidKey = registry._guidForAsset(sceneAsset);
205
+ if (expectedPublication !== undefined) {
206
+ const entries = catalogEntriesForFence(registry);
207
+ if (sceneGuidKey === undefined) {
208
+ return err({
209
+ code: 'asset-generation-fence-mismatch',
210
+ phase: 'instantiate',
211
+ hint: 'generated Scene source has no Catalog identity for publication fence validation',
212
+ retryable: true,
213
+ recoveryActions: ['continue-last-known-good', 'retry-rebuild', 'fresh-reopen'],
214
+ } as const);
215
+ }
216
+ const current = scenePublicationFenceFromCatalog(entries, sceneGuidKey);
217
+ if (!current.ok) return current;
218
+ const matches = compareScenePublicationFences(expectedPublication, current.value);
219
+ if (!matches.ok) return matches;
220
+ }
221
+ const guidToHandle = new Map<string, number>();
222
+ const resolvedSceneHandles = new Map<string, number>();
223
+ const sceneRes = registry._resolveSceneGuids(
224
+ sceneAsset,
225
+ world,
226
+ sceneGuidKey,
227
+ undefined,
228
+ guidToHandle,
229
+ resolvedSceneHandles,
230
+ );
231
+ if (!sceneRes.ok) {
232
+ rollbackSpawn(
233
+ world,
234
+ [],
235
+ [...resolvedSceneHandles.values()],
236
+ [...guidToHandle.values()],
237
+ sharedRefBaseline,
238
+ );
239
+ return sceneRes;
240
+ }
241
+ internedHandles.push(...guidToHandle.values());
242
+ allocHandles.push(...resolvedSceneHandles.values());
243
+
244
+ // feat-20260703 M1 (D-1): register the resolved copy -> original
245
+ // catalog GUID in the origin reverse-index so _guidForAsset can
246
+ // find it even after the local sceneGuidKey variable is discarded.
247
+ if (sceneGuidKey !== undefined) {
248
+ registry._originIndex.set(sceneRes.value, sceneGuidKey);
249
+ }
250
+
251
+ // Register the GUID-resolved SceneAsset as a shared ref so
252
+ // Scene owner resolves it transparently. The shared
253
+ // ref alloc-grant rc=1 stays held by the alloc; the SceneInstance spawn
254
+ // retains to rc=2 and the despawn path releases back to rc=1.
255
+ const sharedHandle = world.allocSharedRef('SceneAsset', sceneRes.value);
256
+ allocHandles.push(unwrapHandle(sharedHandle));
257
+
258
+ // m3-i3: wire identity resolver so mount.source already resolved
259
+ // to a live handle number by _resolveMountsRec passes through.
260
+ // Scene mount resolution calls this resolver; when source is a number (live handle),
261
+ // return it as-is; when source is a string (unresolved GUID),
262
+ // fail (should not happen after resolution, but fail-safe).
263
+ worldSetSceneAssetResolver(world, (source, _parentHandle) => {
264
+ if (typeof source === 'number') {
265
+ return ok(source as unknown as Handle<'SceneAsset', 'shared'>);
266
+ }
267
+ return err({
268
+ code: 'asset-not-found' as const,
269
+ expected: `mount source GUID ${source} resolved before instantiate`,
270
+ hint: PACK_ERROR_HINTS['pack-cyclic-reference'],
271
+ });
272
+ });
273
+
274
+ // C-R2 (feat-20260622-s5 M6): instantiateScene now returns
275
+ // `{ root, diagnostics }` on success. This runtime API keeps its
276
+ // `Result<EntityHandle>` contract; unwrap to `root`. (Surfacing scene
277
+ // unknown-field diagnostics through `assets.instantiate` is M7 README
278
+ // scope — the Scene package owns this boundary.)
279
+ const sceneInst = worldInstantiateScene(world, sharedHandle, parent);
280
+ if (!sceneInst.ok) {
281
+ rollbackSpawn(world, [], allocHandles, internedHandles, sharedRefBaseline);
282
+ return sceneInst as unknown as Result<EntityHandle, AssetError | PackError | EcsError>;
283
+ }
284
+ instantiateResult = ok(sceneInst.value.root);
285
+ } else {
286
+ // Non-resolvable handle: original ecs direct path (backward compat).
287
+ const sceneInst = worldInstantiateScene(
288
+ world,
289
+ handle as Handle<'SceneAsset', 'shared'>,
290
+ parent,
291
+ );
292
+ if (!sceneInst.ok) {
293
+ return sceneInst as unknown as Result<EntityHandle, AssetError | PackError | EcsError>;
294
+ }
295
+ instantiateResult = ok(sceneInst.value.root);
296
+ }
297
+
298
+ // Post-spawn hook: auto-wire Skin.joints from jointPaths. feat-20260614 M8
299
+ // (D-15): the Skin column holds a user-tier SkeletonAsset handle; resolve
300
+ // it to the payload via the two-tier `resolveAssetHandle`, then match the
301
+ // catalogued SkinAsset whose resolved skeleton payload is the same object
302
+ // (the registry holds no handle->guid index).
303
+ //
304
+ // feat-20260705-runtime-tier2-decomposition M1 / w9 (D-1): the hook is
305
+ // injected via `registry.postSpawnHook` (the sole production assembly point
306
+ // createRenderer wires `postSpawnResolveJoints`). When no hook is present
307
+ // (standalone / test registries without joint-wiring needs), instantiate
308
+ // skips the post-spawn wiring silently -- the resolver closure below stays
309
+ // inline (it reads registry-internal state: assetCatalog / _guidForAsset).
310
+ const hook = registry.postSpawnHook;
311
+ if (hook !== undefined) {
312
+ const self = registry;
313
+ const resolver: SkinJointResolver = {
314
+ resolveSkinAsset(skeletonHandleRaw: number) {
315
+ const skelRes = resolveAssetHandle<SkeletonAsset>(
316
+ world,
317
+ skeletonHandleRaw as unknown as Handle<string, 'shared'>,
318
+ );
319
+ if (!skelRes.ok) return undefined;
320
+ const skeletonPayload = skelRes.value as Asset;
321
+ const skeletonGuid = self._guidForAsset(skeletonPayload);
322
+ if (skeletonGuid === undefined) return undefined;
323
+ for (const [, envelope] of self.assetCatalog) {
324
+ const asset = envelope.payload;
325
+ if (asset.kind !== 'skin') continue;
326
+ const skinSkeletonGuid = asset.skeletonGuid;
327
+ if (skinSkeletonGuid === undefined) continue;
328
+ if (skinSkeletonGuid.toLowerCase() === skeletonGuid) {
329
+ return asset;
330
+ }
331
+ }
332
+ return undefined;
333
+ },
334
+ };
335
+ const jointResolveResult = hook(world, resolver, instantiateResult.value);
336
+ if (!jointResolveResult.ok) {
337
+ rollbackSpawn(
338
+ world,
339
+ [instantiateResult.value],
340
+ allocHandles,
341
+ internedHandles,
342
+ sharedRefBaseline,
343
+ );
344
+ return { ok: false, error: jointResolveResult.error } as unknown as Result<
345
+ EntityHandle,
346
+ AssetError | PackError | EcsError
347
+ >;
348
+ }
349
+ }
350
+
351
+ return instantiateResult;
352
+ }
353
+
354
+ /**
355
+ * Materialise a `SceneAsset` FLAT into an existing `World` — the "edit the
356
+ * scene itself" registry entry (#655). Shares the GUID-resolution + shared-ref +
357
+ * SceneAssetResolver prelude with {@link instantiate}, but calls
358
+ * `worldInstantiateSceneFlat` instead of `worldInstantiateScene`: NO synthetic
359
+ * SceneInstance root, NO forced `ChildOf` on top-level members. The scene's own
360
+ * entities become plain top-level world entities; nested prefabs (`mounts[]`)
361
+ * still become their own SceneInstance anchors. Returns the set of top-level
362
+ * entity handles.
363
+ *
364
+ * Use this to OPEN a scene for authoring; use {@link instantiate} (anchor) at
365
+ * runtime / Play and for nested prefabs.
366
+ *
367
+ * The post-spawn Skin.joints hook (when wired via `registry.postSpawnHook`)
368
+ * runs once per top-level root: each GLB root keeps its own `ChildOf` subtree,
369
+ * so joint resolution is scoped to each subtree exactly as the anchor path
370
+ * scopes it to the single synthetic root.
371
+ */
372
+ export function instantiateFlat<T extends SceneAsset>(
373
+ registry: AssetRegistry,
374
+ handle: Handle<TagOf<T>, 'shared'>,
375
+ world: World,
376
+ expectedPublication?: ScenePublicationFence,
377
+ ): Result<EntityHandle[], AssetError | PackError | EcsError | ScenePublicationFenceError> {
378
+ const sharedRefBaseline = world.sharedRefs._liveCount();
379
+ const allocHandles: number[] = [];
380
+ const internedHandles: number[] = [];
381
+ let roots: EntityHandle[];
382
+ let mountEntities: EntityHandle[];
383
+ const sceneRes0 = resolveAssetHandle<SceneAsset>(
384
+ world,
385
+ handle as unknown as Handle<string, 'shared'>,
386
+ );
387
+ const sceneAsset = sceneRes0.ok ? sceneRes0.value : undefined;
388
+ if (sceneAsset !== undefined && sceneAsset.kind === 'scene') {
389
+ const sceneGuidKey = registry._guidForAsset(sceneAsset);
390
+ if (expectedPublication !== undefined) {
391
+ const entries = catalogEntriesForFence(registry);
392
+ if (sceneGuidKey === undefined) {
393
+ return err({
394
+ code: 'asset-generation-fence-mismatch',
395
+ phase: 'instantiate',
396
+ hint: 'generated Scene source has no Catalog identity for publication fence validation',
397
+ retryable: true,
398
+ recoveryActions: ['continue-last-known-good', 'retry-rebuild', 'fresh-reopen'],
399
+ } as const);
400
+ }
401
+ const current = scenePublicationFenceFromCatalog(entries, sceneGuidKey);
402
+ if (!current.ok) return current;
403
+ const matches = compareScenePublicationFences(expectedPublication, current.value);
404
+ if (!matches.ok) return matches;
405
+ }
406
+ const guidToHandle = new Map<string, number>();
407
+ const resolvedSceneHandles = new Map<string, number>();
408
+ const sceneRes = registry._resolveSceneGuids(
409
+ sceneAsset,
410
+ world,
411
+ sceneGuidKey,
412
+ undefined,
413
+ guidToHandle,
414
+ resolvedSceneHandles,
415
+ );
416
+ if (!sceneRes.ok) {
417
+ rollbackSpawn(
418
+ world,
419
+ [],
420
+ [...resolvedSceneHandles.values()],
421
+ [...guidToHandle.values()],
422
+ sharedRefBaseline,
423
+ );
424
+ return sceneRes;
425
+ }
426
+ internedHandles.push(...guidToHandle.values());
427
+ allocHandles.push(...resolvedSceneHandles.values());
428
+ if (sceneGuidKey !== undefined) {
429
+ registry._originIndex.set(sceneRes.value, sceneGuidKey);
430
+ }
431
+ const sharedHandle = world.allocSharedRef('SceneAsset', sceneRes.value);
432
+ allocHandles.push(unwrapHandle(sharedHandle));
433
+ worldSetSceneAssetResolver(world, (source, _parentHandle) => {
434
+ if (typeof source === 'number') {
435
+ return ok(source as unknown as Handle<'SceneAsset', 'shared'>);
436
+ }
437
+ return err({
438
+ code: 'asset-not-found' as const,
439
+ expected: `mount source GUID ${source} resolved before instantiate`,
440
+ hint: PACK_ERROR_HINTS['pack-cyclic-reference'],
441
+ });
442
+ });
443
+ const sceneInst = worldInstantiateSceneFlat(world, sharedHandle);
444
+ if (!sceneInst.ok) {
445
+ rollbackSpawn(world, [], allocHandles, internedHandles, sharedRefBaseline);
446
+ return sceneInst as unknown as Result<EntityHandle[], AssetError | PackError | EcsError>;
447
+ }
448
+ roots = sceneInst.value.roots;
449
+ mountEntities = sceneInst.value.mountEntities;
450
+ } else {
451
+ const sceneInst = worldInstantiateSceneFlat(world, handle as Handle<'SceneAsset', 'shared'>);
452
+ if (!sceneInst.ok) {
453
+ return sceneInst as unknown as Result<EntityHandle[], AssetError | PackError | EcsError>;
454
+ }
455
+ roots = sceneInst.value.roots;
456
+ mountEntities = sceneInst.value.mountEntities;
457
+ }
458
+
459
+ // Post-spawn Skin.joints wiring, per top-level root. Mirrors the anchor
460
+ // path's hook (D-1: injected via `registry.postSpawnHook`); when no hook is
461
+ // present the flat path skips joint wiring silently.
462
+ const hook = registry.postSpawnHook;
463
+ if (hook !== undefined) {
464
+ const self = registry;
465
+ const resolver: SkinJointResolver = {
466
+ resolveSkinAsset(skeletonHandleRaw: number) {
467
+ const skelRes = resolveAssetHandle<SkeletonAsset>(
468
+ world,
469
+ skeletonHandleRaw as unknown as Handle<string, 'shared'>,
470
+ );
471
+ if (!skelRes.ok) return undefined;
472
+ const skeletonPayload = skelRes.value as Asset;
473
+ const skeletonGuid = self._guidForAsset(skeletonPayload);
474
+ if (skeletonGuid === undefined) return undefined;
475
+ for (const [, envelope] of self.assetCatalog) {
476
+ const asset = envelope.payload;
477
+ if (asset.kind !== 'skin') continue;
478
+ const skinSkeletonGuid = asset.skeletonGuid;
479
+ if (skinSkeletonGuid === undefined) continue;
480
+ if (skinSkeletonGuid.toLowerCase() === skeletonGuid) {
481
+ return asset;
482
+ }
483
+ }
484
+ return undefined;
485
+ },
486
+ };
487
+ const hookRoots = new Set<EntityHandle>(roots);
488
+ for (const mountEntity of mountEntities) hookRoots.add(mountEntity);
489
+ for (const root of hookRoots) {
490
+ const jointResolveResult = hook(world, resolver, root);
491
+ if (!jointResolveResult.ok) {
492
+ rollbackSpawn(
493
+ world,
494
+ [...roots, ...mountEntities],
495
+ allocHandles,
496
+ internedHandles,
497
+ sharedRefBaseline,
498
+ );
499
+ return { ok: false, error: jointResolveResult.error } as unknown as Result<
500
+ EntityHandle[],
501
+ AssetError | PackError | EcsError
502
+ >;
503
+ }
504
+ }
505
+ }
506
+
507
+ return ok(roots);
508
+ }
509
+
510
+ /**
511
+ * m3-i2: Recursively resolve mounts[].source GUID strings.
512
+ * Returns a PackError-shaped object on cycle (R-9) or AssetError
513
+ * on child resolution failure.
514
+ *
515
+ * feat-20260713 M3 / w13: also down-drills `mounts[].overrides[].value`,
516
+ * resolving any GUID string bound to a `shared<...>` / `array<shared<...>>`
517
+ * schema field to a live handle (D-2). `guidToHandle` is the caller's per-scene
518
+ * dedup map (shared with the entity-field resolution in `_resolveSceneGuids`) so
519
+ * the same catalogued GUID mints exactly one user-tier handle across entity
520
+ * fields and override values (D-15/D-17 dedup contract); it defaults to a fresh
521
+ * map so standalone callers keep working. Override resolution runs for every
522
+ * mount regardless of `source` branch — an unresolvable GUID fail-fasts here,
523
+ * before any spawn (P3: no half-initialized member).
524
+ */
525
+ export function resolveMountsRec(
526
+ registry: AssetRegistry,
527
+ mounts: readonly SceneInstanceMount[],
528
+ world: World,
529
+ visited: Set<string>,
530
+ guidToHandle: Map<string, number> = new Map(),
531
+ resolvedSceneHandles: Map<string, number> = new Map(),
532
+ ): Result<
533
+ SceneInstanceMount[],
534
+ | AssetError
535
+ | {
536
+ readonly code: 'pack-cyclic-reference';
537
+ readonly expected: string;
538
+ readonly hint: string;
539
+ readonly detail: {
540
+ readonly code: 'pack-cyclic-reference';
541
+ readonly kind: 'mount-asset';
542
+ readonly cycle: readonly string[];
543
+ };
544
+ }
545
+ | ScenePublicationFenceError
546
+ > {
547
+ const out: SceneInstanceMount[] = [];
548
+ for (const m of mounts) {
549
+ if (m.publicationFence !== undefined) {
550
+ const parsedFence = parseScenePublicationFence(m.publicationFence);
551
+ if (!parsedFence.ok) return parsedFence;
552
+ const handleSourceGuid =
553
+ typeof m.source === 'string'
554
+ ? m.source
555
+ : (() => {
556
+ const resolved = resolveAssetHandle<SceneAsset>(world, m.source as never);
557
+ return resolved.ok && resolved.value.kind === 'scene'
558
+ ? registry._guidForAsset(resolved.value)
559
+ : undefined;
560
+ })();
561
+ const sourceGuid =
562
+ handleSourceGuid ??
563
+ (typeof m.source === 'string'
564
+ ? undefined
565
+ : catalogSourceGuidForFence(registry, parsedFence.value));
566
+ if (sourceGuid === undefined) {
567
+ return err({
568
+ code: 'asset-generation-fence-mismatch',
569
+ phase: 'instantiate',
570
+ hint: 'publication-fenced mount has no source GUID identity for validation',
571
+ expected: parsedFence.value,
572
+ retryable: true,
573
+ recoveryActions: ['continue-last-known-good', 'retry-rebuild', 'fresh-reopen'],
574
+ });
575
+ }
576
+ const currentFence = scenePublicationFenceFromCatalog(
577
+ catalogEntriesForFence(registry),
578
+ sourceGuid,
579
+ );
580
+ if (!currentFence.ok) return currentFence;
581
+ const matches = compareScenePublicationFences(parsedFence.value, currentFence.value);
582
+ if (!matches.ok) return matches;
583
+ }
584
+ let componentPatch:
585
+ | { components: NonNullable<SceneInstanceMount['components']> }
586
+ | Record<string, never> = {};
587
+ if (m.components !== undefined) {
588
+ const resolvedComponents = resolveMountComponents(
589
+ registry,
590
+ world,
591
+ m.components,
592
+ guidToHandle,
593
+ );
594
+ if (!resolvedComponents.ok) return resolvedComponents;
595
+ componentPatch = {
596
+ components: resolvedComponents.value as NonNullable<SceneInstanceMount['components']>,
597
+ };
598
+ }
599
+ // Resolve override value GUIDs first — applies to every mount regardless of
600
+ // the source branch below (a number-source mount can still carry overrides).
601
+ let overridePatch: { overrides: readonly MountOverride[] } | Record<string, never> = {};
602
+ if (m.overrides !== undefined && m.overrides.length > 0) {
603
+ const ro = resolveMountOverrides(registry, world, m.overrides, guidToHandle);
604
+ if (!ro.ok) return ro;
605
+ overridePatch = { overrides: ro.value };
606
+ }
607
+
608
+ const src = m.source;
609
+ // m3-i3: if source is already a number (live handle from a prior
610
+ // resolution pass), pass through unchanged.
611
+ if (typeof src === 'number') {
612
+ out.push({ ...m, ...componentPatch, ...overridePatch });
613
+ continue;
614
+ }
615
+
616
+ // source is a GUID string — resolve it.
617
+ const guidKey = src.toLowerCase();
618
+
619
+ // Cycle detection.
620
+ if (visited.has(guidKey)) {
621
+ return err({
622
+ code: 'pack-cyclic-reference' as const,
623
+ expected: 'no circular mount.source GUID references',
624
+ hint: PACK_ERROR_HINTS['pack-cyclic-reference'],
625
+ detail: {
626
+ code: 'pack-cyclic-reference' as const,
627
+ kind: 'mount-asset' as const,
628
+ cycle: [...visited, guidKey],
629
+ },
630
+ });
631
+ }
632
+
633
+ // Look up child scene.
634
+ const childEnv = registry.assetCatalog.get(guidKey);
635
+ if (childEnv === undefined) {
636
+ // Not catalogued — pass through as-is (overrides still resolved above).
637
+ out.push({ ...m, ...componentPatch, ...overridePatch });
638
+ continue;
639
+ }
640
+ const childPayload = childEnv.payload;
641
+ if (
642
+ typeof childPayload !== 'object' ||
643
+ childPayload === null ||
644
+ (childPayload as Asset).kind !== 'scene'
645
+ ) {
646
+ out.push({ ...m, ...componentPatch, ...overridePatch });
647
+ continue;
648
+ }
649
+
650
+ const cachedChildHandle = resolvedSceneHandles.get(guidKey);
651
+ if (cachedChildHandle !== undefined) {
652
+ out.push({
653
+ ...m,
654
+ source: cachedChildHandle,
655
+ ...componentPatch,
656
+ ...overridePatch,
657
+ } as SceneInstanceMount);
658
+ continue;
659
+ }
660
+
661
+ // Resolve mounts recursively.
662
+ const childVisited = new Set(visited);
663
+ // Don't add guidKey to visited here — _resolveSceneGuids will do it
664
+ // via its own _visitedMountGuids parameter.
665
+ const childRes = registry._resolveSceneGuids(
666
+ childPayload as SceneAsset,
667
+ world,
668
+ guidKey,
669
+ childVisited,
670
+ guidToHandle,
671
+ resolvedSceneHandles,
672
+ true,
673
+ );
674
+
675
+ if (!childRes.ok) {
676
+ // Propagate child resolution error.
677
+ return childRes as unknown as Result<SceneInstanceMount[], AssetError>;
678
+ }
679
+
680
+ // Build resolved child with its own mounts.
681
+ const resolvedChild: SceneAsset = {
682
+ kind: 'scene',
683
+ entities: childRes.value.entities,
684
+ ...(childRes.value.mounts !== undefined && childRes.value.mounts.length > 0
685
+ ? { mounts: childRes.value.mounts }
686
+ : {}),
687
+ } as SceneAsset;
688
+
689
+ // allocSharedRef + register in originIndex (D-7).
690
+ const chRaw = unwrapHandle(world.allocSharedRef('SceneAsset', resolvedChild));
691
+ resolvedSceneHandles.set(guidKey, chRaw);
692
+ registry._originIndex.set(resolvedChild, guidKey);
693
+
694
+ // Replace source with live handle number (D-5: source is number|string).
695
+ out.push({
696
+ ...m,
697
+ source: chRaw,
698
+ ...componentPatch,
699
+ ...overridePatch,
700
+ } as SceneInstanceMount);
701
+ }
702
+ return ok(out);
703
+ }
704
+
705
+ function resolveMountComponents(
706
+ registry: AssetRegistry,
707
+ world: World,
708
+ components: NonNullable<SceneInstanceMount['components']>,
709
+ guidToHandle: Map<string, number>,
710
+ ): Result<Record<string, Record<string, unknown>>, AssetError> {
711
+ const source = components as Record<string, Record<string, unknown>>;
712
+ const output: Record<string, Record<string, unknown>> = {};
713
+ for (const [componentName, fields] of Object.entries(source))
714
+ output[componentName] = { ...fields };
715
+ const entries = extractSceneEntityHandleGuids(world.components.entries(), [
716
+ { localId: 0, components: source },
717
+ ]);
718
+ for (const entry of entries) {
719
+ const fieldPath =
720
+ `${entry.componentName}.${entry.fieldName}` +
721
+ (entry.arrayIndex === undefined ? '' : `[${entry.arrayIndex}]`);
722
+ const resolved = resolveHandleGuid(
723
+ registry,
724
+ world,
725
+ entry.guidString,
726
+ guidToHandle,
727
+ fieldPath,
728
+ 'mount.components',
729
+ );
730
+ if (!resolved.ok) return resolved;
731
+ const fields = output[entry.componentName];
732
+ if (fields === undefined) continue;
733
+ if (entry.arrayIndex === undefined) {
734
+ fields[entry.fieldName] = resolved.value;
735
+ continue;
736
+ }
737
+ const current = fields[entry.fieldName];
738
+ if (!Array.isArray(current)) continue;
739
+ const next = [...current];
740
+ next[entry.arrayIndex] = resolved.value;
741
+ fields[entry.fieldName] = next;
742
+ }
743
+ return ok(output);
744
+ }
745
+
746
+ /**
747
+ * Resolve one GUID string to a live user-tier handle (feat-20260713 M3 / w13
748
+ * SSOT). Parses the GUID, uses `guidToHandle` as the per-resolution fast path,
749
+ * and on a miss looks the envelope up in the catalog + interns its shared ref
750
+ * by `(target, payload identity)` in the World. An unparseable / uncatalogued
751
+ * GUID returns `AssetError(code='asset-not-found')` with a breadcrumb hint
752
+ * (`fieldPath` + `location`) for AI-user debuggability (P3). Shared by the
753
+ * entity-field fallback in `_resolveSceneGuids` and the override-value down-drill
754
+ * in {@link resolveMountOverrides} so the parse/dedup/lookup/mint idiom has one
755
+ * home (architecture-principles §1 SSOT).
756
+ */
757
+ export function resolveHandleGuid(
758
+ registry: AssetRegistry,
759
+ world: World,
760
+ guidString: string,
761
+ guidToHandle: Map<string, number>,
762
+ fieldPath: string,
763
+ location: string,
764
+ ): Result<number, AssetError> {
765
+ const guidRes = AssetGuid.parse(guidString);
766
+ if (!guidRes.ok) {
767
+ return err(
768
+ new AssetError({
769
+ code: 'asset-not-found',
770
+ expected: `valid GUID string for field ${fieldPath}`,
771
+ hint: `GUID "${guidString}" could not be parsed; at ${location}, field=${fieldPath}`,
772
+ }),
773
+ );
774
+ }
775
+ const guidKey = guidString.toLowerCase();
776
+ let slot = guidToHandle.get(guidKey);
777
+ if (slot === undefined) {
778
+ const envelope = registry.assetCatalog.get(guidKey);
779
+ if (envelope === undefined) {
780
+ return err(
781
+ new AssetError({
782
+ code: 'asset-not-found',
783
+ expected: `GUID ${guidString} catalogued in AssetRegistry`,
784
+ hint:
785
+ `GUID ${guidString} not catalogued; ` +
786
+ `call loadByGuid('${guidString}') before instantiate; ` +
787
+ `at ${location}, field=${fieldPath}`,
788
+ }),
789
+ );
790
+ }
791
+ slot = unwrapHandle(world.internSharedRef(envelope.payload.kind, envelope.payload));
792
+ guidToHandle.set(guidKey, slot);
793
+ }
794
+ return ok(slot);
795
+ }
796
+
797
+ /**
798
+ * feat-20260713 M3 / w13: resolve every GUID string inside a mount's
799
+ * `overrides[].value` to a live handle, returning a new overrides array whose
800
+ * shared fields hold numeric handles (D-2 — the ecs apply loop never sees a
801
+ * GUID). Identification is delegated to the shared w12 core
802
+ * ({@link extractMountOverrideHandleGuids}); resolution reuses the
803
+ * `envelope.payload → world.internSharedRef` path with the caller's
804
+ * `guidToHandle` fast path (D-15/D-17). Stop-on-first-error: an unparseable /
805
+ * uncatalogued GUID
806
+ * returns `AssetError(code='asset-not-found')` with a breadcrumb hint (P3),
807
+ * aborting before any spawn. Number elements pass through untouched (D-8).
808
+ */
809
+ function resolveMountOverrides(
810
+ registry: AssetRegistry,
811
+ world: World,
812
+ overrides: readonly MountOverride[],
813
+ guidToHandle: Map<string, number>,
814
+ ): Result<MountOverride[], AssetError> {
815
+ const entries = extractMountOverrideHandleGuids(world.components.entries(), overrides);
816
+ if (entries.length === 0) return ok([...overrides]);
817
+
818
+ // resolvedMap key: `${overrideIndex}|${fieldName}|${arrayIndex ?? ''}`.
819
+ const resolvedMap = new Map<string, number>();
820
+ for (const entry of entries) {
821
+ const fieldPath =
822
+ `${entry.componentName}.${entry.fieldName}` +
823
+ (entry.arrayIndex !== undefined ? `[${entry.arrayIndex}]` : '');
824
+ const slot = resolveHandleGuid(
825
+ registry,
826
+ world,
827
+ entry.guidString,
828
+ guidToHandle,
829
+ fieldPath,
830
+ `mount override index=${entry.overrideIndex}`,
831
+ );
832
+ if (!slot.ok) return slot;
833
+ resolvedMap.set(
834
+ overrideFieldKey(entry.overrideIndex, entry.fieldName, entry.arrayIndex),
835
+ slot.value,
836
+ );
837
+ }
838
+
839
+ // Reconstruct: substitute resolved handles back into each override's value.
840
+ const out: MountOverride[] = [];
841
+ for (let ovIdx = 0; ovIdx < overrides.length; ovIdx++) {
842
+ out.push(reconstructOverride(overrides[ovIdx] as MountOverride, ovIdx, resolvedMap));
843
+ }
844
+ return ok(out);
845
+ }
846
+
847
+ /** resolvedMap key for a resolved override handle. */
848
+ function overrideFieldKey(overrideIndex: number, fieldName: string, arrayIndex?: number): string {
849
+ return `${overrideIndex}|${fieldName}|${arrayIndex ?? ''}`;
850
+ }
851
+
852
+ /**
853
+ * Rebuild one override with its shared-field GUID strings replaced by resolved
854
+ * handles from `resolvedMap`. Preserves object identity when nothing resolved
855
+ * (no shared GUID in this override). Patch form substitutes the single `value`;
856
+ * add form substitutes each key of the value map.
857
+ */
858
+ function reconstructOverride(
859
+ ov: MountOverride,
860
+ ovIdx: number,
861
+ resolvedMap: Map<string, number>,
862
+ ): MountOverride {
863
+ if (ov.field !== undefined) {
864
+ const nv = reconstructFieldValue(ovIdx, ov.field, ov.value, resolvedMap);
865
+ return nv === ov.value ? ov : { ...ov, value: nv };
866
+ }
867
+ const value = ov.value;
868
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return ov;
869
+ const map = value as Record<string, unknown>;
870
+ let changed = false;
871
+ const next: Record<string, unknown> = {};
872
+ for (const key of Object.keys(map)) {
873
+ const nv = reconstructFieldValue(ovIdx, key, map[key], resolvedMap);
874
+ next[key] = nv;
875
+ if (nv !== map[key]) changed = true;
876
+ }
877
+ return changed ? { ...ov, value: next } : ov;
878
+ }
879
+
880
+ /**
881
+ * Resolve one field value: a scalar `shared<T>` string becomes its handle; an
882
+ * `array<shared<T>>` becomes an array with resolved handles substituted and
883
+ * already-numeric elements passed through (D-8). Non-shared / unresolved values
884
+ * pass through unchanged (identity preserved).
885
+ */
886
+ function reconstructFieldValue(
887
+ ovIdx: number,
888
+ fieldName: string,
889
+ value: unknown,
890
+ resolvedMap: Map<string, number>,
891
+ ): unknown {
892
+ const scalar = resolvedMap.get(overrideFieldKey(ovIdx, fieldName));
893
+ if (scalar !== undefined) return scalar;
894
+ if (Array.isArray(value)) {
895
+ const arr: unknown[] = [];
896
+ let anyResolved = false;
897
+ for (let i = 0; i < value.length; i++) {
898
+ const resolved = resolvedMap.get(overrideFieldKey(ovIdx, fieldName, i));
899
+ if (resolved !== undefined) {
900
+ arr.push(resolved);
901
+ anyResolved = true;
902
+ } else {
903
+ arr.push(value[i]);
904
+ }
905
+ }
906
+ return anyResolved ? arr : value;
907
+ }
908
+ return value;
909
+ }
910
+
911
+ /**
912
+ * tweak-20260609 M1 helper: build the per-sub-ref parent context for a
913
+ * SceneAsset child. feat-20260622 M3 / w9: re-sourced to lookup in the
914
+ * scene envelope's ``refs[]`` edges instead of walking entity components
915
+ * via extractSceneEntityHandleGuids (D-7). When the scene envelope is not
916
+ * found in the catalog, falls back to the entity-walk path (backward compat
917
+ * for call sites that lack a catalogued envelope).
918
+ *
919
+ * Texture edges (sourceField=undefined) produce ``componentField:
920
+ * undefined`` — the breadcrumb will show GUID+kind only, no per-entity
921
+ * detail (D-2: texture has no per-entity origin).
922
+ */
923
+ export function buildSceneChildContext(
924
+ registry: AssetRegistry,
925
+ scene: Asset & { kind: 'scene' },
926
+ subGuidKey: string,
927
+ sceneGuidKey?: string,
928
+ ):
929
+ | {
930
+ sceneEntityId?: number;
931
+ componentField?: string;
932
+ sourceField?: {
933
+ componentName?: string;
934
+ fieldName: string;
935
+ arrayIndex?: number;
936
+ };
937
+ }
938
+ | undefined {
939
+ // feat-20260622 M3 / w9: direct lookup in envelope.refs edges.
940
+ // feat-20260622 review r1: address the recursing scene's OWN envelope by
941
+ // its guidKey, not the first scene in the catalog -- under a multi-scene
942
+ // glTF catalog the first-scene scan attributes the breadcrumb to the wrong
943
+ // scene. Fall back to the first-scene scan only when no guidKey is given
944
+ // (legacy call sites lacking a catalogued envelope).
945
+ let sceneEnvelope: AssetEnvelope | undefined;
946
+ if (sceneGuidKey !== undefined) {
947
+ const env = registry.assetCatalog.get(sceneGuidKey);
948
+ if (env?.kind === 'scene') sceneEnvelope = env;
949
+ }
950
+ if (sceneEnvelope === undefined) {
951
+ for (const [, env] of registry.assetCatalog) {
952
+ if (env.kind === 'scene' && env.refs !== undefined && env.refs.length > 0) {
953
+ sceneEnvelope = env;
954
+ break;
955
+ }
956
+ }
957
+ }
958
+ let edgeResult:
959
+ | {
960
+ sceneEntityId?: number;
961
+ componentField?: string;
962
+ }
963
+ | undefined;
964
+ if (sceneEnvelope?.refs !== undefined) {
965
+ for (const ref of sceneEnvelope.refs) {
966
+ if (ref.guid.toLowerCase() === subGuidKey) {
967
+ const { sceneEntityId, sourceField } = ref;
968
+ const result: {
969
+ sceneEntityId?: number;
970
+ componentField?: string;
971
+ sourceField?: {
972
+ componentName?: string;
973
+ fieldName: string;
974
+ arrayIndex?: number;
975
+ };
976
+ } = {};
977
+ if (sceneEntityId !== undefined) {
978
+ result.sceneEntityId = sceneEntityId;
979
+ }
980
+ if (sourceField?.componentName !== undefined && sourceField?.fieldName !== undefined) {
981
+ result.componentField =
982
+ `${sourceField.componentName}.${sourceField.fieldName}` +
983
+ (sourceField.arrayIndex !== undefined ? `[${sourceField.arrayIndex}]` : '');
984
+ }
985
+ if (sourceField !== undefined) {
986
+ result.sourceField = sourceField;
987
+ }
988
+ // A rich edge (dev register path) carries full detail — return now.
989
+ if (result.sceneEntityId !== undefined || result.componentField !== undefined) {
990
+ return result;
991
+ }
992
+ // feat-20260622 M4 / w14: a GUID-only edge (prod path: on-disk refs[]
993
+ // strip sourceField / sceneEntityId at the serialization boundary, w7
994
+ // D-10) carries no per-entity detail. Keep this empty-but-defined
995
+ // result as the fallback, then try the entity walk below to recover
996
+ // the entity localId + component.field path (D-7 / B-8). The walk
997
+ // recovers handle-field edges (mesh / material); a texture edge (D-2:
998
+ // no per-entity origin) is not found by the walk, so the empty
999
+ // edgeResult is returned (w10 texture-edge contract preserved).
1000
+ edgeResult = result;
1001
+ break;
1002
+ }
1003
+ }
1004
+ }
1005
+ // Backward compat: fall back to entity walk when the envelope edge carries
1006
+ // no per-entity detail (prod path: GUID-only refs[]) or no envelope is
1007
+ // available (e.g. direct catalog() registration with scene payload, no refs).
1008
+ const entries = extractSceneEntityHandleGuids(
1009
+ registry.componentCatalog,
1010
+ scene.entities as unknown as ReadonlyArray<{
1011
+ readonly localId: number;
1012
+ readonly components: Record<string, Record<string, unknown>>;
1013
+ }>,
1014
+ );
1015
+ for (const entry of entries) {
1016
+ if (entry.guidString.toLowerCase() === subGuidKey) {
1017
+ return {
1018
+ sceneEntityId: entry.entityLocalId,
1019
+ componentField: `${entry.componentName}.${entry.fieldName}${entry.arrayIndex !== undefined ? `[${entry.arrayIndex}]` : ''}`,
1020
+ // feat-20260622 verify r1: also surface the recovered provenance in
1021
+ // structured parts so the failure `.detail` can expose them for AI
1022
+ // property access (charter P3), not only the concatenated hint string.
1023
+ sourceField: {
1024
+ componentName: entry.componentName,
1025
+ fieldName: entry.fieldName,
1026
+ ...(entry.arrayIndex !== undefined ? { arrayIndex: entry.arrayIndex } : {}),
1027
+ },
1028
+ };
1029
+ }
1030
+ }
1031
+ return edgeResult;
1032
+ }
1033
+
1034
+ /**
1035
+ * tweak-20260609 M1 helper: build the error-hint breadcrumb string
1036
+ * containing the parent asset's GUID + kind, enriched with the
1037
+ * caller-provided `parentContext` (entity localId + component.field).
1038
+ *
1039
+ * Per D-7 / B-8: the breadcrumb appears before the sub-asset's own hint,
1040
+ * separated by " / ".
1041
+ */
1042
+ export function buildBreadcrumbHint(
1043
+ parentGuidKey: string,
1044
+ parentKind: string,
1045
+ subGuidKey: string,
1046
+ parentContext?: {
1047
+ sceneEntityId?: number;
1048
+ componentField?: string;
1049
+ },
1050
+ ): string {
1051
+ let breadcrumb = `sub-asset ${subGuidKey} referenced by ${parentKind} ${parentGuidKey}`;
1052
+ if (parentContext?.sceneEntityId !== undefined && parentContext?.componentField !== undefined) {
1053
+ breadcrumb += ` (entity ${parentContext.sceneEntityId}, field ${parentContext.componentField})`;
1054
+ }
1055
+ return breadcrumb;
1056
+ }