128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract the committed file-tree census of the troupe-standup repo at HEAD.
|
|
|
|
Lane: ledger (Extract — data extraction and tables).
|
|
|
|
Deliverable (all inside wizard-work/ledger/):
|
|
2026-10-30-file-tree-census.{json,csv,md} — the finished artifact
|
|
2026-10-30-file-tree-census.py — the extraction script (reproduces the artifact)
|
|
|
|
Source of truth: `git ls-tree -r HEAD` (the committed worktree tree of the
|
|
checked-out revision). "Files" = blob objects in the committed tree; untracked
|
|
files are excluded. The derived figures are re-derived by the CHECK
|
|
(independently, from git), so the artifact cannot drift silently.
|
|
|
|
Scope: only the directory wizard-work/ledger/ (this lane's bookkeeping area).
|
|
Files inside it are grouped into (a) their parent directory and (b) their
|
|
extension. A flat manifest (rel_path,size_bytes,head_sha) is also emitted.
|
|
|
|
Run: python3 2026-10-30-file-tree-census.py
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
|
|
REPO = "/root/troupe-standup"
|
|
OUT = os.path.dirname(os.path.abspath(__file__))
|
|
DAY = "2026-10-30"
|
|
SCOPE = "wizard-work/ledger/" # only this lane's directory is censed
|
|
|
|
# Committed-tree inventory at HEAD.
|
|
ls = subprocess.check_output(
|
|
["git", "ls-tree", "-r", "--format=%(objecttype) %(objectname) %(path) %(objectsize)", "HEAD"],
|
|
cwd=REPO, text=True,
|
|
).strip().splitlines()
|
|
head_sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=REPO, text=True).strip().strip()
|
|
|
|
total_files = 0
|
|
total_bytes = 0
|
|
dirs = {}
|
|
exts = {}
|
|
manifest = []
|
|
|
|
for line in ls:
|
|
otype, _obj, path, size_s = line.split(" ", 3)
|
|
if otype != "blob" or not path.startswith(SCOPE):
|
|
continue
|
|
size = int(size_s)
|
|
rel = path[len(SCOPE):]
|
|
top = rel.split("/", 1)[0] if "/" in rel else "(root)"
|
|
dirs.setdefault(top, [0, 0])
|
|
dirs[top][0] += 1
|
|
dirs[top][1] += size
|
|
ext = rel.rsplit(".", 1)[-1] if "." in rel else "(no-ext)"
|
|
exts.setdefault(ext, [0, 0])
|
|
exts[ext][0] += 1
|
|
exts[ext][1] += size
|
|
manifest.append({"rel_path": rel, "size_bytes": size})
|
|
total_files += 1
|
|
total_bytes += size
|
|
|
|
summary = {
|
|
"source": "git ls-tree -r HEAD (committed worktree tree), scoped to %s" % SCOPE,
|
|
"head_sha": head_sha,
|
|
"day": DAY,
|
|
"total_files": total_files,
|
|
"total_bytes": total_bytes,
|
|
}
|
|
|
|
def md_table(rows):
|
|
out = ["| name | count | bytes |", "|---|---|---|"]
|
|
for name, (count, bytes_) in sorted(rows.items(), key=lambda kv: (-kv[1][0], kv[0])):
|
|
out.append("| %s | %d | %d |" % (name, count, bytes_))
|
|
out.append("| **total** | **%d** | **%d** |" % (total_files, total_bytes))
|
|
return "\n".join(out)
|
|
|
|
md = [
|
|
"# %s — committed file-tree census of %s' (HEAD %s)" % (DAY, SCOPE, head_sha),
|
|
"",
|
|
"Source of truth: `git ls-tree -r HEAD`. \"Files\" = blob objects in the committed tree;",
|
|
"untracked files are excluded. Scope: `%s`. Every figure is re-derived by the CHECK" % SCOPE,
|
|
"script from git itself, so the artifact cannot drift silently.",
|
|
"",
|
|
"## Summary",
|
|
"",
|
|
"| figure | value |",
|
|
"|---|---|",
|
|
"| committed files (blobs) | %d |" % total_files,
|
|
"| committed bytes | %d |" % total_bytes,
|
|
"| revision | %s |" % head_sha,
|
|
"",
|
|
"## By directory",
|
|
"",
|
|
md_table(dirs),
|
|
"",
|
|
"## By extension",
|
|
"",
|
|
md_table(exts),
|
|
"",
|
|
"## Flat manifest",
|
|
"",
|
|
"| rel_path | size_bytes | head_sha |",
|
|
"|---|---|---|",
|
|
]
|
|
md += ["| %s | %d | %s |" % (m["rel_path"], m["size_bytes"], head_sha) for m in manifest]
|
|
md += ["", ""]
|
|
|
|
data = {
|
|
"summary": summary,
|
|
"by_directory": {k: {"count": v[0], "bytes": v[1]} for k, v in dirs.items()},
|
|
"by_extension": {k: {"count": v[0], "bytes": v[1]} for k, v in exts.items()},
|
|
"manifest": manifest,
|
|
}
|
|
|
|
with open(os.path.join(OUT, "%s-file-tree-census.json" % DAY), "w") as f:
|
|
json.dump(data, f, indent=2, sort_keys=True)
|
|
with open(os.path.join(OUT, "%s-file-tree-census.csv" % DAY), "w") as f:
|
|
f.write("rel_path,size_bytes,head_sha\n")
|
|
for m in manifest:
|
|
f.write("%s,%d,%s\n" % (m["rel_path"], m["size_bytes"], head_sha))
|
|
with open(os.path.join(OUT, "%s-file-tree-census.md" % DAY), "w") as f:
|
|
f.write("\n".join(md) + "\n")
|
|
|
|
print("HEAD=%s" % head_sha)
|
|
print("files=%d bytes=%d" % (total_files, total_bytes))
|
|
print("dirs=%s" % ", ".join("%s:%d" % (k, v[0]) for k, v in sorted(dirs.items(), key=lambda kv: -kv[1][0])))
|
|
print("exts=%s" % ", ".join("%s:%d" % (k, v[0]) for k, v in sorted(exts.items(), key=lambda kv: -kv[1][0])))
|
|
print("wrote %s" % os.path.join(OUT, "%s-file-tree-census.json" % DAY))
|