chore: PR #235 회차 3 리뷰 반영 — 판정 보류 분기 + vector_store env

3회차 리뷰가 2회차 지적 8건이 동작 수준에서 해결됐음을 확인하고 머지
가능으로 결론냈다. 남은 LOW 중 값이 있는 셋을 반영한다.

1) 마이그레이션 이력을 못 읽었을 때 파괴적 조언으로 떨어졌다

   `Some(Some((None, Ok(어긋남))))` — 버전은 모르는데 표본은 읽힌 경우 —
   가 `ok: false` + "`kebab reset`" 분기로 갔다. 버전을 모르는 것은 KB 를
   날리라고 말할 근거가 못 된다. 판정을 "V016 이상임이 확인된 스토어"로
   한정하고, 이력을 못 읽으면 `ok: true` + "판정 보류" 로 둔다. 실제
   도달 경로는 사실상 없지만(chunks_fts 가 있으면 이력도 있다) 분기의
   기본값이 파괴적인 쪽인 게 문제였다.

2) "점검하지 못했다" hint 가 가장 흔한 원인을 안 적었다

   2회차에서 고친 HIGH — 이전 버전 doctor 가 남긴 0바이트 kebab.sqlite —
   가 정확히 이 분기로 온다. 테이블이 없어 읽기에 실패한다. "아직 색인
   전이거나" 를 앞에 넣었다.

3) vector_store 블록이 아직 env 를 안 얹고 있었다

   2회차에서 fts_shadow 에 대해 고친 것과 같은 결함이 #234 에서 들어온
   바로 아래 블록에 그대로 남아 있었다. 정보성 체크라 종료 코드에는
   영향이 없지만 detail 이 엉뚱한 디렉토리를 가리킨다. 같이 맞췄다.

미반영: shadow 의 chunk_id 가 NULL 이면 분자에서 빠지는 과소 계수 —
트리거가 NOT NULL 컬럼을 미러링하므로 도달 불가이고 방향도 오탐이 아닌
과소 쪽이다. 새 테스트가 `KEBAB_STORAGE_DATA_DIR` 이 export 된 셸에서
실패하는 것은 점검이 제대로 동작하는 결과라, 원인을 빨리 찾도록 테스트
주석에 적어 두는 선에서 끝냈다.

실측 확인: KB 없음 / V015 / V016 / env override 네 경로 모두 의도한 문구가
나오고, KB 없는 경로는 파일을 남기지 않는다. env override 를 주면
fts_shadow 와 vector_store 가 같은 디렉토리를 본다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
This commit is contained in:
2026-08-16 20:17:15 +09:00
parent 8dbfa89b57
commit 8537e5765e
2 changed files with 52 additions and 29 deletions

View File

@@ -499,36 +499,47 @@ pub fn doctor_with_config_path(
true,
"점검하지 못했다".to_string(),
Some(
"SQLite 를 열거나 읽지 못했다 — 다른 kebab 프로세스가 쓰는 중이거나 \
DB 가 손상됐을 수 있다"
"SQLite 를 열거나 읽지 못했다 — 아직 색인 전이거나, 다른 kebab \
프로세스가 쓰는 중이거나, DB 가 손상됐을 수 있다"
.to_string(),
),
),
// Pre-V016 stores can be misaligned already: V009's backfill
// inserted without an explicit rowid, so a store with gaps in
// `chunks.rowid` at that moment got a densely-numbered shadow.
// That is harmless there — deletes still address rows by
// chunk_id. Applying V016 repopulates with explicit rowids and
// fixes it, so the advice is "migrate", never "wipe".
Some(Some((Some(v), _))) if v < V016 => (
true,
format!("V016 적용 전 (현재 V{v:03}) — 마이그레이션 후 점검된다"),
None,
),
Some(Some((_, Ok((checked, 0))))) => (
true,
format!("chunks_fts rowid 정렬 정상 (양끝 {checked}표본)"),
None,
),
Some(Some((_, Ok((checked, bad))))) => (
false,
format!("chunks_fts rowid 정렬 어긋남 — 표본 {checked}행 중 {bad}행 불일치"),
Some(
"삭제가 엉뚱한 FTS 행을 지우고 있다. `kebab reset` 후 재색인으로 \
인덱스를 다시 만들어라"
.to_string(),
// A verdict is only rendered for a store known to be V016 or
// later. Pre-V016 stores can be misaligned already — V009's
// backfill inserted without an explicit rowid, so a store
// with gaps in `chunks.rowid` at that moment got a densely
// numbered shadow — and there it is harmless, because
// deletes still address rows by chunk_id. Applying V016
// repopulates with explicit rowids and fixes it, so the
// advice is "migrate", never "wipe". An unreadable history
// takes the same branch: not knowing the version is not
// grounds for telling someone to destroy their KB.
Some(Some((ver, Ok((checked, bad))))) => match ver {
Some(v) if v >= V016 && bad > 0 => (
false,
format!("chunks_fts rowid 정렬 어긋남 — 표본 {checked}{bad}행 불일치"),
Some(
"삭제가 엉뚱한 FTS 행을 지우고 있다. `kebab reset` 후 재색인으로 \
인덱스를 다시 만들어라"
.to_string(),
),
),
),
Some(v) if v >= V016 => (
true,
format!("chunks_fts rowid 정렬 정상 (양끝 {checked}행 표본)"),
None,
),
Some(v) => (
true,
format!("V016 적용 전 (현재 V{v:03}) — 마이그레이션 후 점검된다"),
None,
),
None => (
true,
"판정 보류 — 마이그레이션 이력을 읽지 못했다".to_string(),
Some("kebab 이 만든 DB 가 아닐 수 있다".to_string()),
),
},
};
checks.push(DoctorCheck {
name: "fts_shadow".to_string(),
@@ -540,9 +551,16 @@ pub fn doctor_with_config_path(
// vector_store — Lance fragment / version-history health (issue #230).
{
let cfg = loaded_cfg
.clone()
.unwrap_or_else(kebab_config::Config::defaults);
// Same env precedence as the two checks above — this block
// landed in #234 reading the file-only config, so a
// KEBAB_STORAGE_DATA_DIR user got stats for the wrong directory.
let cfg = match loaded_cfg.as_ref() {
Some(c) => {
let env: std::collections::HashMap<String, String> = std::env::vars().collect();
c.clone().apply_env(&env)
}
None => kebab_config::Config::defaults(),
};
let data_dir = kebab_config::expand_path(&cfg.storage.data_dir, "");
let vector_dir =
kebab_config::expand_path(&cfg.storage.vector_dir, &data_dir.to_string_lossy());

View File

@@ -123,6 +123,11 @@ fn doctor_does_not_create_a_store_where_none_exists() {
/// trigger still addresses rows by chunk_id, so the drift is harmless.
/// Telling that user to `kebab reset` would destroy a healthy KB over a
/// condition that applying the migration fixes by itself.
///
/// Note: doctor applies env overrides with the same precedence as
/// `Config::load`, so `KEBAB_STORAGE_DATA_DIR` exported in the shell
/// redirects this check away from the temp store and fails the test.
/// That is the check behaving correctly, not a broken test — unset it.
#[test]
fn doctor_does_not_call_a_pre_v016_store_broken() {
let dir = tempfile::tempdir().unwrap();