#!/usr/bin/env python3 """Validate starter JSON Schemas and their canonical examples. Requires the development-only `jsonschema` package. The core `projectctl.py` remains standard-library-only so repository state can be recovered first. """ from __future__ import annotations import json import sys from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] def load(path: Path) -> Any: return json.loads(path.read_text(encoding="utf-8")) def main() -> int: try: from jsonschema import Draft202012Validator from referencing import Registry, Resource except ModuleNotFoundError: print( "ERROR: contract validation requires the development package 'jsonschema'.", file=sys.stderr, ) return 2 schema_paths = sorted((ROOT / "specs").glob("*.json")) schemas: dict[str, dict[str, Any]] = {} resources: list[tuple[str, Any]] = [] for path in schema_paths: schema = load(path) Draft202012Validator.check_schema(schema) schema_id = schema.get("$id") if not isinstance(schema_id, str) or not schema_id: print(f"ERROR: {path.relative_to(ROOT)} has no $id", file=sys.stderr) return 1 schemas[path.relative_to(ROOT).as_posix()] = schema resource = Resource.from_contents(schema) resources.append((schema_id, resource)) resources.append((path.resolve().as_uri(), resource)) registry = Registry().with_resources(resources) checks: list[tuple[str, str]] = [ ("config/metrics/catalog.example.json", "specs/metric-catalog.schema.json"), ("config/dashboards/default-overview.example.json", "specs/dashboard.schema.json"), ("config/alerts/default-rules.example.json", "specs/alert-rule-set.schema.json"), ("config/probes/probe.example.json", "specs/probe.schema.json"), ] # The canonical private repository validates its implementation ledger. The # curated public-source export deliberately excludes private planning state. if (ROOT / "planning" / "task-ledger.json").exists(): checks.insert(0, ("planning/task-ledger.json", "specs/task-ledger.schema.json")) checks.extend( (path.relative_to(ROOT).as_posix(), "specs/simulator-scenario.schema.json") for path in sorted((ROOT / "fixtures" / "scenarios").glob("*.json")) ) failures = 0 for document_name, schema_name in checks: document = load(ROOT / document_name) schema = schemas[schema_name] validator = Draft202012Validator(schema, registry=registry) errors = sorted(validator.iter_errors(document), key=lambda item: list(item.path)) if not errors: print(f"PASS: {document_name}") continue failures += len(errors) print(f"FAIL: {document_name}", file=sys.stderr) for error in errors: location = "/".join(str(part) for part in error.path) or "" print(f"- {location}: {error.message}", file=sys.stderr) if failures: print(f"CONTRACT VALIDATION: FAIL ({failures} errors)", file=sys.stderr) return 1 print(f"CONTRACT VALIDATION: PASS ({len(schema_paths)} schemas, {len(checks)} documents)") return 0 if __name__ == "__main__": raise SystemExit(main())