@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,453 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * capture-host.js — THE CAPTURE-HOST KIND: a stage executor that runs REMOTELY.
5
+ *
6
+ * Peer of `capture.js` in this directory (the tap MECHANISM) and deliberately narrower:
7
+ * `capture.js` owns dispatch, throw rules and provenance for stages this process can
8
+ * execute; a CAPTURE HOST executes a stage in ANOTHER process — a managed headless
9
+ * browser running the real render stack — and hands back a bag that was already
10
+ * normalized THERE. This file is the contract that makes that safe: the KIND's shape,
11
+ * the ONE boundary codec, and the verification that proves the two processes are
12
+ * talking about the SAME scene.
13
+ *
14
+ * Contract text: docs/internal/engine/RENDERER_CONTRACT.md §9 (the capture-host KIND) and
15
+ * §10.3 (the normalized format this ferries).
16
+ *
17
+ * ── Why a distinct KIND and not a provider-shaped adapter (architect ruling, V3) ──
18
+ *
19
+ * A `RendererProvider` adapter would have been the tempting shape: implement
20
+ * `captureStill()` in node, forward to the browser, let the local dispatcher stamp the
21
+ * provenance. It is WRONG, and not by taste — the local process has none of the facts
22
+ * the provenance is made of. It has no tick (the world is in the page), no pose (the
23
+ * camera is in the page), no ordered filter list, no render options, no canvas dims. A
24
+ * provider-shaped adapter would therefore have had to INVENT them, which is the
25
+ * caller-supplied-provenance anti-pattern at industrial scale: a corpus row that looks
26
+ * fully stamped and is fiction.
27
+ *
28
+ * So the KIND inverts the arrow. **The IN-PAGE dispatcher stamps everything** (it is a
29
+ * real `FrqtlCapture` over a real loop and a real provider — the ONE authority, the same
30
+ * code the browser user runs), and the node side's job is not to stamp but to VERIFY:
31
+ * every still it hands back must carry the SAME scene identity the node side computed
32
+ * independently. Measured fidelity, not trust.
33
+ *
34
+ * ── The KIND (what an implementation must be) ─────────────────────────────────
35
+ *
36
+ * {
37
+ * kind: 'capture-host', // the KIND tag — asserted, never inferred
38
+ * stages: string[], // stages this executor executes REMOTELY
39
+ * identity(): { build, projection }, // SYNC — resolved once at open()
40
+ * capture(stage): Promise<Still>, // a FULLY NORMALIZED bag, stamped remotely
41
+ * record(opts): Promise<VideoEnvelope>, // FrqtlVision.record, run where the world is
42
+ * // (opts.signal aborts it across the boundary)
43
+ * dispose(): Promise<void> // symmetric with whatever open() acquired
44
+ * }
45
+ *
46
+ * `record` FORWARDS to the one recorder, `FrqtlVision.record`, running in the host's page
47
+ * over its own runner — the tick listener, the hold, the stills and the encoder are all
48
+ * there. The host adds no recording logic; the node side re-validates the returned envelope
49
+ * against its owner and VERIFIES its scene (`verifyVideo`), exactly as it verifies a still.
50
+ *
51
+ * `identity()` is SYNCHRONOUS on purpose: `sceneRecord()` builds its record from live
52
+ * field reads and only its HASH is async, so an async identity read here would have
53
+ * forced the record build itself to become async and rippled through every caller. An
54
+ * implementation resolves the page's `build`/`projection` during its own async `open()`
55
+ * and serves them from a field afterwards — they cannot change within a run anyway
56
+ * (§10.3: the display provider is boot-fixed).
57
+ *
58
+ * ── a.3 · identity convergence (the finding that would have split every corpus row) ──
59
+ *
60
+ * As-built, a node-side identity carries `build: {engine:null, renderer:null, …}` and
61
+ * `projection: null`, because those are derived from BROWSER boot authorities that do not
62
+ * exist in node. The page's identity carries the real ones. Two different records ⇒ two
63
+ * different hashes ⇒ EVERY hosted row would have declared a different scene than the
64
+ * browser row of the same scene, silently, forever.
65
+ *
66
+ * The fix is structural, not a comparison relaxation: attaching a host EXTENDS the node
67
+ * identity with the page-resolved `build`/`projection` (`capture.js` reads them through
68
+ * the attached host), so the two records are equal BY CONSTRUCTION and the hash check is
69
+ * a real check rather than a tautology waiting to be loosened. What remains genuinely
70
+ * node-owned — `seed`, `sourceRef` — is what the host must make the page agree to (it boots
71
+ * with `?seed=` and registers the source ref), and `diffSceneRecords` below names any survivor
72
+ * by field. The physics bounds are a boot RESULT: the scene's co-init call writes them, the
73
+ * host reads them back from the page, and a caller's stated bounds are an EXPECTATION.
74
+ *
75
+ * ── Layer 2: physics behaviour ───────────────────────────────────────────────
76
+ * ZERO physics. This is the output boundary, in two processes instead of one. Nothing
77
+ * here touches a frame, a dimension, a tick or an op-program, and the node side never
78
+ * drives a clock: the page's own loop is the tick authority (a second clock across a
79
+ * process boundary is the two-clocks bug with network latency added).
80
+ */
81
+
82
+ import * as RenderStages from '../../../shim/render/render-stages.js';
83
+ import * as CaptureEnvelope from '../../../shim/render/capture-envelope.js';
84
+
85
+
86
+ /** The KIND tag. A `RendererProvider` does not carry it — that is the point. */
87
+ var CAPTURE_HOST_KIND = 'capture-host';
88
+
89
+ /**
90
+ * assertCaptureHost(host) — throw-or-return the KIND gate.
91
+ *
92
+ * The `kind` tag is checked FIRST and by exact string, so the most likely mistake —
93
+ * handing this a provider-shaped object because "it has captureStill()" — names its
94
+ * own diagnosis instead of failing later on a missing method.
95
+ *
96
+ * @param {object} host
97
+ * @returns {object} the same host, when valid
98
+ * @throws {Error} FAIL-LOUD on any missing/mis-shaped member.
99
+ */
100
+ function assertCaptureHost(host) {
101
+ var RS = RenderStages;
102
+ if (!host || typeof host !== 'object') {
103
+ throw new Error('[FrqtlCaptureHost] expected a capture-host object, got ' +
104
+ String(host) + '. (FAIL-LOUD.)');
105
+ }
106
+ if (host.kind !== CAPTURE_HOST_KIND) {
107
+ throw new Error('[FrqtlCaptureHost] this object is not a ' +
108
+ JSON.stringify(CAPTURE_HOST_KIND) + ' (its kind is ' + JSON.stringify(host.kind) +
109
+ '). A capture host is a DISTINCT stage-executor kind, not a RendererProvider: a ' +
110
+ 'provider returns raw pixels and lets the local dispatcher stamp provenance, ' +
111
+ 'which a remote executor cannot honestly do — it holds no tick, no pose, no ' +
112
+ 'filter order. A host returns a bag already normalized by the dispatcher that ' +
113
+ 'DOES hold them. (FAIL-LOUD.)');
114
+ }
115
+ if (!Array.isArray(host.stages) || host.stages.length === 0) {
116
+ throw new Error('[FrqtlCaptureHost] `stages` must be a non-empty array of stage ' +
117
+ 'names this host executes remotely — [' + RS.RENDER_STAGES.join(', ') +
118
+ ']. (FAIL-LOUD.)');
119
+ }
120
+ for (var i = 0; i < host.stages.length; i++) { RS.assertStage(host.stages[i]); }
121
+ ['identity', 'capture', 'record', 'dispose'].forEach(function (m) {
122
+ if (typeof host[m] !== 'function') {
123
+ throw new Error('[FrqtlCaptureHost] a capture host must implement ' + m + '() — ' +
124
+ 'the KIND is {kind, stages, identity(), capture(stage), record(opts), dispose()}. ' +
125
+ '(FAIL-LOUD.)');
126
+ }
127
+ });
128
+ var ident = host.identity();
129
+ if (!ident || typeof ident !== 'object') {
130
+ throw new Error('[FrqtlCaptureHost] identity() must return the page-resolved ' +
131
+ '{build, projection} bag SYNCHRONOUSLY (resolved once during the host\'s own ' +
132
+ 'open()); got ' + String(ident) + '. Without it the node-side scene identity ' +
133
+ 'carries nulls where the page carries real values, and every hosted row splits ' +
134
+ 'into a second scene. (FAIL-LOUD.)');
135
+ }
136
+ return host;
137
+ }
138
+
139
+ // ── The BOUNDARY CODEC — authored ONCE, here ─────────────────────────────
140
+ //
141
+ // Exactly one thing crosses the process boundary: the normalized bag. Never a raw
142
+ // pixel buffer plus reassembly code on the node side, which would mint a second
143
+ // encoder beside `FrqtlVision.toDataURL` and let the two disagree about row order,
144
+ // colour space or alpha.
145
+ //
146
+ // The problem the codec solves is narrow and real: a browser-automation `evaluate`
147
+ // boundary is JSON-shaped, and a typed array does not survive it (it arrives as an
148
+ // object with integer keys — silently, and 4x the bytes). So the payload rides as
149
+ // base64 and is rebuilt into the SAME typed-array constructor the page produced.
150
+
151
+ /**
152
+ * IN_PAGE_CAPTURE — THE IN-PAGE HALF OF THE CODEC. **This function runs in the PAGE.**
153
+ *
154
+ * It is handed to a browser `evaluate()` as source text, so it closes over NOTHING: the
155
+ * runner is the page door's (`window.FrqtlPage.runner`), and every module it needs is
156
+ * IMPORTED in the page from the URL the host hands it (`args.modules`, computed by the
157
+ * hosted eyes from the page URL — the page's own instances, never a copy). Keep it
158
+ * self-contained — a captured variable here fails at runtime in the page, not at build
159
+ * time here.
160
+ *
161
+ * It performs NO capture logic of its own: it calls the page's `capture(stage)` — the
162
+ * one dispatcher, which stamps all provenance — and then encodes the result for
163
+ * transport. Encoding is declared IN the envelope (`encoding`), so the node half
164
+ * obeys the page's declaration instead of re-deriving the rule.
165
+ *
166
+ * @param {{stage: string, modules: {captureHost: string, captureEnvelope: string}}} args
167
+ * @returns {Promise<{bag: object, encoding: string, ctor: (string|null), payload: *}>}
168
+ */
169
+ var IN_PAGE_CAPTURE = async function (args) {
170
+ var app = window.FrqtlPage && window.FrqtlPage.runner;
171
+ if (!app) {
172
+ throw new Error('[FrqtlCaptureHost/in-page] no FrqtlPage.runner on this page — the ' +
173
+ 'host booted something that is not a running FRQTL stack. (FAIL-LOUD.)');
174
+ }
175
+ if (!args || !args.modules || typeof args.modules.captureEnvelope !== 'string') {
176
+ throw new Error('[FrqtlCaptureHost/in-page] no CaptureEnvelope module URL (args.modules.captureEnvelope) — ' +
177
+ 'the ONE transport encoder (lib/shim/render/capture-envelope.js) is imported from the page, never ' +
178
+ 're-encoded by a copy. (FAIL-LOUD.)');
179
+ }
180
+ var CE = await import(args.modules.captureEnvelope);
181
+ var still = await app.capture(args.stage);
182
+ var data = still.data;
183
+ var bag = {};
184
+ for (var k in still) { if (k !== 'data') { bag[k] = still[k]; } }
185
+ // A typed array does not survive a JSON boundary; a plain framedata array does
186
+ // (the normalizer already flattened it to plain data — that is what makes the
187
+ // whole format serialization-safe). The choice is declared, never re-derived.
188
+ if (data && typeof data.BYTES_PER_ELEMENT === 'number') {
189
+ // The ONE transportable-bytes encoder (lib/shim/render/capture-envelope.js) — never a copy here.
190
+ return { bag: bag, encoding: 'base64', ctor: data.constructor.name,
191
+ payload: CE.bytesToBase64(data) };
192
+ }
193
+ return { bag: bag, encoding: 'plain', ctor: null, payload: data };
194
+ };
195
+
196
+ /**
197
+ * IN_PAGE_RECORD_BEGIN / _END / _ABORT — the recorder, in the PAGE, in three calls.
198
+ * **All run in the PAGE** and close over nothing.
199
+ *
200
+ * BEGIN imports the page's capture-host instance (`args.modules.captureHost`) and calls the
201
+ * ONE recorder it holds — `FrqtlVision`, which registers itself with capture-host when its
202
+ * module evaluates (`registerRecorder`) — as `recorder().attach(FrqtlPage.runner).record(opts)`.
203
+ * That subscribes to the tick fan-out SYNCHRONOUSLY; BEGIN parks its Promise plus an
204
+ * `AbortController` (whose signal the page-side record is given — a node-side signal cannot
205
+ * cross `evaluate`) under `id` in the page instance's ledger (`parkRecording`) — so a drive the
206
+ * node side issues after BEGIN resolves cannot run ticks the recorder did not see. END awaits
207
+ * the parked Promise and removes the entry once it settles. ABORT aborts the parked controller
208
+ * and returns true, or returns false when no recording is parked under `id` (it already
209
+ * settled — aborting a finished operation is not an error in the AbortSignal contract). The
210
+ * envelope crosses the boundary as plain JSON: its bytes are already base64 by the envelope
211
+ * owner's contract.
212
+ *
213
+ * @param {{id: number, opts: object, modules: {captureHost: string}}} args (END and ABORT take {id, modules})
214
+ */
215
+ var IN_PAGE_RECORD_BEGIN = async function (args) {
216
+ var app = window.FrqtlPage && window.FrqtlPage.runner;
217
+ if (!app) {
218
+ throw new Error('[FrqtlCaptureHost/in-page] no FrqtlPage.runner on this page — the ' +
219
+ 'host booted something that is not a running FRQTL stack. (FAIL-LOUD.)');
220
+ }
221
+ var CH = (await import(args.modules.captureHost)).FrqtlCaptureHost;
222
+ var controller = new AbortController();
223
+ var opts = {};
224
+ for (var k in args.opts) { opts[k] = args.opts[k]; }
225
+ opts.signal = controller.signal;
226
+ var pending = CH.recorder().attach(app).record(opts);
227
+ pending.then(null, function () {}); // delivered by END, never unhandled meanwhile
228
+ CH.parkRecording(args.id, { pending: pending, controller: controller });
229
+ return args.id;
230
+ };
231
+
232
+ var IN_PAGE_RECORD_END = async function (args) {
233
+ var CH = (await import(args.modules.captureHost)).FrqtlCaptureHost;
234
+ var entry = CH.parkedRecording(args.id);
235
+ if (!entry) {
236
+ throw new Error('[FrqtlCaptureHost/in-page] no recording parked under id ' + args.id +
237
+ '. (FAIL-LOUD.)');
238
+ }
239
+ var forget = function () { CH.forgetRecording(args.id); };
240
+ return entry.pending.then(function (env) { forget(); return env; },
241
+ function (e) { forget(); throw e; });
242
+ };
243
+
244
+ var IN_PAGE_RECORD_ABORT = async function (args) {
245
+ var CH = (await import(args.modules.captureHost)).FrqtlCaptureHost;
246
+ var entry = CH.parkedRecording(args.id);
247
+ if (!entry) { return false; }
248
+ entry.controller.abort();
249
+ return true;
250
+ };
251
+
252
+ // ── The page instance's state: the ONE recorder, and the hosted-recording ledger ──
253
+
254
+ var _recorder = null;
255
+ /** FrqtlVision registers itself here when its module evaluates — capture-host never names it. */
256
+ function registerRecorder(recorder) {
257
+ if (!recorder || typeof recorder.attach !== 'function') {
258
+ throw new TypeError('[FrqtlCaptureHost] registerRecorder(recorder): expected the FrqtlVision face ' +
259
+ '(attach(runner) → handle). (FAIL-LOUD.)');
260
+ }
261
+ if (_recorder && _recorder !== recorder) {
262
+ throw new Error('[FrqtlCaptureHost] a second recorder registered in this realm — FrqtlVision ' +
263
+ 'loaded under two module URLs (one module, one URL). (FAIL-LOUD.)');
264
+ }
265
+ _recorder = recorder;
266
+ }
267
+ /** @returns the registered recorder. THROWS when none registered (FrqtlVision never imported here). */
268
+ function recorder() {
269
+ if (!_recorder) {
270
+ throw new Error('[FrqtlCaptureHost] no recorder registered in this realm — import FrqtlVision ' +
271
+ "(import { FrqtlVision } from '@descix/frqtl-web/parts'); the boot imports the parts barrel. (FAIL-LOUD.)");
272
+ }
273
+ return _recorder;
274
+ }
275
+ var _parked = Object.create(null);
276
+ function parkRecording(id, entry) { _parked[id] = entry; }
277
+ function parkedRecording(id) { return _parked[id] || null; }
278
+ function forgetRecording(id) { delete _parked[id]; }
279
+
280
+ /**
281
+ * decodeStillEnvelope(env) — THE NODE-SIDE HALF of the codec.
282
+ *
283
+ * Rebuilds the payload and re-runs the OWNER'S normalizer over the bag. Re-normalizing
284
+ * is not a second stamping: every provenance field is the page's, ferried verbatim —
285
+ * what the normalizer adds is VALIDATION (a payload that arrived empty, a format that
286
+ * does not match its stage, a missing scene id all fail loud here rather than entering
287
+ * a corpus) and the plain-data snapshot the contract promises.
288
+ *
289
+ * The payload's TYPE is ferried (`ctor`), not mapped: a local format→constructor table
290
+ * would be a hand mirror of the stage table's own declaration, and the page already
291
+ * knows the answer exactly.
292
+ *
293
+ * @param {object} env — an `IN_PAGE_CAPTURE` envelope
294
+ * @returns {object} a full normalized still bag
295
+ */
296
+ function decodeStillEnvelope(env) {
297
+ var RS = RenderStages;
298
+ if (!env || typeof env !== 'object' || !env.bag) {
299
+ throw new Error('[FrqtlCaptureHost.decodeStillEnvelope] expected a capture envelope ' +
300
+ '{bag, encoding, ctor, payload}, got ' + String(env) + '. (FAIL-LOUD.)');
301
+ }
302
+ var data;
303
+ if (env.encoding === 'base64') {
304
+ if (typeof Buffer === 'undefined' || typeof Buffer.from !== 'function') {
305
+ throw new Error('[FrqtlCaptureHost.decodeStillEnvelope] base64 payloads are ' +
306
+ 'decoded with Node\'s Buffer — the node half of the boundary runs in Node by ' +
307
+ 'definition. (FAIL-LOUD.)');
308
+ }
309
+ var Ctor = (typeof globalThis !== 'undefined') ? globalThis[env.ctor] : null;
310
+ if (typeof Ctor !== 'function' || typeof Ctor.BYTES_PER_ELEMENT !== 'number') {
311
+ throw new Error('[FrqtlCaptureHost.decodeStillEnvelope] the envelope declares ' +
312
+ 'payload type ' + JSON.stringify(env.ctor) + ', which is not a typed-array ' +
313
+ 'constructor here. The payload TYPE is ferried from the page, never mapped ' +
314
+ 'from the format — a local table would be a hand mirror. (FAIL-LOUD.)');
315
+ }
316
+ data = new Ctor(Buffer.from(env.payload, 'base64'));
317
+ } else if (env.encoding === 'plain') {
318
+ data = env.payload;
319
+ } else {
320
+ throw new Error('[FrqtlCaptureHost.decodeStillEnvelope] unknown payload encoding ' +
321
+ JSON.stringify(env.encoding) + ' — the ENCODING SIDE declares it (base64 for a ' +
322
+ 'typed buffer, plain for JSON-safe data) and this side obeys. (FAIL-LOUD.)');
323
+ }
324
+ var partial = {};
325
+ for (var k in env.bag) { partial[k] = env.bag[k]; }
326
+ partial.data = data;
327
+ return RS.normalizeStill(partial);
328
+ }
329
+
330
+ // ── VERIFICATION — the node side proves, per still, that it is the same scene ──
331
+
332
+ /**
333
+ * diffSceneRecords(a, b) — the `SCENE_KEYS` on which two records disagree.
334
+ *
335
+ * Driven off the OWNER'S key list and its OWN canonical encoding, so a scene field
336
+ * added to the contract is compared here with no edit. Used at attach time to turn
337
+ * "the hashes differ" — the least actionable error a content-addressed format can
338
+ * produce — into "seed, physicsH".
339
+ *
340
+ * @param {object} a
341
+ * @param {object} b
342
+ * @returns {string[]} the differing key names, in table order
343
+ */
344
+ function diffSceneRecords(a, b) {
345
+ var RS = RenderStages;
346
+ var out = [];
347
+ for (var i = 0; i < RS.SCENE_KEYS.length; i++) {
348
+ var k = RS.SCENE_KEYS[i];
349
+ // canonicalizeScene is the ONE canonical encoding; projecting a single field
350
+ // through a one-key record reuses it rather than re-implementing key-sorted
351
+ // deep equality here.
352
+ var ea = RS.canonicalizeScene(_one(k, a));
353
+ var eb = RS.canonicalizeScene(_one(k, b));
354
+ if (ea !== eb) { out.push(k); }
355
+ }
356
+ return out;
357
+ }
358
+
359
+ /** A record carrying ONE key's value, for the per-field canonical compare. @private */
360
+ function _one(key, rec) {
361
+ var r = {};
362
+ r[key] = rec ? rec[key] : undefined;
363
+ return r;
364
+ }
365
+
366
+ /**
367
+ * verifyStill(expectedSceneId, stage, still) — the per-still fidelity check.
368
+ *
369
+ * The node side computed a scene identity independently; the page stamped one into the
370
+ * still. Equal ⇒ the two processes agree about what was captured. Unequal ⇒ something
371
+ * moved mid-run (a re-init, a resize, a re-seeded reload) and the row is REFUSED — a
372
+ * still that lies about its scene is worse than no still, because it verifies against
373
+ * the wrong header forever.
374
+ *
375
+ * @param {string} expectedSceneId — the node-side `sceneRecord().scene`
376
+ * @param {string} stage
377
+ * @param {object} still — the decoded, normalized bag
378
+ * @returns {object} the same still, when it verifies
379
+ * @throws {Error} FAIL-LOUD on a scene or stage mismatch.
380
+ */
381
+ function verifyStill(expectedSceneId, stage, still) {
382
+ if (!still || typeof still !== 'object') {
383
+ throw new Error('[FrqtlCaptureHost] the host returned no still for stage ' +
384
+ JSON.stringify(stage) + '. (FAIL-LOUD — never a null still.)');
385
+ }
386
+ if (still.stage !== stage) {
387
+ throw new Error('[FrqtlCaptureHost] asked the host for stage ' + JSON.stringify(stage) +
388
+ ' and it returned stage ' + JSON.stringify(still.stage) + '. (FAIL-LOUD.)');
389
+ }
390
+ if (still.scene !== expectedSceneId) {
391
+ throw new Error('[FrqtlCaptureHost] SCENE IDENTITY MISMATCH: the hosted page stamped ' +
392
+ JSON.stringify(still.scene) + ' but this process computed ' +
393
+ JSON.stringify(expectedSceneId) + ' for the same scene. The two are supposed to be ' +
394
+ 'equal BY CONSTRUCTION (host attachment extends the node identity with the ' +
395
+ 'page-resolved build/projection), so a mismatch means the page is no longer ' +
396
+ 'running the scene this handle was opened for. The still is REFUSED rather than ' +
397
+ 'filed against the wrong header. (FAIL-LOUD.)');
398
+ }
399
+ return still;
400
+ }
401
+
402
+ /**
403
+ * verifyVideo(expectedSceneId, envelope) — the per-recording fidelity check.
404
+ *
405
+ * Re-runs the OWNER'S `videoEnvelope` over what the page returned (validation — cadence,
406
+ * contiguous indices, encoded fields — never a second stamping), then refuses a recording
407
+ * whose scene is not the one this process computed, for the same reason `verifyStill`
408
+ * refuses a still.
409
+ *
410
+ * @param {string} expectedSceneId — the node-side `sceneRecord().scene`
411
+ * @param {object} envelope — what the host's `record` resolved
412
+ * @returns {object} the re-normalized envelope, when it verifies
413
+ * @throws {Error} FAIL-LOUD on a malformed envelope or a scene mismatch.
414
+ */
415
+ function verifyVideo(expectedSceneId, envelope) {
416
+ if (!envelope || typeof envelope !== 'object' || envelope.kind !== 'video') {
417
+ throw new Error('[FrqtlCaptureHost] the host returned no video envelope (got ' +
418
+ String(envelope && envelope.kind) + '). (FAIL-LOUD — never a null recording.)');
419
+ }
420
+ var env = CaptureEnvelope.videoEnvelope({
421
+ track: envelope.track, encoded: envelope.encoded, truncated: envelope.truncated
422
+ });
423
+ if (env.track.scene !== expectedSceneId) {
424
+ throw new Error('[FrqtlCaptureHost] SCENE IDENTITY MISMATCH: the hosted page recorded ' +
425
+ 'scene ' + JSON.stringify(env.track.scene) + ' but this process computed ' +
426
+ JSON.stringify(expectedSceneId) + ' for the same scene. The recording is REFUSED ' +
427
+ 'rather than filed against the wrong header. (FAIL-LOUD.)');
428
+ }
429
+ return env;
430
+ }
431
+
432
+ // ── Export ────────────────────────────────────────────────────────────────
433
+
434
+ var FrqtlCaptureHost = {
435
+ CAPTURE_HOST_KIND: CAPTURE_HOST_KIND,
436
+ assertCaptureHost: assertCaptureHost,
437
+ IN_PAGE_CAPTURE: IN_PAGE_CAPTURE,
438
+ IN_PAGE_RECORD_BEGIN: IN_PAGE_RECORD_BEGIN,
439
+ IN_PAGE_RECORD_END: IN_PAGE_RECORD_END,
440
+ IN_PAGE_RECORD_ABORT: IN_PAGE_RECORD_ABORT,
441
+ decodeStillEnvelope: decodeStillEnvelope,
442
+ diffSceneRecords: diffSceneRecords,
443
+ verifyStill: verifyStill,
444
+ verifyVideo: verifyVideo,
445
+ registerRecorder: registerRecorder,
446
+ recorder: recorder,
447
+ parkRecording: parkRecording,
448
+ parkedRecording: parkedRecording,
449
+ forgetRecording: forgetRecording
450
+ };
451
+
452
+ export { FrqtlCaptureHost };
453
+