236 lines
8.6 KiB
Python
236 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract the live wizard roster (2026-10-29) as a data table.
|
|
|
|
Lane: Extract (data extraction and tables).
|
|
|
|
Sources (real, from this checkout host):
|
|
1. WIZARDS-TABLE.md -- canonical wizard roster (name, model, lane).
|
|
2. checkins/ -- newest check-in per wizard (max date in filename).
|
|
3. git log -- newest commit per wizard (author-date).
|
|
|
|
Outputs (all written under OUT, default /root/troupe-standup/wizard-work/ledger):
|
|
live-wizard-roster-2026-10-29.tsv -- summary table (TSV)
|
|
live-wizard-roster-2026-10-29.csv -- full file manifest (CSV)
|
|
live-wizard-roster-2026-10-29.md -- markdown table (human artifact)
|
|
live-wizard-roster-2026-10-29.json -- manifest + tallies
|
|
|
|
The CHECK file (CHECK-live-wizard-roster-2026-10-29) re-derives every row from
|
|
the three sources with independent logic and diffs against the committed TSV.
|
|
The verdict is the script's exit code: 0 = correct, nonzero = wrong.
|
|
"""
|
|
|
|
import csv
|
|
import json
|
|
import os
|
|
import subprocess
|
|
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(os.environ.get("WIZARDWORK_ROOT", "/root/troupe-standup"))
|
|
WIZARD_WORK = REPO_ROOT / "wizard-work" / "ledger"
|
|
OUT = Path(os.environ.get("WIZARD_ROSTER_OUT", str(WIZARD_WORK)))
|
|
|
|
TABLE = REPO_ROOT / "WIZARDS-TABLE.md"
|
|
CHKINS_DIR = REPO_ROOT / "checkins"
|
|
|
|
SUMMARY_NAME = "live-wizard-roster-2026-10-29.tsv"
|
|
MANIFEST_NAME = "live-wizard-roster-2026-10-29.csv"
|
|
MD_NAME = "live-wizard-roster-2026-10-29.md"
|
|
JSON_NAME = "live-wizard-roster-2026-10-29.json"
|
|
|
|
# Canonical wizard names, fixed by WIZARDS-TABLE.md roster table.
|
|
WIZARDS = [
|
|
"Timmy",
|
|
"Zephyrus Sparkborn",
|
|
"Cassian Assayborn",
|
|
"Merlin",
|
|
"Mercury",
|
|
"Gemini",
|
|
"Argus",
|
|
"Volt",
|
|
"Sentinel",
|
|
"Beam",
|
|
]
|
|
|
|
# checkins/ filename slug -> canonical wizard (prefix rules first).
|
|
def slug_to_wizard(slug):
|
|
if slug.startswith("cassian"):
|
|
return "Cassian Assayborn"
|
|
return {
|
|
"zephyrus": "Zephyrus Sparkborn",
|
|
"merlin": "Merlin",
|
|
"mercury": "Mercury",
|
|
"gemini": "Gemini",
|
|
"volt": "Volt",
|
|
"sentinel": "Sentinel",
|
|
}.get(slug)
|
|
|
|
# git log author string -> canonical wizard (author casing varies in the repo).
|
|
AUTHOR_TO_WIZARD = {}
|
|
for _w in WIZARDS:
|
|
for _a in [_w.lower(), _w]:
|
|
AUTHOR_TO_WIZARD[_a] = _w
|
|
for _w, _aliases in {
|
|
"Cassian Assayborn": ["Cassian", "Cassian Assayborn"],
|
|
"Zephyrus Sparkborn": ["zephyrus sparkborn", "zephyrus-sparkborn"],
|
|
}.items():
|
|
for _a in _aliases:
|
|
AUTHOR_TO_WIZARD[_a.lower()] = _w
|
|
|
|
def canonical_wizard(author):
|
|
return AUTHOR_TO_WIZARD.get(author.lower())
|
|
|
|
def parse_roster():
|
|
"""Return {wizard: (model, lane, latest_checkin_in_table, current_unit)}"""
|
|
rows = {}
|
|
with open(TABLE, encoding="utf-8") as fh:
|
|
lines = fh.read().splitlines()
|
|
in_table = False
|
|
for line in lines:
|
|
if line.startswith("| Wizard |"):
|
|
in_table = True
|
|
continue
|
|
if in_table:
|
|
if not line.startswith("|"):
|
|
break
|
|
cells = [c.strip() for c in line.split("|") if c.strip()]
|
|
if len(cells) == 5 and cells[0] != "Wizard":
|
|
rows[cells[0]] = {
|
|
"model": cells[1],
|
|
"lane": cells[2],
|
|
"latest_checkin": cells[3],
|
|
"current_unit": cells[4],
|
|
}
|
|
return rows
|
|
|
|
def newest_checkin_per_wizard():
|
|
"""{wizard: 'YYYY-MM-DD' or None}"""
|
|
cur = {}
|
|
for name in WIZARDS:
|
|
cur[name] = None
|
|
if not CHKINS_DIR.is_dir():
|
|
return cur
|
|
for fh in CHKINS_DIR.iterdir():
|
|
if not fh.is_file() or not fh.name.endswith(".md"):
|
|
continue
|
|
parts = fh.name[:-3].split("-", 3)
|
|
if len(parts) < 4:
|
|
continue
|
|
date, slug = "-".join(parts[:3]), parts[3]
|
|
w = slug_to_wizard(slug)
|
|
if w is None:
|
|
continue
|
|
if cur[w] is None or date > cur[w]:
|
|
cur[w] = date
|
|
return cur
|
|
|
|
def newest_commit_per_wizard():
|
|
"""{wizard: 'HASH DATE' or None}"""
|
|
cur = {w: None for w in WIZARDS}
|
|
out = subprocess.run(
|
|
["git", "log", "--format=%an%x00%H%x00%ad", "--date=short"],
|
|
cwd=REPO_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout
|
|
best = defaultdict(lambda: None) # wizard -> (date, hash)
|
|
for line in out.splitlines():
|
|
if "\x00" not in line:
|
|
continue
|
|
author, hsh, date = line.split("\x00", 2)
|
|
w = canonical_wizard(author)
|
|
if w is None or w not in cur:
|
|
continue
|
|
prev = best[w]
|
|
if prev is None or date > prev[0]:
|
|
best[w] = (date, hsh)
|
|
for w in WIZARDS:
|
|
if best[w] is not None:
|
|
cur[w] = best[w][1] + " " + best[w][0]
|
|
return cur
|
|
|
|
def build_rows():
|
|
roster = parse_roster()
|
|
checkins = newest_checkin_per_wizard()
|
|
commits = newest_commit_per_wizard()
|
|
rows = []
|
|
for w in WIZARDS:
|
|
r = roster.get(w, {})
|
|
rows.append({
|
|
"wizard": w,
|
|
"model": r.get("model", "—"),
|
|
"lane": r.get("lane", "—"),
|
|
"latest_checkin": checkins[w] or "— (no check-in)",
|
|
"newest_commit": commits[w] or "— (no commit)",
|
|
})
|
|
return rows
|
|
|
|
def main():
|
|
rows = build_rows()
|
|
|
|
# --- TSV (summary) ------------------------------------------------------
|
|
tsv_path = OUT / SUMMARY_NAME
|
|
with open(tsv_path, "w", encoding="utf-8", newline="") as fh:
|
|
w = csv.writer(fh, delimiter="\t", lineterminator="\n")
|
|
w.writerow(["Wizard", "Model", "Lane", "Latest check-in", "Newest commit"])
|
|
for r in rows:
|
|
w.writerow([r["wizard"], r["model"], r["lane"],
|
|
r["latest_checkin"], r["newest_commit"]])
|
|
|
|
# --- CSV manifest ---------------------------------------------------------
|
|
manifest_path = OUT / MANIFEST_NAME
|
|
with open(manifest_path, "w", encoding="utf-8", newline="") as fh:
|
|
w = csv.writer(fh, delimiter=",", lineterminator="\n")
|
|
w.writerow(["source", "path", "kind", "bytes", "modified_utc"])
|
|
for label, p in [
|
|
("table", TABLE),
|
|
("checkins", CHKINS_DIR),
|
|
("git", REPO_ROOT / ".git"),
|
|
]:
|
|
if p.is_dir():
|
|
for f in sorted(p.rglob("*")):
|
|
if f.is_file():
|
|
wt = datetime.fromtimestamp(f.stat().st_mtime, timezone.utc)
|
|
w.writerow([label, str(f.relative_to(REPO_ROOT)),
|
|
"file", f.stat().st_size,
|
|
wt.strftime("%Y-%m-%dT%H:%M:%SZ")])
|
|
else:
|
|
wt = datetime.fromtimestamp(p.stat().st_mtime, timezone.utc) if p.exists() else None
|
|
w.writerow([label, str(p), "file",
|
|
p.stat().st_size if p.exists() else 0,
|
|
wt.strftime("%Y-%m-%dT%H:%M:%SZ") if wt else ""])
|
|
|
|
# --- MD -------------------------------------------------------------------
|
|
md_path = OUT / MD_NAME
|
|
with open(md_path, "w", encoding="utf-8") as fh:
|
|
fh.write("# Live wizard roster (2026-10-29)\n\n")
|
|
fh.write("Evidence: roster from `WIZARDS-TABLE.md`; latest check-in from "
|
|
"`checkins/` files; newest commit from `git log`.\n\n")
|
|
fh.write("| Wizard | Model | Lane | Latest check-in | Newest commit |\n")
|
|
fh.write("|--------|-------|------|-----------------|---------------|\n")
|
|
for r in rows:
|
|
fh.write(f"| {r['wizard']} | {r['model']} | {r['lane']} "
|
|
f"| {r['latest_checkin']} | {r['newest_commit']} |\n")
|
|
fh.write("\n*Last refreshed 2026-10-29 (UTC).*\n")
|
|
|
|
# --- JSON manifest ----------------------------------------------------------
|
|
counts = {"check-ins": sum(1 for r in rows if r["latest_checkin"] != "— (no check-in)"),
|
|
"newest-commits": sum(1 for r in rows if r["newest_commit"] != "— (no commit)"),
|
|
"rows": len(rows)}
|
|
json_path = OUT / JSON_NAME
|
|
with open(json_path, "w", encoding="utf-8") as fh:
|
|
json.dump({"extracted_at_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"source": "WIZARDS-TABLE.md + checkins/ + git log",
|
|
"rows": rows, "counts": counts}, fh, indent=2)
|
|
|
|
print(f"wrote {tsv_path}")
|
|
print(f"wrote {manifest_path}")
|
|
print(f"wrote {md_path}")
|
|
print(f"wrote {json_path}")
|
|
for r in rows:
|
|
print(f" {r['wizard']:22s} checkin={r['latest_checkin']:22s} commit={r['newest_commit']}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|