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/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
+ [![GitHub stars](https://img.shields.io/github/stars/harris-ahmad/deadpush?style=social)](https://github.com/harris-ahmad/deadpush)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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.