"""Regression harness for _grow_article_downward. Runs the grow function over the saved fixtures of EVERY cached run, with IDENTICAL auxiliary inputs (claimed = union of OTHER articles' members on the page, dateline_starts = None). Because old and new code receive the same inputs, a diff of the two output snapshots isolates exactly what the code change does. Records per article: the grown member-id set and its union bbox, tagged with the source newspaper (from meta.json) so results can be broken down per paper. Usage: python3 _regress_grow.py snapshot.json # snapshot current code python3 _regress_grow.py --diff a.json b.json # compare two snapshots """ import json import re import sys from collections import Counter from pathlib import Path OUT = Path("output") def union_bbox(member_ids, region_map): bs = [region_map[r]["bbox"] for r in member_ids if r in region_map] if not bs: return None return [min(b[0] for b in bs), min(b[1] for b in bs), max(b[2] for b in bs), max(b[3] for b in bs)] def paper_of(run_dir): meta = run_dir / "meta.json" if meta.exists(): try: fn = json.loads(meta.read_text()).get("filename", "") name = re.split(r"[_0-9]", fn)[0].strip() if name: return name except Exception: pass # Cached fixtures carry no meta.json; identify the paper by page-1 image height, # which is distinct per edition (verified against the printed masthead nameplate). png = run_dir / "pages" / "page_001.png" if png.exists(): try: from PIL import Image h = Image.open(png).height if 7040 <= h <= 7060: return "Sakshi" if h in (6833, 6549): return "Andhra Jyothi" if h == 6422: return "Namaste Telangana" except Exception: pass return "unknown" def run(): from smart_extractor import _grow_article_downward runs = sorted([d for d in OUT.iterdir() if d.is_dir()], key=lambda d: d.stat().st_mtime, reverse=True) snapshot = {} papers = Counter() for run_dir in runs: pages_dir = run_dir / "pages" if not pages_dir.exists(): continue paper = paper_of(run_dir) for reg_path in sorted(pages_dir.glob("page_*.regions.json")): pol_path = reg_path.with_name(reg_path.name.replace(".regions.json", ".political_result.json")) if not pol_path.exists(): continue regions = json.loads(reg_path.read_text())["regions"] region_map = {r["id"]: r for r in regions} political = json.loads(pol_path.read_text()).get("political_articles", []) # claimed = every member of every article (mirrors the real caller's # global claimed set built before growth). all_members = set() for a in political: all_members.update(a.get("member_region_ids", [])) for i, art in enumerate(political): members = art.get("member_region_ids", []) or [] if not members: continue # claimed for THIS article = everything claimed by OTHER articles. claimed = set(all_members) - set(members) grown, added = _grow_article_downward( members, region_map, regions, claimed, dateline_starts=None ) key = f"{run_dir.name}/{reg_path.stem}/art{i}" snapshot[key] = { "paper": paper, "headline": art.get("headline_english", "")[:50], "in_members": sorted(members), "grown_members": sorted(grown), "added": sorted(added), "in_bbox": union_bbox(members, region_map), "grown_bbox": union_bbox(grown, region_map), } papers[paper] += 1 print(f" papers: {dict(papers)}") return snapshot def diff(path_a, path_b): """Compare snapshot A (baseline/OLD) against B (NEW). Reports per-paper how many articles changed their grown member set, plus a sample of the changes.""" a = json.loads(Path(path_a).read_text()) b = json.loads(Path(path_b).read_text()) keys = sorted(set(a) | set(b)) per_paper_total = Counter() per_paper_changed = Counter() grew = Counter() # NEW added regions OLD didn't shrank = Counter() # NEW dropped regions OLD had samples = [] for k in keys: ra, rb = a.get(k), b.get(k) paper = (rb or ra).get("paper", "unknown") per_paper_total[paper] += 1 if ra is None or rb is None: per_paper_changed[paper] += 1 samples.append((k, paper, "ONLY IN ONE SNAPSHOT", None, None)) continue sa, sb = set(ra["grown_members"]), set(rb["grown_members"]) if sa != sb: per_paper_changed[paper] += 1 added = sorted(sb - sa) removed = sorted(sa - sb) if added: grew[paper] += 1 if removed: shrank[paper] += 1 samples.append((k, paper, rb["headline"], added, removed)) print("\n=== REGRESSION DIFF (A=baseline/OLD → B=new) ===") print(f"{'paper':<18}{'articles':>10}{'changed':>9}{'grew':>7}{'shrank':>8}") for paper in sorted(per_paper_total): print(f"{paper:<18}{per_paper_total[paper]:>10}{per_paper_changed[paper]:>9}" f"{grew[paper]:>7}{shrank[paper]:>8}") tot = sum(per_paper_total.values()) chg = sum(per_paper_changed.values()) print(f"{'TOTAL':<18}{tot:>10}{chg:>9}{sum(grew.values()):>7}{sum(shrank.values()):>8}") if samples: print(f"\n--- {len(samples)} changed article(s) (showing up to 40) ---") for k, paper, hl, added, removed in samples[:40]: print(f"\n[{paper}] {k}") print(f" headline: {hl}") if added: print(f" + added : {added}") if removed: print(f" - removed: {removed}") else: print("\nNo differences — grown member sets are identical across all fixtures.") return chg if __name__ == "__main__": if len(sys.argv) >= 4 and sys.argv[1] == "--diff": diff(sys.argv[2], sys.argv[3]) else: out_path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/grow_snapshot.json" snap = run() Path(out_path).write_text(json.dumps(snap, indent=2, ensure_ascii=False)) print(f"wrote {len(snap)} article snapshots to {out_path}")