Files
troupe-standup/wizard-work/ledger/extract_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

91 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Regenerate the ledger file-tree census TSVs from a pinned commit.
Writes, beside this script:
repo_tree.tsv rank, directory, file_count tracked files grouped by directory
repo_exts.tsv rank, extension, file_count tracked files grouped by lowercased extension
repo_files.tsv rank, path every tracked file, one row
repo_census.pin the commit and path count this census describes
The subject is the commit checked out when this runs (`git rev-parse HEAD`), so
the census is a stable snapshot: the verifier grades it against that immutable
commit, not against a moving HEAD, and a later commit does not redden the gate.
A census cannot contain itself -- the files this run lands, including the pin,
are one commit newer than the tree it describes; that is stated in the README.
The repository root is derived from __file__, never a hardcoded host path.
Paths are read with `git ls-tree -r -z`, raw rather than through git's quoted
rendering, which is how an escaped blob once landed in repo_files.tsv.
Ordering is file_count descending then name ascending; repo_files.tsv is sorted
by path. Exit codes: 0 wrote all four files, 2 could not read the tree.
"""
from pathlib import Path
import csv
import subprocess
import sys
from collections import Counter
HERE = Path(__file__).resolve().parent
REPO = HERE.parents[1]
PIN_NAME = "repo_census.pin"
def git(repo, *args):
proc = subprocess.run(["git", "-C", str(repo)] + list(args),
capture_output=True)
if proc.returncode != 0:
print("FATAL git %s rc=%d: %s" % (" ".join(args), proc.returncode,
proc.stderr.decode("utf-8", "replace").strip()))
return None
return proc.stdout
def main():
commit_out = git(REPO, "rev-parse", "HEAD")
if commit_out is None:
return 2
commit = commit_out.decode().strip()
raw = git(REPO, "ls-tree", "-r", "-z", "--name-only", commit)
if raw is None:
return 2
paths = [p for p in raw.decode("utf-8", "surrogateescape").split("\0") if p]
if not paths:
print("FATAL commit %s listed no files" % commit)
return 2
dir_counts = Counter()
ext_counts = Counter()
for p in paths:
d = str(Path(p).parent).replace("\\", "/")
dir_counts[d if d != "." else "(root)"] += 1
ext = Path(p).suffix.lstrip(".").lower() or "(none)"
ext_counts[ext] += 1
tree_rows = sorted(dir_counts.items(), key=lambda kv: (-kv[1], kv[0]))
ext_rows = sorted(ext_counts.items(), key=lambda kv: (-kv[1], kv[0]))
file_rows = [(p,) for p in sorted(paths)]
def write_tsv(name, header, rows):
with open(HERE / name, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f, delimiter="\t")
w.writerow(header)
for i, row in enumerate(rows, 1):
w.writerow([i] + list(row))
print("wrote %s: %d data row(s)" % (name, len(rows)))
write_tsv("repo_tree.tsv", ["rank", "directory", "file_count"], tree_rows)
write_tsv("repo_exts.tsv", ["rank", "extension", "file_count"], ext_rows)
write_tsv("repo_files.tsv", ["rank", "path"], file_rows)
(HERE / PIN_NAME).write_text("commit %s\nfiles %d\n" % (commit, len(paths)))
print("pinned commit %s: %d files, %d directories, %d extensions"
% (commit, len(paths), len(tree_rows), len(ext_rows)))
return 0
if __name__ == "__main__":
sys.exit(main())