@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,736 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module format
|
|
5
|
+
* @description
|
|
6
|
+
* Native FRQTL notebook format — parser + serializer.
|
|
7
|
+
*
|
|
8
|
+
* Named ESM export. PURE module — no engine imports, no Runtime/Bus, no DOM.
|
|
9
|
+
* Runs identically in Node (for tests) and in the browser.
|
|
10
|
+
*
|
|
11
|
+
* The on-disk format is a human-readable HTML file:
|
|
12
|
+
* ```html
|
|
13
|
+
* <frqtl-notebook version="1" title="Double Slit → Rosetta → QFT">
|
|
14
|
+
* <cell type="markdown">## The double-slit experiment …</cell>
|
|
15
|
+
* <cell type="module" name="./setups/double-slit.js">export function setupDoubleSlit …</cell>
|
|
16
|
+
* <cell type="canvas" id="c-ds-canvas" setup="./setups/double-slit.js#setupDoubleSlit" opts="quanta=800" stage="480x480" seed="42"></cell>
|
|
17
|
+
* <cell type="chart" id="c-ds-chart" in="hist" spec="bar:hist"></cell>
|
|
18
|
+
* </frqtl-notebook>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Round-trip discipline: `parse(serialize(nb))` deep-equals `nb`. The cell body
|
|
22
|
+
* is preserved verbatim; a literal `</cell>` inside a body is escaped on serialize
|
|
23
|
+
* and unescaped on parse.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {'markdown'|'js'|'module'|'canvas'|'chart'|'view'} CellType
|
|
28
|
+
* One of {@link CELL_TYPES} — the exported list is the authority; `parse` refuses any other.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* THE legal cell types, in the order of the {@link CellType} typedef. `parse` throws on a
|
|
33
|
+
* cell whose `type` is not listed here. Consumers read this list; they never re-list it.
|
|
34
|
+
*
|
|
35
|
+
* @type {ReadonlyArray<CellType>}
|
|
36
|
+
*/
|
|
37
|
+
export const CELL_TYPES = Object.freeze(['markdown', 'js', 'module', 'canvas', 'chart', 'view']);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The cell types whose body is PROSE (rendered, never executed or imported) — a subset of
|
|
41
|
+
* {@link CELL_TYPES}. A consumer that scans cell bodies as code skips exactly these.
|
|
42
|
+
*
|
|
43
|
+
* @type {ReadonlyArray<CellType>}
|
|
44
|
+
*/
|
|
45
|
+
export const PROSE_CELL_TYPES = Object.freeze(['markdown']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {'math'|'frqtl'} CellLane
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A single parsed notebook cell.
|
|
53
|
+
*
|
|
54
|
+
* @typedef {object} NotebookCell
|
|
55
|
+
* @property {CellType} type
|
|
56
|
+
* Cell type. `markdown` renders prose. `js` executes via the Runtime lane
|
|
57
|
+
* (`math` or `frqtl`). `module` is user-code reachable by import (not runnable).
|
|
58
|
+
* `canvas` mounts a live physics realm. `chart` renders a data view. `view` runs
|
|
59
|
+
* ARBITRARY user code in the realm (WS-NOTEBOOK-VENDOR-VIEW Phase 2b) — the
|
|
60
|
+
* user module (`module="<name>#<export>"`) imports vendored libs via the 2a
|
|
61
|
+
* import-map and renders into a DOM `mount`. `liveChart=` is a blessed PRESET
|
|
62
|
+
* over this same `view` substrate.
|
|
63
|
+
* @property {CellLane} lane
|
|
64
|
+
* Execution lane for `js` cells. `math` caps = `{ math, console, display, inputs }`;
|
|
65
|
+
* `frqtl` caps add `{ frqtl, render }`. Empty for non-`js` cells.
|
|
66
|
+
* @property {string} id
|
|
67
|
+
* Stable cell identity (staleness graph + AI-agent editing).
|
|
68
|
+
* @property {string} name
|
|
69
|
+
* For `module` cells: the notebook-local relative path (e.g. `'./setups/wave.js'`).
|
|
70
|
+
* This is the key in the in-memory module set. Empty for all other cell types.
|
|
71
|
+
* @property {string[]} in
|
|
72
|
+
* Binding names this cell CONSUMES (resolved from the kernel's binding store).
|
|
73
|
+
* @property {string[]} out
|
|
74
|
+
* Binding names this cell PRODUCES (committed to the kernel's binding store on success).
|
|
75
|
+
* @property {string} bind
|
|
76
|
+
* For `canvas` cells: RETIRED runner binding name (WS-NOTEBOOK-SHIM-MANAGED-BUILDER).
|
|
77
|
+
* A canvas that carries BOTH `setup` and a legacy `bind`/`out` runner ref is an
|
|
78
|
+
* unconverted cell and FAILS LOUD at both hosts (C7 migration guard). The runner is
|
|
79
|
+
* now realm-internal, never a kernel binding.
|
|
80
|
+
* @property {string} spec
|
|
81
|
+
* For `chart` cells: view spec string e.g. `'bar:hist'`.
|
|
82
|
+
* For `canvas` cells: RETIRED (the opaque setup-factory name) — a `spec` on a canvas
|
|
83
|
+
* FAILS LOUD at both hosts (factory-retire C3).
|
|
84
|
+
* @property {string} setup
|
|
85
|
+
* For `canvas` cells: the setup declaration (WS-NOTEBOOK-SHIM-MANAGED-BUILDER).
|
|
86
|
+
* `<module-name>` is a `type="module"` cell's `name`. Its grammar depends on `shape`
|
|
87
|
+
* and is owned by lib/shim/notebook/setup-builder.js parseSetupDeclaration: a factory
|
|
88
|
+
* canvas is `'<module-name>#<factoryName>'` (the name REQUIRED), a recipe canvas is
|
|
89
|
+
* `'<module-name>'` (a `#` REFUSED — the recipe is the module's default export). This
|
|
90
|
+
* module stores the attribute verbatim. The shim synthesizes the builder from this
|
|
91
|
+
* declaration and runs it realm-internal.
|
|
92
|
+
* Empty for all other cell types.
|
|
93
|
+
* @property {string} shape
|
|
94
|
+
* For `canvas` cells: OPTIONAL setup-form discriminator naming which SHAPE the
|
|
95
|
+
* declared `setup` module takes (Bite 5c — frqtl-notebook recipe fix). Two forms:
|
|
96
|
+
* - `'factory'` (DEFAULT when omitted): the module export is a setup FACTORY
|
|
97
|
+
* `fn(frqtl, universe, options)` — the shim synthesizes
|
|
98
|
+
* `runner.setup((u,o) => fn(frqtl, u, o), opts)` (the pre-existing gallery form,
|
|
99
|
+
* e.g. `setupDoubleSlit`). Absent → `'factory'`, so every existing notebook is
|
|
100
|
+
* byte-identical.
|
|
101
|
+
* - `'recipe'`: the module's DEFAULT export is a `buildExperiment(exp)` RECIPE (a single
|
|
102
|
+
* RecipeContext arg — the sketch form codegen / `buildFrqnb` / the scene files
|
|
103
|
+
* write). The shim builds a
|
|
104
|
+
* `frqtl.RecipeContext` and runs `exp.build(buildExperiment)` (the canonical
|
|
105
|
+
* co-init call + `addObject()` recipe path), NOT the factory signature.
|
|
106
|
+
* Any OTHER value FAILS LOUD at the synthesis helper (never a silent fallback; the
|
|
107
|
+
* setup-builder authority validates it). PURE PARSE — kept verbatim as a string
|
|
108
|
+
* (mirrors `seed`/`cameraPreset`). Empty → `''` (omitted on serialize; the
|
|
109
|
+
* synthesizer treats empty as `'factory'`). Empty for all other cell types.
|
|
110
|
+
* @property {string} stage
|
|
111
|
+
* For `canvas` cells: the runner stage `'<W>x<H>'` (canvasW × canvasH) — the world size of a
|
|
112
|
+
* FACTORY-shape canvas, where it is REQUIRED (the runner has no default size). A
|
|
113
|
+
* RECIPE-shape canvas states its own world in its exp.createCanvas / exp.createScene call,
|
|
114
|
+
* so a stage there is refused. ONE config authority = this attr, NOT folded into `opts`
|
|
115
|
+
* (C4). Empty for all other cell types.
|
|
116
|
+
* @property {string} seed
|
|
117
|
+
* For `canvas` cells: the runner RNG seed (integer string). Empty → the
|
|
118
|
+
* FrqtlApp default (42). ONE config authority = this attr (C4). Empty for
|
|
119
|
+
* all other cell types.
|
|
120
|
+
* @property {boolean} render3d
|
|
121
|
+
* For `canvas` cells: OPTIONAL — request the three.js 3D renderer + perspective
|
|
122
|
+
* camera for this canvas (WS gym-controls-as-sugar Bite 0 ADDENDUM, 2026-07-03).
|
|
123
|
+
* Selected through the ONE renderer-selection mechanism (the boot module's
|
|
124
|
+
* bootRealm, lib/frqtl/experiments/_shared/boot.js) — NOT a new renderer
|
|
125
|
+
* mechanism. The host (notebook-pane) threads this into `realm-frqtl.js`'s
|
|
126
|
+
* `bootFrqtlRealm` opts, which asks the boot for three. Honored EVEN when this is
|
|
127
|
+
* NOT the first canvas boot in the page session: bootRealm re-selects the renderer
|
|
128
|
+
* when a later boot asks for a different one, or when the previous selection
|
|
129
|
+
* failed, and a still-broken renderer fails loud on the run where it happens. `false`/absent → the
|
|
130
|
+
* p5 2D default, unchanged. Omitted on serialize when `false`. Canvas-only;
|
|
131
|
+
* `false` for all other cell types.
|
|
132
|
+
* @property {Object<string,number|string>} renderOptions
|
|
133
|
+
* For `canvas` cells: OPTIONAL render-INTENT knob assignments parsed from
|
|
134
|
+
* `"key=value,…"` (WS gym-controls-as-sugar fix-loop #6 Finding B, 2026-07-04) —
|
|
135
|
+
* the notebook-cell channel for the SAME render-intent writes a scene file states
|
|
136
|
+
* itself (e.g. lib/frqtl/scenes/collision-matrix-3d.js's
|
|
137
|
+
* `exp.render({ shape: 'sphere', colorScheme: 'roygbiv' })`, a ferry to
|
|
138
|
+
* `runner.setRenderOptions`). PURE PARSE — reuses the identical `_parseOpts`/
|
|
139
|
+
* `_joinOpts` machinery as `opts` (same `kind:'opts'`), but is a DISTINCT,
|
|
140
|
+
* SEPARATE key — never folded into `opts`. This is deliberate channel
|
|
141
|
+
* separation (architect ruling): `opts` feeds the setup
|
|
142
|
+
* FACTORY as physics-knob arguments (`frqtl.knob` → `options` param);
|
|
143
|
+
* `renderOptions` feeds `runner.setRenderOption()` calls AFTER `runner.setup()`
|
|
144
|
+
* returns — a render-knob write must never be mixed into the physics-knob bag.
|
|
145
|
+
* Empty → `{}`; omitted on serialize when empty. Empty for all other cell types.
|
|
146
|
+
* @property {string} cameraPreset
|
|
147
|
+
* For `canvas` cells: OPTIONAL named camera-orbit preset (WS gym-controls-as-sugar
|
|
148
|
+
* fix-loop #6 Finding B) — the notebook-cell channel for a standalone page's
|
|
149
|
+
* `runner.cameraControl.setPreset('<name>')` call (e.g. a 3D page's
|
|
150
|
+
* `window._egptFx.cameraPreset = 'down-barrel'`, applied by `fx-experiment.js`
|
|
151
|
+
* BEFORE the orbit sliders are seeded). A THIRD channel, distinct from both `opts`
|
|
152
|
+
* (physics knobs) and `renderOptions` (`setRenderOption` key/value pairs): this is
|
|
153
|
+
* a CAPABILITY-OBJECT METHOD CALL (`runner.cameraControl`), not a property-bag
|
|
154
|
+
* write, so it gets its own attribute rather than being shoehorned into
|
|
155
|
+
* `renderOptions`'s flat `key=value` shape. Capability-checked at apply time
|
|
156
|
+
* (skipped when the active renderer has no camera control; a camera that does not offer
|
|
157
|
+
* the preset THROWS) — mirrors the
|
|
158
|
+
* page-side `if (cam) { cam.setPreset(...) }` idiom exactly. PURE PARSE — kept
|
|
159
|
+
* verbatim as a string (mirrors `seed`/`produceVia`); the host validates the
|
|
160
|
+
* preset name against `runner.cameraControl.presets` (FAIL-LOUD is the
|
|
161
|
+
* capability-check itself: an unset/no-op result on a 2D canvas is expected, not
|
|
162
|
+
* an error). Empty → `''`; omitted on serialize when empty. Empty for all other
|
|
163
|
+
* cell types.
|
|
164
|
+
* @property {string[]} produces
|
|
165
|
+
* For `canvas` cells: the serializable binding name(s) the canvas COMMITS to the
|
|
166
|
+
* kernel after the realm-internal runner mounts (WS-NOTEBOOK-REALM-KERNEL-BINDING).
|
|
167
|
+
* The realm DERIVES each value at runtime via the frozen handle (`produceVia`) and
|
|
168
|
+
* the host commits it via `kernel.commitViewOutputs`. Parsed with `_splitNames`
|
|
169
|
+
* (→ `string[]`); empty → `[]`; omitted on serialize when empty. On parse a canvas's
|
|
170
|
+
* `produces` names are FOLDED INTO `out` (a produced binding IS an output binding the
|
|
171
|
+
* cell commits) so producer-discovery (`_findProducerCellId` over `out`) + the topo
|
|
172
|
+
* run-order stay ONE authority. Empty for all other cell types.
|
|
173
|
+
* @property {string} produceVia
|
|
174
|
+
* For `canvas` cells: OPTIONAL frozen-handle `call()` name used to DERIVE each
|
|
175
|
+
* produced value. Default `'detectorHistogram'` when omitted (the one blessed frqtl
|
|
176
|
+
* producer). One `produceVia` per canvas (multi-name-multi-source is backlog, C11).
|
|
177
|
+
* Empty → `''`; omitted on serialize when empty. Empty for all other cell types.
|
|
178
|
+
* @property {string} produceAfter
|
|
179
|
+
* For `canvas` cells: OPTIONAL integer-string tick count to WARM UP the realm
|
|
180
|
+
* (deterministic `handle.step()` loop) BEFORE deriving the produced binding
|
|
181
|
+
* (WS-NOTEBOOK-BASELINE FIX B). The runner mounts at tick ~0 where no particle has
|
|
182
|
+
* reached the detector yet — deriving immediately commits an EMPTY histogram. Warming
|
|
183
|
+
* up `produceAfter` ticks lets particles cross the stage and accumulate at the detector
|
|
184
|
+
* so the committed binding (and the downstream chart) shows REAL data. ONE config
|
|
185
|
+
* authority = this attr (mirrors `seed`/`stage`, C4); NOT folded into `opts`. Empty →
|
|
186
|
+
* the host default warmup. Empty for all other cell types.
|
|
187
|
+
* @property {string} liveChart
|
|
188
|
+
* For `canvas` cells: OPTIONAL frozen-handle `call()` projector name the live
|
|
189
|
+
* SAME-CELL accumulation chart polls each throttle tick (WS-NOTEBOOK-SAMECELL-LIVE-CHART).
|
|
190
|
+
* The blessed value is `'detectorHistogram'` (the one frqtl producer) — it reuses the
|
|
191
|
+
* SAME projector vocabulary as `produceVia`. UNLIKE `produces`, `liveChart` commits
|
|
192
|
+
* NOTHING to the kernel: it draws CONTINUOUSLY inside the canvas's OWN realm off the
|
|
193
|
+
* LOCAL `handle.on('tick')` stream, so its data never crosses the renderer↔realm seam.
|
|
194
|
+
* A canvas MAY carry BOTH `produces` and `liveChart` (orthogonal lifecycles). PURE PARSE —
|
|
195
|
+
* the raw string is kept; the host validates it against the blessed projector (FAIL-LOUD),
|
|
196
|
+
* mirroring `produceVia`/`seed`. Empty → `''`; omitted on serialize when empty. Empty for
|
|
197
|
+
* all other cell types.
|
|
198
|
+
* @property {string} liveChartKind
|
|
199
|
+
* For `canvas` cells: OPTIONAL live-chart kind `'bar'|'line'` (default `'bar'` when
|
|
200
|
+
* omitted; mirrors the chart-cell `spec` `kind` half). PURE PARSE — the raw string is
|
|
201
|
+
* kept; the host validates it against `bar|line` (FAIL-LOUD). Empty → `''`; omitted on
|
|
202
|
+
* serialize when empty. Empty for all other cell types.
|
|
203
|
+
* @property {string} realm
|
|
204
|
+
* For `view` cells (and OPTIONALLY any realm cell): the realm-registry pre-load to
|
|
205
|
+
* boot (`frqtl` | `chart` | a BYO name). `_realmNameForCell` ALREADY honors this:
|
|
206
|
+
* an explicit `cell.realm` wins, else the cell TYPE selects the blessed pre-load.
|
|
207
|
+
* PURE PARSE — empty → `''` (omitted on serialize; the host falls back to the
|
|
208
|
+
* type-default `frqtl`). WS-NOTEBOOK-VENDOR-VIEW Phase 2b.
|
|
209
|
+
* @property {string} module
|
|
210
|
+
* For `view` cells: the user render module declaration `'<module-name>#<exportName>'`,
|
|
211
|
+
* mirroring the canvas `setup` shape. `<module-name>` is a `type="module"` cell's
|
|
212
|
+
* `name`; `<exportName>` is optional ONLY when that module has exactly one render
|
|
213
|
+
* export (zero/multiple → FAIL-LOUD at the view-builder synthesis helper, never
|
|
214
|
+
* guess; C5 mirror). The shim runs the named export IN THE REALM with the view
|
|
215
|
+
* `ctx`. PURE PARSE — empty → `''` (omitted on serialize). Empty for non-`view`
|
|
216
|
+
* cell types. WS-NOTEBOOK-VENDOR-VIEW Phase 2b.
|
|
217
|
+
* @property {string} subscribe
|
|
218
|
+
* For `view` cells: OPTIONAL cross-cell — the cellId of ANOTHER canvas whose tick
|
|
219
|
+
* stream this view observes. PARSED + round-tripped now for format stability, but
|
|
220
|
+
* the cross-cell WIRING is DEFERRED (§3.4 of the design): the same-cell path is the
|
|
221
|
+
* primary 2b deliverable. PURE PARSE — empty → `''` (omitted on serialize). Empty
|
|
222
|
+
* for non-`view` cell types. WS-NOTEBOOK-VENDOR-VIEW Phase 2b.
|
|
223
|
+
* @property {Object<string,number|string>} opts
|
|
224
|
+
* For `canvas` cells: driving-knob assignments parsed from `"key=value,…"`.
|
|
225
|
+
* PURE PARSE only — validation is `lib/shim/notebook/knob.js`'s authority. Empty → `{}`.
|
|
226
|
+
* @property {boolean} collapsed
|
|
227
|
+
* When `true` the cell renders collapsed.
|
|
228
|
+
* Omitted on serialize when `false`.
|
|
229
|
+
* @property {string} body
|
|
230
|
+
* Raw cell body (verbatim between the open and close tags).
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A parsed notebook object (output of `parse`; input to `serialize`).
|
|
235
|
+
*
|
|
236
|
+
* @typedef {object} NotebookObject
|
|
237
|
+
* @property {string} title — human-readable notebook title.
|
|
238
|
+
* @property {string} version — format version string (currently `'1'`).
|
|
239
|
+
* @property {NotebookCell[]} cells — cells in document order.
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
'use strict';
|
|
243
|
+
|
|
244
|
+
/* ── escaping ────────────────────────────────────────────────────────────
|
|
245
|
+
*
|
|
246
|
+
* The body is opaque text/markup placed verbatim between <cell …> and </cell>.
|
|
247
|
+
* The ONLY sequence that could prematurely terminate a cell is a literal
|
|
248
|
+
* `</cell>` inside the body. We escape it to a sentinel on serialize and
|
|
249
|
+
* restore it on parse. Attribute VALUES are escaped for the five XML entities.
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
const CLOSE_TAG = '</cell>';
|
|
253
|
+
const CLOSE_SENTINEL = '</cell>';
|
|
254
|
+
|
|
255
|
+
function _escapeBody(body) {
|
|
256
|
+
// Escape any literal close tag so it cannot terminate the cell early.
|
|
257
|
+
return String(body).split(CLOSE_TAG).join(CLOSE_SENTINEL);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function _unescapeBody(body) {
|
|
261
|
+
return String(body).split(CLOSE_SENTINEL).join(CLOSE_TAG);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function _escapeAttr(value) {
|
|
265
|
+
return String(value)
|
|
266
|
+
.replace(/&/g, '&')
|
|
267
|
+
.replace(/</g, '<')
|
|
268
|
+
.replace(/>/g, '>')
|
|
269
|
+
.replace(/"/g, '"')
|
|
270
|
+
.replace(/'/g, ''');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function _unescapeAttr(value) {
|
|
274
|
+
return String(value)
|
|
275
|
+
.replace(/"/g, '"')
|
|
276
|
+
.replace(/'/g, "'")
|
|
277
|
+
.replace(/>/g, '>')
|
|
278
|
+
.replace(/</g, '<')
|
|
279
|
+
.replace(/&/g, '&');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ── attribute helpers ───────────────────────────────────────────────────── */
|
|
283
|
+
|
|
284
|
+
// Parse `name="value"` / `name='value'` pairs from a raw tag-attribute string.
|
|
285
|
+
// Returns an Object<name, decodedValue>.
|
|
286
|
+
function _parseAttrs(raw) {
|
|
287
|
+
const attrs = Object.create(null);
|
|
288
|
+
if (!raw) { return attrs; }
|
|
289
|
+
const re = /([a-zA-Z_][\w-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
290
|
+
let m;
|
|
291
|
+
while ((m = re.exec(raw)) !== null) {
|
|
292
|
+
const name = m[1];
|
|
293
|
+
const value = (m[3] !== undefined) ? m[3] : (m[4] !== undefined ? m[4] : '');
|
|
294
|
+
attrs[name] = _unescapeAttr(value);
|
|
295
|
+
}
|
|
296
|
+
return attrs;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Split a comma-separated binding list into a clean array of names.
|
|
300
|
+
// Empty / whitespace-only → []. Names are trimmed; empty entries dropped.
|
|
301
|
+
function _splitNames(value) {
|
|
302
|
+
if (!value) { return []; }
|
|
303
|
+
return String(value)
|
|
304
|
+
.split(',')
|
|
305
|
+
.map(function (s) { return s.trim(); })
|
|
306
|
+
.filter(function (s) { return s.length > 0; });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function _joinNames(arr) {
|
|
310
|
+
if (!arr || !arr.length) { return ''; }
|
|
311
|
+
return arr.join(', ');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Parse a canvas-cell `opts` attribute "<key>=<value>,…" into a {key:Number}
|
|
315
|
+
// map. PURE — no schema validation (that is knob.js's single authority). A value
|
|
316
|
+
// that is not a finite number is kept as the raw string so knob.js can FAIL-LOUD
|
|
317
|
+
// with a precise message rather than this pure parser swallowing it. Empty → {}.
|
|
318
|
+
function _parseOpts(value) {
|
|
319
|
+
// Plain object (NOT Object.create(null)) so a hand-authored {} round-trips
|
|
320
|
+
// deep-equal (deepStrictEqual is prototype-sensitive). Keys here are notebook
|
|
321
|
+
// knob names, never __proto__/constructor in practice; knob.js validates them.
|
|
322
|
+
var map = {};
|
|
323
|
+
if (!value) { return map; }
|
|
324
|
+
String(value).split(',').forEach(function (pair) {
|
|
325
|
+
var s = pair.trim();
|
|
326
|
+
if (!s) { return; }
|
|
327
|
+
var eq = s.indexOf('=');
|
|
328
|
+
if (eq === -1) { return; }
|
|
329
|
+
var key = s.slice(0, eq).trim();
|
|
330
|
+
var raw = s.slice(eq + 1).trim();
|
|
331
|
+
if (!key) { return; }
|
|
332
|
+
var num = Number(raw);
|
|
333
|
+
map[key] = (raw !== '' && isFinite(num)) ? num : raw;
|
|
334
|
+
});
|
|
335
|
+
return map;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Serialize an opts map back to "<key>=<value>,…" in key order. Empty → ''.
|
|
339
|
+
function _joinOpts(map) {
|
|
340
|
+
if (!map) { return ''; }
|
|
341
|
+
var keys = Object.keys(map);
|
|
342
|
+
if (!keys.length) { return ''; }
|
|
343
|
+
return keys.map(function (k) { return k + '=' + map[k]; }).join(',');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* ── canonical attr schema ───────────────────────────────────────────────────
|
|
347
|
+
*
|
|
348
|
+
* THE SINGLE CANONICAL PER-CELL ATTRIBUTE SCHEMA.
|
|
349
|
+
*
|
|
350
|
+
* `parse()` routes its output through `normalizeCellAttrs()`. `serialize()` emits
|
|
351
|
+
* in this order (see `_cellOpenTag` below). Every consumer
|
|
352
|
+
* ferries the normalized bag instead of re-listing keys.
|
|
353
|
+
*
|
|
354
|
+
* Add a new cell attribute HERE AND NOWHERE ELSE.
|
|
355
|
+
*
|
|
356
|
+
* `body` is intentionally NOT here — the editable text is carried separately.
|
|
357
|
+
*
|
|
358
|
+
* Order MUST MATCH the emit order in `_cellOpenTag` exactly so `serialize`
|
|
359
|
+
* stays byte-identical.
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
export const CELL_ATTRS = [
|
|
363
|
+
{ key: 'type', kind: 'string' },
|
|
364
|
+
{ key: 'lane', kind: 'string' },
|
|
365
|
+
{ key: 'id', kind: 'string' },
|
|
366
|
+
{ key: 'name', kind: 'string' },
|
|
367
|
+
{ key: 'in', kind: 'names' },
|
|
368
|
+
{ key: 'out', kind: 'names' },
|
|
369
|
+
{ key: 'bind', kind: 'string' },
|
|
370
|
+
{ key: 'spec', kind: 'string' },
|
|
371
|
+
{ key: 'realm', kind: 'string' },
|
|
372
|
+
{ key: 'module', kind: 'string' },
|
|
373
|
+
{ key: 'subscribe', kind: 'string' },
|
|
374
|
+
{ key: 'setup', kind: 'string' },
|
|
375
|
+
{ key: 'shape', kind: 'string' },
|
|
376
|
+
{ key: 'stage', kind: 'string' },
|
|
377
|
+
{ key: 'seed', kind: 'string' },
|
|
378
|
+
{ key: 'render3d', kind: 'bool' },
|
|
379
|
+
{ key: 'renderOptions',kind: 'opts' },
|
|
380
|
+
{ key: 'cameraPreset', kind: 'string' },
|
|
381
|
+
{ key: 'produces', kind: 'names' },
|
|
382
|
+
{ key: 'produceVia', kind: 'string' },
|
|
383
|
+
{ key: 'produceAfter', kind: 'string' },
|
|
384
|
+
{ key: 'liveChart', kind: 'string' },
|
|
385
|
+
{ key: 'liveChartKind',kind: 'string' },
|
|
386
|
+
{ key: 'opts', kind: 'opts' },
|
|
387
|
+
{ key: 'collapsed', kind: 'bool' },
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Normalize a partial cell-attribute bag to the canonical shape defined by
|
|
392
|
+
* `CELL_ATTRS`. Enforces correct types for every key; missing keys become the
|
|
393
|
+
* zero value for their kind (empty string / [] / {} / false).
|
|
394
|
+
*
|
|
395
|
+
* Idempotent on a correctly-typed bag (normalize is a value no-op on well-formed
|
|
396
|
+
* input — it only enforces the key set and coerces stray values).
|
|
397
|
+
*
|
|
398
|
+
* @param {object|null|undefined} partial — a partial bag (e.g. a parse()-built
|
|
399
|
+
* cell object before body attachment, or a fm read from cell.metadata.frqtl).
|
|
400
|
+
* @returns {object} the full canonical bag (same key order as CELL_ATTRS).
|
|
401
|
+
*/
|
|
402
|
+
export function normalizeCellAttrs(partial) {
|
|
403
|
+
const p = partial || {};
|
|
404
|
+
const bag = {};
|
|
405
|
+
for (const { key, kind } of CELL_ATTRS) {
|
|
406
|
+
const v = p[key];
|
|
407
|
+
if (kind === 'names') {
|
|
408
|
+
bag[key] = Array.isArray(v) ? v.slice() : [];
|
|
409
|
+
} else if (kind === 'opts') {
|
|
410
|
+
bag[key] = (v && typeof v === 'object' && !Array.isArray(v)) ? v : {};
|
|
411
|
+
} else if (kind === 'bool') {
|
|
412
|
+
bag[key] = !!v;
|
|
413
|
+
} else {
|
|
414
|
+
// 'string'
|
|
415
|
+
bag[key] = (typeof v === 'string') ? v : (v == null ? '' : String(v));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return bag;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Fold a canvas's declared `produces` names into its `out` set (dedup, order-stable:
|
|
423
|
+
* existing `out` first, then any `produces` not already present). This is the SINGLE
|
|
424
|
+
* fold authority — `parse` applies it so `_findProducerCellId`/`runOrder` (which scan
|
|
425
|
+
* `out`) see a producing canvas; any other consumer reuses it rather than
|
|
426
|
+
* re-implementing the loop. Idempotent: folding an already-folded `out`
|
|
427
|
+
* is a no-op.
|
|
428
|
+
*
|
|
429
|
+
* @param {string[]} out — the declared `out` names.
|
|
430
|
+
* @param {string[]} produces — the declared `produces` names.
|
|
431
|
+
* @returns {string[]} a new array: out ∪ produces, out-order-first.
|
|
432
|
+
*/
|
|
433
|
+
export function foldProducesIntoOut(out, produces) {
|
|
434
|
+
const folded = (Array.isArray(out) ? out : []).slice();
|
|
435
|
+
const prod = Array.isArray(produces) ? produces : [];
|
|
436
|
+
for (let i = 0; i < prod.length; i++) {
|
|
437
|
+
if (folded.indexOf(prod[i]) === -1) { folded.push(prod[i]); }
|
|
438
|
+
}
|
|
439
|
+
return folded;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* ── parse ─────────────────────────────────────────────────────────────────
|
|
443
|
+
*
|
|
444
|
+
* FAIL-LOUD on a missing <frqtl-notebook> root, a root with zero cells, a cell
|
|
445
|
+
* open tag with no `>` or no `</cell>` closer, and a cell type outside
|
|
446
|
+
* CELL_TYPES. Cells are read in document order by a forward scan: each open tag
|
|
447
|
+
* is closed by the FIRST `</cell>` after it (a body can never hold a literal
|
|
448
|
+
* `</cell>` — serialize escapes it), so a body that mentions `<cell …>` in prose
|
|
449
|
+
* is never mistaken for a sibling cell.
|
|
450
|
+
*/
|
|
451
|
+
|
|
452
|
+
// The notebook's name in an error: its title, else a marker that it has none.
|
|
453
|
+
function _notebookLabel(rootAttrs) {
|
|
454
|
+
return rootAttrs.title ? 'notebook "' + rootAttrs.title + '"' : 'untitled notebook';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Parse a FRQTL notebook HTML string into a notebook object.
|
|
459
|
+
*
|
|
460
|
+
* FAIL-LOUD: a malformed notebook throws instead of parsing to fewer cells.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} html — raw notebook file contents (UTF-8 string).
|
|
463
|
+
* @returns {NotebookObject}
|
|
464
|
+
* @throws {Error} if the string has no `<frqtl-notebook>` root; if the root holds
|
|
465
|
+
* zero `<cell>` elements; if a `<cell` open tag has no `>` or no `</cell>` closer
|
|
466
|
+
* (naming the cell index); if a cell's `type` is not in {@link CELL_TYPES}
|
|
467
|
+
* (naming the type and the legal list).
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* import { parse } from 'lib/shim/notebook/format.js';
|
|
471
|
+
* const nb = parse(fs.readFileSync('gallery-wave.frqnb', 'utf8'));
|
|
472
|
+
* console.log(nb.title); // 'Wave Interference'
|
|
473
|
+
* console.log(nb.cells.length); // e.g. 5
|
|
474
|
+
*/
|
|
475
|
+
export function parse(html) {
|
|
476
|
+
if (typeof html !== 'string') {
|
|
477
|
+
throw new Error('[notebook/format] parse() requires an HTML string; got ' + typeof html);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const rootMatch = /<frqtl-notebook\b([^>]*)>([\s\S]*?)<\/frqtl-notebook>/i.exec(html);
|
|
481
|
+
if (!rootMatch) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
'[notebook/format] no <frqtl-notebook>…</frqtl-notebook> root found — ' +
|
|
484
|
+
'not a FRQTL native notebook (FAIL-LOUD).'
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const rootAttrs = _parseAttrs(rootMatch[1]);
|
|
489
|
+
const inner = rootMatch[2];
|
|
490
|
+
|
|
491
|
+
const cells = [];
|
|
492
|
+
const openRe = /<cell\b/gi;
|
|
493
|
+
const closeRe = /<\/cell>/gi;
|
|
494
|
+
let om;
|
|
495
|
+
while ((om = openRe.exec(inner)) !== null) {
|
|
496
|
+
const index = cells.length;
|
|
497
|
+
const tagEnd = inner.indexOf('>', om.index);
|
|
498
|
+
if (tagEnd === -1) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
'[notebook/format] ' + _notebookLabel(rootAttrs) + ': cell #' + index +
|
|
501
|
+
' open tag `<cell` has no closing `>` (FAIL-LOUD).'
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const openTag = inner.slice(om.index, tagEnd + 1);
|
|
505
|
+
closeRe.lastIndex = tagEnd + 1;
|
|
506
|
+
const cl = closeRe.exec(inner);
|
|
507
|
+
if (cl === null) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
'[notebook/format] ' + _notebookLabel(rootAttrs) + ': cell #' + index + ' ' +
|
|
510
|
+
openTag + ' has no </cell> closer — the rest of the notebook would be one ' +
|
|
511
|
+
'cell body (FAIL-LOUD).'
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
openRe.lastIndex = cl.index + cl[0].length;
|
|
515
|
+
const a = _parseAttrs(inner.slice(om.index + om[0].length, tagEnd));
|
|
516
|
+
const rawBody = inner.slice(tagEnd + 1, cl.index);
|
|
517
|
+
if (CELL_TYPES.indexOf(a.type) === -1) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
'[notebook/format] ' + _notebookLabel(rootAttrs) + ': cell #' + index + ' has ' +
|
|
520
|
+
(a.type === undefined ? 'no type attribute' : 'unknown type="' + a.type + '"') +
|
|
521
|
+
' — legal types (CELL_TYPES): ' + CELL_TYPES.join(', ') + ' (FAIL-LOUD).'
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
// WS-NOTEBOOK-REALM-KERNEL-BINDING: a canvas's `produces` names are the
|
|
525
|
+
// serializable bindings it COMMITS to the kernel after the realm-internal
|
|
526
|
+
// runner mounts. They are FOLDED INTO `out` here (a produced binding IS an
|
|
527
|
+
// output binding the cell commits) so producer-discovery + the topo run-order
|
|
528
|
+
// stay ONE authority (`_findProducerCellId`/`runOrder` scan `out`). We keep the
|
|
529
|
+
// raw `produces` array on the cell too: it is the declaration the host derives
|
|
530
|
+
// from, and it lets `serialize` re-emit `produces` (and omit the folded `out`)
|
|
531
|
+
// so the round-trip stays idempotent.
|
|
532
|
+
const declaredProduces = _splitNames(a.produces);
|
|
533
|
+
const foldedOut = foldProducesIntoOut(_splitNames(a.out), declaredProduces);
|
|
534
|
+
// Build the pre-normalized bag with all special logic applied (fold, typed
|
|
535
|
+
// values, etc.), then route through normalizeCellAttrs so the output is
|
|
536
|
+
// structurally guaranteed to carry exactly the canonical CELL_ATTRS keys.
|
|
537
|
+
// normalizeCellAttrs is a value no-op on correctly-typed input — it only
|
|
538
|
+
// enforces the key set. `body` is attached after (not part of CELL_ATTRS).
|
|
539
|
+
const built = {
|
|
540
|
+
type: a.type,
|
|
541
|
+
lane: a.lane || '',
|
|
542
|
+
id: a.id || '',
|
|
543
|
+
// B3: module-cell relative path (the in-memory module-set key). Only
|
|
544
|
+
// meaningful for type="module"; empty for every other cell type so the
|
|
545
|
+
// attribute is omitted on serialize (round-trip idempotent).
|
|
546
|
+
name: a.name || '',
|
|
547
|
+
in: _splitNames(a.in),
|
|
548
|
+
out: foldedOut,
|
|
549
|
+
bind: a.bind || '',
|
|
550
|
+
spec: a.spec || '',
|
|
551
|
+
// WS-NOTEBOOK-VENDOR-VIEW Phase 2b: view-cell declaration (realm/module/subscribe).
|
|
552
|
+
// PURE PARSE — kept verbatim; the host validates (FAIL-LOUD). View-only in
|
|
553
|
+
// practice; empty for every other cell type so the attrs are omitted on
|
|
554
|
+
// serialize (round-trip idempotent).
|
|
555
|
+
realm: a.realm || '',
|
|
556
|
+
module: a.module || '',
|
|
557
|
+
subscribe: a.subscribe || '',
|
|
558
|
+
// WS-NOTEBOOK-SHIM-MANAGED-BUILDER: the canvas DECLARES its setup +
|
|
559
|
+
// runner config; the shim synthesizes the builder from these. Only
|
|
560
|
+
// meaningful for type="canvas"; empty for every other type so the
|
|
561
|
+
// attribute is omitted on serialize (round-trip idempotent).
|
|
562
|
+
setup: a.setup || '',
|
|
563
|
+
// Bite 5c: the setup-form discriminator ('factory' | 'recipe'). PURE PARSE
|
|
564
|
+
// — kept verbatim; the setup-builder synthesis helper validates it (empty →
|
|
565
|
+
// 'factory'; unknown → FAIL-LOUD). Canvas-only in practice; empty for every
|
|
566
|
+
// other cell type so the attribute is omitted on serialize (idempotent).
|
|
567
|
+
shape: a.shape || '',
|
|
568
|
+
stage: a.stage || '',
|
|
569
|
+
seed: a.seed || '',
|
|
570
|
+
// WS gym-controls-as-sugar Bite 0 ADDENDUM: request the three.js 3D
|
|
571
|
+
// renderer for this canvas (mirrors `collapsed`'s bool-attr parse —
|
|
572
|
+
// only the literal "true" sets it; absent/anything else → false,
|
|
573
|
+
// omitted on serialize, idempotent round-trip). Canvas-only in
|
|
574
|
+
// practice.
|
|
575
|
+
render3d: a.render3d === 'true',
|
|
576
|
+
// WS gym-controls-as-sugar fix-loop #6 Finding B: render-INTENT knobs
|
|
577
|
+
// (`runner.setRenderOption` key/value pairs) — reuses `_parseOpts`
|
|
578
|
+
// (SAME kind:'opts' parser as `opts`, but a channel-separate key; see
|
|
579
|
+
// the @property doc above). Canvas-only in practice.
|
|
580
|
+
renderOptions: _parseOpts(a.renderOptions),
|
|
581
|
+
// WS gym-controls-as-sugar fix-loop #6 Finding B: named camera-orbit
|
|
582
|
+
// preset (`runner.cameraControl.setPreset` call) — PURE PARSE, kept
|
|
583
|
+
// verbatim as a string (mirrors `seed`). Canvas-only in practice.
|
|
584
|
+
cameraPreset: a.cameraPreset || '',
|
|
585
|
+
// WS-NOTEBOOK-REALM-KERNEL-BINDING: the serializable binding(s) this canvas
|
|
586
|
+
// commits + the frozen-handle call that derives each. Canvas-only; empty for
|
|
587
|
+
// every other cell type so the attrs are omitted on serialize (idempotent).
|
|
588
|
+
produces: declaredProduces,
|
|
589
|
+
produceVia: a.produceVia || '',
|
|
590
|
+
// WS-NOTEBOOK-BASELINE FIX B: integer-string warmup tick count. PURE PARSE —
|
|
591
|
+
// kept verbatim as a string (mirrors `seed`); the host parses + clamps it
|
|
592
|
+
// (numeric validation is the host's authority, not this pure parser). Empty
|
|
593
|
+
// for every other cell type so the attr is omitted on serialize (idempotent).
|
|
594
|
+
produceAfter: a.produceAfter || '',
|
|
595
|
+
// WS-NOTEBOOK-SAMECELL-LIVE-CHART: the SAME-CELL live accumulation chart's
|
|
596
|
+
// projector name + kind. PURE PARSE — kept verbatim as strings (mirrors
|
|
597
|
+
// `produceVia`/`seed`); the host validates the projector against the blessed set
|
|
598
|
+
// and the kind against `bar|line` (FAIL-LOUD). They commit NOTHING to the kernel
|
|
599
|
+
// and do NOT enter `in`/`out` or the topo graph (unlike `produces`). Canvas-only;
|
|
600
|
+
// empty for every other cell type so the attrs are omitted on serialize (idempotent).
|
|
601
|
+
liveChart: a.liveChart || '',
|
|
602
|
+
liveChartKind: a.liveChartKind || '',
|
|
603
|
+
opts: _parseOpts(a.opts),
|
|
604
|
+
// E2d: collapse state — a boolean cell attribute, host-neutral. Only the
|
|
605
|
+
// literal "true" sets it; absent/anything else → false (omitted on
|
|
606
|
+
// serialize), so round-trip is idempotent.
|
|
607
|
+
collapsed: a.collapsed === 'true',
|
|
608
|
+
};
|
|
609
|
+
cells.push({ ...normalizeCellAttrs(built), body: _unescapeBody(rawBody) });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (cells.length === 0) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
'[notebook/format] ' + _notebookLabel(rootAttrs) + ' has zero <cell> elements — ' +
|
|
615
|
+
'nothing to render, run or scan (FAIL-LOUD).'
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
title: rootAttrs.title || '',
|
|
621
|
+
version: rootAttrs.version || '1',
|
|
622
|
+
cells: cells
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* ── serialize ───────────────────────────────────────────────────────────── */
|
|
627
|
+
|
|
628
|
+
function _cellOpenTag(cell) {
|
|
629
|
+
// Emit attributes in CELL_ATTRS order — the SINGLE schema authority drives the emit;
|
|
630
|
+
// there is NO second hand-maintained key list here (a key added to CELL_ATTRS emits
|
|
631
|
+
// automatically). Empty attributes are omitted so parse→serialize→parse is idempotent
|
|
632
|
+
// on the cleaned arrays/strings.
|
|
633
|
+
//
|
|
634
|
+
// The ONLY per-key special case is the structural `out`/`produces` interaction:
|
|
635
|
+
// a canvas's `produces` names were FOLDED INTO `out` on parse, so `out` re-emits ONLY
|
|
636
|
+
// its non-produces names (the produces re-emit via the `produces` attr). A producing
|
|
637
|
+
// canvas whose `out` == `produces` thus emits NO `out` attr — idempotent round-trip.
|
|
638
|
+
const producesArr = Array.isArray(cell.produces) ? cell.produces : [];
|
|
639
|
+
const outArr = Array.isArray(cell.out) ? cell.out : [];
|
|
640
|
+
const outOnly = outArr.filter(function (n) { return producesArr.indexOf(n) === -1; });
|
|
641
|
+
let tag = '<cell';
|
|
642
|
+
for (let i = 0; i < CELL_ATTRS.length; i++) {
|
|
643
|
+
const key = CELL_ATTRS[i].key;
|
|
644
|
+
const kind = CELL_ATTRS[i].kind;
|
|
645
|
+
if (kind === 'bool') {
|
|
646
|
+
// Boolean attribute — emitted ONLY when true (so a false cell is byte-identical
|
|
647
|
+
// to one with no attr). collapsed="true".
|
|
648
|
+
if (cell[key]) { tag += ' ' + key + '="true"'; }
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
let str;
|
|
652
|
+
if (kind === 'names') {
|
|
653
|
+
str = _joinNames(key === 'out' ? outOnly : key === 'produces' ? producesArr : cell[key]);
|
|
654
|
+
} else if (kind === 'opts') {
|
|
655
|
+
// Generalized to `cell[key]` (WS gym-controls-as-sugar fix-loop #6 Finding B):
|
|
656
|
+
// a hardcoded `cell.opts` here was harmless while `opts` was the ONLY
|
|
657
|
+
// kind:'opts' key, but would have silently serialized `opts`'s content
|
|
658
|
+
// under a SECOND opts-kind key's tag (`renderOptions`) once one was added —
|
|
659
|
+
// table-driven per-key access is the correct, general form CELL_ATTRS
|
|
660
|
+
// already promises ("Add a new cell attribute HERE AND NOWHERE ELSE").
|
|
661
|
+
str = _joinOpts(cell[key]);
|
|
662
|
+
} else {
|
|
663
|
+
str = (typeof cell[key] === 'string') ? cell[key] : '';
|
|
664
|
+
}
|
|
665
|
+
if (str) { tag += ' ' + key + '="' + _escapeAttr(str) + '"'; }
|
|
666
|
+
}
|
|
667
|
+
tag += '>';
|
|
668
|
+
return tag;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Serialize a notebook object to a FRQTL notebook HTML string.
|
|
673
|
+
*
|
|
674
|
+
* Attribute order in each `<cell>` tag is fixed for stable, diff-friendly output.
|
|
675
|
+
* Empty attributes are omitted so the round-trip `parse(serialize(nb))` is idempotent.
|
|
676
|
+
*
|
|
677
|
+
* @param {NotebookObject} notebook
|
|
678
|
+
* @returns {string} the serialized HTML string (UTF-8).
|
|
679
|
+
* @throws {Error} if `notebook` is not an object or `notebook.cells` is not an array.
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* import { parse, serialize } from 'lib/shim/notebook/format.js';
|
|
683
|
+
* const html = serialize(nb);
|
|
684
|
+
* fs.writeFileSync('out.frqnb', html, 'utf8');
|
|
685
|
+
*/
|
|
686
|
+
export function serialize(notebook) {
|
|
687
|
+
if (!notebook || typeof notebook !== 'object') {
|
|
688
|
+
throw new Error('[notebook/format] serialize() requires a notebook object.');
|
|
689
|
+
}
|
|
690
|
+
if (!Array.isArray(notebook.cells)) {
|
|
691
|
+
throw new Error('[notebook/format] notebook.cells must be an array.');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const version = notebook.version || '1';
|
|
695
|
+
const title = notebook.title || '';
|
|
696
|
+
|
|
697
|
+
const lines = [];
|
|
698
|
+
lines.push(
|
|
699
|
+
'<frqtl-notebook version="' + _escapeAttr(version) + '"' +
|
|
700
|
+
(title ? ' title="' + _escapeAttr(title) + '"' : '') + '>'
|
|
701
|
+
);
|
|
702
|
+
lines.push('');
|
|
703
|
+
|
|
704
|
+
for (let i = 0; i < notebook.cells.length; i++) {
|
|
705
|
+
const cell = notebook.cells[i];
|
|
706
|
+
const body = _escapeBody(cell.body == null ? '' : cell.body);
|
|
707
|
+
lines.push(_cellOpenTag(cell) + body + CLOSE_TAG);
|
|
708
|
+
lines.push('');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
lines.push('</frqtl-notebook>');
|
|
712
|
+
lines.push('');
|
|
713
|
+
return lines.join('\n');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* ── normalize ───────────────────────────────────────────────────────────── */
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Produce the canonical cleaned shape that `parse()` would yield from a hand-authored
|
|
720
|
+
* notebook object. Used to compare two notebooks ignoring author-side attribute noise
|
|
721
|
+
* (e.g. duplicate spaces in an `in` list).
|
|
722
|
+
*
|
|
723
|
+
* Idempotent: `normalize(normalize(nb))` deep-equals `normalize(nb)`.
|
|
724
|
+
*
|
|
725
|
+
* @param {NotebookObject} notebook
|
|
726
|
+
* @returns {NotebookObject} the normalized notebook.
|
|
727
|
+
*
|
|
728
|
+
* @example
|
|
729
|
+
* import { normalize } from 'lib/shim/notebook/format.js';
|
|
730
|
+
* const a = normalize(nb1);
|
|
731
|
+
* const b = normalize(nb2);
|
|
732
|
+
* assert.deepStrictEqual(a, b); // compare ignoring whitespace noise
|
|
733
|
+
*/
|
|
734
|
+
export function normalize(notebook) {
|
|
735
|
+
return parse(serialize(notebook));
|
|
736
|
+
}
|