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
55 changes: 55 additions & 0 deletions docs/essentials/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,61 @@ export REFACTRON_LOG_LEVEL=DEBUG
export REFACTRON_CONFIG=/path/to/.refactron.yaml
```

## Verification Pipeline

The verification pipeline (`refactron verify`, and `refactron autofix --verify`)
runs safety checks before a change is applied. Its behavior is configurable
through a `verification:` section so teams can trade safety for speed based on
repo size, CI tier, or policy.

```yaml
verification:
# Checks to run, in execution order. Any subset/ordering of:
# syntax, import_integrity, test_gate
enabled_checks: [syntax, import_integrity, test_gate]

# Stop after the first failing check (true) or run them all (false).
short_circuit: true

# Subprocess timeout for the pytest-based test gate, in seconds.
test_gate_timeout_sec: 45

# Extra arguments appended to the pytest command line.
pytest_extra_args: []
```

### Defaults

| Key | Default | Meaning |
|-----|---------|---------|
| `enabled_checks` | `[syntax, import_integrity, test_gate]` | All three checks, in this order |
| `short_circuit` | `true` | Stop on first failure (fastest) |
| `test_gate_timeout_sec` | `45` | pytest subprocess timeout |
| `pytest_extra_args` | `[]` | No extra pytest arguments |

### Common recipes

```yaml
# Fast PR check — skip the (slow) test gate
verification:
enabled_checks: [syntax, import_integrity]

# Slow CI tier — give the test gate more time
verification:
test_gate_timeout_sec: 300

# Full failure report instead of stop-on-first-failure
verification:
short_circuit: false
```

### Migration

The `verification:` section is **optional**. Existing config files without it
keep the exact behavior of earlier releases — the defaults above reproduce the
previously hard-coded values (all checks, 45s test-gate timeout, short-circuit
on first failure). No changes are required to upgrade.

## Per-File Ignores

Ignore specific issues in code using comments:
Expand Down
27 changes: 18 additions & 9 deletions refactron/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,18 @@ def main(ctx: click.Context) -> None:


# Register subcommands
# We import them here to ensure they are registered with the main group
# Using a try-except block to allow partial loading during refactoring
# We import them here to ensure they are registered with the main group.
# Each block is isolated so a single broken subcommand — or a broken optional
# dependency it pulls in (e.g. a failed native-library load raising OSError,
# not ImportError) — degrades gracefully instead of taking down the whole CLI.
try:
from refactron.cli.auth import auth, login, logout, telemetry

main.add_command(login)
main.add_command(logout)
main.add_command(auth)
main.add_command(telemetry)
except ImportError:
except Exception:
pass

try:
Expand All @@ -111,7 +113,7 @@ def main(ctx: click.Context) -> None:
main.add_command(metrics)
main.add_command(serve_metrics)
main.add_command(suggest)
except ImportError:
except Exception:
pass

try:
Expand All @@ -121,28 +123,35 @@ def main(ctx: click.Context) -> None:
main.add_command(autofix)
main.add_command(rollback)
main.add_command(document)
except ImportError:
except Exception:
pass

try:
from refactron.cli.verify import verify

main.add_command(verify)
except Exception:
pass

try:
from refactron.cli.patterns import patterns

main.add_command(patterns)
except ImportError:
except Exception:
pass

try:
from refactron.cli.repo import repo

main.add_command(repo)
except ImportError:
except Exception:
pass

try:
from refactron.cli.rag import rag

main.add_command(rag)
except ImportError:
except Exception:
pass

try:
Expand All @@ -151,5 +160,5 @@ def main(ctx: click.Context) -> None:
main.add_command(generate_cicd)
main.add_command(feedback)
main.add_command(init)
except ImportError:
except Exception:
pass
107 changes: 107 additions & 0 deletions refactron/cli/verify.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,110 @@
"""
Refactron CLI - Verification Module.
Command to run the Verification Engine on a code change and emit either a
human-readable report or a stable, machine-readable JSON object for CI gates.
"""

from pathlib import Path
from typing import Optional

import click

from refactron.cli.ui import _auth_banner, console
from refactron.verification.engine import VerificationEngine
from refactron.verification.report import (
format_verification_result,
format_verification_result_json,
)


@click.command()
@click.argument("target", type=click.Path(exists=True, dir_okay=False))
@click.option(
"--against",
"-a",
"candidate",
type=click.Path(exists=True, dir_okay=False),
default=None,
help=(
"Path to the proposed/modified version of TARGET. "
"If omitted, TARGET is verified against itself."
),
)
@click.option(
"--project-root",
type=click.Path(exists=True, file_okay=False),
default=None,
help="Project root used by the test-suite gate. Defaults to the current directory.",
)
@click.option(
"--config",
"-c",
type=click.Path(exists=True, dir_okay=False),
default=None,
help="Refactron config file; its `verification:` section tunes the pipeline "
"(enabled checks, ordering, test-gate timeout, pytest args).",
)
@click.option(
"--all-checks",
is_flag=True,
default=False,
help=(
"Run every check even after one fails (no short-circuit), so all "
"failure categories surface in a single run. Overrides config."
),
)
@click.option(
"--json",
"as_json",
is_flag=True,
default=False,
help="Emit a stable, machine-readable JSON report instead of formatted text.",
)
def verify(
target: str,
candidate: Optional[str],
project_root: Optional[str],
config: Optional[str],
all_checks: bool,
as_json: bool,
) -> None:
"""
Verify that a code change is safe to apply.

TARGET is the file as it currently lives in the project. With --against,
the proposed new content is checked against it (syntax, import integrity,
test suite). Exit code is 0 when safe to apply, 1 when blocked — and with
--json a versioned JSON object is printed for CI dashboards and bots.
"""
target_path = Path(target)
original = target_path.read_text(encoding="utf-8")
transformed = Path(candidate).read_text(encoding="utf-8") if candidate else original

# A config file's `verification:` section tunes the pipeline; without one
# the engine falls back to its built-in defaults.
verification_cfg = None
if config:
from refactron.core.config import RefactronConfig

verification_cfg = RefactronConfig.from_file(Path(config)).verification

root = Path(project_root) if project_root else Path.cwd()
engine = VerificationEngine(project_root=root, config=verification_cfg)
# --all-checks forces a full run; otherwise honour the configured value.
result = engine.verify(
original,
transformed,
target_path,
short_circuit=False if all_checks else None,
)

if as_json:
# JSON mode prints only the JSON object so consumers can parse stdout.
click.echo(format_verification_result_json(result))
else:
console.print()
_auth_banner("Verification")
format_verification_result(result, console)
"""refactron verify <file> --against <original> — standalone verification command."""

from pathlib import Path
Expand Down
57 changes: 57 additions & 0 deletions refactron/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@
from refactron.core.exceptions import ConfigError


# Valid verification check names, also the default execution order.
DEFAULT_VERIFICATION_CHECKS = ["syntax", "import_integrity", "test_gate"]


@dataclass
class VerificationConfig:
"""Settings for the verification pipeline (`refactron verify`, `--verify`).

Every default reproduces the historical hard-coded behaviour, so existing
projects need no migration: a config file with no ``verification:`` section
behaves exactly as before.

Fields:
enabled_checks: Checks to run, in execution order. Any subset (and any
ordering) of ``syntax``, ``import_integrity``, ``test_gate``. For
fast PR checks a team might use ``[syntax, import_integrity]`` only.
short_circuit: Stop after the first failing check (fast) when True;
run every check for a complete failure report when False.
test_gate_timeout_sec: Subprocess timeout for the pytest-based test
gate, in seconds.
pytest_extra_args: Extra arguments appended to the pytest command line
(e.g. ``["-p", "no:cacheprovider"]``).
"""

enabled_checks: List[str] = field(
default_factory=lambda: list(DEFAULT_VERIFICATION_CHECKS)
)
short_circuit: bool = True
test_gate_timeout_sec: int = 45
pytest_extra_args: List[str] = field(default_factory=list)


@dataclass
class RefactronConfig:
"""Configuration for Refactron analysis and refactoring."""
Expand Down Expand Up @@ -130,6 +162,9 @@ class RefactronConfig:
pattern_learning_enabled: bool = True # Enable learning from feedback
pattern_ranking_enabled: bool = True # Enable ranking based on learned patterns

# Verification pipeline settings (refactron verify / autofix --verify)
verification: VerificationConfig = field(default_factory=VerificationConfig)

@classmethod
def from_file(
cls,
Expand Down Expand Up @@ -168,6 +203,22 @@ def from_file(
):
config_dict[path_field] = Path(config_dict[path_field])

# Convert the nested `verification:` mapping into its dataclass.
if isinstance(config_dict.get("verification"), dict):
try:
config_dict["verification"] = VerificationConfig(
**config_dict["verification"]
)
except TypeError as e:
raise ConfigError(
f"Invalid 'verification' configuration: {e}",
config_path=config_path,
recovery_suggestion=(
"Valid keys: enabled_checks, short_circuit, "
"test_gate_timeout_sec, pytest_extra_args."
),
) from e

try:
return cls(**config_dict)
except TypeError as e:
Expand Down Expand Up @@ -243,6 +294,12 @@ def to_file(self, config_path: Path) -> None:
),
"pattern_learning_enabled": self.pattern_learning_enabled,
"pattern_ranking_enabled": self.pattern_ranking_enabled,
"verification": {
"enabled_checks": self.verification.enabled_checks,
"short_circuit": self.verification.short_circuit,
"test_gate_timeout_sec": self.verification.test_gate_timeout_sec,
"pytest_extra_args": self.verification.pytest_extra_args,
},
}

try:
Expand Down
Loading
Loading