Skip to content

Latest commit

 

History

History
554 lines (450 loc) · 42.9 KB

testing.md

File metadata and controls

554 lines (450 loc) · 42.9 KB

테스트

ctest

ctest --preset <프리셋>이 실행하는 테스트:

이름 내용
unit.<묶음> 단위 테스트 hdtrans_tests를 tests/unit/shards.txt의 묶음별로 나눠 병렬 실행 (아래)
unit.shards 모든 단위 테스트가 한 묶음에 들고, 묶음의 항목마다 테스트가 있는지 확인
tools.selfbase tools/snapshot.sh와 compare.sh를 작은 사전으로 두 번 실행해 결과가 같은지 확인 (Unix)
dicts.ids dicts/의 열 파일마다 키가 다른 두 행이 같은 id를 갖지 않는지 확인. 다른 표기를 모은 words-variants.tsv는 제외 (Unix)
dicts.nn-rows base, words 층의 명사 행 가운데 맨 명사로 읽지 않는 꼴(조사로 끝나는 구, 부사로 옮기는 행)에 문법 코드 Nn이 있거나 tools/nn-allowance.tsv에 올라 있는지, 허용 목록에 필요 없거나 두 번 적힌 줄이 없는지 확인 (tools/nn-rows.py --check, Unix, Python 3)
dicts.classes src/core가 부르는 낱말 부류마다 classes.tsv에 설명 줄과 행이 있고, 표의 부류마다 부르는 코드가 있는지 확인
tools.<이름> tools/의 Python 작업 도구의 --self-test, tools/pick.sh와 tools/check-fix.sh를 작은 저장소에서 돌리는 tests/pick.sh, tests/check-fix.sh (Unix, Python 3)
examples examples/ 번역 확인. HDTRANS_DICTS가 없으면 건너뜀
rules.critical 뜻이 뒤집히는 오역의 사례 tests/rules/critical/<갈래>.tsv. 한 행도 실패하지 않아야 하고 current 행을 받지 않음 (번역 규칙 MNG)
rules.cases.themes tests/rules/cases/<주제>.tsv의 규칙 사례를 dicts/로 번역해 확인 (아래)
rules.cases.speech-<단계> tests/rules/speech-soft/, speech-strict/의 사례를 그 말투 단계로 확인
rules.cases.documents tests/rules/documents/의 입력을 하나씩 문서로 번역해 확인. 줄은 입력과 기대값에 \n으로 씀
rules.drops 규칙 사례와 tests/rules/drops/의 입력에서 낱말이 번역에서 사라지지 않는지 확인 (아래). tools.drops는 도구 자체 시험
sets.drops 측정 세트에서 글이 빠지는 입력 수와 자리를 정하지 못한 가나가 남은 입력 수가 tools/drops-budget.tsv를 넘지 않는지 확인 (아래). 세트 변수가 없으면 건너뜀
rules.variants tests/variants/의 구문 가족마다 바꿔 넣은 문장의 통과율이 그 가족의 하한 이상인지 확인 (아래)
testdata 테스트 데이터의 기대 출력과 비교 (테스트 데이터). HDTRANS_TESTDATA가 없으면 건너뜀
host.admin_priority C API에서 초기화, 다시 읽기, 종료가 대기 중인 번역보다 먼저 처리되는지 확인
host.j2k J2KEngine.dll과 hdTransEngine.dll의 초기화, 종료와 동시 번역 확인 (Windows)
HDTRANS_DICTS=$PWD/dicts ctest --preset mac

단위 테스트

tests/unit/에 있으며, src/core/와 같은 단계별 폴더(dict/, tokenizer/, word/ 등)로 나뉩니다. 프레임워크 없이 tests/unit/hdtest.h의 HD_TEST, HD_CHECK, HD_REQUIRE, HD_SKIP을 씁니다. 실제 입력을 쓰는 표 형식 테스트이며, 픽스처는 tests/unit/**/testdata/의 작은 파일입니다.

build/mac/hdtrans_tests            # 전체
build/mac/hdtrans_tests userdict   # 이름에 userdict가 들어간 테스트만
build/mac/hdtrans_tests --shard tests/unit/shards.txt word    # 묶음 word만
build/mac/hdtrans_tests --check-shards tests/unit/shards.txt  # 묶음이 없는 테스트를 보고

ctest는 단위 테스트를 tests/unit/shards.txt의 줄마다 unit.<이름>으로 따로 실행합니다. 한 줄은 묶음 이름과 항목들이고, 항목은 /로 끝나는 폴더, 소스 파일, 파일:테스트 중 하나입니다. 테스트는 가장 길게 맞는 항목의 묶음에 듭니다. 새 폴더를 만들면 그 폴더를 한 줄에 넣어야 unit.shards가 통과합니다. 이름으로 고를 때는 ctest -R '^unit(\..+)?$'로 묶음 전체를 고릅니다.

러너는 HOME, LOCALAPPDATA, XDG_CACHE_HOME을 임시 폴더로 바꾸고, HDTRANS_CACHE_DIR가 없으면 임시 캐시 폴더를 지정합니다. 실행 중 사용자 캐시 폴더가 바뀌면 실패합니다. tests/runner.sh는 이 러너 동작 자체를 확인하며, Windows가 아닌 빌드에서 ctest tests.runner로 돕니다.

환경 변수 설명
HDTRANS_REQUIRE_ALL=1 건너뛴 테스트가 하나라도 있으면 실패
HDTRANS_TEST_EXCLUDE=a,b 쉼표로 구분한 이름의 테스트를 건너뜀
HDTRANS_CACHE_DIR 테스트가 쓸 캐시 폴더 (절대 경로)

규칙 사례 (tests/rules/cases/)

주제마다 표 하나(tests/rules/cases/<주제>.tsv)로 규칙의 단어 선택을 확인합니다. 열은 rule input expected kind layer이고 입력 한 줄을 한 세션으로 번역합니다(-each -why). 실패는 layer별로 묶어 보입니다.

  • rule: 사례가 겨냥하는 규칙. 지역 규칙은 그 규칙을 이름한 사전 행의 id(dict:jisjk:848), 전역 규칙은 PRIO(G330), 그 밖의 단계는 <단계>:<이름>(sense:お盆, cx:noun-to:10)
  • input: 직접 쓴 짧은 일본어 문장
  • expected: 번역 전체, 또는 rule:dict:<id>=<번역>(그 줄의 어떤 단어를 그 행의 지역 규칙이 그 번역으로 골랐음, -why 기준), !rule:dict:<id>(그 규칙이 아무것도 고르지 않음), has:<말>, lacks:<말>(출력에 그 말이 있음, 없음)
  • kind: fix(고친 규칙이 이제 맞게 고르는 문장), keep(고치기 전 선택이 맞아서 그대로 남아야 하는 문장), current(지금 동작을 그대로 적어 둔 문장)
  • layer: 그 규칙이 도는 실행 층(pipeline-layers.md의 local, local-ending, global-stem, global-last, global-ending, senses 등)

머리줄 앞의 # 줄은 행이 아닙니다. 표가 지키게 하는 번역 규칙의 id를 # 규칙: NUM-1, NUM-3처럼 적습니다.

tools/rule-cases.sh build/mac/hdtrans dicts                                  # 다르면 규칙, 입력, 기대값, 실제값을 보이고 실패
tools/rule-cases.sh build/mac/hdtrans dicts tests/rules/cases/<주제>.tsv --accept   # 그 표 current 행의 번역 전체 기대값을 새 출력으로 바꿈

규칙을 고칠 때는 fix 행과 keep 행을 하나 이상씩 더하고, 커밋 전에 표 전체와 examples/를 돌립니다.

구문 변형 (tests/variants/)

규칙 사례가 고친 문장 하나를 보는 것과 달리, 변형 표는 같은 구문에 다른 낱말을 넣은 문장들로 구문 가족이 얼마나 넓게 맞는지 봅니다. tools/variants.py가 표의 틀에 사전의 낱말(사람 명사, 동사, 형용사)을 넣어 가족마다 문장을 만들고(기본 20개, 씨앗이 정해져 있어 늘 같음), 번역해서 출력이 그 구문의 한국어 모양(정규식)에 맞는지 셉니다.

family	causer-ni	GRM-1	90
slot	P	person	母 父 先生
slot	VP	vp	本を+読む 荷物を+持つ
case	{P}に{VP:cp-ta}。	^{P.ko} 때문에 억지로 	당했|되었
  • slot 이름 종류 낱말…: 종류는 person, noun, verb, vp(목적어+동사), adj, list. 사전에서 그 종류가 아닌 낱말은 경고하고 뺍니다.
  • case 틀 기대 [거부]: {이름:형}은 동사, 형용사의 활용형(te, ta, cp-ta, teireba, chatta …), 기대와 거부의 {이름.ko}는 그 명사의 사전 번역 중 하나입니다.
  • family의 넷째 칸은 그 가족의 통과율 하한(%)이고, ctest rules.variants가 --check로 확인합니다. 하한은 잰 통과율보다 한두 문장 낮게 둡니다(90%를 넘으면 90, 90%이면 85).
tools/variants.py build/mac/hdtrans --fails                                   # 가족별 통과율과 틀린 줄
tools/variants.py build/mac/hdtrans --base "$BASE/build/mac/hdtrans" "$BASE/dicts"   # 기준 → 지금

예제 문서 (examples/)

examples/에는 짧은 일본어 문서와 번역 결과가 있습니다.

  • <case>.ja.txt: 첫 줄은 사례를 설명하는 # 줄, 그다음은 한 문서로 번역할 입력
  • <case>.ko.txt: 같은 # 줄, 그다음은 검토를 마친 현재 엔진 출력
tools/examples.sh build/mac/hdtrans dicts            # *.ko.txt와 다르면 실패
tools/examples.sh build/mac/hdtrans dicts --accept   # 새 출력을 *.ko.txt로 받아들임

출력은 build/examples/에 생깁니다. 사전 인자가 비어 있으면 77로 끝나며, ctest는 이를 건너뜀으로 처리합니다.

표면 문자열 보고 (surface-budget.py)

tools/surface-budget.py는 src/core의 .cpp, .h마다 엔진 코드에 직접 쓴 일본어와 한국어 문자열, 낱말 목록, 표면형 비교 조건을 세어 합계와 많은 파일을 보이는 선택 보고입니다. 검사가 아니어서 CI와 ctest는 돌리지 않고, 릴리스 검토에서 구조를 바꾼 뒤 수가 줄었는지 볼 때 씁니다. 열의 뜻은 도구 첫머리에 있습니다. 고칠 때는 C++ 문자열보다 사전 행과 부류를 먼저 씁니다(docs/fixing-translations.md).

낱말 빠짐 (drops.py)

tools/drops.py는 단어 단계가 한 글자씩 읽고 번역을 비운 글자(가나, 한자)가 둘 이상 이어져 번역에서 사라진 입력을 찾습니다. 그 글자를 홀로 번역한 것이 줄의 번역에 있으면 뒤 단계가 쓴 것이라 빠짐으로 치지 않습니다.

  • ctest rules.drops는 규칙 사례와 tests/rules/drops/의 입력을 보고, known-gaps.txt의 입력만 봐줍니다. 고친 입력은 그 목록에서 지웁니다.
  • --sets는 measure.sh의 네 세트(변수가 있는 것만)를 보고 세트마다 입력 줄 수, 글이 빠지는 서로 다른 입력 수, 자리를 정하지 못한 가나가 남은 서로 다른 입력 수를 씁니다. 자리를 정하지 못한 가나는 단어 단계가 사전 낱말 없이 한 글자씩 만들어 비운 가나 항목으로, 홀로 소리가 없는 가나(spec MNG-5)는 빼고, 뒤 단계가 들리는 대로 쓴 것도 셉니다(--trace로 찾음). 문서도 measure.sh와 달리 줄마다 따로(-each) 번역합니다. ctest sets.drops는 두 수가 tools/drops-budget.tsv의 행(drops, unplaced)을 넘지 않으면 통과합니다. 수를 늘린 변경은 고칩니다. 줄인 변경은 통과하고 행을 낮출 수 있다는 경고를 남기며, 행은 릴리스 때 --update로 낮춥니다. 입력 수가 행과 다르거나 행이 없는 세트는 비교하지 않고 경고만 남깁니다(GitHub Actions에서는 주석). 데이터가 바뀐 것이므로 그 데이터로 --update해 행을 다시 씁니다. CI의 linux 작업은 테스트 데이터를 받은 뒤 문서 세트만 이 검사로 봅니다(웹 예제는 테스트 데이터에 없습니다).
tools/drops.py build/mac/hdtrans dicts --sets --budget tools/drops-budget.tsv [--update]

패스 실험 빌드

단어 단계의 문장 패스(src/core/word/flush.cpp)가 순서에 얼마나 민감한지, 테스트가 패스를 얼마나 붙잡는지 재는 빌드입니다. CMake 옵션 HDTRANS_PASS_EXPERIMENTS(기본 꺼짐)를 켠 빌드만 아래 두 변수를 읽고, 릴리스와 CI 빌드에는 이 코드가 들어가지 않습니다. 켠 빌드도 변수가 없으면 출력이 같습니다.

cmake --preset mac -B build/mac-passexp -DHDTRANS_PASS_EXPERIMENTS=ON
cmake --build build/mac-passexp --parallel --target hdtrans_cli
HDTRANS_PASS_SKIP=QuoteTo build/mac-passexp/hdtrans -dicts dicts -in in.txt  # 그 패스를 빼고 번역
HDTRANS_PASS_SKIP=cx:noun-to:20 build/mac-passexp/hdtrans -dicts dicts -in in.txt  # 구문 표의 행 하나를 빼고 번역
HDTRANS_PASS_SHUFFLE=7 build/mac-passexp/hdtrans -dicts dicts -in in.txt     # 씨앗 7로 패스 순서를 섞어 번역
  • HDTRANS_PASS_SKIP: 뺄 패스 이름, 쉼표로 구분하고 앞뒤 공백은 무시. 이름은 flush.cpp의 Run 호출과 kTextPasses에 있고, 구문 표의 가족 이름이나 행 id(cx:return-to:10)도 됩니다. 모르는 이름은 표준 오류에 알립니다.
  • HDTRANS_PASS_SHUFFLE: 10진 씨앗. kTextPasses(Select부터 EmphaticMo까지)의 순서만 섞고(구문 표의 가족은 자기 패스를 따라감), 그 앞뒤 패스는 그대로 둡니다. 같은 씨앗은 같은 순서입니다. 10진수가 아니면 표준 오류에 알리고 멈춥니다(abort).
  • 두 변수는 프로세스에서 처음 번역할 때 한 번 읽습니다.

tools/pass-mutation.py [-c CLI] [-d DICTS] [-j JOBS] [-t SECONDS] [PASS...]는 이 빌드(기본 build/<프리셋>-passexp/hdtrans, 사전 기본 dicts/)로 패스를 하나씩 빼고 규칙 사례 세 묶음 (cases/, speech-soft/, speech-strict/)과 examples/를 돌려, 패스마다 묶음별 실패 수와 어느 테스트도 잡지 못한 패스를 보입니다. 구문 표의 행도 하나씩 뺍니다. 실행마다 시간 제한(기본 900초)이 있고, CLI가 죽거나(crash) 시간을 넘긴 (timeout) 묶음은 그 패스를 잡은 것으로 칩니다. 기본으로 꺼진 개선 옵션(src/core/word/analyze.h)만 돌리는 패스(FilterMisreads)는 not exercised로 따로 보입니다.

한 입력 살펴보기: 단계 기록 (-stages)

printf '%s' '学生です。' | build/mac/hdtrans -dicts dicts -stages stages

입력 하나가 각 파이프라인 단계를 지나며 남긴 기록을 stages/input.<TAG>.got.tsv에 씁니다. #LINE 머리줄 아래에 이스케이프된 TSV 레코드가 있습니다. HDTRANS_DRIVER_DUMP=stages로도 켤 수 있습니다. -raw를 붙이면 단어 파이프라인만, -each를 붙이면 줄마다 새 세션으로 기록합니다. -why는 단어 선택과 shadow 점수를 표준 오류로 씁니다(api.md).

오독 필터의 기록은 문장의 선택이 끝난 뒤(S와 같은 시점)에 남습니다. MI는 체인의 단어마다 위치, 표면형, 앞뒤 글자, 줄 첫머리 여부, 선택과 출처, MC는 단어의 후보(W)와 어미 후보(E)마다 품사, 의미, 문법 코드, 사용 가능 여부, 한국어와 사전형, MS는 소리 읽기, MF는 결정의 후보마다 내장 모델 점수와 특징입니다. tools/misread/check.py는 MI, MC, MS로 같은 특징을 다시 만들어 MF와 비교합니다.

자체 기준선

tools/snapshot.sh <사전 폴더> <이름>                      # 빌드한 CLI의 출력을 기록
tools/compare.sh <runA> <runB>                            # 두 기록의 차이
tools/explain.sh <binA> <binB> <사전 폴더> '学生です。'   # 처음 달라지는 단계와 레코드
  • snapshot.sh는 빌드하지 않고, 이미 빌드한 CLI를 기본 번역 설정과 기록마다 새 캐시로 실행합니다. 같은 이름의 기록이 있으면 거부합니다.
  • 기록은 ${HDTRANS_RUNS:-../hdtrans-data/cpp-runs} 아래의 바꾸지 않는 폴더입니다. 입력과 출력의 복사본, 커밋, 바이너리, 사전의 SHA-256, 세트별 시간(sets.tsv)이 들어 있습니다.
  • 입력: HDTRANS_DATA(기본 ../hdtrans-data), HDTRANS_INPUTS(기본 $HDTRANS_DATA/hdtrans-golden, 최상위 .in 파일만). *.raw.in은 -raw, *.each.in은 -each, 나머지는 문서로 번역합니다. lyrics/cases.tsv의 가사는 곡별로 묶이고, syosetu/*.ja.txt는 문서로 번역합니다.
  • HDTRANS_BIN: 사용할 CLI (기본 build/mac/hdtrans).
  • compare.sh는 기록 이름이나 폴더 경로를 받아 세트, 줄, 입력, A, B와 세트별, 전체 개수를 출력합니다.
  • compare.sh와 explain.sh의 종료 코드: 0 같음, 1 다름, 2 잘못된 입력.

테스트 데이터

엔드투엔드 테스트의 입력과 기대 출력은 이 저장소 밖에 두고 HDTRANS_TESTDATA로 찾습니다. 데이터가 없으면 아래 테스트는 건너뜁니다.

  • 출처마다 폴더 하나(syosetu/, 앞으로 corpus/, lyrics/, eroge/ 등)이고, 하위 폴더는 없습니다.
  • <set>.<NNN>.ja.txt: 첫 줄은 작품, 작가나 권리자, 원문 URL을 적은 # 줄, 그다음은 입력
  • <set>.<NNN>.ko.txt: 같은 # 줄, 그다음은 입력 전체를 한 문서로 기본 설정으로 번역한 엔진 출력
export HDTRANS_TESTDATA=/path/to/testdata                            # 테스트 데이터 (절대 경로)
tools/e2e.sh --expected "$HDTRANS_TESTDATA" build/mac/hdtrans dicts            # 기대 출력과 비교
tools/e2e.sh --expected "$HDTRANS_TESTDATA" build/mac/hdtrans dicts --accept   # 새 출력을 받아들임
tools/e2e.sh <cliA|runA> build/mac/hdtrans dicts                    # 두 빌드 비교
ctest --preset mac -R testdata                                       # --expected를 ctest로
  • e2e.sh --expected DATA CLI DICTS [--accept]: DATA/*/*.ja.txt를 첫 줄을 빼고 한 문서로 번역해 build/e2e/<폴더>/<name>.ko.txt에 쓰고, DATA의 .ko.txt(A)와 비교합니다. 달라진 문서마다 최대 20줄, 전체 최대 200줄의 변경(파일 줄 번호, A, B)을 출력하고, 마지막 줄에 문서, 줄 개수를 씁니다. .ko.txt가 없는 문서는 모든 줄이 바뀐 것으로 셉니다. --accept는 달라진 .ko.txt를 새 출력으로 덮어씁니다.
  • e2e.sh A B DICTS [DATA]: 같은 문서를 두 빌드로 번역해 build/e2e/<폴더>/(B)와 build/e2e/base/<폴더>/(A)에 쓰고 비교합니다. A에는 CLI 대신 snapshot.sh 기록 폴더를 줄 수 있으며, 그러면 입력이 같은 문서 세트를 A로 씁니다. HDTRANS_DICTS_A는 A에 다른 사전 폴더를 줍니다.
  • DATA가 비었거나 없으면 HDTRANS_TESTDATA를 씁니다. 둘 다 없으면 77로 끝나며, ctest의 testdata는 이를 건너뜀으로 처리합니다. testdata는 저장소의 dicts/를 씁니다.
  • 종료 코드: 0 같음 또는 받아들임, 1 바뀜, 2 잘못된 인자, 데이터, CLI 실패, 비교한 문서 없음, 77 테스트 데이터 없음.

번역이 바뀌었을 때

testdata는 main의 출력을 기대값으로 삼으므로, 작업 브랜치에서는 기대값과 다를 수 있습니다. 내 변경의 영향은 기대값이 아니라 고정한 기준 빌드와 비교해 봅니다(변경 판정). build/e2e/의 출력은 저장소에 커밋되지 않지만 비공개 본문의 번역이므로 다 쓰면 지웁니다.

번역 동작을 바꾸는 변경은 이유를 적은 별도 커밋으로 만듭니다. 그 커밋이 main에 들어가면, 그 커밋으로 빌드한 CLI와 dicts/로 --accept를 실행하고, 테스트 데이터 저장소에 엔진 커밋을 적어 커밋합니다.

챕터 더하기 (syosetu)

printf '# <제목> <화> (작가: <작가>) <URL>\n' > "$HDTRANS_TESTDATA/syosetu/syosetu.052.ja.txt"
tools/fetch-syosetu.sh                  # 첫 줄만 있는 파일에 본문을 받음
tools/e2e.sh --expected "$HDTRANS_TESTDATA" build/mac/hdtrans dicts --accept

fetch-syosetu.sh [DIR](기본 $HDTRANS_TESTDATA/syosetu)는 첫 줄만 있는 .ja.txt마다 그 줄 끝의 小説家になろう URL에서 챕터를 받아, 챕터 제목과 단락마다 한 줄(루비 읽기 제거)을 덧붙입니다. 1초에 한 번 요청하고, 받지 못한 챕터는 보고하고 그대로 둡니다. 작품 제목과 작가는 小説家になろう API (https://api.syosetu.com/novelapi/api/?out=json&of=t-w&ncode=<ncode>)의 title, writer입니다.

CI

Build(.github/workflows/build.yml)의 linux job이 방금 빌드한 CLI로 --expected 비교를 합니다. gate가 matrix를 건너뛴 run(이미 테스트한 트리의 main push 등)에는 이 비교도 없습니다(build.md). 테스트 데이터는 Actions 비밀 HDTRANS_TESTDATA_KEY(테스트 데이터 저장소의 읽기 전용 배포 키)로 받습니다. 비밀을 쓸 수 없는 실행(포크의 풀 리퀘스트)은 알림만 남기고 건너뜁니다. 작업 요약에는 개수만, 로그에는 보고의 처음 몇 줄만 남깁니다. 종료 코드 1(바뀜)은 실패가 아니고 2만 실패입니다. 이어서 같은 데이터의 문서 세트로 낱말 빠짐 예산(drops.py --sets --budget)을 보고, 빠지는 입력 수가 예산보다 많으면 실패합니다. 문서가 늘거나 줄어 입력 수가 행과 다르면 경고만 남깁니다. ctest sets.drops는 테스트 데이터를 받기 전에 돌아 CI에서는 건너뜁니다.

변경 판정 (e2e + 모델 판정)

챕터 본문은 위 비공개 테스트 데이터 저장소에 있습니다. 본문, 번역, 판정 파일은 이 저장소에 커밋하지 않습니다. 아래 도구는 Python 3 표준 라이브러리만 씁니다.

  1. 비교할 커밋을 먼저 고정하고 별도 git worktree에서 base를 빌드합니다. 움직이는 main과 비교하지 않습니다. CLI뿐 아니라 사전도 그 커밋의 것을 씁니다. 아래 명령은 저장소 루트에서 시작합니다.

    BASE_REF=$(git rev-parse HEAD)  # 비교 기준 커밋 해시로 바꿔도 됨
    BASE="$(dirname "$PWD")/hdtrans-base"  # 아직 없는 형제 폴더
    git worktree add --detach "$BASE" "$BASE_REF"
    (cd "$BASE" && export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer &&
      cmake --preset mac && cmake --build build/mac)
    export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
    cmake --preset mac
    cmake --build build/mac
  2. e2e-pairs.py에 base CLI, 사전, head CLI, 사전, 출력 파일, 입력 폴더들을 차례로 줍니다. RUN은 저장소 밖의 결과 폴더입니다. 비공개 저장소의 syosetu/가 개발 세트(고칠 때 보는 챕터), heldout/이 held-out 세트(고칠 때 보지 않는 챕터, 다른 작품)입니다: DEV=$HDTRANS_TESTDATA/syosetu, HELDOUT=$HDTRANS_TESTDATA/heldout. 바뀐 줄이 없으면 판정 출력은 빈 파일이고 다음 단계도 대상 없이 끝납니다.

    mkdir -p "$RUN"
    python3 tools/e2e-pairs.py "$BASE/build/mac/hdtrans" "$BASE/dicts" \
      build/mac/hdtrans dicts "$RUN/dev.pairs.tsv" "$DEV"
    python3 tools/e2e-pairs.py "$BASE/build/mac/hdtrans" "$BASE/dicts" \
      build/mac/hdtrans dicts "$RUN/heldout.pairs.tsv" "$HELDOUT"
    • 각 폴더의 .txt를 재귀적으로 찾되 .ko.txt는 제외합니다. 첫 줄이 #로 시작하면 빼고 챕터 전체를 한 문서로 번역합니다. CLI를 부를 때마다 새 임시 HOME, 캐시를 쓰므로 사용자 캐시나 base/head 캐시를 공유하지 않습니다.
    • 출력은 머리줄 없는 key ja old new TSV입니다. key는 입력폴더이름/상대파일명:원본줄번호이며 줄 번호는 # 머리줄도 포함한 1부터 셉니다. 겹치지 않는 폴더 이름을 씁니다. 바뀐 줄만 기록합니다.
    • 모든 도구의 TSV는 탭, 따옴표가 든 필드를 CSV 방식으로 인용합니다. 직접 파싱할 때도 TSV용 CSV 리더를 씁니다.
  3. 판정은 바뀐 줄을 만든 쪽이 아닌 판정자가, 어느 쪽이 새 출력인지 모르고 합니다. judge.py는 줄마다 X/Y 순서를 섞어 묻습니다. Sonnet(claude-sonnet-5-5)으로 모든 변경 줄을 판정합니다.

  4. Sonnet이 worse 또는 same으로 본 줄만 gpt-6.1-sol의 high effort로 다시 판정합니다.

    python3 tools/judge.py claude-sonnet-5-5 "$RUN/dev.sonnet.tsv" "$RUN/dev.pairs.tsv"
    awk -F '\t' '$2 == "worse" || $2 == "same" {print $1}' \
      "$RUN/dev.sonnet.tsv" > "$RUN/dev.rejudge.keys.txt"
    python3 tools/judge.py gpt-6.1-sol "$RUN/dev.gpt.tsv" "$RUN/dev.pairs.tsv" \
      --only "$RUN/dev.rejudge.keys.txt" --effort high
    • judge.py MODEL OUT.tsv PAIRS.tsv [--only KEYS.txt] [--effort low|high] [--grade] (--grade는 오류율 감사). --only는 키를 한 줄에 하나씩 적은 파일입니다. 출력은 머리줄 없는 key verdict model이며 verdict는 improved, same, worse입니다. X/Y 순서를 섞어 판정한 뒤 old/new 기준으로 돌립니다.

    • 출력에 같은 모델로 판정한 키가 있으면 건너뛰고 이어 씁니다. 입력이나 판정 설정을 바꾸면 새 출력 파일을 씁니다. effort는 --effort → JUDGE_EFFORT → low 순입니다.

    • ANTHROPIC_BASE_URL(기본 https://api.anthropic.com)과 ANTHROPIC_AUTH_TOKEN(Bearer) 또는 ANTHROPIC_API_KEY(x-api-key)를 환경에서 읽습니다. 둘 다 있으면 Bearer를 씁니다. 키, 토큰을 파일에 넣지 않습니다. GPT 모델은 해당 모델을 제공하는 Anthropic Messages 호환 게이트웨이가 필요합니다. 그런 게이트웨이나 API 키가 없으면 아래 블라인드 판정으로 합니다.

    • 줄이 많으면 표본을 뽑지 않고 판정 입력을 나눠(split -l 500 PAIRS.tsv part.) 조각마다 판정을 동시에 돌리고 출력을 이어 붙입니다. 직접 판정한 결과도 같은 열(key verdict judge, 넷째 열에 메모)로 씁니다.

    블라인드 판정 (blind.py). API 없이 판정할 때 씁니다. 바뀐 줄을 만들지 않은 판정자 둘이 따로 블라인드 파일만 보고 답하고, 답이 갈린 줄은 셋째 판정자가 봅니다. 판정자마다 run 폴더 밖의 새 폴더에 파일을 주고, 열쇠 파일, 판정 입력, run 폴더, 다른 판정자의 답, 저장소를 보거나 엔진을 돌리지 않게 합니다.

    J1=$(mktemp -d) J2=$(mktemp -d) J3=$(mktemp -d)
    python3 tools/blind.py make "$RUN/dev.pairs.tsv" "$RUN/dev.key.tsv" "$J1/blind.tsv"
    cp "$J1/blind.tsv" "$J2/"
    # 판정자 1은 $J1/blind.tsv를 읽고 $J1/answers.tsv에, 판정자 2는 $J2에서 같은 일을 함
    python3 tools/blind.py disagree "$J1/blind.tsv" "$J1/answers.tsv" "$J2/answers.tsv" "$J3/blind.tsv"
    # $J3/blind.tsv가 비어 있지 않으면 판정자 3이 읽고 $J3/answers.tsv를 씀
    [ -s "$J3/blind.tsv" ] || : > "$J3/answers.tsv"
    cp "$J1/answers.tsv" "$RUN/dev.answers-1.tsv"; cp "$J2/answers.tsv" "$RUN/dev.answers-2.tsv"
    cp "$J3/answers.tsv" "$RUN/dev.answers-3.tsv"
    python3 tools/blind.py final "$RUN/dev.key.tsv" "$RUN"/dev.answers-[123].tsv > "$RUN/dev.final.tsv"
    • 블라인드 파일은 id ja X Y입니다. id는 줄 번호일 뿐이고 X, Y는 줄마다 무작위로 바꾼 이전, 이후 출력입니다. 답은 id 답 [메모]이고 답은 더 나은 쪽 X, Y, 또는 same입니다.
    • make는 이미 있는 열쇠 파일이나 블라인드 파일을 덮어쓰지 않습니다. 다시 만들면 답도 새로 받습니다.
    • final은 두 답이 같으면 그 답, 다르면 셋째가 고른 쪽, 셋 다 다르면 same을 key verdict judges [메모]로 씁니다. 그래서 worse는 두 판정자가 나빠졌다고 본 줄입니다. 답이 모자란 줄은 빠지고 5에서 판정 안 됨으로 셉니다.
  5. 개수를 셉니다: python3 tools/verdicts.py "$RUN/dev.pairs.tsv" "$RUN/dev.sonnet.tsv" "$RUN/dev.gpt.tsv"는 바뀐 줄, 나아짐, 같음, 나빠짐과 두 모델이 모두 worse로 판정한 키를 출력합니다. 블라인드 판정은 최종 파일을 두 번 줍니다: verdicts.py "$RUN/dev.pairs.tsv" "$RUN/dev.final.tsv" "$RUN/dev.final.tsv". 그 worse가 모두 두 판정자가 나빠졌다고 본 줄이라 both-worse로도 나옵니다. 그런 줄이 있으면 원인 변경을 되돌리거나 조건을 좁힙니다. 의견이 갈린 줄과 정상 줄의 변화도 직접 검토합니다. 나아짐:나빠짐이 2:1 이상이고 한 부류의 줄이 몰려 나빠지지 않을 때 남깁니다.

  6. held-out에도 3~5를 별도로 실행하고, 개발 세트와 held-out의 변경 줄, 개선, 동률, 악화 및 쌍방 악화 수를 분리 보고합니다. held-out을 보고 규칙을 맞췄으면 더는 held-out이 아니므로 새 평가 세트가 필요합니다.

  7. tools/measure.sh는 변수가 있는 세트를 한꺼번에 번역하고 같은 형식의 판정 입력을 쓰므로 3~5를 똑같이 합니다. 기준 run도 같은 변수로 잽니다. 참조 샘플의 chrF는 기준보다 낮지 않아야 합니다.

    세트 변수 번역 판정 입력, 키
    줄 단위 회귀 문장 HDTRANS_CORPUS 줄마다 따로(--each) corpus.pairs.tsv, corpus:줄
    참조 샘플 HDTRANS_REF_SAMPLE 줄마다 따로 sample.pairs.tsv, sample:줄
    테스트 데이터 문서 HDTRANS_TESTDATA e2e.sh처럼 # 머리줄을 빼고 한 문서로, 출력에도 머리줄이 없음 docs.pairs.tsv, docs:폴더/이름:줄
    웹 예제 HDTRANS_EXAMPLES web/examples.tsv에 이름과 함께 적힌 파일을 web/examples/나 이 폴더에서 찾아 한 문서로 examples.pairs.tsv, examples:파일:줄

    줄 번호는 입력 파일에서 셉니다. 네 세트는 동시에 돌고, 문서는 HDTRANS_JOBS(기본 CPU 수)개씩 번역합니다. docs: 키의 폴더로 개발 세트와 held-out을 나눠 셉니다.

나쁜 구간을 고른 층 찾기 (attribute.py)

쌍 비교 판정은 구간을 주지 않습니다. 원인을 좁힐 줄을 골라 별도 구간 판정 TSV를 준비합니다. 열은 line ja out verdict cause span fix이고 머리줄은 선택입니다(line으로 시작). verdict=ok가 아닌 행을 분석합니다. line은 원본 파일의 줄 번호, span은 일본어의 잘못 해석한 구간입니다.

python3 tools/attribute.py --verdicts "$RUN/spans/sample.verdicts.tsv" \
  --chapters "$DEV/sample.ja.txt" --cli build/mac/hdtrans --dicts dicts \
  --output "$RUN/owners.tsv"

--verdicts와 --chapters는 여러 파일을 받으며, sample.verdicts.tsv와 sample.ja.txt처럼 이름으로 짝짓습니다. 현재 CLI로 챕터 전체를 -why 번역하고, 판정 당시와 원문, 번역이 같은 줄만 분석합니다. 출력은 머리줄 있는 chapter line cause span fix ja out owners입니다. owners는 겹친 단어의 층:규칙:표기:한국어를 |로 이으며, 어미 출처가 있으면 +end=층:규칙을 덧붙입니다. 같은 구간이 반복되면 모두 찾습니다. 표면형을 원문에서 순서대로 맞추므로 정규화, 합성된 단어는 빠질 수 있고, -why가 기록하지 않는 층은 pipeline-layers.md와 단계 기록으로 확인합니다. 표준 오류에는 분석, 출력 변화, 구간 누락 개수가 나옵니다.

빠른 측정 (check-fix.sh)

고치는 동안 되풀이하는 측정을 줄입니다. PR 전의 판정은 위 변경 판정대로 합니다.

export HDTRANS_SETS="corpus=$HDTRANS_CORPUS sample=$HDTRANS_REF_SAMPLE/ja.txt corpus-doc=$HDTRANS_CORPUS@blocks=200"
tools/check-fix.sh my-fix          # gate, 기준과 지금의 세트 번역, 바뀐 줄, 판정 묶음
  • 기준 커밋(기본 develop과의 merge base)은 $HDTRANS_WORK/bases/<커밋>/에 한 번만 빌드하고, 그 세트 번역은 $HDTRANS_WORK/sets-cache/에 둡니다. 같은 기준에서 고치는 다음 수정은 자기 쪽만 번역합니다.
  • dicts/만 바뀌었으면 기준 CLI에 이 트리의 사전을 주므로 빌드하지 않습니다. 엔진에 닿는 다른 파일이 바뀌면 빌드하고 모든 줄을 다시 번역합니다.
  • tools/affected.py BASE [HEAD]가 바뀐 행의 키(동사, 형용사는 어간, 한자 키의 가나 표기, 같은 지역 규칙을 부르는 행의 키)를 씁니다. 그 말이 든 줄만 다시 번역하고, 문서와 블록은 그런 줄이 든 것만 다시 번역합니다. 어미 행, end, pat 행, 엔진에 닿는 다른 파일은 *(모든 줄)이고, 말이 200개를 넘으면 모든 줄을 번역하는 편이 빨라 그렇게 합니다.
  • 세트마다 말이 없는 줄 HDTRANS_CANARY(기본 1000)개도 번역해 기준과 다르면 1로 끝납니다. 말 목록이 변경이 닿는 길을 놓친 것입니다. 표본이라 몇 줄만 놓친 것은 지나칠 수 있으므로, PR 전에는 -f로 지금 쪽의 모든 줄을 번역합니다.
  • gate는 세트 번역과 함께 돕니다(-n은 뺌).
  • tools/sets.py run은 줄마다 번역하는 세트의 서로 다른 줄을 한 번씩, CPU 수만큼 나눠 번역합니다. 출력은 한 프로세스로 --each 번역한 것과 바이트까지 같습니다. NAME=FILE@blocks=N은 N줄씩 끊은 문서이며, 한 파일 전체를 한 문서로 번역한 것(@doc)과는 블록 경계의 문맥만큼 다릅니다.
  • tools/judge-packet.py make DIR PAIRS...는 일본어, 이전, 이후가 같은 줄을 하나로 묶고, 바뀐 낱말이 같은 변경끼리 묶어 묶음마다 몇 줄만(--reps, 기본 3) reps.pairs.tsv에 씁니다. 판정한 뒤 expand DIR VERDICTS는 묶음의 판정이 모두 improved나 same으로 같을 때만 나머지 줄에 그 판정을 주고, 나빠짐이나 갈린 묶음은 next.pairs.tsv로 모두 다시 판정하게 합니다.

오류율 감사 (audit.py)

변경 판정은 바뀐 줄만 봅니다. 감사는 릴리스마다 같은 줄을 뽑아 현재 출력에 등급을 매겨, 릴리스끼리 비교할 수 있는 오류율을 냅니다. 결과는 $HDTRANS_WORK/audit/<버전>/에 두고 커밋하지 않습니다.

V=0.4.8                                    # 릴리스 버전, develop이면 develop-<짧은 해시>
W=${HDTRANS_WORK:-build/work}
tools/measure.sh "audit-$V"                # 네 세트의 현재 출력
A=$W/audit/$V; mkdir -p "$A"
python3 tools/audit.py draw "$W/audit-$V" "$A/lines.tsv"
python3 tools/judge.py claude-sonnet-5-5 "$A/grades.tsv" "$A/lines.tsv" --grade
python3 tools/audit.py rate "$A/lines.tsv" "$A/grades.tsv" > "$A/rate.tsv"
  • draw RUN OUT.tsv [--seed S] [--per-set N]: measure.sh run에서 변수가 있는 세트마다 N줄(기본 75, 네 세트면 300줄)을 뽑습니다. 가나나 한자가 있는 줄만, 같은 일본어는 세트에 한 번만 셉니다. 씨앗(기본 1), 세트, 일본어의 해시로 고르므로 같은 씨앗은 어느 run에서나 같은 줄을 뽑고, 세트에 줄이 늘어도 뽑힌 줄은 거의 그대로입니다. 출력은 judge.py 입력 형식(key ja 출력, 넷째 열은 비움)이고 키는 measure.sh와 같습니다.
  • 등급은 correct(그대로 받아들일 번역), minor(뜻은 맞지만 조사, 어미, 띄어쓰기, 낱말, 말투가 눈에 띄게 어긋남), wrong(뜻이 빠지거나 바뀌거나 더해짐, 일본어가 남음, 깨진 한국어)입니다. 줄은 문맥 없이 보며, 어떤 문맥에서 맞을 수 있는 읽기는 오류로 치지 않습니다. judge.py --grade는 둘째 열과 셋째 열만 보고 key 등급 model을 씁니다. 직접 매긴 등급도 같은 열(key grade judge, 넷째 열에 메모)로 씁니다.
  • rate LINES.tsv GRADES.tsv: 세트마다 줄 수, 등급별 개수, wrong %, correct가 아닌 비율을 냅니다. 한 키의 마지막 행이 쓰이고, 등급이 없는 줄이 있으면 1로 끝납니다.
  • 씨앗, 세트 크기, 등급을 매긴 판정이 같은 감사끼리만 비교합니다.

주기 누적 확인 (cycle)

PR마다의 판정은 그 PR이 갈라져 나온 develop과 비교하므로, 같은 주기에 합친 PR끼리의 상호작용은 따로 봐야 합니다.

  • 합치기 전. 번역이 바뀌는 PR은 지금의 develop에 rebase하고, 지금의 develop 머리 빌드의 run을 기준으로 다시 잽니다. 처음 판정한 쌍에 없는 줄만 판정합니다.
  • 합친 뒤. develop에 합칠 때마다 그 커밋을 마지막 릴리스 태그의 run과 네 세트 모두에서 비교하고, run을 $HDTRANS_WORK/cycle/<다음 버전>/에 쌓습니다. 앞선 확인에서 판정한 쌍에 없는 줄만 판정합니다. 릴리스 리뷰는 마지막 확인 뒤에 새로 생긴 줄만 판정하면 됩니다.
W=${HDTRANS_WORK:-build/work}; C=$W/cycle/0.4.9     # 다음 버전
tools/measure.sh -c "$TAG/build/mac/hdtrans" -d "$TAG/dicts" release-v0.4.8   # 태그마다 한 번
H=$(git -C "$DEV" rev-parse --short HEAD)
HDTRANS_WORK=$C tools/measure.sh -c "$DEV/build/mac/hdtrans" -d "$DEV/dicts" -b "$W/release-v0.4.8" "$H"
for s in corpus sample docs examples; do
  [ -f "$C/$H/$s.pairs.tsv" ] || continue
  python3 tools/new-pairs.py "$C/$H/$s.new.tsv" "$C/$H/$s.pairs.tsv" \
    $(find "$C" -name "$s.pairs.tsv" ! -path "$C/$H/*")
done
cat "$C/$H"/*.new.tsv > "$C/$H/new.tsv"
echo "$H" >> "$C/checks.txt"
  • new-pairs.py OUT.tsv NEW.tsv [OLD.tsv...]는 NEW의 행 가운데 키, 이전, 이후 출력이 모두 같은 행이 OLD에 없는 것만 씁니다. 판정한 뒤 이전이나 이후가 바뀐 줄은 다시 새 줄이 됩니다.

  • $DEV는 그 커밋을 고정한 worktree이고 $TAG는 태그의 worktree입니다. $C/$H/new.tsv를 위의 변경 판정 3~5로 판정해 최종 판정을 $C/$H/final.tsv에 둡니다. 새 줄이 없으면 빈 final.tsv를 둡니다.

  • 마지막 릴리스 뒤 누적 개수는 마지막 확인의 쌍마다, 키, 이전, 이후 출력이 같은 줄을 판정한 마지막 확인의 판정으로 셉니다. 한 주기 안에서 바뀌었다가 되돌아간 줄도 지금 출력에 맞는 판정을 씁니다:

    L=$(tail -n 1 "$C/checks.txt")
    cat "$C/$L"/*.pairs.tsv > "$C/total.pairs.tsv"
    python3 tools/new-pairs.py --verdicts "$C/total.tsv" "$C/total.pairs.tsv" \
      $(for H in $(cat "$C/checks.txt"); do echo "$C/$H/new.tsv" "$C/$H/final.tsv"; done)
    python3 tools/verdicts.py "$C/total.pairs.tsv" "$C/total.tsv"

잘못된 출력 찾기 (detect-bad.sh)

tools/detect-bad.sh build/e2e/*/*.ko.txt    # 걸린 곳마다 한 줄: 파일:줄, 패턴, 짧은 구간
tools/detect-bad.sh -c build/e2e/*/*.ko.txt # 패턴별 개수만

한국어 출력에서 올바른 번역에는 나올 수 없는 연쇄를 찾습니다. 첫 줄이 #로 시작하면 건너뜁니다(e2e, 예제 출력의 # 줄). 구간은 걸린 곳 앞뒤 몇 글자만 보여 줍니다. 종료 코드: 0 없음, 1 있음, 2 잘못된 인자.

패턴 찾는 것
ending-honorific 종결 어미 뒤에 붙은 군, 씨, 님, 짱 (했다님, 있어요씨)
doubled-ending 종결 어미가 두 번 (그렇네요군요, 합니다입니다)
stem-eung 동사 어간 바로 뒤의 감탄사 응 (하응, 않응)
particle-after-punct , . ! ? … ~ 뒤에 홀로 선 조사 (! 와, , 는). 가 주(가다)와 와?(오다)는 뺌
residue 출력에 남은 일본어 문자나 엔진 표지 (승용き, &AV@, (이)가)
counter-no-number 수 없이 홀로 선 서수 째, 수가 아닌 말 뒤에 띄어 쓴 원 (괴물 째, 이었던 원이군요). 3번째, 한 번 째, 만 원은 뺌

맞는 출력도 걸릴 수 있는 검사(정확도 100%가 아님)이므로, 걸린 곳을 직접 확인한 뒤 고칩니다. 확인한 결과 모두 실제 오류인 패턴만 엔진 규칙(src/core/word/constraints.cpp의 제약 규칙)으로 옮깁니다.

원문과 함께 보기 (--source)

tools/detect-bad.sh --source build/e2e/syosetu.038.ja.txt build/e2e/syosetu.038.ko.txt   # 원문, 번역 쌍
tools/detect-bad.sh -c --source A.ja.txt A.ko.txt B.ja.txt B.ko.txt                       # 패턴별 개수만

한국어만 보면 멀쩡해 보이는 오역(かッハ……! → 인가 하……!)을 원문과 맞대어 찾습니다. 줄 번호가 맞는 일본어, 한국어 파일 쌍을 받고, 각 줄을 같은 문장 부호(「」『』()、。!?…‥―)에서 잘라 조각 수가 같을 때만 조각끼리 비교합니다. 첫 줄이 #이면 건너뜁니다. 종료 코드는 위와 같습니다.

패턴 찾는 것
sound-function 소리로 읽히는 일본어 조각(가나 2~5자, ー, ~ 포함, 첫 글자가 아닌 っ, ッ가 있음, を로 시작하지 않음)이 기능어, 조사 낱말이 든 한국어가 됨 (かッハ → 인가 하, だっは → 다 는). 다음 조각의 첫 음절 하나로 쓴 말더듬(こっ、この → 이, 이)은 뺌
stutter-mismatch 말더듬(가나 1~2자, 、나 … 뒤의 조각이 앞 조각으로 시작하고 늘인 소리가 아님: ご、ごめ、ごめんなさい)의 한국어 조각이 마지막 조각의 한국어로 시작하지 않음 (째, 째, 미안해요, 그래서, 그렇지만). 마지막 음절은 받침 없이 비교하므로 가, 감사합니다는 뺌

환경 변수 요약

변수 쓰는 곳 설명
HDTRANS_DICTS ctest examples 예제 번역에 쓸 사전 폴더
HDTRANS_REQUIRE_ALL 단위 테스트 1이면 건너뛴 테스트를 실패로
HDTRANS_TEST_EXCLUDE 단위 테스트 건너뛸 테스트 이름 (쉼표 구분)
HDTRANS_DATA, HDTRANS_INPUTS, HDTRANS_RUNS, HDTRANS_BIN snapshot.sh, compare.sh 입력, 기록 위치와 CLI
HDTRANS_TESTDATA e2e.sh, fetch-syosetu.sh, measure.sh, audit.py, drops.py, ctest testdata, sets.drops 테스트 데이터 폴더
HDTRANS_EXAMPLES measure.sh, audit.py, drops.py web/examples/에 없는 웹 예제 텍스트 폴더 (웹 빌드의 HDTRANS_WEB_EXAMPLES와 같은 폴더, 비공개)
HDTRANS_JOBS measure.sh, gate.sh 한꺼번에 번역할 문서 수, 동시에 돌릴 ctest 테스트 수 (기본 CPU 수)
HDTRANS_SETS, HDTRANS_CANARY check-fix.sh sets.py의 세트 목록(공백 구분), 세트마다 확인할 말 없는 줄 수
HDTRANS_DICTS_A e2e.sh A 쪽 사전
HDTRANS_DRIVER_DUMP CLI -stages와 같음
HDTRANS_CACHE_DIR, HDTRANS_SNAPSHOT, HDTRANS_VERIFY_DICTS 엔진 사전 캐시 (dictionary-files.md)
HDTRANS_WORK gate.sh, measure.sh, check-fix.sh 작업 출력 폴더 (기본 build/work)
HDTRANS_CORPUS, HDTRANS_REF_SAMPLE measure.sh, audit.py, count-class.py, drops.py 줄 단위 회귀 문장 파일, ja.txt와 줄이 맞는 사람 번역 ref.txt가 든 폴더 (비공개)
HDTRANS_PRESET gate.sh, measure.sh, regen-rules.sh, shadow.py 프리셋 (기본 macOS mac, 그 밖 linux)
HDTRANS_TIMEOUT gate.sh, measure.sh, pick.sh 오래 걸릴 수 있는 단계의 시간 제한(초, 기본 1800)
HDTRANS_PASS_SKIP, HDTRANS_PASS_SHUFFLE HDTRANS_PASS_EXPERIMENTS 빌드의 엔진 뺄 문장 패스, 패스 순서를 섞는 씨앗 (패스 실험 빌드)
HDTRANS_EXAMPLES_OUT examples.sh 출력 폴더 (기본 build/examples)
HDTRANS_HYGIENE_PATTERNS hygiene.py 커밋하면 안 되는 말의 정규식 파일 (저장소 밖)

작업 절차와 그 도구(gate.sh, measure.sh, pick.sh 등)는 skills/README.md에 있습니다.