"""Render markdown table fragments from /_all_fields.csv. Local file processing only; no DB access. Produces under /tables/: _onlayout.md per-bucket tables for layout fields _offlayout.md dead+writable (agent ghost-write surface) and active-but-hidden summary.md per-object bucket counts regional.md fields carrying region markers in name or label collisions.md label / API-name collision groups diff_baseline.md bucket moves vs --baseline DIR (optional; expects _all_fields.csv or _classified.csv per object) Usage: make_tables.py [OBJECT ...] [--out DIR] [--baseline DIR] """ from __future__ import annotations import collections import csv import json import re import sys from pathlib import Path from common import atomic_write, parse_args REGIONAL_PAT = re.compile( r"(NA[ _/&]?Only|For[ _]NA|APAC|EMEA|LATAM|Japan|JP[ _]|for[ _]?AWS|AWS[ _]|AliCloud|NA&EMEA|Domestic)", re.I, ) # Headings reuse the internal bucket keys verbatim so a grep in the CSVs and a # heading in the tables always mean the same thing. BUCKET_LABEL = { "A-empty-both": "A-empty-both — never filled in either cohort", "B2-bool-never-true": "B2-bool-never-true — boolean never true", "B-legacy-only": "B-legacy-only — legacy data, native fill 0", "D-low-fill": "D-low-fill — both cohorts <5%", "C-auto": "C-auto — formula/rollup/audit, not hand-filled", "E-no-data": "E-no-data — no physical column", "U-indeterminate-no-native": "U-indeterminate-no-native — object has no native rows yet", "F-active": "F-active — in use, keep", } BUCKET_ORDER = [ "A-empty-both", "B2-bool-never-true", "B-legacy-only", "D-low-fill", "C-auto", "E-no-data", "U-indeterminate-no-native", "F-active", ] HDR = "| Field | Label | Type | Migrated fill | Native fill | Required | Dependency refs |\n|---|---|---|---|---|---|---|" def esc(s: str) -> str: return (s or "").replace("|", "\\|") def row_md(r) -> str: req = "**yes**" if r["required"] == "True" else "" return ( f"| `{esc(r['field'])}` | {esc(r['label'])} | {r['type']} | {r['mig_pct']}% | {r['nat_pct']}% | {req} | {esc(r['deps'])} |" ) def load_baseline(baseline: Path, obj: str) -> dict[str, dict]: for name in (f"{obj}_all_fields.csv", f"{obj}_classified.csv"): p = baseline / name if p.exists(): reader = csv.DictReader(open(p)) need = {"field", "label", "bucket"} if not need.issubset(reader.fieldnames or []): # A malformed baseline must fail the run, not silently diff as # "0 bucket moves". print(f"ERROR: baseline {p} lacks columns {sorted(need)}") raise SystemExit(2) return {r["field"]: r for r in reader} return {} def main() -> None: objects, out_dir, baseline = parse_args(sys.argv[1:]) tables = out_dir / "tables" tables.mkdir(parents=True, exist_ok=True) manifest_file = out_dir / "manifest.json" if not manifest_file.exists(): print(f"ERROR: {manifest_file} not found — run probe_full_audit.py first (or the probe crashed mid-run)") raise SystemExit(2) probed = set(json.loads(manifest_file.read_text()).get("probed", [])) fresh: dict[str, list[dict]] = {} for obj in objects: p = out_dir / f"{obj}_all_fields.csv" if obj not in probed: print(f"{obj}: not in this run's probe manifest, skipped") continue if not p.exists(): print(f"{obj}: no {p}, skipped (run classify_full.py first)") continue fresh[obj] = list(csv.DictReader(open(p))) objects = [o for o in objects if o in fresh] # per-object per-bucket tables for obj in objects: rows = fresh[obj] on = [r for r in rows if r["on_layout"] == "True"] off = [r for r in rows if r["on_layout"] != "True"] parts = [] for b in BUCKET_ORDER: grp = [r for r in on if r["bucket"] == b] if not grp: continue grp.sort(key=lambda r: -(float(r["mig_pct"] or 0))) parts.append(f"\n#### {BUCKET_LABEL[b]} ({len(grp)})\n\n{HDR}") parts.extend(row_md(r) for r in grp) atomic_write(tables / f"{obj}_onlayout.md", "\n".join(parts)) dead = [ r for r in off if r["bucket"] in ("A-empty-both", "B2-bool-never-true", "B-legacy-only") and (r["createable"] == "True" or r["updateable"] == "True") ] dead.sort(key=lambda r: (r["bucket"], -(float(r["mig_pct"] or 0)))) parts = [f"\n#### {obj}: off-layout, writable, dead ({len(dead)}) — agent ghost-write surface\n\n{HDR}"] parts.extend(row_md(r) for r in dead) active_off = [r for r in off if r["bucket"] == "F-active"] active_off.sort(key=lambda r: -(float(r["nat_pct"] or 0))) parts.append(f"\n#### {obj}: off-layout but active ({len(active_off)}) — invisible to humans, data moving\n\n{HDR}") parts.extend(row_md(r) for r in active_off) atomic_write(tables / f"{obj}_offlayout.md", "\n".join(parts)) # summary # Column headers are the exact internal bucket keys so they cannot drift # from the values below them; every bucket has a column so the on-layout # counts sum to the "On layout" column. sum_buckets = BUCKET_ORDER lines = [ "| Object | Fields | On layout | " + " | ".join(sum_buckets) + " | Off layout | Off dead+writable | Off active |", "|" + "---|" * (len(sum_buckets) + 6), ] for obj in objects: rows = fresh[obj] on = [r for r in rows if r["on_layout"] == "True"] off = [r for r in rows if r["on_layout"] != "True"] c = collections.Counter(r["bucket"] for r in on) offdead = sum( 1 for r in off if r["bucket"] in ("A-empty-both", "B2-bool-never-true", "B-legacy-only") and (r["createable"] == "True" or r["updateable"] == "True") ) offact = sum(1 for r in off if r["bucket"] == "F-active") bucket_cells = " | ".join(str(c[b]) for b in sum_buckets) lines.append( f"| {obj} | {len(rows)} | {len(on)} | {bucket_cells} | {len(off)} | {offdead} | {offact} |" ) atomic_write(tables / "summary.md", "\n".join(lines)) # regional scan parts = [ "| Object | Field | Label | Type | Layout | Bucket | Migrated fill | Native fill | Dependency refs |", "|---|---|---|---|---|---|---|---|---|", ] for obj in objects: hits = [r for r in fresh[obj] if REGIONAL_PAT.search(r["field"]) or REGIONAL_PAT.search(r["label"])] hits.sort(key=lambda r: (r["bucket"], -(float(r["mig_pct"] or 0)))) for r in hits: onl = "on" if r["on_layout"] == "True" else "off" parts.append( f"| {obj} | `{esc(r['field'])}` | {esc(r['label'])} | {r['type']} | {onl} | {r['bucket']} | {r['mig_pct']}% | {r['nat_pct']}% | {esc(r['deps'])} |" ) atomic_write(tables / "regional.md", "\n".join(parts)) # label & API-name collisions parts = [] total_groups = 0 for obj in objects: rows = fresh[obj] bylabel = collections.defaultdict(list) for r in rows: norm = re.sub(r"[^a-z0-9]", "", r["label"].lower()) if norm: bylabel[norm].append(r) byname = collections.defaultdict(list) for r in rows: norm = re.sub(r"(__c$)", "", r["field"].lower()).replace("_", "") byname[norm].append(r) groups = {} for norm, grp in list(bylabel.items()) + list(byname.items()): if len(grp) > 1: key = tuple(sorted(x["field"] for x in grp)) groups[key] = grp if not groups: continue total_groups += len(groups) parts.append(f"\n#### {obj} ({len(groups)} groups)\n") parts.append("| Group | Field | Label | Layout | Bucket | Migrated fill | Native fill | Writable | Dependency refs |") parts.append("|---|---|---|---|---|---|---|---|---|") for i, (key, grp) in enumerate(sorted(groups.items()), 1): for r in sorted(grp, key=lambda x: x["field"]): onl = "on" if r["on_layout"] == "True" else "off" wr = "yes" if (r["createable"] == "True" or r["updateable"] == "True") else "read-only" parts.append( f"| G{i} | `{esc(r['field'])}` | {esc(r['label'])} | {onl} | {r['bucket']} | {r['mig_pct']}% | {r['nat_pct']}% | {wr} | {esc(r['deps'])} |" ) atomic_write(tables / "collisions.md", "\n".join(parts)) print(f"collision groups: {total_groups}") # baseline diff (optional) if baseline: if not baseline.is_dir(): print(f"ERROR: baseline directory {baseline} does not exist") raise SystemExit(2) if any(baseline.glob("run-*")): print( f"WARNING: baseline {baseline} contains run-* subdirectories — it looks like an " "output ROOT, not a run directory; point --baseline at a specific run- dir" ) parts = [ "| Object | Field | Label | Baseline bucket | Current bucket | Current native fill |", "|---|---|---|---|---|---|", ] moved = 0 matched_objects = 0 for obj in objects: base = load_baseline(baseline, obj) if not base: print(f"baseline: no data for {obj} in {baseline}") continue matched_objects += 1 cur = {r["field"]: r for r in fresh[obj]} for api, br in base.items(): fr = cur.get(api) if fr is None: parts.append(f"| {obj} | `{esc(api)}` | {esc(br['label'])} | {br['bucket']} | (gone) | |") moved += 1 continue if br["bucket"] != fr["bucket"]: parts.append( f"| {obj} | `{esc(api)}` | {esc(br['label'])} | {br['bucket']} | {fr['bucket']} | {fr['nat_pct']}% |" ) moved += 1 for api, fr in cur.items(): if api not in base: parts.append(f"| {obj} | `{esc(api)}` | {esc(fr['label'])} | (new) | {fr['bucket']} | {fr['nat_pct']}% |") moved += 1 if matched_objects == 0: print( f"ERROR: nothing to compare against baseline {baseline} — it has no data for any " "requested object (or this run produced no fresh CSVs); refusing to report 0 moves" ) raise SystemExit(2) atomic_write(tables / "diff_baseline.md", "\n".join(parts)) print(f"bucket moves vs baseline: {moved} (objects compared: {matched_objects})") if __name__ == "__main__": main()