gitreins 0.1.2__tar.gz

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.
gitreins-0.1.2/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bane
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitreins
3
+ Version: 0.1.2
4
+ Summary: Git-native AI agent co-harness — MCP server, static guards, and agentic evaluator for LLM-assisted coding
5
+ Author: Bane
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/totalwindupflightsystems/gitreins
8
+ Project-URL: Repository, https://github.com/totalwindupflightsystems/gitreins
9
+ Project-URL: Issues, https://github.com/totalwindupflightsystems/gitreins/issues
10
+ Keywords: git,ai,llm,mcp,code-review,agent
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Classifier: Topic :: Software Development :: Version Control :: Git
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: mcp>=1.0.0
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: requests>=2.28
26
+ Dynamic: license-file
27
+
28
+ # GitReins (PoC)
29
+
30
+ **Git-Native Agent Co-Harness — Proof of Concept**
31
+
32
+ [![CI](https://github.com/totalwindupflightsystems/gitreins/actions/workflows/ci.yml/badge.svg)](https://github.com/totalwindupflightsystems/gitreins/actions/workflows/ci.yml)
33
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
34
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
35
+ [![version](https://img.shields.io/badge/version-0.1.0-blue)](https://github.com/totalwindupflightsystems/gitreins/releases)
36
+ [![PyPI](https://img.shields.io/pypi/v/gitreins)](https://pypi.org/project/gitreins/)
37
+
38
+ ![GitReins Banner](assets/banner-dark.jpg)
39
+
40
+ GitReins lives inside your git repository as a co-harness. It provides MCP tools for task lifecycle management, an agentic evaluator that judges code completeness against task definitions, and git hooks that ensure nothing bypasses the quality gates.
41
+
42
+ > ✅ **Proof of Concept — Implemented (v0.1.0)** — All engine modules, MCP server, CLI, and git hooks are built and working. 322 tests pass.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ # PyPI (recommended)
48
+ pip install gitreins
49
+
50
+ # GitHub
51
+ pip install git+https://github.com/totalwindupflightsystems/gitreins.git
52
+
53
+ # From source
54
+ git clone https://github.com/totalwindupflightsystems/gitreins.git
55
+ cd gitreins && pip install -e .
56
+ ```
57
+
58
+ Then activate in any repo:
59
+
60
+ ```bash
61
+ cd /path/to/your-project
62
+ gitreins install
63
+ ```
64
+
65
+ ## How It Works
66
+
67
+ 1. **Create tasks** — Define criteria via CLI or MCP tools
68
+ 2. **Work with your AI agent** — Pi, Claude, Hermes, or Codex does code generation
69
+ 3. **Complete tasks** — Agent calls `task.complete`
70
+ 4. **Automatic evaluation** — Tier 1 static guards (secrets, lint, tests) + Tier 2 agentic evaluator
71
+ 5. **Commit through harness** — `commit` tool runs guards, blocks if checks fail
72
+
73
+ The evaluator is an agentic loop: it reads files, runs tests, searches patterns, and delivers a structured verdict with per-criterion PASS/FAIL. No single-shot LLM judgment.
74
+
75
+ ## Architecture & Docs
76
+
77
+ | Document | Purpose |
78
+ |----------|---------|
79
+ | [Full Architecture](docs/architecture.md) | System design and data flow |
80
+ | [Component Map](docs/component-map.md) | Module inventory with paths and line counts |
81
+ | [Agentic Evaluator Design](docs/evaluator-loop.md) | How the 7-tool agentic loop works |
82
+ | [Sandbox](docs/sandbox.md) | Evaluator scratch space (in-memory, with filesystem plans) |
83
+ | [Implementation Plan](docs/implementation-plan.md) | Phase history |
84
+
85
+ Full reverse-engineered specs are in `specs/` — one per component, with realized-by links to actual code files.
86
+
87
+ ## Status
88
+
89
+ **Phase: Fully Implemented (v0.1.0)** — All seven engine modules, MCP server (9 tools), CLI (5 top-level commands: task, guard, judge, commit, mcp-server), git hooks, and install script are built. See [Component Map](docs/component-map.md) for current state.
90
+
91
+ ## Quick Start
92
+
93
+ ```bash
94
+ cd gitreins-poc
95
+ ./gitreins/install # Activate hooks in <10 seconds
96
+
97
+ # Create a task and evaluate it
98
+ python3 gitreins/cli.py task create demo "Demo task" \
99
+ "File exists" "Has tests" "No secrets"
100
+
101
+ # Start the MCP server for your AI agent
102
+ python3 gitreins_mcp/server.py
103
+ ```
104
+
105
+ ## Demos
106
+ Three demo projects are included showing GitReins in action:
107
+ | Project | Type | What it tests |
108
+ |---------|------|---------------|
109
+ | demo-slugify/ | Single-file | URL slug generator — basic criteria verification |
110
+ | demo-calc/ | Multi-file CLI | Calculator with operations, parser, CLI — 13 pytest tests |
111
+ | demo-string-utils/ | Single-file | String utilities with intentional palindrome bug — FAIL→FIX→PASS cycle |
112
+
113
+ Run any demo:
114
+
115
+ ## Tech Stack
116
+
117
+ - **Language:** Python 3.10+
118
+ - **Dependencies:** mcp, pyyaml, requests (3 packages)
119
+ - **MCP Transport:** stdio
120
+ - **Config:** YAML in `.gitreins/` directory
121
+ - **Evaluator Model:** Haiku / GPT-4o-mini (<2s, ~$0.001/check)
122
+
123
+ <!-- axiom:trace work_item=GR-012 spec=specs/01-Architecture.md,specs/09-CLI.md,specs/10-Install-Bootstrap.md,specs/11-Configuration.md plan=.memory-bank/work-items/GR-012/plan.yaml -->
@@ -0,0 +1,96 @@
1
+ # GitReins (PoC)
2
+
3
+ **Git-Native Agent Co-Harness — Proof of Concept**
4
+
5
+ [![CI](https://github.com/totalwindupflightsystems/gitreins/actions/workflows/ci.yml/badge.svg)](https://github.com/totalwindupflightsystems/gitreins/actions/workflows/ci.yml)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
8
+ [![version](https://img.shields.io/badge/version-0.1.0-blue)](https://github.com/totalwindupflightsystems/gitreins/releases)
9
+ [![PyPI](https://img.shields.io/pypi/v/gitreins)](https://pypi.org/project/gitreins/)
10
+
11
+ ![GitReins Banner](assets/banner-dark.jpg)
12
+
13
+ GitReins lives inside your git repository as a co-harness. It provides MCP tools for task lifecycle management, an agentic evaluator that judges code completeness against task definitions, and git hooks that ensure nothing bypasses the quality gates.
14
+
15
+ > ✅ **Proof of Concept — Implemented (v0.1.0)** — All engine modules, MCP server, CLI, and git hooks are built and working. 322 tests pass.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ # PyPI (recommended)
21
+ pip install gitreins
22
+
23
+ # GitHub
24
+ pip install git+https://github.com/totalwindupflightsystems/gitreins.git
25
+
26
+ # From source
27
+ git clone https://github.com/totalwindupflightsystems/gitreins.git
28
+ cd gitreins && pip install -e .
29
+ ```
30
+
31
+ Then activate in any repo:
32
+
33
+ ```bash
34
+ cd /path/to/your-project
35
+ gitreins install
36
+ ```
37
+
38
+ ## How It Works
39
+
40
+ 1. **Create tasks** — Define criteria via CLI or MCP tools
41
+ 2. **Work with your AI agent** — Pi, Claude, Hermes, or Codex does code generation
42
+ 3. **Complete tasks** — Agent calls `task.complete`
43
+ 4. **Automatic evaluation** — Tier 1 static guards (secrets, lint, tests) + Tier 2 agentic evaluator
44
+ 5. **Commit through harness** — `commit` tool runs guards, blocks if checks fail
45
+
46
+ The evaluator is an agentic loop: it reads files, runs tests, searches patterns, and delivers a structured verdict with per-criterion PASS/FAIL. No single-shot LLM judgment.
47
+
48
+ ## Architecture & Docs
49
+
50
+ | Document | Purpose |
51
+ |----------|---------|
52
+ | [Full Architecture](docs/architecture.md) | System design and data flow |
53
+ | [Component Map](docs/component-map.md) | Module inventory with paths and line counts |
54
+ | [Agentic Evaluator Design](docs/evaluator-loop.md) | How the 7-tool agentic loop works |
55
+ | [Sandbox](docs/sandbox.md) | Evaluator scratch space (in-memory, with filesystem plans) |
56
+ | [Implementation Plan](docs/implementation-plan.md) | Phase history |
57
+
58
+ Full reverse-engineered specs are in `specs/` — one per component, with realized-by links to actual code files.
59
+
60
+ ## Status
61
+
62
+ **Phase: Fully Implemented (v0.1.0)** — All seven engine modules, MCP server (9 tools), CLI (5 top-level commands: task, guard, judge, commit, mcp-server), git hooks, and install script are built. See [Component Map](docs/component-map.md) for current state.
63
+
64
+ ## Quick Start
65
+
66
+ ```bash
67
+ cd gitreins-poc
68
+ ./gitreins/install # Activate hooks in <10 seconds
69
+
70
+ # Create a task and evaluate it
71
+ python3 gitreins/cli.py task create demo "Demo task" \
72
+ "File exists" "Has tests" "No secrets"
73
+
74
+ # Start the MCP server for your AI agent
75
+ python3 gitreins_mcp/server.py
76
+ ```
77
+
78
+ ## Demos
79
+ Three demo projects are included showing GitReins in action:
80
+ | Project | Type | What it tests |
81
+ |---------|------|---------------|
82
+ | demo-slugify/ | Single-file | URL slug generator — basic criteria verification |
83
+ | demo-calc/ | Multi-file CLI | Calculator with operations, parser, CLI — 13 pytest tests |
84
+ | demo-string-utils/ | Single-file | String utilities with intentional palindrome bug — FAIL→FIX→PASS cycle |
85
+
86
+ Run any demo:
87
+
88
+ ## Tech Stack
89
+
90
+ - **Language:** Python 3.10+
91
+ - **Dependencies:** mcp, pyyaml, requests (3 packages)
92
+ - **MCP Transport:** stdio
93
+ - **Config:** YAML in `.gitreins/` directory
94
+ - **Evaluator Model:** Haiku / GPT-4o-mini (<2s, ~$0.001/check)
95
+
96
+ <!-- axiom:trace work_item=GR-012 spec=specs/01-Architecture.md,specs/09-CLI.md,specs/10-Install-Bootstrap.md,specs/11-Configuration.md plan=.memory-bank/work-items/GR-012/plan.yaml -->
@@ -0,0 +1,2 @@
1
+ # GitReins Engine
2
+ from engine.version import __version__
@@ -0,0 +1,280 @@
1
+ """Dead Code Detector — AST-based static analysis for unreachable and unused code.
2
+
3
+ Catches:
4
+ 1. Unreachable code — statements after return/raise/break/continue in same block
5
+ 2. Unused functions — defined but never called anywhere in the project
6
+ 3. Unused imports — modules imported but never referenced
7
+ 4. Empty functions — defined with pass/... only, no body
8
+ """
9
+
10
+ import ast
11
+ import os
12
+ from dataclasses import dataclass, field
13
+
14
+
15
+ @dataclass
16
+ class DeadCodeFinding:
17
+ file: str
18
+ line: int
19
+ category: str # unreachable | unused_function | unused_import | empty_function
20
+ message: str
21
+
22
+
23
+ @dataclass
24
+ class DeadCodeReport:
25
+ findings: list[DeadCodeFinding] = field(default_factory=list)
26
+
27
+ @property
28
+ def passed(self) -> bool:
29
+ return len(self.findings) == 0
30
+
31
+ @property
32
+ def summary(self) -> str:
33
+ if not self.findings:
34
+ return "No dead code found"
35
+ lines = []
36
+ by_cat: dict[str, list[DeadCodeFinding]] = {}
37
+ for f in self.findings:
38
+ by_cat.setdefault(f.category, []).append(f)
39
+ for cat, finds in sorted(by_cat.items()):
40
+ lines.append(f"\n {cat.upper()} ({len(finds)}):")
41
+ for f in finds[:10]:
42
+ lines.append(f" {f.file}:{f.line} — {f.message}")
43
+ if len(finds) > 10:
44
+ lines.append(f" ... and {len(finds) - 10} more")
45
+ return "\n".join(lines)
46
+
47
+
48
+ class DeadCodeDetector:
49
+ """AST-based dead code analysis for Python projects."""
50
+
51
+ WHITELIST_FUNCTIONS = {
52
+ # Standard dunder methods
53
+ "__init__", "__repr__", "__str__", "__eq__", "__hash__", "__lt__",
54
+ "__le__", "__gt__", "__ge__", "__add__", "__sub__", "__mul__",
55
+ "__call__", "__getitem__", "__setitem__", "__delitem__",
56
+ "__enter__", "__exit__", "__iter__", "__next__", "__len__",
57
+ "__contains__", "__getattr__", "__setattr__", "__delattr__",
58
+ "__post_init__", "__new__",
59
+ # Test functions
60
+ "setUp", "tearDown", "setUpClass", "tearDownClass",
61
+ # Common framework hooks
62
+ "main", "run", "handle", "process", "execute", "dispatch",
63
+ }
64
+ # Decorators that mean a function IS called (just not via Call AST node)
65
+ CALLED_VIA_DECORATOR = {"property", "cached_property", "staticmethod", "classmethod"}
66
+ # Decorator qualifiers that mark fixture/test functions called by frameworks
67
+ FRAMEWORK_DECORATORS = {"pytest.fixture", "fixture"}
68
+
69
+ def __init__(self, workdir: str = "."):
70
+ self.workdir = os.path.abspath(workdir)
71
+ self._func_defs: dict[str, list[tuple[str, int]]] = {} # func_name -> [(file, line)]
72
+ self._func_calls: set[str] = set()
73
+ self._imports: dict[str, set[str]] = {} # file -> {import names}
74
+
75
+ def scan(self, files: list[str] | None = None) -> DeadCodeReport:
76
+ """Scan project for dead code. If files is None, scans all Python files."""
77
+ report = DeadCodeReport()
78
+
79
+ if files is None:
80
+ files = self._find_python_files()
81
+
82
+ # Phase 1: Build symbol table (definitions, imports)
83
+ for fpath in files:
84
+ self._collect_symbols(fpath)
85
+
86
+ # Phase 2: Collect all function calls across the project
87
+ for fpath in files:
88
+ self._collect_calls(fpath)
89
+
90
+ # Phase 3: Detect dead code per file
91
+ for fpath in files:
92
+ findings = self._analyze_file(fpath)
93
+ report.findings.extend(findings)
94
+
95
+ return report
96
+
97
+ def _find_python_files(self) -> list[str]:
98
+ """Find all Python files in the project (excluding venv, node_modules, etc.)."""
99
+ py_files = []
100
+ skip_dirs = {".git", "__pycache__", ".venv", "venv", "node_modules",
101
+ ".tox", ".eggs", "build", "dist", ".pytest_cache",
102
+ ".gitreins", "temporal-vector"}
103
+ for root, dirs, filenames in os.walk(self.workdir):
104
+ dirs[:] = [d for d in dirs if d not in skip_dirs]
105
+ for fname in filenames:
106
+ if fname.endswith(".py"):
107
+ py_files.append(os.path.join(root, fname))
108
+ return py_files
109
+
110
+ def _relpath(self, abspath: str) -> str:
111
+ try:
112
+ return os.path.relpath(abspath, self.workdir)
113
+ except ValueError:
114
+ return abspath
115
+
116
+ def _collect_symbols(self, fpath: str) -> None:
117
+ """Collect function definitions and imports from a file."""
118
+ try:
119
+ with open(fpath, "r") as f:
120
+ source = f.read()
121
+ tree = ast.parse(source, filename=fpath)
122
+ except (SyntaxError, UnicodeDecodeError, FileNotFoundError, PermissionError):
123
+ return
124
+
125
+ rel = self._relpath(fpath)
126
+
127
+ for node in ast.walk(tree):
128
+ # Function definitions
129
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
130
+ name = node.name
131
+ if name not in self._func_defs:
132
+ self._func_defs[name] = []
133
+ self._func_defs[name].append((rel, node.lineno))
134
+ # Functions decorated with @property, @staticmethod etc. are
135
+ # accessed via attribute access, not Call AST — treat as "called"
136
+ for decorator in node.decorator_list:
137
+ dec_name = None
138
+ if isinstance(decorator, ast.Name):
139
+ dec_name = decorator.id
140
+ elif isinstance(decorator, ast.Attribute):
141
+ dec_name = decorator.attr
142
+ # Check for pytest.fixture style
143
+ if isinstance(decorator.value, ast.Name):
144
+ qualified = f"{decorator.value.id}.{dec_name}"
145
+ if qualified in self.FRAMEWORK_DECORATORS:
146
+ self._func_calls.add(name)
147
+ break
148
+ if dec_name and dec_name in self.CALLED_VIA_DECORATOR:
149
+ self._func_calls.add(name)
150
+ break
151
+ if dec_name in self.FRAMEWORK_DECORATORS:
152
+ self._func_calls.add(name)
153
+ break
154
+
155
+ # Imports
156
+ if isinstance(node, ast.Import):
157
+ for alias in node.names:
158
+ name = alias.asname or alias.name
159
+ self._imports.setdefault(rel, set()).add(name)
160
+ elif isinstance(node, ast.ImportFrom):
161
+ for alias in node.names:
162
+ name = alias.asname or alias.name
163
+ if name != "*":
164
+ self._imports.setdefault(rel, set()).add(name)
165
+
166
+ def _collect_calls(self, fpath: str) -> None:
167
+ """Collect all function calls across the project."""
168
+ try:
169
+ with open(fpath, "r") as f:
170
+ source = f.read()
171
+ tree = ast.parse(source, filename=fpath)
172
+ except (SyntaxError, UnicodeDecodeError, FileNotFoundError, PermissionError):
173
+ return
174
+
175
+ for node in ast.walk(tree):
176
+ if isinstance(node, ast.Call):
177
+ if isinstance(node.func, ast.Name):
178
+ self._func_calls.add(node.func.id)
179
+ elif isinstance(node.func, ast.Attribute):
180
+ self._func_calls.add(node.func.attr)
181
+
182
+ def _analyze_file(self, fpath: str) -> list[DeadCodeFinding]:
183
+ """Analyze a single file for dead code."""
184
+ findings: list[DeadCodeFinding] = []
185
+ try:
186
+ with open(fpath, "r") as f:
187
+ source = f.read()
188
+ tree = ast.parse(source, filename=fpath)
189
+ lines = source.split("\n")
190
+ except (SyntaxError, UnicodeDecodeError, FileNotFoundError, PermissionError):
191
+ return findings
192
+
193
+ rel = self._relpath(fpath)
194
+
195
+ # --- UNREACHABLE CODE (function bodies only) ---
196
+ for node in ast.walk(tree):
197
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
198
+ continue
199
+ body = node.body
200
+ for i, child in enumerate(body):
201
+ if isinstance(child, (ast.Return, ast.Raise, ast.Break, ast.Continue)):
202
+ if i + 1 < len(body):
203
+ next_sib = body[i + 1]
204
+ if isinstance(next_sib, ast.Expr) and isinstance(next_sib.value, ast.Constant):
205
+ continue # Skip docstrings
206
+ findings.append(DeadCodeFinding(
207
+ file=rel, line=next_sib.lineno,
208
+ category="unreachable",
209
+ message=f"Code after {type(child).__name__.lower()} on line {child.lineno} is unreachable",
210
+ ))
211
+
212
+ # --- EMPTY FUNCTIONS ---
213
+ for node in ast.walk(tree):
214
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
215
+ body = node.body
216
+ # Strip docstrings
217
+ if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant):
218
+ body = body[1:]
219
+ if len(body) == 0 or (len(body) == 1 and isinstance(body[0], ast.Pass)):
220
+ findings.append(DeadCodeFinding(
221
+ file=rel, line=node.lineno,
222
+ category="empty_function",
223
+ message=f"Function '{node.name}' has no implementation (empty body)",
224
+ ))
225
+
226
+ # --- UNUSED IMPORTS ---
227
+ if rel in self._imports:
228
+ used_names = set()
229
+ for node in ast.walk(tree):
230
+ if isinstance(node, ast.Name):
231
+ used_names.add(node.id)
232
+ elif isinstance(node, ast.Attribute):
233
+ if isinstance(node.value, ast.Name):
234
+ used_names.add(node.value.id)
235
+
236
+ for imp_name in self._imports[rel]:
237
+ # Split dotted imports to check root
238
+ root = imp_name.split(".")[0]
239
+ if root not in used_names and imp_name not in used_names:
240
+ # Find the import line
241
+ for node in ast.walk(tree):
242
+ if isinstance(node, ast.Import):
243
+ for alias in node.names:
244
+ name = alias.asname or alias.name
245
+ if name == imp_name:
246
+ findings.append(DeadCodeFinding(
247
+ file=rel, line=node.lineno,
248
+ category="unused_import",
249
+ message=f"Import '{imp_name}' is never used",
250
+ ))
251
+ elif isinstance(node, ast.ImportFrom):
252
+ for alias in node.names:
253
+ name = alias.asname or alias.name
254
+ if name == imp_name:
255
+ findings.append(DeadCodeFinding(
256
+ file=rel, line=node.lineno,
257
+ category="unused_import",
258
+ message=f"Import '{imp_name}' is never used",
259
+ ))
260
+
261
+ return findings
262
+
263
+ def find_unused_functions(self) -> list[DeadCodeFinding]:
264
+ """Post-scan: identify functions defined but never called project-wide."""
265
+ findings: list[DeadCodeFinding] = []
266
+ for func_name, defs in self._func_defs.items():
267
+ if func_name.startswith("_"):
268
+ continue # Private functions are often unused by design
269
+ if func_name.startswith("test_"):
270
+ continue # Test functions are called by pytest, not other code
271
+ if func_name in self.WHITELIST_FUNCTIONS:
272
+ continue
273
+ if func_name not in self._func_calls:
274
+ for file, line in defs:
275
+ findings.append(DeadCodeFinding(
276
+ file=file, line=line,
277
+ category="unused_function",
278
+ message=f"Function '{func_name}' is defined but never called in the project",
279
+ ))
280
+ return findings