#!/usr/bin/env python3 """Gate the snapshot before it is allowed to deploy. Exits non-zero on any structural problem so a bad refresh can never reach the live site.""" from __future__ import annotations import json, sys from pathlib import Path UNIVERSES = ("countries","sectors","factors","fx","crypto","macros") MIN_LIVE = 30 COMPS = ("momentum","strength","breadth","options","volatility","safeHaven","credit") def fail(msg): raise SystemExit(f"snapshot INVALID: {msg}") def validate(path): d = json.loads(Path(path).read_text(encoding="utf-8")) if "meta" not in d or "views" not in d: fail("missing meta or views") m = d["meta"] for k in ("thresholds","componentOrder","operator"): if k not in m: fail(f"meta missing {k}") for u in UNIVERSES: if u not in d["views"]: fail(f"missing universe '{u}'") live = 0 for uid, v in d["views"].items(): for e in v.get("entities", []): live += 1 code = e.get("code","?") if not e.get("oneYear"): fail(f"{code}: empty oneYear") if len(e["oneYear"]) < 60: fail(f"{code}: short oneYear ({len(e['oneYear'])})") lat = e.get("latest") if not lat: fail(f"{code}: missing latest") s = lat.get("score") if s is None or not (0 <= s <= 100): fail(f"{code}: bad score {s}") comp = e.get("components", {}) for c in COMPS: cv = comp.get(c) if cv is not None and not (0 <= cv <= 100): fail(f"{code}: bad {c} {cv}") if live < MIN_LIVE: fail(f"only {live} live instruments (need >= {MIN_LIVE})") print(f"snapshot validated — {live} live instruments across {len(d['views'])} universes") if __name__ == "__main__": validate(Path(sys.argv[1] if len(sys.argv) > 1 else "data/latest-snapshot.json"))