@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p4.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 (78) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +8 -4
  3. package/dist/dist-manifest.json +70 -62
  4. package/dist/eyes/index.js +5 -2
  5. package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
  6. package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
  7. package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
  8. package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
  9. package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
  10. package/dist/frqtl/objects/FrqtlApp.js +136 -30
  11. package/dist/frqtl/objects/ObjectFactory.js +4 -5
  12. package/dist/frqtl/objects/RecipeContext.js +617 -9
  13. package/dist/frqtl/objects/instruments/hosted-eyes.js +268 -57
  14. package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
  15. package/dist/frqtl/objects/record/Record.js +326 -0
  16. package/dist/frqtl/objects/record/open-record.js +22 -0
  17. package/dist/frqtl/objects/schema/index.js +72 -0
  18. package/dist/frqtl/parts/DetectorWall.js +1 -1
  19. package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
  20. package/dist/frqtl/parts/FrqtlVision.js +21 -6
  21. package/dist/frqtl/parts/Wall.js +2 -0
  22. package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
  23. package/dist/frqtl/scenes/absorbing-wall.js +18 -14
  24. package/dist/frqtl/scenes/atomic-model.js +3 -0
  25. package/dist/frqtl/scenes/big-bang.js +3 -0
  26. package/dist/frqtl/scenes/blackbody.js +14 -11
  27. package/dist/frqtl/scenes/circuit-sat.js +7 -4
  28. package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
  29. package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
  30. package/dist/frqtl/scenes/counting-wall.js +19 -15
  31. package/dist/frqtl/scenes/double-slit.js +3 -0
  32. package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
  33. package/dist/frqtl/scenes/dse-one-slit.js +59 -55
  34. package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
  35. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
  36. package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
  37. package/dist/frqtl/scenes/dse-two-slits.js +69 -65
  38. package/dist/frqtl/scenes/empty.js +3 -0
  39. package/dist/frqtl/scenes/laser-buildup.js +23 -16
  40. package/dist/frqtl/scenes/mirror-3d.js +31 -27
  41. package/dist/frqtl/scenes/mirror-route.js +4 -1
  42. package/dist/frqtl/scenes/mirror.js +24 -20
  43. package/dist/frqtl/scenes/omni-3d.js +7 -4
  44. package/dist/frqtl/scenes/omni-source.js +7 -4
  45. package/dist/frqtl/scenes/self-interference.js +13 -10
  46. package/dist/frqtl/scenes/smoking-gun.js +15 -11
  47. package/dist/frqtl/scenes/straight-walk.js +7 -4
  48. package/dist/frqtl/scenes/sun.js +11 -8
  49. package/dist/frqtl/scenes/two-source.js +22 -17
  50. package/dist/frqtl/scenes/wave.js +10 -7
  51. package/dist/shim/bundle/index.js +112 -96
  52. package/dist/shim/bundle/indexHtml.js +66 -39
  53. package/dist/shim/engine/router.js +2 -2
  54. package/dist/shim/index.js +1 -1
  55. package/dist/shim/notebook/format.js +1 -1
  56. package/dist/shim/notebook/store.js +2 -3
  57. package/dist/shim/page-params/index.js +2 -2
  58. package/dist/shim/project/index.js +20 -5
  59. package/dist/shim/project/link.js +182 -37
  60. package/dist/shim/project/manifest.js +151 -0
  61. package/dist/shim/project/parts-module.js +67 -0
  62. package/dist/shim/project/parts-writer.js +457 -0
  63. package/dist/shim/project/record-format.js +345 -0
  64. package/dist/shim/project/specifiers.js +41 -13
  65. package/dist/shim/registry/index.js +1 -1
  66. package/dist/shim/registry/scene-source.js +44 -30
  67. package/dist/shim/registry/walkthrough.js +13 -12
  68. package/dist/shim/render-state/apply.js +11 -14
  69. package/dist/shim/storage/codec-chains.js +88 -0
  70. package/dist/shim/storage/folders.js +205 -0
  71. package/dist/shim/storage/index.js +64 -8
  72. package/dist/shim/storage/make-namespace-store.js +5 -5
  73. package/llms.txt +2 -2
  74. package/package.json +1 -1
  75. package/dist/shim/bundle/frqnb.js +0 -112
  76. package/dist/shim/bundle/manifest.js +0 -95
  77. package/dist/shim/storage/modules.js +0 -249
  78. package/dist/shim/storage/projects.js +0 -150
@@ -28,6 +28,16 @@
28
28
  * z absent → [cx − w/2, cy − h/2, w, h], and cz must be 0 — a z-less rect sits at z-centre
29
29
  * 0 by the rule above, so any other cz THROWS rather than being dropped.
30
30
  *
31
+ * A HAND places on the pixel lattice (frames sit on integer pixels; a hand between pixels is a
32
+ * screen-scale artifact), so every hand write is snapped once, here:
33
+ *
34
+ * snapRectPosition(t) → a rect write tuple with its POSITION on the lattice: `Math.round` of left,
35
+ * top (and z of a 6-tuple); the size (w, h, d) is returned byte-identical — a hand moves, it never
36
+ * resizes.
37
+ * snapCenter(c) → a centre ([x, y] / [x, y, z]) with every component rounded.
38
+ * Both are called by the hand doors only (FrqtlApp.placeDeclaredAt; STUDIO's drag / resize write). A
39
+ * value typed into a field is the user's literal and is never snapped; no build path calls them.
40
+ *
31
41
  * ── The drawn VOLUME and the center-ray hit (STUDIO's aim) ──────────────────────
32
42
  *
33
43
  * The same rect also owns the VOLUME the aim ray tests and the shadow-surround paints, so the
@@ -94,6 +104,7 @@ function rectAtCenter(rect, center) {
94
104
  var DEG = Math.PI / 180;
95
105
 
96
106
  /** The rect's z-extent: d ?? w when it carries z (the engine's z-capable rectangle), else 0. */
107
+ function zExtent(rect) { return _zExtent(rect); }
97
108
  function _zExtent(rect) {
98
109
  if (!hasZ(rect)) { return 0; }
99
110
  return (typeof rect.d === 'number') ? rect.d : rect.w;
@@ -220,9 +231,39 @@ function firstHit(origin, dir, volumes, margin) {
220
231
  return best;
221
232
  }
222
233
 
234
+ function _finiteTuple(v, who, lengths) {
235
+ if (!Array.isArray(v) || lengths.indexOf(v.length) === -1 ||
236
+ !v.every(function (n) { return typeof n === 'number' && isFinite(n); })) {
237
+ throw new TypeError('[LoDrawnCenter.' + who + '] ' + lengths.join(' or ') + ' finite numbers; got ' +
238
+ JSON.stringify(v) + '. (FAIL-LOUD.)');
239
+ }
240
+ }
241
+
242
+ /**
243
+ * snapRectPosition(t) — a hand's rect write with its position on the pixel lattice, its size untouched.
244
+ * @param {number[]} t — [left, top, w, h] or [left, top, z, w, h, d] (RECT_TUPLE order) @returns {number[]}
245
+ */
246
+ function snapRectPosition(t) {
247
+ _finiteTuple(t, 'snapRectPosition', [4, 6]);
248
+ var npos = t.length === 6 ? 3 : 2;
249
+ return t.map(function (n, i) { return i < npos ? Math.round(n) : n; });
250
+ }
251
+
252
+ /**
253
+ * snapCenter(c) — a hand's centre write on the pixel lattice: every component rounded.
254
+ * @param {number[]} c — [x, y] or [x, y, z] @returns {number[]}
255
+ */
256
+ function snapCenter(c) {
257
+ _finiteTuple(c, 'snapCenter', [2, 3]);
258
+ return c.map(function (n) { return Math.round(n); });
259
+ }
260
+
223
261
  export {
224
262
  drawnCenter,
225
263
  rectAtCenter,
264
+ snapRectPosition,
265
+ snapCenter,
266
+ zExtent,
226
267
  hasZ,
227
268
  drawnVolume,
228
269
  rayIntersect,
@@ -0,0 +1,60 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * boot-failure.js — THE one boot-failure overlay painter.
5
+ *
6
+ * paintBootFailure(axis, msg) paint a FAIL-LOUD overlay for a boot axis ('engine' | 'renderer')
7
+ * setBootOverlayHost(el) scope later overlays into `el` (the boot's `overlayHost`)
8
+ *
9
+ * A leaf: the boot module (boot.js) and any runner that discovers a boot fault after the boot
10
+ * (FrqtlApp's empty-display-slot diagnosis) paint through this ONE painter.
11
+ */
12
+
13
+ const AXIS_STYLE = {
14
+ engine: { id: 'egpt-engine-error', marker: 'data-egpt-engine-error', bg: '#1a0000', fg: '#ff6b6b', border: '#b6161c' },
15
+ renderer: { id: 'egpt-renderer-error', marker: 'data-egpt-renderer-error', bg: '#000a1a', fg: '#6b9eff', border: '#1640b6' }
16
+ };
17
+ let _overlayHost = null;
18
+
19
+ /** The element the next overlays paint into (null = the full viewport) — set by the boot from its opts. */
20
+ export function setBootOverlayHost(el) { _overlayHost = el || null; }
21
+
22
+ /**
23
+ * THE boot-failure overlay: scoped into the boot's overlay host when one was given, else the full
24
+ * viewport. One overlay per axis (a second failure on the same axis does not stack). Console-loud too.
25
+ *
26
+ * @param {'engine'|'renderer'} axis 'engine' covers every non-renderer boot failure (engine, game layer, sketch)
27
+ * @param {string} msg
28
+ */
29
+ export function paintBootFailure(axis, msg) {
30
+ const st = AXIS_STYLE[axis];
31
+ if (!st) { throw new Error('[boot] paintBootFailure: axis must be engine | renderer (got ' + JSON.stringify(axis) + ')'); }
32
+ if (typeof console !== 'undefined' && console.error) { console.error(msg); }
33
+ function paint() {
34
+ if (_overlayHost) {
35
+ if (_overlayHost.querySelector && _overlayHost.querySelector('[' + st.marker + ']')) { return; }
36
+ const b = document.createElement('div');
37
+ b.setAttribute(st.marker, '1');
38
+ b.setAttribute('data-egpt-boot-error', '1');
39
+ b.style.cssText = 'position:absolute;inset:0;z-index:9998;background:' + st.bg + ';color:' + st.fg + ';' +
40
+ 'font:12px/1.5 ui-monospace,Menlo,Consolas,monospace;padding:14px;white-space:pre-wrap;overflow:auto;' +
41
+ 'border:2px solid ' + st.border + ';';
42
+ b.textContent = msg;
43
+ const cs = getComputedStyle(_overlayHost);
44
+ if (!cs || cs.position === 'static') { _overlayHost.style.position = 'relative'; }
45
+ _overlayHost.appendChild(b);
46
+ return;
47
+ }
48
+ if (document.getElementById(st.id)) { return; }
49
+ const b = document.createElement('div');
50
+ b.id = st.id;
51
+ b.setAttribute('data-egpt-boot-error', '1');
52
+ b.style.cssText = 'position:fixed;inset:0;z-index:99999;background:' + st.bg + ';color:' + st.fg + ';' +
53
+ 'font:13px/1.55 ui-monospace,Menlo,Consolas,monospace;padding:22px;white-space:pre-wrap;overflow:auto;' +
54
+ 'border:2px solid ' + st.border + ';';
55
+ b.textContent = msg;
56
+ (document.body || document.documentElement).appendChild(b);
57
+ }
58
+ if (document.body) { paint(); } else { addEventListener('DOMContentLoaded', paint); }
59
+ }
60
+
@@ -62,6 +62,8 @@
62
62
  */
63
63
 
64
64
  import { ObjectSchema } from './schema/index.js';
65
+ import { kindOf as _kindRow } from './kinds.js';
66
+ import { moduleOfKind } from '../../shim/project/parts-module.js';
65
67
  import { FrqtlCapture } from './instruments/capture.js';
66
68
  import * as FrqtlDisplay from '../compat/renderer/display-slot.js';
67
69
  import * as RendererProviderContract from '../compat/renderer/renderer-provider-contract.js';
@@ -867,9 +869,19 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
867
869
  * THROWS here instead of leaking an unresolvable LO into the scene. `tickFns` are the host
868
870
  * tick-functions the build pushed (an emitter's emit fn, an absorb radiator): what the
869
871
  * declaration owns besides what it draws. THROWS on a re-declaration.
870
- * @param {Object} handle @param {Array} members @param {Array} [tickFns] @returns {Object} handle
871
- */
872
- FrqtlApp.prototype.declare = function (handle, members, tickFns) {
872
+ *
873
+ * `row` = { kind } (ObjectFactory.create): the census fields every declared object carries, read as
874
+ * `d.inParts` / `d.module` off a runner.declared entry (not enumerable, not writable):
875
+ * inParts — true when the object came from the sketch's `export const parts` block (exp.declare),
876
+ * or was written into it since (RecipeContext.toSketch); false for a code declaration.
877
+ * Set only by RecipeContext, the parts block's one owner (_adoptIntoParts)
878
+ * module — the specifier a sketch imports its kind by (lib/shim/project/parts-module.js
879
+ * moduleOfKind over the kind row's `from` and the project files the facade last linked):
880
+ * '@descix/frqtl-web/parts', a project path, or null (an internal kind)
881
+ * @param {Object} handle @param {Array} members @param {Array} [tickFns] @param {{kind: string}} [row]
882
+ * @returns {Object} handle
883
+ */
884
+ FrqtlApp.prototype.declare = function (handle, members, tickFns, row) {
873
885
  if (!handle || typeof handle !== 'object') {
874
886
  throw new TypeError('[FrqtlApp.declare] a declared object is a handle, got ' + typeof handle + '. (FAIL-LOUD.)');
875
887
  }
@@ -893,15 +905,67 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
893
905
  'object. (FAIL-LOUD.)');
894
906
  }
895
907
  }
908
+ var kind = (row && row.kind) || handle._schemaKind;
909
+ var kr = kind ? _kindRow(kind) : null;
910
+ _setCensusRow(handle, {
911
+ inParts: false,
912
+ module: kr ? moduleOfKind(kr.from, this._projectUrls || null) : null
913
+ });
896
914
  return handle;
897
915
  };
898
916
 
917
+ // The census fields of each declared object (FrqtlApp.declare). Keyed by the handle; read through two
918
+ // getters defined on it, so `runner.declared[i].inParts` / `.module` are fields of the one census.
919
+ var _censusRows = new WeakMap();
920
+ function _setCensusRow(handle, r) {
921
+ _censusRows.set(handle, r);
922
+ if (!Object.prototype.hasOwnProperty.call(handle, 'inParts')) {
923
+ Object.defineProperty(handle, 'inParts', { configurable: true, enumerable: false,
924
+ get: function () { var x = _censusRows.get(this); return x ? x.inParts : false; } });
925
+ Object.defineProperty(handle, 'module', { configurable: true, enumerable: false,
926
+ get: function () { var x = _censusRows.get(this); return x ? x.module : null; } });
927
+ }
928
+ }
929
+
930
+ // THE HAND MARK: a declared object a device door wrote since the writer last ran (updateObjectProperty —
931
+ // a drag, a resize, an Inspector or knob write — and placeDeclaredAt — a carry, a lab's hand). The writer
932
+ // (RecipeContext.toSketch) writes, or refuses, ONLY marked objects: a part its own scene code moves (a
933
+ // bullet's flight tick-function) is the run, not a hand.
934
+ function _markHand(app, x) {
935
+ var list = app._declared;
936
+ if (!list || !list.length) { return; } // nothing declared: no object a writer could write
937
+ var d = app.declaredOf(x);
938
+ if (d) { (app._handTouched || (app._handTouched = new Set())).add(d); }
939
+ }
940
+ /** _takeHandTouched() → the declared objects a device door wrote since the last call; the call clears the mark. */
941
+ FrqtlApp.prototype._takeHandTouched = function () {
942
+ var out = this._handTouched || new Set();
943
+ this._handTouched = new Set();
944
+ return out;
945
+ };
946
+
947
+ /**
948
+ * _adoptIntoParts(d) — `d` is an entry of the parts block (its key is its name): declared there (exp.declare)
949
+ * or written there (RecipeContext.toSketch). RecipeContext's alone, the parts block's one owner.
950
+ */
951
+ FrqtlApp.prototype._adoptIntoParts = function (d) {
952
+ var x = _censusRows.get(d);
953
+ if (!x) { throw new Error('[FrqtlApp] _adoptIntoParts: "' + (d && d.name) + '" is not a declared object. (FAIL-LOUD.)'); }
954
+ x.inParts = true;
955
+ };
956
+
957
+ /** beginWorld() — a (re-)inited world has not stepped: its last TickData is gone (worldTick reads 0, repaint draws nothing). */
958
+ FrqtlApp.prototype.beginWorld = function () {
959
+ this._lastTickData = null;
960
+ };
961
+
899
962
  /** clearDeclared() — empty the registry. Called where the world re-inits (FrqtlApp.setup, and the
900
963
  * RecipeContext co-init exp.createCanvas / exp.createScene): a fresh world has no declared objects. */
901
964
  FrqtlApp.prototype.clearDeclared = function () {
902
965
  this._declared = [];
903
966
  this._declaredMembers = new Map();
904
967
  this._declaredTicks = new Map();
968
+ this._handTouched = new Set(); // a fresh world holds no hand
905
969
  };
906
970
 
907
971
  /**
@@ -1077,6 +1141,14 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
1077
1141
  */
1078
1142
  FrqtlApp.prototype.updateObjectProperty = function (lo, prop, value) {
1079
1143
  if (!lo) return;
1144
+ // A parts entry's name IS its key: no door renames it (toSketch refuses, setDeclared throws, and so does this),
1145
+ // whether addressed by its declared handle or by a member LO of it (declaredOf walks the owner chain).
1146
+ var entry = prop === 'name' ? this.declaredOf(lo) : null;
1147
+ if (entry && entry.inParts) {
1148
+ throw new TypeError('[FrqtlApp.updateObjectProperty] "name" of the parts entry "' + entry.name + '": ' +
1149
+ FrqtlApp.RENAME_REFUSAL + ' (FAIL-LOUD.)');
1150
+ }
1151
+ _markHand(this, lo);
1080
1152
 
1081
1153
  // Every shipped object class STAMPS `_schemaKind` (LargeObject, QuantumEmitter,
1082
1154
  // PointSource, Wall, DetectorWall, SphereEmitter, …), so the
@@ -1443,6 +1515,7 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
1443
1515
  * where the object now IS (`centerOf`).
1444
1516
  */
1445
1517
  FrqtlApp.prototype.placeDeclaredAt = function (d, point) {
1518
+ _markHand(this, d);
1446
1519
  this._requireDeclared(d, 'placeDeclaredAt');
1447
1520
  if (!Array.isArray(point) || (point.length !== 2 && point.length !== 3) ||
1448
1521
  !point.every(function (v) { return typeof v === 'number' && isFinite(v); })) {
@@ -1463,9 +1536,10 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
1463
1536
  zHeld = Math.abs(k[2]) > FrqtlApp.Z_HOLD_EPS;
1464
1537
  k = [k[0], k[1], 0];
1465
1538
  }
1466
- this.updateObjectProperty(d, 'rect', L.rectAtCenter(d.rect, k));
1539
+ // A hand places on the pixel lattice (LoDrawnCenter: the position snaps, the size is kept).
1540
+ this.updateObjectProperty(d, 'rect', L.snapRectPosition(L.rectAtCenter(d.rect, k)));
1467
1541
  } else {
1468
- this.updateObjectProperty(d, 'center', k);
1542
+ this.updateObjectProperty(d, 'center', LoDrawnCenter.snapCenter(k));
1469
1543
  }
1470
1544
  return { center: this.centerOf(d), zHeld: zHeld };
1471
1545
  };
@@ -1556,6 +1630,9 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
1556
1630
  /** Beyond this |z| (world px) a carried centre is not expressible by a z-LESS rect (it sits
1557
1631
  * at z-centre 0 — lo-drawn-center): the object stays on the z = 0 plane and says Z HELD. */
1558
1632
  FrqtlApp.Z_HOLD_EPS = 1e-6;
1633
+ /** The one wording of "a parts entry is never renamed by a hand" (updateObjectProperty, RecipeContext). */
1634
+ FrqtlApp.RENAME_REFUSAL = 'the key is the name — a rename is a new key in `parts` (with every reference to it, ' +
1635
+ 'in code) or a fork of the sketch; a hand never writes a `name` into an entry.';
1559
1636
 
1560
1637
  var AIM_OFF = 'off', AIM_ROAMING = 'roaming', AIM_READY = 'grab-ready', AIM_GRABBED = 'grabbed';
1561
1638
 
@@ -2182,9 +2259,10 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
2182
2259
  /**
2183
2260
  * retainSceneReplay(fn) — the ONE retainer of "how this runner's scene was built", so
2184
2261
  * `restart()` can replay it. Whichever path built the scene sets it: `setup(factory, opts)`
2185
- * retains its factory replay; `RecipeContext.build(sketch)` retains `exp.build(sketch)`. A later
2186
- * build replaces the earlier retention (the runner holds ONE scene).
2187
- * @param {Function} fn — replays the scene; its return value is restart()'s.
2262
+ * retains its factory replay; `RecipeContext.build(sketch)` retains its rebuild (a sketch built from
2263
+ * linked text re-links the facade's CURRENT text — what the writer last wrote — and builds that). A
2264
+ * later build replaces the earlier retention (the runner holds ONE scene).
2265
+ * @param {Function} fn — replays the scene; its return value (or the value its promise resolves) is restart()'s.
2188
2266
  */
2189
2267
  FrqtlApp.prototype.retainSceneReplay = function (fn) {
2190
2268
  if (typeof fn !== 'function') {
@@ -2195,25 +2273,24 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
2195
2273
  };
2196
2274
 
2197
2275
  /**
2198
- * restart() — the PAGE-LEVEL restart primitive (presentation pass Item 2,
2199
- * E-P2b): stop → replay the RETAINED scene (`retainSceneReplay` — a setup
2200
- * factory's world + part steps with its ORIGINAL opts, or a sketch's `exp.build(sketch)`
2201
- * on the same runner and canvas) → start. The scene returns to tick 0 IN-PAGE: no reload,
2202
- * so the camera orbit, render options and any host wiring survive. The declared `restart`
2203
- * control (controlAction.js) routes here for every host.
2276
+ * restart() → Promise — the PAGE-LEVEL restart primitive: stop → replay the RETAINED scene
2277
+ * (`retainSceneReplay` — a setup factory's world + part steps with its ORIGINAL opts, a sketch's
2278
+ * rebuild on the same runner and canvas, or — for a sketch built from linked text — a re-link of the
2279
+ * facade's current text, then its build) → start. The scene returns to tick 0 IN-PAGE: no reload, so
2280
+ * the camera orbit, render options and any host wiring survive. The declared `restart` control
2281
+ * (controlAction.js) routes here for every host.
2204
2282
  *
2205
- * Teardown runs EXACTLY ONCE — the replay owns it (`setup()` calls the previous
2206
- * setupResult's `dispose()`; a sketch build tears the previous scene down first —
2207
- * RecipeContext.build), so this method must NOT dispose again (double teardown was the
2208
- * stacking-regression this seam was built to avoid).
2283
+ * ALWAYS a Promise. A replay that returns a plain value (a factory, a function-built sketch) has run
2284
+ * and the loop has restarted before restart() returns; its promise resolves at once. A replay that
2285
+ * returns a promise (a re-link) resolves after the rebuild and the start. Every caller awaits it.
2209
2286
  *
2210
- * DISTINCT BY DESIGN from STUDIO's codegen Restart (AuthorView), which rebuilds
2211
- * a persisted DESIGN and reloads the page: that one is STUDIO chrome and stays
2212
- * where it is. This one replays the already-loaded module.
2287
+ * Teardown runs EXACTLY ONCE — the replay owns it (`setup()` calls the previous setupResult's
2288
+ * `dispose()`; a sketch build tears the previous scene down first — RecipeContext.build), so this
2289
+ * method must NOT dispose again.
2213
2290
  *
2214
- * @returns {*} the replay's result (a setup result, or the sketch's facade).
2215
- * @throws {Error} FAIL-LOUD when nothing built a scene on this runner (there is nothing
2216
- * to replay) — never a silent no-op.
2291
+ * @returns {Promise<*>} the replay's result (a setup result, or the sketch's facade).
2292
+ * @throws {Error} FAIL-LOUD when nothing built a scene on this runner (there is nothing to replay) —
2293
+ * never a silent no-op; a failed replay REJECTS.
2217
2294
  */
2218
2295
  FrqtlApp.prototype.restart = function () {
2219
2296
  if (typeof this._sceneReplay !== 'function') {
@@ -2222,12 +2299,19 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
2222
2299
  '(FAIL-LOUD — a restart with nothing to replay would silently do nothing.)');
2223
2300
  }
2224
2301
  this.stop();
2225
- var result = this._sceneReplay();
2226
- this._lastTickData = null;
2227
- // Always resume: "restart" means run the scene again from tick 0 — a restart
2228
- // that left the loop paused would read as a dead button.
2229
- this.start();
2230
- return result;
2302
+ var self = this;
2303
+ function resume(result) {
2304
+ self.beginWorld();
2305
+ // Always resume: "restart" means run the scene again from tick 0 — a restart
2306
+ // that left the loop paused would read as a dead button.
2307
+ self.start();
2308
+ return result;
2309
+ }
2310
+ var result;
2311
+ try { result = this._sceneReplay(); }
2312
+ catch (err) { return Promise.reject(err); }
2313
+ if (result && typeof result.then === 'function') { return result.then(resume); }
2314
+ return Promise.resolve(resume(result));
2231
2315
  };
2232
2316
 
2233
2317
  // ── Renderer resolution ────────────────────────────────────────────────
@@ -2493,6 +2577,28 @@ import { paintBootFailure } from '../experiments/_shared/boot-failure.js';
2493
2577
  return this._tickSuspend;
2494
2578
  };
2495
2579
 
2580
+ /**
2581
+ * runner.worldTick — READ-ONLY: the tick of THIS world: 0 before its first step, else the last delivered tick.
2582
+ * The 0 is a stated fact, not a default: `_lastTickData` is null until the world's first step, and beginWorld()
2583
+ * (the co-init and restart()) is the one writer of that null. It does NOT read `universe.tick`: the compat wrapper
2584
+ * keeps that field across a re-init until the next doTick, so after a restart it still answers the previous
2585
+ * world's tick. Every runner holds a world (its universe is built at construction), whether a sketch built it
2586
+ * through the replay door or a page built it by hand, so this always answers. THROWS when a delivered tick is not
2587
+ * a whole number ≥ 0.
2588
+ */
2589
+ Object.defineProperty(FrqtlApp.prototype, 'worldTick', {
2590
+ configurable: true,
2591
+ get: function () {
2592
+ if (this._lastTickData === null) { return 0; }
2593
+ var t = this._lastTickData && this._lastTickData.universeData && this._lastTickData.universeData.tick;
2594
+ if (!(typeof t === 'number' && t >= 0 && t % 1 === 0)) {
2595
+ throw new Error('[FrqtlApp.worldTick] the last delivered TickData carries tick ' + String(t) +
2596
+ ', not a whole number ≥ 0. (FAIL-LOUD.)');
2597
+ }
2598
+ return t;
2599
+ }
2600
+ });
2601
+
2496
2602
  /** @returns {boolean} whether the tick clock is currently held. */
2497
2603
  Object.defineProperty(FrqtlApp.prototype, 'ticksSuspended', {
2498
2604
  configurable: true,
@@ -95,8 +95,7 @@ import { V1_REPLACEMENTS } from './v1-replacements.js';
95
95
  // `preset` is expanded by the OBJECT constructor (Wall.expandWallPreset), not by
96
96
  // a builder — it reaches the constructor through the forwarded props bag.
97
97
  'preset',
98
- // Codegen round-trip adapters: a PointSource's construction-only opts, which codegen
99
- // writes back onto its config (lib/features/ide/codegen.js).
98
+ // A PointSource's construction-only opts (its builder reads them; no descriptor field).
100
99
  '_wavelength', '_optColor', '_emergentPhysics'
101
100
  ];
102
101
 
@@ -187,7 +186,7 @@ import { V1_REPLACEMENTS } from './v1-replacements.js';
187
186
  * returns (never one of its members) is recorded in `ctx.runner.declared` with the census LOs
188
187
  * the build added as its members and the host tick-functions the build pushed
189
188
  * (`FrqtlApp.declare` — this is its ONE call site). What a
190
- * declaring call (`exp.addObject`, a palette drop, a codegen replay) produced is what STUDIO
189
+ * declaring call (`exp.addObject`, `exp.declare`, a palette drop) produced is what STUDIO
191
190
  * holds; a part's composition is never declared.
192
191
  *
193
192
  * FAIL-LOUD: an unknown kind THROWS naming the registered kinds and, when
@@ -264,11 +263,11 @@ import { V1_REPLACEMENTS } from './v1-replacements.js';
264
263
  'scene. (FAIL-LOUD.)');
265
264
  }
266
265
  // ... and the host tick-functions it pushed are the declaration's too (an emitter's emit
267
- // fn, an absorb radiator): behaviour the declaration owns, so codegen may write it down.
266
+ // fn, an absorb radiator): behaviour the declaration owns.
268
267
  var tfsAfter = (runner.universe && Array.isArray(runner.universe.experimentTickFunctions))
269
268
  ? runner.universe.experimentTickFunctions : [];
270
269
  var ticks = tfsAfter.filter(function (fn) { return !ticksBefore.has(fn); });
271
- runner.declare(handle, members, ticks);
270
+ runner.declare(handle, members, ticks, { kind: kind });
272
271
  return handle;
273
272
  },
274
273