Files
document-haness/scripts/skill-versions.py
T
DongHyeonkaandClaude Opus 5 edd45dfec6 feat(scripts): 종료 코드를 손으로 적을 수 없게 만들고 스킬에 버전을 붙인다
capture-evidence.py 는 명령을 subprocess 로 직접 돌리고 그 프로세스의 반환값을
그대로 메타에 적는다. 종료 코드를 인자로 받지 않으므로 손으로 적을 경로가 없다.
raw 원문과 실행 메타가 같은 이름으로 함께 떨어져 「raw 는 있는데 meta 가 없다」가
구조적으로 안 생긴다.

skill-versions.py 는 스킬 9개의 metadata.version 과 검사기 17개의 내용 해시를
한 장으로 낸다. 통과 판정을 검증기 버전에 묶으려면 묶을 값이 있어야 한다.
버전 칸이 없던 스킬 여덟에 1.0.0 을 붙였다. 산문은 한 줄도 안 바꿨다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:06:07 +09:00

84 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""스킬과 검사기의 버전을 한 장으로 뽑는다.
통과 판정은 「어느 문서를 어느 검사기로 봤는가」에 묶여야 한다. 스킬은 `SKILL.md` 의
`metadata.version` 이 그 값이고, 스크립트 검사기는 버전 칸이 없으므로 파일 내용의 sha256
앞 12자를 쓴다. 버전을 올리는 것을 잊어도 sha 는 따라 움직인다.
python3 scripts/skill-versions.py # 사람이 읽는 표
python3 scripts/skill-versions.py --json # 판정에 붙일 값
"""
from __future__ import annotations
import argparse
import glob
import hashlib
import json
import os
import re
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GATE_SCRIPTS = [
"scripts/verify-tech-log-tree.py", "scripts/verify-project-layout.py",
"scripts/verify-pipeline-run.py", "scripts/verify-pipeline.py",
"scripts/audit-records.py", "scripts/check-figure-text.py",
"scripts/check-figure-overlap.py", "scripts/build-tech-log-tree.py",
"scripts/studio-body.py", "scripts/capture-evidence.py",
]
def _sha12(path: str) -> str | None:
try:
with open(path, "rb") as fh:
return hashlib.sha256(fh.read()).hexdigest()[:12]
except OSError:
return None
def collect() -> dict:
skills = {}
for path in sorted(glob.glob(os.path.join(ROOT, ".agents/skills/*/SKILL.md"))):
name = os.path.basename(os.path.dirname(path))
text = open(path, encoding="utf-8").read()
m = re.search(r"^metadata:\n(?: .*\n)*? version:\s*\"?([^\"\n]+)\"?",
text, re.M)
skills[name] = {"version": m.group(1).strip() if m else None,
"sha12": _sha12(path)}
checkers = {}
for rel in GATE_SCRIPTS:
checkers[rel] = {"version": None, "sha12": _sha12(os.path.join(ROOT, rel))}
for path in sorted(glob.glob(os.path.join(ROOT, ".agents/skills/*/scripts/*.mjs"))):
rel = os.path.relpath(path, ROOT)
checkers[rel] = {"version": None, "sha12": _sha12(path)}
return {"skills": skills, "checkers": checkers}
def main() -> int:
ap = argparse.ArgumentParser(description="스킬·검사기의 버전과 내용 해시를 뽑는다.")
ap.add_argument("--json", action="store_true")
args = ap.parse_args()
data = collect()
if args.json:
print(json.dumps(data, ensure_ascii=False, indent=2))
return 0
missing = 0
print("스킬")
for name, v in data["skills"].items():
mark = " " if v["version"] else "✗"
if not v["version"]:
missing += 1
print(f" {mark} {name:<40} {v['version'] or '버전 없음':<10} {v['sha12']}")
print("\n검사기 — 버전 칸이 없어 내용 해시로 묶는다")
for name, v in data["checkers"].items():
print(f" {name:<70} {v['sha12']}")
print(f"\nSKILL VERSIONS: {'FAIL' if missing else 'PASS'} — "
f"스킬 {len(data['skills'])} · 버전 없음 {missing} · 검사기 {len(data['checkers'])}")
return 1 if missing else 0
if __name__ == "__main__":
raise SystemExit(main())