From ff4e2fb72e48633154603e611d75764602180384 Mon Sep 17 00:00:00 2001 From: sol Date: Mon, 2 Nov 2026 04:52:57 +0000 Subject: [PATCH] sol: sol-lane untracked-file triage classification 2026-11-02 --- .../sol/CHECK-2026-11-02-untracked-triad | 17 + .../sol/Untracked-Triad-2026-11-02.json | 625 ++++++++++++++++++ wizard-work/sol/Untracked-Triad-2026-11-02.md | 22 + .../sol/build_untracked_triad_2026_11_02.py | 190 ++++++ .../sol/verify_untracked_triad_2026_11_02.py | 148 +++++ 5 files changed, 1002 insertions(+) create mode 100644 wizard-work/sol/CHECK-2026-11-02-untracked-triad create mode 100644 wizard-work/sol/Untracked-Triad-2026-11-02.json create mode 100644 wizard-work/sol/Untracked-Triad-2026-11-02.md create mode 100644 wizard-work/sol/build_untracked_triad_2026_11_02.py create mode 100644 wizard-work/sol/verify_untracked_triad_2026_11_02.py diff --git a/wizard-work/sol/CHECK-2026-11-02-untracked-triad b/wizard-work/sol/CHECK-2026-11-02-untracked-triad new file mode 100644 index 0000000..8f199ed --- /dev/null +++ b/wizard-work/sol/CHECK-2026-11-02-untracked-triad @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# CHECK -- sol: untracked-file triage classification (2026-11-02). +# Exit 0 iff the committed manifest verifies: the manifest re-derives every +# frozen path's category/decision by the canonical rule (no lookup), every +# path is still a real, untracked, non-ignored file, the corpus sha256 matches, +# ids are 1..N, the summary counts are consistent, and the wizard-outputs shard +# is byte-identical to the repo manifest. +set -u + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Resolve the repo root (this script may live inside wizard-work/sol/ or at the +# repo root, so walk up to the directory that holds .git). +ROOT="$(git -C "$HERE" rev-parse --show-toplevel 2>/dev/null)" \ + || { echo "CANNOT GRADE: no git toplevel above $HERE" >&2; exit 2; } +cd "$ROOT" || { echo "CANNOT GRADE: cannot cd to $ROOT" >&2; exit 2; } + +exec python3 wizard-work/sol/verify_untracked_triad_2026_11_02.py diff --git a/wizard-work/sol/Untracked-Triad-2026-11-02.json b/wizard-work/sol/Untracked-Triad-2026-11-02.json new file mode 100644 index 0000000..38ecd41 --- /dev/null +++ b/wizard-work/sol/Untracked-Triad-2026-11-02.json @@ -0,0 +1,625 @@ +{ + "artifact": "Untracked-Triad-2026-11-02.json", + "built_at": "2026-11-02T04:50:51.524666+00:00", + "corpus_sha256": "144a9dd89619300d0016c490033605ff00c19d0b2a99964537127754f22503ab", + "files": [ + "CHECK-2026-11-02-openmeteo-summary", + "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/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/quill/draft-compliance-report-2026-10-31.pin", + "wizard-work/sphinx/archive-mmlongcite-2026-10-25/probe_paper_facts.json" + ], + "items": [ + { + "category": "gate", + "decision": "approve", + "id": 1, + "path": "CHECK-2026-11-02-openmeteo-summary" + }, + { + "category": "gate", + "decision": "approve", + "id": 2, + "path": "CHECK-sol-untracked-classify" + }, + { + "category": "gate", + "decision": "approve", + "id": 3, + "path": "CHECK-triage-2026-10-29" + }, + { + "category": "documentation", + "decision": "info", + "id": 4, + "path": "checkins/2026-10-27-sentinel.md" + }, + { + "category": "data", + "decision": "observe", + "id": 5, + "path": "d.jsonl" + }, + { + "category": "documentation", + "decision": "info", + "id": 6, + "path": "docs/clawrouter-plan.md" + }, + { + "category": "documentation", + "decision": "info", + "id": 7, + "path": "docs/flux2-deploy.md" + }, + { + "category": "documentation", + "decision": "info", + "id": 8, + "path": "docs/video-deploy.md" + }, + { + "category": "tooling", + "decision": "review", + "id": 9, + "path": "inbox/dump_seed.py" + }, + { + "category": "persistence", + "decision": "archive", + "id": 10, + "path": "kanban.db" + }, + { + "category": "documentation", + "decision": "info", + "id": 11, + "path": "recoveries/apply-queue-status-2026-10-25.md" + }, + { + "category": "data", + "decision": "observe", + "id": 12, + "path": "repo_exts.tsv" + }, + { + "category": "data", + "decision": "observe", + "id": 13, + "path": "repo_files.tsv" + }, + { + "category": "data", + "decision": "observe", + "id": 14, + "path": "repo_tree.tsv" + }, + { + "category": "data", + "decision": "observe", + "id": 15, + "path": "s.json" + }, + { + "category": "data", + "decision": "observe", + "id": 16, + "path": "test_sample.txt" + }, + { + "category": "data", + "decision": "observe", + "id": 17, + "path": "test_validation.txt" + }, + { + "category": "data", + "decision": "observe", + "id": 18, + "path": "test_verification.txt" + }, + { + "category": "tooling", + "decision": "review", + "id": 19, + "path": "validate_artifact.py" + }, + { + "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/quill/draft-compliance-report-2026-10-31.pin" + }, + { + "category": "data", + "decision": "observe", + "id": 86, + "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": 10, + "documentation": 7, + "gate": 3, + "other": 2, + "persistence": 1, + "tooling": 63, + "total": 86 + }, + "task": "classify untracked repo files into deterministic buckets" +} diff --git a/wizard-work/sol/Untracked-Triad-2026-11-02.md b/wizard-work/sol/Untracked-Triad-2026-11-02.md new file mode 100644 index 0000000..fe84f5a --- /dev/null +++ b/wizard-work/sol/Untracked-Triad-2026-11-02.md @@ -0,0 +1,22 @@ +# Untracked-file triage classification — 2026-11-02 + +Triaged **86** untracked, non-ignored files under `/root/troupe-standup`. + +## Bucket counts + +| category | decision | count | +|---|---|---| +| gate | approve | 3 | +| persistence | archive | 1 | +| data | observe | 10 | +| tooling | review | 63 | +| documentation | info | 7 | +| other | review | 2 | + +## Notes + +- Rule: path-based deterministic classifier (see MODULE_DOCLINE). +- `corpus_sha256` is a sha256 over the canonical file list; + the independent verifier re-derives this, not the generator. +- Every classified path is re-checked as a real, untracked, + non-ignored file at run time; no path is trusted from memory. diff --git a/wizard-work/sol/build_untracked_triad_2026_11_02.py b/wizard-work/sol/build_untracked_triad_2026_11_02.py new file mode 100644 index 0000000..46b22f5 --- /dev/null +++ b/wizard-work/sol/build_untracked_triad_2026_11_02.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Build Untracked-Triad-2026-11-02.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. + +Outputs +------- +wizard-work/sol/Untracked-Triad-2026-11-02.json machine-readable manifest +wizard-work/sol/Untracked-Triad-2026-11-02.md human-readable summary +/root/.hermes/wizard-outputs/sol/Untracked-Triad-2026-11-02.json + identical shard read by the triage harness + +Run: python3 build_untracked_triad_2026_11_02.py +""" +import hashlib +import json +import os +import shutil +import subprocess +from datetime import datetime, timezone + +SOL_DIR = os.path.dirname(os.path.abspath(__file__)) +REPO_ROOT = os.path.dirname(os.path.dirname(SOL_DIR)) +OUT_REPO = os.path.join(SOL_DIR, "Untracked-Triad-2026-11-02.json") +OUT_MD = os.path.join(SOL_DIR, "Untracked-Triad-2026-11-02.md") +OUT_WIZ = os.path.expanduser( + os.path.join("~", ".hermes", "wizard-outputs", "sol", "Untracked-Triad-2026-11-02.json") +) + +# --------------------------------------------------------------------------- # +# universe & classification +# --------------------------------------------------------------------------- # +def git_untracked(repo): + """Return sorted list of untracked regular files, excluding this pipeline's + own files (wizard-work/sol/ subtree) and the CHECK gate so the snapshot is + a stable, reproducible classification rather than self-referential.""" + 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.endswith("/"): + continue # untracked directories + if path.startswith("wizard-work/sol/"): + continue # this pipeline's own files (incl. manifest/verifier/build) + if os.path.basename(path) == "CHECK-2026-11-02-untracked-triad": + continue # the CHECK gate, committed alongside the manifest + 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() + + +# --------------------------------------------------------------------------- # +# output writers +# --------------------------------------------------------------------------- # +def write_json(obj, path): + with open(path, "w", encoding="utf-8") as fh: + json.dump(obj, fh, indent=2, sort_keys=True) + fh.write("\n") + + +def write_md(manifest, path): + s = manifest["summary"] + lines = [] + lines.append("# Untracked-file triage classification — 2026-11-02") + lines.append("") + lines.append("Triaged **%d** untracked, non-ignored files under `%s`." % ( + s.get("total", 0), manifest["repo"])) + lines.append("") + lines.append("## Bucket counts") + lines.append("") + lines.append("| category | decision | count |") + lines.append("|---|---|---|") + for cat in ["gate", "persistence", "data", "tooling", "documentation", "other"]: + lines.append("| %s | %s | %d |" % ( + cat, DECISION[cat], s.get(cat, 0))) + lines.append("") + lines.append("## Notes") + lines.append("") + lines.append("- Rule: path-based deterministic classifier (see MODULE_DOCLINE).") + lines.append("- `corpus_sha256` is a sha256 over the canonical file list;") + lines.append(" the independent verifier re-derives this, not the generator.") + lines.append("- Every classified path is re-checked as a real, untracked,") + lines.append(" non-ignored file at run time; no path is trusted from memory.") + lines.append("") + with open(path, "w", encoding="utf-8") as fh: + fh.write("\n".join(lines)) + + +# --------------------------------------------------------------------------- # +# main +# --------------------------------------------------------------------------- # +def main(): + files = git_untracked(REPO_ROOT) + items = build(files) + summary = {"total": len(files)} + for it in items: + summary[it["category"]] = summary.get(it["category"], 0) + 1 + manifest = { + "artifact": "Untracked-Triad-2026-11-02.json", + "corpus_sha256": corpus_sha256(files), + "files": files, + "items": items, + "lane": "Triage - everyday Q&A and classification", + "repo": REPO_ROOT, + "rule": "path-based deterministic classifier (see MODULE_DOCLINE)", + "rule_version": "1", + "summary": summary, + "task": "classify untracked repo files into deterministic buckets", + "built_at": datetime.now(timezone.utc).isoformat(), + } + write_json(manifest, OUT_REPO) + write_md(manifest, OUT_MD) + + # identical shard to the triage harness + shutil.copyfile(OUT_REPO, OUT_WIZ) + with open(OUT_REPO, "rb") as a, open(OUT_WIZ, "rb") as b: + if a.read() != b.read(): + sys.exit("shard parity mismatch: %s vs %s" % (OUT_REPO, OUT_WIZ)) + print("wrote: %s (%d files)" % (OUT_REPO, len(files))) + print("wrote: %s" % OUT_MD) + print("shard: %s" % OUT_WIZ) + print("buckets: %s" % json.dumps(summary, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/wizard-work/sol/verify_untracked_triad_2026_11_02.py b/wizard-work/sol/verify_untracked_triad_2026_11_02.py new file mode 100644 index 0000000..e088ae3 --- /dev/null +++ b/wizard-work/sol/verify_untracked_triad_2026_11_02.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Independent verifier for Untracked-Triad-2026-11-02.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, and the wizard-outputs shard must +match the repo manifest byte-for-byte. + +Exit 0 iff all checks pass. +""" +import hashlib +import json +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +MANIFEST = os.path.join(HERE, "Untracked-Triad-2026-11-02.json") +OUT_WIZ = os.path.expanduser( + os.path.join("~", ".hermes", "wizard-outputs", "sol", "Untracked-Triad-2026-11-02.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"], capture_output=True, text=True) + 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", "-C", repo, "check-ignore", "-q", rel], capture_output=True) + return proc.returncode == 0 + + +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) + recorded_sha = man.get("corpus_sha256") + + # 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 != recorded_sha: + errors.append("corpus_sha256 mismatch (recorded %s vs computed %s)" + % (recorded_sha, 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 and verify file state + 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 match 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))) + + # 5. shard parity: the wizard-outputs copy must be byte-identical. + if os.path.exists(OUT_WIZ): + with open(MANIFEST, "rb") as a, open(OUT_WIZ, "rb") as b: + if a.read() != b.read(): + errors.append("shard parity FAIL: %s != %s" % (MANIFEST, OUT_WIZ)) + else: + errors.append("shard missing: %s" % OUT_WIZ) + + 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; " + "shard byte-identical." % (len(files), len(items))) + sys.exit(0) + + +if __name__ == "__main__": + main()