devarch 0.2.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.
- devarch/__init__.py +4 -0
- devarch/__main__.py +4 -0
- devarch/analyzers/__init__.py +2 -0
- devarch/analyzers/ancient.py +48 -0
- devarch/analyzers/dead_code.py +92 -0
- devarch/analyzers/duplicates.py +101 -0
- devarch/analyzers/health.py +60 -0
- devarch/analyzers/maintenance.py +902 -0
- devarch/analyzers/monsters.py +62 -0
- devarch/analyzers/recovery.py +338 -0
- devarch/analyzers/ruins.py +45 -0
- devarch/analyzers/suspicious.py +39 -0
- devarch/analyzers/todos.py +60 -0
- devarch/cli/__init__.py +2 -0
- devarch/cli/main.py +1708 -0
- devarch/models.py +43 -0
- devarch/plugins.py +29 -0
- devarch/reports/__init__.py +2 -0
- devarch/reports/exporters.py +274 -0
- devarch/scanner/__init__.py +2 -0
- devarch/scanner/core.py +15 -0
- devarch/scanner/discovery.py +84 -0
- devarch/scanner/intelligence.py +1559 -0
- devarch/utils/__init__.py +2 -0
- devarch/utils/fs.py +165 -0
- devarch/utils/git_info.py +64 -0
- devarch/utils/rich_ui.py +107 -0
- devarch/version.py +3 -0
- devarch-0.2.0.dist-info/METADATA +317 -0
- devarch-0.2.0.dist-info/RECORD +33 -0
- devarch-0.2.0.dist-info/WHEEL +4 -0
- devarch-0.2.0.dist-info/entry_points.txt +3 -0
- devarch-0.2.0.dist-info/licenses/LICENSE +22 -0
devarch/cli/main.py
ADDED
|
@@ -0,0 +1,1708 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.live import Live
|
|
8
|
+
from rich.markup import escape
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.tree import Tree
|
|
12
|
+
|
|
13
|
+
from ..analyzers.ancient import find_ancient_files
|
|
14
|
+
from ..analyzers.dead_code import find_dead_code
|
|
15
|
+
from ..analyzers.duplicates import find_duplicates, similarity_report
|
|
16
|
+
from ..analyzers.monsters import find_monsters
|
|
17
|
+
from ..analyzers import maintenance
|
|
18
|
+
from ..analyzers import recovery
|
|
19
|
+
from ..analyzers.ruins import find_empty_directories, find_unused_assets
|
|
20
|
+
from ..analyzers.suspicious import find_suspicious
|
|
21
|
+
from ..analyzers.todos import find_todos
|
|
22
|
+
from ..reports.exporters import export_html, export_json, export_markdown, export_pdf
|
|
23
|
+
from ..scanner.core import analyze_repository_root, scan_repository
|
|
24
|
+
from ..scanner.discovery import build_reference_map, build_text_index
|
|
25
|
+
from ..utils.fs import collect_repository, path_kind, read_text, safe_stat
|
|
26
|
+
from ..utils.rich_ui import (
|
|
27
|
+
console,
|
|
28
|
+
health_badge,
|
|
29
|
+
render_artifacts,
|
|
30
|
+
render_bars,
|
|
31
|
+
render_header,
|
|
32
|
+
render_kv,
|
|
33
|
+
render_notice,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
app = typer.Typer(add_completion=False, help="Excavate technical debt and forgotten artifacts from codebases.")
|
|
38
|
+
export_app = typer.Typer(add_completion=False, help="Export excavation reports.")
|
|
39
|
+
report_app = typer.Typer(add_completion=False, help="Generate formal excavation reports.")
|
|
40
|
+
app.add_typer(export_app, name="export")
|
|
41
|
+
app.add_typer(report_app, name="report")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _command_name(command) -> str:
|
|
45
|
+
return command.name or command.callback.__name__.replace("_", "-")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _command_help(command) -> str:
|
|
49
|
+
text = (command.help or (command.callback.__doc__ or "")).strip()
|
|
50
|
+
return text or "n/a"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _iter_commands(app_obj: typer.Typer, prefix: str = "") -> list[tuple[str, str]]:
|
|
54
|
+
rows: list[tuple[str, str]] = []
|
|
55
|
+
for command in app_obj.registered_commands:
|
|
56
|
+
if command.hidden:
|
|
57
|
+
continue
|
|
58
|
+
rows.append((f"{prefix}{_command_name(command)}", _command_help(command)))
|
|
59
|
+
for group in app_obj.registered_groups:
|
|
60
|
+
group_name = group.name or "group"
|
|
61
|
+
rows.extend(_iter_commands(group.typer_instance, prefix=f"{prefix}{group_name} "))
|
|
62
|
+
return rows
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve_target(root: Path, target: Path) -> Path:
|
|
66
|
+
return target if target.is_absolute() else (root / target).resolve()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _file_context(path: Path, line_number: int | None = None, context: int = 2) -> list[str]:
|
|
70
|
+
try:
|
|
71
|
+
lines = read_text(path).splitlines()
|
|
72
|
+
except OSError:
|
|
73
|
+
return [f"Unable to read {path}"]
|
|
74
|
+
if not lines:
|
|
75
|
+
return ["(empty file)"]
|
|
76
|
+
if line_number is None or line_number < 1 or line_number > len(lines):
|
|
77
|
+
start, end = 1, min(len(lines), 10)
|
|
78
|
+
else:
|
|
79
|
+
start = max(1, line_number - context)
|
|
80
|
+
end = min(len(lines), line_number + context)
|
|
81
|
+
output: list[str] = []
|
|
82
|
+
for index in range(start, end + 1):
|
|
83
|
+
marker = ">>" if line_number == index else " "
|
|
84
|
+
output.append(f"{marker} {index:>4} | {lines[index - 1]}")
|
|
85
|
+
return output
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _render_help_catalog() -> None:
|
|
89
|
+
rows = _iter_commands(app)
|
|
90
|
+
table = Table(title="Dev Archaeologist Command Catalog", header_style="bold cyan")
|
|
91
|
+
table.add_column("Command", overflow="fold")
|
|
92
|
+
table.add_column("Purpose", overflow="fold")
|
|
93
|
+
for name, help_text in rows:
|
|
94
|
+
table.add_row(name, help_text)
|
|
95
|
+
console.print(table)
|
|
96
|
+
render_notice(
|
|
97
|
+
"For focused excavation use commands like investigate, inspect, trace, evidence, errorcode, and bugmark.",
|
|
98
|
+
style="green",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _explain_error_text(error_text: str) -> dict[str, str]:
|
|
103
|
+
text = error_text.lower()
|
|
104
|
+
if "module not found" in text or "importerror" in text or "modulenotfounderror" in text:
|
|
105
|
+
return {
|
|
106
|
+
"classification": "Import failure",
|
|
107
|
+
"cause": "A dependency or local module is missing from the runtime path.",
|
|
108
|
+
"fix": "Install the dependency, verify the package name, or update the import path.",
|
|
109
|
+
}
|
|
110
|
+
if "permission denied" in text or "eacces" in text:
|
|
111
|
+
return {
|
|
112
|
+
"classification": "Permission error",
|
|
113
|
+
"cause": "The process does not have access to the requested file or directory.",
|
|
114
|
+
"fix": "Adjust file permissions or run the command with the required access level.",
|
|
115
|
+
}
|
|
116
|
+
if "no such file" in text or "enoent" in text or "file not found" in text:
|
|
117
|
+
return {
|
|
118
|
+
"classification": "Missing file",
|
|
119
|
+
"cause": "The path does not exist or the working directory is incorrect.",
|
|
120
|
+
"fix": "Verify the path, check case sensitivity, and confirm the file was generated.",
|
|
121
|
+
}
|
|
122
|
+
if "timeout" in text or "timed out" in text:
|
|
123
|
+
return {
|
|
124
|
+
"classification": "Timeout",
|
|
125
|
+
"cause": "The operation took longer than the configured limit or stalled on I/O.",
|
|
126
|
+
"fix": "Increase the timeout, reduce workload size, or inspect slow dependencies.",
|
|
127
|
+
}
|
|
128
|
+
if "syntaxerror" in text or "invalid syntax" in text:
|
|
129
|
+
return {
|
|
130
|
+
"classification": "Syntax error",
|
|
131
|
+
"cause": "Python could not parse the file or statement.",
|
|
132
|
+
"fix": "Inspect the reported line and nearby context for unmatched delimiters or typos.",
|
|
133
|
+
}
|
|
134
|
+
if "keyerror" in text:
|
|
135
|
+
return {
|
|
136
|
+
"classification": "Missing key",
|
|
137
|
+
"cause": "The code looked up a dictionary entry that was not present.",
|
|
138
|
+
"fix": "Guard the lookup, provide a default, or validate the input data first.",
|
|
139
|
+
}
|
|
140
|
+
if "indexerror" in text:
|
|
141
|
+
return {
|
|
142
|
+
"classification": "Index error",
|
|
143
|
+
"cause": "A list, tuple, or sequence index was out of range.",
|
|
144
|
+
"fix": "Check collection length before indexing and verify iteration bounds.",
|
|
145
|
+
}
|
|
146
|
+
if "connection refused" in text or "econnrefused" in text:
|
|
147
|
+
return {
|
|
148
|
+
"classification": "Connection failure",
|
|
149
|
+
"cause": "The service was unreachable or not listening on the expected address.",
|
|
150
|
+
"fix": "Confirm the service is running and verify host, port, and firewall settings.",
|
|
151
|
+
}
|
|
152
|
+
if "attributeerror" in text:
|
|
153
|
+
return {
|
|
154
|
+
"classification": "Attribute error",
|
|
155
|
+
"cause": "Code tried to access an attribute that the object does not expose.",
|
|
156
|
+
"fix": "Validate object type and confirm the expected interface before calling it.",
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
"classification": "Unknown error shape",
|
|
160
|
+
"cause": "The provided text does not match a common pattern.",
|
|
161
|
+
"fix": "Paste the full traceback or error code for a more specific excavation.",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _confidence_text(value: float | None) -> str:
|
|
166
|
+
return f"{value:.0%}" if value is not None else "n/a"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _scan(path: Path):
|
|
170
|
+
console.print("[cyan]Excavating repository...[/cyan]")
|
|
171
|
+
return analyze_repository_root(path)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _print_timeline(timeline: dict[str, object]) -> None:
|
|
175
|
+
if not timeline.get("available"):
|
|
176
|
+
render_notice("Git history unavailable.", style="yellow")
|
|
177
|
+
return
|
|
178
|
+
tree = Tree("Repository Timeline")
|
|
179
|
+
tree.add(f"Repository Age: {timeline.get('repository_age_years', 0)} years")
|
|
180
|
+
tree.add(f"Total Commits: {timeline.get('commit_count', 0)}")
|
|
181
|
+
if timeline.get("first_commit"):
|
|
182
|
+
tree.add(f"First Commit: {timeline.get('first_commit')}")
|
|
183
|
+
if timeline.get("last_commit"):
|
|
184
|
+
tree.add(f"Last Commit: {timeline.get('last_commit')}")
|
|
185
|
+
eras = tree.add("Eras")
|
|
186
|
+
for era in timeline.get("eras", []):
|
|
187
|
+
eras.add(f"{era['year']}: {era['title']} ({era['activity']})")
|
|
188
|
+
modified = tree.add("Most Modified Files")
|
|
189
|
+
for name, count in (timeline.get("most_modified_files") or [])[:5]:
|
|
190
|
+
modified.add(f"{name} ({count})")
|
|
191
|
+
console.print(tree)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _render_deps(intelligence) -> None:
|
|
195
|
+
render_header("Dependency Archaeology", f"Repository: {intelligence.root}")
|
|
196
|
+
render_kv(
|
|
197
|
+
"Dependency Graph",
|
|
198
|
+
[
|
|
199
|
+
("Nodes", str(intelligence.graph_node_count)),
|
|
200
|
+
("Edges", str(intelligence.graph_edge_count)),
|
|
201
|
+
("Circular Dependencies", str(len(intelligence.dependency_cycles))),
|
|
202
|
+
],
|
|
203
|
+
border_style="magenta",
|
|
204
|
+
)
|
|
205
|
+
if intelligence.dependency_hubs:
|
|
206
|
+
table = Table(title="Core Dependency Hubs", header_style="bold magenta")
|
|
207
|
+
table.add_column("File", overflow="fold")
|
|
208
|
+
table.add_column("Referenced By", justify="right")
|
|
209
|
+
table.add_column("Depends On", justify="right")
|
|
210
|
+
table.add_column("External", overflow="fold")
|
|
211
|
+
table.add_column("Risk")
|
|
212
|
+
table.add_column("Impact")
|
|
213
|
+
table.add_column("Confidence", justify="right")
|
|
214
|
+
for hub in intelligence.dependency_hubs[:10]:
|
|
215
|
+
table.add_row(
|
|
216
|
+
str(hub.path),
|
|
217
|
+
str(hub.referenced_by),
|
|
218
|
+
str(hub.depends_on),
|
|
219
|
+
", ".join(hub.external_packages) or "n/a",
|
|
220
|
+
hub.dependency_risk,
|
|
221
|
+
hub.failure_impact,
|
|
222
|
+
f"{hub.confidence:.0%}",
|
|
223
|
+
)
|
|
224
|
+
console.print(table)
|
|
225
|
+
if intelligence.dependency_chains:
|
|
226
|
+
tree = Tree("Dependency Chains")
|
|
227
|
+
for chain in intelligence.dependency_chains[:5]:
|
|
228
|
+
branch = tree.add(Path(chain[0]).name)
|
|
229
|
+
for node in chain[1:]:
|
|
230
|
+
branch = branch.add(Path(node).name)
|
|
231
|
+
console.print(tree)
|
|
232
|
+
if intelligence.dependency_cycles:
|
|
233
|
+
table = Table(title="Circular Dependencies", header_style="bold red")
|
|
234
|
+
table.add_column("Cycle")
|
|
235
|
+
for cycle in intelligence.dependency_cycles[:10]:
|
|
236
|
+
table.add_row(" -> ".join(path.name for path in cycle))
|
|
237
|
+
console.print(table)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _render_genealogy(intelligence) -> None:
|
|
241
|
+
render_header("Code Family Trees", f"Repository: {intelligence.root}")
|
|
242
|
+
if not intelligence.genealogy:
|
|
243
|
+
render_notice("No strong module family clusters detected.", style="green")
|
|
244
|
+
return
|
|
245
|
+
for family in intelligence.genealogy[:8]:
|
|
246
|
+
tree = Tree(family.name)
|
|
247
|
+
tree.add(str(family.root))
|
|
248
|
+
if family.children:
|
|
249
|
+
children = tree.add("Children")
|
|
250
|
+
for child in family.children:
|
|
251
|
+
children.add(str(child))
|
|
252
|
+
if family.inherited_classes:
|
|
253
|
+
bases = tree.add("Inherited Classes")
|
|
254
|
+
for item in family.inherited_classes[:8]:
|
|
255
|
+
bases.add(item)
|
|
256
|
+
if family.parent_modules:
|
|
257
|
+
parents = tree.add("Parent Modules")
|
|
258
|
+
for parent in family.parent_modules[:5]:
|
|
259
|
+
parents.add(str(parent))
|
|
260
|
+
console.print(tree)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _render_civilizations(intelligence) -> None:
|
|
264
|
+
render_header("Lost Civilization Detection", f"Repository: {intelligence.root}")
|
|
265
|
+
if not intelligence.civilizations:
|
|
266
|
+
render_notice("No abandoned systems detected.", style="green")
|
|
267
|
+
return
|
|
268
|
+
for civ in intelligence.civilizations[:8]:
|
|
269
|
+
render_kv(
|
|
270
|
+
"Lost Civilization Discovered",
|
|
271
|
+
[
|
|
272
|
+
("Name", civ.name),
|
|
273
|
+
("Files", str(len(civ.files))),
|
|
274
|
+
("Referenced", str(civ.referenced)),
|
|
275
|
+
("Last Active", f"{civ.last_active_days} days ago"),
|
|
276
|
+
("Status", civ.status),
|
|
277
|
+
("Confidence", f"{civ.confidence:.0%}"),
|
|
278
|
+
],
|
|
279
|
+
border_style="yellow" if civ.status == "Dormant" else "red",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _render_debt(intelligence) -> None:
|
|
284
|
+
render_header("Technical Debt Heatmap", f"Repository: {intelligence.root}")
|
|
285
|
+
if not intelligence.debt_heatmap:
|
|
286
|
+
render_notice("No debt hotspots detected.", style="green")
|
|
287
|
+
return
|
|
288
|
+
render_bars("Technical Debt Hotspots", [(bucket.bucket, bucket.score) for bucket in intelligence.debt_heatmap[:12]])
|
|
289
|
+
table = Table(title="Hotspot Details")
|
|
290
|
+
table.add_column("Bucket")
|
|
291
|
+
table.add_column("Score", justify="right")
|
|
292
|
+
table.add_column("Label")
|
|
293
|
+
table.add_column("Files", justify="right")
|
|
294
|
+
for bucket in intelligence.debt_heatmap[:12]:
|
|
295
|
+
table.add_row(bucket.bucket, f"{bucket.score:.1f}", bucket.label, str(bucket.files))
|
|
296
|
+
console.print(table)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _render_personality(intelligence) -> None:
|
|
300
|
+
profile = intelligence.personality
|
|
301
|
+
render_kv(
|
|
302
|
+
"Repository Personality",
|
|
303
|
+
[
|
|
304
|
+
("Type", profile.type),
|
|
305
|
+
("Traits", "; ".join(profile.traits)),
|
|
306
|
+
("Risk", profile.risk),
|
|
307
|
+
],
|
|
308
|
+
border_style="cyan",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _render_forecast(intelligence) -> None:
|
|
313
|
+
forecast = intelligence.forecast
|
|
314
|
+
render_kv(
|
|
315
|
+
"Forecast",
|
|
316
|
+
[
|
|
317
|
+
("Current Health", f"{forecast.current_health}/100"),
|
|
318
|
+
("Projected 6 Months", f"{forecast.projected_6_months}/100"),
|
|
319
|
+
("Projected 12 Months", f"{forecast.projected_12_months}/100"),
|
|
320
|
+
("Reason", forecast.reason),
|
|
321
|
+
],
|
|
322
|
+
border_style="magenta",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _render_dna(intelligence) -> None:
|
|
327
|
+
render_kv(
|
|
328
|
+
"DNA Signature",
|
|
329
|
+
[
|
|
330
|
+
("Signature", ", ".join(intelligence.dna.signature)),
|
|
331
|
+
("Confidence", f"{intelligence.dna.confidence:.0%}"),
|
|
332
|
+
],
|
|
333
|
+
border_style="green",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _render_architecture(intelligence) -> None:
|
|
338
|
+
architecture = intelligence.architecture
|
|
339
|
+
if not architecture:
|
|
340
|
+
render_notice("Architecture classification unavailable.", style="yellow")
|
|
341
|
+
return
|
|
342
|
+
render_kv(
|
|
343
|
+
"Architecture Classification",
|
|
344
|
+
[
|
|
345
|
+
("Primary", architecture.primary),
|
|
346
|
+
("Secondary", architecture.secondary),
|
|
347
|
+
("Confidence", f"{architecture.confidence:.0%}"),
|
|
348
|
+
],
|
|
349
|
+
border_style="cyan",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _render_investigation(intelligence) -> None:
|
|
354
|
+
render_header("Code Crime Scene Investigation", f"Repository: {intelligence.root}")
|
|
355
|
+
if intelligence.incidents:
|
|
356
|
+
for incident in intelligence.incidents:
|
|
357
|
+
render_kv(
|
|
358
|
+
"Investigation Report",
|
|
359
|
+
[
|
|
360
|
+
("Incident", incident.incident),
|
|
361
|
+
("Date", incident.date),
|
|
362
|
+
("Impact", incident.impact),
|
|
363
|
+
("Outcome", incident.outcome),
|
|
364
|
+
("Risk", incident.risk),
|
|
365
|
+
],
|
|
366
|
+
border_style="red" if incident.risk in {"High", "Critical"} else "yellow",
|
|
367
|
+
)
|
|
368
|
+
if incident.evidence:
|
|
369
|
+
table = Table(title="Evidence", header_style="bold red")
|
|
370
|
+
table.add_column("Evidence", overflow="fold")
|
|
371
|
+
for item in incident.evidence:
|
|
372
|
+
table.add_row(item)
|
|
373
|
+
console.print(table)
|
|
374
|
+
else:
|
|
375
|
+
render_notice("No incident cluster detected.", style="green")
|
|
376
|
+
_render_architecture(intelligence)
|
|
377
|
+
_render_weaknesses(intelligence)
|
|
378
|
+
_render_observations(intelligence)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _render_weaknesses(intelligence) -> None:
|
|
382
|
+
render_header("Structural Weakness Detection", f"Repository: {intelligence.root}")
|
|
383
|
+
if not intelligence.weaknesses:
|
|
384
|
+
render_notice("No critical structural weaknesses detected.", style="green")
|
|
385
|
+
return
|
|
386
|
+
table = Table(title="Critical Structural Weaknesses", header_style="bold red")
|
|
387
|
+
table.add_column("Location", overflow="fold")
|
|
388
|
+
table.add_column("Referenced By", justify="right")
|
|
389
|
+
table.add_column("Failure Impact")
|
|
390
|
+
table.add_column("Recovery Difficulty")
|
|
391
|
+
table.add_column("Confidence", justify="right")
|
|
392
|
+
for weakness in intelligence.weaknesses[:12]:
|
|
393
|
+
table.add_row(
|
|
394
|
+
str(weakness.path),
|
|
395
|
+
str(weakness.referenced_by),
|
|
396
|
+
weakness.failure_impact,
|
|
397
|
+
weakness.recovery_difficulty,
|
|
398
|
+
f"{weakness.confidence:.0%}",
|
|
399
|
+
)
|
|
400
|
+
console.print(table)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _render_quake(intelligence, target: str | None = None) -> None:
|
|
404
|
+
simulation = intelligence.quake_simulation
|
|
405
|
+
render_header("Earthquake Simulation", f"Repository: {intelligence.root}")
|
|
406
|
+
if not simulation:
|
|
407
|
+
render_notice("No simulation target available.", style="yellow")
|
|
408
|
+
return
|
|
409
|
+
if target and target not in {simulation.target.name, str(simulation.target)}:
|
|
410
|
+
render_notice(
|
|
411
|
+
f"Using default target {simulation.target.name}; no separate quake model was built for {target}.",
|
|
412
|
+
style="yellow",
|
|
413
|
+
)
|
|
414
|
+
render_kv(
|
|
415
|
+
"Earthquake Simulation",
|
|
416
|
+
[
|
|
417
|
+
("Removing", str(simulation.target)),
|
|
418
|
+
("Projected Damage", str(simulation.projected_damage)),
|
|
419
|
+
("Subsystems Lost", str(simulation.subsystems_lost)),
|
|
420
|
+
("Severity", simulation.severity),
|
|
421
|
+
],
|
|
422
|
+
border_style="red",
|
|
423
|
+
)
|
|
424
|
+
if simulation.affected_files:
|
|
425
|
+
table = Table(title="Affected Files")
|
|
426
|
+
table.add_column("File", overflow="fold")
|
|
427
|
+
for path in simulation.affected_files:
|
|
428
|
+
table.add_row(str(path))
|
|
429
|
+
console.print(table)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _render_map(intelligence) -> None:
|
|
433
|
+
render_header("Repository Knowledge Map", f"Repository: {intelligence.root}")
|
|
434
|
+
tree = Tree("Core")
|
|
435
|
+
for item in intelligence.knowledge_map.core:
|
|
436
|
+
tree.add(item)
|
|
437
|
+
if intelligence.knowledge_map.dependency_graph:
|
|
438
|
+
deps = tree.add("Dependency Graph")
|
|
439
|
+
for dep in intelligence.knowledge_map.dependency_graph[:8]:
|
|
440
|
+
deps.add(dep)
|
|
441
|
+
api = tree.add("API")
|
|
442
|
+
for item in intelligence.knowledge_map.route_graph[:5]:
|
|
443
|
+
api.add(item)
|
|
444
|
+
services = tree.add("Services")
|
|
445
|
+
for item in intelligence.knowledge_map.service_graph[:5]:
|
|
446
|
+
services.add(item)
|
|
447
|
+
architecture = tree.add("Architecture")
|
|
448
|
+
for item in intelligence.knowledge_map.architecture_graph:
|
|
449
|
+
architecture.add(item)
|
|
450
|
+
console.print(tree)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _render_contributors(intelligence) -> None:
|
|
454
|
+
render_header("Developer Behavior Analysis", f"Repository: {intelligence.root}")
|
|
455
|
+
if not intelligence.contributors:
|
|
456
|
+
render_notice("No contributor data available.", style="yellow")
|
|
457
|
+
return
|
|
458
|
+
table = Table(title="Repository Custodians", header_style="bold cyan")
|
|
459
|
+
table.add_column("Area")
|
|
460
|
+
table.add_column("Owner")
|
|
461
|
+
table.add_column("Maintenance")
|
|
462
|
+
table.add_column("Abandoned")
|
|
463
|
+
for item in intelligence.contributors[:20]:
|
|
464
|
+
table.add_row(item.area, item.owner, item.maintenance_owner, item.abandoned_owner)
|
|
465
|
+
console.print(table)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _render_mutations(intelligence) -> None:
|
|
469
|
+
render_header("Evolutionary Mutation Tracking", f"Repository: {intelligence.root}")
|
|
470
|
+
if not intelligence.mutations:
|
|
471
|
+
render_notice("No major mutation detected.", style="green")
|
|
472
|
+
return
|
|
473
|
+
for mutation in intelligence.mutations:
|
|
474
|
+
render_kv(
|
|
475
|
+
"Mutation Detected",
|
|
476
|
+
[
|
|
477
|
+
("Project Type", mutation.project_type),
|
|
478
|
+
("Became", mutation.became),
|
|
479
|
+
("Date", mutation.date),
|
|
480
|
+
("Impact", mutation.impact),
|
|
481
|
+
],
|
|
482
|
+
border_style="magenta",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _render_containment_zones(intelligence) -> None:
|
|
487
|
+
render_header("Complexity Containment Zones", f"Repository: {intelligence.root}")
|
|
488
|
+
if not intelligence.containment_zones:
|
|
489
|
+
render_notice("No containment zones detected.", style="green")
|
|
490
|
+
return
|
|
491
|
+
table = Table(title="Containment Zones", header_style="bold yellow")
|
|
492
|
+
table.add_column("Location", overflow="fold")
|
|
493
|
+
table.add_column("Complexity", justify="right")
|
|
494
|
+
table.add_column("Spread Rate")
|
|
495
|
+
table.add_column("Recommendation")
|
|
496
|
+
for zone in intelligence.containment_zones[:12]:
|
|
497
|
+
table.add_row(zone.location, str(zone.complexity), zone.spread_rate, zone.recommendation)
|
|
498
|
+
console.print(table)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _render_survival(intelligence) -> None:
|
|
502
|
+
render_header("Project Survival Score", f"Repository: {intelligence.root}")
|
|
503
|
+
survival = intelligence.survival
|
|
504
|
+
if not survival:
|
|
505
|
+
render_notice("Survival score unavailable.", style="yellow")
|
|
506
|
+
return
|
|
507
|
+
render_kv(
|
|
508
|
+
"Survival Score",
|
|
509
|
+
[
|
|
510
|
+
("Score", f"{survival.score}/100"),
|
|
511
|
+
("Risk", survival.risk),
|
|
512
|
+
("Single Point Failure", survival.single_point_failure),
|
|
513
|
+
("Maintainability", str(survival.maintainability)),
|
|
514
|
+
("Recoverability", str(survival.recoverability)),
|
|
515
|
+
("Onboarding Difficulty", str(survival.onboarding_difficulty)),
|
|
516
|
+
("Bus Factor", str(survival.bus_factor)),
|
|
517
|
+
],
|
|
518
|
+
border_style="green" if survival.score >= 70 else "red",
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _render_observations(intelligence) -> None:
|
|
523
|
+
render_header("AI-Assisted Archaeological Notes", f"Repository: {intelligence.root}")
|
|
524
|
+
if not intelligence.observations:
|
|
525
|
+
render_notice("No observations generated.", style="yellow")
|
|
526
|
+
return
|
|
527
|
+
for note in intelligence.observations[:8]:
|
|
528
|
+
panel_text = note.observation
|
|
529
|
+
if note.evidence:
|
|
530
|
+
panel_text += "\n\nEvidence:\n" + "\n".join(f"- {item}" for item in note.evidence)
|
|
531
|
+
console.print(Panel(panel_text, border_style="cyan"))
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _render_inspect(root: Path, target: Path) -> None:
|
|
535
|
+
resolved = _resolve_target(root, target)
|
|
536
|
+
if not resolved.exists():
|
|
537
|
+
raise typer.BadParameter(f"Target does not exist: {target}")
|
|
538
|
+
analysis = _scan(root)
|
|
539
|
+
text_cache = analysis.intelligence.text_cache
|
|
540
|
+
references = analysis.intelligence.references
|
|
541
|
+
dependencies = analysis.intelligence.dependencies
|
|
542
|
+
file_text = text_cache.get(resolved) or read_text(resolved)
|
|
543
|
+
lines = file_text.splitlines()
|
|
544
|
+
todos = [finding for finding in find_todos([resolved])]
|
|
545
|
+
render_header("Deep Inspection", f"Target: {resolved}")
|
|
546
|
+
render_kv(
|
|
547
|
+
"Artifact Profile",
|
|
548
|
+
[
|
|
549
|
+
("Kind", path_kind(resolved)),
|
|
550
|
+
("Size", f"{safe_stat(resolved)} bytes"),
|
|
551
|
+
("Lines", str(len(lines))),
|
|
552
|
+
("Referenced By", str(len(references.get(resolved, set())))),
|
|
553
|
+
("Depends On", str(len(dependencies.get(resolved, set())))),
|
|
554
|
+
("TODO Markers", str(len(todos))),
|
|
555
|
+
],
|
|
556
|
+
border_style="cyan",
|
|
557
|
+
)
|
|
558
|
+
if todos:
|
|
559
|
+
table = Table(title="Embedded Notes", header_style="bold red")
|
|
560
|
+
table.add_column("Severity")
|
|
561
|
+
table.add_column("Line")
|
|
562
|
+
table.add_column("Comment", overflow="fold")
|
|
563
|
+
for finding in todos[:10]:
|
|
564
|
+
table.add_row(finding.severity, str(finding.line), finding.comment)
|
|
565
|
+
console.print(table)
|
|
566
|
+
if len(lines) > 0:
|
|
567
|
+
preview = _file_context(resolved, 1, context=4)
|
|
568
|
+
console.print(Panel("\n".join(preview), title="File Preview", border_style="blue"))
|
|
569
|
+
if references.get(resolved):
|
|
570
|
+
table = Table(title="Inbound References", header_style="bold magenta")
|
|
571
|
+
table.add_column("File", overflow="fold")
|
|
572
|
+
for item in sorted(references.get(resolved, set()))[:15]:
|
|
573
|
+
table.add_row(str(item))
|
|
574
|
+
console.print(table)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _render_trace(root: Path, target: str) -> None:
|
|
578
|
+
analysis = _scan(root)
|
|
579
|
+
view = analysis.intelligence.view
|
|
580
|
+
text_cache = analysis.intelligence.text_cache
|
|
581
|
+
references = analysis.intelligence.references
|
|
582
|
+
dependencies = analysis.intelligence.dependencies
|
|
583
|
+
resolved = _resolve_target(root, Path(target))
|
|
584
|
+
render_header("Trace Excavation", f"Repository: {view.root}")
|
|
585
|
+
if resolved.exists():
|
|
586
|
+
table = Table(title="Dependency Trace", header_style="bold magenta")
|
|
587
|
+
table.add_column("Direction")
|
|
588
|
+
table.add_column("File", overflow="fold")
|
|
589
|
+
for item in sorted(references.get(resolved, set()))[:20]:
|
|
590
|
+
table.add_row("Referenced By", str(item))
|
|
591
|
+
for item in sorted(dependencies.get(resolved, set()))[:20]:
|
|
592
|
+
table.add_row("Depends On", str(item))
|
|
593
|
+
console.print(table)
|
|
594
|
+
return
|
|
595
|
+
hits: list[tuple[Path, int, str]] = []
|
|
596
|
+
needle = target.lower()
|
|
597
|
+
for path, content in text_cache.items():
|
|
598
|
+
for index, line in enumerate(content.splitlines(), start=1):
|
|
599
|
+
if needle in line.lower():
|
|
600
|
+
hits.append((path, index, line.strip()))
|
|
601
|
+
if len(hits) >= 30:
|
|
602
|
+
break
|
|
603
|
+
if len(hits) >= 30:
|
|
604
|
+
break
|
|
605
|
+
if not hits:
|
|
606
|
+
render_notice(f"No trace evidence found for {escape(target)}.", style="yellow")
|
|
607
|
+
return
|
|
608
|
+
table = Table(title="Symbol Trace", header_style="bold cyan")
|
|
609
|
+
table.add_column("File", overflow="fold")
|
|
610
|
+
table.add_column("Line", justify="right")
|
|
611
|
+
table.add_column("Match", overflow="fold")
|
|
612
|
+
for path, line_number, line in hits:
|
|
613
|
+
table.add_row(str(path), str(line_number), line)
|
|
614
|
+
console.print(table)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _render_evidence(root: Path, target: str) -> None:
|
|
618
|
+
analysis = _scan(root)
|
|
619
|
+
text_cache = analysis.intelligence.text_cache
|
|
620
|
+
references = analysis.intelligence.references
|
|
621
|
+
resolved = _resolve_target(root, Path(target))
|
|
622
|
+
render_header("Evidence File", f"Repository: {analysis.intelligence.view.root}")
|
|
623
|
+
if resolved.exists():
|
|
624
|
+
lines = (text_cache.get(resolved) or read_text(resolved)).splitlines()
|
|
625
|
+
snippets = _file_context(resolved, 1, context=5)
|
|
626
|
+
render_kv(
|
|
627
|
+
"Evidence Summary",
|
|
628
|
+
[
|
|
629
|
+
("Target", str(resolved)),
|
|
630
|
+
("Referenced By", str(len(references.get(resolved, set())))),
|
|
631
|
+
("Evidence Lines", str(len(lines))),
|
|
632
|
+
("Confidence", "High" if len(references.get(resolved, set())) else "Moderate"),
|
|
633
|
+
],
|
|
634
|
+
border_style="yellow",
|
|
635
|
+
)
|
|
636
|
+
console.print(Panel("\n".join(snippets), title="Evidence Snapshot", border_style="yellow"))
|
|
637
|
+
return
|
|
638
|
+
trace_hits: list[tuple[Path, int, str]] = []
|
|
639
|
+
needle = target.lower()
|
|
640
|
+
for path, content in text_cache.items():
|
|
641
|
+
for index, line in enumerate(content.splitlines(), start=1):
|
|
642
|
+
if needle in line.lower():
|
|
643
|
+
trace_hits.append((path, index, line.strip()))
|
|
644
|
+
if len(trace_hits) >= 15:
|
|
645
|
+
break
|
|
646
|
+
if len(trace_hits) >= 15:
|
|
647
|
+
break
|
|
648
|
+
if not trace_hits:
|
|
649
|
+
render_notice(f"No evidence located for {escape(target)}.", style="yellow")
|
|
650
|
+
return
|
|
651
|
+
table = Table(title="Evidence Trail", header_style="bold yellow")
|
|
652
|
+
table.add_column("File", overflow="fold")
|
|
653
|
+
table.add_column("Line", justify="right")
|
|
654
|
+
table.add_column("Snippet", overflow="fold")
|
|
655
|
+
for path, line_number, line in trace_hits:
|
|
656
|
+
table.add_row(str(path), str(line_number), line)
|
|
657
|
+
console.print(table)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _render_bugmark(root: Path, target: Path, line: int | None, note: str | None) -> None:
|
|
661
|
+
resolved = _resolve_target(root, target)
|
|
662
|
+
if not resolved.exists():
|
|
663
|
+
raise typer.BadParameter(f"Target does not exist: {target}")
|
|
664
|
+
detected_line = line
|
|
665
|
+
if detected_line is None:
|
|
666
|
+
try:
|
|
667
|
+
for index, content_line in enumerate(read_text(resolved).splitlines(), start=1):
|
|
668
|
+
lowered = content_line.lower()
|
|
669
|
+
if any(marker in lowered for marker in ("todo", "fixme", "hack", "bug", "temp", "xxx")):
|
|
670
|
+
detected_line = index
|
|
671
|
+
break
|
|
672
|
+
except OSError:
|
|
673
|
+
detected_line = None
|
|
674
|
+
context = _file_context(resolved, detected_line, context=3)
|
|
675
|
+
title = "Bug Marker"
|
|
676
|
+
if note:
|
|
677
|
+
title = f"Bug Marker - {note}"
|
|
678
|
+
render_kv(
|
|
679
|
+
"Bugmark",
|
|
680
|
+
[
|
|
681
|
+
("File", str(resolved)),
|
|
682
|
+
("Line", str(detected_line or "auto")),
|
|
683
|
+
("Note", note or "n/a"),
|
|
684
|
+
("Status", "Highlighted" if detected_line else "Review"),
|
|
685
|
+
],
|
|
686
|
+
border_style="red",
|
|
687
|
+
)
|
|
688
|
+
console.print(Panel("\n".join(context), title=title, border_style="red"))
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _render_cleanup_plan(analysis) -> None:
|
|
692
|
+
summary = analysis.summary
|
|
693
|
+
plan = recovery.build_cleanup_plan(analysis)
|
|
694
|
+
render_header("Repository Cleanup Plan", f"Repository: {summary.root}")
|
|
695
|
+
if not plan:
|
|
696
|
+
render_notice("No cleanup priorities identified.", style="green")
|
|
697
|
+
return
|
|
698
|
+
for item in plan:
|
|
699
|
+
panel = Panel(
|
|
700
|
+
"\n".join(f"- {line}" for line in item.items),
|
|
701
|
+
title=f"Priority {item.level}",
|
|
702
|
+
border_style="red" if item.level == 1 else "yellow" if item.level == 2 else "green",
|
|
703
|
+
)
|
|
704
|
+
console.print(panel)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _render_delete_check(root: Path, target: str) -> None:
|
|
708
|
+
candidate = Path(target)
|
|
709
|
+
if not candidate.is_absolute():
|
|
710
|
+
candidate = (root / candidate).resolve()
|
|
711
|
+
analysis = recovery.analyze_deletion(candidate, root)
|
|
712
|
+
render_kv(
|
|
713
|
+
"Deletion Confidence",
|
|
714
|
+
[
|
|
715
|
+
("Safe", f"{analysis.safe_confidence:.0f}%"),
|
|
716
|
+
("Files Affected", str(analysis.affected_files)),
|
|
717
|
+
("Recommendation", analysis.recommendation),
|
|
718
|
+
],
|
|
719
|
+
border_style="yellow" if analysis.safe_confidence < 80 else "green",
|
|
720
|
+
)
|
|
721
|
+
if analysis.references or analysis.dependencies:
|
|
722
|
+
table = Table(title="Impact Radius", header_style="bold yellow")
|
|
723
|
+
table.add_column("Type")
|
|
724
|
+
table.add_column("File", overflow="fold")
|
|
725
|
+
for path in analysis.references:
|
|
726
|
+
table.add_row("Reference", str(path))
|
|
727
|
+
for path in analysis.dependencies:
|
|
728
|
+
table.add_row("Dependency", str(path))
|
|
729
|
+
console.print(table)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _render_refactor_candidates(analysis) -> None:
|
|
733
|
+
candidates = recovery.find_refactor_candidates(analysis)
|
|
734
|
+
render_header("Refactor Opportunities", f"Repository: {analysis.summary.root}")
|
|
735
|
+
if not candidates:
|
|
736
|
+
render_notice("No strong refactor candidates found.", style="green")
|
|
737
|
+
return
|
|
738
|
+
for candidate in candidates:
|
|
739
|
+
render_kv(
|
|
740
|
+
"Refactor Candidate",
|
|
741
|
+
[
|
|
742
|
+
("Function", candidate.name),
|
|
743
|
+
("Found In", "\n".join(f"- {path}" for path in candidate.locations)),
|
|
744
|
+
("Recommendation", candidate.recommendation),
|
|
745
|
+
("Confidence", f"{candidate.confidence:.0%}"),
|
|
746
|
+
],
|
|
747
|
+
border_style="magenta",
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _render_routes(analysis) -> None:
|
|
752
|
+
findings = recovery.audit_routes(analysis.intelligence.view, analysis.intelligence.text_cache, analysis.intelligence.references)
|
|
753
|
+
render_header("Route Auditing", f"Repository: {analysis.summary.root}")
|
|
754
|
+
if not findings:
|
|
755
|
+
render_notice("No route-like files detected.", style="green")
|
|
756
|
+
return
|
|
757
|
+
table = Table(title="Route Findings", header_style="bold cyan")
|
|
758
|
+
table.add_column("Kind")
|
|
759
|
+
table.add_column("File", overflow="fold")
|
|
760
|
+
table.add_column("Detail", overflow="fold")
|
|
761
|
+
table.add_column("Confidence", justify="right")
|
|
762
|
+
for finding in findings:
|
|
763
|
+
table.add_row(finding.kind, str(finding.path), finding.detail, f"{finding.confidence:.0%}")
|
|
764
|
+
console.print(table)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _render_configs(analysis) -> None:
|
|
768
|
+
findings = recovery.audit_configs(analysis.intelligence.view, analysis.intelligence.text_cache)
|
|
769
|
+
render_header("Configuration Auditing", f"Repository: {analysis.summary.root}")
|
|
770
|
+
if not findings:
|
|
771
|
+
render_notice("No stale configuration detected.", style="green")
|
|
772
|
+
return
|
|
773
|
+
table = Table(title="Configuration Findings", header_style="bold yellow")
|
|
774
|
+
table.add_column("Kind")
|
|
775
|
+
table.add_column("Name")
|
|
776
|
+
table.add_column("Locations", overflow="fold")
|
|
777
|
+
table.add_column("Confidence", justify="right")
|
|
778
|
+
for finding in findings:
|
|
779
|
+
table.add_row(finding.kind, finding.name, ", ".join(str(path) for path in finding.locations) or "n/a", f"{finding.confidence:.0%}")
|
|
780
|
+
console.print(table)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _render_migrations(analysis) -> None:
|
|
784
|
+
findings = recovery.audit_migrations(analysis.intelligence.view, analysis.intelligence.text_cache)
|
|
785
|
+
render_header("Migration Auditing", f"Repository: {analysis.summary.root}")
|
|
786
|
+
if not findings:
|
|
787
|
+
render_notice("No migration issues detected.", style="green")
|
|
788
|
+
return
|
|
789
|
+
table = Table(title="Migration Findings", header_style="bold red")
|
|
790
|
+
table.add_column("Path", overflow="fold")
|
|
791
|
+
table.add_column("Kind")
|
|
792
|
+
table.add_column("Status")
|
|
793
|
+
table.add_column("Confidence", justify="right")
|
|
794
|
+
for finding in findings:
|
|
795
|
+
table.add_row(str(finding.path), finding.kind, finding.status, f"{finding.confidence:.0%}")
|
|
796
|
+
console.print(table)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _render_dependency_rationalization(analysis) -> None:
|
|
800
|
+
warnings = recovery.rationalize_dependencies(analysis)
|
|
801
|
+
render_header("Dependency Rationalization", f"Repository: {analysis.summary.root}")
|
|
802
|
+
if not warnings:
|
|
803
|
+
render_notice("No dependency rationalization warnings found.", style="green")
|
|
804
|
+
return
|
|
805
|
+
table = Table(title="Dependency Warnings", header_style="bold red")
|
|
806
|
+
table.add_column("Package")
|
|
807
|
+
table.add_column("Used For", justify="right")
|
|
808
|
+
table.add_column("Recommendation", overflow="fold")
|
|
809
|
+
table.add_column("Confidence", justify="right")
|
|
810
|
+
for warning in warnings:
|
|
811
|
+
table.add_row(warning.name, str(warning.only_used_for), warning.recommendation, f"{warning.confidence:.0%}")
|
|
812
|
+
console.print(table)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _render_drift(analysis) -> None:
|
|
816
|
+
drift = recovery.detect_drift(analysis)
|
|
817
|
+
render_kv(
|
|
818
|
+
"Architecture Drift",
|
|
819
|
+
[
|
|
820
|
+
("Original", drift.original),
|
|
821
|
+
("Current", drift.current),
|
|
822
|
+
("Drift Severity", drift.severity),
|
|
823
|
+
("Primary Cause", drift.cause),
|
|
824
|
+
],
|
|
825
|
+
border_style="magenta",
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _render_pr_report(analysis) -> None:
|
|
830
|
+
pr = recovery.build_pr_report(analysis)
|
|
831
|
+
render_header("Automated Pull Request Notes", f"Repository: {analysis.summary.root}")
|
|
832
|
+
table = Table(title="Technical Debt Changes", header_style="bold green")
|
|
833
|
+
table.add_column("Section")
|
|
834
|
+
table.add_column("Items", overflow="fold")
|
|
835
|
+
if pr.removed:
|
|
836
|
+
table.add_row("Removed", "\n".join(f"- {item}" for item in pr.removed))
|
|
837
|
+
if pr.reduced:
|
|
838
|
+
table.add_row("Reduced", "\n".join(f"- {item}" for item in pr.reduced))
|
|
839
|
+
if pr.improved:
|
|
840
|
+
table.add_row("Improved", "\n".join(f"- {item}" for item in pr.improved))
|
|
841
|
+
console.print(table)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _render_status(analysis) -> None:
|
|
845
|
+
status = recovery.build_status_summary(analysis)
|
|
846
|
+
render_header("Repository Maintenance Dashboard", f"Repository: {analysis.summary.root}")
|
|
847
|
+
render_kv(
|
|
848
|
+
"Maintenance Status",
|
|
849
|
+
[
|
|
850
|
+
("Debt", str(status.debt)),
|
|
851
|
+
("Complexity", str(status.complexity)),
|
|
852
|
+
("Dead Code", str(status.dead_code)),
|
|
853
|
+
("Route Count", str(status.route_count)),
|
|
854
|
+
("Dependency Count", str(status.dependency_count)),
|
|
855
|
+
("Cleanup Opportunities", str(status.cleanup_opportunities)),
|
|
856
|
+
],
|
|
857
|
+
border_style="cyan",
|
|
858
|
+
)
|
|
859
|
+
if status.recommendations:
|
|
860
|
+
console.print(Panel("\n".join(f"- {item}" for item in status.recommendations), title="Recommendations", border_style="cyan"))
|
|
861
|
+
baseline = maintenance.load_baseline(analysis.summary.root)
|
|
862
|
+
if baseline:
|
|
863
|
+
render_kv(
|
|
864
|
+
"Baseline Context",
|
|
865
|
+
[
|
|
866
|
+
("Captured", baseline.captured_at),
|
|
867
|
+
("Baseline Health", f"{baseline.health_score}/100"),
|
|
868
|
+
("Baseline Complexity", str(baseline.complexity_score)),
|
|
869
|
+
],
|
|
870
|
+
border_style="green",
|
|
871
|
+
)
|
|
872
|
+
history = maintenance.history_points(analysis.summary.root)
|
|
873
|
+
if history:
|
|
874
|
+
render_notice(f"Maintenance history entries tracked: {len(history)}", style="cyan")
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _render_baseline(analysis) -> None:
|
|
878
|
+
snapshot = maintenance.save_baseline(analysis)
|
|
879
|
+
render_kv(
|
|
880
|
+
"Baseline Snapshot",
|
|
881
|
+
[
|
|
882
|
+
("Captured", snapshot.captured_at),
|
|
883
|
+
("Files", str(snapshot.file_count)),
|
|
884
|
+
("Dependencies", str(snapshot.dependency_count)),
|
|
885
|
+
("Complexity", str(snapshot.complexity_score)),
|
|
886
|
+
("Dead Code", str(snapshot.dead_code_count)),
|
|
887
|
+
("Duplicates", str(snapshot.duplicate_code_count)),
|
|
888
|
+
("Routes", str(snapshot.route_count)),
|
|
889
|
+
("Health", f"{snapshot.health_score}/100"),
|
|
890
|
+
],
|
|
891
|
+
border_style="green",
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _render_regressions(analysis) -> None:
|
|
896
|
+
current = maintenance.build_snapshot(analysis)
|
|
897
|
+
baseline = maintenance.load_baseline(analysis.summary.root)
|
|
898
|
+
render_header("Regression Detection", f"Repository: {analysis.summary.root}")
|
|
899
|
+
if baseline is None:
|
|
900
|
+
render_notice("No baseline found. Run `devarch baseline .` first.", style="yellow")
|
|
901
|
+
return
|
|
902
|
+
report = maintenance.compare_to_baseline(current, baseline)
|
|
903
|
+
render_kv(
|
|
904
|
+
"Regression Report",
|
|
905
|
+
[
|
|
906
|
+
("Complexity", f"{baseline.complexity_score} -> {current.complexity_score} ({report.complexity_delta:+.1f}%)"),
|
|
907
|
+
("Dead Code", f"{baseline.dead_code_count} -> {current.dead_code_count} ({report.dead_code_delta:+d})"),
|
|
908
|
+
("Duplicate Logic", f"{baseline.duplicate_code_count} -> {current.duplicate_code_count} ({report.duplicate_delta:+d})"),
|
|
909
|
+
("Health Score", f"{baseline.health_score} -> {current.health_score} ({report.health_delta:+d})"),
|
|
910
|
+
("Status", report.status),
|
|
911
|
+
],
|
|
912
|
+
border_style="red" if report.status == "Regressed" else "green",
|
|
913
|
+
)
|
|
914
|
+
maintenance.append_history(analysis, label="regression-check")
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _load_budget_config(path: Path, config: Optional[Path]) -> maintenance.BudgetLimits:
|
|
918
|
+
return maintenance.read_budget_limits(path, config=config)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _render_budget(analysis, config: Optional[Path] = None) -> None:
|
|
922
|
+
current = maintenance.build_snapshot(analysis)
|
|
923
|
+
limits = _load_budget_config(analysis.summary.root, config)
|
|
924
|
+
result = maintenance.evaluate_budget(current, limits)
|
|
925
|
+
render_header("Technical Debt Budget", f"Repository: {analysis.summary.root}")
|
|
926
|
+
rows = [
|
|
927
|
+
("Dead Files", f"{current.dead_code_count} / {limits.max_dead_files}"),
|
|
928
|
+
("Complexity", f"{current.complexity_score} / {limits.max_complexity}"),
|
|
929
|
+
("Duplicate Blocks", f"{current.duplicate_code_count} / {limits.max_duplicate_blocks}"),
|
|
930
|
+
("TODOs", f"{current.todo_count} / {limits.max_todos}"),
|
|
931
|
+
("Routes", f"{current.route_count} / {limits.max_routes}"),
|
|
932
|
+
("Dependencies", f"{current.dependency_count} / {limits.max_dependencies}"),
|
|
933
|
+
("Health", f"{current.health_score} / {limits.min_health_score}"),
|
|
934
|
+
("Status", result.status),
|
|
935
|
+
]
|
|
936
|
+
render_kv("Technical Debt Budget", rows, border_style="yellow" if result.exceeded else "green")
|
|
937
|
+
if result.exceeded:
|
|
938
|
+
table = Table(title="Budget Exceeded", header_style="bold red")
|
|
939
|
+
table.add_column("Metric")
|
|
940
|
+
table.add_column("Current", justify="right")
|
|
941
|
+
table.add_column("Limit", justify="right")
|
|
942
|
+
for label, value, limit in result.exceeded:
|
|
943
|
+
table.add_row(label, str(value), str(limit))
|
|
944
|
+
console.print(table)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _render_release_check(analysis, config: Optional[Path] = None) -> None:
|
|
948
|
+
baseline = maintenance.load_baseline(analysis.summary.root)
|
|
949
|
+
limits = _load_budget_config(analysis.summary.root, config)
|
|
950
|
+
report = maintenance.release_check(analysis, baseline=baseline, limits=limits)
|
|
951
|
+
render_header("Release Readiness", f"Repository: {analysis.summary.root}")
|
|
952
|
+
render_kv(
|
|
953
|
+
"Release Readiness",
|
|
954
|
+
[
|
|
955
|
+
("Score", f"{report.score}/100"),
|
|
956
|
+
("Status", report.status),
|
|
957
|
+
("Warnings", str(len(report.warnings))),
|
|
958
|
+
],
|
|
959
|
+
border_style="green" if report.status == "Ready" else "yellow",
|
|
960
|
+
)
|
|
961
|
+
if report.warnings:
|
|
962
|
+
console.print(Panel("\n".join(f"- {item}" for item in report.warnings[:10]), title="Warnings", border_style="yellow"))
|
|
963
|
+
if report.blockers:
|
|
964
|
+
console.print(Panel("\n".join(f"- {item}" for item in report.blockers), title="Blockers", border_style="red"))
|
|
965
|
+
maintenance.append_history(analysis, label="release-check")
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _render_ownership(analysis) -> None:
|
|
969
|
+
findings = maintenance.ownership_report(analysis)
|
|
970
|
+
render_header("Ownership Analysis", f"Repository: {analysis.summary.root}")
|
|
971
|
+
if not findings:
|
|
972
|
+
render_notice("No obvious ownership gaps detected.", style="green")
|
|
973
|
+
return
|
|
974
|
+
table = Table(title="Ownership Warnings", header_style="bold yellow")
|
|
975
|
+
table.add_column("Module")
|
|
976
|
+
table.add_column("Last Significant Activity", justify="right")
|
|
977
|
+
table.add_column("Primary Maintainer")
|
|
978
|
+
table.add_column("Status")
|
|
979
|
+
for item in findings:
|
|
980
|
+
table.add_row(item.module, f"{item.last_significant_activity_days} days", item.primary_maintainer, item.status)
|
|
981
|
+
console.print(table)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _render_dependency_health(analysis) -> None:
|
|
985
|
+
alerts = maintenance.dependency_health_report(analysis)
|
|
986
|
+
render_header("Dependency Lifecycle Monitoring", f"Repository: {analysis.summary.root}")
|
|
987
|
+
if not alerts:
|
|
988
|
+
render_notice("No dependency alerts found.", style="green")
|
|
989
|
+
return
|
|
990
|
+
table = Table(title="Dependency Alerts", header_style="bold red")
|
|
991
|
+
table.add_column("Package")
|
|
992
|
+
table.add_column("Status")
|
|
993
|
+
table.add_column("Used By", justify="right")
|
|
994
|
+
table.add_column("Recommendation", overflow="fold")
|
|
995
|
+
for item in alerts:
|
|
996
|
+
table.add_row(item.package, item.status, str(item.used_by), item.recommendation)
|
|
997
|
+
console.print(table)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _render_cleanup_queue(analysis) -> None:
|
|
1001
|
+
queue = maintenance.cleanup_candidates(analysis)
|
|
1002
|
+
render_header("Cleanup Candidates", f"Repository: {analysis.summary.root}")
|
|
1003
|
+
if not queue:
|
|
1004
|
+
render_notice("No cleanup queue available.", style="green")
|
|
1005
|
+
return
|
|
1006
|
+
table = Table(title="Top Cleanup Opportunities", header_style="bold green")
|
|
1007
|
+
table.add_column("#", justify="right")
|
|
1008
|
+
table.add_column("Opportunity", overflow="fold")
|
|
1009
|
+
for index, item in enumerate(queue[:10], start=1):
|
|
1010
|
+
table.add_row(str(index), item)
|
|
1011
|
+
console.print(table)
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _render_standards(analysis) -> None:
|
|
1015
|
+
report = maintenance.standards_report(analysis)
|
|
1016
|
+
render_header("Repository Standards Enforcement", f"Repository: {analysis.summary.root}")
|
|
1017
|
+
render_kv(
|
|
1018
|
+
"Standards Report",
|
|
1019
|
+
[
|
|
1020
|
+
("Naming", f"{report.naming}%"),
|
|
1021
|
+
("Documentation", f"{report.documentation}%"),
|
|
1022
|
+
("Consistency", f"{report.consistency}%"),
|
|
1023
|
+
("Test Coverage", f"{report.test_coverage}%"),
|
|
1024
|
+
],
|
|
1025
|
+
border_style="cyan",
|
|
1026
|
+
)
|
|
1027
|
+
if report.notes:
|
|
1028
|
+
console.print(Panel("\n".join(f"- {item}" for item in report.notes), title="Notes", border_style="yellow"))
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _render_history(analysis) -> None:
|
|
1032
|
+
points = maintenance.history_points(analysis.summary.root)
|
|
1033
|
+
render_header("Repository Health History", f"Repository: {analysis.summary.root}")
|
|
1034
|
+
if not points:
|
|
1035
|
+
render_notice("No maintenance history recorded yet.", style="yellow")
|
|
1036
|
+
return
|
|
1037
|
+
table = Table(title="Health History", header_style="bold cyan")
|
|
1038
|
+
table.add_column("Captured At")
|
|
1039
|
+
table.add_column("Health", justify="right")
|
|
1040
|
+
table.add_column("Complexity", justify="right")
|
|
1041
|
+
table.add_column("Dead Code", justify="right")
|
|
1042
|
+
table.add_column("Duplicates", justify="right")
|
|
1043
|
+
table.add_column("Label")
|
|
1044
|
+
for point in points[-10:]:
|
|
1045
|
+
table.add_row(point.captured_at, str(point.health_score), str(point.complexity_score), str(point.dead_code_count), str(point.duplicate_code_count), point.label)
|
|
1046
|
+
console.print(table)
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _render_recommendations(analysis) -> None:
|
|
1050
|
+
items = maintenance.recommendation_items(analysis)
|
|
1051
|
+
render_header("Automatic Recommendations", f"Repository: {analysis.summary.root}")
|
|
1052
|
+
if not items:
|
|
1053
|
+
render_notice("No actionable recommendations available.", style="green")
|
|
1054
|
+
return
|
|
1055
|
+
for index, item in enumerate(items, start=1):
|
|
1056
|
+
render_kv(
|
|
1057
|
+
f"Recommendation #{index}",
|
|
1058
|
+
[
|
|
1059
|
+
("Action", item.title),
|
|
1060
|
+
("Current", item.current),
|
|
1061
|
+
("Target", item.target),
|
|
1062
|
+
("Potential Reduction", item.potential_reduction),
|
|
1063
|
+
],
|
|
1064
|
+
border_style="magenta",
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _render_prescription(analysis) -> None:
|
|
1069
|
+
prescription = maintenance.prescribe_repository(analysis)
|
|
1070
|
+
render_header("Repository Prescription", f"Repository: {analysis.summary.root}")
|
|
1071
|
+
render_kv(
|
|
1072
|
+
"Immediate Actions",
|
|
1073
|
+
[
|
|
1074
|
+
("Actions", "\n".join(f"- {item}" for item in prescription.immediate_actions) or "n/a"),
|
|
1075
|
+
("Estimated Time", f"{prescription.estimated_time_minutes // 60} hours {prescription.estimated_time_minutes % 60} minutes"),
|
|
1076
|
+
("Expected Health Increase", f"+{prescription.expected_health_increase} points"),
|
|
1077
|
+
],
|
|
1078
|
+
border_style="green",
|
|
1079
|
+
)
|
|
1080
|
+
if prescription.findings:
|
|
1081
|
+
table = Table(title="Remediation Prescriptions", header_style="bold magenta")
|
|
1082
|
+
table.add_column("Problem", overflow="fold")
|
|
1083
|
+
table.add_column("Fix", overflow="fold")
|
|
1084
|
+
table.add_column("Effort", justify="right")
|
|
1085
|
+
table.add_column("Risk")
|
|
1086
|
+
table.add_column("Confidence", justify="right")
|
|
1087
|
+
for finding in prescription.findings:
|
|
1088
|
+
table.add_row(finding.problem, finding.recommended_fix, finding.estimated_effort, finding.risk_level, f"{finding.confidence:.0%}")
|
|
1089
|
+
console.print(table)
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def _render_repair_plan(analysis) -> None:
|
|
1093
|
+
plan = maintenance.repair_plan(analysis)
|
|
1094
|
+
render_header("Repository Repair Mode", f"Repository: {analysis.summary.root}")
|
|
1095
|
+
if not plan:
|
|
1096
|
+
render_notice("No repair plan available.", style="green")
|
|
1097
|
+
return
|
|
1098
|
+
for week in plan:
|
|
1099
|
+
render_kv(
|
|
1100
|
+
f"Week {week.week}",
|
|
1101
|
+
[
|
|
1102
|
+
("Focus", week.focus),
|
|
1103
|
+
("Actions", "\n".join(f"- {item}" for item in week.actions)),
|
|
1104
|
+
("Expected Health", week.expected_health),
|
|
1105
|
+
],
|
|
1106
|
+
border_style="cyan",
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _render_scan_summary(analysis) -> None:
|
|
1111
|
+
summary = analysis.summary
|
|
1112
|
+
intelligence = analysis.intelligence
|
|
1113
|
+
render_header("Dev Archaeologist", f"Repository: {summary.root}")
|
|
1114
|
+
render_kv(
|
|
1115
|
+
"Excavation Summary",
|
|
1116
|
+
[
|
|
1117
|
+
("Artifacts", str(summary.artifact_count)),
|
|
1118
|
+
("Ancient", str(summary.ancient_count)),
|
|
1119
|
+
("TODOs", str(summary.todo_count)),
|
|
1120
|
+
("Duplicates", str(summary.duplicate_count)),
|
|
1121
|
+
("Health", f"{summary.health_score}/100"),
|
|
1122
|
+
("Status", summary.health_status),
|
|
1123
|
+
("Badge", health_badge(summary.health_score)),
|
|
1124
|
+
],
|
|
1125
|
+
border_style="green",
|
|
1126
|
+
)
|
|
1127
|
+
render_notice(
|
|
1128
|
+
f"Repository Health: {summary.health_score}/100 | Status: {summary.health_status} | Badge: {health_badge(summary.health_score)}",
|
|
1129
|
+
style="cyan",
|
|
1130
|
+
)
|
|
1131
|
+
_print_timeline(summary.timeline)
|
|
1132
|
+
_render_dna(intelligence)
|
|
1133
|
+
confidence = summary.extra.get("artifact_confidence", {})
|
|
1134
|
+
render_kv(
|
|
1135
|
+
"Artifact Confidence",
|
|
1136
|
+
[
|
|
1137
|
+
("Dead Code", f"{confidence.get('dead_code', 0):.0%}" if confidence.get("dead_code") is not None else "n/a"),
|
|
1138
|
+
("Ancient File", f"{confidence.get('ancient_file', 0):.0%}" if confidence.get("ancient_file") is not None else "n/a"),
|
|
1139
|
+
("Duplicate Logic", f"{confidence.get('duplicate_block', 0):.0%}" if confidence.get("duplicate_block") is not None else "n/a"),
|
|
1140
|
+
],
|
|
1141
|
+
border_style="green",
|
|
1142
|
+
)
|
|
1143
|
+
_render_personality(intelligence)
|
|
1144
|
+
_render_forecast(intelligence)
|
|
1145
|
+
render_artifacts("Excavated Artifacts", summary.artifacts[:25])
|
|
1146
|
+
remediation = summary.extra.get("remediation", [])
|
|
1147
|
+
if remediation:
|
|
1148
|
+
table = Table(title="Remediation Actions", header_style="bold magenta")
|
|
1149
|
+
table.add_column("Problem", overflow="fold")
|
|
1150
|
+
table.add_column("Impact")
|
|
1151
|
+
table.add_column("Fix", overflow="fold")
|
|
1152
|
+
table.add_column("Effort", justify="right")
|
|
1153
|
+
table.add_column("Risk")
|
|
1154
|
+
for item in remediation[:12]:
|
|
1155
|
+
table.add_row(item["problem"], item["impact"], item["recommended_fix"], item["estimated_effort"], item["risk_level"])
|
|
1156
|
+
console.print(table)
|
|
1157
|
+
if summary.warnings:
|
|
1158
|
+
render_notice("\n".join(f"- {warning}" for warning in summary.warnings), style="yellow")
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def _export_report(fmt: str, path: Path, output: Optional[Path]) -> Path:
|
|
1162
|
+
analysis = _scan(path)
|
|
1163
|
+
default_names = {
|
|
1164
|
+
"json": "devarch-report.json",
|
|
1165
|
+
"markdown": "devarch-report.md",
|
|
1166
|
+
"html": "devarch-report.html",
|
|
1167
|
+
"pdf": "devarch-report.pdf",
|
|
1168
|
+
}
|
|
1169
|
+
destination = output or path / default_names[fmt]
|
|
1170
|
+
if fmt == "json":
|
|
1171
|
+
export_json(analysis.summary, destination)
|
|
1172
|
+
elif fmt == "markdown":
|
|
1173
|
+
export_markdown(analysis.summary, destination)
|
|
1174
|
+
elif fmt == "html":
|
|
1175
|
+
export_html(analysis.summary, destination)
|
|
1176
|
+
elif fmt == "pdf":
|
|
1177
|
+
export_pdf(analysis.summary, destination)
|
|
1178
|
+
else:
|
|
1179
|
+
raise typer.BadParameter(f"Unsupported report format: {fmt}")
|
|
1180
|
+
return destination
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
@app.command()
|
|
1184
|
+
def scan(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1185
|
+
"""Perform a complete archaeological excavation."""
|
|
1186
|
+
_render_scan_summary(_scan(path))
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
@app.command("help")
|
|
1190
|
+
def help_command() -> None:
|
|
1191
|
+
"""Show the full command catalog."""
|
|
1192
|
+
_render_help_catalog()
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
@app.command()
|
|
1196
|
+
def ancient(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1197
|
+
"""Find ancient, likely abandoned files."""
|
|
1198
|
+
view = collect_repository(path)
|
|
1199
|
+
text_cache = build_text_index(view)
|
|
1200
|
+
references = build_reference_map(view, text_cache)
|
|
1201
|
+
artifacts = find_ancient_files(view.files, references)
|
|
1202
|
+
render_header("Ancient Excavation", f"Repository: {view.root}")
|
|
1203
|
+
if artifacts:
|
|
1204
|
+
for artifact in artifacts:
|
|
1205
|
+
console.print(
|
|
1206
|
+
Panel(
|
|
1207
|
+
f"[bold]File:[/bold]\n{artifact.path}\n\n"
|
|
1208
|
+
f"[bold]Age:[/bold]\n{artifact.age_days} days\n\n"
|
|
1209
|
+
f"[bold]Status:[/bold]\n{artifact.detail}\n\n"
|
|
1210
|
+
f"[bold]Risk:[/bold]\n{artifact.risk}\n\n"
|
|
1211
|
+
f"[bold]Confidence:[/bold]\n{_confidence_text(artifact.confidence)}",
|
|
1212
|
+
title="Ancient Artifact Found",
|
|
1213
|
+
border_style="magenta",
|
|
1214
|
+
)
|
|
1215
|
+
)
|
|
1216
|
+
else:
|
|
1217
|
+
render_notice("No ancient artifacts detected.", style="green")
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
@app.command("dead-code")
|
|
1221
|
+
def dead_code(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1222
|
+
"""Detect unused modules, unreferenced files, orphaned components, and unreachable code."""
|
|
1223
|
+
view = collect_repository(path)
|
|
1224
|
+
text_cache = build_text_index(view)
|
|
1225
|
+
artifacts = find_dead_code(view.root, view.files, text_cache)
|
|
1226
|
+
render_header("Dead Code Excavation", f"Repository: {view.root}")
|
|
1227
|
+
render_artifacts("Potentially Safe to Remove", artifacts)
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
@app.command()
|
|
1231
|
+
def todos(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1232
|
+
"""Search for TODO, FIXME, HACK, BUG, TEMP, and XXX markers."""
|
|
1233
|
+
view = collect_repository(path)
|
|
1234
|
+
findings = find_todos(view.files)
|
|
1235
|
+
render_header("TODO Excavation", f"Repository: {view.root}")
|
|
1236
|
+
if not findings:
|
|
1237
|
+
render_notice("No TODO markers found.", style="green")
|
|
1238
|
+
return
|
|
1239
|
+
table = Table(title="Excavated Notes", header_style="bold red")
|
|
1240
|
+
table.add_column("Severity")
|
|
1241
|
+
table.add_column("File")
|
|
1242
|
+
table.add_column("Line")
|
|
1243
|
+
table.add_column("Comment", overflow="fold")
|
|
1244
|
+
for finding in findings:
|
|
1245
|
+
table.add_row(finding.severity, str(finding.file), str(finding.line), finding.comment)
|
|
1246
|
+
console.print(table)
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
@app.command()
|
|
1250
|
+
def duplicates(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1251
|
+
"""Identify duplicated functions, copied blocks, and repeated logic."""
|
|
1252
|
+
view = collect_repository(path)
|
|
1253
|
+
text_cache = build_text_index(view)
|
|
1254
|
+
artifacts = find_duplicates(view.files, text_cache)
|
|
1255
|
+
pairs = similarity_report(view.files, text_cache)
|
|
1256
|
+
render_header("Duplicate Detector", f"Repository: {view.root}")
|
|
1257
|
+
render_artifacts("Duplicate Blocks", artifacts)
|
|
1258
|
+
if pairs:
|
|
1259
|
+
table = Table(title="Similarity Percentages")
|
|
1260
|
+
table.add_column("Left")
|
|
1261
|
+
table.add_column("Right")
|
|
1262
|
+
table.add_column("Similarity")
|
|
1263
|
+
for item in pairs[:15]:
|
|
1264
|
+
table.add_row(str(item["left"]), str(item["right"]), f'{item["similarity"]}%')
|
|
1265
|
+
console.print(table)
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
@app.command()
|
|
1269
|
+
def monsters(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1270
|
+
"""Detect files with excessive line count, complexity, or dependency count."""
|
|
1271
|
+
view = collect_repository(path)
|
|
1272
|
+
artifacts = find_monsters(view.files)
|
|
1273
|
+
render_header("Monster File Detection", f"Repository: {view.root}")
|
|
1274
|
+
if artifacts:
|
|
1275
|
+
for artifact in artifacts:
|
|
1276
|
+
console.print(
|
|
1277
|
+
Panel(
|
|
1278
|
+
f"[bold]File:[/bold]\n{artifact.path}\n\n"
|
|
1279
|
+
f"[bold]Lines:[/bold]\n{artifact.metadata.get('lines', 'n/a')}\n\n"
|
|
1280
|
+
f"[bold]Complexity:[/bold]\n{artifact.metadata.get('complexity', 'n/a')}\n\n"
|
|
1281
|
+
f"[bold]Threat Level:[/bold]\n{artifact.risk}\n\n"
|
|
1282
|
+
f"[bold]Confidence:[/bold]\n{_confidence_text(artifact.confidence)}",
|
|
1283
|
+
title="Monster Discovered",
|
|
1284
|
+
border_style="red",
|
|
1285
|
+
)
|
|
1286
|
+
)
|
|
1287
|
+
else:
|
|
1288
|
+
render_notice("No monster files detected.", style="green")
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
@app.command()
|
|
1292
|
+
def ruins(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1293
|
+
"""Find empty folders, abandoned directories, and unused assets."""
|
|
1294
|
+
view = collect_repository(path)
|
|
1295
|
+
text_cache = build_text_index(view)
|
|
1296
|
+
artifacts = find_empty_directories(view.directories, view.files) + find_unused_assets(view.files, text_cache)
|
|
1297
|
+
render_header("Ruins Survey", f"Repository: {view.root}")
|
|
1298
|
+
render_artifacts("Empty Structures", artifacts)
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
@app.command()
|
|
1302
|
+
def suspicious(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1303
|
+
"""Detect suspicious backup-like filenames."""
|
|
1304
|
+
view = collect_repository(path)
|
|
1305
|
+
artifacts = find_suspicious(view.files)
|
|
1306
|
+
render_header("Suspicious Artifact Sweep", f"Repository: {view.root}")
|
|
1307
|
+
render_artifacts("Suspicious Files", artifacts)
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
@app.command()
|
|
1311
|
+
def investigate(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1312
|
+
"""Perform forensic analysis on the repository."""
|
|
1313
|
+
_render_investigation(_scan(path).intelligence)
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
@app.command()
|
|
1317
|
+
def inspect(
|
|
1318
|
+
target: Path,
|
|
1319
|
+
root: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1320
|
+
) -> None:
|
|
1321
|
+
"""Inspect a specific file or artifact in more detail."""
|
|
1322
|
+
_render_inspect(root, target)
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
@app.command()
|
|
1326
|
+
def trace(
|
|
1327
|
+
target: str,
|
|
1328
|
+
root: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1329
|
+
) -> None:
|
|
1330
|
+
"""Trace references, dependencies, or symbol mentions across the repository."""
|
|
1331
|
+
_render_trace(root, target)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
@app.command()
|
|
1335
|
+
def evidence(
|
|
1336
|
+
target: str,
|
|
1337
|
+
root: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1338
|
+
) -> None:
|
|
1339
|
+
"""Show direct evidence for a file, symbol, or suspicious string."""
|
|
1340
|
+
_render_evidence(root, target)
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
@app.command()
|
|
1344
|
+
def bugmark(
|
|
1345
|
+
target: Path,
|
|
1346
|
+
root: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1347
|
+
line: Optional[int] = typer.Option(None, "--line", "-l", help="Line number to highlight."),
|
|
1348
|
+
note: Optional[str] = typer.Option(None, "--note", "-n", help="Optional note for the highlighted bug."),
|
|
1349
|
+
) -> None:
|
|
1350
|
+
"""Highlight a suspected bug location without modifying files."""
|
|
1351
|
+
_render_bugmark(root, target, line, note)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
@app.command("errorcode")
|
|
1355
|
+
def errorcode(
|
|
1356
|
+
error_text: str,
|
|
1357
|
+
) -> None:
|
|
1358
|
+
"""Explain a build, runtime, or dependency error in plain language."""
|
|
1359
|
+
explanation = _explain_error_text(error_text)
|
|
1360
|
+
render_kv(
|
|
1361
|
+
"Error Code Excavation",
|
|
1362
|
+
[
|
|
1363
|
+
("Input", error_text),
|
|
1364
|
+
("Classification", explanation["classification"]),
|
|
1365
|
+
("Likely Cause", explanation["cause"]),
|
|
1366
|
+
("Recommended Fix", explanation["fix"]),
|
|
1367
|
+
],
|
|
1368
|
+
border_style="red",
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
@app.command()
|
|
1373
|
+
def weaknesses(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1374
|
+
"""Identify single points of failure and fragile dependency chains."""
|
|
1375
|
+
_render_weaknesses(_scan(path).intelligence)
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
@app.command()
|
|
1379
|
+
def quake(
|
|
1380
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1381
|
+
target: Optional[str] = typer.Option(None, "--target", "-t", help="Target file or module to simulate removing."),
|
|
1382
|
+
) -> None:
|
|
1383
|
+
"""Simulate the breakage radius of deleting a file, class, or module."""
|
|
1384
|
+
_render_quake(_scan(path).intelligence, target=target)
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
@app.command()
|
|
1388
|
+
def architecture(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1389
|
+
"""Identify the repository's architectural pattern."""
|
|
1390
|
+
_render_architecture(_scan(path).intelligence)
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
@app.command()
|
|
1394
|
+
def contributors(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1395
|
+
"""Analyze feature ownership and maintenance custodians."""
|
|
1396
|
+
_render_contributors(_scan(path).intelligence)
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
@app.command()
|
|
1400
|
+
def mutations(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1401
|
+
"""Track major repository transformations."""
|
|
1402
|
+
_render_mutations(_scan(path).intelligence)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
@app.command()
|
|
1406
|
+
def map(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1407
|
+
"""Generate the repository knowledge map."""
|
|
1408
|
+
_render_map(_scan(path).intelligence)
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
@app.command()
|
|
1412
|
+
def survival(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1413
|
+
"""Estimate maintainability, recoverability, and bus factor."""
|
|
1414
|
+
_render_survival(_scan(path).intelligence)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
@app.command()
|
|
1418
|
+
def notes(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1419
|
+
"""Generate AI-assisted archaeological notes."""
|
|
1420
|
+
_render_observations(_scan(path).intelligence)
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
@app.command()
|
|
1424
|
+
def dependencies(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1425
|
+
"""Analyze internal imports, external packages, cycles, and dependency hubs."""
|
|
1426
|
+
_render_deps(_scan(path).intelligence)
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
@app.command()
|
|
1430
|
+
def genealogy(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1431
|
+
"""Discover parent modules, child modules, and inherited classes."""
|
|
1432
|
+
_render_genealogy(_scan(path).intelligence)
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
@app.command()
|
|
1436
|
+
def civilizations(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1437
|
+
"""Detect abandoned clusters of code that look like lost systems."""
|
|
1438
|
+
_render_civilizations(_scan(path).intelligence)
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
@app.command()
|
|
1442
|
+
def debt(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1443
|
+
"""Generate a repository-wide debt heatmap."""
|
|
1444
|
+
_render_debt(_scan(path).intelligence)
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
@app.command()
|
|
1448
|
+
def timeline(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1449
|
+
"""Summarize repository growth and activity by Git era."""
|
|
1450
|
+
analysis = _scan(path)
|
|
1451
|
+
render_header("Repository Evolution Timeline", f"Repository: {analysis.summary.root}")
|
|
1452
|
+
_print_timeline(analysis.summary.timeline)
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
@app.command()
|
|
1456
|
+
def personality(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1457
|
+
"""Classify the repository's working style."""
|
|
1458
|
+
_render_personality(_scan(path).intelligence)
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
@app.command()
|
|
1462
|
+
def forecast(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1463
|
+
"""Predict likely maintenance trajectory."""
|
|
1464
|
+
_render_forecast(_scan(path).intelligence)
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
@app.command()
|
|
1468
|
+
def plan(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1469
|
+
"""Generate a prioritized cleanup roadmap."""
|
|
1470
|
+
_render_cleanup_plan(_scan(path))
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
@app.command("delete-check")
|
|
1474
|
+
def delete_check(
|
|
1475
|
+
target: str,
|
|
1476
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1477
|
+
) -> None:
|
|
1478
|
+
"""Analyze whether a file is safe to remove."""
|
|
1479
|
+
_render_delete_check(path, target)
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
@app.command()
|
|
1483
|
+
def refactor(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1484
|
+
"""Detect duplicate logic and oversized classes."""
|
|
1485
|
+
_render_refactor_candidates(_scan(path))
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
@app.command()
|
|
1489
|
+
def routes(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1490
|
+
"""Audit routes for unused, unreachable, or undocumented endpoints."""
|
|
1491
|
+
_render_routes(_scan(path))
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
@app.command()
|
|
1495
|
+
def configs(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1496
|
+
"""Detect stale environment variables and conflicting config."""
|
|
1497
|
+
_render_configs(_scan(path))
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
@app.command()
|
|
1501
|
+
def migrations(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1502
|
+
"""Inspect migrations for orphaned or incomplete states."""
|
|
1503
|
+
_render_migrations(_scan(path))
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
@app.command("deps")
|
|
1507
|
+
def deps(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1508
|
+
"""Detect duplicate packages and oversized dependency chains."""
|
|
1509
|
+
_render_dependency_rationalization(_scan(path))
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
@app.command()
|
|
1513
|
+
def drift(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1514
|
+
"""Compare current architecture against repository history."""
|
|
1515
|
+
_render_drift(_scan(path))
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
@app.command("pr-report")
|
|
1519
|
+
def pr_report(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1520
|
+
"""Generate pull request notes for maintenance changes."""
|
|
1521
|
+
_render_pr_report(_scan(path))
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
@app.command()
|
|
1525
|
+
def status(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1526
|
+
"""Show a single-command repository maintenance dashboard."""
|
|
1527
|
+
_render_status(_scan(path))
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
@app.command()
|
|
1531
|
+
def baseline(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1532
|
+
"""Create and store a repository snapshot baseline."""
|
|
1533
|
+
_render_baseline(_scan(path))
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
@app.command()
|
|
1537
|
+
def regressions(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1538
|
+
"""Identify health regressions since the last baseline."""
|
|
1539
|
+
_render_regressions(_scan(path))
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
@app.command()
|
|
1543
|
+
def budget(
|
|
1544
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1545
|
+
config: Optional[Path] = typer.Option(None, "--config", "-c", help="Optional budget configuration file."),
|
|
1546
|
+
) -> None:
|
|
1547
|
+
"""Evaluate the repository against technical debt budgets."""
|
|
1548
|
+
_render_budget(_scan(path), config=config)
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
@app.command("release-check")
|
|
1552
|
+
def release_check(
|
|
1553
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1554
|
+
config: Optional[Path] = typer.Option(None, "--config", "-c", help="Optional budget configuration file."),
|
|
1555
|
+
) -> None:
|
|
1556
|
+
"""Evaluate release readiness across debt, config, routes, and migrations."""
|
|
1557
|
+
_render_release_check(_scan(path), config=config)
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
@app.command()
|
|
1561
|
+
def ownership(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1562
|
+
"""Identify unowned modules and stale code areas."""
|
|
1563
|
+
_render_ownership(_scan(path))
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
@app.command("dependency-health")
|
|
1567
|
+
def dependency_health(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1568
|
+
"""Track unused and untracked dependencies."""
|
|
1569
|
+
_render_dependency_health(_scan(path))
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
@app.command()
|
|
1573
|
+
def cleanup(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1574
|
+
"""Generate a prioritized cleanup queue."""
|
|
1575
|
+
_render_cleanup_queue(_scan(path))
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
@app.command()
|
|
1579
|
+
def standards(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1580
|
+
"""Check naming, documentation, and consistency standards."""
|
|
1581
|
+
_render_standards(_scan(path))
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
@app.command()
|
|
1585
|
+
def history(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1586
|
+
"""Track repository health over time."""
|
|
1587
|
+
_render_history(_scan(path))
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
@app.command()
|
|
1591
|
+
def recommend(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1592
|
+
"""Generate practical maintenance recommendations."""
|
|
1593
|
+
_render_recommendations(_scan(path))
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
@app.command()
|
|
1597
|
+
def prescribe(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1598
|
+
"""Generate a repository-wide remediation prescription."""
|
|
1599
|
+
_render_prescription(_scan(path))
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
@app.command("repair-plan")
|
|
1603
|
+
def repair_plan(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1604
|
+
"""Generate a step-by-step repository recovery strategy."""
|
|
1605
|
+
_render_repair_plan(_scan(path))
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
@app.command()
|
|
1609
|
+
def explore(path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True)) -> None:
|
|
1610
|
+
"""Launch a lightweight live excavation dashboard."""
|
|
1611
|
+
analysis = _scan(path)
|
|
1612
|
+
|
|
1613
|
+
def render_dashboard() -> Panel:
|
|
1614
|
+
summary = analysis.summary
|
|
1615
|
+
lines = [
|
|
1616
|
+
f"Repository: {summary.root}",
|
|
1617
|
+
f"Health: {summary.health_score}/100 ({summary.health_status})",
|
|
1618
|
+
f"Artifacts: {summary.artifact_count}",
|
|
1619
|
+
f"DNA: {', '.join(analysis.intelligence.dna.signature)}",
|
|
1620
|
+
f"Personality: {analysis.intelligence.personality.type}",
|
|
1621
|
+
"",
|
|
1622
|
+
"Actions:",
|
|
1623
|
+
"- 1: Refresh dashboard",
|
|
1624
|
+
"- 2: Show dependency hubs",
|
|
1625
|
+
"- 3: Show civilizations",
|
|
1626
|
+
"- 4: Show debt heatmap",
|
|
1627
|
+
"- q: Quit",
|
|
1628
|
+
]
|
|
1629
|
+
return Panel("\n".join(lines), title="Interactive Excavation", border_style="cyan")
|
|
1630
|
+
|
|
1631
|
+
with Live(render_dashboard(), console=console, refresh_per_second=4) as live:
|
|
1632
|
+
while True:
|
|
1633
|
+
choice = typer.prompt("Choose action", default="1")
|
|
1634
|
+
if choice.lower() == "q":
|
|
1635
|
+
break
|
|
1636
|
+
if choice == "1":
|
|
1637
|
+
live.update(render_dashboard())
|
|
1638
|
+
elif choice == "2":
|
|
1639
|
+
_render_deps(analysis.intelligence)
|
|
1640
|
+
elif choice == "3":
|
|
1641
|
+
_render_civilizations(analysis.intelligence)
|
|
1642
|
+
elif choice == "4":
|
|
1643
|
+
_render_debt(analysis.intelligence)
|
|
1644
|
+
else:
|
|
1645
|
+
render_notice("Unknown action.", style="yellow")
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
@export_app.command("json")
|
|
1649
|
+
def export_json_cmd(
|
|
1650
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1651
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path."),
|
|
1652
|
+
) -> None:
|
|
1653
|
+
destination = _export_report("json", path, output)
|
|
1654
|
+
console.print(f"Exported JSON report to {destination}")
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
@export_app.command("markdown")
|
|
1658
|
+
def export_markdown_cmd(
|
|
1659
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1660
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path."),
|
|
1661
|
+
) -> None:
|
|
1662
|
+
destination = _export_report("markdown", path, output)
|
|
1663
|
+
console.print(f"Exported Markdown report to {destination}")
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
@export_app.command("html")
|
|
1667
|
+
def export_html_cmd(
|
|
1668
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1669
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path."),
|
|
1670
|
+
) -> None:
|
|
1671
|
+
destination = _export_report("html", path, output)
|
|
1672
|
+
console.print(f"Exported HTML report to {destination}")
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
@report_app.command("json")
|
|
1676
|
+
def report_json_cmd(
|
|
1677
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1678
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path."),
|
|
1679
|
+
) -> None:
|
|
1680
|
+
destination = _export_report("json", path, output)
|
|
1681
|
+
console.print(f"Generated JSON report at {destination}")
|
|
1682
|
+
|
|
1683
|
+
|
|
1684
|
+
@report_app.command("markdown")
|
|
1685
|
+
def report_markdown_cmd(
|
|
1686
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1687
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path."),
|
|
1688
|
+
) -> None:
|
|
1689
|
+
destination = _export_report("markdown", path, output)
|
|
1690
|
+
console.print(f"Generated Markdown report at {destination}")
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
@report_app.command("html")
|
|
1694
|
+
def report_html_cmd(
|
|
1695
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1696
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path."),
|
|
1697
|
+
) -> None:
|
|
1698
|
+
destination = _export_report("html", path, output)
|
|
1699
|
+
console.print(f"Generated HTML report at {destination}")
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
@report_app.command("pdf")
|
|
1703
|
+
def report_pdf_cmd(
|
|
1704
|
+
path: Path = typer.Argument(Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
1705
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path."),
|
|
1706
|
+
) -> None:
|
|
1707
|
+
destination = _export_report("pdf", path, output)
|
|
1708
|
+
console.print(f"Generated PDF report at {destination}")
|