85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Ledger — independent re-derivation check for the replies index (2026-11-02).
|
|
|
|
Exits 0 only if every row in the catalog TSV is well-formed AND the
|
|
per-recipient/per-day counts in the summary MD match the catalog.
|
|
Does not touch the git tree.
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
|
|
CATALOG = "/root/troupe-standup/wizard-work/ledger/replies-catalog-2026-11-02.tsv"
|
|
SUMMARY = "/root/troupe-standup/wizard-work/ledger/replies-catalog-2026-11-02.md"
|
|
|
|
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
FILENAME_RE = re.compile(
|
|
r"^\d{4}-\d{2}-\d{2}-to-[A-Za-z0-9_-]+(?:-(\d+|interview|preview))?\.md$"
|
|
)
|
|
|
|
def main():
|
|
problems = []
|
|
|
|
with open(CATALOG, encoding="utf-8") as f:
|
|
lines = [ln for ln in f.read().split("\n") if ln.strip() != ""]
|
|
|
|
header = lines[0]
|
|
if header != "date\trecipient\tnumber\tcategory\tfile":
|
|
problems.append("bad header: %r" % header)
|
|
|
|
data = lines[1:]
|
|
if len(data) != 430:
|
|
problems.append("expected 430 data rows, got %d" % len(data))
|
|
|
|
seen_files = set()
|
|
recip_counts = Counter()
|
|
date_counts = Counter()
|
|
cat_counts = Counter()
|
|
for i, ln in enumerate(data, start=2):
|
|
parts = ln.split("\t")
|
|
if len(parts) != 5:
|
|
problems.append("line %d: %d columns" % (i, len(parts)))
|
|
continue
|
|
date, recip, number, category, file = parts
|
|
if not DATE_RE.match(date):
|
|
problems.append("line %d: bad date %r" % (i, date))
|
|
if not FILENAME_RE.match(file):
|
|
problems.append("line %d: bad file %r" % (i, file))
|
|
if not recip:
|
|
problems.append("line %d: empty recipient" % i)
|
|
if file in seen_files:
|
|
problems.append("duplicate file %r" % file)
|
|
seen_files.add(file)
|
|
recip_counts[recip] += 1
|
|
date_counts[date] += 1
|
|
cat_counts[category] += 1
|
|
|
|
with open(SUMMARY, encoding="utf-8") as f:
|
|
smd = f.read()
|
|
|
|
if "total" not in smd:
|
|
problems.append("summary missing a total row")
|
|
for recip, n in recip_counts.items():
|
|
if not re.search(r"\|\s*%s\s*\|\s*%d\s*\|" % (re.escape(recip), n), smd):
|
|
problems.append("summary missing/mismatched per-recipient row for %r" % recip)
|
|
|
|
if sum(recip_counts.values()) != 430:
|
|
problems.append("recipient counts sum != 430")
|
|
if sum(date_counts.values()) != 430:
|
|
problems.append("date counts sum != 430")
|
|
if dict(cat_counts) != {"reply": 430}:
|
|
problems.append("unexpected category mix: %r" % dict(cat_counts))
|
|
|
|
if problems:
|
|
print("FAIL (%d problems)" % len(problems))
|
|
for p in problems[:25]:
|
|
print(" -", p)
|
|
return 1
|
|
|
|
print("OK: %d rows, all well-formed, summary consistent" % len(data))
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|