#!/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()