Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e2973eb
feat(cqrs): introduce CQRS + event sourcing for the scan/fix pipeline
tom-sapletta-com Sep 17, 2026
6c33d69
feat(quality): bootstrap regix regression metrics gate (PLF-025)
tom-sapletta-com Sep 18, 2026
996eef0
fix(quality): exclude example fixtures from code2llm analysis (PLF-028)
tom-sapletta-com Sep 18, 2026
070f049
refactor(examples): single engine construction point in API-usage dem…
tom-sapletta-com Sep 19, 2026
6ca9876
refactor(examples): inline single-use message temp in string-concat d…
tom-sapletta-com Sep 19, 2026
3ccf96e
refactor(git-hooks): centralize hook status detection
tom-sapletta-com Sep 19, 2026
287194c
refactor(examples): rename shared user locals in sample-project fixtu…
tom-sapletta-com Sep 19, 2026
de64c03
refactor(cli): reuse package-shared console in autonomous and rules c…
tom-sapletta-com Sep 19, 2026
6faeb59
refactor(logging): extract shared error-context handling from error a…
tom-sapletta-com Sep 19, 2026
9fc3e56
refactor(cache): bundle rule cache identity into RuleCacheKey (PLF-034)
tom-sapletta-com Sep 19, 2026
ce765c3
refactor(config): consolidate duplicate ExtendedConfig into config.py…
tom-sapletta-com Sep 19, 2026
b04b30e
refactor(cache): bundle scan cache identity into ScanCacheKey (PLF-038)
tom-sapletta-com Sep 19, 2026
65573ab
refactor(performance): bundle parallel work order into FileBatch (PLF…
tom-sapletta-com Sep 19, 2026
014c07b
refactor(cqrs): bundle event bus registration into Subscription (PLF-…
tom-sapletta-com Sep 19, 2026
1e3fb98
Merge remote-tracking branch 'origin/main' into ticket/001-cqrs-event…
tom-sapletta-com Sep 19, 2026
4abcd22
Merge remote-tracking branch 'origin/main' into ticket/001-cqrs-event…
tom-sapletta-com Sep 19, 2026
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: 1 addition & 2 deletions examples/01-individual-rules/string-concat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ def format_data(data):
**After:**
```python
def greet(name, age):
message = f"Hello {name}, you are {age} years old"
return message
return f"Hello {name}, you are {age} years old"

def format_data(data):
result = f"Data: {data}"
Expand Down
3 changes: 1 addition & 2 deletions examples/01-individual-rules/string-concat/after.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

def greet(name, age):
"""Greet someone."""
message = f"Hello {name}, you are {age} years old"
return message
return f"Hello {name}, you are {age} years old"


def format_data(data):
Expand Down
46 changes: 27 additions & 19 deletions examples/06-api-usage/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

from prefact.config import Config
from prefact.engine import RefactoringEngine
from prefact.models import PipelineResult


def run_engine(config: Config) -> PipelineResult:
"""Create the engine for a config and run it."""
engine = RefactoringEngine(config)
return engine.run()


def run_prefact_example(
Expand Down Expand Up @@ -33,35 +40,38 @@ def run_prefact_example(
print(f"Dry run: {dry_run}")
print("-" * 50)

engine = RefactoringEngine(config)
result = engine.run()
result = run_engine(config)

# Display results
print("\n📊 Results:")
print(f" Files scanned: {result.files_scanned}")
print(f" Total issues: {result.total_issues}")
print(f" Issues fixed: {result.total_fixed}")
print(f" Validation passed: {result.all_valid}")

# Show issues by rule
if result.issues_by_rule:
issues_by_rule = {}
for issue in result.issues_found:
issues_by_rule.setdefault(issue.rule_id, []).append(issue)

if issues_by_rule:
print("\n📋 Issues by rule:")
for rule_id, issues in result.issues_by_rule.items():
for rule_id, issues in issues_by_rule.items():
print(f" {rule_id}: {len(issues)} issues")

# Show fix details
if result.fixes:
if result.fixes_applied:
print("\n🔧 Fixes applied:")
for fix in result.fixes[:5]: # Show first 5
print(f" {fix.path}:{fix.line} - {fix.description}")
if len(result.fixes) > 5:
print(f" ... and {len(result.fixes) - 5} more")
for fix in result.fixes_applied[:5]: # Show first 5
print(f" {fix.file}:{fix.issue.line} - {fix.issue.message}")
if len(result.fixes_applied) > 5:
print(f" ... and {len(result.fixes_applied) - 5} more")

# Show validation failures
if result.validation_failures:
failed_validations = [v for v in result.validations if not v.passed]
if failed_validations:
print("\n❌ Validation failures:")
for failure in result.validation_failures:
print(f" {failure.path}: {failure.message}")
for failure in failed_validations:
print(f" {failure.file}: {', '.join(failure.errors)}")

return result

Expand Down Expand Up @@ -104,12 +114,11 @@ def another_function():
return

# Run with custom rules
engine = RefactoringEngine(config)
result = engine.run()
result = run_engine(config)

print("\nCustom rule results:")
print(
f" TODO comments found: {len([i for i in result.all_issues if 'todo' in i.rule_id])}"
f" TODO comments found: {len([i for i in result.issues_found if 'todo' in i.rule_id])}"
)

# Cleanup
Expand Down Expand Up @@ -140,14 +149,13 @@ def batch_processing_example():
config.project_root = project.resolve()
config.dry_run = True # Don't actually fix

engine = RefactoringEngine(config)
result = engine.run()
result = run_engine(config)

results.append(
{
"project": project.name,
"issues": result.total_issues,
"fixable": len([i for i in result.all_issues if i.fixable]),
"fixable": len([i for i in result.issues_found if i.suggested]),
}
)
else:
Expand Down
3 changes: 1 addition & 2 deletions examples/generate_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ def format_data(data):

def greet(name, age):
"""Greet someone."""
message = f"Hello {name}, you are {age} years old"
return message
return f"Hello {name}, you are {age} years old"

def format_data(data):
"""Format data."""
Expand Down
12 changes: 6 additions & 6 deletions examples/sample-project/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ def main(name, email):
sys.exit(1)

# Create user
user = User(id="", name=name, email=email, created_at=datetime.now())
new_user = User(id="", name=name, email=email, created_at=datetime.now())

print("Created user: " + user.name)
print("Created user: " + new_user.name)

# Process some data
processor = DataProcessor()
processor.add_item(user.name)
processor.add_item(user.email)
processor.add_item(new_user.name)
processor.add_item(new_user.email)

result = process_data("test data")
print("Processing result: " + str(result))
Expand All @@ -54,8 +54,8 @@ def users():
User("2", "Bob", "bob@example.com", datetime.now()),
]

for user in users:
print("User: " + user.name + " (" + user.email + ")")
for entry in users:
print("User: " + entry.name + " (" + entry.email + ")")


if __name__ == "__main__":
Expand Down
8 changes: 4 additions & 4 deletions examples/sample-project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ def get_summary(self):

def create_user(name, email):
"""Create a new user."""
user = User(id="", name=name, email=email, created_at=datetime.now())
return user
new_user = User(id="", name=name, email=email, created_at=datetime.now())
return new_user


def load_users_from_file(filepath):
Expand All @@ -54,6 +54,6 @@ def load_users_from_file(filepath):
data = json.load(f)
users = []
for item in data:
user = User(**item)
users.append(user)
loaded = User(**item)
users.append(loaded)
return users
3 changes: 3 additions & 0 deletions regix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ regix:
coverage: pytest-cov
quality: none
docstring: builtin
# mi_granularity: module # or "function" — score each function's own
# # span so helper extraction (lower CC, more LOC)
# # stops dropping module MI below the gate

# ── File filtering ─────────────────────────────────────────
exclude:
Expand Down
7 changes: 1 addition & 6 deletions src/prefact/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import click

from prefact._base import console
from prefact.autonomous import AutonomousRefact
from prefact.config import Config
from prefact.config_extended import ExtendedConfig
Expand Down Expand Up @@ -337,10 +338,6 @@ def autonomous_cmd(
Automatically initializes prefact.yaml if missing, runs examples,
scans for issues, and creates tickets in planfile.yaml.
"""
from rich.console import Console

console = Console()

# Initialize autonomous prefact
auto = AutonomousRefact(
Path(project_path), exclude_patterns=list(exclude) if exclude else None
Expand Down Expand Up @@ -439,12 +436,10 @@ def testql_cmd(
@main.command()
def rules() -> None:
"""List all available rules."""
from rich.console import Console
from rich.table import Table

from prefact.rules import get_all_rules

console = Console()
table = Table(title="Available Rules")
table.add_column("Rule ID", style="bold")
table.add_column("Description")
Expand Down
142 changes: 142 additions & 0 deletions src/prefact/config_extended/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Extended configuration model and helpers."""

from pathlib import Path
from typing import Any, Dict, List, Optional

import yaml

from prefact.config import Config, RuleConfig

from .constants import DEFAULT_EXCLUDE, DEFAULT_INCLUDE


class ExtendedConfig(Config):
"""Extended configuration with additional features."""

def __init__(
self,
project_root: Optional[Path] = None,
package_name: str = "",
include: Optional[List[str]] = None,
exclude: Optional[List[str]] = None,
rules: Optional[Dict[str, Any]] = None,
tools: Optional[Dict[str, Any]] = None,
performance: Optional[Dict[str, Any]] = None,
plugins: Optional[Dict[str, Any]] = None,
environments: Optional[Dict[str, Any]] = None,
**kwargs,
):
# Call parent dataclass __init__ with proper values
super().__init__(
project_root=project_root or Path.cwd(),
package_name=package_name,
include=include or ["**/*.py"],
exclude=exclude or [],
rules=rules or {},
)
self.tools: Dict[str, Any] = tools or {}
self.performance: Dict[str, Any] = performance or {}
self.plugins: Dict[str, Any] = plugins or {}
self.environments: Dict[str, Any] = environments or {}
for key, value in kwargs.items():
setattr(self, key, value)

@classmethod
def from_yaml(
cls, path: Path, environment: Optional[str] = None
) -> "ExtendedConfig":
"""Load configuration from YAML file with environment support."""
if not path.exists():
return cls(project_root=Path.cwd())

with open(path) as f:
raw = yaml.safe_load(f) or {}

if environment and "environments" in raw:
env_config = raw["environments"].get(environment, {})
raw = cls._deep_merge(raw, env_config)

rules = cls._parse_rules(raw.pop("rules", {}))
tools = raw.pop("tools", {})
performance = raw.pop("performance", {})
plugins = raw.pop("plugins", {})
environments = raw.pop("environments", {})
include = raw.pop("include", DEFAULT_INCLUDE)
exclude = raw.pop("exclude", DEFAULT_EXCLUDE)

instance = cls(
project_root=Path(raw.pop("project_root", Path.cwd())),
package_name=raw.pop("package_name", ""),
include=include,
exclude=exclude,
rules=rules,
tools=tools,
performance=performance,
plugins=plugins,
environments=environments,
**{k: v for k, v in raw.items() if k in cls.__dataclass_fields__},
)
# Force set include/exclude to ensure they're not None
instance.include = include
instance.exclude = exclude
return instance

@staticmethod
def _parse_rules(rules_raw: Dict[str, Any]) -> Dict[str, RuleConfig]:
"""Parse rules from raw configuration."""
rules = {}
for rule_id, rule_raw in rules_raw.items():
if isinstance(rule_raw, bool):
rules[rule_id] = RuleConfig(enabled=rule_raw)
elif isinstance(rule_raw, dict):
basic_fields = {
k: v
for k, v in rule_raw.items()
if k in {"enabled", "severity", "options"}
}
rules[rule_id] = RuleConfig(**basic_fields)
if hasattr(rules[rule_id], "_extended"):
rules[rule_id]._extended.update(rule_raw)
else:
rules[rule_id]._extended = {
k: v
for k, v in rule_raw.items()
if k not in {"enabled", "severity", "options"}
}
return rules

@staticmethod
def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
"""Deep merge two dictionaries."""
result = base.copy()
for key, value in override.items():
if (
key in result
and isinstance(result[key], dict)
and isinstance(value, dict)
):
result[key] = ExtendedConfig._deep_merge(result[key], value)
else:
result[key] = value
return result

def get_tool_config(self, tool_name: str) -> Dict[str, Any]:
return self.tools.get(tool_name, {})

def get_performance_setting(self, key: str, default: Any = None) -> Any:
return self.performance.get(key, default)

def get_plugin_config(self, plugin_name: str) -> Dict[str, Any]:
return self.plugins.get(plugin_name, {})

def to_dict(self) -> Dict[str, Any]:
result = super().to_dict()
result.update(
{
"tools": self.tools,
"performance": self.performance,
"plugins": self.plugins,
"environments": self.environments,
}
)
return result
Loading
Loading