@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,151 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * Notebook knob — the SINGLE authority for the canvas-cell driving knob
5
+ * (WS-NOTEBOOK-D GAP2).
6
+ *
7
+ * A canvas cell may carry an `opts="<key>=<value>,…"` attribute (parsed to a
8
+ * `{key: Number}` map by lib/shim/notebook/format.js — a pure module that does
9
+ * NOT validate). This module is the ONE place that:
10
+ *
11
+ * 1. validates each opts key against the exhibit knob SCHEMA, and
12
+ * 2. maps the manifest knob param name → the setup-factory option key,
13
+ *
14
+ * then returns the factory-option object the pane threads into
15
+ * GraphicsPane.loadExperiment(setupFn, opts). It is FAIL-LOUD: an unknown key,
16
+ * an out-of-range value, or a spec that defines no knob THROWS — never a silent
17
+ * ignore (that would be the hint-only/lie-in-surface class the gate forbids).
18
+ *
19
+ * ── Why this lives in lib/shim (not the pane, not exhibits.js) ────────────────
20
+ *
21
+ * The knob SCHEMA lives ONCE, notebook-scoped, in the sibling module
22
+ * lib/shim/notebook/notebook-knobs.js (NOTEBOOK_KNOBS). A canvas cell's live
23
+ * knob is a NOTEBOOK feature, so its declaration belongs with the notebook, not
24
+ * on the public-site Impossible Gallery exhibit card. This module reads that
25
+ * same-layer table (no upward dependency on lib/frqtl) and owns the MECHANISM:
26
+ * validation + the param→factory-option map.
27
+ *
28
+ * The param→factory-option map (e.g. quanta → quantumLimit) lives HERE and
29
+ * ONLY here, so the notebook canvas path and the (future) public-iframe
30
+ * set-param path cannot diverge.
31
+ *
32
+ * Named ESM export. Imports ONLY the sibling knob declaration; no Runtime, no
33
+ * Bus, no DOM, no engine path.
34
+ */
35
+
36
+ 'use strict';
37
+
38
+ import { NOTEBOOK_KNOBS } from './notebook-knobs.js';
39
+
40
+ /* ── The ONE param → setup-factory-option map ────────────────────────────────
41
+ *
42
+ * The exhibit manifest knob `param` is the human/URL-facing name ('quanta').
43
+ * The setup factory reads a different option key ('quantumLimit' in
44
+ * setupWaveInterference). This map is the SINGLE bridge between the two
45
+ * vocabularies. A param present in an exhibit knob MUST have an entry here, or
46
+ * mapping FAILS LOUD (a knob with no wire is a hint-only knob — forbidden).
47
+ *
48
+ * Keyed by manifest param name → factory option key. */
49
+ var PARAM_TO_FACTORY_OPTION = {
50
+ // wave-interference: quanta = max quanta per source = setupWaveInterference's
51
+ // `quantumLimit` option (setupWaveInterference.js:64). More quanta → denser
52
+ // emission → an observably different fringe build-up.
53
+ quanta: 'quantumLimit'
54
+ };
55
+
56
+ /* Index the notebook-scoped declaration by the setup-factory `spec` name (the
57
+ * value the builder cell threads into frqtl.knob). NOTEBOOK_KNOBS is keyed by
58
+ * notebook id; each entry names its `spec`. FAIL-LOUD on a duplicate spec so two
59
+ * notebooks cannot silently declare divergent knobs for one factory. */
60
+ var _knobBySpec = (function () {
61
+ var map = Object.create(null);
62
+ var ids = Object.keys(NOTEBOOK_KNOBS);
63
+ for (var i = 0; i < ids.length; i++) {
64
+ var entry = NOTEBOOK_KNOBS[ids[i]];
65
+ if (!entry || !entry.spec || !entry.knob) { continue; }
66
+ if (map[entry.spec]) {
67
+ throw new Error(
68
+ '[notebook/knob] duplicate knob spec "' + entry.spec + '" across notebooks ' +
69
+ '(' + ids[i] + ' collides with an earlier entry). (FAIL-LOUD.)'
70
+ );
71
+ }
72
+ map[entry.spec] = entry.knob;
73
+ }
74
+ return map;
75
+ })();
76
+
77
+ /**
78
+ * knobSchemaFor(specName) → the exhibit knob schema for a canvas cell `spec`
79
+ * (setup factory name), or null when the spec defines no knob.
80
+ *
81
+ * @param {string} specName
82
+ * @returns {{label:string, param:string, min:number, max:number, step:number, defaultValue:number}|null}
83
+ */
84
+ export function knobSchemaFor(specName) {
85
+ if (!specName) { return null; }
86
+ // The cell spec may be an ordered dependency list whose LAST member is the
87
+ // factory (notebook-pane._runRealmCell convention). Use the last segment.
88
+ var name = String(specName).split(',').pop().trim();
89
+ return _knobBySpec[name] || null;
90
+ }
91
+
92
+ /**
93
+ * validateAndMap(specName, optsMap) → factory-option object, or THROWS.
94
+ *
95
+ * FAIL-LOUD on:
96
+ * - optsMap has a key, but `specName` defines no knob at all.
97
+ * - a key that is not the spec's knob `param`.
98
+ * - a value out of the schema [min, max] range.
99
+ * - a knob `param` with no PARAM_TO_FACTORY_OPTION wire (a hint-only knob).
100
+ *
101
+ * Returns {} when optsMap is empty/absent (no knob requested — not an error).
102
+ *
103
+ * @param {string} specName — the canvas cell `spec` (setup factory name)
104
+ * @param {Object<string,number>} optsMap — parsed opts (format.js), {} when absent
105
+ * @returns {Object<string,number>} — { <factoryOption>: <value> }
106
+ */
107
+ export function validateAndMap(specName, optsMap) {
108
+ var keys = optsMap ? Object.keys(optsMap) : [];
109
+ if (keys.length === 0) { return {}; }
110
+
111
+ var schema = knobSchemaFor(specName);
112
+ if (!schema) {
113
+ throw new Error(
114
+ '[notebook/knob] cell spec "' + specName + '" defines NO knob, but opts ' +
115
+ 'declares ' + JSON.stringify(keys) + '. (FAIL-LOUD — no silent ignore.)'
116
+ );
117
+ }
118
+
119
+ var out = {};
120
+ for (var i = 0; i < keys.length; i++) {
121
+ var key = keys[i];
122
+ if (key !== schema.param) {
123
+ throw new Error(
124
+ '[notebook/knob] unknown knob key "' + key + '" for spec "' + specName +
125
+ '" — the only knob is "' + schema.param + '". (FAIL-LOUD; not silently ignored.)'
126
+ );
127
+ }
128
+ var value = optsMap[key];
129
+ if (typeof value !== 'number' || !isFinite(value)) {
130
+ throw new Error(
131
+ '[notebook/knob] knob "' + key + '" value is not a finite number: ' +
132
+ JSON.stringify(value) + '. (FAIL-LOUD.)'
133
+ );
134
+ }
135
+ if (value < schema.min || value > schema.max) {
136
+ throw new Error(
137
+ '[notebook/knob] knob "' + key + '"=' + value + ' is out of range [' +
138
+ schema.min + ', ' + schema.max + '] for spec "' + specName + '". (FAIL-LOUD.)'
139
+ );
140
+ }
141
+ var factoryOption = PARAM_TO_FACTORY_OPTION[key];
142
+ if (!factoryOption) {
143
+ throw new Error(
144
+ '[notebook/knob] knob param "' + key + '" has NO factory-option wire in ' +
145
+ 'PARAM_TO_FACTORY_OPTION — a hint-only knob is forbidden. (FAIL-LOUD.)'
146
+ );
147
+ }
148
+ out[factoryOption] = value;
149
+ }
150
+ return out;
151
+ }
@@ -0,0 +1,260 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * Notebook cell-as-module resolver — the IN-MEMORY blob closure (WS-NOTEBOOK-IMPORT
5
+ * B2; architect build-gate ruling C + VETO-CLASS constraint #1).
6
+ *
7
+ * Named ESM export. PURE module — NO Runtime/Bus/DOM/engine imports. It is
8
+ * host-neutral (it must NOT assume CodeEditorPane or any pane exists — model-gate
9
+ * ruling 6): the blob-minting + dynamic-import are injected by the caller
10
+ * (Runtime.runCell in the browser; a test stub in node).
11
+ *
12
+ * ── What it does ─────────────────────────────────────────────────────────────
13
+ *
14
+ * Given a notebook's user-code module SET (`{ name → source }`, the `type="module"`
15
+ * cells keyed by their relative path) and a builder cell body that `import`s some
16
+ * of them, it builds the dependency closure ENTIRELY IN MEMORY:
17
+ *
18
+ * - every module member becomes its OWN blob whose relative `./x.js` specifiers
19
+ * are rewritten to its SIBLINGS' blob URLs (a topological create so a
20
+ * referenced module's blob URL exists before the referencer is rewritten);
21
+ * - the entry (builder cell) keeps its top-level imports verbatim (relative
22
+ * specifiers rewritten to the sibling blob URLs) and wraps the rest in an
23
+ * exported `default async (caps, inputs) => {…}` so `caps`/`inputs` stay
24
+ * INJECTED (they are NOT imports) — the caller invokes the default export with
25
+ * the injected objects.
26
+ *
27
+ * ── THE VETO-CLASS CONSTRAINT (#1) THIS HOLDS ────────────────────────────────
28
+ *
29
+ * A relative specifier NEVER resolves to an HTTP sibling on a scoped mount — that
30
+ * is the Phase-A blob-rewriter 404 trap (a blob module has no hierarchical base,
31
+ * so statically-served siblings hit the native loader and relative sub-imports
32
+ * 404). Here a relative `./x.js` ONLY ever resolves to an in-memory sibling blob
33
+ * URL; an unresolved relative specifier FAILS LOUD. Bare specifiers (no leading
34
+ * `.`) pass through verbatim for the host's native loader (third-party / host
35
+ * resolution). There is NO network round-trip per relative import.
36
+ */
37
+
38
+ 'use strict';
39
+
40
+ /* ── path math (pure) ─────────────────────────────────────────────────────── */
41
+
42
+ // Canonicalize a module path into a stable key: drop '.' and '' segments, fold
43
+ // '..'. So './setups/x.js', 'setups/x.js' canonicalize identically, and a
44
+ // sat.js-relative './setupCircuitBasic.js' folds to the same key the module set
45
+ // is keyed by.
46
+ export function canonKey(p) {
47
+ var parts = String(p).split('/');
48
+ var out = [];
49
+ for (var i = 0; i < parts.length; i++) {
50
+ var seg = parts[i];
51
+ if (seg === '' || seg === '.') { continue; }
52
+ if (seg === '..') { out.pop(); continue; }
53
+ out.push(seg);
54
+ }
55
+ return out.join('/');
56
+ }
57
+
58
+ // Resolve a relative `specifier` against the DIRECTORY of `fromName`, returning a
59
+ // canonical key. (Both inputs are relative notebook-local paths.)
60
+ export function resolveRelativeKey(specifier, fromName) {
61
+ var fromParts = canonKey(fromName).split('/');
62
+ fromParts.pop(); // drop the file segment → directory
63
+ var base = fromParts.join('/');
64
+ var joined = base ? (base + '/' + specifier) : specifier;
65
+ return canonKey(joined);
66
+ }
67
+
68
+ /* ── import-detection + body split (pure) ─────────────────────────────────── */
69
+
70
+ // Does the source contain a real top-level `import` statement?
71
+ export function hasImport(code) {
72
+ return /(^|\n)\s*import\s/.test(String(code));
73
+ }
74
+
75
+ // Split a cell body into ITS TOP-LEVEL IMPORTS and the REST. The cell author
76
+ // writes imports at (or near) the top, possibly interleaved with leading comment
77
+ // lines and blanks (the notebook convention). ES modules require `import` at the
78
+ // top level, so we must HOIST every top-level import above the
79
+ // `async (caps, inputs) => {…}` wrapper. We collect:
80
+ // - leading blank lines + full-line comments (// … or /* … */ on one line) AND
81
+ // every top-level `import …` (single- or multi-line) into the PREAMBLE,
82
+ // - everything from the first line of real (non-import) code onward into BODY.
83
+ // A `// import x` comment is NOT an import (it is stripped of leading whitespace,
84
+ // so the `^import` test fails on the `//`). Returns { imports, body }.
85
+ export function splitImportsAndBody(code) {
86
+ var lines = String(code).split('\n');
87
+ var preamble = []; // blanks + leading comments + import statements
88
+ var bodyStart = 0;
89
+ var inImport = false;
90
+ for (var i = 0; i < lines.length; i++) {
91
+ var trimmed = lines[i].replace(/^\s+/, '');
92
+ if (inImport) {
93
+ preamble.push(lines[i]);
94
+ bodyStart = i + 1;
95
+ if (/from\s+['"][^'"]+['"]/.test(lines[i]) || /;\s*$/.test(lines[i])) { inImport = false; }
96
+ continue;
97
+ }
98
+ if (trimmed === '') { preamble.push(lines[i]); bodyStart = i + 1; continue; }
99
+ // A single-line comment or a single-line block comment — part of the
100
+ // leading preamble (so a comment ABOVE the import does not strand the
101
+ // import inside the body). A multi-line /* … */ that does not close on
102
+ // this line ends the preamble (rare in a cell header; treat as body).
103
+ if (/^\/\//.test(trimmed) || /^\/\*.*\*\/\s*$/.test(trimmed)) {
104
+ preamble.push(lines[i]); bodyStart = i + 1; continue;
105
+ }
106
+ if (/^import\s/.test(trimmed)) {
107
+ preamble.push(lines[i]);
108
+ bodyStart = i + 1;
109
+ if (!/from\s+['"][^'"]+['"]/.test(lines[i]) && !/;\s*$/.test(lines[i])) { inImport = true; }
110
+ continue;
111
+ }
112
+ break; // first line of real (non-import) code → body begins here
113
+ }
114
+ return { imports: preamble.join('\n'), body: lines.slice(bodyStart).join('\n') };
115
+ }
116
+
117
+ /* ── specifier rewrite (pure) ─────────────────────────────────────────────── */
118
+
119
+ // Rewrite every relative specifier in `source` to the blob URL of the resolved
120
+ // sibling (looked up in blobUrlByKey) — BOTH static `from '…'` clauses (import/
121
+ // export) AND dynamic `import('…')` call expressions (WS gym-controls-as-sugar
122
+ // Bite 0a REMEDIATION, Finding 2: the synthesized builder now DEFERS its
123
+ // module-cell import to a dynamic `await import(...)` placed AFTER
124
+ // `frqtl.ensureEngine()` — see setup-builder.js — so the relative specifier
125
+ // this rewrite must resolve can appear inside the function BODY, not only the
126
+ // header). Bare specifiers pass through verbatim (host loader). An unresolved
127
+ // relative specifier FAILS LOUD in either form.
128
+ export function rewriteRelativeToSiblingBlobs(source, ownName, blobUrlByKey) {
129
+ function resolveOrThrow(specifier) {
130
+ var key = resolveRelativeKey(specifier, ownName);
131
+ var blobUrl = blobUrlByKey[key];
132
+ if (!blobUrl) {
133
+ throw new Error(
134
+ '[module-resolver] relative import "' + specifier + '" (from "' + ownName +
135
+ '") resolves to "' + key + '", which is NOT a notebook-local module cell. ' +
136
+ 'Add a <cell type="module" name="' + specifier + '">, or fix the path. ' +
137
+ '(FAIL-LOUD — never an HTTP sibling fetch.)'
138
+ );
139
+ }
140
+ return blobUrl;
141
+ }
142
+ var out = String(source).replace(/from\s+(['"])([^'"]+)\1/g, function (match, quote, specifier) {
143
+ if (specifier.charAt(0) !== '.') { return match; } // bare → host loader
144
+ return 'from ' + quote + resolveOrThrow(specifier) + quote;
145
+ });
146
+ out = out.replace(/import\(\s*(['"])([^'"]+)\1\s*\)/g, function (match, quote, specifier) {
147
+ if (specifier.charAt(0) !== '.') { return match; } // bare → host loader
148
+ return 'import(' + quote + resolveOrThrow(specifier) + quote + ')';
149
+ });
150
+ return out;
151
+ }
152
+
153
+ // The relative dependency keys a module source imports.
154
+ function _relativeDepKeys(name, source) {
155
+ var deps = [];
156
+ String(source).replace(/from\s+(['"])([^'"]+)\1/g, function (m, q, spec) {
157
+ if (spec.charAt(0) === '.') { deps.push(resolveRelativeKey(spec, name)); }
158
+ return m;
159
+ });
160
+ return deps;
161
+ }
162
+
163
+ /* ── the in-memory closure ────────────────────────────────────────────────── */
164
+
165
+ /**
166
+ * buildModuleClosure(modules, mintBlobUrl) → { blobUrlByKey, revoke }
167
+ *
168
+ * Mints one blob per module member (via the injected `mintBlobUrl(source)` →
169
+ * url), each rewritten so its relative specifiers point at sibling blob URLs.
170
+ * Topological create (a referenced module's blob URL exists before the referencer
171
+ * is rewritten); FAIL-LOUD on a missing member or a dependency cycle.
172
+ *
173
+ * @param {Object<string,string>} modules — { name → source }
174
+ * @param {(source:string)=>string} mintBlobUrl — host-injected blob URL minter.
175
+ * @returns {{ blobUrlByKey: Object<string,string>, revoke: () => void }}
176
+ * revoke is a no-op here (the caller owns URL lifetime via a revokeAll it
177
+ * accumulates); we return the created URL list through revoke for symmetry.
178
+ */
179
+ export function buildModuleClosure(modules, mintBlobUrl) {
180
+ if (typeof mintBlobUrl !== 'function') {
181
+ throw new Error('[module-resolver] buildModuleClosure requires a mintBlobUrl(source) function.');
182
+ }
183
+ var byKey = Object.create(null);
184
+ var keys = Object.keys(modules || {});
185
+ for (var i = 0; i < keys.length; i++) {
186
+ byKey[canonKey(keys[i])] = { name: keys[i], source: modules[keys[i]] };
187
+ }
188
+
189
+ var blobUrlByKey = Object.create(null);
190
+ var created = [];
191
+
192
+ var remaining = Object.keys(byKey);
193
+ var guard = 0;
194
+ while (remaining.length > 0) {
195
+ if (guard++ > 100000) {
196
+ throw new Error('[module-resolver] module closure did not converge (cycle?). (FAIL-LOUD.)');
197
+ }
198
+ var madeProgress = false;
199
+ var still = [];
200
+ for (var r = 0; r < remaining.length; r++) {
201
+ var key = remaining[r];
202
+ var entry = byKey[key];
203
+ var deps = _relativeDepKeys(entry.name, entry.source);
204
+ var ready = true;
205
+ for (var d = 0; d < deps.length; d++) {
206
+ if (!blobUrlByKey[deps[d]]) {
207
+ if (!byKey[deps[d]]) {
208
+ throw new Error(
209
+ '[module-resolver] module "' + entry.name + '" imports "' + deps[d] +
210
+ '" which is not in the notebook module set. (FAIL-LOUD.)'
211
+ );
212
+ }
213
+ ready = false; break;
214
+ }
215
+ }
216
+ if (!ready) { still.push(key); continue; }
217
+ var rewritten = rewriteRelativeToSiblingBlobs(entry.source, entry.name, blobUrlByKey);
218
+ var url = mintBlobUrl(rewritten);
219
+ blobUrlByKey[key] = url;
220
+ created.push(url);
221
+ madeProgress = true;
222
+ }
223
+ remaining = still;
224
+ if (!madeProgress && remaining.length > 0) {
225
+ throw new Error(
226
+ '[module-resolver] unresolvable module closure (dependency cycle among ' +
227
+ remaining.join(', ') + '). (FAIL-LOUD.)'
228
+ );
229
+ }
230
+ }
231
+
232
+ return { blobUrlByKey: blobUrlByKey, created: created };
233
+ }
234
+
235
+ /**
236
+ * compileEntryModuleSource(code, entryName, blobUrlByKey) → string
237
+ *
238
+ * Build the entry (builder cell) module SOURCE: its top-level imports kept
239
+ * verbatim (relative specifiers rewritten to sibling blob URLs), the remaining
240
+ * statements wrapped in an exported `default async (caps, inputs) => {…}` so
241
+ * caps/inputs stay INJECTED. The caller mints the blob + dynamic-imports it +
242
+ * invokes `mod.default(caps, inputs)`.
243
+ *
244
+ * The rewrite runs on the FULL `code` (imports AND body) BEFORE splitting —
245
+ * WS gym-controls-as-sugar Bite 0a REMEDIATION, Finding 2: the synthesized
246
+ * builder's module-cell reference is now a DYNAMIC `await import('<module>')`
247
+ * placed inside the body (after `frqtl.ensureEngine()`, deliberately deferring
248
+ * evaluation — see setup-builder.js), so a relative specifier needing
249
+ * blob-URL rewrite can live in the body, not only the header. Rewriting first
250
+ * then splitting is behavior-preserving for the header-only case (the
251
+ * structural blank/comment/import scan in splitImportsAndBody does not
252
+ * inspect specifier CONTENT) and additionally catches body-level dynamic
253
+ * imports.
254
+ */
255
+ export function compileEntryModuleSource(code, entryName, blobUrlByKey) {
256
+ var rewritten = rewriteRelativeToSiblingBlobs(code, entryName, blobUrlByKey);
257
+ var split = splitImportsAndBody(rewritten);
258
+ return split.imports + '\n' +
259
+ 'export default async (caps, inputs) => {\n' + split.body + '\n};\n';
260
+ }
@@ -0,0 +1,65 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * NOTEBOOK_KNOBS — the canonical, notebook-scoped declaration of every live
5
+ * canvas-cell driving knob.
6
+ *
7
+ * This is the SINGLE home for the driving-knob SCHEMA. It is keyed by NOTEBOOK
8
+ * ID (the notebooks.json `id`, e.g. 'gallery-wave-interference'); each entry
9
+ * names the setup-factory export it binds (`spec`) and carries the knob schema
10
+ * shape ({ label, param, min, max, step, defaultValue }).
11
+ *
12
+ * ── Why the declaration lives HERE (not on the exhibit card) ──────────────────
13
+ *
14
+ * A canvas cell's live knob is a NOTEBOOK feature: the gallery notebook declares
15
+ * a canvas with `opts="<param>=<value>"`, and the shim validates + maps that
16
+ * param to a setup-factory option at run time (lib/shim/notebook/knob.js →
17
+ * frqtl.knob). The knob therefore belongs to the notebook that hosts the canvas,
18
+ * not to the Impossible Gallery exhibit card (a separate public-site curation
19
+ * surface). Keeping the schema here — a lib/shim sibling of knob.js — makes the
20
+ * notebook path self-contained: knob.js reads this table directly (a same-layer
21
+ * read), with no upward dependency on lib/shim/registry/exhibits.js.
22
+ *
23
+ * knob.js remains the MECHANISM owner (validate + param→factory-option map);
24
+ * this module is only the DECLARATION. One authority, no mirror.
25
+ *
26
+ * Named ESM export. No Runtime, no Bus, no DOM, no engine path.
27
+ */
28
+
29
+ 'use strict';
30
+
31
+ /**
32
+ * @typedef {Object} KnobSchema
33
+ * @property {string} label — human/UI label for the slider.
34
+ * @property {string} param — the URL/opts-facing knob name (e.g. 'quanta').
35
+ * @property {number} min
36
+ * @property {number} max
37
+ * @property {number} step
38
+ * @property {number} defaultValue
39
+ */
40
+
41
+ /**
42
+ * @typedef {Object} NotebookKnobEntry
43
+ * @property {string} spec — the setup-factory export name the knob drives
44
+ * (e.g. 'setupWaveInterference'); this is the
45
+ * `spec` the builder cell passes to frqtl.knob.
46
+ * @property {KnobSchema} knob — the knob schema.
47
+ */
48
+
49
+ /** @type {Object<string, NotebookKnobEntry>} keyed by notebook id. */
50
+ export const NOTEBOOK_KNOBS = {
51
+ // gallery-wave-interference: the `quanta` knob = max quanta per source =
52
+ // setupWaveInterference's `quantumLimit` option (setupWaveInterference.js).
53
+ // More quanta → denser emission → an observably different fringe build-up.
54
+ 'gallery-wave-interference': {
55
+ spec: 'setupWaveInterference',
56
+ knob: {
57
+ label: 'Particles',
58
+ param: 'quanta',
59
+ min: 100,
60
+ max: 2000,
61
+ step: 100,
62
+ defaultValue: 800,
63
+ },
64
+ },
65
+ };
@@ -0,0 +1,67 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * produced-bindings — the PURE derive-the-serializable-binding helper for the
5
+ * realm→kernel binding seam (WS-NOTEBOOK-REALM-KERNEL-BINDING). NO DOM / engine /
6
+ * Runtime / Bus import, so a test exercises it with a fake handle WITHOUT a browser.
7
+ *
8
+ * A DECLARED canvas commits its `produces` binding(s) by DERIVING each value from the
9
+ * live realm-internal runner via the FROZEN handle (`handle.call(produceVia)`), then
10
+ * asserting it serializable before it crosses any authority boundary (the in-app pane
11
+ * commit, the realm-entry `realm:binding` post). It reuses the ONE existing projector
12
+ * (`detectorHistogram`, via `handle.call('detectorHistogram')`) — it invents none.
13
+ *
14
+ * deriveProducedBindings(handle, produces, produceVia) → [{ name, value }, …]
15
+ *
16
+ * FAIL-LOUD: a missing handle, an empty produces with no names (→ []), a `handle.call`
17
+ * that throws (unknown produceVia / no detector) PROPAGATES (the caller surfaces it as a
18
+ * `realm:error` / pane overlay and commits NOTHING), and a non-serializable derived value
19
+ * THROWS via `assertSerializable` (a live runner/handle/function can never cross the seam).
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ import { assertSerializable } from './view-handle.js';
25
+
26
+ var DEFAULT_PRODUCE_VIA = 'detectorHistogram';
27
+
28
+ /**
29
+ * Derive the serializable `{ name, value }` bindings a declared canvas commits.
30
+ *
31
+ * @param {object} handle — the FROZEN view-handle (must expose `call(fn, args)`).
32
+ * @param {string[]} produces — the declared `produces` binding names (canvas-only).
33
+ * @param {string} [produceVia] — the frozen-handle call name; defaults to `'detectorHistogram'`.
34
+ * @returns {Promise<Array<{name:string, value:*}>>} one entry per produced name (empty when none declared).
35
+ * @throws {Error} FAIL-LOUD on a missing/invalid handle, a throwing `handle.call`, or a
36
+ * non-serializable derived value (VETO-CLASS #1 — never commit a live ref).
37
+ *
38
+ * ASYNC: the frozen handle is async-shaped (`call()` returns a Promise on every
39
+ * transport). The derived
40
+ * value MUST be AWAITED before `assertSerializable` — otherwise a Promise (never
41
+ * structured-clone-serializable) reaches the seam and FAILS LOUD spuriously.
42
+ */
43
+ export async function deriveProducedBindings(handle, produces, produceVia) {
44
+ var names = Array.isArray(produces) ? produces : [];
45
+ if (!names.length) { return []; }
46
+ if (!handle || typeof handle.call !== 'function') {
47
+ throw new Error('[produced-bindings] deriveProducedBindings requires a frozen view-handle ' +
48
+ 'with call(). (FAIL-LOUD.)');
49
+ }
50
+ var via = (typeof produceVia === 'string' && produceVia) ? produceVia : DEFAULT_PRODUCE_VIA;
51
+ var out = [];
52
+ for (var i = 0; i < names.length; i++) {
53
+ var name = names[i];
54
+ // DERIVE from the live runner via the frozen handle — the SOLE source of the value
55
+ // (truth-in-labeling: the author never hand-writes the histogram). C11: one
56
+ // `produceVia` per canvas, so every name derives from the same call in this WS.
57
+ // AWAIT it: `handle.call` is async-shaped, so the value is a Promise until resolved.
58
+ var value = await handle.call(via, []);
59
+ // VETO-CLASS #1 at the realm boundary: a non-serializable value (a live runner /
60
+ // function / DOM node) FAILS LOUD here — it is never posted/committed (C2).
61
+ assertSerializable(value, 'produces("' + name + '") from realm canvas');
62
+ out.push({ name: name, value: value });
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export { DEFAULT_PRODUCE_VIA };
@@ -0,0 +1,121 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * @module realm-registry
5
+ * @description
6
+ * The ONE authority for notebook webview-realm pre-loads.
7
+ *
8
+ * A notebook webview output cell is a managed scriptable *realm*. `frqtl canvas`,
9
+ * `chart`, and BYO realms are NOT different cell TYPES — they are different PRE-LOADS
10
+ * of the SAME realm primitive. Adding a new realm = registering a pre-load descriptor,
11
+ * NEVER a new cell type (VETO-CLASS #2).
12
+ *
13
+ * A pre-load descriptor shape:
14
+ * ```js
15
+ * { boot(hostEl, transport) → ViewHandle | Promise<ViewHandle> }
16
+ * ```
17
+ *
18
+ * The registry is module-level (process-global like `MathBackendRegistry`). Blessed
19
+ * pre-loads (`frqtl`, `chart`) self-register when their descriptor module is imported.
20
+ * A BYO realm must call `register('my-realm', descriptor)` before a notebook
21
+ * referencing it loads.
22
+ *
23
+ * @example
24
+ * import { register, get, has } from 'lib/shim/notebook/realm-registry.js';
25
+ *
26
+ * // Register a BYO realm pre-load:
27
+ * register('my-realm', {
28
+ * boot(hostEl, transport) {
29
+ * // set up the realm DOM, return a ViewHandle
30
+ * return makeViewHandle({ ... });
31
+ * }
32
+ * });
33
+ *
34
+ * // Resolve at notebook-pane mount time:
35
+ * const descriptor = get('my-realm'); // FAIL-LOUD if not registered
36
+ * const handle = await descriptor.boot(containerEl, transport);
37
+ */
38
+
39
+ 'use strict';
40
+
41
+ // Realm-name → pre-load descriptor. Module-level (the realm catalog is one
42
+ // process-global authority — mirrors MathBackendRegistry's single-instance shape).
43
+ var _realms = Object.create(null);
44
+
45
+ function _isValidName(name) {
46
+ return typeof name === 'string' && name.length > 0 && /^[a-z][a-z0-9-]*$/.test(name);
47
+ }
48
+
49
+ /**
50
+ * Add a realm pre-load descriptor to the catalog.
51
+ *
52
+ * @param {string} name
53
+ * Realm name (lowercase kebab; e.g. `'frqtl'`, `'chart'`, `'my-realm'`).
54
+ * @param {{ boot: (hostEl: HTMLElement, transport: object) => (ViewHandle|Promise<ViewHandle>) }} descriptor
55
+ * A pre-load descriptor whose `boot(hostEl, transport)` returns (or resolves to)
56
+ * a frozen `ViewHandle`. FAIL-LOUD if `boot` is not a function or `name` is invalid.
57
+ * @returns {void}
58
+ * @throws {Error} on an invalid name or a descriptor without `boot`.
59
+ *
60
+ * @example
61
+ * register('my-realm', {
62
+ * boot(hostEl, transport) {
63
+ * // mount the realm into hostEl, return/resolve a ViewHandle
64
+ * return makeViewHandle({ ... });
65
+ * }
66
+ * });
67
+ */
68
+ export function register(name, descriptor) {
69
+ if (!_isValidName(name)) {
70
+ throw new Error('[realm-registry] register: invalid realm name "' + String(name) +
71
+ '" (expect lowercase-kebab, e.g. "frqtl"). (FAIL-LOUD.)');
72
+ }
73
+ if (!descriptor || typeof descriptor.boot !== 'function') {
74
+ throw new Error('[realm-registry] register("' + name + '"): descriptor must have a ' +
75
+ 'boot(hostEl, transport) function. (FAIL-LOUD.)');
76
+ }
77
+ _realms[name] = descriptor;
78
+ }
79
+
80
+ /**
81
+ * Resolve a registered pre-load descriptor.
82
+ *
83
+ * FAIL-LOUD on an unknown name: a cell referencing an unregistered realm surfaces
84
+ * the error, never renders empty.
85
+ *
86
+ * @param {string} name — realm name (must match a prior `register()` call).
87
+ * @returns {{ boot: Function }} the pre-load descriptor.
88
+ * @throws {Error} if no descriptor is registered for `name`.
89
+ *
90
+ * @example
91
+ * const descriptor = get('frqtl'); // throws if frqtl pre-load not imported
92
+ * const handle = await descriptor.boot(el, transport);
93
+ */
94
+ export function get(name) {
95
+ var d = _realms[name];
96
+ if (!d) {
97
+ throw new Error('[realm-registry] get: no realm registered for "' + String(name) +
98
+ '". Registered: [' + names().join(', ') + ']. ' +
99
+ '(Blessed realms register at import; a BYO realm must register() first.) (FAIL-LOUD.)');
100
+ }
101
+ return d;
102
+ }
103
+
104
+ /** has(name) — non-throwing presence check. */
105
+ export function has(name) {
106
+ return !!_realms[name];
107
+ }
108
+
109
+ /** names() — the registered realm names (diagnostics / docs). */
110
+ export function names() {
111
+ return Object.keys(_realms);
112
+ }
113
+
114
+ /**
115
+ * _resetForTest() — clear the catalog (test isolation ONLY; not part of the
116
+ * runtime contract). The module-level catalog is process-global, so a test that
117
+ * exercises register/get FAIL-LOUD must be able to start from empty.
118
+ */
119
+ export function _resetForTest() {
120
+ _realms = Object.create(null);
121
+ }