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.
- ai_codeindex-0.7.0.dist-info/METADATA +966 -0
- ai_codeindex-0.7.0.dist-info/RECORD +41 -0
- ai_codeindex-0.7.0.dist-info/WHEEL +4 -0
- ai_codeindex-0.7.0.dist-info/entry_points.txt +2 -0
- ai_codeindex-0.7.0.dist-info/licenses/LICENSE +21 -0
- codeindex/README_AI.md +767 -0
- codeindex/__init__.py +11 -0
- codeindex/adaptive_config.py +83 -0
- codeindex/adaptive_selector.py +171 -0
- codeindex/ai_helper.py +48 -0
- codeindex/cli.py +40 -0
- codeindex/cli_common.py +10 -0
- codeindex/cli_config.py +97 -0
- codeindex/cli_docs.py +66 -0
- codeindex/cli_hooks.py +765 -0
- codeindex/cli_scan.py +562 -0
- codeindex/cli_symbols.py +295 -0
- codeindex/cli_tech_debt.py +238 -0
- codeindex/config.py +479 -0
- codeindex/directory_tree.py +229 -0
- codeindex/docstring_processor.py +342 -0
- codeindex/errors.py +62 -0
- codeindex/extractors/__init__.py +9 -0
- codeindex/extractors/thinkphp.py +132 -0
- codeindex/file_classifier.py +148 -0
- codeindex/framework_detect.py +323 -0
- codeindex/hierarchical.py +428 -0
- codeindex/incremental.py +278 -0
- codeindex/invoker.py +260 -0
- codeindex/parallel.py +155 -0
- codeindex/parser.py +740 -0
- codeindex/route_extractor.py +98 -0
- codeindex/route_registry.py +77 -0
- codeindex/scanner.py +167 -0
- codeindex/semantic_extractor.py +408 -0
- codeindex/smart_writer.py +737 -0
- codeindex/symbol_index.py +199 -0
- codeindex/symbol_scorer.py +283 -0
- codeindex/tech_debt.py +619 -0
- codeindex/tech_debt_formatters.py +234 -0
- codeindex/writer.py +164 -0
|
@@ -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))
|