E1: build_stock_pack.py generalized over kinds — record (256x256 sleeves), book (96x256 tall-thin spines), toy (256x256 boxes), per-kind parody word banks, same build-time/parody/atlas/index laws. Book+toy sample packs built + staged (?localdepot) + indexes committed to pipeline/packs/ so Lane C wires book/toy shelves now. Parody deterministic + no distinctive-token leak across all 3 kinds. E2: validate_pack.py QA gate (wired into validate_manifest -> qa gate 3) — bad UV / empty title / bad price_band / dangling atlas / dup id fails qa (proven). qa strict GREEN 5/5. Real 150-250-item packs blocked on GODVERSE_DSN (flagged to Fable); samples are placeholders in the real index format. No fal spend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
#!/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_<kind>_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())
|