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,502 @@
|
|
|
1
|
+
"""Tier 1 Deterministic Fixer.
|
|
2
|
+
|
|
3
|
+
Executes ruff-based fixes on files flagged by health scan.
|
|
4
|
+
Ruff is idempotent, has its own test suite, and exits non-zero on failure.
|
|
5
|
+
No custom AST transforms — only ruff-delegated fixes to eliminate regression risk.
|
|
6
|
+
|
|
7
|
+
Supported rules (all ruff-native, idempotent):
|
|
8
|
+
E722 — bare-except
|
|
9
|
+
T201 — print statements
|
|
10
|
+
F401 — unused imports
|
|
11
|
+
I001 — import order (isort)
|
|
12
|
+
LOG015 — logging.* instead of structlog (flagged, not auto-fixed)
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from ref_agents.tools.tier1_fixer import run_tier1_fixes
|
|
16
|
+
result = run_tier1_fixes(violations, project_root)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import subprocess
|
|
20
|
+
import shutil
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import structlog
|
|
25
|
+
|
|
26
|
+
logger = structlog.get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
# Ruff rule IDs that map to each REF rule.
|
|
29
|
+
# Only rules ruff can fix automatically (--fix flag).
|
|
30
|
+
RUFF_FIXABLE_RULES: dict[str, list[str]] = {
|
|
31
|
+
"bare_except": ["E722"],
|
|
32
|
+
"print_statement": ["T201"],
|
|
33
|
+
"unused_imports": ["F401"],
|
|
34
|
+
"import_organisation": ["I001"],
|
|
35
|
+
"fstring_in_logger": ["G004"],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Rules ruff detects but cannot safely auto-fix.
|
|
39
|
+
# These stay in Tier 2 work order for developer agent.
|
|
40
|
+
RUFF_DETECT_ONLY: dict[str, list[str]] = {
|
|
41
|
+
"standard_logging": ["LOG015"],
|
|
42
|
+
"missing_type_hints": ["ANN001", "ANN201"],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# REF rules handled by Tier 1 fixer (subset of VIOLATION_CLASSIFICATION).
|
|
46
|
+
TIER1_RULES: frozenset[str] = frozenset(RUFF_FIXABLE_RULES.keys())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class FileFixResult:
|
|
51
|
+
"""Result of running ruff --fix on a single file."""
|
|
52
|
+
|
|
53
|
+
file: str
|
|
54
|
+
rules_applied: list[str] = field(default_factory=list)
|
|
55
|
+
violations_fixed: int = 0
|
|
56
|
+
violations_remaining: int = 0
|
|
57
|
+
ruff_available: bool = True
|
|
58
|
+
error: str = ""
|
|
59
|
+
skipped: bool = False
|
|
60
|
+
skip_reason: str = ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Tier1FixResult:
|
|
65
|
+
"""Aggregate result of Tier 1 fix run."""
|
|
66
|
+
|
|
67
|
+
files_processed: int = 0
|
|
68
|
+
files_fixed: int = 0
|
|
69
|
+
files_skipped: int = 0
|
|
70
|
+
total_fixed: int = 0
|
|
71
|
+
total_remaining: int = 0
|
|
72
|
+
ruff_available: bool = True
|
|
73
|
+
stale_report_root: str = "" # Non-empty when report paths don't match project_root
|
|
74
|
+
file_results: list[FileFixResult] = field(default_factory=list)
|
|
75
|
+
errors: list[str] = field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _remap_path(file_path: Path, project_root: Path) -> Path | None:
|
|
79
|
+
"""Try to remap a stale absolute path into project_root by matching the longest suffix.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
file_path = /old/repo/src/foo/bar.py
|
|
83
|
+
project_root = /new/repo
|
|
84
|
+
candidate = /new/repo/src/foo/bar.py → returned if it exists
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
file_path: Absolute path that does not exist.
|
|
88
|
+
project_root: Current project root to search under.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Remapped Path if found, None otherwise.
|
|
92
|
+
"""
|
|
93
|
+
parts = file_path.parts
|
|
94
|
+
# Walk from longest suffix to shortest, stop at first match
|
|
95
|
+
for i in range(1, len(parts)):
|
|
96
|
+
candidate = project_root.joinpath(*parts[i:])
|
|
97
|
+
if candidate.exists():
|
|
98
|
+
return candidate
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _detect_stale_root(paths: list[str], project_root: Path) -> str:
|
|
103
|
+
"""Return the foreign root prefix if all absolute paths share a root outside project_root.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
paths: File path strings from violations.
|
|
107
|
+
project_root: Current project root.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Foreign root string (e.g. '/Users/x/sage/ref-agents') if detected, else ''.
|
|
111
|
+
"""
|
|
112
|
+
abs_paths = [Path(p) for p in paths if Path(p).is_absolute()]
|
|
113
|
+
if not abs_paths:
|
|
114
|
+
return ""
|
|
115
|
+
# All paths outside project_root
|
|
116
|
+
outside = [p for p in abs_paths if not str(p).startswith(str(project_root))]
|
|
117
|
+
if len(outside) != len(abs_paths):
|
|
118
|
+
return ""
|
|
119
|
+
# Find common ancestor by walking parts of first path vs all others
|
|
120
|
+
try:
|
|
121
|
+
reference = outside[0].parts
|
|
122
|
+
common_depth = len(reference)
|
|
123
|
+
for p in outside[1:]:
|
|
124
|
+
for i, (a, b) in enumerate(zip(reference, p.parts)):
|
|
125
|
+
if a != b:
|
|
126
|
+
common_depth = min(common_depth, i)
|
|
127
|
+
break
|
|
128
|
+
else:
|
|
129
|
+
common_depth = min(common_depth, len(p.parts))
|
|
130
|
+
common_parts = reference[:common_depth]
|
|
131
|
+
return (
|
|
132
|
+
str(Path(common_parts[0]).joinpath(*common_parts[1:]))
|
|
133
|
+
if common_parts
|
|
134
|
+
else str(outside[0].parent)
|
|
135
|
+
)
|
|
136
|
+
except (IndexError, OSError, ValueError):
|
|
137
|
+
return str(outside[0].parent)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _find_ruff(project_root: Path) -> str | None:
|
|
141
|
+
"""Find ruff executable: venv first, then PATH.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
project_root: Project root to check for venv.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Path to ruff executable, or None if not found.
|
|
148
|
+
"""
|
|
149
|
+
# Check common venv locations relative to project root
|
|
150
|
+
candidates = [
|
|
151
|
+
project_root / ".venv" / "bin" / "ruff",
|
|
152
|
+
project_root / "venv" / "bin" / "ruff",
|
|
153
|
+
project_root / ".venv" / "Scripts" / "ruff.exe", # Windows
|
|
154
|
+
]
|
|
155
|
+
for candidate in candidates:
|
|
156
|
+
if candidate.exists():
|
|
157
|
+
return str(candidate)
|
|
158
|
+
|
|
159
|
+
# Fall back to PATH
|
|
160
|
+
return shutil.which("ruff")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_ruff_rules_for_violations(violations: list[dict]) -> list[str]:
|
|
164
|
+
"""Extract ruff rule IDs for a list of violations.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
violations: List of violation dicts from remediation session.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Deduplicated list of ruff rule IDs to apply.
|
|
171
|
+
"""
|
|
172
|
+
rules: list[str] = []
|
|
173
|
+
for v in violations:
|
|
174
|
+
rule = v.get("rule", "")
|
|
175
|
+
if rule in RUFF_FIXABLE_RULES:
|
|
176
|
+
rules.extend(RUFF_FIXABLE_RULES[rule])
|
|
177
|
+
return list(dict.fromkeys(rules)) # deduplicate, preserve order
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _run_ruff_fix(
|
|
181
|
+
ruff: str,
|
|
182
|
+
file_path: Path,
|
|
183
|
+
rules: list[str],
|
|
184
|
+
) -> tuple[int, str, str]:
|
|
185
|
+
"""Run ruff --fix on a single file with specific rules.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
ruff: Path to ruff executable.
|
|
189
|
+
file_path: File to fix.
|
|
190
|
+
rules: Ruff rule codes to apply.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Tuple of (returncode, stdout, stderr).
|
|
194
|
+
"""
|
|
195
|
+
select = ",".join(rules)
|
|
196
|
+
cmd = [ruff, "check", "--fix", "--select", select, str(file_path)]
|
|
197
|
+
try:
|
|
198
|
+
result = subprocess.run(
|
|
199
|
+
cmd,
|
|
200
|
+
capture_output=True,
|
|
201
|
+
text=True,
|
|
202
|
+
timeout=30,
|
|
203
|
+
)
|
|
204
|
+
return result.returncode, result.stdout, result.stderr
|
|
205
|
+
except subprocess.TimeoutExpired:
|
|
206
|
+
return 1, "", f"ruff timed out on {file_path}"
|
|
207
|
+
except OSError as e:
|
|
208
|
+
return 1, "", str(e)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _run_ruff_check(
|
|
212
|
+
ruff: str,
|
|
213
|
+
file_path: Path,
|
|
214
|
+
rules: list[str],
|
|
215
|
+
) -> int:
|
|
216
|
+
"""Count remaining violations after fix.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
ruff: Path to ruff executable.
|
|
220
|
+
file_path: File to check.
|
|
221
|
+
rules: Ruff rule codes to count.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Number of remaining violations (0 = clean).
|
|
225
|
+
"""
|
|
226
|
+
select = ",".join(rules)
|
|
227
|
+
cmd = [ruff, "check", "--select", select, str(file_path)]
|
|
228
|
+
try:
|
|
229
|
+
result = subprocess.run(
|
|
230
|
+
cmd,
|
|
231
|
+
capture_output=True,
|
|
232
|
+
text=True,
|
|
233
|
+
timeout=30,
|
|
234
|
+
)
|
|
235
|
+
if result.returncode == 0:
|
|
236
|
+
return 0
|
|
237
|
+
# Count lines that look like violations (contain file:line:col)
|
|
238
|
+
return sum(1 for line in result.stdout.splitlines() if ".py:" in line)
|
|
239
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
240
|
+
return -1 # unknown
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def fix_file(
|
|
244
|
+
ruff: str,
|
|
245
|
+
file_path: Path,
|
|
246
|
+
violations: list[dict],
|
|
247
|
+
project_root: Path | None = None,
|
|
248
|
+
) -> FileFixResult:
|
|
249
|
+
"""Apply Tier 1 ruff fixes to a single file.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
ruff: Path to ruff executable.
|
|
253
|
+
file_path: File to fix.
|
|
254
|
+
violations: Violations for this file from remediation session.
|
|
255
|
+
project_root: Used to attempt path remapping when file_path is stale.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
FileFixResult with outcome details.
|
|
259
|
+
"""
|
|
260
|
+
result = FileFixResult(file=str(file_path))
|
|
261
|
+
|
|
262
|
+
if not file_path.exists():
|
|
263
|
+
if project_root and file_path.is_absolute():
|
|
264
|
+
remapped = _remap_path(file_path, project_root)
|
|
265
|
+
if remapped:
|
|
266
|
+
logger.warning(
|
|
267
|
+
"stale_path_remapped",
|
|
268
|
+
original=str(file_path),
|
|
269
|
+
remapped=str(remapped),
|
|
270
|
+
)
|
|
271
|
+
file_path = remapped
|
|
272
|
+
result.file = str(file_path)
|
|
273
|
+
else:
|
|
274
|
+
result.skipped = True
|
|
275
|
+
parts = file_path.parts
|
|
276
|
+
origin = (
|
|
277
|
+
"/".join(parts[:5]) if len(parts) >= 4 else str(file_path.parent) # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
278
|
+
)
|
|
279
|
+
result.skip_reason = (
|
|
280
|
+
f"stale path: report generated in {origin}"
|
|
281
|
+
f", not found under {project_root}"
|
|
282
|
+
)
|
|
283
|
+
logger.warning(
|
|
284
|
+
"stale_path_not_remapped",
|
|
285
|
+
file=str(file_path),
|
|
286
|
+
project_root=str(project_root),
|
|
287
|
+
)
|
|
288
|
+
return result
|
|
289
|
+
else:
|
|
290
|
+
result.skipped = True
|
|
291
|
+
result.skip_reason = "File not found"
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
if not file_path.suffix == ".py":
|
|
295
|
+
result.skipped = True
|
|
296
|
+
result.skip_reason = "Not a Python file (ruff fixes Python only)"
|
|
297
|
+
return result
|
|
298
|
+
|
|
299
|
+
rules = _get_ruff_rules_for_violations(violations)
|
|
300
|
+
if not rules:
|
|
301
|
+
result.skipped = True
|
|
302
|
+
result.skip_reason = "No ruff-fixable rules in violation set"
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
result.rules_applied = rules
|
|
306
|
+
|
|
307
|
+
# Count violations before fix
|
|
308
|
+
before = _run_ruff_check(ruff, file_path, rules)
|
|
309
|
+
|
|
310
|
+
# Apply fix
|
|
311
|
+
returncode, stdout, stderr = _run_ruff_fix(ruff, file_path, rules)
|
|
312
|
+
|
|
313
|
+
if returncode not in (0, 1):
|
|
314
|
+
# 0 = no issues, 1 = issues found/fixed, anything else = error
|
|
315
|
+
result.error = stderr or stdout or f"ruff exited {returncode}"
|
|
316
|
+
logger.error("ruff_fix_failed", file=str(file_path), error=result.error)
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
# Count violations after fix
|
|
320
|
+
after = _run_ruff_check(ruff, file_path, rules)
|
|
321
|
+
result.violations_fixed = max(0, before - after) if before >= 0 else 0
|
|
322
|
+
result.violations_remaining = max(0, after) if after >= 0 else 0
|
|
323
|
+
|
|
324
|
+
if result.violations_fixed > 0:
|
|
325
|
+
logger.info(
|
|
326
|
+
"tier1_fixed",
|
|
327
|
+
file=str(file_path),
|
|
328
|
+
fixed=result.violations_fixed,
|
|
329
|
+
remaining=result.violations_remaining,
|
|
330
|
+
rules=rules,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return result
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def run_tier1_fixes(
|
|
337
|
+
violations: list[dict],
|
|
338
|
+
project_root: Path,
|
|
339
|
+
) -> Tier1FixResult:
|
|
340
|
+
"""Run Tier 1 ruff fixes on all files with pending Tier 1 violations.
|
|
341
|
+
|
|
342
|
+
Groups violations by file, applies ruff --fix per file with only the
|
|
343
|
+
rules relevant to that file's violations. Verifies fix by re-checking.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
violations: List of violation dicts from remediation session (Tier 1, pending).
|
|
347
|
+
project_root: Project root for locating ruff in venv.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Tier1FixResult with per-file outcomes and aggregate counts.
|
|
351
|
+
"""
|
|
352
|
+
aggregate = Tier1FixResult()
|
|
353
|
+
|
|
354
|
+
ruff = _find_ruff(project_root)
|
|
355
|
+
if not ruff:
|
|
356
|
+
aggregate.ruff_available = False
|
|
357
|
+
aggregate.errors.append(
|
|
358
|
+
"ruff not found. Install with: pip install ruff or uv add ruff"
|
|
359
|
+
)
|
|
360
|
+
logger.error("ruff_not_found", project_root=str(project_root))
|
|
361
|
+
return aggregate
|
|
362
|
+
|
|
363
|
+
logger.info("ruff_found", path=ruff)
|
|
364
|
+
|
|
365
|
+
# Group violations by file
|
|
366
|
+
by_file: dict[str, list[dict]] = {}
|
|
367
|
+
for v in violations:
|
|
368
|
+
f = v.get("file", "")
|
|
369
|
+
if f:
|
|
370
|
+
by_file.setdefault(f, []).append(v)
|
|
371
|
+
|
|
372
|
+
aggregate.files_processed = len(by_file)
|
|
373
|
+
|
|
374
|
+
# Detect stale report before processing any files
|
|
375
|
+
stale_root = _detect_stale_root(list(by_file.keys()), project_root)
|
|
376
|
+
if stale_root:
|
|
377
|
+
aggregate.stale_report_root = stale_root
|
|
378
|
+
aggregate.errors.append(
|
|
379
|
+
f"Health report was generated in a different repo: {stale_root}. "
|
|
380
|
+
f"Current project root: {project_root}. "
|
|
381
|
+
'Attempting path remapping — re-run `scan(target="health")` to fix permanently.'
|
|
382
|
+
)
|
|
383
|
+
logger.error(
|
|
384
|
+
"stale_health_report_detected",
|
|
385
|
+
report_root=stale_root,
|
|
386
|
+
project_root=str(project_root),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
for file_str, file_violations in by_file.items():
|
|
390
|
+
file_path = Path(file_str)
|
|
391
|
+
# Resolve relative paths against project root
|
|
392
|
+
if not file_path.is_absolute():
|
|
393
|
+
file_path = project_root / file_path
|
|
394
|
+
|
|
395
|
+
file_result = fix_file(ruff, file_path, file_violations, project_root)
|
|
396
|
+
aggregate.file_results.append(file_result)
|
|
397
|
+
|
|
398
|
+
if file_result.skipped:
|
|
399
|
+
aggregate.files_skipped += 1
|
|
400
|
+
elif file_result.error:
|
|
401
|
+
aggregate.errors.append(f"{file_str}: {file_result.error}")
|
|
402
|
+
else:
|
|
403
|
+
if file_result.violations_fixed > 0:
|
|
404
|
+
aggregate.files_fixed += 1
|
|
405
|
+
aggregate.total_fixed += file_result.violations_fixed
|
|
406
|
+
aggregate.total_remaining += file_result.violations_remaining
|
|
407
|
+
|
|
408
|
+
logger.info(
|
|
409
|
+
"tier1_complete",
|
|
410
|
+
files_processed=aggregate.files_processed,
|
|
411
|
+
files_fixed=aggregate.files_fixed,
|
|
412
|
+
total_fixed=aggregate.total_fixed,
|
|
413
|
+
total_remaining=aggregate.total_remaining,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return aggregate
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def format_tier1_result(result: Tier1FixResult, session_id: str) -> str:
|
|
420
|
+
"""Format Tier 1 fix result as agent-readable summary.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
result: Tier1FixResult from run_tier1_fixes.
|
|
424
|
+
session_id: Remediation session ID for follow-up calls.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Formatted string for MCP tool response.
|
|
428
|
+
"""
|
|
429
|
+
if not result.ruff_available:
|
|
430
|
+
return (
|
|
431
|
+
"❌ Tier 1 fixes could not run: ruff not found in project venv or PATH.\n"
|
|
432
|
+
"Install ruff: `uv add ruff` or `pip install ruff`\n"
|
|
433
|
+
'Then retry: `remediate(action="execute", session_id="'
|
|
434
|
+
+ session_id
|
|
435
|
+
+ '", tier="tier1")`'
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if result.stale_report_root:
|
|
439
|
+
stale_block = (
|
|
440
|
+
"⚠️ **Stale health report detected.**\n"
|
|
441
|
+
f" Report was generated in: `{result.stale_report_root}`\n"
|
|
442
|
+
" Path remapping attempted. Files that could not be remapped are listed under Skipped.\n"
|
|
443
|
+
' Fix permanently: `scan(target="health", directory="<current_project>")`\n'
|
|
444
|
+
f' Then restart: `remediate(action="start", directory="<current_project>")`\n'
|
|
445
|
+
)
|
|
446
|
+
else:
|
|
447
|
+
stale_block = ""
|
|
448
|
+
|
|
449
|
+
lines = [
|
|
450
|
+
"## ⚡ Tier 1 Fix Complete",
|
|
451
|
+
f"Files processed: {result.files_processed} | "
|
|
452
|
+
f"Fixed: {result.files_fixed} | "
|
|
453
|
+
f"Violations resolved: {result.total_fixed} | "
|
|
454
|
+
f"Remaining: {result.total_remaining}",
|
|
455
|
+
"",
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
if stale_block:
|
|
459
|
+
lines.append(stale_block)
|
|
460
|
+
|
|
461
|
+
if result.errors:
|
|
462
|
+
lines.append("### Errors")
|
|
463
|
+
for e in result.errors:
|
|
464
|
+
lines.append(f" - {e}")
|
|
465
|
+
lines.append("")
|
|
466
|
+
|
|
467
|
+
fixed_files = [r for r in result.file_results if r.violations_fixed > 0]
|
|
468
|
+
if fixed_files:
|
|
469
|
+
lines.append("### Fixed")
|
|
470
|
+
for r in fixed_files:
|
|
471
|
+
lines.append(
|
|
472
|
+
f" - `{r.file}` — {r.violations_fixed} fixed, "
|
|
473
|
+
f"{r.violations_remaining} remaining "
|
|
474
|
+
f"(rules: {', '.join(r.rules_applied)})"
|
|
475
|
+
)
|
|
476
|
+
lines.append("")
|
|
477
|
+
|
|
478
|
+
skipped = [r for r in result.file_results if r.skipped]
|
|
479
|
+
if skipped:
|
|
480
|
+
lines.append("### Skipped")
|
|
481
|
+
for r in skipped:
|
|
482
|
+
lines.append(f" - `{r.file}` — {r.skip_reason}")
|
|
483
|
+
lines.append("")
|
|
484
|
+
|
|
485
|
+
if result.total_remaining > 0:
|
|
486
|
+
lines.append(
|
|
487
|
+
f"⚠️ {result.total_remaining} violations remain after Tier 1. "
|
|
488
|
+
"These require manual developer review."
|
|
489
|
+
)
|
|
490
|
+
lines.append("")
|
|
491
|
+
|
|
492
|
+
lines.append("### Next Step")
|
|
493
|
+
if result.total_remaining == 0 and not result.errors:
|
|
494
|
+
lines.append(
|
|
495
|
+
f'Run Tier 2 approval: `remediate(action="approve", session_id="{session_id}")`'
|
|
496
|
+
)
|
|
497
|
+
else:
|
|
498
|
+
lines.append(
|
|
499
|
+
f'Check full state: `remediate(action="state", session_id="{session_id}")`'
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
return "\n".join(lines)
|