@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
@@ -19,12 +19,39 @@
19
19
  * explicit depth d.
20
20
  *
21
21
  * exp.world the ONE readable answer: frozen {renderer, w, h, d, cfg}
22
- * exp.addObject(kind, config) ObjectFactory.create(kind, config, ctx) sugar
22
+ * exp.addObject(kind, config) ObjectFactory.create(kind, config, ctx) sugar (a kind by name or class)
23
+ * exp.declare(parts) the sketch's `export const parts = { name: [Kind, props] }`, in key order;
24
+ * returns the frozen build-time map { key: handle }
23
25
  * exp.addTickFunction(fn) universe.experimentTickFunctions.push
24
26
  * exp.render(bag) runner.setRenderOptions(bag)
25
- * exp.build(buildFn) run the recipe WITHOUT starting the loop (a sketch held as
26
- * TEXT is a project, linked first: lib/shim/project/link.js linkProject)
27
+ * exp.build(sketch) run the recipe WITHOUT starting the loop; `sketch` is a buildExperiment
28
+ * function or linkProject's result (lib/shim/project/link.js) — then the
29
+ * facade HOLDS that project's text
30
+ * exp.toSketch() write the hands into the held text's `parts` block → { files, changes, refused }
31
+ * exp.setDeclared(name, patch) patch one parts entry and re-run in this document
27
32
  * exp.run(buildFn) build (if needed) + start the loop
33
+ *
34
+ * THE WRITER CONTRACT. The facade that linked a sketch is the ONE owner of its current text: the text it
35
+ * linked, then every toSketch / setDeclared result.
36
+ * - exp.declare(parts) calls exp.addObject(Kind, { name: key, ...props }) per own key (zero physics). It
37
+ * refuses a string Kind (naming the import form), an unregistered class, a props.name (the key IS the
38
+ * name) and a second call in one build. Each object it declares is an `inParts` row of runner.declared;
39
+ * its capture right after construction (ObjectSchema.captureProps) is the writer's baseline. It returns
40
+ * the frozen build-time map { key: handle } (`const p = exp.declare(parts); … p.screen`).
41
+ * - toSketch is a HOST call at the END of an act (STUDIO: a hand's release; a lab: the end of a hand),
42
+ * never per placement. It writes, through lib/shim/project/parts-writer.js patchParts, only what a
43
+ * hand did since the last call (the runner's hand mark): an inParts object's touched writable keys
44
+ * (`set`), a declared object added since the build (`add`, the last key, plus one import line), an
45
+ * inParts object no longer live (`remove`). A hand on a code declaration is refused by name. It
46
+ * adopts its result and resets every baseline, so a second call with no hand between is a no-op.
47
+ * - setDeclared validates each key against the kind's descriptor (writable, its own validator),
48
+ * patches the held text and re-runs through the one restart door in THIS document; the seed (the
49
+ * sketch's own co-init cfg) and the camera pose (its poseFields, re-applied through setOrbit) are kept.
50
+ * - A hand is a write through runner.updateObjectProperty or runner.placeDeclaredAt; a host that calls a
51
+ * part's own setter is driving the run, and the writer does not write it. A hand never renames: the key is
52
+ * the name, so a touched `name` is refused (toSketch) or thrown (setDeclared).
53
+ * - setDeclared refuses while a hand is unwritten (await exp.toSketch() first); its `kept` lists what it kept.
54
+ * - restart() of a linked sketch re-links the CURRENT text (the retained replay) and returns a Promise.
28
55
  * exp.runner / .universe the runner the co-init call built
29
56
  * exp.is3D world.renderer === 'three'
30
57
  *
@@ -43,6 +70,13 @@ import { ObjectFactory } from './ObjectFactory.js';
43
70
  import { FrqtlWorldInit } from './setup/_worldInit.js';
44
71
  import * as FrqtlDisplay from '../compat/renderer/display-slot.js';
45
72
  import { refuseRetiredDoors } from '../compat/engine.js';
73
+ import { ObjectSchema } from './schema/index.js';
74
+ import { kindOf } from './kinds.js';
75
+ import { linkProject } from '../../shim/project/link.js';
76
+ import { patchParts, PARTS_EXPORT, entryKeys, entryLiteral } from '../../shim/project/parts-writer.js';
77
+ import { moduleOfKind } from '../../shim/project/parts-module.js';
78
+ import { MANIFEST_FILE } from '../../shim/project/manifest.js';
79
+ import { Record, openRecordOf } from './record/Record.js';
46
80
 
47
81
  /** The world-init contract (objects/setup/_worldInit.js). */
48
82
  function _worldInit() {
@@ -86,6 +120,13 @@ import { refuseRetiredDoors } from '../compat/engine.js';
86
120
  this._world = null; // set by the co-init call; reset by build()
87
121
  this._built = false;
88
122
  this._buildFn = null;
123
+ this._project = null; // THE held project text: { files: {path: text}, entry, mint, bare? } (build(linked))
124
+ this._urls = null; // the minted URL of each project file, from the last link
125
+ this._parts = new Map(); // declared object → { key, baseline } — the objects in `parts`
126
+ this._code = new Map(); // declared object → baseline — the code declarations at build end
127
+ this._declaredOnce = false;
128
+ this._record = null; // the open Record (exp.startRecord), or null
129
+ this._reauthored = null; // the Record the last re-authoring closed (startRecord checks its close was heard)
89
130
  }
90
131
 
91
132
  function _requireWorld(self) {
@@ -148,6 +189,8 @@ import { refuseRetiredDoors } from '../compat/engine.js';
148
189
  self._runner = new FrqtlApp(ropts);
149
190
  if (!self._opts.deferCanvas) { self._runner.createCanvas(); }
150
191
  }
192
+ // The census maps a declared kind's module against the project files this facade linked (FrqtlApp.declare).
193
+ self._runner._projectUrls = self._urls;
151
194
  var u = self._runner.universe;
152
195
  var args = W.initArgs(cfg);
153
196
  if (renderer === 'three') {
@@ -156,6 +199,7 @@ import { refuseRetiredDoors } from '../compat/engine.js';
156
199
  }
157
200
  // A (re-)inited world has no declared objects; the recipe re-declares what it builds.
158
201
  self._runner.clearDeclared();
202
+ self._runner.beginWorld(); // a (re-)inited world has not stepped
159
203
  W.applyPreInit(cfg, u); // pre-init world keys (iframeIntervalSeconds) — the engine reads them at init
160
204
  u.init.apply(u, args);
161
205
  self._world = Object.freeze({ renderer: renderer, w: w, h: h, d: renderer === 'three' ? d : null,
@@ -190,6 +234,7 @@ import { refuseRetiredDoors } from '../compat/engine.js';
190
234
 
191
235
  // ── parts, tick-fns, render ──────────────────────────────────────────────────────
192
236
 
237
+ /** exp.addObject(kind, config) — a kind by name (a host's JSON) or by its class. */
193
238
  RecipeContext.prototype.addObject = function (kind, config) {
194
239
  _requireWorld(this);
195
240
  var x = (config && config.x != null) ? config.x : 0;
@@ -198,6 +243,487 @@ import { refuseRetiredDoors } from '../compat/engine.js';
198
243
  return ObjectFactory.create(kind, config, ctx);
199
244
  };
200
245
 
246
+ // ── the parts block ───────────────────────────────────────────────────────────────
247
+
248
+ var IMPORT_FORM = 'import { <Kind> } from \'@descix/frqtl-web/parts\'';
249
+
250
+ /** A deep copy of arrays and plain objects (the module-level literal is never shared with a part). */
251
+ function _plain(v) {
252
+ if (v === null || typeof v !== 'object') { return v; }
253
+ if (Array.isArray(v)) { return v.map(_plain); }
254
+ var proto = Object.getPrototypeOf(v);
255
+ if (proto !== Object.prototype && proto !== null) { return v; }
256
+ var out = {};
257
+ Object.keys(v).forEach(function (k) { out[k] = _plain(v[k]); });
258
+ return out;
259
+ }
260
+
261
+ /**
262
+ * exp.declare(parts) — seed the declared parts of `export const parts = { name: [Kind, props], … }`, in key
263
+ * order: `exp.addObject(Kind, { name: key, ...props })` per own key. Packaging, zero physics. Each object
264
+ * declared here is an `inParts` object of the census, and its capture right after construction is the
265
+ * writer's baseline (toSketch diffs against it).
266
+ * RETURNS the build-time map `Object.freeze({ <key>: handle, … })`, in key order — each value the object
267
+ * exp.addObject returned (the same identity as its runner.declared row), so later scene code reads a part
268
+ * as `const p = exp.declare(parts); … p.screen`. A hand add or remove after the build does not update it.
269
+ * REFUSES (throws): a string Kind (name the import form), a Kind that is not a registered class, a
270
+ * `props.name` (the key IS the name), a second declare in one build, an entry not written [Kind, props].
271
+ */
272
+ RecipeContext.prototype.declare = function (parts) {
273
+ _requireWorld(this);
274
+ if (this._declaredOnce) {
275
+ throw new Error('[RecipeContext.declare] exp.declare(parts) runs ONCE per build — the scene\'s one `export const ' +
276
+ PARTS_EXPORT + '` block is its declared parts. (FAIL-LOUD.)');
277
+ }
278
+ if (!parts || typeof parts !== 'object' || Array.isArray(parts)) {
279
+ throw new TypeError('[RecipeContext.declare] exp.declare(parts): parts is the sketch\'s `export const ' + PARTS_EXPORT +
280
+ ' = { name: [Kind, { …props }] }` object. (FAIL-LOUD.)');
281
+ }
282
+ this._declaredOnce = true;
283
+ var self = this;
284
+ var out = {};
285
+ Object.keys(parts).forEach(function (key) {
286
+ var entry = parts[key];
287
+ if (!Array.isArray(entry) || entry.length !== 2 || !entry[1] || typeof entry[1] !== 'object' || Array.isArray(entry[1])) {
288
+ throw new TypeError('[RecipeContext.declare] parts.' + key + ' is not [Kind, { …props }]. (FAIL-LOUD.)');
289
+ }
290
+ var Kind = entry[0], props = entry[1];
291
+ if (typeof Kind === 'string') {
292
+ throw new TypeError('[RecipeContext.declare] parts.' + key + ' names its kind by the string "' + Kind + '" — in ' +
293
+ '`parts` a kind is the imported class: ' + IMPORT_FORM.replace('<Kind>', Kind) + '. (exp.addObject(name, ' +
294
+ 'props) still takes a string.) (FAIL-LOUD.)');
295
+ }
296
+ if (typeof Kind !== 'function' || !kindOf(Kind)) {
297
+ throw new TypeError('[RecipeContext.declare] parts.' + key + ': ' + (typeof Kind === 'function'
298
+ ? 'the class ' + (Kind.name || '(anonymous)') + ' is not a registered kind in this realm'
299
+ : 'the kind is ' + (Kind === null ? 'null' : typeof Kind) + ', not a registered class') +
300
+ ' — ' + IMPORT_FORM + ', or register a forked part\'s renamed class (@descix/frqtl-web/kinds). (FAIL-LOUD.)');
301
+ }
302
+ if (Object.prototype.hasOwnProperty.call(props, 'name')) {
303
+ throw new TypeError('[RecipeContext.declare] parts.' + key + ' carries name ' + JSON.stringify(props.name) +
304
+ ' — in `parts` the KEY is the name ("' + key + '"); delete the name prop. (FAIL-LOUD.)');
305
+ }
306
+ var obj = self.addObject(Kind, Object.assign({ name: key }, _plain(props)));
307
+ self._runner._adoptIntoParts(obj);
308
+ self._parts.set(obj, { key: key, baseline: _capture(self, obj), kind: self._runner.kindOf(obj) });
309
+ out[key] = obj;
310
+ });
311
+ return Object.freeze(out);
312
+ };
313
+
314
+ function _capture(self, obj) {
315
+ return ObjectSchema.captureProps(obj, self._runner.kindOf(obj));
316
+ }
317
+
318
+ /** The fields a hand or setDeclared may write: the descriptor's, minus write-only adapters and readonly reads. */
319
+ function _writable(kind) {
320
+ var d = ObjectSchema.get(kind);
321
+ var out = {};
322
+ ((d && d.fields) || []).forEach(function (f) {
323
+ if (f.writeOnly !== true && f.type !== 'readonly') { out[f.name] = f; }
324
+ });
325
+ return out;
326
+ }
327
+
328
+ /** The writable keys whose live value differs from the baseline (JSON-equal = untouched). */
329
+ function _touched(kind, baseline, live) {
330
+ var w = _writable(kind);
331
+ var out = {};
332
+ var lost = [];
333
+ Object.keys(w).forEach(function (k) {
334
+ var a = baseline[k], b = live[k];
335
+ if (JSON.stringify(a) === JSON.stringify(b)) { return; }
336
+ if (b === undefined) { lost.push(k); return; }
337
+ out[k] = b;
338
+ });
339
+ return { props: out, lost: lost };
340
+ }
341
+
342
+ var RENAME = FrqtlApp.RENAME_REFUSAL;
343
+
344
+ var _JSON = function (v) { return JSON.stringify(v); };
345
+
346
+ /**
347
+ * Spell a touched rect in the entry's OWN form (architect z addendum). A 6-tuple rect of a kind whose descriptor
348
+ * derives reads from it (ObjectSchema.derivedReads: Wall `z`, `depth`) is written as the 4-tuple plus each derived
349
+ * key whose value changed, when the entry spells any of those keys as plain properties (entryKeys); otherwise the
350
+ * 6-tuple stands. A 4-tuple equal to the baseline's is not written (no literalizing an untouched expression).
351
+ * Mutates and returns `props`.
352
+ */
353
+ function _spell(text, name, kind, props, baseline) {
354
+ var derived = ObjectSchema.derivedReads(kind);
355
+ if (!derived.length) { return props; }
356
+ var spelled = entryKeys(text, name);
357
+ var R4 = ObjectSchema.RECT_TUPLE[4], R6 = ObjectSchema.RECT_TUPLE[6];
358
+ Object.keys(props).forEach(function (f) {
359
+ var v = props[f];
360
+ var rows = derived.filter(function (d) { return d.field === f && d.tuple === 6 && spelled.indexOf(d.name) !== -1; });
361
+ if (!rows.length || !Array.isArray(v) || v.length !== 6) { return; }
362
+ var b = Array.isArray(baseline[f]) && baseline[f].length === 6 ? baseline[f] : null;
363
+ var four = R4.map(function (k) { return v[R6.indexOf(k)]; });
364
+ if (b && _JSON(four) === _JSON(R4.map(function (k) { return b[R6.indexOf(k)]; }))) { delete props[f]; }
365
+ else { props[f] = four; }
366
+ rows.forEach(function (d) {
367
+ var nv = v[R6.indexOf(d.key)];
368
+ if (!b || nv !== b[R6.indexOf(d.key)]) { props[d.name] = nv; }
369
+ });
370
+ });
371
+ return props;
372
+ }
373
+
374
+ /**
375
+ * Spell a touched `center` in the entry's OWN arity: two numbers when the entry
376
+ * spells two (a literal, or an array expression — entryLiteral's `length`) and the live z is 0; otherwise the
377
+ * live [x, y, z] (an entry spelled three, a 3D hand off z 0, an entry whose center is not an array). The
378
+ * read-back (_verifyWritten) guards every case. Mutates and returns `props`.
379
+ */
380
+ function _spellCenter(text, name, props) {
381
+ var v = props.center;
382
+ if (!Array.isArray(v) || v.length !== 3 || v[2] !== 0) { return props; }
383
+ if (entryKeys(text, name).indexOf('center') === -1) { return props; }
384
+ var r = entryLiteral(text, name, 'center');
385
+ var n = r.literal ? (Array.isArray(r.value) ? r.value.length : null) : r.length;
386
+ if (n === 2) { props.center = [v[0], v[1]]; }
387
+ return props;
388
+ }
389
+
390
+ /**
391
+ * The post-patch check: what the patched text now declares for entry `name`, read back (entryLiteral) for every
392
+ * written key and folded with the baseline for every unwritten one, must equal the live capture — `rect` in its
393
+ * capture form, through the descriptor's derivedFrom rows. Each mismatch is a refusal naming the key.
394
+ */
395
+ function _verifyWritten(text, name, kind, written, baseline, live) {
396
+ var refused = [];
397
+ var R4 = ObjectSchema.RECT_TUPLE[4], R6 = ObjectSchema.RECT_TUPLE[6];
398
+ var derived = ObjectSchema.derivedReads(kind);
399
+ var decl = {};
400
+ Object.keys(written).forEach(function (k) {
401
+ var r = entryLiteral(text, name, k);
402
+ decl[k] = r.literal ? r.value : undefined;
403
+ // A two-number center declares z 0 (the reading placeDeclaredAt gives an omitted z).
404
+ if (k === 'center' && Array.isArray(decl[k]) && decl[k].length === 2) { decl[k] = decl[k].concat([0]); }
405
+ });
406
+ function refuse(k, declared, liveV) {
407
+ refused.push({ op: 'set', name: name, key: k, reason: 'the written text declares ' + _JSON(declared) + ' but the live object is ' +
408
+ _JSON(liveV) + ' — the hand is not in the text.' });
409
+ }
410
+ var asDerived = {};
411
+ derived.forEach(function (d) { asDerived[d.name] = d; });
412
+ Object.keys(written).forEach(function (k) {
413
+ if (asDerived[k]) { return; }
414
+ var lv = live[k];
415
+ if (Array.isArray(lv) && lv.length === 6 && derived.some(function (d) { return d.field === k; })) { return; }
416
+ if (_JSON(decl[k]) !== _JSON(lv)) { refuse(k, decl[k], lv); }
417
+ });
418
+ var fields = {};
419
+ derived.forEach(function (d) { fields[d.field] = true; });
420
+ Object.keys(fields).forEach(function (f) {
421
+ var lv = live[f];
422
+ if (!Array.isArray(lv) || lv.length !== 6) { return; }
423
+ var touched = written[f] !== undefined || derived.some(function (d) { return d.field === f && written[d.name] !== undefined; });
424
+ if (!touched) { return; }
425
+ var six = Array.isArray(baseline[f]) && baseline[f].length === 6 ? baseline[f].slice() : null;
426
+ var d0 = decl[f];
427
+ if (Array.isArray(d0) && d0.length === 6) { six = d0.slice(); }
428
+ else if (Array.isArray(d0) && d0.length === 4 && six) { R4.forEach(function (k, i) { six[R6.indexOf(k)] = d0[i]; }); }
429
+ derived.forEach(function (d) {
430
+ if (d.field === f && six && decl[d.name] !== undefined) { six[R6.indexOf(d.key)] = decl[d.name]; }
431
+ });
432
+ if (_JSON(six) !== _JSON(lv)) { refuse(f, six, lv); }
433
+ });
434
+ return refused;
435
+ }
436
+
437
+ var OUTSIDE_PARTS = 'this sketch declares its parts outside `parts`; STUDIO can run it but cannot write hand edits ' +
438
+ 'into it — declare them in `export const parts` to make it editable.';
439
+
440
+ function _requireHeld(self, where) {
441
+ if (!self._project) {
442
+ throw new Error('[RecipeContext.' + where + '] this scene was not built from linked text — the writer writes the ' +
443
+ 'text a facade linked: exp.build(await linkProject({ files, entry, mint })) (lib/shim/project/link.js). (FAIL-LOUD.)');
444
+ }
445
+ }
446
+
447
+ /**
448
+ * exp.toSketch() → Promise<{ files, changes, refused }> — write the hands into the sketch's `parts` literal.
449
+ * A HOST call at the END of an act (a drop, a drag end, a delete; a lab's end of a hand), never per
450
+ * placement. For each declared object a hand wrote since the last call (the runner's hand mark: a device
451
+ * door — updateObjectProperty, placeDeclaredAt — wrote it; a part the scene's own code moves is the run,
452
+ * not a hand), it diffs the `inParts` object's live capture against its baseline (touched keys only →
453
+ * `set`), writes each declared object added since the build (`add`, as the last key, plus its import line)
454
+ * and each removed `inParts` object (`remove`), through lib/shim/project/parts-writer.js patchParts on the
455
+ * HELD text. A code-declared object a hand touched (or removed) is refused by name. On return the patched
456
+ * text IS the held text and every baseline is the live capture, so a second call with no hand between
457
+ * returns byte-identical files and no changes.
458
+ * files { path: text } — the whole project as now held
459
+ * changes patchParts' changes (a `literalized` set names the expression it replaced)
460
+ * refused every hand not written, { op, name, key?, reason }
461
+ */
462
+ RecipeContext.prototype.toSketch = async function (opts) {
463
+ _requireWorld(this);
464
+ _requireHeld(this, 'toSketch');
465
+ var by = this._record ? Record.requireBy(opts && opts.by, 'RecipeContext.toSketch') : null;
466
+ var self = this;
467
+ var runner = this._runner;
468
+ var declared = runner.declared;
469
+ var edits = [];
470
+ var refused = [];
471
+ var adopted = [];
472
+ var hands = runner._takeHandTouched();
473
+ this._parts.forEach(function (row, obj) {
474
+ if (declared.indexOf(obj) === -1) { edits.push({ op: 'remove', name: row.key, kind: row.kind, baseline: row.baseline }); return; }
475
+ if (!hands.has(obj)) { return; }
476
+ var kind = runner.kindOf(obj);
477
+ var t = _touched(kind, row.baseline, _capture(self, obj));
478
+ if (Object.prototype.hasOwnProperty.call(t.props, 'name')) {
479
+ refused.push({ op: 'set', name: row.key, key: 'name', reason: RENAME });
480
+ delete t.props.name;
481
+ }
482
+ t.lost.forEach(function (k) {
483
+ refused.push({ op: 'set', name: row.key, key: k, reason: 'the live object no longer reports "' + k + '" — there is no value to write.' });
484
+ });
485
+ if (Object.keys(t.props).length) {
486
+ var raw = _plain(t.props); // the capture form, before the entry's spelling (the Record's row values)
487
+ _spell(self._project.files[self._project.entry], row.key, kind, t.props, row.baseline);
488
+ _spellCenter(self._project.files[self._project.entry], row.key, t.props);
489
+ 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 }); }
490
+ }
491
+ });
492
+ this._code.forEach(function (baseline, obj) {
493
+ var name = obj.name || runner.kindOf(obj);
494
+ if (declared.indexOf(obj) === -1) {
495
+ refused.push({ op: 'remove', name: name, reason: '"' + name + '" is declared in code: ' + OUTSIDE_PARTS });
496
+ return;
497
+ }
498
+ if (!hands.has(obj)) { return; }
499
+ var t = _touched(runner.kindOf(obj), baseline, _capture(self, obj));
500
+ if (Object.keys(t.props).length || t.lost.length) {
501
+ refused.push({ op: 'set', name: name, reason: '"' + name + '" is declared in code: ' + OUTSIDE_PARTS });
502
+ }
503
+ });
504
+ declared.forEach(function (obj) {
505
+ if (self._parts.has(obj) || self._code.has(obj)) { return; }
506
+ var kind = runner.kindOf(obj);
507
+ var row = kindOf(kind);
508
+ var props = _capture(self, obj);
509
+ var w = _writable(kind);
510
+ Object.keys(props).forEach(function (k) { if (!w[k] || k === 'name') { delete props[k]; } });
511
+ edits.push({ op: 'add', name: obj.name || kind, Kind: kind, module: row ? moduleOfKind(row.from, self._urls) : null, props: props });
512
+ adopted.push(obj);
513
+ });
514
+ var entry = this._project.entry;
515
+ var r = patchParts(this._project.files[entry], edits.map(function (e) {
516
+ if (e.op === 'set') { return { op: 'set', name: e.name, props: e.props }; }
517
+ if (e.op === 'remove') { return { op: 'remove', name: e.name }; }
518
+ return { op: 'add', name: e.name, Kind: e.Kind, module: e.module, props: e.props };
519
+ }), { entry: entry });
520
+ // Never decide it succeeded without asking: read the patched text back against each moved object.
521
+ edits.forEach(function (e) {
522
+ if (e.op !== 'set') { return; }
523
+ if (r.refused.some(function (x) { return x.op === 'set' && x.name === e.name; })) { return; }
524
+ refused.push.apply(refused, _verifyWritten(r.text, e.name, e.kind, e.props, e.baseline, _capture(self, e.obj)));
525
+ });
526
+ var refusedAdd = {};
527
+ r.refused.forEach(function (x) { if (x.op === 'add') { refusedAdd[x.name] = true; } });
528
+ var files = Object.assign({}, this._project.files);
529
+ files[entry] = r.text;
530
+ this._project = Object.assign({}, this._project, { files: files });
531
+ // Adopt: the written text is held; baselines follow the live objects.
532
+ var removedRefused = {};
533
+ r.refused.forEach(function (x) { if (x.op === 'remove') { removedRefused[x.name] = true; } });
534
+ this._parts.forEach(function (row, obj) {
535
+ if (declared.indexOf(obj) === -1) { if (!removedRefused[row.key]) { self._parts.delete(obj); } return; }
536
+ row.baseline = _capture(self, obj);
537
+ });
538
+ adopted.forEach(function (obj) {
539
+ var key = obj.name || runner.kindOf(obj);
540
+ if (refusedAdd[key]) { return; }
541
+ runner._adoptIntoParts(obj);
542
+ self._parts.set(obj, { key: key, baseline: _capture(self, obj), kind: runner.kindOf(obj) });
543
+ });
544
+ var allRefused = refused.concat(r.refused);
545
+ if (this._record) { _recordRows(this, edits, allRefused, by); }
546
+ return { files: Object.assign({}, files), changes: r.changes, refused: allRefused };
547
+ };
548
+
549
+ /**
550
+ * The Record's rows for one toSketch, from the writer's own edits as WRITTEN (a refused edit, or a refused key
551
+ * of one, makes no row), in edit order (R3). A `set` whose touched keys include the part's position authority
552
+ * (runner._placementAuthority: `center`, or `rect` with its derivedFrom reads) is a `move` with that ONE key in
553
+ * its capture form; its other touched keys are one `setDeclared` row. `add` → [kind, props as written];
554
+ * `remove` → [kind, the baseline's writable props].
555
+ */
556
+ function _recordRows(self, edits, refused, by) {
557
+ var runner = self._runner;
558
+ var rec = self._record;
559
+ function refusedKeys(name, op) {
560
+ var keys = {};
561
+ var all = false;
562
+ refused.forEach(function (x) {
563
+ if (x.name !== name || x.op !== op) { return; }
564
+ if (x.key === undefined) { all = true; } else { keys[x.key] = true; }
565
+ });
566
+ return all ? null : keys;
567
+ }
568
+ function writableProps(kind, bag) {
569
+ var w = _writable(kind);
570
+ var out = {};
571
+ Object.keys(bag).forEach(function (k) { if (w[k] && k !== 'name') { out[k] = _plain(bag[k]); } });
572
+ return out;
573
+ }
574
+ edits.forEach(function (e) {
575
+ if (e.op === 'add') {
576
+ if (refusedKeys(e.name, 'add') === null) { return; }
577
+ rec._row({ name: e.name, op: 'add', by: by, from: null, to: [e.Kind, _plain(e.props)] });
578
+ return;
579
+ }
580
+ if (e.op === 'remove') {
581
+ if (refusedKeys(e.name, 'remove') === null) { return; }
582
+ rec._row({ name: e.name, op: 'remove', by: by, from: [e.kind, writableProps(e.kind, e.baseline)], to: null });
583
+ return;
584
+ }
585
+ var bad = refusedKeys(e.name, 'set');
586
+ if (bad === null) { return; }
587
+ var raw = e.raw;
588
+ var auth = (raw.center !== undefined || raw.rect !== undefined) ? runner._placementAuthority(e.obj) : null;
589
+ var authReads = {};
590
+ if (auth) {
591
+ authReads[auth] = true;
592
+ ObjectSchema.derivedReads(e.kind).forEach(function (d) { if (d.field === auth) { authReads[d.name] = true; } });
593
+ }
594
+ var moveRefused = Object.keys(bad).some(function (k) { return authReads[k]; });
595
+ if (auth && raw[auth] !== undefined && !moveRefused) {
596
+ var from = {}, to = {};
597
+ from[auth] = _plain(e.baseline[auth]);
598
+ to[auth] = _plain(raw[auth]);
599
+ rec._row({ name: e.name, op: 'move', by: by, from: from, to: to });
600
+ }
601
+ var pf = {}, pt = {}, any = false;
602
+ Object.keys(raw).forEach(function (k) {
603
+ if (k === auth || bad[k]) { return; }
604
+ pf[k] = e.baseline[k] === undefined ? null : _plain(e.baseline[k]);
605
+ pt[k] = _plain(raw[k]);
606
+ any = true;
607
+ });
608
+ if (any) { rec._row({ name: e.name, op: 'setDeclared', by: by, from: pf, to: pt }); }
609
+ });
610
+ }
611
+
612
+ /** The names of declared objects a hand marked whose live capture differs from their baseline (not yet written). */
613
+ function _unwrittenHands(self) {
614
+ var runner = self._runner;
615
+ var marks = runner._handTouched;
616
+ if (!marks || !marks.size) { return []; }
617
+ var out = [];
618
+ marks.forEach(function (obj) {
619
+ if (runner.declared.indexOf(obj) === -1) { return; }
620
+ var row = self._parts.get(obj);
621
+ var baseline = row ? row.baseline : self._code.get(obj);
622
+ if (!baseline) { return; }
623
+ var t = _touched(runner.kindOf(obj), baseline, _capture(self, obj));
624
+ if (Object.keys(t.props).length || t.lost.length) { out.push(row ? row.key : (obj.name || runner.kindOf(obj))); }
625
+ });
626
+ return out;
627
+ }
628
+
629
+ /** The camera pose the runner's camera declares (poseFields), or null with no camera. */
630
+ function _cameraPose(runner) {
631
+ var cam = runner.cameraControl;
632
+ if (!cam || typeof cam.getOrbit !== 'function' || !Array.isArray(cam.poseFields)) { return null; }
633
+ var orbit = cam.getOrbit() || {};
634
+ var pose = {};
635
+ cam.poseFields.forEach(function (f) { if (orbit[f.name] !== undefined) { pose[f.name] = _plain(orbit[f.name]); } });
636
+ return pose;
637
+ }
638
+
639
+ /**
640
+ * exp.setDeclared(name, patch) → Promise<{ set, from, to, tick: 0, kept, files }> — change
641
+ * an `inParts` object's declaration and re-run the scene IN THIS DOCUMENT. `patch` keys must be writable fields
642
+ * of the kind's descriptor and each value must pass the descriptor's own validator (ObjectSchema.normalizeValue);
643
+ * otherwise it refuses (throws) naming the key. The held text is patched (patchParts `set`), then the one restart
644
+ * door (runner.restart → the retained replay re-links the held text) re-runs it: the seed (the sketch's own
645
+ * createScene/createCanvas) and the camera pose (captured through the camera's poseFields, re-applied through
646
+ * setOrbit) are kept; `kept` lists 'cameraPose' only when a pose was captured and re-applied. A `rect` /
647
+ * `center` patch sets where the entry STARTS. The loop's run state is kept. REFUSES (throws) while a hand
648
+ * made since the last toSketch is unwritten (write it first: await exp.toSketch()), and a `name` patch (the
649
+ * key is the name). `from` and `to` are the patched keys' capture (ObjectSchema.captureProps) before and after the
650
+ * 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
651
+ * typed is in `files`. With a Record open, its `setDeclared` row carries the same `from`/`to`, written after the re-run.
652
+ */
653
+ RecipeContext.prototype.setDeclared = async function (name, patch, opts) {
654
+ _requireWorld(this);
655
+ _requireHeld(this, 'setDeclared');
656
+ var by = this._record ? Record.requireBy(opts && opts.by, 'RecipeContext.setDeclared') : null;
657
+ var runner = this._runner;
658
+ var obj = null;
659
+ this._parts.forEach(function (row, o) { if (row.key === name) { obj = o; } });
660
+ if (!obj || runner.declared.indexOf(obj) === -1) {
661
+ throw new Error('[RecipeContext.setDeclared] "' + name + '" is not an object of this sketch\'s `parts` block' +
662
+ (this._code.size ? ' (code declarations: ' + Array.from(this._code.keys()).map(function (o) { return o.name; }).join(', ') +
663
+ ' — ' + OUTSIDE_PARTS + ')' : '') + '. (FAIL-LOUD.)');
664
+ }
665
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch) || !Object.keys(patch).length) {
666
+ throw new TypeError('[RecipeContext.setDeclared] patch is { <field>: value, … } with at least one field. (FAIL-LOUD.)');
667
+ }
668
+ if (Object.prototype.hasOwnProperty.call(patch, 'name')) {
669
+ throw new TypeError('[RecipeContext.setDeclared] "name": ' + RENAME + ' (FAIL-LOUD.)');
670
+ }
671
+ // A hand made since the last toSketch is not in the held text, and the re-run would drop it without a
672
+ // word: refuse, naming the object, until the host writes it (a placement and a setDeclared are two changes).
673
+ var unwritten = _unwrittenHands(this);
674
+ if (unwritten.length) {
675
+ throw new Error('[RecipeContext.setDeclared] "' + unwritten.join('", "') + '" carries a hand the sketch text does ' +
676
+ 'not hold yet — write the hand first: await exp.toSketch(). (FAIL-LOUD.)');
677
+ }
678
+ var kind = runner.kindOf(obj);
679
+ var w = _writable(kind);
680
+ var live = _capture(this, obj);
681
+ var from = {};
682
+ Object.keys(patch).forEach(function (k) {
683
+ if (!w[k]) {
684
+ throw new TypeError('[RecipeContext.setDeclared] "' + k + '" is not a writable field of ' + kind + ' (writable: ' +
685
+ Object.keys(w).join(', ') + '). (FAIL-LOUD.)');
686
+ }
687
+ try { ObjectSchema.normalizeValue(w[k], patch[k], kind); }
688
+ catch (err) {
689
+ throw new TypeError('[RecipeContext.setDeclared] "' + k + '": ' + String((err && err.message) || err) + ' (FAIL-LOUD.)');
690
+ }
691
+ from[k] = live[k] === undefined ? null : live[k];
692
+ });
693
+ var entry = this._project.entry;
694
+ var spelled = _spell(this._project.files[entry], name, kind, _plain(patch), live);
695
+ var r = patchParts(this._project.files[entry], [{ op: 'set', name: name, props: spelled }], { entry: entry });
696
+ if (r.refused.length) {
697
+ throw new Error('[RecipeContext.setDeclared] the sketch text refused the patch: ' +
698
+ r.refused.map(function (x) { return (x.key ? x.key + ': ' : '') + x.reason; }).join('; ') + ' (FAIL-LOUD.)');
699
+ }
700
+ var files = Object.assign({}, this._project.files);
701
+ files[entry] = r.text;
702
+ this._project = Object.assign({}, this._project, { files: files });
703
+ var pose = _cameraPose(runner);
704
+ var wasRunning = !!runner._isLooping;
705
+ await runner.restart();
706
+ if (!wasRunning) { runner.stop(); }
707
+ // `kept` is what WAS kept: the seed by construction (the sketch's own co-init cfg); the camera pose only
708
+ // when one was captured and re-applied (a 2D scene with no pose camera has none).
709
+ var kept = ['seed'];
710
+ if (pose && runner.cameraControl && typeof runner.cameraControl.setOrbit === 'function') {
711
+ runner.cameraControl.setOrbit(pose);
712
+ kept.push('cameraPose');
713
+ }
714
+ // `to` is what the re-run declared, in the capture form `from` is in (the patch as typed is in the text).
715
+ var rebuilt = null;
716
+ this._parts.forEach(function (row, o) { if (row.key === name) { rebuilt = o; } });
717
+ if (!rebuilt) {
718
+ throw new Error('[RecipeContext.setDeclared] the re-run declared no "' + name + '" — the patched text dropped the entry. (FAIL-LOUD.)');
719
+ }
720
+ var after = _capture(this, rebuilt);
721
+ var to = {};
722
+ Object.keys(patch).forEach(function (k) { to[k] = after[k] === undefined ? null : _plain(after[k]); });
723
+ if (this._record) { this._record._row({ name: name, op: 'setDeclared', by: by, from: _plain(from), to: _plain(to) }); }
724
+ return { set: name, from: from, to: to, tick: 0, kept: kept, files: Object.assign({}, files) };
725
+ };
726
+
201
727
  RecipeContext.prototype.addTickFunction = function (fn) {
202
728
  _requireWorld(this);
203
729
  var fns = this._runner.universe.experimentTickFunctions;
@@ -259,24 +785,106 @@ import { refuseRetiredDoors } from '../compat/engine.js';
259
785
  * replay on the runner (`runner.retainSceneReplay`), so `runner.restart()` — and a sketch's
260
786
  * declared `restart` control — replays this build on the same runner and canvas.
261
787
  */
262
- RecipeContext.prototype.build = function (buildFn) {
263
- if (buildFn && typeof buildFn === 'function') {
264
- this._buildFn = buildFn;
788
+ RecipeContext.prototype.build = function (sketch) {
789
+ var linked = null;
790
+ if (sketch && typeof sketch === 'object' && typeof sketch.buildExperiment === 'function') {
791
+ if (!sketch.project || typeof sketch.project !== 'object' || !sketch.project.files || typeof sketch.project.entry !== 'string') {
792
+ throw new TypeError('[RecipeContext.build] build(linked): linked is linkProject\'s result (its contract: ' +
793
+ 'lib/shim/project/link.js). (FAIL-LOUD.)');
794
+ }
795
+ linked = sketch;
796
+ sketch = linked.buildExperiment;
797
+ } else if (sketch !== undefined && sketch !== null && typeof sketch !== 'function') {
798
+ throw new TypeError('[RecipeContext.build] build(sketch): sketch is the scene\'s buildExperiment(exp) function or ' +
799
+ 'linkProject\'s result (got ' + typeof sketch + '). (FAIL-LOUD.)');
800
+ }
801
+ if (typeof sketch === 'function') {
802
+ // ONE Record per booted world: a build of text that is not the held text re-authors it, and closes the
803
+ // open Record with the world before (a rebuild of the same text — restart, setDeclared's re-run — does not).
804
+ if (this._record && !(linked && this._project && _sameFiles(linked.project.files, this._project.files))) {
805
+ this._reauthored = this._record;
806
+ this._record._closeOnReauthor(this._project);
807
+ }
808
+ this._buildFn = sketch;
809
+ this._project = linked ? linked.project : null;
810
+ this._urls = linked ? (linked.urls || null) : null;
265
811
  if (this._runner) { _teardownSceneForRebuild(this); }
266
812
  this._world = null;
267
- buildFn(this);
813
+ this._parts = new Map();
814
+ this._code = new Map();
815
+ this._declaredOnce = false;
816
+ sketch(this);
268
817
  if (!this._world) {
269
818
  throw new Error('[RecipeContext.build] the scene never created its world — its first statement ' +
270
819
  'must be ' + COINIT + '. (FAIL-LOUD.)');
271
820
  }
272
821
  this._built = true;
273
822
  var self = this;
274
- this._runner.retainSceneReplay(function () { return self.build(self._buildFn); });
823
+ this._runner.declared.forEach(function (obj) {
824
+ if (!self._parts.has(obj)) { self._code.set(obj, _capture(self, obj)); }
825
+ });
826
+ this._runner.retainSceneReplay(this._project
827
+ ? function () { return linkProject(self._project).then(function (l) { return self.build(l); }); }
828
+ : function () { return self.build(self._buildFn); });
275
829
  }
276
830
  setPageExperiment(this);
277
831
  return this;
278
832
  };
279
833
 
834
+ /** Two file maps hold the same paths with byte-equal text. */
835
+ function _sameFiles(a, b) {
836
+ var ka = Object.keys(a), kb = Object.keys(b);
837
+ if (ka.length !== kb.length) { return false; }
838
+ return ka.every(function (k) { return Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]; });
839
+ }
840
+
841
+ /**
842
+ * exp.startRecord() → Promise<Record> — open a Record (lib/frqtl/objects/record/Record.js) on this facade's world.
843
+ * REFUSES (rejects): a second open Record (one per facade, one per runner); a held project with no frqtl.json
844
+ * (a Record snapshots a folder); a hand the sketch text does not hold yet (as setDeclared). While it is open,
845
+ * toSketch({ by }) and setDeclared(name, patch, { by }) name who acted.
846
+ */
847
+ RecipeContext.prototype.startRecord = async function () {
848
+ _requireWorld(this);
849
+ _requireHeld(this, 'startRecord');
850
+ if (this._record) {
851
+ throw new Error('[RecipeContext.startRecord] a Record is already open on this facade (one per booted world) — ' +
852
+ 'await rec.stop() first. (FAIL-LOUD.)');
853
+ }
854
+ // A re-authoring closed the previous Record; if that close was REFUSED and nobody read it, its rows would be lost
855
+ // without a word: refuse until the host reads `rec.closed` (the rejection carries err.record).
856
+ var last = this._reauthored;
857
+ if (last) {
858
+ await last._settled;
859
+ if (last._refused && !last._closedRead) {
860
+ var err = new Error('[RecipeContext.startRecord] the previous Record of this facade was closed by a re-authoring and ' +
861
+ 'REFUSED (' + last._refused.message + ') — nobody read rec.closed, so its ' + last._refused.record.changes.length +
862
+ ' row(s) would be lost: read rec.closed (its rejection carries err.record = { changes, clips }) first. (FAIL-LOUD.)');
863
+ err.record = last._refused.record;
864
+ throw err;
865
+ }
866
+ this._reauthored = null;
867
+ }
868
+ if (openRecordOf(this._runner)) {
869
+ throw new Error('[RecipeContext.startRecord] another facade holds an open Record on this runner. (FAIL-LOUD.)');
870
+ }
871
+ if (!Object.prototype.hasOwnProperty.call(this._project.files, MANIFEST_FILE)) {
872
+ throw new Error('[RecipeContext.startRecord] a Record snapshots a folder; boot it as one — the held project carries ' +
873
+ 'no ' + MANIFEST_FILE + ': add files[\'' + MANIFEST_FILE + '\'] = serializeManifest(manifestFor({ name, entry: SKETCH_ENTRY })) ' +
874
+ '(all from ./project) to the files you boot. (FAIL-LOUD.)');
875
+ }
876
+ var unwritten = _unwrittenHands(this);
877
+ if (unwritten.length) {
878
+ throw new Error('[RecipeContext.startRecord] "' + unwritten.join('", "') + '" carries a hand the sketch text does ' +
879
+ 'not hold yet — write the hand first: await exp.toSketch(). (FAIL-LOUD.)');
880
+ }
881
+ var rec = new Record(this);
882
+ this._record = rec;
883
+ try { await rec._ready(this._project); }
884
+ catch (e) { rec._detach(); throw e; }
885
+ return rec;
886
+ };
887
+
280
888
  RecipeContext.prototype.run = function (buildFn) {
281
889
  if (buildFn && typeof buildFn === 'function' && !this._built) { this.build(buildFn); }
282
890
  _requireWorld(this);
@@ -298,7 +906,7 @@ import { refuseRetiredDoors } from '../compat/engine.js';
298
906
  var RETIRED_RECIPE_DOORS = Object.freeze({
299
907
  buildSource: '[RecipeContext] exp.buildSource(src) is retired — a sketch held as text is a project: boot it ' +
300
908
  'with bootProject(mount, { files: { "sketch.js": src }, entry: "sketch.js" }) (@descix/frqtl-web/boot), or ' +
301
- 're-scene a booted facade with exp.build((await linkProject({ files, entry, mint })).buildExperiment) ' +
909
+ 're-scene a booted facade with exp.build(await linkProject({ files, entry, mint })) ' +
302
910
  '(lib/shim/project/link.js). (FAIL-LOUD.)'
303
911
  });
304
912
  refuseRetiredDoors(RecipeContext.prototype, RETIRED_RECIPE_DOORS);