feat(p5-2): kb-eval metrics + compare — AggregateMetrics, CompareReport, kb eval CLI #28

Merged
altair823 merged 2 commits from feat/p5-2-metrics-compare into main 2026-05-02 03:19:01 +00:00
Owner

요약

P5-2 구현. P5-1 이 적은 eval_runs / eval_query_results 위에서 메트릭 계산 + run-vs-run 비교 + Markdown 리포트.

  • kb_eval::metricsAggregateMetrics (hit@k / MRR / recall@k_doc / citation_coverage / groundedness / empty_result_rate / refusal_correctness). NaN metrics 는 JSON null. 4-decimal round + Deserialize 추가로 aggregate_json 라운드트립.
  • kb_eval::comparecompare_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} + owned EvalRunRecord / EvalQueryResultRecord. Write 측 borrow shape 그대로.
  • kb eval CLI: 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 매칭.
  • snapshot fixture (fixtures/eval/compare-1.json) — UPDATE_SNAPSHOTS=1 로 갱신.

Spec deviations (코드 + spec doc 에 명시)

  • graceful 매칭 = doc-id-only. 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) 로 위임.
  • CLI 는 kb-clikb-eval 직접 wire (kb-app 는 P5-1 runner 때문에 이미 kb-eval 의존 → cycle 회피).
  • AggregateMetrics: Deserialize 추가 — aggregate_json 라운드트립.
  • anyhow 가 spec Allowed 에 없지만 워크스페이스 idiom.

검증

  • cargo test -p kb-eval 30/30 (15 unit + 2 loader + 8 metrics+compare 통합 + 7 runner)
  • cargo test -p kb-store-sqlite 33/33
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • forbidden imports 부재 (metrics/compare 모듈에 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 모두 부재)

후속 (이 PR 의 범위 밖)

  • 50% span overlap matching (graceful fallback 강화) — chunks 테이블 archival 기획 후.
  • compare CLI 의 인간 친화적 컬러 출력 — P9 wrap-up.
  • aggregate 메트릭 trend 추적 (runs_dir 스캔) — P9.

Spec 링크

  • tasks/p5/p5-2-metrics-compare.md
  • 디자인 §5.7 eval_runs.aggregate_json, phase epic tasks/phase-5-evaluation.md

Test plan

  • cargo check -p kb-eval
  • cargo test -p kb-eval (30 pass)
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo check -p kb-cli + kb eval --help 동작 확인
  • forbidden deps 부재 (metrics + compare)

🤖 Generated with Claude Code

## 요약 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 는 JSON `null`. 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}` + owned `EvalRunRecord` / `EvalQueryResultRecord`. Write 측 borrow shape 그대로. - `kb eval` CLI: `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 매칭. - snapshot fixture (`fixtures/eval/compare-1.json`) — UPDATE_SNAPSHOTS=1 로 갱신. ## Spec deviations (코드 + spec doc 에 명시) - **graceful 매칭 = doc-id-only.** `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)` 로 위임. - CLI 는 `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-eval` 30/30 (15 unit + 2 loader + 8 metrics+compare 통합 + 7 runner) - `cargo test -p kb-store-sqlite` 33/33 - `cargo clippy --workspace --all-targets -- -D warnings` clean - forbidden imports 부재 (metrics/compare 모듈에 `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` 모두 부재) ## 후속 (이 PR 의 범위 밖) - 50% span overlap matching (graceful fallback 강화) — chunks 테이블 archival 기획 후. - compare CLI 의 인간 친화적 컬러 출력 — P9 wrap-up. - aggregate 메트릭 trend 추적 (`runs_dir` 스캔) — P9. ## Spec 링크 - `tasks/p5/p5-2-metrics-compare.md` - 디자인 §5.7 eval_runs.aggregate_json, phase epic `tasks/phase-5-evaluation.md` ## Test plan - [x] `cargo check -p kb-eval` - [x] `cargo test -p kb-eval` (30 pass) - [x] `cargo clippy --workspace --all-targets -- -D warnings` - [x] `cargo check -p kb-cli` + `kb eval --help` 동작 확인 - [x] forbidden deps 부재 (metrics + compare) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
altair823 added 1 commit 2026-05-02 03:06:10 +00:00
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>
altair823 added 1 commit 2026-05-02 03:17:38 +00:00
두 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>
Author
Owner

자동 리뷰 (spec compliance + code quality)

두 리뷰어 모두 APPROVE. should-fix 4 건 + nit 5 건 push 전 반영 완료 (ee1f233).

Spec compliance 결과

  • 공개 API 시그니처 spec §54-89 와 100% 일치 (AggregateMetrics 9 필드, CompareReport, QueryComparison, ComparisonKind, 4 개 자유 함수).
  • 7 개 metric formula 모두 행동 계약대로 구현 (hit@k denom, MRR top-10 floor, recall@k_doc applicability, citation_coverage grounded-denominator, groundedness must/forbidden, empty_result_rate, refusal_correctness expected_doc_ids=[]).
  • forbidden imports 부재 — metrics.rs / compare.rskb-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-level kb-app dep 은 P5-1 inheritance — 새 모듈 surface 안 씀, deviations 섹션 추가)
  • 10 개 test case 모두 작성 (cargo test -p kb-eval 35 통과 — 18 unit + 2 loader + 8 integration + 7 runner).
  • DoD 6 개 모두 만족.

의도된 deviation (코드 + spec doc 명시):

  • graceful 매칭 = doc-id-only (fallback_doc) — 50% span overlap 은 chunks 테이블 archival 필요해서 P6+ 로 deferred.
  • *_with_config 헬퍼 추가 — TempDir Config 통합 테스트 드라이브용.
  • CLI kb-clikb-eval 직접 wire (kb-app 사이클 회피).
  • AggregateMetrics: Deserializeaggregate_json round-trip.
  • kb-eval crate-level kb-app dep 보존 (P5-1 runner 의존).
  • citation_coverage weaker resolver (path 비어있는지만), document_exists_by_path 도착 시 tighten 예정.
  • refusal_correctness non-RAG 런에서 NaN.
  • groundedness no-check golden skip.

Code quality 결과

Must-fix blocker 없음. 적용한 should-fix 4 건:

  • citation_coverage 코멘트와 코드 mismatch — 빈 citations[] 가 Iterator::all vacuous-true 로 1.0 새는 거 차단 + dead _store 인자 시그니처 제거 (호출 사이트 12 곳 + helper 정리).
  • refusal_correctness "no answer" branch 코멘트 모순 — lexical-only 런에서 분모 증가 안 함 (NaN 출력). 새 unit test refusal_correctness_nan_for_non_rag_run.
  • kb-cli/Cargo.toml rationale 사실 오류 — kb-eval → kb-app 의존이지 반대 아님. 코멘트 정정.
  • groundedness no-check golden skip — unconfigured entry 가 free 1.0 받지 않게 must_contain.is_empty() && forbidden.is_empty() 분모 제외. 새 unit test groundedness_skips_unconfigured_goldens.

적용한 nit 5 건:

  • KB_EVAL_GOLDEN / DEFAULT_GOLDEN_PATH 중복 → metrics::pub(crate) 로 단일화.
  • render_report_md{:?} ComparisonKind → 명시적 lowercase 라벨 함수 (JSON 직렬화 컨벤션과 통일).
  • extract_chunker_version None == None silent risk — defensive 코멘트.
  • delta_null_when_either_nanlet mut suppress hack → struct update syntax.
  • empty_store helper + mem::forget(tmp) 죽은 코드 제거 (test 함수 12 곳 정리).

후속 PR 로 미룸

  • C4 — golden YAML drift detection: compute_aggregate / compare_runscurrent fixtures/golden_queries.yaml 사용. fixture 가 run 사이에 편집되면 silent 새 spec / 옛 retrieval. 최소 query_id 인터섹션 warning, 이상적으로 config_snapshot_json 에 fixture hash 임베드 + drift detect. CLI --golden 플래그도 별도. 큰 변경이라 별도 PR.
  • citation_coveragedocument_exists_by_path 강화 — kb-store-sqlite 에 helper 추가 후.
  • compute_aggregate 의 result_json 재파싱 비용compare_runs 가 1 쿼리 3 번 파싱. 50 row 에 무관, 1000+ 쿼리 시 refactor.
  • chunker_version_matchdeltas 내부 string 대신 CompareReport top-level 필드로 promote.
  • snapshot fixture 의 f32 노이즈 정리.

검증

  • cargo test -p kb-eval 35/35
  • cargo test -p kb-store-sqlite 33/33
  • cargo test -p kb-cli 5/5
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • compare_report_snapshot_matches_fixture 변경 없이 통과 — 새 동작이 스냅샷 입력에 영향 없음 (lexical-only, no must_contain).

승인 후 머지 부탁.

## 자동 리뷰 (spec compliance + code quality) 두 리뷰어 모두 APPROVE. should-fix 4 건 + nit 5 건 push 전 반영 완료 (`ee1f233`). ### Spec compliance 결과 - 공개 API 시그니처 spec §54-89 와 100% 일치 (`AggregateMetrics` 9 필드, `CompareReport`, `QueryComparison`, `ComparisonKind`, 4 개 자유 함수). - 7 개 metric formula 모두 행동 계약대로 구현 (hit@k denom, MRR top-10 floor, recall@k_doc applicability, citation_coverage grounded-denominator, groundedness must/forbidden, empty_result_rate, refusal_correctness `expected_doc_ids=[]`). - forbidden imports 부재 — `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-level `kb-app` dep 은 P5-1 inheritance — 새 모듈 surface 안 씀, deviations 섹션 추가) - 10 개 test case 모두 작성 (`cargo test -p kb-eval` 35 통과 — 18 unit + 2 loader + 8 integration + 7 runner). - DoD 6 개 모두 만족. 의도된 deviation (코드 + spec doc 명시): - graceful 매칭 = doc-id-only (`fallback_doc`) — 50% span overlap 은 chunks 테이블 archival 필요해서 P6+ 로 deferred. - `*_with_config` 헬퍼 추가 — TempDir Config 통합 테스트 드라이브용. - CLI `kb-cli` → `kb-eval` 직접 wire (`kb-app` 사이클 회피). - `AggregateMetrics: Deserialize` — `aggregate_json` round-trip. - `kb-eval` crate-level `kb-app` dep 보존 (P5-1 runner 의존). - `citation_coverage` weaker resolver (path 비어있는지만), `document_exists_by_path` 도착 시 tighten 예정. - `refusal_correctness` non-RAG 런에서 NaN. - `groundedness` no-check golden skip. ### Code quality 결과 Must-fix blocker 없음. 적용한 should-fix 4 건: - **citation_coverage** 코멘트와 코드 mismatch — 빈 citations[] 가 `Iterator::all` vacuous-true 로 1.0 새는 거 차단 + dead `_store` 인자 시그니처 제거 (호출 사이트 12 곳 + helper 정리). - **refusal_correctness** "no answer" branch 코멘트 모순 — lexical-only 런에서 분모 증가 안 함 (NaN 출력). 새 unit test `refusal_correctness_nan_for_non_rag_run`. - **kb-cli/Cargo.toml rationale 사실 오류** — kb-eval → kb-app 의존이지 반대 아님. 코멘트 정정. - **groundedness** no-check golden skip — unconfigured entry 가 free 1.0 받지 않게 `must_contain.is_empty() && forbidden.is_empty()` 분모 제외. 새 unit test `groundedness_skips_unconfigured_goldens`. 적용한 nit 5 건: - `KB_EVAL_GOLDEN` / `DEFAULT_GOLDEN_PATH` 중복 → `metrics::` 의 `pub(crate)` 로 단일화. - `render_report_md` 의 `{:?}` ComparisonKind → 명시적 lowercase 라벨 함수 (JSON 직렬화 컨벤션과 통일). - `extract_chunker_version` `None == None` silent risk — defensive 코멘트. - `delta_null_when_either_nan` 의 `let mut` suppress hack → struct update syntax. - `empty_store` helper + `mem::forget(tmp)` 죽은 코드 제거 (test 함수 12 곳 정리). ### 후속 PR 로 미룸 - **C4 — golden YAML drift detection**: `compute_aggregate` / `compare_runs` 가 *current* `fixtures/golden_queries.yaml` 사용. fixture 가 run 사이에 편집되면 silent 새 spec / 옛 retrieval. 최소 query_id 인터섹션 warning, 이상적으로 `config_snapshot_json` 에 fixture hash 임베드 + drift detect. CLI `--golden` 플래그도 별도. 큰 변경이라 별도 PR. - **citation_coverage** 의 `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 대신 `CompareReport` top-level 필드로 promote. - snapshot fixture 의 f32 노이즈 정리. ### 검증 - `cargo test -p kb-eval` 35/35 - `cargo test -p kb-store-sqlite` 33/33 - `cargo test -p kb-cli` 5/5 - `cargo clippy --workspace --all-targets -- -D warnings` clean - `compare_report_snapshot_matches_fixture` 변경 없이 통과 — 새 동작이 스냅샷 입력에 영향 없음 (lexical-only, no must_contain). 승인 후 머지 부탁.
altair823 merged commit 2aecbf3d9f into main 2026-05-02 03:19:01 +00:00
altair823 deleted branch feat/p5-2-metrics-compare 2026-05-02 03:19:02 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#28