From 8b9e8fbde42b9d2f8c16b22429794fd37438ac17 Mon Sep 17 00:00:00 2001 From: YONGBEOM GWON Date: Tue, 25 Aug 2026 13:56:55 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(tooling):=20annotate=5Fcorpus=20--corp?= =?UTF-8?q?us=20=EB=A1=9C=20=EC=BD=94=ED=8D=BC=EC=8A=A4=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=EB=A5=BC=20=EC=9E=AC=EC=B1=84=EC=A0=90=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README 와 결과보고서가 "누구든 같은 명령으로 재현할 수 있다"고 안내하는 명령이 실제로는 없었다. 인자 없이 실행하면 docstring 만 출력하고 exit 2 이고, --seed 는 첫 페어 하나만 재채점한다. n=11 집계(평균 K-ER·Δ·위반 평균)를 내는 모드가 어디에도 없어서, 인용된 수치를 제3자가 확인할 방법이 없었다. summarize_corpus() 를 추가해 코퍼스 전체를 현재 엔진으로 재채점하고 페어별 값과 집계를 함께 낸다. 이 함수가 README·결과보고서·DATASET_CARD 가 인용하는 수치의 유일한 출처다 — 손으로 적지 않고 여기서 옮긴다. 테스트는 집계 자체보다 정직성 불변식을 고정한다: 코퍼스에 기록된 ker_score 가 지금 엔진의 재채점과 일치하는지(어긋나면 손으로 쓰였거나 재주석을 빠뜨린 것), 전 페어가 Fidelity PASS 인지, 보고 Δ 가 평균에서 파생된 값인지. --- README.md | 2 +- tests/tooling/test_annotate_corpus.py | 38 ++++++++++++++++++ tooling/annotate_corpus.py | 55 ++++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 tests/tooling/test_annotate_corpus.py diff --git a/README.md b/README.md index 59030a0..d822ae1 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ render_html() 원문/쉬운본 나란히 HTML + 면책 + 배지 ## 숫자로 보는 또박 (전부 실측·재현 가능) - 공개 합성 코퍼스 11쌍: K-ER 평균 **71.2 → 80.7 (Δ +9.5)**, 규칙 위반 평균 −2.09건, - **전 페어 Fidelity PASS** — 재현: `python -m tooling.annotate_corpus` + **전 페어 Fidelity PASS** — 재현: `python -m tooling.annotate_corpus --corpus` - 테스트 **417개** 통과 · 라이선스/보안 감사 clean (`ttobak audit`) - CI 5중 게이트: 정적분석(ruff) + pytest + 의존성 라이선스 허용목록 + 자산 분리 검사 + 감사 diff --git a/tests/tooling/test_annotate_corpus.py b/tests/tooling/test_annotate_corpus.py new file mode 100644 index 0000000..7d70619 --- /dev/null +++ b/tests/tooling/test_annotate_corpus.py @@ -0,0 +1,38 @@ +"""corpus 전체 재채점 모드 — 보고서·데이터셋 카드의 집계 수치가 나오는 출처. + +README·결과보고서가 "누구든 같은 명령으로 재현할 수 있다"고 말하는 그 명령이다. +집계를 손으로 적지 않고 여기서 도출한다(재현 가능성 = 정직성). +""" +from pathlib import Path + +from tooling.annotate_corpus import summarize_corpus + +CORPUS = Path("corpus/pairs.jsonl") + + +def test_summarize_corpus_covers_every_pair(): + summary = summarize_corpus(CORPUS) + assert summary["n"] == len(CORPUS.read_text(encoding="utf-8").strip().splitlines()) + assert len(summary["pairs"]) == summary["n"] + + +def test_recorded_ker_scores_match_a_live_rescore(): + """코퍼스에 적힌 ker_score 가 지금 엔진이 내는 값과 같아야 한다. + + 어긋나면 주석이 손으로 쓰였거나 규칙이 바뀐 뒤 재주석하지 않은 것이다. + """ + summary = summarize_corpus(CORPUS) + drifted = [p for p in summary["pairs"] if p["recorded"] != p["after"]] + assert not drifted, f"기록값과 재채점이 다른 페어: {drifted}" + + +def test_every_gold_pair_passes_the_fidelity_gate(): + assert summarize_corpus(CORPUS)["all_fidelity_pass"] is True + + +def test_reported_delta_is_derived_not_asserted(): + summary = summarize_corpus(CORPUS) + assert summary["delta"] == round(summary["after_mean"] - summary["before_mean"], 2) + assert summary["violations_delta"] == round( + summary["violations_after_mean"] - summary["violations_before_mean"], 2 + ) diff --git a/tooling/annotate_corpus.py b/tooling/annotate_corpus.py index ebeea83..24d20e4 100644 --- a/tooling/annotate_corpus.py +++ b/tooling/annotate_corpus.py @@ -7,6 +7,7 @@ 사용: python -m tooling.annotate_corpus "" "" # 단일 페어 채점(JSON) python -m tooling.annotate_corpus --seed # corpus/pairs.jsonl 첫 줄 재채점 + python -m tooling.annotate_corpus --corpus # corpus/pairs.jsonl 전체 재채점 + 집계 프로그램적으로: from tooling.annotate_corpus import annotate @@ -17,6 +18,7 @@ import json import sys from datetime import date +from pathlib import Path from ttobak.parse import parse from ttobak.metric import score @@ -78,6 +80,7 @@ def annotate(source_text: str, easy_text: str, ref: date = REF_DATE) -> dict: "pictogram_refs": [p.model_dump() for p in pics], # 진단용(코퍼스 행에는 미수록) "_before_score": round(before.score, 1), + "_before_violation_count": len(before.violations), "_delta": round(after.score - before.score, 1), "_exact_fail_count": fid.exact_fail_count, "_drift_flags": list(getattr(fid, "drift_flags", []) or []), @@ -85,9 +88,59 @@ def annotate(source_text: str, easy_text: str, ref: date = REF_DATE) -> dict: } +def summarize_corpus(path: Path | str = "corpus/pairs.jsonl") -> dict: + """코퍼스 전체를 현재 엔진으로 재채점해 페어별 값과 집계를 낸다. + + README·결과보고서·DATASET_CARD 가 인용하는 집계 수치(n, 평균 K-ER, Δ, 위반 평균)의 + 유일한 출처다. 손으로 적지 않는다 — 이 함수를 돌려 나온 값을 옮긴다. + """ + rows = [ + json.loads(line) + for line in Path(path).read_text(encoding="utf-8").splitlines() + if line.strip() + ] + + pairs = [] + for row in rows: + ann = annotate(row["source_text"], row["easy_text"]) + pairs.append( + { + "pair_id": row.get("pair_id"), + "before": ann["_before_score"], + "after": ann["ker_score"], + "delta": ann["_delta"], + "recorded": row.get("ker_score"), + "violations_before": ann["_before_violation_count"], + "violations_after": len(ann["ker_rule_violations"]), + "fidelity_verdict": ann["fidelity_verdict"], + } + ) + + def _mean(key: str) -> float: + return round(sum(p[key] for p in pairs) / len(pairs), 2) if pairs else 0.0 + + before_mean, after_mean = _mean("before"), _mean("after") + vb_mean, va_mean = _mean("violations_before"), _mean("violations_after") + return { + "n": len(pairs), + "before_mean": before_mean, + "after_mean": after_mean, + "delta": round(after_mean - before_mean, 2), + "violations_before_mean": vb_mean, + "violations_after_mean": va_mean, + "violations_delta": round(va_mean - vb_mean, 2), + "all_fidelity_pass": all(p["fidelity_verdict"] == "PASS" for p in pairs), + "delta_range": [min(p["delta"] for p in pairs), max(p["delta"] for p in pairs)] if pairs else [], + "pairs": pairs, + } + + def _main(argv: list[str]) -> int: + if "--corpus" in argv: + summary = summarize_corpus() + print(json.dumps(summary, ensure_ascii=False, indent=2)) + return 0 if "--seed" in argv: - from pathlib import Path first = Path("corpus/pairs.jsonl").read_text(encoding="utf-8").splitlines()[0] row = json.loads(first) ann = annotate(row["source_text"], row["easy_text"]) From dac2fdb8ba9bdb5bc1b0b95d33afaa437a2b61d6 Mon Sep 17 00:00:00 2001 From: YONGBEOM GWON Date: Tue, 25 Aug 2026 13:59:43 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test(tooling):=20K-ER=20=EC=9E=AC=EC=B1=84?= =?UTF-8?q?=EC=A0=90=20=EB=8C=80=EC=A1=B0=EC=97=90=20=ED=98=95=ED=83=9C?= =?UTF-8?q?=EC=86=8C=20=EB=B6=84=EC=84=9D=EA=B8=B0=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EC=B0=A8=EB=A5=BC=20=ED=97=88=EC=9A=A9=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 가 잡아낸 사실: kiwipiepy 0.23.2 / kiwipiepy_model 0.23.0 으로 버전이 같아도 플랫폼 빌드가 다르면(macOS arm64 wheel vs manylinux x86_64 wheel) 토큰화가 갈려 K-ER 이 흔들린다. synth-0011 이 macOS 82.4 / Linux 81.9 로 0.5 차이가 났다. 정확일치를 요구하면 이 프로젝트가 실제로 보장할 수 없는 것을 주장하게 된다. 보장 가능한 것(주석이 손이 아니라 실제 엔진에서 나왔는가)만 고정하도록 페어 단위 허용 오차 1.0 을 두고, 관측 근거를 상수 주석에 남긴다. 손으로 쓴 주석이나 규칙 변경은 이보다 훨씬 크게 움직이므로 탐지력은 유지된다. --- tests/tooling/test_annotate_corpus.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/tooling/test_annotate_corpus.py b/tests/tooling/test_annotate_corpus.py index 7d70619..605196f 100644 --- a/tests/tooling/test_annotate_corpus.py +++ b/tests/tooling/test_annotate_corpus.py @@ -16,14 +16,28 @@ def test_summarize_corpus_covers_every_pair(): assert len(summary["pairs"]) == summary["n"] +#: 형태소 분석기 빌드 차이로 허용하는 페어 단위 K-ER 오차. +#: +#: 2026-08-25 관측: kiwipiepy 0.23.2 / kiwipiepy_model 0.23.0 으로 **버전이 같아도** +#: 플랫폼 빌드가 다르면 (macOS arm64 wheel vs manylinux x86_64 wheel) 토큰화가 갈려 +#: 점수가 흔들린다. synth-0011 이 macOS 82.4 / Linux 81.9 로 0.5 차이가 났다. +#: 손으로 쓴 주석이나 규칙 변경은 이보다 훨씬 크게 움직이므로, 이 폭을 허용해도 +#: 테스트의 목적(주석이 실제 엔진에서 나왔는가)은 유지된다. +KER_BUILD_TOLERANCE = 1.0 + + def test_recorded_ker_scores_match_a_live_rescore(): - """코퍼스에 적힌 ker_score 가 지금 엔진이 내는 값과 같아야 한다. + """코퍼스에 적힌 ker_score 가 지금 엔진이 내는 값과 (오차 내에서) 같아야 한다. - 어긋나면 주석이 손으로 쓰였거나 규칙이 바뀐 뒤 재주석하지 않은 것이다. + 크게 어긋나면 주석이 손으로 쓰였거나 규칙이 바뀐 뒤 재주석하지 않은 것이다. + 형태소 분석기 빌드 차이는 KER_BUILD_TOLERANCE 로 흡수한다. """ summary = summarize_corpus(CORPUS) - drifted = [p for p in summary["pairs"] if p["recorded"] != p["after"]] - assert not drifted, f"기록값과 재채점이 다른 페어: {drifted}" + drifted = [ + p for p in summary["pairs"] + if abs(p["recorded"] - p["after"]) > KER_BUILD_TOLERANCE + ] + assert not drifted, f"기록값과 재채점이 {KER_BUILD_TOLERANCE} 넘게 다른 페어: {drifted}" def test_every_gold_pair_passes_the_fidelity_gate():