@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.2
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 +87 -0
- package/LICENSE.md +664 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2920 -0
- package/dist/frqtl/objects/ObjectFactory.js +296 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +710 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +351 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
- package/dist/frqtl/objects/v1-replacements.js +29 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +34 -0
- package/dist/frqtl/parts/CollisionTypes.js +30 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
- package/dist/frqtl/parts/LargeObject.js +405 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +717 -0
- package/dist/frqtl/parts/OvenBox.js +611 -0
- package/dist/frqtl/parts/PointSource.js +762 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +211 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +690 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +325 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +671 -0
- package/dist/shim/notebook/knob.js +150 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +507 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +213 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +804 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +63 -4
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module view-handle
|
|
5
|
+
* @description
|
|
6
|
+
* The FROZEN host↔realm contract. Produced by `makeViewHandle(impl)`.
|
|
7
|
+
*
|
|
8
|
+
* The frozen handle surface (all async-shaped):
|
|
9
|
+
* ```
|
|
10
|
+
* inject(name, data) → Promise<void> // host → realm data push
|
|
11
|
+
* call(fn, args) → Promise<result> // host → realm invoke + result
|
|
12
|
+
* on(event, cb) → unsubFn // realm → host event stream
|
|
13
|
+
* start() → Promise<void> // control-plane game-loop verbs
|
|
14
|
+
* stop() → Promise<void>
|
|
15
|
+
* step() → Promise<tickData|void>
|
|
16
|
+
* dispose() → Promise<void>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Every method is async-shaped so a marshalling host (the hosted-eyes page-evaluate
|
|
20
|
+
* seam, a BYO realm host) sees the same signatures as the in-app direct-scripting
|
|
21
|
+
* transport. Cell code never branches on transport.
|
|
22
|
+
*
|
|
23
|
+
* VETO-CLASS #1: `inject(name, data)` payloads and `on(event, payload)` event
|
|
24
|
+
* payloads MUST be structured-clone-serializable. `assertSerializable` enforces
|
|
25
|
+
* this at the boundary — FAIL-LOUD regardless of transport.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} ViewHandle
|
|
30
|
+
* @property {string} transport
|
|
31
|
+
* DERIVED label from the transport, `'in-app'` today (truth-in-labeling — never hardcoded by a consumer).
|
|
32
|
+
* @property {(name: string, data: *) => Promise<void>} inject
|
|
33
|
+
* Push a serializable value into the realm environment. VETO-CLASS #1 enforced.
|
|
34
|
+
* @property {(fn: string, args: *[]) => Promise<*>} call
|
|
35
|
+
* Invoke a named function in the realm and resolve its result.
|
|
36
|
+
* @property {(event: string, cb: Function) => Function} on
|
|
37
|
+
* Subscribe to a realm event. Returns an unsubscribe function.
|
|
38
|
+
* @property {() => Promise<void>} start — start the realm game loop.
|
|
39
|
+
* @property {() => Promise<void>} stop — pause the realm game loop.
|
|
40
|
+
* @property {() => Promise<*>} step — advance one tick; resolves with tick data.
|
|
41
|
+
* @property {() => Promise<void>} dispose — tear down the realm.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
'use strict';
|
|
45
|
+
|
|
46
|
+
/* ── structured-clone serializability check (VETO-CLASS #1 enforcement) ───────
|
|
47
|
+
*
|
|
48
|
+
* structuredClone is available in Node 17+ and every modern browser. We attempt a
|
|
49
|
+
* clone and surface a FAIL-LOUD error naming the offending plane on failure. We do
|
|
50
|
+
* NOT keep the clone (the realm impl receives the original by value/marshal); the
|
|
51
|
+
* clone is purely the serializability gate. A function, DOM node, live runner,
|
|
52
|
+
* class instance with methods, etc. throws — exactly the refs a marshalling host (the
|
|
53
|
+
* hosted-eyes page-evaluate seam, a BYO realm host) cannot carry. */
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Assert that `value` is structured-clone-serializable.
|
|
57
|
+
*
|
|
58
|
+
* Throws a `TypeError` (FAIL-LOUD) if the value cannot be cloned — e.g. a function,
|
|
59
|
+
* DOM node, live runner handle, or class instance with methods. Falls back to a
|
|
60
|
+
* JSON round-trip on runtimes without `structuredClone` (Node < 17 / old browsers).
|
|
61
|
+
*
|
|
62
|
+
* @param {*} value — the data-plane payload to validate.
|
|
63
|
+
* @param {string} [ctx] — context label for the error message (e.g. `'inject("x") payload'`).
|
|
64
|
+
* @returns {*} `value` unchanged (pass-through for inline use).
|
|
65
|
+
* @throws {TypeError} if the value is not structured-clone-serializable.
|
|
66
|
+
*/
|
|
67
|
+
export function assertSerializable(value, ctx) {
|
|
68
|
+
var label = ctx || 'data-plane payload';
|
|
69
|
+
if (typeof structuredClone !== 'function') {
|
|
70
|
+
// No structuredClone (very old runtime): fall back to a JSON round-trip,
|
|
71
|
+
// which is STRICTER (also rejects functions/undefined) — still FAIL-LOUD,
|
|
72
|
+
// never a silent pass.
|
|
73
|
+
try {
|
|
74
|
+
JSON.parse(JSON.stringify(value));
|
|
75
|
+
} catch (e) {
|
|
76
|
+
throw new TypeError('[view-handle] ' + label + ' is not serializable (JSON fallback): ' +
|
|
77
|
+
(e && e.message ? e.message : String(e)) + ' (VETO-CLASS #1 — the data plane must be ' +
|
|
78
|
+
'structured-clone-serializable so a marshalling host is not a fork.)');
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
structuredClone(value);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
throw new TypeError('[view-handle] ' + label + ' is not structured-clone-serializable: ' +
|
|
86
|
+
(e && e.message ? e.message : String(e)) + ' (VETO-CLASS #1 — never pass a live object ' +
|
|
87
|
+
'ref/function/DOM node across the handle data plane; a marshalling host cannot carry it.)');
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ── normalize a possibly-sync impl method into a Promise ─────────────────── */
|
|
93
|
+
function _toPromise(fn, args) {
|
|
94
|
+
try {
|
|
95
|
+
return Promise.resolve(fn.apply(null, args));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return Promise.reject(e);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Wrap a transport-specific implementation into a frozen view handle.
|
|
103
|
+
*
|
|
104
|
+
* The returned handle enforces VETO-CLASS #1 (structured-clone check) on `inject`
|
|
105
|
+
* payloads and on `on(event, cb)` emitted payloads. Every method is async-shaped.
|
|
106
|
+
* The handle is `Object.freeze`d — a cell cannot monkey-patch the contract.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} impl — the transport-specific implementation object.
|
|
109
|
+
* @param {(name: string, data: *) => void|Promise<void>} impl.inject
|
|
110
|
+
* @param {(fn: string, args: *[]) => *|Promise<*>} impl.call
|
|
111
|
+
* @param {(event: string, cb: Function) => Function} impl.on
|
|
112
|
+
* @param {() => void|Promise<void>} [impl.start]
|
|
113
|
+
* @param {() => void|Promise<void>} [impl.stop]
|
|
114
|
+
* @param {() => *|Promise<*>} [impl.step]
|
|
115
|
+
* @param {() => void|Promise<void>} impl.dispose
|
|
116
|
+
* @param {'in-app'} [impl._transport] — DERIVED transport label (truth-in-labeling).
|
|
117
|
+
* @returns {ViewHandle} frozen handle with IDENTICAL API regardless of transport.
|
|
118
|
+
* @throws {Error} if required methods (`inject`, `call`, `on`, `dispose`) are missing.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // In-app descriptor (same-origin direct scripting):
|
|
122
|
+
* const handle = makeViewHandle({
|
|
123
|
+
* _transport: 'in-app',
|
|
124
|
+
* inject: (name, data) => { window._realmEnv[name] = data; },
|
|
125
|
+
* call: (fn, args) => window._realmEnv[fn](...args),
|
|
126
|
+
* on: (ev, cb) => { /* subscribe * / return () => {}; },
|
|
127
|
+
* start: () => window._realmEnv.start(),
|
|
128
|
+
* stop: () => window._realmEnv.stop(),
|
|
129
|
+
* step: () => window._realmEnv.step(),
|
|
130
|
+
* dispose:() => { window._realmEnv = null; }
|
|
131
|
+
* });
|
|
132
|
+
*/
|
|
133
|
+
export function makeViewHandle(impl) {
|
|
134
|
+
if (!impl || typeof impl.inject !== 'function' || typeof impl.call !== 'function' ||
|
|
135
|
+
typeof impl.on !== 'function' || typeof impl.dispose !== 'function') {
|
|
136
|
+
throw new Error('[view-handle] makeViewHandle requires an impl with ' +
|
|
137
|
+
'inject/call/on/dispose (start/stop/step optional but recommended). (FAIL-LOUD.)');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var handle = {
|
|
141
|
+
// Diagnostic, DERIVED transport label (truth-in-labeling — never hardcoded
|
|
142
|
+
// by a consumer; reflects how this handle actually marshals).
|
|
143
|
+
transport: impl._transport || 'in-app',
|
|
144
|
+
|
|
145
|
+
// ── DATA plane (serializability-gated) ──────────────────────────────
|
|
146
|
+
inject: function (name, data) {
|
|
147
|
+
if (typeof name !== 'string' || !name) {
|
|
148
|
+
return Promise.reject(new Error('[view-handle] inject: name must be a non-empty string.'));
|
|
149
|
+
}
|
|
150
|
+
try { assertSerializable(data, 'inject("' + name + '") payload'); }
|
|
151
|
+
catch (e) { return Promise.reject(e); }
|
|
152
|
+
return _toPromise(impl.inject, [name, data]);
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// ── host → realm invoke + result ────────────────────────────────────
|
|
156
|
+
call: function (fn, args) {
|
|
157
|
+
return _toPromise(impl.call, [fn, args]);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// ── realm → host event stream ───────────────────────────────────────
|
|
161
|
+
// We WRAP the caller's cb so the emitted payload is asserted serializable
|
|
162
|
+
// at the boundary (VETO-CLASS #1 applies to BOTH inject AND on payloads).
|
|
163
|
+
// A non-serializable emit FAILS LOUD via the wrapper's throw — never a
|
|
164
|
+
// silent same-origin-only pass that would break under a marshalling host.
|
|
165
|
+
on: function (event, cb) {
|
|
166
|
+
if (typeof cb !== 'function') {
|
|
167
|
+
throw new Error('[view-handle] on("' + String(event) + '"): cb must be a function.');
|
|
168
|
+
}
|
|
169
|
+
var guarded = function (payload) {
|
|
170
|
+
assertSerializable(payload, 'on("' + String(event) + '") event payload');
|
|
171
|
+
cb(payload);
|
|
172
|
+
};
|
|
173
|
+
var unsub = impl.on(event, guarded);
|
|
174
|
+
return (typeof unsub === 'function') ? unsub : function () {};
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// ── CONTROL plane (exempt from serializability — no rich data) ───────
|
|
178
|
+
start: function () { return impl.start ? _toPromise(impl.start, []) : Promise.resolve(); },
|
|
179
|
+
stop: function () { return impl.stop ? _toPromise(impl.stop, []) : Promise.resolve(); },
|
|
180
|
+
step: function () { return impl.step ? _toPromise(impl.step, []) : Promise.resolve(); },
|
|
181
|
+
|
|
182
|
+
// ── teardown ────────────────────────────────────────────────────────
|
|
183
|
+
dispose: function () { return _toPromise(impl.dispose, []); }
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return Object.freeze(handle);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Duck-type a value as a frozen view handle.
|
|
191
|
+
*
|
|
192
|
+
* Used by the notebook pane to decide whether a cell's `out=`/`bind=` binding
|
|
193
|
+
* value is a realm handle to script vs a plain data binding.
|
|
194
|
+
*
|
|
195
|
+
* @param {*} v
|
|
196
|
+
* @returns {boolean} `true` if `v` has `inject`, `call`, `on`, and `dispose` functions.
|
|
197
|
+
*/
|
|
198
|
+
export function isViewHandle(v) {
|
|
199
|
+
return !!(v && typeof v === 'object' &&
|
|
200
|
+
typeof v.inject === 'function' && typeof v.call === 'function' &&
|
|
201
|
+
typeof v.on === 'function' && typeof v.dispose === 'function');
|
|
202
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* warmup — the PURE warm-up-before-derive helper for the realm→kernel binding seam
|
|
5
|
+
* NO DOM / engine / Runtime / Bus import, so a test
|
|
6
|
+
* exercises it with a fake handle WITHOUT a browser.
|
|
7
|
+
*
|
|
8
|
+
* THE PROBLEM IT SOLVES: a DECLARED canvas that `produces` a binding mounts its
|
|
9
|
+
* realm-internal runner at tick ~0. Deriving the produced value immediately (via
|
|
10
|
+
* `deriveProducedBindings`) reads the detector histogram BEFORE any particle has
|
|
11
|
+
* crossed the stage — an EMPTY `{}`. The chart then faithfully renders nothing.
|
|
12
|
+
*
|
|
13
|
+
* THE BASELINE FIX (one-shot, static): step the realm a DETERMINISTIC number of ticks
|
|
14
|
+
* through the FROZEN handle's control-plane `step()` so the detector accumulates, THEN
|
|
15
|
+
* derive + commit. The warmup count is DECLARED on the canvas (`produceAfter="N"`,
|
|
16
|
+
* parsed by format.js); when omitted it falls back to `defaultTicks`.
|
|
17
|
+
*
|
|
18
|
+
* warmUpRealm(handle, produceAfter, defaultTicks, maxTicks) → Promise<number>
|
|
19
|
+
*
|
|
20
|
+
* Returns the number of ticks actually stepped (for the host to log / a test to assert).
|
|
21
|
+
*
|
|
22
|
+
* FAIL-LOUD: a missing handle or a `handle.step` that throws PROPAGATES (the caller
|
|
23
|
+
* surfaces it as a pane overlay and commits NOTHING). A NEGATIVE / non-integer
|
|
24
|
+
* `produceAfter` (a malformed authored attr) FAILS LOUD here — never silently
|
|
25
|
+
* clamped-to-zero, which would re-introduce the empty-histogram bug under a typo.
|
|
26
|
+
*
|
|
27
|
+
* FOLLOW-UP (interactive review): the live `on('tick')` same-cell chart and
|
|
28
|
+
* arbitrary-import path supersede this static one-shot warmup. This module is the
|
|
29
|
+
* baseline only.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default warmup tick count when a `produces`-declaring canvas omits `produceAfter`.
|
|
36
|
+
* 600 is sized for the canonical double-slit stage (480x480): the SlitLightSource emits
|
|
37
|
+
* at x=20, the DetectorWall sits at x≈352 (wavelength=64), and a particle covers ≤1px/tick
|
|
38
|
+
* relative to its parent — so the leading edge needs ≥332 ticks to reach the detector, and
|
|
39
|
+
* 600 gives a comfortable margin for a clearly non-empty histogram.
|
|
40
|
+
*
|
|
41
|
+
* SINGLE SOURCE OF TRUTH: imported by notebook-pane/index.js (the in-app host), so no
|
|
42
|
+
* consumer duplicates the literal.
|
|
43
|
+
*/
|
|
44
|
+
export var DEFAULT_PRODUCE_AFTER_TICKS = 600;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hard cap on warmup ticks. Defends against an absurd authored `produceAfter` value that
|
|
48
|
+
* would starve the event loop. Imported alongside DEFAULT_PRODUCE_AFTER_TICKS.
|
|
49
|
+
*/
|
|
50
|
+
export var MAX_PRODUCE_AFTER_TICKS = 100000;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the declared `produceAfter` attribute (a verbatim string from format.js) to an
|
|
54
|
+
* integer tick count, FAIL-LOUD on a malformed value. Empty / undefined → `defaultTicks`.
|
|
55
|
+
*
|
|
56
|
+
* @param {string|number|undefined} produceAfter — the canvas `produceAfter` attribute.
|
|
57
|
+
* @param {number} defaultTicks — fallback when the attribute is empty/absent.
|
|
58
|
+
* @returns {number} a non-negative integer tick count.
|
|
59
|
+
* @throws {Error} FAIL-LOUD when the attribute is present but not a non-negative integer.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveWarmupTicks(produceAfter, defaultTicks) {
|
|
62
|
+
if (produceAfter === undefined || produceAfter === null || produceAfter === '') {
|
|
63
|
+
return defaultTicks;
|
|
64
|
+
}
|
|
65
|
+
// Accept a number or an integer-string; reject anything else FAIL-LOUD (a typo'd
|
|
66
|
+
// attribute must not silently become 0 — that re-introduces the empty histogram).
|
|
67
|
+
var n = (typeof produceAfter === 'number') ? produceAfter : Number(String(produceAfter).trim());
|
|
68
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
69
|
+
throw new Error('[warmup] produceAfter must be a non-negative integer tick count; got "' +
|
|
70
|
+
produceAfter + '". (FAIL-LOUD — a malformed warmup count would silently empty the ' +
|
|
71
|
+
'produced histogram.)');
|
|
72
|
+
}
|
|
73
|
+
return n;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Warm up the realm a deterministic number of ticks before its produced binding is derived.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} handle — the FROZEN view-handle (must expose `step()`).
|
|
80
|
+
* @param {string|number|undefined} produceAfter — the declared warmup tick count (canvas attr).
|
|
81
|
+
* @param {number} [defaultTicks=0] — fallback warmup when `produceAfter` is empty.
|
|
82
|
+
* @param {number} [maxTicks=Infinity] — hard cap (defends against an absurd authored N).
|
|
83
|
+
* @returns {Promise<number>} the number of ticks actually stepped.
|
|
84
|
+
* @throws {Error} FAIL-LOUD on a missing handle / a non-stepping handle / a malformed
|
|
85
|
+
* `produceAfter` / a `handle.step()` that throws.
|
|
86
|
+
*
|
|
87
|
+
* The handle's `step()` is async-shaped (Promise) on both transports — each step is
|
|
88
|
+
* AWAITED so the realm advances one engine tick per iteration deterministically.
|
|
89
|
+
*/
|
|
90
|
+
export async function warmUpRealm(handle, produceAfter, defaultTicks, maxTicks) {
|
|
91
|
+
if (!handle || typeof handle.step !== 'function') {
|
|
92
|
+
throw new Error('[warmup] warmUpRealm requires a frozen view-handle with step(). (FAIL-LOUD.)');
|
|
93
|
+
}
|
|
94
|
+
var want = resolveWarmupTicks(produceAfter, (typeof defaultTicks === 'number') ? defaultTicks : 0);
|
|
95
|
+
var cap = (typeof maxTicks === 'number' && isFinite(maxTicks)) ? maxTicks : Infinity;
|
|
96
|
+
var ticks = Math.min(want, cap);
|
|
97
|
+
for (var i = 0; i < ticks; i++) {
|
|
98
|
+
// CONTROL-plane step (exempt from serializability). One engine tick per await.
|
|
99
|
+
await handle.step();
|
|
100
|
+
}
|
|
101
|
+
return ticks;
|
|
102
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/page-params — the URL-param routing-table contract for canonical
|
|
5
|
+
* experiment pages (gym-controls-as-sugar item 0 — the param contract designed
|
|
6
|
+
* properly and built with the controls).
|
|
7
|
+
*
|
|
8
|
+
* Peer of `lib/shim/frame-data`, `lib/shim/expr/ast-schema`, `lib/shim/controls` —
|
|
9
|
+
* same layout convention: a closed vocabulary exported as named ES-module exports,
|
|
10
|
+
* imported by node tests, the boot module and the IDE alike.
|
|
11
|
+
*
|
|
12
|
+
* ── Why this exists ──────────────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* Page-side readers each read their OWN URL param from the page URL; the key
|
|
15
|
+
* string is never a hand-typed literal at the reader:
|
|
16
|
+
* - `lib/frqtl/experiments/_shared/fx-experiment.js` reads `seed`.
|
|
17
|
+
* - `lib/frqtl/experiments/_shared/boot.js` reads `renderer`, `engine` and the
|
|
18
|
+
* HOST-COMPOSITION token `chrome`.
|
|
19
|
+
*
|
|
20
|
+
* A `reader` need not be a `_shared` boot file: `stepping`
|
|
21
|
+
* is read by ONE experiment page's own inline sketch, because it is a
|
|
22
|
+
* per-universe PHYSICS option that page ferries into its own `universe.init()`
|
|
23
|
+
* — the registry's job is that the KEY NAME has exactly one owner, wherever the
|
|
24
|
+
* single reader lives (see that entry's own note for the hoist rule when a
|
|
25
|
+
* second page wants it).
|
|
26
|
+
*
|
|
27
|
+
* This module codifies that vocabulary as ONE table (`PAGE_PARAMS`) plus a
|
|
28
|
+
* sibling table for the host-token (`HOST_TOKENS`) — converged design decision:
|
|
29
|
+
* `chrome` differs in KIND from the other four (see HOST_TOKENS doc below), so
|
|
30
|
+
* it does NOT belong in PAGE_PARAMS even though it is also page-URL-driven.
|
|
31
|
+
*
|
|
32
|
+
* ── Delivery: an ES-module import ───────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* Every reader imports `KEYS` (or a table) from this module — `boot.js` statically,
|
|
35
|
+
* so the key names are bound before the page's boot runs; no reader carries a
|
|
36
|
+
* literal copy of a key.
|
|
37
|
+
*
|
|
38
|
+
* ── page-params is a STATIC ROUTING REGISTRY, not a control schema ──────────
|
|
39
|
+
*
|
|
40
|
+
* Unlike `lib/shim/controls` (live, runtime-mutable descriptors with a
|
|
41
|
+
* throw-or-apply `normalize()`), page-params is LOAD-TIME-ONLY: every entry is
|
|
42
|
+
* read exactly once, at page boot, from the URL. There is deliberately NO
|
|
43
|
+
* `normalize()` here — the tables are frozen static data, not runtime input to
|
|
44
|
+
* validate. The two contracts are ADJACENT (both are consumed by the same
|
|
45
|
+
* experiment pages, and the controls-panel work touches both) but are
|
|
46
|
+
* NEVER unified into one schema/kind-union.
|
|
47
|
+
*
|
|
48
|
+
* Usage:
|
|
49
|
+
* import { KEYS, PAGE_PARAMS, HOST_TOKENS, FORWARDABLE_PARAMS } from 'lib/shim/page-params/index.js';
|
|
50
|
+
* var seed = new URLSearchParams(location.search).get(KEYS.SEED);
|
|
51
|
+
* // host-side (features/apps, forwarding a deep link into an embedded iframe):
|
|
52
|
+
* FORWARDABLE_PARAMS.forEach(function (key) { ... });
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* KEYS — the canonical URL param key strings, named for ergonomic/live-import
|
|
59
|
+
* access (`window.PageParams.KEYS.SEED` reads better at a call site than
|
|
60
|
+
* `window.PageParams.PAGE_PARAMS[0].key`). Every `PAGE_PARAMS` entry's `key`
|
|
61
|
+
* field references one of these — same value, same reference — so `KEYS` and
|
|
62
|
+
* `PAGE_PARAMS` can never drift apart.
|
|
63
|
+
*/
|
|
64
|
+
var KEYS = Object.freeze({
|
|
65
|
+
SEED: 'seed',
|
|
66
|
+
RENDERER: 'renderer',
|
|
67
|
+
ENGINE: 'engine',
|
|
68
|
+
STEPPING: 'stepping'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* PAGE_PARAMS — every URL param a canonical experiment page's SHARED reader
|
|
73
|
+
* modules accept, `{ key, reader, forward, why? }`:
|
|
74
|
+
* key — one of KEYS.
|
|
75
|
+
* reader — a repo-root-relative path to the file that reads this key. The
|
|
76
|
+
* conformance test derives its per-key source-file assertion FROM
|
|
77
|
+
* this field — never a test-side hand map (schema-mirror-drift
|
|
78
|
+
* discipline: the owner exports the reader location, consumers/
|
|
79
|
+
* tests ferry it).
|
|
80
|
+
* forward — true: a HOST embedding the page in an iframe (the GYM, a future
|
|
81
|
+
* control panel, …) may forward this key from its own URL.
|
|
82
|
+
* false: never forward (see `why`).
|
|
83
|
+
* why — REQUIRED when forward is false (conformance-checked).
|
|
84
|
+
*/
|
|
85
|
+
var PAGE_PARAMS = Object.freeze([
|
|
86
|
+
Object.freeze({
|
|
87
|
+
key: KEYS.SEED,
|
|
88
|
+
reader: 'lib/frqtl/experiments/_shared/fx-experiment.js',
|
|
89
|
+
forward: true
|
|
90
|
+
}),
|
|
91
|
+
Object.freeze({
|
|
92
|
+
key: KEYS.RENDERER,
|
|
93
|
+
reader: 'lib/frqtl/experiments/_shared/boot.js',
|
|
94
|
+
forward: true
|
|
95
|
+
}),
|
|
96
|
+
Object.freeze({
|
|
97
|
+
key: KEYS.STEPPING,
|
|
98
|
+
reader: 'lib/frqtl/experiments/single-source-interference/index.html',
|
|
99
|
+
forward: true
|
|
100
|
+
// Circulation §2: `stepping`
|
|
101
|
+
// selects the WAVE-WALK STEPPING SAMPLER for one universe —
|
|
102
|
+
// 'fixed' (deterministic error-accumulator walk, the engine's serde default)
|
|
103
|
+
// vs 'average' (the historic per-axis Bernoulli walk). It is a PER-UNIVERSE
|
|
104
|
+
// PHYSICS option, so it is read at the PAGE plane and ferried RAW into that
|
|
105
|
+
// page's own `universe.init(..., { stepping })` options — deliberately NOT by
|
|
106
|
+
// the boot module, which selects the BACKEND (`?engine=`, forward:false). Putting
|
|
107
|
+
// a physics knob in the backend selection would conflate the two axes.
|
|
108
|
+
//
|
|
109
|
+
// forward:true — unlike `?engine=`, no host owns a competing stepping authority,
|
|
110
|
+
// and a host embedding this page for an A/B (the GYM Compare shape) forwarding
|
|
111
|
+
// its own `?stepping=` is exactly the intended use (the `?renderer=`/`?seed=`
|
|
112
|
+
// precedent). The value is NEVER validated or defaulted page-side: an absent
|
|
113
|
+
// param means the key is omitted from the init bag so the ENGINE's default
|
|
114
|
+
// (the Rust `Stepping` serde enum, `Fixed`) is the ONE authority for it, and a
|
|
115
|
+
// garbage value rides through to `universe.init()`, which throws naming both
|
|
116
|
+
// legal samplers (one validator, zero second interpreters).
|
|
117
|
+
//
|
|
118
|
+
// SCALING (pre-registered, not built): a PAGE_PARAMS entry names ONE reader.
|
|
119
|
+
// When a SECOND page consumes `?stepping=`, HOIST the read into one shared
|
|
120
|
+
// reader module and repoint this entry — never a second entry for the same
|
|
121
|
+
// key, never a copy-pasted inline read.
|
|
122
|
+
}),
|
|
123
|
+
Object.freeze({
|
|
124
|
+
key: KEYS.ENGINE,
|
|
125
|
+
reader: 'lib/frqtl/experiments/_shared/boot.js',
|
|
126
|
+
forward: false,
|
|
127
|
+
why: 'the host\'s own physics-engine selector (e.g. the GYM\'s Benchmark/' +
|
|
128
|
+
'Compare engine dropdown) is the SOLE authority for this axis; ' +
|
|
129
|
+
'forwarding an outer ?engine= would let it silently override an ' +
|
|
130
|
+
'explicit host choice, which is exactly the engine-sniffing-from-' +
|
|
131
|
+
'above-the-shim class of bug this codebase forbids elsewhere'
|
|
132
|
+
})
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* HOST_TOKENS — host-composition tokens: they change what the PAGE's own chrome
|
|
137
|
+
* renders, never a physics or render parameter, so the `{key, reader, forward,
|
|
138
|
+
* why}` shape does not fit them: `{ key, appliedBy, forward, why, acceptedValues }`.
|
|
139
|
+
* appliedBy — the file that applies the token.
|
|
140
|
+
* acceptedValues — the CLOSED set of legal values for this token (frozen
|
|
141
|
+
* array). The single canonical vocabulary — never
|
|
142
|
+
* hand-duplicated by a consumer (`lib/features/ide/presets.js`'s
|
|
143
|
+
* `withHostChrome` and `boot.js`'s `applyChrome` both read
|
|
144
|
+
* THIS list).
|
|
145
|
+
*
|
|
146
|
+
* ── WHEN should a host set `chrome=canvas`? (the criterion, canonical home) ──
|
|
147
|
+
*
|
|
148
|
+
* A host sets `chrome=canvas` IFF its own chrome REPLICATES-OR-SUPERSETS the
|
|
149
|
+
* embedded experiment page's own control surface — i.e. the host provides an
|
|
150
|
+
* equivalent (or richer) way to reach every knob the experiment's own
|
|
151
|
+
* `.experiment-controls`/metrics chrome would otherwise show, so showing the
|
|
152
|
+
* page's raw chrome bar inside the host's viewport would be pure redundancy,
|
|
153
|
+
* never a loss of capability. Two worked, OPPOSITE applications of this ONE
|
|
154
|
+
* rule (read them side by side — this is not "some hosts hide chrome,
|
|
155
|
+
* others don't" as a per-host preference; it is one test with two different
|
|
156
|
+
* answers):
|
|
157
|
+
* - The GYM (`CompareView.jsx`):
|
|
158
|
+
* OMITS `chrome=` entirely (full chrome renders in-iframe). The GYM's own
|
|
159
|
+
* host chrome is deliberately minimal (Run/Pause/camera/rates only) — it
|
|
160
|
+
* does NOT replicate the physics-parametric controls (temperature,
|
|
161
|
+
* wavelength, density sweeps, …) every experiment declares, so hiding the
|
|
162
|
+
* experiment's own chrome would strictly REMOVE capability from the GYM
|
|
163
|
+
* user with no host-side equivalent to fall back on.
|
|
164
|
+
* - Studio (`AuthorView.jsx`, "Studio adoption"): SETS
|
|
165
|
+
* `chrome=canvas`. Studio's own chrome already replicates-or-supersets
|
|
166
|
+
* the experiment's control surface for its purposes: a Design/Run/Pause
|
|
167
|
+
* topbar, an Effects palette (backdrop/water toggles), and a fully
|
|
168
|
+
* generic per-object schema Inspector that can edit ANY field of ANY
|
|
169
|
+
* authored object — a SUPERSET of what any single experiment's own
|
|
170
|
+
* control bar could show. Showing the raw chrome bar too would be
|
|
171
|
+
* redundant and cramped inside Studio's constrained canvas pane, exactly
|
|
172
|
+
* the "gallery-tile-style, host-chrome-owns-everything" case this token
|
|
173
|
+
* was built for.
|
|
174
|
+
* A future host applies the SAME test: does my own chrome already cover
|
|
175
|
+
* (or exceed) what the embedded page's chrome offers? If yes → `chrome=
|
|
176
|
+
* canvas`. If no (the host chrome is thinner than the experiment's own) →
|
|
177
|
+
* omit `chrome=` and let the experiment's own chrome render.
|
|
178
|
+
*/
|
|
179
|
+
var HOST_TOKENS = Object.freeze([
|
|
180
|
+
Object.freeze({
|
|
181
|
+
key: 'chrome',
|
|
182
|
+
appliedBy: 'lib/frqtl/experiments/_shared/boot.js',
|
|
183
|
+
forward: false,
|
|
184
|
+
acceptedValues: Object.freeze(['canvas']),
|
|
185
|
+
why: 'chrome is a HOST-COMPOSITION decision (whether the experiment\'s own ' +
|
|
186
|
+
'control chrome DOM renders), never a per-experiment physics/render ' +
|
|
187
|
+
'parameter; forwarding it from an outer host URL into an embedded ' +
|
|
188
|
+
'iframe would reintroduce the hidden-experiment-chrome bug fixed in ' +
|
|
189
|
+
'WS gym-controls-as-sugar Bite 2 (the GYM must always show the ' +
|
|
190
|
+
'experiment\'s own full chrome, never chrome=canvas — see the ' +
|
|
191
|
+
'"WHEN should a host set chrome=canvas" doc above for the general ' +
|
|
192
|
+
'criterion, not just the GYM\'s specific answer to it)'
|
|
193
|
+
})
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* FORWARDABLE_PARAMS — derived: every PAGE_PARAMS key with forward:true, in
|
|
198
|
+
* table order. THE ONLY list a host may use to forward params into an embedded
|
|
199
|
+
* experiment-page iframe (`lib/features/ide/presets.js` `buildExperimentFrameSrc`
|
|
200
|
+
* consumes this — no host re-derives its own hand list).
|
|
201
|
+
*/
|
|
202
|
+
var FORWARDABLE_PARAMS = Object.freeze(
|
|
203
|
+
PAGE_PARAMS.filter(function (p) { return p.forward; }).map(function (p) { return p.key; })
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
export {
|
|
209
|
+
KEYS,
|
|
210
|
+
PAGE_PARAMS,
|
|
211
|
+
HOST_TOKENS,
|
|
212
|
+
FORWARDABLE_PARAMS
|
|
213
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* identity.js — THE project identity: one published function over a project's files.
|
|
5
|
+
*
|
|
6
|
+
* projectIdentity(project) → Promise<string> lowercase hex sha256
|
|
7
|
+
* project — { files: { '<path>': Uint8Array, … } } — BYTES, never text (any other keys are ignored)
|
|
8
|
+
* projectBytes(files) → { '<path>': Uint8Array } the bytes a { path: text } map denotes (UTF-8)
|
|
9
|
+
*
|
|
10
|
+
* A host passes the bytes it ran: the linker hashes the bytes it fetched (a served folder) or the UTF-8
|
|
11
|
+
* encoding of the text it was handed (a file map, projectBytes); node hands a file's Buffer. A BOM is part
|
|
12
|
+
* of a file's bytes, so it is part of the identity (a decoder strips it only from the executed text).
|
|
13
|
+
*
|
|
14
|
+
* The byte recipe (one recipe from the first pre-release on; there is no normalization):
|
|
15
|
+
* for each file, in ascending BYTE order of its path (UTF-8),
|
|
16
|
+
* the path (UTF-8, '/'-separated, root-relative, no leading './') then a 0x00 byte
|
|
17
|
+
* the decimal byte length of the file (ASCII digits) then a 0x00 byte
|
|
18
|
+
* the file's raw bytes
|
|
19
|
+
* then the sha256 of the whole stream.
|
|
20
|
+
*
|
|
21
|
+
* Swapping the order of the input keys cannot change the value (the path order is canonical); one
|
|
22
|
+
* flipped byte, or a dropped trailing newline, does. A path that is empty, absolute, starts with './'
|
|
23
|
+
* or climbs with '..' is REFUSED, never repaired: a repaired path would hash a project the caller did
|
|
24
|
+
* not hand in.
|
|
25
|
+
*
|
|
26
|
+
* Hashing is the platform's `crypto.subtle` (a global in a page and in node ≥ 19): one source, no host
|
|
27
|
+
* branch. Gate: lib/shim/project/test/identity.test.mjs (an independent printf | shasum pipeline).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const _enc = new TextEncoder();
|
|
31
|
+
|
|
32
|
+
function _fail(msg) {
|
|
33
|
+
throw new Error('[identity] ' + msg + ' (FAIL-LOUD.)');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {Uint8Array} a @param {Uint8Array} b @returns {number} byte-order comparison */
|
|
37
|
+
function _byteCompare(a, b) {
|
|
38
|
+
const n = Math.min(a.length, b.length);
|
|
39
|
+
for (let i = 0; i < n; i++) { if (a[i] !== b[i]) { return a[i] - b[i]; } }
|
|
40
|
+
return a.length - b.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* THE project-path rule (identity and the linker both apply it): root-relative, '/'-separated, no
|
|
45
|
+
* leading './', no empty / '.' / '..' segment. THROWS naming the path.
|
|
46
|
+
* @param {string} p
|
|
47
|
+
*/
|
|
48
|
+
export function checkProjectPath(p) {
|
|
49
|
+
if (!p) { _fail('a project file has an empty path.'); }
|
|
50
|
+
if (p.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(p)) { _fail('"' + p + '" is absolute — a project path is root-relative.'); }
|
|
51
|
+
if (p.startsWith('./')) { _fail('"' + p + '" starts with "./" — a project path carries no leading "./".'); }
|
|
52
|
+
if (p.indexOf('\\') !== -1) { _fail('"' + p + '" uses "\\" — a project path is "/"-separated.'); }
|
|
53
|
+
if (p.split('/').some((seg) => seg === '' || seg === '.' || seg === '..')) {
|
|
54
|
+
_fail('"' + p + '" has an empty, "." or ".." segment — a project path names one file under the root.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The bytes a { path: text } map denotes: each text UTF-8 encoded. THROWS on a non-string value.
|
|
60
|
+
* @param {Object<string, string>} files
|
|
61
|
+
* @returns {Object<string, Uint8Array>}
|
|
62
|
+
*/
|
|
63
|
+
export function projectBytes(files) {
|
|
64
|
+
if (!files || typeof files !== 'object') {
|
|
65
|
+
_fail('projectBytes(files): files is the map { path: text } (got ' + typeof files + ').');
|
|
66
|
+
}
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const p of Object.keys(files)) {
|
|
69
|
+
if (typeof files[p] !== 'string') { _fail('projectBytes: file "' + p + '" is not text (got ' + typeof files[p] + ').'); }
|
|
70
|
+
out[p] = _enc.encode(files[p]);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {{files: Object<string, Uint8Array>}} project
|
|
77
|
+
* @returns {Promise<string>} the lowercase hex sha256
|
|
78
|
+
*/
|
|
79
|
+
export async function projectIdentity(project) {
|
|
80
|
+
const files = project && project.files;
|
|
81
|
+
if (!files || typeof files !== 'object') {
|
|
82
|
+
_fail('projectIdentity(project): project.files is the map { path: bytes } (got ' + typeof files + ').');
|
|
83
|
+
}
|
|
84
|
+
const rows = Object.keys(files).map((p) => {
|
|
85
|
+
checkProjectPath(p);
|
|
86
|
+
const v = files[p];
|
|
87
|
+
if (!(v instanceof Uint8Array)) {
|
|
88
|
+
_fail('file "' + p + '" is not bytes (got ' + typeof v + ') — the identity is over the bytes a host ran; ' +
|
|
89
|
+
'a { path: text } map goes through projectBytes(files) first.');
|
|
90
|
+
}
|
|
91
|
+
return { path: _enc.encode(p), bytes: v };
|
|
92
|
+
});
|
|
93
|
+
if (rows.length === 0) { _fail('projectIdentity(project): the project has no files.'); }
|
|
94
|
+
rows.sort((a, b) => _byteCompare(a.path, b.path));
|
|
95
|
+
const chunks = [];
|
|
96
|
+
let total = 0;
|
|
97
|
+
for (const r of rows) {
|
|
98
|
+
const len = _enc.encode(String(r.bytes.length));
|
|
99
|
+
for (const c of [r.path, Uint8Array.of(0), len, Uint8Array.of(0), r.bytes]) { chunks.push(c); total += c.length; }
|
|
100
|
+
}
|
|
101
|
+
const stream = new Uint8Array(total);
|
|
102
|
+
let at = 0;
|
|
103
|
+
for (const c of chunks) { stream.set(c, at); at += c.length; }
|
|
104
|
+
const subtle = (typeof crypto !== 'undefined') ? crypto.subtle : null;
|
|
105
|
+
if (!subtle || typeof subtle.digest !== 'function') {
|
|
106
|
+
_fail('crypto.subtle.digest is unavailable — the project identity is a content hash with no second hashing path.');
|
|
107
|
+
}
|
|
108
|
+
const view = new Uint8Array(await subtle.digest('SHA-256', stream));
|
|
109
|
+
let hex = '';
|
|
110
|
+
for (let i = 0; i < view.length; i++) { hex += (view[i] < 16 ? '0' : '') + view[i].toString(16); }
|
|
111
|
+
return hex;
|
|
112
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* project/index.js — the `./project` entry of BOTH packages: the specifier table, the link site and the
|
|
5
|
+
* project identity, re-exported from their owners (this file owns nothing).
|
|
6
|
+
*
|
|
7
|
+
* FRQTL_WEB_SPECIFIERS, libPathOf, importMapFor specifiers.js
|
|
8
|
+
* linkProject link.js
|
|
9
|
+
* projectIdentity identity.js
|
|
10
|
+
*
|
|
11
|
+
* PROJECT_EXPORTS is this entry's export list; a conformance test holds the module's exports equal to
|
|
12
|
+
* it, and the package gate holds it equal across @descix/frqtl-web and @descix/frqtl-node.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const PROJECT_EXPORTS = Object.freeze(['FRQTL_WEB_SPECIFIERS', 'libPathOf', 'importMapFor', 'linkProject', 'projectIdentity']);
|
|
16
|
+
|
|
17
|
+
export { FRQTL_WEB_SPECIFIERS, libPathOf, importMapFor } from './specifiers.js';
|
|
18
|
+
export { linkProject } from './link.js';
|
|
19
|
+
export { projectIdentity } from './identity.js';
|