""" JSONB → Markdown serializer for ResolutionFlow tree structures. Converts a tree_structure JSONB dict into the ResolutionFlow Markdown format where each node is a block delimited by YAML frontmatter. """ from typing import Any def serialize_tree_to_markdown( tree_structure: dict[str, Any], metadata: dict[str, Any] | None = None, ) -> str: """Convert a tree structure JSONB dict to ResolutionFlow Markdown format. Args: tree_structure: The recursive tree_structure dict from the database. metadata: Optional tree metadata (name, description, category, tags) to include as a frontmatter block at the top. Returns: Markdown string with YAML frontmatter blocks for each node. """ blocks: list[str] = [] if metadata: meta_lines = ["---"] if metadata.get("name"): meta_lines.append(f"name: {metadata['name']}") if metadata.get("description"): meta_lines.append(f"description: {metadata['description']}") if metadata.get("category"): meta_lines.append(f"category: {metadata['category']}") tags = metadata.get("tags") if tags: tags_str = ", ".join(tags) if isinstance(tags, list) else str(tags) meta_lines.append(f"tags: [{tags_str}]") meta_lines.append("---") blocks.append("\n".join(meta_lines)) _serialize_node(tree_structure, parent_id=None, blocks=blocks) return "\n\n".join(blocks) + "\n" def _serialize_node( node: dict[str, Any], parent_id: str | None, blocks: list[str], ) -> None: """Recursively serialize a single node and its children.""" node_type = node.get("type", "decision") node_id = node.get("id", "unknown") # Build frontmatter frontmatter_lines = [ "---", f"id: {node_id}", f"type: {node_type}", ] if parent_id is not None: frontmatter_lines.append(f"parent: {parent_id}") frontmatter_lines.append("---") # Build body based on node type body_lines: list[str] = [] if node_type == "decision": _serialize_decision(node, body_lines) elif node_type == "action": _serialize_action(node, body_lines) elif node_type == "solution": _serialize_solution(node, body_lines) block = "\n".join(frontmatter_lines) + "\n" + "\n".join(body_lines) blocks.append(block) # Recurse into children children = node.get("children", []) if children: for child in children: _serialize_node(child, parent_id=node_id, blocks=blocks) def _serialize_decision(node: dict[str, Any], lines: list[str]) -> None: """Serialize a decision node body.""" question = node.get("question", "") if question: lines.append(f"# {question}") lines.append("") help_text = node.get("help_text", "") if help_text: for ht_line in help_text.split("\n"): lines.append(f"> {ht_line}") lines.append("") options = node.get("options", []) for i, opt in enumerate(options): label = opt.get("label", "") next_id = opt.get("next_node_id", "") letter = chr(ord("A") + i) if i < 26 else str(i + 1) if next_id: lines.append(f"- [{letter}] {label} → @{next_id}") else: lines.append(f"- [{letter}] {label}") def _serialize_action(node: dict[str, Any], lines: list[str]) -> None: """Serialize an action node body.""" title = node.get("title", "") if title: lines.append(f"## {title}") lines.append("") description = node.get("description", "") if description: lines.append(description) lines.append("") commands = node.get("commands", []) if commands: lines.append("```commands") for cmd in commands: lines.append(cmd) lines.append("```") lines.append("") expected = node.get("expected_outcome", "") if expected: lines.append(f"**Expected:** {expected}") lines.append("") next_id = node.get("next_node_id", "") if next_id: lines.append(f"→ @{next_id}") def _serialize_solution(node: dict[str, Any], lines: list[str]) -> None: """Serialize a solution node body.""" title = node.get("title", "") if title: lines.append(f"## {title}") lines.append("") description = node.get("description", "") if description: lines.append(description) lines.append("") steps = node.get("resolution_steps", []) if steps: for i, step in enumerate(steps, 1): lines.append(f"{i}. {step}") solution = node.get("solution", "") if solution and not description and not steps: lines.append(solution)