Files
troupe-standup/wizard-work/sol/untracked-classify.py
T

124 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Classify every untracked file in the troupe repo into a persistent triage
bucket. Deterministic rule: the first matching prefix wins.
Output:
untracked-classify-2026-10-29.json (machine-readable: path, bucket, rule)
untracked-classify-summary.md (human readable, with total + per-bucket)
"""
import json
import subprocess
from collections import Counter, OrderedDict
REPO_ROOT = "/root/troupe-standup"
OUT_DIR = REPO_ROOT + "/wizard-work/sol"
# Ordered (bucket, canonical match). First match wins. The tooling directory
# "wizard-work/sol/" holds this classifer/checker/JSON/summary (the "tool" bucket),
# and the stale top-level "CHECK-sol-untracked-classify" is also tooling.
RULES = [
("tool", "wizard-work/sol/"),
("tool", "CHECK-sol-untracked-classify"),
("probe", "wizard-work/null/"),
("sphinx", "wizard-work/sphinx/"),
("wizard-work", "wizard-work/"),
("wizard-outputs", "wizard-outputs/"),
("replies", "replies/"),
("checkins", "checkins/"),
("docs", "docs/"),
("inbox", "inbox/"),
("recoveries", "recoveries/"),
("tools", "tools/"),
("dump", "d.jsonl"),
("dump", "s.json"),
("kanban", "kanban.db"),
("inventory", "repo_"),
("ci", "CHECK-"),
("ci", "."),
]
def classify(path):
for bucket, prefix in RULES:
if path == prefix or path.startswith(prefix):
return bucket
return "other"
def run_git(args):
return subprocess.run(
["git", "-C", REPO_ROOT] + args, check=True,
capture_output=True, text=True,
).stdout.splitlines()
def main():
# File set: independent enumeration via git LS.
# Subject = the untracked working-tree files that are NOT this tool's own
# output directory ("wizard-work/sol/") and not the stale top-level CI
# leftover; everything else is the classifiable triage subject.
files = run_git(["ls-files", "--others", "--exclude-standard"])
files = [f for f in files if f]
subject = [p for p in files
if not (p.startswith("wizard-work/sol/")
or p == "CHECK-sol-untracked-classify")]
classified = []
counts = Counter()
for path in sorted(subject):
bucket = classify(path)
counts[bucket] += 1
matched = next(p for b, p in RULES if b == bucket and
(path == p or path.startswith(p)))
classified.append({
"path": path,
"bucket": bucket,
"rule": matched,
})
assert len(classified) == len(subject), "dedup lost a file"
assert len(set(c["path"] for c in classified)) == len(subject), "duplicate path"
with open(OUT_DIR + "/untracked-classify-2026-10-29.json", "w") as f:
json.dump(classified, f, indent=2, sort_keys=True)
# Human summary
lines = []
lines.append("# Untraced-file triage: 2026-10-29")
lines.append("")
lines.append("Classified `%d` untracked files (enumerated via `git ls-files --others --exclude-standard`) "
"into %d bucket(s)." % (len(classified), len(counts)))
lines.append("")
lines.append("| Bucket | Files |")
lines.append("|---|---|")
for b in sorted(counts):
lines.append("| %s | %d |" % (b, counts[b]))
lines.append("| **total** | **%d** |" % len(classified))
lines.append("")
lines.append("## Sample")
lines.append("")
for c in classified[:8]:
lines.append("- `%s` -> `%s` (%s)" % (c["path"], c["bucket"], c["rule"]))
lines.append("")
lines.append("## Buckets (full)")
lines.append("")
for b in sorted(counts):
members = [c["path"] for c in classified if c["bucket"] == b]
lines.append("### %s (%d)" % (b, len(members)))
lines.append("")
for m in members:
lines.append("- `%s`" % m)
lines.append("")
with open(OUT_DIR + "/untracked-classify-summary.md", "w") as f:
f.write("\n".join(lines))
print("files=%d buckets=%d" % (len(classified), len(counts)))
for b in sorted(counts):
print(" %-12s %d" % (b, counts[b]))
print("wrote %s" % OUT_DIR)
if __name__ == "__main__":
main()