246 lines
8.8 KiB
Python
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())
|