Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 100
extend-ignore = E203, W503
9 changes: 6 additions & 3 deletions .github/workflows/auto-label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ name: Auto-label Bot
on:
issues:
types: [opened, edited]
pull_request:
pull_request_target:
types: [opened, edited, synchronize]

# Default permissions for the workflow
permissions:
contents: read
issues: write
pull-requests: write
contents: read

jobs:
label-issues:
Expand All @@ -20,6 +21,7 @@ jobs:
- name: Auto-label issues
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo, number } = context.issue;
const issue = context.payload.issue;
Expand Down Expand Up @@ -116,7 +118,7 @@ jobs:
}

label-pull-requests:
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -127,6 +129,7 @@ jobs:
- name: Auto-label PRs
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo, number } = context.issue;
const pr = context.payload.pull_request;
Expand Down
1 change: 1 addition & 0 deletions coverage.json

Large diffs are not rendered by default.

53 changes: 52 additions & 1 deletion refactron/analyzers/code_smell_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +17 to +23

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Wire this new constructor parameter through the real analyzer factory.

refactron/core/refactron.py:178-200 still instantiates CodeSmellAnalyzer(self.config) without an orchestrator, so enable_ai_triage never 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
Verify each finding against the current code and only fix it if needed.

In `@refactron/analyzers/code_smell_analyzer.py` around lines 17 - 23, The
CodeSmellAnalyzer now accepts an optional orchestrator but the analyzer factory
still calls CodeSmellAnalyzer(self.config) so enable_ai_triage never receives
the orchestrator; update the factory code that constructs CodeSmellAnalyzer to
pass the orchestrator instance (e.g., CodeSmellAnalyzer(self.config,
orchestrator=self.orchestrator) or the local orchestrator variable) and ensure
the factory/Refactron class exposes/initializes self.orchestrator so it is
threaded into other analyzer constructions as needed.


@property
def name(self) -> str:
return "code_smells"
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

rule_id is not unique enough for per-finding confidence lookup.

These IDs identify the rule type (S004, S005, etc.), not a unique finding. If one file produces multiple issues for the same rule, they all collapse onto the same batch score and triage/autofix decisions from one finding bleed into the others. Please use a per-instance key that evaluate_issues_batch() also returns.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/analyzers/code_smell_analyzer.py` around lines 73 - 77, The loop is
using rule_id (a rule type) as the key for confidence_scores which collapses
multiple findings of the same rule; change the lookup to use the per-instance
unique key that evaluate_issues_batch returns instead. Concretely, update the
code around evaluate_issues_batch and the loop that assigns issue_id so that
issue_id = <the unique per-finding key returned/used by evaluate_issues_batch>
(e.g., issue.instance_id or the explicit key name returned by
evaluate_issues_batch) or fall back to f"issue_{i}" only if no per-instance id
exists, and then use that issue_id to index confidence_scores; adjust
evaluate_issues_batch call if needed to ensure it returns a mapping keyed by
that unique per-finding identifier.


# 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]:
Expand Down
4 changes: 2 additions & 2 deletions refactron/analyzers/complexity_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from pathlib import Path
from typing import List, Union

from radon.complexity import cc_visit
from radon.metrics import mi_visit
from radon.complexity import cc_visit # type: ignore
from radon.metrics import mi_visit # type: ignore

from refactron.analyzers.base_analyzer import BaseAnalyzer
from refactron.core.models import CodeIssue, IssueCategory, IssueLevel
Expand Down
13 changes: 11 additions & 2 deletions refactron/autofix/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def _register_fixers(self) -> Dict[str, "BaseFixer"]:
from refactron.autofix.fixers import (
AddDocstringsFixer,
AddMissingCommasFixer,
AISuggestionFixer,
ConvertToFStringFixer,
ExtractMagicNumbersFixer,
FixIndentationFixer,
Expand Down Expand Up @@ -64,6 +65,7 @@ def _register_fixers(self) -> Dict[str, "BaseFixer"]:
FixIndentationFixer,
AddMissingCommasFixer,
RemovePrintStatementsFixer,
AISuggestionFixer,
]:
fixer = fixer_class()
fixers[fixer.name] = fixer
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Only route to ai_suggestion for actual AI-generated code.

CodeIssue.suggestion is already used by analyzers for prose guidance (for example, refactron/analyzers/code_smell_analyzer.py:122-126 stores advisory text, not replacement source). With this fallback, those issues now look β€œfixable”, and AISuggestionFixer can replace the whole file with that explanation string.

🧯 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
Verify each finding against the current code and only fix it if needed.

In `@refactron/autofix/engine.py` around lines 85 - 87, The current fixable check
uses CodeIssue.suggestion (which can be non-AI prose) causing non-AI guidance to
be treated as a code-fixable issue; update the gating so only true AI-generated
suggestions are considered by replacing checks of issue.suggestion with
issue.ai_suggestion (e.g., change the return condition to check
bool(issue.ai_suggestion) and similarly update the other occurrence around lines
106-111), ensuring that AISuggestionFixer is only invoked when
issue.ai_suggestion is present rather than when issue.suggestion contains
advisory text.


def fix(self, issue: CodeIssue, code: str, preview: bool = True) -> FixResult:
"""
Expand All @@ -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:
Expand Down
41 changes: 41 additions & 0 deletions refactron/autofix/fixers.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,3 +731,44 @@ def apply(self, issue: CodeIssue, code: str) -> FixResult:
def _create_diff(self, original: str, fixed: str) -> str:
"""Create a simple diff."""
return f"--- Original\n{original}\n\n+++ Fixed\n{fixed}"


class AISuggestionFixer(BaseFixer):
"""Generic fixer that applies AI-generated suggestions."""

def __init__(self) -> None:
super().__init__(name="ai_suggestion", risk_score=0.5)

def preview(self, issue: CodeIssue, code: str) -> FixResult:
"""Preview the AI-generated suggestion."""
if not issue.suggestion:
return FixResult(
success=False,
reason="No AI suggestion available for this issue",
risk_score=self.risk_score,
)

return FixResult(
success=True,
reason=issue.metadata.get("ai_explanation", "Applied AI-generated refactoring"),
diff=self._create_diff(code, issue.suggestion),
original=code,
fixed=issue.suggestion,
risk_score=self.risk_score,
)

def apply(self, issue: CodeIssue, code: str) -> FixResult:
"""Apply the AI-generated suggestion."""
return self.preview(issue, code)

def _create_diff(self, original: str, fixed: str) -> str:
"""Create a simple diff."""
import difflib

diff = difflib.unified_diff(
original.splitlines(keepends=True),
fixed.splitlines(keepends=True),
fromfile="Original",
tofile="AI Fixed",
)
return "".join(diff)
3 changes: 2 additions & 1 deletion refactron/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import click
import requests # type: ignore
import yaml
import yaml # type: ignore
from rich import box
from rich.align import Align
from rich.console import Console
Expand Down Expand Up @@ -1146,6 +1146,7 @@ def _interactive_file_selector(workspace_path: Path) -> Path:
console.print(
f"\n[success]βœ“ Selected: {selected_path.relative_to(workspace_path)}[/success]\n"
)
return Path(selected_path)
return cast(Path, selected_path)

except (KeyboardInterrupt, EOFError):
Expand Down
6 changes: 5 additions & 1 deletion refactron/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path
from typing import Any, Dict, List, Optional

import yaml
import yaml # type: ignore

from refactron.core.config_loader import ConfigLoader
from refactron.core.config_validator import ConfigValidator
Expand Down Expand Up @@ -130,6 +130,9 @@ class RefactronConfig:
pattern_learning_enabled: bool = True # Enable learning from feedback
pattern_ranking_enabled: bool = True # Enable ranking based on learned patterns

# AI Triage settings
enable_ai_triage: bool = False # Use LLM to filter false positive code smells

@classmethod
def from_file(
cls,
Expand Down Expand Up @@ -243,6 +246,7 @@ def to_file(self, config_path: Path) -> None:
),
"pattern_learning_enabled": self.pattern_learning_enabled,
"pattern_ranking_enabled": self.pattern_ranking_enabled,
"enable_ai_triage": self.enable_ai_triage,
}

try:
Expand Down
2 changes: 1 addition & 1 deletion refactron/core/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path
from typing import Any, Dict, Optional

import yaml
import yaml # type: ignore

from refactron.core.config_validator import ConfigValidator
from refactron.core.exceptions import ConfigError
Expand Down
2 changes: 1 addition & 1 deletion refactron/core/memory_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def __init__(
# Try to import psutil for accurate memory tracking
self._psutil_available = False
try:
import psutil
import psutil # type: ignore

self._psutil = psutil
self._process = psutil.Process(os.getpid())
Expand Down
3 changes: 3 additions & 0 deletions refactron/core/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ def list_repositories(api_base_url: str, timeout_seconds: int = 10) -> List[Repo
)
if not isinstance(repositories_data, list):
raise RuntimeError(
"Unexpected API response format. "
"Expected list or dict with 'repositories' key. "
f"Got: {type(data)} with keys: "
f"Unexpected API response format. Expected list or dict with "
f"'repositories' key. Got: {type(data)} with keys: "
f"{list(data.keys()) if isinstance(data, dict) else 'N/A'}"
Expand Down
2 changes: 2 additions & 0 deletions refactron/llm/backend_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from typing import Optional
from typing import Optional, cast

import requests # type: ignore
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Avoid turning null content into the literal string "None".

str(data["content"]) makes a backend null look like a successful completion containing "None", which is harder to detect downstream than an empty string or explicit error.

πŸ’‘ Proposed fix
             data = response.json()
-            return str(data["content"])
-            return cast(str, data["content"])
+            return str(data.get("content") or "")
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data = response.json()
return str(data["content"])
return cast(str, data["content"])
data = response.json()
return str(data.get("content") or "")
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/llm/backend_client.py` around lines 96 - 98, The code currently
does return str(data["content"]) which turns a backend null into the literal
"None"; change the logic after response.json() to explicitly handle null: call
data = response.json(), then if data.get("content") is None return an empty
string (or raise an explicit error), otherwise return the content as a str (e.g.
return cast(str, data["content"])). Update the code paths that reference
data["content"] (the response.json() handling) to remove the duplicate returns
and ensure None is not converted into "None".


except requests.exceptions.RequestException as e:
Expand Down
1 change: 1 addition & 0 deletions refactron/llm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def generate(
max_tokens=max_tokens or self.max_tokens,
)

return str(response.choices[0].message.content or "")
return cast(str, response.choices[0].message.content)

def check_health(self) -> bool:
Expand Down
16 changes: 16 additions & 0 deletions refactron/llm/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a neutral placeholder score here.

refactron/analyzers/code_smell_analyzer.py:83-95 treats confidence > 0.8 as β€œgenerate an AI fix now”. Returning 1.0 for every issue turns this stub into one extra LLM call per finding, which is a big cost/latency spike rather than a harmless default.

πŸ’‘ 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: source_code

(ARG002)

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/llm/orchestrator.py` around lines 249 - 265, The
evaluate_issues_batch stub currently assigns a high confidence of 1.0 for every
issue, which triggers downstream immediate AI fixes; change the neutral
placeholder to a lower value (e.g., 0.5) so it does not exceed the 0.8 auto-fix
threshold used by the analyzer. In the evaluate_issues_batch method (and when
computing issue_id via getattr(issue, "rule_id", None) or f"issue_{i}"), set
scores[issue_id] = 0.5 (or another safe <0.8 value) instead of 1.0 so the
default behavior is conservative until a real LLM-based implementation is added.

"""Evaluate a batch of issues for a single file to suppress false positives.

Args:
Expand Down
1 change: 1 addition & 0 deletions refactron/llm/safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def _check_dangerous_imports(self, proposed_code: str, original_code: str) -> Li
dangerous_modules = ["subprocess", "os", "shutil", "sys"]

def get_imports(code: str) -> Set[str]:
imports: Set[str] = set()
imports = set()
try:
tree = ast.parse(code)
Expand Down
9 changes: 8 additions & 1 deletion refactron/rag/indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
)
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't overwrite the stored code with the AI summary.

This mutates CodeChunk.content before indexing, so retrieval now returns "Summary: ..." plus code instead of raw code. refactron/rag/retriever.py:22-33 passes that content through verbatim, which means rag search and LLM context consumers stop receiving valid source snippets.

πŸ’‘ 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
Verify each finding against the current code and only fix it if needed.

In `@refactron/rag/indexer.py` around lines 188 - 192, The code currently mutates
CodeChunk.content by prepending "Summary: ..." which overwrites the raw source;
instead, preserve chunk.content and only store the summary in
chunk.metadata["ai_summary"], and use a separate string (e.g., embed_text =
f"Summary: {summary}\n\n{chunk.content}") when creating embeddings/index
entries; update the indexing call that currently consumes chunk.content to use
that embed_text (look for the place where chunk.content is modified in the
indexer and where the indexer passes content to the embedding/index function),
leaving CodeChunk.content untouched so retriever/LLM consumers still get the
original code.

Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Validate the JSON shape before returning metadata.

cast(Dict, json.load(f)) only satisfies the type checker. If metadata.json ever contains a non-object value, get_stats() will fail on .get(...) instead of falling back cleanly.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _load_metadata(self) -> Dict:
"""Load index metadata."""
metadata_file = self.index_path / "metadata.json"
if not metadata_file.exists():
return {}
with open(metadata_file, "r") as f:
return json.load(f)
return cast(Dict, json.load(f))
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:
data = json.load(f)
if not isinstance(data, dict):
return {}
return cast(Dict[str, Any], data)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/rag/indexer.py` around lines 284 - 291, The _load_metadata function
should validate that the parsed JSON is a mapping before returning it: instead
of blindly returning cast(Dict, json.load(f)), load the file inside a try/except
for json.JSONDecodeError, check isinstance(data, dict) (or Mapping) after
json.load(f), and return the dict only if it is an object; otherwise log or
silently return {} so callers like get_stats() can safely call .get(...) without
error. Ensure you still read from metadata_file / "metadata.json" and handle
missing/invalid JSON by returning an empty dict.

return cast(Dict[str, Any], json.load(f))
Comment on lines +289 to 298

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

_load_metadata() is duplicated and breaks the module.

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
Verify each finding against the current code and only fix it if needed.

In `@refactron/rag/indexer.py` around lines 289 - 298, Remove the duplicate
_load_metadata definition and collapse the duplicated return so the function has
a single valid signature and return; specifically, keep one def
_load_metadata(self) -> Dict[str, Any]: implementation, ensure it uses
self.index_path to build metadata_file, checks metadata_file.exists(), opens and
json.load(f), and returns cast(Dict[str, Any], ...). Remove the earlier
duplicated def _load_metadata(self) -> Dict: and the extra return statement so
the module parses cleanly.

Loading
Loading