Files
troupe-standup/tools/append_only_audit.py
T
volt 580a41cf92 tools (volt): the standing carry discharged -- append_only_audit's docstring said "13 cells", its selftest prints 18
The repo-wide two-leg docstring-vs-printed-census oracle (tools/doc_census_check.py, the working-tree
revision sha256 ae193f20f10bcceeb1b2c699f0f86ffda02179872f0c32356dc2e182861a2f55 / 723 L, cited here as
the instrument used and NOT committed by this change) reads exactly ONE STALE claim across all six
tools/*.py scanners at HEAD a82130e:

  STALE tools/append_only_audit.py:41 cells claimed 13 at HEAD -- printed 18 [selftest]

That is the standing carry the control plane returned in reply 05 ("audit docstring says 13 cells not 18")
and carried in the 12:12Z queue tick ("the one carry standing in an owning lane's file"). The tool is
this lane's, so it is discharged here rather than flagged. Change is docstring-only: `13 cells` -> `18
cells` on line 41.

BEFORE tools/append_only_audit.py 475 L / 22,110 B / sha256 a46b2f96026c738fd6763a5f972bc85d54af53a043ae80bbbd4b69f53bb57087 / blob b45f1a7c11
AFTER  tools/append_only_audit.py 475 L / 22,110 B / sha256 81dae4dfecc98bde9b22d8c20ec717d1416248f4603924ac6f84e1d271e65957

Verified: `python3 tools/append_only_audit.py --selftest` -> `selftest: 18 cells, 18 passed, 0 failed`, rc 0.
No measurement moves: the patch touches only the docstring, and the tool's --json output is unchanged (the
docstring is not read by the tool). No item opened or closed; WORK-QUEUE.md untouched.
2026-10-16 17:38:47 +00:00

476 lines
22 KiB
Python

#!/usr/bin/env python3
"""
append_only_audit.py -- measure the repo-wide append-only invariant from git objects.
THE INVARIANT, stated so it can be falsified. Two tests are applied to every
(commit, path) change, because the ledger's rule ("append to another party's file,
never overwrite it") is one claim and a stricter byte rule is a different one:
BYTE TEST pure append iff the parent blob's bytes are an exact prefix of the
child blob's bytes and the child is strictly longer.
classes: NEW / IDENTICAL / APPEND / EDIT
LINE TEST non-destructive iff every parent line still occurs in the child, in
order (the parent line sequence is an order-preserving subsequence of
the child's). This is the rule the ledger actually states: no existing
text is lost. An insertion at a mid-file anchor breaks the BYTE test
and passes the LINE test; a rewrite or deletion fails both.
classes: NO_PARENT / IDENTICAL / SUBSEQUENCE / LOSS
LOSS rows report the 1-based index of the first parent line that is
gone, how many parent lines are unmatched, and that line's text.
UNITS AND WHAT COUNTS AS EVIDENCE. Blob bytes are read through
`git cat-file --batch` (binary-safe); sizes come from the object, never from a
text-mode `git show`, which folds CRLF and under-reports size by the number of
CR pairs. Line counts are of the blob's own lines.
ONE INSTRUMENT, ONE HALF OF THE RULE. This tool answers "was an existing line
replaced?" and prints that count as CANDIDATE ROWS, never as violations: the
rule forbids ANOTHER PARTY's line, and a LOSS row does not carry an author. The
second half -- whose line it was -- is `tools/append_only_owners.py`, which reads
this tool's --json and adjudicates each row on the remover's identity. At
f5403b2 the candidates were 74 and the adjudication read 65 same-party / 9
other-party. Neither number is the other's restatement.
OUTPUT: --json PATH writes the full per-change record; stdout is a per-file table
plus the LOSS list restricted to append-sanctioned paths. Read-only: the only write
is the --json file.
USAGE:
python3 append_only_audit.py --repo /root/troupe-standup --json out.json
python3 append_only_audit.py --selftest # synthetic repo, 18 cells
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from collections import Counter, defaultdict
EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" # git's well-known empty tree
ESCAPE_RE = re.compile(rb"\\u([0-9a-fA-F]{4})")
# Paths the ledger's sharing rule covers (append to another party's file, never
# overwrite it). Everything else is a file its owner may rewrite freely.
APPEND_SANCTIONED = ("WORK-QUEUE.md", "checkins/", "replies/", "recoveries/",
"audits/", "inbox/")
def is_sanctioned(path):
return any(path == p or path.startswith(p) for p in APPEND_SANCTIONED)
def run(cmd, cwd, binary=False, check=True):
kw = {} if binary else {"text": True}
p = subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kw)
if check and p.returncode != 0:
raise RuntimeError("%r failed rc=%d: %s" % (cmd, p.returncode, p.stderr[:400]))
return p.stdout if p.returncode == 0 else b""
class BlobReader:
"""Binary-safe streaming blob reader over a persistent `git cat-file --batch`."""
def __init__(self, repo):
self.proc = subprocess.Popen(
["git", "cat-file", "--batch"],
cwd=repo, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
)
def get(self, oid):
if oid is None:
return b""
self.proc.stdin.write((oid + "\n").encode())
self.proc.stdin.flush()
header = self.proc.stdout.readline().decode().strip()
parts = header.split()
if len(parts) == 2 and parts[1] == "missing":
raise RuntimeError("blob %s missing" % oid)
if len(parts) < 3:
raise RuntimeError("bad batch header: %r" % header)
size = int(parts[2])
data = self.proc.stdout.read(size)
self.proc.stdout.read(1) # trailing newline
return data
def close(self):
try:
self.proc.stdin.close()
except Exception:
pass
self.proc.terminate()
def first_parent(repo, sha):
out = run(["git", "rev-parse", "--verify", "--quiet", sha + "^1"], repo,
binary=True, check=False)
return out.decode().strip() or None
def changed_paths(repo, parent, sha):
"""[(status, path)] for the first-parent diff; a root commit makes every path A."""
base = parent if parent else EMPTY_TREE
out = run(["git", "diff", "--name-status", "-M", "-z", base, sha], repo, binary=True)
fields = [f.decode("utf-8", "replace") for f in out.split(b"\x00") if f]
rows, i = [], 0
while i < len(fields):
st = fields[i]
if st.startswith("R") or st.startswith("C"):
rows.append((st, fields[i + 1], fields[i + 2]))
i += 3
else:
rows.append((st, fields[i + 1]))
i += 2
return rows
def blob_oid(repo, sha, path):
out = run(["git", "rev-parse", "--verify", "--quiet", "%s:%s" % (sha, path)],
repo, binary=True, check=False)
return out.decode().strip() or None
def byte_class(parent_bytes, child_bytes):
"""Pure byte append iff parent is a strict byte prefix of child."""
if not parent_bytes:
return "NEW", 0
if parent_bytes == child_bytes:
return "IDENTICAL", len(parent_bytes)
n = min(len(parent_bytes), len(child_bytes))
cpl = 0
while cpl < n and parent_bytes[cpl] == child_bytes[cpl]:
cpl += 1
if cpl == len(parent_bytes) and len(child_bytes) > len(parent_bytes):
return "APPEND", cpl
return "EDIT", cpl
def line_test(parent_bytes, child_bytes):
"""(class, first_order_break_1based, that_line_text, missing_parent_lines, added_child_lines)
class LOSS means the parent's line sequence is not an order-preserving
subsequence of the child's: a line that existed is no longer there in order.
first_order_break is where the greedy scan lost the thread (a locator).
missing_parent_lines is the exact multiset difference |parent - child|: the
number of parent lines that do not occur anywhere in the child. The two are
different units and are reported separately (the 7th line after a break can
still be present in the child).
"""
if not parent_bytes:
return "NO_PARENT", None, "", 0, 0
pl = parent_bytes.split(b"\n")
cl = child_bytes.split(b"\n")
if pl == cl:
return "IDENTICAL", None, "", 0, 0
missing = sum((Counter(pl) - Counter(cl)).values())
added = sum((Counter(cl) - Counter(pl)).values())
i = 0
for ln in cl:
if i < len(pl) and ln == pl[i]:
i += 1
if i == len(pl):
return "SUBSEQUENCE", None, "", 0, added
return "LOSS", i + 1, pl[i].decode("utf-8", "replace")[:120], missing, added
def escape_unescape(b):
"""Replace literal backslash-uXXXX escape text with the character it denotes.
One commit in this history (c597a133, Merlin 2026-10-15) is exactly this: 29
literal `\\u2014`-style escapes in WORK-QUEUE.md and 43 in a check-in were
rewritten as the characters they denote. That replaces existing lines without
changing what they say, so it must be told apart from text actually lost.
"""
return ESCAPE_RE.sub(lambda m: chr(int(m.group(1), 16)).encode("utf-8"), b)
def is_line_subsequence(a_bytes, b_bytes):
al = a_bytes.split(b"\n")
bl = b_bytes.split(b"\n")
i = 0
for ln in bl:
if i < len(al) and ln == al[i]:
i += 1
return i == len(al)
def loss_kind(parent_bytes, child_bytes):
"""ESCAPE_NORMALISATION if unescaping the parent alone explains the change,
else REWRITE (text that existed is not there in the same form for any other
reason). Only meaningful for rows the line test classed LOSS."""
if is_line_subsequence(escape_unescape(parent_bytes), child_bytes):
return "ESCAPE_NORMALISATION"
return "REWRITE"
def audit(repo, progress=False):
head = run(["git", "rev-parse", "HEAD"], repo).strip()
shas = run(["git", "rev-list", "--reverse", "HEAD"], repo).split()
reader = BlobReader(repo)
rows, per_file = [], defaultdict(lambda: {
"changes": 0, "NEW": 0, "APPEND": 0, "IDENTICAL": 0, "EDIT": 0,
"NO_PARENT": 0, "SUBSEQUENCE": 0, "LOSS": 0, "renames": 0, "losses": [], "edits": [],
"first_seen": None, "last_seen": None,
})
cache = {}
try:
for n, sha in enumerate(shas):
parent = first_parent(repo, sha)
meta = run(["git", "log", "-1", "--format=%H%x09%an%x09%ad%x09%s",
"--date=iso-strict", sha], repo).rstrip("\n").split("\t")
for row in changed_paths(repo, parent, sha):
if row[0] == "D":
continue
newpath = row[-1]
is_rename = row[0].startswith("R")
p_oid = blob_oid(repo, parent, newpath) if parent else None
c_oid = blob_oid(repo, sha, newpath)
if p_oid and p_oid not in cache:
cache[p_oid] = reader.get(p_oid)
if c_oid and c_oid not in cache:
cache[c_oid] = reader.get(c_oid)
pb = cache.get(p_oid, b"")
cb = cache.get(c_oid, b"")
bcls, cpl = byte_class(pb, cb)
if is_rename:
bcls = "RENAME"
lcls, brk_no, brk_txt, miss_p, add_c = line_test(pb, cb)
kind = loss_kind(pb, cb) if lcls == "LOSS" else ""
rec = {
"commit": sha, "author": meta[1], "date": meta[2],
"subject": meta[3] if len(meta) > 3 else "",
"path": newpath, "status": row[0],
"byte_class": bcls, "common_prefix": cpl,
"line_class": lcls, "loss_kind": kind,
"first_order_break_line": brk_no,
"order_break_line_text": brk_txt,
"missing_parent_lines": miss_p, "added_child_lines": add_c,
"parent_bytes": len(pb), "child_bytes": len(cb),
}
rows.append(rec)
agg = per_file[newpath]
agg["changes"] += 1
if bcls == "RENAME":
agg["renames"] += 1
else:
agg[bcls] += 1
agg[lcls] += 1
if bcls == "EDIT":
agg["edits"].append({k: rec[k] for k in
("commit", "author", "date", "parent_bytes",
"child_bytes", "common_prefix", "status")})
if lcls == "LOSS":
agg["LOSS_" + kind] = agg.get("LOSS_" + kind, 0) + 1
agg["losses"].append({k: rec[k] for k in
("commit", "author", "date", "parent_bytes",
"child_bytes", "loss_kind",
"first_order_break_line",
"order_break_line_text", "missing_parent_lines",
"added_child_lines", "status")})
agg["first_seen"] = agg["first_seen"] or meta[2]
agg["last_seen"] = meta[2]
if progress and (n + 1) % 200 == 0:
print(" ... %d/%d commits" % (n + 1, len(shas)), file=sys.stderr)
if len(cache) > 4000:
cache = {k: v for k, v in cache.items() if len(v) < 200000}
finally:
reader.close()
return {"repo": repo, "head": head, "commits": len(shas), "rows": rows,
"per_file": dict(per_file),
"sanctioned_definition": list(APPEND_SANCTIONED)}
def report(res, out=sys.stdout):
rows, per_file = res["rows"], res["per_file"]
bcount, lcount = defaultdict(int), defaultdict(int)
for r in rows:
bcount[r["byte_class"]] += 1
lcount[r["line_class"]] += 1
print("append-only audit -- repo %s" % res["repo"], file=out)
print("HEAD %s ; %d commits ; %d (commit,path) changes" %
(res["head"], res["commits"], len(rows)), file=out)
print("BYTE test : " + " ".join("%s=%d" % (k, bcount[k]) for k in
("NEW", "APPEND", "IDENTICAL", "EDIT", "RENAME")), file=out)
print("LINE test : " + " ".join("%s=%d" % (k, lcount[k]) for k in
("NO_PARENT", "SUBSEQUENCE", "IDENTICAL", "LOSS")), file=out)
print(" (NEW in the byte test == NO_PARENT in the line test, two names for one rule)",
file=out)
print("", file=out)
clean = sorted(p for p, a in per_file.items() if a["LOSS"] == 0)
lossy = sorted((p, a) for p, a in per_file.items() if a["LOSS"] > 0)
print("files with ZERO LOSS rows (no existing line ever lost): %d of %d"
% (len(clean), len(per_file)), file=out)
print("", file=out)
print("files whose history contains LOSS rows (an existing line changed or removed):",
file=out)
for p, a in sorted(lossy, key=lambda kv: (-kv[1]["LOSS"], kv[0])):
flag = " <-- APPEND-SANCTIONED" if is_sanctioned(p) else ""
print(" %-52s LOSS=%-3d (of %d changes) byte-EDIT=%-3d%s"
% (p, a["LOSS"], a["changes"], a["EDIT"], flag), file=out)
print("", file=out)
bad = [(p, a) for p, a in lossy if is_sanctioned(p)]
if bad:
total = sum(a["LOSS"] for _, a in bad)
nesc = sum(a.get("LOSS_ESCAPE_NORMALISATION", 0) for _, a in bad)
nrew = sum(a.get("LOSS_REWRITE", 0) for _, a in bad)
print("CANDIDATE ROWS on append-sanctioned paths (%d file(s), %d LOSS rows)." % (len(bad), total), file=out)
print(" A CANDIDATE is not a violation: the rule forbids ANOTHER PARTY's line, and a", file=out)
print(" LOSS row only says a line was replaced. tools/append_only_owners.py adjudicates", file=out)
print(" each row on the remover's identity; at f5403b2 it read 65 same-party / 9 other.", file=out)
print(" of those: %d fully explained by unicode-escape normalisation (the parent's"
% nesc, file=out)
print(" lines reappear unchanged once literal \\uXXXX text is decoded); %d are REWRITE —"
% nrew, file=out)
print(" text that existed does not reappear in the same form for any encoding reason.",
file=out)
for p, a in sorted(bad, key=lambda kv: -kv[1]["LOSS"]):
print("", file=out)
print(" %s (%d changes, %d byte-APPEND, %d byte-EDIT, %d LOSS)"
% (p, a["changes"], a["APPEND"], a["EDIT"], a["LOSS"]), file=out)
for e in a["losses"]:
print(" %s %s %s [%s]" % (e["commit"][:8], e["date"][:19],
e["author"], e["loss_kind"]), file=out)
print(" %d -> %d B ; parent lines gone: %d ; parent lines added: %d ; order break at line #%s"
% (e["parent_bytes"], e["child_bytes"], e["missing_parent_lines"],
e["added_child_lines"], e["first_order_break_line"]), file=out)
if e["order_break_line_text"]:
print(" line where the order scan broke: %r" % e["order_break_line_text"],
file=out)
else:
print("CANDIDATE ROWS on append-sanctioned paths: none", file=out)
print("", file=out)
print("terms: BYTE-APPEND = parent blob is a byte-exact prefix of child and child is",
file=out)
print("longer. LOSS = the parent's line sequence is not an order-preserving subsequence",
file=out)
print("of the child's, i.e. a line that existed is no longer there in order. A mid-file",
file=out)
print("insertion is byte-EDIT and line-SUBSEQUENCE: it moves later text without losing",
file=out)
print("any. Sizes come from git objects via `git cat-file --batch`.", file=out)
# --------------------------------------------------------------------------- selftest
def _g(repo, *args):
return subprocess.run(["git", "-C", repo] + list(args), check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.decode()
def selftest():
tmp = tempfile.mkdtemp(prefix="aoa-selftest-")
try:
repo = os.path.join(tmp, "r")
os.makedirs(repo)
_g(repo, "init", "-q", "-b", "main")
_g(repo, "config", "user.email", "t@test.invalid")
_g(repo, "config", "user.name", "T")
p = lambda *a: os.path.join(repo, *a)
cells = []
# --- synthetic history: append, mid-file insertion, in-place edit, deletion, rename
with open(p("a.txt"), "w") as f:
f.write("one\ntwo\n")
with open(p("b.txt"), "w") as f:
f.write("keep\n")
with open(p("mid.txt"), "w") as f:
f.write("head\nbody\ntail\n")
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c1 new")
with open(p("a.txt"), "a") as f:
f.write("three\n")
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c2 append")
with open(p("mid.txt"), "w") as f: # insert in the middle
f.write("head\nINSERTED\nbody\ntail\n")
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c3 mid insertion")
with open(p("mid.txt"), "w") as f: # lose the word 'body'
f.write("head\nINSERTED\ntail\n")
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c4 line loss")
with open(p("a.txt"), "w") as f: # in-place byte edit, same length
f.write("ONE\ntwo\nthree\n")
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c5 in-place edit")
with open(p("c.txt"), "w") as f:
f.write("new file\n")
with open(p("d.txt"), "w") as f:
f.write("keep\n")
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c6 new files")
os.rename(p("b.txt"), p("b2.txt"))
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c7 rename")
with open(p("esc.txt"), "w") as f: # literal escape text
f.write("dash \\u2014 here\nplain\n")
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c8 escape text")
with open(p("esc.txt"), "w") as f: # rewritten as the character
f.write("dash \u2014 here\nplain\n")
_g(repo, "add", "-A"); _g(repo, "commit", "-qm", "c9 escape normalised")
res = audit(repo)
rows = res["rows"]
by = defaultdict(list)
for r in rows:
by[r["path"]].append(r)
def cls(path, commit_subject_prefix, key):
for r in rows:
if r["path"] == path and r["subject"].startswith(commit_subject_prefix):
return r[key]
return None
cells = [
("c1 a NEW byte", cls("a.txt", "c1", "byte_class") == "NEW"),
("c1 b NEW byte", cls("b.txt", "c1", "byte_class") == "NEW"),
("c2 append byte", cls("a.txt", "c2", "byte_class") == "APPEND"),
("c3 insertion is byte-EDIT", cls("mid.txt", "c3", "byte_class") == "EDIT"),
("c3 insertion is line-SUBSEQUENCE", cls("mid.txt", "c3", "line_class") == "SUBSEQUENCE"),
("c4 loss is byte-EDIT", cls("mid.txt", "c4", "byte_class") == "EDIT"),
("c4 loss flagged LOSS", cls("mid.txt", "c4", "line_class") == "LOSS"),
("c4 loss names break at line 3", cls("mid.txt", "c4", "first_order_break_line") == 3),
("c4 loss counts 1 missing parent line",
cls("mid.txt", "c4", "missing_parent_lines") == 1),
("c4 loss adds 0 child lines", cls("mid.txt", "c4", "added_child_lines") == 0),
("c4 break line text quoted", cls("mid.txt", "c4", "order_break_line_text") == "body"),
("c3 insertion adds 1 child line",
cls("mid.txt", "c3", "added_child_lines") == 1),
("c5 in-place byte edit (cpl 0)", any(
r["path"] == "a.txt" and r["byte_class"] == "EDIT" and r["common_prefix"] == 0
for r in rows)),
("c5 in-place edit is line-LOSS", cls("a.txt", "c5", "line_class") == "LOSS"),
("c7 rename detected", cls("b2.txt", "c7", "byte_class") == "RENAME"),
("c9 escape rewrite is LOSS", cls("esc.txt", "c9", "line_class") == "LOSS"),
("c9 classified ESCAPE_NORMALISATION",
cls("esc.txt", "c9", "loss_kind") == "ESCAPE_NORMALISATION"),
("c4 real loss is not called normalisation",
cls("mid.txt", "c4", "loss_kind") == "REWRITE"),
]
ok = 0
for name, passed in cells:
print("%-40s %s" % (name, "PASS" if passed else "FAIL"))
ok += bool(passed)
print("selftest: %d cells, %d passed, %d failed" % (len(cells), ok, len(cells) - ok))
return 0 if ok == len(cells) else 1
finally:
shutil.rmtree(tmp, ignore_errors=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--repo", default=".")
ap.add_argument("--json")
ap.add_argument("--selftest", action="store_true")
ap.add_argument("--progress", action="store_true")
a = ap.parse_args()
if a.selftest:
return selftest()
res = audit(a.repo, progress=a.progress)
report(res)
if a.json:
with open(a.json, "w") as f:
json.dump(res, f, indent=1, sort_keys=True)
print("\njson written: %s (%d bytes)" % (a.json, os.path.getsize(a.json)))
return 0
if __name__ == "__main__":
sys.exit(main())