R4 · R13 · R6 · R3. 네 가지가 같은 자리를 본다 — 검사기가 대상을 못 찾았을 때 무엇을
내는가.
세 상태를 가른다 (CLAUDE.md 「검사」 절의 표).
봤고 괜찮다 exit 0 문제 없음 · error 0
대상이 성립하지 않는다 exit 2 대상이 성립하지 않는다 — <이유>
볼 것이 아직 없다 exit 0 기록 0건 — 아직 쓴 기록이 없다
R4 — 없는 프로젝트를 주면 여섯 중 넷이 초록을 냈다. `verify-tech-log-tree.py` 는 그것을
「프로젝트 1 · error 0 · PASS」로 셌다 — 오타 한 번이면 검사를 다 돈 것처럼 보인다.
판정은 `techlog.check_targets()` 하나로 모은다. 여섯 곳에 같은 규칙을 따로 쓰면 다음에
하나만 어긋난다. `check_evidence.mjs` 는 이미 exit 2 라 문구만 맞춘다.
R13 — 프로젝트 이름 쪽만 고치면 `--file` 로 오타를 내는 순간 다시 조용히 0건이 된다.
`techlog.check_files()` 로 같은 자리에 둔다. `preview-figure.py` 는 인자가 아예 없을 때
exit 1 을 냈는데 그것도 「대상이 성립하지 않는다」다.
R6 — 두 자리를 함께 고쳐야 했다.
(a) `verify_projects()` 가 `docs/*/tech-log-studio` 만 훑어 계약 없는 프로젝트가
목록에서 사라졌다. 기준을 `final/document.md` 로 바꾼다 — SSOT 가 있으면 대상이다.
(b) `verify-tech-log-tree.py:194` 가 「분해 계약 없음」을 warn 으로 냈다. CLAUDE.md 는
「계약 미채택도 error 다 — 경고로 두면 옛 스키마로 남아 있는 한 검사를 피한다」고
적어 두었는데, 경고로 두었더니 실제로 그렇게 됐다.
R3 — `verify-pipeline.py` 가 `check-figure-text.py` 와 `check_evidence.mjs --repo` 를
프로젝트마다 돌린다(`OUTPUT CHECKS`). 게시 전에 돌리라고 적어 둔 검사인데 전체 훑기가
부르지 않아 결함이 있는 채로 PASS 로 보고됐다. `check-required-content.py` 자리는 주석으로
남겨 둔다 — 그 파일이 들어온 뒤에 더한다.
회귀 `scripts/tests/test_no_target.py` 7건 (92 → 99). 「실재하는 경로는 통과한다」 대조를
함께 넣는다 — 무조건 거절로 성공률을 올리는 것이 R-003 이 든 실패다. 판정을 일부러
되돌려 FAILED 가 나는 것을 확인한 뒤 복구했다.
알려진 부채는 그대로 둔다. `verify-pipeline.py` 는 exit 1 이다 — 미준수 런 1건과
`OUTPUT CHECKS` 5건, 그리고 `ca-tmpl` 의 계약 없음이 이제 `TECH LOG TREES` 에서도 보인다.
같은 사실이고 두 번 세지 않는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp9jNbePAmWc5jQwCYhK9v
119 lines
4.1 KiB
Python
Executable File
119 lines
4.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""그림 안 <text> 가 전부 이름인지 본다.
|
|
|
|
`code-tables-diagrams.md` 의 자가 점검을 기계로 옮긴 것이다. 그림은 관계를 보이고
|
|
문장은 `<desc>` 와 옆 문단에 둔다. 길이가 아니라 **서술하느냐** 가 기준이다.
|
|
|
|
python3 scripts/check-figure-text.py [프로젝트 ...]
|
|
python3 scripts/check-figure-text.py --file 그림.svg
|
|
|
|
`<title>` 과 `<desc>` 는 화면을 못 보는 사람이 듣는 자리라 검사하지 않는다.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import glob
|
|
import os
|
|
import re
|
|
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
|
|
|
|
TEXT = re.compile(r"<text[^>]*>([^<]*)</text>")
|
|
|
|
# 서술어로 끝난다 — `~있다`, `~막는다`, `~바꾼다`
|
|
PREDICATE = re.compile(
|
|
r"(?:이다|아니다|있다|없다|한다|된다|본다|같다|다르다|늘어난다|줄어든다"
|
|
r"|막는다|바꾼다|만든다|넣는다|뺀다|남는다|생긴다|끝난다)$")
|
|
|
|
# 조사로 두 대상을 잇는다 — `A를 B로`, `A에서 B까지`
|
|
# 관형형 어미(`깊은 페이지`)와 겹치는 는·은·이·가 는 넣지 않는다. 그쪽은 서술어 규칙이 잡는다
|
|
JOINING = re.compile(
|
|
r"[가-힣A-Za-z0-9_)\]]+(?:을|를|에서|에게|에는|으로|로서|로써|으로써|마다|부터|까지|보다|처럼)\s+\S")
|
|
|
|
# 문장 부호로 끝난다. `…` 과 축약형 `(?,…)` 은 뺀다
|
|
SENTENCE_END = re.compile(r"[.?!]$")
|
|
|
|
|
|
def offences(label: str) -> list[str]:
|
|
t = label.strip()
|
|
if not t:
|
|
return []
|
|
found = []
|
|
if SENTENCE_END.search(t) and not t.endswith(("…", "..")):
|
|
found.append("문장 부호로 끝난다")
|
|
if PREDICATE.search(t):
|
|
found.append("서술어가 있다")
|
|
if JOINING.search(t):
|
|
found.append("조사로 두 대상을 잇는다")
|
|
return found
|
|
|
|
|
|
def check(path: str) -> list[tuple[str, list[str]]]:
|
|
body = open(path, encoding="utf-8").read()
|
|
out = []
|
|
for label in TEXT.findall(body):
|
|
why = offences(label)
|
|
if why:
|
|
out.append((label.strip(), why))
|
|
return out
|
|
|
|
|
|
def svgs_of(project: str) -> list[str]:
|
|
assets = os.path.join(ROOT, "docs", project, "final", "assets")
|
|
return sorted(glob.glob(os.path.join(assets, "**", "*.svg"), recursive=True))
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="그림 안 <text> 가 전부 이름인지 본다.")
|
|
ap.add_argument("projects", nargs="*")
|
|
ap.add_argument("--file", action="append", default=[],
|
|
help="프로젝트 대신 SVG 파일을 직접 준다")
|
|
args = ap.parse_args()
|
|
|
|
targets = list(args.file)
|
|
if targets:
|
|
bad = techlog.check_files(targets)
|
|
if bad is not None:
|
|
return bad
|
|
if not targets:
|
|
if args.projects:
|
|
bad = techlog.check_targets(args.projects, ROOT, "final")
|
|
if bad is not None:
|
|
return bad
|
|
projects = args.projects or sorted(
|
|
d for d in os.listdir(os.path.join(ROOT, "docs"))
|
|
if not d.startswith("_")
|
|
and os.path.isdir(os.path.join(ROOT, "docs", d, "final")))
|
|
for project in projects:
|
|
targets.extend(svgs_of(project))
|
|
if not targets:
|
|
print(f"그림 0장 — {' · '.join(projects)} 에 final/assets 아래 SVG 가 아직 없다")
|
|
return 0
|
|
|
|
total = 0
|
|
files = 0
|
|
for path in targets:
|
|
hits = check(path)
|
|
if not hits:
|
|
continue
|
|
files += 1
|
|
total += len(hits)
|
|
print(f" {os.path.relpath(path, ROOT)}")
|
|
for label, why in hits:
|
|
print(f" {label} ← {' · '.join(why)}")
|
|
|
|
checked = len(targets)
|
|
if total:
|
|
print(f"\nFIGURE TEXT: FAIL — 그림 {checked}장 · 문장 {total}건 / 그림 {files}장")
|
|
print("그림 안 <text> 는 이름만 담는다. 문장은 <desc> 와 옆 문단으로 내린다.")
|
|
return 1
|
|
print(f"FIGURE TEXT: PASS — 그림 {checked}장 · 문장 0건")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|