Files
troupe-standup/wizard-work/sol/check_untracked_classify.py
T

130 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""CHECK -- sol: Triage - 2026-10-29.
Independent re-verification of wizard-work/sol/untracked-classify-2026-10-29.json
and its summary. The verdict is the process exit code (0 = correct,
nonzero = wrong). No fallback arms; a failing assertion is fatal.
"""
import json
import os
import re
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(os.path.dirname(HERE)) # /root/troupe-standup
JSON_PATH = os.path.join(HERE, "untracked-classify-2026-10-29.json")
SUMMARY_PATH = os.path.join(HERE, "untracked-classify-summary.md")
# Tooling directory ("wizard-work/sol/") and the stale top-level CI leftover
# hold this tool's own artifacts, which are not the classifiable subject.
def subject(path):
return not (path.startswith("wizard-work/sol/")
or path == "CHECK-sol-untracked-classify")
def live_untracked():
"""Enumeration of untracked files (same source as the generator, but parsed
and filtered here independently). Kept deterministic: git porcelain v1 with
--untracked-files=all summarizes untracked directories, which made the
file-set cross-check fragile, while the bucket re-derivation (step 3),
on-disk existence (step 2) and summary cross-check (step 4) are the real
correctness tests."""
out = subprocess.run(
["git", "-C", REPO_ROOT, "ls-files", "--others", "--exclude-standard"],
check=True, capture_output=True, text=True,
).stdout
return [p for p in out.splitlines() if p]
def independent_classify(path):
"""Re-derivation written as predicates, independent of the generator's
prefix-list loop. Must agree with untracked-classify.py for every file."""
if path.startswith("wizard-work/null"):
return "probe"
if path.startswith("wizard-work/sphinx"):
return "sphinx"
if path.startswith("wizard-work/"):
return "wizard-work"
if path.startswith("wizard-outputs/"):
return "wizard-outputs"
if path.startswith("replies/"):
return "replies"
if path.startswith("checkins/"):
return "checkins"
if path.startswith("docs/"):
return "docs"
if path.startswith("inbox/"):
return "inbox"
if path.startswith("recoveries/"):
return "recoveries"
if path.startswith("tools/"):
return "tools"
if path in ("d.jsonl", "s.json"):
return "dump"
if path == "kanban.db":
return "kanban"
if path.startswith("repo_"):
return "inventory"
if path.startswith("CHECK-") or path.startswith("."):
return "ci"
return "other"
def main():
errors = []
# 1) Independent file enumeration (git status porcelain, per-file).
live = live_untracked()
live = [p for p in live_untracked() if subject(p)]
want = json.load(open(JSON_PATH))
got_paths = sorted(c["path"] for c in want)
if sorted(live) != got_paths:
errors.append("file-set mismatch: live=%d json=%d" % (len(live), len(got_paths)))
# 2) Every committed path exists on disk and every bucket is in-vocabulary.
VOCAB = {"probe", "sphinx", "wizard-work", "wizard-outputs", "replies",
"checkins", "docs", "inbox", "recoveries", "tools", "dump",
"kanban", "inventory", "ci", "other"}
for c in want:
if not os.path.exists(os.path.join(REPO_ROOT, c["path"])):
errors.append("missing on disk: %s" % c["path"])
if c["bucket"] not in VOCAB:
errors.append("unknown bucket %r for %s" % (c["bucket"], c["path"]))
# 3) Independent re-derivation must agree with the committed bucket.
for c in want:
if independent_classify(c["path"]) != c["bucket"]:
errors.append("bucket mismatch for %s: committed=%r indep=%r" %
(c["path"], c["bucket"], independent_classify(c["path"])))
# 4) Summary cross-check.
text = open(SUMMARY_PATH).read()
total_num = None
for line in text.splitlines():
if '| **total**' in line:
total_num = int(line.split('**')[-2])
if total_num is None or total_num != len(want):
errors.append("summary total mismatch: %s" % text.split('**total**')[0])
counts = {}
for c in want:
counts[c["bucket"]] = counts.get(c["bucket"], 0) + 1
for b, n in sorted(counts.items()):
if not re.search(r"\|\s*%s\s*\|\s*%d\s*\|" % (re.escape(b), n), text):
errors.append("summary bucket %s missing/incorrect: %d" % (b, n))
if errors:
print("CHECK FAIL:")
for e in errors[:50]:
print(" - " + e)
print(" ...%d more" % (len(errors) - 50) if len(errors) > 50 else "")
sys.exit(1)
print("CHECK PASS: %d files classified into %d buckets; "
"independent re-derivation + enumeration agree; "
"summary cross-checks." % (len(want), len(counts)))
sys.exit(0)
if __name__ == "__main__":
main()