@patterkit/model 0.1.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.
@@ -0,0 +1,782 @@
1
+ import { AstNode } from '@wildwinter/expr';
2
+
3
+ type ScalarValue = boolean | number | string | string[];
4
+ /** Developer-defined host metadata (spec §17). Opaque to Patter. */
5
+ type GameData = Record<string, unknown>;
6
+ type Effect = {
7
+ kind: "set";
8
+ target: string;
9
+ value: string;
10
+ };
11
+ /** A scene/block id, or the reserved "END". */
12
+ type JumpTarget = string;
13
+ interface Jump {
14
+ to: JumpTarget;
15
+ /** "jump" (one-way, default) or "call" (jump-and-return via the flow callstack). */
16
+ mode?: "jump" | "call";
17
+ }
18
+ interface LineBeat {
19
+ id: string;
20
+ kind: "line";
21
+ /** Speaker; must be a member of the project cast (validated). */
22
+ character?: string;
23
+ /** Performance direction, language-neutral (never localised). */
24
+ direction?: string;
25
+ gameData?: GameData;
26
+ /** Author-defined freeform tags (#215): a cross-cutting label layer that travels to the runtime.
27
+ * At runtime a beat's tags are the UNION of its own and every ancestor's (scene → block → group(s) →
28
+ * snippet → beat). Each tag is letters/digits/symbols with NO comma and NO whitespace; deduped. */
29
+ tags?: string[];
30
+ }
31
+ /**
32
+ * Authorial voice / narration - speaker-less prose (spec §2). Never voiced; its
33
+ * localised text always permits inline `{@name}` interpolation (spec §16). This
34
+ * is the on-screen-text role (e.g. "A door slams!") - distinct from a game-event
35
+ * beat, which is a pure engine instruction with no localised content.
36
+ */
37
+ interface TextBeat {
38
+ id: string;
39
+ kind: "text";
40
+ gameData?: GameData;
41
+ /** Author tags (#215). See LineBeat.tags. */
42
+ tags?: string[];
43
+ }
44
+ /**
45
+ * A GAME EVENT (spec §2): a pure engine instruction with no player-facing text - just `gameData` the host
46
+ * reads when the beat plays (comments / docs attach via the authoring file by id). Named "game event"
47
+ * rather than "action" because a screenplay's "action" is prose, which is our text beat. Player-facing
48
+ * words are a line or text beat instead.
49
+ */
50
+ interface GameEventBeat {
51
+ id: string;
52
+ kind: "gameEvent";
53
+ gameData?: GameData;
54
+ /** Author tags (#215). See LineBeat.tags. */
55
+ tags?: string[];
56
+ }
57
+ type Beat = LineBeat | TextBeat | GameEventBeat;
58
+ type Selector = "run" | "branch" | "sequence" | "choice";
59
+ /** How a `sequence` walks its children. */
60
+ type SelectorOrder = "sequential" | "shuffle";
61
+ /** What a `sequence` does after one full pass through its children. */
62
+ type SelectorExhaust = "once" | "repeat" | "stick";
63
+ /**
64
+ * `sequence` selector config (spec §4). One stateful picker with two orthogonal
65
+ * axes subsumes Ink's stopping / cycle / once / shuffle and their combinations.
66
+ * Defaults: `order: "sequential"`, `exhaust: "once"`. `shuffle` draws without
67
+ * replacement and never repeats a line back-to-back (built in).
68
+ */
69
+ interface SequenceOptions {
70
+ order?: SelectorOrder;
71
+ exhaust?: SelectorExhaust;
72
+ }
73
+ interface Snippet {
74
+ id: string;
75
+ type: "snippet";
76
+ /** Eligibility (and the conditional-jump test). Expression src. */
77
+ condition?: string;
78
+ /** Zero or more beats; zero beats + a jump = a pure jump (spec §3). */
79
+ beats?: Beat[];
80
+ onEnter?: Effect[];
81
+ onExit?: Effect[];
82
+ gameData?: GameData;
83
+ /** Author tags (#215). See LineBeat.tags. */
84
+ tags?: string[];
85
+ /** Optional routing action; fires after `beats`. */
86
+ jump?: Jump;
87
+ /** Omit this option entirely when its condition fails (default: show greyed). */
88
+ secretUntilEligible?: boolean;
89
+ /** Repeatable choice option (spec §5). `true`: always offered while its condition passes (Ink `+`).
90
+ * Default `false`: once-only - after the player follows it once it is gone from `getChoices`
91
+ * entirely (not delivered, not flagged unavailable - just absent; Ink `*`). */
92
+ sticky?: boolean;
93
+ /** The choice's fallback option (spec §5). Never delivered as a normal option; auto-followed the
94
+ * moment it is the ONLY eligible option left (its own condition still applies). At most one per
95
+ * choice. Default `false`. */
96
+ fallback?: boolean;
97
+ }
98
+ /** An option's prompt beat (spec §5): a single line | text beat - the choice text. */
99
+ type PromptBeat = LineBeat | TextBeat;
100
+ interface Group {
101
+ id: string;
102
+ type: "group";
103
+ condition?: string;
104
+ /** How the group's children are walked. Default (omitted) = `"run"`: play them in order. */
105
+ selector?: Selector;
106
+ /**
107
+ * For the memoried `sequence` selector (spec §7): is the selector's cursor
108
+ * SHARED across all flows (one cursor world-wide - e.g. two NPCs never draw the
109
+ * same shuffled line) or kept per-flow? Default `false` (per-flow). Orthogonal to
110
+ * a property's `shared` flag; same name, same idea.
111
+ */
112
+ shared?: boolean;
113
+ /** `sequence` config (order × exhaust, spec §4). */
114
+ options?: SequenceOptions;
115
+ children: Array<Group | Snippet>;
116
+ gameData?: GameData;
117
+ /** Author tags (#215). See LineBeat.tags. */
118
+ tags?: string[];
119
+ /** The choice text (spec §5): a single line | text beat, always present on an
120
+ * authored option. IS the host's choice text - no derivation, no look-ahead. */
121
+ prompt?: PromptBeat;
122
+ /** Keep this option out of `getChoices()` while ineligible (secrecy). Default false. */
123
+ secretUntilEligible?: boolean;
124
+ /** Repeatable choice option (spec §5). `true`: always offered while its condition passes (Ink `+`).
125
+ * Default `false`: once-only - after the player follows it once it is gone from `getChoices`
126
+ * entirely (not delivered, not flagged unavailable - just absent; Ink `*`). */
127
+ sticky?: boolean;
128
+ /** The choice's fallback option (spec §5). Never delivered as a normal option; auto-followed the
129
+ * moment it is the ONLY eligible option left (its own condition still applies). At most one per
130
+ * choice. Default `false`. */
131
+ fallback?: boolean;
132
+ }
133
+ interface Block {
134
+ id: string;
135
+ type: "block";
136
+ /** Mandatory, author-editable (jump targets must show a readable destination). */
137
+ name: string;
138
+ /**
139
+ * The author-editable, host-facing ADDRESS (spec §6) - a readable slug the runtime targets
140
+ * ("play this block"), distinct from the opaque immutable `id` (the internal join key). Absent =
141
+ * derived from `name` (`effectiveGameId`); present = author-pinned (survives renames). Unique
142
+ * within its scene (scene-scoped addressing). Hyphen-slug form (see core `gameIdify`).
143
+ */
144
+ gameId?: string;
145
+ children: Array<Group | Snippet>;
146
+ gameData?: GameData;
147
+ /** Author tags (#215). See LineBeat.tags. */
148
+ tags?: string[];
149
+ }
150
+ interface Scene {
151
+ id: string;
152
+ type: "scene";
153
+ name: string;
154
+ /**
155
+ * The author-editable, host-facing ADDRESS (spec §6) - a readable slug the runtime targets
156
+ * ("play this scene"), distinct from the opaque immutable `id`. Absent = derived from `name`
157
+ * (`effectiveGameId`); present = author-pinned. Unique project-wide. Hyphen-slug form (core `gameIdify`).
158
+ */
159
+ gameId?: string;
160
+ /** Host metadata - e.g. `location` lives here, not as a core field. */
161
+ gameData?: GameData;
162
+ /** Author tags (#215). See LineBeat.tags. */
163
+ tags?: string[];
164
+ /** Entry behaviour / setup (spec §15). */
165
+ onEntry?: Effect[];
166
+ /**
167
+ * Scene-scoped property declarations - `@scene` (spec §7). Each may be marked
168
+ * `shared` (one value across all flows in the scene) or not (per-flow, the
169
+ * default); the reference is `@scene.name` either way.
170
+ */
171
+ sceneProps?: PropertyDecl[];
172
+ /** One or more blocks; the first is the default entry. */
173
+ blocks: Block[];
174
+ }
175
+ /** A flow file (.patterflow) - one scene. */
176
+ interface FlowFile {
177
+ schema: string;
178
+ scene: Scene;
179
+ }
180
+ type FlowNode = Scene | Block | Group | Snippet;
181
+ /**
182
+ * Visit every selectable node (group / snippet) under a children list,
183
+ * depth-first in authored order. Generic over the source AND compiled trees
184
+ * (both share the group/snippet shape) - THE one tree walk; validators,
185
+ * indexers, and exporters all route through it so no hand-rolled walker can
186
+ * forget a level (or a string-bearing field on one).
187
+ */
188
+ declare function walkNodes<N extends {
189
+ type: string;
190
+ }>(nodes: ReadonlyArray<N>, visit: (node: N) => void): void;
191
+ /**
192
+ * A beat with NO content worth keeping: an empty line / text bubble left behind - e.g. a snippet seeded
193
+ * only so a jump could hang off it. Such a beat would render at runtime as a blank line that (lacking any
194
+ * localised string) falls back to emitting its raw id, so the editor drops it on save. `hasText` = the
195
+ * beat has a non-empty display string. A game-event beat is never contentless (it's a pure instruction),
196
+ * and a beat carrying `gameData` or `tags` is meaningful even with no text. A jump-only snippet is then
197
+ * just `{ jump }` with zero beats - which is valid.
198
+ */
199
+ declare function isContentlessBeat(beat: Beat, hasText: boolean): boolean;
200
+ /** Slugify a name into a hyphen-form game id: lowercase, drop apostrophes, runs of
201
+ * other punctuation -> a single hyphen, collapse repeats, no leading / trailing hyphen. */
202
+ declare function gameIdify(text: string): string;
203
+ /** A valid authored game id: lowercase alphanumerics + hyphens, no leading / trailing hyphen. */
204
+ declare function isValidGameId(gameId: string): boolean;
205
+ /** The effective address: the explicit `gameId` if pinned, else derived from `name`. */
206
+ declare function effectiveGameId(entity: {
207
+ gameId?: string;
208
+ name: string;
209
+ }): string;
210
+ type PropertyType = "boolean" | "number" | "string" | "flags" | "enum";
211
+ interface PropertyDecl {
212
+ name: string;
213
+ type: PropertyType;
214
+ default?: ScalarValue;
215
+ /**
216
+ * The orthogonal *sharing* axis (spec §7): is this property's value shared across
217
+ * all flows (one world value) or kept per-flow? It does NOT change the reference
218
+ * syntax - sharing is set here, on the declaration, not by a different scope token.
219
+ * The default depends on the scope it is declared in: a **global** property
220
+ * (project `properties` -> `@patter`) defaults to **shared**; a **scene-local**
221
+ * property (scene `sceneProps` -> `@scene`) defaults to **not shared** (per-flow).
222
+ */
223
+ shared?: boolean;
224
+ /**
225
+ * Persistence axis, for **scene-local (`@scene`) properties** (spec §7). Default
226
+ * `false`: the value PERSISTS across scene re-entries (like every other property).
227
+ * `true`: the value is **reseeded to its default on every scene entry** - "fresh
228
+ * each playthrough" (Ink's `temp`). Orthogonal to `shared`. Ignored on global
229
+ * (`@patter`) properties, which always persist for the life of the piece.
230
+ */
231
+ temporary?: boolean;
232
+ /** For enum / flags. */
233
+ values?: string[];
234
+ /** Free-text author note documenting what this property is for (authoring only; shown as a hint). */
235
+ purpose?: string;
236
+ }
237
+ /**
238
+ * A property of a host / world scope (`@world`, `@game`, ...). The same shape the
239
+ * `@wildwinter/scoperegistry` `scopeRegistrySpec` uses, declared structurally here
240
+ * so the model stays free of a runtime dependency. `default` seeds the standalone
241
+ * runtime's self-backed bag (see `HostScopeSpec`); `writable: false` makes the
242
+ * property read-only to the story (validated at compile time).
243
+ */
244
+ interface HostScopeDecl {
245
+ name: string;
246
+ type: PropertyType;
247
+ values?: string[];
248
+ default?: ScalarValue;
249
+ writable?: boolean;
250
+ /** Free-text author note documenting the property (authoring only; shown as a hint). */
251
+ purpose?: string;
252
+ }
253
+ /** One scope (`token`) in a project's host-scope registry. */
254
+ interface HostScopeSpec {
255
+ /** The scope token after `@` (e.g. `"world"`). Must not collide with Patter's own
256
+ * `patter` / `scene` / `flow`. */
257
+ token: string;
258
+ /** Scope-level read/write default for its declarations (default true). */
259
+ writable?: boolean;
260
+ /** Property declarations; omit for an opaque scope (any name, unchecked). */
261
+ declarations?: HostScopeDecl[];
262
+ }
263
+ /**
264
+ * A project's host-scope registry: the `scopeRegistrySpec` it OWNS (spec
265
+ * design/scope-registry.md §6). Makes `@world` (and any host scope) first-class
266
+ * in a standalone project: the compiler validates references into it, the runtime
267
+ * self-backs it from declaration defaults when no host resolver claims the token,
268
+ * and coverage drives its values. Structurally identical to scoperegistry's
269
+ * `ScopeRegistrySpec` so it threads straight through the compiler.
270
+ */
271
+ interface HostScopeRegistry {
272
+ version: number;
273
+ scopes: HostScopeSpec[];
274
+ }
275
+ /**
276
+ * A coverage input driver (#159, design/scope-registry.md §7): during a coverage run the harness feeds a
277
+ * host-scope property (`@world.x`) values from `values` so branches gated on external state get exercised.
278
+ * @world is the game-engine seam: @patter / @scene are story-owned and covered for free, so are never driven.
279
+ */
280
+ interface CoverageDriver {
281
+ /** The host-scope ref to drive, e.g. `"@world.phase"`. */
282
+ ref: string;
283
+ /** `"initial"`: set once when each playthrough starts. `"recurring"`: re-rolled at choice points so a
284
+ * single run can pass through several world states. */
285
+ kind: "initial" | "recurring";
286
+ /** For a recurring driver: how often to re-roll at a choice point (default `"sometimes"`). */
287
+ cadence?: "rarely" | "sometimes" | "often";
288
+ /** The pool the harness picks from (uniform). Empty = the driver is inert. */
289
+ values: ScalarValue[];
290
+ }
291
+ interface CastMember {
292
+ /** Canonical speaker name (matched by a beat's `character`); language-neutral key. */
293
+ name: string;
294
+ /** Localised player-facing name - a localisation id (project-level strings). */
295
+ displayName?: string;
296
+ /** Free-text production notes about the character (casting, voice, intent) - authoring only. */
297
+ notes?: string;
298
+ /** The voice actor cast for this character, if known - surfaced in the VO script export (spec §16).
299
+ * Authoring-only (not shipped in the runtime bundle). */
300
+ actor?: string;
301
+ gameData?: GameData;
302
+ }
303
+ /** A node TYPE that can carry author-defined gameData fields (the gameData schema, project-level).
304
+ * Beat kinds map straight through: dialogue = `line`, narration = `text`, game event = `gameEvent`. */
305
+ type GameDataNodeKind = "scene" | "block" | "snippet" | "line" | "text" | "gameEvent";
306
+ /** The value type of a gameData field - the property-type vocabulary plus a multiline-text variant.
307
+ * Drives the inspector's editor widget (text / textarea / number / toggle / enum dropdown). */
308
+ type GameDataFieldType = "text" | "multiline" | "number" | "boolean" | "enum";
309
+ /** One author-defined custom field on a node type (host-integration metadata, NOT expression state). */
310
+ interface GameDataField {
311
+ /** Field key - also the key a node stores its value under in `gameData` (values are name-keyed). */
312
+ name: string;
313
+ type: GameDataFieldType;
314
+ /** The value used when a node sets nothing. Storage is SPARSE - nodes hold only their overrides, and
315
+ * a reader falls back to this default (so changing it here propagates to every node that didn't set it). */
316
+ default?: ScalarValue;
317
+ /** Allowed values when `type` is "enum". */
318
+ values?: string[];
319
+ /** Free-text description of what the field is for - shown as a rollover hint in the inspector. */
320
+ purpose?: string;
321
+ }
322
+ /** Author-defined gameData fields, grouped by the node type they attach to (project-level schema). */
323
+ type GameDataFields = Partial<Record<GameDataNodeKind, GameDataField[]>>;
324
+ interface WritingStatusDecl {
325
+ name: string;
326
+ /**
327
+ * Threshold marker: this status AND every later one classify as "ready to
328
+ * record". Declared on exactly one status in the list.
329
+ */
330
+ readyToRecord?: boolean;
331
+ /** Threshold marker: this status and every later one classify as "ready to ship". */
332
+ readyToShip?: boolean;
333
+ /** Theme character-palette slot (0-11) for the per-line status badge / inspector swatch (Patterpad #196).
334
+ * A slot (not a hex) so it adapts to light/dark + the reading palettes. Absent = no colour (neutral). */
335
+ colour?: number;
336
+ }
337
+ /**
338
+ * The default writing-status ladder (used when a project declares none). The
339
+ * FIRST status is the level-0 / absence status: a beat with no recorded status
340
+ * counts as `stub`. Nothing is inferred from content - tracking is opt-in, and a
341
+ * project that ignores statuses reads as all-stub (so its burndown / script
342
+ * export carry no weight, by design - spec §13).
343
+ */
344
+ declare const DEFAULT_WRITING_STATUSES: WritingStatusDecl[];
345
+ /** A recording-status rung (#206): like a writing status but with no readiness markers - the recording
346
+ * ladder is ordered missing -> final and carries a picked palette colour for the inspector chip / report. */
347
+ interface RecordingStatusDecl {
348
+ name: string;
349
+ /** Theme character-palette slot (0-11) for the inspector chip / report bar. Absent = neutral. */
350
+ colour?: number;
351
+ }
352
+ /** The default recording-status ladder (used when a project declares none). The FIRST rung is the
353
+ * level-0 / absence status: a beat with no recorded status counts as `missing`. */
354
+ declare const DEFAULT_RECORDING_STATUSES: RecordingStatusDecl[];
355
+ /** One recording rung mapped to the folder its audio files live in (Audio Folders mode). */
356
+ interface RecordingFolder {
357
+ name: string;
358
+ /** Project-relative folder `<audioRoot>/<slug(name)>`, or undefined for the baseline "not recorded" rung. */
359
+ folder?: string;
360
+ }
361
+ /**
362
+ * Audio Folders (#206): map the recording ladder to its derived folders under a single audio root.
363
+ * Each rung's folder is `<audioRoot>/<slug(name)>`; the FIRST (lowest) rung is the "not recorded"
364
+ * baseline and gets NO folder. The single source of truth shared by the indexer, scratch-save, and the
365
+ * manifest so they never disagree. Returns bare rungs (no folders) when `audioRoot` is empty.
366
+ */
367
+ declare function deriveRecordingFolders(audioRoot: string | undefined, statuses: RecordingStatusDecl[]): RecordingFolder[];
368
+ /**
369
+ * A class of documentation note (spec §18) - the project-defined vocabulary plus
370
+ * where each class is DELIVERED. The editor (Patterpad) always shows every class;
371
+ * `deliver` governs EXPORTS only. A note inherits down the tree, so a class set
372
+ * on a scene/block flows to its lines (outermost-first).
373
+ */
374
+ interface DocumentationClass {
375
+ name: string;
376
+ /**
377
+ * Export channels this class flows to: a list of channel names, or `"*"` for
378
+ * all. Omitted/empty = editor-only (e.g. "writing"). Built-in channels: `vo`
379
+ * (voice-recording scripts), `loc` (localisation handoff); studios add their
380
+ * own (e.g. `sfx`, `art`), carried now and picked up when that export exists.
381
+ */
382
+ deliver?: string[] | "*";
383
+ }
384
+ /**
385
+ * The default documentation classes (used when a project declares none). An
386
+ * untyped note (no class) is editor-only by construction; these are the named
387
+ * routes. The vocabulary is project-extensible - a studio replaces this list.
388
+ */
389
+ declare const DEFAULT_DOCUMENTATION_CLASSES: DocumentationClass[];
390
+ /** The version-control system a project is kept under (spec §12). Drives the lock-aware vs merge-based
391
+ * write path + the emitted VCS config; chosen at create and switchable in Project Settings. */
392
+ type VcsKind = "git" | "perforce" | "plastic" | "svn" | "none";
393
+ /** Spell-check setup for a project (Patterpad #177). */
394
+ interface ProjectDictionary {
395
+ /** The active dictionary id - a built-in language ("en-US"/"en-GB") or an app-level imported Hunspell
396
+ * pair. Absent = derive from the source locale. The imported pair itself lives per-machine (userData),
397
+ * so only the id travels with the project. */
398
+ language?: string;
399
+ /** The project's custom word list - names, places, invented terms - always accepted. Travels with the
400
+ * project (shared via VCS), unlike the per-machine imported dictionaries. */
401
+ words?: string[];
402
+ /** Words the author chose to IGNORE (right-click ▸ Ignore on a flagged word). Distinct from `words`:
403
+ * these aren't vocabulary to add, just tokens to stop flagging in this project (a dialect spelling, a
404
+ * code). Both suppress the squiggle; they differ in intent + where they surface. Travels with the project. */
405
+ ignore?: string[];
406
+ /** Spell-check on/off for this project (default on). */
407
+ enabled?: boolean;
408
+ }
409
+ /**
410
+ * Estimating (writing-burndown, spec §13): when a scene is still all guesswork - every status-tracked
411
+ * beat at or below `thresholdStatus` (an unset beat counts as the lowest rung) - the production report
412
+ * REPLACES its actual (placeholder) line count with an estimate, and shares that estimate across the
413
+ * characters appearing in its placeholder lines. Off by default; when off, no estimate appears anywhere.
414
+ * See design/proposals/estimating.md.
415
+ */
416
+ interface EstimatingConfig {
417
+ /** Master on/off. Off (or absent) = the report shows pure actuals, no estimate anywhere. */
418
+ enabled: boolean;
419
+ /** Writing-ladder rung NAME: a scene is estimated only when EVERY status-tracked beat is at or below
420
+ * this rung. Absent = the lowest rung. */
421
+ thresholdStatus?: string;
422
+ /** The per-scene estimate (written lines) used when no tag override matches. */
423
+ defaultLines: number;
424
+ /** Tag -> lines overrides. A scene carrying a mapped tag uses that number; if it carries several, the
425
+ * LARGEST wins. */
426
+ tagEstimates?: {
427
+ tag: string;
428
+ lines: number;
429
+ }[];
430
+ }
431
+ interface ProjectFile {
432
+ schema: string;
433
+ root?: boolean;
434
+ project: {
435
+ id: string;
436
+ name: string;
437
+ roomKey?: string;
438
+ };
439
+ locales: {
440
+ default: string;
441
+ all: string[];
442
+ };
443
+ /** The authored entry point: the scene (and optional block) a flow starts at when none is given,
444
+ * used by `patter play`, Patterpad's Play, and coverage. Omitted = fall back to the first scene. */
445
+ start?: {
446
+ scene: string;
447
+ block?: string;
448
+ };
449
+ /** The authored scene order (scene ids) for navigation. Scenes not listed follow in file order;
450
+ * listed ids that no longer exist are ignored. Omitted = plain file order. Presentation only:
451
+ * it never affects play, which always starts from `start` / an explicit address. */
452
+ sceneOrder?: string[];
453
+ /** Version-control system (spec §12); omitted = unset / none. */
454
+ vcs?: VcsKind;
455
+ /** Project-wide VO mode (spec §16) - one boolean, no per-scene override. */
456
+ voiced?: boolean;
457
+ /** Track audio/recording status (#206): whether the recording-status ladder, Audio Folders, scratch, and
458
+ * the inspector's Audio row + the report's recording breakdown are active. Only meaningful for a `voiced`
459
+ * project (it gates on both). Authoring metadata, editor-only; never reaches the bundle. Omitted = OFF -
460
+ * opt-in even for a voiced project (a voiced story may want voice scripts without tracking recording status). */
461
+ trackAudioStatus?: boolean;
462
+ /**
463
+ * Inline text formatting. When true, authors can mark dialogue / narration / direction /
464
+ * choice-prompt text bold, italic, or bold+italic; it is stored INSIDE the localised strings
465
+ * (and the flow's `direction`) as `<b>…</b>`, `<i>…</i>`, `<bi>…</bi>`, with literal `<`, `>`,
466
+ * `&` escaped to `&lt;` / `&gt;` / `&amp;`. The runtime treats the string as opaque - it is the
467
+ * GAME's job to parse the tags. Default ON (omitted === enabled); set `false` to disable it for a
468
+ * game that renders plain strings and would otherwise show the tags literally. */
469
+ formatting?: boolean;
470
+ /** Autosave: periodically persist the edited scene without an explicit Save. Default ON (omitted ===
471
+ * enabled); set `false` to require manual saves. */
472
+ autosave?: boolean;
473
+ /** Closed captions (#214): the delimiter pair that wraps non-spoken caption cues inside DIALOGUE
474
+ * lines (e.g. `(sigh)` in `Oh dear. (sigh) What now?`). A game can turn captions off at runtime
475
+ * (`setClosedCaptions(false)`), and the runtime then strips every `open…close` span - delimiters and
476
+ * surrounding whitespace - from line text. Baked into the bundle so the runtime knows the pair;
477
+ * omitted = the default `(` / `)`. Captions are ON by default (full text shown). */
478
+ closedCaptions?: CaptionDelimiters;
479
+ audio?: {
480
+ scratchStore: string;
481
+ };
482
+ layout?: {
483
+ flow?: string;
484
+ strings?: string;
485
+ authoring?: string;
486
+ };
487
+ /** `bundle`: the compiled `.patterc` output path (relative to the project root,
488
+ * or absolute); default `dist/<project-file-stem>.patterc` (spec §11).
489
+ * `localisation`: how strings ship + are resolved (spec §11).
490
+ * - "embedded" (default): every locale's strings live INSIDE the `.patterc`; the runtime resolves
491
+ * them and can switch locale live (`setLocale`).
492
+ * - "ids": the `.patterc` carries NO strings; the runtime emits the beat ID for each line and the
493
+ * game looks it up in its own loc system (Export Localisation hands over the language files).
494
+ * `sourceDebug` embeds the SOURCE language too, purely so the build can be played for debugging;
495
+ * the bundle flags it so the runtime can warn it is not a shippable build. */
496
+ export?: {
497
+ targets?: string[];
498
+ bundle?: string;
499
+ localisation?: {
500
+ mode: "embedded" | "ids";
501
+ sourceDebug?: boolean;
502
+ };
503
+ };
504
+ properties?: PropertyDecl[];
505
+ /** Host / world scopes the project declares (`@world`, `@game`, ...): makes them first-class so the
506
+ * compiler validates references into them, the runtime self-backs them from defaults when no host
507
+ * resolver is bound, and coverage drives their values. Omitted = no host scopes (`@world.x` is then a
508
+ * compile error). See design/scope-registry.md §6. */
509
+ scopeRegistry?: HostScopeRegistry;
510
+ /** Coverage input drivers (#159): values to feed host scopes (`@world`) during a coverage run so
511
+ * externally-gated branches get exercised. Authoring-only (never reaches the runtime bundle). */
512
+ coverageDrivers?: CoverageDriver[];
513
+ cast?: CastMember[];
514
+ gameDataFields?: GameDataFields;
515
+ /** Ordered writing-status ladder (not-done -> done); default `DEFAULT_WRITING_STATUSES`. */
516
+ writingStatuses?: WritingStatusDecl[];
517
+ /** Ordered recording-status ladder (not-done -> done); default `DEFAULT_RECORDING_STATUSES`. */
518
+ recordingStatuses?: RecordingStatusDecl[];
519
+ /** Audio Folders mode (#206): the single project-relative root under which each rung's audio lives, in
520
+ * an auto-derived subfolder `<audioRoot>/<slug(statusName)>/` (see `deriveRecordingFolders`). Only
521
+ * meaningful when `audioFolders` is on. Authoring metadata; never reaches the bundle. */
522
+ audioRoot?: string;
523
+ /** Audio Folders mode (#206): when true, a dialogue line's recording status is DERIVED from which
524
+ * rung's derived folder (under `audioRoot`) holds its `<beatId>.wav|mp3` (top-down the ladder, implicit
525
+ * "missing"), instead of being set manually. Authoring metadata; never reaches the bundle. Default off. */
526
+ audioFolders?: boolean;
527
+ /** Scratch recording (Patterpad #224): the recording-status rung whose folder is the source/dest for
528
+ * in-app "record scratch" takes. When set (and `audioFolders` on), Patterpad offers to record a quick
529
+ * scratch take into this rung's folder for any line at or below this rung. Authoring metadata, editor-
530
+ * only; never reaches the bundle. Unset = scratch recording off. */
531
+ scratchStatus?: string;
532
+ /** Spell-check setup (Patterpad #177): the active dictionary language + the project's custom word list +
533
+ * an on/off flag. Source-language-only, authoring metadata - it never reaches the runtime bundle. */
534
+ dictionary?: ProjectDictionary;
535
+ /**
536
+ * Estimating (spec §13 writing burndown): replace a still-guesswork scene's actual (placeholder)
537
+ * line count with an estimate in the production report. Off by default. See `EstimatingConfig` and
538
+ * design/proposals/estimating.md.
539
+ */
540
+ estimating?: EstimatingConfig;
541
+ /** Documentation-note classes + their export routing (spec §18); default `DEFAULT_DOCUMENTATION_CLASSES`. */
542
+ documentationClasses?: DocumentationClass[];
543
+ }
544
+ interface LocaleFile {
545
+ schema: string;
546
+ /** Scene id this file's strings belong to (or a project-level marker). */
547
+ scene: string;
548
+ locale: string;
549
+ default?: boolean;
550
+ /** beatId -> text. */
551
+ strings: Record<string, string>;
552
+ }
553
+ /** The `scene` marker for a project-level loc shard (`loc/<locale>/_project.patterloc`): strings that
554
+ * aren't tied to a scene beat - currently cast display names, later project title / UI strings. */
555
+ declare const PROJECT_LOCALE_SCENE = "@project";
556
+ /** The project-level loc-string key for a cast member's player-facing name, e.g. `cast:BARKEEP`.
557
+ * Namespaced so it can't collide with opaque beat ids. The default-locale value is seeded from the
558
+ * CastMember's `displayName`; the runtime resolves a character's shown name through this key. */
559
+ declare function castStringKey(name: string): string;
560
+ /** Typed documentation annotation line (spec §18). */
561
+ interface DocLine {
562
+ /** The documentation CLASS (a `DocumentationClass.name`) - routes export
563
+ * visibility. Omitted = editor-only (not delivered to any export). */
564
+ type?: string;
565
+ text: string;
566
+ }
567
+ /** One message in a threaded editor comment: who wrote it, when (ISO timestamp), and the text. */
568
+ interface CommentMessage {
569
+ author: string;
570
+ ts: string;
571
+ body: string;
572
+ }
573
+ /** A sub-text range a comment is pinned to, within its anchor node's say text (#148). Offsets are
574
+ * character positions over the rendered (plain) source text - inline formatting is marks, not part of
575
+ * the count. `quote` is the text the range covered when made: the editor re-anchors by FINDING it (the
576
+ * offsets are just a hint), and a thread whose quote no longer exists is shown demoted, not lost. */
577
+ interface CommentRange {
578
+ from: number;
579
+ to: number;
580
+ quote: string;
581
+ }
582
+ /** Threaded editor comment (collaboration), anchored to a stable beat/node id. `messages[0]` is the
583
+ * opener; replies follow in order (each carries its own author + timestamp, Word/Docs style).
584
+ * `range` pins it to a span of the node's text (absent = the whole beat). `resolved` archives the
585
+ * thread - hidden in the editor unless "show resolved comments" is on. */
586
+ interface Comment {
587
+ id: string;
588
+ /** Anchored to a stable beat/node id. */
589
+ anchor: string;
590
+ /** A sub-text span within the anchor's text; absent = a whole-beat comment. */
591
+ range?: CommentRange;
592
+ resolved?: boolean;
593
+ messages: CommentMessage[];
594
+ }
595
+ /** A "suggest a rewrite" proposal for a single say/prose beat (review flow, design/proposals/
596
+ * suggest-rewrite.md). Whole-beat anchored: `baseline` is the say text WHEN suggested (the "before" +
597
+ * the drift detector - if it no longer matches the live text, the line changed since and the suggestion
598
+ * is shown stale), `proposed` is the replacement. Accept overwrites the beat's say text; both accept and
599
+ * reject set `resolved` (archived) + `outcome` (a light audit trail). Stored in the authoring shard,
600
+ * never in the flow text, so downstream tools ignore it. */
601
+ interface Suggestion {
602
+ id: string;
603
+ /** The say/prose beat's stable id. */
604
+ anchor: string;
605
+ /** The say text at the moment this was suggested (the diff "before" + staleness check). */
606
+ baseline: string;
607
+ /** The proposed replacement say text. */
608
+ proposed: string;
609
+ author: string;
610
+ ts: string;
611
+ /** Accepted or rejected -> archived (hidden unless "show resolved suggestions" is on). */
612
+ resolved?: boolean;
613
+ outcome?: "accepted" | "rejected";
614
+ }
615
+ interface EditRecord {
616
+ modifiedAt?: string;
617
+ by?: string;
618
+ /** Per-locale localisation date -> staleness when source modifiedAt is later. */
619
+ localisedAt?: Record<string, string>;
620
+ }
621
+ interface AuthoringFile {
622
+ schema: string;
623
+ comments?: Comment[];
624
+ /** "Suggest a rewrite" review proposals (design/proposals/suggest-rewrite.md). */
625
+ suggestions?: Suggestion[];
626
+ /** Typed documentation, keyed by node/beat id (spec §18). */
627
+ documentation?: Record<string, DocLine[]>;
628
+ /**
629
+ * Writing status, keyed by BEAT id - a value from the project's writing-status
630
+ * enumeration (spec §13; `writingStatuses`, default `DEFAULT_WRITING_STATUSES`).
631
+ * Tracked on the source language only (translation staleness is `localisedAt`).
632
+ */
633
+ writing?: Record<string, string>;
634
+ /**
635
+ * Recording status, keyed by BEAT id - a value from the project's recording-status
636
+ * enumeration (spec §13/§16; `recordingStatuses`). Single-locale by design: games
637
+ * recording VO in more than one language are rare (and absent at indie level); if
638
+ * ever needed, a per-locale extension comes later.
639
+ */
640
+ recording?: Record<string, string>;
641
+ /**
642
+ * Audio-relationship metadata, keyed by BEAT id. Native (recording) language
643
+ * only - like recording status, VO is single-language by design.
644
+ */
645
+ audio?: Record<string, unknown>;
646
+ /** Author trail + edit/localisation dates, keyed by beat/node id. */
647
+ edits?: Record<string, EditRecord>;
648
+ /**
649
+ * **Cut** content (spec §13): scene or beat ids removed from the production
650
+ * but kept in source. Orthogonal to status (cut is not a degree of doneness);
651
+ * reports exclude cut content from counts / estimates / coverage and surface
652
+ * it as a separate "cut: N" figure so a removal is visible, not vanished.
653
+ */
654
+ cut?: Record<string, boolean>;
655
+ }
656
+ /** A compiled expression envelope: canonical source + pre-derived tagged-tuple AST. */
657
+ interface Expression {
658
+ src: string;
659
+ ast: AstNode;
660
+ }
661
+ type CompiledEffect = {
662
+ kind: "set";
663
+ target: string;
664
+ value: Expression;
665
+ };
666
+ interface CompiledSnippet {
667
+ id: string;
668
+ type: "snippet";
669
+ condition?: Expression;
670
+ beats?: Beat[];
671
+ onEnter?: CompiledEffect[];
672
+ onExit?: CompiledEffect[];
673
+ gameData?: GameData;
674
+ tags?: string[];
675
+ jump?: Jump;
676
+ secretUntilEligible?: boolean;
677
+ /** Option-position: repeatable (spec §5). Default false = once-only. */
678
+ sticky?: boolean;
679
+ /** Option-position: the choice's fallback, auto-followed when last (spec §5). */
680
+ fallback?: boolean;
681
+ }
682
+ interface CompiledGroup {
683
+ id: string;
684
+ type: "group";
685
+ condition?: Expression;
686
+ /** Default (omitted) = `"run"`. */
687
+ selector?: Selector;
688
+ /** Selector cursor shared across flows (default false = per-flow). */
689
+ shared?: boolean;
690
+ options?: SequenceOptions;
691
+ children: Array<CompiledGroup | CompiledSnippet>;
692
+ gameData?: GameData;
693
+ tags?: string[];
694
+ /** Option-position fields (spec §5) - only when a direct child of a `choice`. */
695
+ prompt?: PromptBeat;
696
+ secretUntilEligible?: boolean;
697
+ /** Repeatable (spec §5). Default false = once-only. */
698
+ sticky?: boolean;
699
+ /** The choice's fallback, auto-followed when last (spec §5). */
700
+ fallback?: boolean;
701
+ }
702
+ interface CompiledBlock {
703
+ id: string;
704
+ type: "block";
705
+ name: string;
706
+ /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
707
+ gameId?: string;
708
+ children: Array<CompiledGroup | CompiledSnippet>;
709
+ gameData?: GameData;
710
+ tags?: string[];
711
+ }
712
+ interface CompiledScene {
713
+ id: string;
714
+ type: "scene";
715
+ name: string;
716
+ /** Host-facing address (spec §6); the runtime resolves it to `id`. Absent = derived from `name`. */
717
+ gameId?: string;
718
+ gameData?: GameData;
719
+ tags?: string[];
720
+ onEntry?: CompiledEffect[];
721
+ sceneProps?: PropertyDecl[];
722
+ blocks: CompiledBlock[];
723
+ }
724
+ interface Bundle {
725
+ schema: string;
726
+ /** `hash` fingerprints the WHOLE bundle (binds saves, gates staleness); `structureHash` is the same
727
+ * fingerprint with the string tables left out, so same structureHash + a different hash = a
728
+ * text-only edit, safe to hot-swap in place (live bundle refresh). */
729
+ content: {
730
+ project: string;
731
+ version?: string;
732
+ hash?: string;
733
+ structureHash?: string;
734
+ };
735
+ voiced: boolean;
736
+ locales: {
737
+ default: string;
738
+ included: string[];
739
+ };
740
+ cast?: CastMember[];
741
+ properties?: PropertyDecl[];
742
+ /** Host / world scope declarations, baked from the project so the runtime can self-back a declared
743
+ * scope (`@world`, ...) when no host resolver claims its token. Absent = no host scopes. */
744
+ scopeRegistry?: HostScopeRegistry;
745
+ gameDataFields?: GameDataFields;
746
+ scenes: Record<string, CompiledScene>;
747
+ /** locale -> (beatId -> text). In "embedded" localisation this carries every included locale; in "ids"
748
+ * it is EMPTY (the runtime emits beat IDs), unless `localisation.sourceDebug` embedded the source locale
749
+ * for debug playback. `content.hash` is computed over the FULL strings regardless, so the staleness gate
750
+ * is unaffected. */
751
+ strings: Record<string, Record<string, string>>;
752
+ /** How strings ship + resolve (spec §11). Absent = "embedded" (back-compat default): the runtime resolves
753
+ * `strings` per locale. "ids": the runtime emits beat IDs and the game localises them itself; `sourceDebug`
754
+ * means the source locale is embedded purely for debug playback and the runtime should flag the build as
755
+ * not shippable. */
756
+ localisation?: {
757
+ mode: "embedded" | "ids";
758
+ sourceDebug?: boolean;
759
+ };
760
+ /** Closed-caption delimiters baked from the project (#214). Absent = the default `(` / `)`; the
761
+ * runtime strips spans between them from line text when a game disables captions. */
762
+ closedCaptions?: CaptionDelimiters;
763
+ }
764
+ /** Closed-caption configuration (#214). `open`/`close` wrap a caption cue inside a dialogue line (both
765
+ * non-empty; they MAY be the same token, e.g. `*…*`). `character` names a cast member whose lines are a
766
+ * pure caption: when captions are off, ALL of that character's dialogue (and its speaker label) is
767
+ * omitted - delimiters or not - leaving a silent line that still fires (so audio plays). Absent / empty
768
+ * `character` resolves to the default `SFX` (you "disable" it simply by never using that speaker). */
769
+ interface CaptionDelimiters {
770
+ open: string;
771
+ close: string;
772
+ character?: string;
773
+ }
774
+ /** The default caption delimiters when a project pins none: square brackets, the closed-captioning
775
+ * convention for non-speech cues. Round brackets are deliberately NOT the default: `(` at the start of a
776
+ * line opens a performance direction in the editor, so it would shadow a caption cue there. */
777
+ declare const DEFAULT_CAPTION_DELIMITERS: CaptionDelimiters;
778
+ /** The default caption character: a cast member named `SFX` whose lines are pure captions (omitted when
779
+ * captions are off). Applies even to a project that pins no `closedCaptions`. */
780
+ declare const DEFAULT_CAPTION_CHARACTER = "SFX";
781
+
782
+ export { type AuthoringFile, type Beat, type Block, type Bundle, type CaptionDelimiters, type CastMember, type Comment, type CommentMessage, type CommentRange, type CompiledBlock, type CompiledEffect, type CompiledGroup, type CompiledScene, type CompiledSnippet, type CoverageDriver, DEFAULT_CAPTION_CHARACTER, DEFAULT_CAPTION_DELIMITERS, DEFAULT_DOCUMENTATION_CLASSES, DEFAULT_RECORDING_STATUSES, DEFAULT_WRITING_STATUSES, type DocLine, type DocumentationClass, type EditRecord, type Effect, type EstimatingConfig, type Expression, type FlowFile, type FlowNode, type GameData, type GameDataField, type GameDataFieldType, type GameDataFields, type GameDataNodeKind, type GameEventBeat, type Group, type HostScopeDecl, type HostScopeRegistry, type HostScopeSpec, type Jump, type JumpTarget, type LineBeat, type LocaleFile, PROJECT_LOCALE_SCENE, type ProjectDictionary, type ProjectFile, type PromptBeat, type PropertyDecl, type PropertyType, type RecordingFolder, type RecordingStatusDecl, type ScalarValue, type Scene, type Selector, type SelectorExhaust, type SelectorOrder, type SequenceOptions, type Snippet, type Suggestion, type TextBeat, type VcsKind, type WritingStatusDecl, castStringKey, deriveRecordingFolders, effectiveGameId, gameIdify, isContentlessBeat, isValidGameId, walkNodes };