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

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 (218) hide show
  1. package/CLAUDE.md +87 -0
  2. package/LICENSE.md +664 -0
  3. package/README.md +84 -2
  4. package/dist/dist-manifest.json +218 -0
  5. package/dist/eyes/index.js +109 -0
  6. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  7. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  8. package/dist/fat/pkg-node/package.json +12 -0
  9. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  10. package/dist/frqtl/compat/boot-node.js +34 -0
  11. package/dist/frqtl/compat/engine.js +127 -0
  12. package/dist/frqtl/compat/package-root.js +24 -0
  13. package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
  14. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  15. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  16. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  17. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  18. package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
  19. package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
  20. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  21. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  22. package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
  23. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  24. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  25. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  26. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  27. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  28. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  30. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  31. package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
  32. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  33. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  34. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  35. package/dist/frqtl/compat/vendor.js +196 -0
  36. package/dist/frqtl/node-entry.js +17 -0
  37. package/dist/frqtl/objects/FrqtlApp.js +2920 -0
  38. package/dist/frqtl/objects/ObjectFactory.js +296 -0
  39. package/dist/frqtl/objects/RecipeContext.js +322 -0
  40. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  41. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  42. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  43. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  44. package/dist/frqtl/objects/instruments/capture.js +710 -0
  45. package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
  46. package/dist/frqtl/objects/kinds-entry.js +14 -0
  47. package/dist/frqtl/objects/kinds.js +190 -0
  48. package/dist/frqtl/objects/objects/Box.js +273 -0
  49. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  50. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  51. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  52. package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
  53. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  54. package/dist/frqtl/objects/objects/LightSource.js +351 -0
  55. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  56. package/dist/frqtl/objects/objects/Tube.js +471 -0
  57. package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
  58. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  59. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  60. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  61. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  62. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  66. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  67. package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
  68. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  69. package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
  70. package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
  71. package/dist/frqtl/objects/objects/tagger.js +228 -0
  72. package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
  73. package/dist/frqtl/objects/schema/index.js +256 -0
  74. package/dist/frqtl/objects/schema/scopes.js +457 -0
  75. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  76. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  77. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  78. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  79. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  80. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  81. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  82. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  83. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
  85. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  86. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  87. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  88. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  89. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  90. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  91. package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
  92. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  94. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  95. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
  96. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  97. package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
  98. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  99. package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
  100. package/dist/frqtl/objects/v1-replacements.js +29 -0
  101. package/dist/frqtl/package-core.js +23 -0
  102. package/dist/frqtl/parts/CameraControls.js +643 -0
  103. package/dist/frqtl/parts/CameraPath.js +575 -0
  104. package/dist/frqtl/parts/CollisionActions.js +34 -0
  105. package/dist/frqtl/parts/CollisionTypes.js +30 -0
  106. package/dist/frqtl/parts/DetectorWall.js +496 -0
  107. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  108. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  109. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  110. package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
  111. package/dist/frqtl/parts/LargeObject.js +405 -0
  112. package/dist/frqtl/parts/LightColor.js +255 -0
  113. package/dist/frqtl/parts/LiveChart.js +285 -0
  114. package/dist/frqtl/parts/LogicGate.js +717 -0
  115. package/dist/frqtl/parts/OvenBox.js +611 -0
  116. package/dist/frqtl/parts/PointSource.js +762 -0
  117. package/dist/frqtl/parts/Rectangle.js +200 -0
  118. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  119. package/dist/frqtl/parts/SlitScreen.js +628 -0
  120. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  121. package/dist/frqtl/parts/Trace.js +508 -0
  122. package/dist/frqtl/parts/Tunnel.js +1057 -0
  123. package/dist/frqtl/parts/Wall.js +1211 -0
  124. package/dist/frqtl/parts/index.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  126. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  127. package/dist/frqtl/scenes/atomic-model.js +88 -0
  128. package/dist/frqtl/scenes/big-bang.js +46 -0
  129. package/dist/frqtl/scenes/blackbody.js +81 -0
  130. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  131. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  132. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  133. package/dist/frqtl/scenes/counting-wall.js +38 -0
  134. package/dist/frqtl/scenes/double-slit.js +31 -0
  135. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  136. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  137. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  138. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
  139. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  140. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  141. package/dist/frqtl/scenes/empty.js +14 -0
  142. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  143. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  144. package/dist/frqtl/scenes/mirror-route.js +140 -0
  145. package/dist/frqtl/scenes/mirror.js +38 -0
  146. package/dist/frqtl/scenes/omni-3d.js +27 -0
  147. package/dist/frqtl/scenes/omni-source.js +22 -0
  148. package/dist/frqtl/scenes/self-interference.js +39 -0
  149. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  150. package/dist/frqtl/scenes/straight-walk.js +22 -0
  151. package/dist/frqtl/scenes/sun.js +39 -0
  152. package/dist/frqtl/scenes/two-source.js +37 -0
  153. package/dist/frqtl/scenes/wave.js +26 -0
  154. package/dist/shim/bundle/frqnb.js +112 -0
  155. package/dist/shim/bundle/index.js +121 -0
  156. package/dist/shim/bundle/indexHtml.js +107 -0
  157. package/dist/shim/bundle/manifest.js +95 -0
  158. package/dist/shim/bundle/zip.js +243 -0
  159. package/dist/shim/bus.js +67 -0
  160. package/dist/shim/controls/index.js +627 -0
  161. package/dist/shim/controls/registry.js +274 -0
  162. package/dist/shim/engine/artifact-shell.js +211 -0
  163. package/dist/shim/engine/boot-phases.js +44 -0
  164. package/dist/shim/engine/engine-stack-boot.js +59 -0
  165. package/dist/shim/engine/router.js +690 -0
  166. package/dist/shim/experiment-frame/index.js +296 -0
  167. package/dist/shim/expr/ast-schema.js +369 -0
  168. package/dist/shim/field-descriptor/index.js +325 -0
  169. package/dist/shim/frame-data/index.js +224 -0
  170. package/dist/shim/index.js +67 -0
  171. package/dist/shim/notebook/canvas-guard.js +82 -0
  172. package/dist/shim/notebook/catalog.js +35 -0
  173. package/dist/shim/notebook/format.js +736 -0
  174. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  175. package/dist/shim/notebook/kernel.js +671 -0
  176. package/dist/shim/notebook/knob.js +150 -0
  177. package/dist/shim/notebook/module-resolver.js +260 -0
  178. package/dist/shim/notebook/notebook-knobs.js +65 -0
  179. package/dist/shim/notebook/produced-bindings.js +67 -0
  180. package/dist/shim/notebook/realm-registry.js +121 -0
  181. package/dist/shim/notebook/realm-transport.js +73 -0
  182. package/dist/shim/notebook/setup-builder.js +507 -0
  183. package/dist/shim/notebook/store.js +159 -0
  184. package/dist/shim/notebook/vendor-importmap.js +134 -0
  185. package/dist/shim/notebook/view-builder.js +189 -0
  186. package/dist/shim/notebook/view-ctx.js +100 -0
  187. package/dist/shim/notebook/view-handle.js +202 -0
  188. package/dist/shim/notebook/warmup.js +102 -0
  189. package/dist/shim/package.json +4 -0
  190. package/dist/shim/page-params/index.js +213 -0
  191. package/dist/shim/project/identity.js +112 -0
  192. package/dist/shim/project/index.js +19 -0
  193. package/dist/shim/project/link.js +402 -0
  194. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  195. package/dist/shim/project/register-node.mjs +17 -0
  196. package/dist/shim/project/specifiers.js +106 -0
  197. package/dist/shim/registry/copy-gate.js +110 -0
  198. package/dist/shim/registry/exhibits.js +455 -0
  199. package/dist/shim/registry/experiments.js +215 -0
  200. package/dist/shim/registry/index.js +19 -0
  201. package/dist/shim/registry/scene-source.js +157 -0
  202. package/dist/shim/registry/walkthrough.js +804 -0
  203. package/dist/shim/render/capture-envelope.js +279 -0
  204. package/dist/shim/render/heat-colormap.js +92 -0
  205. package/dist/shim/render/render-stages.js +855 -0
  206. package/dist/shim/render/sun-look.js +33 -0
  207. package/dist/shim/render-state/apply.js +262 -0
  208. package/dist/shim/render-state/index.js +74 -0
  209. package/dist/shim/rpc/index.js +207 -0
  210. package/dist/shim/runtime/index.js +1023 -0
  211. package/dist/shim/storage/boot.js +69 -0
  212. package/dist/shim/storage/index.js +169 -0
  213. package/dist/shim/storage/local-storage-provider.js +230 -0
  214. package/dist/shim/storage/make-namespace-store.js +132 -0
  215. package/dist/shim/storage/modules.js +249 -0
  216. package/dist/shim/storage/projects.js +150 -0
  217. package/llms.txt +12 -0
  218. package/package.json +63 -4
@@ -0,0 +1,298 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * sustainedSource — the ONE hand-placed-quantum source surface (Tunnel Lab and
5
+ * LightSource Lab share ONE source surface and differ only in declared
6
+ * composition). REPLACES two independently
7
+ * hand-rolled injector tick-fns (`setupTunnelLab.js`'s and
8
+ * `setupLightSourceLab.js`'s own, admitted by the latter's own comment to
9
+ * "mirror setupTunnelLab.js byte-for-byte") with ONE authority that both labs,
10
+ * and `LightSource` itself (via its opt-in `options.sustain`), consume.
11
+ *
12
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
13
+ *
14
+ * `SOURCE_SPEC_FIELDS` is the declared contract for what a source stamps on
15
+ * every hand-placed quantum, plus its burst schedule — the schema-mirror
16
+ * discipline (root CLAUDE.md "Engineering Culture"): the owner (this file)
17
+ * exports the field list + defaults; every consumer calls `normalizeSourceSpec`
18
+ * to get a FULL bag (never hand-re-enumerates a subset), so a field omission is
19
+ * structurally impossible to silently diverge on (the exact bug this
20
+ * fixes: one injector stamped `wavelength`, the other didn't, and nothing
21
+ * forced them to agree). `sustained-source.test.mjs` is driven off this exact
22
+ * exported list — adding a field here without a matching test entry is a CI
23
+ * failure, not a silent gap.
24
+ *
25
+ * `placeSourceQuanta(universe, innerRect, capNegX, hasRealDepth, spec, count)`
26
+ * is the single low-level placement primitive — computes the entrance pixel
27
+ * from `innerRect`+`capNegX`+`spec.entranceBias` (the SAME formula
28
+ * `LightSource.js`'s own one-shot burst and both labs' old hand-rolled
29
+ * injectors independently derived), draws a velocity per `spec.velocityProfile`,
30
+ * and calls `makeLeafFrame`/`injectFrame`/`setFrameProperty` — the SAME
31
+ * generic primitives every hand-placed-quantum experiment already uses (G-EMIT-2,
32
+ * the engine boundary model). Used for BOTH a one-shot burst (LightSource's
33
+ * own construction-time placement) and a recurring burst (`buildSustainedSource`'s
34
+ * tick-fn) — "the one injector then stamps quantum properties ... in exactly ONE
35
+ * place" (the worked-example blueprint, Section 4 item 1).
36
+ *
37
+ * `buildSustainedSource(universe, innerRect, capNegX, hasRealDepth, specPartial)`
38
+ * builds+registers a recurring Phase-A host tick-fn (mirrors `emitterProgram.js`'s
39
+ * own registration point — a pre-tick primitive with no Phase-D timing
40
+ * constraint, unlike `directedRedirectProgram.js`'s option1 redirect) that calls
41
+ * `placeSourceQuanta` every `sourceState.rate` ticks. Returns `{sourceState,
42
+ * dispose}` — `sourceState` is the LIVE, mutable normalized spec (a UI slider
43
+ * writes `sourceState.wavelength = v` etc.; the tick-fn reads it fresh every
44
+ * call — no re-registration, no reconstruction, matching the established
45
+ * `setupTunnelLab.js`/`setupLightSourceLab.js` LIVE-knob convention exactly).
46
+ * `dispose()` tags-then-splices the tick-fn out of `experimentTickFunctions` by
47
+ * identity (`injectorTickFn._sourceInjector`) — the SAME idiom
48
+ * `QuantumEmitter.prototype.dispose` and every prior lab-level injector already
49
+ * used; see `FrqtlApp.prototype.setup()`'s own dispose-before-reinit
50
+ * convention (`reference_frqtlapp_setup_canonical_teardown.md`).
51
+ *
52
+ * @typedef {Object} SourceSpec
53
+ * @property {number|null} wavelength - Per-frame wavelength override (Engine
54
+ * Invariant #6 `mass = max(1, capacity/wavelength)`). `null` (default) =
55
+ * engine default (`mass = capacity`, i.e. fullness=1, top of the ROYGBIV
56
+ * ramp). THIS is the field whose divergence (one injector set it, the other
57
+ * didn't) produced "different particle colors".
58
+ * @property {number|null} charge - Per-frame charge override (0=Positive,
59
+ * 1=Negative). `null` (default) = random draw per quantum.
60
+ * @property {'beam'|'spray'} velocityProfile - `'beam'` (default) = collimated
61
+ * single direction, `orientationRad`+`speed` (LightSource's own convention —
62
+ * `vy=vz=0` always, a deliberately directed/phase-locked stream). `'spray'` =
63
+ * a biased random fan (`spraySpec`) — `setupTunnelLab.js`'s own original
64
+ * uniformization-demo shape. Named, selectable, NEVER divergent hand-code
65
+ * (Section 3 finding E).
66
+ * @property {number} speed - `'beam'` profile only: speed magnitude.
67
+ * @property {number} orientationRad - `'beam'` profile only: direction,
68
+ * radians (`0` = +X).
69
+ * @property {{vxMin:number,vxRange:number,vyMin:number,vyRange:number}} spraySpec
70
+ * - `'spray'` profile only: `vx = vxMin + random()*vxRange`,
71
+ * `vy = vyMin + random()*vyRange` — `setupTunnelLab.js`'s ORIGINAL literal
72
+ * values (0.2/0.6/0.1/0.7), now named+declared instead of hardcoded inline.
73
+ * @property {'center'|'top'} entranceBias - `'center'` (default) = bore
74
+ * Y-centreline (`LightSource.js`'s own convention). `'top'` = bunched 2px
75
+ * from the TOP wall (`setupTunnelLab.js`'s original biased-entry shape — "a
76
+ * symmetric/spread entrance is already balanced and shows no washout effect
77
+ * at all").
78
+ * @property {boolean} zIsotropy - When real depth is armed AND
79
+ * `velocityProfile==='spray'`: draw `vz = (random()*2-1)*spraySpec.vyRange`
80
+ * (the SAME half-range as the vy spread) — the 3D-SYMMETRIC EMISSION fix
81
+ * (§4 option (a): vz is drawn symmetric to the vy spread).
82
+ * `'beam'` profile is UNAFFECTED regardless of this flag (`vy=vz=0` stays —
83
+ * collimation is deliberate; §4 option (b): the beam's isotropic
84
+ * TRANSVERSE WAVE already does the 3D work, measured y≈z). Default `true`.
85
+ * @property {number|null} level - `makeLeafFrame`'s `level` override (rarely
86
+ * used; `LightSource.js`'s own `options.particle.level`). `null` (default).
87
+ * @property {number|undefined} mass - post-placement `mass` override via
88
+ * `setFrameProperty` (`LightSource.js`'s own `options.particle.mass`).
89
+ * `undefined` (default) = no override.
90
+ * @property {number} density - Quanta hand-placed per burst.
91
+ * @property {number} rate - Ticks between bursts (an `everyNTicks` cadence).
92
+ * @property {boolean} active - Master on/off for the recurring source.
93
+ *
94
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
95
+ *
96
+ * Zero new physics (canonical-bench SU1): every write here is the SAME
97
+ * `makeLeafFrame`/`injectFrame`/`setFrameProperty`/`universe.random()` primitive
98
+ * chain every hand-placed-quantum experiment already uses — this module only
99
+ * CONSOLIDATES two independently-hand-rolled call sites into one, and adds a
100
+ * genuinely-new DECLARED knob (`zIsotropy`) that draws an EXTRA `vz` momentum
101
+ * component using the identical `(random()*2-1)*range` draw shape the `vy`
102
+ * spread already used — no new mechanism, just the same mechanism applied to a
103
+ * third axis. `velocityProfile:'spray'` + `zIsotropy:true` (tunnel-lab's new
104
+ * default) is expected to raise the settled z/y dispersion-spread ratio from
105
+ * the tester-measured 0.23 (pinned z, zero vz ever drawn) toward something
106
+ * close to the y-spread itself (the injected fan becomes 3D-symmetric; the
107
+ * existing free-walk + NEAR/FAR wall bounces already handle z correctly once
108
+ * they receive real z-momentum to work with — the measured `ax_only` branch
109
+ * gives y≈z isotropic transverse spread).
110
+ */
111
+ 'use strict';
112
+
113
+ // SOURCE_SPEC_FIELDS — the ONE declared contract (schema-mirror discipline,
114
+ // root CLAUDE.md). `default` is either a primitive or an object (deep-copied
115
+ // per-normalize call so no two sourceStates ever share a mutable spraySpec).
116
+ var SOURCE_SPEC_FIELDS = {
117
+ wavelength: { default: null, desc: 'Per-frame wavelength override (Engine Invariant #6 mass=max(1,capacity/wavelength)). null = engine default (mass=capacity).' },
118
+ charge: { default: null, desc: 'Per-frame charge override (0=Positive,1=Negative). null = random draw per quantum.' },
119
+ velocityProfile: { default: 'beam', enum: ['beam', 'spray'], desc: "'beam' = collimated single-direction (orientationRad+speed, vy=vz=0); 'spray' = biased random fan (spraySpec)." },
120
+ speed: { default: 1, desc: "'beam' profile only: speed magnitude." },
121
+ orientationRad: { default: 0, desc: "'beam' profile only: direction, radians (0 = +X)." },
122
+ spraySpec: { default: { vxMin: 0.2, vxRange: 0.6, vyMin: 0.1, vyRange: 0.7 }, desc: "'spray' profile only: vx=vxMin+random()*vxRange, vy=vyMin+random()*vyRange (setupTunnelLab.js's original literal fan)." },
123
+ entranceBias: { default: 'center', enum: ['center', 'top'], desc: "'center' = bore Y-centreline (LightSource.js's convention); 'top' = bunched 2px from the TOP wall (setupTunnelLab.js's original biased-entry/uniformization-demo shape)." },
124
+ zIsotropy: { default: true, desc: "When real depth is armed AND velocityProfile='spray': draw vz=(random()*2-1)*spraySpec.vyRange (SAME half-range as vy) for a 3D-symmetric fan. 'beam' stays vy=vz=0 regardless (deliberate collimation)." },
125
+ level: { default: null, desc: "makeLeafFrame's level override (rarely used)." },
126
+ mass: { default: undefined, desc: 'Post-placement mass override via setFrameProperty. undefined = no override.' },
127
+ density: { default: 5, desc: 'Quanta hand-placed per burst.' },
128
+ rate: { default: 10, desc: 'Ticks between bursts (everyNTicks cadence).' },
129
+ active: { default: true, desc: 'Master on/off for the recurring source.' },
130
+ };
131
+
132
+ var SOURCE_SPEC_FIELD_NAMES = Object.keys(SOURCE_SPEC_FIELDS);
133
+
134
+ /**
135
+ * Normalize a partial spec into a FULL bag (every declared field present,
136
+ * defaults filled in) — the schema-mirror discipline's "consumers ferry the
137
+ * normalized bag" half. Never hand-re-enumerate a subset of
138
+ * SOURCE_SPEC_FIELDS elsewhere; always go through this function.
139
+ * @param {Object} [partial]
140
+ * @returns {SourceSpec}
141
+ */
142
+ function normalizeSourceSpec(partial) {
143
+ partial = partial || {};
144
+ var out = {};
145
+ for (var i = 0; i < SOURCE_SPEC_FIELD_NAMES.length; i++) {
146
+ var key = SOURCE_SPEC_FIELD_NAMES[i];
147
+ if (partial[key] !== undefined) {
148
+ out[key] = (key === 'spraySpec')
149
+ ? Object.assign({}, SOURCE_SPEC_FIELDS.spraySpec.default, partial[key]) // partial spraySpec override
150
+ : partial[key];
151
+ } else {
152
+ var def = SOURCE_SPEC_FIELDS[key].default;
153
+ out[key] = (def !== null && typeof def === 'object') ? Object.assign({}, def) : def; // fresh copy, never shared
154
+ }
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /**
160
+ * Compute the fixed entrance pixel from a tunnel/tube's innerRect — the ONE
161
+ * formula `LightSource.js`'s own one-shot burst and both labs' original
162
+ * hand-rolled injectors independently derived (now declared once).
163
+ * @param {Object} innerRect - {left, top, z, w, h, d}
164
+ * @param {boolean} capNegX - true = closed breech (Gun-Chamber convention).
165
+ * @param {'center'|'top'} entranceBias
166
+ * @returns {{x:number, y:number}}
167
+ */
168
+ function _computeEntrance(innerRect, capNegX, entranceBias) {
169
+ var x = capNegX ? (innerRect.left + 2) : (innerRect.left + innerRect.w / 2);
170
+ var y = (entranceBias === 'top') ? (innerRect.top + 2) : (innerRect.top + innerRect.h / 2);
171
+ return { x: x, y: y };
172
+ }
173
+
174
+ /**
175
+ * The single low-level placement primitive — hand-places `count` quanta at
176
+ * ONE shared entrance pixel (the I1 scale-sweep's own validated ensemble
177
+ * technique: injection-time overlap does not merge same-tick frames; each
178
+ * leaf's own move-gate/wavelength-oscillator RNG draws diverge it from its
179
+ * siblings over subsequent ticks). Used for BOTH a one-shot burst
180
+ * (`LightSource.js`'s construction) and a recurring burst
181
+ * (`buildSustainedSource`'s tick-fn) — ONE place quantum properties get
182
+ * stamped.
183
+ * @param {Object} universe
184
+ * @param {Object} innerRect
185
+ * @param {boolean} capNegX
186
+ * @param {boolean} hasRealDepth - gate on the CALLER's explicit request for
187
+ * z-containment (N1 fix precedent, `LightSource.js`) — NEVER derive this
188
+ * from `innerRect.d`, which is always a positive placeholder even when
189
+ * the tunnel is flat 2D-in-3D.
190
+ * @param {SourceSpec} spec - a NORMALIZED spec (pass through
191
+ * `normalizeSourceSpec` first — this function does not fill defaults).
192
+ * @param {number} count
193
+ * @returns {Array<{handle:*, qid:*}>}
194
+ */
195
+ function placeSourceQuanta(universe, innerRect, capNegX, hasRealDepth, spec, count) {
196
+ var entrance = _computeEntrance(innerRect, capNegX, spec.entranceBias);
197
+ var entranceZ = hasRealDepth ? (innerRect.z + innerRect.d / 2) : null;
198
+ var placed = [];
199
+ for (var i = 0; i < count; i++) {
200
+ var vx, vy, vz;
201
+ if (spec.velocityProfile === 'spray') {
202
+ var s = spec.spraySpec;
203
+ vx = s.vxMin + universe.random() * s.vxRange;
204
+ vy = s.vyMin + universe.random() * s.vyRange;
205
+ // 3D-SYMMETRIC EMISSION (§4 option (a)): draw
206
+ // vz symmetric to the vy spread, SAME half-range, only when real
207
+ // depth is armed AND the caller opted into zIsotropy (default
208
+ // true) — makes the injected fan 3D-isotropic instead of pinning
209
+ // z-momentum at zero forever (the measured z/y=0.18-0.23 flatness
210
+ // root cause — Section 2e, Section C).
211
+ vz = (hasRealDepth && spec.zIsotropy) ? (universe.random() * 2 - 1) * s.vyRange : 0;
212
+ } else { // 'beam' — collimated; vy=vz=0 stays (deliberate, §4 option (b))
213
+ vx = Math.cos(spec.orientationRad) * spec.speed;
214
+ vy = Math.sin(spec.orientationRad) * spec.speed;
215
+ vz = 0;
216
+ }
217
+ var handle = universe.makeLeafFrame(entrance.x, entrance.y, spec.level, spec.wavelength, vx, vy, false, spec.charge, null);
218
+ universe.injectFrame(handle);
219
+ var qid = universe.getFrameQuantumId(handle);
220
+ if (spec.mass !== undefined && spec.mass !== null) {
221
+ universe.setFrameProperty(qid, 'mass', spec.mass);
222
+ }
223
+ if (entranceZ !== null) {
224
+ universe.setFrameProperty(qid, 'z', entranceZ);
225
+ if (vz !== 0) { universe.setFrameProperty(qid, 'vz', vz); }
226
+ }
227
+ placed.push({ handle: handle, qid: qid });
228
+ }
229
+ return placed;
230
+ }
231
+
232
+ /**
233
+ * Build + register the ONE recurring hand-placed-quantum source tick-fn
234
+ * (kills the duplicated tunnel-lab / lightsource-lab injectors). LIVE knobs:
235
+ * every field of the returned `sourceState` is read FRESH on every burst —
236
+ * a UI slider mutating `sourceState.wavelength`/`density`/`rate`/`active`
237
+ * etc. takes effect on the NEXT burst, no re-registration.
238
+ * @param {Object} universe
239
+ * @param {Object} innerRect
240
+ * @param {boolean} capNegX
241
+ * @param {boolean} hasRealDepth
242
+ * @param {Object} [specPartial] - overrides onto SOURCE_SPEC_FIELDS' defaults.
243
+ * @returns {{ sourceState: SourceSpec, dispose: Function }}
244
+ */
245
+ function buildSustainedSource(universe, innerRect, capNegX, hasRealDepth, specPartial) {
246
+ var sourceState = normalizeSourceSpec(specPartial);
247
+
248
+ var tickCounter = 0;
249
+ var injectorTickFn = function () {
250
+ if (!sourceState.active) { return; }
251
+ tickCounter++;
252
+ if (tickCounter % Math.max(1, sourceState.rate) !== 0) { return; }
253
+ placeSourceQuanta(universe, innerRect, capNegX, hasRealDepth, sourceState, sourceState.density);
254
+ };
255
+ // Tag the tick-fn with a back-reference (mirrors QuantumEmitter.dispose's
256
+ // OWN `fns[j]._emState` idiom EXACTLY, and both prior labs' own
257
+ // `_sourceInjector` tag) so dispose() below can find + splice out THIS
258
+ // EXACT closure, never a different injector that happens to also be
259
+ // registered.
260
+ injectorTickFn._sourceInjector = sourceState;
261
+ universe.experimentTickFunctions = universe.experimentTickFunctions || [];
262
+ universe.experimentTickFunctions.push(injectorTickFn);
263
+
264
+ return {
265
+ sourceState: sourceState,
266
+ // CANONICAL TEARDOWN (see reference_frqtlapp_setup_canonical_teardown.md):
267
+ // tombstone (defense in depth) then physically splice the tick-fn out
268
+ // of experimentTickFunctions by identity. Idempotent — a second call
269
+ // is a safe no-op.
270
+ dispose: function () {
271
+ sourceState.active = false;
272
+ var fns = universe.experimentTickFunctions;
273
+ if (!Array.isArray(fns)) { return; }
274
+ for (var j = fns.length - 1; j >= 0; j--) {
275
+ if (fns[j] === injectorTickFn) { fns.splice(j, 1); }
276
+ }
277
+ },
278
+ };
279
+ }
280
+
281
+ /**
282
+ * capacityAtDimension(dim) — the ONE authority for "capacity of a leaf at
283
+ * fundamental dimension `dim`" (Engine Invariant #6: `width=2^dim,
284
+ * capacity=width²`). Both tunnel-lab and lightsource-lab derive their
285
+ * `MAX_USEFUL_WAVELENGTH` slider ceiling from this (the ceiling
286
+ * fix — same MAX_USEFUL_WAVELENGTH clamp authority) —
287
+ * never a hand-typed "16" in either factory.
288
+ * @param {number} dim
289
+ * @returns {number}
290
+ */
291
+ function capacityAtDimension(dim) {
292
+ var width = Math.pow(2, dim);
293
+ return width * width;
294
+ }
295
+
296
+
297
+ export { SOURCE_SPEC_FIELDS, SOURCE_SPEC_FIELD_NAMES, normalizeSourceSpec, placeSourceQuanta, buildSustainedSource, capacityAtDimension };
298
+
@@ -0,0 +1,228 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * Tagger — the MARK part: put a user string on a frame, consistently.
5
+ *
6
+ * ── A Tagger IS … (the datasheet line; the minimal sim §3) ───────────────────────
7
+ *
8
+ * A Tagger is the op-program op `{ set: '0.data.<key>', to: StrLit(<value>) }` — ONE
9
+ * write of a user string into a frame's engine-opaque `string_map` — offered on the
10
+ * two lanes a frame can be reached from, plus the matching read:
11
+ *
12
+ * IN-TICK (`tagOps`) — the op appended to a collision op-program; the engine runs
13
+ * it on every colliding frame (canonical bench LO7 MARK).
14
+ * HOST (`stamp`) — the SAME write between ticks, as a get→merge→set through
15
+ * `universe.getFrameProperty(qid,'data')` /
16
+ * `setFrameProperty(qid,'data',bag)`, for a frame that
17
+ * nothing collided with (no LO, no wall — see §Reuse).
18
+ * READ (`readTags`)— the tags back off a collected `FrameData`, without the
19
+ * caller ever naming `fd.data.string_map`.
20
+ *
21
+ * That is the whole part: three lanes, ONE key/value vocabulary, ZERO physics.
22
+ *
23
+ * ── Layer 1: API contract ───────────────────────────────────────────────────
24
+ *
25
+ * Tagger.tagOps(tags) → Array<op> (append to an op-program's ops)
26
+ * Tagger.stamp(universe, qid, tags) → number (keys written; host lane)
27
+ * Tagger.readTags(frameData) → {k:v} ({} when untagged)
28
+ * Tagger.readTag(frameData[, key]) → string|null (key defaults to MARK_KEY)
29
+ * Tagger.normalizeTags(tags) → {k:v} (the shared coercion; exported
30
+ * so a consumer never re-derives it)
31
+ * Tagger.MARK_KEY = 'detector_name' (the canonical provenance key)
32
+ *
33
+ * `tags` is either a plain object `{ key: value, … }` or a bare string (shorthand for
34
+ * `{ [MARK_KEY]: value }` — the "just mark it with this name" case that MARK/DETECT and
35
+ * the wall `mark` hook have always meant). Values are coerced with `String()`; `''` IS a
36
+ * valid tag value (the wall `mark` hook with no `markTag` stamps the empty string and has
37
+ * always done so — dropping it here would move a compiled program). `null`/`undefined`
38
+ * values are skipped. A non-string / empty key throws (FAIL-LOUD, never a silent no-op).
39
+ *
40
+ * Key order is the object's own insertion order, so `tagOps` is deterministic and a
41
+ * caller's program bytes are stable.
42
+ *
43
+ * ── The knobs ───────────────────────────────────────────────────────────────
44
+ *
45
+ * key — any string. It is carried VERBATIM by the engine, which interprets
46
+ * nothing (`lib/shim/frame-data` §string_map). A dotted key survives whole
47
+ * (`render.color` is a tag like any other — the wall `recolor` hook is a
48
+ * Tagger whose key happens to be one the renderer reads).
49
+ * value — any string. This is USER data; there is no enum, no registry, no engine
50
+ * meaning. Two different LOs stamping the same key is a last-write-wins
51
+ * overwrite inside one tick, in op order.
52
+ *
53
+ * ── Layer 2: physics behavior — measured constants ──────────────────────────
54
+ *
55
+ * ZERO physics (SU1). A tag write costs **0 RNG draws** (`StrLit` is a compile-time
56
+ * constant; `actionCompiler`'s own RNG-cost ledger already lists MARK at 0), touches no
57
+ * position, velocity, mass, capacity, wavelength or `is_alive`, and the engine
58
+ * interprets no `string_map` key. Measured baseline: the canonical bench holds
59
+ * **28/28** across this part landing, with **LO7 MARK + COUNT** the golden that covers
60
+ * it directly (`count=105, mark="probeA"` — the stamped string rides the collected
61
+ * FrameData and is read between ticks). A Tagger cannot move a golden; if one moves,
62
+ * physics leaked into the tag and that is a defect in this file.
63
+ *
64
+ * ── Reuse: a Tagger is NOT a wall part ──────────────────────────────────────
65
+ *
66
+ * The in-tick lane needs a collision, so it rides whatever object already hooks
67
+ * collisions (today: a `Wall`/`LargeObject` op-program — `Wall{hooks:['mark']}` IS a
68
+ * Tagger). The HOST lane needs nothing at all: a bare `experimentTickFunctions` entry
69
+ * that notices something about the world and stamps the frames it noticed is a complete
70
+ * detector with no wall, no rect and no collision — e.g. a thermostat-style probe that
71
+ * marks every frame it counts while the world is above its setpoint. See
72
+ * `lib/frqtl/objects/test/tagger.test.mjs` §"non-wall reuse".
73
+ *
74
+ * ── What this file does NOT own ─────────────────────────────────────────────
75
+ *
76
+ * The BUBBLE SCHEMA (`SECTION_KEYS`, `normalizeDataBubble`) is owned by
77
+ * `lib/shim/frame-data`; this file FERRIES it and never re-lists a section. The AST node
78
+ * kinds are owned by `lib/shim/expr/ast-schema.js`; this file consumes `StrLit` +
79
+ * `HANDLE_FRAME` and never re-derives an op shape. Which render keys are legal is owned
80
+ * by `RENDER_INTENT_KEYS`; a Tagger carries any key, exactly as the engine does.
81
+ */
82
+
83
+ import * as AstSchema from '../../../shim/expr/ast-schema.js';
84
+ import * as FrameDataSchema from '../../../shim/frame-data/index.js';
85
+
86
+
87
+
88
+ /**
89
+ * MARK_KEY — the canonical provenance key. This is the key MARK/DETECT and the wall
90
+ * `mark` hook have always written, and the key `DetectorWall` reads for slit
91
+ * provenance. Named ONCE here so no consumer spells the string again.
92
+ */
93
+ var MARK_KEY = 'detector_name';
94
+
95
+ /**
96
+ * normalizeTags(tags) — the ONE coercion both write lanes share, so the in-tick op
97
+ * and the host stamp can never disagree about what "tagging with X" means.
98
+ *
99
+ * @param {Object|string} tags — `{key: value, …}` or a bare string (→ {MARK_KEY: str}).
100
+ * @returns {Object} a fresh `{key: string}` bag (insertion order preserved).
101
+ * @throws {TypeError} on a non-object/non-string input, or a non-string/empty key.
102
+ */
103
+ function normalizeTags(tags) {
104
+ if (typeof tags === 'string') {
105
+ var one = {};
106
+ one[MARK_KEY] = tags;
107
+ return one;
108
+ }
109
+ if (tags === null || typeof tags !== 'object' || Array.isArray(tags)) {
110
+ throw new TypeError('[Tagger] tags must be a plain object {key: value} or a bare ' +
111
+ 'string (shorthand for {' + MARK_KEY + ': value}); got ' +
112
+ (Array.isArray(tags) ? 'an array' : typeof tags) + '.');
113
+ }
114
+ var out = {};
115
+ var keys = Object.keys(tags);
116
+ for (var i = 0; i < keys.length; i++) {
117
+ var k = keys[i];
118
+ if (typeof k !== 'string' || k.length === 0) {
119
+ throw new TypeError('[Tagger] tag key must be a non-empty string; got ' +
120
+ JSON.stringify(k) + '.');
121
+ }
122
+ var v = tags[k];
123
+ if (v === null || v === undefined) { continue; } // skipped, never stamped as "null"
124
+ out[k] = String(v);
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * tagOps(tags) — the IN-TICK lane. Build the op-program ops that stamp `tags` onto the
131
+ * COLLIDING frame (`HANDLE_FRAME`), for a caller to concat into its own `ops` array.
132
+ *
133
+ * This is the single authority for the tag-op SHAPE: `actionCompiler`'s MARK/DETECT
134
+ * action, its v2 `mark` hook and its v2 `recolor` hook are all this function (the op
135
+ * bytes are unchanged — the compiler ferries them, it does not re-spell them).
136
+ *
137
+ * @param {Object|string} tags
138
+ * @returns {Array<{set:string, to:Object}>} — one op per key, in key order.
139
+ */
140
+ function tagOps(tags) {
141
+ var ast = AstSchema;
142
+ var bag = normalizeTags(tags);
143
+ var ops = [];
144
+ var keys = Object.keys(bag);
145
+ for (var i = 0; i < keys.length; i++) {
146
+ // `0.data.<key>` — the op-apply layer intercepts StrLit and routes it to the
147
+ // frame's string_map write path (the dotted part of a key like 'render.color'
148
+ // survives whole: strKey = prop.slice(5)).
149
+ ops.push({ set: ast.HANDLE_FRAME + '.data.' + keys[i], to: ast.StrLit(bag[keys[i]]) });
150
+ }
151
+ return ops;
152
+ }
153
+
154
+ /**
155
+ * stamp(universe, quantumId, tags) — the HOST lane. Tag a frame BETWEEN ticks, with
156
+ * no collision, no LargeObject and no op-program: read the frame's current data bag,
157
+ * merge the tags into its `string_map`, write the whole bag back.
158
+ *
159
+ * get→merge→set is deliberate (the render-intent stamp rule): the write
160
+ * must PRESERVE the frame's `detection` section and any pre-existing string_map keys,
161
+ * rather than relying on the engine's `set('data')` happening to merge.
162
+ *
163
+ * @param {Object} universe — an EGPTUniverse (get/setFrameProperty accessors).
164
+ * @param {number} quantumId — the frame's QUANTUM ID (`FrameData.id` — NOT the
165
+ * `FrameId` handle `makeLeafFrame` returns; bridge with `getFrameQuantumId`).
166
+ * @param {Object|string} tags
167
+ * @returns {number} the number of keys written.
168
+ * @throws {TypeError} on a universe without the accessors, or a bad tag bag.
169
+ */
170
+ function stamp(universe, quantumId, tags) {
171
+ if (!universe || typeof universe.setFrameProperty !== 'function') {
172
+ throw new TypeError('[Tagger.stamp] needs a universe with setFrameProperty ' +
173
+ '(the host write lane). Got ' + (universe ? typeof universe : universe) + '.');
174
+ }
175
+ var bag = normalizeTags(tags);
176
+ var keys = Object.keys(bag);
177
+ if (keys.length === 0) { return 0; }
178
+
179
+ var current = null;
180
+ if (typeof universe.getFrameProperty === 'function') {
181
+ current = universe.getFrameProperty(quantumId, 'data');
182
+ }
183
+ if (!current || typeof current !== 'object') { current = {}; }
184
+ var sm = (current.string_map && typeof current.string_map === 'object')
185
+ ? current.string_map : {};
186
+ for (var i = 0; i < keys.length; i++) { sm[keys[i]] = bag[keys[i]]; }
187
+ current.string_map = sm;
188
+ universe.setFrameProperty(quantumId, 'data', current);
189
+ return keys.length;
190
+ }
191
+
192
+ /**
193
+ * readTags(frameData) — the READ lane. Pull every tag off a collected `FrameData`
194
+ * (`tickData.objects[handle].frames[i]`, or `tickData.frameData[i]`) without the
195
+ * caller naming `fd.data.string_map`.
196
+ *
197
+ * The bubble shape comes from its owner (`lib/shim/frame-data` `normalizeDataBubble`),
198
+ * so a section change lands here for free and this file never re-lists SECTION_KEYS.
199
+ *
200
+ * @param {Object} frameData — a FrameData (or anything carrying `.data`).
201
+ * @returns {Object} `{key: string}` — `{}` when the frame carries no tags.
202
+ */
203
+ function readTags(frameData) {
204
+ if (!frameData) { return {}; }
205
+ var bubble = FrameDataSchema.normalizeDataBubble(frameData.data);
206
+ var sm = bubble.string_map;
207
+ if (!sm || typeof sm !== 'object') { return {}; }
208
+ var out = {};
209
+ var keys = Object.keys(sm);
210
+ for (var i = 0; i < keys.length; i++) {
211
+ if (sm[keys[i]] !== null && sm[keys[i]] !== undefined) { out[keys[i]] = sm[keys[i]]; }
212
+ }
213
+ return out;
214
+ }
215
+
216
+ /**
217
+ * readTag(frameData, key) — one tag off a collected FrameData.
218
+ * @param {Object} frameData
219
+ * @param {string} [key=MARK_KEY] — defaults to the canonical provenance key.
220
+ * @returns {string|null} the tag value, or null when the frame is untagged.
221
+ */
222
+ function readTag(frameData, key) {
223
+ var k = (key === undefined || key === null) ? MARK_KEY : key;
224
+ var tags = readTags(frameData);
225
+ return (tags[k] !== undefined) ? tags[k] : null;
226
+ }
227
+
228
+ export { MARK_KEY, normalizeTags, tagOps, stamp, readTags, readTag };