#!/usr/bin/env python3 """Validate Gauntlet checklist YAML files against the checklist JSON schema. Usage: validate-checklist Validate all checklists + print summary validate-checklist --check Schema validation only (for pre-PR chain) Exit code 0 = all valid, 1 = validation errors found. """ import json import sys from pathlib import Path import jsonschema import yaml ROOT = Path(__file__).resolve().parent.parent SCHEMA_PATH = ROOT / "server" / "content" / "_schema" / "checklist.schema.json" GAUNTLET_DIR = ROOT / "server" / "content" / "gauntlet" def load_schema(): if not SCHEMA_PATH.exists(): print(f"ERROR: Schema file not found at {_rel(SCHEMA_PATH)}") print("Expected: content/_schema/checklist.schema.json") sys.exit(1) with open(SCHEMA_PATH) as f: return json.load(f) def find_checklists(): """Find all checklist YAML files under content/gauntlet/.""" files = [] if not GAUNTLET_DIR.exists(): return files for path in sorted(GAUNTLET_DIR.rglob("checklist.yaml")): files.append(path) cross = GAUNTLET_DIR / "cross_room_checks.yaml" if cross.exists(): files.append(cross) return files def _rel(path: Path) -> str: try: return str(path.relative_to(ROOT)) except ValueError: return str(path) def validate_schema(files, schema): """Pass 1: JSON Schema validation. Returns (validated, errors).""" errors = 0 validated = 0 for path in files: rel = _rel(path) try: with open(path) as f: data = yaml.safe_load(f) except yaml.YAMLError as e: print(f"YAML ERROR: {rel}: {e}") errors += 1 continue if data is None: print(f"EMPTY: {rel}") errors += 1 continue try: jsonschema.validate(instance=data, schema=schema) validated += 1 except jsonschema.ValidationError as e: print(f"INVALID: {rel}") print(f" Error: {e.message}") if e.absolute_path: print(f" Path: {'.'.join(str(p) for p in e.absolute_path)}") errors += 1 return validated, errors def check_id_uniqueness(files): """Pass 2: Condition ID uniqueness within and across files.""" errors = 0 global_ids: dict[str, Path] = {} for path in files: rel = _rel(path) try: with open(path) as f: data = yaml.safe_load(f) except (yaml.YAMLError, OSError): continue if not data or not isinstance(data, dict): continue local_seen: set[str] = set() for cond in data.get("conditions", []): cid = cond.get("id") if not cid: continue if cid in local_seen: print(f'ID ERROR: duplicate condition id "{cid}" in {rel}') errors += 1 local_seen.add(cid) if cid in global_ids and global_ids[cid] != path: print(f'ID ERROR: condition id "{cid}" used in multiple files') print(f" First: {_rel(global_ids[cid])}") print(f" Also: {rel}") errors += 1 elif cid not in global_ids: global_ids[cid] = path return errors def summarize(files): """Print per-room condition counts and type breakdown.""" total = 0 type_counts: dict[str, int] = {} for path in files: try: with open(path) as f: data = yaml.safe_load(f) except (yaml.YAMLError, OSError): continue if not data or not isinstance(data, dict): continue conditions = data.get("conditions", []) count = len(conditions) total += count room = data.get("room_id", "cross_room") print(f" {room}: {count} conditions") for cond in conditions: ct = cond.get("condition_type", "unknown") type_counts[ct] = type_counts.get(ct, 0) + 1 print(f"\n Total: {total} conditions across {len(files)} files") if type_counts: print(" By type:") for ct in sorted(type_counts): print(f" {ct}: {type_counts[ct]}") def main(): check_only = "--check" in sys.argv if not GAUNTLET_DIR.exists(): print(f"No gauntlet directory at {_rel(GAUNTLET_DIR)}") print("Checklist validation skipped (no content yet).") return 0 files = find_checklists() if not files: print("No checklist files found under content/gauntlet/.") print("Checklist validation skipped.") return 0 schema = load_schema() # Pass 1: Schema validation validated, schema_errors = validate_schema(files, schema) print(f"Pass 1 (schema): {validated} valid, {schema_errors} errors") if schema_errors > 0: print(f"\nSchema validation failed ({schema_errors} errors) — skipping ID checks") return 1 # Pass 2: Condition ID uniqueness id_errors = check_id_uniqueness(files) if id_errors > 0: print(f"Pass 2 (IDs): {id_errors} errors") total_errors = schema_errors + id_errors print(f"\nChecklist validation: {validated} valid, {total_errors} errors") if total_errors > 0: return 1 if not check_only: print("\nChecklist summary:") summarize(files) return 0 if __name__ == "__main__": sys.exit(main())