#!/usr/bin/env python3 """Ledger — independent re-derivation check for the git commit census. Re-derives commits-census.csv and commits-by-day.csv cell-by-cell from git at HEAD (not from the committed tables), then asserts the committed tables match exactly. Also checks commits-full.csv has one row per commit. Exit code is the verdict: 0 tables match git 1 mismatch 2 ungradable (git unreadable) """ from pathlib import Path import csv import subprocess import sys REPO = Path(__file__).resolve().parents[2] OUT = Path(__file__).resolve().parents[2] / 'wizard-work' / 'ledger' # Pinned revision (must match extract_commits_census.py PIN). PIN = "d8d8e3292db6c435b174e39a3ca18792874f9c19" SEP = "\x1f" FMT = "%H" + SEP + "%an" + SEP + "%ae" + SEP + "%aI" + SEP + "%s" CENSUS = "commits-census.csv" BY_DAY = "commits-by-day.csv" FULL = "commits-full.csv" def git_log(): cmd = ["git", "-C", REPO, "log", "--pretty=format:" + FMT, "--date=iso-strict", PIN] out = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout rows = [] for c in out.splitlines(): if not c: continue h, an, ae, ad, s = c.split(SEP) rows.append({"hash": h, "author": an, "email": ae, "author_date": ad, "subject": s}) return rows def read_csv(name): path = f"{OUT}/{name}" with open(path, newline="", encoding="utf-8") as f: return list(csv.reader(f)) def close(a, b): try: return float(a) == float(b) except (TypeError, ValueError): return a == b def main(): rows = git_log() total = len(rows) by_author = {} for r in rows: by_author[r["author"]] = by_author.get(r["author"], 0) + 1 by_day = {} for r in rows: d = r["author_date"][:10] by_day[d] = by_day.get(d, 0) + 1 problems = [] # --- commits-census.csv --- try: census = read_csv(CENSUS) except Exception as e: print("UNGRADABLE: cannot read commits-census.csv:", e) return 2 if census[0] != ["author", "commits"]: problems.append("census header: %r" % census[0]) expected_census = sorted(by_author.items(), key=lambda kv: (-kv[1], kv[0])) if len(census) - 1 != len(expected_census): problems.append("census rows %d != expected %d" % (len(census) - 1, len(expected_census))) for (act, exp) in zip(census[1:], expected_census): if not close(act[1], str(exp[1])): problems.append("census author %r commits %r != %r" % (act[0], act[1], exp[1])) # --- commits-by-day.csv --- try: days = read_csv(BY_DAY) except Exception as e: print("UNGRADABLE: cannot read commits-by-day.csv:", e) return 2 if days[0] != ["date", "commits"]: problems.append("by-day header: %r" % days[0]) expected_days = sorted(by_day.items()) if len(days) - 1 != len(expected_days): problems.append("by-day rows %d != expected %d" % (len(days) - 1, len(expected_days))) for (act, exp) in zip(days[1:], expected_days): if not close(act[1], str(exp[1])): problems.append("by-day %s commits %r != %r" % (act[0], act[1], exp[1])) # --- commits-full.csv row count --- try: full = read_csv(FULL) except Exception as e: print("UNGRADABLE: cannot read commits-full.csv:", e) return 2 if len(full) - 1 != total: problems.append("full rows %d != commit count %d" % (len(full) - 1, total)) if problems: print("FAIL (%d problems)" % len(problems)) for p in problems[:30]: print(" -", p) return 1 print("PASS: commits-census.csv and commits-by-day.csv match git at HEAD; " "commits-full.csv has %d rows; total %d commits" % (len(full) - 1, total)) return 0 if __name__ == "__main__": sys.exit(main())