@descix/frqtl-node 0.6.0-p4.1 → 0.6.0

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 (63) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +5 -3
  3. package/dist/dist-manifest.json +58 -54
  4. package/dist/fat/pkg-node/package.json +2 -10
  5. package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
  6. package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
  7. package/dist/frqtl/objects/FrqtlApp.js +5 -5
  8. package/dist/frqtl/objects/RecipeContext.js +106 -1
  9. package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
  10. package/dist/frqtl/objects/instruments/capture.js +1 -24
  11. package/dist/frqtl/objects/instruments/hosted-eyes.js +17 -1
  12. package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
  13. package/dist/frqtl/objects/record/Record.js +25 -5
  14. package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
  15. package/dist/frqtl/objects/schema/scopes.js +2 -2
  16. package/dist/frqtl/objects/setup/_worldInit.js +1 -2
  17. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
  18. package/dist/frqtl/parts/CameraControls.js +8 -0
  19. package/dist/frqtl/parts/DetectorWall.js +1 -1
  20. package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
  21. package/dist/frqtl/parts/LargeObject.js +2 -2
  22. package/dist/frqtl/parts/LiveChart.js +2 -2
  23. package/dist/frqtl/parts/LogicGate.js +3 -3
  24. package/dist/frqtl/parts/OvenBox.js +1 -1
  25. package/dist/frqtl/parts/PointSource.js +1 -1
  26. package/dist/frqtl/parts/SlitLightSource.js +1 -1
  27. package/dist/frqtl/parts/SlitScreen.js +1 -1
  28. package/dist/frqtl/parts/SphereEmitter.js +12 -12
  29. package/dist/frqtl/parts/Trace.js +1 -1
  30. package/dist/frqtl/parts/Tunnel.js +11 -11
  31. package/dist/frqtl/parts/Wall.js +20 -10
  32. package/dist/frqtl/scenes/circuit-sat.js +1 -0
  33. package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
  34. package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
  35. package/dist/frqtl/scenes/mirror-route.js +96 -79
  36. package/dist/shim/bundle/index.js +6 -0
  37. package/dist/shim/controls/index.js +1 -1
  38. package/dist/shim/engine/artifact-shell.js +1 -1
  39. package/dist/shim/engine/router.js +3 -3
  40. package/dist/shim/host/host-view.js +228 -0
  41. package/dist/shim/host/index.js +18 -0
  42. package/dist/shim/index.js +2 -4
  43. package/dist/shim/notebook/kernel.js +1 -1
  44. package/dist/shim/notebook/setup-builder.js +2 -2
  45. package/dist/shim/package.json +1 -0
  46. package/dist/shim/page-params/index.js +2 -30
  47. package/dist/shim/project/fork.js +293 -0
  48. package/dist/shim/project/identity.js +3 -1
  49. package/dist/shim/project/link.js +43 -3
  50. package/dist/shim/project/parts-writer.js +62 -5
  51. package/dist/shim/project/read-text.js +43 -0
  52. package/dist/shim/project/specifiers.js +104 -15
  53. package/dist/shim/registry/exhibits.js +15 -34
  54. package/dist/shim/registry/index.js +2 -2
  55. package/dist/shim/registry/scene-roster.js +35 -0
  56. package/dist/shim/registry/scene-source.js +6 -36
  57. package/dist/shim/registry/walkthrough.js +2 -3
  58. package/dist/shim/render-state/apply.js +66 -31
  59. package/dist/shim/render-state/index.js +6 -2
  60. package/dist/shim/runtime/index.js +4 -4
  61. package/llms.txt +4 -3
  62. package/package.json +19 -1
  63. package/dist/shim/registry/experiments.js +0 -215
@@ -662,12 +662,21 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
662
662
  return { files: files, record: out.record, clips: out.clips };
663
663
  };
664
664
 
665
- /** One act through the page's own doors: toSketch({by}), setDeclared(name, patch, {by}), placeDeclaredAt(name, point). */
665
+ /** rec.changes (the page's own read: a deep-frozen copy per read) as plain JSON. */
666
+ var _IN_PAGE_RECORD_CHANGES = function () {
667
+ var p = window.__frqtlHostedRecord;
668
+ if (!p || !p.rec) { throw new Error('[FrqtlHostedEyes/in-page] no Record is open — eyes.record.start() first.'); }
669
+ return JSON.parse(JSON.stringify(p.rec.changes));
670
+ };
671
+
672
+ /** One act through the page's own doors: toSketch({by}), setDeclared(name, patch, {by}), fork(name, Kind, {by}),
673
+ * placeDeclaredAt(name, point). */
666
674
  var _IN_PAGE_ACT = async function (args) {
667
675
  var exp = window.FrqtlPage.experiment;
668
676
  var r = window.FrqtlPage.runner;
669
677
  if (args.door === 'toSketch') { return exp.toSketch(args.opts); }
670
678
  if (args.door === 'setDeclared') { return exp.setDeclared(args.name, args.patch, args.opts); }
679
+ if (args.door === 'fork') { return exp.fork(args.name, args.Kind, args.opts); }
671
680
  if (args.door === 'placeDeclaredAt') {
672
681
  var d = r.declared.find(function (o) { return o.name === args.name; });
673
682
  if (!d) { throw new Error('[FrqtlHostedEyes/in-page] no declared object is named ' + JSON.stringify(args.name) + '.'); }
@@ -1113,6 +1122,8 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
1113
1122
  * film(opts) rec.film(opts) → its clip spans. Drive the world while it films:
1114
1123
  * `const f = eyes.record.film(o); await eyes.driveToTick(n); await f`
1115
1124
  * stop() rec.stop() → { files: { path: string | Uint8Array }, record, clips } | { empty: true }
1125
+ * changes() rec.changes → the rows so far, plain JSON (the page's own read). REJECTS when no Record is
1126
+ * open (as stop() does). One act's rows: read the length before the act, slice after.
1116
1127
  */
1117
1128
  record: {
1118
1129
  start: function () {
@@ -1125,6 +1136,9 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
1125
1136
  },
1126
1137
  stop: function () {
1127
1138
  return page.evaluate(_IN_PAGE_RECORD_STOP, { modules: modules }).then(_rethrowRefused).then(_folderFromPage);
1139
+ },
1140
+ changes: function () {
1141
+ return page.evaluate(_IN_PAGE_RECORD_CHANGES);
1128
1142
  }
1129
1143
  },
1130
1144
 
@@ -1132,11 +1146,13 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
1132
1146
  * Acts through the page's own doors (the writer's rows come from these, never from node):
1133
1147
  * toSketch({ by }) exp.toSketch
1134
1148
  * setDeclared(name, patch, { by }) exp.setDeclared
1149
+ * fork(name, Kind, { by }) exp.fork → { fork, file, from, files } (the files as text)
1135
1150
  * placeDeclaredAt(name, [x, y, z?]) runner.placeDeclaredAt on the declared object of that name
1136
1151
  */
1137
1152
  act: {
1138
1153
  toSketch: function (opts) { return page.evaluate(_IN_PAGE_ACT, { door: 'toSketch', opts: opts }); },
1139
1154
  setDeclared: function (name, patch, opts) { return page.evaluate(_IN_PAGE_ACT, { door: 'setDeclared', name: name, patch: patch, opts: opts }); },
1155
+ fork: function (name, Kind, opts) { return page.evaluate(_IN_PAGE_ACT, { door: 'fork', name: name, Kind: Kind, opts: opts }); },
1140
1156
  placeDeclaredAt: function (name, point) { return page.evaluate(_IN_PAGE_ACT, { door: 'placeDeclaredAt', name: name, point: point }); }
1141
1157
  },
1142
1158
 
@@ -254,8 +254,8 @@ function _resolveBounds(field, value) {
254
254
 
255
255
  function _label(field) {
256
256
  // An optional display `label` overrides the field NAME for the
257
- // control's shown label WITHOUT renaming the schema field (the codegen/capture
258
- // key stays `field.name`). e.g. leafDimension → "Emit Dimension", the global
257
+ // control's shown label WITHOUT renaming the schema field (the capture key
258
+ // stays `field.name`). e.g. leafDimension → "Emit Dimension", the global
259
259
  // shape capability → "Default Shape". Absent → the name, as before (byte-identical).
260
260
  var base = (typeof field.label === 'string' && field.label) ? field.label : field.name;
261
261
  return field.unit ? (base + ' (' + field.unit + ')') : base;
@@ -556,8 +556,8 @@ function _rectArrayHandler(field, env) {
556
556
  value: r.slice(),
557
557
  description: (field.description || '') +
558
558
  ' (DISPLAY ONLY — the bind contract {object,prop} has no array-index ' +
559
- 'addressing; a live per-entry write needs either bind.index, a shim-contract ' +
560
- 'extension, or an onChange closure, forbidden this bite. See ' +
559
+ 'addressing; a live per-entry write needs either bind.index or a shim-contract ' +
560
+ 'extension (an onChange closure is not a bind). See ' +
561
561
  'makeControlsForObject.js\'s module docstring, scope note #2.)'
562
562
  });
563
563
  }
@@ -730,7 +730,7 @@ function makeControlsForObject(wrapper, ctx, opts) {
730
730
  // field-name list) marks an ENGINE INTERNAL that must never surface as a
731
731
  // designer control/display (e.g. LargeObject.program's raw op-program AST,
732
732
  // the derived op-program-scope geometry accessors). Skipped entirely here —
733
- // no control, no display row. Still engineVisible + captured for codegen.
733
+ // no control, no display row. Still engineVisible + captured by the parts writer.
734
734
  if (field.internal === true) { continue; }
735
735
  var handler = TYPE_DISPATCH[field.type];
736
736
  if (!handler) {
@@ -22,12 +22,20 @@
22
22
  * `closedBy: 'stop'`, or — when the facade's world is re-authored (a build of
23
23
  * text that is not the held text) — the folder of the world before, with
24
24
  * `closedBy: 'reauthor'` (rejected when that folder is refused, as stop() is).
25
+ * Both carry `droppedClips`: the clips still filming when the Record closed, which
26
+ * the close drops (0 on a stop, which refuses while a clip films); a refused
27
+ * re-author close carries it as err.droppedClips beside err.closedBy.
25
28
  * A refused close nobody READ (the getter records the read) makes the facade's
26
29
  * next exp.startRecord() reject, naming it.
27
30
  * err.record every rejection of stop() and of `closed` carries { changes, clips } — the
28
31
  * Record's rows and clip spans — so a host can name them unheld instead of losing them.
29
32
  *
30
- * `closedBy` and the clip spans are the host's (a lab's own `filmed` list); record.json never carries them.
33
+ * `closedBy`, `droppedClips` and the clip spans are the host's (a lab's own `filmed` list); record.json never
34
+ * carries them.
35
+ *
36
+ * An act (toSketch, setDeclared, fork) is ADMITTED only while the Record is open and not stopping (`_admit`, asked
37
+ * before the act changes the held text): an act during an async stop() is refused naming "stopping", and the
38
+ * held text is unchanged.
31
39
  *
32
40
  * WHERE ITS PARTS COME FROM — nothing here captures a second time:
33
41
  * rows the writer's own edits at act end (RecipeContext.toSketch / setDeclared call `_row`); only what
@@ -152,10 +160,20 @@ Record.requireBy = function (by, where) {
152
160
  return by;
153
161
  };
154
162
 
163
+ /**
164
+ * THE admission check: an act may write into this Record only while it is open and not stopping. The facade asks
165
+ * it BEFORE an act changes the held text (toSketch, setDeclared, fork), and `_row` asks it again: one predicate.
166
+ * @private
167
+ */
168
+ Record.prototype._admit = function (where) {
169
+ var at = where ? where + ': ' : '';
170
+ if (!this._open) { _fail(at + 'this Record is closed — no row is written into it.'); }
171
+ if (this._stopping) { _fail(at + 'this Record is stopping — an act during stop() belongs to the next Record.'); }
172
+ };
173
+
155
174
  /** One change row, at the Record's current tick, in the order acted. @private */
156
175
  Record.prototype._row = function (row) {
157
- if (!this._open) { _fail('this Record is closed — no row is written into it.'); }
158
- if (this._stopping) { _fail('this Record is stopping — an act during stop() belongs to the next Record.'); }
176
+ this._admit();
159
177
  this._rows.push({ tick: this._tick, name: row.name, op: row.op, by: row.by, from: row.from, to: row.to });
160
178
  };
161
179
 
@@ -302,7 +320,7 @@ Record.prototype.stop = async function () {
302
320
  finally { this._stopping = false; }
303
321
  this._closedBy = 'stop';
304
322
  this._detach();
305
- this._closedSettle.resolve(Object.assign({}, out, { closedBy: 'stop' }));
323
+ this._closedSettle.resolve(Object.assign({}, out, { closedBy: 'stop', droppedClips: 0 }));
306
324
  return out;
307
325
  };
308
326
 
@@ -312,13 +330,15 @@ Record.prototype.stop = async function () {
312
330
  */
313
331
  Record.prototype._closeOnReauthor = function (held) {
314
332
  var self = this;
333
+ var dropped = this._inflight; // clips still filming: detaching drops them (their end() finds the Record closed)
315
334
  this._closedBy = 'reauthor';
316
335
  this._detach();
317
336
  this._folder(held).then(function (out) {
318
- self._closedSettle.resolve(Object.assign({}, out, { closedBy: 'reauthor' }));
337
+ self._closedSettle.resolve(Object.assign({}, out, { closedBy: 'reauthor', droppedClips: dropped }));
319
338
  }, function (e) {
320
339
  self._refused = self._carry(e);
321
340
  self._refused.closedBy = 'reauthor';
341
+ self._refused.droppedClips = dropped;
322
342
  self._closedSettle.reject(self._refused);
323
343
  });
324
344
  };
@@ -94,7 +94,7 @@ import * as FrameDataSchema from '../../../shim/frame-data/index.js';
94
94
  engineVisible: true,
95
95
  description: 'Frame center Z position (depth, physics pixels). ' +
96
96
  'Default 0.0 when active_axes.z is false (2D mode). ' +
97
- 'Bite 3 (ws-3d-discrete-walk): set by the 3D emitter; moved by the ' +
97
+ 'Set by the 3D emitter; moved by the ' +
98
98
  'independent z yo-yo coin when active_axes.z is true. ' +
99
99
  'Cube edge = w (spec §11 — no separate d field; cubes only).'
100
100
  },
@@ -322,7 +322,7 @@ import * as FrameDataSchema from '../../../shim/frame-data/index.js';
322
322
  engineVisible: true,
323
323
  description: 'Derived (scope-read): frame CENTER Z before this tick\'s move() ' +
324
324
  '(physics pixels). Default 0.0 when active_axes.z is false. ' +
325
- 'Bite 3 (ws-3d-discrete-walk): saved by Frame::move_frame alongside prev_x/prev_y. ' +
325
+ 'Saved by Frame::move_frame alongside prev_x/prev_y. ' +
326
326
  'Read-only — write THROWS (engine-owned per-tick snapshot).'
327
327
  },
328
328
  // ── Forbidden: engine-owned topology ──────────────────────────────
@@ -273,7 +273,7 @@ export function shapeValuesMap() {
273
273
  // existing scene is byte-identical. A live edit (updateObjectProperty) sets the
274
274
  // per-instance override that _visibleLargeObjects reads.
275
275
  description: 'When this object\'s visual shell renders (render-only): design = only ' +
276
- 'while authoring (Bite-4b marker default); run = always visible (the "red light ' +
276
+ 'while authoring (the marker default); run = always visible (the "red light ' +
277
277
  'bulb" opt-in); never = hidden. Generalizes the design-only marker suppression ' +
278
278
  'at the single _visibleLargeObjects authority; engine collision is unaffected.'
279
279
  },
@@ -322,7 +322,7 @@ export function shapeValuesMap() {
322
322
  // designer control — the raw JSON must never render in the STUDIO Inspector.
323
323
  // `internal:true` is the ONE authority for "hide from the designer surface"
324
324
  // (consumed by makeControlsForObject + AuthorView's renderInspector); the
325
- // program is still engineVisible + captured for codegen/round-trip.
325
+ // program is still engineVisible + captured (the parts writer's round trip).
326
326
  internal: true,
327
327
  description: 'Op-program attached to this LO (§10.3 SLICE 3). ' +
328
328
  'Shape: { on: "collision"|"tick", ops: [{ set: "<handle>.<prop>", to: ExprAST }] }. ' +
@@ -23,8 +23,7 @@
23
23
  *
24
24
  * The list is derived from what `initArgs()` below ACTUALLY reads. `interQuantumCollisions`
25
25
  * is in it because it is still ACCEPTED (inert — it fed a removed positional slot) and
26
- * STUDIO codegen prints it into every saved recipe: dropping it from the vocabulary
27
- * would make every saved scene throw.
26
+ * saved scenes carry it: dropping it from the vocabulary would make them throw.
28
27
  *
29
28
  * @type {Array<{key:string, effect:string}>}
30
29
  */
@@ -34,8 +34,7 @@
34
34
  * — as `lib/frqtl/scenes/blackbody.js` sets it. ENGINE physics; no new
35
35
  * op-programs. Expected JS↔WASM divergence on parent-frame count (deferred §6).
36
36
  *
37
- * Set any flag to `false` only for progressive test isolation (L1/L2/L3 runs in
38
- * `experiments/blackbody-substrate/run.js`). The default (all true) runs the
37
+ * Set any flag to `false` only for progressive test isolation (L1/L2/L3 runs). The default (all true) runs the
39
38
  * scene's flags (brownian on, inter-quantum collisions on).
40
39
  *
41
40
  * ## Thermostat control loop (as-intended §2.1, Dispatch C2)
@@ -177,6 +177,12 @@ import * as OrbitBag from '../compat/renderer/orbit-bag.js';
177
177
  if (mountEl && mountEl.__frqtlCameraHandle && mountEl.__frqtlCameraHandle.mounted) {
178
178
  return mountEl.__frqtlCameraHandle;
179
179
  }
180
+ // ...and per RUNNER: one runner has one camera controller. A host that mounted the pads before the scene
181
+ // could (STUDIO: the sizeless page builds before its runner has a control host), then a re-run in the same
182
+ // document (a fork, setDeclared) that reaches the scene's own attach with another container, gets the live one.
183
+ if (runner.__frqtlCameraHandle && runner.__frqtlCameraHandle.mounted) {
184
+ return runner.__frqtlCameraHandle;
185
+ }
180
186
 
181
187
  var doc = (mountEl && mountEl.ownerDocument) ? mountEl.ownerDocument
182
188
  : (typeof document !== 'undefined' ? document : null);
@@ -581,6 +587,7 @@ import * as OrbitBag from '../compat/renderer/orbit-bag.js';
581
587
  if (mountEl && mountEl.__frqtlCameraHandle === liveHandle) {
582
588
  mountEl.__frqtlCameraHandle = null;
583
589
  }
590
+ if (runner.__frqtlCameraHandle === liveHandle) { runner.__frqtlCameraHandle = null; }
584
591
  if (rafId !== null) { cAF(rafId); rafId = null; }
585
592
  // Force-release any in-flight joystick drag (detaches its doc listeners).
586
593
  for (var i = 0; i < padCleanups.length; i++) {
@@ -617,6 +624,7 @@ import * as OrbitBag from '../compat/renderer/orbit-bag.js';
617
624
  };
618
625
  // F4: mark the container as owning this live instance (idempotency guard above).
619
626
  if (mountEl) { mountEl.__frqtlCameraHandle = liveHandle; }
627
+ runner.__frqtlCameraHandle = liveHandle;
620
628
  return liveHandle;
621
629
  }
622
630
 
@@ -111,7 +111,7 @@ import { registerKind } from '../objects/kinds.js';
111
111
  var depth = (opts.depth !== undefined && opts.depth !== null) ? opts.depth : null;
112
112
 
113
113
  this.universe = universe;
114
- this._schemaKind = 'DetectorWall';
114
+ this._schemaKind = this.constructor.name;
115
115
 
116
116
  // The world rect, through the ONE normalizer (a zero-high detector counts nothing
117
117
  // and would report it as physics — so a missing dimension throws, never zeroes).
@@ -24,8 +24,8 @@
24
24
  * so every Wall option, live method and property accessor applies verbatim.
25
25
  * When neither `preset` nor `collisionBehavior` is supplied it defaults to the
26
26
  * `detector-screen` preset (destroy + count) — the counting authority. Extra
27
- * options, all DECLARED descriptor fields (so a codegen Restart round-trip
28
- * rebuilds the heatmap wall from captured state — the W2 declared-field rule):
27
+ * options, all DECLARED descriptor fields (so the parts writer captures them and the
28
+ * sketch rebuilds the heatmap wall from that state — the W2 declared-field rule):
29
29
  *
30
30
  * @param {number} [opts.binPx=12] — face bin size in physics pixels. Bins
31
31
  * are anchored at the FACE ORIGIN (its top / near-z corner), so the map runs
@@ -105,7 +105,7 @@ import { registerKind, kindOf } from '../objects/kinds.js';
105
105
  * painting of the only spatial information a 2D scene carries.
106
106
  *
107
107
  * @param {Array|Object} rect — the LO rect ([l,t,w,h] / [l,t,z,w,h,d] /
108
- * {left,top,w,h[,z,d]} — the shape a codegen round-trip captures).
108
+ * {left,top,w,h[,z,d]} — the shape a live LO's rect reads as).
109
109
  * @param {number} binPx
110
110
  * @returns {{top:number,height:number,z0:number,depth:number,binPx:number,
111
111
  * canvasW:number,canvasH:number}}
@@ -248,9 +248,9 @@ import { registerKind, kindOf } from '../objects/kinds.js';
248
248
  opts = merged;
249
249
  }
250
250
  WallCls.call(this, universe, opts);
251
- this._schemaKind = 'GraphingDetectorWall';
251
+ this._schemaKind = this.constructor.name;
252
252
 
253
- // Declared heatmap fields (own props → codegen captureProps round-trip).
253
+ // Declared heatmap fields (own props → the parts writer's captureProps).
254
254
  this.binPx = _num(opts.binPx, 12);
255
255
  this.refreshMs = _num(opts.refreshMs, 100);
256
256
  this.heatBase = _num(opts.heatBase, 0.35);
@@ -399,8 +399,8 @@ import { registerKind, kindOf } from '../objects/kinds.js';
399
399
  *
400
400
  * FIELDS ARE FERRIED FROM WallDescriptor (never re-listed — the
401
401
  * schema-mirror-drift discipline): this descriptor is Wall's field table plus
402
- * the three heatmap fields. Every one is a DECLARED field, so a codegen
403
- * Restart round-trip rebuilds the heatmap wall from captured state.
402
+ * the three heatmap fields. Every one is a DECLARED field, so the parts writer
403
+ * captures it and the sketch rebuilds the heatmap wall from that state.
404
404
  */
405
405
  var WallDescriptor = kindOf(WallCls).descriptor;
406
406
  var GraphingDetectorWallDescriptor = {
@@ -35,7 +35,7 @@ import { SCOPE_DESCRIPTORS } from '../objects/schema/scopes.js';
35
35
  * Build a LargeObject `.rect` for the 2D (4-element) case: a REAL Rectangle instance
36
36
  * (is_centered=false) so a center write (`.x`/`.y`) auto-recomputes left/top and vice
37
37
  * versa (PR-1 — audit item 3). A non-enumerable `toJSON` is attached so JSON-serializing
38
- * consumers (codegen `_jsonCopy`, bundle export, save-state) still produce the SAME plain
38
+ * consumers (bundle export, save-state, any JSON copy) still produce the SAME plain
39
39
  * `{left,top,w,h}` shape they always did — the Rectangle's own enumerable fields are
40
40
  * `_left/_x/...`, which WOULD drift; `toJSON` pins the wire shape. (`left/top/w/h` are read
41
41
  * via getters by every direct consumer, so those are unaffected.) 6-element z/d rects are
@@ -128,7 +128,7 @@ import { SCOPE_DESCRIPTORS } from '../objects/schema/scopes.js';
128
128
  collectCollided = collectCollided || false;
129
129
 
130
130
  this.universe = universe;
131
- this._schemaKind = 'LargeObject';
131
+ this._schemaKind = this.constructor.name;
132
132
  // rect is [left, top, w, h] (legacy) or [left, top, z, w, h, d] (FRQTL-MAINT
133
133
  // 2026-07-01, explicit z/depth) — is_centered=false convention either way.
134
134
  var ra = rectToArray(rect);
@@ -267,8 +267,8 @@ import { registerKind } from '../objects/kinds.js';
267
267
  };
268
268
  // Mirror the SERIALIZABLE chartSpec fields onto the LO under their DESCRIPTOR
269
269
  // names (restart-bug fix, 2026-07-25): captureProps(lo, LiveChartDescriptor)
270
- // reads lo[field.name], so without these mirrors a codegen round-trip (Save /
271
- // Restart evals the generated module) silently rebuilt every chart UNBOUND
270
+ // reads lo[field.name], so without these mirrors a captured chart (the parts writer's
271
+ // capture of a declared chart) would rebuild UNBOUND
272
272
  // with default projector/label ("detector"). The name string — never the live
273
273
  // sourceHandle — is the captured source (re-resolved here on rebuild).
274
274
  lo.source = sourceName;
@@ -219,7 +219,7 @@ import { registerKind } from '../objects/kinds.js';
219
219
  function LogicGate(universe, opts) {
220
220
  opts = opts || {};
221
221
  this.universe = universe;
222
- this._schemaKind = 'LogicGate';
222
+ this._schemaKind = this.constructor.name;
223
223
 
224
224
  var TunnelCls = _Tunnel;
225
225
  var WallCls = _Wall;
@@ -705,8 +705,8 @@ import { registerKind } from '../objects/kinds.js';
705
705
  var gate = new GateCls(ctx.runner.universe, opts);
706
706
  gate._schemaKind = Class.name;
707
707
  // The comparator is a HOST tick-fn: subscribe it to the runner's per-tick
708
- // fan-out here, off the DECLARED fields, so the switching survives
709
- // codegen → Restart (a recipe-authored tick-fn would be lost). Idempotent.
708
+ // fan-out here, off the DECLARED fields, so the switching belongs to the declared
709
+ // object: every build of it (a sketch entry, a Restart) re-wires it. Idempotent.
710
710
  if (typeof gate.attachToRunner === 'function') { gate.attachToRunner(ctx.runner); }
711
711
  return gate;
712
712
  }
@@ -199,7 +199,7 @@ import { registerKind } from '../objects/kinds.js';
199
199
  options = options || {};
200
200
 
201
201
  this.universe = universe;
202
- this._schemaKind = 'OvenBox';
202
+ this._schemaKind = this.constructor.name;
203
203
  this.name = options.name || 'oven'; // its declared name (the Tunnel/LogicGate idiom)
204
204
  this.temperature = options.temperature || 300;
205
205
 
@@ -169,7 +169,7 @@ import { declaredCount } from '../objects/ObjectFactory.js';
169
169
  var zDepth = opts.zDepth !== undefined ? opts.zDepth : null;
170
170
 
171
171
  this.universe = universe;
172
- this._schemaKind = 'PointSource';
172
+ this._schemaKind = this.constructor.name;
173
173
  this.x = x;
174
174
  this.y = y;
175
175
  this.diameter = diameter;
@@ -82,7 +82,7 @@ import { V1_REPLACEMENTS } from '../objects/v1-replacements.js';
82
82
  var zDepth = (depth !== null) ? depth : null;
83
83
 
84
84
  this.universe = universe;
85
- this._schemaKind = 'SlitLightSource';
85
+ this._schemaKind = this.constructor.name;
86
86
 
87
87
  // Light source diameter: hardcoded to 16 matching devsdk's pointSOURCESIZE.
88
88
  // (wavelength * 0.25 * 2 = 32 would double the emission radius and velocity)
@@ -137,7 +137,7 @@ import { ObjectSchema } from '../objects/schema/index.js';
137
137
  */
138
138
  function SlitScreen(universe, wallLeft, wallWidth, canvasHeight, wavelength, absorbAtSlitWall, detectAtSlits, slitHeight, gapHeight, depth, holeDepth, linerScatter) {
139
139
  this.universe = universe;
140
- this._schemaKind = 'SlitScreen';
140
+ this._schemaKind = this.constructor.name;
141
141
  this.wavelength = wavelength;
142
142
  this.depth = (depth !== undefined && depth !== null) ? depth : null;
143
143
  // 'current' (default/omitted) = today's exact liner behavior, byte-identical.
@@ -157,7 +157,7 @@ import { CollisionTypes } from './CollisionTypes.js';
157
157
  * @param {Object} [opts]
158
158
  */
159
159
  function SphereEmitter(position, opts) {
160
- this._schemaKind = 'SphereEmitter';
160
+ this._schemaKind = this.constructor.name;
161
161
  // First-arg overload: an array is a position; a plain object is opts
162
162
  // (position defaults to canvas center → byte-identical factory default).
163
163
  if (Array.isArray(position)) {
@@ -180,7 +180,7 @@ import { CollisionTypes } from './CollisionTypes.js';
180
180
  // self-throttle target — the emitter deactivates when the live population
181
181
  // measured by the counting Wall NAMED by `gateCounter` reaches the cap, and
182
182
  // reactivates below it. Compiled at CONSTRUCTION into a host tick-fn (see
183
- // _installEmissionGate in addTo) so the gate is codegen-round-trippable and
183
+ // _installEmissionGate in addTo) so the gate is captured by the parts writer and
184
184
  // Restart-safe (the Restart-safe host-tick-fn rule — the gate is a
185
185
  // construction side effect of captured descriptor fields, exactly like the
186
186
  // ABSORB radiator, NEVER free recipe JS). Validation is throw-or-apply:
@@ -275,7 +275,7 @@ import { CollisionTypes } from './CollisionTypes.js';
275
275
  * `fn(universe)` seam — the SAME seam as the emit tick-fn, the blackbody
276
276
  * thermostat, and the ABSORB radiator). Each tick it:
277
277
  * 1. LAZILY resolves the counting Wall by NAME on the FIRST tick (handles are
278
- * registration-fresh and never survive codegen — the name is the reference;
278
+ * registration-fresh and never survive a rebuild — the name is the reference;
279
279
  * lazy resolution makes emitter/counter construction order irrelevant).
280
280
  * Unresolved after the first tick → THROW (never a silent uncapped run).
281
281
  * 2. Reads the live population through `counter.getLiveCount(universe._lastTickData)`
@@ -447,7 +447,7 @@ import { CollisionTypes } from './CollisionTypes.js';
447
447
  // captureProps(sphereObj, SphereEmitterDescriptor) reads `sphereObj[field.name]`
448
448
  // DIRECTLY for every descriptor field (a getter satisfies it). These expose the
449
449
  // LIVE resolved value (from the factory's control) so a dragged/edited emitter
450
- // Saves at its current state — the codegen round-trip authority. Boot-level fields
450
+ // Saves at its current state — what the parts writer captures. Boot-level fields
451
451
  // (fundamentalWaveLength) that the control doesn't surface read from the
452
452
  // construction opts (they cannot change live). `rect` is a WRITE-ONLY reposition
453
453
  // adapter: its getter is deliberately `undefined` so captureProps SKIPS it —
@@ -463,7 +463,7 @@ import { CollisionTypes } from './CollisionTypes.js';
463
463
  // the ObjectFactory marker diameter + makeControlsForObject's rect-seed. captureProps no
464
464
  // longer trips on this: the descriptor's rect field is `writeOnly:true`, so captureProps
465
465
  // SKIPS it by the descriptor flag (not by the getter being undefined) — `center` stays the
466
- // single captured position authority (codegen round-trip unchanged). The write path
466
+ // single captured position authority. The write path
467
467
  // (rect → setCenter) is still the applyProp adapter (updateObjectProperty).
468
468
  get: function () {
469
469
  var c = (this._control && typeof this._control.getCenter === 'function')
@@ -487,7 +487,7 @@ import { CollisionTypes } from './CollisionTypes.js';
487
487
  Object.defineProperty(SphereEmitter.prototype, 'is2D', _liveGetter(function (c) { return c.getIs2D(); }));
488
488
  Object.defineProperty(SphereEmitter.prototype, 'orientationDeg', _liveGetter(function (c) { return c.getOrientation(); }));
489
489
  Object.defineProperty(SphereEmitter.prototype, 'emissionBox', _liveGetter(function (c) { return c.getEmissionBox(); }));
490
- // Declared emission-gate fields (captured for codegen round-trip; undefined
490
+ // Declared emission-gate fields (captured by the parts writer; undefined
491
491
  // when uncapped → captureProps SKIPS them → byte-identical legacy scenes).
492
492
  Object.defineProperty(SphereEmitter.prototype, 'emissionCap', {
493
493
  get: function () { return this._emissionCap; }
@@ -585,7 +585,7 @@ import { CollisionTypes } from './CollisionTypes.js';
585
585
  * builder composes a LargeObject-visual marker + the factory's host emit
586
586
  * tick-fn (SU1 — the visual LO is NONE-collision, non-leaf, zero physics).
587
587
  *
588
- * This descriptor is the SINGLE field authority: captureProps (codegen),
588
+ * This descriptor is the SINGLE field authority: captureProps (the parts writer),
589
589
  * makeControlsForObject (the Inspector), and FrqtlApp.updateObjectProperty
590
590
  * (the live-edit write path) ALL drive off it — no hand-list anywhere.
591
591
  *
@@ -600,7 +600,7 @@ import { CollisionTypes } from './CollisionTypes.js';
600
600
  * round-trip; the Inspector just renders it read-only.
601
601
  *
602
602
  * FIELD SET is COMPLETE w.r.t. every scene-affecting setupSphereEmitter3D opt
603
- * (a missing opt = lossy codegen). `emissionBoxSide` is subsumed by the
603
+ * (a missing opt = a lossy capture). `emissionBoxSide` is subsumed by the
604
604
  * captured derived `emissionBox`. There is no init flag: the SKETCH inits the
605
605
  * world once (setupSphereEmitter3D.initWorld / the co-init call), so a second emitter on
606
606
  * one universe (6A/6B/6C) is simply a second object.
@@ -635,7 +635,7 @@ import { CollisionTypes } from './CollisionTypes.js';
635
635
  live: true,
636
636
  // PR-1 item iii: the facade's `.rect` getter now returns a REAL rect DERIVED from
637
637
  // the live center (so computeEntityBboxForDesign reads fresh — no stale bbox). To
638
- // keep `center` the SINGLE captured position authority (codegen round-trip), this
638
+ // keep `center` the SINGLE captured position authority, this
639
639
  // field is `writeOnly:true` → captureProps SKIPS it BY THE FLAG (not by the getter
640
640
  // being undefined). The WRITE path is unchanged: a rect write maps rect-CENTER →
641
641
  // the emission center (control.setCenter, via applyProp).
@@ -653,7 +653,7 @@ import { CollisionTypes } from './CollisionTypes.js';
653
653
  engineVisible: false,
654
654
  live: false,
655
655
  description: 'Emission SOURCE POSITION in absolute canvas px as [x,y,z] (the captured ' +
656
- 'position authority — codegen round-trips this, not rect). Edited live via the rect ' +
656
+ 'position authority — the parts writer writes this, not rect). Edited live via the rect ' +
657
657
  'adapter / the visual marker drag; shown read-only here.'
658
658
  },
659
659
  {
@@ -698,7 +698,7 @@ import { CollisionTypes } from './CollisionTypes.js';
698
698
  // pattern; circulation-emission-gate-contract §2 field shapes).
699
699
  // Sensor = a placeable counting Wall (name in gateCounter); regulation =
700
700
  // emitter-side sugar compiled at CONSTRUCTION into a host tick-fn (the
701
- // Restart-safe host-tick-fn rule — codegen captures these two fields and
701
+ // Restart-safe host-tick-fn rule — the parts writer captures these two fields and
702
702
  // the reconstructed emitter's constructor rebuilds the gate).
703
703
  name: 'emissionCap',
704
704
  type: 'number',
@@ -811,7 +811,7 @@ import { CollisionTypes } from './CollisionTypes.js';
811
811
  constraint: { min: 0, integer: true },
812
812
  engineVisible: false,
813
813
  live: true,
814
- // Field NAME stays `leafDimension` (codegen/capture key; a rename = round-trip
814
+ // Field NAME stays `leafDimension` (the capture key; a rename = round-trip
815
815
  // break + a second name = schema mirror — Q-A2). The Inspector shows
816
816
  // the display LABEL "Emit Dimension"; the write routes through applyProp →
817
817
  // control.setLeafDimension (host-side addDimension + closure var flip; the NEXT
@@ -245,7 +245,7 @@ import { Wall as _Wall } from './Wall.js';
245
245
  function Trace(universe, opts) {
246
246
  opts = opts || {};
247
247
  this.universe = universe;
248
- this._schemaKind = 'Trace';
248
+ this._schemaKind = this.constructor.name;
249
249
 
250
250
  var path = opts.path;
251
251
  if (!Array.isArray(path) || path.length < 2) {
@@ -239,7 +239,7 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
239
239
  function Tunnel(universe, opts) {
240
240
  opts = opts || {};
241
241
  this.universe = universe;
242
- this._schemaKind = 'Tunnel';
242
+ this._schemaKind = this.constructor.name;
243
243
 
244
244
  var left = opts.left, top = opts.top;
245
245
  var length = opts.length, bore = opts.bore;
@@ -307,7 +307,7 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
307
307
  'contention rather than the point source. (FAIL-LOUD.)');
308
308
  }
309
309
 
310
- // Stash the authored construction values (the codegen round-trip authority —
310
+ // Stash the authored construction values (what the parts writer captures —
311
311
  // the descriptor fields read these back verbatim, Box precedent).
312
312
  this.left = left; this.top = top; this.length = length; this.bore = bore;
313
313
  this.wallThickness = wallThickness; this.axis = axis; this.color = color;
@@ -316,7 +316,7 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
316
316
  // knob, so the Inspector must render its selector on every tunnel, which needs a
317
317
  // defined value to read. The three directed-only tunables follow the z/depth
318
318
  // OPT-IN precedent instead — left undefined in mirror mode so captureProps skips
319
- // them and a mirror tunnel's codegen gains exactly ONE key, not four.
319
+ // them and a mirror tunnel's captured entry gains exactly ONE key, not four.
320
320
  this.mode = mode;
321
321
  if (directed) {
322
322
  this.directedAngle = directedAngleDeg;
@@ -331,9 +331,9 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
331
331
  this.exitPhaseReset = exitPhaseReset;
332
332
  }
333
333
  // z/depth are DECLARED descriptor fields (TunnelDescriptor) so a 3D corridor
334
- // round-trips through codegen/Restart. A FLAT (2D) tunnel leaves them
335
- // `undefined` — captureProps skips undefined, so a 2D codegen stays
336
- // byte-identical to the pre-3D output (no `"z":0,"depth":null` noise).
334
+ // round-trips through the parts writer. A FLAT (2D) tunnel leaves them
335
+ // `undefined` — captureProps skips undefined, so a 2D capture carries
336
+ // no `"z":0,"depth":null` noise.
337
337
  if (hasDepth) { this.z = z; this.depth = depth; }
338
338
 
339
339
  // ── Mirror wall rects (two parallel plates a `bore` apart) ───────────
@@ -734,9 +734,9 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
734
734
  * Subscribe this tunnel's redirect to a runner's per-tick fan-out
735
735
  * (`FrqtlApp.addTickListener` — the multi-subscriber peer of the page's single
736
736
  * `onTick` slot, so it never steals it). Called by the ObjectFactory builder, which is
737
- * what makes the directed mode survive codegen → Restart: the mode is a DECLARED
738
- * descriptor field on the facade, so the rebuilt object re-wires itself. A raw tick-fn
739
- * hand-written in a recipe would NOT survive (Restart evals the codegen'd module).
737
+ * what makes the directed mode part of the declared object: the mode is a DECLARED
738
+ * descriptor field on the facade, so every build of it (a sketch entry, a Restart)
739
+ * re-wires itself.
740
740
  *
741
741
  * Idempotent, and a no-op for a mirror tunnel or a runner without the fan-out.
742
742
  * @param {Object} runner - an FrqtlApp (needs `addTickListener`).
@@ -1045,8 +1045,8 @@ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
1045
1045
  // mode:'directed' — subscribe the host redirect to the runner's per-tick fan-out.
1046
1046
  // Self-gating + idempotent, so this is called unconditionally (a mirror tunnel
1047
1047
  // no-ops). Wiring it HERE, off the DECLARED `mode` field, is what makes the
1048
- // directed behavior survive codegen → Restart: the rebuilt facade re-subscribes
1049
- // itself, where a recipe-authored tick-fn would be lost.
1048
+ // directed behavior part of the declared object: every build of the facade
1049
+ // re-subscribes itself.
1050
1050
  if (typeof tunnel.attachToRunner === 'function') { tunnel.attachToRunner(ctx.runner); }
1051
1051
  return tunnel;
1052
1052
  }