{"version":3,"file":"contract-BHqxMUWm.cjs","names":[],"sources":["../src/core/contract.ts"],"sourcesContent":["// Host-agnostic dispatch envelope for the presentMulmoScript View. The Vue\n// View is decoupled from any one host's REST surface: it calls\n// `useRuntime().dispatch({ kind, … })`, the host routes that to its\n// mulmoScript dispatch handler, and every response is an `{ ok: … }`\n// envelope so failures travel as data (no HTTP-status coupling, no\n// \"dispatch failed (500)\" prefixes in user-facing errors).\n//\n// Long-running generation (movie / PDF) is a single long-held dispatch that\n// resolves when the pipeline finishes; per-beat progress arrives on the\n// plugin pubsub channel (`GENERATION_EVENT`) instead of an SSE stream, which\n// also covers generations started elsewhere (background autoGenerateMovie,\n// another tab, the agent).\n\n/** One in-flight or per-beat generation notice, published on the plugin\n *  pubsub `generation` channel and returned by the `pendingGenerations`\n *  snapshot. Value strings mirror @mulmobridge/protocol's GENERATION_KINDS\n *  so MulmoClaude's host bridge maps 1:1 without a lookup table. */\nexport interface MulmoScriptGenerationEvent {\n  kind: \"beatImage\" | \"beatAudio\" | \"characterImage\" | \"movie\" | \"pdf\";\n  /** Wire `stories/…` path of the script the generation belongs to. */\n  filePath: string;\n  /** Which stories root `filePath` is relative to (#3014). Absent = the\n   *  host's default root, which is every event this package emitted before\n   *  roots existed. */\n  root?: string;\n  /** beatIndex (as string) for beat*, character key for characterImage, \"\" for movie/pdf. */\n  key: string;\n  /** false = started, true = finished (reload the asset off disk). */\n  done: boolean;\n  /** Only set on done=true when the work failed. */\n  error?: string;\n}\n\n/** Plugin pubsub event name the host publishes generation events on\n *  (full channel: `plugin:<scope>:generation`). */\nexport const GENERATION_EVENT = \"generation\";\n\n/** Plugin pubsub event name for \"this script changed on disk\"\n *  (full channel: `plugin:<scope>:scriptChanged`). */\nexport const SCRIPT_CHANGED_EVENT = \"scriptChanged\";\n\n/**\n * A script was written — by the agent, or by another View.\n *\n * `origin` is who wrote it. A View passes its own id on every write and ignores the echo of\n * its own: without that, a keystroke would round-trip through the server and reload the very\n * element the caret is in. An agent write carries no origin, so every View reloads.\n */\nexport interface MulmoScriptChangedEvent {\n  filePath: string;\n  /** Which stories root `filePath` is relative to (#3014). Absent = default. */\n  root?: string;\n  origin?: string;\n}\n\n/**\n * Whether a View watching `watching` should reload because of `event`.\n *\n * A pure rule rather than a condition inside the subscriber, because the case that matters is\n * the one that is invisible when it is wrong: a View acting on the echo of its own write\n * rebuilds the element the caret is in, on every keystroke.\n */\nexport const shouldReloadForScriptChange = (event: MulmoScriptChangedEvent, watching: string, ownOrigin: string, watchingRoot?: string): boolean =>\n  watching !== \"\" && event.filePath === watching && sameRoot(event.root, watchingRoot) && event.origin !== ownOrigin;\n\n/** The default root: what a caller that names no root is asking for. */\nexport const DEFAULT_ROOT = \"\";\n\n/**\n * The one spelling of a root that every comparison and every key must use.\n *\n * It lives HERE, in the module both the server and the View import, because\n * the server had its own copy and the browser side compared raw strings — so\n * `publishGeneration` emitted `\"repoA\"` while a View watching `\" repoA \"`\n * dropped every event of its own generation (CodeRabbit on #3015). A rule\n * that two sides must agree on cannot live on one of the two sides.\n *\n * Trimmed, because the codebase's other opaque \"which project root\" reader —\n * `readCommandScope` in `@mulmoclaude/core/remote-host` — trims its value and\n * shares the \"absent = the host's own root\" convention. Without this,\n * `\" repoA \"` is one root there and a different one here.\n *\n * A non-string reads as the default root rather than throwing. The type says\n * that cannot happen, but this package is published and its callers include\n * untyped JavaScript: a subscription whose `root()` returns `42` reached\n * `.trim()` and threw from INSIDE a pubsub callback, where nothing catches it\n * and the View simply stops updating (Codex P2 on #3015). The guard belongs\n * here rather than at the three call sites, because a rule enforced by\n * enumerating its callers is what this PR got wrong repeatedly.\n */\nexport const normalizeRoot = (root: string | undefined): string => (typeof root === \"string\" ? root.trim() : DEFAULT_ROOT);\n\n/**\n * Two roots are the same when they name the same one, with absent meaning the\n * host's default (#3014).\n *\n * The identity of a script is the PAIR, not the path: `stories/deck.json` in\n * two repositories is two files, and comparing paths alone would reload the\n * View watching one because the other was saved. Absent normalises to the\n * default so a pre-`root` event and a default-root watcher still match — that\n * equivalence is what keeps every existing card working untouched.\n */\nexport const sameRoot = (a: string | undefined, b: string | undefined): boolean => normalizeRoot(a) === normalizeRoot(b);\n\ninterface BeatRef {\n  filePath: string;\n  beatIndex: number;\n}\n\ninterface CharacterRef {\n  filePath: string;\n  key: string;\n}\n\n/** Session tag for hosts that surface per-session generation indicators\n *  (MulmoClaude's sidebar). Optional everywhere; hosts without sessions\n *  ignore it. */\ninterface SessionTag {\n  chatSessionId?: string | undefined;\n}\n\n/**\n * Which registered stories root the named script lives in (#3014).\n *\n * Intersected into the whole union below, so every dispatch that names a\n * `filePath` can name its root — a union member that could not would make\n * root-aware calls untypeable while the handler happily read `args.root`\n * off an untyped record (Codex P1 on #3015).\n */\ninterface RootTag {\n  root?: string | undefined;\n}\n\nexport type MulmoScriptDispatchArgs = RootTag &\n  (\n    | ({ kind: \"save\" } & { filePath?: string; script?: unknown; filename?: string })\n    | { kind: \"updateBeat\"; filePath: string; beatIndex: number; beat: unknown; origin?: string }\n    | { kind: \"updateScript\"; filePath: string; script: unknown; origin?: string }\n    | ({ kind: \"beatImage\" } & BeatRef)\n    | ({ kind: \"beatAudio\" } & BeatRef)\n    | ({ kind: \"beatMovie\" } & BeatRef)\n    | ({ kind: \"renderBeat\" } & BeatRef & SessionTag & { force?: boolean })\n    | ({ kind: \"generateBeatAudio\" } & BeatRef & SessionTag & { force?: boolean })\n    | ({ kind: \"uploadBeatImage\" } & BeatRef & { imageData: string })\n    | ({ kind: \"characterImage\" } & CharacterRef)\n    | ({ kind: \"renderCharacter\" } & CharacterRef & SessionTag & { force?: boolean })\n    | ({ kind: \"uploadCharacterImage\" } & CharacterRef & { imageData: string })\n    | ({ kind: \"movieStatus\" } & { filePath: string })\n    | ({ kind: \"pdfStatus\" } & { filePath: string })\n    | ({ kind: \"generateMovie\" } & { filePath: string } & SessionTag)\n    | ({ kind: \"generatePdf\" } & { filePath: string } & SessionTag)\n    | { kind: \"pendingGenerations\"; filePath: string }\n  );\n\nexport type MulmoScriptDispatchKind = MulmoScriptDispatchArgs[\"kind\"];\n\n/** Failure half of every dispatch response. `code` mirrors the phase-1\n *  outcome codes so a host can log/telemetry on it; the View only reads\n *  `error`. */\nexport interface DispatchFailure {\n  ok: false;\n  code?: \"bad_request\" | \"not_found\" | \"server_error\";\n  error: string;\n}\n\nexport type DispatchEnvelope<T> = ({ ok: true } & T) | DispatchFailure;\n\n/** The success payload of each dispatch `kind`, BEFORE the root tag below is\n *  applied. Not exported: `MulmoScriptDispatchResult` is the type callers use. */\ninterface DispatchResultPayloads {\n  save: { script: Record<string, unknown>; filePath: string; message: string };\n  updateBeat: Record<string, never>;\n  updateScript: Record<string, never>;\n  beatImage: { image: string | null };\n  beatAudio: { audio: string | null };\n  beatMovie: { moviePath: string | null };\n  renderBeat: { image: string };\n  generateBeatAudio: { audio: string };\n  uploadBeatImage: { image: string };\n  characterImage: { image: string | null };\n  renderCharacter: { image: string };\n  uploadCharacterImage: { image: string };\n  movieStatus: { moviePath: string | null };\n  pdfStatus: { pdfPath: string | null };\n  generateMovie: { moviePath: string };\n  generatePdf: { pdfPath: string };\n  pendingGenerations: { pending: MulmoScriptGenerationEvent[] };\n}\n\n/**\n * Maps a dispatch `kind` to its success payload, every one of them carrying\n * the root it acted in.\n *\n * A host builds its cards from these results, and a card's identity is the\n * PAIR `(root, filePath)` — `stories/deck.json` exists in every registered\n * root. Without the root here, two repositories' identically-named decks\n * collapse onto one card, which is #3014's third collision point, and no\n * amount of fixing the host's identity function helps because the value never\n * arrives.\n *\n * `root` was threaded through the ARGS and the EVENTS in #3015 and not through\n * the results — the same shape that PR got wrong repeatedly: the comparison\n * widened while the path carrying the data to it did not. Applied as a mapped\n * type rather than field by field so a kind added later cannot be forgotten.\n *\n * Absent means the default root, so every pre-`root` card is unchanged.\n */\nexport type MulmoScriptDispatchResult = {\n  [K in keyof DispatchResultPayloads]: DispatchResultPayloads[K] & RootTag;\n};\n"],"mappings":";;;AAmCA,IAAa,mBAAmB;;;AAIhC,IAAa,uBAAuB;;;;;;;;AAuBpC,IAAa,+BAA+B,OAAgC,UAAkB,WAAmB,iBAC/G,aAAa,MAAM,MAAM,aAAa,YAAY,SAAS,MAAM,MAAM,YAAY,KAAK,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;AA2B3G,IAAa,iBAAiB,SAAsC,OAAO,SAAS,WAAW,KAAK,KAAK,IAAA;;;;;;;;;;;AAYzG,IAAa,YAAY,GAAuB,MAAmC,cAAc,CAAC,MAAM,cAAc,CAAC"}