POSA_Copyrighter/scripts/update_diagram_manifest.py
changukyu 93a4a82bff test: 다이어그램 드리프트 게이트 신설, 재생성 절차를 실행 가능하게
감사에서 나온 결론은 "그림은 지금 정확하지만 정확함을 지킬 장치가 없다" 였다.
코드 유래 리터럴 18개가 전부 코드와 일치하고 deliver 는 바이트 결정적인데,
드리프트를 막는 게이트·테스트·훅이 0개였고 문서에 적힌 재생성 절차는 그대로는
실행되지 않았다 — `archify` 실행 파일은 PATH 에 없고 실제로는 저장소 밖 전역
스킬을 node 로 불러야 한다. 다음 사람은 첫 명령에서 막혔을 것이다.

  scripts/rebuild-diagrams.ps1       validate -> deliver -> visual-check -> manifest
  scripts/update_diagram_manifest.py 수신증의 단일 작성자
  docs/diagrams/manifest.json        spec/html sha256 + generator 버전
  tests/test_diagram_sync.py         게이트 14건

게이트는 브라우저도 Node 도 archify 도 없이 돈다. 산출물이 바이트 결정적이라
수신증 대조만으로 충분하고, 그래서 폐쇄망 CI 에서도 돈다. 두 사고 시나리오를
실제로 만들어 차단을 확인했다 — HTML 의 단어 하나를 고치면 html sha 가,
spec 의 태그 하나를 고치면 spec sha 가 어긋나 verify 가 실패한다. 노드 id 가
HTML 에 data-node-id 로 박히므로 의미적 연결도 함께 본다.

git 위생 두 건도 같이 정리했다. *.visual-check.json 이 개발 머신 절대 경로와
크롬 설치 경로를 평문으로 담은 채 미추적·비ignore 상태였다(24개) — 납품
저장소가 removable media 로 건너가는 구조라 한 번 커밋되면 제거가 어렵다.
그리고 15,000줄 생성 HTML 을 텍스트로 머지하려 드는 것을 막았다.

재생성해도 4종 sha256 이 전부 불변이라 바이트 결정성을 다시 확인했다.

감사에서 나온 나머지 부채(한글 폰트 미번들, 영어 뷰어 UI, architecture 좌표
하드코딩, lifecycle 손튜닝 8개소, 콘솔 CSP 비호환)는 PROJECT_MAP 부채 표와
다이어그램 절에 등재했다. 폰트와 UI 는 archify 쪽 한계라 spec 으로는 못 고친다.

검증: ruff clean, pytest 600 passed (게이트 +14).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
2026-09-21 10:51:55 +09:00

93 lines
3.1 KiB
Python

"""docs/diagrams/manifest.json 을 산출물 실제 바이트에서 다시 쓴다.
수신증의 단일 작성자다. PowerShell 과 Python 이 각자 JSON 을 쓰면 들여쓰기·키 순서가
갈려 매니페스트가 왔다 갔다 하므로, rebuild-diagrams.ps1 도 이 스크립트를 부른다.
단독 실행도 된다: python scripts/update_diagram_manifest.py
"""
from __future__ import annotations
import hashlib
import io
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DIAGRAM_DIR = ROOT / "docs" / "diagrams"
# (타입, 파일 basename). 다이어그램을 추가하면 여기와 tests/test_diagram_sync.py 의
# NODE_COLLECTION 을 함께 고친다.
DIAGRAMS = [
("architecture", "system-architecture"),
("dataflow", "evidence-dataflow"),
("workflow", "operator-review-workflow"),
("lifecycle", "submission-lifecycle"),
]
NOTE = (
"archify deliver 는 바이트 결정적이다. 이 수신증은 tests/test_diagram_sync.py 가 "
"강제한다 - HTML 을 직접 고치거나 spec 만 고치고 재생성을 잊으면 커밋 게이트가 막는다. "
"갱신은 손으로 하지 말고 scripts/rebuild-diagrams.ps1 로 한다."
)
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _generator_stamp(html: Path) -> str:
match = re.search(
r'<meta name="generator" content="([^"]+)"', html.read_text(encoding="utf-8")
)
if not match:
raise SystemExit(f"{html.name} 에 generator meta 가 없다 - archify 산출물이 맞는가?")
return match.group(1)
def main() -> int:
entries = []
for diagram_type, base in DIAGRAMS:
spec = DIAGRAM_DIR / f"{base}.{diagram_type}.json"
html = DIAGRAM_DIR / f"{base}.html"
for path in (spec, html):
if not path.exists():
raise SystemExit(f"없는 파일: {path}")
entries.append(
{
"type": diagram_type,
"spec": spec.name,
"html": html.name,
"specSha256": _sha256(spec),
"htmlSha256": _sha256(html),
"htmlBytes": html.stat().st_size,
}
)
generators = {_generator_stamp(DIAGRAM_DIR / e["html"]) for e in entries}
if len(generators) != 1:
raise SystemExit(
f"산출물마다 generator 가 다르다: {sorted(generators)} - 전부 재생성해야 한다."
)
manifest = {
"note": NOTE,
"generator": generators.pop(),
"diagrams": entries,
}
target = DIAGRAM_DIR / "manifest.json"
with io.open(target, "w", encoding="utf-8", newline="\n") as handle:
json.dump(manifest, handle, ensure_ascii=False, indent=2)
handle.write("\n")
# 비ASCII 는 stdout 에 넣지 않는다 (콘솔이 cp949 라 UnicodeEncodeError 로 죽은 이력).
print("manifest updated: %d diagrams, generator %s" % (len(entries), manifest["generator"]))
for entry in entries:
print(" %-30s %s.. %d B" % (entry["html"], entry["htmlSha256"][:8], entry["htmlBytes"]))
return 0
if __name__ == "__main__":
sys.exit(main())