#!/usr/bin/env python3 """Validate content catalogs against JSON Schema (CI + local). Today: each object in content/skills/*.json top-level ``skills`` array vs content/schemas/skill-def.schema.json. Extend this script as new catalogs land. """ from __future__ import annotations import json import sys from pathlib import Path from jsonschema import Draft202012Validator REPO_ROOT = Path(__file__).resolve().parent.parent SKILL_SCHEMA = REPO_ROOT / "content/schemas/skill-def.schema.json" SKILLS_DIR = REPO_ROOT / "content/skills" def main() -> int: if not SKILL_SCHEMA.is_file(): print(f"error: missing schema {SKILL_SCHEMA}", file=sys.stderr) return 1 schema = json.loads(SKILL_SCHEMA.read_text(encoding="utf-8")) validator = Draft202012Validator(schema) if not SKILLS_DIR.is_dir(): print(f"error: missing directory {SKILLS_DIR}", file=sys.stderr) return 1 json_files = sorted(SKILLS_DIR.glob("*.json")) if not json_files: print(f"error: no JSON files under {SKILLS_DIR}", file=sys.stderr) return 1 seen_ids: dict[str, str] = {} errors = 0 for path in json_files: data = json.loads(path.read_text(encoding="utf-8")) skills = data.get("skills") if not isinstance(skills, list): print(f"error: {path.relative_to(REPO_ROOT)}: expected top-level 'skills' array", file=sys.stderr) errors += 1 continue for i, row in enumerate(skills): if not isinstance(row, dict): print(f"error: {path.relative_to(REPO_ROOT)}: skills[{i}] must be an object", file=sys.stderr) errors += 1 continue for err in sorted(validator.iter_errors(row), key=lambda e: e.path): loc = ".".join(str(p) for p in err.path) or "(root)" print( f"error: {path.relative_to(REPO_ROOT)} skills[{i}] {loc}: {err.message}", file=sys.stderr, ) errors += 1 sid = row.get("id") if isinstance(sid, str): prev = seen_ids.get(sid) if prev: print( f"error: duplicate skill id {sid!r} in {prev} and {path.relative_to(REPO_ROOT)}", file=sys.stderr, ) errors += 1 else: seen_ids[sid] = str(path.relative_to(REPO_ROOT)) if errors: print(f"content validation failed with {errors} error(s)", file=sys.stderr) return 1 print(f"content OK: {len(json_files)} skill catalog file(s), {len(seen_ids)} unique skill id(s)") return 0 if __name__ == "__main__": raise SystemExit(main())