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,815 @@
1
+ """AI Anti-Pattern Detector for REF.
2
+
3
+ Multi-language detection of AI-generated code anti-patterns.
4
+ Supports: Python, TypeScript, JavaScript, Java.
5
+ Frameworks: React, Angular, Spring Boot.
6
+
7
+ Patterns detected:
8
+ - Over-abstraction (excessive class/function ratio)
9
+ - Unused parameters in functions
10
+ - Generic naming (data, result, temp, handler, etc.)
11
+ - Verbose/redundant comments
12
+ - Copy-paste similarity between code blocks
13
+ """
14
+
15
+ import ast
16
+ import hashlib
17
+ import re
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import structlog
23
+
24
+ from ref_agents.core.config_loader import ConfigLoader, LanguageConfig
25
+ from ref_agents.core.language_detector import LanguageDetector, LanguageInfo
26
+ from ref_agents.tools.test_smell_walker import (
27
+ TestSmellFinding,
28
+ apply_suppressions,
29
+ detect_test_smells,
30
+ detect_test_smells_regex,
31
+ is_test_path,
32
+ )
33
+
34
+ logger = structlog.get_logger(__name__)
35
+
36
+
37
+ @dataclass
38
+ class PatternFinding:
39
+ """Represents a detected anti-pattern.
40
+
41
+ Attributes:
42
+ pattern_id: Unique identifier for the pattern type.
43
+ severity: LOW, MEDIUM, or HIGH.
44
+ line: Line number where pattern was detected.
45
+ message: Human-readable description.
46
+ suggestion: Recommended fix.
47
+ language: Source language.
48
+ """
49
+
50
+ pattern_id: str
51
+ severity: str
52
+ line: int
53
+ message: str
54
+ suggestion: str
55
+ language: str = "python"
56
+
57
+
58
+ # Default fallback configuration
59
+ DEFAULT_CONFIG: dict[str, Any] = {
60
+ "enabled_patterns": [
61
+ "over_abstraction",
62
+ "unused_params",
63
+ "generic_naming",
64
+ "verbose_comments",
65
+ "copy_paste_blocks",
66
+ ],
67
+ "thresholds": {
68
+ "class_to_function_ratio": 0.5,
69
+ "comment_to_code_ratio": 0.4,
70
+ "similarity_threshold": 0.85,
71
+ "min_block_size": 5,
72
+ },
73
+ "generic_names": [
74
+ "data",
75
+ "result",
76
+ "temp",
77
+ "tmp",
78
+ "handler",
79
+ "manager",
80
+ "helper",
81
+ "utils",
82
+ "misc",
83
+ "stuff",
84
+ "thing",
85
+ "obj",
86
+ "val",
87
+ "var",
88
+ "foo",
89
+ "bar",
90
+ "baz",
91
+ "x",
92
+ "y",
93
+ "z",
94
+ "item",
95
+ "elem",
96
+ "info",
97
+ ],
98
+ "severity_weights": {
99
+ "over_abstraction": "MEDIUM",
100
+ "unused_params": "LOW",
101
+ "generic_naming": "LOW",
102
+ "verbose_comments": "LOW",
103
+ "copy_paste_blocks": "HIGH",
104
+ },
105
+ # STORY-TQ-001 M1a — structural test-smell detection.
106
+ # Mirrors src/ref_agents/config/ai_patterns.yaml `test_smells` block;
107
+ # M1b introduces yaml loader and removes this duplication.
108
+ "test_smells": {
109
+ "enabled": True,
110
+ "test_path_globs": (
111
+ "tests/**/*.py",
112
+ "test_*.py",
113
+ "*_test.py",
114
+ ),
115
+ "excluded_filenames": ("conftest.py", "__init__.py"),
116
+ # /tests/generated/ holds smoke tests written by symbol_smoke_runner
117
+ # (STORY-TQ-003 M3b). They use `assert x is not None` by design.
118
+ "excluded_path_substrings": ("/fixtures/", "/tests/generated/"),
119
+ "patterns": {
120
+ "no_assertions": {"enabled": True, "severity": "HIGH"},
121
+ "trivial_assertion": {"enabled": True, "severity": "HIGH"},
122
+ "mock_called_only": {"enabled": True, "severity": "HIGH"},
123
+ "uut_mocked": {"enabled": True, "severity": "HIGH"},
124
+ "swallowed_exception": {"enabled": True, "severity": "HIGH"},
125
+ # Demoted MEDIUM → LOW: baseline showed mostly legitimate
126
+ # `assert x is None` contract assertions. Informational only.
127
+ "not_none_only": {"enabled": True, "severity": "LOW"},
128
+ },
129
+ },
130
+ }
131
+
132
+
133
+ class AIPatternDetector:
134
+ """Multi-language AI anti-pattern detector.
135
+
136
+ Attributes:
137
+ config: Configuration dictionary for pattern detection.
138
+ findings: List of detected pattern findings.
139
+ language_detector: Language detection utility.
140
+ config_loader: Configuration loader for language rules.
141
+ """
142
+
143
+ def __init__(
144
+ self,
145
+ config: dict[str, Any] | None = None,
146
+ project_root: Path | None = None,
147
+ ) -> None:
148
+ """Initialize detector.
149
+
150
+ Args:
151
+ config: Optional configuration override. Uses lang config if None.
152
+ project_root: Project root for framework detection.
153
+ """
154
+ self.custom_config = config
155
+ self.findings: list[PatternFinding] = []
156
+ self._code_blocks: list[tuple[int, str]] = []
157
+ self.language_detector = LanguageDetector(project_root or Path.cwd())
158
+ self.config_loader = ConfigLoader()
159
+
160
+ def _get_config(self, lang_config: LanguageConfig | None) -> dict[str, Any]:
161
+ """Get effective configuration.
162
+
163
+ Args:
164
+ lang_config: Language configuration if available.
165
+
166
+ Returns:
167
+ Configuration dictionary.
168
+ """
169
+ if self.custom_config:
170
+ return self.custom_config
171
+
172
+ if lang_config and lang_config.anti_patterns:
173
+ # Merge language config with defaults
174
+ merged = DEFAULT_CONFIG.copy()
175
+ merged["generic_names"] = list(
176
+ set(merged["generic_names"]) | set(lang_config.naming.generic_names)
177
+ )
178
+ for key, value in lang_config.anti_patterns.items():
179
+ if isinstance(value, dict) and "threshold" in value:
180
+ merged["thresholds"][f"{key}_threshold"] = value["threshold"]
181
+ return merged
182
+
183
+ return DEFAULT_CONFIG
184
+
185
+ def detect_patterns(self, file_path: Path) -> list[PatternFinding]:
186
+ """Run all enabled pattern detectors on a file.
187
+
188
+ Args:
189
+ file_path: Path to file to analyze.
190
+
191
+ Returns:
192
+ List of PatternFinding objects for detected issues.
193
+
194
+ Raises:
195
+ FileNotFoundError: If file_path does not exist.
196
+ """
197
+ if not file_path.exists():
198
+ raise FileNotFoundError(f"File not found: {file_path}")
199
+
200
+ self.findings = []
201
+ self._code_blocks = []
202
+
203
+ # Detect language
204
+ lang_info = self.language_detector.detect(file_path)
205
+
206
+ if lang_info.language == "unknown":
207
+ logger.warning("unsupported_language", file=str(file_path))
208
+ return []
209
+
210
+ # Load language config
211
+ lang_config: LanguageConfig | None = None
212
+ try:
213
+ lang_config = self.config_loader.load(
214
+ lang_info.language, lang_info.framework
215
+ )
216
+ except FileNotFoundError:
217
+ logger.debug(
218
+ "lang_config_not_found",
219
+ language=lang_info.language,
220
+ using="default_config",
221
+ )
222
+
223
+ config = self._get_config(lang_config)
224
+
225
+ try:
226
+ content = file_path.read_text(encoding="utf-8")
227
+ lines = content.splitlines()
228
+ except UnicodeDecodeError as e:
229
+ logger.warning("unicode_decode_error", file=str(file_path), error=str(e))
230
+ return []
231
+
232
+ # Choose detection strategy based on language
233
+ if lang_info.language == "python":
234
+ return self._detect_python(file_path, content, lines, config, lang_info)
235
+ return self._detect_regex_based(file_path, lines, config, lang_info)
236
+
237
+ def _detect_python( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
238
+ self,
239
+ file_path: Path,
240
+ content: str,
241
+ lines: list[str],
242
+ config: dict[str, Any],
243
+ lang_info: LanguageInfo,
244
+ ) -> list[PatternFinding]:
245
+ """Detect patterns in Python using AST.
246
+
247
+ Args:
248
+ file_path: Path to file.
249
+ content: File content.
250
+ lines: File lines.
251
+ config: Configuration.
252
+ lang_info: Language info.
253
+
254
+ Returns:
255
+ List of findings.
256
+ """
257
+ try:
258
+ tree = ast.parse(content)
259
+ except SyntaxError as e:
260
+ logger.warning("syntax_error", file=str(file_path), error=str(e))
261
+ return [
262
+ PatternFinding(
263
+ pattern_id="syntax_error",
264
+ severity="HIGH",
265
+ line=e.lineno or 1,
266
+ message=f"Syntax error: {e.msg}",
267
+ suggestion="Fix syntax before pattern analysis",
268
+ language=str(lang_info),
269
+ )
270
+ ]
271
+
272
+ enabled = config.get("enabled_patterns", [])
273
+
274
+ if "over_abstraction" in enabled:
275
+ self._detect_over_abstraction(tree, config, lang_info)
276
+
277
+ if "unused_params" in enabled:
278
+ self._detect_unused_params_python(tree, config, lang_info)
279
+
280
+ if "generic_naming" in enabled:
281
+ self._detect_generic_naming_python(tree, config, lang_info)
282
+
283
+ if "verbose_comments" in enabled:
284
+ self._detect_verbose_comments(lines, config, lang_info)
285
+
286
+ if "copy_paste_blocks" in enabled:
287
+ self._detect_copy_paste(lines, config, lang_info)
288
+
289
+ self._detect_test_smells_python(file_path, tree, config, lang_info)
290
+
291
+ logger.info(
292
+ "pattern_detection_complete",
293
+ file=str(file_path),
294
+ language=str(lang_info),
295
+ findings_count=len(self.findings),
296
+ )
297
+
298
+ return self.findings
299
+
300
+ def _detect_test_smells_python(
301
+ self,
302
+ file_path: Path,
303
+ tree: ast.AST,
304
+ config: dict[str, Any],
305
+ lang_info: LanguageInfo,
306
+ ) -> None:
307
+ """Route Python test files through the test-smell walker (STORY-TQ-001)."""
308
+ smells_config = config.get("test_smells")
309
+ if not isinstance(smells_config, dict):
310
+ return
311
+
312
+ project_root = self.language_detector.project_root
313
+ if not is_test_path(file_path, smells_config, project_root):
314
+ return
315
+
316
+ smell_findings = detect_test_smells(
317
+ tree, file_path, smells_config, project_root
318
+ )
319
+
320
+ # M1b: apply inline `ref:allow` directives + emit WARNING for invalid ones.
321
+ try:
322
+ source_lines = file_path.read_text(encoding="utf-8").splitlines()
323
+ except OSError:
324
+ source_lines = []
325
+ kept, warnings = apply_suppressions(smell_findings, source_lines)
326
+
327
+ for finding in kept + warnings:
328
+ self.findings.append(self._convert_smell_finding(finding, lang_info))
329
+
330
+ def _detect_test_smells_regex(
331
+ self,
332
+ file_path: Path,
333
+ content: str,
334
+ config: dict[str, Any],
335
+ lang_info: LanguageInfo,
336
+ ) -> None:
337
+ """Route TS/JS/Go test files through the regex walker (STORY-TQ-001 M1b)."""
338
+ smells_config = config.get("test_smells")
339
+ if not isinstance(smells_config, dict):
340
+ return
341
+
342
+ project_root = self.language_detector.project_root
343
+ if not is_test_path(file_path, smells_config, project_root):
344
+ return
345
+
346
+ smell_findings = detect_test_smells_regex(content, file_path, smells_config)
347
+ kept, warnings = apply_suppressions(smell_findings, content.splitlines())
348
+
349
+ for finding in kept + warnings:
350
+ self.findings.append(self._convert_smell_finding(finding, lang_info))
351
+
352
+ @staticmethod
353
+ def _convert_smell_finding(
354
+ finding: TestSmellFinding,
355
+ lang_info: LanguageInfo,
356
+ ) -> PatternFinding:
357
+ return PatternFinding(
358
+ pattern_id=finding.pattern_id,
359
+ severity=finding.severity,
360
+ line=finding.line,
361
+ message=f"[{finding.function_name}] {finding.message}",
362
+ suggestion=finding.suggestion,
363
+ language=str(lang_info),
364
+ )
365
+
366
+ def _detect_regex_based(
367
+ self,
368
+ file_path: Path,
369
+ lines: list[str],
370
+ config: dict[str, Any],
371
+ lang_info: LanguageInfo,
372
+ ) -> list[PatternFinding]:
373
+ """Detect patterns using regex for non-Python languages.
374
+
375
+ Args:
376
+ file_path: Path to file.
377
+ lines: File lines.
378
+ config: Configuration.
379
+ lang_info: Language info.
380
+
381
+ Returns:
382
+ List of findings.
383
+ """
384
+ enabled = config.get("enabled_patterns", [])
385
+
386
+ if "generic_naming" in enabled:
387
+ self._detect_generic_naming_regex(lines, config, lang_info)
388
+
389
+ if "verbose_comments" in enabled:
390
+ self._detect_verbose_comments(lines, config, lang_info)
391
+
392
+ if "copy_paste_blocks" in enabled:
393
+ self._detect_copy_paste(lines, config, lang_info)
394
+
395
+ self._detect_test_smells_regex(file_path, "\n".join(lines), config, lang_info)
396
+
397
+ logger.info(
398
+ "pattern_detection_complete",
399
+ file=str(file_path),
400
+ language=str(lang_info),
401
+ findings_count=len(self.findings),
402
+ )
403
+
404
+ return self.findings
405
+
406
+ def _detect_over_abstraction(
407
+ self,
408
+ tree: ast.AST,
409
+ config: dict[str, Any],
410
+ lang_info: LanguageInfo,
411
+ ) -> None:
412
+ """Detect excessive class-to-function ratio."""
413
+ class_count = 0
414
+ function_count = 0
415
+
416
+ for node in ast.walk(tree):
417
+ if isinstance(node, ast.ClassDef):
418
+ class_count += 1
419
+ elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
420
+ function_count += 1
421
+
422
+ if function_count == 0:
423
+ return
424
+
425
+ ratio = class_count / function_count
426
+ threshold = config["thresholds"].get("class_to_function_ratio", 0.5)
427
+
428
+ if ratio > threshold:
429
+ self.findings.append(
430
+ PatternFinding(
431
+ pattern_id="over_abstraction",
432
+ severity=config["severity_weights"]["over_abstraction"],
433
+ line=1,
434
+ message=f"High class-to-function ratio: {ratio:.2f} (threshold: {threshold})",
435
+ suggestion="Consider if all classes are necessary.",
436
+ language=str(lang_info),
437
+ )
438
+ )
439
+
440
+ def _detect_unused_params_python(
441
+ self,
442
+ tree: ast.AST,
443
+ config: dict[str, Any],
444
+ lang_info: LanguageInfo,
445
+ ) -> None:
446
+ """Detect unused parameters in Python functions."""
447
+ for node in ast.walk(tree):
448
+ if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
449
+ continue
450
+
451
+ if not node.body:
452
+ continue
453
+
454
+ param_names: set[str] = set()
455
+ for arg in node.args.args:
456
+ if arg.arg not in ("self", "cls"):
457
+ param_names.add(arg.arg)
458
+ for arg in node.args.kwonlyargs:
459
+ param_names.add(arg.arg)
460
+
461
+ if not param_names:
462
+ continue
463
+
464
+ used_names: set[str] = set()
465
+ for child in ast.walk(node):
466
+ if isinstance(child, ast.Name):
467
+ used_names.add(child.id)
468
+
469
+ unused = param_names - used_names
470
+
471
+ for param in unused:
472
+ if param.startswith("_"):
473
+ continue
474
+
475
+ self.findings.append(
476
+ PatternFinding(
477
+ pattern_id="unused_params",
478
+ severity=config["severity_weights"]["unused_params"],
479
+ line=node.lineno,
480
+ message=f"Unused parameter '{param}' in '{node.name}'",
481
+ suggestion=f"Remove '{param}' or prefix with underscore.",
482
+ language=str(lang_info),
483
+ )
484
+ )
485
+
486
+ def _detect_generic_naming_python(
487
+ self,
488
+ tree: ast.AST,
489
+ config: dict[str, Any],
490
+ lang_info: LanguageInfo,
491
+ ) -> None:
492
+ """Detect generic naming in Python code."""
493
+ generic_names = set(config.get("generic_names", []))
494
+
495
+ for node in ast.walk(tree):
496
+ name: str | None = None
497
+ node_type: str = ""
498
+
499
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
500
+ name = node.name
501
+ node_type = "function"
502
+ elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
503
+ name = node.id
504
+ node_type = "variable"
505
+
506
+ if name and name.lower() in generic_names:
507
+ if len(name) == 1 and node_type == "variable":
508
+ continue
509
+
510
+ self.findings.append(
511
+ PatternFinding(
512
+ pattern_id="generic_naming",
513
+ severity=config["severity_weights"]["generic_naming"],
514
+ line=getattr(node, "lineno", 1),
515
+ message=f"Generic {node_type} name: '{name}'",
516
+ suggestion="Use a more descriptive name.",
517
+ language=str(lang_info),
518
+ )
519
+ )
520
+
521
+ def _detect_generic_naming_regex(
522
+ self,
523
+ lines: list[str],
524
+ config: dict[str, Any],
525
+ lang_info: LanguageInfo,
526
+ ) -> None:
527
+ """Detect generic naming using regex for JS/TS/Java."""
528
+ generic_names = set(config.get("generic_names", []))
529
+
530
+ # Patterns for variable/function declarations by language
531
+ patterns = {
532
+ "typescript": [
533
+ r"(?:const|let|var)\s+(\w+)\s*[=:]",
534
+ r"function\s+(\w+)\s*\(",
535
+ ],
536
+ "javascript": [
537
+ r"(?:const|let|var)\s+(\w+)\s*=",
538
+ r"function\s+(\w+)\s*\(",
539
+ ],
540
+ "java": [
541
+ r"(?:public|private|protected)?\s*\w+\s+(\w+)\s*[=;]",
542
+ r"(?:public|private|protected)?\s*\w+\s+(\w+)\s*\(",
543
+ ],
544
+ }
545
+
546
+ lang_patterns = patterns.get(lang_info.language, [])
547
+
548
+ for i, line in enumerate(lines, start=1):
549
+ for pattern in lang_patterns:
550
+ for match in re.finditer(pattern, line):
551
+ name = match.group(1)
552
+ if name and name.lower() in generic_names:
553
+ self.findings.append(
554
+ PatternFinding(
555
+ pattern_id="generic_naming",
556
+ severity=config["severity_weights"]["generic_naming"],
557
+ line=i,
558
+ message=f"Generic name: '{name}'",
559
+ suggestion="Use a more descriptive name.",
560
+ language=str(lang_info),
561
+ )
562
+ )
563
+
564
+ def _detect_verbose_comments(
565
+ self,
566
+ lines: list[str],
567
+ config: dict[str, Any],
568
+ lang_info: LanguageInfo,
569
+ ) -> None:
570
+ """Detect excessive comments."""
571
+ comment_lines = 0
572
+ code_lines = 0
573
+
574
+ # Comment patterns by language
575
+ single_comment = {
576
+ "python": "#",
577
+ "typescript": "//",
578
+ "javascript": "//",
579
+ "java": "//",
580
+ }
581
+ comment_char = single_comment.get(lang_info.language, "#")
582
+
583
+ for line in lines:
584
+ stripped = line.strip()
585
+ if not stripped:
586
+ continue
587
+ if stripped.startswith(comment_char):
588
+ comment_lines += 1
589
+ elif not stripped.startswith(('"""', "'''", "/*", "*")):
590
+ code_lines += 1
591
+
592
+ if code_lines == 0:
593
+ return
594
+
595
+ ratio = comment_lines / code_lines
596
+ threshold = config["thresholds"].get("comment_to_code_ratio", 0.4)
597
+
598
+ if ratio > threshold:
599
+ self.findings.append(
600
+ PatternFinding(
601
+ pattern_id="verbose_comments",
602
+ severity=config["severity_weights"]["verbose_comments"],
603
+ line=1,
604
+ message=f"High comment-to-code ratio: {ratio:.2f}",
605
+ suggestion="Remove redundant comments.",
606
+ language=str(lang_info),
607
+ )
608
+ )
609
+
610
+ def _detect_copy_paste(
611
+ self,
612
+ lines: list[str],
613
+ config: dict[str, Any],
614
+ lang_info: LanguageInfo,
615
+ ) -> None:
616
+ """Detect copy-paste code blocks."""
617
+ min_block = config["thresholds"].get("min_block_size", 5)
618
+
619
+ # Extract non-empty, non-comment lines
620
+ comment_char = "#" if lang_info.language == "python" else "//"
621
+ code_lines: list[tuple[int, str]] = []
622
+
623
+ for idx, line in enumerate(lines):
624
+ stripped = line.strip()
625
+ if stripped and not stripped.startswith(comment_char):
626
+ # Normalize identifiers
627
+ normalized = re.sub(r"[a-zA-Z_][a-zA-Z0-9_]*", "X", stripped)
628
+ code_lines.append((idx + 1, normalized))
629
+
630
+ if len(code_lines) < min_block * 2:
631
+ return
632
+
633
+ blocks: list[tuple[int, str]] = []
634
+
635
+ for i in range(len(code_lines) - min_block + 1):
636
+ block_content = "\n".join(line for _, line in code_lines[i : i + min_block])
637
+ block_hash = hashlib.md5(
638
+ block_content.encode(), usedforsecurity=False
639
+ ).hexdigest()
640
+ start_line = code_lines[i][0]
641
+ blocks.append((start_line, block_hash))
642
+
643
+ # Find duplicates
644
+ seen_hashes: dict[str, int] = {}
645
+ reported_lines: set[int] = set()
646
+
647
+ for start_line, block_hash in blocks:
648
+ if block_hash in seen_hashes:
649
+ original_line = seen_hashes[block_hash]
650
+ if abs(start_line - original_line) < min_block:
651
+ continue
652
+ if start_line in reported_lines:
653
+ continue
654
+
655
+ self.findings.append(
656
+ PatternFinding(
657
+ pattern_id="copy_paste_blocks",
658
+ severity=config["severity_weights"]["copy_paste_blocks"],
659
+ line=start_line,
660
+ message=f"Potential copy-paste from line {original_line}",
661
+ suggestion="Extract duplicated logic into a reusable function.",
662
+ language=str(lang_info),
663
+ )
664
+ )
665
+ reported_lines.add(start_line)
666
+ else:
667
+ seen_hashes[block_hash] = start_line
668
+
669
+
670
+ def detect_ai_patterns_tool(file_path: str, config_path: str | None = None) -> str:
671
+ """Detect AI anti-patterns in a source file.
672
+
673
+ Multi-language support: Python, TypeScript, JavaScript, Java.
674
+ Framework support: React, Angular, Spring Boot.
675
+
676
+ Patterns detected:
677
+ - Over-abstraction (too many classes)
678
+ - Unused parameters
679
+ - Generic naming (data, result, temp, etc.)
680
+ - Verbose comments
681
+ - Copy-paste code blocks
682
+
683
+ Args:
684
+ file_path: Path to the file to analyze.
685
+ config_path: Optional path to config override.
686
+
687
+ Returns:
688
+ Markdown-formatted report of findings.
689
+ """
690
+ from ref_agents.constants import Status
691
+ from ref_agents.session import SessionManager
692
+ from ref_agents.tools.context_tools import _format_available_agents_numbered
693
+ from ref_agents.tools.report_utils import write_report
694
+
695
+ # Enforce agent activation requirement
696
+ active_agent = SessionManager.get().active_agent
697
+ if not active_agent:
698
+ agents_output = _format_available_agents_numbered()
699
+ return (
700
+ f"{Status.ERROR} No REF Agent is active.\n\n"
701
+ "**AI pattern detection requires an active agent for proper context.**\n\n"
702
+ f"{agents_output}\n\n"
703
+ 'Activate an agent first: agent(action="activate", name=agent_name) or agent(action="activate", name=number)'
704
+ )
705
+
706
+ p = Path(file_path)
707
+
708
+ # Load custom config if provided
709
+ config: dict[str, Any] | None = None
710
+ if config_path:
711
+ config_file = Path(config_path)
712
+ if config_file.exists():
713
+ try:
714
+ import yaml
715
+
716
+ config = yaml.safe_load(config_file.read_text())
717
+ logger.info("config_loaded", path=config_path)
718
+ except ImportError:
719
+ logger.warning("yaml_not_installed", using="default_config")
720
+ except (OSError, ValueError) as e:
721
+ logger.warning(
722
+ "config_load_failed", error=str(e), using="default_config"
723
+ )
724
+
725
+ detector = AIPatternDetector(config)
726
+
727
+ try:
728
+ findings = detector.detect_patterns(p)
729
+ except FileNotFoundError:
730
+ return f"Error: File not found: {file_path}"
731
+
732
+ # Generate report
733
+ report_lines: list[str] = []
734
+
735
+ if not findings:
736
+ report_lines.append("✅ No AI anti-patterns detected.\n")
737
+ else:
738
+ high = [f for f in findings if f.severity == "HIGH"]
739
+ medium = [f for f in findings if f.severity == "MEDIUM"]
740
+ low = [f for f in findings if f.severity == "LOW"]
741
+
742
+ report_lines.append(f"Found {len(findings)} potential issues:\n")
743
+ report_lines.append(f"- 🔴 HIGH: {len(high)}")
744
+ report_lines.append(f"- 🟡 MEDIUM: {len(medium)}")
745
+ report_lines.append(f"- 🟢 LOW: {len(low)}\n")
746
+
747
+ report_lines.append("| Severity | Line | Pattern | Message | Suggestion |")
748
+ report_lines.append("|----------|------|---------|---------|------------|")
749
+
750
+ for finding in sorted(findings, key=lambda f: (f.severity != "HIGH", f.line)):
751
+ icon = {"HIGH": "🔴", "MEDIUM": "🟡", "LOW": "🟢"}.get(
752
+ finding.severity, "⚪"
753
+ )
754
+ report_lines.append(
755
+ f"| {icon} {finding.severity} | {finding.line} | "
756
+ f"`{finding.pattern_id}` | {finding.message} | {finding.suggestion} |"
757
+ )
758
+
759
+ report_content = "\n".join(report_lines)
760
+
761
+ # active_agent already validated above, safe to use
762
+ # Fallback to platform_engineer if agent is "unknown" or None
763
+ agent = (
764
+ active_agent
765
+ if active_agent and active_agent != "unknown"
766
+ else "platform_engineer"
767
+ )
768
+ report_name = f"ai_patterns_{p.stem}"
769
+ title = f"AI Pattern Analysis: {p.name}"
770
+
771
+ try:
772
+ # Auto-set project root from file's parent directory if not already set
773
+ try:
774
+ from ref_agents.session import SessionManager
775
+
776
+ _fp = Path(file_path).resolve()
777
+ if not SessionManager.get().project_root and _fp.parent.exists():
778
+ SessionManager.get().project_root = _fp.parent
779
+ except (ImportError, AttributeError):
780
+ pass
781
+ report_path = write_report(agent, report_name, report_content, title=title)
782
+ _ai_pattern_write_graph(findings)
783
+ return f"Analysis complete. Report: `{report_path}`\n\n{report_content}"
784
+ except OSError as e:
785
+ _ai_pattern_write_graph(findings)
786
+ return f"Analysis complete (report save failed: {e}):\n\n{report_content}"
787
+
788
+
789
+ def _ai_pattern_write_graph(findings: list[PatternFinding]) -> None:
790
+ """Write HIGH/MEDIUM pattern findings as nodes in the context graph.
791
+
792
+ Args:
793
+ findings: List of PatternFinding objects from the detector run.
794
+ """
795
+ try:
796
+ from datetime import datetime, timezone
797
+
798
+ from ref_agents.tools.context_graph import ContextNode, get_context_graph
799
+
800
+ graph = get_context_graph()
801
+ for finding in [
802
+ f for f in findings if getattr(f, "severity", "") in ("HIGH", "MEDIUM")
803
+ ]:
804
+ ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S%f")
805
+ node = ContextNode(
806
+ id=f"pattern-{ts}",
807
+ node_type="pattern",
808
+ content=f"[{finding.severity}] {finding.message[:120]}",
809
+ story_id="",
810
+ metadata={"suggestion": str(getattr(finding, "suggestion", ""))[:100]},
811
+ )
812
+ graph.add_node(node)
813
+ graph.save()
814
+ except OSError as _e:
815
+ logger.warning("pattern_graph_write_failed", error=str(_e))