@wildwinter/scoperegistry 0.4.0 → 0.5.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.cjs CHANGED
@@ -1,5 +1,69 @@
1
1
  'use strict';
2
2
 
3
+ // src/state-logger.ts
4
+ function diffState(prev, next) {
5
+ const changes = [];
6
+ const paths = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
7
+ for (const path of [...paths].sort()) {
8
+ const from = prev[path], to = next[path];
9
+ if (JSON.stringify(from) !== JSON.stringify(to)) changes.push({ path, from, to });
10
+ }
11
+ return changes;
12
+ }
13
+ var show = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
14
+ var prefixOf = (m) => m.pathPrefix ?? m.bag.pathPrefix;
15
+ function createStateLogger(adapter, opts = {}) {
16
+ const sink = opts.sink ?? ((line) => console.log(line));
17
+ const label = opts.label ?? "";
18
+ const emit = (c) => {
19
+ sink(`${label}${c.path}: ${show(c.from)} -> ${show(c.to)}`);
20
+ };
21
+ const full = () => {
22
+ const out = {};
23
+ for (const m of adapter.mounts()) {
24
+ const prefix = prefixOf(m);
25
+ for (const [name, value] of Object.entries(m.bag.values)) out[prefix + name] = value;
26
+ }
27
+ Object.assign(out, adapter.extra?.() ?? {});
28
+ return structuredClone(out);
29
+ };
30
+ let baseline = full();
31
+ let pushed = [];
32
+ let mounted = [];
33
+ const hook = (prefix, bag) => bag.onAudit((change) => {
34
+ const c = structuredClone({ path: prefix + change.name, from: change.prev, to: change.next });
35
+ emit(c);
36
+ pushed.push(c);
37
+ baseline[c.path] = structuredClone(change.next);
38
+ });
39
+ const mount = () => {
40
+ const mounts = adapter.mounts();
41
+ const same = mounted.length === mounts.length && mounts.every((m, i) => mounted[i].bag === m.bag);
42
+ if (same) return;
43
+ for (const m of mounted) m.off();
44
+ mounted = mounts.map((m) => ({ bag: m.bag, off: hook(prefixOf(m), m.bag) }));
45
+ };
46
+ mount();
47
+ return {
48
+ snapshot: full,
49
+ capture() {
50
+ const next = full();
51
+ const diffed = diffState(baseline, next);
52
+ for (const c of diffed) emit(c);
53
+ const changes = [...pushed, ...diffed];
54
+ pushed = [];
55
+ baseline = next;
56
+ mount();
57
+ return changes;
58
+ },
59
+ dispose() {
60
+ for (const m of mounted) m.off();
61
+ mounted = [];
62
+ pushed = [];
63
+ }
64
+ };
65
+ }
66
+
3
67
  // src/index.ts
4
68
  var SUPPORTED_SPEC_VERSIONS = [1];
5
69
  function readScopeRegistrySpec(source) {
@@ -354,7 +418,9 @@ exports.PropertyBag = PropertyBag;
354
418
  exports.SAVE_FRAGMENT_VERSION = SAVE_FRAGMENT_VERSION;
355
419
  exports.SUPPORTED_SPEC_VERSIONS = SUPPORTED_SPEC_VERSIONS;
356
420
  exports.ScopeRegistry = ScopeRegistry;
421
+ exports.createStateLogger = createStateLogger;
357
422
  exports.defaultFor = defaultFor;
423
+ exports.diffState = diffState;
358
424
  exports.readScopeRegistrySpec = readScopeRegistrySpec;
359
425
  //# sourceMappingURL=index.cjs.map
360
426
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n /** A quality's ordered ladder of stage names (quality.md). */\n stages?: string[];\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// PropertyBag - the state kernel's unit of state (added 0.2.0; design:\n// storylets-new/design/engine-runtimes.md 3.1). A typed, declared property\n// bag with defaults, the firing rule (engine writes notify subscribers;\n// host writes are silent but always auditable), examiner rows, one\n// sanctioned clone door, and bare-value save/load. Owned registry scopes\n// are bags; products may also hold bag families of their own (per-box,\n// per-scene) and mount the shared ones.\n// ---------------------------------------------------------------------------\n\n/** One property change. `silent` marks a host write (the firing rule: it\n * reaches the audit hook but not subscribers); `reason` is the host's own\n * note for its log. */\nexport interface BagChange {\n name: string;\n prev?: ScalarValue;\n next: ScalarValue;\n silent: boolean;\n reason?: string;\n}\n\n/** One examiner row: what a property examiner/editor needs to render and\n * edit a declared property. */\nexport interface PropertyRow {\n name: string;\n /** The address this property answers to - what getProperty/setProperty take.\n * A bag composes it from its own `pathPrefix` and the name, so a row is\n * self-describing: an examiner can render and write a row without being told\n * separately where it came from.\n *\n * The PREFIX CARRIES ITS OWN SEPARATOR rather than the bag assuming a dot,\n * because a prefix is not always a bare scope token: the Storylet Engine\n * addresses a deck's properties as `deck.<id>.name`, so the prefix is already\n * a dotted path. Patterplay's `@patter.gold` and `@scene.mood` are the plain\n * case. (`@gold` also resolves - splitRef defaults an unqualified name to the\n * patter scope - but it is the shorthand, not the address a row reports.)\n *\n * With no prefix this is just the name. Both families forked this interface\n * to add exactly this field - once per runtime - which is the same reason\n * `stages` is here. */\n path: string;\n type: PropertyType;\n value: ScalarValue | undefined;\n default: ScalarValue;\n values?: string[];\n /** A quality's ordered stage ladder, so an inspector can offer the stages\n * instead of a free-text box. `quality` has been in PropertyType since the\n * ladder landed, and the evaluator compares stages by LADDER POSITION and\n * refuses an unknown one, so free text is not a soft failure: a typo breaks\n * play rather than being corrected. This row is the only thing an examiner\n * sees, so a ladder it cannot carry is a ladder no editor can offer. One\n * consumer forked this whole interface to add the field; the field belongs\n * here, beside the `values` it is the closed-set twin of. */\n stages?: string[];\n writable: boolean;\n}\n\nexport class PropertyBag {\n /** The live values record (stable identity across reseed, so an\n * EvalContext built over it stays valid). Read-path for evaluation;\n * writes go through `set` so the firing rule applies. */\n readonly values: Record<string, ScalarValue> = {};\n private decls = new Map<string, ScopeDeclaration>();\n private readonly subscribers = new Set<(change: BagChange) => void>();\n private readonly auditors = new Set<(change: BagChange) => void>();\n /** Name normalisation policy: lowercase by default (the registry's\n * long-standing contract); a product whose names are case-significant\n * passes identity. */\n private readonly norm: (name: string) => string;\n\n /** The address prefix this bag's rows carry, separator included (`@`,\n * `@scene.`, `world.`, `deck.<id>.`). Empty means a row's path is its name. */\n readonly pathPrefix: string;\n\n constructor(\n declarations: ScopeDeclaration[] = [],\n opts?: { normalise?: (name: string) => string; pathPrefix?: string },\n ) {\n this.norm = opts?.normalise ?? ((n) => n.toLowerCase());\n this.pathPrefix = opts?.pathPrefix ?? \"\";\n this.seed(declarations);\n }\n\n private seed(declarations: ScopeDeclaration[]): void {\n for (const d of declarations) {\n const name = this.norm(d.name);\n this.decls.set(name, d);\n // Cloned so bags seeded from one declaration set never share a\n // mutable default (flags arrays).\n this.values[name] = structuredClone(d.default ?? defaultFor(d));\n }\n }\n\n get(name: string): ScalarValue | undefined {\n return this.values[this.norm(name)];\n }\n\n /** Write a property. Engine writes (the default) notify subscribers;\n * pass `silent: true` for a host write, which reaches only the audit\n * hook. Throws on a read-only property. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string }): BagChange {\n const n = this.norm(name);\n if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);\n const change: BagChange = {\n name: n,\n prev: this.values[n],\n next: value,\n silent: opts?.silent ?? false,\n reason: opts?.reason,\n };\n this.values[n] = value;\n for (const audit of this.auditors) audit(change);\n if (!change.silent) for (const fn of this.subscribers) fn(change);\n return change;\n }\n\n /** Notified of engine (non-silent) writes. Returns the unsubscribe. */\n subscribe(fn: (change: BagChange) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n /** Notified of EVERY write, silent or not. Returns the unsubscribe. */\n onAudit(fn: (change: BagChange) => void): () => void {\n this.auditors.add(fn);\n return () => this.auditors.delete(fn);\n }\n\n /** Examiner rows: the declared surface only (stray values are storage,\n * not surface). */\n rows(): PropertyRow[] {\n return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), undefined, name, this.pathPrefix));\n }\n\n declarations(): ScopeDeclaration[] {\n return [...this.decls.values()];\n }\n\n /** The one sanctioned copy door: values deep-copied, declarations\n * duplicated, the normalisation policy carried, subscriptions NOT\n * carried. */\n clone(): PropertyBag {\n const c = new PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });\n c.decls = new Map(this.decls);\n Object.assign(c.values, structuredClone(this.values));\n return c;\n }\n\n /** Clear and re-seed from new declarations, in place (the values record\n * keeps its identity, so contexts built over it stay valid). */\n reseed(declarations: ScopeDeclaration[]): void {\n for (const k of Object.keys(this.values)) delete this.values[k];\n this.decls.clear();\n this.seed(declarations);\n }\n\n /** Bare values, ready to embed in a product's save. */\n save(): Record<string, ScalarValue> {\n return structuredClone(this.values);\n }\n\n /** Lay saved values over the current ones (call after a fresh seed:\n * orphans land as strays, new declarations keep their defaults; the\n * product decides whether to prune). Does not fire events. */\n load(values: Record<string, ScalarValue>): void {\n for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;\n }\n}\n\nfunction rowFor(\n d: ScopeDeclaration,\n value: ScalarValue | undefined,\n writable?: boolean,\n name?: string,\n pathPrefix = \"\",\n): PropertyRow {\n const rowName = name ?? d.name.toLowerCase();\n return {\n name: rowName,\n path: pathPrefix + rowName,\n type: d.type,\n value,\n default: d.default ?? defaultFor(d),\n ...(d.values !== undefined ? { values: d.values } : {}),\n // `stages` was added to the row so an examiner could offer a quality's ladder\n // instead of a free-text box, and then never populated here: every quality row\n // this function built came out without one. Fixed 2026-09-02.\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n writable: writable ?? d.writable ?? true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: PropertyBag;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\n/** The versioned owned-state fragment both product save envelopes embed\n * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[], pathPrefix?: string): this {\n // The scope knows its own token, so its rows can address themselves: `world.hp`.\n // The ADDRESS GRAMMAR is the product's, though, not the registry's - Patterplay\n // writes `@patter.gold` where the Storylet Engine writes `world.gold` - so a\n // caller may say how its addresses look. A bag MOUNTED here keeps whatever prefix\n // its holder gave it: the holder owns the addressing.\n return this.mountOwned(token, new PropertyBag(declarations, { pathPrefix: pathPrefix ?? `${token}.` }));\n }\n\n /**\n * Attach an EXISTING bag as an owned scope - the shared-container move: a\n * host (or the other product) holds the bag; this registry reads, writes\n * and lists it like its own, but the holder saves it.\n */\n mountOwned(token: string, bag: PropertyBag): this {\n this.assertFree(token);\n this.scopes.set(token, { kind: \"owned\", bag });\n return this;\n }\n\n /** An owned scope's bag (subscribe, audit, rows live there). */\n ownedBag(token: string): PropertyBag {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n return e.bag;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.ownedBag(token).reseed(declarations);\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n return e.kind === \"owned\" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());\n }\n\n /** Write a property (an ENGINE write: the bag's subscribers fire; use\n * the bag directly for silent host writes). Throws on an unknown or\n * read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n if (e.kind === \"owned\") {\n try {\n e.bag.set(name, value);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n e.resolver.set!(n, value);\n }\n\n private foreignWritable(e: ForeignScope, name: string): boolean {\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /** Examiner rows across every scope with a declared surface: owned bags\n * first, then declared foreign scopes (values read through, writability\n * reflecting the resolver). Opaque foreign scopes are not listed. */\n listProperties(): ({ scope: string } & PropertyRow)[] {\n const out: ({ scope: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...row });\n } else {\n for (const d of e.decls.values()) {\n out.push({\n scope: token,\n ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase()),\n undefined, `${token}.`),\n });\n }\n }\n }\n return out;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\n }\n // The quality channel (quality.md): declared here once, so a host that\n // registers a quality gets ordering comparisons and advance() with no\n // further wiring. Only added when a quality exists, so contexts stay\n // byte-identical for products that declare none.\n const qualities = this.qualityLadders();\n return qualities.size === 0 ? { scopes, host } : {\n scopes, host,\n qualities: (scope, name) => qualities.get(scope)?.get(name.toLowerCase()),\n };\n }\n\n /** Every quality declaration's ladder, keyed scope token then name. */\n private qualityLadders(): Map<string, Map<string, readonly string[]>> {\n const out = new Map<string, Map<string, readonly string[]>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n for (const d of decls) {\n if (d.type !== \"quality\" || d.stages === undefined) continue;\n let m = out.get(token);\n if (!m) { m = new Map(); out.set(token, m); }\n m.set(d.name.toLowerCase(), d.stages);\n }\n }\n return out;\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>();\n for (const d of decls) m.set(d.name.toLowerCase(), {\n type: d.type, enumValues: d.values,\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned,\n * host-saved), as bare bags - the 0.1.x shape, kept stable so existing\n * consumers' save formats are untouched. A product embedding the\n * versioned cross-product shape uses `saveFragment`. */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = e.bag.save();\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes\n * are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") e.bag.load(vals);\n }\n }\n\n /** The versioned owned-state fragment (the one serialisation shape both\n * product families' save envelopes embed when they adopt the kernel;\n * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /** Restore from a versioned fragment; an unsupported version throws. */\n loadFragment(fragment: OwnedStateFragment): void {\n if (fragment.version !== SAVE_FRAGMENT_VERSION) {\n throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);\n }\n this.load(fragment.scopes);\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\n/** The seed value for a declared property: its own `default`, else the type's.\n *\n * Exported because it was being written again wherever a declaration needed seeding, and a\n * copy of a defaults table is a copy that stops agreeing. Patterplay carried three of them in\n * one file, for its shared decls, its host-scope decls and its scene decls - three declaration\n * TYPES, one behaviour, and nothing to notice if a case drifted. The parameter is structurally\n * typed for exactly that reason: anything with `type` and the optional `default` / `values` /\n * `stages` fits, whatever the caller calls its declaration.\n *\n * A quality seeds at the FIRST rung of its ladder: the ladder's start is the story's start. */\nexport function defaultFor(d: Pick<ScopeDeclaration, \"type\" | \"default\" | \"values\" | \"stages\">): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n // A quality starts at the first rung of its ladder.\n case \"quality\": return d.stages?.[0] ?? \"\";\n // Unreachable for a well-typed declaration, and deliberately present anyway: a bundle\n // is DATA, and a hand-edited or newer-than-this-build one can carry a type string the\n // union does not have. Falling off the switch would seed `undefined`, which is not a\n // ScalarValue and travels a long way before it fails. Patterplay's copy of this had the\n // guard and this one did not, which is the drift you only find by removing a duplicate.\n default: return false;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/state-logger.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The state logger: what changed in the state kernel, as it changes.\n//\n// Both product families shipped one of these, in four runtimes each, and they\n// were not the same shape. The Storylet Engine's was PUSH-based on the\n// PropertyBag audit hook - a write logs the moment it lands, with the previous\n// value straight off the event. Patterplay's diffed whole saveGame() snapshots,\n// so it could only ever say what changed BETWEEN captures, and only for state a\n// save persists. This is the first one, because it is the better one: a diff\n// cannot tell a write from a write-and-write-back, cannot name the reason a\n// host attached to a write, and cannot see a value that changed and changed\n// back.\n//\n// A diff is still needed, and is kept, for everything that is NOT in a bag: a\n// product's own non-property state (turns, cooldowns, visit counts) arrives\n// through the adapter's `extra()` and is diffed on capture. Bags replaced\n// wholesale by a load fire no audit events either, so capture() re-reads and\n// re-mounts.\n//\n// Line format: `${label}${path}: ${from} -> ${to}`, `<unset>` for a value that\n// was not there.\n// ---------------------------------------------------------------------------\n\nimport type { PropertyBag, ScalarValue } from \"./index.js\";\n\n/** A flattened snapshot: path -> value. */\nexport type StateSnapshot = Record<string, ScalarValue>;\n\nexport interface StateChange {\n path: string;\n from: ScalarValue | undefined;\n to: ScalarValue | undefined;\n}\n\n/**\n * One bag on the logger's path space.\n *\n * Named for the LOG, not the bag: a product may already have its own type for enumerating\n * bags (the Storylet Engine's LogMount, which labels a mount \"story\" for its own purposes),\n * and in the ported runtimes both land in one namespace. They are also not the same thing,\n * which the prefix rule below is about.\n *\n * `pathPrefix` is used VERBATIM, separator included, exactly as the bag's own\n * is - it is not a scope token with a dot implied. Omit it and the bag's own\n * `pathPrefix` is used, which is what a product wants whenever its log paths\n * and its property addresses agree.\n *\n * They do not always agree, which is why this can be overridden: Patterplay\n * addresses a scene property `@scene.mood` (relative to the flow's current\n * scene) but has to LOG it as `@scene:kitchen.mood`, because a log covering\n * several scenes needs to say which one.\n */\nexport interface LogMount {\n bag: PropertyBag;\n pathPrefix?: string;\n}\n\n/** What a product supplies: its kernel bags (re-read on every capture, so a\n * product that replaces its bags on load re-mounts), and its non-property\n * state as flattened paths. */\nexport interface StateLoggerAdapter {\n mounts(): LogMount[];\n extra?(): StateSnapshot;\n}\n\nexport interface StateLoggerOptions {\n /** Where lines go; defaults to console.log. */\n sink?: (line: string) => void;\n /** Prefixed to every line, verbatim (e.g. `\"[board] \"`). */\n label?: string;\n}\n\nexport interface StateLogger {\n /** The current flattened state. Logs nothing. */\n snapshot(): StateSnapshot;\n /** Everything since the last capture: the audited writes already logged as\n * they landed, plus anything that changed WITHOUT an audit event, diffed,\n * logged and re-baselined. */\n capture(): StateChange[];\n /** Unhook the bag auditors. The logger is inert afterwards. */\n dispose(): void;\n}\n\n/** The sorted set of paths that differ between two snapshots. */\nexport function diffState(prev: StateSnapshot, next: StateSnapshot): StateChange[] {\n const changes: StateChange[] = [];\n const paths = new Set([...Object.keys(prev), ...Object.keys(next)]);\n for (const path of [...paths].sort()) {\n const from = prev[path], to = next[path];\n if (JSON.stringify(from) !== JSON.stringify(to)) changes.push({ path, from, to });\n }\n return changes;\n}\n\nconst show = (v: ScalarValue | undefined): string => (v === undefined ? \"<unset>\" : JSON.stringify(v));\n\nconst prefixOf = (m: LogMount): string => m.pathPrefix ?? m.bag.pathPrefix;\n\nexport function createStateLogger(adapter: StateLoggerAdapter, opts: StateLoggerOptions = {}): StateLogger {\n const sink = opts.sink ?? ((line: string) => console.log(line));\n const label = opts.label ?? \"\";\n const emit = (c: StateChange): void => { sink(`${label}${c.path}: ${show(c.from)} -> ${show(c.to)}`); };\n\n const full = (): StateSnapshot => {\n const out: StateSnapshot = {};\n for (const m of adapter.mounts()) {\n const prefix = prefixOf(m);\n for (const [name, value] of Object.entries(m.bag.values)) out[prefix + name] = value;\n }\n Object.assign(out, adapter.extra?.() ?? {});\n return structuredClone(out);\n };\n\n let baseline = full();\n let pushed: StateChange[] = [];\n let mounted: { bag: PropertyBag; off: () => void }[] = [];\n\n const hook = (prefix: string, bag: PropertyBag): (() => void) =>\n bag.onAudit((change) => {\n // The write logs as it lands, `from` straight off the event; the baseline\n // moves with it so capture() never re-reports what was already said.\n const c: StateChange = structuredClone({ path: prefix + change.name, from: change.prev, to: change.next });\n emit(c);\n pushed.push(c);\n baseline[c.path] = structuredClone(change.next);\n });\n\n const mount = (): void => {\n const mounts = adapter.mounts();\n const same = mounted.length === mounts.length && mounts.every((m, i) => mounted[i]!.bag === m.bag);\n if (same) return;\n for (const m of mounted) m.off();\n mounted = mounts.map((m) => ({ bag: m.bag, off: hook(prefixOf(m), m.bag) }));\n };\n mount();\n\n return {\n snapshot: full,\n capture(): StateChange[] {\n // Whatever arrived WITHOUT an audit event: the adapter's non-property\n // paths, and bag values replaced wholesale by a load (which fires none).\n const next = full();\n const diffed = diffState(baseline, next);\n for (const c of diffed) emit(c);\n const changes = [...pushed, ...diffed];\n pushed = [];\n baseline = next;\n mount(); // a load replaces a product's bags; re-hook them\n return changes;\n },\n dispose(): void {\n for (const m of mounted) m.off();\n mounted = [];\n pushed = [];\n },\n };\n}\n","// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n /** A quality's ordered ladder of stage names (quality.md). */\n stages?: string[];\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// PropertyBag - the state kernel's unit of state (added 0.2.0; design:\n// storylets-new/design/engine-runtimes.md 3.1). A typed, declared property\n// bag with defaults, the firing rule (engine writes notify subscribers;\n// host writes are silent but always auditable), examiner rows, one\n// sanctioned clone door, and bare-value save/load. Owned registry scopes\n// are bags; products may also hold bag families of their own (per-box,\n// per-scene) and mount the shared ones.\n// ---------------------------------------------------------------------------\n\n/** One property change. `silent` marks a host write (the firing rule: it\n * reaches the audit hook but not subscribers); `reason` is the host's own\n * note for its log. */\nexport interface BagChange {\n name: string;\n prev?: ScalarValue;\n next: ScalarValue;\n silent: boolean;\n reason?: string;\n}\n\n/** One examiner row: what a property examiner/editor needs to render and\n * edit a declared property. */\nexport interface PropertyRow {\n name: string;\n /** The address this property answers to - what getProperty/setProperty take.\n * A bag composes it from its own `pathPrefix` and the name, so a row is\n * self-describing: an examiner can render and write a row without being told\n * separately where it came from.\n *\n * The PREFIX CARRIES ITS OWN SEPARATOR rather than the bag assuming a dot,\n * because a prefix is not always a bare scope token: the Storylet Engine\n * addresses a deck's properties as `deck.<id>.name`, so the prefix is already\n * a dotted path. Patterplay's `@patter.gold` and `@scene.mood` are the plain\n * case. (`@gold` also resolves - splitRef defaults an unqualified name to the\n * patter scope - but it is the shorthand, not the address a row reports.)\n *\n * With no prefix this is just the name. Both families forked this interface\n * to add exactly this field - once per runtime - which is the same reason\n * `stages` is here. */\n path: string;\n type: PropertyType;\n value: ScalarValue | undefined;\n default: ScalarValue;\n values?: string[];\n /** A quality's ordered stage ladder, so an inspector can offer the stages\n * instead of a free-text box. `quality` has been in PropertyType since the\n * ladder landed, and the evaluator compares stages by LADDER POSITION and\n * refuses an unknown one, so free text is not a soft failure: a typo breaks\n * play rather than being corrected. This row is the only thing an examiner\n * sees, so a ladder it cannot carry is a ladder no editor can offer. One\n * consumer forked this whole interface to add the field; the field belongs\n * here, beside the `values` it is the closed-set twin of. */\n stages?: string[];\n writable: boolean;\n}\n\nexport class PropertyBag {\n /** The live values record (stable identity across reseed, so an\n * EvalContext built over it stays valid). Read-path for evaluation;\n * writes go through `set` so the firing rule applies. */\n readonly values: Record<string, ScalarValue> = {};\n private decls = new Map<string, ScopeDeclaration>();\n private readonly subscribers = new Set<(change: BagChange) => void>();\n private readonly auditors = new Set<(change: BagChange) => void>();\n /** Name normalisation policy: lowercase by default (the registry's\n * long-standing contract); a product whose names are case-significant\n * passes identity. */\n private readonly norm: (name: string) => string;\n\n /** The address prefix this bag's rows carry, separator included (`@`,\n * `@scene.`, `world.`, `deck.<id>.`). Empty means a row's path is its name. */\n readonly pathPrefix: string;\n\n constructor(\n declarations: ScopeDeclaration[] = [],\n opts?: { normalise?: (name: string) => string; pathPrefix?: string },\n ) {\n this.norm = opts?.normalise ?? ((n) => n.toLowerCase());\n this.pathPrefix = opts?.pathPrefix ?? \"\";\n this.seed(declarations);\n }\n\n private seed(declarations: ScopeDeclaration[]): void {\n for (const d of declarations) {\n const name = this.norm(d.name);\n this.decls.set(name, d);\n // Cloned so bags seeded from one declaration set never share a\n // mutable default (flags arrays).\n this.values[name] = structuredClone(d.default ?? defaultFor(d));\n }\n }\n\n get(name: string): ScalarValue | undefined {\n return this.values[this.norm(name)];\n }\n\n /** Write a property. Engine writes (the default) notify subscribers;\n * pass `silent: true` for a host write, which reaches only the audit\n * hook. Throws on a read-only property. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string }): BagChange {\n const n = this.norm(name);\n if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);\n const change: BagChange = {\n name: n,\n prev: this.values[n],\n next: value,\n silent: opts?.silent ?? false,\n reason: opts?.reason,\n };\n this.values[n] = value;\n for (const audit of this.auditors) audit(change);\n if (!change.silent) for (const fn of this.subscribers) fn(change);\n return change;\n }\n\n /** Notified of engine (non-silent) writes. Returns the unsubscribe. */\n subscribe(fn: (change: BagChange) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n /** Notified of EVERY write, silent or not. Returns the unsubscribe. */\n onAudit(fn: (change: BagChange) => void): () => void {\n this.auditors.add(fn);\n return () => this.auditors.delete(fn);\n }\n\n /** Examiner rows: the declared surface only (stray values are storage,\n * not surface). */\n rows(): PropertyRow[] {\n return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), undefined, name, this.pathPrefix));\n }\n\n declarations(): ScopeDeclaration[] {\n return [...this.decls.values()];\n }\n\n /** The one sanctioned copy door: values deep-copied, declarations\n * duplicated, the normalisation policy carried, subscriptions NOT\n * carried. */\n clone(): PropertyBag {\n const c = new PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });\n c.decls = new Map(this.decls);\n Object.assign(c.values, structuredClone(this.values));\n return c;\n }\n\n /** Clear and re-seed from new declarations, in place (the values record\n * keeps its identity, so contexts built over it stay valid). */\n reseed(declarations: ScopeDeclaration[]): void {\n for (const k of Object.keys(this.values)) delete this.values[k];\n this.decls.clear();\n this.seed(declarations);\n }\n\n /** Bare values, ready to embed in a product's save. */\n save(): Record<string, ScalarValue> {\n return structuredClone(this.values);\n }\n\n /** Lay saved values over the current ones (call after a fresh seed:\n * orphans land as strays, new declarations keep their defaults; the\n * product decides whether to prune). Does not fire events. */\n load(values: Record<string, ScalarValue>): void {\n for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;\n }\n}\n\nfunction rowFor(\n d: ScopeDeclaration,\n value: ScalarValue | undefined,\n writable?: boolean,\n name?: string,\n pathPrefix = \"\",\n): PropertyRow {\n const rowName = name ?? d.name.toLowerCase();\n return {\n name: rowName,\n path: pathPrefix + rowName,\n type: d.type,\n value,\n default: d.default ?? defaultFor(d),\n ...(d.values !== undefined ? { values: d.values } : {}),\n // `stages` was added to the row so an examiner could offer a quality's ladder\n // instead of a free-text box, and then never populated here: every quality row\n // this function built came out without one. Fixed 2026-09-02.\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n writable: writable ?? d.writable ?? true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: PropertyBag;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\n/** The versioned owned-state fragment both product save envelopes embed\n * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[], pathPrefix?: string): this {\n // The scope knows its own token, so its rows can address themselves: `world.hp`.\n // The ADDRESS GRAMMAR is the product's, though, not the registry's - Patterplay\n // writes `@patter.gold` where the Storylet Engine writes `world.gold` - so a\n // caller may say how its addresses look. A bag MOUNTED here keeps whatever prefix\n // its holder gave it: the holder owns the addressing.\n return this.mountOwned(token, new PropertyBag(declarations, { pathPrefix: pathPrefix ?? `${token}.` }));\n }\n\n /**\n * Attach an EXISTING bag as an owned scope - the shared-container move: a\n * host (or the other product) holds the bag; this registry reads, writes\n * and lists it like its own, but the holder saves it.\n */\n mountOwned(token: string, bag: PropertyBag): this {\n this.assertFree(token);\n this.scopes.set(token, { kind: \"owned\", bag });\n return this;\n }\n\n /** An owned scope's bag (subscribe, audit, rows live there). */\n ownedBag(token: string): PropertyBag {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n return e.bag;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.ownedBag(token).reseed(declarations);\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n return e.kind === \"owned\" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());\n }\n\n /** Write a property (an ENGINE write: the bag's subscribers fire; use\n * the bag directly for silent host writes). Throws on an unknown or\n * read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n if (e.kind === \"owned\") {\n try {\n e.bag.set(name, value);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n e.resolver.set!(n, value);\n }\n\n private foreignWritable(e: ForeignScope, name: string): boolean {\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /** Examiner rows across every scope with a declared surface: owned bags\n * first, then declared foreign scopes (values read through, writability\n * reflecting the resolver). Opaque foreign scopes are not listed. */\n listProperties(): ({ scope: string } & PropertyRow)[] {\n const out: ({ scope: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...row });\n } else {\n for (const d of e.decls.values()) {\n out.push({\n scope: token,\n ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase()),\n undefined, `${token}.`),\n });\n }\n }\n }\n return out;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\n }\n // The quality channel (quality.md): declared here once, so a host that\n // registers a quality gets ordering comparisons and advance() with no\n // further wiring. Only added when a quality exists, so contexts stay\n // byte-identical for products that declare none.\n const qualities = this.qualityLadders();\n return qualities.size === 0 ? { scopes, host } : {\n scopes, host,\n qualities: (scope, name) => qualities.get(scope)?.get(name.toLowerCase()),\n };\n }\n\n /** Every quality declaration's ladder, keyed scope token then name. */\n private qualityLadders(): Map<string, Map<string, readonly string[]>> {\n const out = new Map<string, Map<string, readonly string[]>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n for (const d of decls) {\n if (d.type !== \"quality\" || d.stages === undefined) continue;\n let m = out.get(token);\n if (!m) { m = new Map(); out.set(token, m); }\n m.set(d.name.toLowerCase(), d.stages);\n }\n }\n return out;\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>();\n for (const d of decls) m.set(d.name.toLowerCase(), {\n type: d.type, enumValues: d.values,\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned,\n * host-saved), as bare bags - the 0.1.x shape, kept stable so existing\n * consumers' save formats are untouched. A product embedding the\n * versioned cross-product shape uses `saveFragment`. */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = e.bag.save();\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes\n * are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") e.bag.load(vals);\n }\n }\n\n /** The versioned owned-state fragment (the one serialisation shape both\n * product families' save envelopes embed when they adopt the kernel;\n * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /** Restore from a versioned fragment; an unsupported version throws. */\n loadFragment(fragment: OwnedStateFragment): void {\n if (fragment.version !== SAVE_FRAGMENT_VERSION) {\n throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);\n }\n this.load(fragment.scopes);\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\n/** The seed value for a declared property: its own `default`, else the type's.\n *\n * Exported because it was being written again wherever a declaration needed seeding, and a\n * copy of a defaults table is a copy that stops agreeing. Patterplay carried three of them in\n * one file, for its shared decls, its host-scope decls and its scene decls - three declaration\n * TYPES, one behaviour, and nothing to notice if a case drifted. The parameter is structurally\n * typed for exactly that reason: anything with `type` and the optional `default` / `values` /\n * `stages` fits, whatever the caller calls its declaration.\n *\n * A quality seeds at the FIRST rung of its ladder: the ladder's start is the story's start. */\nexport function defaultFor(d: Pick<ScopeDeclaration, \"type\" | \"default\" | \"values\" | \"stages\">): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n // A quality starts at the first rung of its ladder.\n case \"quality\": return d.stages?.[0] ?? \"\";\n // Unreachable for a well-typed declaration, and deliberately present anyway: a bundle\n // is DATA, and a hand-edited or newer-than-this-build one can carry a type string the\n // union does not have. Falling off the switch would seed `undefined`, which is not a\n // ScalarValue and travels a long way before it fails. Patterplay's copy of this had the\n // guard and this one did not, which is the drift you only find by removing a duplicate.\n default: return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// The state logger, which both product families had written twice each.\n// ---------------------------------------------------------------------------\nexport type {\n StateSnapshot, StateChange, LogMount, StateLoggerAdapter, StateLoggerOptions, StateLogger,\n} from \"./state-logger.js\";\nexport { createStateLogger, diffState } from \"./state-logger.js\";\n"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,62 @@
1
1
  import { ScalarValue, PropertyType, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
2
2
  export { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from '@wildwinter/expr';
3
3
 
4
+ /** A flattened snapshot: path -> value. */
5
+ type StateSnapshot = Record<string, ScalarValue>;
6
+ interface StateChange {
7
+ path: string;
8
+ from: ScalarValue | undefined;
9
+ to: ScalarValue | undefined;
10
+ }
11
+ /**
12
+ * One bag on the logger's path space.
13
+ *
14
+ * Named for the LOG, not the bag: a product may already have its own type for enumerating
15
+ * bags (the Storylet Engine's LogMount, which labels a mount "story" for its own purposes),
16
+ * and in the ported runtimes both land in one namespace. They are also not the same thing,
17
+ * which the prefix rule below is about.
18
+ *
19
+ * `pathPrefix` is used VERBATIM, separator included, exactly as the bag's own
20
+ * is - it is not a scope token with a dot implied. Omit it and the bag's own
21
+ * `pathPrefix` is used, which is what a product wants whenever its log paths
22
+ * and its property addresses agree.
23
+ *
24
+ * They do not always agree, which is why this can be overridden: Patterplay
25
+ * addresses a scene property `@scene.mood` (relative to the flow's current
26
+ * scene) but has to LOG it as `@scene:kitchen.mood`, because a log covering
27
+ * several scenes needs to say which one.
28
+ */
29
+ interface LogMount {
30
+ bag: PropertyBag;
31
+ pathPrefix?: string;
32
+ }
33
+ /** What a product supplies: its kernel bags (re-read on every capture, so a
34
+ * product that replaces its bags on load re-mounts), and its non-property
35
+ * state as flattened paths. */
36
+ interface StateLoggerAdapter {
37
+ mounts(): LogMount[];
38
+ extra?(): StateSnapshot;
39
+ }
40
+ interface StateLoggerOptions {
41
+ /** Where lines go; defaults to console.log. */
42
+ sink?: (line: string) => void;
43
+ /** Prefixed to every line, verbatim (e.g. `"[board] "`). */
44
+ label?: string;
45
+ }
46
+ interface StateLogger {
47
+ /** The current flattened state. Logs nothing. */
48
+ snapshot(): StateSnapshot;
49
+ /** Everything since the last capture: the audited writes already logged as
50
+ * they landed, plus anything that changed WITHOUT an audit event, diffed,
51
+ * logged and re-baselined. */
52
+ capture(): StateChange[];
53
+ /** Unhook the bag auditors. The logger is inert afterwards. */
54
+ dispose(): void;
55
+ }
56
+ /** The sorted set of paths that differ between two snapshots. */
57
+ declare function diffState(prev: StateSnapshot, next: StateSnapshot): StateChange[];
58
+ declare function createStateLogger(adapter: StateLoggerAdapter, opts?: StateLoggerOptions): StateLogger;
59
+
4
60
  /**
5
61
  * A property declaration. `default` is used by an *owned* scope to seed its bag
6
62
  * (foreign scopes ignore it - the host owns the value). `writable: false` makes
@@ -230,4 +286,4 @@ declare class ScopeRegistry {
230
286
  * A quality seeds at the FIRST rung of its ladder: the ladder's start is the story's start. */
231
287
  declare function defaultFor(d: Pick<ScopeDeclaration, "type" | "default" | "values" | "stages">): ScalarValue;
232
288
 
233
- export { type BagChange, type OwnedStateFragment, PropertyBag, type PropertyRow, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, defaultFor, readScopeRegistrySpec };
289
+ export { type BagChange, type LogMount, type OwnedStateFragment, PropertyBag, type PropertyRow, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, type StateChange, type StateLogger, type StateLoggerAdapter, type StateLoggerOptions, type StateSnapshot, createStateLogger, defaultFor, diffState, readScopeRegistrySpec };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,62 @@
1
1
  import { ScalarValue, PropertyType, ScopeResolver, EvalContext, ExpressionSchema } from '@wildwinter/expr';
2
2
  export { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from '@wildwinter/expr';
3
3
 
4
+ /** A flattened snapshot: path -> value. */
5
+ type StateSnapshot = Record<string, ScalarValue>;
6
+ interface StateChange {
7
+ path: string;
8
+ from: ScalarValue | undefined;
9
+ to: ScalarValue | undefined;
10
+ }
11
+ /**
12
+ * One bag on the logger's path space.
13
+ *
14
+ * Named for the LOG, not the bag: a product may already have its own type for enumerating
15
+ * bags (the Storylet Engine's LogMount, which labels a mount "story" for its own purposes),
16
+ * and in the ported runtimes both land in one namespace. They are also not the same thing,
17
+ * which the prefix rule below is about.
18
+ *
19
+ * `pathPrefix` is used VERBATIM, separator included, exactly as the bag's own
20
+ * is - it is not a scope token with a dot implied. Omit it and the bag's own
21
+ * `pathPrefix` is used, which is what a product wants whenever its log paths
22
+ * and its property addresses agree.
23
+ *
24
+ * They do not always agree, which is why this can be overridden: Patterplay
25
+ * addresses a scene property `@scene.mood` (relative to the flow's current
26
+ * scene) but has to LOG it as `@scene:kitchen.mood`, because a log covering
27
+ * several scenes needs to say which one.
28
+ */
29
+ interface LogMount {
30
+ bag: PropertyBag;
31
+ pathPrefix?: string;
32
+ }
33
+ /** What a product supplies: its kernel bags (re-read on every capture, so a
34
+ * product that replaces its bags on load re-mounts), and its non-property
35
+ * state as flattened paths. */
36
+ interface StateLoggerAdapter {
37
+ mounts(): LogMount[];
38
+ extra?(): StateSnapshot;
39
+ }
40
+ interface StateLoggerOptions {
41
+ /** Where lines go; defaults to console.log. */
42
+ sink?: (line: string) => void;
43
+ /** Prefixed to every line, verbatim (e.g. `"[board] "`). */
44
+ label?: string;
45
+ }
46
+ interface StateLogger {
47
+ /** The current flattened state. Logs nothing. */
48
+ snapshot(): StateSnapshot;
49
+ /** Everything since the last capture: the audited writes already logged as
50
+ * they landed, plus anything that changed WITHOUT an audit event, diffed,
51
+ * logged and re-baselined. */
52
+ capture(): StateChange[];
53
+ /** Unhook the bag auditors. The logger is inert afterwards. */
54
+ dispose(): void;
55
+ }
56
+ /** The sorted set of paths that differ between two snapshots. */
57
+ declare function diffState(prev: StateSnapshot, next: StateSnapshot): StateChange[];
58
+ declare function createStateLogger(adapter: StateLoggerAdapter, opts?: StateLoggerOptions): StateLogger;
59
+
4
60
  /**
5
61
  * A property declaration. `default` is used by an *owned* scope to seed its bag
6
62
  * (foreign scopes ignore it - the host owns the value). `writable: false` makes
@@ -230,4 +286,4 @@ declare class ScopeRegistry {
230
286
  * A quality seeds at the FIRST rung of its ladder: the ladder's start is the story's start. */
231
287
  declare function defaultFor(d: Pick<ScopeDeclaration, "type" | "default" | "values" | "stages">): ScalarValue;
232
288
 
233
- export { type BagChange, type OwnedStateFragment, PropertyBag, type PropertyRow, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, defaultFor, readScopeRegistrySpec };
289
+ export { type BagChange, type LogMount, type OwnedStateFragment, PropertyBag, type PropertyRow, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, type ScopeDeclaration, ScopeRegistry, type ScopeRegistrySpec, type ScopeSpec, type StateChange, type StateLogger, type StateLoggerAdapter, type StateLoggerOptions, type StateSnapshot, createStateLogger, defaultFor, diffState, readScopeRegistrySpec };
package/dist/index.js CHANGED
@@ -1,3 +1,67 @@
1
+ // src/state-logger.ts
2
+ function diffState(prev, next) {
3
+ const changes = [];
4
+ const paths = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
5
+ for (const path of [...paths].sort()) {
6
+ const from = prev[path], to = next[path];
7
+ if (JSON.stringify(from) !== JSON.stringify(to)) changes.push({ path, from, to });
8
+ }
9
+ return changes;
10
+ }
11
+ var show = (v) => v === void 0 ? "<unset>" : JSON.stringify(v);
12
+ var prefixOf = (m) => m.pathPrefix ?? m.bag.pathPrefix;
13
+ function createStateLogger(adapter, opts = {}) {
14
+ const sink = opts.sink ?? ((line) => console.log(line));
15
+ const label = opts.label ?? "";
16
+ const emit = (c) => {
17
+ sink(`${label}${c.path}: ${show(c.from)} -> ${show(c.to)}`);
18
+ };
19
+ const full = () => {
20
+ const out = {};
21
+ for (const m of adapter.mounts()) {
22
+ const prefix = prefixOf(m);
23
+ for (const [name, value] of Object.entries(m.bag.values)) out[prefix + name] = value;
24
+ }
25
+ Object.assign(out, adapter.extra?.() ?? {});
26
+ return structuredClone(out);
27
+ };
28
+ let baseline = full();
29
+ let pushed = [];
30
+ let mounted = [];
31
+ const hook = (prefix, bag) => bag.onAudit((change) => {
32
+ const c = structuredClone({ path: prefix + change.name, from: change.prev, to: change.next });
33
+ emit(c);
34
+ pushed.push(c);
35
+ baseline[c.path] = structuredClone(change.next);
36
+ });
37
+ const mount = () => {
38
+ const mounts = adapter.mounts();
39
+ const same = mounted.length === mounts.length && mounts.every((m, i) => mounted[i].bag === m.bag);
40
+ if (same) return;
41
+ for (const m of mounted) m.off();
42
+ mounted = mounts.map((m) => ({ bag: m.bag, off: hook(prefixOf(m), m.bag) }));
43
+ };
44
+ mount();
45
+ return {
46
+ snapshot: full,
47
+ capture() {
48
+ const next = full();
49
+ const diffed = diffState(baseline, next);
50
+ for (const c of diffed) emit(c);
51
+ const changes = [...pushed, ...diffed];
52
+ pushed = [];
53
+ baseline = next;
54
+ mount();
55
+ return changes;
56
+ },
57
+ dispose() {
58
+ for (const m of mounted) m.off();
59
+ mounted = [];
60
+ pushed = [];
61
+ }
62
+ };
63
+ }
64
+
1
65
  // src/index.ts
2
66
  var SUPPORTED_SPEC_VERSIONS = [1];
3
67
  function readScopeRegistrySpec(source) {
@@ -348,6 +412,6 @@ function defaultFor(d) {
348
412
  }
349
413
  }
350
414
 
351
- export { PropertyBag, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, ScopeRegistry, defaultFor, readScopeRegistrySpec };
415
+ export { PropertyBag, SAVE_FRAGMENT_VERSION, SUPPORTED_SPEC_VERSIONS, ScopeRegistry, createStateLogger, defaultFor, diffState, readScopeRegistrySpec };
352
416
  //# sourceMappingURL=index.js.map
353
417
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n /** A quality's ordered ladder of stage names (quality.md). */\n stages?: string[];\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// PropertyBag - the state kernel's unit of state (added 0.2.0; design:\n// storylets-new/design/engine-runtimes.md 3.1). A typed, declared property\n// bag with defaults, the firing rule (engine writes notify subscribers;\n// host writes are silent but always auditable), examiner rows, one\n// sanctioned clone door, and bare-value save/load. Owned registry scopes\n// are bags; products may also hold bag families of their own (per-box,\n// per-scene) and mount the shared ones.\n// ---------------------------------------------------------------------------\n\n/** One property change. `silent` marks a host write (the firing rule: it\n * reaches the audit hook but not subscribers); `reason` is the host's own\n * note for its log. */\nexport interface BagChange {\n name: string;\n prev?: ScalarValue;\n next: ScalarValue;\n silent: boolean;\n reason?: string;\n}\n\n/** One examiner row: what a property examiner/editor needs to render and\n * edit a declared property. */\nexport interface PropertyRow {\n name: string;\n /** The address this property answers to - what getProperty/setProperty take.\n * A bag composes it from its own `pathPrefix` and the name, so a row is\n * self-describing: an examiner can render and write a row without being told\n * separately where it came from.\n *\n * The PREFIX CARRIES ITS OWN SEPARATOR rather than the bag assuming a dot,\n * because a prefix is not always a bare scope token: the Storylet Engine\n * addresses a deck's properties as `deck.<id>.name`, so the prefix is already\n * a dotted path. Patterplay's `@patter.gold` and `@scene.mood` are the plain\n * case. (`@gold` also resolves - splitRef defaults an unqualified name to the\n * patter scope - but it is the shorthand, not the address a row reports.)\n *\n * With no prefix this is just the name. Both families forked this interface\n * to add exactly this field - once per runtime - which is the same reason\n * `stages` is here. */\n path: string;\n type: PropertyType;\n value: ScalarValue | undefined;\n default: ScalarValue;\n values?: string[];\n /** A quality's ordered stage ladder, so an inspector can offer the stages\n * instead of a free-text box. `quality` has been in PropertyType since the\n * ladder landed, and the evaluator compares stages by LADDER POSITION and\n * refuses an unknown one, so free text is not a soft failure: a typo breaks\n * play rather than being corrected. This row is the only thing an examiner\n * sees, so a ladder it cannot carry is a ladder no editor can offer. One\n * consumer forked this whole interface to add the field; the field belongs\n * here, beside the `values` it is the closed-set twin of. */\n stages?: string[];\n writable: boolean;\n}\n\nexport class PropertyBag {\n /** The live values record (stable identity across reseed, so an\n * EvalContext built over it stays valid). Read-path for evaluation;\n * writes go through `set` so the firing rule applies. */\n readonly values: Record<string, ScalarValue> = {};\n private decls = new Map<string, ScopeDeclaration>();\n private readonly subscribers = new Set<(change: BagChange) => void>();\n private readonly auditors = new Set<(change: BagChange) => void>();\n /** Name normalisation policy: lowercase by default (the registry's\n * long-standing contract); a product whose names are case-significant\n * passes identity. */\n private readonly norm: (name: string) => string;\n\n /** The address prefix this bag's rows carry, separator included (`@`,\n * `@scene.`, `world.`, `deck.<id>.`). Empty means a row's path is its name. */\n readonly pathPrefix: string;\n\n constructor(\n declarations: ScopeDeclaration[] = [],\n opts?: { normalise?: (name: string) => string; pathPrefix?: string },\n ) {\n this.norm = opts?.normalise ?? ((n) => n.toLowerCase());\n this.pathPrefix = opts?.pathPrefix ?? \"\";\n this.seed(declarations);\n }\n\n private seed(declarations: ScopeDeclaration[]): void {\n for (const d of declarations) {\n const name = this.norm(d.name);\n this.decls.set(name, d);\n // Cloned so bags seeded from one declaration set never share a\n // mutable default (flags arrays).\n this.values[name] = structuredClone(d.default ?? defaultFor(d));\n }\n }\n\n get(name: string): ScalarValue | undefined {\n return this.values[this.norm(name)];\n }\n\n /** Write a property. Engine writes (the default) notify subscribers;\n * pass `silent: true` for a host write, which reaches only the audit\n * hook. Throws on a read-only property. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string }): BagChange {\n const n = this.norm(name);\n if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);\n const change: BagChange = {\n name: n,\n prev: this.values[n],\n next: value,\n silent: opts?.silent ?? false,\n reason: opts?.reason,\n };\n this.values[n] = value;\n for (const audit of this.auditors) audit(change);\n if (!change.silent) for (const fn of this.subscribers) fn(change);\n return change;\n }\n\n /** Notified of engine (non-silent) writes. Returns the unsubscribe. */\n subscribe(fn: (change: BagChange) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n /** Notified of EVERY write, silent or not. Returns the unsubscribe. */\n onAudit(fn: (change: BagChange) => void): () => void {\n this.auditors.add(fn);\n return () => this.auditors.delete(fn);\n }\n\n /** Examiner rows: the declared surface only (stray values are storage,\n * not surface). */\n rows(): PropertyRow[] {\n return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), undefined, name, this.pathPrefix));\n }\n\n declarations(): ScopeDeclaration[] {\n return [...this.decls.values()];\n }\n\n /** The one sanctioned copy door: values deep-copied, declarations\n * duplicated, the normalisation policy carried, subscriptions NOT\n * carried. */\n clone(): PropertyBag {\n const c = new PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });\n c.decls = new Map(this.decls);\n Object.assign(c.values, structuredClone(this.values));\n return c;\n }\n\n /** Clear and re-seed from new declarations, in place (the values record\n * keeps its identity, so contexts built over it stay valid). */\n reseed(declarations: ScopeDeclaration[]): void {\n for (const k of Object.keys(this.values)) delete this.values[k];\n this.decls.clear();\n this.seed(declarations);\n }\n\n /** Bare values, ready to embed in a product's save. */\n save(): Record<string, ScalarValue> {\n return structuredClone(this.values);\n }\n\n /** Lay saved values over the current ones (call after a fresh seed:\n * orphans land as strays, new declarations keep their defaults; the\n * product decides whether to prune). Does not fire events. */\n load(values: Record<string, ScalarValue>): void {\n for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;\n }\n}\n\nfunction rowFor(\n d: ScopeDeclaration,\n value: ScalarValue | undefined,\n writable?: boolean,\n name?: string,\n pathPrefix = \"\",\n): PropertyRow {\n const rowName = name ?? d.name.toLowerCase();\n return {\n name: rowName,\n path: pathPrefix + rowName,\n type: d.type,\n value,\n default: d.default ?? defaultFor(d),\n ...(d.values !== undefined ? { values: d.values } : {}),\n // `stages` was added to the row so an examiner could offer a quality's ladder\n // instead of a free-text box, and then never populated here: every quality row\n // this function built came out without one. Fixed 2026-09-02.\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n writable: writable ?? d.writable ?? true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: PropertyBag;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\n/** The versioned owned-state fragment both product save envelopes embed\n * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[], pathPrefix?: string): this {\n // The scope knows its own token, so its rows can address themselves: `world.hp`.\n // The ADDRESS GRAMMAR is the product's, though, not the registry's - Patterplay\n // writes `@patter.gold` where the Storylet Engine writes `world.gold` - so a\n // caller may say how its addresses look. A bag MOUNTED here keeps whatever prefix\n // its holder gave it: the holder owns the addressing.\n return this.mountOwned(token, new PropertyBag(declarations, { pathPrefix: pathPrefix ?? `${token}.` }));\n }\n\n /**\n * Attach an EXISTING bag as an owned scope - the shared-container move: a\n * host (or the other product) holds the bag; this registry reads, writes\n * and lists it like its own, but the holder saves it.\n */\n mountOwned(token: string, bag: PropertyBag): this {\n this.assertFree(token);\n this.scopes.set(token, { kind: \"owned\", bag });\n return this;\n }\n\n /** An owned scope's bag (subscribe, audit, rows live there). */\n ownedBag(token: string): PropertyBag {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n return e.bag;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.ownedBag(token).reseed(declarations);\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n return e.kind === \"owned\" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());\n }\n\n /** Write a property (an ENGINE write: the bag's subscribers fire; use\n * the bag directly for silent host writes). Throws on an unknown or\n * read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n if (e.kind === \"owned\") {\n try {\n e.bag.set(name, value);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n e.resolver.set!(n, value);\n }\n\n private foreignWritable(e: ForeignScope, name: string): boolean {\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /** Examiner rows across every scope with a declared surface: owned bags\n * first, then declared foreign scopes (values read through, writability\n * reflecting the resolver). Opaque foreign scopes are not listed. */\n listProperties(): ({ scope: string } & PropertyRow)[] {\n const out: ({ scope: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...row });\n } else {\n for (const d of e.decls.values()) {\n out.push({\n scope: token,\n ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase()),\n undefined, `${token}.`),\n });\n }\n }\n }\n return out;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\n }\n // The quality channel (quality.md): declared here once, so a host that\n // registers a quality gets ordering comparisons and advance() with no\n // further wiring. Only added when a quality exists, so contexts stay\n // byte-identical for products that declare none.\n const qualities = this.qualityLadders();\n return qualities.size === 0 ? { scopes, host } : {\n scopes, host,\n qualities: (scope, name) => qualities.get(scope)?.get(name.toLowerCase()),\n };\n }\n\n /** Every quality declaration's ladder, keyed scope token then name. */\n private qualityLadders(): Map<string, Map<string, readonly string[]>> {\n const out = new Map<string, Map<string, readonly string[]>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n for (const d of decls) {\n if (d.type !== \"quality\" || d.stages === undefined) continue;\n let m = out.get(token);\n if (!m) { m = new Map(); out.set(token, m); }\n m.set(d.name.toLowerCase(), d.stages);\n }\n }\n return out;\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>();\n for (const d of decls) m.set(d.name.toLowerCase(), {\n type: d.type, enumValues: d.values,\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned,\n * host-saved), as bare bags - the 0.1.x shape, kept stable so existing\n * consumers' save formats are untouched. A product embedding the\n * versioned cross-product shape uses `saveFragment`. */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = e.bag.save();\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes\n * are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") e.bag.load(vals);\n }\n }\n\n /** The versioned owned-state fragment (the one serialisation shape both\n * product families' save envelopes embed when they adopt the kernel;\n * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /** Restore from a versioned fragment; an unsupported version throws. */\n loadFragment(fragment: OwnedStateFragment): void {\n if (fragment.version !== SAVE_FRAGMENT_VERSION) {\n throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);\n }\n this.load(fragment.scopes);\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\n/** The seed value for a declared property: its own `default`, else the type's.\n *\n * Exported because it was being written again wherever a declaration needed seeding, and a\n * copy of a defaults table is a copy that stops agreeing. Patterplay carried three of them in\n * one file, for its shared decls, its host-scope decls and its scene decls - three declaration\n * TYPES, one behaviour, and nothing to notice if a case drifted. The parameter is structurally\n * typed for exactly that reason: anything with `type` and the optional `default` / `values` /\n * `stages` fits, whatever the caller calls its declaration.\n *\n * A quality seeds at the FIRST rung of its ladder: the ladder's start is the story's start. */\nexport function defaultFor(d: Pick<ScopeDeclaration, \"type\" | \"default\" | \"values\" | \"stages\">): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n // A quality starts at the first rung of its ladder.\n case \"quality\": return d.stages?.[0] ?? \"\";\n // Unreachable for a well-typed declaration, and deliberately present anyway: a bundle\n // is DATA, and a hand-edited or newer-than-this-build one can carry a type string the\n // union does not have. Falling off the switch would seed `undefined`, which is not a\n // ScalarValue and travels a long way before it fails. Patterplay's copy of this had the\n // guard and this one did not, which is the drift you only find by removing a duplicate.\n default: return false;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/state-logger.ts","../src/index.ts"],"names":[],"mappings":";AAoFO,SAAS,SAAA,CAAU,MAAqB,IAAA,EAAoC;AACjF,EAAA,MAAM,UAAyB,EAAC;AAChC,EAAA,MAAM,KAAA,mBAAQ,IAAI,GAAA,CAAI,CAAC,GAAG,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG,GAAG,MAAA,CAAO,IAAA,CAAK,IAAI,CAAC,CAAC,CAAA;AAClE,EAAA,KAAA,MAAW,QAAQ,CAAC,GAAG,KAAK,CAAA,CAAE,MAAK,EAAG;AACpC,IAAA,MAAM,OAAO,IAAA,CAAK,IAAI,CAAA,EAAG,EAAA,GAAK,KAAK,IAAI,CAAA;AACvC,IAAA,IAAI,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,KAAM,KAAK,SAAA,CAAU,EAAE,CAAA,EAAG,OAAA,CAAQ,IAAA,CAAK,EAAE,IAAA,EAAM,IAAA,EAAM,IAAI,CAAA;AAAA,EAClF;AACA,EAAA,OAAO,OAAA;AACT;AAEA,IAAM,IAAA,GAAO,CAAC,CAAA,KAAwC,CAAA,KAAM,SAAY,SAAA,GAAY,IAAA,CAAK,UAAU,CAAC,CAAA;AAEpG,IAAM,WAAW,CAAC,CAAA,KAAwB,CAAA,CAAE,UAAA,IAAc,EAAE,GAAA,CAAI,UAAA;AAEzD,SAAS,iBAAA,CAAkB,OAAA,EAA6B,IAAA,GAA2B,EAAC,EAAgB;AACzG,EAAA,MAAM,OAAO,IAAA,CAAK,IAAA,KAAS,CAAC,IAAA,KAAiB,OAAA,CAAQ,IAAI,IAAI,CAAA,CAAA;AAC7D,EAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,IAAS,EAAA;AAC5B,EAAA,MAAM,IAAA,GAAO,CAAC,CAAA,KAAyB;AAAE,IAAA,IAAA,CAAK,CAAA,EAAG,KAAK,CAAA,EAAG,CAAA,CAAE,IAAI,CAAA,EAAA,EAAK,IAAA,CAAK,CAAA,CAAE,IAAI,CAAC,CAAA,IAAA,EAAO,IAAA,CAAK,CAAA,CAAE,EAAE,CAAC,CAAA,CAAE,CAAA;AAAA,EAAG,CAAA;AAEtG,EAAA,MAAM,OAAO,MAAqB;AAChC,IAAA,MAAM,MAAqB,EAAC;AAC5B,IAAA,KAAA,MAAW,CAAA,IAAK,OAAA,CAAQ,MAAA,EAAO,EAAG;AAChC,MAAA,MAAM,MAAA,GAAS,SAAS,CAAC,CAAA;AACzB,MAAA,KAAA,MAAW,CAAC,IAAA,EAAM,KAAK,CAAA,IAAK,MAAA,CAAO,OAAA,CAAQ,CAAA,CAAE,GAAA,CAAI,MAAM,CAAA,EAAG,GAAA,CAAI,MAAA,GAAS,IAAI,CAAA,GAAI,KAAA;AAAA,IACjF;AACA,IAAA,MAAA,CAAO,OAAO,GAAA,EAAK,OAAA,CAAQ,KAAA,IAAQ,IAAK,EAAE,CAAA;AAC1C,IAAA,OAAO,gBAAgB,GAAG,CAAA;AAAA,EAC5B,CAAA;AAEA,EAAA,IAAI,WAAW,IAAA,EAAK;AACpB,EAAA,IAAI,SAAwB,EAAC;AAC7B,EAAA,IAAI,UAAmD,EAAC;AAExD,EAAA,MAAM,OAAO,CAAC,MAAA,EAAgB,QAC5B,GAAA,CAAI,OAAA,CAAQ,CAAC,MAAA,KAAW;AAGtB,IAAA,MAAM,CAAA,GAAiB,eAAA,CAAgB,EAAE,IAAA,EAAM,MAAA,GAAS,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,MAAA,CAAO,IAAA,EAAM,EAAA,EAAI,MAAA,CAAO,MAAM,CAAA;AACzG,IAAA,IAAA,CAAK,CAAC,CAAA;AACN,IAAA,MAAA,CAAO,KAAK,CAAC,CAAA;AACb,IAAA,QAAA,CAAS,CAAA,CAAE,IAAI,CAAA,GAAI,eAAA,CAAgB,OAAO,IAAI,CAAA;AAAA,EAChD,CAAC,CAAA;AAEH,EAAA,MAAM,QAAQ,MAAY;AACxB,IAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,EAAO;AAC9B,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,KAAW,MAAA,CAAO,UAAU,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA,EAAG,MAAM,OAAA,CAAQ,CAAC,CAAA,CAAG,GAAA,KAAQ,EAAE,GAAG,CAAA;AACjG,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,KAAA,MAAW,CAAA,IAAK,OAAA,EAAS,CAAA,CAAE,GAAA,EAAI;AAC/B,IAAA,OAAA,GAAU,OAAO,GAAA,CAAI,CAAC,CAAA,MAAO,EAAE,KAAK,CAAA,CAAE,GAAA,EAAK,GAAA,EAAK,IAAA,CAAK,SAAS,CAAC,CAAA,EAAG,CAAA,CAAE,GAAG,GAAE,CAAE,CAAA;AAAA,EAC7E,CAAA;AACA,EAAA,KAAA,EAAM;AAEN,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,IAAA;AAAA,IACV,OAAA,GAAyB;AAGvB,MAAA,MAAM,OAAO,IAAA,EAAK;AAClB,MAAA,MAAM,MAAA,GAAS,SAAA,CAAU,QAAA,EAAU,IAAI,CAAA;AACvC,MAAA,KAAA,MAAW,CAAA,IAAK,MAAA,EAAQ,IAAA,CAAK,CAAC,CAAA;AAC9B,MAAA,MAAM,OAAA,GAAU,CAAC,GAAG,MAAA,EAAQ,GAAG,MAAM,CAAA;AACrC,MAAA,MAAA,GAAS,EAAC;AACV,MAAA,QAAA,GAAW,IAAA;AACX,MAAA,KAAA,EAAM;AACN,MAAA,OAAO,OAAA;AAAA,IACT,CAAA;AAAA,IACA,OAAA,GAAgB;AACd,MAAA,KAAA,MAAW,CAAA,IAAK,OAAA,EAAS,CAAA,CAAE,GAAA,EAAI;AAC/B,MAAA,OAAA,GAAU,EAAC;AACX,MAAA,MAAA,GAAS,EAAC;AAAA,IACZ;AAAA,GACF;AACF;;;AC9FO,IAAM,uBAAA,GAA0B,CAAC,CAAC;AAQlC,SAAS,sBAAsB,MAAA,EAA2C;AAC/E,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,IAAA;AAClD,EAAA,MAAM,MAAO,MAAA,CAAmC,iBAAA;AAChD,EAAA,IAAI,GAAA,KAAQ,QAAW,OAAO,IAAA;AAC9B,EAAA,IAAI,OAAO,QAAQ,QAAA,IAAY,GAAA,KAAQ,MAAM,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAClG,EAAA,MAAM,IAAA,GAAO,GAAA;AACb,EAAA,IAAI,OAAO,IAAA,CAAK,OAAA,KAAY,UAAU,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAClG,EAAA,IAAI,CAAE,uBAAA,CAA8C,QAAA,CAAS,IAAA,CAAK,OAAO,CAAA,EAAG;AAC1E,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sCAAA,EAAyC,IAAA,CAAK,OAAO,gBAAgB,uBAAA,CAAwB,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,EAC5H;AACA,EAAA,IAAI,CAAC,MAAM,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,2CAA2C,CAAA;AAC5F,EAAA,KAAA,MAAW,CAAA,IAAK,KAAK,MAAA,EAAQ;AAC3B,IAAA,IAAI,CAAC,KAAK,OAAO,CAAA,KAAM,YAAY,OAAQ,CAAA,CAAgB,UAAU,QAAA,EAAU;AAC7E,MAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,IACrE;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AA2DO,IAAM,WAAA,GAAN,MAAM,YAAA,CAAY;AAAA;AAAA;AAAA;AAAA,EAId,SAAsC,EAAC;AAAA,EACxC,KAAA,uBAAY,GAAA,EAA8B;AAAA,EACjC,WAAA,uBAAkB,GAAA,EAAiC;AAAA,EACnD,QAAA,uBAAe,GAAA,EAAiC;AAAA;AAAA;AAAA;AAAA,EAIhD,IAAA;AAAA;AAAA;AAAA,EAIR,UAAA;AAAA,EAET,WAAA,CACE,YAAA,GAAmC,EAAC,EACpC,IAAA,EACA;AACA,IAAA,IAAA,CAAK,OAAO,IAAA,EAAM,SAAA,KAAc,CAAC,CAAA,KAAM,EAAE,WAAA,EAAY,CAAA;AACrD,IAAA,IAAA,CAAK,UAAA,GAAa,MAAM,UAAA,IAAc,EAAA;AACtC,IAAA,IAAA,CAAK,KAAK,YAAY,CAAA;AAAA,EACxB;AAAA,EAEQ,KAAK,YAAA,EAAwC;AACnD,IAAA,KAAA,MAAW,KAAK,YAAA,EAAc;AAC5B,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,CAAA,CAAE,IAAI,CAAA;AAC7B,MAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,IAAA,EAAM,CAAC,CAAA;AAGtB,MAAA,IAAA,CAAK,MAAA,CAAO,IAAI,CAAA,GAAI,eAAA,CAAgB,EAAE,OAAA,IAAW,UAAA,CAAW,CAAC,CAAC,CAAA;AAAA,IAChE;AAAA,EACF;AAAA,EAEA,IAAI,IAAA,EAAuC;AACzC,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,GAAA,CAAI,IAAA,EAAc,KAAA,EAAoB,IAAA,EAAyD;AAC7F,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA;AACxB,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,EAAG,QAAA,KAAa,KAAA,EAAO,MAAM,IAAI,KAAA,CAAM,CAAA,CAAA,EAAI,IAAI,CAAA,cAAA,CAAgB,CAAA;AACnF,IAAA,MAAM,MAAA,GAAoB;AAAA,MACxB,IAAA,EAAM,CAAA;AAAA,MACN,IAAA,EAAM,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA;AAAA,MACnB,IAAA,EAAM,KAAA;AAAA,MACN,MAAA,EAAQ,MAAM,MAAA,IAAU,KAAA;AAAA,MACxB,QAAQ,IAAA,EAAM;AAAA,KAChB;AACA,IAAA,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,GAAI,KAAA;AACjB,IAAA,KAAA,MAAW,KAAA,IAAS,IAAA,CAAK,QAAA,EAAU,KAAA,CAAM,MAAM,CAAA;AAC/C,IAAA,IAAI,CAAC,OAAO,MAAA,EAAQ,KAAA,MAAW,MAAM,IAAA,CAAK,WAAA,KAAgB,MAAM,CAAA;AAChE,IAAA,OAAO,MAAA;AAAA,EACT;AAAA;AAAA,EAGA,UAAU,EAAA,EAA6C;AACrD,IAAA,IAAA,CAAK,WAAA,CAAY,IAAI,EAAE,CAAA;AACvB,IAAA,OAAO,MAAM,IAAA,CAAK,WAAA,CAAY,MAAA,CAAO,EAAE,CAAA;AAAA,EACzC;AAAA;AAAA,EAGA,QAAQ,EAAA,EAA6C;AACnD,IAAA,IAAA,CAAK,QAAA,CAAS,IAAI,EAAE,CAAA;AACpB,IAAA,OAAO,MAAM,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,EAAE,CAAA;AAAA,EACtC;AAAA;AAAA;AAAA,EAIA,IAAA,GAAsB;AACpB,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,IAAA,EAAM,CAAC,CAAA,KAAM,MAAA,CAAO,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAI,GAAG,MAAA,EAAW,IAAA,EAAM,IAAA,CAAK,UAAU,CAAC,CAAA;AAAA,EACjH;AAAA,EAEA,YAAA,GAAmC;AACjC,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,KAAA,GAAqB;AACnB,IAAA,MAAM,CAAA,GAAI,IAAI,YAAA,CAAY,EAAC,EAAG,EAAE,SAAA,EAAW,IAAA,CAAK,IAAA,EAAM,UAAA,EAAY,IAAA,CAAK,UAAA,EAAY,CAAA;AACnF,IAAA,CAAA,CAAE,KAAA,GAAQ,IAAI,GAAA,CAAI,IAAA,CAAK,KAAK,CAAA;AAC5B,IAAA,MAAA,CAAO,OAAO,CAAA,CAAE,MAAA,EAAQ,eAAA,CAAgB,IAAA,CAAK,MAAM,CAAC,CAAA;AACpD,IAAA,OAAO,CAAA;AAAA,EACT;AAAA;AAAA;AAAA,EAIA,OAAO,YAAA,EAAwC;AAC7C,IAAA,KAAA,MAAW,CAAA,IAAK,OAAO,IAAA,CAAK,IAAA,CAAK,MAAM,CAAA,EAAG,OAAO,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA;AAC9D,IAAA,IAAA,CAAK,MAAM,KAAA,EAAM;AACjB,IAAA,IAAA,CAAK,KAAK,YAAY,CAAA;AAAA,EACxB;AAAA;AAAA,EAGA,IAAA,GAAoC;AAClC,IAAA,OAAO,eAAA,CAAgB,KAAK,MAAM,CAAA;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK,MAAA,EAA2C;AAC9C,IAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,CAAA,IAAK,OAAO,OAAA,CAAQ,MAAM,CAAA,EAAG,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,CAAC,CAAC,CAAA,GAAI,CAAA;AAAA,EAC3E;AACF;AAEA,SAAS,OACP,CAAA,EACA,KAAA,EACA,QAAA,EACA,IAAA,EACA,aAAa,EAAA,EACA;AACb,EAAA,MAAM,OAAA,GAAU,IAAA,IAAQ,CAAA,CAAE,IAAA,CAAK,WAAA,EAAY;AAC3C,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,OAAA;AAAA,IACN,MAAM,UAAA,GAAa,OAAA;AAAA,IACnB,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,KAAA;AAAA,IACA,OAAA,EAAS,CAAA,CAAE,OAAA,IAAW,UAAA,CAAW,CAAC,CAAA;AAAA,IAClC,GAAI,EAAE,MAAA,KAAW,MAAA,GAAY,EAAE,MAAA,EAAQ,CAAA,CAAE,MAAA,EAAO,GAAI,EAAC;AAAA;AAAA;AAAA;AAAA,IAIrD,GAAI,EAAE,MAAA,KAAW,MAAA,GAAY,EAAE,MAAA,EAAQ,CAAA,CAAE,MAAA,EAAO,GAAI,EAAC;AAAA,IACrD,QAAA,EAAU,QAAA,IAAY,CAAA,CAAE,QAAA,IAAY;AAAA,GACtC;AACF;AAyBO,IAAM,qBAAA,GAAwB;AAE9B,IAAM,gBAAN,MAAoB;AAAA,EACR,MAAA,uBAAa,GAAA,EAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjD,WAAA,CAAY,KAAA,EAAe,YAAA,EAAkC,UAAA,EAA2B;AAMtF,IAAA,OAAO,IAAA,CAAK,UAAA,CAAW,KAAA,EAAO,IAAI,WAAA,CAAY,YAAA,EAAc,EAAE,UAAA,EAAY,UAAA,IAAc,CAAA,EAAG,KAAK,CAAA,CAAA,CAAA,EAAK,CAAC,CAAA;AAAA,EACxG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAA,CAAW,OAAe,GAAA,EAAwB;AAChD,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,IAAA,CAAK,OAAO,GAAA,CAAI,KAAA,EAAO,EAAE,IAAA,EAAM,OAAA,EAAS,KAAK,CAAA;AAC7C,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA,EAGA,SAAS,KAAA,EAA4B;AACnC,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,CAAA,IAAK,CAAA,CAAE,IAAA,KAAS,OAAA,QAAe,IAAI,KAAA,CAAM,CAAA,EAAA,EAAK,KAAK,CAAA,uBAAA,CAAyB,CAAA;AACjF,IAAA,OAAO,CAAA,CAAE,GAAA;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,WAAA,CAAY,OAAe,YAAA,EAAwC;AACjE,IAAA,IAAA,CAAK,QAAA,CAAS,KAAK,CAAA,CAAE,MAAA,CAAO,YAAY,CAAA;AACxC,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,cACE,KAAA,EACA,QAAA,EACA,eAAmC,EAAC,EACpC,gBAAgB,IAAA,EACV;AACN,IAAA,IAAA,CAAK,WAAW,KAAK,CAAA;AACrB,IAAA,MAAM,KAAA,uBAAY,GAAA,EAA8B;AAChD,IAAA,KAAA,MAAW,CAAA,IAAK,cAAc,KAAA,CAAM,GAAA,CAAI,EAAE,IAAA,CAAK,WAAA,IAAe,CAAC,CAAA;AAC/D,IAAA,IAAA,CAAK,MAAA,CAAO,IAAI,KAAA,EAAO,EAAE,MAAM,SAAA,EAAW,QAAA,EAAU,KAAA,EAAO,aAAA,EAAe,CAAA;AAC1E,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,IAAI,KAAA,EAAwB;AAC1B,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAAA,EAC9B;AAAA;AAAA,EAGA,GAAA,CAAI,OAAe,IAAA,EAAuC;AACxD,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,GAAG,OAAO,MAAA;AACf,IAAA,OAAO,CAAA,CAAE,IAAA,KAAS,OAAA,GAAU,CAAA,CAAE,GAAA,CAAI,GAAA,CAAI,IAAI,CAAA,GAAI,CAAA,CAAE,QAAA,CAAS,GAAA,CAAI,IAAA,CAAK,aAAa,CAAA;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA,EAKA,GAAA,CAAI,KAAA,EAAe,IAAA,EAAc,KAAA,EAA0B;AACzD,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,gBAAA,EAAmB,KAAK,CAAA,CAAA,CAAG,CAAA;AACnD,IAAA,IAAI,CAAA,CAAE,SAAS,OAAA,EAAS;AACtB,MAAA,IAAI;AACF,QAAA,CAAA,CAAE,GAAA,CAAI,GAAA,CAAI,IAAA,EAAM,KAAK,CAAA;AAAA,MACvB,CAAA,CAAA,MAAQ;AACN,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAA,EAAK,KAAK,CAAA,CAAA,EAAI,IAAI,CAAA,cAAA,CAAgB,CAAA;AAAA,MACpD;AACA,MAAA;AAAA,IACF;AACA,IAAA,MAAM,CAAA,GAAI,KAAK,WAAA,EAAY;AAC3B,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,CAAgB,CAAA,EAAG,CAAC,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,EAAA,EAAK,KAAK,CAAA,CAAA,EAAI,IAAI,CAAA,cAAA,CAAgB,CAAA;AACnF,IAAA,CAAA,CAAE,QAAA,CAAS,GAAA,CAAK,CAAA,EAAG,KAAK,CAAA;AAAA,EAC1B;AAAA,EAEQ,eAAA,CAAgB,GAAiB,IAAA,EAAuB;AAC9D,IAAA,IAAI,CAAC,CAAA,CAAE,QAAA,CAAS,GAAA,EAAK,OAAO,KAAA;AAC5B,IAAA,OAAO,EAAE,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA,EAAG,YAAY,CAAA,CAAE,aAAA;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,cAAA,GAAsD;AACpD,IAAA,MAAM,MAA2C,EAAC;AAClD,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,CAAC,CAAA,IAAK,KAAK,MAAA,EAAQ;AACpC,MAAA,IAAI,CAAA,CAAE,SAAS,OAAA,EAAS;AACtB,QAAA,KAAA,MAAW,GAAA,IAAO,CAAA,CAAE,GAAA,CAAI,IAAA,EAAK,EAAG,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,KAAA,EAAO,GAAG,GAAA,EAAK,CAAA;AAAA,MACnE,CAAA,MAAO;AACL,QAAA,KAAA,MAAW,CAAA,IAAK,CAAA,CAAE,KAAA,CAAM,MAAA,EAAO,EAAG;AAChC,UAAA,GAAA,CAAI,IAAA,CAAK;AAAA,YACP,KAAA,EAAO,KAAA;AAAA,YACP,GAAG,MAAA;AAAA,cAAO,CAAA;AAAA,cAAG,EAAE,QAAA,CAAS,GAAA,CAAI,CAAA,CAAE,IAAA,CAAK,aAAa,CAAA;AAAA,cAAG,KAAK,eAAA,CAAgB,CAAA,EAAG,CAAA,CAAE,IAAA,CAAK,aAAa,CAAA;AAAA,cACrF,MAAA;AAAA,cAAW,GAAG,KAAK,CAAA,CAAA;AAAA;AAAG,WACjC,CAAA;AAAA,QACH;AAAA,MACF;AAAA,IACF;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAc,IAAA,EAA6C;AACzD,IAAA,MAAM,SAAgC,EAAC;AACvC,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,CAAC,CAAA,IAAK,KAAK,MAAA,EAAQ;AACpC,MAAA,MAAA,CAAO,KAAK,IAAI,CAAA,CAAE,IAAA,KAAS,UAAU,CAAA,CAAE,GAAA,CAAI,SAAS,CAAA,CAAE,QAAA;AAAA,IACxD;AAKA,IAAA,MAAM,SAAA,GAAY,KAAK,cAAA,EAAe;AACtC,IAAA,OAAO,UAAU,IAAA,KAAS,CAAA,GAAI,EAAE,MAAA,EAAQ,MAAK,GAAI;AAAA,MAC/C,MAAA;AAAA,MAAQ,IAAA;AAAA,MACR,SAAA,EAAW,CAAC,KAAA,EAAO,IAAA,KAAS,SAAA,CAAU,GAAA,CAAI,KAAK,CAAA,EAAG,GAAA,CAAI,IAAA,CAAK,WAAA,EAAa;AAAA,KAC1E;AAAA,EACF;AAAA;AAAA,EAGQ,cAAA,GAA8D;AACpE,IAAA,MAAM,GAAA,uBAAU,GAAA,EAA4C;AAC5D,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,CAAC,CAAA,IAAK,KAAK,MAAA,EAAQ;AACpC,MAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,IAAA,KAAS,OAAA,GAAU,CAAA,CAAE,GAAA,CAAI,YAAA,EAAa,GAAI,CAAC,GAAG,CAAA,CAAE,KAAA,CAAM,QAAQ,CAAA;AAC9E,MAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,QAAA,IAAI,CAAA,CAAE,IAAA,KAAS,SAAA,IAAa,CAAA,CAAE,WAAW,MAAA,EAAW;AACpD,QAAA,IAAI,CAAA,GAAI,GAAA,CAAI,GAAA,CAAI,KAAK,CAAA;AACrB,QAAA,IAAI,CAAC,CAAA,EAAG;AAAE,UAAA,CAAA,uBAAQ,GAAA,EAAI;AAAG,UAAA,GAAA,CAAI,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,QAAG;AAC5C,QAAA,CAAA,CAAE,IAAI,CAAA,CAAE,IAAA,CAAK,WAAA,EAAY,EAAG,EAAE,MAAM,CAAA;AAAA,MACtC;AAAA,IACF;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAA,GAA6B;AAC3B,IAAA,MAAM,UAAA,uBAAiB,GAAA,EAA2F;AAClH,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,CAAC,CAAA,IAAK,KAAK,MAAA,EAAQ;AACpC,MAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,IAAA,KAAS,OAAA,GAAU,CAAA,CAAE,GAAA,CAAI,YAAA,EAAa,GAAI,CAAC,GAAG,CAAA,CAAE,KAAA,CAAM,QAAQ,CAAA;AAC9E,MAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACxB,MAAA,MAAM,CAAA,uBAAQ,GAAA,EAA8E;AAC5F,MAAA,KAAA,MAAW,KAAK,KAAA,EAAO,CAAA,CAAE,IAAI,CAAA,CAAE,IAAA,CAAK,aAAY,EAAG;AAAA,QACjD,MAAM,CAAA,CAAE,IAAA;AAAA,QAAM,YAAY,CAAA,CAAE,MAAA;AAAA,QAC5B,GAAI,EAAE,MAAA,KAAW,MAAA,GAAY,EAAE,MAAA,EAAQ,CAAA,CAAE,MAAA,EAAO,GAAI;AAAC,OACtD,CAAA;AACD,MAAA,UAAA,CAAW,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,IACzB;AACA,IAAA,OAAO,EAAE,UAAA,EAAW;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAA,GAAoD;AAClD,IAAA,MAAM,MAAmD,EAAC;AAC1D,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,CAAC,CAAA,IAAK,KAAK,MAAA,EAAQ,IAAI,CAAA,CAAE,IAAA,KAAS,SAAS,GAAA,CAAI,KAAK,CAAA,GAAI,CAAA,CAAE,IAAI,IAAA,EAAK;AACtF,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA;AAAA,EAIA,KAAK,IAAA,EAAyD;AAC5D,IAAA,KAAA,MAAW,CAAC,KAAA,EAAO,IAAI,KAAK,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAA,EAAG;AAChD,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA;AAC/B,MAAA,IAAI,GAAG,IAAA,KAAS,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,KAAK,IAAI,CAAA;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,YAAA,GAAmC;AACjC,IAAA,OAAO,EAAE,OAAA,EAAS,qBAAA,EAAuB,MAAA,EAAQ,IAAA,CAAK,MAAK,EAAE;AAAA,EAC/D;AAAA;AAAA,EAGA,aAAa,QAAA,EAAoC;AAC/C,IAAA,IAAI,QAAA,CAAS,YAAY,qBAAA,EAAuB;AAC9C,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yCAAA,EAA4C,SAAS,OAAO,CAAA,aAAA,EAAgB,qBAAqB,CAAA,CAAA,CAAG,CAAA;AAAA,IACtH;AACA,IAAA,IAAA,CAAK,IAAA,CAAK,SAAS,MAAM,CAAA;AAAA,EAC3B;AAAA,EAEQ,WAAW,KAAA,EAAqB;AACtC,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,KAAK,CAAA,QAAS,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,uBAAA,CAAyB,CAAA;AAAA,EACvF;AACF;AAYO,SAAS,WAAW,CAAA,EAAkF;AAC3G,EAAA,IAAI,CAAA,CAAE,OAAA,KAAY,MAAA,EAAW,OAAO,CAAA,CAAE,OAAA;AACtC,EAAA,QAAQ,EAAE,IAAA;AAAM,IACd,KAAK,SAAA;AAAW,MAAA,OAAO,KAAA;AAAA,IACvB,KAAK,QAAA;AAAU,MAAA,OAAO,CAAA;AAAA,IACtB,KAAK,QAAA;AAAU,MAAA,OAAO,EAAA;AAAA,IACtB,KAAK,MAAA;AAAQ,MAAA,OAAO,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA,IAAK,EAAA;AAAA,IACrC,KAAK,OAAA;AAAS,MAAA,OAAO,EAAC;AAAA;AAAA,IAEtB,KAAK,SAAA;AAAW,MAAA,OAAO,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA,IAAK,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMxC;AAAS,MAAA,OAAO,KAAA;AAAA;AAEpB","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The state logger: what changed in the state kernel, as it changes.\n//\n// Both product families shipped one of these, in four runtimes each, and they\n// were not the same shape. The Storylet Engine's was PUSH-based on the\n// PropertyBag audit hook - a write logs the moment it lands, with the previous\n// value straight off the event. Patterplay's diffed whole saveGame() snapshots,\n// so it could only ever say what changed BETWEEN captures, and only for state a\n// save persists. This is the first one, because it is the better one: a diff\n// cannot tell a write from a write-and-write-back, cannot name the reason a\n// host attached to a write, and cannot see a value that changed and changed\n// back.\n//\n// A diff is still needed, and is kept, for everything that is NOT in a bag: a\n// product's own non-property state (turns, cooldowns, visit counts) arrives\n// through the adapter's `extra()` and is diffed on capture. Bags replaced\n// wholesale by a load fire no audit events either, so capture() re-reads and\n// re-mounts.\n//\n// Line format: `${label}${path}: ${from} -> ${to}`, `<unset>` for a value that\n// was not there.\n// ---------------------------------------------------------------------------\n\nimport type { PropertyBag, ScalarValue } from \"./index.js\";\n\n/** A flattened snapshot: path -> value. */\nexport type StateSnapshot = Record<string, ScalarValue>;\n\nexport interface StateChange {\n path: string;\n from: ScalarValue | undefined;\n to: ScalarValue | undefined;\n}\n\n/**\n * One bag on the logger's path space.\n *\n * Named for the LOG, not the bag: a product may already have its own type for enumerating\n * bags (the Storylet Engine's LogMount, which labels a mount \"story\" for its own purposes),\n * and in the ported runtimes both land in one namespace. They are also not the same thing,\n * which the prefix rule below is about.\n *\n * `pathPrefix` is used VERBATIM, separator included, exactly as the bag's own\n * is - it is not a scope token with a dot implied. Omit it and the bag's own\n * `pathPrefix` is used, which is what a product wants whenever its log paths\n * and its property addresses agree.\n *\n * They do not always agree, which is why this can be overridden: Patterplay\n * addresses a scene property `@scene.mood` (relative to the flow's current\n * scene) but has to LOG it as `@scene:kitchen.mood`, because a log covering\n * several scenes needs to say which one.\n */\nexport interface LogMount {\n bag: PropertyBag;\n pathPrefix?: string;\n}\n\n/** What a product supplies: its kernel bags (re-read on every capture, so a\n * product that replaces its bags on load re-mounts), and its non-property\n * state as flattened paths. */\nexport interface StateLoggerAdapter {\n mounts(): LogMount[];\n extra?(): StateSnapshot;\n}\n\nexport interface StateLoggerOptions {\n /** Where lines go; defaults to console.log. */\n sink?: (line: string) => void;\n /** Prefixed to every line, verbatim (e.g. `\"[board] \"`). */\n label?: string;\n}\n\nexport interface StateLogger {\n /** The current flattened state. Logs nothing. */\n snapshot(): StateSnapshot;\n /** Everything since the last capture: the audited writes already logged as\n * they landed, plus anything that changed WITHOUT an audit event, diffed,\n * logged and re-baselined. */\n capture(): StateChange[];\n /** Unhook the bag auditors. The logger is inert afterwards. */\n dispose(): void;\n}\n\n/** The sorted set of paths that differ between two snapshots. */\nexport function diffState(prev: StateSnapshot, next: StateSnapshot): StateChange[] {\n const changes: StateChange[] = [];\n const paths = new Set([...Object.keys(prev), ...Object.keys(next)]);\n for (const path of [...paths].sort()) {\n const from = prev[path], to = next[path];\n if (JSON.stringify(from) !== JSON.stringify(to)) changes.push({ path, from, to });\n }\n return changes;\n}\n\nconst show = (v: ScalarValue | undefined): string => (v === undefined ? \"<unset>\" : JSON.stringify(v));\n\nconst prefixOf = (m: LogMount): string => m.pathPrefix ?? m.bag.pathPrefix;\n\nexport function createStateLogger(adapter: StateLoggerAdapter, opts: StateLoggerOptions = {}): StateLogger {\n const sink = opts.sink ?? ((line: string) => console.log(line));\n const label = opts.label ?? \"\";\n const emit = (c: StateChange): void => { sink(`${label}${c.path}: ${show(c.from)} -> ${show(c.to)}`); };\n\n const full = (): StateSnapshot => {\n const out: StateSnapshot = {};\n for (const m of adapter.mounts()) {\n const prefix = prefixOf(m);\n for (const [name, value] of Object.entries(m.bag.values)) out[prefix + name] = value;\n }\n Object.assign(out, adapter.extra?.() ?? {});\n return structuredClone(out);\n };\n\n let baseline = full();\n let pushed: StateChange[] = [];\n let mounted: { bag: PropertyBag; off: () => void }[] = [];\n\n const hook = (prefix: string, bag: PropertyBag): (() => void) =>\n bag.onAudit((change) => {\n // The write logs as it lands, `from` straight off the event; the baseline\n // moves with it so capture() never re-reports what was already said.\n const c: StateChange = structuredClone({ path: prefix + change.name, from: change.prev, to: change.next });\n emit(c);\n pushed.push(c);\n baseline[c.path] = structuredClone(change.next);\n });\n\n const mount = (): void => {\n const mounts = adapter.mounts();\n const same = mounted.length === mounts.length && mounts.every((m, i) => mounted[i]!.bag === m.bag);\n if (same) return;\n for (const m of mounted) m.off();\n mounted = mounts.map((m) => ({ bag: m.bag, off: hook(prefixOf(m), m.bag) }));\n };\n mount();\n\n return {\n snapshot: full,\n capture(): StateChange[] {\n // Whatever arrived WITHOUT an audit event: the adapter's non-property\n // paths, and bag values replaced wholesale by a load (which fires none).\n const next = full();\n const diffed = diffState(baseline, next);\n for (const c of diffed) emit(c);\n const changes = [...pushed, ...diffed];\n pushed = [];\n baseline = next;\n mount(); // a load replaces a product's bags; re-hook them\n return changes;\n },\n dispose(): void {\n for (const m of mounted) m.off();\n mounted = [];\n pushed = [];\n },\n };\n}\n","// ---------------------------------------------------------------------------\n// @wildwinter/scoperegistry - the scope registry / runtime state container that\n// sits on top of @wildwinter/expr.\n//\n// expr is a stateless calculator: given an AST, an EvalContext (the state), and\n// a Dialect, it computes. This package is the *state* layer: it owns the world\n// state as a set of named scopes - each either an **owned** scope (a property\n// bag this registry stores and saves) or a **foreign** scope (host- or\n// other-engine-resolved at runtime, never stored here) - and produces the\n// `EvalContext` (for evaluation) and `ExpressionSchema` (for validation) that\n// expr consumes. Plus the `scopeRegistrySpec` interop format for importing a\n// foreign owner's scope declarations.\n//\n// Design: design/scope-registry.md (in the patter repo). expr never depends on\n// this; this depends one-way on expr.\n// ---------------------------------------------------------------------------\n\nimport type {\n EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver,\n} from \"@wildwinter/expr\";\n\nexport type { EvalContext, ExpressionSchema, PropertyType, ScalarValue, ScopeResolver } from \"@wildwinter/expr\";\n\n// ---------------------------------------------------------------------------\n// Declarations + the scopeRegistrySpec interop format\n// ---------------------------------------------------------------------------\n\n/**\n * A property declaration. `default` is used by an *owned* scope to seed its bag\n * (foreign scopes ignore it - the host owns the value). `writable: false` makes\n * a property read-only; default is read/write. (`type`/`values` feed validation.)\n */\nexport interface ScopeDeclaration {\n name: string;\n type: PropertyType;\n values?: string[]; // for enum / flags\n /** A quality's ordered ladder of stage names (quality.md). */\n stages?: string[];\n default?: ScalarValue; // owned scopes: seed value\n writable?: boolean; // default true\n}\n\n/** One scope in a `scopeRegistrySpec`: a token + (optional) declarations. */\nexport interface ScopeSpec {\n token: string;\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Property declarations; omit for an opaque scope (any name, unchecked). */\n declarations?: ScopeDeclaration[];\n}\n\n/**\n * The interop format an owner (Storylet Studio, a host game) exports so another\n * engine can validate references into its scopes. Carried under the well-known\n * `scopeRegistrySpec` JSON key (inside a `.storyworld`, or a standalone file).\n */\nexport interface ScopeRegistrySpec {\n version: number;\n scopes: ScopeSpec[];\n}\n\n/** The spec versions this build understands. */\nexport const SUPPORTED_SPEC_VERSIONS = [1] as const;\n\n/**\n * Extract + validate a `scopeRegistrySpec` from any JSON value (a parsed\n * `.storyworld` bundle, or a vanilla `{ scopeRegistrySpec: ... }` manifest).\n * Returns null when the key is absent (so callers can probe arbitrary files);\n * throws on a malformed or unsupported-version spec.\n */\nexport function readScopeRegistrySpec(source: unknown): ScopeRegistrySpec | null {\n if (!source || typeof source !== \"object\") return null;\n const raw = (source as Record<string, unknown>).scopeRegistrySpec;\n if (raw === undefined) return null;\n if (typeof raw !== \"object\" || raw === null) throw new Error(\"scopeRegistrySpec must be an object\");\n const spec = raw as Record<string, unknown>;\n if (typeof spec.version !== \"number\") throw new Error(\"scopeRegistrySpec.version must be a number\");\n if (!(SUPPORTED_SPEC_VERSIONS as readonly number[]).includes(spec.version)) {\n throw new Error(`unsupported scopeRegistrySpec version ${spec.version} (supported: ${SUPPORTED_SPEC_VERSIONS.join(\", \")})`);\n }\n if (!Array.isArray(spec.scopes)) throw new Error(\"scopeRegistrySpec.scopes must be an array\");\n for (const s of spec.scopes) {\n if (!s || typeof s !== \"object\" || typeof (s as ScopeSpec).token !== \"string\") {\n throw new Error(\"each scopeRegistrySpec scope needs a string token\");\n }\n }\n return spec as unknown as ScopeRegistrySpec;\n}\n\n// ---------------------------------------------------------------------------\n// PropertyBag - the state kernel's unit of state (added 0.2.0; design:\n// storylets-new/design/engine-runtimes.md 3.1). A typed, declared property\n// bag with defaults, the firing rule (engine writes notify subscribers;\n// host writes are silent but always auditable), examiner rows, one\n// sanctioned clone door, and bare-value save/load. Owned registry scopes\n// are bags; products may also hold bag families of their own (per-box,\n// per-scene) and mount the shared ones.\n// ---------------------------------------------------------------------------\n\n/** One property change. `silent` marks a host write (the firing rule: it\n * reaches the audit hook but not subscribers); `reason` is the host's own\n * note for its log. */\nexport interface BagChange {\n name: string;\n prev?: ScalarValue;\n next: ScalarValue;\n silent: boolean;\n reason?: string;\n}\n\n/** One examiner row: what a property examiner/editor needs to render and\n * edit a declared property. */\nexport interface PropertyRow {\n name: string;\n /** The address this property answers to - what getProperty/setProperty take.\n * A bag composes it from its own `pathPrefix` and the name, so a row is\n * self-describing: an examiner can render and write a row without being told\n * separately where it came from.\n *\n * The PREFIX CARRIES ITS OWN SEPARATOR rather than the bag assuming a dot,\n * because a prefix is not always a bare scope token: the Storylet Engine\n * addresses a deck's properties as `deck.<id>.name`, so the prefix is already\n * a dotted path. Patterplay's `@patter.gold` and `@scene.mood` are the plain\n * case. (`@gold` also resolves - splitRef defaults an unqualified name to the\n * patter scope - but it is the shorthand, not the address a row reports.)\n *\n * With no prefix this is just the name. Both families forked this interface\n * to add exactly this field - once per runtime - which is the same reason\n * `stages` is here. */\n path: string;\n type: PropertyType;\n value: ScalarValue | undefined;\n default: ScalarValue;\n values?: string[];\n /** A quality's ordered stage ladder, so an inspector can offer the stages\n * instead of a free-text box. `quality` has been in PropertyType since the\n * ladder landed, and the evaluator compares stages by LADDER POSITION and\n * refuses an unknown one, so free text is not a soft failure: a typo breaks\n * play rather than being corrected. This row is the only thing an examiner\n * sees, so a ladder it cannot carry is a ladder no editor can offer. One\n * consumer forked this whole interface to add the field; the field belongs\n * here, beside the `values` it is the closed-set twin of. */\n stages?: string[];\n writable: boolean;\n}\n\nexport class PropertyBag {\n /** The live values record (stable identity across reseed, so an\n * EvalContext built over it stays valid). Read-path for evaluation;\n * writes go through `set` so the firing rule applies. */\n readonly values: Record<string, ScalarValue> = {};\n private decls = new Map<string, ScopeDeclaration>();\n private readonly subscribers = new Set<(change: BagChange) => void>();\n private readonly auditors = new Set<(change: BagChange) => void>();\n /** Name normalisation policy: lowercase by default (the registry's\n * long-standing contract); a product whose names are case-significant\n * passes identity. */\n private readonly norm: (name: string) => string;\n\n /** The address prefix this bag's rows carry, separator included (`@`,\n * `@scene.`, `world.`, `deck.<id>.`). Empty means a row's path is its name. */\n readonly pathPrefix: string;\n\n constructor(\n declarations: ScopeDeclaration[] = [],\n opts?: { normalise?: (name: string) => string; pathPrefix?: string },\n ) {\n this.norm = opts?.normalise ?? ((n) => n.toLowerCase());\n this.pathPrefix = opts?.pathPrefix ?? \"\";\n this.seed(declarations);\n }\n\n private seed(declarations: ScopeDeclaration[]): void {\n for (const d of declarations) {\n const name = this.norm(d.name);\n this.decls.set(name, d);\n // Cloned so bags seeded from one declaration set never share a\n // mutable default (flags arrays).\n this.values[name] = structuredClone(d.default ?? defaultFor(d));\n }\n }\n\n get(name: string): ScalarValue | undefined {\n return this.values[this.norm(name)];\n }\n\n /** Write a property. Engine writes (the default) notify subscribers;\n * pass `silent: true` for a host write, which reaches only the audit\n * hook. Throws on a read-only property. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string }): BagChange {\n const n = this.norm(name);\n if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);\n const change: BagChange = {\n name: n,\n prev: this.values[n],\n next: value,\n silent: opts?.silent ?? false,\n reason: opts?.reason,\n };\n this.values[n] = value;\n for (const audit of this.auditors) audit(change);\n if (!change.silent) for (const fn of this.subscribers) fn(change);\n return change;\n }\n\n /** Notified of engine (non-silent) writes. Returns the unsubscribe. */\n subscribe(fn: (change: BagChange) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n /** Notified of EVERY write, silent or not. Returns the unsubscribe. */\n onAudit(fn: (change: BagChange) => void): () => void {\n this.auditors.add(fn);\n return () => this.auditors.delete(fn);\n }\n\n /** Examiner rows: the declared surface only (stray values are storage,\n * not surface). */\n rows(): PropertyRow[] {\n return [...this.decls.entries()].map(([name, d]) => rowFor(d, this.get(name), undefined, name, this.pathPrefix));\n }\n\n declarations(): ScopeDeclaration[] {\n return [...this.decls.values()];\n }\n\n /** The one sanctioned copy door: values deep-copied, declarations\n * duplicated, the normalisation policy carried, subscriptions NOT\n * carried. */\n clone(): PropertyBag {\n const c = new PropertyBag([], { normalise: this.norm, pathPrefix: this.pathPrefix });\n c.decls = new Map(this.decls);\n Object.assign(c.values, structuredClone(this.values));\n return c;\n }\n\n /** Clear and re-seed from new declarations, in place (the values record\n * keeps its identity, so contexts built over it stay valid). */\n reseed(declarations: ScopeDeclaration[]): void {\n for (const k of Object.keys(this.values)) delete this.values[k];\n this.decls.clear();\n this.seed(declarations);\n }\n\n /** Bare values, ready to embed in a product's save. */\n save(): Record<string, ScalarValue> {\n return structuredClone(this.values);\n }\n\n /** Lay saved values over the current ones (call after a fresh seed:\n * orphans land as strays, new declarations keep their defaults; the\n * product decides whether to prune). Does not fire events. */\n load(values: Record<string, ScalarValue>): void {\n for (const [k, v] of Object.entries(values)) this.values[this.norm(k)] = v;\n }\n}\n\nfunction rowFor(\n d: ScopeDeclaration,\n value: ScalarValue | undefined,\n writable?: boolean,\n name?: string,\n pathPrefix = \"\",\n): PropertyRow {\n const rowName = name ?? d.name.toLowerCase();\n return {\n name: rowName,\n path: pathPrefix + rowName,\n type: d.type,\n value,\n default: d.default ?? defaultFor(d),\n ...(d.values !== undefined ? { values: d.values } : {}),\n // `stages` was added to the row so an examiner could offer a quality's ladder\n // instead of a free-text box, and then never populated here: every quality row\n // this function built came out without one. Fixed 2026-09-02.\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n writable: writable ?? d.writable ?? true,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The registry / state container\n// ---------------------------------------------------------------------------\n\ninterface OwnedScope {\n kind: \"owned\";\n bag: PropertyBag;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n}\ntype Entry = OwnedScope | ForeignScope;\n\n/** The versioned owned-state fragment both product save envelopes embed\n * (design/engine-runtimes.md 3.1: one serialisation shape for bags). */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n\n /**\n * Register a scope this registry **owns and stores**. Its bag is seeded from\n * each declaration's `default` (or a type default). Owned scopes are\n * type-checked (declarations) and serialized by `save`/`load`.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[], pathPrefix?: string): this {\n // The scope knows its own token, so its rows can address themselves: `world.hp`.\n // The ADDRESS GRAMMAR is the product's, though, not the registry's - Patterplay\n // writes `@patter.gold` where the Storylet Engine writes `world.gold` - so a\n // caller may say how its addresses look. A bag MOUNTED here keeps whatever prefix\n // its holder gave it: the holder owns the addressing.\n return this.mountOwned(token, new PropertyBag(declarations, { pathPrefix: pathPrefix ?? `${token}.` }));\n }\n\n /**\n * Attach an EXISTING bag as an owned scope - the shared-container move: a\n * host (or the other product) holds the bag; this registry reads, writes\n * and lists it like its own, but the holder saves it.\n */\n mountOwned(token: string, bag: PropertyBag): this {\n this.assertFree(token);\n this.scopes.set(token, { kind: \"owned\", bag });\n return this;\n }\n\n /** An owned scope's bag (subscribe, audit, rows live there). */\n ownedBag(token: string): PropertyBag {\n const e = this.scopes.get(token);\n if (!e || e.kind !== \"owned\") throw new Error(`'@${token}' is not an owned scope`);\n return e.bag;\n }\n\n /**\n * Re-initialise an existing **owned** scope's bag from new declarations,\n * clearing its current values. For scope-local state that resets on a context\n * change (e.g. entering a new scene / site / deck) without disturbing other\n * scopes. Mutates the bag in place, so an `EvalContext` already built from this\n * registry stays valid.\n */\n reseedOwned(token: string, declarations: ScopeDeclaration[]): this {\n this.ownedBag(token).reseed(declarations);\n return this;\n }\n\n /**\n * Register a **foreign** scope backed by a host `{ get, set? }` resolver. The\n * values live in the host/other engine and are never stored or saved here.\n * `declarations` (optional, e.g. imported from a `scopeRegistrySpec`) are used\n * only for validation; omit them for an opaque scope.\n */\n defineForeign(\n token: string,\n resolver: ScopeResolver,\n declarations: ScopeDeclaration[] = [],\n scopeWritable = true,\n ): this {\n this.assertFree(token);\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(d.name.toLowerCase(), d);\n this.scopes.set(token, { kind: \"foreign\", resolver, decls, scopeWritable });\n return this;\n }\n\n has(token: string): boolean {\n return this.scopes.has(token);\n }\n\n /** Read a property; undefined if the scope or property is not present. */\n get(scope: string, name: string): ScalarValue | undefined {\n const e = this.scopes.get(scope);\n if (!e) return undefined;\n return e.kind === \"owned\" ? e.bag.get(name) : e.resolver.get(name.toLowerCase());\n }\n\n /** Write a property (an ENGINE write: the bag's subscribers fire; use\n * the bag directly for silent host writes). Throws on an unknown or\n * read-only scope/property. */\n set(scope: string, name: string, value: ScalarValue): void {\n const e = this.scopes.get(scope);\n if (!e) throw new Error(`unknown scope '@${scope}'`);\n if (e.kind === \"owned\") {\n try {\n e.bag.set(name, value);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!this.foreignWritable(e, n)) throw new Error(`'@${scope}.${name}' is read-only`);\n e.resolver.set!(n, value);\n }\n\n private foreignWritable(e: ForeignScope, name: string): boolean {\n if (!e.resolver.set) return false; // no setter => read-only scope\n return e.decls.get(name)?.writable ?? e.scopeWritable;\n }\n\n /** Examiner rows across every scope with a declared surface: owned bags\n * first, then declared foreign scopes (values read through, writability\n * reflecting the resolver). Opaque foreign scopes are not listed. */\n listProperties(): ({ scope: string } & PropertyRow)[] {\n const out: ({ scope: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...row });\n } else {\n for (const d of e.decls.values()) {\n out.push({\n scope: token,\n ...rowFor(d, e.resolver.get(d.name.toLowerCase()), this.foreignWritable(e, d.name.toLowerCase()),\n undefined, `${token}.`),\n });\n }\n }\n }\n return out;\n }\n\n /**\n * Build the `EvalContext` expr's `evaluate` consumes: owned scopes as static\n * bags, foreign scopes as their resolvers. `host` carries dialect-function\n * callbacks (PRNG, tag lookups) and is passed through untouched.\n */\n toEvalContext(host?: Record<string, unknown>): EvalContext {\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of this.scopes) {\n scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\n }\n // The quality channel (quality.md): declared here once, so a host that\n // registers a quality gets ordering comparisons and advance() with no\n // further wiring. Only added when a quality exists, so contexts stay\n // byte-identical for products that declare none.\n const qualities = this.qualityLadders();\n return qualities.size === 0 ? { scopes, host } : {\n scopes, host,\n qualities: (scope, name) => qualities.get(scope)?.get(name.toLowerCase()),\n };\n }\n\n /** Every quality declaration's ladder, keyed scope token then name. */\n private qualityLadders(): Map<string, Map<string, readonly string[]>> {\n const out = new Map<string, Map<string, readonly string[]>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n for (const d of decls) {\n if (d.type !== \"quality\" || d.stages === undefined) continue;\n let m = out.get(token);\n if (!m) { m = new Map(); out.set(token, m); }\n m.set(d.name.toLowerCase(), d.stages);\n }\n }\n return out;\n }\n\n /**\n * Build the `ExpressionSchema` expr's validator consumes. Scopes with no\n * declarations are **omitted** (opaque - references into them are not flagged);\n * declared scopes contribute their property types for validation.\n */\n toSchema(): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>>();\n for (const [token, e] of this.scopes) {\n const decls = e.kind === \"owned\" ? e.bag.declarations() : [...e.decls.values()];\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>();\n for (const d of decls) m.set(d.name.toLowerCase(), {\n type: d.type, enumValues: d.values,\n ...(d.stages !== undefined ? { stages: d.stages } : {}),\n });\n properties.set(token, m);\n }\n return { properties };\n }\n\n /** Serialize **owned** scopes only (foreign scopes are host-owned,\n * host-saved), as bare bags - the 0.1.x shape, kept stable so existing\n * consumers' save formats are untouched. A product embedding the\n * versioned cross-product shape uses `saveFragment`. */\n save(): Record<string, Record<string, ScalarValue>> {\n const out: Record<string, Record<string, ScalarValue>> = {};\n for (const [token, e] of this.scopes) if (e.kind === \"owned\") out[token] = e.bag.save();\n return out;\n }\n\n /** Restore owned-scope values from a `save` blob. Unknown/foreign scopes\n * are ignored. */\n load(blob: Record<string, Record<string, ScalarValue>>): void {\n for (const [token, vals] of Object.entries(blob)) {\n const e = this.scopes.get(token);\n if (e?.kind === \"owned\") e.bag.load(vals);\n }\n }\n\n /** The versioned owned-state fragment (the one serialisation shape both\n * product families' save envelopes embed when they adopt the kernel;\n * design/engine-runtimes.md 3.1). `save()` wrapped with a version stamp. */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /** Restore from a versioned fragment; an unsupported version throws. */\n loadFragment(fragment: OwnedStateFragment): void {\n if (fragment.version !== SAVE_FRAGMENT_VERSION) {\n throw new Error(`unsupported owned-state fragment version ${fragment.version} (supported: ${SAVE_FRAGMENT_VERSION})`);\n }\n this.load(fragment.scopes);\n }\n\n private assertFree(token: string): void {\n if (this.scopes.has(token)) throw new Error(`scope '@${token}' is already registered`);\n }\n}\n\n/** The seed value for a declared property: its own `default`, else the type's.\n *\n * Exported because it was being written again wherever a declaration needed seeding, and a\n * copy of a defaults table is a copy that stops agreeing. Patterplay carried three of them in\n * one file, for its shared decls, its host-scope decls and its scene decls - three declaration\n * TYPES, one behaviour, and nothing to notice if a case drifted. The parameter is structurally\n * typed for exactly that reason: anything with `type` and the optional `default` / `values` /\n * `stages` fits, whatever the caller calls its declaration.\n *\n * A quality seeds at the FIRST rung of its ladder: the ladder's start is the story's start. */\nexport function defaultFor(d: Pick<ScopeDeclaration, \"type\" | \"default\" | \"values\" | \"stages\">): ScalarValue {\n if (d.default !== undefined) return d.default;\n switch (d.type) {\n case \"boolean\": return false;\n case \"number\": return 0;\n case \"string\": return \"\";\n case \"enum\": return d.values?.[0] ?? \"\";\n case \"flags\": return [];\n // A quality starts at the first rung of its ladder.\n case \"quality\": return d.stages?.[0] ?? \"\";\n // Unreachable for a well-typed declaration, and deliberately present anyway: a bundle\n // is DATA, and a hand-edited or newer-than-this-build one can carry a type string the\n // union does not have. Falling off the switch would seed `undefined`, which is not a\n // ScalarValue and travels a long way before it fails. Patterplay's copy of this had the\n // guard and this one did not, which is the drift you only find by removing a duplicate.\n default: return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// The state logger, which both product families had written twice each.\n// ---------------------------------------------------------------------------\nexport type {\n StateSnapshot, StateChange, LogMount, StateLoggerAdapter, StateLoggerOptions, StateLogger,\n} from \"./state-logger.js\";\nexport { createStateLogger, diffState } from \"./state-logger.js\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildwinter/scoperegistry",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scope registry / runtime state container for @wildwinter/expr: owned property scopes + foreign (host-resolved) scopes, save/load, and the scopeRegistrySpec interop format. Produces the EvalContext and ExpressionSchema that expr consumes.",
5
5
  "type": "module",
6
6
  "license": "MIT",