#!/usr/bin/env python3 """Validate GODVERSE stock-pack indexes — round-8 E2 QA gate. A bad bake (out-of-range UVs, empty titles, unknown price band, dangling atlas ref) must fail QA loudly instead of rendering blank/garbled sleeves in the dig. Validates every `pipeline/packs/stock__index.json` (the committed pack contract; atlases live on the depot). Called by validate_manifest.py so it runs inside qa.sh gate 3. python3 pipeline/validate_pack.py # exit 0 = all packs valid, 1 = a bad index """ import json, os, sys, glob ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PACKS = os.path.join(ROOT, "pipeline", "packs") KINDS = {"record", "book", "toy"} BANDS = {"bargain", "standard", "collector", "grail"} REQUIRED = ("id", "title", "artist", "price", "price_band", "atlas", "uv") def validate_index(path): errs = [] try: idx = json.load(open(path)) except Exception as e: return [f"{os.path.basename(path)}: does not parse: {e}"] name = os.path.basename(path) if idx.get("kind") not in KINDS: errs.append(f"{name}: bad kind {idx.get('kind')!r}") atlases = set(idx.get("atlases", [])) if not atlases: errs.append(f"{name}: no atlases listed") items = idx.get("items", []) if not items: errs.append(f"{name}: no items") ids = set() for i, it in enumerate(items): miss = [f for f in REQUIRED if f not in it] if miss: errs.append(f"{name}[{i}]: missing {miss}"); continue if not str(it["title"]).strip() or not str(it["artist"]).strip(): errs.append(f"{name}[{it['id']}]: empty title/artist") if it["id"] in ids: errs.append(f"{name}: duplicate id {it['id']}") ids.add(it["id"]) if it["price_band"] not in BANDS: errs.append(f"{name}[{it['id']}]: bad price_band {it['price_band']!r}") if not isinstance(it["price"], (int, float)) or it["price"] < 0: errs.append(f"{name}[{it['id']}]: bad price {it['price']!r}") if it["atlas"] not in atlases: errs.append(f"{name}[{it['id']}]: atlas {it['atlas']!r} not in atlases[]") uv = it["uv"] if not (isinstance(uv, list) and len(uv) == 4): errs.append(f"{name}[{it['id']}]: uv must be [u0,v0,u1,v1]"); continue u0, v0, u1, v1 = uv if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}") return errs def main(): idxs = sorted(glob.glob(os.path.join(PACKS, "stock_*_index.json"))) if not idxs: print("pack-QA: no committed pack indexes in pipeline/packs/ — nothing to validate") return 0 all_errs = [] for p in idxs: e = validate_index(p) all_errs += e idx = json.load(open(p)) print(f" {os.path.basename(p):28} kind={idx.get('kind')} items={len(idx.get('items', []))} " f"{'OK' if not e else 'ERR('+str(len(e))+')'}") for e in all_errs: print(f" ERR {e}") if all_errs: print(f"pack-QA FAIL — {len(all_errs)} error(s)") return 1 print(f"pack-QA OK — {len(idxs)} pack index(es) valid") return 0 if __name__ == "__main__": sys.exit(main())