From 49ce70f8130674f99767378b95bf90e6bb717d41 Mon Sep 17 00:00:00 2001 From: sol Date: Wed, 28 Oct 2026 18:39:36 +0000 Subject: [PATCH] sol: 2026-10-28 daily sol-profile class triage (JSON + MD + independent CHECK) --- ...build_sol_profile_file_class_2026-10-28.py | 172 +++++++ .../sol-profile-file-class-2026-10-28.json | 423 ++++++++++++++++++ .../sol/sol-profile-file-class-2026-10-28.md | 121 +++++ ...erify_sol_profile_file_class_2026-10-28.py | 110 +++++ 4 files changed, 826 insertions(+) create mode 100644 wizard-work/sol/build_sol_profile_file_class_2026-10-28.py create mode 100644 wizard-work/sol/sol-profile-file-class-2026-10-28.json create mode 100644 wizard-work/sol/sol-profile-file-class-2026-10-28.md create mode 100644 wizard-work/sol/verify_sol_profile_file_class_2026-10-28.py diff --git a/wizard-work/sol/build_sol_profile_file_class_2026-10-28.py b/wizard-work/sol/build_sol_profile_file_class_2026-10-28.py new file mode 100644 index 0000000..3874141 --- /dev/null +++ b/wizard-work/sol/build_sol_profile_file_class_2026-10-28.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Generator — daily sol-profile file & directory class triage. + +Produces, under wizard-work/sol/: + - sol-profile-file-class-2026-10-28.json + - sol-profile-file-class-2026-10-28.md + +Classification is deterministic over real disk state (os.listdir + os.stat) +of `/root/.hermes/profiles/sol`; nothing is invented. The companion checker +`verify_sol_profile_file_class_2026-10-28.py` re-derives the same classification +independently and diffs it against this artifact; exit 0 == match. +""" +import json +import os +import stat +from datetime import datetime, timezone + +PROFILE = "/root/.hermes/profiles/sol" +OUTDIR = "/root/troupe-standup/wizard-work/sol" +OUT_JSON = os.path.join(OUTDIR, "sol-profile-file-class-2026-10-28.json") +OUT_MD = os.path.join(OUTDIR, "sol-profile-file-class-2026-10-28.md") + +# Ordered, deterministic classification rules. Files and dirs are both +# classified; dir and file shape are decided by os.stat, not by name alone. +RULES = [ + ("SKILL_CONTAINER", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "skills"), + ("LSP", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "lsp"), + ("BIN", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "bin"), + ("FORGE", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "forge"), + ("BACKUP", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "backups"), + ("CACHE", lambda n, st: stat.S_ISDIR(st.st_mode) and n in + ("cache", "image_cache", "audio_cache", "keys")), + ("RUNTIME_DIR", lambda n, st: stat.S_ISDIR(st.st_mode) and n in + ("cron", "sessions", "sandboxes", "runtime", "workspace")), + ("PLUGIN_DIR", lambda n, st: stat.S_ISDIR(st.st_mode) and n in + ("plugins", "plugin-update-checks")), + ("VAULT", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "vault"), + ("LOG_DIR", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "logs"), + ("MEMORY", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "memories"), + ("SKIN", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "skins"), + ("PAIR_PLAT", lambda n, st: stat.S_ISDIR(st.st_mode) and n in + ("pairing", "platforms")), + ("HOOK", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "hooks"), + ("CHECK_DIR", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "source-checks"), + ("HOME_DIR", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "home"), + ("OUTPUT_DIR", lambda n, st: stat.S_ISDIR(st.st_mode) and n == "wizard-outputs"), + ("CONFIG_YAML", lambda n, st: not stat.S_ISDIR(st.st_mode) and + n.endswith((".yaml", ".yml"))), + ("CONFIG_JSON", lambda n, st: not stat.S_ISDIR(st.st_mode) and n.endswith(".json")), + ("SCRIPT_PY", lambda n, st: not stat.S_ISDIR(st.st_mode) and n.endswith(".py")), + ("SCRIPT_SH", lambda n, st: not stat.S_ISDIR(st.st_mode) and n.endswith(".sh")), + ("STATE_DB", lambda n, st: not stat.S_ISDIR(st.st_mode) and ".db" in n), + ("LOCK", lambda n, st: not stat.S_ISDIR(st.st_mode) and n.endswith(".lock")), + ("CACHE_ETAG", lambda n, st: not stat.S_ISDIR(st.st_mode) and n.endswith(".etag")), + ("DOC", lambda n, st: not stat.S_ISDIR(st.st_mode) and n.endswith(".md")), + ("LOG_TXT", lambda n, st: not stat.S_ISDIR(st.st_mode) and n.endswith(".txt")), + ("FILE_UNCLASSIFIED", lambda n, st: not stat.S_ISDIR(st.st_mode)), + ("DIR_UNCLASSIFIED", lambda n, st: stat.S_ISDIR(st.st_mode)), +] + + +def classify(name, st): + for cls, rule in RULES: + if rule(name, st): + return cls + return "UNCLASSIFIED" + + +def main(): + entries = [] + files = dirs = 0 + for name in sorted(os.listdir(PROFILE)): + p = os.path.join(PROFILE, name) + st = os.stat(p) + cls = classify(name, st) + entries.append({ + "name": name, + "kind": "dir" if stat.S_ISDIR(st.st_mode) else "file", + "class": cls, + "size": st.st_size, + }) + if stat.S_ISDIR(st.st_mode): + dirs += 1 + else: + files += 1 + + classes = {} + for e in entries: + classes[e["class"]] = classes.get(e["class"], 0) + 1 + + # Real daily verdict from live state, not a guess. + now = datetime.now(timezone.utc).timestamp() + log_age = None + for e in entries: + if e["name"] == "logs": + logdir = os.path.join(PROFILE, "logs") + for fn in os.listdir(logdir): + mtime = os.path.getmtime(os.path.join(logdir, fn)) + if log_age is None or mtime > log_age: + log_age = mtime + age_h = (now - log_age) / 3600.0 if log_age else None + verdict = "ACTIVE" if (age_h is not None and age_h < 24) else ("WATCH" if age_h is not None else "STALE") + + out = { + "artifact": "sol-profile-file-class-2026-10-28.json", + "profile": PROFILE, + "generated_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "verdict": verdict, + "files": files, + "dirs": dirs, + "total_entries": len(entries), + "classes": dict(sorted(classes.items())), + "entries": entries, + } + os.makedirs(OUTDIR, exist_ok=True) + with open(OUT_JSON, "w", encoding="utf-8") as f: + json.dump(out, f, indent=2) + + # ---- markdown report ---- + L = [] + L.append("# Sol profile — file & directory class triage") + L.append("") + L.append("Date: 2026-10-28 | Evidence: `os.stat` of every direct entry of `%s`; " + "classifier rules in `build_sol_profile_file_class_2026-10-28.py`." + % PROFILE) + L.append("") + L.append("## Verdict") + L.append("") + L.append("sol profile status: **%s** (live log mtime within 24h)" % verdict) + L.append("") + L.append("## Summary") + L.append("") + L.append("| Metric | Count |") + L.append("|---|---|") + L.append("| Total direct entries | %d |" % len(entries)) + L.append("| Files | %d |" % files) + L.append("| Direct dirs | %d |" % dirs) + L.append("") + L.append("## Class counts") + L.append("") + L.append("| class | count |") + L.append("|---|---|") + for c in sorted(classes): + L.append("| %s | %d |" % (c, classes[c])) + L.append("") + L.append("## Findings") + L.append("") + L.append("1. Class counts sum to the entry total (%d): files + dirs " + "= %d + %d = %d." % (len(entries), files, dirs, files + dirs)) + L.append("2. `state.db*` family is one STATE_DB logical family: " + ".db + 2 blobs + 3 lock files; do not over-count rows.") + L.append("3. Unclassified families are only `%s` (files) and `%s` (dirs)." + % ("FILE_UNCLASSIFIED", "DIR_UNCLASSIFIED")) + L.append("") + L.append("## File+dir rows") + L.append("") + L.append("| name | kind | class | size_bytes |") + L.append("|---|---|---|---|") + for e in entries: + L.append("| %s | %s | %s | %d |" % ( + e["name"], e["kind"], e["class"].replace("_", " ").replace("::", " → "), e["size"])) + L.append("") + with open(OUT_MD, "w", encoding="utf-8") as f: + f.write("\n".join(L)) + + print("WROTE", OUT_JSON) + print("WROTE", OUT_MD) + print("files=%d dirs=%d total=%d verdict=%s" % (files, dirs, len(entries), verdict)) + + +if __name__ == "__main__": + main() diff --git a/wizard-work/sol/sol-profile-file-class-2026-10-28.json b/wizard-work/sol/sol-profile-file-class-2026-10-28.json new file mode 100644 index 0000000..7e0b1d6 --- /dev/null +++ b/wizard-work/sol/sol-profile-file-class-2026-10-28.json @@ -0,0 +1,423 @@ +{ + "artifact": "sol-profile-file-class-2026-10-28.json", + "profile": "/root/.hermes/profiles/sol", + "generated_utc": "2026-10-28T18:39:11Z", + "verdict": "ACTIVE", + "files": 37, + "dirs": 27, + "total_entries": 64, + "classes": { + "BACKUP": 1, + "BIN": 1, + "CACHE": 4, + "CACHE_ETAG": 1, + "CHECK_DIR": 1, + "CONFIG_JSON": 3, + "CONFIG_YAML": 2, + "DIR_UNCLASSIFIED": 1, + "DOC": 1, + "FILE_UNCLASSIFIED": 3, + "FORGE": 1, + "HOME_DIR": 1, + "HOOK": 1, + "LOCK": 2, + "LOG_DIR": 1, + "LSP": 1, + "MEMORY": 1, + "OUTPUT_DIR": 1, + "PAIR_PLAT": 2, + "PLUGIN_DIR": 2, + "RUNTIME_DIR": 5, + "SCRIPT_PY": 19, + "SKILL_CONTAINER": 1, + "SKIN": 1, + "STATE_DB": 6, + "VAULT": 1 + }, + "entries": [ + { + "name": ".env", + "kind": "file", + "class": "FILE_UNCLASSIFIED", + "size": 165 + }, + { + "name": ".env.bak-20261014T165715Z", + "kind": "file", + "class": "FILE_UNCLASSIFIED", + "size": 165 + }, + { + "name": ".env.lock", + "kind": "file", + "class": "LOCK", + "size": 0 + }, + { + "name": ".skills_prompt_snapshot.json", + "kind": "file", + "class": "CONFIG_JSON", + "size": 42870 + }, + { + "name": "SOUL.md", + "kind": "file", + "class": "DOC", + "size": 685 + }, + { + "name": "_check_repo_json.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 515 + }, + { + "name": "_gen_daily_ledger_json.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 2414 + }, + { + "name": "_gen_daily_ledger_report.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 3340 + }, + { + "name": "_gen_daily_triage.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 2012 + }, + { + "name": "_gen_daily_triage_fresh.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 1475 + }, + { + "name": "_verify_daily_triage.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 2738 + }, + { + "name": "audio_cache", + "kind": "dir", + "class": "CACHE", + "size": 4096 + }, + { + "name": "auth.json", + "kind": "file", + "class": "CONFIG_JSON", + "size": 143 + }, + { + "name": "auth.lock", + "kind": "file", + "class": "LOCK", + "size": 0 + }, + { + "name": "backups", + "kind": "dir", + "class": "BACKUP", + "size": 4096 + }, + { + "name": "bin", + "kind": "dir", + "class": "BIN", + "size": 4096 + }, + { + "name": "cache", + "kind": "dir", + "class": "CACHE", + "size": 4096 + }, + { + "name": "config.yaml", + "kind": "file", + "class": "CONFIG_YAML", + "size": 510 + }, + { + "name": "config.yaml.bak-20261014T165715Z", + "kind": "file", + "class": "FILE_UNCLASSIFIED", + "size": 510 + }, + { + "name": "context_length_cache.yaml", + "kind": "file", + "class": "CONFIG_YAML", + "size": 94 + }, + { + "name": "cron", + "kind": "dir", + "class": "RUNTIME_DIR", + "size": 4096 + }, + { + "name": "cron-fleet-triage-2026-10-17.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 5407 + }, + { + "name": "cron-fleet-triage-2026-10-18.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 5355 + }, + { + "name": "cron-fleet-verify.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 1790 + }, + { + "name": "diag.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 1182 + }, + { + "name": "diag2.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 550 + }, + { + "name": "diag3.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 1401 + }, + { + "name": "diag4.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 1000 + }, + { + "name": "forge", + "kind": "dir", + "class": "FORGE", + "size": 4096 + }, + { + "name": "gen_sol_profile_triage.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 4372 + }, + { + "name": "home", + "kind": "dir", + "class": "HOME_DIR", + "size": 4096 + }, + { + "name": "hooks", + "kind": "dir", + "class": "HOOK", + "size": 4096 + }, + { + "name": "image_cache", + "kind": "dir", + "class": "CACHE", + "size": 4096 + }, + { + "name": "inventory_skills.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 6472 + }, + { + "name": "keys", + "kind": "dir", + "class": "CACHE", + "size": 4096 + }, + { + "name": "logs", + "kind": "dir", + "class": "LOG_DIR", + "size": 4096 + }, + { + "name": "lsp", + "kind": "dir", + "class": "LSP", + "size": 4096 + }, + { + "name": "memories", + "kind": "dir", + "class": "MEMORY", + "size": 4096 + }, + { + "name": "models_dev_cache.etag", + "kind": "file", + "class": "CACHE_ETAG", + "size": 36 + }, + { + "name": "models_dev_cache.json", + "kind": "file", + "class": "CONFIG_JSON", + "size": 5922427 + }, + { + "name": "pairing", + "kind": "dir", + "class": "PAIR_PLAT", + "size": 4096 + }, + { + "name": "plans", + "kind": "dir", + "class": "DIR_UNCLASSIFIED", + "size": 4096 + }, + { + "name": "platforms", + "kind": "dir", + "class": "PAIR_PLAT", + "size": 4096 + }, + { + "name": "plugin-update-checks", + "kind": "dir", + "class": "PLUGIN_DIR", + "size": 4096 + }, + { + "name": "plugins", + "kind": "dir", + "class": "PLUGIN_DIR", + "size": 4096 + }, + { + "name": "runtime", + "kind": "dir", + "class": "RUNTIME_DIR", + "size": 4096 + }, + { + "name": "sandboxes", + "kind": "dir", + "class": "RUNTIME_DIR", + "size": 4096 + }, + { + "name": "sessions", + "kind": "dir", + "class": "RUNTIME_DIR", + "size": 4096 + }, + { + "name": "skills", + "kind": "dir", + "class": "SKILL_CONTAINER", + "size": 4096 + }, + { + "name": "skins", + "kind": "dir", + "class": "SKIN", + "size": 4096 + }, + { + "name": "source-checks", + "kind": "dir", + "class": "CHECK_DIR", + "size": 4096 + }, + { + "name": "state.db", + "kind": "file", + "class": "STATE_DB", + "size": 79552512 + }, + { + "name": "state.db-shm", + "kind": "file", + "class": "STATE_DB", + "size": 32768 + }, + { + "name": "state.db-wal", + "kind": "file", + "class": "STATE_DB", + "size": 3893432 + }, + { + "name": "state.db.auto-maintenance.lock", + "kind": "file", + "class": "STATE_DB", + "size": 0 + }, + { + "name": "state.db.fts_rebuild.lock", + "kind": "file", + "class": "STATE_DB", + "size": 0 + }, + { + "name": "state.db.quarantine.lock", + "kind": "file", + "class": "STATE_DB", + "size": 0 + }, + { + "name": "triage_check.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 3732 + }, + { + "name": "triage_profiles.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 4093 + }, + { + "name": "triage_skill_report.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 6354 + }, + { + "name": "vault", + "kind": "dir", + "class": "VAULT", + "size": 4096 + }, + { + "name": "verify_skills.py", + "kind": "file", + "class": "SCRIPT_PY", + "size": 1442 + }, + { + "name": "wizard-outputs", + "kind": "dir", + "class": "OUTPUT_DIR", + "size": 4096 + }, + { + "name": "workspace", + "kind": "dir", + "class": "RUNTIME_DIR", + "size": 4096 + } + ] +} \ No newline at end of file diff --git a/wizard-work/sol/sol-profile-file-class-2026-10-28.md b/wizard-work/sol/sol-profile-file-class-2026-10-28.md new file mode 100644 index 0000000..e40fe24 --- /dev/null +++ b/wizard-work/sol/sol-profile-file-class-2026-10-28.md @@ -0,0 +1,121 @@ +# Sol profile — file & directory class triage + +Date: 2026-10-28 | Evidence: `os.stat` of every direct entry of `/root/.hermes/profiles/sol`; classifier rules in `build_sol_profile_file_class_2026-10-28.py`. + +## Verdict + +sol profile status: **ACTIVE** (live log mtime within 24h) + +## Summary + +| Metric | Count | +|---|---| +| Total direct entries | 64 | +| Files | 37 | +| Direct dirs | 27 | + +## Class counts + +| class | count | +|---|---| +| BACKUP | 1 | +| BIN | 1 | +| CACHE | 4 | +| CACHE_ETAG | 1 | +| CHECK_DIR | 1 | +| CONFIG_JSON | 3 | +| CONFIG_YAML | 2 | +| DIR_UNCLASSIFIED | 1 | +| DOC | 1 | +| FILE_UNCLASSIFIED | 3 | +| FORGE | 1 | +| HOME_DIR | 1 | +| HOOK | 1 | +| LOCK | 2 | +| LOG_DIR | 1 | +| LSP | 1 | +| MEMORY | 1 | +| OUTPUT_DIR | 1 | +| PAIR_PLAT | 2 | +| PLUGIN_DIR | 2 | +| RUNTIME_DIR | 5 | +| SCRIPT_PY | 19 | +| SKILL_CONTAINER | 1 | +| SKIN | 1 | +| STATE_DB | 6 | +| VAULT | 1 | + +## Findings + +1. Class counts sum to the entry total (64): files + dirs = 37 + 27 = 64. +2. `state.db*` family is one STATE_DB logical family: .db + 2 blobs + 3 lock files; do not over-count rows. +3. Unclassified families are only `FILE_UNCLASSIFIED` (files) and `DIR_UNCLASSIFIED` (dirs). + +## File+dir rows + +| name | kind | class | size_bytes | +|---|---|---|---| +| .env | file | FILE UNCLASSIFIED | 165 | +| .env.bak-20261014T165715Z | file | FILE UNCLASSIFIED | 165 | +| .env.lock | file | LOCK | 0 | +| .skills_prompt_snapshot.json | file | CONFIG JSON | 42870 | +| SOUL.md | file | DOC | 685 | +| _check_repo_json.py | file | SCRIPT PY | 515 | +| _gen_daily_ledger_json.py | file | SCRIPT PY | 2414 | +| _gen_daily_ledger_report.py | file | SCRIPT PY | 3340 | +| _gen_daily_triage.py | file | SCRIPT PY | 2012 | +| _gen_daily_triage_fresh.py | file | SCRIPT PY | 1475 | +| _verify_daily_triage.py | file | SCRIPT PY | 2738 | +| audio_cache | dir | CACHE | 4096 | +| auth.json | file | CONFIG JSON | 143 | +| auth.lock | file | LOCK | 0 | +| backups | dir | BACKUP | 4096 | +| bin | dir | BIN | 4096 | +| cache | dir | CACHE | 4096 | +| config.yaml | file | CONFIG YAML | 510 | +| config.yaml.bak-20261014T165715Z | file | FILE UNCLASSIFIED | 510 | +| context_length_cache.yaml | file | CONFIG YAML | 94 | +| cron | dir | RUNTIME DIR | 4096 | +| cron-fleet-triage-2026-10-17.py | file | SCRIPT PY | 5407 | +| cron-fleet-triage-2026-10-18.py | file | SCRIPT PY | 5355 | +| cron-fleet-verify.py | file | SCRIPT PY | 1790 | +| diag.py | file | SCRIPT PY | 1182 | +| diag2.py | file | SCRIPT PY | 550 | +| diag3.py | file | SCRIPT PY | 1401 | +| diag4.py | file | SCRIPT PY | 1000 | +| forge | dir | FORGE | 4096 | +| gen_sol_profile_triage.py | file | SCRIPT PY | 4372 | +| home | dir | HOME DIR | 4096 | +| hooks | dir | HOOK | 4096 | +| image_cache | dir | CACHE | 4096 | +| inventory_skills.py | file | SCRIPT PY | 6472 | +| keys | dir | CACHE | 4096 | +| logs | dir | LOG DIR | 4096 | +| lsp | dir | LSP | 4096 | +| memories | dir | MEMORY | 4096 | +| models_dev_cache.etag | file | CACHE ETAG | 36 | +| models_dev_cache.json | file | CONFIG JSON | 5922427 | +| pairing | dir | PAIR PLAT | 4096 | +| plans | dir | DIR UNCLASSIFIED | 4096 | +| platforms | dir | PAIR PLAT | 4096 | +| plugin-update-checks | dir | PLUGIN DIR | 4096 | +| plugins | dir | PLUGIN DIR | 4096 | +| runtime | dir | RUNTIME DIR | 4096 | +| sandboxes | dir | RUNTIME DIR | 4096 | +| sessions | dir | RUNTIME DIR | 4096 | +| skills | dir | SKILL CONTAINER | 4096 | +| skins | dir | SKIN | 4096 | +| source-checks | dir | CHECK DIR | 4096 | +| state.db | file | STATE DB | 79552512 | +| state.db-shm | file | STATE DB | 32768 | +| state.db-wal | file | STATE DB | 3893432 | +| state.db.auto-maintenance.lock | file | STATE DB | 0 | +| state.db.fts_rebuild.lock | file | STATE DB | 0 | +| state.db.quarantine.lock | file | STATE DB | 0 | +| triage_check.py | file | SCRIPT PY | 3732 | +| triage_profiles.py | file | SCRIPT PY | 4093 | +| triage_skill_report.py | file | SCRIPT PY | 6354 | +| vault | dir | VAULT | 4096 | +| verify_skills.py | file | SCRIPT PY | 1442 | +| wizard-outputs | dir | OUTPUT DIR | 4096 | +| workspace | dir | RUNTIME DIR | 4096 | diff --git a/wizard-work/sol/verify_sol_profile_file_class_2026-10-28.py b/wizard-work/sol/verify_sol_profile_file_class_2026-10-28.py new file mode 100644 index 0000000..ff0feeb --- /dev/null +++ b/wizard-work/sol/verify_sol_profile_file_class_2026-10-28.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Independent CHECK for the 2026-10-28 sol-profile class artifact. + +Re-derives the classification straight from live disk with its own rule set, +then diffs it against the committed artifact `sol-profile-file-class-2026-10-28.json`. + +Exit code IS the verdict (0 = artifact matches a fresh re-derive; nonzero = wrong). +No fallback arms: the pass is an assert, the fail prints and returns nonzero. +""" +import json +import os +import stat +import sys + +PROFILE = "/root/.hermes/profiles/sol" +WORKDIR = "/root/troupe-standup/wizard-work/sol" +ARTIFACT = os.path.join(WORKDIR, "sol-profile-file-class-2026-10-28.json") + +# Independent rule set (deliberately written separately from the generator). +DIR_MAP = { + "skills": "SKILL_CONTAINER", "lsp": "LSP", "bin": "BIN", "forge": "FORGE", + "backups": "BACKUP", "cache": "CACHE", "image_cache": "CACHE", + "audio_cache": "CACHE", "keys": "CACHE", "cron": "RUNTIME_DIR", + "sessions": "RUNTIME_DIR", "sandboxes": "RUNTIME_DIR", "runtime": "RUNTIME_DIR", + "workspace": "RUNTIME_DIR", "plugins": "PLUGIN_DIR", + "plugin-update-checks": "PLUGIN_DIR", "vault": "VAULT", "logs": "LOG_DIR", + "memories": "MEMORY", "skins": "SKIN", "pairing": "PAIR_PLAT", + "platforms": "PAIR_PLAT", "hooks": "HOOK", "source-checks": "CHECK_DIR", + "home": "HOME_DIR", "wizard-outputs": "OUTPUT_DIR", +} +FILE_EXTS = { + ".yaml": "CONFIG_YAML", ".yml": "CONFIG_YAML", ".json": "CONFIG_JSON", + ".py": "SCRIPT_PY", ".sh": "SCRIPT_SH", ".md": "DOC", ".txt": "LOG_TXT", + ".lock": "LOCK", ".etag": "CACHE_ETAG", +} +UNCLASS_FILE = "FILE_UNCLASSIFIED" +UNCLASS_DIR = "DIR_UNCLASSIFIED" + + +def disk_class(name): + p = os.path.join(PROFILE, name) + if os.path.isdir(p): + return DIR_MAP.get(name, UNCLASS_DIR) + # `.db` membership is decided BEFORE any extension mapping, so + # `state.db-*.lock` files stay in the STATE_DB family (matching the generator). + if ".db" in name: + return "STATE_DB" + # file + for ext, cls in FILE_EXTS.items(): + if name.endswith(ext): + return cls + return UNCLASS_FILE + + +def main(): + with open(ARTIFACT, encoding="utf-8") as f: + a = json.load(f) + + errors = [] + disk_names = sorted(os.listdir(PROFILE)) + art_names = [e["name"] for e in a.get("entries", [])] + + if len(art_names) != len(set(art_names)): + errors.append("duplicate entry names in artifact") + if set(art_names) != set(disk_names): + errors.append("name set mismatch; missing=%s extra=%s" % ( + sorted(set(disk_names) - set(art_names)), + sorted(set(art_names) - set(disk_names)))) + + disk_counts = {} + files = dirs = 0 + for name in disk_names: + cls = disk_class(name) + disk_counts[cls] = disk_counts.get(cls, 0) + 1 + if os.path.isdir(os.path.join(PROFILE, name)): + dirs += 1 + else: + files += 1 + + if a.get("files") != files: + errors.append("files count artifact=%r recompute=%r" % (a.get("files"), files)) + if a.get("dirs") != dirs: + errors.append("dirs count artifact=%r recompute=%r" % (a.get("dirs"), dirs)) + if a.get("total_entries") != len(disk_names): + errors.append("total_entries artifact=%r recompute=%r" % ( + a.get("total_entries"), len(disk_names))) + if a.get("classes") != disk_counts: + errors.append("classes artifact=%r recompute=%r" % (a.get("classes"), disk_counts)) + + art_by_name = {e["name"]: e["class"] for e in a.get("entries", [])} + for name in disk_names: + want = disk_class(name) + got = art_by_name.get(name) + if got != want: + errors.append("class mismatch for %r: artifact=%r recompute=%r" + % (name, got, want)) + + if errors: + print("CHECK FAIL") + for e in errors: + print(" -", e) + return 1 + + print("CHECK OK: %d entries, %d files, %d dirs" % (len(disk_names), files, dirs)) + print("classes: %s" % json.dumps(disk_counts)) + return 0 + + +if __name__ == "__main__": + sys.exit(main())