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
84 lines
3.1 KiB
Python
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())
|