@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,299 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * sustainedSource — the ONE hand-placed-quantum source surface (ws-dse-3d, CEO
5
+ * ruling 2026-07-14: "Tunnel Lab and LightSource Lab share ONE source surface
6
+ * and differ only in declared 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 bite
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
+ * `docs/internal/engine/ENGINE_BOUNDARY_MODEL.md`). 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 architect's own 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 the CEO's "different particle colors" report — see
58
+ * `docs/physics/COLOR_RENDER_LEARNINGS.md` and the ws-dse-3d investigation.
59
+ * @property {number|null} charge - Per-frame charge override (0=Positive,
60
+ * 1=Negative). `null` (default) = random draw per quantum.
61
+ * @property {'beam'|'spray'} velocityProfile - `'beam'` (default) = collimated
62
+ * single direction, `orientationRad`+`speed` (LightSource's own convention —
63
+ * `vy=vz=0` always, a deliberately directed/phase-locked stream). `'spray'` =
64
+ * a biased random fan (`spraySpec`) — `setupTunnelLab.js`'s own original
65
+ * uniformization-demo shape. Named, selectable, NEVER divergent hand-code
66
+ * (architect Section 3 finding E).
67
+ * @property {number} speed - `'beam'` profile only: speed magnitude.
68
+ * @property {number} orientationRad - `'beam'` profile only: direction,
69
+ * radians (`0` = +X).
70
+ * @property {{vxMin:number,vxRange:number,vyMin:number,vyRange:number}} spraySpec
71
+ * - `'spray'` profile only: `vx = vxMin + random()*vxRange`,
72
+ * `vy = vyMin + random()*vyRange` — `setupTunnelLab.js`'s ORIGINAL literal
73
+ * values (0.2/0.6/0.1/0.7), now named+declared instead of hardcoded inline.
74
+ * @property {'center'|'top'} entranceBias - `'center'` (default) = bore
75
+ * Y-centreline (`LightSource.js`'s own convention). `'top'` = bunched 2px
76
+ * from the TOP wall (`setupTunnelLab.js`'s original biased-entry shape — "a
77
+ * symmetric/spread entrance is already balanced and shows no washout effect
78
+ * at all").
79
+ * @property {boolean} zIsotropy - When real depth is armed AND
80
+ * `velocityProfile==='spray'`: draw `vz = (random()*2-1)*spraySpec.vyRange`
81
+ * (the SAME half-range as the vy spread) — the 3D-SYMMETRIC EMISSION fix
82
+ * (ws-dse-3d, theorist §4 option (a): "draw vz symmetric to the vy spread").
83
+ * `'beam'` profile is UNAFFECTED regardless of this flag (`vy=vz=0` stays —
84
+ * collimation is deliberate; theorist §4 option (b): the beam's isotropic
85
+ * TRANSVERSE WAVE already does the 3D work, measured y≈z). Default `true`.
86
+ * @property {number|null} level - `makeLeafFrame`'s `level` override (rarely
87
+ * used; `LightSource.js`'s own `options.particle.level`). `null` (default).
88
+ * @property {number|undefined} mass - post-placement `mass` override via
89
+ * `setFrameProperty` (`LightSource.js`'s own `options.particle.mass`).
90
+ * `undefined` (default) = no override.
91
+ * @property {number} density - Quanta hand-placed per burst.
92
+ * @property {number} rate - Ticks between bursts (an `everyNTicks` cadence).
93
+ * @property {boolean} active - Master on/off for the recurring source.
94
+ *
95
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
96
+ *
97
+ * Zero new physics (canonical-bench SU1): every write here is the SAME
98
+ * `makeLeafFrame`/`injectFrame`/`setFrameProperty`/`universe.random()` primitive
99
+ * chain every hand-placed-quantum experiment already uses — this module only
100
+ * CONSOLIDATES two independently-hand-rolled call sites into one, and adds a
101
+ * genuinely-new DECLARED knob (`zIsotropy`) that draws an EXTRA `vz` momentum
102
+ * component using the identical `(random()*2-1)*range` draw shape the `vy`
103
+ * spread already used — no new mechanism, just the same mechanism applied to a
104
+ * third axis. `velocityProfile:'spray'` + `zIsotropy:true` (tunnel-lab's new
105
+ * default) is expected to raise the settled z/y dispersion-spread ratio from
106
+ * the tester-measured 0.23 (pinned z, zero vz ever drawn) toward something
107
+ * close to the y-spread itself (the injected fan becomes 3D-symmetric; the
108
+ * existing free-walk + NEAR/FAR wall bounces already handle z correctly once
109
+ * they receive real z-momentum to work with — the measured `ax_only` branch
110
+ * gives y≈z isotropic transverse spread).
111
+ */
112
+ 'use strict';
113
+
114
+ // SOURCE_SPEC_FIELDS — the ONE declared contract (schema-mirror discipline,
115
+ // root CLAUDE.md). `default` is either a primitive or an object (deep-copied
116
+ // per-normalize call so no two sourceStates ever share a mutable spraySpec).
117
+ var SOURCE_SPEC_FIELDS = {
118
+ wavelength: { default: null, desc: 'Per-frame wavelength override (Engine Invariant #6 mass=max(1,capacity/wavelength)). null = engine default (mass=capacity).' },
119
+ charge: { default: null, desc: 'Per-frame charge override (0=Positive,1=Negative). null = random draw per quantum.' },
120
+ velocityProfile: { default: 'beam', enum: ['beam', 'spray'], desc: "'beam' = collimated single-direction (orientationRad+speed, vy=vz=0); 'spray' = biased random fan (spraySpec)." },
121
+ speed: { default: 1, desc: "'beam' profile only: speed magnitude." },
122
+ orientationRad: { default: 0, desc: "'beam' profile only: direction, radians (0 = +X)." },
123
+ 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)." },
124
+ 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)." },
125
+ 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)." },
126
+ level: { default: null, desc: "makeLeafFrame's level override (rarely used)." },
127
+ mass: { default: undefined, desc: 'Post-placement mass override via setFrameProperty. undefined = no override.' },
128
+ density: { default: 5, desc: 'Quanta hand-placed per burst.' },
129
+ rate: { default: 10, desc: 'Ticks between bursts (everyNTicks cadence).' },
130
+ active: { default: true, desc: 'Master on/off for the recurring source.' },
131
+ };
132
+
133
+ var SOURCE_SPEC_FIELD_NAMES = Object.keys(SOURCE_SPEC_FIELDS);
134
+
135
+ /**
136
+ * Normalize a partial spec into a FULL bag (every declared field present,
137
+ * defaults filled in) — the schema-mirror discipline's "consumers ferry the
138
+ * normalized bag" half. Never hand-re-enumerate a subset of
139
+ * SOURCE_SPEC_FIELDS elsewhere; always go through this function.
140
+ * @param {Object} [partial]
141
+ * @returns {SourceSpec}
142
+ */
143
+ function normalizeSourceSpec(partial) {
144
+ partial = partial || {};
145
+ var out = {};
146
+ for (var i = 0; i < SOURCE_SPEC_FIELD_NAMES.length; i++) {
147
+ var key = SOURCE_SPEC_FIELD_NAMES[i];
148
+ if (partial[key] !== undefined) {
149
+ out[key] = (key === 'spraySpec')
150
+ ? Object.assign({}, SOURCE_SPEC_FIELDS.spraySpec.default, partial[key]) // partial spraySpec override
151
+ : partial[key];
152
+ } else {
153
+ var def = SOURCE_SPEC_FIELDS[key].default;
154
+ out[key] = (def !== null && typeof def === 'object') ? Object.assign({}, def) : def; // fresh copy, never shared
155
+ }
156
+ }
157
+ return out;
158
+ }
159
+
160
+ /**
161
+ * Compute the fixed entrance pixel from a tunnel/tube's innerRect — the ONE
162
+ * formula `LightSource.js`'s own one-shot burst and both labs' original
163
+ * hand-rolled injectors independently derived (now declared once).
164
+ * @param {Object} innerRect - {left, top, z, w, h, d}
165
+ * @param {boolean} capNegX - true = closed breech (Gun-Chamber convention).
166
+ * @param {'center'|'top'} entranceBias
167
+ * @returns {{x:number, y:number}}
168
+ */
169
+ function _computeEntrance(innerRect, capNegX, entranceBias) {
170
+ var x = capNegX ? (innerRect.left + 2) : (innerRect.left + innerRect.w / 2);
171
+ var y = (entranceBias === 'top') ? (innerRect.top + 2) : (innerRect.top + innerRect.h / 2);
172
+ return { x: x, y: y };
173
+ }
174
+
175
+ /**
176
+ * The single low-level placement primitive — hand-places `count` quanta at
177
+ * ONE shared entrance pixel (the I1 scale-sweep's own validated ensemble
178
+ * technique: injection-time overlap does not merge same-tick frames; each
179
+ * leaf's own move-gate/wavelength-oscillator RNG draws diverge it from its
180
+ * siblings over subsequent ticks). Used for BOTH a one-shot burst
181
+ * (`LightSource.js`'s construction) and a recurring burst
182
+ * (`buildSustainedSource`'s tick-fn) — ONE place quantum properties get
183
+ * stamped.
184
+ * @param {Object} universe
185
+ * @param {Object} innerRect
186
+ * @param {boolean} capNegX
187
+ * @param {boolean} hasRealDepth - gate on the CALLER's explicit request for
188
+ * z-containment (N1 fix precedent, `LightSource.js`) — NEVER derive this
189
+ * from `innerRect.d`, which is always a positive placeholder even when
190
+ * the tunnel is flat 2D-in-3D.
191
+ * @param {SourceSpec} spec - a NORMALIZED spec (pass through
192
+ * `normalizeSourceSpec` first — this function does not fill defaults).
193
+ * @param {number} count
194
+ * @returns {Array<{handle:*, qid:*}>}
195
+ */
196
+ function placeSourceQuanta(universe, innerRect, capNegX, hasRealDepth, spec, count) {
197
+ var entrance = _computeEntrance(innerRect, capNegX, spec.entranceBias);
198
+ var entranceZ = hasRealDepth ? (innerRect.z + innerRect.d / 2) : null;
199
+ var placed = [];
200
+ for (var i = 0; i < count; i++) {
201
+ var vx, vy, vz;
202
+ if (spec.velocityProfile === 'spray') {
203
+ var s = spec.spraySpec;
204
+ vx = s.vxMin + universe.random() * s.vxRange;
205
+ vy = s.vyMin + universe.random() * s.vyRange;
206
+ // 3D-SYMMETRIC EMISSION (ws-dse-3d, theorist §4 option (a)): draw
207
+ // vz symmetric to the vy spread, SAME half-range, only when real
208
+ // depth is armed AND the caller opted into zIsotropy (default
209
+ // true) — makes the injected fan 3D-isotropic instead of pinning
210
+ // z-momentum at zero forever (the measured z/y=0.18-0.23 flatness
211
+ // root cause — architect Section 2e, tester Section C).
212
+ vz = (hasRealDepth && spec.zIsotropy) ? (universe.random() * 2 - 1) * s.vyRange : 0;
213
+ } else { // 'beam' — collimated; vy=vz=0 stays (deliberate, theorist §4 option (b))
214
+ vx = Math.cos(spec.orientationRad) * spec.speed;
215
+ vy = Math.sin(spec.orientationRad) * spec.speed;
216
+ vz = 0;
217
+ }
218
+ var handle = universe.makeLeafFrame(entrance.x, entrance.y, spec.level, spec.wavelength, vx, vy, false, spec.charge, null);
219
+ universe.injectFrame(handle);
220
+ var qid = universe.getFrameQuantumId(handle);
221
+ if (spec.mass !== undefined && spec.mass !== null) {
222
+ universe.setFrameProperty(qid, 'mass', spec.mass);
223
+ }
224
+ if (entranceZ !== null) {
225
+ universe.setFrameProperty(qid, 'z', entranceZ);
226
+ if (vz !== 0) { universe.setFrameProperty(qid, 'vz', vz); }
227
+ }
228
+ placed.push({ handle: handle, qid: qid });
229
+ }
230
+ return placed;
231
+ }
232
+
233
+ /**
234
+ * Build + register the ONE recurring hand-placed-quantum source tick-fn
235
+ * (kills the duplicated tunnel-lab / lightsource-lab injectors). LIVE knobs:
236
+ * every field of the returned `sourceState` is read FRESH on every burst —
237
+ * a UI slider mutating `sourceState.wavelength`/`density`/`rate`/`active`
238
+ * etc. takes effect on the NEXT burst, no re-registration.
239
+ * @param {Object} universe
240
+ * @param {Object} innerRect
241
+ * @param {boolean} capNegX
242
+ * @param {boolean} hasRealDepth
243
+ * @param {Object} [specPartial] - overrides onto SOURCE_SPEC_FIELDS' defaults.
244
+ * @returns {{ sourceState: SourceSpec, dispose: Function }}
245
+ */
246
+ function buildSustainedSource(universe, innerRect, capNegX, hasRealDepth, specPartial) {
247
+ var sourceState = normalizeSourceSpec(specPartial);
248
+
249
+ var tickCounter = 0;
250
+ var injectorTickFn = function () {
251
+ if (!sourceState.active) { return; }
252
+ tickCounter++;
253
+ if (tickCounter % Math.max(1, sourceState.rate) !== 0) { return; }
254
+ placeSourceQuanta(universe, innerRect, capNegX, hasRealDepth, sourceState, sourceState.density);
255
+ };
256
+ // Tag the tick-fn with a back-reference (mirrors QuantumEmitter.dispose's
257
+ // OWN `fns[j]._emState` idiom EXACTLY, and both prior labs' own
258
+ // `_sourceInjector` tag) so dispose() below can find + splice out THIS
259
+ // EXACT closure, never a different injector that happens to also be
260
+ // registered.
261
+ injectorTickFn._sourceInjector = sourceState;
262
+ universe.experimentTickFunctions = universe.experimentTickFunctions || [];
263
+ universe.experimentTickFunctions.push(injectorTickFn);
264
+
265
+ return {
266
+ sourceState: sourceState,
267
+ // CANONICAL TEARDOWN (see reference_frqtlapp_setup_canonical_teardown.md):
268
+ // tombstone (defense in depth) then physically splice the tick-fn out
269
+ // of experimentTickFunctions by identity. Idempotent — a second call
270
+ // is a safe no-op.
271
+ dispose: function () {
272
+ sourceState.active = false;
273
+ var fns = universe.experimentTickFunctions;
274
+ if (!Array.isArray(fns)) { return; }
275
+ for (var j = fns.length - 1; j >= 0; j--) {
276
+ if (fns[j] === injectorTickFn) { fns.splice(j, 1); }
277
+ }
278
+ },
279
+ };
280
+ }
281
+
282
+ /**
283
+ * capacityAtDimension(dim) — the ONE authority for "capacity of a leaf at
284
+ * fundamental dimension `dim`" (Engine Invariant #6: `width=2^dim,
285
+ * capacity=width²`). Both tunnel-lab and lightsource-lab derive their
286
+ * `MAX_USEFUL_WAVELENGTH` slider ceiling from this (CEO drive #2 ceiling
287
+ * fix, ws-dse-3d AC4 — "same MAX_USEFUL_WAVELENGTH clamp authority") —
288
+ * never a hand-typed "16" in either factory.
289
+ * @param {number} dim
290
+ * @returns {number}
291
+ */
292
+ function capacityAtDimension(dim) {
293
+ var width = Math.pow(2, dim);
294
+ return width * width;
295
+ }
296
+
297
+
298
+ export { SOURCE_SPEC_FIELDS, SOURCE_SPEC_FIELD_NAMES, normalizeSourceSpec, placeSourceQuanta, buildSustainedSource, capacityAtDimension };
299
+
@@ -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; docs/internal/engine/THE_MINIMAL_SIM.md §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 architect's render-intent stamp ruling): 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 };