184 lines
6.5 KiB
Python
Executable File
184 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""verify_wizards.py — re-derive the full commit-history extraction from live git
|
|
and compare every committed artifact (CSV/JSON/MD) against it. Exits 0 only when
|
|
every check matches exactly. No stubs: every number is re-derived from git."""
|
|
import subprocess, sys, json, csv, collections, os
|
|
|
|
REPO = '/root/troupe-standup'
|
|
OUT = '/root/troupe-standup/wizard-work/ledger'
|
|
CSV = os.path.join(OUT, '2026-10-25-git-history.csv')
|
|
JSON = os.path.join(OUT, '2026-10-25-git-history.json')
|
|
MD = os.path.join(OUT, '2026-10-25-git-history.md')
|
|
|
|
fails = []
|
|
def ok(msg):
|
|
print('PASS: ' + msg)
|
|
def bad(msg):
|
|
print('FAIL: ' + msg)
|
|
fails.append(msg)
|
|
|
|
def git(*args):
|
|
return subprocess.run(['git', '-C', REPO] + list(args),
|
|
check=True, capture_output=True, text=True).stdout
|
|
|
|
def file_count(hashfull):
|
|
out = git('show', '--name-status', '--format=', hashfull)
|
|
n = 0
|
|
for l in out.split('\n'):
|
|
if l and not l.startswith('only'):
|
|
n += 1
|
|
return n
|
|
|
|
# ---- Live ground truth (re-derived from git, nothing trusted) ----
|
|
commits = []
|
|
for line in git('log', '--date=iso-strict',
|
|
'--format=%x1f%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s').split('\n'):
|
|
if not line:
|
|
continue
|
|
f = line.split('\x1f')
|
|
assert len(f) == 7, line
|
|
commits.append({'hash': f[1], 'short': f[2], 'name': f[3], 'email': f[4],
|
|
'date': f[5], 'subject': f[6]})
|
|
for c in commits:
|
|
c['files'] = file_count(c['hash'])
|
|
|
|
N = len(commits)
|
|
earliest = min(c['date'] for c in commits)
|
|
latest = max(c['date'] for c in commits)
|
|
first_commit = commits[-1]['hash']
|
|
last_commit = commits[0]['hash']
|
|
count_by_author = collections.Counter((c['name'], c['email']) for c in commits)
|
|
census = [{'name': n, 'email': e, 'count': cnt}
|
|
for (n, e), cnt in sorted(count_by_author.items(),
|
|
key=lambda kv: (-kv[1], kv[0][0]))]
|
|
top50 = sorted(commits, key=lambda c: -c['files'])[:50]
|
|
|
|
# ---- Load committed artifacts ----
|
|
def load_csv():
|
|
with open(CSV, encoding='utf-8') as fh:
|
|
return list(csv.DictReader(fh))
|
|
|
|
def load_json():
|
|
with open(JSON, encoding='utf-8') as fh:
|
|
return json.load(fh)
|
|
|
|
def load_md_top50():
|
|
rows = []
|
|
with open(MD, encoding='utf-8') as fh:
|
|
for line in fh:
|
|
if line.startswith('| 1 |') or (line.startswith('| ') and '|' in line):
|
|
pass
|
|
# robust: parse any line starting with '| <rank> |'
|
|
import re
|
|
pat = re.compile(r'^\|\s*(\d+)\s*\|\s*`?([0-9a-f]+)`?\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*(\d+)\s*\|')
|
|
out = []
|
|
with open(MD, encoding='utf-8') as fh:
|
|
for line in fh:
|
|
m = pat.match(line)
|
|
if m:
|
|
out.append({'rank': int(m.group(1)), 'hash': m.group(2),
|
|
'author': m.group(3).strip(), 'date': m.group(4).strip(),
|
|
'subject': m.group(5).strip(), 'files': int(m.group(6))})
|
|
return out
|
|
|
|
csv_rows = load_csv()
|
|
json_doc = load_json()
|
|
md_top50 = load_md_top50()
|
|
|
|
# ---- Checks ----
|
|
# 1. Counts
|
|
if len(csv_rows) == N:
|
|
ok('csv data rows = %d == live commit count %d' % (len(csv_rows), N))
|
|
else:
|
|
bad('csv data rows = %d, expected %d' % (len(csv_rows), N))
|
|
if json_doc.get('commits') == N:
|
|
ok('json commits = %d == live count %d' % (json_doc.get('commits'), N))
|
|
else:
|
|
bad('json commits = %r, expected %d' % (json_doc.get('commits'), N))
|
|
|
|
# 2. Earliest / latest
|
|
if earliest == json_doc.get('earliest') == csv_rows[0]['date']:
|
|
ok('earliest = %s' % earliest)
|
|
else:
|
|
bad('earliest declared=%r observed=%r' % (json_doc.get('earliest'), earliest))
|
|
if latest == json_doc.get('latest') == csv_rows[-1]['date']:
|
|
ok('latest = %s' % latest)
|
|
else:
|
|
bad('latest declared=%r observed=%r' % (json_doc.get('latest'), latest))
|
|
|
|
# 3. First / last commits
|
|
if csv_rows[0]['hash'] == last_commit and csv_rows[-1]['hash'] == first_commit:
|
|
ok('csv first/last commits match live HEAD/root')
|
|
else:
|
|
bad('csv first/last mismatch: %r / %r' % (csv_rows[0]['hash'], csv_rows[-1]['hash']))
|
|
if json_doc.get('first_commit') == first_commit and json_doc.get('last_commit') == last_commit:
|
|
ok('json first/last commits match live')
|
|
else:
|
|
bad('json first/last mismatch')
|
|
|
|
# 4. Field-for-field CSV vs live
|
|
bad_rows = 0
|
|
for i, c in enumerate(commits, 1):
|
|
r = csv_rows[i-1]
|
|
live = ['%d' % i, c['hash'], c['short'], c['name'], c['email'], c['date'], c['subject'], '%d' % c['files']]
|
|
if [r.get(k) for k in ('rank','hash','short','name','email','date','subject','files')] != live:
|
|
bad_rows += 1
|
|
if bad_rows <= 3:
|
|
bad('csv row %d mismatch: %r' % (i, r))
|
|
if bad_rows == 0:
|
|
ok('csv field-for-field matches live derivation for all %d rows' % N)
|
|
|
|
# 5. JSON rows vs live
|
|
jrows = json_doc.get('rows', [])
|
|
if len(jrows) == N:
|
|
ok('json rows = %d' % len(jrows))
|
|
else:
|
|
bad('json rows = %d, expected %d' % (len(jrows), N))
|
|
jbad = 0
|
|
for i, c in enumerate(commits, 1):
|
|
j = jrows[i-1]
|
|
live = {'hash': c['hash'], 'short': c['short'], 'name': c['name'],
|
|
'email': c['email'], 'date': c['date'], 'subject': c['subject'],
|
|
'files': c['files']}
|
|
if {k: j.get(k) for k in live} != live:
|
|
jbad += 1
|
|
if jbad <= 3:
|
|
bad('json row %d mismatch' % i)
|
|
if jbad == 0:
|
|
ok('json rows field-for-field match live derivation for all %d rows' % N)
|
|
|
|
# 6. Census
|
|
if json_doc.get('census') == census:
|
|
ok('json census matches live derivation (%d authors)' % len(census))
|
|
else:
|
|
bad('json census mismatch')
|
|
|
|
# 7. Top-50 markdown vs live
|
|
if len(md_top50) == 50:
|
|
ok('md top-50 rows = %d' % len(md_top50))
|
|
else:
|
|
bad('md top-50 rows = %d, expected 50' % len(md_top50))
|
|
mismatch = 0
|
|
for live50 in top50:
|
|
m = next((x for x in md_top50 if x['rank'] == commits.index(live50) + 1), None)
|
|
# top50 order == md order
|
|
if not m or m['hash'] != live50['short'] or m['author'] != live50['name'] or m['date'] != live50['date'] or m['files'] != live50['files']:
|
|
mismatch += 1
|
|
if mismatch <= 3:
|
|
bad('md top-50 row %d mismatch vs live' % live50['rank'])
|
|
if mismatch == 0:
|
|
ok('md Top-50 table matches live re-derivation')
|
|
|
|
# 8. Root commit present & files count
|
|
root = json_doc.get('first_commit')
|
|
if root and git('rev-parse','-q','--verify', root + '^{commit}').strip():
|
|
ok('root commit present: %s' % root)
|
|
else:
|
|
bad('root commit missing from json or not a commit')
|
|
|
|
print('')
|
|
if fails:
|
|
print('CHECK FAIL: %d check(s) failed.' % len(fails))
|
|
sys.exit(1)
|
|
print('CHECK PASS: git commit-history extraction re-derives cleanly against live git.')
|