diff --git a/wizard-work/ledger/CHECK-troupe-tree-census b/wizard-work/ledger/CHECK-troupe-tree-census new file mode 100644 index 0000000..13a95a8 --- /dev/null +++ b/wizard-work/ledger/CHECK-troupe-tree-census @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# CHECK-troupe-tree-census +# +# Independently re-derives the top-level directory census of the troupe repo for a +# single pinned revision and compares it byte-for-byte against the committed +# artifact. Exit code is the verdict: 0 = correct, nonzero = wrong. +# +# The pinned revision is read from troupe-tree-census-2026-11-02.md (the "pin +# revision" line), so the check is deterministic: it grades a fixed revision +# rather than a moving HEAD, and it never imports the extractor or hardcodes +# numbers. Size derivation uses `git cat-file -s` (a different read path than the +# `ls-tree -l` size column used by the extractor). +set -u + +REPO="/root/troupe-standup" +MD="${REPO}/wizard-work/ledger/troupe-tree-census-2026-11-02.md" +ARTIFACT="${REPO}/wizard-work/ledger/troupe-tree-census-2026-11-02.csv" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +# --- 1. read the pinned revision from the md ----------------------------------- +REV="$(grep -oE '[0-9a-f]{40}' "$MD" | head -n1)" +if [ -z "$REV" ]; then + echo "CHECK: FAIL - no 40-hex pin revision found in the md" + exit 2 +fi +REV="$(git -C "$REPO" rev-parse "$REV")" || { echo "CHECK: FAIL - cannot resolve pin $REV"; exit 2; } + +# --- 2. independent re-derivation of the census for the pin -------------------- +: > "${WORK}/recomputed.csv" +python3 - "$WORK/recomputed.csv" "$REV" <<'PY' +import subprocess, sys +from collections import defaultdict + +out, rev = sys.argv[1], sys.argv[2] +counts = defaultdict(int) +sizes = defaultdict(int) + +ls = subprocess.run( + ["git", "-C", "/root/troupe-standup", "ls-tree", "-r", rev], + capture_output=True, text=True, check=True, +).stdout +for line in ls.splitlines(): + if not line.strip(): + continue + head, sep, path = line.partition("\t") + if not sep: + raise SystemExit("bad ls-tree line: missing tab") + fhead = head.split() + if len(fhead) < 3: + raise SystemExit("bad ls-tree line: too few fields") + sha = fhead[2] + size = subprocess.run( + ["git", "-C", "/root/troupe-standup", "cat-file", "-s", sha], + capture_output=True, text=True, check=True, + ).stdout.strip() + try: + size = int(size) + except ValueError: + size = 0 + top = "(root)" if "/" not in path else path.split("/", 1)[0] + counts[top] += 1 + sizes[top] += size + +with open(out, "w", encoding="utf-8", newline="\n") as fh: + fh.write("top_level,files,sized_bytes\n") + for top in sorted(counts): + fh.write(f"{top},{counts[top]},{sizes[top]}\n") +PY + +# --- 3. byte-for-byte compare with the committed artifact ----------------------- +if ! diff -u "${WORK}/recomputed.csv" "${ARTIFACT}" > "${WORK}/diff.txt"; then + echo "CHECK: FAIL (pin $REV) - recomputed census != artifact" + cat "${WORK}/diff.txt" + exit 1 +fi +echo "CHECK: PASS (pin $REV) - recomputed census identical to artifact" + +# --- 4. headline cross-checks against the pin revision ------------------------- +total_files=$(git -C "$REPO" ls-tree -r "$REV" | wc -l | tr -d ' ') +total_bytes=$(git -C "$REPO" ls-tree -r -l "$REV" | awk '{print $4}' | paste -sd+ | bc) +recomputed_files=$(cut -d, -f2 "${WORK}/recomputed.csv" | tail -n +2 | paste -sd+ | bc) +recomputed_bytes=$(cut -d, -f3 "${WORK}/recomputed.csv" | tail -n +2 | paste -sd+ | bc) + +if [ "$total_files" -ne "$recomputed_files" ]; then + echo "CHECK: FAIL (pin $REV) - file count mismatch: git=$total_files recomputed=$recomputed_files" + exit 1 +fi +if [ "$total_bytes" -ne "$recomputed_bytes" ]; then + echo "CHECK: FAIL (pin $REV) - byte total mismatch: git=$total_bytes recomputed=$recomputed_bytes" + exit 1 +fi + +echo "CHECK: headline ok ($recomputed_files files, $recomputed_bytes bytes)" +exit 0 diff --git a/wizard-work/ledger/_probe_counts.py b/wizard-work/ledger/_probe_counts.py new file mode 100644 index 0000000..0f789b1 --- /dev/null +++ b/wizard-work/ledger/_probe_counts.py @@ -0,0 +1,38 @@ +import subprocess +from collections import defaultdict + +REPO = "/root/troupe-standup" + +def parse(extra): + counts = defaultdict(int) + ls = subprocess.run(["git", "-C", REPO, "ls-tree", "-r", extra, "HEAD"], + capture_output=True, text=True, check=True).stdout + for line in ls.splitlines(): + if not line.strip(): + continue + head, sep, path = line.partition("\t") + fhead = head.split() + top = "(root)" if "/" not in path else path.split("/", 1)[0] + counts[top] += 1 + return counts + +for extra in ["-l", "-r"]: + c = parse(extra) + print("EXTRA", extra, "groups:", dict(c)) + +ls = subprocess.run(["git", "-C", REPO, "ls-tree", "-r", "HEAD"], + capture_output=True, text=True, check=True).stdout +lines = [l for l in ls.splitlines() if l.strip()] +print("total ls-tree lines:", len(lines)) +from collections import Counter +first = Counter() +rootfiles = 0 +for l in lines: + path = l.partition("\t")[2] + top = "(root)" if "/" not in path else path.split("/", 1)[0] + first[top] += 1 + if "/" not in path: + rootfiles += 1 +print("unique first-components:", sorted(first)) +print("root-group file count by direct count:", rootfiles) +print("sum of groups:", sum(first.values())) diff --git a/wizard-work/ledger/troupe-tree-census-2026-11-02.csv b/wizard-work/ledger/troupe-tree-census-2026-11-02.csv new file mode 100644 index 0000000..a1986c2 --- /dev/null +++ b/wizard-work/ledger/troupe-tree-census-2026-11-02.csv @@ -0,0 +1,15 @@ +top_level,files,sized_bytes +(root),31,2497350 +access,17,5414 +audits,2,15203 +career-lane,31,757169 +checkins,111,1198002 +decisions,5,12436 +docs,10,385225 +harness-reflections,12,33844 +inbox,1,9484 +notes,5,16982 +recoveries,63,1378660 +replies,434,2866416 +tools,27,928314 +wizard-work,4092,195498089 diff --git a/wizard-work/ledger/troupe-tree-census-2026-11-02.md b/wizard-work/ledger/troupe-tree-census-2026-11-02.md new file mode 100644 index 0000000..98ec8ae --- /dev/null +++ b/wizard-work/ledger/troupe-tree-census-2026-11-02.md @@ -0,0 +1,30 @@ +# Troupe repo -- top-level directory census (extract lane) + +- **pin revision:** `484821b2325b2b72c5f85e942c63f5a667dc4cfb` (HEAD at generation) +- **method:** `git ls-tree -r -l ` -> group every tracked file by + its top-level path component -> count files and sum blob sizes per group. + Working tree excluded. +- **headline:** 4841 tracked files, 205602588 bytes (~196.1 MiB), 14 top-level groups. + +| top_level | files | sized_bytes | +|---|---|---| +| (root) | 31 | 2497350 | +| access | 17 | 5414 | +| audits | 2 | 15203 | +| career-lane | 31 | 757169 | +| checkins | 111 | 1198002 | +| decisions | 5 | 12436 | +| docs | 10 | 385225 | +| harness-reflections | 12 | 33844 | +| inbox | 1 | 9484 | +| notes | 5 | 16982 | +| recoveries | 63 | 1378660 | +| replies | 434 | 2866416 | +| tools | 27 | 928314 | +| wizard-work | 4092 | 195498089 | + +**Reading:** the repository is a ledger of lanes - `wizard-work/` alone +holds 4092 files / 186.4 MB (~95% of the tree); the other 13 directories are +thin metadata, check-ins, and queue tooling. + +**Files:** `troupe_tree_census_2026_11_02.py` (extractor), `troupe-tree-census-2026-11-02.csv` (table), `CHECK-troupe-tree-census` (independent re-derivation grader). diff --git a/wizard-work/ledger/troupe_tree_census_2026_11_02.py b/wizard-work/ledger/troupe_tree_census_2026_11_02.py new file mode 100644 index 0000000..76959e7 --- /dev/null +++ b/wizard-work/ledger/troupe_tree_census_2026_11_02.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Extract the committed tree of the troupe repo into a top-level directory census. + +Extract lane ("ledger" bookkeeping): turn a repository tree into a table. +Scope: a single committed revision (the pin), never the working tree. + +Outputs (in the repo's ledger lane): + troupe-tree-census-2026-11-02.csv one row per top-level entry (for the pin) + troupe-tree-census-2026-11-02.md human-readable table + headline + pin + CHECK-troupe-tree-census independent re-derivation grader for the pin +""" + +import os +import subprocess + +REPO = "/root/troupe-standup" +OUT = os.path.join(REPO, "wizard-work/ledger") + +# Pin this revision so the census is a reproducible snapshot, not a live pulse. +PIN = subprocess.run( + ["git", "-C", REPO, "rev-parse", "HEAD"], + capture_output=True, text=True, check=True, +).stdout.strip() + + +def git(*args): + proc = subprocess.run( + ["git", "-C", REPO, *args], capture_output=True, text=True, check=True + ) + return proc.stdout + + +def derive(): + """Return ordered list of (top_level, agg) for the pinned revision.""" + out = git("ls-tree", "-r", "-l", PIN) + agg = {} + for line in out.splitlines(): + if not line.strip(): + continue + head, _, _ = line.partition("\t") + fhead = head.split() + if len(fhead) < 3: + continue + typ = fhead[1] + try: + size = int(fhead[3]) if len(fhead) >= 4 else 0 + except ValueError: + size = 0 + path = line.split("\t", 1)[1] + top = "(root)" if "/" not in path else path.split("/", 1)[0] + kind = "file" + key = (top, kind) + entry = agg.setdefault(key, {"files": 0, "sized_bytes": 0}) + entry["files"] += 1 + if kind == "file": + entry["sized_bytes"] += size + + def sort_key(item): + top, _ = item + if top == "(root)": + return ("\x00", top) + return ("\x01", top) + + return [(top, entry) for (top, _), entry in sorted(agg.items(), key=sort_key)] + + +def write_csv(rows, path): + with open(path, "w", encoding="utf-8", newline="\n") as fh: + fh.write("top_level,files,sized_bytes\n") + for top, entry in rows: + fh.write(f"{top},{entry['files']},{entry['sized_bytes']}\n") + + +def main(): + rows = derive() + csv_path = os.path.join(OUT, "troupe-tree-census-2026-11-02.csv") + write_csv(rows, csv_path) + md_path = os.path.join(OUT, "troupe-tree-census-2026-11-02.md") + with open(md_path, "w", encoding="utf-8") as fh: + fh.write("# Troupe repo -- top-level directory census (extract lane)\n\n") + fh.write("- **pin revision:** `%s` (HEAD at generation)\n" % PIN) + fh.write("- **method:** `git ls-tree -r -l ` -> group every tracked file by\n") + fh.write(" its top-level path component -> count files and sum blob sizes per group.\n") + fh.write(" Working tree excluded.\n") + fh.write("- **headline:** %d tracked files, %d bytes (~%.1f MiB), %d top-level groups.\n\n" + % (sum(e["files"] for _, e in rows), + sum(e["sized_bytes"] for _, e in rows), + sum(e["sized_bytes"] for _, e in rows) / 1048576.0, + len(rows))) + fh.write("| top_level | files | sized_bytes |\n|---|---|---|\n") + for top, entry in rows: + fh.write("| %s | %d | %d |\n" % (top, entry["files"], entry["sized_bytes"])) + fh.write("\n**Reading:** the repository is a ledger of lanes - `wizard-work/` alone\n") + fh.write("holds %d files / %.1f MB (~95%% of the tree); the other %d directories are\n" + % (rows[13][1]["files"], rows[13][1]["sized_bytes"] / 1048576.0, len(rows) - 1)) + fh.write("thin metadata, check-ins, and queue tooling.\n\n") + fh.write("**Files:** `troupe_tree_census_2026_11_02.py` (extractor), `" + "troupe-tree-census-2026-11-02.csv` (table), `CHECK-troupe-tree-census` " + "(independent re-derivation grader).\n" % ()) + print("rows:", len(rows)) + print("total_files:", sum(e["files"] for _, e in rows)) + print("total_bytes:", sum(e["sized_bytes"] for _, e in rows)) + print("pin:", PIN[:12]) + print("wrote", csv_path) + print("wrote", md_path) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())