Files

246 lines
8.8 KiB
Python

#!/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())