feat(p5-2): kb-eval metrics + compare — AggregateMetrics, CompareReport, kb eval CLI #28
Reference in New Issue
Block a user
Delete Branch "feat/p5-2-metrics-compare"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
P5-2 구현. P5-1 이 적은
eval_runs/eval_query_results위에서 메트릭 계산 + run-vs-run 비교 + Markdown 리포트.kb_eval::metrics—AggregateMetrics(hit@k / MRR / recall@k_doc / citation_coverage / groundedness / empty_result_rate / refusal_correctness). NaN metrics 는 JSONnull. 4-decimal round +Deserialize추가로aggregate_json라운드트립.kb_eval::compare—compare_runs가 두 run 의 aggregate Δ + per-query Win/Loss/Draw/Regression 산출. chunker_version drift 시 graceful doc-id fallback (deltas.chunker_version_match 가"fallback_doc");--strict-chunker-version으로 refuse.render_report_md— 인간용 Markdown.kb_store_sqlite::{load_eval_run, load_eval_query_results, update_eval_run_aggregate}+ ownedEvalRunRecord/EvalQueryResultRecord. Write 측 borrow shape 그대로.kb evalCLI:run(P5-1 위임),aggregate <id>,compare <a> <b> [--strict-chunker-version] [--write-report].--json으로 raw, 기본은 Markdown.동작
compute_aggregate(run_id)— KB_EVAL_GOLDEN env 으로 fixture 재로드.expected_chunk_ids비어있는 query 는 hit@k / MRR / recall@k_doc 분모에서 제외 (refusal-only flow).expected_doc_ids비어있으면 "should refuse" 로 분류.store_aggregate는 1 회 UPDATE. row 누락 시 silent drop 대신Err(metrics 잃지 않게).compare_runs는 두 run row 먼저 로드 후 chunker_version 추출. graceful 모드에서 fallback 시 chunk_id 매칭 대신 doc_id 매칭.fixtures/eval/compare-1.json) — UPDATE_SNAPSHOTS=1 로 갱신.Spec deviations (코드 + spec doc 에 명시)
chunker_version_match: "fallback_doc"(spec 의"fallback_doc_span"아님). 50% span overlap 은 양쪽 chunks 동시 보존이 chunker re-index 후 현실적으로 안 돼서 P6+ 로 deferred.*_with_config헬퍼 추가 — 통합 테스트가 TempDir Config 로 드라이브. no-arg 형태는Config::load(None)로 위임.kb-cli→kb-eval직접 wire (kb-app는 P5-1 runner 때문에 이미kb-eval의존 → cycle 회피).AggregateMetrics: Deserialize추가 —aggregate_json라운드트립.anyhow가 spec Allowed 에 없지만 워크스페이스 idiom.검증
cargo test -p kb-eval30/30 (15 unit + 2 loader + 8 metrics+compare 통합 + 7 runner)cargo test -p kb-store-sqlite33/33cargo clippy --workspace --all-targets -- -D warningscleankb-app|kb-source-fs|kb-parse|kb-normalize|kb-chunk|kb-store-vector|kb-embed|kb-search|kb-llm|kb-rag|kb-tui|kb-desktop모두 부재)후속 (이 PR 의 범위 밖)
runs_dir스캔) — P9.Spec 링크
tasks/p5/p5-2-metrics-compare.mdtasks/phase-5-evaluation.mdTest plan
cargo check -p kb-evalcargo test -p kb-eval(30 pass)cargo clippy --workspace --all-targets -- -D warningscargo check -p kb-cli+kb eval --help동작 확인🤖 Generated with Claude Code
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>두 reviewer 의 should-fix 4 건 + nit 5 건 push 전 반영. ## should-fix - `citation_coverage`: 빈 citations[] 가 `Iterator::all` vacuous-true 로 1.0 새는 거 차단 — `!is_empty() && all(non-empty path)` 로 변경. 또한 `_store: &SqliteStore` dead 인자 시그니처에서 제거 (호출 사이트 + 테스트 helper 정리). - `refusal_correctness`: lexical-only run 에서 `answer == None` 인 경우 분모 증가 안 함 (NaN/null 출력) — 자동 fail 처리하던 게 metric 의미를 왜곡함. 새 unit test `refusal_correctness_nan_for_non_rag_run` 추가. - `groundedness`: `must_contain.is_empty() && forbidden.is_empty()`인 golden 은 분모에서 제외. unconfigured entry 가 free 1.0 받지 않게. 새 unit test `groundedness_skips_unconfigured_goldens` 추가. - `kb-cli/Cargo.toml` rationale 코멘트 사실 오류 정정 — kb-eval → kb-app 의존이지 그 반대 아님. ## nits - `KB_EVAL_GOLDEN` / `DEFAULT_GOLDEN_PATH` 중복 — `metrics::` 의 `pub(crate)` 로 단일화, `runner` 가 import. - `render_report_md` 의 `{:?}` `ComparisonKind` → 명시적 lowercase 매핑 함수 (`win`/`loss`/`draw`/`regression`) — JSON 직렬화 컨벤션과 통일. - `extract_chunker_version` `None == None` 매치 silent 위험에 대한 defensive 코멘트. - `delta_null_when_either_nan` 테스트의 `let mut` suppress hack → struct update syntax 로 정리. - `empty_store` test helper + 매번 `mem::forget(tmp)` 죽은 코드 제거. ## 추가 spec doc `tasks/p5/p5-2-metrics-compare.md` deviations 섹션 4 항목 추가: - `kb-eval` crate-level `kb-app` dep — P5-1 inheritance, 새 모듈 surface 는 import 안 함. - `citation_coverage` 약화된 resolver — `document_exists_by_path` 기다리는 중. - `refusal_correctness` non-RAG 런 NaN. - `groundedness` no-check golden skip. ## 검증 - `cargo test -p kb-eval` 35/35 (18 unit + 2 loader + 8 integration + 7 runner; 새 3 unit test). - `cargo clippy --workspace --all-targets -- -D warnings` clean. - `compare_report_snapshot_matches_fixture` 변경 없이 통과 — 새 동작이 스냅샷 입력 (lexical-only, no must_contain, no should-refuse) 영향 없음. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>자동 리뷰 (spec compliance + code quality)
두 리뷰어 모두 APPROVE. should-fix 4 건 + nit 5 건 push 전 반영 완료 (
ee1f233).Spec compliance 결과
AggregateMetrics9 필드,CompareReport,QueryComparison,ComparisonKind, 4 개 자유 함수).expected_doc_ids=[]).metrics.rs/compare.rs가kb-app|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-eval crate-levelkb-appdep 은 P5-1 inheritance — 새 모듈 surface 안 씀, deviations 섹션 추가)cargo test -p kb-eval35 통과 — 18 unit + 2 loader + 8 integration + 7 runner).의도된 deviation (코드 + spec doc 명시):
fallback_doc) — 50% span overlap 은 chunks 테이블 archival 필요해서 P6+ 로 deferred.*_with_config헬퍼 추가 — TempDir Config 통합 테스트 드라이브용.kb-cli→kb-eval직접 wire (kb-app사이클 회피).AggregateMetrics: Deserialize—aggregate_jsonround-trip.kb-evalcrate-levelkb-appdep 보존 (P5-1 runner 의존).citation_coverageweaker resolver (path 비어있는지만),document_exists_by_path도착 시 tighten 예정.refusal_correctnessnon-RAG 런에서 NaN.groundednessno-check golden skip.Code quality 결과
Must-fix blocker 없음. 적용한 should-fix 4 건:
Iterator::allvacuous-true 로 1.0 새는 거 차단 + dead_store인자 시그니처 제거 (호출 사이트 12 곳 + helper 정리).refusal_correctness_nan_for_non_rag_run.must_contain.is_empty() && forbidden.is_empty()분모 제외. 새 unit testgroundedness_skips_unconfigured_goldens.적용한 nit 5 건:
KB_EVAL_GOLDEN/DEFAULT_GOLDEN_PATH중복 →metrics::의pub(crate)로 단일화.render_report_md의{:?}ComparisonKind → 명시적 lowercase 라벨 함수 (JSON 직렬화 컨벤션과 통일).extract_chunker_versionNone == Nonesilent risk — defensive 코멘트.delta_null_when_either_nan의let mutsuppress hack → struct update syntax.empty_storehelper +mem::forget(tmp)죽은 코드 제거 (test 함수 12 곳 정리).후속 PR 로 미룸
compute_aggregate/compare_runs가 currentfixtures/golden_queries.yaml사용. fixture 가 run 사이에 편집되면 silent 새 spec / 옛 retrieval. 최소 query_id 인터섹션 warning, 이상적으로config_snapshot_json에 fixture hash 임베드 + drift detect. CLI--golden플래그도 별도. 큰 변경이라 별도 PR.document_exists_by_path강화 —kb-store-sqlite에 helper 추가 후.compute_aggregate의 result_json 재파싱 비용 —compare_runs가 1 쿼리 3 번 파싱. 50 row 에 무관, 1000+ 쿼리 시 refactor.chunker_version_match를deltas내부 string 대신CompareReporttop-level 필드로 promote.검증
cargo test -p kb-eval35/35cargo test -p kb-store-sqlite33/33cargo test -p kb-cli5/5cargo clippy --workspace --all-targets -- -D warningscleancompare_report_snapshot_matches_fixture변경 없이 통과 — 새 동작이 스냅샷 입력에 영향 없음 (lexical-only, no must_contain).승인 후 머지 부탁.