refactor(scripts): reorganize scripts/ into seed/ and validate/ subfolders
Move 9 init/seed scripts into scripts/seed/ and 7 validation scripts (+ validators/ subfolder) into scripts/validate/ to reduce clutter in the root scripts/ directory. Update all references across Makefile, CI/CD configs, pre-commit hooks, docs (~40 files), and Python imports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
290
scripts/validate/base_validator.py
Executable file
290
scripts/validate/base_validator.py
Executable file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
Base Validator Class
|
||||
|
||||
Shared functionality for all validators.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Severity(str, Enum):
|
||||
"""Severity levels for validation findings."""
|
||||
ERROR = "error"
|
||||
WARNING = "warning"
|
||||
INFO = "info"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Violation:
|
||||
"""A single validation violation."""
|
||||
rule_id: str
|
||||
message: str
|
||||
severity: Severity
|
||||
file_path: str = ""
|
||||
line: int = 0
|
||||
suggestion: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Result of a validation run."""
|
||||
violations: list[Violation] = field(default_factory=list)
|
||||
files_checked: int = 0
|
||||
|
||||
def has_errors(self) -> bool:
|
||||
"""Check if there are any error-level violations."""
|
||||
return any(v.severity == Severity.ERROR for v in self.violations)
|
||||
|
||||
def error_count(self) -> int:
|
||||
"""Count error-level violations."""
|
||||
return sum(1 for v in self.violations if v.severity == Severity.ERROR)
|
||||
|
||||
def warning_count(self) -> int:
|
||||
"""Count warning-level violations."""
|
||||
return sum(1 for v in self.violations if v.severity == Severity.WARNING)
|
||||
|
||||
def info_count(self) -> int:
|
||||
"""Count info-level violations."""
|
||||
return sum(1 for v in self.violations if v.severity == Severity.INFO)
|
||||
|
||||
|
||||
class BaseValidator(ABC):
|
||||
"""Base class for architecture, security, and performance validators."""
|
||||
|
||||
# Directories/patterns to ignore by default
|
||||
IGNORE_PATTERNS = [
|
||||
".venv", "venv", "node_modules", "__pycache__", ".git",
|
||||
".pytest_cache", ".mypy_cache", "dist", "build", "*.egg-info",
|
||||
"migrations", "alembic/versions", ".tox", "htmlcov",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rules_dir: str = "",
|
||||
project_root: Path | None = None,
|
||||
verbose: bool = False,
|
||||
):
|
||||
self.rules_dir = rules_dir
|
||||
self.project_root = project_root or Path.cwd()
|
||||
self.verbose = verbose
|
||||
self.rules: list[dict[str, Any]] = []
|
||||
self.errors: list[dict[str, Any]] = []
|
||||
self.warnings: list[dict[str, Any]] = []
|
||||
self.result = ValidationResult()
|
||||
|
||||
def load_rules(self) -> None:
|
||||
"""Load rules from YAML files."""
|
||||
rules_path = self.project_root / self.rules_dir
|
||||
if not rules_path.exists():
|
||||
print(f"Rules directory not found: {rules_path}")
|
||||
return
|
||||
|
||||
for rule_file in rules_path.glob("*.yaml"):
|
||||
if rule_file.name.startswith("_"):
|
||||
continue # Skip main config
|
||||
|
||||
with open(rule_file) as f:
|
||||
data = yaml.safe_load(f)
|
||||
if data and "rules" in data:
|
||||
self.rules.extend(data["rules"])
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""Run validation. Returns True if passed.
|
||||
|
||||
Subclasses should implement validate_all() instead.
|
||||
"""
|
||||
result = self.validate_all()
|
||||
return not result.has_errors() if hasattr(result, 'has_errors') else True
|
||||
|
||||
def validate_all(self, target_path: Path | None = None) -> ValidationResult:
|
||||
"""Run all validations. Override in subclasses."""
|
||||
return ValidationResult()
|
||||
|
||||
def add_error(
|
||||
self, rule_id: str, message: str, file: str = "", line: int = 0
|
||||
) -> None:
|
||||
"""Add an error."""
|
||||
self.errors.append(
|
||||
{
|
||||
"rule_id": rule_id,
|
||||
"message": message,
|
||||
"file": file,
|
||||
"line": line,
|
||||
"severity": "error",
|
||||
}
|
||||
)
|
||||
|
||||
def add_warning(
|
||||
self, rule_id: str, message: str, file: str = "", line: int = 0
|
||||
) -> None:
|
||||
"""Add a warning."""
|
||||
self.warnings.append(
|
||||
{
|
||||
"rule_id": rule_id,
|
||||
"message": message,
|
||||
"file": file,
|
||||
"line": line,
|
||||
"severity": "warning",
|
||||
}
|
||||
)
|
||||
|
||||
def add_info(
|
||||
self, rule_id: str, message: str, file: str = "", line: int = 0
|
||||
) -> None:
|
||||
"""Add an informational note."""
|
||||
self.warnings.append(
|
||||
{
|
||||
"rule_id": rule_id,
|
||||
"message": message,
|
||||
"file": file,
|
||||
"line": line,
|
||||
"severity": "info",
|
||||
}
|
||||
)
|
||||
|
||||
def print_results(self) -> None:
|
||||
"""Print validation results."""
|
||||
if not self.errors and not self.warnings:
|
||||
print(f"✅ All {self.rules_dir} rules passed!")
|
||||
return
|
||||
|
||||
if self.errors:
|
||||
print(f"\n❌ {len(self.errors)} errors found:")
|
||||
for error in self.errors:
|
||||
print(f" [{error['rule_id']}] {error['message']}")
|
||||
if error["file"]:
|
||||
print(f" File: {error['file']}:{error['line']}")
|
||||
|
||||
if self.warnings:
|
||||
print(f"\n⚠️ {len(self.warnings)} warnings:")
|
||||
for warning in self.warnings:
|
||||
print(f" [{warning['rule_id']}] {warning['message']}")
|
||||
if warning["file"]:
|
||||
print(f" File: {warning['file']}:{warning['line']}")
|
||||
|
||||
def run(self) -> int:
|
||||
"""Run validation and return exit code."""
|
||||
self.load_rules()
|
||||
passed = self.validate()
|
||||
self.print_results()
|
||||
return 0 if passed else 1
|
||||
|
||||
def _should_ignore_file(self, file_path: Path) -> bool:
|
||||
"""Check if a file should be ignored based on patterns."""
|
||||
path_str = str(file_path)
|
||||
for pattern in self.IGNORE_PATTERNS:
|
||||
if pattern in path_str:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _add_violation(
|
||||
self,
|
||||
rule_id: str,
|
||||
rule_name: str,
|
||||
severity: Severity,
|
||||
file_path: Path,
|
||||
line_number: int,
|
||||
message: str,
|
||||
context: str = "",
|
||||
suggestion: str = "",
|
||||
) -> None:
|
||||
"""Add a violation to the result."""
|
||||
violation = Violation(
|
||||
rule_id=rule_id,
|
||||
message=f"{rule_name}: {message}",
|
||||
severity=severity,
|
||||
file_path=str(file_path),
|
||||
line=line_number,
|
||||
suggestion=suggestion,
|
||||
)
|
||||
self.result.violations.append(violation)
|
||||
|
||||
if self.verbose and context:
|
||||
print(f" [{rule_id}] {file_path}:{line_number}")
|
||||
print(f" {message}")
|
||||
print(f" Context: {context}")
|
||||
|
||||
def validate_file(self, file_path: Path) -> ValidationResult:
|
||||
"""Validate a single file."""
|
||||
if not file_path.exists():
|
||||
print(f"File not found: {file_path}")
|
||||
return self.result
|
||||
|
||||
self.result.files_checked = 1
|
||||
content = file_path.read_text()
|
||||
lines = content.split("\n")
|
||||
self._validate_file_content(file_path, content, lines)
|
||||
return self.result
|
||||
|
||||
def _validate_file_content(self, file_path: Path, content: str, lines: list[str]):
|
||||
"""Validate file content. Override in subclasses."""
|
||||
pass
|
||||
|
||||
def output_results(self, json_output: bool = False, errors_only: bool = False) -> None:
|
||||
"""Output validation results."""
|
||||
if json_output:
|
||||
import json
|
||||
output = {
|
||||
"files_checked": self.result.files_checked,
|
||||
"violations": [
|
||||
{
|
||||
"rule_id": v.rule_id,
|
||||
"message": v.message,
|
||||
"severity": v.severity.value,
|
||||
"file": v.file_path,
|
||||
"line": v.line,
|
||||
"suggestion": v.suggestion,
|
||||
}
|
||||
for v in self.result.violations
|
||||
if not errors_only or v.severity == Severity.ERROR
|
||||
],
|
||||
}
|
||||
print(json.dumps(output, indent=2))
|
||||
else:
|
||||
self._print_violations(errors_only)
|
||||
|
||||
def _print_violations(self, errors_only: bool = False) -> None:
|
||||
"""Print violations in human-readable format."""
|
||||
violations = self.result.violations
|
||||
if errors_only:
|
||||
violations = [v for v in violations if v.severity == Severity.ERROR]
|
||||
|
||||
if not violations:
|
||||
print(f"\n✅ No issues found! ({self.result.files_checked} files checked)")
|
||||
return
|
||||
|
||||
errors = [v for v in violations if v.severity == Severity.ERROR]
|
||||
warnings = [v for v in violations if v.severity == Severity.WARNING]
|
||||
info = [v for v in violations if v.severity == Severity.INFO]
|
||||
|
||||
if errors:
|
||||
print(f"\n❌ {len(errors)} errors:")
|
||||
for v in errors:
|
||||
print(f" [{v.rule_id}] {v.file_path}:{v.line}")
|
||||
print(f" {v.message}")
|
||||
if v.suggestion:
|
||||
print(f" 💡 {v.suggestion}")
|
||||
|
||||
if warnings and not errors_only:
|
||||
print(f"\n⚠️ {len(warnings)} warnings:")
|
||||
for v in warnings:
|
||||
print(f" [{v.rule_id}] {v.file_path}:{v.line}")
|
||||
print(f" {v.message}")
|
||||
|
||||
if info and not errors_only:
|
||||
print(f"\nℹ️ {len(info)} info:")
|
||||
for v in info:
|
||||
print(f" [{v.rule_id}] {v.file_path}:{v.line}")
|
||||
print(f" {v.message}")
|
||||
|
||||
print(f"\n📊 Summary: {len(errors)} errors, {len(warnings)} warnings, {len(info)} info")
|
||||
|
||||
def get_exit_code(self) -> int:
|
||||
"""Get exit code based on validation results."""
|
||||
return 1 if self.result.has_errors() else 0
|
||||
Reference in New Issue
Block a user