@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +8 -4
  3. package/dist/dist-manifest.json +70 -62
  4. package/dist/eyes/index.js +5 -2
  5. package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
  6. package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
  7. package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
  8. package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
  9. package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
  10. package/dist/frqtl/objects/FrqtlApp.js +136 -30
  11. package/dist/frqtl/objects/ObjectFactory.js +4 -5
  12. package/dist/frqtl/objects/RecipeContext.js +617 -9
  13. package/dist/frqtl/objects/instruments/hosted-eyes.js +268 -57
  14. package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
  15. package/dist/frqtl/objects/record/Record.js +326 -0
  16. package/dist/frqtl/objects/record/open-record.js +22 -0
  17. package/dist/frqtl/objects/schema/index.js +72 -0
  18. package/dist/frqtl/parts/DetectorWall.js +1 -1
  19. package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
  20. package/dist/frqtl/parts/FrqtlVision.js +21 -6
  21. package/dist/frqtl/parts/Wall.js +2 -0
  22. package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
  23. package/dist/frqtl/scenes/absorbing-wall.js +18 -14
  24. package/dist/frqtl/scenes/atomic-model.js +3 -0
  25. package/dist/frqtl/scenes/big-bang.js +3 -0
  26. package/dist/frqtl/scenes/blackbody.js +14 -11
  27. package/dist/frqtl/scenes/circuit-sat.js +7 -4
  28. package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
  29. package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
  30. package/dist/frqtl/scenes/counting-wall.js +19 -15
  31. package/dist/frqtl/scenes/double-slit.js +3 -0
  32. package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
  33. package/dist/frqtl/scenes/dse-one-slit.js +59 -55
  34. package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
  35. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
  36. package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
  37. package/dist/frqtl/scenes/dse-two-slits.js +69 -65
  38. package/dist/frqtl/scenes/empty.js +3 -0
  39. package/dist/frqtl/scenes/laser-buildup.js +23 -16
  40. package/dist/frqtl/scenes/mirror-3d.js +31 -27
  41. package/dist/frqtl/scenes/mirror-route.js +4 -1
  42. package/dist/frqtl/scenes/mirror.js +24 -20
  43. package/dist/frqtl/scenes/omni-3d.js +7 -4
  44. package/dist/frqtl/scenes/omni-source.js +7 -4
  45. package/dist/frqtl/scenes/self-interference.js +13 -10
  46. package/dist/frqtl/scenes/smoking-gun.js +15 -11
  47. package/dist/frqtl/scenes/straight-walk.js +7 -4
  48. package/dist/frqtl/scenes/sun.js +11 -8
  49. package/dist/frqtl/scenes/two-source.js +22 -17
  50. package/dist/frqtl/scenes/wave.js +10 -7
  51. package/dist/shim/bundle/index.js +112 -96
  52. package/dist/shim/bundle/indexHtml.js +66 -39
  53. package/dist/shim/engine/router.js +2 -2
  54. package/dist/shim/index.js +1 -1
  55. package/dist/shim/notebook/format.js +1 -1
  56. package/dist/shim/notebook/store.js +2 -3
  57. package/dist/shim/page-params/index.js +2 -2
  58. package/dist/shim/project/index.js +20 -5
  59. package/dist/shim/project/link.js +182 -37
  60. package/dist/shim/project/manifest.js +151 -0
  61. package/dist/shim/project/parts-module.js +67 -0
  62. package/dist/shim/project/parts-writer.js +457 -0
  63. package/dist/shim/project/record-format.js +345 -0
  64. package/dist/shim/project/specifiers.js +41 -13
  65. package/dist/shim/registry/index.js +1 -1
  66. package/dist/shim/registry/scene-source.js +44 -30
  67. package/dist/shim/registry/walkthrough.js +13 -12
  68. package/dist/shim/render-state/apply.js +11 -14
  69. package/dist/shim/storage/codec-chains.js +88 -0
  70. package/dist/shim/storage/folders.js +205 -0
  71. package/dist/shim/storage/index.js +64 -8
  72. package/dist/shim/storage/make-namespace-store.js +5 -5
  73. package/llms.txt +2 -2
  74. package/package.json +1 -1
  75. package/dist/shim/bundle/frqnb.js +0 -112
  76. package/dist/shim/bundle/manifest.js +0 -95
  77. package/dist/shim/storage/modules.js +0 -249
  78. package/dist/shim/storage/projects.js +0 -150
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
7
  * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`exp.addObject`) and the render intent are all here; the sugar
8
+ * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
9
  * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
10
  * (rung r8-sun).
11
11
  */
@@ -21,18 +21,21 @@
21
21
 
22
22
  import { CameraControls, SphereEmitter } from '@descix/frqtl-web/parts';
23
23
 
24
+ export const parts = {
25
+ source: [SphereEmitter, {
26
+ preset: 'spherical', emissionBoxSide: 0, is2D: false,
27
+ chargeMode: 'random', fundamentalDimension: 2,
28
+ wavelength: 1, fundamentalWaveLength: 64, emitEveryTicks: 1
29
+ }],
30
+ };
31
+
24
32
  export default function buildExperiment(exp) {
25
33
  // Rung 8 (Sun) — the sun.png panel regime: a 3D omni POINT source, virtual mass 16
26
34
  // (fullness 1) via wavelength:1, λ scale 16 via fundamentalWaveLength:64, emit every 1.
27
35
  // The sphere shape + god-rays + glow FX ride the renderState (three-only capabilities).
28
36
  // The sun's yellow is the NATURAL fullness->color (mass=capacity, fullness 1); no forced color.
29
- var SRC = {
30
- preset: 'spherical', emissionBoxSide: 0, is2D: false,
31
- chargeMode: 'random', fundamentalDimension: 2,
32
- wavelength: 1, fundamentalWaveLength: 64, emitEveryTicks: 1
33
- };
34
- exp.createScene(600, 400, 600, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 3D scene + universe, 600x400x600 — first, then parts
35
- exp.addObject('SphereEmitter', SRC);
37
+ exp.createScene(600, 400, 600, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 3D scene + universe, 600x400x600 — first, then parts
38
+ exp.declare(parts);
36
39
  if (typeof document !== 'undefined' && exp.runner) {
37
40
  if (exp.runner.controlHost) { CameraControls.attach(exp.runner, exp.runner.controlHost); }
38
41
  }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
7
  * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`exp.addObject`) and the render intent are all here; the sugar
8
+ * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
9
  * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
10
  * (rungs r6a-two-source, r6b-two-fountains, r6c-plasma-fusion).
11
11
  */
@@ -16,22 +16,27 @@
16
16
 
17
17
  import { SphereEmitter } from '@descix/frqtl-web/parts';
18
18
 
19
+ // The classic self-interference config called TWICE, centered ±32px from canvas
20
+ // center (64px apart = 1×fundamentalWaveLength, i.e. λ_eff/4 since λ_eff = 4×fWL).
21
+ // The world is init'd ONCE, then TWO parts are constructed on it — no flag, no
22
+ // re-init, no stacking. The
23
+ // lattice BETWEEN the two visible sources is EMERGENT density superposition (SU1).
24
+ const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
25
+ const cx = Math.floor(W / 2), cy = Math.floor(H / 2);
26
+ const CFG = {
27
+ is2D: true, preset: 'spherical',
28
+ emissionShell: 'surface', innerR: 16,
29
+ fundamentalDimension: 2, wavelength: 32,
30
+ chargeMode: 'random', fundamentalWaveLength: 64,
31
+ burstSize: 72, emitEveryTicks: 1
32
+ };
33
+
34
+ export const parts = {
35
+ left: [SphereEmitter, { ...CFG, center: [cx - 32, cy] }],
36
+ right: [SphereEmitter, { ...CFG, center: [cx + 32, cy] }],
37
+ };
38
+
19
39
  export default function buildExperiment(exp) {
20
- // The classic self-interference config called TWICE, centered ±32px from canvas
21
- // center (64px apart = 1×fundamentalWaveLength, i.e. λ_eff/4 since λ_eff = 4×fWL).
22
- // The world is init'd ONCE, then TWO parts are constructed on it — no flag, no
23
- // re-init, no stacking. The
24
- // lattice BETWEEN the two visible sources is EMERGENT density superposition (SU1).
25
- var W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
26
- var cx = Math.floor(W / 2), cy = Math.floor(H / 2);
27
- var CFG = {
28
- is2D: true, preset: 'spherical',
29
- emissionShell: 'surface', innerR: 16,
30
- fundamentalDimension: 2, wavelength: 32,
31
- chargeMode: 'random', fundamentalWaveLength: 64,
32
- burstSize: 72, emitEveryTicks: 1
33
- };
34
40
  exp.createCanvas(W, H, SphereEmitter.worldCfg(CFG)); // THE WORLD: a 2D canvas + universe — first, then parts
35
- exp.addObject('SphereEmitter', Object.assign({}, CFG, { center: [cx - 32, cy] }));
36
- exp.addObject('SphereEmitter', Object.assign({}, CFG, { center: [cx + 32, cy] }));
41
+ exp.declare(parts);
37
42
  }
@@ -5,22 +5,25 @@
5
5
  *
6
6
  * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
7
  * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`exp.addObject`) and the render intent are all here; the sugar
8
+ * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
9
  * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
10
  * (rung r2-wave).
11
11
  */
12
12
 
13
13
  import { SphereEmitter } from '@descix/frqtl-web/parts';
14
14
 
15
+ export const parts = {
16
+ source: [SphereEmitter, {
17
+ preset: 'particle-walk', is2D: true,
18
+ fundamentalDimension: 2, wavelength: 8
19
+ }],
20
+ };
21
+
15
22
  export default function buildExperiment(exp) {
16
23
  // Rung 2 — Rung 1 + raise the leaf dimension to 2 (WAVELENGTH_DEMO_DIMENSION):
17
24
  // wavelength is a NO-OP at dim 0 (capacity=1 saturates mass), so raising the
18
25
  // fundamental gives the wavelength arg leverage. wavelength:8 -> mass 2 ->
19
26
  // transverse period grows from ~16px (Rung 1) to a visible ~128px snake.
20
- var SRC = {
21
- preset: 'particle-walk', is2D: true,
22
- fundamentalDimension: 2, wavelength: 8
23
- };
24
- exp.createCanvas(600, 400, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
25
- exp.addObject('SphereEmitter', SRC);
27
+ exp.createCanvas(600, 400, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
28
+ exp.declare(parts);
26
29
  }
@@ -1,121 +1,137 @@
1
1
  /* Copyright 2023-2026 by Essam Abadir */
2
2
 
3
3
  /**
4
- * lib/shim/bundle/index.js — the recipe-bundle assembler + importer
5
- * The canonical owner of the bundle format
6
- * (design §2): a folder that zips for download —
4
+ * lib/shim/bundle/index.js — Export and Import of a project folder.
7
5
  *
8
- * experiment.js ← THE source of truth (the buildExperiment(exp) recipe)
9
- * experiment.frqnb ← teaching/gallery projection (notebook/format.serialize)
10
- * index.html ← standalone host (@descix/frqtl-web bootProject over the served folder)
11
- * manifest.json ← Module envelope minus `source` + bundle-only fields (E1)
12
- * preview.png ← optional golden screenshot
6
+ * A project is a folder (the stored row's owner is
7
+ * lib/shim/storage/folders.js). The Export IS the folder, zipped, plus the host page that boots it:
13
8
  *
14
- * PURE + DOM-free: emits/consumes `Uint8Array`. STUDIO's export button and any
15
- * future DeSciX publisher CONSUME this — passing the generated `experiment.js`
16
- * source + envelope + renderState DOWN (this module never imports codegen/
17
- * save-state — those live above the shim; the arrow stays one-way). The browser
18
- * `Blob`+`a.click` download trigger is a THIN feature-side helper.
9
+ * exportFolder(files, renderState, { sdkVersion }) → zipBytes
10
+ * the zip holds EXACTLY the folder's files plus a generated `index.html` (indexHtml.js: bootProject over the
11
+ * served folder, frqtl.json naming the entry, the boot bag inlined by its one writer).
12
+ * importArchive(bytes, fileName) → { files, renderState } (a normalized folder row)
13
+ * a zip → its files minus `index.html`, whose boot bag (indexHtml.js readBootBag) becomes renderState;
14
+ * it must carry frqtl.json at its root (an archive with no index.html imports with
15
+ * renderState null)
16
+ * one `.js` → the one-file project { 'sketch.js': text, 'frqtl.json': manifestFor({ name: <basename>,
17
+ * entry: 'sketch.js' }) }, renderState null — a 3D sketch then fails loud at boot on the
18
+ * renderer (the existing named refusal); nothing guesses a display
19
+ * REFUSED, naming the migration: a zip carrying `experiment.js` or `manifest.json`, or a JSON v4 module
20
+ * envelope — the retired export format. REFUSED by rule: a one-file `.mjs` (frqtl.json's entry rule admits a
21
+ * `.js` entry only), anything else.
19
22
  *
20
- * Layer 2 (physics): none — pure format/packaging.
23
+ * PURE + DOM-free (Uint8Array in and out). The browser download and file read are lib/features/ide/bundle-export.js.
24
+ * zip.js (the STORE zip) is imported by the Record too; its API is frozen. Layer 2 (physics): none.
21
25
  */
22
26
 
23
27
  'use strict';
24
28
 
25
- import { buildManifest, manifestToEnvelope } from './manifest.js';
26
- import { buildFrqnb } from './frqnb.js';
27
- import { buildIndexHtml } from './indexHtml.js';
28
- import { zipStore, unzipStore, textBytes, bytesText } from './zip.js';
29
- import { normalizeSavedRenderState } from '../render-state/index.js';
29
+ import { zipStore, unzipStore, textBytes } from './zip.js';
30
+ import { buildIndexHtml, readBootBag } from './indexHtml.js';
31
+ import { normalizeFolderRow, EXPORT_PAGE } from '../storage/folders.js';
32
+ import { MANIFEST_FILE, MANIFEST_KEYS, parseManifest, serializeManifest, manifestFor } from '../project/manifest.js';
30
33
 
31
- // ── Canonical bundle file names ──────────────────────────────────────────────
32
- export var FILE_EXPERIMENT_JS = 'experiment.js';
33
- export var FILE_EXPERIMENT_FRQNB = 'experiment.frqnb';
34
- export var FILE_INDEX_HTML = 'index.html';
35
- export var FILE_MANIFEST_JSON = 'manifest.json';
36
- export var FILE_PREVIEW_PNG = 'preview.png';
34
+ /** The files of the retired export format (the v4 envelope's bundle). */
35
+ export var RETIRED_ARCHIVE_FILES = Object.freeze(['experiment.js', 'manifest.json']);
37
36
 
38
- /**
39
- * Assemble a recipe bundle from a Module envelope + bundle inputs.
40
- *
41
- * Conceptually the dispatch's `(source, envelopeMeta, renderState, previewPngBytes,
42
- * sdkVersion)`: here `source` + `renderState` + `envelopeMeta` arrive together AS
43
- * the Module envelope (the exact shape STUDIO's Save path already builds), so the
44
- * bundle↔envelope round-trip is a clean identity. `manifest` ferries the envelope
45
- * minus `source` (E1) — no field re-enumeration.
46
- *
47
- * @param {object} envelope — a `lib/shim/storage/modules.js` Module envelope
48
- * (`{id,name,source,renderState,…}`; `source` = the recipe, REQUIRED).
49
- * @param {object} opts
50
- * @param {string} opts.sdkVersion — REQUIRED CDN pin (derived, never a literal).
51
- * @param {Uint8Array|null} [opts.previewPngBytes] — preview image bytes, or null.
52
- * @param {string} [opts.stage='600x400'] — canvas stage for the .frqnb projection.
53
- * @param {string} [opts.prose] — optional teaching blurb for the .frqnb markdown.
54
- * @returns {{files: {name:string, bytes:Uint8Array}[], zipBytes: Uint8Array}}
55
- */
56
- export function assembleBundle(envelope, opts) {
57
- if (!envelope || typeof envelope !== 'object') {
58
- throw new Error('[bundle] assembleBundle(envelope): envelope must be an object.');
59
- }
60
- if (typeof envelope.source !== 'string' || envelope.source.length === 0) {
61
- throw new Error('[bundle] assembleBundle: envelope.source (the recipe) is required.');
62
- }
63
- opts = opts || {};
64
- var preview = (opts.previewPngBytes instanceof Uint8Array && opts.previewPngBytes.length > 0)
65
- ? opts.previewPngBytes : null;
37
+ var RETIRED = 'this is the retired export format; the project folder replaced it (frqtl.json + sketch.js)';
38
+ var SKETCH = 'sketch.js';
66
39
 
67
- var manifest = buildManifest(envelope, {
68
- sdkVersion: opts.sdkVersion,
69
- preview: preview ? FILE_PREVIEW_PNG : null
70
- });
40
+ var _strict = new TextDecoder('utf-8', { fatal: true });
71
41
 
72
- var normalizedRs = normalizeSavedRenderState(envelope.renderState);
73
-
74
- var files = [
75
- { name: FILE_EXPERIMENT_JS, bytes: textBytes(envelope.source) },
76
- { name: FILE_EXPERIMENT_FRQNB, bytes: textBytes(buildFrqnb({
77
- name: envelope.name, source: envelope.source,
78
- renderState: normalizedRs, stage: opts.stage, prose: opts.prose
79
- })) },
80
- { name: FILE_INDEX_HTML, bytes: textBytes(buildIndexHtml({
81
- name: envelope.name, sdkVersion: opts.sdkVersion, renderState: normalizedRs,
82
- scripts: Array.isArray(envelope.scripts) ? envelope.scripts : [] // the envelope's setup-factory delta
83
- })) },
84
- { name: FILE_MANIFEST_JSON, bytes: textBytes(JSON.stringify(manifest, null, 2)) }
85
- ];
86
- if (preview) { files.push({ name: FILE_PREVIEW_PNG, bytes: preview }); }
87
-
88
- return { files: files, zipBytes: zipStore(files) };
42
+ function _fail(msg) {
43
+ throw new Error('[bundle] ' + msg + ' (FAIL-LOUD.)');
89
44
  }
90
45
 
91
46
  /**
92
- * Import a recipe bundle: recover the Module envelope (source re-attached from
93
- * `experiment.js` per E1) + the preview bytes.
94
- *
95
- * @param {Uint8Array} zipBytes — a bundle `.zip` (from `assembleBundle`).
96
- * @returns {{envelope: object, previewPngBytes: Uint8Array|null}}
47
+ * @param {Object<string,string>} files — the folder (frqtl.json + its files)
48
+ * @param {object|null} renderState — the host's boot bag (normalized by its owner through the folder row)
49
+ * @param {{sdkVersion: string}} opts — the `@descix/frqtl-web` pin the page imports (derived, never a literal)
50
+ * @returns {Uint8Array} the zip
97
51
  */
98
- export function importBundle(zipBytes) {
99
- var entries = unzipStore(zipBytes);
100
- var byName = Object.create(null);
101
- for (var i = 0; i < entries.length; i++) { byName[entries[i].name] = entries[i].bytes; }
52
+ export function exportFolder(files, renderState, opts) {
53
+ var row = normalizeFolderRow({ files: files, renderState: renderState });
54
+ var sdkVersion = opts && opts.sdkVersion;
55
+ var name = parseManifest(row.files[MANIFEST_FILE]).name;
56
+ var entries = Object.keys(row.files).map(function (p) { return { name: p, bytes: textBytes(row.files[p]) }; });
57
+ entries.push({ name: EXPORT_PAGE, bytes: textBytes(buildIndexHtml({ name: name, sdkVersion: sdkVersion, renderState: row.renderState })) });
58
+ return zipStore(entries);
59
+ }
60
+
61
+ function _isZip(bytes) {
62
+ return bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b &&
63
+ ((bytes[2] === 0x03 && bytes[3] === 0x04) || (bytes[2] === 0x05 && bytes[3] === 0x06));
64
+ }
102
65
 
103
- if (!byName[FILE_MANIFEST_JSON]) {
104
- throw new Error('[bundle] importBundle: ' + FILE_MANIFEST_JSON + ' missing from the archive.');
66
+ function _text(bytes, what) {
67
+ try { return _strict.decode(bytes); }
68
+ catch (e) { _fail(what + ' is not UTF-8 text — a project folder holds text files only.'); }
69
+ }
70
+
71
+ function _baseName(fileName) {
72
+ var base = String(fileName).split(/[\\/]/).pop();
73
+ return base;
74
+ }
75
+
76
+ function _fromZip(bytes) {
77
+ var entries = unzipStore(bytes).filter(function (e) { return !/\/$/.test(e.name); });
78
+ var names = entries.map(function (e) { return e.name; });
79
+ var retired = RETIRED_ARCHIVE_FILES.filter(function (f) { return names.indexOf(f) !== -1; });
80
+ if (retired.length) {
81
+ _fail('the archive carries ' + retired.join(' and ') + ': ' + RETIRED + '.');
105
82
  }
106
- if (!byName[FILE_EXPERIMENT_JS]) {
107
- throw new Error('[bundle] importBundle: ' + FILE_EXPERIMENT_JS + ' missing from the archive.');
83
+ if (names.indexOf(MANIFEST_FILE) === -1) {
84
+ _fail('the archive carries no ' + MANIFEST_FILE + ' at its root — a project archive is the folder zipped ' +
85
+ '(frqtl.json + sketch.js + …), as an Export writes it.');
108
86
  }
87
+ var files = {};
88
+ var renderState = null;
89
+ entries.forEach(function (e) {
90
+ var text = _text(e.bytes, '"' + e.name + '"');
91
+ if (e.name === EXPORT_PAGE) { renderState = readBootBag(text); return; }
92
+ files[e.name] = text;
93
+ });
94
+ return normalizeFolderRow({ files: files, renderState: renderState });
95
+ }
109
96
 
110
- var manifest;
111
- try {
112
- manifest = JSON.parse(bytesText(byName[FILE_MANIFEST_JSON]));
113
- } catch (e) {
114
- throw new Error('[bundle] importBundle: ' + FILE_MANIFEST_JSON + ' is not valid JSON — ' + (e && e.message));
97
+ function _fromJson(text) {
98
+ var raw;
99
+ try { raw = JSON.parse(text); }
100
+ catch (e) { _fail('the file is neither a project archive (.zip) nor a sketch (.js), and not valid JSON either.'); }
101
+ if (raw && typeof raw === 'object' && ('schemaVersion' in raw || 'scripts' in raw) && 'source' in raw) {
102
+ _fail('the file is a v4 module envelope: ' + RETIRED + '.');
115
103
  }
116
- var source = bytesText(byName[FILE_EXPERIMENT_JS]);
117
- var envelope = manifestToEnvelope(manifest, source);
118
- var previewPngBytes = byName[FILE_PREVIEW_PNG] || null;
104
+ _fail('the JSON file is not a project — Import takes a project archive (.zip) or one sketch (.js).');
105
+ }
106
+
107
+ function _fromSketch(text, fileName) {
108
+ var base = _baseName(fileName);
109
+ var name = base.replace(/\.js$/, '');
110
+ if (!name) { _fail('the sketch file "' + base + '" has no name before ".js".'); }
111
+ var files = {};
112
+ files[MANIFEST_FILE] = serializeManifest(manifestFor({ name: name, entry: SKETCH }));
113
+ files[SKETCH] = text;
114
+ return normalizeFolderRow({ files: files, renderState: null });
115
+ }
119
116
 
120
- return { envelope: envelope, previewPngBytes: previewPngBytes };
117
+ /**
118
+ * @param {Uint8Array} bytes
119
+ * @param {string} fileName — the imported file's name (a one-file sketch's basename is its project name)
120
+ * @returns {Readonly<{files: Object<string,string>, renderState: object}>}
121
+ */
122
+ export function importArchive(bytes, fileName) {
123
+ if (!(bytes instanceof Uint8Array)) { _fail('importArchive(bytes, fileName): bytes is a Uint8Array.'); }
124
+ if (typeof fileName !== 'string' || fileName.length === 0) { _fail('importArchive(bytes, fileName): the file\'s name.'); }
125
+ if (_isZip(bytes)) { return _fromZip(bytes); }
126
+ var base = _baseName(fileName);
127
+ if (/\.mjs$/i.test(base)) {
128
+ var entryRule = MANIFEST_KEYS.filter(function (r) { return r.key === 'entry'; })[0].rule;
129
+ _fail('"' + base + '" is a .mjs module — a one-file Import takes a .js sketch, because a project\'s entry is ' +
130
+ entryRule + ' (frqtl.json, lib/shim/project/manifest.js MANIFEST_KEYS). Rename it to .js and import it again.');
131
+ }
132
+ var text = _text(bytes, '"' + base + '"');
133
+ if (/\.js$/.test(base)) { return _fromSketch(text, base); }
134
+ if (/\.json$/i.test(base) || /^\s*\{/.test(text)) { return _fromJson(text); }
135
+ _fail('"' + base + '" is not a project — Import takes a project archive (.zip, as an Export writes it) or one ' +
136
+ 'sketch (.js).');
121
137
  }
@@ -1,21 +1,24 @@
1
1
  /* Copyright 2023-2026 by Essam Abadir */
2
2
 
3
3
  /**
4
- * lib/shim/bundle/indexHtml.js — the standalone `index.html` projection of a
5
- * recipe bundle.
4
+ * lib/shim/bundle/indexHtml.js — the generated `index.html` of an exported project folder: the HOST PAGE,
5
+ * and the ONE writer + ONE parser of the boot bag it carries.
6
6
  *
7
- * A byte-template host that boots the bundle folder as a SERVED PROJECT: `bootProject(mount,
8
- * { url: './', entry: 'experiment.js' }, { renderState })` from `@descix/frqtl-web@<pin>` on the CDN. The
9
- * recipe is a sketch module whose imports name the published self-reference (`@descix/frqtl-web/...`);
10
- * the page's import map carries those rows and the scene doors' row, rendered by the ONE table
11
- * (`pageImportMapFor(<the pinned dist root>/)`, lib/shim/project/specifiers.js), so the sketch resolves
12
- * to the SAME module URLs the boot loads. The pinned dist root is the router's one spelling
13
- * (`cdnDistBaseFor`); the `@<pin>` is the passed-in `sdkVersion` (DERIVED at export time — never a
14
- * literal here).
7
+ * buildIndexHtml({ name, sdkVersion, renderState }) → the page text
8
+ * readBootBag(html) → the renderState it carries (the import's one reader)
9
+ * BOOT_BAG_ID 'frqtl-boot' — the bag's element id
15
10
  *
16
- * DRY: `experiment.js` stays the ONE source of truth — the page links it where it is served rather than
17
- * embedding a second copy. `renderState` (small, manifest-derived) is embedded inline as JSON. A page
18
- * opened via `file://` cannot fetch the project: the error says to serve the folder over http.
11
+ * The page boots the folder it sits in as a SERVED PROJECT: `bootProject(mount, { url: './' }, { renderState })`
12
+ * from `@descix/frqtl-web@<pin>` on the CDN. `frqtl.json` names the entry (P2a: the linker reads it first), so
13
+ * the page names no file. The import map is the ONE table's (`pageImportMapFor(<the pinned dist root>/)`,
14
+ * lib/shim/project/specifiers.js), so a sketch's `@descix/frqtl-web/...` imports resolve to the same module URLs
15
+ * the boot loads. The pinned dist root is the router's one spelling (`cdnDistBaseFor`); the pin is the passed-in
16
+ * `sdkVersion` (derived at export time, never a literal here).
17
+ *
18
+ * The boot bag — the host's renderState, which never enters frqtl.json or the project identity — is embedded as
19
+ * `<script type="application/json" id="frqtl-boot">`, `<` escaped so no payload closes the element. The page reads
20
+ * it from that element; an Import reads it back with readBootBag and drops the page. A page opened via `file://`
21
+ * cannot fetch the folder: it says to serve the folder over http.
19
22
  *
20
23
  * Layer 2 (physics): none — pure host projection.
21
24
  */
@@ -25,41 +28,38 @@
25
28
  import { pageImportMapFor, FRQTL_WEB_BOOT } from '../project/specifiers.js';
26
29
  import { cdnDistBaseFor } from '../engine/router.js';
27
30
 
31
+ /** The boot bag's element id. */
32
+ export var BOOT_BAG_ID = 'frqtl-boot';
33
+
34
+ var _BAG_OPEN = '<script type="application/json" id="' + BOOT_BAG_ID + '">';
35
+ var _BAG_CLOSE = '</script>';
36
+
28
37
  function _escapeHtml(s) {
29
38
  return String(s == null ? '' : s)
30
39
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
31
40
  }
32
41
 
33
- /** Safe inline-JSON: escape `<` so a `</script>`-like payload can't break out. */
42
+ /** Safe inline JSON: `<` escaped so a `</script>`-like payload cannot break out. */
34
43
  function _inlineJson(obj) {
35
44
  return JSON.stringify(obj == null ? null : obj).replace(/</g, '\\u003c');
36
45
  }
37
46
 
38
47
  /**
39
- * Build the standalone `index.html` for a recipe bundle.
40
- *
41
48
  * @param {object} args
42
- * @param {string} args.name — project name (page title).
43
- * @param {string} args.sdkVersion — the `@descix/frqtl-web@<pin>` version (REQUIRED).
44
- * @param {object} [args.renderState] — normalized renderState bag (inlined as JSON;
45
- * forwarded to bootProject's `{renderState}`).
46
- * @param {string[]} [args.scripts] — the v4 envelope's setup-factory delta (inlined as JSON): nothing
47
- * classic is left to load, so the page REFUSES a non-empty list at boot,
48
- * naming the fix (import the factory in the sketch).
49
- * @param {string} [args.recipeFile='experiment.js'] — the project's entry file.
50
- * @returns {string} the complete `index.html`.
49
+ * @param {string} args.name — the project's name (frqtl.json `name`; the page title)
50
+ * @param {string} args.sdkVersion — the `@descix/frqtl-web@<pin>` version (REQUIRED)
51
+ * @param {object|null} args.renderState — the normalized boot bag (null boots with the page's defaults)
52
+ * @returns {string} the complete `index.html`
51
53
  */
52
54
  export function buildIndexHtml(args) {
53
55
  args = args || {};
54
56
  if (typeof args.sdkVersion !== 'string' || args.sdkVersion.length === 0) {
55
- throw new Error('[bundle/indexHtml] buildIndexHtml: args.sdkVersion is REQUIRED (the CDN pin).');
57
+ throw new Error('[bundle/indexHtml] buildIndexHtml: args.sdkVersion is REQUIRED (the CDN pin). (FAIL-LOUD.)');
58
+ }
59
+ if (typeof args.name !== 'string' || args.name.length === 0) {
60
+ throw new Error('[bundle/indexHtml] buildIndexHtml: args.name is the project\'s frqtl.json name. (FAIL-LOUD.)');
56
61
  }
57
- var name = (typeof args.name === 'string' && args.name.length > 0) ? args.name : 'FRQTL Recipe';
58
- var recipeFile = (typeof args.recipeFile === 'string' && args.recipeFile.length > 0)
59
- ? args.recipeFile : 'experiment.js';
60
62
  var importMapJson = _inlineJson(pageImportMapFor(cdnDistBaseFor(args.sdkVersion) + '/'));
61
- var renderStateJson = _inlineJson(args.renderState || null);
62
- var scriptsJson = _inlineJson(Array.isArray(args.scripts) ? args.scripts : []);
63
63
 
64
64
  return [
65
65
  '<!DOCTYPE html>',
@@ -67,7 +67,7 @@ export function buildIndexHtml(args) {
67
67
  '<head>',
68
68
  ' <meta charset="UTF-8">',
69
69
  ' <meta name="viewport" content="width=device-width, initial-scale=1">',
70
- ' <title>' + _escapeHtml(name) + ' — FRQTL</title>',
70
+ ' <title>' + _escapeHtml(args.name) + ' — FRQTL</title>',
71
71
  ' <style>',
72
72
  ' html,body{margin:0;height:100%;background:#0b0b0f;color:#ddd;',
73
73
  ' font:14px/1.5 ui-sans-serif,system-ui,sans-serif;}',
@@ -80,23 +80,21 @@ export function buildIndexHtml(args) {
80
80
  '<body>',
81
81
  ' <div id="frqtl-mount"></div>',
82
82
  ' <pre id="frqtl-err"></pre>',
83
+ ' ' + _BAG_OPEN + _inlineJson(args.renderState) + _BAG_CLOSE,
83
84
  ' <script type="importmap">' + importMapJson + '</script>',
84
85
  ' <script type="module">',
85
86
  ' import { bootProject } from ' + JSON.stringify(FRQTL_WEB_BOOT.specifier) + ';',
86
- ' const RENDER_STATE = ' + renderStateJson + ';',
87
- ' const SCRIPTS = ' + scriptsJson + ';',
87
+ ' const renderState = JSON.parse(document.getElementById(' + JSON.stringify(BOOT_BAG_ID) + ').textContent);',
88
88
  ' const mount = document.getElementById("frqtl-mount");',
89
89
  ' const errBox = document.getElementById("frqtl-err");',
90
90
  ' function fail(title, detail){',
91
91
  ' errBox.style.display = "block";',
92
92
  ' errBox.textContent = title + "\\n\\n" + detail;',
93
93
  ' }',
94
- ' if (SCRIPTS.length > 0) {',
95
- ' fail("FRQTL project boot refused", "scripts " + JSON.stringify(SCRIPTS) + " is a classic setup-factory delta, and nothing classic is left to load — import the factory in the sketch (it imports what it uses from @descix/frqtl-web/...).");',
96
- ' } else if (location.protocol === "file:") {',
97
- ' fail("Serve this folder over http", "e.g. `npx serve` — a file:// page cannot fetch ' + _escapeHtml(recipeFile) + '.");',
94
+ ' if (location.protocol === "file:") {',
95
+ ' fail("Serve this folder over http", "e.g. `npx serve` — a file:// page cannot fetch the project files.");',
98
96
  ' } else {',
99
- ' bootProject(mount, { url: new URL("./", location.href).href, entry: ' + JSON.stringify(recipeFile) + ' }, { renderState: RENDER_STATE })',
97
+ ' bootProject(mount, { url: new URL("./", location.href).href }, { renderState: renderState })',
100
98
  ' .catch((e) => fail("FRQTL project boot failed", String(e && e.stack || e)));',
101
99
  ' }',
102
100
  ' </script>',
@@ -105,3 +103,32 @@ export function buildIndexHtml(args) {
105
103
  ''
106
104
  ].join('\n');
107
105
  }
106
+
107
+ /**
108
+ * THE parser of the boot bag: the renderState an exported page carries (null when it carries none).
109
+ * THROWS when the page holds no bag element, or more than one, or one that is not JSON.
110
+ * @param {string} html
111
+ * @returns {object|null}
112
+ */
113
+ export function readBootBag(html) {
114
+ if (typeof html !== 'string') {
115
+ throw new Error('[bundle/indexHtml] readBootBag(html): the page text (got ' + typeof html + '). (FAIL-LOUD.)');
116
+ }
117
+ var at = html.indexOf(_BAG_OPEN);
118
+ if (at === -1) {
119
+ throw new Error('[bundle/indexHtml] the index.html carries no boot bag (<script type="application/json" id="' +
120
+ BOOT_BAG_ID + '">) — it is not the page an Export writes. (FAIL-LOUD.)');
121
+ }
122
+ if (html.indexOf(_BAG_OPEN, at + 1) !== -1) {
123
+ throw new Error('[bundle/indexHtml] the index.html carries two boot bags — an Export writes one. (FAIL-LOUD.)');
124
+ }
125
+ var start = at + _BAG_OPEN.length;
126
+ var end = html.indexOf(_BAG_CLOSE, start);
127
+ if (end === -1) {
128
+ throw new Error('[bundle/indexHtml] the boot bag element is not closed. (FAIL-LOUD.)');
129
+ }
130
+ try { return JSON.parse(html.slice(start, end)); }
131
+ catch (e) {
132
+ throw new Error('[bundle/indexHtml] the boot bag is not valid JSON: ' + ((e && e.message) || e) + '. (FAIL-LOUD.)');
133
+ }
134
+ }
@@ -148,7 +148,7 @@ var DIST_PROFILE = buildDistProfile('../..');
148
148
  // lib/shim/engine/test/router.test.mjs asserts them against the two
149
149
  // package.json#version fields so a forgotten bump fails `make verify`, not a
150
150
  // silent stale-CDN-URL bug.
151
- var CDN_SDK_VERSION = '0.6.0-p0.2'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
151
+ var CDN_SDK_VERSION = '0.6.0-p4.1'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
152
152
  var CDN_MATH_SDK_VERSION = '0.3.2'; // sdk/egpt-math-sdk/package.json#version — bound by router.test.mjs; bumped with each npm publish
153
153
 
154
154
  /**
@@ -507,7 +507,7 @@ var _assertProfileParity = (function () {
507
507
  * // CDN mode (static hosting — the artifact host shell has no server, no page anchor):
508
508
  * const cdnRouter = engineRouter({ mode: 'cdn', engine: 'wasm' });
509
509
  * console.log(cdnRouter.resolve('compat/engine.js'));
510
- * // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.0-p0.2/dist/frqtl/compat/engine.js'
510
+ * // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.0-p4.1/dist/frqtl/compat/engine.js'
511
511
  *
512
512
  * // CDN mode, self-hosted: a copy of dist/ served from the consumer's own origin:
513
513
  * const selfHosted = engineRouter({ mode: 'cdn', engine: 'wasm', base: 'https://example.org/vendor/frqtl-web/dist' });
@@ -22,7 +22,7 @@ export { WALKTHROUGH, walkthroughIs3D, walkthroughModes, walkthroughGroup,
22
22
  walkthroughById,
23
23
  FIRST_CONTACT_WALKTHROUGH_ID,
24
24
  defaultWalkthroughModeId, effectiveRenderState } from './registry/walkthrough.js';
25
- export { loadSceneSource, scenePath, EMPTY_SCENE } from './registry/scene-source.js';
25
+ export { projectOf, scenePath, EMPTY_SCENE } from './registry/scene-source.js';
26
26
  // The host-side experiment-iframe guard (the false-200 trap). Exported from the
27
27
  // barrel so every host that frames an experiment page (lib/features/ide's GYM)
28
28
  // asserts against the SAME contract.
@@ -99,7 +99,7 @@ export const PROSE_CELL_TYPES = Object.freeze(['markdown']);
99
99
  * e.g. `setupDoubleSlit`). Absent → `'factory'`, so every existing notebook is
100
100
  * byte-identical.
101
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
102
+ * RecipeContext arg — the sketch form the scene files
103
103
  * write). The shim builds a
104
104
  * `frqtl.RecipeContext` and runs `exp.build(buildExperiment)` (the canonical
105
105
  * co-init call + `addObject()` recipe path), NOT the factory signature.
@@ -5,9 +5,8 @@
5
5
  *
6
6
  * Thin CRUD helpers that persist a serialized <frqtl-notebook> document on top
7
7
  * of any StorageProvider (lib/shim/storage/index.js) using the "notebooks"
8
- * namespace. Mirrors the shape of lib/shim/storage/modules.js and
9
- * lib/shim/storage/projects.js — ONE persistent-store pattern across Studio
10
- * projects, Studio modules, and Notebooks.
8
+ * namespace, through the shared namespace-CRUD factory
9
+ * (lib/shim/storage/make-namespace-store.js).
11
10
  *
12
11
  * ── Why this lives in lib/shim ──────────────────────────────────────────────
13
12
  *
@@ -185,8 +185,8 @@ var HOST_TOKENS = Object.freeze([
185
185
  why: 'chrome is a HOST-COMPOSITION decision (whether the experiment\'s own ' +
186
186
  'control chrome DOM renders), never a per-experiment physics/render ' +
187
187
  'parameter; forwarding it from an outer host URL into an embedded ' +
188
- 'iframe would reintroduce the hidden-experiment-chrome bug fixed in ' +
189
- 'WS gym-controls-as-sugar Bite 2 (the GYM must always show the ' +
188
+ 'iframe would reintroduce the hidden-experiment-chrome bug (the GYM ' +
189
+ 'must always show the ' +
190
190
  'experiment\'s own full chrome, never chrome=canvas — see the ' +
191
191
  '"WHEN should a host set chrome=canvas" doc above for the general ' +
192
192
  'criterion, not just the GYM\'s specific answer to it)'