Files
troupe-standup/wizard-work/ledger/verify_tree_census.py
T
volt a2a5f6ee3a ledger: file-tree census gate grades the checkout it is run in (Q69 ledger leg)
verify.sh no longer cds to /root/troupe-standup; repo root comes from $0 /
__file__. Paths are read with ls-tree -z (raw), the census is pinned to the
commit it describes (repo_census.pin), and the verifier grades that pin, so a
later commit does not redden the gate. New committed extractor regenerates the
three TSVs; artifacts re-derived from 716c3b3 (5311 files, 217 dirs, 34 exts).
2026-11-05 14:06:35 +00:00

159 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Verify the file-tree census & extension census artifact against a fresh recompute.
Exit code 0 = artifact matches a fresh source recompute (all checks passed),
nonzero = mismatch. Not a tautology: it re-enumerates a git tree and compares
the produced tables field-for-field against the committed files.
The repository root is derived from __file__, never a hardcoded host path. The
tree graded is the one named in `repo_census.pin` -- the commit checked out when
the census was taken, immutable -- so this gate does not turn red merely because
HEAD has since moved. Paths are read with `git ls-tree -z` -- raw, not git's
quoted rendering. Run inside a `git archive` export (no .git) the export's own
files are walked instead; that export must be the pinned commit, which is the
case Q69's done-means asks for. The two paths the census run itself added (the
pin and the extractor) post-date the pinned commit and are excluded from a walk.
"""
from pathlib import Path
import csv
import os
import subprocess
import sys
from collections import Counter
REPO = Path(__file__).resolve().parents[2]
DEST = os.path.join(REPO, "wizard-work", "ledger")
FILES = ["repo_tree.tsv", "repo_exts.tsv", "repo_files.tsv"]
PIN_NAME = "repo_census.pin"
PIN_SELF = "wizard-work/ledger/" + PIN_NAME
# Added by the census run itself, so absent from the commit the census describes.
NEW_SELF = {PIN_SELF, "wizard-work/ledger/extract_tree_census.py"}
def read_pin():
fields = {}
with open(os.path.join(DEST, PIN_NAME), encoding="utf-8") as f:
for line in f:
if line.strip():
key, value = line.split(None, 1)
fields[key] = value.strip()
if "commit" not in fields or "files" not in fields:
raise SystemExit("FATAL %s lacks commit/files lines" % PIN_NAME)
return fields["commit"], int(fields["files"])
def enumerate_paths(commit):
"""Paths of the pinned commit, plus the source they were read from.
git present: `git ls-tree -z` on the pinned commit -- raw names.
export: every regular file in the export (content is the pinned commit plus
this run's two new files, which are dropped).
"""
if os.path.exists(os.path.join(str(REPO), ".git")):
proc = subprocess.run(["git", "ls-tree", "-r", "-z", "--name-only", commit],
cwd=REPO, capture_output=True)
if proc.returncode != 0:
return None, "git ls-tree rc=%d: %s" % (
proc.returncode,
proc.stderr.decode("utf-8", "replace").strip())
raw = proc.stdout.decode("utf-8", "surrogateescape")
return [p for p in raw.split("\0") if p], "git commit %s" % commit[:12]
walked = []
for root, dirs, files in os.walk(REPO):
dirs[:] = sorted(d for d in dirs if d != ".git")
for name in files:
rel = str(Path(os.path.join(root, name)).relative_to(REPO)
.as_posix())
if rel not in NEW_SELF:
walked.append(rel)
return sorted(walked), "export walk"
def read_tsv(name):
with open(os.path.join(DEST, name), newline="", encoding="utf-8") as f:
return list(csv.reader(f, delimiter="\t"))
def main():
failures = 0
def check(name, cond, detail=""):
nonlocal failures
print(("%s %s" % ("PASS" if cond else "FAIL", name)) + ((" " + detail) if (detail and not cond) else ""))
if not cond:
failures += 1
# 1) fresh enumeration of the pinned tree
pin, pin_files = read_pin()
paths, source = enumerate_paths(pin)
print("enumerate source: %s" % source)
if paths is None:
print("FATAL could not enumerate the tree: %s" % source)
raise SystemExit(2)
check("pin file count matches enumeration", len(paths) == pin_files,
"pin says %d, enumerated %d" % (pin_files, len(paths)))
check("enumeration lists files", len(paths) > 0, "got %d" % len(paths))
check("enumeration has no duplicate paths", len(paths) == len(set(paths)),
"%d duplicate(s)" % (len(paths) - len(set(paths))))
dir_counts = Counter()
ext_counts = Counter()
for p in paths:
d = os.path.dirname(p)
dir_counts[d if d else "(root)"] += 1
ext = os.path.splitext(p)[1].lstrip(".").lower() or "(none)"
ext_counts[ext] += 1
# 2) repo_tree.tsv
rows = read_tsv("repo_tree.tsv")
check("repo_tree header", rows[0] == ["rank", "directory", "file_count"], str(rows[0]))
data = rows[1:]
expected_tree = sorted(dir_counts.items(), key=lambda kv: (-kv[1], kv[0]))
check("repo_tree rows == directory groups",
len(data) == len(expected_tree),
"got %d, expected %d" % (len(data), len(expected_tree)))
check("repo_tree dir/counts match recompute",
[(r[1], int(r[2])) for r in data] == [(d, c) for d, c in expected_tree])
# 3) repo_exts.tsv
rows = read_tsv("repo_exts.tsv")
check("repo_exts header", rows[0] == ["rank", "extension", "file_count"], str(rows[0]))
data = rows[1:]
expected_ext = sorted(ext_counts.items(), key=lambda kv: (-kv[1], kv[0]))
check("repo_exts rows == extension groups",
len(data) == len(expected_ext),
"got %d, expected %d" % (len(data), len(expected_ext)))
check("repo_exts ext/counts match recompute",
[(r[1], int(r[2])) for r in data] == [(d, c) for d, c in expected_ext])
# 4) repo_files.tsv
rows = read_tsv("repo_files.tsv")
check("repo_files header", rows[0] == ["rank", "path"], str(rows[0]))
file_paths = [r[1] for r in rows[1:]]
check("repo_files data rows == enumeration rows",
len(file_paths) == len(paths),
"got %d, enumerated %d" % (len(file_paths), len(paths)))
check("repo_files rows equal enumeration set",
sorted(file_paths) == sorted(paths))
check("repo_files ranks are 1..N",
len(rows) > 1 and all(r[0] == str(i) for i, r in enumerate(rows[1:], 1)),
"rank sequence broken")
control = [r[1] for r in rows[1:] if any(ord(c) < 32 for c in r[1])]
check("repo_files paths carry no control characters", not control,
"%d row(s), first %r" % (len(control), control[:1]))
# 5) group sums reconcile to total
check("dir sum == total", sum(dir_counts.values()) == len(paths),
"got %d, total %d" % (sum(dir_counts.values()), len(paths)))
check("ext sum == total", sum(ext_counts.values()) == len(paths),
"got %d, total %d" % (sum(ext_counts.values()), len(paths)))
print("result:", "PASS" if failures == 0 else "FAIL")
sys.exit(1 if failures else 0)
if __name__ == "__main__":
main()