@descix/frqtl-node 0.6.1-p3.3 → 0.7.0-p0

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 (64) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +4 -3
  3. package/dist/dist-manifest.json +26 -56
  4. package/dist/frqtl/experiments/_shared/empty-sketch.js +17 -0
  5. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +6 -6
  6. package/dist/frqtl/objects/setup/setupFxSmoke.js +2 -2
  7. package/dist/frqtl/objects/setup/setupG3Collide.js +1 -1
  8. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +2 -2
  9. package/dist/frqtl/objects/v1-replacements.js +1 -1
  10. package/dist/frqtl/parts/OvenBox.js +2 -2
  11. package/dist/frqtl/parts/SphereEmitter.js +1 -1
  12. package/dist/shim/engine/router.js +18 -6
  13. package/dist/shim/engine/site-assemble.mjs +268 -0
  14. package/dist/shim/engine/site-graph.js +42 -14
  15. package/dist/shim/host/device.js +1 -1
  16. package/dist/shim/index.js +3 -7
  17. package/dist/shim/notebook/catalog.js +2 -2
  18. package/dist/shim/notebook/format.js +1 -1
  19. package/dist/shim/notebook/knob.js +1 -1
  20. package/dist/shim/notebook/notebook-knobs.js +1 -1
  21. package/dist/shim/project/specifiers.js +8 -8
  22. package/dist/shim/registry/copy-gate.js +3 -3
  23. package/dist/shim/registry/index.js +9 -12
  24. package/dist/shim/registry/roster-link.js +142 -0
  25. package/dist/shim/registry/roster.js +595 -0
  26. package/dist/shim/registry/scene-source.js +80 -105
  27. package/dist/shim/registry/studio-landing.js +15 -7
  28. package/dist/shim/render/sun-look.js +1 -2
  29. package/llms.txt +4 -2
  30. package/package.json +1 -1
  31. package/dist/frqtl/scenes/absorbing-wall-3d.js +0 -37
  32. package/dist/frqtl/scenes/absorbing-wall.js +0 -36
  33. package/dist/frqtl/scenes/atomic-model.js +0 -91
  34. package/dist/frqtl/scenes/big-bang.js +0 -49
  35. package/dist/frqtl/scenes/blackbody.js +0 -84
  36. package/dist/frqtl/scenes/circuit-sat.js +0 -131
  37. package/dist/frqtl/scenes/collision-matrix-3d.js +0 -110
  38. package/dist/frqtl/scenes/counting-wall-3d.js +0 -40
  39. package/dist/frqtl/scenes/counting-wall.js +0 -42
  40. package/dist/frqtl/scenes/double-slit.js +0 -34
  41. package/dist/frqtl/scenes/dse-one-emitter.js +0 -200
  42. package/dist/frqtl/scenes/dse-one-slit.js +0 -118
  43. package/dist/frqtl/scenes/dse-photoelectric.js +0 -74
  44. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +0 -279
  45. package/dist/frqtl/scenes/dse-two-emitters.js +0 -201
  46. package/dist/frqtl/scenes/dse-two-slits.js +0 -136
  47. package/dist/frqtl/scenes/empty.js +0 -17
  48. package/dist/frqtl/scenes/laser-buildup.js +0 -56
  49. package/dist/frqtl/scenes/mirror-3d.js +0 -60
  50. package/dist/frqtl/scenes/mirror-route.js +0 -164
  51. package/dist/frqtl/scenes/mirror.js +0 -42
  52. package/dist/frqtl/scenes/omni-3d.js +0 -30
  53. package/dist/frqtl/scenes/omni-source.js +0 -25
  54. package/dist/frqtl/scenes/self-interference.js +0 -42
  55. package/dist/frqtl/scenes/smoking-gun.js +0 -126
  56. package/dist/frqtl/scenes/straight-walk.js +0 -25
  57. package/dist/frqtl/scenes/sun.js +0 -42
  58. package/dist/frqtl/scenes/two-source.js +0 -42
  59. package/dist/frqtl/scenes/wave.js +0 -29
  60. package/dist/shim/engine/artifact-shell.js +0 -211
  61. package/dist/shim/registry/exhibits.js +0 -436
  62. package/dist/shim/registry/scene-roster.js +0 -35
  63. package/dist/shim/registry/showcase-link.js +0 -61
  64. package/dist/shim/registry/walkthrough.js +0 -752
@@ -1,141 +1,116 @@
1
1
  /* Copyright 2023-2026 by Essam Abadir */
2
2
 
3
3
  /**
4
- * scene-source.js — the ONE reader of a registry scene as a PROJECT, in a page AND in node.
4
+ * scene-source.js — the ONE reader of a scene as a PROJECT, in a page AND in node.
5
5
  *
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.
6
+ * A scene is ONE sketch file: a module whose default export is `buildExperiment(exp)`, holding the whole scene
7
+ * (world, parts, render intent). A host's roster names each scene's file relative to the roster
8
+ * (roster.js::sceneUrlOf); the Empty Universe is the package's own sketch
9
+ * (lib/frqtl/experiments/_shared/empty-sketch.js), read through the router like any package file.
11
10
  *
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' })),
11
+ * projectOf(roster, id, { fetch?, mode? }) → Promise<{ manifest, files, renderState }>
12
+ * files { 'frqtl.json': serializeManifest(manifestFor({ name: id, entry: 'sketch.js' })),
16
13
  * 'sketch.js': <the scene file's text> } — the folder a host boots
17
- * (bootProject(mount, { files, mint }, { renderState }), no entry needed)
18
14
  * 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)
15
+ * renderState effectiveRenderState(row, mode) — boot facts ride BESIDE the folder, never in frqtl.json
16
+ * emptyProjectOf({ router, fetch? }) → the same shape for the Empty Universe (name EMPTY_PROJECT_NAME,
17
+ * renderState normalizeSavedRenderState({}))
18
+ * EMPTY_PROJECT_NAME 'empty'
19
+ * EMPTY_SKETCH_PATH the Empty Universe's router path (a package builder places that file
20
+ * in every dist this module ships in: sdk/_build/hosts.mjs)
21
21
  *
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):
25
- * - `opts.router` — the host's own (the IDE's engineRouter); a host always passes it.
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).
22
+ * The read is lib/shim/project/read-text.js readText (a `file:` URL through node's fs, anything else fetched).
31
23
  *
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.
24
+ * FAIL-LOUD: a non-bag roster, an undeclared id or mode, an unknown option, a non-200, an unreadable file or an
25
+ * empty body REJECTS naming the scene and the URL. A blank scene is never a result.
34
26
  *
35
27
  * Zero physics: this moves text. The scene's physics is whatever its file composes.
36
28
  */
37
29
 
38
30
  'use strict';
39
31
 
40
- import { WALKTHROUGH, walkthroughById, effectiveRenderState } from './walkthrough.js';
41
- import { engineRouter } from '../engine/router.js';
32
+ import { assertRosterBag, walkthroughById, effectiveRenderState, sceneUrlOf } from './roster.js';
33
+ import { normalizeSavedRenderState } from '../render-state/index.js';
42
34
  import { MANIFEST_FILE, manifestFor, serializeManifest } from '../project/manifest.js';
43
35
  import { SKETCH_ENTRY } from '../project/link.js';
44
36
  import { readText } from '../project/read-text.js';
45
37
 
46
- /** The one path prefix a scene file lives under (the router's `scenes/` profile key). */
47
- export var SCENES_PREFIX = 'scenes/';
38
+ /** The project name of the Empty Universe (its frqtl.json `name`). */
39
+ export const EMPTY_PROJECT_NAME = 'empty';
48
40
 
49
- /**
50
- * Where the package's dist root sits relative to this module once shipped: build.js places
51
- * lib/shim/ at dist/shim/ (sdk/_build/lib-dist-placement.cjs), so this file is
52
- * dist/shim/registry/scene-source.js. verify-publish-surface reads every rung through it on the
53
- * built dist, so a moved placement fails there.
54
- */
55
- var PACKAGE_ROOT_FROM_HERE = '../../';
56
-
57
- /**
58
- * The blank world — STUDIO's "Empty Universe" — is itself a scene file (the empty page states no
59
- * world of its own). A row-shaped handle so it reads through the same `projectOf`.
60
- */
61
- export var EMPTY_SCENE = Object.freeze({ id: 'empty', source: 'scenes/empty.js' });
41
+ /** The Empty Universe's sketch, a lib-identity path every router profile resolves. */
42
+ export const EMPTY_SKETCH_PATH = 'experiments/_shared/empty-sketch.js';
62
43
 
63
- var OPT_KEYS = ['router', 'fetch', 'mode'];
44
+ const PROJECT_OF = 'projectOf(roster, id, { fetch, mode })';
45
+ const EMPTY_PROJECT_OF = 'emptyProjectOf({ router, fetch })';
64
46
 
65
- /**
66
- * @param {{id?:string, source?:string}} rung — a registry row.
67
- * @returns {string} the frqtl-relative scene path.
68
- */
69
- export function scenePath(rung) {
70
- var src = rung && rung.source;
71
- if (typeof src !== 'string' || src.indexOf(SCENES_PREFIX) !== 0 || !/\.js$/.test(src)) {
72
- throw new Error('[scene-source] rung "' + (rung && rung.id) + '" declares source ' +
73
- JSON.stringify(src) + ' — a scene row carries the path of its sketch file, ' +
74
- "'scenes/<file>.js' (lib/frqtl/scenes/). (FAIL-LOUD.)");
47
+ function assertOpts(opts, allowed, signature) {
48
+ const o = opts === undefined ? {} : opts;
49
+ if (o === null || typeof o !== 'object' || Array.isArray(o)) {
50
+ throw new Error('[scene-source] ' + signature + ': options are an object (got ' + JSON.stringify(opts) + '). (FAIL-LOUD.)');
75
51
  }
76
- return src;
52
+ Object.keys(o).forEach((k) => {
53
+ if (allowed.indexOf(k) === -1) {
54
+ throw new Error('[scene-source] ' + signature + ' has no option "' + k + '" — options: ' + allowed.join(', ') +
55
+ (k === 'router' ? '; a roster scene\'s URL is sceneUrlOf(roster, row), not a router path' : '') + '. (FAIL-LOUD.)');
56
+ }
57
+ });
58
+ return o;
77
59
  }
78
60
 
79
- function _rowOf(scene) {
80
- if (typeof scene === 'string') {
81
- var rung = walkthroughById(scene);
82
- if (!rung) {
83
- throw new Error('[scene-source] unknown rung id "' + scene + '" — declared ids: [' +
84
- WALKTHROUGH.map(function (r) { return r.id; }).join(', ') + ']. (FAIL-LOUD.)');
61
+ function folderOf(name, url, label, opts, renderState) {
62
+ return readText(url, { fetch: opts.fetch }).catch((e) => {
63
+ throw new Error('[scene-source] ' + label + ' — ' + ((e && e.message) || e) + '. (FAIL-LOUD.)');
64
+ }).then((t) => {
65
+ if (typeof t !== 'string' || !t.trim()) {
66
+ throw new Error('[scene-source] ' + label + ' — ' + url + ' has an empty body. (FAIL-LOUD.)');
85
67
  }
86
- return rung;
87
- }
88
- if (!scene || typeof scene !== 'object') {
89
- throw new Error('[scene-source] projectOf takes a rung id or a registry row, got ' +
90
- JSON.stringify(scene) + '. (FAIL-LOUD.)');
91
- }
92
- return scene;
68
+ const manifest = manifestFor({ name, entry: SKETCH_ENTRY });
69
+ const files = {};
70
+ files[MANIFEST_FILE] = serializeManifest(manifest);
71
+ files[SKETCH_ENTRY] = t;
72
+ return { manifest, files, renderState };
73
+ });
93
74
  }
94
75
 
95
- function _packageRouter() {
96
- var root = new URL(PACKAGE_ROOT_FROM_HERE, import.meta.url).href.replace(/\/$/, '');
97
- return engineRouter({ mode: 'cdn', base: root });
76
+ /**
77
+ * A roster scene as the project folder a host boots, with its renderState beside it.
78
+ * @param {object} roster a normalized roster bag (roster.js normalizeRoster / roster-link.js loadRoster)
79
+ * @param {string} id a scene id the roster declares
80
+ * @param {{fetch?: function, mode?: string|null}} [opts] fetch: injectable for a non-file URL; mode: a declared
81
+ * mode of the scene (absent: its default) for the renderState
82
+ * @returns {Promise<{manifest: object, files: Object<string,string>, renderState: object}>}
83
+ */
84
+ export function projectOf(roster, id, opts) {
85
+ return Promise.resolve().then(() => {
86
+ assertRosterBag(roster, PROJECT_OF);
87
+ const o = assertOpts(opts, ['fetch', 'mode'], PROJECT_OF);
88
+ if (typeof id !== 'string' || id === '') {
89
+ throw new Error('[scene-source] ' + PROJECT_OF + ' takes a scene id string (got ' + JSON.stringify(id) + '). (FAIL-LOUD.)');
90
+ }
91
+ const row = walkthroughById(roster, id);
92
+ if (!row) {
93
+ throw new Error('[scene-source] ' + PROJECT_OF + ': "' + id + '" is not a declared scene — declared: [' +
94
+ roster.rows.map((r) => r.id).join(', ') + ']. (FAIL-LOUD.)');
95
+ }
96
+ const renderState = effectiveRenderState(row, o.mode); // an undeclared mode rejects before the read
97
+ return folderOf(row.id, sceneUrlOf(roster, row), 'scene "' + row.id + '"', o, renderState);
98
+ });
98
99
  }
99
100
 
100
101
  /**
101
- * A registry scene as the project folder a host boots, with the host's boot bag beside it.
102
- * @param {string|{id:string, source:string}} scene — a rung id, or a registry row.
103
- * @param {{router?:{resolve:function(string):string}, fetch?:function, mode?:string|null}} [opts]
104
- * router — the host's engineRouter (absent: the package's own dist root);
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.
102
+ * The Empty Universe as a project folder: the package sketch read through the host's router.
103
+ * @param {{router: {resolve: function(string): string}, fetch?: function}} opts
107
104
  * @returns {Promise<{manifest: object, files: Object<string,string>, renderState: object}>}
108
105
  */
109
- export function projectOf(scene, opts) {
110
- return Promise.resolve().then(function () {
111
- opts = opts || {};
112
- Object.keys(opts).forEach(function (k) {
113
- if (OPT_KEYS.indexOf(k) === -1) {
114
- throw new Error('[scene-source] projectOf has no option "' + k + '" — options: ' +
115
- OPT_KEYS.join(', ') + '. (FAIL-LOUD.)');
116
- }
117
- });
118
- var row = _rowOf(scene);
119
- var rel = scenePath(row);
120
- var renderState = effectiveRenderState(row, opts.mode); // an undeclared mode rejects before the read
121
- var router = opts.router || _packageRouter();
122
- if (typeof router.resolve !== 'function') {
123
- throw new Error('[scene-source] opts.router has no resolve() — pass an engineRouter ' +
124
- '(lib/shim/engine/router.js) to resolve ' + rel + '. (FAIL-LOUD.)');
106
+ export function emptyProjectOf(opts) {
107
+ return Promise.resolve().then(() => {
108
+ const o = assertOpts(opts, ['router', 'fetch'], EMPTY_PROJECT_OF);
109
+ if (!o.router || typeof o.router.resolve !== 'function') {
110
+ throw new Error('[scene-source] ' + EMPTY_PROJECT_OF + ' needs the host\'s engineRouter ' +
111
+ '(lib/shim/engine/router.js) to resolve ' + EMPTY_SKETCH_PATH + '. (FAIL-LOUD.)');
125
112
  }
126
- var url = router.resolve(rel);
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) {
130
- if (typeof t !== 'string' || !t.trim()) {
131
- throw new Error('[scene-source] scene "' + row.id + '" — ' + url +
132
- ' has an empty body. (FAIL-LOUD.)');
133
- }
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 };
139
- });
113
+ const url = o.router.resolve(EMPTY_SKETCH_PATH);
114
+ return folderOf(EMPTY_PROJECT_NAME, url, 'the Empty Universe', o, normalizeSavedRenderState({}));
140
115
  });
141
116
  }
@@ -4,7 +4,7 @@
4
4
  * studio-landing.js — the ONE record of how STUDIO resolved the deep link it opened with, as host-view.js records
5
5
  * its view outcome: STUDIO's resolver (lib/features/ide/studio-landing-of.js::studioLandingOf) decides the row and
6
6
  * AuthorView.jsx writes it on `document.documentElement` under STUDIO_LANDING_ATTR, through recordStudioLanding,
7
- * and a gate that opens a link (a showcase tile, showcase-link.js) reads that attribute instead of STUDIO's DOM or
7
+ * and a gate that opens a link (a showcase tile, roster.js showcaseHref) reads that attribute instead of STUDIO's DOM or
8
8
  * its error copy.
9
9
  *
10
10
  * STUDIO_LANDING_ATTR the <html> attribute carrying the outcome id
@@ -33,23 +33,31 @@ export const STUDIO_LANDING_ATTR = 'data-frqtl-studio-landing';
33
33
  /** The closed table of STUDIO deep-link landings, in the order the resolver tests them (lib/features/ide/test/studio-landing-of.test.mjs holds it). */
34
34
  export const STUDIO_LANDING_OUTCOMES = Object.freeze([
35
35
  { id: 'refused-key', runs: false,
36
- means: 'the link carries a key STUDIO does not own, or a deleted key (deep-link.js deepLinkRefusalOf): the refusal is on the deep-link strip and the opening rung opens' },
36
+ means: 'the link carries a key STUDIO does not own, or a deleted key (deep-link.js deepLinkRefusalOf): the refusal is on the deep-link strip and the opening scene opens' },
37
37
  { id: 'conflict', runs: false,
38
- means: 'the link names both a walkthrough and a saved project: nothing opens' },
38
+ means: 'the link names more than one scene source (two of walkthrough, project and scene), or a renderer beside a walkthrough or a project, which carry their own: nothing opens' },
39
39
  { id: 'mode-without-walkthrough', runs: false,
40
40
  means: 'the link names a mode and no walkthrough: nothing opens' },
41
+ { id: 'renderer-without-scene', runs: false,
42
+ means: 'the link names a renderer and no scene; a renderer is chosen only for a scene link: nothing opens' },
41
43
  { id: 'unknown-walkthrough', runs: false,
42
- means: 'the linked walkthrough is not in the walkthrough registry (walkthroughById): nothing opens' },
44
+ means: 'the linked walkthrough is not a scene of the loaded roster (walkthroughById(roster, id)): nothing opens' },
43
45
  { id: 'unknown-mode', runs: false,
44
- means: 'the linked mode is not one the linked rung declares (walkthroughModes): nothing opens' },
46
+ means: 'the linked mode is not one the linked scene declares (walkthroughModes): nothing opens' },
47
+ { id: 'scene-refused-origin', runs: false,
48
+ means: 'the linked scene URL is on neither the page\'s origin nor the roster\'s (roster-link.js sceneOriginRefusalOf): the strip names the rule and both origins; nothing opens' },
49
+ { id: 'scene-unreadable', runs: false,
50
+ means: 'the linked scene URL is allowed but its read failed (a non-200, an empty body, or not a .js sketch): nothing opens' },
45
51
  { id: 'project-unreadable', runs: false,
46
52
  means: 'the linked saved project is present but its folder is refused by the load: nothing opens' },
47
53
  { id: 'project-not-found', runs: false,
48
54
  means: 'no saved project of the linked name in this browser: nothing opens' },
49
55
  { id: 'ok', runs: true,
50
- means: 'the linked walkthrough (in the linked mode, else its default) or saved project resolved and was handed to STUDIO\'s selection path; the scene starts on load' },
56
+ means: 'the linked walkthrough (in the linked mode, else its default), saved project or scene resolved and was handed to STUDIO\'s selection path; the scene starts on load' },
51
57
  { id: 'first-contact', runs: false,
52
- means: 'no STUDIO deep link: the walkthrough registry\'s opening rung opens' },
58
+ means: 'no STUDIO deep link: the roster\'s first-contact scene (firstContactOf(roster)) opens' },
59
+ { id: 'first-contact-empty', runs: false,
60
+ means: 'no STUDIO deep link and the roster declares no first contact: the Empty Universe opens' },
53
61
  ].map(Object.freeze));
54
62
 
55
63
  const IDS = new Set(STUDIO_LANDING_OUTCOMES.map((r) => r.id));
@@ -13,8 +13,7 @@
13
13
  * `godrayMode` 'radial' is the sun's own (its rays fan from one light point, the source frames' centroid).
14
14
  * The camera is NOT here: a pose is a rung row's view, not a look.
15
15
  *
16
- * Readers: `lib/shim/registry/walkthrough.js` (a named import); a scene sketch may import it too
17
- * (`@descix/frqtl-web/shim/render/sun-look.js`). Zero physics: render
16
+ * Readers: a scene sketch, by its package specifier (`@descix/frqtl-web/shim/render/sun-look.js`). Zero physics: render
18
17
  * options are read only by the renderer.
19
18
  */
20
19
 
package/llms.txt CHANGED
@@ -1,13 +1,15 @@
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. 0.6.1-p3.3 on npm dist-tag `next`. Node ≥ 22.12.
4
+ > `bootNode` and the hosted eyes. 0.7.0-p0 on npm dist-tag `next`. Node ≥ 22.12.
5
5
 
6
6
  - Install: `npm install @descix/frqtl-node`
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
+ - `./registry`: the roster contract: `loadRoster(<file: URL of a host's roster>)` → the bag every reader takes first (`projectOf(roster, id, { mode })`, `effectiveRenderState(row, mode)`, `showcaseHref(roster, card)`, …); the package ships no scene rows.
11
+ - See it run: https://egpt-frqtl.descix.net
10
12
  - `./host-view`: `declareHostView`, `HOST_VIEW_OUTCOMES`, `CONTRACT_VIOLATIONS`, `HOST_SDK_SCRIPT` — the same declarer as @descix/frqtl-web/host-view, pure, for a node test of a call site.
11
13
  - `./register`: `node --import @descix/frqtl-node/register scene.mjs` resolves a scene's `@descix/frqtl-web/...` imports to this package.
12
- - Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.6.1-p3.3/CLAUDE.md
14
+ - Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.7.0-p0/CLAUDE.md
13
15
  - 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.1-p3.3",
3
+ "version": "0.7.0-p0",
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,37 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * SCENE — 1B · Absorbing Wall (3D)
5
- *
6
- * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
- * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
- * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
- * (rung cl1b-absorbing-wall-3d).
11
- */
12
-
13
- import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
14
-
15
- // Collision Lab 1B · 3D — a volumetric (spherical) source into a 3D ABSORB wall
16
- // slab (z-slab centered on 0 so the 3D spray collides); orbit the camera to view.
17
- const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
18
- const cy = Math.round(H / 2);
19
-
20
- export const parts = {
21
- source: [SphereEmitter, {
22
- preset: 'spherical', is2D: false, center: [Math.round(W * 0.15), cy],
23
- chargeMode: 'random', fundamentalDimension: 2, wavelength: 32,
24
- burstSize: 24, emitEveryTicks: 1
25
- }],
26
- absorber: [Wall, {
27
- rect: [Math.round(W * 0.6), Math.round(H * 0.2), 20, Math.round(H * 0.6)],
28
- z: -80, depth: 160,
29
- collisionBehavior: 'absorb', capacity: 64,
30
- color: [150, 90, 210]
31
- }],
32
- };
33
-
34
- export default function buildExperiment(exp) {
35
- exp.createScene(W, H, 600, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 3D scene + universe, 600x400x600 — first, then parts
36
- exp.declare(parts);
37
- }
@@ -1,36 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * SCENE — 1B · Absorbing Wall
5
- *
6
- * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
- * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
- * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
- * (rung cl1b-absorbing-wall).
11
- */
12
-
13
- import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
14
-
15
- // Collision Lab 1B (Absorbing Wall) — ABSORB v2 (Track A): the wall LO is backed by
16
- // a REAL leaf frame; a host tick-fn tallies the killed masses into it + radiates.
17
- const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
18
- const cy = Math.round(H / 2);
19
-
20
- export const parts = {
21
- source: [SphereEmitter, {
22
- preset: 'spherical', is2D: true, center: [Math.round(W * 0.15), cy],
23
- chargeMode: 'random', fundamentalDimension: 2, wavelength: 32,
24
- burstSize: 24, emitEveryTicks: 1
25
- }],
26
- absorber: [Wall, {
27
- rect: [Math.round(W * 0.6), Math.round(H * 0.2), 20, Math.round(H * 0.6)],
28
- collisionBehavior: 'absorb', capacity: 64,
29
- color: [150, 90, 210]
30
- }],
31
- };
32
-
33
- export default function buildExperiment(exp) {
34
- exp.createCanvas(W, H, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
35
- exp.declare(parts);
36
- }
@@ -1,91 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * SCENE — Atomic model
5
- *
6
- * A sketch file: the WHOLE scene as one `export default function buildExperiment(exp)` module. The
7
- * world and the three-level frame hierarchy are built here from the engine's own primitives (leaf
8
- * frames — `engine().Frame`, read when the scene builds — and parent frames from a rect). Registered by
9
- * lib/shim/registry/walkthrough.js (rung atomic-model).
10
- *
11
- * Physics (zero new — bench H1, A1, A2, M1, M4): 48 one-pixel quarks fill an 8x8 proton frame; the
12
- * proton and one electron sit in a 64x64 atom frame. There is no force equation. Each parent's
13
- * compress() pulls its children one pixel toward its centre each tick (A1: a stationary parent
14
- * CONFINES a child), a parent's motion is carried by its children (A2), a parent's mass is its
15
- * child count (H1), and its wavelength is capacity/mass (M4) — 64/48 → the proton's oscillation.
16
- * The atom is stationary: the lab frame. `bonding` (noEscape) traps each quark in its parent's
17
- * live rect, so the nucleus holds together: binding emerges from compress, not from a law.
18
- *
19
- * Order (it seeds the run): world → the three layers → quarks (grid-filled, registered) → proton
20
- * from its rect → electron (registered) → atom from its rect → electron into the atom.
21
- */
22
-
23
- import { Rectangle } from '@descix/frqtl-web/parts';
24
- import * as OrbitBag from '@descix/frqtl-web/compat/renderer/orbit-bag.js';
25
- import { engine } from '@descix/frqtl-web/compat/engine.js';
26
-
27
- export const parts = {};
28
-
29
- export default function buildExperiment(exp) {
30
- var W = 600, H = 600; // THE WORLD this scene states (its co-init call below)
31
-
32
- var QUARK_LAYER = 0; // 1x1 leaves
33
- var SUBATOMIC_LAYER = 3; // the proton: 8x8, capacity 64
34
- var ATOM_LAYER = 6; // the atom: 64x64, capacity 4096
35
- var QUARKS_PER_PROTON = 48; // the proton's mass at t=0 → wavelength 64/48
36
-
37
- // ── WORLD — a 2D canvas + universe: quark layer fundamental, λ multiplier 1, quarks confined ──
38
- exp.createCanvas(W, H, { dimension: QUARK_LAYER, wavelengthScale: 1, bonding: true });
39
- exp.declare(parts);
40
- var u = exp.universe;
41
-
42
- var quarkDim = u.addDimension(QUARK_LAYER);
43
- var subatomicDim = u.addDimension(SUBATOMIC_LAYER);
44
- var atomDim = u.addDimension(ATOM_LAYER);
45
- var subatomicW = subatomicDim.mb_w, atomW = atomDim.mb_w;
46
-
47
- var Frame = engine().Frame, Charge = engine().Charge; // the engine's own frame primitive + charge ABI
48
- var cx = Math.round(W / 2), cy = Math.round(H / 2);
49
- var atomRect = new Rectangle(cx, cy, atomW, atomW, null, true);
50
- var protonRect = new Rectangle(cx, cy, subatomicW, subatomicW, null, true);
51
- var rect = exp.runner.canvas_rect;
52
-
53
- // ── QUARKS — fill the proton rect column by column ─────────────────────
54
- var quarkW = Math.max(1, Math.pow(2, QUARK_LAYER));
55
- var quarks = [];
56
- for (var x = protonRect.left; x < protonRect.left + protonRect.w && quarks.length < QUARKS_PER_PROTON; x += quarkW) {
57
- for (var y = protonRect.top; y < protonRect.top + protonRect.h && quarks.length < QUARKS_PER_PROTON; y += quarkW) {
58
- var q = Frame.makeLeafFrame(x, y, QUARK_LAYER, 1, 0, 0, false, Charge.POSITIVE, rect);
59
- q.name = 'quark_' + quarks.length;
60
- quarks.push(q);
61
- }
62
- }
63
- for (var i = 0; i < quarks.length; i++) { quarkDim.addFrame(quarks[i]); }
64
-
65
- // ── PROTON — the parent frame of the quarks ────────────────────────────
66
- var proton = subatomicDim.createFrameFromRect(protonRect, quarks, 0, 0);
67
- proton.name = 'proton';
68
-
69
- // ── ELECTRON — one negative leaf, upper-right of the atom ──────────────
70
- var electron = Frame.makeLeafFrame(atomRect.left + Math.round(atomW * 0.75), atomRect.top + Math.round(atomW * 0.25),
71
- QUARK_LAYER, 1, 0, 0, false, Charge.NEGATIVE, rect);
72
- electron.name = 'electron';
73
- quarkDim.addFrame(electron);
74
-
75
- // ── ATOM — stationary parent of the proton; the electron joins directly ─
76
- var atom = atomDim.createFrameFromRect(atomRect, [proton], 0, 0, true, true);
77
- atom.name = 'atom';
78
- atom.insert(electron);
79
-
80
- u.leafQuantaWidth = quarkW;
81
- u.brownianMotion = true;
82
-
83
- // ── VIEW — frame the atom (render only: the 2D camera's `distance` is the visible physics-px
84
- // height). The atom is 64 px in the world; at 1.5x its width it fills two thirds of the view,
85
- // so the proton, its quarks and the electron are individually legible. ─────────────────────
86
- var cam = exp.runner.cameraControl;
87
- var flat = !OrbitBag.hasAngularPose(cam);
88
- if (cam && typeof cam.setOrbit === 'function' && flat) {
89
- cam.setOrbit({ target: { x: cx, y: cy }, distance: Math.round(atomW * 1.5) });
90
- }
91
- }
@@ -1,49 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * SCENE — Big Bang
5
- *
6
- * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world and the
7
- * point source are built here; the sugar (PointSource — deprecated V1 sugar, byte-identical), the
8
- * renderer and the engine are imports. Registered by lib/shim/registry/walkthrough.js (rung big-bang).
9
- *
10
- * Physics (zero new — bench M1, H1, H2a-c): one radial burst at the canvas centre — 72 rays (5°) at
11
- * radii 32 → 2 — of leaves with NO initial momentum (emergentPhysics). The world generates parent
12
- * frames top-down (by how many frames an area can parent, brownian on), so a uniform field clumps into
13
- * filaments and voids with no gravity law and no cosmological constant. The source shuts off after
14
- * 20 s of ticks (1200).
15
- *
16
- * Order (it seeds the run): world (with its parent-generation interval) → source → the shut-off tick-fn.
17
- */
18
-
19
- import { PointSource } from '@descix/frqtl-web/parts';
20
-
21
- export const parts = {};
22
-
23
- export default function buildExperiment(exp) {
24
- var W = 600, H = 600; // THE WORLD this scene states (its co-init call below)
25
-
26
- var SOURCE_DIAMETER = 64; // emission radii 32 → 2 (step 2)
27
- var ANGLE_STEP = 5; // degrees between rays → 72 rays
28
- var EMIT_TICKS = 20 * 60; // the source fires for 1200 ticks
29
-
30
- // ── WORLD — a 2D canvas + universe: fundamental layer 0, λ multiplier 1, parents top-down ──
31
- // iframeIntervalSeconds 1/60: parents are generated EVERY tick (the engine freezes the interval at
32
- // init, so it rides the world cfg — at the default 1 s, Phase B ran every 60 ticks).
33
- exp.createCanvas(W, H, { dimension: 0, wavelengthScale: 1, frames: true, iframeIntervalSeconds: 1 / 60 });
34
- exp.declare(parts);
35
- var u = exp.universe;
36
-
37
- // ── THE SOURCE — at the centre, no initial momentum, random charge ──────
38
- var source = new PointSource(u, Math.floor(W / 2), Math.floor(H / 2), SOURCE_DIAMETER,
39
- 0, null, true, Infinity, 0, ANGLE_STEP,
40
- { emergentPhysics: true, minRadius: 2 });
41
-
42
- // ── SHUT-OFF — the bang is one burst era, then the field evolves on its own ──
43
- var t = 0;
44
- exp.addTickFunction(function () {
45
- t++;
46
- if (t >= EMIT_TICKS) { source.setActive(false); }
47
- return [];
48
- });
49
- }
@@ -1,84 +0,0 @@
1
- /* Copyright 2023-2026 by Essam Abadir */
2
-
3
- /**
4
- * SCENE — Blackbody
5
- *
6
- * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world, the
7
- * oven, its thermostat and the radiation sink are all constructed here; the sugar objects
8
- * (OvenBox, LargeObject), the renderer and the engine are imports. Registered by
9
- * lib/shim/registry/walkthrough.js (rung blackbody).
10
- *
11
- * Physics (zero new — composition of goldened behaviours): a cavity of absorbing walls holds a
12
- * population of leaves; the OvenBox thermostat keeps the cavity's quanta near the TEMPERATURE
13
- * target by emitting, and colliding leaves merge into parent frames (bench H2a–c,
14
- * collide → merge → promote; `withInterQuantumCollisions`). A parent's fullness IS its frequency
15
- * (wavelength = capacity/mass, bench M4), so what leaks out of the slit in the right wall is a
16
- * spectrum of frame sizes — the emergent Planck-like curve — with no energy quanta assumed.
17
- * Everything that escapes is absorbed by the sink at the right edge (the experiment boundary).
18
- *
19
- * Order matters (it seeds the run): the world, the brownian/collision flags, the quantum
20
- * layer, the thermostat SLOT (reserved before the oven so the oven's own tick-fns run after it),
21
- * the oven, the thermostat, the sink. Same calls, same order, same seed ⇒ the same RNG stream.
22
- */
23
-
24
- import { CollisionActions, CollisionTypes, FrqtlControlOverlay, FrqtlControlsPanel, LargeObject, Rectangle } from '@descix/frqtl-web/parts';
25
- import * as ControlRegistry from '@descix/frqtl-web/shim/controls/registry.js';
26
-
27
- const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
28
- const SINK_DEPTH = 30; // the absorbing strip at the right canvas edge (px)
29
-
30
- export const parts = {
31
- // ── THE SINK — the experiment boundary on the slit side ─────────────────
32
- radiation_sink: [LargeObject, {
33
- rect: { left: W - SINK_DEPTH, top: 0, w: SINK_DEPTH, h: H },
34
- collision_type: CollisionTypes.NONE, actions: [CollisionActions.ABSORB],
35
- reposition: 'closest', color: [0, 0, 0]
36
- }],
37
- };
38
-
39
- export default function buildExperiment(exp) {
40
- var TEMPERATURE = 1500; // target quanta inside the cavity (the knob below)
41
- var QUANTUM_DIM = 3; // the layer leaves promote into: an 8 px box edge, 64 cells of capacity in 2D
42
- var BOX_FRACTION = 1 / 3; // the cavity's outer box, as a fraction of the canvas
43
- var WALL = 10; // wall thickness (px)
44
- var SLIT_HEIGHT = 60; // the radiation slit in the right wall (px)
45
-
46
- // ── WORLD — a 2D canvas + universe, fundamental layer 0 (1x1 leaves), λ multiplier 1 ──
47
- exp.createCanvas(W, H, { dimension: 0, wavelengthScale: 1 });
48
- var u = exp.universe;
49
- u.brownianMotion = true;
50
- u.withInterQuantumCollisions = true;
51
- u.addDimension(QUANTUM_DIM);
52
-
53
- // ── THE OVEN — the cavity, centred on the canvas ────────────────────────
54
- var outer = new Rectangle(Math.round(W / 2), Math.round(H / 2),
55
- Math.round(W * BOX_FRACTION), Math.round(H * BOX_FRACTION), null, true);
56
- // The thermostat runs FIRST each tick: reserve its slot before the oven registers its own.
57
- var thermostatSlot = u.experimentTickFunctions.length;
58
- u.experimentTickFunctions.push(null);
59
- var oven = exp.addObject('OvenBox', {
60
- name: 'oven',
61
- left: outer.left + WALL, top: outer.top + WALL,
62
- width: outer.w - 2 * WALL, height: outer.h - 2 * WALL,
63
- wallThickness: WALL, temperature: TEMPERATURE,
64
- slitHeight: SLIT_HEIGHT, // the slit is in the RIGHT wall (OvenBox's own slit side)
65
- color: [120, 120, 120]
66
- });
67
- u.experimentTickFunctions[thermostatSlot] = oven.getTemperatureTickFunction();
68
-
69
- exp.declare(parts); // the sink (parts.radiation_sink)
70
-
71
- // ── THE KNOB — temperature, live (the oven's own thermostat target) ─────
72
- var registry = ControlRegistry.makeControlRegistry();
73
- registry.registerControls('blackbody', [
74
- { kind: 'scalar', id: 'temperature', label: 'temperature (quanta)', min: 50, max: 8000, step: 50,
75
- value: TEMPERATURE, fmt: function (v) { return String(v); },
76
- onChange: function (v) { oven.setTemperature(v); },
77
- place: { x: 0.02, y: 0.03 } }
78
- ]);
79
- // Chrome (a page only): expose the knobs to a host frame, and mount the overlay through the
80
- // runner's ONE door (runner.controlHost) — before a canvas is mounted (a notebook pane) there is
81
- // none, so the knobs stay in the exposed registry only.
82
- FrqtlControlsPanel.exposeRegistry(registry, { runner: exp.runner });
83
- if (exp.runner.controlHost) { FrqtlControlOverlay.mount(exp.runner, { registry: registry, name: 'blackbody' }); }
84
- }