@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,297 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * ObjectFactory — registry-driven construction of all placeable object kinds.
5
+ *
6
+ * This is the ONE canonical construction path for every IDE-placeable kind.
7
+ * It was extracted from lib/features/ide/project-design.js `buildObject` in
8
+ * Phase 1 of the ObjectFactory refactor so the construction logic lives in
9
+ * the objects layer (engine-agnostic) rather than in the IDE feature layer.
10
+ *
11
+ * API:
12
+ * ObjectFactory.create(kind, config, ctx)
13
+ * kind — placeable kind string (e.g. 'QuantumEmitter', 'SlitScreen')
14
+ * config — override props (merged over schema defaultProps)
15
+ * ctx — { runner, x, y, fromPalette? }
16
+ * The world size is READ from ctx.runner.canvas_rect — the rect the scene's
17
+ * co-init call wrote (exp.createCanvas(w, h, cfg) / exp.createScene(w, h, d, cfg));
18
+ * ctx carries no size of its own.
19
+ * returns: the DECLARED part (recorded in ctx.runner.declared); THROWS on a kind no
20
+ * module registered in this realm, on any build result that is not a handle,
21
+ * and on a member LO whose `_owner` chain does not end at the part
22
+ *
23
+ * ObjectFactory.list() → ObjectSchema.placeableCatalog()
24
+ * ObjectFactory.kinds() → the registered kinds that carry a build
25
+ *
26
+ * Construction: the kind's registry row (objects/kinds.js — the part module registered its
27
+ * descriptor, defaults and build when it was imported) → the generic checks below → the row's
28
+ * `build(props, ctx)`.
29
+ *
30
+ * Behavior contract:
31
+ * - Every special case from buildObject is preserved verbatim:
32
+ * • QuantumEmitter / PointSource / SphereEmitter: the emitter + a visual
33
+ * LargeObject marker it OWNS (attachMarker); the emitter is returned.
34
+ * • A builder stamps nothing: each part stamps `_owner` on what it composed,
35
+ * and create() declares the returned part with the LOs the build added.
36
+ * • PointSource opts dict re-pass (_wavelength, _optColor, _emergentPhysics).
37
+ * • layer → observedDimensions=[layer] (null = all dims).
38
+ * • SlitLightSource: THROWS naming the pairing (needs a paired SlitScreen).
39
+ * • Unknown kind: THROW naming the registered kinds + the V2 replacement.
40
+ */
41
+
42
+ import { ObjectSchema } from './schema/index.js';
43
+ import { kindOf, registeredKinds, unregisteredKindMessage } from './kinds.js';
44
+ import { setupSphereEmitter3D } from './setup/setupSphereEmitter3D.js';
45
+ import { SphereEmitter } from '../parts/SphereEmitter.js';
46
+ import { V1_REPLACEMENTS } from './v1-replacements.js';
47
+
48
+ // ── Internal helpers ──────────────────────────────────────────────────────
49
+
50
+ function _deepCopy(obj) {
51
+ if (obj === null || typeof obj !== 'object') { return obj; }
52
+ if (Array.isArray(obj)) { return obj.map(_deepCopy); }
53
+ var out = {};
54
+ for (var k in obj) {
55
+ if (Object.prototype.hasOwnProperty.call(obj, k)) { out[k] = _deepCopy(obj[k]); }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /**
61
+ * Merge schema defaultProps with caller overrides and return the merged props.
62
+ * @param {string} kind
63
+ * @param {object|null} config — override props (caller-supplied)
64
+ * @returns {object}
65
+ */
66
+ // Construction defaults come from ObjectSchema.defaultPropsFor(kind) — the ONE
67
+ // per-kind default bag (the `defaults` the kind registered with, objects/kinds.js). NOT from
68
+ // placeableCatalog(): the palette answers "what can be DROPPED in STUDIO", which
69
+ // is a strictly smaller set (V2 stock only) than "what a recipe can construct".
70
+ //
71
+ function _mergeProps(kind, config) {
72
+ var defaults = _deepCopy(ObjectSchema.defaultPropsFor(kind));
73
+ return Object.assign({}, defaults, config || {});
74
+ }
75
+
76
+ // ── CONSTRUCTION_ADAPTERS — ObjectFactory's OWN cross-kind config vocabulary ──
77
+ //
78
+ // `create()` already threw on an unknown KIND with the registered list and the
79
+ // V1→V2 replacement. It never looked at the config's PROP KEYS, so
80
+ // `create('Wall', { collisonBehavior: 'bounce' })` built a wall that ignored the
81
+ // typo and silently used its own default — the same silence the setup-factory gate
82
+ // removes one layer up.
83
+ //
84
+ // The schema descriptor alone is NOT the legal set. These keys are read by the
85
+ // builders (or forwarded verbatim into an object constructor) while being declared
86
+ // by no kind's field list, so validating on the descriptor alone would reject
87
+ // working callers. They are ObjectFactory's own construction API — it owns the call
88
+ // shape — so they are declared HERE, and `objectfactory-prop-keys.test.mjs` asserts
89
+ // each one is genuinely reachable rather than a list someone widened to silence a
90
+ // failure.
91
+ var CONSTRUCTION_ADAPTERS = [
92
+ // Geometry/identity every builder resolves a rect and a name from. `x`/`y` are
93
+ // read by RecipeContext.addObject to build ctx before create() runs.
94
+ 'x', 'y', 'w', 'h', 'left', 'top', 'rect', 'layer', 'name',
95
+ // `preset` is expanded by the OBJECT constructor (Wall.expandWallPreset), not by
96
+ // a builder — it reaches the constructor through the forwarded props bag.
97
+ 'preset',
98
+ // Codegen round-trip adapters: a PointSource's construction-only opts, which codegen
99
+ // writes back onto its config (lib/features/ide/codegen.js).
100
+ '_wavelength', '_optColor', '_emergentPhysics'
101
+ ];
102
+
103
+ // ── FORWARDED — kinds whose config bag is a PART's option bag, not just a descriptor ──
104
+ //
105
+ // A facade kind hands its config to a setup factory, so its legal set includes that
106
+ // factory's vocabulary. Measured: `emissionBoxSide` is a real SphereEmitter option
107
+ // that the descriptor does not declare, and gating on the descriptor alone would
108
+ // reject the walkthrough's own recipes.
109
+ //
110
+ // CONSUMED, never retyped: `setupSphereEmitter3D.OPTIONS` and
111
+ // `SphereEmitter.FACADE_OPTIONS` are the owners' exported contracts.
112
+ function _forwardedVocabulary(kind) {
113
+ if (kind !== 'SphereEmitter') { return []; }
114
+ var out = [];
115
+ var f = setupSphereEmitter3D;
116
+ var o = SphereEmitter;
117
+ if (f.OPTIONS) { out = out.concat(f.OPTIONS); }
118
+ // Retired names pass THROUGH this gate so the owner's specific migration message
119
+ // wins over a generic "unknown prop key" (the a1f1b324 control-D ordering, again).
120
+ if (f.RETIRED_OPTIONS) { out = out.concat(f.RETIRED_OPTIONS); }
121
+ if (o.FACADE_OPTIONS) { out = out.concat(o.FACADE_OPTIONS); }
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * Throw on a config key that no kind, default, adapter or forwarded vocabulary
127
+ * declares. Every list is DERIVED (from the schema `create()` already resolved, or
128
+ * from an owner's export) — nothing here re-enumerates another module's fields.
129
+ */
130
+ function _assertPropKeys(kind, config) {
131
+ if (!config) { return; }
132
+ var descriptor = ObjectSchema.get(kind);
133
+ if (!descriptor || !descriptor.fields) { return; }
134
+
135
+ var legal = {};
136
+ descriptor.fields.forEach(function (f) { legal[f.name] = true; });
137
+ Object.keys(ObjectSchema.defaultPropsFor(kind)).forEach(function (k) { legal[k] = true; });
138
+ CONSTRUCTION_ADAPTERS.forEach(function (k) { legal[k] = true; });
139
+ _forwardedVocabulary(kind).forEach(function (k) { legal[k] = true; });
140
+
141
+ var unknown = Object.keys(config).filter(function (k) { return !legal[k]; });
142
+ if (!unknown.length) { return; }
143
+
144
+ var declaredFields = descriptor.fields.map(function (f) { return f.name; }).sort();
145
+ throw new Error(
146
+ '[ObjectFactory] create("' + kind + '"): unknown prop key' +
147
+ (unknown.length > 1 ? 's' : '') + ' ' + unknown.map(function (k) {
148
+ return '"' + k + '"';
149
+ }).join(', ') + '. The builder reads only declared props, so this would have ' +
150
+ 'been merged in, read by nobody, and the object built with ITS OWN default — ' +
151
+ 'a different scene than the one you wrote, with no signal. "' + kind +
152
+ '" declares: ' + declaredFields.join(', ') +
153
+ '. Construction adapters: ' + CONSTRUCTION_ADAPTERS.slice().sort().join(', ') + '.'
154
+ );
155
+ }
156
+
157
+ /**
158
+ * A `writeOnly` descriptor field is a live reposition ADAPTER (a SphereEmitter's or a Tunnel's
159
+ * `rect`): it moves a built part, and its read getter derives from the part's real position
160
+ * fields. It is never a construction key — the part is built from those real fields — so a
161
+ * config carrying one THROWS here rather than being dropped silently by the builder.
162
+ */
163
+ function _refuseWriteOnlyKeys(kind, config) {
164
+ if (!config) { return; }
165
+ var descriptor = ObjectSchema.get(kind);
166
+ if (!descriptor || !descriptor.fields) { return; }
167
+ var wo = descriptor.fields.filter(function (fd) { return fd.writeOnly === true && config[fd.name] !== undefined; });
168
+ if (!wo.length) { return; }
169
+ var real = descriptor.fields.filter(function (fd) { return !fd.writeOnly; }).map(function (fd) { return fd.name; });
170
+ throw new Error('[ObjectFactory] create("' + kind + '"): "' + wo[0].name + '" is a write-only reposition ' +
171
+ 'adapter (it moves a built ' + kind + '), not a construction key. Declare the position with the ' +
172
+ 'fields the part is built from: ' + real.join(', ') + '. (FAIL-LOUD.)');
173
+ }
174
+
175
+
176
+ // How many objects of `kind` this runner has declared — the ordinal a numbered default name
177
+ // ('emitter N', 'source N') carries.
178
+ function declaredCount(runner, kind) {
179
+ return runner.declared.filter(function (d) { return d._schemaKind === kind; }).length;
180
+ }
181
+
182
+ // ── ObjectFactory ─────────────────────────────────────────────────────────
183
+
184
+ var ObjectFactory = {
185
+ /**
186
+ * Construct a placeable object of the given kind and DECLARE it: the part the builder
187
+ * returns (never one of its members) is recorded in `ctx.runner.declared` with the census LOs
188
+ * the build added as its members and the host tick-functions the build pushed
189
+ * (`FrqtlApp.declare` — this is its ONE call site). What a
190
+ * declaring call (`exp.addObject`, a palette drop, a codegen replay) produced is what STUDIO
191
+ * holds; a part's composition is never declared.
192
+ *
193
+ * FAIL-LOUD: an unknown kind THROWS naming the registered kinds and, when
194
+ * the kind is a removed V1 part, its V2 replacement composition. A recipe
195
+ * whose builder no longer exists must never half-run into a scene with
196
+ * missing parts.
197
+ *
198
+ * @param {string|Function} kind — the kind name, or its registered class
199
+ * @param {object|null} config — override props (merged over schema defaultProps)
200
+ * @param {object} ctx — { runner, x, y, fromPalette? } (the size is runner.canvas_rect)
201
+ * @returns {object} — the declared part (a Tunnel, not its plate; a SphereEmitter, not its marker)
202
+ * @throws {Error} on a kind no module registered here, a kind registered without a build,
203
+ * a ctx without a runner, or a build result that is not a handle
204
+ */
205
+ create: function (kind, config, ctx) {
206
+ if (!ctx || !ctx.runner) {
207
+ throw new Error('[ObjectFactory] create("' + kind + '"): ctx.runner is required — ' +
208
+ 'pass { runner, x, y }.');
209
+ }
210
+ if (typeof kind === 'function') {
211
+ // A kind is named by its class: the class itself is the import-form spelling.
212
+ if (!kindOf(kind)) {
213
+ throw new Error('[ObjectFactory] create: the class ' + (kind.name || '(anonymous)') + ' is not a ' +
214
+ 'registered kind in this realm — ' + unregisteredKindMessage(kind.name || '?') + '. (FAIL-LOUD.)');
215
+ }
216
+ kind = kind.name;
217
+ }
218
+ var row = kindOf(kind);
219
+ if (!row) {
220
+ var msg = '[ObjectFactory] create: unknown kind "' + kind + '". Registered kinds: ' +
221
+ ObjectFactory.kinds().slice().sort().join(', ') + '. The ' + unregisteredKindMessage(kind) + '.';
222
+ if (Object.prototype.hasOwnProperty.call(V1_REPLACEMENTS, kind)) {
223
+ msg += ' "' + kind + '" is a retired V1 part, not a published part — compose ' + V1_REPLACEMENTS[kind] + ' instead ' +
224
+ '(docs/internal/engine/V2_PARTS_CLASSIFICATION.md).';
225
+ }
226
+ throw new Error(msg + ' (FAIL-LOUD.)');
227
+ }
228
+ if (typeof row.build !== 'function') {
229
+ throw new Error('[ObjectFactory] create: kind "' + kind + '" registers a descriptor only — it is not ' +
230
+ 'constructed by ObjectFactory; construct it with new ' + kind + '(…) (' + row.from + '). (FAIL-LOUD.)');
231
+ }
232
+
233
+ // ORDER IS LOAD-BEARING: the unknown-KIND throw runs first (it names the V1→V2
234
+ // replacement), then the prop-key check, then the merge. Validating props for a
235
+ // kind that does not exist would replace a useful migration message with a
236
+ // confusing field list.
237
+ _assertPropKeys(kind, config);
238
+ _refuseWriteOnlyKeys(kind, config);
239
+
240
+ var props = _mergeProps(kind, config);
241
+
242
+ var runner = ctx.runner;
243
+ var before = new Set(runner.largeObjects);
244
+ var tfs = (runner.universe && Array.isArray(runner.universe.experimentTickFunctions))
245
+ ? runner.universe.experimentTickFunctions : null;
246
+ var ticksBefore = new Set(tfs || []);
247
+ var handle = row.build(props, ctx, row.Class); // the registry builds the class that registered
248
+ if (handle === null || typeof handle !== 'object') {
249
+ throw new Error('[ObjectFactory] create("' + kind + '"): the builder returned ' +
250
+ (handle === null ? 'null' : typeof handle) + ', not a handle. Every kind returns ' +
251
+ 'the object it built or throws naming what is missing — a non-handle here is a ' +
252
+ 'part silently absent from the scene. (FAIL-LOUD.)');
253
+ }
254
+ if (typeof runner.declare !== 'function') {
255
+ throw new Error('[ObjectFactory] create("' + kind + '"): ctx.runner has no declare() — the ' +
256
+ 'runner (FrqtlApp) owns the declared-object registry every created part is recorded in. ' +
257
+ '(FAIL-LOUD.)');
258
+ }
259
+ // THE ONE REGISTRATION SITE: what a declaring call produced is the declared object; its
260
+ // members are exactly the census LOs the build added (each must resolve up `_owner` to it).
261
+ var members = runner.largeObjects.filter(function (lo) { return !before.has(lo); });
262
+ if (!members.length) {
263
+ throw new Error('[ObjectFactory] create("' + kind + '"): the build put nothing in the world — ' +
264
+ 'every part draws at least one LO, so a part with none is silently absent from the ' +
265
+ 'scene. (FAIL-LOUD.)');
266
+ }
267
+ // ... and the host tick-functions it pushed are the declaration's too (an emitter's emit
268
+ // fn, an absorb radiator): behaviour the declaration owns, so codegen may write it down.
269
+ var tfsAfter = (runner.universe && Array.isArray(runner.universe.experimentTickFunctions))
270
+ ? runner.universe.experimentTickFunctions : [];
271
+ var ticks = tfsAfter.filter(function (fn) { return !ticksBefore.has(fn); });
272
+ runner.declare(handle, members, ticks);
273
+ return handle;
274
+ },
275
+
276
+ /**
277
+ * Returns the schema-driven palette catalog.
278
+ * Delegates to ObjectSchema.placeableCatalog().
279
+ * @returns {Array}
280
+ */
281
+ list: function () {
282
+ return ObjectSchema.placeableCatalog();
283
+ },
284
+
285
+ /**
286
+ * The registered kinds that carry a build (constructible through create()).
287
+ * @returns {string[]}
288
+ */
289
+ kinds: function () {
290
+ return registeredKinds().filter(function (k) { return typeof kindOf(k).build === 'function'; });
291
+ }
292
+ };
293
+
294
+ // ── Export ────────────────────────────────────────────────────────────────
295
+
296
+ export { ObjectFactory, declaredCount };
297
+
@@ -0,0 +1,322 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * RecipeContext — the recipe facade (`exp`) a scene's buildExperiment(exp) is evaluated against.
5
+ *
6
+ * A scene's FIRST statement co-initializes its display and its universe — one call, one rect,
7
+ * named by the renderer it initializes (the Pixi `new Application({width, height})` / p5
8
+ * `createCanvas(w, h)` line of a FRQTL sketch):
9
+ *
10
+ * exp.createCanvas(w, h, worldCfg) p5: a 2D canvas AND a 2D universe of exactly w×h
11
+ * exp.createScene(w, h, d, worldCfg) three: a 3D scene AND a 3D universe w×h×d
12
+ *
13
+ * That call is the ONLY place a world size is written: the facade is constructed SIZELESS
14
+ * (`{canvasParentId, seed, onTick, …}`) and builds its runner INSIDE the call
15
+ * (`new FrqtlApp({canvasW: w, canvasH: h, …})` → `runner.createCanvas()` →
16
+ * `universe.init(...FrqtlWorldInit.initArgs(cfg))`). `worldCfg` is the world
17
+ * vocabulary minus what the call owns (`FrqtlWorldInit.INIT_CFG`; a size, `activeAxes` or
18
+ * `depth` in it THROWS naming this call). 2D ⇒ the engine's {x,y} default; 3D ⇒ {x,y,z} and the
19
+ * explicit depth d.
20
+ *
21
+ * exp.world the ONE readable answer: frozen {renderer, w, h, d, cfg}
22
+ * exp.addObject(kind, config) ObjectFactory.create(kind, config, ctx) sugar
23
+ * exp.addTickFunction(fn) universe.experimentTickFunctions.push
24
+ * exp.render(bag) runner.setRenderOptions(bag)
25
+ * exp.build(buildFn) run the recipe WITHOUT starting the loop (a sketch held as
26
+ * TEXT is a project, linked first: lib/shim/project/link.js linkProject)
27
+ * exp.run(buildFn) build (if needed) + start the loop
28
+ * exp.runner / .universe the runner the co-init call built
29
+ * exp.is3D world.renderer === 'three'
30
+ *
31
+ * FAIL-LOUD: every member that needs the world THROWS before the call; a second call in one
32
+ * recipe run THROWS; a call naming a renderer the host did not boot THROWS; a recipe that returns
33
+ * without a world THROWS from build(); `exp.init(cfg)` is DELETED and throws naming the two
34
+ * methods. None degrades to a default size.
35
+ *
36
+ * The page's facade: build()/run() make THIS facade the page's experiment (`pageExperiment()`,
37
+ * read by a host frame through the page door as FrqtlPage.experiment); the runner it built is the
38
+ * page's runner once its loop starts (FrqtlApp `pageRunner()`).
39
+ */
40
+
41
+ import { FrqtlApp } from './FrqtlApp.js';
42
+ import { ObjectFactory } from './ObjectFactory.js';
43
+ import { FrqtlWorldInit } from './setup/_worldInit.js';
44
+ import * as FrqtlDisplay from '../compat/renderer/display-slot.js';
45
+ import { refuseRetiredDoors } from '../compat/engine.js';
46
+
47
+ /** The world-init contract (objects/setup/_worldInit.js). */
48
+ function _worldInit() {
49
+ return FrqtlWorldInit;
50
+ }
51
+
52
+ /** The display slot's booted renderer name (FrqtlDisplay — the ONE display owner). */
53
+ function _bootedRenderer() {
54
+ var n = FrqtlDisplay.name();
55
+ if (!n) {
56
+ throw new Error('[RecipeContext] no display is booted (FrqtlDisplay.name() is empty): the host ' +
57
+ 'boots the renderer the scene\'s registry row names (renderState.renderer) before the ' +
58
+ 'scene runs. (FAIL-LOUD.)');
59
+ }
60
+ return n;
61
+ }
62
+
63
+ var COINIT = 'exp.createCanvas(w, h, cfg) (p5, 2D) or exp.createScene(w, h, d, cfg) (three, 3D)';
64
+ var NO_WORLD = '[RecipeContext] the scene has not created its world — its first statement is ' + COINIT + '.';
65
+
66
+ // The ctor options a SIZELESS facade forwards to the runner it builds (everything but a size).
67
+ var RUNNER_OPTS = ['canvasParentId', 'seed', 'tickHz', 'drawHz', 'stepsPerFrame', 'onTick', 'onReady', 'engine'];
68
+ var FACADE_OPTS = RUNNER_OPTS.concat(['deferCanvas']);
69
+
70
+ /**
71
+ * @param {Object} [opts] — `{canvasParentId, seed, onTick, onReady, tickHz, drawHz, stepsPerFrame,
72
+ * engine, deferCanvas}`. NO size and NO 3D flag: the scene's co-init call states both.
73
+ * `deferCanvas:true` leaves `runner.createCanvas()` to a host mount seam (the notebook realm).
74
+ */
75
+ function RecipeContext(opts) {
76
+ opts = opts || {};
77
+ Object.keys(opts).forEach(function (k) {
78
+ if (FACADE_OPTS.indexOf(k) === -1) {
79
+ throw new Error('[RecipeContext] unknown option "' + k + '" — the facade is SIZELESS: it takes ' +
80
+ FACADE_OPTS.join(', ') + '. A world size (canvasW/canvasH) or 3D-ness is the scene\'s, ' +
81
+ 'stated by ' + COINIT + '. (FAIL-LOUD.)');
82
+ }
83
+ });
84
+ this._opts = opts;
85
+ this._runner = null; // built by the co-init call
86
+ this._world = null; // set by the co-init call; reset by build()
87
+ this._built = false;
88
+ this._buildFn = null;
89
+ }
90
+
91
+ function _requireWorld(self) {
92
+ if (!self._world) { throw new Error(NO_WORLD); }
93
+ }
94
+
95
+ Object.defineProperty(RecipeContext.prototype, 'world', { get: function () { return this._world; } });
96
+ Object.defineProperty(RecipeContext.prototype, 'runner', {
97
+ get: function () { _requireWorld(this); return this._runner; }
98
+ });
99
+ Object.defineProperty(RecipeContext.prototype, 'universe', {
100
+ get: function () { _requireWorld(this); return this._runner.universe; }
101
+ });
102
+ /** READ-ONLY: did THIS scene make a 3D world? The method it called answers (three ⇔ 3D). */
103
+ Object.defineProperty(RecipeContext.prototype, 'is3D', {
104
+ get: function () { _requireWorld(this); return this._world.renderer === 'three'; }
105
+ });
106
+
107
+ // ── the co-init call ─────────────────────────────────────────────────────────────
108
+
109
+ function _coinit(self, method, renderer, w, h, d, cfg) {
110
+ var where = 'RecipeContext.' + method;
111
+ if (self._world) {
112
+ throw new Error('[' + where + '] one world per scene: this recipe already created ' +
113
+ JSON.stringify({ renderer: self._world.renderer, w: self._world.w, h: self._world.h, d: self._world.d }) +
114
+ '. (FAIL-LOUD.)');
115
+ }
116
+ [['w', w], ['h', h]].concat(renderer === 'three' ? [['d', d]] : []).forEach(function (p) {
117
+ if (typeof p[1] !== 'number' || !(p[1] > 0)) {
118
+ throw new Error('[' + where + '] ' + p[0] + ' must be a positive number (got ' + JSON.stringify(p[1]) +
119
+ ') — every scene states its whole world' + (p[0] === 'd' ? '; the engine\'s cube default is not relied on' : '') +
120
+ '. (FAIL-LOUD.)');
121
+ }
122
+ });
123
+ cfg = cfg || {};
124
+ var W = _worldInit();
125
+ W.assertInitCfg(where + '(…, cfg)', cfg, null);
126
+ var booted = _bootedRenderer();
127
+ if (booted !== renderer) {
128
+ var err = new Error('[' + where + '] the scene calls ' + method + ' (' + renderer + ') but the host ' +
129
+ 'booted "' + booted + '" — the host\'s display (a registry row\'s renderState.renderer, bootScene\'s ' +
130
+ 'opts.renderer) and the scene disagree. (FAIL-LOUD.)');
131
+ err.frqtlRequiredRenderer = renderer;
132
+ err.frqtlBootedRenderer = booted;
133
+ throw err;
134
+ }
135
+ if (self._runner) {
136
+ // Restart on the same page replays the same single call: the runner (and its canvas)
137
+ // already ARE that world. A different world needs a fresh page.
138
+ var r = self._runner.canvas_rect;
139
+ if (r.w !== w || r.h !== h || self._runner._depth !== (renderer === 'three' ? d : null) ||
140
+ self._runner._is3D !== (renderer === 'three')) {
141
+ throw new Error('[' + where + '] this page already holds a ' + r.w + 'x' + r.h + ' world; a scene ' +
142
+ 'with a different world needs a fresh page. (FAIL-LOUD.)');
143
+ }
144
+ } else {
145
+ var ropts = { canvasW: w, canvasH: h, is3D: renderer === 'three' };
146
+ if (renderer === 'three') { ropts.canvasD = d; }
147
+ RUNNER_OPTS.forEach(function (k) { if (self._opts[k] !== undefined) { ropts[k] = self._opts[k]; } });
148
+ self._runner = new FrqtlApp(ropts);
149
+ if (!self._opts.deferCanvas) { self._runner.createCanvas(); }
150
+ }
151
+ var u = self._runner.universe;
152
+ var args = W.initArgs(cfg);
153
+ if (renderer === 'three') {
154
+ args[1].activeAxes = { x: true, y: true, z: true };
155
+ args[1].depth = d;
156
+ }
157
+ // A (re-)inited world has no declared objects; the recipe re-declares what it builds.
158
+ self._runner.clearDeclared();
159
+ W.applyPreInit(cfg, u); // pre-init world keys (iframeIntervalSeconds) — the engine reads them at init
160
+ u.init.apply(u, args);
161
+ self._world = Object.freeze({ renderer: renderer, w: w, h: h, d: renderer === 'three' ? d : null,
162
+ cfg: Object.freeze(Object.assign({}, cfg)) });
163
+ return self;
164
+ }
165
+
166
+ /** p5: a 2D canvas AND a 2D universe of exactly w×h. */
167
+ RecipeContext.prototype.createCanvas = function (w, h, cfg) {
168
+ if (arguments.length > 3) {
169
+ throw new Error('[RecipeContext.createCanvas] takes (w, h, cfg) — a 2D world has no depth; a 3D ' +
170
+ 'world is exp.createScene(w, h, d, cfg). (FAIL-LOUD.)');
171
+ }
172
+ return _coinit(this, 'createCanvas', 'p5', w, h, null, cfg);
173
+ };
174
+
175
+ /** three: a 3D scene AND a 3D universe w×h×d (d REQUIRED). */
176
+ RecipeContext.prototype.createScene = function (w, h, d, cfg) {
177
+ return _coinit(this, 'createScene', 'three', w, h, d, cfg);
178
+ };
179
+
180
+ // ── deleted: init(cfg) / initCfg ─────────────────────────────────────────────────
181
+ RecipeContext.prototype.init = function () {
182
+ throw new Error('[RecipeContext] exp.init(cfg) is DELETED — a scene states its world, size included, ' +
183
+ 'with ' + COINIT + '. (FAIL-LOUD: a saved module carrying exp.init( must be re-saved.)');
184
+ };
185
+ Object.defineProperty(RecipeContext.prototype, 'initCfg', {
186
+ get: function () {
187
+ throw new Error('[RecipeContext] exp.initCfg is DELETED — read exp.world ({renderer, w, h, d, cfg}). (FAIL-LOUD.)');
188
+ }
189
+ });
190
+
191
+ // ── parts, tick-fns, render ──────────────────────────────────────────────────────
192
+
193
+ RecipeContext.prototype.addObject = function (kind, config) {
194
+ _requireWorld(this);
195
+ var x = (config && config.x != null) ? config.x : 0;
196
+ var y = (config && config.y != null) ? config.y : 0;
197
+ var ctx = { runner: this._runner, cw: null, x: x, y: y };
198
+ return ObjectFactory.create(kind, config, ctx);
199
+ };
200
+
201
+ RecipeContext.prototype.addTickFunction = function (fn) {
202
+ _requireWorld(this);
203
+ var fns = this._runner.universe.experimentTickFunctions;
204
+ fns.push(fn);
205
+ return fns.length - 1;
206
+ };
207
+
208
+ /** Name the recipe's LOOK in one call — a pure ferry to `runner.setRenderOptions(bag)`. */
209
+ RecipeContext.prototype.render = function (bag) {
210
+ _requireWorld(this);
211
+ this._runner.setRenderOptions(bag);
212
+ return this;
213
+ };
214
+
215
+ // ── build / run ──────────────────────────────────────────────────────────────────
216
+
217
+ /**
218
+ * Tear down the scene a previous build put on THIS runner, before a same-runner rebuild
219
+ * (`restart()`, a re-applied module on a live page): FAIL-LOUD, never a sweep.
220
+ *
221
+ * OWNERSHIP: `universe.experimentTickFunctions` is the SCENE's (primitive #3) — rebuilt by the
222
+ * replay, so the old scene's entries must all be gone first. `runner.addTickListener` is the
223
+ * HOST's (a recorder, a live chart, a per-tick lerp) and SURVIVES the rebuild untouched.
224
+ *
225
+ * 1. stop the loop;
226
+ * 2. read the scene's UNDECLARED tick-fns (`exp.addTickFunction`) while the registry still
227
+ * says which fns the declared parts own;
228
+ * 3. remove every declared object through `runner.removeObject` — the one per-part path
229
+ * (registry drop → its own dispose() → census check, which THROWS naming a part whose
230
+ * dispose() left a tick-fn or an LO behind);
231
+ * 4. splice the undeclared fns read at step 2;
232
+ * 5. ASSERT the list is empty — a survivor is a tick-fn nothing owned, never cleared for it.
233
+ */
234
+ function _teardownSceneForRebuild(self) {
235
+ var runner = self._runner;
236
+ runner.stop();
237
+ var fns = runner.universe.experimentTickFunctions;
238
+ var undeclared = runner.undeclaredTickFunctions();
239
+ var declared = runner.declared.slice();
240
+ for (var i = declared.length - 1; i >= 0; i--) { runner.removeObject(declared[i]); }
241
+ for (var j = 0; j < undeclared.length; j++) {
242
+ var at = fns.indexOf(undeclared[j]);
243
+ if (at !== -1) { fns.splice(at, 1); }
244
+ }
245
+ if (fns.length) {
246
+ throw new Error('[RecipeContext.build] rebuilding on the same runner: ' + fns.length + ' tick-function(s) ' +
247
+ 'of the previous scene survived its teardown (' + fns.map(function (fn, k) {
248
+ return fn.name ? fn.name : '<anonymous #' + k + '>'; }).join(', ') + ') — no declared part ' +
249
+ 'owned them and the scene did not add them (exp.addTickFunction). Host per-tick work belongs ' +
250
+ 'on runner.addTickListener, which survives a rebuild. (FAIL-LOUD.)');
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Run a recipe WITHOUT starting the loop. Resets `world` first, so a re-run replays the same
256
+ * single co-init call; a recipe that returns without creating its world THROWS. On a runner a
257
+ * previous build already holds, the previous scene is torn down first (_teardownSceneForRebuild:
258
+ * the scene's tick-fns go, the host's tick listeners stay). A successful build RETAINS its
259
+ * replay on the runner (`runner.retainSceneReplay`), so `runner.restart()` — and a sketch's
260
+ * declared `restart` control — replays this build on the same runner and canvas.
261
+ */
262
+ RecipeContext.prototype.build = function (buildFn) {
263
+ if (buildFn && typeof buildFn === 'function') {
264
+ this._buildFn = buildFn;
265
+ if (this._runner) { _teardownSceneForRebuild(this); }
266
+ this._world = null;
267
+ buildFn(this);
268
+ if (!this._world) {
269
+ throw new Error('[RecipeContext.build] the scene never created its world — its first statement ' +
270
+ 'must be ' + COINIT + '. (FAIL-LOUD.)');
271
+ }
272
+ this._built = true;
273
+ var self = this;
274
+ this._runner.retainSceneReplay(function () { return self.build(self._buildFn); });
275
+ }
276
+ setPageExperiment(this);
277
+ return this;
278
+ };
279
+
280
+ RecipeContext.prototype.run = function (buildFn) {
281
+ if (buildFn && typeof buildFn === 'function' && !this._built) { this.build(buildFn); }
282
+ _requireWorld(this);
283
+ setPageExperiment(this);
284
+ this._runner.start();
285
+ };
286
+
287
+ // The world-init contract is NOT a RecipeContext static: reading one of its names THROWS.
288
+ ['INIT_CFG', 'assertInitCfg', 'initArgs'].forEach(function (name) {
289
+ Object.defineProperty(RecipeContext, name, {
290
+ get: function () {
291
+ throw new Error('RecipeContext.' + name + ' does not exist: the world-cfg contract is ' +
292
+ 'FrqtlWorldInit.' + name + ' (objects/setup/_worldInit.js).');
293
+ }
294
+ });
295
+ });
296
+
297
+ /** Retired facade members → the message reading one throws (compat/engine.js refuseRetiredDoors). */
298
+ var RETIRED_RECIPE_DOORS = Object.freeze({
299
+ buildSource: '[RecipeContext] exp.buildSource(src) is retired — a sketch held as text is a project: boot it ' +
300
+ 'with bootProject(mount, { files: { "sketch.js": src }, entry: "sketch.js" }) (@descix/frqtl-web/boot), or ' +
301
+ 're-scene a booted facade with exp.build((await linkProject({ files, entry, mint })).buildExperiment) ' +
302
+ '(lib/shim/project/link.js). (FAIL-LOUD.)'
303
+ });
304
+ refuseRetiredDoors(RecipeContext.prototype, RETIRED_RECIPE_DOORS);
305
+
306
+ // ── The page's experiment ─────────────────────────────────────────────────
307
+ // The facade a page built its scene through (or, on the empty page, the sizeless facade it
308
+ // constructed for hosts to apply scenes against). The page door (experiments/_shared/page-door.js)
309
+ // reads it as FrqtlPage.experiment — this is its ONE store.
310
+ var _pageExperiment = null;
311
+ /** @param {RecipeContext} exp */
312
+ function setPageExperiment(exp) {
313
+ if (!(exp instanceof RecipeContext)) {
314
+ throw new TypeError('[RecipeContext] setPageExperiment(exp): expected a RecipeContext. (FAIL-LOUD.)');
315
+ }
316
+ _pageExperiment = exp;
317
+ }
318
+ /** @returns {RecipeContext|null} */
319
+ function pageExperiment() { return _pageExperiment; }
320
+
321
+ export { RecipeContext, RETIRED_RECIPE_DOORS, setPageExperiment, pageExperiment };
322
+