@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1

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 (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,692 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * @module engineRouter
5
+ * @description
6
+ * The single source of truth for engine path routing.
7
+ *
8
+ * Four named profiles:
9
+ * - `DEV_CORE_PROFILE` — in-core dev mount (`lib/server` plugin serves `/frqtl`, `/fat`).
10
+ * - `DIST_PROFILE` — flat dist layout, same-origin RELATIVE paths (`sdk/frqtl-web/dist`;
11
+ * baked into the dist pages at build time by `sdk/frqtl-web/build.js`).
12
+ * - `CDN_PROFILE` — the SAME dist layout, resolved to ABSOLUTE jsdelivr URLs
13
+ * (`https://cdn.jsdelivr.net/npm/@descix/frqtl-web@<ver>/dist/...`) for static hosting
14
+ * with no server process at all (ws-artifact-showcase, CEO 2026-07-10). CDN_PROFILE is
15
+ * a DISTINCT profile object, not a DIST_PROFILE parameterization (locked ruling —
16
+ * docs/design/frqtl-artifact-showcase-scoping-2026-07-10.md §5.2): DIST_PROFILE's
17
+ * relative bases are meaningless outside the exact dist/experiments/{id}/ page
18
+ * context they were computed for, whereas CDN_PROFILE's
19
+ * absolute bases resolve correctly from ANY host page (a claude.ai Artifact iframe has
20
+ * no such anchor point in the dist tree at all).
21
+ * - the static profile (`buildStaticProfile(graphBase, entryBase)`) — the SAME dist layout,
22
+ * its graph under the site's build-stamped `./pub/<build>` directory and its experiment
23
+ * entry pages at the fixed `./pub/experiments/`, RELATIVE TO THE DOCUMENT, with the math +
24
+ * notebook lanes co-served in the same tree (self-contained no-backend site served under
25
+ * ANY path prefix — the DeSciX platform serves it at /<env>/<app_id>/site/). Like
26
+ * CDN_PROFILE it is a DISTINCT profile (its math/notebook bases point SAME-ORIGIN).
27
+ *
28
+ * Profile parity is asserted at module load time: all four profiles MUST cover the same
29
+ * non-private path keys or an error is thrown (prevents dev/dist/cdn/static divergence).
30
+ *
31
+ * @example
32
+ * import { engineRouter } from 'lib/shim';
33
+ * const router = engineRouter({ mode: 'dev-core', engine: 'wasm' });
34
+ * // router.resolve(relPath)→ string (URL of a frqtl-relative path, e.g. 'experiments/_shared/boot.js')
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ // ── Path profiles ────────────────────────────────────────────────────────────
40
+ //
41
+ // Each profile maps a frqtl-relative prefix → URL prefix.
42
+ // The FIRST matching prefix wins (insertion order).
43
+ //
44
+ // DEV_CORE_PROFILE uses the lib/server plugin's in-process mounts:
45
+ // /frqtl → lib/frqtl /fat → lib/fat
46
+ //
47
+ // DIST_PROFILE uses the flat dist layout inside sdk/frqtl-web/dist/
48
+ // (mirrors what build.js:254-271 used to hand-synthesize into the html).
49
+
50
+ var DEV_CORE_PROFILE = {
51
+ // THE package root (lib/server/mounts.js serves lib/ as lib-identity siblings: /frqtl, /shim, /fat).
52
+ // Every game-layer path (LIB_IDENTITY_PREFIXES) resolves under it; this is the one game-layer fact.
53
+ '__frqtlRoot': '/',
54
+ // External-geometry fixtures (lib/frqtl/assets/models/) — asset DATA a page's own render.mesh call
55
+ // resolves; the dists place it outside the lib-identity tree (js/models/).
56
+ 'assets/models/': '/frqtl/assets/models/',
57
+ // The experiment ENTRY pages (a general experiments/<id>/ path; experiments/_shared/ is lib identity).
58
+ 'experiments/': '/frqtl/experiments/',
59
+ // M1: Math SDK served from SOURCE via vite's transform pipeline (dependency
60
+ // map rule 2: dev consumes source — a /pub_sdk dist route here is the
61
+ // "dev-core route pointing at a dist artifact" anti-pattern).
62
+ // /egpt-src/index.js is a virtual module resolved by fraqtlServe's
63
+ // resolveId/load hooks to sdk/egpt-math-sdk/src/browser/sdk-entry.js;
64
+ // '#egpt-math-canonical' resolves via the config() alias to lib/egpt source.
65
+ // MathView._deriveSdkBase: '/egpt-src/index.js' ends with '/index.js' ✓ —
66
+ // catalog loads from /egpt-src/editor/ (scoped static mounts in mounts.js).
67
+ '__mathSdkUrl': '/egpt-src/index.js',
68
+ // M4 fix: WASM math bridge served from SOURCE via the scoped /egpt-src/wasm
69
+ // static mount (mounts.js ← egpt-paths.js mathWasmSrcDir, the single path
70
+ // authority). Artifacts are the wasm-pack output of `npm run build:wasm-bridge`
71
+ // (sdk/egpt-math-sdk): glue module + binary. Empty strings here were the M4
72
+ // verification failure — no browser surface could ever register wasm-math.
73
+ // If the bridge isn't built the mount 404s and wasm-math SELECTION fails
74
+ // loud (Runtime._applyBackendSelection names the build command).
75
+ '__mathWasmUrl': '/egpt-src/wasm/egpt_math_sdk_wasm_bg.wasm',
76
+ '__mathWasmModuleUrl': '/egpt-src/wasm/egpt_math_sdk_wasm.js',
77
+ // M4: Math backend selection authority — the single source of truth for which
78
+ // MathBackendProvider id is selected at startup. Runtime.ensureMath().then
79
+ // calls MathBackendRegistry.select(mathBackend) if non-empty.
80
+ // Dev consumes source, so its default is 'js-reference' (registered by the dev
81
+ // math SDK entry). Every distribution profile defaults to 'wasm-math' — compute
82
+ // is WASM in every distribution, and js-reference is not registrable there.
83
+ // Normalisation: 'wasm' → 'wasm-math' (same alias as ?mathBackend=wasm URL param).
84
+ '__mathBackend': 'js-reference',
85
+ // Curated native notebooks (lib/notebooks/) — dev is the lib/server static
86
+ // mount at /notebooks (mounts.js). The notebook surfaces NEVER derive this
87
+ // base themselves; they receive router.notebookBase threaded in as a prop
88
+ // (same discipline as __mathSdkUrl / monacoBaseUrl). This is the single path
89
+ // authority for the notebook URL space — it closes the N3 carry that had
90
+ // presets.js hardcoding a '/notebooks/' literal (dev-only, 404s in dist).
91
+ '__notebookBase': '/notebooks',
92
+ // Placed vendor libs (lib/frqtl/assets/vendor/, gitignored artifacts) — the
93
+ // STABLE root-level /vendor alias mounts.js exposes (WS-NOTEBOOK-VENDOR §2a.1).
94
+ // A realm cell's bare-specifier import (`import * as d3 from 'd3'`) resolves via
95
+ // a manifest-generated import-map whose base is THIS value (vendor-importmap.js).
96
+ // Single path authority for the vendor URL space — surfaces NEVER derive '/vendor'
97
+ // themselves; they read router.vendorBase (same discipline as __notebookBase).
98
+ '__vendorBase': '/vendor'
99
+ };
100
+
101
+ // buildDistProfile(base) — the flat dist layout, every target RELATIVE to a page whose
102
+ // dist root is `base` ('../..' for a page at dist/experiments/{id}/ — DIST_PROFILE below;
103
+ // '../../<build>' for the static site's fixed experiment entry at pub/experiments/{id}/,
104
+ // whose graph sits under the build-stamped pub/<build>/). The math / notebook / vendor
105
+ // private keys sit one level ABOVE the dist root (the egpt-math-sdk dist co-served as a sibling).
106
+ function buildDistProfile(base) {
107
+ if (typeof base !== 'string' || !base || /\/$/.test(base)) {
108
+ throw new Error('[engineRouter] buildDistProfile(base) takes a relative dist-root path with no ' +
109
+ 'trailing slash (e.g. "../.."), got ' + JSON.stringify(base) + '.');
110
+ }
111
+ return {
112
+ // THE package root: the game layer and lib/shim sit at their lib-identity placement under the dist
113
+ // root (dist/frqtl/…, dist/shim/…, dist/fat/…), so every relative import resolves as in lib/.
114
+ '__frqtlRoot': base + '/',
115
+ // External-geometry fixtures — own dist subdir (js/models/).
116
+ 'assets/models/': base + '/js/models/',
117
+ // The experiment entry pages: from dist/experiments/{id}/index.html, base + '/experiments/'.
118
+ 'experiments/': base + '/experiments/',
119
+ // Math SDK path relative to dist/experiments/{id}/ (the page this '../..' base is for).
120
+ // sdk/frqtl-web/dist/experiments/{id}/ → ../../../egpt-math-sdk/index.js
121
+ '__mathSdkUrl': base + '/../egpt-math-sdk/index.js',
122
+ // M4 fix: dist-relative shape matching sdk/egpt-math-sdk/dist/wasm/ (built by
123
+ // scripts/build.mjs), same base as __mathSdkUrl (relative to dist/experiments/{id}/).
124
+ // wasm-loader normalizes relative URLs against document.baseURI in the browser
125
+ // so the dynamic import resolves page-relative, not bundle-relative.
126
+ '__mathWasmUrl': base + '/../egpt-math-sdk/wasm/egpt_math_sdk_wasm_bg.wasm',
127
+ '__mathWasmModuleUrl': base + '/../egpt-math-sdk/wasm/egpt_math_sdk_wasm.js',
128
+ // M4: Math backend default for dist — WASM (a distribution carries no JS math core).
129
+ '__mathBackend': 'wasm-math',
130
+ // Curated native notebooks relative to dist/experiments/{id}/ — same base shape as
131
+ // __mathSdkUrl (sdk/frqtl-web/dist/experiments/{id}/ → ../../../egpt-math-sdk/notebooks/).
132
+ // The sdk build copies lib/notebooks/ → dist/notebooks/ (build.mjs copyDirFromAbs).
133
+ '__notebookBase': base + '/../egpt-math-sdk/notebooks',
134
+ // Placed vendor libs in dist — same dist-relative shape as __notebookBase
135
+ // (sdk/frqtl-web/dist/experiments/{id}/ → ../../../egpt-math-sdk/vendor). The sdk build
136
+ // copies the manifest-listed vendor entries into the dist (2a-tail follow-up,
137
+ // §8.1); the LIVE consumer is dev (apps/ide).
138
+ '__vendorBase': base + '/../egpt-math-sdk/vendor'
139
+ };
140
+ }
141
+
142
+ var DIST_PROFILE = buildDistProfile('../..');
143
+
144
+ // ── CDN_PROFILE (jsdelivr static hosting — ws-artifact-showcase Bite A1) ────────
145
+ //
146
+ // Pinned-version constants — the "one constant" the packet's absolute jsdelivr
147
+ // bases resolve from (docs/design/frqtl-artifact-showcase-scoping-2026-07-10.md
148
+ // §3b). These are hardcoded (not read from package.json — this module runs in
149
+ // the browser, no fs access) and MUST be bumped alongside each npm publish;
150
+ // lib/shim/engine/test/router.test.mjs asserts them against the two
151
+ // package.json#version fields so a forgotten bump fails `make verify`, not a
152
+ // silent stale-CDN-URL bug.
153
+ var CDN_SDK_VERSION = '0.6.0-p0.1'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
154
+ var CDN_MATH_SDK_VERSION = '0.3.2'; // sdk/egpt-math-sdk/package.json#version — bound by router.test.mjs; bumped with each npm publish
155
+
156
+ /**
157
+ * The jsdelivr dist root of @descix/frqtl-web at `version` — the ONE spelling of the package's CDN URL.
158
+ * @param {string} version
159
+ * @returns {string} no trailing slash
160
+ */
161
+ function cdnDistBaseFor(version) {
162
+ if (typeof version !== 'string' || !version) {
163
+ throw new Error('[engineRouter] cdnDistBaseFor(version): a published @descix/frqtl-web version (got ' + JSON.stringify(version) + ').');
164
+ }
165
+ return 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@' + version + '/dist';
166
+ }
167
+ var CDN_BASE = cdnDistBaseFor(CDN_SDK_VERSION);
168
+ var CDN_MATH_BASE = 'https://cdn.jsdelivr.net/npm/@descix/egpt-math-sdk@' + CDN_MATH_SDK_VERSION + '/dist';
169
+
170
+ // CDN_PROFILE mirrors DIST_PROFILE's internal dist-relative targets (both profiles
171
+ // resolve into the SAME published sdk/frqtl-web/dist/ tree — see build.js) but
172
+ // expresses each target as an absolute URL anchored at a base (CDN_BASE by
173
+ // default) instead of a page-relative '../../' navigation. A non-private key
174
+ // here MUST resolve to the identical dist sub-path DIST_PROFILE's matching key
175
+ // resolves to (both are the same physical file on disk, served two different
176
+ // ways) — the parity test also asserts this dist-subpath equivalence, not just
177
+ // key-name parity.
178
+ //
179
+ // buildCdnProfile(base) — a FACTORY over `base`, so one base re-anchors EVERY URL the router
180
+ // resolves (engine, renderer, game layer, vendor, shim), never just an entry module.
181
+ // `CDN_MATH_BASE` (the sibling egpt-math-sdk package) is deliberately not parameterized: it is a
182
+ // different published package.
183
+ //
184
+ // `base` anchors the dist layout at an absolute dist root. With no base it is CDN_BASE (the
185
+ // published jsdelivr copy). With one, it is the SELF-HOSTED form (eabadir/fraqtl#165 GAP 6, CEO
186
+ // approved at 4321684c item 9): any URL serving an UNMODIFIED copy of the package's dist/ tree —
187
+ // every file dist/dist-manifest.json lists, byte for byte, at its relative path. A hand-picked
188
+ // subset is not supported: the profile below addresses every subtree of the dist, and which files
189
+ // a scene reaches is the game layer's business, not the host's. Callers: bootScene/bootRecipe
190
+ // `opts.base` (a consumer's own site), the browser parity core and the dist's own pages (their own
191
+ // dist root, derived from import.meta), and the registry door's scene reader
192
+ // (lib/shim/registry/scene-source.js). `assertDistBase` is the shape gate. The math lane stays on CDN_MATH_BASE (see
193
+ // below), so a self-hosted base re-anchors this package only.
194
+
195
+ // The shape gate for a cdn-mode base: an ABSOLUTE URL (parsable with no document) with no
196
+ // trailing slash. A relative base would resolve against the document for <script> tags but
197
+ // against the importing MODULE for the engine's dynamic import(), so one base would name two
198
+ // places; a trailing slash doubles every joined path. Both are refused, naming the base.
199
+ function assertDistBase(base) {
200
+ var ok = typeof base === 'string' && base !== '' && !/\/$/.test(base);
201
+ if (ok) { try { new URL(base); } catch (_) { ok = false; } }
202
+ if (!ok) {
203
+ throw new Error('[engineRouter] mode:"cdn" opts.base must be the absolute URL of a copy of the package\'s ' +
204
+ 'dist/ directory, with no trailing slash (e.g. "https://example.org/vendor/frqtl-web/dist"); got ' +
205
+ JSON.stringify(base) + '. From a page: new URL(\'./vendor/frqtl-web/dist\', location.href).href. (FAIL-LOUD.)');
206
+ }
207
+ return base;
208
+ }
209
+
210
+ function buildCdnProfile(base) {
211
+ assertDistBase(base);
212
+ return {
213
+ // THE package root (the same lib-identity dist tree as DIST_PROFILE, absolute at `base`).
214
+ '__frqtlRoot': base + '/',
215
+ 'assets/models/': base + '/js/models/',
216
+ 'experiments/': base + '/experiments/',
217
+ // Math SDK / math lane — INTENTIONALLY still anchored at CDN_MATH_BASE
218
+ // (the closed-over module constant), never `base` — see the factory
219
+ // docstring above: this is a different, sibling published package.
220
+ // Wired identically to DIST_PROFILE's shape. DEFERRED (packet §3b): not
221
+ // exercised by the double-slit showcase, but present so a future
222
+ // math-notebook artifact needs no new profile keys, only a version bump
223
+ // once egpt-math-sdk is published.
224
+ '__mathSdkUrl': CDN_MATH_BASE + '/index.js',
225
+ '__mathWasmUrl': CDN_MATH_BASE + '/wasm/egpt_math_sdk_wasm_bg.wasm',
226
+ '__mathWasmModuleUrl': CDN_MATH_BASE + '/wasm/egpt_math_sdk_wasm.js',
227
+ '__mathBackend': 'wasm-math',
228
+ '__notebookBase': CDN_MATH_BASE + '/notebooks',
229
+ // Vendor import-map root — frqtl-web's OWN dist/vendor/ (manifest-driven
230
+ // copy, build.js copyVendorManifestToDist()). Unlike DIST_PROFILE.__vendorBase
231
+ // (which points at the egpt-math-sdk SIBLING dist, co-served same-origin),
232
+ // a CDN host cannot assume a sibling package is co-published
233
+ // — frqtl-web must be self-sufficient for its own realm-import-map vendor
234
+ // libs (packet §3b). Uses `base` — THIS package's own dist/vendor/.
235
+ '__vendorBase': base + '/vendor'
236
+ };
237
+ }
238
+
239
+ // The default CDN profile — the SAME factory, called with the module constant.
240
+ // Every existing consumer (the parity assertion below, router.test.mjs,
241
+ // build.js's imports) gets a byte-identical object to before this refactor.
242
+ var CDN_PROFILE = buildCdnProfile(CDN_BASE);
243
+
244
+ // ── The static site profile (self-contained same-origin static host — ws-egpt-frqtl-site-deploy) ──
245
+ //
246
+ // The React FraqtlIde deployed as a dumb-static, no-backend, WASM-only site (DeSciX
247
+ // CodeSite / any GCS-backed host — no fraqtlServe, no lib/server/server.js). The host serves the
248
+ // site under a path PREFIX it chooses (/<env>/<app_id>/site/ on the platform, /p/<app_id>/
249
+ // under `descix serve`), so every location is known only RELATIVE TO THE DOCUMENT.
250
+ //
251
+ // ENTRIES vs GRAPH. The site's HTML entries (the IDE document, pub/index.html, pub/sdk.html,
252
+ // pub/artifact/*.html, pub/experiments/{id}/index.html) sit at FIXED URLs — `*.html` is
253
+ // no-store on the platform (app-developer runbook §8.1). Everything those entries LOAD (the
254
+ // sdk payload: JS, WASM, CSS, JSON, notebooks) is the GRAPH and sits under the build-stamped
255
+ // directory STATIC_SDK_DIR/<build>/, so a browser holding a file from an earlier build never
256
+ // has it addressed by new HTML. `staticGraphDir(build)` is the ONE spelling of that directory.
257
+ // The build id is read off the document's own <html SITE_BUILD_ATTR> (written by the static
258
+ // build, apps/ide/vite.config.js), never guessed.
259
+ //
260
+ // - engineRouter({mode:'static'}) = buildStaticProfile(staticSdkBase(document.baseURI, build),
261
+ // staticEntryBase(document.baseURI)) — ABSOLUTE at runtime, because a relative URL handed to
262
+ // import(), an iframe inside an experiment page, or Monaco's loader would resolve against
263
+ // the wrong anchor (the importing module, not the document).
264
+ // - engineRouter({mode:'static', documentRelative:true, build}) = the same profile with
265
+ // DOCUMENT-RELATIVE values, for a BUILD-TIME consumer whose output the document resolves
266
+ // (the import map baked into index.html).
267
+ //
268
+ // THE DELIBERATE DIVERGENCE FROM CDN_PROFILE (architect design §A.3 / §E flag 4):
269
+ // CDN_PROFILE's math + notebook bases are hardwired CROSS-ORIGIN at CDN_MATH_BASE; the
270
+ // static-site semantic is "everything self-contained under ONE origin prefix": the math SDK
271
+ // is co-served at graphBase + '/egpt-math-sdk/...' and the curated notebooks ride the
272
+ // frqtl-web dist's OWN copy at graphBase + '/notebooks'. A DISTINCT profile here is the
273
+ // correct DRY call — the SAME justification that keeps CDN_PROFILE distinct from DIST_PROFILE.
274
+ //
275
+ // buildStaticProfile(graphBase, entryBase) — every GRAPH key anchors at `graphBase`; the
276
+ // general 'experiments/' key (the experiment ENTRY pages STUDIO/GYM frame) anchors at
277
+ // `entryBase`. Both are required: a host whose entries and graph share one directory passes
278
+ // the same base twice.
279
+ function buildStaticProfile(graphBase, entryBase) {
280
+ if (!graphBase || !entryBase) {
281
+ throw new Error('[engineRouter] buildStaticProfile(graphBase, entryBase) needs both bases (got ' +
282
+ JSON.stringify(graphBase) + ', ' + JSON.stringify(entryBase) + ') — the graph and the entry ' +
283
+ 'pages are anchored separately; a co-served host passes the same base twice.');
284
+ }
285
+ return {
286
+ // THE package root: the GRAPH (immutable per build) at its lib-identity placement.
287
+ '__frqtlRoot': graphBase + '/',
288
+ 'assets/models/': graphBase + '/js/models/',
289
+ // The experiment pages are ENTRIES: resolve('experiments/{id}/index.html') is the fixed page,
290
+ // which itself points into the graph.
291
+ 'experiments/': entryBase + '/experiments/',
292
+ // Math + notebook lanes anchored SAME-ORIGIN into the co-served payload —
293
+ // the deliberate divergence from CDN_PROFILE. See the factory docstring.
294
+ '__mathSdkUrl': graphBase + '/egpt-math-sdk/index.js',
295
+ '__mathWasmUrl': graphBase + '/egpt-math-sdk/wasm/egpt_math_sdk_wasm_bg.wasm',
296
+ '__mathWasmModuleUrl': graphBase + '/egpt-math-sdk/wasm/egpt_math_sdk_wasm.js',
297
+ '__mathBackend': 'wasm-math',
298
+ // Notebooks come from the frqtl-web dist's OWN copy (build.js copyDir
299
+ // lib/notebooks → dist/notebooks) — NOT under egpt-math-sdk (architect §A.3/§B.1).
300
+ '__notebookBase': graphBase + '/notebooks',
301
+ // Vendor import-map root — frqtl-web's OWN dist/vendor/ (same as CDN, self-
302
+ // sufficient), just same-origin instead of jsdelivr.
303
+ '__vendorBase': graphBase + '/vendor'
304
+ };
305
+ }
306
+
307
+ // The static site's sdk directory, relative to the site's document. The ONLY place it is
308
+ // spelled; the entries sit directly in it, the graph under its build-stamped child.
309
+ var STATIC_SDK_DIR = './pub';
310
+
311
+ // The <html> attribute carrying the static site's build id — the cross-repo contract
312
+ // (egpt-frqtl scripts/refresh-site.sh reads it). Every reader and writer names it through here.
313
+ var SITE_BUILD_ATTR = 'data-frqtl-site-build';
314
+ // A build id: the 8-hex-digit short sha of the commit the bundle was built from.
315
+ var SITE_BUILD_ID_RE = /^[0-9a-f]{8}$/;
316
+
317
+ /**
318
+ * The validated build id, or a thrown error naming what was supplied.
319
+ * @param {string} build
320
+ * @returns {string}
321
+ */
322
+ function assertSiteBuildId(build) {
323
+ if (typeof build !== 'string' || !SITE_BUILD_ID_RE.test(build)) {
324
+ throw new Error('[engineRouter] the static site build id must match ' + SITE_BUILD_ID_RE +
325
+ ' (the bundle\'s <html ' + SITE_BUILD_ATTR + '>), got ' + JSON.stringify(build) + '.');
326
+ }
327
+ return build;
328
+ }
329
+
330
+ /**
331
+ * The build-stamped graph directory, relative to the document — './pub/<build>'. The ONE
332
+ * spelling of the stamped prefix.
333
+ * @param {string} build
334
+ * @returns {string}
335
+ */
336
+ function staticGraphDir(build) {
337
+ return STATIC_SDK_DIR + '/' + assertSiteBuildId(build);
338
+ }
339
+
340
+ function _requireDocumentBase(documentBaseURI) {
341
+ if (!documentBaseURI) {
342
+ throw new Error('[engineRouter] mode:"static" resolves against the document — no document.baseURI here. ' +
343
+ 'Outside a browser pass opts.documentBaseURI (the URL of the directory serving the static site).');
344
+ }
345
+ return documentBaseURI;
346
+ }
347
+
348
+ /**
349
+ * The static GRAPH's absolute base for a document: staticGraphDir(build) resolved against the
350
+ * document's base URI (e.g. 'https://h/prod/app/site/index.html?preset=author', 'ab12cd34' →
351
+ * 'https://h/prod/app/site/pub/ab12cd34'). FAILS LOUD without a base URI or a valid build id.
352
+ * @param {string} documentBaseURI
353
+ * @param {string} build
354
+ * @returns {string}
355
+ */
356
+ function staticSdkBase(documentBaseURI, build) {
357
+ return new URL(staticGraphDir(build), _requireDocumentBase(documentBaseURI)).href;
358
+ }
359
+
360
+ /**
361
+ * The static ENTRY directory's absolute base for a document (e.g. 'https://h/prod/app/site/pub')
362
+ * — where the fixed HTML entries live.
363
+ * @param {string} documentBaseURI
364
+ * @returns {string}
365
+ */
366
+ function staticEntryBase(documentBaseURI) {
367
+ return new URL(STATIC_SDK_DIR, _requireDocumentBase(documentBaseURI)).href;
368
+ }
369
+
370
+ /**
371
+ * The build id a static document carries on <html SITE_BUILD_ATTR>. THROWS when absent.
372
+ * @returns {string}
373
+ */
374
+ function documentSiteBuild() {
375
+ var attr = (typeof document !== 'undefined' && document.documentElement &&
376
+ typeof document.documentElement.getAttribute === 'function')
377
+ ? document.documentElement.getAttribute(SITE_BUILD_ATTR) : null;
378
+ if (!attr) {
379
+ throw new Error('[engineRouter] mode:"static": this document carries no build stamp (<html ' +
380
+ SITE_BUILD_ATTR + '>) — only the assembled bundle\'s pages can resolve the stamped graph ' +
381
+ '(make static-deploy).');
382
+ }
383
+ return assertSiteBuildId(attr);
384
+ }
385
+
386
+ // The static IDE bundle's entry document, beside its front page. The bundle ships THIS name and no
387
+ // index.html: a site's index.html is the publisher's front page (host-view.js::HOST_FRONT_PAGE),
388
+ // which the IDE brand links home to.
389
+ var STATIC_APP_PAGE = 'app.html';
390
+
391
+ // The static IDE bundle's own chunk directory (vite assetsDir), relative to the document. Not
392
+ // `assets/`: the DeSciX platform routes `/assets/` on an app subdomain to app MEDIA, not the site.
393
+ var STATIC_APP_ASSETS_DIR = 'app-assets';
394
+ // The ONE stable-named (unhashed) static entry: the host-view declarer, which a publisher's
395
+ // front page beside the IDE loads by name. apps/ide/vite.config.js emits it here and the static
396
+ // URL gate asserts it is served (and that it imports nothing a release can delete).
397
+ var STATIC_HOST_VIEW_ENTRY = STATIC_APP_ASSETS_DIR + '/host-view.js';
398
+
399
+ // Parity assertion: all FOUR profiles must cover identical non-private keys.
400
+ // Private keys begin with '__'. The static profile is a factory; a sample instance is checked.
401
+ var _assertProfileParity = (function () {
402
+ var STATIC_SAMPLE = buildStaticProfile(staticGraphDir('00000000'), STATIC_SDK_DIR);
403
+ var devKeys = Object.keys(DEV_CORE_PROFILE).filter(function (k) { return k.indexOf('__') !== 0; }).sort();
404
+ var distKeys = Object.keys(DIST_PROFILE).filter(function (k) { return k.indexOf('__') !== 0; }).sort();
405
+ var cdnKeys = Object.keys(CDN_PROFILE).filter(function (k) { return k.indexOf('__') !== 0; }).sort();
406
+ var staticKeys = Object.keys(STATIC_SAMPLE).filter(function (k) { return k.indexOf('__') !== 0; }).sort();
407
+ if (devKeys.join(',') !== distKeys.join(',') ||
408
+ devKeys.join(',') !== cdnKeys.join(',') ||
409
+ devKeys.join(',') !== staticKeys.join(',')) {
410
+ throw new Error('[engineRouter] Profile parity FAIL: dev/dist/cdn/static profiles have different path keys.\n' +
411
+ 'dev: ' + devKeys.join(', ') + '\n' +
412
+ 'dist: ' + distKeys.join(', ') + '\n' +
413
+ 'cdn: ' + cdnKeys.join(', ') + '\n' +
414
+ 'static: ' + staticKeys.join(', '));
415
+ }
416
+ // The vendor base (__vendorBase) is a PRIVATE key but it MUST exist in ALL
417
+ // FOUR profiles (the single path authority for the /vendor URL space — every
418
+ // host resolves a realm import-map base through it). Assert it explicitly so a
419
+ // profile can never half-define it (WS-NOTEBOOK-VENDOR §2a.4).
420
+ if (!DEV_CORE_PROFILE.__vendorBase || !DIST_PROFILE.__vendorBase ||
421
+ !CDN_PROFILE.__vendorBase || !STATIC_SAMPLE.__vendorBase) {
422
+ throw new Error('[engineRouter] Profile parity FAIL: __vendorBase must be defined in ALL FOUR profiles.');
423
+ }
424
+ return true;
425
+ })();
426
+
427
+ // ── engineRouter factory ─────────────────────────────────────────────────────
428
+
429
+ /**
430
+ * @typedef {object} EngineRouter
431
+ * @property {(relPath: string) => string} resolve
432
+ * Map a frqtl-relative path (e.g. `'experiments/_shared/boot.js'`) to an absolute URL
433
+ * according to the active profile. Returns `''` for paths omitted in dist (rule 5).
434
+ * @property {'wasm'} engine
435
+ * The physics engine — always 'wasm' (FRQTL is WASM-only). Consumers read this;
436
+ * never hardcode an engine id above the shim.
437
+ * @property {string} mathSdkUrl — egpt-math-sdk bundle URL.
438
+ * @property {string} mathWasmUrl — WASM math bridge binary URL.
439
+ * @property {string} mathWasmModuleUrl — WASM math bridge glue module URL.
440
+ * @property {string} mathBackend — active math backend id (`'js-reference'` | `'wasm-math'`).
441
+ * @property {string} notebookBase — curated-notebook URL base (dev: `/notebooks`).
442
+ * @property {string} vendorBase — placed-vendor URL base (dev: `/vendor`); the
443
+ * import-map base for realm bare-specifier imports (vendor-importmap.buildImportMap).
444
+ */
445
+
446
+ /**
447
+ * Create an engine router for the given mode and engine.
448
+ *
449
+ * @param {object} [opts]
450
+ * @param {'dev-core'|'dist'|'cdn'|'static'} [opts.mode='dev-core']
451
+ * `'dev-core'` uses the `lib/server` plugin mounts (`/frqtl`, `/fat`).
452
+ * `'dist'` uses the flat `sdk/frqtl-web/dist` layout, same-origin RELATIVE paths.
453
+ * `'cdn'` uses the SAME dist layout resolved to ABSOLUTE jsdelivr URLs (static
454
+ * hosting with no server — ws-artifact-showcase).
455
+ * `'static'` uses the SAME dist layout resolved to ABSOLUTE URLs under the site's
456
+ * build-stamped `./pub/<build>` directory (experiment entries at `./pub/experiments/`),
457
+ * anchored at the document (`opts.documentBaseURI`, default `document.baseURI`), with the
458
+ * math + notebook lanes co-served in the same tree — so the site works under any path
459
+ * prefix. `opts.base` THROWS in this mode: the static base is derived, never supplied.
460
+ * @param {'wasm'} [opts.engine='wasm']
461
+ * Physics engine. FRQTL is WASM-only; an explicit `'js'` throws (archived).
462
+ * @param {string} [opts.mathSdkUrl]
463
+ * Override the egpt-math-sdk URL. Falls back to `window._egptMathSdkUrl` then
464
+ * the profile default.
465
+ * @param {string} [opts.mathWasmUrl]
466
+ * Override the WASM math bridge binary URL.
467
+ * @param {string} [opts.mathWasmModuleUrl]
468
+ * Override the WASM math bridge glue module URL.
469
+ * @param {string} [opts.mathBackend]
470
+ * Override the math backend id. `'wasm'` is normalized to `'wasm-math'`. Falls
471
+ * back to `?mathBackend=` URL param then the profile default (`__mathBackend`:
472
+ * `'js-reference'` for DEV_CORE_PROFILE, `'wasm-math'` for every distribution profile).
473
+ * @param {boolean} [opts.documentRelative]
474
+ * **`mode:'static'` ONLY.** Resolve to DOCUMENT-RELATIVE values (`./pub/<build>/...`)
475
+ * instead of anchoring at a document — for a build-time consumer whose output the document
476
+ * itself resolves (the static import map baked into index.html). Requires `opts.build`.
477
+ * Throws with any other mode, or together with `opts.documentBaseURI`.
478
+ * @param {string} [opts.build]
479
+ * **`mode:'static'` ONLY.** The site build id (`SITE_BUILD_ID_RE`). In a browser it is READ
480
+ * from the document's `<html SITE_BUILD_ATTR>` and must not be supplied; outside a browser
481
+ * (`documentBaseURI` / `documentRelative` — the build, the assembler, tests) it is required.
482
+ * @param {string} [opts.documentBaseURI]
483
+ * **`mode:'static'` ONLY.** The document URL the static site is served from (default
484
+ * `document.baseURI`). Supplied only outside a browser (the deploy assembler, tests) —
485
+ * e.g. `resolve('assets/vendor/monaco/vs')` with `'https://h/prod/app/site/'` and build
486
+ * `'ab12cd34'` → `'https://h/prod/app/site/pub/ab12cd34/frqtl/assets/vendor/monaco/vs'`.
487
+ * @param {string} [opts.base]
488
+ * **`mode:'cdn'` ONLY.** The absolute URL of the dist root every resolved URL is anchored at.
489
+ * Omitted: `CDN_BASE`, the published jsdelivr copy of this version. Supplied: the SELF-HOSTED
490
+ * form — a URL serving an UNMODIFIED copy of the package's `dist/` tree (every file
491
+ * `dist/dist-manifest.json` lists, byte for byte, at its relative path; a subset is
492
+ * unsupported). It must be absolute with no trailing slash, else this THROWS naming it
493
+ * (`assertDistBase`). The math lane (`mathSdkUrl`, `notebookBase`) is NOT re-anchored: it is
494
+ * the sibling package's CDN copy. Ignored for `mode:'dev-core'|'dist'`; throws for `'static'`.
495
+ * Consumer guide: docs/public/frqtl-sdk/COMPOSE_AND_CAPTURE.md, "Self-hosting the SDK".
496
+ * @returns {EngineRouter}
497
+ *
498
+ * @example
499
+ * import { engineRouter } from 'lib/shim';
500
+ *
501
+ * // Dev mode (used by apps/ide during `vite dev`):
502
+ * const devRouter = engineRouter({ mode: 'dev-core', engine: 'wasm' });
503
+ * console.log(devRouter.mathSdkUrl); // '/egpt-src/index.js'
504
+ *
505
+ * // Dist mode (used by sdk/frqtl-web/build.js for the packaged shell):
506
+ * const distRouter = engineRouter({ mode: 'dist', engine: 'wasm' });
507
+ * console.log(distRouter.resolve('experiments/_shared/boot.js')); // '../../frqtl/experiments/_shared/boot.js'
508
+ *
509
+ * // CDN mode (static hosting — the artifact host shell has no server, no page anchor):
510
+ * const cdnRouter = engineRouter({ mode: 'cdn', engine: 'wasm' });
511
+ * console.log(cdnRouter.resolve('compat/engine.js'));
512
+ * // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.0-p0.1/dist/frqtl/compat/engine.js'
513
+ *
514
+ * // CDN mode, self-hosted: a copy of dist/ served from the consumer's own origin:
515
+ * const selfHosted = engineRouter({ mode: 'cdn', engine: 'wasm', base: 'https://example.org/vendor/frqtl-web/dist' });
516
+ * console.log(selfHosted.resolve('compat/engine.js'));
517
+ * // 'https://example.org/vendor/frqtl-web/dist/frqtl/compat/engine.js'
518
+ * console.log(selfHosted.distBase); // 'https://example.org/vendor/frqtl-web/dist'
519
+ */
520
+ // The static profile for engineRouter's opts — the build id read from the document in a
521
+ // browser, supplied outside one; never both, never neither.
522
+ function _staticProfileFor(opts) {
523
+ var offDocument = !!(opts.documentRelative || opts.documentBaseURI);
524
+ if (offDocument && opts.build === undefined) {
525
+ throw new Error('[engineRouter] mode:"static" outside a browser needs opts.build — the site build id ' +
526
+ 'the stamped graph directory is named by (scripts/site-build.mjs siteBuildId()).');
527
+ }
528
+ if (!offDocument && opts.build !== undefined) {
529
+ throw new Error('[engineRouter] mode:"static" in a browser reads the build from <html ' + SITE_BUILD_ATTR +
530
+ '>; remove opts.build:' + JSON.stringify(opts.build) + '.');
531
+ }
532
+ if (opts.documentRelative) {
533
+ return buildStaticProfile(staticGraphDir(opts.build), STATIC_SDK_DIR);
534
+ }
535
+ var doc = _requireDocumentBase(opts.documentBaseURI || (typeof document !== 'undefined' ? document.baseURI : ''));
536
+ var build = offDocument ? opts.build : documentSiteBuild();
537
+ return buildStaticProfile(staticSdkBase(doc, build), staticEntryBase(doc));
538
+ }
539
+
540
+ /**
541
+ * The game layer's lib-identity prefixes (router-relative): every path under one resolves under the
542
+ * profile's package root (`__frqtlRoot`) at its lib path — 'shim/…' as itself, everything else under
543
+ * 'frqtl/'. The module graph needs nothing more: a module locates its imports relative to itself.
544
+ */
545
+ var LIB_IDENTITY_PREFIXES = Object.freeze(['assets/vendor/', 'compat/', 'objects/', 'parts/', 'experiments/_shared/', 'scenes/', 'shim/']);
546
+
547
+ /**
548
+ * Map a frqtl-relative path through a profile: a lib-identity path under the package root, else the
549
+ * FIRST matching public prefix (insertion order). Returns null when nothing matches — the caller decides
550
+ * what an unmapped path means.
551
+ * @param {object} profile
552
+ * @param {string} relPath
553
+ * @returns {string|null}
554
+ */
555
+ export function resolveInProfile(profile, relPath) {
556
+ for (var j = 0; j < LIB_IDENTITY_PREFIXES.length; j++) {
557
+ if (relPath.indexOf(LIB_IDENTITY_PREFIXES[j]) === 0) {
558
+ return profile.__frqtlRoot + (relPath.indexOf('shim/') === 0 ? relPath : 'frqtl/' + relPath);
559
+ }
560
+ }
561
+ var keys = Object.keys(profile).filter(function (k) { return k.indexOf('__') !== 0; });
562
+ for (var i = 0; i < keys.length; i++) {
563
+ var prefix = keys[i];
564
+ if (relPath === prefix || relPath.indexOf(prefix) === 0) {
565
+ return profile[prefix] + relPath.slice(prefix.length);
566
+ }
567
+ }
568
+ return null;
569
+ }
570
+
571
+ export function engineRouter(opts) {
572
+ opts = opts || {};
573
+ var mode = opts.mode || 'dev-core';
574
+ var engine = opts.engine || 'wasm';
575
+ // FRQTL physics is WASM-only. An explicit 'js' request FAILS LOUD — the JS
576
+ // reference engine is archived (internal/archive/frqtl-js-reference/), not a runtime backend.
577
+ if (engine === 'js') {
578
+ throw new Error('[engineRouter] JS reference engine archived — FRQTL is WASM-only. ' +
579
+ 'Remove engine:"js"; the only FRQTL physics engine is the compiled Rust/WASM engine.');
580
+ }
581
+ if (mode === 'static' && opts.base) {
582
+ throw new Error('[engineRouter] mode:"static" takes no opts.base — the static base is derived from the ' +
583
+ 'document (STATIC_SDK_DIR against document.baseURI). Remove base:' + JSON.stringify(opts.base) + '.');
584
+ }
585
+ if (opts.documentRelative && mode !== 'static') {
586
+ throw new Error('[engineRouter] opts.documentRelative applies only to mode:"static" (got mode:"' + mode + '").');
587
+ }
588
+ if (opts.documentRelative && opts.documentBaseURI) {
589
+ throw new Error('[engineRouter] opts.documentRelative and opts.documentBaseURI are exclusive — a ' +
590
+ 'document-relative router is anchored at no document.');
591
+ }
592
+ if (opts.build !== undefined && mode !== 'static') {
593
+ throw new Error('[engineRouter] opts.build applies only to mode:"static" (got mode:"' + mode + '").');
594
+ }
595
+ var profile = mode === 'dist'
596
+ ? DIST_PROFILE
597
+ : (mode === 'cdn'
598
+ ? (opts.base !== undefined ? buildCdnProfile(opts.base) : CDN_PROFILE)
599
+ : (mode === 'static'
600
+ ? _staticProfileFor(opts)
601
+ : DEV_CORE_PROFILE));
602
+
603
+ function resolve(relPath) {
604
+ // Check the __distOmit list first: files omitted in dist return '' (signal to skip).
605
+ var omitList = profile.__distOmit;
606
+ if (omitList && Array.isArray(omitList)) {
607
+ for (var oi = 0; oi < omitList.length; oi++) {
608
+ if (relPath === omitList[oi]) { return ''; }
609
+ }
610
+ }
611
+ var mapped = resolveInProfile(profile, relPath);
612
+ if (mapped !== null) { return mapped; }
613
+ // Fallback: the dev-core /frqtl mount serves lib/frqtl 1:1. Every other profile maps a
614
+ // FLATTENED or re-anchored tree, where a root-absolute '/frqtl/' guess is a silent 404
615
+ // — so an unmapped path there fails loud and names the profile key to add.
616
+ if (profile !== DEV_CORE_PROFILE) {
617
+ throw new Error('[engineRouter] mode:"' + mode + '" has no profile prefix for "' + relPath + '". ' +
618
+ 'Add the prefix to every profile in lib/shim/engine/router.js (profile parity).');
619
+ }
620
+ return '/frqtl/' + relPath;
621
+ }
622
+
623
+ // Resolve the math SDK URL:
624
+ // 1. Caller-supplied explicit override always wins.
625
+ // 2. window._egptMathSdkUrl (runtime injection used by dist shell).
626
+ // 3. Profile __mathSdkUrl entry (authoritative default per environment).
627
+ var mathSdkUrl = opts.mathSdkUrl
628
+ || (typeof window !== 'undefined' && window._egptMathSdkUrl)
629
+ || profile.__mathSdkUrl
630
+ || '';
631
+
632
+ var mathWasmUrl = opts.mathWasmUrl
633
+ || (typeof window !== 'undefined' && window._egptMathWasmUrl)
634
+ || profile.__mathWasmUrl
635
+ || '';
636
+
637
+ var mathWasmModuleUrl = opts.mathWasmModuleUrl
638
+ || (typeof window !== 'undefined' && window._egptMathWasmModuleUrl)
639
+ || profile.__mathWasmModuleUrl
640
+ || '';
641
+
642
+ // Resolve the math backend id.
643
+ // URL param ?mathBackend=wasm|js-reference|wasm-math takes the lowest priority
644
+ // (can be overridden by the caller). 'wasm' is a friendly alias for 'wasm-math'.
645
+ // Profile __mathBackend is the environment default; caller opts.mathBackend wins.
646
+ var _urlMathBackend = '';
647
+ if (typeof window !== 'undefined' && typeof URLSearchParams !== 'undefined') {
648
+ try {
649
+ _urlMathBackend = new URLSearchParams(window.location.search).get('mathBackend') || '';
650
+ } catch (_) { /* ignore in non-browser */ }
651
+ }
652
+ var mathBackend = opts.mathBackend
653
+ || _urlMathBackend
654
+ || profile.__mathBackend;
655
+ // Normalise friendly alias: 'wasm' → 'wasm-math'.
656
+ if (mathBackend === 'wasm') { mathBackend = 'wasm-math'; }
657
+
658
+ // Notebook URL base (dev=/notebooks, dist=../../../egpt-math-sdk/notebooks).
659
+ // Single path authority for the curated-notebook URL space — surfaces never
660
+ // derive it; FraqtlIde threads it into the notebook browser/pane as a prop.
661
+ var notebookBase = profile.__notebookBase || '/notebooks';
662
+
663
+ // Vendor URL base (dev=/vendor, dist=../../../egpt-math-sdk/vendor). Single path
664
+ // authority for the placed-vendor URL space — the notebook surfaces NEVER derive
665
+ // '/vendor'; the dev serve-time splice and the static bake build the import-map base from
666
+ // THIS (vendor-importmap.buildImportMap(manifest, router.vendorBase)).
667
+ var vendorBase = profile.__vendorBase || '/vendor';
668
+
669
+ return {
670
+ resolve: resolve,
671
+ // THE game-layer fact of this profile: the lib-identity package root (ends in '/').
672
+ frqtlRoot: function () { return profile.__frqtlRoot; },
673
+ // The physics engine — always 'wasm' (FRQTL is WASM-only). Consumers
674
+ // (e.g. the notebook realm-frqtl descriptor) READ this instead of hardcoding
675
+ // an engine or sniffing an audience flag (the dependency-map rule: never an
676
+ // `if (audience) engine='wasm'` branch above the shim).
677
+ engine: engine,
678
+ mathSdkUrl: mathSdkUrl,
679
+ mathWasmUrl: mathWasmUrl,
680
+ mathWasmModuleUrl: mathWasmModuleUrl,
681
+ mathBackend: mathBackend,
682
+ notebookBase: notebookBase,
683
+ vendorBase: vendorBase,
684
+ // mode:'cdn' only: the absolute dist root this router anchors at (opts.base, else
685
+ // CDN_BASE) — what a boot names when that root serves nothing. null in every other mode.
686
+ distBase: mode === 'cdn' ? (opts.base || CDN_BASE) : null
687
+ };
688
+ }
689
+
690
+
691
+ // Export profiles for the sdk build to consume.
692
+ export { LIB_IDENTITY_PREFIXES, DEV_CORE_PROFILE, DIST_PROFILE, CDN_PROFILE, STATIC_SDK_DIR, STATIC_APP_PAGE, STATIC_APP_ASSETS_DIR, STATIC_HOST_VIEW_ENTRY, SITE_BUILD_ATTR, SITE_BUILD_ID_RE, CDN_SDK_VERSION, CDN_MATH_SDK_VERSION, CDN_BASE, cdnDistBaseFor, CDN_MATH_BASE, buildCdnProfile, buildDistProfile, buildStaticProfile, staticGraphDir, staticSdkBase, staticEntryBase };