#!/usr/bin/env python3 """39개 침해 케이스 커버리지 갭 분석 — 컴북스 데이터 요청 근거 산출. 데이터 없이 지금 실행 가능: taxonomy(분류체계)만으로 '탐지 대상이나 샘플이 없는 케이스' 목록을 뽑아 정밀도 97% 달성을 위해 요청할 데이터를 케이스 단위로 정리한다. 옵션으로 케이스별 보유 샘플 수 파일을 주면 실제 커버리지를 계산한다. 샘플 수 파일 형식 (JSON): {"A1": 12, "A2": 0, ...} 사용: python -m scripts.analyze_case_coverage python -m scripts.analyze_case_coverage --samples data/eval/case_counts.json python -m scripts.analyze_case_coverage --md > docs/CASE_COVERAGE.md """ from __future__ import annotations import argparse import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from app.core.config import get_settings # noqa: E402 from app.engine.case_coverage import analyze_coverage # noqa: E402 from app.engine.taxonomy import load_taxonomy # noqa: E402 def main() -> None: ap = argparse.ArgumentParser(description="39 케이스 커버리지 갭 분석") ap.add_argument("--samples", default=None, help="케이스별 보유 샘플 수 JSON") ap.add_argument("--md", action="store_true", help="Markdown 표로 출력") args = ap.parse_args() tax = load_taxonomy(get_settings().taxonomy_path) if tax is None: print("taxonomy 로드 실패", file=sys.stderr) sys.exit(1) counts = None if args.samples and Path(args.samples).exists(): counts = json.loads(Path(args.samples).read_text(encoding="utf-8")) rep = analyze_coverage(tax.cases, counts) if args.md: print("# 39개 침해 케이스 커버리지 갭 분석\n") print(f"- 전체 케이스: {rep['total_cases']}") print(f"- 엔진 탐지 대상: {rep['detectable_cases']}") print(f"- 평가 샘플 보유(covered): {rep['covered_cases']}") print(f"- 탐지 대상이나 데이터 없음(요청 대상): {rep['need_data_cases']}") print(f"- 커버리지: {rep['coverage_ratio']:.1%}\n") print("| case_id | subgroup | actor | 탐지대상 | 샘플수 | 상태 |") print("|---|---|---|---|---|---|") for r in rep["rows"]: print(f"| {r.case_id} | {r.subgroup} | {r.actor} | " f"{'O' if r.detectable else '-'} | {r.sample_count} | {r.status} |") return print("\n=== 39개 침해 케이스 커버리지 ===") print(f"전체 {rep['total_cases']} / 탐지대상 {rep['detectable_cases']} / " f"covered {rep['covered_cases']} / 요청대상 {rep['need_data_cases']} / " f"커버리지 {rep['coverage_ratio']:.1%}") print("\n[데이터 요청 대상 — 탐지 가능하나 샘플 없는 케이스]") if not rep["data_request_list"]: print(" (없음 — 모든 탐지대상 케이스에 샘플 보유)") for item in rep["data_request_list"]: print(f" - {item['case_id']:<4} {item['subgroup']} [{item['actor']}]") print("\n※ '탐지 대상이나 데이터 없음' 케이스가 정밀도 97% 달성을 위한 컴북스 요청 데이터 목록입니다.") print(" out_of_engine_scope(유족/플랫폼/약관 등)는 탐지 모듈 범위 밖 → 데이터 불필요.") if __name__ == "__main__": main()