@patterkit/ops 0.7.7 → 0.8.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 +185 -17
  2. package/dist/index.js +430 -122
  3. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -1,11 +1,252 @@
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 game = {
215
+ version: 1,
216
+ owner: GAME_SCOPES_OWNER,
217
+ scopes: (project.scopeRegistry?.scopes ?? []).filter((h) => isGameToken(h.token, void 0)).map((h) => ({
218
+ token: h.token,
219
+ ...h.writable !== void 0 ? { writable: h.writable } : {},
220
+ ...h.declarations ? { declarations: h.declarations.map((d) => ({ ...d })) } : {}
221
+ }))
222
+ };
223
+ const writes = [
224
+ { path: join(target, PATTER_SCOPES_FILE), content: serialiseScopesFile(patterScopesFile(project)) },
225
+ { path: join(target, GAME_SCOPES_FILE), content: serialiseScopesFile(game) }
226
+ ];
227
+ let found = false;
228
+ for (let at = resolve(root); ; ) {
229
+ if (join(at, GAME_SCOPES_DIR) === target) {
230
+ found = true;
231
+ break;
232
+ }
233
+ const up = dirname(at);
234
+ if (existsSync(join(at, ".git")) || up === at) break;
235
+ at = up;
236
+ }
237
+ const { gameScopes: _drop, ...rest } = project;
238
+ const next = found ? rest : { ...rest, gameScopes: relative(resolve(root), target) || "." };
239
+ return { writes, project: next };
240
+ }
241
+
242
+ // src/load.ts
243
+ function withGameScopes(root, project) {
244
+ const { gameScopes, missing } = discoverGameScopes(root, project);
245
+ return { ...gameScopes ? { gameScopes } : {}, ...missing ? { gameScopesMissing: missing } : {} };
246
+ }
6
247
  function readDirSafe(dir) {
7
248
  try {
8
- return readdirSync(dir, { withFileTypes: true });
249
+ return readdirSync2(dir, { withFileTypes: true });
9
250
  } catch {
10
251
  return [];
11
252
  }
@@ -14,26 +255,26 @@ function walkFiles(dir, ext) {
14
255
  const out = [];
15
256
  for (const e of readDirSafe(dir)) {
16
257
  if (e.name.startsWith(".")) continue;
17
- const p = join(dir, e.name);
258
+ const p = join2(dir, e.name);
18
259
  if (e.isDirectory()) out.push(...walkFiles(p, ext));
19
260
  else if (e.isFile() && e.name.endsWith(ext)) out.push(p);
20
261
  }
21
262
  return out.sort();
22
263
  }
23
264
  function findProjectFile(startPath) {
24
- let dir = resolve(startPath);
265
+ let dir = resolve2(startPath);
25
266
  try {
26
267
  if (statSync(dir).isFile()) {
27
268
  if (dir.endsWith(".patterproj")) return dir;
28
- dir = dirname(dir);
269
+ dir = dirname2(dir);
29
270
  }
30
271
  } catch {
31
272
  }
32
273
  for (; ; ) {
33
- const here = readDirSafe(dir).filter((e) => e.isFile() && e.name.endsWith(".patterproj")).map((e) => join(dir, e.name)).sort();
274
+ const here = readDirSafe(dir).filter((e) => e.isFile() && e.name.endsWith(".patterproj")).map((e) => join2(dir, e.name)).sort();
34
275
  if (here.length > 1) throw new Error(`multiple project files in ${dir}: ${here.map((p) => p.slice(dir.length + 1)).join(", ")}`);
35
276
  if (here.length === 1) return here[0];
36
- const parent = dirname(dir);
277
+ const parent = dirname2(dir);
37
278
  if (parent === dir) throw new Error(`no .patterproj found at or above ${startPath}`);
38
279
  dir = parent;
39
280
  }
@@ -41,7 +282,7 @@ function findProjectFile(startPath) {
41
282
  function parseFile(file, expectSchema, shapeKey) {
42
283
  let parsed;
43
284
  try {
44
- parsed = parseSource(readFileSync(file, "utf8"));
285
+ parsed = parseSource(readFileSync2(file, "utf8"));
45
286
  } catch (e) {
46
287
  throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
47
288
  }
@@ -97,16 +338,16 @@ function foldDeclaredNames(parsed, expectSchema) {
97
338
  }
98
339
  }
99
340
  function sceneIdForShard(shardPath) {
100
- const p = resolve(shardPath);
341
+ const p = resolve2(shardPath);
101
342
  try {
102
- if (p.endsWith(".patterflow")) return parseSource(readFileSync(p, "utf8")).scene?.id;
103
- if (p.endsWith(".patterloc")) return parseSource(readFileSync(p, "utf8")).scene;
343
+ if (p.endsWith(".patterflow")) return parseSource(readFileSync2(p, "utf8")).scene?.id;
344
+ if (p.endsWith(".patterloc")) return parseSource(readFileSync2(p, "utf8")).scene;
104
345
  if (p.endsWith(".patterx")) {
105
346
  const projectFile = findProjectFile(p);
106
347
  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;
348
+ const flowDir = join2(dirname2(projectFile), project.layout?.flow ?? "scenes/");
349
+ const flowPath = join2(flowDir, basename(p).replace(/\.patterx$/, ".patterflow"));
350
+ return parseSource(readFileSync2(flowPath, "utf8")).scene?.id;
110
351
  }
111
352
  } catch {
112
353
  }
@@ -114,13 +355,13 @@ function sceneIdForShard(shardPath) {
114
355
  }
115
356
  function loadProjectLanding(startPath, opts) {
116
357
  const projectFile = findProjectFile(startPath);
117
- const root = dirname(projectFile);
358
+ const root = dirname2(projectFile);
118
359
  const project = parseFile(projectFile, "patter/project", "project");
119
360
  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;
361
+ const flowFiles = walkFiles(join2(root, layout.flow), ".patterflow");
362
+ const launchPath = opts?.launchPath ? resolve2(opts.launchPath) : void 0;
122
363
  let landingFile;
123
- if (launchPath?.endsWith(".patterflow")) landingFile = flowFiles.find((f) => resolve(f) === launchPath);
364
+ if (launchPath?.endsWith(".patterflow")) landingFile = flowFiles.find((f) => resolve2(f) === launchPath);
124
365
  if (!landingFile) {
125
366
  const targetId = (opts?.launchPath ? sceneIdForShard(opts.launchPath) : void 0) ?? opts?.preferId;
126
367
  if (targetId) {
@@ -144,10 +385,10 @@ function loadProjectLanding(startPath, opts) {
144
385
  const localeFiles = [];
145
386
  if (landingScene && landingFile) {
146
387
  const defaultLocale = project.locales.default;
147
- const strings = join(root, layout.strings);
148
- const guess = join(strings, defaultLocale, `${basename(landingFile).replace(/\.patterflow$/, "")}.patterloc`);
388
+ const strings = join2(root, layout.strings);
389
+ const guess = join2(strings, defaultLocale, `${basename(landingFile).replace(/\.patterflow$/, "")}.patterloc`);
149
390
  let found = false;
150
- if (existsSync(guess)) {
391
+ if (existsSync2(guess)) {
151
392
  try {
152
393
  const loc = parseFile(guess, "patter/strings", "strings");
153
394
  if (loc.scene === landingScene.id) {
@@ -168,7 +409,7 @@ function loadProjectLanding(startPath, opts) {
168
409
  }
169
410
  }
170
411
  }
171
- return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [] };
412
+ return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring: [], authoringFiles: [], ...withGameScopes(root, project) };
172
413
  }
173
414
  function applySceneOrder(scenes, order) {
174
415
  if (!order?.length) return;
@@ -184,12 +425,12 @@ function applySceneOrder(scenes, order) {
184
425
  }
185
426
  function loadProject(startPath) {
186
427
  const projectFile = findProjectFile(startPath);
187
- const root = dirname(projectFile);
428
+ const root = dirname2(projectFile);
188
429
  const project = parseFile(projectFile, "patter/project", "project");
189
430
  const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...project.layout };
190
431
  const scenes = [];
191
432
  const sceneFiles = {};
192
- for (const f of walkFiles(join(root, layout.flow), ".patterflow")) {
433
+ for (const f of walkFiles(join2(root, layout.flow), ".patterflow")) {
193
434
  const scene = parseFile(f, "patter/flow", "scene").scene;
194
435
  const existing = sceneFiles[scene.id];
195
436
  if (existing !== void 0) {
@@ -201,38 +442,38 @@ function loadProject(startPath) {
201
442
  applySceneOrder(scenes, project.sceneOrder);
202
443
  const locales = [];
203
444
  const localeFiles = [];
204
- for (const f of walkFiles(join(root, layout.strings), ".patterloc")) {
445
+ for (const f of walkFiles(join2(root, layout.strings), ".patterloc")) {
205
446
  locales.push(parseFile(f, "patter/strings", "strings"));
206
447
  localeFiles.push(f);
207
448
  }
208
449
  const authoring = [];
209
450
  const authoringFiles = [];
210
- for (const f of walkFiles(join(root, layout.authoring), ".patterx")) {
451
+ for (const f of walkFiles(join2(root, layout.authoring), ".patterx")) {
211
452
  authoring.push(parseFile(f, "patter/authoring", "schema"));
212
453
  authoringFiles.push(f);
213
454
  }
214
- return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles };
455
+ return { root, projectFile, project, scenes, locales, sceneFiles, localeFiles, authoring, authoringFiles, ...withGameScopes(root, project) };
215
456
  }
216
457
 
217
458
  // src/write.ts
218
459
  import { writeFileSync, mkdirSync } from "fs";
219
- import { dirname as dirname2 } from "path";
460
+ import { dirname as dirname3 } from "path";
220
461
  function applyWrites(writes) {
221
462
  for (const w of writes) {
222
- mkdirSync(dirname2(w.path), { recursive: true });
463
+ mkdirSync(dirname3(w.path), { recursive: true });
223
464
  writeFileSync(w.path, w.content, "utf8");
224
465
  }
225
466
  }
226
467
 
227
468
  // 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";
469
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
470
+ import { basename as basename2, isAbsolute, join as join3, relative as relative2 } from "path";
230
471
 
231
472
  // src/merge.ts
232
- import { canonicalStringify } from "@patterkit/core";
473
+ import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
233
474
  var UnsupportedMergeError = class extends Error {
234
475
  };
235
- var eq = (a, b) => canonicalStringify(a) === canonicalStringify(b);
476
+ var eq = (a, b) => canonicalStringify2(a) === canonicalStringify2(b);
236
477
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
237
478
  var CONFLICT_SIDECAR = ".patterconflict";
238
479
  function sidecarIssues(sidecarPaths) {
@@ -273,7 +514,7 @@ function merge3(base, ours, theirs, id, path, conflicts) {
273
514
  conflicts.push({ id, path, base, ours, theirs, kind: deletedKind(base, ours, theirs) });
274
515
  return ours;
275
516
  }
276
- function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
517
+ function mergeMap(base, ours, theirs, prefix, conflicts, resolve6) {
277
518
  const out = {};
278
519
  for (const k of /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])) {
279
520
  const b = base[k], o = ours[k], t = theirs[k];
@@ -282,7 +523,7 @@ function mergeMap(base, ours, theirs, prefix, conflicts, resolve5) {
282
523
  else if (eq(b, o)) v = t;
283
524
  else if (eq(b, t)) v = o;
284
525
  else {
285
- const r = resolve5?.(k, b, o, t);
526
+ const r = resolve6?.(k, b, o, t);
286
527
  if (r && r.resolved) v = r.value;
287
528
  else {
288
529
  conflicts.push({ id: k, path: `${prefix}.${k}`, base: b, ours: o, theirs: t, kind: r ? r.kind : deletedKind(b, o, t) });
@@ -572,7 +813,7 @@ var arrEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
572
813
 
573
814
  // src/validate.ts
574
815
  import { validateProject, parseSource as parseSource2 } from "@patterkit/core";
575
- import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec } from "@patterkit/compiler";
816
+ import { validateConditions, validateInterpolation, exportBundle as exportBundle2, hostScopesToSpec, projectScopes } from "@patterkit/compiler";
576
817
 
577
818
  // src/reachability.ts
578
819
  import { exportBundle } from "@patterkit/compiler";
@@ -682,8 +923,8 @@ var termsOfAst = (ast, sceneId) => terms(ast, sceneId, keyOf);
682
923
  var requirementsOf = (expr, sceneId) => expr === void 0 ? [] : termsOfAst(expr.ast, sceneId);
683
924
  var asFlags = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
684
925
  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));
926
+ const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
927
+ const hostTokens = hostScopeTokens(loaded, bundle);
687
928
  const preset = /* @__PURE__ */ new Set();
688
929
  const notALatch = (decl, key) => {
689
930
  if (decl.temporary) {
@@ -826,14 +1067,17 @@ function reachabilityIssues(loaded, compiled) {
826
1067
  function runValidate(loaded) {
827
1068
  const { project, scenes, locales } = loaded;
828
1069
  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 });
1070
+ const merged = loaded.gameScopes?.merged;
1071
+ const scopes = projectScopes(project, merged);
1072
+ const foreignScopes = hostScopesToSpec(scopes.host);
1073
+ const conditions = validateConditions({ project, scenes }, { foreignScopes, gameScopes: merged });
1074
+ const interpolation = validateInterpolation({ project, scenes, locales }, { foreignScopes, gameScopes: merged });
1075
+ const gameScopes = gameScopesIssues(loaded, scopes.notes);
832
1076
  const hygiene = checkHygiene([loaded.projectFile, ...Object.values(loaded.sceneFiles), ...loaded.localeFiles, ...loaded.authoringFiles]);
833
1077
  const staleBundles = checkBundles(loaded);
834
1078
  const unresolvedMerges = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
835
1079
  const orphans = orphanShards(loaded);
836
- const reachability = structural.length === 0 && conditions.length === 0 ? reachabilityIssues(loaded) : [];
1080
+ const reachability = structural.length === 0 && !conditions.some(isError) ? reachabilityIssues(loaded) : [];
837
1081
  return {
838
1082
  structural,
839
1083
  conditions,
@@ -843,9 +1087,22 @@ function runValidate(loaded) {
843
1087
  staleBundles,
844
1088
  unresolvedMerges,
845
1089
  orphans,
846
- ok: structural.length === 0 && conditions.length === 0 && interpolation.length === 0 && hygiene.length === 0 && staleBundles.length === 0 && unresolvedMerges.length === 0 && orphans.length === 0
1090
+ gameScopes,
1091
+ 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
1092
  };
848
1093
  }
1094
+ var isError = (i) => i.severity === "error";
1095
+ function gameScopesIssues(loaded, notes) {
1096
+ const out = [];
1097
+ if (loaded.gameScopesMissing) out.push({ file: loaded.projectFile, severity: "error", message: loaded.gameScopesMissing });
1098
+ const gs = loaded.gameScopes;
1099
+ if (!gs) return out;
1100
+ for (const i of gs.issues) out.push({ file: i.file, severity: i.severity, message: i.message });
1101
+ for (const n of notes) out.push({ file: join3(gs.dir, n.file), severity: "warning", message: n.message });
1102
+ const stale = patterScopesStale(loaded.project, gs);
1103
+ if (stale) out.push({ ...stale, severity: "warning" });
1104
+ return out;
1105
+ }
849
1106
  function orphanShards(loaded) {
850
1107
  const layout = { flow: "scenes/", strings: "loc/", authoring: "authoring/", ...loaded.project.layout };
851
1108
  const kind = {
@@ -855,14 +1112,14 @@ function orphanShards(loaded) {
855
1112
  ".patterproj": { what: "project", home: null }
856
1113
  };
857
1114
  const inside = (file, dir) => {
858
- const r = relative(dir, file);
1115
+ const r = relative2(dir, file);
859
1116
  return r !== "" && !r.startsWith("..") && !isAbsolute(r);
860
1117
  };
861
1118
  const out = [];
862
1119
  for (const [ext, { what, home }] of Object.entries(kind)) {
863
1120
  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);
1121
+ if (home ? inside(file, join3(loaded.root, home)) : file === loaded.projectFile) continue;
1122
+ const rel = relative2(loaded.root, file);
866
1123
  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
1124
  out.push({ file, message });
868
1125
  }
@@ -875,14 +1132,14 @@ function checkBundles(loaded) {
875
1132
  if (bundles.length === 0) return issues;
876
1133
  let fresh;
877
1134
  try {
878
- fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales }).content.hash;
1135
+ fresh = exportBundle2({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged }).content.hash;
879
1136
  } catch {
880
1137
  return issues;
881
1138
  }
882
1139
  for (const file of bundles) {
883
1140
  let hash;
884
1141
  try {
885
- const parsed = parseSource2(readFileSync2(file, "utf8"));
1142
+ const parsed = parseSource2(readFileSync3(file, "utf8"));
886
1143
  hash = parsed?.content?.hash;
887
1144
  } catch {
888
1145
  issues.push({ file, message: "compiled bundle is unparseable - run `patter export`" });
@@ -911,7 +1168,7 @@ function checkHygiene(files) {
911
1168
  }
912
1169
  const fileIssues = [];
913
1170
  try {
914
- const text = readFileSync2(file, "utf8");
1171
+ const text = readFileSync3(file, "utf8");
915
1172
  if (text.charCodeAt(0) === 65279) {
916
1173
  fileIssues.push({ file, message: "file starts with a UTF-8 BOM (canonical form is UTF-8 without BOM - run `patter format`)" });
917
1174
  }
@@ -928,7 +1185,7 @@ function checkHygiene(files) {
928
1185
  }
929
1186
 
930
1187
  // src/export.ts
931
- import { basename as basename3, isAbsolute as isAbsolute2, join as join3 } from "path";
1188
+ import { basename as basename3, isAbsolute as isAbsolute2, join as join4 } from "path";
932
1189
  import { exportBundle as exportBundle3 } from "@patterkit/compiler";
933
1190
  import { isContentlessBeat } from "@patterkit/model";
934
1191
  function compileScenes(loaded) {
@@ -961,7 +1218,7 @@ function refuseUnresolvedMerge(loaded) {
961
1218
  function runExport(loaded) {
962
1219
  refuseUnresolvedMerge(loaded);
963
1220
  const { project, locales } = loaded;
964
- const full = exportBundle3({ project, scenes: compileScenes(loaded), locales });
1221
+ const full = exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
965
1222
  const loc = project.export?.localisation;
966
1223
  if (!loc || loc.mode === "embedded") return full;
967
1224
  const strings = loc.sourceDebug ? { [full.locales.default]: full.strings[full.locales.default] ?? {} } : {};
@@ -970,17 +1227,17 @@ function runExport(loaded) {
970
1227
  function runExportFull(loaded) {
971
1228
  refuseUnresolvedMerge(loaded);
972
1229
  const { project, locales } = loaded;
973
- return exportBundle3({ project, scenes: compileScenes(loaded), locales });
1230
+ return exportBundle3({ project, scenes: compileScenes(loaded), locales, gameScopes: loaded.gameScopes?.merged });
974
1231
  }
975
1232
  function bundleOutputPath(loaded) {
976
1233
  const override = loaded.project.export?.bundle;
977
- if (override) return isAbsolute2(override) ? override : join3(loaded.root, override);
1234
+ if (override) return isAbsolute2(override) ? override : join4(loaded.root, override);
978
1235
  const stem = basename3(loaded.projectFile).replace(/\.patterproj$/, "");
979
- return join3(loaded.root, "dist", `${stem}.patterc`);
1236
+ return join4(loaded.root, "dist", `${stem}.patterc`);
980
1237
  }
981
1238
 
982
1239
  // src/export-html.ts
983
- import { canonicalStringify as canonicalStringify2 } from "@patterkit/core";
1240
+ import { canonicalStringify as canonicalStringify3 } from "@patterkit/core";
984
1241
 
985
1242
  // src/playable-runtime.ts
986
1243
  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 +1365,7 @@ function sourceStory(loaded) {
1108
1365
  const def = full.locales?.default ?? Object.keys(full.strings)[0] ?? "en";
1109
1366
  const bundle = { ...full, locales: { default: def, included: [def] }, strings: { [def]: full.strings[def] ?? {} } };
1110
1367
  const title = loaded.project.project.name?.trim() || "A Patter story";
1111
- const bundleJson = canonicalStringify2(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
1368
+ const bundleJson = canonicalStringify3(bundle, { trailingComma: false }).replace(/</g, "\\u003c");
1112
1369
  return { title, lang: def, bundleJson };
1113
1370
  }
1114
1371
  function page(title, lang, head, tail) {
@@ -1180,8 +1437,8 @@ ${PLAYER_JS}`,
1180
1437
  }
1181
1438
 
1182
1439
  // src/audio-scan.ts
1183
- import { readdirSync as readdirSync2 } from "fs";
1184
- import { resolve as resolve2, extname, basename as basename4 } from "path";
1440
+ import { readdirSync as readdirSync3 } from "fs";
1441
+ import { resolve as resolve3, extname, basename as basename4 } from "path";
1185
1442
  import { deriveRecordingFolders, DEFAULT_RECORDING_STATUSES } from "@patterkit/model";
1186
1443
  var AUDIO_EXT = /* @__PURE__ */ new Set([".wav", ".mp3"]);
1187
1444
  function scanAudioStatus(loaded) {
@@ -1194,7 +1451,7 @@ function scanAudioStatus(loaded) {
1194
1451
  if (!rung.folder) continue;
1195
1452
  let files;
1196
1453
  try {
1197
- files = readdirSync2(resolve2(loaded.root, rung.folder));
1454
+ files = readdirSync3(resolve3(loaded.root, rung.folder));
1198
1455
  } catch {
1199
1456
  continue;
1200
1457
  }
@@ -1261,9 +1518,10 @@ function mergeAuthoring2(loaded) {
1261
1518
 
1262
1519
  // src/play.ts
1263
1520
  function runPlay(loaded, opts = {}) {
1264
- const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
1521
+ const bundle = exportBundle4({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
1265
1522
  const events = [];
1266
- const engine = new Engine(bundle, { seed: opts.seed });
1523
+ const registry = previewRegistry(loaded.gameScopes, bundle);
1524
+ const engine = new Engine(bundle, { seed: opts.seed, ...registry ? { registry } : {} });
1267
1525
  const start = resolveStart(loaded, opts);
1268
1526
  const flow = engine.openFlow("main", { scene: start.scene, block: start.block });
1269
1527
  const scripted = [...opts.choices ?? []];
@@ -1474,12 +1732,13 @@ function blockedGates(beatId, analysis, drivenRefs, reachedRuns) {
1474
1732
  return out;
1475
1733
  }
1476
1734
  function proposeCoverageDrivers(loaded) {
1477
- const hostTokens = new Set((loaded.project.scopeRegistry?.scopes ?? []).map((s) => s.token));
1735
+ const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
1736
+ const hostScopes = bundle.scopeRegistry?.scopes ?? [];
1737
+ const hostTokens = new Set(hostScopes.map((s) => s.token));
1478
1738
  if (hostTokens.size === 0) return [];
1479
- const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
1480
1739
  const { written, proposals } = analyzeHostScopes(bundle, hostTokens);
1481
1740
  const declByRef = /* @__PURE__ */ new Map();
1482
- for (const s of loaded.project.scopeRegistry?.scopes ?? []) {
1741
+ for (const s of hostScopes) {
1483
1742
  for (const d of s.declarations ?? []) declByRef.set(`@${s.token}.${d.name}`, { type: d.type, values: d.values, stages: d.stages });
1484
1743
  }
1485
1744
  const drivers = [];
@@ -1537,9 +1796,9 @@ function* sweep(loaded, options = {}, hooks = {}) {
1537
1796
  const reachedRuns = new Map(order.map((id) => [id, 0]));
1538
1797
  const dryRuns = /* @__PURE__ */ new Map();
1539
1798
  const termination = { ended: 0, capped: 0, stalled: 0, evalError: 0 };
1540
- const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
1799
+ const bundle = exportBundle5({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales, gameScopes: loaded.gameScopes?.merged });
1541
1800
  const rng = mulberry32(seed);
1542
- const hostTokens = new Set((loaded.project.scopeRegistry?.scopes ?? []).map((s) => s.token));
1801
+ const hostTokens = hostScopeTokens(loaded, bundle);
1543
1802
  const analysis = analyzeHostScopes(bundle, hostTokens);
1544
1803
  const drivers = (options.drivers ?? loaded.project.coverageDrivers ?? []).filter(
1545
1804
  (d) => d.values.length > 0 && hostTokens.has(d.ref.replace(/^@/, "").split(".")[0] ?? "")
@@ -1556,9 +1815,11 @@ function* sweep(loaded, options = {}, hooks = {}) {
1556
1815
  break;
1557
1816
  }
1558
1817
  const dryThisRun = /* @__PURE__ */ new Set();
1818
+ const registry = previewRegistry(loaded.gameScopes, bundle);
1559
1819
  const engine = new Engine2(bundle, {
1560
1820
  seed: Math.floor(rng() * 4294967296),
1561
- onDryChoice: (groupId) => dryThisRun.add(groupId)
1821
+ onDryChoice: (groupId) => dryThisRun.add(groupId),
1822
+ ...registry ? { registry } : {}
1562
1823
  });
1563
1824
  for (const d of initialDrivers) engine.setProperty(d.ref, pick(d.values));
1564
1825
  const flow = engine.openFlow("cov", { scene: start.scene, block: start.block });
@@ -1703,14 +1964,14 @@ function renderCoverageText(report, sceneName = (id) => id) {
1703
1964
  }
1704
1965
 
1705
1966
  // src/format.ts
1706
- import { readFileSync as readFileSync3 } from "fs";
1707
- import { canonicalStringify as canonicalStringify3, parseSource as parseSource3 } from "@patterkit/core";
1967
+ import { readFileSync as readFileSync4 } from "fs";
1968
+ import { canonicalStringify as canonicalStringify4, parseSource as parseSource3 } from "@patterkit/core";
1708
1969
  function runFormat(files) {
1709
1970
  return files.map((file) => {
1710
- const original = readFileSync3(file, "utf8");
1971
+ const original = readFileSync4(file, "utf8");
1711
1972
  let canonical;
1712
1973
  try {
1713
- canonical = canonicalStringify3(parseSource3(original));
1974
+ canonical = canonicalStringify4(parseSource3(original));
1714
1975
  } catch (e) {
1715
1976
  throw new Error(`${file}: ${e instanceof Error ? e.message : String(e)}`);
1716
1977
  }
@@ -2094,8 +2355,8 @@ async function runReportXlsx(data) {
2094
2355
  }
2095
2356
 
2096
2357
  // src/localisation.ts
2097
- import { basename as basename5, join as join4, sep } from "path";
2098
- import { canonicalStringify as canonicalStringify4 } from "@patterkit/core";
2358
+ import { basename as basename5, join as join5, sep } from "path";
2359
+ import { canonicalStringify as canonicalStringify5 } from "@patterkit/core";
2099
2360
  import { walkNodes as walkNodes3, castStringKey, PROJECT_LOCALE_SCENE } from "@patterkit/model";
2100
2361
 
2101
2362
  // src/documentation.ts
@@ -2216,14 +2477,14 @@ function targetLocPath(loaded, scene, locale, defaultLocale) {
2216
2477
  }
2217
2478
  }
2218
2479
  const stem = scene === PROJECT_LOCALE_SCENE ? "_project" : scene;
2219
- return join4(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
2480
+ return join5(loaded.root, layoutOf(loaded).strings, locale, `${stem}.patterloc`);
2220
2481
  }
2221
2482
  function authoringPath(loaded, scene) {
2222
- const dir = join4(loaded.root, layoutOf(loaded).authoring);
2223
- if (scene === PROJECT_LOCALE_SCENE) return join4(dir, "_project.patterx");
2483
+ const dir = join5(loaded.root, layoutOf(loaded).authoring);
2484
+ if (scene === PROJECT_LOCALE_SCENE) return join5(dir, "_project.patterx");
2224
2485
  const flow = loaded.sceneFiles[scene];
2225
2486
  const stem = flow ? basename5(flow).replace(/\.patterflow$/, "") : scene;
2226
- return join4(dir, `${stem}.patterx`);
2487
+ return join5(dir, `${stem}.patterx`);
2227
2488
  }
2228
2489
  var findFile = (files, items, path) => {
2229
2490
  const i = files.indexOf(path);
@@ -2253,7 +2514,7 @@ function applyLoc(loaded, catalog, opts = {}) {
2253
2514
  merged[id] = text;
2254
2515
  }
2255
2516
  const file = { schema: existing?.schema ?? "patter/strings@0", scene, locale, strings: merged };
2256
- writes.push({ path, content: canonicalStringify4(file) });
2517
+ writes.push({ path, content: canonicalStringify5(file) });
2257
2518
  const fresh = bySceneFresh.get(scene);
2258
2519
  if (!fresh?.size) continue;
2259
2520
  const aPath = authoringPath(loaded, scene);
@@ -2262,7 +2523,7 @@ function applyLoc(loaded, catalog, opts = {}) {
2262
2523
  const editsMap = { ...authoring.edits ?? {} };
2263
2524
  for (const id of fresh) editsMap[id] = { ...editsMap[id], localisedAt: { ...editsMap[id]?.localisedAt, [locale]: now } };
2264
2525
  authoring.edits = editsMap;
2265
- writes.push({ path: aPath, content: canonicalStringify4(authoring) });
2526
+ writes.push({ path: aPath, content: canonicalStringify5(authoring) });
2266
2527
  }
2267
2528
  return { writes, stats: { updated, files: bySceneText.size } };
2268
2529
  }
@@ -2880,7 +3141,7 @@ var SERIF2 = (bold, italic) => bold && italic ? "Serif-BoldItalic" : bold ? "Ser
2880
3141
  var isEmoji = (cp) => cp >= 126976 || cp >= 9728 && cp <= 10175;
2881
3142
  async function scriptToPdf(doc) {
2882
3143
  const { scriptFont } = await import("./script-fonts-AQNQOADI.js");
2883
- return new Promise((resolve5, reject) => {
3144
+ return new Promise((resolve6, reject) => {
2884
3145
  const pdf = new PDFDocument({ size: "A4", margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN }, info: { Title: doc.project, Author: "Patter" }, font: null });
2885
3146
  pdf.registerFont("Serif", scriptFont("serif"));
2886
3147
  pdf.registerFont("Serif-Bold", scriptFont("serifBold"));
@@ -2921,7 +3182,7 @@ async function scriptToPdf(doc) {
2921
3182
  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
3183
  const chunks = [];
2923
3184
  pdf.on("data", (c) => chunks.push(c));
2924
- pdf.on("end", () => resolve5(Buffer.concat(chunks)));
3185
+ pdf.on("end", () => resolve6(Buffer.concat(chunks)));
2925
3186
  pdf.on("error", reject);
2926
3187
  const pageRight = pdf.page.width - MARGIN;
2927
3188
  const pageBottom = pdf.page.height - MARGIN;
@@ -3068,16 +3329,16 @@ async function scriptToPdf(doc) {
3068
3329
  }
3069
3330
 
3070
3331
  // 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";
3332
+ import { join as join6, basename as basename6, resolve as resolve4 } from "path";
3333
+ import { readdirSync as readdirSync4, existsSync as existsSync3 } from "fs";
3334
+ import { newId, slug, canonicalStringify as canonicalStringify6 } from "@patterkit/core";
3074
3335
  function runInit(opts) {
3075
- const dir = resolve3(opts.dir);
3336
+ const dir = resolve4(opts.dir);
3076
3337
  const name = opts.name?.trim() || basename6(dir).replace(/\.patter$/, "");
3077
3338
  const fileSlug = slug(name);
3078
3339
  let existing = [];
3079
3340
  try {
3080
- existing = readdirSync3(dir).filter((f) => f.endsWith(".patterproj"));
3341
+ existing = readdirSync4(dir).filter((f) => f.endsWith(".patterproj"));
3081
3342
  } catch {
3082
3343
  }
3083
3344
  if (existing.length > 0) throw new Error(`a project already exists here: ${existing[0]}`);
@@ -3121,25 +3382,25 @@ function runInit(opts) {
3121
3382
  strings: { [beatId]: `Welcome to ${name}. This is the first line of your story - replace it with your own.` }
3122
3383
  };
3123
3384
  const bundle = opts.bundle ?? "commit";
3124
- const projectFile = join5(dir, `${fileSlug}.patterproj`);
3385
+ const projectFile = join6(dir, `${fileSlug}.patterproj`);
3125
3386
  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 },
3387
+ { path: projectFile, content: canonicalStringify6(project) },
3388
+ { path: join6(dir, "scenes", "start.patterflow"), content: canonicalStringify6(flow) },
3389
+ { path: join6(dir, "loc", "en", "start.patterloc"), content: canonicalStringify6(locale) },
3390
+ { path: join6(dir, ".editorconfig"), content: EDITORCONFIG },
3130
3391
  ...vcsConfigWrites(dir, opts.vcs, bundle)
3131
3392
  ];
3132
- const collisions = writes.map((w) => w.path).filter((p) => existsSync2(p));
3393
+ const collisions = writes.map((w) => w.path).filter((p) => existsSync3(p));
3133
3394
  if (collisions.length > 0) {
3134
3395
  throw new Error(`refusing to overwrite existing file(s): ${collisions.join(", ")}`);
3135
3396
  }
3136
3397
  return { writes, projectFile, name };
3137
3398
  }
3138
3399
  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 });
3400
+ const writes = [{ path: join6(dir, "vcs-setup.md"), content: vcsSetup(vcs, bundle) }];
3401
+ if (vcs === "git") writes.push({ path: join6(dir, ".gitattributes"), content: GITATTRIBUTES });
3141
3402
  const ignore = ignoreFileFor(vcs);
3142
- if (ignore) writes.push({ path: join5(dir, ignore), content: ignoreContent(bundle) });
3403
+ if (ignore) writes.push({ path: join6(dir, ignore), content: ignoreContent(bundle) });
3143
3404
  return writes;
3144
3405
  }
3145
3406
  var EDITORCONFIG = `# Patter projects are UTF-8 + LF, always (spec - patter format enforces this).
@@ -3561,7 +3822,7 @@ function buildIndex(loaded) {
3561
3822
  }
3562
3823
 
3563
3824
  // src/replace.ts
3564
- import { canonicalStringify as canonicalStringify6 } from "@patterkit/core";
3825
+ import { canonicalStringify as canonicalStringify7 } from "@patterkit/core";
3565
3826
  import { walkNodes as walkNodes5, PROJECT_LOCALE_SCENE as PROJECT_LOCALE_SCENE2 } from "@patterkit/model";
3566
3827
  import { findMatcher } from "@wildwinter/toolkit";
3567
3828
  function runReplace(loaded, opts) {
@@ -3603,7 +3864,7 @@ function runReplace(loaded, opts) {
3603
3864
  }
3604
3865
  if (changed) {
3605
3866
  const next = { ...shard, strings: merged };
3606
- plan.writes.push({ path, content: canonicalStringify6(next) });
3867
+ plan.writes.push({ path, content: canonicalStringify7(next) });
3607
3868
  plan.shards.push(next);
3608
3869
  plan.scenes++;
3609
3870
  }
@@ -3613,16 +3874,16 @@ function runReplace(loaded, opts) {
3613
3874
 
3614
3875
  // src/pack.ts
3615
3876
  import JSZip from "jszip";
3616
- import { readFileSync as readFileSync4 } from "fs";
3617
- import { dirname as dirname4, relative as relative2, sep as sep2 } from "path";
3877
+ import { readFileSync as readFileSync5 } from "fs";
3878
+ import { dirname as dirname5, relative as relative3, sep as sep2 } from "path";
3618
3879
  import { parseSource as parseSource4 } from "@patterkit/core";
3619
3880
  import { ARCHIVE_ENTRY_OPTS } from "@wildwinter/toolkit/archive";
3620
3881
  var SHARD_EXTENSIONS = [".patterflow", ".patterloc", ".patterx", ".patterproj"];
3621
3882
  var ENTRY_OPTS = ARCHIVE_ENTRY_OPTS;
3622
3883
  async function runPack(startPath) {
3623
3884
  const projectFile = findProjectFile(startPath);
3624
- const root = dirname4(projectFile);
3625
- const project = parseSource4(readFileSync4(projectFile, "utf8"));
3885
+ const root = dirname5(projectFile);
3886
+ const project = parseSource4(readFileSync5(projectFile, "utf8"));
3626
3887
  const unresolved = sidecarIssues(walkFiles(root, CONFLICT_SIDECAR));
3627
3888
  if (unresolved.length) {
3628
3889
  throw new Error(
@@ -3630,31 +3891,40 @@ async function runPack(startPath) {
3630
3891
  ` + unresolved.map((i) => ` ${i.file}`).join("\n")
3631
3892
  );
3632
3893
  }
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));
3894
+ 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));
3895
+ const scopes = gameScopesSnapshot(root, project);
3634
3896
  const manifest = {
3635
3897
  schema: "patter/document@0",
3636
3898
  project: { id: project.project.id, name: project.project.name },
3637
- files: files.map((f) => f.rel)
3899
+ files: files.map((f) => f.rel),
3900
+ ...scopes.length ? { gameScopes: scopes.map((f) => f.fileName) } : {}
3638
3901
  };
3639
3902
  const zip = new JSZip();
3640
3903
  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);
3904
+ for (const f of files) zip.file(f.rel, readFileSync5(f.abs, "utf8"), ENTRY_OPTS);
3905
+ for (const f of scopes) zip.file(`${GAME_SCOPES_DIR}/${f.fileName}`, f.text, ENTRY_OPTS);
3642
3906
  return zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", streamFiles: false });
3643
3907
  }
3644
3908
 
3645
3909
  // src/unpack.ts
3646
3910
  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";
3911
+ import { join as join7, resolve as resolve5, sep as sep3 } from "path";
3912
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
3913
+ import { parseSource as parseSource5, canonicalStringify as canonicalStringify8 } from "@patterkit/core";
3650
3914
  import { isUnsafeEntry } from "@wildwinter/toolkit/archive";
3915
+ import { SCOPES_FILE_SUFFIX as SCOPES_FILE_SUFFIX2 } from "@wildwinter/scoperegistry/scopes";
3651
3916
  import { isUnsafeEntry as isUnsafeEntry2 } from "@wildwinter/toolkit/archive";
3652
3917
  var MANIFEST = "patter.manifest.json";
3918
+ var isScopesEntry = (name) => {
3919
+ const rest = name.startsWith(`${GAME_SCOPES_DIR}/`) ? name.slice(GAME_SCOPES_DIR.length + 1) : void 0;
3920
+ return rest !== void 0 && !rest.includes("/") && rest.endsWith(SCOPES_FILE_SUFFIX2);
3921
+ };
3653
3922
  var UnsafeEntryError = class extends Error {
3654
3923
  };
3655
3924
  async function readDoc(bytes) {
3656
3925
  const zip = await JSZip2.loadAsync(bytes);
3657
3926
  const shards = /* @__PURE__ */ new Map();
3927
+ const scopes = /* @__PURE__ */ new Map();
3658
3928
  let manifest;
3659
3929
  for (const [name, entry] of Object.entries(zip.files)) {
3660
3930
  if (entry.dir) continue;
@@ -3666,16 +3936,14 @@ async function readDoc(bytes) {
3666
3936
  continue;
3667
3937
  }
3668
3938
  if (isUnsafeEntry(name)) throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
3669
- shards.set(name, await entry.async("string"));
3939
+ (isScopesEntry(name) ? scopes : shards).set(name, await entry.async("string"));
3670
3940
  }
3671
- return { shards, manifest };
3672
- }
3673
- async function readDocShards(bytes) {
3674
- return (await readDoc(bytes)).shards;
3941
+ return { shards, scopes, ...manifest ? { manifest } : {} };
3675
3942
  }
3676
3943
  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));
3944
+ const doc = await readDoc(bytes);
3945
+ const plan = (entries) => [...entries.entries()].map(([name, content]) => ({ path: containedWrite(targetDir, name), content })).sort((a, b) => a.path.localeCompare(b.path));
3946
+ return { shards: plan(doc.shards), scopes: plan(doc.scopes) };
3679
3947
  }
3680
3948
  function checkProvenance(returned, base, target) {
3681
3949
  const known = [returned, base, target].filter((id) => typeof id === "string" && id !== "");
@@ -3688,7 +3956,7 @@ function checkProvenance(returned, base, target) {
3688
3956
  }
3689
3957
  function targetProjectId(projectDir) {
3690
3958
  try {
3691
- const pf = parseSource5(readFileSync5(findProjectFile(projectDir), "utf8"));
3959
+ const pf = parseSource5(readFileSync6(findProjectFile(projectDir), "utf8"));
3692
3960
  return pf.project?.id;
3693
3961
  } catch {
3694
3962
  return void 0;
@@ -3704,9 +3972,11 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
3704
3972
  const writes = [];
3705
3973
  const sidecars = [];
3706
3974
  let conflicts = 0, warnings = 0;
3975
+ const projectRel = projectEntry(theirs);
3976
+ let projectSides;
3707
3977
  for (const [rel, theirText] of [...theirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
3708
3978
  const outPath = containedWrite(projectDir, rel);
3709
- if (!existsSync3(outPath)) {
3979
+ if (!existsSync4(outPath)) {
3710
3980
  writes.push({ path: outPath, content: theirText });
3711
3981
  shards.push({ path: rel, added: true });
3712
3982
  continue;
@@ -3718,12 +3988,15 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
3718
3988
  throw new Error(`${rel}: the ${side} copy is not readable Patter source - ${e instanceof Error ? e.message : String(e)}`);
3719
3989
  }
3720
3990
  };
3721
- const oursObj = readSide("ours", readFileSync5(outPath, "utf8"));
3991
+ const oursObj = readSide("ours", readFileSync6(outPath, "utf8"));
3722
3992
  const theirsObj = readSide("theirs", theirText);
3723
3993
  const baseText = base.get(rel);
3724
3994
  const baseObj = baseText !== void 0 ? readSide("base", baseText) : {};
3725
3995
  const result = runMerge(baseObj, oursObj, theirsObj);
3726
- writes.push({ path: outPath, content: canonicalStringify7(result.merged) });
3996
+ writes.push({ path: outPath, content: canonicalStringify8(result.merged) });
3997
+ if (rel === projectRel && baseText !== void 0) {
3998
+ projectSides = { ours: oursObj, base: baseObj, theirs: theirsObj, merged: result.merged };
3999
+ }
3727
4000
  if (result.conflicts.length > 0) {
3728
4001
  sidecars.push({ path: `${outPath}.patterconflict`, content: JSON.stringify({ type: result.type, conflicts: result.conflicts, warnings: result.warnings }, null, 2) + "\n" });
3729
4002
  conflicts += result.conflicts.length;
@@ -3731,11 +4004,24 @@ async function runUnpackMerge(returnedBytes, baseBytes, projectDir) {
3731
4004
  warnings += result.warnings.length;
3732
4005
  shards.push({ path: rel, result, added: false });
3733
4006
  }
3734
- return { shards, writes, sidecars, conflicts, warnings, provenance };
4007
+ let gameScopes;
4008
+ const local = projectSides && discoverGameScopes(resolve5(projectDir), projectSides.ours).gameScopes;
4009
+ if (projectSides && local) {
4010
+ const world = planReturnedWorld(projectSides.ours, projectSides.base, projectSides.theirs, projectSides.merged, local);
4011
+ if (world.write) {
4012
+ writes.push(world.write);
4013
+ gameScopes = { path: world.write.path };
4014
+ } else if (world.error) gameScopes = { path: join7(local.dir, GAME_SCOPES_FILE), error: world.error };
4015
+ }
4016
+ return { shards, writes, sidecars, conflicts, warnings, provenance, ...gameScopes ? { gameScopes } : {} };
4017
+ }
4018
+ function projectEntry(shards) {
4019
+ const at = [...shards.keys()].filter((n) => !n.includes("/") && n.endsWith(".patterproj"));
4020
+ return at.length === 1 ? at[0] : void 0;
3735
4021
  }
3736
4022
  function containedWrite(dir, name) {
3737
- const root = resolve4(dir);
3738
- const full = resolve4(join6(dir, name));
4023
+ const root = resolve5(dir);
4024
+ const full = resolve5(join7(dir, name));
3739
4025
  if (full !== root && !full.startsWith(root + sep3)) {
3740
4026
  throw new UnsafeEntryError(`document entry escapes the target directory: ${name}`);
3741
4027
  }
@@ -3744,6 +4030,11 @@ function containedWrite(dir, name) {
3744
4030
  export {
3745
4031
  AUTHORING_HANDLED,
3746
4032
  CONFLICT_SIDECAR,
4033
+ GAME_SCOPES_DIR,
4034
+ GAME_SCOPES_FILE,
4035
+ GAME_SCOPES_OWNER,
4036
+ PATTER_SCOPES_FILE,
4037
+ PATTER_SCOPES_OWNER,
3747
4038
  SHARD_EXTENSIONS,
3748
4039
  UnsafeEntryError,
3749
4040
  UnsupportedMergeError,
@@ -3755,17 +4046,32 @@ export {
3755
4046
  catalogToPo,
3756
4047
  catalogToXlsx,
3757
4048
  classesForChannel,
4049
+ defaultGameScopesDir,
3758
4050
  detectMergeType,
4051
+ discoverGameScopes,
4052
+ drivableScopes,
3759
4053
  extractLoc,
3760
4054
  findProjectFile,
4055
+ gameScopeTokens,
4056
+ gameScopesCatalogue,
4057
+ gameScopesSnapshot,
4058
+ hostScopeTokens,
3761
4059
  isUnsafeEntry2 as isUnsafeEntry,
3762
4060
  jsonToCatalog,
3763
4061
  listProjectTags,
3764
4062
  loadProject,
3765
4063
  loadProjectLanding,
4064
+ patterScopesFile,
4065
+ patterScopesStale,
4066
+ patterScopesWrite,
4067
+ planReturnedWorld,
4068
+ planShareScopes,
4069
+ planWorldSave,
3766
4070
  poToCatalog,
4071
+ previewRegistry,
3767
4072
  proposeCoverageDrivers,
3768
4073
  reachabilityIssues,
4074
+ readGameScopes,
3769
4075
  renderCoverageText,
3770
4076
  renderPlay,
3771
4077
  renderReportText,
@@ -3799,8 +4105,10 @@ export {
3799
4105
  sceneIdForShard,
3800
4106
  scriptToDocx,
3801
4107
  scriptToPdf,
4108
+ serialiseScopesFile,
3802
4109
  sidecarIssues,
3803
4110
  vcsConfigWrites,
3804
4111
  voiceScriptToXlsx,
4112
+ worldSettingsScopes,
3805
4113
  xlsxToCatalog
3806
4114
  };