@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,310 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * absorbRadiation.js — ABSORB v2 on the LO's REAL backing-frame mass (Track A).
5
+ *
6
+ * CEO mass-surface ruling (Living Log 384ef5ee): use the OBJECT approach, not a
7
+ * duplicative op-prop mass surface. A LargeObject is backed by a REAL leaf Frame, so the
8
+ * absorbed mass lands in a real frame's `mass` reachable through the SAME frame accessors
9
+ * everything else uses.
10
+ *
11
+ * MANDATORY BACKING (CEO dry-realign 8d45fcdb): the backing frame is now created by the engine
12
+ * at `register_large_object` (every LO is frame-backed — no unbacked path). This sugar READS
13
+ * that ledger via `getLargeObjectFrameId` instead of creating + attaching one.
14
+ *
15
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
16
+ * `attachAbsorberBackingFrame(universe, loHandle, rect, capacity)` → READS the LO's
17
+ * registration-created backing leaf (store-resident mass ledger — uninjected, zero
18
+ * single-occupancy / quadtree interaction, theorist §2a), primes it with the mass-1 empty
19
+ * baseline, and returns `{ backingFrameHandle, backingQid, level, capacity }`. `capacity` is
20
+ * the absorb CLOCK — the fill ceiling and the `1/capacity` emission probability — snapped to
21
+ * the nearest LEVEL the engine can actually size (`level` is that quantization's record).
22
+ * `buildAbsorbRadiationTickFn(cfg)` → a host tick-fn (the emitter/thermostat seam)
23
+ * that each tick: (1) tallies the collected KILLED colliders' masses into the
24
+ * backing frame's REAL mass, KEYED BY THE LAYER each collider came from; (2) at-cap gating —
25
+ * deactivates the LO's collision when full, reactivates when radiation drains it below cap;
26
+ * (3) radiates a leaf with probability 1/capacity, AT one of the layers it absorbed from and
27
+ * carrying `emissivity` of that layer's cell capacity, decrementing EXACTLY. Mass is
28
+ * conserved exactly, now PER LAYER as well as in total.
29
+ *
30
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
31
+ * A wall that fills up as it absorbs, stops soaking at capacity, and drips the stored mass
32
+ * back out as leaves OF THE SCALE IT SOAKED — with NO mass created or destroyed (Invariant
33
+ * 5/7). This is the blackbody absorb/re-emit paradigm as pure sugar.
34
+ *
35
+ * EMISSIVITY is the wall's radiating efficiency, expressed the only way this engine expresses
36
+ * "how much of a body is there": as a FULLNESS FRACTION of the emitted leaf. 1.0 emits a FULL
37
+ * leaf (fullness 1 — light speed, shortest wavelength); 0.5 (the default) emits a half-full
38
+ * one, which moves at half the gate probability and carries twice the wavelength. Emissivity
39
+ * is therefore a colour/temperature knob, not a bookkeeping constant: it sets what the
40
+ * re-emitted quantum IS, and speed and λ fall out of that together.
41
+ */
42
+
43
+ /**
44
+ * The default radiating efficiency for an absorber that never sets one (CEO ruling
45
+ * 2026-08-11). A half-full leaf: it steps half the ticks and carries twice the wavelength
46
+ * of a full one — a visibly "warm" re-emitter rather than a light-speed one.
47
+ */
48
+ var DEFAULT_EMISSIVITY = 0.5;
49
+
50
+ /**
51
+ * Emissivity is a FULLNESS fraction, so it lives in (0, 1] — a fullness above 1 is a move
52
+ * gate above 1, which cannot mean anything. FAIL LOUD rather than clamp: a silently
53
+ * clamped 2.0 would look like it worked and quietly emit light-speed leaves.
54
+ *
55
+ * NOTE (open, see the datasheet): 0 is REFUSED, not read as "never re-emit". The ruled mass
56
+ * law floors the emitted mass at 1, so a 0 could not actually suppress emission — accepting
57
+ * it would be a silent lie. A true perfect-absorber knob is a separate, unruled question.
58
+ */
59
+ function _validEmissivity(v, whence) {
60
+ var n = Number(v);
61
+ if (!isFinite(n) || n <= 0 || n > 1) {
62
+ throw new Error('[' + whence + '] emissivity must be a fullness fraction in (0, 1] — got ' +
63
+ JSON.stringify(v) + '. Above 1 is a move gate above 1; 0 is refused because the ' +
64
+ 'emitted-mass law floors at 1 cell and could not honour it.');
65
+ }
66
+ return n;
67
+ }
68
+
69
+ /**
70
+ * Snap a requested absorb-clock capacity to a LEVEL the engine can actually size, by ASKING
71
+ * the engine (`universe.levelCapacity`) rather than inverting `4^L` — which is the A={x,y}
72
+ * answer only. Returns the smallest level whose real capacity covers the request, so the
73
+ * clock never promises a ceiling the frame has no cells for. Minimum level 1 (an LO is a
74
+ * leaf frame at Dim>0, CEO round-2 (3)).
75
+ */
76
+ function deriveBackingLevel(universe, capacity) {
77
+ var want = Math.max(1, Number(capacity) || 1);
78
+ for (var lvl = 1; lvl <= 64; lvl++) {
79
+ if (universe.levelCapacity(lvl) >= want) { return lvl; }
80
+ }
81
+ throw new Error('[deriveBackingLevel] no leaf level up to 64 reaches the requested absorb ' +
82
+ 'capacity ' + want + ' on this universe\'s active axes.');
83
+ }
84
+
85
+ // Read [cx, cy] centre from a rect ([l,t,w,h] / [l,t,z,w,h,d] / {left,top,w,h} / Rectangle).
86
+ function _rectCenter(rect) {
87
+ if (Array.isArray(rect)) {
88
+ if (rect.length === 6) { return [rect[0] + rect[3] / 2, rect[1] + rect[4] / 2]; }
89
+ return [rect[0] + rect[2] / 2, rect[1] + rect[3] / 2];
90
+ }
91
+ var l = (rect.left !== undefined) ? rect.left : rect._left;
92
+ var t = (rect.top !== undefined) ? rect.top : rect._top;
93
+ return [l + rect.w / 2, t + rect.h / 2];
94
+ }
95
+ // Right-face exit point (radiated leaves are placed OUTSIDE the wall so they fly
96
+ // away rather than immediately re-colliding with their own absorber).
97
+ function _rectRightExit(rect) {
98
+ var c = _rectCenter(rect);
99
+ var right;
100
+ if (Array.isArray(rect)) { right = (rect.length === 6) ? (rect[0] + rect[3]) : (rect[0] + rect[2]); }
101
+ else { var l = (rect.left !== undefined) ? rect.left : rect._left; right = l + rect.w; }
102
+ return [right, c[1]];
103
+ }
104
+
105
+ // Per-layer ledger helpers (Map<layer:number, mass:number>).
106
+ function _bump(map, layer, delta) {
107
+ map.set(layer, (map.get(layer) || 0) + delta);
108
+ }
109
+ function _layersAscending(map) {
110
+ var out = [];
111
+ map.forEach(function (_v, k) { out.push(k); });
112
+ out.sort(function (a, b) { return a - b; });
113
+ return out;
114
+ }
115
+
116
+ /**
117
+ * READ the LO's mandatory backing leaf (created at registration) and prime it as the
118
+ * absorber's mass ledger. Returns the backing-frame descriptor.
119
+ *
120
+ * `level`/`capacity` are the ABSORB-CLOCK knobs (how fast the wall fills and how often it
121
+ * radiates), derived from the requested `capacity` through the engine's published capacity
122
+ * law — independent of the backing frame's own geometric level. `rect` is retained in the
123
+ * signature for call-site compatibility but is no longer used (the engine owns the center).
124
+ */
125
+ function attachAbsorberBackingFrame(universe, loHandle, rect, capacity) { // eslint-disable-line no-unused-vars
126
+ var level = deriveBackingLevel(universe, capacity);
127
+ var realCap = universe.levelCapacity(level);
128
+ var fh = universe.getLargeObjectFrameId(loHandle); // the registration-created ledger
129
+ var backingQid = universe.getFrameQuantumId(fh);
130
+ // Start EMPTY (mass 1 = nearly-empty floor; mass 0 risks the cap/mass wavelength write).
131
+ // The +1 baseline is subtracted out of every conservation reading below.
132
+ universe.setFrameProperty(backingQid, 'mass', 1);
133
+ return { backingFrameHandle: fh, backingQid: backingQid, level: level, capacity: realCap };
134
+ }
135
+
136
+ /**
137
+ * Build the ABSORB radiation host tick-fn. `cfg`:
138
+ * universe, loHandle, backingQid, capacity, rect, [emissivity], [loActiveSetter], [exit]
139
+ * Exposes:
140
+ * `.stats()` → { stored, absorbedTotal, radiatedTotal, byLayer[] } for tests/readouts
141
+ * `.emissivity()` / `.setEmissivity(v)` → the LIVE radiating-efficiency knob
142
+ * `.setRect(rect)` → the wall moved: the exit follows it (Wall.setRect calls this)
143
+ * `stored`/`absorbedTotal`/`radiatedTotal` all EXCLUDE the mass-1 backing baseline.
144
+ */
145
+ function buildAbsorbRadiationTickFn(cfg) {
146
+ var universe = cfg.universe, loHandle = cfg.loHandle, backingQid = cfg.backingQid;
147
+ var capacity = cfg.capacity, rect = cfg.rect;
148
+ var exit = cfg.exit || _rectRightExit(rect);
149
+ // Emission happens AT a layer the wall absorbed from (per-layer ledgers below), and an
150
+ // absorbed frame lived in its own dimension — so every emission layer already exists.
151
+ // Nothing here can make routing allocate a layer, and so nothing here can re-base the
152
+ // world's floor (universe.rs::route_injected_frames).
153
+ var BASE = 1; // the mass-1 empty baseline (see attach)
154
+ var absorbedTotal = 0, radiatedTotal = 0, referenceMass = BASE;
155
+ var emissivity = (cfg.emissivity === undefined || cfg.emissivity === null)
156
+ ? DEFAULT_EMISSIVITY
157
+ : _validEmissivity(cfg.emissivity, 'buildAbsorbRadiationTickFn');
158
+
159
+ // Per-layer provenance of the ONE scalar backing mass. `stored` is what is still held
160
+ // from each layer; `absorbed`/`radiated` are cumulative. The invariant this exists to
161
+ // make checkable: for EVERY layer, stored + radiated == absorbed, and the stored values
162
+ // sum to the backing frame's real mass.
163
+ var absorbedByLayer = new Map(), radiatedByLayer = new Map(), storedByLayer = new Map();
164
+
165
+ function setActive(active) {
166
+ if (typeof cfg.loActiveSetter === 'function') { cfg.loActiveSetter(active); }
167
+ else if (universe && typeof universe.setLargeObjectActive === 'function') {
168
+ universe.setLargeObjectActive(loHandle, active);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * MULTI-LAYER APPORTIONING — an LO that observes collisions at several layers radiates
174
+ * at EACH layer in proportion to the mass it absorbed from that layer's collisions, so
175
+ * conservation holds per layer and not merely in total.
176
+ *
177
+ * PENDING CEO CONFIRM (orchestrator's interpretation of the CEO's direction, 2026-08-11): the
178
+ * proportion is drawn against each layer's UNRADIATED stored mass, which is what makes
179
+ * "in proportion to what it absorbed from that layer" exactly true in the ledger rather
180
+ * than only in expectation — a layer that has already given back everything it soaked
181
+ * stops being a candidate instead of borrowing another layer's mass.
182
+ *
183
+ * The draw is SKIPPED when only one layer is in play, so a single-layer world consumes
184
+ * exactly the RNG stream it did before this rule existed.
185
+ */
186
+ function pickEmissionLayer() {
187
+ var layers = _layersAscending(storedByLayer).filter(function (L) {
188
+ return (storedByLayer.get(L) || 0) > 0;
189
+ });
190
+ if (layers.length === 0) { return null; }
191
+ if (layers.length === 1) { return layers[0]; }
192
+ var total = 0, i;
193
+ for (i = 0; i < layers.length; i++) { total += storedByLayer.get(layers[i]); }
194
+ var draw = ((typeof universe.random === 'function') ? universe.random() : Math.random()) * total;
195
+ for (i = 0; i < layers.length; i++) {
196
+ draw -= storedByLayer.get(layers[i]);
197
+ if (draw < 0) { return layers[i]; }
198
+ }
199
+ return layers[layers.length - 1];
200
+ }
201
+
202
+ var tick = function absorbRadiationTick(arg) {
203
+ // Resolve the tickData carrying THIS/last tick's collected KILLED frames.
204
+ //
205
+ // This host tick-fn rides `universe.experimentTickFunctions`, which the engine calls
206
+ // as `fn(universe)` BEFORE do_tick (the Phase-A emitter/thermostat seam). But a radiation
207
+ // consumer needs the POST-tick output (TickData.objects[handle].frames). So when called
208
+ // with the universe (no `.objects`), read the universe's cached LAST tickData — a
209
+ // one-tick lag; mass is still exactly conserved. The bench/tests call `fn(tickData)`
210
+ // directly (same-tick). The ORIGINAL K5-realm bug was reading `universe.objects`
211
+ // (undefined) here → `absorbedThisTick=0` every tick → silent zero, no radiation.
212
+ var tickData = (arg && arg.objects) ? arg
213
+ : (universe && universe._lastTickData) ? universe._lastTickData
214
+ : null;
215
+
216
+ // (1) tally the collected KILLED colliders into the backing frame's REAL mass,
217
+ // recording WHICH LAYER each one came from. The cap clamp is applied per
218
+ // collider (in collection order) rather than to the tick's sum: the landed
219
+ // total is identical, and every layer's share of it is then exact — no
220
+ // proportional fudge to apportion a clamped remainder with.
221
+ var o = tickData && tickData.objects && tickData.objects[loHandle];
222
+ var cur = universe.getFrameProperty(backingQid, 'mass');
223
+ var curBefore = cur;
224
+ if (o && Array.isArray(o.frames)) {
225
+ for (var i = 0; i < o.frames.length; i++) {
226
+ var f = o.frames[i];
227
+ if (!f || f.isAlive !== false || typeof f.mass !== 'number') { continue; }
228
+ var room = capacity - cur;
229
+ if (room <= 0) { break; } // full: the rest pile/pass (a sink)
230
+ var landed = Math.min(f.mass, room);
231
+ if (landed <= 0) { continue; }
232
+ cur += landed;
233
+ absorbedTotal += landed;
234
+ _bump(absorbedByLayer, f.layer, landed);
235
+ _bump(storedByLayer, f.layer, landed);
236
+ }
237
+ }
238
+ if (cur !== curBefore) { universe.setFrameProperty(backingQid, 'mass', cur); }
239
+ if (cur > referenceMass) { referenceMass = cur; }
240
+
241
+ // (2) at-cap gating: stop absorbing when full; resume once radiation drains it.
242
+ setActive(cur < capacity);
243
+
244
+ // (3) radiate with p = 1/capacity, AT a layer this wall actually soaked from, with
245
+ // the emitted leaf's mass set by EMISSIVITY as a fraction of that layer's own
246
+ // cell capacity (read from the engine — never a hand-derived 4^level). Never
247
+ // more than that layer still holds, never below HALF the absorbed peak.
248
+ if ((cur - BASE) > 0) {
249
+ var coin = (typeof universe.random === 'function') ? universe.random() : Math.random();
250
+ if (coin < (1 / capacity)) {
251
+ var L = pickEmissionLayer();
252
+ if (L !== null) {
253
+ var heldAtL = storedByLayer.get(L);
254
+ var radMass = Math.min(
255
+ Math.max(1, Math.round(emissivity * universe.levelCapacity(L))),
256
+ heldAtL
257
+ );
258
+ var floor = BASE + (referenceMass - BASE) / 2; // never below half the peak
259
+ if ((cur - radMass) >= floor) {
260
+ var half = Math.pow(2, L) / 2;
261
+ var rh = universe.makeLeafFrame(exit[0] + half + 1, exit[1] - half, L, null, 1, 0, true, 0, null);
262
+ universe.injectFrame(rh);
263
+ var rq = universe.getFrameQuantumId(rh);
264
+ universe.setFrameProperty(rq, 'mass', radMass);
265
+ universe.setFrameProperty(backingQid, 'mass', cur - radMass);
266
+ radiatedTotal += radMass;
267
+ _bump(radiatedByLayer, L, radMass);
268
+ _bump(storedByLayer, L, -radMass);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ };
274
+
275
+ tick.stats = function () {
276
+ var stored = universe.getFrameProperty(backingQid, 'mass') - BASE;
277
+ var seen = new Map();
278
+ absorbedByLayer.forEach(function (_v, k) { seen.set(k, true); });
279
+ radiatedByLayer.forEach(function (_v, k) { seen.set(k, true); });
280
+ var byLayer = _layersAscending(seen).map(function (L) {
281
+ return {
282
+ layer: L,
283
+ absorbed: absorbedByLayer.get(L) || 0,
284
+ radiated: radiatedByLayer.get(L) || 0,
285
+ stored: storedByLayer.get(L) || 0
286
+ };
287
+ });
288
+ return { stored: stored, absorbedTotal: absorbedTotal, radiatedTotal: radiatedTotal, byLayer: byLayer };
289
+ };
290
+
291
+ /** The wall's LIVE radiating efficiency (fullness fraction of the emitted leaf). */
292
+ tick.emissivity = function () { return emissivity; };
293
+ /** Live-set the radiating efficiency; takes effect at the next emission (no recompile). */
294
+ tick.setEmissivity = function (v) {
295
+ emissivity = _validEmissivity(v, 'setEmissivity');
296
+ return emissivity;
297
+ };
298
+
299
+ /** The wall MOVED (Wall.setRect): re-derive the right-face exit from the new rect, so a
300
+ * radiated leaf leaves the wall where it now stands. A caller-stated `cfg.exit` is the
301
+ * caller's own placement and is kept. */
302
+ tick.setRect = function (r) {
303
+ if (!cfg.exit) { exit = _rectRightExit(r); }
304
+ };
305
+
306
+ return tick;
307
+ }
308
+
309
+ export { DEFAULT_EMISSIVITY, deriveBackingLevel, attachAbsorberBackingFrame, buildAbsorbRadiationTickFn };
310
+