{"version":3,"sources":["../../src/notifiers/discord.ts","../../src/notifiers/base.ts","../../src/notifiers/types.ts","../../src/renderers/ascii.ts","../../src/types.ts","../../src/utils/timing.ts","../../src/renderers/colors.ts","../../src/renderers/mermaid.ts","../../src/performance-analyzer.ts","../../src/kroki/encoder.ts","../../src/kroki/url.ts","../../src/kroki/mermaid-ink.ts","../../src/notifiers/context.ts","../../src/live/live-session.ts","../../src/parallel-detector.ts","../../src/ir-builder/index.ts","../../src/notifiers/adapters/discord.ts"],"sourcesContent":["/**\n * Discord Notifier\n *\n * Discord webhook notifier for pushing workflow visualizations.\n * Uses plain HTTP webhooks - no Discord SDK required.\n */\n\nimport type { Notifier, BaseNotifierOptions, ProviderOptions } from \"./types\";\nimport { createNotifier } from \"./base\";\nimport { createDiscordAdapter, type DiscordAdapterOptions } from \"./adapters/discord\";\n\n/**\n * Options for Discord notifier.\n */\nexport interface DiscordNotifierOptions extends BaseNotifierOptions {\n  /** Discord webhook URL */\n  webhookUrl: string;\n  /** Bot username to display (optional) */\n  username?: string;\n  /** Bot avatar URL (optional) */\n  avatarUrl?: string;\n  /** Request timeout in milliseconds (default: 30000) */\n  timeout?: number;\n  /**\n   * Diagram rendering provider configuration.\n   * REQUIRED - no silent default to prevent surprise network calls.\n   */\n  diagramProvider: ProviderOptions;\n}\n\n/**\n * Create a Discord notifier.\n *\n * @param options - Discord configuration\n * @returns A Notifier instance\n *\n * @example\n * ```typescript\n * import { createDiscordNotifier } from 'awaitly-visualizer/notifiers/discord';\n *\n * // Using Kroki (default)\n * const discord = createDiscordNotifier({\n *   webhookUrl: process.env.DISCORD_WEBHOOK!,\n *   username: 'Workflow Bot',\n *   diagramProvider: { provider: 'kroki' },\n * });\n *\n * // Using mermaid.ink with dark theme\n * const discordDark = createDiscordNotifier({\n *   webhookUrl: process.env.DISCORD_WEBHOOK!,\n *   diagramProvider: {\n *     provider: 'mermaid-ink',\n *     theme: 'dark',\n *     bgColor: '1b1b1f',\n *   },\n * });\n *\n * // One-time notification\n * await discord.notify(workflowIR, { title: 'Order Processing' });\n *\n * // Live updates\n * const live = discord.createLive({ title: 'Order #123' });\n * workflow.on('event', (e) => live.update(e));\n * await live.finalize();\n * ```\n */\nexport function createDiscordNotifier(\n  options: DiscordNotifierOptions\n): Notifier {\n  const { webhookUrl, username, avatarUrl, timeout, ...baseOptions } = options;\n\n  const adapter = createDiscordAdapter({\n    webhookUrl,\n    username,\n    avatarUrl,\n    timeout,\n  });\n\n  return createNotifier(baseOptions, adapter);\n}\n","/**\n * Base Notifier Factory\n *\n * Creates a notifier from an adapter.\n * Handles all shared logic - adapters only handle platform-specific concerns.\n */\n\nimport { ok, err, type AsyncResult } from \"awaitly\";\nimport type { WorkflowIR } from \"../types\";\nimport type {\n  Notifier,\n  NotifyOptions,\n  LiveSession,\n  LiveSessionOptions,\n  BaseNotifierOptions,\n  WorkflowStatus,\n  NotifyError,\n} from \"./types\";\nimport type { NotifierAdapter } from \"./adapter\";\nimport { createNotifierContext } from \"./context\";\nimport { createLiveSession } from \"../live/live-session\";\n\n/**\n * Resolve workflow status from IR state.\n */\nfunction resolveStatus(ir: WorkflowIR): WorkflowStatus | \"running\" {\n  switch (ir.root.state) {\n    case \"success\":\n      return \"completed\";\n    case \"error\":\n      return \"failed\";\n    case \"aborted\":\n      return \"cancelled\";\n    default:\n      return \"running\";\n  }\n}\n\n/**\n * Create a notifier from an adapter.\n * Handles all shared logic - adapters only handle platform-specific concerns.\n *\n * @param options - Base notifier options\n * @param adapter - Platform-specific adapter\n * @returns A Notifier instance\n *\n * @example\n * ```typescript\n * const notifier = createNotifier(\n *   { diagramProvider: { provider: 'kroki' } },\n *   createDiscordAdapter({ webhookUrl: '...' })\n * );\n * ```\n */\nexport function createNotifier<TMessageId = string>(\n  options: BaseNotifierOptions,\n  adapter: NotifierAdapter<TMessageId>\n): Notifier {\n  const { diagramProvider, debounceMs = 500, maxWaitMs = 5000 } = options;\n\n  // Validate required options (runtime check for JS consumers)\n  if (!diagramProvider) {\n    throw new Error(\n      `${adapter.name}Notifier: diagramProvider is required. ` +\n        `Pass { provider: 'kroki' } or { provider: 'mermaid-ink' }.`\n    );\n  }\n\n  // Create shared context once\n  const ctx = createNotifierContext(diagramProvider);\n\n  async function notify(\n    ir: WorkflowIR,\n    notifyOptions: NotifyOptions = {}\n  ): AsyncResult<string | undefined, NotifyError> {\n    const title = notifyOptions.title ?? \"Workflow\";\n    const status = resolveStatus(ir);\n\n    try {\n      const message = adapter.buildMessage({ ir, title, status }, ctx);\n      const messageId = await adapter.sendNew(message);\n      return ok(messageId?.toString());\n    } catch {\n      return err(\"SEND_FAILED\");\n    }\n  }\n\n  function createLive(sessionOptions: LiveSessionOptions): LiveSession {\n    return createLiveSession(\n      {\n        ...sessionOptions,\n        debounceMs: sessionOptions.debounceMs ?? debounceMs,\n        maxWaitMs: sessionOptions.maxWaitMs ?? maxWaitMs,\n      },\n      {\n        async postNew(ir, title) {\n          const message = adapter.buildMessage(\n            { ir, title, status: \"running\" },\n            ctx\n          );\n          const messageId = await adapter.sendNew(message);\n          return messageId?.toString();\n        },\n\n        async updateExisting(messageId, ir, title) {\n          const message = adapter.buildMessage(\n            { ir, title, status: \"running\" },\n            ctx\n          );\n          await adapter.sendUpdate(messageId as TMessageId, message);\n        },\n\n        async finalize(messageId, ir, title, status) {\n          const message = adapter.buildMessage({ ir, title, status }, ctx);\n          if (messageId) {\n            await adapter.sendUpdate(messageId as TMessageId, message);\n          } else {\n            await adapter.sendNew(message);\n          }\n        },\n\n        async cancel(messageId) {\n          if (adapter.sendCancel && messageId) {\n            await adapter.sendCancel(messageId as TMessageId);\n          }\n        },\n      }\n    );\n  }\n\n  return {\n    notify,\n    createLive,\n    getType: () => adapter.name.toLowerCase(),\n  };\n}\n","/**\n * Notifier Types\n *\n * Interfaces for notification systems that push workflow visualizations\n * to external services like Slack, Discord, or custom webhooks.\n */\n\nimport type { AsyncResult } from \"awaitly\";\nimport type { WorkflowEvent } from \"awaitly\";\nimport type {\n  FlowNode,\n  WorkflowIR,\n  ScopeStartEvent,\n  ScopeEndEvent,\n  DecisionStartEvent,\n  DecisionBranchEvent,\n  DecisionEndEvent,\n} from \"../types\";\nimport type { MermaidInkOptions, MermaidInkTheme } from \"../kroki/mermaid-ink\";\nimport type { UrlGeneratorOptions } from \"../kroki/url\";\n\n/**\n * Extended event types that the live session can handle.\n * Includes both core WorkflowEvent and visualizer-specific events.\n */\nexport type LiveSessionEvent =\n  | WorkflowEvent<unknown>\n  | ScopeStartEvent\n  | ScopeEndEvent\n  | DecisionStartEvent\n  | DecisionBranchEvent\n  | DecisionEndEvent;\n\n/**\n * Error types for notifier operations.\n */\nexport type NotifyError = \"SEND_FAILED\" | \"INVALID_CONFIG\";\n\n/**\n * Diagram rendering provider.\n */\nexport type DiagramProvider = \"kroki\" | \"mermaid-ink\";\n\n/**\n * Options for Kroki provider.\n */\nexport interface KrokiProviderOptions extends UrlGeneratorOptions {\n  /** Provider type */\n  provider: \"kroki\";\n}\n\n/**\n * Options for mermaid.ink provider.\n */\nexport interface MermaidInkProviderOptions extends MermaidInkOptions {\n  /** Provider type */\n  provider: \"mermaid-ink\";\n}\n\n/**\n * Combined provider options.\n */\nexport type ProviderOptions = KrokiProviderOptions | MermaidInkProviderOptions;\n\n/**\n * Status of a workflow when finalized.\n */\nexport type WorkflowStatus = \"completed\" | \"failed\" | \"cancelled\";\n\n/**\n * Options for creating a live session.\n */\nexport interface LiveSessionOptions {\n  /** Title for the notification (e.g., \"Order #123 Processing\") */\n  title: string;\n  /** Debounce interval in milliseconds (default: 500) */\n  debounceMs?: number;\n  /** Max time between posts even during constant updates (default: 5000) */\n  maxWaitMs?: number;\n  /** Additional metadata to include in notifications */\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Live session for real-time workflow updates.\n *\n * Behavior:\n * - First `update()`: Posts new message with Kroki URL\n * - Subsequent `update()`: Edits same message (debounced)\n * - `finalize()`: Posts final state, marks session complete\n * - `cancel()`: Aborts session, optionally deletes partial message\n */\nexport interface LiveSession {\n  /** Update the visualization (debounced) */\n  update(eventOrIr: LiveSessionEvent | WorkflowIR): void;\n\n  /** Finalize the session with final status */\n  finalize(status?: WorkflowStatus): Promise<void>;\n\n  /** Cancel the session, optionally deleting the message */\n  cancel(): void;\n\n  /** Get the current session ID (e.g., message timestamp for Slack) */\n  getSessionId(): string | undefined;\n\n  /** Check if the session is still active */\n  isActive(): boolean;\n}\n\n/**\n * Result of a notification operation.\n */\nexport interface NotifyResult {\n  /** Whether the operation succeeded */\n  success: boolean;\n  /** Error message if failed */\n  error?: string;\n  /** Platform-specific message ID */\n  messageId?: string;\n}\n\n/**\n * Notifier interface for pushing workflow visualizations.\n */\nexport interface Notifier {\n  /** Send a one-time notification with the current workflow state */\n  notify(ir: WorkflowIR, options?: NotifyOptions): AsyncResult<string | undefined, NotifyError>;\n\n  /** Create a live session for real-time updates */\n  createLive(options: LiveSessionOptions): LiveSession;\n\n  /** Get the notifier type (e.g., \"slack\", \"discord\", \"webhook\") */\n  getType(): string;\n}\n\n/**\n * Options for one-time notifications.\n */\nexport interface NotifyOptions {\n  /** Title for the notification */\n  title?: string;\n  /** Additional text content */\n  text?: string;\n  /** Additional metadata */\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Base options for all notifiers.\n */\nexport interface BaseNotifierOptions {\n  /** Debounce interval in milliseconds for live sessions (default: 500) */\n  debounceMs?: number;\n  /** Max wait between updates during constant churn (default: 5000) */\n  maxWaitMs?: number;\n  /**\n   * Diagram rendering provider configuration.\n   * REQUIRED - no silent default to prevent surprise network calls.\n   *\n   * @example\n   * ```typescript\n   * // Use Kroki\n   * { provider: 'kroki' }\n   *\n   * // Use mermaid.ink with dark theme\n   * { provider: 'mermaid-ink', theme: 'dark', bgColor: '1b1b1f' }\n   *\n   * // Use self-hosted Kroki\n   * { provider: 'kroki', baseUrl: 'https://kroki.internal' }\n   * ```\n   */\n  diagramProvider: ProviderOptions;\n}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Recursively count step nodes in a workflow IR.\n * Counts steps nested inside parallel, race, and decision containers.\n */\nexport function countSteps(ir: WorkflowIR): number {\n  function countInNodes(nodes: FlowNode[]): number {\n    let count = 0;\n    for (const node of nodes) {\n      if (node.type === \"step\") {\n        count++;\n      } else if (node.type === \"decision\" && node.branches) {\n        // Only count from branches (taken branch children); skip node.children to avoid double-counting when both exist\n        for (const branch of node.branches) {\n          if (branch.taken) {\n            count += countInNodes(branch.children);\n          }\n        }\n      } else if (\"children\" in node && node.children) {\n        count += countInNodes(node.children);\n      }\n    }\n    return count;\n  }\n  return countInNodes(ir.root.children);\n}\n","/**\n * ASCII Terminal Renderer\n *\n * Renders the workflow IR as ASCII art with box-drawing characters\n * and ANSI colors for terminal display.\n */\n\nimport { ok, err, type Result } from \"awaitly\";\nimport type {\n  FlowNode,\n  ParallelNode,\n  RaceNode,\n  DecisionNode,\n  StreamNode,\n  Renderer,\n  RenderOptions,\n  StepNode,\n  WorkflowIR,\n  EnhancedRenderOptions,\n  HeatLevel,\n  WorkflowHooks,\n  HookExecution,\n} from \"../types\";\nimport { isParallelNode, isRaceNode, isStepNode, isDecisionNode, isStreamNode } from \"../types\";\nimport { formatDuration } from \"../utils/timing\";\nimport {\n  bold,\n  colorByState,\n  colorize,\n  defaultColorScheme,\n  dim,\n  getColoredSymbol,\n  stripAnsi,\n} from \"./colors\";\n\n/**\n * Error types for stringify operations.\n */\nexport type StringifyError = \"STRINGIFY_ERROR\";\n\n// =============================================================================\n// Box Drawing Characters\n// =============================================================================\n\nconst BOX = {\n  topLeft: \"┌\",\n  topRight: \"┐\",\n  bottomLeft: \"└\",\n  bottomRight: \"┘\",\n  horizontal: \"─\",\n  vertical: \"│\",\n  teeRight: \"├\",\n  teeLeft: \"┤\",\n  teeDown: \"┬\",\n  teeUp: \"┴\",\n  cross: \"┼\",\n} as const;\n\n// =============================================================================\n// Heatmap Colors (ANSI)\n// =============================================================================\n\n/**\n * ANSI color codes for heatmap visualization.\n */\nconst HEAT_COLORS: Record<HeatLevel, string> = {\n  cold: \"\\x1b[34m\",      // Blue\n  cool: \"\\x1b[36m\",      // Cyan\n  neutral: \"\",           // Default (no color)\n  warm: \"\\x1b[33m\",      // Yellow\n  hot: \"\\x1b[31m\",       // Red\n  critical: \"\\x1b[41m\",  // Red background\n};\n\nconst RESET = \"\\x1b[0m\";\n\n/**\n * Get ANSI color code for a heat level.\n */\nfunction getHeatColor(heat: number): string {\n  if (heat < 0.2) return HEAT_COLORS.cold;\n  if (heat < 0.4) return HEAT_COLORS.cool;\n  if (heat < 0.6) return HEAT_COLORS.neutral;\n  if (heat < 0.8) return HEAT_COLORS.warm;\n  if (heat < 0.95) return HEAT_COLORS.hot;\n  return HEAT_COLORS.critical;\n}\n\n/**\n * Apply heat coloring to a string.\n */\nfunction applyHeatColor(text: string, heat: number): string {\n  const color = getHeatColor(heat);\n  if (!color) return text;\n  return `${color}${text}${RESET}`;\n}\n\n/**\n * Safely stringify a value, handling circular references and BigInt.\n * Returns Result with either the stringified value or an error.\n */\nfunction safeStringify(value: unknown): Result<string, StringifyError> {\n  try {\n    const replacer = (_key: string, v: unknown): unknown => {\n      if (typeof v !== \"bigint\") return v;\n      const n = Number(v);\n      return Number.isSafeInteger(n) ? n : v.toString();\n    };\n    return ok(JSON.stringify(value, replacer));\n  } catch {\n    return err(\"STRINGIFY_ERROR\");\n  }\n}\n\n/**\n * Get stringified value or fallback for unserializable values.\n */\nfunction getStringified(value: unknown): string {\n  const result = safeStringify(value);\n  return result.ok ? result.value : \"[unserializable]\";\n}\n\n// =============================================================================\n// Sparkline Characters\n// =============================================================================\n\nconst SPARK_CHARS = \"▁▂▃▄▅▆▇█\";\n\n/**\n * Render a sparkline from an array of values.\n *\n * @param values Array of numeric values\n * @param width Maximum characters to use (default: 10)\n * @returns Sparkline string\n */\nexport function renderSparkline(values: number[], width = 10): string {\n  if (values.length === 0) return \"\";\n\n  // Take last N values\n  const subset = values.slice(-width);\n  const min = Math.min(...subset);\n  const max = Math.max(...subset);\n  const range = max - min || 1;\n\n  return subset\n    .map((v) => {\n      const normalized = (v - min) / range;\n      const index = Math.floor(normalized * (SPARK_CHARS.length - 1));\n      return SPARK_CHARS[index];\n    })\n    .join(\"\");\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Pad a string to a fixed width, accounting for ANSI codes.\n */\nfunction padEnd(str: string, width: number): string {\n  const visibleLen = stripAnsi(str).length;\n  const padding = Math.max(0, width - visibleLen);\n  return str + \" \".repeat(padding);\n}\n\n/**\n * Create a horizontal line with optional title.\n */\nfunction horizontalLine(width: number, title?: string): string {\n  if (!title) {\n    return BOX.horizontal.repeat(width);\n  }\n\n  const titleText = ` ${title} `;\n  // Use visible length (strip ANSI codes) for width calculation\n  const visibleTitleLen = stripAnsi(titleText).length;\n  const remainingWidth = width - visibleTitleLen;\n  if (remainingWidth < 4) {\n    return BOX.horizontal.repeat(width);\n  }\n\n  const leftPad = 2;\n  const rightPad = remainingWidth - leftPad;\n\n  return (\n    BOX.horizontal.repeat(leftPad) + titleText + BOX.horizontal.repeat(rightPad)\n  );\n}\n\n// =============================================================================\n// Hook Rendering\n// =============================================================================\n\n/**\n * Render a single hook execution.\n */\nfunction renderHookExecution(\n  hook: HookExecution,\n  label: string,\n  colors: ReturnType<typeof Object.assign>\n): string {\n  const symbol = hook.state === \"success\"\n    ? colorize(\"⚙\", colors.success)\n    : colorize(\"⚠\", colors.error);\n\n  const timing = hook.durationMs !== undefined\n    ? dim(` [${formatDuration(hook.durationMs)}]`)\n    : \"\";\n\n  let context = \"\";\n  if (hook.type === \"shouldRun\" && hook.context?.skipped) {\n    context = dim(\" → workflow skipped\");\n  } else if (hook.type === \"shouldRun\" && hook.context?.result === true) {\n    context = dim(\" → proceed\");\n  } else if (hook.type === \"onBeforeStart\" && hook.context?.skipped) {\n    context = dim(\" → workflow skipped\");\n  } else if (hook.type === \"onAfterStep\" && hook.context?.stepKey) {\n    context = dim(` (${hook.context.stepKey})`);\n  }\n\n  const error = hook.state === \"error\" && hook.error\n    ? dim(` error: ${String(hook.error)}`)\n    : \"\";\n\n  return `${symbol} ${dim(label)}${context}${timing}${error}`;\n}\n\n/**\n * Render workflow hooks section.\n */\nfunction renderHooks(\n  hooks: WorkflowHooks,\n  colors: ReturnType<typeof Object.assign>\n): string[] {\n  const lines: string[] = [];\n\n  // Render shouldRun hook\n  if (hooks.shouldRun) {\n    lines.push(renderHookExecution(hooks.shouldRun, \"shouldRun\", colors));\n  }\n\n  // Render onBeforeStart hook\n  if (hooks.onBeforeStart) {\n    lines.push(renderHookExecution(hooks.onBeforeStart, \"onBeforeStart\", colors));\n  }\n\n  // We don't render onAfterStep hooks here - they're shown inline with steps\n  // But if there are any, add a separator\n  if (lines.length > 0) {\n    lines.push(dim(\"────────────────────\")); // Separator between hooks and steps\n  }\n\n  return lines;\n}\n\n// =============================================================================\n// ASCII Renderer\n// =============================================================================\n\n/**\n * Create the ASCII terminal renderer.\n */\nexport function asciiRenderer(): Renderer {\n  return {\n    name: \"ascii\",\n    supportsLive: true,\n\n    render(ir: WorkflowIR, options: RenderOptions): string {\n      const colors = { ...defaultColorScheme, ...options.colors };\n      // Ensure minimum width to prevent negative repeat counts\n      const width = Math.max(options.terminalWidth ?? 60, 5);\n      const innerWidth = width - 4; // Account for borders\n\n      const lines: string[] = [];\n\n      // Header\n      const workflowName = ir.root.name ?? \"workflow\";\n      const headerTitle = bold(workflowName);\n      lines.push(\n        `${BOX.topLeft}${horizontalLine(width - 2, headerTitle)}${BOX.topRight}`\n      );\n      lines.push(`${BOX.vertical}${\" \".repeat(width - 2)}${BOX.vertical}`);\n\n      // Render hooks (if any)\n      if (ir.hooks) {\n        const hookLines = renderHooks(ir.hooks, colors);\n        for (const line of hookLines) {\n          lines.push(\n            `${BOX.vertical}  ${padEnd(line, innerWidth)}${BOX.vertical}`\n          );\n        }\n      }\n\n      // Render children\n      const childLines = renderNodes(ir.root.children, options, colors, 0, ir.hooks);\n      for (const line of childLines) {\n        lines.push(\n          `${BOX.vertical}  ${padEnd(line, innerWidth)}${BOX.vertical}`\n        );\n      }\n\n      // Footer with timing\n      lines.push(`${BOX.vertical}${\" \".repeat(width - 2)}${BOX.vertical}`);\n\n      if (ir.root.durationMs !== undefined && options.showTimings) {\n        const status =\n          ir.root.state === \"success\"\n            ? \"Completed\"\n            : ir.root.state === \"aborted\"\n              ? \"Cancelled\"\n              : \"Failed\";\n        const statusColored = colorByState(status, ir.root.state, colors);\n        const footer = `${statusColored} in ${formatDuration(ir.root.durationMs)}`;\n        lines.push(\n          `${BOX.vertical}  ${padEnd(footer, innerWidth)}${BOX.vertical}`\n        );\n        lines.push(`${BOX.vertical}${\" \".repeat(width - 2)}${BOX.vertical}`);\n      }\n\n      lines.push(\n        `${BOX.bottomLeft}${BOX.horizontal.repeat(width - 2)}${BOX.bottomRight}`\n      );\n\n      return lines.join(\"\\n\");\n    },\n  };\n}\n\n/**\n * Render a list of nodes.\n */\nfunction renderNodes(\n  nodes: FlowNode[],\n  options: RenderOptions,\n  colors: ReturnType<typeof Object.assign>,\n  depth: number,\n  hooks?: WorkflowHooks\n): string[] {\n  const lines: string[] = [];\n\n  for (const node of nodes) {\n    if (isStepNode(node)) {\n      lines.push(renderStepNode(node, options, colors, hooks));\n    } else if (isParallelNode(node)) {\n      lines.push(...renderParallelNode(node, options, colors, depth, hooks));\n    } else if (isRaceNode(node)) {\n      lines.push(...renderRaceNode(node, options, colors, depth, hooks));\n    } else if (isDecisionNode(node)) {\n      lines.push(...renderDecisionNode(node, options, colors, depth, hooks));\n    } else if (isStreamNode(node)) {\n      lines.push(renderStreamNode(node, options, colors));\n    }\n  }\n\n  return lines;\n}\n\n/**\n * Render a single step node.\n */\nfunction renderStepNode(\n  node: StepNode,\n  options: RenderOptions,\n  colors: ReturnType<typeof Object.assign>,\n  hooks?: WorkflowHooks\n): string {\n  const symbol = getColoredSymbol(node.state, colors);\n  const name = node.name ?? node.key ?? \"step\";\n\n  // Check for enhanced options\n  const enhanced = options as EnhancedRenderOptions;\n  // Heatmap lookup order matches PerformanceAnalyzer: key ?? name ?? id\n  const heat = enhanced.showHeatmap && enhanced.heatmapData\n    ? enhanced.heatmapData.heat.get(node.key ?? \"\") ??\n      enhanced.heatmapData.heat.get(node.name ?? \"\") ??\n      enhanced.heatmapData.heat.get(node.id)\n    : undefined;\n\n  // Apply heat coloring or default state coloring\n  let nameColored: string;\n  if (heat !== undefined) {\n    nameColored = applyHeatColor(name, heat);\n  } else {\n    nameColored = colorByState(name, node.state, colors);\n  }\n\n  let line = `${symbol} ${nameColored}`;\n\n  // Add key if requested (only when step has a name, so key is not already the label)\n  if (options.showKeys && node.key && node.name) {\n    line += dim(` [key: ${node.key}]`);\n  }\n\n  // Add input/output if available (for decision understanding)\n  if (node.input !== undefined) {\n    const inputStr = typeof node.input === \"string\"\n      ? node.input\n      : getStringified(node.input).slice(0, 30);\n    line += dim(` [in: ${inputStr}${inputStr.length >= 30 ? \"...\" : \"\"}]`);\n  }\n  if (node.output !== undefined && node.state === \"success\") {\n    const outputStr = typeof node.output === \"string\"\n      ? node.output\n      : getStringified(node.output).slice(0, 30);\n    line += dim(` [out: ${outputStr}${outputStr.length >= 30 ? \"...\" : \"\"}]`);\n  }\n\n  // Add timing if available and requested\n  if (options.showTimings && node.durationMs !== undefined) {\n    // Apply heat coloring to timing if enabled\n    const timingStr = formatDuration(node.durationMs);\n    const timingDisplay = heat !== undefined\n      ? applyHeatColor(`[${timingStr}]`, heat)\n      : dim(`[${timingStr}]`);\n    line += ` ${timingDisplay}`;\n  }\n\n  // Add sparkline if enabled and history available (lookup order: key ?? name ?? id, like analyzer)\n  if (enhanced.showSparklines && enhanced.timingHistory) {\n    const history =\n      enhanced.timingHistory.get(node.key ?? \"\") ??\n      enhanced.timingHistory.get(node.name ?? \"\") ??\n      enhanced.timingHistory.get(node.id);\n    if (history && history.length > 1) {\n      line += ` ${dim(renderSparkline(history))}`;\n    }\n  }\n\n  // Add retry indicator if retries occurred\n  if (node.retryCount !== undefined && node.retryCount > 0) {\n    line += dim(` [${node.retryCount} ${node.retryCount === 1 ? \"retry\" : \"retries\"}]`);\n  }\n\n  // Add timeout indicator if step timed out\n  if (node.timedOut) {\n    const timeoutInfo = node.timeoutMs !== undefined ? ` ${node.timeoutMs}ms` : \"\";\n    line += dim(` [timeout${timeoutInfo}]`);\n  }\n\n  // Add onAfterStep hook indicator if present (check by key first, then by id)\n  const hookKey = node.key ?? node.id;\n  if (hooks && hookKey && hooks.onAfterStep.has(hookKey)) {\n    const hookExec = hooks.onAfterStep.get(hookKey)!;\n    const hookSymbol = hookExec.state === \"success\"\n      ? colorize(\"⚙\", colors.success)\n      : colorize(\"⚠\", colors.error);\n    const hookTiming = hookExec.durationMs !== undefined\n      ? dim(` ${formatDuration(hookExec.durationMs)}`)\n      : \"\";\n    line += ` ${hookSymbol}${hookTiming}`;\n  }\n\n  return line;\n}\n\n/**\n * Render a stream node.\n */\nfunction renderStreamNode(\n  node: StreamNode,\n  options: RenderOptions,\n  colors: ReturnType<typeof Object.assign>\n): string {\n  // Use stream-specific symbol\n  const stateSymbol = node.streamState === \"active\"\n    ? colorize(\"⟳\", colors.running)\n    : node.streamState === \"closed\"\n      ? colorize(\"✓\", colors.success)\n      : colorize(\"✗\", colors.error);\n\n  const name = `stream:${node.namespace}`;\n  const nameColored = colorByState(name, node.state, colors);\n\n  // Show write/read counts\n  const counts = dim(`[W:${node.writeCount} R:${node.readCount}]`);\n\n  let line = `${stateSymbol} ${nameColored} ${counts}`;\n\n  // Add timing if available and requested\n  if (options.showTimings && node.durationMs !== undefined) {\n    line += ` ${dim(`[${formatDuration(node.durationMs)}]`)}`;\n  }\n\n  // Add backpressure indicator if occurred\n  if (node.backpressureOccurred) {\n    line += dim(\" [backpressure]\");\n  }\n\n  // Add final position\n  if (node.streamState === \"closed\") {\n    line += dim(` pos:${node.finalPosition}`);\n  }\n\n  return line;\n}\n\n/**\n * Render a parallel node (allAsync).\n */\nfunction renderParallelNode(\n  node: ParallelNode,\n  options: RenderOptions,\n  colors: ReturnType<typeof Object.assign>,\n  depth: number,\n  hooks?: WorkflowHooks\n): string[] {\n  const lines: string[] = [];\n  const indent = \"  \".repeat(depth);\n\n  // Header\n  const symbol = getColoredSymbol(node.state, colors);\n  const name = node.name ?? \"parallel\";\n  const mode = node.mode === \"allSettled\" ? \" (allSettled)\" : \"\";\n  lines.push(`${indent}${BOX.teeRight}${BOX.teeDown}${BOX.horizontal} ${symbol} ${bold(name)}${mode}`);\n\n  // Children\n  if (node.children.length === 0) {\n    // Empty parallel scope - operations inside allAsync/anyAsync weren't tracked as steps\n    lines.push(`${indent}${BOX.vertical} ${dim(\"(operations not individually tracked)\")}`);\n    lines.push(`${indent}${BOX.vertical} ${dim(\"(wrap each operation with step() to see individual steps)\")}`);\n  } else {\n    for (let i = 0; i < node.children.length; i++) {\n      const child = node.children[i];\n      const isLast = i === node.children.length - 1;\n      const prefix = isLast ? `${indent}${BOX.vertical} ${BOX.bottomLeft}` : `${indent}${BOX.vertical} ${BOX.teeRight}`;\n\n      if (isStepNode(child)) {\n        lines.push(`${prefix} ${renderStepNode(child, options, colors, hooks)}`);\n      } else {\n        // Nested structure - recurse\n        const nestedLines = renderNodes([child], options, colors, depth + 1, hooks);\n        for (const line of nestedLines) {\n          lines.push(`${indent}${BOX.vertical}   ${line}`);\n        }\n      }\n    }\n  }\n\n  // Timing footer\n  if (options.showTimings && node.durationMs !== undefined) {\n    lines.push(`${indent}${BOX.bottomLeft}${BOX.horizontal}${BOX.horizontal} ${dim(`[${formatDuration(node.durationMs)}]`)}`);\n  }\n\n  return lines;\n}\n\n/**\n * Render a race node (anyAsync).\n */\nfunction renderRaceNode(\n  node: RaceNode,\n  options: RenderOptions,\n  colors: ReturnType<typeof Object.assign>,\n  depth: number,\n  hooks?: WorkflowHooks\n): string[] {\n  const lines: string[] = [];\n  const indent = \"  \".repeat(depth);\n\n  // Header with lightning bolt for race\n  const symbol = getColoredSymbol(node.state, colors);\n  const name = node.name ?? \"race\";\n  lines.push(`${indent}${BOX.teeRight}⚡ ${symbol} ${bold(name)}`);\n\n  // Children\n  if (node.children.length === 0) {\n    // Empty race scope - operations inside anyAsync weren't tracked as steps\n    lines.push(`${indent}${BOX.vertical} ${dim(\"(operations not individually tracked)\")}`);\n    lines.push(`${indent}${BOX.vertical} ${dim(\"(wrap each operation with step() to see individual steps)\")}`);\n  } else {\n    for (let i = 0; i < node.children.length; i++) {\n      const child = node.children[i];\n      const isLast = i === node.children.length - 1;\n      const prefix = isLast ? `${indent}${BOX.vertical} ${BOX.bottomLeft}` : `${indent}${BOX.vertical} ${BOX.teeRight}`;\n\n      // Mark winner\n      const isWinner = node.winnerId && child.id === node.winnerId;\n      const winnerSuffix = isWinner ? dim(\" (winner)\") : \"\";\n\n      if (isStepNode(child)) {\n        lines.push(`${prefix} ${renderStepNode(child, options, colors, hooks)}${winnerSuffix}`);\n      } else {\n        const nestedLines = renderNodes([child], options, colors, depth + 1, hooks);\n        for (const line of nestedLines) {\n          lines.push(`${indent}${BOX.vertical}   ${line}`);\n        }\n      }\n    }\n  }\n\n  // Timing footer\n  if (options.showTimings && node.durationMs !== undefined) {\n    lines.push(`${indent}${BOX.bottomLeft}${BOX.horizontal}${BOX.horizontal} ${dim(`[${formatDuration(node.durationMs)}]`)}`);\n  }\n\n  return lines;\n}\n\n/**\n * Render a decision node (conditional branch).\n */\nfunction renderDecisionNode(\n  node: DecisionNode,\n  options: RenderOptions,\n  colors: ReturnType<typeof Object.assign>,\n  depth: number,\n  hooks?: WorkflowHooks\n): string[] {\n  const lines: string[] = [];\n  const indent = \"  \".repeat(depth);\n\n  // Header with decision info\n  const symbol = getColoredSymbol(node.state, colors);\n  const name = node.name ?? \"decision\";\n  const condition = node.condition\n    ? dim(` (${node.condition})`)\n    : \"\";\n  const decisionValue = node.decisionValue !== undefined\n    ? dim(` = ${String(node.decisionValue)}`)\n    : \"\";\n  const branchTaken = node.branchTaken !== undefined\n    ? dim(` → ${String(node.branchTaken)}`)\n    : \"\";\n\n  lines.push(\n    `${indent}${BOX.teeRight}${BOX.teeDown}${BOX.horizontal} ${symbol} ${bold(name)}${condition}${decisionValue}${branchTaken}`\n  );\n\n  // Render branches\n  for (let i = 0; i < node.branches.length; i++) {\n    const branch = node.branches[i];\n    const isLast = i === node.branches.length - 1;\n    const prefix = isLast\n      ? `${indent}${BOX.vertical} ${BOX.bottomLeft}`\n      : `${indent}${BOX.vertical} ${BOX.teeRight}`;\n\n    // Branch label with taken/skipped indicator\n    const branchSymbol = branch.taken ? \"✓\" : \"⊘\";\n    const branchColor = branch.taken ? colors.success : colors.skipped;\n    const branchLabel = colorize(\n      `${branchSymbol} ${branch.label}`,\n      branchColor\n    );\n    const branchCondition = branch.condition\n      ? dim(` (${branch.condition})`)\n      : \"\";\n\n    lines.push(`${prefix} ${branchLabel}${branchCondition}`);\n\n    // Render children of this branch\n    if (branch.children.length > 0) {\n      const childLines = renderNodes(branch.children, options, colors, depth + 1, hooks);\n      for (const line of childLines) {\n        lines.push(`${indent}${BOX.vertical}   ${line}`);\n      }\n    } else if (!branch.taken) {\n      // Show that this branch was skipped\n      lines.push(\n        `${indent}${BOX.vertical}   ${dim(\"(skipped)\")}`\n      );\n    }\n  }\n\n  // Timing footer\n  if (options.showTimings && node.durationMs !== undefined) {\n    lines.push(\n      `${indent}${BOX.bottomLeft}${BOX.horizontal}${BOX.horizontal} ${dim(`[${formatDuration(node.durationMs)}]`)}`\n    );\n  }\n\n  return lines;\n}\n\nexport { defaultColorScheme };\n","/**\n * Workflow Visualization - Intermediate Representation Types\n *\n * The IR (Intermediate Representation) is a DSL that represents workflow\n * execution structure. Events are converted to IR, which can then be\n * rendered to various output formats (ASCII, Mermaid, JSON, etc.).\n */\n\n// =============================================================================\n// Step States\n// =============================================================================\n\n/**\n * Execution state of a step with semantic meaning for visualization.\n *\n * Color mapping:\n * - pending  → white/clear (not started)\n * - running  → yellow (currently executing)\n * - success  → green (completed successfully)\n * - error    → red (failed with error)\n * - aborted  → gray (cancelled, e.g., in race)\n * - cached   → blue (served from cache)\n * - skipped  → dim gray (not executed due to conditional logic)\n */\nexport type StepState =\n  | \"pending\"\n  | \"running\"\n  | \"success\"\n  | \"error\"\n  | \"aborted\"\n  | \"cached\"\n  | \"skipped\";\n\n// =============================================================================\n// Node Types\n// =============================================================================\n\n/**\n * Base properties shared by all IR nodes.\n */\nexport interface BaseNode {\n  /** Unique identifier for this node */\n  id: string;\n  /** Human-readable name (from step options or inferred) */\n  name?: string;\n  /** Cache key if this is a keyed step */\n  key?: string;\n  /** Current execution state */\n  state: StepState;\n  /** Timestamp when execution started */\n  startTs?: number;\n  /** Timestamp when execution ended */\n  endTs?: number;\n  /** Duration in milliseconds */\n  durationMs?: number;\n  /** Error value if state is 'error' */\n  error?: unknown;\n  /** Input value that triggered this step (for decision understanding) */\n  input?: unknown;\n  /** Output value from this step (for decision understanding) */\n  output?: unknown;\n  /** Number of retry attempts made (0 = no retries, 1 = one retry, etc.) */\n  retryCount?: number;\n  /** Whether this step experienced a timeout (may have retried after) */\n  timedOut?: boolean;\n  /** Timeout duration in ms (if timed out) */\n  timeoutMs?: number;\n  /** Agent metadata from step options (domain, intent, owner, etc.) */\n  metadata?: import(\"awaitly\").StepMetadata;\n  /** Error diagnostics (tag, classification, origin) */\n  errorDiagnostics?: import(\"awaitly\").StepErrorDiagnostics;\n}\n\n/**\n * A single step execution node.\n */\nexport interface StepNode extends BaseNode {\n  type: \"step\";\n}\n\n/**\n * Sequential execution - steps run one after another.\n * This is the implicit structure when steps are awaited in sequence.\n */\nexport interface SequenceNode extends BaseNode {\n  type: \"sequence\";\n  children: FlowNode[];\n}\n\n/**\n * Parallel execution - all branches run simultaneously.\n * Created by allAsync() or allSettledAsync().\n */\nexport interface ParallelNode extends BaseNode {\n  type: \"parallel\";\n  children: FlowNode[];\n  /**\n   * Execution mode:\n   * - 'all': Fails on first error (allAsync)\n   * - 'allSettled': Collects all results (allSettledAsync)\n   */\n  mode: \"all\" | \"allSettled\";\n}\n\n/**\n * Race execution - first to complete wins.\n * Created by anyAsync().\n */\nexport interface RaceNode extends BaseNode {\n  type: \"race\";\n  children: FlowNode[];\n  /** ID of the winning branch (first to succeed) */\n  winnerId?: string;\n}\n\n/**\n * Stream operation node.\n * Tracks streaming events (write, read, close, error, backpressure).\n */\nexport interface StreamNode extends BaseNode {\n  type: \"stream\";\n  /** Stream namespace identifier */\n  namespace: string;\n  /** Total number of write operations */\n  writeCount: number;\n  /** Total number of read operations */\n  readCount: number;\n  /** Final position when stream closed */\n  finalPosition: number;\n  /** Current stream state */\n  streamState: \"active\" | \"closed\" | \"error\";\n  /** Whether backpressure was encountered during streaming */\n  backpressureOccurred: boolean;\n}\n\n/**\n * Decision point - conditional branch (if/switch).\n * Shows which branch was taken and why.\n */\nexport interface DecisionNode extends BaseNode {\n  type: \"decision\";\n  /** Condition that was evaluated (e.g., \"user.role === 'admin'\") */\n  condition?: string;\n  /** Value that was evaluated (the input to the decision) */\n  decisionValue?: unknown;\n  /** Which branch was taken (true/false, or the matched case) */\n  branchTaken?: string | boolean;\n  /** All possible branches (including skipped ones) */\n  branches: DecisionBranch[];\n}\n\n/**\n * A branch in a decision node.\n */\nexport interface DecisionBranch {\n  /** Label for this branch (e.g., \"if\", \"else\", \"case 'admin'\") */\n  label: string;\n  /** Condition that would trigger this branch */\n  condition?: string;\n  /** Whether this branch was taken */\n  taken: boolean;\n  /** Steps in this branch */\n  children: FlowNode[];\n}\n\n/**\n * Union of all flow node types.\n */\nexport type FlowNode = StepNode | SequenceNode | ParallelNode | RaceNode | DecisionNode | StreamNode;\n\n/**\n * Root node representing the entire workflow.\n */\nexport interface WorkflowNode extends BaseNode {\n  type: \"workflow\";\n  /** Correlation ID from the workflow execution */\n  workflowId: string;\n  /** Child nodes (steps, parallel blocks, etc.) */\n  children: FlowNode[];\n}\n\n// =============================================================================\n// Workflow IR\n// =============================================================================\n\n/**\n * Complete workflow intermediate representation.\n * This is the main data structure produced by the IR builder.\n */\nexport interface WorkflowIR {\n  /** Root workflow node */\n  root: WorkflowNode;\n  /** Metadata about the IR */\n  metadata: {\n    /** When the IR was first created */\n    createdAt: number;\n    /** When the IR was last updated */\n    lastUpdatedAt: number;\n  };\n  /** Hook executions (if any hooks are configured) */\n  hooks?: WorkflowHooks;\n}\n\n// =============================================================================\n// Scope Events (for parallel/race detection)\n// =============================================================================\n\n// Re-export ScopeType from awaitly for consistency\nexport type { ScopeType } from \"awaitly\";\nimport type { ScopeType, WorkflowEvent, WorkflowOptions } from \"awaitly\";\nimport type { UnexpectedError } from \"awaitly\";\n\n/**\n * Event emitted when entering a parallel/race scope.\n * This matches the scope_start event in WorkflowEvent.\n */\nexport interface ScopeStartEvent {\n  type: \"scope_start\";\n  workflowId: string;\n  scopeId: string;\n  scopeType: ScopeType;\n  name?: string;\n  ts: number;\n}\n\n/**\n * Event emitted when exiting a parallel/race scope.\n */\nexport interface ScopeEndEvent {\n  type: \"scope_end\";\n  workflowId: string;\n  scopeId: string;\n  ts: number;\n  durationMs: number;\n  /** For race scopes, the ID of the winning branch */\n  winnerId?: string;\n}\n\n/**\n * Event emitted when a decision point is encountered.\n * Use this to track conditional logic (if/switch).\n */\nexport interface DecisionStartEvent {\n  type: \"decision_start\";\n  workflowId: string;\n  decisionId: string;\n  /** Condition being evaluated (e.g., \"user.role === 'admin'\") */\n  condition?: string;\n  /** Value being evaluated */\n  decisionValue?: unknown;\n  /** Name/label for this decision point */\n  name?: string;\n  ts: number;\n}\n\n/**\n * Event emitted when a decision branch is taken.\n */\nexport interface DecisionBranchEvent {\n  type: \"decision_branch\";\n  workflowId: string;\n  decisionId: string;\n  /** Label for this branch (e.g., \"if\", \"else\", \"case 'admin'\") */\n  branchLabel: string;\n  /** Condition for this branch */\n  condition?: string;\n  /** Whether this branch was taken */\n  taken: boolean;\n  ts: number;\n}\n\n/**\n * Event emitted when a decision point completes.\n */\nexport interface DecisionEndEvent {\n  type: \"decision_end\";\n  workflowId: string;\n  decisionId: string;\n  /** Which branch was taken */\n  branchTaken?: string | boolean;\n  ts: number;\n  durationMs: number;\n}\n\n/**\n * Event emitted when a step is skipped due to conditional logic.\n */\nexport interface StepSkippedEvent {\n  type: \"step_skipped\";\n  workflowId: string;\n  stepKey?: string;\n  name?: string;\n  /** Reason why this step was skipped (e.g., \"condition was false\") */\n  reason?: string;\n  /** The decision that caused this skip */\n  decisionId?: string;\n  ts: number;\n}\n\n/**\n * Union of scope-related events.\n */\nexport type ScopeEvent = ScopeStartEvent | ScopeEndEvent;\n\n/**\n * Union of decision-related events.\n */\nexport type DecisionEvent = DecisionStartEvent | DecisionBranchEvent | DecisionEndEvent;\n\n// =============================================================================\n// Renderer Types\n// =============================================================================\n\n/**\n * Color scheme for rendering step states.\n */\nexport interface ColorScheme {\n  pending: string;\n  running: string;\n  success: string;\n  error: string;\n  aborted: string;\n  cached: string;\n  skipped: string;\n}\n\n/**\n * Options passed to renderers.\n */\nexport interface RenderOptions {\n  /** Show timing information (duration) */\n  showTimings: boolean;\n  /** Show step cache keys */\n  showKeys: boolean;\n  /** Terminal width for ASCII renderer */\n  terminalWidth?: number;\n  /** Color scheme */\n  colors: ColorScheme;\n}\n\n/**\n * Extended options for Mermaid renderer.\n * Controls how edges are displayed for retries, errors, and timeouts.\n */\nexport interface MermaidRenderOptions extends RenderOptions {\n  /** Show retry as self-loop edge (default: true) */\n  showRetryEdges?: boolean;\n  /** Show error flow to error node (default: true) */\n  showErrorEdges?: boolean;\n  /** Show timeout as alternative path (default: true) */\n  showTimeoutEdges?: boolean;\n}\n\n/**\n * Renderer interface - transforms IR to output format.\n */\nexport interface Renderer {\n  /** Unique identifier for this renderer */\n  readonly name: string;\n  /** Render IR to string output */\n  render(ir: WorkflowIR, options: RenderOptions): string;\n  /** Whether this renderer supports live (incremental) updates */\n  supportsLive?: boolean;\n  /** Render incremental update (optional) */\n  renderUpdate?(\n    ir: WorkflowIR,\n    changedNodes: FlowNode[],\n    options: RenderOptions\n  ): string;\n}\n\n// =============================================================================\n// Visualizer Types\n// =============================================================================\n\n/**\n * Output format for rendering.\n */\nexport type OutputFormat = \"ascii\" | \"mermaid\" | \"json\" | \"logger\" | \"flowchart\";\n\n/**\n * Options for creating a visualizer.\n */\nexport interface VisualizerOptions {\n  /** Name for the workflow in visualizations */\n  workflowName?: string;\n  /** Enable parallel detection heuristics (default: true) */\n  detectParallel?: boolean;\n  /** Show timing information (default: true) */\n  showTimings?: boolean;\n  /** Show step keys (default: false) */\n  showKeys?: boolean;\n  /** Custom color scheme */\n  colors?: Partial<ColorScheme>;\n  /**\n   * Export configuration for URL generation methods.\n   * Note: Treated as immutable after creation - do not mutate.\n   */\n  export?: {\n    /** Default export provider (opt-in). If not set, export methods require explicit provider. */\n    default?: ExportOptions;\n  };\n}\n\n/**\n * Options for createVisualizingWorkflow convenience factory.\n * Combines WorkflowOptions with VisualizerOptions.\n *\n * @example\n * ```typescript\n * const { workflow, visualizer } = createVisualizingWorkflow(deps, {\n *   workflowName: 'checkout',\n *   showTimings: true,\n *   forwardTo: (event) => console.log(event.type),\n * });\n * ```\n */\nexport interface VisualizingWorkflowOptions<E, C = void>\n  extends Omit<WorkflowOptions<E, C>, \"onEvent\">,\n    VisualizerOptions {\n  /** Forward events to additional handler (runs after visualization) */\n  forwardTo?: (event: WorkflowEvent<E | UnexpectedError, C>, ctx: C) => void;\n}\n\n/**\n * Options for live visualization.\n */\nexport interface LiveVisualizerOptions extends VisualizerOptions {\n  /** Output stream (default: process.stdout) */\n  stream?: NodeJS.WriteStream;\n  /** Update interval in ms (default: 100) */\n  updateInterval?: number;\n}\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Check if a node is a StepNode.\n */\nexport function isStepNode(node: FlowNode): node is StepNode {\n  return node.type === \"step\";\n}\n\n/**\n * Check if a node is a SequenceNode.\n */\nexport function isSequenceNode(node: FlowNode): node is SequenceNode {\n  return node.type === \"sequence\";\n}\n\n/**\n * Check if a node is a ParallelNode.\n */\nexport function isParallelNode(node: FlowNode): node is ParallelNode {\n  return node.type === \"parallel\";\n}\n\n/**\n * Check if a node is a RaceNode.\n */\nexport function isRaceNode(node: FlowNode): node is RaceNode {\n  return node.type === \"race\";\n}\n\n/**\n * Check if a node is a DecisionNode.\n */\nexport function isDecisionNode(node: FlowNode): node is DecisionNode {\n  return node.type === \"decision\";\n}\n\n/**\n * Check if a node is a StreamNode.\n */\nexport function isStreamNode(node: FlowNode): node is StreamNode {\n  return node.type === \"stream\";\n}\n\n/**\n * Check if a node has children.\n */\nexport function hasChildren(\n  node: FlowNode\n): node is SequenceNode | ParallelNode | RaceNode | DecisionNode {\n  return \"children\" in node || (node.type === \"decision\" && \"branches\" in node);\n}\n\n// =============================================================================\n// Time Travel Types\n// =============================================================================\n\n/**\n * Snapshot of an active step's state at a point in time.\n */\nexport interface ActiveStepSnapshot {\n  id: string;\n  name?: string;\n  key?: string;\n  startTs: number;\n  retryCount: number;\n  timedOut: boolean;\n  timeoutMs?: number;\n}\n\n/**\n * A snapshot of the complete IR state at a specific point in time.\n * Used for time-travel debugging - each event creates a snapshot.\n */\nexport interface IRSnapshot {\n  /** Unique identifier for this snapshot */\n  id: string;\n  /** Index in the event sequence (0-based) */\n  eventIndex: number;\n  /** The event that triggered this snapshot */\n  event: unknown; // WorkflowEvent - avoid circular import\n  /** Complete IR state at this moment */\n  ir: WorkflowIR;\n  /** Timestamp when snapshot was taken */\n  timestamp: number;\n  /** Active step states at this moment (for debugging) */\n  activeSteps: Map<string, ActiveStepSnapshot>;\n}\n\n/**\n * State of the time-travel controller.\n */\nexport interface TimeTravelState {\n  /** All recorded snapshots */\n  snapshots: IRSnapshot[];\n  /** Current snapshot index (for playback position) */\n  currentIndex: number;\n  /** Whether playback is active */\n  isPlaying: boolean;\n  /** Playback speed multiplier (1.0 = realtime, 2.0 = 2x speed) */\n  playbackSpeed: number;\n  /** Whether recording is active */\n  isRecording: boolean;\n}\n\n// =============================================================================\n// Performance Analysis Types\n// =============================================================================\n\n/**\n * Performance metrics for a single node across multiple runs.\n */\nexport interface NodePerformance {\n  /** Node identifier (name or step ID) */\n  nodeId: string;\n  /** Average duration across all samples */\n  avgDurationMs: number;\n  /** Minimum duration observed */\n  minDurationMs: number;\n  /** Maximum duration observed */\n  maxDurationMs: number;\n  /** Standard deviation of durations */\n  stdDevMs: number;\n  /** Number of timing samples collected */\n  samples: number;\n  /** Retry frequency (0-1, where 1 = always retries) */\n  retryRate: number;\n  /** Timeout frequency (0-1) */\n  timeoutRate: number;\n  /** Error rate (0-1) */\n  errorRate: number;\n  /** Percentile data for distribution analysis */\n  percentiles: {\n    p50: number;\n    p90: number;\n    p95: number;\n    p99: number;\n  };\n}\n\n/**\n * Heatmap data for visualizing performance across nodes.\n */\nexport interface HeatmapData {\n  /** Map of node ID to heat level (0-1, where 1 is hottest/slowest) */\n  heat: Map<string, number>;\n  /** The metric used for heat calculation */\n  metric: \"duration\" | \"retryRate\" | \"errorRate\";\n  /** Statistics used to compute heat values */\n  stats: {\n    /** Minimum value in the dataset */\n    min: number;\n    /** Maximum value in the dataset */\n    max: number;\n    /** Mean value */\n    mean: number;\n    /** Threshold above which a node is considered \"hot\" */\n    threshold: number;\n  };\n}\n\n/**\n * Heat level for visual styling.\n */\nexport type HeatLevel = \"cold\" | \"cool\" | \"neutral\" | \"warm\" | \"hot\" | \"critical\";\n\n// =============================================================================\n// HTML Renderer Types\n// =============================================================================\n\n/**\n * Theme for the HTML visualizer.\n */\nexport type HTMLTheme = \"light\" | \"dark\" | \"auto\";\n\n/**\n * Layout direction for the workflow diagram.\n */\nexport type LayoutDirection = \"TB\" | \"LR\" | \"BT\" | \"RL\";\n\n/**\n * Options for the HTML renderer.\n */\nexport interface HTMLRenderOptions extends RenderOptions {\n  /** Enable interactive features (click to inspect, zoom/pan) */\n  interactive: boolean;\n  /** Include time-travel controls */\n  timeTravel: boolean;\n  /** Include performance heatmap overlay */\n  heatmap: boolean;\n  /** Animation duration for transitions (ms) */\n  animationDuration: number;\n  /** Color theme */\n  theme: HTMLTheme;\n  /** Diagram layout direction */\n  layout: LayoutDirection;\n  /** Heatmap data (if heatmap is enabled) */\n  heatmapData?: HeatmapData;\n  /** WebSocket URL for live updates (if streaming) */\n  wsUrl?: string;\n}\n\n/**\n * Message sent from the web visualizer to the dev server.\n */\nexport interface WebVisualizerMessage {\n  type:\n    | \"time_travel_seek\"\n    | \"time_travel_play\"\n    | \"time_travel_pause\"\n    | \"time_travel_step_forward\"\n    | \"time_travel_step_backward\"\n    | \"request_snapshots\"\n    | \"toggle_heatmap\"\n    | \"set_heatmap_metric\";\n  payload?: unknown;\n}\n\n/**\n * Message sent from the dev server to the web visualizer.\n */\nexport interface ServerMessage {\n  type:\n    | \"ir_update\"\n    | \"snapshot\"\n    | \"snapshots_list\"\n    | \"performance_data\"\n    | \"workflow_complete\"\n    | \"time_travel_state\";\n  payload: unknown;\n}\n\n// =============================================================================\n// Enhanced ASCII Renderer Types\n// =============================================================================\n\n/**\n * Extended render options for the enhanced ASCII renderer.\n */\nexport interface EnhancedRenderOptions extends RenderOptions {\n  /** Show performance heatmap coloring */\n  showHeatmap?: boolean;\n  /** Heatmap data for coloring nodes */\n  heatmapData?: HeatmapData;\n  /** Show timing sparklines (requires historical data) */\n  showSparklines?: boolean;\n  /** Historical timing data for sparklines: nodeId → array of durations */\n  timingHistory?: Map<string, number[]>;\n}\n\n/**\n * Options for the flowchart ASCII renderer.\n * Renders workflow as a proper flowchart with boxes and arrows.\n */\nexport interface FlowchartRenderOptions extends EnhancedRenderOptions {\n  /** Show start and end nodes (default: true) */\n  showStartEnd?: boolean;\n  /** Reduce vertical spacing between nodes (default: false) */\n  compact?: boolean;\n  /** Box border style (default: 'single') */\n  boxStyle?: \"single\" | \"double\" | \"rounded\";\n}\n\n// =============================================================================\n// Hook Execution Types\n// =============================================================================\n\n/**\n * State of a hook execution.\n */\nexport type HookState = \"pending\" | \"running\" | \"success\" | \"error\";\n\n/**\n * Execution record for a workflow hook.\n */\nexport interface HookExecution {\n  /** Hook type identifier */\n  type: \"shouldRun\" | \"onBeforeStart\" | \"onAfterStep\";\n  /** Execution state */\n  state: HookState;\n  /** Timestamp when hook started */\n  ts: number;\n  /** Duration in milliseconds */\n  durationMs?: number;\n  /** Error if hook failed */\n  error?: unknown;\n  /** Additional context (e.g., stepKey for onAfterStep) */\n  context?: {\n    /** Step key for onAfterStep hooks */\n    stepKey?: string;\n    /** Result of shouldRun hook */\n    result?: boolean;\n    /** Whether workflow was skipped due to shouldRun returning false */\n    skipped?: boolean;\n  };\n}\n\n/**\n * Hook execution summary for the workflow.\n */\nexport interface WorkflowHooks {\n  /** shouldRun hook execution (if configured) */\n  shouldRun?: HookExecution;\n  /** onBeforeStart hook execution (if configured) */\n  onBeforeStart?: HookExecution;\n  /** onAfterStep hook executions (keyed by stepKey) */\n  onAfterStep: Map<string, HookExecution>;\n}\n\n// =============================================================================\n// Export Types\n// =============================================================================\n\n/**\n * Export format for diagram URLs.\n */\nexport type ExportFormat = \"svg\" | \"png\" | \"pdf\";\n\n/**\n * Diagram source - future-proof union for multiple diagram types.\n * Uses \"kind\" internally, maps to \"diagramType\" for Kroki API.\n */\nexport type DiagramSource =\n  | { kind: \"mermaid\"; source: string }\n  | { kind: \"graphviz\"; source: string }\n  | { kind: \"plantuml\"; source: string };\n\n/**\n * Kroki-specific export options.\n * Note: No background/scale - Kroki doesn't support them for mermaid diagrams.\n */\nexport interface KrokiExportOptions {\n  /** Provider identifier */\n  provider: \"kroki\";\n  /** Base URL for self-hosted Kroki (default: https://kroki.io) */\n  baseUrl?: string;\n}\n\n/**\n * Mermaid.ink-specific export options.\n * Supports additional styling options like background, scale, and theme.\n */\nexport interface MermaidInkExportOptions {\n  /** Provider identifier */\n  provider: \"mermaid-ink\";\n  /** Mermaid theme */\n  mermaidTheme?: \"default\" | \"dark\" | \"forest\" | \"neutral\";\n  /** Background color: \"transparent\" or hex color (e.g., \"1b1b1f\") */\n  background?: \"transparent\" | string;\n  /** Image scale (1-3) */\n  scale?: number;\n  /** Fit PDF to diagram size */\n  fit?: boolean;\n  /** Image width in pixels */\n  width?: number;\n  /** Image height in pixels */\n  height?: number;\n  /** Paper size for PDF */\n  paper?: \"a4\" | \"letter\";\n}\n\n/**\n * Discriminated union of export options.\n * Provider is the discriminant - no implicit defaults.\n */\nexport type ExportOptions = KrokiExportOptions | MermaidInkExportOptions;\n","/**\n * Timing utilities for workflow visualization.\n */\n\n/**\n * Format duration in milliseconds to a human-readable string.\n *\n * @example\n * formatDuration(23) // \"23ms\"\n * formatDuration(1500) // \"1.5s\"\n * formatDuration(65000) // \"1m 5s\"\n */\nexport function formatDuration(ms: number): string {\n  if (ms < 1000) {\n    return `${Math.round(ms)}ms`;\n  }\n\n  if (ms < 60000) {\n    const seconds = ms / 1000;\n    // Show one decimal for seconds\n    return `${seconds.toFixed(1).replace(/\\.0$/, \"\")}s`;\n  }\n\n  let minutes = Math.floor(ms / 60000);\n  let seconds = Math.round((ms % 60000) / 1000);\n  if (seconds >= 60) {\n    minutes += 1;\n    seconds = 0;\n  }\n\n  if (seconds === 0) {\n    return `${minutes}m`;\n  }\n\n  return `${minutes}m ${seconds}s`;\n}\n\n/**\n * Generate a unique ID for nodes.\n */\nexport function generateId(): string {\n  return `node_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n}\n","/**\n * ANSI color utilities for terminal output.\n */\n\nimport type { ColorScheme, StepState } from \"../types\";\n\n// =============================================================================\n// ANSI Escape Codes\n// =============================================================================\n\nconst RESET = \"\\x1b[0m\";\nconst BOLD = \"\\x1b[1m\";\nconst DIM = \"\\x1b[2m\";\n\n// Foreground colors\nconst FG_RED = \"\\x1b[31m\";\nconst FG_GREEN = \"\\x1b[32m\";\nconst FG_YELLOW = \"\\x1b[33m\";\nconst FG_BLUE = \"\\x1b[34m\";\nconst FG_GRAY = \"\\x1b[90m\";\nconst FG_WHITE = \"\\x1b[37m\";\n\n// =============================================================================\n// Color Functions\n// =============================================================================\n\n/**\n * Apply ANSI color to text.\n */\nexport function colorize(text: string, color: string): string {\n  if (!color) return text;\n  return `${color}${text}${RESET}`;\n}\n\n/**\n * Make text bold.\n */\nexport function bold(text: string): string {\n  return `${BOLD}${text}${RESET}`;\n}\n\n/**\n * Make text dim.\n */\nexport function dim(text: string): string {\n  return `${DIM}${text}${RESET}`;\n}\n\n// =============================================================================\n// Default Color Scheme\n// =============================================================================\n\n/**\n * Default ANSI color scheme for step states.\n */\nexport const defaultColorScheme: ColorScheme = {\n  pending: FG_WHITE,\n  running: FG_YELLOW,\n  success: FG_GREEN,\n  error: FG_RED,\n  aborted: FG_GRAY,\n  cached: FG_BLUE,\n  skipped: DIM + FG_GRAY, // Dim gray for skipped steps\n};\n\n// =============================================================================\n// State Symbols\n// =============================================================================\n\n/**\n * Get the symbol for a step state.\n */\nexport function getStateSymbol(state: StepState): string {\n  switch (state) {\n    case \"pending\":\n      return \"○\"; // Empty circle\n    case \"running\":\n      return \"⟳\"; // Rotating arrows\n    case \"success\":\n      return \"✓\"; // Check mark\n    case \"error\":\n      return \"✗\"; // X mark\n    case \"aborted\":\n      return \"⊘\"; // Circled slash\n    case \"cached\":\n      return \"↺\"; // Cached/replay\n    case \"skipped\":\n      return \"⊘\"; // Circled slash (same as aborted, but different color)\n  }\n}\n\n/**\n * Get the colored symbol for a step state.\n */\nexport function getColoredSymbol(state: StepState, colors: ColorScheme): string {\n  const symbol = getStateSymbol(state);\n  return colorize(symbol, colors[state]);\n}\n\n/**\n * Get colored text based on step state.\n */\nexport function colorByState(\n  text: string,\n  state: StepState,\n  colors: ColorScheme\n): string {\n  return colorize(text, colors[state]);\n}\n\n// =============================================================================\n// Strip ANSI\n// =============================================================================\n\n/**\n * Strip ANSI escape codes from a string.\n * Useful for calculating visible string length.\n */\nexport function stripAnsi(str: string): string {\n  // eslint-disable-next-line no-control-regex\n  return str.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n}\n\n/**\n * Get the visible length of a string (without ANSI codes).\n */\nexport function visibleLength(str: string): string {\n  return stripAnsi(str);\n}\n","/**\n * Mermaid Diagram Renderer\n *\n * Renders the workflow IR as a Mermaid flowchart diagram.\n * Supports sequential flows, parallel (subgraph), and race patterns.\n */\n\nimport { ok, err, type Result } from \"awaitly\";\nimport type {\n  FlowNode,\n  ParallelNode,\n  RaceNode,\n  DecisionNode,\n  StreamNode,\n  Renderer,\n  RenderOptions,\n  MermaidRenderOptions,\n  StepNode,\n  StepState,\n  WorkflowIR,\n  EnhancedRenderOptions,\n  HeatLevel,\n  WorkflowHooks,\n} from \"../types\";\nimport { isParallelNode, isRaceNode, isStepNode, isDecisionNode, isStreamNode } from \"../types\";\nimport { formatDuration } from \"../utils/timing\";\nimport { getHeatLevel } from \"../performance-analyzer\";\n\n/**\n * Error types for stringify operations.\n */\nexport type StringifyError = \"STRINGIFY_ERROR\";\n\n// =============================================================================\n// Mermaid Style Definitions\n// =============================================================================\n\n/**\n * Get Mermaid class definition for step states.\n * Colors inspired by AWS Step Functions and XState visualizers for professional appearance.\n */\nfunction getStyleDefinitions(): string[] {\n  return [\n    // Pending - light gray, subtle\n    \"    classDef pending fill:#f3f4f6,stroke:#9ca3af,stroke-width:2px,color:#374151\",\n    // Running - amber/yellow, indicates active execution\n    \"    classDef running fill:#fef3c7,stroke:#f59e0b,stroke-width:3px,color:#92400e\",\n    // Success - green, clear positive indicator\n    \"    classDef success fill:#d1fae5,stroke:#10b981,stroke-width:3px,color:#065f46\",\n    // Error - red, clear negative indicator\n    \"    classDef error fill:#fee2e2,stroke:#ef4444,stroke-width:3px,color:#991b1b\",\n    // Aborted - gray, indicates cancellation\n    \"    classDef aborted fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#4b5563,stroke-dasharray: 5 5\",\n    // Cached - blue, indicates cache hit\n    \"    classDef cached fill:#dbeafe,stroke:#3b82f6,stroke-width:3px,color:#1e40af\",\n    // Skipped - light gray with dashed border\n    \"    classDef skipped fill:#f9fafb,stroke:#d1d5db,stroke-width:2px,color:#6b7280,stroke-dasharray: 5 5\",\n    // Stream - purple/violet, indicates streaming operation\n    \"    classDef stream fill:#ede9fe,stroke:#8b5cf6,stroke-width:3px,color:#5b21b6\",\n    // Stream active - purple with animation indicator\n    \"    classDef streamActive fill:#ddd6fe,stroke:#7c3aed,stroke-width:3px,color:#4c1d95\",\n    // Stream error - purple-red for stream errors\n    \"    classDef streamError fill:#fce7f3,stroke:#db2777,stroke-width:3px,color:#9d174d\",\n  ];\n}\n\n/**\n * Get Mermaid class definitions for heatmap visualization.\n */\nfunction getHeatmapStyleDefinitions(): string[] {\n  return [\n    // Heatmap colors - cold to hot\n    \"    classDef heat_cold fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e40af\",\n    \"    classDef heat_cool fill:#ccfbf1,stroke:#14b8a6,stroke-width:2px,color:#0f766e\",\n    \"    classDef heat_neutral fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151\",\n    \"    classDef heat_warm fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#92400e\",\n    \"    classDef heat_hot fill:#fed7aa,stroke:#f97316,stroke-width:3px,color:#c2410c\",\n    \"    classDef heat_critical fill:#fecaca,stroke:#ef4444,stroke-width:3px,color:#b91c1c\",\n  ];\n}\n\n/**\n * Get the Mermaid class name for a heat level.\n */\nfunction getHeatClass(level: HeatLevel): string {\n  return `heat_${level}`;\n}\n\n/**\n * Get the Mermaid class name for a step state.\n */\nfunction getStateClass(state: StepState): string {\n  return state;\n}\n\n/**\n * Get Mermaid class definitions for hook visualization.\n */\nfunction getHookStyleDefinitions(): string[] {\n  return [\n    // Hook styles - gear icon aesthetic\n    \"    classDef hook_success fill:#e0f2fe,stroke:#0284c7,stroke-width:2px,color:#0c4a6e\",\n    \"    classDef hook_error fill:#fef2f2,stroke:#dc2626,stroke-width:2px,color:#7f1d1d\",\n  ];\n}\n\n/**\n * Safely stringify a value, handling circular references and BigInt.\n * Returns Result with either the stringified value or an error.\n */\nfunction safeStringify(value: unknown): Result<string, StringifyError> {\n  try {\n    const replacer = (_key: string, v: unknown): unknown => {\n      if (typeof v !== \"bigint\") return v;\n      const n = Number(v);\n      return Number.isSafeInteger(n) ? n : v.toString();\n    };\n    return ok(JSON.stringify(value, replacer));\n  } catch {\n    return err(\"STRINGIFY_ERROR\");\n  }\n}\n\n/**\n * Get stringified value or fallback for unserializable values.\n */\nfunction getStringified(value: unknown): string {\n  const result = safeStringify(value);\n  return result.ok ? result.value : \"[unserializable]\";\n}\n\n/**\n * Render hooks as nodes before the workflow starts.\n * Returns the ID of the last hook node (to connect to workflow start).\n */\nfunction renderHooks(\n  hooks: WorkflowHooks,\n  lines: string[],\n  options: RenderOptions\n): { lastHookId: string | undefined } {\n  let lastHookId: string | undefined;\n\n  // Render shouldRun hook\n  if (hooks.shouldRun) {\n    const hookId = \"hook_shouldRun\";\n    const state = hooks.shouldRun.state === \"success\" ? \"hook_success\" : \"hook_error\";\n    const icon = hooks.shouldRun.state === \"success\" ? \"⚙\" : \"⚠\";\n    const timing = options.showTimings && hooks.shouldRun.durationMs !== undefined\n      ? ` ${formatDuration(hooks.shouldRun.durationMs)}`\n      : \"\";\n    const context = hooks.shouldRun.context?.skipped\n      ? \"\\\\nskipped workflow\"\n      : hooks.shouldRun.context?.result === true\n        ? \"\\\\nproceed\"\n        : \"\";\n\n    lines.push(`    ${hookId}[[\"${icon} shouldRun${context}${timing}\"]]:::${state}`);\n    lastHookId = hookId;\n  }\n\n  // Render onBeforeStart hook\n  if (hooks.onBeforeStart) {\n    const hookId = \"hook_beforeStart\";\n    const state = hooks.onBeforeStart.state === \"success\" ? \"hook_success\" : \"hook_error\";\n    const icon = hooks.onBeforeStart.state === \"success\" ? \"⚙\" : \"⚠\";\n    const timing = options.showTimings && hooks.onBeforeStart.durationMs !== undefined\n      ? ` ${formatDuration(hooks.onBeforeStart.durationMs)}`\n      : \"\";\n    const context = hooks.onBeforeStart.context?.skipped\n      ? \"\\\\nskipped workflow\"\n      : \"\";\n\n    lines.push(`    ${hookId}[[\"${icon} onBeforeStart${context}${timing}\"]]:::${state}`);\n\n    // Connect from previous hook if exists\n    if (lastHookId) {\n      lines.push(`    ${lastHookId} --> ${hookId}`);\n    }\n    lastHookId = hookId;\n  }\n\n  return { lastHookId };\n}\n\n// =============================================================================\n// Node ID Generation\n// =============================================================================\n\nlet nodeCounter = 0;\nconst usedDecisionIds = new Set<string>();\nconst usedStepIds = new Set<string>();\n\nfunction generateNodeId(prefix: string = \"node\"): string {\n  return `${prefix}_${++nodeCounter}`;\n}\n\nfunction resetNodeCounter(): void {\n  nodeCounter = 0;\n  usedDecisionIds.clear();\n  usedStepIds.clear();\n}\n\n// =============================================================================\n// Mermaid Text Escaping\n// =============================================================================\n\n/**\n * Escape text for use in Mermaid diagrams.\n * Only escapes characters that break quoted strings in Mermaid.\n *\n * With bracket-quote syntax (e.g., `nodeId[\"label\"]`), special characters\n * like {}[]() are allowed inside the quoted label.\n *\n * @param text - Text to escape\n * @returns Escaped text safe for Mermaid quoted labels\n */\nfunction escapeMermaidText(text: string): string {\n  return text\n    .replace(/\"/g, \"#quot;\")  // Escape double quotes for Mermaid\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\")\n    .trim();\n}\n\n/**\n * Escape text for use in Mermaid subgraph names.\n * Subgraph names need special handling - brackets and braces must be removed.\n *\n * @param text - Text to escape for subgraph name\n * @returns Escaped text safe for subgraph names\n */\nfunction escapeSubgraphName(text: string): string {\n  return escapeMermaidText(text)\n    .replace(/[{}[\\]()]/g, \"\"); // Remove brackets, braces, and parentheses from subgraph names\n}\n\n// =============================================================================\n// Mermaid Renderer\n// =============================================================================\n\n/**\n * Create the Mermaid diagram renderer.\n */\nexport function mermaidRenderer(): Renderer {\n  return {\n    name: \"mermaid\",\n    supportsLive: false,\n\n    render(ir: WorkflowIR, options: RenderOptions): string {\n      resetNodeCounter();\n      const lines: string[] = [];\n\n      // Check for enhanced options (heatmap)\n      const enhanced = options as EnhancedRenderOptions;\n\n      // Diagram header\n      lines.push(\"flowchart TD\");\n\n      // Render hooks first (if any)\n      let hookExitId: string | undefined;\n      if (ir.hooks) {\n        const hookResult = renderHooks(ir.hooks, lines, options);\n        hookExitId = hookResult.lastHookId;\n      }\n\n      // Start node - more visually distinctive\n      const startId = \"start\";\n      lines.push(`    ${startId}((\"▶ Start\"))`);\n\n      // Connect hooks to start node\n      if (hookExitId) {\n        lines.push(`    ${hookExitId} --> ${startId}`);\n      }\n\n      // Track the last node for connections\n      let prevNodeId = startId;\n\n      // Render children (passing hooks for onAfterStep annotations)\n      for (const child of ir.root.children) {\n        const result = renderNode(child, options, lines, enhanced, ir.hooks);\n        lines.push(`    ${prevNodeId} --> ${result.entryId}`);\n        prevNodeId = result.exitId;\n      }\n\n      // End node (if workflow reached a terminal state) - more visually distinctive\n      const terminalStates = [\"success\", \"error\", \"aborted\"] as const;\n      if (terminalStates.includes(ir.root.state as (typeof terminalStates)[number])) {\n        const endId = \"finish\";\n        const endIcon =\n          ir.root.state === \"success\" ? \"✓\"\n            : ir.root.state === \"error\" ? \"✗\"\n              : \"⊘\";\n        const endLabel =\n          ir.root.state === \"success\" ? \"Done\"\n            : ir.root.state === \"error\" ? \"Failed\"\n              : \"Cancelled\";\n        const endShape = `((\"${endIcon} ${endLabel}\"))`;\n        const endClass =\n          ir.root.state === \"success\" ? \":::success\"\n            : ir.root.state === \"error\" ? \":::error\"\n              : \":::aborted\";\n        lines.push(`    ${endId}${endShape}${endClass}`);\n        lines.push(`    ${prevNodeId} --> ${endId}`);\n      }\n\n      // Add style definitions\n      lines.push(\"\");\n      lines.push(...getStyleDefinitions());\n\n      // Add heatmap styles if enabled\n      if (enhanced.showHeatmap) {\n        lines.push(...getHeatmapStyleDefinitions());\n      }\n\n      // Add hook styles if hooks were rendered\n      if (ir.hooks) {\n        lines.push(...getHookStyleDefinitions());\n      }\n\n      return lines.join(\"\\n\");\n    },\n  };\n}\n\n/**\n * Render result with entry and exit node IDs.\n */\ninterface RenderResult {\n  entryId: string;\n  exitId: string;\n}\n\n/**\n * Render a node and return its entry/exit IDs.\n */\nfunction renderNode(\n  node: FlowNode,\n  options: RenderOptions,\n  lines: string[],\n  enhanced?: EnhancedRenderOptions,\n  hooks?: WorkflowHooks\n): RenderResult {\n  if (isStepNode(node)) {\n    return renderStepNode(node, options, lines, enhanced, hooks);\n  } else if (isParallelNode(node)) {\n    return renderParallelNode(node, options, lines, enhanced, hooks);\n  } else if (isRaceNode(node)) {\n    return renderRaceNode(node, options, lines, enhanced, hooks);\n  } else if (isDecisionNode(node)) {\n    return renderDecisionNode(node, options, lines, enhanced, hooks);\n  } else if (isStreamNode(node)) {\n    return renderStreamNode(node, options, lines);\n  }\n\n  // Fallback for sequence or unknown nodes\n  const id = generateNodeId(\"unknown\");\n  lines.push(`    ${id}[\"Unknown Node\"]`);\n  return { entryId: id, exitId: id };\n}\n\n/**\n * Render a step node.\n */\nfunction renderStepNode(\n  node: StepNode,\n  options: RenderOptions,\n  lines: string[],\n  enhanced?: EnhancedRenderOptions,\n  hooks?: WorkflowHooks\n): RenderResult {\n  // Cast to MermaidRenderOptions to access extended options\n  const mermaidOpts = options as MermaidRenderOptions;\n  const showRetryEdges = mermaidOpts.showRetryEdges ?? true;\n  const showErrorEdges = mermaidOpts.showErrorEdges ?? true;\n  const showTimeoutEdges = mermaidOpts.showTimeoutEdges ?? true;\n\n  // Generate step ID, ensuring uniqueness even with duplicate keys\n  let id = node.key\n    ? `step_${node.key.replace(/[^a-zA-Z0-9]/g, \"_\")}`\n    : generateNodeId(\"step\");\n\n  // Ensure uniqueness by appending suffix if collision\n  if (usedStepIds.has(id)) {\n    let suffix = 2;\n    while (usedStepIds.has(`${id}_${suffix}`)) {\n      suffix++;\n    }\n    id = `${id}_${suffix}`;\n  }\n  usedStepIds.add(id);\n\n  const baseLabel = node.name ?? node.key ?? \"Step\";\n  const labelText = options.showKeys && node.key && node.name\n    ? `${baseLabel} [${node.key}]`\n    : baseLabel;\n  const label = escapeMermaidText(labelText);\n\n  // Format timing - use space instead of parentheses to avoid Mermaid parse errors\n  const timing =\n    options.showTimings && node.durationMs !== undefined\n      ? ` ${formatDuration(node.durationMs)}`\n      : \"\";\n\n  // Add visual indicators based on state (like XState/AWS Step Functions)\n  let stateIcon = \"\";\n  switch (node.state) {\n    case \"success\":\n      stateIcon = \"✓ \";\n      break;\n    case \"error\":\n      stateIcon = \"✗ \";\n      break;\n    case \"cached\":\n      stateIcon = \"💾 \";\n      break;\n    case \"running\":\n      stateIcon = \"⏳ \";\n      break;\n    case \"skipped\":\n      stateIcon = \"⊘ \";\n      break;\n  }\n\n  // Add input/output info if available\n  // Use newlines for multi-line labels, but escape special characters\n  let ioInfo = \"\";\n  if (node.input !== undefined) {\n    const inputStr = typeof node.input === \"string\"\n      ? escapeMermaidText(node.input)\n      : escapeMermaidText(getStringified(node.input).slice(0, 20));\n    ioInfo += `\\\\nin: ${inputStr}`;\n  }\n  if (node.output !== undefined && node.state === \"success\") {\n    const outputStr = typeof node.output === \"string\"\n      ? escapeMermaidText(node.output)\n      : escapeMermaidText(getStringified(node.output).slice(0, 20));\n    ioInfo += `\\\\nout: ${outputStr}`;\n  }\n\n  // Add onAfterStep hook info if present (check by key first, then by id)\n  let hookInfo = \"\";\n  const hookKey = node.key ?? node.id;\n  if (hooks && hookKey && hooks.onAfterStep.has(hookKey)) {\n    const hookExec = hooks.onAfterStep.get(hookKey)!;\n    const hookIcon = hookExec.state === \"success\" ? \"⚙\" : \"⚠\";\n    const hookTiming = options.showTimings && hookExec.durationMs !== undefined\n      ? ` ${formatDuration(hookExec.durationMs)}`\n      : \"\";\n    hookInfo = `\\\\n${hookIcon} hook${hookTiming}`;\n  }\n\n  // Combine all label parts with icon (retry/timeout info moved to edges)\n  const escapedLabel = (stateIcon + label + ioInfo + hookInfo + timing).trim();\n\n  // Determine class: use heatmap if enabled and data available, otherwise use state\n  // Lookup order matches PerformanceAnalyzer: key ?? name ?? id\n  let nodeClass: string;\n  const heat = enhanced?.showHeatmap && enhanced.heatmapData\n    ? enhanced.heatmapData.heat.get(node.key ?? \"\") ??\n      enhanced.heatmapData.heat.get(node.name ?? \"\") ??\n      enhanced.heatmapData.heat.get(node.id)\n    : undefined;\n\n  if (heat !== undefined) {\n    const level = getHeatLevel(heat);\n    nodeClass = getHeatClass(level);\n  } else {\n    nodeClass = getStateClass(node.state);\n  }\n\n  // Use different shapes based on state (like AWS Step Functions)\n  let shape: string;\n  switch (node.state) {\n    case \"error\":\n      // Hexagon for errors (more distinctive)\n      shape = `{{\"${escapedLabel}\"}}`;\n      break;\n    case \"cached\":\n      // Rounded rectangle with double border for cached\n      shape = `[(\"${escapedLabel}\")]`;\n      break;\n    case \"skipped\":\n      // Dashed border via class (applied once in lines.push below)\n      shape = `[\"${escapedLabel}\"]`;\n      break;\n    default:\n      // Standard rectangle for normal steps\n      shape = `[\"${escapedLabel}\"]`;\n  }\n\n  lines.push(`    ${id}${shape}:::${nodeClass}`);\n\n  // NEW: Add retry loop edge (self-loop showing retries)\n  if (showRetryEdges && node.retryCount !== undefined && node.retryCount > 0) {\n    const retryLabel = `↻ ${node.retryCount} retr${node.retryCount === 1 ? \"y\" : \"ies\"}`;\n    lines.push(`    ${id} -.->|\"${retryLabel}\"| ${id}`);\n  }\n\n  // NEW: Add error path edge (flow to error node)\n  if (showErrorEdges && node.state === \"error\" && node.error !== undefined) {\n    const errorNodeId = `ERR_${id}`;\n    const errorLabel = escapeMermaidText(String(node.error)).slice(0, 30);\n    lines.push(`    ${errorNodeId}{{\"${errorLabel}\"}}`);\n    lines.push(`    ${id} -->|error| ${errorNodeId}`);\n    lines.push(`    style ${errorNodeId} fill:#fee2e2,stroke:#dc2626`);\n  }\n\n  // NEW: Add timeout edge (alternative timeout path)\n  if (showTimeoutEdges && node.timedOut) {\n    const timeoutNodeId = `TO_${id}`;\n    const timeoutMs = node.timeoutMs !== undefined ? `${node.timeoutMs}ms` : \"\";\n    lines.push(`    ${timeoutNodeId}{{\"⏱ Timeout ${timeoutMs}\"}}`);\n    lines.push(`    ${id} -.->|timeout| ${timeoutNodeId}`);\n    lines.push(`    style ${timeoutNodeId} fill:#fef3c7,stroke:#f59e0b`);\n  }\n\n  return { entryId: id, exitId: id };\n}\n\n/**\n * Render a parallel node as a subgraph with fork/join.\n */\nfunction renderParallelNode(\n  node: ParallelNode,\n  options: RenderOptions,\n  lines: string[],\n  enhanced?: EnhancedRenderOptions,\n  hooks?: WorkflowHooks\n): RenderResult {\n  const subgraphId = generateNodeId(\"parallel\");\n  const forkId = `${subgraphId}_fork`;\n  const joinId = `${subgraphId}_join`;\n  const name = escapeSubgraphName(node.name ?? \"Parallel\");\n  const modeLabel = node.mode === \"allSettled\" ? \" (allSettled)\" : \"\";\n\n  // If no children, render as a simple step-like node with note\n  if (node.children.length === 0) {\n    const id = subgraphId;\n    const label = escapeMermaidText(`${name}${modeLabel}`);\n    const note = \"operations not individually tracked\";\n    const timing = options.showTimings && node.durationMs !== undefined\n      ? ` ${formatDuration(node.durationMs)}`\n      : \"\";\n\n    // Use a rounded rectangle to indicate it's a parallel operation\n    lines.push(`    ${id}[\"${label}${timing}\\\\n${note}\"]:::${getStateClass(node.state)}`);\n    return { entryId: id, exitId: id };\n  }\n\n  // Subgraph for parallel block with proper visual hierarchy\n  lines.push(`    subgraph ${subgraphId}[\"${name}${modeLabel}\"]`);\n  lines.push(`    direction TB`);\n\n  // Fork node (diamond) - more visually distinct\n  lines.push(`    ${forkId}{\"⚡ Fork\"}`);\n\n  // Child branches - render in parallel columns\n  const childExitIds: string[] = [];\n  for (const child of node.children) {\n    const result = renderNode(child, options, lines, enhanced, hooks);\n    lines.push(`    ${forkId} --> ${result.entryId}`);\n    childExitIds.push(result.exitId);\n  }\n\n  // Join node (diamond) - visually distinct\n  lines.push(`    ${joinId}{\"✓ Join\"}`);\n  for (const exitId of childExitIds) {\n    lines.push(`    ${exitId} --> ${joinId}`);\n  }\n\n  lines.push(`    end`);\n\n  // Apply state styling to subgraph\n  const stateClass = getStateClass(node.state);\n  lines.push(`    class ${subgraphId} ${stateClass}`);\n\n  return { entryId: forkId, exitId: joinId };\n}\n\n/**\n * Render a race node as a subgraph with racing indicator.\n */\nfunction renderRaceNode(\n  node: RaceNode,\n  options: RenderOptions,\n  lines: string[],\n  enhanced?: EnhancedRenderOptions,\n  hooks?: WorkflowHooks\n): RenderResult {\n  const subgraphId = generateNodeId(\"race\");\n  const startId = `${subgraphId}_start`;\n  const endId = `${subgraphId}_end`;\n  const name = escapeSubgraphName(node.name ?? \"Race\");\n\n  // If no children, render as a simple step-like node with note\n  if (node.children.length === 0) {\n    const id = subgraphId;\n    const label = escapeMermaidText(name);\n    const note = \"operations not individually tracked\";\n    const timing = options.showTimings && node.durationMs !== undefined\n      ? ` ${formatDuration(node.durationMs)}`\n      : \"\";\n\n    lines.push(`    ${id}[\"⚡ ${label}${timing}\\\\n${note}\"]:::${getStateClass(node.state)}`);\n    return { entryId: id, exitId: id };\n  }\n\n  // Subgraph for race block - escape name and emoji is safe in quoted strings\n  lines.push(`    subgraph ${subgraphId}[\"⚡ ${name}\"]`);\n  lines.push(`    direction TB`);\n\n  // Start node - use a more distinctive shape\n  lines.push(`    ${startId}((\"🏁 Start\"))`);\n\n  // Child branches\n  const childExitIds: Array<{ exitId: string; isWinner: boolean }> = [];\n  let winnerExitId: string | undefined;\n\n  for (const child of node.children) {\n    const result = renderNode(child, options, lines, enhanced, hooks);\n    const isWinner = node.winnerId === child.id;\n    lines.push(`    ${startId} --> ${result.entryId}`);\n\n    if (isWinner) {\n      winnerExitId = result.exitId;\n    }\n    childExitIds.push({ exitId: result.exitId, isWinner });\n  }\n\n  // End node - more distinctive\n  lines.push(`    ${endId}((\"✓ First\"))`);\n\n  // Connect winner with thick line, others with dashed (cancelled)\n  for (const { exitId, isWinner } of childExitIds) {\n    if (isWinner && winnerExitId) {\n      lines.push(`    ${exitId} ==>|🏆 Winner| ${endId}`);\n    } else if (node.winnerId) {\n      // Non-winner: show as cancelled\n      lines.push(`    ${exitId} -. cancelled .-> ${endId}`);\n    } else {\n      // No winner determined, normal connection\n      lines.push(`    ${exitId} --> ${endId}`);\n    }\n  }\n\n  lines.push(`    end`);\n\n  const stateClass = getStateClass(node.state);\n  lines.push(`    class ${subgraphId} ${stateClass}`);\n\n  return { entryId: startId, exitId: endId };\n}\n\n/**\n * Render a decision node as a diamond with branches.\n */\nfunction renderDecisionNode(\n  node: DecisionNode,\n  options: RenderOptions,\n  lines: string[],\n  enhanced?: EnhancedRenderOptions,\n  hooks?: WorkflowHooks\n): RenderResult {\n  // Generate decision ID, ensuring uniqueness even with duplicate keys\n  let decisionId = node.key\n    ? `decision_${node.key.replace(/[^a-zA-Z0-9]/g, \"_\")}`\n    : generateNodeId(\"decision\");\n\n  // Ensure uniqueness by appending suffix if collision\n  if (usedDecisionIds.has(decisionId)) {\n    let suffix = 2;\n    while (usedDecisionIds.has(`${decisionId}_${suffix}`)) {\n      suffix++;\n    }\n    decisionId = `${decisionId}_${suffix}`;\n  }\n  usedDecisionIds.add(decisionId);\n\n  // Escape condition and decision value - remove characters that break Mermaid\n  const condition = escapeMermaidText(node.condition ?? \"condition\");\n  const decisionValue = node.decisionValue !== undefined\n    ? ` = ${escapeMermaidText(String(node.decisionValue)).slice(0, 30)}`\n    : \"\";\n\n  // Decision diamond - ensure no invalid characters\n  const decisionLabel = `${condition}${decisionValue}`.trim();\n  lines.push(`    ${decisionId}{\"${decisionLabel}\"}`);\n\n  // Render branches\n  const branchExitIds: string[] = [];\n  let takenBranchExitId: string | undefined;\n  const usedBranchIds = new Set<string>();\n\n  for (const branch of node.branches) {\n    // Generate base branch ID from sanitized label\n    let branchId = `${decisionId}_${branch.label.replace(/[^a-zA-Z0-9]/g, \"_\")}`;\n    // Ensure uniqueness by appending index if collision\n    if (usedBranchIds.has(branchId)) {\n      let suffix = 2;\n      while (usedBranchIds.has(`${branchId}_${suffix}`)) {\n        suffix++;\n      }\n      branchId = `${branchId}_${suffix}`;\n    }\n    usedBranchIds.add(branchId);\n    // Escape branch label - remove parentheses and other special chars\n    const branchLabelText = escapeMermaidText(branch.label);\n    const branchLabel = branch.taken\n      ? `${branchLabelText} ✓`\n      : `${branchLabelText} skipped`;\n    const branchClass = branch.taken ? \":::success\" : \":::skipped\";\n\n    // Branch label node\n    lines.push(`    ${branchId}[\"${branchLabel}\"]${branchClass}`);\n\n    // Connect decision to branch\n    // Mermaid edge labels must be simple text - escape special characters\n    // Also remove pipe character as it's used for edge label syntax\n    const edgeLabel = branch.condition\n      ? `|${escapeMermaidText(branch.condition).replace(/\\|/g, \"\")}|`\n      : \"\";\n    lines.push(`    ${decisionId} -->${edgeLabel} ${branchId}`);\n\n    // Render children of this branch\n    if (branch.children.length > 0) {\n      let prevId = branchId;\n      for (const child of branch.children) {\n        const result = renderNode(child, options, lines, enhanced, hooks);\n        lines.push(`    ${prevId} --> ${result.entryId}`);\n        prevId = result.exitId;\n      }\n      branchExitIds.push(prevId);\n      if (branch.taken) {\n        takenBranchExitId = prevId;\n      }\n    } else {\n      branchExitIds.push(branchId);\n      if (branch.taken) {\n        takenBranchExitId = branchId;\n      }\n    }\n  }\n\n  // Join point (if we have a taken branch)\n  if (takenBranchExitId) {\n    return { entryId: decisionId, exitId: takenBranchExitId };\n  }\n\n  // If no branch was taken, return decision as exit\n  return { entryId: decisionId, exitId: decisionId };\n}\n\n/**\n * Render a stream node.\n * Uses hexagonal shape to distinguish from regular steps.\n */\nfunction renderStreamNode(\n  node: StreamNode,\n  options: RenderOptions,\n  lines: string[]\n): RenderResult {\n  const id = `stream_${node.namespace.replace(/[^a-zA-Z0-9]/g, \"_\")}_${generateNodeId(\"\")}`;\n\n  // Format counts\n  const counts = `W:${node.writeCount} R:${node.readCount}`;\n\n  // Add state icon\n  let stateIcon = \"\";\n  switch (node.streamState) {\n    case \"active\":\n      stateIcon = \"⟳ \";\n      break;\n    case \"closed\":\n      stateIcon = \"✓ \";\n      break;\n    case \"error\":\n      stateIcon = \"✗ \";\n      break;\n  }\n\n  // Format timing\n  const timing =\n    options.showTimings && node.durationMs !== undefined\n      ? ` ${formatDuration(node.durationMs)}`\n      : \"\";\n\n  // Backpressure indicator\n  const backpressure = node.backpressureOccurred ? \"\\\\nbackpressure\" : \"\";\n\n  // Combine label parts - use hexagon shape for streams\n  const label = `${stateIcon}stream:${escapeMermaidText(node.namespace)}\\\\n${counts}${backpressure}${timing}`;\n\n  // Determine class based on stream state\n  let nodeClass: string;\n  if (node.streamState === \"error\") {\n    nodeClass = \"streamError\";\n  } else if (node.streamState === \"active\") {\n    nodeClass = \"streamActive\";\n  } else {\n    nodeClass = \"stream\";\n  }\n\n  // Hexagonal shape for streams: {{\"label\"}}\n  lines.push(`    ${id}{{\"${label}\"}}:::${nodeClass}`);\n\n  return { entryId: id, exitId: id };\n}\n\nexport { mermaidRenderer as default };\n","/**\n * Performance Analyzer\n *\n * Analyzes workflow execution data to identify:\n * - Slow steps (bottlenecks)\n * - Retry patterns\n * - Error-prone steps\n * - Timing anomalies\n *\n * Aggregates metrics across multiple workflow runs to provide\n * statistical insights and heatmap visualization data.\n */\n\nimport type { WorkflowEvent } from \"awaitly\";\nimport type {\n  NodePerformance,\n  HeatmapData,\n  WorkflowIR,\n  FlowNode,\n  HeatLevel,\n} from \"./types\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * A recorded workflow run for analysis.\n */\nexport interface WorkflowRun {\n  /** Unique identifier for this run */\n  id: string;\n  /** Workflow start timestamp */\n  startTime: number;\n  /** All events from the workflow execution */\n  events: WorkflowEvent<unknown>[];\n}\n\n/**\n * Performance analyzer interface.\n */\nexport interface PerformanceAnalyzer {\n  /** Add a completed workflow run for analysis */\n  addRun: (run: WorkflowRun) => void;\n\n  /** Add events incrementally (alternative to addRun) */\n  addEvent: (event: WorkflowEvent<unknown>) => void;\n\n  /** Finalize current run (when using addEvent) */\n  finalizeRun: (runId: string) => void;\n\n  /** Get performance stats for a specific node */\n  getNodePerformance: (nodeId: string) => NodePerformance | undefined;\n\n  /** Get heatmap data for an IR */\n  getHeatmap: (\n    ir: WorkflowIR,\n    metric?: \"duration\" | \"retryRate\" | \"errorRate\"\n  ) => HeatmapData;\n\n  /** Get slowest nodes */\n  getSlowestNodes: (limit?: number) => NodePerformance[];\n\n  /** Get error-prone nodes */\n  getErrorProneNodes: (limit?: number) => NodePerformance[];\n\n  /** Get retry-prone nodes */\n  getRetryProneNodes: (limit?: number) => NodePerformance[];\n\n  /** Get all performance data */\n  getAllPerformance: () => Map<string, NodePerformance>;\n\n  /** Export performance data as JSON */\n  exportData: () => string;\n\n  /** Import performance data from JSON */\n  importData: (json: string) => void;\n\n  /** Clear all collected data */\n  clear: () => void;\n}\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Flatten all nodes from an IR tree.\n */\nfunction flattenNodes(nodes: FlowNode[]): FlowNode[] {\n  const result: FlowNode[] = [];\n  for (const node of nodes) {\n    result.push(node);\n    if (\"children\" in node && Array.isArray(node.children)) {\n      result.push(...flattenNodes(node.children));\n    }\n    if (\"branches\" in node) {\n      for (const branch of node.branches) {\n        result.push(...flattenNodes(branch.children));\n      }\n    }\n  }\n  return result;\n}\n\n/**\n * Calculate percentile value from sorted array.\n */\nfunction percentile(sortedValues: number[], p: number): number {\n  if (sortedValues.length === 0) return 0;\n  const index = Math.floor(sortedValues.length * p);\n  return sortedValues[Math.min(index, sortedValues.length - 1)];\n}\n\n/**\n * Get heat level from normalized value (0-1).\n */\nexport function getHeatLevel(heat: number): HeatLevel {\n  if (heat < 0.2) return \"cold\";\n  if (heat < 0.4) return \"cool\";\n  if (heat < 0.6) return \"neutral\";\n  if (heat < 0.8) return \"warm\";\n  if (heat < 0.95) return \"hot\";\n  return \"critical\";\n}\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Create a performance analyzer for workflow metrics.\n *\n * @example\n * ```typescript\n * const analyzer = createPerformanceAnalyzer();\n *\n * // Add completed runs\n * analyzer.addRun({ id: 'run-1', startTime: Date.now(), events });\n *\n * // Get insights\n * const slowest = analyzer.getSlowestNodes(5);\n * const heatmap = analyzer.getHeatmap(ir, 'duration');\n * ```\n */\nexport function createPerformanceAnalyzer(): PerformanceAnalyzer {\n  // Timing data: nodeId → array of durations (ms)\n  const timingData = new Map<string, number[]>();\n\n  // Retry data: nodeId → { retried runs, total runs }\n  const retryData = new Map<string, { retried: number; total: number }>();\n\n  // Error data: nodeId → { error runs, total runs }\n  const errorData = new Map<string, { errors: number; total: number }>();\n\n  // Timeout data: nodeId → { timed out, total }\n  const timeoutData = new Map<string, { timedOut: number; total: number }>();\n\n  // Current run state (for incremental event adding)\n  let currentRunEvents: WorkflowEvent<unknown>[] = [];\n\n  /**\n   * Get node ID from event (for grouping and metrics).\n   * Prioritizes stepKey (cache/instance identity), then stepId (step label), then name\n   * so distinct steps (e.g. same id in a loop, different key) are not merged.\n   * With the current awaitly API, stepId is always set; stepKey is set when key option is used.\n   */\n  function getNodeId(event: {\n    stepId?: string;\n    stepKey?: string;\n    name?: string;\n  }): string {\n    return event.stepKey ?? event.stepId ?? event.name ?? \"unknown\";\n  }\n\n  /**\n   * Process events from a workflow run.\n   */\n  function processEvents(events: WorkflowEvent<unknown>[]): void {\n    // Track step state during processing\n    const stepState = new Map<\n      string,\n      {\n        retried: boolean;\n        timedOut: boolean;\n      }\n    >();\n\n    for (const event of events) {\n      switch (event.type) {\n        case \"step_start\": {\n          const id = getNodeId(event);\n          stepState.set(id, { retried: false, timedOut: false });\n          break;\n        }\n\n        case \"step_retry\": {\n          const id = getNodeId(event);\n          const state = stepState.get(id);\n          if (state) {\n            state.retried = true;\n          }\n          break;\n        }\n\n        case \"step_timeout\": {\n          const id = getNodeId(event);\n          const state = stepState.get(id);\n          if (state) {\n            state.timedOut = true;\n          }\n          // Don't update counts here - wait for step completion\n          // to get accurate total count across all runs\n          break;\n        }\n\n        case \"step_success\": {\n          const id = getNodeId(event);\n          const state = stepState.get(id);\n\n          // Record timing\n          const timings = timingData.get(id) ?? [];\n          timings.push(event.durationMs);\n          timingData.set(id, timings);\n\n          // Record retry status\n          const retry = retryData.get(id) ?? { retried: 0, total: 0 };\n          retry.total++;\n          if (state?.retried) retry.retried++;\n          retryData.set(id, retry);\n\n          // Record timeout status\n          const timeout = timeoutData.get(id) ?? { timedOut: 0, total: 0 };\n          timeout.total++;\n          if (state?.timedOut) timeout.timedOut++;\n          timeoutData.set(id, timeout);\n\n          // Record success (no error)\n          const error = errorData.get(id) ?? { errors: 0, total: 0 };\n          error.total++;\n          errorData.set(id, error);\n\n          stepState.delete(id);\n          break;\n        }\n\n        case \"step_error\": {\n          const id = getNodeId(event);\n          const state = stepState.get(id);\n\n          // Record timing\n          const timings = timingData.get(id) ?? [];\n          timings.push(event.durationMs);\n          timingData.set(id, timings);\n\n          // Record retry status\n          const retry = retryData.get(id) ?? { retried: 0, total: 0 };\n          retry.total++;\n          if (state?.retried) retry.retried++;\n          retryData.set(id, retry);\n\n          // Record timeout status\n          const timeout = timeoutData.get(id) ?? { timedOut: 0, total: 0 };\n          timeout.total++;\n          if (state?.timedOut) timeout.timedOut++;\n          timeoutData.set(id, timeout);\n\n          // Record error\n          const error = errorData.get(id) ?? { errors: 0, total: 0 };\n          error.total++;\n          error.errors++;\n          errorData.set(id, error);\n\n          stepState.delete(id);\n          break;\n        }\n      }\n    }\n  }\n\n  /**\n   * Add a completed workflow run.\n   */\n  function addRun(run: WorkflowRun): void {\n    processEvents(run.events);\n  }\n\n  /**\n   * Add an event incrementally.\n   */\n  function addEvent(event: WorkflowEvent<unknown>): void {\n    currentRunEvents.push(event);\n  }\n\n  /**\n   * Finalize current run (process accumulated events).\n   */\n  function finalizeRun(_runId: string): void {\n    if (currentRunEvents.length > 0) {\n      processEvents(currentRunEvents);\n      currentRunEvents = [];\n    }\n  }\n\n  /**\n   * Compute performance metrics for a node.\n   */\n  function computePerformance(nodeId: string): NodePerformance | undefined {\n    const timings = timingData.get(nodeId);\n    if (!timings || timings.length === 0) return undefined;\n\n    const sorted = [...timings].sort((a, b) => a - b);\n    const sum = sorted.reduce((a, b) => a + b, 0);\n    const mean = sum / sorted.length;\n    const variance =\n      sorted.reduce((acc, t) => acc + (t - mean) ** 2, 0) / sorted.length;\n\n    const retry = retryData.get(nodeId) ?? { retried: 0, total: 1 };\n    const error = errorData.get(nodeId) ?? { errors: 0, total: 1 };\n    const timeout = timeoutData.get(nodeId) ?? { timedOut: 0, total: 1 };\n\n    return {\n      nodeId,\n      avgDurationMs: mean,\n      minDurationMs: sorted[0],\n      maxDurationMs: sorted[sorted.length - 1],\n      stdDevMs: Math.sqrt(variance),\n      samples: sorted.length,\n      retryRate: retry.total > 0 ? retry.retried / retry.total : 0,\n      timeoutRate: timeout.total > 0 ? timeout.timedOut / timeout.total : 0,\n      errorRate: error.total > 0 ? error.errors / error.total : 0,\n      percentiles: {\n        p50: percentile(sorted, 0.5),\n        p90: percentile(sorted, 0.9),\n        p95: percentile(sorted, 0.95),\n        p99: percentile(sorted, 0.99),\n      },\n    };\n  }\n\n  /**\n   * Get performance stats for a specific node.\n   */\n  function getNodePerformance(nodeId: string): NodePerformance | undefined {\n    return computePerformance(nodeId);\n  }\n\n  /**\n   * Get heatmap data for an IR.\n   */\n  function getHeatmap(\n    ir: WorkflowIR,\n    metric: \"duration\" | \"retryRate\" | \"errorRate\" = \"duration\"\n  ): HeatmapData {\n    const heat = new Map<string, number>();\n    const allNodes = flattenNodes(ir.root.children);\n\n    // Compute values for all nodes\n    const values: Array<{ id: string; value: number }> = [];\n    for (const node of allNodes) {\n      // Use same lookup order as getNodeId: stepKey ?? stepId ?? name (IR has key, id, name)\n      const lookupKey =\n        (\"key\" in node ? node.key : undefined) ?? node.id ?? node.name;\n      const perf = computePerformance(lookupKey);\n      if (perf) {\n        let value: number;\n        switch (metric) {\n          case \"duration\":\n            value = perf.avgDurationMs;\n            break;\n          case \"retryRate\":\n            value = perf.retryRate;\n            break;\n          case \"errorRate\":\n            value = perf.errorRate;\n            break;\n        }\n        values.push({ id: node.id, value });\n      }\n    }\n\n    if (values.length === 0) {\n      return {\n        heat,\n        metric,\n        stats: { min: 0, max: 0, mean: 0, threshold: 0 },\n      };\n    }\n\n    // Compute statistics\n    const vals = values.map((v) => v.value);\n    const min = Math.min(...vals);\n    const max = Math.max(...vals);\n    const mean = vals.reduce((a, b) => a + b, 0) / vals.length;\n    const range = max - min || 1;\n\n    // Normalize to 0-1 heat values\n    for (const { id, value } of values) {\n      heat.set(id, (value - min) / range);\n    }\n\n    return {\n      heat,\n      metric,\n      stats: {\n        min,\n        max,\n        mean,\n        threshold: mean + (max - mean) * 0.5, // 50% above mean is \"hot\"\n      },\n    };\n  }\n\n  /**\n   * Get all performance data.\n   */\n  function getAllPerformance(): Map<string, NodePerformance> {\n    const result = new Map<string, NodePerformance>();\n    for (const nodeId of timingData.keys()) {\n      const perf = computePerformance(nodeId);\n      if (perf) result.set(nodeId, perf);\n    }\n    return result;\n  }\n\n  /**\n   * Get slowest nodes by average duration.\n   */\n  function getSlowestNodes(limit = 10): NodePerformance[] {\n    const all = getAllPerformance();\n    return [...all.values()]\n      .sort((a, b) => b.avgDurationMs - a.avgDurationMs)\n      .slice(0, limit);\n  }\n\n  /**\n   * Get error-prone nodes by error rate.\n   */\n  function getErrorProneNodes(limit = 10): NodePerformance[] {\n    const all = getAllPerformance();\n    return [...all.values()]\n      .filter((p) => p.errorRate > 0)\n      .sort((a, b) => b.errorRate - a.errorRate)\n      .slice(0, limit);\n  }\n\n  /**\n   * Get retry-prone nodes by retry rate.\n   */\n  function getRetryProneNodes(limit = 10): NodePerformance[] {\n    const all = getAllPerformance();\n    return [...all.values()]\n      .filter((p) => p.retryRate > 0)\n      .sort((a, b) => b.retryRate - a.retryRate)\n      .slice(0, limit);\n  }\n\n  /**\n   * Export performance data as JSON.\n   */\n  function exportData(): string {\n    return JSON.stringify({\n      timingData: Object.fromEntries(timingData),\n      retryData: Object.fromEntries(retryData),\n      errorData: Object.fromEntries(errorData),\n      timeoutData: Object.fromEntries(timeoutData),\n    });\n  }\n\n  /**\n   * Import performance data from JSON.\n   */\n  function importData(json: string): void {\n    const data = JSON.parse(json) as {\n      timingData?: Record<string, number[]>;\n      retryData?: Record<string, { retried: number; total: number }>;\n      errorData?: Record<string, { errors: number; total: number }>;\n      timeoutData?: Record<string, { timedOut: number; total: number }>;\n    };\n\n    // Clear existing data\n    timingData.clear();\n    retryData.clear();\n    errorData.clear();\n    timeoutData.clear();\n\n    // Import timing data\n    for (const [k, v] of Object.entries(data.timingData ?? {})) {\n      timingData.set(k, v);\n    }\n\n    // Import retry data\n    for (const [k, v] of Object.entries(data.retryData ?? {})) {\n      retryData.set(k, v);\n    }\n\n    // Import error data\n    for (const [k, v] of Object.entries(data.errorData ?? {})) {\n      errorData.set(k, v);\n    }\n\n    // Import timeout data\n    for (const [k, v] of Object.entries(data.timeoutData ?? {})) {\n      timeoutData.set(k, v);\n    }\n  }\n\n  /**\n   * Clear all collected data.\n   */\n  function clear(): void {\n    timingData.clear();\n    retryData.clear();\n    errorData.clear();\n    timeoutData.clear();\n    currentRunEvents = [];\n  }\n\n  return {\n    addRun,\n    addEvent,\n    finalizeRun,\n    getNodePerformance,\n    getHeatmap,\n    getSlowestNodes,\n    getErrorProneNodes,\n    getRetryProneNodes,\n    getAllPerformance,\n    exportData,\n    importData,\n    clear,\n  };\n}\n","/**\n * Kroki Encoder\n *\n * Encodes Mermaid diagram text for Kroki URLs using pako deflate + base64url.\n * Uses Buffer in Node (btoa/atob not available) and btoa/atob in browsers.\n */\n\nimport pako from \"pako\";\n\n/** True when Buffer is available (Node). */\nconst hasBuffer = typeof globalThis !== \"undefined\" && \"Buffer\" in globalThis && typeof (globalThis as { Buffer?: unknown }).Buffer === \"function\";\n\n/**\n * Base64URL encode bytes (URL-safe base64).\n * Uses `-` and `_` instead of `+` and `/`, and omits padding.\n * Node-safe: uses Buffer when available, otherwise btoa.\n */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n  let base64: string;\n  if (hasBuffer) {\n    const B = (globalThis as unknown as { Buffer: { from: (u: Uint8Array) => { toString: (enc: string) => string } } }).Buffer;\n    base64 = B.from(bytes).toString(\"base64\");\n  } else {\n    let binary = \"\";\n    for (let i = 0; i < bytes.length; i++) {\n      binary += String.fromCharCode(bytes[i]);\n    }\n    base64 = btoa(binary);\n  }\n  return base64\n    .replace(/\\+/g, \"-\")\n    .replace(/\\//g, \"_\")\n    .replace(/=+$/, \"\"); // Remove padding\n}\n\n/**\n * Decode standard base64 to bytes.\n * Node-safe: uses Buffer when available, otherwise atob.\n */\nfunction base64ToBytes(base64: string): Uint8Array {\n  if (hasBuffer) {\n    const B = (globalThis as unknown as { Buffer: { from: (s: string, enc: string) => Uint8Array } }).Buffer;\n    return B.from(base64, \"base64\");\n  }\n  const binary = atob(base64);\n  const bytes = new Uint8Array(binary.length);\n  for (let i = 0; i < binary.length; i++) {\n    bytes[i] = binary.charCodeAt(i);\n  }\n  return bytes;\n}\n\n/**\n * Encode text for Kroki URL.\n * Uses pako deflate compression + base64url encoding.\n *\n * @param text - The text to encode (e.g., Mermaid diagram)\n * @returns URL-safe encoded string\n *\n * @example\n * ```typescript\n * const encoded = encodeForKroki('flowchart TD\\n  A-->B');\n * // => \"eNpLzs8tyc9NTgQADsMDmA\"\n * ```\n */\nexport function encodeForKroki(text: string): string {\n  // Convert string to UTF-8 bytes\n  const textEncoder = new TextEncoder();\n  const textBytes = textEncoder.encode(text);\n\n  // Compress with deflate\n  const compressed = pako.deflate(textBytes);\n\n  // Base64URL encode\n  return base64UrlEncode(compressed);\n}\n\n/**\n * Decode Kroki URL payload back to text.\n * Uses base64url decoding + pako inflate.\n *\n * @param encoded - The encoded string from a Kroki URL\n * @returns The original text\n *\n * @example\n * ```typescript\n * const text = decodeFromKroki('eNpLzs8tyc9NTgQADsMDmA');\n * // => \"flowchart TD\\n  A-->B\"\n * ```\n */\nexport function decodeFromKroki(encoded: string): string {\n  // Convert URL-safe base64 to standard base64\n  let base64 = encoded.replace(/-/g, \"+\").replace(/_/g, \"/\");\n\n  // Add padding if needed\n  const padding = 4 - (base64.length % 4);\n  if (padding !== 4) {\n    base64 += \"=\".repeat(padding);\n  }\n\n  const bytes = base64ToBytes(base64);\n  const decompressed = pako.inflate(bytes);\n  const textDecoder = new TextDecoder();\n  return textDecoder.decode(decompressed);\n}\n","/**\n * Kroki URL Generation\n *\n * Generates shareable URLs for Kroki diagram rendering service.\n * Works in both browser and Node.js environments.\n */\n\nimport type { WorkflowIR, RenderOptions, KrokiExportOptions } from \"../types\";\nimport { mermaidRenderer, defaultColorScheme } from \"../renderers\";\nimport { encodeForKroki } from \"./encoder\";\n\n/**\n * Supported Kroki output formats.\n */\nexport type KrokiFormat = \"svg\" | \"png\" | \"pdf\" | \"jpeg\";\n\n/**\n * Options for URL generator.\n */\nexport interface UrlGeneratorOptions {\n  /** Base URL for Kroki service (default: https://kroki.io) */\n  baseUrl?: string;\n}\n\n/**\n * Default Kroki base URL.\n */\nconst DEFAULT_KROKI_URL = \"https://kroki.io\";\n\n/**\n * Build a Kroki URL for the given diagram.\n *\n * @param diagramType - Diagram type (e.g., \"mermaid\", \"plantuml\", \"graphviz\")\n * @param format - Output format (svg, png, pdf, jpeg)\n * @param text - The diagram text\n * @param options - URL generator options (KrokiExportOptions or legacy UrlGeneratorOptions)\n * @returns The Kroki URL\n *\n * @example\n * ```typescript\n * const url = buildKrokiUrl('mermaid', 'svg', 'flowchart TD\\n  A-->B');\n * // => \"https://kroki.io/mermaid/svg/eNpLzs8tyc9NTgQADsMDmA\"\n *\n * // With explicit KrokiExportOptions\n * const url2 = buildKrokiUrl('mermaid', 'svg', 'flowchart TD\\n  A-->B', {\n *   provider: 'kroki',\n *   baseUrl: 'https://kroki.internal'\n * });\n * ```\n */\nexport function buildKrokiUrl(\n  diagramType: string,\n  format: KrokiFormat,\n  text: string,\n  options: KrokiExportOptions | UrlGeneratorOptions = {}\n): string {\n  const baseUrl = options.baseUrl ?? DEFAULT_KROKI_URL;\n  const encoded = encodeForKroki(text);\n  return `${baseUrl}/${diagramType}/${format}/${encoded}`;\n}\n\n/**\n * Generate a Kroki URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param format - Output format (default: 'svg')\n * @param options - Optional URL generator options\n * @returns The Kroki URL\n *\n * @example\n * ```typescript\n * const url = toKrokiUrl(workflowIR, 'svg');\n * // Share this URL - image renders when viewed\n * ```\n */\nexport function toKrokiUrl(\n  ir: WorkflowIR,\n  format: KrokiFormat = \"svg\",\n  options: UrlGeneratorOptions = {}\n): string {\n  const renderer = mermaidRenderer();\n  const renderOptions: RenderOptions = {\n    showTimings: true,\n    showKeys: false,\n    terminalWidth: 80,\n    colors: defaultColorScheme,\n  };\n\n  const mermaidText = renderer.render(ir, renderOptions);\n  return buildKrokiUrl(\"mermaid\", format, mermaidText, options);\n}\n\n/**\n * Generate a Kroki SVG URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param options - Optional URL generator options\n * @returns The Kroki SVG URL\n *\n * @example\n * ```typescript\n * const svgUrl = toKrokiSvgUrl(workflowIR);\n * // => \"https://kroki.io/mermaid/svg/eNp...\"\n * ```\n */\nexport function toKrokiSvgUrl(\n  ir: WorkflowIR,\n  options: UrlGeneratorOptions = {}\n): string {\n  return toKrokiUrl(ir, \"svg\", options);\n}\n\n/**\n * Generate a Kroki PNG URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param options - Optional URL generator options\n * @returns The Kroki PNG URL\n *\n * @example\n * ```typescript\n * const pngUrl = toKrokiPngUrl(workflowIR);\n * // => \"https://kroki.io/mermaid/png/eNp...\"\n * ```\n */\nexport function toKrokiPngUrl(\n  ir: WorkflowIR,\n  options: UrlGeneratorOptions = {}\n): string {\n  return toKrokiUrl(ir, \"png\", options);\n}\n\n/**\n * URL Generator with configured base URL.\n */\nexport interface UrlGenerator {\n  /** Generate URL with specified format */\n  toUrl(ir: WorkflowIR, format: KrokiFormat): string;\n  /** Generate SVG URL */\n  toSvgUrl(ir: WorkflowIR): string;\n  /** Generate PNG URL */\n  toPngUrl(ir: WorkflowIR): string;\n  /** Generate PDF URL */\n  toPdfUrl(ir: WorkflowIR): string;\n  /** Get the configured base URL */\n  getBaseUrl(): string;\n}\n\n/**\n * Create a URL generator with a custom base URL.\n * Useful for self-hosted Kroki instances.\n *\n * @param options - URL generator options\n * @returns A URL generator instance\n *\n * @example\n * ```typescript\n * // Use self-hosted Kroki\n * const generator = createUrlGenerator({ baseUrl: 'https://my-kroki.internal' });\n * const url = generator.toSvgUrl(workflowIR);\n *\n * // Default public Kroki\n * const defaultGenerator = createUrlGenerator();\n * const publicUrl = defaultGenerator.toSvgUrl(workflowIR);\n * ```\n */\nexport function createUrlGenerator(options: UrlGeneratorOptions = {}): UrlGenerator {\n  const baseUrl = options.baseUrl ?? DEFAULT_KROKI_URL;\n\n  return {\n    toUrl(ir: WorkflowIR, format: KrokiFormat): string {\n      return toKrokiUrl(ir, format, { baseUrl });\n    },\n\n    toSvgUrl(ir: WorkflowIR): string {\n      return toKrokiUrl(ir, \"svg\", { baseUrl });\n    },\n\n    toPngUrl(ir: WorkflowIR): string {\n      return toKrokiUrl(ir, \"png\", { baseUrl });\n    },\n\n    toPdfUrl(ir: WorkflowIR): string {\n      return toKrokiUrl(ir, \"pdf\", { baseUrl });\n    },\n\n    getBaseUrl(): string {\n      return baseUrl;\n    },\n  };\n}\n","/**\n * Mermaid.ink URL Generation\n *\n * Generates shareable URLs for mermaid.ink diagram rendering service.\n * Alternative to Kroki with additional features like themes, background colors, and sizing.\n *\n * @see https://mermaid.ink/\n */\n\nimport type { WorkflowIR, RenderOptions, MermaidInkExportOptions } from \"../types\";\nimport { mermaidRenderer, defaultColorScheme } from \"../renderers\";\nimport { encodeForKroki } from \"./encoder\";\n\n/**\n * Supported mermaid.ink output formats.\n */\nexport type MermaidInkFormat = \"svg\" | \"img\" | \"pdf\";\n\n/**\n * Image type for /img endpoint.\n */\nexport type MermaidInkImageType = \"jpeg\" | \"png\" | \"webp\";\n\n/**\n * Mermaid.ink built-in themes.\n */\nexport type MermaidInkTheme = \"default\" | \"neutral\" | \"dark\" | \"forest\";\n\n/**\n * PDF paper sizes.\n */\nexport type MermaidInkPaperSize =\n  | \"letter\"\n  | \"legal\"\n  | \"tabloid\"\n  | \"ledger\"\n  | \"a0\"\n  | \"a1\"\n  | \"a2\"\n  | \"a3\"\n  | \"a4\"\n  | \"a5\"\n  | \"a6\";\n\n/**\n * Options for mermaid.ink URL generation.\n */\nexport interface MermaidInkOptions {\n  /** Base URL for mermaid.ink service (default: https://mermaid.ink) */\n  baseUrl?: string;\n\n  /**\n   * Background color.\n   * - Hex color without #: \"FF0000\" for red\n   * - Named color with ! prefix: \"!white\", \"!black\"\n   */\n  bgColor?: string;\n\n  /** Mermaid theme */\n  theme?: MermaidInkTheme;\n\n  /** Image width in pixels */\n  width?: number;\n\n  /** Image height in pixels */\n  height?: number;\n\n  /** Image scale (1-3). Only applies if width or height is set */\n  scale?: number;\n\n  /** Image type for /img endpoint (default: jpeg) */\n  imageType?: MermaidInkImageType;\n\n  // PDF-specific options\n\n  /** Fit PDF size to diagram size */\n  fit?: boolean;\n\n  /** Paper size for PDF (default: a4) */\n  paper?: MermaidInkPaperSize;\n\n  /** Landscape orientation for PDF */\n  landscape?: boolean;\n}\n\n/**\n * Default mermaid.ink base URL.\n */\nconst DEFAULT_MERMAID_INK_URL = \"https://mermaid.ink\";\n\n/**\n * Encode text for mermaid.ink URL.\n * Uses pako deflate compression + base64 encoding with \"pako:\" prefix.\n *\n * @param text - The Mermaid diagram text\n * @returns Encoded string with \"pako:\" prefix\n */\nexport function encodeForMermaidInk(text: string): string {\n  const encoded = encodeForKroki(text);\n  return `pako:${encoded}`;\n}\n\n/**\n * Normalize export options to internal MermaidInkOptions.\n * Maps MermaidInkExportOptions fields to MermaidInkOptions fields.\n */\nfunction normalizeOptions(\n  options: MermaidInkOptions | MermaidInkExportOptions\n): MermaidInkOptions {\n  // If it's MermaidInkExportOptions (has \"provider\" field), normalize it\n  if (\"provider\" in options) {\n    const exportOpts = options as MermaidInkExportOptions;\n    return {\n      theme: exportOpts.mermaidTheme,\n      bgColor: exportOpts.background,\n      scale: exportOpts.scale,\n      fit: exportOpts.fit,\n      width: exportOpts.width,\n      height: exportOpts.height,\n      paper: exportOpts.paper as MermaidInkPaperSize | undefined,\n      // MermaidInkExportOptions uses \"png\" format via toExportUrl, set imageType\n      imageType: \"png\",\n    };\n  }\n  // Already MermaidInkOptions\n  return options;\n}\n\n/**\n * Build query string from options.\n */\nfunction buildQueryString(\n  format: MermaidInkFormat,\n  options: MermaidInkOptions\n): string {\n  const params: string[] = [];\n\n  // Common options\n  if (options.bgColor) {\n    params.push(`bgColor=${encodeURIComponent(options.bgColor)}`);\n  }\n  if (options.theme) {\n    params.push(`theme=${options.theme}`);\n  }\n  if (options.width !== undefined) {\n    params.push(`width=${options.width}`);\n  }\n  if (options.height !== undefined) {\n    params.push(`height=${options.height}`);\n  }\n  if (options.scale !== undefined && (options.width !== undefined || options.height !== undefined)) {\n    params.push(`scale=${options.scale}`);\n  }\n\n  // Image-specific options\n  if (format === \"img\" && options.imageType && options.imageType !== \"jpeg\") {\n    params.push(`type=${options.imageType}`);\n  }\n\n  // PDF-specific options\n  if (format === \"pdf\") {\n    if (options.fit) {\n      params.push(\"fit\");\n    }\n    if (options.paper && !options.fit) {\n      params.push(`paper=${options.paper}`);\n    }\n    if (options.landscape && !options.fit) {\n      params.push(\"landscape\");\n    }\n  }\n\n  return params.length > 0 ? `?${params.join(\"&\")}` : \"\";\n}\n\n/**\n * Build a mermaid.ink URL for the given Mermaid diagram text.\n *\n * @param format - Output format (svg, img, pdf)\n * @param text - The Mermaid diagram text\n * @param options - Optional mermaid.ink options (MermaidInkOptions or MermaidInkExportOptions)\n * @returns The mermaid.ink URL\n *\n * @example\n * ```typescript\n * const url = buildMermaidInkUrl('svg', 'flowchart TD\\n  A-->B');\n * // => \"https://mermaid.ink/svg/pako:eNpLzs8tyc9NTgQADsMDmA\"\n *\n * const darkUrl = buildMermaidInkUrl('svg', 'flowchart TD\\n  A-->B', {\n *   theme: 'dark',\n *   bgColor: '1b1b1f'\n * });\n * // => \"https://mermaid.ink/svg/pako:eNp...?theme=dark&bgColor=1b1b1f\"\n *\n * // With MermaidInkExportOptions\n * const exportUrl = buildMermaidInkUrl('svg', 'flowchart TD\\n  A-->B', {\n *   provider: 'mermaid-ink',\n *   mermaidTheme: 'dark',\n *   background: '1b1b1f'\n * });\n * ```\n */\nexport function buildMermaidInkUrl(\n  format: MermaidInkFormat,\n  text: string,\n  options: MermaidInkOptions | MermaidInkExportOptions = {}\n): string {\n  const normalized = normalizeOptions(options);\n  const baseUrl = normalized.baseUrl ?? DEFAULT_MERMAID_INK_URL;\n  const encoded = encodeForMermaidInk(text);\n  const queryString = buildQueryString(format, normalized);\n  return `${baseUrl}/${format}/${encoded}${queryString}`;\n}\n\n/**\n * Generate a mermaid.ink URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param format - Output format (default: 'svg')\n * @param options - Optional mermaid.ink options\n * @returns The mermaid.ink URL\n *\n * @example\n * ```typescript\n * const url = toMermaidInkUrl(workflowIR, 'svg');\n * // Share this URL - image renders when viewed\n *\n * const darkUrl = toMermaidInkUrl(workflowIR, 'svg', { theme: 'dark' });\n * ```\n */\nexport function toMermaidInkUrl(\n  ir: WorkflowIR,\n  format: MermaidInkFormat = \"svg\",\n  options: MermaidInkOptions = {}\n): string {\n  const renderer = mermaidRenderer();\n  const renderOptions: RenderOptions = {\n    showTimings: true,\n    showKeys: false,\n    terminalWidth: 80,\n    colors: defaultColorScheme,\n  };\n\n  const mermaidText = renderer.render(ir, renderOptions);\n  return buildMermaidInkUrl(format, mermaidText, options);\n}\n\n/**\n * Generate a mermaid.ink SVG URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param options - Optional mermaid.ink options\n * @returns The mermaid.ink SVG URL\n *\n * @example\n * ```typescript\n * const svgUrl = toMermaidInkSvgUrl(workflowIR);\n * // => \"https://mermaid.ink/svg/pako:eNp...\"\n *\n * const darkSvg = toMermaidInkSvgUrl(workflowIR, { theme: 'dark' });\n * ```\n */\nexport function toMermaidInkSvgUrl(\n  ir: WorkflowIR,\n  options: MermaidInkOptions = {}\n): string {\n  return toMermaidInkUrl(ir, \"svg\", options);\n}\n\n/**\n * Generate a mermaid.ink PNG URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param options - Optional mermaid.ink options\n * @returns The mermaid.ink PNG URL\n *\n * @example\n * ```typescript\n * const pngUrl = toMermaidInkPngUrl(workflowIR);\n * // => \"https://mermaid.ink/img/pako:eNp...?type=png\"\n *\n * const scaledPng = toMermaidInkPngUrl(workflowIR, { width: 800, scale: 2 });\n * ```\n */\nexport function toMermaidInkPngUrl(\n  ir: WorkflowIR,\n  options: MermaidInkOptions = {}\n): string {\n  return toMermaidInkUrl(ir, \"img\", { ...options, imageType: \"png\" });\n}\n\n/**\n * Generate a mermaid.ink JPEG URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param options - Optional mermaid.ink options\n * @returns The mermaid.ink JPEG URL\n */\nexport function toMermaidInkJpegUrl(\n  ir: WorkflowIR,\n  options: MermaidInkOptions = {}\n): string {\n  return toMermaidInkUrl(ir, \"img\", { ...options, imageType: \"jpeg\" });\n}\n\n/**\n * Generate a mermaid.ink WebP URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param options - Optional mermaid.ink options\n * @returns The mermaid.ink WebP URL\n */\nexport function toMermaidInkWebpUrl(\n  ir: WorkflowIR,\n  options: MermaidInkOptions = {}\n): string {\n  return toMermaidInkUrl(ir, \"img\", { ...options, imageType: \"webp\" });\n}\n\n/**\n * Generate a mermaid.ink PDF URL from workflow IR.\n *\n * @param ir - Workflow intermediate representation\n * @param options - Optional mermaid.ink options (fit, paper, landscape)\n * @returns The mermaid.ink PDF URL\n *\n * @example\n * ```typescript\n * // Fit PDF to diagram size\n * const fitPdf = toMermaidInkPdfUrl(workflowIR, { fit: true });\n *\n * // A3 landscape\n * const a3Pdf = toMermaidInkPdfUrl(workflowIR, { paper: 'a3', landscape: true });\n * ```\n */\nexport function toMermaidInkPdfUrl(\n  ir: WorkflowIR,\n  options: MermaidInkOptions = {}\n): string {\n  return toMermaidInkUrl(ir, \"pdf\", options);\n}\n\n/**\n * Mermaid.ink URL Generator interface.\n */\nexport interface MermaidInkGenerator {\n  /** Generate URL with specified format */\n  toUrl(ir: WorkflowIR, format: MermaidInkFormat): string;\n  /** Generate SVG URL */\n  toSvgUrl(ir: WorkflowIR): string;\n  /** Generate PNG URL */\n  toPngUrl(ir: WorkflowIR): string;\n  /** Generate JPEG URL */\n  toJpegUrl(ir: WorkflowIR): string;\n  /** Generate WebP URL */\n  toWebpUrl(ir: WorkflowIR): string;\n  /** Generate PDF URL */\n  toPdfUrl(ir: WorkflowIR): string;\n  /** Get the configured base URL */\n  getBaseUrl(): string;\n  /** Get the configured options */\n  getOptions(): MermaidInkOptions;\n}\n\n/**\n * Create a mermaid.ink URL generator with default options.\n * Useful for consistent theming across all generated URLs.\n *\n * @param options - Default mermaid.ink options applied to all URLs\n * @returns A mermaid.ink URL generator instance\n *\n * @example\n * ```typescript\n * // Create generator with dark theme defaults\n * const generator = createMermaidInkGenerator({\n *   theme: 'dark',\n *   bgColor: '1b1b1f',\n * });\n *\n * // All URLs will use dark theme\n * const svgUrl = generator.toSvgUrl(workflowIR);\n * const pngUrl = generator.toPngUrl(workflowIR);\n *\n * // Self-hosted mermaid.ink\n * const privateGenerator = createMermaidInkGenerator({\n *   baseUrl: 'https://mermaid.internal.company.com',\n * });\n * ```\n */\nexport function createMermaidInkGenerator(\n  options: MermaidInkOptions = {}\n): MermaidInkGenerator {\n  const baseUrl = options.baseUrl ?? DEFAULT_MERMAID_INK_URL;\n\n  return {\n    toUrl(ir: WorkflowIR, format: MermaidInkFormat): string {\n      return toMermaidInkUrl(ir, format, options);\n    },\n\n    toSvgUrl(ir: WorkflowIR): string {\n      return toMermaidInkUrl(ir, \"svg\", options);\n    },\n\n    toPngUrl(ir: WorkflowIR): string {\n      return toMermaidInkUrl(ir, \"img\", { ...options, imageType: \"png\" });\n    },\n\n    toJpegUrl(ir: WorkflowIR): string {\n      return toMermaidInkUrl(ir, \"img\", { ...options, imageType: \"jpeg\" });\n    },\n\n    toWebpUrl(ir: WorkflowIR): string {\n      return toMermaidInkUrl(ir, \"img\", { ...options, imageType: \"webp\" });\n    },\n\n    toPdfUrl(ir: WorkflowIR): string {\n      return toMermaidInkUrl(ir, \"pdf\", options);\n    },\n\n    getBaseUrl(): string {\n      return baseUrl;\n    },\n\n    getOptions(): MermaidInkOptions {\n      return { ...options };\n    },\n  };\n}\n","/**\n * Notifier Context Factory\n *\n * Creates shared context for notifier adapters.\n * Single source of truth for diagram URL generation and utilities.\n */\n\nimport type { WorkflowIR } from \"../types\";\nimport type { ProviderOptions } from \"./types\";\nimport type { NotifierContext } from \"./adapter\";\nimport { countSteps } from \"./types\";\nimport { toKrokiSvgUrl } from \"../kroki/url\";\nimport { toMermaidInkSvgUrl } from \"../kroki/mermaid-ink\";\n\n/**\n * Create shared context for notifier adapters.\n * Single source of truth for diagram URL generation.\n *\n * @param diagramProvider - Provider configuration\n * @returns NotifierContext with shared utilities\n */\nexport function createNotifierContext(\n  diagramProvider: ProviderOptions\n): NotifierContext {\n  return {\n    getDiagramUrl(ir: WorkflowIR): string {\n      if (diagramProvider.provider === \"mermaid-ink\") {\n        const { provider: _, ...options } = diagramProvider;\n        return toMermaidInkSvgUrl(ir, options);\n      }\n      // Kroki provider\n      return toKrokiSvgUrl(ir, { baseUrl: diagramProvider.baseUrl });\n    },\n\n    countSteps(ir: WorkflowIR): number {\n      return countSteps(ir);\n    },\n\n    formatDuration(ir: WorkflowIR): string {\n      if (ir.root.endTs !== undefined && ir.root.startTs !== undefined) {\n        return `${ir.root.endTs - ir.root.startTs}ms`;\n      }\n      if (ir.root.durationMs !== undefined) {\n        return `${ir.root.durationMs}ms`;\n      }\n      return \"In progress\";\n    },\n  };\n}\n","/**\n * Live Session Base\n *\n * Base class for live workflow update sessions with debouncing.\n * Handles timing logic for batching rapid updates while ensuring\n * periodic updates during constant churn.\n */\n\nimport { ok, err, type Result } from \"awaitly\";\nimport type { WorkflowEvent } from \"awaitly\";\nimport type {\n  WorkflowIR,\n  ScopeStartEvent,\n  ScopeEndEvent,\n  DecisionStartEvent,\n  DecisionBranchEvent,\n  DecisionEndEvent,\n} from \"../types\";\nimport type { LiveSession, LiveSessionOptions, WorkflowStatus } from \"../notifiers/types\";\nimport { createIRBuilder } from \"../ir-builder\";\n\n/**\n * Extended event types that the live session can handle.\n * Includes both core WorkflowEvent and visualizer-specific events.\n */\ntype ExtendedEvent =\n  | WorkflowEvent<unknown>\n  | ScopeStartEvent\n  | ScopeEndEvent\n  | DecisionStartEvent\n  | DecisionBranchEvent\n  | DecisionEndEvent;\n\n/**\n * Error types for live session flush operations.\n */\nexport type FlushError = \"CALLBACK_ERROR\";\n\n/**\n * Default debounce interval (ms).\n */\nconst DEFAULT_DEBOUNCE_MS = 500;\n\n/**\n * Default max wait between posts (ms).\n */\nconst DEFAULT_MAX_WAIT_MS = 5000;\n\n/**\n * Callbacks for concrete notifier implementations.\n */\nexport interface LiveSessionCallbacks {\n  /** Called to post a new message, returns message ID */\n  postNew(ir: WorkflowIR, title: string): Promise<string | undefined>;\n\n  /** Called to update an existing message */\n  updateExisting(\n    messageId: string,\n    ir: WorkflowIR,\n    title: string\n  ): Promise<void>;\n\n  /** Called to finalize the message with status */\n  finalize(\n    messageId: string | undefined,\n    ir: WorkflowIR,\n    title: string,\n    status: WorkflowStatus\n  ): Promise<void>;\n\n  /** Called to cancel/delete the message (optional) */\n  cancel?(messageId: string): Promise<void>;\n}\n\n/**\n * Create a live session with debouncing.\n *\n * @param options - Session options\n * @param callbacks - Platform-specific callbacks\n * @returns A LiveSession instance\n */\nexport function createLiveSession(\n  options: LiveSessionOptions,\n  callbacks: LiveSessionCallbacks\n): LiveSession {\n  const {\n    title,\n    debounceMs = DEFAULT_DEBOUNCE_MS,\n    maxWaitMs = DEFAULT_MAX_WAIT_MS,\n  } = options;\n\n  // State\n  let messageId: string | undefined;\n  let active = true;\n  let pendingIR: WorkflowIR | undefined;\n  /** Last IR we had (pending or flushed) so finalize() can use it after a flush */\n  let lastIR: WorkflowIR | undefined;\n  let debounceTimer: ReturnType<typeof setTimeout> | undefined;\n  let maxWaitTimer: ReturnType<typeof setTimeout> | undefined;\n  let lastPostTime: number | undefined;\n  let firstUpdateTime: number | undefined;\n  /** Track last flush result for error reporting */\n  let lastFlushResult: Result<void, FlushError> = ok(undefined);\n\n  // IR builder for accumulating events\n  const irBuilder = createIRBuilder({ detectParallel: true });\n\n  function clearTimers(): void {\n    if (debounceTimer) {\n      clearTimeout(debounceTimer);\n      debounceTimer = undefined;\n    }\n    if (maxWaitTimer) {\n      clearTimeout(maxWaitTimer);\n      maxWaitTimer = undefined;\n    }\n  }\n\n  async function flush(): Promise<Result<void, FlushError>> {\n    if (!pendingIR || !active) return ok(undefined);\n\n    const irToPost = pendingIR;\n    lastIR = irToPost;\n    pendingIR = undefined;\n    clearTimers();\n\n    try {\n      if (!messageId) {\n        // First post - create new message\n        messageId = await callbacks.postNew(irToPost, title);\n      } else {\n        // Update existing message\n        await callbacks.updateExisting(messageId, irToPost, title);\n      }\n      lastPostTime = Date.now();\n      lastFlushResult = ok(undefined);\n      return lastFlushResult;\n    } catch {\n      // Capture errors instead of silently swallowing\n      lastFlushResult = err(\"CALLBACK_ERROR\");\n      return lastFlushResult;\n    }\n  }\n\n  function scheduleFlush(): void {\n    // Clear existing debounce timer\n    if (debounceTimer) {\n      clearTimeout(debounceTimer);\n    }\n\n    // Set new debounce timer\n    debounceTimer = setTimeout(() => {\n      void flush();\n    }, debounceMs);\n\n    // Set max wait timer if not already set\n    // Use lastPostTime if we've posted, otherwise use firstUpdateTime\n    const referenceTime = lastPostTime ?? firstUpdateTime;\n    if (!maxWaitTimer && referenceTime !== undefined) {\n      const timeSinceReference = Date.now() - referenceTime;\n      const remainingMaxWait = Math.max(0, maxWaitMs - timeSinceReference);\n\n      maxWaitTimer = setTimeout(() => {\n        maxWaitTimer = undefined;\n        void flush();\n      }, remainingMaxWait);\n    }\n  }\n\n  function update(eventOrIr: ExtendedEvent | WorkflowIR): void {\n    if (!active) return;\n\n    // Track first update time for max-wait before first post\n    if (firstUpdateTime === undefined) {\n      firstUpdateTime = Date.now();\n    }\n\n    // Handle event or IR\n    if (\"root\" in eventOrIr) {\n      // It's an IR\n      pendingIR = eventOrIr;\n      lastIR = eventOrIr;\n    } else {\n      // It's an event - route to correct handler based on type\n      const event = eventOrIr;\n      if (event.type === \"scope_start\" || event.type === \"scope_end\") {\n        irBuilder.handleScopeEvent(event as ScopeStartEvent | ScopeEndEvent);\n      } else if (\n        event.type === \"decision_start\" ||\n        event.type === \"decision_branch\" ||\n        event.type === \"decision_end\"\n      ) {\n        irBuilder.handleDecisionEvent(event as DecisionStartEvent | DecisionBranchEvent | DecisionEndEvent);\n      } else {\n        irBuilder.handleEvent(event as WorkflowEvent<unknown>);\n      }\n      pendingIR = irBuilder.getIR();\n      lastIR = pendingIR;\n    }\n\n    scheduleFlush();\n  }\n\n  async function finalize(status: WorkflowStatus = \"completed\"): Promise<void> {\n    if (!active) return;\n\n    active = false;\n    clearTimers();\n\n    // Get final IR (pending, or last flushed/direct, or builder snapshot)\n    const finalIR = pendingIR ?? lastIR ?? irBuilder.getIR();\n\n    await callbacks.finalize(messageId, finalIR, title, status);\n  }\n\n  function cancel(): void {\n    if (!active) return;\n\n    active = false;\n    clearTimers();\n\n    if (messageId && callbacks.cancel) {\n      void callbacks.cancel(messageId);\n    }\n  }\n\n  function getSessionId(): string | undefined {\n    return messageId;\n  }\n\n  function isActive(): boolean {\n    return active;\n  }\n\n  return {\n    update,\n    finalize,\n    cancel,\n    getSessionId,\n    isActive,\n  };\n}\n","/**\n * Parallel Detection - Heuristic detection of parallel execution from timing.\n *\n * When steps overlap in time (one starts before another ends), they are\n * likely running in parallel. This module detects such patterns and\n * groups overlapping steps into ParallelNode structures.\n */\n\nimport type { FlowNode, ParallelNode, StepNode } from \"./types\";\n\n/**\n * Options for parallel detection.\n */\nexport interface ParallelDetectorOptions {\n  /**\n   * Minimum overlap in milliseconds to consider steps parallel.\n   * Default: 0 (any overlap counts)\n   */\n  minOverlapMs?: number;\n\n  /**\n   * Maximum gap in milliseconds to still consider steps as part of same parallel group.\n   * Default: 5 (steps starting within 5ms are grouped)\n   */\n  maxGapMs?: number;\n}\n\n/**\n * Step timing information for overlap detection.\n */\ninterface StepTiming {\n  node: StepNode;\n  startTs: number;\n  endTs: number;\n}\n\n/**\n * Check if nodes contain real scope nodes (from scope_start/scope_end events).\n * When real scope nodes exist, heuristic detection should be skipped to avoid\n * duplicating or conflicting with the explicit structure.\n */\nfunction hasRealScopeNodes(nodes: FlowNode[]): boolean {\n  for (const node of nodes) {\n    // Real scope nodes are parallel/race/sequence that came from scope events\n    // (not from heuristic detection, which uses ids starting with \"detected_\")\n    if (\n      (node.type === \"parallel\" || node.type === \"race\" || node.type === \"sequence\") &&\n      !node.id.startsWith(\"detected_\")\n    ) {\n      return true;\n    }\n    // Also check for decision nodes if present\n    if (node.type === \"decision\") {\n      return true;\n    }\n  }\n  return false;\n}\n\n/**\n * Group overlapping steps into parallel nodes.\n *\n * Algorithm:\n * 1. Sort steps by start time\n * 2. For each step, check if it overlaps with existing parallel groups\n * 3. If it overlaps, add to group; otherwise start new sequence\n * 4. Merge overlapping groups when step bridges them\n *\n * Note: If real scope nodes (from scope_start/scope_end) are present,\n * heuristic detection is skipped to avoid conflicts.\n */\nexport function detectParallelGroups(\n  nodes: FlowNode[],\n  options: ParallelDetectorOptions = {}\n): FlowNode[] {\n  // If real scope nodes exist, skip heuristic detection\n  // The explicit scope events provide accurate structure\n  if (hasRealScopeNodes(nodes)) {\n    return nodes;\n  }\n\n  const { minOverlapMs = 0, maxGapMs = 5 } = options;\n\n  // Extract step nodes with timing info, preserving indices for position restoration\n  const stepsWithTiming: (StepTiming & { originalIndex: number })[] = [];\n  const nonStepNodes: { node: FlowNode; originalIndex: number }[] = [];\n\n  for (let i = 0; i < nodes.length; i++) {\n    const node = nodes[i];\n    if (node.type === \"step\" && node.startTs !== undefined) {\n      stepsWithTiming.push({\n        node,\n        startTs: node.startTs,\n        endTs: node.endTs ?? node.startTs + (node.durationMs ?? 0),\n        originalIndex: i,\n      });\n    } else {\n      // Keep non-step nodes with their original position\n      nonStepNodes.push({ node, originalIndex: i });\n    }\n  }\n\n  if (stepsWithTiming.length <= 1) {\n    return nodes; // Nothing to group\n  }\n\n  // Sort by start time\n  stepsWithTiming.sort((a, b) => a.startTs - b.startTs);\n\n  // Group overlapping steps\n  type StepTimingWithIndex = StepTiming & { originalIndex: number };\n  const groups: StepTimingWithIndex[][] = [];\n  let currentGroup: StepTimingWithIndex[] = [stepsWithTiming[0]];\n\n  for (let i = 1; i < stepsWithTiming.length; i++) {\n    const step = stepsWithTiming[i];\n    const groupStart = Math.min(...currentGroup.map((s) => s.startTs));\n    const groupEnd = Math.max(...currentGroup.map((s) => s.endTs));\n\n    // Two ways steps can be parallel:\n    // 1. They started together (within maxGapMs) - handles timing jitter\n    // 2. They genuinely overlap (step starts before group ends)\n    const startedTogether = step.startTs <= groupStart + maxGapMs;\n    const hasTrueOverlap = step.startTs < groupEnd;\n\n    if (!startedTogether && !hasTrueOverlap) {\n      // Sequential: step started after group ended AND not with the group\n      groups.push(currentGroup);\n      currentGroup = [step];\n      continue;\n    }\n\n    // Check minOverlapMs threshold for overlap duration\n    // For steps that started together, overlap is measured from step start to group end\n    // For steps with true overlap, it's from step start to min(step end, group end)\n    const overlapDuration = hasTrueOverlap\n      ? Math.min(step.endTs, groupEnd) - step.startTs\n      : 0;\n\n    // Started together bypasses minOverlapMs (they're parallel by definition)\n    // True overlap must meet the minOverlapMs threshold\n    if (startedTogether || overlapDuration >= minOverlapMs) {\n      currentGroup.push(step);\n    } else {\n      // Overlap too small - treat as sequential\n      groups.push(currentGroup);\n      currentGroup = [step];\n    }\n  }\n  groups.push(currentGroup);\n\n  // Convert groups to nodes with position tracking\n  const groupedNodes: { node: FlowNode; position: number }[] = [];\n\n  for (const group of groups) {\n    // Use the minimum original index as the position for the group\n    const position = Math.min(...group.map((s) => s.originalIndex));\n\n    if (group.length === 1) {\n      // Single step - no parallel grouping needed\n      groupedNodes.push({ node: group[0].node, position });\n    } else {\n      // Multiple overlapping steps - create parallel node\n      const children = group.map((s) => s.node);\n      const startTs = Math.min(...group.map((s) => s.startTs));\n      const endTs = Math.max(...group.map((s) => s.endTs));\n\n      const parallelNode: ParallelNode = {\n        type: \"parallel\",\n        id: `detected_parallel_${startTs}`,\n        name: `${children.length} parallel steps`,\n        state: deriveGroupState(children),\n        mode: \"all\",\n        children,\n        startTs,\n        endTs,\n        durationMs: endTs - startTs,\n      };\n\n      groupedNodes.push({ node: parallelNode, position });\n    }\n  }\n\n  // Add non-step nodes with their original positions\n  for (const { node, originalIndex } of nonStepNodes) {\n    groupedNodes.push({ node, position: originalIndex });\n  }\n\n  // Sort by original position to preserve ordering\n  groupedNodes.sort((a, b) => a.position - b.position);\n\n  return groupedNodes.map((g) => g.node);\n}\n\n/**\n * Derive the state of a group from its children.\n */\nfunction deriveGroupState(\n  children: FlowNode[]\n): \"pending\" | \"running\" | \"success\" | \"error\" | \"aborted\" | \"cached\" {\n  const hasError = children.some((c) => c.state === \"error\");\n  if (hasError) return \"error\";\n\n  const hasRunning = children.some((c) => c.state === \"running\");\n  if (hasRunning) return \"running\";\n\n  const hasPending = children.some((c) => c.state === \"pending\");\n  if (hasPending) return \"pending\";\n\n  const allSuccess = children.every(\n    (c) => c.state === \"success\" || c.state === \"cached\"\n  );\n  if (allSuccess) return \"success\";\n\n  return \"success\";\n}\n\n/**\n * Create a parallel detector that processes nodes.\n */\nexport function createParallelDetector(options: ParallelDetectorOptions = {}) {\n  return {\n    /**\n     * Process nodes and group overlapping ones into parallel nodes.\n     */\n    detect: (nodes: FlowNode[]) => detectParallelGroups(nodes, options),\n  };\n}\n","/**\n * IR Builder - Converts workflow events to Intermediate Representation.\n *\n * The builder maintains state as events arrive, constructing a tree\n * representation of the workflow execution that can be rendered.\n */\n\nimport type { WorkflowEvent } from \"awaitly\";\nimport type {\n  FlowNode,\n  ScopeEndEvent,\n  ScopeStartEvent,\n  ScopeType,\n  StepNode,\n  StepState,\n  WorkflowIR,\n  WorkflowNode,\n  ParallelNode,\n  RaceNode,\n  DecisionNode,\n  DecisionStartEvent,\n  DecisionBranchEvent,\n  DecisionEndEvent,\n  DecisionBranch,\n  IRSnapshot,\n  ActiveStepSnapshot,\n  WorkflowHooks,\n  HookExecution,\n  StreamNode,\n} from \"../types\";\nimport { generateId } from \"../utils/timing\";\nimport { detectParallelGroups, type ParallelDetectorOptions } from \"../parallel-detector\";\n\n// =============================================================================\n// Builder Options\n// =============================================================================\n\n/**\n * Options for the IR builder.\n */\nexport interface IRBuilderOptions {\n  /**\n   * Enable heuristic parallel detection based on timing.\n   * When true, overlapping steps are grouped into ParallelNodes.\n   * Default: true\n   */\n  detectParallel?: boolean;\n\n  /**\n   * Options for parallel detection.\n   */\n  parallelDetection?: ParallelDetectorOptions;\n\n  /**\n   * Enable snapshot recording for time-travel debugging.\n   * When true, the builder captures IR state after each event.\n   * Default: false\n   */\n  enableSnapshots?: boolean;\n\n  /**\n   * Maximum number of snapshots to keep (ring buffer behavior).\n   * When exceeded, oldest snapshots are discarded.\n   * Default: 1000\n   */\n  maxSnapshots?: number;\n}\n\n// =============================================================================\n// Builder State\n// =============================================================================\n\ninterface ActiveStep {\n  id: string;\n  name?: string;\n  key?: string;\n  startTs: number;\n  retryCount: number;\n  timedOut: boolean;\n  timeoutMs?: number;\n  metadata?: import(\"awaitly\").StepMetadata;\n}\n\ninterface ActiveScope {\n  id: string;\n  name?: string;\n  type: ScopeType;\n  startTs: number;\n  children: FlowNode[];\n}\n\ninterface ActiveDecision {\n  id: string;\n  name?: string;\n  condition?: string;\n  decisionValue?: unknown;\n  startTs: number;\n  branches: Map<string, DecisionBranch>;\n  branchTaken?: string | boolean;\n  /** Nodes that arrived before any branch was created */\n  pendingChildren: FlowNode[];\n}\n\ninterface ActiveStream {\n  id: string;\n  namespace: string;\n  startTs: number;\n  writeCount: number;\n  readCount: number;\n  streamState: \"active\" | \"closed\" | \"error\";\n  backpressureOccurred: boolean;\n  finalPosition: number;\n}\n\n// =============================================================================\n// IR Builder\n// =============================================================================\n\n/**\n * Creates an IR builder that processes workflow events.\n */\nexport function createIRBuilder(options: IRBuilderOptions = {}) {\n  const {\n    detectParallel = true,\n    parallelDetection,\n    enableSnapshots: initialEnableSnapshots = false,\n    maxSnapshots = 1000,\n  } = options;\n\n  // Mutable so time-travel stopRecording() can pause snapshot accumulation\n  let enableSnapshots = initialEnableSnapshots;\n\n  // Current workflow state\n  // Generate a stable default ID for use before workflow_start event\n  const defaultWorkflowId = generateId();\n  let workflowId: string | undefined;\n  let workflowStartTs: number | undefined;\n  let workflowEndTs: number | undefined;\n  let workflowState: StepState = \"pending\";\n  let workflowError: unknown;\n  let workflowDurationMs: number | undefined;\n\n  // Active steps (currently running)\n  const activeSteps = new Map<string, ActiveStep>();\n\n  // Active scopes (parallel/race blocks)\n  const scopeStack: ActiveScope[] = [];\n\n  // Active decisions (conditional branches)\n  const decisionStack: ActiveDecision[] = [];\n\n  // Active streams (streaming operations)\n  const activeStreams = new Map<string, ActiveStream>();\n\n  // Completed nodes at the current scope level\n  let currentNodes: FlowNode[] = [];\n\n  // Metadata\n  let createdAt = Date.now();\n  let lastUpdatedAt = createdAt;\n\n  // Hook executions\n  let hookState: WorkflowHooks = {\n    onAfterStep: new Map(),\n  };\n  /** WorkflowId that the current pre-start hooks (shouldRun, onBeforeStart) belong to */\n  let preStartHookWorkflowId: string | undefined;\n\n  // Snapshot state for time-travel debugging\n  const snapshots: IRSnapshot[] = [];\n  let eventIndex = 0;\n\n  /**\n   * Get the step ID from an event.\n   * With the current awaitly API (ID-first for all step types), stepId is always set.\n   * Falls back to stepKey, then name, then a generated ID for backwards compatibility.\n   */\n  function getStepId(event: { stepId?: string; stepKey?: string; name?: string }): string {\n    return event.stepId ?? event.stepKey ?? event.name ?? generateId();\n  }\n\n  /**\n   * Add a completed node to the current scope or decision branch.\n   * Scope stack takes priority over decision stack because scopes are\n   * the innermost container - when a scope ends, its node will be added\n   * to the decision branch via this same function.\n   */\n  function addNode(node: FlowNode): void {\n    // If we're in a scope, add to the innermost scope\n    // (scope takes priority - when scope ends, the scope node will be added to decision)\n    if (scopeStack.length > 0) {\n      scopeStack[scopeStack.length - 1].children.push(node);\n      lastUpdatedAt = Date.now();\n      return;\n    }\n\n    // If we're in a decision (but not in a scope), add to the taken branch\n    if (decisionStack.length > 0) {\n      const decision = decisionStack[decisionStack.length - 1];\n      // Find the taken branch\n      for (const branch of decision.branches.values()) {\n        if (branch.taken) {\n          branch.children.push(node);\n          lastUpdatedAt = Date.now();\n          return;\n        }\n      }\n      // No branch is taken yet (or first branch was taken: false) - buffer for later\n      decision.pendingChildren.push(node);\n      lastUpdatedAt = Date.now();\n      return;\n    }\n\n    // Add to the root level\n    currentNodes.push(node);\n    lastUpdatedAt = Date.now();\n  }\n\n  /**\n   * Capture a snapshot of the current IR state (for time-travel debugging).\n   * Called after each event is processed.\n   */\n  function captureSnapshot(event: WorkflowEvent<unknown>): void {\n    if (!enableSnapshots) return;\n\n    // Deep clone the current IR state\n    const ir = getIR();\n\n    // Clone active steps for debugging\n    const activeStepsCopy = new Map<string, ActiveStepSnapshot>();\n    for (const [id, step] of activeSteps) {\n      activeStepsCopy.set(id, {\n        id: step.id,\n        name: step.name,\n        key: step.key,\n        startTs: step.startTs,\n        retryCount: step.retryCount,\n        timedOut: step.timedOut,\n        timeoutMs: step.timeoutMs,\n      });\n    }\n\n    const snapshot: IRSnapshot = {\n      id: `snapshot_${eventIndex}`,\n      eventIndex,\n      event: structuredClone(event),\n      ir: structuredClone(ir),\n      timestamp: Date.now(),\n      activeSteps: activeStepsCopy,\n    };\n\n    snapshots.push(snapshot);\n\n    // Ring buffer: remove oldest if we exceed max\n    if (snapshots.length > maxSnapshots) {\n      snapshots.shift();\n    }\n\n    eventIndex++;\n  }\n\n  /**\n   * Handle a workflow event and update the IR.\n   */\n  function handleEvent(event: WorkflowEvent<unknown>): void {\n    switch (event.type) {\n      case \"workflow_start\": {\n        // New run: clear previous nodes and end-state so visualizations don't accumulate\n        currentNodes = [];\n        workflowEndTs = undefined;\n        workflowDurationMs = undefined;\n        workflowError = undefined;\n        activeSteps.clear();\n        scopeStack.length = 0;\n        decisionStack.length = 0;\n        activeStreams.clear();\n\n        workflowId = event.workflowId;\n        workflowStartTs = event.ts;\n        workflowState = \"running\";\n        createdAt = Date.now();\n        lastUpdatedAt = createdAt;\n        // Clear pre-start hooks from a previous run so a new run without hook events doesn't inherit them\n        if (preStartHookWorkflowId !== undefined && preStartHookWorkflowId !== event.workflowId) {\n          hookState.shouldRun = undefined;\n          hookState.onBeforeStart = undefined;\n        }\n        preStartHookWorkflowId = event.workflowId;\n        hookState.onAfterStep = new Map();\n        break;\n      }\n\n      case \"workflow_success\":\n        workflowState = \"success\";\n        workflowEndTs = event.ts;\n        workflowDurationMs = event.durationMs;\n        lastUpdatedAt = Date.now();\n        break;\n\n      case \"workflow_error\":\n        workflowState = \"error\";\n        workflowEndTs = event.ts;\n        workflowError = event.error;\n        workflowDurationMs = event.durationMs;\n        lastUpdatedAt = Date.now();\n        break;\n\n      case \"workflow_cancelled\":\n        workflowState = \"aborted\";\n        workflowEndTs = event.ts;\n        workflowDurationMs = event.durationMs;\n        lastUpdatedAt = Date.now();\n        break;\n\n      case \"step_start\": {\n        const id = getStepId(event);\n        activeSteps.set(id, {\n          id,\n          name: event.name,\n          key: event.stepKey,\n          startTs: event.ts,\n          retryCount: 0,\n          timedOut: false,\n          metadata: (event as any).metadata,\n        });\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"step_success\": {\n        const id = getStepId(event);\n        const active = activeSteps.get(id);\n        if (active) {\n          const node: StepNode = {\n            type: \"step\",\n            id: active.id,\n            name: active.name,\n            key: active.key,\n            state: \"success\",\n            startTs: active.startTs,\n            endTs: event.ts,\n            durationMs: event.durationMs,\n            ...(active.retryCount > 0 && { retryCount: active.retryCount }),\n            ...(active.timedOut && { timedOut: true, timeoutMs: active.timeoutMs }),\n            ...(active.metadata && { metadata: active.metadata }),\n          };\n          addNode(node);\n          activeSteps.delete(id);\n        }\n        break;\n      }\n\n      case \"step_error\": {\n        const id = getStepId(event);\n        const active = activeSteps.get(id);\n        if (active) {\n          const node: StepNode = {\n            type: \"step\",\n            id: active.id,\n            name: active.name,\n            key: active.key,\n            state: \"error\",\n            startTs: active.startTs,\n            endTs: event.ts,\n            durationMs: event.durationMs,\n            error: event.error,\n            ...(active.retryCount > 0 && { retryCount: active.retryCount }),\n            ...(active.timedOut && { timedOut: true, timeoutMs: active.timeoutMs }),\n            ...(active.metadata && { metadata: active.metadata }),\n            ...((event as any).diagnostics && { errorDiagnostics: (event as any).diagnostics }),\n          };\n          addNode(node);\n          activeSteps.delete(id);\n        }\n        break;\n      }\n\n      case \"step_aborted\": {\n        const id = getStepId(event);\n        const active = activeSteps.get(id);\n        if (active) {\n          const node: StepNode = {\n            type: \"step\",\n            id: active.id,\n            name: active.name,\n            key: active.key,\n            state: \"aborted\",\n            startTs: active.startTs,\n            endTs: event.ts,\n            durationMs: event.durationMs,\n            ...(active.retryCount > 0 && { retryCount: active.retryCount }),\n            ...(active.timedOut && { timedOut: true, timeoutMs: active.timeoutMs }),\n            ...(active.metadata && { metadata: active.metadata }),\n          };\n          addNode(node);\n          activeSteps.delete(id);\n        }\n        break;\n      }\n\n      case \"step_cache_hit\": {\n        const id = getStepId(event);\n        const node: StepNode = {\n          type: \"step\",\n          id,\n          name: event.name,\n          key: event.stepKey,\n          state: \"cached\",\n          startTs: event.ts,\n          endTs: event.ts,\n          durationMs: 0,\n        };\n        addNode(node);\n        break;\n      }\n\n      case \"step_cache_miss\":\n        // Cache miss just means the step will execute normally\n        // We'll get a step_start event next\n        break;\n\n      case \"step_complete\":\n        // step_complete is for state persistence, not visualization\n        // We already handled the step via step_success/step_error\n        break;\n\n      case \"step_timeout\": {\n        // Timeout is an intermediate event - step may retry or will get step_error\n        // Track timeout info on the active step\n        const id = getStepId(event);\n        const active = activeSteps.get(id);\n        if (active) {\n          active.timedOut = true;\n          active.timeoutMs = event.timeoutMs;\n        }\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"step_retry\": {\n        // Retry is an intermediate event - increment retry counter\n        const id = getStepId(event);\n        const active = activeSteps.get(id);\n        if (active) {\n          active.retryCount = (event.attempt ?? 1) - 1; // attempt is 1-indexed, retryCount is 0-indexed\n        }\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"step_retries_exhausted\":\n        // All retries exhausted - step_error will follow\n        // The error state will be set by step_error handler\n        lastUpdatedAt = Date.now();\n        break;\n\n      case \"step_skipped\": {\n        const id = getStepId(event);\n        const node: StepNode = {\n          type: \"step\",\n          id,\n          name: event.name,\n          key: event.stepKey,\n          state: \"skipped\",\n          startTs: event.ts,\n          endTs: event.ts,\n          durationMs: 0,\n          ...((event as any).metadata && { metadata: (event as any).metadata }),\n        };\n        addNode(node);\n        break;\n      }\n\n      // Core decision event (emitted automatically by step.if / step.branch).\n      case \"decision\": {\n        const e = event as unknown as {\n          decisionId: string;\n          label?: string;\n          branch: string;\n          value: unknown;\n          phase?: \"start\" | \"end\";\n          durationMs?: number;\n          ts: number;\n        };\n\n        // step.branch owns arm execution and emits a scoped decision\n        // (phase start/end). Route it through the decision machinery so the\n        // arm's steps nest inside the taken branch, exactly like trackIf.\n        if (e.phase === \"start\") {\n          handleDecisionEvent({\n            type: \"decision_start\",\n            workflowId: (event as { workflowId: string }).workflowId,\n            decisionId: e.decisionId,\n            name: e.label ?? e.decisionId,\n            condition: e.label,\n            decisionValue: e.value,\n            ts: e.ts,\n          });\n          handleDecisionEvent({\n            type: \"decision_branch\",\n            workflowId: (event as { workflowId: string }).workflowId,\n            decisionId: e.decisionId,\n            branchLabel: \"then\",\n            condition: e.label,\n            taken: e.branch === \"then\",\n            ts: e.ts,\n          });\n          handleDecisionEvent({\n            type: \"decision_branch\",\n            workflowId: (event as { workflowId: string }).workflowId,\n            decisionId: e.decisionId,\n            branchLabel: \"else\",\n            taken: e.branch === \"else\",\n            ts: e.ts,\n          });\n          break;\n        }\n        if (e.phase === \"end\") {\n          handleDecisionEvent({\n            type: \"decision_end\",\n            workflowId: (event as { workflowId: string }).workflowId,\n            decisionId: e.decisionId,\n            branchTaken: e.branch,\n            ts: e.ts,\n            durationMs: e.durationMs ?? 0,\n          });\n          break;\n        }\n\n        // step.if is instantaneous — core cannot see the extent of a raw\n        // `if` block, so this is a decision marker: the branch taken is\n        // recorded, but steps executed inside the branch appear as siblings\n        // (the static graph carries the nesting).\n        const node: DecisionNode = {\n          type: \"decision\",\n          id: e.decisionId,\n          name: e.label ?? e.decisionId,\n          state: \"success\",\n          startTs: e.ts,\n          endTs: e.ts,\n          durationMs: 0,\n          condition: e.label,\n          decisionValue: e.value,\n          branchTaken: e.branch,\n          branches: [\n            { label: \"then\", condition: e.label, taken: e.branch === \"then\", children: [] },\n            { label: \"else\", taken: e.branch === \"else\", children: [] },\n          ],\n        };\n        addNode(node);\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      // Hook events\n      case \"hook_should_run\": {\n        const hookExec: HookExecution = {\n          type: \"shouldRun\",\n          state: \"success\",\n          ts: event.ts,\n          durationMs: event.durationMs,\n          context: {\n            result: event.result,\n            skipped: event.skipped,\n          },\n        };\n        hookState.shouldRun = hookExec;\n        preStartHookWorkflowId = event.workflowId;\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"hook_should_run_error\": {\n        const hookExec: HookExecution = {\n          type: \"shouldRun\",\n          state: \"error\",\n          ts: event.ts,\n          durationMs: event.durationMs,\n          error: event.error,\n        };\n        hookState.shouldRun = hookExec;\n        preStartHookWorkflowId = event.workflowId;\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"hook_before_start\": {\n        const hookExec: HookExecution = {\n          type: \"onBeforeStart\",\n          state: \"success\",\n          ts: event.ts,\n          durationMs: event.durationMs,\n          context: {\n            result: event.result,\n            skipped: event.skipped,\n          },\n        };\n        hookState.onBeforeStart = hookExec;\n        preStartHookWorkflowId = event.workflowId;\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"hook_before_start_error\": {\n        const hookExec: HookExecution = {\n          type: \"onBeforeStart\",\n          state: \"error\",\n          ts: event.ts,\n          durationMs: event.durationMs,\n          error: event.error,\n        };\n        hookState.onBeforeStart = hookExec;\n        preStartHookWorkflowId = event.workflowId;\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"hook_after_step\": {\n        const hookExec: HookExecution = {\n          type: \"onAfterStep\",\n          state: \"success\",\n          ts: event.ts,\n          durationMs: event.durationMs,\n          context: {\n            stepKey: event.stepKey,\n          },\n        };\n        hookState.onAfterStep.set(event.stepKey, hookExec);\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"hook_after_step_error\": {\n        const hookExec: HookExecution = {\n          type: \"onAfterStep\",\n          state: \"error\",\n          ts: event.ts,\n          durationMs: event.durationMs,\n          error: event.error,\n          context: {\n            stepKey: event.stepKey,\n          },\n        };\n        hookState.onAfterStep.set(event.stepKey, hookExec);\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      // Stream events\n      case \"stream_created\": {\n        const streamKey = `${event.workflowId}:${event.namespace}`;\n        activeStreams.set(streamKey, {\n          id: generateId(),\n          namespace: event.namespace,\n          startTs: event.ts,\n          writeCount: 0,\n          readCount: 0,\n          streamState: \"active\",\n          backpressureOccurred: false,\n          finalPosition: 0,\n        });\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"stream_write\": {\n        const streamKey = `${event.workflowId}:${event.namespace}`;\n        const stream = activeStreams.get(streamKey);\n        if (stream) {\n          stream.writeCount++;\n          stream.finalPosition = Math.max(stream.finalPosition, event.position);\n        }\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"stream_read\": {\n        const streamKey = `${event.workflowId}:${event.namespace}`;\n        const stream = activeStreams.get(streamKey);\n        if (stream) {\n          stream.readCount++;\n        }\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"stream_close\": {\n        const streamKey = `${event.workflowId}:${event.namespace}`;\n        const stream = activeStreams.get(streamKey);\n        if (stream) {\n          stream.streamState = \"closed\";\n          stream.finalPosition = event.finalPosition;\n          // Create StreamNode and add to current nodes\n          const node: StreamNode = {\n            type: \"stream\",\n            id: stream.id,\n            namespace: stream.namespace,\n            state: \"success\",\n            startTs: stream.startTs,\n            endTs: event.ts,\n            durationMs: event.ts - stream.startTs,\n            writeCount: stream.writeCount,\n            readCount: stream.readCount,\n            finalPosition: event.finalPosition,\n            streamState: \"closed\",\n            backpressureOccurred: stream.backpressureOccurred,\n          };\n          addNode(node);\n          activeStreams.delete(streamKey);\n        }\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"stream_error\": {\n        const streamKey = `${event.workflowId}:${event.namespace}`;\n        const stream = activeStreams.get(streamKey);\n        if (stream) {\n          stream.streamState = \"error\";\n          // Create StreamNode with error state and add to current nodes\n          const node: StreamNode = {\n            type: \"stream\",\n            id: stream.id,\n            namespace: stream.namespace,\n            state: \"error\",\n            error: event.error,\n            startTs: stream.startTs,\n            endTs: event.ts,\n            durationMs: event.ts - stream.startTs,\n            writeCount: stream.writeCount,\n            readCount: stream.readCount,\n            finalPosition: event.position,\n            streamState: \"error\",\n            backpressureOccurred: stream.backpressureOccurred,\n          };\n          addNode(node);\n          activeStreams.delete(streamKey);\n        }\n        lastUpdatedAt = Date.now();\n        break;\n      }\n\n      case \"stream_backpressure\": {\n        const streamKey = `${event.workflowId}:${event.namespace}`;\n        const stream = activeStreams.get(streamKey);\n        if (stream) {\n          stream.backpressureOccurred = true;\n        }\n        lastUpdatedAt = Date.now();\n        break;\n      }\n    }\n\n    // Capture snapshot after processing event (for time-travel)\n    captureSnapshot(event);\n  }\n\n  /**\n   * Handle a scope event (parallel/race start/end).\n   */\n  function handleScopeEvent(event: ScopeStartEvent | ScopeEndEvent): void {\n    if (event.type === \"scope_start\") {\n      scopeStack.push({\n        id: event.scopeId,\n        name: event.name,\n        type: event.scopeType,\n        startTs: event.ts,\n        children: [],\n      });\n      lastUpdatedAt = Date.now();\n    } else if (event.type === \"scope_end\") {\n      // Find the scope by ID (not just pop from top) to handle out-of-order events\n      const scopeIndex = scopeStack.findIndex((s) => s.id === event.scopeId);\n      if (scopeIndex === -1) {\n        // Scope not found (already closed), ignore event\n        return;\n      }\n\n      // Close all nested scopes first (those that started after this one)\n      // Process in reverse order (innermost first) so each gets added to its parent\n      while (scopeStack.length > scopeIndex + 1) {\n        const nestedScope = scopeStack.pop()!;\n        const nestedNode: ParallelNode | RaceNode =\n          nestedScope.type === \"race\"\n            ? {\n                type: \"race\",\n                id: nestedScope.id,\n                name: nestedScope.name,\n                state: deriveState(nestedScope.children),\n                startTs: nestedScope.startTs,\n                endTs: event.ts,\n                children: nestedScope.children,\n              }\n            : {\n                type: \"parallel\",\n                id: nestedScope.id,\n                name: nestedScope.name,\n                state: deriveState(nestedScope.children),\n                startTs: nestedScope.startTs,\n                endTs: event.ts,\n                children: nestedScope.children,\n                mode: nestedScope.type === \"allSettled\" ? \"allSettled\" : \"all\",\n              };\n        // Add to the parent scope (which is now at the top of the remaining stack)\n        scopeStack[scopeStack.length - 1].children.push(nestedNode);\n      }\n\n      // Now close the target scope\n      const [scope] = scopeStack.splice(scopeIndex, 1);\n\n      const node: ParallelNode | RaceNode =\n        scope.type === \"race\"\n          ? {\n              type: \"race\",\n              id: scope.id,\n              name: scope.name,\n              state: deriveState(scope.children),\n              startTs: scope.startTs,\n              endTs: event.ts,\n              durationMs: event.durationMs,\n              children: scope.children,\n              winnerId: event.winnerId,\n            }\n          : {\n              type: \"parallel\",\n              id: scope.id,\n              name: scope.name,\n              state: deriveState(scope.children),\n              startTs: scope.startTs,\n              endTs: event.ts,\n              durationMs: event.durationMs,\n              children: scope.children,\n              mode: scope.type === \"allSettled\" ? \"allSettled\" : \"all\",\n            };\n      addNode(node);\n    }\n  }\n\n  /**\n   * Handle a decision event (conditional branch start/branch/end).\n   */\n  function handleDecisionEvent(\n    event: DecisionStartEvent | DecisionBranchEvent | DecisionEndEvent\n  ): void {\n    if (event.type === \"decision_start\") {\n      decisionStack.push({\n        id: event.decisionId,\n        name: event.name,\n        condition: event.condition,\n        decisionValue: event.decisionValue,\n        startTs: event.ts,\n        branches: new Map(),\n        pendingChildren: [],\n      });\n      lastUpdatedAt = Date.now();\n    } else if (event.type === \"decision_branch\") {\n      // Match by decisionId so outer decisions receive branch events while inner is on stack\n      const decision = decisionStack.find((d) => d.id === event.decisionId);\n      if (decision) {\n        // Find or create branch\n        const branchKey = event.branchLabel;\n        const existing = decision.branches.get(branchKey);\n        if (existing) {\n          // Update existing branch\n          existing.taken = event.taken;\n          // If this branch is now taken and there are pending children, transfer them\n          if (event.taken && decision.pendingChildren.length > 0) {\n            existing.children.unshift(...decision.pendingChildren);\n            decision.pendingChildren = [];\n          }\n        } else {\n          // Create new branch - if taken, include any pending children\n          const children = event.taken ? [...decision.pendingChildren] : [];\n          if (event.taken) {\n            decision.pendingChildren = [];\n          }\n          decision.branches.set(branchKey, {\n            label: event.branchLabel,\n            condition: event.condition,\n            taken: event.taken,\n            children,\n          });\n        }\n        lastUpdatedAt = Date.now();\n      }\n    } else if (event.type === \"decision_end\") {\n      // Match by decisionId so out-of-order end (e.g. outer ends before inner) is handled\n      const index = decisionStack.findIndex((d) => d.id === event.decisionId);\n      if (index !== -1) {\n        const [decision] = decisionStack.splice(index, 1);\n        // Temporarily trim stack to parent scope so addNode attaches to the right place\n        const toRestore = decisionStack.splice(index);\n\n        // Convert branches map to array; if no branch events were emitted, put pendingChildren in a single synthetic branch\n        let branches: DecisionBranch[] = Array.from(decision.branches.values());\n        if (branches.length === 0 && decision.pendingChildren.length > 0) {\n          branches = [\n            {\n              label: \"default\",\n              taken: true,\n              children: [...decision.pendingChildren],\n            },\n          ];\n        }\n\n        // Infer branchTaken from branches if not provided in event\n        const inferredBranchTaken = branches.find((b) => b.taken)?.label;\n\n        const node: DecisionNode = {\n          type: \"decision\",\n          id: decision.id,\n          name: decision.name,\n          state: deriveState(\n            branches.flatMap((b) => (b.taken ? b.children : []))\n          ),\n          startTs: decision.startTs,\n          endTs: event.ts,\n          durationMs: event.durationMs,\n          condition: decision.condition,\n          decisionValue: decision.decisionValue,\n          branchTaken: event.branchTaken ?? inferredBranchTaken,\n          branches,\n        };\n        addNode(node);\n        decisionStack.push(...toRestore);\n        lastUpdatedAt = Date.now();\n      }\n    }\n  }\n\n  /**\n   * Derive the state of a parent node from its children.\n   */\n  function deriveState(children: FlowNode[]): StepState {\n    if (children.length === 0) return \"success\";\n\n    const hasError = children.some((c) => c.state === \"error\");\n    if (hasError) return \"error\";\n\n    const allSuccess = children.every(\n      (c) => c.state === \"success\" || c.state === \"cached\"\n    );\n    if (allSuccess) return \"success\";\n\n    const hasRunning = children.some((c) => c.state === \"running\");\n    if (hasRunning) return \"running\";\n\n    return \"pending\";\n  }\n\n  /**\n   * Get the current nodes including any active (running) steps and streams.\n   */\n  function getCurrentNodes(): FlowNode[] {\n    const nodes = [...currentNodes];\n\n    // Add active steps as running nodes\n    for (const [, active] of activeSteps) {\n      nodes.push({\n        type: \"step\",\n        id: active.id,\n        name: active.name,\n        key: active.key,\n        state: \"running\",\n        startTs: active.startTs,\n        ...(active.retryCount > 0 && { retryCount: active.retryCount }),\n        ...(active.timedOut && { timedOut: true, timeoutMs: active.timeoutMs }),\n      });\n    }\n\n    // Add active streams as running nodes\n    for (const [, stream] of activeStreams) {\n      nodes.push({\n        type: \"stream\",\n        id: stream.id,\n        namespace: stream.namespace,\n        state: \"running\",\n        startTs: stream.startTs,\n        writeCount: stream.writeCount,\n        readCount: stream.readCount,\n        finalPosition: stream.finalPosition,\n        streamState: stream.streamState,\n        backpressureOccurred: stream.backpressureOccurred,\n      } satisfies StreamNode);\n    }\n\n    return nodes;\n  }\n\n  /**\n   * Build and return the current IR state.\n   */\n  function getIR(): WorkflowIR {\n    let children = getCurrentNodes();\n\n    // Apply parallel detection if enabled\n    if (detectParallel) {\n      children = detectParallelGroups(children, parallelDetection);\n    }\n\n    const root: WorkflowNode = {\n      type: \"workflow\",\n      id: workflowId ?? defaultWorkflowId,\n      workflowId: workflowId ?? defaultWorkflowId,\n      state: workflowState,\n      startTs: workflowStartTs,\n      endTs: workflowEndTs,\n      durationMs: workflowDurationMs,\n      children,\n      error: workflowError,\n    };\n\n    // Include hooks if any have been recorded\n    const hasHooks =\n      hookState.shouldRun !== undefined ||\n      hookState.onBeforeStart !== undefined ||\n      hookState.onAfterStep.size > 0;\n\n    return {\n      root,\n      metadata: {\n        createdAt,\n        lastUpdatedAt,\n      },\n      ...(hasHooks && { hooks: hookState }),\n    };\n  }\n\n  /**\n   * Reset the builder state.\n   */\n  function reset(): void {\n    workflowId = undefined;\n    workflowStartTs = undefined;\n    workflowEndTs = undefined;\n    workflowState = \"pending\";\n    workflowError = undefined;\n    workflowDurationMs = undefined;\n    activeSteps.clear();\n    scopeStack.length = 0;\n    decisionStack.length = 0;\n    activeStreams.clear();\n    currentNodes = [];\n    createdAt = Date.now();\n    lastUpdatedAt = createdAt;\n    // Clear hooks\n    hookState = {\n      onAfterStep: new Map(),\n    };\n    preStartHookWorkflowId = undefined;\n    // Clear snapshots\n    snapshots.length = 0;\n    eventIndex = 0;\n  }\n\n  /**\n   * Get all recorded snapshots.\n   */\n  function getSnapshots(): IRSnapshot[] {\n    return [...snapshots];\n  }\n\n  /**\n   * Get a snapshot at a specific index.\n   */\n  function getSnapshotAt(index: number): IRSnapshot | undefined {\n    return snapshots[index];\n  }\n\n  /**\n   * Get the IR state at a specific snapshot index.\n   */\n  function getIRAt(index: number): WorkflowIR | undefined {\n    return snapshots[index]?.ir;\n  }\n\n  /**\n   * Clear all recorded snapshots.\n   */\n  function clearSnapshots(): void {\n    snapshots.length = 0;\n    eventIndex = 0;\n  }\n\n  return {\n    handleEvent,\n    handleScopeEvent,\n    handleDecisionEvent,\n    getIR,\n    reset,\n    // Snapshot methods for time-travel\n    getSnapshots,\n    getSnapshotAt,\n    getIRAt,\n    clearSnapshots,\n    /** Check if there are active (running) steps */\n    get hasActiveSteps() {\n      return activeSteps.size > 0;\n    },\n    /** Get the current workflow state */\n    get state() {\n      return workflowState;\n    },\n    /** Get the number of recorded snapshots */\n    get snapshotCount() {\n      return snapshots.length;\n    },\n    /** Check if snapshot recording is enabled */\n    get snapshotsEnabled() {\n      return enableSnapshots;\n    },\n    /** Enable or disable snapshot recording (e.g. for time-travel stop/start) */\n    setSnapshotsEnabled(enabled: boolean): void {\n      enableSnapshots = enabled;\n    },\n  };\n}\n\n/**\n * Type for the IR builder instance.\n */\nexport type IRBuilder = ReturnType<typeof createIRBuilder>;\n","/**\n * Discord Adapter\n *\n * Platform-specific adapter for Discord webhooks.\n * Handles only Discord-specific message formatting and API calls.\n */\n\nimport { ok, err, type AsyncResult } from \"awaitly\";\nimport type { NotifierAdapter, NotifierContext } from \"../adapter\";\nimport type { WorkflowIR } from \"../../types\";\nimport type { WorkflowStatus } from \"../types\";\n\n/**\n * Error types for Discord adapter operations.\n */\nexport type DiscordError = \"INVALID_WEBHOOK_URL\" | \"SEND_FAILED\" | \"TIMEOUT\";\n\n/**\n * Options for Discord adapter.\n */\nexport interface DiscordAdapterOptions {\n  /** Discord webhook URL */\n  webhookUrl: string;\n  /** Bot username to display (optional) */\n  username?: string;\n  /** Bot avatar URL (optional) */\n  avatarUrl?: string;\n  /** Request timeout in milliseconds (default: 30000) */\n  timeout?: number;\n}\n\n/**\n * Discord webhook message structure.\n */\ninterface DiscordMessage {\n  content?: string;\n  username?: string;\n  avatar_url?: string;\n  embeds?: DiscordEmbed[];\n}\n\n/**\n * Discord embed structure.\n */\ninterface DiscordEmbed {\n  title?: string;\n  description?: string;\n  color?: number;\n  image?: { url: string };\n  fields?: Array<{ name: string; value: string; inline?: boolean }>;\n  footer?: { text: string };\n  timestamp?: string;\n}\n\n/**\n * Status colors for Discord embeds.\n */\nconst STATUS_COLORS: Record<WorkflowStatus | \"running\", number> = {\n  running: 0x3498db, // Blue\n  completed: 0x2ecc71, // Green\n  failed: 0xe74c3c, // Red\n  cancelled: 0x95a5a6, // Gray\n};\n\n/**\n * Create a Discord adapter.\n *\n * @param options - Discord adapter options\n * @returns A NotifierAdapter for Discord\n */\nexport function createDiscordAdapter(\n  options: DiscordAdapterOptions\n): NotifierAdapter<string> {\n  const { webhookUrl, username, avatarUrl, timeout = 30000 } = options;\n\n  // Validate webhook URL\n  if (!webhookUrl.includes(\"discord.com/api/webhooks/\")) {\n    throw new Error(\"Invalid Discord webhook URL\");\n  }\n\n  async function sendMessage(\n    message: DiscordMessage,\n    messageId?: string\n  ): AsyncResult<string | undefined, DiscordError> {\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n    try {\n      // Use ?wait=true to get message ID back\n      const url = messageId\n        ? `${webhookUrl}/messages/${messageId}`\n        : `${webhookUrl}?wait=true`;\n\n      const method = messageId ? \"PATCH\" : \"POST\";\n\n      const response = await fetch(url, {\n        method,\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify(message),\n        signal: controller.signal,\n      });\n\n      if (!response.ok) {\n        return err(\"SEND_FAILED\");\n      }\n\n      // Extract message ID from response\n      const data = (await response.json()) as { id?: string };\n      return ok(data.id);\n    } catch (error) {\n      if (error instanceof Error && error.name === \"AbortError\") {\n        return err(\"TIMEOUT\");\n      }\n      return err(\"SEND_FAILED\");\n    } finally {\n      clearTimeout(timeoutId);\n    }\n  }\n\n  return {\n    name: \"Discord\",\n\n    buildMessage(\n      {\n        ir,\n        title,\n        status,\n      }: { ir: WorkflowIR; title: string; status: WorkflowStatus | \"running\" },\n      ctx: NotifierContext\n    ): DiscordMessage {\n      const embed: DiscordEmbed = {\n        title,\n        color: STATUS_COLORS[status],\n        image: { url: ctx.getDiagramUrl(ir) },\n        fields: [\n          { name: \"Steps\", value: String(ctx.countSteps(ir)), inline: true },\n          { name: \"Duration\", value: ctx.formatDuration(ir), inline: true },\n          {\n            name: \"Status\",\n            value: status.charAt(0).toUpperCase() + status.slice(1),\n            inline: true,\n          },\n        ],\n        timestamp: new Date().toISOString(),\n      };\n\n      return {\n        username,\n        avatar_url: avatarUrl,\n        embeds: [embed],\n      };\n    },\n\n    async sendNew(message: unknown): Promise<string | undefined> {\n      const result = await sendMessage(message as DiscordMessage);\n      if (!result.ok) {\n        throw new Error(`Discord send failed: ${result.error}`);\n      }\n      return result.value;\n    },\n\n    async sendUpdate(messageId: string, message: unknown): Promise<void> {\n      const result = await sendMessage(message as DiscordMessage, messageId);\n      if (!result.ok) {\n        throw new Error(`Discord update failed: ${result.error}`);\n      }\n    },\n  };\n}\n"],"mappings":"ukBAAA,IAAAA,GAAA,GAAAC,GAAAD,GAAA,2BAAAE,KAAA,eAAAC,GAAAH,ICOA,IAAAI,EAA0C,mBC+KnC,SAASC,EAAWC,EAAwB,CACjD,SAASC,EAAaC,EAA2B,CAC/C,IAAIC,EAAQ,EACZ,QAAWC,KAAQF,EACjB,GAAIE,EAAK,OAAS,OAChBD,YACSC,EAAK,OAAS,YAAcA,EAAK,SAE1C,QAAWC,KAAUD,EAAK,SACpBC,EAAO,QACTF,GAASF,EAAaI,EAAO,QAAQ,OAGhC,aAAcD,GAAQA,EAAK,WACpCD,GAASF,EAAaG,EAAK,QAAQ,GAGvC,OAAOD,CACT,CACA,OAAOF,EAAaD,EAAG,KAAK,QAAQ,CACtC,CCnMA,IAAAM,GAAqC,mBCkb9B,SAASC,EAAWC,EAAkC,CAC3D,OAAOA,EAAK,OAAS,MACvB,CAYO,SAASC,GAAeC,EAAsC,CACnE,OAAOA,EAAK,OAAS,UACvB,CAKO,SAASC,GAAWD,EAAkC,CAC3D,OAAOA,EAAK,OAAS,MACvB,CAKO,SAASE,GAAeF,EAAsC,CACnE,OAAOA,EAAK,OAAS,UACvB,CAKO,SAASG,GAAaH,EAAoC,CAC/D,OAAOA,EAAK,OAAS,QACvB,CCldO,SAASI,EAAeC,EAAoB,CACjD,GAAIA,EAAK,IACP,MAAO,GAAG,KAAK,MAAMA,CAAE,CAAC,KAG1B,GAAIA,EAAK,IAGP,MAAO,IAFSA,EAAK,KAEH,QAAQ,CAAC,EAAE,QAAQ,OAAQ,EAAE,CAAC,IAGlD,IAAIC,EAAU,KAAK,MAAMD,EAAK,GAAK,EAC/BE,EAAU,KAAK,MAAOF,EAAK,IAAS,GAAI,EAM5C,OALIE,GAAW,KACbD,GAAW,EACXC,EAAU,GAGRA,IAAY,EACP,GAAGD,CAAO,IAGZ,GAAGA,CAAO,KAAKC,CAAO,GAC/B,CAKO,SAASC,GAAqB,CACnC,MAAO,QAAQ,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,CAAC,CAAC,EACrE,CC9BA,IAAMC,GAAM,UAGNC,GAAS,WACTC,GAAW,WACXC,GAAY,WACZC,GAAU,WACVC,GAAU,WACVC,GAAW,WAmCV,IAAMC,EAAkC,CAC7C,QAASC,GACT,QAASC,GACT,QAASC,GACT,MAAOC,GACP,QAASC,GACT,OAAQC,GACR,QAASC,GAAMF,EACjB,ECxDA,IAAAG,EAAqC,mBC8G9B,SAASC,GAAaC,EAAyB,CACpD,OAAIA,EAAO,GAAY,OACnBA,EAAO,GAAY,OACnBA,EAAO,GAAY,UACnBA,EAAO,GAAY,OACnBA,EAAO,IAAa,MACjB,UACT,CDnFA,SAASC,IAAgC,CACvC,MAAO,CAEL,kFAEA,kFAEA,kFAEA,gFAEA,wGAEA,iFAEA,wGAEA,iFAEA,uFAEA,qFACF,CACF,CAKA,SAASC,IAAuC,CAC9C,MAAO,CAEL,oFACA,oFACA,uFACA,oFACA,mFACA,uFACF,CACF,CAKA,SAASC,GAAaC,EAA0B,CAC9C,MAAO,QAAQA,CAAK,EACtB,CAYA,SAASC,IAAoC,CAC3C,MAAO,CAEL,uFACA,oFACF,CACF,CAMA,SAASC,GAAcC,EAAgD,CACrE,GAAI,CAMF,SAAO,MAAG,KAAK,UAAUA,EALR,CAACC,EAAcC,IAAwB,CACtD,GAAI,OAAOA,GAAM,SAAU,OAAOA,EAClC,IAAMC,EAAI,OAAOD,CAAC,EAClB,OAAO,OAAO,cAAcC,CAAC,EAAIA,EAAID,EAAE,SAAS,CAClD,CACwC,CAAC,CAC3C,MAAQ,CACN,SAAO,OAAI,iBAAiB,CAC9B,CACF,CAKA,SAASE,GAAeJ,EAAwB,CAC9C,IAAMK,EAASN,GAAcC,CAAK,EAClC,OAAOK,EAAO,GAAKA,EAAO,MAAQ,kBACpC,CAMA,SAASC,GACPC,EACAC,EACAC,EACoC,CACpC,IAAIC,EAGJ,GAAIH,EAAM,UAAW,CACnB,IAAMI,EAAS,iBACTC,EAAQL,EAAM,UAAU,QAAU,UAAY,eAAiB,aAC/DM,EAAON,EAAM,UAAU,QAAU,UAAY,SAAM,SACnDO,EAASL,EAAQ,aAAeF,EAAM,UAAU,aAAe,OACjE,IAAIQ,EAAeR,EAAM,UAAU,UAAU,CAAC,GAC9C,GACES,EAAUT,EAAM,UAAU,SAAS,QACrC,sBACAA,EAAM,UAAU,SAAS,SAAW,GAClC,aACA,GAENC,EAAM,KAAK,OAAOG,CAAM,MAAME,CAAI,aAAaG,CAAO,GAAGF,CAAM,SAASF,CAAK,EAAE,EAC/EF,EAAaC,CACf,CAGA,GAAIJ,EAAM,cAAe,CACvB,IAAMI,EAAS,mBACTC,EAAQL,EAAM,cAAc,QAAU,UAAY,eAAiB,aACnEM,EAAON,EAAM,cAAc,QAAU,UAAY,SAAM,SACvDO,EAASL,EAAQ,aAAeF,EAAM,cAAc,aAAe,OACrE,IAAIQ,EAAeR,EAAM,cAAc,UAAU,CAAC,GAClD,GACES,EAAUT,EAAM,cAAc,SAAS,QACzC,sBACA,GAEJC,EAAM,KAAK,OAAOG,CAAM,MAAME,CAAI,iBAAiBG,CAAO,GAAGF,CAAM,SAASF,CAAK,EAAE,EAG/EF,GACFF,EAAM,KAAK,OAAOE,CAAU,QAAQC,CAAM,EAAE,EAE9CD,EAAaC,CACf,CAEA,MAAO,CAAE,WAAAD,CAAW,CACtB,CAMA,IAAIO,GAAc,EACZC,EAAkB,IAAI,IACtBC,EAAc,IAAI,IAExB,SAASC,EAAeC,EAAiB,OAAgB,CACvD,MAAO,GAAGA,CAAM,IAAI,EAAEJ,EAAW,EACnC,CAEA,SAASK,IAAyB,CAChCL,GAAc,EACdC,EAAgB,MAAM,EACtBC,EAAY,MAAM,CACpB,CAgBA,SAASI,EAAkBC,EAAsB,CAC/C,OAAOA,EACJ,QAAQ,KAAM,QAAQ,EACtB,QAAQ,KAAM,MAAM,EACpB,QAAQ,KAAM,MAAM,EACpB,KAAK,CACV,CASA,SAASC,GAAmBD,EAAsB,CAChD,OAAOD,EAAkBC,CAAI,EAC1B,QAAQ,aAAc,EAAE,CAC7B,CASO,SAASE,GAA4B,CAC1C,MAAO,CACL,KAAM,UACN,aAAc,GAEd,OAAOC,EAAgBlB,EAAgC,CACrDa,GAAiB,EACjB,IAAMd,EAAkB,CAAC,EAGnBoB,EAAWnB,EAGjBD,EAAM,KAAK,cAAc,EAGzB,IAAIqB,EACAF,EAAG,QAELE,EADmBvB,GAAYqB,EAAG,MAAOnB,EAAOC,CAAO,EAC/B,YAI1B,IAAMqB,EAAU,QAChBtB,EAAM,KAAK,OAAOsB,CAAO,oBAAe,EAGpCD,GACFrB,EAAM,KAAK,OAAOqB,CAAU,QAAQC,CAAO,EAAE,EAI/C,IAAIC,EAAaD,EAGjB,QAAWE,KAASL,EAAG,KAAK,SAAU,CACpC,IAAMtB,EAAS4B,EAAWD,EAAOvB,EAASD,EAAOoB,EAAUD,EAAG,KAAK,EACnEnB,EAAM,KAAK,OAAOuB,CAAU,QAAQ1B,EAAO,OAAO,EAAE,EACpD0B,EAAa1B,EAAO,MACtB,CAIA,GADuB,CAAC,UAAW,QAAS,SAAS,EAClC,SAASsB,EAAG,KAAK,KAAwC,EAAG,CAC7E,IAAMO,EAAQ,SACRC,EACJR,EAAG,KAAK,QAAU,UAAY,SAC1BA,EAAG,KAAK,QAAU,QAAU,SAC1B,SACFS,EACJT,EAAG,KAAK,QAAU,UAAY,OAC1BA,EAAG,KAAK,QAAU,QAAU,SAC1B,YACFU,EAAW,MAAMF,CAAO,IAAIC,CAAQ,MACpCE,EACJX,EAAG,KAAK,QAAU,UAAY,aAC1BA,EAAG,KAAK,QAAU,QAAU,WAC1B,aACRnB,EAAM,KAAK,OAAO0B,CAAK,GAAGG,CAAQ,GAAGC,CAAQ,EAAE,EAC/C9B,EAAM,KAAK,OAAOuB,CAAU,QAAQG,CAAK,EAAE,CAC7C,CAGA,OAAA1B,EAAM,KAAK,EAAE,EACbA,EAAM,KAAK,GAAG+B,GAAoB,CAAC,EAG/BX,EAAS,aACXpB,EAAM,KAAK,GAAGgC,GAA2B,CAAC,EAIxCb,EAAG,OACLnB,EAAM,KAAK,GAAGV,GAAwB,CAAC,EAGlCU,EAAM,KAAK;AAAA,CAAI,CACxB,CACF,CACF,CAaA,SAASyB,EACPQ,EACAhC,EACAD,EACAoB,EACArB,EACc,CACd,GAAImC,EAAWD,CAAI,EACjB,OAAOE,GAAeF,EAAMhC,EAASD,EAAOoB,EAAUrB,CAAK,EACtD,GAAIqC,GAAeH,CAAI,EAC5B,OAAOI,GAAmBJ,EAAMhC,EAASD,EAAOoB,EAAUrB,CAAK,EAC1D,GAAIuC,GAAWL,CAAI,EACxB,OAAOM,GAAeN,EAAMhC,EAASD,EAAOoB,EAAUrB,CAAK,EACtD,GAAIyC,GAAeP,CAAI,EAC5B,OAAOQ,GAAmBR,EAAMhC,EAASD,EAAOoB,EAAUrB,CAAK,EAC1D,GAAI2C,GAAaT,CAAI,EAC1B,OAAOU,GAAiBV,EAAMhC,EAASD,CAAK,EAI9C,IAAM4C,EAAKhC,EAAe,SAAS,EACnC,OAAAZ,EAAM,KAAK,OAAO4C,CAAE,kBAAkB,EAC/B,CAAE,QAASA,EAAI,OAAQA,CAAG,CACnC,CAKA,SAAST,GACPF,EACAhC,EACAD,EACAoB,EACArB,EACc,CAEd,IAAM8C,EAAc5C,EACd6C,EAAiBD,EAAY,gBAAkB,GAC/CE,EAAiBF,EAAY,gBAAkB,GAC/CG,EAAmBH,EAAY,kBAAoB,GAGrDD,EAAKX,EAAK,IACV,QAAQA,EAAK,IAAI,QAAQ,gBAAiB,GAAG,CAAC,GAC9CrB,EAAe,MAAM,EAGzB,GAAID,EAAY,IAAIiC,CAAE,EAAG,CACvB,IAAIK,EAAS,EACb,KAAOtC,EAAY,IAAI,GAAGiC,CAAE,IAAIK,CAAM,EAAE,GACtCA,IAEFL,EAAK,GAAGA,CAAE,IAAIK,CAAM,EACtB,CACAtC,EAAY,IAAIiC,CAAE,EAElB,IAAMM,EAAYjB,EAAK,MAAQA,EAAK,KAAO,OACrCkB,EAAYlD,EAAQ,UAAYgC,EAAK,KAAOA,EAAK,KACnD,GAAGiB,CAAS,KAAKjB,EAAK,GAAG,IACzBiB,EACEE,EAAQrC,EAAkBoC,CAAS,EAGnC7C,EACJL,EAAQ,aAAegC,EAAK,aAAe,OACvC,IAAI1B,EAAe0B,EAAK,UAAU,CAAC,GACnC,GAGFoB,EAAY,GAChB,OAAQpB,EAAK,MAAO,CAClB,IAAK,UACHoB,EAAY,UACZ,MACF,IAAK,QACHA,EAAY,UACZ,MACF,IAAK,SACHA,EAAY,aACZ,MACF,IAAK,UACHA,EAAY,UACZ,MACF,IAAK,UACHA,EAAY,UACZ,KACJ,CAIA,IAAIC,EAAS,GACb,GAAIrB,EAAK,QAAU,OAAW,CAC5B,IAAMsB,EAAW,OAAOtB,EAAK,OAAU,SACnClB,EAAkBkB,EAAK,KAAK,EAC5BlB,EAAkBnB,GAAeqC,EAAK,KAAK,EAAE,MAAM,EAAG,EAAE,CAAC,EAC7DqB,GAAU,UAAUC,CAAQ,EAC9B,CACA,GAAItB,EAAK,SAAW,QAAaA,EAAK,QAAU,UAAW,CACzD,IAAMuB,EAAY,OAAOvB,EAAK,QAAW,SACrClB,EAAkBkB,EAAK,MAAM,EAC7BlB,EAAkBnB,GAAeqC,EAAK,MAAM,EAAE,MAAM,EAAG,EAAE,CAAC,EAC9DqB,GAAU,WAAWE,CAAS,EAChC,CAGA,IAAIC,EAAW,GACTC,EAAUzB,EAAK,KAAOA,EAAK,GACjC,GAAIlC,GAAS2D,GAAW3D,EAAM,YAAY,IAAI2D,CAAO,EAAG,CACtD,IAAMC,EAAW5D,EAAM,YAAY,IAAI2D,CAAO,EACxCE,EAAWD,EAAS,QAAU,UAAY,SAAM,SAChDE,EAAa5D,EAAQ,aAAe0D,EAAS,aAAe,OAC9D,IAAIpD,EAAeoD,EAAS,UAAU,CAAC,GACvC,GACJF,EAAW,MAAMG,CAAQ,QAAQC,CAAU,EAC7C,CAGA,IAAMC,GAAgBT,EAAYD,EAAQE,EAASG,EAAWnD,GAAQ,KAAK,EAIvEyD,EACEC,EAAO5C,GAAU,aAAeA,EAAS,YAC3CA,EAAS,YAAY,KAAK,IAAIa,EAAK,KAAO,EAAE,GAC5Cb,EAAS,YAAY,KAAK,IAAIa,EAAK,MAAQ,EAAE,GAC7Cb,EAAS,YAAY,KAAK,IAAIa,EAAK,EAAE,EACrC,OAEJ,GAAI+B,IAAS,OAAW,CACtB,IAAMC,EAAQC,GAAaF,CAAI,EAC/BD,EAAYI,GAAaF,CAAK,CAChC,MACEF,EAA0B9B,EAAK,MAIjC,IAAImC,EACJ,OAAQnC,EAAK,MAAO,CAClB,IAAK,QAEHmC,EAAQ,MAAMN,CAAY,MAC1B,MACF,IAAK,SAEHM,EAAQ,MAAMN,CAAY,MAC1B,MACF,IAAK,UAEHM,EAAQ,KAAKN,CAAY,KACzB,MACF,QAEEM,EAAQ,KAAKN,CAAY,IAC7B,CAKA,GAHA9D,EAAM,KAAK,OAAO4C,CAAE,GAAGwB,CAAK,MAAML,CAAS,EAAE,EAGzCjB,GAAkBb,EAAK,aAAe,QAAaA,EAAK,WAAa,EAAG,CAC1E,IAAMoC,EAAa,UAAKpC,EAAK,UAAU,QAAQA,EAAK,aAAe,EAAI,IAAM,KAAK,GAClFjC,EAAM,KAAK,OAAO4C,CAAE,UAAUyB,CAAU,MAAMzB,CAAE,EAAE,CACpD,CAGA,GAAIG,GAAkBd,EAAK,QAAU,SAAWA,EAAK,QAAU,OAAW,CACxE,IAAMqC,EAAc,OAAO1B,CAAE,GACvB2B,EAAaxD,EAAkB,OAAOkB,EAAK,KAAK,CAAC,EAAE,MAAM,EAAG,EAAE,EACpEjC,EAAM,KAAK,OAAOsE,CAAW,MAAMC,CAAU,KAAK,EAClDvE,EAAM,KAAK,OAAO4C,CAAE,eAAe0B,CAAW,EAAE,EAChDtE,EAAM,KAAK,aAAasE,CAAW,8BAA8B,CACnE,CAGA,GAAItB,GAAoBf,EAAK,SAAU,CACrC,IAAMuC,EAAgB,MAAM5B,CAAE,GACxB6B,EAAYxC,EAAK,YAAc,OAAY,GAAGA,EAAK,SAAS,KAAO,GACzEjC,EAAM,KAAK,OAAOwE,CAAa,qBAAgBC,CAAS,KAAK,EAC7DzE,EAAM,KAAK,OAAO4C,CAAE,kBAAkB4B,CAAa,EAAE,EACrDxE,EAAM,KAAK,aAAawE,CAAa,8BAA8B,CACrE,CAEA,MAAO,CAAE,QAAS5B,EAAI,OAAQA,CAAG,CACnC,CAKA,SAASP,GACPJ,EACAhC,EACAD,EACAoB,EACArB,EACc,CACd,IAAM2E,EAAa9D,EAAe,UAAU,EACtC+D,EAAS,GAAGD,CAAU,QACtBE,EAAS,GAAGF,CAAU,QACtBG,EAAO5D,GAAmBgB,EAAK,MAAQ,UAAU,EACjD6C,EAAY7C,EAAK,OAAS,aAAe,gBAAkB,GAGjE,GAAIA,EAAK,SAAS,SAAW,EAAG,CAC9B,IAAMW,EAAK8B,EACLtB,EAAQrC,EAAkB,GAAG8D,CAAI,GAAGC,CAAS,EAAE,EAC/CC,EAAO,sCACPzE,EAASL,EAAQ,aAAegC,EAAK,aAAe,OACtD,IAAI1B,EAAe0B,EAAK,UAAU,CAAC,GACnC,GAGJ,OAAAjC,EAAM,KAAK,OAAO4C,CAAE,KAAKQ,CAAK,GAAG9C,CAAM,MAAMyE,CAAI,QAAsB9C,EAAK,KAAM,EAAE,EAC7E,CAAE,QAASW,EAAI,OAAQA,CAAG,CACnC,CAGA5C,EAAM,KAAK,gBAAgB0E,CAAU,KAAKG,CAAI,GAAGC,CAAS,IAAI,EAC9D9E,EAAM,KAAK,kBAAkB,EAG7BA,EAAM,KAAK,OAAO2E,CAAM,iBAAY,EAGpC,IAAMK,EAAyB,CAAC,EAChC,QAAWxD,KAASS,EAAK,SAAU,CACjC,IAAMpC,EAAS4B,EAAWD,EAAOvB,EAASD,EAAOoB,EAAUrB,CAAK,EAChEC,EAAM,KAAK,OAAO2E,CAAM,QAAQ9E,EAAO,OAAO,EAAE,EAChDmF,EAAa,KAAKnF,EAAO,MAAM,CACjC,CAGAG,EAAM,KAAK,OAAO4E,CAAM,iBAAY,EACpC,QAAWK,KAAUD,EACnBhF,EAAM,KAAK,OAAOiF,CAAM,QAAQL,CAAM,EAAE,EAG1C5E,EAAM,KAAK,SAAS,EAGpB,IAAMkF,EAA2BjD,EAAK,MACtC,OAAAjC,EAAM,KAAK,aAAa0E,CAAU,IAAIQ,CAAU,EAAE,EAE3C,CAAE,QAASP,EAAQ,OAAQC,CAAO,CAC3C,CAKA,SAASrC,GACPN,EACAhC,EACAD,EACAoB,EACArB,EACc,CACd,IAAM2E,EAAa9D,EAAe,MAAM,EAClCU,EAAU,GAAGoD,CAAU,SACvBhD,EAAQ,GAAGgD,CAAU,OACrBG,EAAO5D,GAAmBgB,EAAK,MAAQ,MAAM,EAGnD,GAAIA,EAAK,SAAS,SAAW,EAAG,CAC9B,IAAMW,EAAK8B,EACLtB,EAAQrC,EAAkB8D,CAAI,EAC9BE,EAAO,sCACPzE,EAASL,EAAQ,aAAegC,EAAK,aAAe,OACtD,IAAI1B,EAAe0B,EAAK,UAAU,CAAC,GACnC,GAEJ,OAAAjC,EAAM,KAAK,OAAO4C,CAAE,YAAOQ,CAAK,GAAG9C,CAAM,MAAMyE,CAAI,QAAsB9C,EAAK,KAAM,EAAE,EAC/E,CAAE,QAASW,EAAI,OAAQA,CAAG,CACnC,CAGA5C,EAAM,KAAK,gBAAgB0E,CAAU,YAAOG,CAAI,IAAI,EACpD7E,EAAM,KAAK,kBAAkB,EAG7BA,EAAM,KAAK,OAAOsB,CAAO,uBAAgB,EAGzC,IAAM0D,EAA6D,CAAC,EAChEG,EAEJ,QAAW3D,KAASS,EAAK,SAAU,CACjC,IAAMpC,EAAS4B,EAAWD,EAAOvB,EAASD,EAAOoB,EAAUrB,CAAK,EAC1DqF,EAAWnD,EAAK,WAAaT,EAAM,GACzCxB,EAAM,KAAK,OAAOsB,CAAO,QAAQzB,EAAO,OAAO,EAAE,EAE7CuF,IACFD,EAAetF,EAAO,QAExBmF,EAAa,KAAK,CAAE,OAAQnF,EAAO,OAAQ,SAAAuF,CAAS,CAAC,CACvD,CAGApF,EAAM,KAAK,OAAO0B,CAAK,oBAAe,EAGtC,OAAW,CAAE,OAAAuD,EAAQ,SAAAG,CAAS,IAAKJ,EAC7BI,GAAYD,EACdnF,EAAM,KAAK,OAAOiF,CAAM,0BAAmBvD,CAAK,EAAE,EACzCO,EAAK,SAEdjC,EAAM,KAAK,OAAOiF,CAAM,qBAAqBvD,CAAK,EAAE,EAGpD1B,EAAM,KAAK,OAAOiF,CAAM,QAAQvD,CAAK,EAAE,EAI3C1B,EAAM,KAAK,SAAS,EAEpB,IAAMkF,EAA2BjD,EAAK,MACtC,OAAAjC,EAAM,KAAK,aAAa0E,CAAU,IAAIQ,CAAU,EAAE,EAE3C,CAAE,QAAS5D,EAAS,OAAQI,CAAM,CAC3C,CAKA,SAASe,GACPR,EACAhC,EACAD,EACAoB,EACArB,EACc,CAEd,IAAIsF,EAAapD,EAAK,IAClB,YAAYA,EAAK,IAAI,QAAQ,gBAAiB,GAAG,CAAC,GAClDrB,EAAe,UAAU,EAG7B,GAAIF,EAAgB,IAAI2E,CAAU,EAAG,CACnC,IAAIpC,EAAS,EACb,KAAOvC,EAAgB,IAAI,GAAG2E,CAAU,IAAIpC,CAAM,EAAE,GAClDA,IAEFoC,EAAa,GAAGA,CAAU,IAAIpC,CAAM,EACtC,CACAvC,EAAgB,IAAI2E,CAAU,EAG9B,IAAMC,EAAYvE,EAAkBkB,EAAK,WAAa,WAAW,EAC3DsD,EAAgBtD,EAAK,gBAAkB,OACzC,MAAMlB,EAAkB,OAAOkB,EAAK,aAAa,CAAC,EAAE,MAAM,EAAG,EAAE,CAAC,GAChE,GAGEuD,EAAgB,GAAGF,CAAS,GAAGC,CAAa,GAAG,KAAK,EAC1DvF,EAAM,KAAK,OAAOqF,CAAU,KAAKG,CAAa,IAAI,EAGlD,IAAMC,EAA0B,CAAC,EAC7BC,EACEC,EAAgB,IAAI,IAE1B,QAAWC,KAAU3D,EAAK,SAAU,CAElC,IAAI4D,EAAW,GAAGR,CAAU,IAAIO,EAAO,MAAM,QAAQ,gBAAiB,GAAG,CAAC,GAE1E,GAAID,EAAc,IAAIE,CAAQ,EAAG,CAC/B,IAAI5C,EAAS,EACb,KAAO0C,EAAc,IAAI,GAAGE,CAAQ,IAAI5C,CAAM,EAAE,GAC9CA,IAEF4C,EAAW,GAAGA,CAAQ,IAAI5C,CAAM,EAClC,CACA0C,EAAc,IAAIE,CAAQ,EAE1B,IAAMC,EAAkB/E,EAAkB6E,EAAO,KAAK,EAChDG,EAAcH,EAAO,MACvB,GAAGE,CAAe,UAClB,GAAGA,CAAe,WAChBE,EAAcJ,EAAO,MAAQ,aAAe,aAGlD5F,EAAM,KAAK,OAAO6F,CAAQ,KAAKE,CAAW,KAAKC,CAAW,EAAE,EAK5D,IAAMC,EAAYL,EAAO,UACrB,IAAI7E,EAAkB6E,EAAO,SAAS,EAAE,QAAQ,MAAO,EAAE,CAAC,IAC1D,GAIJ,GAHA5F,EAAM,KAAK,OAAOqF,CAAU,OAAOY,CAAS,IAAIJ,CAAQ,EAAE,EAGtDD,EAAO,SAAS,OAAS,EAAG,CAC9B,IAAIM,EAASL,EACb,QAAWrE,KAASoE,EAAO,SAAU,CACnC,IAAM/F,EAAS4B,EAAWD,EAAOvB,EAASD,EAAOoB,EAAUrB,CAAK,EAChEC,EAAM,KAAK,OAAOkG,CAAM,QAAQrG,EAAO,OAAO,EAAE,EAChDqG,EAASrG,EAAO,MAClB,CACA4F,EAAc,KAAKS,CAAM,EACrBN,EAAO,QACTF,EAAoBQ,EAExB,MACET,EAAc,KAAKI,CAAQ,EACvBD,EAAO,QACTF,EAAoBG,EAG1B,CAGA,OAAIH,EACK,CAAE,QAASL,EAAY,OAAQK,CAAkB,EAInD,CAAE,QAASL,EAAY,OAAQA,CAAW,CACnD,CAMA,SAAS1C,GACPV,EACAhC,EACAD,EACc,CACd,IAAM4C,EAAK,UAAUX,EAAK,UAAU,QAAQ,gBAAiB,GAAG,CAAC,IAAIrB,EAAe,EAAE,CAAC,GAGjFuF,EAAS,KAAKlE,EAAK,UAAU,MAAMA,EAAK,SAAS,GAGnDoB,EAAY,GAChB,OAAQpB,EAAK,YAAa,CACxB,IAAK,SACHoB,EAAY,UACZ,MACF,IAAK,SACHA,EAAY,UACZ,MACF,IAAK,QACHA,EAAY,UACZ,KACJ,CAGA,IAAM/C,EACJL,EAAQ,aAAegC,EAAK,aAAe,OACvC,IAAI1B,EAAe0B,EAAK,UAAU,CAAC,GACnC,GAGAmE,EAAenE,EAAK,qBAAuB,kBAAoB,GAG/DmB,EAAQ,GAAGC,CAAS,UAAUtC,EAAkBkB,EAAK,SAAS,CAAC,MAAMkE,CAAM,GAAGC,CAAY,GAAG9F,CAAM,GAGrGyD,EACJ,OAAI9B,EAAK,cAAgB,QACvB8B,EAAY,cACH9B,EAAK,cAAgB,SAC9B8B,EAAY,eAEZA,EAAY,SAId/D,EAAM,KAAK,OAAO4C,CAAE,MAAMQ,CAAK,SAASW,CAAS,EAAE,EAE5C,CAAE,QAASnB,EAAI,OAAQA,CAAG,CACnC,CE/xBA,IAAAyD,GAAiB,sBAGXC,GAAY,OAAO,WAAe,KAAe,WAAY,YAAc,OAAQ,WAAoC,QAAW,WAOxI,SAASC,GAAgBC,EAA2B,CAClD,IAAIC,EACJ,GAAIH,GAEFG,EADW,WAAyG,OACzG,KAAKD,CAAK,EAAE,SAAS,QAAQ,MACnC,CACL,IAAIE,EAAS,GACb,QAASC,EAAI,EAAGA,EAAIH,EAAM,OAAQG,IAChCD,GAAU,OAAO,aAAaF,EAAMG,CAAC,CAAC,EAExCF,EAAS,KAAKC,CAAM,CACtB,CACA,OAAOD,EACJ,QAAQ,MAAO,GAAG,EAClB,QAAQ,MAAO,GAAG,EAClB,QAAQ,MAAO,EAAE,CACtB,CAgCO,SAASG,EAAeC,EAAsB,CAGnD,IAAMC,EADc,IAAI,YAAY,EACN,OAAOD,CAAI,EAGnCE,EAAa,GAAAC,QAAK,QAAQF,CAAS,EAGzC,OAAOG,GAAgBF,CAAU,CACnC,CChDA,IAAMG,GAAoB,mBAuBnB,SAASC,GACdC,EACAC,EACAC,EACAC,EAAoD,CAAC,EAC7C,CACR,IAAMC,EAAUD,EAAQ,SAAWL,GAC7BO,EAAUC,EAAeJ,CAAI,EACnC,MAAO,GAAGE,CAAO,IAAIJ,CAAW,IAAIC,CAAM,IAAII,CAAO,EACvD,CAgBO,SAASE,GACdC,EACAP,EAAsB,MACtBE,EAA+B,CAAC,EACxB,CACR,IAAMM,EAAWC,EAAgB,EAC3BC,EAA+B,CACnC,YAAa,GACb,SAAU,GACV,cAAe,GACf,OAAQC,CACV,EAEMC,EAAcJ,EAAS,OAAOD,EAAIG,CAAa,EACrD,OAAOZ,GAAc,UAAWE,EAAQY,EAAaV,CAAO,CAC9D,CAeO,SAASW,GACdN,EACAL,EAA+B,CAAC,EACxB,CACR,OAAOI,GAAWC,EAAI,MAAOL,CAAO,CACtC,CCtBA,IAAMY,GAA0B,sBASzB,SAASC,GAAoBC,EAAsB,CAExD,MAAO,QADSC,EAAeD,CAAI,CACb,EACxB,CAMA,SAASE,GACPC,EACmB,CAEnB,GAAI,aAAcA,EAAS,CACzB,IAAMC,EAAaD,EACnB,MAAO,CACL,MAAOC,EAAW,aAClB,QAASA,EAAW,WACpB,MAAOA,EAAW,MAClB,IAAKA,EAAW,IAChB,MAAOA,EAAW,MAClB,OAAQA,EAAW,OACnB,MAAOA,EAAW,MAElB,UAAW,KACb,CACF,CAEA,OAAOD,CACT,CAKA,SAASE,GACPC,EACAH,EACQ,CACR,IAAMI,EAAmB,CAAC,EAG1B,OAAIJ,EAAQ,SACVI,EAAO,KAAK,WAAW,mBAAmBJ,EAAQ,OAAO,CAAC,EAAE,EAE1DA,EAAQ,OACVI,EAAO,KAAK,SAASJ,EAAQ,KAAK,EAAE,EAElCA,EAAQ,QAAU,QACpBI,EAAO,KAAK,SAASJ,EAAQ,KAAK,EAAE,EAElCA,EAAQ,SAAW,QACrBI,EAAO,KAAK,UAAUJ,EAAQ,MAAM,EAAE,EAEpCA,EAAQ,QAAU,SAAcA,EAAQ,QAAU,QAAaA,EAAQ,SAAW,SACpFI,EAAO,KAAK,SAASJ,EAAQ,KAAK,EAAE,EAIlCG,IAAW,OAASH,EAAQ,WAAaA,EAAQ,YAAc,QACjEI,EAAO,KAAK,QAAQJ,EAAQ,SAAS,EAAE,EAIrCG,IAAW,QACTH,EAAQ,KACVI,EAAO,KAAK,KAAK,EAEfJ,EAAQ,OAAS,CAACA,EAAQ,KAC5BI,EAAO,KAAK,SAASJ,EAAQ,KAAK,EAAE,EAElCA,EAAQ,WAAa,CAACA,EAAQ,KAChCI,EAAO,KAAK,WAAW,GAIpBA,EAAO,OAAS,EAAI,IAAIA,EAAO,KAAK,GAAG,CAAC,GAAK,EACtD,CA6BO,SAASC,GACdF,EACAN,EACAG,EAAuD,CAAC,EAChD,CACR,IAAMM,EAAaP,GAAiBC,CAAO,EACrCO,EAAUD,EAAW,SAAWX,GAChCa,EAAUZ,GAAoBC,CAAI,EAClCY,EAAcP,GAAiBC,EAAQG,CAAU,EACvD,MAAO,GAAGC,CAAO,IAAIJ,CAAM,IAAIK,CAAO,GAAGC,CAAW,EACtD,CAkBO,SAASC,GACdC,EACAR,EAA2B,MAC3BH,EAA6B,CAAC,EACtB,CACR,IAAMY,EAAWC,EAAgB,EAC3BC,EAA+B,CACnC,YAAa,GACb,SAAU,GACV,cAAe,GACf,OAAQC,CACV,EAEMC,EAAcJ,EAAS,OAAOD,EAAIG,CAAa,EACrD,OAAOT,GAAmBF,EAAQa,EAAahB,CAAO,CACxD,CAiBO,SAASiB,GACdN,EACAX,EAA6B,CAAC,EACtB,CACR,OAAOU,GAAgBC,EAAI,MAAOX,CAAO,CAC3C,CCtPO,SAASkB,GACdC,EACiB,CACjB,MAAO,CACL,cAAcC,EAAwB,CACpC,GAAID,EAAgB,WAAa,cAAe,CAC9C,GAAM,CAAE,SAAUE,EAAG,GAAGC,CAAQ,EAAIH,EACpC,OAAOI,GAAmBH,EAAIE,CAAO,CACvC,CAEA,OAAOE,GAAcJ,EAAI,CAAE,QAASD,EAAgB,OAAQ,CAAC,CAC/D,EAEA,WAAWC,EAAwB,CACjC,OAAOK,EAAWL,CAAE,CACtB,EAEA,eAAeA,EAAwB,CACrC,OAAIA,EAAG,KAAK,QAAU,QAAaA,EAAG,KAAK,UAAY,OAC9C,GAAGA,EAAG,KAAK,MAAQA,EAAG,KAAK,OAAO,KAEvCA,EAAG,KAAK,aAAe,OAClB,GAAGA,EAAG,KAAK,UAAU,KAEvB,aACT,CACF,CACF,CCxCA,IAAAM,EAAqC,mBCiCrC,SAASC,GAAkBC,EAA4B,CACrD,QAAWC,KAAQD,EAUjB,IANGC,EAAK,OAAS,YAAcA,EAAK,OAAS,QAAUA,EAAK,OAAS,aACnE,CAACA,EAAK,GAAG,WAAW,WAAW,GAK7BA,EAAK,OAAS,WAChB,MAAO,GAGX,MAAO,EACT,CAcO,SAASC,GACdF,EACAG,EAAmC,CAAC,EACxB,CAGZ,GAAIJ,GAAkBC,CAAK,EACzB,OAAOA,EAGT,GAAM,CAAE,aAAAI,EAAe,EAAG,SAAAC,EAAW,CAAE,EAAIF,EAGrCG,EAA8D,CAAC,EAC/DC,EAA4D,CAAC,EAEnE,QAAS,EAAI,EAAG,EAAIP,EAAM,OAAQ,IAAK,CACrC,IAAMC,EAAOD,EAAM,CAAC,EAChBC,EAAK,OAAS,QAAUA,EAAK,UAAY,OAC3CK,EAAgB,KAAK,CACnB,KAAAL,EACA,QAASA,EAAK,QACd,MAAOA,EAAK,OAASA,EAAK,SAAWA,EAAK,YAAc,GACxD,cAAe,CACjB,CAAC,EAGDM,EAAa,KAAK,CAAE,KAAAN,EAAM,cAAe,CAAE,CAAC,CAEhD,CAEA,GAAIK,EAAgB,QAAU,EAC5B,OAAON,EAITM,EAAgB,KAAK,CAACE,EAAGC,IAAMD,EAAE,QAAUC,EAAE,OAAO,EAIpD,IAAMC,EAAkC,CAAC,EACrCC,EAAsC,CAACL,EAAgB,CAAC,CAAC,EAE7D,QAAS,EAAI,EAAG,EAAIA,EAAgB,OAAQ,IAAK,CAC/C,IAAMM,EAAON,EAAgB,CAAC,EACxBO,EAAa,KAAK,IAAI,GAAGF,EAAa,IAAKG,GAAMA,EAAE,OAAO,CAAC,EAC3DC,EAAW,KAAK,IAAI,GAAGJ,EAAa,IAAKG,GAAMA,EAAE,KAAK,CAAC,EAKvDE,EAAkBJ,EAAK,SAAWC,EAAaR,EAC/CY,EAAiBL,EAAK,QAAUG,EAEtC,GAAI,CAACC,GAAmB,CAACC,EAAgB,CAEvCP,EAAO,KAAKC,CAAY,EACxBA,EAAe,CAACC,CAAI,EACpB,QACF,CAKA,IAAMM,EAAkBD,EACpB,KAAK,IAAIL,EAAK,MAAOG,CAAQ,EAAIH,EAAK,QACtC,EAIAI,GAAmBE,GAAmBd,EACxCO,EAAa,KAAKC,CAAI,GAGtBF,EAAO,KAAKC,CAAY,EACxBA,EAAe,CAACC,CAAI,EAExB,CACAF,EAAO,KAAKC,CAAY,EAGxB,IAAMQ,EAAuD,CAAC,EAE9D,QAAWC,KAASV,EAAQ,CAE1B,IAAMW,EAAW,KAAK,IAAI,GAAGD,EAAM,IAAKN,GAAMA,EAAE,aAAa,CAAC,EAE9D,GAAIM,EAAM,SAAW,EAEnBD,EAAa,KAAK,CAAE,KAAMC,EAAM,CAAC,EAAE,KAAM,SAAAC,CAAS,CAAC,MAC9C,CAEL,IAAMC,EAAWF,EAAM,IAAKN,GAAMA,EAAE,IAAI,EAClCS,EAAU,KAAK,IAAI,GAAGH,EAAM,IAAKN,GAAMA,EAAE,OAAO,CAAC,EACjDU,EAAQ,KAAK,IAAI,GAAGJ,EAAM,IAAKN,GAAMA,EAAE,KAAK,CAAC,EAE7CW,EAA6B,CACjC,KAAM,WACN,GAAI,qBAAqBF,CAAO,GAChC,KAAM,GAAGD,EAAS,MAAM,kBACxB,MAAOI,GAAiBJ,CAAQ,EAChC,KAAM,MACN,SAAAA,EACA,QAAAC,EACA,MAAAC,EACA,WAAYA,EAAQD,CACtB,EAEAJ,EAAa,KAAK,CAAE,KAAMM,EAAc,SAAAJ,CAAS,CAAC,CACpD,CACF,CAGA,OAAW,CAAE,KAAApB,EAAM,cAAA0B,CAAc,IAAKpB,EACpCY,EAAa,KAAK,CAAE,KAAAlB,EAAM,SAAU0B,CAAc,CAAC,EAIrD,OAAAR,EAAa,KAAK,CAACX,EAAGC,IAAMD,EAAE,SAAWC,EAAE,QAAQ,EAE5CU,EAAa,IAAKS,GAAMA,EAAE,IAAI,CACvC,CAKA,SAASF,GACPJ,EACoE,CAEpE,OADiBA,EAAS,KAAMO,GAAMA,EAAE,QAAU,OAAO,EACpC,QAEFP,EAAS,KAAMO,GAAMA,EAAE,QAAU,SAAS,EACtC,UAEJP,EAAS,KAAMO,GAAMA,EAAE,QAAU,SAAS,EACtC,WAEJP,EAAS,MACzBO,GAAMA,EAAE,QAAU,WAAaA,EAAE,QAAU,QAC9C,EACuB,UAGzB,CC9FO,SAASC,GAAgBC,EAA4B,CAAC,EAAG,CAC9D,GAAM,CACJ,eAAAC,EAAiB,GACjB,kBAAAC,EACA,gBAAiBC,EAAyB,GAC1C,aAAAC,EAAe,GACjB,EAAIJ,EAGAK,EAAkBF,EAIhBG,EAAoBC,EAAW,EACjCC,EACAC,EACAC,EACAC,EAA2B,UAC3BC,EACAC,EAGEC,EAAc,IAAI,IAGlBC,EAA4B,CAAC,EAG7BC,EAAkC,CAAC,EAGnCC,EAAgB,IAAI,IAGtBC,EAA2B,CAAC,EAG5BC,EAAY,KAAK,IAAI,EACrBC,EAAgBD,EAGhBE,EAA2B,CAC7B,YAAa,IAAI,GACnB,EAEIC,EAGEC,EAA0B,CAAC,EAC7BC,EAAa,EAOjB,SAASC,EAAUC,EAAqE,CACtF,OAAOA,EAAM,QAAUA,EAAM,SAAWA,EAAM,MAAQnB,EAAW,CACnE,CAQA,SAASoB,EAAQC,EAAsB,CAGrC,GAAIb,EAAW,OAAS,EAAG,CACzBA,EAAWA,EAAW,OAAS,CAAC,EAAE,SAAS,KAAKa,CAAI,EACpDR,EAAgB,KAAK,IAAI,EACzB,MACF,CAGA,GAAIJ,EAAc,OAAS,EAAG,CAC5B,IAAMa,EAAWb,EAAcA,EAAc,OAAS,CAAC,EAEvD,QAAWc,KAAUD,EAAS,SAAS,OAAO,EAC5C,GAAIC,EAAO,MAAO,CAChBA,EAAO,SAAS,KAAKF,CAAI,EACzBR,EAAgB,KAAK,IAAI,EACzB,MACF,CAGFS,EAAS,gBAAgB,KAAKD,CAAI,EAClCR,EAAgB,KAAK,IAAI,EACzB,MACF,CAGAF,EAAa,KAAKU,CAAI,EACtBR,EAAgB,KAAK,IAAI,CAC3B,CAMA,SAASW,GAAgBL,EAAqC,CAC5D,GAAI,CAACrB,EAAiB,OAGtB,IAAM2B,EAAKC,EAAM,EAGXC,EAAkB,IAAI,IAC5B,OAAW,CAACC,EAAIC,CAAI,IAAKtB,EACvBoB,EAAgB,IAAIC,EAAI,CACtB,GAAIC,EAAK,GACT,KAAMA,EAAK,KACX,IAAKA,EAAK,IACV,QAASA,EAAK,QACd,WAAYA,EAAK,WACjB,SAAUA,EAAK,SACf,UAAWA,EAAK,SAClB,CAAC,EAGH,IAAMC,EAAuB,CAC3B,GAAI,YAAYb,CAAU,GAC1B,WAAAA,EACA,MAAO,gBAAgBE,CAAK,EAC5B,GAAI,gBAAgBM,CAAE,EACtB,UAAW,KAAK,IAAI,EACpB,YAAaE,CACf,EAEAX,EAAU,KAAKc,CAAQ,EAGnBd,EAAU,OAASnB,GACrBmB,EAAU,MAAM,EAGlBC,GACF,CAKA,SAASc,GAAYZ,EAAqC,CACxD,OAAQA,EAAM,KAAM,CAClB,IAAK,iBAAkB,CAErBR,EAAe,CAAC,EAChBR,EAAgB,OAChBG,EAAqB,OACrBD,EAAgB,OAChBE,EAAY,MAAM,EAClBC,EAAW,OAAS,EACpBC,EAAc,OAAS,EACvBC,EAAc,MAAM,EAEpBT,EAAakB,EAAM,WACnBjB,EAAkBiB,EAAM,GACxBf,EAAgB,UAChBQ,EAAY,KAAK,IAAI,EACrBC,EAAgBD,EAEZG,IAA2B,QAAaA,IAA2BI,EAAM,aAC3EL,EAAU,UAAY,OACtBA,EAAU,cAAgB,QAE5BC,EAAyBI,EAAM,WAC/BL,EAAU,YAAc,IAAI,IAC5B,KACF,CAEA,IAAK,mBACHV,EAAgB,UAChBD,EAAgBgB,EAAM,GACtBb,EAAqBa,EAAM,WAC3BN,EAAgB,KAAK,IAAI,EACzB,MAEF,IAAK,iBACHT,EAAgB,QAChBD,EAAgBgB,EAAM,GACtBd,EAAgBc,EAAM,MACtBb,EAAqBa,EAAM,WAC3BN,EAAgB,KAAK,IAAI,EACzB,MAEF,IAAK,qBACHT,EAAgB,UAChBD,EAAgBgB,EAAM,GACtBb,EAAqBa,EAAM,WAC3BN,EAAgB,KAAK,IAAI,EACzB,MAEF,IAAK,aAAc,CACjB,IAAMe,EAAKV,EAAUC,CAAK,EAC1BZ,EAAY,IAAIqB,EAAI,CAClB,GAAAA,EACA,KAAMT,EAAM,KACZ,IAAKA,EAAM,QACX,QAASA,EAAM,GACf,WAAY,EACZ,SAAU,GACV,SAAWA,EAAc,QAC3B,CAAC,EACDN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,eAAgB,CACnB,IAAMe,EAAKV,EAAUC,CAAK,EACpBa,EAASzB,EAAY,IAAIqB,CAAE,EACjC,GAAII,EAAQ,CACV,IAAMX,EAAiB,CACrB,KAAM,OACN,GAAIW,EAAO,GACX,KAAMA,EAAO,KACb,IAAKA,EAAO,IACZ,MAAO,UACP,QAASA,EAAO,QAChB,MAAOb,EAAM,GACb,WAAYA,EAAM,WAClB,GAAIa,EAAO,WAAa,GAAK,CAAE,WAAYA,EAAO,UAAW,EAC7D,GAAIA,EAAO,UAAY,CAAE,SAAU,GAAM,UAAWA,EAAO,SAAU,EACrE,GAAIA,EAAO,UAAY,CAAE,SAAUA,EAAO,QAAS,CACrD,EACAZ,EAAQC,CAAI,EACZd,EAAY,OAAOqB,CAAE,CACvB,CACA,KACF,CAEA,IAAK,aAAc,CACjB,IAAMA,EAAKV,EAAUC,CAAK,EACpBa,EAASzB,EAAY,IAAIqB,CAAE,EACjC,GAAII,EAAQ,CACV,IAAMX,EAAiB,CACrB,KAAM,OACN,GAAIW,EAAO,GACX,KAAMA,EAAO,KACb,IAAKA,EAAO,IACZ,MAAO,QACP,QAASA,EAAO,QAChB,MAAOb,EAAM,GACb,WAAYA,EAAM,WAClB,MAAOA,EAAM,MACb,GAAIa,EAAO,WAAa,GAAK,CAAE,WAAYA,EAAO,UAAW,EAC7D,GAAIA,EAAO,UAAY,CAAE,SAAU,GAAM,UAAWA,EAAO,SAAU,EACrE,GAAIA,EAAO,UAAY,CAAE,SAAUA,EAAO,QAAS,EACnD,GAAKb,EAAc,aAAe,CAAE,iBAAmBA,EAAc,WAAY,CACnF,EACAC,EAAQC,CAAI,EACZd,EAAY,OAAOqB,CAAE,CACvB,CACA,KACF,CAEA,IAAK,eAAgB,CACnB,IAAMA,EAAKV,EAAUC,CAAK,EACpBa,EAASzB,EAAY,IAAIqB,CAAE,EACjC,GAAII,EAAQ,CACV,IAAMX,EAAiB,CACrB,KAAM,OACN,GAAIW,EAAO,GACX,KAAMA,EAAO,KACb,IAAKA,EAAO,IACZ,MAAO,UACP,QAASA,EAAO,QAChB,MAAOb,EAAM,GACb,WAAYA,EAAM,WAClB,GAAIa,EAAO,WAAa,GAAK,CAAE,WAAYA,EAAO,UAAW,EAC7D,GAAIA,EAAO,UAAY,CAAE,SAAU,GAAM,UAAWA,EAAO,SAAU,EACrE,GAAIA,EAAO,UAAY,CAAE,SAAUA,EAAO,QAAS,CACrD,EACAZ,EAAQC,CAAI,EACZd,EAAY,OAAOqB,CAAE,CACvB,CACA,KACF,CAEA,IAAK,iBAAkB,CAErB,IAAMP,EAAiB,CACrB,KAAM,OACN,GAHSH,EAAUC,CAAK,EAIxB,KAAMA,EAAM,KACZ,IAAKA,EAAM,QACX,MAAO,SACP,QAASA,EAAM,GACf,MAAOA,EAAM,GACb,WAAY,CACd,EACAC,EAAQC,CAAI,EACZ,KACF,CAEA,IAAK,kBAGH,MAEF,IAAK,gBAGH,MAEF,IAAK,eAAgB,CAGnB,IAAMO,EAAKV,EAAUC,CAAK,EACpBa,EAASzB,EAAY,IAAIqB,CAAE,EAC7BI,IACFA,EAAO,SAAW,GAClBA,EAAO,UAAYb,EAAM,WAE3BN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,aAAc,CAEjB,IAAMe,EAAKV,EAAUC,CAAK,EACpBa,EAASzB,EAAY,IAAIqB,CAAE,EAC7BI,IACFA,EAAO,YAAcb,EAAM,SAAW,GAAK,GAE7CN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,yBAGHA,EAAgB,KAAK,IAAI,EACzB,MAEF,IAAK,eAAgB,CAEnB,IAAMQ,EAAiB,CACrB,KAAM,OACN,GAHSH,EAAUC,CAAK,EAIxB,KAAMA,EAAM,KACZ,IAAKA,EAAM,QACX,MAAO,UACP,QAASA,EAAM,GACf,MAAOA,EAAM,GACb,WAAY,EACZ,GAAKA,EAAc,UAAY,CAAE,SAAWA,EAAc,QAAS,CACrE,EACAC,EAAQC,CAAI,EACZ,KACF,CAGA,IAAK,WAAY,CACf,IAAMY,EAAId,EAaV,GAAIc,EAAE,QAAU,QAAS,CACvBC,EAAoB,CAClB,KAAM,iBACN,WAAaf,EAAiC,WAC9C,WAAYc,EAAE,WACd,KAAMA,EAAE,OAASA,EAAE,WACnB,UAAWA,EAAE,MACb,cAAeA,EAAE,MACjB,GAAIA,EAAE,EACR,CAAC,EACDC,EAAoB,CAClB,KAAM,kBACN,WAAaf,EAAiC,WAC9C,WAAYc,EAAE,WACd,YAAa,OACb,UAAWA,EAAE,MACb,MAAOA,EAAE,SAAW,OACpB,GAAIA,EAAE,EACR,CAAC,EACDC,EAAoB,CAClB,KAAM,kBACN,WAAaf,EAAiC,WAC9C,WAAYc,EAAE,WACd,YAAa,OACb,MAAOA,EAAE,SAAW,OACpB,GAAIA,EAAE,EACR,CAAC,EACD,KACF,CACA,GAAIA,EAAE,QAAU,MAAO,CACrBC,EAAoB,CAClB,KAAM,eACN,WAAaf,EAAiC,WAC9C,WAAYc,EAAE,WACd,YAAaA,EAAE,OACf,GAAIA,EAAE,GACN,WAAYA,EAAE,YAAc,CAC9B,CAAC,EACD,KACF,CAMA,IAAMZ,EAAqB,CACzB,KAAM,WACN,GAAIY,EAAE,WACN,KAAMA,EAAE,OAASA,EAAE,WACnB,MAAO,UACP,QAASA,EAAE,GACX,MAAOA,EAAE,GACT,WAAY,EACZ,UAAWA,EAAE,MACb,cAAeA,EAAE,MACjB,YAAaA,EAAE,OACf,SAAU,CACR,CAAE,MAAO,OAAQ,UAAWA,EAAE,MAAO,MAAOA,EAAE,SAAW,OAAQ,SAAU,CAAC,CAAE,EAC9E,CAAE,MAAO,OAAQ,MAAOA,EAAE,SAAW,OAAQ,SAAU,CAAC,CAAE,CAC5D,CACF,EACAb,EAAQC,CAAI,EACZR,EAAgB,KAAK,IAAI,EACzB,KACF,CAGA,IAAK,kBAAmB,CACtB,IAAMsB,EAA0B,CAC9B,KAAM,YACN,MAAO,UACP,GAAIhB,EAAM,GACV,WAAYA,EAAM,WAClB,QAAS,CACP,OAAQA,EAAM,OACd,QAASA,EAAM,OACjB,CACF,EACAL,EAAU,UAAYqB,EACtBpB,EAAyBI,EAAM,WAC/BN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,wBAAyB,CAC5B,IAAMsB,EAA0B,CAC9B,KAAM,YACN,MAAO,QACP,GAAIhB,EAAM,GACV,WAAYA,EAAM,WAClB,MAAOA,EAAM,KACf,EACAL,EAAU,UAAYqB,EACtBpB,EAAyBI,EAAM,WAC/BN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,oBAAqB,CACxB,IAAMsB,EAA0B,CAC9B,KAAM,gBACN,MAAO,UACP,GAAIhB,EAAM,GACV,WAAYA,EAAM,WAClB,QAAS,CACP,OAAQA,EAAM,OACd,QAASA,EAAM,OACjB,CACF,EACAL,EAAU,cAAgBqB,EAC1BpB,EAAyBI,EAAM,WAC/BN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,0BAA2B,CAC9B,IAAMsB,EAA0B,CAC9B,KAAM,gBACN,MAAO,QACP,GAAIhB,EAAM,GACV,WAAYA,EAAM,WAClB,MAAOA,EAAM,KACf,EACAL,EAAU,cAAgBqB,EAC1BpB,EAAyBI,EAAM,WAC/BN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,kBAAmB,CACtB,IAAMsB,EAA0B,CAC9B,KAAM,cACN,MAAO,UACP,GAAIhB,EAAM,GACV,WAAYA,EAAM,WAClB,QAAS,CACP,QAASA,EAAM,OACjB,CACF,EACAL,EAAU,YAAY,IAAIK,EAAM,QAASgB,CAAQ,EACjDtB,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,wBAAyB,CAC5B,IAAMsB,EAA0B,CAC9B,KAAM,cACN,MAAO,QACP,GAAIhB,EAAM,GACV,WAAYA,EAAM,WAClB,MAAOA,EAAM,MACb,QAAS,CACP,QAASA,EAAM,OACjB,CACF,EACAL,EAAU,YAAY,IAAIK,EAAM,QAASgB,CAAQ,EACjDtB,EAAgB,KAAK,IAAI,EACzB,KACF,CAGA,IAAK,iBAAkB,CACrB,IAAMuB,EAAY,GAAGjB,EAAM,UAAU,IAAIA,EAAM,SAAS,GACxDT,EAAc,IAAI0B,EAAW,CAC3B,GAAIpC,EAAW,EACf,UAAWmB,EAAM,UACjB,QAASA,EAAM,GACf,WAAY,EACZ,UAAW,EACX,YAAa,SACb,qBAAsB,GACtB,cAAe,CACjB,CAAC,EACDN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,eAAgB,CACnB,IAAMuB,EAAY,GAAGjB,EAAM,UAAU,IAAIA,EAAM,SAAS,GAClDkB,EAAS3B,EAAc,IAAI0B,CAAS,EACtCC,IACFA,EAAO,aACPA,EAAO,cAAgB,KAAK,IAAIA,EAAO,cAAelB,EAAM,QAAQ,GAEtEN,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,cAAe,CAClB,IAAMuB,EAAY,GAAGjB,EAAM,UAAU,IAAIA,EAAM,SAAS,GAClDkB,EAAS3B,EAAc,IAAI0B,CAAS,EACtCC,GACFA,EAAO,YAETxB,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,eAAgB,CACnB,IAAMuB,EAAY,GAAGjB,EAAM,UAAU,IAAIA,EAAM,SAAS,GAClDkB,EAAS3B,EAAc,IAAI0B,CAAS,EAC1C,GAAIC,EAAQ,CACVA,EAAO,YAAc,SACrBA,EAAO,cAAgBlB,EAAM,cAE7B,IAAME,EAAmB,CACvB,KAAM,SACN,GAAIgB,EAAO,GACX,UAAWA,EAAO,UAClB,MAAO,UACP,QAASA,EAAO,QAChB,MAAOlB,EAAM,GACb,WAAYA,EAAM,GAAKkB,EAAO,QAC9B,WAAYA,EAAO,WACnB,UAAWA,EAAO,UAClB,cAAelB,EAAM,cACrB,YAAa,SACb,qBAAsBkB,EAAO,oBAC/B,EACAjB,EAAQC,CAAI,EACZX,EAAc,OAAO0B,CAAS,CAChC,CACAvB,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,eAAgB,CACnB,IAAMuB,EAAY,GAAGjB,EAAM,UAAU,IAAIA,EAAM,SAAS,GAClDkB,EAAS3B,EAAc,IAAI0B,CAAS,EAC1C,GAAIC,EAAQ,CACVA,EAAO,YAAc,QAErB,IAAMhB,EAAmB,CACvB,KAAM,SACN,GAAIgB,EAAO,GACX,UAAWA,EAAO,UAClB,MAAO,QACP,MAAOlB,EAAM,MACb,QAASkB,EAAO,QAChB,MAAOlB,EAAM,GACb,WAAYA,EAAM,GAAKkB,EAAO,QAC9B,WAAYA,EAAO,WACnB,UAAWA,EAAO,UAClB,cAAelB,EAAM,SACrB,YAAa,QACb,qBAAsBkB,EAAO,oBAC/B,EACAjB,EAAQC,CAAI,EACZX,EAAc,OAAO0B,CAAS,CAChC,CACAvB,EAAgB,KAAK,IAAI,EACzB,KACF,CAEA,IAAK,sBAAuB,CAC1B,IAAMuB,EAAY,GAAGjB,EAAM,UAAU,IAAIA,EAAM,SAAS,GAClDkB,EAAS3B,EAAc,IAAI0B,CAAS,EACtCC,IACFA,EAAO,qBAAuB,IAEhCxB,EAAgB,KAAK,IAAI,EACzB,KACF,CACF,CAGAW,GAAgBL,CAAK,CACvB,CAKA,SAASmB,GAAiBnB,EAA8C,CACtE,GAAIA,EAAM,OAAS,cACjBX,EAAW,KAAK,CACd,GAAIW,EAAM,QACV,KAAMA,EAAM,KACZ,KAAMA,EAAM,UACZ,QAASA,EAAM,GACf,SAAU,CAAC,CACb,CAAC,EACDN,EAAgB,KAAK,IAAI,UAChBM,EAAM,OAAS,YAAa,CAErC,IAAMoB,EAAa/B,EAAW,UAAWgC,GAAMA,EAAE,KAAOrB,EAAM,OAAO,EACrE,GAAIoB,IAAe,GAEjB,OAKF,KAAO/B,EAAW,OAAS+B,EAAa,GAAG,CACzC,IAAME,EAAcjC,EAAW,IAAI,EAC7BkC,EACJD,EAAY,OAAS,OACjB,CACE,KAAM,OACN,GAAIA,EAAY,GAChB,KAAMA,EAAY,KAClB,MAAOE,EAAYF,EAAY,QAAQ,EACvC,QAASA,EAAY,QACrB,MAAOtB,EAAM,GACb,SAAUsB,EAAY,QACxB,EACA,CACE,KAAM,WACN,GAAIA,EAAY,GAChB,KAAMA,EAAY,KAClB,MAAOE,EAAYF,EAAY,QAAQ,EACvC,QAASA,EAAY,QACrB,MAAOtB,EAAM,GACb,SAAUsB,EAAY,SACtB,KAAMA,EAAY,OAAS,aAAe,aAAe,KAC3D,EAENjC,EAAWA,EAAW,OAAS,CAAC,EAAE,SAAS,KAAKkC,CAAU,CAC5D,CAGA,GAAM,CAACE,CAAK,EAAIpC,EAAW,OAAO+B,EAAY,CAAC,EAEzClB,EACJuB,EAAM,OAAS,OACX,CACE,KAAM,OACN,GAAIA,EAAM,GACV,KAAMA,EAAM,KACZ,MAAOD,EAAYC,EAAM,QAAQ,EACjC,QAASA,EAAM,QACf,MAAOzB,EAAM,GACb,WAAYA,EAAM,WAClB,SAAUyB,EAAM,SAChB,SAAUzB,EAAM,QAClB,EACA,CACE,KAAM,WACN,GAAIyB,EAAM,GACV,KAAMA,EAAM,KACZ,MAAOD,EAAYC,EAAM,QAAQ,EACjC,QAASA,EAAM,QACf,MAAOzB,EAAM,GACb,WAAYA,EAAM,WAClB,SAAUyB,EAAM,SAChB,KAAMA,EAAM,OAAS,aAAe,aAAe,KACrD,EACNxB,EAAQC,CAAI,CACd,CACF,CAKA,SAASa,EACPf,EACM,CACN,GAAIA,EAAM,OAAS,iBACjBV,EAAc,KAAK,CACjB,GAAIU,EAAM,WACV,KAAMA,EAAM,KACZ,UAAWA,EAAM,UACjB,cAAeA,EAAM,cACrB,QAASA,EAAM,GACf,SAAU,IAAI,IACd,gBAAiB,CAAC,CACpB,CAAC,EACDN,EAAgB,KAAK,IAAI,UAChBM,EAAM,OAAS,kBAAmB,CAE3C,IAAMG,EAAWb,EAAc,KAAMoC,GAAMA,EAAE,KAAO1B,EAAM,UAAU,EACpE,GAAIG,EAAU,CAEZ,IAAMwB,EAAY3B,EAAM,YAClB4B,EAAWzB,EAAS,SAAS,IAAIwB,CAAS,EAChD,GAAIC,EAEFA,EAAS,MAAQ5B,EAAM,MAEnBA,EAAM,OAASG,EAAS,gBAAgB,OAAS,IACnDyB,EAAS,SAAS,QAAQ,GAAGzB,EAAS,eAAe,EACrDA,EAAS,gBAAkB,CAAC,OAEzB,CAEL,IAAM0B,EAAW7B,EAAM,MAAQ,CAAC,GAAGG,EAAS,eAAe,EAAI,CAAC,EAC5DH,EAAM,QACRG,EAAS,gBAAkB,CAAC,GAE9BA,EAAS,SAAS,IAAIwB,EAAW,CAC/B,MAAO3B,EAAM,YACb,UAAWA,EAAM,UACjB,MAAOA,EAAM,MACb,SAAA6B,CACF,CAAC,CACH,CACAnC,EAAgB,KAAK,IAAI,CAC3B,CACF,SAAWM,EAAM,OAAS,eAAgB,CAExC,IAAM8B,EAAQxC,EAAc,UAAWoC,GAAMA,EAAE,KAAO1B,EAAM,UAAU,EACtE,GAAI8B,IAAU,GAAI,CAChB,GAAM,CAAC3B,CAAQ,EAAIb,EAAc,OAAOwC,EAAO,CAAC,EAE1CC,EAAYzC,EAAc,OAAOwC,CAAK,EAGxCE,EAA6B,MAAM,KAAK7B,EAAS,SAAS,OAAO,CAAC,EAClE6B,EAAS,SAAW,GAAK7B,EAAS,gBAAgB,OAAS,IAC7D6B,EAAW,CACT,CACE,MAAO,UACP,MAAO,GACP,SAAU,CAAC,GAAG7B,EAAS,eAAe,CACxC,CACF,GAIF,IAAM8B,EAAsBD,EAAS,KAAME,GAAMA,EAAE,KAAK,GAAG,MAErDhC,GAAqB,CACzB,KAAM,WACN,GAAIC,EAAS,GACb,KAAMA,EAAS,KACf,MAAOqB,EACLQ,EAAS,QAASE,GAAOA,EAAE,MAAQA,EAAE,SAAW,CAAC,CAAE,CACrD,EACA,QAAS/B,EAAS,QAClB,MAAOH,EAAM,GACb,WAAYA,EAAM,WAClB,UAAWG,EAAS,UACpB,cAAeA,EAAS,cACxB,YAAaH,EAAM,aAAeiC,EAClC,SAAAD,CACF,EACA/B,EAAQC,EAAI,EACZZ,EAAc,KAAK,GAAGyC,CAAS,EAC/BrC,EAAgB,KAAK,IAAI,CAC3B,CACF,CACF,CAKA,SAAS8B,EAAYK,EAAiC,CACpD,OAAIA,EAAS,SAAW,EAAU,UAEjBA,EAAS,KAAMM,GAAMA,EAAE,QAAU,OAAO,EACpC,QAEFN,EAAS,MACzBM,GAAMA,EAAE,QAAU,WAAaA,EAAE,QAAU,QAC9C,EACuB,UAEJN,EAAS,KAAMM,GAAMA,EAAE,QAAU,SAAS,EACtC,UAEhB,SACT,CAKA,SAASC,IAA8B,CACrC,IAAMC,EAAQ,CAAC,GAAG7C,CAAY,EAG9B,OAAW,CAAC,CAAEqB,CAAM,IAAKzB,EACvBiD,EAAM,KAAK,CACT,KAAM,OACN,GAAIxB,EAAO,GACX,KAAMA,EAAO,KACb,IAAKA,EAAO,IACZ,MAAO,UACP,QAASA,EAAO,QAChB,GAAIA,EAAO,WAAa,GAAK,CAAE,WAAYA,EAAO,UAAW,EAC7D,GAAIA,EAAO,UAAY,CAAE,SAAU,GAAM,UAAWA,EAAO,SAAU,CACvE,CAAC,EAIH,OAAW,CAAC,CAAEK,CAAM,IAAK3B,EACvB8C,EAAM,KAAK,CACT,KAAM,SACN,GAAInB,EAAO,GACX,UAAWA,EAAO,UAClB,MAAO,UACP,QAASA,EAAO,QAChB,WAAYA,EAAO,WACnB,UAAWA,EAAO,UAClB,cAAeA,EAAO,cACtB,YAAaA,EAAO,YACpB,qBAAsBA,EAAO,oBAC/B,CAAsB,EAGxB,OAAOmB,CACT,CAKA,SAAS9B,GAAoB,CAC3B,IAAIsB,EAAWO,GAAgB,EAG3B7D,IACFsD,EAAWS,GAAqBT,EAAUrD,CAAiB,GAG7D,IAAM+D,EAAqB,CACzB,KAAM,WACN,GAAIzD,GAAcF,EAClB,WAAYE,GAAcF,EAC1B,MAAOK,EACP,QAASF,EACT,MAAOC,EACP,WAAYG,EACZ,SAAA0C,EACA,MAAO3C,CACT,EAGMsD,EACJ7C,EAAU,YAAc,QACxBA,EAAU,gBAAkB,QAC5BA,EAAU,YAAY,KAAO,EAE/B,MAAO,CACL,KAAA4C,EACA,SAAU,CACR,UAAA9C,EACA,cAAAC,CACF,EACA,GAAI8C,GAAY,CAAE,MAAO7C,CAAU,CACrC,CACF,CAKA,SAAS8C,IAAc,CACrB3D,EAAa,OACbC,EAAkB,OAClBC,EAAgB,OAChBC,EAAgB,UAChBC,EAAgB,OAChBC,EAAqB,OACrBC,EAAY,MAAM,EAClBC,EAAW,OAAS,EACpBC,EAAc,OAAS,EACvBC,EAAc,MAAM,EACpBC,EAAe,CAAC,EAChBC,EAAY,KAAK,IAAI,EACrBC,EAAgBD,EAEhBE,EAAY,CACV,YAAa,IAAI,GACnB,EACAC,EAAyB,OAEzBC,EAAU,OAAS,EACnBC,EAAa,CACf,CAKA,SAAS4C,IAA6B,CACpC,MAAO,CAAC,GAAG7C,CAAS,CACtB,CAKA,SAAS8C,GAAcb,EAAuC,CAC5D,OAAOjC,EAAUiC,CAAK,CACxB,CAKA,SAASc,GAAQd,EAAuC,CACtD,OAAOjC,EAAUiC,CAAK,GAAG,EAC3B,CAKA,SAASe,IAAuB,CAC9BhD,EAAU,OAAS,EACnBC,EAAa,CACf,CAEA,MAAO,CACL,YAAAc,GACA,iBAAAO,GACA,oBAAAJ,EACA,MAAAR,EACA,MAAAkC,GAEA,aAAAC,GACA,cAAAC,GACA,QAAAC,GACA,eAAAC,GAEA,IAAI,gBAAiB,CACnB,OAAOzD,EAAY,KAAO,CAC5B,EAEA,IAAI,OAAQ,CACV,OAAOH,CACT,EAEA,IAAI,eAAgB,CAClB,OAAOY,EAAU,MACnB,EAEA,IAAI,kBAAmB,CACrB,OAAOlB,CACT,EAEA,oBAAoBmE,EAAwB,CAC1CnE,EAAkBmE,CACpB,CACF,CACF,CFnjCA,IAAMC,GAAsB,IAKtBC,GAAsB,IAmCrB,SAASC,GACdC,EACAC,EACa,CACb,GAAM,CACJ,MAAAC,EACA,WAAAC,EAAaN,GACb,UAAAO,EAAYN,EACd,EAAIE,EAGAK,EACAC,EAAS,GACTC,EAEAC,EACAC,EACAC,EACAC,EACAC,EAEAC,KAA4C,MAAG,MAAS,EAGtDC,EAAYC,GAAgB,CAAE,eAAgB,EAAK,CAAC,EAE1D,SAASC,GAAoB,CACvBP,IACF,aAAaA,CAAa,EAC1BA,EAAgB,QAEdC,IACF,aAAaA,CAAY,EACzBA,EAAe,OAEnB,CAEA,eAAeO,GAA2C,CACxD,GAAI,CAACV,GAAa,CAACD,EAAQ,SAAO,MAAG,MAAS,EAE9C,IAAMY,EAAWX,EACjBC,EAASU,EACTX,EAAY,OACZS,EAAY,EAEZ,GAAI,CACF,OAAKX,EAKH,MAAMJ,EAAU,eAAeI,EAAWa,EAAUhB,CAAK,EAHzDG,EAAY,MAAMJ,EAAU,QAAQiB,EAAUhB,CAAK,EAKrDS,EAAe,KAAK,IAAI,EACxBE,KAAkB,MAAG,MAAS,EACvBA,CACT,MAAQ,CAEN,OAAAA,KAAkB,OAAI,gBAAgB,EAC/BA,CACT,CACF,CAEA,SAASM,GAAsB,CAEzBV,GACF,aAAaA,CAAa,EAI5BA,EAAgB,WAAW,IAAM,CAC1BQ,EAAM,CACb,EAAGd,CAAU,EAIb,IAAMiB,EAAgBT,GAAgBC,EACtC,GAAI,CAACF,GAAgBU,IAAkB,OAAW,CAChD,IAAMC,EAAqB,KAAK,IAAI,EAAID,EAClCE,EAAmB,KAAK,IAAI,EAAGlB,EAAYiB,CAAkB,EAEnEX,EAAe,WAAW,IAAM,CAC9BA,EAAe,OACVO,EAAM,CACb,EAAGK,CAAgB,CACrB,CACF,CAEA,SAASC,EAAOC,EAA6C,CAC3D,GAAKlB,EAQL,IALIM,IAAoB,SACtBA,EAAkB,KAAK,IAAI,GAIzB,SAAUY,EAEZjB,EAAYiB,EACZhB,EAASgB,MACJ,CAEL,IAAMC,EAAQD,EACVC,EAAM,OAAS,eAAiBA,EAAM,OAAS,YACjDX,EAAU,iBAAiBW,CAAwC,EAEnEA,EAAM,OAAS,kBACfA,EAAM,OAAS,mBACfA,EAAM,OAAS,eAEfX,EAAU,oBAAoBW,CAAoE,EAElGX,EAAU,YAAYW,CAA+B,EAEvDlB,EAAYO,EAAU,MAAM,EAC5BN,EAASD,CACX,CAEAY,EAAc,EAChB,CAEA,eAAeO,EAASC,EAAyB,YAA4B,CAC3E,GAAI,CAACrB,EAAQ,OAEbA,EAAS,GACTU,EAAY,EAGZ,IAAMY,EAAUrB,GAAaC,GAAUM,EAAU,MAAM,EAEvD,MAAMb,EAAU,SAASI,EAAWuB,EAAS1B,EAAOyB,CAAM,CAC5D,CAEA,SAASE,GAAe,CACjBvB,IAELA,EAAS,GACTU,EAAY,EAERX,GAAaJ,EAAU,QACpBA,EAAU,OAAOI,CAAS,EAEnC,CAEA,SAASyB,GAAmC,CAC1C,OAAOzB,CACT,CAEA,SAAS0B,GAAoB,CAC3B,OAAOzB,CACT,CAEA,MAAO,CACL,OAAAiB,EACA,SAAAG,EACA,OAAAG,EACA,aAAAC,EACA,SAAAC,CACF,CACF,CZxNA,SAASC,GAAcC,EAA4C,CACjE,OAAQA,EAAG,KAAK,MAAO,CACrB,IAAK,UACH,MAAO,YACT,IAAK,QACH,MAAO,SACT,IAAK,UACH,MAAO,YACT,QACE,MAAO,SACX,CACF,CAkBO,SAASC,GACdC,EACAC,EACU,CACV,GAAM,CAAE,gBAAAC,EAAiB,WAAAC,EAAa,IAAK,UAAAC,EAAY,GAAK,EAAIJ,EAGhE,GAAI,CAACE,EACH,MAAM,IAAI,MACR,GAAGD,EAAQ,IAAI,mGAEjB,EAIF,IAAMI,EAAMC,GAAsBJ,CAAe,EAEjD,eAAeK,EACbT,EACAU,EAA+B,CAAC,EACc,CAC9C,IAAMC,EAAQD,EAAc,OAAS,WAC/BE,EAASb,GAAcC,CAAE,EAE/B,GAAI,CACF,IAAMa,EAAUV,EAAQ,aAAa,CAAE,GAAAH,EAAI,MAAAW,EAAO,OAAAC,CAAO,EAAGL,CAAG,EACzDO,EAAY,MAAMX,EAAQ,QAAQU,CAAO,EAC/C,SAAO,MAAGC,GAAW,SAAS,CAAC,CACjC,MAAQ,CACN,SAAO,OAAI,aAAa,CAC1B,CACF,CAEA,SAASC,EAAWC,EAAiD,CACnE,OAAOC,GACL,CACE,GAAGD,EACH,WAAYA,EAAe,YAAcX,EACzC,UAAWW,EAAe,WAAaV,CACzC,EACA,CACE,MAAM,QAAQN,EAAIW,EAAO,CACvB,IAAME,EAAUV,EAAQ,aACtB,CAAE,GAAAH,EAAI,MAAAW,EAAO,OAAQ,SAAU,EAC/BJ,CACF,EAEA,OADkB,MAAMJ,EAAQ,QAAQU,CAAO,IAC7B,SAAS,CAC7B,EAEA,MAAM,eAAeC,EAAWd,EAAIW,EAAO,CACzC,IAAME,EAAUV,EAAQ,aACtB,CAAE,GAAAH,EAAI,MAAAW,EAAO,OAAQ,SAAU,EAC/BJ,CACF,EACA,MAAMJ,EAAQ,WAAWW,EAAyBD,CAAO,CAC3D,EAEA,MAAM,SAASC,EAAWd,EAAIW,EAAOC,EAAQ,CAC3C,IAAMC,EAAUV,EAAQ,aAAa,CAAE,GAAAH,EAAI,MAAAW,EAAO,OAAAC,CAAO,EAAGL,CAAG,EAC3DO,EACF,MAAMX,EAAQ,WAAWW,EAAyBD,CAAO,EAEzD,MAAMV,EAAQ,QAAQU,CAAO,CAEjC,EAEA,MAAM,OAAOC,EAAW,CAClBX,EAAQ,YAAcW,GACxB,MAAMX,EAAQ,WAAWW,CAAuB,CAEpD,CACF,CACF,CACF,CAEA,MAAO,CACL,OAAAL,EACA,WAAAM,EACA,QAAS,IAAMZ,EAAQ,KAAK,YAAY,CAC1C,CACF,CehIA,IAAAe,EAA0C,mBAkDpCC,GAA4D,CAChE,QAAS,QACT,UAAW,QACX,OAAQ,SACR,UAAW,OACb,EAQO,SAASC,GACdC,EACyB,CACzB,GAAM,CAAE,WAAAC,EAAY,SAAAC,EAAU,UAAAC,EAAW,QAAAC,EAAU,GAAM,EAAIJ,EAG7D,GAAI,CAACC,EAAW,SAAS,2BAA2B,EAClD,MAAM,IAAI,MAAM,6BAA6B,EAG/C,eAAeI,EACbC,EACAC,EAC+C,CAC/C,IAAMC,EAAa,IAAI,gBACjBC,EAAY,WAAW,IAAMD,EAAW,MAAM,EAAGJ,CAAO,EAE9D,GAAI,CAEF,IAAMM,EAAMH,EACR,GAAGN,CAAU,aAAaM,CAAS,GACnC,GAAGN,CAAU,aAIXU,EAAW,MAAM,MAAMD,EAAK,CAChC,OAHaH,EAAY,QAAU,OAInC,QAAS,CACP,eAAgB,kBAClB,EACA,KAAM,KAAK,UAAUD,CAAO,EAC5B,OAAQE,EAAW,MACrB,CAAC,EAED,GAAI,CAACG,EAAS,GACZ,SAAO,OAAI,aAAa,EAI1B,IAAMC,EAAQ,MAAMD,EAAS,KAAK,EAClC,SAAO,MAAGC,EAAK,EAAE,CACnB,OAASC,EAAO,CACd,OAAIA,aAAiB,OAASA,EAAM,OAAS,gBACpC,OAAI,SAAS,KAEf,OAAI,aAAa,CAC1B,QAAE,CACA,aAAaJ,CAAS,CACxB,CACF,CAEA,MAAO,CACL,KAAM,UAEN,aACE,CACE,GAAAK,EACA,MAAAC,EACA,OAAAC,CACF,EACAC,EACgB,CAChB,IAAMC,EAAsB,CAC1B,MAAAH,EACA,MAAOjB,GAAckB,CAAM,EAC3B,MAAO,CAAE,IAAKC,EAAI,cAAcH,CAAE,CAAE,EACpC,OAAQ,CACN,CAAE,KAAM,QAAS,MAAO,OAAOG,EAAI,WAAWH,CAAE,CAAC,EAAG,OAAQ,EAAK,EACjE,CAAE,KAAM,WAAY,MAAOG,EAAI,eAAeH,CAAE,EAAG,OAAQ,EAAK,EAChE,CACE,KAAM,SACN,MAAOE,EAAO,OAAO,CAAC,EAAE,YAAY,EAAIA,EAAO,MAAM,CAAC,EACtD,OAAQ,EACV,CACF,EACA,UAAW,IAAI,KAAK,EAAE,YAAY,CACpC,EAEA,MAAO,CACL,SAAAd,EACA,WAAYC,EACZ,OAAQ,CAACe,CAAK,CAChB,CACF,EAEA,MAAM,QAAQZ,EAA+C,CAC3D,IAAMa,EAAS,MAAMd,EAAYC,CAAyB,EAC1D,GAAI,CAACa,EAAO,GACV,MAAM,IAAI,MAAM,wBAAwBA,EAAO,KAAK,EAAE,EAExD,OAAOA,EAAO,KAChB,EAEA,MAAM,WAAWZ,EAAmBD,EAAiC,CACnE,IAAMa,EAAS,MAAMd,EAAYC,EAA2BC,CAAS,EACrE,GAAI,CAACY,EAAO,GACV,MAAM,IAAI,MAAM,0BAA0BA,EAAO,KAAK,EAAE,CAE5D,CACF,CACF,ChBxGO,SAASC,GACdC,EACU,CACV,GAAM,CAAE,WAAAC,EAAY,SAAAC,EAAU,UAAAC,EAAW,QAAAC,EAAS,GAAGC,CAAY,EAAIL,EAE/DM,EAAUC,GAAqB,CACnC,WAAAN,EACA,SAAAC,EACA,UAAAC,EACA,QAAAC,CACF,CAAC,EAED,OAAOI,GAAeH,EAAaC,CAAO,CAC5C","names":["discord_exports","__export","createDiscordNotifier","__toCommonJS","import_awaitly","countSteps","ir","countInNodes","nodes","count","node","branch","import_awaitly","isStepNode","node","isParallelNode","node","isRaceNode","isDecisionNode","isStreamNode","formatDuration","ms","minutes","seconds","generateId","DIM","FG_RED","FG_GREEN","FG_YELLOW","FG_BLUE","FG_GRAY","FG_WHITE","defaultColorScheme","FG_WHITE","FG_YELLOW","FG_GREEN","FG_RED","FG_GRAY","FG_BLUE","DIM","import_awaitly","getHeatLevel","heat","getStyleDefinitions","getHeatmapStyleDefinitions","getHeatClass","level","getHookStyleDefinitions","safeStringify","value","_key","v","n","getStringified","result","renderHooks","hooks","lines","options","lastHookId","hookId","state","icon","timing","formatDuration","context","nodeCounter","usedDecisionIds","usedStepIds","generateNodeId","prefix","resetNodeCounter","escapeMermaidText","text","escapeSubgraphName","mermaidRenderer","ir","enhanced","hookExitId","startId","prevNodeId","child","renderNode","endId","endIcon","endLabel","endShape","endClass","getStyleDefinitions","getHeatmapStyleDefinitions","node","isStepNode","renderStepNode","isParallelNode","renderParallelNode","isRaceNode","renderRaceNode","isDecisionNode","renderDecisionNode","isStreamNode","renderStreamNode","id","mermaidOpts","showRetryEdges","showErrorEdges","showTimeoutEdges","suffix","baseLabel","labelText","label","stateIcon","ioInfo","inputStr","outputStr","hookInfo","hookKey","hookExec","hookIcon","hookTiming","escapedLabel","nodeClass","heat","level","getHeatLevel","getHeatClass","shape","retryLabel","errorNodeId","errorLabel","timeoutNodeId","timeoutMs","subgraphId","forkId","joinId","name","modeLabel","note","childExitIds","exitId","stateClass","winnerExitId","isWinner","decisionId","condition","decisionValue","decisionLabel","branchExitIds","takenBranchExitId","usedBranchIds","branch","branchId","branchLabelText","branchLabel","branchClass","edgeLabel","prevId","counts","backpressure","import_pako","hasBuffer","base64UrlEncode","bytes","base64","binary","i","encodeForKroki","text","textBytes","compressed","pako","base64UrlEncode","DEFAULT_KROKI_URL","buildKrokiUrl","diagramType","format","text","options","baseUrl","encoded","encodeForKroki","toKrokiUrl","ir","renderer","mermaidRenderer","renderOptions","defaultColorScheme","mermaidText","toKrokiSvgUrl","DEFAULT_MERMAID_INK_URL","encodeForMermaidInk","text","encodeForKroki","normalizeOptions","options","exportOpts","buildQueryString","format","params","buildMermaidInkUrl","normalized","baseUrl","encoded","queryString","toMermaidInkUrl","ir","renderer","mermaidRenderer","renderOptions","defaultColorScheme","mermaidText","toMermaidInkSvgUrl","createNotifierContext","diagramProvider","ir","_","options","toMermaidInkSvgUrl","toKrokiSvgUrl","countSteps","import_awaitly","hasRealScopeNodes","nodes","node","detectParallelGroups","options","minOverlapMs","maxGapMs","stepsWithTiming","nonStepNodes","a","b","groups","currentGroup","step","groupStart","s","groupEnd","startedTogether","hasTrueOverlap","overlapDuration","groupedNodes","group","position","children","startTs","endTs","parallelNode","deriveGroupState","originalIndex","g","c","createIRBuilder","options","detectParallel","parallelDetection","initialEnableSnapshots","maxSnapshots","enableSnapshots","defaultWorkflowId","generateId","workflowId","workflowStartTs","workflowEndTs","workflowState","workflowError","workflowDurationMs","activeSteps","scopeStack","decisionStack","activeStreams","currentNodes","createdAt","lastUpdatedAt","hookState","preStartHookWorkflowId","snapshots","eventIndex","getStepId","event","addNode","node","decision","branch","captureSnapshot","ir","getIR","activeStepsCopy","id","step","snapshot","handleEvent","active","e","handleDecisionEvent","hookExec","streamKey","stream","handleScopeEvent","scopeIndex","s","nestedScope","nestedNode","deriveState","scope","d","branchKey","existing","children","index","toRestore","branches","inferredBranchTaken","b","c","getCurrentNodes","nodes","detectParallelGroups","root","hasHooks","reset","getSnapshots","getSnapshotAt","getIRAt","clearSnapshots","enabled","DEFAULT_DEBOUNCE_MS","DEFAULT_MAX_WAIT_MS","createLiveSession","options","callbacks","title","debounceMs","maxWaitMs","messageId","active","pendingIR","lastIR","debounceTimer","maxWaitTimer","lastPostTime","firstUpdateTime","lastFlushResult","irBuilder","createIRBuilder","clearTimers","flush","irToPost","scheduleFlush","referenceTime","timeSinceReference","remainingMaxWait","update","eventOrIr","event","finalize","status","finalIR","cancel","getSessionId","isActive","resolveStatus","ir","createNotifier","options","adapter","diagramProvider","debounceMs","maxWaitMs","ctx","createNotifierContext","notify","notifyOptions","title","status","message","messageId","createLive","sessionOptions","createLiveSession","import_awaitly","STATUS_COLORS","createDiscordAdapter","options","webhookUrl","username","avatarUrl","timeout","sendMessage","message","messageId","controller","timeoutId","url","response","data","error","ir","title","status","ctx","embed","result","createDiscordNotifier","options","webhookUrl","username","avatarUrl","timeout","baseOptions","adapter","createDiscordAdapter","createNotifier"]}