ref-agents 1.0.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.
Files changed (175) hide show
  1. ref_agents/__init__.py +9 -0
  2. ref_agents/api_keys.json.example +8 -0
  3. ref_agents/auth.py +129 -0
  4. ref_agents/codemap/..md +62 -0
  5. ref_agents/codemap/CODE_MAP.md +37 -0
  6. ref_agents/codemap/core.md +43 -0
  7. ref_agents/codemap/models.md +43 -0
  8. ref_agents/codemap/prompts.md +40 -0
  9. ref_agents/codemap/security.md +45 -0
  10. ref_agents/codemap/tools.md +94 -0
  11. ref_agents/codemap/tools_browser.md +44 -0
  12. ref_agents/codemap/utils.md +42 -0
  13. ref_agents/codemap/workflow.md +42 -0
  14. ref_agents/config/ai_patterns.yaml +101 -0
  15. ref_agents/config/frameworks/angular.yaml +104 -0
  16. ref_agents/config/frameworks/aspnet.yaml +84 -0
  17. ref_agents/config/frameworks/ef_core.yaml +81 -0
  18. ref_agents/config/frameworks/react.yaml +111 -0
  19. ref_agents/config/frameworks/spring_boot.yaml +117 -0
  20. ref_agents/config/languages/csharp.yaml +153 -0
  21. ref_agents/config/languages/java.yaml +188 -0
  22. ref_agents/config/languages/javascript.yaml +172 -0
  23. ref_agents/config/languages/python.yaml +153 -0
  24. ref_agents/config/languages/typescript.yaml +193 -0
  25. ref_agents/constants.py +553 -0
  26. ref_agents/core/__init__.py +15 -0
  27. ref_agents/core/config_loader.py +160 -0
  28. ref_agents/core/config_models.py +167 -0
  29. ref_agents/core/config_parsing.py +84 -0
  30. ref_agents/core/language_detector.py +388 -0
  31. ref_agents/core/validation_models.py +66 -0
  32. ref_agents/core/validation_primitives.py +176 -0
  33. ref_agents/errors.py +34 -0
  34. ref_agents/license_client.py +247 -0
  35. ref_agents/models/__init__.py +22 -0
  36. ref_agents/models/gherkin.py +45 -0
  37. ref_agents/models/hierarchy.py +80 -0
  38. ref_agents/models/invest.py +59 -0
  39. ref_agents/models/version.py +49 -0
  40. ref_agents/prompts/__init__.py +9 -0
  41. ref_agents/prompts/start_agent.py +772 -0
  42. ref_agents/rules/architecture/backend_patterns.md +43 -0
  43. ref_agents/rules/architecture/diagramming.md +100 -0
  44. ref_agents/rules/architecture/frontend_patterns.md +40 -0
  45. ref_agents/rules/architecture/impact_analysis.md +129 -0
  46. ref_agents/rules/architecture/migration_strategy.md +208 -0
  47. ref_agents/rules/architecture/regression_protocol.md +77 -0
  48. ref_agents/rules/architecture/system_design.md +97 -0
  49. ref_agents/rules/common/codemap_standard.md +97 -0
  50. ref_agents/rules/common/core_protocol.md +59 -0
  51. ref_agents/rules/common/prompt_engineering.md +294 -0
  52. ref_agents/rules/development/debugging.md +32 -0
  53. ref_agents/rules/development/implementation.md +205 -0
  54. ref_agents/rules/operations/completion.md +119 -0
  55. ref_agents/rules/operations/cutover_protocol.md +218 -0
  56. ref_agents/rules/operations/discovery.md +179 -0
  57. ref_agents/rules/operations/fix_workflow.md +87 -0
  58. ref_agents/rules/operations/forensics.md +278 -0
  59. ref_agents/rules/operations/platform.md +263 -0
  60. ref_agents/rules/operations/synchronous_flow.md +25 -0
  61. ref_agents/rules/product/ac_validation.md +25 -0
  62. ref_agents/rules/product/brainstorming.md +27 -0
  63. ref_agents/rules/product/ref_flow.md +101 -0
  64. ref_agents/rules/product/requirements_std.md +114 -0
  65. ref_agents/rules/product/spec_writing.md +235 -0
  66. ref_agents/rules/product/strategy.md +96 -0
  67. ref_agents/rules/quality/documentation_standards.md +46 -0
  68. ref_agents/rules/quality/parity_testing.md +234 -0
  69. ref_agents/rules/quality/project_documentation.md +56 -0
  70. ref_agents/rules/quality/qa_lead.md +111 -0
  71. ref_agents/rules/quality/test_design.md +146 -0
  72. ref_agents/rules/quality/testing_standards.md +293 -0
  73. ref_agents/rules/review/pr_review.md +116 -0
  74. ref_agents/rules/security/security_audit.md +83 -0
  75. ref_agents/security/__init__.py +22 -0
  76. ref_agents/security/dependency_audit.py +188 -0
  77. ref_agents/security/file_audit.py +208 -0
  78. ref_agents/security/network_scan.py +179 -0
  79. ref_agents/security/report_generator.py +313 -0
  80. ref_agents/security/secret_scan.py +252 -0
  81. ref_agents/security/url_scan.py +240 -0
  82. ref_agents/security_scan.py +236 -0
  83. ref_agents/server.py +1586 -0
  84. ref_agents/session.py +100 -0
  85. ref_agents/tool_names.py +55 -0
  86. ref_agents/tools/__init__.py +8 -0
  87. ref_agents/tools/agents_generator.py +315 -0
  88. ref_agents/tools/ai_pattern_detector.py +815 -0
  89. ref_agents/tools/brownfield_populator.py +529 -0
  90. ref_agents/tools/browser/__init__.py +50 -0
  91. ref_agents/tools/browser/evidence_verifier.py +302 -0
  92. ref_agents/tools/browser/execution_logger.py +249 -0
  93. ref_agents/tools/browser/playwright_mcp_client.py +259 -0
  94. ref_agents/tools/browser/screenshot_utils.py +184 -0
  95. ref_agents/tools/browser/test_executor.py +537 -0
  96. ref_agents/tools/code_quality_scanner.py +629 -0
  97. ref_agents/tools/codemap/..md +93 -0
  98. ref_agents/tools/codemap/CODE_MAP.md +30 -0
  99. ref_agents/tools/codemap/browser.md +44 -0
  100. ref_agents/tools/codemap.py +403 -0
  101. ref_agents/tools/codemap_freshness.py +234 -0
  102. ref_agents/tools/comment_smell_scanner.py +346 -0
  103. ref_agents/tools/complexity.py +436 -0
  104. ref_agents/tools/complexity_ast.py +333 -0
  105. ref_agents/tools/compliance.py +246 -0
  106. ref_agents/tools/compliance_remediation.py +846 -0
  107. ref_agents/tools/context_graph.py +839 -0
  108. ref_agents/tools/context_manager.py +550 -0
  109. ref_agents/tools/context_tools.py +121 -0
  110. ref_agents/tools/cross_repo_linker.py +393 -0
  111. ref_agents/tools/dead_code_scanner.py +637 -0
  112. ref_agents/tools/debt_scanner.py +1092 -0
  113. ref_agents/tools/dependency_graph.py +272 -0
  114. ref_agents/tools/discovery_audit.py +372 -0
  115. ref_agents/tools/docs_scanner.py +600 -0
  116. ref_agents/tools/evaluate_gate.py +119 -0
  117. ref_agents/tools/external_detector.py +524 -0
  118. ref_agents/tools/features_generator.py +282 -0
  119. ref_agents/tools/flow_gap_detector.py +373 -0
  120. ref_agents/tools/flow_mapper.py +327 -0
  121. ref_agents/tools/full_suite_runner.py +740 -0
  122. ref_agents/tools/gherkin_parser.py +227 -0
  123. ref_agents/tools/guard_tools.py +139 -0
  124. ref_agents/tools/handoff_tools.py +282 -0
  125. ref_agents/tools/health_scanner.py +1211 -0
  126. ref_agents/tools/hierarchy_manager.py +289 -0
  127. ref_agents/tools/invest_scorer.py +249 -0
  128. ref_agents/tools/jira_confluence_export.py +306 -0
  129. ref_agents/tools/json_output.py +76 -0
  130. ref_agents/tools/migration_mapper.py +946 -0
  131. ref_agents/tools/migration_readiness_scanner.py +209 -0
  132. ref_agents/tools/pattern_learner.py +522 -0
  133. ref_agents/tools/report_utils.py +155 -0
  134. ref_agents/tools/requirements_serializer.py +225 -0
  135. ref_agents/tools/security_audit_tool.py +106 -0
  136. ref_agents/tools/sequencing_engine.py +288 -0
  137. ref_agents/tools/summary_generator.py +275 -0
  138. ref_agents/tools/symbol_resolver.py +306 -0
  139. ref_agents/tools/symbol_smoke_runner.py +336 -0
  140. ref_agents/tools/test_plan_validator.py +189 -0
  141. ref_agents/tools/test_smell_walker.py +902 -0
  142. ref_agents/tools/tier1_fixer.py +502 -0
  143. ref_agents/tools/validators/__init__.py +419 -0
  144. ref_agents/tools/validators/architect.py +268 -0
  145. ref_agents/tools/validators/cutover_engineer.py +167 -0
  146. ref_agents/tools/validators/developer.py +180 -0
  147. ref_agents/tools/validators/discovery.py +150 -0
  148. ref_agents/tools/validators/forensic_engineer.py +191 -0
  149. ref_agents/tools/validators/impact_architect.py +181 -0
  150. ref_agents/tools/validators/migration_planner.py +166 -0
  151. ref_agents/tools/validators/parity_tester.py +155 -0
  152. ref_agents/tools/validators/platform_engineer.py +134 -0
  153. ref_agents/tools/validators/pr_reviewer.py +129 -0
  154. ref_agents/tools/validators/product_manager.py +291 -0
  155. ref_agents/tools/validators/qa_lead.py +172 -0
  156. ref_agents/tools/validators/scrum_master.py +212 -0
  157. ref_agents/tools/validators/security_owner.py +162 -0
  158. ref_agents/tools/validators/specifier.py +134 -0
  159. ref_agents/tools/validators/strategist.py +149 -0
  160. ref_agents/tools/validators/tester.py +121 -0
  161. ref_agents/tools/version_manager.py +202 -0
  162. ref_agents/tools/workflow_tools.py +1549 -0
  163. ref_agents/utils/__init__.py +21 -0
  164. ref_agents/utils/git_utils.py +351 -0
  165. ref_agents/utils/handoff_logger.py +368 -0
  166. ref_agents/utils/ignore_matcher.py +270 -0
  167. ref_agents/workflow/__init__.py +19 -0
  168. ref_agents/workflow/capabilities.py +328 -0
  169. ref_agents/workflow/state_machine.py +708 -0
  170. ref_agents/workflow/transitions.py +658 -0
  171. ref_agents-1.0.0.dist-info/METADATA +365 -0
  172. ref_agents-1.0.0.dist-info/RECORD +175 -0
  173. ref_agents-1.0.0.dist-info/WHEEL +4 -0
  174. ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
  175. ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
@@ -0,0 +1,637 @@
1
+ """Dead Code Scanner for REF Agents.
2
+
3
+ Detects unused code:
4
+ - Unused imports
5
+ - Unused local functions (not exported)
6
+ - Orphan files (never imported)
7
+
8
+ Supports Python (AST), TypeScript, JavaScript, Java (regex).
9
+
10
+ Respects .gitignore and .dockerignore to avoid false positives.
11
+ """
12
+
13
+ import ast
14
+ import re
15
+ from typing import Any
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ import structlog
20
+
21
+ from ref_agents.constants import Severity
22
+ from ref_agents.core.language_detector import LanguageDetector, LANGUAGE_EXTENSIONS
23
+ from ref_agents.tools.debt_scanner import DebtItem, ScanResult
24
+ from ref_agents.utils.ignore_matcher import IgnorePatternMatcher
25
+
26
+ logger = structlog.get_logger(__name__)
27
+
28
+
29
+ @dataclass
30
+ class ImportInfo:
31
+ """Information about an import statement.
32
+
33
+ Attributes:
34
+ module: Module being imported.
35
+ names: Names imported from module.
36
+ file_path: File containing import.
37
+ line_number: Line number of import.
38
+ """
39
+
40
+ module: str
41
+ names: list[str]
42
+ file_path: Path
43
+ line_number: int
44
+
45
+
46
+ @dataclass
47
+ class FunctionInfo:
48
+ """Information about a function definition.
49
+
50
+ Attributes:
51
+ name: Function name.
52
+ file_path: File containing function.
53
+ line_number: Line number of definition.
54
+ is_private: True if name starts with _.
55
+ is_exported: True if referenced in __all__.
56
+ """
57
+
58
+ name: str
59
+ file_path: Path
60
+ line_number: int
61
+ is_private: bool = False
62
+ is_exported: bool = False
63
+
64
+
65
+ @dataclass
66
+ class DeadCodeResult:
67
+ """Result of dead code scan.
68
+
69
+ Attributes:
70
+ unused_imports: List of unused imports.
71
+ orphan_files: Files not imported anywhere.
72
+ unused_functions: Functions never called.
73
+ files_scanned: Number of files analyzed.
74
+ """
75
+
76
+ unused_imports: list[DebtItem] = field(default_factory=list)
77
+ orphan_files: list[DebtItem] = field(default_factory=list)
78
+ unused_functions: list[DebtItem] = field(default_factory=list)
79
+ files_scanned: int = 0
80
+
81
+ def to_scan_result(self) -> ScanResult:
82
+ """Convert to ScanResult for integration with debt scanner."""
83
+ result = ScanResult(files_scanned=self.files_scanned)
84
+ result.items.extend(self.unused_imports)
85
+ result.items.extend(self.orphan_files)
86
+ result.items.extend(self.unused_functions)
87
+ return result
88
+
89
+ @property
90
+ def total_items(self) -> int:
91
+ """Total dead code items found."""
92
+ return (
93
+ len(self.unused_imports)
94
+ + len(self.orphan_files)
95
+ + len(self.unused_functions)
96
+ )
97
+
98
+
99
+ class UnusedImportDetector(ast.NodeVisitor):
100
+ """AST visitor to detect unused imports in a Python file.
101
+
102
+ Attributes:
103
+ imports: Dictionary mapping imported names to line numbers.
104
+ used_names: Set of names used in the file.
105
+ """
106
+
107
+ def __init__(self) -> None:
108
+ """Initialize detector."""
109
+ self.imports: dict[str, int] = {}
110
+ self.used_names: set[str] = set()
111
+ self._in_import = False
112
+
113
+ def visit_Import(self, node: ast.Import) -> None:
114
+ """Record import statements."""
115
+ self._in_import = True
116
+ for alias in node.names:
117
+ name = alias.asname if alias.asname else alias.name.split(".")[0]
118
+ self.imports[name] = node.lineno
119
+ self._in_import = False
120
+ self.generic_visit(node)
121
+
122
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
123
+ """Record from...import statements."""
124
+ self._in_import = True
125
+ for alias in node.names:
126
+ if alias.name == "*":
127
+ # Can't track star imports
128
+ continue
129
+ name = alias.asname if alias.asname else alias.name
130
+ self.imports[name] = node.lineno
131
+ self._in_import = False
132
+ self.generic_visit(node)
133
+
134
+ def visit_Name(self, node: ast.Name) -> None:
135
+ """Record name usage."""
136
+ if not self._in_import:
137
+ self.used_names.add(node.id)
138
+ self.generic_visit(node)
139
+
140
+ def visit_Attribute(self, node: ast.Attribute) -> None:
141
+ """Record attribute access (module.attr)."""
142
+ # Get the root name
143
+ current = node
144
+ while isinstance(current, ast.Attribute):
145
+ current = current.value
146
+ if isinstance(current, ast.Name) and not self._in_import:
147
+ self.used_names.add(current.id)
148
+ self.generic_visit(node)
149
+
150
+ def get_unused_imports(self) -> dict[str, int]:
151
+ """Get imports that are never used.
152
+
153
+ Returns:
154
+ Dictionary mapping unused import names to line numbers.
155
+ """
156
+ unused = {}
157
+ for name, lineno in self.imports.items():
158
+ if name not in self.used_names:
159
+ # Skip common false positives
160
+ if name in ("__future__", "annotations", "TYPE_CHECKING"):
161
+ continue
162
+ unused[name] = lineno
163
+ return unused
164
+
165
+
166
+ class DeadCodeScanner:
167
+ """Scanner for detecting dead code across a project.
168
+
169
+ Attributes:
170
+ project_root: Root directory of project.
171
+ ignore_matcher: Matcher for .gitignore/.dockerignore patterns.
172
+ """
173
+
174
+ def __init__(self, project_root: Path) -> None:
175
+ """Initialize scanner.
176
+
177
+ Args:
178
+ project_root: Project root directory.
179
+ """
180
+ self.project_root = project_root.resolve()
181
+ self.ignore_matcher = IgnorePatternMatcher.from_directory(self.project_root)
182
+ self._debt_counter = 0
183
+
184
+ # Excluded directories
185
+ self._exclude_dirs = {
186
+ "__pycache__",
187
+ ".git",
188
+ ".venv",
189
+ ".ref",
190
+ "venv",
191
+ "node_modules",
192
+ ".tox",
193
+ "dist",
194
+ "build",
195
+ "target",
196
+ ".pytest_cache",
197
+ ".mypy_cache",
198
+ ".ruff_cache",
199
+ "egg-info",
200
+ }
201
+
202
+ def _next_debt_id(self) -> str:
203
+ """Generate next debt ID."""
204
+ self._debt_counter += 1
205
+ return f"DEAD-{self._debt_counter:03d}"
206
+
207
+ def _should_skip_file(self, file_path: Path) -> bool:
208
+ """Check if file should be skipped.
209
+
210
+ Args:
211
+ file_path: Path to check.
212
+
213
+ Returns:
214
+ True if file should be skipped.
215
+ """
216
+ # Check excluded directories
217
+ if any(excluded in file_path.parts for excluded in self._exclude_dirs):
218
+ return True
219
+
220
+ # Check ignore patterns
221
+ if self.ignore_matcher.is_ignored(file_path):
222
+ return True
223
+
224
+ return False
225
+
226
+ def _scan_imports_regex(self, content: str, language: str) -> list[tuple[str, int]]:
227
+ """Extract imports using regex for TS/JS/Java.
228
+
229
+ Args:
230
+ content: File content.
231
+ language: Language name.
232
+
233
+ Returns:
234
+ List of (import_name, line_number) tuples.
235
+ """
236
+ imports: list[tuple[str, int]] = []
237
+ lines = content.splitlines()
238
+
239
+ if language in ("typescript", "javascript"):
240
+ # import ... from '...'
241
+ pattern1 = re.compile(
242
+ r"import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+).*?\s+from\s+['\"]([^'\"]+)['\"]"
243
+ )
244
+ # import '...'
245
+ pattern2 = re.compile(r"import\s+['\"]([^'\"]+)['\"]")
246
+ # require('...')
247
+ pattern3 = re.compile(r"require\(['\"]([^'\"]+)['\"]\)")
248
+
249
+ for i, line in enumerate(lines, 1):
250
+ for pattern in [pattern1, pattern2, pattern3]:
251
+ matches = pattern.findall(line)
252
+ imports.extend((match, i) for match in matches)
253
+
254
+ elif language == "java":
255
+ # import package.Class;
256
+ pattern = re.compile(r"import\s+(?:static\s+)?([\w.]+);")
257
+ for i, line in enumerate(lines, 1):
258
+ matches = pattern.findall(line)
259
+ imports.extend((match, i) for match in matches)
260
+
261
+ elif language == "csharp":
262
+ # using Namespace; / using static Namespace; / using Alias = Namespace;
263
+ pattern = re.compile(r"^using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;")
264
+ for i, line in enumerate(lines, 1):
265
+ match = pattern.match(line.strip())
266
+ if match:
267
+ imports.append((match.group(1), i))
268
+
269
+ return imports
270
+
271
+ def scan_unused_imports(self, file_path: Path) -> list[DebtItem]:
272
+ """Scan single file for unused imports.
273
+
274
+ Supports Python (AST), TypeScript, JavaScript, Java (regex).
275
+
276
+ Args:
277
+ file_path: Path to file.
278
+
279
+ Returns:
280
+ List of debt items for unused imports.
281
+ """
282
+ if self._should_skip_file(file_path):
283
+ return []
284
+
285
+ # Detect language
286
+ detector = LanguageDetector()
287
+ lang_info = detector.detect(file_path)
288
+
289
+ if lang_info.language == "unknown":
290
+ return []
291
+
292
+ try:
293
+ content = file_path.read_text(encoding="utf-8")
294
+ except (OSError, UnicodeDecodeError) as e:
295
+ logger.debug("file_read_error", file=str(file_path), error=str(e))
296
+ return []
297
+
298
+ items: list[DebtItem] = []
299
+
300
+ if lang_info.language == "python":
301
+ try:
302
+ tree = ast.parse(content)
303
+ except SyntaxError as e:
304
+ logger.debug("file_parse_error", file=str(file_path), error=str(e))
305
+ return []
306
+
307
+ detector_ast = UnusedImportDetector()
308
+ detector_ast.visit(tree)
309
+ unused = detector_ast.get_unused_imports()
310
+
311
+ for name, lineno in unused.items():
312
+ items.append(
313
+ DebtItem(
314
+ debt_id=self._next_debt_id(),
315
+ location=f"{file_path}:{lineno}",
316
+ debt_type="unused-import",
317
+ severity=Severity.LOW,
318
+ description=f"Unused import: `{name}`",
319
+ line_number=lineno,
320
+ language="python",
321
+ )
322
+ )
323
+ else:
324
+ # For TS/JS/Java, we can detect imports but unused detection is limited
325
+ # without full AST parsing. For now, we'll skip unused import detection
326
+ # for these languages (can be enhanced later with proper parsers).
327
+ logger.debug(
328
+ "unused_import_scan_skipped",
329
+ language=lang_info.language,
330
+ file=str(file_path),
331
+ )
332
+
333
+ return items
334
+
335
+ def _collect_all_files(self, dir_path: Path) -> set[Path]:
336
+ """Collect all supported language files."""
337
+ all_files: set[Path] = set()
338
+ for extensions in LANGUAGE_EXTENSIONS.values():
339
+ for ext in extensions:
340
+ for file_path in dir_path.rglob(f"*{ext}"):
341
+ if self._should_skip_file(file_path):
342
+ continue
343
+ all_files.add(file_path)
344
+ return all_files
345
+
346
+ def _collect_imported_modules(
347
+ self, files: set[Path], detector: LanguageDetector
348
+ ) -> set[str]:
349
+ """Collect all imports across files."""
350
+ imported_modules: set[str] = set()
351
+ for file_path in files:
352
+ lang_info = detector.detect(file_path)
353
+ if lang_info.language == "unknown":
354
+ continue
355
+
356
+ try:
357
+ content = file_path.read_text(encoding="utf-8")
358
+ except (OSError, UnicodeDecodeError):
359
+ continue
360
+
361
+ if lang_info.language == "python":
362
+ try:
363
+ tree = ast.parse(content)
364
+ except SyntaxError:
365
+ continue
366
+
367
+ for node in ast.walk(tree):
368
+ if isinstance(node, ast.Import):
369
+ for alias in node.names:
370
+ self._add_module_and_parents(imported_modules, alias.name)
371
+ elif isinstance(node, ast.ImportFrom) and node.module:
372
+ self._add_module_and_parents(imported_modules, node.module)
373
+ else:
374
+ imports = self._scan_imports_regex(content, lang_info.language)
375
+ for import_name, _ in imports:
376
+ self._add_module_and_parents(imported_modules, import_name)
377
+ return imported_modules
378
+
379
+ def _add_module_and_parents(self, modules: set[str], name: str) -> None:
380
+ """Add module and its parent packages to set."""
381
+ modules.add(name)
382
+ parts = name.split(".")
383
+ for i in range(len(parts)):
384
+ modules.add(".".join(parts[: i + 1]))
385
+
386
+ def _is_entry_point_or_test(self, file_path: Path, lang_info: Any) -> bool:
387
+ """Check if file is entry point or test."""
388
+ skip_names = {
389
+ "python": (
390
+ "__init__.py",
391
+ "__main__.py",
392
+ "setup.py",
393
+ "conftest.py",
394
+ "manage.py",
395
+ "main.py",
396
+ "app.py",
397
+ "wsgi.py",
398
+ "asgi.py",
399
+ "cli.py",
400
+ "server.py",
401
+ ),
402
+ "typescript": ("index.ts", "index.tsx", "main.ts", "app.tsx"),
403
+ "javascript": ("index.js", "index.jsx", "main.js", "app.jsx"),
404
+ "java": ("Main.java", "Application.java"),
405
+ "csharp": (
406
+ "Program.cs",
407
+ "Startup.cs",
408
+ "GlobalUsings.cs",
409
+ "AssemblyInfo.cs",
410
+ ),
411
+ }
412
+
413
+ if file_path.name in skip_names.get(lang_info.language, ()):
414
+ return True
415
+
416
+ test_patterns = {
417
+ "python": (r"^test_.*\.py$", r".*_test\.py$"),
418
+ "typescript": (r"^.*\.test\.tsx?$", r"^.*\.spec\.tsx?$"),
419
+ "javascript": (r"^.*\.test\.jsx?$", r"^.*\.spec\.jsx?$"),
420
+ "java": (r"^.*Test\.java$", r"^.*Tests\.java$"),
421
+ "csharp": (r"^.*Tests?\.cs$", r"^.*Spec\.cs$"),
422
+ }
423
+
424
+ for pattern in test_patterns.get(lang_info.language, []):
425
+ if re.match(pattern, file_path.name):
426
+ return True
427
+ return False
428
+
429
+ def _get_module_candidates(
430
+ self, file_path: Path, dir_path: Path, language: str
431
+ ) -> list[str]:
432
+ """Get possible module names for a file."""
433
+ try:
434
+ rel_path = file_path.relative_to(dir_path)
435
+ except ValueError:
436
+ return []
437
+
438
+ candidates = []
439
+ if language == "python":
440
+ module_parts = list(rel_path.parts[:-1]) + [rel_path.stem]
441
+ candidates.append(".".join(module_parts))
442
+ if module_parts and module_parts[0] == "src":
443
+ candidates.append(".".join(module_parts[1:]))
444
+ candidates.append(rel_path.stem)
445
+ elif language in ("typescript", "javascript"):
446
+ module_parts = list(rel_path.parts[:-1]) + [rel_path.stem]
447
+ candidates.append("/".join(module_parts))
448
+ candidates.append(".".join(module_parts))
449
+ if module_parts and module_parts[0] == "src":
450
+ candidates.append("/".join(module_parts[1:]))
451
+ candidates.append(".".join(module_parts[1:]))
452
+ candidates.append(rel_path.stem)
453
+ elif language == "java":
454
+ module_parts = list(rel_path.parts[:-1])
455
+ if module_parts:
456
+ candidates.append(".".join(module_parts))
457
+ candidates.append(rel_path.stem)
458
+
459
+ elif language == "csharp":
460
+ # C# files are identified by namespace segments + class name
461
+ module_parts = list(rel_path.parts[:-1]) + [rel_path.stem]
462
+ candidates.append(".".join(module_parts))
463
+ # Strip leading src/ convention
464
+ if module_parts and module_parts[0] == "src":
465
+ candidates.append(".".join(module_parts[1:]))
466
+ candidates.append(rel_path.stem)
467
+
468
+ return candidates
469
+
470
+ def scan_orphan_files(self, directory: Path) -> list[DebtItem]:
471
+ """Find files that are never imported.
472
+
473
+ Supports Python (AST), TypeScript, JavaScript, Java (regex).
474
+
475
+ Args:
476
+ directory: Directory to scan.
477
+
478
+ Returns:
479
+ List of debt items for orphan files.
480
+ """
481
+ dir_path = directory.resolve()
482
+ detector = LanguageDetector()
483
+
484
+ all_files = self._collect_all_files(dir_path)
485
+ imported_modules = self._collect_imported_modules(all_files, detector)
486
+
487
+ items = []
488
+ for file_path in all_files:
489
+ lang_info = detector.detect(file_path)
490
+ if lang_info.language == "unknown":
491
+ continue
492
+
493
+ if self._is_entry_point_or_test(file_path, lang_info):
494
+ continue
495
+
496
+ possible_modules = self._get_module_candidates(
497
+ file_path, dir_path, lang_info.language
498
+ )
499
+ is_imported = any(mod in imported_modules for mod in possible_modules)
500
+
501
+ if not is_imported:
502
+ # Use relative path for display
503
+ try:
504
+ display_path = str(file_path.relative_to(dir_path))
505
+ except ValueError:
506
+ display_path = str(file_path)
507
+
508
+ items.append(
509
+ DebtItem(
510
+ debt_id=self._next_debt_id(),
511
+ location=str(file_path),
512
+ debt_type="orphan-file",
513
+ severity=Severity.MEDIUM,
514
+ description=f"File never imported: `{display_path}`",
515
+ line_number=None,
516
+ language=lang_info.language,
517
+ )
518
+ )
519
+
520
+ return items
521
+
522
+ def scan_directory(self, directory: Path | str) -> DeadCodeResult:
523
+ """Scan directory for all dead code patterns.
524
+
525
+ Args:
526
+ directory: Directory to scan.
527
+
528
+ Returns:
529
+ DeadCodeResult with all findings.
530
+ """
531
+ dir_path = Path(directory).resolve()
532
+ result = DeadCodeResult()
533
+
534
+ if not dir_path.exists():
535
+ logger.error("directory_not_found", directory=str(directory))
536
+ return result
537
+
538
+ # Scan for unused imports in all supported language files
539
+ for extensions in LANGUAGE_EXTENSIONS.values():
540
+ for ext in extensions:
541
+ for file_path in dir_path.rglob(f"*{ext}"):
542
+ if self._should_skip_file(file_path):
543
+ continue
544
+
545
+ result.files_scanned += 1
546
+ unused_imports = self.scan_unused_imports(file_path)
547
+ result.unused_imports.extend(unused_imports)
548
+
549
+ # Scan for orphan files
550
+ orphan_files = self.scan_orphan_files(dir_path)
551
+ result.orphan_files.extend(orphan_files)
552
+
553
+ logger.info(
554
+ "dead_code_scan_complete",
555
+ files_scanned=result.files_scanned,
556
+ unused_imports=len(result.unused_imports),
557
+ orphan_files=len(result.orphan_files),
558
+ )
559
+
560
+ return result
561
+
562
+
563
+ def scan_dead_code(directory: str, summary: bool = False) -> str:
564
+ """Scan directory for dead code.
565
+
566
+ Args:
567
+ directory: Path to scan.
568
+ summary: If True, return quick summary. If False, return full report.
569
+
570
+ Returns:
571
+ Markdown formatted report or summary.
572
+ """
573
+ dir_path = Path(directory).resolve()
574
+ scanner = DeadCodeScanner(dir_path)
575
+ result = scanner.scan_directory(dir_path)
576
+
577
+ if result.total_items == 0:
578
+ return f"✅ No dead code found in {result.files_scanned} files."
579
+
580
+ # Summary mode
581
+ if summary:
582
+ return f"""📋 **Dead Code Summary** ({result.files_scanned} files scanned)
583
+
584
+ | Type | Count |
585
+ |------|-------|
586
+ | Unused Imports | {len(result.unused_imports)} |
587
+ | Orphan Files | {len(result.orphan_files)} |
588
+
589
+ **Total:** {result.total_items} items
590
+
591
+ Run `scan_dead_code(directory, summary=False)` for full report."""
592
+
593
+ # Full report mode
594
+ output = f"""# Dead Code Scan Report
595
+
596
+ *Scanned: {result.files_scanned} files*
597
+ *Found: {result.total_items} items*
598
+
599
+ ## Summary
600
+
601
+ | Type | Count | Severity |
602
+ |------|-------|----------|
603
+ | Unused Imports | {len(result.unused_imports)} | {Severity.LOW} |
604
+ | Orphan Files | {len(result.orphan_files)} | {Severity.MEDIUM} |
605
+
606
+ """
607
+
608
+ if result.unused_imports:
609
+ output += "## Unused Imports\n\n"
610
+ output += "| ID | Location | Import |\n"
611
+ output += "|----|----------|--------|\n"
612
+ for item in result.unused_imports:
613
+ # Extract import name from description
614
+ import_name = item.description.replace("Unused import: ", "")
615
+ output += f"| {item.debt_id} | `{item.location}` | {import_name} |\n"
616
+ output += "\n"
617
+
618
+ if result.orphan_files:
619
+ output += "## Orphan Files (Never Imported)\n\n"
620
+ output += "| ID | File | Note |\n"
621
+ output += "|----|------|------|\n"
622
+ for item in result.orphan_files:
623
+ output += f"| {item.debt_id} | `{item.location}` | Verify if needed |\n"
624
+ output += "\n"
625
+
626
+ output += """## Recommendations
627
+
628
+ 1. **Unused Imports**: Remove to reduce file size and improve clarity
629
+ 2. **Orphan Files**: Verify if file is:
630
+ - Entry point (add to exclusions)
631
+ - Dynamically imported (add comment)
632
+ - Actually unused (safe to delete)
633
+
634
+ *Note: Files in .gitignore/.dockerignore are automatically excluded.*
635
+ """
636
+
637
+ return output