@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.
- package/CLAUDE.md +87 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +212 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +692 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +326 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +672 -0
- package/dist/shim/notebook/knob.js +151 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +510 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +214 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +807 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- 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
|
+
}
|