o2o-plagiarism-ai/tests/test_case_coverage.py
hbyang 52a0fdcdf0 feat: expand infringement case matching to v1.3 precedent mapping
태그 동점 시 첫 케이스만 반환해 나머지를 버리던 문제를 고친다.
A1·A2·A3·A4·A5·A6·A15 는 주/보조 태그가 동일하고 실제 구별자는 원본의
종류라 태그로 좁혀지지 않는다. find_case -> find_cases 로 바꿔 동점군을
전부 내보내고, 확정은 사람이 원본 종류로 한다.

- cases_v1.3.json 신설(v1.2 삭제): 39건 전부에 대표판례·처리구분(●/○) 적재.
  판례 미지정 10건은 사유를 값으로 남긴다.
- detectable_internal 10 -> 19건 (v1.3 IX장 총괄표 ● 기준으로 정정)
- _assign_tags 주 태그 조합 3 -> 5종. 유사도로 판단 가능한 쟁점만 주 태그로
  낸다. 도달 케이스 3 -> 16건(● 19건 중).
- B3·C1·D1 은 게재 사실·편집 개입·성명표시가 필요해 텍스트로 알 수 없다.
  추측하지 않고 LegalContext 대기로 두며 경계를 테스트로 고정한다.
- 케이스 수 검증을 38~39 범위에서 39 로 고정. v1.3 본문 통계 줄(●16/○22=38)이
  같은 문서의 표(●19/○20=39)와 어긋난 것이 38/39 혼재의 원인이었다.
- docs/PRECEDENT_SOURCE_GAP.md: 대표판례이나 적재본에 없어 출처를 제시할 수
  없는 14건. 응답에서도 precedents_without_source 로 구분한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:42:34 +09:00

88 lines
2.8 KiB
Python

"""39 케이스 커버리지 갭 분석 단위테스트."""
from __future__ import annotations
from dataclasses import dataclass
from app.engine.case_coverage import analyze_coverage
@dataclass
class _Case:
case_id: str
subgroup: str
actor: str
detectable_internal: bool
_CASES = [
_Case("A1", "A-1", "저자(가해)", True),
_Case("A2", "A-1", "저자(가해)", True),
_Case("C1", "C", "플랫폼", False), # 탐지 범위 밖
]
def test_no_data_all_detectable_need_data():
rep = analyze_coverage(_CASES, None)
assert rep["detectable_cases"] == 2
assert rep["need_data_cases"] == 2
assert rep["covered_cases"] == 0
assert rep["coverage_ratio"] == 0.0
def test_with_samples_marks_covered():
rep = analyze_coverage(_CASES, {"A1": 10})
assert rep["covered_cases"] == 1
assert rep["need_data_cases"] == 1
assert rep["coverage_ratio"] == 0.5
def test_out_of_scope_excluded_from_request():
rep = analyze_coverage(_CASES, {"A1": 5, "A2": 5})
ids = {x["case_id"] for x in rep["data_request_list"]}
assert "C1" not in ids
assert rep["need_data_cases"] == 0
# --- 태그 → 케이스 도달 범위 회귀 (v1.3) ---------------------------------
from pathlib import Path
from app.engine.taxonomy import load_taxonomy
ROOT = Path(__file__).resolve().parents[1]
#: _assign_tags 가 실제로 낼 수 있는 주 태그 조합 전부.
#: 이 목록이 곧 케이스 도달 범위의 상한이므로 조합이 바뀌면 여기도 바뀌어야 한다.
_EMITTED_PRIMARY = [
["reproduction", "citation_missing"],
["reproduction", "derivative_work"],
["derivative_work", "citation_missing"],
["derivative_work", "substandard_derivative"],
["reproduction"],
]
def _reachable():
tax = load_taxonomy(ROOT / "data/taxonomy")
return {c.case_id for combo in _EMITTED_PRIMARY for c in tax.find_cases(combo)}
def test_reachable_cases_cover_most_technical_detection_cases():
"""v1.3 ● 19건 중 16건이 태그만으로 후보에 오른다.
남은 B3·C1·D1 은 게재 사실·편집 개입·성명표시처럼 텍스트에서 알 수 없는
사실이 필요해 LegalContext 없이는 도달할 수 없다. 이 경계를 고정한다.
"""
tax = load_taxonomy(ROOT / "data/taxonomy")
detectable = {c.case_id for c in tax.cases if c.detectable_internal}
reachable = _reachable()
assert detectable - reachable == {"B3", "C1", "D1"}
assert len(reachable & detectable) == 16
def test_no_tie_group_is_silently_collapsed():
"""동점군이 2건 이상이면 전부 반환된다 — 하나만 남으면 나머지가 사장된다."""
tax = load_taxonomy(ROOT / "data/taxonomy")
assert len(tax.find_cases(["reproduction", "citation_missing"])) == 7
assert len(tax.find_cases(["derivative_work", "substandard_derivative"])) == 2