@storylet-studio/play-helpers 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../expr/packages/scoperegistry/src/state-logger.ts","../../../../expr/packages/scoperegistry/src/index.ts","../src/logger.ts","../../model/src/index.ts","../src/save.ts","../src/inspector.ts","../src/bundle-inspector.ts","../src/live-link.ts","../src/refresh.ts","../src/world.ts"],"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 TO THE STORY; the HOST still writes it, by passing\n * `{ host: true }` (see `set`). Default is read/write. (`type`/`values` feed\n * validation.)\n *\n * The distinction is the whole point of the flag on a foreign scope, where the\n * value is the game's own: a flag carried in the story's bundle must not lock a\n * game out of its own state. Ruled 2026-09-05, after both products met it - the\n * Storylet Engine's venue clock and Patter's coverage driver were each blocked\n * from the one property they existed to move.\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 unless the caller says it is the\n * HOST (`host: true`), for whom `writable: false` was never a rule - it is\n * the story's promise, not the game's. `silent` and `host` are separate on\n * purpose: one is about who hears the write, the other about who may make\n * it. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string; host?: boolean }): BagChange {\n const n = this.norm(name);\n if (!opts?.host && 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 scope.\n *\n * `writable: false` is the STORY's promise, so a story write is refused and\n * a HOST write is not: pass `{ host: true }` from a host's own surface (its\n * `setProperty`, its tooling, a coverage driver) and never from the path an\n * outcome or effect takes. A foreign scope whose resolver has no `set` is\n * refused for everyone, host included - that is not a rule to bypass, it is\n * a game that gave no way to write. */\n set(scope: string, name: string, value: ScalarValue, opts?: { host?: boolean }): 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, opts?.host ? { host: true } : undefined);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = name.toLowerCase();\n if (!e.resolver.set) throw new Error(`'@${scope}.${name}' is read-only`);\n if (!opts?.host && !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","// ---------------------------------------------------------------------------\n// The storylets state logger: an ADAPTER over the kernel logger in\n// @wildwinter/scoperegistry (design/engine-runtimes.md 3.4 is the design of\n// record). The kernel does the work - push-based property logging on the\n// PropertyBag audit hook, so a write logs the moment it lands, plus a diff of\n// everything that has no audit hook - and this supplies the two product-shaped\n// pieces: which bags to watch, and the non-property state (turns / cooldowns /\n// board) as flattened paths.\n//\n// The core lived here, marked \"moves into the kernel package wholesale when the\n// vendor-sync slice lands\". It has. Patterplay's logger, which diffed save\n// snapshots and so could not see a value that changed and changed back, is an\n// adapter over the same core now.\n//\n// Flattened path scheme:\n// world.x / story.x / box.<gameId>.x / deck.<gameId>.x / hand.<gameId>.x /\n// value.<tagGameId>.x, and value.<boxGameId>/<tagGameId>.x for a tag gameId\n// two boxes share (4.4)\n// turn:<boxId> per-box clocks\n// cooldown:<cardId> next-eligible turns\n// board:<handId> hand contents (card ids, dealt order)\n// Line format: `${label}${path}: ${from} -> ${to}`, `<unset>` for undefined.\n// ---------------------------------------------------------------------------\n\nimport type { Engine, Flow } from \"@storylet-studio/runtime\";\nimport type { FlowSave, ScalarValue } from \"@storylet-studio/model\";\nimport {\n createStateLogger as createKernelStateLogger, diffState,\n} from \"@wildwinter/scoperegistry\";\nimport type {\n StateSnapshot, StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions,\n} from \"@wildwinter/scoperegistry\";\n\n// Re-exported: these were declared here, and a host importing them from\n// @storylet-studio/play-helpers should not have to care that they moved.\nexport { createKernelStateLogger, diffState };\nexport type { StateSnapshot, StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions };\n\n/** The full flattened snapshot of ONE FLOW's view - the shared partitions\n * plus that flow's own - plus its turns / cooldowns / board. @world is not\n * here for the same reason it is not in a save envelope: the host owns that\n * container and mounts/saves it itself (createWorldContainer).\n *\n * Taken off the BAGS, which is what a save envelope is made of, rather than\n * off the envelope itself. The two used to be interchangeable; from 4.4 they\n * are not, because a property ADDRESS names its owner by gameId while the\n * envelope stays keyed by internal id (a save has to survive a rename). The\n * bags carry the address, so reading them is what keeps this snapshot and\n * the live logger's lines in ONE path space - which is the invariant the\n * whole diff rests on. */\nexport function snapshotState(engine: Engine, flow: Flow): StateSnapshot {\n const out: StateSnapshot = {};\n // Shared under the flow's own: names are disjoint (shared XOR per-flow by\n // declaration), so one path space holds both without collision.\n for (const { bag } of [...engine.listBags(), ...flow.listBags()]) {\n for (const row of bag.rows()) {\n if (row.value !== undefined) out[row.path] = row.value as ScalarValue;\n }\n }\n Object.assign(out, extraState(engine.saveGame().flows[flow.id]));\n return out;\n}\n\n/** The storylets path-provider adapter for non-property state (design 3.4):\n * one flow's turns / cooldowns / board as flattened paths, off its blob in\n * the envelope (absent for a just-closed flow: no paths). */\nfunction extraState(saved: FlowSave | undefined): StateSnapshot {\n const out: StateSnapshot = {};\n if (saved === undefined) return out;\n for (const [boxId, turn] of Object.entries(saved.turns)) out[`turn:${boxId}`] = turn;\n for (const [cardId, at] of Object.entries(saved.cooldowns)) out[`cooldown:${cardId}`] = at;\n for (const [handId, cards] of Object.entries(saved.board)) out[`board:${handId}`] = [...cards];\n return out;\n}\n\n/** The storylets state logger: the kernel core mounted on the SHARED bags\n * (engine.listBags()) and one flow's own (flow.listBags()) - the same\n * prefixes, one path space, names disjoint - plus the flow's turns /\n * cooldowns / board adapter. A host that wants @world lines mounts its\n * world container's bag through createKernelStateLogger itself. */\nexport function createStateLogger(engine: Engine, flow: Flow, opts: StateLoggerOptions = {}): StateLogger {\n // By NAME, not by handle: loadGame rebuilds every flow and the handle we\n // were given goes inert; capture()'s re-mount picks up the rebuilt one.\n const id = flow.id;\n const live = (): Flow | undefined => engine.getFlow(id);\n return createKernelStateLogger({\n // A BagMount's `prefix` (\"story\", \"deck.<id>\") is the engine's label for the mount;\n // the kernel composes paths from the BAG's own pathPrefix (\"story.\", \"deck.<id>.\")\n // and needs none passed. Same strings, one owner.\n mounts: () => [...engine.listBags(), ...(live()?.listBags() ?? [])].map(({ bag }) => ({ bag })),\n extra: () => extraState(engine.saveGame().flows[id]),\n }, opts);\n}\n","// ---------------------------------------------------------------------------\n// @storylet-studio/model - the shape source-of-truth.\n//\n// Transcribes design/storylets-schema.md (bundle, save) and\n// design/storylets-source.md (shards). Entity shapes are generic over their\n// expression representation E: source shards use plain `src` strings\n// (Card<string>), the compiled bundle uses { src, ast } envelopes\n// (Card<Expression>). No behaviour lives here.\n// ---------------------------------------------------------------------------\n\nimport type { Expression, ScalarValue } from \"@wildwinter/expr\";\n\nexport type { Expression, ScalarValue, AstNode } from \"@wildwinter/expr\";\n\n// --- shared declarations -----------------------------------------------------\n\nexport type PropertyType = \"boolean\" | \"number\" | \"string\" | \"enum\" | \"flags\" | \"quality\";\n\n/** A property declaration: @world / @story / @box / @deck / tag / hand\n * state. A declared property always has a value (`default` is required);\n * referencing an undeclared property is a publish-time error. */\nexport interface PropertyDecl {\n name: string;\n type: PropertyType;\n default: ScalarValue;\n values?: string[];\n /**\n * A quality's ordered ladder of stage names (design/quality.md). Order IS\n * the meaning: `>=` compares by position here, and `advance()` steps along\n * it. The one order-semantic list in the format, accepted as such: it is a\n * declaration, and inserting a stage mid-ladder is the design's whole point.\n */\n stages?: string[];\n /**\n * `@world` only. `false` makes the property read-only TO THE STORY: a\n * condition may read it, an outcome that writes it is a compile error. The\n * game still moves it through its resolver; this is the story's statement\n * of intent, not the game's policy. Mirrors Patter's `HostScopeDecl.writable`\n * name for name (Reboot.md 10, ruled 2026-09-03). Ignored on every other\n * scope. Absent = writable.\n */\n writable?: boolean;\n /**\n * The sharing axis (design/flows.md, Patter's flag adopted): is this\n * property's value one world value across all flows, or a copy per flow?\n * It does NOT change reference syntax - sharing is set here, on the\n * declaration, not by a different scope token. Absent = the scope\n * default: `@story` shared; box, deck, hand and tag properties per-flow.\n * On a `@world` declaration the flag is a validation error - `@world` is\n * the game's own state, always engine-level, never per-flow.\n */\n shared?: boolean;\n /**\n * The durability axis (design/engine-server.md 4.2), valid wherever `shared`\n * is valid and orthogonal to it: `shared` says whose value this is WITHIN a\n * run, `durable` says whether the value survives the run at all. A durable\n * shared property is the installation's memory (\"trolls defeated since we\n * opened\"); a durable per-flow one is the player's pocket (visits,\n * allegiance, what they earned).\n *\n * INERT TO THE RUNTIME. The engine partitions by `shared` alone and never\n * reads this. Durability is what the SERVER does at a run boundary: it reads\n * the declarations, lifts the durable values out of the partitions before the\n * world restarts, and writes them back into the fresh engine afterwards,\n * entirely through `getProperty` / `setProperty`.\n *\n * On a `@world` declaration the flag is a validation error, for the reason\n * `shared` is: @world is the game's own state, and how long the game keeps it\n * is the game's business.\n */\n durable?: boolean;\n purpose?: string;\n}\n\n/** A template field (box-defined), of the card template or of the outcome\n * fields. Data for the host; the engine never interprets fields and they are\n * not addressable from expressions. */\nexport interface FieldDecl {\n name: string;\n type: PropertyType;\n default: ScalarValue;\n values?: string[];\n purpose?: string;\n}\n\n/** Cooldown policy, in turns (schema 3.4). */\nexport type RedrawPolicy = \"always\" | \"never\" | number;\n\n// --- gameId derivation (Patter's effectiveGameId, adopted 2026-07-20) --------\n//\n// gameId is the renameable host-facing address; it is OPTIONAL in source and\n// derived from the entity's title until the author pins one, so a rename of\n// the title carries the address with it (no \"new-deck\" stuck placeholder).\n// The compiler fills a concrete gameId into every bundle entity.\n\n/** Slugify a human label into a filename- / address-safe gameId. */\nexport function gameIdify(text: string): string {\n return text.toLowerCase().replace(/['’]/g, \"\")\n .replace(/[^a-z0-9-]+/g, \"-\").replace(/-+/g, \"-\").replace(/^-+|-+$/g, \"\");\n}\n\nexport function isValidGameId(gameId: string): boolean {\n return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);\n}\n\n// --- property names (adopted 2026-08-18, with Patter, from one rule) ---------\n//\n// Design and argument: `@wildwinter/app-shell` src/property-names.ts. Not house\n// style: the rule is what `@wildwinter/expr` can parse. Its lexer takes an\n// identifier as /[a-zA-Z_][a-zA-Z0-9_]*/ and folds it to lower case, so\n// `@story.isNight` reaches a property called `isnight`, `@story.9lives` and\n// `@story.not` are parse errors, and `@story.is-night` is not an error at all: it\n// compiles to `@story.is` MINUS the string \"night\". That last one is why the rule\n// is enforced rather than trusted - it is the only violation that silently means\n// something else.\n//\n// Here rather than behind an import of the UI kit because the compiler, the CLI\n// and the embedded runtime resolve state by these. `property-name-parity.test.ts`\n// holds them to the shell's, and `property-name-grammar.test.ts` holds them to the\n// parser they came from.\n\n/** The words `@wildwinter/expr` lexes as keywords, so no property may be called one. */\n// A legal property NAME is a fact about the expression language, not about this\n// model: `not` is reserved because the tokeniser reads it as an operator. Both\n// families kept their own copy of the rule AND of the keyword list, a list\n// neither owned. @wildwinter/expr derives the list from its own tokeniser, so a\n// keyword added there cannot leave a stale copy here.\n//\n// Re-exported so nothing that imports them has to move.\nexport {\n propertyNameify, isValidPropertyName, isCaseOnlyPropertyName, RESERVED_PROPERTY_NAMES,\n} from \"@wildwinter/expr\";\n/** The effective address: a pinned gameId, else derived from the title, else\n * the immutable id (so there is always something addressable). */\nexport function effectiveGameId(entity: { gameId?: string; title?: string; id: string }): string {\n const pinned = entity.gameId?.trim();\n if (pinned) return pinned;\n const fromTitle = entity.title ? gameIdify(entity.title) : \"\";\n return fromTitle || entity.id;\n}\n\n// --- the value scope's owner segment (design/engine-server.md 4.4) -----------\n//\n// Every other owned scope names its owner with a gameId that is unique across\n// the bundle: box, deck, hand and card. A TAG's gameId is unique only within\n// its group, and a group's only within its box, so two boxes may each name a\n// tag \"docks\" - as ordinary as two boxes each having a \"zone\" group - and\n// `value.docks.danger` then names two stores.\n//\n// So the value scope's owner segment is box-qualified where it has to be:\n// `value.<boxGameId>/<tagGameId>.<name>`. The slash sits INSIDE the owner\n// segment, so the address still splits into three on the dot and no parser\n// changes shape. The qualified form is always accepted; the short form is\n// accepted while exactly one tag in the bundle carries that gameId, and\n// refused when more do, naming the qualified candidates. What a runtime\n// PRINTS - `listProperties`, a write on the trace, a load report, an\n// examiner - is the short form except where the gameId repeats.\n//\n// One definition, because the Board draws these addresses from the bundle\n// while the engine builds them from its own index, and an address the editor\n// shows that the engine will not take is the fault 4.4 was fixing.\n\n/** The three answers a value address needs, all derived from the bundle. */\nexport interface ValueAddresses {\n /** Tag internal id -> the owner segment an address PRINTS for it. */\n print: Map<string, string>;\n /** Every owner segment a value address ACCEPTS -> the tag's internal id.\n * Holds the qualified form for every tag and the short form only for a\n * gameId no other tag shares. */\n accept: Map<string, string>;\n /** A tag gameId more than one box uses -> its qualified forms, in bundle\n * order. Empty for the overwhelming majority of projects, and what a\n * refusal lists. */\n repeated: Map<string, string[]>;\n}\n\n/** The owner segment of every tag in the bundle, both ways round. */\nexport function valueAddresses(bundle: {\n boxes: readonly { id: string; gameId?: string; title?: string; tagGroups: readonly TagGroup[] }[];\n}): ValueAddresses {\n const tags: { id: string; gameId: string; qualified: string }[] = [];\n for (const box of bundle.boxes) {\n const boxGameId = effectiveGameId(box);\n for (const group of box.tagGroups) {\n for (const tag of group.tags) {\n const gameId = effectiveGameId(tag);\n tags.push({ id: tag.id, gameId, qualified: `${boxGameId}/${gameId}` });\n }\n }\n }\n // Distinct qualified forms per gameId. Distinct rather than a count: two\n // groups in ONE box may also name a tag the same way, and a refusal that\n // offered the same address twice would be no help at all. Those two share\n // the qualified segment, and the first in bundle order answers to it, which\n // is what the short form did for everything before this rule.\n //\n // That last case is closing at the source rather than here (question 16,\n // ruled 2026-09-06): the compiler WARNS that a tag gameId must be unique\n // within its box, across all of that box's groups, and refuses it from the\n // next release. So this stays as the reading rule for a bundle built before\n // that, and a bundle built after it has no repeated qualified form to read.\n const forms = new Map<string, string[]>();\n for (const tag of tags) {\n const list = forms.get(tag.gameId) ?? [];\n if (!list.includes(tag.qualified)) list.push(tag.qualified);\n forms.set(tag.gameId, list);\n }\n const print = new Map<string, string>();\n const accept = new Map<string, string>();\n const repeated = new Map<string, string[]>();\n for (const tag of tags) {\n const candidates = forms.get(tag.gameId) ?? [tag.qualified];\n const ambiguous = candidates.length > 1;\n print.set(tag.id, ambiguous ? tag.qualified : tag.gameId);\n if (!accept.has(tag.qualified)) accept.set(tag.qualified, tag.id);\n if (!ambiguous && !accept.has(tag.gameId)) accept.set(tag.gameId, tag.id);\n if (ambiguous) repeated.set(tag.gameId, candidates);\n }\n return { print, accept, repeated };\n}\n\n/** What an ambiguous short-form value address is told: the candidates, in\n * full, because \"that names two tags\" without them leaves a host reading a\n * bundle it did not write to find out which boxes. */\nexport function ambiguousValueAddressMessage(\n segment: string, name: string, candidates: readonly string[],\n): string {\n const forms = candidates.map((q) => `\"value.${q}.${name}\"`);\n const list = forms.length <= 1 ? (forms[0] ?? \"\")\n : `${forms.slice(0, -1).join(\", \")} or ${forms[forms.length - 1]}`;\n return `\"value.${segment}.${name}\" names a tag in ${candidates.length} boxes; write ${list}`;\n}\n\n/**\n * The first free gameId of the form `base`, `base-2`, `base-3`, ... not already\n * in `taken`.\n *\n * A gameId is API - `deal()` and the play log speak it - so a name minted for a\n * new or duplicated entity must not collide with an existing one. This lived in\n * two copies, one in the editor and one in the CLI's kit scaffolder, character\n * for character the same; two copies of an addressing rule can drift, and a\n * drift here means the same act produces different addresses depending on which\n * program did it. It is here, beside `gameIdify`, because both programs need it\n * and no UI touches it.\n */\nexport function freeGameId(base: string, taken: ReadonlySet<string>): string {\n let gameId = base;\n for (let n = 2; taken.has(gameId); n++) gameId = `${base}-${n}`;\n return gameId;\n}\n\n/**\n * The first free TITLE of the form `base`, `base 2`, `base 3`, ... whose\n * derived gameId is not already in `taken`.\n *\n * The sibling of `freeGameId` for the \"New box\", \"New deck\" case, where the\n * author is given a title and the address follows from it. The dedupe is on the\n * DERIVED gameId rather than the title, because two titles that slug to one\n * address are the collision that matters.\n */\n/**\n * An id-sorted collection in the order a person should SEE it.\n *\n * Storage is sorted by immutable id (source rule 5) so that two authors adding\n * one item each never touch the same line. That makes array position useless as\n * display order, so the order the author arranged rides in a sparse `order`\n * field, with position as the fallback and id to break a tie.\n *\n * One definition, because this rule has to give the same answer in four places\n * that a reader compares side by side: the compiler (what the bundle carries),\n * the editor (what the card document lists), the exports, and Find. When they\n * disagree, the editor shows one order and the game plays another.\n */\nexport function byDisplayOrder<T extends { id?: string; order?: number }>(items: readonly T[]): T[] {\n return items\n .map((x, i) => ({ x, key: x.order ?? i, i }))\n .sort((a, b) => a.key - b.key || ((a.x.id ?? \"\") < (b.x.id ?? \"\") ? -1 : (a.x.id ?? \"\") > (b.x.id ?? \"\") ? 1 : a.i - b.i))\n .map((e) => e.x);\n}\n\nexport function freeTitle(base: string, taken: ReadonlySet<string>): string {\n let title = base;\n for (let n = 2; taken.has(gameIdify(title)); n++) title = `${base} ${n}`;\n return title;\n}\n\n/**\n * A count of a TIMED box's turns, said as time (design/engine-server.md 4.8):\n * `turnSpan(30, 60)` is \"30 min\", and `turnSpan(30, 60, true)` is \"30 minutes\".\n *\n * One definition, because the conversion appears wherever a designer might\n * otherwise have to do it in their head: the card editor's Redraw field, the\n * box page, the Board's advance buttons, and the coverage report's turn\n * budget. Two of those want the unit spelled out and two want it short, which\n * is the whole of `long`.\n */\nexport function turnSpan(turns: number, seconds: number, long = false): string {\n const total = Math.max(0, Math.round(turns * seconds));\n const say = (n: number, short: string, one: string, many: string): string =>\n long ? `${n} ${n === 1 ? one : many}` : `${n} ${short}`;\n if (total < 60) return long ? say(total, \"s\", \"second\", \"seconds\") : `${total}s`;\n if (total < 3600) {\n const minutes = total % 60 === 0 ? total / 60 : Math.round(total / 6) / 10;\n return say(minutes, \"min\", \"minute\", \"minutes\");\n }\n let hours = Math.floor(total / 3600);\n let rest = Math.round((total % 3600) / 60);\n if (rest === 60) { hours += 1; rest = 0; }\n const said = say(hours, \"hr\", \"hour\", \"hours\");\n return rest === 0 ? said : `${said} ${say(rest, \"min\", \"minute\", \"minutes\")}`;\n}\n\n// --- entities (generic over expression representation E) ---------------------\n\nexport interface Outcome<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Unlike `Card.order` this one IS compiled into the bundle:\n * which option is offered first is authorial, and a host reading a dealt\n * card's outcomes is building the player's menu. */\n order?: number;\n /** Gating; availability is always evaluated against current state. */\n condition?: E;\n /** Target (\"@scope.name\") -> expression; all right-hand sides evaluate\n * against pre-play state (schema 3.7). */\n changes: Record<string, E>;\n /** Template data, as `Card.fields` is: field name -> value, declared by\n * the box's `outcomeFields`, validated at publish, and handed to the host\n * with the outcome. The engine never reads it: a press can say one line\n * (\"The notice is in your pocket\") without spending a card on it. */\n fields?: Record<string, ScalarValue>;\n}\n\nexport interface Card<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order within the deck (sparse; a card without one falls\n * back to its id position). Merges as a per-card value, so id-sorted storage\n * stays merge-clean (Reboot 7.4); dropped from the compiled bundle. */\n order?: number;\n condition?: E;\n /** Default 0; an expression must evaluate to a number. */\n priority: number | E;\n redraw: RedrawPolicy;\n /** Tags: tag group id -> tag ids. An absent group is a wildcard (matches\n * any binding of it), except the reserved home group, whose default\n * inverts (schema 2.4). Editors and fixtures speak gameIds; stored\n * references are ids. */\n tags?: Record<string, string[]>;\n /** How many hands may hold this card at once (schema 3.5): integer >= 1,\n * default 1. One copy is the exclusivity rule; copies: N is the\n * deliberate opt-out for interchangeable filler. Always counted WITHIN a\n * flow, whether or not the card is shared. */\n copies?: number;\n /** Scarcity across flows (design/shared-scarcity.md). Absent takes the\n * deck's flag; set here it overrides the deck, so a single unique card can\n * stay in the content it belongs to. A shared card's claims count every\n * flow's board, and a shared `redraw: \"never\"` is spent for everyone the\n * first time anyone plays it.\n *\n * A finite `redraw` stays PER FLOW even when shared: a cooldown is an\n * absolute turn of the card's box clock and clocks are per flow, so\n * \"3 turns of whose clock?\" has no answer. A world-wide timer is a @world\n * question, not an engine one (shared-scarcity 9.3.3). */\n shared?: boolean;\n /** The world cap: how many hands ACROSS EVERY FLOW may hold this at once.\n * Read only when the card is shared, and defaults to `copies`, so the\n * common case writes one number and \"five in the world, one to a customer\"\n * is `copies: 1, sharedCopies: 5`. */\n sharedCopies?: number;\n /** Does this card's `redraw: \"never\"` spend survive the run\n * (design/engine-server.md 4.2)? Absent takes the deck's flag, set here it\n * overrides the deck, exactly as `shared` does. `shared` decides who a\n * spend counts for WITHIN a run; this decides whether it outlives one.\n *\n * Only `\"never\"` crosses the run boundary, for the reason only `\"never\"`\n * crosses the flow boundary (shared-scarcity 9.3.2): a finite cooldown is\n * an absolute turn of a box clock, and the clock resets with the run. On\n * any other redraw the flag is a compile warning.\n *\n * INERT TO THE RUNTIME, like the declaration flag: the server lifts the\n * durable spends at run end (per-flow ones from the flow's `never`\n * cooldowns, shared ones from the engine's spent set) and puts them back\n * through `openFlow(id, { restore })` and `markTaken`. */\n durable?: boolean;\n /** Card-template data: field name -> value, validated at publish. */\n fields?: Record<string, ScalarValue>;\n outcomes: Outcome<E>[];\n}\n\nexport interface Deck<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** The deck gate, evaluated once per draw in the draw's environment. */\n condition?: E;\n /** This pile is scarce across flows (design/shared-scarcity.md): every card\n * in it is shared unless the card says otherwise. The container is where\n * Patter puts its own shared-memory flag, and the deck is our container. */\n shared?: boolean;\n /** Every `redraw: \"never\"` card in this pile is spent for good, past the end\n * of the run, unless the card says otherwise (design/engine-server.md 4.2).\n * The container carries the flag for the reason `shared` is carried here:\n * a pile is what an author reaches for when a rule is true of all of it. */\n durable?: boolean;\n properties: PropertyDecl[];\n cards: Card<E>[];\n}\n\nexport interface Tag {\n id: string;\n gameId?: string;\n /**\n * This tag's own starting values for properties its GROUP declares\n * (design/hand-typing.md). The group says what the property IS; a tag says\n * only where it starts, so \"every zone has a haunting level\" is written once\n * and \"the cave starts at 2\" is written where it belongs.\n *\n * A name here that the group does not declare is an error: it would be a\n * value for nothing.\n */\n values?: Record<string, ScalarValue>;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n properties?: PropertyDecl[];\n /** Template-of-play extras (e.g. spatial geometry). Source only: preserved\n * in shards, never compiled into the bundle. */\n templates?: Record<string, unknown>;\n}\n\n/** A named axis for cross-cutting cards (schema 2.4, renamed from\n * Dimension). Tags are declared, not freeform. */\nexport interface TagGroup {\n id: string;\n gameId?: string;\n purpose?: string;\n /**\n * A property reference (`\"@story.act\"`) whose value names a tag in this group\n * by gameId. The engine reads it at every ask and binds the group, exactly as\n * if the asking hand had chosen that tag; a hand's own binding wins.\n *\n * For an axis driven by STATE rather than by place: acts, chapters, a\n * difficulty band. Without it, only a hand can bind a group, so such an axis\n * had nowhere to gate and every card needed its own condition.\n *\n * A reference rather than an expression on purpose (design/where-and-\n * selectors.md Part B): a computed binding belongs in a property the outcomes\n * maintain, and an expression here would make this type generic for no gain.\n */\n boundBy?: string;\n /**\n * What omitting this group means for a card. Default false: omission is a\n * wildcard, so the card matches whatever the group is bound to. True inverts\n * it, so a card that names no tag here is unavailable wherever the group IS\n * bound (and unaffected where it is not).\n *\n * `place` is the built-in instance of this pair: bound to the asking hand,\n * and inverted per card rather than per group.\n */\n required?: boolean;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n /**\n * Properties EVERY tag in this group has (design/hand-typing.md). The\n * declaration lives here and each tag carries only its own starting value in\n * `Tag.values`, which is the separation the format was missing: a tag's own\n * `properties` entry has to restate the type on every tag purely in order to\n * say the value, and a tag added later silently arrives without it.\n *\n * Compiled by FLATTENING onto each tag, so the bundle keeps its per-tag\n * shape and no runtime, port or bundle schema changes: source is where the\n * author works and where merges happen, the bundle is a compiled artefact\n * that can afford to be explicit.\n *\n * A tag may still declare its own `properties` for a group whose tags\n * genuinely differ. Declaring the same NAME both ways is an error.\n */\n properties?: PropertyDecl[];\n tags: Tag[];\n /** Template-of-play extras for the GROUP, the same bag its tags carry: this is\n * where a group is marked spatial and where that template keeps its own\n * group-level configuration. Source only, preserved but never compiled.\n *\n * A bag rather than a `spatial: true` flag because the marker and the\n * configuration are one thing (see model/spatial.ts), and because core is not\n * meant to grow a field per template of play. */\n templates?: Record<string, unknown>;\n}\n\n/** The reserved tag group (schema 2.4): present in every box without\n * declaration, its tags the box's hand ids. Every hand implicitly binds it to\n * itself; a card that names a place is available only at that place.\n *\n * Called `place` rather than `home` since 2026-08-21: one word for one thing\n * across the format, the editor and the exports. \"Where\" is the QUESTION a\n * card answers (at a place, or anywhere in a region); \"place\" is the direct\n * half of that answer. `home` was a metaphor an author had to learn, and it\n * leaked into hand-edited shards and the docs. */\nexport const PLACE_GROUP = \"place\";\n\n/** The scopes a movable hole may be filled from (design/engine-server.md 4.6):\n * the two `boundBy` already allows, plus `@hand` - the asking hand's OWN\n * declared property, resolved before tag composition so a movable hole can\n * never depend on the tags it is choosing. */\nexport type HoleRefScope = \"hand\" | \"story\" | \"world\";\n\n/** A parsed hole reference: `@hand.zone` -> `{ scope: \"hand\", name: \"zone\" }`. */\nexport interface HoleRef {\n scope: HoleRefScope;\n name: string;\n}\n\nconst HOLE_REF = /^@(hand|world|story)\\.([a-z][a-z0-9_-]*)$/;\n\n/**\n * Is this `chosen` / binding value MEANT as a property reference rather than a\n * tag id?\n *\n * The test is the leading `@` alone, deliberately: a value that starts with\n * one and does not parse is a mistyped reference, which the compiler should\n * name as such, not a tag id that happens to look odd. Tag ids never begin\n * with `@`.\n */\nexport const isHoleRef = (value: string): boolean => value.startsWith(\"@\");\n\n/** Parse a hole reference, or undefined when it is not one. The on-disk form\n * stays a plain string, so the canonical serialiser and the shard merge need\n * no change at all: a hole is still one group name against one value. */\nexport const parseHoleRef = (value: string): HoleRef | undefined => {\n const m = HOLE_REF.exec(value);\n return m === null ? undefined : { scope: m[1] as HoleRefScope, name: m[2]! };\n};\n\n/** A declared kind of hand (schema 2.6): live-inherited, author-side only,\n * never called from game code. One condition governs every instance. */\nexport interface HandTemplate<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n\n /** Fixed tag bindings: tag group id -> tag id. Literal tags only: what a\n * template FIXES is the same for every instance, and a hole that moves is\n * the instance's own business (`Hand.chosen`, 4.6). */\n bindings?: Record<string, string>;\n /** The holes: tag group ids each instance fills (one tag each, or one\n * property reference: 4.6). */\n chooses?: string[];\n /** Shared availability condition, ANDed in (schema 3.1); evaluated per\n * instance against that instance's composed @hand. */\n condition?: E;\n /** Default slot cap. */\n slots: number | \"unbounded\";\n /** Declared @hand state every instance carries. */\n properties: PropertyDecl[];\n}\n\n/** A standalone hand's inline rule (schema 2.6): owned by the hand. */\nexport interface HandRule<E> {\n /**\n * Tag group id -> tag id, or a PROPERTY REFERENCE (`\"@hand.zone\"`,\n * `\"@story.where\"`, `\"@world.place\"`) the runtime resolves at ask time\n * (design/engine-server.md 4.6, the hand that moves). Still a plain string\n * on disk, so the canonical serialiser and the merge are untouched; what\n * widened is the meaning, and `parseHoleRef` is where it is read.\n *\n * `place` is never fillable this way: it is the hand's own name, not an axis.\n */\n bindings?: Record<string, string>;\n condition?: E;\n slots: number | \"unbounded\";\n}\n\n/** A hand (schema 2.6): a template instance (template + chosen) or a\n * standalone hand (rule). Exactly one of template / rule. Fully concrete:\n * deal is name-only. */\nexport interface Hand<E> {\n id: string;\n /** The name deal() is called with; a rename is a breaking change\n * (Reboot 7.4). */\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Hand template id (not gameId). */\n template?: string;\n /**\n * Template instances: tag group id -> tag id, one per `chooses` hole.\n *\n * A value may instead be a PROPERTY REFERENCE (`\"@hand.zone\"`,\n * `\"@story.where\"`, `\"@world.place\"`), which makes the hole MOVABLE: the\n * runtime resolves the reference at ask time and binds the hole to the tag\n * the value names, so moving the Elder to the forest is `setProperty` and\n * nothing else (design/engine-server.md 4.6). Still a plain string on disk,\n * so the canonical serialiser and the shard merge need no change; read it\n * with `parseHoleRef`.\n */\n chosen?: Record<string, string>;\n /** Standalone hands: the inline rule. */\n rule?: HandRule<E>;\n /** Override; defaults to the template's / rule's slots. The ONLY template\n * field an instance may override (schema 2.6). */\n slots?: number;\n /** Standalone hands' own @hand state (template instances inherit the\n * template's declarations). */\n properties?: PropertyDecl[];\n /** Authored display order within the box (sparse; authoring-only, never\n * compiled into the bundle - the compiler's explicit field list drops it). */\n order?: number;\n /** Template-of-play extras (e.g. a spatial pin). Source only. */\n templates?: Record<string, unknown>;\n}\n\nexport interface Box<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** The only per-box ranking policy (Reboot 2.2). */\n ranking: { specificity: boolean };\n /**\n * A TIMED box: its clock counts real time, one turn every `seconds` of the\n * run (design/engine-server.md 4.8). Absent is the ordinary box, whose turn\n * is a play.\n *\n * Two things follow, and only two. In the ENGINE, a play in this box\n * defaults to advancing nothing: `settings.playAdvancesTurns` does not\n * apply, so a designer cannot declare the convention and then forget to\n * switch play-advance off. Everywhere else it is what the tools SAY: the\n * host ticks the box (the runtime has no clock and gains none here), and a\n * card's `redraw: N` reads as N x `seconds`, which the editors, the bundle\n * inspectors and the coverage report spell out rather than leaving a\n * designer to know that 30 meant minutes.\n *\n * The number itself is inert to the runtime, which never reads it.\n */\n turn?: { seconds: number };\n /** The card template: what every card in this box carries. */\n fields: FieldDecl[];\n /** What every outcome in this box may carry, declared the same way.\n * Absent when the box declares none, so a bundle without them is byte for\n * byte what it was. */\n outcomeFields?: FieldDecl[];\n properties: PropertyDecl[];\n tagGroups: TagGroup[];\n decks: Deck<E>[];\n handTemplates: HandTemplate<E>[];\n hands: Hand<E>[];\n}\n\n// --- the compiled bundle (.storyletsc) ---------------------------------------\n\nexport const BUNDLE_SCHEMA = \"storylets/bundle@0\";\n\n/** Binds bundles to shards (staleness gate) and saves to bundles. */\nexport interface BundleContent {\n project: string;\n version: string;\n /** hash32 over the canonical source shards (schema 2.8). */\n hash: string;\n}\n\nexport interface BundleSettings {\n playAdvancesTurns: number;\n}\n\n/**\n * The play ladder (design/engine-server.md 4.10): how much of itself\n * Storyletter shows this project, in one setting with three rungs rather than\n * a set of toggles, because the features nest.\n *\n * solo one player, one flow: no sharing, no durability, no venue features\n * shared several players over one world: sharing appears\n * venue a production: nothing is hidden\n *\n * EDITOR-SIDE ONLY. It stays in the project shard beside `coverage` and\n * `export` and is never compiled: a solo project plays on the same Engine as a\n * venue one. Hidden is hidden rather than disabled, so going DOWN a rung is\n * refused when the project already contains what the rung would hide, and a\n * hand-edited shard above its rung is a compile warning.\n */\nexport type PlayRung = \"solo\" | \"shared\" | \"venue\";\n\n/** The default rung: a project shard that says nothing is a solo game. */\nexport const DEFAULT_PLAY_RUNG: PlayRung = \"solo\";\n\n/** The project shard's settings block: what the bundle carries, plus the\n * authoring-side play rung that it does not. */\nexport interface ProjectSettings extends BundleSettings {\n /** The play ladder rung (see `PlayRung`). Absent = \"solo\". */\n play?: PlayRung;\n}\n\n/**\n * A map that a bundle was asked to carry: one spatial tag group's geometry,\n * flattened for a host to draw (design/graphical-views.md 2, \"The map MAY ship\").\n *\n * INERT PAYLOAD. Nothing in the engine reads this and nothing ever will: the\n * runtime deals in tag names. It is here so a host that wants an in-game map does\n * not have to invent its own export, and it is absent unless the project asked\n * for it (`export.map`), so a build that does not want a map carries no bytes.\n *\n * GAME IDS throughout, never internal ids. Internal ids are authoring identity\n * and mean nothing outside the project; a host matches these against the same\n * names it passes to `peek`. There is nothing here to strip either, which is why\n * `metadata: \"stripped\"` needs no special case: no titles, no purposes.\n *\n * SITES ARE HERE, which reverses a ruling. Until 2026-09-05 this comment said\n * they were deliberately not: a site was where an author parked a hand while\n * working, held in the view sidecar precisely because it was not content, and a\n * host that wanted to place a hand had its zone from the compiled binding. That\n * held for a game, where a hand's zone is its only real-world meaning. It does\n * not hold for a physical experience (design/engine-server.md 4.3), where the\n * position IS content: it is where the kiosk stands, and a producer's map is\n * simply wrong without it. The alternative was a second file beside the bundle,\n * which would cost a format the inspectors do not read and would put the view\n * sidecar in the shipping path by the back door.\n */\nexport interface BundleMap {\n /** The owning box, by gameId (tag groups are box-scoped). */\n box: string;\n /** The tag group this is a map of, by gameId. */\n group: string;\n /** One entry per zone that has been drawn; a tag with no polygon is not a\n * place yet and is left out rather than shipped as an empty shape. */\n zones: { tag: string; polygon: ViewPoint[] }[];\n /** Background pictures, back to front, as bundle-relative paths. Hidden ones\n * do not ship: what an author put away is not something to spring on a host. */\n backgrounds?: BundleBackground[];\n /** Where the placed hands stand on this map, by hand gameId, sorted by that\n * gameId so the bytes do not depend on authoring order. A hand nobody has\n * placed has no entry, and a map with no placed hand has no key at all. The\n * zone a site sits in is NOT repeated here: the hand's own binding is what\n * the runtime deals from, and a second copy could only go on to disagree. */\n sites?: { hand: string; x: number; y: number }[];\n}\n\n/** One shipped picture. `locked` and `hidden` are authoring state and do not\n * travel; the draw order is the array order. */\nexport interface BundleBackground {\n /** Where the file sits relative to the bundle (\"assets/<box>/<file>\"). */\n file: string;\n x: number;\n y: number;\n width: number;\n height: number;\n opacity?: number;\n}\n\nexport interface Bundle {\n schema: typeof BUNDLE_SCHEMA;\n content: BundleContent;\n metadata: \"full\" | \"stripped\";\n settings: BundleSettings;\n world: {\n properties: PropertyDecl[];\n /** ScopeRegistrySpec (@wildwinter/scoperegistry): the owned/foreign\n * split. Absent = engine-owned @world (standalone play). */\n registry?: unknown;\n };\n story: {\n properties: PropertyDecl[];\n };\n boxes: Box<Expression>[];\n /** Maps, when the project asked for them. Absent is the normal state. */\n maps?: BundleMap[];\n}\n\n// --- the save envelope --------------------------------------------------------\n\nexport const SAVE_SCHEMA = \"storylets/save@1\";\n\nexport interface PlayRecord {\n /** Card and outcome by gameId (feeds the play-history functions). */\n card: string;\n /** \"\" for a card with no outcomes, played with none: the key is always\n * there, so a save's shape does not depend on the card. */\n outcome: string;\n turn: number;\n}\n\n/** A property bag: name -> value. */\nexport type PropertyBag = Record<string, ScalarValue>;\n\n/** The per-scope property partitions one side of the sharing flag holds:\n * a save carries one of these for the shared values and one per flow\n * (design/flows.md). NO world key, in either: @world is the game's own\n * state, resolved through the world resolver and saved by whoever owns\n * it - \"host saves its container once, each engine saves its own\n * envelope\" (engine-runtimes.md 3.1). */\nexport interface PropsPartition {\n story: PropertyBag;\n box: Record<string, PropertyBag>;\n deck: Record<string, PropertyBag>;\n hand: Record<string, PropertyBag>;\n /** Tag state, keyed by tag id. */\n value: Record<string, PropertyBag>;\n}\n\n/** One flow's snapshot inside the envelope (schema 4). */\nexport interface FlowSave {\n /** The per-flow property partitions. */\n props: PropsPartition;\n /** Per-box turn counters, keyed by box id (schema 3.4) - per flow: there\n * is deliberately no global turn. */\n turns: Record<string, number>;\n /** mulberry32 state, uint32 (schema 3.3), per flow. */\n prng: number;\n /** Absolute next-eligible turn (of the card's box's clock) per card id;\n * MAX_SAFE_INTEGER = never (deliberately not Infinity, which\n * JSON-serialises to null). */\n cooldowns: Record<string, number>;\n /** Hand contents (card ids, in dealt order), keyed by hand id. The claims\n * ledger is derived from this (schema 3.5). */\n board: Record<string, string[]>;\n playLog: PlayRecord[];\n}\n\n/** The whole engine, one envelope: the shared partitions once, then every\n * live flow keyed by its id - Patter's shape (one shared blob + N flow\n * blobs; multi-flow and save/load are the same feature). */\n/** The engine's half of a save: what every flow shares. Properties, and the\n * cards a shared `redraw: \"never\"` has taken out of the world for good\n * (design/shared-scarcity.md). Claims are NOT here: they are derived from the\n * live boards, and each flow's board rides its own blob. */\nexport interface SharedSave {\n props: PropsPartition;\n /** Card ids, sorted, so a save is byte-stable for a diff. */\n spent: string[];\n}\n\nexport interface SaveEnvelope {\n schema: typeof SAVE_SCHEMA;\n content: BundleContent;\n shared: SharedSave;\n flows: Record<string, FlowSave>;\n}\n\n// --- the load report (design/engine-server.md 4.9) ----------------------------\n//\n// `loadGame` is forgiving by design: a card the bundle no longer has drops off\n// the board, a property the save does not carry keeps its default, and a\n// version two builds newer loads without a word. That forgiveness is what makes\n// a save survive an edit, and it is also what hides the cost of a content\n// update from whoever is about to apply one. The report is the same walk,\n// itemised: `previewLoad` computes it and changes nothing, `loadGame` computes\n// it and applies it, and `previewFlowRestore` answers the same questions for\n// one flow (4.1's `openFlow(id, { restore })`).\n//\n// Card, hand and flow identities are GAME IDS: a report is host-facing and\n// internal ids mean nothing outside the project. The one exception is an\n// entity the edit DELETED - a vanished card, a vanished hand - which has no\n// gameId left to give, so the report carries the id the save itself carries.\n// There is nothing else to name it by.\n//\n// A property is named differently, and deliberately: by its ENGINE ADDRESS,\n// the string listProperties() prints and getProperty()/setProperty() accept.\n// A report entry is then something a host can act on rather than merely\n// print, and the runtimes have one property grammar instead of two.\n\n/** One card that a restore refused to put back on the board.\n *\n * `vanished` and `hand-vanished` are the edit's doing (the card, or the hand\n * it sat in, is no longer in the bundle). `claimed-elsewhere` is only ever a\n * single-flow restore into a LIVE engine: the card is shared, and the other\n * open flows already hold every copy the world has. */\nexport interface LoadEviction {\n flow: string;\n hand: string;\n card: string;\n reason: \"vanished\" | \"hand-vanished\" | \"claimed-elsewhere\";\n}\n\n/** One property the restore could not put back as it was. `flow` names the\n * flow whose half it belongs to; absent, it is the shared half.\n *\n * `path` is the engine's property address, spelled exactly as\n * `Flow.listProperties()` / `Engine.listProperties()` print it and exactly as\n * `getProperty` and `setProperty` accept it: `story.<name>` for the story\n * scope, `<scope>.<ownerGameId>.<name>` for the box, deck, hand and tag\n * scopes. No `@`, which belongs to the expression language and not to an\n * address.\n *\n * The owner segment is its GAMEID (design/engine-server.md 4.4), the name it\n * is called by everywhere else, so an operator reading a hot-swap report can\n * paste the address straight into `setProperty`. An owner the build no longer\n * has keeps the id the save carried: there is no gameId left to give it,\n * which is the rule the eviction list above has always used. */\nexport interface LoadProperty {\n flow?: string;\n path: string;\n}\n\n/** What a load or a flow restore would do that is not a plain restore\n * (design/engine-server.md 4.9). Arrays are sorted, so two runtimes given the\n * same save and bundle produce the same bytes; `flows` alone keeps the\n * envelope's own order, because a caller re-takes its handles in it. */\nexport interface LoadReport {\n /** No drift and nothing dropped, defaulted or retyped: the save goes back\n * exactly as it was. `flows` is not a divergence and does not count. */\n exact: boolean;\n project: string;\n /** Drift when the two differ; reported, never refused. */\n version: { saved: string; bundle: string };\n /** Drift when the two differ; reported, never refused. */\n hash: { saved: string; bundle: string };\n /** The flows this restores, in the order it restores them. */\n flows: string[];\n evicted: LoadEviction[];\n /** Cooldowns held for cards the bundle no longer has. */\n droppedCooldowns: { flow: string; card: string }[];\n /** Shared `redraw: \"never\"` entries for cards the bundle no longer has. */\n droppedSpent: string[];\n /** In the save, not declared any more. */\n droppedProperties: LoadProperty[];\n /** Declared, not in the save: it takes the declaration's default. */\n defaultedProperties: LoadProperty[];\n /** In the save, still declared, but the saved value no longer fits the\n * declaration (its type changed, or an enum value / quality stage was\n * edited away). It takes the declaration's default. */\n retypedProperties: LoadProperty[];\n}\n\n/** The .storyletsave FILE: the HOST's file, not the engine's - the engine's\n * envelope plus, when the host keeps one, its @world container. This is\n * \"host saves its container once, each engine saves its own envelope\"\n * folded into one file for the single-host case; the ENGINE never reads or\n * writes `world` (loadGame takes the envelope alone). */\nexport const SAVEFILE_SCHEMA = \"storylets/savefile@1\";\n\nexport interface SaveFile {\n schema: typeof SAVEFILE_SCHEMA;\n engine: SaveEnvelope;\n /** The host's @world values, saved and restored by the host. */\n world?: PropertyBag;\n}\n\n// --- source shards (design/storylets-source.md) --------------------------------\n\n/** The project folder: a macOS package, a plain folder elsewhere. */\nexport const PROJECT_FOLDER_EXTENSION = \".storylets\";\n/** The compiled bundle (strict JSON; generated, never hand-edited). */\nexport const BUNDLE_EXTENSION = \".storyletsc\";\n\n/**\n * Where a shipped background sits, relative to the bundle file.\n *\n * One function so the compiler (which writes the name into the bundle) and the\n * export op (which writes the bytes) cannot drift apart: a path agreed in two\n * places is a path that eventually disagrees. Per BOX, because two boxes may\n * each have their own `plan.png` and a build must not silently keep one of them.\n */\nexport const bundleAssetPath = (boxGameId: string, file: string): string =>\n `assets/${boxGameId}/${file}`;\n/** Per-type shard extensions, JSON5 inside (source doc section 2). */\nexport const SHARD_EXTENSIONS = {\n project: \".storyletproj\",\n box: \".storyletbox\",\n tags: \".storylettags\",\n hands: \".storylethands\",\n deck: \".storyletdeck\",\n /** The AUTHOR's arrangement layer: the canvases, where cards sit on a deck's\n * node canvas and the furniture drawn round them. Its own shard because\n * positions churn (an afternoon of tidying a canvas touches every card) and\n * content does not, so a designer arranging and a writer editing never\n * collide on one file (design/graphical-views.md section 1.2). */\n view: \".storyletview\",\n /** The DESIGNER's map: where a box's hands stand in space, and the furniture\n * round them. One per box, beside the view shard.\n *\n * Split out of the view shard on 2026-09-06 (design/engine-server.md 9.1\n * point 5) because the two halves stopped having one owner. A hand's\n * position ships in the bundle's `maps` block (4.3) and is where a venue's\n * kiosk stands, so it is SHAPE, which a server's author key may not change;\n * the canvases are the author's own working drawing and never leave the\n * project folder. One file could not be both. */\n map: \".storyletmap\",\n /** Threaded comments: content-ADJACENT, so neither in a content shard (a\n * writer's deck edit must not conflict with a reviewer's comment) nor in the\n * arrangement sidecar (this is not where anything sits). One per box,\n * id-keyed (design/annotation.md). Documentation NOTES used to share this\n * file and were retired: `purpose` already says why a thing exists, and\n * Patterpad's typed routing has no destination here. */\n notes: \".storyletnotes\",\n /** An installation contract: what a VENUE depends on, one file per\n * installation in `contracts/` at the project root\n * (design/engine-server.md 4.11). Its own shard, and its own folder, for the\n * walkthrough's reason (Reboot 7.5, S4): a different owner, a different\n * change rate, and a merge that must never collide with the author's edits,\n * since the server always wins its own file. */\n contract: \".storyletcontract\",\n} as const;\n\n/** Where the installation contracts live, relative to the project root. The\n * directory is the registry, as it is for a box's decks: a contract exists\n * because its file exists. */\nexport const CONTRACTS_DIR = \"contracts\";\n\nexport const PROJECT_SCHEMA = \"storylets/project@0\";\nexport const BOX_SCHEMA = \"storylets/box@0\";\nexport const TAGS_SCHEMA = \"storylets/tags@0\";\nexport const HANDS_SCHEMA = \"storylets/hands@0\";\nexport const DECK_SCHEMA = \"storylets/deck@0\";\nexport const VIEW_SCHEMA = \"storylets/view@0\";\nexport const MAP_SCHEMA = \"storylets/map@0\";\n/** The comment sidecar's schema. Still called \"notes\" on disk: the file already\n * held both, and renaming it would break every project for no gain. */\nexport const NOTES_SCHEMA = \"storylets/notes@0\";\nexport const CONTRACT_SCHEMA = \"storylets/contract@0\";\n\n/**\n * What one installation depends on, written by the venue's server and read by\n * `validate` (design/engine-server.md 4.11).\n *\n * NOT THE AUTHOR'S FILE. A venue is provisioned against names - the hands its\n * stations deal, the boxes its scheduler ticks, the properties its clocks drive,\n * the fields its crew read - and the server writes them out so the tools that\n * already gate a build can refuse a rename before it reaches the venue. A\n * project playing at two venues has two of these. The author never edits one,\n * and today, with no server built, a project either receives one or has none.\n *\n * NEVER COMPILED. It is project-side config like `coverage` and `export`: the\n * server does not need its own contract handed back, it needs the bundle to\n * still honour it.\n *\n * BY GAMEID throughout, because a gameId is the name that crosses the project's\n * border and an internal id is authoring identity.\n */\nexport interface ContractShard {\n schema: typeof CONTRACT_SCHEMA;\n /** The installation this contract speaks for. One file per installation, and\n * two files naming the same one is an error. */\n installation: string;\n /** Who wrote it, for a human reading the file (\"Storylet Server 0.1.0\"). */\n by?: string;\n /** The server's revision when it wrote this. */\n revision?: number;\n /** Hands a station is bound to, by gameId: they may not be renamed or\n * removed. */\n hands?: string[];\n /** Timed boxes the venue's scheduler ticks, by box gameId, with the turn unit\n * in SECONDS it was provisioned against. A box whose unit changed means every\n * rest on its cards changed meaning. */\n boxes?: Record<string, { turn: number }>;\n /** Property paths the venue reads or drives, in the engine's own address\n * grammar with no `@` (\"world.time_wall\", \"story.visits\"), which is how\n * `listProperties()` prints them. */\n properties?: ContractProperty[];\n /** Card-template field names the crew and the bridges read. */\n fields?: string[];\n /** Outcome field names they read, the same way: the after-line a station\n * shows when a press lands. What `fields` is to the card template, this is\n * to the box's `outcomeFields`. */\n outcomeFields?: string[];\n}\n\n/**\n * One contracted property.\n *\n * A bare path is the common form and the one the spec's example writes. The\n * object form adds the TYPE the venue was provisioned against, which is the only\n * way `validate` can catch the break that costs a producer most: a property that\n * still exists under the same name and now holds something else. A server that\n * knows the type should write the object form; a hand-written contract may say\n * only the path and get the existence check alone.\n */\nexport type ContractProperty = string | { path: string; type?: PropertyType };\n\n/** The path a contracted property names, whichever form it was written in. */\nexport const contractPropertyPath = (p: ContractProperty): string =>\n typeof p === \"string\" ? p : p.path;\n\n/** The type a contracted property was provisioned against, when it says. */\nexport const contractPropertyType = (p: ContractProperty): PropertyType | undefined =>\n typeof p === \"string\" ? undefined : p.type;\n\n/** A point in a canvas's own coordinates. */\nexport interface ViewPoint {\n x: number;\n y: number;\n}\n\n/**\n * Canvas furniture: what an author draws AROUND the content to make sense of it\n * (design/graphical-views.md 3, \"Frames and sites\").\n *\n * Both canvases carry the same thing, which is why they share a type: a node\n * canvas and a map are different views of different material, but \"put a box\n * round this lot and call it act two\" is the same thought on either.\n *\n * It lives in the view sidecar because it is ARRANGEMENT. Nothing here is\n * content: no runtime reads it, no bundle carries it, and deleting the sidecar\n * loses only the drawing. Threaded comments are the\n * other thing entirely - they attach to entities and they travel - but a canvas\n * DRAWS their markers, while owning none of them.\n */\nexport interface CanvasFurniture {\n /** Titled areas behind the content, back to front (see `stacked`).\n *\n * There was a second kind, a `stickies` list, retired on 2026-08-10\n * (design/annotation.md): a dropped comment marker does the same job in a\n * fraction of the space, and an annotation that takes as much room as the\n * thing it is about is a bad trade on a canvas. */\n frames?: Frame[];\n}\n\n/**\n * A titled area behind a group of things: Unreal's comment box.\n *\n * Deliberately dumb about what is inside it. It has no membership list and\n * computes none: a frame is a thing an author DREW, and the cards under it are\n * whatever happens to be under it now. That is what keeps it honest when content\n * moves, and it is the same reasoning that keeps a zone's sites out of the map's\n * sidecar.\n */\nexport interface Frame extends ViewPoint {\n id: string;\n w: number;\n h: number;\n /** Shown in the frame's bar, and the handle it is dragged by. */\n title?: string;\n /** One of the furniture palette's names (see `FURNITURE_COLOURS`); the theme\n * decides what that looks like, so a frame does not carry a hex value that\n * would fight the palette on the day somebody switches theme. */\n colour?: string;\n /** Place in the frame band (sparse, `stacked`). Frames can nest. */\n z?: number;\n}\n\n/** The furniture palette: names, not colours. The theme maps them, so the same\n * shard reads correctly on linen and on baize. */\nexport const FURNITURE_COLOURS = [\"paper\", \"amber\", \"sage\", \"sky\", \"rose\", \"slate\"] as const;\nexport type FurnitureColour = typeof FURNITURE_COLOURS[number];\n\n/** One deck's node canvas: where its cards sit, and the furniture around them.\n * Sparse throughout. A card with no entry lays out by default, and an entry for\n * a card that no longer exists is inert, so there is no referential integrity to\n * maintain against content that moves underneath. */\nexport interface DeckCanvas extends CanvasFurniture {\n /** Keyed by CARD id. */\n cards?: Record<string, ViewPoint>;\n}\n\n/** The box's map: where its hands sit in space, and the furniture around them.\n * Carried by the MAP shard since 2026-09-06; `ViewShard.map` is the old\n * address, read for one release and never written. */\nexport interface BoxMap extends CanvasFurniture {\n /** Keyed by HAND id. WHERE a site is, and nothing else.\n *\n * Which zone it is IN is not recorded here, and deliberately (2026-08-06,\n * with the rebinding drag): a hand that binds a zone already says so in its\n * own shard, as `chosen` or as a rule binding, and that is the truth the\n * runtime deals from. A copy here could only ever go on to disagree with it,\n * and a site whose recorded zone contradicts the hand it stands for would be\n * the most misleading thing on the map.\n *\n * Called `pins` until 2026-08-10 (design/annotation.md). No compatibility\n * branch: the only projects that exist are the examples in this repo, and they\n * were edited. */\n sites?: Record<string, ViewPoint>;\n}\n\n/** The AUTHOR's arrangement layer for one box: where cards sit on their decks'\n * canvases, and the furniture drawn round them.\n *\n * Its own shard on purpose (design/graphical-views.md section 1.2). Positions\n * churn, content does not: an afternoon of tidying a canvas touches every card,\n * and if that lived in the deck shard then a designer arranging and a writer\n * editing card text would collide on one file all day, while a content review\n * would be full of coordinates. Keyed by id throughout so the existing merge\n * engine handles two designers rearranging different things without a conflict.\n *\n * Source-only. It never reaches the compiled bundle, exactly as `order` does\n * not: the compiler reads the fields it names and this is not among them. */\nexport interface ViewShard {\n schema: typeof VIEW_SCHEMA;\n /** Keyed by DECK id: one node canvas each. */\n canvases?: Record<string, DeckCanvas>;\n /** @deprecated The box map's old address, kept for one release and READ ONLY.\n * A reader that meets it uses it when the box has no `MapShard`, and the\n * formatter moves it; nothing writes it any more. Removed after the next\n * release, at which point a map left here is simply lost. */\n map?: BoxMap;\n}\n\n/** The DESIGNER's map for one box: where its hands stand in space.\n *\n * Split out of the view shard on 2026-09-06 (design/engine-server.md 9.1 point\n * 5). The two halves had stopped sharing an owner: a hand's position ships in\n * the bundle's `maps` block (4.3), which makes it the thing a venue provisions\n * its kiosks against, while a deck's canvas is a working drawing that never\n * leaves the folder. A server's author key may change the canvases and not\n * this.\n *\n * The map is NESTED under `map` rather than flattened to the top level, and\n * deliberately: the block's bytes are then exactly what the view shard held, so\n * the migration is a move of a value rather than a reshaping of it, the merge\n * strategy carries over word for word, and a reader that has to look in both\n * places is one expression (`box.map?.map ?? box.view?.map`).\n *\n * Source-only in the sense the view shard is not: `compileMaps` reads the\n * positions for the bundle's `maps` block, under `export.map`. */\nexport interface MapShard {\n schema: typeof MAP_SCHEMA;\n map: BoxMap;\n}\n\n/** A coverage input driver: during a coverage run the harness feeds a\n * host-seam property (`@world.x`) values from `values`, so content gated on\n * external state gets exercised (Patter's coverageDrivers, carried whole). */\nexport interface CoverageDriver {\n /** \"initial\": set once as each playthrough starts. \"recurring\": re-rolled\n * per turn at the cadence, so one run passes through several states. */\n kind: \"initial\" | \"recurring\";\n /** For recurring drivers: how often to re-roll per turn (default \"sometimes\"). */\n cadence?: \"rarely\" | \"sometimes\" | \"often\";\n /** The pool the harness picks from (uniform). Empty = inert. */\n values: ScalarValue[];\n}\n\n/** Authoring-side coverage configuration (never compiled into the bundle). */\nexport interface CoverageConfig {\n /** Property drivers, keyed by ref (\"@world.danger\"). */\n drivers?: Record<string, CoverageDriver>;\n\n}\n\nexport interface ProjectShard {\n schema: typeof PROJECT_SCHEMA;\n project: {\n id: string;\n name: string;\n version: string;\n };\n settings: ProjectSettings;\n /** Coverage drivers + argument domains (authoring/testing config; stays\n * out of the compiled bundle). */\n coverage?: CoverageConfig;\n /** Validation switches (authoring config; never compiled). Off is written\n * as ABSENT, like `export.map`: a shard says what an author chose. */\n validation?: {\n /** Also warn when state is WRITTEN but nothing reads it. Off by default:\n * cards are routinely written ahead of the content that will read them,\n * so mid-development this warning is mostly noise. The read side (a gate\n * on state nothing writes) always warns, because that kills cards now. */\n warnUnreadWrites?: boolean;\n };\n world: {\n properties: PropertyDecl[];\n registry?: unknown;\n };\n story: {\n properties: PropertyDecl[];\n };\n /** Templates of play: configuration bags keyed by template name. Core\n * validates only what it knows. */\n templates: Record<string, unknown>;\n export: {\n bundle: string;\n metadata: \"full\" | \"stripped\";\n /**\n * Does a `.storyletpack` carry the boxes' binary assets (background images)?\n *\n * Default false, and a project-level DEFAULT rather than a rule: a pack is a\n * delivery, so the caller can override it per pack (2026-08-07). Some\n * projects would benefit from sending their pictures in certain\n * circumstances and others never would, which is why neither \"always\" nor\n * \"never\" is the answer.\n *\n * Nothing to do with the compiled bundle, which has its own switch: `map`.\n */\n packAssets?: boolean;\n /**\n * Does the compiled bundle carry the maps (zone shapes and background\n * pictures)?\n *\n * Default false, and the default matters: geometry is authoring data, the\n * runtime deals in tag names, and a shipping build should carry nothing it\n * does not use. But a host that wants an in-game map should not have to\n * invent its own export, and it is most useful early - a prototype with a\n * real map beats a prototype with a list of zone names.\n *\n * It sits beside `metadata` on purpose: that is already the switch for\n * \"authoring data that may or may not ship\", and this is its sibling rather\n * than a new concept. With it on, `export` also writes the background files\n * next to the bundle, and `describeBundle` says what is in there.\n */\n map?: boolean;\n };\n}\n\nexport interface BoxShard {\n schema: typeof BOX_SCHEMA;\n box: {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order among boxes (sparse; absent falls back to the\n * folder-name position). Authoring-only, like a card's (never compiled\n * into the bundle); merges as a per-field value. */\n order?: number;\n ranking: { specificity: boolean };\n /** Declares a timed box (see `Box.turn`); compiled through unchanged. */\n turn?: { seconds: number };\n fields: FieldDecl[];\n /** The outcome fields (see `Box.outcomeFields`); a shard without the key\n * declares none. */\n outcomeFields?: FieldDecl[];\n properties: PropertyDecl[];\n };\n}\n\n/** The box's tag groups: how its cards are filed. */\nexport interface TagsShard {\n schema: typeof TAGS_SCHEMA;\n groups: TagGroup[];\n}\n\n/** The box's hand templates + hands (the writer/programmer contract). */\nexport interface HandsShard {\n schema: typeof HANDS_SCHEMA;\n templates: HandTemplate<string>[];\n hands: Hand<string>[];\n}\n\nexport interface DeckShard {\n schema: typeof DECK_SCHEMA;\n deck: {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n condition?: string;\n /** Scarce across flows: see Deck.shared. */\n shared?: boolean;\n /** Its `redraw: \"never\"` cards are spent past the run: see Deck.durable. */\n durable?: boolean;\n /** Authored display order within the box (sparse; see BoxShard). */\n order?: number;\n properties: PropertyDecl[];\n };\n cards: Card<string>[];\n}\n\n// --- templates of play --------------------------------------------------------\n// The spatial template's types, field access and geometry. Re-exported here so the\n// package has one entry point, and kept in its own module because core schema and\n// a template of play are different things (Reboot 6).\nexport * from \"./spatial.js\";\n\n// How a hand reaches a tag group, and whether that binding is the hand's own to\n// change. Core schema rather than a template of play, but the map is what needed\n// it said out loud.\nexport * from \"./hands.js\";\n\n// Frames: what an author draws around the content. Arrangement,\n// so it lives in the sidecar and reads forgivingly (furniture.ts says why).\nexport * from \"./furniture.js\";\n\n// Threaded comments: the conversation about a thing, in its own sidecar.\nexport * from \"./comments.js\";\n\n// Guessing a property's type from what an outcome writes: the quick fix's input.\nexport * from \"./infer.js\";\n","// ---------------------------------------------------------------------------\n// Save-file plumbing over the .storyletsave file (storylets/savefile@1): the\n// HOST's file - the engine's envelope (storylets/save@1, shared partitions +\n// every flow) plus, when the host keeps one, its @world container. That is\n// \"host saves its container once, each engine saves its own envelope\"\n// (design/flows.md) folded into one file for the single-host case. These\n// helpers are the string boundary - a foreign or malformed blob throws\n// rather than corrupting a run.\n// ---------------------------------------------------------------------------\n\nimport { SAVEFILE_SCHEMA, SAVE_SCHEMA } from \"@storylet-studio/model\";\nimport type { PropertyBag, SaveFile } from \"@storylet-studio/model\";\nimport type { Engine } from \"@storylet-studio/runtime\";\n\n/** The current engine state (and the host's @world values, if given) as\n * pretty-printed .storyletsave JSON. */\nexport function serializeState(engine: Engine, world?: PropertyBag): string {\n return JSON.stringify(saveState(engine, world), null, 2);\n}\n\n/**\n * Capture the whole engine (and the host's @world values, if it keeps any) as\n * the tagged save-file OBJECT.\n *\n * Four verbs, in Patterplay's pairing (`patter` play-helpers `save.ts`, and\n * the same in all four of its runtimes): saveState / loadState work on the\n * PARSED object, serializeState / deserializeState work on TEXT.\n *\n * This reference had a different shape until 2026-08-29 - `deserializeState`\n * parsed and did not restore, `loadState` took text - so one name meant two\n * things across the four Storylets runtimes, and neither matched the family.\n * Godot and Unreal already had Patter's shape; these two were brought to it.\n */\nexport function saveState(engine: Engine, world?: PropertyBag): SaveFile {\n return {\n schema: SAVEFILE_SCHEMA,\n engine: engine.saveGame(),\n ...(world !== undefined ? { world } : {}),\n };\n}\n\n/** Restore a {@link saveState} file into an engine. EVERY FLOW IS REBUILT, so\n * the Flow handles you held before are inert: re-take them with\n * `engine.getFlow(id)`, NOT `engine.openFlow(id)`. `openFlow` on an existing\n * id REPLACES it, which here throws away the hand the file just restored, and\n * the failure lands later, as `play()` refusing a card as \"not dealt\". (The\n * engine's `onReplacedFlow` hook reports exactly this.) Throws on a foreign or malformed\n * file, and the runtime's own project check still applies. Returns the file's\n * @world values, if any - the HOST applies them to its container; the engine\n * never touches them. */\nexport function loadState(engine: Engine, file: SaveFile): PropertyBag | undefined {\n if (!file || typeof file !== \"object\"\n || file.schema !== SAVEFILE_SCHEMA || file.engine?.schema !== SAVE_SCHEMA) {\n throw new Error(`not a storylets save (expected schema \"${SAVEFILE_SCHEMA}\")`);\n }\n engine.loadGame(file.engine);\n return file.world;\n}\n\n/** Parse + restore a {@link serializeState} string: the TEXT twin of\n * loadState, as Patterplay pairs them. Throws on malformed JSON, a foreign\n * file or a project mismatch. Returns the file's @world values for the host. */\nexport function deserializeState(engine: Engine, json: string): PropertyBag | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(\"not valid JSON\");\n }\n return loadState(engine, parsed as SaveFile);\n}\n","// ---------------------------------------------------------------------------\n// The property examiner/editor, JS idiom: a self-styled DOM panel (the\n// parity member Unity renders as an EditorWindow, Unreal as a Slate tab,\n// Godot as an in-game panel). Rows come from live().listProperties() and\n// are built once (declared properties are fixed for a bundle); values\n// refresh on a poll that SKIPS the focused widget; every row has a\n// reset-to-default that disables itself at the default. Edits commit via\n// flow.setProperty, which is a silent host write under the firing rule.\n// Save state / Load state carry the whole run over the .storyletsave string\n// boundary (save.ts); a filter narrows the property rows; the read-only\n// turns and board sections mirror the engine examiners (design 2.4).\n// The JS game runs in-process, so the engine and a flow are passed directly\n// (no debug registry needed here).\n//\n// The log panel (design 2.3: the flow's retained log surfaced in every\n// examiner; the old port's Unreal log panel is the high-water mark): the\n// lines of live().log() behind per-kind filters (a peek files under Deal -\n// both are asks), with Autoscroll, Copy and Clear. Empty until the engine\n// is created with the log option.\n// ---------------------------------------------------------------------------\n/// <reference lib=\"dom\" />\n\nimport type { Engine, EngineLogEntry, Flow, LogEntry, PropertyRow } from \"@storylet-studio/runtime\";\nimport type { ScalarValue } from \"@storylet-studio/model\";\nimport { serializeState, deserializeState } from \"./save.js\";\n\nexport interface PropertyInspectorOptions {\n /** Mount point; defaults to document.body. */\n container?: HTMLElement;\n title?: string;\n /** Value-refresh poll; 0 disables polling. */\n pollMs?: number;\n}\n\nexport interface PropertyInspector {\n el: HTMLElement;\n refresh(): void;\n destroy(): void;\n}\n\nconst STYLE_ID = \"sl-inspector-style\";\nconst CSS = `\n.sl-insp { font: 12px system-ui, sans-serif; color: var(--ink, #222); background: var(--surface, #fafafa);\n border: 1px solid var(--line, #ccc); border-radius: 8px; padding: 10px 12px; max-width: 26rem; }\n.sl-insp h3 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--muted, #666); }\n.sl-insp .sl-head { display: flex; align-items: baseline; gap: 6px; }\n.sl-insp .sl-head h3 { flex: 1; }\n.sl-insp .sl-save, .sl-insp .sl-load { font: inherit; font-size: 11px; padding: 1px 6px; cursor: pointer; }\n.sl-insp .sl-filter { display: block; width: 100%; box-sizing: border-box; margin: 0 0 6px; }\n.sl-insp .sl-group { margin: 8px 0 2px; font-weight: 600; font-size: 11px; color: var(--muted, #666); }\n.sl-insp .sl-section { margin: 10px 0 2px; font-weight: 600; font-size: 11px; text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--muted, #666); }\n.sl-insp .sl-line { padding: 1px 0; }\n.sl-insp .sl-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; }\n.sl-insp .sl-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.sl-insp input[type=\"text\"], .sl-insp input[type=\"number\"], .sl-insp select {\n font: inherit; width: 9rem; padding: 1px 4px; }\n.sl-insp .sl-reset { border: 0; background: none; cursor: pointer; color: var(--muted, #666); }\n.sl-insp .sl-reset:disabled { opacity: 0.3; cursor: default; }\n.sl-insp .sl-logbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 2px 0; }\n.sl-insp .sl-logbar label { display: inline-flex; align-items: center; gap: 2px; }\n.sl-insp .sl-logbar button { font: inherit; font-size: 11px; padding: 1px 6px; cursor: pointer; }\n.sl-insp .sl-log { font-family: ui-monospace, monospace; font-size: 11px; max-height: 12rem;\n overflow: auto; white-space: pre; border: 1px solid var(--line, #ccc); padding: 4px 6px; }\n.sl-insp details.sl-fold > summary { cursor: pointer; margin: 10px 0 2px; font-weight: 600;\n font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted, #666); }\n.sl-insp .sl-ident { font-family: ui-monospace, monospace; font-size: 11px; }\n.sl-insp .sl-ident b { font-weight: 600; }\n.sl-insp .sl-note { color: var(--muted, #666); }\n`;\n\n/** Inject the shared panel stylesheet once. Exported so the bundle inspector\n * (bundle-inspector.ts) renders in the same CSS grammar. */\nexport function ensureInspectorStyle(): void {\n if (document.getElementById(STYLE_ID)) return;\n const style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = CSS;\n document.head.append(style);\n}\n\nconst eq = (a: ScalarValue | undefined, b: ScalarValue | undefined): boolean =>\n JSON.stringify(a) === JSON.stringify(b);\n\n// --- the log panel (design 2.3) ---------------------------------------------\n\n/** The filterable kinds; a peek files under \"deal\" (both are asks). */\nconst LOG_KINDS = [\"deal\", \"play\", \"write\", \"evict\", \"turns\", \"diagnostic\"] as const;\nconst LOG_KIND_LABELS: Record<(typeof LOG_KINDS)[number], string> = {\n deal: \"Deal\", play: \"Play\", write: \"Write\", evict: \"Evict\", turns: \"Turns\", diagnostic: \"Diag\",\n};\n\nconst logKindOf = (e: LogEntry): (typeof LOG_KINDS)[number] =>\n e.type === \"peek\" ? \"deal\" : e.type;\n\nconst showVal = (v: ScalarValue | undefined): string =>\n v === undefined ? \"<unset>\" : JSON.stringify(v);\n\n/** One line per entry, `[turn]`-stamped where the event has a box context\n * (write lines share the state logger's `path: from -> to` reading). */\nexport function formatLogEntry(e: LogEntry | EngineLogEntry): string {\n // The run's log names the flow that acted, after the turn stamp and in the\n // same place all four examiners put it; a flow's own log omits it, because\n // its section heading already says whose it is.\n return formatLogBody(e, \"flow\" in e && e.flow ? `${e.flow} ` : \"\");\n}\n\nfunction formatLogBody(e: LogEntry, flow: string): string {\n const stamp = (e.turn !== undefined ? `[${e.turn}] ` : \"[-] \") + flow;\n switch (e.type) {\n case \"deal\": {\n const dealt = e.cards.filter((c) => c.verdict === \"dealt\").map((c) => c.id);\n return `${stamp}deal ${e.hand}: ${dealt.length > 0 ? dealt.join(\", \") : \"(none)\"} (${e.cards.length} considered)`;\n }\n case \"peek\": {\n const crit = Object.entries(e.criteria).map(([g, t]) => `${g}=${t}`).join(\", \");\n const listed = e.cards.filter((c) => c.verdict === \"dealt\").map((c) => c.id);\n return `${stamp}peek ${e.box}${crit ? ` [${crit}]` : \"\"}: `\n + `${listed.length > 0 ? listed.join(\", \") : \"(none)\"} (${e.cards.length} considered)`;\n }\n case \"evict\": return `${stamp}evict ${e.card} from ${e.hand} (${e.reason})`;\n // A card played with none (\"\") has no outcome to name.\n case \"play\": return `${stamp}play ${e.card}${e.outcome === \"\" ? \"\" : ` -> ${e.outcome}`}`;\n case \"write\": return `${stamp}write ${e.path}: ${showVal(e.prev)} -> ${showVal(e.value)}`;\n case \"turns\": return `${stamp}turns ${e.box} -> ${e.turn}`;\n default: return `${stamp}diagnostic ${e.where}: ${e.message}`;\n }\n}\n\n/** The group label a row files under (\"world\", \"story\", \"box <id>\", ...). */\nconst groupOf = (path: string): string => {\n const parts = path.split(\".\");\n return parts.length === 3 ? `${parts[0]} ${parts[1]}` : parts[0]!;\n};\n\nexport function createPropertyInspector(engine: Engine, flow: Flow, opts: PropertyInspectorOptions = {}): PropertyInspector {\n ensureInspectorStyle();\n\n // loadGame rebuilds every flow and the handle we were given goes inert\n // (the runtime's stale-handle rule), so every read goes through this\n // accessor and Load state re-takes the same-named flow.\n let liveFlow = flow;\n const live = (): Flow => liveFlow;\n\n const el = document.createElement(\"div\");\n el.className = \"sl-insp\";\n\n // Header: the title plus Save state / Load state (the .storyletsave\n // string boundary, in every examiner - the parity rule, design 2.4).\n const head = document.createElement(\"div\");\n head.className = \"sl-head\";\n const h = document.createElement(\"h3\");\n h.textContent = opts.title ?? \"Runtime state\";\n head.append(h);\n\n const saveBtn = document.createElement(\"button\");\n saveBtn.type = \"button\";\n saveBtn.className = \"sl-save\";\n saveBtn.textContent = \"Save state\";\n saveBtn.addEventListener(\"click\", () => {\n const blob = new Blob([serializeState(engine)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = \"save.storyletsave\";\n a.click();\n URL.revokeObjectURL(url);\n });\n head.append(saveBtn);\n\n const filePicker = document.createElement(\"input\");\n filePicker.type = \"file\";\n filePicker.accept = \".storyletsave,application/json\";\n filePicker.hidden = true;\n filePicker.addEventListener(\"change\", () => {\n const file = filePicker.files?.[0];\n if (!file) return;\n const reader = new FileReader();\n reader.onload = () => {\n // A foreign or malformed blob is refused by deserializeState, never applied.\n try {\n deserializeState(engine, String(reader.result));\n liveFlow = engine.getFlow(flow.id) ?? engine.openFlow(flow.id);\n refresh();\n } catch (e) {\n console.error(\"storylets inspector: load failed:\", e instanceof Error ? e.message : e);\n }\n filePicker.value = \"\";\n };\n reader.readAsText(file);\n });\n\n const loadBtn = document.createElement(\"button\");\n loadBtn.type = \"button\";\n loadBtn.className = \"sl-load\";\n loadBtn.textContent = \"Load state\";\n loadBtn.addEventListener(\"click\", () => filePicker.click());\n head.append(loadBtn, filePicker);\n el.append(head);\n\n // The property filter (name/path substring, case-blind - the parity\n // member Unreal renders as an SSearchBox).\n const filter = document.createElement(\"input\");\n filter.type = \"text\";\n filter.className = \"sl-filter\";\n filter.placeholder = \"Filter properties\";\n el.append(filter);\n\n // Rows are built once: the declared surface is fixed for a bundle. The\n // filter only toggles visibility (a group hides with its last row).\n const editors: { row: PropertyRow; read: () => void }[] = [];\n const groups: { el: HTMLElement; rows: { el: HTMLElement; text: string }[] }[] = [];\n let lastGroup = \"\";\n for (const row of live().listProperties()) {\n const group = groupOf(row.path);\n if (group !== lastGroup) {\n const g = document.createElement(\"div\");\n g.className = \"sl-group\";\n g.textContent = group;\n el.append(g);\n groups.push({ el: g, rows: [] });\n lastGroup = group;\n }\n const rowEl = buildRow(live, row, editors);\n groups[groups.length - 1]!.rows.push({ el: rowEl, text: `${row.name} ${row.path}`.toLowerCase() });\n el.append(rowEl);\n }\n\n filter.addEventListener(\"input\", () => {\n const q = filter.value.trim().toLowerCase();\n for (const group of groups) {\n let any = false;\n for (const row of group.rows) {\n const show = q === \"\" || row.text.includes(q);\n row.el.style.display = show ? \"\" : \"none\";\n any = any || show;\n }\n group.el.style.display = any ? \"\" : \"none\";\n }\n });\n\n // Read-only: each box's clock, then the board's current hands (title or\n // gameId, never internal ids) - the same sections as the engine examiners.\n const turnsHead = document.createElement(\"div\");\n turnsHead.className = \"sl-section\";\n turnsHead.textContent = \"Turns (per box)\";\n const turnsBody = document.createElement(\"div\");\n turnsBody.className = \"sl-turns\";\n const boardHead = document.createElement(\"div\");\n boardHead.className = \"sl-section\";\n boardHead.textContent = \"Board\";\n const boardBody = document.createElement(\"div\");\n boardBody.className = \"sl-board\";\n el.append(turnsHead, turnsBody, boardHead, boardBody);\n\n // A retained log (design 2.3), behind per-kind filters, with Autoscroll,\n // Copy and Clear - the JS rendering of the engine examiners' log panel.\n // Built twice: once for the RUN (every flow's events in one order, each line\n // naming its flow) and once for this flow's own. Both exist because a flow's\n // log cannot show a story action in another flow moving shared state\n // (design/shared-scarcity.md 8.2). Empty until the engine had the log option.\n const check = (text: string, onChange: (on: boolean) => void, cls: string): HTMLLabelElement => {\n const label = document.createElement(\"label\");\n label.className = cls;\n const box = document.createElement(\"input\");\n box.type = \"checkbox\";\n box.checked = true;\n box.addEventListener(\"change\", () => onChange(box.checked));\n label.append(box, text);\n return label;\n };\n\n interface LogPanel { render: (force?: boolean) => void }\n const buildLogPanel = (\n which: \"flow\" | \"run\",\n caption: string,\n entriesOf: () => readonly (LogEntry | EngineLogEntry)[],\n clear: () => void,\n empty: string,\n ): LogPanel => {\n // Both panels carry the same controls, so each element takes a `sl-flow` /\n // `sl-run` modifier: without one, a selector for \"the Clear button\" is\n // ambiguous, which is exactly what the inspector test caught.\n const head = document.createElement(\"div\");\n head.className = `sl-section sl-${which}`;\n head.textContent = caption;\n const bar = document.createElement(\"div\");\n bar.className = `sl-logbar sl-${which}`;\n const body = document.createElement(\"div\");\n body.className = `sl-log sl-${which}`;\n\n const kindOn = new Map<string, boolean>();\n const visibleLines = (): string[] =>\n entriesOf().filter((e) => kindOn.get(logKindOf(e)) !== false).map(formatLogEntry);\n let autoscroll = true;\n let stampSeen = \"\";\n const render = (force = false): void => {\n const entries = entriesOf();\n const stamp = `${entries.length}:${entries.length > 0 ? entries[entries.length - 1]!.seq : -1}`;\n if (!force && stamp === stampSeen) return;\n stampSeen = stamp;\n const lines = visibleLines();\n body.textContent = lines.length > 0 ? lines.join(\"\\n\") : empty;\n if (autoscroll) body.scrollTop = body.scrollHeight;\n };\n\n for (const kind of LOG_KINDS) {\n kindOn.set(kind, true);\n bar.append(check(LOG_KIND_LABELS[kind], (on) => {\n kindOn.set(kind, on);\n render(true);\n }, \"sl-logkind\"));\n }\n bar.append(check(\"Autoscroll\", (on) => { autoscroll = on; }, \"sl-logscroll\"));\n const copyBtn = document.createElement(\"button\");\n copyBtn.type = \"button\";\n copyBtn.className = \"sl-logcopy\";\n copyBtn.textContent = \"Copy\";\n copyBtn.title = \"Copy the visible (filtered) log to the clipboard\";\n copyBtn.addEventListener(\"click\", () => { void navigator.clipboard?.writeText(visibleLines().join(\"\\n\")); });\n const clearBtn = document.createElement(\"button\");\n clearBtn.type = \"button\";\n clearBtn.className = \"sl-logclear\";\n clearBtn.textContent = \"Clear\";\n clearBtn.title = \"Drop the retained log entries (cosmetic - no game state changes)\";\n clearBtn.addEventListener(\"click\", () => { clear(); render(true); });\n bar.append(copyBtn, clearBtn);\n el.append(head, bar, body);\n return { render };\n };\n\n // This flow's own log first: it is what the panel was mounted on. The run's\n // log follows, because it is the wider view and it only earns its space once\n // a second flow exists.\n const flowLog = buildLogPanel(\"flow\", \"Log\", () => live().log(), () => live().clearLog(),\n \"(empty - new Engine(bundle, { log: true }) retains the flow log)\");\n const runLog = buildLogPanel(\"run\", \"Run log (every flow)\", () => engine.log(), () => engine.clearLog(),\n \"(empty - new Engine(bundle, { log: true }) retains the run log)\");\n const renderLog = (force = false): void => { runLog.render(force); flowLog.render(force); };\n\n const line = (parent: HTMLElement, text: string): void => {\n const div = document.createElement(\"div\");\n div.className = \"sl-line\";\n div.textContent = text;\n parent.append(div);\n };\n const readLive = (): void => {\n turnsBody.textContent = \"\";\n for (const box of live().listBoxes()) {\n line(turnsBody, `${box.title ?? box.gameId}: turn ${box.turn}`);\n }\n boardBody.textContent = \"\";\n for (const [hand, cards] of Object.entries(live().board())) {\n const names = cards.map((c) => c.title ?? c.gameId);\n line(boardBody, `${hand}: ${names.length > 0 ? names.join(\", \") : \"(empty)\"}`);\n }\n };\n\n const refresh = (): void => {\n for (const e of editors) e.read();\n readLive();\n renderLog();\n };\n readLive();\n renderLog(true);\n\n let timer: ReturnType<typeof setInterval> | undefined;\n const pollMs = opts.pollMs ?? 250;\n if (pollMs > 0) timer = setInterval(refresh, pollMs);\n\n (opts.container ?? document.body).append(el);\n return {\n el,\n refresh,\n destroy(): void {\n if (timer !== undefined) clearInterval(timer);\n el.remove();\n },\n };\n}\n\nfunction buildRow(\n live: () => Flow,\n row: PropertyRow,\n editors: { row: PropertyRow; read: () => void }[],\n): HTMLElement {\n const div = document.createElement(\"div\");\n div.className = \"sl-row\";\n const name = document.createElement(\"span\");\n name.className = \"sl-name\";\n name.textContent = row.name;\n name.title = row.path;\n\n const current = (): ScalarValue | undefined => {\n try { return live().getProperty(row.path); } catch { return undefined; }\n };\n // Forward-declared so commit can refresh the whole row (widget + reset\n // state) once everything below is built.\n let sync: () => void = () => {};\n const commit = (value: ScalarValue): void => { live().setProperty(row.path, value); sync(); };\n\n const reset = document.createElement(\"button\");\n reset.type = \"button\";\n reset.className = \"sl-reset\";\n reset.textContent = \"↺\";\n reset.title = \"Reset to default\";\n reset.addEventListener(\"click\", () => commit(row.default));\n\n let widget: HTMLElement;\n let read: () => void;\n const focused = (w: HTMLElement): boolean => document.activeElement === w;\n\n switch (row.type) {\n case \"boolean\": {\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.addEventListener(\"change\", () => commit(input.checked));\n widget = input;\n read = () => { if (!focused(input)) input.checked = current() === true; };\n break;\n }\n case \"number\": {\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.addEventListener(\"change\", () => commit(Number(input.value)));\n widget = input;\n read = () => { if (!focused(input)) input.value = String(current() ?? 0); };\n break;\n }\n case \"enum\":\n case \"quality\": {\n // A quality edits as a dropdown of its STAGE LADDER, closed exactly like an\n // enum's values. It fell to the string branch until 2026-09-01, so a free-text\n // box accepted any stage name at all - and an unknown stage is not a harmless\n // typo: the evaluator refuses it (\"X is not a stage of this quality\"), so a\n // slip here broke play rather than being corrected. listProperties has carried\n // `stages` for this since it was written; nothing consumed it.\n const select = document.createElement(\"select\");\n for (const v of (row.type === \"quality\" ? row.stages : row.values) ?? []) {\n const o = document.createElement(\"option\");\n o.value = v;\n o.textContent = v;\n select.append(o);\n }\n select.addEventListener(\"change\", () => commit(select.value));\n widget = select;\n read = () => { if (!focused(select)) select.value = String(current() ?? \"\"); };\n break;\n }\n case \"flags\": {\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.placeholder = \"comma, separated, flags\";\n input.addEventListener(\"change\", () =>\n commit(input.value.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0)));\n widget = input;\n read = () => { if (!focused(input)) input.value = ((current() as string[] | undefined) ?? []).join(\", \"); };\n break;\n }\n default: { // string\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.addEventListener(\"change\", () => commit(input.value));\n widget = input;\n read = () => { if (!focused(input)) input.value = String(current() ?? \"\"); };\n }\n }\n\n const readAll = (): void => { read(); reset.disabled = eq(current(), row.default); };\n sync = readAll;\n readAll();\n editors.push({ row, read: readAll });\n div.append(name, widget, reset);\n return div;\n}\n","// ---------------------------------------------------------------------------\n// The bundle inspector, JS idiom (design/engine-runtimes.md 2, piece 6).\n//\n// describeBundle() IS the JS half of the parity member - JS has no engine\n// asset pipeline to hang an editor view off - so this is the optional DOM\n// rendering of it: a read-only panel over a compiled bundle, in the property\n// examiner's CSS grammar (inspector.ts), with NO session anywhere. Identity\n// first, then collapsible sections: hands (the deal() surface), tags by box\n// (the peek() criteria surface), declared properties, counts.\n//\n// Read-only by construction: there is no state to edit here, only the shape\n// that shipped.\n// ---------------------------------------------------------------------------\n/// <reference lib=\"dom\" />\n\nimport { describeBundle } from \"@storylet-studio/runtime\";\nimport type {\n BundleDescription, PropertyScopeSummary, PropertySummary,\n} from \"@storylet-studio/runtime\";\nimport type { Bundle, ScalarValue } from \"@storylet-studio/model\";\nimport { ensureInspectorStyle } from \"./inspector.js\";\n\nexport interface BundleInspectorOptions {\n /** Mount point; defaults to document.body. */\n container?: HTMLElement;\n title?: string;\n /** Start the collapsible sections open (default true). */\n open?: boolean;\n}\n\nexport interface BundleInspector {\n el: HTMLElement;\n /** The description this panel rendered (the API is the parity member). */\n description: BundleDescription;\n destroy(): void;\n}\n\nconst showVal = (v: ScalarValue | undefined): string =>\n v === undefined ? \"<unset>\" : JSON.stringify(v);\n\n/** \"name: type = default\", plus enum/flags options where declared, plus\n * \"(durable)\" where the declaration says the value outlives a run\n * (design/engine-server.md 4.2). Nothing is added for the ordinary\n * run-scoped property: that is what a property is. */\nexport function formatPropertySummary(p: PropertySummary): string {\n const options = p.values !== undefined && p.values.length > 0 ? ` [${p.values.join(\", \")}]` : \"\";\n const durable = p.durable === true ? \" (durable)\" : \"\";\n return `${p.name}: ${p.type} = ${showVal(p.default)}${options}${durable}`;\n}\n\n/** The scope label a declaration block files under (\"world\", \"box box\",\n * \"tag docks (zone)\"). */\nexport function formatScopeLabel(scope: PropertyScopeSummary): string {\n if (scope.scope === \"world\" || scope.scope === \"story\") return scope.scope;\n const group = scope.group !== undefined ? ` (${scope.group})` : \"\";\n return `${scope.scope} ${scope.owner}${group}`;\n}\n\nconst line = (parent: HTMLElement, text: string, cls = \"sl-line\"): HTMLElement => {\n const div = document.createElement(\"div\");\n div.className = cls;\n div.textContent = text;\n parent.append(div);\n return div;\n};\n\n/** A collapsible section in the examiner's grammar. */\nconst fold = (parent: HTMLElement, label: string, open: boolean): HTMLElement => {\n const details = document.createElement(\"details\");\n details.className = \"sl-fold\";\n details.open = open;\n const summary = document.createElement(\"summary\");\n summary.textContent = label;\n details.append(summary);\n const body = document.createElement(\"div\");\n details.append(body);\n parent.append(details);\n return body;\n};\n\n/** Render a read-only summary of a compiled bundle: what an integrator may\n * call, with no session and no game running. */\nexport function createBundleInspector(\n bundle: Bundle,\n opts: BundleInspectorOptions = {},\n): BundleInspector {\n ensureInspectorStyle();\n const description = describeBundle(bundle);\n const open = opts.open ?? true;\n\n const el = document.createElement(\"div\");\n el.className = \"sl-insp\";\n\n const head = document.createElement(\"div\");\n head.className = \"sl-head\";\n const h = document.createElement(\"h3\");\n h.textContent = opts.title ?? \"Bundle\";\n head.append(h);\n el.append(head);\n\n // --- identity (always visible: which bundle is this?) --------------------\n const { identity, totals } = description;\n const ident = document.createElement(\"div\");\n ident.className = \"sl-ident\";\n el.append(ident);\n line(ident, `${identity.project} ${identity.version}`, \"sl-line\");\n line(ident, `schema ${identity.schema}`, \"sl-line sl-note\");\n line(ident, `hash ${identity.hash === \"\" ? \"(none)\" : identity.hash} - metadata ${identity.metadata}`,\n \"sl-line sl-note\");\n\n // --- hands: the deal() surface -------------------------------------------\n const handsBody = fold(el, \"Hands (deal)\", open);\n handsBody.className = \"sl-hands\";\n if (description.hands.length === 0) {\n line(handsBody, \"(no hands - this bundle is peek-only)\", \"sl-line sl-note\");\n }\n for (const hand of description.hands) {\n const template = hand.template !== undefined ? `, template ${hand.template}` : \"\";\n // A movable hole is the one thing about a hand its name cannot say: write\n // that property and the hand moves (4.6).\n const moves = hand.movable === undefined ? \"\"\n : `, moves ${hand.movable.map((m) => `${m.group} from ${m.from}`).join(\" and \")}`;\n line(handsBody, `${hand.gameId}: box ${hand.box}, slots ${hand.slots}${template}${moves}`\n + (hand.title !== undefined ? ` - ${hand.title}` : \"\"));\n }\n\n // --- tag groups by box: the peek() criteria surface ----------------------\n const tagsBody = fold(el, \"Tags by box (peek criteria)\", open);\n tagsBody.className = \"sl-tags\";\n for (const box of description.boxes) {\n line(tagsBody, `${box.title ?? box.gameId}`, \"sl-group\");\n if (box.tagGroups.length === 0) {\n line(tagsBody, \" (no tag groups)\", \"sl-line sl-note\");\n }\n for (const group of box.tagGroups) {\n line(tagsBody, ` ${group.gameId}: ${group.tags.length > 0 ? group.tags.join(\", \") : \"(no tags)\"}`);\n }\n }\n\n // --- declared properties: what expressions read, what a host may set ----\n const propsBody = fold(el, \"Properties (declared)\", open);\n propsBody.className = \"sl-props\";\n for (const scope of description.properties) {\n line(propsBody, formatScopeLabel(scope), \"sl-group\");\n if (scope.properties.length === 0) {\n line(propsBody, \" (none declared)\", \"sl-line sl-note\");\n }\n for (const p of scope.properties) {\n line(propsBody, ` ${formatPropertySummary(p)}`);\n }\n }\n\n // --- maps: inert payload, and therefore worth saying out loud -----------\n //\n // Only when there ARE some. An empty section on every ordinary bundle would\n // teach the reader to skip a section that only ever matters when it is not\n // empty, and most bundles carry no geometry at all.\n if (description.maps.length > 0) {\n const mapsBody = fold(el, \"Maps (carried, not read)\", open);\n mapsBody.className = \"sl-maps\";\n line(mapsBody, \"Geometry the build was asked to carry. The engine ignores it.\", \"sl-line sl-note\");\n for (const map of description.maps) {\n line(mapsBody, `${map.box} - ${map.group}: zones ${map.zones}, pictures ${map.backgrounds}, sites ${map.sites}`);\n }\n }\n\n // --- counts: orientation, not inventory ---------------------------------\n const countsBody = fold(el, \"Counts\", open);\n countsBody.className = \"sl-counts\";\n line(countsBody, `boxes ${totals.boxes} - decks ${totals.decks} - cards ${totals.cards}`);\n line(countsBody, `hands ${totals.hands} - templates ${totals.templates} - tag groups ${totals.tagGroups}`);\n for (const box of description.boxes) {\n // A timed box says its unit here (design/engine-server.md 4.8), because\n // this is the line an integrator reads to find out what their host has to\n // tick. Nothing is added for an ordinary box: the answer \"a turn is a\n // play\" belongs in the docs, not on every line of every bundle.\n line(countsBody, `${box.gameId}: decks ${box.counts.decks}, cards ${box.counts.cards}, `\n + `hands ${box.counts.hands}, templates ${box.counts.templates}, `\n + `tag groups ${box.counts.tagGroups}, ranking.specificity ${box.ranking.specificity}`\n + (box.turn !== undefined ? `, turn = ${box.turn.seconds}s` : \"\")\n // Only when there are any: a box whose cards all come back with the run\n // has nothing for a server to lift, and the zero would be noise.\n + (box.durableCards !== undefined ? `, durable cards ${box.durableCards}` : \"\"));\n }\n\n (opts.container ?? document.body).append(el);\n return {\n el,\n description,\n destroy(): void {\n el.remove();\n },\n };\n}\n","// ---------------------------------------------------------------------------\n// Live Link (design/live-link.md): the game-side client.\n//\n// Joins a running game to Storyletter over a loopback WebSocket. Two things\n// travel on it: the flow's trace stream and board snapshots go UP, so the\n// editor's Board can show the game's run instead of its own (observe-only: the\n// editor never drives the game); freshly compiled bundles come DOWN after a\n// save, so the run picks up the edit without restarting (applyLiveBundle in\n// refresh.ts does the swap).\n//\n// Wire protocol `storyletengine/debug@1` (one JSON object per message):\n// hello : { t:\"hello\", v:2, build, project?, boxes?, flows:[id...] }\n// - on open, and again on setBuild\n// flowOpen / flowClose : { t:\"flowOpen\"|\"flowClose\", flow }\n// - a flow appeared or went\n// trace : { t:\"trace\", flow, event } - every TraceEvent any flow emits\n// board : { t:\"board\", flow, hands:{ hand: [card...] }, turns:{ box: n } }\n// - after hello, and after every deal /\n// play / evict / turns event\n// bundle: { t:\"bundle\", v:1, build, data } - EDITOR -> game: the full .storyletsc\n// JSON as a string\n// Identity in frames is by gameId (hands, boxes, cards), and since 4.4 that\n// holds for the trace event too: it is forwarded verbatim, and the runtime's\n// own ids are gameIds now, so the rule has no exception left.\n//\n// Patterpad's createDebugLink is the template (Patter play-helpers/debug.ts):\n// hello first, frames queue until the socket opens, a missing editor is a\n// silent no-op, nothing here ever throws into the game, and no WebSocket\n// implementation at all degrades to a no-op handle. `observe(...)` became a\n// trace subscription, which is why this one takes an ENGINE: attach(engine)\n// subscribes, detach() stops, and a live refresh replaces the flow\n// (detach the old one, attach the new one, then setBuild).\n//\n// const link = createLiveLink({ build: bundle.content.hash, onBundle: ... });\n// link.attach(engine); // the ENGINE: the link discovers your flows itself\n// ---------------------------------------------------------------------------\n\nimport type { Engine, Flow, TraceEvent } from \"@storylet-studio/runtime\";\n\n/** A minimal structural type for a WebSocket implementation (browsers and\n * Node 22+ have a global one). */\nexport interface LiveSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n addEventListener(type: \"open\" | \"close\" | \"error\", listener: () => void): void;\n /** Incoming editor messages (the pushed bundle). Optional so a bare\n * send-only socket still fits. */\n addEventListener(type: \"message\", listener: (ev: { data: unknown }) => void): void;\n}\ntype LiveSocketCtor = new (url: string) => LiveSocketLike;\n\nexport interface LiveLinkOptions {\n /** The running bundle's build identity: pass `bundle.content.hash`. The\n * editor compares it with its own compiled hash (in sync / stale). */\n build: string;\n /** Optional project name, shown in the editor's connect-chip tooltip. */\n project?: string;\n /** Editor WebSocket URL. Default `ws://127.0.0.1:4472`. */\n url?: string;\n /** A WebSocket constructor to use instead of the global one (tests with a\n * fake socket, or a host without a global WebSocket). */\n WebSocket?: LiveSocketCtor;\n /** Live refresh: the editor pushed a freshly compiled bundle. `data` is\n * the .storyletsc JSON; hand it (with your current Engine) to\n * `applyLiveBundle`, `attach` the engine it returns, then call\n * `link.setBuild(build)`. Never called with a malformed frame. */\n onBundle?: (msg: { build: string; data: string }) => void;\n}\n\nexport interface LiveLink {\n /** Start forwarding this ENGINE's trace: every flow's events, each frame\n * naming the flow it came from, so the editor can follow one participant\n * and switch. An earlier engine is detached first. Sends a board snapshot\n * per open flow straight away, queued behind the hello if the socket is\n * not open yet.\n *\n * Flows are discovered rather than declared: the link diffs `engine.flows()`\n * whenever anything happens and emits `flowOpen` / `flowClose` itself. That\n * is a deliberate departure from Patterplay, whose host calls `FlowOpened`\n * by hand - it has no engine-level trace tap to hang the diff on and we do,\n * so the host has nothing to remember and cannot get the editor's flow list\n * wrong. The one cost: a flow that opens and then does nothing at all is not\n * announced until the next event anywhere in the run. */\n attach(engine: Engine): void;\n /** Stop forwarding. A refresh replaces the engine, so attach the new one\n * afterwards. */\n detach(): void;\n /** After applying a pushed bundle: report the build now running (re-hellos\n * with the new build and a fresh board snapshot, so the editor's chip goes\n * back to in sync and it stops re-pushing the same bundle). */\n setBuild(build: string): void;\n /** Close the link; every later call is a no-op. */\n close(): void;\n}\n\n/** One game-to-editor frame, as the client serialises it. Exported for the\n * fixture test; hosts never build these by hand. */\nexport type LiveFrame =\n | { t: \"hello\"; v: 2; build: string; project?: string; boxes?: string[]; flows: string[] }\n | { t: \"flowOpen\"; flow: string }\n | { t: \"flowClose\"; flow: string }\n | { t: \"trace\"; flow: string; event: TraceEvent }\n | { t: \"board\"; flow: string; hands: Record<string, string[]>; turns: Record<string, number> };\n\nconst OPEN = 1; // WebSocket.OPEN\n\n/** How many frames may wait for a socket that has not opened yet. Generous:\n * the point of queueing is that a game's first moments are not lost while the\n * editor's socket is still connecting. */\nconst QUEUE_CAP = 512;\nconst DEFAULT_URL = \"ws://127.0.0.1:4472\";\n\n/** The trace kinds that move the board, and so are followed by a snapshot. */\nconst BOARD_EVENTS: ReadonlySet<TraceEvent[\"type\"]> = new Set([\"deal\", \"play\", \"evict\", \"turns\"]);\n\n/** The cheap snapshot: hands by gameId holding card gameIds in dealt order,\n * and every box's clock by gameId. */\nexport function boardFrame(flow: Flow): Extract<LiveFrame, { t: \"board\" }> {\n const id = flow.id;\n const hands: Record<string, string[]> = {};\n for (const [hand, cards] of Object.entries(flow.board())) hands[hand] = cards.map((c) => c.gameId);\n const turns: Record<string, number> = {};\n for (const box of flow.listBoxes()) turns[box.gameId] = box.turn;\n return { t: \"board\", flow: id, hands, turns };\n}\n\n/**\n * Open a Live Link to Storyletter. Returns a handle whose calls are no-ops once\n * the editor disconnects or if it was never listening: safe to leave wired into\n * a shipping build behind a flag.\n */\nexport function createLiveLink(opts: LiveLinkOptions): LiveLink {\n const url = opts.url ?? DEFAULT_URL;\n const Ctor: LiveSocketCtor | undefined = opts.WebSocket ?? (globalThis as { WebSocket?: LiveSocketCtor }).WebSocket;\n let queue: string[] = [];\n let sock: LiveSocketLike | null = null;\n let closed = false;\n let build = opts.build; // mutable: setBuild() after a live refresh lands\n let engine: Engine | null = null;\n let unsubscribe: (() => void) | null = null;\n // The flows the EDITOR believes are open. Diffed against engine.flows() so\n // flowOpen / flowClose are the link's own business, not the host's.\n let announced = new Set<string>();\n\n if (!Ctor) {\n // No WebSocket available (no global, none passed): a silent no-op link.\n return { attach() {}, detach() {}, setBuild() {}, close() { closed = true; } };\n }\n\n const flush = (): void => {\n if (!sock || sock.readyState !== OPEN) return;\n for (const m of queue) { try { sock.send(m); } catch { /* socket went away */ } }\n queue = [];\n };\n const post = (frame: LiveFrame): void => {\n if (closed) return;\n queue.push(JSON.stringify(frame));\n // A cap for the CONNECTING window, where queueing is the point: the hello\n // and the frames a game emits during those first milliseconds have to\n // land. Beyond that many, the editor is not coming - drop the oldest, so\n // what survives is the most recent story rather than the first moments of\n // it. Once the socket has actually closed the queue is dropped outright\n // (see the close listener); this is only the never-opened case.\n if (queue.length > QUEUE_CAP) queue.splice(0, queue.length - QUEUE_CAP);\n flush();\n };\n\n // The handshake goes straight to the socket, never through the queue: it\n // must be the first thing the editor reads, ahead of anything queued while\n // the socket was still connecting.\n const liveFlows = (): Flow[] => {\n try { return engine ? engine.flows() : []; } catch { return []; }\n };\n const sendHello = (): void => {\n const flows = liveFlows();\n const hello: LiveFrame = { t: \"hello\", v: 2, build, flows: flows.map((f) => f.id) };\n if (opts.project !== undefined) hello.project = opts.project;\n const first = flows[0];\n if (first) {\n try { hello.boxes = first.listBoxes().map((b) => b.gameId); } catch { /* mid-swap: no boxes */ }\n }\n // The editor's list starts from the hello, so the diff starts there too.\n announced = new Set(flows.map((f) => f.id));\n try { sock?.send(JSON.stringify(hello)); } catch { /* race: closed immediately */ }\n };\n const postBoard = (flow: Flow): void => {\n try { post(boardFrame(flow)); } catch { /* never into the game */ }\n };\n /** Announce anything that opened or closed since the last look. Runs before\n * each forwarded event, so a frame never names a flow the editor has not\n * been told about. */\n const syncFlows = (): void => {\n const now = liveFlows();\n const ids = new Set(now.map((f) => f.id));\n for (const f of now) {\n if (announced.has(f.id)) continue;\n announced.add(f.id);\n post({ t: \"flowOpen\", flow: f.id });\n postBoard(f);\n }\n for (const id of [...announced]) {\n if (ids.has(id)) continue;\n announced.delete(id);\n post({ t: \"flowClose\", flow: id });\n }\n };\n const onTrace = (flowId: string, event: TraceEvent): void => {\n try {\n syncFlows();\n post({ t: \"trace\", flow: flowId, event });\n if (BOARD_EVENTS.has(event.type)) {\n const f = engine?.getFlow(flowId);\n if (f) postBoard(f);\n }\n } catch { /* never into the game */ }\n };\n\n try {\n sock = new Ctor(url);\n sock.addEventListener(\"open\", () => {\n sendHello();\n flush();\n });\n // Live refresh: the editor pushed a new bundle. The shape is checked here\n // so the host's handler never sees a malformed frame; anything else the\n // editor might send is ignored.\n sock.addEventListener(\"message\", (ev: { data: unknown }) => {\n if (!opts.onBundle || typeof ev.data !== \"string\") return;\n try {\n const msg = JSON.parse(ev.data) as Record<string, unknown>;\n if (msg.t === \"bundle\" && typeof msg.build === \"string\" && typeof msg.data === \"string\") {\n opts.onBundle({ build: msg.build, data: msg.data });\n }\n } catch { /* not for us */ }\n });\n sock.addEventListener(\"error\", () => { /* editor not listening: stay a no-op */ });\n // The socket is gone and this link does not reconnect, so the link is\n // INERT from here rather than merely unable to send. It used to set `sock`\n // to null and nothing else, leaving `closed` false, so every later trace\n // event pushed another string onto a queue nothing would ever drain: one\n // heap allocation per deal, play and write for the rest of the session.\n // Godot and Unity both already stopped at this point; JS and Unreal did\n // not. Found by the pre-release audit, 2026-08-29.\n sock.addEventListener(\"close\", () => { sock = null; closed = true; queue = []; });\n } catch { sock = null; closed = true; } // malformed URL etc.: never throw into the game\n\n const detach = (): void => {\n unsubscribe?.();\n unsubscribe = null;\n engine = null;\n announced = new Set();\n };\n\n return {\n attach(next: Engine): void {\n if (closed) return;\n detach();\n engine = next;\n try { unsubscribe = next.subscribeTrace(onTrace); } catch { engine = null; return; }\n // Every open flow's board up front: the editor can show any of them the\n // moment it connects, without waiting for that participant to move.\n announced = new Set(liveFlows().map((f) => f.id));\n for (const f of liveFlows()) postBoard(f);\n },\n detach,\n setBuild(next: string): void {\n if (closed || next === build) return;\n build = next;\n // Re-handshake: the editor re-reads the build, then gets every flow's\n // table as the new engine has it.\n if (sock && sock.readyState === OPEN) {\n sendHello();\n for (const f of liveFlows()) postBoard(f);\n }\n },\n close(): void {\n closed = true;\n detach();\n queue = [];\n try { sock?.close(); } catch { /* already gone */ }\n sock = null;\n },\n };\n}\n","// ---------------------------------------------------------------------------\n// Live refresh (design/live-link.md): the game-side applier. The editor pushes\n// a freshly compiled bundle over the Live Link (createLiveLink's `onBundle`);\n// this swaps it in under the running engine: a new Engine over the new\n// bundle, loaded from the old one's save. The runtime's loadGame() already\n// tolerates edited content (a deleted card leaves the table, orphaned\n// cooldowns and hand contents drop, a new property takes its default), so the\n// run carries across - every flow of it; it refuses only a save from another\n// project.\n//\n// Patter's applyLiveBundle has two tiers (strings-only vs hot swap) because it\n// has string tables and a cursor to re-find; we have neither, so this is the\n// one tier. Wire-up:\n//\n// let engine = new Engine(bundle, { seed: 7, log: true });\n// let flow = engine.openFlow(\"main\");\n// const link = createLiveLink({\n// build: bundle.content.hash,\n// onBundle: ({ build, data }) => {\n// const r = applyLiveBundle(engine, data, { log: true });\n// if (!r.ok) return console.warn(r.error);\n// engine = r.engine; // re-bind your handles: loadGame\n// flow = engine.getFlow(\"main\") // rebuilt every flow, so the old\n// ?? engine.openFlow(\"main\"); // Flow objects are inert\n// link.attach(engine); // re-attach the ENGINE: loadGame rebuilt every flow\n// link.setBuild(build);\n// },\n// });\n// ---------------------------------------------------------------------------\n\nimport type { Bundle } from \"@storylet-studio/model\";\nimport { Engine } from \"@storylet-studio/runtime\";\nimport type { EngineOptions } from \"@storylet-studio/runtime\";\n\nexport type LiveBundleResult =\n /** The new engine, carrying the old one's run (all flows), and the bundle\n * it runs. */\n | { ok: true; engine: Engine; bundle: Bundle }\n /** Nothing changed: keep the engine you have. */\n | { ok: false; error: string };\n\n/**\n * Apply a bundle the editor pushed over the Live Link: `new Engine(parsed,\n * opts)` then `loadGame(engine.saveGame())`, returning the new engine. Never\n * throws; a failure (unparseable JSON, a bundle the runtime rejects, a\n * different project) comes back as `{ ok: false, error }` and the old engine\n * is untouched.\n *\n * `opts` are the options the old engine was created with. An engine does\n * not expose its seed, and it does not matter here: the save envelope carries\n * each flow's PRNG state, so `loadGame` resumes the draw sequences exactly\n * where they were and `seed` only shapes fresh flows. `log` does matter (the\n * retained log is per flow), and so does `world` (the host's binding does not\n * ride the envelope), so pass them if you had them.\n */\nexport function applyLiveBundle(engine: Engine, bundleJson: string, opts: EngineOptions = {}): LiveBundleResult {\n let bundle: Bundle;\n try {\n bundle = JSON.parse(bundleJson) as Bundle;\n } catch {\n return { ok: false, error: \"pushed bundle is not valid JSON\" };\n }\n try {\n const next = new Engine(bundle, opts);\n next.loadGame(engine.saveGame());\n return { ok: true, engine: next, bundle };\n } catch (e) {\n return { ok: false, error: e instanceof Error ? e.message : String(e) };\n }\n}\n","// ---------------------------------------------------------------------------\n// The host's @world container (design/flows.md; engine-runtimes.md 3.1).\n//\n// @world is the game's own state: the engine resolves it through a resolver\n// and NEVER saves it - \"host saves its container once, each engine saves its\n// own envelope\". A real game binds its own state here; a host that has no\n// state of its own (the demos, the playable page, the Board) uses this\n// ready-made container so @world still persists across its save/load.\n//\n// This is also what keeps a mixed Patter + Storylet Engine game honest: ONE\n// container, both engines mounting it foreign, neither writing it into its\n// envelope.\n// ---------------------------------------------------------------------------\n\nimport { PropertyBag as StateBag } from \"@wildwinter/scoperegistry\";\nimport type { ScalarValue } from \"@wildwinter/expr\";\nimport type { ScopeResolver } from \"@wildwinter/expr\";\nimport type { Bundle, PropertyBag } from \"@storylet-studio/model\";\n\nexport interface WorldContainer {\n /** Pass as `new Engine(bundle, { world: container.resolver })`. */\n resolver: ScopeResolver;\n /** The kernel bag itself (subscribe, audit, rows live there) - mount it\n * into a state logger or examiner beside the engine's own bags. Writing it\n * DIRECTLY is writing the kernel, so a `writable: false` declaration asks\n * the kernel's question: pass `{ host: true }` to say the game is speaking\n * (`bag.set(name, value, { host: true })`). Through `resolver` or an\n * engine's setProperty that is already answered. */\n bag: StateBag;\n /** The current values, for saving beside the engine's envelope. */\n values(): PropertyBag;\n /** Restore saved values over fresh defaults: orphaned keys drop, new\n * declarations keep their defaults - the same drift rule as loadGame. */\n load(values: PropertyBag): void;\n}\n\n/** A world container seeded from the bundle's @world declarations.\n *\n * The container is the GAME's state, so it WRITES - even a declaration\n * carrying `writable: false`. That flag is the STORY's promise not to write\n * the value (Reboot.md 10), and the engine keeps it where the story writes: an\n * outcome is refused against the engine's read-only table before it ever\n * reaches this resolver. Enforcing it here as well refused the HOST too - the\n * clock the game must move, the harness driving the value it is testing\n * against - which is the opposite of what the flag says.\n *\n * So the declarations are seeded AS DECLARED, which is what an examiner over\n * this container should read, and the writes go through as HOST writes\n * (scoperegistry 0.6.0's `{ host: true }`). The resolver's `set` is the\n * engine's own doorway and passes the flag too: the engine has already sorted\n * story from host by then - a story write was refused earlier, a host write is\n * the only kind that arrives - and a resolver takes a name and a value with no\n * room to say which. A game wanting a rule of its own binds its own resolver\n * rather than this one; the ports' container (Unreal's UStoryletWorld) draws\n * the same line, with HostSet never refused and StorySet asking the game's own\n * read-only list. */\nexport function createWorldContainer(bundle: Bundle): WorldContainer {\n const bag = new StateBag(bundle.world.properties, { normalise: (n) => n });\n return {\n resolver: {\n get: (n) => bag.get(n),\n set: (n: string, v: ScalarValue) => { bag.set(n, v, { host: true }); },\n },\n bag,\n values: () => bag.values,\n load: (values) => bag.load(values),\n };\n}\n"],"mappings":"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","names":["line","createStateLogger","show","head","line","showVal"]}
1
+ {"version":3,"sources":["../../../../expr/packages/scoperegistry/src/state-logger.ts","../../../../expr/packages/scoperegistry/src/index.ts","../src/logger.ts","../../model/src/index.ts","../src/save.ts","../src/inspector.ts","../src/bundle-inspector.ts","../src/live-link.ts","../src/refresh.ts","../src/world.ts"],"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 TO THE STORY; the HOST still writes it, by passing\n * `{ host: true }` (see `set`). Default is read/write. (`type`/`values` feed\n * validation.)\n *\n * The distinction is the whole point of the flag on a foreign scope, where the\n * value is the game's own: a flag carried in the story's bundle must not lock a\n * game out of its own state. Ruled 2026-09-05, after both products met it - the\n * Storylet Engine's venue clock and Patter's coverage driver were each blocked\n * from the one property they existed to move.\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 /** A name as this bag keys it: its normalisation policy applied. The registry\n * uses it to key quality ladders and the validation schema the bag's own way,\n * so a case-significant (identity) bag is not quietly folded to lower case\n * one layer up. */\n normalise(name: string): string {\n return 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 unless the caller says it is the\n * HOST (`host: true`), for whom `writable: false` was never a rule - it is\n * the story's promise, not the game's. `silent` and `host` are separate on\n * purpose: one is about who hears the write, the other about who may make\n * it. Returns the change. */\n set(name: string, value: ScalarValue, opts?: { silent?: boolean; reason?: string; host?: boolean }): BagChange {\n const n = this.norm(name);\n if (!opts?.host && 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 owner?: string;\n}\ninterface ForeignScope {\n kind: \"foreign\";\n resolver: ScopeResolver;\n decls: Map<string, ScopeDeclaration>;\n scopeWritable: boolean;\n norm: (name: string) => string;\n owner?: string;\n}\ntype Entry = OwnedScope | ForeignScope;\n\nconst lowerCase = (name: string): string => name.toLowerCase();\n\n/** Options for an owned scope the registry builds (`defineOwned`). */\nexport interface OwnedScopeOptions {\n /** The address prefix its examiner rows carry, separator included. Defaults\n * to `<token>.`; the address grammar is the product's, not the registry's. */\n pathPrefix?: string;\n /** Name normalisation: lower case by default; a case-significant product\n * passes identity. */\n normalise?: (name: string) => string;\n /** Who registered it (an engine's name). Named in a clash error and carried\n * on examiner rows, so one examiner can group a combined game by engine. */\n owner?: string;\n}\n\n/** Options for a foreign scope (`defineForeign`). */\nexport interface ForeignScopeOptions {\n /** Scope-level read/write default for its declarations (default true). */\n writable?: boolean;\n /** Name normalisation for the names passed to the resolver: lower case by\n * default; a case-significant product passes identity. */\n normalise?: (name: string) => string;\n /** Who registered it. See `OwnedScopeOptions.owner`. */\n owner?: string;\n}\n\n/** Options for a context or schema built from the registry. */\nexport interface AliasOptions {\n /**\n * Expression token -> registered key. `{ scene: \"patter/flow-2/scene/tavern\" }`\n * makes `@scene` read that instance bag, for this context only.\n *\n * The registry learns nothing about what the token MEANS: which flow, which\n * scene, which deck is an engine's own idea, and the engine names the key per\n * evaluation. It has to be the registry's mechanism rather than the engine\n * patching the context afterwards, because quality ladders and validation are\n * looked up by token too, and an alias applies to all three alike.\n *\n * An alias to a key that is not registered throws: a condition evaluated\n * against a scope that is not there is an engine bug, not a graceful false.\n */\n aliases?: Record<string, string>;\n}\n\n/** Options for `remove`. */\nexport interface RemoveOptions {\n /** Park an owned scope's values, to be handed back when the same key is next\n * registered (a live reload rebuilding an engine). No effect on a foreign\n * scope, whose values were never the registry's. */\n keep?: boolean;\n}\n\n/** Options for `load`. */\nexport interface LoadOptions {\n /** Keep what earlier loads parked, adding this blob's unclaimed sections to\n * it (a section for the same key replaces the parked one). Without it, each\n * load replaces whatever an earlier load parked. For an engine moving an\n * older save's values into a registry the game has already loaded. */\n keepParked?: boolean;\n}\n\n/**\n * The versioned owned-state fragment.\n *\n * @deprecated Versioning belongs to the save that embeds the values, not to the\n * registry; no engine ever called this. Embed `save()` in your own versioned\n * save instead. Removed at the next breaking release.\n */\nexport interface OwnedStateFragment {\n version: number;\n scopes: Record<string, Record<string, ScalarValue>>;\n}\n\n/** @deprecated See `OwnedStateFragment`. Removed at the next breaking release. */\nexport const SAVE_FRAGMENT_VERSION = 1;\n\nexport class ScopeRegistry {\n private readonly scopes = new Map<string, Entry>();\n /** Values loaded for keys nobody has registered yet, waiting to be claimed. */\n private readonly parked = new Map<string, Record<string, ScalarValue>>();\n private rev = 0;\n\n /**\n * A counter that moves whenever a scope is registered or removed, and at no\n * other time: it starts at 0 and each registration or removal adds 1. Values\n * changing does not move it. A caller that caches a context built by\n * `toEvalContext()` rebuilds it when this moves, because the context's set of\n * scopes is fixed when it is built while the values it reads stay live.\n */\n get revision(): number {\n return this.rev;\n }\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 * The third argument may be the path prefix alone (the pre-0.7 form) or an\n * options object.\n */\n defineOwned(token: string, declarations: ScopeDeclaration[], opts?: string | OwnedScopeOptions): this {\n const o: OwnedScopeOptions = typeof opts === \"string\" ? { pathPrefix: opts } : opts ?? {};\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 const bag = new PropertyBag(declarations, {\n pathPrefix: o.pathPrefix ?? `${token}.`,\n ...(o.normalise ? { normalise: o.normalise } : {}),\n });\n return this.mountOwned(token, bag, o.owner !== undefined ? { owner: o.owner } : undefined);\n }\n\n /**\n * Attach an EXISTING bag as an owned scope: an engine (or a host) holds the\n * bag and this registry reads, writes, lists and saves it like its own.\n *\n * If values were loaded for this key before anyone registered it, the bag\n * claims them now: laid over its seeded defaults by the bag's own `load` rule.\n */\n mountOwned(token: string, bag: PropertyBag, opts?: { owner?: string }): this {\n this.assertFree(token, opts?.owner);\n this.scopes.set(token, { kind: \"owned\", bag, ...(opts?.owner !== undefined ? { owner: opts.owner } : {}) });\n this.rev++;\n const waiting = this.parked.get(token);\n if (waiting) {\n bag.load(waiting);\n this.parked.delete(token);\n }\n return this;\n }\n\n /**\n * Unregister a scope. With `{ keep: true }` an owned scope's values are parked\n * and handed back when the same key is next registered, which is how a live\n * reload hands an engine's state to its replacement. Throws on an unknown key.\n */\n remove(token: string, opts?: RemoveOptions): this {\n const e = this.scopes.get(token);\n if (!e) throw new Error(`unknown scope '@${token}'`);\n if (opts?.keep && e.kind === \"owned\") this.parked.set(token, e.bag.save());\n this.scopes.delete(token);\n this.rev++;\n return this;\n }\n\n /**\n * Drop parked values nobody claimed. Parked values are kept in the next save by\n * default, so nothing loaded is lost to a flow or deck that simply has not\n * reopened yet; a game that knows they are dead drops them here.\n *\n * With a `prefix`, only keys starting with it are dropped: an engine resetting\n * itself drops its own instance keys (`my-engine/`) and leaves every other\n * engine's alone.\n */\n discardParked(prefix?: string): this {\n if (prefix === undefined) this.parked.clear();\n else for (const key of [...this.parked.keys()]) if (key.startsWith(prefix)) this.parked.delete(key);\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 opts: boolean | ForeignScopeOptions = true,\n ): this {\n // A boolean is the pre-0.7 form: the scope-level writable default alone.\n const o: ForeignScopeOptions = typeof opts === \"boolean\" ? { writable: opts } : opts;\n this.assertFree(token, o.owner);\n const norm = o.normalise ?? lowerCase;\n const decls = new Map<string, ScopeDeclaration>();\n for (const d of declarations) decls.set(norm(d.name), d);\n this.scopes.set(token, {\n kind: \"foreign\", resolver, decls, scopeWritable: o.writable ?? true, norm,\n ...(o.owner !== undefined ? { owner: o.owner } : {}),\n });\n this.rev++;\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(e.norm(name));\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 scope.\n *\n * `writable: false` is the STORY's promise, so a story write is refused and\n * a HOST write is not: pass `{ host: true }` from a host's own surface (its\n * `setProperty`, its tooling, a coverage driver) and never from the path an\n * outcome or effect takes. A foreign scope whose resolver has no `set` is\n * refused for everyone, host included - that is not a rule to bypass, it is\n * a game that gave no way to write. */\n set(scope: string, name: string, value: ScalarValue, opts?: { host?: boolean }): 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, opts?.host ? { host: true } : undefined);\n } catch {\n throw new Error(`'@${scope}.${name}' is read-only`);\n }\n return;\n }\n const n = e.norm(name);\n if (!e.resolver.set) throw new Error(`'@${scope}.${name}' is read-only`);\n if (!opts?.host && !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; owner?: string } & PropertyRow)[] {\n const out: ({ scope: string; owner?: string } & PropertyRow)[] = [];\n for (const [token, e] of this.scopes) {\n const owner = e.owner !== undefined ? { owner: e.owner } : {};\n if (e.kind === \"owned\") {\n for (const row of e.bag.rows()) out.push({ scope: token, ...owner, ...row });\n } else {\n for (const [n, d] of e.decls) {\n out.push({\n scope: token, ...owner,\n ...rowFor(d, e.resolver.get(n), this.foreignWritable(e, n), n, `${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>, opts?: AliasOptions): EvalContext {\n const view = this.view(opts?.aliases);\n const scopes: EvalContext[\"scopes\"] = {};\n for (const [token, e] of view) scopes[token] = e.kind === \"owned\" ? e.bag.values : e.resolver;\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(view);\n return qualities.size === 0 ? { scopes, host } : {\n scopes, host,\n qualities: (scope, name) => {\n const e = view.get(scope);\n return e ? qualities.get(scope)?.get(normOf(e)(name)) : undefined;\n },\n };\n }\n\n /**\n * The scopes an expression sees: every registered key under its own token,\n * then each alias token pointing at its key's entry (an alias shadows a key of\n * the same name). Keys an engine uses for instance bags (`engine/flow-2/...`)\n * are not valid expression tokens, so they are present but unreachable.\n */\n private view(aliases?: Record<string, string>): Map<string, Entry> {\n const out = new Map(this.scopes);\n for (const [token, key] of Object.entries(aliases ?? {})) {\n const e = this.scopes.get(key);\n if (!e) throw new Error(`alias '@${token}' names '${key}', which is not registered`);\n out.set(token, e);\n }\n return out;\n }\n\n /** Every quality declaration's ladder, keyed scope token then name (the\n * scope's own normalisation). */\n private qualityLadders(view: Map<string, Entry>): Map<string, Map<string, readonly string[]>> {\n const out = new Map<string, Map<string, readonly string[]>>();\n for (const [token, e] of view) {\n for (const [n, d] of declsOf(e)) {\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(n, 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. Aliases\n * apply as they do to `toEvalContext`, so a condition written against `@scene`\n * validates against the instance bag the engine names.\n */\n toSchema(opts?: AliasOptions): ExpressionSchema {\n const properties = new Map<string, Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>>();\n for (const [token, e] of this.view(opts?.aliases)) {\n const decls = declsOf(e);\n if (decls.length === 0) continue;\n const m = new Map<string, { type: PropertyType; enumValues?: string[]; stages?: string[] }>();\n for (const [n, d] of decls) m.set(n, {\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 (foreign scopes are the game's, and the game\n * saves them), as bare bags keyed by token, plus any values still parked, so\n * a save taken before every engine has re-registered loses nothing. The\n * registry knows nothing about game saves: a game embeds this in its own. */\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 for (const [token, vals] of this.parked) out[token] = structuredClone(vals);\n return out;\n }\n\n /**\n * Restore from a `save` blob. An owned scope lays its section over its current\n * values (the bag's `load` rule). A section for a key nobody has registered\n * yet is PARKED and handed over when that key registers, so a game can load\n * its registry before its engines have reopened their flows or decks. A\n * section for a foreign scope is ignored: those values are the game's.\n *\n * A load replaces whatever was parked before it: it is a whole restore, and\n * residue from an earlier load must not leak into this one.\n *\n * Changed in 0.7.0: sections for unregistered keys used to be dropped.\n */\n load(blob: Record<string, Record<string, ScalarValue>>, opts?: LoadOptions): void {\n if (!opts?.keepParked) this.parked.clear();\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 else if (!e) this.parked.set(token, structuredClone(vals));\n }\n }\n\n /**\n * `save()` wrapped with a version stamp.\n *\n * @deprecated Versioning belongs to the save that embeds the values; no\n * engine ever called this. Embed `save()` in your own versioned save.\n * Removed at the next breaking release.\n */\n saveFragment(): OwnedStateFragment {\n return { version: SAVE_FRAGMENT_VERSION, scopes: this.save() };\n }\n\n /**\n * Restore from a versioned fragment; an unsupported version throws.\n *\n * @deprecated See `saveFragment`. Removed at the next breaking release.\n */\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 /**\n * A token is taken once. There is no reserved-token list: a clash surfaces\n * here, the moment a game combines its engines, which is the only moment\n * anyone knows which engines are present. With owners recorded the error says\n * whose token it already is.\n */\n private assertFree(token: string, owner?: string): void {\n const e = this.scopes.get(token);\n if (!e) return;\n const by = e.owner !== undefined ? ` by ${e.owner}` : \"\";\n const wants = owner !== undefined ? ` (wanted by ${owner})` : \"\";\n throw new Error(`scope '@${token}' is already registered${by}${wants}`);\n }\n}\n\n/** A scope entry's declarations, keyed by its own normalisation. */\nfunction declsOf(e: Entry): [string, ScopeDeclaration][] {\n if (e.kind === \"foreign\") return [...e.decls.entries()];\n return e.bag.declarations().map((d) => [e.bag.normalise(d.name), d]);\n}\n\n/** A scope entry's name normalisation. */\nfunction normOf(e: Entry): (name: string) => string {\n return e.kind === \"foreign\" ? e.norm : (n) => e.bag.normalise(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","// ---------------------------------------------------------------------------\n// The storylets state logger: an ADAPTER over the kernel logger in\n// @wildwinter/scoperegistry (design/engine-runtimes.md 3.4 is the design of\n// record). The kernel does the work - push-based property logging on the\n// PropertyBag audit hook, so a write logs the moment it lands, plus a diff of\n// everything that has no audit hook - and this supplies the two product-shaped\n// pieces: which bags to watch, and the non-property state (turns / cooldowns /\n// board) as flattened paths.\n//\n// The core lived here, marked \"moves into the kernel package wholesale when the\n// vendor-sync slice lands\". It has. Patterplay's logger, which diffed save\n// snapshots and so could not see a value that changed and changed back, is an\n// adapter over the same core now.\n//\n// Flattened path scheme:\n// world.x / story.x / box.<gameId>.x / deck.<gameId>.x / hand.<gameId>.x /\n// value.<tagGameId>.x, and value.<boxGameId>/<tagGameId>.x for a tag gameId\n// two boxes share (4.4)\n// turn:<boxId> per-box clocks\n// cooldown:<cardId> next-eligible turns\n// board:<handId> hand contents (card ids, dealt order)\n// Line format: `${label}${path}: ${from} -> ${to}`, `<unset>` for undefined.\n// ---------------------------------------------------------------------------\n\nimport type { Engine, Flow } from \"@storylet-studio/runtime\";\nimport type { FlowSave, ScalarValue } from \"@storylet-studio/model\";\nimport {\n createStateLogger as createKernelStateLogger, diffState,\n} from \"@wildwinter/scoperegistry\";\nimport type {\n StateSnapshot, StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions,\n} from \"@wildwinter/scoperegistry\";\n\n// Re-exported: these were declared here, and a host importing them from\n// @storylet-studio/play-helpers should not have to care that they moved.\nexport { createKernelStateLogger, diffState };\nexport type { StateSnapshot, StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions };\n\n/** The full flattened snapshot of ONE FLOW's view - the shared partitions\n * plus that flow's own - plus its turns / cooldowns / board. @world is not\n * here for the same reason it is not in a save envelope: the host owns that\n * container and mounts/saves it itself (createWorldContainer).\n *\n * Taken off the BAGS, which is what a save envelope is made of, rather than\n * off the envelope itself. The two used to be interchangeable; from 4.4 they\n * are not, because a property ADDRESS names its owner by gameId while the\n * envelope stays keyed by internal id (a save has to survive a rename). The\n * bags carry the address, so reading them is what keeps this snapshot and\n * the live logger's lines in ONE path space - which is the invariant the\n * whole diff rests on. */\nexport function snapshotState(engine: Engine, flow: Flow): StateSnapshot {\n const out: StateSnapshot = {};\n // Shared under the flow's own: names are disjoint (shared XOR per-flow by\n // declaration), so one path space holds both without collision.\n for (const { bag } of [...engine.listBags(), ...flow.listBags()]) {\n for (const row of bag.rows()) {\n if (row.value !== undefined) out[row.path] = row.value as ScalarValue;\n }\n }\n Object.assign(out, extraState(engine.saveGame().flows[flow.id]));\n return out;\n}\n\n/** The storylets path-provider adapter for non-property state (design 3.4):\n * one flow's turns / cooldowns / board as flattened paths, off its blob in\n * the envelope (absent for a just-closed flow: no paths). */\nfunction extraState(saved: FlowSave | undefined): StateSnapshot {\n const out: StateSnapshot = {};\n if (saved === undefined) return out;\n for (const [boxId, turn] of Object.entries(saved.turns)) out[`turn:${boxId}`] = turn;\n for (const [cardId, at] of Object.entries(saved.cooldowns)) out[`cooldown:${cardId}`] = at;\n for (const [handId, cards] of Object.entries(saved.board)) out[`board:${handId}`] = [...cards];\n return out;\n}\n\n/** The storylets state logger: the kernel core mounted on the SHARED bags\n * (engine.listBags()) and one flow's own (flow.listBags()) - the same\n * prefixes, one path space, names disjoint - plus the flow's turns /\n * cooldowns / board adapter. A host that wants @world lines mounts its\n * world container's bag through createKernelStateLogger itself. */\nexport function createStateLogger(engine: Engine, flow: Flow, opts: StateLoggerOptions = {}): StateLogger {\n // By NAME, not by handle: loadGame rebuilds every flow and the handle we\n // were given goes inert; capture()'s re-mount picks up the rebuilt one.\n const id = flow.id;\n const live = (): Flow | undefined => engine.getFlow(id);\n return createKernelStateLogger({\n // A BagMount's `prefix` (\"story\", \"deck.<id>\") is the engine's label for the mount;\n // the kernel composes paths from the BAG's own pathPrefix (\"story.\", \"deck.<id>.\")\n // and needs none passed. Same strings, one owner.\n mounts: () => [...engine.listBags(), ...(live()?.listBags() ?? [])].map(({ bag }) => ({ bag })),\n extra: () => extraState(engine.saveGame().flows[id]),\n }, opts);\n}\n","// ---------------------------------------------------------------------------\n// @storylet-studio/model - the shape source-of-truth.\n//\n// Transcribes design/storylets-schema.md (bundle, save) and\n// design/storylets-source.md (shards). Entity shapes are generic over their\n// expression representation E: source shards use plain `src` strings\n// (Card<string>), the compiled bundle uses { src, ast } envelopes\n// (Card<Expression>). No behaviour lives here.\n// ---------------------------------------------------------------------------\n\nimport type { Expression, ScalarValue } from \"@wildwinter/expr\";\n\nexport type { Expression, ScalarValue, AstNode } from \"@wildwinter/expr\";\n\n// --- shared declarations -----------------------------------------------------\n\nexport type PropertyType = \"boolean\" | \"number\" | \"string\" | \"enum\" | \"flags\" | \"quality\";\n\n/** A property declaration: @world / @story / @box / @deck / tag / hand\n * state. A declared property always has a value (`default` is required);\n * referencing an undeclared property is a publish-time error. */\nexport interface PropertyDecl {\n name: string;\n type: PropertyType;\n default: ScalarValue;\n values?: string[];\n /**\n * A quality's ordered ladder of stage names (design/quality.md). Order IS\n * the meaning: `>=` compares by position here, and `advance()` steps along\n * it. The one order-semantic list in the format, accepted as such: it is a\n * declaration, and inserting a stage mid-ladder is the design's whole point.\n */\n stages?: string[];\n /**\n * `@world` only. `false` makes the property read-only TO THE STORY: a\n * condition may read it, an outcome that writes it is a compile error. The\n * game still moves it through its resolver; this is the story's statement\n * of intent, not the game's policy. Mirrors Patter's `HostScopeDecl.writable`\n * name for name (Reboot.md 10, ruled 2026-09-03). Ignored on every other\n * scope. Absent = writable.\n */\n writable?: boolean;\n /**\n * The sharing axis (design/flows.md, Patter's flag adopted): is this\n * property's value one world value across all flows, or a copy per flow?\n * It does NOT change reference syntax - sharing is set here, on the\n * declaration, not by a different scope token. Absent = the scope\n * default: `@story` shared; box, deck, hand and tag properties per-flow.\n * On a `@world` declaration the flag is a validation error - `@world` is\n * the game's own state, always engine-level, never per-flow.\n */\n shared?: boolean;\n /**\n * The durability axis (design/engine-server.md 4.2), valid wherever `shared`\n * is valid and orthogonal to it: `shared` says whose value this is WITHIN a\n * run, `durable` says whether the value survives the run at all. A durable\n * shared property is the installation's memory (\"trolls defeated since we\n * opened\"); a durable per-flow one is the player's pocket (visits,\n * allegiance, what they earned).\n *\n * INERT TO THE RUNTIME. The engine partitions by `shared` alone and never\n * reads this. Durability is what the SERVER does at a run boundary: it reads\n * the declarations, lifts the durable values out of the partitions before the\n * world restarts, and writes them back into the fresh engine afterwards,\n * entirely through `getProperty` / `setProperty`.\n *\n * On a `@world` declaration the flag is a validation error, for the reason\n * `shared` is: @world is the game's own state, and how long the game keeps it\n * is the game's business.\n */\n durable?: boolean;\n purpose?: string;\n}\n\n/** A template field (box-defined), of the card template or of the outcome\n * fields. Data for the host; the engine never interprets fields and they are\n * not addressable from expressions. */\nexport interface FieldDecl {\n name: string;\n type: PropertyType;\n default: ScalarValue;\n values?: string[];\n purpose?: string;\n}\n\n/** Cooldown policy, in turns (schema 3.4). */\nexport type RedrawPolicy = \"always\" | \"never\" | number;\n\n// --- gameId derivation (Patter's effectiveGameId, adopted 2026-07-20) --------\n//\n// gameId is the renameable host-facing address; it is OPTIONAL in source and\n// derived from the entity's title until the author pins one, so a rename of\n// the title carries the address with it (no \"new-deck\" stuck placeholder).\n// The compiler fills a concrete gameId into every bundle entity.\n\n/** Slugify a human label into a filename- / address-safe gameId. */\nexport function gameIdify(text: string): string {\n return text.toLowerCase().replace(/['’]/g, \"\")\n .replace(/[^a-z0-9-]+/g, \"-\").replace(/-+/g, \"-\").replace(/^-+|-+$/g, \"\");\n}\n\nexport function isValidGameId(gameId: string): boolean {\n return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);\n}\n\n// --- property names (adopted 2026-08-18, with Patter, from one rule) ---------\n//\n// Design and argument: `@wildwinter/app-shell` src/property-names.ts. Not house\n// style: the rule is what `@wildwinter/expr` can parse. Its lexer takes an\n// identifier as /[a-zA-Z_][a-zA-Z0-9_]*/ and folds it to lower case, so\n// `@story.isNight` reaches a property called `isnight`, `@story.9lives` and\n// `@story.not` are parse errors, and `@story.is-night` is not an error at all: it\n// compiles to `@story.is` MINUS the string \"night\". That last one is why the rule\n// is enforced rather than trusted - it is the only violation that silently means\n// something else.\n//\n// Here rather than behind an import of the UI kit because the compiler, the CLI\n// and the embedded runtime resolve state by these. `property-name-parity.test.ts`\n// holds them to the shell's, and `property-name-grammar.test.ts` holds them to the\n// parser they came from.\n\n/** The words `@wildwinter/expr` lexes as keywords, so no property may be called one. */\n// A legal property NAME is a fact about the expression language, not about this\n// model: `not` is reserved because the tokeniser reads it as an operator. Both\n// families kept their own copy of the rule AND of the keyword list, a list\n// neither owned. @wildwinter/expr derives the list from its own tokeniser, so a\n// keyword added there cannot leave a stale copy here.\n//\n// Re-exported so nothing that imports them has to move.\nexport {\n propertyNameify, isValidPropertyName, isCaseOnlyPropertyName, RESERVED_PROPERTY_NAMES,\n} from \"@wildwinter/expr\";\n/** The effective address: a pinned gameId, else derived from the title, else\n * the immutable id (so there is always something addressable). */\nexport function effectiveGameId(entity: { gameId?: string; title?: string; id: string }): string {\n const pinned = entity.gameId?.trim();\n if (pinned) return pinned;\n const fromTitle = entity.title ? gameIdify(entity.title) : \"\";\n return fromTitle || entity.id;\n}\n\n// --- the value scope's owner segment (design/engine-server.md 4.4) -----------\n//\n// Every other owned scope names its owner with a gameId that is unique across\n// the bundle: box, deck, hand and card. A TAG's gameId is unique only within\n// its group, and a group's only within its box, so two boxes may each name a\n// tag \"docks\" - as ordinary as two boxes each having a \"zone\" group - and\n// `value.docks.danger` then names two stores.\n//\n// So the value scope's owner segment is box-qualified where it has to be:\n// `value.<boxGameId>/<tagGameId>.<name>`. The slash sits INSIDE the owner\n// segment, so the address still splits into three on the dot and no parser\n// changes shape. The qualified form is always accepted; the short form is\n// accepted while exactly one tag in the bundle carries that gameId, and\n// refused when more do, naming the qualified candidates. What a runtime\n// PRINTS - `listProperties`, a write on the trace, a load report, an\n// examiner - is the short form except where the gameId repeats.\n//\n// One definition, because the Board draws these addresses from the bundle\n// while the engine builds them from its own index, and an address the editor\n// shows that the engine will not take is the fault 4.4 was fixing.\n\n/** The three answers a value address needs, all derived from the bundle. */\nexport interface ValueAddresses {\n /** Tag internal id -> the owner segment an address PRINTS for it. */\n print: Map<string, string>;\n /** Every owner segment a value address ACCEPTS -> the tag's internal id.\n * Holds the qualified form for every tag and the short form only for a\n * gameId no other tag shares. */\n accept: Map<string, string>;\n /** A tag gameId more than one box uses -> its qualified forms, in bundle\n * order. Empty for the overwhelming majority of projects, and what a\n * refusal lists. */\n repeated: Map<string, string[]>;\n}\n\n/** The owner segment of every tag in the bundle, both ways round. */\nexport function valueAddresses(bundle: {\n boxes: readonly { id: string; gameId?: string; title?: string; tagGroups: readonly TagGroup[] }[];\n}): ValueAddresses {\n const tags: { id: string; gameId: string; qualified: string }[] = [];\n for (const box of bundle.boxes) {\n const boxGameId = effectiveGameId(box);\n for (const group of box.tagGroups) {\n for (const tag of group.tags) {\n const gameId = effectiveGameId(tag);\n tags.push({ id: tag.id, gameId, qualified: `${boxGameId}/${gameId}` });\n }\n }\n }\n // Distinct qualified forms per gameId. Distinct rather than a count: two\n // groups in ONE box may also name a tag the same way, and a refusal that\n // offered the same address twice would be no help at all. Those two share\n // the qualified segment, and the first in bundle order answers to it, which\n // is what the short form did for everything before this rule.\n //\n // That last case is closing at the source rather than here (question 16,\n // ruled 2026-09-06): the compiler WARNS that a tag gameId must be unique\n // within its box, across all of that box's groups, and refuses it from the\n // next release. So this stays as the reading rule for a bundle built before\n // that, and a bundle built after it has no repeated qualified form to read.\n const forms = new Map<string, string[]>();\n for (const tag of tags) {\n const list = forms.get(tag.gameId) ?? [];\n if (!list.includes(tag.qualified)) list.push(tag.qualified);\n forms.set(tag.gameId, list);\n }\n const print = new Map<string, string>();\n const accept = new Map<string, string>();\n const repeated = new Map<string, string[]>();\n for (const tag of tags) {\n const candidates = forms.get(tag.gameId) ?? [tag.qualified];\n const ambiguous = candidates.length > 1;\n print.set(tag.id, ambiguous ? tag.qualified : tag.gameId);\n if (!accept.has(tag.qualified)) accept.set(tag.qualified, tag.id);\n if (!ambiguous && !accept.has(tag.gameId)) accept.set(tag.gameId, tag.id);\n if (ambiguous) repeated.set(tag.gameId, candidates);\n }\n return { print, accept, repeated };\n}\n\n/** What an ambiguous short-form value address is told: the candidates, in\n * full, because \"that names two tags\" without them leaves a host reading a\n * bundle it did not write to find out which boxes. */\nexport function ambiguousValueAddressMessage(\n segment: string, name: string, candidates: readonly string[],\n): string {\n const forms = candidates.map((q) => `\"value.${q}.${name}\"`);\n const list = forms.length <= 1 ? (forms[0] ?? \"\")\n : `${forms.slice(0, -1).join(\", \")} or ${forms[forms.length - 1]}`;\n return `\"value.${segment}.${name}\" names a tag in ${candidates.length} boxes; write ${list}`;\n}\n\n/**\n * The first free gameId of the form `base`, `base-2`, `base-3`, ... not already\n * in `taken`.\n *\n * A gameId is API - `deal()` and the play log speak it - so a name minted for a\n * new or duplicated entity must not collide with an existing one. This lived in\n * two copies, one in the editor and one in the CLI's kit scaffolder, character\n * for character the same; two copies of an addressing rule can drift, and a\n * drift here means the same act produces different addresses depending on which\n * program did it. It is here, beside `gameIdify`, because both programs need it\n * and no UI touches it.\n */\nexport function freeGameId(base: string, taken: ReadonlySet<string>): string {\n let gameId = base;\n for (let n = 2; taken.has(gameId); n++) gameId = `${base}-${n}`;\n return gameId;\n}\n\n/**\n * The first free TITLE of the form `base`, `base 2`, `base 3`, ... whose\n * derived gameId is not already in `taken`.\n *\n * The sibling of `freeGameId` for the \"New box\", \"New deck\" case, where the\n * author is given a title and the address follows from it. The dedupe is on the\n * DERIVED gameId rather than the title, because two titles that slug to one\n * address are the collision that matters.\n */\n/**\n * An id-sorted collection in the order a person should SEE it.\n *\n * Storage is sorted by immutable id (source rule 5) so that two authors adding\n * one item each never touch the same line. That makes array position useless as\n * display order, so the order the author arranged rides in a sparse `order`\n * field, with position as the fallback and id to break a tie.\n *\n * One definition, because this rule has to give the same answer in four places\n * that a reader compares side by side: the compiler (what the bundle carries),\n * the editor (what the card document lists), the exports, and Find. When they\n * disagree, the editor shows one order and the game plays another.\n */\nexport function byDisplayOrder<T extends { id?: string; order?: number }>(items: readonly T[]): T[] {\n return items\n .map((x, i) => ({ x, key: x.order ?? i, i }))\n .sort((a, b) => a.key - b.key || ((a.x.id ?? \"\") < (b.x.id ?? \"\") ? -1 : (a.x.id ?? \"\") > (b.x.id ?? \"\") ? 1 : a.i - b.i))\n .map((e) => e.x);\n}\n\nexport function freeTitle(base: string, taken: ReadonlySet<string>): string {\n let title = base;\n for (let n = 2; taken.has(gameIdify(title)); n++) title = `${base} ${n}`;\n return title;\n}\n\n/**\n * A count of a TIMED box's turns, said as time (design/engine-server.md 4.8):\n * `turnSpan(30, 60)` is \"30 min\", and `turnSpan(30, 60, true)` is \"30 minutes\".\n *\n * One definition, because the conversion appears wherever a designer might\n * otherwise have to do it in their head: the card editor's Redraw field, the\n * box page, the Board's advance buttons, and the coverage report's turn\n * budget. Two of those want the unit spelled out and two want it short, which\n * is the whole of `long`.\n */\nexport function turnSpan(turns: number, seconds: number, long = false): string {\n const total = Math.max(0, Math.round(turns * seconds));\n const say = (n: number, short: string, one: string, many: string): string =>\n long ? `${n} ${n === 1 ? one : many}` : `${n} ${short}`;\n if (total < 60) return long ? say(total, \"s\", \"second\", \"seconds\") : `${total}s`;\n if (total < 3600) {\n const minutes = total % 60 === 0 ? total / 60 : Math.round(total / 6) / 10;\n return say(minutes, \"min\", \"minute\", \"minutes\");\n }\n let hours = Math.floor(total / 3600);\n let rest = Math.round((total % 3600) / 60);\n if (rest === 60) { hours += 1; rest = 0; }\n const said = say(hours, \"hr\", \"hour\", \"hours\");\n return rest === 0 ? said : `${said} ${say(rest, \"min\", \"minute\", \"minutes\")}`;\n}\n\n// --- entities (generic over expression representation E) ---------------------\n\nexport interface Outcome<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Unlike `Card.order` this one IS compiled into the bundle:\n * which option is offered first is authorial, and a host reading a dealt\n * card's outcomes is building the player's menu. */\n order?: number;\n /** Gating; availability is always evaluated against current state. */\n condition?: E;\n /** Target (\"@scope.name\") -> expression; all right-hand sides evaluate\n * against pre-play state (schema 3.7). */\n changes: Record<string, E>;\n /** Template data, as `Card.fields` is: field name -> value, declared by\n * the box's `outcomeFields`, validated at publish, and handed to the host\n * with the outcome. The engine never reads it: a press can say one line\n * (\"The notice is in your pocket\") without spending a card on it. */\n fields?: Record<string, ScalarValue>;\n}\n\nexport interface Card<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order within the deck (sparse; a card without one falls\n * back to its id position). Merges as a per-card value, so id-sorted storage\n * stays merge-clean (Reboot 7.4); dropped from the compiled bundle. */\n order?: number;\n condition?: E;\n /** Default 0; an expression must evaluate to a number. */\n priority: number | E;\n redraw: RedrawPolicy;\n /** Tags: tag group id -> tag ids. An absent group is a wildcard (matches\n * any binding of it), except the reserved home group, whose default\n * inverts (schema 2.4). Editors and fixtures speak gameIds; stored\n * references are ids. */\n tags?: Record<string, string[]>;\n /** How many hands may hold this card at once (schema 3.5): integer >= 1,\n * default 1. One copy is the exclusivity rule; copies: N is the\n * deliberate opt-out for interchangeable filler. Always counted WITHIN a\n * flow, whether or not the card is shared. */\n copies?: number;\n /** Scarcity across flows (design/shared-scarcity.md). Absent takes the\n * deck's flag; set here it overrides the deck, so a single unique card can\n * stay in the content it belongs to. A shared card's claims count every\n * flow's board, and a shared `redraw: \"never\"` is spent for everyone the\n * first time anyone plays it.\n *\n * A finite `redraw` stays PER FLOW even when shared: a cooldown is an\n * absolute turn of the card's box clock and clocks are per flow, so\n * \"3 turns of whose clock?\" has no answer. A world-wide timer is a @world\n * question, not an engine one (shared-scarcity 9.3.3). */\n shared?: boolean;\n /** The world cap: how many hands ACROSS EVERY FLOW may hold this at once.\n * Read only when the card is shared, and defaults to `copies`, so the\n * common case writes one number and \"five in the world, one to a customer\"\n * is `copies: 1, sharedCopies: 5`. */\n sharedCopies?: number;\n /** Does this card's `redraw: \"never\"` spend survive the run\n * (design/engine-server.md 4.2)? Absent takes the deck's flag, set here it\n * overrides the deck, exactly as `shared` does. `shared` decides who a\n * spend counts for WITHIN a run; this decides whether it outlives one.\n *\n * Only `\"never\"` crosses the run boundary, for the reason only `\"never\"`\n * crosses the flow boundary (shared-scarcity 9.3.2): a finite cooldown is\n * an absolute turn of a box clock, and the clock resets with the run. On\n * any other redraw the flag is a compile warning.\n *\n * INERT TO THE RUNTIME, like the declaration flag: the server lifts the\n * durable spends at run end (per-flow ones from the flow's `never`\n * cooldowns, shared ones from the engine's spent set) and puts them back\n * through `openFlow(id, { restore })` and `markTaken`. */\n durable?: boolean;\n /** Card-template data: field name -> value, validated at publish. */\n fields?: Record<string, ScalarValue>;\n outcomes: Outcome<E>[];\n}\n\nexport interface Deck<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** The deck gate, evaluated once per draw in the draw's environment. */\n condition?: E;\n /** This pile is scarce across flows (design/shared-scarcity.md): every card\n * in it is shared unless the card says otherwise. The container is where\n * Patter puts its own shared-memory flag, and the deck is our container. */\n shared?: boolean;\n /** Every `redraw: \"never\"` card in this pile is spent for good, past the end\n * of the run, unless the card says otherwise (design/engine-server.md 4.2).\n * The container carries the flag for the reason `shared` is carried here:\n * a pile is what an author reaches for when a rule is true of all of it. */\n durable?: boolean;\n properties: PropertyDecl[];\n cards: Card<E>[];\n}\n\nexport interface Tag {\n id: string;\n gameId?: string;\n /**\n * This tag's own starting values for properties its GROUP declares\n * (design/hand-typing.md). The group says what the property IS; a tag says\n * only where it starts, so \"every zone has a haunting level\" is written once\n * and \"the cave starts at 2\" is written where it belongs.\n *\n * A name here that the group does not declare is an error: it would be a\n * value for nothing.\n */\n values?: Record<string, ScalarValue>;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n properties?: PropertyDecl[];\n /** Template-of-play extras (e.g. spatial geometry). Source only: preserved\n * in shards, never compiled into the bundle. */\n templates?: Record<string, unknown>;\n}\n\n/** A named axis for cross-cutting cards (schema 2.4, renamed from\n * Dimension). Tags are declared, not freeform. */\nexport interface TagGroup {\n id: string;\n gameId?: string;\n purpose?: string;\n /**\n * A property reference (`\"@story.act\"`) whose value names a tag in this group\n * by gameId. The engine reads it at every ask and binds the group, exactly as\n * if the asking hand had chosen that tag; a hand's own binding wins.\n *\n * For an axis driven by STATE rather than by place: acts, chapters, a\n * difficulty band. Without it, only a hand can bind a group, so such an axis\n * had nowhere to gate and every card needed its own condition.\n *\n * A reference rather than an expression on purpose (design/where-and-\n * selectors.md Part B): a computed binding belongs in a property the outcomes\n * maintain, and an expression here would make this type generic for no gain.\n */\n boundBy?: string;\n /**\n * What omitting this group means for a card. Default false: omission is a\n * wildcard, so the card matches whatever the group is bound to. True inverts\n * it, so a card that names no tag here is unavailable wherever the group IS\n * bound (and unaffected where it is not).\n *\n * `place` is the built-in instance of this pair: bound to the asking hand,\n * and inverted per card rather than per group.\n */\n required?: boolean;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n /**\n * Properties EVERY tag in this group has (design/hand-typing.md). The\n * declaration lives here and each tag carries only its own starting value in\n * `Tag.values`, which is the separation the format was missing: a tag's own\n * `properties` entry has to restate the type on every tag purely in order to\n * say the value, and a tag added later silently arrives without it.\n *\n * Compiled by FLATTENING onto each tag, so the bundle keeps its per-tag\n * shape and no runtime, port or bundle schema changes: source is where the\n * author works and where merges happen, the bundle is a compiled artefact\n * that can afford to be explicit.\n *\n * A tag may still declare its own `properties` for a group whose tags\n * genuinely differ. Declaring the same NAME both ways is an error.\n */\n properties?: PropertyDecl[];\n tags: Tag[];\n /** Template-of-play extras for the GROUP, the same bag its tags carry: this is\n * where a group is marked spatial and where that template keeps its own\n * group-level configuration. Source only, preserved but never compiled.\n *\n * A bag rather than a `spatial: true` flag because the marker and the\n * configuration are one thing (see model/spatial.ts), and because core is not\n * meant to grow a field per template of play. */\n templates?: Record<string, unknown>;\n}\n\n/** The reserved tag group (schema 2.4): present in every box without\n * declaration, its tags the box's hand ids. Every hand implicitly binds it to\n * itself; a card that names a place is available only at that place.\n *\n * Called `place` rather than `home` since 2026-08-21: one word for one thing\n * across the format, the editor and the exports. \"Where\" is the QUESTION a\n * card answers (at a place, or anywhere in a region); \"place\" is the direct\n * half of that answer. `home` was a metaphor an author had to learn, and it\n * leaked into hand-edited shards and the docs. */\nexport const PLACE_GROUP = \"place\";\n\n/** The scopes a movable hole may be filled from (design/engine-server.md 4.6):\n * the two `boundBy` already allows, plus `@hand` - the asking hand's OWN\n * declared property, resolved before tag composition so a movable hole can\n * never depend on the tags it is choosing. */\nexport type HoleRefScope = \"hand\" | \"story\" | \"world\";\n\n/** A parsed hole reference: `@hand.zone` -> `{ scope: \"hand\", name: \"zone\" }`. */\nexport interface HoleRef {\n scope: HoleRefScope;\n name: string;\n}\n\nconst HOLE_REF = /^@(hand|world|story)\\.([a-z][a-z0-9_-]*)$/;\n\n/**\n * Is this `chosen` / binding value MEANT as a property reference rather than a\n * tag id?\n *\n * The test is the leading `@` alone, deliberately: a value that starts with\n * one and does not parse is a mistyped reference, which the compiler should\n * name as such, not a tag id that happens to look odd. Tag ids never begin\n * with `@`.\n */\nexport const isHoleRef = (value: string): boolean => value.startsWith(\"@\");\n\n/** Parse a hole reference, or undefined when it is not one. The on-disk form\n * stays a plain string, so the canonical serialiser and the shard merge need\n * no change at all: a hole is still one group name against one value. */\nexport const parseHoleRef = (value: string): HoleRef | undefined => {\n const m = HOLE_REF.exec(value);\n return m === null ? undefined : { scope: m[1] as HoleRefScope, name: m[2]! };\n};\n\n/** A declared kind of hand (schema 2.6): live-inherited, author-side only,\n * never called from game code. One condition governs every instance. */\nexport interface HandTemplate<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order (sparse; one without it falls back to its id\n * position). Merges as a per-item value, so id-sorted storage stays\n * merge-clean (Reboot 7.4). */\n order?: number;\n\n /** Fixed tag bindings: tag group id -> tag id. Literal tags only: what a\n * template FIXES is the same for every instance, and a hole that moves is\n * the instance's own business (`Hand.chosen`, 4.6). */\n bindings?: Record<string, string>;\n /** The holes: tag group ids each instance fills (one tag each, or one\n * property reference: 4.6). */\n chooses?: string[];\n /** Shared availability condition, ANDed in (schema 3.1); evaluated per\n * instance against that instance's composed @hand. */\n condition?: E;\n /** Default slot cap. */\n slots: number | \"unbounded\";\n /** Declared @hand state every instance carries. */\n properties: PropertyDecl[];\n}\n\n/** A standalone hand's inline rule (schema 2.6): owned by the hand. */\nexport interface HandRule<E> {\n /**\n * Tag group id -> tag id, or a PROPERTY REFERENCE (`\"@hand.zone\"`,\n * `\"@story.where\"`, `\"@world.place\"`) the runtime resolves at ask time\n * (design/engine-server.md 4.6, the hand that moves). Still a plain string\n * on disk, so the canonical serialiser and the merge are untouched; what\n * widened is the meaning, and `parseHoleRef` is where it is read.\n *\n * `place` is never fillable this way: it is the hand's own name, not an axis.\n */\n bindings?: Record<string, string>;\n condition?: E;\n slots: number | \"unbounded\";\n}\n\n/** A hand (schema 2.6): a template instance (template + chosen) or a\n * standalone hand (rule). Exactly one of template / rule. Fully concrete:\n * deal is name-only. */\nexport interface Hand<E> {\n id: string;\n /** The name deal() is called with; a rename is a breaking change\n * (Reboot 7.4). */\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Hand template id (not gameId). */\n template?: string;\n /**\n * Template instances: tag group id -> tag id, one per `chooses` hole.\n *\n * A value may instead be a PROPERTY REFERENCE (`\"@hand.zone\"`,\n * `\"@story.where\"`, `\"@world.place\"`), which makes the hole MOVABLE: the\n * runtime resolves the reference at ask time and binds the hole to the tag\n * the value names, so moving the Elder to the forest is `setProperty` and\n * nothing else (design/engine-server.md 4.6). Still a plain string on disk,\n * so the canonical serialiser and the shard merge need no change; read it\n * with `parseHoleRef`.\n */\n chosen?: Record<string, string>;\n /** Standalone hands: the inline rule. */\n rule?: HandRule<E>;\n /** Override; defaults to the template's / rule's slots. The ONLY template\n * field an instance may override (schema 2.6). */\n slots?: number;\n /** Standalone hands' own @hand state (template instances inherit the\n * template's declarations). */\n properties?: PropertyDecl[];\n /** Authored display order within the box (sparse; authoring-only, never\n * compiled into the bundle - the compiler's explicit field list drops it). */\n order?: number;\n /** Template-of-play extras (e.g. a spatial pin). Source only. */\n templates?: Record<string, unknown>;\n}\n\nexport interface Box<E> {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** The only per-box ranking policy (Reboot 2.2). */\n ranking: { specificity: boolean };\n /**\n * A TIMED box: its clock counts real time, one turn every `seconds` of the\n * run (design/engine-server.md 4.8). Absent is the ordinary box, whose turn\n * is a play.\n *\n * Two things follow, and only two. In the ENGINE, a play in this box\n * defaults to advancing nothing: `settings.playAdvancesTurns` does not\n * apply, so a designer cannot declare the convention and then forget to\n * switch play-advance off. Everywhere else it is what the tools SAY: the\n * host ticks the box (the runtime has no clock and gains none here), and a\n * card's `redraw: N` reads as N x `seconds`, which the editors, the bundle\n * inspectors and the coverage report spell out rather than leaving a\n * designer to know that 30 meant minutes.\n *\n * The number itself is inert to the runtime, which never reads it.\n */\n turn?: { seconds: number };\n /** The card template: what every card in this box carries. */\n fields: FieldDecl[];\n /** What every outcome in this box may carry, declared the same way.\n * Absent when the box declares none, so a bundle without them is byte for\n * byte what it was. */\n outcomeFields?: FieldDecl[];\n properties: PropertyDecl[];\n tagGroups: TagGroup[];\n decks: Deck<E>[];\n handTemplates: HandTemplate<E>[];\n hands: Hand<E>[];\n}\n\n// --- the compiled bundle (.storyletsc) ---------------------------------------\n\nexport const BUNDLE_SCHEMA = \"storylets/bundle@0\";\n\n/** Binds bundles to shards (staleness gate) and saves to bundles. */\nexport interface BundleContent {\n project: string;\n version: string;\n /** hash32 over the canonical source shards (schema 2.8). */\n hash: string;\n}\n\nexport interface BundleSettings {\n playAdvancesTurns: number;\n}\n\n/**\n * The play ladder (design/engine-server.md 4.10): how much of itself\n * Storyletter shows this project, in one setting with three rungs rather than\n * a set of toggles, because the features nest.\n *\n * solo one player, one flow: no sharing, no durability, no venue features\n * shared several players over one world: sharing appears\n * venue a production: nothing is hidden\n *\n * EDITOR-SIDE ONLY. It stays in the project shard beside `coverage` and\n * `export` and is never compiled: a solo project plays on the same Engine as a\n * venue one. Hidden is hidden rather than disabled, so going DOWN a rung is\n * refused when the project already contains what the rung would hide, and a\n * hand-edited shard above its rung is a compile warning.\n */\nexport type PlayRung = \"solo\" | \"shared\" | \"venue\";\n\n/** The default rung: a project shard that says nothing is a solo game. */\nexport const DEFAULT_PLAY_RUNG: PlayRung = \"solo\";\n\n/** The project shard's settings block: what the bundle carries, plus the\n * authoring-side play rung that it does not. */\nexport interface ProjectSettings extends BundleSettings {\n /** The play ladder rung (see `PlayRung`). Absent = \"solo\". */\n play?: PlayRung;\n}\n\n/**\n * A map that a bundle was asked to carry: one spatial tag group's geometry,\n * flattened for a host to draw (design/graphical-views.md 2, \"The map MAY ship\").\n *\n * INERT PAYLOAD. Nothing in the engine reads this and nothing ever will: the\n * runtime deals in tag names. It is here so a host that wants an in-game map does\n * not have to invent its own export, and it is absent unless the project asked\n * for it (`export.map`), so a build that does not want a map carries no bytes.\n *\n * GAME IDS throughout, never internal ids. Internal ids are authoring identity\n * and mean nothing outside the project; a host matches these against the same\n * names it passes to `peek`. There is nothing here to strip either, which is why\n * `metadata: \"stripped\"` needs no special case: no titles, no purposes.\n *\n * SITES ARE HERE, which reverses a ruling. Until 2026-09-05 this comment said\n * they were deliberately not: a site was where an author parked a hand while\n * working, held in the view sidecar precisely because it was not content, and a\n * host that wanted to place a hand had its zone from the compiled binding. That\n * held for a game, where a hand's zone is its only real-world meaning. It does\n * not hold for a physical experience (design/engine-server.md 4.3), where the\n * position IS content: it is where the kiosk stands, and a producer's map is\n * simply wrong without it. The alternative was a second file beside the bundle,\n * which would cost a format the inspectors do not read and would put the view\n * sidecar in the shipping path by the back door.\n */\nexport interface BundleMap {\n /** The owning box, by gameId (tag groups are box-scoped). */\n box: string;\n /** The tag group this is a map of, by gameId. */\n group: string;\n /** One entry per zone that has been drawn; a tag with no polygon is not a\n * place yet and is left out rather than shipped as an empty shape. */\n zones: { tag: string; polygon: ViewPoint[] }[];\n /** Background pictures, back to front, as bundle-relative paths. Hidden ones\n * do not ship: what an author put away is not something to spring on a host. */\n backgrounds?: BundleBackground[];\n /** Where the placed hands stand on this map, by hand gameId, sorted by that\n * gameId so the bytes do not depend on authoring order. A hand nobody has\n * placed has no entry, and a map with no placed hand has no key at all. The\n * zone a site sits in is NOT repeated here: the hand's own binding is what\n * the runtime deals from, and a second copy could only go on to disagree. */\n sites?: { hand: string; x: number; y: number }[];\n}\n\n/** One shipped picture. `locked` and `hidden` are authoring state and do not\n * travel; the draw order is the array order. */\nexport interface BundleBackground {\n /** Where the file sits relative to the bundle (\"assets/<box>/<file>\"). */\n file: string;\n x: number;\n y: number;\n width: number;\n height: number;\n opacity?: number;\n}\n\nexport interface Bundle {\n schema: typeof BUNDLE_SCHEMA;\n content: BundleContent;\n metadata: \"full\" | \"stripped\";\n settings: BundleSettings;\n world: {\n properties: PropertyDecl[];\n /** ScopeRegistrySpec (@wildwinter/scoperegistry): the owned/foreign\n * split. Absent = engine-owned @world (standalone play). */\n registry?: unknown;\n };\n story: {\n properties: PropertyDecl[];\n };\n boxes: Box<Expression>[];\n /** Maps, when the project asked for them. Absent is the normal state. */\n maps?: BundleMap[];\n /** Other engines' game-wide scopes the content names (`patter`), sorted: the\n * family's shared vocabulary, let through unchecked by the compiler. The\n * engine reports when the game has not registered one. Absent when none. */\n externalScopes?: string[];\n}\n\n// --- the save envelope --------------------------------------------------------\n//\n// Version 2 (the one-registry model): property values are the game's\n// ScopeRegistry's, not the engine's. The envelope holds what is NOT a property\n// (boards, clocks, cooldowns, PRNGs, play logs, spent cards), plus, when the\n// engine made its own registry (a standalone game), that registry's values\n// under `registry`. A game that passed a registry saves it once itself.\n// Version 1 envelopes still load on every runtime: their property partitions\n// move into the registry under the keys below.\n//\n// Registry keys (identical on every runtime, since they are in the save):\n// `story` for the shared @story; `storylets/<kind>/<id>` for a shared box,\n// deck, hand, or value bag (kind is `box`, `deck`, `hand`, or `value`, id the\n// internal id); `storylets/flow/<flowId>/story` and\n// `storylets/flow/<flowId>/<kind>/<id>` for a flow's own. Ids escape `%` as\n// `%25` and `/` as `%2F`. A bag with no declared properties is not registered.\n// A self-backed @world (no resolver bound) is a stored property too, under\n// `world`.\n\nexport const SAVE_SCHEMA = \"storylets/save@2\";\n/** The version 1 envelope's schema tag, still read. */\nexport const SAVE_SCHEMA_V1 = \"storylets/save@1\";\n\nexport interface PlayRecord {\n /** Card and outcome by gameId (feeds the play-history functions). */\n card: string;\n /** \"\" for a card with no outcomes, played with none: the key is always\n * there, so a save's shape does not depend on the card. */\n outcome: string;\n turn: number;\n}\n\n/** A property bag: name -> value. */\nexport type PropertyBag = Record<string, ScalarValue>;\n\n/** The per-scope property partitions one side of the sharing flag holds:\n * a save carries one of these for the shared values and one per flow\n * (design/flows.md). NO world key, in either: @world is the game's own\n * state, resolved through the world resolver and saved by whoever owns\n * it - \"host saves its container once, each engine saves its own\n * envelope\" (engine-runtimes.md 3.1). */\nexport interface PropsPartition {\n story: PropertyBag;\n box: Record<string, PropertyBag>;\n deck: Record<string, PropertyBag>;\n hand: Record<string, PropertyBag>;\n /** Tag state, keyed by tag id. */\n value: Record<string, PropertyBag>;\n}\n\n/** One flow's snapshot inside the envelope (schema 4), and the blob\n * `saveFlow` parks. */\nexport interface FlowSave {\n /** The per-flow property partitions. Carried by `saveFlow`, which parks one\n * flow whole; absent from a version 2 envelope's flows, whose properties\n * are the registry's. Present in every flow of a version 1 envelope. */\n props?: PropsPartition;\n /** Per-box turn counters, keyed by box id (schema 3.4) - per flow: there\n * is deliberately no global turn. */\n turns: Record<string, number>;\n /** mulberry32 state, uint32 (schema 3.3), per flow. */\n prng: number;\n /** Absolute next-eligible turn (of the card's box's clock) per card id;\n * MAX_SAFE_INTEGER = never (deliberately not Infinity, which\n * JSON-serialises to null). */\n cooldowns: Record<string, number>;\n /** Hand contents (card ids, in dealt order), keyed by hand id. The claims\n * ledger is derived from this (schema 3.5). */\n board: Record<string, string[]>;\n playLog: PlayRecord[];\n}\n\n/** The whole engine, one envelope: the shared partitions once, then every\n * live flow keyed by its id - Patter's shape (one shared blob + N flow\n * blobs; multi-flow and save/load are the same feature). */\n/** The engine's half of a save: what every flow shares. Properties, and the\n * cards a shared `redraw: \"never\"` has taken out of the world for good\n * (design/shared-scarcity.md). Claims are NOT here: they are derived from the\n * live boards, and each flow's board rides its own blob. */\nexport interface SharedSave {\n /** The shared property partitions: version 1 only. */\n props?: PropsPartition;\n /** Card ids, sorted, so a save is byte-stable for a diff. */\n spent: string[];\n}\n\nexport interface SaveEnvelope {\n schema: typeof SAVE_SCHEMA;\n content: BundleContent;\n /** The engine's own registry's values, keyed by registry key: present only\n * when the engine made the registry itself (a standalone game). A game that\n * passed a registry saves it once, beside this envelope. */\n registry?: Record<string, PropertyBag>;\n shared: SharedSave;\n flows: Record<string, FlowSave>;\n}\n\n/** A version 1 envelope, from before the registry held the properties. Every\n * runtime still reads it: its partitions move into the registry as it loads. */\nexport interface SaveEnvelopeV1 {\n schema: typeof SAVE_SCHEMA_V1;\n content: BundleContent;\n shared: SharedSave & { props: PropsPartition };\n flows: Record<string, FlowSave & { props: PropsPartition }>;\n}\n\n// --- the load report (design/engine-server.md 4.9) ----------------------------\n//\n// `loadGame` is forgiving by design: a card the bundle no longer has drops off\n// the board, a property the save does not carry keeps its default, and a\n// version two builds newer loads without a word. That forgiveness is what makes\n// a save survive an edit, and it is also what hides the cost of a content\n// update from whoever is about to apply one. The report is the same walk,\n// itemised: `previewLoad` computes it and changes nothing, `loadGame` computes\n// it and applies it, and `previewFlowRestore` answers the same questions for\n// one flow (4.1's `openFlow(id, { restore })`).\n//\n// Card, hand and flow identities are GAME IDS: a report is host-facing and\n// internal ids mean nothing outside the project. The one exception is an\n// entity the edit DELETED - a vanished card, a vanished hand - which has no\n// gameId left to give, so the report carries the id the save itself carries.\n// There is nothing else to name it by.\n//\n// A property is named differently, and deliberately: by its ENGINE ADDRESS,\n// the string listProperties() prints and getProperty()/setProperty() accept.\n// A report entry is then something a host can act on rather than merely\n// print, and the runtimes have one property grammar instead of two.\n\n/** One card that a restore refused to put back on the board.\n *\n * `vanished` and `hand-vanished` are the edit's doing (the card, or the hand\n * it sat in, is no longer in the bundle). `claimed-elsewhere` is only ever a\n * single-flow restore into a LIVE engine: the card is shared, and the other\n * open flows already hold every copy the world has. */\nexport interface LoadEviction {\n flow: string;\n hand: string;\n card: string;\n reason: \"vanished\" | \"hand-vanished\" | \"claimed-elsewhere\";\n}\n\n/** One property the restore could not put back as it was. `flow` names the\n * flow whose half it belongs to; absent, it is the shared half.\n *\n * `path` is the engine's property address, spelled exactly as\n * `Flow.listProperties()` / `Engine.listProperties()` print it and exactly as\n * `getProperty` and `setProperty` accept it: `story.<name>` for the story\n * scope, `<scope>.<ownerGameId>.<name>` for the box, deck, hand and tag\n * scopes. No `@`, which belongs to the expression language and not to an\n * address.\n *\n * The owner segment is its GAMEID (design/engine-server.md 4.4), the name it\n * is called by everywhere else, so an operator reading a hot-swap report can\n * paste the address straight into `setProperty`. An owner the build no longer\n * has keeps the id the save carried: there is no gameId left to give it,\n * which is the rule the eviction list above has always used. */\nexport interface LoadProperty {\n flow?: string;\n path: string;\n}\n\n/** What a load or a flow restore would do that is not a plain restore\n * (design/engine-server.md 4.9). Arrays are sorted, so two runtimes given the\n * same save and bundle produce the same bytes; `flows` alone keeps the\n * envelope's own order, because a caller re-takes its handles in it. */\nexport interface LoadReport {\n /** No drift and nothing dropped, defaulted or retyped: the save goes back\n * exactly as it was. `flows` is not a divergence and does not count. */\n exact: boolean;\n project: string;\n /** Drift when the two differ; reported, never refused. */\n version: { saved: string; bundle: string };\n /** Drift when the two differ; reported, never refused. */\n hash: { saved: string; bundle: string };\n /** The flows this restores, in the order it restores them. */\n flows: string[];\n evicted: LoadEviction[];\n /** Cooldowns held for cards the bundle no longer has. */\n droppedCooldowns: { flow: string; card: string }[];\n /** Shared `redraw: \"never\"` entries for cards the bundle no longer has. */\n droppedSpent: string[];\n /** In the save, not declared any more. */\n droppedProperties: LoadProperty[];\n /** Declared, not in the save: it takes the declaration's default. */\n defaultedProperties: LoadProperty[];\n /** In the save, still declared, but the saved value no longer fits the\n * declaration (its type changed, or an enum value / quality stage was\n * edited away). It takes the declaration's default. */\n retypedProperties: LoadProperty[];\n}\n\n/** The .storyletsave FILE: the HOST's file, not the engine's - the engine's\n * envelope plus, when the host keeps one, its @world container. This is\n * \"host saves its container once, each engine saves its own envelope\"\n * folded into one file for the single-host case; the ENGINE never reads or\n * writes `world` (loadGame takes the envelope alone). */\nexport const SAVEFILE_SCHEMA = \"storylets/savefile@1\";\n\nexport interface SaveFile {\n schema: typeof SAVEFILE_SCHEMA;\n /** The engine's envelope: version 2 when written today, version 1 still read. */\n engine: SaveEnvelope | SaveEnvelopeV1;\n /** The host's @world values, saved and restored by the host. */\n world?: PropertyBag;\n}\n\n// --- source shards (design/storylets-source.md) --------------------------------\n\n/** The project folder: a macOS package, a plain folder elsewhere. */\nexport const PROJECT_FOLDER_EXTENSION = \".storylets\";\n/** The compiled bundle (strict JSON; generated, never hand-edited). */\nexport const BUNDLE_EXTENSION = \".storyletsc\";\n\n/**\n * Where a shipped background sits, relative to the bundle file.\n *\n * One function so the compiler (which writes the name into the bundle) and the\n * export op (which writes the bytes) cannot drift apart: a path agreed in two\n * places is a path that eventually disagrees. Per BOX, because two boxes may\n * each have their own `plan.png` and a build must not silently keep one of them.\n */\nexport const bundleAssetPath = (boxGameId: string, file: string): string =>\n `assets/${boxGameId}/${file}`;\n/** Per-type shard extensions, JSON5 inside (source doc section 2). */\nexport const SHARD_EXTENSIONS = {\n project: \".storyletproj\",\n box: \".storyletbox\",\n tags: \".storylettags\",\n hands: \".storylethands\",\n deck: \".storyletdeck\",\n /** The AUTHOR's arrangement layer: the canvases, where cards sit on a deck's\n * node canvas and the furniture drawn round them. Its own shard because\n * positions churn (an afternoon of tidying a canvas touches every card) and\n * content does not, so a designer arranging and a writer editing never\n * collide on one file (design/graphical-views.md section 1.2). */\n view: \".storyletview\",\n /** The DESIGNER's map: where a box's hands stand in space, and the furniture\n * round them. One per box, beside the view shard.\n *\n * Split out of the view shard on 2026-09-06 (design/engine-server.md 9.1\n * point 5) because the two halves stopped having one owner. A hand's\n * position ships in the bundle's `maps` block (4.3) and is where a venue's\n * kiosk stands, so it is SHAPE, which a server's author key may not change;\n * the canvases are the author's own working drawing and never leave the\n * project folder. One file could not be both. */\n map: \".storyletmap\",\n /** Threaded comments: content-ADJACENT, so neither in a content shard (a\n * writer's deck edit must not conflict with a reviewer's comment) nor in the\n * arrangement sidecar (this is not where anything sits). One per box,\n * id-keyed (design/annotation.md). Documentation NOTES used to share this\n * file and were retired: `purpose` already says why a thing exists, and\n * Patterpad's typed routing has no destination here. */\n notes: \".storyletnotes\",\n /** An installation contract: what a VENUE depends on, one file per\n * installation in `contracts/` at the project root\n * (design/engine-server.md 4.11). Its own shard, and its own folder, for the\n * walkthrough's reason (Reboot 7.5, S4): a different owner, a different\n * change rate, and a merge that must never collide with the author's edits,\n * since the server always wins its own file. */\n contract: \".storyletcontract\",\n} as const;\n\n/** Where the installation contracts live, relative to the project root. The\n * directory is the registry, as it is for a box's decks: a contract exists\n * because its file exists. */\nexport const CONTRACTS_DIR = \"contracts\";\n\nexport const PROJECT_SCHEMA = \"storylets/project@0\";\nexport const BOX_SCHEMA = \"storylets/box@0\";\nexport const TAGS_SCHEMA = \"storylets/tags@0\";\nexport const HANDS_SCHEMA = \"storylets/hands@0\";\nexport const DECK_SCHEMA = \"storylets/deck@0\";\nexport const VIEW_SCHEMA = \"storylets/view@0\";\nexport const MAP_SCHEMA = \"storylets/map@0\";\n/** The comment sidecar's schema. Still called \"notes\" on disk: the file already\n * held both, and renaming it would break every project for no gain. */\nexport const NOTES_SCHEMA = \"storylets/notes@0\";\nexport const CONTRACT_SCHEMA = \"storylets/contract@0\";\n\n/**\n * What one installation depends on, written by the venue's server and read by\n * `validate` (design/engine-server.md 4.11).\n *\n * NOT THE AUTHOR'S FILE. A venue is provisioned against names - the hands its\n * stations deal, the boxes its scheduler ticks, the properties its clocks drive,\n * the fields its crew read - and the server writes them out so the tools that\n * already gate a build can refuse a rename before it reaches the venue. A\n * project playing at two venues has two of these. The author never edits one,\n * and today, with no server built, a project either receives one or has none.\n *\n * NEVER COMPILED. It is project-side config like `coverage` and `export`: the\n * server does not need its own contract handed back, it needs the bundle to\n * still honour it.\n *\n * BY GAMEID throughout, because a gameId is the name that crosses the project's\n * border and an internal id is authoring identity.\n */\nexport interface ContractShard {\n schema: typeof CONTRACT_SCHEMA;\n /** The installation this contract speaks for. One file per installation, and\n * two files naming the same one is an error. */\n installation: string;\n /** Who wrote it, for a human reading the file (\"Storylet Server 0.1.0\"). */\n by?: string;\n /** The server's revision when it wrote this. */\n revision?: number;\n /** Hands a station is bound to, by gameId: they may not be renamed or\n * removed. */\n hands?: string[];\n /** Timed boxes the venue's scheduler ticks, by box gameId, with the turn unit\n * in SECONDS it was provisioned against. A box whose unit changed means every\n * rest on its cards changed meaning. */\n boxes?: Record<string, { turn: number }>;\n /** Property paths the venue reads or drives, in the engine's own address\n * grammar with no `@` (\"world.time_wall\", \"story.visits\"), which is how\n * `listProperties()` prints them. */\n properties?: ContractProperty[];\n /** Card-template field names the crew and the bridges read. */\n fields?: string[];\n /** Outcome field names they read, the same way: the after-line a station\n * shows when a press lands. What `fields` is to the card template, this is\n * to the box's `outcomeFields`. */\n outcomeFields?: string[];\n}\n\n/**\n * One contracted property.\n *\n * A bare path is the common form and the one the spec's example writes. The\n * object form adds the TYPE the venue was provisioned against, which is the only\n * way `validate` can catch the break that costs a producer most: a property that\n * still exists under the same name and now holds something else. A server that\n * knows the type should write the object form; a hand-written contract may say\n * only the path and get the existence check alone.\n */\nexport type ContractProperty = string | { path: string; type?: PropertyType };\n\n/** The path a contracted property names, whichever form it was written in. */\nexport const contractPropertyPath = (p: ContractProperty): string =>\n typeof p === \"string\" ? p : p.path;\n\n/** The type a contracted property was provisioned against, when it says. */\nexport const contractPropertyType = (p: ContractProperty): PropertyType | undefined =>\n typeof p === \"string\" ? undefined : p.type;\n\n/** A point in a canvas's own coordinates. */\nexport interface ViewPoint {\n x: number;\n y: number;\n}\n\n/**\n * Canvas furniture: what an author draws AROUND the content to make sense of it\n * (design/graphical-views.md 3, \"Frames and sites\").\n *\n * Both canvases carry the same thing, which is why they share a type: a node\n * canvas and a map are different views of different material, but \"put a box\n * round this lot and call it act two\" is the same thought on either.\n *\n * It lives in the view sidecar because it is ARRANGEMENT. Nothing here is\n * content: no runtime reads it, no bundle carries it, and deleting the sidecar\n * loses only the drawing. Threaded comments are the\n * other thing entirely - they attach to entities and they travel - but a canvas\n * DRAWS their markers, while owning none of them.\n */\nexport interface CanvasFurniture {\n /** Titled areas behind the content, back to front (see `stacked`).\n *\n * There was a second kind, a `stickies` list, retired on 2026-08-10\n * (design/annotation.md): a dropped comment marker does the same job in a\n * fraction of the space, and an annotation that takes as much room as the\n * thing it is about is a bad trade on a canvas. */\n frames?: Frame[];\n}\n\n/**\n * A titled area behind a group of things: Unreal's comment box.\n *\n * Deliberately dumb about what is inside it. It has no membership list and\n * computes none: a frame is a thing an author DREW, and the cards under it are\n * whatever happens to be under it now. That is what keeps it honest when content\n * moves, and it is the same reasoning that keeps a zone's sites out of the map's\n * sidecar.\n */\nexport interface Frame extends ViewPoint {\n id: string;\n w: number;\n h: number;\n /** Shown in the frame's bar, and the handle it is dragged by. */\n title?: string;\n /** One of the furniture palette's names (see `FURNITURE_COLOURS`); the theme\n * decides what that looks like, so a frame does not carry a hex value that\n * would fight the palette on the day somebody switches theme. */\n colour?: string;\n /** Place in the frame band (sparse, `stacked`). Frames can nest. */\n z?: number;\n}\n\n/** The furniture palette: names, not colours. The theme maps them, so the same\n * shard reads correctly on linen and on baize. */\nexport const FURNITURE_COLOURS = [\"paper\", \"amber\", \"sage\", \"sky\", \"rose\", \"slate\"] as const;\nexport type FurnitureColour = typeof FURNITURE_COLOURS[number];\n\n/** One deck's node canvas: where its cards sit, and the furniture around them.\n * Sparse throughout. A card with no entry lays out by default, and an entry for\n * a card that no longer exists is inert, so there is no referential integrity to\n * maintain against content that moves underneath. */\nexport interface DeckCanvas extends CanvasFurniture {\n /** Keyed by CARD id. */\n cards?: Record<string, ViewPoint>;\n}\n\n/** The box's map: where its hands sit in space, and the furniture around them.\n * Carried by the MAP shard since 2026-09-06; `ViewShard.map` is the old\n * address, read for one release and never written. */\nexport interface BoxMap extends CanvasFurniture {\n /** Keyed by HAND id. WHERE a site is, and nothing else.\n *\n * Which zone it is IN is not recorded here, and deliberately (2026-08-06,\n * with the rebinding drag): a hand that binds a zone already says so in its\n * own shard, as `chosen` or as a rule binding, and that is the truth the\n * runtime deals from. A copy here could only ever go on to disagree with it,\n * and a site whose recorded zone contradicts the hand it stands for would be\n * the most misleading thing on the map.\n *\n * Called `pins` until 2026-08-10 (design/annotation.md). No compatibility\n * branch: the only projects that exist are the examples in this repo, and they\n * were edited. */\n sites?: Record<string, ViewPoint>;\n}\n\n/** The AUTHOR's arrangement layer for one box: where cards sit on their decks'\n * canvases, and the furniture drawn round them.\n *\n * Its own shard on purpose (design/graphical-views.md section 1.2). Positions\n * churn, content does not: an afternoon of tidying a canvas touches every card,\n * and if that lived in the deck shard then a designer arranging and a writer\n * editing card text would collide on one file all day, while a content review\n * would be full of coordinates. Keyed by id throughout so the existing merge\n * engine handles two designers rearranging different things without a conflict.\n *\n * Source-only. It never reaches the compiled bundle, exactly as `order` does\n * not: the compiler reads the fields it names and this is not among them. */\nexport interface ViewShard {\n schema: typeof VIEW_SCHEMA;\n /** Keyed by DECK id: one node canvas each. */\n canvases?: Record<string, DeckCanvas>;\n /** @deprecated The box map's old address, kept for one release and READ ONLY.\n * A reader that meets it uses it when the box has no `MapShard`, and the\n * formatter moves it; nothing writes it any more. Removed after the next\n * release, at which point a map left here is simply lost. */\n map?: BoxMap;\n}\n\n/** The DESIGNER's map for one box: where its hands stand in space.\n *\n * Split out of the view shard on 2026-09-06 (design/engine-server.md 9.1 point\n * 5). The two halves had stopped sharing an owner: a hand's position ships in\n * the bundle's `maps` block (4.3), which makes it the thing a venue provisions\n * its kiosks against, while a deck's canvas is a working drawing that never\n * leaves the folder. A server's author key may change the canvases and not\n * this.\n *\n * The map is NESTED under `map` rather than flattened to the top level, and\n * deliberately: the block's bytes are then exactly what the view shard held, so\n * the migration is a move of a value rather than a reshaping of it, the merge\n * strategy carries over word for word, and a reader that has to look in both\n * places is one expression (`box.map?.map ?? box.view?.map`).\n *\n * Source-only in the sense the view shard is not: `compileMaps` reads the\n * positions for the bundle's `maps` block, under `export.map`. */\nexport interface MapShard {\n schema: typeof MAP_SCHEMA;\n map: BoxMap;\n}\n\n/** A coverage input driver: during a coverage run the harness feeds a\n * host-seam property (`@world.x`) values from `values`, so content gated on\n * external state gets exercised (Patter's coverageDrivers, carried whole). */\nexport interface CoverageDriver {\n /** \"initial\": set once as each playthrough starts. \"recurring\": re-rolled\n * per turn at the cadence, so one run passes through several states. */\n kind: \"initial\" | \"recurring\";\n /** For recurring drivers: how often to re-roll per turn (default \"sometimes\"). */\n cadence?: \"rarely\" | \"sometimes\" | \"often\";\n /** The pool the harness picks from (uniform). Empty = inert. */\n values: ScalarValue[];\n}\n\n/** Authoring-side coverage configuration (never compiled into the bundle). */\nexport interface CoverageConfig {\n /** Property drivers, keyed by ref (\"@world.danger\"). */\n drivers?: Record<string, CoverageDriver>;\n\n}\n\nexport interface ProjectShard {\n schema: typeof PROJECT_SCHEMA;\n project: {\n id: string;\n name: string;\n version: string;\n };\n settings: ProjectSettings;\n /** Coverage drivers + argument domains (authoring/testing config; stays\n * out of the compiled bundle). */\n coverage?: CoverageConfig;\n /** Validation switches (authoring config; never compiled). Off is written\n * as ABSENT, like `export.map`: a shard says what an author chose. */\n validation?: {\n /** Also warn when state is WRITTEN but nothing reads it. Off by default:\n * cards are routinely written ahead of the content that will read them,\n * so mid-development this warning is mostly noise. The read side (a gate\n * on state nothing writes) always warns, because that kills cards now. */\n warnUnreadWrites?: boolean;\n };\n world: {\n properties: PropertyDecl[];\n registry?: unknown;\n };\n story: {\n properties: PropertyDecl[];\n };\n /** Templates of play: configuration bags keyed by template name. Core\n * validates only what it knows. */\n templates: Record<string, unknown>;\n export: {\n bundle: string;\n metadata: \"full\" | \"stripped\";\n /**\n * Does a `.storyletpack` carry the boxes' binary assets (background images)?\n *\n * Default false, and a project-level DEFAULT rather than a rule: a pack is a\n * delivery, so the caller can override it per pack (2026-08-07). Some\n * projects would benefit from sending their pictures in certain\n * circumstances and others never would, which is why neither \"always\" nor\n * \"never\" is the answer.\n *\n * Nothing to do with the compiled bundle, which has its own switch: `map`.\n */\n packAssets?: boolean;\n /**\n * Does the compiled bundle carry the maps (zone shapes and background\n * pictures)?\n *\n * Default false, and the default matters: geometry is authoring data, the\n * runtime deals in tag names, and a shipping build should carry nothing it\n * does not use. But a host that wants an in-game map should not have to\n * invent its own export, and it is most useful early - a prototype with a\n * real map beats a prototype with a list of zone names.\n *\n * It sits beside `metadata` on purpose: that is already the switch for\n * \"authoring data that may or may not ship\", and this is its sibling rather\n * than a new concept. With it on, `export` also writes the background files\n * next to the bundle, and `describeBundle` says what is in there.\n */\n map?: boolean;\n };\n}\n\nexport interface BoxShard {\n schema: typeof BOX_SCHEMA;\n box: {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n /** Authored display order among boxes (sparse; absent falls back to the\n * folder-name position). Authoring-only, like a card's (never compiled\n * into the bundle); merges as a per-field value. */\n order?: number;\n ranking: { specificity: boolean };\n /** Declares a timed box (see `Box.turn`); compiled through unchanged. */\n turn?: { seconds: number };\n fields: FieldDecl[];\n /** The outcome fields (see `Box.outcomeFields`); a shard without the key\n * declares none. */\n outcomeFields?: FieldDecl[];\n properties: PropertyDecl[];\n };\n}\n\n/** The box's tag groups: how its cards are filed. */\nexport interface TagsShard {\n schema: typeof TAGS_SCHEMA;\n groups: TagGroup[];\n}\n\n/** The box's hand templates + hands (the writer/programmer contract). */\nexport interface HandsShard {\n schema: typeof HANDS_SCHEMA;\n templates: HandTemplate<string>[];\n hands: Hand<string>[];\n}\n\nexport interface DeckShard {\n schema: typeof DECK_SCHEMA;\n deck: {\n id: string;\n gameId?: string;\n title?: string;\n purpose?: string;\n condition?: string;\n /** Scarce across flows: see Deck.shared. */\n shared?: boolean;\n /** Its `redraw: \"never\"` cards are spent past the run: see Deck.durable. */\n durable?: boolean;\n /** Authored display order within the box (sparse; see BoxShard). */\n order?: number;\n properties: PropertyDecl[];\n };\n cards: Card<string>[];\n}\n\n// --- templates of play --------------------------------------------------------\n// The spatial template's types, field access and geometry. Re-exported here so the\n// package has one entry point, and kept in its own module because core schema and\n// a template of play are different things (Reboot 6).\nexport * from \"./spatial.js\";\n\n// How a hand reaches a tag group, and whether that binding is the hand's own to\n// change. Core schema rather than a template of play, but the map is what needed\n// it said out loud.\nexport * from \"./hands.js\";\n\n// Frames: what an author draws around the content. Arrangement,\n// so it lives in the sidecar and reads forgivingly (furniture.ts says why).\nexport * from \"./furniture.js\";\n\n// Threaded comments: the conversation about a thing, in its own sidecar.\nexport * from \"./comments.js\";\n\n// Guessing a property's type from what an outcome writes: the quick fix's input.\nexport * from \"./infer.js\";\n","// ---------------------------------------------------------------------------\n// Save-file plumbing over the .storyletsave file (storylets/savefile@1): the\n// HOST's file - the engine's envelope (storylets/save@2; @1 still read) plus,\n// when the host keeps one, its @world container. That is\n// \"host saves its container once, each engine saves its own envelope\"\n// (design/flows.md) folded into one file for the single-host case. These\n// helpers are the string boundary - a foreign or malformed blob throws\n// rather than corrupting a run.\n// ---------------------------------------------------------------------------\n\nimport { SAVEFILE_SCHEMA, SAVE_SCHEMA, SAVE_SCHEMA_V1 } from \"@storylet-studio/model\";\nimport type { PropertyBag, SaveFile } from \"@storylet-studio/model\";\nimport type { Engine } from \"@storylet-studio/runtime\";\n\n/** The current engine state (and the host's @world values, if given) as\n * pretty-printed .storyletsave JSON. */\nexport function serializeState(engine: Engine, world?: PropertyBag): string {\n return JSON.stringify(saveState(engine, world), null, 2);\n}\n\n/**\n * Capture the whole engine (and the host's @world values, if it keeps any) as\n * the tagged save-file OBJECT.\n *\n * Four verbs, in Patterplay's pairing (`patter` play-helpers `save.ts`, and\n * the same in all four of its runtimes): saveState / loadState work on the\n * PARSED object, serializeState / deserializeState work on TEXT.\n *\n * This reference had a different shape until 2026-08-29 - `deserializeState`\n * parsed and did not restore, `loadState` took text - so one name meant two\n * things across the four Storylets runtimes, and neither matched the family.\n * Godot and Unreal already had Patter's shape; these two were brought to it.\n */\nexport function saveState(engine: Engine, world?: PropertyBag): SaveFile {\n return {\n schema: SAVEFILE_SCHEMA,\n engine: engine.saveGame(),\n ...(world !== undefined ? { world } : {}),\n };\n}\n\n/** Restore a {@link saveState} file into an engine. EVERY FLOW IS REBUILT, so\n * the Flow handles you held before are inert: re-take them with\n * `engine.getFlow(id)`, NOT `engine.openFlow(id)`. `openFlow` on an existing\n * id REPLACES it, which here throws away the hand the file just restored, and\n * the failure lands later, as `play()` refusing a card as \"not dealt\". (The\n * engine's `onReplacedFlow` hook reports exactly this.) Throws on a foreign or malformed\n * file, and the runtime's own project check still applies. Returns the file's\n * @world values, if any - the HOST applies them to its container; the engine\n * never touches them. */\nexport function loadState(engine: Engine, file: SaveFile): PropertyBag | undefined {\n if (!file || typeof file !== \"object\"\n || file.schema !== SAVEFILE_SCHEMA\n || (file.engine?.schema !== SAVE_SCHEMA && file.engine?.schema !== SAVE_SCHEMA_V1)) {\n throw new Error(`not a storylets save (expected schema \"${SAVEFILE_SCHEMA}\")`);\n }\n engine.loadGame(file.engine);\n return file.world;\n}\n\n/** Parse + restore a {@link serializeState} string: the TEXT twin of\n * loadState, as Patterplay pairs them. Throws on malformed JSON, a foreign\n * file or a project mismatch. Returns the file's @world values for the host. */\nexport function deserializeState(engine: Engine, json: string): PropertyBag | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(\"not valid JSON\");\n }\n return loadState(engine, parsed as SaveFile);\n}\n","// ---------------------------------------------------------------------------\n// The property examiner/editor, JS idiom: a self-styled DOM panel (the\n// parity member Unity renders as an EditorWindow, Unreal as a Slate tab,\n// Godot as an in-game panel). Rows come from live().listProperties() and\n// are built once (declared properties are fixed for a bundle); values\n// refresh on a poll that SKIPS the focused widget; every row has a\n// reset-to-default that disables itself at the default. Edits commit via\n// flow.setProperty, which is a silent host write under the firing rule.\n// Save state / Load state carry the whole run over the .storyletsave string\n// boundary (save.ts); a filter narrows the property rows; the read-only\n// turns and board sections mirror the engine examiners (design 2.4).\n// The JS game runs in-process, so the engine and a flow are passed directly\n// (no debug registry needed here).\n//\n// The log panel (design 2.3: the flow's retained log surfaced in every\n// examiner; the old port's Unreal log panel is the high-water mark): the\n// lines of live().log() behind per-kind filters (a peek files under Deal -\n// both are asks), with Autoscroll, Copy and Clear. Empty until the engine\n// is created with the log option.\n// ---------------------------------------------------------------------------\n/// <reference lib=\"dom\" />\n\nimport type { Engine, EngineLogEntry, Flow, LogEntry, PropertyRow } from \"@storylet-studio/runtime\";\nimport type { ScalarValue } from \"@storylet-studio/model\";\nimport { serializeState, deserializeState } from \"./save.js\";\n\nexport interface PropertyInspectorOptions {\n /** Mount point; defaults to document.body. */\n container?: HTMLElement;\n title?: string;\n /** Value-refresh poll; 0 disables polling. */\n pollMs?: number;\n}\n\nexport interface PropertyInspector {\n el: HTMLElement;\n refresh(): void;\n destroy(): void;\n}\n\nconst STYLE_ID = \"sl-inspector-style\";\nconst CSS = `\n.sl-insp { font: 12px system-ui, sans-serif; color: var(--ink, #222); background: var(--surface, #fafafa);\n border: 1px solid var(--line, #ccc); border-radius: 8px; padding: 10px 12px; max-width: 26rem; }\n.sl-insp h3 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--muted, #666); }\n.sl-insp .sl-head { display: flex; align-items: baseline; gap: 6px; }\n.sl-insp .sl-head h3 { flex: 1; }\n.sl-insp .sl-save, .sl-insp .sl-load { font: inherit; font-size: 11px; padding: 1px 6px; cursor: pointer; }\n.sl-insp .sl-filter { display: block; width: 100%; box-sizing: border-box; margin: 0 0 6px; }\n.sl-insp .sl-group { margin: 8px 0 2px; font-weight: 600; font-size: 11px; color: var(--muted, #666); }\n.sl-insp .sl-section { margin: 10px 0 2px; font-weight: 600; font-size: 11px; text-transform: uppercase;\n letter-spacing: 0.06em; color: var(--muted, #666); }\n.sl-insp .sl-line { padding: 1px 0; }\n.sl-insp .sl-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; }\n.sl-insp .sl-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.sl-insp input[type=\"text\"], .sl-insp input[type=\"number\"], .sl-insp select {\n font: inherit; width: 9rem; padding: 1px 4px; }\n.sl-insp .sl-reset { border: 0; background: none; cursor: pointer; color: var(--muted, #666); }\n.sl-insp .sl-reset:disabled { opacity: 0.3; cursor: default; }\n.sl-insp .sl-logbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 2px 0; }\n.sl-insp .sl-logbar label { display: inline-flex; align-items: center; gap: 2px; }\n.sl-insp .sl-logbar button { font: inherit; font-size: 11px; padding: 1px 6px; cursor: pointer; }\n.sl-insp .sl-log { font-family: ui-monospace, monospace; font-size: 11px; max-height: 12rem;\n overflow: auto; white-space: pre; border: 1px solid var(--line, #ccc); padding: 4px 6px; }\n.sl-insp details.sl-fold > summary { cursor: pointer; margin: 10px 0 2px; font-weight: 600;\n font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted, #666); }\n.sl-insp .sl-ident { font-family: ui-monospace, monospace; font-size: 11px; }\n.sl-insp .sl-ident b { font-weight: 600; }\n.sl-insp .sl-note { color: var(--muted, #666); }\n`;\n\n/** Inject the shared panel stylesheet once. Exported so the bundle inspector\n * (bundle-inspector.ts) renders in the same CSS grammar. */\nexport function ensureInspectorStyle(): void {\n if (document.getElementById(STYLE_ID)) return;\n const style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = CSS;\n document.head.append(style);\n}\n\nconst eq = (a: ScalarValue | undefined, b: ScalarValue | undefined): boolean =>\n JSON.stringify(a) === JSON.stringify(b);\n\n// --- the log panel (design 2.3) ---------------------------------------------\n\n/** The filterable kinds; a peek files under \"deal\" (both are asks). */\nconst LOG_KINDS = [\"deal\", \"play\", \"write\", \"evict\", \"turns\", \"diagnostic\"] as const;\nconst LOG_KIND_LABELS: Record<(typeof LOG_KINDS)[number], string> = {\n deal: \"Deal\", play: \"Play\", write: \"Write\", evict: \"Evict\", turns: \"Turns\", diagnostic: \"Diag\",\n};\n\nconst logKindOf = (e: LogEntry): (typeof LOG_KINDS)[number] =>\n e.type === \"peek\" ? \"deal\" : e.type;\n\nconst showVal = (v: ScalarValue | undefined): string =>\n v === undefined ? \"<unset>\" : JSON.stringify(v);\n\n/** One line per entry, `[turn]`-stamped where the event has a box context\n * (write lines share the state logger's `path: from -> to` reading). */\nexport function formatLogEntry(e: LogEntry | EngineLogEntry): string {\n // The run's log names the flow that acted, after the turn stamp and in the\n // same place all four examiners put it; a flow's own log omits it, because\n // its section heading already says whose it is.\n return formatLogBody(e, \"flow\" in e && e.flow ? `${e.flow} ` : \"\");\n}\n\nfunction formatLogBody(e: LogEntry, flow: string): string {\n const stamp = (e.turn !== undefined ? `[${e.turn}] ` : \"[-] \") + flow;\n switch (e.type) {\n case \"deal\": {\n const dealt = e.cards.filter((c) => c.verdict === \"dealt\").map((c) => c.id);\n return `${stamp}deal ${e.hand}: ${dealt.length > 0 ? dealt.join(\", \") : \"(none)\"} (${e.cards.length} considered)`;\n }\n case \"peek\": {\n const crit = Object.entries(e.criteria).map(([g, t]) => `${g}=${t}`).join(\", \");\n const listed = e.cards.filter((c) => c.verdict === \"dealt\").map((c) => c.id);\n return `${stamp}peek ${e.box}${crit ? ` [${crit}]` : \"\"}: `\n + `${listed.length > 0 ? listed.join(\", \") : \"(none)\"} (${e.cards.length} considered)`;\n }\n case \"evict\": return `${stamp}evict ${e.card} from ${e.hand} (${e.reason})`;\n // A card played with none (\"\") has no outcome to name.\n case \"play\": return `${stamp}play ${e.card}${e.outcome === \"\" ? \"\" : ` -> ${e.outcome}`}`;\n case \"write\": return `${stamp}write ${e.path}: ${showVal(e.prev)} -> ${showVal(e.value)}`;\n case \"turns\": return `${stamp}turns ${e.box} -> ${e.turn}`;\n default: return `${stamp}diagnostic ${e.where}: ${e.message}`;\n }\n}\n\n/** The group label a row files under (\"world\", \"story\", \"box <id>\", ...). */\nconst groupOf = (path: string): string => {\n const parts = path.split(\".\");\n return parts.length === 3 ? `${parts[0]} ${parts[1]}` : parts[0]!;\n};\n\nexport function createPropertyInspector(engine: Engine, flow: Flow, opts: PropertyInspectorOptions = {}): PropertyInspector {\n ensureInspectorStyle();\n\n // loadGame rebuilds every flow and the handle we were given goes inert\n // (the runtime's stale-handle rule), so every read goes through this\n // accessor and Load state re-takes the same-named flow.\n let liveFlow = flow;\n const live = (): Flow => liveFlow;\n\n const el = document.createElement(\"div\");\n el.className = \"sl-insp\";\n\n // Header: the title plus Save state / Load state (the .storyletsave\n // string boundary, in every examiner - the parity rule, design 2.4).\n const head = document.createElement(\"div\");\n head.className = \"sl-head\";\n const h = document.createElement(\"h3\");\n h.textContent = opts.title ?? \"Runtime state\";\n head.append(h);\n\n const saveBtn = document.createElement(\"button\");\n saveBtn.type = \"button\";\n saveBtn.className = \"sl-save\";\n saveBtn.textContent = \"Save state\";\n saveBtn.addEventListener(\"click\", () => {\n const blob = new Blob([serializeState(engine)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = \"save.storyletsave\";\n a.click();\n URL.revokeObjectURL(url);\n });\n head.append(saveBtn);\n\n const filePicker = document.createElement(\"input\");\n filePicker.type = \"file\";\n filePicker.accept = \".storyletsave,application/json\";\n filePicker.hidden = true;\n filePicker.addEventListener(\"change\", () => {\n const file = filePicker.files?.[0];\n if (!file) return;\n const reader = new FileReader();\n reader.onload = () => {\n // A foreign or malformed blob is refused by deserializeState, never applied.\n try {\n deserializeState(engine, String(reader.result));\n liveFlow = engine.getFlow(flow.id) ?? engine.openFlow(flow.id);\n refresh();\n } catch (e) {\n console.error(\"storylets inspector: load failed:\", e instanceof Error ? e.message : e);\n }\n filePicker.value = \"\";\n };\n reader.readAsText(file);\n });\n\n const loadBtn = document.createElement(\"button\");\n loadBtn.type = \"button\";\n loadBtn.className = \"sl-load\";\n loadBtn.textContent = \"Load state\";\n loadBtn.addEventListener(\"click\", () => filePicker.click());\n head.append(loadBtn, filePicker);\n el.append(head);\n\n // The property filter (name/path substring, case-blind - the parity\n // member Unreal renders as an SSearchBox).\n const filter = document.createElement(\"input\");\n filter.type = \"text\";\n filter.className = \"sl-filter\";\n filter.placeholder = \"Filter properties\";\n el.append(filter);\n\n // Rows are built once: the declared surface is fixed for a bundle. The\n // filter only toggles visibility (a group hides with its last row).\n const editors: { row: PropertyRow; read: () => void }[] = [];\n const groups: { el: HTMLElement; rows: { el: HTMLElement; text: string }[] }[] = [];\n let lastGroup = \"\";\n for (const row of live().listProperties()) {\n const group = groupOf(row.path);\n if (group !== lastGroup) {\n const g = document.createElement(\"div\");\n g.className = \"sl-group\";\n g.textContent = group;\n el.append(g);\n groups.push({ el: g, rows: [] });\n lastGroup = group;\n }\n const rowEl = buildRow(live, row, editors);\n groups[groups.length - 1]!.rows.push({ el: rowEl, text: `${row.name} ${row.path}`.toLowerCase() });\n el.append(rowEl);\n }\n\n filter.addEventListener(\"input\", () => {\n const q = filter.value.trim().toLowerCase();\n for (const group of groups) {\n let any = false;\n for (const row of group.rows) {\n const show = q === \"\" || row.text.includes(q);\n row.el.style.display = show ? \"\" : \"none\";\n any = any || show;\n }\n group.el.style.display = any ? \"\" : \"none\";\n }\n });\n\n // Read-only: each box's clock, then the board's current hands (title or\n // gameId, never internal ids) - the same sections as the engine examiners.\n const turnsHead = document.createElement(\"div\");\n turnsHead.className = \"sl-section\";\n turnsHead.textContent = \"Turns (per box)\";\n const turnsBody = document.createElement(\"div\");\n turnsBody.className = \"sl-turns\";\n const boardHead = document.createElement(\"div\");\n boardHead.className = \"sl-section\";\n boardHead.textContent = \"Board\";\n const boardBody = document.createElement(\"div\");\n boardBody.className = \"sl-board\";\n el.append(turnsHead, turnsBody, boardHead, boardBody);\n\n // A retained log (design 2.3), behind per-kind filters, with Autoscroll,\n // Copy and Clear - the JS rendering of the engine examiners' log panel.\n // Built twice: once for the RUN (every flow's events in one order, each line\n // naming its flow) and once for this flow's own. Both exist because a flow's\n // log cannot show a story action in another flow moving shared state\n // (design/shared-scarcity.md 8.2). Empty until the engine had the log option.\n const check = (text: string, onChange: (on: boolean) => void, cls: string): HTMLLabelElement => {\n const label = document.createElement(\"label\");\n label.className = cls;\n const box = document.createElement(\"input\");\n box.type = \"checkbox\";\n box.checked = true;\n box.addEventListener(\"change\", () => onChange(box.checked));\n label.append(box, text);\n return label;\n };\n\n interface LogPanel { render: (force?: boolean) => void }\n const buildLogPanel = (\n which: \"flow\" | \"run\",\n caption: string,\n entriesOf: () => readonly (LogEntry | EngineLogEntry)[],\n clear: () => void,\n empty: string,\n ): LogPanel => {\n // Both panels carry the same controls, so each element takes a `sl-flow` /\n // `sl-run` modifier: without one, a selector for \"the Clear button\" is\n // ambiguous, which is exactly what the inspector test caught.\n const head = document.createElement(\"div\");\n head.className = `sl-section sl-${which}`;\n head.textContent = caption;\n const bar = document.createElement(\"div\");\n bar.className = `sl-logbar sl-${which}`;\n const body = document.createElement(\"div\");\n body.className = `sl-log sl-${which}`;\n\n const kindOn = new Map<string, boolean>();\n const visibleLines = (): string[] =>\n entriesOf().filter((e) => kindOn.get(logKindOf(e)) !== false).map(formatLogEntry);\n let autoscroll = true;\n let stampSeen = \"\";\n const render = (force = false): void => {\n const entries = entriesOf();\n const stamp = `${entries.length}:${entries.length > 0 ? entries[entries.length - 1]!.seq : -1}`;\n if (!force && stamp === stampSeen) return;\n stampSeen = stamp;\n const lines = visibleLines();\n body.textContent = lines.length > 0 ? lines.join(\"\\n\") : empty;\n if (autoscroll) body.scrollTop = body.scrollHeight;\n };\n\n for (const kind of LOG_KINDS) {\n kindOn.set(kind, true);\n bar.append(check(LOG_KIND_LABELS[kind], (on) => {\n kindOn.set(kind, on);\n render(true);\n }, \"sl-logkind\"));\n }\n bar.append(check(\"Autoscroll\", (on) => { autoscroll = on; }, \"sl-logscroll\"));\n const copyBtn = document.createElement(\"button\");\n copyBtn.type = \"button\";\n copyBtn.className = \"sl-logcopy\";\n copyBtn.textContent = \"Copy\";\n copyBtn.title = \"Copy the visible (filtered) log to the clipboard\";\n copyBtn.addEventListener(\"click\", () => { void navigator.clipboard?.writeText(visibleLines().join(\"\\n\")); });\n const clearBtn = document.createElement(\"button\");\n clearBtn.type = \"button\";\n clearBtn.className = \"sl-logclear\";\n clearBtn.textContent = \"Clear\";\n clearBtn.title = \"Drop the retained log entries (cosmetic - no game state changes)\";\n clearBtn.addEventListener(\"click\", () => { clear(); render(true); });\n bar.append(copyBtn, clearBtn);\n el.append(head, bar, body);\n return { render };\n };\n\n // This flow's own log first: it is what the panel was mounted on. The run's\n // log follows, because it is the wider view and it only earns its space once\n // a second flow exists.\n const flowLog = buildLogPanel(\"flow\", \"Log\", () => live().log(), () => live().clearLog(),\n \"(empty - new Engine(bundle, { log: true }) retains the flow log)\");\n const runLog = buildLogPanel(\"run\", \"Run log (every flow)\", () => engine.log(), () => engine.clearLog(),\n \"(empty - new Engine(bundle, { log: true }) retains the run log)\");\n const renderLog = (force = false): void => { runLog.render(force); flowLog.render(force); };\n\n const line = (parent: HTMLElement, text: string): void => {\n const div = document.createElement(\"div\");\n div.className = \"sl-line\";\n div.textContent = text;\n parent.append(div);\n };\n const readLive = (): void => {\n turnsBody.textContent = \"\";\n for (const box of live().listBoxes()) {\n line(turnsBody, `${box.title ?? box.gameId}: turn ${box.turn}`);\n }\n boardBody.textContent = \"\";\n for (const [hand, cards] of Object.entries(live().board())) {\n const names = cards.map((c) => c.title ?? c.gameId);\n line(boardBody, `${hand}: ${names.length > 0 ? names.join(\", \") : \"(empty)\"}`);\n }\n };\n\n const refresh = (): void => {\n for (const e of editors) e.read();\n readLive();\n renderLog();\n };\n readLive();\n renderLog(true);\n\n let timer: ReturnType<typeof setInterval> | undefined;\n const pollMs = opts.pollMs ?? 250;\n if (pollMs > 0) timer = setInterval(refresh, pollMs);\n\n (opts.container ?? document.body).append(el);\n return {\n el,\n refresh,\n destroy(): void {\n if (timer !== undefined) clearInterval(timer);\n el.remove();\n },\n };\n}\n\nfunction buildRow(\n live: () => Flow,\n row: PropertyRow,\n editors: { row: PropertyRow; read: () => void }[],\n): HTMLElement {\n const div = document.createElement(\"div\");\n div.className = \"sl-row\";\n const name = document.createElement(\"span\");\n name.className = \"sl-name\";\n name.textContent = row.name;\n name.title = row.path;\n\n const current = (): ScalarValue | undefined => {\n try { return live().getProperty(row.path); } catch { return undefined; }\n };\n // Forward-declared so commit can refresh the whole row (widget + reset\n // state) once everything below is built.\n let sync: () => void = () => {};\n const commit = (value: ScalarValue): void => { live().setProperty(row.path, value); sync(); };\n\n const reset = document.createElement(\"button\");\n reset.type = \"button\";\n reset.className = \"sl-reset\";\n reset.textContent = \"↺\";\n reset.title = \"Reset to default\";\n reset.addEventListener(\"click\", () => commit(row.default));\n\n let widget: HTMLElement;\n let read: () => void;\n const focused = (w: HTMLElement): boolean => document.activeElement === w;\n\n switch (row.type) {\n case \"boolean\": {\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.addEventListener(\"change\", () => commit(input.checked));\n widget = input;\n read = () => { if (!focused(input)) input.checked = current() === true; };\n break;\n }\n case \"number\": {\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.addEventListener(\"change\", () => commit(Number(input.value)));\n widget = input;\n read = () => { if (!focused(input)) input.value = String(current() ?? 0); };\n break;\n }\n case \"enum\":\n case \"quality\": {\n // A quality edits as a dropdown of its STAGE LADDER, closed exactly like an\n // enum's values. It fell to the string branch until 2026-09-01, so a free-text\n // box accepted any stage name at all - and an unknown stage is not a harmless\n // typo: the evaluator refuses it (\"X is not a stage of this quality\"), so a\n // slip here broke play rather than being corrected. listProperties has carried\n // `stages` for this since it was written; nothing consumed it.\n const select = document.createElement(\"select\");\n for (const v of (row.type === \"quality\" ? row.stages : row.values) ?? []) {\n const o = document.createElement(\"option\");\n o.value = v;\n o.textContent = v;\n select.append(o);\n }\n select.addEventListener(\"change\", () => commit(select.value));\n widget = select;\n read = () => { if (!focused(select)) select.value = String(current() ?? \"\"); };\n break;\n }\n case \"flags\": {\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.placeholder = \"comma, separated, flags\";\n input.addEventListener(\"change\", () =>\n commit(input.value.split(\",\").map((s) => s.trim()).filter((s) => s.length > 0)));\n widget = input;\n read = () => { if (!focused(input)) input.value = ((current() as string[] | undefined) ?? []).join(\", \"); };\n break;\n }\n default: { // string\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.addEventListener(\"change\", () => commit(input.value));\n widget = input;\n read = () => { if (!focused(input)) input.value = String(current() ?? \"\"); };\n }\n }\n\n const readAll = (): void => { read(); reset.disabled = eq(current(), row.default); };\n sync = readAll;\n readAll();\n editors.push({ row, read: readAll });\n div.append(name, widget, reset);\n return div;\n}\n","// ---------------------------------------------------------------------------\n// The bundle inspector, JS idiom (design/engine-runtimes.md 2, piece 6).\n//\n// describeBundle() IS the JS half of the parity member - JS has no engine\n// asset pipeline to hang an editor view off - so this is the optional DOM\n// rendering of it: a read-only panel over a compiled bundle, in the property\n// examiner's CSS grammar (inspector.ts), with NO session anywhere. Identity\n// first, then collapsible sections: hands (the deal() surface), tags by box\n// (the peek() criteria surface), declared properties, counts.\n//\n// Read-only by construction: there is no state to edit here, only the shape\n// that shipped.\n// ---------------------------------------------------------------------------\n/// <reference lib=\"dom\" />\n\nimport { describeBundle } from \"@storylet-studio/runtime\";\nimport type {\n BundleDescription, PropertyScopeSummary, PropertySummary,\n} from \"@storylet-studio/runtime\";\nimport type { Bundle, ScalarValue } from \"@storylet-studio/model\";\nimport { ensureInspectorStyle } from \"./inspector.js\";\n\nexport interface BundleInspectorOptions {\n /** Mount point; defaults to document.body. */\n container?: HTMLElement;\n title?: string;\n /** Start the collapsible sections open (default true). */\n open?: boolean;\n}\n\nexport interface BundleInspector {\n el: HTMLElement;\n /** The description this panel rendered (the API is the parity member). */\n description: BundleDescription;\n destroy(): void;\n}\n\nconst showVal = (v: ScalarValue | undefined): string =>\n v === undefined ? \"<unset>\" : JSON.stringify(v);\n\n/** \"name: type = default\", plus enum/flags options where declared, plus\n * \"(durable)\" where the declaration says the value outlives a run\n * (design/engine-server.md 4.2). Nothing is added for the ordinary\n * run-scoped property: that is what a property is. */\nexport function formatPropertySummary(p: PropertySummary): string {\n const options = p.values !== undefined && p.values.length > 0 ? ` [${p.values.join(\", \")}]` : \"\";\n const durable = p.durable === true ? \" (durable)\" : \"\";\n return `${p.name}: ${p.type} = ${showVal(p.default)}${options}${durable}`;\n}\n\n/** The scope label a declaration block files under (\"world\", \"box box\",\n * \"tag docks (zone)\"). */\nexport function formatScopeLabel(scope: PropertyScopeSummary): string {\n if (scope.scope === \"world\" || scope.scope === \"story\") return scope.scope;\n const group = scope.group !== undefined ? ` (${scope.group})` : \"\";\n return `${scope.scope} ${scope.owner}${group}`;\n}\n\nconst line = (parent: HTMLElement, text: string, cls = \"sl-line\"): HTMLElement => {\n const div = document.createElement(\"div\");\n div.className = cls;\n div.textContent = text;\n parent.append(div);\n return div;\n};\n\n/** A collapsible section in the examiner's grammar. */\nconst fold = (parent: HTMLElement, label: string, open: boolean): HTMLElement => {\n const details = document.createElement(\"details\");\n details.className = \"sl-fold\";\n details.open = open;\n const summary = document.createElement(\"summary\");\n summary.textContent = label;\n details.append(summary);\n const body = document.createElement(\"div\");\n details.append(body);\n parent.append(details);\n return body;\n};\n\n/** Render a read-only summary of a compiled bundle: what an integrator may\n * call, with no session and no game running. */\nexport function createBundleInspector(\n bundle: Bundle,\n opts: BundleInspectorOptions = {},\n): BundleInspector {\n ensureInspectorStyle();\n const description = describeBundle(bundle);\n const open = opts.open ?? true;\n\n const el = document.createElement(\"div\");\n el.className = \"sl-insp\";\n\n const head = document.createElement(\"div\");\n head.className = \"sl-head\";\n const h = document.createElement(\"h3\");\n h.textContent = opts.title ?? \"Bundle\";\n head.append(h);\n el.append(head);\n\n // --- identity (always visible: which bundle is this?) --------------------\n const { identity, totals } = description;\n const ident = document.createElement(\"div\");\n ident.className = \"sl-ident\";\n el.append(ident);\n line(ident, `${identity.project} ${identity.version}`, \"sl-line\");\n line(ident, `schema ${identity.schema}`, \"sl-line sl-note\");\n line(ident, `hash ${identity.hash === \"\" ? \"(none)\" : identity.hash} - metadata ${identity.metadata}`,\n \"sl-line sl-note\");\n\n // --- hands: the deal() surface -------------------------------------------\n const handsBody = fold(el, \"Hands (deal)\", open);\n handsBody.className = \"sl-hands\";\n if (description.hands.length === 0) {\n line(handsBody, \"(no hands - this bundle is peek-only)\", \"sl-line sl-note\");\n }\n for (const hand of description.hands) {\n const template = hand.template !== undefined ? `, template ${hand.template}` : \"\";\n // A movable hole is the one thing about a hand its name cannot say: write\n // that property and the hand moves (4.6).\n const moves = hand.movable === undefined ? \"\"\n : `, moves ${hand.movable.map((m) => `${m.group} from ${m.from}`).join(\" and \")}`;\n line(handsBody, `${hand.gameId}: box ${hand.box}, slots ${hand.slots}${template}${moves}`\n + (hand.title !== undefined ? ` - ${hand.title}` : \"\"));\n }\n\n // --- tag groups by box: the peek() criteria surface ----------------------\n const tagsBody = fold(el, \"Tags by box (peek criteria)\", open);\n tagsBody.className = \"sl-tags\";\n for (const box of description.boxes) {\n line(tagsBody, `${box.title ?? box.gameId}`, \"sl-group\");\n if (box.tagGroups.length === 0) {\n line(tagsBody, \" (no tag groups)\", \"sl-line sl-note\");\n }\n for (const group of box.tagGroups) {\n line(tagsBody, ` ${group.gameId}: ${group.tags.length > 0 ? group.tags.join(\", \") : \"(no tags)\"}`);\n }\n }\n\n // --- declared properties: what expressions read, what a host may set ----\n const propsBody = fold(el, \"Properties (declared)\", open);\n propsBody.className = \"sl-props\";\n for (const scope of description.properties) {\n line(propsBody, formatScopeLabel(scope), \"sl-group\");\n if (scope.properties.length === 0) {\n line(propsBody, \" (none declared)\", \"sl-line sl-note\");\n }\n for (const p of scope.properties) {\n line(propsBody, ` ${formatPropertySummary(p)}`);\n }\n }\n\n // --- maps: inert payload, and therefore worth saying out loud -----------\n //\n // Only when there ARE some. An empty section on every ordinary bundle would\n // teach the reader to skip a section that only ever matters when it is not\n // empty, and most bundles carry no geometry at all.\n if (description.maps.length > 0) {\n const mapsBody = fold(el, \"Maps (carried, not read)\", open);\n mapsBody.className = \"sl-maps\";\n line(mapsBody, \"Geometry the build was asked to carry. The engine ignores it.\", \"sl-line sl-note\");\n for (const map of description.maps) {\n line(mapsBody, `${map.box} - ${map.group}: zones ${map.zones}, pictures ${map.backgrounds}, sites ${map.sites}`);\n }\n }\n\n // --- counts: orientation, not inventory ---------------------------------\n const countsBody = fold(el, \"Counts\", open);\n countsBody.className = \"sl-counts\";\n line(countsBody, `boxes ${totals.boxes} - decks ${totals.decks} - cards ${totals.cards}`);\n line(countsBody, `hands ${totals.hands} - templates ${totals.templates} - tag groups ${totals.tagGroups}`);\n for (const box of description.boxes) {\n // A timed box says its unit here (design/engine-server.md 4.8), because\n // this is the line an integrator reads to find out what their host has to\n // tick. Nothing is added for an ordinary box: the answer \"a turn is a\n // play\" belongs in the docs, not on every line of every bundle.\n line(countsBody, `${box.gameId}: decks ${box.counts.decks}, cards ${box.counts.cards}, `\n + `hands ${box.counts.hands}, templates ${box.counts.templates}, `\n + `tag groups ${box.counts.tagGroups}, ranking.specificity ${box.ranking.specificity}`\n + (box.turn !== undefined ? `, turn = ${box.turn.seconds}s` : \"\")\n // Only when there are any: a box whose cards all come back with the run\n // has nothing for a server to lift, and the zero would be noise.\n + (box.durableCards !== undefined ? `, durable cards ${box.durableCards}` : \"\"));\n }\n\n (opts.container ?? document.body).append(el);\n return {\n el,\n description,\n destroy(): void {\n el.remove();\n },\n };\n}\n","// ---------------------------------------------------------------------------\n// Live Link (design/live-link.md): the game-side client.\n//\n// Joins a running game to Storyletter over a loopback WebSocket. Two things\n// travel on it: the flow's trace stream and board snapshots go UP, so the\n// editor's Board can show the game's run instead of its own (observe-only: the\n// editor never drives the game); freshly compiled bundles come DOWN after a\n// save, so the run picks up the edit without restarting (applyLiveBundle in\n// refresh.ts does the swap).\n//\n// Wire protocol `storyletengine/debug@1` (one JSON object per message):\n// hello : { t:\"hello\", v:2, build, project?, boxes?, flows:[id...] }\n// - on open, and again on setBuild\n// flowOpen / flowClose : { t:\"flowOpen\"|\"flowClose\", flow }\n// - a flow appeared or went\n// trace : { t:\"trace\", flow, event } - every TraceEvent any flow emits\n// board : { t:\"board\", flow, hands:{ hand: [card...] }, turns:{ box: n } }\n// - after hello, and after every deal /\n// play / evict / turns event\n// bundle: { t:\"bundle\", v:1, build, data } - EDITOR -> game: the full .storyletsc\n// JSON as a string\n// Identity in frames is by gameId (hands, boxes, cards), and since 4.4 that\n// holds for the trace event too: it is forwarded verbatim, and the runtime's\n// own ids are gameIds now, so the rule has no exception left.\n//\n// Patterpad's createDebugLink is the template (Patter play-helpers/debug.ts):\n// hello first, frames queue until the socket opens, a missing editor is a\n// silent no-op, nothing here ever throws into the game, and no WebSocket\n// implementation at all degrades to a no-op handle. `observe(...)` became a\n// trace subscription, which is why this one takes an ENGINE: attach(engine)\n// subscribes, detach() stops, and a live refresh replaces the flow\n// (detach the old one, attach the new one, then setBuild).\n//\n// const link = createLiveLink({ build: bundle.content.hash, onBundle: ... });\n// link.attach(engine); // the ENGINE: the link discovers your flows itself\n// ---------------------------------------------------------------------------\n\nimport type { Engine, Flow, TraceEvent } from \"@storylet-studio/runtime\";\n\n/** A minimal structural type for a WebSocket implementation (browsers and\n * Node 22+ have a global one). */\nexport interface LiveSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n addEventListener(type: \"open\" | \"close\" | \"error\", listener: () => void): void;\n /** Incoming editor messages (the pushed bundle). Optional so a bare\n * send-only socket still fits. */\n addEventListener(type: \"message\", listener: (ev: { data: unknown }) => void): void;\n}\ntype LiveSocketCtor = new (url: string) => LiveSocketLike;\n\nexport interface LiveLinkOptions {\n /** The running bundle's build identity: pass `bundle.content.hash`. The\n * editor compares it with its own compiled hash (in sync / stale). */\n build: string;\n /** Optional project name, shown in the editor's connect-chip tooltip. */\n project?: string;\n /** Editor WebSocket URL. Default `ws://127.0.0.1:4472`. */\n url?: string;\n /** A WebSocket constructor to use instead of the global one (tests with a\n * fake socket, or a host without a global WebSocket). */\n WebSocket?: LiveSocketCtor;\n /** Live refresh: the editor pushed a freshly compiled bundle. `data` is\n * the .storyletsc JSON; hand it (with your current Engine) to\n * `applyLiveBundle`, `attach` the engine it returns, then call\n * `link.setBuild(build)`. Never called with a malformed frame. */\n onBundle?: (msg: { build: string; data: string }) => void;\n}\n\nexport interface LiveLink {\n /** Start forwarding this ENGINE's trace: every flow's events, each frame\n * naming the flow it came from, so the editor can follow one participant\n * and switch. An earlier engine is detached first. Sends a board snapshot\n * per open flow straight away, queued behind the hello if the socket is\n * not open yet.\n *\n * Flows are discovered rather than declared: the link diffs `engine.flows()`\n * whenever anything happens and emits `flowOpen` / `flowClose` itself. That\n * is a deliberate departure from Patterplay, whose host calls `FlowOpened`\n * by hand - it has no engine-level trace tap to hang the diff on and we do,\n * so the host has nothing to remember and cannot get the editor's flow list\n * wrong. The one cost: a flow that opens and then does nothing at all is not\n * announced until the next event anywhere in the run. */\n attach(engine: Engine): void;\n /** Stop forwarding. A refresh replaces the engine, so attach the new one\n * afterwards. */\n detach(): void;\n /** After applying a pushed bundle: report the build now running (re-hellos\n * with the new build and a fresh board snapshot, so the editor's chip goes\n * back to in sync and it stops re-pushing the same bundle). */\n setBuild(build: string): void;\n /** Close the link; every later call is a no-op. */\n close(): void;\n}\n\n/** One game-to-editor frame, as the client serialises it. Exported for the\n * fixture test; hosts never build these by hand. */\nexport type LiveFrame =\n | { t: \"hello\"; v: 2; build: string; project?: string; boxes?: string[]; flows: string[] }\n | { t: \"flowOpen\"; flow: string }\n | { t: \"flowClose\"; flow: string }\n | { t: \"trace\"; flow: string; event: TraceEvent }\n | { t: \"board\"; flow: string; hands: Record<string, string[]>; turns: Record<string, number> };\n\nconst OPEN = 1; // WebSocket.OPEN\n\n/** How many frames may wait for a socket that has not opened yet. Generous:\n * the point of queueing is that a game's first moments are not lost while the\n * editor's socket is still connecting. */\nconst QUEUE_CAP = 512;\nconst DEFAULT_URL = \"ws://127.0.0.1:4472\";\n\n/** The trace kinds that move the board, and so are followed by a snapshot. */\nconst BOARD_EVENTS: ReadonlySet<TraceEvent[\"type\"]> = new Set([\"deal\", \"play\", \"evict\", \"turns\"]);\n\n/** The cheap snapshot: hands by gameId holding card gameIds in dealt order,\n * and every box's clock by gameId. */\nexport function boardFrame(flow: Flow): Extract<LiveFrame, { t: \"board\" }> {\n const id = flow.id;\n const hands: Record<string, string[]> = {};\n for (const [hand, cards] of Object.entries(flow.board())) hands[hand] = cards.map((c) => c.gameId);\n const turns: Record<string, number> = {};\n for (const box of flow.listBoxes()) turns[box.gameId] = box.turn;\n return { t: \"board\", flow: id, hands, turns };\n}\n\n/**\n * Open a Live Link to Storyletter. Returns a handle whose calls are no-ops once\n * the editor disconnects or if it was never listening: safe to leave wired into\n * a shipping build behind a flag.\n */\nexport function createLiveLink(opts: LiveLinkOptions): LiveLink {\n const url = opts.url ?? DEFAULT_URL;\n const Ctor: LiveSocketCtor | undefined = opts.WebSocket ?? (globalThis as { WebSocket?: LiveSocketCtor }).WebSocket;\n let queue: string[] = [];\n let sock: LiveSocketLike | null = null;\n let closed = false;\n let build = opts.build; // mutable: setBuild() after a live refresh lands\n let engine: Engine | null = null;\n let unsubscribe: (() => void) | null = null;\n // The flows the EDITOR believes are open. Diffed against engine.flows() so\n // flowOpen / flowClose are the link's own business, not the host's.\n let announced = new Set<string>();\n\n if (!Ctor) {\n // No WebSocket available (no global, none passed): a silent no-op link.\n return { attach() {}, detach() {}, setBuild() {}, close() { closed = true; } };\n }\n\n const flush = (): void => {\n if (!sock || sock.readyState !== OPEN) return;\n for (const m of queue) { try { sock.send(m); } catch { /* socket went away */ } }\n queue = [];\n };\n const post = (frame: LiveFrame): void => {\n if (closed) return;\n queue.push(JSON.stringify(frame));\n // A cap for the CONNECTING window, where queueing is the point: the hello\n // and the frames a game emits during those first milliseconds have to\n // land. Beyond that many, the editor is not coming - drop the oldest, so\n // what survives is the most recent story rather than the first moments of\n // it. Once the socket has actually closed the queue is dropped outright\n // (see the close listener); this is only the never-opened case.\n if (queue.length > QUEUE_CAP) queue.splice(0, queue.length - QUEUE_CAP);\n flush();\n };\n\n // The handshake goes straight to the socket, never through the queue: it\n // must be the first thing the editor reads, ahead of anything queued while\n // the socket was still connecting.\n const liveFlows = (): Flow[] => {\n try { return engine ? engine.flows() : []; } catch { return []; }\n };\n const sendHello = (): void => {\n const flows = liveFlows();\n const hello: LiveFrame = { t: \"hello\", v: 2, build, flows: flows.map((f) => f.id) };\n if (opts.project !== undefined) hello.project = opts.project;\n const first = flows[0];\n if (first) {\n try { hello.boxes = first.listBoxes().map((b) => b.gameId); } catch { /* mid-swap: no boxes */ }\n }\n // The editor's list starts from the hello, so the diff starts there too.\n announced = new Set(flows.map((f) => f.id));\n try { sock?.send(JSON.stringify(hello)); } catch { /* race: closed immediately */ }\n };\n const postBoard = (flow: Flow): void => {\n try { post(boardFrame(flow)); } catch { /* never into the game */ }\n };\n /** Announce anything that opened or closed since the last look. Runs before\n * each forwarded event, so a frame never names a flow the editor has not\n * been told about. */\n const syncFlows = (): void => {\n const now = liveFlows();\n const ids = new Set(now.map((f) => f.id));\n for (const f of now) {\n if (announced.has(f.id)) continue;\n announced.add(f.id);\n post({ t: \"flowOpen\", flow: f.id });\n postBoard(f);\n }\n for (const id of [...announced]) {\n if (ids.has(id)) continue;\n announced.delete(id);\n post({ t: \"flowClose\", flow: id });\n }\n };\n const onTrace = (flowId: string, event: TraceEvent): void => {\n try {\n syncFlows();\n post({ t: \"trace\", flow: flowId, event });\n if (BOARD_EVENTS.has(event.type)) {\n const f = engine?.getFlow(flowId);\n if (f) postBoard(f);\n }\n } catch { /* never into the game */ }\n };\n\n try {\n sock = new Ctor(url);\n sock.addEventListener(\"open\", () => {\n sendHello();\n flush();\n });\n // Live refresh: the editor pushed a new bundle. The shape is checked here\n // so the host's handler never sees a malformed frame; anything else the\n // editor might send is ignored.\n sock.addEventListener(\"message\", (ev: { data: unknown }) => {\n if (!opts.onBundle || typeof ev.data !== \"string\") return;\n try {\n const msg = JSON.parse(ev.data) as Record<string, unknown>;\n if (msg.t === \"bundle\" && typeof msg.build === \"string\" && typeof msg.data === \"string\") {\n opts.onBundle({ build: msg.build, data: msg.data });\n }\n } catch { /* not for us */ }\n });\n sock.addEventListener(\"error\", () => { /* editor not listening: stay a no-op */ });\n // The socket is gone and this link does not reconnect, so the link is\n // INERT from here rather than merely unable to send. It used to set `sock`\n // to null and nothing else, leaving `closed` false, so every later trace\n // event pushed another string onto a queue nothing would ever drain: one\n // heap allocation per deal, play and write for the rest of the session.\n // Godot and Unity both already stopped at this point; JS and Unreal did\n // not. Found by the pre-release audit, 2026-08-29.\n sock.addEventListener(\"close\", () => { sock = null; closed = true; queue = []; });\n } catch { sock = null; closed = true; } // malformed URL etc.: never throw into the game\n\n const detach = (): void => {\n unsubscribe?.();\n unsubscribe = null;\n engine = null;\n announced = new Set();\n };\n\n return {\n attach(next: Engine): void {\n if (closed) return;\n detach();\n engine = next;\n try { unsubscribe = next.subscribeTrace(onTrace); } catch { engine = null; return; }\n // Every open flow's board up front: the editor can show any of them the\n // moment it connects, without waiting for that participant to move.\n announced = new Set(liveFlows().map((f) => f.id));\n for (const f of liveFlows()) postBoard(f);\n },\n detach,\n setBuild(next: string): void {\n if (closed || next === build) return;\n build = next;\n // Re-handshake: the editor re-reads the build, then gets every flow's\n // table as the new engine has it.\n if (sock && sock.readyState === OPEN) {\n sendHello();\n for (const f of liveFlows()) postBoard(f);\n }\n },\n close(): void {\n closed = true;\n detach();\n queue = [];\n try { sock?.close(); } catch { /* already gone */ }\n sock = null;\n },\n };\n}\n","// ---------------------------------------------------------------------------\n// Live refresh (design/live-link.md): the game-side applier. The editor pushes\n// a freshly compiled bundle over the Live Link (createLiveLink's `onBundle`);\n// this swaps it in under the running engine through engine.hotSwap: a new\n// Engine over the new bundle, carrying the old one's run, on the same registry. The runtime's loadGame() already\n// tolerates edited content (a deleted card leaves the table, orphaned\n// cooldowns and hand contents drop, a new property takes its default), so the\n// run carries across - every flow of it; it refuses only a save from another\n// project.\n//\n// Patter's applyLiveBundle has two tiers (strings-only vs hot swap) because it\n// has string tables and a cursor to re-find; we have neither, so this is the\n// one tier. Wire-up:\n//\n// let engine = new Engine(bundle, { seed: 7, log: true });\n// let flow = engine.openFlow(\"main\");\n// const link = createLiveLink({\n// build: bundle.content.hash,\n// onBundle: ({ build, data }) => {\n// const r = applyLiveBundle(engine, data, { log: true });\n// if (!r.ok) return console.warn(r.error);\n// engine = r.engine; // re-bind your handles: loadGame\n// flow = engine.getFlow(\"main\") // rebuilt every flow, so the old\n// ?? engine.openFlow(\"main\"); // Flow objects are inert\n// link.attach(engine); // re-attach the ENGINE: loadGame rebuilt every flow\n// link.setBuild(build);\n// },\n// });\n// ---------------------------------------------------------------------------\n\nimport type { Bundle } from \"@storylet-studio/model\";\nimport { Engine } from \"@storylet-studio/runtime\";\nimport type { EngineOptions } from \"@storylet-studio/runtime\";\n\nexport type LiveBundleResult =\n /** The new engine, carrying the old one's run (all flows), and the bundle\n * it runs. */\n | { ok: true; engine: Engine; bundle: Bundle }\n /** Nothing changed: keep the engine you have. */\n | { ok: false; error: string };\n\n/**\n * Apply a bundle the editor pushed over the Live Link through the engine's own\n * `hotSwap`, returning the replacement. Never throws; a failure (unparseable\n * JSON, a bundle the runtime rejects, a different project) comes back as\n * `{ ok: false, error }` and the old engine is left as it was.\n *\n * Works whether the engine made its own registry or was given the game's: on\n * the game's registry the old engine hands its keys to the replacement, which a\n * plain save and load into a second engine cannot do (the two would clash).\n *\n * The engine remembers the options it was built with (seed, log, world,\n * registry), so `opts` is only for overriding one of them for the replacement.\n */\nexport function applyLiveBundle(engine: Engine, bundleJson: string, opts: EngineOptions = {}): LiveBundleResult {\n let bundle: Bundle;\n try {\n bundle = JSON.parse(bundleJson) as Bundle;\n } catch {\n return { ok: false, error: \"pushed bundle is not valid JSON\" };\n }\n try {\n // The engine's own hotSwap: on the game's registry the old engine has to hand\n // its keys over, which a plain save and load into a new engine cannot do.\n const { engine: next } = engine.hotSwap(bundle, opts);\n return { ok: true, engine: next, bundle };\n } catch (e) {\n return { ok: false, error: e instanceof Error ? e.message : String(e) };\n }\n}\n","// ---------------------------------------------------------------------------\n// The host's @world container (design/flows.md; engine-runtimes.md 3.1).\n//\n// @world is the game's own state: the engine resolves it through a resolver\n// and NEVER saves it - \"host saves its container once, each engine saves its\n// own envelope\". A real game binds its own state here; a host that has no\n// state of its own (the demos, the playable page, the Board) uses this\n// ready-made container so @world still persists across its save/load.\n//\n// This is also what keeps a mixed Patter + Storylet Engine game honest: ONE\n// container, both engines mounting it foreign, neither writing it into its\n// envelope.\n// ---------------------------------------------------------------------------\n\nimport { PropertyBag as StateBag } from \"@wildwinter/scoperegistry\";\nimport type { ScalarValue } from \"@wildwinter/expr\";\nimport type { ScopeResolver } from \"@wildwinter/expr\";\nimport type { Bundle, PropertyBag } from \"@storylet-studio/model\";\n\nexport interface WorldContainer {\n /** Pass as `new Engine(bundle, { world: container.resolver })`. */\n resolver: ScopeResolver;\n /** The kernel bag itself (subscribe, audit, rows live there) - mount it\n * into a state logger or examiner beside the engine's own bags. Writing it\n * DIRECTLY is writing the kernel, so a `writable: false` declaration asks\n * the kernel's question: pass `{ host: true }` to say the game is speaking\n * (`bag.set(name, value, { host: true })`). Through `resolver` or an\n * engine's setProperty that is already answered. */\n bag: StateBag;\n /** The current values, for saving beside the engine's envelope. */\n values(): PropertyBag;\n /** Restore saved values over fresh defaults: orphaned keys drop, new\n * declarations keep their defaults - the same drift rule as loadGame. */\n load(values: PropertyBag): void;\n}\n\n/** A world container seeded from the bundle's @world declarations.\n *\n * The container is the GAME's state, so it WRITES - even a declaration\n * carrying `writable: false`. That flag is the STORY's promise not to write\n * the value (Reboot.md 10), and the engine keeps it where the story writes: an\n * outcome is refused against the engine's read-only table before it ever\n * reaches this resolver. Enforcing it here as well refused the HOST too - the\n * clock the game must move, the harness driving the value it is testing\n * against - which is the opposite of what the flag says.\n *\n * So the declarations are seeded AS DECLARED, which is what an examiner over\n * this container should read, and the writes go through as HOST writes\n * (scoperegistry 0.6.0's `{ host: true }`). The resolver's `set` is the\n * engine's own doorway and passes the flag too: the engine has already sorted\n * story from host by then - a story write was refused earlier, a host write is\n * the only kind that arrives - and a resolver takes a name and a value with no\n * room to say which. A game wanting a rule of its own binds its own resolver\n * rather than this one; the ports' container (Unreal's UStoryletWorld) draws\n * the same line, with HostSet never refused and StorySet asking the game's own\n * read-only list. */\nexport function createWorldContainer(bundle: Bundle): WorldContainer {\n const bag = new StateBag(bundle.world.properties, { normalise: (n) => n });\n return {\n resolver: {\n get: (n) => bag.get(n),\n set: (n: string, v: ScalarValue) => { bag.set(n, v, { host: true }); },\n },\n bag,\n values: () => bag.values,\n load: (values) => bag.load(values),\n };\n}\n"],"mappings":";AAoFO,SAAS,UAAU,MAAqB,MAAoC;AACjF,QAAM,UAAyB,CAAC;AAChC,QAAM,QAAQ,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,IAAI,GAAG,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC;AAClE,aAAW,QAAQ,CAAC,GAAG,KAAK,EAAE,KAAK,GAAG;AACpC,UAAM,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI;AACvC,QAAI,KAAK,UAAU,IAAI,MAAM,KAAK,UAAU,EAAE,EAAG,SAAQ,KAAK,EAAE,MAAM,MAAM,GAAG,CAAC;AAAA,EAClF;AACA,SAAO;AACT;AAEA,IAAM,OAAO,CAAC,MAAwC,MAAM,SAAY,YAAY,KAAK,UAAU,CAAC;AAEpG,IAAM,WAAW,CAAC,MAAwB,EAAE,cAAc,EAAE,IAAI;AAEzD,SAAS,kBAAkB,SAA6B,OAA2B,CAAC,GAAgB;AACzG,QAAM,OAAO,KAAK,SAAS,CAACA,UAAiB,QAAQ,IAAIA,KAAI;AAC7D,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,OAAO,CAAC,MAAyB;AAAE,SAAK,GAAG,KAAK,GAAG,EAAE,IAAI,KAAK,KAAK,EAAE,IAAI,CAAC,OAAO,KAAK,EAAE,EAAE,CAAC,EAAE;AAAA,EAAG;AAEtG,QAAM,OAAO,MAAqB;AAChC,UAAM,MAAqB,CAAC;AAC5B,eAAW,KAAK,QAAQ,OAAO,GAAG;AAChC,YAAM,SAAS,SAAS,CAAC;AACzB,iBAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,EAAE,IAAI,MAAM,EAAG,KAAI,SAAS,IAAI,IAAI;AAAA,IACjF;AACA,WAAO,OAAO,KAAK,QAAQ,QAAQ,KAAK,CAAC,CAAC;AAC1C,WAAO,gBAAgB,GAAG;AAAA,EAC5B;AAEA,MAAI,WAAW,KAAK;AACpB,MAAI,SAAwB,CAAC;AAC7B,MAAI,UAAmD,CAAC;AAExD,QAAM,OAAO,CAAC,QAAgB,QAC5B,IAAI,QAAQ,CAAC,WAAW;AAGtB,UAAM,IAAiB,gBAAgB,EAAE,MAAM,SAAS,OAAO,MAAM,MAAM,OAAO,MAAM,IAAI,OAAO,KAAK,CAAC;AACzG,SAAK,CAAC;AACN,WAAO,KAAK,CAAC;AACb,aAAS,EAAE,IAAI,IAAI,gBAAgB,OAAO,IAAI;AAAA,EAChD,CAAC;AAEH,QAAM,QAAQ,MAAY;AACxB,UAAM,SAAS,QAAQ,OAAO;AAC9B,UAAM,OAAO,QAAQ,WAAW,OAAO,UAAU,OAAO,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,EAAG,QAAQ,EAAE,GAAG;AACjG,QAAI,KAAM;AACV,eAAW,KAAK,QAAS,GAAE,IAAI;AAC/B,cAAU,OAAO,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,KAAK,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;AAAA,EAC7E;AACA,QAAM;AAEN,SAAO;AAAA,IACL,UAAU;AAAA,IACV,UAAyB;AAGvB,YAAM,OAAO,KAAK;AAClB,YAAM,SAAS,UAAU,UAAU,IAAI;AACvC,iBAAW,KAAK,OAAQ,MAAK,CAAC;AAC9B,YAAM,UAAU,CAAC,GAAG,QAAQ,GAAG,MAAM;AACrC,eAAS,CAAC;AACV,iBAAW;AACX,YAAM;AACN,aAAO;AAAA,IACT;AAAA,IACA,UAAgB;AACd,iBAAW,KAAK,QAAS,GAAE,IAAI;AAC/B,gBAAU,CAAC;AACX,eAAS,CAAC;AAAA,IACZ;AAAA,EACF;AACF;;;ACFO,IAAM,cAAN,MAAM,aAAY;AAAA;AAAA;AAAA;AAAA,EAId,SAAsC,CAAC;AAAA,EACxC,QAAQ,oBAAI,IAA8B;AAAA,EACjC,cAAc,oBAAI,IAAiC;AAAA,EACnD,WAAW,oBAAI,IAAiC;AAAA;AAAA;AAAA;AAAA,EAIhD;AAAA;AAAA;AAAA,EAIR;AAAA,EAET,YACE,eAAmC,CAAC,GACpC,MACA;AACA,SAAK,OAAO,MAAM,cAAc,CAAC,MAAM,EAAE,YAAY;AACrD,SAAK,aAAa,MAAM,cAAc;AACtC,SAAK,KAAK,YAAY;AAAA,EACxB;AAAA,EAEQ,KAAK,cAAwC;AACnD,eAAW,KAAK,cAAc;AAC5B,YAAM,OAAO,KAAK,KAAK,EAAE,IAAI;AAC7B,WAAK,MAAM,IAAI,MAAM,CAAC;AAGtB,WAAK,OAAO,IAAI,IAAI,gBAAgB,EAAE,WAAW,WAAW,CAAC,CAAC;AAAA,IAChE;AAAA,EACF;AAAA,EAEA,IAAI,MAAuC;AACzC,WAAO,KAAK,OAAO,KAAK,KAAK,IAAI,CAAC;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,MAAsB;AAC9B,WAAO,KAAK,KAAK,IAAI;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,IAAI,MAAc,OAAoB,MAAyE;AAC7G,UAAM,IAAI,KAAK,KAAK,IAAI;AACxB,QAAI,CAAC,MAAM,QAAQ,KAAK,MAAM,IAAI,CAAC,GAAG,aAAa,MAAO,OAAM,IAAI,MAAM,IAAI,IAAI,gBAAgB;AAClG,UAAM,SAAoB;AAAA,MACxB,MAAM;AAAA,MACN,MAAM,KAAK,OAAO,CAAC;AAAA,MACnB,MAAM;AAAA,MACN,QAAQ,MAAM,UAAU;AAAA,MACxB,QAAQ,MAAM;AAAA,IAChB;AACA,SAAK,OAAO,CAAC,IAAI;AACjB,eAAW,SAAS,KAAK,SAAU,OAAM,MAAM;AAC/C,QAAI,CAAC,OAAO,OAAQ,YAAW,MAAM,KAAK,YAAa,IAAG,MAAM;AAChE,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAU,IAA6C;AACrD,SAAK,YAAY,IAAI,EAAE;AACvB,WAAO,MAAM,KAAK,YAAY,OAAO,EAAE;AAAA,EACzC;AAAA;AAAA,EAGA,QAAQ,IAA6C;AACnD,SAAK,SAAS,IAAI,EAAE;AACpB,WAAO,MAAM,KAAK,SAAS,OAAO,EAAE;AAAA,EACtC;AAAA;AAAA;AAAA,EAIA,OAAsB;AACpB,WAAO,CAAC,GAAG,KAAK,MAAM,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,IAAI,IAAI,GAAG,QAAW,MAAM,KAAK,UAAU,CAAC;AAAA,EACjH;AAAA,EAEA,eAAmC;AACjC,WAAO,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,QAAqB;AACnB,UAAM,IAAI,IAAI,aAAY,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,YAAY,KAAK,WAAW,CAAC;AACnF,MAAE,QAAQ,IAAI,IAAI,KAAK,KAAK;AAC5B,WAAO,OAAO,EAAE,QAAQ,gBAAgB,KAAK,MAAM,CAAC;AACpD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA,EAIA,OAAO,cAAwC;AAC7C,eAAW,KAAK,OAAO,KAAK,KAAK,MAAM,EAAG,QAAO,KAAK,OAAO,CAAC;AAC9D,SAAK,MAAM,MAAM;AACjB,SAAK,KAAK,YAAY;AAAA,EACxB;AAAA;AAAA,EAGA,OAAoC;AAClC,WAAO,gBAAgB,KAAK,MAAM;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK,QAA2C;AAC9C,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,EAAG,MAAK,OAAO,KAAK,KAAK,CAAC,CAAC,IAAI;AAAA,EAC3E;AACF;AAEA,SAAS,OACP,GACA,OACA,UACA,MACA,aAAa,IACA;AACb,QAAM,UAAU,QAAQ,EAAE,KAAK,YAAY;AAC3C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,aAAa;AAAA,IACnB,MAAM,EAAE;AAAA,IACR;AAAA,IACA,SAAS,EAAE,WAAW,WAAW,CAAC;AAAA,IAClC,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,IAIrD,GAAI,EAAE,WAAW,SAAY,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACrD,UAAU,YAAY,EAAE,YAAY;AAAA,EACtC;AACF;AA2cO,SAAS,WAAW,GAAkF;AAC3G,MAAI,EAAE,YAAY,OAAW,QAAO,EAAE;AACtC,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AAAW,aAAO;AAAA,IACvB,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAQ,aAAO,EAAE,SAAS,CAAC,KAAK;AAAA,IACrC,KAAK;AAAS,aAAO,CAAC;AAAA;AAAA,IAEtB,KAAK;AAAW,aAAO,EAAE,SAAS,CAAC,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMxC;AAAS,aAAO;AAAA,EAClB;AACF;;;ACrtBO,SAAS,cAAc,QAAgB,MAA2B;AACvE,QAAM,MAAqB,CAAC;AAG5B,aAAW,EAAE,IAAI,KAAK,CAAC,GAAG,OAAO,SAAS,GAAG,GAAG,KAAK,SAAS,CAAC,GAAG;AAChE,eAAW,OAAO,IAAI,KAAK,GAAG;AAC5B,UAAI,IAAI,UAAU,OAAW,KAAI,IAAI,IAAI,IAAI,IAAI;AAAA,IACnD;AAAA,EACF;AACA,SAAO,OAAO,KAAK,WAAW,OAAO,SAAS,EAAE,MAAM,KAAK,EAAE,CAAC,CAAC;AAC/D,SAAO;AACT;AAKA,SAAS,WAAW,OAA4C;AAC9D,QAAM,MAAqB,CAAC;AAC5B,MAAI,UAAU,OAAW,QAAO;AAChC,aAAW,CAAC,OAAO,IAAI,KAAK,OAAO,QAAQ,MAAM,KAAK,EAAG,KAAI,QAAQ,KAAK,EAAE,IAAI;AAChF,aAAW,CAAC,QAAQ,EAAE,KAAK,OAAO,QAAQ,MAAM,SAAS,EAAG,KAAI,YAAY,MAAM,EAAE,IAAI;AACxF,aAAW,CAAC,QAAQ,KAAK,KAAK,OAAO,QAAQ,MAAM,KAAK,EAAG,KAAI,SAAS,MAAM,EAAE,IAAI,CAAC,GAAG,KAAK;AAC7F,SAAO;AACT;AAOO,SAASC,mBAAkB,QAAgB,MAAY,OAA2B,CAAC,GAAgB;AAGxG,QAAM,KAAK,KAAK;AAChB,QAAM,OAAO,MAAwB,OAAO,QAAQ,EAAE;AACtD,SAAO,kBAAwB;AAAA;AAAA;AAAA;AAAA,IAI7B,QAAQ,MAAM,CAAC,GAAG,OAAO,SAAS,GAAG,GAAI,KAAK,GAAG,SAAS,KAAK,CAAC,CAAE,EAAE,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,IAAI,EAAE;AAAA,IAC9F,OAAO,MAAM,WAAW,OAAO,SAAS,EAAE,MAAM,EAAE,CAAC;AAAA,EACrD,GAAG,IAAI;AACT;;;ACwsBO,IAAM,cAAc;AAEpB,IAAM,iBAAiB;AA+KvB,IAAM,kBAAkB;;;ACr8BxB,SAAS,eAAe,QAAgB,OAA6B;AAC1E,SAAO,KAAK,UAAU,UAAU,QAAQ,KAAK,GAAG,MAAM,CAAC;AACzD;AAeO,SAAS,UAAU,QAAgB,OAA+B;AACvE,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ,OAAO,SAAS;AAAA,IACxB,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC;AAAA,EACzC;AACF;AAWO,SAAS,UAAU,QAAgB,MAAyC;AACjF,MAAI,CAAC,QAAQ,OAAO,SAAS,YACxB,KAAK,WAAW,mBACf,KAAK,QAAQ,WAAW,eAAe,KAAK,QAAQ,WAAW,gBAAiB;AACpF,UAAM,IAAI,MAAM,0CAA0C,eAAe,IAAI;AAAA,EAC/E;AACA,SAAO,SAAS,KAAK,MAAM;AAC3B,SAAO,KAAK;AACd;AAKO,SAAS,iBAAiB,QAAgB,MAAuC;AACtF,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,IAAI;AAAA,EAC1B,QAAQ;AACN,UAAM,IAAI,MAAM,gBAAgB;AAAA,EAClC;AACA,SAAO,UAAU,QAAQ,MAAkB;AAC7C;;;AC/BA,IAAM,WAAW;AACjB,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiCL,SAAS,uBAA6B;AAC3C,MAAI,SAAS,eAAe,QAAQ,EAAG;AACvC,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,KAAK;AACX,QAAM,cAAc;AACpB,WAAS,KAAK,OAAO,KAAK;AAC5B;AAEA,IAAM,KAAK,CAAC,GAA4B,MACtC,KAAK,UAAU,CAAC,MAAM,KAAK,UAAU,CAAC;AAKxC,IAAM,YAAY,CAAC,QAAQ,QAAQ,SAAS,SAAS,SAAS,YAAY;AAC1E,IAAM,kBAA8D;AAAA,EAClE,MAAM;AAAA,EAAQ,MAAM;AAAA,EAAQ,OAAO;AAAA,EAAS,OAAO;AAAA,EAAS,OAAO;AAAA,EAAS,YAAY;AAC1F;AAEA,IAAM,YAAY,CAAC,MACjB,EAAE,SAAS,SAAS,SAAS,EAAE;AAEjC,IAAM,UAAU,CAAC,MACf,MAAM,SAAY,YAAY,KAAK,UAAU,CAAC;AAIzC,SAAS,eAAe,GAAsC;AAInE,SAAO,cAAc,GAAG,UAAU,KAAK,EAAE,OAAO,GAAG,EAAE,IAAI,MAAM,EAAE;AACnE;AAEA,SAAS,cAAc,GAAa,MAAsB;AACxD,QAAM,SAAS,EAAE,SAAS,SAAY,IAAI,EAAE,IAAI,OAAO,UAAU;AACjE,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK,QAAQ;AACX,YAAM,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,YAAY,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AAC1E,aAAO,GAAG,KAAK,QAAQ,EAAE,IAAI,KAAK,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,QAAQ,KAAK,EAAE,MAAM,MAAM;AAAA,IACrG;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,OAAO,OAAO,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,IAAI;AAC9E,YAAM,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,YAAY,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AAC3E,aAAO,GAAG,KAAK,QAAQ,EAAE,GAAG,GAAG,OAAO,KAAK,IAAI,MAAM,EAAE,KAChD,OAAO,SAAS,IAAI,OAAO,KAAK,IAAI,IAAI,QAAQ,KAAK,EAAE,MAAM,MAAM;AAAA,IAC5E;AAAA,IACA,KAAK;AAAS,aAAO,GAAG,KAAK,SAAS,EAAE,IAAI,SAAS,EAAE,IAAI,KAAK,EAAE,MAAM;AAAA;AAAA,IAExE,KAAK;AAAQ,aAAO,GAAG,KAAK,QAAQ,EAAE,IAAI,GAAG,EAAE,YAAY,KAAK,KAAK,OAAO,EAAE,OAAO,EAAE;AAAA,IACvF,KAAK;AAAS,aAAO,GAAG,KAAK,SAAS,EAAE,IAAI,KAAK,QAAQ,EAAE,IAAI,CAAC,OAAO,QAAQ,EAAE,KAAK,CAAC;AAAA,IACvF,KAAK;AAAS,aAAO,GAAG,KAAK,SAAS,EAAE,GAAG,OAAO,EAAE,IAAI;AAAA,IACxD;AAAS,aAAO,GAAG,KAAK,cAAc,EAAE,KAAK,KAAK,EAAE,OAAO;AAAA,EAC7D;AACF;AAGA,IAAM,UAAU,CAAC,SAAyB;AACxC,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,SAAO,MAAM,WAAW,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC;AACjE;AAEO,SAAS,wBAAwB,QAAgB,MAAY,OAAiC,CAAC,GAAsB;AAC1H,uBAAqB;AAKrB,MAAI,WAAW;AACf,QAAM,OAAO,MAAY;AAEzB,QAAM,KAAK,SAAS,cAAc,KAAK;AACvC,KAAG,YAAY;AAIf,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,YAAY;AACjB,QAAM,IAAI,SAAS,cAAc,IAAI;AACrC,IAAE,cAAc,KAAK,SAAS;AAC9B,OAAK,OAAO,CAAC;AAEb,QAAM,UAAU,SAAS,cAAc,QAAQ;AAC/C,UAAQ,OAAO;AACf,UAAQ,YAAY;AACpB,UAAQ,cAAc;AACtB,UAAQ,iBAAiB,SAAS,MAAM;AACtC,UAAM,OAAO,IAAI,KAAK,CAAC,eAAe,MAAM,CAAC,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAC5E,UAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,UAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAE,OAAO;AACT,MAAE,WAAW;AACb,MAAE,MAAM;AACR,QAAI,gBAAgB,GAAG;AAAA,EACzB,CAAC;AACD,OAAK,OAAO,OAAO;AAEnB,QAAM,aAAa,SAAS,cAAc,OAAO;AACjD,aAAW,OAAO;AAClB,aAAW,SAAS;AACpB,aAAW,SAAS;AACpB,aAAW,iBAAiB,UAAU,MAAM;AAC1C,UAAM,OAAO,WAAW,QAAQ,CAAC;AACjC,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,IAAI,WAAW;AAC9B,WAAO,SAAS,MAAM;AAEpB,UAAI;AACF,yBAAiB,QAAQ,OAAO,OAAO,MAAM,CAAC;AAC9C,mBAAW,OAAO,QAAQ,KAAK,EAAE,KAAK,OAAO,SAAS,KAAK,EAAE;AAC7D,gBAAQ;AAAA,MACV,SAAS,GAAG;AACV,gBAAQ,MAAM,qCAAqC,aAAa,QAAQ,EAAE,UAAU,CAAC;AAAA,MACvF;AACA,iBAAW,QAAQ;AAAA,IACrB;AACA,WAAO,WAAW,IAAI;AAAA,EACxB,CAAC;AAED,QAAM,UAAU,SAAS,cAAc,QAAQ;AAC/C,UAAQ,OAAO;AACf,UAAQ,YAAY;AACpB,UAAQ,cAAc;AACtB,UAAQ,iBAAiB,SAAS,MAAM,WAAW,MAAM,CAAC;AAC1D,OAAK,OAAO,SAAS,UAAU;AAC/B,KAAG,OAAO,IAAI;AAId,QAAM,SAAS,SAAS,cAAc,OAAO;AAC7C,SAAO,OAAO;AACd,SAAO,YAAY;AACnB,SAAO,cAAc;AACrB,KAAG,OAAO,MAAM;AAIhB,QAAM,UAAoD,CAAC;AAC3D,QAAM,SAA2E,CAAC;AAClF,MAAI,YAAY;AAChB,aAAW,OAAO,KAAK,EAAE,eAAe,GAAG;AACzC,UAAM,QAAQ,QAAQ,IAAI,IAAI;AAC9B,QAAI,UAAU,WAAW;AACvB,YAAM,IAAI,SAAS,cAAc,KAAK;AACtC,QAAE,YAAY;AACd,QAAE,cAAc;AAChB,SAAG,OAAO,CAAC;AACX,aAAO,KAAK,EAAE,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC;AAC/B,kBAAY;AAAA,IACd;AACA,UAAM,QAAQ,SAAS,MAAM,KAAK,OAAO;AACzC,WAAO,OAAO,SAAS,CAAC,EAAG,KAAK,KAAK,EAAE,IAAI,OAAO,MAAM,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,GAAG,YAAY,EAAE,CAAC;AACjG,OAAG,OAAO,KAAK;AAAA,EACjB;AAEA,SAAO,iBAAiB,SAAS,MAAM;AACrC,UAAM,IAAI,OAAO,MAAM,KAAK,EAAE,YAAY;AAC1C,eAAW,SAAS,QAAQ;AAC1B,UAAI,MAAM;AACV,iBAAW,OAAO,MAAM,MAAM;AAC5B,cAAMC,QAAO,MAAM,MAAM,IAAI,KAAK,SAAS,CAAC;AAC5C,YAAI,GAAG,MAAM,UAAUA,QAAO,KAAK;AACnC,cAAM,OAAOA;AAAA,MACf;AACA,YAAM,GAAG,MAAM,UAAU,MAAM,KAAK;AAAA,IACtC;AAAA,EACF,CAAC;AAID,QAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,YAAU,YAAY;AACtB,YAAU,cAAc;AACxB,QAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,YAAU,YAAY;AACtB,QAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,YAAU,YAAY;AACtB,YAAU,cAAc;AACxB,QAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,YAAU,YAAY;AACtB,KAAG,OAAO,WAAW,WAAW,WAAW,SAAS;AAQpD,QAAM,QAAQ,CAAC,MAAc,UAAiC,QAAkC;AAC9F,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,YAAY;AAClB,UAAM,MAAM,SAAS,cAAc,OAAO;AAC1C,QAAI,OAAO;AACX,QAAI,UAAU;AACd,QAAI,iBAAiB,UAAU,MAAM,SAAS,IAAI,OAAO,CAAC;AAC1D,UAAM,OAAO,KAAK,IAAI;AACtB,WAAO;AAAA,EACT;AAGA,QAAM,gBAAgB,CACpB,OACA,SACA,WACA,OACA,UACa;AAIb,UAAMC,QAAO,SAAS,cAAc,KAAK;AACzC,IAAAA,MAAK,YAAY,iBAAiB,KAAK;AACvC,IAAAA,MAAK,cAAc;AACnB,UAAM,MAAM,SAAS,cAAc,KAAK;AACxC,QAAI,YAAY,gBAAgB,KAAK;AACrC,UAAM,OAAO,SAAS,cAAc,KAAK;AACzC,SAAK,YAAY,aAAa,KAAK;AAEnC,UAAM,SAAS,oBAAI,IAAqB;AACxC,UAAM,eAAe,MACnB,UAAU,EAAE,OAAO,CAAC,MAAM,OAAO,IAAI,UAAU,CAAC,CAAC,MAAM,KAAK,EAAE,IAAI,cAAc;AAClF,QAAI,aAAa;AACjB,QAAI,YAAY;AAChB,UAAM,SAAS,CAAC,QAAQ,UAAgB;AACtC,YAAM,UAAU,UAAU;AAC1B,YAAM,QAAQ,GAAG,QAAQ,MAAM,IAAI,QAAQ,SAAS,IAAI,QAAQ,QAAQ,SAAS,CAAC,EAAG,MAAM,EAAE;AAC7F,UAAI,CAAC,SAAS,UAAU,UAAW;AACnC,kBAAY;AACZ,YAAM,QAAQ,aAAa;AAC3B,WAAK,cAAc,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI;AACzD,UAAI,WAAY,MAAK,YAAY,KAAK;AAAA,IACxC;AAEA,eAAW,QAAQ,WAAW;AAC5B,aAAO,IAAI,MAAM,IAAI;AACrB,UAAI,OAAO,MAAM,gBAAgB,IAAI,GAAG,CAAC,OAAO;AAC9C,eAAO,IAAI,MAAM,EAAE;AACnB,eAAO,IAAI;AAAA,MACb,GAAG,YAAY,CAAC;AAAA,IAClB;AACA,QAAI,OAAO,MAAM,cAAc,CAAC,OAAO;AAAE,mBAAa;AAAA,IAAI,GAAG,cAAc,CAAC;AAC5E,UAAM,UAAU,SAAS,cAAc,QAAQ;AAC/C,YAAQ,OAAO;AACf,YAAQ,YAAY;AACpB,YAAQ,cAAc;AACtB,YAAQ,QAAQ;AAChB,YAAQ,iBAAiB,SAAS,MAAM;AAAE,WAAK,UAAU,WAAW,UAAU,aAAa,EAAE,KAAK,IAAI,CAAC;AAAA,IAAG,CAAC;AAC3G,UAAM,WAAW,SAAS,cAAc,QAAQ;AAChD,aAAS,OAAO;AAChB,aAAS,YAAY;AACrB,aAAS,cAAc;AACvB,aAAS,QAAQ;AACjB,aAAS,iBAAiB,SAAS,MAAM;AAAE,YAAM;AAAG,aAAO,IAAI;AAAA,IAAG,CAAC;AACnE,QAAI,OAAO,SAAS,QAAQ;AAC5B,OAAG,OAAOA,OAAM,KAAK,IAAI;AACzB,WAAO,EAAE,OAAO;AAAA,EAClB;AAKA,QAAM,UAAU;AAAA,IAAc;AAAA,IAAQ;AAAA,IAAO,MAAM,KAAK,EAAE,IAAI;AAAA,IAAG,MAAM,KAAK,EAAE,SAAS;AAAA,IACrF;AAAA,EAAkE;AACpE,QAAM,SAAS;AAAA,IAAc;AAAA,IAAO;AAAA,IAAwB,MAAM,OAAO,IAAI;AAAA,IAAG,MAAM,OAAO,SAAS;AAAA,IACpG;AAAA,EAAiE;AACnE,QAAM,YAAY,CAAC,QAAQ,UAAgB;AAAE,WAAO,OAAO,KAAK;AAAG,YAAQ,OAAO,KAAK;AAAA,EAAG;AAE1F,QAAMC,QAAO,CAAC,QAAqB,SAAuB;AACxD,UAAM,MAAM,SAAS,cAAc,KAAK;AACxC,QAAI,YAAY;AAChB,QAAI,cAAc;AAClB,WAAO,OAAO,GAAG;AAAA,EACnB;AACA,QAAM,WAAW,MAAY;AAC3B,cAAU,cAAc;AACxB,eAAW,OAAO,KAAK,EAAE,UAAU,GAAG;AACpC,MAAAA,MAAK,WAAW,GAAG,IAAI,SAAS,IAAI,MAAM,UAAU,IAAI,IAAI,EAAE;AAAA,IAChE;AACA,cAAU,cAAc;AACxB,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,KAAK,EAAE,MAAM,CAAC,GAAG;AAC1D,YAAM,QAAQ,MAAM,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM;AAClD,MAAAA,MAAK,WAAW,GAAG,IAAI,KAAK,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,SAAS,EAAE;AAAA,IAC/E;AAAA,EACF;AAEA,QAAM,UAAU,MAAY;AAC1B,eAAW,KAAK,QAAS,GAAE,KAAK;AAChC,aAAS;AACT,cAAU;AAAA,EACZ;AACA,WAAS;AACT,YAAU,IAAI;AAEd,MAAI;AACJ,QAAM,SAAS,KAAK,UAAU;AAC9B,MAAI,SAAS,EAAG,SAAQ,YAAY,SAAS,MAAM;AAEnD,GAAC,KAAK,aAAa,SAAS,MAAM,OAAO,EAAE;AAC3C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAgB;AACd,UAAI,UAAU,OAAW,eAAc,KAAK;AAC5C,SAAG,OAAO;AAAA,IACZ;AAAA,EACF;AACF;AAEA,SAAS,SACP,MACA,KACA,SACa;AACb,QAAM,MAAM,SAAS,cAAc,KAAK;AACxC,MAAI,YAAY;AAChB,QAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,OAAK,YAAY;AACjB,OAAK,cAAc,IAAI;AACvB,OAAK,QAAQ,IAAI;AAEjB,QAAM,UAAU,MAA+B;AAC7C,QAAI;AAAE,aAAO,KAAK,EAAE,YAAY,IAAI,IAAI;AAAA,IAAG,QAAQ;AAAE,aAAO;AAAA,IAAW;AAAA,EACzE;AAGA,MAAI,OAAmB,MAAM;AAAA,EAAC;AAC9B,QAAM,SAAS,CAAC,UAA6B;AAAE,SAAK,EAAE,YAAY,IAAI,MAAM,KAAK;AAAG,SAAK;AAAA,EAAG;AAE5F,QAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,QAAM,OAAO;AACb,QAAM,YAAY;AAClB,QAAM,cAAc;AACpB,QAAM,QAAQ;AACd,QAAM,iBAAiB,SAAS,MAAM,OAAO,IAAI,OAAO,CAAC;AAEzD,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,CAAC,MAA4B,SAAS,kBAAkB;AAExE,UAAQ,IAAI,MAAM;AAAA,IAChB,KAAK,WAAW;AACd,YAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,YAAM,OAAO;AACb,YAAM,iBAAiB,UAAU,MAAM,OAAO,MAAM,OAAO,CAAC;AAC5D,eAAS;AACT,aAAO,MAAM;AAAE,YAAI,CAAC,QAAQ,KAAK,EAAG,OAAM,UAAU,QAAQ,MAAM;AAAA,MAAM;AACxE;AAAA,IACF;AAAA,IACA,KAAK,UAAU;AACb,YAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,YAAM,OAAO;AACb,YAAM,iBAAiB,UAAU,MAAM,OAAO,OAAO,MAAM,KAAK,CAAC,CAAC;AAClE,eAAS;AACT,aAAO,MAAM;AAAE,YAAI,CAAC,QAAQ,KAAK,EAAG,OAAM,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAAA,MAAG;AAC1E;AAAA,IACF;AAAA,IACA,KAAK;AAAA,IACL,KAAK,WAAW;AAOd,YAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,iBAAW,MAAM,IAAI,SAAS,YAAY,IAAI,SAAS,IAAI,WAAW,CAAC,GAAG;AACxE,cAAM,IAAI,SAAS,cAAc,QAAQ;AACzC,UAAE,QAAQ;AACV,UAAE,cAAc;AAChB,eAAO,OAAO,CAAC;AAAA,MACjB;AACA,aAAO,iBAAiB,UAAU,MAAM,OAAO,OAAO,KAAK,CAAC;AAC5D,eAAS;AACT,aAAO,MAAM;AAAE,YAAI,CAAC,QAAQ,MAAM,EAAG,QAAO,QAAQ,OAAO,QAAQ,KAAK,EAAE;AAAA,MAAG;AAC7E;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,YAAM,OAAO;AACb,YAAM,cAAc;AACpB,YAAM,iBAAiB,UAAU,MAC/B,OAAO,MAAM,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AACjF,eAAS;AACT,aAAO,MAAM;AAAE,YAAI,CAAC,QAAQ,KAAK,EAAG,OAAM,SAAU,QAAQ,KAA8B,CAAC,GAAG,KAAK,IAAI;AAAA,MAAG;AAC1G;AAAA,IACF;AAAA,IACA,SAAS;AACP,YAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,YAAM,OAAO;AACb,YAAM,iBAAiB,UAAU,MAAM,OAAO,MAAM,KAAK,CAAC;AAC1D,eAAS;AACT,aAAO,MAAM;AAAE,YAAI,CAAC,QAAQ,KAAK,EAAG,OAAM,QAAQ,OAAO,QAAQ,KAAK,EAAE;AAAA,MAAG;AAAA,IAC7E;AAAA,EACF;AAEA,QAAM,UAAU,MAAY;AAAE,SAAK;AAAG,UAAM,WAAW,GAAG,QAAQ,GAAG,IAAI,OAAO;AAAA,EAAG;AACnF,SAAO;AACP,UAAQ;AACR,UAAQ,KAAK,EAAE,KAAK,MAAM,QAAQ,CAAC;AACnC,MAAI,OAAO,MAAM,QAAQ,KAAK;AAC9B,SAAO;AACT;;;AC5cA,SAAS,sBAAsB;AAsB/B,IAAMC,WAAU,CAAC,MACf,MAAM,SAAY,YAAY,KAAK,UAAU,CAAC;AAMzC,SAAS,sBAAsB,GAA4B;AAChE,QAAM,UAAU,EAAE,WAAW,UAAa,EAAE,OAAO,SAAS,IAAI,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,MAAM;AAC9F,QAAM,UAAU,EAAE,YAAY,OAAO,eAAe;AACpD,SAAO,GAAG,EAAE,IAAI,KAAK,EAAE,IAAI,MAAMA,SAAQ,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,OAAO;AACzE;AAIO,SAAS,iBAAiB,OAAqC;AACpE,MAAI,MAAM,UAAU,WAAW,MAAM,UAAU,QAAS,QAAO,MAAM;AACrE,QAAM,QAAQ,MAAM,UAAU,SAAY,KAAK,MAAM,KAAK,MAAM;AAChE,SAAO,GAAG,MAAM,KAAK,IAAI,MAAM,KAAK,GAAG,KAAK;AAC9C;AAEA,IAAM,OAAO,CAAC,QAAqB,MAAc,MAAM,cAA2B;AAChF,QAAM,MAAM,SAAS,cAAc,KAAK;AACxC,MAAI,YAAY;AAChB,MAAI,cAAc;AAClB,SAAO,OAAO,GAAG;AACjB,SAAO;AACT;AAGA,IAAM,OAAO,CAAC,QAAqB,OAAe,SAA+B;AAC/E,QAAM,UAAU,SAAS,cAAc,SAAS;AAChD,UAAQ,YAAY;AACpB,UAAQ,OAAO;AACf,QAAM,UAAU,SAAS,cAAc,SAAS;AAChD,UAAQ,cAAc;AACtB,UAAQ,OAAO,OAAO;AACtB,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,UAAQ,OAAO,IAAI;AACnB,SAAO,OAAO,OAAO;AACrB,SAAO;AACT;AAIO,SAAS,sBACd,QACA,OAA+B,CAAC,GACf;AACjB,uBAAqB;AACrB,QAAM,cAAc,eAAe,MAAM;AACzC,QAAM,OAAO,KAAK,QAAQ;AAE1B,QAAM,KAAK,SAAS,cAAc,KAAK;AACvC,KAAG,YAAY;AAEf,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,YAAY;AACjB,QAAM,IAAI,SAAS,cAAc,IAAI;AACrC,IAAE,cAAc,KAAK,SAAS;AAC9B,OAAK,OAAO,CAAC;AACb,KAAG,OAAO,IAAI;AAGd,QAAM,EAAE,UAAU,OAAO,IAAI;AAC7B,QAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,QAAM,YAAY;AAClB,KAAG,OAAO,KAAK;AACf,OAAK,OAAO,GAAG,SAAS,OAAO,IAAI,SAAS,OAAO,IAAI,SAAS;AAChE,OAAK,OAAO,UAAU,SAAS,MAAM,IAAI,iBAAiB;AAC1D;AAAA,IAAK;AAAA,IAAO,QAAQ,SAAS,SAAS,KAAK,WAAW,SAAS,IAAI,eAAe,SAAS,QAAQ;AAAA,IACjG;AAAA,EAAiB;AAGnB,QAAM,YAAY,KAAK,IAAI,gBAAgB,IAAI;AAC/C,YAAU,YAAY;AACtB,MAAI,YAAY,MAAM,WAAW,GAAG;AAClC,SAAK,WAAW,yCAAyC,iBAAiB;AAAA,EAC5E;AACA,aAAW,QAAQ,YAAY,OAAO;AACpC,UAAM,WAAW,KAAK,aAAa,SAAY,cAAc,KAAK,QAAQ,KAAK;AAG/E,UAAM,QAAQ,KAAK,YAAY,SAAY,KACvC,WAAW,KAAK,QAAQ,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,SAAS,EAAE,IAAI,EAAE,EAAE,KAAK,OAAO,CAAC;AACjF,SAAK,WAAW,GAAG,KAAK,MAAM,SAAS,KAAK,GAAG,WAAW,KAAK,KAAK,GAAG,QAAQ,GAAG,KAAK,MAClF,KAAK,UAAU,SAAY,MAAM,KAAK,KAAK,KAAK,GAAG;AAAA,EAC1D;AAGA,QAAM,WAAW,KAAK,IAAI,+BAA+B,IAAI;AAC7D,WAAS,YAAY;AACrB,aAAW,OAAO,YAAY,OAAO;AACnC,SAAK,UAAU,GAAG,IAAI,SAAS,IAAI,MAAM,IAAI,UAAU;AACvD,QAAI,IAAI,UAAU,WAAW,GAAG;AAC9B,WAAK,UAAU,qBAAqB,iBAAiB;AAAA,IACvD;AACA,eAAW,SAAS,IAAI,WAAW;AACjC,WAAK,UAAU,KAAK,MAAM,MAAM,KAAK,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,KAAK,IAAI,IAAI,WAAW,EAAE;AAAA,IACpG;AAAA,EACF;AAGA,QAAM,YAAY,KAAK,IAAI,yBAAyB,IAAI;AACxD,YAAU,YAAY;AACtB,aAAW,SAAS,YAAY,YAAY;AAC1C,SAAK,WAAW,iBAAiB,KAAK,GAAG,UAAU;AACnD,QAAI,MAAM,WAAW,WAAW,GAAG;AACjC,WAAK,WAAW,qBAAqB,iBAAiB;AAAA,IACxD;AACA,eAAW,KAAK,MAAM,YAAY;AAChC,WAAK,WAAW,KAAK,sBAAsB,CAAC,CAAC,EAAE;AAAA,IACjD;AAAA,EACF;AAOA,MAAI,YAAY,KAAK,SAAS,GAAG;AAC/B,UAAM,WAAW,KAAK,IAAI,4BAA4B,IAAI;AAC1D,aAAS,YAAY;AACrB,SAAK,UAAU,iEAAiE,iBAAiB;AACjG,eAAW,OAAO,YAAY,MAAM;AAClC,WAAK,UAAU,GAAG,IAAI,GAAG,MAAM,IAAI,KAAK,WAAW,IAAI,KAAK,cAAc,IAAI,WAAW,WAAW,IAAI,KAAK,EAAE;AAAA,IACjH;AAAA,EACF;AAGA,QAAM,aAAa,KAAK,IAAI,UAAU,IAAI;AAC1C,aAAW,YAAY;AACvB,OAAK,YAAY,SAAS,OAAO,KAAK,YAAY,OAAO,KAAK,YAAY,OAAO,KAAK,EAAE;AACxF,OAAK,YAAY,SAAS,OAAO,KAAK,gBAAgB,OAAO,SAAS,iBAAiB,OAAO,SAAS,EAAE;AACzG,aAAW,OAAO,YAAY,OAAO;AAKnC,SAAK,YAAY,GAAG,IAAI,MAAM,WAAW,IAAI,OAAO,KAAK,WAAW,IAAI,OAAO,KAAK,WACvE,IAAI,OAAO,KAAK,eAAe,IAAI,OAAO,SAAS,gBAC9C,IAAI,OAAO,SAAS,yBAAyB,IAAI,QAAQ,WAAW,MACjF,IAAI,SAAS,SAAY,YAAY,IAAI,KAAK,OAAO,MAAM,OAG3D,IAAI,iBAAiB,SAAY,mBAAmB,IAAI,YAAY,KAAK,GAAG;AAAA,EACnF;AAEA,GAAC,KAAK,aAAa,SAAS,MAAM,OAAO,EAAE;AAC3C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAgB;AACd,SAAG,OAAO;AAAA,IACZ;AAAA,EACF;AACF;;;ACxFA,IAAM,OAAO;AAKb,IAAM,YAAY;AAClB,IAAM,cAAc;AAGpB,IAAM,eAAgD,oBAAI,IAAI,CAAC,QAAQ,QAAQ,SAAS,OAAO,CAAC;AAIzF,SAAS,WAAW,MAAgD;AACzE,QAAM,KAAK,KAAK;AAChB,QAAM,QAAkC,CAAC;AACzC,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,KAAK,MAAM,CAAC,EAAG,OAAM,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM;AACjG,QAAM,QAAgC,CAAC;AACvC,aAAW,OAAO,KAAK,UAAU,EAAG,OAAM,IAAI,MAAM,IAAI,IAAI;AAC5D,SAAO,EAAE,GAAG,SAAS,MAAM,IAAI,OAAO,MAAM;AAC9C;AAOO,SAAS,eAAe,MAAiC;AAC9D,QAAM,MAAM,KAAK,OAAO;AACxB,QAAM,OAAmC,KAAK,aAAc,WAA8C;AAC1G,MAAI,QAAkB,CAAC;AACvB,MAAI,OAA8B;AAClC,MAAI,SAAS;AACb,MAAI,QAAQ,KAAK;AACjB,MAAI,SAAwB;AAC5B,MAAI,cAAmC;AAGvC,MAAI,YAAY,oBAAI,IAAY;AAEhC,MAAI,CAAC,MAAM;AAET,WAAO,EAAE,SAAS;AAAA,IAAC,GAAG,SAAS;AAAA,IAAC,GAAG,WAAW;AAAA,IAAC,GAAG,QAAQ;AAAE,eAAS;AAAA,IAAM,EAAE;AAAA,EAC/E;AAEA,QAAM,QAAQ,MAAY;AACxB,QAAI,CAAC,QAAQ,KAAK,eAAe,KAAM;AACvC,eAAW,KAAK,OAAO;AAAE,UAAI;AAAE,aAAK,KAAK,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAyB;AAAA,IAAE;AAChF,YAAQ,CAAC;AAAA,EACX;AACA,QAAM,OAAO,CAAC,UAA2B;AACvC,QAAI,OAAQ;AACZ,UAAM,KAAK,KAAK,UAAU,KAAK,CAAC;AAOhC,QAAI,MAAM,SAAS,UAAW,OAAM,OAAO,GAAG,MAAM,SAAS,SAAS;AACtE,UAAM;AAAA,EACR;AAKA,QAAM,YAAY,MAAc;AAC9B,QAAI;AAAE,aAAO,SAAS,OAAO,MAAM,IAAI,CAAC;AAAA,IAAG,QAAQ;AAAE,aAAO,CAAC;AAAA,IAAG;AAAA,EAClE;AACA,QAAM,YAAY,MAAY;AAC5B,UAAM,QAAQ,UAAU;AACxB,UAAM,QAAmB,EAAE,GAAG,SAAS,GAAG,GAAG,OAAO,OAAO,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE;AAClF,QAAI,KAAK,YAAY,OAAW,OAAM,UAAU,KAAK;AACrD,UAAM,QAAQ,MAAM,CAAC;AACrB,QAAI,OAAO;AACT,UAAI;AAAE,cAAM,QAAQ,MAAM,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM;AAAA,MAAG,QAAQ;AAAA,MAA2B;AAAA,IACjG;AAEA,gBAAY,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAC1C,QAAI;AAAE,YAAM,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,IAAG,QAAQ;AAAA,IAAiC;AAAA,EACpF;AACA,QAAM,YAAY,CAAC,SAAqB;AACtC,QAAI;AAAE,WAAK,WAAW,IAAI,CAAC;AAAA,IAAG,QAAQ;AAAA,IAA4B;AAAA,EACpE;AAIA,QAAM,YAAY,MAAY;AAC5B,UAAM,MAAM,UAAU;AACtB,UAAM,MAAM,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AACxC,eAAW,KAAK,KAAK;AACnB,UAAI,UAAU,IAAI,EAAE,EAAE,EAAG;AACzB,gBAAU,IAAI,EAAE,EAAE;AAClB,WAAK,EAAE,GAAG,YAAY,MAAM,EAAE,GAAG,CAAC;AAClC,gBAAU,CAAC;AAAA,IACb;AACA,eAAW,MAAM,CAAC,GAAG,SAAS,GAAG;AAC/B,UAAI,IAAI,IAAI,EAAE,EAAG;AACjB,gBAAU,OAAO,EAAE;AACnB,WAAK,EAAE,GAAG,aAAa,MAAM,GAAG,CAAC;AAAA,IACnC;AAAA,EACF;AACA,QAAM,UAAU,CAAC,QAAgB,UAA4B;AAC3D,QAAI;AACF,gBAAU;AACV,WAAK,EAAE,GAAG,SAAS,MAAM,QAAQ,MAAM,CAAC;AACxC,UAAI,aAAa,IAAI,MAAM,IAAI,GAAG;AAChC,cAAM,IAAI,QAAQ,QAAQ,MAAM;AAChC,YAAI,EAAG,WAAU,CAAC;AAAA,MACpB;AAAA,IACF,QAAQ;AAAA,IAA4B;AAAA,EACtC;AAEA,MAAI;AACF,WAAO,IAAI,KAAK,GAAG;AACnB,SAAK,iBAAiB,QAAQ,MAAM;AAClC,gBAAU;AACV,YAAM;AAAA,IACR,CAAC;AAID,SAAK,iBAAiB,WAAW,CAAC,OAA0B;AAC1D,UAAI,CAAC,KAAK,YAAY,OAAO,GAAG,SAAS,SAAU;AACnD,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,GAAG,IAAI;AAC9B,YAAI,IAAI,MAAM,YAAY,OAAO,IAAI,UAAU,YAAY,OAAO,IAAI,SAAS,UAAU;AACvF,eAAK,SAAS,EAAE,OAAO,IAAI,OAAO,MAAM,IAAI,KAAK,CAAC;AAAA,QACpD;AAAA,MACF,QAAQ;AAAA,MAAmB;AAAA,IAC7B,CAAC;AACD,SAAK,iBAAiB,SAAS,MAAM;AAAA,IAA2C,CAAC;AAQjF,SAAK,iBAAiB,SAAS,MAAM;AAAE,aAAO;AAAM,eAAS;AAAM,cAAQ,CAAC;AAAA,IAAG,CAAC;AAAA,EAClF,QAAQ;AAAE,WAAO;AAAM,aAAS;AAAA,EAAM;AAEtC,QAAM,SAAS,MAAY;AACzB,kBAAc;AACd,kBAAc;AACd,aAAS;AACT,gBAAY,oBAAI,IAAI;AAAA,EACtB;AAEA,SAAO;AAAA,IACL,OAAO,MAAoB;AACzB,UAAI,OAAQ;AACZ,aAAO;AACP,eAAS;AACT,UAAI;AAAE,sBAAc,KAAK,eAAe,OAAO;AAAA,MAAG,QAAQ;AAAE,iBAAS;AAAM;AAAA,MAAQ;AAGnF,kBAAY,IAAI,IAAI,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAChD,iBAAW,KAAK,UAAU,EAAG,WAAU,CAAC;AAAA,IAC1C;AAAA,IACA;AAAA,IACA,SAAS,MAAoB;AAC3B,UAAI,UAAU,SAAS,MAAO;AAC9B,cAAQ;AAGR,UAAI,QAAQ,KAAK,eAAe,MAAM;AACpC,kBAAU;AACV,mBAAW,KAAK,UAAU,EAAG,WAAU,CAAC;AAAA,MAC1C;AAAA,IACF;AAAA,IACA,QAAc;AACZ,eAAS;AACT,aAAO;AACP,cAAQ,CAAC;AACT,UAAI;AAAE,cAAM,MAAM;AAAA,MAAG,QAAQ;AAAA,MAAqB;AAClD,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;AC7PA,OAAuB;AAuBhB,SAAS,gBAAgB,QAAgB,YAAoB,OAAsB,CAAC,GAAqB;AAC9G,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,UAAU;AAAA,EAChC,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,OAAO,kCAAkC;AAAA,EAC/D;AACA,MAAI;AAGF,UAAM,EAAE,QAAQ,KAAK,IAAI,OAAO,QAAQ,QAAQ,IAAI;AACpD,WAAO,EAAE,IAAI,MAAM,QAAQ,MAAM,OAAO;AAAA,EAC1C,SAAS,GAAG;AACV,WAAO,EAAE,IAAI,OAAO,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,EAAE;AAAA,EACxE;AACF;;;ACbO,SAAS,qBAAqB,QAAgC;AACnE,QAAM,MAAM,IAAI,YAAS,OAAO,MAAM,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC;AACzE,SAAO;AAAA,IACL,UAAU;AAAA,MACR,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC;AAAA,MACrB,KAAK,CAAC,GAAW,MAAmB;AAAE,YAAI,IAAI,GAAG,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,MAAG;AAAA,IACvE;AAAA,IACA;AAAA,IACA,QAAQ,MAAM,IAAI;AAAA,IAClB,MAAM,CAAC,WAAW,IAAI,KAAK,MAAM;AAAA,EACnC;AACF;","names":["line","createStateLogger","show","head","line","showVal"]}