@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
@@ -31,13 +31,14 @@
31
31
  * `runner.updateObjectProperty(lo, 'visible', v)`, backdrop:v. An ABSENT
32
32
  * `backdropVisible` (null after normalizing) is not written — the scene's own
33
33
  * `addBackdrop(…, {visible})` rules — and reports backdrop:null, as does no backdrop LO.
34
- * `pageControls` are NOT applied here: they are STUDIO's host-specific override.
34
+ * Page controls are not state: a sketch declares them in its own code.
35
35
  *
36
36
  * backdropOf(runner) — THE answer to "which LO is the backdrop" for a host: the
37
37
  * census LO marked `backdrop === true`, or null. STUDIO's
38
38
  * save captures its visibility through it, so capture and
39
39
  * replay read one fact.
40
- * normalizeSavedRenderState(raw) — tolerant normalizer for a LOADED bag (never throws).
40
+ * normalizeSavedRenderState(raw) — tolerant normalizer for a LOADED bag; THROWS only on a bag
41
+ * that carries page controls (they are author code now).
41
42
  * pickCameraReplay(saved) — pure decision: named preset over raw orbit.
42
43
  * partitionRenderRows(rows, declared, known) — apply / retain / throw, before anything is written.
43
44
  * renderRowsFromBag(bag) — a { key: value } render-option bag as ordered rows.
@@ -83,12 +84,18 @@ function pickCameraReplay(saved) {
83
84
  * seed: string|null,
84
85
  * backdropVisible: boolean|null,
85
86
  * camera: {orbit:object|null, preset:string|null}|null,
86
- * render: {key:string, value:*}[],
87
- * pageControls: {id:string, value:*, bind:{object:string,prop:string}|null}[]
87
+ * render: {key:string, value:*}[]
88
88
  * }}
89
+ * @throws {Error} a bag whose `pageControls` holds a row — page controls are declared in the sketch's own
90
+ * code, so a saved bag never carries them; migrate the project (move the controls into its sketch).
89
91
  */
90
92
  function normalizeSavedRenderState(raw) {
91
93
  var r = _isPlainObject(raw) ? raw : {};
94
+ if (Array.isArray(r.pageControls) && r.pageControls.length > 0) {
95
+ throw new Error('[render-state] this saved project carries page controls (renderState.pageControls: ' +
96
+ r.pageControls.map(function (row) { return _isPlainObject(row) ? String(row.id) : '?'; }).join(', ') +
97
+ ') — page controls are declared in the sketch\'s own code; move them into the sketch and save again. (FAIL-LOUD.)');
98
+ }
92
99
  var camera = null;
93
100
  if (_isPlainObject(r.camera)) {
94
101
  camera = {
@@ -105,16 +112,6 @@ function normalizeSavedRenderState(raw) {
105
112
  camera: camera,
106
113
  render: Array.isArray(r.render) ? r.render.filter(function (row) {
107
114
  return _isPlainObject(row) && typeof row.key === 'string' && row.key.length > 0;
108
- }) : [],
109
- pageControls: Array.isArray(r.pageControls) ? r.pageControls.filter(function (row) {
110
- return _isPlainObject(row) && typeof row.id === 'string' && row.id.length > 0;
111
- }).map(function (row) {
112
- return {
113
- id: row.id,
114
- value: row.value,
115
- bind: (_isPlainObject(row.bind) && typeof row.bind.object === 'string' &&
116
- typeof row.bind.prop === 'string') ? { object: row.bind.object, prop: row.bind.prop } : null
117
- };
118
115
  }) : []
119
116
  };
120
117
  }
@@ -0,0 +1,88 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/storage/codec-chains.js — THE owner of a ROSETTA codec chain saved in browser storage.
5
+ *
6
+ * A codec chain is one module text (the codec page's `chainToModule` output) under a name. It is not a sketch
7
+ * project (frqtl.json is "kind": "scene"), so it has its own small store:
8
+ *
9
+ * CODEC_CHAINS_NAMESPACE = 'codec-chains'
10
+ * CODEC_CHAIN_KEYS = ['name', 'source'] the stored row, keyed by `name`
11
+ *
12
+ * saveChain(provider, row) → name Save: write under its own name (overwrites)
13
+ * saveChainAs(provider, row, newName) → newName Save As: REFUSED when newName is taken
14
+ * loadChain(provider, name) → row | null
15
+ * listChains(provider) → [{ name }] ascending
16
+ * deleteChain(provider, name)
17
+ *
18
+ * The name follows the storage seam's key rule (index.js checkStorageKey), as a project folder's does. The
19
+ * codec page's deep link `?module=<name>` is a key of this namespace.
20
+ *
21
+ * FAIL-LOUD. Layer 2 (physics): none.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ import { checkStorageKey } from './index.js';
27
+
28
+ export var CODEC_CHAINS_NAMESPACE = 'codec-chains';
29
+ export var CODEC_CHAIN_KEYS = Object.freeze(['name', 'source']);
30
+
31
+ function _fail(msg) {
32
+ throw new Error('[codec-chains] ' + msg + ' (FAIL-LOUD.)');
33
+ }
34
+
35
+ /** @returns {Readonly<{name: string, source: string}>} */
36
+ export function normalizeChainRow(row) {
37
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
38
+ _fail('a codec chain row is { ' + CODEC_CHAIN_KEYS.join(', ') + ' } (got ' + (row === null ? 'null' : typeof row) + ').');
39
+ }
40
+ var unknown = Object.keys(row).filter(function (k) { return CODEC_CHAIN_KEYS.indexOf(k) === -1; });
41
+ if (unknown.length) {
42
+ _fail('unknown key ' + unknown.map(function (k) { return JSON.stringify(k); }).join(', ') +
43
+ ' — a codec chain row holds exactly CODEC_CHAIN_KEYS: ' + CODEC_CHAIN_KEYS.join(', ') + '.');
44
+ }
45
+ checkStorageKey(row.name, 'a codec chain name');
46
+ if (typeof row.source !== 'string' || row.source.length === 0) {
47
+ _fail('"source" is the chain\'s module text, a non-empty string.');
48
+ }
49
+ return Object.freeze({ name: row.name, source: row.source });
50
+ }
51
+
52
+ export function saveChain(provider, row) {
53
+ var n = normalizeChainRow(row);
54
+ provider.put(CODEC_CHAINS_NAMESPACE, n.name, { name: n.name, source: n.source });
55
+ return n.name;
56
+ }
57
+
58
+ export function saveChainAs(provider, row, newName) {
59
+ checkStorageKey(newName, 'a codec chain name');
60
+ var n = normalizeChainRow(Object.assign({}, row, { name: newName }));
61
+ if (provider.get(CODEC_CHAINS_NAMESPACE, newName) !== null) {
62
+ _fail('a codec chain named "' + newName + '" is already saved — Save As takes a name no saved chain has.');
63
+ }
64
+ provider.put(CODEC_CHAINS_NAMESPACE, n.name, { name: n.name, source: n.source });
65
+ return n.name;
66
+ }
67
+
68
+ export function loadChain(provider, name) {
69
+ checkStorageKey(name, 'a codec chain name');
70
+ var raw = provider.get(CODEC_CHAINS_NAMESPACE, name);
71
+ if (raw === null || raw === undefined) { return null; }
72
+ var row;
73
+ try { row = normalizeChainRow(raw); }
74
+ catch (e) { _fail('the saved chain "' + name + '" is not a codec chain row: ' + ((e && e.message) || e)); }
75
+ if (row.name !== name) { _fail('the saved chain under "' + name + '" names itself "' + row.name + '".'); }
76
+ return row;
77
+ }
78
+
79
+ export function listChains(provider) {
80
+ return provider.list(CODEC_CHAINS_NAMESPACE).map(function (e) { return e.key; }).sort().map(function (k) {
81
+ return { name: k };
82
+ });
83
+ }
84
+
85
+ export function deleteChain(provider, name) {
86
+ checkStorageKey(name, 'a codec chain name');
87
+ provider['delete'](CODEC_CHAINS_NAMESPACE, name);
88
+ }
@@ -0,0 +1,205 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/storage/folders.js — THE owner of a project saved in browser storage: one project = one folder.
5
+ *
6
+ * A project is a folder of files: `frqtl.json` (its owner is
7
+ * lib/shim/project/manifest.js), the entry sketch it names, and whatever else the author keeps beside them.
8
+ * The stored ROW is the folder plus the host's boot bag, and nothing else:
9
+ *
10
+ * FOLDER_ROW_KEYS = ['files', 'renderState']
11
+ * files { path: text } — text only (a binary value is refused by name); every path through
12
+ * lib/shim/project/identity.js checkProjectPath, the one path rule; a root `index.html` is
13
+ * refused (EXPORT_PAGE: the Export writes it); `frqtl.json` is required and read by its owner.
14
+ * renderState the host's boot bag (STUDIO's capture, a rung's effectiveRenderState), normalized by its one
15
+ * owner (lib/shim/render-state normalizeSavedRenderState) and ferried — never re-listed here.
16
+ * Boot facts live beside the folder, never in frqtl.json.
17
+ *
18
+ * The key is the manifest `name`, in FOLDERS_NAMESPACE. Exports:
19
+ *
20
+ * normalizeFolderRow(row) → the frozen row (the one validation every reader and writer uses)
21
+ * folderNameOf(row) → its frqtl.json name
22
+ * saveFolder(provider, row) → name Save: write the folder under its own name (overwrites)
23
+ * saveFolderAs(provider, row, newName) → newName Save As: frqtl.json's name rewritten through
24
+ * serializeManifest; REFUSED when newName is taken
25
+ * copyFolder(provider, name, newName) → newName a stored folder copied (saveFolderAs over loadFolder)
26
+ * loadFolder(provider, name) → row | null
27
+ * listFolders(provider) → [{ name }] in ascending name order
28
+ * deleteFolder(provider, name)
29
+ *
30
+ * Retired saves (DESCIX IS NOT LIVE; there is no migration tool): the namespaces RETIRED_NAMESPACES (owned by
31
+ * the storage seam, index.js, which refuses a write into one) held the v4 module envelope and the design
32
+ * envelope. They are never read and never converted:
33
+ *
34
+ * retiredSaves(provider) → [{ namespace, key }] — every key in a retired namespace
35
+ * deleteRetiredSaves(provider, saves) → the count deleted — EXACTLY the listed keys (each must be in a
36
+ * retired namespace); nothing else is touched
37
+ *
38
+ * FAIL-LOUD: every refusal names the rule it applies. Layer 2 (physics): none — this stores text.
39
+ */
40
+
41
+ 'use strict';
42
+
43
+ import { RETIRED_NAMESPACES, checkStorageKey } from './index.js';
44
+ import { checkProjectPath } from '../project/identity.js';
45
+ import { MANIFEST_FILE, parseManifest, serializeManifest } from '../project/manifest.js';
46
+ import { normalizeSavedRenderState } from '../render-state/index.js';
47
+
48
+ export { RETIRED_NAMESPACES };
49
+
50
+ /** The storage namespace every project folder is saved under. */
51
+ export var FOLDERS_NAMESPACE = 'project-folders';
52
+
53
+ /** The stored row's keys — the folder and the host's boot bag. */
54
+ export var FOLDER_ROW_KEYS = Object.freeze(['files', 'renderState']);
55
+
56
+ /** The root page the Export generates (lib/shim/bundle) — reserved, so a folder never holds one. */
57
+ export var EXPORT_PAGE = 'index.html';
58
+
59
+ function _fail(msg) {
60
+ throw new Error('[folders] ' + msg + ' (FAIL-LOUD.)');
61
+ }
62
+
63
+ function _isPlainObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
64
+
65
+ /**
66
+ * @param {{files: Object<string,string>, renderState?: object|null}} row
67
+ * @returns {Readonly<{files: Readonly<Object<string,string>>, renderState: object}>}
68
+ */
69
+ export function normalizeFolderRow(row) {
70
+ if (!_isPlainObject(row)) {
71
+ _fail('a project folder row is { ' + FOLDER_ROW_KEYS.join(', ') + ' } (got ' +
72
+ (row === null ? 'null' : Array.isArray(row) ? 'an array' : typeof row) + ').');
73
+ }
74
+ var unknown = Object.keys(row).filter(function (k) { return FOLDER_ROW_KEYS.indexOf(k) === -1; });
75
+ if (unknown.length) {
76
+ _fail('unknown key ' + unknown.map(function (k) { return JSON.stringify(k); }).join(', ') +
77
+ ' — a project folder row holds exactly FOLDER_ROW_KEYS (lib/shim/storage/folders.js): ' +
78
+ FOLDER_ROW_KEYS.join(', ') + '.');
79
+ }
80
+ var files = row.files;
81
+ if (!_isPlainObject(files) || Object.keys(files).length === 0) {
82
+ _fail('"files" is the folder { path: text } with at least ' + MANIFEST_FILE + ' (got ' +
83
+ (_isPlainObject(files) ? 'an empty map' : typeof files) + ').');
84
+ }
85
+ var out = {};
86
+ Object.keys(files).forEach(function (p) {
87
+ checkProjectPath(p);
88
+ if (p === EXPORT_PAGE) {
89
+ _fail('"' + EXPORT_PAGE + '" at the folder root is reserved — the Export generates it (the host page ' +
90
+ 'that carries the boot bag); a project folder never holds one.');
91
+ }
92
+ if (typeof files[p] !== 'string') {
93
+ _fail('"' + p + '" is not text (got ' + (files[p] instanceof Uint8Array ? 'bytes' : typeof files[p]) +
94
+ ') — a project folder holds text files only.');
95
+ }
96
+ out[p] = files[p];
97
+ });
98
+ if (typeof out[MANIFEST_FILE] !== 'string') {
99
+ _fail('the folder carries no ' + MANIFEST_FILE + ' — a project folder is named and entered by its manifest ' +
100
+ '(lib/shim/project/manifest.js manifestFor({ name, entry }) writes one).');
101
+ }
102
+ var name = parseManifest(out[MANIFEST_FILE]).name;
103
+ checkStorageKey(name, 'a project folder name (frqtl.json "name")');
104
+ return Object.freeze({ files: Object.freeze(out), renderState: normalizeSavedRenderState(row.renderState) });
105
+ }
106
+
107
+ /** @param {object} row @returns {string} the folder's frqtl.json name */
108
+ export function folderNameOf(row) {
109
+ return parseManifest(normalizeFolderRow(row).files[MANIFEST_FILE]).name;
110
+ }
111
+
112
+ function _stored(row) {
113
+ return { files: Object.assign({}, row.files), renderState: row.renderState };
114
+ }
115
+
116
+ /**
117
+ * Save: write the folder under its own frqtl.json name, overwriting that name's folder.
118
+ * @returns {string} the name
119
+ */
120
+ export function saveFolder(provider, row) {
121
+ var n = normalizeFolderRow(row);
122
+ var name = parseManifest(n.files[MANIFEST_FILE]).name;
123
+ provider.put(FOLDERS_NAMESPACE, name, _stored(n));
124
+ return name;
125
+ }
126
+
127
+ /**
128
+ * Save As: the folder under `newName` — frqtl.json's name rewritten through serializeManifest (the one writer).
129
+ * REFUSED when a folder named `newName` is already saved.
130
+ * @returns {string} newName
131
+ */
132
+ export function saveFolderAs(provider, row, newName) {
133
+ checkStorageKey(newName, 'a project folder name');
134
+ var n = normalizeFolderRow(row);
135
+ if (provider.get(FOLDERS_NAMESPACE, newName) !== null) {
136
+ _fail('a project named "' + newName + '" is already saved — Save As takes a name no saved project has ' +
137
+ '(choose another, or delete the saved one first).');
138
+ }
139
+ var files = Object.assign({}, n.files);
140
+ files[MANIFEST_FILE] = serializeManifest(Object.assign({}, parseManifest(n.files[MANIFEST_FILE]), { name: newName }));
141
+ var renamed = normalizeFolderRow({ files: files, renderState: n.renderState });
142
+ provider.put(FOLDERS_NAMESPACE, newName, _stored(renamed));
143
+ return newName;
144
+ }
145
+
146
+ /** A stored folder copied under `newName` (frqtl.json's name rewritten); refuses a taken name or a missing source. */
147
+ export function copyFolder(provider, name, newName) {
148
+ var row = loadFolder(provider, name);
149
+ if (!row) { _fail('no saved project named "' + name + '" to copy.'); }
150
+ return saveFolderAs(provider, row, newName);
151
+ }
152
+
153
+ /** @returns {Readonly<object>|null} the normalized row, or null when no folder has that name */
154
+ export function loadFolder(provider, name) {
155
+ checkStorageKey(name, 'a project folder name');
156
+ var raw = provider.get(FOLDERS_NAMESPACE, name);
157
+ if (raw === null || raw === undefined) { return null; }
158
+ var row;
159
+ try { row = normalizeFolderRow(raw); }
160
+ catch (e) { _fail('the saved project "' + name + '" is not a project folder row: ' + ((e && e.message) || e)); }
161
+ var stored = parseManifest(row.files[MANIFEST_FILE]).name;
162
+ if (stored !== name) {
163
+ _fail('the saved project under "' + name + '" names itself "' + stored + '" in ' + MANIFEST_FILE +
164
+ ' — a folder is keyed by its manifest name.');
165
+ }
166
+ return row;
167
+ }
168
+
169
+ /** @returns {Array<{name: string}>} every saved folder, ascending by name */
170
+ export function listFolders(provider) {
171
+ return provider.list(FOLDERS_NAMESPACE).map(function (e) { return e.key; }).sort().map(function (k) {
172
+ return { name: k };
173
+ });
174
+ }
175
+
176
+ export function deleteFolder(provider, name) {
177
+ checkStorageKey(name, 'a project folder name');
178
+ provider['delete'](FOLDERS_NAMESPACE, name);
179
+ }
180
+
181
+ /** @returns {Array<{namespace: string, key: string}>} every key in a retired namespace (never read) */
182
+ export function retiredSaves(provider) {
183
+ var out = [];
184
+ RETIRED_NAMESPACES.forEach(function (ns) {
185
+ provider.list(ns).forEach(function (e) { out.push({ namespace: ns, key: e.key }); });
186
+ });
187
+ return out;
188
+ }
189
+
190
+ /**
191
+ * Delete exactly `saves` (retiredSaves' rows); a row outside a retired namespace is refused before anything
192
+ * is deleted.
193
+ * @returns {number} the count deleted
194
+ */
195
+ export function deleteRetiredSaves(provider, saves) {
196
+ if (!Array.isArray(saves)) { _fail('deleteRetiredSaves(provider, saves): saves is retiredSaves(provider)\'s list.'); }
197
+ saves.forEach(function (s) {
198
+ if (!s || RETIRED_NAMESPACES.indexOf(s.namespace) === -1 || typeof s.key !== 'string') {
199
+ _fail('deleteRetiredSaves deletes only retired saves ({ namespace in [' + RETIRED_NAMESPACES.join(', ') +
200
+ '], key }) — got ' + JSON.stringify(s) + '.');
201
+ }
202
+ });
203
+ saves.forEach(function (s) { provider['delete'](s.namespace, s.key); });
204
+ return saves.length;
205
+ }
@@ -45,16 +45,27 @@
45
45
  * // Then anywhere in the IDE:
46
46
  * import { getProvider } from 'lib/shim/storage';
47
47
  * const storage = getProvider();
48
- * storage.put('projects', 'abc123', { name: 'My project' });
49
- * const saved = storage.get('projects', 'abc123');
50
- * const all = storage.list('projects');
51
- * storage.delete('projects', 'abc123');
48
+ * storage.put('explainers', 'gym', { seen: true });
49
+ * const saved = storage.get('explainers', 'gym');
50
+ * const all = storage.list('explainers');
51
+ * storage.delete('explainers', 'gym');
52
52
  *
53
53
  * ── Contract validation ─────────────────────────────────────────────────────
54
54
  *
55
55
  * validateProvider(provider) — throws if provider does not implement the
56
56
  * contract. Called by setActiveProvider; consumers may also call it.
57
57
  *
58
+ * ── Retired namespaces ──────────────────────────────────────────────────────
59
+ *
60
+ * RETIRED_NAMESPACES — the namespaces of the retired save formats ('files': the v4 module
61
+ * envelope STUDIO and ROSETTA wrote; 'projects': the opaque design envelope). The project
62
+ * folder (folders.js) and the codec chain (codec-chains.js) replaced them. The active provider
63
+ * (getProvider) REFUSES a write into one, naming the replacement; reads, lists and deletes go
64
+ * through, so the retired-saves strip can name them and delete exactly them (folders.js
65
+ * retiredSaves / deleteRetiredSaves). Nothing reads or converts their rows.
66
+ * checkStorageKey(key, what) — THE key rule of a store keyed by a name (a project folder's
67
+ * frqtl.json name, a codec chain's name): a non-empty string with no "/" (reserved).
68
+ *
58
69
  * ── Server-side seam ───────────────────────────────────────────────────────
59
70
  *
60
71
  * The matching server routes are defined in lib/server/mounts.js under
@@ -69,6 +80,49 @@
69
80
 
70
81
  /** @type {StorageProvider|null} */
71
82
  var _activeProvider = null;
83
+ /** @type {StorageProvider|null} the active provider behind the retired-namespace write guard */
84
+ var _activeGuard = null;
85
+
86
+ /** The namespaces of the retired save formats — written by nothing (see the header). */
87
+ export var RETIRED_NAMESPACES = Object.freeze(['files', 'projects']);
88
+
89
+ var _RETIRED_BY = Object.freeze({
90
+ files: 'the project folder (lib/shim/storage/folders.js, namespace "project-folders") for a STUDIO save, ' +
91
+ 'the codec chain (lib/shim/storage/codec-chains.js, namespace "codec-chains") for a ROSETTA save',
92
+ projects: 'the project folder (lib/shim/storage/folders.js, namespace "project-folders")'
93
+ });
94
+
95
+ /**
96
+ * THE key rule of a store keyed by a name: a non-empty string with no "/" (reserved by the contract).
97
+ * @param {*} key
98
+ * @param {string} what — the store's noun, for the message (e.g. 'a project folder name')
99
+ * @throws {Error}
100
+ */
101
+ export function checkStorageKey(key, what) {
102
+ if (typeof key !== 'string' || key.length === 0) {
103
+ throw new Error('[StorageProvider] ' + what + ' is a non-empty string (got ' + JSON.stringify(key) + '). (FAIL-LOUD.)');
104
+ }
105
+ if (key.indexOf('/') !== -1) {
106
+ throw new Error('[StorageProvider] ' + what + ' ' + JSON.stringify(key) + ' contains "/" — "/" is reserved ' +
107
+ 'in a storage key; name it without one. (FAIL-LOUD.)');
108
+ }
109
+ }
110
+
111
+ function _guarded(provider) {
112
+ return Object.freeze({
113
+ get: function (namespace, key) { return provider.get(namespace, key); },
114
+ put: function (namespace, key, value) {
115
+ if (RETIRED_NAMESPACES.indexOf(namespace) !== -1) {
116
+ throw new Error('[StorageProvider] a write into the retired namespace "' + namespace + '" (key ' +
117
+ JSON.stringify(key) + ') is refused — that save format is retired; ' + _RETIRED_BY[namespace] +
118
+ ' replaced it. (FAIL-LOUD.)');
119
+ }
120
+ return provider.put(namespace, key, value);
121
+ },
122
+ 'delete': function (namespace, key) { return provider['delete'](namespace, key); },
123
+ list: function (namespace) { return provider.list(namespace); }
124
+ });
125
+ }
72
126
 
73
127
  // ── Contract typedefs ──────────────────────────────────────────────────────
74
128
 
@@ -129,10 +183,11 @@ export function validateProvider(provider) {
129
183
  export function setActiveProvider(provider) {
130
184
  validateProvider(provider);
131
185
  _activeProvider = provider;
186
+ _activeGuard = _guarded(provider);
132
187
  }
133
188
 
134
189
  /**
135
- * Retrieve the active storage driver.
190
+ * Retrieve the active storage driver, behind the retired-namespace write guard (RETIRED_NAMESPACES).
136
191
  * FAIL-LOUD: throws if no provider has been registered.
137
192
  *
138
193
  * @returns {StorageProvider}
@@ -146,18 +201,18 @@ export function getProvider() {
146
201
  'for a non-browser driver.'
147
202
  );
148
203
  }
149
- return _activeProvider;
204
+ return _activeGuard;
150
205
  }
151
206
 
152
207
  /**
153
- * Return the active provider, or null if none has been registered.
208
+ * Return the active provider (behind the same guard), or null if none has been registered.
154
209
  * Use this instead of getProvider() when a nil-safe check is appropriate
155
210
  * (e.g. during test setup before a provider is wired).
156
211
  *
157
212
  * @returns {StorageProvider|null}
158
213
  */
159
214
  export function peekProvider() {
160
- return _activeProvider;
215
+ return _activeGuard;
161
216
  }
162
217
 
163
218
  /**
@@ -166,4 +221,5 @@ export function peekProvider() {
166
221
  */
167
222
  export function resetProvider() {
168
223
  _activeProvider = null;
224
+ _activeGuard = null;
169
225
  }
@@ -3,9 +3,9 @@
3
3
  /**
4
4
  * lib/shim/storage/make-namespace-store.js — Generic namespace-CRUD factory.
5
5
  *
6
- * Factors out the byte-identical pattern shared by modules.js, projects.js,
7
- * and lib/shim/notebook/store.js. Each of those files is a thin config call
8
- * over this factory; the public-API exports they expose are unchanged.
6
+ * The id-keyed record store lib/shim/notebook/store.js is a thin config call
7
+ * over. (A project folder and a codec chain are keyed by NAME and have their
8
+ * own owners: folders.js, codec-chains.js.)
9
9
  *
10
10
  * ── Signature ──────────────────────────────────────────────────────────────
11
11
  *
@@ -13,12 +13,12 @@
13
13
  *
14
14
  * ── Config ─────────────────────────────────────────────────────────────────
15
15
  *
16
- * namespace {string} — StorageProvider namespace key (e.g. 'files').
16
+ * namespace {string} — StorageProvider namespace key (e.g. 'notebooks').
17
17
  * schemaVersion {number} — Current schema version integer (stamped on save
18
18
  * by callers; used here only in the returned
19
19
  * constant — actual stamping is the caller's job,
20
20
  * as in the originals).
21
- * idPrefix {string} — Prefix for generated ids (e.g. 'mod_').
21
+ * idPrefix {string} — Prefix for generated ids (e.g. 'nb_').
22
22
  * validate {function} — validate(record): throws on invalid envelope.
23
23
  * summaryFields {function} — summaryFields(record) → summary object, or
24
24
  * returns null to silently skip the record.
package/llms.txt CHANGED
@@ -1,12 +1,12 @@
1
1
  # @descix/frqtl-node
2
2
 
3
3
  > The FRQTL physics engine (compiled Rust → WASM) for node: the same modules as @descix/frqtl-web, plus
4
- > `bootNode` and the hosted eyes. Pre-release 0.6.0-p0.2 on npm dist-tag `next`. Node ≥ 22.12.
4
+ > `bootNode` and the hosted eyes. Pre-release 0.6.0-p4.1 on npm dist-tag `next`. Node ≥ 22.12.
5
5
 
6
6
  - Install: `npm install @descix/frqtl-node@next`
7
7
  - `import { bootNode, Wall } from '@descix/frqtl-node'` (require() works too); `await bootNode()` → the engine handle.
8
8
  - `./eyes`: `openHostedEyes({ sceneRecord, sceneSource: { kind: 'project', files, entry }, pageUrl })`.
9
9
  - `./project`: `linkProject` links a scene file against this package's own copy.
10
10
  - `./register`: `node --import @descix/frqtl-node/register scene.mjs` resolves a scene's `@descix/frqtl-web/...` imports to this package.
11
- - Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.6.0-p0.2/CLAUDE.md
11
+ - Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.6.0-p4.1/CLAUDE.md
12
12
  - Acceptance is visual; no CLI ships. No account, credits or tokens are needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descix/frqtl-node",
3
- "version": "0.6.0-p0.2",
3
+ "version": "0.6.0-p4.1",
4
4
  "description": "FRQTL for node \u2014 the discrete fractal physics engine (compiled Rust \u2192 WASM), its parts as ES modules, the node engine door (bootNode) and the hosted eyes.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,112 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * lib/shim/bundle/frqnb.js — the `.frqnb` teaching/gallery projection of a
5
- * recipe bundle.
6
- *
7
- * Builds a notebook OBJECT (never hand-emitted `<cell>` markup) and serializes
8
- * it THROUGH `lib/shim/notebook/format.js` `serialize` — the ONE notebook
9
- * serializer (no second serializer, no CELL_ATTRS mirror). The projection is:
10
- *
11
- * 1. markdown prose cell — title + teaching blurb.
12
- * 2. module cell (name './experiment.js') — carries the recipe source verbatim.
13
- * 3. canvas cell — wired to '<module>' (a recipe is the module's default export), carrying the render
14
- * intent (stage/seed/render3d/cameraPreset/renderOptions) ferried from the
15
- * normalized renderState bag.
16
- *
17
- * Because it serializes through format.js, ANY new CELL_ATTR added there flows
18
- * through this projection automatically — the conformance gate asserts it.
19
- *
20
- * The canvas cell carries `shape="recipe"` (the CELL_ATTRS discriminator owned by format.js):
21
- * the module's default export is a RecipeContext recipe (its co-init call + `exp.addObject`), and
22
- * the shim's setup-builder synthesizes `frqtl.RecipeContext` + `exp.build(buildExperiment)` for it.
23
- *
24
- * Layer 2 (physics): none — pure notebook projection.
25
- */
26
-
27
- 'use strict';
28
-
29
- import { serialize } from '../notebook/format.js';
30
-
31
- var MODULE_NAME = './experiment.js';
32
- var CANVAS_ID = 'c-recipe';
33
-
34
- /**
35
- * Fold a normalized renderState `render` array ([{key,value}]) into a plain
36
- * `{key: value}` bag for the canvas cell's `renderOptions` (CELL_ATTRS 'opts' kind).
37
- * @param {{key:string,value:*}[]} render
38
- * @returns {object}
39
- */
40
- function _renderOptionsBag(render) {
41
- var bag = {};
42
- if (Array.isArray(render)) {
43
- for (var i = 0; i < render.length; i++) {
44
- var row = render[i];
45
- if (row && typeof row.key === 'string' && row.key.length > 0) {
46
- bag[row.key] = row.value;
47
- }
48
- }
49
- }
50
- return bag;
51
- }
52
-
53
- /**
54
- * Build the `.frqnb` notebook string for a recipe bundle.
55
- *
56
- * @param {object} args
57
- * @param {string} args.name — human-readable project name (notebook title + markdown heading).
58
- * @param {string} args.source — the `experiment.js` recipe source (module cell body).
59
- * @param {object} [args.renderState] — a NORMALIZED renderState bag (from
60
- * `lib/shim/render-state` normalizeSavedRenderState). Render intent is ferried
61
- * onto the canvas cell; absent → the canvas carries stage/seed defaults only.
62
- * @param {string} [args.stage='600x400'] — canvas stage '<W>x<H>'.
63
- * @param {string} [args.prose] — optional teaching blurb appended under the heading.
64
- * @returns {string} the serialized `.frqnb` notebook.
65
- */
66
- export function buildFrqnb(args) {
67
- args = args || {};
68
- if (typeof args.source !== 'string' || args.source.length === 0) {
69
- throw new Error('[bundle/frqnb] buildFrqnb: args.source (experiment.js) is required.');
70
- }
71
- var name = (typeof args.name === 'string' && args.name.length > 0) ? args.name : 'FRQTL Recipe';
72
- var rs = args.renderState || {};
73
- var stage = (typeof args.stage === 'string' && args.stage.length > 0) ? args.stage : '600x400';
74
- var seed = (rs.seed != null && rs.seed !== '') ? String(rs.seed) : '42';
75
-
76
- var markdownBody = '# ' + name + '\n\n' +
77
- (typeof args.prose === 'string' && args.prose.length > 0
78
- ? args.prose
79
- : 'A FRQTL recipe. The module cell below is the `experiment.js` source of truth; ' +
80
- 'the canvas runs it live.');
81
-
82
- var canvasCell = {
83
- type: 'canvas',
84
- id: CANVAS_ID,
85
- setup: MODULE_NAME,
86
- // The module's DEFAULT export is a `buildExperiment(exp)` RECIPE (RecipeContext
87
- // form), NOT a `(frqtl,universe,options)` setup factory — declare it so the
88
- // shim synthesizes the RecipeContext path (a recipe canvas names no export).
89
- shape: 'recipe',
90
- stage: stage,
91
- seed: seed
92
- };
93
- // Ferry render intent from the normalized renderState bag onto the canvas cell.
94
- if (rs.renderer === 'three') { canvasCell.render3d = true; } // three IS the 3D display
95
- if (rs.camera && typeof rs.camera.preset === 'string' && rs.camera.preset.length > 0) {
96
- canvasCell.cameraPreset = rs.camera.preset;
97
- }
98
- var renderOptions = _renderOptionsBag(rs.render);
99
- if (Object.keys(renderOptions).length > 0) { canvasCell.renderOptions = renderOptions; }
100
-
101
- var notebook = {
102
- version: '1',
103
- title: name,
104
- cells: [
105
- { type: 'markdown', body: markdownBody },
106
- { type: 'module', name: MODULE_NAME, body: args.source },
107
- canvasCell
108
- ]
109
- };
110
-
111
- return serialize(notebook);
112
- }