Files

136 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Build the ledger tracked-file census for /root/troupe-standup.
Reads the live git index (git ls-files), then writes:
wizard-work/ledger/ledger-tracked-files.csv one data row per tracked file
wizard-work/ledger/ledger-tracked-files.md mode counts + row tally
Every field is derived from real tool output (git ls-files / git cat-file -s);
nothing is fabricated here.
"""
from __future__ import annotations
import os
import subprocess
import sys
import hashlib
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
REPO = "/root/troupe-standup"
OUTDIR = os.path.join(REPO, "wizard-work", "ledger")
CSV_PATH = os.path.join(OUTDIR, "ledger-tracked-files.csv")
SUMMARY_PATH = os.path.join(OUTDIR, "ledger-tracked-files.md")
BAD_CHARS = {"\t", ",", "\"", "\n", "\r"}
def git(*args: str) -> str:
return subprocess.run(
["git", "-C", REPO] + list(args), check=True,
capture_output=True, text=True,
).stdout
def cat_file_size(sha1: str) -> int:
out = subprocess.run(
["git", "cat-file", "-s", sha1], check=True,
capture_output=True, text=True,
).stdout
return int(out.split()[0])
def main() -> None:
os.makedirs(OUTDIR, exist_ok=True)
paths = git("ls-files").splitlines()
paths.sort()
n = len(paths)
print(f"tracked files: {n}", file=sys.stderr)
# Index metadata: path -> (sha1, mode, stage)
sha1_of_path = {}
mode_of_sha1 = {}
stage_of_path = {}
for line in git("ls-files", "-s").splitlines():
head, sep, path = line.partition("\t")
if not sep:
continue
mode, sha1, stage = head.split()
sha1_of_path[path] = sha1
mode_of_sha1[sha1] = mode
stage_of_path[path] = stage
# Object length for each sha1 (git cat-file -s), parallelised.
def size_at(i: int) -> int:
return cat_file_size(sha1_of_path[paths[i]])
lengths = [0] * n
with ThreadPoolExecutor(max_workers=16) as pool:
future_to_i = {pool.submit(size_at, i): i for i in range(n)}
for fut in future_to_i:
lengths[future_to_i[fut]] = fut.result()
# Guard: raw paths must not contain CSV-special characters, else the
# row text would not round-trip through a plain one-line-per-row file.
weird = [p for p in paths if any(ch in BAD_CHARS for ch in p)]
if weird:
print(f"ABORT: paths contain CSV-special chars: {weird[:5]}", file=sys.stderr)
sys.exit(1)
rows = []
mode_counter = Counter()
for i, p in enumerate(paths, start=1):
sha1 = sha1_of_path[p]
mode = mode_of_sha1[sha1]
stage = stage_of_path[p]
mode_counter[mode] += 1
rows.append(f"{i},{p},{mode},{sha1},{lengths[i-1]},{stage}")
with open(CSV_PATH, "w", encoding="utf-8", newline="\n") as fh:
fh.write("row,path,mode,sha1,length_bytes,stage\n")
fh.write("\n".join(rows))
fh.write("\n")
md5 = hashlib.md5(open(CSV_PATH, "rb").read()).hexdigest()
print(f"wrote {CSV_PATH} md5={md5}", file=sys.stderr)
lines = [
"# Ledger: tracked-file census",
"",
f"- Generated: {datetime.now(timezone.utc).isoformat()}Z",
f"- Repository: {REPO}",
f"- Tracked files (git ls-files): {n}",
"",
"## Mode counts",
"",
"| mode | count |",
"| --- | --- |",
]
for m in sorted(mode_counter):
lines.append(f"| {m} | {mode_counter[m]} |")
lines += [
"",
"## Row tally",
"",
f"Total data rows: {n}",
"",
"## Verification",
"",
"A companion CHECK (printed in the standup note) diffs the CSV data rows, sorted,",
"against `git ls-files | sort`; it must be empty, and the data-row count must equal",
"the live `git ls-files | wc -l` count.",
"",
]
with open(SUMMARY_PATH, "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(lines))
print(f"wrote {SUMMARY_PATH}", file=sys.stderr)
print(f"modes: {dict(mode_counter)}", file=sys.stderr)
if __name__ == "__main__":
main()