market-registry/scripts/validate.py
2026-07-02 04:10:01 +02:00

54 lines
2.3 KiB
Python

#!/usr/bin/env python3
"""Validate all listings against the frozen v1 schemas.
Used by CI and locally: python3 scripts/validate.py
Exits non-zero on any violation. jsonschema required (pip install jsonschema).
"""
import json
import sys
from pathlib import Path
from jsonschema import Draft202012Validator
ROOT = Path(__file__).resolve().parent.parent
SOLUTION = Draft202012Validator(json.loads((ROOT / "schemas/solution.schema.json").read_text()))
LISTING = Draft202012Validator(json.loads((ROOT / "schemas/listing.schema.json").read_text()))
def main() -> int:
failures = []
for d in sorted((ROOT / "listings").iterdir()):
if not d.is_dir():
continue
for name, validator in (("listing.json", LISTING), ("solution.json", SOLUTION)):
f = d / name
if not f.exists():
failures.append(f"{d.name}: missing {name}")
continue
errors = sorted(validator.iter_errors(json.loads(f.read_text())), key=str)
failures += [f"{d.name}/{name}: {e.json_path}: {e.message}" for e in errors]
if (d / "listing.json").exists():
listing = json.loads((d / "listing.json").read_text())
if listing.get("listing_id") != d.name:
failures.append(f"{d.name}: directory name != listing_id")
sol_file = d / "solution.json"
if sol_file.exists():
sol = json.loads(sol_file.read_text())
scope = sol.get("tenant_scope")
vis = listing.get("tenant_visibility", [])
# tenant firewall coherence: a tenant-scoped solution must not be
# visible beyond its tenant (constitution IV)
if scope and scope != "m2-core" and ("*" in vis or any(t != scope for t in vis)):
failures.append(
f"{d.name}: tenant_scope={scope} but tenant_visibility={vis} leaks beyond the tenant"
)
if sol.get("solution_id") and listing.get("solution_id") != sol["solution_id"]:
failures.append(f"{d.name}: listing.solution_id != solution.solution_id")
if failures:
print("VALIDATION FAILED:")
print("\n".join(f" - {f}" for f in failures))
return 1
print("all listings valid")
return 0
if __name__ == "__main__":
sys.exit(main())