@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,274 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/controls/registry — the runtime-registerable instance registry
5
+ * ("Instance addressing").
6
+ *
7
+ * Sugar objects (`SphereEmitter`, `Tube`, a future `*Layer` class, …) and their
8
+ * declared control descriptors (`lib/shim/controls/index.js` shapes) need a
9
+ * NAMED, runtime-addressable catalog: a page may host MULTIPLE same-kind
10
+ * instances (two `SphereEmitter`s, two `Tube`s) and must add objects/controls
11
+ * dynamically (not only at `setup()` time) — named lookup + enumeration,
12
+ * FAIL-LOUD on an id collision or an unknown-id lookup (never a silent
13
+ * overwrite or a silent `undefined`).
14
+ *
15
+ * ── FACTORY, not a singleton ─────────────────────────────────────────────────
16
+ *
17
+ * Unlike `lib/shim/notebook/realm-registry.js` (a process-global singleton —
18
+ * appropriate there because there is conceptually ONE catalog of realm KINDS
19
+ * for the whole session), `makeControlRegistry()` is a FACTORY: each call
20
+ * returns an ISOLATED registry instance. A page hosting two independent
21
+ * experiment runners side-by-side (e.g. the Physics Compare / GYM view, which
22
+ * runs a JS engine and a WASM engine simultaneously) must not have one
23
+ * runner's object named `"tube"` collide with the other runner's own `"tube"` —
24
+ * each runner/experiment owns its OWN registry instance (mirrors the
25
+ * `makeFrqtlHandle`/`makeChartHandle`/`makeViewHandle` factory precedent in
26
+ * `lib/shim/notebook/`, not the single-catalog `realm-registry.js` precedent —
27
+ * the difference being INSTANCES-per-scene vs KINDS-for-the-session).
28
+ *
29
+ * Two independent catalogs share one registry instance: OBJECTS (sugar object
30
+ * instances) and CONTROLS (their declared control-descriptor arrays,
31
+ * normalized via `normalizeControlDescriptor`). A name may be registered in
32
+ * either catalog, both, or neither — an object without controls, or a bare
33
+ * control array with no backing object instance, are both legal.
34
+ *
35
+ * Usage:
36
+ * import { makeControlRegistry } from 'lib/shim/controls/registry.js';
37
+ * const registry = makeControlRegistry();
38
+ * registry.registerObject('source', sphereEmitter, 'SphereEmitter');
39
+ * registry.registerControls('source', sphereEmitter.controls);
40
+ * // … later, dynamically, from anywhere holding `registry`:
41
+ * registry.getObject('muzzleEmitter').setActive(false);
42
+ * registry.getControls('muzzleEmitter').forEach(c => host.render(c));
43
+ */
44
+
45
+ import * as ControlsSchema from './index.js';
46
+
47
+
48
+
49
+ function _isValidName(name) {
50
+ return typeof name === 'string' && name.length > 0;
51
+ }
52
+
53
+ /**
54
+ * makeControlRegistry() → a fresh, isolated registry instance.
55
+ *
56
+ * @returns {{
57
+ * registerObject: (name:string, instance:*, kind?:string) => void,
58
+ * getObject: (name:string) => *,
59
+ * getObjectKind: (name:string) => string|null,
60
+ * hasObject: (name:string) => boolean,
61
+ * objectNames: () => string[],
62
+ * unregisterObject: (name:string) => void,
63
+ * registerControls: (name:string, descriptors:object[]) => object[],
64
+ * getControls: (name:string) => object[],
65
+ * hasControls: (name:string) => boolean,
66
+ * controlNames: () => string[],
67
+ * unregisterControls: (name:string) => void,
68
+ * allControls: () => Object<string, object[]>
69
+ * }}
70
+ */
71
+ function makeControlRegistry() {
72
+ // name -> { instance, kind } — the OBJECT catalog.
73
+ var _objects = Object.create(null);
74
+ // name -> normalized descriptor[] — the CONTROLS catalog.
75
+ var _controls = Object.create(null);
76
+
77
+ // ── Objects ──────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Register a sugar-object instance under a name.
81
+ *
82
+ * @param {string} name — a non-empty, registry-unique addressing key.
83
+ * @param {*} instance — the live object (a SphereEmitter, Tube, LargeObject, …).
84
+ * @param {string} [kind] — a free-form label (e.g. 'SphereEmitter') for
85
+ * diagnostics/enumeration; not validated against any closed vocabulary
86
+ * (sugar-object KINDS are open-ended, unlike control-descriptor KINDS).
87
+ * @throws {Error} FAIL-LOUD on an invalid/empty name, a missing instance,
88
+ * or a name already registered (never a silent overwrite).
89
+ */
90
+ function registerObject(name, instance, kind) {
91
+ if (!_isValidName(name)) {
92
+ throw new Error('[controls/registry] registerObject: name must be a non-empty ' +
93
+ 'string; got ' + JSON.stringify(name) + '. (FAIL-LOUD.)');
94
+ }
95
+ if (instance === undefined || instance === null) {
96
+ throw new Error('[controls/registry] registerObject("' + name +
97
+ '"): instance must not be null/undefined. (FAIL-LOUD.)');
98
+ }
99
+ if (_objects[name]) {
100
+ throw new Error('[controls/registry] registerObject: id collision — "' + name +
101
+ '" is already registered (existing kind: "' + (_objects[name].kind || 'unknown') +
102
+ '"). Call unregisterObject("' + name + '") first, or choose a different name. ' +
103
+ '(FAIL-LOUD — never a silent overwrite.)');
104
+ }
105
+ _objects[name] = { instance: instance, kind: kind || null };
106
+ }
107
+
108
+ /**
109
+ * Resolve a registered object instance.
110
+ * @throws {Error} FAIL-LOUD if `name` is not registered.
111
+ */
112
+ function getObject(name) {
113
+ var rec = _objects[name];
114
+ if (!rec) {
115
+ throw new Error('[controls/registry] getObject: no object registered for "' +
116
+ String(name) + '". Registered: [' + objectNames().join(', ') + ']. (FAIL-LOUD.)');
117
+ }
118
+ return rec.instance;
119
+ }
120
+
121
+ /**
122
+ * Resolve a registered object's `kind` label (gym-controls-as-sugar
123
+ * binds+ControlSync FINAL design — objects-side bind execution needs BOTH
124
+ * the instance (`getObject`, unchanged) AND its kind, to route a scalar
125
+ * descriptor's `bind.object` to the correct property-accessor family
126
+ * (Frame → getFrameProperty/setFrameProperty; LargeObject → updateObjectProperty).
127
+ * Additive — `getObject` keeps returning the bare instance, unchanged, for
128
+ * every existing caller.
129
+ * @throws {Error} FAIL-LOUD if `name` is not registered (mirrors `getObject`).
130
+ */
131
+ function getObjectKind(name) {
132
+ var rec = _objects[name];
133
+ if (!rec) {
134
+ throw new Error('[controls/registry] getObjectKind: no object registered for "' +
135
+ String(name) + '". Registered: [' + objectNames().join(', ') + ']. (FAIL-LOUD.)');
136
+ }
137
+ return rec.kind;
138
+ }
139
+
140
+ /** hasObject(name) — non-throwing presence check. */
141
+ function hasObject(name) { return !!_objects[name]; }
142
+
143
+ /** objectNames() — registered object names (diagnostics/enumeration). */
144
+ function objectNames() { return Object.keys(_objects); }
145
+
146
+ /**
147
+ * Remove a registered object (frees the name for re-registration).
148
+ * @throws {Error} FAIL-LOUD if `name` is not registered — an unregister of
149
+ * an unknown name is a caller bug, never a silent no-op.
150
+ */
151
+ function unregisterObject(name) {
152
+ if (!_objects[name]) {
153
+ throw new Error('[controls/registry] unregisterObject: no object registered ' +
154
+ 'for "' + String(name) + '". (FAIL-LOUD.)');
155
+ }
156
+ delete _objects[name];
157
+ }
158
+
159
+ // ── Controls ─────────────────────────────────────────────────────────
160
+
161
+ /**
162
+ * Register a named array of control descriptors. Each descriptor is
163
+ * normalized via `normalizeControlDescriptor` (throw-or-apply — a
164
+ * malformed descriptor anywhere in the array fails the WHOLE call,
165
+ * nothing partial is committed).
166
+ *
167
+ * @param {string} name — a non-empty, registry-unique addressing key
168
+ * (independent of the OBJECTS catalog's namespace — an object and its
169
+ * controls conventionally share the same name, but this is
170
+ * convention, not enforced).
171
+ * @param {object[]} descriptors — candidate control descriptors (see
172
+ * `lib/shim/controls/index.js` KINDS for the accepted shapes).
173
+ * @returns {object[]} the normalized descriptor array (NOT the same
174
+ * array reference as the input — mirrors normalizeControlDescriptor's
175
+ * never-mutate-the-input discipline).
176
+ * @throws {Error} FAIL-LOUD on an invalid/empty name, a name already
177
+ * registered, a non-array `descriptors`, a duplicate `id` WITHIN the
178
+ * array, or any descriptor that fails `normalizeControlDescriptor`.
179
+ */
180
+ function registerControls(name, descriptors) {
181
+ if (!_isValidName(name)) {
182
+ throw new Error('[controls/registry] registerControls: name must be a ' +
183
+ 'non-empty string; got ' + JSON.stringify(name) + '. (FAIL-LOUD.)');
184
+ }
185
+ if (_controls[name]) {
186
+ throw new Error('[controls/registry] registerControls: id collision — "' + name +
187
+ '" already has controls registered. Call unregisterControls("' + name +
188
+ '") first, or choose a different name. (FAIL-LOUD — never a silent overwrite.)');
189
+ }
190
+ if (!Array.isArray(descriptors)) {
191
+ throw new Error('[controls/registry] registerControls("' + name +
192
+ '"): descriptors must be an array. (FAIL-LOUD.)');
193
+ }
194
+ var normalized = [];
195
+ var seenIds = Object.create(null);
196
+ for (var i = 0; i < descriptors.length; i++) {
197
+ var bag = ControlsSchema.normalizeControlDescriptor(descriptors[i]);
198
+ if (seenIds[bag.id]) {
199
+ throw new Error('[controls/registry] registerControls("' + name +
200
+ '"): duplicate control id "' + bag.id + '" within the same array at ' +
201
+ 'index ' + i + '. (FAIL-LOUD.)');
202
+ }
203
+ seenIds[bag.id] = true;
204
+ normalized.push(bag);
205
+ }
206
+ _controls[name] = normalized;
207
+ return normalized;
208
+ }
209
+
210
+ /**
211
+ * Resolve a registered controls array.
212
+ * @throws {Error} FAIL-LOUD if `name` is not registered.
213
+ */
214
+ function getControls(name) {
215
+ var arr = _controls[name];
216
+ if (!arr) {
217
+ throw new Error('[controls/registry] getControls: no controls registered ' +
218
+ 'for "' + String(name) + '". Registered: [' + controlNames().join(', ') +
219
+ ']. (FAIL-LOUD.)');
220
+ }
221
+ return arr;
222
+ }
223
+
224
+ /** hasControls(name) — non-throwing presence check. */
225
+ function hasControls(name) { return !!_controls[name]; }
226
+
227
+ /** controlNames() — registered control-array names (diagnostics/enumeration). */
228
+ function controlNames() { return Object.keys(_controls); }
229
+
230
+ /**
231
+ * Remove a registered controls array (frees the name for re-registration).
232
+ * @throws {Error} FAIL-LOUD if `name` is not registered.
233
+ */
234
+ function unregisterControls(name) {
235
+ if (!_controls[name]) {
236
+ throw new Error('[controls/registry] unregisterControls: no controls ' +
237
+ 'registered for "' + String(name) + '". (FAIL-LOUD.)');
238
+ }
239
+ delete _controls[name];
240
+ }
241
+
242
+ /**
243
+ * allControls() — a snapshot map `{ name: descriptor[] }` of every
244
+ * registered controls array (for a future host that enumerates
245
+ * everything without knowing names ahead of time — e.g. the
246
+ * control panel).
247
+ * @returns {Object<string, object[]>}
248
+ */
249
+ function allControls() {
250
+ var out = {};
251
+ var names = controlNames();
252
+ for (var i = 0; i < names.length; i++) { out[names[i]] = _controls[names[i]]; }
253
+ return out;
254
+ }
255
+
256
+ return {
257
+ registerObject: registerObject,
258
+ getObject: getObject,
259
+ getObjectKind: getObjectKind,
260
+ hasObject: hasObject,
261
+ objectNames: objectNames,
262
+ unregisterObject: unregisterObject,
263
+ registerControls: registerControls,
264
+ getControls: getControls,
265
+ hasControls: hasControls,
266
+ controlNames: controlNames,
267
+ unregisterControls: unregisterControls,
268
+ allControls: allControls
269
+ };
270
+ }
271
+
272
+ // ── Export ────────────────────────────────────────────────────────────────
273
+
274
+ export { makeControlRegistry };
@@ -0,0 +1,211 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * artifact-shell.js — pure generator for the claude.ai Artifact host shell.
5
+ *
6
+ * The "artifact host shell" is a SINGLE self-contained HTML file a user pastes
7
+ * into a claude.ai chat as an Artifact. It has no server, no build step, no
8
+ * relative-file dependencies of its own — every asset it needs is fetched from
9
+ * `cdn.jsdelivr.net/npm/` (the ONLY non-`self` script-src origin this shell
10
+ * needs; see §1 of the design doc for the full captured CSP).
11
+ *
12
+ * This module is PURE (no DOM, no browser globals) and node-testable. It has
13
+ * exactly two responsibilities, kept separate so the "never hand-drifted"
14
+ * requirement is mechanically enforced rather than asserted in prose:
15
+ *
16
+ * buildArtifactImportMap() — the ONE import-map entry the shell embeds,
17
+ * derived from router.js's CDN_BASE (the single
18
+ * CDN path authority — never a second hand-typed
19
+ * jsdelivr URL literal).
20
+ * renderArtifactShellHtml() — the full HTML document text, calling
21
+ * buildArtifactImportMap() internally (so the
22
+ * embedded `<script type="importmap">` can never
23
+ * drift from the generator — a conformance test
24
+ * asserts this by construction, see
25
+ * lib/shim/engine/test/artifact-shell.test.mjs).
26
+ *
27
+ * WHAT the import map holds (and why it never lists engine/renderer files): the shell's inline
28
+ * `<script type="module">` imports `bootScene` from `'@descix/frqtl-web/boot'` (the package's scene doors,
29
+ * lib/frqtl/experiments/_shared/doors.js) — everything the boot loads (engine, renderer, game layer)
30
+ * resolves relative to that module's own URL — and the embedded scene imports its parts by the
31
+ * published self-reference. Both come from the ONE specifier table (`pageImportMapFor`). Growing this
32
+ * map by hand per script would re-enumerate the module graph by hand — the schema-mirror-drift
33
+ * anti-pattern.
34
+ *
35
+ * CSP notes baked into the generated HTML (never re-derive elsewhere):
36
+ * - NO `blob:` module imports (forbidden by the captured CSP's script-src).
37
+ * - NO script/style/font origin outside the allowlist in the design doc §1
38
+ * (jsdelivr/npm, cdnjs, claudeusercontent, tailwindcss CDN, jquery CDN) —
39
+ * this shell in fact uses ONLY jsdelivr/npm plus inline scripts/styles
40
+ * ('unsafe-inline' is granted), so it is a strict subset of the allowlist.
41
+ * - The inline `<script type="module">` and the `<script type="importmap">`
42
+ * are both confirmed-PASS under this exact CSP (Step 0, design doc §1).
43
+ */
44
+
45
+ 'use strict';
46
+
47
+ import { CDN_BASE } from './router.js';
48
+ import { pageImportMapFor, FRQTL_WEB_BOOT } from '../project/specifiers.js';
49
+
50
+ /**
51
+ * The bare-specifier import map the artifact shell embeds: the ONE table, `pageImportMapFor(<the dist
52
+ * root>/)` (lib/shim/project/specifiers.js) — the scene doors (`@descix/frqtl-web/boot`) and the sketch's
53
+ * self-reference rows, at their lib-identity paths under the dist root, so the embedded scene's imports
54
+ * resolve to the SAME module URLs the boot loads.
55
+ *
56
+ * @param {object} [opts]
57
+ * @param {string} [opts.base] - The dist base (default `CDN_BASE` from router.js): the absolute URL
58
+ * (no trailing slash) of an unmodified copy of the package's dist/ tree. The PUBLISHED shell
59
+ * (`sdk/frqtl-web/build.js`) never passes it; the local mirror
60
+ * (`sdk/frqtl-web/scripts/serve-local-cdn-mirror.mjs`) does.
61
+ * @returns {{imports: Object<string,string>}}
62
+ */
63
+ export function buildArtifactImportMap(opts) {
64
+ opts = opts || {};
65
+ var base = opts.base !== undefined ? opts.base : CDN_BASE;
66
+ return pageImportMapFor(base + '/');
67
+ }
68
+
69
+ /**
70
+ * The scene the artifact shell boots — a frqtl-relative scene file (lib/frqtl/<ARTIFACT_SCENE>).
71
+ * The generator is pure: its caller reads this file and hands the TEXT in (`opts.sceneSource`),
72
+ * and the page embeds it verbatim. The scene is its one home; its constants are its knobs.
73
+ */
74
+ export var ARTIFACT_SCENE = 'scenes/double-slit.js';
75
+
76
+ /**
77
+ * The bounded wait an artifact page gives its boot before it FAILS LOUD. An SDK entry that never
78
+ * loads (a 404, an unreachable CDN, an unpublished pin) runs NO code, so nothing inside the module
79
+ * can report it — without this deadline the page sits on "booting FRQTL…" forever. The authored
80
+ * sdk/frqtl-web/src/artifact/sun.html carries BOOT_DEADLINE_MARKER, where the build splices
81
+ * bootDeadlineScript() — one script, one number.
82
+ */
83
+ export var ARTIFACT_BOOT_DEADLINE_MS = 30000;
84
+
85
+ /** Where an AUTHORED artifact page (sun.html) takes the deadline script: the build splices it here. */
86
+ export var BOOT_DEADLINE_MARKER = '<!-- FRQTL:BOOT-DEADLINE -->';
87
+
88
+ /**
89
+ * The classic deadline script (runs whether or not the module ever loads). On expiry with no live
90
+ * boot it stamps `data-egpt-boot-timeout="1"` on #frqtl-status, names the entry the page boots from
91
+ * (the import map's `@descix/frqtl-web/boot` row, else the frqtl-web.min.js <script>), and logs it. The module sets `window.__frqtlLive = true` once the boot resolves.
92
+ * @returns {string} a `<script>…</script>` element
93
+ */
94
+ export function bootDeadlineScript() {
95
+ return (
96
+ '<script>\n' +
97
+ ' /* BOOT DEADLINE — lib/shim/engine/artifact-shell.js ARTIFACT_BOOT_DEADLINE_MS. */\n' +
98
+ ' setTimeout(function () {\n' +
99
+ ' if (window.__frqtlLive) { return; }\n' +
100
+ ' var map = document.querySelector(\'script[type="importmap"]\');\n' +
101
+ ' var min = document.querySelector(\'script[src$="frqtl-web.min.js"]\');\n' +
102
+ ' var entry = min ? min.src : \'(no import map)\';\n' +
103
+ ' try { if (map) { entry = new URL(JSON.parse(map.textContent).imports[\'' + FRQTL_WEB_BOOT.specifier + '\'], document.baseURI).href; } } catch (e) { /* the message names the absence */ }\n' +
104
+ ' var msg = \'FRQTL BOOT TIMEOUT — not live after \' + (' + ARTIFACT_BOOT_DEADLINE_MS + ' / 1000) + \' s: the SDK entry \' + entry +\n' +
105
+ ' \' did not load, or its boot never reached a first tick.\';\n' +
106
+ ' var el = document.getElementById(\'frqtl-status\');\n' +
107
+ ' if (el) { el.textContent = msg; el.setAttribute(\'data-egpt-boot-timeout\', \'1\'); }\n' +
108
+ ' if (window.console && console.error) { console.error(\'[frqtl-artifact]\', msg); }\n' +
109
+ ' }, ' + ARTIFACT_BOOT_DEADLINE_MS + ');\n' +
110
+ '</script>\n'
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Render the complete, self-contained artifact host shell HTML.
116
+ *
117
+ * The document boots the scene file's sketch (ARTIFACT_SCENE, embedded VERBATIM) via
118
+ * `bootScene()` — the scene doors (lib/frqtl/experiments/_shared/doors.js) — into a mount div.
119
+ * FAIL-LOUD: `bootScene`'s own promise rejection is caught here and surfaced in the status
120
+ * line; `bootScene` itself paints a detailed error overlay inside the mount div on any
121
+ * load/compile/engine failure (never a silent blank canvas).
122
+ *
123
+ * The shell writes NO size: the mount is `width:fit-content`, sized by the canvas the scene's
124
+ * co-init call creates.
125
+ *
126
+ * WHERE THE PACKAGE COMES FROM (`opts.host`):
127
+ * 'cdn' (default) — the import map is the jsdelivr pin (buildArtifactImportMap; `opts.base`
128
+ * re-anchors it): the self-contained page a user pastes into a claude.ai Artifact.
129
+ * 'dist' — the import map is page-relative, `pageImportMapFor('../')` (placed by the build like
130
+ * sun.html's): the page the package ships boots against WHATEVER dist serves it.
131
+ * Either way the doors locate every file they load relative to their own URL, and the page carries a
132
+ * bounded boot DEADLINE (bootDeadlineScript): a silent hang fails loud.
133
+ *
134
+ * @param {object} opts
135
+ * @param {string} opts.sceneSource - REQUIRED: the text of lib/frqtl/<ARTIFACT_SCENE>, a sketch
136
+ * module (`export default function buildExperiment(exp)`), inlined into the page's module script —
137
+ * its imports resolve through the page's import map. Missing, or defining no buildExperiment, THROWS.
138
+ * @param {number} [opts.seed=42] - RNG seed.
139
+ * @param {string} [opts.base] - Forwarded to `buildArtifactImportMap` (a 'cdn' page re-anchored
140
+ * at a self-hosted dist root). Omitted by the published shell.
141
+ * @returns {string} the full `<!DOCTYPE html>...</html>` document text.
142
+ */
143
+ export function renderArtifactShellHtml(opts) {
144
+ opts = opts || {};
145
+ var sceneSource = opts.sceneSource;
146
+ if (typeof sceneSource !== 'string' || !sceneSource.trim()) {
147
+ throw new Error('[artifact-shell] renderArtifactShellHtml needs opts.sceneSource — the text of lib/frqtl/' +
148
+ ARTIFACT_SCENE + ' (the caller reads the file; this generator is pure). (FAIL-LOUD.)');
149
+ }
150
+ if (!/function buildExperiment\s*\(/.test(sceneSource)) {
151
+ throw new Error('[artifact-shell] opts.sceneSource defines no `function buildExperiment(exp)` — it must be the ' +
152
+ 'scene file ' + ARTIFACT_SCENE + '. (FAIL-LOUD.)');
153
+ }
154
+ var seed = opts.seed !== undefined ? opts.seed : 42;
155
+ var host = opts.host === undefined ? 'cdn' : opts.host;
156
+ if (host !== 'cdn' && host !== 'dist') {
157
+ throw new Error('[artifact-shell] opts.host must be "cdn" or "dist" (got ' + JSON.stringify(host) + '). (FAIL-LOUD.)');
158
+ }
159
+ if (host === 'dist' && opts.base) {
160
+ throw new Error('[artifact-shell] opts.base re-anchors the CDN pin; a "dist" page anchors at its own entry. (FAIL-LOUD.)');
161
+ }
162
+ var importMapJson = JSON.stringify(host === 'dist' ? pageImportMapFor('../') : buildArtifactImportMap(opts));
163
+
164
+ return (
165
+ '<!DOCTYPE html>\n' +
166
+ '<!-- Copyright 2023-2026 by Essam Abadir -->\n' +
167
+ '<!-- FRQTL — claude.ai Artifact host shell (ws-artifact-showcase Bite A2).\n' +
168
+ ' Generated by lib/shim/engine/artifact-shell.js — DO NOT hand-edit.\n' +
169
+ ' Paste this ENTIRE file into a claude.ai chat as an Artifact (type: HTML). -->\n' +
170
+ '<html lang="en">\n' +
171
+ '<head>\n' +
172
+ '<meta charset="UTF-8">\n' +
173
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n' +
174
+ '<title>FRQTL — Double-Slit (live)</title>\n' +
175
+ '<style>\n' +
176
+ ' html,body{margin:0;padding:0;background:#0b0b0b;color:#c9c9c9;font-family:ui-monospace,Menlo,Consolas,monospace;}\n' +
177
+ ' #frqtl-status{text-align:center;font-size:12px;color:#888;padding:10px 0 4px;}\n' +
178
+ ' #frqtl-mount{width:fit-content;margin:0 auto 24px;position:relative;}\n' +
179
+ '</style>\n' +
180
+ '</head>\n' +
181
+ '<body>\n' +
182
+ '<div id="frqtl-status" data-role="frqtl-artifact-status">booting FRQTL…</div>\n' +
183
+ '<div id="frqtl-mount" data-role="frqtl-artifact-mount"></div>\n' +
184
+ '<script type="importmap">' + importMapJson + '</script>\n' +
185
+ bootDeadlineScript() +
186
+ '<script type="module">\n' +
187
+ ' import { bootScene } from \'' + FRQTL_WEB_BOOT.specifier + '\';\n' +
188
+ ' var statusEl = document.getElementById(\'frqtl-status\');\n' +
189
+ ' var tickCount = 0;\n' +
190
+ sceneSource.replace(/\s*$/, '') + '\n' +
191
+ ' bootScene(\'frqtl-mount\', buildExperiment, {\n' +
192
+ ' seed: ' + seed + ',\n' +
193
+ ' onTick: function () {\n' +
194
+ ' tickCount++;\n' +
195
+ ' if (tickCount % 20 === 0) {\n' +
196
+ ' statusEl.textContent = \'FRQTL double-slit — live — tick \' + tickCount;\n' +
197
+ ' }\n' +
198
+ ' }\n' +
199
+ ' }).then(function () {\n' +
200
+ ' window.__frqtlLive = true;\n' +
201
+ ' statusEl.textContent = \'FRQTL double-slit — live\';\n' +
202
+ ' }).catch(function (err) {\n' +
203
+ ' window.__frqtlLive = true; // failed LOUDLY already — the deadline has nothing to add\n' +
204
+ ' statusEl.textContent = \'FRQTL boot FAILED — see overlay below.\';\n' +
205
+ ' if (window.console && console.error) { console.error(\'[frqtl-artifact]\', err); }\n' +
206
+ ' });\n' +
207
+ '</script>\n' +
208
+ '</body>\n' +
209
+ '</html>\n'
210
+ );
211
+ }
@@ -0,0 +1,44 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * boot-phases.js — THE vocabulary of a FRQTL page boot, in order.
5
+ *
6
+ * The boot record (lib/frqtl/experiments/_shared/boot.js `createBootRecord`) is written in these
7
+ * phases; a progress UI (lib/components/run-once-card, lib/features/ide/frame-boot-progress.js)
8
+ * draws one segment per phase and captions it with `means`. Every phase is 0/1: it completes when
9
+ * its promise settles (the boot imports modules — there is no script list to count).
10
+ *
11
+ * modules the parts barrel imported (every published kind registered)
12
+ * engine-ready the WASM engine booted (compat/engine.js bootEngine)
13
+ * renderer the vendor renderer loaded and its provider registered in the display slot
14
+ * sketch the page's scene built
15
+ *
16
+ * Consumers:
17
+ * - lib/frqtl/experiments/_shared/boot.js — creates the record with BOOT_PHASE_IDS.
18
+ * - lib/features/ide/frame-boot-progress.js — the host-side watcher; hands BOOT_PHASES to the card.
19
+ */
20
+
21
+ export const BOOT_PHASES = Object.freeze([
22
+ { id: 'modules', means: 'Loading the scene objects' },
23
+ { id: 'engine-ready', means: 'Starting the WASM engine' },
24
+ { id: 'renderer', means: 'Loading the renderer' },
25
+ { id: 'sketch', means: 'Building the scene' },
26
+ ].map(Object.freeze));
27
+
28
+ /** The ids, in order — what the boot record is created with. */
29
+ export const BOOT_PHASE_IDS = Object.freeze(BOOT_PHASES.map((p) => p.id));
30
+
31
+ /** The event the boot record dispatches on the document (and its frame element). */
32
+ export const BOOT_PHASE_EVENT = 'frqtl:boot-phase';
33
+
34
+ /**
35
+ * The phase row for `id`, or a thrown error naming every legal id.
36
+ * @param {string} id
37
+ */
38
+ export function bootPhase(id) {
39
+ const row = BOOT_PHASES.find((p) => p.id === id);
40
+ if (!row) {
41
+ throw new Error(`boot-phases: "${id}" is not a boot phase — ${BOOT_PHASE_IDS.join(', ')}.`);
42
+ }
43
+ return row;
44
+ }
@@ -0,0 +1,59 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * ensureEngineStack — boot the canonical engine + game layer in THIS realm and hand back the realm's
5
+ * own module instances a builder cell needs (the parts, FrqtlApp, RecipeContext, the engine accessor).
6
+ *
7
+ * Named ESM export. Lives in lib/shim so the runtime builtin `frqtl.ensureEngine` can boot the engine
8
+ * WITHOUT importing GraphicsPane (the upward-arrow veto) and WITHOUT importing lib/frqtl (the
9
+ * cross-arrow veto). It imports THE boot module (`experiments/_shared/boot.js`) from the served tree
10
+ * through the caller's router — the same module instance GraphicsPane.boot() and every experiment page
11
+ * boot through — awaits `bootRealm()`, then imports each REALM_MODULES row through the SAME router, so
12
+ * every module is the instance the boot loaded (one module, one URL). ONE boot authority; no second
13
+ * loader.
14
+ *
15
+ * REALM_MODULES key → router-relative module path (the one table of what a realm hands out)
16
+ * ensureEngineStack(router) → Promise<Object<key, namespace>> frozen; latched by the boot module for
17
+ * the document, so a later call returns the same instances, and a failed engine boot stays failed:
18
+ * recovering from one takes a fresh document.
19
+ *
20
+ * @param {object} router — an engineRouter() instance (needs .resolve)
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ /** The realm's module instances a builder cell reads, by router-relative path. */
26
+ export const REALM_MODULES = Object.freeze({
27
+ parts: 'parts/index.js',
28
+ app: 'objects/FrqtlApp.js',
29
+ recipe: 'objects/RecipeContext.js',
30
+ engine: 'compat/engine.js',
31
+ display: 'compat/renderer/display-slot.js',
32
+ QuantumEmitter: 'objects/objects/QuantumEmitter.js',
33
+ Box: 'objects/objects/Box.js',
34
+ CircuitGate: 'objects/objects/CircuitGate.js',
35
+ CircuitProbe: 'objects/objects/CircuitProbe.js'
36
+ });
37
+
38
+ export function ensureEngineStack(router) {
39
+ if (!router || typeof router.resolve !== 'function') {
40
+ return Promise.reject(new Error(
41
+ '[engine-stack-boot] ensureEngineStack requires a router (an engineRouter() instance with ' +
42
+ '.resolve, not a bare resolve function). (FAIL-LOUD.)'
43
+ ));
44
+ }
45
+ function url(rel) { return new URL(router.resolve(rel), document.baseURI).href; }
46
+ return Promise.all([import(/* @vite-ignore */ url('experiments/_shared/boot.js')),
47
+ import(/* @vite-ignore */ url('experiments/_shared/page-door.js'))]).then(function (mods) {
48
+ mods[1].refuseRetiredGlobals(); // an in-tree realm: a retired page global throws its repoint
49
+ return mods[0].bootRealm();
50
+ }).then(function () {
51
+ var keys = Object.keys(REALM_MODULES);
52
+ return Promise.all(keys.map(function (k) { return import(/* @vite-ignore */ url(REALM_MODULES[k])); }))
53
+ .then(function (mods) {
54
+ var out = {};
55
+ keys.forEach(function (k, i) { out[k] = mods[i]; });
56
+ return Object.freeze(out);
57
+ });
58
+ });
59
+ }