195 lines
8.4 KiB
Python
195 lines
8.4 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""design — report the finding register (CB-WP-0022 T05).
|
||
|
|
|
||
|
|
Implements the metrics in `specs/GameDesign.md` §4 over the register in
|
||
|
|
`specs/GroundRules.md`. ADR-0012 D8 retired `design-baseline.py`, which
|
||
|
|
was a hand-maintained dict counting itself; the difference that matters is
|
||
|
|
that **every number here is computed over rows that name real files, and
|
||
|
|
the reproduction check stats the file.**
|
||
|
|
|
||
|
|
`design-baseline.py`'s `has_reproduction` was `p.startswith("scenarios/")`
|
||
|
|
and nothing else, so its own positive control was green against a path
|
||
|
|
that had been deleted. The self-test below asserts the opposite property
|
||
|
|
directly: a row citing a nonexistent file must NOT count as reproduced.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os, re, sys, subprocess, datetime
|
||
|
|
|
||
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
REGISTER = os.path.join(ROOT, "specs", "GroundRules.md")
|
||
|
|
|
||
|
|
BEGIN = "<!-- design-register:begin -->"
|
||
|
|
END = "<!-- design-register:end -->"
|
||
|
|
|
||
|
|
KINDS = {"underdetermined", "inconsistent", "inert", "degenerate", "unplayed"}
|
||
|
|
OPEN_STATES = {"raised", "reported", "ruled"} # the queue: not yet applied
|
||
|
|
CLOSED_STATES = {"applied", "withdrawn"} # the log
|
||
|
|
NOTE = "note"
|
||
|
|
NOTE_EXPIRY_DAYS = 30 # GameDesign §3.1, same figure as rule-coverage.py
|
||
|
|
|
||
|
|
|
||
|
|
def parse(text):
|
||
|
|
"""Rows between the register markers. Raises if the block is absent —
|
||
|
|
a register that silently reports zero findings is worse than one that
|
||
|
|
fails."""
|
||
|
|
try:
|
||
|
|
block = text.split(BEGIN)[1].split(END)[0]
|
||
|
|
except IndexError:
|
||
|
|
raise SystemExit(f"no design-register block in {REGISTER}")
|
||
|
|
rows = []
|
||
|
|
for line in block.splitlines():
|
||
|
|
line = line.strip()
|
||
|
|
if not line.startswith("|") or line.startswith("|---"):
|
||
|
|
continue
|
||
|
|
cells = [c.strip() for c in line.strip("|").split("|")]
|
||
|
|
if len(cells) != 7 or cells[0] in ("id",):
|
||
|
|
continue
|
||
|
|
rows.append(dict(zip(
|
||
|
|
("id", "kind", "state", "repro", "role", "raised", "owner"), cells)))
|
||
|
|
return rows
|
||
|
|
|
||
|
|
|
||
|
|
def reproduced(row, root=ROOT):
|
||
|
|
"""GameDesign §1.1: the artifact must resolve. A named test is admitted
|
||
|
|
by its `crate::module::name` shape; anything else must be a real path
|
||
|
|
on disk, and that is checked by stat, not by prefix."""
|
||
|
|
p = row["repro"]
|
||
|
|
if p in ("", "—", "-"):
|
||
|
|
return False
|
||
|
|
if "::" in p: # a named test
|
||
|
|
return True
|
||
|
|
return os.path.exists(os.path.join(root, p))
|
||
|
|
|
||
|
|
|
||
|
|
_SIM = None
|
||
|
|
|
||
|
|
|
||
|
|
def sim_passes(root=ROOT):
|
||
|
|
"""`make sim` is the authority on whether scenarios pass. Run once and
|
||
|
|
cache — asking per row costs a full suite per finding."""
|
||
|
|
global _SIM
|
||
|
|
if _SIM is None:
|
||
|
|
r = subprocess.run(["make", "sim"], cwd=root, capture_output=True, text=True)
|
||
|
|
_SIM = r.returncode == 0
|
||
|
|
return _SIM
|
||
|
|
|
||
|
|
|
||
|
|
def alarming(row, root=ROOT):
|
||
|
|
"""§1.3, and the distinction the backfill discovered: **only a
|
||
|
|
counterexample alarms when green.**
|
||
|
|
|
||
|
|
A `default` reproduction encodes a provisional choice and is SUPPOSED
|
||
|
|
to pass — U2's scenario is green because the default it documents is
|
||
|
|
implemented, which says nothing about whether ground-game agrees. A
|
||
|
|
`counterexample` is the model-checker shape: it demonstrates the
|
||
|
|
defect, so it must be red while the finding is open. GR-E01's went
|
||
|
|
green when the edition landed and nothing noticed for four days.
|
||
|
|
"""
|
||
|
|
if row["role"] != "counterexample":
|
||
|
|
return False
|
||
|
|
if not row["repro"].startswith("scenarios/"):
|
||
|
|
return False
|
||
|
|
return sim_passes(root)
|
||
|
|
|
||
|
|
|
||
|
|
def report(root=ROOT, today=None):
|
||
|
|
today = today or datetime.date.today()
|
||
|
|
rows = parse(open(os.path.join(root, "specs", "GroundRules.md")).read())
|
||
|
|
|
||
|
|
findings = [r for r in rows if r["state"] != NOTE]
|
||
|
|
notes = [r for r in rows if r["state"] == NOTE]
|
||
|
|
queue = [r for r in findings if r["state"] in OPEN_STATES]
|
||
|
|
closed = [r for r in findings if r["state"] in CLOSED_STATES]
|
||
|
|
|
||
|
|
print("design — the finding register\n")
|
||
|
|
print(" QUEUE (open findings)")
|
||
|
|
for r in sorted(queue, key=lambda r: r["raised"]):
|
||
|
|
age = (today - datetime.date.fromisoformat(r["raised"])).days
|
||
|
|
mark = "repro" if reproduced(r, root) else " - "
|
||
|
|
print(f" {mark} {r['id']:<4} {r['kind']:<16} {r['state']:<9} {age:>4}d {r['owner']}")
|
||
|
|
|
||
|
|
if notes:
|
||
|
|
print("\n NOTES (not reportable — GameDesign §3.1)")
|
||
|
|
for r in sorted(notes, key=lambda r: r["raised"]):
|
||
|
|
age = (today - datetime.date.fromisoformat(r["raised"])).days
|
||
|
|
flag = " EXPIRED" if age > NOTE_EXPIRY_DAYS else ""
|
||
|
|
print(f" {r['id']:<4} {r['kind']:<16} {age:>4}d{flag}")
|
||
|
|
|
||
|
|
# ---- §4 metrics. The log is reported on request, not by default,
|
||
|
|
# because a default view mixing open and closed loses the queue
|
||
|
|
# property (ADR-0012 D5).
|
||
|
|
n = len(findings)
|
||
|
|
repro = sum(1 for r in findings if reproduced(r, root))
|
||
|
|
debt = [r for r in queue if not reproduced(r, root)]
|
||
|
|
expired = [r for r in notes
|
||
|
|
if (today - datetime.date.fromisoformat(r["raised"])).days > NOTE_EXPIRY_DAYS]
|
||
|
|
unresolved_green = [r for r in queue if reproduced(r, root) and alarming(r, root)]
|
||
|
|
|
||
|
|
print(f"\n findings {n} (+{len(notes)} note(s))")
|
||
|
|
print(f" with a resolving reproduction {repro}/{n}"
|
||
|
|
f"{'' if not n else f' = {100*repro//n}%'} target 100%")
|
||
|
|
print(f" open, lacking a reproduction {len(debt)} target 0"
|
||
|
|
+ (f" [{', '.join(r['id'] for r in debt)}]" if debt else ""))
|
||
|
|
print(f" reproductions green while open {len(unresolved_green)} target 0"
|
||
|
|
+ (" <-- ALARM, GameDesign §1.3" if unresolved_green else ""))
|
||
|
|
print(f" notes past {NOTE_EXPIRY_DAYS} days {len(expired)} target 0")
|
||
|
|
print(f" closed (log) {len(closed)}"
|
||
|
|
f" [{', '.join(r['id'] for r in closed)}]" if closed else "")
|
||
|
|
|
||
|
|
bad = [r for r in rows if r["kind"] not in KINDS]
|
||
|
|
if bad:
|
||
|
|
print(f"\n UNKNOWN KIND: {', '.join(r['id'] for r in bad)}"
|
||
|
|
" — a sixth kind means the taxonomy was invented (ADR-0012 D4)")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def self_test():
|
||
|
|
ok = True
|
||
|
|
|
||
|
|
def check(name, cond, detail=""):
|
||
|
|
nonlocal ok
|
||
|
|
ok = ok and bool(cond)
|
||
|
|
print(f" {'ok ' if cond else 'FAIL'} {name}{' ' + detail if detail else ''}")
|
||
|
|
|
||
|
|
rows = parse(open(REGISTER).read())
|
||
|
|
check("the register parses", len(rows) >= 14, f"{len(rows)} row(s)")
|
||
|
|
check("every kind is one of the five",
|
||
|
|
all(r["kind"] in KINDS for r in rows),
|
||
|
|
"a sixth kind means the taxonomy was invented")
|
||
|
|
check("every state is known",
|
||
|
|
all(r["state"] in OPEN_STATES | CLOSED_STATES | {NOTE} for r in rows))
|
||
|
|
|
||
|
|
# THE control design-baseline.py did not have. Its YES-control passed a
|
||
|
|
# path that had been deleted and still returned True.
|
||
|
|
check("a nonexistent reproduction does NOT count",
|
||
|
|
not reproduced({"repro": "scenarios/ground/gr-e01-threshold-unreachable-2p.yaml"}),
|
||
|
|
"this exact path was deleted by 2da19a4 and the old tool said yes")
|
||
|
|
check("a real reproduction DOES count",
|
||
|
|
reproduced({"repro": "scenarios/ground/gr-p05-solve-legality.yaml"}))
|
||
|
|
check("a named test counts", reproduced({"repro": "games_ground::view::tests::a_spectator_sees_no_hands"}))
|
||
|
|
check("an em-dash does not count", not reproduced({"repro": "—"}))
|
||
|
|
|
||
|
|
# The distinction the backfill discovered: a green DEFAULT is expected,
|
||
|
|
# a green COUNTEREXAMPLE is the alarm. Without this the report cried
|
||
|
|
# wolf over U2, whose scenario is green precisely because the
|
||
|
|
# provisional default it documents is implemented.
|
||
|
|
check("a green default does not alarm",
|
||
|
|
not alarming({"role": "default", "repro": "scenarios/ground/gr-d01-darvo-trigger.yaml"}))
|
||
|
|
check("role is recorded for every reproduced row",
|
||
|
|
all(r["role"] in ("counterexample", "default")
|
||
|
|
for r in rows if r["repro"] not in ("—", "-", "")))
|
||
|
|
|
||
|
|
# The measured claim in the backfill: only U2 cites its U-item.
|
||
|
|
cited = [u for u in [f"U{i}" for i in range(1, 11)]
|
||
|
|
if subprocess.run(["grep", "-rlE", rf"\b{u}\b"] +
|
||
|
|
[os.path.join(ROOT, "scenarios", "ground")],
|
||
|
|
capture_output=True, text=True, cwd=ROOT).stdout.strip()]
|
||
|
|
check("exactly one U-item is cited by a scenario", cited == ["U2"], f"{cited}")
|
||
|
|
|
||
|
|
print("design self-test (positive control)")
|
||
|
|
return 0 if ok else 1
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(self_test() if "--self-test" in sys.argv else report())
|