#!/usr/bin/env python3 """Validate content YAML files against their JSON schemas. Schema mapping is by directory context: campaign.yaml → campaign.schema.json system.yaml → system.schema.json station.yaml → station.schema.json district.yaml → district.schema.json npcs/*.yaml → npc-profile.schema.json locations/*.yaml → location.schema.json triangles/*.yaml → triangle.schema.json dialogue/**/*.yaml → dialogue-pool.schema.json monologue/**/*.yaml → monologue-pool.schema.json routines/*.yaml → routine.schema.json Files under global/ and content.yaml are skipped (no schema yet). Exit code 0 = all valid, 1 = validation errors found. """ import json import sys from pathlib import Path import jsonschema import yaml CONTENT_DIR = Path(__file__).resolve().parent.parent / "content" SCHEMA_DIR = CONTENT_DIR / "_schema" # Map directory parent name (or filename) to schema file FILENAME_SCHEMAS = { "campaign.yaml": "campaign.schema.json", "system.yaml": "system.schema.json", "station.yaml": "station.schema.json", "district.yaml": "district.schema.json", } DIR_SCHEMAS = { "npcs": "npc-profile.schema.json", "locations": "location.schema.json", "triangles": "triangle.schema.json", "dialogue": "dialogue-pool.schema.json", "monologue": "monologue-pool.schema.json", "routines": "routine.schema.json", } def resolve_schema(yaml_path: Path) -> Path | None: """Determine which schema applies to a content YAML file.""" name = yaml_path.name if name in FILENAME_SCHEMAS: return SCHEMA_DIR / FILENAME_SCHEMAS[name] # Walk up parents to find a matching directory name rel = yaml_path.relative_to(CONTENT_DIR) for part in reversed(rel.parts[:-1]): if part in DIR_SCHEMAS: return SCHEMA_DIR / DIR_SCHEMAS[part] return None def main() -> int: errors = 0 validated = 0 skipped = 0 # Cache loaded schemas schema_cache: dict[str, dict] = {} campaigns_dir = CONTENT_DIR / "campaigns" if not campaigns_dir.exists(): print(f"No campaigns directory at {campaigns_dir}", file=sys.stderr) return 1 yaml_files = sorted(campaigns_dir.rglob("*.yaml")) if not yaml_files: print("No YAML files found under campaigns/", file=sys.stderr) return 1 for yaml_path in yaml_files: schema_path = resolve_schema(yaml_path) if schema_path is None: skipped += 1 continue if not schema_path.exists(): print(f"MISSING SCHEMA: {schema_path.name} for {yaml_path.relative_to(CONTENT_DIR)}") errors += 1 continue # Load schema (cached) schema_key = str(schema_path) if schema_key not in schema_cache: with open(schema_path) as f: schema_cache[schema_key] = json.load(f) schema = schema_cache[schema_key] # Load YAML try: with open(yaml_path) as f: data = yaml.safe_load(f) except yaml.YAMLError as e: print(f"YAML ERROR: {yaml_path.relative_to(CONTENT_DIR)}: {e}") errors += 1 continue if data is None: # Comment-only or empty placeholder files are valid stubs skipped += 1 continue # Validate try: jsonschema.validate(instance=data, schema=schema) validated += 1 except jsonschema.ValidationError as e: rel = yaml_path.relative_to(CONTENT_DIR) print(f"INVALID: {rel}") print(f" Schema: {schema_path.name}") print(f" Error: {e.message}") if e.absolute_path: print(f" Path: {'.'.join(str(p) for p in e.absolute_path)}") errors += 1 print(f"\nValidated {validated} files, {skipped} skipped, {errors} errors") return 1 if errors else 0 if __name__ == "__main__": sys.exit(main())