From 376afd33f4aab2a4f7a943faf0a3af0aa263dd2f Mon Sep 17 00:00:00 2001 From: sol Date: Sun, 1 Nov 2026 07:26:42 +0000 Subject: [PATCH] sol: untracked-file triage classification manifest + verifier --- CHECK-untracked-triad-2026-11-01 | 12 + .../sol/Untracked-Triad-2026-11-01.json | 668 ++++++++++++++++++ .../sol/build_untracked_triad_2026_11_01.py | 156 ++++ .../sol/verify_untracked_triad_2026_11_01.py | 134 ++++ 4 files changed, 970 insertions(+) create mode 100644 CHECK-untracked-triad-2026-11-01 create mode 100644 wizard-work/sol/Untracked-Triad-2026-11-01.json create mode 100644 wizard-work/sol/build_untracked_triad_2026_11_01.py create mode 100644 wizard-work/sol/verify_untracked_triad_2026_11_01.py diff --git a/CHECK-untracked-triad-2026-11-01 b/CHECK-untracked-triad-2026-11-01 new file mode 100644 index 0000000..ef4ee87 --- /dev/null +++ b/CHECK-untracked-triad-2026-11-01 @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# CHECK -- sol: Untracked-file triage classification (forge: verify lane). +# Exit 0 iff the committed Untracked-Triad-2026-11-01.json re-derives every +# classified path's category/decision from the canonical rule, and each path is +# still a real, untracked, non-ignored file on disk. +set -u + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR" && pwd)" +cd "$REPO_ROOT" || exit 2 + +exec python3 wizard-work/sol/verify_untracked_triad_2026_11_01.py diff --git a/wizard-work/sol/Untracked-Triad-2026-11-01.json b/wizard-work/sol/Untracked-Triad-2026-11-01.json new file mode 100644 index 0000000..ad8abe9 --- /dev/null +++ b/wizard-work/sol/Untracked-Triad-2026-11-01.json @@ -0,0 +1,668 @@ +{ + "artifact": "Untracked-Triad-2026-11-01.json", + "corpus_sha256": "321891e6717758c2018fcf7273dd6b37bc61c9325312b54f5a3b82134e1d8826", + "files": [ + "CHECK-sol-untracked-classify", + "CHECK-triage-2026-10-29", + "checkins/2026-10-27-sentinel.md", + "d.jsonl", + "docs/clawrouter-plan.md", + "docs/flux2-deploy.md", + "docs/video-deploy.md", + "inbox/dump_seed.py", + "kanban.db", + "recoveries/apply-queue-status-2026-10-25.md", + "repo_exts.tsv", + "repo_files.tsv", + "repo_tree.tsv", + "s.json", + "test_sample.txt", + "test_validation.txt", + "test_verification.txt", + "validate_artifact.py", + "wizard-work/null/.hermes-tmp.RqZp3z", + "wizard-work/null/README-gatehygiene.md", + "wizard-work/null/README-q62-scan.md", + "wizard-work/null/_probe10.py", + "wizard-work/null/_probe11.py", + "wizard-work/null/_probe12.py", + "wizard-work/null/_probe13.py", + "wizard-work/null/_probe14.py", + "wizard-work/null/_probe5.py", + "wizard-work/null/_probe6.py", + "wizard-work/null/_probe7.py", + "wizard-work/null/_probe8.py", + "wizard-work/null/_probe9.py", + "wizard-work/null/_probe_binding_defects.py", + "wizard-work/null/_probe_gh1.py", + "wizard-work/null/_probe_gh2.py", + "wizard-work/null/_probe_gh3.py", + "wizard-work/null/_probe_gh4.py", + "wizard-work/null/_probe_gh5.py", + "wizard-work/null/_probe_gh6.py", + "wizard-work/null/_probe_gh7.py", + "wizard-work/null/_probe_gh8.py", + "wizard-work/null/_probe_gh9.py", + "wizard-work/null/_probe_mut2.py", + "wizard-work/null/_probe_mut_check.py", + "wizard-work/null/_probe_q70_5h.py", + "wizard-work/null/_probe_repro1.py", + "wizard-work/null/_probe_repro2.py", + "wizard-work/null/_probe_repro3.py", + "wizard-work/null/_probe_repro4.py", + "wizard-work/null/_probe_repro5.py", + "wizard-work/null/_probe_repro6.py", + "wizard-work/null/_probe_repro7.py", + "wizard-work/null/check_gatehygiene.py", + "wizard-work/null/check_provenance.py", + "wizard-work/null/check_provenance_baseline.tsv", + "wizard-work/null/check_provenance_ledger.py", + "wizard-work/null/check_q62_scan.py", + "wizard-work/null/cli_contract.py", + "wizard-work/null/diffkit/__init__.py", + "wizard-work/null/diffkit/_flatpkg.py", + "wizard-work/null/diffkit/core.py", + "wizard-work/null/diffkit/diffkit_cli.py", + "wizard-work/null/diffkit/myers.py", + "wizard-work/null/diffkit_gate.py", + "wizard-work/null/gatehygiene.py", + "wizard-work/null/json_salvage.py", + "wizard-work/null/membershipkit/.hermes-tmp.sx2gIa", + "wizard-work/null/membershipkit/check_membershipkit.sh", + "wizard-work/null/membershipkit/mutate_membershipkit.py", + "wizard-work/null/membershipkit/test_membershipkit.py", + "wizard-work/null/mutate_resolver.py", + "wizard-work/null/mutate_salvage.py", + "wizard-work/null/q58_warncontrol.py", + "wizard-work/null/q62_scan.py", + "wizard-work/null/q70_sabotage.py", + "wizard-work/null/q76_binding.py", + "wizard-work/null/q80_warn.py", + "wizard-work/null/revwalk.py", + "wizard-work/null/test_gate.py", + "wizard-work/null/test_gate_mutants.py", + "wizard-work/null/test_json_salvage.py", + "wizard-work/null/test_myers_diff.py", + "wizard-work/null/test_revwalk.py", + "wizard-work/null/verify_check_provenance.py", + "wizard-work/null/verify_staged.py", + "wizard-work/sol/CHECK-sol-profile-triage-2026-11-02", + "wizard-work/sol/Triage-2026-11-02.json", + "wizard-work/sol/Triage-2026-11-02.md", + "wizard-work/sol/_print_paths.py", + "wizard-work/sol/build_untracked_triad_2026_11_01.py", + "wizard-work/sol/verify_sol_triage_2026_11_02.py", + "wizard-work/sol/verify_untracked_triad_2026_11_01.py", + "wizard-work/sphinx/archive-mmlongcite-2026-10-25/probe_paper_facts.json" + ], + "files_count": 92, + "generated_at": "2026-11-01T07:23:13Z", + "items": [ + { + "category": "gate", + "decision": "approve", + "id": 1, + "path": "CHECK-sol-untracked-classify" + }, + { + "category": "gate", + "decision": "approve", + "id": 2, + "path": "CHECK-triage-2026-10-29" + }, + { + "category": "documentation", + "decision": "info", + "id": 3, + "path": "checkins/2026-10-27-sentinel.md" + }, + { + "category": "data", + "decision": "observe", + "id": 4, + "path": "d.jsonl" + }, + { + "category": "documentation", + "decision": "info", + "id": 5, + "path": "docs/clawrouter-plan.md" + }, + { + "category": "documentation", + "decision": "info", + "id": 6, + "path": "docs/flux2-deploy.md" + }, + { + "category": "documentation", + "decision": "info", + "id": 7, + "path": "docs/video-deploy.md" + }, + { + "category": "tooling", + "decision": "review", + "id": 8, + "path": "inbox/dump_seed.py" + }, + { + "category": "persistence", + "decision": "archive", + "id": 9, + "path": "kanban.db" + }, + { + "category": "documentation", + "decision": "info", + "id": 10, + "path": "recoveries/apply-queue-status-2026-10-25.md" + }, + { + "category": "data", + "decision": "observe", + "id": 11, + "path": "repo_exts.tsv" + }, + { + "category": "data", + "decision": "observe", + "id": 12, + "path": "repo_files.tsv" + }, + { + "category": "data", + "decision": "observe", + "id": 13, + "path": "repo_tree.tsv" + }, + { + "category": "data", + "decision": "observe", + "id": 14, + "path": "s.json" + }, + { + "category": "data", + "decision": "observe", + "id": 15, + "path": "test_sample.txt" + }, + { + "category": "data", + "decision": "observe", + "id": 16, + "path": "test_validation.txt" + }, + { + "category": "data", + "decision": "observe", + "id": 17, + "path": "test_verification.txt" + }, + { + "category": "tooling", + "decision": "review", + "id": 18, + "path": "validate_artifact.py" + }, + { + "category": "other", + "decision": "review", + "id": 19, + "path": "wizard-work/null/.hermes-tmp.RqZp3z" + }, + { + "category": "documentation", + "decision": "info", + "id": 20, + "path": "wizard-work/null/README-gatehygiene.md" + }, + { + "category": "documentation", + "decision": "info", + "id": 21, + "path": "wizard-work/null/README-q62-scan.md" + }, + { + "category": "tooling", + "decision": "review", + "id": 22, + "path": "wizard-work/null/_probe10.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 23, + "path": "wizard-work/null/_probe11.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 24, + "path": "wizard-work/null/_probe12.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 25, + "path": "wizard-work/null/_probe13.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 26, + "path": "wizard-work/null/_probe14.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 27, + "path": "wizard-work/null/_probe5.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 28, + "path": "wizard-work/null/_probe6.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 29, + "path": "wizard-work/null/_probe7.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 30, + "path": "wizard-work/null/_probe8.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 31, + "path": "wizard-work/null/_probe9.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 32, + "path": "wizard-work/null/_probe_binding_defects.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 33, + "path": "wizard-work/null/_probe_gh1.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 34, + "path": "wizard-work/null/_probe_gh2.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 35, + "path": "wizard-work/null/_probe_gh3.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 36, + "path": "wizard-work/null/_probe_gh4.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 37, + "path": "wizard-work/null/_probe_gh5.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 38, + "path": "wizard-work/null/_probe_gh6.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 39, + "path": "wizard-work/null/_probe_gh7.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 40, + "path": "wizard-work/null/_probe_gh8.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 41, + "path": "wizard-work/null/_probe_gh9.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 42, + "path": "wizard-work/null/_probe_mut2.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 43, + "path": "wizard-work/null/_probe_mut_check.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 44, + "path": "wizard-work/null/_probe_q70_5h.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 45, + "path": "wizard-work/null/_probe_repro1.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 46, + "path": "wizard-work/null/_probe_repro2.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 47, + "path": "wizard-work/null/_probe_repro3.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 48, + "path": "wizard-work/null/_probe_repro4.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 49, + "path": "wizard-work/null/_probe_repro5.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 50, + "path": "wizard-work/null/_probe_repro6.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 51, + "path": "wizard-work/null/_probe_repro7.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 52, + "path": "wizard-work/null/check_gatehygiene.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 53, + "path": "wizard-work/null/check_provenance.py" + }, + { + "category": "data", + "decision": "observe", + "id": 54, + "path": "wizard-work/null/check_provenance_baseline.tsv" + }, + { + "category": "tooling", + "decision": "review", + "id": 55, + "path": "wizard-work/null/check_provenance_ledger.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 56, + "path": "wizard-work/null/check_q62_scan.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 57, + "path": "wizard-work/null/cli_contract.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 58, + "path": "wizard-work/null/diffkit/__init__.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 59, + "path": "wizard-work/null/diffkit/_flatpkg.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 60, + "path": "wizard-work/null/diffkit/core.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 61, + "path": "wizard-work/null/diffkit/diffkit_cli.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 62, + "path": "wizard-work/null/diffkit/myers.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 63, + "path": "wizard-work/null/diffkit_gate.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 64, + "path": "wizard-work/null/gatehygiene.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 65, + "path": "wizard-work/null/json_salvage.py" + }, + { + "category": "other", + "decision": "review", + "id": 66, + "path": "wizard-work/null/membershipkit/.hermes-tmp.sx2gIa" + }, + { + "category": "tooling", + "decision": "review", + "id": 67, + "path": "wizard-work/null/membershipkit/check_membershipkit.sh" + }, + { + "category": "tooling", + "decision": "review", + "id": 68, + "path": "wizard-work/null/membershipkit/mutate_membershipkit.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 69, + "path": "wizard-work/null/membershipkit/test_membershipkit.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 70, + "path": "wizard-work/null/mutate_resolver.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 71, + "path": "wizard-work/null/mutate_salvage.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 72, + "path": "wizard-work/null/q58_warncontrol.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 73, + "path": "wizard-work/null/q62_scan.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 74, + "path": "wizard-work/null/q70_sabotage.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 75, + "path": "wizard-work/null/q76_binding.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 76, + "path": "wizard-work/null/q80_warn.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 77, + "path": "wizard-work/null/revwalk.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 78, + "path": "wizard-work/null/test_gate.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 79, + "path": "wizard-work/null/test_gate_mutants.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 80, + "path": "wizard-work/null/test_json_salvage.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 81, + "path": "wizard-work/null/test_myers_diff.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 82, + "path": "wizard-work/null/test_revwalk.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 83, + "path": "wizard-work/null/verify_check_provenance.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 84, + "path": "wizard-work/null/verify_staged.py" + }, + { + "category": "other", + "decision": "review", + "id": 85, + "path": "wizard-work/sol/CHECK-sol-profile-triage-2026-11-02" + }, + { + "category": "data", + "decision": "observe", + "id": 86, + "path": "wizard-work/sol/Triage-2026-11-02.json" + }, + { + "category": "documentation", + "decision": "info", + "id": 87, + "path": "wizard-work/sol/Triage-2026-11-02.md" + }, + { + "category": "tooling", + "decision": "review", + "id": 88, + "path": "wizard-work/sol/_print_paths.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 89, + "path": "wizard-work/sol/build_untracked_triad_2026_11_01.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 90, + "path": "wizard-work/sol/verify_sol_triage_2026_11_02.py" + }, + { + "category": "tooling", + "decision": "review", + "id": 91, + "path": "wizard-work/sol/verify_untracked_triad_2026_11_01.py" + }, + { + "category": "data", + "decision": "observe", + "id": 92, + "path": "wizard-work/sphinx/archive-mmlongcite-2026-10-25/probe_paper_facts.json" + } + ], + "lane": "Triage - everyday Q&A and classification", + "repo": "/root/troupe-standup", + "rule": "path-based deterministic classifier (see MODULE_DOCLINE)", + "rule_version": "1", + "summary": { + "data": 11, + "documentation": 8, + "gate": 2, + "other": 3, + "persistence": 1, + "tooling": 67, + "total": 92 + }, + "task": "classify untracked repo files into deterministic buckets" +} diff --git a/wizard-work/sol/build_untracked_triad_2026_11_01.py b/wizard-work/sol/build_untracked_triad_2026_11_01.py new file mode 100644 index 0000000..9b5d0a0 --- /dev/null +++ b/wizard-work/sol/build_untracked_triad_2026_11_01.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Build Untracked-Triad-2026-11-01.json. + +Triage: classify the untracked (git ?? ) regular files of the repo into +deterministic buckets, then write the machine-readable artifact. + +Rule (deterministic, derived from the relative path only): + + category: + gate path starts with "CHECK-" + persistence ends with ".db" + data ends with ".json", ".jsonl", ".tsv" or ".txt" + tooling ends with ".py" or ".sh" + documentation ends with ".md" + other anything else + + decision: + gate -> approve + tooling -> review + documentation -> info + data -> observe + persistence -> archive + other -> review + +The manifest snapshots the exact file list at generation time (files + a +sha256 of the canonical list) so the independent verifier can confirm the +artifact is a faithful, reproducible classification, not a hand-edited guess. +""" + +import hashlib +import json +import os +import subprocess +import sys +from datetime import datetime, timezone + +def find_repo_root(start): + """Walk up from *start* until a directory that contains a .git marker.""" + d = os.path.abspath(start) + for _ in range(64): + if os.path.isdir(os.path.join(d, ".git")): + return d + parent = os.path.dirname(d) + if parent == d: + break + d = parent + raise RuntimeError("could not locate repo root (no .git) above " + start) + + +REPO_ROOT = find_repo_root(os.path.dirname(os.path.abspath(__file__))) +OUT_REPO = os.path.join( + REPO_ROOT, "wizard-work", "sol", "Untracked-Triad-2026-11-01.json" +) +OUT_WIZ = os.path.expanduser( + os.path.join("~", ".hermes", "wizard-outputs", "sol", "Untracked-Triad-2026-11-01.json") +) + + +def git_untracked(repo): + """Return sorted list of untracked regular files (skip untracked dirs).""" + proc = subprocess.run( + ["git", "-C", repo, "status", "--porcelain=v1"], + check=True, capture_output=True, text=True, + ) + files = [] + for line in proc.stdout.splitlines(): + if line.startswith("??"): + path = line[3:].strip() + if path and not path.endswith("/"): + files.append(path) + return sorted(files) + + +def category_of(rel): + low = rel.lower() + if low.startswith("check-"): + return "gate" + if low.endswith(".db"): + return "persistence" + if low.endswith((".json", ".jsonl", ".tsv", ".txt")): + return "data" + if low.endswith((".py", ".sh")): + return "tooling" + if low.endswith(".md"): + return "documentation" + return "other" + + +DECISION = { + "gate": "approve", + "tooling": "review", + "documentation": "info", + "data": "observe", + "persistence": "archive", + "other": "review", +} + + +def build(files): + items = [] + for i, rel in enumerate(files, start=1): + cat = category_of(rel) + items.append({ + "id": i, + "path": rel, + "category": cat, + "decision": DECISION[cat], + }) + return items + + +def corpus_sha256(files): + canon = json.dumps({"files": files}, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(canon).hexdigest() + + +def main(): + files = git_untracked(REPO_ROOT) + items = build(files) + manifest = { + "artifact": "Untracked-Triad-2026-11-01.json", + "lane": "Triage - everyday Q&A and classification", + "task": "classify untracked repo files into deterministic buckets", + "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "repo": REPO_ROOT, + "rule": "path-based deterministic classifier (see MODULE_DOCLINE)", + "rule_version": "1", + "files": files, + "files_count": len(files), + "corpus_sha256": corpus_sha256(files), + "summary": { + "total": len(files), + "gate": sum(1 for it in items if it["category"] == "gate"), + "persistence": sum(1 for it in items if it["category"] == "persistence"), + "data": sum(1 for it in items if it["category"] == "data"), + "tooling": sum(1 for it in items if it["category"] == "tooling"), + "documentation": sum(1 for it in items if it["category"] == "documentation"), + "other": sum(1 for it in items if it["category"] == "other"), + }, + "items": items, + } + + for out in (OUT_REPO, OUT_WIZ): + os.makedirs(os.path.dirname(out), exist_ok=True) + with open(out, "w", encoding="utf-8") as fh: + json.dump(manifest, fh, indent=2, sort_keys=True) + fh.write("\n") + + print("Untracked-Triad-2026-11-01.json written") + print(" files:", len(files)) + print(" corpus_sha256:", manifest["corpus_sha256"]) + print(" categories:", manifest["summary"]) + + +if __name__ == "__main__": + main() diff --git a/wizard-work/sol/verify_untracked_triad_2026_11_01.py b/wizard-work/sol/verify_untracked_triad_2026_11_01.py new file mode 100644 index 0000000..56cc4dd --- /dev/null +++ b/wizard-work/sol/verify_untracked_triad_2026_11_01.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Independent verifier for Untracked-Triad-2026-11-01.json. + +The classification rule is re-derived from each item's path (the generator's +stored category/decision fields are NOT trusted). A wrong classification makes +this exit non-zero. Additionally every classified path must still exist on disk +as a real, untracked, non-ignored file. + +Exit 0 iff all checks pass. +""" + +import hashlib +import json +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +MANIFEST = os.path.join(HERE, "Untracked-Triad-2026-11-01.json") + +REPO_ROOT = os.path.dirname(os.path.dirname(HERE)) + + +def cat_of(rel): + low = rel.lower() + if low.startswith("check-"): + return "gate" + if low.endswith(".db"): + return "persistence" + if low.endswith((".json", ".jsonl", ".tsv", ".txt")): + return "data" + if low.endswith((".py", ".sh")): + return "tooling" + if low.endswith(".md"): + return "documentation" + return "other" + + +DECISION = { + "gate": "approve", + "tooling": "review", + "documentation": "info", + "data": "observe", + "persistence": "archive", + "other": "review", +} + + +def git_status_untracked(repo): + proc = subprocess_run(["git", "-C", repo, "status", "--porcelain=v1"]) + return set(line[3:].strip() for line in proc.stdout.splitlines() + if line.startswith("??") and not line[3:].strip().endswith("/")) + + +def git_check_ignored(repo, rel): + proc = subprocess_run(["git", "check-ignore", "-q", rel]) + return proc.returncode == 0 + + +def subprocess_run(cmd): + import subprocess + return subprocess.run(cmd, capture_output=True, text=True) + + +def main(): + errors = [] + try: + with open(MANIFEST, encoding="utf-8") as fh: + man = json.load(fh) + except Exception as exc: + print("FAIL: cannot load manifest:", exc) + sys.exit(1) + + files = man.get("files", []) + items = man.get("items", []) + repo = man.get("repo", REPO_ROOT) + + # 1. corpus sha256 must match the recorded file list + canon = json.dumps({"files": sorted(files)}, sort_keys=True, separators=(",", ":")).encode("utf-8") + computed_sha = hashlib.sha256(canon).hexdigest() + if computed_sha != man.get("corpus_sha256"): + errors.append("corpus_sha256 mismatch (recorded %s vs computed %s)" % + (man.get("corpus_sha256"), computed_sha)) + + # 2. ids must be unique and exactly 1..N + ids = [it.get("id") for it in items] + if sorted(ids) != list(range(1, len(items) + 1)): + errors.append("ids are not exactly 1..N") + + # 3. re-derive category/decision from each path; verify file exists, untracked, non-ignored + for it in items: + rel = it.get("path") + exp_cat = it.get("category") + exp_dec = it.get("decision") + if not rel: + errors.append("missing path on item id=%r" % (it.get("id"),)) + continue + act_cat = cat_of(rel) + act_dec = DECISION.get(act_cat) + if exp_cat != act_cat: + errors.append("id=%s path=%s category %s != re-derived %s" % (it.get("id"), rel, exp_cat, act_cat)) + if exp_dec != act_dec: + errors.append("id=%s path=%s decision %s != re-derived %s" % (it.get("id"), rel, exp_dec, act_dec)) + if not os.path.isfile(os.path.join(repo, rel)): + errors.append("id=%s path=%s does not exist as a real file" % (it.get("id"), rel)) + if rel not in git_status_untracked(repo): + errors.append("id=%s path=%s is not currently untracked" % (it.get("id"), rel)) + if git_check_ignored(repo, rel): + errors.append("id=%s path=%s is git-ignored" % (it.get("id"), rel)) + + # 4. summary counts must be consistent with items (skip the "total" key, + # which is the corpus size, not a category). + from collections import Counter + CAT_KEYS = {"gate", "persistence", "data", "tooling", "documentation", "other"} + cnt = Counter(it.get("category") for it in items) + for cat, n in man.get("summary", {}).items(): + if cat not in CAT_KEYS: + continue + if cnt.get(cat, 0) != n: + errors.append("summary %s=%d but items have %d" % (cat, n, cnt.get(cat, 0))) + + if errors: + print("FAIL: %d problem(s)" % len(errors)) + for e in errors[:40]: + print(" -", e) + sys.exit(1) + + print("PASS: %d files classified; %d re-derive to matching category/decision; " + "all files real, untracked, non-ignored; corpus sha256 matches." + % (len(files), len(items))) + sys.exit(0) + + +if __name__ == "__main__": + main()