@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,612 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * OvenBox — Engine-agnostic thermostat box for blackbody experiments.
5
+ *
6
+ * Creates a [`Box`](./Box.js) with four inward-firing wall emitters, eject
7
+ * op-programs on every wall (§10.3 — bit-identical to native BOUNCE/WALL, proven
8
+ * EP21–EP28), and a configurable thermostat that holds the cavity at a target
9
+ * quanta count.
10
+ *
11
+ * Engine-agnostic: works with both the JS reference engine and the Rust/WASM engine.
12
+ *
13
+ * ## Walls — op-programs, not native CollisionTypes.WALL
14
+ *
15
+ * Every wall is registered as `collision_type: NONE` with an `on:'collision'`
16
+ * op-program that implements block (L3) + velocity-flip (L4) via pure Bin/Ref/Lit
17
+ * AST expressions. This is §10.3 expressiveness proof (conformance EP21–EP28):
18
+ * the op-program output is **bit-identical** to the native WALL eject on both
19
+ * JS and WASM engines. No native `CollisionType.WALL` / `CollisionActions` are
20
+ * used on the walls themselves.
21
+ *
22
+ * The slit opening (the radiation aperture) is preserved exactly as before — the
23
+ * Box helper creates the top/bottom slit liner segments in the usual way, each
24
+ * getting the same eject+velocity-flip program as the solid walls.
25
+ *
26
+ * Pass `options.layer3 = false` to disable eject op-programs (L1/L2 test mode —
27
+ * all walls become pass-through NONE without any program). Pass
28
+ * `options.layer4 = false` to apply L3 block only without the velocity flip.
29
+ *
30
+ * ## Thermostat — configurable count source
31
+ *
32
+ * The thermostat has two count sources, selected by `options.countSource`:
33
+ *
34
+ * ### `countSource: 'active-quanta'` (default — config a)
35
+ *
36
+ * `getTemperatureTickFunction()` returns a tick function that reads
37
+ * `universe.activeQuanta` each tick and directly CRUDs the four emitters.
38
+ * Push this into `universe.experimentTickFunctions` as scenes/blackbody.js already
39
+ * does — no other wiring needed.
40
+ *
41
+ * ### `countSource: 'oven-rect'` (config b — the "better oven")
42
+ *
43
+ * OvenBox internally builds an **oven-interior counter-LO** as a `Wall`
44
+ * (the Tunnel exit-gate / Tube open-end-sink shape — a physics-only region
45
+ * that is genuinely in the world, not a headless-only raw registration):
46
+ * - rect = the cavity interior (innerLeft, innerTop, innerW, innerH)
47
+ * - `collisionBehavior: 'nothing'`, `visible: false` — collides with
48
+ * nothing and is never drawn (a box over the cavity would read as an
49
+ * obstruction that is not there)
50
+ * - on:'collision' op-program attached after construction, through the
51
+ * same `applyProgramTrigger` authority Tunnel's `_applyExitGateProgram`
52
+ * uses: `set self.count = self.count + 1`
53
+ *
54
+ * The op-program counts cavity occupancy natively in-tick, bit-identical
55
+ * across JS↔WASM. `this.ovenHandle` stores the counter Wall's LO handle.
56
+ *
57
+ * `thermostatStep(tickData)` is the between-tick controller:
58
+ * a. Reset `count` = 0 via `updateObjectProperty` (so next tick starts fresh).
59
+ * b. Read `cavityCount = tickData.objects[ovenHandle].count`.
60
+ * c. Compute burst per emitter.
61
+ * d. Drive the emitters via their own setters (`setBurstSize`/`setActive`) —
62
+ * `QuantumEmitter` is sugar that owns its `emState`; there is no provider
63
+ * emitter ABI to route a `updateObjectProperty` write to.
64
+ *
65
+ * Call `thermostatStep(tickData)` after every `doTick()` call in your harness.
66
+ * `getTemperatureTickFunction()` is NOT applicable for this count source — it
67
+ * throws an explicit error if called with countSource='oven-rect'.
68
+ *
69
+ * ## What you can drive
70
+ *
71
+ * - **`options.temperature`** is the thermostat set-point. Call
72
+ * `setTemperature(n)` to change it live.
73
+ * - **`options.slitHeight`** controls the slit aperture in the slit wall.
74
+ * - **`options.slitWall`** picks which wall face ('right' | 'left' | 'top' |
75
+ * 'bottom') carries the slit. Default: `'right'`.
76
+ *
77
+ * OBSERVED (seed 42, tick 600, engine=js): temperature 300–4000 transitions
78
+ * the cavity from sparse leaf dots to near-saturated compound-frame packing.
79
+ * See `docs/internal/records/physics-lab/2026-06-16-blackbody-temperature.md`.
80
+ */
81
+
82
+ import { Box } from '../objects/objects/Box.js';
83
+ import { QuantumEmitter } from '../objects/objects/QuantumEmitter.js';
84
+ import { LargeObject } from './LargeObject.js';
85
+ import { CollisionTypes } from './CollisionTypes.js';
86
+ import { CollisionActions } from './CollisionActions.js';
87
+ import { Wall } from './Wall.js';
88
+ import * as AstSchema from '../../shim/expr/ast-schema.js';
89
+ import * as ActionCompiler from '../objects/objects/actionCompiler.js';
90
+ import { registerKind } from '../objects/kinds.js';
91
+
92
+ // ── Eject op-program builder (§10.3) ─────────────────────────────────
93
+
94
+ /**
95
+ * Build the collision op-program for a wall.
96
+ *
97
+ * L3 (block): repositions the frame just outside the wall via a set-expression
98
+ * on frame.left or frame.top. Bit-identical to native WALL eject (EP22–EP25).
99
+ *
100
+ * L4 (bounce): appends a velocity-flip op (0 - vx or 0 - vy depending on wall
101
+ * axis). Bit-identical to native BOUNCE (EP27–EP28).
102
+ *
103
+ * @param {string} reposition - 'left' | 'right' | 'top' | 'bottom' | 'closest'
104
+ * @param {string} wallName - Wall identifier, e.g. 'TOP', 'RIGHT_T', 'LEFT_B'
105
+ * @param {boolean} withBounce - true = L4 (eject + velocity flip); false = L3 only
106
+ * @returns {{ on: 'collision', ops: Array }|null} null when reposition='closest'
107
+ */
108
+ function makeEjectProgram(reposition, wallName, withBounce) {
109
+ var ast = AstSchema;
110
+ var Lit = ast.Lit;
111
+ var Ref = ast.Ref;
112
+ var Bin = ast.Bin;
113
+ var Cmp = ast.Cmp;
114
+ var Cond = ast.Cond;
115
+ var Fn = ast.Fn;
116
+ var HANDLE_FRAME = ast.HANDLE_FRAME; // 0
117
+ var HANDLE_SELF = ast.HANDLE_SELF; // 1
118
+
119
+ var ops = [];
120
+ var ejectFlip = ActionCompiler;
121
+
122
+ // ── L3: reposition (eject) op ────────────────────────────────────
123
+ // Q-L1-4 DRY: formulas now come from the shared actionCompiler authority
124
+ // (bit-identical output — same Bin/Ref AST this function used to build inline).
125
+ var ejectOps = ejectFlip.buildEjectOps(reposition, HANDLE_FRAME, HANDLE_SELF, Lit, Ref, Bin, Cmp, Cond, Fn);
126
+ if (!ejectOps) {
127
+ // 'closest' or unknown — no eject program
128
+ return null;
129
+ }
130
+ for (var ei = 0; ei < ejectOps.length; ei++) ops.push(ejectOps[ei]);
131
+
132
+ // ── L4: velocity-flip op ─────────────────────────────────────────
133
+ // Q-L1-4 DRY: shared authority (native BOUNCE flip formula, EGPTfrqtl.js:1159-1168).
134
+ if (withBounce) {
135
+ var flipOp = ejectFlip.buildVelocityFlipOp(wallName, HANDLE_FRAME, Lit, Ref, Bin);
136
+ if (flipOp) ops.push(flipOp);
137
+ }
138
+
139
+ if (ops.length === 0) return null;
140
+
141
+ // G2b: attach the program-level trigger (single authority: actionCompiler).
142
+ // The trigger is evaluated ONCE in run_op_program_with_frame BEFORE any ops modify
143
+ // the scope. This prevents the per-op Cond bug where the eject op's scope write
144
+ // caused the bounce op's intersects check to return false (G2b per-op Cond bug,
145
+ // 2026-06-25).
146
+ //
147
+ // OvenBox walls use withPastEdge=true because in G2 the native is_frame_past_wall()
148
+ // check fired on any LO whose name matched 'LEFT'/'RIGHT'/'TOP'/'BOTTOM' (which
149
+ // OvenBox walls do via Box.js naming). That check caught frames that slipped THROUGH
150
+ // the wall in one tick (current position past the wall exterior face). Without this,
151
+ // a frame that passes through the RIGHT wall exits the box and the thermostat does
152
+ // not account for it → divergence from the golden (blackbody tick=69: +1 total_mass).
153
+ var helpers = ActionCompiler;
154
+ var trigger = helpers.buildTriggerExpr(
155
+ reposition, true,
156
+ HANDLE_FRAME, HANDLE_SELF,
157
+ Lit, Ref, Cmp, Cond, Fn
158
+ );
159
+ var program = { on: 'collision', ops: ops };
160
+ helpers.applyProgramTrigger(program, trigger);
161
+ return program;
162
+ }
163
+
164
+ // ── OvenBox ─────────────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Thermostat box with wall op-programs and configurable thermostat for blackbody
168
+ * radiation experiments.
169
+ *
170
+ * @param {Object} universe - The engine universe instance (JS or WASM).
171
+ * @param {Object} options - Configuration.
172
+ * @param {number} options.left - Inner box left X (physics pixels).
173
+ * @param {number} options.top - Inner box top Y (physics pixels).
174
+ * @param {number} options.width - Inner box width (physics pixels).
175
+ * @param {number} options.height - Inner box height (physics pixels).
176
+ * @param {number} [options.wallThickness=10] - Wall thickness (pixels).
177
+ * @param {number} [options.temperature=300] - Initial thermostat target (quanta
178
+ * count). Change live via `setTemperature(n)`.
179
+ * @param {number} [options.slitHeight=60] - Slit aperture height on the slit
180
+ * wall (pixels). Affects how many frames can escape per tick.
181
+ * @param {string} [options.slitWall='right'] - Wall face for the slit:
182
+ * `'right'` | `'left'` | `'top'` | `'bottom'`.
183
+ * @param {Array<number>} [options.color=[120,120,120]] - Wall color `[r,g,b]`.
184
+ * @param {string} [options.name='oven'] - The oven's name (its declared-object row).
185
+ * @param {string} [options.countSource='active-quanta'] - Thermostat count source:
186
+ * `'active-quanta'` reads `universe.activeQuanta` each tick (config a —
187
+ * original behaviour, compatible with `getTemperatureTickFunction()`);
188
+ * `'oven-rect'` registers an interior counter-LO and uses the collision count
189
+ * from `tickData.objects[ovenHandle].count` (config b — the "better oven";
190
+ * use `thermostatStep(tickData)` instead of `getTemperatureTickFunction()`).
191
+ * @param {boolean} [options.layer3=true] - Attach eject op-programs to walls
192
+ * (L3 block). Set false for L1/L2 test isolation (walls become pass-through).
193
+ * @param {boolean} [options.layer4=true] - Append velocity-flip to eject
194
+ * op-programs (L4 bounce = L3 + flip). Requires layer3=true; ignored if
195
+ * layer3=false.
196
+ *
197
+ * @deprecated V1 sugar, kept for byte-identical scenes (blackbody). V2 direction: Wall{ collisionBehavior:'absorb' } + SphereEmitter{ emissionCap, gateCounter }.
198
+ */
199
+ function OvenBox(universe, options) {
200
+ options = options || {};
201
+
202
+ this.universe = universe;
203
+ this._schemaKind = 'OvenBox';
204
+ this.name = options.name || 'oven'; // its declared name (the Tunnel/LogicGate idiom)
205
+ this.temperature = options.temperature || 300;
206
+
207
+ var wt = options.wallThickness || 10;
208
+ var slitHeight = options.slitHeight || 60;
209
+ var slitWall = options.slitWall || 'right';
210
+ var USE_L3 = options.layer3 !== undefined ? options.layer3 : true;
211
+ var USE_L4 = USE_L3 && (options.layer4 !== undefined ? options.layer4 : true);
212
+
213
+ // ── Box: walls as NONE (op-programs carry the physics) ──────────
214
+ // Walls are registered with collision_type=NONE so the engine does NOT
215
+ // run its native eject/bounce logic. The op-programs below provide
216
+ // the equivalent behavior as pure set-expressions (§10.3, EP21–EP28).
217
+ this.box = new Box(universe, {
218
+ left: options.left,
219
+ top: options.top,
220
+ width: options.width,
221
+ height: options.height,
222
+ wallThickness: wt,
223
+ collisionType: CollisionTypes.NONE,
224
+ actions: [],
225
+ color: options.color || [120, 120, 120],
226
+ slit: { wall: slitWall, height: slitHeight }
227
+ });
228
+
229
+ // Expose walls from the box
230
+ this.walls = this.box.walls;
231
+
232
+ var innerLeft = options.left;
233
+ var innerTop = options.top;
234
+ var innerW = options.width;
235
+ var innerH = options.height;
236
+
237
+ // ── L3/L4: attach eject op-programs to all walls ─────────────────
238
+ // Each wall carries a collision op-program that repositions overlapping
239
+ // frames via pure set-expressions (L3 block) plus a velocity flip
240
+ // (L4 bounce). wall.reposition and wall.name are set by Box.js.
241
+ // universe.updateObjectProperty routes through the schema and the engine's
242
+ // native program registration — no engine-sniffing here.
243
+ if (USE_L3) {
244
+ for (var wi = 0; wi < this.walls.length; wi++) {
245
+ var wall = this.walls[wi];
246
+ var ejProg = makeEjectProgram(wall.reposition, wall.name, USE_L4);
247
+ if (ejProg) {
248
+ universe.updateObjectProperty(
249
+ wall.handle, 'LargeObject', 'program', ejProg
250
+ );
251
+ }
252
+ }
253
+ }
254
+
255
+ // ── Emitters (4 inward-firing wall emitters) ─────────────────────
256
+ // Outer rect edges — walls extend outward from the inner box by wt.
257
+ var outerLeft = innerLeft - wt;
258
+ var outerTop = innerTop - wt;
259
+ var outerW = innerW + 2 * wt;
260
+ var outerH = innerH + 2 * wt;
261
+
262
+ // auto_fire=false — the thermostat controls emission each tick.
263
+ this.emitters = [];
264
+
265
+ // Left wall emitter (shoots right)
266
+ this.emitters.push(new QuantumEmitter(
267
+ universe,
268
+ [[outerLeft + wt + 4, outerTop + 2, 1, outerH - 4]],
269
+ 1, 0,
270
+ 1, null, false, false, Infinity, 0
271
+ ));
272
+
273
+ // Right wall emitter (shoots left)
274
+ this.emitters.push(new QuantumEmitter(
275
+ universe,
276
+ [[outerLeft + outerW - wt - 4, outerTop + 2, 1, outerH - 4]],
277
+ -1, 0,
278
+ 1, null, false, false, Infinity, 0
279
+ ));
280
+
281
+ // Top wall emitter (shoots down)
282
+ this.emitters.push(new QuantumEmitter(
283
+ universe,
284
+ [[outerLeft + 2 * wt + 2, outerTop + wt + 5, outerW - 2 * wt - 4, 1]],
285
+ 0, 1,
286
+ 1, null, false, false, Infinity, 0
287
+ ));
288
+
289
+ // Bottom wall emitter (shoots up)
290
+ this.emitters.push(new QuantumEmitter(
291
+ universe,
292
+ [[outerLeft + 2 * wt + 2, outerTop + outerH - wt - 2, outerW - 2 * wt - 4, 1]],
293
+ 0, -1,
294
+ 1, null, false, false, Infinity, 0
295
+ ));
296
+
297
+ // THE ONE UP-LINK: the oven stamps exactly what IT composed — its Box (which stamps its own
298
+ // walls) and its four emitters. The counter wall (oven-rect) is stamped where it is built.
299
+ this.box._owner = this;
300
+ for (var _oi = 0; _oi < this.emitters.length; _oi++) { this.emitters[_oi]._owner = this; }
301
+
302
+ // ── Count source: oven-rect counter-LO (config b) ────────────────
303
+ // When countSource='oven-rect', register an interior LO whose
304
+ // on:'collision' op-program counts frames inside the cavity IN-TICK.
305
+ // The between-tick controller reads tickData.objects[ovenHandle].count,
306
+ // resets count to 0, and CRUDs emitters (as-intended §2.1).
307
+ //
308
+ // This is the ONE config line that distinguishes (a) from (b):
309
+ // config b passes countSource: 'oven-rect'
310
+ //
311
+ // The ovenHandle and thermostatStep method are only set up when countSource
312
+ // is 'oven-rect'. For 'active-quanta' (the default), getTemperatureTickFunction()
313
+ // is the entry point instead.
314
+
315
+ var countSource = options.countSource || 'active-quanta';
316
+ this._countSource = countSource;
317
+ this.ovenHandle = null; // set below for 'oven-rect'
318
+ this._counterWall = null; // the oven-rect counter Wall, when built (disposed with the oven)
319
+
320
+ if (countSource === 'oven-rect') {
321
+ // Construct the interior counter-LO through the Wall facade — the SAME
322
+ // shape Tunnel's exit gate uses (collisionBehavior:'nothing', visible:false,
323
+ // a custom op-program attached AFTER construction through the actionCompiler
324
+ // authority). The raw `universe.registerLargeObject(spec)` path this used to
325
+ // take registers with the ENGINE only, not the world's render census — an
326
+ // invisible-by-construction instrument region is exactly the "physics-
327
+ // registered, render-invisible" case FrqtlApp._assertRenderCensus refuses
328
+ // (FAIL-LOUD, e8b579e). A Wall with visible:false is the sanctioned way to
329
+ // build a physics-only region that IS in the census on purpose — the
330
+ // Tunnel/Tube precedent, not a design choice made here.
331
+ var ovenCounterWall = new Wall(universe, {
332
+ rect: [innerLeft, innerTop, innerW, innerH],
333
+ collisionBehavior: 'nothing',
334
+ visible: false,
335
+ name: 'oven_counter'
336
+ });
337
+ ovenCounterWall._owner = this;
338
+ this._counterWall = ovenCounterWall;
339
+
340
+ // op-program: set self.count = self.count + 1 (on:'collision')
341
+ // G2b: Add intersects trigger so only frames overlapping the oven rect are counted.
342
+ // Without a trigger, the engine runs this program for EVERY frame in the
343
+ // search_rect (search_rect = lo_rect + 1px padding). Frames in the 1px padding
344
+ // zone would be counted as "inside" → thermostat fires fewer emitters →
345
+ // different emission schedule from G2 (which had the native overlaps gate).
346
+ var _ast2 = AstSchema;
347
+ var _Lit2 = _ast2.Lit, _Ref2 = _ast2.Ref, _Bin2 = _ast2.Bin, _Fn2 = _ast2.Fn;
348
+ var _HF2 = _ast2.HANDLE_FRAME;
349
+ var _HS2 = _ast2.HANDLE_SELF;
350
+ var _triggerHelpers2 = ActionCompiler;
351
+ var _ovenTrigger = _triggerHelpers2.buildTriggerExpr(
352
+ null, false, // no eject direction; intersects-only
353
+ _HF2, _HS2,
354
+ _Lit2, _Ref2,
355
+ function(op,a,b){return{kind:'Cmp',op:op,a:a,b:b};},
356
+ function(t,th,el){return{kind:'Cond',test:t,then:th,else:el};},
357
+ _Fn2
358
+ );
359
+ var ovenCounterProgram = {
360
+ on: 'collision',
361
+ ops: [
362
+ { set: _HS2 + '.count',
363
+ to: _Bin2('+', _Ref2(_HS2, 'count'), _Lit2(1)) }
364
+ ]
365
+ };
366
+ // Attach the trigger through the SAME authority Tunnel's own
367
+ // _applyExitGateProgram uses (actionCompiler's applyProgramTrigger — a plain
368
+ // `program.trigger = trigger` setter), then push through the SAME live path
369
+ // as the Wall's own construction-time program push (lo.program + the
370
+ // 'LargeObject'/'program' updateObjectProperty write). Op-program DATA is
371
+ // byte-identical to before; only the JS wrapper (and therefore the census) changes.
372
+ _triggerHelpers2.applyProgramTrigger(ovenCounterProgram, _ovenTrigger);
373
+ ovenCounterWall.lo.program = ovenCounterProgram;
374
+ if (universe && typeof universe.updateObjectProperty === 'function') {
375
+ universe.updateObjectProperty(ovenCounterWall.lo.handle, 'LargeObject', 'program', ovenCounterProgram);
376
+ }
377
+
378
+ this.ovenHandle = ovenCounterWall.lo.handle;
379
+
380
+ // Between-tick thermostat (as-intended §2.1, Dispatch C2):
381
+ // a. Reset count to 0 so next tick starts fresh.
382
+ // b. Read cavityCount from tickData.objects[ovenHandle].count (B5.1).
383
+ // c. Compute burst per emitter.
384
+ // d. Drive the emitters through their own setters (see below).
385
+ var self = this;
386
+ this.thermostatStep = function (tickData) {
387
+ // a. Reset oven counter for the next tick.
388
+ universe.updateObjectProperty(
389
+ self.ovenHandle, 'LargeObject', 'count', 0
390
+ );
391
+
392
+ // b. Read cavity occupancy — ONE read authority (B5.1, B2.1).
393
+ var cavityCount = 0;
394
+ if (tickData && tickData.objects &&
395
+ tickData.objects[self.ovenHandle] !== undefined) {
396
+ cavityCount = tickData.objects[self.ovenHandle].count || 0;
397
+ }
398
+
399
+ // c. Burst per emitter.
400
+ var burst = 0;
401
+ if (cavityCount < self.temperature) {
402
+ burst = Math.max(1, Math.floor(
403
+ (self.temperature - cavityCount) / self.emitters.length
404
+ ));
405
+ }
406
+
407
+ // d. Drive the emitters DIRECTLY through their own setters.
408
+ // QuantumEmitter is engine-agnostic SUGAR: the real state lives in
409
+ // its emState bags and there is NO provider emitter ABI, so
410
+ // `universe.updateObjectProperty(handle,'QuantumEmitter',...)` has
411
+ // nothing to route to. It used to be called here and silently
412
+ // vanished (the whole oven-rect thermostat was therefore dead);
413
+ // since 5cecffea it throws. `getTemperatureTickFunction()` — the
414
+ // 'active-quanta' twin of this loop — has always driven the setters
415
+ // directly; this is the same discipline, one count source over.
416
+ // setBurstSize BEFORE setActive: setActive(true) may register a new
417
+ // emState off _specTemplate, which setBurstSize has just updated.
418
+ for (var i = 0; i < self.emitters.length; i++) {
419
+ if (burst > 0) { self.emitters[i].setBurstSize(burst); }
420
+ self.emitters[i].setActive(burst > 0);
421
+ }
422
+ };
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Get the thermostat tick function for `countSource: 'active-quanta'` (config a).
428
+ *
429
+ * Push this onto `universe.experimentTickFunctions` for automatic temperature
430
+ * control. The tick function runs each tick BEFORE Phase A (emitter injection):
431
+ * - If `activeQuanta >= temperature`: silence all four emitters.
432
+ * - Otherwise: set each emitter's burst size to
433
+ * `max(1, floor((temperature - activeQuanta) / 4))` and activate them.
434
+ *
435
+ * This is the ORIGINAL thermostat behavior — identical to the pre-convergence
436
+ * OvenBox and compatible with scenes/blackbody.js (which calls this method
437
+ * without arguments).
438
+ *
439
+ * NOT applicable when `countSource: 'oven-rect'` — use `thermostatStep(tickData)`
440
+ * instead. Calling this method with countSource='oven-rect' throws FAIL-LOUD.
441
+ *
442
+ * @returns {Function} Tick function for `universe.experimentTickFunctions`.
443
+ */
444
+ OvenBox.prototype.getTemperatureTickFunction = function () {
445
+ if (this._countSource === 'oven-rect') {
446
+ throw new Error(
447
+ '[OvenBox] getTemperatureTickFunction() is not applicable when ' +
448
+ 'countSource=\'oven-rect\'. Use thermostatStep(tickData) instead — ' +
449
+ 'call it after every doTick() in your harness.'
450
+ );
451
+ }
452
+ var self = this;
453
+ return function () {
454
+ var activeQuanta = typeof self.universe.activeQuanta === 'function'
455
+ ? self.universe.activeQuanta()
456
+ : self.universe.activeQuanta;
457
+ var remainingBurst = Math.floor(self.temperature - activeQuanta);
458
+
459
+ if (remainingBurst <= 0) {
460
+ for (var i = 0; i < self.emitters.length; i++) {
461
+ self.emitters[i].setActive(false);
462
+ }
463
+ return [];
464
+ }
465
+
466
+ var burstPerEmitter = Math.max(1, Math.floor(remainingBurst / self.emitters.length));
467
+ for (var i = 0; i < self.emitters.length; i++) {
468
+ self.emitters[i].setBurstSize(burstPerEmitter);
469
+ self.emitters[i].setActive(true);
470
+ }
471
+ return [];
472
+ };
473
+ };
474
+
475
+ /**
476
+ * Set the target temperature (quanta count).
477
+ * Works for both count sources. For 'oven-rect', the thermostatStep closure
478
+ * reads `this.temperature` directly — no AST rebuild needed.
479
+ * @param {number} temp
480
+ */
481
+ OvenBox.prototype.setTemperature = function (temp) {
482
+ this.temperature = temp;
483
+ };
484
+
485
+ /**
486
+ * Get the current target temperature.
487
+ * @returns {number}
488
+ */
489
+ OvenBox.prototype.getTemperature = function () {
490
+ return this.temperature;
491
+ };
492
+
493
+ /**
494
+ * Full teardown of what this oven composed: the four emitters (each splices its own host
495
+ * tick-fn), the Box (its walls), and the oven-rect counter wall when present. Idempotent
496
+ * (every member guards itself).
497
+ */
498
+ OvenBox.prototype.dispose = function () {
499
+ for (var i = 0; i < this.emitters.length; i++) { this.emitters[i].dispose(); }
500
+ this.box.dispose();
501
+ if (this._counterWall) { this._counterWall.dispose(); }
502
+ };
503
+
504
+ // ── The kind (objects/kinds.js): descriptor, construction defaults, builder ──────────
505
+
506
+ /**
507
+ * OvenBox — Thermostat box with wall emitters for blackbody experiments.
508
+ * Constructor: OvenBox(universe, options)
509
+ * options: { left, top, width, height, wallThickness, temperature, slitHeight, slitWall, color }
510
+ * Group wrapper: its Box (which stamps its own walls) and 4 QuantumEmitter sub-objects are
511
+ * stamped with the ONE up-link `_owner` = <this>.
512
+ */
513
+ var OvenBoxDescriptor = {
514
+ description: 'A thermostat-controlled cavity for blackbody radiation experiments. ' +
515
+ 'Builds a Box with a slit on one wall and installs 4 inward-firing wall emitters ' +
516
+ '(top, bottom, left, right). A tick function adjusts emitter burst sizes to maintain ' +
517
+ 'the target quanta count (temperature). Demonstrates how energy equilibrium emerges ' +
518
+ 'from local emitter control rather than a global energy equation.',
519
+ fields: [
520
+ {
521
+ name: 'left',
522
+ type: 'number',
523
+ constraint: { integer: true },
524
+ engineVisible: true,
525
+ description: 'Inner box left X position (physics pixels).'
526
+ },
527
+ {
528
+ name: 'top',
529
+ type: 'number',
530
+ constraint: { integer: true },
531
+ engineVisible: true,
532
+ description: 'Inner box top Y position (physics pixels).'
533
+ },
534
+ {
535
+ name: 'width',
536
+ type: 'number',
537
+ constraint: { min: 1, integer: true },
538
+ engineVisible: true,
539
+ description: 'Inner box width (physics pixels).'
540
+ },
541
+ {
542
+ name: 'height',
543
+ type: 'number',
544
+ constraint: { min: 1, integer: true },
545
+ engineVisible: true,
546
+ description: 'Inner box height (physics pixels).'
547
+ },
548
+ {
549
+ name: 'wallThickness',
550
+ type: 'number',
551
+ constraint: { min: 1, integer: true },
552
+ engineVisible: true,
553
+ description: 'Wall thickness in physics pixels (default 10).'
554
+ },
555
+ {
556
+ name: 'temperature',
557
+ type: 'number',
558
+ constraint: { min: 1, integer: true },
559
+ engineVisible: false,
560
+ widgetConfig: { max: 3000, step: 50 }, // slider range (constraint has no max)
561
+ description: 'Target quanta count inside the cavity (the "temperature"). ' +
562
+ 'The thermostat tick function increases emission until this count is reached, ' +
563
+ 'then stops all emitters. Higher = more particles = higher "temperature".'
564
+ },
565
+ {
566
+ name: 'slitHeight',
567
+ type: 'number',
568
+ constraint: { min: 1, integer: true },
569
+ engineVisible: true,
570
+ unit: 'px',
571
+ description: 'Height of the slit opening on the exit wall (physics pixels, default 60). ' +
572
+ 'Particles leak through this slit to the outside for blackbody spectrum measurement.'
573
+ },
574
+ {
575
+ name: 'color',
576
+ type: 'rgb',
577
+ engineVisible: false,
578
+ live: true,
579
+ description: 'Render color [r, g, b] for the box walls (render-only). A live edit fans ' +
580
+ 'out to every wall the group owns (the `_owner` fan-out in updateObjectProperty).'
581
+ }
582
+ ]
583
+ };
584
+
585
+ registerKind(OvenBox, {
586
+ from: import.meta.url,
587
+ descriptor: OvenBoxDescriptor,
588
+ defaults: {
589
+ left: 60, top: 40,
590
+ width: 100, height: 120,
591
+ wallThickness: 10,
592
+ temperature: 300,
593
+ slitHeight: 60,
594
+ color: [120, 120, 120]
595
+ },
596
+ build: function (props, ctx, Class) {
597
+ var OvenBoxCls = Class; // the class that registered (a composed fork's own class)
598
+ var ovenBox = new OvenBoxCls(ctx.runner.universe, Object.assign({
599
+ left: props.left != null ? props.left : Math.round(ctx.x - 50),
600
+ top: props.top != null ? props.top : Math.round(ctx.y - 60),
601
+ width: 100, height: 120,
602
+ wallThickness: 10, temperature: 300, slitHeight: 60, color: [120, 120, 120]
603
+ }, props));
604
+ ovenBox._schemaKind = Class.name;
605
+ return ovenBox;
606
+ }
607
+ });
608
+
609
+ // ── Export ───────────────────────────────────────────────────────────
610
+
611
+ export { OvenBox };
612
+