#!/usr/bin/env python3 """Diff hub-route SHAPES between production (known-good) and staging. Values legitimately differ (different clubs, different piles). What must not differ is structure: a key production sends that staging omits, or a type/null mismatch, is a candidate cause for the client rejecting FUT bootstrap. """ import json import os PROD, STAG = "/tmp/hub_prod", "/tmp/hub_stag" def t(v): return "null" if v is None else type(v).__name__ def shape(obj, prefix="", depth=0): """path -> type, descending into the FIRST element of lists (representative).""" out = {} if depth > 6: return out if isinstance(obj, dict): for k, v in obj.items(): p = f"{prefix}.{k}" if prefix else k out[p] = t(v) out.update(shape(v, p, depth + 1)) elif isinstance(obj, list): out[(prefix or "") + "[]"] = "list" if obj: out.update(shape(obj[0], f"{prefix}[0]", depth + 1)) return out total = 0 for name in sorted(os.listdir(PROD)): pf, sf = os.path.join(PROD, name), os.path.join(STAG, name) if not os.path.exists(sf): print(f"{name}: MISSING on staging") continue p = json.load(open(pf)) s = json.load(open(sf)) ps, ss = shape(p.get("body")), shape(s.get("body")) missing = [k for k in ps if k not in ss] types = [(k, ps[k], ss[k]) for k in ps if k in ss and ps[k] != ss[k]] nulls = [k for k in ps if k in ss and ss[k] == "null" and ps[k] != "null"] if p.get("status") != s.get("status"): print(f"\n### {name}: STATUS prod={p.get('status')} staging={s.get('status')}") total += 1 if missing or types: print(f"\n### {name}") for k in missing: print(f" MISSING on staging: {k:<44} prod_type={ps[k]}") for k, a, b in types: mark = " <-- NULL" if b == "null" else "" print(f" TYPE {k:<48} prod={a:<7} staging={b}{mark}") total += len(missing) + len(types) print(f"\n=== {total} structural difference(s) across {len(os.listdir(PROD))} routes ===") if total == 0: print("Every hub route matches production's shape. The bootstrap failure is not a") print("missing/mistyped field in these responses.")