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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- 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))
|