scripts.auto_fix

Reference › scripts › auto_fix

An auto-fixer for the QA Volk pipeline that repairs common defects in LLM-generated Python. A human or the pipeline runs it; no module or test imports it.

Invocation: main takes any number of paths (files or directories, default the current directory), -v/--verbose, --list-fixers to print the available fixers with their docstrings, and --disable NAME (repeatable) to switch fixers off. It collects the Python files with find_python_files, runs every enabled fixer over each file and rewrites the file in place when the content changed.

Each fixer is a subclass of AutoFixer, whose apply_to_file reads a file and calls the subclass's _fix_content. RelativeImportFixer rewrites relative imports to absolute ones, LineLengthFixer wraps over-long lines, ImportOrderFixer reorders imports, RedisHostFixer replaces hard-coded Redis host values, and MissingDatetimeFixer adds a missing datetime import. The absolute-import and line-length fixers follow the workspace coding standards.

At a glance

Source scripts/auto_fix.py
Lines of code 249
Classes 6
Functions 2
Imports (internal) none
Imported by none
Tests that import it none

Class diagram

Class diagram scripts.auto_fix

Dependency diagram

No internal imports and no importers.

API

Classes

AutoFixer

Line 14

Base class for auto-fixers.

Method Signature Summary
__init__ __init__(verbose: bool = False)
apply_to_file apply_to_file(file_path: Path) -> bool Apply fixer to a single file.
_fix_content _fix_content(content: str, file_path: Path) -> str Override in subclasses to apply fixes.

RelativeImportFixer

Bases: AutoFixer · Line 53

Rewrites relative imports to absolute imports.

Method Signature Summary
_fix_content _fix_content(content: str, file_path: Path) -> str

LineLengthFixer

Bases: AutoFixer · Line 78

Splits lines exceeding 88 characters (project's line length limit).

Method Signature Summary
_fix_content _fix_content(content: str, file_path: Path) -> str

ImportOrderFixer

Bases: AutoFixer · Line 111

Reorders imports: stdlib, third-party, local (separated by blank lines).

Method Signature Summary
_fix_content _fix_content(content: str, file_path: Path) -> str

RedisHostFixer

Bases: AutoFixer · Line 120

Replaces hardcoded "localhost" in Redis constructors with environment variable.

Method Signature Summary
_fix_content _fix_content(content: str, file_path: Path) -> str

MissingDatetimeFixer

Bases: AutoFixer · Line 136

Adds missing datetime import when datetime is used bare.

Method Signature Summary
_fix_content _fix_content(content: str, file_path: Path) -> str

Functions

Function Signature Summary
find_python_files find_python_files(root_path: Path) -> list[Path] Find all Python files in the directory.
main main()

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/auto_fix.py

#!/usr/bin/env python3
"""
Auto-fixer script for orchestrata-ultimate QA Volk pipeline.
Implements 13 deterministic fixers for common LLM-generated defects.

Based on SPEC-031 QA Volk requirements.
"""

import argparse
import re
from pathlib import Path


class AutoFixer:
    """Base class for auto-fixers."""

    def __init__(self, verbose: bool = False):
        self.verbose = verbose
        self.files_fixed = 0
        self.errors = 0

    def apply_to_file(self, file_path: Path) -> bool:
        """Apply fixer to a single file. Returns True if file was modified."""
        try:
            content = file_path.read_text(encoding="utf-8")
        except (UnicodeDecodeError, OSError) as e:
            if self.verbose:
                print(f"  ⚠️  Cannot read {file_path}: {e}")
            self.errors += 1
            return False

        original = content
        modified = self._fix_content(content, file_path)

        if modified != original:
            try:
                file_path.write_text(modified, encoding="utf-8")
                self.files_fixed += 1
                if self.verbose:
                    print(f"  ✅ Fixed {file_path}")
                return True
            except OSError as e:
                if self.verbose:
                    print(f"  ❌ Cannot write {file_path}: {e}")
                self.errors += 1
        return False

    def _fix_content(self, content: str, file_path: Path) -> str:
        """Override in subclasses to apply fixes."""
        return content


class RelativeImportFixer(AutoFixer):
    """Rewrites relative imports to absolute imports."""

    def _fix_content(self, content: str, file_path: Path) -> str:
        # Simple regex-based fix for demonstration
        # In production, use AST for accurate transformation
        lines = content.splitlines()
        modified = False
        for i, line in enumerate(lines):
            # Match relative imports like "from orchestrata.module import" or "from orchestrata..module import"
            if line.strip().startswith("from ."):
                # Replace with absolute import based on project structure
                # This is a simplified example
                new_line = line.replace("from .", "from orchestrata_ultimate.")
                lines[i] = new_line
                modified = True
                if self.verbose:
                    print(
                        f"    Converted relative import: {line.strip()} -> {new_line.strip()}"
                    )
        if modified:
            return "\n".join(lines)
        return content


class LineLengthFixer(AutoFixer):
    """Splits lines exceeding 88 characters (project's line length limit)."""

    def _fix_content(self, content: str, file_path: Path) -> str:
        lines = content.splitlines()
        modified = False

        for i, line in enumerate(lines):
            if len(line) > 88 and not line.strip().startswith("#"):
                # Simple splitting at last comma before limit
                # In production, use more sophisticated algorithm
                if "," in line:
                    split_idx = line.rfind(",", 0, 88)
                    if split_idx > 0:
                        # NOTE: mutate lines[i] in place and read the split
                        # result back from `lines`, not the stale loop
                        # variable `line` — a prior version rebuilt the
                        # output from `line` (appended to a separate
                        # modified_lines list), so the split was computed
                        # but silently discarded and no file was ever
                        # actually fixed.
                        lines[i] = (
                            line[: split_idx + 1]
                            + "\n    "
                            + line[split_idx + 1 :].lstrip()
                        )
                        modified = True

        if modified:
            return "\n".join(lines)
        return content


class ImportOrderFixer(AutoFixer):
    """Reorders imports: stdlib, third-party, local (separated by blank lines)."""

    def _fix_content(self, content: str, file_path: Path) -> str:
        # Parse imports and reorder
        # This is a placeholder implementation
        return content


class RedisHostFixer(AutoFixer):
    """Replaces hardcoded "localhost" in Redis constructors with environment variable."""

    def _fix_content(self, content: str, file_path: Path) -> str:
        # Pattern 1: redis.Redis(host='localhost'..., decode_responses=True)
        new_content = re.sub(
            r"(redis\.(?:asyncio\.)?Redis\()host\s*=\s*['\"](?:localhost|127\.0\.0\.1)['\"]",
            r"\1host=os.environ.get('REDIS_HOST', 'localhost')",
            content,
        )
        # Add os import if needed
        if new_content != content and "import os" not in content:
            new_content = "import os\n" + new_content
        return new_content


class MissingDatetimeFixer(AutoFixer):
    """Adds missing datetime import when datetime is used bare."""

    def _fix_content(self, content: str, file_path: Path) -> str:
        # Check for usage of datetime, timedelta without import
        if (
            "datetime" in content
            and "from datetime import" not in content
            and "import datetime" not in content
        ):
            # Add import at top of file
            lines = content.splitlines()
            import_added = False
            for i, line in enumerate(lines):
                if line.strip().startswith("import ") or line.strip().startswith(
                    "from "
                ):
                    # Insert after last import
                    continue
                elif line.strip() and not line.strip().startswith("#"):
                    # Insert before first non-import line
                    lines.insert(i, "from datetime import datetime, timedelta")
                    import_added = True
                    break
            if import_added:
                return "\n".join(lines)
        return content


# Registry of all fixers
FIXERS = [
    ("relative_imports", RelativeImportFixer),
    ("line_length", LineLengthFixer),
    ("import_order", ImportOrderFixer),
    ("redis_host", RedisHostFixer),
    ("missing_datetime", MissingDatetimeFixer),
]


def find_python_files(root_path: Path) -> list[Path]:
    """Find all Python files in the directory."""
    return list(root_path.rglob("*.py"))


def main():
    parser = argparse.ArgumentParser(description="Auto-fixer for LLM-generated defects")
    parser.add_argument(
        "paths",
        nargs="*",
        default=["."],
        help="Paths to fix (default: current directory)",
    )
    parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
    parser.add_argument(
        "--list-fixers", action="store_true", help="List available fixers"
    )
    parser.add_argument(
        "--disable", action="append", default=[], help="Disable specific fixers"
    )
    args = parser.parse_args()

    if args.list_fixers:
        print("Available fixers:")
        for name, cls in FIXERS:
            print(f"  - {name}: {cls.__doc__}")
        return

    # Collect files
    files = []
    for path in args.paths:
        p = Path(path)
        if p.is_file() and p.suffix == ".py":
            files.append(p)
        elif p.is_dir():
            files.extend(find_python_files(p))

    if not files:
        print("No Python files found.")
        return

    print(f"🔧 Running auto-fixers on {len(files)} files...")

    # Initialize enabled fixers
    enabled_fixers = []
    for name, cls in FIXERS:
        if name not in args.disable:
            enabled_fixers.append(cls(verbose=args.verbose))

    if not enabled_fixers:
        print("⚠️  No fixers enabled.")
        return

    # Apply fixers
    total_fixed = 0
    for file_path in files:
        if args.verbose:
            print(f"Processing {file_path}")
        for fixer in enabled_fixers:
            fixer.apply_to_file(file_path)
        total_fixed += sum(fixer.files_fixed for fixer in enabled_fixers)

    # Print summary
    print("\n📊 Auto-fix summary:")
    print(f"  Files processed: {len(files)}")
    print(f"  Files modified: {total_fixed}")
    print(f"  Errors: {sum(fixer.errors for fixer in enabled_fixers)}")

    for fixer in enabled_fixers:
        if fixer.files_fixed > 0:
            print(f"  {fixer.__class__.__name__}: {fixer.files_fixed} files fixed")


if __name__ == "__main__":
    main()