///|
fn node_kind_name(kind : @core.NodeKind) -> String {
  match kind {
    @core.Function => "Function"
    @core.Llm => "LLM"
    @core.CodingAgent => "Coding Agent"
    @core.Custom(name) => "Custom: \{name}"
  }
}

///|
fn escape_mermaid_label(value : String) -> String {
  value
  .replace_all(old="#", new="#35;")
  .replace_all(old="&", new="#38;")
  .replace_all(old="\"", new="#quot;")
  .replace_all(old="<", new="#60;")
  .replace_all(old=">", new="#62;")
  .replace_all(old="\r", new=" ")
  .replace_all(old="\n", new=" ")
}

///|
fn node_label(node : @core.CompiledNodeSnapshot) -> String {
  let summary = escape_mermaid_label(
    "\{node.metadata.name} [\{node.id.to_string()}] / \{node_kind_name(node.metadata.kind)}",
  )
  match node.metadata.description {
    Some(description) => "\{summary}
\{escape_mermaid_label(description)}" None => summary } } ///| fn write_route_edge( output : StringBuilder, from : String, to : String, route : @core.DeclaredRoute, ) -> Unit { match route.metadata.label { Some(label) => output.write_string( " \{from} -->|\{escape_mermaid_label(label)}| \{to}\n", ) None => output.write_string(" \{from} --> \{to}\n") } } ///| /// Renders a compiled graph as a deterministic Mermaid flowchart. /// /// Node and router descriptions and declared route labels come from core /// metadata. Routers with multiple declared destinations or descriptions are /// rendered as decision diamonds. Runtime-only conditions, `End`, and `Fail` /// outcomes are not inferred. pub fn[S, P] to_mermaid(graph : @core.CompiledGraph[S, P]) -> String { let snapshot = graph.snapshot() let mermaid_ids : Map[@core.NodeId, String] = Map([]) for index, node in snapshot.nodes { mermaid_ids[node.id] = "node_\{index}" } let output = StringBuilder::new() output.write_string("flowchart TD\n") for node in snapshot.nodes { output.write_string(" \{mermaid_ids[node.id]}[\"\{node_label(node)}\"]\n") } output.write_string( " graph_entry([\"Entry\"]) --> \{mermaid_ids[snapshot.entry]}\n", ) for index, node in snapshot.nodes { if node.declared_routes.length() > 1 || node.router_metadata.description is Some(_) { let router_id = "router_\{index}" let router_label = match node.router_metadata.description { Some(description) => escape_mermaid_label(description) None => escape_mermaid_label("\{node.metadata.name} Router") } output.write_string( " \{mermaid_ids[node.id]} --> \{router_id}{\"\{router_label}\"}\n", ) for route in node.declared_routes { write_route_edge(output, router_id, mermaid_ids[route.target], route) } } else { for route in node.declared_routes { write_route_edge( output, mermaid_ids[node.id], mermaid_ids[route.target], route, ) } } } output.to_string() }