|
| 1 | +"""Response formatters that convert API responses to markdown. |
| 2 | +
|
| 3 | +Each formatter is a pure function: takes API response dict, returns markdown string. |
| 4 | +This makes them independently testable without any HTTP calls. |
| 5 | +""" |
| 6 | + |
| 7 | + |
| 8 | +def format_search_results(result: dict) -> str: |
| 9 | + """Format semantic search results as markdown.""" |
| 10 | + count = result.get("count", 0) |
| 11 | + cached = " (cached)" if result.get("cached") else "" |
| 12 | + output = f"# Code Search Results\n\nFound {count} results{cached}\n\n" |
| 13 | + |
| 14 | + if not result.get("results"): |
| 15 | + return output + "No results found.\n" |
| 16 | + |
| 17 | + for idx, res in enumerate(result["results"], 1): |
| 18 | + score = res.get("score", 0) * 100 |
| 19 | + name = res.get("name", "unknown") |
| 20 | + file_path = res.get("file_path", "unknown") |
| 21 | + file_type = res.get("type", "unknown") |
| 22 | + lang = res.get("language", "unknown") |
| 23 | + line_start = res.get("line_start", 0) |
| 24 | + line_end = res.get("line_end", 0) |
| 25 | + code = res.get("code", "") |
| 26 | + |
| 27 | + output += f"## {idx}. {name} ({score:.0f}% match)\n" |
| 28 | + output += f"**File:** `{file_path}`\n" |
| 29 | + output += f"**Type:** {file_type} | **Language:** {lang}\n" |
| 30 | + output += f"**Lines:** {line_start}-{line_end}\n\n" |
| 31 | + output += f"```{lang}\n{code}\n```\n\n" |
| 32 | + |
| 33 | + return output |
| 34 | + |
| 35 | + |
| 36 | +def format_repositories(result: dict) -> str: |
| 37 | + """Format repository listing as markdown.""" |
| 38 | + output = "# Indexed Repositories\n\n" |
| 39 | + |
| 40 | + if not result.get("repositories"): |
| 41 | + return output + "No repositories indexed yet.\n" |
| 42 | + |
| 43 | + for repo in result["repositories"]: |
| 44 | + output += f"### {repo.get('name', 'unknown')}\n" |
| 45 | + output += f"- **ID:** `{repo.get('id')}`\n" |
| 46 | + output += f"- **Status:** {repo.get('status', 'unknown')}\n" |
| 47 | + output += f"- **Functions:** {repo.get('file_count', 0):,}\n" |
| 48 | + output += f"- **Branch:** {repo.get('branch', 'main')}\n\n" |
| 49 | + |
| 50 | + return output |
| 51 | + |
| 52 | + |
| 53 | +def format_dependency_graph(result: dict) -> str: |
| 54 | + """Format dependency graph analysis as markdown.""" |
| 55 | + nodes = result.get("nodes", []) |
| 56 | + edges = result.get("edges", []) |
| 57 | + metrics = result.get("metrics", {}) |
| 58 | + |
| 59 | + output = "# Dependency Graph Analysis\n\n" |
| 60 | + output += f"**Total Files:** {len(nodes)}\n" |
| 61 | + output += f"**Total Dependencies:** {metrics.get('total_edges', len(edges))}\n" |
| 62 | + output += f"**Avg Dependencies per File:** {metrics.get('avg_dependencies', 0):.1f}\n\n" |
| 63 | + |
| 64 | + # Most-imported files (highest number of dependents) |
| 65 | + dependent_count: dict[str, int] = {} |
| 66 | + for edge in edges: |
| 67 | + target = edge.get("target", "") |
| 68 | + dependent_count[target] = dependent_count.get(target, 0) + 1 |
| 69 | + |
| 70 | + if dependent_count: |
| 71 | + sorted_deps = sorted( |
| 72 | + dependent_count.items(), key=lambda x: x[1], reverse=True |
| 73 | + )[:5] |
| 74 | + output += "## Most Critical Files (High Impact)\n\n" |
| 75 | + for file, count in sorted_deps: |
| 76 | + output += f"- `{file}` - **{count} dependents**\n" |
| 77 | + output += "\n" |
| 78 | + |
| 79 | + high_import = [n for n in nodes if n.get("imports", 0) >= 3] |
| 80 | + if high_import: |
| 81 | + output += "## Files with Most Imports\n\n" |
| 82 | + for f in sorted(high_import, key=lambda x: x.get("imports", 0), reverse=True)[:5]: |
| 83 | + output += f"- `{f['id']}` - imports {f['imports']} files\n" |
| 84 | + |
| 85 | + return output |
| 86 | + |
| 87 | + |
| 88 | +def format_code_style(result: dict) -> str: |
| 89 | + """Format code style analysis as markdown.""" |
| 90 | + summary = result.get("summary", {}) |
| 91 | + output = "# Code Style Analysis\n\n" |
| 92 | + output += f"**Files Analyzed:** {summary.get('total_files_analyzed', 0)}\n" |
| 93 | + output += f"**Functions:** {summary.get('total_functions', 0)}\n" |
| 94 | + output += f"**Async Adoption:** {summary.get('async_adoption', '0%')}\n" |
| 95 | + output += f"**Type Hints:** {summary.get('type_hints_usage', '0%')}\n\n" |
| 96 | + |
| 97 | + naming = result.get("naming_conventions", {}).get("functions") |
| 98 | + if naming: |
| 99 | + output += "## Function Naming Conventions\n\n" |
| 100 | + for conv, info in naming.items(): |
| 101 | + output += f"- **{conv}:** {info['percentage']} ({info['count']} functions)\n" |
| 102 | + output += "\n" |
| 103 | + |
| 104 | + top_imports = result.get("top_imports") |
| 105 | + if top_imports: |
| 106 | + output += "## Most Common Imports\n\n" |
| 107 | + for item in top_imports[:10]: |
| 108 | + output += f"- `{item['module']}` (used {item['count']}x)\n" |
| 109 | + |
| 110 | + return output |
| 111 | + |
| 112 | + |
| 113 | +def format_impact_analysis(result: dict) -> str: |
| 114 | + """Format file impact analysis as markdown.""" |
| 115 | + output = f"# Impact Analysis: {result.get('file', 'unknown')}\n\n" |
| 116 | + output += f"**Risk Level:** {result.get('risk_level', 'unknown').upper()}\n" |
| 117 | + output += f"**Impact Summary:** {result.get('impact_summary', '')}\n\n" |
| 118 | + |
| 119 | + deps = result.get("direct_dependencies", []) |
| 120 | + output += f"## Dependencies ({len(deps)})\n" |
| 121 | + output += "Files this file imports:\n" |
| 122 | + for dep in deps[:10]: |
| 123 | + output += f"- `{dep}`\n" |
| 124 | + output += "\n" |
| 125 | + |
| 126 | + dependents = result.get("all_dependents", []) |
| 127 | + output += f"## Dependents ({len(dependents)})\n" |
| 128 | + output += "Files that would be affected by changes:\n" |
| 129 | + for dep in dependents[:15]: |
| 130 | + output += f"- `{dep}`\n" |
| 131 | + |
| 132 | + test_files = result.get("test_files") |
| 133 | + if test_files: |
| 134 | + output += "\n## Related Tests\n" |
| 135 | + for test in test_files: |
| 136 | + output += f"- `{test}`\n" |
| 137 | + |
| 138 | + return output |
| 139 | + |
| 140 | + |
| 141 | +def format_repository_insights(result: dict) -> str: |
| 142 | + """Format repository insights as markdown.""" |
| 143 | + output = f"# Repository Insights: {result.get('name', 'unknown')}\n\n" |
| 144 | + output += f"**Status:** {result.get('status', 'unknown')}\n" |
| 145 | + output += f"**Functions Indexed:** {result.get('functions_indexed', 0):,}\n" |
| 146 | + output += f"**Total Files:** {result.get('total_files', 0)}\n" |
| 147 | + output += f"**Total Dependencies:** {result.get('total_dependencies', 0)}\n\n" |
| 148 | + |
| 149 | + metrics = result.get("graph_metrics", {}) |
| 150 | + critical = metrics.get("most_critical_files") |
| 151 | + if critical: |
| 152 | + output += "## Most Critical Files\n" |
| 153 | + for item in critical[:5]: |
| 154 | + output += f"- `{item['file']}` ({item['dependents']} dependents)\n" |
| 155 | + |
| 156 | + return output |
| 157 | + |
| 158 | + |
| 159 | +def format_codebase_dna(result: dict) -> str: |
| 160 | + """Format codebase DNA extraction as markdown.""" |
| 161 | + dna_markdown = result.get("dna", "") |
| 162 | + cached = " (cached)" if result.get("cached") else "" |
| 163 | + |
| 164 | + output = f"# Codebase DNA{cached}\n\n" |
| 165 | + output += "**Use this information to write code that matches existing patterns.**\n\n" |
| 166 | + output += dna_markdown |
| 167 | + output += "\n---\n" |
| 168 | + output += "**Instructions:** When generating code for this codebase:\n" |
| 169 | + output += "1. Follow the auth patterns shown above\n" |
| 170 | + output += "2. Use the service layer structure (singletons in dependencies.py)\n" |
| 171 | + output += "3. Match the database conventions (ID types, timestamps, RLS)\n" |
| 172 | + output += "4. Use the logging patterns shown\n" |
| 173 | + output += "5. Follow the naming conventions\n" |
| 174 | + |
| 175 | + return output |
0 commit comments