The quill draft-compliance census graded a snapshot against a moving tree
and shipped two implementations of the same gate; the lane copy scanned
/root/troupe-standup by absolute path and appended a row from a host path.
Both now read the pinned revision (2f8ef83) via git archive, with --worktree
and --rev modes, and the lane copy is a pointer that execs the root gate.
Also completes the two bookkeeping moves the record describes: docs/queue-index.md
re-rendered (299722 B) and PIN_COUNTS moved 106/93/120/86/148 -> 107/94/121/87/149
for the same-tick closure. verify.py --fast: 12 legs, 11 pass, 0 fault, 1 skipped.
580 lines
22 KiB
Python
580 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""Independent verifier for draft-compliance-report-2026-10-31.{tsv,md}.
|
|
|
|
WHAT IT GRADES, AND AGAINST WHAT
|
|
The committed TSV is a claim about the .md drafts beside it. This verifier
|
|
re-derives every row from the files with its OWN code -- its own line counter,
|
|
its own `VERDICT:`/`CHECK:` detection and its own implementation of the bare-
|
|
command rule -- and compares in both directions: every TSV row against the
|
|
file it names, and every file against the row it should have. It also grades
|
|
the report's Evidence table against the counts it just measured, so a stale
|
|
numeral in the prose is a fault rather than a reader's problem.
|
|
|
|
WHICH TREE -- three modes, because a dated census describes a revision
|
|
(default) the lane as it stands at the pin in
|
|
`draft-compliance-report-2026-10-31.pin`, extracted with
|
|
`git archive`; the committed TSV and report are graded against
|
|
it. Drift since the pin is printed as a note, never a fault.
|
|
--worktree the live worktree; this is the "is the census current?" reading
|
|
and it faults on any difference, additions included.
|
|
--rev REV the lane at REV, for probing whether a given tree's census is
|
|
true and for grading an older landing.
|
|
|
|
Why: the 2026-10-31 table was byte-true at 2f8ef83 and read red at HEAD for
|
|
eight drafts that landed after it -- a snapshot cannot be graded against a
|
|
tree it never described. And the fix must not be "grade nothing": the pin is
|
|
what makes the claim falsifiable (Q154).
|
|
|
|
Paths come from THIS FILE's location. The 2026-10-31 version hardcoded an
|
|
absolute home-directory path in both the scanner and the verifier, so a
|
|
clone's CHECK graded the host's tree and a mutated copy of the committed TSV
|
|
still read PASS; `--selftest` cell `no-host-path` greps the sources for that
|
|
class, because this file's own fix must not reintroduce it.
|
|
|
|
EXIT CODES -- the archive's convention
|
|
0 the TSV and the report re-derive from the tree they describe
|
|
1 a fault: a row, a file-set entry or a reported count disagrees, or the
|
|
committed TSV is not the bytes the pin names
|
|
2 could not grade: the report, the TSV or the pin is missing/unreadable, the
|
|
pin names no rev, the rev cannot be extracted, or the repository root
|
|
cannot be found. Never a pass.
|
|
|
|
--selftest runs the cells in a temp tree: one green control, eight mutants and
|
|
four pin-contract cells, so a green reading is never the only thing this
|
|
checker can print.
|
|
|
|
NOTE ON THE SECOND IMPLEMENTATION OF `is_bare`
|
|
It reads operators on the raw string and only checks quoting for parity, so an
|
|
operator inside a quoted argument reads as not bare where the shlex-based rule
|
|
would read it as bare. That is deliberate and conservative (it over-reports
|
|
"not bare", never over-reports "bare"), and `rule-agreement` fails loudly if
|
|
the two implementations ever disagree on a command this corpus actually holds.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HERE)
|
|
|
|
import draft_compliance_rule as rule # noqa: E402
|
|
|
|
# The path inside the repository that holds this lane. Pinned grading archives
|
|
# exactly this directory at the pinned rev; nothing outside it is read.
|
|
PATHSPEC = "wizard-work/quill"
|
|
|
|
# The report's Evidence table, label -> the metric it claims. The labels are
|
|
# matched verbatim; a missing label is a fault (the report must say what it
|
|
# measured).
|
|
REPORT_LABELS = (
|
|
("Total `.md` drafts scanned", "total"),
|
|
("Drafts with `VERDICT:`", "verdict"),
|
|
("Drafts with `CHECK:`", "check"),
|
|
("Drafts with a bare `CHECK:` command", "bare"),
|
|
("Drafts failing at least one column", "failing"),
|
|
("`VERDICT:` and `CHECK:` but a non-bare command", "nonbare_both"),
|
|
)
|
|
|
|
NULL_RE = re.compile(r"(?:^|[\s;&|(])(true|false)(?=$|[\s;&|)])")
|
|
|
|
|
|
def own_is_bare(cmd):
|
|
"""The same contract as `rule.is_bare`, implemented differently.
|
|
|
|
Quoted regions are removed before the operator scan, so an operator inside a
|
|
quoted argument is an argument, not an operator; parity of quotes is checked
|
|
first, because a command that does not close its quotes does not tokenize
|
|
and is therefore not bare under the contract.
|
|
"""
|
|
if not cmd:
|
|
return False
|
|
if cmd.count("'") % 2 or cmd.count('"') % 2:
|
|
return False
|
|
stripped = re.sub(r"'[^']*'|\"[^\"]*\"", "''", cmd)
|
|
if ";" in stripped or "||" in stripped:
|
|
return False
|
|
rest = stripped.replace("&&", "")
|
|
if "&" in rest or "|" in rest:
|
|
return False
|
|
return NULL_RE.search(stripped) is None
|
|
|
|
|
|
def own_line_count(text):
|
|
"""Line count without splitlines(): newlines plus a final unterminated line."""
|
|
return text.count("\n") + (1 if text and not text.endswith("\n") else 0)
|
|
|
|
|
|
def own_rows(lane, exclude=rule.ARTIFACT_MD):
|
|
"""Re-derive the table from the files, without calling the generator."""
|
|
rows = []
|
|
names = sorted(n for n in os.listdir(lane)
|
|
if n.endswith(".md") and n != exclude)
|
|
for name in names:
|
|
path = os.path.join(lane, name)
|
|
if not os.path.isfile(path):
|
|
continue
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
text = f.read()
|
|
has_v = has_c = False
|
|
cmd = ""
|
|
for ln in text.split("\n"):
|
|
if ln.startswith("VERDICT:"):
|
|
has_v = True
|
|
elif ln.startswith("CHECK:"):
|
|
has_c = True
|
|
rest = ln[len("CHECK:"):].strip()
|
|
if rest and not cmd:
|
|
cmd = rest
|
|
rows.append((name, str(own_line_count(text)), str(has_v), str(has_c),
|
|
str(own_is_bare(cmd)), cmd))
|
|
return rows
|
|
|
|
|
|
def read_tsv(path):
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
raw = f.read().split("\n")
|
|
comment = ""
|
|
if raw and raw[0].startswith("#"):
|
|
comment = raw[0]
|
|
raw = raw[1:]
|
|
body = [ln for ln in raw if ln.strip()]
|
|
header = body[0].split("\t") if body else []
|
|
rows = [tuple(ln.split("\t")) for ln in body[1:]]
|
|
return comment, header, rows
|
|
|
|
|
|
def read_report_counts(path):
|
|
counts = {}
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
for ln in f:
|
|
m = re.match(r"^\|\s*(.+?)\s*\|\s*(\d+)\s*\|\s*$", ln.rstrip("\n"))
|
|
if m:
|
|
counts[m.group(1)] = int(m.group(2))
|
|
return counts
|
|
|
|
|
|
def metrics(rows):
|
|
def ok(r):
|
|
return r[2] == "True" and r[3] == "True" and r[4] == "True"
|
|
|
|
return {
|
|
"total": len(rows),
|
|
"verdict": sum(1 for r in rows if r[2] == "True"),
|
|
"check": sum(1 for r in rows if r[3] == "True"),
|
|
"bare": sum(1 for r in rows if r[4] == "True"),
|
|
"failing": sum(1 for r in rows if not ok(r)),
|
|
"nonbare_both": sum(1 for r in rows
|
|
if r[2] == "True" and r[3] == "True" and r[4] != "True"),
|
|
}
|
|
|
|
|
|
def check(lane=None, report=None, tsv=None):
|
|
"""Return (rc, faults, notes). Never raises for bad input."""
|
|
lane = lane or HERE
|
|
report = report or os.path.join(lane, rule.ARTIFACT_MD)
|
|
tsv = tsv or os.path.join(lane, rule.ARTIFACT_TSV)
|
|
faults, notes = [], []
|
|
|
|
if not os.path.isdir(lane):
|
|
return 2, ["lane directory missing: %s" % lane], notes
|
|
for path, what in ((report, "report"), (tsv, "TSV")):
|
|
if not os.path.isfile(path):
|
|
return 2, ["%s missing: %s" % (what, path)], notes
|
|
|
|
try:
|
|
comment, header, claimed = read_tsv(tsv)
|
|
reported = read_report_counts(report)
|
|
except (OSError, UnicodeDecodeError) as exc:
|
|
return 2, ["cannot read the artifact: %s" % exc], notes
|
|
|
|
if tuple(header) != rule.COLUMNS:
|
|
faults.append("TSV header is %r, contract is %r"
|
|
% (tuple(header), rule.COLUMNS))
|
|
|
|
derived = own_rows(lane)
|
|
der_map = {r[0]: r for r in derived}
|
|
claim_map = {r[0]: r for r in claimed}
|
|
|
|
for name in sorted(set(claim_map) - set(der_map)):
|
|
faults.append("TSV row names a file the lane does not hold: %s" % name)
|
|
for name in sorted(set(der_map) - set(claim_map)):
|
|
faults.append("lane file has no TSV row: %s" % name)
|
|
for name in sorted(set(der_map) & set(claim_map)):
|
|
if der_map[name] != claim_map[name]:
|
|
faults.append("row mismatch for %s: claimed=%r derived=%r"
|
|
% (name, claim_map[name], der_map[name]))
|
|
|
|
expected = "# measured %d markdown files" % len(derived)
|
|
if comment != expected:
|
|
faults.append("TSV comment is %r, measured %r" % (comment, expected))
|
|
|
|
m = metrics(derived)
|
|
for label, key in REPORT_LABELS:
|
|
if label not in reported:
|
|
faults.append("report table row missing: %r" % label)
|
|
elif reported[label] != m[key]:
|
|
faults.append("report says %r = %d, measured %d"
|
|
% (label, reported[label], m[key]))
|
|
|
|
# The contract has two implementations; they must agree on every command the
|
|
# corpus carries, or one of them is grading a different rule.
|
|
for r in derived:
|
|
if rule.is_bare(r[5]) != own_is_bare(r[5]):
|
|
faults.append("rule divergence on %s: rule=%s verifier=%s cmd=%r"
|
|
% (r[0], rule.is_bare(r[5]), own_is_bare(r[5]), r[5]))
|
|
|
|
notes.append("population %d, verdict %d, check %d, bare %d, failing %d"
|
|
% (m["total"], m["verdict"], m["check"], m["bare"], m["failing"]))
|
|
return (1 if faults else 0), faults, notes
|
|
|
|
|
|
# --- the pin: which revision this census describes ---------------------------
|
|
|
|
def repo_root(start):
|
|
"""Nearest ancestor that holds a `.git` entry, or None."""
|
|
path = os.path.abspath(start)
|
|
while True:
|
|
if os.path.exists(os.path.join(path, ".git")):
|
|
return path
|
|
parent = os.path.dirname(path)
|
|
if parent == path:
|
|
return None
|
|
path = parent
|
|
|
|
|
|
def materialize(rev, root, dest):
|
|
"""Extract PATHSPEC at `rev` under `dest`; return (lane_dir, error)."""
|
|
try:
|
|
src = subprocess.run(["git", "-C", root, "archive", rev, "--", PATHSPEC],
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
except OSError as exc:
|
|
return None, "cannot run git: %s" % exc
|
|
if src.returncode != 0:
|
|
return None, "cannot archive %s: %s" % (
|
|
rev, src.stderr.decode("utf-8", "replace").strip()[:200])
|
|
lane = os.path.join(dest, "lane")
|
|
os.makedirs(lane)
|
|
try:
|
|
untar = subprocess.run(["tar", "-x", "-C", lane], input=src.stdout,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
except OSError as exc:
|
|
return None, "cannot run tar: %s" % exc
|
|
if untar.returncode != 0:
|
|
return None, "cannot extract %s: %s" % (
|
|
rev, untar.stderr.decode("utf-8", "replace").strip()[:200])
|
|
return os.path.join(lane, PATHSPEC), ""
|
|
|
|
|
|
def read_pin(path):
|
|
"""The pin as a dict; raises OSError when it cannot be read."""
|
|
values = {}
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
parts = line.split(None, 1)
|
|
if len(parts) == 2:
|
|
values[parts[0]] = parts[1].strip()
|
|
return values
|
|
|
|
|
|
def pin_verdict(pin, tsv):
|
|
"""(rc, faults) for the pin contract: 2 ungradable, 1 not the bytes, 0 ok.
|
|
|
|
The digest is what makes "the committed table" a fixed claim: without it a
|
|
silent edit to the TSV would be graded as the new truth.
|
|
"""
|
|
try:
|
|
values = read_pin(pin)
|
|
except OSError as exc:
|
|
return 2, ["pin missing or unreadable: %s" % exc]
|
|
if not values.get("rev"):
|
|
return 2, ["pin names no rev: %s" % pin]
|
|
try:
|
|
with open(tsv, "rb") as f:
|
|
digest = hashlib.sha256(f.read()).hexdigest()
|
|
except OSError as exc:
|
|
return 2, ["TSV unreadable: %s" % exc]
|
|
want = values.get("tsv")
|
|
if want and want != digest:
|
|
return 1, ["committed TSV is not the pinned bytes: sha256 %s, pin %s"
|
|
% (digest, want)]
|
|
return 0, []
|
|
|
|
|
|
def grade_at_rev(rev, claim_dir, lane=None):
|
|
"""Grade the artifact in `claim_dir` against the drafts at `rev`."""
|
|
root = repo_root(claim_dir)
|
|
if root is None:
|
|
return 2, ["cannot find the repository root above %s" % claim_dir], []
|
|
tmp = tempfile.mkdtemp(prefix="q154-rev-")
|
|
try:
|
|
tree, error = materialize(rev, root, tmp)
|
|
if tree is None:
|
|
return 2, [error], []
|
|
return check(tree, os.path.join(claim_dir, rule.ARTIFACT_MD),
|
|
os.path.join(claim_dir, rule.ARTIFACT_TSV))
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
|
def drift_note(pinned_rows, live_rows):
|
|
"""What moved between the pinned census and the live tree. Advisory text."""
|
|
pin_map = {r[0]: r for r in pinned_rows}
|
|
live_map = {r[0]: r for r in live_rows}
|
|
added = sorted(set(live_map) - set(pin_map))
|
|
gone = sorted(set(pin_map) - set(live_map))
|
|
changed = sorted(n for n in set(pin_map) & set(live_map)
|
|
if pin_map[n] != live_map[n])
|
|
text = "drift since the pin: %d added, %d changed, %d removed" % (
|
|
len(added), len(changed), len(gone))
|
|
if added:
|
|
text += "; added: " + ", ".join(added[:6]) + (" ..." if len(added) > 6 else "")
|
|
return text
|
|
|
|
|
|
FIXTURE_FILES = {
|
|
"good.md": "# t\n\nVERDICT: ok\nCHECK: python3 x.py\n",
|
|
"plain.md": "# nothing\n",
|
|
"piped.md": "# t\n\nVERDICT: ok\nCHECK: python3 x.py | grep -q ok\n",
|
|
}
|
|
|
|
|
|
def _fixture_report(claimed):
|
|
lines = ["# fixture", "", "| Metric | Value |", "|---|---|"]
|
|
for label, _key in REPORT_LABELS:
|
|
lines.append("| %s | %d |" % (label, claimed[label]))
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def _fixture_tree(root, mutate=None):
|
|
lane = os.path.join(root, "lane")
|
|
os.makedirs(lane)
|
|
for name, text in FIXTURE_FILES.items():
|
|
with open(os.path.join(lane, name), "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
rows = own_rows(lane)
|
|
m = metrics(rows)
|
|
claimed = {label: m[key] for label, key in REPORT_LABELS}
|
|
report = os.path.join(lane, rule.ARTIFACT_MD)
|
|
with open(report, "w", encoding="utf-8") as f:
|
|
f.write(_fixture_report(claimed))
|
|
tsv = os.path.join(lane, rule.ARTIFACT_TSV)
|
|
with open(tsv, "w", encoding="utf-8") as f:
|
|
f.write("# measured %d markdown files\n" % len(rows))
|
|
f.write("\t".join(rule.COLUMNS) + "\n")
|
|
for r in rows:
|
|
f.write("\t".join(r) + "\n")
|
|
if mutate:
|
|
mutate(lane, report, tsv)
|
|
return lane, report, tsv
|
|
|
|
|
|
def selftest(verbose=True):
|
|
cells, failed = [], []
|
|
|
|
def cell(name, want, mutate=None):
|
|
tmp = tempfile.mkdtemp(prefix="q142-")
|
|
try:
|
|
lane, report, tsv = _fixture_tree(tmp, mutate)
|
|
got, faults, _notes = check(lane, report, tsv)
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
cells.append(name)
|
|
if got != want:
|
|
failed.append((name, want, got, faults[:3]))
|
|
if verbose:
|
|
print("%-22s rc=%d want=%d %s"
|
|
% (name, got, want, "ok" if got == want else "FAIL"))
|
|
|
|
def bump_line(lane, report, tsv):
|
|
with open(os.path.join(lane, "good.md"), "a", encoding="utf-8") as f:
|
|
f.write("one more line\n")
|
|
|
|
def drop_verdict(lane, report, tsv):
|
|
path = os.path.join(lane, "good.md")
|
|
with open(path, encoding="utf-8") as f:
|
|
text = f.read()
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(text.replace("VERDICT: ok\n", ""))
|
|
|
|
def drop_row(lane, report, tsv):
|
|
with open(tsv, encoding="utf-8") as f:
|
|
lines = f.read().split("\n")
|
|
with open(tsv, "w", encoding="utf-8") as f:
|
|
f.write("\n".join(ln for ln in lines if not ln.startswith("piped.md")))
|
|
|
|
def extra_file(lane, report, tsv):
|
|
with open(os.path.join(lane, "late.md"), "w", encoding="utf-8") as f:
|
|
f.write("# late\n")
|
|
|
|
def no_report(lane, report, tsv):
|
|
os.remove(report)
|
|
|
|
def no_tsv(lane, report, tsv):
|
|
os.remove(tsv)
|
|
|
|
def stale_count(lane, report, tsv):
|
|
with open(report, encoding="utf-8") as f:
|
|
text = f.read()
|
|
text = re.sub(r"(\| Total `\.md` drafts scanned \| )3( \|)",
|
|
r"\g<1>2\g<2>", text)
|
|
with open(report, "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
|
|
def check_line_edited(lane, report, tsv):
|
|
with open(os.path.join(lane, "good.md"), "w", encoding="utf-8") as f:
|
|
f.write("# t\n\nVERDICT: ok\nCHECK: python3 y.py\n")
|
|
|
|
cell("green-control", 0)
|
|
cell("line-count-moved", 1, bump_line)
|
|
cell("verdict-removed", 1, drop_verdict)
|
|
cell("row-deleted", 1, drop_row)
|
|
cell("file-without-row", 1, extra_file)
|
|
cell("report-missing", 2, no_report)
|
|
cell("tsv-missing", 2, no_tsv)
|
|
cell("report-count-stale", 1, stale_count)
|
|
cell("check-line-edited", 1, check_line_edited)
|
|
|
|
# The pin contract: a snapshot is graded against the bytes and the rev it
|
|
# names, and an unreadable pin is ungradable rather than a pass.
|
|
good_tsv = "x\n"
|
|
|
|
def pin_cell(name, want, pin_text):
|
|
tmp = tempfile.mkdtemp(prefix="q154-pin-")
|
|
try:
|
|
tsv = os.path.join(tmp, "t.tsv")
|
|
with open(tsv, "w", encoding="utf-8") as f:
|
|
f.write(good_tsv)
|
|
pin = os.path.join(tmp, "p.pin")
|
|
if pin_text is not None:
|
|
with open(pin, "w", encoding="utf-8") as f:
|
|
f.write(pin_text)
|
|
got, faults = pin_verdict(pin, tsv)
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
cells.append(name)
|
|
if got != want:
|
|
failed.append((name, want, got, faults[:2]))
|
|
if verbose:
|
|
print("%-22s rc=%d want=%d %s"
|
|
% (name, got, want, "ok" if got == want else "FAIL"))
|
|
|
|
digest = hashlib.sha256(good_tsv.encode("utf-8")).hexdigest()
|
|
pin_cell("pin-ok", 0, "rev 0123456789abcdef\ntsv %s\n" % digest)
|
|
pin_cell("pin-tsv-digest-moved", 1, "rev 0123456789abcdef\ntsv %s\n" % ("0" * 64))
|
|
pin_cell("pin-no-rev", 2, "tsv %s\n" % digest)
|
|
pin_cell("pin-absent", 2, None)
|
|
|
|
# The two implementations of the rule, over the fixture cases and this
|
|
# directory's own corpus. A divergence here is what the 2026-10-31 copies had.
|
|
divergence = [cmd for cmd, want, _why in rule.SELFTEST_CASES
|
|
if own_is_bare(cmd) != want]
|
|
for r in own_rows(HERE):
|
|
if rule.is_bare(r[5]) != own_is_bare(r[5]):
|
|
divergence.append(r[0])
|
|
cells.append("rule-agreement")
|
|
if divergence:
|
|
failed.append(("rule-agreement", 0, 1, divergence[:3]))
|
|
if verbose:
|
|
print("%-22s %s" % ("rule-agreement",
|
|
"ok" if not divergence else "FAIL %r" % divergence[:3]))
|
|
|
|
# No host path in the sources: the 2026-10-31 defect, graded at the source.
|
|
offenders = []
|
|
needle = os.sep + "root" + os.sep
|
|
for name in (os.path.basename(__file__), "draft_compliance_report.py",
|
|
"draft_compliance_rule.py"):
|
|
with open(os.path.join(HERE, name), encoding="utf-8") as f:
|
|
if needle in f.read():
|
|
offenders.append(name)
|
|
cells.append("no-host-path")
|
|
if offenders:
|
|
failed.append(("no-host-path", 0, 1, offenders))
|
|
if verbose:
|
|
print("%-22s %s" % ("no-host-path",
|
|
"ok" if not offenders else "FAIL %r" % offenders))
|
|
|
|
print("\nselftest: %d cells, %d passed, %d failed"
|
|
% (len(cells), len(cells) - len(failed), len(failed)))
|
|
for name, want, got, detail in failed:
|
|
print(" FAIL %s: rc %d wanted %d -- %r" % (name, got, want, detail))
|
|
return 1 if failed else 0
|
|
|
|
|
|
def _emit(rc, faults, notes, mode):
|
|
for note in notes:
|
|
print(note)
|
|
if rc == 0:
|
|
print("VERDICT: PASS -- TSV and report re-derive from %s (%s)"
|
|
% (mode, notes[0] if notes else "no counts"))
|
|
elif rc == 2:
|
|
print("VERDICT: UNGRADABLE")
|
|
for f in faults:
|
|
print(" -", f)
|
|
else:
|
|
print("VERDICT: FAIL")
|
|
for f in faults:
|
|
print(" -", f)
|
|
return rc
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(
|
|
description="Verify the quill draft-compliance report.")
|
|
ap.add_argument("--lane", default=None,
|
|
help="grade this directory directly (fixtures; default: "
|
|
"the pinned revision)")
|
|
ap.add_argument("--report", default=None, help="report .md (default: in --lane)")
|
|
ap.add_argument("--tsv", default=None, help="report .tsv (default: in --lane)")
|
|
ap.add_argument("--pin", default=None,
|
|
help="pin file (default: %s beside the artifact)"
|
|
% rule.ARTIFACT_PIN)
|
|
ap.add_argument("--rev", default=None,
|
|
help="grade the committed artifact against the lane at REV")
|
|
ap.add_argument("--worktree", action="store_true",
|
|
help="grade against the live worktree instead of the pin")
|
|
ap.add_argument("--selftest", action="store_true", help="run the cells")
|
|
a = ap.parse_args(argv)
|
|
|
|
if a.selftest:
|
|
return selftest()
|
|
|
|
claim = a.lane or HERE
|
|
report = a.report or os.path.join(claim, rule.ARTIFACT_MD)
|
|
tsv = a.tsv or os.path.join(claim, rule.ARTIFACT_TSV)
|
|
pin = a.pin or os.path.join(claim, rule.ARTIFACT_PIN)
|
|
|
|
if a.worktree or a.lane:
|
|
rc, faults, notes = check(claim, report, tsv)
|
|
mode = "the live worktree" if a.worktree else "the tree at %s" % claim
|
|
elif a.rev:
|
|
rc, faults, notes = grade_at_rev(a.rev, claim)
|
|
mode = "the lane at %s" % a.rev
|
|
else:
|
|
rc_pin, faults_pin = pin_verdict(pin, tsv)
|
|
if rc_pin:
|
|
return _emit(rc_pin, faults_pin, [], "the pin")
|
|
rev = read_pin(pin)["rev"]
|
|
rc, faults, notes = grade_at_rev(rev, claim)
|
|
mode = "the pinned revision %s" % rev[:12]
|
|
|
|
# A snapshot is graded against the revision it names; what has landed since
|
|
# is drift to report, not a fault of the table. `--worktree` grades it.
|
|
if mode != "the live worktree":
|
|
try:
|
|
notes = list(notes) + [drift_note(read_tsv(tsv)[2], own_rows(HERE))]
|
|
except OSError:
|
|
pass
|
|
return _emit(rc, faults, notes, mode)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|