@storylet-studio/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,1113 @@
1
+ import { ScalarValue, Expression } from '@wildwinter/expr';
2
+ export { AstNode, Expression, RESERVED_PROPERTY_NAMES, ScalarValue, isCaseOnlyPropertyName, isValidPropertyName, propertyNameify } from '@wildwinter/expr';
3
+
4
+ /** The template's key in every `templates` bag it appears in. */
5
+ declare const SPATIAL = "spatial";
6
+ /** A zone's outline, in the map's own coordinate space (the same space the view
7
+ * sidecar's sites use). No units: a map is to its own scale. */
8
+ type Polygon = ViewPoint[];
9
+ /**
10
+ * Where something sits in the stack: bigger is nearer the front.
11
+ *
12
+ * SPARSE, and the fallback is what makes it so: an item without one takes its
13
+ * position in the list, which is the order everything already drew in, so a
14
+ * project that has never been restacked looks exactly as it did. Only a moved
15
+ * item gains a number, set midway between its new neighbours - the same scheme
16
+ * cards, decks and hands use for authored order, and merge-clean for the same
17
+ * reason: two authors restacking different things touch different entries.
18
+ *
19
+ * Generic on purpose. Zones need it now; BACKGROUNDS will need the identical
20
+ * thing, in their own band below the zones, and this is the piece they share.
21
+ */
22
+ type Stacked = {
23
+ id: string;
24
+ z?: number;
25
+ };
26
+ /** A stack move, in the vocabulary every drawing tool uses. */
27
+ type StackMove = "front" | "forward" | "backward" | "back";
28
+ /** Back to front: the order to DRAW in, so the frontmost lands on top. */
29
+ declare function stacked<T extends Stacked>(items: T[]): T[];
30
+ /**
31
+ * The `z` that puts `id` where the move asks, or undefined when it is already
32
+ * there (so a no-op never writes a file or costs an undo step).
33
+ *
34
+ * Moving one place uses the midpoint between the item it passes and the one
35
+ * beyond, which is why the numbers stay sparse and nothing has to be renumbered.
36
+ */
37
+ declare function restack<T extends Stacked>(items: T[], id: string, move: StackMove): number | undefined;
38
+ /**
39
+ * A background image behind a map: a picture of the real space, so a designer
40
+ * (and later an experience runner) can map content onto somewhere physical.
41
+ *
42
+ * NOT a player-facing asset, which is what settles most of its design. The bytes
43
+ * are never touched - no transcoding, no downscaling - because the file on disk
44
+ * is also what a runtime server will one day serve, and legibility at a glance
45
+ * matters more than fidelity.
46
+ *
47
+ * On the GROUP rather than on a zone, because zones cross images: a site plan is
48
+ * one picture with a dozen zones traced over it, and half of them straddle two
49
+ * sheets. Several per group, composing one picture - tiles of a large site, or
50
+ * deliberate overlaps - never alternates, so nothing here is exclusive with
51
+ * anything else.
52
+ */
53
+ interface SpatialBackground {
54
+ /** Stable id, so two authors adding images do not collide. */
55
+ id: string;
56
+ /** The file's name inside the box's `assets/` folder. A NAME, not a path:
57
+ * assets belong to their box and travel with it. */
58
+ file: string;
59
+ /** Where it sits, in map units. Placement and scale in one rectangle, because
60
+ * a separate scale factor is a second thing to reason about and there is no
61
+ * rotation to make it worth having. */
62
+ x: number;
63
+ y: number;
64
+ width: number;
65
+ height: number;
66
+ /** 0 to 1, default 1. A tracing base wants to sit back. */
67
+ opacity?: number;
68
+ /** Its place in the stack among the OTHER BACKGROUNDS (see `stacked`).
69
+ * Backgrounds are a band strictly below the zones, structurally, so no value
70
+ * here can put an image over a zone. */
71
+ z?: number;
72
+ /** Out of the way while working on something else. */
73
+ hidden?: boolean;
74
+ /** Locked, the diagram-tool convention: invisible to the pointer, so clicks
75
+ * pass through to whatever is above. What a tracing base should be once it is
76
+ * placed. */
77
+ locked?: boolean;
78
+ }
79
+ /** What the spatial template keeps on a GROUP: the marker, and the pictures
80
+ * behind the map. A bag rather than a boolean precisely so this could arrive
81
+ * without a schema change - the marker and the configuration are one object, so
82
+ * a group is spatial exactly when it has one. */
83
+ interface SpatialGroup {
84
+ /** Present and true for a spatial group. */
85
+ map: true;
86
+ backgrounds?: SpatialBackground[];
87
+ }
88
+ /** Is this group a map? */
89
+ declare function isSpatial(group: TagGroup): boolean;
90
+ /** The group's spatial configuration, or undefined when it is an ordinary group. */
91
+ declare function spatialOf(group: TagGroup): SpatialGroup | undefined;
92
+ /**
93
+ * The group's backgrounds, in DRAW order (back to front), skipping any entry
94
+ * that is not one.
95
+ *
96
+ * Forgiving in the same way `polygonOf` is, and for the same reason: a
97
+ * hand-edited or badly merged shard must not throw inside a canvas. Anything
98
+ * malformed reads as absent HERE and is reported by validation instead, which is
99
+ * a place an author can see it.
100
+ */
101
+ declare function backgroundsOf(group: TagGroup): SpatialBackground[];
102
+ /**
103
+ * Where a dropped picture lands: fitted inside 60% of the viewport, in BOTH axes,
104
+ * centred on `at`, in map units.
105
+ *
106
+ * It does NOT define the map's coordinate space and does not arrive at one pixel
107
+ * to one unit (a 2816px site plan would fill the county). The requirement is
108
+ * narrower and more useful: whatever the zoom, a picture arrives at a size
109
+ * comfortable to GRAB.
110
+ *
111
+ * Computed once, at drop, and stored. Never re-derived from the camera, or the
112
+ * picture would move about when somebody zoomed.
113
+ *
114
+ * 60% of the shorter side was the first rule and it was wrong: scale-invariant as
115
+ * intended, but it put a 1.83:1 plan at a third of the width of a wide window,
116
+ * which is fiddly to grab. Fitting both axes measures 55% x 60% of the view for
117
+ * that image, at every zoom.
118
+ */
119
+ declare function droppedRect(natural: {
120
+ width: number;
121
+ height: number;
122
+ }, view: {
123
+ width: number;
124
+ height: number;
125
+ }, scale: number, at: ViewPoint): Rect;
126
+ /** Replace the group's backgrounds, returning the new `templates` bag. Keeps the
127
+ * marker and every other template's bag, as every writer here does. */
128
+ declare function withBackgrounds(group: TagGroup, backgrounds: SpatialBackground[]): TagGroup["templates"];
129
+ /** A zone's outline, or undefined when the tag has never been drawn. Anything
130
+ * malformed reads as undefined here and is REPORTED by validation instead: a
131
+ * canvas that threw on a hand-edited shard would be a poor way to find out. */
132
+ declare function polygonOf(tag: Tag): Polygon | undefined;
133
+ /**
134
+ * Mark a group spatial, or clear the marker, returning the new `templates` bag.
135
+ *
136
+ * Every one of these writers PRESERVES keys it does not own, because a shard may
137
+ * carry another template's bag (or a newer version of this app's) and dropping it
138
+ * would be data loss on a file somebody else owns.
139
+ */
140
+ declare function withSpatialGroup(group: TagGroup, on: boolean): TagGroup["templates"];
141
+ /** A zone's place in the stack, or undefined when it has never been moved. */
142
+ declare function zOf(tag: Tag): number | undefined;
143
+ /** Set a zone's place in the stack, returning the new `templates` bag. */
144
+ declare function withZ(tag: Tag, z: number): Tag["templates"];
145
+ /** Set or clear a zone's outline, returning the new `templates` bag. */
146
+ declare function withPolygon(tag: Tag, polygon: Polygon | undefined): Tag["templates"];
147
+ interface Rect {
148
+ x: number;
149
+ y: number;
150
+ width: number;
151
+ height: number;
152
+ }
153
+ /** The axis-aligned box around a polygon: what a canvas needs to hit-test
154
+ * cheaply, to place a selection ring, and to frame a fit. */
155
+ declare function polygonBounds(polygon: Polygon): Rect;
156
+ /**
157
+ * A polygon's centre of AREA, not the average of its vertices.
158
+ *
159
+ * The two agree only for regular shapes: on an outline with a cluster of vertices
160
+ * along a fiddly coastline, the vertex average is dragged towards the crowded side.
161
+ * Falls back to the vertex average for a degenerate (zero-area) polygon, where
162
+ * there is no better answer.
163
+ *
164
+ * NOT the place to put a label: see `labelPoint`. The centre of area of a concave
165
+ * shape can lie outside the shape, which an L-shaped zone demonstrates in one
166
+ * line of arithmetic.
167
+ */
168
+ declare function centroid(polygon: Polygon): ViewPoint;
169
+ /**
170
+ * Where a zone's LABEL goes: a point guaranteed to be inside the zone.
171
+ *
172
+ * The centre of area is the obvious answer and it is wrong for concave outlines.
173
+ * An L-shaped zone puts its centre of area in the notch, so a label placed there
174
+ * sits outside its own zone, next to whatever is drawn in the gap. Since half the
175
+ * zones anyone draws over a floor plan are L-shaped corridors, this needs to be
176
+ * right rather than usually right.
177
+ *
178
+ * So: the centre of area when that is inside, and otherwise the middle of the
179
+ * widest run of the zone along the horizontal line through it. That keeps the
180
+ * label vertically where the eye expects and moves it sideways into the shape,
181
+ * which for an L means "along the arm". Cheap, stable as the polygon is dragged,
182
+ * and no substitute for the proper pole-of-inaccessibility if a zone ever needs
183
+ * one.
184
+ */
185
+ declare function labelPoint(polygon: Polygon, opts?: {
186
+ bias?: "middle" | "top";
187
+ }): ViewPoint;
188
+ /**
189
+ * Is this point inside the polygon? The ray-casting test, which handles concave
190
+ * outlines and self-touching ones alike.
191
+ *
192
+ * This is the function that earns the map its keep: dragging a hand's pin from
193
+ * the docks into the market is not a cosmetic act, it rebinds the hand, and this
194
+ * is how the view knows which zone the pin was dropped in.
195
+ */
196
+ declare function pointInPolygon(point: ViewPoint, polygon: Polygon): boolean;
197
+ /**
198
+ * Which zone a point falls in, given the zones of a spatial group in draw order.
199
+ *
200
+ * Zones may overlap (a room inside a wing, a market square inside a district), so
201
+ * "which one" needs an answer rather than a shrug: the FRONTMOST match wins,
202
+ * because that is the one drawn on top and therefore the one the author sees at
203
+ * that point. Front is the stack (see `stacked`), which for a project nobody has
204
+ * restacked is the order the zones are listed in, exactly as before. Returns
205
+ * undefined for a point in open space.
206
+ */
207
+ declare function zoneAt(point: ViewPoint, zones: (Stacked & {
208
+ polygon: Polygon;
209
+ })[]): string | undefined;
210
+ /**
211
+ * EVERY zone a point falls in, frontmost first.
212
+ *
213
+ * `zoneAt` is this list's head, so the two cannot disagree about which zone wins.
214
+ * The tail is what an author needs telling about: zones are allowed to overlap,
215
+ * so a site can sit inside two outlines while belonging to exactly one of them,
216
+ * and a picture that shows containment the model does not have is a picture that
217
+ * misleads. The editor uses the length of this to say so.
218
+ */
219
+ declare function zonesAt(point: ViewPoint, zones: (Stacked & {
220
+ polygon: Polygon;
221
+ })[]): string[];
222
+
223
+ /**
224
+ * What a hand's relationship to one tag group is.
225
+ *
226
+ * `tag` is the tag id it is bound to, absent when the route exists but is not
227
+ * filled in yet (a hole nobody has chosen for). `editable` says whether this
228
+ * hand can be rebound on its own: the whole point of the distinction.
229
+ */
230
+ type HandBinding =
231
+ /** A hole the hand's template declares and this hand fills. */
232
+ {
233
+ kind: "chosen";
234
+ tag?: string;
235
+ editable: true;
236
+ }
237
+ /** A standalone hand's own binding. */
238
+ | {
239
+ kind: "rule";
240
+ tag?: string;
241
+ editable: true;
242
+ }
243
+ /** The template binds it for every instance: not this hand's to change. */
244
+ | {
245
+ kind: "fixed";
246
+ tag: string;
247
+ editable: false;
248
+ }
249
+ /** The hand has nothing to do with this group. */
250
+ | {
251
+ kind: "none";
252
+ editable: false;
253
+ };
254
+ /**
255
+ * How `hand` is bound to `groupId`, given its template (undefined for a
256
+ * standalone hand, or when the template has gone missing).
257
+ *
258
+ * Order matters: a standalone hand answers from its own rule, and a template
259
+ * instance prefers the HOLE, because a template that both binds a group and
260
+ * declares it as a hole is malformed and the hole is the one an instance can
261
+ * actually fill.
262
+ */
263
+ declare function handBinding<E>(hand: Hand<E>, template: HandTemplate<E> | undefined, groupId: string): HandBinding;
264
+ /**
265
+ * Take `hand` OFF its binding for `groupId`, IN PLACE, and say whether anything
266
+ * changed.
267
+ *
268
+ * The hand is left LOOSE, which for a template instance with a hole is an error
269
+ * the compiler already names ("missing chosen tag ... a hand is fully concrete").
270
+ * That is the point of clearing rather than quietly keeping the old value: a hand
271
+ * whose pin has ended up outside every zone genuinely has no zone, and an error
272
+ * an author can see beats a link that is silently wrong.
273
+ */
274
+ declare function unbindHand<E>(hand: Hand<E>, template: HandTemplate<E> | undefined, groupId: string): boolean;
275
+ /**
276
+ * Bind `hand` to `tagId` for `groupId`, IN PLACE, and say whether anything
277
+ * changed.
278
+ *
279
+ * Refuses anything the binding says is not this hand's to change, so a caller
280
+ * cannot rebind one instance and silently move its siblings. The caller is
281
+ * expected to have asked `handBinding` first and offered the gesture only where
282
+ * it means something; this is the backstop rather than the manners.
283
+ */
284
+ declare function bindHand<E>(hand: Hand<E>, template: HandTemplate<E> | undefined, groupId: string, tagId: string): boolean;
285
+
286
+ /** The frames on a canvas, in DRAW order (back to front). */
287
+ declare function framesOf(canvas: CanvasFurniture | undefined): Frame[];
288
+
289
+ /**
290
+ * The comment sidecar for one box.
291
+ *
292
+ * A flat LIST rather than a map: a thread carries its own anchor, so there is
293
+ * no key to hang it on. Id'd so the merge engine can tell two reviewers apart.
294
+ */
295
+ interface NotesShard {
296
+ schema: string;
297
+ comments?: Comment[];
298
+ }
299
+ /** One message in a thread. The author is a NAME, stamped at posting time, not a
300
+ * reference to anything: people leave projects and their words stay. */
301
+ interface CommentMessage {
302
+ author: string;
303
+ /** ISO 8601, stamped when posted. */
304
+ ts: string;
305
+ body: string;
306
+ /**
307
+ * A TOMBSTONE: withdrawn, but still a turn in the conversation.
308
+ *
309
+ * A reply removed outright would renumber the argument around it - the answer
310
+ * to a question nobody can see any more reads as a non-sequitur - so what is
311
+ * left records who spoke and when, and that they took it back. The body is
312
+ * EMPTIED when this is set: "deleted" has to mean gone from the file, or the
313
+ * word is a lie to somebody who typed something they regret.
314
+ *
315
+ * Absent on every message that was never withdrawn, so an ordinary thread's
316
+ * shard is unchanged.
317
+ */
318
+ deleted?: true;
319
+ }
320
+ /**
321
+ * Where a thread is DRAWN, when it was dropped on a canvas rather than opened
322
+ * from an editor (design/annotation.md 3).
323
+ *
324
+ * `canvas` names the canvas it appears on: a deck id, or `map:<boxId>` for a
325
+ * box's map. `x`/`y` are that canvas's own coordinates when the thread is
326
+ * anchored to the canvas itself, and an OFFSET from the item's origin when the
327
+ * thread's `anchor` names an item on that canvas. One field, two readings,
328
+ * distinguished by a single comparison - `anchor === canvas` - because the
329
+ * alternative was a second key that could disagree with the first.
330
+ *
331
+ * A thread with no `mark` is not drawn anywhere: it was opened from an editor,
332
+ * and it lives in that document's topline where it always did.
333
+ */
334
+ interface CommentMark {
335
+ canvas: string;
336
+ x: number;
337
+ y: number;
338
+ }
339
+ /**
340
+ * A thread, anchored to the id of the thing it is about.
341
+ *
342
+ * `anchor` is the SUBJECT: an item id, or a canvas id for a comment about a
343
+ * place rather than a thing. `mark` is only about where it is drawn.
344
+ */
345
+ interface Comment {
346
+ id: string;
347
+ anchor: string;
348
+ /** Marked complete: hidden unless the reviewer asks to see resolved threads. */
349
+ resolved?: boolean;
350
+ /** Present when this thread is a marker on a canvas. */
351
+ mark?: CommentMark;
352
+ messages: CommentMessage[];
353
+ }
354
+ /** Is this thread drawn as a marker, and is it following an item or sitting on
355
+ * the canvas? Answers both questions at once, so no caller has to remember the
356
+ * `anchor === canvas` rule. */
357
+ declare function markOf(thread: Comment): {
358
+ canvas: string;
359
+ x: number;
360
+ y: number;
361
+ item?: string;
362
+ } | undefined;
363
+ /** The threads drawn on one canvas, in the order they were written. */
364
+ declare const marksOn: (shard: NotesShard | undefined, canvas: string) => Comment[];
365
+ /** Every thread in the shard, forgiving of a mangled one. */
366
+ declare function commentsOf(shard: NotesShard | undefined): Comment[];
367
+ /** The threads about one thing, oldest first (their order in the shard). */
368
+ declare const threadsFor: (shard: NotesShard | undefined, anchor: string) => Comment[];
369
+ /** How many UNRESOLVED threads each anchor has: what a speech-bubble shows. A
370
+ * resolved thread is done, and a badge that kept counting it would never
371
+ * return to zero. */
372
+ declare function openThreadCounts(shard: NotesShard | undefined): Record<string, number>;
373
+
374
+ /** A confident reading of what `src` writes, or undefined. */
375
+ declare function inferDeclFromWrite(src: string): Pick<PropertyDecl, "type" | "default"> | undefined;
376
+
377
+ type PropertyType = "boolean" | "number" | "string" | "enum" | "flags" | "quality";
378
+ /** A property declaration: @world / @story / @box / @deck / tag / hand
379
+ * state. A declared property always has a value (`default` is required);
380
+ * referencing an undeclared property is a publish-time error. */
381
+ interface PropertyDecl {
382
+ name: string;
383
+ type: PropertyType;
384
+ default: ScalarValue;
385
+ values?: string[];
386
+ /**
387
+ * A quality's ordered ladder of stage names (design/quality.md). Order IS
388
+ * the meaning: `>=` compares by position here, and `advance()` steps along
389
+ * it. The one order-semantic list in the format, accepted as such: it is a
390
+ * declaration, and inserting a stage mid-ladder is the design's whole point.
391
+ */
392
+ stages?: string[];
393
+ /**
394
+ * The sharing axis (design/flows.md, Patter's flag adopted): is this
395
+ * property's value one world value across all flows, or a copy per flow?
396
+ * It does NOT change reference syntax - sharing is set here, on the
397
+ * declaration, not by a different scope token. Absent = the scope
398
+ * default: `@story` shared; box, deck, hand and tag properties per-flow.
399
+ * On a `@world` declaration the flag is a validation error - `@world` is
400
+ * the game's own state, always engine-level, never per-flow.
401
+ */
402
+ shared?: boolean;
403
+ purpose?: string;
404
+ }
405
+ /** A card-template field (box-defined). Data for the host; the engine never
406
+ * interprets fields and they are not addressable from expressions. */
407
+ interface FieldDecl {
408
+ name: string;
409
+ type: PropertyType;
410
+ default: ScalarValue;
411
+ values?: string[];
412
+ purpose?: string;
413
+ }
414
+ /** Cooldown policy, in turns (schema 3.4). */
415
+ type RedrawPolicy = "always" | "never" | number;
416
+ /** Slugify a human label into a filename- / address-safe gameId. */
417
+ declare function gameIdify(text: string): string;
418
+ declare function isValidGameId(gameId: string): boolean;
419
+
420
+ /** The effective address: a pinned gameId, else derived from the title, else
421
+ * the immutable id (so there is always something addressable). */
422
+ declare function effectiveGameId(entity: {
423
+ gameId?: string;
424
+ title?: string;
425
+ id: string;
426
+ }): string;
427
+ /**
428
+ * The first free gameId of the form `base`, `base-2`, `base-3`, ... not already
429
+ * in `taken`.
430
+ *
431
+ * A gameId is API - `deal()` and the play log speak it - so a name minted for a
432
+ * new or duplicated entity must not collide with an existing one. This lived in
433
+ * two copies, one in the editor and one in the CLI's kit scaffolder, character
434
+ * for character the same; two copies of an addressing rule can drift, and a
435
+ * drift here means the same act produces different addresses depending on which
436
+ * program did it. It is here, beside `gameIdify`, because both programs need it
437
+ * and no UI touches it.
438
+ */
439
+ declare function freeGameId(base: string, taken: ReadonlySet<string>): string;
440
+ /**
441
+ * The first free TITLE of the form `base`, `base 2`, `base 3`, ... whose
442
+ * derived gameId is not already in `taken`.
443
+ *
444
+ * The sibling of `freeGameId` for the "New box", "New deck" case, where the
445
+ * author is given a title and the address follows from it. The dedupe is on the
446
+ * DERIVED gameId rather than the title, because two titles that slug to one
447
+ * address are the collision that matters.
448
+ */
449
+ /**
450
+ * An id-sorted collection in the order a person should SEE it.
451
+ *
452
+ * Storage is sorted by immutable id (source rule 5) so that two authors adding
453
+ * one item each never touch the same line. That makes array position useless as
454
+ * display order, so the order the author arranged rides in a sparse `order`
455
+ * field, with position as the fallback and id to break a tie.
456
+ *
457
+ * One definition, because this rule has to give the same answer in four places
458
+ * that a reader compares side by side: the compiler (what the bundle carries),
459
+ * the editor (what the card document lists), the exports, and Find. When they
460
+ * disagree, the editor shows one order and the game plays another.
461
+ */
462
+ declare function byDisplayOrder<T extends {
463
+ id?: string;
464
+ order?: number;
465
+ }>(items: readonly T[]): T[];
466
+ declare function freeTitle(base: string, taken: ReadonlySet<string>): string;
467
+ interface Outcome<E> {
468
+ id: string;
469
+ gameId?: string;
470
+ title?: string;
471
+ purpose?: string;
472
+ /** Authored display order (sparse; one without it falls back to its id
473
+ * position). Unlike `Card.order` this one IS compiled into the bundle:
474
+ * which option is offered first is authorial, and a host reading a dealt
475
+ * card's outcomes is building the player's menu. */
476
+ order?: number;
477
+ /** Gating; availability is always evaluated against current state. */
478
+ condition?: E;
479
+ /** Target ("@scope.name") -> expression; all right-hand sides evaluate
480
+ * against pre-play state (schema 3.7). */
481
+ changes: Record<string, E>;
482
+ }
483
+ interface Card<E> {
484
+ id: string;
485
+ gameId?: string;
486
+ title?: string;
487
+ purpose?: string;
488
+ /** Authored display order within the deck (sparse; a card without one falls
489
+ * back to its id position). Merges as a per-card value, so id-sorted storage
490
+ * stays merge-clean (Reboot 7.4); dropped from the compiled bundle. */
491
+ order?: number;
492
+ condition?: E;
493
+ /** Default 0; an expression must evaluate to a number. */
494
+ priority: number | E;
495
+ redraw: RedrawPolicy;
496
+ /** Tags: tag group id -> tag ids. An absent group is a wildcard (matches
497
+ * any binding of it), except the reserved home group, whose default
498
+ * inverts (schema 2.4). Editors and fixtures speak gameIds; stored
499
+ * references are ids. */
500
+ tags?: Record<string, string[]>;
501
+ /** How many hands may hold this card at once (schema 3.5): integer >= 1,
502
+ * default 1. One copy is the exclusivity rule; copies: N is the
503
+ * deliberate opt-out for interchangeable filler. Always counted WITHIN a
504
+ * flow, whether or not the card is shared. */
505
+ copies?: number;
506
+ /** Scarcity across flows (design/shared-scarcity.md). Absent takes the
507
+ * deck's flag; set here it overrides the deck, so a single unique card can
508
+ * stay in the content it belongs to. A shared card's claims count every
509
+ * flow's board, and a shared `redraw: "never"` is spent for everyone the
510
+ * first time anyone plays it.
511
+ *
512
+ * A finite `redraw` stays PER FLOW even when shared: a cooldown is an
513
+ * absolute turn of the card's box clock and clocks are per flow, so
514
+ * "3 turns of whose clock?" has no answer. A world-wide timer is a @world
515
+ * question, not an engine one (shared-scarcity 9.3.3). */
516
+ shared?: boolean;
517
+ /** The world cap: how many hands ACROSS EVERY FLOW may hold this at once.
518
+ * Read only when the card is shared, and defaults to `copies`, so the
519
+ * common case writes one number and "five in the world, one to a customer"
520
+ * is `copies: 1, sharedCopies: 5`. */
521
+ sharedCopies?: number;
522
+ /** Card-template data: field name -> value, validated at publish. */
523
+ fields?: Record<string, ScalarValue>;
524
+ outcomes: Outcome<E>[];
525
+ }
526
+ interface Deck<E> {
527
+ id: string;
528
+ gameId?: string;
529
+ title?: string;
530
+ purpose?: string;
531
+ /** The deck gate, evaluated once per draw in the draw's environment. */
532
+ condition?: E;
533
+ /** This pile is scarce across flows (design/shared-scarcity.md): every card
534
+ * in it is shared unless the card says otherwise. The container is where
535
+ * Patter puts its own shared-memory flag, and the deck is our container. */
536
+ shared?: boolean;
537
+ properties: PropertyDecl[];
538
+ cards: Card<E>[];
539
+ }
540
+ interface Tag {
541
+ id: string;
542
+ gameId?: string;
543
+ /**
544
+ * This tag's own starting values for properties its GROUP declares
545
+ * (design/hand-typing.md). The group says what the property IS; a tag says
546
+ * only where it starts, so "every zone has a haunting level" is written once
547
+ * and "the cave starts at 2" is written where it belongs.
548
+ *
549
+ * A name here that the group does not declare is an error: it would be a
550
+ * value for nothing.
551
+ */
552
+ values?: Record<string, ScalarValue>;
553
+ /** Authored display order (sparse; one without it falls back to its id
554
+ * position). Merges as a per-item value, so id-sorted storage stays
555
+ * merge-clean (Reboot 7.4). */
556
+ order?: number;
557
+ properties?: PropertyDecl[];
558
+ /** Template-of-play extras (e.g. spatial geometry). Source only: preserved
559
+ * in shards, never compiled into the bundle. */
560
+ templates?: Record<string, unknown>;
561
+ }
562
+ /** A named axis for cross-cutting cards (schema 2.4, renamed from
563
+ * Dimension). Tags are declared, not freeform. */
564
+ interface TagGroup {
565
+ id: string;
566
+ gameId?: string;
567
+ purpose?: string;
568
+ /**
569
+ * A property reference (`"@story.act"`) whose value names a tag in this group
570
+ * by gameId. The engine reads it at every ask and binds the group, exactly as
571
+ * if the asking hand had chosen that tag; a hand's own binding wins.
572
+ *
573
+ * For an axis driven by STATE rather than by place: acts, chapters, a
574
+ * difficulty band. Without it, only a hand can bind a group, so such an axis
575
+ * had nowhere to gate and every card needed its own condition.
576
+ *
577
+ * A reference rather than an expression on purpose (design/where-and-
578
+ * selectors.md Part B): a computed binding belongs in a property the outcomes
579
+ * maintain, and an expression here would make this type generic for no gain.
580
+ */
581
+ boundBy?: string;
582
+ /**
583
+ * What omitting this group means for a card. Default false: omission is a
584
+ * wildcard, so the card matches whatever the group is bound to. True inverts
585
+ * it, so a card that names no tag here is unavailable wherever the group IS
586
+ * bound (and unaffected where it is not).
587
+ *
588
+ * `place` is the built-in instance of this pair: bound to the asking hand,
589
+ * and inverted per card rather than per group.
590
+ */
591
+ required?: boolean;
592
+ /** Authored display order (sparse; one without it falls back to its id
593
+ * position). Merges as a per-item value, so id-sorted storage stays
594
+ * merge-clean (Reboot 7.4). */
595
+ order?: number;
596
+ /**
597
+ * Properties EVERY tag in this group has (design/hand-typing.md). The
598
+ * declaration lives here and each tag carries only its own starting value in
599
+ * `Tag.values`, which is the separation the format was missing: a tag's own
600
+ * `properties` entry has to restate the type on every tag purely in order to
601
+ * say the value, and a tag added later silently arrives without it.
602
+ *
603
+ * Compiled by FLATTENING onto each tag, so the bundle keeps its per-tag
604
+ * shape and no runtime, port or bundle schema changes: source is where the
605
+ * author works and where merges happen, the bundle is a compiled artefact
606
+ * that can afford to be explicit.
607
+ *
608
+ * A tag may still declare its own `properties` for a group whose tags
609
+ * genuinely differ. Declaring the same NAME both ways is an error.
610
+ */
611
+ properties?: PropertyDecl[];
612
+ tags: Tag[];
613
+ /** Template-of-play extras for the GROUP, the same bag its tags carry: this is
614
+ * where a group is marked spatial and where that template keeps its own
615
+ * group-level configuration. Source only, preserved but never compiled.
616
+ *
617
+ * A bag rather than a `spatial: true` flag because the marker and the
618
+ * configuration are one thing (see model/spatial.ts), and because core is not
619
+ * meant to grow a field per template of play. */
620
+ templates?: Record<string, unknown>;
621
+ }
622
+ /** The reserved tag group (schema 2.4): present in every box without
623
+ * declaration, its tags the box's hand ids. Every hand implicitly binds it to
624
+ * itself; a card that names a place is available only at that place.
625
+ *
626
+ * Called `place` rather than `home` since 2026-08-21: one word for one thing
627
+ * across the format, the editor and the exports. "Where" is the QUESTION a
628
+ * card answers (at a place, or anywhere in a region); "place" is the direct
629
+ * half of that answer. `home` was a metaphor an author had to learn, and it
630
+ * leaked into hand-edited shards and the docs. */
631
+ declare const PLACE_GROUP = "place";
632
+ /** A declared kind of hand (schema 2.6): live-inherited, author-side only,
633
+ * never called from game code. One condition governs every instance. */
634
+ interface HandTemplate<E> {
635
+ id: string;
636
+ gameId?: string;
637
+ title?: string;
638
+ purpose?: string;
639
+ /** Authored display order (sparse; one without it falls back to its id
640
+ * position). Merges as a per-item value, so id-sorted storage stays
641
+ * merge-clean (Reboot 7.4). */
642
+ order?: number;
643
+ /** Fixed tag bindings: tag group id -> tag id. */
644
+ bindings?: Record<string, string>;
645
+ /** The holes: tag group ids each instance fills (one tag each). */
646
+ chooses?: string[];
647
+ /** Shared availability condition, ANDed in (schema 3.1); evaluated per
648
+ * instance against that instance's composed @hand. */
649
+ condition?: E;
650
+ /** Default slot cap. */
651
+ slots: number | "unbounded";
652
+ /** Declared @hand state every instance carries. */
653
+ properties: PropertyDecl[];
654
+ }
655
+ /** A standalone hand's inline rule (schema 2.6): owned by the hand. */
656
+ interface HandRule<E> {
657
+ bindings?: Record<string, string>;
658
+ condition?: E;
659
+ slots: number | "unbounded";
660
+ }
661
+ /** A hand (schema 2.6): a template instance (template + chosen) or a
662
+ * standalone hand (rule). Exactly one of template / rule. Fully concrete:
663
+ * deal is name-only. */
664
+ interface Hand<E> {
665
+ id: string;
666
+ /** The name deal() is called with; a rename is a breaking change
667
+ * (Reboot 7.4). */
668
+ gameId?: string;
669
+ title?: string;
670
+ purpose?: string;
671
+ /** Hand template id (not gameId). */
672
+ template?: string;
673
+ /** Template instances: tag group id -> tag id, one per `chooses` hole. */
674
+ chosen?: Record<string, string>;
675
+ /** Standalone hands: the inline rule. */
676
+ rule?: HandRule<E>;
677
+ /** Override; defaults to the template's / rule's slots. The ONLY template
678
+ * field an instance may override (schema 2.6). */
679
+ slots?: number;
680
+ /** Standalone hands' own @hand state (template instances inherit the
681
+ * template's declarations). */
682
+ properties?: PropertyDecl[];
683
+ /** Authored display order within the box (sparse; authoring-only, never
684
+ * compiled into the bundle - the compiler's explicit field list drops it). */
685
+ order?: number;
686
+ /** Template-of-play extras (e.g. a spatial pin). Source only. */
687
+ templates?: Record<string, unknown>;
688
+ }
689
+ interface Box<E> {
690
+ id: string;
691
+ gameId?: string;
692
+ title?: string;
693
+ purpose?: string;
694
+ /** The only per-box ranking policy (Reboot 2.2). */
695
+ ranking: {
696
+ specificity: boolean;
697
+ };
698
+ /** The card template: what every card in this box carries. */
699
+ fields: FieldDecl[];
700
+ properties: PropertyDecl[];
701
+ tagGroups: TagGroup[];
702
+ decks: Deck<E>[];
703
+ handTemplates: HandTemplate<E>[];
704
+ hands: Hand<E>[];
705
+ }
706
+ declare const BUNDLE_SCHEMA = "storylets/bundle@0";
707
+ /** Binds bundles to shards (staleness gate) and saves to bundles. */
708
+ interface BundleContent {
709
+ project: string;
710
+ version: string;
711
+ /** hash32 over the canonical source shards (schema 2.8). */
712
+ hash: string;
713
+ }
714
+ interface BundleSettings {
715
+ playAdvancesTurns: number;
716
+ }
717
+ /**
718
+ * A map that a bundle was asked to carry: one spatial tag group's geometry,
719
+ * flattened for a host to draw (design/graphical-views.md 2, "The map MAY ship").
720
+ *
721
+ * INERT PAYLOAD. Nothing in the engine reads this and nothing ever will: the
722
+ * runtime deals in tag names. It is here so a host that wants an in-game map does
723
+ * not have to invent its own export, and it is absent unless the project asked
724
+ * for it (`export.map`), so a build that does not want a map carries no bytes.
725
+ *
726
+ * GAME IDS throughout, never internal ids. Internal ids are authoring identity
727
+ * and mean nothing outside the project; a host matches these against the same
728
+ * names it passes to `peek`. There is nothing here to strip either, which is why
729
+ * `metadata: "stripped"` needs no special case: no titles, no purposes.
730
+ *
731
+ * Sites are deliberately NOT here. A site is where an author parked a hand while
732
+ * working, held in the view sidecar precisely because it is not content, and a
733
+ * host that wants to place a hand already has its zone from the compiled binding.
734
+ */
735
+ interface BundleMap {
736
+ /** The owning box, by gameId (tag groups are box-scoped). */
737
+ box: string;
738
+ /** The tag group this is a map of, by gameId. */
739
+ group: string;
740
+ /** One entry per zone that has been drawn; a tag with no polygon is not a
741
+ * place yet and is left out rather than shipped as an empty shape. */
742
+ zones: {
743
+ tag: string;
744
+ polygon: ViewPoint[];
745
+ }[];
746
+ /** Background pictures, back to front, as bundle-relative paths. Hidden ones
747
+ * do not ship: what an author put away is not something to spring on a host. */
748
+ backgrounds?: BundleBackground[];
749
+ }
750
+ /** One shipped picture. `locked` and `hidden` are authoring state and do not
751
+ * travel; the draw order is the array order. */
752
+ interface BundleBackground {
753
+ /** Where the file sits relative to the bundle ("assets/<box>/<file>"). */
754
+ file: string;
755
+ x: number;
756
+ y: number;
757
+ width: number;
758
+ height: number;
759
+ opacity?: number;
760
+ }
761
+ interface Bundle {
762
+ schema: typeof BUNDLE_SCHEMA;
763
+ content: BundleContent;
764
+ metadata: "full" | "stripped";
765
+ settings: BundleSettings;
766
+ world: {
767
+ properties: PropertyDecl[];
768
+ /** ScopeRegistrySpec (@wildwinter/scoperegistry): the owned/foreign
769
+ * split. Absent = engine-owned @world (standalone play). */
770
+ registry?: unknown;
771
+ };
772
+ story: {
773
+ properties: PropertyDecl[];
774
+ };
775
+ boxes: Box<Expression>[];
776
+ /** Maps, when the project asked for them. Absent is the normal state. */
777
+ maps?: BundleMap[];
778
+ }
779
+ declare const SAVE_SCHEMA = "storylets/save@1";
780
+ interface PlayRecord {
781
+ /** Card and outcome by gameId (feeds the play-history functions). */
782
+ card: string;
783
+ outcome: string;
784
+ turn: number;
785
+ }
786
+ /** A property bag: name -> value. */
787
+ type PropertyBag = Record<string, ScalarValue>;
788
+ /** The per-scope property partitions one side of the sharing flag holds:
789
+ * a save carries one of these for the shared values and one per flow
790
+ * (design/flows.md). NO world key, in either: @world is the game's own
791
+ * state, resolved through the world resolver and saved by whoever owns
792
+ * it - "host saves its container once, each engine saves its own
793
+ * envelope" (engine-runtimes.md 3.1). */
794
+ interface PropsPartition {
795
+ story: PropertyBag;
796
+ box: Record<string, PropertyBag>;
797
+ deck: Record<string, PropertyBag>;
798
+ hand: Record<string, PropertyBag>;
799
+ /** Tag state, keyed by tag id. */
800
+ value: Record<string, PropertyBag>;
801
+ }
802
+ /** One flow's snapshot inside the envelope (schema 4). */
803
+ interface FlowSave {
804
+ /** The per-flow property partitions. */
805
+ props: PropsPartition;
806
+ /** Per-box turn counters, keyed by box id (schema 3.4) - per flow: there
807
+ * is deliberately no global turn. */
808
+ turns: Record<string, number>;
809
+ /** mulberry32 state, uint32 (schema 3.3), per flow. */
810
+ prng: number;
811
+ /** Absolute next-eligible turn (of the card's box's clock) per card id;
812
+ * MAX_SAFE_INTEGER = never (deliberately not Infinity, which
813
+ * JSON-serialises to null). */
814
+ cooldowns: Record<string, number>;
815
+ /** Hand contents (card ids, in dealt order), keyed by hand id. The claims
816
+ * ledger is derived from this (schema 3.5). */
817
+ board: Record<string, string[]>;
818
+ playLog: PlayRecord[];
819
+ }
820
+ /** The whole engine, one envelope: the shared partitions once, then every
821
+ * live flow keyed by its id - Patter's shape (one shared blob + N flow
822
+ * blobs; multi-flow and save/load are the same feature). */
823
+ /** The engine's half of a save: what every flow shares. Properties, and the
824
+ * cards a shared `redraw: "never"` has taken out of the world for good
825
+ * (design/shared-scarcity.md). Claims are NOT here: they are derived from the
826
+ * live boards, and each flow's board rides its own blob. */
827
+ interface SharedSave {
828
+ props: PropsPartition;
829
+ /** Card ids, sorted, so a save is byte-stable for a diff. */
830
+ spent: string[];
831
+ }
832
+ interface SaveEnvelope {
833
+ schema: typeof SAVE_SCHEMA;
834
+ content: BundleContent;
835
+ shared: SharedSave;
836
+ flows: Record<string, FlowSave>;
837
+ }
838
+ /** The .storyletsave FILE: the HOST's file, not the engine's - the engine's
839
+ * envelope plus, when the host keeps one, its @world container. This is
840
+ * "host saves its container once, each engine saves its own envelope"
841
+ * folded into one file for the single-host case; the ENGINE never reads or
842
+ * writes `world` (loadGame takes the envelope alone). */
843
+ declare const SAVEFILE_SCHEMA = "storylets/savefile@1";
844
+ interface SaveFile {
845
+ schema: typeof SAVEFILE_SCHEMA;
846
+ engine: SaveEnvelope;
847
+ /** The host's @world values, saved and restored by the host. */
848
+ world?: PropertyBag;
849
+ }
850
+ /** The project folder: a macOS package, a plain folder elsewhere. */
851
+ declare const PROJECT_FOLDER_EXTENSION = ".storylets";
852
+ /** The compiled bundle (strict JSON; generated, never hand-edited). */
853
+ declare const BUNDLE_EXTENSION = ".storyletsc";
854
+ /**
855
+ * Where a shipped background sits, relative to the bundle file.
856
+ *
857
+ * One function so the compiler (which writes the name into the bundle) and the
858
+ * export op (which writes the bytes) cannot drift apart: a path agreed in two
859
+ * places is a path that eventually disagrees. Per BOX, because two boxes may
860
+ * each have their own `plan.png` and a build must not silently keep one of them.
861
+ */
862
+ declare const bundleAssetPath: (boxGameId: string, file: string) => string;
863
+ /** Per-type shard extensions, JSON5 inside (source doc section 2). */
864
+ declare const SHARD_EXTENSIONS: {
865
+ readonly project: ".storyletproj";
866
+ readonly box: ".storyletbox";
867
+ readonly tags: ".storylettags";
868
+ readonly hands: ".storylethands";
869
+ readonly deck: ".storyletdeck";
870
+ /** The arrangement layer: where things SIT, never what they are. Its own shard
871
+ * because positions churn (an afternoon of tidying a canvas touches every
872
+ * card) and content does not, so a designer arranging and a writer editing
873
+ * never collide on one file (design/graphical-views.md section 1.2). */
874
+ readonly view: ".storyletview";
875
+ /** Threaded comments: content-ADJACENT, so neither in a content shard (a
876
+ * writer's deck edit must not conflict with a reviewer's comment) nor in the
877
+ * arrangement sidecar (this is not where anything sits). One per box,
878
+ * id-keyed (design/annotation.md). Documentation NOTES used to share this
879
+ * file and were retired: `purpose` already says why a thing exists, and
880
+ * Patterpad's typed routing has no destination here. */
881
+ readonly notes: ".storyletnotes";
882
+ };
883
+ declare const PROJECT_SCHEMA = "storylets/project@0";
884
+ declare const BOX_SCHEMA = "storylets/box@0";
885
+ declare const TAGS_SCHEMA = "storylets/tags@0";
886
+ declare const HANDS_SCHEMA = "storylets/hands@0";
887
+ declare const DECK_SCHEMA = "storylets/deck@0";
888
+ declare const VIEW_SCHEMA = "storylets/view@0";
889
+ /** The comment sidecar's schema. Still called "notes" on disk: the file already
890
+ * held both, and renaming it would break every project for no gain. */
891
+ declare const NOTES_SCHEMA = "storylets/notes@0";
892
+ /** A point in a canvas's own coordinates. */
893
+ interface ViewPoint {
894
+ x: number;
895
+ y: number;
896
+ }
897
+ /**
898
+ * Canvas furniture: what an author draws AROUND the content to make sense of it
899
+ * (design/graphical-views.md 3, "Frames and sites").
900
+ *
901
+ * Both canvases carry the same thing, which is why they share a type: a node
902
+ * canvas and a map are different views of different material, but "put a box
903
+ * round this lot and call it act two" is the same thought on either.
904
+ *
905
+ * It lives in the view sidecar because it is ARRANGEMENT. Nothing here is
906
+ * content: no runtime reads it, no bundle carries it, and deleting the sidecar
907
+ * loses only the drawing. Threaded comments are the
908
+ * other thing entirely - they attach to entities and they travel - but a canvas
909
+ * DRAWS their markers, while owning none of them.
910
+ */
911
+ interface CanvasFurniture {
912
+ /** Titled areas behind the content, back to front (see `stacked`).
913
+ *
914
+ * There was a second kind, a `stickies` list, retired on 2026-08-10
915
+ * (design/annotation.md): a dropped comment marker does the same job in a
916
+ * fraction of the space, and an annotation that takes as much room as the
917
+ * thing it is about is a bad trade on a canvas. */
918
+ frames?: Frame[];
919
+ }
920
+ /**
921
+ * A titled area behind a group of things: Unreal's comment box.
922
+ *
923
+ * Deliberately dumb about what is inside it. It has no membership list and
924
+ * computes none: a frame is a thing an author DREW, and the cards under it are
925
+ * whatever happens to be under it now. That is what keeps it honest when content
926
+ * moves, and it is the same reasoning that keeps a zone's sites out of the map's
927
+ * sidecar.
928
+ */
929
+ interface Frame extends ViewPoint {
930
+ id: string;
931
+ w: number;
932
+ h: number;
933
+ /** Shown in the frame's bar, and the handle it is dragged by. */
934
+ title?: string;
935
+ /** One of the furniture palette's names (see `FURNITURE_COLOURS`); the theme
936
+ * decides what that looks like, so a frame does not carry a hex value that
937
+ * would fight the palette on the day somebody switches theme. */
938
+ colour?: string;
939
+ /** Place in the frame band (sparse, `stacked`). Frames can nest. */
940
+ z?: number;
941
+ }
942
+ /** The furniture palette: names, not colours. The theme maps them, so the same
943
+ * shard reads correctly on linen and on baize. */
944
+ declare const FURNITURE_COLOURS: readonly ["paper", "amber", "sage", "sky", "rose", "slate"];
945
+ type FurnitureColour = typeof FURNITURE_COLOURS[number];
946
+ /** One deck's node canvas: where its cards sit, and the furniture around them.
947
+ * Sparse throughout. A card with no entry lays out by default, and an entry for
948
+ * a card that no longer exists is inert, so there is no referential integrity to
949
+ * maintain against content that moves underneath. */
950
+ interface DeckCanvas extends CanvasFurniture {
951
+ /** Keyed by CARD id. */
952
+ cards?: Record<string, ViewPoint>;
953
+ }
954
+ /** The box's map: where its hands sit in space, and the furniture around them. */
955
+ interface BoxMap extends CanvasFurniture {
956
+ /** Keyed by HAND id. WHERE a site is, and nothing else.
957
+ *
958
+ * Which zone it is IN is not recorded here, and deliberately (2026-08-06,
959
+ * with the rebinding drag): a hand that binds a zone already says so in its
960
+ * own shard, as `chosen` or as a rule binding, and that is the truth the
961
+ * runtime deals from. A copy here could only ever go on to disagree with it,
962
+ * and a site whose recorded zone contradicts the hand it stands for would be
963
+ * the most misleading thing on the map.
964
+ *
965
+ * Called `pins` until 2026-08-10 (design/annotation.md). No compatibility
966
+ * branch: the only projects that exist are the examples in this repo, and they
967
+ * were edited. */
968
+ sites?: Record<string, ViewPoint>;
969
+ }
970
+ /** The arrangement layer for one box: where things SIT, never what they are.
971
+ *
972
+ * Its own shard on purpose (design/graphical-views.md section 1.2). Positions
973
+ * churn, content does not: an afternoon of tidying a canvas touches every card,
974
+ * and if that lived in the deck shard then a designer arranging and a writer
975
+ * editing card text would collide on one file all day, while a content review
976
+ * would be full of coordinates. Keyed by id throughout so the existing merge
977
+ * engine handles two designers rearranging different things without a conflict.
978
+ *
979
+ * Source-only. It never reaches the compiled bundle, exactly as `order` does
980
+ * not: the compiler reads the fields it names and this is not among them. */
981
+ interface ViewShard {
982
+ schema: typeof VIEW_SCHEMA;
983
+ /** Keyed by DECK id: one node canvas each. */
984
+ canvases?: Record<string, DeckCanvas>;
985
+ map?: BoxMap;
986
+ }
987
+ /** A coverage input driver: during a coverage run the harness feeds a
988
+ * host-seam property (`@world.x`) values from `values`, so content gated on
989
+ * external state gets exercised (Patter's coverageDrivers, carried whole). */
990
+ interface CoverageDriver {
991
+ /** "initial": set once as each playthrough starts. "recurring": re-rolled
992
+ * per turn at the cadence, so one run passes through several states. */
993
+ kind: "initial" | "recurring";
994
+ /** For recurring drivers: how often to re-roll per turn (default "sometimes"). */
995
+ cadence?: "rarely" | "sometimes" | "often";
996
+ /** The pool the harness picks from (uniform). Empty = inert. */
997
+ values: ScalarValue[];
998
+ }
999
+ /** Authoring-side coverage configuration (never compiled into the bundle). */
1000
+ interface CoverageConfig {
1001
+ /** Property drivers, keyed by ref ("@world.danger"). */
1002
+ drivers?: Record<string, CoverageDriver>;
1003
+ }
1004
+ interface ProjectShard {
1005
+ schema: typeof PROJECT_SCHEMA;
1006
+ project: {
1007
+ id: string;
1008
+ name: string;
1009
+ version: string;
1010
+ };
1011
+ settings: BundleSettings;
1012
+ /** Coverage drivers + argument domains (authoring/testing config; stays
1013
+ * out of the compiled bundle). */
1014
+ coverage?: CoverageConfig;
1015
+ /** Validation switches (authoring config; never compiled). Off is written
1016
+ * as ABSENT, like `export.map`: a shard says what an author chose. */
1017
+ validation?: {
1018
+ /** Also warn when state is WRITTEN but nothing reads it. Off by default:
1019
+ * cards are routinely written ahead of the content that will read them,
1020
+ * so mid-development this warning is mostly noise. The read side (a gate
1021
+ * on state nothing writes) always warns, because that kills cards now. */
1022
+ warnUnreadWrites?: boolean;
1023
+ };
1024
+ world: {
1025
+ properties: PropertyDecl[];
1026
+ registry?: unknown;
1027
+ };
1028
+ story: {
1029
+ properties: PropertyDecl[];
1030
+ };
1031
+ /** Templates of play: configuration bags keyed by template name. Core
1032
+ * validates only what it knows. */
1033
+ templates: Record<string, unknown>;
1034
+ export: {
1035
+ bundle: string;
1036
+ metadata: "full" | "stripped";
1037
+ /**
1038
+ * Does a `.storyletpack` carry the boxes' binary assets (background images)?
1039
+ *
1040
+ * Default false, and a project-level DEFAULT rather than a rule: a pack is a
1041
+ * delivery, so the caller can override it per pack (2026-08-07). Some
1042
+ * projects would benefit from sending their pictures in certain
1043
+ * circumstances and others never would, which is why neither "always" nor
1044
+ * "never" is the answer.
1045
+ *
1046
+ * Nothing to do with the compiled bundle, which has its own switch: `map`.
1047
+ */
1048
+ packAssets?: boolean;
1049
+ /**
1050
+ * Does the compiled bundle carry the maps (zone shapes and background
1051
+ * pictures)?
1052
+ *
1053
+ * Default false, and the default matters: geometry is authoring data, the
1054
+ * runtime deals in tag names, and a shipping build should carry nothing it
1055
+ * does not use. But a host that wants an in-game map should not have to
1056
+ * invent its own export, and it is most useful early - a prototype with a
1057
+ * real map beats a prototype with a list of zone names.
1058
+ *
1059
+ * It sits beside `metadata` on purpose: that is already the switch for
1060
+ * "authoring data that may or may not ship", and this is its sibling rather
1061
+ * than a new concept. With it on, `export` also writes the background files
1062
+ * next to the bundle, and `describeBundle` says what is in there.
1063
+ */
1064
+ map?: boolean;
1065
+ };
1066
+ }
1067
+ interface BoxShard {
1068
+ schema: typeof BOX_SCHEMA;
1069
+ box: {
1070
+ id: string;
1071
+ gameId?: string;
1072
+ title?: string;
1073
+ purpose?: string;
1074
+ /** Authored display order among boxes (sparse; absent falls back to the
1075
+ * folder-name position). Authoring-only, like a card's (never compiled
1076
+ * into the bundle); merges as a per-field value. */
1077
+ order?: number;
1078
+ ranking: {
1079
+ specificity: boolean;
1080
+ };
1081
+ fields: FieldDecl[];
1082
+ properties: PropertyDecl[];
1083
+ };
1084
+ }
1085
+ /** The box's tag groups: how its cards are filed. */
1086
+ interface TagsShard {
1087
+ schema: typeof TAGS_SCHEMA;
1088
+ groups: TagGroup[];
1089
+ }
1090
+ /** The box's hand templates + hands (the writer/programmer contract). */
1091
+ interface HandsShard {
1092
+ schema: typeof HANDS_SCHEMA;
1093
+ templates: HandTemplate<string>[];
1094
+ hands: Hand<string>[];
1095
+ }
1096
+ interface DeckShard {
1097
+ schema: typeof DECK_SCHEMA;
1098
+ deck: {
1099
+ id: string;
1100
+ gameId?: string;
1101
+ title?: string;
1102
+ purpose?: string;
1103
+ condition?: string;
1104
+ /** Scarce across flows: see Deck.shared. */
1105
+ shared?: boolean;
1106
+ /** Authored display order within the box (sparse; see BoxShard). */
1107
+ order?: number;
1108
+ properties: PropertyDecl[];
1109
+ };
1110
+ cards: Card<string>[];
1111
+ }
1112
+
1113
+ export { BOX_SCHEMA, BUNDLE_EXTENSION, BUNDLE_SCHEMA, type Box, type BoxMap, type BoxShard, type Bundle, type BundleBackground, type BundleContent, type BundleMap, type BundleSettings, type CanvasFurniture, type Card, type Comment, type CommentMark, type CommentMessage, type CoverageConfig, type CoverageDriver, DECK_SCHEMA, type Deck, type DeckCanvas, type DeckShard, FURNITURE_COLOURS, type FieldDecl, type FlowSave, type Frame, type FurnitureColour, HANDS_SCHEMA, type Hand, type HandBinding, type HandRule, type HandTemplate, type HandsShard, NOTES_SCHEMA, type NotesShard, type Outcome, PLACE_GROUP, PROJECT_FOLDER_EXTENSION, PROJECT_SCHEMA, type PlayRecord, type Polygon, type ProjectShard, type PropertyBag, type PropertyDecl, type PropertyType, type PropsPartition, type Rect, type RedrawPolicy, SAVEFILE_SCHEMA, SAVE_SCHEMA, SHARD_EXTENSIONS, SPATIAL, type SaveEnvelope, type SaveFile, type SharedSave, type SpatialBackground, type SpatialGroup, type StackMove, type Stacked, TAGS_SCHEMA, type Tag, type TagGroup, type TagsShard, VIEW_SCHEMA, type ViewPoint, type ViewShard, backgroundsOf, bindHand, bundleAssetPath, byDisplayOrder, centroid, commentsOf, droppedRect, effectiveGameId, framesOf, freeGameId, freeTitle, gameIdify, handBinding, inferDeclFromWrite, isSpatial, isValidGameId, labelPoint, markOf, marksOn, openThreadCounts, pointInPolygon, polygonBounds, polygonOf, restack, spatialOf, stacked, threadsFor, unbindHand, withBackgrounds, withPolygon, withSpatialGroup, withZ, zOf, zoneAt, zonesAt };