@patterkit/runtime 0.8.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -33,6 +33,76 @@ version number always means the same runtime behaviour. This package is versione
33
33
 
34
34
  ## [Unreleased]
35
35
 
36
+ ## [0.10.0] - 2026-09-02
37
+
38
+ ### Added
39
+
40
+ - **`Engine.listBags()` and `Flow.listBags()`.** The kernel bags with the path each answers
41
+ to in a log - the shared `@patter` globals and per-scene props on the engine, a flow's own
42
+ halves prefixed with its id. Parity with the Storylet Engine's method of the same name; it
43
+ is what a state logger mounts. A stage bag's LOG path is `@scene:<sceneId>.` where its
44
+ address is `@scene.`, because a log spans scenes and has to say which one.
45
+
46
+ ### Changed
47
+
48
+ - **The state logger watches the property bags instead of diffing save snapshots.** A
49
+ property write is logged when it LANDS, on the bag's audit hook, rather than at the next
50
+ capture. The visit counts live in no bag, so those are still diffed - which is all this
51
+ logger used to do for everything. What it buys: a diff can only report the NET change
52
+ between two captures, so a value that changed and changed back was invisible, and every
53
+ write was reported late. The core is shared with the Storylet Engine, which has always
54
+ worked this way.
55
+ - **Requires `@wildwinter/scoperegistry` ^0.5.0**, which carries the shared state logger.
56
+
57
+ ## [0.9.0] - 2026-09-02
58
+
59
+ ### Changed
60
+
61
+ - **BREAKING: a property row's address is `path`, not `ref`, and the row carries `name` and
62
+ `writable`.** `listProperties()` returns the row type from `@wildwinter/scoperegistry` itself,
63
+ rather than a local interface redeclaring it. `path` holds exactly what `ref`
64
+ held: the reference `getProperty` / `setProperty` take. `name` is the bare declared name.
65
+ `writable` is always true here, because Patter has no read-only shared property; it is carried
66
+ because the row is shared with the Storylet Engine, which does declare them.
67
+
68
+ The fork it replaces was not free. A quality's `stages` was added to the local copy and so never
69
+ reached the shared type, and the Storylet Engine - reading the same property model through the
70
+ same registry - could not carry a ladder at all: it edited a quality as free text on every
71
+ platform it ships. One row, one place to add the next field.
72
+ - **BREAKING: a row's address is the qualified one, `@patter.gold`, not `@gold`.** Both forms
73
+ have always resolved on input and still do - an unqualified name defaults to the `patter`
74
+ scope - so `getProperty("@gold")` is unaffected. What changed is the address a row REPORTS,
75
+ which is what a state panel displays and what an inspector writes back through. It matches
76
+ what `@scene` and the other family's scopes have always looked like.
77
+
78
+ - **Scene and stage state is held in the shared property bag.** `@scene` properties lived in
79
+ hand-rolled maps that duplicated the bag's own seeding, so they missed its guards: two flows
80
+ entering one scene now never share a mutable flags list, and a `temporary` property's reset on
81
+ re-entry goes through the bag, which means a state logger sees it. **The save format is
82
+ unchanged** - a flat name/value map per scene, and a load that seeds from the bundle's
83
+ declarations before laying saved values over, so a property a save predates keeps its default.
84
+ - **BREAKING: `PropertyView` is gone; `listProperties()` returns `PropertyRow`.** It was the
85
+ shared row plus a `path`, and `path` is on the shared row now, so the name was a second name
86
+ for one type. `PropertyRow` is re-exported from `@patterkit/runtime`, so naming a row needs no
87
+ dependency on `@wildwinter/scoperegistry`.
88
+ - **Requires `@wildwinter/scoperegistry` ^0.4.0**, which is where the row's `path` lives.
89
+
90
+ ### Added
91
+
92
+ - **A decision log, and `onDryChoice`.** Opening a run with `log: true` records what the engine
93
+ decided and why - each choice with the options it offered, the ones it greyed out and the
94
+ reason, each jump, each property write with the value it replaced. `Engine.log()` is the whole
95
+ run in order, a `Flow`'s own log is flow-local, and `onTrace` streams entries live rather than
96
+ retaining them. `onDryChoice` fires when a choice runs dry - no takeable option, no eligible
97
+ fallback - so the silent fall-through is observable; it survives alongside the log because it is
98
+ live feedback, not a record.
99
+
100
+ ### Fixed
101
+
102
+ - **A quality row carries its ladder.** `stages` was on the row so an examiner could offer the
103
+ stages instead of a free-text box, and the code that builds rows never filled it in - on this
104
+ runtime and two others. Every quality row came out without one.
105
+
36
106
  ## [0.8.0] - 2026-09-01
37
107
 
38
108
  ### Changed
package/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ module.exports = __toCommonJS(index_exports);
34
34
  var import_expr = require("@wildwinter/expr");
35
35
  var import_expr_specificity = require("@wildwinter/expr-specificity");
36
36
  var import_scoperegistry = require("@wildwinter/scoperegistry");
37
+ var import_scoperegistry2 = require("@wildwinter/scoperegistry");
37
38
  var import_dialect = require("@patterkit/dialect");
38
39
  var import_model = require("@patterkit/model");
39
40
 
@@ -92,6 +93,10 @@ var Engine = class _Engine {
92
93
  /** The options this engine was built with - reused verbatim by `hotSwap` so the replacement
93
94
  * engine keeps the same world resolver, custom RNG, and diagnostic hooks. */
94
95
  creationOptions;
96
+ /** The run's ordered stream: every flow's events, each naming its flow. Empty and
97
+ * unwritten unless `options.log` asked for it. */
98
+ engineLog = [];
99
+ engineTraceHandlers = /* @__PURE__ */ new Set();
95
100
  constructor(bundle, options = {}) {
96
101
  this.creationOptions = options;
97
102
  const locale = options.locale ?? bundle.locales.default;
@@ -127,7 +132,7 @@ var Engine = class _Engine {
127
132
  const patterSharedDecls = props.filter((p) => p.shared ?? true).map(toDecl);
128
133
  const patterLocalDecls = props.filter((p) => !(p.shared ?? true)).map(toDecl);
129
134
  const patterSharedNames = new Set(patterSharedDecls.map((d) => d.name.toLowerCase()));
130
- const shared = new import_scoperegistry.ScopeRegistry().defineOwned("patter", patterSharedDecls);
135
+ const shared = new import_scoperegistry.ScopeRegistry().defineOwned("patter", patterSharedDecls, "@patter.");
131
136
  const hostBound = /* @__PURE__ */ new Set();
132
137
  if (options.world) {
133
138
  const worldSpec = bundle.scopeRegistry?.scopes.find((s) => s.token === "world");
@@ -146,6 +151,8 @@ var Engine = class _Engine {
146
151
  sceneSharedNames.set(sceneId, names);
147
152
  }
148
153
  this.host = {
154
+ logEnabled: options.log ?? false,
155
+ emitEngine: (flow, event, scene) => this.emitEngine(flow, event, scene),
149
156
  bundle,
150
157
  emitIds,
151
158
  strings,
@@ -488,6 +495,22 @@ var Engine = class _Engine {
488
495
  flows() {
489
496
  return [...this.flowsById.values()];
490
497
  }
498
+ /**
499
+ * The SHARED kernel bags with the path each answers to in a log: the `@patter` globals,
500
+ * and one per scene for the shared `@scene` props. Parity with the Storylet Engine's
501
+ * listBags of the same name - it is what a state logger mounts.
502
+ *
503
+ * A stage bag's log path is `@scene:<sceneId>.`, not the bag's own `@scene.`: a property
504
+ * is ADDRESSED relative to a flow's current scene, but a log spans scenes and has to say
505
+ * which one. That is why a mount may override the bag's prefix.
506
+ *
507
+ * loadGame() replaces every bag, so re-enumerate after a load.
508
+ */
509
+ listBags() {
510
+ const mounts = [{ bag: this.host.shared.ownedBag("patter") }];
511
+ for (const [sceneId, bag] of this.host.stageBags) mounts.push({ bag, pathPrefix: `@scene:${sceneId}.` });
512
+ return mounts;
513
+ }
491
514
  /** Close (remove) a flow. The flow object is FINISHED, not merely unregistered, so a host still
492
515
  * holding it cannot keep advancing it into the shared world (see {@link Flow.close}). */
493
516
  closeFlow(id) {
@@ -520,14 +543,44 @@ var Engine = class _Engine {
520
543
  }
521
544
  /** The shared `@patter` properties, for a live state inspector: each with its ref, type, current
522
545
  * value, declared default (for reset), and enum options. Mirrors the Unity / Godot ports. */
546
+ /** The run's decisions, in order, each naming the flow it happened in. Empty unless the
547
+ * run was opened with `log: true`. A flow's own log stays flow-local; this is the only
548
+ * place a story spanning several flows reads as one sequence. */
549
+ log() {
550
+ return this.engineLog;
551
+ }
552
+ /** Drop the retained entries. `seq` does NOT restart: two reads of a log either side of a
553
+ * clear still agree about what came first. */
554
+ clearLog() {
555
+ this.engineLog.length = 0;
556
+ }
557
+ /** Live tap on the run's decisions, for tooling that wants them as they happen rather than
558
+ * retained. Returns its own unsubscribe. */
559
+ onTrace(handler) {
560
+ this.engineTraceHandlers.add(handler);
561
+ return () => this.engineTraceHandlers.delete(handler);
562
+ }
563
+ emitEngine(flow, event, scene) {
564
+ for (const h of this.engineTraceHandlers) h(flow, event);
565
+ if (!this.host.logEnabled) return;
566
+ this.engineLog.push({ ...event, flow, seq: this.engineLog.length, ...scene ? { scene } : {} });
567
+ }
523
568
  listProperties() {
524
569
  return this.host.patterSharedDecls.map((d) => ({
525
- ref: `@${d.name}`,
570
+ name: d.name,
571
+ // The qualified address, matching what the bag composes for every other scope.
572
+ // `@gold` still resolves on input; it is the shorthand, not the address.
573
+ path: `@patter.${d.name}`,
526
574
  type: d.type,
527
575
  values: d.values,
528
576
  stages: d.stages,
529
577
  value: this.getProperty(`@${d.name}`),
530
- default: declDefault(d)
578
+ default: (0, import_scoperegistry2.defaultFor)(d),
579
+ // Part of the shared row. Always true here today: `toDecl` never sets it, because
580
+ // Patter has no read-only shared property. The Storylet Engine does declare them
581
+ // and its panels disable the editor accordingly, so the field is carried rather
582
+ // than dropped - and the day a read-only @patter property exists, the row says so.
583
+ writable: d.writable ?? true
531
584
  }));
532
585
  }
533
586
  // @scene is scene-namespaced and needs a flow's current scene - silently
@@ -560,7 +613,7 @@ var Engine = class _Engine {
560
613
  shared: this.host.shared.save(),
561
614
  sharedVisits: Object.fromEntries(this.host.sharedVisits),
562
615
  sharedSelectors: serialiseSelectors(this.host.sharedSelectors),
563
- stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, { ...bag }])),
616
+ stageBags: Object.fromEntries([...this.host.stageBags].map(([s, bag]) => [s, bag.save()])),
564
617
  flows
565
618
  };
566
619
  }
@@ -573,7 +626,13 @@ var Engine = class _Engine {
573
626
  this.host.sharedSelectors.clear();
574
627
  for (const [id, st] of deserialiseSelectors(save.sharedSelectors)) this.host.sharedSelectors.set(id, st);
575
628
  this.host.stageBags.clear();
576
- for (const [s, bag] of Object.entries(save.stageBags ?? {})) this.host.stageBags.set(s, { ...bag });
629
+ for (const [s, values] of Object.entries(save.stageBags ?? {})) {
630
+ const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
631
+ const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => shared.has(d.name.toLowerCase()));
632
+ const bag = new import_scoperegistry2.PropertyBag(decls);
633
+ bag.load(values);
634
+ this.host.stageBags.set(s, bag);
635
+ }
577
636
  this.flowsById.clear();
578
637
  for (const [id, snap] of Object.entries(save.flows)) {
579
638
  const flow = new Flow(id, this.host, this.defaultSeed);
@@ -616,6 +675,7 @@ var Flow = class {
616
675
  // The SHARED halves live on the host (`host.shared` / `host.stageBags`). Each
617
676
  // resolver presents one merged scope, routing each property to its half by the
618
677
  // declared `shared` flag.
678
+ /** This flow's per-scene LOCAL scene props; see FlowHost.stageBags. */
619
679
  sceneBags = /* @__PURE__ */ new Map();
620
680
  patterResolver = {
621
681
  get: (n) => this.host.patterSharedNames.has(n) ? this.host.shared.get("patter", n) : this.local.get("patter", n),
@@ -629,13 +689,13 @@ var Flow = class {
629
689
  const s = this.currentSceneId;
630
690
  if (s === null) return void 0;
631
691
  const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
632
- return bag?.[n];
692
+ return bag?.get(n);
633
693
  },
634
694
  set: (n, v) => {
635
695
  const s = this.currentSceneId;
636
696
  if (s === null) return;
637
697
  const bag = this.host.sceneSharedNames.get(s)?.has(n) ? this.host.stageBags.get(s) : this.sceneBags.get(s);
638
- if (bag) bag[n] = v;
698
+ if (bag) bag.set(n, v);
639
699
  }
640
700
  };
641
701
  // The eval context is built ONCE: every constituent resolves live state at
@@ -644,6 +704,9 @@ var Flow = class {
644
704
  // `local`/`sceneBags`/`currentSceneId`; the host callbacks read current flow
645
705
  // fields). Rebuilding it per evaluation was the engine's hottest allocation.
646
706
  evalCtx;
707
+ flowLog = [];
708
+ /** Monotonic across the flow's life; survives clearLog so two reads agree on order. */
709
+ flowSeq = 0;
647
710
  constructor(id, host, seed) {
648
711
  this.id = id;
649
712
  this.host = host;
@@ -869,7 +932,17 @@ var Flow = class {
869
932
  this.stack.pop();
870
933
  continue;
871
934
  }
935
+ const from = frame.index;
872
936
  while (frame.index < children.length && !this.eligible(children[frame.index])) frame.index++;
937
+ if (this.host.logEnabled && frame.index !== from) {
938
+ this.emit({
939
+ type: "select",
940
+ group: frame.containerId,
941
+ selector: "run",
942
+ children: children.slice(from, frame.index + 1).map((c, i) => ({ id: c.id, eligible: from + i === frame.index })),
943
+ picked: children[frame.index]?.id ?? null
944
+ });
945
+ }
873
946
  if (frame.index >= children.length) {
874
947
  this.stack.pop();
875
948
  continue;
@@ -877,6 +950,34 @@ var Flow = class {
877
950
  this.enterChild(children[frame.index++]);
878
951
  }
879
952
  }
953
+ /**
954
+ * THIS flow's own kernel bags: its not-shared `@patter` half and its per-scene `@scene`
955
+ * props, each prefixed with the flow id so one path space holds every flow. The shared
956
+ * halves are the Engine's listBags.
957
+ */
958
+ listBags() {
959
+ const mounts = [{ bag: this.local.ownedBag("patter"), pathPrefix: `${this.id}/@patter.` }];
960
+ for (const [sceneId, bag] of this.sceneBags) mounts.push({ bag, pathPrefix: `${this.id}/@scene:${sceneId}.` });
961
+ return mounts;
962
+ }
963
+ /** This flow's decisions, in order. Empty unless the run was opened with `log: true`.
964
+ * The engine's log carries the same events tagged with the flow; this one is what a
965
+ * single conversation reads as. */
966
+ log() {
967
+ return this.flowLog;
968
+ }
969
+ /** Drop the retained entries. `seq` keeps counting, so order survives a clear. */
970
+ clearLog() {
971
+ this.flowLog.length = 0;
972
+ }
973
+ /** Record one decision, on this flow's log and the engine's. Cheap to call with logging
974
+ * off: the entry is never built. */
975
+ emit(event) {
976
+ const scene = this.currentSceneId ?? void 0;
977
+ this.host.emitEngine(this.id, event, scene);
978
+ if (!this.host.logEnabled) return;
979
+ this.flowLog.push({ ...event, seq: this.flowSeq++, ...scene ? { scene } : {} });
980
+ }
880
981
  /** The options of a pending choice (empty when not at a choice point). */
881
982
  getChoices() {
882
983
  return this.pendingChoice?.options ?? [];
@@ -889,6 +990,7 @@ var Flow = class {
889
990
  if (!option) throw new Error(`unknown choice option: ${id}`);
890
991
  if (!option.eligible) throw new Error(`choice option is not eligible: ${id}`);
891
992
  const node = choice.byId.get(id);
993
+ this.emit({ type: "chose", group: choice.groupId, option: id });
892
994
  this.pendingChoice = null;
893
995
  this.pendingPromptBeat = this.host.replayPromptOnChoose ? this.promptBeatOf(node) ?? null : null;
894
996
  this.pendingPromptOwnerId = this.pendingPromptBeat ? node.id : null;
@@ -922,7 +1024,7 @@ var Flow = class {
922
1024
  return {
923
1025
  scopes: this.local.save(),
924
1026
  // owned scope "patter" = the NOT-shared globals (@scene saved separately)
925
- sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, { ...bag }])),
1027
+ sceneBags: Object.fromEntries([...this.sceneBags].map(([s, bag]) => [s, bag.save()])),
926
1028
  rngState: this.rngState,
927
1029
  visits: Object.fromEntries(this.visitCounts),
928
1030
  cursor: {
@@ -960,7 +1062,13 @@ var Flow = class {
960
1062
  }
961
1063
  return { ...frame };
962
1064
  });
963
- this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s, bag]) => [s, { ...bag }]));
1065
+ this.sceneBags = new Map(Object.entries(snap.sceneBags ?? {}).map(([s, values]) => {
1066
+ const shared = this.host.sceneSharedNames.get(s) ?? /* @__PURE__ */ new Set();
1067
+ const decls = (this.host.bundle.scenes[s]?.sceneProps ?? []).filter((d) => !shared.has(d.name.toLowerCase()));
1068
+ const bag = new import_scoperegistry2.PropertyBag(decls);
1069
+ bag.load(values);
1070
+ return [s, bag];
1071
+ }));
964
1072
  this.local = this.freshLocal();
965
1073
  this.local.load(snap.scopes);
966
1074
  this.activeSnippet = null;
@@ -1054,6 +1162,7 @@ var Flow = class {
1054
1162
  byId.set(child.id, child);
1055
1163
  }
1056
1164
  if (options.length > 0) {
1165
+ this.emit({ type: "choice", group: group.id, options: options.map((o) => ({ id: o.id, eligible: o.eligible })) });
1057
1166
  this.pendingChoice = { groupId: group.id, options, byId };
1058
1167
  return;
1059
1168
  }
@@ -1062,6 +1171,7 @@ var Flow = class {
1062
1171
  this.enterChild(fallback);
1063
1172
  return;
1064
1173
  }
1174
+ this.emit({ type: "dry", group: group.id });
1065
1175
  this.host.onDryChoice?.(group.id);
1066
1176
  }
1067
1177
  // -- Jumps (jump / call-return) ----------------------------------------
@@ -1076,6 +1186,7 @@ var Flow = class {
1076
1186
  * hard-ends the flow regardless of the callstack.
1077
1187
  */
1078
1188
  enterTarget(to, mode) {
1189
+ this.emit({ type: "jump", to, mode });
1079
1190
  if (to === "END") {
1080
1191
  this.flowEnded = true;
1081
1192
  this.stack = [];
@@ -1107,17 +1218,28 @@ var Flow = class {
1107
1218
  }
1108
1219
  // -- Selectors ------------------------------------------------------------
1109
1220
  selectChild(group) {
1221
+ const verdicts = group.children.map((c) => ({ id: c.id, eligible: this.eligible(c) }));
1110
1222
  const eligible = group.children.filter((c) => this.eligible(c));
1111
- if (eligible.length === 0) return null;
1223
+ const order = group.options?.order ?? "sequential";
1224
+ const exhaust = group.options?.exhaust ?? "once";
1225
+ const trace = (picked) => {
1226
+ this.emit({
1227
+ type: "select",
1228
+ group: group.id,
1229
+ selector: group.selector ?? "default",
1230
+ ...group.selector === "sequence" ? { order, exhaust } : {},
1231
+ children: verdicts,
1232
+ picked: picked?.id ?? null
1233
+ });
1234
+ return picked;
1235
+ };
1236
+ if (eligible.length === 0) return trace(null);
1112
1237
  const st = this.selectorState(group);
1113
1238
  switch (group.selector) {
1114
1239
  case "branch":
1115
- return eligible[0];
1116
- case "sequence": {
1117
- const order = group.options?.order ?? "sequential";
1118
- const exhaust = group.options?.exhaust ?? "once";
1119
- return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
1120
- }
1240
+ return trace(eligible[0]);
1241
+ case "sequence":
1242
+ return trace(order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st));
1121
1243
  case "run":
1122
1244
  case "choice":
1123
1245
  default:
@@ -1235,7 +1357,10 @@ var Flow = class {
1235
1357
  // -- Effects + expressions ------------------------------------------------
1236
1358
  runEffects(effects) {
1237
1359
  for (const e of effects ?? []) {
1238
- this.setProperty(e.target, this.evalExpr(e.value));
1360
+ const value = this.evalExpr(e.value);
1361
+ const prev = this.host.logEnabled ? this.getProperty(e.target) : void 0;
1362
+ this.setProperty(e.target, value);
1363
+ this.emit({ type: "write", target: e.target, value, ...prev !== void 0 ? { prev } : {} });
1239
1364
  }
1240
1365
  }
1241
1366
  eligible(node) {
@@ -1379,7 +1504,7 @@ var Flow = class {
1379
1504
  }
1380
1505
  /** The per-flow registry: the NOT-shared `@patter` globals (the shared ones live on the host). */
1381
1506
  freshLocal() {
1382
- return new import_scoperegistry.ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls);
1507
+ return new import_scoperegistry.ScopeRegistry().defineOwned("patter", this.host.patterLocalDecls, "@patter.");
1383
1508
  }
1384
1509
  /**
1385
1510
  * Seed a scene's `@scene` props (spec §7). The not-shared props seed THIS flow's
@@ -1390,27 +1515,18 @@ var Flow = class {
1390
1515
  */
1391
1516
  seedScene(scene) {
1392
1517
  const shared = this.host.sceneSharedNames.get(scene.id) ?? /* @__PURE__ */ new Set();
1518
+ const props = scene.sceneProps ?? [];
1393
1519
  if (!this.sceneBags.has(scene.id)) {
1394
- const bag = {};
1395
- for (const decl of scene.sceneProps ?? []) {
1396
- const name = decl.name.toLowerCase();
1397
- if (!shared.has(name)) bag[name] = sceneDefault(decl);
1398
- }
1399
- this.sceneBags.set(scene.id, bag);
1520
+ this.sceneBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => !shared.has(d.name.toLowerCase()))));
1400
1521
  }
1401
1522
  if (!this.host.stageBags.has(scene.id)) {
1402
- const bag = {};
1403
- for (const decl of scene.sceneProps ?? []) {
1404
- const name = decl.name.toLowerCase();
1405
- if (shared.has(name)) bag[name] = sceneDefault(decl);
1406
- }
1407
- this.host.stageBags.set(scene.id, bag);
1523
+ this.host.stageBags.set(scene.id, new import_scoperegistry2.PropertyBag(props.filter((d) => shared.has(d.name.toLowerCase()))));
1408
1524
  }
1409
1525
  for (const decl of scene.sceneProps ?? []) {
1410
1526
  if (!decl.temporary) continue;
1411
1527
  const name = decl.name.toLowerCase();
1412
1528
  const bag = shared.has(name) ? this.host.stageBags.get(scene.id) : this.sceneBags.get(scene.id);
1413
- if (bag) bag[name] = sceneDefault(decl);
1529
+ if (bag) bag.set(name, (0, import_scoperegistry2.defaultFor)(decl));
1414
1530
  }
1415
1531
  }
1416
1532
  };
@@ -1448,47 +1564,13 @@ function deserialiseSelectors(rec) {
1448
1564
  function toDecl(decl) {
1449
1565
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default };
1450
1566
  }
1451
- function declDefault(d) {
1452
- if (d.default !== void 0) return d.default;
1453
- switch (d.type) {
1454
- case "number":
1455
- return 0;
1456
- case "string":
1457
- return "";
1458
- case "flags":
1459
- return [];
1460
- case "enum":
1461
- return d.values?.[0] ?? "";
1462
- case "quality":
1463
- return d.stages?.[0] ?? "";
1464
- default:
1465
- return false;
1466
- }
1467
- }
1468
1567
  function toForeignDecl(decl) {
1469
1568
  return { name: decl.name, type: decl.type, values: decl.values, stages: decl.stages, default: decl.default, writable: decl.writable };
1470
1569
  }
1471
- function hostScopeDefault(decl) {
1472
- if (decl.default !== void 0) return decl.default;
1473
- switch (decl.type) {
1474
- case "boolean":
1475
- return false;
1476
- case "number":
1477
- return 0;
1478
- case "string":
1479
- return "";
1480
- case "flags":
1481
- return [];
1482
- case "enum":
1483
- return decl.values?.[0] ?? "";
1484
- case "quality":
1485
- return decl.stages?.[0] ?? "";
1486
- }
1487
- }
1488
1570
  function selfBackedResolver(decls) {
1489
1571
  const key = (name) => name.toLowerCase();
1490
1572
  const bag = /* @__PURE__ */ new Map();
1491
- for (const d of decls) bag.set(key(d.name), hostScopeDefault(d));
1573
+ for (const d of decls) bag.set(key(d.name), (0, import_scoperegistry2.defaultFor)(d));
1492
1574
  return {
1493
1575
  get: (name) => bag.get(key(name)),
1494
1576
  set: (name, value) => {
@@ -1496,23 +1578,6 @@ function selfBackedResolver(decls) {
1496
1578
  }
1497
1579
  };
1498
1580
  }
1499
- function sceneDefault(decl) {
1500
- if (decl.default !== void 0) return decl.default;
1501
- switch (decl.type) {
1502
- case "boolean":
1503
- return false;
1504
- case "number":
1505
- return 0;
1506
- case "string":
1507
- return "";
1508
- case "flags":
1509
- return [];
1510
- case "enum":
1511
- return decl.values?.[0] ?? "";
1512
- case "quality":
1513
- return decl.stages?.[0] ?? "";
1514
- }
1515
- }
1516
1581
  function truthy(v) {
1517
1582
  if (typeof v === "boolean") return v;
1518
1583
  if (typeof v === "number") return v !== 0;