@patterkit/ops 0.7.7 → 0.9.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +192 -17
  2. package/dist/index.js +435 -122
  3. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -1,11 +1,257 @@
1
1
  // src/load.ts
2
- import { readdirSync, readFileSync, statSync, existsSync } from "fs";
3
- import { join, dirname, resolve, basename } from "path";
2
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "fs";
3
+ import { join as join2, dirname as dirname2, resolve as resolve2, basename } from "path";
4
4
  import { parseSource } from "@patterkit/core";
5
5
  import { isCaseOnlyPropertyName } from "@patterkit/model";
6
+
7
+ // src/game-scopes.ts
8
+ import { existsSync, readdirSync, readFileSync } from "fs";
9
+ import { dirname, join, relative, resolve } from "path";
10
+ import {
11
+ findGameScopes,
12
+ mergeScopes,
13
+ parseScopesFile,
14
+ scopesCatalogue,
15
+ serialiseScopesFile,
16
+ standInRegistry,
17
+ GAME_SCOPES_DIR,
18
+ GAME_SCOPES_FILE,
19
+ SCOPES_FILE_SUFFIX
20
+ } from "@wildwinter/scoperegistry/scopes";
21
+ import { canonicalStringify } from "@patterkit/core";
22
+ import { PATTER_SCOPE, EXTERNAL_SCOPES } from "@patterkit/compiler";
23
+ var PATTER_SCOPES_FILE = `${PATTER_SCOPE}${SCOPES_FILE_SUFFIX}`;
24
+ var PATTER_SCOPES_OWNER = "Patter";
25
+ var GAME_SCOPES_OWNER = "Game";
26
+ var nodeFs = {
27
+ exists: (p) => existsSync(p),
28
+ parent: (p) => dirname(p),
29
+ join: (dir, name) => resolve(dir, name)
30
+ };
31
+ function readGameScopes(dir) {
32
+ const files = [];
33
+ const issues = [];
34
+ let patterText;
35
+ let names = [];
36
+ try {
37
+ names = readdirSync(dir).filter((n) => n.endsWith(SCOPES_FILE_SUFFIX)).sort();
38
+ } catch {
39
+ }
40
+ for (const name of names) {
41
+ const path = join(dir, name);
42
+ let text;
43
+ try {
44
+ text = readFileSync(path, "utf8");
45
+ } catch (e) {
46
+ issues.push({ severity: "error", file: path, message: `cannot read it: ${e instanceof Error ? e.message : String(e)}` });
47
+ continue;
48
+ }
49
+ if (name === PATTER_SCOPES_FILE) patterText = text;
50
+ const parsed = parseScopesFile(text, name);
51
+ for (const i of parsed.issues) issues.push({ ...i, file: path });
52
+ if (parsed.file) files.push({ fileName: name, file: parsed.file });
53
+ }
54
+ const merged = mergeScopes(files);
55
+ for (const i of merged.issues) issues.push({ ...i, file: join(dir, i.file) });
56
+ return { dir, files, merged, issues, ...patterText !== void 0 ? { patterText } : {} };
57
+ }
58
+ function discoverGameScopes(root, project) {
59
+ const found = findGameScopes(root, nodeFs, project.gameScopes !== void 0 ? { override: project.gameScopes } : {});
60
+ if (found.issue) return { missing: found.issue };
61
+ return found.dir ? { gameScopes: readGameScopes(found.dir) } : {};
62
+ }
63
+ function gameScopesSnapshot(root, project) {
64
+ const found = findGameScopes(root, nodeFs, project.gameScopes !== void 0 ? { override: project.gameScopes } : {});
65
+ if (!found.dir) return [];
66
+ let names = [];
67
+ try {
68
+ names = readdirSync(found.dir).filter((n) => n.endsWith(SCOPES_FILE_SUFFIX)).sort();
69
+ } catch {
70
+ }
71
+ return names.map((fileName) => ({ fileName, text: readFileSync(join(found.dir, fileName), "utf8") }));
72
+ }
73
+ function patterScopesFile(project) {
74
+ const declarations = (project.properties ?? []).filter((p) => p.shared ?? true).map((p) => ({
75
+ name: p.name,
76
+ type: p.type,
77
+ ...p.values !== void 0 ? { values: p.values } : {},
78
+ ...p.stages !== void 0 ? { stages: p.stages } : {},
79
+ ...p.default !== void 0 ? { default: p.default } : {},
80
+ ...p.purpose !== void 0 ? { purpose: p.purpose } : {}
81
+ }));
82
+ return { version: 1, owner: PATTER_SCOPES_OWNER, scopes: [{ token: PATTER_SCOPE, declarations }] };
83
+ }
84
+ function patterScopesWrite(project, gameScopes) {
85
+ if (!gameScopes || !existsSync(gameScopes.dir)) return void 0;
86
+ const content = serialiseScopesFile(patterScopesFile(project));
87
+ if (content === gameScopes.patterText) return void 0;
88
+ return { path: join(gameScopes.dir, PATTER_SCOPES_FILE), content };
89
+ }
90
+ function patterScopesStale(project, gameScopes) {
91
+ if (!gameScopes) return void 0;
92
+ if (serialiseScopesFile(patterScopesFile(project)) === gameScopes.patterText) return void 0;
93
+ return {
94
+ file: join(gameScopes.dir, PATTER_SCOPES_FILE),
95
+ message: `${GAME_SCOPES_DIR}/${PATTER_SCOPES_FILE} is ${gameScopes.patterText === void 0 ? "missing" : "out of date"}: save the project in Patterpad or run export`
96
+ };
97
+ }
98
+ function previewRegistry(gameScopes, bundle, into) {
99
+ if (!gameScopes || !bundle.externalScopes?.length) return void 0;
100
+ const registry = into ?? standInRegistry(gameScopes.merged, { except: [PATTER_SCOPE] });
101
+ if (into) {
102
+ for (const s of gameScopes.merged.spec.scopes) {
103
+ if (s.token === PATTER_SCOPE || registry.has(s.token)) continue;
104
+ registry.defineOwned(s.token, ownedDecls(s), { owner: gameScopes.merged.owners.get(s.token)?.owner ?? GAME_SCOPES_OWNER });
105
+ }
106
+ }
107
+ for (const spec of bundle.scopeRegistry?.scopes ?? []) {
108
+ if (!registry.has(spec.token)) registry.defineOwned(spec.token, ownedDecls(spec), { owner: PATTER_SCOPES_OWNER });
109
+ }
110
+ return registry;
111
+ }
112
+ function ownedDecls(s) {
113
+ return (s.declarations ?? []).map((d) => {
114
+ const writable = d.writable ?? s.writable;
115
+ return {
116
+ name: d.name,
117
+ type: d.type,
118
+ values: d.values,
119
+ stages: d.stages,
120
+ default: d.default,
121
+ ...writable !== void 0 ? { writable } : {}
122
+ };
123
+ });
124
+ }
125
+ function drivableScopes(loaded, bundle) {
126
+ const own = bundle.scopeRegistry?.scopes ?? [];
127
+ if (!loaded.gameScopes || !bundle.externalScopes?.length) return own;
128
+ const have = new Set(own.map((s) => s.token));
129
+ return [...own, ...loaded.gameScopes.merged.spec.scopes.filter((s) => s.token !== PATTER_SCOPE && !have.has(s.token))];
130
+ }
131
+ var hostScopeTokens = (loaded, bundle) => new Set(drivableScopes(loaded, bundle).map((s) => s.token));
132
+ var gameScopesCatalogue = (gameScopes) => gameScopes ? scopesCatalogue(gameScopes.merged, { except: [PATTER_SCOPE] }) : [];
133
+ var gameScopeTokens = (gameScopes) => (gameScopes?.merged.spec.scopes ?? []).map((s) => s.token).filter((t) => t !== PATTER_SCOPE);
134
+ var asHostScope = (s) => ({
135
+ token: s.token,
136
+ ...s.writable !== void 0 ? { writable: s.writable } : {},
137
+ ...s.declarations ? { declarations: s.declarations.map((d) => ({ ...d })) } : {}
138
+ });
139
+ function isGameToken(token, merged) {
140
+ if (token === PATTER_SCOPE || EXTERNAL_SCOPES.includes(token)) return false;
141
+ const who = merged?.owners.get(token);
142
+ return !who || who.fileName === GAME_SCOPES_FILE;
143
+ }
144
+ function worldSettingsScopes(project, gameScopes) {
145
+ const own = project.scopeRegistry;
146
+ if (!gameScopes) return own;
147
+ const merged = gameScopes.merged;
148
+ const fromGame = (token) => merged.owners.get(token)?.fileName === GAME_SCOPES_FILE ? merged.spec.scopes.find((s) => s.token === token) : void 0;
149
+ const scopes = (own?.scopes ?? []).map((h) => {
150
+ const g = fromGame(h.token);
151
+ return g ? asHostScope(g) : h;
152
+ });
153
+ const world = fromGame("world");
154
+ if (world && !scopes.some((s) => s.token === "world")) scopes.push(asHostScope(world));
155
+ return scopes.length ? { version: own?.version ?? 1, scopes } : void 0;
156
+ }
157
+ function planWorldSave(project, gameScopes, next) {
158
+ const path = join(gameScopes.dir, GAME_SCOPES_FILE);
159
+ let current;
160
+ let text;
161
+ if (existsSync(path)) {
162
+ text = readFileSync(path, "utf8");
163
+ const parsed = parseScopesFile(text, GAME_SCOPES_FILE);
164
+ if (!parsed.file) return { error: `${GAME_SCOPES_DIR}/${GAME_SCOPES_FILE} won't parse, so it was left alone: ${parsed.issues.map((i) => i.message).join("; ")}` };
165
+ current = parsed.file;
166
+ }
167
+ const merged = readGameScopes(gameScopes.dir).merged;
168
+ const ui = next?.scopes ?? [];
169
+ const scopes = [...current?.scopes ?? []];
170
+ for (const h of ui) {
171
+ if (!isGameToken(h.token, merged)) continue;
172
+ const at = scopes.findIndex((s) => s.token === h.token);
173
+ const spec = {
174
+ token: h.token,
175
+ ...h.writable !== void 0 ? { writable: h.writable } : {},
176
+ ...h.declarations ? { declarations: h.declarations.map((d) => ({ ...d })) } : {}
177
+ };
178
+ if (at >= 0) scopes[at] = spec;
179
+ else scopes.push(spec);
180
+ }
181
+ const shown2 = /* @__PURE__ */ new Set(["world", ...(project.scopeRegistry?.scopes ?? []).map((s) => s.token)]);
182
+ const kept = scopes.filter((s) => !(shown2.has(s.token) && isGameToken(s.token, merged) && !ui.some((h) => h.token === s.token)));
183
+ const file = { version: current?.version ?? 1, owner: current?.owner ?? GAME_SCOPES_OWNER, scopes: kept };
184
+ const content = serialiseScopesFile(file);
185
+ const write = content === text || !current && kept.length === 0 ? void 0 : { path, content };
186
+ return { ...write ? { write } : {}, ...next && next.scopes.length ? { scopeRegistry: next } : {} };
187
+ }
188
+ function planReturnedWorld(ours, base, theirs, merged, gameScopes) {
189
+ const now = readGameScopes(gameScopes.dir);
190
+ const game = (p) => new Map((p.scopeRegistry?.scopes ?? []).filter((s) => isGameToken(s.token, now.merged)).map((s) => [s.token, canonicalStringify(s)]));
191
+ const was = game(base), came = game(theirs);
192
+ const changed = new Set([...was.keys(), ...came.keys()].filter((t) => was.get(t) !== came.get(t)));
193
+ if (!changed.size) return {};
194
+ const file = now.files.find((f) => f.fileName === GAME_SCOPES_FILE)?.file;
195
+ const next = { version: 1, scopes: [
196
+ ...(file?.scopes ?? []).filter((s) => !changed.has(s.token)).map(asHostScope),
197
+ ...(merged.scopeRegistry?.scopes ?? []).filter((s) => changed.has(s.token))
198
+ ] };
199
+ const shown2 = { ...ours, scopeRegistry: { version: 1, scopes: [...ours.scopeRegistry?.scopes ?? [], ...base.scopeRegistry?.scopes ?? []] } };
200
+ const plan = planWorldSave(shown2, gameScopes, next);
201
+ return { ...plan.write ? { write: plan.write } : {}, ...plan.error ? { error: plan.error } : {} };
202
+ }
203
+ function defaultGameScopesDir(root) {
204
+ for (let dir = resolve(root); ; ) {
205
+ if (existsSync(join(dir, ".git"))) return join(dir, GAME_SCOPES_DIR);
206
+ const up = dirname(dir);
207
+ if (up === dir) break;
208
+ dir = up;
209
+ }
210
+ return join(dirname(resolve(root)), GAME_SCOPES_DIR);
211
+ }
212
+ function planShareScopes(root, project, dir) {
213
+ const target = resolve(dir);
214
+ const gamePath = join(target, GAME_SCOPES_FILE);
215
+ let existing = { version: 1, owner: GAME_SCOPES_OWNER, scopes: [] };
216
+ if (existsSync(gamePath)) {
217
+ const parsed = parseScopesFile(readFileSync(gamePath, "utf8"), GAME_SCOPES_FILE);
218
+ if (!parsed.file) return { error: `${GAME_SCOPES_FILE} in ${target} won't parse, so it was left alone: ${parsed.issues.map((i) => i.message).join("; ")}` };
219
+ existing = parsed.file;
220
+ }
221
+ const held = new Set(existing.scopes.map((s) => s.token));
222
+ const added = (project.scopeRegistry?.scopes ?? []).filter((h) => isGameToken(h.token, void 0) && !held.has(h.token)).map((h) => ({
223
+ token: h.token,
224
+ ...h.writable !== void 0 ? { writable: h.writable } : {},
225
+ ...h.declarations ? { declarations: h.declarations.map((d) => ({ ...d })) } : {}
226
+ }));
227
+ const game = { ...existing, scopes: [...existing.scopes, ...added] };
228
+ const writes = [
229
+ { path: join(target, PATTER_SCOPES_FILE), content: serialiseScopesFile(patterScopesFile(project)) },
230
+ { path: gamePath, content: serialiseScopesFile(game) }
231
+ ];
232
+ let found = false;
233
+ for (let at = resolve(root); ; ) {
234
+ if (join(at, GAME_SCOPES_DIR) === target) {
235
+ found = true;
236
+ break;
237
+ }
238
+ const up = dirname(at);
239
+ if (existsSync(join(at, ".git")) || up === at) break;
240
+ at = up;
241
+ }
242
+ const { gameScopes: _drop, ...rest } = project;
243
+ const next = found ? rest : { ...rest, gameScopes: relative(resolve(root), target) || "." };
244
+ return { writes, project: next };
245
+ }
246
+
247
+ // src/load.ts
248
+ function withGameScopes(root, project) {
249
+ const { gameScopes, missing } = discoverGameScopes(root, project);
250
+ return { ...gameScopes ? { gameScopes } : {}, ...missing ? { gameScopesMissing: missing } : {} };
251
+ }
6
252
  function readDirSafe(dir) {
7
253
  try {
8
- return readdirSync(dir, { withFileTypes: true });
254
+ return readdirSync2(dir, { withFileTypes: true });
9
255
  } catch {
10
256
  return [];
11
257
  }
@@ -14,26 +260,26 @@ function walkFiles(dir, ext) {
14
260
  const out = [];
15
261
  for (const e of readDirSafe(dir)) {
16
262
  if (e.name.startsWith(".")) continue;
17
- const p = join(dir, e.name);
263
+ const p = join2(dir, e.name);
18
264
  if (e.isDirectory()) out.push(...walkFiles(p, ext));
19
265
  else if (e.isFile() && e.name.endsWith(ext)) out.push(p);
20
266
  }
21
267
  return out.sort();
22
268
  }
23
269
  function findProjectFile(startPath) {
24
- let dir = resolve(startPath);
270
+ let dir = resolve2(startPath);
25
271
  try {
26
272
  if (statSync(dir).isFile()) {
27
273
  if (dir.endsWith(".patterproj")) return dir;
28
- dir = dirname(dir);
274
+ dir = dirname2(dir);
29
275
  }
30
276
  } catch {
31
277
  }
32
278
  for (; ; ) {
33
- const here = readDirSafe(dir).filter((e) => e.isFile() && e.name.endsWith(".patterproj")).map((e) => join(dir, e.name)).sort();
279
+ const here = readDirSafe(dir).filter((e) => e.isFile() && e.name.endsWith(".patterproj")).map((e) => join2(dir, e.name)).sort();
34
280
  if (here.length > 1) throw new Error(`multiple project files in ${dir}: ${here.map((p) => p.slice(dir.length + 1)).join(", ")}`);
35
281
  if (here.length === 1) return here[0];
36
- const parent = dirname(dir);
282
+ const parent = dirname2(dir);
37
283
  if (parent === dir) throw new Error(`no .patterproj found at or above ${startPath}`);
38
284
  dir = parent;
39
285
  }
@@ -41,7 +287,7 @@ function findProjectFile(startPath) {
41
287
  function parseFile(file, expectSchema, shapeKey) {
42
288
  let parsed;
43
289
  try {
44
- parsed = parseSource(readFileSync(file, "utf8"));
290
+ parsed = parseSource(readFileSync2(file, "utf8"));
45
291
  } catch (e) {
46
292
  throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
47
293
  }
@@ -97,16 +343,16 @@ function foldDeclaredNames(parsed, expectSchema) {
97
343
  }
98
344
  }
99
345
  function sceneIdForShard(shardPath) {
100
- const p = resolve(shardPath);
346
+ const p = resolve2(shardPath);
101
347
  try {
102
- if (p.endsWith(".patterflow")) return parseSource(readFileSync(p, "utf8")).scene?.id;
103
- if (p.endsWith(".patterloc")) return parseSource(readFileSync(p, "utf8")).scene;
348
+ if (p.endsWith(".patterflow")) return parseSource(readFileSync2(p, "utf8")).scene?.id;
349
+ if (p.endsWith(".patterloc")) return parseSource(readFileSync2(p, "utf8")).scene;
104
350
  if (p.endsWith(".patterx")) {
105
351
  const projectFile = findProjectFile(p);
106
352
  const project = parseFile(projectFile, "patter/project", "project");
107
- const flowDir = join(dirname(projectFile), project.layout?.flow ?? "scenes/");
108
- const flowPath = join(flowDir, basename(p).replace(/\.patterx$/, ".patterflow"));
109
- return parseSource(readFileSync(flowPath, "utf8")).scene?.id;
353
+ const flowDir = join2(dirname2(projectFile), project.layout?.flow ?? "scenes/");
354
+ const flowPath = join2(flowDir, basename(p).replace(/\.patterx$/, ".patterflow"));
355
+ return parseSource(readFileSync2(flowPath, "utf8")).scene?.id;
110
356
  }
111
357
  } catch {
112
358
  }
@@ -114,13 +360,13 @@ function sceneIdForShard(shardPath) {
114
360
  }
115
361
  function loadProjectLanding(startPath, opts) {
116
362
  const projectFile = findProjectFile(startPath);
117
- const root = dirname(projectFile);
363
+ const root = dirname2(projectFile);
118
364
  const project = parseFile(projectFile, "patter/project", "project");
119
365
  const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
120
- const flowFiles = walkFiles(join(root, layout.flow), ".patterflow");
121
- const launchPath = opts?.launchPath ? resolve(opts.launchPath) : void 0;
366
+ const flowFiles = walkFiles(join2(root, layout.flow), ".patterflow");
367
+ const launchPath = opts?.launchPath ? resolve2(opts.launchPath) : void 0;
122
368
  let landingFile;
123
- if (launchPath?.endsWith(".patterflow")) landingFile = flowFiles.find((f) => resolve(f) === launchPath);
369
+ if (launchPath?.endsWith(".patterflow")) landingFile = flowFiles.find((f) => resolve2(f) === launchPath);
124
370
  if (!landingFile) {
125
371
  const targetId = (opts?.launchPath ? sceneIdForShard(opts.launchPath) : void 0) ?? opts?.preferId;
126
372
  if (targetId) {
@@ -144,10 +390,10 @@ function loadProjectLanding(startPath, opts) {
144
390
  const localeFiles = [];
145
391
  if (landingScene && landingFile) {
146
392
  const defaultLocale = project.locales.default;
147
- const strings = join(root, layout.strings);
148
- const guess = join(strings, defaultLocale, `${basename(landingFile).replace(/\.patterflow$/, "")}.patterloc`);
393
+ const strings = join2(root, layout.strings);
394
+ const guess = join2(strings, defaultLocale, `${basename(landingFile).replace(/\.patterflow$/, "")}.patterloc`);
149
395
  let found = false;
150
- if (existsSync(guess)) {
396
+ if (existsSync2(guess)) {
151
397
  try {
152
398
  const loc = parseFile(guess, "patter/strings", "strings");
153
399
  if (loc.scene === landingScene.id) {
@@ -168,7 +414,7 @@ function loadProjectLanding(startPath, opts) {
168
414
  }
169
415
  }
170
416
  }
171
- return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [] };
417
+ return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [], ...withGameScopes(root, project) };
172
418
  }
173
419
  function applySceneOrder(scenes, order) {
174
420
  if (!order?.length) return;
@@ -184,12 +430,12 @@ function applySceneOrder(scenes, order) {
184
430
  }
185
431
  function loadProject(startPath) {
186
432
  const projectFile = findProjectFile(startPath);
187
- const root = dirname(projectFile);
433
+ const root = dirname2(projectFile);
188
434
  const project = parseFile(projectFile, "patter/project", "project");
189
435
  const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
190
436
  const scenes = [];
191
437
  const sceneFiles = {};
192
- for (const f of walkFiles(join(root, layout.flow), ".patterflow")) {
438
+ for (const f of walkFiles(join2(root, layout.flow), ".patterflow")) {
193
439
  const scene = parseFile(f, "patter/flow", "scene").scene;
194
440
  const existing = sceneFiles[scene.id];
195
441
  if (existing !== void 0) {
@@ -201,38 +447,38 @@ function loadProject(startPath) {
201
447
  applySceneOrder(scenes, project.sceneOrder);
202
448
  const locales = [];
203
449
  const localeFiles = [];
204
- for (const f of walkFiles(join(root, layout.strings), ".patterloc")) {
450
+ for (const f of walkFiles(join2(root, layout.strings), ".patterloc")) {
205
451
  locales.push(parseFile(f, "patter/strings", "strings"));
206
452
  localeFiles.push(f);
207
453
  }
208
454
  const authoring = [];
209
455
  const authoringFiles = [];
210
- for (const f of walkFiles(join(root, layout.authoring), ".patterx")) {
456
+ for (const f of walkFiles(join2(root, layout.authoring), ".patterx")) {
211
457
  authoring.push(parseFile(f, "patter/authoring", "schema"));
212
458
  authoringFiles.push(f);
213
459
  }
214
- return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles };
460
+ return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles, ...withGameScopes(root, project) };
215
461
  }
216
462
 
217
463
  // src/write.ts
218
464
  import { writeFileSync, mkdirSync } from "fs";
219
- import { dirname as dirname2 } from "path";
465
+ import { dirname as dirname3 } from "path";
220
466
  function applyWrites(writes) {
221
467
  for (const w of writes) {
222
- mkdirSync(dirname2(w.path), { recursive: true });
468
+ mkdirSync(dirname3(w.path), { recursive: true });
223
469
  writeFileSync(w.path, w.content, "utf8");
224
470
  }
225
471
  }
226
472
 
227
473
  // src/validate.ts
228
- import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
229
- import { basename as basename2, isAbsolute, join as join2, relative } from "path";
474
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
475
+ import { basename as basename2, isAbsolute, join as join3, relative as relative2 } from "path";
230
476
 
231
477
  // src/merge.ts
232
- import { canonicalStringify } from "@patterkit/core";
478
+ import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
233
479
  var UnsupportedMergeError = class extends Error {
234
480
  };
235
- var eq = (a, b) => canonicalStringify(a) === canonicalStringify(b);
481
+ var eq = (a, b) => canonicalStringify2(a) === canonicalStringify2(b);
236
482
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
237
483
  var CONFLICT_SIDECAR = ".patterconflict";
238
484
  function sidecarIssues(sidecarPaths) {
@@ -273,7 +519,7 @@ function merge3(base, ours, theirs, id, path, conflicts) {
273
519
  conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
274
520
  return ours;
275
521
  }
276
- function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
522
+ function mergeMap(base, ours, theirs, prefix, conflicts, resolve6) {
277
523
  const out = {};
278
524
  for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
279
525
  const b = base[k], o = ours[k], t = theirs[k];
@@ -282,7 +528,7 @@ function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
282
528
  else if (eq(b, o)) v = t;
283
529
  else if (eq(b, t)) v = o;
284
530
  else {
285
- const r = resolve5?.(k, b, o, t);
531
+ const r = resolve6?.(k, b, o, t);
286
532
  if (r && r.resolved) v = r.value;
287
533
  else {
288
534
  conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
@@ -572,7 +818,7 @@ var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
572
818
 
573
819
  // src/validate.ts
574
820
  import { validateProject, parseSource as parseSource2 } from "@patterkit/core";
575
- import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec } from "@patterkit/compiler";
821
+ import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec, projectScopes } from "@patterkit/compiler";
576
822
 
577
823
  // src/reachability.ts
578
824
  import { exportBundle } from "@patterkit/compiler";
@@ -682,8 +928,8 @@ var termsOfAst = (ast, sceneId) => terms(ast, sceneId, keyOf);
682
928
  var requirementsOf = (expr, sceneId) => expr === void 0 ? [] : termsOfAst(expr.ast, sceneId);
683
929
  var asFlags = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
684
930
  function reachabilityIssues(loaded, compiled) {
685
- const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
686
- const hostTokens = new Set((loaded.project.scopeRegistry?.scopes ?? []).map((s) => s.token));
931
+ const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
932
+ const hostTokens = hostScopeTokens(loaded, bundle);
687
933
  const preset = /* @__PURE__ */ new Set();
688
934
  const notALatch = (decl, key) => {
689
935
  if (decl.temporary) {
@@ -826,14 +1072,17 @@ function reachabilityIssues(loaded, compiled) {
826
1072
  function runValidate(loaded) {
827
1073
  const { project, scenes, locales } = loaded;
828
1074
  const structural = validateProject({ project, scenes, authoring: loaded.authoring });
829
- const foreignScopes = hostScopesToSpec(project.scopeRegistry);
830
- const conditions = validateConditions({ project, scenes }, { foreignScopes });
831
- const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes });
1075
+ const merged = loaded.gameScopes?.merged;
1076
+ const scopes = projectScopes(project, merged);
1077
+ const foreignScopes = hostScopesToSpec(scopes.host);
1078
+ const conditions = validateConditions({ project, scenes }, { foreignScopes, gameScopes: merged });
1079
+ const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged });
1080
+ const gameScopes = gameScopesIssues(loaded, scopes.notes);
832
1081
  const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles]);
833
1082
  const staleBundles = checkBundles(loaded);
834
1083
  const unresolvedMerges = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
835
1084
  const orphans = orphanShards(loaded);
836
- const reachability = structural.length === 0 && conditions.length === 0 ? reachabilityIssues(loaded) : [];
1085
+ const reachability = structural.length === 0 && !conditions.some(isError) ? reachabilityIssues(loaded) : [];
837
1086
  return {
838
1087
  structural,
839
1088
  conditions,
@@ -843,9 +1092,22 @@ function runValidate(loaded) {
843
1092
  staleBundles,
844
1093
  unresolvedMerges,
845
1094
  orphans,
846
- ok: structural.length === 0 && conditions.length === 0 && interpolation.length === 0 && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0
1095
+ gameScopes,
1096
+ ok: structural.length === 0 && !conditions.some(isError) && !interpolation.some(isError) && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0 && !gameScopes.some(isError)
847
1097
  };
848
1098
  }
1099
+ var isError = (i) => i.severity === "error";
1100
+ function gameScopesIssues(loaded, notes) {
1101
+ const out = [];
1102
+ if (loaded.gameScopesMissing) out.push({ file: loaded.projectFile, severity: "error", message: loaded.gameScopesMissing });
1103
+ const gs = loaded.gameScopes;
1104
+ if (!gs) return out;
1105
+ for (const i of gs.issues) out.push({ file: i.file, severity: i.severity, message: i.message });
1106
+ for (const n of notes) out.push({ file: join3(gs.dir, n.file), severity: "warning", message: n.message });
1107
+ const stale = patterScopesStale(loaded.project, gs);
1108
+ if (stale) out.push({ ...stale, severity: "warning" });
1109
+ return out;
1110
+ }
849
1111
  function orphanShards(loaded) {
850
1112
  const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...loaded.project.layout };
851
1113
  const kind = {
@@ -855,14 +1117,14 @@ function orphanShards(loaded) {
855
1117
  ".patterproj": { what: "project", home: null }
856
1118
  };
857
1119
  const inside = (file, dir) => {
858
- const r = relative(dir, file);
1120
+ const r = relative2(dir, file);
859
1121
  return r !== "" && !r.startsWith("..") && !isAbsolute(r);
860
1122
  };
861
1123
  const out = [];
862
1124
  for (const [ext, { what, home }] of Object.entries(kind)) {
863
1125
  for (const file of walkFiles(loaded.root, ext)) {
864
- if (home ? inside(file, join2(loaded.root, home)) : file === loaded.projectFile) continue;
865
- const rel = relative(loaded.root, file);
1126
+ if (home ? inside(file, join3(loaded.root, home)) : file === loaded.projectFile) continue;
1127
+ const rel = relative2(loaded.root, file);
866
1128
  const message = home ? `${rel} is a ${what} file outside the project's folders: nothing loads it, so none of it is in the project. Move it under ${home}, or delete it.` : `${rel} is a second project file: only ${basename2(loaded.projectFile)} is read. Delete it, or move it out of the project.`;
867
1129
  out.push({ file, message });
868
1130
  }
@@ -875,14 +1137,14 @@ function checkBundles(loaded) {
875
1137
  if (bundles.length === 0) return issues;
876
1138
  let fresh;
877
1139
  try {
878
- fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales }).content.hash;
1140
+ fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged }).content.hash;
879
1141
  } catch {
880
1142
  return issues;
881
1143
  }
882
1144
  for (const file of bundles) {
883
1145
  let hash;
884
1146
  try {
885
- const parsed = parseSource2(readFileSync2(file, "utf8"));
1147
+ const parsed = parseSource2(readFileSync3(file, "utf8"));
886
1148
  hash = parsed?.content?.hash;
887
1149
  } catch {
888
1150
  issues.push({ file, message: "compiled bundle is unparseable - run `patter export`" });
@@ -911,7 +1173,7 @@ function checkHygiene(files) {
911
1173
  }
912
1174
  const fileIssues = [];
913
1175
  try {
914
- const text = readFileSync2(file, "utf8");
1176
+ const text = readFileSync3(file, "utf8");
915
1177
  if (text.charCodeAt(0) === 65279) {
916
1178
  fileIssues.push({ file, message: "file starts with a UTF-8 BOM (canonical form is UTF-8 without BOM - run `patter format`)" });
917
1179
  }
@@ -928,7 +1190,7 @@ function checkHygiene(files) {
928
1190
  }
929
1191
 
930
1192
  // src/export.ts
931
- import { basename as basename3, isAbsolute as isAbsolute2, join as join3 } from "path";
1193
+ import { basename as basename3, isAbsolute as isAbsolute2, join as join4 } from "path";
932
1194
  import { exportBundle as exportBundle3 } from "@patterkit/compiler";
933
1195
  import { isContentlessBeat } from "@patterkit/model";
934
1196
  function compileScenes(loaded) {
@@ -961,7 +1223,7 @@ function refuseUnresolvedMerge(loaded) {
961
1223
  function runExport(loaded) {
962
1224
  refuseUnresolvedMerge(loaded);
963
1225
  const { project, locales } = loaded;
964
- const full = exportBundle3({ project, scenes: compileScenes(loaded), locales });
1226
+ const full = exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
965
1227
  const loc = project.export?.localisation;
966
1228
  if (!loc || loc.mode === "embedded") return full;
967
1229
  const strings = loc.sourceDebug ? { [full.locales.default]: full.strings[full.locales.default] ?? {} } : {};
@@ -970,17 +1232,17 @@ function runExport(loaded) {
970
1232
  function runExportFull(loaded) {
971
1233
  refuseUnresolvedMerge(loaded);
972
1234
  const { project, locales } = loaded;
973
- return exportBundle3({ project, scenes: compileScenes(loaded), locales });
1235
+ return exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
974
1236
  }
975
1237
  function bundleOutputPath(loaded) {
976
1238
  const override = loaded.project.export?.bundle;
977
- if (override) return isAbsolute2(override) ? override : join3(loaded.root, override);
1239
+ if (override) return isAbsolute2(override) ? override : join4(loaded.root, override);
978
1240
  const stem = basename3(loaded.projectFile).replace(/\.patterproj$/, "");
979
- return join3(loaded.root, "dist", `${stem}.patterc`);
1241
+ return join4(loaded.root, "dist", `${stem}.patterc`);
980
1242
  }
981
1243
 
982
1244
  // src/export-html.ts
983
- import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
1245
+ import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
984
1246
 
985
1247
  // src/playable-runtime.ts
986
1248
  var PLAYABLE_RUNTIME_JS = 'var Patterplay=(()=>{var q=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var Be=Object.getOwnPropertyNames;var Pe=Object.prototype.hasOwnProperty;var Ne=(n,e)=>{for(var t in e)q(n,t,{get:e[t],enumerable:!0})},Fe=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Be(e))!Pe.call(n,s)&&s!==t&&q(n,s,{get:()=>e[s],enumerable:!(r=Oe(e,s))||r.enumerable});return n};var Te=n=>Fe(q({},"__esModule",{value:!0}),n);var Qe={};Ne(Qe,{Engine:()=>$,Flow:()=>D,buildTagIndex:()=>M,describeBundle:()=>ke,effectiveGameData:()=>Ie,gameDataFields:()=>Ce,gameDataValue:()=>Q});function E(n){switch(n[0]){case"b":return{kind:"bool",value:n[1]};case"n":return{kind:"number",value:n[1]};case"s":return{kind:"string",value:n[1]};case"sv":return{kind:"scopedvar",scope:n[1],name:n[2]};case"u":return{kind:"unary",op:n[1],operand:E(n[2])};case"bin":return{kind:"binary",op:n[1],left:E(n[2]),right:E(n[3])};case"call":{let e=n.slice(2).map(E);return{kind:"call",name:n[1],args:e}}case"fd":return{kind:"flagdelta",sign:n[1],name:n[2]}}}var p=class extends Error{constructor(e){super(e),this.name="EvalError"}};function F(n,e,t){let r=new Map(t.scopes.map(i=>[i.token,i.missing??"false"])),s=i=>{switch(i.kind){case"bool":return i.value;case"number":return i.value;case"string":return i.value;case"scopedvar":{let o=e.scopes[i.scope];if(o===void 0)return!1;let a=typeof o.get=="function"?o.get(i.name):o[i.name];if(a===void 0){if(r.get(i.scope)==="throw")throw new p(`@${i.scope}.${i.name} is not declared on the current ${i.scope}.`);return!1}return a}case"call":{if(i.name==="advance"&&!t.functions[i.name]){let a=i.args[0];if(i.args.length!==1||a===void 0)throw new p(`advance() takes exactly 1 argument, got ${i.args.length}`);let c=_(a,e);if(c===void 0)throw new p("advance() needs a quality reference (@scope.name of a quality property)");let l=w(s(a),c,"advance");return c[Math.min(l+1,c.length-1)]}let o=t.functions[i.name];if(!o)throw new p(`unknown function \'${i.name}\'`);return o.eval(i.args,{evaluate:s,ctx:e})}case"flagdelta":throw new p("flagdelta node is only valid as an argument to a flag-delta function");case"unary":{if(i.op==="not"){let a=s(i.operand);if(typeof a!="boolean")throw new p(`\'not\' requires a boolean operand, got ${typeof a}`);return!a}let o=s(i.operand);if(typeof o!="number")throw new p(`unary \'-\' requires a numeric operand, got ${typeof o}`);return-o}case"binary":{if(i.op==="and"){let u=s(i.left);if(typeof u!="boolean")throw new p(`\'and\' requires boolean operands, left is ${typeof u}`);if(!u)return!1;let S=s(i.right);if(typeof S!="boolean")throw new p(`\'and\' requires boolean operands, right is ${typeof S}`);return S}if(i.op==="or"){let u=s(i.left);if(typeof u!="boolean")throw new p(`\'or\' requires boolean operands, left is ${typeof u}`);if(u)return!0;let S=s(i.right);if(typeof S!="boolean")throw new p(`\'or\' requires boolean operands, right is ${typeof S}`);return S}let o=s(i.left),a=s(i.right),c=_(i.left,e),l=_(i.right,e),d=c??l;if(d!==void 0){if(c&&l&&!Ae(c,l)&&(i.op===">"||i.op===">="||i.op==="<"||i.op==="<="))throw new p(`\'${i.op}\' compares two different qualities, whose stage orders are unrelated`);switch(i.op){case">":return w(o,d,">")>w(a,d,">");case">=":return w(o,d,">=")>=w(a,d,">=");case"<":return w(o,d,"<")<w(a,d,"<");case"<=":return w(o,d,"<=")<=w(a,d,"<=");case"+":case"-":case"*":case"/":throw new p(`\'${i.op}\' cannot be applied to a quality - a stage is a position, not a number; use advance() to move it`);default:break}}switch(i.op){case"==":return se(o,a);case"!=":return!se(o,a);case">":return k(o,a,">"),o>a;case">=":return k(o,a,">="),o>=a;case"<":return k(o,a,"<"),o<a;case"<=":return k(o,a,"<="),o<=a;case"+":if(typeof o=="number"&&typeof a=="number"||typeof o=="string"&&typeof a=="string")return o+a;throw new p(`\'+\' requires two numbers or two strings, got ${typeof o} and ${typeof a}`);case"-":return k(o,a,"-"),o-a;case"*":return k(o,a,"*"),o*a;case"/":if(k(o,a,"/"),a===0)throw new p("division by zero");return o/a}}}};return s(n)}function se(n,e){if(Array.isArray(n)||Array.isArray(e)){if(!Array.isArray(n)||!Array.isArray(e)||n.length!==e.length)return!1;let t=[...n].sort(),r=[...e].sort();for(let s=0;s<t.length;s++)if(t[s]!==r[s])return!1;return!0}return n===e}function k(n,e,t){if(typeof n!="number"||typeof e!="number")throw new p(`\'${t}\' requires numeric operands, got ${typeof n} and ${typeof e}`)}function _(n,e){if(!(n.kind!=="scopedvar"||e.qualities===void 0))return e.qualities(n.scope,n.name)}function w(n,e,t){if(typeof n!="string")throw new p(`\'${t}\' on a quality compares stages, got ${typeof n}`);let r=e.indexOf(n);if(r<0)throw new p(`"${n}" is not a stage of this quality (stages: ${e.join(", ")})`);return r}var Ae=(n,e)=>n.length===e.length&&n.every((t,r)=>t===e[r]);function R(n){if(Number.isNaN(n)||!Number.isFinite(n))return 0;let e=Math.trunc(n)%4294967296;return e<0?e+4294967296:e}function H(n){let e=R(n);return{next(){e=e+1831565813>>>0;let t=Math.imul(e^e>>>15,1|e);return t=t+Math.imul(t^t>>>7,61|t)^t,((t^t>>>14)>>>0)/4294967296},state(){return e}}}var Ge={name:"check_flags",count:n=>Math.max(1,n.args.length-1)},Ve=[Ge];function ie(n,e,t){let r=t?.countingCalls??Ve;return T(n,t?.want??!0,e,r)}function T(n,e,t,r){if(n.kind==="binary"&&(n.op==="and"||n.op==="or")){let s=T(n.left,e,t,r),i=T(n.right,e,t,r);return n.op==="and"===e?s>0&&i>0?s+i:0:Math.max(s,i)}if(n.kind==="unary"&&n.op==="not")return T(n.operand,!e,t,r);if(n.kind==="call"){let s=r.find(i=>i.name===n.name);if(s){let i=s.count(n),o=t(n);return e?o?i:0:o?0:1}}return t(n)===e?1:0}var b=class n{values={};decls=new Map;subscribers=new Set;auditors=new Set;norm;pathPrefix;constructor(e=[],t){this.norm=t?.normalise??(r=>r.toLowerCase()),this.pathPrefix=t?.pathPrefix??"",this.seed(e)}seed(e){for(let t of e){let r=this.norm(t.name);this.decls.set(r,t),this.values[r]=structuredClone(t.default??O(t))}}get(e){return this.values[this.norm(e)]}normalise(e){return this.norm(e)}set(e,t,r){let s=this.norm(e);if(!r?.host&&this.decls.get(s)?.writable===!1)throw new Error(`\'${e}\' is read-only`);let i={name:s,prev:this.values[s],next:t,silent:r?.silent??!1,reason:r?.reason};this.values[s]=t;for(let o of this.auditors)o(i);if(!i.silent)for(let o of this.subscribers)o(i);return i}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}onAudit(e){return this.auditors.add(e),()=>this.auditors.delete(e)}rows(){return[...this.decls.entries()].map(([e,t])=>ae(t,this.get(e),void 0,e,this.pathPrefix))}declarations(){return[...this.decls.values()]}clone(){let e=new n([],{normalise:this.norm,pathPrefix:this.pathPrefix});return e.decls=new Map(this.decls),Object.assign(e.values,structuredClone(this.values)),e}reseed(e){for(let t of Object.keys(this.values))delete this.values[t];this.decls.clear(),this.seed(e)}save(){return structuredClone(this.values)}load(e){for(let[t,r]of Object.entries(e))this.values[this.norm(t)]=r}};function ae(n,e,t,r,s=""){let i=r??n.name.toLowerCase();return{name:i,path:s+i,type:n.type,value:e,default:n.default??O(n),...n.values!==void 0?{values:n.values}:{},...n.stages!==void 0?{stages:n.stages}:{},writable:t??n.writable??!0}}var Le=n=>n.toLowerCase(),U=1,A=class{scopes=new Map;parked=new Map;rev=0;get revision(){return this.rev}defineOwned(e,t,r){let s=typeof r=="string"?{pathPrefix:r}:r??{},i=new b(t,{pathPrefix:s.pathPrefix??`${e}.`,...s.normalise?{normalise:s.normalise}:{}});return this.mountOwned(e,i,s.owner!==void 0?{owner:s.owner}:void 0)}mountOwned(e,t,r){this.assertFree(e,r?.owner),this.scopes.set(e,{kind:"owned",bag:t,...r?.owner!==void 0?{owner:r.owner}:{}}),this.rev++;let s=this.parked.get(e);return s&&(t.load(s),this.parked.delete(e)),this}remove(e,t){let r=this.scopes.get(e);if(!r)throw new Error(`unknown scope \'@${e}\'`);return t?.keep&&r.kind==="owned"&&this.parked.set(e,r.bag.save()),this.scopes.delete(e),this.rev++,this}discardParked(e){if(e===void 0)this.parked.clear();else for(let t of[...this.parked.keys()])t.startsWith(e)&&this.parked.delete(t);return this}ownedBag(e){let t=this.scopes.get(e);if(!t||t.kind!=="owned")throw new Error(`\'@${e}\' is not an owned scope`);return t.bag}reseedOwned(e,t){return this.ownedBag(e).reseed(t),this}defineForeign(e,t,r=[],s=!0){let i=typeof s=="boolean"?{writable:s}:s;this.assertFree(e,i.owner);let o=i.normalise??Le,a=new Map;for(let c of r)a.set(o(c.name),c);return this.scopes.set(e,{kind:"foreign",resolver:t,decls:a,scopeWritable:i.writable??!0,norm:o,...i.owner!==void 0?{owner:i.owner}:{}}),this.rev++,this}has(e){return this.scopes.has(e)}get(e,t){let r=this.scopes.get(e);if(r)return r.kind==="owned"?r.bag.get(t):r.resolver.get(r.norm(t))}set(e,t,r,s){let i=this.scopes.get(e);if(!i)throw new Error(`unknown scope \'@${e}\'`);if(i.kind==="owned"){try{i.bag.set(t,r,s?.host?{host:!0}:void 0)}catch{throw new Error(`\'@${e}.${t}\' is read-only`)}return}let o=i.norm(t);if(!i.resolver.set)throw new Error(`\'@${e}.${t}\' is read-only`);if(!s?.host&&!this.foreignWritable(i,o))throw new Error(`\'@${e}.${t}\' is read-only`);i.resolver.set(o,r)}foreignWritable(e,t){return e.resolver.set?e.decls.get(t)?.writable??e.scopeWritable:!1}listProperties(){let e=[];for(let[t,r]of this.scopes){let s=r.owner!==void 0?{owner:r.owner}:{};if(r.kind==="owned")for(let i of r.bag.rows())e.push({scope:t,...s,...i});else for(let[i,o]of r.decls)e.push({scope:t,...s,...ae(o,r.resolver.get(i),this.foreignWritable(r,i),i,`${t}.`)})}return e}toEvalContext(e,t){let r=this.view(t?.aliases),s={};for(let[o,a]of r)s[o]=a.kind==="owned"?a.bag.values:a.resolver;let i=this.qualityLadders(r);return i.size===0?{scopes:s,host:e}:{scopes:s,host:e,qualities:(o,a)=>{let c=r.get(o);return c?i.get(o)?.get(Me(c)(a)):void 0}}}view(e){let t=new Map(this.scopes);for(let[r,s]of Object.entries(e??{})){let i=this.scopes.get(s);if(!i)throw new Error(`alias \'@${r}\' names \'${s}\', which is not registered`);t.set(r,i)}return t}qualityLadders(e){let t=new Map;for(let[r,s]of e)for(let[i,o]of oe(s)){if(o.type!=="quality"||o.stages===void 0)continue;let a=t.get(r);a||(a=new Map,t.set(r,a)),a.set(i,o.stages)}return t}toSchema(e){let t=new Map;for(let[r,s]of this.view(e?.aliases)){let i=oe(s);if(i.length===0)continue;let o=new Map;for(let[a,c]of i)o.set(a,{type:c.type,enumValues:c.values,...c.stages!==void 0?{stages:c.stages}:{}});t.set(r,o)}return{properties:t}}save(){let e={};for(let[t,r]of this.scopes)r.kind==="owned"&&(e[t]=r.bag.save());for(let[t,r]of this.parked)e[t]=structuredClone(r);return e}load(e,t){t?.keepParked||this.parked.clear();for(let[r,s]of Object.entries(e)){let i=this.scopes.get(r);i?.kind==="owned"?i.bag.load(s):i||this.parked.set(r,structuredClone(s))}}saveFragment(){return{version:U,scopes:this.save()}}loadFragment(e){if(e.version!==U)throw new Error(`unsupported owned-state fragment version ${e.version} (supported: ${U})`);this.load(e.scopes)}assertFree(e,t){let r=this.scopes.get(e);if(!r)return;let s=r.owner!==void 0?` by ${r.owner}`:"",i=t!==void 0?` (wanted by ${t})`:"";throw new Error(`scope \'@${e}\' is already registered${s}${i}`)}};function oe(n){return n.kind==="foreign"?[...n.decls.entries()]:n.bag.declarations().map(e=>[n.bag.normalise(e.name),e])}function Me(n){return n.kind==="foreign"?n.norm:e=>n.bag.normalise(e)}function O(n){if(n.default!==void 0)return n.default;switch(n.type){case"boolean":return!1;case"number":return 0;case"string":return"";case"enum":return n.values?.[0]??"";case"flags":return[];case"quality":return n.stages?.[0]??"";default:return!1}}var ce=[{token:"patter",engine:"Patterplay",means:"Patter\'s shared globals"},{token:"story",engine:"Storylet Engine",means:"the Storylet Engine\'s shared @story properties"}];function B(n){return n.ctx.host??{}}var W={defaultScope:"patter",scopes:[{token:"patter"},{token:"scene"}],functions:{random:{minArgs:2,maxArgs:2,returnType:"number",eval(n,e){if(n.length!==2)throw new p("random(a, b) requires exactly 2 arguments");let t=B(e).nextRandom;if(!t)throw new p("random() called without a PRNG in context");let r=e.evaluate(n[0]),s=e.evaluate(n[1]);if(typeof r!="number"||typeof s!="number")throw new p("random(a, b) arguments must be numbers");if(!Number.isInteger(r)||!Number.isInteger(s))throw new p("random(a, b) arguments must be integers");let i=Math.min(r,s),o=Math.max(r,s);return Math.floor(t()*(o-i+1))+i}},check_flags:{minArgs:1,returnType:"boolean",flagDeltaArgs:!0,validate:pe("check_flags"),eval(n,e){let t=le(n[0],e,"check_flags");for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("check_flags() flag args must be +flagName or -flagName");if(s.sign==="+"?!t.includes(s.name):t.includes(s.name))return!1}return!0}},set_flags:{minArgs:1,returnType:"flags",flagDeltaArgs:!0,validate:pe("set_flags"),eval(n,e){let t=[...le(n[0],e,"set_flags")];for(let r=1;r<n.length;r++){let s=n[r];if(s.kind!=="flagdelta")throw new p("set_flags() flag args must be +flagName or -flagName");if(s.sign==="+")t.includes(s.name)||t.push(s.name);else{let i=t.indexOf(s.name);i>=0&&t.splice(i,1)}}return t}},visits:{minArgs:1,maxArgs:1,returnType:"number",validate:V("visits"),eval:(n,e)=>B(e).visits?.(G(n,e,"visits"))??0},seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:V("seen"),eval:(n,e)=>(B(e).visits?.(G(n,e,"seen"))??0)>0},patter_visits:{minArgs:1,maxArgs:1,returnType:"number",validate:V("patter_visits"),eval:(n,e)=>B(e).patterVisits?.(G(n,e,"patter_visits"))??0},patter_seen:{minArgs:1,maxArgs:1,returnType:"boolean",validate:V("patter_seen"),eval:(n,e)=>(B(e).patterVisits?.(G(n,e,"patter_seen"))??0)>0}}},ut=ce.map(n=>n.token).filter(n=>n!=="patter"&&n!=="scene");function de(n,e){let t=n.replace(/^@/,"").split(".");return t.length===2&&e(t[0])?{scope:t[0],name:t[1].toLowerCase()}:{scope:"patter",name:t.join(".").toLowerCase()}}function G(n,e,t){let r=e.evaluate(n[0]);if(typeof r!="string")throw new p(`${t}(id) requires a string node id`);return r}function V(n){return(e,t)=>{let r=e[0];r&&r.kind!=="string"&&t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(id): the argument must be a string id literal (a scene / block / node id)`})}}function le(n,e,t){if(!n)throw new p(`${t}() requires at least one argument (the flags variable)`);let r=e.evaluate(n);if(Array.isArray(r))return r;if(r===!1||r===null||r===void 0)return[];throw new p(`${t}() first argument must be a flags property`)}function pe(n){return(e,t)=>{if(e.length===0)return;let r=e[0];if(r.kind!=="scopedvar"){t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): first argument must be a flags property reference (@name or @scope.name)`});return}let s=t.schema.properties.get(r.scope)?.get(r.name);if(s&&s.type!=="flags"){let i=r.scope===t.defaultScope?r.name:`${r.scope}.${r.name}`;t.report({path:[...t.path,"args",0],kind:"wrong-arg-type",severity:"error",message:`${n}(): \'@${i}\' is not a flags property (got ${s.type})`});return}for(let i=1;i<e.length;i++){let o=e[i];o.kind!=="flagdelta"?t.report({path:[...t.path,"args",i],kind:"wrong-arg-type",severity:"error",message:`${n}(): argument ${i+1} must be +flagName or -flagName`}):s?.type==="flags"&&s.enumValues&&!s.enumValues.includes(o.name)&&t.report({path:[...t.path,"args",i],kind:"unknown-flag-name",severity:"error",message:`${n}(): unknown flag \'${o.name}\'`,reference:o.name})}}}var $e=/^@[A-Za-z0-9_.]+$/;function*je(n){let e="",t=0;for(;t<n.length;){let r=n[t];if(r==="{"&&n[t+1]==="{"){e+="{",t+=2;continue}if(r==="}"&&n[t+1]==="}"){e+="}",t+=2;continue}if(r==="{"){let s=n.indexOf("}",t+1);if(s!==-1){let i=n.slice(t,s+1),o=n.slice(t+1,s).trim();if(o.startsWith("@")){e&&(yield{kind:"text",value:e},e=""),yield{kind:"slot",raw:i,inner:o,ref:$e.test(o)?o:void 0},t=s+1;continue}e+=i,t=s+1;continue}}e+=r,t+=1}e&&(yield{kind:"text",value:e})}function qe(n){return Array.isArray(n)?n.join(", "):typeof n=="boolean"?n?"true":"false":String(n)}function _e(n){return n===" "||n===" "||n===`\n`||n==="\\r"||n==="\\f"||n==="\\v"}function He(n){let e="",t=!1;for(let r of n){if(_e(r)){t=!0;continue}t&&e.length>0&&(e+=" "),t=!1,e+=r}return e}function ge(n,e,t){if(e.length===0||n.indexOf(e)<0)return n;let r="",s=0,i=!1;for(;s<n.length;){if(n.startsWith(e,s)){let o=n.indexOf(t,s+e.length);if(o>=0){s=o+t.length,i=!0;continue}r+=n.slice(s);break}r+=n[s],s+=1}return i?He(r):n}function ue(n,e){if(n.indexOf("{")<0)return n;let t="";for(let r of je(n)){if(r.kind==="text"){t+=r.value;continue}if(!r.ref){t+=r.raw;continue}let s=e(r.ref);t+=s===void 0?"":qe(s)}return t}function C(n,e){for(let t of n){e(t);let r=t.children;r&&C(r,e)}}function Ue(n){return n.toLowerCase().replace(/[\'\u2019]/g,"").replace(/[^a-z0-9-]+/g,"-").replace(/-+/g,"-").replace(/^-+|-+$/g,"")}function m(n){let e=n.gameId?.trim();return e||Ue(n.name)}function K(n){return`cast:${n}`}var J={open:"[",close:"]"},he="SFX";var z=3;function L(n){let e=new Set,t=[];for(let r of n)e.has(r)||(e.add(r),t.push(r));return t}function M(n){let e=new Map,t=(r,s)=>{let i=L([...s,...r.tags??[]]);if(e.set(r.id,i),r.type==="group")for(let o of r.children)t(o,i);else for(let o of r.beats??[])e.set(o.id,L([...i,...o.tags??[]]))};for(let r of Object.values(n.scenes)){let s=L(r.tags??[]);e.set(r.id,s);for(let i of r.blocks){let o=L([...s,...i.tags??[]]);e.set(i.id,o);for(let a of i.children)t(a,o)}}return e}var fe=new WeakMap,I="Patter",P=n=>n.replace(/%/g,"%25").replace(/\\//g,"%2F"),me=Symbol("ownsRegistry"),v={stage:n=>`patter/scene/${P(n)}`,flow:n=>`patter/flow/${P(n)}/`,flowGlobals:n=>`patter/flow/${P(n)}/patter`,flowScene:(n,e)=>`patter/flow/${P(n)}/scene/${P(e)}`},$=class n{host;defaultSeed;flowsById=new Map;allStrings;currentLocale;sourceDebug;sceneGameIdToId=new Map;blockGameIdToId=new Map;creationOptions;engineLog=[];engineTraceHandlers=new Set;constructor(e,t={}){this.creationOptions=t;let r=t.locale??e.locales.default,s=e.strings;this.allStrings=s,this.currentLocale=r;let i=s[r]??{},o=s[e.locales.default]??{},a=e.localisation,c=a?.mode==="ids"&&!a.sourceDebug;this.sourceDebug=a?.mode==="ids"&&!!a.sourceDebug,this.sourceDebug&&typeof console<"u"&&console.warn("[Patterplay] source-only DEBUG build: strings are the source language for debugging, not a shippable localised build.");let l=new Map;for(let g of e.cast??[])g.displayName&&l.set(g.name,g.displayName);this.defaultSeed=(t.seed??2654435769)>>>0;let d=new Map,u=new Map,S=new Map;for(let[g,h]of Object.entries(e.scenes)){this.sceneGameIdToId.set(m(h),g);let y=new Map;for(let f of h.blocks)u.set(f.id,{sceneId:g}),S.set(f.id,f),y.set(m(f),f.id),C(f.children,ne=>d.set(ne.id,ne));this.blockGameIdToId.set(g,y)}let X=e.properties??[],j=X.filter(g=>g.shared??!0).map(ye),De=X.filter(g=>!(g.shared??!0)).map(ye),Re=new Set(j.map(g=>g.name.toLowerCase())),x=t.registry??new A,Z=!t.registry||t[me]===!0,ee=new b(j,{pathPrefix:"@patter."}),te=[],N=[];try{x.mountOwned("patter",ee,{owner:I}),N.push("patter");let g=new Set;if(t.world){let h=e.scopeRegistry?.scopes.find(f=>f.token==="world"),y=(h?.declarations??[]).map(xe);x.defineForeign("world",t.world,y,{writable:h?.writable??!0,owner:I}),N.push("world"),g.add("world")}for(let h of Z?e.scopeRegistry?.scopes??[]:[]){if(g.has(h.token)||x.has(h.token))continue;let y=(h.declarations??[]).map(f=>We(f,h.writable));x.defineOwned(h.token,y,{owner:I}),N.push(h.token),te.push(h.token)}}catch(g){for(let h of N)x.remove(h,{keep:!0});throw g}let re=new Map;for(let[g,h]of Object.entries(e.scenes)){let y=new Set((h.sceneProps??[]).filter(f=>f.shared??!1).map(f=>f.name.toLowerCase()));re.set(g,y)}this.host={logEnabled:t.log??!1,emitEngine:(g,h,y)=>this.emitEngine(g,h,y),bundle:e,emitIds:c,strings:i,defaultStrings:o,castDisplay:l,nodeIndex:d,blockIndex:u,blockById:S,sceneGameIdToId:this.sceneGameIdToId,blockGameIdToId:this.blockGameIdToId,tagIndex:M(e),registry:x,ownsRegistry:Z,patterBag:ee,hostScopes:te,patterSharedDecls:j,patterLocalDecls:De,patterSharedNames:Re,sceneSharedNames:re,sharedVisits:new Map,sharedSelectors:new Map,stageBags:new Map,customRng:t.rng,onDryChoice:t.onDryChoice,replayPromptOnChoose:t.replayPromptOnChoose??!1,captionsOn:t.closedCaptions??!0,captionOpen:(e.closedCaptions??J).open,captionClose:(e.closedCaptions??J).close,captionCharacter:e.closedCaptions?.character||he,refSplitCache:new Map,refSplitRevision:x.revision}}get locale(){return this.currentLocale}get isSourceDebug(){return this.sourceDebug}setLocale(e){this.currentLocale=e,this.host.strings=this.allStrings[e]??{}}replaceStrings(e){this.allStrings=e.strings,this.host.strings=this.allStrings[this.currentLocale]??{},this.host.defaultStrings=this.allStrings[this.host.bundle.locales.default]??{}}hotSwap(e){let t=this.saveGame(),r=o=>(o.setLocale(this.currentLocale),o.setClosedCaptions(this.host.captionsOn),o),s=()=>new n(e,{...this.creationOptions,registry:this.host.registry,[me]:this.host.ownsRegistry});this.release(!0);let i=s();try{return i.loadGame(t),r(i)}catch{i.release(!0);let o=s();for(let[a,c]of Object.entries(t.flows)){let l=c.cursor.currentSceneId;try{o.openFlow(a,l!==null?{scene:l}:{})}catch{}}return r(o)}}get closedCaptions(){return this.host.captionsOn}setClosedCaptions(e){this.host.captionsOn=e}openFlow(e,t={}){this.assertExternalScopes();let r=this.resolveSceneRef(t.scene),s=this.resolveBlockRef(r,t.block);this.flowsById.get(e)?.close();let i=new D(e,this.host,t.seed??this.defaultSeed);return this.flowsById.set(e,i),i.start(r,s),i}runFlow(e,t,r){let s=this.flowsById.get(e);if(!s)return this.openFlow(e,{scene:t,block:r}).advanceToStop().played;if(!s.goto(t,r))throw new Error(`runFlow: address not found: ${t}${r===void 0?"":` / ${r}`}`);return s.advanceToStop().played}assertExternalScopes(){for(let e of this.host.bundle.externalScopes??[])if(!this.host.registry.has(e))throw new Error(`this content names @${e}, which no engine on this registry registered: give every engine the game\'s one registry`)}resolveSceneRef(e){if(e!=null)return this.host.bundle.scenes[e]?e:this.sceneGameIdToId.get(e)??e}resolveBlockRef(e,t){if(t!=null){if(this.host.blockById.has(t))return t;if(e!=null){let r=this.blockGameIdToId.get(e)?.get(t);if(r)return r}return t}}sceneAddress(e){let t=this.host.bundle.scenes[e];return t?m(t):void 0}blockAddress(e){let t=this.host.blockById.get(e);return t?m(t):void 0}tagsForBeat(e){return this.host.tagIndex.get(e)??[]}tagsForScene(e){let t=this.resolveSceneRef(e);return(t!=null?this.host.tagIndex.get(t):void 0)??[]}tagsForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t);return(s!=null?this.host.tagIndex.get(s):void 0)??[]}getCast(){let e=[];for(let t of this.host.bundle.cast??[])t?.name&&e.push(t.name);return e}castForScene(e){let t=this.resolveSceneRef(e),r=t!=null?this.host.bundle.scenes[t]:void 0;if(!r)return[];let s=new Set;for(let i of r.blocks)Se(i.children,s);return[...s]}castForBlock(e,t){let r=this.resolveSceneRef(e),s=this.resolveBlockRef(r,t),i=s!=null?this.host.blockById.get(s):void 0;if(!i)return[];let o=new Set;return Se(i.children,o),[...o]}getOutline(){return Object.values(this.host.bundle.scenes).map(e=>({id:e.id,...m(e)?{gameId:m(e)}:{},name:e.name,...this.tagsField(e.id),blocks:e.blocks.map(t=>({id:t.id,...m(t)?{gameId:m(t)}:{},name:t.name,...this.tagsField(t.id),children:t.children.map(r=>this.outlineNode(r))}))}))}getBeatSequence(){let e=[];for(let t of Object.values(this.host.bundle.scenes))for(let r of t.blocks)C(r.children,s=>{if(s.type==="snippet")for(let i of s.beats??[])e.push({sceneId:t.id,blockId:r.id,snippetId:s.id,beat:this.beatInfo(i)})});return e}outlineNode(e){return e.type==="group"?{type:"group",id:e.id,...this.tagsField(e.id),...e.selector?{selector:e.selector}:{},...e.prompt?{prompt:this.beatInfo(e.prompt)}:{},children:e.children.map(t=>this.outlineNode(t))}:{type:"snippet",id:e.id,...this.tagsField(e.id),beats:(e.beats??[]).map(t=>this.beatInfo(t)),...e.jump?{jumpTo:e.jump.to,...e.jump.mode?{jumpMode:e.jump.mode}:{}}:{}}}beatInfo(e){let t=this.host.tagIndex.get(e.id),r={id:e.id,kind:e.kind};if(e.kind==="line"){if(e.character!==void 0){r.character=e.character;let s=this.host.defaultStrings[K(e.character)]??this.host.castDisplay.get(e.character);s!==void 0&&(r.characterName=s)}e.direction!==void 0&&(r.direction=e.direction)}if(e.kind==="line"||e.kind==="text"){let s=this.host.defaultStrings[e.id];s!==void 0&&(r.text=s)}return e.gameData&&Object.keys(e.gameData).length&&(r.gameData=e.gameData),t&&t.length&&(r.tags=t),r}tagsField(e){let t=this.host.tagIndex.get(e);return t&&t.length?{tags:t}:{}}getFlow(e){return this.flowsById.get(e)}flows(){return[...this.flowsById.values()]}listBags(){let e=[{bag:this.host.patterBag}];for(let[t,r]of this.host.stageBags)e.push({bag:r,pathPrefix:`@scene:${t}.`});return e}closeFlow(e){this.flowsById.get(e)?.close(),this.flowsById.delete(e)}reset(){for(let e of this.flowsById.values())e.close();this.flowsById.clear(),this.host.patterBag.reseed(this.host.patterSharedDecls),this.host.sharedVisits.clear(),this.host.sharedSelectors.clear();for(let e of this.host.stageBags.keys())this.host.registry.remove(v.stage(e));this.host.stageBags.clear(),this.host.registry.discardParked("patter/")}release(e){for(let r of this.flowsById.values())r.releaseBags(e),r.close();this.flowsById.clear();let t=this.host.registry;for(let r of this.host.stageBags.keys())t.remove(v.stage(r),{keep:e});this.host.stageBags.clear();for(let r of["patter",...this.host.hostScopes])t.has(r)&&t.remove(r,{keep:e});this.creationOptions.world&&t.has("world")&&t.remove("world")}getProperty(e){let{scope:t,name:r}=this.splitShared(e);return this.host.registry.get(t,r)}setProperty(e,t){let{scope:r,name:s}=this.splitShared(e);this.host.registry.set(r,s,t,{host:!0})}log(){return this.engineLog}clearLog(){this.engineLog.length=0}onTrace(e){return this.engineTraceHandlers.add(e),()=>this.engineTraceHandlers.delete(e)}emitEngine(e,t,r){for(let s of this.engineTraceHandlers)s(e,t);this.host.logEnabled&&this.engineLog.push({...t,flow:e,seq:this.engineLog.length,...r?{scene:r}:{}})}listProperties(){return this.host.patterSharedDecls.map(e=>({name:e.name,path:`@patter.${e.name}`,type:e.type,values:e.values,stages:e.stages,value:this.getProperty(`@${e.name}`),default:O(e),writable:e.writable??!0}))}splitShared(e){let t=Ee(this.host,e);if(t.scope==="scene")throw new Error(`\'${e}\': @scene properties are scene-scoped - read/write them on a Flow, not the Engine`);return t}save(){return{patter:this.host.patterBag.save()}}load(e){e.patter&&this.host.patterBag.load(e.patter)}saveGame(){let e={};for(let[t,r]of this.flowsById)e[t]=r.snapshot();return{version:z,...this.host.ownsRegistry?{registry:this.host.registry.save()}:{},sharedVisits:Object.fromEntries(this.host.sharedVisits),sharedSelectors:we(this.host.sharedSelectors),flows:e}}loadGame(e){let t=e.version;if(t!==2&&t!==z)throw new Error(`unsupported save version: ${String(t)}`);this.assertExternalScopes();let r=this.host.registry;for(let[i,o]of this.flowsById)o.releaseBags(i in e.flows),o.close();this.flowsById.clear();for(let i of this.host.stageBags.keys())r.remove(v.stage(i),{keep:!0});this.host.stageBags.clear();let s=e.version===2?Ke(e):e.registry;s&&(this.host.ownsRegistry?r.load(s):r.load(s,{keepParked:!0})),this.host.sharedVisits.clear();for(let[i,o]of Object.entries(e.sharedVisits??{}))this.host.sharedVisits.set(i,o);this.host.sharedSelectors.clear();for(let[i,o]of be(e.sharedSelectors))this.host.sharedSelectors.set(i,o);for(let[i,o]of Object.entries(e.flows)){let a=new D(i,this.host,this.defaultSeed);a.restore(o),this.flowsById.set(i,a)}}},D=class{id;host;local;registered=new Set;rngState;started=!1;flowEnded=!1;closed=!1;currentSceneId=null;stack=[];activeSnippet=null;beatIndex=0;pendingChoice=null;pendingPromptBeat=null;pendingPromptOwnerId=null;selectors=new Map;visitCounts=new Map;sceneBags=new Map;patterResolver={get:e=>this.host.patterSharedNames.has(e)?this.host.patterBag.get(e):this.local.get(e),set:(e,t)=>{this.host.patterSharedNames.has(e)?this.host.registry.set("patter",e,t):this.local.set(e,t)}};sceneResolver={get:e=>{let t=this.currentSceneId;if(t!==null)return this.sceneBagFor(t,e)?.get(e)},set:(e,t)=>{let r=this.currentSceneId;if(r===null)return;let s=this.sceneBagFor(r,e);s&&s.set(e,t)}};evalCtx;ctxRevision=-1;registryQualities;flowLog=[];flowSeq=0;constructor(e,t,r){this.id=e,this.host=t,this.rngState=R(r),this.local=this.newLocal(),this.evalCtx={scopes:{},host:{nextRandom:this.rng,visits:s=>this.visitCounts.get(s)??0,patterVisits:s=>this.host.sharedVisits.get(s)??0},qualities:(s,i)=>this.stagesFor(s,i)}}context(){let e=this.host.registry;if(e.revision!==this.ctxRevision){let t=e.toEvalContext(),r=this.evalCtx.scopes;for(let s of Object.keys(r))delete r[s];Object.assign(r,t.scopes),r.patter=this.patterResolver,r.scene=this.sceneResolver,this.registryQualities=t.qualities,this.ctxRevision=e.revision}return this.evalCtx}stagesFor(e,t){let r=t.toLowerCase(),s=i=>i?.find(o=>o.name.toLowerCase()===r&&o.type==="quality")?.stages;if(e==="patter")return s(this.host.patterSharedDecls)??s(this.host.patterLocalDecls);if(e==="scene"){let i=this.currentSceneId!=null?this.host.bundle.scenes[this.currentSceneId]:void 0;return s(i?.sceneProps)}return this.registryQualities?.(e,t)??s(this.host.bundle.scopeRegistry?.scopes.find(i=>i.token===e)?.declarations)}start(e,t){if(this.releaseBags(!1),this.host.registry.discardParked(v.flow(this.id)),this.mountLocal(),this.selectors.clear(),this.visitCounts.clear(),this.stack=[],this.currentSceneId=null,this.flowEnded=!1,this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.started=!0,t){let r=this.host.blockIndex.get(t);if(!r)throw new Error(`unknown block: ${t}`);this.enterSceneSetup(r.sceneId),this.stack=[{sceneId:r.sceneId,containerId:t,index:0}],this.enter(t)}else{let r=e??Object.keys(this.host.bundle.scenes)[0],s=r?this.host.bundle.scenes[r]:void 0;if(!s)throw new Error(r?`unknown scene: ${r}`:"no scenes in bundle");this.enterSceneSetup(r);let i=s.blocks[0];i&&(this.stack=[{sceneId:r,containerId:i.id,index:0}],this.enter(i.id))}this.settle()}reset(e,t){this.start(e,t)}goto(e,t){if(this.closed)return!1;if(e==="END")return this.started=!0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!0,this.stack=[],!0;let r=this.host.sceneGameIdToId.get(e)??(this.host.bundle.scenes[e]?e:void 0);if(r===void 0)return!1;let s;return t!==void 0&&(s=this.host.blockGameIdToId.get(r)?.get(t)??(this.host.blockIndex.get(t)?.sceneId===r?t:void 0),s===void 0)?!1:this.started?(this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.activeSnippet=null,this.beatIndex=0,this.flowEnded=!1,this.enterTarget(s??r,"jump"),this.settle(),!0):(this.start(r,s),!0)}close(){this.releaseBags(!1),this.closed=!0,this.flowEnded=!0,this.stack=[],this.activeSnippet=null,this.beatIndex=0,this.pendingChoice=null,this.pendingPromptBeat=null,this.pendingPromptOwnerId=null}get isClosed(){return this.closed}get currentScene(){return this.currentSceneId}advance(){if(this.closed)return{type:"end"};if(!this.started)throw new Error("flow has not been started");if(this.pendingPromptBeat){let e=this.pendingPromptBeat;return this.pendingPromptBeat=null,this.pendingPromptOwnerId=null,this.beatResult(e)}return this.settle(),this.flowEnded?{type:"end"}:this.pendingChoice?{type:"choice",groupId:this.pendingChoice.groupId,options:this.pendingChoice.options}:this.activeSnippet?this.beatResult(this.activeSnippet.beats[this.beatIndex++]):(this.flowEnded=!0,{type:"end"})}advanceToStop(){let e=[];for(;;){let t=this.advance();if(t.type==="choice"||t.type==="end")return{played:e,stop:t};e.push(t)}}settle(){let e=0;for(;;){if(++e>1e4)throw new Error("flow did not settle after 10000 transitions - likely a jump cycle with no deliverable content");if(this.flowEnded||this.pendingChoice)return;if(this.activeSnippet){if(this.beatIndex<(this.activeSnippet.beats?.length??0))return;this.runEffects(this.activeSnippet.onExit);let i=this.activeSnippet.jump;this.activeSnippet=null,this.beatIndex=0,this.resolveJump(i);continue}let t=this.stack[this.stack.length-1];if(!t){this.flowEnded=!0;return}t.sceneId!==this.currentSceneId&&(this.currentSceneId=t.sceneId);let r=this.childrenOf(t.containerId);if(!r){this.stack.pop();continue}let s=t.index;for(;t.index<r.length&&!this.eligible(r[t.index]);)t.index++;if(this.host.logEnabled&&t.index!==s&&this.emit({type:"select",group:t.containerId,selector:"run",children:r.slice(s,t.index+1).map((i,o)=>({id:i.id,eligible:s+o===t.index})),picked:r[t.index]?.id??null}),t.index>=r.length){this.stack.pop();continue}this.enterChild(r[t.index++])}}listBags(){let e=[{bag:this.local,pathPrefix:`${this.id}/@patter.`}];for(let[t,r]of this.sceneBags)e.push({bag:r,pathPrefix:`${this.id}/@scene:${t}.`});return e}log(){return this.flowLog}clearLog(){this.flowLog.length=0}emit(e){let t=this.currentSceneId??void 0;this.host.emitEngine(this.id,e,t),this.host.logEnabled&&this.flowLog.push({...e,seq:this.flowSeq++,...t?{scene:t}:{}})}getChoices(){return this.pendingChoice?.options??[]}choose(e){let t=this.pendingChoice;if(!t)throw new Error("no choice is pending");let r=t.options.find(i=>i.id===e);if(!r)throw new Error(`unknown choice option: ${e}`);if(!r.eligible)throw new Error(`choice option is not eligible: ${e}`);let s=t.byId.get(e);this.emit({type:"chose",group:t.groupId,option:e}),this.pendingChoice=null,this.pendingPromptBeat=this.host.replayPromptOnChoose?this.promptBeatOf(s)??null:null,this.pendingPromptOwnerId=this.pendingPromptBeat?s.id:null,this.enterChild(s)}isEnded(){return this.flowEnded}getProperty(e){let{scope:t,name:r}=this.splitRef(e);return t==="patter"?this.patterResolver.get(r):t==="scene"?this.sceneResolver.get(r):this.host.registry.get(t,r)}setProperty(e,t){this.writeProperty(e,t,!0)}writeProperty(e,t,r){let{scope:s,name:i}=this.splitRef(e);if(s==="patter")this.patterResolver.set(i,t);else if(s==="scene"){if(this.currentSceneId===null)throw new Error(`\'${e}\': the flow has not entered a scene yet`);this.sceneResolver.set(i,t)}else this.host.registry.set(s,i,t,r?{host:!0}:void 0)}snapshot(){return{rngState:this.rngState,visits:Object.fromEntries(this.visitCounts),cursor:{flowEnded:this.flowEnded,currentSceneId:this.currentSceneId,stack:this.stack.map(e=>{let t=this.childrenOf(e.containerId)?.[e.index];return t?{...e,nextId:t.id}:{...e}}),activeSnippetId:this.activeSnippet?.id??null,beatIndex:this.beatIndex,pendingChoice:this.pendingChoice?{groupId:this.pendingChoice.groupId,options:this.pendingChoice.options.map(e=>({...e}))}:null,pendingPromptOwnerId:this.pendingPromptOwnerId,selectors:we(this.selectors)}}}restore(e){this.rngState=R(e.rngState),this.visitCounts=new Map(Object.entries(e.visits??{}));let t=e.cursor;this.started=!0,this.flowEnded=t.flowEnded,this.beatIndex=t.beatIndex,this.currentSceneId=t.currentSceneId,this.stack=t.stack.map(r=>{let{nextId:s,...i}=r;if(s!==void 0){let o=this.childrenOf(i.containerId)?.findIndex(a=>a.id===s)??-1;if(o>=0)return{...i,index:o}}return{...i}}),this.releaseBags(!1),this.mountLocal();for(let r of new Set([t.currentSceneId,...this.stack.map(s=>s.sceneId)]))r!==null&&this.host.bundle.scenes[r]&&this.ensureSceneBags(r);if(this.activeSnippet=null,t.activeSnippetId!==null){let r=this.host.nodeIndex.get(t.activeSnippetId);r&&r.type==="snippet"&&(this.activeSnippet=r)}if(this.selectors=be(t.selectors),this.pendingChoice=null,t.pendingChoice!==null){let r=new Map,s=[];for(let i of t.pendingChoice.options){let o=this.host.nodeIndex.get(i.id);o&&(r.set(i.id,o),s.push({...i}))}s.length>0&&(this.pendingChoice={groupId:t.pendingChoice.groupId,options:s,byId:r})}if(this.pendingPromptBeat=null,this.pendingPromptOwnerId=t.pendingPromptOwnerId??null,this.pendingPromptOwnerId){let r=this.host.nodeIndex.get(this.pendingPromptOwnerId);this.pendingPromptBeat=r?this.promptBeatOf(r)??null:null,this.pendingPromptBeat||(this.pendingPromptOwnerId=null)}}enterSceneSetup(e){let t=this.host.bundle.scenes[e];if(!t)throw new Error(`unknown scene: ${e}`);this.currentSceneId=e,this.enter(e),this.seedScene(t),this.runEffects(t.onEntry)}enterChild(e){if(this.enter(e.id),e.type==="snippet"){this.beginSnippet(e);return}let t=e.selector??"run";if(t==="run"){this.stack.push({sceneId:this.currentSceneId,containerId:e.id,index:0});return}if(t==="choice"){this.setupChoice(e);return}let r=this.selectChild(e);r&&this.enterChild(r)}childrenOf(e){let t=this.host.blockById.get(e);if(t)return t.children;let r=this.host.nodeIndex.get(e);if(r&&r.type==="group")return r.children}beginSnippet(e){this.runEffects(e.onEnter),this.activeSnippet=e,this.beatIndex=0}setupChoice(e){let t=[],r=new Map,s=[];for(let o of e.children){if(o.fallback===!0){s.push(o);continue}if(o.sticky!==!0&&(this.visitCounts.get(o.id)??0)>=1)continue;let a=this.eligible(o),c=o.secretUntilEligible===!0;!a&&c||(t.push({id:o.id,prompt:this.promptFor(o),eligible:a,gameData:o.gameData}),r.set(o.id,o))}if(t.length>0){this.emit({type:"choice",group:e.id,options:t.map(o=>({id:o.id,eligible:o.eligible}))}),this.pendingChoice={groupId:e.id,options:t,byId:r};return}let i=s.find(o=>this.eligible(o));if(i){this.enterChild(i);return}this.emit({type:"dry",group:e.id}),this.host.onDryChoice?.(e.id)}resolveJump(e){e&&this.enterTarget(e.to,e.mode==="call"?"call":"jump")}enterTarget(e,t){if(this.emit({type:"jump",to:e,mode:t}),e==="END"){this.flowEnded=!0,this.stack=[];return}let r,s,i=this.host.bundle.scenes[e];if(i){this.enterSceneSetup(e);let a=i.blocks[0];if(!a){t==="jump"&&(this.stack=[]);return}r=e,s=a.id}else{let a=this.host.blockIndex.get(e);if(!a)throw new Error(`jump target not found: ${e}`);a.sceneId!==this.currentSceneId&&this.enterSceneSetup(a.sceneId),r=a.sceneId,s=e}this.enter(s);let o={sceneId:r,containerId:s,index:0};t==="call"?this.stack.push(o):this.stack=[o]}selectChild(e){let t=e.children.map(c=>({id:c.id,eligible:this.eligible(c)})),r=e.children.filter(c=>this.eligible(c)),s=e.options?.order??"sequential",i=e.options?.exhaust??"once",o=c=>(this.emit({type:"select",group:e.id,selector:e.selector??"default",...e.selector==="sequence"?{order:s,exhaust:i}:{},children:t,picked:c?.id??null}),c);if(r.length===0)return o(null);let a=this.selectorState(e);switch(e.selector){case"branch":return o(r[0]);case"sequence":return o(s==="shuffle"?this.pickShuffle(r,i,a):s==="specificity"?this.pickSpecificity(r,i,a):this.pickSequential(r,i,a));default:return null}}pickSequential(e,t,r){let s=e.length,i=r.seq??0;return r.seq=i+1,t==="repeat"?e[i%s]:i<s?e[i]:t==="stick"?e[s-1]:null}pickShuffle(e,t,r){let s=e.length,i=t==="stick",o=()=>(i?e.slice(0,s-1):e).map(u=>u.id);if(r.bag===void 0&&(r.bag=o()),r.bag.length===0){if(t==="once")return null;if(i){let u=e[s-1];return r.last=u.id,u}r.bag=o()}let a=r.bag,c=r.last!==void 0&&a.length>1?a.indexOf(r.last):-1,l=Math.floor(this.rng()*(c>=0?a.length-1:a.length));c>=0&&l>=c&&l++;let d=a[l];return a.splice(l,1),r.last=d,e.find(u=>u.id===d)}pickSpecificity(e,t,r){let s=e;if(t!=="repeat"){r.bag===void 0&&(r.bag=e.map(d=>d.id));let l=new Set(r.bag);if(s=e.filter(d=>l.has(d.id)),s.length===0)return t==="stick"&&r.last!==void 0?e.find(d=>d.id===r.last)??null:null}let i=-1,a=s.map(l=>{let d=this.specScore(l);return d>i&&(i=d),{c:l,s:d}}).filter(l=>l.s===i).map(l=>l.c),c;if(a.length===1)c=a[0];else{let l=r.last!==void 0?a.findIndex(u=>u.id===r.last):-1,d=Math.floor(this.rng()*(l>=0?a.length-1:a.length));l>=0&&d>=l&&d++,c=a[d]}return t!=="repeat"&&(r.bag=r.bag.filter(l=>l!==c.id)),r.last=c.id,c}specScore(e){return e.condition?this.matchedSpec(this.conditionAst(e.condition),!0):0}matchedSpec(e,t){return ie(e,s=>ve(F(s,this.context(),W)),{want:t})}selectorState(e){let t=e.shared?this.host.sharedSelectors:this.selectors,r=t.get(e.id);return r||(r={},t.set(e.id,r)),r}runEffects(e){for(let t of e??[]){let r=this.evalExpr(t.value),s=this.host.logEnabled?this.getProperty(t.target):void 0;this.writeProperty(t.target,r,!1),this.emit({type:"write",target:t.target,value:r,...s!==void 0?{prev:s}:{}})}}eligible(e){return e.condition?ve(this.evalExpr(e.condition)):!0}evalExpr(e){return F(this.conditionAst(e),this.context(),W)}conditionAst(e){let t=fe.get(e);return t||(t=E(e.ast),fe.set(e,t)),t}enter(e){this.visitCounts.set(e,(this.visitCounts.get(e)??0)+1),this.host.sharedVisits.set(e,(this.host.sharedVisits.get(e)??0)+1)}rng=()=>{if(this.host.customRng)return this.host.customRng();let e=H(this.rngState),t=e.next();return this.rngState=e.state(),t};beatResult(e){let t=this.host.tagIndex.get(e.id),r=t&&t.length?{tags:t}:{};switch(e.kind){case"gameEvent":return{type:"gameEvent",id:e.id,gameData:e.gameData,...r};case"text":return{type:"text",id:e.id,text:this.interpolate(this.resolveString(e.id)),gameData:e.gameData,...r};case"line":{let s=this.resolveString(e.id),i=!this.host.captionsOn,a=i&&e.character===this.host.captionCharacter?"":this.captionLine(this.host.bundle.voiced?s:this.interpolate(s)),c=i&&a.length===0;return{type:"line",id:e.id,text:a,character:c?void 0:e.character,characterName:c?void 0:this.resolveCharacterName(e.character),direction:c?void 0:e.direction,gameData:e.gameData,...r}}}}interpolate(e){return ue(e,t=>this.getProperty(t))}stripCaptions(e){return ge(e,this.host.captionOpen,this.host.captionClose)}captionLine(e){return this.host.captionsOn?e:this.stripCaptions(e)}promptFor(e){let t=this.promptBeatOf(e);if(!t)return;let r=this.interpolate(this.resolveString(t.id));return t.kind==="line"?{kind:"line",text:this.captionLine(r),character:t.character,characterName:this.resolveCharacterName(t.character),direction:t.direction}:{kind:"text",text:r}}promptBeatOf(e){return e.type==="group"&&e.prompt?e.prompt:((e.type==="snippet"?e:this.firstTextSnippetIn(e.children))?.beats??[]).find(r=>r.kind==="line"||r.kind==="text")}firstTextSnippetIn(e){let t;return C(e,r=>{!t&&r.type==="snippet"&&(r.beats??[]).some(s=>s.kind==="line"||s.kind==="text")&&(t=r)}),t}resolveString(e){if(this.host.emitIds)return e;let t=this.host.strings[e];if(t!==void 0)return t;let r=this.host.defaultStrings[e];return r!==void 0?`<Untranslated: ${e}> ${r}`:e}resolveCharacterName(e){if(e===void 0||this.host.emitIds)return;let t=K(e);return this.host.strings[t]??this.host.defaultStrings[t]??this.host.castDisplay.get(e)}splitRef(e){return Ee(this.host,e)}newLocal(){return new b(this.host.patterLocalDecls,{pathPrefix:"@patter."})}mountLocal(){this.local=this.newLocal();let e=v.flowGlobals(this.id);this.host.registry.mountOwned(e,this.local,{owner:I}),this.registered.add(e)}releaseBags(e){for(let t of this.registered)this.host.registry.remove(t,{keep:e});this.registered.clear(),this.sceneBags.clear()}sceneBagFor(e,t){if(this.host.bundle.scenes[e])return this.ensureSceneBags(e),this.host.sceneSharedNames.get(e)?.has(t.toLowerCase())?this.host.stageBags.get(e):this.sceneBags.get(e)}ensureSceneBags(e){let t=this.host.sceneSharedNames.get(e)??new Set,r=this.host.bundle.scenes[e]?.sceneProps??[];if(!this.sceneBags.has(e)){let s=new b(r.filter(o=>!t.has(o.name.toLowerCase())),{pathPrefix:"@scene."}),i=v.flowScene(this.id,e);this.host.registry.mountOwned(i,s,{owner:I}),this.registered.add(i),this.sceneBags.set(e,s)}if(!this.host.stageBags.has(e)){let s=new b(r.filter(i=>t.has(i.name.toLowerCase())),{pathPrefix:"@scene."});this.host.registry.mountOwned(v.stage(e),s,{owner:I}),this.host.stageBags.set(e,s)}}seedScene(e){let t=this.host.sceneSharedNames.get(e.id)??new Set;this.ensureSceneBags(e.id);for(let r of e.sceneProps??[]){if(!r.temporary)continue;let s=r.name.toLowerCase(),i=t.has(s)?this.host.stageBags.get(e.id):this.sceneBags.get(e.id);i&&i.set(s,O(r))}}};function Se(n,e){C(n,t=>{if(t.type==="group"){t.prompt?.kind==="line"&&t.prompt.character&&e.add(t.prompt.character);return}for(let r of t.beats??[])r.kind==="line"&&r.character&&e.add(r.character)})}function we(n){let e={};for(let[t,r]of n){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e[t]=s}return e}function be(n){let e=new Map;for(let[t,r]of Object.entries(n??{})){let s={};r.seq!==void 0&&(s.seq=r.seq),r.bag&&(s.bag=[...r.bag]),r.last!==void 0&&(s.last=r.last),e.set(t,s)}return e}function ye(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default}}function xe(n){return{name:n.name,type:n.type,values:n.values,stages:n.stages,default:n.default,writable:n.writable}}function We(n,e){let t=xe(n),r=n.writable??e;return r===void 0?t:{...t,writable:r}}function Ee(n,e){n.refSplitRevision!==n.registry.revision&&(n.refSplitCache.clear(),n.refSplitRevision=n.registry.revision);let t=n.refSplitCache.get(e);return t||(t=de(e,r=>r==="scene"||n.registry.has(r)||(n.bundle.externalScopes?.includes(r)??!1)),n.refSplitCache.set(e,t)),t}function Ke(n){let e={};n.shared?.patter&&(e.patter=n.shared.patter);for(let[t,r]of Object.entries(n.stageBags??{}))e[v.stage(t)]=r;for(let[t,r]of Object.entries(n.flows??{})){r.scopes?.patter&&(e[v.flowGlobals(t)]=r.scopes.patter);for(let[s,i]of Object.entries(r.sceneBags??{}))e[v.flowScene(t,s)]=i}return e}function ve(n){return typeof n=="boolean"?n:typeof n=="number"?n!==0:typeof n=="string"?n!=="":n.length>0}var Je=(n,e)=>n.shared??e;function Y(n,e){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.default!==void 0?{default:n.default}:{},shared:Je(n,e)}}function ze(n){return{name:n.name,type:n.type,hasDefault:n.default!==void 0,...n.values?{values:[...n.values]}:{},...n.purpose?{purpose:n.purpose}:{}}}function Ye(n,e){e.blocks++;let t=[...n.children];for(;t.length;){let r=t.pop();if(r.type==="group"){e.groups++,r.prompt&&e.prompts++,t.push(...r.children);continue}e.snippets++;for(let s of r.beats??[])e.beats++,s.kind==="gameEvent"&&e.gameEvents++}}function ke(n){let e={scenes:0,blocks:0,groups:0,snippets:0,beats:0,prompts:0,gameEvents:0,cast:n.cast?.length??0},t=[],r=[];for(let o of Object.values(n.scenes)){e.scenes++;let a=m(o);t.push({gameId:a,name:o.name,blocks:o.blocks.map(c=>({gameId:m(c),name:c.name}))});for(let c of o.blocks)Ye(c,e);o.sceneProps?.length&&r.push({gameId:a,properties:o.sceneProps.map(c=>Y(c,!1))})}let s=(n.scopeRegistry?.scopes??[]).map(o=>({token:o.token,writable:o.writable??!0,opaque:o.declarations===void 0,properties:(o.declarations??[]).map(a=>Y(a,!0))})),i=Object.entries(n.gameDataFields??{}).filter(([,o])=>(o?.length??0)>0).map(([o,a])=>({kind:o,fields:(a??[]).map(ze)}));return{identity:{schema:n.schema,project:n.content.project,...n.content.version!==void 0?{version:n.content.version}:{},...n.content.hash!==void 0?{hash:n.content.hash}:{},...n.content.structureHash!==void 0?{structureHash:n.content.structureHash}:{},voiced:n.voiced,defaultLocale:n.locales.default,locales:[...n.locales.included],localisation:n.localisation?.mode??"embedded",sourceDebug:n.localisation?.sourceDebug??!1},addresses:t,hostScopes:s,properties:{patter:(n.properties??[]).map(o=>Y(o,!0)),scene:r},gameData:i,counts:e}}function Ce(n,e){return n.gameDataFields?.[e]??[]}function Q(n,e,t){return e&&Object.prototype.hasOwnProperty.call(e,t)?e[t]:n.find(r=>r.name===t)?.default}function Ie(n,e){let t={};for(let r of n){let s=Q(n,e,r.name);s!==void 0&&(t[r.name]=s)}for(let[r,s]of Object.entries(e??{}))r in t||(t[r]=s);return t}return Te(Qe);})();\n';
@@ -1108,7 +1370,7 @@ function sourceStory(loaded) {
1108
1370
  const def = full.locales?.default ?? Object.keys(full.strings)[0] ?? "en";
1109
1371
  const bundle = { ...full, locales: { default: def, included: [def] }, strings: { [def]: full.strings[def] ?? {} } };
1110
1372
  const title = loaded.project.project.name?.trim() || "A Patter story";
1111
- const bundleJson = canonicalStringify2(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
1373
+ const bundleJson = canonicalStringify3(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
1112
1374
  return { title, lang: def, bundleJson };
1113
1375
  }
1114
1376
  function page(title, lang, head, tail) {
@@ -1180,8 +1442,8 @@ ${PLAYER_JS}`,
1180
1442
  }
1181
1443
 
1182
1444
  // src/audio-scan.ts
1183
- import { readdirSync as readdirSync2 } from "fs";
1184
- import { resolve as resolve2, extname, basename as basename4 } from "path";
1445
+ import { readdirSync as readdirSync3 } from "fs";
1446
+ import { resolve as resolve3, extname, basename as basename4 } from "path";
1185
1447
  import { deriveRecordingFolders, DEFAULT_RECORDING_STATUSES } from "@patterkit/model";
1186
1448
  var AUDIO_EXT = /* @__PURE__ */ new Set([".wav", ".mp3"]);
1187
1449
  function scanAudioStatus(loaded) {
@@ -1194,7 +1456,7 @@ function scanAudioStatus(loaded) {
1194
1456
  if (!rung.folder) continue;
1195
1457
  let files;
1196
1458
  try {
1197
- files = readdirSync2(resolve2(loaded.root, rung.folder));
1459
+ files = readdirSync3(resolve3(loaded.root, rung.folder));
1198
1460
  } catch {
1199
1461
  continue;
1200
1462
  }
@@ -1261,9 +1523,10 @@ function mergeAuthoring2(loaded) {
1261
1523
 
1262
1524
  // src/play.ts
1263
1525
  function runPlay(loaded, opts = {}) {
1264
- const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
1526
+ const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
1265
1527
  const events = [];
1266
- const engine = new Engine(bundle, { seed: opts.seed });
1528
+ const registry = previewRegistry(loaded.gameScopes, bundle);
1529
+ const engine = new Engine(bundle, { seed: opts.seed, ...registry ? { registry } : {} });
1267
1530
  const start = resolveStart(loaded, opts);
1268
1531
  const flow = engine.openFlow("main", { scene: start.scene, block: start.block });
1269
1532
  const scripted = [...opts.choices ?? []];
@@ -1474,12 +1737,13 @@ function blockedGates(beatId, analysis, drivenRefs, reachedRuns) {
1474
1737
  return out;
1475
1738
  }
1476
1739
  function proposeCoverageDrivers(loaded) {
1477
- const hostTokens = new Set((loaded.project.scopeRegistry?.scopes ?? []).map((s) => s.token));
1740
+ const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
1741
+ const hostScopes = bundle.scopeRegistry?.scopes ?? [];
1742
+ const hostTokens = new Set(hostScopes.map((s) => s.token));
1478
1743
  if (hostTokens.size === 0) return [];
1479
- const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
1480
1744
  const { written, proposals } = analyzeHostScopes(bundle, hostTokens);
1481
1745
  const declByRef = /* @__PURE__ */ new Map();
1482
- for (const s of loaded.project.scopeRegistry?.scopes ?? []) {
1746
+ for (const s of hostScopes) {
1483
1747
  for (const d of s.declarations ?? []) declByRef.set(`@${s.token}.${d.name}`, { type: d.type, values: d.values, stages: d.stages });
1484
1748
  }
1485
1749
  const drivers = [];
@@ -1537,9 +1801,9 @@ function* sweep(loaded, options = {}, hooks = {}) {
1537
1801
  const reachedRuns = new Map(order.map((id) => [id, 0]));
1538
1802
  const dryRuns = /* @__PURE__ */ new Map();
1539
1803
  const termination = { ended: 0, capped: 0, stalled: 0, evalError: 0 };
1540
- const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
1804
+ const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
1541
1805
  const rng = mulberry32(seed);
1542
- const hostTokens = new Set((loaded.project.scopeRegistry?.scopes ?? []).map((s) => s.token));
1806
+ const hostTokens = hostScopeTokens(loaded, bundle);
1543
1807
  const analysis = analyzeHostScopes(bundle, hostTokens);
1544
1808
  const drivers = (options.drivers ?? loaded.project.coverageDrivers ?? []).filter(
1545
1809
  (d) => d.values.length > 0 && hostTokens.has(d.ref.replace(/^@/, "").split(".")[0] ?? "")
@@ -1556,9 +1820,11 @@ function* sweep(loaded, options = {}, hooks = {}) {
1556
1820
  break;
1557
1821
  }
1558
1822
  const dryThisRun = /* @__PURE__ */ new Set();
1823
+ const registry = previewRegistry(loaded.gameScopes, bundle);
1559
1824
  const engine = new Engine2(bundle, {
1560
1825
  seed: Math.floor(rng() * 4294967296),
1561
- onDryChoice: (groupId) => dryThisRun.add(groupId)
1826
+ onDryChoice: (groupId) => dryThisRun.add(groupId),
1827
+ ...registry ? { registry } : {}
1562
1828
  });
1563
1829
  for (const d of initialDrivers) engine.setProperty(d.ref, pick(d.values));
1564
1830
  const flow = engine.openFlow("cov", { scene: start.scene, block: start.block });
@@ -1703,14 +1969,14 @@ function renderCoverageText(report, sceneName = (id) => id) {
1703
1969
  }
1704
1970
 
1705
1971
  // src/format.ts
1706
- import { readFileSync as readFileSync3 } from "fs";
1707
- import { canonicalStringify as canonicalStringify3, parseSource as parseSource3 } from "@patterkit/core";
1972
+ import { readFileSync as readFileSync4 } from "fs";
1973
+ import { canonicalStringify as canonicalStringify4, parseSource as parseSource3 } from "@patterkit/core";
1708
1974
  function runFormat(files) {
1709
1975
  return files.map((file) => {
1710
- const original = readFileSync3(file, "utf8");
1976
+ const original = readFileSync4(file, "utf8");
1711
1977
  let canonical;
1712
1978
  try {
1713
- canonical = canonicalStringify3(parseSource3(original));
1979
+ canonical = canonicalStringify4(parseSource3(original));
1714
1980
  } catch (e) {
1715
1981
  throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
1716
1982
  }
@@ -2094,8 +2360,8 @@ async function runReportXlsx(data) {
2094
2360
  }
2095
2361
 
2096
2362
  // src/localisation.ts
2097
- import { basename as basename5, join as join4, sep } from "path";
2098
- import { canonicalStringify as canonicalStringify4 } from "@patterkit/core";
2363
+ import { basename as basename5, join as join5, sep } from "path";
2364
+ import { canonicalStringify as canonicalStringify5 } from "@patterkit/core";
2099
2365
  import { walkNodes as walkNodes3, castStringKey, PROJECT_LOCALE_SCENE } from "@patterkit/model";
2100
2366
 
2101
2367
  // src/documentation.ts
@@ -2216,14 +2482,14 @@ function targetLocPath(loaded, scene, locale, defaultLocale) {
2216
2482
  }
2217
2483
  }
2218
2484
  const stem = scene === PROJECT_LOCALE_SCENE ? "_project" : scene;
2219
- return join4(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
2485
+ return join5(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
2220
2486
  }
2221
2487
  function authoringPath(loaded, scene) {
2222
- const dir = join4(loaded.root, layoutOf(loaded).authoring);
2223
- if (scene === PROJECT_LOCALE_SCENE) return join4(dir, "_project.patterx");
2488
+ const dir = join5(loaded.root, layoutOf(loaded).authoring);
2489
+ if (scene === PROJECT_LOCALE_SCENE) return join5(dir, "_project.patterx");
2224
2490
  const flow = loaded.sceneFiles[scene];
2225
2491
  const stem = flow ? basename5(flow).replace(/\.patterflow$/, "") : scene;
2226
- return join4(dir, `${stem}.patterx`);
2492
+ return join5(dir, `${stem}.patterx`);
2227
2493
  }
2228
2494
  var findFile = (files, items, path) => {
2229
2495
  const i = files.indexOf(path);
@@ -2253,7 +2519,7 @@ function applyLoc(loaded, catalog, opts = {}) {
2253
2519
  merged[id] = text;
2254
2520
  }
2255
2521
  const file = { schema: existing?.schema ?? "patter/strings@0", scene, locale, strings: merged };
2256
- writes.push({ path, content: canonicalStringify4(file) });
2522
+ writes.push({ path, content: canonicalStringify5(file) });
2257
2523
  const fresh = bySceneFresh.get(scene);
2258
2524
  if (!fresh?.size) continue;
2259
2525
  const aPath = authoringPath(loaded, scene);
@@ -2262,7 +2528,7 @@ function applyLoc(loaded, catalog, opts = {}) {
2262
2528
  const editsMap = { ...authoring.edits ?? {} };
2263
2529
  for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
2264
2530
  authoring.edits = editsMap;
2265
- writes.push({ path: aPath, content: canonicalStringify4(authoring) });
2531
+ writes.push({ path: aPath, content: canonicalStringify5(authoring) });
2266
2532
  }
2267
2533
  return { writes, stats: { updated, files: bySceneText.size } };
2268
2534
  }
@@ -2880,7 +3146,7 @@ var SERIF2 = (bold, italic) => bold && italic ? "Serif-BoldItalic" : bold ? "Ser
2880
3146
  var isEmoji = (cp) => cp >= 126976 || cp >= 9728 && cp <= 10175;
2881
3147
  async function scriptToPdf(doc) {
2882
3148
  const { scriptFont } = await import("./script-fonts-AQNQOADI.js");
2883
- return new Promise((resolve5, reject) => {
3149
+ return new Promise((resolve6, reject) => {
2884
3150
  const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
2885
3151
  pdf.registerFont("Serif", scriptFont("serif"));
2886
3152
  pdf.registerFont("Serif-Bold", scriptFont("serifBold"));
@@ -2921,7 +3187,7 @@ async function scriptToPdf(doc) {
2921
3187
  const bodyPieces = (runs, color, base = {}) => runs.flatMap((r) => r.code ? split(r.text, "Mono", ACCENT) : split(r.text, SERIF2(base.bold || r.bold, base.italic || r.italic), color));
2922
3188
  const chunks = [];
2923
3189
  pdf.on("data", (c) => chunks.push(c));
2924
- pdf.on("end", () => resolve5(Buffer.concat(chunks)));
3190
+ pdf.on("end", () => resolve6(Buffer.concat(chunks)));
2925
3191
  pdf.on("error", reject);
2926
3192
  const pageRight = pdf.page.width - MARGIN;
2927
3193
  const pageBottom = pdf.page.height - MARGIN;
@@ -3068,16 +3334,16 @@ async function scriptToPdf(doc) {
3068
3334
  }
3069
3335
 
3070
3336
  // src/init.ts
3071
- import { join as join5, basename as basename6, resolve as resolve3 } from "path";
3072
- import { readdirSync as readdirSync3, existsSync as existsSync2 } from "fs";
3073
- import { newId, slug, canonicalStringify as canonicalStringify5 } from "@patterkit/core";
3337
+ import { join as join6, basename as basename6, resolve as resolve4 } from "path";
3338
+ import { readdirSync as readdirSync4, existsSync as existsSync3 } from "fs";
3339
+ import { newId, slug, canonicalStringify as canonicalStringify6 } from "@patterkit/core";
3074
3340
  function runInit(opts) {
3075
- const dir = resolve3(opts.dir);
3341
+ const dir = resolve4(opts.dir);
3076
3342
  const name = opts.name?.trim() || basename6(dir).replace(/\.patter$/, "");
3077
3343
  const fileSlug = slug(name);
3078
3344
  let existing = [];
3079
3345
  try {
3080
- existing = readdirSync3(dir).filter((f) => f.endsWith(".patterproj"));
3346
+ existing = readdirSync4(dir).filter((f) => f.endsWith(".patterproj"));
3081
3347
  } catch {
3082
3348
  }
3083
3349
  if (existing.length > 0) throw new Error(`a project already exists here: ${existing[0]}`);
@@ -3121,25 +3387,25 @@ function runInit(opts) {
3121
3387
  strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
3122
3388
  };
3123
3389
  const bundle = opts.bundle ?? "commit";
3124
- const projectFile = join5(dir, `${fileSlug}.patterproj`);
3390
+ const projectFile = join6(dir, `${fileSlug}.patterproj`);
3125
3391
  const writes = [
3126
- { path: projectFile, content: canonicalStringify5(project) },
3127
- { path: join5(dir, "scenes", "start.patterflow"), content: canonicalStringify5(flow) },
3128
- { path: join5(dir, "loc", "en", "start.patterloc"), content: canonicalStringify5(locale) },
3129
- { path: join5(dir, ".editorconfig"), content: EDITORCONFIG },
3392
+ { path: projectFile, content: canonicalStringify6(project) },
3393
+ { path: join6(dir, "scenes", "start.patterflow"), content: canonicalStringify6(flow) },
3394
+ { path: join6(dir, "loc", "en", "start.patterloc"), content: canonicalStringify6(locale) },
3395
+ { path: join6(dir, ".editorconfig"), content: EDITORCONFIG },
3130
3396
  ...vcsConfigWrites(dir, opts.vcs, bundle)
3131
3397
  ];
3132
- const collisions = writes.map((w) => w.path).filter((p) => existsSync2(p));
3398
+ const collisions = writes.map((w) => w.path).filter((p) => existsSync3(p));
3133
3399
  if (collisions.length > 0) {
3134
3400
  throw new Error(`refusing to overwrite existing file(s): ${collisions.join(", ")}`);
3135
3401
  }
3136
3402
  return { writes, projectFile, name };
3137
3403
  }
3138
3404
  function vcsConfigWrites(dir, vcs, bundle = "commit") {
3139
- const writes = [{ path: join5(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
3140
- if (vcs === "git") writes.push({ path: join5(dir, ".gitattributes"), content: GITATTRIBUTES });
3405
+ const writes = [{ path: join6(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
3406
+ if (vcs === "git") writes.push({ path: join6(dir, ".gitattributes"), content: GITATTRIBUTES });
3141
3407
  const ignore = ignoreFileFor(vcs);
3142
- if (ignore) writes.push({ path: join5(dir, ignore), content: ignoreContent(bundle) });
3408
+ if (ignore) writes.push({ path: join6(dir, ignore), content: ignoreContent(bundle) });
3143
3409
  return writes;
3144
3410
  }
3145
3411
  var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
@@ -3561,7 +3827,7 @@ function buildIndex(loaded) {
3561
3827
  }
3562
3828
 
3563
3829
  // src/replace.ts
3564
- import { canonicalStringify as canonicalStringify6 } from "@patterkit/core";
3830
+ import { canonicalStringify as canonicalStringify7 } from "@patterkit/core";
3565
3831
  import { walkNodes as walkNodes5, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2 } from "@patterkit/model";
3566
3832
  import { findMatcher } from "@wildwinter/toolkit";
3567
3833
  function runReplace(loaded, opts) {
@@ -3603,7 +3869,7 @@ function runReplace(loaded, opts) {
3603
3869
  }
3604
3870
  if (changed) {
3605
3871
  const next = { ...shard, strings: merged };
3606
- plan.writes.push({ path, content: canonicalStringify6(next) });
3872
+ plan.writes.push({ path, content: canonicalStringify7(next) });
3607
3873
  plan.shards.push(next);
3608
3874
  plan.scenes++;
3609
3875
  }
@@ -3613,16 +3879,16 @@ function runReplace(loaded, opts) {
3613
3879
 
3614
3880
  // src/pack.ts
3615
3881
  import JSZip from "jszip";
3616
- import { readFileSync as readFileSync4 } from "fs";
3617
- import { dirname as dirname4, relative as relative2, sep as sep2 } from "path";
3882
+ import { readFileSync as readFileSync5 } from "fs";
3883
+ import { dirname as dirname5, relative as relative3, sep as sep2 } from "path";
3618
3884
  import { parseSource as parseSource4 } from "@patterkit/core";
3619
3885
  import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
3620
3886
  var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
3621
3887
  var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
3622
3888
  async function runPack(startPath) {
3623
3889
  const projectFile = findProjectFile(startPath);
3624
- const root = dirname4(projectFile);
3625
- const project = parseSource4(readFileSync4(projectFile, "utf8"));
3890
+ const root = dirname5(projectFile);
3891
+ const project = parseSource4(readFileSync5(projectFile, "utf8"));
3626
3892
  const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
3627
3893
  if (unresolved.length) {
3628
3894
  throw new Error(
@@ -3630,31 +3896,40 @@ async function runPack(startPath) {
3630
3896
  ` + unresolved.map((i) => ` ${i.file}`).join("\n")
3631
3897
  );
3632
3898
  }
3633
- const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative2(root, abs).split(sep2).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
3899
+ const files = SHARD_EXTENSIONS.flatMap((ext) => walkFiles(root, ext)).map((abs) => ({ abs, rel: relative3(root, abs).split(sep2).join("/") })).sort((a, b) => a.rel.localeCompare(b.rel));
3900
+ const scopes = gameScopesSnapshot(root, project);
3634
3901
  const manifest = {
3635
3902
  schema: "patter/document@0",
3636
3903
  project: { id: project.project.id, name: project.project.name },
3637
- files: files.map((f) => f.rel)
3904
+ files: files.map((f) => f.rel),
3905
+ ...scopes.length ? { gameScopes: scopes.map((f) => f.fileName) } : {}
3638
3906
  };
3639
3907
  const zip = new JSZip();
3640
3908
  zip.file("patter.manifest.json", JSON.stringify(manifest, null, 2) + "\n", ENTRY_OPTS);
3641
- for (const f of files) zip.file(f.rel, readFileSync4(f.abs, "utf8"), ENTRY_OPTS);
3909
+ for (const f of files) zip.file(f.rel, readFileSync5(f.abs, "utf8"), ENTRY_OPTS);
3910
+ for (const f of scopes) zip.file(`${GAME_SCOPES_DIR}/${f.fileName}`, f.text, ENTRY_OPTS);
3642
3911
  return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
3643
3912
  }
3644
3913
 
3645
3914
  // src/unpack.ts
3646
3915
  import JSZip2 from "jszip";
3647
- import { join as join6, resolve as resolve4, sep as sep3 } from "path";
3648
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
3649
- import { parseSource as parseSource5, canonicalStringify as canonicalStringify7 } from "@patterkit/core";
3916
+ import { join as join7, resolve as resolve5, sep as sep3 } from "path";
3917
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3918
+ import { parseSource as parseSource5, canonicalStringify as canonicalStringify8 } from "@patterkit/core";
3650
3919
  import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
3920
+ import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
3651
3921
  import { isUnsafeEntry as isUnsafeEntry2 } from "@wildwinter/toolkit/archive";
3652
3922
  var MANIFEST = "patter.manifest.json";
3923
+ var isScopesEntry = (name) => {
3924
+ const rest = name.startsWith(`${GAME_SCOPES_DIR}/`) ? name.slice(GAME_SCOPES_DIR.length + 1) : void 0;
3925
+ return rest !== void 0 && !rest.includes("/") && rest.endsWith(SCOPES_FILE_SUFFIX2);
3926
+ };
3653
3927
  var UnsafeEntryError = class extends Error {
3654
3928
  };
3655
3929
  async function readDoc(bytes) {
3656
3930
  const zip = await JSZip2.loadAsync(bytes);
3657
3931
  const shards = /* @__PURE__ */ new Map();
3932
+ const scopes = /* @__PURE__ */ new Map();
3658
3933
  let manifest;
3659
3934
  for (const [name, entry] of Object.entries(zip.files)) {
3660
3935
  if (entry.dir) continue;
@@ -3666,16 +3941,14 @@ async function readDoc(bytes) {
3666
3941
  continue;
3667
3942
  }
3668
3943
  if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
3669
- shards.set(name, await entry.async("string"));
3944
+ (isScopesEntry(name) ? scopes : shards).set(name, await entry.async("string"));
3670
3945
  }
3671
- return { shards, manifest };
3672
- }
3673
- async function readDocShards(bytes) {
3674
- return (await readDoc(bytes)).shards;
3946
+ return { shards, scopes, ...manifest ? { manifest } : {} };
3675
3947
  }
3676
3948
  async function runUnpack(bytes, targetDir) {
3677
- const shards = await readDocShards(bytes);
3678
- return [...shards.entries()].map(([name, content]) => ({ path: containedWrite(targetDir, name), content })).sort((a, b) => a.path.localeCompare(b.path));
3949
+ const doc = await readDoc(bytes);
3950
+ const plan = (entries) => [...entries.entries()].map(([name, content]) => ({ path: containedWrite(targetDir, name), content })).sort((a, b) => a.path.localeCompare(b.path));
3951
+ return { shards: plan(doc.shards), scopes: plan(doc.scopes) };
3679
3952
  }
3680
3953
  function checkProvenance(returned, base, target) {
3681
3954
  const known = [returned, base, target].filter((id) => typeof id === "string" && id !== "");
@@ -3688,7 +3961,7 @@ function checkProvenance(returned, base, target) {
3688
3961
  }
3689
3962
  function targetProjectId(projectDir) {
3690
3963
  try {
3691
- const pf = parseSource5(readFileSync5(findProjectFile(projectDir), "utf8"));
3964
+ const pf = parseSource5(readFileSync6(findProjectFile(projectDir), "utf8"));
3692
3965
  return pf.project?.id;
3693
3966
  } catch {
3694
3967
  return void 0;
@@ -3704,9 +3977,11 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
3704
3977
  const writes = [];
3705
3978
  const sidecars = [];
3706
3979
  let conflicts = 0, warnings = 0;
3980
+ const projectRel = projectEntry(theirs);
3981
+ let projectSides;
3707
3982
  for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
3708
3983
  const outPath = containedWrite(projectDir, rel);
3709
- if (!existsSync3(outPath)) {
3984
+ if (!existsSync4(outPath)) {
3710
3985
  writes.push({ path: outPath, content: theirText });
3711
3986
  shards.push({ path: rel, added: true });
3712
3987
  continue;
@@ -3718,12 +3993,15 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
3718
3993
  throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
3719
3994
  }
3720
3995
  };
3721
- const oursObj = readSide("ours", readFileSync5(outPath, "utf8"));
3996
+ const oursObj = readSide("ours", readFileSync6(outPath, "utf8"));
3722
3997
  const theirsObj = readSide("theirs", theirText);
3723
3998
  const baseText = base.get(rel);
3724
3999
  const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
3725
4000
  const result = runMerge(baseObj, oursObj, theirsObj);
3726
- writes.push({ path: outPath, content: canonicalStringify7(result.merged) });
4001
+ writes.push({ path: outPath, content: canonicalStringify8(result.merged) });
4002
+ if (rel === projectRel && baseText !== void 0) {
4003
+ projectSides = { ours: oursObj, base: baseObj, theirs: theirsObj, merged: result.merged };
4004
+ }
3727
4005
  if (result.conflicts.length > 0) {
3728
4006
  sidecars.push({ path: `${outPath}.patterconflict`, content: JSON.stringify({ type: result.type, conflicts: result.conflicts, warnings: result.warnings }, null, 2) + "\n" });
3729
4007
  conflicts += result.conflicts.length;
@@ -3731,11 +4009,24 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
3731
4009
  warnings += result.warnings.length;
3732
4010
  shards.push({ path: rel, result, added: false });
3733
4011
  }
3734
- return { shards, writes, sidecars, conflicts, warnings, provenance };
4012
+ let gameScopes;
4013
+ const local = projectSides && discoverGameScopes(resolve5(projectDir), projectSides.ours).gameScopes;
4014
+ if (projectSides && local) {
4015
+ const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local);
4016
+ if (world.write) {
4017
+ writes.push(world.write);
4018
+ gameScopes = { path: world.write.path };
4019
+ } else if (world.error) gameScopes = { path: join7(local.dir, GAME_SCOPES_FILE), error: world.error };
4020
+ }
4021
+ return { shards, writes, sidecars, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {} };
4022
+ }
4023
+ function projectEntry(shards) {
4024
+ const at = [...shards.keys()].filter((n) => !n.includes("/") && n.endsWith(".patterproj"));
4025
+ return at.length === 1 ? at[0] : void 0;
3735
4026
  }
3736
4027
  function containedWrite(dir, name) {
3737
- const root = resolve4(dir);
3738
- const full = resolve4(join6(dir, name));
4028
+ const root = resolve5(dir);
4029
+ const full = resolve5(join7(dir, name));
3739
4030
  if (full !== root && !full.startsWith(root + sep3)) {
3740
4031
  throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
3741
4032
  }
@@ -3744,6 +4035,11 @@ function containedWrite(dir, name) {
3744
4035
  export {
3745
4036
  AUTHORING_HANDLED,
3746
4037
  CONFLICT_SIDECAR,
4038
+ GAME_SCOPES_DIR,
4039
+ GAME_SCOPES_FILE,
4040
+ GAME_SCOPES_OWNER,
4041
+ PATTER_SCOPES_FILE,
4042
+ PATTER_SCOPES_OWNER,
3747
4043
  SHARD_EXTENSIONS,
3748
4044
  UnsafeEntryError,
3749
4045
  UnsupportedMergeError,
@@ -3755,17 +4051,32 @@ export {
3755
4051
  catalogToPo,
3756
4052
  catalogToXlsx,
3757
4053
  classesForChannel,
4054
+ defaultGameScopesDir,
3758
4055
  detectMergeType,
4056
+ discoverGameScopes,
4057
+ drivableScopes,
3759
4058
  extractLoc,
3760
4059
  findProjectFile,
4060
+ gameScopeTokens,
4061
+ gameScopesCatalogue,
4062
+ gameScopesSnapshot,
4063
+ hostScopeTokens,
3761
4064
  isUnsafeEntry2 as isUnsafeEntry,
3762
4065
  jsonToCatalog,
3763
4066
  listProjectTags,
3764
4067
  loadProject,
3765
4068
  loadProjectLanding,
4069
+ patterScopesFile,
4070
+ patterScopesStale,
4071
+ patterScopesWrite,
4072
+ planReturnedWorld,
4073
+ planShareScopes,
4074
+ planWorldSave,
3766
4075
  poToCatalog,
4076
+ previewRegistry,
3767
4077
  proposeCoverageDrivers,
3768
4078
  reachabilityIssues,
4079
+ readGameScopes,
3769
4080
  renderCoverageText,
3770
4081
  renderPlay,
3771
4082
  renderReportText,
@@ -3799,8 +4110,10 @@ export {
3799
4110
  sceneIdForShard,
3800
4111
  scriptToDocx,
3801
4112
  scriptToPdf,
4113
+ serialiseScopesFile,
3802
4114
  sidecarIssues,
3803
4115
  vcsConfigWrites,
3804
4116
  voiceScriptToXlsx,
4117
+ worldSettingsScopes,
3805
4118
  xlsxToCatalog
3806
4119
  };