@storylet-studio/runtime 0.4.1 → 0.6.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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ScalarValue, ScopeResolver, ExprNode } from '@wildwinter/expr';
2
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';
3
+ import { Bundle, FlowSave, LoadReport, Card, Expression, Deck, Box, Hand, HandTemplate, TagGroup, PropertyDecl, SaveEnvelope, PropertyType, ScalarValue as ScalarValue$1 } from '@storylet-studio/model';
4
4
  import { PropertyBag, PropertyRow } from '@wildwinter/scoperegistry';
5
5
  export { PropertyRow } from '@wildwinter/scoperegistry';
6
6
 
@@ -38,6 +38,25 @@ interface EngineOptions {
38
38
  interface OpenFlowOptions {
39
39
  /** Seed for this flow's PRNG (defaults to the engine's `seed`). */
40
40
  seed?: number;
41
+ /**
42
+ * Open this flow AS IT WAS: a blob from `saveFlow`, applied to the freshly
43
+ * opened (or replaced) flow before the handle comes back
44
+ * (design/engine-server.md 4.1).
45
+ *
46
+ * An option on `openFlow` rather than a `Flow.restore` verb on purpose:
47
+ * restoring INTO a running flow is the trap hosts keep falling into
48
+ * (openFlow REPLACES), and "open this flow as it was" is one act. Drift is
49
+ * tolerated exactly as `loadGame` tolerates it, with one addition, because
50
+ * this restore lands in a LIVE engine: a shared card whose world copies are
51
+ * all held by the OTHER open flows is not put back, and is reported as
52
+ * `claimed-elsewhere`. Ask `previewFlowRestore` first to see that coming.
53
+ */
54
+ restore?: FlowSave;
55
+ /** Handed the `restore`'s LoadReport as it happens - the same report
56
+ * `previewFlowRestore` returns for the same blob. Ignored without
57
+ * `restore`; the report has nowhere else to go, since `openFlow` returns
58
+ * the handle. */
59
+ onRestoreReport?: (report: LoadReport) => void;
41
60
  }
42
61
  /** A card view in a dealt hand or a peeked list. Carries NO outcome
43
62
  * availability - ask `outcomes()` for current truth (schema 5). */
@@ -53,6 +72,9 @@ interface OutcomeView {
53
72
  gameId: string;
54
73
  title?: string;
55
74
  purpose?: string;
75
+ /** The outcome's fields, exactly as the bundle carries them: game data
76
+ * declared by the box's `outcomeFields`, never read by the engine. */
77
+ fields?: Record<string, ScalarValue>;
56
78
  /** Evaluated against CURRENT state at the moment of the ask. */
57
79
  available: boolean;
58
80
  }
@@ -63,7 +85,8 @@ interface RankedList {
63
85
  cards: DealtCard[];
64
86
  }
65
87
  interface PlayOptions {
66
- /** Turn advance override; default settings.playAdvancesTurns. */
88
+ /** Turn advance override; default settings.playAdvancesTurns, or 0 when the
89
+ * card's box is timed (design/engine-server.md 4.8). */
67
90
  advanceTurns?: number;
68
91
  }
69
92
  /** Why a card did or did not make an ask, in availability order (schema 3.1). */
@@ -71,11 +94,18 @@ type TraceVerdict = "dealt" | "capped" | "cooldown" | "deck-gate" | "tags" | "co
71
94
  /** One event on the deal/play log - "why did Ambush at the ford get dealt
72
95
  * here?" is answered by the ask event's per-card verdicts and keys. The
73
96
  * verb is the event type, so a peek is distinguishable from a deal when
74
- * reading a run back. */
97
+ * reading a run back.
98
+ *
99
+ * IDENTITY IS BY GAMEID throughout (design/engine-server.md 4.4). It was
100
+ * mixed until then: `deal.hand` and `peek.box` were gameIds while
101
+ * `evict.hand`, `play.card` and every `cards[].id` were internal ids, so
102
+ * every consumer outside the engine - the Board, the four examiners, the
103
+ * Live Link, a wire a kiosk reads - mapped one to the other itself. */
75
104
  type TraceEvent = {
76
105
  type: "deal";
77
106
  /** Hand gameId. */
78
107
  hand: string;
108
+ /** `id` is the card's GAMEID (design/engine-server.md 4.4). */
79
109
  cards: {
80
110
  id: string;
81
111
  verdict: TraceVerdict;
@@ -87,24 +117,31 @@ type TraceEvent = {
87
117
  /** Box gameId. */
88
118
  box: string;
89
119
  criteria: Record<string, string>;
120
+ /** `id` is the card's GAMEID (design/engine-server.md 4.4). */
90
121
  cards: {
91
122
  id: string;
92
123
  verdict: TraceVerdict;
93
124
  priority?: number;
94
125
  specificity?: number;
95
126
  }[];
96
- } | {
127
+ }
128
+ /** Hand and card gameIds. A card the build no longer has (`vanished`) has
129
+ * no gameId left and is named by the id the board carried. */
130
+ | {
97
131
  type: "evict";
98
132
  hand: string;
99
133
  card: string;
100
134
  reason: TraceVerdict | "hand-condition" | "vanished";
101
- } | {
135
+ }
136
+ /** Card and outcome gameIds. */
137
+ | {
102
138
  type: "play";
103
139
  card: string;
104
140
  outcome: string;
105
141
  turn: number;
106
142
  }
107
- /** One landed outcome change; `path` is the resolved store location (a
143
+ /** One landed outcome change; `path` is the resolved store location, in the
144
+ * address grammar `getProperty` takes - the owner segment is its gameId (a
108
145
  * routed @hand write shows where it actually went, schema 3.6). `prev`
109
146
  * is the value it replaced, so a log can read "0 -> 1". */
110
147
  | {
@@ -153,8 +190,8 @@ interface CardEntry {
153
190
  deck: Deck<Expression>;
154
191
  box: Box<Expression>;
155
192
  }
156
- /** One kernel bag with its store path prefix (story / box.<id> / deck.<id>
157
- * / hand.<id> / value.<id>): the state logger's mount surface
193
+ /** One kernel bag with its store path prefix (story / box.<gameId> / deck.<gameId>
194
+ * / hand.<gameId> / value.<gameId>): the state logger's mount surface
158
195
  * (design/engine-runtimes.md 3.4 - the logger builds on the PropertyBag
159
196
  * audit hook, so it needs the bags themselves, not just their rows).
160
197
  * The Engine lists the shared bags, a Flow its own; the @world container
@@ -172,6 +209,50 @@ interface BoxView {
172
209
  title?: string;
173
210
  turn: number;
174
211
  }
212
+ /** One side's five declaration lists, keyed by owner id where the scope has
213
+ * owners. The bags are built from these; so is the load report's answer to
214
+ * "what does this build declare that the save does not carry". */
215
+ interface DeclSet {
216
+ story: PropertyDecl[];
217
+ box: Map<string, PropertyDecl[]>;
218
+ deck: Map<string, PropertyDecl[]>;
219
+ hand: Map<string, PropertyDecl[]>;
220
+ value: Map<string, PropertyDecl[]>;
221
+ }
222
+ /** The four owned property scopes: the ones whose address carries an owner
223
+ * segment. `story` has no owner and `world` is the host's. */
224
+ type OwnedScope = "box" | "deck" | "hand" | "value";
225
+ /** The owner segment of a property address, both ways round
226
+ * (design/engine-server.md 4.4).
227
+ *
228
+ * `gameId` is the segment the ADDRESS uses; `id` is the internal id everything
229
+ * inside the engine is keyed by - the bags, the save envelope, the ladders.
230
+ * Both maps are built in bundle order and a repeated gameId does NOT
231
+ * overwrite the first.
232
+ *
233
+ * Box, deck, hand and card gameIds are unique bundle-wide, so for three of the
234
+ * four scopes the segment is simply the gameId. A TAG's is unique only within
235
+ * its group, and a group's only within its box, so two boxes may each name a
236
+ * tag "docks": the value scope's segment is box-qualified,
237
+ * `value.<boxGameId>/<tagGameId>.<name>`, wherever a gameId repeats, and the
238
+ * short form is REFUSED there rather than resolved to the first in bundle
239
+ * order. `valueAddresses` in the model is the one definition of that rule -
240
+ * the Board draws these addresses from the bundle while the engine builds them
241
+ * from this index, and the two have to agree - and `repeated` is what it
242
+ * found, so a refusal can name the candidates.
243
+ *
244
+ * Two GROUPS in one box naming the same tag is the case the box qualifier
245
+ * cannot separate, and it is closing at the source rather than here (question
246
+ * 16, ruled 2026-09-06): the compiler warns that a tag gameId must be unique
247
+ * within its box, and refuses it from the next release. Until then the first
248
+ * in bundle order answers, as it always did.
249
+ */
250
+ interface OwnerIndex {
251
+ gameId: Map<string, string>;
252
+ id: Map<string, string>;
253
+ repeated: Map<string, string[]>;
254
+ }
255
+ type OwnerIndexes = Record<OwnedScope, OwnerIndex>;
175
256
  /** One side's five stores (shared on the engine, per-flow on each flow). */
176
257
  interface Partition {
177
258
  story: PropertyBag;
@@ -199,6 +280,8 @@ interface Internals {
199
280
  hand: Hand<Expression>;
200
281
  box: Box<Expression>;
201
282
  }>;
283
+ /** The owner segment of a property address, both ways round (4.4). */
284
+ owners: OwnerIndexes;
202
285
  templatesById: Map<string, HandTemplate<Expression>>;
203
286
  groupsById: Map<string, {
204
287
  group: TagGroup;
@@ -223,17 +306,26 @@ interface Internals {
223
306
  hasShared: boolean;
224
307
  /** The per-flow halves of every declaration list, precomputed once: each
225
308
  * new flow builds its bags from these. */
226
- flowDecls: {
227
- story: PropertyDecl[];
228
- box: Map<string, PropertyDecl[]>;
229
- deck: Map<string, PropertyDecl[]>;
230
- hand: Map<string, PropertyDecl[]>;
231
- value: Map<string, PropertyDecl[]>;
232
- };
309
+ flowDecls: DeclSet;
310
+ /** The shared halves, the same way. Not used to build anything - the shared
311
+ * bags are built straight from the bundle - but a load report has to say
312
+ * what the shared side WOULD hold without building a bag, which is what
313
+ * makes previewLoad pure. */
314
+ sharedDecls: DeclSet;
233
315
  /** The shared stores. Reassigned wholesale by loadGame/reset. */
234
316
  shared: Partition;
235
317
  /** @world: the host's resolver, or the self-backed bag's. */
236
318
  worldResolver: ScopeResolver;
319
+ /** The @world WRITE seam. `host` says the caller is the GAME's own surface -
320
+ * setProperty, the coverage harness, the CLI's --set - which the shared
321
+ * kernel lets past a `writable: false` (scoperegistry 0.6.0): that flag is
322
+ * the story's promise, not the game's. The story's refusal is the
323
+ * worldReadOnly table below, consulted before this seam is reached. A BOUND
324
+ * resolver is opaque - it takes a name and a value and keeps whatever rule
325
+ * the game has - so the flag only ever reaches the self-backed bag.
326
+ * Undefined when @world cannot be written at all (a resolver bound with no
327
+ * `set`). */
328
+ worldSet?: (name: string, value: ScalarValue, host?: boolean) => void;
237
329
  /** @world names declared `writable: false`: the story's promise, kept at
238
330
  * runtime as the compiler keeps it at publish (Reboot.md 10). */
239
331
  worldReadOnly: Set<string>;
@@ -307,15 +399,24 @@ declare class Engine {
307
399
  * Derived, which is what makes closeFlow and the openFlow replace release
308
400
  * what a flow was holding: its board leaves the map with it. */
309
401
  sharedClaims(): Map<string, number>;
402
+ /** The same ledger with one name left out: what the REST of the world
403
+ * holds, which is the question a resume under that name has to ask. */
404
+ private sharedClaimsExcept;
310
405
  /**
311
406
  * Read shared state by path: "world.x", "story.gold" (when shared),
312
- * "box.b_x.heat" (when shared). A ref that resolves PER-FLOW throws,
407
+ * "box.village.heat" (when shared) - the owner segment is its GAMEID
408
+ * (design/engine-server.md 4.4). A ref that resolves PER-FLOW throws,
313
409
  * naming the fix - silently answering with some flow's copy (or a junk
314
410
  * default) was the bug Patter's engine.getProperty guard exists to stop.
315
411
  */
316
412
  getProperty(path: string): ScalarValue;
317
413
  setProperty(path: string, value: ScalarValue): void;
318
414
  private resolveShared;
415
+ /** The engine's own surface has no flow, so an engine-level diagnostic
416
+ * carries the empty flow id - the same way a LoadReport's shared half
417
+ * carries no flow. It reaches the run log and the engine tap; there is
418
+ * nowhere else for it to go, and it fires only on a legacy address. */
419
+ private diagnose;
319
420
  /** The shared surface as examiner rows: @world (read through the
320
421
  * resolver) then the shared partitions. Per-flow rows live on each Flow. */
321
422
  listProperties(): PropertyRow[];
@@ -329,10 +430,40 @@ declare class Engine {
329
430
  * every live flow keyed by its id. @world is NEVER here - the host
330
431
  * saves its container, each engine saves its own envelope. */
331
432
  saveGame(): SaveEnvelope;
433
+ /** ONE flow's blob, to park a visit that is walking away: the same shape
434
+ * the envelope carries per flow, and the same shape `openFlow`'s `restore`
435
+ * option takes back (design/engine-server.md 4.1). Saving the whole
436
+ * envelope to park one of four hundred players is wrong in cost and in
437
+ * meaning. Throws for a name that is not open - a closed flow has nothing
438
+ * left to save. */
439
+ saveFlow(id: string): FlowSave;
440
+ /** What `loadGame(envelope)` would do that is not a plain restore, without
441
+ * doing any of it (design/engine-server.md 4.9). Pure: nothing on this
442
+ * engine moves. A project mismatch is refused here exactly as `loadGame`
443
+ * refuses it - it is the one thing neither call will tolerate. */
444
+ previewLoad(envelope: SaveEnvelope): LoadReport;
445
+ /** What `openFlow(id, { restore: saved })` would do to a flow of that name,
446
+ * without doing it: the same report shape, since a visit parked under one
447
+ * build and resumed under the next raises the same questions. Pure. */
448
+ previewFlowRestore(id: string, saved: FlowSave): LoadReport;
332
449
  /** Restore: shared state once, then every flow REBUILT from its blob.
333
450
  * Handles held from before the load are closed and inert (Patter's
334
- * rule); take fresh ones from getFlow()/flows(). */
335
- loadGame(envelope: SaveEnvelope): void;
451
+ * rule); take fresh ones from getFlow()/flows().
452
+ *
453
+ * Returns the report `previewLoad` would have given for this envelope: the
454
+ * drift tolerance that makes a load forgiving is what hides its cost, so
455
+ * the cost comes back with the load whether or not anybody looked first. */
456
+ loadGame(envelope: SaveEnvelope): LoadReport;
457
+ private assertSameProject;
458
+ /** The whole-envelope walk: the report, and the cleaned state the apply
459
+ * half writes. Nothing here touches the engine, which is what lets
460
+ * previewLoad and loadGame share it. */
461
+ private planLoad;
462
+ /** One flow's walk. `otherClaims` is the rest of the world's shared ledger
463
+ * and is present only for a SINGLE-flow restore into a live engine: a
464
+ * whole-envelope load rebuilds every flow from one consistent moment, so
465
+ * there is nobody else to compete with. */
466
+ private planFlowRestore;
336
467
  }
337
468
  declare class Flow {
338
469
  readonly id: string;
@@ -432,6 +563,23 @@ declare class Flow {
432
563
  /** A peek's ask: raw criteria ({group gameId: tag gameId}), bindings only,
433
564
  * no condition slot (schema 3.1; the boundary, Reboot 4). */
434
565
  private askForPeek;
566
+ /**
567
+ * Fill one hole from the property its value names: the hand that moves
568
+ * (design/engine-server.md 4.6).
569
+ *
570
+ * The semantics are `bindStateGroups`' below, word for word, applied per
571
+ * HOLE instead of per group: resolved at ask time, and a value naming no tag
572
+ * leaves the hole UNBOUND (a wildcard) with a diagnostic rather than dealing
573
+ * a silently empty hand. What is added is the `@hand` scope - the asking
574
+ * hand's OWN declared state, read here from the flow's merged view (the
575
+ * shared half under the flow's own, so a `shared: true` declaration moves
576
+ * the hole for every flow and a per-flow one moves it for this flow alone).
577
+ *
578
+ * Read BEFORE tag composition, which is the whole reason it is safe: the
579
+ * @hand bag a card sees is built from the bound tags, so resolving a hole
580
+ * from it would be circular. A hand's own declarations are not, so they are.
581
+ */
582
+ private fillHoleFromProperty;
435
583
  /**
436
584
  * Bind every `boundBy` group in the box from the property it names.
437
585
  *
@@ -477,7 +625,9 @@ declare class Flow {
477
625
  * null for available. `trace` (when a subscriber exists) collects the
478
626
  * per-card verdicts. */
479
627
  private runAsk;
480
- /** Flip eligible-but-not-taken trace entries to "capped". */
628
+ /** Flip eligible-but-not-taken trace entries to "capped". `taken` is keyed
629
+ * by GAMEID, as the trace rows are (4.4): the two must move together or
630
+ * every dealt card silently reads as capped. */
481
631
  private capTrace;
482
632
  private view;
483
633
  private handCapacity;
@@ -512,6 +662,8 @@ declare class Flow {
512
662
  * board (you never play a card from inside the deck). Throws before any
513
663
  * mutation on a gated-shut outcome or a bad write target. */
514
664
  play(cardId: string, outcomeGameId: string, from: string, opts?: PlayOptions): void;
665
+ /** One owned property's address, owner segment and all (4.4). */
666
+ private address;
515
667
  /** Land one change in whichever partition declares the name: the flow's
516
668
  * bag when the property is per-flow, the shared bag when it is shared -
517
669
  * the union/partition invariant made executable. */
@@ -536,9 +688,14 @@ declare class Flow {
536
688
  * Bundle order: world, story, then per-box / per-deck / per-hand /
537
689
  * per-tag stores. */
538
690
  listProperties(): PropertyRow[];
539
- /** Read by path: "world.x", "story.gold", "value.v_docks.danger",
540
- * "box.b_x.heat", "deck.k_main.n", "hand.h_board.owner" - the flow's
541
- * merged view, routed by the declaration's sharing. */
691
+ /** Read by path: "world.x", "story.gold", "value.docks.danger",
692
+ * "box.village.heat", "deck.wares.n", "hand.the-elder.zone" - the flow's
693
+ * merged view, routed by the declaration's sharing.
694
+ *
695
+ * The owner segment is the entity's GAMEID, the name it is called by
696
+ * everywhere else (4.4). Its internal id is accepted for this release and
697
+ * earns a `diagnostic` naming the address to move to; the next lockstep
698
+ * release refuses it. */
542
699
  getProperty(path: string): ScalarValue;
543
700
  setProperty(path: string, value: ScalarValue): void;
544
701
  private resolvePath;
@@ -562,6 +719,13 @@ interface BundleIdentity {
562
719
  /** "full" | "stripped": whether authoring metadata (titles) survived. */
563
720
  metadata: string;
564
721
  }
722
+ /** One hole this hand fills from a property rather than with a tag: the hand
723
+ * MOVES when that property is written (design/engine-server.md 4.6). `group`
724
+ * is the tag group's gameId, `from` the reference exactly as authored. */
725
+ interface MovableHole {
726
+ group: string;
727
+ from: string;
728
+ }
565
729
  /** One hand: the deal() surface. `gameId` is the name deal() is called with. */
566
730
  interface HandSummary {
567
731
  gameId: string;
@@ -573,6 +737,16 @@ interface HandSummary {
573
737
  slots: number | "unbounded";
574
738
  /** The hand template's gameId; absent for a standalone (inline-rule) hand. */
575
739
  template?: string;
740
+ /**
741
+ * The holes filled from a property, in bundle order. Absent when the hand
742
+ * has none, which is the ordinary case.
743
+ *
744
+ * Reported because it is the one thing about a hand an integrator cannot see
745
+ * from its name: a movable hole means writing that property MOVES the hand,
746
+ * so it is the difference between a fixed kiosk and a performer who walks
747
+ * about. `setProperty` is the whole verb; there is no other.
748
+ */
749
+ movable?: MovableHole[];
576
750
  }
577
751
  /** One tag group and its tags, by gameId: the peek() criteria surface (a
578
752
  * criteria entry is `{ [group gameId]: tag gameId }`). */
@@ -588,6 +762,19 @@ interface BoxSummary {
588
762
  ranking: {
589
763
  specificity: boolean;
590
764
  };
765
+ /** Present on a TIMED box (design/engine-server.md 4.8): how long one of
766
+ * its turns lasts. An integrator reading a bundle needs it to know which
767
+ * boxes their host must tick, and how often. Absent is the ordinary box. */
768
+ turn?: {
769
+ seconds: number;
770
+ };
771
+ /** How many cards in this box are DURABLE (design/engine-server.md 4.2):
772
+ * their `redraw: "never"` spend outlives the run, and a server has to lift
773
+ * and restore it. A count rather than a list, like every other number here:
774
+ * an integrator needs to know whether this box has any such cards at all,
775
+ * and which ones is the authoring tool's question. Absent when there are
776
+ * none, which is the ordinary bundle. */
777
+ durableCards?: number;
591
778
  tagGroups: TagGroupSummary[];
592
779
  counts: {
593
780
  decks: number;
@@ -604,6 +791,11 @@ interface PropertySummary {
604
791
  default: ScalarValue$1;
605
792
  /** Enum / flags options, where declared. */
606
793
  values?: string[];
794
+ /** Declared DURABLE (design/engine-server.md 4.2): the value survives a run,
795
+ * and a server lifts and restores it across one. The engine never reads it;
796
+ * it is reported because it is the difference between a value an integrator
797
+ * may reset and one somebody is going to expect back. Absent = run-scoped. */
798
+ durable?: true;
607
799
  purpose?: string;
608
800
  }
609
801
  /** The scope a declaration block belongs to. `tag` declarations compose into
@@ -634,6 +826,9 @@ interface MapSummary {
634
826
  group: string;
635
827
  zones: number;
636
828
  backgrounds: number;
829
+ /** Placed hands standing on this map (design/engine-server.md 4.3): where the
830
+ * kiosks are, in a bundle that carries geometry at all. */
831
+ sites: number;
637
832
  }
638
833
  /** What a bundle offers a host, read from the asset alone. */
639
834
  interface BundleDescription {
@@ -663,4 +858,4 @@ interface BundleDescription {
663
858
  * throughout; the same shape every runtime returns. */
664
859
  declare function describeBundle(bundle: Bundle): BundleDescription;
665
860
 
666
- 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 RankedList, type TagGroupSummary, type TraceEvent, type TraceHandler, type TraceVerdict, describeBundle };
861
+ 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 MovableHole, type OpenFlowOptions, type OutcomeView, type PlayOptions, type PropertyScopeKind, type PropertyScopeSummary, type PropertySummary, type RankedList, type TagGroupSummary, type TraceEvent, type TraceHandler, type TraceVerdict, describeBundle };