@storylet-studio/runtime 0.2.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,656 @@
1
+ import { ScalarValue, ScopeResolver, ExprNode } from '@wildwinter/expr';
2
+ export { Prng, makePrng, shuffleInPlace } from '@wildwinter/expr';
3
+ import { Bundle, Card, Expression, Deck, Box, Hand, HandTemplate, TagGroup, PropertyDecl, FlowSave, SaveEnvelope, PropertyType, ScalarValue as ScalarValue$1 } from '@storylet-studio/model';
4
+ import { PropertyBag, PropertyRow } from '@wildwinter/scoperegistry';
5
+
6
+ interface EngineOptions {
7
+ /** Default seed for each flow's PRNG; override per flow in openFlow
8
+ * (cross-runtime determinism, schema 3.3). Default 0. */
9
+ seed?: number;
10
+ /** Retain each flow's event log for introspection - the game-engine seam
11
+ * (schema 5): every trace event, sequence-stamped and turn-stamped where
12
+ * the event has a box context. `true` keeps the default 1000 entries
13
+ * (oldest dropped first). Off by default; subscribeTrace stays the
14
+ * zero-retention stream. */
15
+ log?: boolean | {
16
+ cap?: number;
17
+ };
18
+ /**
19
+ * The host's resolver for @world - the values the game owns and the
20
+ * story reads (and, where `set` is offered, writes). Omit it and the
21
+ * engine self-backs @world from the declared defaults. Engine-level,
22
+ * shared by all flows, never in saveGame(): the host saves its container
23
+ * once, each engine saves its own envelope (design/flows.md).
24
+ */
25
+ world?: ScopeResolver;
26
+ }
27
+ interface OpenFlowOptions {
28
+ /** Seed for this flow's PRNG (defaults to the engine's `seed`). */
29
+ seed?: number;
30
+ }
31
+ /** A card view in a dealt hand or a peeked list. Carries NO outcome
32
+ * availability - ask `outcomes()` for current truth (schema 5). */
33
+ interface DealtCard {
34
+ id: string;
35
+ gameId: string;
36
+ title?: string;
37
+ purpose?: string;
38
+ fields?: Record<string, ScalarValue>;
39
+ }
40
+ interface OutcomeView {
41
+ id: string;
42
+ gameId: string;
43
+ title?: string;
44
+ purpose?: string;
45
+ /** Evaluated against CURRENT state at the moment of the ask. */
46
+ available: boolean;
47
+ }
48
+ /** What a peek returns: the top of the stock, looked at and put back.
49
+ * The engine has no pick policy (Reboot 2.1). */
50
+ interface RankedList {
51
+ box: string;
52
+ cards: DealtCard[];
53
+ }
54
+ interface PlayOptions {
55
+ /** Turn advance override; default settings.playAdvancesTurns. */
56
+ advanceTurns?: number;
57
+ }
58
+ /** Why a card did or did not make an ask, in availability order (schema 3.1). */
59
+ type TraceVerdict = "dealt" | "capped" | "cooldown" | "deck-gate" | "tags" | "condition" | "priority" | "claimed" | "claimed-elsewhere" | "taken";
60
+ /** One event on the deal/play log - "why did Ambush at the ford get dealt
61
+ * here?" is answered by the ask event's per-card verdicts and keys. The
62
+ * verb is the event type, so a peek is distinguishable from a deal when
63
+ * reading a run back. */
64
+ type TraceEvent = {
65
+ type: "deal";
66
+ /** Hand gameId. */
67
+ hand: string;
68
+ cards: {
69
+ id: string;
70
+ verdict: TraceVerdict;
71
+ priority?: number;
72
+ specificity?: number;
73
+ }[];
74
+ } | {
75
+ type: "peek";
76
+ /** Box gameId. */
77
+ box: string;
78
+ criteria: Record<string, string>;
79
+ cards: {
80
+ id: string;
81
+ verdict: TraceVerdict;
82
+ priority?: number;
83
+ specificity?: number;
84
+ }[];
85
+ } | {
86
+ type: "evict";
87
+ hand: string;
88
+ card: string;
89
+ reason: TraceVerdict | "hand-condition" | "vanished";
90
+ } | {
91
+ type: "play";
92
+ card: string;
93
+ outcome: string;
94
+ turn: number;
95
+ }
96
+ /** One landed outcome change; `path` is the resolved store location (a
97
+ * routed @hand write shows where it actually went, schema 3.6). `prev`
98
+ * is the value it replaced, so a log can read "0 -> 1". */
99
+ | {
100
+ type: "write";
101
+ target: string;
102
+ path: string;
103
+ value: ScalarValue;
104
+ prev?: ScalarValue;
105
+ }
106
+ /** An explicit clock advance via advanceTurns (schema 3.4); `turn` is the
107
+ * box's new value. Plays stamp their own turn on the play event. */
108
+ | {
109
+ type: "turns";
110
+ box: string;
111
+ turn: number;
112
+ }
113
+ /** An expression eval error: never a silent pass (schema 3.1), always a
114
+ * visible diagnostic. */
115
+ | {
116
+ type: "diagnostic";
117
+ where: string;
118
+ message: string;
119
+ };
120
+ type TraceHandler = (event: TraceEvent) => void;
121
+ /** The engine-level tap: every flow's events, tagged with the flow id -
122
+ * the tools' one stream. */
123
+ type EngineTraceHandler = (flow: string, event: TraceEvent) => void;
124
+ /** A retained log entry: the trace event plus its place in flow time.
125
+ * `seq` orders the whole flow (monotonic; survives clearLog). `turn` is
126
+ * the clock of the box the event happened in when it fired (peek: the box;
127
+ * deal/evict: the hand's box; play and its writes: the played card's box,
128
+ * stamped together with the play's own turn). Diagnostics carry no turn. */
129
+ type LogEntry = TraceEvent & {
130
+ seq: number;
131
+ turn?: number;
132
+ };
133
+ /** One entry on the ENGINE's log: the same event, plus the flow it happened
134
+ * in. A run is several flows over shared state, so "what happened in this
135
+ * run" is only answerable in one ordered stream, and only if each line says
136
+ * who. The flow's own log stays flow-local and unchanged. */
137
+ type EngineLogEntry = LogEntry & {
138
+ flow: string;
139
+ };
140
+ interface CardEntry {
141
+ card: Card<Expression>;
142
+ deck: Deck<Expression>;
143
+ box: Box<Expression>;
144
+ }
145
+ /** One examiner row, addressed by the property-path grammar
146
+ * (getProperty / setProperty take the same `path`). */
147
+ interface PropertyView extends PropertyRow {
148
+ path: string;
149
+ }
150
+ /** One kernel bag with its store path prefix (story / box.<id> / deck.<id>
151
+ * / hand.<id> / value.<id>): the state logger's mount surface
152
+ * (design/engine-runtimes.md 3.4 - the logger builds on the PropertyBag
153
+ * audit hook, so it needs the bags themselves, not just their rows).
154
+ * The Engine lists the shared bags, a Flow its own; the @world container
155
+ * is the host's bag and the host mounts it itself. loadGame() replaces
156
+ * every bag, so re-enumerate after a load. */
157
+ interface BagMount {
158
+ prefix: string;
159
+ bag: PropertyBag;
160
+ }
161
+ /** One box on the enumeration surface (examiners, hosts): identity plus
162
+ * its clock (per flow). */
163
+ interface BoxView {
164
+ id: string;
165
+ gameId: string;
166
+ title?: string;
167
+ turn: number;
168
+ }
169
+ /** One side's five stores (shared on the engine, per-flow on each flow). */
170
+ interface Partition {
171
+ story: PropertyBag;
172
+ box: Map<string, PropertyBag>;
173
+ deck: Map<string, PropertyBag>;
174
+ hand: Map<string, PropertyBag>;
175
+ value: Map<string, PropertyBag>;
176
+ }
177
+ /** Everything a Flow shares with its Engine: the bundle-derived lookups
178
+ * (immutable), the shared stores (replaced wholesale by loadGame/reset),
179
+ * and the seams. One object, held by both classes - the two are one
180
+ * machine in two lifetimes. */
181
+ interface Internals {
182
+ bundle: Bundle;
183
+ logCap?: number;
184
+ cardsById: Map<string, CardEntry>;
185
+ cardsByGameId: Map<string, CardEntry>;
186
+ boxesByGameId: Map<string, Box<Expression>>;
187
+ boxesById: Map<string, Box<Expression>>;
188
+ handsById: Map<string, {
189
+ hand: Hand<Expression>;
190
+ box: Box<Expression>;
191
+ }>;
192
+ handsByGameId: Map<string, {
193
+ hand: Hand<Expression>;
194
+ box: Box<Expression>;
195
+ }>;
196
+ templatesById: Map<string, HandTemplate<Expression>>;
197
+ groupsById: Map<string, {
198
+ group: TagGroup;
199
+ box: Box<Expression>;
200
+ }>;
201
+ requiredGroups: Set<string>;
202
+ nodeCache: WeakMap<Expression, ExprNode>;
203
+ ladders: {
204
+ world: Map<string, readonly string[]>;
205
+ story: Map<string, readonly string[]>;
206
+ box: Map<string, Map<string, readonly string[]>>;
207
+ deck: Map<string, Map<string, readonly string[]>>;
208
+ value: Map<string, Map<string, readonly string[]>>;
209
+ hand: Map<string, Map<string, readonly string[]>>;
210
+ };
211
+ hasQualities: boolean;
212
+ /** Does ANY deck or card in the bundle opt into shared scarcity? False for
213
+ * the overwhelming majority of projects, and when it is false the two
214
+ * claim-ledger walks in dealing are skipped entirely. Same idea as
215
+ * `hasQualities` above: a bundle that does not use a feature must not pay
216
+ * for it. */
217
+ hasShared: boolean;
218
+ /** The per-flow halves of every declaration list, precomputed once: each
219
+ * new flow builds its bags from these. */
220
+ flowDecls: {
221
+ story: PropertyDecl[];
222
+ box: Map<string, PropertyDecl[]>;
223
+ deck: Map<string, PropertyDecl[]>;
224
+ hand: Map<string, PropertyDecl[]>;
225
+ value: Map<string, PropertyDecl[]>;
226
+ };
227
+ /** The shared stores. Reassigned wholesale by loadGame/reset. */
228
+ shared: Partition;
229
+ /** @world: the host's resolver, or the self-backed bag's. */
230
+ worldResolver: ScopeResolver;
231
+ /** `turn` is the box clock the event happened on, where the caller knows it
232
+ * - the same stamp the flow's own log carries. Unity and Unreal passed it
233
+ * from the start; JS and Godot dropped it, so their examiners printed "[-]"
234
+ * on every deal, peek, evict and write line while the other two printed the
235
+ * real turn. Four runtimes, two different run logs (2026-08-29). */
236
+ emitEngine: (flow: string, event: TraceEvent, turn?: number) => void;
237
+ engineTracing: () => boolean;
238
+ }
239
+ declare class Engine {
240
+ private readonly internals;
241
+ private readonly seed;
242
+ private readonly flowsById;
243
+ private readonly engineTraceHandlers;
244
+ /** The host's @world binding, if the engine was built with one: it
245
+ * outlives reset/loadGame (the host's container is the host's). The
246
+ * self-backed resolver is rebuilt instead. */
247
+ private readonly hostWorld?;
248
+ constructor(bundle: Bundle, opts?: EngineOptions);
249
+ /** Build the shared stores and the @world seam. `hostWorld` sticks for the
250
+ * engine's lifetime; reset/loadGame rebuild the shared bags around it. */
251
+ private initShared;
252
+ /** Quality ladders by scope for the eval channel (design/quality.md):
253
+ * world/story keyed by name; box/deck/value keyed by owner id then name.
254
+ * Built once - a bundle's declarations never change. Ladders are
255
+ * declaration-level, so the sharing flag does not touch them. */
256
+ private initLadders;
257
+ /** Open (or REPLACE) the named flow. An existing id's flow is closed
258
+ * first - re-opening a name is a reset of that name's whole per-flow
259
+ * state; shared state is untouched. There is no default flow: "main" is
260
+ * a caller convention, not an engine rule. */
261
+ openFlow(id: string, opts?: OpenFlowOptions): Flow;
262
+ getFlow(id: string): Flow | undefined;
263
+ /** Every live flow, open order. */
264
+ flows(): Flow[];
265
+ /** Close the named flow: its handle goes INERT (every verb throws). A
266
+ * dropped-but-held flow must not keep writing shared state (Patter's
267
+ * stale-handle lesson). Unknown ids are a quiet no-op, like closing a
268
+ * closed door. */
269
+ closeFlow(id: string): void;
270
+ /** @internal - Flow.close() routes here so both doors agree. */
271
+ dropFlow(id: string, flow: Flow): void;
272
+ /** Close every flow and reseed the shared state to its defaults (the
273
+ * self-backed @world included; a host-bound @world is the host's and is
274
+ * not touched). */
275
+ reset(): void;
276
+ /** Cards a shared `redraw: "never"` has taken out of the world, by card id.
277
+ * The claim ledger is DERIVED from live boards and so needs no storage;
278
+ * this one is durable, so it rides the save's shared half. */
279
+ private spent;
280
+ /** @internal */
281
+ isTaken(cardId: string): boolean;
282
+ /** @internal */
283
+ markTaken(cardId: string): void;
284
+ /** Every flow's events in one ordered stream, each tagged with its flow.
285
+ * Opt in with the same `log` option the flow logs use; capped the same way.
286
+ *
287
+ * This exists because a flow's own log cannot answer the question a run
288
+ * raises: when a story action in ANOTHER flow moves shared state, your
289
+ * flow's log says nothing and your value simply changes. Reading a run
290
+ * needs one stream that says who did what, and merging the per-flow logs by
291
+ * hand is a thing every host would otherwise have to write. */
292
+ private engineLog;
293
+ private engineSeq;
294
+ log(): readonly EngineLogEntry[];
295
+ clearLog(): void;
296
+ /** @internal - shared claims across every LIVE flow, card id -> holders.
297
+ * Derived, which is what makes closeFlow and the openFlow replace release
298
+ * what a flow was holding: its board leaves the map with it. */
299
+ sharedClaims(): Map<string, number>;
300
+ /**
301
+ * Read shared state by path: "world.x", "story.gold" (when shared),
302
+ * "box.b_x.heat" (when shared). A ref that resolves PER-FLOW throws,
303
+ * naming the fix - silently answering with some flow's copy (or a junk
304
+ * default) was the bug Patter's engine.getProperty guard exists to stop.
305
+ */
306
+ getProperty(path: string): ScalarValue;
307
+ setProperty(path: string, value: ScalarValue): void;
308
+ private resolveShared;
309
+ /** The shared surface as examiner rows: @world (read through the
310
+ * resolver) then the shared partitions. Per-flow rows live on each Flow. */
311
+ listProperties(): PropertyView[];
312
+ /** The SHARED kernel bags with their store path prefixes (the state
313
+ * logger's mount surface). The @world container is the host's own bag -
314
+ * the host mounts it itself. */
315
+ listBags(): BagMount[];
316
+ /** Every flow's trace, one stream, each event tagged with its flow id. */
317
+ subscribeTrace(handler: EngineTraceHandler): () => void;
318
+ /** The whole engine, one envelope: the shared partitions once, then
319
+ * every live flow keyed by its id. @world is NEVER here - the host
320
+ * saves its container, each engine saves its own envelope. */
321
+ saveGame(): SaveEnvelope;
322
+ /** Restore: shared state once, then every flow REBUILT from its blob.
323
+ * Handles held from before the load are closed and inert (Patter's
324
+ * rule); take fresh ones from getFlow()/flows(). */
325
+ loadGame(envelope: SaveEnvelope): void;
326
+ }
327
+ declare class Flow {
328
+ readonly id: string;
329
+ private readonly engine;
330
+ private readonly internals;
331
+ private closed;
332
+ private prng;
333
+ /** Per-box turn counters, keyed by box id (schema 3.4) - per flow. */
334
+ private turnCounts;
335
+ private cooldowns;
336
+ /** The board: hand contents (card ids, dealt order), keyed by hand id. */
337
+ private boardContents;
338
+ private playLog;
339
+ private playCount;
340
+ private lastPlayOf;
341
+ private tagPlayCount;
342
+ private lastPlayInTag;
343
+ /** The per-flow property partitions (the not-shared halves). */
344
+ private stores;
345
+ private traceHandlers;
346
+ private logEntries;
347
+ private logSeq;
348
+ /** Merged read view per scope, built once (bags are stable for the
349
+ * flow's life): the flow's own bag first, the shared bag behind it.
350
+ * Names are disjoint (shared XOR per-flow by declaration), so "first"
351
+ * is routing, not shadowing. */
352
+ private readonly storyReader;
353
+ private readonly boxReaders;
354
+ private readonly deckReaders;
355
+ /** @internal - built by Engine.openFlow / Engine.loadGame only. */
356
+ constructor(engine: Engine, internals: Internals, id: string, seed: number);
357
+ get isClosed(): boolean;
358
+ /** Close this flow: the handle goes inert, every verb throws. */
359
+ close(): void;
360
+ /** @internal */
361
+ markClosed(): void;
362
+ private assertOpen;
363
+ /** A box's current turn (schema 3.4), on THIS flow's clock. */
364
+ turn(boxRef: string): number;
365
+ /** Subscribe to this flow's deal/play trace (schema 5). Returns the
366
+ * unsubscribe. With no subscribers anywhere the flow does no trace work. */
367
+ subscribeTrace(handler: TraceHandler): () => void;
368
+ private get tracing();
369
+ private emit;
370
+ /** The retained flow log (opt-in via the Engine's `log`), oldest first,
371
+ * capped. The introspection seam for hosts and tools; the durable play
372
+ * history in a save stays `playLog` (schema 4) - the log is a
373
+ * flow-lifetime utility and is NOT saved. */
374
+ log(): readonly LogEntry[];
375
+ /** Empty the retained log; `seq` keeps counting, so ordering across a
376
+ * clear stays meaningful. */
377
+ clearLog(): void;
378
+ private node;
379
+ /** Tag group names are box-scoped: two boxes may name a group the same way
380
+ * (schema 1 - boxes namespace their groups), so a name is only ever
381
+ * resolved inside the box being asked, never bundle-wide. Ids are
382
+ * project-unique and accepted here too, still confined to the box. */
383
+ private groupInBox;
384
+ /** Fold one play into the indexes. O(the card's tags), not O(the log). */
385
+ private indexPlay;
386
+ /** Rebuild the indexes from the log. Called wherever `playLog` is REPLACED
387
+ * rather than appended to, which is `restore` alone. */
388
+ private rebuildPlayIndex;
389
+ /** `box` is the box whose ask is being evaluated: the play-history
390
+ * functions take a bare group name, so it resolves there (a card's tags
391
+ * reference its own box's group, which keeps the counts box-local).
392
+ * History is THIS flow's: countPlayed answers "have I done this". */
393
+ /** One host per box, built once.
394
+ *
395
+ * The closures below read `this.playCount`, `this.turnCounts` and the rest
396
+ * LIVE, so a cached host answers with current state - which is what makes
397
+ * caching safe rather than a snapshot bug. Unreal did this from the start
398
+ * (`hostsByBox_`, built at flow construction) and the other three rebuilt a
399
+ * host, and its closures, on every `evalCtx` call: once per deck per ask,
400
+ * and once per surviving card in the eviction pass. Structural divergence
401
+ * in one port AND the allocation the audit flagged, so the other three
402
+ * copied it (2026-08-29). Lazy rather than eager, so a bundle's unvisited
403
+ * boxes cost nothing. */
404
+ private hostsByBox;
405
+ private host;
406
+ private makeHost;
407
+ /** The evaluation environment (schema 3.1/6.2): @box/@deck resolve to the
408
+ * card under evaluation; in hand-condition contexts @deck is an empty bag,
409
+ * so any reference is an eval error (missing-policy throw). Every scope
410
+ * is the flow's MERGED view - its own copies over the shared values,
411
+ * names disjoint - and @world reads through the engine's resolver. */
412
+ private evalCtx;
413
+ /** The ladder behind one composed @hand name, or undefined when the name is
414
+ * not a quality (or came from criteria, which are tag NAMES, never state). */
415
+ private handLadder;
416
+ private eval;
417
+ private passes;
418
+ private tagByGameId;
419
+ /** A deal's ask: the hand's template bindings + chosen tags, or its rule's
420
+ * bindings, plus the implicit home binding (schema 2.4). */
421
+ private askForHand;
422
+ /** A peek's ask: raw criteria ({group gameId: tag gameId}), bindings only,
423
+ * no condition slot (schema 3.1; the boundary, Reboot 4). */
424
+ private askForPeek;
425
+ /**
426
+ * Bind every `boundBy` group in the box from the property it names.
427
+ *
428
+ * The gap this closes: only a hand could bind a group, and `deal` takes no
429
+ * criteria, so an axis driven by state (an act, a chapter) had nowhere to
430
+ * gate. Runs AFTER the hand's own bindings and never overwrites one: an
431
+ * explicit binding is a deliberate act and beats a default.
432
+ *
433
+ * A value naming no tag in the group leaves the group UNBOUND rather than
434
+ * matching nothing. Unbound is a wildcard, so the ask still deals; a silent
435
+ * empty hand would look like content that does not exist, and the diagnostic
436
+ * is what says otherwise.
437
+ */
438
+ private bindStateGroups;
439
+ /** A store's full value view for one owner: the shared half under the
440
+ * flow's half. Names are disjoint, so the spread is routing, not
441
+ * shadowing. */
442
+ private valuesOf;
443
+ private buildHandEnv;
444
+ /** The claims ledger, derived from THIS flow's board: card id -> holding
445
+ * hands (schema 3.5). Claims are per flow - another flow holding the
446
+ * card is another playthrough, not a rival hand. */
447
+ private claims;
448
+ /** @internal - every card id on THIS flow's board, one entry per holding
449
+ * hand. The engine sums these across live flows for the shared ledger. */
450
+ heldCardIds(): string[];
451
+ private copiesOf;
452
+ /** The claims step (schema 3.1 step 6) for one card, as a verdict or null
453
+ * for "available". Two caps apply to a shared card and they are different
454
+ * statements, so they get different verdicts: `copies` is your own board
455
+ * filling up, `sharedCopies` is somebody else already holding it, and a
456
+ * participant told "claimed" about a card sitting on another person's
457
+ * table would read it as an engine fault (design/shared-scarcity 9.3.1).
458
+ *
459
+ * `mine` counts this flow's holdings, `world` every live flow's. */
460
+ private claimVerdict;
461
+ /** Tag matching (schema 3.1 step 3): for every bound group the card lists
462
+ * the bound tag or omits the group (wildcard); the home group inverts -
463
+ * a homed card requires a matching home binding (schema 2.4). */
464
+ private tagsMatch;
465
+ /** Run one ask: availability filter then ranking. `claimed` decides the
466
+ * claims step (step 6) per card, returning the verdict that refused it or
467
+ * null for available. `trace` (when a subscriber exists) collects the
468
+ * per-card verdicts. */
469
+ private runAsk;
470
+ /** Flip eligible-but-not-taken trace entries to "capped". */
471
+ private capTrace;
472
+ private view;
473
+ private handCapacity;
474
+ private resolveHand;
475
+ /** Look at the top of the stock through raw tag criteria (schema 3.1):
476
+ * claims respected, nothing registered, nothing left behind but the
477
+ * trace line. You can never play a card you only peeked. */
478
+ peek(boxRef: string, criteria?: Record<string, string>, n?: number): RankedList;
479
+ /** Refresh one hand (schema 3.5); returns its new shape. */
480
+ deal(handRef: string): DealtCard[];
481
+ /** Re-deal several / all hands (schema 3.5): seeded hand-order shuffle
482
+ * (fairness), evict, seed the ledger from survivors, fill in order.
483
+ * Returns the dealt slice - the new contents of exactly the hands this
484
+ * call dealt, keyed by hand gameId (board() stays the whole-board read). */
485
+ dealMany(handRefs?: string[]): Record<string, DealtCard[]>;
486
+ /** The board: current hand contents, in dealt order, keyed by hand gameId
487
+ * (schema 5). Read it for what is out; peek the stock for what could
488
+ * come.
489
+ *
490
+ * `boxRef` (a box gameId or id) narrows the read to that box's hands, in
491
+ * the same shape and the same order: "give me the barks hands" is a
492
+ * common host query, and boxes are how a game separates its storylet
493
+ * systems, so the grouping belongs here rather than in every host. An
494
+ * unknown box throws, as it does on turn() and peek(). */
495
+ board(boxRef?: string): Record<string, DealtCard[]>;
496
+ /** Resolve a played/inspected card within a hand on the board. */
497
+ private resolveDealt;
498
+ /** Outcome availability, evaluated against CURRENT state on every ask
499
+ * (schema 3.1/5) - never a deal-time snapshot. */
500
+ outcomes(cardId: string, from: string): OutcomeView[];
501
+ /** Apply an outcome (schema 3.7): the card must sit in a hand on the
502
+ * board (you never play a card from inside the deck). Throws before any
503
+ * mutation on a gated-shut outcome or a bad write target. */
504
+ play(cardId: string, outcomeGameId: string, from: string, opts?: PlayOptions): void;
505
+ /** Land one change in whichever partition declares the name: the flow's
506
+ * bag when the property is per-flow, the shared bag when it is shared -
507
+ * the union/partition invariant made executable. */
508
+ private landIn;
509
+ /** Land one change; returns the resolved store path (for the trace) and
510
+ * the value it replaced (for the log's "0 -> 1" reading). */
511
+ private applyWrite;
512
+ /** Advance one box's clock (schema 3.4): a turn is one draw-from-stock
513
+ * session for that box, on THIS flow's clock. */
514
+ advanceTurns(boxRef: string, n?: number): void;
515
+ /** Every box, bundle order: identity + this flow's clock (the enumeration
516
+ * surface examiners key their turns sections on; parity member). */
517
+ listBoxes(): BoxView[];
518
+ /** THIS flow's kernel bags with their store path prefixes (the state
519
+ * logger's mount surface; parity member). The shared bags are the
520
+ * Engine's listBags; the flows are rebuilt by loadGame, so consumers
521
+ * re-enumerate after a load. */
522
+ listBags(): BagMount[];
523
+ /** The flow's FULL merged view as examiner rows (the property examiner /
524
+ * editor surface, parity across all runtimes): @world read through the
525
+ * resolver, then per scope the shared values and this flow's own.
526
+ * Bundle order: world, story, then per-box / per-deck / per-hand /
527
+ * per-tag stores. */
528
+ listProperties(): PropertyView[];
529
+ /** Read by path: "world.x", "story.gold", "value.v_docks.danger",
530
+ * "box.b_x.heat", "deck.k_main.n", "hand.h_board.owner" - the flow's
531
+ * merged view, routed by the declaration's sharing. */
532
+ getProperty(path: string): ScalarValue;
533
+ setProperty(path: string, value: ScalarValue): void;
534
+ private resolvePath;
535
+ /** @internal - this flow's blob inside the engine's envelope. */
536
+ snapshot(): FlowSave;
537
+ /** @internal - restore a freshly opened flow from its blob (loadGame).
538
+ * Orphaned keys (deleted entities) drop; new declarations keep defaults. */
539
+ restore(saved: FlowSave): void;
540
+ }
541
+
542
+ /** What bundle this is: the staleness/identity triple plus the schema tag. */
543
+ interface BundleIdentity {
544
+ /** The bundle schema tag ("storylets/bundle@0"). */
545
+ schema: string;
546
+ /** content.project - the project name a save must agree with. */
547
+ project: string;
548
+ /** content.version - the authored bundle version. */
549
+ version: string;
550
+ /** content.hash - hash32 over the canonical source shards (schema 2.8). */
551
+ hash: string;
552
+ /** "full" | "stripped": whether authoring metadata (titles) survived. */
553
+ metadata: string;
554
+ }
555
+ /** One hand: the deal() surface. `gameId` is the name deal() is called with. */
556
+ interface HandSummary {
557
+ gameId: string;
558
+ title?: string;
559
+ /** The owning box's gameId (peek's first argument for the same stock). */
560
+ box: string;
561
+ /** The effective slot cap: the hand's override, else its template's or
562
+ * rule's, else "unbounded". */
563
+ slots: number | "unbounded";
564
+ /** The hand template's gameId; absent for a standalone (inline-rule) hand. */
565
+ template?: string;
566
+ }
567
+ /** One tag group and its tags, by gameId: the peek() criteria surface (a
568
+ * criteria entry is `{ [group gameId]: tag gameId }`). */
569
+ interface TagGroupSummary {
570
+ gameId: string;
571
+ tags: string[];
572
+ }
573
+ /** One box: identity, its ranking policy, its tag groups, and counts. */
574
+ interface BoxSummary {
575
+ gameId: string;
576
+ title?: string;
577
+ /** The only per-box ranking policy (Reboot 2.2). */
578
+ ranking: {
579
+ specificity: boolean;
580
+ };
581
+ tagGroups: TagGroupSummary[];
582
+ counts: {
583
+ decks: number;
584
+ cards: number;
585
+ hands: number;
586
+ templates: number;
587
+ tagGroups: number;
588
+ };
589
+ }
590
+ /** One declared property: what expressions read and what a host may set. */
591
+ interface PropertySummary {
592
+ name: string;
593
+ type: PropertyType;
594
+ default: ScalarValue$1;
595
+ /** Enum / flags options, where declared. */
596
+ values?: string[];
597
+ purpose?: string;
598
+ }
599
+ /** The scope a declaration block belongs to. `tag` declarations compose into
600
+ * @hand for any ask that binds the tag (schema 3.6). */
601
+ type PropertyScopeKind = "world" | "story" | "box" | "deck" | "hand" | "tag";
602
+ /** One scope's declared properties. `owner` is the owning entity's gameId
603
+ * (empty for world / story); `box` names its box; `group` names a tag's
604
+ * group. */
605
+ interface PropertyScopeSummary {
606
+ scope: PropertyScopeKind;
607
+ owner: string;
608
+ box?: string;
609
+ group?: string;
610
+ properties: PropertySummary[];
611
+ }
612
+ /**
613
+ * One map the bundle was asked to carry (design/graphical-views.md 2).
614
+ *
615
+ * Counts rather than the geometry itself, which is the same judgement the rest
616
+ * of this file makes: an inspector answers "what is in here", and a host that
617
+ * wants the polygons reads `bundle.maps` directly. Reported because a bundle
618
+ * that silently carried a map would fail the promise this API exists for.
619
+ */
620
+ interface MapSummary {
621
+ /** The owning box, by gameId. */
622
+ box: string;
623
+ /** The tag group this is a map of, by gameId. */
624
+ group: string;
625
+ zones: number;
626
+ backgrounds: number;
627
+ }
628
+ /** What a bundle offers a host, read from the asset alone. */
629
+ interface BundleDescription {
630
+ identity: BundleIdentity;
631
+ /** Orientation, not inventory: no card lists (Reboot 2.1). */
632
+ totals: {
633
+ boxes: number;
634
+ decks: number;
635
+ cards: number;
636
+ hands: number;
637
+ templates: number;
638
+ tagGroups: number;
639
+ };
640
+ boxes: BoxSummary[];
641
+ /** Every hand in the bundle, box by box: the deal() surface. */
642
+ hands: HandSummary[];
643
+ /** world, story, then per box: the box, its decks, its hands, its tags.
644
+ * Scopes that declare nothing are omitted (world and story always show,
645
+ * so their absence reads as "this bundle declares none"). */
646
+ properties: PropertyScopeSummary[];
647
+ /** Maps carried as inert payload, when the build asked for them. Empty is
648
+ * the normal state and means the bundle has no geometry in it. */
649
+ maps: MapSummary[];
650
+ }
651
+ /** Describe a compiled bundle: the callable surface of an imported asset, no
652
+ * session required (design/engine-runtimes.md 2, piece 6). Bundle order
653
+ * throughout; the same shape every runtime returns. */
654
+ declare function describeBundle(bundle: Bundle): BundleDescription;
655
+
656
+ export { type BagMount, type BoxSummary, type BoxView, type BundleDescription, type BundleIdentity, type DealtCard, Engine, type EngineLogEntry, type EngineOptions, type EngineTraceHandler, Flow, type HandSummary, type LogEntry, type MapSummary, type OpenFlowOptions, type OutcomeView, type PlayOptions, type PropertyScopeKind, type PropertyScopeSummary, type PropertySummary, type PropertyView, type RankedList, type TagGroupSummary, type TraceEvent, type TraceHandler, type TraceVerdict, describeBundle };