@descix/frqtl-node 0.6.0-p1.1 → 0.6.0

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 (80) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +6 -4
  3. package/dist/dist-manifest.json +71 -65
  4. package/dist/eyes/index.js +5 -2
  5. package/dist/fat/pkg-node/package.json +2 -10
  6. package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
  7. package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
  8. package/dist/frqtl/objects/FrqtlApp.js +33 -6
  9. package/dist/frqtl/objects/RecipeContext.js +284 -13
  10. package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
  11. package/dist/frqtl/objects/instruments/capture.js +1 -24
  12. package/dist/frqtl/objects/instruments/hosted-eyes.js +216 -29
  13. package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
  14. package/dist/frqtl/objects/record/Record.js +346 -0
  15. package/dist/frqtl/objects/record/open-record.js +22 -0
  16. package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
  17. package/dist/frqtl/objects/schema/scopes.js +2 -2
  18. package/dist/frqtl/objects/setup/_worldInit.js +1 -2
  19. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
  20. package/dist/frqtl/parts/CameraControls.js +8 -0
  21. package/dist/frqtl/parts/DetectorWall.js +1 -1
  22. package/dist/frqtl/parts/FrqtlVision.js +21 -6
  23. package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
  24. package/dist/frqtl/parts/LargeObject.js +2 -2
  25. package/dist/frqtl/parts/LiveChart.js +2 -2
  26. package/dist/frqtl/parts/LogicGate.js +3 -3
  27. package/dist/frqtl/parts/OvenBox.js +1 -1
  28. package/dist/frqtl/parts/PointSource.js +1 -1
  29. package/dist/frqtl/parts/SlitLightSource.js +1 -1
  30. package/dist/frqtl/parts/SlitScreen.js +1 -1
  31. package/dist/frqtl/parts/SphereEmitter.js +12 -12
  32. package/dist/frqtl/parts/Trace.js +1 -1
  33. package/dist/frqtl/parts/Tunnel.js +11 -11
  34. package/dist/frqtl/parts/Wall.js +20 -10
  35. package/dist/frqtl/scenes/circuit-sat.js +1 -0
  36. package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
  37. package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
  38. package/dist/frqtl/scenes/mirror-route.js +96 -79
  39. package/dist/shim/bundle/index.js +118 -96
  40. package/dist/shim/bundle/indexHtml.js +66 -39
  41. package/dist/shim/controls/index.js +1 -1
  42. package/dist/shim/engine/artifact-shell.js +1 -1
  43. package/dist/shim/engine/router.js +3 -3
  44. package/dist/shim/host/host-view.js +228 -0
  45. package/dist/shim/host/index.js +18 -0
  46. package/dist/shim/index.js +3 -5
  47. package/dist/shim/notebook/format.js +1 -1
  48. package/dist/shim/notebook/kernel.js +1 -1
  49. package/dist/shim/notebook/setup-builder.js +2 -2
  50. package/dist/shim/notebook/store.js +2 -3
  51. package/dist/shim/package.json +1 -0
  52. package/dist/shim/page-params/index.js +2 -30
  53. package/dist/shim/project/fork.js +293 -0
  54. package/dist/shim/project/identity.js +3 -1
  55. package/dist/shim/project/index.js +16 -3
  56. package/dist/shim/project/link.js +224 -46
  57. package/dist/shim/project/manifest.js +151 -0
  58. package/dist/shim/project/parts-writer.js +68 -7
  59. package/dist/shim/project/read-text.js +43 -0
  60. package/dist/shim/project/record-format.js +345 -0
  61. package/dist/shim/project/specifiers.js +104 -15
  62. package/dist/shim/registry/exhibits.js +15 -34
  63. package/dist/shim/registry/index.js +3 -3
  64. package/dist/shim/registry/scene-roster.js +35 -0
  65. package/dist/shim/registry/scene-source.js +48 -64
  66. package/dist/shim/registry/walkthrough.js +6 -6
  67. package/dist/shim/render-state/apply.js +66 -31
  68. package/dist/shim/render-state/index.js +6 -2
  69. package/dist/shim/runtime/index.js +4 -4
  70. package/dist/shim/storage/codec-chains.js +88 -0
  71. package/dist/shim/storage/folders.js +205 -0
  72. package/dist/shim/storage/index.js +64 -8
  73. package/dist/shim/storage/make-namespace-store.js +5 -5
  74. package/llms.txt +4 -3
  75. package/package.json +19 -1
  76. package/dist/shim/bundle/frqnb.js +0 -112
  77. package/dist/shim/bundle/manifest.js +0 -95
  78. package/dist/shim/registry/experiments.js +0 -215
  79. package/dist/shim/storage/modules.js +0 -249
  80. package/dist/shim/storage/projects.js +0 -150
@@ -3,9 +3,16 @@
3
3
  /**
4
4
  * specifiers.js — THE one table of `@descix/frqtl-web/...` specifiers a sketch may import.
5
5
  *
6
- * FRQTL_WEB_SPECIFIERS rows { specifier, libPath, why } — a row whose specifier ends in `/`
7
- * is a PREFIX row (one part = one file under it)
6
+ * FRQTL_WEB_SPECIFIERS rows { specifier, libPath, why, tier } — a row whose specifier ends in `/`
7
+ * is a PREFIX row (one part = one file under it). `tier` is a key of SPECIFIER_TIERS:
8
+ * 'scene' (a sketch imports it) or 'part-internal' (a forked part file under parts/
9
+ * imports it, never a sketch: the parts' own internal imports, one row per file —
10
+ * the set equals what the registering parts import, held by fork.test.mjs G-CENSUS)
11
+ * SPECIFIER_TIERS { scene, 'part-internal' } — the tier vocabulary and who may import each
8
12
  * libPathOf(specifier) → the lib-identity path it names; THROWS on an unknown specifier
13
+ * tierOf(specifier) → the row's tier; THROWS on an unknown specifier (libPathOf's rows)
14
+ * specifierOfLibPath(libPath) → the specifier naming that lib-identity file, or null: libPathOf's inverse over
15
+ * the same rows (a prefix row answers for each file directly under it)
9
16
  * importMapFor(libRootUrl) → { imports } — the rows as an import map, every libPath appended to
10
17
  * the URL of the lib-identity root AS SEEN FROM THE PAGE: an absolute
11
18
  * URL or one starting with '/', './' or '../' (any other root THROWS —
@@ -20,32 +27,72 @@
20
27
  *
21
28
  * Inside lib/ the specifier is a SELF-REFERENCE — the package naming itself, the way node resolves a
22
29
  * package importing itself by name — not a lib→sdk edge. It is legal ONLY in sketch content
23
- * (lib/frqtl/scenes/**, codegen output, sketch and notebook text); closure modules import relatively
30
+ * (lib/frqtl/scenes/**, written sketches, sketch and notebook text); closure modules import relatively
24
31
  * (scripts/verify-arrows.mjs holds the scope). Consumers ferry this table and never restate it:
25
32
  * every page import map (importMapFor), the node resolve hook (node-specifier-hook.mjs) and the sdk
26
33
  * build's module closure (libPathOf).
27
34
  */
28
35
 
29
- /** The published surface a sketch imports. Every row is a public exports entry of both packages (sdk/_build/exports-contract.mjs derives them from this table). */
36
+ /** Who imports a row of each tier. */
37
+ export const SPECIFIER_TIERS = Object.freeze({
38
+ scene: Object.freeze({ id: 'scene', importedBy: 'any project file (a sketch imports it)' }),
39
+ 'part-internal': Object.freeze({ id: 'part-internal', importedBy: 'a part file (parts/*.js) of a project — a forked part imports its parent part\'s internals; never a sketch' })
40
+ });
41
+
42
+ /** The published surface a project imports. Every row is a public exports entry of both packages (sdk/_build/exports-contract.mjs derives them from this table). */
30
43
  export const FRQTL_WEB_SPECIFIERS = Object.freeze([
31
44
  Object.freeze({ specifier: '@descix/frqtl-web/parts', libPath: 'frqtl/parts/index.js',
32
- why: 'the parts barrel' }),
45
+ why: 'the parts barrel', tier: 'scene' }),
33
46
  Object.freeze({ specifier: '@descix/frqtl-web/parts/', libPath: 'frqtl/parts/',
34
- why: 'one part = one file' }),
47
+ why: 'one part = one file', tier: 'scene' }),
35
48
  Object.freeze({ specifier: '@descix/frqtl-web/shim/controls/registry.js', libPath: 'shim/controls/registry.js',
36
- why: 'knob registration (blackbody, circuit-sat, collision-matrix-3d, dse-tunnel-rebuild)' }),
49
+ why: 'knob registration (blackbody, circuit-sat, collision-matrix-3d, dse-tunnel-rebuild)', tier: 'scene' }),
37
50
  Object.freeze({ specifier: '@descix/frqtl-web/shim/render/sun-look.js', libPath: 'shim/render/sun-look.js',
38
- why: "the one owner of the sun's FX bag (dse-tunnel-rebuild)" }),
51
+ why: "the one owner of the sun's FX bag (dse-tunnel-rebuild)", tier: 'scene' }),
39
52
  Object.freeze({ specifier: '@descix/frqtl-web/compat/renderer/display-slot.js', libPath: 'frqtl/compat/renderer/display-slot.js',
40
- why: 'the booted display name before co-init (mirror-route, dse-tunnel-rebuild)' }),
53
+ why: 'the booted display name before co-init (mirror-route, dse-tunnel-rebuild)', tier: 'scene' }),
41
54
  Object.freeze({ specifier: '@descix/frqtl-web/compat/renderer/orbit-bag.js', libPath: 'frqtl/compat/renderer/orbit-bag.js',
42
- why: 'hasAngularPose, THE 3D-ness predicate (atomic-model)' }),
55
+ why: 'hasAngularPose, THE 3D-ness predicate (atomic-model)', tier: 'scene' }),
43
56
  Object.freeze({ specifier: '@descix/frqtl-web/objects/setup/_collisionMatrixLanes.js', libPath: 'frqtl/objects/setup/_collisionMatrixLanes.js',
44
- why: 'the one lane table (collision-matrix-3d)' }),
57
+ why: 'the one lane table (collision-matrix-3d)', tier: 'scene' }),
45
58
  Object.freeze({ specifier: '@descix/frqtl-web/compat/engine.js', libPath: 'frqtl/compat/engine.js',
46
- why: 'the engine accessor (atomic-model builds its frames by hand)' }),
59
+ why: 'the engine accessor (atomic-model builds its frames by hand)', tier: 'scene' }),
47
60
  Object.freeze({ specifier: '@descix/frqtl-web/kinds', libPath: 'frqtl/objects/kinds-entry.js',
48
- why: 'the kind registry: a composed fork registers its renamed class (registerKind, kindNamed, kindOf, registeredKinds)' })
61
+ why: 'the kind registry: a composed fork registers its renamed class (registerKind, kindNamed, kindOf, registeredKinds)', tier: 'scene' }),
62
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/tagger.js', libPath: 'frqtl/objects/objects/tagger.js',
63
+ why: 'imported by DetectorWall — a forked copy imports it', tier: 'part-internal' }),
64
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/schema/index.js', libPath: 'frqtl/objects/schema/index.js',
65
+ why: 'imported by DetectorWall, SlitScreen, Wall — a forked copy imports it', tier: 'part-internal' }),
66
+ Object.freeze({ specifier: '@descix/frqtl-web/shim/render/heat-colormap.js', libPath: 'shim/render/heat-colormap.js',
67
+ why: 'imported by GraphingDetectorWall — a forked copy imports it', tier: 'part-internal' }),
68
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/schema/valueNormalizer.js', libPath: 'frqtl/objects/schema/valueNormalizer.js',
69
+ why: 'imported by LiveChart, Wall — a forked copy imports it', tier: 'part-internal' }),
70
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/Box.js', libPath: 'frqtl/objects/objects/Box.js',
71
+ why: 'imported by OvenBox — a forked copy imports it', tier: 'part-internal' }),
72
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/QuantumEmitter.js', libPath: 'frqtl/objects/objects/QuantumEmitter.js',
73
+ why: 'imported by OvenBox — a forked copy imports it', tier: 'part-internal' }),
74
+ Object.freeze({ specifier: '@descix/frqtl-web/shim/expr/ast-schema.js', libPath: 'shim/expr/ast-schema.js',
75
+ why: 'imported by OvenBox, Tunnel — a forked copy imports it', tier: 'part-internal' }),
76
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/actionCompiler.js', libPath: 'frqtl/objects/objects/actionCompiler.js',
77
+ why: 'imported by OvenBox, Tunnel — a forked copy imports it', tier: 'part-internal' }),
78
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/v1-replacements.js', libPath: 'frqtl/objects/v1-replacements.js',
79
+ why: 'imported by SlitLightSource — a forked copy imports it', tier: 'part-internal' }),
80
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/slitLinerScatter.js', libPath: 'frqtl/objects/objects/slitLinerScatter.js',
81
+ why: 'imported by SlitScreen, Tunnel — a forked copy imports it', tier: 'part-internal' }),
82
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/setup/setupSphereEmitter3D.js', libPath: 'frqtl/objects/setup/setupSphereEmitter3D.js',
83
+ why: 'imported by SphereEmitter — a forked copy imports it', tier: 'part-internal' }),
84
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/directedRedirectProgram.js', libPath: 'frqtl/objects/objects/directedRedirectProgram.js',
85
+ why: 'imported by Tunnel — a forked copy imports it', tier: 'part-internal' }),
86
+ Object.freeze({ specifier: '@descix/frqtl-web/compat/renderer/lo-drawn-center.js', libPath: 'frqtl/compat/renderer/lo-drawn-center.js',
87
+ why: 'imported by Tunnel — a forked copy imports it', tier: 'part-internal' }),
88
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/setup/setupTunnelPointExit.js', libPath: 'frqtl/objects/setup/setupTunnelPointExit.js',
89
+ why: 'imported by Tunnel — a forked copy imports it', tier: 'part-internal' }),
90
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/constants/WallBehaviors.js', libPath: 'frqtl/objects/constants/WallBehaviors.js',
91
+ why: 'imported by Wall — a forked copy imports it', tier: 'part-internal' }),
92
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/objects/absorbRadiation.js', libPath: 'frqtl/objects/objects/absorbRadiation.js',
93
+ why: 'imported by Wall — a forked copy imports it', tier: 'part-internal' }),
94
+ Object.freeze({ specifier: '@descix/frqtl-web/objects/geometry/SlabFace.js', libPath: 'frqtl/objects/geometry/SlabFace.js',
95
+ why: 'imported by Wall — a forked copy imports it', tier: 'part-internal' })
49
96
  ]);
50
97
 
51
98
  /** The scene doors a host page imports — the package's `./boot` export; never a sketch specifier. */
@@ -56,9 +103,11 @@ export const FRQTL_WEB_BOOT = Object.freeze({ specifier: '@descix/frqtl-web/boot
56
103
  export const FRQTL_WEB_HOST_SPECIFIERS = Object.freeze([
57
104
  FRQTL_WEB_BOOT,
58
105
  Object.freeze({ specifier: '@descix/frqtl-web/registry', libPath: 'shim/registry/index.js',
59
- why: 'the walk-through rungs, the showcase roster, the experiment registry, loadSceneSource' }),
106
+ why: 'the walk-through rungs, the showcase roster, the scene list (sceneGroups), projectOf' }),
60
107
  Object.freeze({ specifier: '@descix/frqtl-web/project', libPath: 'shim/project/index.js',
61
- why: 'the specifier table, the link site, sketchProject and the project identity' })
108
+ why: 'the specifier table, the link site, sketchProject and the project identity' }),
109
+ Object.freeze({ specifier: '@descix/frqtl-web/host-view', libPath: 'shim/host/index.js',
110
+ why: 'the DeSciX host-view declarer: a page declares its own view' })
62
111
  ]);
63
112
 
64
113
  /** The package a self-reference names. */
@@ -92,6 +141,46 @@ export function libPathOf(specifier) {
92
141
  '. (FAIL-LOUD.)');
93
142
  }
94
143
 
144
+ /** The row naming `specifier` (libPathOf's rule: an exact row first, else a prefix row with a file under it), or null. */
145
+ function _rowOf(specifier) {
146
+ for (const row of FRQTL_WEB_SPECIFIERS) {
147
+ if (!_isPrefix(row) && row.specifier === specifier) { return row; }
148
+ }
149
+ for (const row of FRQTL_WEB_SPECIFIERS) {
150
+ if (_isPrefix(row) && specifier.startsWith(row.specifier) && specifier.length > row.specifier.length) { return row; }
151
+ }
152
+ return null;
153
+ }
154
+
155
+ /**
156
+ * @param {string} specifier — a `@descix/frqtl-web/...` specifier
157
+ * @returns {string} its row's tier (a key of SPECIFIER_TIERS); THROWS on a specifier no row names
158
+ */
159
+ export function tierOf(specifier) {
160
+ libPathOf(specifier); // the one validation (and its refusal text)
161
+ return _rowOf(specifier).tier;
162
+ }
163
+
164
+ /**
165
+ * @param {string} libPath — a lib-identity path (e.g. 'frqtl/parts/Wall.js', 'frqtl/objects/schema/index.js')
166
+ * @returns {string|null} the specifier whose row names that file, or null when no row does
167
+ */
168
+ export function specifierOfLibPath(libPath) {
169
+ if (typeof libPath !== 'string' || !libPath) {
170
+ throw new TypeError('[specifiers] specifierOfLibPath(libPath): a lib-identity path (got ' + JSON.stringify(libPath) + '). (FAIL-LOUD.)');
171
+ }
172
+ for (const row of FRQTL_WEB_SPECIFIERS) {
173
+ if (!_isPrefix(row) && row.libPath === libPath) { return row.specifier; }
174
+ }
175
+ for (const row of FRQTL_WEB_SPECIFIERS) {
176
+ if (_isPrefix(row) && libPath.startsWith(row.libPath)) {
177
+ const rest = libPath.slice(row.libPath.length);
178
+ if (rest && rest.indexOf('/') === -1) { return row.specifier + rest; }
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+
95
184
  /** An import map resolves an address only if it is an absolute URL or starts with '/', './' or '../'. */
96
185
  function _isMapAddress(url) {
97
186
  if (/^(\/|\.\/|\.\.\/)/.test(url)) { return true; }
@@ -8,9 +8,9 @@
8
8
  * EXHIBITS is the ONE source for the front-page tiles, their GYM deep links and the scene card:
9
9
  * nine rows in front-page order, each
10
10
  * `{ kind, id, mode, title, tile, what, why, how, controls, still }`.
11
- * - `kind` is 'rung' (a walk-through rung, lib/shim/registry/walkthrough.js) or 'experiment'
12
- * (a canonical experiment page, lib/shim/registry/experiments.js); `id` resolves there.
13
- * - `mode` is a rung's declared mode (null for a single-mode rung and for an experiment).
11
+ * - `kind` is 'rung' (a walk-through rung, lib/shim/registry/walkthrough.js); `id` resolves there. A row of
12
+ * any other kind THROWS naming the rung form.
13
+ * - `mode` is a rung's declared mode (null for a single-mode rung).
14
14
  * - `title` is the tile's and the card's title; `tile` is the published one-sentence tile line.
15
15
  * - `what` (What you are seeing), `why` (Why it is impossible the usual way) and `how` (How FRQTL
16
16
  * gets it) are the card's copy, CARD_PARTS, every one REQUIRED on every row.
@@ -47,14 +47,13 @@
47
47
  * @module exhibits
48
48
  */
49
49
 
50
- import { EXPERIMENTS, experimentById, experimentIs3D } from './experiments.js';
51
50
  import {
52
51
  WALKTHROUGH, walkthroughById, walkthroughModes, defaultWalkthroughModeId, walkthroughIs3D, effectiveRenderState
53
52
  } from './walkthrough.js';
54
53
  import { assertCopyText, assertCopyParagraphs } from './copy-gate.js';
55
54
 
56
55
  /** The scene kinds a roster row (and a player deep link) can name. */
57
- export const EXHIBIT_KINDS = Object.freeze(['rung', 'experiment']);
56
+ export const EXHIBIT_KINDS = Object.freeze(['rung']);
58
57
 
59
58
  /** The copy parts a run-once CARD shows, in order; every roster row carries all of them. */
60
59
  export const CARD_PARTS = Object.freeze(['what', 'why', 'how']);
@@ -287,15 +286,6 @@ function rungOrThrow(id) {
287
286
  return rung;
288
287
  }
289
288
 
290
- function experimentOrThrow(id) {
291
- const entry = experimentById(id);
292
- if (!entry) {
293
- throw new Error('[exhibits] unknown experiment "' + id + '" — declared experiments: [' +
294
- EXPERIMENTS.map((e) => e.id).join(', ') + ']. Add it to lib/shim/registry/experiments.js first.');
295
- }
296
- return entry;
297
- }
298
-
299
289
  /** A rung's effective mode: the declared one (validated), or null for a single-mode rung. */
300
290
  function rungMode(rung, modeId) {
301
291
  const eff = (modeId !== undefined && modeId !== null) ? modeId : defaultWalkthroughModeId(rung);
@@ -322,8 +312,8 @@ function assertNotPasted(part, where) {
322
312
  }
323
313
 
324
314
  /**
325
- * assertRoster — the load-time validator: every row's kind and id resolve in their registry, a
326
- * rung's mode is declared, an experiment carries none, no scene repeats, no unknown field, every
315
+ * assertRoster — the load-time validator: every row is a rung whose id resolves in the walk-through
316
+ * registry, its mode is declared, no scene repeats, no unknown field, every
327
317
  * row has a title, a tile line and every CARD_PARTS part, `controls` is non-empty when present, all
328
318
  * copy passes the copy gate, and no part pastes the invitation or an intro paragraph. Exported so a
329
319
  * test can show each refusal on a planted roster. @returns the normalized, frozen rows (with `order`).
@@ -337,17 +327,10 @@ export function assertRoster(rows) {
337
327
  }
338
328
  });
339
329
  if (EXHIBIT_KINDS.indexOf(row.kind) === -1) {
340
- throw new Error('[exhibits] row ' + order + ' kind ' + JSON.stringify(row.kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
341
- }
342
- let mode = null;
343
- if (row.kind === 'rung') {
344
- mode = rungMode(rungOrThrow(row.id), row.mode);
345
- } else {
346
- experimentOrThrow(row.id);
347
- if (row.mode !== undefined) {
348
- throw new Error('[exhibits] experiment row "' + row.id + '" names a mode — only a rung has modes.');
349
- }
330
+ throw new Error('[exhibits] row ' + order + ' kind ' + JSON.stringify(row.kind) + ' — an exhibit is a walk-through rung: ' +
331
+ '{ kind: \'rung\', id: <a lib/shim/registry/walkthrough.js id>, mode? } (EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + ').');
350
332
  }
333
+ const mode = rungMode(rungOrThrow(row.id), row.mode);
351
334
  if (row.title === undefined || row.title === null) {
352
335
  throw new Error('[exhibits] row "' + row.id + '" has no title — every card is titled.');
353
336
  }
@@ -380,15 +363,14 @@ export function exhibitsOfKind(kind) {
380
363
  * The roster row for a scene, or null when the scene is not on the roster. A null is a DECLARED
381
364
  * outcome the caller records (the player plays such a scene with no card: 'not-offered-no-roster-row').
382
365
  * The scene itself must exist: an unknown kind, id or mode throws.
383
- * @param {{kind: 'rung'|'experiment', id: string, modeId?: string|null}} scene
366
+ * @param {{kind: 'rung', id: string, modeId?: string|null}} scene
384
367
  */
385
368
  export function exhibitFor(scene) {
386
369
  const kind = scene && scene.kind;
387
370
  if (EXHIBIT_KINDS.indexOf(kind) === -1) {
388
371
  throw new Error('[exhibits] unknown scene kind ' + JSON.stringify(kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
389
372
  }
390
- let mode = null;
391
- if (kind === 'rung') { mode = rungMode(rungOrThrow(scene.id), scene.modeId); } else { experimentOrThrow(scene.id); }
373
+ const mode = rungMode(rungOrThrow(scene.id), scene.modeId);
392
374
  for (let i = 0; i < EXHIBITS.length; i++) {
393
375
  const e = EXHIBITS[i];
394
376
  if (e.kind === kind && e.id === scene.id && e.mode === mode) { return e; }
@@ -414,21 +396,20 @@ export const JOYSTICK_EXPLAINER = Object.freeze([
414
396
  * sceneKey — the run-once seen-key of a scene: kind, id and, for a multi-mode rung, its EFFECTIVE
415
397
  * mode (the one normalizer, rungMode); never a bare id, so it cannot collide with a tab key.
416
398
  * Throws for an unknown kind, id or mode.
417
- * @param {{kind: 'rung'|'experiment', id: string, modeId?: string|null}} scene
399
+ * @param {{kind: 'rung', id: string, modeId?: string|null}} scene
418
400
  */
419
401
  export function sceneKey(scene) {
420
402
  const kind = scene && scene.kind;
421
403
  if (EXHIBIT_KINDS.indexOf(kind) === -1) {
422
404
  throw new Error('[exhibits] unknown scene kind ' + JSON.stringify(kind) + ' — EXHIBIT_KINDS: ' + EXHIBIT_KINDS.join(', ') + '.');
423
405
  }
424
- let mode = null;
425
- if (kind === 'rung') { mode = rungMode(rungOrThrow(scene.id), scene.modeId); } else { experimentOrThrow(scene.id); }
406
+ const mode = rungMode(rungOrThrow(scene.id), scene.modeId);
426
407
  return 'scene-' + kind + '-' + scene.id + (mode ? '-' + mode : '');
427
408
  }
428
409
 
429
410
  /**
430
411
  * The run-once card of a ROSTER row: title, then `what`, `why`, `how`, `controls` when present, the
431
- * JOYSTICK_EXPLAINER on a 3D scene (3D read from walkthroughIs3D / experimentIs3D), and
412
+ * JOYSTICK_EXPLAINER on a 3D scene (3D read from walkthroughIs3D), and
432
413
  * SHOWCASE_INVITE last, by reference. Composed from the row and the showcase constants only: no
433
414
  * registry prose ever. The bag's { seenKey, title, paragraphs } is what lib/components/run-once-card
434
415
  * renders.
@@ -440,7 +421,7 @@ export function showcaseCard(row) {
440
421
  throw new Error('[exhibits] showcaseCard takes a roster row (an EXHIBITS element, e.g. exhibitFor(scene)); got ' +
441
422
  JSON.stringify(row && { kind: row.kind, id: row.id }) + '.');
442
423
  }
443
- const is3D = row.kind === 'rung' ? walkthroughIs3D(row.id, row.mode) : experimentIs3D(row.id);
424
+ const is3D = walkthroughIs3D(row.id, row.mode);
444
425
  let paragraphs = CARD_PARTS.map((p) => Object.freeze([row[p]]));
445
426
  if (row.controls !== null) { paragraphs = paragraphs.concat([Object.freeze([row.controls])]); }
446
427
  if (is3D) { paragraphs = paragraphs.concat(JOYSTICK_EXPLAINER); }
@@ -9,11 +9,11 @@
9
9
  *
10
10
  * walkthrough.js the walk-through rungs: walkthroughById, effectiveRenderState, modes
11
11
  * exhibits.js the showcase roster
12
- * experiments.js the experiment registry and its tiers
13
- * scene-source.js loadSceneSource(id) — a rung's sketch text (fetch in a page, file read in node)
12
+ * scene-roster.js sceneGroups() — THE scene list both views show (the showcase, then the walk-through groups)
13
+ * scene-source.js projectOf(id, { mode }) — a rung as a project folder + its renderState (fetch in a page, file read in node)
14
14
  */
15
15
 
16
16
  export * from './walkthrough.js';
17
17
  export * from './exhibits.js';
18
- export * from './experiments.js';
18
+ export * from './scene-roster.js';
19
19
  export * from './scene-source.js';
@@ -0,0 +1,35 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * scene-roster.js — THE scene list both views show (the GYM's select and STUDIO's dropdown): one owner, read by
5
+ * both, so the two lists are the same rows in the same order.
6
+ *
7
+ * SHOWCASE_GROUP the label of the showcase group ('Impossible things')
8
+ * sceneGroups() → [{ label, rows: [{ id, title }] }] — the showcase roster first (EXHIBITS, in tile order,
9
+ * each row its exhibit title), then every walk-through rung in WALKTHROUGH order under its
10
+ * lab group (walkthroughGroup), with NO rung removed from its own group: a showcase rung
11
+ * appears twice, and both rows name the same rung (a deep link selects it either way).
12
+ *
13
+ * A row is a scene id a host opens by ?walkthrough=<id>. A host keeps its own non-scene rows (STUDIO's blank world
14
+ * and saved projects) around these groups; the GYM's default scene is the first row.
15
+ */
16
+
17
+ import { EXHIBITS } from './exhibits.js';
18
+ import { WALKTHROUGH, walkthroughGroup } from './walkthrough.js';
19
+
20
+ /** The showcase group's label. */
21
+ export const SHOWCASE_GROUP = 'Impossible things';
22
+
23
+ /**
24
+ * @returns {Array<{label: string, rows: Array<{id: string, title: string}>}>} a new array on every call
25
+ */
26
+ export function sceneGroups() {
27
+ var groups = [{ label: SHOWCASE_GROUP, rows: EXHIBITS.map(function (e) { return { id: e.id, title: e.title }; }) }];
28
+ WALKTHROUGH.forEach(function (r) {
29
+ var g = walkthroughGroup(r);
30
+ var last = groups[groups.length - 1];
31
+ if (groups.length === 1 || last.label !== g) { groups.push({ label: g, rows: [] }); last = groups[groups.length - 1]; }
32
+ last.rows.push({ id: r.id, title: r.title });
33
+ });
34
+ return groups;
35
+ }
@@ -1,38 +1,47 @@
1
1
  /* Copyright 2023-2026 by Essam Abadir */
2
2
 
3
3
  /**
4
- * scene-source.js — the ONE reader of a scene's sketch file, in a page AND in node.
4
+ * scene-source.js — the ONE reader of a registry scene as a PROJECT, in a page AND in node.
5
5
  *
6
- * A scene is ONE file, `lib/frqtl/scenes/<file>.js`: a dual-mode `buildExperiment(exp)`
7
- * module holding the whole scene (world, parts, render intent). A registry row carries
8
- * its frqtl-relative PATH (`source: 'scenes/<file>.js'`), never its text. Every consumer
9
- * that needs the text (the GYM's rung frame, STUDIO, the hero, a package consumer of
10
- * `@descix/frqtl-web/registry`) asks here, so the id → path → URL → text chain exists once.
6
+ * A scene is ONE sketch file, `lib/frqtl/scenes/<file>.js`: a module whose default export is
7
+ * `buildExperiment(exp)`, holding the whole scene (world, parts, render intent). A registry row carries
8
+ * its frqtl-relative PATH (`source: 'scenes/<file>.js'`), never its text. Every consumer that needs the
9
+ * scene (the GYM's rung frame, STUDIO, the hero, a package consumer of `@descix/frqtl-web/registry`) asks
10
+ * here, so the id → path → URL → project chain exists once.
11
11
  *
12
- * scenePath(rung) → 'scenes/<file>.js' (THROWS on any other shape)
13
- * loadSceneSource(scene, opts?) → Promise<string> (the file's text)
12
+ * scenePath(rung) → 'scenes/<file>.js' (THROWS on any other shape)
13
+ * projectOf(scene, { router?, fetch?, mode? })
14
+ * → Promise<{ manifest, files, renderState }>
15
+ * files { 'frqtl.json': serializeManifest(manifestFor({ name: row.id, entry: 'sketch.js' })),
16
+ * 'sketch.js': <the scene file's text> } — the folder a host boots
17
+ * (bootProject(mount, { files, mint }, { renderState }), no entry needed)
18
+ * manifest that frqtl.json's normalized bag
19
+ * renderState effectiveRenderState(row, mode) — the registry's one derivation, ferried BESIDE the folder
20
+ * (boot facts never enter frqtl.json or the project identity)
14
21
  *
15
- * `scene` is a rung id (resolved by `walkthroughById`; an unknown id REJECTS naming the
16
- * declared ids) or a row (`{id, source}`, e.g. `EMPTY_SCENE`). The URL comes from a router,
17
- * the one path authority (lib/shim/engine/router.js, `scenes/` profile key):
22
+ * `scene` is a rung id (resolved by `walkthroughById`; an unknown id REJECTS naming the declared ids) or a
23
+ * row (`{id, source}`, e.g. `EMPTY_SCENE`). The URL comes from a router, the one path authority
24
+ * (lib/shim/engine/router.js, `scenes/` profile key):
18
25
  * - `opts.router` — the host's own (the IDE's engineRouter); a host always passes it.
19
- * - absent — the PACKAGE's: the dist layout anchored at the dist root this module ships
20
- * in (`PACKAGE_ROOT_FROM_HERE`, dist/shim/registry/ → dist/). This is the
21
- * `@descix/frqtl-web/registry` door's reader; in the source tree it names a file that
22
- * is not there and rejects.
23
- * The read is a CAPABILITY split on the resolved URL, never a host sniff: a `file:` URL is
24
- * read through node's fs (`process.getBuiltinModule`), anything else is fetched.
26
+ * - absent — the PACKAGE's: the dist layout anchored at the dist root this module ships in
27
+ * (`PACKAGE_ROOT_FROM_HERE`, dist/shim/registry/ → dist/). This is the `@descix/frqtl-web/registry`
28
+ * door's reader; in the source tree it names a file that is not there and rejects.
29
+ * The read is lib/shim/project/read-text.js readText (a capability split on the resolved URL: a `file:` URL
30
+ * through node's fs, anything else fetched).
25
31
  *
26
- * FAIL-LOUD: an unknown id, a row without a scenes/ path, a non-200 response, an unreadable
27
- * file or an empty body REJECTS naming the scene and the URL. A blank scene is never a result.
32
+ * FAIL-LOUD: an unknown id, a row without a scenes/ path, an undeclared mode, a non-200 response, an
33
+ * unreadable file or an empty body REJECTS naming the scene and the URL. A blank scene is never a result.
28
34
  *
29
35
  * Zero physics: this moves text. The scene's physics is whatever its file composes.
30
36
  */
31
37
 
32
38
  'use strict';
33
39
 
34
- import { WALKTHROUGH, walkthroughById } from './walkthrough.js';
40
+ import { WALKTHROUGH, walkthroughById, effectiveRenderState } from './walkthrough.js';
35
41
  import { engineRouter } from '../engine/router.js';
42
+ import { MANIFEST_FILE, manifestFor, serializeManifest } from '../project/manifest.js';
43
+ import { SKETCH_ENTRY } from '../project/link.js';
44
+ import { readText } from '../project/read-text.js';
36
45
 
37
46
  /** The one path prefix a scene file lives under (the router's `scenes/` profile key). */
38
47
  export var SCENES_PREFIX = 'scenes/';
@@ -47,11 +56,11 @@ var PACKAGE_ROOT_FROM_HERE = '../../';
47
56
 
48
57
  /**
49
58
  * The blank world — STUDIO's "Empty Universe" — is itself a scene file (the empty page states no
50
- * world of its own). A row-shaped handle so it reads through the same `loadSceneSource`.
59
+ * world of its own). A row-shaped handle so it reads through the same `projectOf`.
51
60
  */
52
61
  export var EMPTY_SCENE = Object.freeze({ id: 'empty', source: 'scenes/empty.js' });
53
62
 
54
- var OPT_KEYS = ['router', 'fetch'];
63
+ var OPT_KEYS = ['router', 'fetch', 'mode'];
55
64
 
56
65
  /**
57
66
  * @param {{id?:string, source?:string}} rung — a registry row.
@@ -77,7 +86,7 @@ function _rowOf(scene) {
77
86
  return rung;
78
87
  }
79
88
  if (!scene || typeof scene !== 'object') {
80
- throw new Error('[scene-source] loadSceneSource takes a rung id or a registry row, got ' +
89
+ throw new Error('[scene-source] projectOf takes a rung id or a registry row, got ' +
81
90
  JSON.stringify(scene) + '. (FAIL-LOUD.)');
82
91
  }
83
92
  return scene;
@@ -88,70 +97,45 @@ function _packageRouter() {
88
97
  return engineRouter({ mode: 'cdn', base: root });
89
98
  }
90
99
 
91
- function _readFileUrl(url, id) {
92
- var proc = globalThis.process;
93
- var getBuiltin = proc && proc.getBuiltinModule;
94
- if (typeof getBuiltin !== 'function') {
95
- throw new Error('[scene-source] scene "' + id + '" resolves to ' + url + ', a file: URL; reading ' +
96
- 'one needs node\'s process.getBuiltinModule (Node 20.16+ / 22.3+). In a page, serve the ' +
97
- 'package over http. (FAIL-LOUD.)');
98
- }
99
- var fs = getBuiltin('node:fs');
100
- var fileURLToPath = getBuiltin('node:url').fileURLToPath;
101
- return fs.promises.readFile(fileURLToPath(url), 'utf8').catch(function (e) {
102
- throw new Error('[scene-source] scene "' + id + '" — reading ' + url + ' failed: ' +
103
- (e && e.message) + '. (FAIL-LOUD.)');
104
- });
105
- }
106
-
107
- function _fetchText(url, id, fetchImpl) {
108
- var f = fetchImpl || (typeof fetch === 'function' ? fetch : null);
109
- if (!f) {
110
- throw new Error('[scene-source] no fetch available to load ' + url + '.');
111
- }
112
- return Promise.resolve(f(url)).then(function (res) {
113
- if (!res || !res.ok) {
114
- throw new Error('[scene-source] scene "' + id + '" — GET ' + url + ' answered ' +
115
- (res ? res.status : 'nothing') + '. (FAIL-LOUD.)');
116
- }
117
- return res.text();
118
- });
119
- }
120
-
121
100
  /**
122
- * Read a scene file's text through a router's path authority.
101
+ * A registry scene as the project folder a host boots, with the host's boot bag beside it.
123
102
  * @param {string|{id:string, source:string}} scene — a rung id, or a registry row.
124
- * @param {{router?:{resolve:function(string):string}, fetch?:function}} [opts]
103
+ * @param {{router?:{resolve:function(string):string}, fetch?:function, mode?:string|null}} [opts]
125
104
  * router — the host's engineRouter (absent: the package's own dist root);
126
- * fetch — the fetch used for a non-file URL (defaults to the global; injectable for tests).
127
- * @returns {Promise<string>}
105
+ * fetch — the fetch used for a non-file URL (defaults to the global; injectable for tests);
106
+ * mode — a declared walk-through mode (absent: the rung's default) for the renderState.
107
+ * @returns {Promise<{manifest: object, files: Object<string,string>, renderState: object}>}
128
108
  */
129
- export function loadSceneSource(scene, opts) {
109
+ export function projectOf(scene, opts) {
130
110
  return Promise.resolve().then(function () {
131
111
  opts = opts || {};
132
112
  Object.keys(opts).forEach(function (k) {
133
113
  if (OPT_KEYS.indexOf(k) === -1) {
134
- throw new Error('[scene-source] loadSceneSource has no option "' + k + '" — options: ' +
114
+ throw new Error('[scene-source] projectOf has no option "' + k + '" — options: ' +
135
115
  OPT_KEYS.join(', ') + '. (FAIL-LOUD.)');
136
116
  }
137
117
  });
138
118
  var row = _rowOf(scene);
139
119
  var rel = scenePath(row);
120
+ var renderState = effectiveRenderState(row, opts.mode); // an undeclared mode rejects before the read
140
121
  var router = opts.router || _packageRouter();
141
122
  if (typeof router.resolve !== 'function') {
142
123
  throw new Error('[scene-source] opts.router has no resolve() — pass an engineRouter ' +
143
124
  '(lib/shim/engine/router.js) to resolve ' + rel + '. (FAIL-LOUD.)');
144
125
  }
145
126
  var url = router.resolve(rel);
146
- var text = url.indexOf('file:') === 0
147
- ? _readFileUrl(url, row.id)
148
- : _fetchText(url, row.id, opts.fetch);
149
- return Promise.resolve(text).then(function (t) {
127
+ return readText(url, { fetch: opts.fetch }).catch(function (e) {
128
+ throw new Error('[scene-source] scene "' + row.id + '" — ' + ((e && e.message) || e) + '. (FAIL-LOUD.)');
129
+ }).then(function (t) {
150
130
  if (typeof t !== 'string' || !t.trim()) {
151
131
  throw new Error('[scene-source] scene "' + row.id + '" — ' + url +
152
132
  ' has an empty body. (FAIL-LOUD.)');
153
133
  }
154
- return t;
134
+ var manifest = manifestFor({ name: row.id, entry: SKETCH_ENTRY });
135
+ var files = {};
136
+ files[MANIFEST_FILE] = serializeManifest(manifest);
137
+ files[SKETCH_ENTRY] = t;
138
+ return { manifest: manifest, files: files, renderState: renderState };
155
139
  });
156
140
  });
157
141
  }
@@ -11,9 +11,10 @@
11
11
  *
12
12
  * A rung row carries the PATH of its scene, never its text: each scene is ONE sketch
13
13
  * file, `lib/frqtl/scenes/<file>.js` — a dual-mode `buildExperiment(exp)` module that
14
- * holds the whole scene. Its text is read through `loadSceneSource(id | rung, opts)`
15
- * (./scene-source.js: a fetch in a page, a file read in node; FAIL-LOUD on an unknown id,
16
- * a non-200 or an empty body). Rungs that share one physics name the SAME file.
14
+ * holds the whole scene. A host reads it as a project through `projectOf(id | rung, opts)`
15
+ * (./scene-source.js: frqtl.json + sketch.js, with effectiveRenderState beside it; a fetch in a
16
+ * page, a file read in node; FAIL-LOUD on an unknown id, a non-200 or an empty body). Rungs
17
+ * that share one physics name the SAME file.
17
18
  *
18
19
  * ── Rung descriptor ──────────────────────────────────────────────────────────
19
20
  * id : stable dropdown id (the `walkthrough:<id>` selection value)
@@ -773,8 +774,7 @@ export function walkthroughById(id) {
773
774
  });
774
775
 
775
776
  // ── is3D normalization (E-W0a; mode-aware S3/W2) ─────────────────────────────
776
- // STUDIO must read ONE `is3D` field for a selection regardless of whether it came
777
- // from the gallery registry (experiments.js `is3D`) or a walkthrough rung. A rung's
777
+ // STUDIO reads ONE `is3D` field for a selection. A rung's
778
778
  // 3D-ness is ALREADY declared by its (possibly mode-merged) `renderState.renderer` —
779
779
  // three IS the 3D renderer, the ONE display fact the load path threads — so `is3D` here is
780
780
  // DERIVED through `effectiveRenderState` for the rung's DEFAULT mode, never a
@@ -786,7 +786,7 @@ WALKTHROUGH.forEach(function (rung) {
786
786
 
787
787
  /**
788
788
  * walkthroughIs3D(id, modeId) — the ONE mount-time 3D predicate for a walkthrough
789
- * rung, the peer of experiments.js `experimentIs3D`. Derived through
789
+ * rung. Derived through
790
790
  * `effectiveRenderState` (the single derivation point) for the given mode, or the
791
791
  * rung's default mode when `modeId` is omitted. Returns false for an unknown id;
792
792
  * THROWS (via effectiveRenderState) for a known id with an undeclared mode.