#!/usr/bin/env python3 """Lines-of-Code (LOC) Quality Guard (EPIC R10 - CASAN Gate S). Enforces the Single Responsibility Principle by ensuring that no production Python file in Clean Architecture packages exceeds the configured limit (400 LOC). """ from __future__ import annotations import argparse import os import sys from pathlib import Path from typing import List, Tuple # Ensure stdout handles UTF-8 on Windows consoles without codec crash if hasattr(sys.stdout, "reconfigure"): try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass # Default target directories strictly subjected to the 400 LOC constraint DEFAULT_TARGET_DIRS = ["domain", "application", "infrastructure", "presentation"] DEFAULT_MAX_LINES = 400 def count_file_lines(file_path: Path) -> int: """Read a python file and return total physical line count.""" try: content = file_path.read_text(encoding="utf-8", errors="ignore") return len(content.splitlines()) except Exception as exc: print(f"[WARN] Failed to read {file_path}: {exc}", file=sys.stderr) return 0 def scan_directories( root_dir: Path, target_dirs: List[str], max_lines: int, verbose: bool = False ) -> Tuple[int, List[Tuple[str, int]]]: """Recursively scan target packages for files exceeding the maximum LOC limit. Returns: A tuple of (total_files_scanned, list_of_violations_as_(relative_path, line_count)) """ total_files = 0 violations: List[Tuple[str, int]] = [] for target in target_dirs: dir_path = root_dir / target if not dir_path.is_dir(): if verbose: print(f"[INFO] Skipping missing directory: {target}") continue for current_root, _, files in os.walk(dir_path): for file_name in files: if not file_name.endswith(".py"): continue full_path = Path(current_root) / file_name rel_path = full_path.relative_to(root_dir).as_posix() lines = count_file_lines(full_path) total_files += 1 if verbose: print(f" {rel_path}: {lines} lines") if lines > max_lines: violations.append((rel_path, lines)) return total_files, violations def main() -> int: """CLI entry point for the LOC guard script.""" parser = argparse.ArgumentParser( description="Verify that production source files do not exceed the LOC ceiling." ) parser.add_argument( "--max-lines", type=int, default=DEFAULT_MAX_LINES, help=f"Maximum allowed lines per file (default: {DEFAULT_MAX_LINES})", ) parser.add_argument( "--dirs", nargs="+", default=DEFAULT_TARGET_DIRS, help=f"Target directories to scan (default: {' '.join(DEFAULT_TARGET_DIRS)})", ) parser.add_argument( "--root", type=str, default=str(Path(__file__).resolve().parent.parent), help="Root repository directory", ) parser.add_argument( "-v", "--verbose", action="store_true", help="Enable verbose output listing all scanned files", ) args = parser.parse_args() root_dir = Path(args.root).resolve() print("=" * 70) print(f"CASAN Guard 'S' (Single Responsibility): Checking file length <= {args.max_lines} LOC") print(f"Scanning target directories: {args.dirs}") print("=" * 70) total_files, violations = scan_directories( root_dir=root_dir, target_dirs=args.dirs, max_lines=args.max_lines, verbose=args.verbose, ) if violations: print(f"\n[FAIL] Found {len(violations)} oversized file(s) (> {args.max_lines} LOC):") for file_path, lines in sorted(violations, key=lambda x: x[1], reverse=True): print(f" ❌ {file_path}: {lines} lines (exceeds limit by {lines - args.max_lines})") print("\nAction Required: Refactor oversized files into smaller single-responsibility modules.") return 1 print(f"\n[PASS] All {total_files} production files in {args.dirs} satisfy <= {args.max_lines} LOC limit.") return 0 if __name__ == "__main__": sys.exit(main())