Files
settled-reach/tooling/validate-checklist
T
jpmschweitzerandClaude Opus 4.6 b2ef56440f refactor(simulation): move content/ under server/ (#669)
Content files (RON templates, YAML campaigns, gauntlet data) are consumed
exclusively by the server. Colocate them at server/content/ and update all
path references in Rust source, tooling scripts, and schema files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 10:31:20 +01:00

195 lines
5.4 KiB
Python
Executable File

#!/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())