#!/usr/bin/env python3 """Extract the WIZARDS-TABLE.md roster into wizards.csv plus its pin manifest. WHAT THIS IS `wizards.csv` is a machine-readable transcription of the roster table in `WIZARDS-TABLE.md`, and `wizards.manifest.json` is the pin that makes the transcription falsifiable: the exact source bytes (sha256 + git blob), the commit the blob was read at, the extraction clock, the column list, and the correction markers carried into the CSV. WHY THE 18 ROWS, NOT 10 The table has held ten wizard rows since 2026-10-13 and gained eight lane signer rows on 2026-10-28 (Q124: "lanes sign as themselves, and the rows above put them in the roster"). The earlier extraction stopped at the ten, so the artifact dropped eight rows the table now carries. This extractor takes every roster row and labels which is which in the `Kind` column. The rule is stated, not inferred from position: a row is a `lane signer` when its Model cell carries no model of its own -- the table writes those cells as `-- (lane identity, no model of its own)`; every other row is a `wizard`. `verify_wizards.py` re-implements this rule independently. WHY THE `Correction` COLUMN The table appends a control-plane correction at :90-99 declaring the Zephyrus row "false as written" and withdrawing its escalate-line ("this needs Alexander's one line -- re-task the lane or let it lapse -- not silence"). A transcription that copies the cell and drops the correction re-issues a withdrawn escalation. So every row gets a `Correction` cell: empty when nothing is withdrawn, and exactly the marker named in the manifest's `corrections` table when something is. The withdrawn text and the authority span are data in the manifest, so the rule is a lookup, not a site heuristic that could one day hide a genuine escalation. Pinned source `source` is repo-relative; every path here is derived from this file's own location, so the extractor works in a clone, a worktree or /tmp. Usage: python3 extract_wizards.py # rewrite wizards.csv + manifest python3 extract_wizards.py --check # report what would change, write nothing Exit codes: 0 written (or already current), 1 table unparseable, 2 cannot grade. """ import argparse import csv import hashlib import io import json import os import re import subprocess import sys from datetime import datetime, timezone HERE = os.path.dirname(os.path.realpath(__file__)) ROOT = os.path.dirname(os.path.dirname(HERE)) # wizard-work/ledger -> repo root SOURCE_REL = "WIZARDS-TABLE.md" CSV_NAME = "wizards.csv" MANIFEST_NAME = "wizards.manifest.json" COLUMNS = ["Kind", "Wizard", "Model", "Lane", "Latest check-in", "Current unit", "Correction"] TABLE_HEADER = ["Wizard", "Model", "Lane", "Latest check-in", "Current unit"] # A row whose Model cell has no model of its own is a lane signer (Q124). LANE_SIGNER_MARK = "no model of its own" # Data, not heuristics: the corrections the table appends, by row. CORRECTIONS = [ { "row": "Zephyrus Sparkborn", "column": "Current unit", "marker": ("WITHDRAWN - escalate-line withdrawn by " "WIZARDS-TABLE.md:90-99; the lane was egress-blocked, " "not silent"), "withdrawn_text": ("this needs Alexander's one line — re-task the " "lane or let it lapse — not silence"), "authority_line_start": 90, "authority_line_end": 99, "authority_phrase": "the Zephyrus row above is false as written", }, ] def sha256_bytes(data): return hashlib.sha256(data).hexdigest() def _cells(line): """Split one markdown table line into stripped cells, dropping the edges.""" parts = [c.strip() for c in line.strip().split("|")] if parts and parts[0] == "": parts = parts[1:] if parts and parts[-1] == "": parts = parts[:-1] return parts def parse_roster(text): """Return (rows, stated_date) from the roster table in `text`. rows is a list of [lead, model, lane, latest, unit]; stated_date is the table's own `*Roster as of ...` line when the file carries one. """ lines = text.splitlines() start = None for i, line in enumerate(lines): if _cells(line) == TABLE_HEADER: start = i + 1 break if start is None: raise SystemExit("CANNOT GRADE: roster header not found in %s" % SOURCE_REL) sep = _cells(lines[start]) if start < len(lines) else [] if not sep or not all(set(c) <= set("-: ") for c in sep): raise SystemExit("CANNOT GRADE: no header separator after the roster header") rows = [] for line in lines[start + 1:]: s = line.strip() if not s or s.startswith("#"): break if not s.startswith("|"): break cells = _cells(s) if len(cells) != 5: raise SystemExit("CANNOT GRADE: roster row is not 5 cells: %r" % s[:80]) rows.append(cells) stated = None for line in lines: if "roster as of" in line.lower(): m = re.search(r"\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}Z)?", line) if m: stated = m.group(0) break return rows, stated def kind_of(model_cell): return "lane signer" if LANE_SIGNER_MARK in model_cell else "wizard" def build(source_bytes, stated): rows, _ = parse_roster(source_bytes.decode("utf-8")) corrections = {c["row"]: c for c in CORRECTIONS} out = [] for lead, model, lane, latest, unit in rows: corr = corrections.get(lead) out.append([kind_of(model), lead, model, lane, latest, unit, corr["marker"] if corr else ""]) return out def git(args): try: proc = subprocess.run(["git", "-C", ROOT] + args, capture_output=True, text=True, timeout=20) except (OSError, subprocess.SubprocessError): return None return proc.stdout.strip() if proc.returncode == 0 else None def csv_bytes(rows): buf = io.StringIO() writer = csv.writer(buf) writer.writerow(COLUMNS) writer.writerows(rows) return buf.getvalue().encode("utf-8") def main(argv=None): ap = argparse.ArgumentParser( description="Extract the WIZARDS-TABLE.md roster into wizards.csv " "plus its pin manifest.") ap.add_argument("--check", action="store_true", help="report drift, write nothing") args = ap.parse_args(argv) source_path = os.path.join(ROOT, SOURCE_REL) try: with open(source_path, "rb") as fh: source_bytes = fh.read() except OSError as exc: print("CANNOT GRADE: %s unreadable: %s" % (source_path, exc)) return 2 rows, stated = parse_roster(source_bytes.decode("utf-8")) built = build(source_bytes, stated) payload = csv_bytes(built) blob = git(["hash-object", SOURCE_REL]) or "unknown" commit = git(["log", "-1", "--format=%H", "--", SOURCE_REL]) or "unknown" now = datetime.now(timezone.utc).replace(microsecond=0).isoformat() manifest = { "artifact": CSV_NAME, "source": SOURCE_REL, "source_blob": blob, "source_sha256": sha256_bytes(source_bytes), "source_bytes": len(source_bytes), "source_commit": commit, "source_stated_date": stated, "extracted_utc": now, "columns": COLUMNS, "row_count": len(built), "wizard_rows": sum(1 for r in built if r[0] == "wizard"), "lane_signer_rows": sum(1 for r in built if r[0] == "lane signer"), "csv_sha256": sha256_bytes(payload), "corrections": CORRECTIONS, } csv_path = os.path.join(HERE, CSV_NAME) man_path = os.path.join(HERE, MANIFEST_NAME) current_csv = None if os.path.exists(csv_path): with open(csv_path, "rb") as fh: current_csv = fh.read() if args.check: drift = "csv differs" if current_csv != payload else "csv current" print("rows=%d wizards=%d lane_signers=%d source_blob=%s %s" % (manifest["row_count"], manifest["wizard_rows"], manifest["lane_signer_rows"], blob[:12], drift)) return 0 if current_csv == payload else 1 with open(csv_path, "wb") as fh: fh.write(payload) with open(man_path, "w", encoding="utf-8") as fh: json.dump(manifest, fh, indent=2, sort_keys=True) fh.write("\n") print("wrote %s: %d rows (%d wizard, %d lane signer) from %s blob %s" % (CSV_NAME, manifest["row_count"], manifest["wizard_rows"], manifest["lane_signer_rows"], SOURCE_REL, blob[:12])) print("wrote %s: source_sha256=%s csv_sha256=%s stated=%s extracted=%s" % (MANIFEST_NAME, manifest["source_sha256"][:12], manifest["csv_sha256"][:12], stated, now)) return 0 if __name__ == "__main__": sys.exit(main())