feat(scripts): 경고를 읽는 장치가 아니라 만드는 장치를 더한다
남은 셋은 묶음 경고가 0건이라 게이트를 아무리 조여도 지나간다. 뿌리가 하나다 — 지금 검사기는 사라진 것과 새로 생긴 보호 구간을 보고, 범위가 넓어진 것과 정본을 안 거친 것을 보는 자리가 없었다. 그림이 정본을 거쳤는지 본다. SVG 는 바뀌었는데 spec.json 은 그대로면 그 그림은 정본에서 나온 것이 아니고, 화살표 뒤집기가 그 모양이다. spec 의 간선과 SVG 의 경로를 직접 견주려면 렌더러가 id 를 어떻게 붙이는지 알아야 하는데, 정본을 거쳤는지만 보면 몰라도 된다. 작업 트리와 이력 두 자리를 본다. 정본이 없는 그림은 볼 것이 아니라 세기만 한다. 적용 범위를 넓히는 말이 새로 들어왔는지 본다. 로컬에서 확인했다에 운영 환경에서도를 더하기만 하면 유보도 보호 구간도 안 바뀐다 — 지운 것이 없기 때문이다. 유보 감소의 반대편이고, 판정하지 않고 경고로 올린다. 경고 생산자를 더하는 것이 과잉 차단이 생기는 자리라 후보마다 채택 편집 100쌍을 돌렸다. error 도 경고도 안 늘었다. 정상 편집에 경고가 붙으면 그것도 사실상 차단이다. 목록에 운영·항상 같은 흔한 말이 들어가서, 원래 있던 말을 두고 주변만 고쳐도 걸리는지 보는 대조군을 회귀에 넣었다. 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
16cbe141a0
commit
e3230ce5ed
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""그림이 정본에서 나온 것인지 본다 — **손으로 고친 SVG 를 잡는다.**
|
||||
|
||||
`technical-visualizer` 는 `.techviz/<이름>/spec.json` 을 정본으로 삼아 SVG 를 만든다.
|
||||
**SVG 는 바뀌었는데 spec 은 그대로면 그 그림은 정본에서 나온 것이 아니다.** 화살표를
|
||||
뒤집거나 라벨을 바꾸는 편집이 정확히 그 모양이다.
|
||||
|
||||
지금 그림 쪽 검사기가 보는 것은 셋이다 — `<text>` 가 이름인지 · 상자와 라벨이 겹치는지 ·
|
||||
해시가 검토 뒤에 바뀌었는지. **셋 다 그림이 무엇을 말하는지는 안 본다.** 화살표 방향과
|
||||
주체는 사람이 봐야 하지만, **그림이 정본을 거치지 않았다는 것은 문자열로 판정된다.**
|
||||
|
||||
`spec.json` 의 간선과 SVG 의 경로를 직접 견주는 길도 있지만, 어느 `<path>` 가 어느 간선인지
|
||||
대조하려면 렌더러가 id 를 어떻게 붙이는지 알아야 한다. **정본을 거쳤는지만 보면 렌더러를
|
||||
몰라도 된다.**
|
||||
|
||||
두 자리를 본다.
|
||||
|
||||
- **작업 트리** — SVG 가 고쳐졌는데 spec 은 안 고쳐졌다. 지금 손으로 고치는 중이다
|
||||
- **이력** — SVG 의 마지막 커밋이 spec 의 마지막 커밋보다 나중이다. 손으로 고쳐 커밋했다
|
||||
|
||||
**spec 이 없는 그림은 이 검사기가 볼 것이 아니다.** 저장소의 그림 272장 중 209장이 그렇고,
|
||||
그것은 `verify-project-layout.py` 의 「techviz 정본이 없는 그림」이 세는 자리다. 여기서는
|
||||
못 본 것으로 센다 — 조용히 건너뛰면 「전부 맞다」가 「본 것만 맞다」를 가린다.
|
||||
|
||||
python3 scripts/check-figure-provenance.py <프로젝트>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "scripts"))
|
||||
import techlog # noqa: E402
|
||||
|
||||
|
||||
def _git(*args: str) -> str:
|
||||
try:
|
||||
p = subprocess.run(["git", "-C", ROOT, *args],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return ""
|
||||
return p.stdout.strip() if p.returncode == 0 else ""
|
||||
|
||||
|
||||
def _dirty(rel: str) -> bool:
|
||||
return bool(_git("status", "--porcelain", "--", rel))
|
||||
|
||||
|
||||
def _last_commit_time(rel: str) -> int | None:
|
||||
out = _git("log", "-1", "--format=%ct", "--", rel)
|
||||
return int(out) if out.isdigit() else None
|
||||
|
||||
|
||||
def verify(project: str) -> techlog.Report:
|
||||
rep = techlog.Report(project)
|
||||
base = os.path.join(ROOT, "docs", project, "final")
|
||||
paired = unpaired = 0
|
||||
for svg in sorted(glob.glob(os.path.join(base, "assets", "**", "*.svg"),
|
||||
recursive=True)):
|
||||
name = os.path.basename(svg)[:-4]
|
||||
spec = os.path.join(base, ".techviz", name, "spec.json")
|
||||
svg_rel = os.path.relpath(svg, ROOT)
|
||||
if not os.path.exists(spec):
|
||||
unpaired += 1
|
||||
continue
|
||||
paired += 1
|
||||
spec_rel = os.path.relpath(spec, ROOT)
|
||||
|
||||
if _dirty(svg_rel) and not _dirty(spec_rel):
|
||||
rep.error("정본을 거치지 않고 고친 그림 — 작업 트리",
|
||||
f"{svg_rel} 이 고쳐졌는데 {spec_rel} 은 그대로다")
|
||||
continue
|
||||
st, sp = _last_commit_time(svg_rel), _last_commit_time(spec_rel)
|
||||
if st is not None and sp is not None and st > sp:
|
||||
rep.error("정본을 거치지 않고 고친 그림 — 이력",
|
||||
f"{svg_rel} 의 마지막 커밋이 {spec_rel} 보다 {st - sp}초 나중이다")
|
||||
rep.facts["정본과 짝지은 그림"] = paired
|
||||
if unpaired:
|
||||
rep.facts["정본이 없어 못 본 그림"] = unpaired
|
||||
return rep
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="그림이 정본에서 나온 것인지 본다.")
|
||||
ap.add_argument("projects", nargs="*")
|
||||
ap.add_argument("--samples", type=int, default=3)
|
||||
args = ap.parse_args()
|
||||
|
||||
projects = args.projects or sorted(
|
||||
os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
for p in glob.glob(os.path.join(ROOT, "docs/*/final/assets"))
|
||||
if not os.path.basename(os.path.dirname(os.path.dirname(p))).startswith("_"))
|
||||
missing = [p for p in projects if not os.path.isdir(os.path.join(ROOT, "docs", p))]
|
||||
if missing:
|
||||
print(f"대상이 성립하지 않는다 — 그런 프로젝트가 없다: {', '.join(missing)}",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
if not projects:
|
||||
print("대상이 성립하지 않는다 — 그림을 가진 프로젝트가 없다", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
reports = [verify(p) for p in projects]
|
||||
for rep in reports:
|
||||
facts = " · ".join(f"{k}={v}" for k, v in rep.facts.items()) or "그림 없음"
|
||||
print(f"\n[{rep.project}] {facts}")
|
||||
for rule, details in sorted(rep.errors.items(), key=lambda kv: -len(kv[1])):
|
||||
print(f" ✗ error {len(details):>4} {rule}")
|
||||
for d in details[:args.samples]:
|
||||
print(f" · {d}")
|
||||
e = sum(r.error_count for r in reports)
|
||||
print(f"\nFIGURE PROVENANCE: {'FAIL' if e else 'PASS'} — "
|
||||
f"프로젝트 {len(reports)} · error {e}")
|
||||
return 1 if e else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user