feat(p5-1): kb-eval crate — golden-fixture runner + eval persistence #27
Reference in New Issue
Block a user
Delete Branch "feat/p5-1-golden-fixture-runner"
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-1 구현. Golden YAML 코퍼스를 로드하고 각 쿼리를
kb-app(search/ask) 으로 돌려서eval_runs/eval_query_resultsSQLite 테이블 +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+ENfixtures/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 가 채울 자리).temperature=0+ 고정 seed → lexical 모드에서 두 번 실행 시per_query.jsonl바이트 동일.KB_EVAL_GOLDEN환경변수로 fixture 경로 override.테스트
cargo test -p kb-eval13/13 pass (loader 4 + runner 7 + lib unit 2).cargo test -p kb-store-sqlite33/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 를 호출.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-queriesflag: spec 을 update 해서 P5-2 로 deferred (rationale 명시).anyhow/uuid가 spec Allowed 목록에 명시 안 되어 있지만 워크스페이스 idiom 상 필요 — kb-app / kb-store-sqlite 도 동일.후속 PR 로 미룸 (이 PR 의 범위 밖)
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-eval3 군데 복���.kb-config로 hoist 필요.Spec 링크
tasks/p5/p5-1-golden-fixture-runner.mdTest plan
cargo check -p kb-evalcargo test -p kb-eval(13 pass)cargo clippy -p kb-eval --all-targets -- -D warningscargo check/test/clippy -p kb-store-sqlitegrep으로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
자동 리뷰 (spec compliance + code quality)
두 리뷰어 (spec compliance + code quality) 모두 APPROVE. 6 개 quick fix 는 push 전에 적용. 실제 머지 승인은 검토 후 직접 부탁.
Spec compliance 결과
crates/kb-eval/src/types.rs:14-87,lib.rs:31-32)."{}") 모두 검증.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모두 미사용 확인.의도된 deviation (PR body 에 정리, spec doc 도 update):
run_idUUIDv7 vs ULID — 이미 워크스페이스 deps 인 uuid 활용.load_golden_set_validatedpub(crate)+#[cfg(test)]— 프로덕션은validate_against_db직접 호출.index_version: null— Config 에 부재, kb-app 합성 값.--max-queriesdeferred — spec 의Risks/notes갱신.Code quality 결과
Must-fix blocker 없음. 적용한 should-fix:
mint_run_id의 dead.to_lowercase()제거 (UUIDv7 simple 은 이미 lowercase hex).record_eval_run_with_results(&row, &results[])— partial state 방지 (crates/kb-store-sqlite/src/eval.rs).thiserrordep 제거 (crates/kb-eval/Cargo.toml).serde_jsondev-dep 제거.load_golden_set_validatedpub(crate)+ 테스트 모듈을loader.rs내부로 이동.first_hit { chunk_id, doc_id, heading_path, score }포함 (필드명 변경/타입 변경 캐치 가능).후속 PR 로 미룸
App::open으로 SQLite + LanceDB + fastembed 재초기화. lexical 5 건 스모크는 OK 지만 hybrid 50 건 이상 시 setup 비용이elapsed_ms지배. P5-2 골든셋 측정 시작 전에 처리 권장.expand_path3 군데 (kb-store-vector::paths,kb-store-sqlite,kb-eval) 클론 —kb-config로 hoist.검증 결과
cargo check -p kb-evalcleancargo test -p kb-eval13/13 passcargo clippy -p kb-eval --all-targets -- -D warningscleancargo check/test/clippy -p kb-store-sqlite33/33 pass + clean승인 후 머지 부탁.
후속 리뷰 항목 반영 (
e6ff9c4)첫 리뷰의 deferred 항목 모두 적용:
App reuse — 가장 큰 perf 개선
kb-app:App을pub로 승격,open_with_config/search/ask메서드 추가.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_pathhoistkb-config::paths::expand_path(raw, data_dir) -> PathBuf신설 + 8 단위테스트 (data_dir 치환, XDG env, tilde, 절대경로 단락 등 분리 검증).kb-store-sqlite,kb-store-vector,kb-embed-local,kb-eval) 모두 import 로 교체.write_per_query_jsonl에서 명시적 2 단계 (expand(data_dir, "") → expand(runs_dir, resolved)) 로 보존.Nits
elapsed_ms_u32헬퍼 추출 — 2 군데 반복 통합.loader.rs의 redundanttracing::debug!("loading...")제거 (with_context가 이미 path 명시).tests/runner.rs의:1dead-port 를bind-then-release임시 포트로 교체 (TOCTOU race 가능하지만 실패 빠름).검증
cargo clippy --workspace --all-targets -- -D warningsclean.kb-app12+3 ignored,kb-eval11,kb-config17,kb-store-sqlite33,kb-store-vector7+8 AVX-gated,kb-embed-local7+7).미반영 (P5 wrap-up 또는 별도)