태그 동점 시 첫 케이스만 반환해 나머지를 버리던 문제를 고친다. 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>
88 lines
2.8 KiB
Python
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
|