@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
@@ -69,7 +69,8 @@
69
69
  * project whose entry's default export is `buildExperiment(exp)` (a one-string sketch, such as a
70
70
  * shipped scene file, is `{ files: { 'sketch.js': text }, entry: 'sketch.js' }`). The
71
71
  * page links it through its one door and stamps the project's identity (projectIdentity) as the
72
- * record's `sourceSha256`; this process computes the same identity from the same files, and the
72
+ * record's `sourceSha256`; this process computes the same identity from the same linked set
73
+ * (lib/shim/project/link.js linkedFiles — frqtl.json and the module files; a README rides unhashed), and the
73
74
  * open-time record check holds the two equal. A caller without one is REPLAY-grade
74
75
  * (an ad-hoc tick-fn scene, a live-edited scene, a bare raster row) and is REFUSED in
75
76
  * RENDERER_CONTRACT §10.3's own vocabulary: there is nothing to regenerate FROM, and a
@@ -132,6 +133,7 @@ import { FrqtlCaptureHost } from './capture-host.js';
132
133
  import { FrqtlVision } from '../../parts/FrqtlVision.js';
133
134
  import { FrqtlCapture } from './capture.js';
134
135
  import { projectIdentity, projectBytes } from '../../../shim/project/identity.js';
136
+ import { linkedFiles, entryOf } from '../../../shim/project/link.js';
135
137
  import { libPathOf } from '../../../shim/project/specifiers.js';
136
138
 
137
139
 
@@ -191,7 +193,8 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
191
193
  // ── The REGENERATE gate ───────────────────────────────────────────────────
192
194
 
193
195
  /**
194
- * assertHostable({sceneRecord, sceneSource}) — throw-or-return, in §10.3's vocabulary.
196
+ * assertHostable({sceneRecord, sceneSource}) — throw-or-return the entry, in §10.3's vocabulary. The entry is
197
+ * THE entry rule's answer (link.js entryOf): the entry frqtl.json names, else sceneSource.entry.
195
198
  * @private
196
199
  */
197
200
  function _assertHostable(opts) {
@@ -213,18 +216,20 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
213
216
  '(more files beside it for a project with forked parts). (FAIL-LOUD.)');
214
217
  }
215
218
  if (!src || typeof src !== 'object' || src.kind !== 'project' ||
216
- !src.files || typeof src.files !== 'object' || Object.keys(src.files).length === 0 ||
217
- typeof src.entry !== 'string' || !src.entry) {
219
+ !src.files || typeof src.files !== 'object' || Object.keys(src.files).length === 0) {
218
220
  throw new Error('[FrqtlHostedEyes.open] this scene is REPLAY-grade, not ' +
219
221
  'REGENERATE-grade: hosting re-RUNS the scene in another process, so a ' +
220
222
  'SceneRecord alone is not enough (it is identity + reproduction pins, with no ' +
221
- 'scene spec in it). Pass the pair — sceneSource: { kind: "project", files, entry } — ' +
223
+ 'scene spec in it). Pass the pair — sceneSource: { kind: "project", files, entry? } (entry optional when ' +
224
+ 'files carries the frqtl.json that names it) — ' +
222
225
  'where the entry\'s default export is buildExperiment(exp) and the canonical empty ' +
223
226
  'page links it. An ad-hoc tick-fn scene, a live-edited scene or a ' +
224
227
  'bare raster row has nothing to regenerate FROM, and a silently approximate ' +
225
228
  're-run is the one failure a content-addressed corpus cannot detect afterwards. ' +
226
229
  '(FAIL-LOUD — see RENDERER_CONTRACT.md §10.3.)');
227
230
  }
231
+ // THE entry rule (link.js entryOf): frqtl.json's entry, a given one equal to it, else the given one.
232
+ return entryOf(src.files, src.entry);
228
233
  }
229
234
 
230
235
  /**
@@ -478,16 +483,10 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
478
483
  }
479
484
  applied = window.FrqtlPage.applyRenderState(args.renderState);
480
485
  }
481
- // ESTABLISH THE CLOCK with one step, deliberately. Applying a recipe RE-INITS the
482
- // universe, and neither the host's retained TickData nor the compat universe's own
483
- // `tick` field is cleared by that re-init — both keep reporting the blank page's last
484
- // value until the next tick overwrites them (observed: a freshly regenerated scene
485
- // read as "tick 10"). Every later read here is exact BECAUSE this process is the only
486
- // stepper (the RAF loop is stopped above), so the fix is to make the first reading a
487
- // real one rather than to trust a cache the re-init left behind. It costs nothing the
488
- // contract was not already charging: a capture before the first tick is REFUSED
489
- // anyway ("no tick has run yet" — never an empty buffer), so tick 1 is the earliest
490
- // capturable coordinate of any scene.
486
+ // ESTABLISH THE CLOCK with one step, deliberately: applying a recipe begins a world that has not stepped
487
+ // (runner.worldTick 0, no TickData yet), and a capture before the first tick is REFUSED ("no tick has run yet"
488
+ // — never an empty buffer), so tick 1 is the earliest capturable coordinate of any scene. Every later read is
489
+ // exact because this process is the only stepper (the RAF loop is stopped above).
491
490
  r.step();
492
491
  return { renderState: applied };
493
492
  };
@@ -553,8 +552,8 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
553
552
  return;
554
553
  }
555
554
  try {
556
- var td = r.step();
557
- resolve((td && td.universeData) ? td.universeData.tick : null);
555
+ r.step();
556
+ resolve(r.worldTick);
558
557
  } catch (e) { reject(e); }
559
558
  })();
560
559
  });
@@ -562,8 +561,9 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
562
561
 
563
562
  /** The current tick, read (never advanced). */
564
563
  var _IN_PAGE_TICK = function () {
565
- var td = window.FrqtlPage.runner._lastTickData;
566
- return (td && td.universeData) ? td.universeData.tick : null;
564
+ var r = window.FrqtlPage.runner;
565
+ if (!r) { throw new Error('[FrqtlHostedEyes/in-page] there is no runner to read a tick from (FrqtlPage.runner). (FAIL-LOUD.)'); }
566
+ return r.worldTick;
567
567
  };
568
568
 
569
569
  /**
@@ -606,6 +606,108 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
606
606
  return cam.getOrbit();
607
607
  };
608
608
 
609
+ // ── The Record door, in the page (lib/frqtl/objects/record/Record.js through FrqtlPage.experiment) ──
610
+ // The page's facade owns the Record; these routines only forward to it, so the folder a node host gets
611
+ // is the one the page's own door wrote (one implementation, every host). The open Record and a film
612
+ // in flight are parked on the page under ONE name, `__frqtlHostedRecord`.
613
+
614
+ /** exp.startRecord() on the page's facade; parks the Record. */
615
+ var _IN_PAGE_RECORD_START = async function () {
616
+ var exp = window.FrqtlPage.experiment;
617
+ if (!exp || typeof exp.startRecord !== 'function') {
618
+ throw new Error('[FrqtlHostedEyes/in-page] this page has no FrqtlPage.experiment facade to record.');
619
+ }
620
+ var rec;
621
+ // A refusal crosses the page boundary as data, so its err.record reaches node (page.evaluate keeps only a message).
622
+ try { rec = await exp.startRecord(); }
623
+ catch (e) { if (e && e.record) { return { refused: { message: e.message, record: e.record } }; } throw e; }
624
+ window.__frqtlHostedRecord = { rec: rec, film: null };
625
+ return { tick: rec.tick };
626
+ };
627
+
628
+ /** rec.film(opts) begins: its first segment subscribes before this returns (the drive gate waits on it). */
629
+ var _IN_PAGE_RECORD_FILM_BEGIN = function (opts) {
630
+ var p = window.__frqtlHostedRecord;
631
+ if (!p || !p.rec) { throw new Error('[FrqtlHostedEyes/in-page] no Record is open — eyes.record.start() first.'); }
632
+ if (p.film) { throw new Error('[FrqtlHostedEyes/in-page] a film is already running — await it first.'); }
633
+ p.film = p.rec.film(opts);
634
+ p.film.then(null, function () {});
635
+ return true;
636
+ };
637
+
638
+ /** The film's clip spans, when it ends. */
639
+ var _IN_PAGE_RECORD_FILM_END = function () {
640
+ var p = window.__frqtlHostedRecord;
641
+ var f = p && p.film;
642
+ if (!f) { throw new Error('[FrqtlHostedEyes/in-page] no film is running.'); }
643
+ return f.then(function (spans) { p.film = null; return spans; }, function (e) { p.film = null; throw e; });
644
+ };
645
+
646
+ /** rec.stop() → the folder, each clip's bytes as base64 (the one encoder, capture-envelope.js). */
647
+ var _IN_PAGE_RECORD_STOP = async function (args) {
648
+ var p = window.__frqtlHostedRecord;
649
+ if (!p || !p.rec) { throw new Error('[FrqtlHostedEyes/in-page] no Record is open — eyes.record.start() first.'); }
650
+ var CE = await import(args.modules.captureEnvelope);
651
+ var out;
652
+ // A refused stop() leaves the Record open (still parked); its err.record crosses as data.
653
+ try { out = await p.rec.stop(); }
654
+ catch (e) { if (e && e.record) { return { refused: { message: e.message, record: e.record } }; } throw e; }
655
+ window.__frqtlHostedRecord = null;
656
+ if (out.empty) { return { empty: true }; }
657
+ var files = {};
658
+ Object.keys(out.files).forEach(function (k) {
659
+ var v = out.files[k];
660
+ files[k] = typeof v === 'string' ? { text: v } : { base64: CE.bytesToBase64(v) };
661
+ });
662
+ return { files: files, record: out.record, clips: out.clips };
663
+ };
664
+
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). */
674
+ var _IN_PAGE_ACT = async function (args) {
675
+ var exp = window.FrqtlPage.experiment;
676
+ var r = window.FrqtlPage.runner;
677
+ if (args.door === 'toSketch') { return exp.toSketch(args.opts); }
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); }
680
+ if (args.door === 'placeDeclaredAt') {
681
+ var d = r.declared.find(function (o) { return o.name === args.name; });
682
+ if (!d) { throw new Error('[FrqtlHostedEyes/in-page] no declared object is named ' + JSON.stringify(args.name) + '.'); }
683
+ var res = r.placeDeclaredAt(d, args.point);
684
+ r.repaint();
685
+ return res;
686
+ }
687
+ throw new Error('[FrqtlHostedEyes/in-page] unknown act door ' + JSON.stringify(args.door) + '.');
688
+ };
689
+
690
+ /** A page refusal `{ refused: { message, record } }` → a thrown Error carrying `.record`, as in a page. @private */
691
+ function _rethrowRefused(out) {
692
+ if (out && out.refused) {
693
+ var e = new Error(out.refused.message);
694
+ e.record = out.refused.record;
695
+ throw e;
696
+ }
697
+ return out;
698
+ }
699
+
700
+ /** A page folder (text | base64 per path) → { path: string | Uint8Array }. @private */
701
+ function _folderFromPage(out) {
702
+ if (out.empty) { return { empty: true }; }
703
+ var files = {};
704
+ Object.keys(out.files).forEach(function (k) {
705
+ var v = out.files[k];
706
+ files[k] = (v.text !== undefined) ? v.text : new Uint8Array(Buffer.from(v.base64, 'base64'));
707
+ });
708
+ return { files: files, record: out.record, clips: out.clips };
709
+ }
710
+
609
711
  // ── open() ────────────────────────────────────────────────────────────────
610
712
 
611
713
  /**
@@ -617,8 +719,9 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
617
719
  * world the source's co-init call built). Supplying any of them is an EXPECTATION, checked
618
720
  * after boot and THROWN on mismatch — never a request; a stated bound that is not a
619
721
  * positive integer is refused before a browser is acquired.
620
- * @param {{kind:'project', files:Object<string,string>, entry:string}} opts.sceneSource — the
621
- * REGENERATE-grade scene: a project, linked in the page by its one door.
722
+ * @param {{kind:'project', files:Object<string,string>, entry?:string}} opts.sceneSource — the
723
+ * REGENERATE-grade scene: a project, linked in the page by its one door. `entry` is optional when
724
+ * `files` carries the frqtl.json that names it (a different one is refused).
622
725
  * @param {object} [opts.renderState] — the declared LOOK, ferried: a renderState bag (for a
623
726
  * rung, `effectiveRenderState(rung, mode)`; or a saved project's bag). Its `renderer` boots
624
727
  * the page; its rows, camera and backdrop are applied by `FrqtlPage.applyRenderState` before
@@ -654,7 +757,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
654
757
  /** @private — the body; see `open` for the contract. */
655
758
  function _open(opts) {
656
759
  var C = _contracts();
657
- _assertHostable(opts);
760
+ var entry = _assertHostable(opts);
658
761
  _assertBoundShape(opts.sceneRecord, C.RS);
659
762
  var declared = _declaredRenderState(opts, opts.sceneRecord, C.RST);
660
763
  if (!opts.pageUrl || typeof opts.pageUrl !== 'string') {
@@ -725,7 +828,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
725
828
  })
726
829
  .then(function () {
727
830
  return page.evaluate(_IN_PAGE_APPLY,
728
- { files: opts.sceneSource.files, entry: opts.sceneSource.entry, sourceRef: sourceRef, renderState: declared });
831
+ { files: opts.sceneSource.files, entry: entry, sourceRef: sourceRef, renderState: declared });
729
832
  })
730
833
  .then(function (applied) {
731
834
  return page.evaluate(_IN_PAGE_DISPLAY).then(function (disp) {
@@ -860,7 +963,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
860
963
  );
861
964
  inst.attachCaptureHost(host);
862
965
 
863
- return projectIdentity({ files: projectBytes(opts.sceneSource.files) }).then(function (sha) {
966
+ return projectIdentity({ files: projectBytes(linkedFiles(opts.sceneSource.files)) }).then(function (sha) {
864
967
  inst.setSourceSha256(sha);
865
968
  return inst.sceneRecord();
866
969
  }).then(function (nodeScene) {
@@ -875,7 +978,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
875
978
  'them. (FAIL-LOUD — a hosted still filed under the wrong scene verifies ' +
876
979
  'forever against the wrong header.)');
877
980
  }
878
- return _handle(browser, page, host, inst, nodeScene, gate, timeout);
981
+ return _handle(browser, page, host, inst, nodeScene, gate, timeout, modules);
879
982
  });
880
983
  }).catch(function (e) {
881
984
  // A half-opened handle that leaked a browser would poison the zero-orphans gate
@@ -894,7 +997,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
894
997
  }
895
998
 
896
999
  /** The consumer-facing handle. @private */
897
- function _handle(browser, page, host, inst, scene, gate, timeout) {
1000
+ function _handle(browser, page, host, inst, scene, gate, timeout, modules) {
898
1001
  var self = {
899
1002
  /**
900
1003
  * The SAME face as everywhere else: getScene / getStill / stages / toPNG, and
@@ -907,7 +1010,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
907
1010
  /** The node-side SceneRecord (page-extended) + its content id. */
908
1011
  scene: scene,
909
1012
 
910
- /** @returns {Promise<number|null>} the page's current tick — READ, never advanced. */
1013
+ /** @returns {Promise<number>} the world's own tick (runner.worldTick: 0 before its first step) — READ, never advanced. */
911
1014
  tick: function () { return page.evaluate(_IN_PAGE_TICK); },
912
1015
 
913
1016
  /**
@@ -926,7 +1029,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
926
1029
  'non-negative integer coordinate; got ' + String(n) + '. (FAIL-LOUD.)'));
927
1030
  }
928
1031
  return gate.opened.then(function () { return self.tick(); }).then(function (now) {
929
- var at = (typeof now === 'number') ? now : 0;
1032
+ var at = now;
930
1033
  if (at > n) {
931
1034
  throw new Error('[FrqtlHostedEyes.driveToTick] the hosted world is ' +
932
1035
  'already at tick ' + at + ', past ' + n + '. A world runs forward ' +
@@ -1010,6 +1113,49 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
1010
1113
  return file;
1011
1114
  },
1012
1115
 
1116
+ /**
1117
+ * The Record door, forwarded to the PAGE's facade (lib/frqtl/objects/record/Record.js): the folder is
1118
+ * the one the page's own door writes, so a node host and a page produce the same files for the same acts.
1119
+ * The handle has no re-authoring door: a Record opened by `eyes.record.start()` closes only by
1120
+ * `eyes.record.stop()`. A refusal carries `err.record = { changes, clips }`, as in a page.
1121
+ * start() exp.startRecord() → the Record's tick (0)
1122
+ * film(opts) rec.film(opts) → its clip spans. Drive the world while it films:
1123
+ * `const f = eyes.record.film(o); await eyes.driveToTick(n); await f`
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.
1127
+ */
1128
+ record: {
1129
+ start: function () {
1130
+ return page.evaluate(_IN_PAGE_RECORD_START).then(_rethrowRefused).then(function (o) { return o.tick; });
1131
+ },
1132
+ film: function (opts) {
1133
+ var begun = page.evaluate(_IN_PAGE_RECORD_FILM_BEGIN, opts);
1134
+ gate.opened = begun.then(null, function () {});
1135
+ return begun.then(function () { return page.evaluate(_IN_PAGE_RECORD_FILM_END); });
1136
+ },
1137
+ stop: function () {
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);
1142
+ }
1143
+ },
1144
+
1145
+ /**
1146
+ * Acts through the page's own doors (the writer's rows come from these, never from node):
1147
+ * toSketch({ by }) exp.toSketch
1148
+ * setDeclared(name, patch, { by }) exp.setDeclared
1149
+ * fork(name, Kind, { by }) exp.fork → { fork, file, from, files } (the files as text)
1150
+ * placeDeclaredAt(name, [x, y, z?]) runner.placeDeclaredAt on the declared object of that name
1151
+ */
1152
+ act: {
1153
+ toSketch: function (opts) { return page.evaluate(_IN_PAGE_ACT, { door: 'toSketch', opts: opts }); },
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 }); },
1156
+ placeDeclaredAt: function (name, point) { return page.evaluate(_IN_PAGE_ACT, { door: 'placeDeclaredAt', name: name, point: point }); }
1157
+ },
1158
+
1013
1159
  /** Close the managed browser. Symmetric with open(), idempotent. */
1014
1160
  dispose: function () { return host.dispose(); }
1015
1161
  };
@@ -1020,5 +1166,46 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
1020
1166
 
1021
1167
  var FrqtlHostedEyes = { open: open };
1022
1168
 
1023
- export { FrqtlHostedEyes };
1169
+ /**
1170
+ * writeRecordFolder(folder, dir) — write a Record folder (what `eyes.record.stop()` resolves) to `dir`, each path
1171
+ * verbatim: text as UTF-8, clip bytes as they are. The ONE node writer of the folder.
1172
+ * REFUSES (throws): an empty Record (`{ empty: true }` is a state with no folder); a `dir` that exists and is not
1173
+ * empty (a Record is never written over another); a path that is absolute or climbs out of `dir`.
1174
+ * @param {{ files: Object<string, string|Uint8Array> }} folder
1175
+ * @param {string} dir
1176
+ * @returns {string[]} the paths written, in the folder's order
1177
+ */
1178
+ function writeRecordFolder(folder, dir) {
1179
+ if (folder && folder.empty === true) {
1180
+ throw new Error('[FrqtlHostedEyes] writeRecordFolder: this Record is empty — no change and no clip — a state with no folder. (FAIL-LOUD.)');
1181
+ }
1182
+ if (!folder || !folder.files || typeof folder.files !== 'object') {
1183
+ throw new Error('[FrqtlHostedEyes] writeRecordFolder(folder, dir): folder is what eyes.record.stop() resolves, { files, record, clips }. (FAIL-LOUD.)');
1184
+ }
1185
+ if (typeof dir !== 'string' || !dir) { throw new Error('[FrqtlHostedEyes] writeRecordFolder(folder, dir): dir is a directory path. (FAIL-LOUD.)'); }
1186
+ const root = path.resolve(dir);
1187
+ if (fs.existsSync(root) && fs.readdirSync(root).length) {
1188
+ throw new Error('[FrqtlHostedEyes] writeRecordFolder: ' + root + ' exists and is not empty — a Record is never written over another; ' +
1189
+ 'name a new directory. (FAIL-LOUD.)');
1190
+ }
1191
+ const paths = Object.keys(folder.files);
1192
+ for (const p of paths) {
1193
+ const at = path.resolve(root, p);
1194
+ if (path.isAbsolute(p) || (at !== root && !at.startsWith(root + path.sep))) {
1195
+ throw new Error('[FrqtlHostedEyes] writeRecordFolder: the folder path ' + JSON.stringify(p) + ' is not inside the folder. (FAIL-LOUD.)');
1196
+ }
1197
+ const v = folder.files[p];
1198
+ if (typeof v !== 'string' && !(v instanceof Uint8Array)) {
1199
+ throw new Error('[FrqtlHostedEyes] writeRecordFolder: ' + JSON.stringify(p) + ' is neither text nor bytes. (FAIL-LOUD.)');
1200
+ }
1201
+ }
1202
+ for (const p of paths) {
1203
+ const at = path.resolve(root, p);
1204
+ fs.mkdirSync(path.dirname(at), { recursive: true });
1205
+ fs.writeFileSync(at, folder.files[p]);
1206
+ }
1207
+ return paths;
1208
+ }
1209
+
1210
+ export { FrqtlHostedEyes, writeRecordFolder };
1024
1211
 
@@ -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) {