P5-2 구현. 저장된 eval_runs / eval_query_results 위에서:
- `kb_eval::metrics`: hit@k / MRR / recall@k_doc / citation_coverage /
groundedness / empty_result_rate / refusal_correctness 계산. NaN
metrics (분모 0)는 JSON null. 4-decimal round + Deserialize 추가로
aggregate_json 라운드트립.
- `kb_eval::compare`: 두 run 비교 → CompareReport (per-metric Δ + per-
query Win/Loss/Draw/Regression). chunker_version drift 시 graceful
doc-id fallback (chunker_version_match: "fallback_doc"), `strict`
옵션이면 refuse.
- `render_report_md`: 인간용 Markdown (집계 + Wins/Losses/Regressions
표).
- `SqliteStore::{load_eval_run, load_eval_query_results,
update_eval_run_aggregate}` + owned `EvalRunRecord` /
`EvalQueryResultRecord` 추가 — write 측 borrow-shape는 그대로.
- `kb eval` CLI: `run` (P5-1 위임), `aggregate <id>`, `compare <a> <b>
[--strict-chunker-version] [--write-report]`. `--json` 으로 raw
CompareReport, 기본은 Markdown 출력.
## Spec deviations (intentional, doc 명시)
- Graceful 매칭은 doc-id-only (chunker_version_match: "fallback_doc")
— 50% span overlap은 chunker re-index 후 양쪽 chunks 동시 보존이
현실적으로 안 돼서 P6+ 로 deferred.
- `*_with_config` 헬퍼 추가: 통합 테스트가 TempDir Config 로 드라이브.
no-arg 형태는 Config::load(None) 로 위임.
- CLI 는 kb-cli → kb-eval 직접 wire (kb-app cycle 회피). DoD 의 "via
kb-app" 의도는 facade 단일화였지만 cycle 발생.
- `AggregateMetrics: Deserialize` 추가 — aggregate_json 라운드트립.
## 검증
- `cargo test -p kb-eval` 30/30 (15 unit + 2 loader + 8 metrics+compare
통합 + 7 runner). 8 통합 중 snapshot 1 건 (`compare-1.json`).
- `cargo test -p kb-store-sqlite` 33/33.
- `cargo clippy --workspace --all-targets -- -D warnings` clean.
- forbidden imports 부재 (`kb-source-fs|kb-parse|kb-normalize|kb-chunk|
kb-store-vector|kb-embed|kb-search|kb-llm|kb-rag|kb-tui|kb-desktop|
kb-app` — kb-app 는 metrics/compare 모듈에 부재; runner 만 사용).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
198 KiB
198 KiB