@storylet-studio/play-helpers 0.8.2 → 0.9.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/README.md +2 -2
- package/dist/index.cjs +8 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/storyletengine.min.js +4 -4
- package/dist/storyletengine.min.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ same name, so an integration reads the same in every engine
|
|
|
25
25
|
run, and a save in the editor swaps the new bundle in underneath it without
|
|
26
26
|
a restart. Attach the **engine**, not a flow: the link discovers your flows
|
|
27
27
|
itself and announces them as they open and close.
|
|
28
|
-
→ [Live Link](https://
|
|
28
|
+
→ [Live Link](https://storylets.dev/play/live-link/)
|
|
29
29
|
- **The examiners** - `createPropertyInspector`, `createBundleInspector`. A
|
|
30
30
|
DOM panel showing live state, the run log and what a bundle offers. The web
|
|
31
31
|
equivalent of the Runtime State window each engine port ships.
|
|
@@ -52,4 +52,4 @@ const saved = serializeState(engine); // hand the string to your save system
|
|
|
52
52
|
The demo in [`demo/`](./demo) wires all of it together against the Hamlet
|
|
53
53
|
bundle and is the shortest complete example.
|
|
54
54
|
|
|
55
|
-
→ Full documentation: [
|
|
55
|
+
→ Full documentation: [storylets.dev/play/javascript](https://storylets.dev/play/javascript/)
|
package/dist/index.cjs
CHANGED
|
@@ -584,13 +584,14 @@ function createBundleInspector(bundle, opts = {}) {
|
|
|
584
584
|
line(propsBody, ` ${formatPropertySummary(p)}`);
|
|
585
585
|
}
|
|
586
586
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
587
|
+
const map = description.map;
|
|
588
|
+
if (map !== void 0) {
|
|
589
|
+
const mapBody = fold(el, "Project map", open);
|
|
590
|
+
mapBody.className = "sl-maps";
|
|
591
|
+
line(mapBody, `${map.group}: ${map.tags.join(", ")}`);
|
|
592
|
+
line(mapBody, `boxes on the map: ${map.boxes.length > 0 ? map.boxes.join(", ") : "none"}`);
|
|
593
|
+
const sites = Object.entries(map.sites).map(([box, n]) => `${box} ${n}`).join(", ");
|
|
594
|
+
line(mapBody, `geometry carried (the engine ignores it): zones ${map.zones}, pictures ${map.backgrounds}, sites ${sites === "" ? "0" : sites}`, "sl-line sl-note");
|
|
594
595
|
}
|
|
595
596
|
const countsBody = fold(el, "Counts", open);
|
|
596
597
|
countsBody.className = "sl-counts";
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/logger.ts","../src/save.ts","../src/inspector.ts","../src/bundle-inspector.ts","../src/live-link.ts","../src/refresh.ts","../src/world.ts"],"sourcesContent":["// @storylet-studio/play-helpers - the dev-experience layer over the pure\n// runtime (design/engine-runtimes.md 1: the split is purity, not\n// distribution; the runtime never touches the DOM or I/O, this does).\nexport { createKernelStateLogger, createStateLogger, snapshotState, diffState } from \"./logger.js\";\nexport type { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, StateSnapshot } from \"./logger.js\";\nexport { serializeState, deserializeState, saveState, loadState } from \"./save.js\";\nexport { createPropertyInspector, ensureInspectorStyle, formatLogEntry } from \"./inspector.js\";\nexport type { PropertyInspector, PropertyInspectorOptions } from \"./inspector.js\";\nexport { createBundleInspector, formatPropertySummary, formatScopeLabel } from \"./bundle-inspector.js\";\nexport type { BundleInspector, BundleInspectorOptions } from \"./bundle-inspector.js\";\nexport { createLiveLink, boardFrame } from \"./live-link.js\";\nexport type { LiveLink, LiveLinkOptions, LiveSocketLike, LiveFrame } from \"./live-link.js\";\nexport { applyLiveBundle } from \"./refresh.js\";\nexport type { LiveBundleResult } from \"./refresh.js\";\nexport { createWorldContainer } from \"./world.js\";\nexport type { WorldContainer } from \"./world.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// 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":"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","names":["createKernelStateLogger","createKernelStateLogger","head","line","showVal","import_runtime","import_scoperegistry","StateBag"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/logger.ts","../src/save.ts","../src/inspector.ts","../src/bundle-inspector.ts","../src/live-link.ts","../src/refresh.ts","../src/world.ts"],"sourcesContent":["// @storylet-studio/play-helpers - the dev-experience layer over the pure\n// runtime (design/engine-runtimes.md 1: the split is purity, not\n// distribution; the runtime never touches the DOM or I/O, this does).\nexport { createKernelStateLogger, createStateLogger, snapshotState, diffState } from \"./logger.js\";\nexport type { StateChange, StateLogger, StateLoggerAdapter, StateLoggerOptions, StateSnapshot } from \"./logger.js\";\nexport { serializeState, deserializeState, saveState, loadState } from \"./save.js\";\nexport { createPropertyInspector, ensureInspectorStyle, formatLogEntry } from \"./inspector.js\";\nexport type { PropertyInspector, PropertyInspectorOptions } from \"./inspector.js\";\nexport { createBundleInspector, formatPropertySummary, formatScopeLabel } from \"./bundle-inspector.js\";\nexport type { BundleInspector, BundleInspectorOptions } from \"./bundle-inspector.js\";\nexport { createLiveLink, boardFrame } from \"./live-link.js\";\nexport type { LiveLink, LiveLinkOptions, LiveSocketLike, LiveFrame } from \"./live-link.js\";\nexport { applyLiveBundle } from \"./refresh.js\";\nexport type { LiveBundleResult } from \"./refresh.js\";\nexport { createWorldContainer } from \"./world.js\";\nexport type { WorldContainer } from \"./world.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// 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 // --- the project map: which boxes are on it, and any geometry carried ---\n //\n // Only when there IS one. 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. The zones themselves are already under Properties and the opted-in\n // boxes' peek criteria; this says where they live and what the build carried\n // besides, since the geometry is inert and would otherwise go unmentioned.\n const map = description.map;\n if (map !== undefined) {\n const mapBody = fold(el, \"Project map\", open);\n mapBody.className = \"sl-maps\";\n line(mapBody, `${map.group}: ${map.tags.join(\", \")}`);\n line(mapBody, `boxes on the map: ${map.boxes.length > 0 ? map.boxes.join(\", \") : \"none\"}`);\n const sites = Object.entries(map.sites).map(([box, n]) => `${box} ${n}`).join(\", \");\n line(mapBody, `geometry carried (the engine ignores it): zones ${map.zones}, pictures ${map.backgrounds}`\n + `, sites ${sites === \"\" ? \"0\" : sites}`, \"sl-line sl-note\");\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":"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","names":["createKernelStateLogger","createKernelStateLogger","head","line","showVal","import_runtime","import_scoperegistry","StateBag"]}
|
package/dist/index.js
CHANGED
|
@@ -544,13 +544,14 @@ function createBundleInspector(bundle, opts = {}) {
|
|
|
544
544
|
line(propsBody, ` ${formatPropertySummary(p)}`);
|
|
545
545
|
}
|
|
546
546
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
547
|
+
const map = description.map;
|
|
548
|
+
if (map !== void 0) {
|
|
549
|
+
const mapBody = fold(el, "Project map", open);
|
|
550
|
+
mapBody.className = "sl-maps";
|
|
551
|
+
line(mapBody, `${map.group}: ${map.tags.join(", ")}`);
|
|
552
|
+
line(mapBody, `boxes on the map: ${map.boxes.length > 0 ? map.boxes.join(", ") : "none"}`);
|
|
553
|
+
const sites = Object.entries(map.sites).map(([box, n]) => `${box} ${n}`).join(", ");
|
|
554
|
+
line(mapBody, `geometry carried (the engine ignores it): zones ${map.zones}, pictures ${map.backgrounds}, sites ${sites === "" ? "0" : sites}`, "sl-line sl-note");
|
|
554
555
|
}
|
|
555
556
|
const countsBody = fold(el, "Counts", open);
|
|
556
557
|
countsBody.className = "sl-counts";
|