Files
troupe-standup/wizard-work/ledger/build_ledger_census.py
T

154 lines
5.4 KiB
Python

#!/usr/bin/env python3
# Ledger — wizard-work/ledger census (Extract lane).
#
# Walks /root/troupe-standup/wizard-work/ledger and emits a machine-verified
# snapshot as three artifacts:
# ledger-census-2026-10-28.md # human-readable report with tables
# ledger-census-2026-10-28.csv # flat: type, relpath, size, mtime, category
# ledger-census-2026-10-28.json # structured record (input to the gate)
#
# Measured from the live filesystem (os.stat, no symlink following). The rows
# below are excluded so the census is a stable snapshot of the *data* tree:
# build_ledger_census.py, verify_ledger_census.py,
# ledger-census-2026-10-28.md/.csv/.json
import os
import sys
import json
import csv
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path("/root/troupe-standup/wizard-work/ledger")
EXCLUDE_DIRS = {"__pycache__", ".git"}
EXCLUDED_NAMES = {
"build_ledger_census.py",
"verify_ledger_census.py",
"ledger-census-2026-10-28.md",
"ledger-census-2026-10-28.csv",
"ledger-census-2026-10-28.json",
}
# Category -> extensions (checked in this order).
CATEGORY_RULES = [
("script", {".py", ".sh"}),
("config", {".yaml", ".yml", ".json", ".toml", ".env", ".lock", ".ini", ".pem"}),
("doc", {".md", ".txt"}),
("data", {".csv", ".tsv", ".xml", ".db", ".sqlite"}),
("media", {".mp3", ".wav", ".png", ".jpg", ".jpeg", ".etag"}),
]
def classify(name: str) -> str:
suffix = os.path.splitext(name)[1].lower()
for cat, exts in CATEGORY_RULES:
if suffix in exts:
return cat
return "other"
def main() -> int:
if not ROOT.is_dir():
print(f"FATAL: missing {ROOT}", file=sys.stderr)
return 1
files = []
dirs = []
for dirpath, dirnames, filenames in os.walk(ROOT):
dirnames.sort()
dirnames[:] = sorted(d for d in dirnames if d not in EXCLUDE_DIRS)
for fn in sorted(filenames):
p = Path(dirpath) / fn
if p.name in EXCLUDED_NAMES:
continue
try:
st = p.stat()
except OSError:
continue
files.append({
"type": "file",
"rel": p.relative_to(ROOT).as_posix(),
"size": st.st_size,
"mtime": datetime.fromtimestamp(st.st_mtime, tz=timezone.utc).isoformat(),
"cat": classify(fn),
})
for dn in dirnames:
dirs.append({
"type": "dir",
"rel": os.path.relpath(str(Path(dirpath) / dn), str(ROOT)),
"size": 0,
"mtime": "",
"cat": "dir",
})
total_bytes = sum(f["size"] for f in files)
cat_counts = Counter(f["cat"] for f in files)
# ---- markdown report ----
md = []
w = md.append
w("# wizard-work/ledger census — 2026-10-28")
w("")
w("*source*: os.walk + os.stat over `{}` (no symlink following)".format(ROOT))
w("*classification*: extension-based per CATEGORY_RULES; row count = files + dirs")
w("")
w("## Headline")
w("")
w("| measure | value |")
w("| --- | --- |")
w("| files | {} |".format(len(files)))
w("| subdirectories | {} |".format(len(dirs)))
w("| total entries | {} |".format(len(files) + len(dirs)))
w("| total bytes | {:,} |".format(total_bytes))
w("| categories | {} |".format(len(cat_counts)))
w("")
w("## Category rollup")
w("")
w("| category | files | bytes |")
w("| --- | --- | --- |")
for cat in sorted(cat_counts):
b = sum(f["size"] for f in files if f["cat"] == cat)
w("| {} | {} | {:,} |".format(cat, cat_counts[cat], b))
w("")
w("## File inventory")
w("")
w("| category | relpath | size | mtime(UTC) |")
w("| --- | --- | --- | --- |")
for f in sorted(files, key=lambda x: (x["cat"], x["rel"])):
w("| {} | `{}` | {} | {} |".format(f["cat"], f["rel"], f["size"], f["mtime"]))
w("")
md_text = "\n".join(md)
# ---- flat CSV ----
csv_rows = [["type", "relpath", "size_bytes", "mtime_utc", "category"]]
for f in sorted(files, key=lambda x: (x["cat"], x["rel"])):
csv_rows.append([f["type"], f["rel"], str(f["size"]), f["mtime"], f["cat"]])
csv_text = "\n".join(",".join(csvRow) for csvRow in csv_rows)
# ---- JSON record ----
json_obj = {
"source": str(ROOT),
"classification_rules": {cat: sorted(exts) for cat, exts in CATEGORY_RULES},
"excluded_dirs": sorted(EXCLUDE_DIRS),
"excluded_names": sorted(EXCLUDED_NAMES),
"counts": {"files": len(files), "dirs": len(dirs), "total_entries": len(files) + len(dirs), "total_bytes": total_bytes},
"category_counts": {cat: cat_counts.get(cat, 0) for cat in [c for c, _ in CATEGORY_RULES] + ["other"]},
"files": files,
"dirs": dirs,
}
# ---- write ----
(ROOT / "ledger-census-2026-10-28.md").write_text(md_text, encoding="utf-8")
(ROOT / "ledger-census-2026-10-28.csv").write_text(csv_text + "\n", encoding="utf-8")
(ROOT / "ledger-census-2026-10-28.json").write_text(
json.dumps(json_obj, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
print("wrote markdown, csv, json")
print("files={} dirs={} bytes={:,} cats={}".format(
len(files), len(dirs), total_bytes, dict(sorted(cat_counts.items()))))
return 0
if __name__ == "__main__":
sys.exit(main())