Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 + 의존성 라이선스 허용목록 + 자산 분리 검사 + 감사

Expand Down
52 changes: 52 additions & 0 deletions tests/tooling/test_annotate_corpus.py
Original file line number Diff line number Diff line change
@@ -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
)
55 changes: 54 additions & 1 deletion tooling/annotate_corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
사용:
python -m tooling.annotate_corpus "<source_text>" "<easy_text>" # 단일 페어 채점(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
Expand All @@ -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
Expand Down Expand Up @@ -78,16 +80,67 @@ 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 []),
"_slot_types": sorted({_verdict_str(s.type) for s in fid.slots}),
}


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"])
Expand Down
Loading