"""Top-level PrERT command runner for command discovery and execution.""" from __future__ import annotations import argparse import importlib from pathlib import Path import sys from typing import Callable, Dict, List, Tuple from prert.config import load_dotenv_if_available ENTRYPOINTS: Dict[str, str] = { "extract": "prert.cli.extract", "migrate": "prert.cli.migrate", "phase2": "prert.cli.phase2", "opp115": "prert.cli.opp115", "app350": "prert.cli.app350", "phase3": "prert.cli.phase3", "phase3-freeze": "prert.cli.phase3_freeze", "phase4": "prert.cli.phase4", "phase4-web": "prert.cli.phase4_web", "phase4-synth": "prert.cli.phase4_synthetic", } ALIASES: Dict[str, str] = { "phase1-extract": "extract", "phase1-migrate": "migrate", } REQUIRED_DOCX_INPUTS = ( "docs/Standards/Regulations/GDPR-2016_679.docx", "docs/Standards/Regulations/NIST-1.1.docx", ) REQUIRED_CHROMA_ENV_VARS = ( "CHROMA_HOST", "CHROMA_API_KEY", "CHROMA_TENANT", "CHROMA_DATABASE", ) OPTIONAL_ENV_VARS = ( "CHROMA_COLLECTION_NAME", "HF_TOKEN", ) def main() -> None: raise SystemExit(run()) def run(argv: List[str] | None = None) -> int: args = list(sys.argv[1:] if argv is None else argv) load_dotenv_if_available(None) if not args: _print_help() return 0 command = args[0] command_args = args[1:] if command in {"-h", "--help", "help"}: _print_help() return 0 if command in {"doctor", "guide", "interactive"}: if command == "doctor": return _run_doctor(command_args) if command == "interactive": return _run_interactive(command_args) return _run_guide(command_args) canonical = ALIASES.get(command, command) if canonical not in ENTRYPOINTS: print(f"Unknown command: {command}") print("Use 'prert help' to list supported commands.") return 2 return _dispatch_to_entrypoint(canonical, command_args) def _dispatch_to_entrypoint(command: str, command_args: List[str]) -> int: module_name = ENTRYPOINTS[command] module = importlib.import_module(module_name) entrypoint: Callable[[], None] = getattr(module, "main") previous_argv = list(sys.argv) try: sys.argv = [f"prert {command}", *command_args] entrypoint() except SystemExit as exc: code = exc.code if isinstance(code, int): return code return 0 finally: sys.argv = previous_argv return 0 def _run_doctor(argv: List[str]) -> int: parser = argparse.ArgumentParser(description="Validate PrERT setup prerequisites") parser.add_argument( "--root", type=Path, default=Path.cwd(), help="Project root to validate. Defaults to current working directory.", ) args = parser.parse_args(argv) root = args.root.resolve() failures = 0 print(f"PrERT Doctor: {root}") failures += _check_python_version() failures += _check_docx_inputs(root) failures += _check_iso_inputs(root) failures += _check_env_file(root) if failures: print("\nResult: FAIL") print("Fix the items above and rerun: prert doctor") return 1 print("\nResult: PASS") return 0 def _run_guide(argv: List[str]) -> int: parser = argparse.ArgumentParser(description="Show guided PrERT command sequences") parser.add_argument( "--goal", choices=("full", "phase1", "phase2", "phase3", "phase4", "validation"), default="full", help="Workflow goal to print recommended next commands.", ) args = parser.parse_args(argv) print("PrERT Guided Commands") print(f"Goal: {args.goal}\n") if args.goal in {"full", "phase1"}: print("Phase 1") print("1. prert extract --chunk --output-dir artifacts/phase-1") print("2. prert migrate --input-dir artifacts/phase-1") if args.goal == "phase1": return 0 print("") if args.goal in {"full", "phase2"}: print("Phase 2") print("1. prert phase2") print("2. Optional public mapping: prert opp115") print("3. Optional enrichment: prert phase2 --public-input data/processed/opp115_public_mapping.csv") if args.goal == "phase2": return 0 print("") if args.goal in {"full", "phase3"}: print("Phase 3") print("1. prert phase3") print("2. Optional acceptance freeze: prert phase3-freeze --strict") if args.goal == "phase3": return 0 print("") if args.goal in {"full", "phase4", "validation"}: print("Phase 4") print("1. Validate artifacts: prert phase4 --baseline-dir artifacts/phase-3-freeze") print("2. Launch web app: prert phase4-web --public") print("3. Optional synthetic fixtures: prert phase4-synth --output-dir artifacts/phase-4/synthetic-compliance") return 0 def _run_interactive(argv: List[str]) -> int: parser = argparse.ArgumentParser(description="Interactive PrERT command picker") parser.add_argument( "--goal", choices=("full", "phase1", "phase2", "phase3", "phase4", "validation"), default="full", help="Workflow goal to filter interactive options.", ) parser.add_argument( "--select", type=int, default=None, help="Option number to select without prompt.", ) parser.add_argument( "--execute", action="store_true", help="Execute the selected option immediately.", ) args = parser.parse_args(argv) options = _interactive_options(args.goal) print("PrERT Interactive Runner") print("") for index, (title, command_args) in enumerate(options, start=1): print(f"{index}. {title}") print(f" prert {' '.join(command_args)}") selected_index = args.select if selected_index is None: raw = input("\nSelect an option number (Enter to exit): ").strip() if not raw: print("No selection made.") return 0 if not raw.isdigit(): print("Invalid selection. Enter a numeric option.") return 2 selected_index = int(raw) if selected_index < 1 or selected_index > len(options): print(f"Invalid selection: {selected_index}. Choose 1 to {len(options)}.") return 2 title, selected_command = options[selected_index - 1] print("") print(f"Selected: {title}") print(f"Command: prert {' '.join(selected_command)}") should_execute = args.execute if not should_execute and args.select is None: confirm = input("Execute now? [y/N]: ").strip().lower() should_execute = confirm in {"y", "yes"} if not should_execute: print("Use --execute to run selected commands directly.") return 0 return run(selected_command) def _interactive_options(goal: str) -> List[Tuple[str, List[str]]]: options: List[Tuple[str, List[str]]] = [] if goal in {"full", "phase1"}: options.extend( [ ("Phase 1: Extract controls and chunks", ["extract", "--chunk", "--output-dir", "artifacts/phase-1"]), ("Phase 1: Migrate chunks to Chroma", ["migrate", "--input-dir", "artifacts/phase-1"]), ] ) if goal in {"full", "phase2"}: options.extend( [ ("Phase 2: Run baseline metrics pipeline", ["phase2"]), ("Phase 2: Build OPP-115 mapping export", ["opp115"]), ] ) if goal in {"full", "phase3"}: options.extend( [ ("Phase 3: Run baseline training", ["phase3"]), ("Phase 3: Run acceptance freeze", ["phase3-freeze", "--strict"]), ] ) if goal in {"full", "phase4", "validation"}: options.extend( [ ("Phase 4: Validate baseline artifacts", ["phase4", "--baseline-dir", "artifacts/phase-3-freeze"]), ("Phase 4: Launch compliance web app", ["phase4-web", "--port", "7860"]), ( "Phase 4: Generate synthetic compliance fixtures", ["phase4-synth", "--output-dir", "artifacts/phase-4/synthetic-compliance"], ), ] ) return options def _check_python_version() -> int: major, minor = sys.version_info.major, sys.version_info.minor if (major, minor) >= (3, 11): print(f"[PASS] Python version {major}.{minor}") return 0 print(f"[FAIL] Python version {major}.{minor} (requires >= 3.11)") return 1 def _check_docx_inputs(root: Path) -> int: failures = 0 for relative_path in REQUIRED_DOCX_INPUTS: candidate = root / relative_path if candidate.exists(): print(f"[PASS] Found {relative_path}") else: print(f"[FAIL] Missing required input {relative_path}") failures += 1 return failures def _check_iso_inputs(root: Path) -> int: regulations_dir = root / "docs/Standards/Regulations" if not regulations_dir.exists(): print("[FAIL] Missing docs/Standards/Regulations directory") return 1 iso_files = sorted( path for path in regulations_dir.glob("*.docx") if path.name.startswith("ISO") or path.name.startswith("BS EN ISO") or path.name.startswith("BS ISO") ) if iso_files: print(f"[PASS] ISO DOCX inputs discovered: {len(iso_files)}") return 0 print("[FAIL] No ISO DOCX inputs found in docs/Standards/Regulations") return 1 def _check_env_file(root: Path) -> int: env_path = root / ".env" if not env_path.exists(): print("[FAIL] Missing .env file") return 1 values = _load_env_values(env_path) failures = 0 for key in REQUIRED_CHROMA_ENV_VARS: if values.get(key, "").strip(): print(f"[PASS] {key} is set") else: print(f"[FAIL] {key} is missing or empty") failures += 1 for key in OPTIONAL_ENV_VARS: if values.get(key, "").strip(): print(f"[PASS] {key} is set") else: print(f"[WARN] {key} is not set (optional)") return failures def _load_env_values(path: Path) -> Dict[str, str]: values: Dict[str, str] = {} for line in path.read_text(encoding="utf-8").splitlines(): stripped = line.strip() if not stripped or stripped.startswith("#") or "=" not in stripped: continue key, raw = stripped.split("=", 1) values[key.strip()] = raw.strip().strip('"').strip("'") return values def _print_help() -> None: print("PrERT Command Runner") print("") print("Usage:") print(" prert [args]") print("") print("Core commands:") print(" extract Run Phase 1 extraction") print(" migrate Run Phase 1 Chroma migration") print(" phase2 Run Phase 2 metrics pipeline") print(" opp115 Preprocess OPP-115 public mapping inputs") print(" phase3 Run Phase 3 baseline training") print(" phase3-freeze Run Phase 3 acceptance freeze") print(" phase4 Run Phase 4 validation benchmark") print(" phase4-web Launch Phase 4 Gradio web app") print(" phase4-synth Generate synthetic policy/schema fixtures") print("") print("Helper commands:") print(" doctor Validate environment, inputs, and credentials") print(" guide Print recommended command order") print(" interactive Pick commands from an interactive menu") print("") print("Examples:") print(" prert guide --goal full") print(" prert interactive") print(" prert interactive --goal phase1 --select 1 --execute") print(" prert extract --chunk --output-dir artifacts/phase-1") print(" prert phase4-web --public") if __name__ == "__main__": main()