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:
2026-02-09 21:35:53 +01:00
parent d201221fb1
commit 7a9dda282d
63 changed files with 173 additions and 174 deletions

View 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