Files
troupe-standup/tools/queue_append_audit.py
T
volt 1ec068cae0 volt: Q168 land -- the append audit grades the pin axis beside the index axis
queue_append_audit.py now runs two graders out of one `git archive`: grade_index
(unchanged vocabulary) and grade_pin, which runs the rev's own open_set.py
--check beside the rev's own queue. PASS / DRIFT / UNGRADABLE / ERROR; rc 1 when
either axis is fatal, so --guard means "index current and pin moved".
--axis {both,index,pin} narrows the run; the report prints index:/pin:/drift:.

Checks landed with it: CHECK-append-pin (5 legs, rc 0 here).
Measured at 12fb1b0's parent line: --selftest 43/43; --guard rc 0 pin:PASS;
1c3c855..56d3853 rc 1 pin:DRIFT "count headings moved 116 -> 117" while
--axis index over the same range reads rc 0, and 12fb1b0's pre-axis blob reads
rc 0 too, so the red is the axis and not the range.

PIN_COUNTS moved to the post-append counts (119 headings/103 ids, 131
records/97 ids) and docs/queue-index.md re-rendered; docs/TOOLS.md row follows
the tool's flags.
2026-11-04 12:47:43 +00:00

748 lines
34 KiB
Python

#!/usr/bin/env python3
"""queue_append_audit.py -- did every queue-touching commit leave its own tree
consistent: its index current, and its pin moved to where the queue's counts
went?
WHY THIS EXISTS
`docs/queue-index.md` is rendered from `WORK-QUEUE.md` and pins it by sha256,
so a commit that appends to the queue and does not re-render the index is
STALE by construction -- there is no "might be stale" case left to argue.
Nothing obliged a commit to re-render, and the ledger measured the cost twice:
`f2c3067` and `067afdf` each left the gate red until a later repair commit,
the second for 2h37m until `de3dcdc`. The rule the ledger now states is
"append and re-render in the SAME commit; `--check` must read rc 0 at the
commit it lands in".
Enforcing that rule by hand cost the control plane three wrong-slice counts in
one week, and its own 15:16Z correction names the cause: a `queue_index
--check` run in a working tree at HEAD says NOTHING about a cited revision --
"my first probe loop reported rc 0 'at 067afdf' while grading HEAD's tree".
This tool takes the hand out of the loop. Each commit's own tree is
materialised with `git archive` and that tree's OWN `tools/queue_index.py`
runs inside it, so every row is a measurement of the commit it names and of
the gate as it existed there.
WHAT IT MEASURES -- one row per commit that touches the queue in the range
verdict the INDEX axis: the commit's own tree passes its own
`tools/queue_index.py --check` (rc 0)
PASS rc 0
STALE --check graded a stale rendered document (rc 1)
UNGRADABLE the gate did not exist in that tree (no
tools/queue_index.py, or no index document), or the
rev's own tool refused with rc 2. A commit that
cannot be graded is NOT reported as stale, and is
counted separately -- folding the two into one
bucket is the exact defect this tool was built after.
ERROR any other exit, a crash, or a timeout. rc 1 without
the tool's `STALE:` line is a crash, not a fault.
pin the PIN axis: the same tree's own
`wizard-work/volt/q137-open-set/open_set.py --check`, which
compares WORK-QUEUE.md's counts with the PIN_COUNTS that file
carries.
PASS rc 0, the pin moved with the append
DRIFT rc 1, the queue moved and the pin did not
UNGRADABLE no open_set.py at that rev (the pin is younger than
the queue), or the tool refused with rc 2
ERROR any other exit, a crash, or a timeout
The two axes are graded in ONE materialised tree and reported
side by side, because neither stands for the other: `56d3853`
re-rendered its index and left the pin two headings behind, so
its row reads `PASS` on the index and `DRIFT` on the pin.
queue_add/del numstat for the queue in that commit.
index_changed whether the same commit also changed the index document -- the
mechanical shape of the rule, reported BESIDE the verdict and
never instead of it: a commit can touch the index and still be
STALE, and a commit can leave the index alone and still be
current (an append that lands outside every section's extent
would be such a case, and the verdict would say so).
Precedence is deliberate: the verdict comes from a checkout of the commit,
never from its message, and never from a run at HEAD. `--axis` narrows the run
to one of the two axes; the default grades both.
WHAT IT DOES NOT DO
It repairs nothing, decides nothing about whether an UNGRADABLE commit should
have had a gate, and adjudicates no queue content -- closure is
`tools/queue_claim_check.py`'s verdict. `--guard` is the same measurement
restricted to the NEWEST queue-touching commit, which is the shape a tick's
`check.sh` can call: rc 0 means the commit about to be pushed left the index
current AND the pin moved in its own tree.
EXIT CODES (house contract, decided 2026-10-17 at 6ee8fce)
0 ran; nothing graded faulty (UNGRADABLE rows are reported, not faults)
1 ran and graded a fault: at least one STALE or ERROR row
2 could not grade: not a git repository, an unknown revision, no commit in
the range touches the queue, or the queue is not tracked at the target rev
3 retired, never returned
USAGE
python3 queue_append_audit.py --repo /root/troupe-standup
python3 queue_append_audit.py --repo . --from 1423c4d --to HEAD --json out.json
python3 queue_append_audit.py --repo . --guard # newest queue-touching commit
python3 queue_append_audit.py --selftest # 25 cells, a synthetic history
"""
from __future__ import annotations
import argparse
import io
import json
import os
import shutil
import subprocess
import sys
import tempfile
QUEUE_DEFAULT = "WORK-QUEUE.md"
INDEX_DEFAULT = "docs/queue-index.md"
GATE_TOOL = "tools/queue_index.py"
# The queue's own pin, graded in the SAME materialised tree: `open_set.py
# --check` compares WORK-QUEUE.md's counts against the PIN_COUNTS the file
# carries. The index axis cannot see it -- `56d3853` rendered its index and left
# the pin two headings behind -- so the audit grades both axes, each reported
# beside the other and neither standing for the other.
PIN_TOOL = "wizard-work/volt/q137-open-set/open_set.py"
HERE = os.path.dirname(os.path.abspath(__file__))
FATAL = ("STALE", "ERROR")
PIN_FATAL = ("DRIFT", "ERROR")
# ---------------------------------------------------------------- git plumbing
def git(repo, *args, timeout=120):
"""(rc, stdout, stderr) -- never a traceback for a git-level refusal."""
try:
p = subprocess.run(["git", "-C", repo] + list(args),
capture_output=True, timeout=timeout)
except FileNotFoundError:
return 127, b"", b"git not found on PATH"
except subprocess.TimeoutExpired:
return 124, b"", ("git %s timed out after %ss" % (args[0], timeout)).encode()
return p.returncode, p.stdout, p.stderr
def out_text(rc, out, err):
s = (out or b"").decode("utf-8", "replace")
e = (err or b"").decode("utf-8", "replace")
return s, e
def is_repo(repo):
rc, _, _ = git(repo, "rev-parse", "--git-dir")
return rc == 0
def resolve(repo, rev):
rc, out, _ = git(repo, "rev-parse", "--verify", "--quiet", rev + "^{commit}")
s, _ = out_text(rc, out, b"")
return s.strip() if rc == 0 else None
def exists(repo, rev, path):
rc, _, _ = git(repo, "cat-file", "-e", "%s:%s" % (rev, path))
return rc == 0
def commit_meta(repo, rev):
rc, out, _ = git(repo, "show", "-s", "--format=%h%x00%ad%x00%s",
"--date=iso", rev)
s, _ = out_text(rc, out, b"")
parts = s.rstrip("\n").split("\x00") if s else []
while len(parts) < 3:
parts.append("")
return {"short": parts[0][:7] or rev[:7], "date": parts[1], "subject": parts[2]}
def numstat(repo, rev, queue, index):
"""(queue_add, queue_del, index_changed) for one commit."""
rc, out, _ = git(repo, "show", "--numstat", "--format=", rev, "--", queue, index)
s, _ = out_text(rc, out, b"")
add = delete = None
touched_index = False
for line in s.splitlines():
parts = line.split("\t")
if len(parts) != 3:
continue
a, d, path = parts
if path == queue:
add = int(a) if a.isdigit() else a
delete = int(d) if d.isdigit() else d
elif path == index:
touched_index = True
return add, delete, touched_index
def materialise(repo, rev, paths, dest):
"""Extract exactly `paths` from `rev` into `dest`. Returns (ok, error)."""
rc, out, err = git(repo, "archive", "--format=tar", rev, "--", *paths)
if rc != 0:
s, e = out_text(rc, out, err)
return False, (e or s or "git archive rc %d" % rc).strip().splitlines()[0]
try:
p = subprocess.run(["tar", "-xf", "-", "-C", dest], input=out,
capture_output=True, timeout=120)
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
return False, "could not unpack: %s" % exc
if p.returncode != 0:
return False, "tar rc %d: %s" % (p.returncode,
p.stderr.decode("utf-8", "replace").strip())
return True, ""
# ------------------------------------------------------------------- one commit
def run_check(cmd, cwd, timeout):
"""(rc, stdout, stderr, why) -- a timeout is a verdict, never a traceback."""
try:
p = subprocess.run(cmd, cwd=cwd, capture_output=True, timeout=timeout)
except subprocess.TimeoutExpired:
return None, "", "", "--check timed out after %ss" % timeout
return (p.returncode, p.stdout.decode("utf-8", "replace"),
p.stderr.decode("utf-8", "replace"), "")
def grade_index(td, queue, index, timeout):
"""The index axis, measured in `td`: {verdict, rc, detail}."""
tool = os.path.join(td, GATE_TOOL)
cmd = [sys.executable, tool,
"--repo", td,
"--queue", os.path.join(td, queue),
"--check", os.path.join(td, index)]
rc, s, e, why = run_check(cmd, td, timeout)
if why:
return {"verdict": "ERROR", "rc": None, "detail": why}
first = (s.strip() or e.strip() or "").splitlines()
out = {"rc": rc, "detail": first[0][:180] if first else ""}
if rc == 0:
out["verdict"] = "PASS"
elif rc == 1 and "STALE:" in s:
out["verdict"] = "STALE"
elif rc == 2:
out["verdict"] = "UNGRADABLE"
else:
# rc 1 with no STALE line is a crash wearing the verdict code; rc 3
# is retired. Neither is a graded fault, and neither is a pass.
out["verdict"] = "ERROR"
return out
def grade_pin(td, timeout):
"""The pin axis, measured in `td`: {pin, pin_rc, pin_detail}.
`open_set.py --check` compares the queue's own counts against the PIN_COUNTS
the file carries, so a commit that appends a heading or a record and leaves
the pin where it was reads DRIFT here even when its index is current -- the
exact shape `56d3853` landed in, which the index axis read PASS.
"""
tool = os.path.join(td, PIN_TOOL)
rc, s, e, why = run_check([sys.executable, tool, "--check"], td, timeout)
if why:
return {"pin": "ERROR", "pin_rc": None, "pin_detail": why}
lines = (s.strip() or e.strip() or "").splitlines()
fault = [ln for ln in lines if ln.startswith("FAIL")]
out = {"pin_rc": rc, "pin_detail": (fault[0] if fault else (lines[-1] if lines else ""))[:180]}
if rc == 0:
out["pin"] = "PASS"
elif rc == 1:
out["pin"] = "DRIFT"
elif rc == 2:
# the pin tool itself refused: a tree it cannot read is not a drift
out["pin"] = "UNGRADABLE"
else:
out["pin"] = "ERROR"
return out
def audit_commit(repo, rev, queue, index, timeout, axis="both"):
"""Grade one commit IN ITS OWN TREE. Never raises.
Two axes, graded independently in one materialised tree, because neither
stands for the other: a commit can leave the rendered index current and the
queue's own pin two headings behind (Q167's landing), and a commit can move
the pin while shipping no index at all.
"""
row = {"rev": rev}
row.update(commit_meta(repo, rev))
add, delete, touched_index = numstat(repo, rev, queue, index)
row.update({"queue_add": add, "queue_del": delete,
"index_changed": touched_index, "rc": None, "detail": "",
"pin_rc": None, "pin_detail": ""})
want_index = axis in ("both", "index")
want_pin = axis in ("both", "pin")
if not exists(repo, rev, queue):
missing = "%s does not exist at this rev" % queue
row.update(verdict="UNGRADABLE", detail=missing,
pin="UNGRADABLE", pin_detail=missing)
return row
if want_index:
# The gate as it existed AT THAT REV, not as it exists today: a revision
# where queue_index.py had not been written yet has no gate, and no
# claim about its index can be true or false.
if not exists(repo, rev, GATE_TOOL):
row.update(verdict="UNGRADABLE",
detail="no %s at this rev (the gate did not exist yet)" % GATE_TOOL)
want_index = False
elif not exists(repo, rev, index):
row.update(verdict="UNGRADABLE",
detail="no %s at this rev (nothing to compare against)" % index)
want_index = False
if want_pin and not exists(repo, rev, PIN_TOOL):
# the pin is younger than the queue: absent is UNGRADABLE, never DRIFT
row.update(pin="UNGRADABLE",
pin_detail="no %s at this rev" % PIN_TOOL)
want_pin = False
if not want_index and not want_pin:
return row
wanted = [p for p in (queue, index, GATE_TOOL, PIN_TOOL) if exists(repo, rev, p)]
td = tempfile.mkdtemp(prefix="qaa-")
try:
ok, why = materialise(repo, rev, wanted, td)
if not ok:
why = "could not materialise: %s" % why
if want_index:
row.update(verdict="ERROR", detail=why)
if want_pin:
row.update(pin="ERROR", pin_detail=why)
return row
if want_index:
row.update(grade_index(td, queue, index, timeout))
if want_pin:
row.update(grade_pin(td, timeout))
return row
finally:
shutil.rmtree(td, ignore_errors=True)
# ------------------------------------------------------------------ the range
def queue_revs(repo, queue, rev_from, rev_to):
args = ["rev-list", "--reverse", rev_to]
if rev_from:
args = ["rev-list", "--reverse", "%s..%s" % (rev_from, rev_to)]
rc, out, err = git(repo, *(args + ["--", queue]))
s, e = out_text(rc, out, err)
return rc, [h for h in s.split() if h], e
def audit(repo, queue, index, rev_from, rev_to, guard=False, timeout=30,
all_history=False, axis="both"):
"""(rc, payload). The only place exit codes are decided.
Range rules, in order:
--from REV grade exactly REV..TO (REV excluded), graded or not.
--all grade every commit that ever touched the queue, including the
hundreds that predate the index. Each of those is
UNGRADABLE, which is the honest answer and a useless table.
(default) begin at the commit that INTRODUCED the index: no commit
before it can be graded, and listing 328 rows that all say so
buries the 9 that can. The count left out is printed, never
silently dropped.
"""
payload = {"repo": repo, "queue": queue, "index": index,
"from": None, "to": None, "guard": bool(guard),
"auto_from": False, "pre_gate": 0, "axis": axis,
"rows": [], "counts": {}, "pin_counts": {},
"graded": 0, "pin_graded": 0, "rc": None}
def _grade_error(msg):
"""Attach a grade error to the payload and return the (2, payload) verdict.
Every precondition failure in this function is the same three-statement
shape: set the message, set rc=2, return. Six inline copies were the
smell; this helper is the single source of that shape.
"""
payload["error"] = msg
payload["rc"] = 2
return 2, payload
if not os.path.isdir(repo):
return _grade_error("not a directory: %s" % repo)
if not is_repo(repo):
return _grade_error("not a git repository: %s" % repo)
to_sha = resolve(repo, rev_to)
if to_sha is None:
return _grade_error("unknown revision: %s" % rev_to)
payload["to"] = to_sha
from_sha = None
if rev_from:
from_sha = resolve(repo, rev_from)
if from_sha is None:
return _grade_error("unknown revision: %s" % rev_from)
elif not all_history:
rc, out, _ = git(repo, "log", "--diff-filter=A", "--format=%H", "-1",
to_sha, "--", index)
intro = out_text(rc, out, b"")[0].strip()
if intro:
# Include the introducing commit itself: it may also carry a queue
# append, and a commit that adds both is exactly the shape to grade.
rc2, out2, _ = git(repo, "rev-parse", intro + "^")
from_sha = out2.decode("utf-8", "replace").strip() if rc2 == 0 else intro
payload["auto_from"] = True
payload["from"] = from_sha
rc, revs, err = queue_revs(repo, queue, from_sha, to_sha)
if rc != 0:
return _grade_error("git rev-list failed: %s" % ((err.strip().splitlines() or [""])[0]))
if guard:
revs = revs[-1:]
if not revs:
where = ("%s..%s" % (rev_from, rev_to) if rev_from else rev_to)
return _grade_error("no commit in %s touches %s, so there is no index "
"currency to grade" % (where, queue))
if payload["auto_from"] and not guard:
rc_n, out_n, _ = git(repo, "rev-list", "--count", to_sha, "--", queue)
try:
payload["pre_gate"] = max(0, int(out_n.decode().strip()) - len(revs))
except (ValueError, AttributeError):
payload["pre_gate"] = 0
rows = [audit_commit(repo, r, queue, index, timeout, axis) for r in revs]
counts, pin_counts = {}, {}
for r in rows:
if "verdict" in r:
counts[r["verdict"]] = counts.get(r["verdict"], 0) + 1
if "pin" in r:
pin_counts[r["pin"]] = pin_counts.get(r["pin"], 0) + 1
payload["rows"] = rows
payload["counts"] = counts
payload["pin_counts"] = pin_counts
payload["graded"] = sum(counts.get(k, 0) for k in ("PASS", "STALE", "ERROR"))
payload["pin_graded"] = sum(pin_counts.get(k, 0) for k in ("PASS", "DRIFT", "ERROR"))
index_fault = any(r.get("verdict") in FATAL for r in rows)
pin_fault = any(r.get("pin") in PIN_FATAL for r in rows)
payload["rc"] = 1 if (index_fault or pin_fault) else 0
return payload["rc"], payload
# --------------------------------------------------------------------- report
def report(payload, stream=sys.stdout):
w = stream.write
if payload.get("error"):
w("queue_append_audit: %s\n" % payload["error"])
return
frm, to = payload.get("from"), payload.get("to") or payload["to"]
if payload["guard"]:
where = "newest queue-touching commit at or below %s" % (to or "")[:7]
elif payload["auto_from"]:
where = "%s..%s (auto: from the commit that introduced %s)" % (
(frm or "")[:7], (to or "")[:7], payload["index"])
elif frm:
where = "%s..%s" % (frm[:7], (to or "")[:7])
else:
where = "%s (all history)" % (to or "")[:7]
w("queue_append_audit -- queue: %s index: %s range: %s%s\n"
% (payload["queue"], payload["index"], where,
" [guard]" if payload["guard"] else ""))
if payload.get("pre_gate"):
w(" %d earlier commit(s) touch the queue before the gate existed; they "
"cannot be graded and are not listed (--all lists them)\n"
% payload["pre_gate"])
w(" %d commit(s) touch the queue\n\n" % len(payload["rows"]))
for r in payload["rows"]:
add, dele = r["queue_add"], r["queue_del"]
q = "q %s/%s" % (add if add is not None else "-", dele if dele is not None else "-")
note = r.get("detail") or ""
if r.get("pin") and r["pin"] != "PASS":
note = "%s | pin: %s" % (note, r.get("pin_detail") or r["pin"])
w(" %-10s %-8s %-25s %-9s index:%-3s pin:%-10s %s\n"
% (r.get("verdict") or "-", r["short"], r["date"][:25], q,
"yes" if r["index_changed"] else "no", r.get("pin") or "-", note))
c = payload["counts"]
pc = payload["pin_counts"]
w("\nsummary: %d commit(s) -- %s\n"
% (len(payload["rows"]),
", ".join("%d %s" % (c[k], k) for k in ("PASS", "STALE", "UNGRADABLE", "ERROR") if k in c)
or "nothing graded"))
w("pin: %s\n"
% (", ".join("%d %s" % (pc[k], k) for k in ("PASS", "DRIFT", "UNGRADABLE", "ERROR") if k in pc)
or "not graded"))
ungradable = c.get("UNGRADABLE", 0)
w("graded: %d of %d index, %d of %d pin%s\n"
% (payload["graded"], len(payload["rows"]),
payload["pin_graded"], len(payload["rows"]),
(" (%d index UNGRADABLE -- reported, NOT counted as stale)" % ungradable)
if ungradable else ""))
stale = [r["short"] for r in payload["rows"] if r.get("verdict") == "STALE"]
if stale:
w("stale: %s\n" % " ".join(stale))
drift = [r["short"] for r in payload["rows"] if r.get("pin") == "DRIFT"]
if drift:
w("drift: %s\n" % " ".join(drift))
bad = [r["short"] for r in payload["rows"] if r.get("verdict") == "ERROR"]
pbad = [r["short"] for r in payload["rows"] if r.get("pin") == "ERROR"]
if bad:
w("error: %s\n" % " ".join(bad))
if pbad:
w("pin err: %s\n" % " ".join(pbad))
if payload["rc"] == 0:
w("verdict: every graded commit left its own tree consistent -- "
"the index current and the pin where the queue put it\n")
else:
w("verdict: %d commit(s) left their own tree inconsistent -- "
"%d index, %d pin -- rc 1\n"
% (len(stale) + len(bad) + len(drift) + len(pbad), len(stale) + len(bad),
len(drift) + len(pbad)))
# ------------------------------------------------------------------- selftest
def _sh(cwd, *args):
return subprocess.run(args, cwd=cwd, capture_output=True, text=True)
PIN_STUB = '''#!/usr/bin/env python3
"""A stand-in for wizard-work/volt/q137-open-set/open_set.py's --check: count
the queue's headings and compare them with the PIN this commit carries."""
import argparse
import sys
QUEUE = "WORK-QUEUE.md"
PIN = @PIN@
ap = argparse.ArgumentParser()
ap.add_argument("--check", action="store_true")
ap.parse_args()
n = len([ln for ln in open(QUEUE, encoding="utf-8") if ln.startswith("## ")])
print("queue: %d heading(s)" % n)
if n != PIN:
print("FAIL count headings moved %d -> %d" % (PIN, n))
print("CHECK 1 fault(s)")
sys.exit(1)
print("CHECK OK: counts match the pin")
sys.exit(0)
'''
def _fixture(base):
"""A seven-commit history with one commit of each kind. Returns (repo, revs).
A: queue only, no gate tool -> index UNGRADABLE, pin UNGRADABLE
B: appends, adds tools/queue_index.py + its render + the pin tool (PIN=1)
-> both PASS
C: appends to the queue, no re-render -> index STALE, pin PASS
D: appends AND re-renders -> both PASS
F: appends a HEADING and re-renders, pin left -> index PASS, pin DRIFT
G: appends a record and moves the pin to 2 -> both PASS
E: touches an unrelated file -> not selected at all
"""
repo = os.path.join(base, "repo")
os.makedirs(repo)
_sh(base, "git", "init", "-q", repo)
_sh(repo, "git", "config", "user.name", "Selftest")
_sh(repo, "git", "config", "user.email", "selftest@invalid")
_sh(repo, "git", "config", "commit.gpgsign", "false")
queue = os.path.join(repo, "WORK-QUEUE.md")
with open(queue, "w", encoding="utf-8") as fh:
fh.write("## Q1 -- alpha\n\nStatus: open\n")
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "A: queue only")
pin_dir = os.path.join(repo, os.path.dirname(PIN_TOOL))
os.makedirs(pin_dir)
os.makedirs(os.path.join(repo, "tools"))
os.makedirs(os.path.join(repo, "docs"))
with open(os.path.join(repo, PIN_TOOL), "w", encoding="utf-8") as fh:
fh.write(PIN_STUB.replace("@PIN@", "1"))
with open(os.path.join(repo, GATE_TOOL), "w", encoding="utf-8") as fh:
fh.write(open(os.path.join(HERE, "queue_index.py"), encoding="utf-8").read())
with open(queue, "a", encoding="utf-8") as fh:
fh.write("- beta appended\n")
_sh(repo, sys.executable, GATE_TOOL, "--repo", ".", "--write", INDEX_DEFAULT)
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "B: append + gate + pin + index")
with open(queue, "a", encoding="utf-8") as fh:
fh.write("- gamma appended\n")
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "C: append, no re-render")
with open(queue, "a", encoding="utf-8") as fh:
fh.write("- delta appended\n")
_sh(repo, sys.executable, GATE_TOOL, "--repo", ".", "--write", INDEX_DEFAULT)
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "D: append + re-render")
# F: the shape Q167 landed in -- the index is re-rendered and current, the
# pin is not moved, so the two axes disagree in ONE row.
with open(queue, "a", encoding="utf-8") as fh:
fh.write("\n## Q2 -- beta\n\nStatus: open\n")
_sh(repo, sys.executable, GATE_TOOL, "--repo", ".", "--write", INDEX_DEFAULT)
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "F: new heading + re-render, pin left")
# G: the repair commit -- touches the queue and moves the pin with it.
with open(queue, "a", encoding="utf-8") as fh:
fh.write("- epsilon appended\n")
with open(os.path.join(repo, PIN_TOOL), "w", encoding="utf-8") as fh:
fh.write(PIN_STUB.replace("@PIN@", "2"))
_sh(repo, sys.executable, GATE_TOOL, "--repo", ".", "--write", INDEX_DEFAULT)
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "G: append + move the pin")
with open(os.path.join(repo, "notes.txt"), "w", encoding="utf-8") as fh:
fh.write("unrelated\n")
_sh(repo, "git", "add", "-A")
_sh(repo, "git", "commit", "-q", "-m", "E: unrelated file")
revs = _sh(repo, "git", "rev-list", "--reverse", "HEAD").stdout.split()
return repo, revs
def selftest():
cells = []
def check(name, got, want):
cells.append((name, got == want, got, want))
base = tempfile.mkdtemp(prefix="qaa-selftest-")
try:
repo, revs = _fixture(base)
a, b, c, d, f, g, e = revs
rc, p = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD",
all_history=True)
rows = {r["rev"]: r for r in p["rows"]}
check("the audit selects the 6 queue-touching commits, not the 7th", len(p["rows"]), 6)
check("a commit with no gate tool is UNGRADABLE, not STALE", rows[a]["verdict"], "UNGRADABLE")
check("the commit that added the gate and its index is PASS", rows[b]["verdict"], "PASS")
check("an append with no re-render is STALE", rows[c]["verdict"], "STALE")
check("an append that re-renders in the same commit is PASS", rows[d]["verdict"], "PASS")
check("STALE row says the index was not touched", rows[c]["index_changed"], False)
check("PASS row says the index was touched", rows[d]["index_changed"], True)
check("the stale row's numstat is the append", (rows[c]["queue_add"], rows[c]["queue_del"]), (1, 0))
check("a range with a STALE row exits 1", rc, 1)
check("counts keep UNGRADABLE out of the STALE bucket", p["counts"].get("STALE"), 1)
check("graded counts PASS+STALE+ERROR only", p["graded"], 5)
check("the STALE detail carries the tool's own word", "STALE:" in rows[c]["detail"], True)
# -- the pin axis, measured in the same trees -----------------------
check("a commit that predates the pin tool is pin UNGRADABLE", rows[a]["pin"], "UNGRADABLE")
check("the commit that shipped the pin reads pin PASS", rows[b]["pin"], "PASS")
check("an index-stale commit whose pin matches reads pin PASS", rows[c]["pin"], "PASS")
check("a new heading with the pin left behind reads pin DRIFT", rows[f]["pin"], "DRIFT")
check("the DRIFT row's INDEX is still PASS -- the axes are independent",
rows[f]["verdict"], "PASS")
check("the DRIFT row names the count that moved",
"count headings moved 1 -> 2" in rows[f]["pin_detail"], True)
check("a queue-touching commit that moves the pin reads pin PASS", rows[g]["pin"], "PASS")
check("DRIFT is kept out of the pin PASS bucket", p["pin_counts"].get("DRIFT"), 1)
check("pin UNGRADABLE is counted, not folded into DRIFT", p["pin_counts"].get("UNGRADABLE"), 1)
check("pin_graded counts PASS+DRIFT+ERROR only", p["pin_graded"], 5)
check("a DRIFT row alone exits 1", rows[f]["pin_rc"], 1)
rc_p, p_p = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, f, guard=True,
axis="pin")
check("--axis pin grades the pin alone and still exits 1 on DRIFT",
(rc_p, p_p["rows"][0]["pin"]), (1, "DRIFT"))
check("--axis pin sets no index verdict on the row",
"verdict" in p_p["rows"][0], False)
check("--axis pin counts no index rows", p_p["counts"], {})
rc_i, p_i = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, c, guard=True,
axis="index")
check("--axis index reads the same commit STALE on its own axis",
(rc_i, p_i["rows"][0]["verdict"], "pin" in p_i["rows"][0]),
(1, "STALE", False))
rc_g, p_g = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD", guard=True)
check("--guard on a clean head exits 0", rc_g, 0)
check("--guard reports exactly one commit", len(p_g["rows"]), 1)
check("--guard's one row is clean on BOTH axes",
(p_g["rows"][0]["verdict"], p_g["rows"][0]["pin"]), ("PASS", "PASS"))
rc_s, _ = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, c, guard=True)
check("--guard on the stale commit exits 1", rc_s, 1)
rc_f, _ = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, f, guard=True)
check("--guard on the drift commit exits 1", rc_f, 1)
# Default range: begin where the gate does. B introduced the index, so
# the default table starts at B's parent and A is counted, not listed.
rc_a, p_a = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD")
check("the default range begins at the commit that introduced the index",
(p_a["auto_from"], len(p_a["rows"])), (True, 5))
check("pre-gate commits are counted, not silently dropped", p_a["pre_gate"], 1)
check("the default range still grades the stale commit", rc_a, 1)
rc_e, p_e = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, d, d)
check("an empty range is rc 2, not a clean 0", (rc_e, p_e.get("error") is not None), (2, True))
rc_r, _ = audit(os.path.join(base, "no-such-repo"), QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD")
check("a missing repo is rc 2", rc_r, 2)
rc_b, p_b = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "no-such-rev")
check("an unknown revision is rc 2", (rc_b, p_b.get("error") is not None), (2, True))
rc_q, p_q = audit(repo, "NOT-THE-QUEUE.md", INDEX_DEFAULT, None, "HEAD")
check("a queue no commit touches is rc 2", (rc_q, "touches" in (p_q.get("error") or "")), (2, True))
json_path = os.path.join(base, "out.json")
buf = io.StringIO()
rc_j, p_j = audit(repo, QUEUE_DEFAULT, INDEX_DEFAULT, None, "HEAD",
all_history=True)
with open(json_path, "w", encoding="utf-8") as fh:
json.dump(p_j, fh, indent=1)
report(p_j, stream=buf)
with open(json_path, encoding="utf-8") as fh:
loaded = json.load(fh)
check("--json round-trips the same 6 rows", len(loaded["rows"]), 6)
check("the report names the stale commit by short sha", c[:7] in buf.getvalue(), True)
check("the report names the drift commit on its own line", "drift: " + f[:7] in buf.getvalue(), True)
check("a report with a fault never prints a clean verdict",
"every graded commit" in buf.getvalue(), False)
finally:
shutil.rmtree(base, ignore_errors=True)
bad = [c for c in cells if not c[1]]
for name, ok, got, want in bad:
print("FAIL %s\n got: %r\n want: %r" % (name, got, want))
print("selftest: %d cells, %d passed, %d failed"
% (len(cells), len(cells) - len(bad), len(bad)))
return 0 if not bad else 1
# ----------------------------------------------------------------------- main
def main(argv=None):
ap = argparse.ArgumentParser(
description="Did every commit that touched the work queue leave its "
"index current IN ITS OWN TREE?")
ap.add_argument("--repo", default=".")
ap.add_argument("--queue", default=QUEUE_DEFAULT)
ap.add_argument("--index", default=INDEX_DEFAULT)
ap.add_argument("--from", dest="rev_from", default=None,
help="exclusive lower bound; default: the commit that "
"introduced the index document")
ap.add_argument("--to", dest="rev_to", default="HEAD")
ap.add_argument("--all", dest="all_history", action="store_true",
help="start at the root of history, listing the pre-gate "
"commits as UNGRADABLE rows too")
ap.add_argument("--guard", action="store_true",
help="grade only the newest queue-touching commit")
ap.add_argument("--axis", choices=("both", "index", "pin"), default="both",
help="which axis to grade: the rendered index, the queue's "
"own PIN_COUNTS, or both (default both)")
ap.add_argument("--timeout", type=int, default=30,
help="seconds per commit's --check (default 30)")
ap.add_argument("--json", metavar="PATH", help="write the rows here")
ap.add_argument("--selftest", action="store_true")
args = ap.parse_args(argv)
if args.selftest:
return selftest()
rc, payload = audit(args.repo, args.queue, args.index,
args.rev_from, args.rev_to, args.guard, args.timeout,
args.all_history, args.axis)
report(payload)
if args.json:
try:
with open(args.json, "w", encoding="utf-8") as fh:
json.dump(payload, fh, indent=1)
except OSError as exc:
print("cannot write json: %s" % exc, file=sys.stderr)
return 2
return rc
if __name__ == "__main__":
sys.exit(main())