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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CLAUDE.md +87 -0
  2. package/LICENSE.md +664 -0
  3. package/README.md +84 -2
  4. package/dist/dist-manifest.json +218 -0
  5. package/dist/eyes/index.js +109 -0
  6. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  7. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  8. package/dist/fat/pkg-node/package.json +12 -0
  9. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  10. package/dist/frqtl/compat/boot-node.js +34 -0
  11. package/dist/frqtl/compat/engine.js +127 -0
  12. package/dist/frqtl/compat/package-root.js +24 -0
  13. package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
  14. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  15. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  16. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  17. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  18. package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
  19. package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
  20. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  21. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  22. package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
  23. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  24. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  25. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  26. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  27. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  28. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  30. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  31. package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
  32. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  33. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  34. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  35. package/dist/frqtl/compat/vendor.js +196 -0
  36. package/dist/frqtl/node-entry.js +17 -0
  37. package/dist/frqtl/objects/FrqtlApp.js +2920 -0
  38. package/dist/frqtl/objects/ObjectFactory.js +296 -0
  39. package/dist/frqtl/objects/RecipeContext.js +322 -0
  40. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  41. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  42. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  43. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  44. package/dist/frqtl/objects/instruments/capture.js +710 -0
  45. package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
  46. package/dist/frqtl/objects/kinds-entry.js +14 -0
  47. package/dist/frqtl/objects/kinds.js +190 -0
  48. package/dist/frqtl/objects/objects/Box.js +273 -0
  49. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  50. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  51. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  52. package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
  53. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  54. package/dist/frqtl/objects/objects/LightSource.js +351 -0
  55. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  56. package/dist/frqtl/objects/objects/Tube.js +471 -0
  57. package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
  58. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  59. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  60. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  61. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  62. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  66. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  67. package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
  68. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  69. package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
  70. package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
  71. package/dist/frqtl/objects/objects/tagger.js +228 -0
  72. package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
  73. package/dist/frqtl/objects/schema/index.js +256 -0
  74. package/dist/frqtl/objects/schema/scopes.js +457 -0
  75. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  76. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  77. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  78. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  79. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  80. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  81. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  82. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  83. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
  85. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  86. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  87. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  88. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  89. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  90. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  91. package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
  92. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  94. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  95. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
  96. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  97. package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
  98. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  99. package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
  100. package/dist/frqtl/objects/v1-replacements.js +29 -0
  101. package/dist/frqtl/package-core.js +23 -0
  102. package/dist/frqtl/parts/CameraControls.js +643 -0
  103. package/dist/frqtl/parts/CameraPath.js +575 -0
  104. package/dist/frqtl/parts/CollisionActions.js +34 -0
  105. package/dist/frqtl/parts/CollisionTypes.js +30 -0
  106. package/dist/frqtl/parts/DetectorWall.js +496 -0
  107. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  108. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  109. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  110. package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
  111. package/dist/frqtl/parts/LargeObject.js +405 -0
  112. package/dist/frqtl/parts/LightColor.js +255 -0
  113. package/dist/frqtl/parts/LiveChart.js +285 -0
  114. package/dist/frqtl/parts/LogicGate.js +717 -0
  115. package/dist/frqtl/parts/OvenBox.js +611 -0
  116. package/dist/frqtl/parts/PointSource.js +762 -0
  117. package/dist/frqtl/parts/Rectangle.js +200 -0
  118. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  119. package/dist/frqtl/parts/SlitScreen.js +628 -0
  120. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  121. package/dist/frqtl/parts/Trace.js +508 -0
  122. package/dist/frqtl/parts/Tunnel.js +1057 -0
  123. package/dist/frqtl/parts/Wall.js +1211 -0
  124. package/dist/frqtl/parts/index.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  126. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  127. package/dist/frqtl/scenes/atomic-model.js +88 -0
  128. package/dist/frqtl/scenes/big-bang.js +46 -0
  129. package/dist/frqtl/scenes/blackbody.js +81 -0
  130. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  131. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  132. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  133. package/dist/frqtl/scenes/counting-wall.js +38 -0
  134. package/dist/frqtl/scenes/double-slit.js +31 -0
  135. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  136. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  137. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  138. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
  139. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  140. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  141. package/dist/frqtl/scenes/empty.js +14 -0
  142. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  143. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  144. package/dist/frqtl/scenes/mirror-route.js +140 -0
  145. package/dist/frqtl/scenes/mirror.js +38 -0
  146. package/dist/frqtl/scenes/omni-3d.js +27 -0
  147. package/dist/frqtl/scenes/omni-source.js +22 -0
  148. package/dist/frqtl/scenes/self-interference.js +39 -0
  149. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  150. package/dist/frqtl/scenes/straight-walk.js +22 -0
  151. package/dist/frqtl/scenes/sun.js +39 -0
  152. package/dist/frqtl/scenes/two-source.js +37 -0
  153. package/dist/frqtl/scenes/wave.js +26 -0
  154. package/dist/shim/bundle/frqnb.js +112 -0
  155. package/dist/shim/bundle/index.js +121 -0
  156. package/dist/shim/bundle/indexHtml.js +107 -0
  157. package/dist/shim/bundle/manifest.js +95 -0
  158. package/dist/shim/bundle/zip.js +243 -0
  159. package/dist/shim/bus.js +67 -0
  160. package/dist/shim/controls/index.js +627 -0
  161. package/dist/shim/controls/registry.js +274 -0
  162. package/dist/shim/engine/artifact-shell.js +211 -0
  163. package/dist/shim/engine/boot-phases.js +44 -0
  164. package/dist/shim/engine/engine-stack-boot.js +59 -0
  165. package/dist/shim/engine/router.js +690 -0
  166. package/dist/shim/experiment-frame/index.js +296 -0
  167. package/dist/shim/expr/ast-schema.js +369 -0
  168. package/dist/shim/field-descriptor/index.js +325 -0
  169. package/dist/shim/frame-data/index.js +224 -0
  170. package/dist/shim/index.js +67 -0
  171. package/dist/shim/notebook/canvas-guard.js +82 -0
  172. package/dist/shim/notebook/catalog.js +35 -0
  173. package/dist/shim/notebook/format.js +736 -0
  174. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  175. package/dist/shim/notebook/kernel.js +671 -0
  176. package/dist/shim/notebook/knob.js +150 -0
  177. package/dist/shim/notebook/module-resolver.js +260 -0
  178. package/dist/shim/notebook/notebook-knobs.js +65 -0
  179. package/dist/shim/notebook/produced-bindings.js +67 -0
  180. package/dist/shim/notebook/realm-registry.js +121 -0
  181. package/dist/shim/notebook/realm-transport.js +73 -0
  182. package/dist/shim/notebook/setup-builder.js +507 -0
  183. package/dist/shim/notebook/store.js +159 -0
  184. package/dist/shim/notebook/vendor-importmap.js +134 -0
  185. package/dist/shim/notebook/view-builder.js +189 -0
  186. package/dist/shim/notebook/view-ctx.js +100 -0
  187. package/dist/shim/notebook/view-handle.js +202 -0
  188. package/dist/shim/notebook/warmup.js +102 -0
  189. package/dist/shim/package.json +4 -0
  190. package/dist/shim/page-params/index.js +213 -0
  191. package/dist/shim/project/identity.js +112 -0
  192. package/dist/shim/project/index.js +19 -0
  193. package/dist/shim/project/link.js +402 -0
  194. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  195. package/dist/shim/project/register-node.mjs +17 -0
  196. package/dist/shim/project/specifiers.js +106 -0
  197. package/dist/shim/registry/copy-gate.js +110 -0
  198. package/dist/shim/registry/exhibits.js +455 -0
  199. package/dist/shim/registry/experiments.js +215 -0
  200. package/dist/shim/registry/index.js +19 -0
  201. package/dist/shim/registry/scene-source.js +157 -0
  202. package/dist/shim/registry/walkthrough.js +804 -0
  203. package/dist/shim/render/capture-envelope.js +279 -0
  204. package/dist/shim/render/heat-colormap.js +92 -0
  205. package/dist/shim/render/render-stages.js +855 -0
  206. package/dist/shim/render/sun-look.js +33 -0
  207. package/dist/shim/render-state/apply.js +262 -0
  208. package/dist/shim/render-state/index.js +74 -0
  209. package/dist/shim/rpc/index.js +207 -0
  210. package/dist/shim/runtime/index.js +1023 -0
  211. package/dist/shim/storage/boot.js +69 -0
  212. package/dist/shim/storage/index.js +169 -0
  213. package/dist/shim/storage/local-storage-provider.js +230 -0
  214. package/dist/shim/storage/make-namespace-store.js +132 -0
  215. package/dist/shim/storage/modules.js +249 -0
  216. package/dist/shim/storage/projects.js +150 -0
  217. package/llms.txt +12 -0
  218. package/package.json +63 -4
@@ -0,0 +1,215 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * EXPERIMENTS — canonical experiment registry.
5
+ *
6
+ * Single ESM named export. Replaces the dual window.EXPERIMENTS + CommonJS export.
7
+ * Consumed by lib/features/ide via import; SimPane reads experiments from the prop.
8
+ *
9
+ * id : the directory name under frqtl/experiments/{id}/index.html
10
+ * title : human-readable label
11
+ * cat : category label
12
+ * jsOnly : true if the experiment requires JS-engine-only APIs
13
+ *
14
+ * Ordering (G1):
15
+ * Experiments listed in the exhibit manifest appear FIRST, in manifest order.
16
+ * Remaining experiments follow in their original declaration order.
17
+ * This ordering is consumed by SimPane's dropdown via the existing prop path
18
+ * (zero SimPane changes required).
19
+ *
20
+ * The manifest import is done lazily via a factory to avoid circular dependency
21
+ * (exhibits.js imports EXPERIMENTS from here; we cannot import exhibits.js at
22
+ * the top level of this file). The ordered list is computed once on first access.
23
+ */
24
+
25
+ /**
26
+ * EXPERIMENT_TIERS — the tier vocabulary and what each tier means for PUBLIC
27
+ * SURFACING. This table is the contract; consumers never hardcode a tier string.
28
+ *
29
+ * - A row with NO `tier` is public (absence = public; there is deliberately no
30
+ * `tier:'public'` literal to maintain on every row).
31
+ * - A row whose tier resolves `public:false` is dropped from the public
32
+ * experiment choosers by `isPublicExperiment()` / `publicExperiments()`.
33
+ * - An UNKNOWN tier value FAILS LOUD (`isPublicExperiment` throws) — a typo'd
34
+ * tier must never silently publish a lab.
35
+ * - Tier gates the CURATED public chooser (STUDIO Templates). The GYM lists
36
+ * `internalExperiments()` instead — see that function's doc.
37
+ *
38
+ * Visibility only: a non-public tier NEVER affects loadability, deep-linking,
39
+ * ordering, `is3D`, or the label lookup. Those all read the full EXPERIMENTS list
40
+ * / `experimentById()`. `sdk/frqtl-web/build.js` ships `publicExperiments()` +
41
+ * the named infra ids: a dev-tier page is absent from the npm/CDN package, but
42
+ * present in the GYM (both the dev IDE and the shipped static deploy).
43
+ *
44
+ * ── The public set is the NEW-SUGAR content ONLY ─────────────────────────────
45
+ * EVERY row below is `tier:'dev'`: none of these pages has been rebuilt on the
46
+ * current sugar objects (SphereEmitter / v2 `Wall` / `Tunnel` / LiveChart /
47
+ * GraphingDetectorWall), so none is public and none ships. The PUBLIC gallery
48
+ * content is the WALK-THROUGH rung registry (`./walkthrough.js`), which is built
49
+ * entirely on those objects; `sdk/frqtl-web/src/index.html` is its dist surface.
50
+ * An unmigrated page FAILING LOUD in a dist context is the DESIRED signal — it is
51
+ * the list of what remains to migrate. A row returns to the public set by being
52
+ * REBUILT on the new objects and having its `tier` dropped — never by exception.
53
+ *
54
+ * @type {Readonly<Record<string, Readonly<{id:string, public:boolean, label:string}>>>}
55
+ */
56
+ export const EXPERIMENT_TIERS = Object.freeze({
57
+ dev: Object.freeze({
58
+ id: 'dev',
59
+ public: false,
60
+ label: 'Developer isolation lab / demo',
61
+ }),
62
+ });
63
+
64
+ /** @type {Array<{id:string, title:string, cat:string, jsOnly?:boolean, wasmOnly?:boolean, is3D?:boolean, tier?:string}>}
65
+ * jsOnly : requires JS-engine-only APIs (no WASM build) — filtered out of Compare.
66
+ * tier : a key of EXPERIMENT_TIERS (above). `tier:'dev'` marks a DEVELOPER-tier
67
+ * isolation lab / demo: fully registered and BOOTABLE by id (direct
68
+ * experiment-page URL + STUDIO deep link — the dev surface), HIDDEN from
69
+ * the PUBLIC experiment chooser (STUDIO Templates). It stays LISTED in
70
+ * the GYM (reads `internalExperiments()`), which is what driving these
71
+ * labs is for. Omit `tier` for a public experiment.
72
+ * wasmOnly: 3D experiment; the JS reference is a frozen 2D fixture, so Compare runs
73
+ * WASM-only (JS-reference pane disabled). See CompareView filter.
74
+ * is3D : the ONE mount-time 3D-render authority (E-W0a). TRUE iff
75
+ * the experiment page unconditionally boots a 3D display — its runner is
76
+ * `new FrqtlApp({ is3D: true, … })` on the three display — the ground-truth
77
+ * signal, NOT wasmOnly (orthogonal: collision-matrix is wasmOnly+2D). STUDIO
78
+ * reads THIS field pre-boot to light up the camera strip + MOVE/LOOK drive for a
79
+ * template-loaded 3D experiment. A gate test (experiment-is3d-agreement.test.mjs)
80
+ * asserts THIS field ↔ the page's runner `is3D: true` — a hand-typed drift is a
81
+ * CI failure (mirror kill). A page that switches 3D only via a `?renderer=three`
82
+ * reinit toggle (single-source-interference) is 2D-DEFAULT → is3D omitted/false. */
83
+ const _EXPERIMENTS_BASE = [
84
+ { id: 'g1-lo-race', title: 'G1: LO races a quantum', cat: 'Engine Tests', wasmOnly: true, is3D: true, tier: 'dev' },
85
+ { id: 'g2-lo-sweep', title: 'G2: moving LO sweeps a field', cat: 'Engine Tests', wasmOnly: true, tier: 'dev' },
86
+ { id: 'g3-lo-collide', title: 'G3: Collision Lab (box-of-balls)', cat: 'Engine Tests', wasmOnly: true, tier: 'dev' },
87
+ { id: 'collision-matrix', title: 'Collision Matrix (density sweep)', cat: 'Engine Tests', wasmOnly: true, tier: 'dev' },
88
+ { id: 'tunnel-lab', title: 'Tunnel Lab (DirectedTunnel isolation)', cat: 'Engine Tests', wasmOnly: true, is3D: true, tier: 'dev' },
89
+ { id: 'lightsource-lab', title: 'LightSource Lab (LightSource isolation)', cat: 'Engine Tests', wasmOnly: true, is3D: true, tier: 'dev' },
90
+ { id: 'mesh-demo', title: 'render.mesh demo (external glTF)', cat: 'Render Effects', wasmOnly: true, is3D: true, tier: 'dev' },
91
+ { id: 'cone-emitter-3d', title: 'Cone Emitter 3D (laser ↔ flashlight)', cat: 'Wave Mechanics', wasmOnly: true, is3D: true, tier: 'dev' },
92
+ { id: 'sphere-emitter-3d', title: 'Sphere Emitter 3D (sun / laser / drum)', cat: 'Wave Mechanics', wasmOnly: true, is3D: true, tier: 'dev' },
93
+ { id: 'single-source-interference', title: 'Single-Source Self-Interference (omni, 2D)', cat: 'Wave Mechanics', wasmOnly: true, tier: 'dev' },
94
+ { id: 'tunnel-point-exit', title: 'Tunnel Point-Exit (collapsed mouth, 2D)', cat: 'Wave Mechanics', wasmOnly: true, tier: 'dev' },
95
+ { id: 'fx-glow', title: 'FX: Glow', cat: 'Render Effects', wasmOnly: true, is3D: true, tier: 'dev' },
96
+ { id: 'fx-godrays', title: 'FX: God-rays', cat: 'Render Effects', wasmOnly: true, is3D: true, tier: 'dev' },
97
+ { id: 'fx-smoke', title: 'FX: Smoke', cat: 'Render Effects', wasmOnly: true, is3D: true, tier: 'dev' },
98
+ { id: 'blackbody-substrate', title: 'Blackbody Substrate (L1+L2+L3+L4)', cat: 'Thermodynamics', tier: 'dev' },
99
+ { id: 'circuit-basic', title: 'Circuit: Series Resistor', cat: 'Computing', tier: 'dev' },
100
+ { id: 'trace-circuit', title: 'Trace: the wire part', cat: 'Computing', wasmOnly: true, tier: 'dev' },
101
+ { id: 'oscillator', title: 'Quantum Oscillator', cat: 'Structure & Forces', tier: 'dev' }
102
+ ];
103
+
104
+ /**
105
+ * The flat experiment list, in declaration order. (The showcase roster — the front-page tiles — is
106
+ * scene files, lib/shim/registry/exhibits.js; no experiment page is a tile.)
107
+ *
108
+ * @type {Array<{id:string, title:string, cat:string, jsOnly?:boolean, wasmOnly?:boolean}>}
109
+ */
110
+ export const EXPERIMENTS = _EXPERIMENTS_BASE;
111
+
112
+ /**
113
+ * isPublicExperiment(entry) — THE tier-visibility predicate. Resolves an entry's
114
+ * `tier` against the EXPERIMENT_TIERS contract: no tier ⇒ public; a known tier ⇒
115
+ * that tier's `public` flag; an unknown tier ⇒ THROWS (fail loud — a typo must
116
+ * never silently publish a dev lab).
117
+ *
118
+ * This is the ONE place the tier→visibility rule lives. Any surface that lists
119
+ * experiments to the public FERRIES this predicate (or `publicExperiments()`
120
+ * below) — re-writing `e.tier !== 'dev'` in a consumer is the schema-mirror
121
+ * anti-pattern and will drift the moment a second non-public tier is added.
122
+ *
123
+ * @param {{id?:string, tier?:string}} entry — an EXPERIMENTS row.
124
+ * @returns {boolean} true if the entry belongs in a public experiment chooser.
125
+ * @throws {Error} if `entry.tier` is not a key of EXPERIMENT_TIERS.
126
+ */
127
+ export function isPublicExperiment(entry) {
128
+ if (!entry || !entry.tier) { return true; }
129
+ var tier = EXPERIMENT_TIERS[entry.tier];
130
+ if (!tier) {
131
+ throw new Error(
132
+ '[experiments] unknown tier "' + entry.tier + '" on experiment "' +
133
+ (entry.id || '?') + '". Known tiers: ' +
134
+ Object.keys(EXPERIMENT_TIERS).join(', ') + ' (omit `tier` for public).'
135
+ );
136
+ }
137
+ return tier.public === true;
138
+ }
139
+
140
+ /**
141
+ * publicExperiments() — the EXPERIMENTS a PUBLIC chooser should list, filtered by
142
+ * `isPublicExperiment`. The single filtered view both public experiment-chooser
143
+ * surfaces (STUDIO Templates via ExperimentDropdown, the GYM/Compare select)
144
+ * consume, so the dev-tier hide is declared ONCE. Non-public experiments remain
145
+ * registered and bootable by id (direct experiment-page URL, STUDIO deep link) —
146
+ * they are only hidden from these curated lists.
147
+ *
148
+ * @returns {Array<{id:string, title:string, cat:string}>}
149
+ */
150
+ export function publicExperiments() {
151
+ return EXPERIMENTS.filter(isPublicExperiment);
152
+ }
153
+
154
+ /**
155
+ * internalExperiments() — the roster an INTERNAL-ONLY experiment chooser lists.
156
+ *
157
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
158
+ * Every registered row, tier-independent, in registry order. This is the peer of
159
+ * `publicExperiments()`, and the split is by SURFACE AUDIENCE, not by taste:
160
+ *
161
+ * - `publicExperiments()` — for a chooser a STRANGER can reach (STUDIO
162
+ * Templates). Tier gates it, because tier means "has this page been rebuilt
163
+ * on the current sugar and is it fit to publish".
164
+ * - `internalExperiments()` — for the GYM (`preset='compare'`), which drives
165
+ * dev-tier isolation labs directly by id and so needs the full roster
166
+ * regardless of tier.
167
+ *
168
+ * WHY THIS EXISTS (2026-07-28): with the whole legacy roster moved to `tier:'dev'`,
169
+ * `publicExperiments()` is legitimately EMPTY. A surface that lists it therefore
170
+ * offers nothing — which is correct for a public chooser (the public content set
171
+ * is the WALK-THROUGH registry) and wrong for the maintainer's driving
172
+ * instrument. Passing the audience question through the tier table was the
173
+ * conflation; naming the two rosters separately is the fix.
174
+ *
175
+ * A consumer picks the roster that matches ITS audience and derives everything
176
+ * else (its default selection included) from that ONE list — never a second
177
+ * hardcoded id.
178
+ *
179
+ * @returns {Array<{id:string, title:string, cat:string}>} registry rows BY REFERENCE.
180
+ */
181
+ export function internalExperiments() {
182
+ return EXPERIMENTS.slice();
183
+ }
184
+
185
+ /**
186
+ * experimentById(id) — THE id→entry resolution for the registry. Reads the FULL
187
+ * EXPERIMENTS list, tier-independent: a dev-tier experiment resolves exactly like
188
+ * a public one, which is what keeps it reachable by direct URL / deep link while
189
+ * hidden from the public choosers.
190
+ *
191
+ * @param {string} id — an experiment id (directory name under experiments/).
192
+ * @returns {{id:string, title:string, cat:string}|null} the entry, or null if unknown.
193
+ */
194
+ export function experimentById(id) {
195
+ for (var i = 0; i < EXPERIMENTS.length; i++) {
196
+ if (EXPERIMENTS[i].id === id) { return EXPERIMENTS[i]; }
197
+ }
198
+ return null;
199
+ }
200
+
201
+ /**
202
+ * experimentIs3D(id) — the ONE mount-time 3D-render predicate for a gallery
203
+ * experiment (E-W0a). Reads the declarative `is3D` field off the registry entry
204
+ * (the single authority; see the `is3D` doc on `_EXPERIMENTS_BASE`). Returns
205
+ * false for an unknown id or a 2D-default experiment. STUDIO consults this pre-boot
206
+ * for a template-loaded experiment — the axis the walkthrough path derives from
207
+ * `renderState.renderer`. Never sniffs a page or a runner: 3D-ness is DATA here.
208
+ *
209
+ * @param {string} id — an experiment id (directory name under experiments/).
210
+ * @returns {boolean}
211
+ */
212
+ export function experimentIs3D(id) {
213
+ var entry = experimentById(id);
214
+ return !!entry && entry.is3D === true;
215
+ }
@@ -0,0 +1,19 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * The registry barrel — the module `@descix/frqtl-web/registry` and `@descix/frqtl-node/registry` resolve
5
+ * to (dist/shim/registry/index.js). Host-neutral ESM: it runs unchanged in node and in a page.
6
+ * Each module below owns its own surface; this file only ferries them.
7
+ * lib/shim/package.json {"type":"module"} is why every loader (node, Playwright's transform, bundlers)
8
+ * agrees the tree is ESM, rather than each sniffing the syntax.
9
+ *
10
+ * walkthrough.js the walk-through rungs: walkthroughById, effectiveRenderState, modes
11
+ * exhibits.js the showcase roster
12
+ * experiments.js the experiment registry and its tiers
13
+ * scene-source.js loadSceneSource(id) — a rung's sketch text (fetch in a page, file read in node)
14
+ */
15
+
16
+ export * from './walkthrough.js';
17
+ export * from './exhibits.js';
18
+ export * from './experiments.js';
19
+ export * from './scene-source.js';
@@ -0,0 +1,157 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * scene-source.js — the ONE reader of a scene's sketch file, in a page AND in node.
5
+ *
6
+ * A scene is ONE file, `lib/frqtl/scenes/<file>.js`: a dual-mode `buildExperiment(exp)`
7
+ * module holding the whole scene (world, parts, render intent). A registry row carries
8
+ * its frqtl-relative PATH (`source: 'scenes/<file>.js'`), never its text. Every consumer
9
+ * that needs the text (the GYM's rung frame, STUDIO, the hero, a package consumer of
10
+ * `@descix/frqtl-web/registry`) asks here, so the id → path → URL → text chain exists once.
11
+ *
12
+ * scenePath(rung) → 'scenes/<file>.js' (THROWS on any other shape)
13
+ * loadSceneSource(scene, opts?) → Promise<string> (the file's text)
14
+ *
15
+ * `scene` is a rung id (resolved by `walkthroughById`; an unknown id REJECTS naming the
16
+ * declared ids) or a row (`{id, source}`, e.g. `EMPTY_SCENE`). The URL comes from a router,
17
+ * the one path authority (lib/shim/engine/router.js, `scenes/` profile key):
18
+ * - `opts.router` — the host's own (the IDE's engineRouter); a host always passes it.
19
+ * - absent — the PACKAGE's: the dist layout anchored at the dist root this module ships
20
+ * in (`PACKAGE_ROOT_FROM_HERE`, dist/shim/registry/ → dist/). This is the
21
+ * `@descix/frqtl-web/registry` door's reader; in the source tree it names a file that
22
+ * is not there and rejects.
23
+ * The read is a CAPABILITY split on the resolved URL, never a host sniff: a `file:` URL is
24
+ * read through node's fs (`process.getBuiltinModule`), anything else is fetched.
25
+ *
26
+ * FAIL-LOUD: an unknown id, a row without a scenes/ path, a non-200 response, an unreadable
27
+ * file or an empty body REJECTS naming the scene and the URL. A blank scene is never a result.
28
+ *
29
+ * Zero physics: this moves text. The scene's physics is whatever its file composes.
30
+ */
31
+
32
+ 'use strict';
33
+
34
+ import { WALKTHROUGH, walkthroughById } from './walkthrough.js';
35
+ import { engineRouter } from '../engine/router.js';
36
+
37
+ /** The one path prefix a scene file lives under (the router's `scenes/` profile key). */
38
+ export var SCENES_PREFIX = 'scenes/';
39
+
40
+ /**
41
+ * Where the package's dist root sits relative to this module once shipped: build.js places
42
+ * lib/shim/ at dist/shim/ (sdk/_build/lib-dist-placement.cjs), so this file is
43
+ * dist/shim/registry/scene-source.js. verify-publish-surface reads every rung through it on the
44
+ * built dist, so a moved placement fails there.
45
+ */
46
+ var PACKAGE_ROOT_FROM_HERE = '../../';
47
+
48
+ /**
49
+ * The blank world — STUDIO's "Empty Universe" — is itself a scene file (the empty page states no
50
+ * world of its own). A row-shaped handle so it reads through the same `loadSceneSource`.
51
+ */
52
+ export var EMPTY_SCENE = Object.freeze({ id: 'empty', source: 'scenes/empty.js' });
53
+
54
+ var OPT_KEYS = ['router', 'fetch'];
55
+
56
+ /**
57
+ * @param {{id?:string, source?:string}} rung — a registry row.
58
+ * @returns {string} the frqtl-relative scene path.
59
+ */
60
+ export function scenePath(rung) {
61
+ var src = rung && rung.source;
62
+ if (typeof src !== 'string' || src.indexOf(SCENES_PREFIX) !== 0 || !/\.js$/.test(src)) {
63
+ throw new Error('[scene-source] rung "' + (rung && rung.id) + '" declares source ' +
64
+ JSON.stringify(src) + ' — a scene row carries the path of its sketch file, ' +
65
+ "'scenes/<file>.js' (lib/frqtl/scenes/). (FAIL-LOUD.)");
66
+ }
67
+ return src;
68
+ }
69
+
70
+ function _rowOf(scene) {
71
+ if (typeof scene === 'string') {
72
+ var rung = walkthroughById(scene);
73
+ if (!rung) {
74
+ throw new Error('[scene-source] unknown rung id "' + scene + '" — declared ids: [' +
75
+ WALKTHROUGH.map(function (r) { return r.id; }).join(', ') + ']. (FAIL-LOUD.)');
76
+ }
77
+ return rung;
78
+ }
79
+ if (!scene || typeof scene !== 'object') {
80
+ throw new Error('[scene-source] loadSceneSource takes a rung id or a registry row, got ' +
81
+ JSON.stringify(scene) + '. (FAIL-LOUD.)');
82
+ }
83
+ return scene;
84
+ }
85
+
86
+ function _packageRouter() {
87
+ var root = new URL(PACKAGE_ROOT_FROM_HERE, import.meta.url).href.replace(/\/$/, '');
88
+ return engineRouter({ mode: 'cdn', base: root });
89
+ }
90
+
91
+ function _readFileUrl(url, id) {
92
+ var proc = globalThis.process;
93
+ var getBuiltin = proc && proc.getBuiltinModule;
94
+ if (typeof getBuiltin !== 'function') {
95
+ throw new Error('[scene-source] scene "' + id + '" resolves to ' + url + ', a file: URL; reading ' +
96
+ 'one needs node\'s process.getBuiltinModule (Node 20.16+ / 22.3+). In a page, serve the ' +
97
+ 'package over http. (FAIL-LOUD.)');
98
+ }
99
+ var fs = getBuiltin('node:fs');
100
+ var fileURLToPath = getBuiltin('node:url').fileURLToPath;
101
+ return fs.promises.readFile(fileURLToPath(url), 'utf8').catch(function (e) {
102
+ throw new Error('[scene-source] scene "' + id + '" — reading ' + url + ' failed: ' +
103
+ (e && e.message) + '. (FAIL-LOUD.)');
104
+ });
105
+ }
106
+
107
+ function _fetchText(url, id, fetchImpl) {
108
+ var f = fetchImpl || (typeof fetch === 'function' ? fetch : null);
109
+ if (!f) {
110
+ throw new Error('[scene-source] no fetch available to load ' + url + '.');
111
+ }
112
+ return Promise.resolve(f(url)).then(function (res) {
113
+ if (!res || !res.ok) {
114
+ throw new Error('[scene-source] scene "' + id + '" — GET ' + url + ' answered ' +
115
+ (res ? res.status : 'nothing') + '. (FAIL-LOUD.)');
116
+ }
117
+ return res.text();
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Read a scene file's text through a router's path authority.
123
+ * @param {string|{id:string, source:string}} scene — a rung id, or a registry row.
124
+ * @param {{router?:{resolve:function(string):string}, fetch?:function}} [opts]
125
+ * router — the host's engineRouter (absent: the package's own dist root);
126
+ * fetch — the fetch used for a non-file URL (defaults to the global; injectable for tests).
127
+ * @returns {Promise<string>}
128
+ */
129
+ export function loadSceneSource(scene, opts) {
130
+ return Promise.resolve().then(function () {
131
+ opts = opts || {};
132
+ Object.keys(opts).forEach(function (k) {
133
+ if (OPT_KEYS.indexOf(k) === -1) {
134
+ throw new Error('[scene-source] loadSceneSource has no option "' + k + '" — options: ' +
135
+ OPT_KEYS.join(', ') + '. (FAIL-LOUD.)');
136
+ }
137
+ });
138
+ var row = _rowOf(scene);
139
+ var rel = scenePath(row);
140
+ var router = opts.router || _packageRouter();
141
+ if (typeof router.resolve !== 'function') {
142
+ throw new Error('[scene-source] opts.router has no resolve() — pass an engineRouter ' +
143
+ '(lib/shim/engine/router.js) to resolve ' + rel + '. (FAIL-LOUD.)');
144
+ }
145
+ var url = router.resolve(rel);
146
+ var text = url.indexOf('file:') === 0
147
+ ? _readFileUrl(url, row.id)
148
+ : _fetchText(url, row.id, opts.fetch);
149
+ return Promise.resolve(text).then(function (t) {
150
+ if (typeof t !== 'string' || !t.trim()) {
151
+ throw new Error('[scene-source] scene "' + row.id + '" — ' + url +
152
+ ' has an empty body. (FAIL-LOUD.)');
153
+ }
154
+ return t;
155
+ });
156
+ });
157
+ }