@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.
- package/CLAUDE.md +2 -2
- package/README.md +6 -4
- package/dist/dist-manifest.json +71 -65
- package/dist/eyes/index.js +5 -2
- package/dist/fat/pkg-node/package.json +2 -10
- package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
- package/dist/frqtl/objects/FrqtlApp.js +33 -6
- package/dist/frqtl/objects/RecipeContext.js +284 -13
- package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
- package/dist/frqtl/objects/instruments/capture.js +1 -24
- package/dist/frqtl/objects/instruments/hosted-eyes.js +216 -29
- package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
- package/dist/frqtl/objects/record/Record.js +346 -0
- package/dist/frqtl/objects/record/open-record.js +22 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
- package/dist/frqtl/objects/schema/scopes.js +2 -2
- package/dist/frqtl/objects/setup/_worldInit.js +1 -2
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
- package/dist/frqtl/parts/CameraControls.js +8 -0
- package/dist/frqtl/parts/DetectorWall.js +1 -1
- package/dist/frqtl/parts/FrqtlVision.js +21 -6
- package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
- package/dist/frqtl/parts/LargeObject.js +2 -2
- package/dist/frqtl/parts/LiveChart.js +2 -2
- package/dist/frqtl/parts/LogicGate.js +3 -3
- package/dist/frqtl/parts/OvenBox.js +1 -1
- package/dist/frqtl/parts/PointSource.js +1 -1
- package/dist/frqtl/parts/SlitLightSource.js +1 -1
- package/dist/frqtl/parts/SlitScreen.js +1 -1
- package/dist/frqtl/parts/SphereEmitter.js +12 -12
- package/dist/frqtl/parts/Trace.js +1 -1
- package/dist/frqtl/parts/Tunnel.js +11 -11
- package/dist/frqtl/parts/Wall.js +20 -10
- package/dist/frqtl/scenes/circuit-sat.js +1 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
- package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
- package/dist/frqtl/scenes/mirror-route.js +96 -79
- package/dist/shim/bundle/index.js +118 -96
- package/dist/shim/bundle/indexHtml.js +66 -39
- package/dist/shim/controls/index.js +1 -1
- package/dist/shim/engine/artifact-shell.js +1 -1
- package/dist/shim/engine/router.js +3 -3
- package/dist/shim/host/host-view.js +228 -0
- package/dist/shim/host/index.js +18 -0
- package/dist/shim/index.js +3 -5
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/kernel.js +1 -1
- package/dist/shim/notebook/setup-builder.js +2 -2
- package/dist/shim/notebook/store.js +2 -3
- package/dist/shim/package.json +1 -0
- package/dist/shim/page-params/index.js +2 -30
- package/dist/shim/project/fork.js +293 -0
- package/dist/shim/project/identity.js +3 -1
- package/dist/shim/project/index.js +16 -3
- package/dist/shim/project/link.js +224 -46
- package/dist/shim/project/manifest.js +151 -0
- package/dist/shim/project/parts-writer.js +68 -7
- package/dist/shim/project/read-text.js +43 -0
- package/dist/shim/project/record-format.js +345 -0
- package/dist/shim/project/specifiers.js +104 -15
- package/dist/shim/registry/exhibits.js +15 -34
- package/dist/shim/registry/index.js +3 -3
- package/dist/shim/registry/scene-roster.js +35 -0
- package/dist/shim/registry/scene-source.js +48 -64
- package/dist/shim/registry/walkthrough.js +6 -6
- package/dist/shim/render-state/apply.js +66 -31
- package/dist/shim/render-state/index.js +6 -2
- package/dist/shim/runtime/index.js +4 -4
- package/dist/shim/storage/codec-chains.js +88 -0
- package/dist/shim/storage/folders.js +205 -0
- package/dist/shim/storage/index.js +64 -8
- package/dist/shim/storage/make-namespace-store.js +5 -5
- package/llms.txt +4 -3
- package/package.json +19 -1
- package/dist/shim/bundle/frqnb.js +0 -112
- package/dist/shim/bundle/manifest.js +0 -95
- package/dist/shim/registry/experiments.js +0 -215
- package/dist/shim/storage/modules.js +0 -249
- package/dist/shim/storage/projects.js +0 -150
package/llms.txt
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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.
|
|
4
|
+
> `bootNode` and the hosted eyes. 0.6.0 on npm dist-tag `latest`. Node ≥ 22.12.
|
|
5
5
|
|
|
6
|
-
- Install: `npm install @descix/frqtl-node
|
|
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
|
+
- `./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.
|
|
10
11
|
- `./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
|
|
12
|
+
- Full agent guide: https://cdn.jsdelivr.net/npm/@descix/frqtl-node@0.6.0/CLAUDE.md
|
|
12
13
|
- 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
|
|
3
|
+
"version": "0.6.0",
|
|
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": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./parts/*": "./dist/frqtl/parts/*",
|
|
14
14
|
"./project": "./dist/shim/project/index.js",
|
|
15
15
|
"./registry": "./dist/shim/registry/index.js",
|
|
16
|
+
"./host-view": "./dist/shim/host/index.js",
|
|
16
17
|
"./register": "./dist/shim/project/register-node.mjs",
|
|
17
18
|
"./shim/controls/registry.js": "./dist/shim/controls/registry.js",
|
|
18
19
|
"./shim/render/sun-look.js": "./dist/shim/render/sun-look.js",
|
|
@@ -21,6 +22,23 @@
|
|
|
21
22
|
"./objects/setup/_collisionMatrixLanes.js": "./dist/frqtl/objects/setup/_collisionMatrixLanes.js",
|
|
22
23
|
"./compat/engine.js": "./dist/frqtl/compat/engine.js",
|
|
23
24
|
"./kinds": "./dist/frqtl/objects/kinds-entry.js",
|
|
25
|
+
"./objects/objects/tagger.js": "./dist/frqtl/objects/objects/tagger.js",
|
|
26
|
+
"./objects/schema/index.js": "./dist/frqtl/objects/schema/index.js",
|
|
27
|
+
"./shim/render/heat-colormap.js": "./dist/shim/render/heat-colormap.js",
|
|
28
|
+
"./objects/schema/valueNormalizer.js": "./dist/frqtl/objects/schema/valueNormalizer.js",
|
|
29
|
+
"./objects/objects/Box.js": "./dist/frqtl/objects/objects/Box.js",
|
|
30
|
+
"./objects/objects/QuantumEmitter.js": "./dist/frqtl/objects/objects/QuantumEmitter.js",
|
|
31
|
+
"./shim/expr/ast-schema.js": "./dist/shim/expr/ast-schema.js",
|
|
32
|
+
"./objects/objects/actionCompiler.js": "./dist/frqtl/objects/objects/actionCompiler.js",
|
|
33
|
+
"./objects/v1-replacements.js": "./dist/frqtl/objects/v1-replacements.js",
|
|
34
|
+
"./objects/objects/slitLinerScatter.js": "./dist/frqtl/objects/objects/slitLinerScatter.js",
|
|
35
|
+
"./objects/setup/setupSphereEmitter3D.js": "./dist/frqtl/objects/setup/setupSphereEmitter3D.js",
|
|
36
|
+
"./objects/objects/directedRedirectProgram.js": "./dist/frqtl/objects/objects/directedRedirectProgram.js",
|
|
37
|
+
"./compat/renderer/lo-drawn-center.js": "./dist/frqtl/compat/renderer/lo-drawn-center.js",
|
|
38
|
+
"./objects/setup/setupTunnelPointExit.js": "./dist/frqtl/objects/setup/setupTunnelPointExit.js",
|
|
39
|
+
"./objects/constants/WallBehaviors.js": "./dist/frqtl/objects/constants/WallBehaviors.js",
|
|
40
|
+
"./objects/objects/absorbRadiation.js": "./dist/frqtl/objects/objects/absorbRadiation.js",
|
|
41
|
+
"./objects/geometry/SlabFace.js": "./dist/frqtl/objects/geometry/SlabFace.js",
|
|
24
42
|
"./dist-manifest.json": "./dist/dist-manifest.json",
|
|
25
43
|
"./package.json": "./package.json"
|
|
26
44
|
},
|
|
@@ -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
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* lib/shim/bundle/manifest.js — the bundle manifest as an ENVELOPE FERRY
|
|
5
|
-
* (E1).
|
|
6
|
-
*
|
|
7
|
-
* ── The rule (E1): manifest = the Module envelope minus `source` ─────────────
|
|
8
|
-
*
|
|
9
|
-
* The bundle is a projection of the `lib/shim/storage/modules.js` Module
|
|
10
|
-
* envelope. `experiment.js` = `module.source`. The manifest carries EVERYTHING
|
|
11
|
-
* ELSE from the envelope, ferried VERBATIM (`{...envelope}` minus `source`) plus
|
|
12
|
-
* a small set of bundle-only fields. Critically this module NEVER re-enumerates
|
|
13
|
-
* the envelope's field list (`{name: mod.name, renderState: mod.renderState, …}`)
|
|
14
|
-
* — that would be a schema mirror of modules.js. It spreads the whole envelope
|
|
15
|
-
* and deletes the ONE key (`source`) the sibling `experiment.js` file owns. Any
|
|
16
|
-
* field modules.js adds in future rides through automatically; `renderState`
|
|
17
|
-
* rides through as an ordinary envelope field, opaque (never parsed here).
|
|
18
|
-
*
|
|
19
|
-
* ── Bundle-only fields ───────────────────────────────────────────────────────
|
|
20
|
-
*
|
|
21
|
-
* sdkVersion : string — the `@descix/frqtl-web@<pin>` the index.html imports.
|
|
22
|
-
* A VALUE passed IN by the caller (the feature side
|
|
23
|
-
* DERIVES it from the built SDK's package.json at export
|
|
24
|
-
* time — truth-in-labeling; NEVER a literal in bundle
|
|
25
|
-
* code). REQUIRED — FAIL-LOUD when absent.
|
|
26
|
-
* preview : string|null — the preview image filename ref (e.g. 'preview.png'),
|
|
27
|
-
* or null when no preview was captured.
|
|
28
|
-
* descix : {} — reserved inert publish block (DeSciX now-vs-later).
|
|
29
|
-
*
|
|
30
|
-
* Layer 2 (physics): none — pure metadata projection.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
'use strict';
|
|
34
|
-
|
|
35
|
-
/** The envelope key the sibling `experiment.js` file owns (never in the manifest). */
|
|
36
|
-
export var SOURCE_KEY = 'source';
|
|
37
|
-
|
|
38
|
-
/** Bundle-only manifest keys (NOT envelope fields — stripped on import). */
|
|
39
|
-
export var BUNDLE_ONLY_KEYS = ['sdkVersion', 'preview', 'descix'];
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Build the manifest object from a Module envelope + bundle-only inputs.
|
|
43
|
-
*
|
|
44
|
-
* @param {object} envelope — a `lib/shim/storage/modules.js` Module envelope
|
|
45
|
-
* (MUST carry `source`; every other field is ferried verbatim).
|
|
46
|
-
* @param {object} opts
|
|
47
|
-
* @param {string} opts.sdkVersion — REQUIRED; the CDN pin (e.g. '0.6.0-p1.1').
|
|
48
|
-
* @param {string|null} [opts.preview] — preview filename ref, or null/omitted.
|
|
49
|
-
* @returns {object} the manifest (JSON-serializable).
|
|
50
|
-
*/
|
|
51
|
-
export function buildManifest(envelope, opts) {
|
|
52
|
-
if (!envelope || typeof envelope !== 'object') {
|
|
53
|
-
throw new Error('[bundle/manifest] buildManifest(envelope): envelope must be an object.');
|
|
54
|
-
}
|
|
55
|
-
opts = opts || {};
|
|
56
|
-
if (typeof opts.sdkVersion !== 'string' || opts.sdkVersion.length === 0) {
|
|
57
|
-
throw new Error('[bundle/manifest] buildManifest: opts.sdkVersion is REQUIRED (a derived value, ' +
|
|
58
|
-
'never a literal) — the index.html CDN pin depends on it.');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Ferry the WHOLE envelope minus `source` (no per-field re-enumeration).
|
|
62
|
-
var manifest = Object.assign({}, envelope);
|
|
63
|
-
delete manifest[SOURCE_KEY];
|
|
64
|
-
|
|
65
|
-
// Bundle-only fields.
|
|
66
|
-
manifest.sdkVersion = opts.sdkVersion;
|
|
67
|
-
manifest.preview = (opts.preview != null) ? opts.preview : null;
|
|
68
|
-
manifest.descix = {}; // reserved inert publish block
|
|
69
|
-
|
|
70
|
-
return manifest;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Recover the Module envelope from a manifest + the `experiment.js` source.
|
|
75
|
-
* Inverse of `buildManifest`: strips the bundle-only fields and re-attaches
|
|
76
|
-
* `source`. The result is a full Module envelope (validatable by modules.js).
|
|
77
|
-
*
|
|
78
|
-
* @param {object} manifest — the parsed manifest.json object.
|
|
79
|
-
* @param {string} source — the `experiment.js` recipe source.
|
|
80
|
-
* @returns {object} the reconstructed Module envelope.
|
|
81
|
-
*/
|
|
82
|
-
export function manifestToEnvelope(manifest, source) {
|
|
83
|
-
if (!manifest || typeof manifest !== 'object') {
|
|
84
|
-
throw new Error('[bundle/manifest] manifestToEnvelope(manifest): manifest must be an object.');
|
|
85
|
-
}
|
|
86
|
-
if (typeof source !== 'string') {
|
|
87
|
-
throw new Error('[bundle/manifest] manifestToEnvelope: source (experiment.js) must be a string.');
|
|
88
|
-
}
|
|
89
|
-
var envelope = Object.assign({}, manifest);
|
|
90
|
-
for (var i = 0; i < BUNDLE_ONLY_KEYS.length; i++) {
|
|
91
|
-
delete envelope[BUNDLE_ONLY_KEYS[i]];
|
|
92
|
-
}
|
|
93
|
-
envelope[SOURCE_KEY] = source;
|
|
94
|
-
return envelope;
|
|
95
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* EXPERIMENTS — canonical experiment registry.
|
|
5
|
-
*
|
|
6
|
-
* Single ESM named export. Replaces the dual window.EXPERIMENTS + CommonJS export.
|
|
7
|
-
* Consumed by lib/features/ide via import; SimPane reads experiments from the prop.
|
|
8
|
-
*
|
|
9
|
-
* id : the directory name under frqtl/experiments/{id}/index.html
|
|
10
|
-
* title : human-readable label
|
|
11
|
-
* cat : category label
|
|
12
|
-
* jsOnly : true if the experiment requires JS-engine-only APIs
|
|
13
|
-
*
|
|
14
|
-
* Ordering (G1):
|
|
15
|
-
* Experiments listed in the exhibit manifest appear FIRST, in manifest order.
|
|
16
|
-
* Remaining experiments follow in their original declaration order.
|
|
17
|
-
* This ordering is consumed by SimPane's dropdown via the existing prop path
|
|
18
|
-
* (zero SimPane changes required).
|
|
19
|
-
*
|
|
20
|
-
* The manifest import is done lazily via a factory to avoid circular dependency
|
|
21
|
-
* (exhibits.js imports EXPERIMENTS from here; we cannot import exhibits.js at
|
|
22
|
-
* the top level of this file). The ordered list is computed once on first access.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* EXPERIMENT_TIERS — the tier vocabulary and what each tier means for PUBLIC
|
|
27
|
-
* SURFACING. This table is the contract; consumers never hardcode a tier string.
|
|
28
|
-
*
|
|
29
|
-
* - A row with NO `tier` is public (absence = public; there is deliberately no
|
|
30
|
-
* `tier:'public'` literal to maintain on every row).
|
|
31
|
-
* - A row whose tier resolves `public:false` is dropped from the public
|
|
32
|
-
* experiment choosers by `isPublicExperiment()` / `publicExperiments()`.
|
|
33
|
-
* - An UNKNOWN tier value FAILS LOUD (`isPublicExperiment` throws) — a typo'd
|
|
34
|
-
* tier must never silently publish a lab.
|
|
35
|
-
* - Tier gates the CURATED public chooser (STUDIO Templates). The GYM lists
|
|
36
|
-
* `internalExperiments()` instead — see that function's doc.
|
|
37
|
-
*
|
|
38
|
-
* Visibility only: a non-public tier NEVER affects loadability, deep-linking,
|
|
39
|
-
* ordering, `is3D`, or the label lookup. Those all read the full EXPERIMENTS list
|
|
40
|
-
* / `experimentById()`. `sdk/frqtl-web/build.js` ships `publicExperiments()` +
|
|
41
|
-
* the named infra ids: a dev-tier page is absent from the npm/CDN package, but
|
|
42
|
-
* present in the GYM (both the dev IDE and the shipped static deploy).
|
|
43
|
-
*
|
|
44
|
-
* ── The public set is the NEW-SUGAR content ONLY ─────────────────────────────
|
|
45
|
-
* EVERY row below is `tier:'dev'`: none of these pages has been rebuilt on the
|
|
46
|
-
* current sugar objects (SphereEmitter / v2 `Wall` / `Tunnel` / LiveChart /
|
|
47
|
-
* GraphingDetectorWall), so none is public and none ships. The PUBLIC gallery
|
|
48
|
-
* content is the WALK-THROUGH rung registry (`./walkthrough.js`), which is built
|
|
49
|
-
* entirely on those objects; `sdk/frqtl-web/src/index.html` is its dist surface.
|
|
50
|
-
* An unmigrated page FAILING LOUD in a dist context is the DESIRED signal — it is
|
|
51
|
-
* the list of what remains to migrate. A row returns to the public set by being
|
|
52
|
-
* REBUILT on the new objects and having its `tier` dropped — never by exception.
|
|
53
|
-
*
|
|
54
|
-
* @type {Readonly<Record<string, Readonly<{id:string, public:boolean, label:string}>>>}
|
|
55
|
-
*/
|
|
56
|
-
export const EXPERIMENT_TIERS = Object.freeze({
|
|
57
|
-
dev: Object.freeze({
|
|
58
|
-
id: 'dev',
|
|
59
|
-
public: false,
|
|
60
|
-
label: 'Developer isolation lab / demo',
|
|
61
|
-
}),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/** @type {Array<{id:string, title:string, cat:string, jsOnly?:boolean, wasmOnly?:boolean, is3D?:boolean, tier?:string}>}
|
|
65
|
-
* jsOnly : requires JS-engine-only APIs (no WASM build) — filtered out of Compare.
|
|
66
|
-
* tier : a key of EXPERIMENT_TIERS (above). `tier:'dev'` marks a DEVELOPER-tier
|
|
67
|
-
* isolation lab / demo: fully registered and BOOTABLE by id (direct
|
|
68
|
-
* experiment-page URL + STUDIO deep link — the dev surface), HIDDEN from
|
|
69
|
-
* the PUBLIC experiment chooser (STUDIO Templates). It stays LISTED in
|
|
70
|
-
* the GYM (reads `internalExperiments()`), which is what driving these
|
|
71
|
-
* labs is for. Omit `tier` for a public experiment.
|
|
72
|
-
* wasmOnly: 3D experiment; the JS reference is a frozen 2D fixture, so Compare runs
|
|
73
|
-
* WASM-only (JS-reference pane disabled). See CompareView filter.
|
|
74
|
-
* is3D : the ONE mount-time 3D-render authority (E-W0a). TRUE iff
|
|
75
|
-
* the experiment page unconditionally boots a 3D display — its runner is
|
|
76
|
-
* `new FrqtlApp({ is3D: true, … })` on the three display — the ground-truth
|
|
77
|
-
* signal, NOT wasmOnly (orthogonal: collision-matrix is wasmOnly+2D). STUDIO
|
|
78
|
-
* reads THIS field pre-boot to light up the camera strip + MOVE/LOOK drive for a
|
|
79
|
-
* template-loaded 3D experiment. A gate test (experiment-is3d-agreement.test.mjs)
|
|
80
|
-
* asserts THIS field ↔ the page's runner `is3D: true` — a hand-typed drift is a
|
|
81
|
-
* CI failure (mirror kill). A page that switches 3D only via a `?renderer=three`
|
|
82
|
-
* reinit toggle (single-source-interference) is 2D-DEFAULT → is3D omitted/false. */
|
|
83
|
-
const _EXPERIMENTS_BASE = [
|
|
84
|
-
{ id: 'g1-lo-race', title: 'G1: LO races a quantum', cat: 'Engine Tests', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
85
|
-
{ id: 'g2-lo-sweep', title: 'G2: moving LO sweeps a field', cat: 'Engine Tests', wasmOnly: true, tier: 'dev' },
|
|
86
|
-
{ id: 'g3-lo-collide', title: 'G3: Collision Lab (box-of-balls)', cat: 'Engine Tests', wasmOnly: true, tier: 'dev' },
|
|
87
|
-
{ id: 'collision-matrix', title: 'Collision Matrix (density sweep)', cat: 'Engine Tests', wasmOnly: true, tier: 'dev' },
|
|
88
|
-
{ id: 'tunnel-lab', title: 'Tunnel Lab (DirectedTunnel isolation)', cat: 'Engine Tests', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
89
|
-
{ id: 'lightsource-lab', title: 'LightSource Lab (LightSource isolation)', cat: 'Engine Tests', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
90
|
-
{ id: 'mesh-demo', title: 'render.mesh demo (external glTF)', cat: 'Render Effects', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
91
|
-
{ id: 'cone-emitter-3d', title: 'Cone Emitter 3D (laser ↔ flashlight)', cat: 'Wave Mechanics', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
92
|
-
{ id: 'sphere-emitter-3d', title: 'Sphere Emitter 3D (sun / laser / drum)', cat: 'Wave Mechanics', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
93
|
-
{ id: 'single-source-interference', title: 'Single-Source Self-Interference (omni, 2D)', cat: 'Wave Mechanics', wasmOnly: true, tier: 'dev' },
|
|
94
|
-
{ id: 'tunnel-point-exit', title: 'Tunnel Point-Exit (collapsed mouth, 2D)', cat: 'Wave Mechanics', wasmOnly: true, tier: 'dev' },
|
|
95
|
-
{ id: 'fx-glow', title: 'FX: Glow', cat: 'Render Effects', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
96
|
-
{ id: 'fx-godrays', title: 'FX: God-rays', cat: 'Render Effects', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
97
|
-
{ id: 'fx-smoke', title: 'FX: Smoke', cat: 'Render Effects', wasmOnly: true, is3D: true, tier: 'dev' },
|
|
98
|
-
{ id: 'blackbody-substrate', title: 'Blackbody Substrate (L1+L2+L3+L4)', cat: 'Thermodynamics', tier: 'dev' },
|
|
99
|
-
{ id: 'circuit-basic', title: 'Circuit: Series Resistor', cat: 'Computing', tier: 'dev' },
|
|
100
|
-
{ id: 'trace-circuit', title: 'Trace: the wire part', cat: 'Computing', wasmOnly: true, tier: 'dev' },
|
|
101
|
-
{ id: 'oscillator', title: 'Quantum Oscillator', cat: 'Structure & Forces', tier: 'dev' }
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* The flat experiment list, in declaration order. (The showcase roster — the front-page tiles — is
|
|
106
|
-
* scene files, lib/shim/registry/exhibits.js; no experiment page is a tile.)
|
|
107
|
-
*
|
|
108
|
-
* @type {Array<{id:string, title:string, cat:string, jsOnly?:boolean, wasmOnly?:boolean}>}
|
|
109
|
-
*/
|
|
110
|
-
export const EXPERIMENTS = _EXPERIMENTS_BASE;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* isPublicExperiment(entry) — THE tier-visibility predicate. Resolves an entry's
|
|
114
|
-
* `tier` against the EXPERIMENT_TIERS contract: no tier ⇒ public; a known tier ⇒
|
|
115
|
-
* that tier's `public` flag; an unknown tier ⇒ THROWS (fail loud — a typo must
|
|
116
|
-
* never silently publish a dev lab).
|
|
117
|
-
*
|
|
118
|
-
* This is the ONE place the tier→visibility rule lives. Any surface that lists
|
|
119
|
-
* experiments to the public FERRIES this predicate (or `publicExperiments()`
|
|
120
|
-
* below) — re-writing `e.tier !== 'dev'` in a consumer is the schema-mirror
|
|
121
|
-
* anti-pattern and will drift the moment a second non-public tier is added.
|
|
122
|
-
*
|
|
123
|
-
* @param {{id?:string, tier?:string}} entry — an EXPERIMENTS row.
|
|
124
|
-
* @returns {boolean} true if the entry belongs in a public experiment chooser.
|
|
125
|
-
* @throws {Error} if `entry.tier` is not a key of EXPERIMENT_TIERS.
|
|
126
|
-
*/
|
|
127
|
-
export function isPublicExperiment(entry) {
|
|
128
|
-
if (!entry || !entry.tier) { return true; }
|
|
129
|
-
var tier = EXPERIMENT_TIERS[entry.tier];
|
|
130
|
-
if (!tier) {
|
|
131
|
-
throw new Error(
|
|
132
|
-
'[experiments] unknown tier "' + entry.tier + '" on experiment "' +
|
|
133
|
-
(entry.id || '?') + '". Known tiers: ' +
|
|
134
|
-
Object.keys(EXPERIMENT_TIERS).join(', ') + ' (omit `tier` for public).'
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
return tier.public === true;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* publicExperiments() — the EXPERIMENTS a PUBLIC chooser should list, filtered by
|
|
142
|
-
* `isPublicExperiment`. The single filtered view both public experiment-chooser
|
|
143
|
-
* surfaces (STUDIO Templates via ExperimentDropdown, the GYM/Compare select)
|
|
144
|
-
* consume, so the dev-tier hide is declared ONCE. Non-public experiments remain
|
|
145
|
-
* registered and bootable by id (direct experiment-page URL, STUDIO deep link) —
|
|
146
|
-
* they are only hidden from these curated lists.
|
|
147
|
-
*
|
|
148
|
-
* @returns {Array<{id:string, title:string, cat:string}>}
|
|
149
|
-
*/
|
|
150
|
-
export function publicExperiments() {
|
|
151
|
-
return EXPERIMENTS.filter(isPublicExperiment);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* internalExperiments() — the roster an INTERNAL-ONLY experiment chooser lists.
|
|
156
|
-
*
|
|
157
|
-
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
158
|
-
* Every registered row, tier-independent, in registry order. This is the peer of
|
|
159
|
-
* `publicExperiments()`, and the split is by SURFACE AUDIENCE, not by taste:
|
|
160
|
-
*
|
|
161
|
-
* - `publicExperiments()` — for a chooser a STRANGER can reach (STUDIO
|
|
162
|
-
* Templates). Tier gates it, because tier means "has this page been rebuilt
|
|
163
|
-
* on the current sugar and is it fit to publish".
|
|
164
|
-
* - `internalExperiments()` — for the GYM (`preset='compare'`), which drives
|
|
165
|
-
* dev-tier isolation labs directly by id and so needs the full roster
|
|
166
|
-
* regardless of tier.
|
|
167
|
-
*
|
|
168
|
-
* WHY THIS EXISTS (2026-07-28): with the whole legacy roster moved to `tier:'dev'`,
|
|
169
|
-
* `publicExperiments()` is legitimately EMPTY. A surface that lists it therefore
|
|
170
|
-
* offers nothing — which is correct for a public chooser (the public content set
|
|
171
|
-
* is the WALK-THROUGH registry) and wrong for the maintainer's driving
|
|
172
|
-
* instrument. Passing the audience question through the tier table was the
|
|
173
|
-
* conflation; naming the two rosters separately is the fix.
|
|
174
|
-
*
|
|
175
|
-
* A consumer picks the roster that matches ITS audience and derives everything
|
|
176
|
-
* else (its default selection included) from that ONE list — never a second
|
|
177
|
-
* hardcoded id.
|
|
178
|
-
*
|
|
179
|
-
* @returns {Array<{id:string, title:string, cat:string}>} registry rows BY REFERENCE.
|
|
180
|
-
*/
|
|
181
|
-
export function internalExperiments() {
|
|
182
|
-
return EXPERIMENTS.slice();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* experimentById(id) — THE id→entry resolution for the registry. Reads the FULL
|
|
187
|
-
* EXPERIMENTS list, tier-independent: a dev-tier experiment resolves exactly like
|
|
188
|
-
* a public one, which is what keeps it reachable by direct URL / deep link while
|
|
189
|
-
* hidden from the public choosers.
|
|
190
|
-
*
|
|
191
|
-
* @param {string} id — an experiment id (directory name under experiments/).
|
|
192
|
-
* @returns {{id:string, title:string, cat:string}|null} the entry, or null if unknown.
|
|
193
|
-
*/
|
|
194
|
-
export function experimentById(id) {
|
|
195
|
-
for (var i = 0; i < EXPERIMENTS.length; i++) {
|
|
196
|
-
if (EXPERIMENTS[i].id === id) { return EXPERIMENTS[i]; }
|
|
197
|
-
}
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* experimentIs3D(id) — the ONE mount-time 3D-render predicate for a gallery
|
|
203
|
-
* experiment (E-W0a). Reads the declarative `is3D` field off the registry entry
|
|
204
|
-
* (the single authority; see the `is3D` doc on `_EXPERIMENTS_BASE`). Returns
|
|
205
|
-
* false for an unknown id or a 2D-default experiment. STUDIO consults this pre-boot
|
|
206
|
-
* for a template-loaded experiment — the axis the walkthrough path derives from
|
|
207
|
-
* `renderState.renderer`. Never sniffs a page or a runner: 3D-ness is DATA here.
|
|
208
|
-
*
|
|
209
|
-
* @param {string} id — an experiment id (directory name under experiments/).
|
|
210
|
-
* @returns {boolean}
|
|
211
|
-
*/
|
|
212
|
-
export function experimentIs3D(id) {
|
|
213
|
-
var entry = experimentById(id);
|
|
214
|
-
return !!entry && entry.is3D === true;
|
|
215
|
-
}
|