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