@patterkit/runtime 0.20.0 → 0.22.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,33 @@ version number always means the same runtime behaviour. This package is versione
33
33
 
34
34
  ## [Unreleased]
35
35
 
36
+ ## [0.22.0] - 2026-10-10
37
+
38
+ ### Added
39
+
40
+ - **Speaker qualifiers**: a line can say how it is delivered, a screenplay's `TAM (O.S.)`, without becoming a
41
+ second character. A `line` step, and a line-kind choice prompt, carries `qualifier`, the qualifier's `gameId`
42
+ (`vo`, `os`, `radio`, or the project's own), and `qualifierName`, its shown name, resolved as `characterName`
43
+ is: the active locale's `qualifier:<gameId>` string, then the default locale's, then the authored name.
44
+ `qualifierName` is absent in an IDs-only build, and both are dropped on a line silenced by closed captions.
45
+ Saves and the outline and beat sequence carry them too. What a qualifier means is the game's to decide.
46
+
47
+ ## [0.21.0] - 2026-10-07
48
+
49
+ ### Changed
50
+
51
+ - **A new `Engine` on a bundle another engine already uses no longer rebuilds the tag index.** It is built once
52
+ per bundle and shared, so a tool that starts a fresh engine for every run (coverage does) starts each in about
53
+ a third of the time: 0.5 ms rather than 1.6 ms on a 100-scene project.
54
+ - **The conformance corpus covers more, and every runtime is held to it**: the outline and beat sequence, the
55
+ audio resolver's joins, `listProperties`, what a checkpoint refuses while it is open, a save taken inside one,
56
+ and a block named under another scene.
57
+
58
+ ### Fixed
59
+
60
+ - **Tags handed out are the caller's own.** `tagsForBeat`, `tagsForScene`, `tagsForBlock`, and a delivered
61
+ line's `tags` are copies, so a game that edits one changes nothing the engine says afterwards.
62
+
36
63
  ## [0.20.0] - 2026-10-06
37
64
 
38
65
  ### Added
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");
@@ -138,6 +150,8 @@ var Engine = class _Engine {
138
150
  }
139
151
  const castDisplay = /* @__PURE__ */ new Map();
140
152
  for (const c of bundle.cast ?? []) if (c.displayName) castDisplay.set(c.name, c.displayName);
153
+ const qualifierDisplay = /* @__PURE__ */ new Map();
154
+ for (const q of bundle.qualifiers ?? []) if (q?.gameId) qualifierDisplay.set(q.gameId, q.name);
141
155
  this.defaultSeed = (options.seed ?? 2654435769) >>> 0;
142
156
  const nodeIndex = /* @__PURE__ */ new Map();
143
157
  const blockIndex = /* @__PURE__ */ new Map();
@@ -198,13 +212,14 @@ var Engine = class _Engine {
198
212
  strings,
199
213
  defaultStrings,
200
214
  castDisplay,
215
+ qualifierDisplay,
201
216
  nodeIndex,
202
217
  blockIndex,
203
218
  blockById,
204
219
  sceneGameIdToId: this.sceneGameIdToId,
205
220
  blockGameIdToId: this.blockGameIdToId,
206
221
  // same instances the engine resolves with
207
- tagIndex: buildTagIndex(bundle),
222
+ tagIndex: tagIndexFor(bundle),
208
223
  registry,
209
224
  ownsRegistry,
210
225
  patterBag,
@@ -463,18 +478,18 @@ var Engine = class _Engine {
463
478
  * delivered step carries. Empty array for an unknown id or a beat with no tags anywhere up the chain.
464
479
  */
465
480
  tagsForBeat(beatId) {
466
- return this.host.tagIndex.get(beatId) ?? [];
481
+ return tagsOf(this.host, beatId) ?? [];
467
482
  }
468
483
  /** A scene's own tags (by internal id or gameId address). Empty when none / unknown. */
469
484
  tagsForScene(sceneRef) {
470
485
  const id = this.resolveSceneRef(sceneRef);
471
- return (id != null ? this.host.tagIndex.get(id) : void 0) ?? [];
486
+ return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
472
487
  }
473
488
  /** A block's accumulated tags (scene + block), by scene + block ref (id or gameId). Empty when none / unknown. */
474
489
  tagsForBlock(sceneRef, blockRef) {
475
490
  const sceneId = this.resolveSceneRef(sceneRef);
476
491
  const id = this.resolveBlockRef(sceneId, blockRef);
477
- return (id != null ? this.host.tagIndex.get(id) : void 0) ?? [];
492
+ return (id != null ? tagsOf(this.host, id) : void 0) ?? [];
478
493
  }
479
494
  /**
480
495
  * A scene's own author gameData (by internal id or gameId address): the RAW sparse overrides, exactly
@@ -593,7 +608,7 @@ var Engine = class _Engine {
593
608
  }
594
609
  /** One beat's static data (source locale), the same shape a delivered step carries. */
595
610
  beatInfo(beat) {
596
- const tags = this.host.tagIndex.get(beat.id);
611
+ const tags = tagsOf(this.host, beat.id);
597
612
  const info = { id: beat.id, kind: beat.kind };
598
613
  if (beat.kind === "line") {
599
614
  if (beat.character !== void 0) {
@@ -602,6 +617,11 @@ var Engine = class _Engine {
602
617
  if (name !== void 0) info.characterName = name;
603
618
  }
604
619
  if (beat.direction !== void 0) info.direction = beat.direction;
620
+ if (beat.qualifier !== void 0) {
621
+ info.qualifier = beat.qualifier;
622
+ const name = this.host.defaultStrings[(0, import_model.qualifierStringKey)(beat.qualifier)] ?? this.host.qualifierDisplay.get(beat.qualifier);
623
+ if (name !== void 0) info.qualifierName = name;
624
+ }
605
625
  }
606
626
  if (beat.kind === "line" || beat.kind === "text") {
607
627
  const source = this.host.defaultStrings[beat.id];
@@ -613,7 +633,7 @@ var Engine = class _Engine {
613
633
  }
614
634
  /** A `{ tags }` fragment for an id, present only when the id has accumulated tags (keeps output tidy). */
615
635
  tagsField(id) {
616
- const tags = this.host.tagIndex.get(id);
636
+ const tags = tagsOf(this.host, id);
617
637
  return tags && tags.length ? { tags } : {};
618
638
  }
619
639
  /** Retrieve an open flow by id (undefined if none / closed). */
@@ -750,8 +770,6 @@ var Engine = class _Engine {
750
770
  this.host.registry.set(scope, name, value, { host: true });
751
771
  if (this.host.journal && prev !== void 0) this.host.journal.undo.push(() => this.host.registry.set(scope, name, prev, { host: true }));
752
772
  }
753
- /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
754
- * value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
755
773
  /** The run's decisions, in order, each naming the flow it happened in. Empty unless the
756
774
  * run was opened with `log: true`. A flow's own log stays flow-local; this is the only
757
775
  * place a story spanning several flows reads as one sequence. */
@@ -783,6 +801,8 @@ var Engine = class _Engine {
783
801
  getVisitCounts() {
784
802
  return Object.fromEntries(this.host.sharedVisits);
785
803
  }
804
+ /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
805
+ * value, declared default (for reset), and enum options. Every runtime has the same call. */
786
806
  listProperties() {
787
807
  return this.host.patterSharedDecls.map((d) => ({
788
808
  name: d.name,
@@ -1770,7 +1790,7 @@ var Flow = class {
1770
1790
  };
1771
1791
  // -- Strings / beats ------------------------------------------------------
1772
1792
  beatResult(beat) {
1773
- const tags = this.host.tagIndex.get(beat.id);
1793
+ const tags = tagsOf(this.host, beat.id);
1774
1794
  const withTags = tags && tags.length ? { tags } : {};
1775
1795
  switch (beat.kind) {
1776
1796
  case "gameEvent":
@@ -1790,6 +1810,8 @@ var Flow = class {
1790
1810
  character: silent ? void 0 : beat.character,
1791
1811
  characterName: silent ? void 0 : this.resolveCharacterName(beat.character),
1792
1812
  direction: silent ? void 0 : beat.direction,
1813
+ qualifier: silent ? void 0 : beat.qualifier,
1814
+ qualifierName: silent ? void 0 : this.resolveQualifierName(beat.qualifier),
1793
1815
  gameData: beat.gameData,
1794
1816
  ...withTags
1795
1817
  };
@@ -1828,7 +1850,15 @@ var Flow = class {
1828
1850
  const beat = this.promptBeatOf(node);
1829
1851
  if (!beat) return void 0;
1830
1852
  const text = this.interpolate(this.resolveString(beat.id));
1831
- return beat.kind === "line" ? { kind: "line", text: this.captionLine(text), character: beat.character, characterName: this.resolveCharacterName(beat.character), direction: beat.direction } : { kind: "text", text };
1853
+ return beat.kind === "line" ? {
1854
+ kind: "line",
1855
+ text: this.captionLine(text),
1856
+ character: beat.character,
1857
+ characterName: this.resolveCharacterName(beat.character),
1858
+ direction: beat.direction,
1859
+ qualifier: beat.qualifier,
1860
+ qualifierName: this.resolveQualifierName(beat.qualifier)
1861
+ } : { kind: "text", text };
1832
1862
  }
1833
1863
  /** An option's AUTHORED prompt beat: an Option group's own `prompt`. The only prompt a replay speaks. */
1834
1864
  authoredPromptOf(node) {
@@ -1837,7 +1867,7 @@ var Flow = class {
1837
1867
  /** A replayed prompt as a step: the beat's id, gameData, and tags, and the text and speaker fields the
1838
1868
  * choice showed. */
1839
1869
  promptResult(beat, shown) {
1840
- const tags = this.host.tagIndex.get(beat.id);
1870
+ const tags = tagsOf(this.host, beat.id);
1841
1871
  const withTags = tags && tags.length ? { tags } : {};
1842
1872
  if (shown.kind === "text") return { type: "text", id: beat.id, text: shown.text, gameData: beat.gameData, ...withTags };
1843
1873
  return {
@@ -1847,6 +1877,8 @@ var Flow = class {
1847
1877
  character: shown.character,
1848
1878
  characterName: shown.characterName,
1849
1879
  direction: shown.direction,
1880
+ qualifier: shown.qualifier,
1881
+ qualifierName: shown.qualifierName,
1850
1882
  gameData: beat.gameData,
1851
1883
  ...withTags
1852
1884
  };
@@ -1883,6 +1915,15 @@ var Flow = class {
1883
1915
  const key = (0, import_model.castStringKey)(character);
1884
1916
  return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.castDisplay.get(character);
1885
1917
  }
1918
+ /** A speaker qualifier's shown name, resolved as a character's is: the `qualifier:<gameId>` string in the
1919
+ * active locale, else the default locale, else the authored name. Undefined in IDs-only mode (the game
1920
+ * maps the `qualifier` gameId itself). */
1921
+ resolveQualifierName(qualifier) {
1922
+ if (qualifier === void 0) return void 0;
1923
+ if (this.host.emitIds) return void 0;
1924
+ const key = (0, import_model.qualifierStringKey)(qualifier);
1925
+ return this.host.strings[key] ?? this.host.defaultStrings[key] ?? this.host.qualifierDisplay.get(qualifier);
1926
+ }
1886
1927
  /** Split a ref into scope + name. Tokens: `@scene`, registered tokens, else `@patter` (incl. bare `@name`). */
1887
1928
  splitRef(ref) {
1888
1929
  return splitHostRef(this.host, ref);
@@ -1979,6 +2020,8 @@ function savedPrompt(p) {
1979
2020
  if (p.character !== void 0) out.character = p.character;
1980
2021
  if (p.characterName !== void 0) out.characterName = p.characterName;
1981
2022
  if (p.direction !== void 0) out.direction = p.direction;
2023
+ if (p.qualifier !== void 0) out.qualifier = p.qualifier;
2024
+ if (p.qualifierName !== void 0) out.qualifierName = p.qualifierName;
1982
2025
  return out;
1983
2026
  }
1984
2027
  function serialiseSelectors(map) {