Spaces:
Runtime error
Runtime error
Aaron Brown
Add authoring lint, artifact verify, trajectory analytics, and curriculum runner
f36b499 | """Artifact source of truth for OpenRange builder templates. | |
| Computes and verifies SHA256 hashes of template files to ensure they | |
| haven't been modified unexpectedly. | |
| Usage:: | |
| python -m open_range.artifact_verify update # regenerate hashes | |
| python -m open_range.artifact_verify verify # check hashes match | |
| python -m open_range.artifact_verify # defaults to verify | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| import sys | |
| from pathlib import Path | |
| # Default templates directory relative to this file | |
| _DEFAULT_TEMPLATES_DIR = Path(__file__).parent / "builder" / "templates" | |
| _DEFAULT_HASHES_FILE = _DEFAULT_TEMPLATES_DIR / ".hashes.json" | |
| def _hash_file(path: Path) -> str: | |
| """Compute SHA256 hex digest of a file.""" | |
| h = hashlib.sha256() | |
| with open(path, "rb") as f: | |
| for chunk in iter(lambda: f.read(8192), b""): | |
| h.update(chunk) | |
| return h.hexdigest() | |
| def _find_templates(templates_dir: Path) -> list[Path]: | |
| """Find all template files in the templates directory. | |
| Returns sorted list of template file paths (excludes .hashes.json | |
| and hidden files). | |
| """ | |
| if not templates_dir.is_dir(): | |
| return [] | |
| templates = [] | |
| for p in sorted(templates_dir.iterdir()): | |
| if p.is_file() and not p.name.startswith("."): | |
| templates.append(p) | |
| return templates | |
| def compute_hashes(templates_dir: Path | None = None) -> dict[str, str]: | |
| """Compute SHA256 hashes for all template files. | |
| Args: | |
| templates_dir: Directory containing templates. Defaults to | |
| ``src/open_range/builder/templates/``. | |
| Returns: | |
| Dict mapping filename to SHA256 hex digest. | |
| """ | |
| templates_dir = templates_dir or _DEFAULT_TEMPLATES_DIR | |
| hashes: dict[str, str] = {} | |
| for path in _find_templates(templates_dir): | |
| hashes[path.name] = _hash_file(path) | |
| return hashes | |
| def update( | |
| templates_dir: Path | None = None, | |
| hashes_file: Path | None = None, | |
| ) -> dict[str, str]: | |
| """Regenerate the hash manifest file. | |
| Args: | |
| templates_dir: Directory containing templates. | |
| hashes_file: Path to write the JSON hash manifest. | |
| Returns: | |
| The computed hashes dict. | |
| """ | |
| templates_dir = templates_dir or _DEFAULT_TEMPLATES_DIR | |
| hashes_file = hashes_file or (templates_dir / ".hashes.json") | |
| hashes = compute_hashes(templates_dir) | |
| hashes_file.parent.mkdir(parents=True, exist_ok=True) | |
| with open(hashes_file, "w") as f: | |
| json.dump(hashes, f, indent=2, sort_keys=True) | |
| f.write("\n") | |
| return hashes | |
| def verify( | |
| templates_dir: Path | None = None, | |
| hashes_file: Path | None = None, | |
| ) -> dict[str, str]: | |
| """Verify current templates match stored hashes. | |
| Args: | |
| templates_dir: Directory containing templates. | |
| hashes_file: Path to the JSON hash manifest. | |
| Returns: | |
| Dict mapping filename to status string: | |
| - "ok" if hash matches | |
| - "modified" if hash differs | |
| - "missing" if file in manifest but not on disk | |
| - "new" if file on disk but not in manifest | |
| Raises: | |
| FileNotFoundError: If the hashes file does not exist. | |
| """ | |
| templates_dir = templates_dir or _DEFAULT_TEMPLATES_DIR | |
| hashes_file = hashes_file or (templates_dir / ".hashes.json") | |
| if not hashes_file.exists(): | |
| raise FileNotFoundError( | |
| f"Hash manifest not found: {hashes_file}. " | |
| f"Run 'python -m open_range.artifact_verify update' first." | |
| ) | |
| with open(hashes_file) as f: | |
| stored_hashes: dict[str, str] = json.load(f) | |
| current_hashes = compute_hashes(templates_dir) | |
| results: dict[str, str] = {} | |
| # Check files in manifest | |
| for name, stored_hash in stored_hashes.items(): | |
| if name not in current_hashes: | |
| results[name] = "missing" | |
| elif current_hashes[name] != stored_hash: | |
| results[name] = "modified" | |
| else: | |
| results[name] = "ok" | |
| # Check for new files not in manifest | |
| for name in current_hashes: | |
| if name not in stored_hashes: | |
| results[name] = "new" | |
| return results | |
| # --------------------------------------------------------------------------- | |
| # CLI | |
| # --------------------------------------------------------------------------- | |
| # ANSI color codes | |
| _GREEN = "\033[32m" | |
| _RED = "\033[31m" | |
| _YELLOW = "\033[33m" | |
| _BOLD = "\033[1m" | |
| _RESET = "\033[0m" | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description="Verify or update OpenRange template artifact hashes", | |
| ) | |
| parser.add_argument( | |
| "action", | |
| nargs="?", | |
| default="verify", | |
| choices=["verify", "update"], | |
| help="Action to perform (default: verify)", | |
| ) | |
| parser.add_argument( | |
| "--templates-dir", | |
| type=Path, | |
| default=None, | |
| help="Templates directory (default: src/open_range/builder/templates/)", | |
| ) | |
| args = parser.parse_args() | |
| if args.action == "update": | |
| hashes = update(templates_dir=args.templates_dir) | |
| print(f"{_GREEN}Updated hash manifest with {len(hashes)} files:{_RESET}") | |
| for name, h in sorted(hashes.items()): | |
| print(f" {name}: {h[:16]}...") | |
| sys.exit(0) | |
| # verify | |
| try: | |
| results = verify(templates_dir=args.templates_dir) | |
| except FileNotFoundError as exc: | |
| print(f"{_RED}{exc}{_RESET}", file=sys.stderr) | |
| sys.exit(1) | |
| any_issues = False | |
| for name, status in sorted(results.items()): | |
| if status == "ok": | |
| print(f" {_GREEN}OK{_RESET} {name}") | |
| elif status == "modified": | |
| print(f" {_RED}MODIFIED{_RESET} {name}") | |
| any_issues = True | |
| elif status == "missing": | |
| print(f" {_RED}MISSING{_RESET} {name}") | |
| any_issues = True | |
| elif status == "new": | |
| print(f" {_YELLOW}NEW{_RESET} {name}") | |
| any_issues = True | |
| if any_issues: | |
| print( | |
| f"\n{_RED}Verification failed.{_RESET} " | |
| f"Run 'python -m open_range.artifact_verify update' to refresh." | |
| ) | |
| sys.exit(1) | |
| else: | |
| print(f"\n{_GREEN}All templates verified.{_RESET}") | |
| sys.exit(0) | |
| if __name__ == "__main__": | |
| main() | |