@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -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
+ * (WS-NOTEBOOK-BASELINE FIX B). 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 (CEO 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,4 @@
1
+ {
2
+ "private": true,
3
+ "type": "module"
4
+ }
@@ -0,0 +1,214 @@
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 (WS gym-controls-as-sugar Bite 3 item 0 — "the CEO ruled the
6
+ * param contract gets designed properly now and built WITH Bite 3", FINAL
7
+ * design circulated + converged).
8
+ *
9
+ * Peer of `lib/shim/frame-data`, `lib/shim/expr/ast-schema`, `lib/shim/controls` —
10
+ * same layout convention: a closed vocabulary exported as named ES-module exports,
11
+ * imported by node tests, the boot module and the IDE alike.
12
+ *
13
+ * ── Why this exists ──────────────────────────────────────────────────────────
14
+ *
15
+ * Page-side readers each read their OWN URL param from the page URL; the key
16
+ * string is never a hand-typed literal at the reader:
17
+ * - `lib/frqtl/experiments/_shared/fx-experiment.js` reads `seed`.
18
+ * - `lib/frqtl/experiments/_shared/boot.js` reads `renderer`, `engine` and the
19
+ * HOST-COMPOSITION token `chrome`.
20
+ *
21
+ * A `reader` need not be a `_shared` boot file: `stepping` (ws-walk-unification
22
+ * Bite 1) is read by ONE experiment page's own inline sketch, because it is a
23
+ * per-universe PHYSICS option that page ferries into its own `universe.init()`
24
+ * — the registry's job is that the KEY NAME has exactly one owner, wherever the
25
+ * single reader lives (see that entry's own note for the hoist rule when a
26
+ * second page wants it).
27
+ *
28
+ * This module codifies that vocabulary as ONE table (`PAGE_PARAMS`) plus a
29
+ * sibling table for the host-token (`HOST_TOKENS`) — converged design decision:
30
+ * `chrome` differs in KIND from the other four (see HOST_TOKENS doc below), so
31
+ * it does NOT belong in PAGE_PARAMS even though it is also page-URL-driven.
32
+ *
33
+ * ── Delivery: an ES-module import ───────────────────────────────────────────
34
+ *
35
+ * Every reader imports `KEYS` (or a table) from this module — `boot.js` statically,
36
+ * so the key names are bound before the page's boot runs; no reader carries a
37
+ * literal copy of a key.
38
+ *
39
+ * ── page-params is a STATIC ROUTING REGISTRY, not a control schema ──────────
40
+ *
41
+ * Unlike `lib/shim/controls` (live, runtime-mutable descriptors with a
42
+ * throw-or-apply `normalize()`), page-params is LOAD-TIME-ONLY: every entry is
43
+ * read exactly once, at page boot, from the URL. There is deliberately NO
44
+ * `normalize()` here — the tables are frozen static data, not runtime input to
45
+ * validate. The two contracts are ADJACENT (both are consumed by the same
46
+ * experiment pages, and Bite-3's controls-panel work will touch both) but are
47
+ * NEVER unified into one schema/kind-union.
48
+ *
49
+ * Usage:
50
+ * import { KEYS, PAGE_PARAMS, HOST_TOKENS, FORWARDABLE_PARAMS } from 'lib/shim/page-params/index.js';
51
+ * var seed = new URLSearchParams(location.search).get(KEYS.SEED);
52
+ * // host-side (features/apps, forwarding a deep link into an embedded iframe):
53
+ * FORWARDABLE_PARAMS.forEach(function (key) { ... });
54
+ */
55
+
56
+
57
+
58
+ /**
59
+ * KEYS — the canonical URL param key strings, named for ergonomic/live-import
60
+ * access (`window.PageParams.KEYS.SEED` reads better at a call site than
61
+ * `window.PageParams.PAGE_PARAMS[0].key`). Every `PAGE_PARAMS` entry's `key`
62
+ * field references one of these — same value, same reference — so `KEYS` and
63
+ * `PAGE_PARAMS` can never drift apart.
64
+ */
65
+ var KEYS = Object.freeze({
66
+ SEED: 'seed',
67
+ RENDERER: 'renderer',
68
+ ENGINE: 'engine',
69
+ STEPPING: 'stepping'
70
+ });
71
+
72
+ /**
73
+ * PAGE_PARAMS — every URL param a canonical experiment page's SHARED reader
74
+ * modules accept, `{ key, reader, forward, why? }`:
75
+ * key — one of KEYS.
76
+ * reader — a repo-root-relative path to the file that reads this key. The
77
+ * conformance test derives its per-key source-file assertion FROM
78
+ * this field — never a test-side hand map (schema-mirror-drift
79
+ * discipline: the owner exports the reader location, consumers/
80
+ * tests ferry it).
81
+ * forward — true: a HOST embedding the page in an iframe (the GYM, a future
82
+ * control panel, …) may forward this key from its own URL.
83
+ * false: never forward (see `why`).
84
+ * why — REQUIRED when forward is false (conformance-checked).
85
+ */
86
+ var PAGE_PARAMS = Object.freeze([
87
+ Object.freeze({
88
+ key: KEYS.SEED,
89
+ reader: 'lib/frqtl/experiments/_shared/fx-experiment.js',
90
+ forward: true
91
+ }),
92
+ Object.freeze({
93
+ key: KEYS.RENDERER,
94
+ reader: 'lib/frqtl/experiments/_shared/boot.js',
95
+ forward: true
96
+ }),
97
+ Object.freeze({
98
+ key: KEYS.STEPPING,
99
+ reader: 'lib/frqtl/experiments/single-source-interference/index.html',
100
+ forward: true
101
+ // ws-walk-unification Bite 1 (architect circulation ruling §2). `stepping`
102
+ // selects the WAVE-WALK STEPPING SAMPLER for one universe —
103
+ // 'fixed' (deterministic error-accumulator walk, the engine's serde default)
104
+ // vs 'average' (the historic per-axis Bernoulli walk). It is a PER-UNIVERSE
105
+ // PHYSICS option, so it is read at the PAGE plane and ferried RAW into that
106
+ // page's own `universe.init(..., { stepping })` options — deliberately NOT by
107
+ // the boot module, which selects the BACKEND (`?engine=`, forward:false). Putting
108
+ // a physics knob in the backend selection would conflate the two axes.
109
+ //
110
+ // forward:true — unlike `?engine=`, no host owns a competing stepping authority,
111
+ // and a host embedding this page for an A/B (the GYM Compare shape) forwarding
112
+ // its own `?stepping=` is exactly the intended use (the `?renderer=`/`?seed=`
113
+ // precedent). The value is NEVER validated or defaulted page-side: an absent
114
+ // param means the key is omitted from the init bag so the ENGINE's default
115
+ // (the Rust `Stepping` serde enum, `Fixed`) is the ONE authority for it, and a
116
+ // garbage value rides through to `universe.init()`, which throws naming both
117
+ // legal samplers (one validator, zero second interpreters).
118
+ //
119
+ // SCALING (pre-registered, not built): a PAGE_PARAMS entry names ONE reader.
120
+ // When a SECOND page consumes `?stepping=`, HOIST the read into one shared
121
+ // reader module and repoint this entry — never a second entry for the same
122
+ // key, never a copy-pasted inline read.
123
+ }),
124
+ Object.freeze({
125
+ key: KEYS.ENGINE,
126
+ reader: 'lib/frqtl/experiments/_shared/boot.js',
127
+ forward: false,
128
+ why: 'the host\'s own physics-engine selector (e.g. the GYM\'s Benchmark/' +
129
+ 'Compare engine dropdown) is the SOLE authority for this axis; ' +
130
+ 'forwarding an outer ?engine= would let it silently override an ' +
131
+ 'explicit host choice, which is exactly the engine-sniffing-from-' +
132
+ 'above-the-shim class of bug this codebase forbids elsewhere'
133
+ })
134
+ ]);
135
+
136
+ /**
137
+ * HOST_TOKENS — host-composition tokens: they change what the PAGE's own chrome
138
+ * renders, never a physics or render parameter, so the `{key, reader, forward,
139
+ * why}` shape does not fit them: `{ key, appliedBy, forward, why, acceptedValues }`.
140
+ * appliedBy — the file that applies the token.
141
+ * acceptedValues — the CLOSED set of legal values for this token (frozen
142
+ * array). The single canonical vocabulary — never
143
+ * hand-duplicated by a consumer (`lib/features/ide/presets.js`'s
144
+ * `withHostChrome` and `boot.js`'s `applyChrome` both read
145
+ * THIS list).
146
+ *
147
+ * ── WHEN should a host set `chrome=canvas`? (the criterion, canonical home) ──
148
+ *
149
+ * A host sets `chrome=canvas` IFF its own chrome REPLICATES-OR-SUPERSETS the
150
+ * embedded experiment page's own control surface — i.e. the host provides an
151
+ * equivalent (or richer) way to reach every knob the experiment's own
152
+ * `.experiment-controls`/metrics chrome would otherwise show, so showing the
153
+ * page's raw chrome bar inside the host's viewport would be pure redundancy,
154
+ * never a loss of capability. Two worked, OPPOSITE applications of this ONE
155
+ * rule (read them side by side — this is not "some hosts hide chrome,
156
+ * others don't" as a per-host preference; it is one test with two different
157
+ * answers):
158
+ * - The GYM (`CompareView.jsx`, WS gym-controls-as-sugar Bite 2 ruling):
159
+ * OMITS `chrome=` entirely (full chrome renders in-iframe). The GYM's own
160
+ * host chrome is deliberately minimal (Run/Pause/camera/rates only) — it
161
+ * does NOT replicate the physics-parametric controls (temperature,
162
+ * wavelength, density sweeps, …) every experiment declares, so hiding the
163
+ * experiment's own chrome would strictly REMOVE capability from the GYM
164
+ * user with no host-side equivalent to fall back on.
165
+ * - Studio (`AuthorView.jsx`, Bite 4 "Studio adoption"): SETS
166
+ * `chrome=canvas`. Studio's own chrome already replicates-or-supersets
167
+ * the experiment's control surface for its purposes: a Design/Run/Pause
168
+ * topbar, an Effects palette (backdrop/water toggles), and a fully
169
+ * generic per-object schema Inspector that can edit ANY field of ANY
170
+ * authored object — a SUPERSET of what any single experiment's own
171
+ * control bar could show. Showing the raw chrome bar too would be
172
+ * redundant and cramped inside Studio's constrained canvas pane, exactly
173
+ * the "gallery-tile-style, host-chrome-owns-everything" case this token
174
+ * was built for.
175
+ * A future host applies the SAME test: does my own chrome already cover
176
+ * (or exceed) what the embedded page's chrome offers? If yes → `chrome=
177
+ * canvas`. If no (the host chrome is thinner than the experiment's own) →
178
+ * omit `chrome=` and let the experiment's own chrome render.
179
+ */
180
+ var HOST_TOKENS = Object.freeze([
181
+ Object.freeze({
182
+ key: 'chrome',
183
+ appliedBy: 'lib/frqtl/experiments/_shared/boot.js',
184
+ forward: false,
185
+ acceptedValues: Object.freeze(['canvas']),
186
+ why: 'chrome is a HOST-COMPOSITION decision (whether the experiment\'s own ' +
187
+ 'control chrome DOM renders), never a per-experiment physics/render ' +
188
+ 'parameter; forwarding it from an outer host URL into an embedded ' +
189
+ 'iframe would reintroduce the hidden-experiment-chrome bug fixed in ' +
190
+ 'WS gym-controls-as-sugar Bite 2 (the GYM must always show the ' +
191
+ 'experiment\'s own full chrome, never chrome=canvas — see the ' +
192
+ '"WHEN should a host set chrome=canvas" doc above for the general ' +
193
+ 'criterion, not just the GYM\'s specific answer to it)'
194
+ })
195
+ ]);
196
+
197
+ /**
198
+ * FORWARDABLE_PARAMS — derived: every PAGE_PARAMS key with forward:true, in
199
+ * table order. THE ONLY list a host may use to forward params into an embedded
200
+ * experiment-page iframe (`lib/features/ide/presets.js` `buildExperimentFrameSrc`
201
+ * consumes this — no host re-derives its own hand list).
202
+ */
203
+ var FORWARDABLE_PARAMS = Object.freeze(
204
+ PAGE_PARAMS.filter(function (p) { return p.forward; }).map(function (p) { return p.key; })
205
+ );
206
+
207
+ // ── Export ────────────────────────────────────────────────────────────────────
208
+
209
+ export {
210
+ KEYS,
211
+ PAGE_PARAMS,
212
+ HOST_TOKENS,
213
+ FORWARDABLE_PARAMS
214
+ };
@@ -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';