fix(cli): honor --config flag + improve search output legibility #20

Merged
altair823 merged 1 commits from fix/cli-config-flag-and-search-output into main 2026-05-01 13:26:11 +00:00
Owner

변경 요약

P3-5 머지 후 직접 워크스페이스 (/tmp/kb-smoke/, 6개 markdown — Rust/Python/Architecture/SQLite 주제, 한국어+영어 혼합)에 대해 CLI smoke를 돌리다 발견한 두 가지 이슈 핫픽스입니다.

발견된 문제

1. --config flag CLI에서 무시됨

kb-cli/src/main.rs:216Ingest arm이 cli.config로 SourceScope만 만들고, 그 후 kb_app::ingest(scope, summary_only) 호출은 내부적으로 Config::load(None)을 부릅니다 — ~/.config/kb/config.toml만 봄. search, list, inspect, doctor 모두 동일.

스모크 중 다음 명령:

$ target/debug/kb --config /tmp/kb-smoke/config.toml doctor
✓ config_loaded   /home/altair823/.config/kb/config.toml (defaults)
✓ data_dir_writable  /home/altair823/.local/share/kb

--config 패스를 완전히 건너뛰고 XDG 기본을 출력. 사용자는 KB_* 환경 변수로만 우회 가능했습니다.

2. Search 출력에서 RRF 점수가 "0.02"로 뭉개짐 + heading_path 누락

 1. 0.02  arch/rag-architecture.md
 2. 0.02  arch/rag-architecture.md
 3. 0.02  arch/rag-architecture.md

{:.2} 포맷이 RRF 합산 점수의 (0, ~0.033] 범위 (k_rrf=60 default 기준)를 모두 "0.02"로 잘라버려서 ranking signal 사라짐. 게다가 같은 문서에서 나온 다른 청크가 시각적으로 구분 안 됨.

수정

kb-app

  • *_with_config(Config, ...) 헬퍼 (P3-5에서 통합 테스트 seam으로 도���한 #[doc(hidden)] pub)를 정식 "config-explicit" API로 재정의. 모듈 doc-comment를 "test-only seam" 표현에서 "three callers: CLI --config, integration tests, TUI session"으로 업데이트. #[doc(hidden)]는 유지 — rustdoc은 깔끔하게 두고 cross-crate 호출은 허용.
  • doctor()doctor_with_config_path(Option<&Path>)으로 재작성. config_loaded 프로브가 실제 검사한 경로를 보고하고, --config가 존재하지 않거나 malformed 파일을 가리키면 hard error로 fail (defaults가 silently mask하지 않게). data_dir_writable은 로드된 config의 storage.data_dir을 (env override 적용 후) 풀어서 probe하므로 사용자 정의 경로가 출력에 그대로 반영됩니다. 기존 doctor() 시그니처는 None-passing wrapper로 남김.

kb-cli

  • ingest/search/list/inspect/doctor 모두 subcommand 시작에서 Config::load(cli.config.as_deref())으로 Config을 한 번 빌드한 뒤 *_with_config 변형에 직접 thread.
  • search printer 포맷을 {:.4}로 변경 (RRF 점수 범위 ≤ ~0.033이라 4자리는 가장 보수적 정확도 충족).
  • heading_path가 비어있지 않으면 > heading1 / heading2 접미사를 doc_path 뒤에 추가 — 같은 문서 내 다른 chunk를 시각적으로 분간.

수정 후 검증

$ target/debug/kb --config /tmp/kb-smoke/config.toml doctor
✓ config_loaded   /tmp/kb-smoke/config.toml
✓ data_dir_writable  /tmp/kb-smoke/data

$ target/debug/kb --config /tmp/kb-smoke/config.toml search \"ownership memory safety\" --mode hybrid --k 5
 1. 0.0164  rust/ownership.md  >  Rust 소유권 모델 / Borrow checker
 2. 0.0161  arch/rag-architecture.md  >  RAG architecture overview
 3. 0.0159  etc/sqlite-fts5.md  >  SQLite FTS5 lexical index / Triggers for staying in sync
 4. 0.0156  rust/ownership.md  >  Rust 소유권 모델 / Move semantics
 5. 0.0154  arch/rag-architecture.md  >  RAG architecture overview / Failure modes

워크스페이스 269 passed / 24 ignored / 0 failed. cargo clippy --workspace --all-targets -- -D warnings clean.

변경 파일

  • crates/kb-app/src/lib.rs (도큐먼트 + doctor_with_config_path 추가)
  • crates/kb-cli/src/main.rs (5개 subcommand에서 *_with_config* 호출 + 출력 포맷)

Out of scope

  • kb ask body는 P4-3 owner.
  • TUI / desktop은 P9.
  • fastembed model 캐시 process-level — CLI는 어차피 매 invocation 새 process라 의미 없고 P9 TUI 진입 시 OnceLock 효력 (P3-5에서 이미 처리).
## 변경 요약 P3-5 머지 후 직접 워크스페이스 (`/tmp/kb-smoke/`, 6개 markdown — Rust/Python/Architecture/SQLite 주제, 한국어+영어 혼합)에 대해 CLI smoke를 돌리다 발견한 두 가지 이슈 핫픽스입니다. ## 발견된 문제 ### 1. `--config` flag CLI에서 무시됨 `kb-cli/src/main.rs:216`의 `Ingest` arm이 `cli.config`로 SourceScope만 만들고, 그 후 `kb_app::ingest(scope, summary_only)` 호출은 내부적으로 `Config::load(None)`을 부릅니다 — `~/.config/kb/config.toml`만 봄. `search`, `list`, `inspect`, `doctor` 모두 동일. 스모크 중 다음 명령: ``` $ target/debug/kb --config /tmp/kb-smoke/config.toml doctor ✓ config_loaded /home/altair823/.config/kb/config.toml (defaults) ✓ data_dir_writable /home/altair823/.local/share/kb ``` `--config` 패스를 완전히 건너뛰고 XDG 기본을 출력. 사용자는 `KB_*` 환경 변수로만 우회 가능했습니다. ### 2. Search 출력에서 RRF 점수가 \"0.02\"로 뭉개짐 + heading_path 누락 ``` 1. 0.02 arch/rag-architecture.md 2. 0.02 arch/rag-architecture.md 3. 0.02 arch/rag-architecture.md ``` `{:.2}` 포맷이 RRF 합산 점수의 (0, ~0.033] 범위 (k_rrf=60 default 기준)를 모두 \"0.02\"로 잘라버려서 ranking signal 사라짐. 게다가 같은 문서에서 나온 다른 청크가 시각적으로 구분 안 됨. ## 수정 ### kb-app - `*_with_config(Config, ...)` 헬퍼 (P3-5에서 통합 테스트 seam으로 도���한 `#[doc(hidden)] pub`)를 정식 \"config-explicit\" API로 재정의. 모듈 doc-comment를 \"test-only seam\" 표현에서 \"three callers: CLI `--config`, integration tests, TUI session\"으로 업데이트. `#[doc(hidden)]`는 유지 — rustdoc은 깔끔하게 두고 cross-crate 호출은 허용. - `doctor()`를 `doctor_with_config_path(Option<&Path>)`으로 재작성. config_loaded 프로브가 실제 검사한 경로를 보고하고, `--config`가 존재하지 않거나 malformed 파일을 가리키면 hard error로 fail (defaults가 silently mask하지 않게). data_dir_writable은 로드된 config의 `storage.data_dir`을 (env override 적용 후) 풀어서 probe하므로 사용자 정의 경로가 출력에 그대로 반영됩니다. 기존 `doctor()` 시그니처는 `None`-passing wrapper로 남김. ### kb-cli - `ingest`/`search`/`list`/`inspect`/`doctor` 모두 subcommand 시작에서 `Config::load(cli.config.as_deref())`으로 Config을 한 번 빌드한 뒤 `*_with_config` 변형에 직접 thread. - search printer 포맷을 `{:.4}`로 변경 (RRF 점수 범위 ≤ ~0.033이라 4자리는 가장 보수적 정확도 충족). - heading_path가 비어있지 않으면 `> heading1 / heading2` 접미사를 doc_path 뒤에 추가 — 같은 문서 내 다른 chunk를 시각적으로 분간. ## 수정 후 검증 ``` $ target/debug/kb --config /tmp/kb-smoke/config.toml doctor ✓ config_loaded /tmp/kb-smoke/config.toml ✓ data_dir_writable /tmp/kb-smoke/data $ target/debug/kb --config /tmp/kb-smoke/config.toml search \"ownership memory safety\" --mode hybrid --k 5 1. 0.0164 rust/ownership.md > Rust 소유권 모델 / Borrow checker 2. 0.0161 arch/rag-architecture.md > RAG architecture overview 3. 0.0159 etc/sqlite-fts5.md > SQLite FTS5 lexical index / Triggers for staying in sync 4. 0.0156 rust/ownership.md > Rust 소유권 모델 / Move semantics 5. 0.0154 arch/rag-architecture.md > RAG architecture overview / Failure modes ``` 워크스페이스 269 passed / 24 ignored / 0 failed. `cargo clippy --workspace --all-targets -- -D warnings` clean. ## 변경 파일 - `crates/kb-app/src/lib.rs` (도큐먼트 + `doctor_with_config_path` 추가) - `crates/kb-cli/src/main.rs` (5개 subcommand에서 `*_with_config*` 호출 + 출력 포맷) ## Out of scope - `kb ask` body는 P4-3 owner. - TUI / desktop은 P9. - fastembed model 캐시 process-level — CLI는 어차피 매 invocation 새 process라 의미 없고 P9 TUI 진입 시 OnceLock 효력 (P3-5에서 이미 처리).
altair823 added 1 commit 2026-05-01 12:48:09 +00:00
Two issues surfaced during the post-P3-5 manual smoke test against a
six-document workspace:

1. --config flag was silently ignored. kb-cli read cli.config only
   while building SourceScope inside the Ingest arm, then called
   kb_app::ingest(scope, summary_only) which internally re-loads
   Config::load(None) — falling back to ~/.config/kb/config.toml
   regardless of what the user passed. Same pattern in search,
   list, inspect, doctor. Users had to rely on KB_* env vars to
   point at a non-default config.

2. Search output collapsed RRF hybrid scores to "0.02" because
   `{:.2}` truncated the (0, 0.033]-bounded fused score, and
   chunks from the same document showed up as identical lines
   ("3. 0.02  arch/rag-architecture.md") since heading_path was
   never printed.

Fix:

- kb-app: doctor/ingest/search/list/inspect already had
  *_with_config(Config, ...) seams introduced for integration tests
  (#[doc(hidden)] pub). Repurpose them as the official "config-explicit"
  API — kb-cli now builds the Config once via
  Config::load(cli.config.as_deref()) at the top of every subcommand
  and threads it into the *_with_config variant. Module doc-comment
  updated to reflect three callers (CLI --config, integration tests,
  TUI session) instead of "test-only seam".
- kb-app: doctor() rewritten as doctor_with_config_path(Option<&Path>)
  that respects an explicit path. config_loaded probe now reports the
  actual path checked, returning a clear hard error if --config points
  at a non-existent or malformed file (defaults would silently mask
  user intent). data_dir_writable resolves storage.data_dir from the
  loaded config (with env overrides applied via Config::apply_env) so
  --config users see their custom paths reflected. Original doctor()
  signature kept as a None-passing wrapper.
- kb-cli: ingest/search/list/inspect/doctor each call the
  *_with_config* companion. Search printer switches to {:.4} score
  formatting (RRF hybrid range bounded by ~2/k_rrf ≈ 0.033 at k_rrf=60
  default) and appends `> head1 / head2` when heading_path is non-
  empty so chunks from the same document are visually distinguishable.

Verified manually:
- `kb --config /tmp/kb-smoke/config.toml doctor` reports the
  custom config path + custom data_dir, not the XDG defaults.
- `kb --config /tmp/kb-smoke/config.toml search "..." --mode hybrid`
  returns hits with distinct 4-digit scores and heading paths
  ("rust/ownership.md > Rust 소유권 모델 / Borrow checker").

Workspace 269 passed / 24 ignored / 0 failed; cargo clippy
--workspace --all-targets -- -D warnings clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 reviewed 2026-05-01 12:48:52 +00:00
claude-reviewer-01 left a comment
Member

P3-5 핫픽스 코드 리뷰 — 셀프 머지 게이트로 인해 COMMENT only.

P3-5 직후 직접 워크스페이스 (/tmp/kb-smoke/, 6개 markdown — 한국어 + 영어 mixed corpus) 대상으로 CLI smoke를 돌리다 두 이슈를 발견하고 같은 PR에서 닫았습니다:

  1. --config flag가 모든 subcommand (ingest/search/list/inspect/doctor)에서 silent하게 무시되고 XDG default로 fallback. 사용자 우회는 KB_* env vars만 가능했음.
  2. RRF 합산 점수가 (0, ~0.033] 범위인데 search printer가 {:.2} 포맷이라 모든 hit이 "0.02"로 뭉개졌고, heading_path 미표시로 같은 문서 내 다른 chunk 분간 불가능.

해결:

  • P3-5에서 통합 테스트 seam으로 도입했던 #[doc(hidden)] pub fn *_with_config(Config, ...)을 정식 "config-explicit" API로 의미 확장. kb-cli가 매 subcommand에서 Config::load(cli.config.as_deref())으로 Config을 한 번 빌드한 뒤 *_with_config 변형에 직접 threading.
  • doctor()doctor_with_config_path(Option<&Path>)으로 재작성. --config가 없거나 malformed면 hard fail해서 "config_loaded ✓ defaults"의 거짓 통과 방지.
  • search printer를 {:.4} + > heading1 / heading2 suffix.

실제 출력 변화:

이전: 1. 0.02  arch/rag-architecture.md
이후: 1. 0.0164  rust/ownership.md  >  Rust 소유권 모델 / Borrow checker

워크스페이스 269 passed / 24 ignored / 0 failed. clippy clean. inline 코멘트는 모두 잘 만든 결정에 대한 노트입니다. 머지 진행해도 됩니다.

P3-5 핫픽스 코드 리뷰 — 셀프 머지 게이트로 인해 COMMENT only. P3-5 직후 직접 워크스페이스 (`/tmp/kb-smoke/`, 6개 markdown — 한국어 + 영어 mixed corpus) 대상으로 CLI smoke를 돌리다 두 이슈를 발견하고 같은 PR에서 닫았습니다: 1. `--config` flag가 모든 subcommand (ingest/search/list/inspect/doctor)에서 silent하게 무시되고 XDG default로 fallback. 사용자 우회는 `KB_*` env vars만 가능했음. 2. RRF 합산 점수가 (0, ~0.033] 범위인데 search printer가 `{:.2}` 포맷이라 모든 hit이 \"0.02\"로 뭉개졌고, heading_path 미표시로 같은 문서 내 다른 chunk 분간 불가능. 해결: - P3-5에서 통합 테스트 seam으로 도입했던 `#[doc(hidden)] pub fn *_with_config(Config, ...)`을 정식 \"config-explicit\" API로 의미 확장. kb-cli가 매 subcommand에서 `Config::load(cli.config.as_deref())`으로 Config을 한 번 빌드한 뒤 *_with_config 변형에 직접 threading. - `doctor()`를 `doctor_with_config_path(Option<&Path>)`으로 재작성. `--config`가 없거나 malformed면 hard fail해서 \"config_loaded ✓ defaults\"의 거짓 통과 방지. - search printer를 `{:.4}` + `> heading1 / heading2` suffix. 실제 출력 변화: ``` 이전: 1. 0.02 arch/rag-architecture.md 이후: 1. 0.0164 rust/ownership.md > Rust 소유권 모델 / Borrow checker ``` 워크스페이스 269 passed / 24 ignored / 0 failed. clippy clean. inline 코멘트는 모두 잘 만든 결정에 대한 노트입니다. 머지 진행해도 됩니다.
@@ -149,3 +157,2 @@
/// Test-only seam — kb-cli must call the public free function
/// ([`ingest`]), not this. See module docs.
/// Config-explicit variant — bypasses [`load_config`] when the

P3-5의 #[doc(hidden)] pub fn *_with_config을 test seam에서 "config-explicit API"로 의미 확장한 결정이 정확합니다 — --config 우회를 위해 새 함수를 추가하기보다 이미 존재하는 seam을 정식 cross-crate API로 promote. #[doc(hidden)]은 유지해 rustdoc은 깔끔하지만 kb-cli 같은 workspace 내 caller는 호출 가능. 의미 변화를 module doc-comment에 명시한 점도 좋습니다.

P3-5의 `#[doc(hidden)] pub fn *_with_config`을 test seam에서 "config-explicit API"로 의미 확장한 결정이 정확합니다 — `--config` 우회를 위해 새 함수를 추가하기보다 이미 존재하는 seam을 정식 cross-crate API로 promote. `#[doc(hidden)]`은 유지해 rustdoc은 깔끔하지만 kb-cli 같은 workspace 내 caller는 호출 가능. 의미 변화를 module doc-comment에 명시한 점도 좋습니다.
@@ -840,2 +877,4 @@
None => kb_config::Config::xdg_data_dir(),
};
let writable = (|| -> anyhow::Result<()> {
std::fs::create_dir_all(&data_dir)?;

doctor_with_config_path--config <path>가 존재하지 않을 때 silent default fallback이 아니라 hard error로 실패. defaults가 사용자 의도를 가리는 시나리오를 차단 — kb doctor --config wrong.toml이 "config_loaded ✓ ~/.config/kb/config.toml (defaults)"로 거짓 통과하지 않습니다.

`doctor_with_config_path`이 `--config <path>`가 존재하지 않을 때 silent default fallback이 아니라 hard error로 실패. defaults가 사용자 의도를 가리는 시나리오를 차단 — `kb doctor --config wrong.toml`이 "config_loaded ✓ ~/.config/kb/config.toml (defaults)"로 거짓 통과하지 않습니다.

data_dir_writable이 로드된 config의 storage.data_dir을 풀어서 probe + env override까지 적용 후 expand_tilde. --config 사용자가 "내가 지정한 data_dir이 정말 검증되는가"를 즉시 확인 가능. P3-5의 search/ingest path와 동일한 precedence를 doctor에서도 재현.

data_dir_writable이 로드된 config의 `storage.data_dir`을 풀어서 probe + env override까지 적용 후 expand_tilde. `--config` 사용자가 "내가 지정한 data_dir이 정말 검증되는가"를 즉시 확인 가능. P3-5의 search/ingest path와 동일한 precedence를 doctor에서도 재현.
@@ -289,2 +293,3 @@
for h in &hits {
println!("{:>2}. {:.2} {}", h.rank, h.retrieval.fusion_score, h.doc_path.0);
// Show 4-digit score so RRF fused scores (bounded
// ~00.033 for k_rrf=60) don't all collapse to "0.02".

search printer 변경 두 가지 모두 사용자 입장에서 즉시 체감되는 개선:

  1. {:.4} — RRF 합산 점수가 (0, ~2/k_rrf] 즉 k_rrf=60 default에서 ≤ 0.033이라 {:.2}는 모든 hit을 "0.02"로 뭉개버렸습니다. 4자리로 ordering signal 보존.
  2. > head1 / head2 접미사 — 같은 문서에서 나온 다른 chunk가 " arch/rag-architecture.md"로만 표시되어 분간 불가능했던 문제 해결. heading_path가 비어있으면 접미사 생략해 lexical short-doc 케이스에서 noise 안 만듦.
search printer 변경 두 가지 모두 사용자 입장에서 즉시 체감되는 개선: 1. `{:.4}` — RRF 합산 점수가 (0, ~2/k_rrf] 즉 k_rrf=60 default에서 ≤ 0.033이라 `{:.2}`는 모든 hit을 "0.02"로 뭉개버렸습니다. 4자리로 ordering signal 보존. 2. `> head1 / head2` 접미사 — 같은 문서에서 나온 다른 chunk가 " arch/rag-architecture.md"로만 표시되어 분간 불가능했던 문제 해결. heading_path가 비어있으면 접미사 생략해 lexical short-doc 케이스에서 noise 안 만듦.
altair823 merged commit 38ff886c37 into main 2026-05-01 13:26:11 +00:00
altair823 deleted branch fix/cli-config-flag-and-search-output 2026-05-01 13:26:28 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#20