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/utils/fs.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from pathspec import PathSpec # type: ignore
|
|
8
|
+
except ModuleNotFoundError: # pragma: no cover - fallback for minimal environments
|
|
9
|
+
class PathSpec: # type: ignore[no-redef]
|
|
10
|
+
def __init__(self, patterns: list[str]) -> None:
|
|
11
|
+
self.patterns = patterns
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_lines(cls, _style: str, lines: list[str]) -> "PathSpec":
|
|
15
|
+
return cls(lines)
|
|
16
|
+
|
|
17
|
+
def match_file(self, rel_path: str) -> bool:
|
|
18
|
+
candidate = rel_path.replace("\\", "/")
|
|
19
|
+
for pattern in self.patterns:
|
|
20
|
+
pattern = pattern.strip()
|
|
21
|
+
if not pattern:
|
|
22
|
+
continue
|
|
23
|
+
normalized = pattern.replace("\\", "/")
|
|
24
|
+
if normalized.endswith("/"):
|
|
25
|
+
prefix = normalized[:-1]
|
|
26
|
+
if candidate == prefix or candidate.startswith(f"{prefix}/"):
|
|
27
|
+
return True
|
|
28
|
+
elif candidate == normalized or candidate.endswith(f"/{normalized}"):
|
|
29
|
+
return True
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
DEFAULT_IGNORE_DIRS = {
|
|
34
|
+
".git",
|
|
35
|
+
".hg",
|
|
36
|
+
".svn",
|
|
37
|
+
".tox",
|
|
38
|
+
".venv",
|
|
39
|
+
"venv",
|
|
40
|
+
"node_modules",
|
|
41
|
+
"__pycache__",
|
|
42
|
+
".mypy_cache",
|
|
43
|
+
".pytest_cache",
|
|
44
|
+
"dist",
|
|
45
|
+
"build",
|
|
46
|
+
".next",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
DEFAULT_IGNORE_FILES = {
|
|
50
|
+
"devarch-report.md",
|
|
51
|
+
"devarch-report.markdown",
|
|
52
|
+
"devarch-report.html",
|
|
53
|
+
"devarch-report.json",
|
|
54
|
+
"devarch-report.pdf",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
TEXT_EXTENSIONS = {
|
|
58
|
+
".py",
|
|
59
|
+
".pyi",
|
|
60
|
+
".js",
|
|
61
|
+
".jsx",
|
|
62
|
+
".ts",
|
|
63
|
+
".tsx",
|
|
64
|
+
".json",
|
|
65
|
+
".md",
|
|
66
|
+
".txt",
|
|
67
|
+
".yml",
|
|
68
|
+
".yaml",
|
|
69
|
+
".toml",
|
|
70
|
+
".ini",
|
|
71
|
+
".cfg",
|
|
72
|
+
".css",
|
|
73
|
+
".html",
|
|
74
|
+
".sh",
|
|
75
|
+
".bat",
|
|
76
|
+
".ps1",
|
|
77
|
+
".mjs",
|
|
78
|
+
".cjs",
|
|
79
|
+
".sql",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ASSET_EXTENSIONS = {
|
|
83
|
+
".png",
|
|
84
|
+
".jpg",
|
|
85
|
+
".jpeg",
|
|
86
|
+
".gif",
|
|
87
|
+
".webp",
|
|
88
|
+
".svg",
|
|
89
|
+
".ico",
|
|
90
|
+
".bmp",
|
|
91
|
+
".mp3",
|
|
92
|
+
".mp4",
|
|
93
|
+
".mov",
|
|
94
|
+
".webm",
|
|
95
|
+
".wav",
|
|
96
|
+
".pdf",
|
|
97
|
+
".zip",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(slots=True)
|
|
102
|
+
class RepoView:
|
|
103
|
+
root: Path
|
|
104
|
+
files: list[Path]
|
|
105
|
+
directories: list[Path]
|
|
106
|
+
ignore_spec: PathSpec | None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def read_gitignore(root: Path) -> PathSpec | None:
|
|
110
|
+
patterns: list[str] = []
|
|
111
|
+
gitignore = root / ".gitignore"
|
|
112
|
+
if gitignore.exists():
|
|
113
|
+
patterns.extend(
|
|
114
|
+
line.strip()
|
|
115
|
+
for line in gitignore.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
116
|
+
if line.strip() and not line.strip().startswith("#")
|
|
117
|
+
)
|
|
118
|
+
patterns.extend(f"{name}/" for name in DEFAULT_IGNORE_DIRS)
|
|
119
|
+
patterns.extend(DEFAULT_IGNORE_FILES)
|
|
120
|
+
return PathSpec.from_lines("gitwildmatch", patterns) if patterns else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_ignored(path: Path, root: Path, ignore_spec: PathSpec | None) -> bool:
|
|
124
|
+
rel = path.relative_to(root).as_posix()
|
|
125
|
+
if ignore_spec and ignore_spec.match_file(rel):
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def collect_repository(root: Path) -> RepoView:
|
|
131
|
+
root = root.resolve()
|
|
132
|
+
ignore_spec = read_gitignore(root)
|
|
133
|
+
files: list[Path] = []
|
|
134
|
+
directories: list[Path] = []
|
|
135
|
+
for path in sorted(root.rglob("*")):
|
|
136
|
+
if path.is_dir():
|
|
137
|
+
if path == root:
|
|
138
|
+
continue
|
|
139
|
+
if is_ignored(path, root, ignore_spec):
|
|
140
|
+
directories.append(path)
|
|
141
|
+
continue
|
|
142
|
+
if is_ignored(path, root, ignore_spec):
|
|
143
|
+
continue
|
|
144
|
+
files.append(path)
|
|
145
|
+
return RepoView(root=root, files=files, directories=directories, ignore_spec=ignore_spec)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def read_text(path: Path) -> str:
|
|
149
|
+
return path.read_text(encoding="utf-8", errors="ignore")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def safe_stat(path: Path) -> int:
|
|
153
|
+
try:
|
|
154
|
+
return path.stat().st_size
|
|
155
|
+
except OSError:
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def path_kind(path: Path) -> str:
|
|
160
|
+
suffix = path.suffix.lower()
|
|
161
|
+
if suffix in TEXT_EXTENSIONS:
|
|
162
|
+
return "text"
|
|
163
|
+
if suffix in ASSET_EXTENSIONS:
|
|
164
|
+
return "asset"
|
|
165
|
+
return "binary"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class GitSummary:
|
|
11
|
+
available: bool
|
|
12
|
+
commit_count: int = 0
|
|
13
|
+
repository_age_days: int = 0
|
|
14
|
+
first_commit: datetime | None = None
|
|
15
|
+
last_commit: datetime | None = None
|
|
16
|
+
most_modified_files: list[tuple[str, int]] = None # type: ignore[assignment]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run_git(root: Path, *args: str) -> str | None:
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["git", "-C", str(root), *args],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True,
|
|
25
|
+
check=True,
|
|
26
|
+
)
|
|
27
|
+
except (OSError, subprocess.CalledProcessError):
|
|
28
|
+
return None
|
|
29
|
+
return result.stdout.strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def collect_git_summary(root: Path) -> GitSummary:
|
|
33
|
+
log_format = "%ct"
|
|
34
|
+
commit_count_raw = _run_git(root, "rev-list", "--count", "HEAD")
|
|
35
|
+
if commit_count_raw is None:
|
|
36
|
+
return GitSummary(available=False, most_modified_files=[])
|
|
37
|
+
|
|
38
|
+
first_commit_raw = _run_git(root, "log", "--reverse", "--format=%ct", "HEAD")
|
|
39
|
+
last_commit_raw = _run_git(root, "log", "-1", "--format=%ct", "HEAD")
|
|
40
|
+
stats_raw = _run_git(root, "log", "--name-only", "--pretty=format:")
|
|
41
|
+
|
|
42
|
+
first_commit = datetime.fromtimestamp(int(first_commit_raw.splitlines()[0]), tz=timezone.utc) if first_commit_raw else None
|
|
43
|
+
last_commit = datetime.fromtimestamp(int(last_commit_raw.splitlines()[0]), tz=timezone.utc) if last_commit_raw else None
|
|
44
|
+
repository_age_days = 0
|
|
45
|
+
if first_commit and last_commit:
|
|
46
|
+
repository_age_days = max((last_commit - first_commit).days, 0)
|
|
47
|
+
|
|
48
|
+
file_counts: dict[str, int] = {}
|
|
49
|
+
if stats_raw:
|
|
50
|
+
for line in stats_raw.splitlines():
|
|
51
|
+
if line.strip():
|
|
52
|
+
file_counts[line.strip()] = file_counts.get(line.strip(), 0) + 1
|
|
53
|
+
|
|
54
|
+
most_modified_files = sorted(file_counts.items(), key=lambda item: item[1], reverse=True)[:10]
|
|
55
|
+
|
|
56
|
+
return GitSummary(
|
|
57
|
+
available=True,
|
|
58
|
+
commit_count=int(commit_count_raw),
|
|
59
|
+
repository_age_days=repository_age_days,
|
|
60
|
+
first_commit=first_commit,
|
|
61
|
+
last_commit=last_commit,
|
|
62
|
+
most_modified_files=most_modified_files,
|
|
63
|
+
)
|
|
64
|
+
|
devarch/utils/rich_ui.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from ..models import Artifact, ScanSummary
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fmt_days(days: int | None) -> str:
|
|
17
|
+
if days is None:
|
|
18
|
+
return "n/a"
|
|
19
|
+
if days < 30:
|
|
20
|
+
return f"{days} days"
|
|
21
|
+
years = days / 365
|
|
22
|
+
return f"{years:.1f} years"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def health_badge(score: int) -> str:
|
|
26
|
+
if score >= 85:
|
|
27
|
+
return "Healthy"
|
|
28
|
+
if score >= 65:
|
|
29
|
+
return "Worn"
|
|
30
|
+
if score >= 45:
|
|
31
|
+
return "Unearthed Debt"
|
|
32
|
+
return "Critical"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render_header(title: str, subtitle: str | None = None) -> None:
|
|
36
|
+
text = Text(title, style="bold cyan")
|
|
37
|
+
if subtitle:
|
|
38
|
+
text.append(f"\n{subtitle}", style="dim")
|
|
39
|
+
console.print(Panel(text, border_style="cyan"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_metric_grid(summary: ScanSummary) -> None:
|
|
43
|
+
table = Table.grid(expand=True)
|
|
44
|
+
table.add_column()
|
|
45
|
+
table.add_column()
|
|
46
|
+
table.add_column()
|
|
47
|
+
table.add_row(
|
|
48
|
+
f"[bold]Artifacts[/bold]\n{summary.artifact_count}",
|
|
49
|
+
f"[bold]Ancient[/bold]\n{summary.ancient_count}",
|
|
50
|
+
f"[bold]Health[/bold]\n{summary.health_score}/100",
|
|
51
|
+
)
|
|
52
|
+
table.add_row(
|
|
53
|
+
f"[bold]TODOs[/bold]\n{summary.todo_count}",
|
|
54
|
+
f"[bold]Duplicates[/bold]\n{summary.duplicate_count}",
|
|
55
|
+
f"[bold]Debt[/bold]\n{summary.technical_debt_estimate:.1f}",
|
|
56
|
+
)
|
|
57
|
+
console.print(Panel(table, title="Excavation Summary", border_style="green"))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def render_artifacts(title: str, artifacts: Iterable[Artifact]) -> None:
|
|
61
|
+
table = Table(title=title, show_lines=False, header_style="bold magenta")
|
|
62
|
+
table.add_column("File", overflow="fold")
|
|
63
|
+
table.add_column("Age", style="yellow", no_wrap=True)
|
|
64
|
+
table.add_column("Size", style="cyan", no_wrap=True)
|
|
65
|
+
table.add_column("Risk", style="red", no_wrap=True)
|
|
66
|
+
table.add_column("Confidence", style="green", no_wrap=True)
|
|
67
|
+
table.add_column("Detail", overflow="fold")
|
|
68
|
+
count = 0
|
|
69
|
+
for artifact in artifacts:
|
|
70
|
+
count += 1
|
|
71
|
+
table.add_row(
|
|
72
|
+
str(artifact.path),
|
|
73
|
+
f"{artifact.age_days} days" if artifact.age_days is not None else "n/a",
|
|
74
|
+
f"{artifact.size_bytes} B" if artifact.size_bytes is not None else "n/a",
|
|
75
|
+
artifact.risk,
|
|
76
|
+
f"{artifact.confidence:.0%}" if artifact.confidence is not None else "n/a",
|
|
77
|
+
artifact.detail or artifact.kind,
|
|
78
|
+
)
|
|
79
|
+
if count:
|
|
80
|
+
console.print(table)
|
|
81
|
+
else:
|
|
82
|
+
console.print(Panel("No artifacts found.", border_style="green"))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def render_notice(message: str, style: str = "yellow") -> None:
|
|
86
|
+
console.print(Panel(message, border_style=style))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_kv(title: str, rows: list[tuple[str, str]], border_style: str = "cyan") -> None:
|
|
90
|
+
table = Table.grid(expand=True)
|
|
91
|
+
table.add_column(justify="left")
|
|
92
|
+
table.add_column(justify="left")
|
|
93
|
+
for label, value in rows:
|
|
94
|
+
table.add_row(f"[bold]{label}[/bold]", value)
|
|
95
|
+
console.print(Panel(table, title=title, border_style=border_style))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def render_bars(title: str, rows: list[tuple[str, float]]) -> None:
|
|
99
|
+
table = Table(title=title, header_style="bold magenta")
|
|
100
|
+
table.add_column("Bucket", overflow="fold")
|
|
101
|
+
table.add_column("Score", justify="right")
|
|
102
|
+
table.add_column("Bar", overflow="fold")
|
|
103
|
+
max_score = max((score for _, score in rows), default=0.0) or 1.0
|
|
104
|
+
for label, score in rows:
|
|
105
|
+
width = max(1, int((score / max_score) * 20))
|
|
106
|
+
table.add_row(label, f"{score:.1f}", "#" * width)
|
|
107
|
+
console.print(table)
|
devarch/version.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devarch
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Dev Archaeologist: excavate dead code, technical debt, and forgotten artifacts in software projects.
|
|
5
|
+
Author: magnexis
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: typer>=0.12.3
|
|
11
|
+
Requires-Dist: rich>=13.7.1
|
|
12
|
+
Requires-Dist: pathspec>=0.12.1
|
|
13
|
+
Requires-Dist: radon>=6.0.1 ; extra == "extended"
|
|
14
|
+
Requires-Dist: networkx>=3.3 ; extra == "extended"
|
|
15
|
+
Requires-Dist: gitpython>=3.1.43 ; extra == "extended"
|
|
16
|
+
Requires-Dist: tree-sitter>=0.22.3 ; extra == "extended"
|
|
17
|
+
Requires-Dist: reportlab>=4.2.2 ; extra == "pdf"
|
|
18
|
+
Requires-Dist: pytest>=8.2.2 ; extra == "test"
|
|
19
|
+
Provides-Extra: extended
|
|
20
|
+
Provides-Extra: pdf
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
|
|
23
|
+
# Dev Archaeologist
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="assets/devarch-logo.png" alt="Dev Archaeologist logo" width="220">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<em>Software archaeology and repository intelligence from Magnexis<img src="assets/magnexis-logo.png" alt="Magnexis logo" width="35"></em>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<a href="#install"><img alt="Python 3.12+" src="https://img.shields.io/badge/Python-3.12%2B-blue.svg"></a>
|
|
36
|
+
<a href="#license"><img alt="MIT License" src="https://img.shields.io/badge/License-MIT-green.svg"></a>
|
|
37
|
+
<a href="#use"><img alt="CLI" src="https://img.shields.io/badge/CLI-devarch-8A6A3A.svg"></a>
|
|
38
|
+
<a href="#project-overview"><img alt="Magnexis" src="https://img.shields.io/badge/Brand-Magnexis-1F1F1F.svg"></a>
|
|
39
|
+
<a href="#release-artifacts"><img alt="Build sdist" src="https://img.shields.io/badge/Build-sdist%20%2B%20wheel-5E4B35.svg"></a>
|
|
40
|
+
<a href="#release-artifacts"><img alt="Release zip" src="https://img.shields.io/badge/Build-release%20zip-444444.svg"></a>
|
|
41
|
+
<a href="#release-artifacts"><img alt="Release manifest" src="https://img.shields.io/badge/Build-manifest%20%2B%20checksums-B8894D.svg"></a>
|
|
42
|
+
<a href="#project-overview"><img alt="Version" src="https://img.shields.io/badge/Version-0.2.0-444444.svg"></a>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
Dev Archaeologist is a Magnexis-built Python CLI for excavating hidden technical debt, structural decay, and forgotten implementation artifacts in software repositories.
|
|
46
|
+
|
|
47
|
+
It treats every codebase like an archaeological dig site and helps you answer questions like:
|
|
48
|
+
|
|
49
|
+
- What code is ancient and likely abandoned?
|
|
50
|
+
- Where is the repository accumulating risk?
|
|
51
|
+
- Which files are structural weak points?
|
|
52
|
+
- What can be safely removed, refactored, or archived?
|
|
53
|
+
- How is the project evolving over time?
|
|
54
|
+
|
|
55
|
+
## Project Overview
|
|
56
|
+
|
|
57
|
+
The tool scans a repository and turns the results into a rich, terminal-first excavation report.
|
|
58
|
+
|
|
59
|
+
It can surface:
|
|
60
|
+
|
|
61
|
+
- dead code
|
|
62
|
+
- ancient files
|
|
63
|
+
- TODO, FIXME, HACK, BUG, TEMP, and XXX markers
|
|
64
|
+
- duplicated logic
|
|
65
|
+
- unused assets and empty directories
|
|
66
|
+
- suspicious backup-style filenames
|
|
67
|
+
- oversized, complex "monster" files
|
|
68
|
+
- dependency hotspots and fragile chains
|
|
69
|
+
- architectural drift and release readiness issues
|
|
70
|
+
- remediation suggestions with estimated effort
|
|
71
|
+
|
|
72
|
+
The design goals are:
|
|
73
|
+
|
|
74
|
+
- strong terminal UX
|
|
75
|
+
- modular analyzers
|
|
76
|
+
- readable artifact reports
|
|
77
|
+
- release-friendly packaging
|
|
78
|
+
- future plugin support
|
|
79
|
+
|
|
80
|
+
## Magnexis Brand
|
|
81
|
+
|
|
82
|
+
Dev Archaeologist is presented as part of the Magnexis tooling line.
|
|
83
|
+
|
|
84
|
+
Brand cues used in this repository:
|
|
85
|
+
|
|
86
|
+
- the archaeological emblem in the project logo
|
|
87
|
+
- the Magnexis name treatment in the README header
|
|
88
|
+
- the Magnexis mark used as a small brand seal
|
|
89
|
+
- a dedicated brand badge in the top badge row
|
|
90
|
+
- consistent earth-toned release and CLI styling
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
Install from PyPI:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install devarch
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Install with optional extras:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install devarch[extended]
|
|
104
|
+
pip install devarch[release]
|
|
105
|
+
pip install devarch[test]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The `release` extra is useful if you want to build local distributions.
|
|
109
|
+
|
|
110
|
+
## Quick Start
|
|
111
|
+
|
|
112
|
+
Run a full excavation over the current directory:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
devarch scan .
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
List every available command:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
devarch help
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Generate a markdown report:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
devarch export markdown
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Generate a PDF report:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
devarch report pdf
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Release Artifacts
|
|
137
|
+
|
|
138
|
+
Dev Archaeologist includes a repeatable release build flow for local packaging and CI artifact generation.
|
|
139
|
+
|
|
140
|
+
Build the release bundle locally:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pip install .[release]
|
|
144
|
+
python scripts/build_release.py
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This produces the following artifacts in `dist/`:
|
|
148
|
+
|
|
149
|
+
- `*.whl` for Python wheel distribution
|
|
150
|
+
- `*.tar.gz` for source distribution
|
|
151
|
+
- `*-release.zip` for a bundled release archive
|
|
152
|
+
- `release-manifest.json` for artifact metadata
|
|
153
|
+
- `SHA256SUMS.txt` for checksum verification
|
|
154
|
+
|
|
155
|
+
The zip bundle is convenient for sharing the release set as a single downloadable package.
|
|
156
|
+
|
|
157
|
+
See [RELEASE_NOTES.md](RELEASE_NOTES.md) for the full release summary and artifact inventory.
|
|
158
|
+
|
|
159
|
+
## Command Reference
|
|
160
|
+
|
|
161
|
+
### Core excavation
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
devarch scan .
|
|
165
|
+
devarch help
|
|
166
|
+
devarch ancient .
|
|
167
|
+
devarch dead-code .
|
|
168
|
+
devarch todos .
|
|
169
|
+
devarch duplicates .
|
|
170
|
+
devarch monsters .
|
|
171
|
+
devarch ruins .
|
|
172
|
+
devarch suspicious .
|
|
173
|
+
devarch inspect src/app.py
|
|
174
|
+
devarch trace auth
|
|
175
|
+
devarch evidence auth
|
|
176
|
+
devarch bugmark src/app.py --line 128
|
|
177
|
+
devarch errorcode "ModuleNotFoundError: No module named 'rich'"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Repository intelligence
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
devarch dependencies .
|
|
184
|
+
devarch genealogy .
|
|
185
|
+
devarch civilizations .
|
|
186
|
+
devarch debt .
|
|
187
|
+
devarch timeline .
|
|
188
|
+
devarch personality .
|
|
189
|
+
devarch forecast .
|
|
190
|
+
devarch explore .
|
|
191
|
+
devarch investigate .
|
|
192
|
+
devarch weaknesses .
|
|
193
|
+
devarch quake .
|
|
194
|
+
devarch architecture .
|
|
195
|
+
devarch contributors .
|
|
196
|
+
devarch mutations .
|
|
197
|
+
devarch map .
|
|
198
|
+
devarch survival .
|
|
199
|
+
devarch notes .
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Forensic helpers
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
devarch investigate .
|
|
206
|
+
devarch inspect src/app.py
|
|
207
|
+
devarch trace auth
|
|
208
|
+
devarch evidence auth
|
|
209
|
+
devarch bugmark src/app.py --line 128
|
|
210
|
+
devarch errorcode "ModuleNotFoundError: No module named 'rich'"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Recovery and maintenance
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
devarch plan .
|
|
217
|
+
devarch delete-check src/legacy_auth.py
|
|
218
|
+
devarch refactor .
|
|
219
|
+
devarch routes .
|
|
220
|
+
devarch configs .
|
|
221
|
+
devarch migrations .
|
|
222
|
+
devarch deps .
|
|
223
|
+
devarch drift .
|
|
224
|
+
devarch pr-report .
|
|
225
|
+
devarch status .
|
|
226
|
+
devarch baseline .
|
|
227
|
+
devarch regressions .
|
|
228
|
+
devarch budget .
|
|
229
|
+
devarch release-check .
|
|
230
|
+
devarch ownership .
|
|
231
|
+
devarch dependency-health .
|
|
232
|
+
devarch cleanup .
|
|
233
|
+
devarch standards .
|
|
234
|
+
devarch history .
|
|
235
|
+
devarch recommend .
|
|
236
|
+
devarch prescribe .
|
|
237
|
+
devarch repair-plan .
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Reporting
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
devarch export json
|
|
244
|
+
devarch export markdown
|
|
245
|
+
devarch export html
|
|
246
|
+
devarch report markdown
|
|
247
|
+
devarch report html
|
|
248
|
+
devarch report pdf
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Output Philosophy
|
|
252
|
+
|
|
253
|
+
Each finding is designed to be actionable instead of just descriptive.
|
|
254
|
+
|
|
255
|
+
Typical output includes:
|
|
256
|
+
|
|
257
|
+
- problem
|
|
258
|
+
- evidence
|
|
259
|
+
- impact
|
|
260
|
+
- confidence
|
|
261
|
+
- recommended fix
|
|
262
|
+
- estimated effort
|
|
263
|
+
- risk level
|
|
264
|
+
|
|
265
|
+
That makes the tool useful not just for audits, but also for cleanup planning, code review, and release preparation.
|
|
266
|
+
|
|
267
|
+
## Plugin Architecture
|
|
268
|
+
|
|
269
|
+
Dev Archaeologist exposes a lightweight plugin registry via the `devarch.plugins` entry-point group so future extensions can hook into the excavation pipeline.
|
|
270
|
+
|
|
271
|
+
Planned extension areas include:
|
|
272
|
+
|
|
273
|
+
- `devarch-plugin-security`
|
|
274
|
+
- `devarch-plugin-ai`
|
|
275
|
+
- `devarch-plugin-performance`
|
|
276
|
+
|
|
277
|
+
## Development
|
|
278
|
+
|
|
279
|
+
Project layout:
|
|
280
|
+
|
|
281
|
+
```text
|
|
282
|
+
devarch/
|
|
283
|
+
├── analyzers/
|
|
284
|
+
├── cli/
|
|
285
|
+
├── reports/
|
|
286
|
+
├── scanner/
|
|
287
|
+
├── utils/
|
|
288
|
+
└── tests/
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Useful commands:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
python -m pytest -q
|
|
295
|
+
python -m compileall devarch
|
|
296
|
+
python scripts/build_release.py
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Repository Maintenance
|
|
300
|
+
|
|
301
|
+
The maintenance engine supports:
|
|
302
|
+
|
|
303
|
+
- baseline snapshots
|
|
304
|
+
- regression detection
|
|
305
|
+
- debt budgets
|
|
306
|
+
- release readiness checks
|
|
307
|
+
- ownership analysis
|
|
308
|
+
- dependency health monitoring
|
|
309
|
+
- cleanup recommendations
|
|
310
|
+
- standards checks
|
|
311
|
+
- health history
|
|
312
|
+
- remediation prescriptions
|
|
313
|
+
|
|
314
|
+
## License
|
|
315
|
+
|
|
316
|
+
MIT
|
|
317
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
devarch/__init__.py,sha256=YVazM_ZDhQW5KVzwqysNM_cG9XZA_hutMJlMEdFi8yY,68
|
|
2
|
+
devarch/__main__.py,sha256=6LIynMFySr5bFjaGgaHQh8rXbH7-wFEhlvb7OkeaQIA,34
|
|
3
|
+
devarch/models.py,sha256=sIJh7KVrbP85ZkoGf4Kr207JBZ6N3qKHcQYItSrEpdA,1080
|
|
4
|
+
devarch/plugins.py,sha256=vnUDXxkpTSmxD0qBkbjrR5wLf-t3S7_U-SLFTNlA6og,665
|
|
5
|
+
devarch/version.py,sha256=tMP78-EG1UxDi5nOPt1f8aHzscjq4bCIjndKFu8fxp4,46
|
|
6
|
+
devarch/analyzers/__init__.py,sha256=b1IduLIS67cyWSMmmOIpp1N-yLP4rPfLAUi0yKuc89g,51
|
|
7
|
+
devarch/analyzers/ancient.py,sha256=qzP1KjpFXFQDTspaRiZ6P_W2QOuURHSgXbLVwjp0P7Y,1440
|
|
8
|
+
devarch/analyzers/dead_code.py,sha256=z_bEtkAO1uZKZlvFu62kG7IGM-Dg35oPPOCefyUm5Co,3516
|
|
9
|
+
devarch/analyzers/duplicates.py,sha256=vxtHpvqi3XTiJKDGPqvycY8_RFIpuvpUdCQMjwWZVWA,3508
|
|
10
|
+
devarch/analyzers/health.py,sha256=GmoD1iCqSYzoVDclJKlCL7CJhP4N2TZz17S_qjtA2jQ,1589
|
|
11
|
+
devarch/analyzers/maintenance.py,sha256=FW4J17ars3dJ9nYCZim7elFlPAYY2UCBCNMZ7K_C1MU,35776
|
|
12
|
+
devarch/analyzers/monsters.py,sha256=2Yw8Cta10Bt06lPb1Z24l956CX0w2upTlkMZDPH19UI,2124
|
|
13
|
+
devarch/analyzers/recovery.py,sha256=e6CHFpQdXPXnPcUEcr86Fq-cZIus3AyccHLc5USxyI8,12665
|
|
14
|
+
devarch/analyzers/ruins.py,sha256=bAfHXmG2-4RVQxypyv8_BaqzSRKlFCT2f7SgJguoxAE,1487
|
|
15
|
+
devarch/analyzers/suspicious.py,sha256=UHr5rxb04FisKPIUbLdsZNn9kqoKGIqmLA7x8GqjHuk,881
|
|
16
|
+
devarch/analyzers/todos.py,sha256=Wu9nanQWWqtespS9yh_ws-5xh5SqdPyRVuNilMMSa6E,1623
|
|
17
|
+
devarch/cli/__init__.py,sha256=wO5goN2mKPI5VdamtBJOdfRN4oqKsJbzrArrALuarIk,46
|
|
18
|
+
devarch/cli/main.py,sha256=cdzYALw3xHN4BE_1aASLms1e5Wi5PiEl8qfLdM2maHs,70361
|
|
19
|
+
devarch/reports/__init__.py,sha256=BYiAYWxgepd0BL-_jV86fjVE2whvB7EDTgFhmmgVkO8,47
|
|
20
|
+
devarch/reports/exporters.py,sha256=afG7l3jyKK0ac6YPGwliW-5fqI9BopalNcx698WzznQ,12811
|
|
21
|
+
devarch/scanner/__init__.py,sha256=Wy_Kdf_pg8qZGIhWQ2dSyznZ30jS3wh57QNCRssKjJc,36
|
|
22
|
+
devarch/scanner/core.py,sha256=FKjQdyHX31Z8mUVvE7A1P-vRDj0S08Kw4DNIkYtPggg,356
|
|
23
|
+
devarch/scanner/discovery.py,sha256=z2liHJLCOFSUG8KjdRGzVHk50QTBMukFcPVjmQN9dMU,2809
|
|
24
|
+
devarch/scanner/intelligence.py,sha256=l18bzDnJ6ARoPK37sKoRHyWlLlh918mKkCQoSIArePQ,63849
|
|
25
|
+
devarch/utils/__init__.py,sha256=a9S2-DWQjB-FBUrjD_z13mYXFsxktzUysn10OuuuaMw,46
|
|
26
|
+
devarch/utils/fs.py,sha256=htWvNPLThrw2EWCXveaZh_9BZHLqfqkRxBSfWPi5R2g,3949
|
|
27
|
+
devarch/utils/git_info.py,sha256=lc2mBb9n2fEVbvqJDrMG1pJNJGKqb2OnmQZHTHZ59Rk,2216
|
|
28
|
+
devarch/utils/rich_ui.py,sha256=5XSsD29nbOTpD5RrLXGAOUEOUj14ICx2BgZlq_nOmaE,3580
|
|
29
|
+
devarch-0.2.0.dist-info/entry_points.txt,sha256=kS4r4c52fXX01tnkcJtz1Bj30kODX2DUEQwk5OnNZxM,48
|
|
30
|
+
devarch-0.2.0.dist-info/licenses/LICENSE,sha256=jg1VkRpDaIEH3ryczciGYD9-hKUH-fsY4Ny11PlGdsU,1088
|
|
31
|
+
devarch-0.2.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
32
|
+
devarch-0.2.0.dist-info/METADATA,sha256=G9dFbAbcecH5iCVBD_X0-3t0Yp2ke3RZcnzP5Ho8yQY,7585
|
|
33
|
+
devarch-0.2.0.dist-info/RECORD,,
|