ai-codeindex 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ """Formatters for technical debt reports.
2
+
3
+ This module provides different output formatters for technical debt reports:
4
+ - ConsoleFormatter: Human-readable console output with colors
5
+ - MarkdownFormatter: Markdown format for documentation
6
+ - JSONFormatter: Machine-readable JSON format
7
+ """
8
+
9
+ import json
10
+ from abc import ABC, abstractmethod
11
+
12
+ from codeindex.tech_debt import DebtSeverity, TechDebtReport
13
+
14
+
15
+ class ReportFormatter(ABC):
16
+ """Abstract base class for report formatters."""
17
+
18
+ @abstractmethod
19
+ def format(self, report: TechDebtReport) -> str:
20
+ """Format a technical debt report.
21
+
22
+ Args:
23
+ report: TechDebtReport to format
24
+
25
+ Returns:
26
+ Formatted report as string
27
+ """
28
+ pass
29
+
30
+
31
+ class ConsoleFormatter(ReportFormatter):
32
+ """Formatter for console output with ANSI colors."""
33
+
34
+ # ANSI color codes
35
+ RED = "\033[91m"
36
+ YELLOW = "\033[93m"
37
+ GREEN = "\033[92m"
38
+ RESET = "\033[0m"
39
+ BOLD = "\033[1m"
40
+
41
+ def format(self, report: TechDebtReport) -> str:
42
+ """Format report for console output.
43
+
44
+ Args:
45
+ report: TechDebtReport to format
46
+
47
+ Returns:
48
+ Formatted string with ANSI colors
49
+ """
50
+ lines = []
51
+
52
+ # Header
53
+ lines.append(f"\n{self.BOLD}Technical Debt Report{self.RESET}")
54
+ lines.append("=" * 50)
55
+
56
+ # Summary
57
+ lines.append(f"\n{self.BOLD}Summary:{self.RESET}")
58
+ lines.append(f" Files analyzed: {report.total_files} files analyzed")
59
+ lines.append(f" Total issues: {report.total_issues} issues found")
60
+ lines.append(f" Quality Score: {report.average_quality_score:.1f}")
61
+
62
+ # Severity breakdown
63
+ if report.total_issues > 0:
64
+ lines.append(f"\n{self.BOLD}Issues by Severity:{self.RESET}")
65
+ if report.critical_issues > 0:
66
+ lines.append(f" {self.RED}CRITICAL: {report.critical_issues}{self.RESET}")
67
+ if report.high_issues > 0:
68
+ lines.append(f" {self.YELLOW}HIGH: {report.high_issues}{self.RESET}")
69
+ if report.medium_issues > 0:
70
+ lines.append(f" MEDIUM: {report.medium_issues}")
71
+ if report.low_issues > 0:
72
+ lines.append(f" LOW: {report.low_issues}")
73
+
74
+ # File details
75
+ if report.file_reports:
76
+ lines.append(f"\n{self.BOLD}Files:{self.RESET}")
77
+ for file_report in report.file_reports:
78
+ if file_report.total_issues > 0:
79
+ lines.append(f"\n {file_report.file_path}:")
80
+ for issue in file_report.debt_analysis.issues:
81
+ severity_color = self._get_severity_color(issue.severity)
82
+ lines.append(
83
+ f" {severity_color}{issue.severity.name}{self.RESET} "
84
+ f"[{issue.category}] {issue.description}"
85
+ )
86
+
87
+ lines.append("")
88
+ return "\n".join(lines)
89
+
90
+ def _get_severity_color(self, severity: DebtSeverity) -> str:
91
+ """Get ANSI color code for severity level."""
92
+ if severity == DebtSeverity.CRITICAL:
93
+ return self.RED
94
+ elif severity == DebtSeverity.HIGH:
95
+ return self.YELLOW
96
+ else:
97
+ return ""
98
+
99
+
100
+ class MarkdownFormatter(ReportFormatter):
101
+ """Formatter for Markdown output."""
102
+
103
+ def format(self, report: TechDebtReport) -> str:
104
+ """Format report as Markdown.
105
+
106
+ Args:
107
+ report: TechDebtReport to format
108
+
109
+ Returns:
110
+ Formatted Markdown string
111
+ """
112
+ lines = []
113
+
114
+ # Header
115
+ lines.append("# Technical Debt Report")
116
+ lines.append("")
117
+
118
+ # Summary
119
+ lines.append("## Summary")
120
+ lines.append("")
121
+ lines.append(f"- **Files Analyzed:** {report.total_files}")
122
+ lines.append(f"- **Total Issues:** {report.total_issues}")
123
+ lines.append(f"- **Quality Score:** {report.average_quality_score:.1f}/100")
124
+ lines.append("")
125
+
126
+ # Severity breakdown
127
+ if report.total_issues > 0:
128
+ lines.append("### Issues by Severity")
129
+ lines.append("")
130
+ lines.append(f"- **CRITICAL:** {report.critical_issues}")
131
+ lines.append(f"- **HIGH:** {report.high_issues}")
132
+ lines.append(f"- **MEDIUM:** {report.medium_issues}")
133
+ lines.append(f"- **LOW:** {report.low_issues}")
134
+ lines.append("")
135
+
136
+ # Issues by severity level
137
+ if report.total_issues > 0:
138
+ lines.append("## Issues by Severity")
139
+ lines.append("")
140
+
141
+ # CRITICAL issues
142
+ if report.critical_issues > 0:
143
+ lines.append(f"### CRITICAL ({report.critical_issues})")
144
+ lines.append("")
145
+ lines.extend(self._format_issues_table(report, DebtSeverity.CRITICAL))
146
+ lines.append("")
147
+
148
+ # HIGH issues
149
+ if report.high_issues > 0:
150
+ lines.append(f"### HIGH ({report.high_issues})")
151
+ lines.append("")
152
+ lines.extend(self._format_issues_table(report, DebtSeverity.HIGH))
153
+ lines.append("")
154
+
155
+ # MEDIUM issues
156
+ if report.medium_issues > 0:
157
+ lines.append(f"### MEDIUM ({report.medium_issues})")
158
+ lines.append("")
159
+ lines.extend(self._format_issues_table(report, DebtSeverity.MEDIUM))
160
+ lines.append("")
161
+
162
+ # LOW issues
163
+ if report.low_issues > 0:
164
+ lines.append(f"### LOW ({report.low_issues})")
165
+ lines.append("")
166
+ lines.extend(self._format_issues_table(report, DebtSeverity.LOW))
167
+ lines.append("")
168
+
169
+ return "\n".join(lines)
170
+
171
+ def _format_issues_table(
172
+ self, report: TechDebtReport, severity: DebtSeverity
173
+ ) -> list[str]:
174
+ """Format issues of a specific severity as a markdown table."""
175
+ lines = []
176
+ lines.append("| File | Category | Description | Suggestion |")
177
+ lines.append("| --- | --- | --- | --- |")
178
+
179
+ for file_report in report.file_reports:
180
+ for issue in file_report.debt_analysis.issues:
181
+ if issue.severity == severity:
182
+ file_path = issue.file_path.name
183
+ lines.append(
184
+ f"| {file_path} | {issue.category} | "
185
+ f"{issue.description} | {issue.suggestion} |"
186
+ )
187
+
188
+ return lines
189
+
190
+
191
+ class JSONFormatter(ReportFormatter):
192
+ """Formatter for JSON output."""
193
+
194
+ def format(self, report: TechDebtReport) -> str:
195
+ """Format report as JSON.
196
+
197
+ Args:
198
+ report: TechDebtReport to format
199
+
200
+ Returns:
201
+ Formatted JSON string
202
+ """
203
+ data = {
204
+ "total_files": report.total_files,
205
+ "total_issues": report.total_issues,
206
+ "critical_issues": report.critical_issues,
207
+ "high_issues": report.high_issues,
208
+ "medium_issues": report.medium_issues,
209
+ "low_issues": report.low_issues,
210
+ "average_quality_score": report.average_quality_score,
211
+ "file_reports": [
212
+ {
213
+ "file_path": str(file_report.file_path),
214
+ "quality_score": file_report.debt_analysis.quality_score,
215
+ "file_lines": file_report.debt_analysis.file_lines,
216
+ "total_symbols": file_report.debt_analysis.total_symbols,
217
+ "total_issues": file_report.total_issues,
218
+ "issues": [
219
+ {
220
+ "severity": issue.severity.name,
221
+ "category": issue.category,
222
+ "metric_value": issue.metric_value,
223
+ "threshold": issue.threshold,
224
+ "description": issue.description,
225
+ "suggestion": issue.suggestion,
226
+ }
227
+ for issue in file_report.debt_analysis.issues
228
+ ],
229
+ }
230
+ for file_report in report.file_reports
231
+ ],
232
+ }
233
+
234
+ return json.dumps(data, indent=2)
codeindex/writer.py ADDED
@@ -0,0 +1,164 @@
1
+ """Markdown writer for README_AI.md files."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ from .parser import ParseResult
8
+
9
+
10
+ @dataclass
11
+ class WriteResult:
12
+ """Result of writing a README_AI.md file."""
13
+
14
+ path: Path
15
+ success: bool
16
+ error: str = ""
17
+
18
+
19
+ def format_symbols_for_prompt(results: list[ParseResult]) -> str:
20
+ """Format parsed symbols into a readable string for LLM prompt."""
21
+ lines = []
22
+
23
+ for result in results:
24
+ if result.error:
25
+ lines.append(f"- {result.path.name}: [parse error] {result.error}")
26
+ continue
27
+
28
+ lines.append(f"\n### {result.path.name}")
29
+
30
+ if result.module_docstring:
31
+ lines.append(f"Module: {result.module_docstring[:200]}...")
32
+
33
+ for symbol in result.symbols:
34
+ if symbol.kind == "class":
35
+ lines.append(f"\n**{symbol.kind}** `{symbol.signature}`")
36
+ if symbol.docstring:
37
+ lines.append(f" {symbol.docstring[:150]}...")
38
+ elif symbol.kind in ("function", "method"):
39
+ indent = " " if symbol.kind == "method" else ""
40
+ lines.append(f"{indent}- `{symbol.signature}`")
41
+ if symbol.docstring:
42
+ lines.append(f"{indent} {symbol.docstring[:100]}...")
43
+
44
+ return "\n".join(lines) if lines else "No symbols found."
45
+
46
+
47
+ def format_imports_for_prompt(results: list[ParseResult]) -> str:
48
+ """Format imports into a readable string for LLM prompt."""
49
+ all_imports: dict[str, set[str]] = {}
50
+
51
+ for result in results:
52
+ for imp in result.imports:
53
+ if imp.module not in all_imports:
54
+ all_imports[imp.module] = set()
55
+ all_imports[imp.module].update(imp.names)
56
+
57
+ if not all_imports:
58
+ return "No imports found."
59
+
60
+ lines = []
61
+ for module, names in sorted(all_imports.items()):
62
+ if names:
63
+ lines.append(f"- {module}: {', '.join(sorted(names))}")
64
+ else:
65
+ lines.append(f"- {module}")
66
+
67
+ return "\n".join(lines)
68
+
69
+
70
+ def format_files_for_prompt(results: list[ParseResult]) -> str:
71
+ """Format file list for LLM prompt."""
72
+ lines = []
73
+ for result in results:
74
+ if result.error:
75
+ lines.append(f"- {result.path.name} [error]")
76
+ else:
77
+ symbol_count = len(result.symbols)
78
+ lines.append(f"- {result.path.name} ({symbol_count} symbols)")
79
+
80
+ return "\n".join(lines) if lines else "No files found."
81
+
82
+
83
+ def write_readme(
84
+ dir_path: Path,
85
+ content: str,
86
+ output_file: str = "README_AI.md",
87
+ ) -> WriteResult:
88
+ """
89
+ Write the README_AI.md file.
90
+
91
+ Args:
92
+ dir_path: Directory to write to
93
+ content: Content from AI CLI
94
+ output_file: Output filename
95
+
96
+ Returns:
97
+ WriteResult indicating success or failure
98
+ """
99
+ output_path = dir_path / output_file
100
+
101
+ try:
102
+ # Add a small header with metadata
103
+ timestamp = datetime.now().isoformat()
104
+ header = f"<!-- Generated by codeindex at {timestamp} -->\n\n"
105
+
106
+ with open(output_path, "w") as f:
107
+ f.write(header)
108
+ f.write(content)
109
+
110
+ return WriteResult(path=output_path, success=True)
111
+
112
+ except Exception as e:
113
+ return WriteResult(path=output_path, success=False, error=str(e))
114
+
115
+
116
+ def generate_fallback_readme(
117
+ dir_path: Path,
118
+ results: list[ParseResult],
119
+ output_file: str = "README_AI.md",
120
+ ) -> WriteResult:
121
+ """
122
+ Generate a basic README_AI.md without AI, using just parsed data.
123
+
124
+ Useful when AI CLI is not available or fails.
125
+
126
+ Args:
127
+ dir_path: Directory to write to
128
+ results: Parse results from the parser
129
+ output_file: Output filename
130
+
131
+ Returns:
132
+ WriteResult indicating success or failure
133
+ """
134
+ output_path = dir_path / output_file
135
+
136
+ try:
137
+ timestamp = datetime.now().isoformat()
138
+
139
+ lines = [
140
+ f"<!-- Generated by codeindex (fallback mode) at {timestamp} -->",
141
+ "",
142
+ f"# {dir_path.name}",
143
+ "",
144
+ "## Files",
145
+ "",
146
+ format_files_for_prompt(results),
147
+ "",
148
+ "## Symbols",
149
+ "",
150
+ format_symbols_for_prompt(results),
151
+ "",
152
+ "## Dependencies",
153
+ "",
154
+ format_imports_for_prompt(results),
155
+ "",
156
+ ]
157
+
158
+ with open(output_path, "w") as f:
159
+ f.write("\n".join(lines))
160
+
161
+ return WriteResult(path=output_path, success=True)
162
+
163
+ except Exception as e:
164
+ return WriteResult(path=output_path, success=False, error=str(e))