codebase-digest-ai 0.1.1__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.
- codebase_digest/__init__.py +8 -0
- codebase_digest/analyzer/__init__.py +7 -0
- codebase_digest/analyzer/codebase_analyzer.py +183 -0
- codebase_digest/analyzer/flow_analyzer.py +164 -0
- codebase_digest/analyzer/metrics_analyzer.py +130 -0
- codebase_digest/cli/__init__.py +1 -0
- codebase_digest/cli/main.py +284 -0
- codebase_digest/exporters/__init__.py +9 -0
- codebase_digest/exporters/graph_exporter.py +1038 -0
- codebase_digest/exporters/html_exporter.py +1052 -0
- codebase_digest/exporters/json_exporter.py +105 -0
- codebase_digest/exporters/markdown_exporter.py +273 -0
- codebase_digest/exporters/readme_exporter.py +306 -0
- codebase_digest/models.py +81 -0
- codebase_digest/parser/__init__.py +7 -0
- codebase_digest/parser/base.py +41 -0
- codebase_digest/parser/javascript_parser.py +36 -0
- codebase_digest/parser/python_parser.py +270 -0
- codebase_digest_ai-0.1.1.dist-info/METADATA +233 -0
- codebase_digest_ai-0.1.1.dist-info/RECORD +24 -0
- codebase_digest_ai-0.1.1.dist-info/WHEEL +5 -0
- codebase_digest_ai-0.1.1.dist-info/entry_points.txt +2 -0
- codebase_digest_ai-0.1.1.dist-info/licenses/LICENSE +21 -0
- codebase_digest_ai-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Main CLI application."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from ..analyzer import CodebaseAnalyzer
|
|
11
|
+
from ..exporters import HTMLExporter, MarkdownExporter, JSONExporter, GraphExporter, ReadmeExporter
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="codebase-digest",
|
|
15
|
+
help="AI-native code intelligence engine for semantic codebase analysis",
|
|
16
|
+
no_args_is_help=True
|
|
17
|
+
)
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def build(
|
|
23
|
+
path: Optional[Path] = typer.Argument(None, help="Path to codebase (default: current directory)"),
|
|
24
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output directory (default: .digest)"),
|
|
25
|
+
format: str = typer.Option("all", "--format", "-f", help="Output format: html, markdown, json, or all"),
|
|
26
|
+
graph: bool = typer.Option(False, "--graph", help="Generate interactive call graph visualization"),
|
|
27
|
+
graph_depth: Optional[int] = typer.Option(None, "--graph-depth", help="Limit graph to N hops from entrypoints (default: no limit)")
|
|
28
|
+
):
|
|
29
|
+
"""Build complete codebase analysis and generate reports."""
|
|
30
|
+
|
|
31
|
+
# Set defaults
|
|
32
|
+
if path is None:
|
|
33
|
+
path = Path.cwd()
|
|
34
|
+
if output is None:
|
|
35
|
+
output = path / ".digest"
|
|
36
|
+
|
|
37
|
+
# Validate inputs
|
|
38
|
+
if not path.exists():
|
|
39
|
+
console.print(f"[red]Error: Path {path} does not exist[/red]")
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
|
|
42
|
+
if not path.is_dir():
|
|
43
|
+
console.print(f"[red]Error: Path {path} is not a directory[/red]")
|
|
44
|
+
raise typer.Exit(1)
|
|
45
|
+
|
|
46
|
+
# Create output directory
|
|
47
|
+
output.mkdir(exist_ok=True)
|
|
48
|
+
|
|
49
|
+
console.print(f"[blue]Analyzing codebase at:[/blue] {path}")
|
|
50
|
+
console.print(f"[blue]Output directory:[/blue] {output}")
|
|
51
|
+
|
|
52
|
+
# Analyze codebase
|
|
53
|
+
with Progress(
|
|
54
|
+
SpinnerColumn(),
|
|
55
|
+
TextColumn("[progress.description]{task.description}"),
|
|
56
|
+
console=console
|
|
57
|
+
) as progress:
|
|
58
|
+
|
|
59
|
+
task = progress.add_task("Analyzing codebase...", total=None)
|
|
60
|
+
analyzer = CodebaseAnalyzer(path)
|
|
61
|
+
analysis = analyzer.analyze()
|
|
62
|
+
progress.update(task, description="Analysis complete!")
|
|
63
|
+
|
|
64
|
+
# Generate reports
|
|
65
|
+
formats_to_generate = []
|
|
66
|
+
if format == "all":
|
|
67
|
+
formats_to_generate = ["html", "markdown", "json"]
|
|
68
|
+
else:
|
|
69
|
+
formats_to_generate = [format]
|
|
70
|
+
|
|
71
|
+
for fmt in formats_to_generate:
|
|
72
|
+
if fmt == "html":
|
|
73
|
+
html_exporter = HTMLExporter(analysis)
|
|
74
|
+
html_exporter.export(output / "report.html")
|
|
75
|
+
console.print(f"[green]✓[/green] Generated HTML report: {output / 'report.html'}")
|
|
76
|
+
|
|
77
|
+
elif fmt == "markdown":
|
|
78
|
+
md_exporter = MarkdownExporter(analysis)
|
|
79
|
+
md_exporter.export(output / "architecture.md")
|
|
80
|
+
console.print(f"[green]✓[/green] Generated Markdown report: {output / 'architecture.md'}")
|
|
81
|
+
|
|
82
|
+
elif fmt == "json":
|
|
83
|
+
json_exporter = JSONExporter(analysis)
|
|
84
|
+
json_exporter.export(output / "entities.json")
|
|
85
|
+
console.print(f"[green]✓[/green] Generated JSON data: {output / 'entities.json'}")
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
console.print(f"[red]Error: Unknown format '{fmt}'[/red]")
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
|
|
91
|
+
# Generate additional files
|
|
92
|
+
_generate_flows_md(analysis, output / "flows.md")
|
|
93
|
+
_generate_ai_context_md(analysis, output / "ai-context.md")
|
|
94
|
+
|
|
95
|
+
# Generate project README.md
|
|
96
|
+
readme_exporter = ReadmeExporter(analysis)
|
|
97
|
+
readme_exporter.export(output / "README.md")
|
|
98
|
+
console.print(f"[green]✓[/green] Generated project README: {output / 'README.md'}")
|
|
99
|
+
|
|
100
|
+
# Always generate interactive call graph
|
|
101
|
+
try:
|
|
102
|
+
graph_exporter = GraphExporter(analysis, max_depth=graph_depth)
|
|
103
|
+
graph_exporter.export(output / "callgraph.html")
|
|
104
|
+
console.print(f"[green]✓[/green] Generated interactive call graph: {output / 'callgraph.html'}")
|
|
105
|
+
|
|
106
|
+
# Show graph statistics
|
|
107
|
+
stats = graph_exporter.get_graph_stats()
|
|
108
|
+
console.print(f"[blue]Graph Stats:[/blue] {stats['nodes']} nodes, {stats['edges']} edges, {stats['components']} components")
|
|
109
|
+
|
|
110
|
+
except ImportError:
|
|
111
|
+
console.print(f"[red]Error: pyvis not installed. Install with: pip install pyvis[/red]")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
console.print(f"[red]Error generating call graph: {e}[/red]")
|
|
114
|
+
|
|
115
|
+
# Generate additional call graph if explicitly requested (for backward compatibility)
|
|
116
|
+
if graph:
|
|
117
|
+
console.print("[blue]Note: Call graph is now generated by default[/blue]")
|
|
118
|
+
|
|
119
|
+
total_files = len(formats_to_generate) + 4 # +4 for flows.md, ai-context.md, README.md, callgraph.html
|
|
120
|
+
console.print(f"\n[green]Analysis complete![/green] Generated {total_files} files in {output}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command()
|
|
124
|
+
def stats(
|
|
125
|
+
path: Optional[Path] = typer.Argument(None, help="Path to codebase (default: current directory)")
|
|
126
|
+
):
|
|
127
|
+
"""Show quick statistics about the codebase."""
|
|
128
|
+
|
|
129
|
+
if path is None:
|
|
130
|
+
path = Path.cwd()
|
|
131
|
+
|
|
132
|
+
if not path.exists() or not path.is_dir():
|
|
133
|
+
console.print(f"[red]Error: Invalid directory {path}[/red]")
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
|
|
136
|
+
console.print(f"[blue]Analyzing:[/blue] {path}")
|
|
137
|
+
|
|
138
|
+
with Progress(
|
|
139
|
+
SpinnerColumn(),
|
|
140
|
+
TextColumn("[progress.description]{task.description}"),
|
|
141
|
+
console=console
|
|
142
|
+
) as progress:
|
|
143
|
+
|
|
144
|
+
task = progress.add_task("Gathering statistics...", total=None)
|
|
145
|
+
analyzer = CodebaseAnalyzer(path)
|
|
146
|
+
analysis = analyzer.analyze()
|
|
147
|
+
progress.update(task, description="Complete!")
|
|
148
|
+
|
|
149
|
+
# Create statistics table
|
|
150
|
+
table = Table(title=f"Codebase Statistics - {path.name}")
|
|
151
|
+
table.add_column("Metric", style="cyan")
|
|
152
|
+
table.add_column("Value", style="green")
|
|
153
|
+
|
|
154
|
+
table.add_row("Total Files", str(analysis.total_files))
|
|
155
|
+
table.add_row("Lines of Code", f"{analysis.total_lines:,}")
|
|
156
|
+
table.add_row("Languages", ", ".join(sorted(analysis.languages)))
|
|
157
|
+
table.add_row("Functions", str(len([s for s in analysis.symbols if s.type == 'function'])))
|
|
158
|
+
table.add_row("Classes", str(len([s for s in analysis.symbols if s.type == 'class'])))
|
|
159
|
+
table.add_row("Domain Entities", str(len(analysis.domain_entities)))
|
|
160
|
+
table.add_row("Execution Flows", str(len(analysis.execution_flows)))
|
|
161
|
+
table.add_row("Complexity Score", f"{analysis.complexity_score:.1f}")
|
|
162
|
+
|
|
163
|
+
console.print(table)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.command()
|
|
167
|
+
def query(
|
|
168
|
+
search_term: str = typer.Argument(..., help="Search term or pattern"),
|
|
169
|
+
path: Optional[Path] = typer.Argument(None, help="Path to codebase (default: current directory)")
|
|
170
|
+
):
|
|
171
|
+
"""Search for patterns in the codebase analysis."""
|
|
172
|
+
|
|
173
|
+
if path is None:
|
|
174
|
+
path = Path.cwd()
|
|
175
|
+
|
|
176
|
+
if not path.exists() or not path.is_dir():
|
|
177
|
+
console.print(f"[red]Error: Invalid directory {path}[/red]")
|
|
178
|
+
raise typer.Exit(1)
|
|
179
|
+
|
|
180
|
+
console.print(f"[blue]Searching for:[/blue] '{search_term}' in {path}")
|
|
181
|
+
|
|
182
|
+
with Progress(
|
|
183
|
+
SpinnerColumn(),
|
|
184
|
+
TextColumn("[progress.description]{task.description}"),
|
|
185
|
+
console=console
|
|
186
|
+
) as progress:
|
|
187
|
+
|
|
188
|
+
task = progress.add_task("Analyzing and searching...", total=None)
|
|
189
|
+
analyzer = CodebaseAnalyzer(path)
|
|
190
|
+
analysis = analyzer.analyze()
|
|
191
|
+
progress.update(task, description="Searching...")
|
|
192
|
+
|
|
193
|
+
results = _search_analysis(analysis, search_term.lower())
|
|
194
|
+
progress.update(task, description="Complete!")
|
|
195
|
+
|
|
196
|
+
if not results:
|
|
197
|
+
console.print(f"[yellow]No results found for '{search_term}'[/yellow]")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
console.print(f"\n[green]Found {len(results)} results:[/green]")
|
|
201
|
+
|
|
202
|
+
for result in results:
|
|
203
|
+
console.print(f"[cyan]•[/cyan] {result}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _generate_flows_md(analysis, output_path: Path):
|
|
207
|
+
"""Generate flows.md file."""
|
|
208
|
+
content = "# Execution Flows\n\n"
|
|
209
|
+
|
|
210
|
+
if not analysis.execution_flows:
|
|
211
|
+
content += "No execution flows detected.\n"
|
|
212
|
+
else:
|
|
213
|
+
for flow in analysis.execution_flows:
|
|
214
|
+
content += f"## {flow.name}\n\n"
|
|
215
|
+
content += f"{flow.description}\n\n"
|
|
216
|
+
content += f"**Entry Point:** `{flow.entry_point}`\n\n"
|
|
217
|
+
content += "**Flow Steps:**\n"
|
|
218
|
+
for i, step in enumerate(flow.steps, 1):
|
|
219
|
+
content += f"{i}. `{step}`\n"
|
|
220
|
+
content += "\n"
|
|
221
|
+
|
|
222
|
+
if flow.files_involved:
|
|
223
|
+
content += "**Files Involved:**\n"
|
|
224
|
+
for file_path in sorted(flow.files_involved):
|
|
225
|
+
rel_path = file_path.relative_to(analysis.root_path) if file_path.is_absolute() else file_path
|
|
226
|
+
content += f"- {rel_path}\n"
|
|
227
|
+
content += "\n"
|
|
228
|
+
|
|
229
|
+
output_path.write_text(content, encoding='utf-8')
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _generate_ai_context_md(analysis, output_path: Path):
|
|
233
|
+
"""Generate AI-optimized context file."""
|
|
234
|
+
content = f"""# AI Context - {analysis.root_path.name}
|
|
235
|
+
|
|
236
|
+
## System Overview
|
|
237
|
+
This codebase contains {analysis.total_files} files with {analysis.total_lines:,} lines of code across {len(analysis.languages)} languages.
|
|
238
|
+
|
|
239
|
+
## Key Components
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
# Add top symbols
|
|
243
|
+
for symbol in analysis.symbols[:20]:
|
|
244
|
+
rel_path = symbol.file_path.relative_to(analysis.root_path)
|
|
245
|
+
content += f"- `{symbol.name}` ({symbol.type}) - {rel_path}:{symbol.line_number}\n"
|
|
246
|
+
|
|
247
|
+
content += "\n## Domain Entities\n"
|
|
248
|
+
for entity in analysis.domain_entities:
|
|
249
|
+
rel_path = entity.file_path.relative_to(analysis.root_path)
|
|
250
|
+
content += f"- `{entity.name}` - {rel_path}\n"
|
|
251
|
+
|
|
252
|
+
content += "\n## Execution Flows\n"
|
|
253
|
+
for flow in analysis.execution_flows:
|
|
254
|
+
content += f"- {flow.name}: {' → '.join(flow.steps[:3])}{'...' if len(flow.steps) > 3 else ''}\n"
|
|
255
|
+
|
|
256
|
+
output_path.write_text(content, encoding='utf-8')
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _search_analysis(analysis, search_term: str):
|
|
260
|
+
"""Search through analysis results."""
|
|
261
|
+
results = []
|
|
262
|
+
|
|
263
|
+
# Search symbols
|
|
264
|
+
for symbol in analysis.symbols:
|
|
265
|
+
if search_term in symbol.name.lower():
|
|
266
|
+
rel_path = symbol.file_path.relative_to(analysis.root_path)
|
|
267
|
+
results.append(f"Symbol: {symbol.name} ({symbol.type}) in {rel_path}:{symbol.line_number}")
|
|
268
|
+
|
|
269
|
+
# Search domain entities
|
|
270
|
+
for entity in analysis.domain_entities:
|
|
271
|
+
if search_term in entity.name.lower():
|
|
272
|
+
rel_path = entity.file_path.relative_to(analysis.root_path)
|
|
273
|
+
results.append(f"Entity: {entity.name} in {rel_path}")
|
|
274
|
+
|
|
275
|
+
# Search execution flows
|
|
276
|
+
for flow in analysis.execution_flows:
|
|
277
|
+
if search_term in flow.name.lower() or any(search_term in step.lower() for step in flow.steps):
|
|
278
|
+
results.append(f"Flow: {flow.name} - {flow.description}")
|
|
279
|
+
|
|
280
|
+
return results
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
app()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Export modules for generating reports and documentation."""
|
|
2
|
+
|
|
3
|
+
from .html_exporter import HTMLExporter
|
|
4
|
+
from .markdown_exporter import MarkdownExporter
|
|
5
|
+
from .json_exporter import JSONExporter
|
|
6
|
+
from .graph_exporter import GraphExporter
|
|
7
|
+
from .readme_exporter import ReadmeExporter
|
|
8
|
+
|
|
9
|
+
__all__ = ["HTMLExporter", "MarkdownExporter", "JSONExporter", "GraphExporter", "ReadmeExporter"]
|