@patterkit/runtime 0.19.0 → 0.21.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/CHANGELOG.md CHANGED
@@ -33,6 +33,62 @@ version number always means the same runtime behaviour. This package is versione
33
33
 
34
34
  ## [Unreleased]
35
35
 
36
+ ## [0.21.0] - 2026-10-07
37
+
38
+ ### Changed
39
+
40
+ - **A new `Engine` on a bundle another engine already uses no longer rebuilds the tag index.** It is built once
41
+ per bundle and shared, so a tool that starts a fresh engine for every run (coverage does) starts each in about
42
+ a third of the time: 0.5 ms rather than 1.6 ms on a 100-scene project.
43
+ - **The conformance corpus covers more, and every runtime is held to it**: the outline and beat sequence, the
44
+ audio resolver's joins, `listProperties`, what a checkpoint refuses while it is open, a save taken inside one,
45
+ and a block named under another scene.
46
+
47
+ ### Fixed
48
+
49
+ - **Tags handed out are the caller's own.** `tagsForBeat`, `tagsForScene`, `tagsForBlock`, and a delivered
50
+ line's `tags` are copies, so a game that edits one changes nothing the engine says afterwards.
51
+
52
+ ## [0.20.0] - 2026-10-06
53
+
54
+ ### Added
55
+
56
+ - **`hostScopes`, the engine option every other runtime takes**: the scopes the game binds, by token, each a `{
57
+ get, set? }` over values it owns, as in `hostScopes: { world: { get, set } }`. A resolver's type is
58
+ `HostScope`.
59
+ - **`engine.buildId`**, as the other runtimes have: the bundle's content hash, the build a debug link handshakes
60
+ with.
61
+ - **A bundle-description section in the conformance corpus.** What `describeBundle` says about one bundle, field
62
+ for field, held on every runtime.
63
+ - **`getVisitCounts()` on the engine and on a flow**, on every runtime: how many times each node has been
64
+ entered, across every flow or in this one, read without a save.
65
+
66
+ ### Changed
67
+
68
+ - **Less work per step.** A decision's log entry is built only while the log is on or something is tracing, and
69
+ a quality's stage ladder is looked up once per set of declarations rather than scanned for on every
70
+ comparison.
71
+
72
+ ### Deprecated
73
+
74
+ - **`flow.start()` is deprecated: use `flow.reset()`.** They were the same call under two names. `flow.reset()`
75
+ is the one public name for beginning a flow again on every runtime (opening a flow begins a new one);
76
+ `flow.start()` goes in a later release.
77
+ - **The `world` option and the `WorldResolver` type: use `hostScopes: { world }` and `HostScope`.** `world` is
78
+ still the world scope, and goes in a later release.
79
+ - **The `TraceHandler` type.** Nothing takes one: `onTrace` takes an `EngineTraceHandler`. It goes in a later
80
+ release.
81
+
82
+ ### Fixed
83
+
84
+ - **`reset()` no longer replays a prompt from the run it abandoned.** With prompt replay on, `choose()` leaves
85
+ the chosen option's prompt waiting for the next `advance()`. `reset()` in between cleared only the choice, so
86
+ the old prompt played as the restarted run's first beat. Every move that leaves the flow's place (`reset()`,
87
+ `goto`, `close`) now drops both, through one helper.
88
+ - **`onTrace` hears every decision with the log off.** A `run` walk past a snippet whose condition failed, and a
89
+ write's previous value, were only worked out when the log was on, so a tool tracing a run without the log
90
+ missed the one and got a write without the other. They are now worked out whenever anything is tracing.
91
+
36
92
  ## [0.19.0] - 2026-10-06
37
93
 
38
94
  ### Added
package/README.md CHANGED
@@ -44,9 +44,10 @@ for (;;) {
44
44
  }
45
45
  ```
46
46
 
47
- `new Engine(bundle, options)` takes `{ rng?, seed?, locale?, world?, registry?, replayPromptOnChoose?, closedCaptions? }`:
48
- - `world` is the host's resolver for `@world` properties (World Properties): `{ get(name), set?(name, value) }`.
49
- Omit it and the runtime self-backs `@world` from the declared defaults, as a property it saves.
47
+ `new Engine(bundle, options)` takes `{ rng?, seed?, locale?, hostScopes?, registry?, replayPromptOnChoose?, closedCaptions? }`:
48
+ - `hostScopes` binds the scopes your game owns, by token, each `{ get(name), set?(name, value) }`: for World
49
+ Properties, `hostScopes: { world: { get, set } }`. Leave `world` unbound and the runtime self-backs `@world`
50
+ from the declared defaults, as a property it saves.
50
51
  - `registry` is the game's one `ScopeRegistry` (from `@wildwinter/scoperegistry`), holding every
51
52
  engine's properties. Omit it and the engine makes its own. See *One registry per game* below.
52
53
  - `locale` plays a non-default language (embedded localisation; an IDs-only bundle ignores it).
package/dist/index.cjs CHANGED
@@ -76,6 +76,18 @@ function buildTagIndex(bundle) {
76
76
  // src/engine.ts
77
77
  var import_model2 = require("@patterkit/model");
78
78
  var astCache = /* @__PURE__ */ new WeakMap();
79
+ var tagIndexCache = /* @__PURE__ */ new WeakMap();
80
+ function tagIndexFor(bundle) {
81
+ let index = tagIndexCache.get(bundle);
82
+ if (!index) {
83
+ index = buildTagIndex(bundle);
84
+ tagIndexCache.set(bundle, index);
85
+ }
86
+ return index;
87
+ }
88
+ function tagsOf(host, id) {
89
+ return host.tagIndex.get(id)?.slice();
90
+ }
79
91
  var OWNER = "Patter";
80
92
  var esc = (id) => id.replace(/%/g, "%25").replace(/\//g, "%2F");
81
93
  var OWNS_REGISTRY = /* @__PURE__ */ Symbol("ownsRegistry");
@@ -114,7 +126,7 @@ var Engine = class _Engine {
114
126
  sceneGameIdToId = /* @__PURE__ */ new Map();
115
127
  blockGameIdToId = /* @__PURE__ */ new Map();
116
128
  /** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
117
- * engine keeps the same world resolver, custom RNG, and diagnostic hooks. */
129
+ * engine keeps the same host scopes, custom RNG, and diagnostic hooks. */
118
130
  creationOptions;
119
131
  /** The run's ordered stream: every flow's events, each naming its flow. Empty and
120
132
  * unwritten unless `options.log` asked for it. */
@@ -166,12 +178,12 @@ var Engine = class _Engine {
166
178
  registry.mountOwned("patter", patterBag, { owner: OWNER });
167
179
  registered.push("patter");
168
180
  const hostBound = /* @__PURE__ */ new Set();
169
- if (options.world) {
170
- const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
171
- const decls = (worldSpec?.declarations ?? []).map(toForeignDecl);
172
- registry.defineForeign("world", options.world, decls, { writable: worldSpec?.writable ?? true, owner: OWNER });
173
- registered.push("world");
174
- hostBound.add("world");
181
+ for (const [token, resolver] of Object.entries(boundScopes(options))) {
182
+ const spec = bundle.scopeRegistry?.scopes.find((s) => s.token === token);
183
+ const decls = (spec?.declarations ?? []).map(toForeignDecl);
184
+ registry.defineForeign(token, resolver, decls, { writable: spec?.writable ?? true, owner: OWNER });
185
+ registered.push(token);
186
+ hostBound.add(token);
175
187
  }
176
188
  for (const spec of ownsRegistry ? bundle.scopeRegistry?.scopes ?? [] : []) {
177
189
  if (hostBound.has(spec.token) || registry.has(spec.token)) continue;
@@ -191,6 +203,7 @@ var Engine = class _Engine {
191
203
  }
192
204
  this.host = {
193
205
  logEnabled: options.log ?? false,
206
+ tracing: options.log ?? false,
194
207
  emitEngine: (flow, event, scene) => this.emitEngine(flow, event, scene),
195
208
  bundle,
196
209
  emitIds,
@@ -203,7 +216,7 @@ var Engine = class _Engine {
203
216
  sceneGameIdToId: this.sceneGameIdToId,
204
217
  blockGameIdToId: this.blockGameIdToId,
205
218
  // same instances the engine resolves with
206
- tagIndex: buildTagIndex(bundle),
219
+ tagIndex: tagIndexFor(bundle),
207
220
  registry,
208
221
  ownsRegistry,
209
222
  patterBag,
@@ -213,6 +226,7 @@ var Engine = class _Engine {
213
226
  patterSharedNames,
214
227
  sceneSharedNames,
215
228
  sharedVisits: /* @__PURE__ */ new Map(),
229
+ qualityLadders: /* @__PURE__ */ new Map(),
216
230
  sharedSelectors: /* @__PURE__ */ new Map(),
217
231
  stageBags: /* @__PURE__ */ new Map(),
218
232
  journal: null,
@@ -239,6 +253,11 @@ var Engine = class _Engine {
239
253
  get isSourceDebug() {
240
254
  return this.sourceDebug;
241
255
  }
256
+ /** The bundle's build identity (its content hash), the one a debug link handshakes with, or an empty
257
+ * string for a bundle compiled without one. */
258
+ get buildId() {
259
+ return this.host.bundle.content?.hash ?? "";
260
+ }
242
261
  /**
243
262
  * Switch the active locale LIVE - a real game's "language" setting can change mid-session. Subsequent
244
263
  * string lookups (new beats, re-resolved character names, `{@ref}` interpolation) render in the new
@@ -359,7 +378,7 @@ var Engine = class _Engine {
359
378
  });
360
379
  }
361
380
  this.flowsById.set(id, flow);
362
- flow.start(sceneId, blockId);
381
+ flow.begin(sceneId, blockId);
363
382
  return flow;
364
383
  }
365
384
  /**
@@ -456,18 +475,18 @@ var Engine = class _Engine {
456
475
  * delivered step carries. Empty array for an unknown id or a beat with no tags anywhere up the chain.
457
476
  */
458
477
  tagsForBeat(beatId) {
459
- return this.host.tagIndex.get(beatId) ?? [];
478
+ return tagsOf(this.host, beatId) ?? [];
460
479
  }
461
480
  /** A scene's own tags (by internal id or gameId address). Empty when none / unknown. */
462
481
  tagsForScene(sceneRef) {
463
482
  const id = this.resolveSceneRef(sceneRef);
464
- return (id != null ? this.host.tagIndex.get(id) : void 0) ?? [];
483
+ return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
465
484
  }
466
485
  /** A block's accumulated tags (scene + block), by scene + block ref (id or gameId). Empty when none / unknown. */
467
486
  tagsForBlock(sceneRef, blockRef) {
468
487
  const sceneId = this.resolveSceneRef(sceneRef);
469
488
  const id = this.resolveBlockRef(sceneId, blockRef);
470
- return (id != null ? this.host.tagIndex.get(id) : void 0) ?? [];
489
+ return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
471
490
  }
472
491
  /**
473
492
  * A scene's own author gameData (by internal id or gameId address): the RAW sparse overrides, exactly
@@ -586,7 +605,7 @@ var Engine = class _Engine {
586
605
  }
587
606
  /** One beat's static data (source locale), the same shape a delivered step carries. */
588
607
  beatInfo(beat) {
589
- const tags = this.host.tagIndex.get(beat.id);
608
+ const tags = tagsOf(this.host, beat.id);
590
609
  const info = { id: beat.id, kind: beat.kind };
591
610
  if (beat.kind === "line") {
592
611
  if (beat.character !== void 0) {
@@ -606,7 +625,7 @@ var Engine = class _Engine {
606
625
  }
607
626
  /** A `{ tags }` fragment for an id, present only when the id has accumulated tags (keeps output tidy). */
608
627
  tagsField(id) {
609
- const tags = this.host.tagIndex.get(id);
628
+ const tags = tagsOf(this.host, id);
610
629
  return tags && tags.length ? { tags } : {};
611
630
  }
612
631
  /** Retrieve an open flow by id (undefined if none / closed). */
@@ -723,7 +742,7 @@ var Engine = class _Engine {
723
742
  for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep });
724
743
  this.host.stageBags.clear();
725
744
  for (const t of ["patter", ...this.host.hostScopes]) if (reg.has(t)) reg.remove(t, { keep });
726
- if (this.creationOptions.world && reg.has("world")) reg.remove("world");
745
+ for (const t of Object.keys(boundScopes(this.creationOptions))) if (reg.has(t)) reg.remove(t);
727
746
  }
728
747
  /** Read a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level). */
729
748
  getProperty(ref) {
@@ -743,8 +762,6 @@ var Engine = class _Engine {
743
762
  this.host.registry.set(scope, name, value, { host: true });
744
763
  if (this.host.journal && prev !== void 0) this.host.journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
745
764
  }
746
- /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
747
- * value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
748
765
  /** The run's decisions, in order, each naming the flow it happened in. Empty unless the
749
766
  * run was opened with `log: true`. A flow's own log stays flow-local; this is the only
750
767
  * place a story spanning several flows reads as one sequence. */
@@ -760,13 +777,24 @@ var Engine = class _Engine {
760
777
  * retained. Returns its own unsubscribe. */
761
778
  onTrace(handler) {
762
779
  this.engineTraceHandlers.add(handler);
763
- return () => this.engineTraceHandlers.delete(handler);
780
+ this.host.tracing = true;
781
+ return () => {
782
+ this.engineTraceHandlers.delete(handler);
783
+ this.host.tracing = this.host.logEnabled || this.engineTraceHandlers.size > 0;
784
+ };
764
785
  }
765
786
  emitEngine(flow, event, scene) {
766
787
  for (const h of this.engineTraceHandlers) h(flow, event);
767
788
  if (!this.host.logEnabled) return;
768
789
  this.engineLog.push({ ...event, flow, seq: this.engineSeq++, ...scene ? { scene } : {} });
769
790
  }
791
+ /** How many times each node has been entered across every flow, by node id: the shared count. With a
792
+ * flow's own {@link Flow.getVisitCounts}, every visit count the run keeps, read without a save. */
793
+ getVisitCounts() {
794
+ return Object.fromEntries(this.host.sharedVisits);
795
+ }
796
+ /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
797
+ * value, declared default (for reset), and enum options. Every runtime has the same call. */
770
798
  listProperties() {
771
799
  return this.host.patterSharedDecls.map((d) => ({
772
800
  name: d.name,
@@ -971,19 +999,21 @@ var Flow = class {
971
999
  * lowercase, as the compiler emits references (the selfBackedResolver lesson). */
972
1000
  stagesFor(scope, name) {
973
1001
  const key = name.toLowerCase();
974
- const fromDecls = (decls) => decls?.find((d) => d.name.toLowerCase() === key && d.type === "quality")?.stages;
975
1002
  if (scope === "patter") {
976
- return fromDecls(this.host.patterSharedDecls) ?? fromDecls(this.host.patterLocalDecls);
1003
+ return ladders(this.host, "patter", () => [...this.host.patterSharedDecls, ...this.host.patterLocalDecls]).get(key);
977
1004
  }
978
1005
  if (scope === "scene") {
979
- const scene = this.currentSceneId != null ? this.host.bundle.scenes[this.currentSceneId] : void 0;
980
- return fromDecls(scene?.sceneProps);
1006
+ const sceneId = this.currentSceneId;
1007
+ if (sceneId == null) return void 0;
1008
+ return ladders(this.host, `scene/${sceneId}`, () => this.host.bundle.scenes[sceneId]?.sceneProps).get(key);
981
1009
  }
982
- return this.registryQualities?.(scope, name) ?? fromDecls(this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations);
1010
+ return this.registryQualities?.(scope, name) ?? ladders(this.host, `host/${scope}`, () => this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations).get(key);
983
1011
  }
984
1012
  // -- Host API -------------------------------------------------------------
985
- /** Begin this flow at a scene (and optionally a specific block within it). */
986
- start(sceneId, blockId) {
1013
+ /** Begin this flow at a scene (and optionally a specific block within it). The engine's own entry point:
1014
+ * openFlow and a goto on an unstarted flow begin a flow here. A game calls {@link reset}.
1015
+ * @internal */
1016
+ begin(sceneId, blockId) {
987
1017
  if (this.host.journal && !this.host.journal.opened.has(this)) throw new Error("a flow can't be started or reset while a checkpoint is open");
988
1018
  this.releaseBags(false);
989
1019
  this.host.registry.discardParked(keys.flow(this.id));
@@ -995,7 +1025,7 @@ var Flow = class {
995
1025
  this.flowEnded = false;
996
1026
  this.activeSnippet = null;
997
1027
  this.beatIndex = 0;
998
- this.pendingChoice = null;
1028
+ this.clearPending();
999
1029
  this.started = true;
1000
1030
  if (blockId) {
1001
1031
  const loc = this.host.blockIndex.get(blockId);
@@ -1020,10 +1050,15 @@ var Flow = class {
1020
1050
  * Forget everything in this flow and begin again - its per-flow state (not-shared
1021
1051
  * `@patter` globals + `@scene` props), cursor, callstack, selector cursors, and
1022
1052
  * visit counts. Shared state (shared `@patter` / `@scene`, world visit counts) is
1023
- * untouched. A clearer-named alias of `start()`.
1053
+ * untouched. The one public way to begin a flow again (openFlow begins a new one).
1024
1054
  */
1025
1055
  reset(sceneId, blockId) {
1026
- this.start(sceneId, blockId);
1056
+ this.begin(sceneId, blockId);
1057
+ }
1058
+ /** @deprecated Use {@link reset}, the one public name for beginning a flow again on every runtime. `start`
1059
+ * was the same call under a second name, and goes in a later release. */
1060
+ start(sceneId, blockId) {
1061
+ this.reset(sceneId, blockId);
1027
1062
  }
1028
1063
  /**
1029
1064
  * Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
@@ -1047,9 +1082,7 @@ var Flow = class {
1047
1082
  this.touch();
1048
1083
  if (scene === "END") {
1049
1084
  this.started = true;
1050
- this.pendingChoice = null;
1051
- this.pendingPrompt = null;
1052
- this.pendingPromptOwnerId = null;
1085
+ this.clearPending();
1053
1086
  this.activeSnippet = null;
1054
1087
  this.beatIndex = 0;
1055
1088
  this.flowEnded = true;
@@ -1064,12 +1097,10 @@ var Flow = class {
1064
1097
  if (blockId === void 0) return false;
1065
1098
  }
1066
1099
  if (!this.started) {
1067
- this.start(sceneId, blockId);
1100
+ this.begin(sceneId, blockId);
1068
1101
  return true;
1069
1102
  }
1070
- this.pendingChoice = null;
1071
- this.pendingPrompt = null;
1072
- this.pendingPromptOwnerId = null;
1103
+ this.clearPending();
1073
1104
  this.activeSnippet = null;
1074
1105
  this.beatIndex = 0;
1075
1106
  this.flowEnded = false;
@@ -1094,6 +1125,15 @@ var Flow = class {
1094
1125
  this.stack = [];
1095
1126
  this.activeSnippet = null;
1096
1127
  this.beatIndex = 0;
1128
+ this.clearPending();
1129
+ }
1130
+ /**
1131
+ * Drop everything waiting to be delivered: an open choice, and a chosen option's prompt still to be
1132
+ * replayed. Every move that abandons the flow's place (start and reset, goto, close) does this, so none
1133
+ * leaves a stale prompt behind. start() cleared only the choice, so a reset between choose() and the
1134
+ * next advance() replayed the abandoned run's prompt as the new run's first beat.
1135
+ */
1136
+ clearPending() {
1097
1137
  this.pendingChoice = null;
1098
1138
  this.pendingPrompt = null;
1099
1139
  this.pendingPromptOwnerId = null;
@@ -1190,7 +1230,7 @@ var Flow = class {
1190
1230
  }
1191
1231
  const from = frame.index;
1192
1232
  while (frame.index < children.length && !this.eligible(children[frame.index])) frame.index++;
1193
- if (this.host.logEnabled && frame.index !== from) {
1233
+ if (this.host.tracing && frame.index !== from) {
1194
1234
  this.emit({
1195
1235
  type: "select",
1196
1236
  group: frame.containerId,
@@ -1226,14 +1266,18 @@ var Flow = class {
1226
1266
  clearLog() {
1227
1267
  this.flowLog.length = 0;
1228
1268
  }
1229
- /** Record one decision, on this flow's log and the engine's. Cheap to call with logging
1230
- * off: the entry is never built. */
1269
+ /** Record one decision, on this flow's log and the engine's, and hand it to any `onTrace`. Each call
1270
+ * site checks `host.tracing` first, so with nothing logging or tracing the entry is never built. */
1231
1271
  emit(event) {
1232
1272
  const scene = this.currentSceneId ?? void 0;
1233
1273
  this.host.emitEngine(this.id, event, scene);
1234
1274
  if (!this.host.logEnabled) return;
1235
1275
  this.flowLog.push({ ...event, seq: this.flowSeq++, ...scene ? { scene } : {} });
1236
1276
  }
1277
+ /** How many times this flow has entered each node, by node id. */
1278
+ getVisitCounts() {
1279
+ return Object.fromEntries(this.visitCounts);
1280
+ }
1237
1281
  /** The options of a pending choice (empty when not at a choice point). */
1238
1282
  getChoices() {
1239
1283
  return this.pendingChoice?.options ?? [];
@@ -1247,7 +1291,7 @@ var Flow = class {
1247
1291
  if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
1248
1292
  this.touch();
1249
1293
  const node = choice.byId.get(id);
1250
- this.emit({ type: "chose", group: choice.groupId, option: id });
1294
+ if (this.host.tracing) this.emit({ type: "chose", group: choice.groupId, option: id });
1251
1295
  this.pendingChoice = null;
1252
1296
  const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
1253
1297
  this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
@@ -1443,7 +1487,7 @@ var Flow = class {
1443
1487
  byId.set(child.id, child);
1444
1488
  }
1445
1489
  if (options.some((o) => o.eligible)) {
1446
- this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
1490
+ if (this.host.tracing) this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
1447
1491
  this.pendingChoice = { groupId: group.id, options, byId };
1448
1492
  return;
1449
1493
  }
@@ -1452,7 +1496,7 @@ var Flow = class {
1452
1496
  this.enterChild(fallback);
1453
1497
  return;
1454
1498
  }
1455
- this.emit({ type: "dry", group: group.id });
1499
+ if (this.host.tracing) this.emit({ type: "dry", group: group.id });
1456
1500
  this.host.onDryChoice?.(group.id);
1457
1501
  }
1458
1502
  // -- Jumps (jump / call-return) ----------------------------------------
@@ -1467,7 +1511,7 @@ var Flow = class {
1467
1511
  * hard-ends the flow regardless of the callstack.
1468
1512
  */
1469
1513
  enterTarget(to, mode) {
1470
- this.emit({ type: "jump", to, mode });
1514
+ if (this.host.tracing) this.emit({ type: "jump", to, mode });
1471
1515
  if (to === "END") {
1472
1516
  this.flowEnded = true;
1473
1517
  this.stack = [];
@@ -1504,7 +1548,7 @@ var Flow = class {
1504
1548
  const order = group.options?.order ?? "sequential";
1505
1549
  const exhaust = group.options?.exhaust ?? "once";
1506
1550
  const trace = (picked) => {
1507
- this.emit({
1551
+ if (this.host.tracing) this.emit({
1508
1552
  type: "select",
1509
1553
  group: group.id,
1510
1554
  selector: group.selector ?? "default",
@@ -1669,9 +1713,9 @@ var Flow = class {
1669
1713
  let value;
1670
1714
  try {
1671
1715
  value = this.evalExpr(e.value);
1672
- const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
1716
+ const prev = this.host.tracing ? this.getProperty(e.target) : void 0;
1673
1717
  this.writeProperty(e.target, value, false);
1674
- this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
1718
+ if (this.host.tracing) this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
1675
1719
  } catch (err) {
1676
1720
  this.reportError("effect", owner, e.value, err);
1677
1721
  }
@@ -1692,7 +1736,7 @@ var Flow = class {
1692
1736
  const message = err instanceof Error ? err.message : String(err);
1693
1737
  const source = expr?.src;
1694
1738
  this.host.onError({ flow: this.id, kind, node, ...source ? { source } : {}, message });
1695
- this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
1739
+ if (this.host.tracing) this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
1696
1740
  }
1697
1741
  evalExpr(expr) {
1698
1742
  return (0, import_expr.evaluate)(this.conditionAst(expr), this.context(), import_dialect.patterDialect);
@@ -1738,7 +1782,7 @@ var Flow = class {
1738
1782
  };
1739
1783
  // -- Strings / beats ------------------------------------------------------
1740
1784
  beatResult(beat) {
1741
- const tags = this.host.tagIndex.get(beat.id);
1785
+ const tags = tagsOf(this.host, beat.id);
1742
1786
  const withTags = tags && tags.length ? { tags } : {};
1743
1787
  switch (beat.kind) {
1744
1788
  case "gameEvent":
@@ -1805,7 +1849,7 @@ var Flow = class {
1805
1849
  /** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
1806
1850
  * choice showed. */
1807
1851
  promptResult(beat, shown) {
1808
- const tags = this.host.tagIndex.get(beat.id);
1852
+ const tags = tagsOf(this.host, beat.id);
1809
1853
  const withTags = tags && tags.length ? { tags } : {};
1810
1854
  if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
1811
1855
  return {
@@ -1979,6 +2023,21 @@ function mountClaims(registry, key, bag, ask) {
1979
2023
  function toDecl(decl) {
1980
2024
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
1981
2025
  }
2026
+ function boundScopes(options) {
2027
+ return { ...options.world ? { world: options.world } : {}, ...options.hostScopes };
2028
+ }
2029
+ function ladders(host, cacheKey, decls) {
2030
+ let found = host.qualityLadders.get(cacheKey);
2031
+ if (!found) {
2032
+ found = /* @__PURE__ */ new Map();
2033
+ for (const d of decls() ?? []) {
2034
+ const k = d.name.toLowerCase();
2035
+ if (d.type === "quality" && d.stages && !found.has(k)) found.set(k, d.stages);
2036
+ }
2037
+ host.qualityLadders.set(cacheKey, found);
2038
+ }
2039
+ return found;
2040
+ }
1982
2041
  function toForeignDecl(decl) {
1983
2042
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
1984
2043
  }