deadpush 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.
- deadpush/__init__.py +1 -0
- deadpush/churn.py +189 -0
- deadpush/cli.py +1584 -0
- deadpush/comments.py +265 -0
- deadpush/complexity.py +254 -0
- deadpush/config.py +284 -0
- deadpush/crawler.py +133 -0
- deadpush/deadness.py +477 -0
- deadpush/debris.py +729 -0
- deadpush/deps.py +323 -0
- deadpush/deps_guard.py +382 -0
- deadpush/entrypoints.py +193 -0
- deadpush/graph.py +401 -0
- deadpush/guard.py +1386 -0
- deadpush/hooks.py +369 -0
- deadpush/importgraph.py +122 -0
- deadpush/imports.py +239 -0
- deadpush/intercept.py +995 -0
- deadpush/languages/__init__.py +143 -0
- deadpush/languages/base.py +70 -0
- deadpush/languages/cpp.py +150 -0
- deadpush/languages/go_.py +177 -0
- deadpush/languages/java.py +185 -0
- deadpush/languages/javascript.py +202 -0
- deadpush/languages/python_.py +278 -0
- deadpush/languages/rust.py +147 -0
- deadpush/languages/typescript.py +192 -0
- deadpush/layers.py +197 -0
- deadpush/mcp_server.py +1061 -0
- deadpush/reachability.py +183 -0
- deadpush/registration.py +280 -0
- deadpush/report.py +113 -0
- deadpush/rules.py +190 -0
- deadpush/sarif.py +123 -0
- deadpush/scorer.py +151 -0
- deadpush/security.py +187 -0
- deadpush/session.py +224 -0
- deadpush/tests.py +333 -0
- deadpush/ui.py +156 -0
- deadpush/verifier.py +168 -0
- deadpush/watch.py +103 -0
- deadpush-0.2.0.dist-info/METADATA +230 -0
- deadpush-0.2.0.dist-info/RECORD +46 -0
- deadpush-0.2.0.dist-info/WHEEL +4 -0
- deadpush-0.2.0.dist-info/entry_points.txt +2 -0
- deadpush-0.2.0.dist-info/licenses/LICENSE +21 -0
deadpush/ui.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Premium Rich-powered terminal UI for deadpush.
|
|
3
|
+
|
|
4
|
+
Makes every command feel modern, beautiful, and trustworthy.
|
|
5
|
+
Only activated if `rich` is installed (graceful fallback to plain output).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.tree import Tree
|
|
18
|
+
from rich import box
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
RICH_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
RICH_AVAILABLE = False
|
|
23
|
+
Console = None # type: ignore
|
|
24
|
+
|
|
25
|
+
from .graph import DeadSymbol, DebrisFile
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
console = Console() if RICH_AVAILABLE else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_rich_available() -> bool:
|
|
32
|
+
return RICH_AVAILABLE
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def print_header(title: str, subtitle: str = "") -> None:
|
|
36
|
+
if not RICH_AVAILABLE:
|
|
37
|
+
print(f"\n=== {title} ===")
|
|
38
|
+
if subtitle:
|
|
39
|
+
print(subtitle)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
panel = Panel(
|
|
43
|
+
f"[bold cyan]{title}[/bold cyan]\n[dim]{subtitle}[/dim]",
|
|
44
|
+
border_style="cyan",
|
|
45
|
+
box=box.ROUNDED,
|
|
46
|
+
padding=(1, 2),
|
|
47
|
+
)
|
|
48
|
+
console.print(panel)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def print_success(message: str) -> None:
|
|
52
|
+
if RICH_AVAILABLE:
|
|
53
|
+
console.print(f"[bold green]✅ {message}[/bold green]")
|
|
54
|
+
else:
|
|
55
|
+
print(f"✅ {message}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def print_warning(message: str) -> None:
|
|
59
|
+
if RICH_AVAILABLE:
|
|
60
|
+
console.print(f"[bold yellow]⚠️ {message}[/bold yellow]")
|
|
61
|
+
else:
|
|
62
|
+
print(f"⚠️ {message}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def print_error(message: str) -> None:
|
|
66
|
+
if RICH_AVAILABLE:
|
|
67
|
+
console.print(f"[bold red]❌ {message}[/bold red]")
|
|
68
|
+
else:
|
|
69
|
+
print(f"❌ {message}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create_debris_table(debris: list[DebrisFile]) -> Table | str:
|
|
73
|
+
if not RICH_AVAILABLE:
|
|
74
|
+
return "\n".join([f" - {d.path} ({d.category})" for d in debris])
|
|
75
|
+
|
|
76
|
+
table = Table(title="Debris Detected", box=box.ROUNDED, show_lines=True)
|
|
77
|
+
table.add_column("Path", style="cyan", no_wrap=True)
|
|
78
|
+
table.add_column("Category", style="magenta")
|
|
79
|
+
table.add_column("Confidence", justify="right")
|
|
80
|
+
table.add_column("Block Push?", justify="center")
|
|
81
|
+
table.add_column("Suggestion", style="dim")
|
|
82
|
+
|
|
83
|
+
for d in debris:
|
|
84
|
+
block_style = "[bold red]YES[/bold red]" if d.block_push else "[green]No[/green]"
|
|
85
|
+
conf = f"{d.confidence * 100:.0f}%"
|
|
86
|
+
table.add_row(
|
|
87
|
+
d.path,
|
|
88
|
+
d.category.replace("_", " ").title(),
|
|
89
|
+
conf,
|
|
90
|
+
block_style,
|
|
91
|
+
d.suggestion[:60] + "..." if len(d.suggestion) > 60 else d.suggestion,
|
|
92
|
+
)
|
|
93
|
+
return table
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def create_dead_symbols_tree(dead_symbols: list[DeadSymbol]) -> Tree | str:
|
|
97
|
+
if not RICH_AVAILABLE:
|
|
98
|
+
return f"Found {len(dead_symbols)} dead symbols."
|
|
99
|
+
|
|
100
|
+
tree = Tree("[bold red]Dead Code Clusters[/bold red]")
|
|
101
|
+
|
|
102
|
+
by_file: dict[str, list[DeadSymbol]] = {}
|
|
103
|
+
for ds in dead_symbols:
|
|
104
|
+
by_file.setdefault(ds.symbol.path, []).append(ds)
|
|
105
|
+
|
|
106
|
+
for filepath, symbols in sorted(by_file.items()):
|
|
107
|
+
file_branch = tree.add(f"[cyan]{filepath}[/cyan] ({len(symbols)} symbols)")
|
|
108
|
+
|
|
109
|
+
for ds in symbols:
|
|
110
|
+
tier_color = {
|
|
111
|
+
"definite": "red",
|
|
112
|
+
"probable": "yellow",
|
|
113
|
+
"suspicious": "orange1"
|
|
114
|
+
}.get(ds.tier, "white")
|
|
115
|
+
|
|
116
|
+
label = (
|
|
117
|
+
f"[{tier_color}]{ds.symbol.name}[/{tier_color}] "
|
|
118
|
+
f"({ds.tier}, {ds.confidence*100:.0f}%)"
|
|
119
|
+
)
|
|
120
|
+
file_branch.add(label)
|
|
121
|
+
|
|
122
|
+
return tree
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def print_scan_summary(
|
|
126
|
+
total_files: int,
|
|
127
|
+
dead_count: int,
|
|
128
|
+
debris_count: int,
|
|
129
|
+
blocking_debris: int,
|
|
130
|
+
entry_points: int,
|
|
131
|
+
) -> None:
|
|
132
|
+
if not RICH_AVAILABLE:
|
|
133
|
+
print(f"\nScan complete: {dead_count} dead | {debris_count} debris ({blocking_debris} blocking)")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
table = Table.grid(padding=(0, 2))
|
|
137
|
+
table.add_row("[bold]Files Scanned[/bold]", str(total_files))
|
|
138
|
+
table.add_row("[bold red]Dead Symbols[/bold red]", str(dead_count))
|
|
139
|
+
table.add_row("[bold yellow]Debris Found[/bold yellow]", f"{debris_count} ({blocking_debris} blocking)")
|
|
140
|
+
table.add_row("[bold green]Entry Points[/bold green]", str(entry_points))
|
|
141
|
+
|
|
142
|
+
panel = Panel(table, title="[bold cyan]deadpush Scan Summary[/bold cyan]", border_style="cyan")
|
|
143
|
+
console.print(panel)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def print_blocking_warning(debris: list[DebrisFile]) -> None:
|
|
147
|
+
if not RICH_AVAILABLE:
|
|
148
|
+
print("\nCRITICAL: Blocking debris found!")
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
console.print("\n[bold red on white] 🚫 PUSH BLOCKED BY deadpush [/bold red on white]\n")
|
|
152
|
+
|
|
153
|
+
for d in debris:
|
|
154
|
+
if d.block_push:
|
|
155
|
+
console.print(f"[red]• {d.path}[/red] — {d.category}")
|
|
156
|
+
console.print(f" [dim]{d.suggestion}[/dim]\n")
|
deadpush/verifier.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Post-write test verification.
|
|
3
|
+
|
|
4
|
+
Discovers and runs the most relevant test file for a given source file.
|
|
5
|
+
Used by the MCP verify_write tool so agents can verify their changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .config import Config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
TEST_RESULTS_DIR = ".deadpush/test_results"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class TestResult:
|
|
26
|
+
passed: bool
|
|
27
|
+
test_file: str
|
|
28
|
+
command: str
|
|
29
|
+
stdout: str
|
|
30
|
+
stderr: str
|
|
31
|
+
exit_code: int
|
|
32
|
+
timestamp: str
|
|
33
|
+
source_path: str = ""
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict[str, Any]:
|
|
36
|
+
return {
|
|
37
|
+
"passed": self.passed,
|
|
38
|
+
"test_file": self.test_file,
|
|
39
|
+
"command": self.command,
|
|
40
|
+
"stdout": self.stdout[-2000:] if len(self.stdout) > 2000 else self.stdout,
|
|
41
|
+
"stderr": self.stderr[-2000:] if len(self.stderr) > 2000 else self.stderr,
|
|
42
|
+
"exit_code": self.exit_code,
|
|
43
|
+
"timestamp": self.timestamp,
|
|
44
|
+
"source_path": self.source_path,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestVerifier:
|
|
49
|
+
"""Discover and run tests for a given source file."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, config: Config):
|
|
52
|
+
self.config = config
|
|
53
|
+
self.repo_root = config.repo_root
|
|
54
|
+
|
|
55
|
+
def find_test_for(self, source_path: str) -> Path | None:
|
|
56
|
+
"""Find the most relevant test file for a source path.
|
|
57
|
+
|
|
58
|
+
Convention: deadpush/x.py → tests/test_x.py
|
|
59
|
+
src/foo/bar.py → tests/test_foo/bar.py, tests/test_bar.py, etc.
|
|
60
|
+
"""
|
|
61
|
+
rel = Path(source_path)
|
|
62
|
+
stem = rel.stem
|
|
63
|
+
parent = rel.parent
|
|
64
|
+
|
|
65
|
+
candidates = [
|
|
66
|
+
self.repo_root / "tests" / f"test_{stem}{rel.suffix}",
|
|
67
|
+
self.repo_root / "tests" / f"test_{parent.name}_{stem}{rel.suffix}",
|
|
68
|
+
self.repo_root / "tests" / parent.name / f"test_{stem}{rel.suffix}",
|
|
69
|
+
self.repo_root / "tests" / parent.name / f"test_{parent.name}{rel.suffix}",
|
|
70
|
+
self.repo_root / f"test_{rel}",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for candidate in candidates:
|
|
74
|
+
if candidate.exists() and candidate.is_file():
|
|
75
|
+
return candidate.resolve()
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def find_or_create_results_dir(self) -> Path:
|
|
80
|
+
results_dir = self.repo_root / TEST_RESULTS_DIR
|
|
81
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
return results_dir
|
|
83
|
+
|
|
84
|
+
def run_test(self, test_file: Path) -> TestResult:
|
|
85
|
+
"""Run a test file and return structured results."""
|
|
86
|
+
cmd_config = self.config.test
|
|
87
|
+
test_path = str(test_file.resolve())
|
|
88
|
+
command = cmd_config.command.format(test_file=test_path) if "{test_file}" in cmd_config.command else f"{cmd_config.command} {test_path}"
|
|
89
|
+
timeout = cmd_config.timeout_seconds
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
proc = subprocess.run(
|
|
93
|
+
command.split(),
|
|
94
|
+
capture_output=True,
|
|
95
|
+
text=True,
|
|
96
|
+
timeout=timeout,
|
|
97
|
+
cwd=str(self.repo_root),
|
|
98
|
+
)
|
|
99
|
+
exit_code = proc.returncode
|
|
100
|
+
stdout = proc.stdout or ""
|
|
101
|
+
stderr = proc.stderr or ""
|
|
102
|
+
except subprocess.TimeoutExpired:
|
|
103
|
+
exit_code = -1
|
|
104
|
+
stdout = ""
|
|
105
|
+
stderr = f"Test timed out after {timeout}s"
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
exit_code = -2
|
|
108
|
+
stdout = ""
|
|
109
|
+
stderr = f"Test command not found: {cmd_config.command}"
|
|
110
|
+
except Exception as e:
|
|
111
|
+
exit_code = -3
|
|
112
|
+
stdout = ""
|
|
113
|
+
stderr = str(e)
|
|
114
|
+
|
|
115
|
+
return TestResult(
|
|
116
|
+
passed=exit_code == 0,
|
|
117
|
+
test_file=str(test_file),
|
|
118
|
+
command=command,
|
|
119
|
+
stdout=stdout,
|
|
120
|
+
stderr=stderr,
|
|
121
|
+
exit_code=exit_code,
|
|
122
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def verify_write(self, path: str, content: str) -> dict[str, Any]:
|
|
126
|
+
"""Full verification flow: find test → run test → return result.
|
|
127
|
+
|
|
128
|
+
Called from the MCP verify_write tool handler.
|
|
129
|
+
"""
|
|
130
|
+
source_path = Path(path)
|
|
131
|
+
|
|
132
|
+
# Find test file
|
|
133
|
+
test_file = self.find_test_for(str(source_path))
|
|
134
|
+
if not test_file:
|
|
135
|
+
return {
|
|
136
|
+
"verifiable": False,
|
|
137
|
+
"reason": "No test file found for this source path.",
|
|
138
|
+
"test_result": None,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Run the test
|
|
142
|
+
result = self.run_test(test_file)
|
|
143
|
+
|
|
144
|
+
# Store result for get_test_results
|
|
145
|
+
results_dir = self.find_or_create_results_dir()
|
|
146
|
+
safe_name = source_path.name.replace("/", "__").replace("\\", "__")
|
|
147
|
+
result_path = results_dir / f"{safe_name}.json"
|
|
148
|
+
result.source_path = str(source_path)
|
|
149
|
+
result_path.write_text(json.dumps(result.to_dict(), indent=2), encoding="utf-8")
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"verifiable": True,
|
|
153
|
+
"reason": None,
|
|
154
|
+
"test_result": result.to_dict(),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def load_recent_results(config: Config, limit: int = 10) -> list[dict[str, Any]]:
|
|
159
|
+
"""Load recent test verification results for get_test_results MCP tool."""
|
|
160
|
+
results_dir = config.repo_root / TEST_RESULTS_DIR
|
|
161
|
+
entries = []
|
|
162
|
+
if results_dir.exists():
|
|
163
|
+
for f in sorted(results_dir.glob("*.json"), reverse=True)[:limit]:
|
|
164
|
+
try:
|
|
165
|
+
entries.append(json.loads(f.read_text(encoding="utf-8")))
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
return entries
|
deadpush/watch.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Watch mode for deadpush.
|
|
3
|
+
|
|
4
|
+
Continuously monitors the repository for new debris (especially dangerous ones like CLAUDE.md)
|
|
5
|
+
while the developer is actively coding with AI tools.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
deadpush watch
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Callable
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from watchdog.observers import Observer
|
|
19
|
+
from watchdog.events import FileSystemEventHandler, FileCreatedEvent, FileModifiedEvent
|
|
20
|
+
WATCHDOG_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
WATCHDOG_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
from .config import load_config
|
|
25
|
+
from .crawler import iter_source_files
|
|
26
|
+
from .debris import DebrisDetector
|
|
27
|
+
from .ui import print_warning, print_success, print_error, is_rich_available
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DebrisEventHandler(FileSystemEventHandler):
|
|
31
|
+
def __init__(self, config, callback: Callable):
|
|
32
|
+
self.config = config
|
|
33
|
+
self.callback = callback
|
|
34
|
+
self.detector = DebrisDetector(config)
|
|
35
|
+
|
|
36
|
+
def on_created(self, event):
|
|
37
|
+
if event.is_directory:
|
|
38
|
+
return
|
|
39
|
+
self._check_file(Path(event.src_path))
|
|
40
|
+
|
|
41
|
+
def on_modified(self, event):
|
|
42
|
+
if event.is_directory:
|
|
43
|
+
return
|
|
44
|
+
self._check_file(Path(event.src_path))
|
|
45
|
+
|
|
46
|
+
def _check_file(self, path: Path):
|
|
47
|
+
# Only check relevant source/config files
|
|
48
|
+
from .crawler import get_supported_extensions
|
|
49
|
+
# always watch code + docs/config for debris
|
|
50
|
+
code_exts = get_supported_extensions()
|
|
51
|
+
if path.suffix.lower() not in code_exts | {".md", ".txt", ".env", ".toml", ".yaml", ".yml", ".json"}:
|
|
52
|
+
return
|
|
53
|
+
if any(x in str(path) for x in ["__pycache__", ".git", "node_modules", ".deadpush-archive"]):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Quick single file debris check
|
|
58
|
+
from .crawler import FileInfo
|
|
59
|
+
fi = FileInfo(
|
|
60
|
+
path=path,
|
|
61
|
+
rel_path=path.relative_to(self.config.repo_root),
|
|
62
|
+
size=path.stat().st_size if path.exists() else 0,
|
|
63
|
+
is_text=True,
|
|
64
|
+
mtime=time.time()
|
|
65
|
+
)
|
|
66
|
+
debris = self.detector.scan([fi])
|
|
67
|
+
blocking = [d for d in debris if d.block_push]
|
|
68
|
+
|
|
69
|
+
if blocking:
|
|
70
|
+
self.callback(blocking, path)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass # Never crash the watcher
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def start_watch(callback: Callable | None = None):
|
|
76
|
+
if not WATCHDOG_AVAILABLE:
|
|
77
|
+
print_error("Watch mode requires 'watchdog'. Install with: pip install deadpush[watch]")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
config = load_config()
|
|
81
|
+
print_success(f"Watching {config.repo_root} for new debris... (Ctrl+C to stop)")
|
|
82
|
+
|
|
83
|
+
if callback is None:
|
|
84
|
+
def default_callback(blocking_debris, changed_path):
|
|
85
|
+
print_warning(f"\nNew blocking debris detected in {changed_path.name}!")
|
|
86
|
+
for d in blocking_debris:
|
|
87
|
+
print(f" → {d.path} ({d.category})")
|
|
88
|
+
print(f" {d.suggestion}")
|
|
89
|
+
callback = default_callback
|
|
90
|
+
|
|
91
|
+
event_handler = DebrisEventHandler(config, callback)
|
|
92
|
+
observer = Observer()
|
|
93
|
+
observer.schedule(event_handler, str(config.repo_root), recursive=True)
|
|
94
|
+
observer.start()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
while True:
|
|
98
|
+
time.sleep(1)
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
observer.stop()
|
|
101
|
+
print_success("\nWatch mode stopped.")
|
|
102
|
+
|
|
103
|
+
observer.join()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deadpush
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Guardrails for the vibe coding era — reachability-based dead code detection + semantic debris detection with pre-push git hooks
|
|
5
|
+
Project-URL: Homepage, https://github.com/harris-ahmad/deadpush
|
|
6
|
+
Project-URL: Source, https://github.com/harris-ahmad/deadpush
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/harris-ahmad/deadpush/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/harris-ahmad/deadpush#readme
|
|
9
|
+
Author-email: Harris Ahmad <harris@deadpush.dev>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-coding,call-graph,dead-code,debris-detection,git-hook,llm,reachability,static-analysis
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: click>=8.1.7
|
|
21
|
+
Requires-Dist: pathspec>=0.12.1
|
|
22
|
+
Requires-Dist: tree-sitter-cpp>=0.23.0
|
|
23
|
+
Requires-Dist: tree-sitter-go>=0.23.0
|
|
24
|
+
Requires-Dist: tree-sitter-java>=0.23.0
|
|
25
|
+
Requires-Dist: tree-sitter-javascript>=0.23.0
|
|
26
|
+
Requires-Dist: tree-sitter-python>=0.23.0
|
|
27
|
+
Requires-Dist: tree-sitter-rust>=0.23.0
|
|
28
|
+
Requires-Dist: tree-sitter-typescript>=0.23.0
|
|
29
|
+
Requires-Dist: tree-sitter>=0.23.0
|
|
30
|
+
Provides-Extra: anthropic
|
|
31
|
+
Requires-Dist: anthropic>=0.45.0; extra == 'anthropic'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: rich>=13.7.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
36
|
+
Requires-Dist: watchdog>=4.0.0; extra == 'dev'
|
|
37
|
+
Provides-Extra: rich
|
|
38
|
+
Requires-Dist: rich>=13.7.0; extra == 'rich'
|
|
39
|
+
Provides-Extra: watch
|
|
40
|
+
Requires-Dist: watchdog>=4.0.0; extra == 'watch'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# deadpush
|
|
44
|
+
|
|
45
|
+
[](https://github.com/harris-ahmad/deadpush)
|
|
46
|
+
[](https://opensource.org/licenses/MIT)
|
|
47
|
+
|
|
48
|
+
**Your personal AI Agent Guardian.**
|
|
49
|
+
Protects you from the mistakes, secrets, and context pollution that AI coding agents (Claude, Cursor, Windsurf, etc.) inevitably create — even when you're not watching.
|
|
50
|
+
|
|
51
|
+
Run it once with `deadpush protect --daemon` and it runs in the background forever, monitoring your filesystem in real time.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## The Problem (2026 AI Coding Reality)
|
|
56
|
+
|
|
57
|
+
You tell your agent to "add the new feature" and walk away.
|
|
58
|
+
|
|
59
|
+
30 minutes later you come back to:
|
|
60
|
+
- A `claude.md` or `.cursorrules` file committed to the repo
|
|
61
|
+
- Hardcoded API keys in `.env` files the agent "helpfully" created
|
|
62
|
+
- 47 new "temporary" scripts and scratchpads
|
|
63
|
+
- Dead code and duplicated logic everywhere
|
|
64
|
+
|
|
65
|
+
**deadpush** is the always-on guardian that catches this the moment it happens.
|
|
66
|
+
|
|
67
|
+
## One Command. Real Protection.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install deadpush[watch,rich]
|
|
71
|
+
deadpush protect --daemon
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's it.
|
|
75
|
+
|
|
76
|
+
It will:
|
|
77
|
+
- Install a smart pre-push git hook
|
|
78
|
+
- Merge AI-specific ignore patterns into `.cursorignore`, `.claudeignore`, and `.gitignore`
|
|
79
|
+
- Start a persistent background process that watches your entire repo
|
|
80
|
+
- Automatically quarantine dangerous files the second they appear
|
|
81
|
+
- Track a **Safety Score** that reacts intelligently when multiple agents are going wild
|
|
82
|
+
|
|
83
|
+
While you're at the gym, in a meeting, or sleeping, deadpush is on duty.
|
|
84
|
+
|
|
85
|
+
## See It In Action
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# After running protect --daemon, try simulating an agent:
|
|
89
|
+
mkdir -p .deadpush-e2e-sandbox
|
|
90
|
+
touch .deadpush-e2e-sandbox/claude.md
|
|
91
|
+
echo 'OPENAI_API_KEY=sk-...' > .deadpush-e2e-sandbox/.env.bad
|
|
92
|
+
|
|
93
|
+
deadpush status
|
|
94
|
+
deadpush quarantine list
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
You'll see the guardian react, drop the Safety Score, and quarantine the files.
|
|
98
|
+
|
|
99
|
+
For a full automated demo of every feature (including burst simulation and call-graph verification):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python scripts/full_e2e_test.py --simulate-agent --burst --run-scan
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Key Features
|
|
106
|
+
|
|
107
|
+
- **True background guardian** — Survives terminal close, supports systemd/launchd autostart
|
|
108
|
+
- **Smart multi-agent Safety Score** — Penalizes bursts of dangerous activity from parallel agents
|
|
109
|
+
- **Automatic quarantine** (never hard-delete) — Easy `deadpush quarantine list` / `restore`
|
|
110
|
+
- **Local Control Interface for agents** — Your AI coding agents can query the guardian themselves (`GET /status`, `/quarantine-list`, etc. on localhost)
|
|
111
|
+
- **Cross-platform pre-push hook** — Works in PowerShell, CMD, and Git Bash
|
|
112
|
+
- **Strong static analysis + verification** — Structured call graphs + `deadpush verify` so you can actually trust (or challenge) the dead code reports
|
|
113
|
+
- **Debris detection** — LLM context files, vibe scratchpads, hardcoded secrets, AI-generated duplicates
|
|
114
|
+
|
|
115
|
+
## Commands You'll Actually Use
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
deadpush protect --daemon # The one command you run per repo
|
|
119
|
+
deadpush status # Is the guardian alive? What's the Safety Score?
|
|
120
|
+
deadpush quarantine list # See what it caught
|
|
121
|
+
deadpush verify # Cross-check the static analysis with real references
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Why This Matters in the AI Era
|
|
125
|
+
|
|
126
|
+
AI agents are incredible productivity multipliers.
|
|
127
|
+
|
|
128
|
+
They are also incredibly good at creating technical debt, leaking secrets, and polluting your context — especially when you give them long-running tasks and step away.
|
|
129
|
+
|
|
130
|
+
deadpush is the missing safety net.
|
|
131
|
+
|
|
132
|
+
## Installation
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install deadpush[watch,rich]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Then run `deadpush protect --daemon` in any repo you care about.
|
|
139
|
+
|
|
140
|
+
## Windows Users
|
|
141
|
+
|
|
142
|
+
The pre-push hook ships as a Python script + `.cmd` shim. It works from PowerShell, Command Prompt, and Git Bash. The `deadpush protect` command records the exact Python interpreter so everything works even inside virtualenvs.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
git clone https://github.com/harris-ahmad/deadpush
|
|
148
|
+
cd deadpush
|
|
149
|
+
pip install -e ".[dev,watch,rich]"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Architecture
|
|
153
|
+
|
|
154
|
+
deadpush is organized into four layers that work together:
|
|
155
|
+
|
|
156
|
+
### 1. Intercept Layer (`deadpush/intercept.py`)
|
|
157
|
+
The real-time guardrail engine. Every file write is checked against:
|
|
158
|
+
- **Security guardrails**: `eval`, `subprocess`, pickle deserialization, SQL injection patterns
|
|
159
|
+
- **Secret detection**: Hardcoded API keys, tokens, passwords (with **path-aware lowering** — test/mock files get `warn` instead of `block`)
|
|
160
|
+
- **Prompt injection**: AI prompt manipulation patterns (ignore-previous-instructions, role-play overrides, chat markup)
|
|
161
|
+
- **Destructive change detection**: Near-empty rewrites, >50% line reduction
|
|
162
|
+
- **Sensitive config protection**: CI/CD, deployment, Docker files
|
|
163
|
+
- **Layer violations**: Architecture import rules
|
|
164
|
+
- **Debris detection**: AI artifacts, stub code, temp files
|
|
165
|
+
|
|
166
|
+
**Path-aware guardrails**: Files in `test/`, `spec/`, `tests/`, `__tests__/`, `mocks/`, `fixtures/` directories, or files with `test_`, `_test`, `_spec` stems, automatically receive lowered severity for security/secret checks — recognizing that test code commonly uses patterns that would be dangerous in production.
|
|
167
|
+
|
|
168
|
+
**Learned false positive suppression**: When the agent adjudicator confirms a finding is a false positive, the pattern is persisted to `.deadpush/learned_patterns.json` and auto-suppressed on future checks. This creates a **feedback-driven learning loop** that reduces noise over time.
|
|
169
|
+
|
|
170
|
+
### 2. Analysis Layer (`deadpush/deadness.py`, `deadpush/graph.py`, `deadpush/importgraph.py`)
|
|
171
|
+
Multi-factor dead code scoring combining 8 signals:
|
|
172
|
+
- **Call graph in-degree** (0.30): How many callers reference the symbol
|
|
173
|
+
- **Registration detection** (0.20): Framework decorators, URL routes, CLI commands
|
|
174
|
+
- **String reference** (0.10): Name appears as string literal elsewhere
|
|
175
|
+
- **Import count** (0.10): External module imports
|
|
176
|
+
- **Entry point reachability** (0.05): Reachable from detected entry points
|
|
177
|
+
- **Git freshness** (0.05): Recently modified (git blame)
|
|
178
|
+
- **Call chain propagation** (0.10): Callers are themselves live (pass-through scoring)
|
|
179
|
+
- **Test coverage** (0.10): Referenced in test files
|
|
180
|
+
|
|
181
|
+
Each symbol gets a `DeadnessResult` with an `alive_score` (0.0–1.0), a `tier` (high/medium/low/uncertain), factor breakdown, reasons, and an **uncertainty** field explaining why the classifier might be wrong.
|
|
182
|
+
|
|
183
|
+
The `uncertainty` field is populated when the signal is ambiguous (e.g., "String reference detected but could be coincidental", "Import found but likely re-export", "Only one caller — may be indirect").
|
|
184
|
+
|
|
185
|
+
### 3. Call Graph Resolution (`deadpush/cli.py:84-148`)
|
|
186
|
+
The `_resolve_callee_to_symbol` function uses a 5-step heuristic pipeline:
|
|
187
|
+
1. **Local exact match**: Symbol exists in the same file
|
|
188
|
+
2. **Method receiver resolution**: Class methods via receiver name (`self`, `this`, or class name)
|
|
189
|
+
3. **Import resolution**: Module-qualified names from `file_imports`
|
|
190
|
+
4. **Dotted name resolution**: `module.function` style callee splitting
|
|
191
|
+
5. **Fallback name match**: Any symbol with matching name across the project (lowest confidence)
|
|
192
|
+
|
|
193
|
+
Each step uses exact prefix/suffix matching rather than loose substring checks to avoid false edges.
|
|
194
|
+
|
|
195
|
+
### 4. MCP Server (`deadpush/mcp_server.py`)
|
|
196
|
+
A Model Context Protocol server (stdio transport) exposing all capabilities as tools:
|
|
197
|
+
- **Agent-as-Adjudicator**: `verify_finding` and `learn_false_positive` tools let the agent itself adjudicate uncertain findings and teach deadpush about false positive patterns, creating a **feedback-driven learning loop**.
|
|
198
|
+
- **Write/Check pipeline**: `write_file`, `check_file`, `get_write_diff`, `retry_write`
|
|
199
|
+
- **Test-verified writes**: `verify_write` runs guardrails + tests atomically
|
|
200
|
+
- **Scanning**: `scan`, `get_dead_symbols`, `get_debris`, `get_test_issues`, `get_stale_docs`, `get_layer_violations`, `get_security_boundaries`, `get_complexity_alerts`
|
|
201
|
+
- **Configuration**: `add_allowed_pattern`, `ignore_path`, `set_guardrail_level`, `reset_runtime_config`
|
|
202
|
+
- **Feedback**: `get_feedback`, `get_recent_feedback`, `acknowledge_feedback`
|
|
203
|
+
|
|
204
|
+
### Data Flow
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
Agent writes file
|
|
208
|
+
↓
|
|
209
|
+
Intercept Layer checks (security, secrets, prompt injection, debris, layers, destructive changes)
|
|
210
|
+
↓
|
|
211
|
+
Path-aware lowering for test/mock files → Learned pattern suppression
|
|
212
|
+
↓
|
|
213
|
+
Approved? → Blocked → Quarantine + Feedback
|
|
214
|
+
Yes
|
|
215
|
+
↓
|
|
216
|
+
MCP verify_write (optional) → Run tests → Pass? → Write
|
|
217
|
+
Fail → Quarantine + Restore from git
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Philosophy
|
|
221
|
+
|
|
222
|
+
Set it and forget it.
|
|
223
|
+
|
|
224
|
+
The best guardian is one you forget exists — until the moment it saves you from your own agent.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
**Star the repo** if you think every developer running AI coding agents in 2026 should have this running in the background.
|
|
229
|
+
|
|
230
|
+
For the complete source and architecture, see the implementation notes in the repo.
|