Files
artex/scripts/check-web-cjk.py
T
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

130 lines
6.9 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""정적 내보내기한 웹 UI(`web/out`)의 HTML 에 중국어(한자)가 새어 나왔는지 검사한다.
이 포크의 핵심 성과이자 차별점은 "사용자에게 보이는 화면에 중국어가 없다"는 것이다
(백로그 B4 가 수립한 `out` HTML 한자 0 속성). 그 속성을 손검증에만 맡기면, 기여자가
중국어 문자열을 하드코딩하거나 번역이 누락되는 회귀가 생겨도 다음 사람이 눈으로
발견할 때까지 조용히 깨진 채 남는다. 이 스크립트는 그 회귀를 머지 게이트에서 막는다.
검사 방식
---------
`web/out` 아래 모든 `.html` 파일의 **원시 내용**(렌더된 마크업뿐 아니라 인라인
스크립트·React Server Components 플라이트 데이터까지)을 훑어 한자가 하나라도 있으면
종료 코드 1 로 끝난다. 원시 내용을 그대로 보는 이유는, 사용자에게 보이는 텍스트가
플라이트 데이터(JSON 꼴 문자열)에 실려 HTML 에 함께 내려오기 때문이다. 태그만
벗겨 내면 그 경로의 누출을 놓친다.
검사 범위 밖 (정직한 한계)
-----------------------
이 게이트는 정적 내보내기 결과물만 본다. 그래서 **MOCK 모드(`NEXT_PUBLIC_MOCK=1`,
공개 데모가 쓰는 모드)에서 브라우저가 실행 중에 그려 내는 내용**까지는 검사하지 못한다.
`web/src/lib/mock/data.ts`·`handler.ts` 와 각 페이지의 하드코딩 샘플은 하이드레이션
뒤 `useEffect` 가 `mockHandle()` 로 가져와 React 상태로 렌더하므로, `out` HTML 이
아니라 런타임 JS 번들에만 실린다. 즉 이 경로로 들어온 중국어는 이 게이트를 통과한다
(과거 공개 데모의 `/system/logs` 샘플 로그에 중국어가 그렇게 조용히 남아 있었고,
백로그 G114 에서 한국어로 고쳤다).
그 표면은 소스 검토로 지킨다. 렌더 대상 파일(`web/src` 의 `.tsx`·클라이언트 데이터)에
남는 중국어는 반드시 다음 중 하나여야 한다: 코드 주석, `t.has(...)` 로 막혀 실제로는
쓰이지 않는 i18n 폴백, 백엔드 와이어 포맷 토큰(챗 멘션 분류 라벨·`[模型]` 결정 출처
센티넬), locale 가 `zh` 일 때만 보이는 분기 리터럴, 또는 성능 보존을 위해 번역하지
않는 에이전트 두뇌(段 [A]) 프롬프트를 그대로 비추는 미러. 이 가운데 어디에도 속하지
않고 "그냥 화면에 보이는 중국어"라면 한국어로 번역한다(백로그 G115 가 이 분류를
전수 확인했다).
허용·탐지 범위
-------------
- **한글(`가-힣` 등)은 허용**한다. 한자 블록과 한글 블록은 유니코드에서 겹치지 않으므로,
한자 블록만 탐지하면 한글은 자동으로 통과한다.
- **한자(CJK 한자)만 탐지**한다: 통합 한자와 확장 A(`㐀`–`鿿`), 호환 한자
(`豈`–`﫿`), 그리고 보충 평면의 확장 B 이상(`𠀀`–). 가나·전각 문장부호는
"한자"가 아니므로 이 게이트의 대상이 아니다(원문 보존 코드 블록이나 증거 인용에
섞여 들어와 거짓 양성을 내는 것을 막기 위해 범위를 한자로 좁힌다).
상류 보존이 필요한 의도적 한자
-----------------------------
현재 `out` HTML 에는 한자가 한 글자도 없어 허용 목록이 비어 있다. 앞으로 성능 보존을
위해 원문(중국어)을 일부러 화면에 남겨야 하는 경우(예: 언어 스위처의 `中文` 라벨)가
생기면, 그 글자를 아래 `ALLOWED_HAN` 에 추가하고 왜 남기는지 주석으로 적는다. 그래야
게이트는 엄격하게 유지되면서도 의도된 예외만 좁게 통과시킬 수 있다.
표준 라이브러리만 쓰고 네트워크에 접속하지 않는다.
실행: `python3 -I scripts/check-web-cjk.py [out 디렉터리]`
(기본값은 저장소 루트의 `web/out`. CI 는 `web` 작업 디렉터리에서
`python3 -I ../scripts/check-web-cjk.py` 로 부른다.)
"""
import os
import re
import subprocess
import sys
# CJK 한자 블록만. 한글(AC00–D7A3)·가나·전각 문장부호는 들어 있지 않다.
# 범위: 확장 A + 통합 한자(U+3400–U+9FFF = 㐀–鿿), 호환 한자(U+F900–U+FAFF = 豈–﫿),
# 보충 평면 확장 B 이상(U+20000–U+2FFFF).
HAN = re.compile(r"[㐀-鿿豈-﫿\U00020000-\U0002ffff]")
# 화면에 일부러 남기는 한자(예: 언어 스위처 `中文`). 지금은 비어 있다 —
# 비우면 어떤 한자든 게이트에 걸린다. 예외를 더할 때는 글자와 사유를 함께 적는다.
# 예) ALLOWED_HAN = {"中", "文"} # 언어 스위처 '中文' 라벨(상류 대조용)
ALLOWED_HAN: set = set()
def find_out_dir() -> str:
"""검사할 `out` 디렉터리를 정한다.
인자로 경로를 주면 그것을, 없으면 git 저장소 루트의 `web/out` 를 쓴다.
"""
if len(sys.argv) > 1:
return os.path.abspath(sys.argv[1])
root = subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
).strip()
return os.path.join(root, "web", "out")
def main() -> int:
out_dir = find_out_dir()
if not os.path.isdir(out_dir):
print(
f"오류: 검사할 디렉터리가 없습니다 — {out_dir}\n"
"먼저 `npm run build:static` 로 정적 내보내기를 만든 뒤 실행하세요."
)
return 2
checked = 0
offenders = [] # (상대경로, 한자 개수, 미리보기)
for dirpath, _dirnames, filenames in os.walk(out_dir):
for name in filenames:
if not name.endswith(".html"):
continue
checked += 1
path = os.path.join(dirpath, name)
with open(path, encoding="utf-8", errors="ignore") as fh:
text = fh.read()
found = [ch for ch in HAN.findall(text) if ch not in ALLOWED_HAN]
if found:
rel = os.path.relpath(path, out_dir)
kinds = "".join(sorted(set(found)))
offenders.append((rel, len(found), kinds))
print(f"검사한 HTML {checked}개 (대상: {out_dir})")
if offenders:
print(f"중국어(한자) 누출 {len(offenders)}개 파일 — 사용자 노출 HTML 에 한자가 있습니다:")
for rel, count, kinds in offenders:
preview = kinds if len(kinds) <= 30 else kinds[:30] + "…"
print(f" out/{rel}: 한자 {count}개 · 종류 {preview}")
print(
"\n한글로 번역하거나, 상류 보존이 꼭 필요한 의도적 한자라면 "
"scripts/check-web-cjk.py 의 ALLOWED_HAN 에 사유와 함께 추가하세요."
)
return 1
if checked == 0:
print("경고: 검사한 HTML 이 없습니다 — 빌드가 제대로 되었는지 확인하세요.")
return 2
print("중국어(한자) 누출 0 — 사용자 노출 HTML 이 전부 한국어/비한자입니다.")
return 0
if __name__ == "__main__":
sys.exit(main())