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
This commit is contained in:
co-authored by
Claude Opus 5
parent
43e1aadef0
commit
edd45dfec6
@@ -0,0 +1,83 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user