210 lines
7.6 KiB
Python
210 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Independent verifier for the ledger skill-catalog extraction artifact.
|
|
|
|
Re-derives every skill row from the live filesystem (/root/.hermes/profiles/
|
|
ledger/skills) and diffs row-for-row against the committed artifact files:
|
|
skill_metadata.csv, skill_metadata.jsonl, skill_metadata.md,
|
|
source_manifest.txt, summary.txt. Exits 0 iff all match.
|
|
|
|
This is a real test, not a tautology: the committed files are regenerated from
|
|
raw SKILL.md/DESCRIPTION.md frontmatter and compared to what was committed.
|
|
"""
|
|
import csv
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
PROFILE = "/root/.hermes/profiles/ledger/skills"
|
|
OUT_DIR = "/root/troupe-standup/wizard-work/ledger"
|
|
CSV_PATH = os.path.join(OUT_DIR, "skill_metadata.csv")
|
|
JSONL_PATH = os.path.join(OUT_DIR, "skill_metadata.jsonl")
|
|
MD_PATH = os.path.join(OUT_DIR, "skill_metadata.md")
|
|
MAN_PATH = os.path.join(OUT_DIR, "source_manifest.txt")
|
|
SUM_PATH = os.path.join(OUT_DIR, "summary.txt")
|
|
|
|
CSV_HEADER = ["category", "name", "description", "version", "platforms", "file", "sha256"]
|
|
|
|
|
|
def sha256_of(path):
|
|
h = hashlib.sha256()
|
|
with open(path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
def parse_frontmatter(text):
|
|
if not text.startswith("---"):
|
|
return "", "", ""
|
|
m = re.match(r"---\s*\n(.*?)\n---|.*?$", text, re.DOTALL)
|
|
if not m or m.group(1) is None:
|
|
return "", "", ""
|
|
body = m.group(1)
|
|
desc = version = platforms = ""
|
|
for raw in body.splitlines():
|
|
line = raw.rstrip()
|
|
if not line.strip() or line.strip().startswith("#"):
|
|
continue
|
|
if ":" in line:
|
|
k, _, v = line.partition(":")
|
|
k, v = k.strip(), v.strip()
|
|
if k == "description":
|
|
desc = v.strip('"').strip("'")
|
|
elif k == "version":
|
|
version = v.strip('"').strip("'")
|
|
elif k == "platforms":
|
|
platforms = v
|
|
return desc, version, platforms
|
|
|
|
|
|
def re_derive_rows():
|
|
rows = []
|
|
for dirpath, dirnames, filenames in os.walk(PROFILE):
|
|
dirnames.sort()
|
|
for fn in sorted(filenames):
|
|
if fn not in ("SKILL.md", "DESCRIPTION.md"):
|
|
continue
|
|
full = os.path.join(dirpath, fn)
|
|
rel = os.path.relpath(full, PROFILE)
|
|
parts = rel.split(os.sep)
|
|
category = parts[0]
|
|
name = os.path.splitext(os.path.splitext(rel)[0])[0].replace(os.sep, "/")
|
|
text = open(full, encoding="utf-8").read()
|
|
h = sha256_of(full)
|
|
if rel.endswith("SKILL.md"):
|
|
desc, version, platforms = parse_frontmatter(text)
|
|
else:
|
|
desc, version, platforms = parse_frontmatter(text)
|
|
desc = desc.strip()
|
|
if not desc:
|
|
desc = text.splitlines()[0].strip()
|
|
version = "1.0.0"
|
|
platforms = "linux"
|
|
rows.append((category, name, desc, version, platforms, rel, h))
|
|
rows.sort(key=lambda r: r[5])
|
|
return rows
|
|
|
|
|
|
def derive_csv_text(rows):
|
|
import io
|
|
buf = io.StringIO()
|
|
w = csv.writer(buf, lineterminator="\n")
|
|
w.writerow(["category", "name", "description", "version", "platforms", "file", "sha256"])
|
|
for cat, name, desc, ver, plat, fn, h in rows:
|
|
w.writerow([cat, name, desc, ver, plat, fn, h])
|
|
return buf.getvalue()
|
|
|
|
|
|
def derive_jsonl_text(rows):
|
|
out = []
|
|
for cat, name, desc, ver, plat, fn, h in rows:
|
|
out.append(json.dumps({
|
|
"category": cat, "name": name, "description": desc, "version": ver,
|
|
"platforms": plat, "file": fn, "sha256": h}, ensure_ascii=False))
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
def main():
|
|
rows = re_derive_rows()
|
|
failures = 0
|
|
|
|
def check(label, expected, actual):
|
|
nonlocal failures
|
|
if expected == actual:
|
|
print(f" OK {label}")
|
|
else:
|
|
failures += 1
|
|
print(f" FAIL {label}")
|
|
|
|
# 1) CSV
|
|
print("CSV skill_metadata.csv")
|
|
committed_csv = open(CSV_PATH, encoding="utf-8").read()
|
|
check("byte-identical", derive_csv_text(rows), committed_csv)
|
|
with open(CSV_PATH, newline="", encoding="utf-8") as f:
|
|
r = csv.reader(f)
|
|
header = next(r)
|
|
committed_rows = [tuple(x) for x in r]
|
|
check("header", CSV_HEADER, header)
|
|
check("row count", str(len(rows)), str(len(committed_rows)))
|
|
check("records", [tuple(x) for x in rows], committed_rows)
|
|
|
|
# 2) JSONL
|
|
print("JSONL skill_metadata.jsonl")
|
|
committed_jsonl = open(JSONL_PATH, encoding="utf-8").read()
|
|
check("byte-identical", derive_jsonl_text(rows), committed_jsonl)
|
|
|
|
# 3) Markdown
|
|
print("Markdown skill_metadata.md")
|
|
counts = {}
|
|
for cat, name, desc, ver, plat, fn, h in rows:
|
|
counts[cat] = counts.get(cat, 0) + 1
|
|
md_lines = []
|
|
md_lines.append("# Ledger Profile — Installed Skills Catalog")
|
|
md_lines.append("")
|
|
md_lines.append(f"Source: every `SKILL.md` and `DESCRIPTION.md` under `{PROFILE}` "
|
|
"(real filesystem, parsed 2026-10-30).")
|
|
md_lines.append("")
|
|
md_lines.append("## Counts by category")
|
|
md_lines.append("")
|
|
md_lines.append("| Category | Skills |")
|
|
md_lines.append("|---|---|")
|
|
for cat in sorted(counts):
|
|
md_lines.append(f"| {cat} | {counts[cat]} |")
|
|
md_lines.append("")
|
|
md_lines.append(f"**Total skills: {len(rows)}**")
|
|
md_lines.append("")
|
|
md_lines.append("## All skills")
|
|
md_lines.append("")
|
|
md_lines.append("| Category | Name | Version | Platforms | File | Description |")
|
|
md_lines.append("|---|---|---|---|---|---|")
|
|
for cat, name, desc, ver, plat, fn, h in rows:
|
|
d = desc.replace("|", "\\|")
|
|
md_lines.append(f"| {cat} | {name} | {ver} | {plat} | {fn} | {d} |")
|
|
committed_md = open(MD_PATH, encoding="utf-8").read()
|
|
check("byte-identical", "\n".join(md_lines) + "\n", committed_md)
|
|
|
|
# 4) source_manifest.txt
|
|
print("source_manifest.txt")
|
|
man_lines = [f"Source: {PROFILE}", f"Skill files found: {len(rows)}", "",
|
|
"rel_path\tbytes\tsha256"]
|
|
for cat, name, desc, ver, plat, fn, h in rows:
|
|
full = os.path.join(PROFILE, fn)
|
|
size = os.path.getsize(full)
|
|
man_lines.append(f"{fn}\t{size}\t{h}")
|
|
committed_man = open(MAN_PATH, encoding="utf-8").read()
|
|
check("byte-identical", "\n".join(man_lines) + "\n", committed_man)
|
|
|
|
# 5) summary.txt
|
|
print("summary.txt")
|
|
summ_lines = [
|
|
"Ledger Profile — Installed Skills Catalog — extraction summary",
|
|
"=" * 64,
|
|
"",
|
|
f"Source directory : {PROFILE}",
|
|
f"Files scanned : {len(rows)}",
|
|
f"Categories : {len(counts)}",
|
|
"Category counts : " + ", ".join(f"{c}={counts[c]}" for c in sorted(counts)),
|
|
f"SKILL.md entries : {sum(1 for r in rows if r[5].endswith('SKILL.md'))}",
|
|
f"DESCRIPTION.md entries : {sum(1 for r in rows if r[5].endswith('DESCRIPTION.md'))}",
|
|
f"Rows with version field : {sum(1 for r in rows if r[3])}",
|
|
"",
|
|
"Artifacts produced : skill_metadata.csv, skill_metadata.jsonl,",
|
|
" skill_metadata.md, source_manifest.txt,",
|
|
" summary.txt",
|
|
"",
|
|
]
|
|
committed_sum = open(SUM_PATH, encoding="utf-8").read()
|
|
check("byte-identical", "\n".join(summ_lines), committed_sum)
|
|
|
|
if failures:
|
|
print(f"FAIL: {failures} mismatch(es)")
|
|
sys.exit(1)
|
|
print("PASS: all skill-catalog artifacts re-derive from the filesystem.")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|