@patterkit/runtime 0.19.0 → 0.20.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.d.cts CHANGED
@@ -127,12 +127,14 @@ interface ChoiceOption {
127
127
  gameData?: GameData;
128
128
  }
129
129
  /**
130
- * The host's **World Properties** resolver: a `{ get, set? }` the game provides so the story can read
131
- * (and, if you allow it, write) its `@world.*` values at runtime. Property metadata (types, read-only)
132
- * comes from the compiled bundle's declared world properties; the values themselves live in the host and
130
+ * A host scope's resolver: a `{ get, set? }` the game provides so the story can read (and, if you allow
131
+ * it, write) values the game owns, `@world.*` among them, at runtime. Property metadata (types,
132
+ * read-only) comes from the compiled bundle's declarations; the values themselves live in the host and
133
133
  * are never stored or saved by this engine.
134
134
  */
135
- type WorldResolver = ScopeResolver;
135
+ type HostScope = ScopeResolver;
136
+ /** @deprecated Use {@link HostScope}, the name every Patterplay runtime uses. */
137
+ type WorldResolver = HostScope;
136
138
  /** What the engine DECIDED, as opposed to what it produced. A step tells you the line that
137
139
  * played; a trace event tells you why that line and not its siblings - which children were
138
140
  * eligible, where the selector's cursor stood, which choice options were live.
@@ -222,6 +224,8 @@ interface PlayError {
222
224
  source?: string;
223
225
  message: string;
224
226
  }
227
+ /** @deprecated Nothing takes one: a flow has no tap of its own, and `onTrace` takes an
228
+ * {@link EngineTraceHandler}. Goes in a later release. */
225
229
  type TraceHandler = (event: TraceEvent) => void;
226
230
  /** The engine-level tap: every flow's events, tagged with the flow id - one stream for tools. */
227
231
  type EngineTraceHandler = (flow: string, event: TraceEvent) => void;
@@ -249,14 +253,18 @@ interface EngineOptions {
249
253
  /** Active locale for string lookups (embedded localisation). Defaults to the bundle's default locale.
250
254
  * Ignored by an "ids" bundle, which emits beat IDs for the game to localise itself. */
251
255
  locale?: string;
252
- /** The host's resolver for **World Properties** (`@world.*`): the values the game owns and the story
253
- * reads. Omit it and the runtime self-backs `@world` from the declared defaults, as a property the
254
- * registry stores and saves. Shared by all flows. A game running several engines registers `@world`
255
- * in its registry itself instead, and no engine is given a resolver. */
256
- world?: WorldResolver;
256
+ /** The host scopes the game binds, by token: for each, a resolver over values the game owns and the
257
+ * story reads, such as `{ world: { get, set } }` for **World Properties** (`@world.*`). A declared
258
+ * scope left unbound is self-backed from its defaults, as a property the registry stores and saves.
259
+ * Shared by all flows. A game running several engines registers its scopes in its registry itself
260
+ * instead, and binds none here. The same option on every runtime. */
261
+ hostScopes?: Record<string, HostScope>;
262
+ /** @deprecated Use `hostScopes: { world }`, the option every Patterplay runtime takes. Goes in a later
263
+ * release. */
264
+ world?: HostScope;
257
265
  /** The game's registry: ONE per game, holding every engine's properties except those the game keeps
258
266
  * itself, saved once. Given one, the engine registers its own scopes in it (`@patter` under `patter`,
259
- * its per-flow and per-scene bags under keys starting `patter/`, and `@world` if `world` is passed),
267
+ * its per-flow and per-scene bags under keys starting `patter/`, and each scope `hostScopes` binds),
260
268
  * reads every other scope from it, and `saveGame()` leaves the property values to the game. Host
261
269
  * scopes the bundle declares (`@world`) are then the game's to register: owned if the registry should
262
270
  * store them, foreign if the game keeps them. Omit it and the engine makes its own registry and acts as
@@ -324,8 +332,13 @@ declare class Checkpoint {
324
332
  }
325
333
  /** Shared, read-mostly context the engine hands to every flow it owns. */
326
334
  interface FlowHost {
327
- /** True when the run asked for a log; flows skip building entries otherwise. */
335
+ /** True when the run asked for a log. */
328
336
  logEnabled: boolean;
337
+ /** Each declaration set's quality ladders, built on first use (see `ladders`). */
338
+ qualityLadders: Map<string, Map<string, readonly string[]>>;
339
+ /** True when anything takes the decisions: the log, or an {@link Engine.onTrace} handler. Flows skip
340
+ * the work only a decision needs (a `run` walk's entry, a write's previous value) otherwise. */
341
+ tracing: boolean;
329
342
  /** A flow's events reach the ENGINE's stream through here, tagged with the flow id. */
330
343
  emitEngine: (flow: string, event: TraceEvent, scene?: string) => void;
331
344
  bundle: Bundle;
@@ -419,7 +432,7 @@ declare class Engine {
419
432
  private readonly sceneGameIdToId;
420
433
  private readonly blockGameIdToId;
421
434
  /** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
422
- * engine keeps the same world resolver, custom RNG, and diagnostic hooks. */
435
+ * engine keeps the same host scopes, custom RNG, and diagnostic hooks. */
423
436
  private readonly creationOptions;
424
437
  /** The run's ordered stream: every flow's events, each naming its flow. Empty and
425
438
  * unwritten unless `options.log` asked for it. */
@@ -433,6 +446,9 @@ declare class Engine {
433
446
  /** True for a source-only DEBUG build: the embedded strings are the source language (for debugging),
434
447
  * not a shippable localised build. An IDs-only ship build is `false`. */
435
448
  get isSourceDebug(): boolean;
449
+ /** The bundle's build identity (its content hash), the one a debug link handshakes with, or an empty
450
+ * string for a bundle compiled without one. */
451
+ get buildId(): string;
436
452
  /**
437
453
  * Switch the active locale LIVE - a real game's "language" setting can change mid-session. Subsequent
438
454
  * string lookups (new beats, re-resolved character names, `{@ref}` interpolation) render in the new
@@ -677,6 +693,9 @@ declare class Engine {
677
693
  * retained. Returns its own unsubscribe. */
678
694
  onTrace(handler: EngineTraceHandler): () => void;
679
695
  private emitEngine;
696
+ /** How many times each node has been entered across every flow, by node id: the shared count. With a
697
+ * flow's own {@link Flow.getVisitCounts}, every visit count the run keeps, read without a save. */
698
+ getVisitCounts(): Record<string, number>;
680
699
  listProperties(): PropertyRow[];
681
700
  private splitShared;
682
701
  /** The shared `@patter` values alone, as `{ patter: {...} }`.
@@ -744,15 +763,20 @@ declare class Flow {
744
763
  /** The stage ladder of `@scope.name` when it is a declared quality, else undefined. Names compare
745
764
  * lowercase, as the compiler emits references (the selfBackedResolver lesson). */
746
765
  private stagesFor;
747
- /** Begin this flow at a scene (and optionally a specific block within it). */
748
- start(sceneId?: string, blockId?: string): void;
766
+ /** Begin this flow at a scene (and optionally a specific block within it). The engine's own entry point:
767
+ * openFlow and a goto on an unstarted flow begin a flow here. A game calls {@link reset}.
768
+ * @internal */
769
+ begin(sceneId?: string, blockId?: string): void;
749
770
  /**
750
771
  * Forget everything in this flow and begin again - its per-flow state (not-shared
751
772
  * `@patter` globals + `@scene` props), cursor, callstack, selector cursors, and
752
773
  * visit counts. Shared state (shared `@patter` / `@scene`, world visit counts) is
753
- * untouched. A clearer-named alias of `start()`.
774
+ * untouched. The one public way to begin a flow again (openFlow begins a new one).
754
775
  */
755
776
  reset(sceneId?: string, blockId?: string): void;
777
+ /** @deprecated Use {@link reset}, the one public name for beginning a flow again on every runtime. `start`
778
+ * was the same call under a second name, and goes in a later release. */
779
+ start(sceneId?: string, blockId?: string): void;
756
780
  /**
757
781
  * Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
758
782
  * `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
@@ -782,6 +806,13 @@ declare class Flow {
782
806
  * cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
783
807
  */
784
808
  close(): void;
809
+ /**
810
+ * Drop everything waiting to be delivered: an open choice, and a chosen option's prompt still to be
811
+ * replayed. Every move that abandons the flow's place (start and reset, goto, close) does this, so none
812
+ * leaves a stale prompt behind. start() cleared only the choice, so a reset between choose() and the
813
+ * next advance() replayed the abandoned run's prompt as the new run's first beat.
814
+ */
815
+ private clearPending;
785
816
  /** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
786
817
  * put them back: a handful of fields and a copy of the call stack (a few frames), never its history.
787
818
  * Its visits, selector cursors, and property values are recorded change by change as they happen. */
@@ -820,9 +851,11 @@ declare class Flow {
820
851
  log(): readonly LogEntry[];
821
852
  /** Drop the retained entries. `seq` keeps counting, so order survives a clear. */
822
853
  clearLog(): void;
823
- /** Record one decision, on this flow's log and the engine's. Cheap to call with logging
824
- * off: the entry is never built. */
854
+ /** Record one decision, on this flow's log and the engine's, and hand it to any `onTrace`. Each call
855
+ * site checks `host.tracing` first, so with nothing logging or tracing the entry is never built. */
825
856
  private emit;
857
+ /** How many times this flow has entered each node, by node id. */
858
+ getVisitCounts(): Record<string, number>;
826
859
  /** The options of a pending choice (empty when not at a choice point). */
827
860
  getChoices(): ChoiceOption[];
828
861
  /** Pick an eligible option by id; the next `advance()` runs it. */
@@ -1139,4 +1172,4 @@ declare function effectiveGameData(fields: GameDataField[], node: GameData | und
1139
1172
  */
1140
1173
  declare function buildTagIndex(bundle: Bundle): Map<string, string[]>;
1141
1174
 
1142
- export { type AddressSummary, type AdvanceToStopResult, type BeatInfo, type BundleCounts, type BundleDescription, type BundleIdentity, Checkpoint, type ChoiceOption, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, type FlatBeat, Flow, type GameDataFieldSummary, type GameDataSummary, type HostScopeSummary, type LogEntry, type OpenFlowOptions, type OutlineBlock, type OutlineNode, type OutlineScene, type OwnedProperties, type PlayError, type PropertySummary, type StepResult, type TraceEvent, type TraceHandler, type WorldResolver, buildTagIndex, describeBundle, effectiveGameData, gameDataFields, gameDataValue };
1175
+ export { type AddressSummary, type AdvanceToStopResult, type BeatInfo, type BundleCounts, type BundleDescription, type BundleIdentity, Checkpoint, type ChoiceOption, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, type FlatBeat, Flow, type GameDataFieldSummary, type GameDataSummary, type HostScope, type HostScopeSummary, type LogEntry, type OpenFlowOptions, type OutlineBlock, type OutlineNode, type OutlineScene, type OwnedProperties, type PlayError, type PropertySummary, type StepResult, type TraceEvent, type TraceHandler, type WorldResolver, buildTagIndex, describeBundle, effectiveGameData, gameDataFields, gameDataValue };
package/dist/index.d.ts CHANGED
@@ -127,12 +127,14 @@ interface ChoiceOption {
127
127
  gameData?: GameData;
128
128
  }
129
129
  /**
130
- * The host's **World Properties** resolver: a `{ get, set? }` the game provides so the story can read
131
- * (and, if you allow it, write) its `@world.*` values at runtime. Property metadata (types, read-only)
132
- * comes from the compiled bundle's declared world properties; the values themselves live in the host and
130
+ * A host scope's resolver: a `{ get, set? }` the game provides so the story can read (and, if you allow
131
+ * it, write) values the game owns, `@world.*` among them, at runtime. Property metadata (types,
132
+ * read-only) comes from the compiled bundle's declarations; the values themselves live in the host and
133
133
  * are never stored or saved by this engine.
134
134
  */
135
- type WorldResolver = ScopeResolver;
135
+ type HostScope = ScopeResolver;
136
+ /** @deprecated Use {@link HostScope}, the name every Patterplay runtime uses. */
137
+ type WorldResolver = HostScope;
136
138
  /** What the engine DECIDED, as opposed to what it produced. A step tells you the line that
137
139
  * played; a trace event tells you why that line and not its siblings - which children were
138
140
  * eligible, where the selector's cursor stood, which choice options were live.
@@ -222,6 +224,8 @@ interface PlayError {
222
224
  source?: string;
223
225
  message: string;
224
226
  }
227
+ /** @deprecated Nothing takes one: a flow has no tap of its own, and `onTrace` takes an
228
+ * {@link EngineTraceHandler}. Goes in a later release. */
225
229
  type TraceHandler = (event: TraceEvent) => void;
226
230
  /** The engine-level tap: every flow's events, tagged with the flow id - one stream for tools. */
227
231
  type EngineTraceHandler = (flow: string, event: TraceEvent) => void;
@@ -249,14 +253,18 @@ interface EngineOptions {
249
253
  /** Active locale for string lookups (embedded localisation). Defaults to the bundle's default locale.
250
254
  * Ignored by an "ids" bundle, which emits beat IDs for the game to localise itself. */
251
255
  locale?: string;
252
- /** The host's resolver for **World Properties** (`@world.*`): the values the game owns and the story
253
- * reads. Omit it and the runtime self-backs `@world` from the declared defaults, as a property the
254
- * registry stores and saves. Shared by all flows. A game running several engines registers `@world`
255
- * in its registry itself instead, and no engine is given a resolver. */
256
- world?: WorldResolver;
256
+ /** The host scopes the game binds, by token: for each, a resolver over values the game owns and the
257
+ * story reads, such as `{ world: { get, set } }` for **World Properties** (`@world.*`). A declared
258
+ * scope left unbound is self-backed from its defaults, as a property the registry stores and saves.
259
+ * Shared by all flows. A game running several engines registers its scopes in its registry itself
260
+ * instead, and binds none here. The same option on every runtime. */
261
+ hostScopes?: Record<string, HostScope>;
262
+ /** @deprecated Use `hostScopes: { world }`, the option every Patterplay runtime takes. Goes in a later
263
+ * release. */
264
+ world?: HostScope;
257
265
  /** The game's registry: ONE per game, holding every engine's properties except those the game keeps
258
266
  * itself, saved once. Given one, the engine registers its own scopes in it (`@patter` under `patter`,
259
- * its per-flow and per-scene bags under keys starting `patter/`, and `@world` if `world` is passed),
267
+ * its per-flow and per-scene bags under keys starting `patter/`, and each scope `hostScopes` binds),
260
268
  * reads every other scope from it, and `saveGame()` leaves the property values to the game. Host
261
269
  * scopes the bundle declares (`@world`) are then the game's to register: owned if the registry should
262
270
  * store them, foreign if the game keeps them. Omit it and the engine makes its own registry and acts as
@@ -324,8 +332,13 @@ declare class Checkpoint {
324
332
  }
325
333
  /** Shared, read-mostly context the engine hands to every flow it owns. */
326
334
  interface FlowHost {
327
- /** True when the run asked for a log; flows skip building entries otherwise. */
335
+ /** True when the run asked for a log. */
328
336
  logEnabled: boolean;
337
+ /** Each declaration set's quality ladders, built on first use (see `ladders`). */
338
+ qualityLadders: Map<string, Map<string, readonly string[]>>;
339
+ /** True when anything takes the decisions: the log, or an {@link Engine.onTrace} handler. Flows skip
340
+ * the work only a decision needs (a `run` walk's entry, a write's previous value) otherwise. */
341
+ tracing: boolean;
329
342
  /** A flow's events reach the ENGINE's stream through here, tagged with the flow id. */
330
343
  emitEngine: (flow: string, event: TraceEvent, scene?: string) => void;
331
344
  bundle: Bundle;
@@ -419,7 +432,7 @@ declare class Engine {
419
432
  private readonly sceneGameIdToId;
420
433
  private readonly blockGameIdToId;
421
434
  /** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
422
- * engine keeps the same world resolver, custom RNG, and diagnostic hooks. */
435
+ * engine keeps the same host scopes, custom RNG, and diagnostic hooks. */
423
436
  private readonly creationOptions;
424
437
  /** The run's ordered stream: every flow's events, each naming its flow. Empty and
425
438
  * unwritten unless `options.log` asked for it. */
@@ -433,6 +446,9 @@ declare class Engine {
433
446
  /** True for a source-only DEBUG build: the embedded strings are the source language (for debugging),
434
447
  * not a shippable localised build. An IDs-only ship build is `false`. */
435
448
  get isSourceDebug(): boolean;
449
+ /** The bundle's build identity (its content hash), the one a debug link handshakes with, or an empty
450
+ * string for a bundle compiled without one. */
451
+ get buildId(): string;
436
452
  /**
437
453
  * Switch the active locale LIVE - a real game's "language" setting can change mid-session. Subsequent
438
454
  * string lookups (new beats, re-resolved character names, `{@ref}` interpolation) render in the new
@@ -677,6 +693,9 @@ declare class Engine {
677
693
  * retained. Returns its own unsubscribe. */
678
694
  onTrace(handler: EngineTraceHandler): () => void;
679
695
  private emitEngine;
696
+ /** How many times each node has been entered across every flow, by node id: the shared count. With a
697
+ * flow's own {@link Flow.getVisitCounts}, every visit count the run keeps, read without a save. */
698
+ getVisitCounts(): Record<string, number>;
680
699
  listProperties(): PropertyRow[];
681
700
  private splitShared;
682
701
  /** The shared `@patter` values alone, as `{ patter: {...} }`.
@@ -744,15 +763,20 @@ declare class Flow {
744
763
  /** The stage ladder of `@scope.name` when it is a declared quality, else undefined. Names compare
745
764
  * lowercase, as the compiler emits references (the selfBackedResolver lesson). */
746
765
  private stagesFor;
747
- /** Begin this flow at a scene (and optionally a specific block within it). */
748
- start(sceneId?: string, blockId?: string): void;
766
+ /** Begin this flow at a scene (and optionally a specific block within it). The engine's own entry point:
767
+ * openFlow and a goto on an unstarted flow begin a flow here. A game calls {@link reset}.
768
+ * @internal */
769
+ begin(sceneId?: string, blockId?: string): void;
749
770
  /**
750
771
  * Forget everything in this flow and begin again - its per-flow state (not-shared
751
772
  * `@patter` globals + `@scene` props), cursor, callstack, selector cursors, and
752
773
  * visit counts. Shared state (shared `@patter` / `@scene`, world visit counts) is
753
- * untouched. A clearer-named alias of `start()`.
774
+ * untouched. The one public way to begin a flow again (openFlow begins a new one).
754
775
  */
755
776
  reset(sceneId?: string, blockId?: string): void;
777
+ /** @deprecated Use {@link reset}, the one public name for beginning a flow again on every runtime. `start`
778
+ * was the same call under a second name, and goes in a later release. */
779
+ start(sceneId?: string, blockId?: string): void;
756
780
  /**
757
781
  * Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
758
782
  * `onEntry` effects run, entering counts as a visit, and the callstack is REPLACED - any pending
@@ -782,6 +806,13 @@ declare class Flow {
782
806
  * cannot quietly mutate the world. Terminal: unlike ending, a close is never revived.
783
807
  */
784
808
  close(): void;
809
+ /**
810
+ * Drop everything waiting to be delivered: an open choice, and a chosen option's prompt still to be
811
+ * replayed. Every move that abandons the flow's place (start and reset, goto, close) does this, so none
812
+ * leaves a stale prompt behind. start() cleared only the choice, so a reset between choose() and the
813
+ * next advance() replayed the abandoned run's prompt as the new run's first beat.
814
+ */
815
+ private clearPending;
785
816
  /** Inside a checkpoint, the first change to this flow records its cursor and PRNG, so a rollback can
786
817
  * put them back: a handful of fields and a copy of the call stack (a few frames), never its history.
787
818
  * Its visits, selector cursors, and property values are recorded change by change as they happen. */
@@ -820,9 +851,11 @@ declare class Flow {
820
851
  log(): readonly LogEntry[];
821
852
  /** Drop the retained entries. `seq` keeps counting, so order survives a clear. */
822
853
  clearLog(): void;
823
- /** Record one decision, on this flow's log and the engine's. Cheap to call with logging
824
- * off: the entry is never built. */
854
+ /** Record one decision, on this flow's log and the engine's, and hand it to any `onTrace`. Each call
855
+ * site checks `host.tracing` first, so with nothing logging or tracing the entry is never built. */
825
856
  private emit;
857
+ /** How many times this flow has entered each node, by node id. */
858
+ getVisitCounts(): Record<string, number>;
826
859
  /** The options of a pending choice (empty when not at a choice point). */
827
860
  getChoices(): ChoiceOption[];
828
861
  /** Pick an eligible option by id; the next `advance()` runs it. */
@@ -1139,4 +1172,4 @@ declare function effectiveGameData(fields: GameDataField[], node: GameData | und
1139
1172
  */
1140
1173
  declare function buildTagIndex(bundle: Bundle): Map<string, string[]>;
1141
1174
 
1142
- export { type AddressSummary, type AdvanceToStopResult, type BeatInfo, type BundleCounts, type BundleDescription, type BundleIdentity, Checkpoint, type ChoiceOption, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, type FlatBeat, Flow, type GameDataFieldSummary, type GameDataSummary, type HostScopeSummary, type LogEntry, type OpenFlowOptions, type OutlineBlock, type OutlineNode, type OutlineScene, type OwnedProperties, type PlayError, type PropertySummary, type StepResult, type TraceEvent, type TraceHandler, type WorldResolver, buildTagIndex, describeBundle, effectiveGameData, gameDataFields, gameDataValue };
1175
+ export { type AddressSummary, type AdvanceToStopResult, type BeatInfo, type BundleCounts, type BundleDescription, type BundleIdentity, Checkpoint, type ChoiceOption, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, type FlatBeat, Flow, type GameDataFieldSummary, type GameDataSummary, type HostScope, type HostScopeSummary, type LogEntry, type OpenFlowOptions, type OutlineBlock, type OutlineNode, type OutlineScene, type OwnedProperties, type PlayError, type PropertySummary, type StepResult, type TraceEvent, type TraceHandler, type WorldResolver, buildTagIndex, describeBundle, effectiveGameData, gameDataFields, gameDataValue };
package/dist/index.js CHANGED
@@ -81,7 +81,7 @@ var Engine = class _Engine {
81
81
  sceneGameIdToId = /* @__PURE__ */ new Map();
82
82
  blockGameIdToId = /* @__PURE__ */ new Map();
83
83
  /** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
84
- * engine keeps the same world resolver, custom RNG, and diagnostic hooks. */
84
+ * engine keeps the same host scopes, custom RNG, and diagnostic hooks. */
85
85
  creationOptions;
86
86
  /** The run's ordered stream: every flow's events, each naming its flow. Empty and
87
87
  * unwritten unless `options.log` asked for it. */
@@ -133,12 +133,12 @@ var Engine = class _Engine {
133
133
  registry.mountOwned("patter", patterBag, { owner: OWNER });
134
134
  registered.push("patter");
135
135
  const hostBound = /* @__PURE__ */ new Set();
136
- if (options.world) {
137
- const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
138
- const decls = (worldSpec?.declarations ?? []).map(toForeignDecl);
139
- registry.defineForeign("world", options.world, decls, { writable: worldSpec?.writable ?? true, owner: OWNER });
140
- registered.push("world");
141
- hostBound.add("world");
136
+ for (const [token, resolver] of Object.entries(boundScopes(options))) {
137
+ const spec = bundle.scopeRegistry?.scopes.find((s) => s.token === token);
138
+ const decls = (spec?.declarations ?? []).map(toForeignDecl);
139
+ registry.defineForeign(token, resolver, decls, { writable: spec?.writable ?? true, owner: OWNER });
140
+ registered.push(token);
141
+ hostBound.add(token);
142
142
  }
143
143
  for (const spec of ownsRegistry ? bundle.scopeRegistry?.scopes ?? [] : []) {
144
144
  if (hostBound.has(spec.token) || registry.has(spec.token)) continue;
@@ -158,6 +158,7 @@ var Engine = class _Engine {
158
158
  }
159
159
  this.host = {
160
160
  logEnabled: options.log ?? false,
161
+ tracing: options.log ?? false,
161
162
  emitEngine: (flow, event, scene) => this.emitEngine(flow, event, scene),
162
163
  bundle,
163
164
  emitIds,
@@ -180,6 +181,7 @@ var Engine = class _Engine {
180
181
  patterSharedNames,
181
182
  sceneSharedNames,
182
183
  sharedVisits: /* @__PURE__ */ new Map(),
184
+ qualityLadders: /* @__PURE__ */ new Map(),
183
185
  sharedSelectors: /* @__PURE__ */ new Map(),
184
186
  stageBags: /* @__PURE__ */ new Map(),
185
187
  journal: null,
@@ -206,6 +208,11 @@ var Engine = class _Engine {
206
208
  get isSourceDebug() {
207
209
  return this.sourceDebug;
208
210
  }
211
+ /** The bundle's build identity (its content hash), the one a debug link handshakes with, or an empty
212
+ * string for a bundle compiled without one. */
213
+ get buildId() {
214
+ return this.host.bundle.content?.hash ?? "";
215
+ }
209
216
  /**
210
217
  * Switch the active locale LIVE - a real game's "language" setting can change mid-session. Subsequent
211
218
  * string lookups (new beats, re-resolved character names, `{@ref}` interpolation) render in the new
@@ -326,7 +333,7 @@ var Engine = class _Engine {
326
333
  });
327
334
  }
328
335
  this.flowsById.set(id, flow);
329
- flow.start(sceneId, blockId);
336
+ flow.begin(sceneId, blockId);
330
337
  return flow;
331
338
  }
332
339
  /**
@@ -690,7 +697,7 @@ var Engine = class _Engine {
690
697
  for (const s of this.host.stageBags.keys()) reg.remove(keys.stage(s), { keep });
691
698
  this.host.stageBags.clear();
692
699
  for (const t of ["patter", ...this.host.hostScopes]) if (reg.has(t)) reg.remove(t, { keep });
693
- if (this.creationOptions.world && reg.has("world")) reg.remove("world");
700
+ for (const t of Object.keys(boundScopes(this.creationOptions))) if (reg.has(t)) reg.remove(t);
694
701
  }
695
702
  /** Read a shared (`@patter` / foreign) property by ref. `@scene` refs are rejected (flow-level). */
696
703
  getProperty(ref) {
@@ -727,13 +734,22 @@ var Engine = class _Engine {
727
734
  * retained. Returns its own unsubscribe. */
728
735
  onTrace(handler) {
729
736
  this.engineTraceHandlers.add(handler);
730
- return () => this.engineTraceHandlers.delete(handler);
737
+ this.host.tracing = true;
738
+ return () => {
739
+ this.engineTraceHandlers.delete(handler);
740
+ this.host.tracing = this.host.logEnabled || this.engineTraceHandlers.size > 0;
741
+ };
731
742
  }
732
743
  emitEngine(flow, event, scene) {
733
744
  for (const h of this.engineTraceHandlers) h(flow, event);
734
745
  if (!this.host.logEnabled) return;
735
746
  this.engineLog.push({ ...event, flow, seq: this.engineSeq++, ...scene ? { scene } : {} });
736
747
  }
748
+ /** How many times each node has been entered across every flow, by node id: the shared count. With a
749
+ * flow's own {@link Flow.getVisitCounts}, every visit count the run keeps, read without a save. */
750
+ getVisitCounts() {
751
+ return Object.fromEntries(this.host.sharedVisits);
752
+ }
737
753
  listProperties() {
738
754
  return this.host.patterSharedDecls.map((d) => ({
739
755
  name: d.name,
@@ -938,19 +954,21 @@ var Flow = class {
938
954
  * lowercase, as the compiler emits references (the selfBackedResolver lesson). */
939
955
  stagesFor(scope, name) {
940
956
  const key = name.toLowerCase();
941
- const fromDecls = (decls) => decls?.find((d) => d.name.toLowerCase() === key && d.type === "quality")?.stages;
942
957
  if (scope === "patter") {
943
- return fromDecls(this.host.patterSharedDecls) ?? fromDecls(this.host.patterLocalDecls);
958
+ return ladders(this.host, "patter", () => [...this.host.patterSharedDecls, ...this.host.patterLocalDecls]).get(key);
944
959
  }
945
960
  if (scope === "scene") {
946
- const scene = this.currentSceneId != null ? this.host.bundle.scenes[this.currentSceneId] : void 0;
947
- return fromDecls(scene?.sceneProps);
961
+ const sceneId = this.currentSceneId;
962
+ if (sceneId == null) return void 0;
963
+ return ladders(this.host, `scene/${sceneId}`, () => this.host.bundle.scenes[sceneId]?.sceneProps).get(key);
948
964
  }
949
- return this.registryQualities?.(scope, name) ?? fromDecls(this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations);
965
+ return this.registryQualities?.(scope, name) ?? ladders(this.host, `host/${scope}`, () => this.host.bundle.scopeRegistry?.scopes.find((s) => s.token === scope)?.declarations).get(key);
950
966
  }
951
967
  // -- Host API -------------------------------------------------------------
952
- /** Begin this flow at a scene (and optionally a specific block within it). */
953
- start(sceneId, blockId) {
968
+ /** Begin this flow at a scene (and optionally a specific block within it). The engine's own entry point:
969
+ * openFlow and a goto on an unstarted flow begin a flow here. A game calls {@link reset}.
970
+ * @internal */
971
+ begin(sceneId, blockId) {
954
972
  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");
955
973
  this.releaseBags(false);
956
974
  this.host.registry.discardParked(keys.flow(this.id));
@@ -962,7 +980,7 @@ var Flow = class {
962
980
  this.flowEnded = false;
963
981
  this.activeSnippet = null;
964
982
  this.beatIndex = 0;
965
- this.pendingChoice = null;
983
+ this.clearPending();
966
984
  this.started = true;
967
985
  if (blockId) {
968
986
  const loc = this.host.blockIndex.get(blockId);
@@ -987,10 +1005,15 @@ var Flow = class {
987
1005
  * Forget everything in this flow and begin again - its per-flow state (not-shared
988
1006
  * `@patter` globals + `@scene` props), cursor, callstack, selector cursors, and
989
1007
  * visit counts. Shared state (shared `@patter` / `@scene`, world visit counts) is
990
- * untouched. A clearer-named alias of `start()`.
1008
+ * untouched. The one public way to begin a flow again (openFlow begins a new one).
991
1009
  */
992
1010
  reset(sceneId, blockId) {
993
- this.start(sceneId, blockId);
1011
+ this.begin(sceneId, blockId);
1012
+ }
1013
+ /** @deprecated Use {@link reset}, the one public name for beginning a flow again on every runtime. `start`
1014
+ * was the same call under a second name, and goes in a later release. */
1015
+ start(sceneId, blockId) {
1016
+ this.reset(sceneId, blockId);
994
1017
  }
995
1018
  /**
996
1019
  * Send this flow's cursor to an ADDRESS, exactly as an authored `go` jump would: the target scene's
@@ -1014,9 +1037,7 @@ var Flow = class {
1014
1037
  this.touch();
1015
1038
  if (scene === "END") {
1016
1039
  this.started = true;
1017
- this.pendingChoice = null;
1018
- this.pendingPrompt = null;
1019
- this.pendingPromptOwnerId = null;
1040
+ this.clearPending();
1020
1041
  this.activeSnippet = null;
1021
1042
  this.beatIndex = 0;
1022
1043
  this.flowEnded = true;
@@ -1031,12 +1052,10 @@ var Flow = class {
1031
1052
  if (blockId === void 0) return false;
1032
1053
  }
1033
1054
  if (!this.started) {
1034
- this.start(sceneId, blockId);
1055
+ this.begin(sceneId, blockId);
1035
1056
  return true;
1036
1057
  }
1037
- this.pendingChoice = null;
1038
- this.pendingPrompt = null;
1039
- this.pendingPromptOwnerId = null;
1058
+ this.clearPending();
1040
1059
  this.activeSnippet = null;
1041
1060
  this.beatIndex = 0;
1042
1061
  this.flowEnded = false;
@@ -1061,6 +1080,15 @@ var Flow = class {
1061
1080
  this.stack = [];
1062
1081
  this.activeSnippet = null;
1063
1082
  this.beatIndex = 0;
1083
+ this.clearPending();
1084
+ }
1085
+ /**
1086
+ * Drop everything waiting to be delivered: an open choice, and a chosen option's prompt still to be
1087
+ * replayed. Every move that abandons the flow's place (start and reset, goto, close) does this, so none
1088
+ * leaves a stale prompt behind. start() cleared only the choice, so a reset between choose() and the
1089
+ * next advance() replayed the abandoned run's prompt as the new run's first beat.
1090
+ */
1091
+ clearPending() {
1064
1092
  this.pendingChoice = null;
1065
1093
  this.pendingPrompt = null;
1066
1094
  this.pendingPromptOwnerId = null;
@@ -1157,7 +1185,7 @@ var Flow = class {
1157
1185
  }
1158
1186
  const from = frame.index;
1159
1187
  while (frame.index < children.length && !this.eligible(children[frame.index])) frame.index++;
1160
- if (this.host.logEnabled && frame.index !== from) {
1188
+ if (this.host.tracing && frame.index !== from) {
1161
1189
  this.emit({
1162
1190
  type: "select",
1163
1191
  group: frame.containerId,
@@ -1193,14 +1221,18 @@ var Flow = class {
1193
1221
  clearLog() {
1194
1222
  this.flowLog.length = 0;
1195
1223
  }
1196
- /** Record one decision, on this flow's log and the engine's. Cheap to call with logging
1197
- * off: the entry is never built. */
1224
+ /** Record one decision, on this flow's log and the engine's, and hand it to any `onTrace`. Each call
1225
+ * site checks `host.tracing` first, so with nothing logging or tracing the entry is never built. */
1198
1226
  emit(event) {
1199
1227
  const scene = this.currentSceneId ?? void 0;
1200
1228
  this.host.emitEngine(this.id, event, scene);
1201
1229
  if (!this.host.logEnabled) return;
1202
1230
  this.flowLog.push({ ...event, seq: this.flowSeq++, ...scene ? { scene } : {} });
1203
1231
  }
1232
+ /** How many times this flow has entered each node, by node id. */
1233
+ getVisitCounts() {
1234
+ return Object.fromEntries(this.visitCounts);
1235
+ }
1204
1236
  /** The options of a pending choice (empty when not at a choice point). */
1205
1237
  getChoices() {
1206
1238
  return this.pendingChoice?.options ?? [];
@@ -1214,7 +1246,7 @@ var Flow = class {
1214
1246
  if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
1215
1247
  this.touch();
1216
1248
  const node = choice.byId.get(id);
1217
- this.emit({ type: "chose", group: choice.groupId, option: id });
1249
+ if (this.host.tracing) this.emit({ type: "chose", group: choice.groupId, option: id });
1218
1250
  this.pendingChoice = null;
1219
1251
  const authored = this.host.replayPromptOnChoose ? this.authoredPromptOf(node) : void 0;
1220
1252
  this.pendingPrompt = authored && option.prompt ? { beat: authored, shown: { ...option.prompt } } : null;
@@ -1410,7 +1442,7 @@ var Flow = class {
1410
1442
  byId.set(child.id, child);
1411
1443
  }
1412
1444
  if (options.some((o) => o.eligible)) {
1413
- this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
1445
+ if (this.host.tracing) this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
1414
1446
  this.pendingChoice = { groupId: group.id, options, byId };
1415
1447
  return;
1416
1448
  }
@@ -1419,7 +1451,7 @@ var Flow = class {
1419
1451
  this.enterChild(fallback);
1420
1452
  return;
1421
1453
  }
1422
- this.emit({ type: "dry", group: group.id });
1454
+ if (this.host.tracing) this.emit({ type: "dry", group: group.id });
1423
1455
  this.host.onDryChoice?.(group.id);
1424
1456
  }
1425
1457
  // -- Jumps (jump / call-return) ----------------------------------------
@@ -1434,7 +1466,7 @@ var Flow = class {
1434
1466
  * hard-ends the flow regardless of the callstack.
1435
1467
  */
1436
1468
  enterTarget(to, mode) {
1437
- this.emit({ type: "jump", to, mode });
1469
+ if (this.host.tracing) this.emit({ type: "jump", to, mode });
1438
1470
  if (to === "END") {
1439
1471
  this.flowEnded = true;
1440
1472
  this.stack = [];
@@ -1471,7 +1503,7 @@ var Flow = class {
1471
1503
  const order = group.options?.order ?? "sequential";
1472
1504
  const exhaust = group.options?.exhaust ?? "once";
1473
1505
  const trace = (picked) => {
1474
- this.emit({
1506
+ if (this.host.tracing) this.emit({
1475
1507
  type: "select",
1476
1508
  group: group.id,
1477
1509
  selector: group.selector ?? "default",
@@ -1636,9 +1668,9 @@ var Flow = class {
1636
1668
  let value;
1637
1669
  try {
1638
1670
  value = this.evalExpr(e.value);
1639
- const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
1671
+ const prev = this.host.tracing ? this.getProperty(e.target) : void 0;
1640
1672
  this.writeProperty(e.target, value, false);
1641
- this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
1673
+ if (this.host.tracing) this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
1642
1674
  } catch (err) {
1643
1675
  this.reportError("effect", owner, e.value, err);
1644
1676
  }
@@ -1659,7 +1691,7 @@ var Flow = class {
1659
1691
  const message = err instanceof Error ? err.message : String(err);
1660
1692
  const source = expr?.src;
1661
1693
  this.host.onError({ flow: this.id, kind, node, ...source ? { source } : {}, message });
1662
- this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
1694
+ if (this.host.tracing) this.emit({ type: "diagnostic", kind, node, ...source ? { source } : {}, message });
1663
1695
  }
1664
1696
  evalExpr(expr) {
1665
1697
  return evaluate(this.conditionAst(expr), this.context(), patterDialect);
@@ -1946,6 +1978,21 @@ function mountClaims(registry, key, bag, ask) {
1946
1978
  function toDecl(decl) {
1947
1979
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
1948
1980
  }
1981
+ function boundScopes(options) {
1982
+ return { ...options.world ? { world: options.world } : {}, ...options.hostScopes };
1983
+ }
1984
+ function ladders(host, cacheKey, decls) {
1985
+ let found = host.qualityLadders.get(cacheKey);
1986
+ if (!found) {
1987
+ found = /* @__PURE__ */ new Map();
1988
+ for (const d of decls() ?? []) {
1989
+ const k = d.name.toLowerCase();
1990
+ if (d.type === "quality" && d.stages && !found.has(k)) found.set(k, d.stages);
1991
+ }
1992
+ host.qualityLadders.set(cacheKey, found);
1993
+ }
1994
+ return found;
1995
+ }
1949
1996
  function toForeignDecl(decl) {
1950
1997
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
1951
1998
  }