#!/usr/bin/env python3 """Extract the Wizards Table roster from WIZARDS-TABLE.md into clean tables. Lane: Extract (data extraction & tables). Output: - /root/.hermes/wizard-outputs/ledger/wizard-roster.csv - /root/.hermes/wizard-outputs/ledger/roster-summary.json - /root/troupe-standup/wizard-work/ledger/wizard-roster.csv - /root/troupe-standup/wizard-work/ledger/roster-summary.json """ import csv import json import os import re SRC = "/root/troupe-standup/WIZARDS-TABLE.md" OUT_DIR = "/root/.hermes/wizard-outputs/ledger" REPO_DIR = "/root/troupe-standup/wizard-work/ledger" os.makedirs(OUT_DIR, exist_ok=True) os.makedirs(REPO_DIR, exist_ok=True) with open(SRC, "r", encoding="utf-8") as f: lines = f.read().splitlines() # Locate the roster header row. header_idx = None for i, ln in enumerate(lines): cells = [c.strip() for c in ln.split("|")] cells = [c for c in cells if c != ""] if cells == ["Wizard", "Model", "Lane", "Latest check-in", "Current unit"]: header_idx = i break if header_idx is None: raise SystemExit("roster header not found") # Collect data rows that follow the header + separator, until a non-table line. rows = [] j = header_idx + 2 # skip header, skip separator while j < len(lines): ln = lines[j].strip() if not ln: j += 1 continue cells = [c.strip() for c in ln.split("|")] cells = [c for c in cells if c != ""] # separator line (all dashes) -> stop if all(re.fullmatch(r"-+", c) for c in cells) and len(cells) >= 2: break # stop at any heading or non-table line if ln.startswith("#"): break if len(cells) == 5: rows.append(cells) j += 1 expected_cols = ["Wizard", "Model", "Lane", "Latest check-in", "Current unit"] assert len(rows) == 10, f"expected 10 roster rows, got {len(rows)}" for r in rows: assert len(r) == 5, f"row has {len(r)} cells: {r}" assert r[0] == r[0].strip() and r[0], f"empty wizard name: {r}" wizards = [ {"Wizard": r[0], "Model": r[1], "Lane": r[2], "Latest check-in": r[3], "Current unit": r[4]} for r in rows ] # ---- CSV ---- csv_path = os.path.join(REPO_DIR, "wizard-roster.csv") with open(csv_path, "w", newline="", encoding="utf-8") as f: w = csv.DictWriter(f, fieldnames=expected_cols, lineterminator="\n") w.writeheader() w.writerows(wizards) # ---- Summary JSON ---- staffed = [w["Wizard"] for w in wizards if "Staffed" in w["Current unit"] or "Staffed" in w["Lane"] or "staffed" in w["Current unit"].lower()] staffed = [w["Wizard"] for w in wizards if "2026-10-15" in w["Latest check-in"]] staffed += [w["Wizard"] for w in wizards if "Staffed" in w["Current unit"]] staffed = sorted(set(staffed)) summary = { "source": SRC, "extract_at": "2026-10-21", "total_wizards": len(wizards), "wizards": wizards, "staffed_count": len(staffed), "staffed": staffed, "lanes": sorted(set(w["Lane"] for w in wizards)), "checkin_dates": sorted(set(w["Latest check-in"] for w in wizards if w["Latest check-in"] != "— (replies daily)")), } sum_path = os.path.join(REPO_DIR, "roster-summary.json") with open(sum_path, "w", encoding="utf-8") as f: json.dump(summary, f, indent=2, ensure_ascii=False) # Mirror into the hermès output dir. csv_out = os.path.join(OUT_DIR, "wizard-roster.csv") sum_out = os.path.join(OUT_DIR, "roster-summary.json") for s, d in [(csv_path, csv_out), (sum_path, sum_out)]: with open(s, "rb") as f: data = f.read() with open(d, "wb") as f: f.write(data) print("rows extracted:", len(rows)) print("staffed:", staffed) print("lanes:", summary["lanes"]) print("wizard dates:", summary["checkin_dates"]) print("wrote:", csv_path, sum_path)