@patterkit/runtime 0.13.0 → 0.14.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.
package/dist/index.cjs CHANGED
@@ -72,7 +72,21 @@ function buildTagIndex(bundle) {
72
72
  }
73
73
 
74
74
  // src/engine.ts
75
+ var import_model2 = require("@patterkit/model");
75
76
  var astCache = /* @__PURE__ */ new WeakMap();
77
+ var OWNER = "Patter";
78
+ var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
79
+ var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
80
+ var keys = {
81
+ /** A scene's SHARED `@scene` props (one bag per scene, every flow's). */
82
+ stage: (sceneId) => `patter/scene/${esc(sceneId)}`,
83
+ /** Everything one flow registers starts with this. */
84
+ flow: (flowId) => `patter/flow/${esc(flowId)}/`,
85
+ /** A flow's NOT-shared `@patter` globals. */
86
+ flowGlobals: (flowId) => `patter/flow/${esc(flowId)}/patter`,
87
+ /** A flow's NOT-shared `@scene` props for one scene. */
88
+ flowScene: (flowId, sceneId) => `patter/flow/${esc(flowId)}/scene/${esc(sceneId)}`
89
+ };
76
90
  var Engine = class _Engine {
77
91
  host;
78
92
  defaultSeed;
@@ -132,18 +146,32 @@ var Engine = class _Engine {
132
146
  const patterSharedDecls = props.filter((p) => p.shared ?? true).map(toDecl);
133
147
  const patterLocalDecls = props.filter((p) => !(p.shared ?? true)).map(toDecl);
134
148
  const patterSharedNames = new Set(patterSharedDecls.map((d) => d.name.toLowerCase()));
135
- const shared = new import_scoperegistry.ScopeRegistry().defineOwned("patter", patterSharedDecls, "@patter.");
136
- const hostBound = /* @__PURE__ */ new Set();
137
- if (options.world) {
138
- const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
139
- const decls = (worldSpec?.declarations ?? []).map(toForeignDecl);
140
- shared.defineForeign("world", options.world, decls, worldSpec?.writable ?? true);
141
- hostBound.add("world");
142
- }
143
- for (const spec of bundle.scopeRegistry?.scopes ?? []) {
144
- if (hostBound.has(spec.token)) continue;
145
- const decls = (spec.declarations ?? []).map(toForeignDecl);
146
- shared.defineForeign(spec.token, selfBackedResolver(spec.declarations ?? []), decls, spec.writable ?? true);
149
+ const registry = options.registry ?? new import_scoperegistry.ScopeRegistry();
150
+ const ownsRegistry = !options.registry || options[OWNS_REGISTRY] === true;
151
+ const patterBag = new import_scoperegistry2.PropertyBag(patterSharedDecls, { pathPrefix: "@patter." });
152
+ const hostScopes = [];
153
+ const registered = [];
154
+ try {
155
+ registry.mountOwned("patter", patterBag, { owner: OWNER });
156
+ registered.push("patter");
157
+ const hostBound = /* @__PURE__ */ new Set();
158
+ if (options.world) {
159
+ const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
160
+ const decls = (worldSpec?.declarations ?? []).map(toForeignDecl);
161
+ registry.defineForeign("world", options.world, decls, { writable: worldSpec?.writable ?? true, owner: OWNER });
162
+ registered.push("world");
163
+ hostBound.add("world");
164
+ }
165
+ for (const spec of ownsRegistry ? bundle.scopeRegistry?.scopes ?? [] : []) {
166
+ if (hostBound.has(spec.token) || registry.has(spec.token)) continue;
167
+ const decls = (spec.declarations ?? []).map((d) => selfBackedDecl(d, spec.writable));
168
+ registry.defineOwned(spec.token, decls, { owner: OWNER });
169
+ registered.push(spec.token);
170
+ hostScopes.push(spec.token);
171
+ }
172
+ } catch (e) {
173
+ for (const k of registered) registry.remove(k, { keep: true });
174
+ throw e;
147
175
  }
148
176
  const sceneSharedNames = /* @__PURE__ */ new Map();
149
177
  for (const [sceneId, scene] of Object.entries(bundle.scenes)) {
@@ -165,7 +193,10 @@ var Engine = class _Engine {
165
193
  blockGameIdToId: this.blockGameIdToId,
166
194
  // same instances the engine resolves with
167
195
  tagIndex: buildTagIndex(bundle),
168
- shared,
196
+ registry,
197
+ ownsRegistry,
198
+ patterBag,
199
+ hostScopes,
169
200
  patterSharedDecls,
170
201
  patterLocalDecls,
171
202
  patterSharedNames,
@@ -182,7 +213,8 @@ var Engine = class _Engine {
182
213
  captionClose: (bundle.closedCaptions ?? import_model.DEFAULT_CAPTION_DELIMITERS).close,
183
214
  captionCharacter: bundle.closedCaptions?.character || import_model.DEFAULT_CAPTION_CHARACTER,
184
215
  // absent/empty -> SFX
185
- refSplitCache: /* @__PURE__ */ new Map()
216
+ refSplitCache: /* @__PURE__ */ new Map(),
217
+ refSplitRevision: registry.revision
186
218
  };
187
219
  }
188
220
  /** The active locale (string + character-name lookups resolve in it). */
@@ -227,10 +259,12 @@ var Engine = class _Engine {
227
259
  * content-drift policy (§9.8) resolves edits under the cursor: stack frames re-find their next
228
260
  * child by id, drifted options drop, a vanished snippet is skipped.
229
261
  *
230
- * Returns the REPLACEMENT engine; this one is left untouched and should be discarded. Hosts
231
- * re-bind their flow handles via `next.getFlow(id)`. If the restore throws (defensive - §9.8
232
- * makes this unreachable for ordinary edits), the swap falls back to a cold engine with each
233
- * saved flow restarted from the top of the scene it was in.
262
+ * Returns the REPLACEMENT engine, on the same registry. This one hands its bags over (each is
263
+ * removed from the registry with its values kept, and the replacement claims them as it
264
+ * registers), its flows are closed, and it should be discarded. Hosts re-bind their flow handles
265
+ * via `next.getFlow(id)`. If the restore throws (defensive - §9.8 makes this unreachable for
266
+ * ordinary edits), the swap falls back to a fresh engine with each saved flow restarted from the
267
+ * top of the scene it was in; the shared properties carry over.
234
268
  */
235
269
  hotSwap(bundle) {
236
270
  const snapshot = this.saveGame();
@@ -239,12 +273,19 @@ var Engine = class _Engine {
239
273
  next2.setClosedCaptions(this.host.captionsOn);
240
274
  return next2;
241
275
  };
242
- const next = new _Engine(bundle, this.creationOptions);
276
+ const make = () => new _Engine(bundle, {
277
+ ...this.creationOptions,
278
+ registry: this.host.registry,
279
+ [OWNS_REGISTRY]: this.host.ownsRegistry
280
+ });
281
+ this.release(true);
282
+ const next = make();
243
283
  try {
244
284
  next.loadGame(snapshot);
245
285
  return carryOver(next);
246
286
  } catch {
247
- const fresh = new _Engine(bundle, this.creationOptions);
287
+ next.release(true);
288
+ const fresh = make();
248
289
  for (const [id, f] of Object.entries(snapshot.flows)) {
249
290
  const sceneId = f.cursor.currentSceneId;
250
291
  try {
@@ -283,6 +324,7 @@ var Engine = class _Engine {
283
324
  * call to reach for when you want a speaker's variation state to carry on.
284
325
  */
285
326
  openFlow(id, opts = {}) {
327
+ this.assertExternalScopes();
286
328
  const sceneId = this.resolveSceneRef(opts.scene);
287
329
  const blockId = this.resolveBlockRef(sceneId, opts.block);
288
330
  this.flowsById.get(id)?.close();
@@ -322,6 +364,17 @@ var Engine = class _Engine {
322
364
  }
323
365
  return existing.advanceToStop().played;
324
366
  }
367
+ /** Content that names another engine's scope (`@story.act`) runs only where that engine is on this
368
+ * registry: without it every read would answer false and every write fail, so the flow is refused as
369
+ * it opens (or the save as it loads), before anything changes. By then a game has built all of its
370
+ * engines, whatever order it built them in. The same message on every runtime. */
371
+ assertExternalScopes() {
372
+ for (const token of this.host.bundle.externalScopes ?? []) {
373
+ if (!this.host.registry.has(token)) {
374
+ throw new Error(`this content names @${token}, which no engine on this registry registered: give every engine the game's one registry`);
375
+ }
376
+ }
377
+ }
325
378
  /** Resolve a scene reference (a gameId address OR an internal id) to its internal id. */
326
379
  resolveSceneRef(ref) {
327
380
  if (ref == null) return void 0;
@@ -507,7 +560,7 @@ var Engine = class _Engine {
507
560
  * loadGame() replaces every bag, so re-enumerate after a load.
508
561
  */
509
562
  listBags() {
510
- const mounts = [{ bag: this.host.shared.ownedBag("patter") }];
563
+ const mounts = [{ bag: this.host.patterBag }];
511
564
  for (const [sceneId, bag] of this.host.stageBags) mounts.push({ bag, pathPrefix: `@scene:${sceneId}.` });
512
565
  return mounts;
513
566
  }
@@ -526,15 +579,31 @@ var Engine = class _Engine {
526
579
  reset() {
527
580
  for (const flow of this.flowsById.values()) flow.close();
528
581
  this.flowsById.clear();
529
- this.host.shared.reseedOwned("patter", this.host.patterSharedDecls);
582
+ this.host.patterBag.reseed(this.host.patterSharedDecls);
530
583
  this.host.sharedVisits.clear();
531
584
  this.host.sharedSelectors.clear();
585
+ for (const s of this.host.stageBags.keys()) this.host.registry.remove(keys.stage(s));
586
+ this.host.stageBags.clear();
587
+ this.host.registry.discardParked("patter/");
588
+ }
589
+ /** Remove every bag this engine registered, keeping the values parked when `keep` (a live reload
590
+ * handing its state to a replacement), and close its flows. The engine is inert afterwards. */
591
+ release(keep) {
592
+ for (const flow of this.flowsById.values()) {
593
+ flow.releaseBags(keep);
594
+ flow.close();
595
+ }
596
+ this.flowsById.clear();
597
+ const reg = this.host.registry;
598
+ for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep });
532
599
  this.host.stageBags.clear();
600
+ for (const t of ["patter", ...this.host.hostScopes]) if (reg.has(t)) reg.remove(t, { keep });
601
+ if (this.creationOptions.world && reg.has("world")) reg.remove("world");
533
602
  }
534
603
  /** Read a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level). */
535
604
  getProperty(ref) {
536
605
  const { scope, name } = this.splitShared(ref);
537
- return this.host.shared.get(scope, name);
606
+ return this.host.registry.get(scope, name);
538
607
  }
539
608
  /** Write a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level).
540
609
  *
@@ -545,7 +614,7 @@ var Engine = class _Engine {
545
614
  * were each refused the one property they existed to move.) */
546
615
  setProperty(ref, value) {
547
616
  const { scope, name } = this.splitShared(ref);
548
- this.host.shared.set(scope, name, value, { host: true });
617
+ this.host.registry.set(scope, name, value, { host: true });
549
618
  }
550
619
  /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
551
620
  * value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
@@ -592,54 +661,66 @@ var Engine = class _Engine {
592
661
  // @scene is scene-namespaced and needs a flow's current scene - silently
593
662
  // routing it into the shared bag (as a junk "scene.x" key) was a trap.
594
663
  splitShared(ref) {
595
- let split = this.host.refSplitCache.get(ref);
596
- if (!split) {
597
- split = (0, import_dialect.splitRef)(ref, (t) => t === "scene" || this.host.shared.has(t));
598
- this.host.refSplitCache.set(ref, split);
599
- }
664
+ const split = splitHostRef(this.host, ref);
600
665
  if (split.scope === "scene") {
601
666
  throw new Error(`'${ref}': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);
602
667
  }
603
668
  return split;
604
669
  }
605
- /** Snapshot shared `@patter` state only (for a unified cross-engine save blob, Phase D). */
670
+ /** The shared `@patter` values alone, as `{ patter: {...} }`.
671
+ * @deprecated The registry holds every property now: a game saves `registry.save()` once, and a
672
+ * standalone engine's `saveGame()` carries its registry's values. Removed at the next breaking release. */
606
673
  save() {
607
- return this.host.shared.save();
674
+ return { patter: this.host.patterBag.save() };
608
675
  }
609
- /** Restore shared `@patter` values (world properties untouched). */
676
+ /** Lay a `{ patter: {...} }` section over the shared `@patter` values; anything else is ignored.
677
+ * @deprecated See `save`. Removed at the next breaking release. */
610
678
  load(blob) {
611
- this.host.shared.load(blob);
679
+ if (blob.patter) this.host.patterBag.load(blob.patter);
612
680
  }
613
- /** Snapshot the whole game: shared `@patter` + visit counts + every live flow. */
681
+ /** Snapshot the whole game's NON-property state: visit counts, shared selector cursors, and every
682
+ * live flow's cursor and PRNG. The property values are the registry's: a standalone engine (one that
683
+ * made its own registry) carries them here under `registry`; a game that passed a registry saves it
684
+ * once itself, beside each engine's `saveGame()`. */
614
685
  saveGame() {
615
686
  const flows = {};
616
687
  for (const [id, flow] of this.flowsById) flows[id] = flow.snapshot();
617
688
  return {
618
- version: 2,
619
- shared: this.host.shared.save(),
689
+ version: import_model2.SAVE_VERSION,
690
+ ...this.host.ownsRegistry ? { registry: this.host.registry.save() } : {},
620
691
  sharedVisits: Object.fromEntries(this.host.sharedVisits),
621
692
  sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
622
- stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, bag.save()])),
623
693
  flows
624
694
  };
625
695
  }
626
- /** Restore a `saveGame()`: shared globals + visit counts + shared scene bags + reconstruct every flow. */
696
+ /**
697
+ * Restore a `saveGame()`: visit counts, shared selector cursors, and every flow. Property values come
698
+ * from the registry. A save that carries them (a standalone engine's, or a version 2 save from before
699
+ * the registry held them) has them moved into the registry here; otherwise the game loads its
700
+ * registry itself, before or after this call. Either order works: this engine's bags are handed back
701
+ * to the registry (values kept) and the restored flows claim them as they register.
702
+ */
627
703
  loadGame(save) {
628
- if (save.version !== 2) throw new Error(`unsupported save version: ${save.version}`);
629
- this.host.shared.load(save.shared);
704
+ const version = save.version;
705
+ if (version !== 2 && version !== import_model2.SAVE_VERSION) throw new Error(`unsupported save version: ${String(version)}`);
706
+ this.assertExternalScopes();
707
+ const reg = this.host.registry;
708
+ for (const [id, flow] of this.flowsById) {
709
+ flow.releaseBags(id in save.flows);
710
+ flow.close();
711
+ }
712
+ this.flowsById.clear();
713
+ for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep: true });
714
+ this.host.stageBags.clear();
715
+ const values = save.version === 2 ? sectionsFromV2(save) : save.registry;
716
+ if (values) {
717
+ if (this.host.ownsRegistry) reg.load(values);
718
+ else reg.load(values, { keepParked: true });
719
+ }
630
720
  this.host.sharedVisits.clear();
631
721
  for (const [id, n] of Object.entries(save.sharedVisits ?? {})) this.host.sharedVisits.set(id, n);
632
722
  this.host.sharedSelectors.clear();
633
723
  for (const [id, st] of deserialiseSelectors(save.sharedSelectors)) this.host.sharedSelectors.set(id, st);
634
- this.host.stageBags.clear();
635
- for (const [s, values] of Object.entries(save.stageBags ?? {})) {
636
- const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
637
- const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => shared.has(d.name.toLowerCase()));
638
- const bag = new import_scoperegistry2.PropertyBag(decls);
639
- bag.load(values);
640
- this.host.stageBags.set(s, bag);
641
- }
642
- this.flowsById.clear();
643
724
  for (const [id, snap] of Object.entries(save.flows)) {
644
725
  const flow = new Flow(id, this.host, this.defaultSeed);
645
726
  flow.restore(snap);
@@ -651,7 +732,9 @@ var Flow = class {
651
732
  id;
652
733
  host;
653
734
  local;
654
- // owns "patter" = the NOT-shared globals (this flow's copy)
735
+ // the NOT-shared `@patter` globals (this flow's copy), keys.flowGlobals
736
+ /** The registry keys this flow has registered (its globals and each scene bag). */
737
+ registered = /* @__PURE__ */ new Set();
655
738
  rngState;
656
739
  // Execution cursor. The `stack` is the continuation stack: each frame is a
657
740
  // position within a block's children (the top frame is the active block run;
@@ -676,40 +759,44 @@ var Flow = class {
676
759
  /** Per-node entry counts for this flow (node id -> times entered). */
677
760
  visitCounts = /* @__PURE__ */ new Map();
678
761
  // Per-flow halves of the two scopes. The NOT-shared `@patter` globals live in
679
- // `local` (owned scope "patter"); the NOT-shared `@scene` props live in
680
- // `sceneBags` (namespaced per scene; they PERSIST across re-entries, spec §7).
681
- // The SHARED halves live on the host (`host.shared` / `host.stageBags`). Each
682
- // resolver presents one merged scope, routing each property to its half by the
683
- // declared `shared` flag.
762
+ // `local`; the NOT-shared `@scene` props live in `sceneBags` (namespaced per
763
+ // scene; they PERSIST across re-entries, spec §7). The SHARED halves live on the
764
+ // host (`host.patterBag` / `host.stageBags`). All of them are registered in the
765
+ // game's registry. Each resolver presents one merged scope, routing each property
766
+ // to its half by the declared `shared` flag: that split is why `@patter` and
767
+ // `@scene` are composed here rather than aliased to a single registry key.
684
768
  /** This flow's per-scene LOCAL scene props; see FlowHost.stageBags. */
685
769
  sceneBags = /* @__PURE__ */ new Map();
686
770
  patterResolver = {
687
- get: (n) => this.host.patterSharedNames.has(n) ? this.host.shared.get("patter", n) : this.local.get("patter", n),
771
+ get: (n) => this.host.patterSharedNames.has(n) ? this.host.patterBag.get(n) : this.local.get(n),
688
772
  set: (n, v) => {
689
- if (this.host.patterSharedNames.has(n)) this.host.shared.set("patter", n, v);
690
- else this.local.set("patter", n, v);
773
+ if (this.host.patterSharedNames.has(n)) this.host.registry.set("patter", n, v);
774
+ else this.local.set(n, v);
691
775
  }
692
776
  };
693
777
  sceneResolver = {
694
778
  get: (n) => {
695
779
  const s = this.currentSceneId;
696
780
  if (s === null) return void 0;
697
- const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
698
- return bag?.get(n);
781
+ return this.sceneBagFor(s, n)?.get(n);
699
782
  },
700
783
  set: (n, v) => {
701
784
  const s = this.currentSceneId;
702
785
  if (s === null) return;
703
- const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
786
+ const bag = this.sceneBagFor(s, n);
704
787
  if (bag) bag.set(n, v);
705
788
  }
706
789
  };
707
- // The eval context is built ONCE: every constituent resolves live state at
708
- // call time (shared bags mutate in place per scoperegistry's contract;
709
- // patter/scene route through this flow's resolvers, which read the current
710
- // `local`/`sceneBags`/`currentSceneId`; the host callbacks read current flow
711
- // fields). Rebuilding it per evaluation was the engine's hottest allocation.
790
+ // The eval context is built once and REFRESHED only when the registry's set of
791
+ // scopes moves (its `revision`): every constituent resolves live state at call
792
+ // time (bags mutate in place per scoperegistry's contract; patter/scene route
793
+ // through this flow's resolvers, which read the current `local`/`sceneBags`/
794
+ // `currentSceneId`; the host callbacks read current flow fields), but another
795
+ // engine registering `@story` after this flow opened must still be readable.
796
+ // Rebuilding it per evaluation was the engine's hottest allocation.
712
797
  evalCtx;
798
+ ctxRevision = -1;
799
+ registryQualities;
713
800
  flowLog = [];
714
801
  /** Monotonic across the flow's life; survives clearLog so two reads agree on order. */
715
802
  flowSeq = 0;
@@ -717,12 +804,9 @@ var Flow = class {
717
804
  this.id = id;
718
805
  this.host = host;
719
806
  this.rngState = (0, import_expr.toUint32)(seed);
720
- this.local = this.freshLocal();
721
- const scopes = { ...host.shared.toEvalContext().scopes };
722
- scopes["patter"] = this.patterResolver;
723
- scopes["scene"] = this.sceneResolver;
807
+ this.local = this.newLocal();
724
808
  this.evalCtx = {
725
- scopes,
809
+ scopes: {},
726
810
  host: {
727
811
  nextRandom: this.rng,
728
812
  visits: (id2) => this.visitCounts.get(id2) ?? 0,
@@ -735,6 +819,21 @@ var Flow = class {
735
819
  qualities: (scope, name) => this.stagesFor(scope, name)
736
820
  };
737
821
  }
822
+ /** The eval context, its scopes refreshed if the registry's set of scopes has moved since. */
823
+ context() {
824
+ const reg = this.host.registry;
825
+ if (reg.revision !== this.ctxRevision) {
826
+ const base = reg.toEvalContext();
827
+ const scopes = this.evalCtx.scopes;
828
+ for (const k of Object.keys(scopes)) delete scopes[k];
829
+ Object.assign(scopes, base.scopes);
830
+ scopes["patter"] = this.patterResolver;
831
+ scopes["scene"] = this.sceneResolver;
832
+ this.registryQualities = base.qualities;
833
+ this.ctxRevision = reg.revision;
834
+ }
835
+ return this.evalCtx;
836
+ }
738
837
  /** The stage ladder of `@scope.name` when it is a declared quality, else undefined. Names compare
739
838
  * lowercase, as the compiler emits references (the selfBackedResolver lesson). */
740
839
  stagesFor(scope, name) {
@@ -747,13 +846,14 @@ var Flow = class {
747
846
  const scene = this.currentSceneId != null ? this.host.bundle.scenes[this.currentSceneId] : void 0;
748
847
  return fromDecls(scene?.sceneProps);
749
848
  }
750
- return fromDecls(this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations);
849
+ return this.registryQualities?.(scope, name) ?? fromDecls(this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations);
751
850
  }
752
851
  // -- Host API -------------------------------------------------------------
753
852
  /** Begin this flow at a scene (and optionally a specific block within it). */
754
853
  start(sceneId, blockId) {
755
- this.sceneBags.clear();
756
- this.local = this.freshLocal();
854
+ this.releaseBags(false);
855
+ this.host.registry.discardParked(keys.flow(this.id));
856
+ this.mountLocal();
757
857
  this.selectors.clear();
758
858
  this.visitCounts.clear();
759
859
  this.stack = [];
@@ -853,6 +953,7 @@ var Flow = class {
853
953
  * cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
854
954
  */
855
955
  close() {
956
+ this.releaseBags(false);
856
957
  this.closed = true;
857
958
  this.flowEnded = true;
858
959
  this.stack = [];
@@ -962,7 +1063,7 @@ var Flow = class {
962
1063
  * halves are the Engine's listBags.
963
1064
  */
964
1065
  listBags() {
965
- const mounts = [{ bag: this.local.ownedBag("patter"), pathPrefix: `${this.id}/@patter.` }];
1066
+ const mounts = [{ bag: this.local, pathPrefix: `${this.id}/@patter.` }];
966
1067
  for (const [sceneId, bag] of this.sceneBags) mounts.push({ bag, pathPrefix: `${this.id}/@scene:${sceneId}.` });
967
1068
  return mounts;
968
1069
  }
@@ -1010,7 +1111,7 @@ var Flow = class {
1010
1111
  const { scope, name } = this.splitRef(ref);
1011
1112
  if (scope === "patter") return this.patterResolver.get(name);
1012
1113
  if (scope === "scene") return this.sceneResolver.get(name);
1013
- return this.host.shared.get(scope, name);
1114
+ return this.host.registry.get(scope, name);
1014
1115
  }
1015
1116
  /** Write a property by ref (routed by scope, then by the property's `shared` flag). The GAME's
1016
1117
  * surface: it writes with host authority, so a `writable: false` host declaration does not refuse
@@ -1028,16 +1129,13 @@ var Flow = class {
1028
1129
  if (this.currentSceneId === null) throw new Error(`'${ref}': the flow has not entered a scene yet`);
1029
1130
  this.sceneResolver.set(name, value);
1030
1131
  } else {
1031
- this.host.shared.set(scope, name, value, host ? { host: true } : void 0);
1132
+ this.host.registry.set(scope, name, value, host ? { host: true } : void 0);
1032
1133
  }
1033
1134
  }
1034
1135
  // -- Save / restore (engine-driven) --------------------------------------
1035
- /** @internal Snapshot this flow's cursor + per-flow scopes (not-shared `@patter`/`@scene`) + PRNG. */
1136
+ /** @internal Snapshot this flow's cursor + PRNG + visits. Its properties are the registry's. */
1036
1137
  snapshot() {
1037
1138
  return {
1038
- scopes: this.local.save(),
1039
- // owned scope "patter" = the NOT-shared globals (@scene saved separately)
1040
- sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, bag.save()])),
1041
1139
  rngState: this.rngState,
1042
1140
  visits: Object.fromEntries(this.visitCounts),
1043
1141
  cursor: {
@@ -1075,15 +1173,11 @@ var Flow = class {
1075
1173
  }
1076
1174
  return { ...frame };
1077
1175
  });
1078
- this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s, values]) => {
1079
- const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
1080
- const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => !shared.has(d.name.toLowerCase()));
1081
- const bag = new import_scoperegistry2.PropertyBag(decls);
1082
- bag.load(values);
1083
- return [s, bag];
1084
- }));
1085
- this.local = this.freshLocal();
1086
- this.local.load(snap.scopes);
1176
+ this.releaseBags(false);
1177
+ this.mountLocal();
1178
+ for (const s of /* @__PURE__ */ new Set([c.currentSceneId, ...this.stack.map((f) => f.sceneId)])) {
1179
+ if (s !== null && this.host.bundle.scenes[s]) this.ensureSceneBags(s);
1180
+ }
1087
1181
  this.activeSnippet = null;
1088
1182
  if (c.activeSnippetId !== null) {
1089
1183
  const node = this.host.nodeIndex.get(c.activeSnippetId);
@@ -1354,7 +1448,7 @@ var Flow = class {
1354
1448
  * node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
1355
1449
  */
1356
1450
  matchedSpec(node, want) {
1357
- const evalTruthy = (n) => truthy((0, import_expr.evaluate)(n, this.evalCtx, import_dialect.patterDialect));
1451
+ const evalTruthy = (n) => truthy((0, import_expr.evaluate)(n, this.context(), import_dialect.patterDialect));
1358
1452
  return (0, import_expr_specificity.matchedSpecificity)(node, evalTruthy, { want });
1359
1453
  }
1360
1454
  /** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
@@ -1381,7 +1475,7 @@ var Flow = class {
1381
1475
  return truthy(this.evalExpr(node.condition));
1382
1476
  }
1383
1477
  evalExpr(expr) {
1384
- return (0, import_expr.evaluate)(this.conditionAst(expr), this.evalCtx, import_dialect.patterDialect);
1478
+ return (0, import_expr.evaluate)(this.conditionAst(expr), this.context(), import_dialect.patterDialect);
1385
1479
  }
1386
1480
  /** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
1387
1481
  * evaluator and the Best-match specificity walker so both work off one parse. */
@@ -1506,18 +1600,51 @@ var Flow = class {
1506
1600
  const key = (0, import_model.castStringKey)(character);
1507
1601
  return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
1508
1602
  }
1509
- /** Split a ref into scope + name. Tokens: `@scene`, foreign tokens, else `@patter` (incl. bare `@name`). */
1603
+ /** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
1510
1604
  splitRef(ref) {
1511
- let hit = this.host.refSplitCache.get(ref);
1512
- if (!hit) {
1513
- hit = (0, import_dialect.splitRef)(ref, (t) => t === "scene" || this.host.shared.has(t));
1514
- this.host.refSplitCache.set(ref, hit);
1515
- }
1516
- return hit;
1605
+ return splitHostRef(this.host, ref);
1606
+ }
1607
+ /** A fresh, unregistered bag for the NOT-shared `@patter` globals (the shared ones live on the host). */
1608
+ newLocal() {
1609
+ return new import_scoperegistry2.PropertyBag(this.host.patterLocalDecls, { pathPrefix: "@patter." });
1610
+ }
1611
+ /** Register a fresh globals bag under this flow's key; it claims any values waiting there. */
1612
+ mountLocal() {
1613
+ this.local = this.newLocal();
1614
+ const key = keys.flowGlobals(this.id);
1615
+ this.host.registry.mountOwned(key, this.local, { owner: OWNER });
1616
+ this.registered.add(key);
1617
+ }
1618
+ /** @internal Remove every bag this flow registered; with `keep`, their values wait in the registry for
1619
+ * the flow that replaces this one. Engine-driven (close, loadGame, hotSwap). */
1620
+ releaseBags(keep) {
1621
+ for (const key of this.registered) this.host.registry.remove(key, { keep });
1622
+ this.registered.clear();
1623
+ this.sceneBags.clear();
1517
1624
  }
1518
- /** The per-flow registry: the NOT-shared `@patter` globals (the shared ones live on the host). */
1519
- freshLocal() {
1520
- return new import_scoperegistry.ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls, "@patter.");
1625
+ /** The bag a `@scene` property of scene `s` lives in (stage or this flow's), made if missing. */
1626
+ sceneBagFor(s, name) {
1627
+ if (!this.host.bundle.scenes[s]) return void 0;
1628
+ this.ensureSceneBags(s);
1629
+ return this.host.sceneSharedNames.get(s)?.has(name.toLowerCase()) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
1630
+ }
1631
+ /** Make (and register) scene `s`'s stage bag and this flow's bag for it, if not made yet. A bag made
1632
+ * here claims whatever values the registry holds for its key: that is how a loaded save reaches it. */
1633
+ ensureSceneBags(s) {
1634
+ const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
1635
+ const props = this.host.bundle.scenes[s]?.sceneProps ?? [];
1636
+ if (!this.sceneBags.has(s)) {
1637
+ const bag = new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
1638
+ const key = keys.flowScene(this.id, s);
1639
+ this.host.registry.mountOwned(key, bag, { owner: OWNER });
1640
+ this.registered.add(key);
1641
+ this.sceneBags.set(s, bag);
1642
+ }
1643
+ if (!this.host.stageBags.has(s)) {
1644
+ const bag = new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase())), { pathPrefix: "@scene." });
1645
+ this.host.registry.mountOwned(keys.stage(s), bag, { owner: OWNER });
1646
+ this.host.stageBags.set(s, bag);
1647
+ }
1521
1648
  }
1522
1649
  /**
1523
1650
  * Seed a scene's `@scene` props (spec §7). The not-shared props seed THIS flow's
@@ -1528,13 +1655,7 @@ var Flow = class {
1528
1655
  */
1529
1656
  seedScene(scene) {
1530
1657
  const shared = this.host.sceneSharedNames.get(scene.id) ?? /* @__PURE__ */ new Set();
1531
- const props = scene.sceneProps ?? [];
1532
- if (!this.sceneBags.has(scene.id)) {
1533
- this.sceneBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase()))));
1534
- }
1535
- if (!this.host.stageBags.has(scene.id)) {
1536
- this.host.stageBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase()))));
1537
- }
1658
+ this.ensureSceneBags(scene.id);
1538
1659
  for (const decl of scene.sceneProps ?? []) {
1539
1660
  if (!decl.temporary) continue;
1540
1661
  const name = decl.name.toLowerCase();
@@ -1580,16 +1701,32 @@ function toDecl(decl) {
1580
1701
  function toForeignDecl(decl) {
1581
1702
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
1582
1703
  }
1583
- function selfBackedResolver(decls) {
1584
- const key = (name) => name.toLowerCase();
1585
- const bag = /* @__PURE__ */ new Map();
1586
- for (const d of decls) bag.set(key(d.name), (0, import_scoperegistry2.defaultFor)(d));
1587
- return {
1588
- get: (name) => bag.get(key(name)),
1589
- set: (name, value) => {
1590
- bag.set(key(name), value);
1591
- }
1592
- };
1704
+ function selfBackedDecl(decl, scopeWritable) {
1705
+ const d = toForeignDecl(decl);
1706
+ const writable = decl.writable ?? scopeWritable;
1707
+ return writable === void 0 ? d : { ...d, writable };
1708
+ }
1709
+ function splitHostRef(host, ref) {
1710
+ if (host.refSplitRevision !== host.registry.revision) {
1711
+ host.refSplitCache.clear();
1712
+ host.refSplitRevision = host.registry.revision;
1713
+ }
1714
+ let hit = host.refSplitCache.get(ref);
1715
+ if (!hit) {
1716
+ hit = (0, import_dialect.splitRef)(ref, (t) => t === "scene" || host.registry.has(t) || (host.bundle.externalScopes?.includes(t) ?? false));
1717
+ host.refSplitCache.set(ref, hit);
1718
+ }
1719
+ return hit;
1720
+ }
1721
+ function sectionsFromV2(save) {
1722
+ const out = {};
1723
+ if (save.shared?.patter) out["patter"] = save.shared.patter;
1724
+ for (const [s, v] of Object.entries(save.stageBags ?? {})) out[keys.stage(s)] = v;
1725
+ for (const [id, f] of Object.entries(save.flows ?? {})) {
1726
+ if (f.scopes?.patter) out[keys.flowGlobals(id)] = f.scopes.patter;
1727
+ for (const [s, v] of Object.entries(f.sceneBags ?? {})) out[keys.flowScene(id, s)] = v;
1728
+ }
1729
+ return out;
1593
1730
  }
1594
1731
  function truthy(v) {
1595
1732
  if (typeof v === "boolean") return v;
@@ -1599,7 +1736,7 @@ function truthy(v) {
1599
1736
  }
1600
1737
 
1601
1738
  // src/describe.ts
1602
- var import_model2 = require("@patterkit/model");
1739
+ var import_model3 = require("@patterkit/model");
1603
1740
  var isShared = (d, scopeDefault) => d.shared ?? scopeDefault;
1604
1741
  function summariseProperty(d, scopeDefault) {
1605
1742
  return {
@@ -1652,11 +1789,11 @@ function describeBundle(bundle) {
1652
1789
  const sceneProps = [];
1653
1790
  for (const scene of Object.values(bundle.scenes)) {
1654
1791
  counts.scenes++;
1655
- const gameId = (0, import_model2.effectiveGameId)(scene);
1792
+ const gameId = (0, import_model3.effectiveGameId)(scene);
1656
1793
  addresses.push({
1657
1794
  gameId,
1658
1795
  name: scene.name,
1659
- blocks: scene.blocks.map((b) => ({ gameId: (0, import_model2.effectiveGameId)(b), name: b.name }))
1796
+ blocks: scene.blocks.map((b) => ({ gameId: (0, import_model3.effectiveGameId)(b), name: b.name }))
1660
1797
  });
1661
1798
  for (const block of scene.blocks) countBlock(block, counts);
1662
1799
  if (scene.sceneProps?.length) {