@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,296 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lib/shim/experiment-frame — THE fail-loud guard for a host that embeds a
5
+ * canonical experiment page in an iframe.
6
+ *
7
+ * ── Why this module exists (the false-200 trap, observed 2026-07-28) ─────────
8
+ * A dev/prod vite host answers an UNKNOWN path with its SPA fallback: HTTP 200,
9
+ * `text/html`, the host app's OWN `index.html`. So when an experiment id stops
10
+ * existing (a retirement), `GET /frqtl/experiments/<gone>/index.html` does not
11
+ * 404 — it returns the SITE. The host's hero iframe then embeds the host inside
12
+ * itself, recursively: the nav bar renders three times, the stage is blank, and
13
+ * EVERY automated signal is clean (0 HTTP 4xx/5xx, 0 `requestfailed`, 0
14
+ * `pageerror`). A missing experiment degraded SILENTLY into a self-embed.
15
+ *
16
+ * This module makes that impossible to miss. It does NOT trust the HTTP status
17
+ * (which lies) — it inspects the DOCUMENT that actually loaded and asserts it is
18
+ * a canonical experiment page, by two independent signals:
19
+ *
20
+ * 1. POSITIVE PROOF — the boot record markers. Every canonical experiment
21
+ * page boots through `_shared/boot.js`, whose boot record writes
22
+ * `EXPERIMENT_PAGE_MARKERS` on its own <html> while the page's module script
23
+ * evaluates — before the document reaches `readyState === 'complete'`. No
24
+ * other document in this repo writes them. Their presence is what "an
25
+ * experiment page booted" MEANS; their absence at document-complete is a
26
+ * hard failure.
27
+ * 2. POSITIVE IDENTIFICATION of the impostor — the host SPA shell root
28
+ * (`HOST_SHELL_ROOT_SELECTOR`, the `<div id="root">` in
29
+ * `apps/ide/index.html`). A document that
30
+ * has the shell root and none of the markers IS the SPA fallback, and the
31
+ * guard says so by name rather than reporting a generic timeout.
32
+ *
33
+ * A 200 cannot fool either signal: the check reads the loaded document, and the
34
+ * SPA shell provably lacks every marker.
35
+ *
36
+ * ── Layer 1: API contract ───────────────────────────────────────────────────
37
+ * classifyFrameProbe(probe) — PURE verdict function (no DOM). Unit-testable.
38
+ * probeExperimentFrame(el) — DOM adapter: iframe element → probe bag.
39
+ * watchExperimentFrame(el,o) — poll + paint a visible overlay on failure.
40
+ *
41
+ * ── Layer 2: physics behavior ───────────────────────────────────────────────
42
+ * None. This is host-side plumbing: it observes whether a document booted and
43
+ * never touches the engine, a universe, or a tick.
44
+ */
45
+
46
+ 'use strict';
47
+
48
+ /**
49
+ * The <html> attributes the boot record (`lib/frqtl/experiments/_shared/boot.js`
50
+ * `createBootRecord`) writes the moment a page's boot begins — during the page's
51
+ * module-script evaluation, so both exist by the time the document reaches
52
+ * `readyState === 'complete'`, regardless of whether the engine has finished
53
+ * fetching WASM.
54
+ *
55
+ * This is the ONE list; a consumer asserts against it, never against a
56
+ * hand-picked attribute of its own.
57
+ *
58
+ * @type {ReadonlyArray<string>}
59
+ */
60
+ export var EXPERIMENT_PAGE_MARKERS = Object.freeze([
61
+ 'data-egpt-boot-phase',
62
+ 'data-egpt-boot-progress'
63
+ ]);
64
+
65
+ /**
66
+ * The mount point the host SPA shell renders React into (`apps/ide/index.html`).
67
+ * Its presence inside an experiment iframe is the fingerprint of the vite SPA
68
+ * fallback having answered the request with the host's own page.
69
+ * @type {string}
70
+ */
71
+ export var HOST_SHELL_ROOT_SELECTOR = '#root';
72
+
73
+ /**
74
+ * The verdict vocabulary. `ok` and `pending` are non-failures; everything else
75
+ * is a FAIL-LOUD outcome the host must surface visibly.
76
+ * @type {Readonly<Record<string,string>>}
77
+ */
78
+ export var FRAME_VERDICTS = Object.freeze({
79
+ OK: 'ok',
80
+ PENDING: 'pending',
81
+ CROSS_ORIGIN: 'cross-origin',
82
+ SPA_SHELL: 'spa-shell',
83
+ NOT_EXPERIMENT: 'not-an-experiment-page'
84
+ });
85
+
86
+ /** Default grace period before a not-yet-complete document is called failed. */
87
+ export var DEFAULT_FRAME_TIMEOUT_MS = 10000;
88
+
89
+ /**
90
+ * classifyFrameProbe(probe) — the PURE verdict function.
91
+ *
92
+ * @param {{
93
+ * src?: string, // the URL the host ASKED for
94
+ * href?: string, // the URL the frame's document actually has
95
+ * sameOrigin?: boolean, // false when reading contentWindow threw
96
+ * readyState?: string, // the frame document's readyState
97
+ * markers?: string[], // which EXPERIMENT_PAGE_MARKERS were found
98
+ * hasHostShellRoot?: boolean,
99
+ * elapsedMs?: number,
100
+ * timeoutMs?: number
101
+ * }} probe
102
+ * @returns {{verdict:string, ok:boolean, done:boolean, message:string}}
103
+ * `done` is false only for `pending` (keep polling).
104
+ */
105
+ export function classifyFrameProbe(probe) {
106
+ var p = probe || {};
107
+ var src = p.src || '(unknown src)';
108
+ var href = typeof p.href === 'string' ? p.href : '';
109
+ var found = Array.isArray(p.markers) ? p.markers : [];
110
+ var timeout = typeof p.timeoutMs === 'number' ? p.timeoutMs : DEFAULT_FRAME_TIMEOUT_MS;
111
+ var elapsed = typeof p.elapsedMs === 'number' ? p.elapsedMs : 0;
112
+
113
+ // A cross-origin frame cannot be inspected at all — say so rather than
114
+ // silently trusting it. The /frqtl mount is same-origin BY DESIGN (that is
115
+ // the precondition every host-side runner grab already relies on), so this
116
+ // verdict means the host built the wrong URL.
117
+ if (p.sameOrigin === false) {
118
+ return _verdict(FRAME_VERDICTS.CROSS_ORIGIN, false, true,
119
+ 'the experiment frame is CROSS-ORIGIN, so the host cannot verify what loaded.\n' +
120
+ 'requested: ' + src + '\n' +
121
+ 'Experiment pages must be served from the host\'s own origin (the /frqtl mount).');
122
+ }
123
+
124
+ // A frame that has not navigated to the requested document yet (about:blank
125
+ // between src assignments) is PENDING, never a failure.
126
+ if (href === '' || href === 'about:blank') {
127
+ return _pendingOrTimeout(elapsed, timeout, src,
128
+ 'the frame is still on about:blank — it never navigated to the requested URL.');
129
+ }
130
+
131
+ // POSITIVE PROOF: every boot marker present ⇒ a canonical experiment page.
132
+ var missing = EXPERIMENT_PAGE_MARKERS.filter(function (m) { return found.indexOf(m) === -1; });
133
+ if (missing.length === 0) {
134
+ return _verdict(FRAME_VERDICTS.OK, true, true, 'experiment page booted: ' + src);
135
+ }
136
+
137
+ // POSITIVE IDENTIFICATION of the impostor. The SPA shell verdict does NOT
138
+ // wait for the timeout: a document carrying the host's own React root and
139
+ // none of the boot markers is definitively not an experiment page, whatever
140
+ // status code delivered it.
141
+ if (p.hasHostShellRoot === true) {
142
+ return _verdict(FRAME_VERDICTS.SPA_SHELL, false, true,
143
+ 'THE FALSE-200 TRAP: the request returned HTTP 200 text/html, but the document is the\n' +
144
+ 'HOST\'S OWN SPA SHELL (it has ' + HOST_SHELL_ROOT_SELECTOR + ' and none of the experiment-page\n' +
145
+ 'boot markers [' + EXPERIMENT_PAGE_MARKERS.join(', ') + ']).\n' +
146
+ 'requested: ' + src + '\n' +
147
+ 'loaded: ' + href + '\n' +
148
+ 'The dev server answers an UNKNOWN path with its SPA fallback, so a MISSING experiment id\n' +
149
+ 'looks like a successful load and this host ends up embedding ITSELF, recursively.\n' +
150
+ 'Fix the id at its single declaration site — do not add a fallback id here.');
151
+ }
152
+
153
+ // Document finished loading and it is neither an experiment page nor the
154
+ // recognised shell — still a hard failure, reported honestly.
155
+ if (p.readyState === 'complete') {
156
+ return _verdict(FRAME_VERDICTS.NOT_EXPERIMENT, false, true,
157
+ 'the frame finished loading but it is NOT a canonical experiment page.\n' +
158
+ 'requested: ' + src + '\n' +
159
+ 'loaded: ' + href + '\n' +
160
+ 'missing boot markers: [' + missing.join(', ') + ']\n' +
161
+ 'A canonical experiment page boots through _shared/boot.js, whose boot record writes all of\n' +
162
+ '[' + EXPERIMENT_PAGE_MARKERS.join(', ') + '] on its <html> as the boot begins.');
163
+ }
164
+
165
+ return _pendingOrTimeout(elapsed, timeout, src,
166
+ 'missing boot markers: [' + missing.join(', ') + '] (document readyState=' +
167
+ (p.readyState || 'unknown') + ').');
168
+ }
169
+
170
+ function _verdict(verdict, ok, done, message) {
171
+ return { verdict: verdict, ok: ok, done: done, message: '[experiment-frame] ' + message };
172
+ }
173
+
174
+ function _pendingOrTimeout(elapsed, timeout, src, detail) {
175
+ if (elapsed >= timeout) {
176
+ return _verdict(FRAME_VERDICTS.NOT_EXPERIMENT, false, true,
177
+ 'the frame never became a canonical experiment page within ' + timeout + 'ms.\n' +
178
+ 'requested: ' + src + '\n' + detail);
179
+ }
180
+ return _verdict(FRAME_VERDICTS.PENDING, false, false, 'waiting for ' + src + ' — ' + detail);
181
+ }
182
+
183
+ /**
184
+ * probeExperimentFrame(iframeEl, opts) — the DOM adapter. Reads the frame's own
185
+ * document (same-origin) and returns the probe bag `classifyFrameProbe` consumes.
186
+ *
187
+ * @param {HTMLIFrameElement} iframeEl
188
+ * @param {{elapsedMs?:number, timeoutMs?:number}} [opts]
189
+ * @returns {object} the probe bag.
190
+ */
191
+ export function probeExperimentFrame(iframeEl, opts) {
192
+ var o = opts || {};
193
+ var probe = {
194
+ src: iframeEl && iframeEl.src ? iframeEl.src : '',
195
+ href: '',
196
+ sameOrigin: true,
197
+ readyState: undefined,
198
+ markers: [],
199
+ hasHostShellRoot: false,
200
+ elapsedMs: o.elapsedMs,
201
+ timeoutMs: o.timeoutMs
202
+ };
203
+ var cw = null;
204
+ try {
205
+ cw = iframeEl && iframeEl.contentWindow;
206
+ if (!cw) { return probe; } // not attached yet → PENDING
207
+ probe.href = (cw.location && cw.location.href) || '';
208
+ var doc = cw.document;
209
+ probe.readyState = doc ? doc.readyState : undefined;
210
+ var root = doc && doc.documentElement;
211
+ probe.markers = EXPERIMENT_PAGE_MARKERS.filter(function (m) {
212
+ return !!(root && root.hasAttribute && root.hasAttribute(m));
213
+ });
214
+ probe.hasHostShellRoot = !!(doc && doc.querySelector &&
215
+ doc.querySelector(HOST_SHELL_ROOT_SELECTOR));
216
+ } catch (e) {
217
+ // A same-origin read that throws means the document is cross-origin.
218
+ probe.sameOrigin = false;
219
+ }
220
+ return probe;
221
+ }
222
+
223
+ /**
224
+ * paintFrameGuardOverlay(hostEl, message) — the VISIBLE failure. Mirrors the
225
+ * empty page's own `_egptPaintWalkthroughError` styling so a guard failure and a
226
+ * recipe failure read as one family.
227
+ *
228
+ * @param {Element} hostEl — the element to overlay (the iframe's wrapper).
229
+ * @param {string} message
230
+ * @returns {Element} the overlay node.
231
+ */
232
+ export function paintFrameGuardOverlay(hostEl, message) {
233
+ var host = hostEl || (typeof document !== 'undefined' ? document.body : null);
234
+ if (!host) { return null; }
235
+ var existing = host.querySelector && host.querySelector('[data-role="experiment-frame-error"]');
236
+ if (existing) { existing.parentNode.removeChild(existing); }
237
+ var box = host.ownerDocument.createElement('div');
238
+ box.setAttribute('data-role', 'experiment-frame-error');
239
+ box.style.cssText =
240
+ 'position:absolute;inset:0;z-index:9999;padding:16px;overflow:auto;' +
241
+ 'white-space:pre-wrap;background:#1a0000;color:#ff6b6b;border:2px solid #b6161c;' +
242
+ 'font:12px/1.55 ui-monospace,Menlo,Consolas,monospace;';
243
+ box.textContent = message;
244
+ if (host.style && (!host.style.position || host.style.position === 'static')) {
245
+ host.style.position = 'relative';
246
+ }
247
+ host.appendChild(box);
248
+ return box;
249
+ }
250
+
251
+ /**
252
+ * watchExperimentFrame(iframeEl, opts) — poll the frame until it is proven to be
253
+ * a canonical experiment page, or FAIL LOUD (console.error + a visible overlay +
254
+ * `onError`).
255
+ *
256
+ * @param {HTMLIFrameElement} iframeEl
257
+ * @param {{
258
+ * overlayHost?: Element, // defaults to iframeEl.parentNode
259
+ * timeoutMs?: number,
260
+ * pollMs?: number,
261
+ * onOk?: function(Window):void,
262
+ * onError?: function({verdict:string,message:string}):void
263
+ * }} [opts]
264
+ * @returns {function():void} stop() — cancels the watch (call from useEffect cleanup).
265
+ */
266
+ export function watchExperimentFrame(iframeEl, opts) {
267
+ var o = opts || {};
268
+ var timeout = typeof o.timeoutMs === 'number' ? o.timeoutMs : DEFAULT_FRAME_TIMEOUT_MS;
269
+ var pollMs = typeof o.pollMs === 'number' ? o.pollMs : 150;
270
+ var started = Date.now();
271
+ var stopped = false;
272
+ var timer = setInterval(tick, pollMs);
273
+ tick();
274
+
275
+ function tick() {
276
+ if (stopped) { return; }
277
+ var probe = probeExperimentFrame(iframeEl, { elapsedMs: Date.now() - started, timeoutMs: timeout });
278
+ var result = classifyFrameProbe(probe);
279
+ if (!result.done) { return; }
280
+ stop();
281
+ if (result.ok) {
282
+ if (typeof o.onOk === 'function') { o.onOk(iframeEl.contentWindow); }
283
+ return;
284
+ }
285
+ if (typeof console !== 'undefined' && console.error) { console.error(result.message); }
286
+ paintFrameGuardOverlay(o.overlayHost || (iframeEl && iframeEl.parentNode), result.message);
287
+ if (typeof o.onError === 'function') { o.onError(result); }
288
+ }
289
+
290
+ function stop() {
291
+ if (stopped) { return; }
292
+ stopped = true;
293
+ clearInterval(timer);
294
+ }
295
+ return stop;
296
+ }
@@ -0,0 +1,369 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * ast-schema.js — Expression AST WIRE CONTRACT for FRQTL op-programs.
5
+ *
6
+ * This module is the cross-engine authority for op-program AST node shapes,
7
+ * operator vocabularies, and evaluation conventions. It is read-contract-shaped
8
+ * (like lib/shim/frame-data SECTION_KEYS) — it exports node SHAPES and vocab
9
+ * LISTS; it does NOT contain any parsing or evaluation logic.
10
+ *
11
+ * Architecture: lib/shim/expr/ (this file) ← imported by lib/frqtl/objects/
12
+ * sugar (JS-side parser → ast-schema shapes)
13
+ * and by the dual-native evaluators in the JS reference and Rust engines.
14
+ * Arrow-legality: lib/frqtl/objects imports lib/shim (downward — legal).
15
+ *
16
+ * Gate-2 architect binding (2026-06-21, slice 3):
17
+ * • Rust NEVER parses — AST-only payload; this schema is the data shape.
18
+ * • NODE_KINDS and vocab lists are the conformance iteration surface
19
+ * (binding item 3 — conformance iterates these lists).
20
+ * • Integer-first evaluation convention is documented here (binding item 4).
21
+ * • Named refs compile to integer handles JS-side (binding item 5) — this
22
+ * schema defines the Ref shape ({handle:number, prop:string}).
23
+ *
24
+ * HOW TO READ THIS FILE
25
+ * ─────────────────────
26
+ * An ExprAST is one of:
27
+ *
28
+ * Lit { kind:'Lit', value: number }
29
+ * A numeric literal. Floating-point JSON number; rounded to integer
30
+ * by integer-first eval where the target property is integer-domain.
31
+ *
32
+ * Ref { kind:'Ref', handle: number, prop: string }
33
+ * Read handle.prop at eval time. handle is an integer index into the
34
+ * scope's object table (compiled JS-side — NOT a string name).
35
+ * Special handles: HANDLE_FRAME / HANDLE_SELF / HANDLE_UNIVERSE (below).
36
+ *
37
+ * Bin { kind:'Bin', op: BIN_OP, a: ExprAST, b: ExprAST }
38
+ * Binary arithmetic. INTEGER-FIRST: '/' is integer division (truncate
39
+ * toward zero). Result of '/' is Math.trunc(a/b) in JS, a/b as i64
40
+ * truncated in Rust. Never produces a float in integer-domain props.
41
+ *
42
+ * Cmp { kind:'Cmp', op: CMP_OP, a: ExprAST, b: ExprAST }
43
+ * Comparison. Returns 1 (true) or 0 (false) — integer, not boolean.
44
+ * Required for Cond tests and guard expressions.
45
+ *
46
+ * Cond { kind:'Cond', test: ExprAST, then: ExprAST, else: ExprAST }
47
+ * Conditional: if test != 0 then 'then' else 'else'.
48
+ * test is truthy when non-zero (follows from Cmp returning 0/1).
49
+ *
50
+ * Fn { kind:'Fn', name: FN_NAME, args: ExprAST[] }
51
+ * Built-in function. min/max accept two args; floor accepts one.
52
+ * INTEGER-FIRST: floor(x) is already integer in integer-domain
53
+ * contexts; on float-domain props it applies Math.floor / Rust floor.
54
+ *
55
+ * StrLit { kind:'StrLit', value: string }
56
+ * A string literal — valid ONLY as the `to` value of a `set` op
57
+ * whose target is `<handle>.data.<key>` (a string-map write).
58
+ * MUST throw (JS) / panic (Rust) if used as a child of Bin, Cmp,
59
+ * Fn, or Cond.test — eval_expr stays numeric-pure (INTEGER-FIRST
60
+ * ruling; Gate-2 2026-06-21 item 2). The op-apply layer
61
+ * (_runOpProgram / apply_set_op) detects StrLit before calling
62
+ * eval_expr and routes it to the string-map writer.
63
+ *
64
+ * OP-PROGRAM SHAPE
65
+ * ─────────────────
66
+ * { on: TRIGGER, ops: [ { set: '<handle>.<prop>', to: ExprAST }, ... ] }
67
+ *
68
+ * TRIGGER:
69
+ * 'collision' — ops run per colliding frame (scope: frame, self, universe)
70
+ * 'tick' — ops run per LO per tick (scope: self, named refs, universe)
71
+ *
72
+ * set target format: '<integer-handle>.<prop-name>'
73
+ * The JS sugar compiles readable names ('frame', 'self', 'emitterA', …)
74
+ * to integer handles before the AST crosses the boundary. Rust parses
75
+ * '<number>.<string>' — no string-name registry on the Rust side.
76
+ *
77
+ * EVALUATION SCOPE
78
+ * ─────────────────
79
+ * on:'collision':
80
+ * HANDLE_FRAME (0) — the colliding frame (Frame properties)
81
+ * HANDLE_SELF (1) — this LO / emitter (LO/emitter properties)
82
+ * HANDLE_UNIVERSE (2) — universe read-only (tick, activeQuanta)
83
+ *
84
+ * on:'tick':
85
+ * HANDLE_SELF (1) — this LO/emitter
86
+ * HANDLE_UNIVERSE (2) — universe
87
+ * 3, 4, 5, … — named refs (other registered objects by handle)
88
+ *
89
+ * INTEGER-FIRST EVALUATION CONVENTION (binding item 4)
90
+ * ──────────────────────────────────────────────────────
91
+ * All arithmetic operates on the raw numeric values from the scope.
92
+ * '/' is INTEGER DIVISION: Math.trunc(a / b) in JS; (a as i64) / (b as i64)
93
+ * in Rust. Division by zero → 0 (not an error; matches discrete physics).
94
+ * '==' / '<' / '>' return 1 (true) or 0 (false) — integer.
95
+ * 'floor' on an already-integer value is a no-op (included for completeness).
96
+ * NO silent float result is written into an integer-domain property (x, y, vx,
97
+ * vy, mass, count, etc.). The 'set' op applies Math.round() / Rust f64.round()
98
+ * before writing integer-domain props identified in the Frame / LO tables.
99
+ *
100
+ * ADVERSARIAL CASES (conformance must cover these — binding item 4):
101
+ * Bin('/', Lit(7), Lit(2)) → 3 (truncate, not round or floor)
102
+ * Bin('/', Lit(-7), Lit(2)) → -3 (truncate toward zero, not floor: -4)
103
+ * Fn('floor', [Lit(7.9)]) → 7 (floor, not truncate: same for positives,
104
+ * differs for negatives: floor(-7.9)=-8 ≠ trunc(-7.9)=-7)
105
+ * Cmp('==', Lit(1), Lit(1)) → 1
106
+ * Cmp('==', Lit(1), Lit(2)) → 0
107
+ * Cmp('<', Lit(3), Lit(5)) → 1
108
+ * Cmp('>', Lit(5), Lit(3)) → 1
109
+ * nested Cond(Cmp…, Lit, Lit)
110
+ */
111
+
112
+
113
+
114
+ // ── Node kind names ────────────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * NODE_KINDS — the complete, ordered list of expression AST node kinds.
118
+ *
119
+ * Conformance iterates this list to assert coverage (binding item 3):
120
+ * every kind must have at least one representative scenario in
121
+ * expr_program_scenarios.js.
122
+ *
123
+ * THIS LIST IS CLOSED AT 7 — do NOT add structural op kinds here.
124
+ * Structural op kinds live in OP_KINDS (below). The two lists are orthogonal.
125
+ *
126
+ * NOTE on StrLit (item 7): StrLit is ONLY valid as the `to` value of a `set`
127
+ * op targeting `<handle>.data.<key>` (a string-map write). Using StrLit as a
128
+ * child of Bin, Cmp, Fn, or Cond.test MUST throw/panic — eval_expr is
129
+ * numeric-pure (INTEGER-FIRST ruling, Gate-2 2026-06-21 item 2).
130
+ *
131
+ * @type {string[]}
132
+ */
133
+ var NODE_KINDS = Object.freeze([
134
+ 'Lit', // numeric literal
135
+ 'Ref', // property read via handle
136
+ 'Bin', // binary arithmetic
137
+ 'Cmp', // comparison → 0/1
138
+ 'Cond', // conditional
139
+ 'Fn', // built-in function
140
+ 'StrLit' // string literal — ONLY valid as `to` in data.<key> set ops
141
+ ]);
142
+
143
+ /**
144
+ * OP_KINDS — the complete, ordered list of op-program STATEMENT kinds.
145
+ *
146
+ * These are the kinds of ops that can appear in program.ops[].
147
+ * They are DISTINCT from the value-grammar node kinds in NODE_KINDS.
148
+ *
149
+ * §2e (2026-06-24): OP_KINDS is closed at ['set'].
150
+ * addFrames / subtractFrames / addDimension were NEVER implemented as op-kinds
151
+ * in the Rust engine (OpProgram = ops: Vec<SetOp>; SetOp = {set, to}). Their
152
+ * structural analogs are engine METHODS, not op-program statements:
153
+ * - Frame injection → emitters / makeLeafFrame / createFrameFromRect / insert
154
+ * - Frame removal → set 0.is_alive=0 + dead-parent GC
155
+ * - Dimension alloc → universe.addDimension(n) / add_dimension engine method
156
+ *
157
+ * Gate-2 architect binding (2026-06-21, §10.8 B3):
158
+ * • B3.2 — 'collect' is NOT a 2nd op. It compiles to:
159
+ * { kind:'set', set:'1.count', to: Bin('+', Ref(HANDLE_SELF,'count'), Lit(1)) }
160
+ * i.e. a plain 'set' op on the LO's 'count' property. No special sink.
161
+ *
162
+ * Op shapes:
163
+ * set — { kind:'set', set:'<handle>.<prop>', to: ExprAST }
164
+ *
165
+ * Conformance iterates this list (binding item 3):
166
+ * every kind must have at least one scenario in control_loop_scenarios.js.
167
+ *
168
+ * @type {string[]}
169
+ */
170
+ var OP_KINDS = Object.freeze([
171
+ 'set' // property write: routes through setFrameProperty / setObjectProperty
172
+ ]);
173
+
174
+ // ── Operator vocabularies ──────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * BIN_OPS — binary arithmetic operators.
178
+ * '/' is INTEGER DIVISION (truncate toward zero) — see convention above.
179
+ *
180
+ * Conformance must cover all four.
181
+ * @type {string[]}
182
+ */
183
+ var BIN_OPS = Object.freeze(['+', '-', '*', '/']);
184
+
185
+ /**
186
+ * CMP_OPS — comparison operators. All return 1 or 0.
187
+ * Conformance must cover all three.
188
+ * @type {string[]}
189
+ */
190
+ var CMP_OPS = Object.freeze(['<', '>', '==']);
191
+
192
+ /**
193
+ * FN_NAMES — built-in function names.
194
+ * min/max: 2 args. floor: 1 arg. random: 0 args.
195
+ * Conformance must cover all four.
196
+ *
197
+ * NOTE on 'random' (G2, 2026-06-24):
198
+ * Fn('random', []) draws one value from the engine's single Mulberry32 stream.
199
+ * It is a SIDE-EFFECTING draw — each eval call advances the RNG by one step.
200
+ * JS eval: closes over the engine's _rng() function (already in scope in
201
+ * _runOpProgram).
202
+ * Rust eval: eval_expr receives &mut rng so the draw advances the shared stream.
203
+ * Draw order: the ops array is evaluated left-to-right (op 0 before op 1 etc.)
204
+ * and each Fn('random',[]) in a `to` expression is evaluated in tree order
205
+ * (left child before right child for Bin/Fn). This is deterministic.
206
+ * RNG discipline: velocity set-ops MUST appear LAST in the op-program ops array
207
+ * (after any ABSORB/COUNT/MARK/BLOCK ops) so that random() draws land at the
208
+ * same point in the global RNG sequence as the native RANDOM_* dispatch did
209
+ * (which ran AFTER action dispatch in the collision loop).
210
+ *
211
+ * NOTE on 'intersects' (G2b, 2026-06-25):
212
+ * Fn('intersects', [Lit(h0), Lit(h1)]) is a NATIVE GEOMETRY CALL — not numeric
213
+ * arithmetic. It calls the native Rectangle::intersects primitive on the
214
+ * scope-resolved rects for handle h0 and h1 (reading left/right/top/bottom from
215
+ * the scope bag for each handle). Returns 1 (true) or 0 (false).
216
+ *
217
+ * In practice args are always [Lit(HANDLE_FRAME), Lit(HANDLE_SELF)] (frame vs LO).
218
+ * Compiled exclusively by actionCompiler._buildTriggerExpr — do NOT construct
219
+ * manually.
220
+ *
221
+ * This is the canonical overlap test for op-programs (replaces the native
222
+ * overlaps/past_wall gate that was deleted from process_large_object_collisions in
223
+ * G2b). The JS reference (EGPTfrqtl.js) is FROZEN — intersects is a WASM-canonical
224
+ * addition only (JS reference keeps the old native gate).
225
+ *
226
+ * Draw discipline: intersects draws ZERO RNG values (deterministic geometry).
227
+ * @type {string[]}
228
+ */
229
+ var FN_NAMES = Object.freeze(['min', 'max', 'floor', 'random', 'intersects']);
230
+
231
+ // ── Trigger vocabulary ─────────────────────────────────────────────────────────
232
+
233
+ /**
234
+ * TRIGGERS — valid values for program.on.
235
+ * @type {string[]}
236
+ */
237
+ var TRIGGERS = Object.freeze(['collision', 'tick']);
238
+
239
+ // ── Reserved scope handles ─────────────────────────────────────────────────────
240
+
241
+ /**
242
+ * HANDLE_FRAME — scope handle for the colliding frame in on:'collision' programs.
243
+ * Reads Frame properties (§2.3 table: x, y, vx, vy, mass, …).
244
+ * @type {number}
245
+ */
246
+ var HANDLE_FRAME = 0;
247
+
248
+ /**
249
+ * HANDLE_SELF — scope handle for the owning LO/emitter.
250
+ * Reads LO descriptor properties (rect, count, …) or emitter properties.
251
+ * @type {number}
252
+ */
253
+ var HANDLE_SELF = 1;
254
+
255
+ /**
256
+ * HANDLE_UNIVERSE — scope handle for universe read-only state.
257
+ * Properties: 'tick' (u64), 'activeQuanta' (usize).
258
+ * @type {number}
259
+ */
260
+ var HANDLE_UNIVERSE = 2;
261
+
262
+ // ── Node shape helpers (JS-side construction) ──────────────────────────────────
263
+ //
264
+ // These are OPTIONAL helpers for constructing AST nodes from JS sugar.
265
+ // The shapes they produce are the canonical wire format — Rust deserializes
266
+ // the same JSON structure. Using these helpers is not required; you may
267
+ // construct the object literal directly.
268
+
269
+ /**
270
+ * Lit(n) — numeric literal node.
271
+ * @param {number} value
272
+ * @returns {{ kind:'Lit', value:number }}
273
+ */
274
+ function Lit(value) {
275
+ return { kind: 'Lit', value: value };
276
+ }
277
+
278
+ /**
279
+ * Ref(handle, prop) — property read node.
280
+ * @param {number} handle — integer scope handle (HANDLE_FRAME / HANDLE_SELF / HANDLE_UNIVERSE / …)
281
+ * @param {string} prop — property name
282
+ * @returns {{ kind:'Ref', handle:number, prop:string }}
283
+ */
284
+ function Ref(handle, prop) {
285
+ return { kind: 'Ref', handle: handle, prop: prop };
286
+ }
287
+
288
+ /**
289
+ * Bin(op, a, b) — binary arithmetic node.
290
+ * op must be a member of BIN_OPS.
291
+ * @param {string} op
292
+ * @param {*} a — ExprAST
293
+ * @param {*} b — ExprAST
294
+ * @returns {{ kind:'Bin', op:string, a:*, b:* }}
295
+ */
296
+ function Bin(op, a, b) {
297
+ return { kind: 'Bin', op: op, a: a, b: b };
298
+ }
299
+
300
+ /**
301
+ * Cmp(op, a, b) — comparison node. Returns 1 or 0.
302
+ * op must be a member of CMP_OPS.
303
+ * @param {string} op
304
+ * @param {*} a — ExprAST
305
+ * @param {*} b — ExprAST
306
+ * @returns {{ kind:'Cmp', op:string, a:*, b:* }}
307
+ */
308
+ function Cmp(op, a, b) {
309
+ return { kind: 'Cmp', op: op, a: a, b: b };
310
+ }
311
+
312
+ /**
313
+ * Cond(test, then_, else_) — conditional node.
314
+ * @param {*} test — ExprAST; truthy when non-zero
315
+ * @param {*} then_ — ExprAST; result when test != 0
316
+ * @param {*} else_ — ExprAST; result when test == 0
317
+ * @returns {{ kind:'Cond', test:*, then:*, else:* }}
318
+ */
319
+ function Cond(test, then_, else_) {
320
+ return { kind: 'Cond', test: test, then: then_, else: else_ };
321
+ }
322
+
323
+ /**
324
+ * Fn(name, args) — built-in function node.
325
+ * name must be a member of FN_NAMES.
326
+ * @param {string} name
327
+ * @param {Array<*>} args — ExprAST[]
328
+ * @returns {{ kind:'Fn', name:string, args:Array }}
329
+ */
330
+ function Fn(name, args) {
331
+ return { kind: 'Fn', name: name, args: args };
332
+ }
333
+
334
+ /**
335
+ * StrLit(value) — string literal node (NODE_KINDS item 7).
336
+ *
337
+ * ONLY valid as the `to` value of a `set` op whose target is
338
+ * `<handle>.data.<key>` (a string-map write into the frame bubble).
339
+ *
340
+ * ILLEGAL as a child of Bin / Cmp / Fn / Cond.test — eval_expr is
341
+ * numeric-pure. The op-apply layer detects StrLit before calling eval_expr.
342
+ *
343
+ * @param {string} value
344
+ * @returns {{ kind:'StrLit', value:string }}
345
+ */
346
+ function StrLit(value) {
347
+ return { kind: 'StrLit', value: value };
348
+ }
349
+
350
+ // ── Exports ────────────────────────────────────────────────────────────────────
351
+
352
+ export {
353
+ NODE_KINDS,
354
+ OP_KINDS,
355
+ BIN_OPS,
356
+ CMP_OPS,
357
+ FN_NAMES,
358
+ TRIGGERS,
359
+ HANDLE_FRAME,
360
+ HANDLE_SELF,
361
+ HANDLE_UNIVERSE,
362
+ Lit,
363
+ Ref,
364
+ Bin,
365
+ Cmp,
366
+ Cond,
367
+ Fn,
368
+ StrLit
369
+ };