#!/usr/bin/env python3 """Validate the schema fixtures against the FLUID wire-contract schemas. Every fixture is transcribed from a worked example in spec/. If the specification moves and the schemas do not, this fails the build. """ import json import pathlib import sys import yaml from jsonschema import Draft202012Validator, RefResolver ROOT = pathlib.Path(__file__).resolve().parent.parent SCHEMAS = ROOT / "schemas" FIXTURES = ROOT / "conformance" / "fixtures" # fixture stem -> schema stem PAIRS = { "pressure": "pressure", "hypothesis": "hypothesis", "revision": "revision", "experiment": "experiment", "event": "event", "feedback": "feedback", "backend-requirement": "backend-requirement", "revision-descriptor": "revision-descriptor", "routing-policy": "routing-policy", "telemetry-envelope": "telemetry-envelope", } def load_store(): """Map every schema by both its $id and its bare filename. Schemas cross-reference each other as "common.schema.json#/$defs/...", a relative reference, so the bare filename has to resolve too. """ store = {} for path in SCHEMAS.glob("*.schema.json"): schema = json.loads(path.read_text()) store[path.name] = schema if "$id" in schema: store[schema["$id"]] = schema return store # Instances that MUST be rejected. Without these the suite could pass # vacuously if reference resolution silently degraded to "accept anything". NEGATIVE = [ ( "pressure", "pressure without evidence references is not auditable", {"fluid_pressure": { "schema_version": "0.1", "id": "P-1", "interface_id": "x", "class": "natural_usage", "first_seen": "2026-01-01T00:00:00Z", "last_seen": "2026-01-01T00:00:00Z", "summary": "s", "evidence_refs": [], "status": "OPEN"}}, ), ( "pressure", "pressure class outside the standard taxonomy", {"fluid_pressure": { "schema_version": "0.1", "id": "P-1", "interface_id": "x", "class": "vibes", "first_seen": "2026-01-01T00:00:00Z", "last_seen": "2026-01-01T00:00:00Z", "summary": "s", "evidence_refs": ["telemetry:1"], "status": "OPEN"}}, ), ( "hypothesis", "hypothesis with no falsifiable expected outcome", {"fluid_hypothesis": { "schema_version": "0.1", "id": "H-1", "interface_id": "x", "state": "DRAFT", "title": "t", "observation": {"summary": "s", "evidence_refs": ["telemetry:1"]}, "pressure": {"classes": ["natural_usage"]}, "explanation": {"claim": "c"}, "proposed_adaptation": {"class": "contract", "summary": "s"}, "expected_outcomes": [], "guardrails": [], "complexity": {"expected_delta": {}}, "risk": {"level": "LOW"}, "success_criteria": {"expression": "e"}}}, ), ( "experiment", "experiment with no stop condition is not interruptible by policy", {"fluid_experiment": { "schema_version": "0.1", "id": "E-1", "interface_id": "x", "hypothesis_refs": ["H-1"], "control_revision": "R-1", "candidate_revisions": ["R-2"], "allocation": {"control": 0.9, "candidate": 0.1}, "metrics": {"primary": ["m"]}, "stop_conditions": [], "result": {"state": "PLANNED"}}}, ), ( "revision-descriptor", "descriptor with no governing intent cannot be audited", {"revision": { "schema_version": "0.1", "id": "R-1", "interface": "x", "state": "stable", "contract": {"type": "openapi", "digest": "sha256:" + "0" * 64}, "runtime": {"upstream": "http://a:8080"}, "policy": {"compatibility": "additive", "security_check": "passed"}}}, ), ( "revision-descriptor", "unknown revision state", {"revision": { "schema_version": "0.1", "id": "R-1", "interface": "x", "state": "probably-fine", "contract": {"type": "openapi", "digest": "sha256:" + "0" * 64}, "runtime": {"upstream": "http://a:8080"}, "intent": {"version": "IEI-1"}, "policy": {"compatibility": "additive", "security_check": "passed"}}}, ), ] def run_negative(store): failures = 0 for schema_stem, why, instance in NEGATIVE: schema = json.loads((SCHEMAS / f"{schema_stem}.schema.json").read_text()) resolver = RefResolver(base_uri="", referrer=schema, store=store) validator = Draft202012Validator(schema, resolver=resolver) if validator.is_valid(instance): print(f"FAIL rejected-case accepted: {why}") failures += 1 else: print(f"ok rejects: {why}") return failures def main() -> int: store = load_store() failures = 0 for fixture_stem, schema_stem in sorted(PAIRS.items()): fixture_path = FIXTURES / f"{fixture_stem}.yaml" schema_path = SCHEMAS / f"{schema_stem}.schema.json" if not fixture_path.exists(): print(f"MISSING {fixture_path.relative_to(ROOT)}") failures += 1 continue schema = json.loads(schema_path.read_text()) instance = yaml.safe_load(fixture_path.read_text()) resolver = RefResolver(base_uri="", referrer=schema, store=store) validator = Draft202012Validator(schema, resolver=resolver) errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.path)) if errors: failures += 1 print(f"FAIL {fixture_stem}") for err in errors: where = "/".join(str(p) for p in err.absolute_path) or "" print(f" {where}: {err.message}") else: print(f"ok {fixture_stem}") # A schema that no fixture exercises is a schema nothing protects. unexercised = { p.stem.removesuffix(".schema") for p in SCHEMAS.glob("*.schema.json") } - set(PAIRS.values()) - {"common"} for stem in sorted(unexercised): print(f"WARN {stem}.schema.json has no fixture") # Emit JSON copies so the Go round-trip test can read fixtures without a # YAML dependency (the build stays free of third-party Go modules). json_dir = FIXTURES / "json" json_dir.mkdir(exist_ok=True) for fixture_stem in PAIRS: instance = yaml.safe_load((FIXTURES / f"{fixture_stem}.yaml").read_text()) (json_dir / f"{fixture_stem}.json").write_text( json.dumps(instance, indent=2, sort_keys=True) + "\n" ) print() failures += run_negative(store) if failures: print(f"\n{failures} check(s) failed") return 1 print(f"\nAll {len(PAIRS)} fixtures validate; all {len(NEGATIVE)} rejected cases refused") return 0 if __name__ == "__main__": sys.exit(main())