feat(p5-1): kb-eval crate — golden-fixture runner + eval persistence #27

Merged
altair823 merged 2 commits from feat/p5-1-golden-fixture-runner into main 2026-05-02 02:47:24 +00:00
Owner

요약

P5-1 구현. Golden YAML 코퍼스를 로드하고 각 쿼리를 kb-app (search/ask) 으로 돌려서 eval_runs / eval_query_results SQLite 테이블 + runs_dir/<run_id>/per_query.jsonl 미러로 기록하는 평가 러너.

새 크레이트 kb-eval (lib/types/loader/runner) + kb-store-sqlite::eval 모듈 (record_eval_run_with_results 트랜잭션 + document_exists/chunk_exists 검증 프로브). 5건 KO+EN fixtures/golden_queries.yaml 템플릿 동봉.

동작

  • load_golden_set(path) — YAML 파싱 + id 중복 검출. 누락 IDs 는 BTreeSet 기반 결정적 정렬.
  • run_eval(opts) — 쿼리별 kb_app::search_with_config 호출 (with_rag 시 ask_with_config). 쿼리 단위 에러는 error: Some(msg) 로 캡쳐, 런 자체는 abort 안 함.
  • aggregate_json"{}" 로 비워둠 (P5-2 가 채울 자리).
  • 트랜잭션 1 회로 run row + 모든 query result row 를 한 번에 커밋 — 부분 상태 방지.
  • temperature=0 + 고정 seed → lexical 모드에서 두 번 실행 시 per_query.jsonl 바이트 동일.
  • KB_EVAL_GOLDEN 환경변수로 fixture 경로 override.

테스트

  • cargo test -p kb-eval 13/13 pass (loader 4 + runner 7 + lib unit 2).
  • cargo test -p kb-store-sqlite 33/33 pass.
  • cargo clippy --all-targets -- -D warnings 두 크레이트 모두 clean.

Spec 대비 의도된 deviation (코드 + spec doc 에 명시)

  • run_id: uuid::Uuid::now_v7().simple() (timestamp-ordered hex). spec 은 ULID 명시했으나 uuid 가 이미 워크스페이스 의존이라 별도 ULID 크레이트 도입 회피. 다운스트림은 opaque PK 라 무관.
  • load_golden_set_validated: pub(crate) + #[cfg(test)] 로 격리. 프로덕션 경로는 validate_against_db 를 직접 호출해서 wrapper 가 dead path. 테스트만 wrapper 를 호출.
  • snapshot fixture: id/query/mode/first_hit{chunk_id, doc_id, heading_path, score} 로 정규화 projection. byte-stable 검증은 별도 determinism 테스트가 담당.
  • index_version: config_snapshot_jsonnull. kb-app 가 호출 시점에 합성하는 값이라 Config 에 부재.
  • --max-queries flag: spec 을 update 해서 P5-2 로 deferred (rationale 명시).
  • anyhow / uuid 가 spec Allowed 목록에 명시 안 되어 있지만 워크스페이스 idiom 상 필요 — kb-app / kb-store-sqlite 도 동일.

후속 PR 로 미룸 (이 PR 의 범위 밖)

  • App reuse: 현재 쿼리마다 App::open (SQLite + LanceDB + fastembed init 매번). lexical 5건 스모크 OK 지만 hybrid 50건 시 setup 비용이 elapsed_ms 지배. _with_app 변형 추가 필요.
  • expand_path 헬퍼가 kb-store-vector::paths, kb-store-sqlite, kb-eval 3 군데 복���. kb-config 로 hoist 필요.
  • 쿼리 직렬 처리 — 병렬화는 P5 마무리에서 검토.

Spec 링크

  • tasks/p5/p5-1-golden-fixture-runner.md
  • 디자인 §5.7 eval_runs / eval_query_results, §6.3 runs_dir

Test plan

  • cargo check -p kb-eval
  • cargo test -p kb-eval (13 pass)
  • cargo clippy -p kb-eval --all-targets -- -D warnings
  • cargo check/test/clippy -p kb-store-sqlite
  • no forbidden deps (grep 으로 kb-source-fs|kb-parse|kb-normalize|kb-chunk|kb-store-vector|kb-embed|kb-search|kb-llm|kb-rag|kb-tui|kb-desktop 부재 확인)
  • time::OffsetDateTime::now_utc() 만 사용 (local TZ 누출 없음)

🤖 Generated with Claude Code

## 요약 P5-1 구현. Golden YAML 코퍼스를 로드하고 각 쿼리를 `kb-app` (search/ask) 으로 돌려서 `eval_runs` / `eval_query_results` SQLite 테이블 + `runs_dir/<run_id>/per_query.jsonl` 미러로 기록하는 평가 러너. 새 크레이트 `kb-eval` (lib/types/loader/runner) + `kb-store-sqlite::eval` 모듈 (`record_eval_run_with_results` 트랜잭션 + `document_exists`/`chunk_exists` 검증 프로브). 5건 KO+EN `fixtures/golden_queries.yaml` 템플릿 동봉. ## 동작 - `load_golden_set(path)` — YAML 파싱 + `id` 중복 검출. 누락 IDs 는 BTreeSet 기반 결정적 정렬. - `run_eval(opts)` — 쿼리별 `kb_app::search_with_config` 호출 (with_rag 시 `ask_with_config`). 쿼리 단위 에러는 `error: Some(msg)` 로 캡쳐, 런 자체는 abort 안 함. - `aggregate_json` 은 `"{}"` 로 비워둠 (P5-2 가 채울 자리). - 트랜잭션 1 회로 run row + 모든 query result row 를 한 번에 커밋 — 부분 상태 방지. - `temperature=0` + 고정 seed → lexical 모드에서 두 번 실행 시 `per_query.jsonl` 바이트 동일. - `KB_EVAL_GOLDEN` 환경변수로 fixture 경로 override. ## 테스트 - `cargo test -p kb-eval` 13/13 pass (loader 4 + runner 7 + lib unit 2). - `cargo test -p kb-store-sqlite` 33/33 pass. - `cargo clippy --all-targets -- -D warnings` 두 크레이트 모두 clean. ## Spec 대비 의도된 deviation (코드 + spec doc 에 명시) - `run_id`: `uuid::Uuid::now_v7().simple()` (timestamp-ordered hex). spec 은 ULID 명시했으나 `uuid` 가 이미 워크스페이스 의존이라 별도 ULID 크레이트 도입 회피. 다운스트림은 opaque PK 라 무관. - `load_golden_set_validated`: `pub(crate)` + `#[cfg(test)]` 로 격리. 프로덕션 경로는 `validate_against_db` 를 직접 호출해서 wrapper 가 dead path. 테스트만 wrapper 를 호출. - snapshot fixture: `id/query/mode/first_hit{chunk_id, doc_id, heading_path, score}` 로 정규화 projection. byte-stable 검증은 별도 determinism 테스트가 담당. - `index_version`: `config_snapshot_json` 에 `null`. `kb-app` 가 호출 시점에 합성하는 값이라 `Config` 에 부재. - `--max-queries` flag: spec 을 update 해서 P5-2 로 deferred (rationale 명시). - `anyhow` / `uuid` 가 spec Allowed 목록에 명시 안 되어 있지만 워크스페이스 idiom 상 필요 — kb-app / kb-store-sqlite 도 동일. ## 후속 PR 로 미룸 (이 PR 의 범위 밖) - App reuse: 현재 쿼리마다 `App::open` (SQLite + LanceDB + fastembed init 매번). lexical 5건 스모크 OK 지만 hybrid 50건 시 setup 비용이 elapsed_ms 지배. `_with_app` 변형 추가 필요. - `expand_path` 헬퍼가 `kb-store-vector::paths`, `kb-store-sqlite`, `kb-eval` 3 군데 복���. `kb-config` 로 hoist 필요. - 쿼리 직렬 처리 — 병렬화는 P5 마무리에서 검토. ## Spec 링크 - `tasks/p5/p5-1-golden-fixture-runner.md` - 디자인 §5.7 eval_runs / eval_query_results, §6.3 runs_dir ## Test plan - [x] `cargo check -p kb-eval` - [x] `cargo test -p kb-eval` (13 pass) - [x] `cargo clippy -p kb-eval --all-targets -- -D warnings` - [x] `cargo check/test/clippy -p kb-store-sqlite` - [x] no forbidden deps (`grep` 으로 `kb-source-fs|kb-parse|kb-normalize|kb-chunk|kb-store-vector|kb-embed|kb-search|kb-llm|kb-rag|kb-tui|kb-desktop` 부재 확인) - [x] `time::OffsetDateTime::now_utc()` 만 사용 (local TZ 누출 없음) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
altair823 added 1 commit 2026-05-01 18:11:10 +00:00
- new kb-eval crate: load_golden_set (YAML) + run_eval (per-query search/ask + persistence)
- new kb-store-sqlite::eval module: record_eval_run_with_results (transactional), document_exists / chunk_exists probes
- fixtures/golden_queries.yaml: 5-entry KO+EN template
- tests: 13 pass (loader: parse, dup-id, missing chunk_id; runner: elapsed, snapshot, error capture, JSONL, determinism, persistence, config_snapshot)
- per_query.jsonl mirror written to runs_dir/<run_id>/
- temperature=0 + fixed seed → byte-identical per_query.jsonl (lexical)

deviations from spec (documented in code):
- run_id uses uuid::Uuid::now_v7().simple() (timestamp-ordered hex) instead of ULID — uuid already in workspace deps
- load_golden_set_validated kept #[cfg(test)] pub(crate) — production inlines validate_against_db
- snapshot fixture uses normalized projection (id/query/mode/first_hit) — full byte-determinism covered by separate test
- index_version in config_snapshot left null (composed per call by kb-app, not config-level)

deferred to follow-up:
- App reuse across queries (currently rebuilds App per query)
- expand_path hoist to kb-config (3 crate clones now)
- --max-queries flag (deferred to P5-2 per updated spec)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
altair823 reviewed 2026-05-01 18:11:40 +00:00
altair823 left a comment
Author
Owner

자동 리뷰 (spec compliance + code quality)

두 리뷰어 (spec compliance + code quality) 모두 APPROVE. 6 개 quick fix 는 push 전에 적용. 실제 머지 승인은 검토 후 직접 부탁.

Spec compliance 결과

  • 공개 API 시그니처 spec 56–98 라인과 100% 일치 (crates/kb-eval/src/types.rs:14-87, lib.rs:31-32).
  • 행동 계약 (load_golden_set, run_eval, KB_EVAL_GOLDEN env, OffsetDateTime now_utc, per-query 에러 캡쳐, aggregate_json "{}") 모두 검증.
  • 금지 의존성 부재 — grep 으로 kb-source-fs|kb-parse|kb-normalize|kb-chunk|kb-store-vector|kb-embed*|kb-search|kb-llm*|kb-rag|kb-tui|kb-desktop 모두 미사용 확인.
  • 5 건 fixture 동봉, 메트릭 계산은 P5-2 로 분리.

의도된 deviation (PR body 에 정리, spec doc 도 update):

  • run_id UUIDv7 vs ULID — 이미 워크스페이스 deps 인 uuid 활용.
  • load_golden_set_validated pub(crate) + #[cfg(test)] — 프로덕션은 validate_against_db 직접 호출.
  • index_version: null — Config 에 부재, kb-app 합성 값.
  • --max-queries deferred — spec 의 Risks/notes 갱신.

Code quality 결과

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

  • mint_run_id 의 dead .to_lowercase() 제거 (UUIDv7 simple 은 이미 lowercase hex).
  • 트랜잭션 1 회로 record_eval_run_with_results(&row, &results[]) — partial state 방지 (crates/kb-store-sqlite/src/eval.rs).
  • 미사용 thiserror dep 제거 (crates/kb-eval/Cargo.toml).
  • 중복 serde_json dev-dep 제거.
  • load_golden_set_validated pub(crate) + 테스트 모듈을 loader.rs 내부로 이동.
  • snapshot projection 확장: first_hit { chunk_id, doc_id, heading_path, score } 포함 (필드명 변경/타입 변경 캐치 가능).

후속 PR 로 미룸

  • App reuse: 매 쿼리마다 App::open 으로 SQLite + LanceDB + fastembed 재초기화. lexical 5 건 스모크는 OK 지만 hybrid 50 건 이상 시 setup 비용이 elapsed_ms 지배. P5-2 골든셋 측정 시작 전에 처리 권장.
  • expand_path 3 군데 (kb-store-vector::paths, kb-store-sqlite, kb-eval) 클론 — kb-config 로 hoist.
  • 쿼리 직렬 처리 — 병렬화는 P5 wrap-up.

검증 결과

  • cargo check -p kb-eval clean
  • cargo test -p kb-eval 13/13 pass
  • cargo clippy -p kb-eval --all-targets -- -D warnings clean
  • cargo check/test/clippy -p kb-store-sqlite 33/33 pass + clean
  • workspace cargo check clean (재빌드 후)

승인 후 머지 부탁.

## 자동 리뷰 (spec compliance + code quality) 두 리뷰어 (spec compliance + code quality) 모두 APPROVE. 6 개 quick fix 는 push 전에 적용. 실제 머지 승인은 검토 후 직접 부탁. ### Spec compliance 결과 - 공개 API 시그니처 spec 56–98 라인과 100% 일치 (`crates/kb-eval/src/types.rs:14-87`, `lib.rs:31-32`). - 행동 계약 (load_golden_set, run_eval, KB_EVAL_GOLDEN env, OffsetDateTime now_utc, per-query 에러 캡쳐, aggregate_json `"{}"`) 모두 검증. - 금지 의존성 부재 — `grep` 으로 `kb-source-fs|kb-parse|kb-normalize|kb-chunk|kb-store-vector|kb-embed*|kb-search|kb-llm*|kb-rag|kb-tui|kb-desktop` 모두 미사용 확인. - 5 건 fixture 동봉, 메트릭 계산은 P5-2 로 분리. 의도된 deviation (PR body 에 정리, spec doc 도 update): - `run_id` UUIDv7 vs ULID — 이미 워크스페이스 deps 인 uuid 활용. - `load_golden_set_validated` `pub(crate)` + `#[cfg(test)]` — 프로덕션은 `validate_against_db` 직접 호출. - `index_version: null` — Config 에 부재, kb-app 합성 값. - `--max-queries` deferred — spec 의 `Risks/notes` 갱신. ### Code quality 결과 Must-fix blocker 없음. 적용한 should-fix: - `mint_run_id` 의 dead `.to_lowercase()` 제거 (UUIDv7 simple 은 이미 lowercase hex). - 트랜잭션 1 회로 `record_eval_run_with_results(&row, &results[])` — partial state 방지 (`crates/kb-store-sqlite/src/eval.rs`). - 미사용 `thiserror` dep 제거 (`crates/kb-eval/Cargo.toml`). - 중복 `serde_json` dev-dep 제거. - `load_golden_set_validated` `pub(crate)` + 테스트 모듈을 `loader.rs` 내부로 이동. - snapshot projection 확장: `first_hit { chunk_id, doc_id, heading_path, score }` 포함 (필드명 변경/타입 변경 캐치 가능). ### 후속 PR 로 미룸 - App reuse: 매 쿼리마다 `App::open` 으로 SQLite + LanceDB + fastembed 재초기화. lexical 5 건 스모크는 OK 지만 hybrid 50 건 이상 시 setup 비용이 `elapsed_ms` 지배. P5-2 골든셋 측정 시작 전에 처리 권장. - `expand_path` 3 군데 (`kb-store-vector::paths`, `kb-store-sqlite`, `kb-eval`) 클론 — `kb-config` 로 hoist. - 쿼리 직렬 처리 — 병렬화는 P5 wrap-up. ### 검증 결과 - `cargo check -p kb-eval` clean - `cargo test -p kb-eval` 13/13 pass - `cargo clippy -p kb-eval --all-targets -- -D warnings` clean - `cargo check/test/clippy -p kb-store-sqlite` 33/33 pass + clean - workspace cargo check clean (재빌드 후) 승인 후 머지 부탁.
altair823 added 1 commit 2026-05-01 18:55:28 +00:00
- kb-app: promote App to pub, add open_with_config / search / ask methods
  so kb-eval (and future TUI) can amortize embedder + vector store + LLM
  cold-start across many queries on one App instance. Memoization is
  per-instance via OnceLock; *_with_config free functions delegate.
- kb-config: add canonical expand_path helper + 8 unit tests; drop the
  4 duplicate copies in kb-store-sqlite, kb-store-vector, kb-embed-local,
  kb-eval (net: -6 duplicate tests, +8 canonical tests).
- kb-eval: extract elapsed_ms_u32 helper, drop redundant tracing debug
  log (with_context already names path on error), replace dead-port :1
  test with bind-then-release ephemeral port.

Verified: cargo clippy --workspace --all-targets -D warnings clean,
all crate tests green (kb-app 12+3 ignored, kb-eval 11, kb-config 17,
kb-store-sqlite 33, kb-store-vector 7+8 AVX-gated, kb-embed-local 7+7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Owner

후속 리뷰 항목 반영 (e6ff9c4)

첫 리뷰의 deferred 항목 모두 적용:

App reuse — 가장 큰 perf 개선

  • kb-app: Apppub 로 승격, open_with_config / search / ask 메서드 추가.
  • 임베더 / 벡터 스토어 / LLM 모두 App 인스턴스의 OnceLock 으로 메모이제이션 — 한 번 init 후 모든 쿼리가 재사용.
  • kb-eval::run_eval_with_config 가 한 번만 App::open_with_config(cfg) 호출, 쿼리 루프는 app.search(...) / app.ask(...) 를 공유 인스턴스에 호출.
  • *_with_config 자유 함수는 한 줄 위임 (App::open_with_config(cfg)?.search(query)) — 기존 caller 무영향.

expand_path hoist

  • kb-config::paths::expand_path(raw, data_dir) -> PathBuf 신설 + 8 단위테스트 (data_dir 치환, XDG env, tilde, 절대경로 단락 등 분리 검증).
  • 4 군데 중복 (kb-store-sqlite, kb-store-vector, kb-embed-local, kb-eval) 모두 import 로 교체.
  • kb-eval 만 가지던 "data_dir 자체도 pre-expand" 시맨틱은 write_per_query_jsonl 에서 명시적 2 단계 (expand(data_dir, "") → expand(runs_dir, resolved)) 로 보존.

Nits

  • elapsed_ms_u32 헬퍼 추출 — 2 군데 반복 통합.
  • loader.rs 의 redundant tracing::debug!("loading...") 제거 (with_context 가 이미 path 명시).
  • tests/runner.rs:1 dead-port 를 bind-then-release 임시 포트로 교체 (TOCTOU race 가능하지만 실패 빠름).

검증

  • cargo clippy --workspace --all-targets -- -D warnings clean.
  • 영향받은 모든 크레이트 테스트 green (kb-app 12+3 ignored, kb-eval 11, kb-config 17, kb-store-sqlite 33, kb-store-vector 7+8 AVX-gated, kb-embed-local 7+7).

미반영 (P5 wrap-up 또는 별도)

  • 쿼리 병렬화 — 결정적 출력 순서 보장 + LLM 동시 호출 정책 검토 후 진행.
## 후속 리뷰 항목 반영 (e6ff9c4) 첫 리뷰의 deferred 항목 모두 적용: ### App reuse — 가장 큰 perf 개선 - `kb-app`: `App` 을 `pub` 로 승격, `open_with_config` / `search` / `ask` 메서드 추가. - 임베더 / 벡터 스토어 / LLM 모두 `App` 인스턴스의 `OnceLock` 으로 메모이제이션 — 한 번 init 후 모든 쿼리가 재사용. - `kb-eval::run_eval_with_config` 가 한 번만 `App::open_with_config(cfg)` 호출, 쿼리 루프는 `app.search(...)` / `app.ask(...)` 를 공유 인스턴스에 호출. - `*_with_config` 자유 함수는 한 줄 위임 (`App::open_with_config(cfg)?.search(query)`) — 기존 caller 무영향. ### `expand_path` hoist - `kb-config::paths::expand_path(raw, data_dir) -> PathBuf` 신설 + 8 단위테스트 (data_dir 치환, XDG env, tilde, 절대경로 단락 등 분리 검증). - 4 군데 중복 (`kb-store-sqlite`, `kb-store-vector`, `kb-embed-local`, `kb-eval`) 모두 import 로 교체. - kb-eval 만 가지던 "data_dir 자체도 pre-expand" 시맨틱은 `write_per_query_jsonl` 에서 명시적 2 단계 (`expand(data_dir, "") → expand(runs_dir, resolved)`) 로 보존. ### Nits - `elapsed_ms_u32` 헬퍼 추출 — 2 군데 반복 통합. - `loader.rs` 의 redundant `tracing::debug!("loading...")` 제거 (`with_context` 가 이미 path 명시). - `tests/runner.rs` 의 `:1` dead-port 를 `bind-then-release` 임시 포트로 교체 (TOCTOU race 가능하지만 실패 빠름). ### 검증 - `cargo clippy --workspace --all-targets -- -D warnings` clean. - 영향받은 모든 크레이트 테스트 green (`kb-app` 12+3 ignored, `kb-eval` 11, `kb-config` 17, `kb-store-sqlite` 33, `kb-store-vector` 7+8 AVX-gated, `kb-embed-local` 7+7). ### 미반영 (P5 wrap-up 또는 별도) - 쿼리 병렬화 — 결정적 출력 순서 보장 + LLM 동시 호출 정책 검토 후 진행.
altair823 merged commit 33ec13bad7 into main 2026-05-02 02:47:24 +00:00
altair823 deleted branch feat/p5-1-golden-fixture-runner 2026-05-02 02:47:25 +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#27