-
Notifications
You must be signed in to change notification settings - Fork 4
Feat/automated high confidence autofix phase III #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9b328cb
4e3ca22
ab375e2
95740f9
806d92a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| [flake8] | ||
| max-line-length = 100 | ||
| extend-ignore = E203, W503 |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,15 +3,25 @@ | |
| import ast | ||
| import copy | ||
| from pathlib import Path | ||
| from typing import Dict, List, Set | ||
| from typing import Dict, List, Optional, Set | ||
|
|
||
| from refactron.analyzers.base_analyzer import BaseAnalyzer | ||
| from refactron.core.config import RefactronConfig | ||
| from refactron.core.models import CodeIssue, IssueCategory, IssueLevel | ||
| from refactron.llm.orchestrator import LLMOrchestrator | ||
|
|
||
|
|
||
| class CodeSmellAnalyzer(BaseAnalyzer): | ||
| """Detects common code smells and anti-patterns.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| config: RefactronConfig, | ||
| orchestrator: Optional[LLMOrchestrator] = None, | ||
| ): | ||
| super().__init__(config) | ||
| self.orchestrator = orchestrator | ||
|
|
||
| @property | ||
| def name(self) -> str: | ||
| return "code_smells" | ||
|
|
@@ -51,6 +61,47 @@ def analyze(self, file_path: Path, source_code: str) -> List[CodeIssue]: | |
| ) | ||
| issues.append(issue) | ||
|
|
||
| # AI Triage: Filter out safe/intentional smells | ||
| if self.config.enable_ai_triage and self.orchestrator and issues: | ||
| # Batch evaluate all issues | ||
| # evaluate_issues_batch returns Dict[str, float] mapping issue_id to confidence | ||
| confidence_scores = self.orchestrator.evaluate_issues_batch(issues, source_code) | ||
|
|
||
| # Filter issues with a confidence < 0.3 | ||
| # (meaning LLM thinks it might be a false positive/safe) | ||
| final_issues = [] | ||
| for i, issue in enumerate(issues): | ||
| # evaluate_issues_batch defaults to f"issue_{i}" if rule_id is absent | ||
| issue_id = getattr(issue, "rule_id", None) or f"issue_{i}" | ||
|
|
||
| confidence = confidence_scores.get(issue_id, 1.0) | ||
|
Comment on lines
+73
to
+77
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These IDs identify the rule type ( π€ Prompt for AI Agents |
||
|
|
||
| # Optional: Attach the confidence score to the issue metadata | ||
| # for reporting/debugging | ||
| issue.metadata["validation_confidence"] = confidence | ||
|
|
||
| if confidence >= 0.3: | ||
| # If confidence is very high, trigger auto-fix suggestion | ||
| if confidence > 0.8 and self.orchestrator: | ||
| try: | ||
| # Generate a suggestion using the full source code for context | ||
| suggestion_obj = self.orchestrator.generate_suggestion( | ||
| issue, source_code | ||
| ) | ||
| if suggestion_obj.proposed_code: | ||
| issue.suggestion = suggestion_obj.proposed_code | ||
| issue.metadata["ai_fix_available"] = True | ||
| issue.metadata["ai_explanation"] = suggestion_obj.explanation | ||
| issue.metadata["ai_reasoning"] = suggestion_obj.reasoning | ||
| except Exception: | ||
| # Failing to generate a suggestion shouldn't break triage | ||
| pass | ||
| else: | ||
| pass | ||
|
|
||
| final_issues.append(issue) | ||
| issues = final_issues | ||
|
|
||
| return issues | ||
|
|
||
| def _check_too_many_parameters(self, tree: ast.AST, file_path: Path) -> List[CodeIssue]: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ def _register_fixers(self) -> Dict[str, "BaseFixer"]: | |
| from refactron.autofix.fixers import ( | ||
| AddDocstringsFixer, | ||
| AddMissingCommasFixer, | ||
| AISuggestionFixer, | ||
| ConvertToFStringFixer, | ||
| ExtractMagicNumbersFixer, | ||
| FixIndentationFixer, | ||
|
|
@@ -64,6 +65,7 @@ def _register_fixers(self) -> Dict[str, "BaseFixer"]: | |
| FixIndentationFixer, | ||
| AddMissingCommasFixer, | ||
| RemovePrintStatementsFixer, | ||
| AISuggestionFixer, | ||
| ]: | ||
| fixer = fixer_class() | ||
| fixers[fixer.name] = fixer | ||
|
|
@@ -80,7 +82,9 @@ def can_fix(self, issue: CodeIssue) -> bool: | |
| Returns: | ||
| True if a fixer is available, False otherwise | ||
| """ | ||
| return issue.rule_id in self.fixers if issue.rule_id else False | ||
| if issue.rule_id in self.fixers: | ||
| return True | ||
| return bool(issue.suggestion) | ||
|
Comment on lines
+85
to
+87
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only route to
π§― Safer fallback gating def can_fix(self, issue: CodeIssue) -> bool:
@@
if issue.rule_id in self.fixers:
return True
- return bool(issue.suggestion)
+ return bool(issue.suggestion) and bool(issue.metadata.get("ai_fix_available"))
@@
- if issue.rule_id and issue.rule_id in self.fixers:
+ if issue.rule_id and issue.rule_id in self.fixers:
fixer = self.fixers[issue.rule_id]
- else:
- # Must have issue.suggestion based on can_fix() check
+ elif issue.suggestion and issue.metadata.get("ai_fix_available"):
fixer = self.fixers["ai_suggestion"]
+ else:
+ return FixResult(
+ success=False,
+ reason=f"No fixer available for issue: {issue.rule_id or 'unknown'}",
+ )Also applies to: 106-111 π€ Prompt for AI Agents |
||
|
|
||
| def fix(self, issue: CodeIssue, code: str, preview: bool = True) -> FixResult: | ||
| """ | ||
|
|
@@ -99,7 +103,12 @@ def fix(self, issue: CodeIssue, code: str, preview: bool = True) -> FixResult: | |
| success=False, reason=f"No fixer available for issue: {issue.rule_id or 'unknown'}" | ||
| ) | ||
|
|
||
| fixer = self.fixers[issue.rule_id] | ||
| # Prefer rule-based fixer if available, otherwise use AI suggestion | ||
| if issue.rule_id and issue.rule_id in self.fixers: | ||
| fixer = self.fixers[issue.rule_id] | ||
| else: | ||
| # Must have issue.suggestion based on can_fix() check | ||
| fixer = self.fixers["ai_suggestion"] | ||
|
|
||
| # Check risk level | ||
| if fixer.risk_score > self.safety_level.value: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||||||||||
|
|
||||||||||||
| from __future__ import annotations | ||||||||||||
|
|
||||||||||||
| from typing import Optional | ||||||||||||
| from typing import Optional, cast | ||||||||||||
|
|
||||||||||||
| import requests # type: ignore | ||||||||||||
|
|
@@ -93,6 +94,7 @@ def generate( | |||||||||||
| raise RuntimeError(f"Backend LLM proxy error ({response.status_code}): {error_msg}") | ||||||||||||
|
|
||||||||||||
| data = response.json() | ||||||||||||
| return str(data["content"]) | ||||||||||||
| return cast(str, data["content"]) | ||||||||||||
|
Comment on lines
96
to
98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid turning null content into the literal string
π‘ Proposed fix data = response.json()
- return str(data["content"])
- return cast(str, data["content"])
+ return str(data.get("content") or "")π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| except requests.exceptions.RequestException as e: | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -254,6 +254,22 @@ def generate_documentation( | |
| ) | ||
|
|
||
| def evaluate_issues_batch(self, issues: List[CodeIssue], source_code: str) -> Dict[str, float]: | ||
| """Batch evaluate confidence for multiple issues. | ||
|
|
||
| Args: | ||
| issues: List of issues to evaluate. | ||
| source_code: The source code context. | ||
|
|
||
| Returns: | ||
| A dictionary mapping issue rule_ids (or fallback IDs) to a confidence score. | ||
| """ | ||
| # Default implementation returns 1.0 (high confidence) for all issues | ||
| # Can be enhanced to actually call the LLM for batch triage | ||
| scores = {} | ||
| for i, issue in enumerate(issues): | ||
| issue_id = getattr(issue, "rule_id", None) or f"issue_{i}" | ||
| scores[issue_id] = 1.0 | ||
| return scores | ||
|
Comment on lines
+256
to
+272
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a neutral placeholder score here.
π‘ Safer interim behavior- def evaluate_issues_batch(self, issues: List[CodeIssue], source_code: str) -> Dict[str, float]:
+ def evaluate_issues_batch(self, issues: List[CodeIssue], _source_code: str) -> Dict[str, float]:
"""Batch evaluate confidence for multiple issues.
@@
- # Default implementation returns 1.0 (high confidence) for all issues
- # Can be enhanced to actually call the LLM for batch triage
+ # Default implementation keeps issues visible without forcing auto-fix
+ # until real batch triage is wired in.
scores = {}
for i, issue in enumerate(issues):
issue_id = getattr(issue, "rule_id", None) or f"issue_{i}"
- scores[issue_id] = 1.0
+ scores[issue_id] = 0.5
return scoresπ§° Toolsπͺ Ruff (0.15.5)[warning] 249-249: Unused method argument: (ARG002) π€ Prompt for AI Agents |
||
| """Evaluate a batch of issues for a single file to suppress false positives. | ||
|
|
||
| Args: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,12 +5,13 @@ | |||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Dict, List, Optional, cast | ||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Dict, List, Optional, cast | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| import chromadb | ||||||||||||||||||||||||||||||||||||||||||||||
| from chromadb.config import Settings | ||||||||||||||||||||||||||||||||||||||||||||||
| from sentence_transformers import SentenceTransformer | ||||||||||||||||||||||||||||||||||||||||||||||
| from sentence_transformers import SentenceTransformer # type: ignore | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| CHROMA_AVAILABLE = True | ||||||||||||||||||||||||||||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -60,6 +61,8 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| if not CHROMA_AVAILABLE: | ||||||||||||||||||||||||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||||||||||||||||||||||||
| "ChromaDB is not available. Install with: " | ||||||||||||||||||||||||||||||||||||||||||||||
| "pip install chromadb sentence-transformers" | ||||||||||||||||||||||||||||||||||||||||||||||
| "ChromaDB is not available. " | ||||||||||||||||||||||||||||||||||||||||||||||
| "Install with: pip install chromadb sentence-transformers" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -182,6 +185,8 @@ def _index_file(self, file_path: Path, summarize: bool = False) -> List[CodeChun | |||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| summary = self._summarize_chunk(chunk) | ||||||||||||||||||||||||||||||||||||||||||||||
| if summary: | ||||||||||||||||||||||||||||||||||||||||||||||
| # Prepend summary to content for embedding | ||||||||||||||||||||||||||||||||||||||||||||||
| # (makes it searchable by plain English) | ||||||||||||||||||||||||||||||||||||||||||||||
| # Prepend summary for semantic searchability | ||||||||||||||||||||||||||||||||||||||||||||||
| chunk.content = f"Summary: {summary}\n\n{chunk.content}" | ||||||||||||||||||||||||||||||||||||||||||||||
| chunk.metadata["ai_summary"] = summary | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+188
to
192
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't overwrite the stored code with the AI summary. This mutates π‘ Safer approach- if summary:
- # Prepend summary to content for embedding
- # (makes it searchable by plain English)
- chunk.content = f"Summary: {summary}\n\n{chunk.content}"
- chunk.metadata["ai_summary"] = summary
+ if summary:
+ chunk.metadata["ai_summary"] = summaryπ€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -281,11 +286,13 @@ def _save_metadata(self, metadata: Dict[str, Any]) -> None: | |||||||||||||||||||||||||||||||||||||||||||||
| with open(metadata_file, "w") as f: | ||||||||||||||||||||||||||||||||||||||||||||||
| json.dump(metadata, f, indent=2) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _load_metadata(self) -> Dict: | ||||||||||||||||||||||||||||||||||||||||||||||
| def _load_metadata(self) -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Load index metadata.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| metadata_file = self.index_path / "metadata.json" | ||||||||||||||||||||||||||||||||||||||||||||||
| if not metadata_file.exists(): | ||||||||||||||||||||||||||||||||||||||||||||||
| return {} | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| with open(metadata_file, "r") as f: | ||||||||||||||||||||||||||||||||||||||||||||||
| return cast(Dict, json.load(f)) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+289
to
+297
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate the JSON shape before returning metadata.
Suggested fix-from typing import Dict, List, Optional, cast
+from typing import Any, Dict, List, Optional, cast
...
- def _load_metadata(self) -> Dict:
+ def _load_metadata(self) -> Dict[str, Any]:
"""Load index metadata."""
metadata_file = self.index_path / "metadata.json"
if not metadata_file.exists():
return {}
with open(metadata_file, "r") as f:
- return cast(Dict, json.load(f))
+ data = json.load(f)
+
+ if not isinstance(data, dict):
+ return {}
+
+ return cast(Dict[str, Any], data)π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| return cast(Dict[str, Any], json.load(f)) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+289
to
298
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 289 starts one function definition and Line 290 immediately starts another, so the file will not parse. The duplicated return on Lines 297-298 should also be collapsed once the signature is fixed. π Proposed fix- def _load_metadata(self) -> Dict:
def _load_metadata(self) -> Dict[str, Any]:
"""Load index metadata."""
metadata_file = self.index_path / "metadata.json"
if not metadata_file.exists():
return {}
with open(metadata_file, "r") as f:
- return cast(Dict, json.load(f))
return cast(Dict[str, Any], json.load(f))π§° Toolsπͺ Ruff (0.15.5)[warning] 290-290: Expected an indented block after function definition (invalid-syntax) π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wire this new constructor parameter through the real analyzer factory.
refactron/core/refactron.py:178-200still instantiatesCodeSmellAnalyzer(self.config)without an orchestrator, soenable_ai_triagenever reaches the new AI branch in normal CLI/runtime usage. Right now this only works in tests that inject a mock orchestrator directly.π€ Prompt for AI Agents