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..605196f --- /dev/null +++ b/tests/tooling/test_annotate_corpus.py @@ -0,0 +1,52 @@ +"""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"] + + +#: 형태소 분석기 빌드 차이로 허용하는 페어 단위 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_BUILD_TOLERANCE 로 흡수한다. + """ + summary = summarize_corpus(CORPUS) + 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(): + 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"])