@descix/frqtl-node 0.6.0-p1.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 (80) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +6 -4
  3. package/dist/dist-manifest.json +71 -65
  4. package/dist/eyes/index.js +5 -2
  5. package/dist/fat/pkg-node/package.json +2 -10
  6. package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
  7. package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
  8. package/dist/frqtl/objects/FrqtlApp.js +33 -6
  9. package/dist/frqtl/objects/RecipeContext.js +284 -13
  10. package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
  11. package/dist/frqtl/objects/instruments/capture.js +1 -24
  12. package/dist/frqtl/objects/instruments/hosted-eyes.js +216 -29
  13. package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
  14. package/dist/frqtl/objects/record/Record.js +346 -0
  15. package/dist/frqtl/objects/record/open-record.js +22 -0
  16. package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
  17. package/dist/frqtl/objects/schema/scopes.js +2 -2
  18. package/dist/frqtl/objects/setup/_worldInit.js +1 -2
  19. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
  20. package/dist/frqtl/parts/CameraControls.js +8 -0
  21. package/dist/frqtl/parts/DetectorWall.js +1 -1
  22. package/dist/frqtl/parts/FrqtlVision.js +21 -6
  23. package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
  24. package/dist/frqtl/parts/LargeObject.js +2 -2
  25. package/dist/frqtl/parts/LiveChart.js +2 -2
  26. package/dist/frqtl/parts/LogicGate.js +3 -3
  27. package/dist/frqtl/parts/OvenBox.js +1 -1
  28. package/dist/frqtl/parts/PointSource.js +1 -1
  29. package/dist/frqtl/parts/SlitLightSource.js +1 -1
  30. package/dist/frqtl/parts/SlitScreen.js +1 -1
  31. package/dist/frqtl/parts/SphereEmitter.js +12 -12
  32. package/dist/frqtl/parts/Trace.js +1 -1
  33. package/dist/frqtl/parts/Tunnel.js +11 -11
  34. package/dist/frqtl/parts/Wall.js +20 -10
  35. package/dist/frqtl/scenes/circuit-sat.js +1 -0
  36. package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
  37. package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
  38. package/dist/frqtl/scenes/mirror-route.js +96 -79
  39. package/dist/shim/bundle/index.js +118 -96
  40. package/dist/shim/bundle/indexHtml.js +66 -39
  41. package/dist/shim/controls/index.js +1 -1
  42. package/dist/shim/engine/artifact-shell.js +1 -1
  43. package/dist/shim/engine/router.js +3 -3
  44. package/dist/shim/host/host-view.js +228 -0
  45. package/dist/shim/host/index.js +18 -0
  46. package/dist/shim/index.js +3 -5
  47. package/dist/shim/notebook/format.js +1 -1
  48. package/dist/shim/notebook/kernel.js +1 -1
  49. package/dist/shim/notebook/setup-builder.js +2 -2
  50. package/dist/shim/notebook/store.js +2 -3
  51. package/dist/shim/package.json +1 -0
  52. package/dist/shim/page-params/index.js +2 -30
  53. package/dist/shim/project/fork.js +293 -0
  54. package/dist/shim/project/identity.js +3 -1
  55. package/dist/shim/project/index.js +16 -3
  56. package/dist/shim/project/link.js +224 -46
  57. package/dist/shim/project/manifest.js +151 -0
  58. package/dist/shim/project/parts-writer.js +68 -7
  59. package/dist/shim/project/read-text.js +43 -0
  60. package/dist/shim/project/record-format.js +345 -0
  61. package/dist/shim/project/specifiers.js +104 -15
  62. package/dist/shim/registry/exhibits.js +15 -34
  63. package/dist/shim/registry/index.js +3 -3
  64. package/dist/shim/registry/scene-roster.js +35 -0
  65. package/dist/shim/registry/scene-source.js +48 -64
  66. package/dist/shim/registry/walkthrough.js +6 -6
  67. package/dist/shim/render-state/apply.js +66 -31
  68. package/dist/shim/render-state/index.js +6 -2
  69. package/dist/shim/runtime/index.js +4 -4
  70. package/dist/shim/storage/codec-chains.js +88 -0
  71. package/dist/shim/storage/folders.js +205 -0
  72. package/dist/shim/storage/index.js +64 -8
  73. package/dist/shim/storage/make-namespace-store.js +5 -5
  74. package/llms.txt +4 -3
  75. package/package.json +19 -1
  76. package/dist/shim/bundle/frqnb.js +0 -112
  77. package/dist/shim/bundle/manifest.js +0 -95
  78. package/dist/shim/registry/experiments.js +0 -215
  79. package/dist/shim/storage/modules.js +0 -249
  80. package/dist/shim/storage/projects.js +0 -150
@@ -74,7 +74,12 @@ import { ObjectSchema } from './schema/index.js';
74
74
  import { kindOf } from './kinds.js';
75
75
  import { linkProject } from '../../shim/project/link.js';
76
76
  import { patchParts, PARTS_EXPORT, entryKeys, entryLiteral } from '../../shim/project/parts-writer.js';
77
- import { moduleOfKind } from '../../shim/project/parts-module.js';
77
+ import { moduleOfKind, PARTS_SPECIFIER } from '../../shim/project/parts-module.js';
78
+ import { forkPartText, FORK_REFUSED } from '../../shim/project/fork.js';
79
+ import { readText } from '../../shim/project/read-text.js';
80
+ import { packageRoot } from '../compat/package-root.js';
81
+ import { MANIFEST_FILE } from '../../shim/project/manifest.js';
82
+ import { Record, openRecordOf } from './record/Record.js';
78
83
 
79
84
  /** The world-init contract (objects/setup/_worldInit.js). */
80
85
  function _worldInit() {
@@ -123,6 +128,8 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
123
128
  this._parts = new Map(); // declared object → { key, baseline } — the objects in `parts`
124
129
  this._code = new Map(); // declared object → baseline — the code declarations at build end
125
130
  this._declaredOnce = false;
131
+ this._record = null; // the open Record (exp.startRecord), or null
132
+ this._reauthored = null; // the Record the last re-authoring closed (startRecord checks its close was heard)
126
133
  }
127
134
 
128
135
  function _requireWorld(self) {
@@ -195,6 +202,7 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
195
202
  }
196
203
  // A (re-)inited world has no declared objects; the recipe re-declares what it builds.
197
204
  self._runner.clearDeclared();
205
+ self._runner.beginWorld(); // a (re-)inited world has not stepped
198
206
  W.applyPreInit(cfg, u); // pre-init world keys (iframeIntervalSeconds) — the engine reads them at init
199
207
  u.init.apply(u, args);
200
208
  self._world = Object.freeze({ renderer: renderer, w: w, h: h, d: renderer === 'three' ? d : null,
@@ -300,7 +308,7 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
300
308
  }
301
309
  var obj = self.addObject(Kind, Object.assign({ name: key }, _plain(props)));
302
310
  self._runner._adoptIntoParts(obj);
303
- self._parts.set(obj, { key: key, baseline: _capture(self, obj) });
311
+ self._parts.set(obj, { key: key, baseline: _capture(self, obj), kind: self._runner.kindOf(obj) });
304
312
  out[key] = obj;
305
313
  });
306
314
  return Object.freeze(out);
@@ -366,6 +374,22 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
366
374
  return props;
367
375
  }
368
376
 
377
+ /**
378
+ * Spell a touched `center` in the entry's OWN arity: two numbers when the entry
379
+ * spells two (a literal, or an array expression — entryLiteral's `length`) and the live z is 0; otherwise the
380
+ * live [x, y, z] (an entry spelled three, a 3D hand off z 0, an entry whose center is not an array). The
381
+ * read-back (_verifyWritten) guards every case. Mutates and returns `props`.
382
+ */
383
+ function _spellCenter(text, name, props) {
384
+ var v = props.center;
385
+ if (!Array.isArray(v) || v.length !== 3 || v[2] !== 0) { return props; }
386
+ if (entryKeys(text, name).indexOf('center') === -1) { return props; }
387
+ var r = entryLiteral(text, name, 'center');
388
+ var n = r.literal ? (Array.isArray(r.value) ? r.value.length : null) : r.length;
389
+ if (n === 2) { props.center = [v[0], v[1]]; }
390
+ return props;
391
+ }
392
+
369
393
  /**
370
394
  * The post-patch check: what the patched text now declares for entry `name`, read back (entryLiteral) for every
371
395
  * written key and folded with the baseline for every unwritten one, must equal the live capture — `rect` in its
@@ -379,6 +403,8 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
379
403
  Object.keys(written).forEach(function (k) {
380
404
  var r = entryLiteral(text, name, k);
381
405
  decl[k] = r.literal ? r.value : undefined;
406
+ // A two-number center declares z 0 (the reading placeDeclaredAt gives an omitted z).
407
+ if (k === 'center' && Array.isArray(decl[k]) && decl[k].length === 2) { decl[k] = decl[k].concat([0]); }
382
408
  });
383
409
  function refuse(k, declared, liveV) {
384
410
  refused.push({ op: 'set', name: name, key: k, reason: 'the written text declares ' + _JSON(declared) + ' but the live object is ' +
@@ -436,9 +462,11 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
436
462
  * changes patchParts' changes (a `literalized` set names the expression it replaced)
437
463
  * refused every hand not written, { op, name, key?, reason }
438
464
  */
439
- RecipeContext.prototype.toSketch = async function () {
465
+ RecipeContext.prototype.toSketch = async function (opts) {
440
466
  _requireWorld(this);
441
467
  _requireHeld(this, 'toSketch');
468
+ var by = this._record ? Record.requireBy(opts && opts.by, 'RecipeContext.toSketch') : null;
469
+ if (this._record) { this._record._admit('RecipeContext.toSketch'); }
442
470
  var self = this;
443
471
  var runner = this._runner;
444
472
  var declared = runner.declared;
@@ -447,7 +475,7 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
447
475
  var adopted = [];
448
476
  var hands = runner._takeHandTouched();
449
477
  this._parts.forEach(function (row, obj) {
450
- if (declared.indexOf(obj) === -1) { edits.push({ op: 'remove', name: row.key }); return; }
478
+ if (declared.indexOf(obj) === -1) { edits.push({ op: 'remove', name: row.key, kind: row.kind, baseline: row.baseline }); return; }
451
479
  if (!hands.has(obj)) { return; }
452
480
  var kind = runner.kindOf(obj);
453
481
  var t = _touched(kind, row.baseline, _capture(self, obj));
@@ -459,8 +487,10 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
459
487
  refused.push({ op: 'set', name: row.key, key: k, reason: 'the live object no longer reports "' + k + '" — there is no value to write.' });
460
488
  });
461
489
  if (Object.keys(t.props).length) {
490
+ var raw = _plain(t.props); // the capture form, before the entry's spelling (the Record's row values)
462
491
  _spell(self._project.files[self._project.entry], row.key, kind, t.props, row.baseline);
463
- if (Object.keys(t.props).length) { edits.push({ op: 'set', name: row.key, props: t.props, obj: obj, kind: kind, baseline: row.baseline }); }
492
+ _spellCenter(self._project.files[self._project.entry], row.key, t.props);
493
+ if (Object.keys(t.props).length) { edits.push({ op: 'set', name: row.key, props: t.props, obj: obj, kind: kind, baseline: row.baseline, raw: raw }); }
464
494
  }
465
495
  });
466
496
  this._code.forEach(function (baseline, obj) {
@@ -487,7 +517,9 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
487
517
  });
488
518
  var entry = this._project.entry;
489
519
  var r = patchParts(this._project.files[entry], edits.map(function (e) {
490
- return e.op === 'set' ? { op: 'set', name: e.name, props: e.props } : e;
520
+ if (e.op === 'set') { return { op: 'set', name: e.name, props: e.props }; }
521
+ if (e.op === 'remove') { return { op: 'remove', name: e.name }; }
522
+ return { op: 'add', name: e.name, Kind: e.Kind, module: e.module, props: e.props };
491
523
  }), { entry: entry });
492
524
  // Never decide it succeeded without asking: read the patched text back against each moved object.
493
525
  edits.forEach(function (e) {
@@ -511,11 +543,76 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
511
543
  var key = obj.name || runner.kindOf(obj);
512
544
  if (refusedAdd[key]) { return; }
513
545
  runner._adoptIntoParts(obj);
514
- self._parts.set(obj, { key: key, baseline: _capture(self, obj) });
546
+ self._parts.set(obj, { key: key, baseline: _capture(self, obj), kind: runner.kindOf(obj) });
515
547
  });
516
- return { files: Object.assign({}, files), changes: r.changes, refused: refused.concat(r.refused) };
548
+ var allRefused = refused.concat(r.refused);
549
+ if (this._record) { _recordRows(this, edits, allRefused, by); }
550
+ return { files: Object.assign({}, files), changes: r.changes, refused: allRefused };
517
551
  };
518
552
 
553
+ /**
554
+ * The Record's rows for one toSketch, from the writer's own edits as WRITTEN (a refused edit, or a refused key
555
+ * of one, makes no row), in edit order (R3). A `set` whose touched keys include the part's position authority
556
+ * (runner._placementAuthority: `center`, or `rect` with its derivedFrom reads) is a `move` with that ONE key in
557
+ * its capture form; its other touched keys are one `setDeclared` row. `add` → [kind, props as written];
558
+ * `remove` → [kind, the baseline's writable props].
559
+ */
560
+ function _recordRows(self, edits, refused, by) {
561
+ var runner = self._runner;
562
+ var rec = self._record;
563
+ function refusedKeys(name, op) {
564
+ var keys = {};
565
+ var all = false;
566
+ refused.forEach(function (x) {
567
+ if (x.name !== name || x.op !== op) { return; }
568
+ if (x.key === undefined) { all = true; } else { keys[x.key] = true; }
569
+ });
570
+ return all ? null : keys;
571
+ }
572
+ function writableProps(kind, bag) {
573
+ var w = _writable(kind);
574
+ var out = {};
575
+ Object.keys(bag).forEach(function (k) { if (w[k] && k !== 'name') { out[k] = _plain(bag[k]); } });
576
+ return out;
577
+ }
578
+ edits.forEach(function (e) {
579
+ if (e.op === 'add') {
580
+ if (refusedKeys(e.name, 'add') === null) { return; }
581
+ rec._row({ name: e.name, op: 'add', by: by, from: null, to: [e.Kind, _plain(e.props)] });
582
+ return;
583
+ }
584
+ if (e.op === 'remove') {
585
+ if (refusedKeys(e.name, 'remove') === null) { return; }
586
+ rec._row({ name: e.name, op: 'remove', by: by, from: [e.kind, writableProps(e.kind, e.baseline)], to: null });
587
+ return;
588
+ }
589
+ var bad = refusedKeys(e.name, 'set');
590
+ if (bad === null) { return; }
591
+ var raw = e.raw;
592
+ var auth = (raw.center !== undefined || raw.rect !== undefined) ? runner._placementAuthority(e.obj) : null;
593
+ var authReads = {};
594
+ if (auth) {
595
+ authReads[auth] = true;
596
+ ObjectSchema.derivedReads(e.kind).forEach(function (d) { if (d.field === auth) { authReads[d.name] = true; } });
597
+ }
598
+ var moveRefused = Object.keys(bad).some(function (k) { return authReads[k]; });
599
+ if (auth && raw[auth] !== undefined && !moveRefused) {
600
+ var from = {}, to = {};
601
+ from[auth] = _plain(e.baseline[auth]);
602
+ to[auth] = _plain(raw[auth]);
603
+ rec._row({ name: e.name, op: 'move', by: by, from: from, to: to });
604
+ }
605
+ var pf = {}, pt = {}, any = false;
606
+ Object.keys(raw).forEach(function (k) {
607
+ if (k === auth || bad[k]) { return; }
608
+ pf[k] = e.baseline[k] === undefined ? null : _plain(e.baseline[k]);
609
+ pt[k] = _plain(raw[k]);
610
+ any = true;
611
+ });
612
+ if (any) { rec._row({ name: e.name, op: 'setDeclared', by: by, from: pf, to: pt }); }
613
+ });
614
+ }
615
+
519
616
  /** The names of declared objects a hand marked whose live capture differs from their baseline (not yet written). */
520
617
  function _unwrittenHands(self) {
521
618
  var runner = self._runner;
@@ -553,11 +650,15 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
553
650
  * setOrbit) are kept; `kept` lists 'cameraPose' only when a pose was captured and re-applied. A `rect` /
554
651
  * `center` patch sets where the entry STARTS. The loop's run state is kept. REFUSES (throws) while a hand
555
652
  * made since the last toSketch is unwritten (write it first: await exp.toSketch()), and a `name` patch (the
556
- * key is the name).
653
+ * key is the name). `from` and `to` are the patched keys' capture (ObjectSchema.captureProps) before and after the
654
+ * re-run — one form on both sides (a z-slab wall's rect is 6 numbers even when the patch typed 4); the patch as
655
+ * typed is in `files`. With a Record open, its `setDeclared` row carries the same `from`/`to`, written after the re-run.
557
656
  */
558
- RecipeContext.prototype.setDeclared = async function (name, patch) {
657
+ RecipeContext.prototype.setDeclared = async function (name, patch, opts) {
559
658
  _requireWorld(this);
560
659
  _requireHeld(this, 'setDeclared');
660
+ var by = this._record ? Record.requireBy(opts && opts.by, 'RecipeContext.setDeclared') : null;
661
+ if (this._record) { this._record._admit('RecipeContext.setDeclared'); }
561
662
  var runner = this._runner;
562
663
  var obj = null;
563
664
  this._parts.forEach(function (row, o) { if (row.key === name) { obj = o; } });
@@ -615,7 +716,117 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
615
716
  runner.cameraControl.setOrbit(pose);
616
717
  kept.push('cameraPose');
617
718
  }
618
- return { set: name, from: from, to: _plain(patch), tick: 0, kept: kept, files: Object.assign({}, files) };
719
+ // `to` is what the re-run declared, in the capture form `from` is in (the patch as typed is in the text).
720
+ var rebuilt = null;
721
+ this._parts.forEach(function (row, o) { if (row.key === name) { rebuilt = o; } });
722
+ if (!rebuilt) {
723
+ throw new Error('[RecipeContext.setDeclared] the re-run declared no "' + name + '" — the patched text dropped the entry. (FAIL-LOUD.)');
724
+ }
725
+ var after = _capture(this, rebuilt);
726
+ var to = {};
727
+ Object.keys(patch).forEach(function (k) { to[k] = after[k] === undefined ? null : _plain(after[k]); });
728
+ if (this._record) { this._record._row({ name: name, op: 'setDeclared', by: by, from: _plain(from), to: _plain(to) }); }
729
+ return { set: name, from: from, to: to, tick: 0, kept: kept, files: Object.assign({}, files) };
730
+ };
731
+
732
+ /**
733
+ * exp.fork(name, NewKind[, { by }]) → Promise<{ fork, file, from, files }> — copy the part file of `inParts` object
734
+ * `name`'s kind into the project as `parts/<NewKind>.js` (lib/shim/project/fork.js forkPartText: the class renamed,
735
+ * its imports rewritten to package specifiers, every other byte the source's), point the entry at the fork (the
736
+ * parts writer's `kind` edit: the Kind identifier and one import line), and re-run the scene IN THIS DOCUMENT
737
+ * through the one restart door, as setDeclared does: the seed and the camera pose are kept, the loop's run state
738
+ * is kept. The source text is the exact bytes the kind's `from` names: a package part's file (read through
739
+ * read-text.js readText), or a project fork's held file. The held project is updated BEFORE the re-run, so an open
740
+ * Record stays open; with one open, `by` is required and its `fork` row ({ from: <kind>, to: { kind, file } }) is
741
+ * written after the re-run. REFUSES (throws, the held text unchanged): a name that is not an `inParts` object, a
742
+ * hand made since the last toSketch that is unwritten, a kind with no file a fork can copy, every forkPartText
743
+ * refusal, a writer refusal, and a re-run whose entry is not the fork.
744
+ */
745
+ /**
746
+ * The fork's plan for `inParts` object `name`, before any read: { obj, from, row, module }. THROWS the refusal
747
+ * fork() and forkRefusal() both name: not an `inParts` object, an unwritten hand, a kind FORK_REFUSED names
748
+ * (lib/shim/project/fork.js, the census reason), a kind with no part file a fork can copy.
749
+ */
750
+ function _forkPlan(self, name) {
751
+ var runner = self._runner;
752
+ var obj = null;
753
+ self._parts.forEach(function (row, o) { if (row.key === name) { obj = o; } });
754
+ if (!obj || runner.declared.indexOf(obj) === -1) {
755
+ throw new Error('[RecipeContext.fork] "' + name + '" is not an object of this sketch\'s `parts` block' +
756
+ (self._code.size ? ' (code declarations: ' + Array.from(self._code.keys()).map(function (o) { return o.name; }).join(', ') +
757
+ ' — ' + OUTSIDE_PARTS + ')' : '') + '. (FAIL-LOUD.)');
758
+ }
759
+ var unwritten = _unwrittenHands(self);
760
+ if (unwritten.length) {
761
+ throw new Error('[RecipeContext.fork] "' + unwritten.join('", "') + '" carries a hand the sketch text does ' +
762
+ 'not hold yet — write the hand first: await exp.toSketch(). (FAIL-LOUD.)');
763
+ }
764
+ var from = runner.kindOf(obj);
765
+ if (Object.prototype.hasOwnProperty.call(FORK_REFUSED, from)) {
766
+ throw new Error('[RecipeContext.fork] ' + from + ' (the kind of "' + name + '") is not forkable — ' + FORK_REFUSED[from] + '. (FAIL-LOUD.)');
767
+ }
768
+ var row = kindOf(from);
769
+ var module = row ? moduleOfKind(row.from, self._urls) : null;
770
+ if (module !== PARTS_SPECIFIER && !(module && Object.prototype.hasOwnProperty.call(self._project.files, module))) {
771
+ throw new Error('[RecipeContext.fork] ' + from + ' (the kind of "' + name + '") has no part file a fork can copy — a fork ' +
772
+ 'copies a part of @descix/frqtl-web/parts or a part file of this project. (FAIL-LOUD.)');
773
+ }
774
+ return { obj: obj, from: from, row: row, module: module };
775
+ }
776
+
777
+ /**
778
+ * exp.forkRefusal(name) → null when exp.fork(name, …) may run, else the refusal it would throw (one sentence). A
779
+ * host's "Fork into project" control reads it for its disabled state and title; fork() runs the same plan.
780
+ */
781
+ RecipeContext.prototype.forkRefusal = function (name) {
782
+ _requireWorld(this);
783
+ _requireHeld(this, 'forkRefusal');
784
+ try { _forkPlan(this, name); return null; }
785
+ catch (err) { return String(err && err.message).replace(/^\[RecipeContext\.fork\] /, '').replace(/ \(FAIL-LOUD\.\)$/, ''); }
786
+ };
787
+
788
+ RecipeContext.prototype.fork = async function (name, NewKind, opts) {
789
+ _requireWorld(this);
790
+ _requireHeld(this, 'fork');
791
+ var by = this._record ? Record.requireBy(opts && opts.by, 'RecipeContext.fork') : null;
792
+ if (this._record) { this._record._admit('RecipeContext.fork'); }
793
+ var runner = this._runner;
794
+ var plan = _forkPlan(this, name);
795
+ var from = plan.from, row = plan.row, module = plan.module;
796
+ var text, libPath;
797
+ if (module === PARTS_SPECIFIER) {
798
+ libPath = row.from.slice(packageRoot().length);
799
+ try { text = await readText(row.from); }
800
+ catch (err) { throw new Error('[RecipeContext.fork] the part file of ' + from + ': ' + ((err && err.message) || err) + '. (FAIL-LOUD.)'); }
801
+ } else {
802
+ libPath = module;
803
+ text = this._project.files[module];
804
+ }
805
+ var forked = forkPartText(text, { from: from, to: NewKind, libPath: libPath, files: this._project.files });
806
+ var file = 'parts/' + NewKind + '.js';
807
+ var entry = this._project.entry;
808
+ var r = patchParts(this._project.files[entry], [{ op: 'kind', name: name, Kind: NewKind, module: file }], { entry: entry });
809
+ if (r.refused.length) {
810
+ throw new Error('[RecipeContext.fork] the sketch text refused the fork: ' +
811
+ r.refused.map(function (x) { return x.reason; }).join('; ') + ' (FAIL-LOUD.)');
812
+ }
813
+ var files = Object.assign({}, this._project.files);
814
+ files[file] = forked.text;
815
+ files[entry] = r.text;
816
+ this._project = Object.assign({}, this._project, { files: files });
817
+ var pose = _cameraPose(runner);
818
+ var wasRunning = !!runner._isLooping;
819
+ await runner.restart();
820
+ if (!wasRunning) { runner.stop(); }
821
+ if (pose && runner.cameraControl && typeof runner.cameraControl.setOrbit === 'function') { runner.cameraControl.setOrbit(pose); }
822
+ var rebuilt = null;
823
+ this._parts.forEach(function (row2, o) { if (row2.key === name) { rebuilt = o; } });
824
+ if (!rebuilt || runner.kindOf(rebuilt) !== NewKind) {
825
+ throw new Error('[RecipeContext.fork] the re-run declared "' + name + '" as ' + (rebuilt ? runner.kindOf(rebuilt) : 'nothing') +
826
+ ', not the fork ' + NewKind + '. (FAIL-LOUD.)');
827
+ }
828
+ if (this._record) { this._record._row({ name: name, op: 'fork', by: by, from: from, to: { kind: NewKind, file: file } }); }
829
+ return { fork: NewKind, file: file, from: from, files: Object.assign({}, files) };
619
830
  };
620
831
 
621
832
  RecipeContext.prototype.addTickFunction = function (fn) {
@@ -683,8 +894,8 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
683
894
  var linked = null;
684
895
  if (sketch && typeof sketch === 'object' && typeof sketch.buildExperiment === 'function') {
685
896
  if (!sketch.project || typeof sketch.project !== 'object' || !sketch.project.files || typeof sketch.project.entry !== 'string') {
686
- throw new TypeError('[RecipeContext.build] build(linked): linked is linkProject\'s result { buildExperiment, files, ' +
687
- 'urls, project } (lib/shim/project/link.js). (FAIL-LOUD.)');
897
+ throw new TypeError('[RecipeContext.build] build(linked): linked is linkProject\'s result (its contract: ' +
898
+ 'lib/shim/project/link.js). (FAIL-LOUD.)');
688
899
  }
689
900
  linked = sketch;
690
901
  sketch = linked.buildExperiment;
@@ -693,6 +904,12 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
693
904
  'linkProject\'s result (got ' + typeof sketch + '). (FAIL-LOUD.)');
694
905
  }
695
906
  if (typeof sketch === 'function') {
907
+ // ONE Record per booted world: a build of text that is not the held text re-authors it, and closes the
908
+ // open Record with the world before (a rebuild of the same text — restart, setDeclared's re-run — does not).
909
+ if (this._record && !(linked && this._project && _sameFiles(linked.project.files, this._project.files))) {
910
+ this._reauthored = this._record;
911
+ this._record._closeOnReauthor(this._project);
912
+ }
696
913
  this._buildFn = sketch;
697
914
  this._project = linked ? linked.project : null;
698
915
  this._urls = linked ? (linked.urls || null) : null;
@@ -719,6 +936,60 @@ import { moduleOfKind } from '../../shim/project/parts-module.js';
719
936
  return this;
720
937
  };
721
938
 
939
+ /** Two file maps hold the same paths with byte-equal text. */
940
+ function _sameFiles(a, b) {
941
+ var ka = Object.keys(a), kb = Object.keys(b);
942
+ if (ka.length !== kb.length) { return false; }
943
+ return ka.every(function (k) { return Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]; });
944
+ }
945
+
946
+ /**
947
+ * exp.startRecord() → Promise<Record> — open a Record (lib/frqtl/objects/record/Record.js) on this facade's world.
948
+ * REFUSES (rejects): a second open Record (one per facade, one per runner); a held project with no frqtl.json
949
+ * (a Record snapshots a folder); a hand the sketch text does not hold yet (as setDeclared). While it is open,
950
+ * toSketch({ by }) and setDeclared(name, patch, { by }) name who acted.
951
+ */
952
+ RecipeContext.prototype.startRecord = async function () {
953
+ _requireWorld(this);
954
+ _requireHeld(this, 'startRecord');
955
+ if (this._record) {
956
+ throw new Error('[RecipeContext.startRecord] a Record is already open on this facade (one per booted world) — ' +
957
+ 'await rec.stop() first. (FAIL-LOUD.)');
958
+ }
959
+ // A re-authoring closed the previous Record; if that close was REFUSED and nobody read it, its rows would be lost
960
+ // without a word: refuse until the host reads `rec.closed` (the rejection carries err.record).
961
+ var last = this._reauthored;
962
+ if (last) {
963
+ await last._settled;
964
+ if (last._refused && !last._closedRead) {
965
+ var err = new Error('[RecipeContext.startRecord] the previous Record of this facade was closed by a re-authoring and ' +
966
+ 'REFUSED (' + last._refused.message + ') — nobody read rec.closed, so its ' + last._refused.record.changes.length +
967
+ ' row(s) would be lost: read rec.closed (its rejection carries err.record = { changes, clips }) first. (FAIL-LOUD.)');
968
+ err.record = last._refused.record;
969
+ throw err;
970
+ }
971
+ this._reauthored = null;
972
+ }
973
+ if (openRecordOf(this._runner)) {
974
+ throw new Error('[RecipeContext.startRecord] another facade holds an open Record on this runner. (FAIL-LOUD.)');
975
+ }
976
+ if (!Object.prototype.hasOwnProperty.call(this._project.files, MANIFEST_FILE)) {
977
+ throw new Error('[RecipeContext.startRecord] a Record snapshots a folder; boot it as one — the held project carries ' +
978
+ 'no ' + MANIFEST_FILE + ': add files[\'' + MANIFEST_FILE + '\'] = serializeManifest(manifestFor({ name, entry: SKETCH_ENTRY })) ' +
979
+ '(all from ./project) to the files you boot. (FAIL-LOUD.)');
980
+ }
981
+ var unwritten = _unwrittenHands(this);
982
+ if (unwritten.length) {
983
+ throw new Error('[RecipeContext.startRecord] "' + unwritten.join('", "') + '" carries a hand the sketch text does ' +
984
+ 'not hold yet — write the hand first: await exp.toSketch(). (FAIL-LOUD.)');
985
+ }
986
+ var rec = new Record(this);
987
+ this._record = rec;
988
+ try { await rec._ready(this._project); }
989
+ catch (e) { rec._detach(); throw e; }
990
+ return rec;
991
+ };
992
+
722
993
  RecipeContext.prototype.run = function (buildFn) {
723
994
  if (buildFn && typeof buildFn === 'function' && !this._built) { this.build(buildFn); }
724
995
  _requireWorld(this);
@@ -128,7 +128,7 @@ import { compileWallV2 } from '../objects/actionCompiler.js';
128
128
  }
129
129
 
130
130
  // ── Presets (B6) — pure CONSTRUCTION sugar expanding to the v2 descriptor fields.
131
- // The recipe/codegen round-trips the EXPANDED fields (never the preset name).
131
+ // The parts writer captures the EXPANDED fields (never the preset name).
132
132
  //
133
133
  // THE NAMING RULE: a preset's NAME MUST ASSERT ITS BEHAVIOR.
134
134
  // `collisionBehavior:'nothing'` (pass-through) belongs ONLY on a preset whose name SAYS
@@ -71,29 +71,6 @@ import { loadedWasmSha256 } from '../../compat/providers/wasm-load-browser.js';
71
71
  import * as FrqtlDisplay from '../../compat/renderer/display-slot.js';
72
72
 
73
73
 
74
- /**
75
- * The experiment identity the page ALREADY carries, ferried — not a second authority.
76
- *
77
- * The experiments registry DEFINES an experiment id as "the directory name under
78
- * experiments/" (`experimentById`), and a canonical page is served from
79
- * `…/experiments/<id>/index.html`. So the path segment IS the registry id, and reading it
80
- * ferries the existing identity rather than minting a parallel one. The registry module
81
- * is a registry, not a page fact, which is why the id is taken from the URL the registry's
82
- * own ids are defined by.
83
- *
84
- * Fails SOFT to null (no DOM, a notebook realm, an ad-hoc page): `sourceRef: null` is the
85
- * honest "this scene has no authored identity" value, and the corpus row then carries the
86
- * code itself. A caller with a better identity overrides via `setSourceRef()`.
87
- * @private
88
- */
89
- function _defaultSourceRef() {
90
- try {
91
- if (typeof location === 'undefined' || !location) { return null; }
92
- var m = String(location.pathname).match(/\/experiments\/([^/]+)\//);
93
- return (m && m[1]) ? m[1] : null;
94
- } catch (e) { return null; }
95
- }
96
-
97
74
  /**
98
75
  * The RASTER PATH pin — which GL backend this browser actually rasters with.
99
76
  *
@@ -207,7 +184,7 @@ import * as FrqtlDisplay from '../../compat/renderer/display-slot.js';
207
184
  // harness via setSourceRef(). null is HONEST (an ad-hoc universe); the corpus row then
208
185
  // carries the code itself. Never per-capture caller-supplied provenance.
209
186
  this._sourceRef = (identity.sourceRef === undefined || identity.sourceRef === null)
210
- ? _defaultSourceRef() : String(identity.sourceRef);
187
+ ? null : String(identity.sourceRef);
211
188
  // The code identity (setSourceSha256) — null until a door that linked a project registers it.
212
189
  this._sourceSha256 = null;
213
190
  // Memo for the content-addressed SceneRecord — see sceneRecord().