@storylet-studio/runtime 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { ScalarValue, ScopeResolver, ExprNode } from '@wildwinter/expr';
1
+ import { ScalarValue, ScopeResolver, ExprNode, EvalContext } from '@wildwinter/expr';
2
2
  export { Prng, makePrng, shuffleInPlace } from '@wildwinter/expr';
3
- import { Bundle, FlowSave, LoadReport, Card, Expression, Deck, Box, Hand, HandTemplate, TagGroup, PropertyDecl, SaveEnvelope, PropertyType, ScalarValue as ScalarValue$1 } from '@storylet-studio/model';
4
- import { PropertyBag, PropertyRow } from '@wildwinter/scoperegistry';
3
+ import { Bundle, FlowSave, LoadReport, Card, Expression, Deck, Box, Hand, HandTemplate, TagGroup, PropertyDecl, SaveEnvelope, SaveEnvelopeV1, PropertyType, ScalarValue as ScalarValue$1 } from '@storylet-studio/model';
4
+ import { PropertyBag, ScopeRegistry, PropertyRow } from '@wildwinter/scoperegistry';
5
5
  export { PropertyRow } from '@wildwinter/scoperegistry';
6
6
 
7
7
  interface EngineOptions {
@@ -18,12 +18,25 @@ interface EngineOptions {
18
18
  };
19
19
  /**
20
20
  * The host's resolver for @world - the values the game owns and the
21
- * story reads (and, where `set` is offered, writes). Omit it and the
22
- * engine self-backs @world from the declared defaults. Engine-level,
23
- * shared by all flows, never in saveGame(): the host saves its container
24
- * once, each engine saves its own envelope (design/flows.md).
21
+ * story reads (and, where `set` is offered, writes). Engine-level, shared
22
+ * by all flows, never saved: the game keeps these values. Omit it and a
23
+ * standalone engine self-backs @world from the declared defaults, as a
24
+ * property its registry stores and saves. A game running several engines
25
+ * registers @world in its registry itself instead.
25
26
  */
26
27
  world?: ScopeResolver;
28
+ /**
29
+ * The game's registry: ONE per game, holding every engine's properties
30
+ * except those the game keeps itself, saved once. Given one, the engine
31
+ * registers its own scopes in it (@story under `story`, every other bag
32
+ * under a key starting `storylets/`, and @world if `world` is passed),
33
+ * reads every other scope from it, and `saveGame()` leaves the property
34
+ * values to the game. @world is then the game's to register: owned if the
35
+ * registry should store it, foreign if the game keeps it. Omit it and the
36
+ * engine makes its own registry and acts as its own game: it self-backs
37
+ * @world, and `saveGame()` carries the registry's values too.
38
+ */
39
+ registry?: ScopeRegistry;
27
40
  /**
28
41
  * Diagnostics hook (opt-in, dev tooling only): fired when `openFlow` REPLACES
29
42
  * a flow that still had cards dealt, with the flow id and how many. The
@@ -133,7 +146,8 @@ type TraceEvent = {
133
146
  card: string;
134
147
  reason: TraceVerdict | "hand-condition" | "vanished";
135
148
  }
136
- /** Card and outcome gameIds. */
149
+ /** Card and outcome gameIds; `outcome` is "" for a card with no outcomes,
150
+ * played with none. */
137
151
  | {
138
152
  type: "play";
139
153
  card: string;
@@ -312,9 +326,24 @@ interface Internals {
312
326
  * what the shared side WOULD hold without building a bag, which is what
313
327
  * makes previewLoad pure. */
314
328
  sharedDecls: DeclSet;
315
- /** The shared stores. Reassigned wholesale by loadGame/reset. */
329
+ /** The shared stores, registered in the registry for the engine's life.
330
+ * Reseeded in place by reset and by a load that carries values. */
316
331
  shared: Partition;
317
- /** @world: the host's resolver, or the self-backed bag's. */
332
+ /** The game's one registry (or the engine's own, when it is standalone). */
333
+ registry: ScopeRegistry;
334
+ /** True when the engine made the registry: `saveGame()` then carries its values. */
335
+ ownsRegistry: boolean;
336
+ /** True when the engine self-backed @world (standalone, no resolver bound). */
337
+ selfWorld: boolean;
338
+ /** Every OTHER scope in the registry, as an eval context sees it (instance
339
+ * keys left out), rebuilt only when the registry's set of scopes moves. */
340
+ registryView: () => {
341
+ scopes: EvalContext["scopes"];
342
+ qualities: EvalContext["qualities"];
343
+ };
344
+ /** @world, read through the registry by name (the scope's own normalisation),
345
+ * so a @world the game registered folded to lower case still answers the
346
+ * names as authored. */
318
347
  worldResolver: ScopeResolver;
319
348
  /** The @world WRITE seam. `host` says the caller is the GAME's own surface -
320
349
  * setProperty, the coverage harness, the CLI's --set - which the shared
@@ -347,10 +376,19 @@ declare class Engine {
347
376
  * outlives reset/loadGame (the host's container is the host's). The
348
377
  * self-backed resolver is rebuilt instead. */
349
378
  private readonly hostWorld?;
379
+ /** The options this engine was built with: hotSwap builds its replacement from them. */
380
+ private readonly creationOptions;
381
+ /** How to register each shared scope again, in registration order: a failed
382
+ * hotSwap puts this engine back exactly as it was. */
383
+ private readonly sharedMounts;
350
384
  constructor(bundle: Bundle, opts?: EngineOptions);
351
- /** Build the shared stores and the @world seam. `hostWorld` sticks for the
352
- * engine's lifetime; reset/loadGame rebuild the shared bags around it. */
385
+ /** Build the shared stores, register them and @world, and set up the @world
386
+ * seam. Once, for the engine's life: reset and loads reseed the bags in
387
+ * place, so the registry never sees them come and go. */
353
388
  private initShared;
389
+ /** Every shared bag back to its declared defaults, in place (the registry
390
+ * keeps them registered), the self-backed @world included. */
391
+ private reseedShared;
354
392
  /** Quality ladders by scope for the eval channel (design/quality.md):
355
393
  * world/story keyed by name; box/deck/value keyed by owner id then name.
356
394
  * Built once - a bundle's declarations never change. Ladders are
@@ -361,6 +399,10 @@ declare class Engine {
361
399
  * state; shared state is untouched. There is no default flow: "main" is
362
400
  * a caller convention, not an engine rule. */
363
401
  openFlow(id: string, opts?: OpenFlowOptions): Flow;
402
+ /** openFlow, and loadGame's rebuild. `claim` says the new flow's bags take
403
+ * the values the registry holds for them (a load); a fresh open is a reset
404
+ * of that name, so anything waiting for it is discarded first. */
405
+ private open;
364
406
  getFlow(id: string): Flow | undefined;
365
407
  /** Every live flow, open order. */
366
408
  flows(): Flow[];
@@ -375,6 +417,11 @@ declare class Engine {
375
417
  * self-backed @world included; a host-bound @world is the host's and is
376
418
  * not touched). */
377
419
  reset(): void;
420
+ /** End the run: clear the log, close every flow, forget spent cards. Each
421
+ * flow's bags leave the registry; `keepFlows` names the flows whose values
422
+ * are kept there for the flow that replaces them (a load into the game's
423
+ * registry). */
424
+ private dropRun;
378
425
  /** Cards a shared `redraw: "never"` has taken out of the world, by card id.
379
426
  * The claim ledger is DERIVED from live boards and so needs no storage;
380
427
  * this one is durable, so it rides the save's shared half. */
@@ -417,6 +464,12 @@ declare class Engine {
417
464
  * carries no flow. It reaches the run log and the engine tap; there is
418
465
  * nowhere else for it to go, and it fires only on a legacy address. */
419
466
  private diagnose;
467
+ /** Content that names another engine's scope (`@patter.visits`) runs only
468
+ * where that engine is on this registry: without it every read would answer
469
+ * false and every write fail, so the flow is refused as it opens, before
470
+ * anything changes. By then a game has built all of its engines, whatever
471
+ * order it built them in. The same message on every runtime. */
472
+ private assertExternalScopes;
420
473
  /** The shared surface as examiner rows: @world (read through the
421
474
  * resolver) then the shared partitions. Per-flow rows live on each Flow. */
422
475
  listProperties(): PropertyRow[];
@@ -426,9 +479,33 @@ declare class Engine {
426
479
  listBags(): BagMount[];
427
480
  /** Every flow's trace, one stream, each event tagged with its flow id. */
428
481
  subscribeTrace(handler: EngineTraceHandler): () => void;
429
- /** The whole engine, one envelope: the shared partitions once, then
430
- * every live flow keyed by its id. @world is NEVER here - the host
431
- * saves its container, each engine saves its own envelope. */
482
+ /**
483
+ * Live bundle refresh: rebuild on an edited bundle with the whole run carried
484
+ * over, and return the replacement with the report its load produced.
485
+ *
486
+ * Standalone, that is a save and a load into a new engine, and this one is left
487
+ * untouched (discard it). With the game's registry the two cannot both hold the
488
+ * same keys, so this engine is spent afterwards (its flows closed, the
489
+ * replacement holding everything on the same registry): it carries its
490
+ * own values into the snapshot, steps out of the registry, and the replacement
491
+ * loads them the way a standalone save loads: so the report covers the
492
+ * properties the edit dropped, defaulted, or retyped, and a dropped property
493
+ * is dropped rather than kept. Values the game loaded that were still waiting
494
+ * for a flow of this engine carry across as they were, and nothing belonging
495
+ * to any other engine is touched. A save for another project is refused before
496
+ * anything moves; if the rebuild fails for any other reason, this engine takes
497
+ * its registrations back and is left exactly as it was.
498
+ */
499
+ hotSwap(bundle: Bundle, opts?: EngineOptions): {
500
+ engine: Engine;
501
+ report: LoadReport;
502
+ };
503
+ /** The whole engine's NON-property state, one envelope: the spent cards
504
+ * once, then every live flow (board, clocks, cooldowns, PRNG, play log)
505
+ * keyed by its id. The property values are the registry's: a standalone
506
+ * engine (one that made its own registry) carries them here under
507
+ * `registry`, self-backed @world included; a game that passed a registry
508
+ * saves it once itself, beside each engine's envelope. */
432
509
  saveGame(): SaveEnvelope;
433
510
  /** ONE flow's blob, to park a visit that is walking away: the same shape
434
511
  * the envelope carries per flow, and the same shape `openFlow`'s `restore`
@@ -441,7 +518,7 @@ declare class Engine {
441
518
  * doing any of it (design/engine-server.md 4.9). Pure: nothing on this
442
519
  * engine moves. A project mismatch is refused here exactly as `loadGame`
443
520
  * refuses it - it is the one thing neither call will tolerate. */
444
- previewLoad(envelope: SaveEnvelope): LoadReport;
521
+ previewLoad(envelope: SaveEnvelope | SaveEnvelopeV1): LoadReport;
445
522
  /** What `openFlow(id, { restore: saved })` would do to a flow of that name,
446
523
  * without doing it: the same report shape, since a visit parked under one
447
524
  * build and resumed under the next raises the same questions. Pure. */
@@ -450,10 +527,18 @@ declare class Engine {
450
527
  * Handles held from before the load are closed and inert (Patter's
451
528
  * rule); take fresh ones from getFlow()/flows().
452
529
  *
530
+ * Property values come from the registry. An envelope that carries them
531
+ * (a standalone engine's, or a version 1 envelope) has them walked, cleaned,
532
+ * and moved into the registry here, over fresh defaults. Otherwise the game
533
+ * loads its registry itself, before or after this call: each flow's bags
534
+ * are handed back to the registry with their values, and the restored
535
+ * flows claim them. The report then covers only what this envelope holds;
536
+ * the registry's own load rule applies to the values.
537
+ *
453
538
  * Returns the report `previewLoad` would have given for this envelope: the
454
539
  * drift tolerance that makes a load forgiving is what hides its cost, so
455
540
  * the cost comes back with the load whether or not anybody looked first. */
456
- loadGame(envelope: SaveEnvelope): LoadReport;
541
+ loadGame(envelope: SaveEnvelope | SaveEnvelopeV1): LoadReport;
457
542
  private assertSameProject;
458
543
  /** The whole-envelope walk: the report, and the cleaned state the apply
459
544
  * half writes. Nothing here touches the engine, which is what lets
@@ -481,8 +566,12 @@ declare class Flow {
481
566
  private lastPlayOf;
482
567
  private tagPlayCount;
483
568
  private lastPlayInTag;
484
- /** The per-flow property partitions (the not-shared halves). */
569
+ /** The per-flow property partitions (the not-shared halves), each bag
570
+ * that declares something registered under this flow's keys. */
485
571
  private stores;
572
+ private readonly registered;
573
+ /** This flow's bags that declare something, under their registry keys. */
574
+ private readonly bagKeys;
486
575
  private traceHandlers;
487
576
  private logEntries;
488
577
  private logSeq;
@@ -500,6 +589,15 @@ declare class Flow {
500
589
  close(): void;
501
590
  /** @internal */
502
591
  markClosed(): void;
592
+ /** @internal - take this flow's bags out of the registry; with `keep`, their
593
+ * values wait there for the flow that replaces this one (a load into the
594
+ * game's registry). Idempotent. */
595
+ releaseBags(keep: boolean): void;
596
+ /** @internal - the registry keys this flow holds right now. */
597
+ registeredKeys(): string[];
598
+ /** @internal - register this flow's bags (again): at construction, and when a
599
+ * failed hotSwap hands them back. Each claims what the registry holds for it. */
600
+ mountBags(): void;
503
601
  private assertOpen;
504
602
  /** A box's current turn (schema 3.4), on THIS flow's clock. */
505
603
  turn(boxRef: string): number;
@@ -660,7 +758,16 @@ declare class Flow {
660
758
  outcomes(cardId: string, from: string): OutcomeView[];
661
759
  /** Apply an outcome (schema 3.7): the card must sit in a hand on the
662
760
  * board (you never play a card from inside the deck). Throws before any
663
- * mutation on a gated-shut outcome or a bad write target. */
761
+ * mutation on a gated-shut outcome or a bad write target.
762
+ *
763
+ * A card with NO outcomes is played with none, named as "" (the
764
+ * no-outcome-play brief, 2026-09-14): a masthead, a notice, a codex entry,
765
+ * whose play means "shown". It is everything a play is except the writes:
766
+ * the play log and the history functions count it, the box's turn moves by
767
+ * the usual rule, the redraw rests it and it leaves its hand. "" is the one
768
+ * spelling in all four runtimes, because a Blueprint pin cannot be absent.
769
+ * Only the empty-for-empty case is new: "" on a card that has outcomes is
770
+ * refused, and a named outcome on a card with none is refused as before. */
664
771
  play(cardId: string, outcomeGameId: string, from: string, opts?: PlayOptions): void;
665
772
  /** One owned property's address, owner segment and all (4.4). */
666
773
  private address;
@@ -699,8 +806,9 @@ declare class Flow {
699
806
  getProperty(path: string): ScalarValue;
700
807
  setProperty(path: string, value: ScalarValue): void;
701
808
  private resolvePath;
702
- /** @internal - this flow's blob inside the engine's envelope. */
703
- snapshot(): FlowSave;
809
+ /** @internal - this flow's blob: inside the engine's envelope without its
810
+ * properties (the registry has them), or parked whole by saveFlow. */
811
+ snapshot(withProps: boolean): FlowSave;
704
812
  /** @internal - restore a freshly opened flow from its blob (loadGame).
705
813
  * Orphaned keys (deleted entities) drop; new declarations keep defaults. */
706
814
  restore(saved: FlowSave): void;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { ScalarValue, ScopeResolver, ExprNode } from '@wildwinter/expr';
1
+ import { ScalarValue, ScopeResolver, ExprNode, EvalContext } from '@wildwinter/expr';
2
2
  export { Prng, makePrng, shuffleInPlace } from '@wildwinter/expr';
3
- import { Bundle, FlowSave, LoadReport, Card, Expression, Deck, Box, Hand, HandTemplate, TagGroup, PropertyDecl, SaveEnvelope, PropertyType, ScalarValue as ScalarValue$1 } from '@storylet-studio/model';
4
- import { PropertyBag, PropertyRow } from '@wildwinter/scoperegistry';
3
+ import { Bundle, FlowSave, LoadReport, Card, Expression, Deck, Box, Hand, HandTemplate, TagGroup, PropertyDecl, SaveEnvelope, SaveEnvelopeV1, PropertyType, ScalarValue as ScalarValue$1 } from '@storylet-studio/model';
4
+ import { PropertyBag, ScopeRegistry, PropertyRow } from '@wildwinter/scoperegistry';
5
5
  export { PropertyRow } from '@wildwinter/scoperegistry';
6
6
 
7
7
  interface EngineOptions {
@@ -18,12 +18,25 @@ interface EngineOptions {
18
18
  };
19
19
  /**
20
20
  * The host's resolver for @world - the values the game owns and the
21
- * story reads (and, where `set` is offered, writes). Omit it and the
22
- * engine self-backs @world from the declared defaults. Engine-level,
23
- * shared by all flows, never in saveGame(): the host saves its container
24
- * once, each engine saves its own envelope (design/flows.md).
21
+ * story reads (and, where `set` is offered, writes). Engine-level, shared
22
+ * by all flows, never saved: the game keeps these values. Omit it and a
23
+ * standalone engine self-backs @world from the declared defaults, as a
24
+ * property its registry stores and saves. A game running several engines
25
+ * registers @world in its registry itself instead.
25
26
  */
26
27
  world?: ScopeResolver;
28
+ /**
29
+ * The game's registry: ONE per game, holding every engine's properties
30
+ * except those the game keeps itself, saved once. Given one, the engine
31
+ * registers its own scopes in it (@story under `story`, every other bag
32
+ * under a key starting `storylets/`, and @world if `world` is passed),
33
+ * reads every other scope from it, and `saveGame()` leaves the property
34
+ * values to the game. @world is then the game's to register: owned if the
35
+ * registry should store it, foreign if the game keeps it. Omit it and the
36
+ * engine makes its own registry and acts as its own game: it self-backs
37
+ * @world, and `saveGame()` carries the registry's values too.
38
+ */
39
+ registry?: ScopeRegistry;
27
40
  /**
28
41
  * Diagnostics hook (opt-in, dev tooling only): fired when `openFlow` REPLACES
29
42
  * a flow that still had cards dealt, with the flow id and how many. The
@@ -133,7 +146,8 @@ type TraceEvent = {
133
146
  card: string;
134
147
  reason: TraceVerdict | "hand-condition" | "vanished";
135
148
  }
136
- /** Card and outcome gameIds. */
149
+ /** Card and outcome gameIds; `outcome` is "" for a card with no outcomes,
150
+ * played with none. */
137
151
  | {
138
152
  type: "play";
139
153
  card: string;
@@ -312,9 +326,24 @@ interface Internals {
312
326
  * what the shared side WOULD hold without building a bag, which is what
313
327
  * makes previewLoad pure. */
314
328
  sharedDecls: DeclSet;
315
- /** The shared stores. Reassigned wholesale by loadGame/reset. */
329
+ /** The shared stores, registered in the registry for the engine's life.
330
+ * Reseeded in place by reset and by a load that carries values. */
316
331
  shared: Partition;
317
- /** @world: the host's resolver, or the self-backed bag's. */
332
+ /** The game's one registry (or the engine's own, when it is standalone). */
333
+ registry: ScopeRegistry;
334
+ /** True when the engine made the registry: `saveGame()` then carries its values. */
335
+ ownsRegistry: boolean;
336
+ /** True when the engine self-backed @world (standalone, no resolver bound). */
337
+ selfWorld: boolean;
338
+ /** Every OTHER scope in the registry, as an eval context sees it (instance
339
+ * keys left out), rebuilt only when the registry's set of scopes moves. */
340
+ registryView: () => {
341
+ scopes: EvalContext["scopes"];
342
+ qualities: EvalContext["qualities"];
343
+ };
344
+ /** @world, read through the registry by name (the scope's own normalisation),
345
+ * so a @world the game registered folded to lower case still answers the
346
+ * names as authored. */
318
347
  worldResolver: ScopeResolver;
319
348
  /** The @world WRITE seam. `host` says the caller is the GAME's own surface -
320
349
  * setProperty, the coverage harness, the CLI's --set - which the shared
@@ -347,10 +376,19 @@ declare class Engine {
347
376
  * outlives reset/loadGame (the host's container is the host's). The
348
377
  * self-backed resolver is rebuilt instead. */
349
378
  private readonly hostWorld?;
379
+ /** The options this engine was built with: hotSwap builds its replacement from them. */
380
+ private readonly creationOptions;
381
+ /** How to register each shared scope again, in registration order: a failed
382
+ * hotSwap puts this engine back exactly as it was. */
383
+ private readonly sharedMounts;
350
384
  constructor(bundle: Bundle, opts?: EngineOptions);
351
- /** Build the shared stores and the @world seam. `hostWorld` sticks for the
352
- * engine's lifetime; reset/loadGame rebuild the shared bags around it. */
385
+ /** Build the shared stores, register them and @world, and set up the @world
386
+ * seam. Once, for the engine's life: reset and loads reseed the bags in
387
+ * place, so the registry never sees them come and go. */
353
388
  private initShared;
389
+ /** Every shared bag back to its declared defaults, in place (the registry
390
+ * keeps them registered), the self-backed @world included. */
391
+ private reseedShared;
354
392
  /** Quality ladders by scope for the eval channel (design/quality.md):
355
393
  * world/story keyed by name; box/deck/value keyed by owner id then name.
356
394
  * Built once - a bundle's declarations never change. Ladders are
@@ -361,6 +399,10 @@ declare class Engine {
361
399
  * state; shared state is untouched. There is no default flow: "main" is
362
400
  * a caller convention, not an engine rule. */
363
401
  openFlow(id: string, opts?: OpenFlowOptions): Flow;
402
+ /** openFlow, and loadGame's rebuild. `claim` says the new flow's bags take
403
+ * the values the registry holds for them (a load); a fresh open is a reset
404
+ * of that name, so anything waiting for it is discarded first. */
405
+ private open;
364
406
  getFlow(id: string): Flow | undefined;
365
407
  /** Every live flow, open order. */
366
408
  flows(): Flow[];
@@ -375,6 +417,11 @@ declare class Engine {
375
417
  * self-backed @world included; a host-bound @world is the host's and is
376
418
  * not touched). */
377
419
  reset(): void;
420
+ /** End the run: clear the log, close every flow, forget spent cards. Each
421
+ * flow's bags leave the registry; `keepFlows` names the flows whose values
422
+ * are kept there for the flow that replaces them (a load into the game's
423
+ * registry). */
424
+ private dropRun;
378
425
  /** Cards a shared `redraw: "never"` has taken out of the world, by card id.
379
426
  * The claim ledger is DERIVED from live boards and so needs no storage;
380
427
  * this one is durable, so it rides the save's shared half. */
@@ -417,6 +464,12 @@ declare class Engine {
417
464
  * carries no flow. It reaches the run log and the engine tap; there is
418
465
  * nowhere else for it to go, and it fires only on a legacy address. */
419
466
  private diagnose;
467
+ /** Content that names another engine's scope (`@patter.visits`) runs only
468
+ * where that engine is on this registry: without it every read would answer
469
+ * false and every write fail, so the flow is refused as it opens, before
470
+ * anything changes. By then a game has built all of its engines, whatever
471
+ * order it built them in. The same message on every runtime. */
472
+ private assertExternalScopes;
420
473
  /** The shared surface as examiner rows: @world (read through the
421
474
  * resolver) then the shared partitions. Per-flow rows live on each Flow. */
422
475
  listProperties(): PropertyRow[];
@@ -426,9 +479,33 @@ declare class Engine {
426
479
  listBags(): BagMount[];
427
480
  /** Every flow's trace, one stream, each event tagged with its flow id. */
428
481
  subscribeTrace(handler: EngineTraceHandler): () => void;
429
- /** The whole engine, one envelope: the shared partitions once, then
430
- * every live flow keyed by its id. @world is NEVER here - the host
431
- * saves its container, each engine saves its own envelope. */
482
+ /**
483
+ * Live bundle refresh: rebuild on an edited bundle with the whole run carried
484
+ * over, and return the replacement with the report its load produced.
485
+ *
486
+ * Standalone, that is a save and a load into a new engine, and this one is left
487
+ * untouched (discard it). With the game's registry the two cannot both hold the
488
+ * same keys, so this engine is spent afterwards (its flows closed, the
489
+ * replacement holding everything on the same registry): it carries its
490
+ * own values into the snapshot, steps out of the registry, and the replacement
491
+ * loads them the way a standalone save loads: so the report covers the
492
+ * properties the edit dropped, defaulted, or retyped, and a dropped property
493
+ * is dropped rather than kept. Values the game loaded that were still waiting
494
+ * for a flow of this engine carry across as they were, and nothing belonging
495
+ * to any other engine is touched. A save for another project is refused before
496
+ * anything moves; if the rebuild fails for any other reason, this engine takes
497
+ * its registrations back and is left exactly as it was.
498
+ */
499
+ hotSwap(bundle: Bundle, opts?: EngineOptions): {
500
+ engine: Engine;
501
+ report: LoadReport;
502
+ };
503
+ /** The whole engine's NON-property state, one envelope: the spent cards
504
+ * once, then every live flow (board, clocks, cooldowns, PRNG, play log)
505
+ * keyed by its id. The property values are the registry's: a standalone
506
+ * engine (one that made its own registry) carries them here under
507
+ * `registry`, self-backed @world included; a game that passed a registry
508
+ * saves it once itself, beside each engine's envelope. */
432
509
  saveGame(): SaveEnvelope;
433
510
  /** ONE flow's blob, to park a visit that is walking away: the same shape
434
511
  * the envelope carries per flow, and the same shape `openFlow`'s `restore`
@@ -441,7 +518,7 @@ declare class Engine {
441
518
  * doing any of it (design/engine-server.md 4.9). Pure: nothing on this
442
519
  * engine moves. A project mismatch is refused here exactly as `loadGame`
443
520
  * refuses it - it is the one thing neither call will tolerate. */
444
- previewLoad(envelope: SaveEnvelope): LoadReport;
521
+ previewLoad(envelope: SaveEnvelope | SaveEnvelopeV1): LoadReport;
445
522
  /** What `openFlow(id, { restore: saved })` would do to a flow of that name,
446
523
  * without doing it: the same report shape, since a visit parked under one
447
524
  * build and resumed under the next raises the same questions. Pure. */
@@ -450,10 +527,18 @@ declare class Engine {
450
527
  * Handles held from before the load are closed and inert (Patter's
451
528
  * rule); take fresh ones from getFlow()/flows().
452
529
  *
530
+ * Property values come from the registry. An envelope that carries them
531
+ * (a standalone engine's, or a version 1 envelope) has them walked, cleaned,
532
+ * and moved into the registry here, over fresh defaults. Otherwise the game
533
+ * loads its registry itself, before or after this call: each flow's bags
534
+ * are handed back to the registry with their values, and the restored
535
+ * flows claim them. The report then covers only what this envelope holds;
536
+ * the registry's own load rule applies to the values.
537
+ *
453
538
  * Returns the report `previewLoad` would have given for this envelope: the
454
539
  * drift tolerance that makes a load forgiving is what hides its cost, so
455
540
  * the cost comes back with the load whether or not anybody looked first. */
456
- loadGame(envelope: SaveEnvelope): LoadReport;
541
+ loadGame(envelope: SaveEnvelope | SaveEnvelopeV1): LoadReport;
457
542
  private assertSameProject;
458
543
  /** The whole-envelope walk: the report, and the cleaned state the apply
459
544
  * half writes. Nothing here touches the engine, which is what lets
@@ -481,8 +566,12 @@ declare class Flow {
481
566
  private lastPlayOf;
482
567
  private tagPlayCount;
483
568
  private lastPlayInTag;
484
- /** The per-flow property partitions (the not-shared halves). */
569
+ /** The per-flow property partitions (the not-shared halves), each bag
570
+ * that declares something registered under this flow's keys. */
485
571
  private stores;
572
+ private readonly registered;
573
+ /** This flow's bags that declare something, under their registry keys. */
574
+ private readonly bagKeys;
486
575
  private traceHandlers;
487
576
  private logEntries;
488
577
  private logSeq;
@@ -500,6 +589,15 @@ declare class Flow {
500
589
  close(): void;
501
590
  /** @internal */
502
591
  markClosed(): void;
592
+ /** @internal - take this flow's bags out of the registry; with `keep`, their
593
+ * values wait there for the flow that replaces this one (a load into the
594
+ * game's registry). Idempotent. */
595
+ releaseBags(keep: boolean): void;
596
+ /** @internal - the registry keys this flow holds right now. */
597
+ registeredKeys(): string[];
598
+ /** @internal - register this flow's bags (again): at construction, and when a
599
+ * failed hotSwap hands them back. Each claims what the registry holds for it. */
600
+ mountBags(): void;
503
601
  private assertOpen;
504
602
  /** A box's current turn (schema 3.4), on THIS flow's clock. */
505
603
  turn(boxRef: string): number;
@@ -660,7 +758,16 @@ declare class Flow {
660
758
  outcomes(cardId: string, from: string): OutcomeView[];
661
759
  /** Apply an outcome (schema 3.7): the card must sit in a hand on the
662
760
  * board (you never play a card from inside the deck). Throws before any
663
- * mutation on a gated-shut outcome or a bad write target. */
761
+ * mutation on a gated-shut outcome or a bad write target.
762
+ *
763
+ * A card with NO outcomes is played with none, named as "" (the
764
+ * no-outcome-play brief, 2026-09-14): a masthead, a notice, a codex entry,
765
+ * whose play means "shown". It is everything a play is except the writes:
766
+ * the play log and the history functions count it, the box's turn moves by
767
+ * the usual rule, the redraw rests it and it leaves its hand. "" is the one
768
+ * spelling in all four runtimes, because a Blueprint pin cannot be absent.
769
+ * Only the empty-for-empty case is new: "" on a card that has outcomes is
770
+ * refused, and a named outcome on a card with none is refused as before. */
664
771
  play(cardId: string, outcomeGameId: string, from: string, opts?: PlayOptions): void;
665
772
  /** One owned property's address, owner segment and all (4.4). */
666
773
  private address;
@@ -699,8 +806,9 @@ declare class Flow {
699
806
  getProperty(path: string): ScalarValue;
700
807
  setProperty(path: string, value: ScalarValue): void;
701
808
  private resolvePath;
702
- /** @internal - this flow's blob inside the engine's envelope. */
703
- snapshot(): FlowSave;
809
+ /** @internal - this flow's blob: inside the engine's envelope without its
810
+ * properties (the registry has them), or parked whole by saveFlow. */
811
+ snapshot(withProps: boolean): FlowSave;
704
812
  /** @internal - restore a freshly opened flow from its blob (loadGame).
705
813
  * Orphaned keys (deleted entities) drop; new declarations keep defaults. */
706
814
  restore(saved: FlowSave): void;