Skip to content
Closed
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]
Comment on lines +7 to 8

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

🧩 Analysis chain

🌐 Web query:

For GitHub Actions, what are the security implications of using pull_request_targettogether withactions/checkout, and what is the recommended safe pattern for labeling/comment-only workflows?

💡 Result:

Security implications (pull_request_target + actions/checkout)

  • pull_request_target runs in the context of the base repo, and GitHub explicitly warns that running untrusted PR code under this trigger can lead to issues like cache poisoning and unintended access to write privileges or secrets. This event is intended for actions like labeling/commenting on PRs from forks, and you should avoid it if you need to build or run PR code. [1]
  • The classic “pwn request” happens when a pull_request_target workflow checks out the PR’s head commit (attacker-controlled) and then runs build/scripts. Even if you think you’re not using secrets, the job still has a privileged token available; and if actions/checkout leaves credentials available to later steps, that makes exfiltration easier. [2]
  • actions/checkout can persist auth material for later git operations (and has evolved how it stores it), so treating any subsequent step as potentially able to access credentials is part of the risk model—especially dangerous if you’ve checked out attacker-controlled code. [2][3]

Recommended safe pattern for labeling/comment-only workflows

Use pull_request_target without checking out PR code and with least-privilege token permissions, then interact with the PR only via the API (e.g., actions/github-script).

name: pr-triage

on:
  pull_request_target:
    types: [opened, reopened, synchronize]

permissions:
  pull-requests: write   # for labels
  issues: write          # for comments (PRs are issues)
  contents: none         # avoid repo content access unless truly needed

jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v8
        with:
          script: |
            const pr = context.payload.pull_request
            // label example
            await github.rest.issues.addLabels({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: pr.number,
              labels: ['needs-triage'],
            })
            // comment example
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: pr.number,
              body: "Thanks for the PR! A maintainer will review it soon.",
            })

If you must use actions/checkout in a pull_request_target workflow, only check out a trusted ref (typically the default branch / base repo ref) and do not execute PR-provided code; GitHub also recently reinforced that pull_request_target uses the default branch as the workflow source/reference, but still notes the elevated-risk nature of the event. [4]

Sources: GitHub docs warning on pull_request_target [1], GitHub Security Lab “Preventing pwn requests” (checkout + token/credential persistence risk) [2], actions/checkout docs (credential persistence behavior) [3], GitHub Changelog on pull_request_target default-branch execution + risk note [4].


🏁 Script executed:

# Check the auto-label.yml file structure
echo "=== Checking auto-label.yml ===" 
cat -n .github/workflows/auto-label.yml | head -160 | tail -150

Repository: Refactron-ai/Refactron_lib

Length of output: 6512


🏁 Script executed:

# Also check first-time-contributor.yml pattern for comparison
echo "=== Checking first-time-contributor.yml (lines 44-68) ==="
cat -n .github/workflows/first-time-contributor.yml | sed -n '40,75p'

Repository: Refactron-ai/Refactron_lib

Length of output: 2462


Remove the checkout step from the pull_request_target job—it is unnecessary and creates a security risk.

This job only uses the GitHub API (github.rest.pulls.listFiles, github.rest.issues.addLabels) and does not read or execute code from the repository. Running actions/checkout@v6 (lines 124–127) under pull_request_target creates an unnecessary privilege escalation: the workflow token becomes available in an environment where untrusted PR code is checked out, increasing the risk of credential exfiltration or unintended access to secrets.

.github/workflows/first-time-contributor.yml (lines 44–68) already demonstrates the safe pattern for pull_request_target workflows: interact with PRs only via the GitHub API without checking out code.

Additionally, reduce workflow permissions to least-privilege: remove contents: read (line 12) since the job does not access repository files.

🔒 Proposed fix
 jobs:
   label-pull-requests:
     if: github.event_name == 'pull_request_target'
     runs-on: ubuntu-latest
     steps:
-      - name: Checkout code
-        uses: actions/checkout@v6
-        with:
-          fetch-depth: 0
-
       - name: Auto-label PRs
         uses: actions/github-script@v8
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/auto-label.yml around lines 7 - 8, Remove the unnecessary
and risky actions/checkout@v6 step from the pull_request_target job and drop the
repository contents read permission; specifically, eliminate the checkout
invocation currently present under the pull_request_target job
(actions/checkout@v6) and remove "contents: read" from the workflow permissions
so the job only uses GitHub API calls (e.g., github.rest.pulls.listFiles and
github.rest.issues.addLabels) without checking out repository code.


# Default permissions for the workflow
permissions:
contents: read
issues: write
pull-requests: write
Comment on lines +10 to 14

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

🧩 Analysis chain

🌐 Web query:

For a GitHub Actions workflow that labels pull requests using github.rest.issues.addLabelsand reads changed files withgithub.rest.pulls.listFiles, what are the minimum required permissions values?

💡 Result:

Minimum required permissions for a workflow that:

  • calls github.rest.pulls.listFiles (GET /pulls/{pull_number}/files) and
  • labels PRs via github.rest.issues.addLabels (POST /issues/{issue_number}/labels)

is:

permissions:
  pull-requests: write

Because listing PR files requires pull-requests: read [2] and adding labels can be authorized by pull-requests: write (as an alternative to issues: write) [1], and write implies read. GitHub’s own actions/labeler also documents pull-requests: write as sufficient for applying existing labels [3].

If you prefer splitting permissions, this also works:

permissions:
  pull-requests: read
  issues: write

[1] (docs.github.com)
[2] (docs.github.com)
[3] (github.com)

Citations:


🏁 Script executed:

cat -n .github/workflows/auto-label.yml

Repository: Refactron-ai/Refactron_lib

Length of output: 10030


Fix the proposed permissions — contents: read is required for actions/checkout.

The concern about pull-requests: write being overly broad is valid, but the proposed fix has a critical flaw: removing contents: read will break the workflow. The label-pull-requests job uses actions/checkout@v6 (line 125), which requires contents: read to access repository files.

The correct approach is to keep contents: read at the workflow level and either:

  1. Reduce pull-requests: write to pull-requests: read at the workflow level, or
  2. Use job-scoped permissions to grant only what each job needs:
🔐 Corrected fix
 permissions:
   contents: read
   issues: write
-  pull-requests: write
+  pull-requests: read

 jobs:
   label-issues:
     if: github.event_name == 'issues'
+    permissions:
+      issues: write
     runs-on: ubuntu-latest

   label-pull-requests:
     if: github.event_name == 'pull_request_target'
+    permissions:
+      contents: read
+      pull-requests: read
+      issues: write
     runs-on: ubuntu-latest
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/auto-label.yml around lines 10 - 14, Keep contents: read
at the workflow level because actions/checkout@v6 (used by the
label-pull-requests job) requires it; change the overly-broad pull-requests:
write to pull-requests: read at the workflow-level, or alternatively leave
workflow-level contents: read and move pull-requests: write down to only the
job(s) that actually need write access (e.g., the job that labels PRs), using
job-scoped permissions for least privilege.

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.

35 changes: 34 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

@property
def name(self) -> str:
return "code_smells"
Expand Down Expand Up @@ -51,6 +61,29 @@ 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
Comment on lines +65 to +66

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

With enable_ai_triage=True, triage is skipped unless an orchestrator is manually passed in. Since Refactron initializes CodeSmellAnalyzer(self.config) without an orchestrator, this config flag currently has no effect in normal usage. Consider instantiating a default LLMOrchestrator when enable_ai_triage is enabled (or wiring one in from the main Refactron initialization path).

Copilot uses AI. Check for mistakes.
# evaluate_issues_batch returns Dict[str, float] mapping issue_id to confidence
confidence_scores = self.orchestrator.evaluate_issues_batch(issues, source_code)

Comment on lines +65 to +69

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

LLMOrchestrator currently does not define evaluate_issues_batch, so this call will raise AttributeError at runtime when AI triage is enabled. Either implement evaluate_issues_batch on LLMOrchestrator (and ensure it returns the dict shape expected here) or change this code to call an existing orchestrator API.

Copilot uses AI. Check for mistakes.
# 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}"
Comment on lines +66 to +75

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

The triage result map is keyed by rule_id when present, which can collide when multiple issues share the same rule (e.g., multiple S002 findings). That can incorrectly apply one confidence score to all occurrences. Consider using a unique per-issue identifier (e.g., include line/column or a generated ID) and ensure the orchestrator returns scores keyed by that identifier.

Suggested change
# 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}"
# Assign a unique, stable triage ID per issue to avoid key collisions
for i, issue in enumerate(issues):
# Use rule_id, file path, and line number to help uniqueness and traceability
rule_id = getattr(issue, "rule_id", "GENERIC")
line_number = getattr(issue, "line_number", 0) or 0
triage_id = f"{rule_id}:{file_path}:{line_number}:{i}"
# Ensure metadata exists and record the triage ID
if getattr(issue, "metadata", None) is None:
issue.metadata = {}
issue.metadata.setdefault("triage_id", triage_id)
# Batch evaluate all issues
# evaluate_issues_batch returns Dict[str, float] mapping triage_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):
# Use the unique triage_id; fall back to a per-index ID if missing
issue_id = issue.metadata.get("triage_id") if getattr(issue, "metadata", None) else None
if not issue_id:
issue_id = f"issue_{i}"

Copilot uses AI. Check for mistakes.

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

Don’t use rule_id as the confidence key.

rule_id identifies the rule (S004, S005, etc.), not the individual finding. Two missing-docstring issues in the same file would share one key and be kept or dropped together with the same score. Use a stable per-issue identifier, or have the batch API return scores aligned to input order.

🤖 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 code is
using issue.rule_id as the key into confidence_scores which groups different
findings of the same rule together; instead, construct and use a stable
per-issue identifier (or rely on an index-aligned API) when looking up
confidence. Change the lookup in the loop inside the analyzer (where issues are
enumerated) to compute a stable id such as getattr(issue, "id", None) or a
deterministic composite like
f"{getattr(issue,'path','')}-{getattr(issue,'line',0)}-{getattr(issue,'col',0)}-{getattr(issue,'rule_id','')}"
and use that key for confidence_scores.get(..., 1.0); alternatively update
evaluate_issues_batch to return scores in input order so you can use the
enumerate index i to index into the returned scores.


# Optional: Attach the confidence score to the issue metadata
# for reporting/debugging
issue.metadata["validation_confidence"] = confidence

if confidence >= 0.3:
final_issues.append(issue)
issues = final_issues
Comment on lines +64 to +85

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

Fail open when AI triage is unavailable.

evaluate_issues_batch() is an external/injected call, and right now any exception here will make analyze() raise after all findings were already collected. Please catch triage failures and fall back to the original issues.

Suggested fallback
         # 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)
+            try:
+                # 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
+                )
+            except Exception:
+                return issues
📝 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
# 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)
# Optional: Attach the confidence score to the issue metadata
# for reporting/debugging
issue.metadata["validation_confidence"] = confidence
if confidence >= 0.3:
final_issues.append(issue)
issues = final_issues
# AI Triage: Filter out safe/intentional smells
if self.config.enable_ai_triage and self.orchestrator and issues:
try:
# 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
)
except Exception:
return issues
# 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)
# Optional: Attach the confidence score to the issue metadata
# for reporting/debugging
issue.metadata["validation_confidence"] = confidence
if confidence >= 0.3:
final_issues.append(issue)
issues = final_issues
🤖 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 64 - 85, The AI
triage call evaluate_issues_batch in analyze() can raise exceptions and
currently will bubble up and fail the whole analysis; wrap the call and
subsequent processing in a try/except that catches any Exception (or specific
orchestration errors) and logs the failure, then fall back to leaving issues
unchanged (i.e., do not filter) when self.config.enable_ai_triage and
self.orchestrator are present; ensure you still attach no confidence metadata on
failure and preserve the original issues list, and reference
evaluate_issues_batch, analyze, self.config.enable_ai_triage, and
self.orchestrator when locating the change.


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
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

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n \
  --glob 'pyproject.toml' \
  --glob 'setup.cfg' \
  --glob 'setup.py' \
  --glob 'requirements*.txt' \
  --glob 'Pipfile' \
  --glob 'poetry.lock' \
  --glob 'uv.lock' \
  'types-PyYAML|PyYAML|ignore_missing_imports|import-untyped'

Repository: Refactron-ai/Refactron_lib

Length of output: 107


🏁 Script executed:

# Check what yaml APIs are actually used in the file
rg -n 'yaml\.' refactron/cli.py | head -20

Repository: Refactron-ai/Refactron_lib

Length of output: 157


🏁 Script executed:

# Check for mypy configuration
fd -e 'mypy.ini' -e '.mypy.ini' -e 'pyrightconfig.json' -e 'pyproject.toml' | xargs -I {} sh -c 'grep -l "mypy\|pyrightconfig\|type_check" {} 2>/dev/null || true'

Repository: Refactron-ai/Refactron_lib

Length of output: 52


🏁 Script executed:

# Check if there are any other yaml-related ignores or comments in the file
rg -n 'yaml|type.*ignore' refactron/cli.py

Repository: Refactron-ai/Refactron_lib

Length of output: 674


🏁 Script executed:

cat pyproject.toml | head -100

Repository: Refactron-ai/Refactron_lib

Length of output: 2703


🏁 Script executed:

cat pyproject.toml | tail -50

Repository: Refactron-ai/Refactron_lib

Length of output: 1108


Narrow or remove the blanket type: ignore on yaml.

types-PyYAML is already declared in dev dependencies, so the stubs are available. The blanket # type: ignore still suppresses any real type errors from PyYAML API misuse. Since the only usage (yaml.dump() on line 1800) is a standard API call, try removing this ignore entirely—or if mypy still complains, narrow it to # type: ignore[import-untyped].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/cli.py` at line 15, Remove the blanket "# type: ignore" on the
"import yaml" statement in refactron/cli.py and either delete it entirely
(preferred, since types-PyYAML is available) or replace it with a narrowed
ignore like "# type: ignore[import-untyped]" if mypy still complains; ensure
after the change that usages such as yaml.dump(...) (the call site in this
module) type-check cleanly and run the type checker to confirm no other PyYAML
API misuse warnings remain.

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

Comment on lines +133 to +135

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

enable_ai_triage also needs validator coverage.

RefactronConfig.from_file() will accept any YAML scalar here unless refactron/core/config_validator.py is updated too. Because dataclasses do not enforce runtime types, a quoted value like "false" stays truthy and can unexpectedly enable LLM triage at runtime.

Suggested follow-up
# refactron/core/config_validator.py
         boolean_fields = {
             "show_details",
             "require_preview",
             "backup_enabled",
             "enable_ast_cache",
             "enable_incremental_analysis",
             "enable_parallel_processing",
             "use_multiprocessing",
             "enable_memory_profiling",
             "enable_console_logging",
             "enable_file_logging",
             "enable_metrics",
             "metrics_detailed",
             "enable_prometheus",
             "enable_telemetry",
+            "enable_ai_triage",
             "enable_pattern_learning",
             "pattern_learning_enabled",
             "pattern_ranking_enabled",
         }
📝 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
# AI Triage settings
enable_ai_triage: bool = False # Use LLM to filter false positive code smells
boolean_fields = {
"show_details",
"require_preview",
"backup_enabled",
"enable_ast_cache",
"enable_incremental_analysis",
"enable_parallel_processing",
"use_multiprocessing",
"enable_memory_profiling",
"enable_console_logging",
"enable_file_logging",
"enable_metrics",
"metrics_detailed",
"enable_prometheus",
"enable_telemetry",
"enable_ai_triage",
"enable_pattern_learning",
"pattern_learning_enabled",
"pattern_ranking_enabled",
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/core/config.py` around lines 133 - 135, Refactor the config
validation to cover the enable_ai_triage field so RefactronConfig.from_file()
cannot accept quoted/string scalars like "false"; update
refactron/core/config_validator.py to add a validator/schema entry for
enable_ai_triage that enforces a boolean type (or coerces common YAML
boolean-like strings to actual bools and rejects invalid values) and wire it
into the existing validation flow used by RefactronConfig.from_file() so the
dataclass field enable_ai_triage is guaranteed to be a real bool at runtime.

@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"])

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 +266 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

Resolve the conflicting implementations in evaluate_issues_batch.

The unconditional return scores on Line 272 makes Lines 273-360 unreachable, so this method never executes the batch-triage prompt/LLM path and every issue is forced to 1.0. If the placeholder is intentional, the dead implementation below should be removed; otherwise the fallback needs to move into the empty/error path. Also, keying the fallback only by rule_id can collapse multiple issues from the same rule into one entry.

Also applies to: 273-360

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/llm/orchestrator.py` around lines 266 - 272, In
evaluate_issues_batch, remove the unconditional early return of scores so the
LLM/batch-triage path can run, or if the simple 1.0 placeholder is intended keep
it only as the fallback inside the empty/error branch; update the fallback key
logic so it cannot collapse multiple issues with the same rule_id (e.g., build
keys using getattr(issue,"id",None) or combine rule_id with the loop index or
issue-specific uid when setting scores[...]). Specifically, edit
evaluate_issues_batch to either (A) delete the lines that return the default
scores immediately and let the subsequent prompt/LLM code run, or (B) wrap the
LLM call in try/except and move the default scoring into the except/empty-input
path, and change issue_id construction from just rule_id to a unique identifier
like f"{rule_id or 'issue'}_{i}" or use issue.id/uuid when present.

"""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
Comment on lines +8 to 9

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

Remove the duplicated typing import.

Lines 8-9 import the same symbols twice, which matches the isort failure reported in CI. Keep a single from typing import Any, Dict, List, Optional, cast here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/rag/indexer.py` around lines 8 - 9, Remove the duplicated typing
import: there are two identical import lines importing Any, Dict, List,
Optional, cast; keep only a single line reading from typing import Any, Dict,
List, Optional, cast and delete the duplicate so symbols like Any, Dict, List,
Optional, cast are imported exactly once (adjust in the top of the module where
the imports occur, e.g., the duplicated from typing import ... lines in
indexer.py).


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

Keep chunk.content as raw source.

add_chunks() stores chunk.content as the Chroma document, refactron/rag/retriever.py:106-125 returns that document verbatim in RetrievedContext.content, and refactron/cli.py renders result.content as code. Prepending Summary: ... here means downstream consumers now see malformed source snippets instead of the original chunk. Store the summary in metadata or a separate embedding-only field instead of mutating the chunk body.

🤖 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 is mutating
chunk.content by prepending "Summary: ..." which corrupts the source stored by
add_chunks() and later returned by retriever.RetrievedContext.content; instead,
stop changing chunk.content and store the summary in metadata (e.g.,
chunk.metadata["ai_summary"]) or an embedding-only field. Update the block in
indexer.py that currently sets chunk.content = f"Summary:
{summary}\n\n{chunk.content}" to only set chunk.metadata["ai_summary"] = summary
(or add a new metadata key like "embedding_summary") and ensure add_chunks() and
downstream consumers continue to receive the original chunk.content unchanged.
Ensure any references in retriever or cli that expect the summary read it from
chunk.metadata instead of the content.

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))
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

🧩 Analysis chain

🏁 Script executed:

head -n 310 refactron/rag/indexer.py | tail -n 30

Repository: Refactron-ai/Refactron_lib

Length of output: 1109


Restore a single _load_metadata definition.

The duplicate function header on lines 289-290 leaves the first definition without a body, causing an IndentationError. The second header uses the more specific type annotation Dict[str, Any] and should be kept along with the corresponding return statement on line 298. Remove the first header on line 289 and the first return statement on line 297.

Suggested fix
-    def _load_metadata(self) -> Dict:
-    def _load_metadata(self) -> Dict[str, Any]:
+    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))
📝 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:
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))
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[str, Any], json.load(f))
🧰 Tools
🪛 GitHub Actions: Pre-commit

[error] 290-290: Black: Cannot format due to parse error. IndentationError: expected an indented block at line 290.


[error] 289-289: Flake8: IndentationError: expected an indented block after function definition on line 288.


[error] 289-289: Mypy: Syntax error due to indentation issue at line 289.


[error] 289-289: Flake8: IndentationError: expected an indented block after function definition on line 288.

🪛 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 stray duplicate
function header for _load_metadata (the first "def _load_metadata(self) ->
Dict:") so only the correctly typed signature "def _load_metadata(self) ->
Dict[str, Any]:" remains; also delete the earlier redundant return statement
that returns cast(Dict, json.load(f)) and keep the second return cast(Dict[str,
Any], json.load(f)) within the with-open block so metadata_file and
self.index_path logic stays intact and the function has a single body.

39 changes: 33 additions & 6 deletions refactron/rag/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,50 @@ def __init__(self) -> None:
"""Initialize the parser."""
if not TREE_SITTER_AVAILABLE:
raise RuntimeError(
"tree-sitter is not available. Install with: "
"pip install tree-sitter tree-sitter-python"
"tree-sitter is not available. "
"Install with: pip install tree-sitter tree-sitter-python"
)

# Initialize Python language - handle different tree-sitter API versions
lang = tspython.language()
lang_data = tspython.language()

# In some versions, tspython.language() already returns a Language object
if isinstance(lang, Language):
PY_LANGUAGE = lang
# Try to get a proper Language object
py_language = None
if isinstance(lang_data, Language):
py_language = lang_data
else:
# Try newer API first (single argument)
try:
PY_LANGUAGE = Language(lang)
except TypeError:
py_language = Language(lang_data)
except (TypeError, ValueError):
# Try older API (needs name)
try:
py_language = Language(lang_data, "python")
except (TypeError, ValueError):
try:
py_language = Language(lang_data, name="python")
except (TypeError, ValueError):
# Fallback to using the raw data if it can be used directly
py_language = lang_data

# Initialize Parser - handle different tree-sitter API versions
try:
self.parser = Parser(py_language)
except Exception:
# Older API might not take it in constructor
self.parser = Parser()
try:
self.parser.set_language(py_language)
except Exception:
# If everything fails, it might be due to a mismatch between
# tree-sitter-python and tree-sitter versions.
# There's not much more we can do here but raise with context.
raise RuntimeError(
"Failed to initialize tree-sitter parser with language "
f"data of type {type(lang_data)}"
)
Comment on lines +87 to +101

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

🧩 Analysis chain

🏁 Script executed:

cat -n refactron/rag/parser.py | sed -n '85,140p'

Repository: Refactron-ai/Refactron_lib

Length of output: 2859


Remove the stale PY_LANGUAGE initialization block (lines 102-135).

The old initialization code is orphaned after the raise RuntimeError statement. This creates a syntax error—the except TypeError at line 103 has no matching try statement. Remove lines 102-135 entirely; the new fallback logic at lines 87-101 properly handles all initialization cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@refactron/rag/parser.py` around lines 87 - 101, Remove the stale orphaned
PY_LANGUAGE initialization block (the leftover except TypeError / PY_LANGUAGE
branch) that appears after the RuntimeError raise so the only initialization
flow is the existing try/except that attempts Parser(py_language) then falls
back to Parser() + set_language(py_language); specifically delete the code
referencing PY_LANGUAGE and the unmatched except TypeError, leaving the Parser,
py_language and lang_data handling as-is.

PY_LANGUAGE = Language(lang, "python")
except TypeError:
# Try using the path to the compiled library (for very old or CI bindings)
Expand Down
4 changes: 3 additions & 1 deletion refactron/rag/retriever.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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 @@ -50,6 +50,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
2 changes: 2 additions & 0 deletions scripts/analyze_feedback_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sys
from collections import Counter
from pathlib import Path
from typing import Dict, Optional
from typing import Any, Dict, Optional

# Add parent directory to path for imports
Expand All @@ -20,6 +21,7 @@
from refactron.patterns.storage import PatternStorage # noqa: E402


def analyze_feedback() -> Optional[Dict]:
def analyze_feedback() -> Optional[Dict[str, Any]]:
"""Analyze all available feedback data."""

Expand Down
Loading
Loading