@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,824 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * makeControlsForObject.js — the schema→controls FACTORY (WS Studio Controls
5
+ * Realignment, Bite S2, "the factory spine").
6
+ *
7
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
8
+ *
9
+ * `makeControlsForObject(wrapper, ctx, opts)` → `{ kind, objectName, controls, display }`.
10
+ *
11
+ * Walks `ObjectSchema.get(kind).fields` (the property-bag authority,
12
+ * `lib/frqtl/objects/schema/`) and mechanically emits ONE contract-legal control
13
+ * descriptor per editable field (`lib/shim/controls` — the control-surface
14
+ * authority) — never a hand-mirrored per-object control array. Each generated
15
+ * control carries `bind:{object, prop}` (a `channel:'physics'` object bind,
16
+ * resolved at execution time by `controlBind.js`'s `resolveBind`, this file's
17
+ * sibling) — this factory NEVER synthesizes an `onChange` closure (see the
18
+ * "no-onChange-synthesis" scope note below; it is a hard constraint, not an
19
+ * oversight, and it is WHY several field types below are DISPLAY-ONLY rather
20
+ * than live controls in this bite).
21
+ *
22
+ * @param {*} wrapper — the live object to introspect:
23
+ * - LargeObject/QuantumEmitter/PointSource/DetectorWall/SlitScreen/Box/OvenBox/
24
+ * CircuitGate/CircuitProbe/backdrop wrappers — a JS object carrying `_schemaKind`
25
+ * (kind is read off it automatically; `opts.kind` may still override).
26
+ * - a Frame — NOT an object; per the registry's own documented precedent
27
+ * (`lib/frqtl/objects/objects/controlBind.js` file docstring, "Registration
28
+ * precedent"), a Frame bind target is the bare QUANTUM_ID (a number, from
29
+ * `universe.getFrameQuantumId(handle)`). Pass the qid as `wrapper` and
30
+ * `opts.kind = 'Frame'` (required — a number has no `_schemaKind`).
31
+ * - the Universe singleton — pass ANY truthy placeholder as `wrapper` (unused)
32
+ * and `opts.kind = 'Universe'`; `objectName` resolves to the reserved
33
+ * `'universe'` sentinel automatically (matches `controlBind.js`'s
34
+ * `bind.object === 'universe'` dispatch branch) unless overridden.
35
+ * @param {object} [ctx] — read-context, passed straight through to per-field
36
+ * live-value readers (NOT to `resolveBind` — that happens later, at dispatch
37
+ * time, when a host actually EXECUTES a generated control's bind):
38
+ * - `ctx.universe` — required when `kind === 'Frame'` (`getFrameProperty`).
39
+ * - `ctx.runner` — required when `kind === 'Universe'` (`getUniverseProperty`).
40
+ * Every other kind reads its live value directly off `wrapper[field.name]`
41
+ * (LargeObject/QuantumEmitter/etc. wrappers are plain JS objects with their
42
+ * schema fields as properties — no ctx needed).
43
+ * @param {object} [opts]
44
+ * - `opts.kind` — override/supply the schema kind (REQUIRED for Frame/Universe).
45
+ * - `opts.name` / `opts.prefix` — the registry NAME this object is (or will be)
46
+ * registered under; used to build `id = "<name>.<field>"` and
47
+ * `bind.object = "<name>"`. `opts.prefix` wins if both are supplied.
48
+ * Falls back to `wrapper.name` (LargeObject/etc. often carry a `name` field)
49
+ * then to the `'universe'` sentinel for `kind === 'Universe'`; REQUIRED
50
+ * otherwise (FAIL-LOUD if nothing resolves — an unaddressable id is a
51
+ * factory bug, never silently guessed).
52
+ *
53
+ * @returns {{kind:string, objectName:string,
54
+ * controls:object[], // normalized descriptors — ready for
55
+ * // registry.registerControls(objectName, controls)
56
+ * display:object[]}} // NON-control passthrough entries — see below
57
+ *
58
+ * Every entry in `controls` has ALREADY been run through
59
+ * `normalizeControlDescriptor` (fail-fast: a factory bug throws HERE, at
60
+ * generation time, not later at registration time). `registry.registerControls`
61
+ * re-normalizing them again is harmless (idempotent — normalize only validates
62
+ * + copies) and is how the SAME arrow-legality precedent
63
+ * (`lib/frqtl/objects/schema/index.js` importing `lib/shim/frame-data` directly,
64
+ * see the "Arrow legality" note below) plays out for the controls contract too.
65
+ *
66
+ * `display` entries are NOT control descriptors — they never touch
67
+ * `normalizeControlDescriptor` (there is no "display" kind in `ControlsSchema.KINDS`,
68
+ * and inventing one would violate the closed-union discipline that contract
69
+ * documents). Shape: `{id, label, fieldType, unit, value, description}`. A host
70
+ * (Inspector, panel) renders these as read-only info rows — exactly how
71
+ * `renderFieldControl`'s `'readonly'` branch already renders `<span>{val}</span>`
72
+ * today (`lib/features/ide/AuthorView.jsx`) — this factory just makes the
73
+ * SAME decision mechanically, off the schema, for every kind at once.
74
+ *
75
+ * ── Arrow legality (design §2.1 "Arrow condition") ───────────────────────────
76
+ *
77
+ * This file `require`s `lib/shim/controls/index.js` directly (Node) / reads
78
+ * `window.ControlsSchema` (browser) — an `objects → shim/controls` edge.
79
+ * Checked against `scripts/verify-arrows.mjs`: rule [A] (core-lib-skips-shim)
80
+ * scopes ONLY `lib/features` + `lib/components`; `lib/frqtl/objects` is
81
+ * explicitly annotated there as "user-space sugar — NOT a violation." Rule [B]
82
+ * (engine-sniffing) doesn't apply (no `engine===` branches here). `verify:lib-acyclic`
83
+ * likewise scopes `lib/components`/`lib/features`, not `lib/frqtl/objects`. This
84
+ * exactly mirrors the PRE-EXISTING precedent one directory over —
85
+ * `lib/frqtl/objects/schema/index.js` already `require`s
86
+ * `lib/shim/frame-data/index.js` directly (see that file's own "Imports" comment
87
+ * block) for the identical reason: a schema-contract dependency, not an engine
88
+ * dependency. DEPENDENCY INJECTION (accepting `normalizeControlDescriptor` via
89
+ * `ctx`/`opts` instead of importing it) was the escape hatch IF `verify-arrows`
90
+ * rejected the import — it did not, so this file imports directly, same as its
91
+ * schema/index.js precedent. (`make verify` run confirms — see the Bite S2
92
+ * build report.)
93
+ *
94
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
95
+ *
96
+ * Zero physics. This is pure schema-introspection + descriptor-shape glue —
97
+ * generating a control changes NOTHING about the simulation until a host
98
+ * actually calls the generated control's `bind` through `resolveBind` (which
99
+ * itself adds zero physics — see that file's own Layer 2 note). A
100
+ * factory-generated control produces the IDENTICAL effect as hand-writing the
101
+ * same `{kind, id, bind}` literal.
102
+ *
103
+ * ── Scope decisions this bite makes explicit (read before extending) ────────
104
+ *
105
+ * 1. NO onChange synthesis (dispatch constraint) — every live control's write
106
+ * path is `bind:{object, prop}` ONLY. This is SAFE for every numeric field
107
+ * (JSON/WASM round-trips a JS number losslessly either way) and safe for
108
+ * every STRING-valued enum/flags field (see #2) — but it is UNSAFE for two
109
+ * field shapes, which are therefore DISPLAY-ONLY in this bite, not scalar/
110
+ * enum live controls (documented per-branch below, not silently degraded):
111
+ * (a) a `boolean`-typed field with `engineVisible !== false` — its value
112
+ * reaches the engine ABI via `setFrameProperty`'s
113
+ * `JSON.stringify(value)` (or the WASM LargeObject ABI equivalent);
114
+ * Rust's serde expects a JSON `true`/`false` literal for a `bool`
115
+ * field, not a number — a bare 0/1 scalar-fold passthrough (the
116
+ * shape `SphereEmitter.controls`'s scalar rows use, but THAT control
117
+ * uses an explicit `onChange: v => self.setActive(!!v)` coercion,
118
+ * which this factory is forbidden from synthesizing) risks writing
119
+ * the wrong wire type. `engineVisible:false` booleans (LargeObject/
120
+ * backdrop `.visible`, `.drawAsCircle`) are UNAFFECTED — their write
121
+ * path is a bare `lo[prop] = value` JS assignment (confirmed against
122
+ * every consumer: `p5-provider.js`/`FrqtlApp.hitTest` all
123
+ * check `=== false` or truthiness, never `=== true`) — numeric 0/1
124
+ * is safe there, so those DO become live scalar controls.
125
+ * (b) an `enum`/`enum-set` field whose `constraint.values` is NOT an
126
+ * IDENTITY map (key === value for every key) — e.g.
127
+ * `SlitScreen.detectAtSlits: {NONE:0, DETECT_RANDOM_XY:1, DETECT_MARK:2}`,
128
+ * the ONE such field in the whole schema (verified exhaustively at
129
+ * build time). A bind-driven enum control's `value`/`values` must be
130
+ * STRINGS (`ControlsSchema._validateEnum`); writing the string KEY
131
+ * back through a bare bind would write `'NONE'` into a field the
132
+ * engine expects as the NUMBER `0`. Needs either an onChange
133
+ * translator (forbidden here) or a `resolveBind`-side value-mapping
134
+ * extension — an architect decision, not a factory guess.
135
+ * Both are named, tested exceptions — NOT a silent drop. See
136
+ * `_booleanHandler`/`_enumHandler`/`_flagsHandler`/`_writableHandler` below.
137
+ * 2. `rect-array` fields (`QuantumEmitter.source_rects`) become N DISPLAY
138
+ * entries, one per array item — NOT N live vector controls. The `bind`
139
+ * contract (`lib/shim/controls` `BIND_KEYS = ['object','prop']`) has no
140
+ * array-index addressing; a correct per-entry live write needs either
141
+ * `bind.index` (a shim-contract extension) or an onChange closure
142
+ * (forbidden). Flagged per-entry in the `description` field, and in the
143
+ * Bite S2 build report for S3/S4/architect follow-up.
144
+ * 3. `QuantumEmitter`/`PointSource`'s singular `rect` field has NO live getter
145
+ * on the wrapper today (a PRE-EXISTING gap — `AuthorView.jsx`'s current
146
+ * Inspector reads `obj['rect']` too and gets the identical `undefined`).
147
+ * `FIELD_VALUE_OVERRIDES` derives a best-effort INITIAL value
148
+ * (`source_rects[0]` / `[x-d/2, y-d/2, d, d]`) so the generated `vector`
149
+ * control still has something to show; when even that isn't derivable, the
150
+ * field is a DEFINED SKIP (no control generated at all) — never a
151
+ * fabricated read (mirrors `controlBind.js`'s own "defined skip" discipline).
152
+ * 4. The "coupled emitter move" op (design §2.2's coder finding — dragging an
153
+ * emitter is TWO writes today, LO rect + emitter sub-rect) has NO factory
154
+ * support yet — TODO for S4: a composite-move op or two coupled `vector`
155
+ * controls sharing one drag gesture. This factory generates each field's
156
+ * control independently; it does not know about cross-field coupling.
157
+ * 5. Behavior-lowering fields (`collision_type`, `actions` — compiled to an
158
+ * op-program at REGISTRATION by `actionCompiler.js`, `engineVisible:false`)
159
+ * DO get live `enum`/`flags` controls here (their values are identity-mapped
160
+ * strings, safe per #1's rule) — but per design §2.3.5, a live edit only
161
+ * mutates the RENDER-layer JS wrapper mirror; it does NOT recompile/repush
162
+ * the running op-program. That gap is CEO call #7's `recompile` write class,
163
+ * explicitly deferred to S4 — this factory does not attempt it.
164
+ * 6. **Bind-executable-kind gate (S3, architect ruling `ruling_studio_controls_
165
+ * s2_factory.md` watch-out #1 — MANDATORY, closes an S2-documented KNOWN
166
+ * GAP).** `controlBind.js`'s `resolveBind` only EXECUTES a bind for a
167
+ * registry-resolved kind in `{Frame, LargeObject, backdrop}`, plus the
168
+ * `bind.object === 'universe'` sentinel (this factory's `kind === 'Universe'`)
169
+ * — it FAILS LOUD for every other kind (`QuantumEmitter`, `Box`, `OvenBox`,
170
+ * `DetectorWall`, `SlitScreen`, `CircuitGate`, `CircuitProbe`, `PointSource`,
171
+ * `Tube`, `EgptFractal`, …) because there is no real
172
+ * accessor path from a bare `{object,prop}` pair to that object's actual
173
+ * state — a `QuantumEmitter`'s real state lives in `emState`/`_specTemplate`
174
+ * (mutated only via `setVelocity`/`setBurstSize`/…), and a composite like
175
+ * `Box` exposes CONVENIENCE fields (`width`, `height`, …) that require
176
+ * rebuilding multiple walls, not a bare property set. A host (Studio's
177
+ * Inspector, the page-level panel) that registers this factory's output and
178
+ * resolves binds against it MUST NEVER be handed a control descriptor whose
179
+ * bind is GUARANTEED to throw — an object of a non-bind-executable kind
180
+ * sitting in the scene must not break the whole panel just by existing.
181
+ * So: for ANY kind outside `BIND_EXECUTABLE_KINDS`, every field is demoted
182
+ * to a DISPLAY entry — never a live control — regardless of what its
183
+ * per-field `TYPE_DISPATCH` handler would otherwise produce. This is a
184
+ * per-KIND gate (applied once, after the per-field handler runs), not a
185
+ * per-field one — it sits at the SAME "safe live-write path, or a
186
+ * documented display passthrough" grain as the existing boolean/enum
187
+ * per-field demotions (scope notes #1(a)/#1(b) above), just scoped to the
188
+ * whole object instead of one field. See `_demoteToDisplay` below.
189
+ *
190
+ */
191
+
192
+ import { ObjectSchema as _ObjectSchemaNode } from '../schema/index.js';
193
+ import * as _ControlsSchemaNode from '../../../shim/controls/index.js';
194
+
195
+
196
+ // ── Nullable-override sentinel (W0-D, CEO 2026-07-23) ───────────────────────────
197
+ // A LIVE per-leaf render-intent OVERRIDE field (SphereEmitter.renderShape,
198
+ // .renderColor) defaults to NULL — "no key, byte-identical, scheme-driven default"
199
+ // (the SU1 guarantee: a non-null default would stamp every leaf and move a golden).
200
+ // Its control must still be EDITABLE (the CEO's "renderShape/Color is not changeable
201
+ // in the Inspector" defect), so a null-valued live override renders a control seeded
202
+ // with this reserved sentinel meaning "unset / scheme default". The consuming
203
+ // Inspector maps the sentinel BACK to null on write (AuthorView commitField/
204
+ // commitEmitterField), so the descriptor's own vocab (constraint.values) stays the
205
+ // ONE owner and the null-byte-identity is preserved. Exposed on the exported
206
+ // namespace so the host reads ONE definition (never a hand-copied magic string).
207
+ var NULL_ENUM_SENTINEL = '(default)';
208
+
209
+ // ── Imports (lazy-guarded, mirrors controls/registry.js's own precedent) ────
210
+
211
+
212
+
213
+
214
+
215
+
216
+ // ── Default-bounds policy (dispatch condition: "never emit an unbounded ──────
217
+ // slider and never invent silent magic numbers without documenting them") ─
218
+ //
219
+ // Precedence per numeric field: `widgetConfig.{min,max,step}` (explicit,
220
+ // deliberate author override — the pre-existing `OvenBox.temperature`/
221
+ // `Universe.wavelengthScale` pattern) > `constraint.{min,max}` (schema-
222
+ // declared bound) > the DEFAULT below. Only ~40% of number fields declare a
223
+ // `max` at all (verified at build time) — most physics-pixel/position/velocity
224
+ // fields are genuinely open-ended, so an unbounded-slider default is not
225
+ // acceptable (dispatch condition) and a fixed universal constant would be
226
+ // meaningless across wildly different field magnitudes (a wall's `left` vs. a
227
+ // `windowTicks` count). The chosen default SCALES WITH THE FIELD'S OWN CURRENT
228
+ // VALUE, exactly mirroring the ONE pre-existing precedent for an un-annotated
229
+ // bounded slider in this codebase — `SphereEmitter.controls`'s
230
+ // `burstSize` row (`max: 200`, lib/frqtl/objects/setup/setupSphereEmitter3D.js).
231
+ // DEFAULT_MAX_FLOOR=40 is that SAME constant, reused for consistency, not
232
+ // invented fresh.
233
+ var DEFAULT_MAX_FLOOR = 40;
234
+
235
+ function _isFiniteNum(v) { return typeof v === 'number' && isFinite(v); }
236
+
237
+ /**
238
+ * Resolve {min, max, step} for a scalar numeric field, per the precedence above.
239
+ * `min` defaults to a SYMMETRIC `-max` when `constraint.min` is not declared
240
+ * (covers signed fields — velocities, positions that may go negative — the
241
+ * majority of the schema's `min`-less number fields, verified at build time).
242
+ */
243
+ function _resolveBounds(field, value) {
244
+ var c = field.constraint || {};
245
+ var wc = field.widgetConfig || {};
246
+ var base = _isFiniteNum(value) ? Math.abs(value) : 0;
247
+ var max = (typeof wc.max === 'number') ? wc.max
248
+ : (typeof c.max === 'number') ? c.max
249
+ : Math.max(base * 3, DEFAULT_MAX_FLOOR);
250
+ var min = (typeof wc.min === 'number') ? wc.min
251
+ : (typeof c.min === 'number') ? c.min
252
+ : -max;
253
+ var step = (typeof wc.step === 'number') ? wc.step
254
+ : (c.integer ? 1 : Math.max(max / 100, 0.01));
255
+ return { min: min, max: max, step: step };
256
+ }
257
+
258
+ function _label(field) {
259
+ // An optional display `label` (Bite 5b) overrides the field NAME for the
260
+ // control's shown label WITHOUT renaming the schema field (the codegen/capture
261
+ // key stays `field.name`). e.g. leafDimension → "Emit Dimension", the global
262
+ // shape capability → "Default Shape". Absent → the name, as before (byte-identical).
263
+ var base = (typeof field.label === 'string' && field.label) ? field.label : field.name;
264
+ return field.unit ? (base + ' (' + field.unit + ')') : base;
265
+ }
266
+
267
+ function _scalarFmt(field) {
268
+ var integer = !!(field.constraint && field.constraint.integer);
269
+ return function (v) { return integer ? String(Math.round(v)) : Number(v).toFixed(2); };
270
+ }
271
+
272
+ // ── Live-value reading (kind-dependent — see file docstring @param ctx) ─────
273
+
274
+ // FIELD_VALUE_OVERRIDES — see scope note #3 above.
275
+ var FIELD_VALUE_OVERRIDES = {
276
+ QuantumEmitter: {
277
+ rect: function (w) {
278
+ return (w && Array.isArray(w.source_rects) && w.source_rects[0])
279
+ ? w.source_rects[0].slice()
280
+ : undefined;
281
+ }
282
+ },
283
+ PointSource: {
284
+ rect: function (w) {
285
+ if (!w || !_isFiniteNum(w.x) || !_isFiniteNum(w.y) || !_isFiniteNum(w.diameter)) { return undefined; }
286
+ var d = w.diameter;
287
+ return [w.x - d / 2, w.y - d / 2, d, d];
288
+ }
289
+ },
290
+ // SphereEmitter (Bite 4b): `rect` is the WRITE-ONLY reposition adapter — its
291
+ // direct getter is undefined (so captureProps skips it; `center` is the captured
292
+ // position authority). Seed the Inspector's live rect vector from `center` so the
293
+ // field still shows/edits a position (edit → updateObjectProperty → setCenter).
294
+ SphereEmitter: {
295
+ rect: function (w) {
296
+ var c = w && w.center;
297
+ if (!Array.isArray(c) || !_isFiniteNum(c[0]) || !_isFiniteNum(c[1])) { return undefined; }
298
+ var D = 24; // marker diameter (matches the ObjectFactory SphereEmitter builder)
299
+ return [c[0] - D / 2, c[1] - D / 2, D, D];
300
+ }
301
+ }
302
+ };
303
+
304
+ function _readValue(env, propName) {
305
+ var kind = env.kind, wrapper = env.wrapper, ctx = env.ctx;
306
+ if (kind === 'Frame') {
307
+ if (!ctx.universe || typeof ctx.universe.getFrameProperty !== 'function') {
308
+ throw new Error('[makeControlsForObject] kind="Frame" requires ctx.universe ' +
309
+ '(getFrameProperty) to read live field values. (FAIL-LOUD.)');
310
+ }
311
+ try { return ctx.universe.getFrameProperty(wrapper, propName); }
312
+ catch (e) { return undefined; } // defined skip — mirrors controlBind.js's own discipline
313
+ }
314
+ if (kind === 'Universe') {
315
+ if (!ctx.runner || typeof ctx.runner.getUniverseProperty !== 'function') {
316
+ throw new Error('[makeControlsForObject] kind="Universe" requires ctx.runner ' +
317
+ '(getUniverseProperty) to read live field values. (FAIL-LOUD.)');
318
+ }
319
+ try { return ctx.runner.getUniverseProperty(propName); }
320
+ catch (e) { return undefined; }
321
+ }
322
+ var overrides = FIELD_VALUE_OVERRIDES[kind];
323
+ if (overrides && typeof overrides[propName] === 'function') { return overrides[propName](wrapper); }
324
+ return (wrapper && typeof wrapper === 'object') ? wrapper[propName] : undefined;
325
+ }
326
+
327
+ // ── Display-passthrough entries (readonly/forbidden/string/object/deferred) ──
328
+
329
+ function _displayEntry(field, env) {
330
+ var value;
331
+ try { value = _readValue(env, field.name); } catch (e) { value = undefined; }
332
+ return {
333
+ kind: 'display',
334
+ entries: [{
335
+ id: env.objectName + '.' + field.name,
336
+ label: _label(field),
337
+ fieldType: field.type,
338
+ unit: field.unit || null,
339
+ value: value,
340
+ description: field.description || ''
341
+ }]
342
+ };
343
+ }
344
+
345
+ // ── Per-field-type handlers ──────────────────────────────────────────────────
346
+ // TYPE_DISPATCH is the EXHAUSTIVE map — every field.type value that can appear
347
+ // in ObjectSchema MUST have an entry here (even if that entry's action is a
348
+ // documented display/skip). An unrecognized type throws (see the main loop) —
349
+ // this is the fail-loud gate the conformance suite drives its exhaustiveness
350
+ // check off of (never a hardcoded type list re-derived in the test file).
351
+
352
+ function _numberHandler(field, env) {
353
+ var value = _readValue(env, field.name);
354
+ if (!_isFiniteNum(value)) { return null; } // defined skip — no live value to seed the slider
355
+ var b = _resolveBounds(field, value);
356
+ return {
357
+ kind: 'control',
358
+ descriptor: {
359
+ kind: 'scalar', id: env.objectName + '.' + field.name, label: _label(field),
360
+ min: b.min, max: b.max, step: b.step, value: value,
361
+ fmt: _scalarFmt(field),
362
+ bind: { object: env.objectName, prop: field.name }
363
+ }
364
+ };
365
+ }
366
+
367
+ /** See scope note #1(a) above for the engineVisible:true branch's rationale. */
368
+ function _booleanHandler(field, env) {
369
+ if (field.engineVisible !== false) { return _displayEntry(field, env); }
370
+ var value = _readValue(env, field.name);
371
+ if (value === undefined || value === null) { return null; }
372
+ return {
373
+ kind: 'control',
374
+ descriptor: {
375
+ kind: 'scalar', id: env.objectName + '.' + field.name, label: _label(field),
376
+ min: 0, max: 1, step: 1, value: value ? 1 : 0,
377
+ fmt: function (v) { return v ? 'true' : 'false'; },
378
+ bind: { object: env.objectName, prop: field.name }
379
+ }
380
+ };
381
+ }
382
+
383
+ /** Every key must equal its own value (see scope note #1(b)). */
384
+ function _isIdentityValuesMap(valuesObj) {
385
+ var keys = Object.keys(valuesObj);
386
+ for (var i = 0; i < keys.length; i++) {
387
+ if (valuesObj[keys[i]] !== keys[i]) { return false; }
388
+ }
389
+ return true;
390
+ }
391
+
392
+ function _enumHandler(field, env) {
393
+ var valuesObj = (field.constraint && field.constraint.values) || {};
394
+ if (!_isIdentityValuesMap(valuesObj)) { return _displayEntry(field, env); }
395
+ var keys = Object.keys(valuesObj);
396
+ var value = _readValue(env, field.name);
397
+ if (typeof value !== 'string' || keys.indexOf(value) === -1) {
398
+ // W0-D (CEO 2026-07-23): a LIVE enum whose value is null/unset — the
399
+ // SphereEmitter.renderShape override, kept null for byte-identity (SU1) — still
400
+ // renders an EDITABLE control, seeded with the '(default)' sentinel option
401
+ // representing "unset / scheme default". Picking a real key writes the string
402
+ // (the existing updateObjectProperty → applyProp → setRenderShape path); the
403
+ // Inspector maps the sentinel back to null. A NON-live null enum keeps the
404
+ // pre-existing skip (byte-identical — the sentinel only serves nullable LIVE
405
+ // overrides, never a required enum like collision_type which is always valued).
406
+ if (field.live !== true) { return null; }
407
+ return {
408
+ kind: 'control',
409
+ descriptor: {
410
+ kind: 'enum', id: env.objectName + '.' + field.name, label: _label(field),
411
+ values: [NULL_ENUM_SENTINEL].concat(keys), value: NULL_ENUM_SENTINEL,
412
+ fmt: function (v) { return v; },
413
+ bind: { object: env.objectName, prop: field.name }
414
+ }
415
+ };
416
+ }
417
+ return {
418
+ kind: 'control',
419
+ descriptor: {
420
+ kind: 'enum', id: env.objectName + '.' + field.name, label: _label(field),
421
+ values: keys, value: value,
422
+ fmt: function (v) { return v; },
423
+ bind: { object: env.objectName, prop: field.name }
424
+ }
425
+ };
426
+ }
427
+
428
+ function _flagsHandler(field, env) {
429
+ var valuesObj = (field.constraint && field.constraint.values) || {};
430
+ if (!_isIdentityValuesMap(valuesObj)) { return _displayEntry(field, env); }
431
+ var keys = Object.keys(valuesObj);
432
+ var value = _readValue(env, field.name);
433
+ if (!Array.isArray(value)) { return null; }
434
+ return {
435
+ kind: 'control',
436
+ descriptor: {
437
+ kind: 'flags', id: env.objectName + '.' + field.name, label: _label(field),
438
+ values: keys, value: value.slice(),
439
+ fmt: function (v) { return Array.isArray(v) ? v.join(', ') : String(v); },
440
+ bind: { object: env.objectName, prop: field.name }
441
+ }
442
+ };
443
+ }
444
+
445
+ // The "unset / scheme default" swatch a null-valued LIVE rgb override seeds its
446
+ // editable control with (W0-D2). White = a neutral "no tint" placeholder; the
447
+ // override is genuinely UNSET (scheme-driven fullness→ROYGBIV) until the user picks
448
+ // a color, at which point setRenderColor stamps the chosen [r,g,b]. (A true
449
+ // "return to unset" gesture needs a companion clear affordance — out of the minimal
450
+ // scope; see the W0-D2 note in the deliverable.)
451
+ var NULL_RGB_DEFAULT_SWATCH = [255, 255, 255];
452
+
453
+ /**
454
+ * Is `value` acceptable for `field`'s DECLARED type? The predicate is the schema owner's
455
+ * write-time normalizer (`ObjectSchema.normalizeValue`, throw-or-apply) run as a probe —
456
+ * never a second hand-written per-type test here. A read-side seeding decision and a
457
+ * write-side gate must agree by construction, so there is exactly one implementation.
458
+ */
459
+ function _isValidForField(field, value, kind) {
460
+ // `null`/`undefined` is a legal WRITE everywhere ("unset" — the normalizer passes it
461
+ // through by contract), but it can never SEED a control: there is no live value to
462
+ // show. So the READ-side probe treats it as not-usable and the caller falls to its
463
+ // own unset branch.
464
+ if (value === null || value === undefined) { return false; }
465
+ try { _ObjectSchemaNode.normalizeValue(field, value, kind); return true; }
466
+ catch (e) { return false; }
467
+ }
468
+
469
+ function _rgbHandler(field, env) {
470
+ var value = _readValue(env, field.name);
471
+ var isValid = _isValidForField(field, value, env.kind);
472
+ if (!isValid) {
473
+ // W0-D2 (CEO 2026-07-23): a LIVE rgb override whose value is null/unset — the
474
+ // SphereEmitter.renderColor override, kept null for byte-identity (SU1) — still
475
+ // renders an EDITABLE color control seeded with the neutral default swatch, so
476
+ // the field is not a dead "–" in the Inspector. Picking a color writes [r,g,b]
477
+ // (updateObjectProperty → applyProp → setRenderColor). A NON-live null rgb keeps
478
+ // the pre-existing skip (byte-identical — same discipline as the enum branch).
479
+ if (field.live !== true) { return null; }
480
+ value = NULL_RGB_DEFAULT_SWATCH;
481
+ }
482
+ return {
483
+ kind: 'control',
484
+ descriptor: {
485
+ kind: 'vector', id: env.objectName + '.' + field.name, label: _label(field),
486
+ value: value.slice(), min: [0, 0, 0], max: [255, 255, 255], step: [1, 1, 1],
487
+ widgetHint: 'color',
488
+ fmt: function (v) { return 'rgb(' + v.map(function (n) { return Math.round(n); }).join(',') + ')'; },
489
+ bind: { object: env.objectName, prop: field.name }
490
+ }
491
+ };
492
+ }
493
+
494
+ // Fixed component-name ORDER (LO z/depth wire contract — z is THIRD, not last:
495
+ // [left, top, z, w, h, d]). Position components (left/top/z) may be signed;
496
+ // size components (w/h/d) are non-negative — this drives the per-component
497
+ // min/max policy below.
498
+ var RECT4_KEYS = ['left', 'top', 'w', 'h'];
499
+ var RECT6_KEYS = ['left', 'top', 'z', 'w', 'h', 'd'];
500
+
501
+ function _rectComponentBounds(keys, values) {
502
+ var mins = [], maxs = [], steps = [];
503
+ for (var i = 0; i < keys.length; i++) {
504
+ var isSize = (keys[i] === 'w' || keys[i] === 'h' || keys[i] === 'd');
505
+ var v = values[i];
506
+ var m = Math.max(Math.abs(_isFiniteNum(v) ? v : 0) * 3, DEFAULT_MAX_FLOOR);
507
+ maxs.push(m);
508
+ mins.push(isSize ? 0 : -m);
509
+ steps.push(1);
510
+ }
511
+ return { min: mins, max: maxs, step: steps };
512
+ }
513
+
514
+ /**
515
+ * `rect` → `vector`. Tuple length (4 vs 6) is derived from the LIVE current
516
+ * value, NOT a static schema flag — a LargeObject's `.rect` is a plain object
517
+ * `{left,top,w,h}` OR `{left,top,z,w,h,d}` depending on what was actually
518
+ * passed at CONSTRUCTION time (`LargeObject.js`) — genuinely per-INSTANCE, not
519
+ * per-kind. This matches the `vector` kind's own "length is free" contract
520
+ * (`lib/shim/controls/index.js` doc comment).
521
+ */
522
+ function _rectHandler(field, env) {
523
+ var raw = _readValue(env, field.name);
524
+ var arr = null, keys = RECT4_KEYS;
525
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
526
+ var hasZD = ('z' in raw) && ('d' in raw);
527
+ keys = hasZD ? RECT6_KEYS : RECT4_KEYS;
528
+ arr = keys.map(function (k) { return raw[k]; });
529
+ } else if (Array.isArray(raw)) {
530
+ keys = (raw.length === 6) ? RECT6_KEYS : RECT4_KEYS;
531
+ arr = raw.slice();
532
+ }
533
+ if (!arr || arr.length === 0 || !arr.every(_isFiniteNum)) { return null; } // defined skip
534
+ var b = _rectComponentBounds(keys, arr);
535
+ return {
536
+ kind: 'control',
537
+ descriptor: {
538
+ kind: 'vector', id: env.objectName + '.' + field.name, label: _label(field),
539
+ value: arr, min: b.min, max: b.max, step: b.step,
540
+ fmt: function (v) { return '[' + v.map(function (n) { return Math.round(n); }).join(', ') + ']'; },
541
+ bind: { object: env.objectName, prop: field.name }
542
+ }
543
+ };
544
+ }
545
+
546
+ /** See scope note #2 above — DISPLAY entries, one per array item, never a live control. */
547
+ function _rectArrayHandler(field, env) {
548
+ var arr = _readValue(env, field.name);
549
+ if (!Array.isArray(arr) || arr.length === 0) { return null; }
550
+ var entries = [];
551
+ for (var i = 0; i < arr.length; i++) {
552
+ var r = arr[i];
553
+ if (!Array.isArray(r)) { continue; }
554
+ entries.push({
555
+ id: env.objectName + '.' + field.name + '[' + i + ']',
556
+ label: _label(field) + ' [' + i + ']',
557
+ fieldType: 'rect-array-entry',
558
+ unit: field.unit || null,
559
+ value: r.slice(),
560
+ description: (field.description || '') +
561
+ ' (DISPLAY ONLY — the bind contract {object,prop} has no array-index ' +
562
+ 'addressing; a live per-entry write needs either bind.index, a shim-contract ' +
563
+ 'extension, or an onChange closure, forbidden this bite. See ' +
564
+ 'makeControlsForObject.js\'s module docstring, scope note #2.)'
565
+ });
566
+ }
567
+ return entries.length ? { kind: 'display', entries: entries } : null;
568
+ }
569
+
570
+ function _readonlyHandler(field, env) { return _displayEntry(field, env); }
571
+ function _forbiddenHandler(field, env) { return _displayEntry(field, env); }
572
+ function _stringHandler(field, env) { return _displayEntry(field, env); }
573
+ function _objectHandler(field, env) { return _displayEntry(field, env); }
574
+
575
+ /**
576
+ * `writable` (Frame kind ONLY) — this schema type is an ACCESS-LEVEL tag, not
577
+ * a value-type tag (unlike 'number'/'boolean'/'string' on every other kind,
578
+ * which ARE value-typed). x/y/z/vx/vy/vz/mass/speed_prob/local_time are
579
+ * numbers; is_alive/is_stationary/wrap are booleans; charge is a string;
580
+ * data is an object — all declared `type:'writable'`. This handler infers the
581
+ * actual JS value type from the LIVE read and re-dispatches accordingly.
582
+ */
583
+ function _writableHandler(field, env) {
584
+ var value = _readValue(env, field.name);
585
+ if (value === undefined) { return null; } // defined skip
586
+ if (typeof value === 'number') { return _numberHandler(field, env); }
587
+ if (typeof value === 'boolean') {
588
+ // Frame's writable booleans are ALWAYS engineVisible:true and reach the
589
+ // engine ABI via setFrameProperty's JSON.stringify — same coercion risk
590
+ // as the dedicated 'boolean' handler's engineVisible:true branch (scope
591
+ // note #1(a)). DISPLAY ONLY.
592
+ return _displayEntry(field, env);
593
+ }
594
+ // string (charge — no declared candidate-values set to build a safe 'enum'
595
+ // from) or object (the data bubble — an arbitrary bag) — neither has a
596
+ // live-control shape without inventing behavior the schema doesn't declare.
597
+ return _displayEntry(field, env);
598
+ }
599
+
600
+ var TYPE_DISPATCH = {
601
+ number: _numberHandler,
602
+ boolean: _booleanHandler,
603
+ enum: _enumHandler,
604
+ 'enum-set': _flagsHandler,
605
+ rgb: _rgbHandler,
606
+ rect: _rectHandler,
607
+ 'rect-array': _rectArrayHandler,
608
+ readonly: _readonlyHandler,
609
+ forbidden: _forbiddenHandler,
610
+ string: _stringHandler,
611
+ object: _objectHandler,
612
+ writable: _writableHandler
613
+ };
614
+
615
+ // ── Bind-executable-kind gate (see module docstring scope note #6) ──────────
616
+ //
617
+ // The EXACT set of kinds `controlBind.js`'s `resolveBind` can execute a bind
618
+ // against: the three registry-resolved kinds it dispatches on
619
+ // (`Frame`, `LargeObject`, `backdrop`) plus `Universe` (this factory's own
620
+ // `kind === 'Universe'` — resolved through the `bind.object === 'universe'`
621
+ // reserved sentinel, never a registry lookup). Kept as a literal, minimal
622
+ // mirror of `resolveBind`'s own three-way dispatch + the sentinel branch —
623
+ // NOT re-derived from `ObjectSchema.kinds()` (that list is open-ended and
624
+ // growing; this one is closed by what `resolveBind` can actually execute).
625
+ var BIND_EXECUTABLE_KINDS = Object.freeze(['Frame', 'LargeObject', 'backdrop', 'Universe']);
626
+
627
+ /**
628
+ * Demote an already-generated 'control' result to a 'display' result — used
629
+ * ONLY when `env.kind` is outside `BIND_EXECUTABLE_KINDS` (scope note #6).
630
+ * Reuses the descriptor's ALREADY-COMPUTED `id`/`label`/`value` (no second
631
+ * live-value read — the handler already did that work; demoting never
632
+ * re-derives it) so the demoted row shows the exact same value a live control
633
+ * would have shown.
634
+ */
635
+ function _demoteToDisplay(field, env, result) {
636
+ var d = result.descriptor;
637
+ return {
638
+ kind: 'display',
639
+ entries: [{
640
+ id: d.id,
641
+ label: d.label,
642
+ fieldType: field.type,
643
+ unit: field.unit || null,
644
+ value: d.value,
645
+ description: (field.description || '') +
646
+ ' (DISPLAY ONLY — kind "' + env.kind + '" is not in controlBind.js\'s bind-execution ' +
647
+ 'allowlist {Frame, LargeObject, backdrop, Universe}; resolveBind FAILS LOUD for any ' +
648
+ 'other kind rather than silently no-op through an accessor path that would not reach ' +
649
+ 'this object\'s real state. Demoted per the S3 architect gate — see ' +
650
+ '.claude/agent-memory/frqtl-architect/ruling_studio_controls_s2_factory.md watch-out #1.)'
651
+ }]
652
+ };
653
+ }
654
+
655
+ // ── Conditional visibility (Collision LEXICON v2, architect §1b) ────────────────
656
+ //
657
+ // ONE declarative `visibleWhen` predicate + ONE evaluator — the general substrate for a
658
+ // field that shows only when a SIBLING field holds a value (bounceDirection only for a
659
+ // bounce wall; angle/decoherence only for a random bounce; markTag only when the mark
660
+ // hook is set). Evaluated LIVE against the current captured props (NOT baked at generation
661
+ // time), so picking 'bounce' reveals bounceDirection immediately. Orthogonal to
662
+ // `internal:true` (always-hide): `internal` is a never-show, `visibleWhen` is a
663
+ // conditional-show — the two are never folded. Shape:
664
+ // { field:'collisionBehavior', equals:'bounce' } — sibling equals a value
665
+ // { field:'hooks', includes:'mark' } — sibling enum-set contains a member
666
+ // [ {…}, {…} ] — ARRAY = AND (the nested case)
667
+ function evalVisibleWhen(field, props) {
668
+ var vw = field && field.visibleWhen;
669
+ if (!vw) { return true; }
670
+ var conds = Array.isArray(vw) ? vw : [vw];
671
+ for (var i = 0; i < conds.length; i++) {
672
+ var c = conds[i];
673
+ var v = (props && typeof props === 'object') ? props[c.field] : undefined;
674
+ if (c.equals !== undefined) { if (v !== c.equals) { return false; } }
675
+ else if (c.includes !== undefined) { if (!Array.isArray(v) || v.indexOf(c.includes) === -1) { return false; } }
676
+ // `present` (emission-gate contract §2): the field shows iff the referenced
677
+ // field is set (non-null) — e.g. gateCounter only once emissionCap is declared.
678
+ else if (c.present !== undefined) { if ((v != null) !== !!c.present) { return false; } }
679
+ // `truthy` (R2AX two-axis mirror, 2026-07-26): the field shows iff the referenced
680
+ // field is truthy — e.g. pitchDeg only on a 3D wall (depth > 0; a 2D wall's derived
681
+ // depth is 0, which `present` cannot distinguish). The ONE evaluator, extended
682
+ // additively like `present` before it.
683
+ else if (c.truthy !== undefined) { if (!!v !== !!c.truthy) { return false; } }
684
+ }
685
+ return true;
686
+ }
687
+
688
+ // ── The factory ───────────────────────────────────────────────────────────────
689
+
690
+ /**
691
+ * @see file docstring for the full contract.
692
+ */
693
+ function makeControlsForObject(wrapper, ctx, opts) {
694
+ ctx = ctx || {};
695
+ opts = opts || {};
696
+
697
+ var ObjectSchemaRef = _ObjectSchemaNode;
698
+ var normalizeControlDescriptor = _ControlsSchemaNode.normalizeControlDescriptor;
699
+
700
+ var kind = opts.kind ||
701
+ (wrapper && typeof wrapper === 'object' && wrapper._schemaKind) ||
702
+ null;
703
+ if (!kind) {
704
+ throw new Error('[makeControlsForObject] cannot determine object kind — pass opts.kind, or a ' +
705
+ 'wrapper object with a "_schemaKind" property (every LargeObject/QuantumEmitter/etc. sets ' +
706
+ 'this at construction). A bare Frame quantum_id (a number) or the Universe singleton has no ' +
707
+ '"_schemaKind" — opts.kind is REQUIRED for those. (FAIL-LOUD.)');
708
+ }
709
+ var descriptor = ObjectSchemaRef.get(kind);
710
+ if (!descriptor) {
711
+ throw new Error('[makeControlsForObject] ObjectSchema has no descriptor for kind "' + kind +
712
+ '". (FAIL-LOUD.)');
713
+ }
714
+
715
+ var objectName = opts.prefix || opts.name ||
716
+ (kind === 'Universe' ? 'universe' : null) ||
717
+ (wrapper && typeof wrapper === 'object' ? wrapper.name : null) ||
718
+ null;
719
+ if (!objectName) {
720
+ throw new Error('[makeControlsForObject] cannot determine the registry object name (used to ' +
721
+ 'prefix every generated control id "<objectName>.<field>" and to build bind.object) — pass ' +
722
+ 'opts.name (or opts.prefix). (FAIL-LOUD.)');
723
+ }
724
+
725
+ var env = { kind: kind, wrapper: wrapper, ctx: ctx, opts: opts, objectName: objectName };
726
+
727
+ var controls = [];
728
+ var display = [];
729
+
730
+ for (var i = 0; i < descriptor.fields.length; i++) {
731
+ var field = descriptor.fields[i];
732
+ // ── Designer-internal fields (W1 tweak T3) ───────────────────────────
733
+ // `internal:true` (the ONE authority, a descriptor flag — NOT a hardcoded
734
+ // field-name list) marks an ENGINE INTERNAL that must never surface as a
735
+ // designer control/display (e.g. LargeObject.program's raw op-program AST,
736
+ // the derived op-program-scope geometry accessors). Skipped entirely here —
737
+ // no control, no display row. Still engineVisible + captured for codegen.
738
+ if (field.internal === true) { continue; }
739
+ var handler = TYPE_DISPATCH[field.type];
740
+ if (!handler) {
741
+ throw new Error('[makeControlsForObject] unmapped field.type "' + field.type + '" on kind "' +
742
+ kind + '" field "' + field.name + '" — every ObjectSchema field.type must have an ' +
743
+ 'explicit TYPE_DISPATCH entry in makeControlsForObject.js (even a documented display/skip). ' +
744
+ '(FAIL-LOUD — never a silent drop.)');
745
+ }
746
+ // ── Explicit per-field LIVE attribute (Bite 4b) ──────────────────────
747
+ // `live:false` → a read-only DISPLAY row regardless of type (boot-level /
748
+ // reload-to-change / derived fields — SphereEmitter's is2D, fundamentalDimension,
749
+ // emissionBox, …). captureProps STILL captures the value; the Inspector just
750
+ // renders it read-only. A field with NO `live` attribute keeps the pre-existing
751
+ // behavior EXACTLY (undefined !== false / !== true → both new branches no-op),
752
+ // so every existing kind is byte-identical.
753
+ if (field.live === false) {
754
+ display = display.concat(_displayEntry(field, env).entries);
755
+ continue;
756
+ }
757
+ var result = handler(field, env);
758
+ if (!result) { continue; } // documented, deliberate skip (e.g. no live value to seed a control)
759
+ // `live:true` EXEMPTS from the bind-executable demotion: the consuming
760
+ // Inspector / cross-context registry writes this field via
761
+ // updateObjectProperty(wrapper, prop, v) (mechanism A → the kind's own
762
+ // updateObjectProperty branch — e.g. SphereEmitter → control setter), NEVER
763
+ // resolveBind, so the bind-executable allowlist does not apply. Without an
764
+ // explicit `live:true`, the pre-existing demotion stands (S3 gate, scope
765
+ // note #6) — this object's kind has no working resolveBind path.
766
+ if (result.kind === 'control' && field.live !== true && BIND_EXECUTABLE_KINDS.indexOf(kind) === -1) {
767
+ result = _demoteToDisplay(field, env, result);
768
+ }
769
+ // Pass `visibleWhen` THROUGH onto the emitted entry (architect §1b) — a live,
770
+ // sibling-dependent RENDER concern the Inspector evaluates at render time; NOT a
771
+ // generation-time skip (unlike internal:true). Attached AFTER normalization so the
772
+ // control descriptor's own contract is unchanged (normalize never sees it).
773
+ if (result.kind === 'display') {
774
+ if (field.visibleWhen) { result.entries.forEach(function (e) { e.visibleWhen = field.visibleWhen; }); }
775
+ display = display.concat(result.entries);
776
+ } else if (result.kind === 'control') {
777
+ var norm = normalizeControlDescriptor(result.descriptor);
778
+ if (field.visibleWhen) { norm.visibleWhen = field.visibleWhen; }
779
+ controls.push(norm);
780
+ }
781
+ }
782
+
783
+ return { kind: kind, objectName: objectName, controls: controls, display: display };
784
+ }
785
+
786
+ // ── Export ────────────────────────────────────────────────────────────────────
787
+
788
+ var MakeControlsForObject = {
789
+ makeControlsForObject: makeControlsForObject,
790
+ // The ONE conditional-visibility evaluator (Collision LEXICON v2, architect §1b) —
791
+ // the AuthorView Inspector filter calls THIS beside its existing internal-field drop,
792
+ // never a hand-rolled per-field branch.
793
+ evalVisibleWhen: evalVisibleWhen,
794
+ // W0-D: the reserved "unset / scheme default" sentinel a nullable LIVE enum
795
+ // override control is seeded with. The ONE definition — the host (AuthorView
796
+ // Inspector commit) reads THIS to map the sentinel back to null on write, never
797
+ // a hand-copied magic string.
798
+ NULL_ENUM_SENTINEL: NULL_ENUM_SENTINEL,
799
+ // Exposed for the exhaustiveness conformance test — driven off THIS list,
800
+ // never a hand-copied duplicate (schema-mirror discipline).
801
+ HANDLED_FIELD_TYPES: Object.keys(TYPE_DISPATCH),
802
+ // Exposed so a consuming test (or a future host) can assert against the
803
+ // SAME gate list this factory enforces, never a hand-copied duplicate —
804
+ // see scope note #6 / BIND_EXECUTABLE_KINDS above.
805
+ BIND_EXECUTABLE_KINDS: BIND_EXECUTABLE_KINDS,
806
+ // ── Reuse seam for makeControlsForRenderSurface.js (WS Studio Controls ────
807
+ // Realignment §7 r2, architect condition: "REUSES makeControlsForObject's
808
+ // TYPE_DISPATCH — the big DRY win", NOT a third hand-parallel conversion).
809
+ // Exposes ONLY the per-field-type handlers a renderCapabilities() descriptor
810
+ // uses (`number`, `enum`, `boolean`, `string`) — deliberately NOT the full
811
+ // TYPE_DISPATCH map: the rest bake in Frame/Universe-specific assumptions (the
812
+ // rect z/d component ordering, the Frame writable-boolean engine-ABI coercion
813
+ // trap in scope note #1(a)). A boolean render capability declares
814
+ // `engineVisible:false` (it is render-only), so `_booleanHandler` emits its 0/1
815
+ // control; a `string` capability (an OPEN-ID asset id) is a display entry, which
816
+ // the render factory does not turn into a control. These handlers call the
817
+ // generic `_readValue` fallback (`wrapper[propName]`) for any `env.kind` outside
818
+ // {'Frame','Universe'}, so `env.kind:'RenderSurface'` with a synthetic
819
+ // single-field `wrapper` reuses them UNMODIFIED.
820
+ RENDER_TYPE_HANDLERS: { number: _numberHandler, enum: _enumHandler, boolean: _booleanHandler, string: _stringHandler }
821
+ };
822
+
823
+ export { MakeControlsForObject };
824
+