---
title: v0.20.x — 한국어 morphological tokenizer (Bug #8 follow-up) — plan
created: 2026-05-28
status: accepted
spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md
critic_r1: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec-critic-r1.md
critic_r2: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec-critic-r2.md
parent_handoff: docs/superpowers/handoffs/2026-05-28-v0.20.x-c-korean-morphological-tokenizer-handoff.md
branch: feat/korean-morphological-tokenizer
step_count: 11
commit_count: 10
---

# v0.20.x — 한국어 morphological tokenizer — implementation plan

> **For agentic workers:** REQUIRED SUB-SKILL — `superpowers:executing-plans` (또는 `superpowers:subagent-driven-development`). 각 Step 은 단일 commit boundary 를 가지고, 마지막 Step (S11 verify-only) 은 commit 없음. step-level checkbox `- [ ]` syntax 사용. spec 의 frozen contract 를 보존하며, 본 plan 은 implementation 일정만 다룬다.

**Goal.** V007 trigram FTS5 tokenizer 의 한계 (2-char 한국어 query 0-hit) 를 해결한다. lindera + lindera-dict-ko-dic 기반 형태소 분석기를 도입하고, 한국어 chunk 의 분해된 morpheme 을 별 column `tokenized_korean_text` 에 pre-fill 한 뒤 FTS5 의 `unicode61` tokenizer 가 공백 경계로 token 화하도록 구성한다. V009 migration 으로 schema 를 한 번 교체하고, 첫 부팅 시 자동 eager backfill 로 기존 KB 의 모든 chunk 를 재-tokenize 한다 (사용자 재-ingest 불필요).

**Architecture.** V009 migration 은 schema 만 변경 (column ADD, chunks_fts DROP+재정의, triggers 갱신). lindera 호출은 Rust 측 (`kebab-chunk::tokenize_korean_morphological`) 에서 일어나며, ingest pipeline 의 신규 chunk INSERT 시 동일 transaction 안에 `tokenized_korean_text` 를 pre-fill 한다. 기존 chunk 의 backfill 은 `App::open_with_config` 의 first-boot hook 에서 trigger 되어 chunks 전체에 대해 UPDATE → chunks_au trigger 가 chunks_fts 를 자동 재-index. `lexical_index_version` 은 V007 → V009 로 bump 되어 사용자 visible `index_version` 값이 변경된다. 영어 substring 매칭은 V002 동작 (whole-token only) 으로 회귀 — release notes 에 정직히 기술된다.

**Tech stack.** Rust 2024, rusqlite (existing), refinery (existing), `lindera = "0.32"` (또는 시점 latest stable) workspace dep 신규, `lindera-dict-ko-dic` per-crate feature dep 신규. CARGO_TARGET_DIR=/build/out/cargo-target/target, `-j 4` default.

**Spec contract.** `docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md` (668 line, ACCEPT, frozen). critic R2 의 9 finding traceability 매트릭스 + Appendix A~D 포함. 본 plan 은 spec 의 §1–§12 를 8 step 으로 분해 + 3 보조 step (test / docs / release).

---

## File map

**Modify (12):**
- `Cargo.toml` — workspace `[workspace.dependencies]` 에 `lindera`, `lindera-dict-ko-dic` 추가.
- `crates/kebab-chunk/Cargo.toml` — `lindera` + `lindera-dict-ko-dic` per-crate dep 추가 (또는 feature gate). chunk crate 가 한국어 tokenizer 의 owner.
- `crates/kebab-chunk/src/lib.rs` — `tokenize_korean_morphological(text: &str) -> Option<String>` helper 신규 + chunk builder pipeline 에 호출 추가.
- `crates/kebab-store-sqlite/src/store.rs` — chunk INSERT path 에 `tokenized_korean_text` column 추가, backfill API `backfill_tokenized_korean_text(progress_cb)` 신규.
- `crates/kebab-store-sqlite/tests/fts.rs` — 기존 V007 verbatim test rename + 신규 V009 verbatim test + 한국어 morphological hit test.
- `crates/kebab-search/src/lexical.rs` — `build_match_string()` 의 trigram-specific 분기 단순화 (보존 정책 결정).
- `crates/kebab-app/src/app.rs` — `short_query_hint()` 함수 + 2 호출 site 제거, `lexical_index_version()` 의 source 갱신.
- `crates/kebab-app/src/lib.rs` — `short_query_hint` re-export 제거, `App::open_with_config` 에 first-boot eager backfill hook 추가.
- `crates/kebab-tui/src/app.rs`, `crates/kebab-tui/src/search.rs`, `crates/kebab-tui/src/run.rs` — `short_query_hint` 필드 + 호출 제거 (TUI 측 cascade).
- `crates/kebab-cli/tests/wire_search_response.rs` — `search_plain_emits_short_query_hint_to_stderr` test 삭제 또는 inverted (hint 가 더 이상 emit 안 됨).
- `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` — §5.5 chunks_fts 블록을 V009 의 unicode61 + CASE expression 트리거 본문으로 갱신 (verbatim diff-check 대상).
- `README.md`, `HANDOFF.md`, `docs/ARCHITECTURE.md`, `integrations/claude-code/kebab/SKILL.md`, `tasks/HOTFIXES.md` — surface cascade (S9 묶음).

**Create (3):**
- `migrations/V009__fts_korean_morphological.sql` — column ADD + chunks_fts re-create + triggers + corpus_revision bump (Section 5 verbatim).
- `crates/kebab-app/tests/search_korean.rs` — end-to-end 한국어 2-char query integration test.
- (선택) `crates/kebab-chunk/tests/tokenize_korean.rs` — lindera tokenize 단위 테스트.

**Do NOT modify:**
- `migrations/V007__fts_trigram.sql` — historical migration, 그대로 유지 (replay path 보존).
- `migrations/V002__fts.sql` — V007 가 이미 design verbatim 비교 대상에서 제외했고, V009 도 동일.
- spec 의 ACCEPT 본문 — 본 plan 은 spec 의 implementation 일정만 다룬다. 후속 deviation 은 `tasks/HOTFIXES.md` 에 기록.
- `docs/wire-schema/v1/*.schema.json` — wire schema shape 불변 (§11.3, hit ordering 만 변화).

---

## 1. Scope summary

이 plan 이 cover 하는 spec section:

- **§4 Design Decision** (Option A 선택 rationale).
- **§5 Migration Cascade (V009)** — DDL + corpus_revision + CI diff-check rename.
- **§6 Tokenizer Integration** — lindera 의존성 + pre-tokenize 우회 + invariant + fallback.
- **§7 Query Path** — lexical.rs 정리 + short_query_hint 제거 + surface cascade.
- **§8 Backward Compatibility + Eager Backfill** — first-boot hook.
- **§9 Acceptance Criteria** — 신규 단위/통합 test.
- **§10 Risks + Evidence** — Appendix C/D 의 estimate 와 실측 reconciliation.
- **§11 Version Cascade** — `lexical_index_version` bump.
- **§12 Release Strategy** — v0.20.1 patch release + dogfood verification.

**제외 (별 PR / 별 follow-up)**:

- **일본어/중국어 morphological tokenizer** — spec §10.4. 본 plan 은 한국어 ko-dic 만. 동일 패턴 재사용은 별 plan.
- **Eval golden baseline regenerate** — spec §11.3 결정상 본 PR scope 에 포함하나 별 step (S8) 으로 분리. crate `kebab-eval` 의 goldens.csv 가 변경되므로 별 commit 권장.
- **Streaming progress for eager backfill** — spec §10.3 mitigation 의 "background job + streaming feedback" 은 v0.20.x 의 후속 P5 sub-item 으로 미룬다.

---

## 2. Step decomposition

8 implementation step + 3 보조 step = 총 11 step, 10 commit (S11 verify-only). spec 의 권장 11 step (S1~S11) 을 본 plan 에서는 의존성 그룹에 따라 재배치하고, S2 (design §5.5) + S1 (migration) + S8 (test) 를 한 commit 단위로 묶는다.

### Step 1 — V009 migration + design §5.5 갱신 + CI diff-check rename

**Implements:** §5.1, §5.2, §5.3. **Commit 1/10.**

**Spec sections covered:** §5.1 DDL skeleton, §5.2 corpus_revision bump, §5.3 design + CI diff-check 갱신.

**Files to modify:**
- Create `migrations/V009__fts_korean_morphological.sql`.
- Modify `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` (§5.5 블록).
- Modify `crates/kebab-store-sqlite/tests/fts.rs` (V007 verbatim test → V009 rename).

**Implementation outline:**

- [ ] **1.1 V009 migration 파일 작성** — spec §5.1 의 DDL skeleton 을 verbatim 복사한다. 4 영역으로 구성: (a) `ALTER TABLE chunks ADD COLUMN tokenized_korean_text TEXT;`, (b) 기존 chunks_fts + 3 trigger DROP, (c) 신규 chunks_fts (unicode61) + chunks_ai/ad/au trigger 재정의 (CASE expression 포함), (d) `INSERT INTO chunks_fts ... SELECT ... FROM chunks;` (기존 row 재-index), (e) 마지막 줄 `UPDATE kv SET v = v + 1 WHERE k = 'corpus_revision';`.
- [ ] **1.2 §5.5 verbatim 마커** — V007 의 design verbatim block 마커 (`-- ── BEGIN §5.5 verbatim block ──` / `-- ── End §5.5 verbatim block ──`) 와 동일한 marker 를 V009 migration 의 (c) 블록 주위에 삽입. CI diff-check 가 마커로 슬라이스 추출하므로 형식 일관성 필수.
- [ ] **1.3 design §5.5 갱신** — `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §5.5 의 chunks_fts 정의 + 3 trigger 본문을 V009 의 unicode61 + CASE expression 형태로 다시 쓴다. critic R1 finding #5 + critic R2 의 verbatim scope (CASE expression 포함) 따라 trigger body 전체가 verbatim diff 대상. spec drafter 가 §5.3 에서 명시한 "whitespace-normalized string compare of the §5.5 block, scope = CASE expression 포함" 정의 적용.
- [ ] **1.4 V007 verbatim test rename** — `crates/kebab-store-sqlite/tests/fts.rs:407` 의 `fts_v007_matches_design_section_5_5_verbatim` 를 `fts_v009_matches_design_section_5_5_verbatim` 로 rename. 함수 body 의 marker 검색 대상도 `migrations/V007__fts_trigram.sql` → `migrations/V009__fts_korean_morphological.sql` 로 변경. fts.rs:402-405 의 doc-comment 갱신 ("V007 stays for historical replay, V009 is the source of truth"). V002 + V007 모두 더 이상 design 비교 대상 아님 명시.
- [ ] **1.5 corpus_revision SQL 의 정상 동작 검증** — 신규 단위 test `v009_bumps_corpus_revision` 를 fts.rs 에 추가. TempDir SqliteStore 열기 → migration apply → `SELECT v FROM kv WHERE k='corpus_revision'` 가 V008 적용 시점 대비 +1 이상.
- [ ] **1.6 Verify.** `cargo test -p kebab-store-sqlite --test fts -j 4` 가 통과. `fts_v009_matches_design_section_5_5_verbatim` 와 `v009_bumps_corpus_revision` 모두 hit. 기존 V002/V007 test 는 syntactic 만 유지된 상태로 pass.

**Acceptance criteria:**
- `cargo test -p kebab-store-sqlite --test fts fts_v009_matches_design_section_5_5_verbatim -j 4` → exit 0, assertion 통과 (V009 의 §5.5 block 이 design §5.5 와 whitespace-normalized string-equal).
- `cargo test -p kebab-store-sqlite --test fts v009_bumps_corpus_revision -j 4` → exit 0, corpus_revision 값이 strict-monotonic 증가.
- `grep -c "fts_v007_matches_design_section_5_5_verbatim" crates/kebab-store-sqlite/tests/fts.rs` → `0` (rename 완료 확인).
- `cargo clippy -p kebab-store-sqlite --all-targets -j 4 -- -D warnings` → clean.

---

### Step 2 — lindera dependency + license 검증

**Implements:** §6.1, §10.1. **Commit 2/10.**

**Spec sections covered:** §6.1 라이브러리 선정, §10.1 라이센스 검증 (Appendix D).

**Files to modify:**
- `Cargo.toml` (workspace) — `[workspace.dependencies]` 에 `lindera = "0.32"` 와 `lindera-dict-ko-dic = "0.32"` 추가. version pin 은 cargo search 결과의 stable latest 로 결정.
- `crates/kebab-chunk/Cargo.toml` — per-crate dep 에 `lindera = { workspace = true }` + `lindera-dict-ko-dic = { workspace = true, features = ["embedded-dict"] }` 추가. dict 의 정확한 feature name 은 lindera-dict-ko-dic 의 Cargo.toml metadata 확인 후 결정.
- `crates/kebab-app/Cargo.toml` — `[features]` 에 `fts_korean_morphological = ["dep:lindera"]` + `default = ["fts_korean_morphological"]` 등록 (spec §6.3 feature gate).

**Implementation outline:**

- [ ] **2.1 cargo search 로 stable latest 확인** — `cargo search lindera lindera-dict-ko-dic --limit 5`. spec §6.1 의 라이브러리 선정 의도와 일치하는 minor version 선택. crate 가 multi-dictionary feature flag (`ko-dic`, `embedded-dict`) 를 갖고 있다면 그것을 활성화.
- [ ] **2.2 Workspace + per-crate dep 추가** — `Cargo.toml` 의 `[workspace.dependencies]` 에 두 dep 추가 (version pin, registry). `crates/kebab-chunk/Cargo.toml` 의 `[dependencies]` 에 `lindera = { workspace = true }` 와 `lindera-dict-ko-dic = { workspace = true, features = [...] }` 추가.
- [ ] **2.3 cargo build 의 dict 다운로드 정책 검증** — spec §10 risks, Appendix C 의 "release binary 에 embed +15-25 MB" 를 만족시키려면 dict 가 build-time 에 embedded 되어야 한다. `cargo build -p kebab-chunk --release 2>&1 | tail -20` 으로 dict download / decompression step 가 성공하는지 확인. 실패 시 (network egress 차단 환경) lindera-dict-ko-dic 의 `embedded-dict` 또는 동급 feature 가 정확히 어떤 이름인지 crate docs 참조 후 수정. 최악 fallback: `build.rs` proactive download 또는 vendored crate (별 follow-up).
- [ ] **2.4 license fingerprint 확인** — `cargo tree --depth 1 -p lindera -p lindera-dict-ko-dic` 로 두 crate 의 SPDX 확인. Appendix D 의 가정 (`MIT OR Apache-2.0` for lindera, `Apache-2.0` for dict) 와 일치하는지 정직히 비교. 불일치 시 plan 의 후속 step (S9 docs sync) 의 release notes 표현을 조정.
- [ ] **2.5 deny.toml 부재 확인** — 현재 repo 에 `deny.toml` 이 없음 (probe 결과 `ls deny.toml` → no such file). spec §10.1 + Appendix D 가 명시한 `cargo deny check` 절차는 본 plan 에서 **deferred**. 대신 (a) `cargo tree` 출력의 SPDX 를 `tasks/HOTFIXES.md` 에 evidence 로 기록, (b) spec drafter 의 CC BY-SA 라이센스 미포함 fail-fast 정책은 cargo tree 의 license field 수동 확인으로 대체. follow-up: `cargo-deny` 도입은 별 P9 sub-item 으로 분리 (HANDOFF 에 추가).
- [ ] **2.6 dependency-only commit** — 본 step 은 lindera 호출 site 추가 없이 dep 만 추가한다. `cargo build --workspace -j 4 2>&1 | tail -3` 가 success (어떤 호출도 없으므로 unused-dependency lint trigger 가능 — `#[allow(unused_imports)]` 또는 implicit use 는 다음 step S3 에서 해소). 본 step 의 commit boundary 는 "dep만 추가, 실제 use 는 next commit" 로 명시.

**Acceptance criteria:**
- `cargo build --workspace -j 4 2>&1 | grep -E "^error\\[E" | wc -l` → `0`.
- `cargo tree --depth 1 -p lindera -p lindera-dict-ko-dic 2>&1 | grep -iE "MIT|Apache" | wc -l` → `≥ 2`. SPDX 가 Apache-2.0 + MIT 둘 중 적어도 하나 포함.
- `grep -c "lindera" Cargo.toml` → `≥ 1` (workspace dep 등록).
- `grep -c "lindera" crates/kebab-chunk/Cargo.toml` → `≥ 1` (per-crate dep 등록).

---

### Step 3 — kebab-chunk 에 `tokenize_korean_morphological()` helper + ingest pipeline 통합

**Implements:** §6.2 (transaction invariant + fallback). **Commit 3/10.**

**Spec sections covered:** §6.2 Pre-tokenize 우회 + Invariant + 실패 처리.

**Files to modify:**
- `crates/kebab-chunk/src/lib.rs` — `tokenize_korean_morphological()` 함수 신규 + chunk builder pipeline 에 호출.
- (선택) `crates/kebab-chunk/tests/tokenize_korean.rs` — lindera segmentation 의 단위 검증.

**Implementation outline:**

- [ ] **3.1 helper 함수 signature** — `kebab-chunk/src/lib.rs` 에 다음 추가:

  ```rust
  /// 한국어 chunk text 를 lindera ko-dic 으로 형태소 분해해 공백 join 한 결과를 반환.
  /// 분석 실패 시 None — 호출자는 NULL fallback 처리.
  pub fn tokenize_korean_morphological(text: &str) -> Option<String> {
      use lindera::{
          dictionary::{DictionaryConfig, DictionaryKind, load_dictionary_from_config},
          mode::Mode,
          segmenter::Segmenter,
          tokenizer::Tokenizer,
      };
      let dict_config = DictionaryConfig { kind: Some(DictionaryKind::KoDic), path: None };
      let dictionary = load_dictionary_from_config(dict_config).ok()?;
      let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
      let tokenizer = Tokenizer::new(segmenter);
      let tokens = tokenizer.tokenize(text).ok()?;
      let joined = tokens.iter().map(|t| t.text.as_ref()).collect::<Vec<_>>().join(" ");
      if joined.trim().is_empty() { None } else { Some(joined) }
  }
  ```

  실제 API 는 lindera 0.32 의 builder pattern 따라 조정 (crate docs 참조). `OnceCell` 또는 `lazy_static` 으로 Tokenizer 1회만 초기화하는 캐시 패턴 적용 — segmentation cost 의 dictionary load 의존이 hot loop 에 영향 안 가도록.

- [ ] **3.2 fallback 정책 명시** — spec §6.2 "tokenize_korean_morphological() 실패 처리" 따라 dictionary load fail 또는 token error 시 `None` 반환. 호출 site 에서 `tracing::warn!(target: "kebab-chunk", "tokenize_korean_morphological fallback to NULL: chunk_id={...}, err={...}");` 발화. chunk 자체 ingest 는 성공 + chunks_ai trigger 가 ELSE branch (raw text 만 index) 를 탄다.
- [ ] **3.3 chunk builder pipeline 통합** — `kebab-chunk` 의 chunk emit / builder loop (구체 함수명은 probe 결과 따라 결정; chunk struct 의 `text` field 가 채워진 직후 시점) 에서 `tokenize_korean_morphological(&chunk.text)` 호출 → 결과를 `chunk.tokenized_korean_text: Option<String>` 신규 field 에 저장. chunk struct definition 도 갱신.
- [ ] **3.4 store INSERT path 갱신** — `crates/kebab-store-sqlite/src/store.rs` 의 chunks INSERT SQL 에 `tokenized_korean_text` column 추가. signature 갱신: prepared statement 의 placeholder count 가 +1 됨. row binding 도 동일 transaction 안에서 단일 INSERT 로 처리 (spec §6.2 "lindera tokenize → chunks INSERT 는 동일 Rust transaction 내에서" invariant 보장).
- [ ] **3.5 단위 테스트** — `crates/kebab-chunk/tests/tokenize_korean.rs` 신규:

  ```rust
  #[test]
  fn tokenize_korean_morphological_splits_2char_word() {
      let out = kebab_chunk::tokenize_korean_morphological("한국 문화는 오래되었다").unwrap();
      // 공백으로 join 된 token 시퀀스에 "한국" 이 독립 token 으로 존재해야 함.
      let tokens: Vec<&str> = out.split_whitespace().collect();
      assert!(tokens.contains(&"한국"), "tokens = {:?}", tokens);
  }

  #[test]
  fn tokenize_korean_morphological_empty_returns_none() {
      assert!(kebab_chunk::tokenize_korean_morphological("").is_none());
      assert!(kebab_chunk::tokenize_korean_morphological("   ").is_none());
  }
  ```

  `한국` token 의 hit 가 lindera ko-dic 의 실제 segmentation 동작 의존이므로 (Appendix B 의 prior-knowledge 예측), 실측 실패 시 fixture text 를 spec Appendix B 의 검증 명령 출력으로 교체.

- [ ] **3.6 Verify.** `cargo test -p kebab-chunk --test tokenize_korean -j 4` → exit 0. `cargo build --workspace -j 4` → success.

**Acceptance criteria:**
- `cargo test -p kebab-chunk --test tokenize_korean tokenize_korean_morphological_splits_2char_word -j 4` → exit 0, fixture `"한국 문화는 오래되었다"` 분해 결과에 token `"한국"` 포함.
- `cargo test -p kebab-chunk --test tokenize_korean tokenize_korean_morphological_empty_returns_none -j 4` → exit 0.
- `cargo build --workspace -j 4 2>&1 | grep -c "warning: unused import.*lindera"` → `0` (lindera dep 가 실제 사용되고 unused import 없음).
- chunk struct 의 `tokenized_korean_text` field 존재 (`grep -n "tokenized_korean_text" crates/kebab-chunk/src/lib.rs` → `≥ 1`).
- `cargo clippy -p kebab-chunk --all-targets -j 4 -- -D warnings` → clean.

---

### Step 4 — `App::open_with_config` 의 first-boot eager backfill hook

**Implements:** §8.2 eager backfill. **Commit 4/10.**

**Spec sections covered:** §8.1 V007 trigram index 처리, §8.2 자동 eager backfill, §6.2 backfill atomic transaction.

**Files to modify:**
- `crates/kebab-store-sqlite/src/store.rs` — `backfill_tokenized_korean_text(progress_cb)` API 신규.
- `crates/kebab-app/src/lib.rs` — `App::open_with_config` 에 first-boot hook 추가 + idempotency guard.

**Implementation outline:**

- [ ] **4.1 backfill API signature** — `store.rs` 에 다음 추가:

  ```rust
  /// 모든 chunks 의 tokenized_korean_text 가 NULL 인 row 를 찾아
  /// lindera tokenize → UPDATE. 각 row 의 UPDATE 는 chunks_au trigger 를 fire
  /// 시켜 chunks_fts 가 재-index 됨. 모든 작업은 동일 transaction 안.
  /// 결과: 처리된 row 수.
  pub fn backfill_tokenized_korean_text<F>(&self, progress: F) -> anyhow::Result<u64>
  where F: Fn(u64, u64);
  ```

  body: (a) `SELECT chunk_id, text FROM chunks WHERE tokenized_korean_text IS NULL` 로 후보 row 수집. (b) 각 row 에 대해 `kebab_chunk::tokenize_korean_morphological(text)` 호출 → 결과를 `UPDATE chunks SET tokenized_korean_text = ? WHERE chunk_id = ?`. (c) BEGIN/COMMIT batch (예: 1000 row 마다 commit) 으로 단일 거대 transaction 회피 + crash recovery 의 partial progress 보장. (d) `progress(done, total)` 콜백 발화. (e) lindera 가 None 반환 시 `UPDATE ... SET tokenized_korean_text = ''` (빈 문자열) 대신 row 를 skip — chunks_au trigger 가 ELSE branch 를 타게 둔다.

- [ ] **4.2 idempotency 보장** — backfill 은 `IS NULL` 필터로 partial completion 후 재실행 시에도 idempotent. 한 번 채워진 row 는 다시 처리되지 않음. lindera 가 None 을 반환한 row 는 영구히 NULL 로 남으나, dictionary update 후 재실행 시 다시 후보가 되도록 별 marker column 은 추가하지 않음 — spec §8.2 의 "부분 완료 상태 search 동작" 보존.
- [ ] **4.3 App::open_with_config 의 hook 위치** — `crates/kebab-app/src/lib.rs` 의 `App::open_with_config` 본체에서 migration apply 직후 (즉 store 가 V009 schema 를 보장한 직후) 다음 호출 추가:

  ```rust
  let backfill_count = app.sqlite
      .backfill_tokenized_korean_text(|done, total| {
          if total > 0 && done % 500 == 0 {
              tracing::info!(target: "kebab-app",
                  "korean tokenizer backfill: {done}/{total}");
          }
      })
      .unwrap_or_else(|e| {
          tracing::warn!(target: "kebab-app",
              "korean tokenizer backfill failed: {e}");
          0
      });
  if backfill_count > 0 {
      tracing::info!(target: "kebab-app",
          "korean tokenizer backfill complete: {backfill_count} chunks updated");
  }
  ```

  backfill 실패 (lindera dict load 등) 는 fatal 아님 — App open 자체는 성공해 사용자가 vector/hybrid mode 로 계속 사용 가능.
- [ ] **4.4 startup latency 의 사용자 인지** — KB 가 큰 경우 첫 부팅이 spec §8.2 "약 10,000 chunk 당 ~30-60초" 동안 visible delay. CLI 사용자는 `kebab` 첫 호출이 늦어지는 것을 본다. stderr 로 progress info 발화 (위 callback 의 `tracing::info!`) — 사용자가 hang 으로 오인 안 하도록.
- [ ] **4.5 backfill 단위 테스트** — `crates/kebab-store-sqlite/tests/fts.rs` 에 추가:

  ```rust
  #[test]
  fn backfill_tokenized_korean_text_populates_nullable_rows() {
      // TempDir KB 열기, V009 적용된 schema 상태.
      // chunks 에 ('한국 문화', NULL) row 두 개 INSERT.
      // backfill_tokenized_korean_text(noop) 호출 → 반환값 == 2.
      // 두 row 의 tokenized_korean_text IS NOT NULL.
      // 두번째 호출 → 0 반환 (idempotent).
  }
  ```

- [ ] **4.6 Verify.** `cargo test -p kebab-store-sqlite --test fts backfill_tokenized_korean_text_populates_nullable_rows -j 4` → exit 0. `cargo build -p kebab-app -j 4` → success.

**Acceptance criteria:**
- `cargo test -p kebab-store-sqlite --test fts backfill_tokenized_korean_text_populates_nullable_rows -j 4` → exit 0, idempotency 확인.
- `App::open_with_config` 두 번 연속 호출 시 두번째 호출의 backfill_count = 0 (idempotency 의 production 측면). `backfill_tokenized_korean_text_populates_nullable_rows` test 의 두 번째 `backfill_tokenized_korean_text` 호출 assert (반환값 == 0) 이 이 AC 를 cover 함. `cargo test -p kebab-store-sqlite --test fts backfill_tokenized_korean_text_populates_nullable_rows -j 4` → exit 0 으로 검증.
- `cargo clippy -p kebab-app --all-targets -j 4 -- -D warnings` → clean.

---

### Step 5 — `short_query_hint()` 제거 + lexical.rs build_match_string 정리

**Implements:** §7.2, §7.3. **Commit 5/10.**

**Spec sections covered:** §7.2 lexical.rs 조정, §7.3 CLI hint 제거.

**Files to modify:**
- `crates/kebab-app/src/app.rs` — `short_query_hint()` 함수 정의 + 2 호출 site (line 532, 616) 제거.
- `crates/kebab-app/src/lib.rs` — re-export `pub use app::{..., short_query_hint};` 의 `short_query_hint` 제거.
- `crates/kebab-tui/src/app.rs` (161, 188), `crates/kebab-tui/src/search.rs` (443, 619, 703, 711), `crates/kebab-tui/src/run.rs` (394) — `short_query_hint` field + 호출 cascade 제거.
- `crates/kebab-cli/tests/wire_search_response.rs:215` — `search_plain_emits_short_query_hint_to_stderr` test 삭제.
- `crates/kebab-search/src/lexical.rs::build_match_string()` — trigram-specific 분기 단순화 검토.

**Implementation outline:**

- [ ] **5.1 short_query_hint 정의 + 호출 제거** — `crates/kebab-app/src/app.rs:98` 의 `pub fn short_query_hint(query_text: &str, hits_empty: bool) -> Option<String>` 함수 본체 삭제. line 532, 616 의 두 호출 site (search response 의 hint 채우는 부분) 도 함께 제거 — 해당 SearchResponse field 가 항상 None 으로 셋팅되거나, SearchResponse 의 hint field 자체를 제거 (wire schema 영향 검토 필요).
- [ ] **5.2 wire schema 영향 확인** — `SearchResponse` struct 의 `short_query_hint` 또는 `hint` field 가 `search_hit.v1` / `search_response.v1` JSON Schema 에 포함되어 있는지 `docs/wire-schema/v1/*.schema.json` grep. (a) 포함되어 있다면 wire 의 additive minor 가 아닌 removal 이므로 별도 결정 — spec §11.3 의 "wire schema shape 변경 없음" 과 모순. → 그 경우 field 는 struct 에 남기고 항상 None / 생략 (Option/Default) 으로 유지. (b) 포함되어 있지 않으면 struct field 도 함께 제거.
- [ ] **5.3 lib.rs re-export 제거** — `crates/kebab-app/src/lib.rs:75` 의 `pub use app::{App, SearchResponse, short_query_hint};` 에서 `short_query_hint` 부분 제거.
- [ ] **5.4 TUI cascade 제거** — `crates/kebab-tui/src/app.rs:161,188` 의 `pub short_query_hint: Option<String>` field + default init 삭제. `crates/kebab-tui/src/search.rs:443,619,703,711` 의 4 곳에서 `s.short_query_hint = None` / `s.short_query_hint = kebab_app::short_query_hint(...)` 라인 모두 삭제. `crates/kebab-tui/src/run.rs:394` 의 `.and_then(|s| s.short_query_hint.as_deref())` 도 제거 — 그 자리의 UI 표현은 빈 상태 또는 다른 hint 로 대체.
- [ ] **5.5 CLI test 정리** — `crates/kebab-cli/tests/wire_search_response.rs:215` 의 `search_plain_emits_short_query_hint_to_stderr` test 전체를 삭제. 또는 test name 을 `search_plain_does_not_emit_short_query_hint_to_stderr` 로 rename 후 assertion 을 negative 로 invert.
- [ ] **5.6 lexical.rs::build_match_string 검토** — `crates/kebab-search/src/lexical.rs:205` 의 함수 본체를 읽어 trigram-specific 처리 (예: 2-char Korean token 의 OR-combine, character-class 분해) 존재 여부 확인. spec §7.2 권장: "backward-compat 차원에서 기존 로직 보존" → 본 step 에서는 **변경 없음** (보존). 단 함수 doc-comment 에 "V009 unicode61 + 형태소 tokenizer 환경에서는 multi-token Korean query 의 OR-combine 분기는 redundant 하나 보존" 한 줄 추가.
- [ ] **5.7 Verify.** `cargo test --workspace -j 1 -- --skip wire_search_response::search_plain_emits 2>&1 | tail -5` 으로 cascade 통과 확인. `cargo build --workspace -j 4` success.

**Acceptance criteria:**
- `grep -rn "short_query_hint" crates/ tests/ 2>/dev/null | wc -l` → `0` (또는 doc-comment 의 1 줄만 남음).
- `cargo build --workspace -j 4 2>&1 | grep -E "^error" | wc -l` → `0` (cascade 누락으로 인한 컴파일 에러 없음).
- `cargo test -p kebab-cli --test wire_search_response -j 4` → exit 0, `search_plain_emits_short_query_hint_to_stderr` 가 test list 에서 사라짐.
- `cargo clippy --workspace --all-targets -j 4 -- -D warnings` → clean.

---

### Step 6 — `lexical_index_version()` 의 V009 bump

**Implements:** §11.1. **Commit 6/10.**

**Spec sections covered:** §11.1 index_version bump, §11.3 wire content 변화 명시.

**Files to modify:**
- `crates/kebab-app/src/app.rs:991-993` — `lexical_index_version()` 함수의 반환 문자열 갱신.

**Implementation outline:**

- [ ] **6.1 현재 source-of-truth 확인** — line 991-993 의 `fn lexical_index_version` 는 `IndexVersion(format!("lex:{}", config.chunking.chunker_version))` 반환. fts version 정보가 없음. spec §11.1 의 권장 문자열 `"fts5-v009-korean-morphological"` 또는 `"v009-morpho"` 를 채택하기 위해 format 변경 필요.
- [ ] **6.2 신규 format** — 변경:

  ```rust
  fn lexical_index_version(config: &kebab_config::Config) -> IndexVersion {
      IndexVersion(format!(
          "lex:{}:fts5-v009-korean-morphological",
          config.chunking.chunker_version
      ))
  }
  ```

  기존 5 호출 site (app.rs line 367, 389, 479, 661, 680) 는 함수 호출만 하므로 자동 cascade. test `lexical_index_version_is_returned_unchanged` (`crates/kebab-search/tests/lexical.rs:650`) 의 assertion 갱신 필요.
- [ ] **6.3 lexical test 갱신** — `crates/kebab-search/tests/lexical.rs:650` 의 `lexical_index_version_is_returned_unchanged` test 가 hard-coded 한 expected 값을 V009 신규 format 으로 갱신. 또는 test 의 의도가 "format 자체의 invariant 보장" 이라면 substring match (`actual.0.contains("fts5-v009")`) 로 변경.
- [ ] **6.4 Verify.** `cargo test -p kebab-search --test lexical lexical_index_version_is_returned_unchanged -j 4` → exit 0. `cargo test -p kebab-app -j 4` → 회귀 0.

**Acceptance criteria:**
- `cargo test -p kebab-search --test lexical lexical_index_version_is_returned_unchanged -j 4` → exit 0.
- `cargo build -p kebab-cli -j 4 && ./target/debug/kebab schema --json | jq -r '.index_versions.lexical // empty' 2>/dev/null | grep -c "fts5-v009"` → `≥ 1`.
- `cargo clippy -p kebab-app --all-targets -j 4 -- -D warnings` → clean.

---

### Step 7 — 신규 unit/integration test (Korean morphological FTS scenarios)

**Implements:** §9.1 AC, §9.2 test coverage. **Commit 7/10.**

**Spec sections covered:** §9.1 lexical-mode scenarios, §9.2 test coverage.

**Files to modify:**
- `crates/kebab-store-sqlite/tests/fts.rs` — `fts_v009_korean_morphological_2char_query_hits` + `fts_v009_english_whole_token_only` 추가.
- Create `crates/kebab-app/tests/search_korean.rs` — `korean_morphological_2char_query_lexical_mode` + `korean_morphological_mixed_english_korean_query`.

**Implementation outline:**

- [ ] **7.1 fts.rs 의 신규 단위 test** — spec §9.2 의 verbatim 시그니처 따라:

  ```rust
  #[test]
  fn fts_v009_korean_morphological_2char_query_hits() {
      // 1. TempDir KB 열기 + V009 schema 보장.
      // 2. chunks 에 ('한국 문화는 오래되었다', tokenize_korean_morphological(...)) row INSERT.
      //    triggers chunks_ai 가 chunks_fts 에 자동 index.
      // 3. SELECT chunk_id FROM chunks_fts WHERE chunks_fts MATCH '한국'
      //    → 결과 row count >= 1.
      // 4. 동일 방식으로 '문화' query → row count >= 1.
  }

  #[test]
  fn fts_v009_english_whole_token_only() {
      // Path A 회귀 확인: V007 trigram substring 매칭 사라짐.
      // 1. ('the tokenizer normalizes whitespace', None) row INSERT.
      // 2. MATCH 'token' → 0 row (substring of 'tokenizer' is NOT matched by unicode61).
      // 3. MATCH 'tokenizer' → >= 1 row.
  }
  ```

- [ ] **7.2 search_korean.rs end-to-end test** — `crates/kebab-app/tests/search_korean.rs` 신규:

  ```rust
  #[test]
  fn korean_morphological_2char_query_lexical_mode() {
      // 1. TempDir config, App::open_with_config.
      // 2. ingest_with_config_opts 로 fixture 한국어 markdown 파일 ingest.
      //    (fixture = "한국어를 공부합니다.\n서울은 한국의 수도입니다.")
      // 3. App::search 의 SearchQuery { text: "한국", mode: Lexical, ... }.
      //    → hits.len() >= 1.
      // 4. '서울' query → hits.len() >= 1.
  }

  #[test]
  fn korean_morphological_mixed_english_korean_query() {
      // 1. fixture = "Rust 최적화는 zero-cost abstraction 을 강조한다."
      // 2. 'Rust' query → hit, '최적화' query → hit.
      // 3. 'Rust 최적화' multi-token query → hit (build_match_string 의 OR-combine).
  }
  ```

  fixture 가 lindera ko-dic 의 실제 segmentation 동작 의존이므로 spec Appendix B 의 prior-knowledge 예측이 실패할 경우 fixture text 와 query 를 조정 (한국어 corpus 의 representative case).

- [ ] **7.3 spec §9.2 `fts_v009_matches_design_section_5_5_verbatim` 와의 관계** — 이 test 는 이미 S1 의 rename 으로 생성됨. 본 step 에서 추가하지 않음.
- [ ] **7.4 Verify.** `cargo test -p kebab-store-sqlite --test fts fts_v009_korean -j 4` → 2 test exit 0. `cargo test -p kebab-app --test search_korean -j 4` → 2 test exit 0.

**Acceptance criteria:**
- `cargo test -p kebab-store-sqlite --test fts fts_v009_korean_morphological_2char_query_hits -j 4` → exit 0, fixture chunk `"한국 문화는 오래되었다"` 가 query `"한국"` 의 hit list 에 존재.
- `cargo test -p kebab-store-sqlite --test fts fts_v009_english_whole_token_only -j 4` → exit 0, fixture `"the tokenizer..."` 가 query `"token"` 에서 0-hit + query `"tokenizer"` 에서 hit.
- `cargo test -p kebab-app --test search_korean korean_morphological_2char_query_lexical_mode -j 4` → exit 0.
- `cargo test -p kebab-app --test search_korean korean_morphological_mixed_english_korean_query -j 4` → exit 0.
- 신규 test binary 2개 (`fts` 확장 + `search_korean` 신규) 의 추가로 workspace test count 가 baseline +4 이상. 검증 명령:
  - `cargo test -p kebab-store-sqlite --test fts -- --list 2>&1 | grep "fts_v009_korean" | wc -l` → `≥ 1`
  - `cargo test -p kebab-app --test search_korean -- --list 2>&1 | wc -l` → `≥ 2`

---

### Step 8 — eval golden baseline regenerate

**Implements:** §11.3 wire content 변화 (eval golden 재생성 책임). **Commit 8/10.**

**Spec sections covered:** §11.3 eval golden baseline regenerate.

**Files to modify:**
- `crates/kebab-eval/goldens/*.csv` (또는 동급 fixture) — V007 기준 expected rank/hit_id 시퀀스를 V009 기준 으로 재생성.
- `crates/kebab-eval/tests/*` — 회귀 0.

**Implementation outline:**

- [ ] **8.1 baseline regenerate 절차 확인** — `crates/kebab-eval/` 의 README 또는 doc-comment 에 명시된 baseline regenerate 명령 (e.g. `cargo run -p kebab-eval -- regenerate-goldens --kb /tmp/eval-kb`) 또는 동급 운영 절차 실행. 정확한 명령은 crate 의 main.rs / lib.rs 의 subcommand 정의 확인 후 결정.
- [ ] **8.2 V009 적용된 KB 로 baseline 재계산** — spec §12.2 dogfood verification 의 fresh KB 사용 가능 (또는 `tasks/HOTFIXES.md` 의 dogfood corpus 경로 재사용). 재생성된 CSV 의 hit_id / rank 시퀀스가 V007 baseline 과 다름은 의도된 변화 — git diff 가 보여주는 변경량 검토 후 commit.
- [ ] **8.3 commit 시점 분리** — baseline regenerate 는 별 commit (S8) 이며, executor / dogfood 단계에서 의미 있는 deviation 발견 시 다시 regenerate 가능. PR scope 내부 commit.
- [ ] **8.4 Verify.** `cargo test -p kebab-eval --workspace -j 4 2>&1 | grep -E "^test result" | head -3` → 모두 `0 failed`.

**Acceptance criteria:**
- `cargo test -p kebab-eval -j 4 2>&1 | grep "test result.*failed" | grep -v "0 failed" | wc -l` → `0`.
- `git diff --stat crates/kebab-eval/goldens/` → 변경 라인 수 > 0 (baseline 이 실제로 갱신됨).
- `cargo clippy -p kebab-eval --all-targets -j 4 -- -D warnings` → clean.

**Note:** spec §11.3 + plan brief 의 "spec §11 의 결정 따라 본 plan scope 에 포함 또는 별 follow-up" 중 본 plan 은 **scope 포함** 선택 — 단 deviation (regenerate 실패, baseline 차이 너무 큼) 시 별 P5 follow-up 으로 후퇴 가능. 후퇴 시 본 S8 은 verify-only 로 단순화하고 `tasks/HOTFIXES.md` 에 한 줄 deviation 기록.

---

### Step 9 — docs sync (README + HANDOFF + ARCHITECTURE + SKILL + HOTFIXES)

**Implements:** §7.4 surface cascade. **Commit 9/10.**

**Spec sections covered:** §7.4 README/SKILL/HANDOFF/ARCHITECTURE 갱신.

**Files to modify:**
- `README.md` — 명령 table `kebab search` 행 갱신 + Configuration section 보조 정보.
- `integrations/claude-code/kebab/SKILL.md` — V007 3-char hint 제거 + 2-char 한국어 query 지원 표현 추가 + 영어 substring 회귀 (optional advanced note).
- `HANDOFF.md` — v0.20.1 patch release 의 G section 또는 round-up entry 에 본 변경 한 줄 추가.
- `docs/ARCHITECTURE.md` — crate dependency graph 의 lindera 추가, FTS tokenizer 섹션 V007 trigram → V009 unicode61 + 형태소 분해.
- `tasks/HOTFIXES.md` — V009 cascade 의 dated entry + cargo deny 도입 deferral note.

**Implementation outline:**

- [ ] **9.1 README.md 변경** — CLAUDE.md "Docs split" rule 따라 narrow scope. (a) `kebab search` 명령 row 의 description 에 "한국어 2자 query 지원 (예: '한국', '서울')" 추가. (b) Configuration section 의 변경은 없음 (config 노브 없음, S6.3 Option A). (c) Mermaid logical-architecture diagram 의 변경 없음 (FTS5 는 내부 store 측 detail).
- [ ] **9.2 SKILL.md 변경** — `integrations/claude-code/kebab/SKILL.md` 의 description / 사용법 섹션에서 "한국어 query 는 3자 이상 권장" 류 표현 검색 + 제거. 신규: "한국어 2자 단어 검색 지원 (예: '한국', '서울')". 영어 substring 매칭 회귀는 일반 사용자에게는 노이즈 → optional advanced note 로 짧게 (1줄) 만 추가. 또는 release notes 만 다루고 SKILL.md 는 user-facing happy path 로 한정 (권장).
- [ ] **9.3 HANDOFF.md 변경** — v0.20.0 sub-item 1 머지 후 priorities section 의 C 항목 status flip ('지연' / '미해결' → '완료'). v0.20.1 patch release section 의 release notes scope 에 본 변경 추가. 머지 후 발견된 버그 / 결정 summary 행 한 줄 추가 ("Bug #8 한국어 2자 query → v0.20.1 의 V009 morphological tokenizer 로 해소").
- [ ] **9.4 docs/ARCHITECTURE.md 변경** — (a) crate dependency graph 에 `kebab-chunk → lindera, lindera-dict-ko-dic` edge 추가. (b) FTS tokenizer 섹션이 있다면 V007 trigram → V009 unicode61 + 형태소 분해로 갱신 + locked-in 결정 table 의 row 갱신. (c) directory tree 변경 없음 (신규 crate 없음).
- [ ] **9.5 tasks/HOTFIXES.md entry** — 2026-05-28 dated entry 추가:

  > ### 2026-05-28 — Bug #8 한국어 2자 query 해소 (V009 morphological tokenizer)
  >
  > - **Discovered**: 도그푸딩 round 3/4 (2026-05-28). '한국' / '서울' 0-hit 반복.
  > - **Symptom**: V007 trigram tokenizer 의 ≥3-char minimum 한계.
  > - **Root cause**: trigram 의 bucket 미존재.
  > - **Fix**: V009 migration + lindera ko-dic + tokenized_korean_text column + first-boot eager backfill. branch `feat/korean-morphological-tokenizer`.
  > - **Amends**: design §5.5 (unicode61 + CASE expression triggers 로 갱신), §9 (index_version cascade), tasks/HOTFIXES.md 2026-05-22 trigram entry (한국어 2자 query 미해결 footnote 해소).
  > - **Deferred**: `cargo-deny` 정식 도입 (workspace 의 deny.toml) 은 별 P9 follow-up 으로 분리. 현 PR 은 `cargo tree` 의 SPDX 수동 검증 + lindera/ko-dic license 의 fail-fast 정책 적용.

- [ ] **9.6 Verify.** 4 docs file 의 변경 라인 합 > 30 (충분한 cascade). 한 줄짜리 cosmetic 만 들어간 케이스 회피.

**Acceptance criteria:**
- `git diff --stat README.md HANDOFF.md docs/ARCHITECTURE.md integrations/claude-code/kebab/SKILL.md tasks/HOTFIXES.md` → 5 file 모두 변경 (각 file 의 변경 라인 ≥ 1).
- `grep -c "한국어 2자" README.md` → `≥ 1`.
- `grep -c "V009" tasks/HOTFIXES.md` → `≥ 1`.
- `grep -c "lindera" docs/ARCHITECTURE.md` → `≥ 1`.

---

### Step 10 — version bump (Cargo.toml workspace `version`)

**Implements:** §12.1 v0.20.1 patch release. **Commit 10/10.**

**Spec sections covered:** §12.1 release strategy.

**Files to modify:**
- `Cargo.toml` (workspace) — `[workspace.package] version = "0.20.0"` → `"0.20.1"`.
- `Cargo.lock` — 자동 갱신.

**Implementation outline:**

- [ ] **10.1 version bump** — CLAUDE.md `Release / binary version bump` rule 따라 본 변경은 "user 가 새 바이너리로 도그푸딩 또는 실사용을 할 필요" + "frozen design contract 변경 (design §5.5 갱신)" 두 트리거 모두 해당 → bump 필수. `Cargo.toml` workspace `version` 을 `"0.20.0"` → `"0.20.1"`.
- [ ] **10.2 Cargo.lock 갱신** — `cargo update --workspace --offline 2>&1 | tail -3` 또는 단순 `cargo build --workspace -j 4` 가 Cargo.lock 의 version 부분 자동 갱신.
- [ ] **10.3 commit 직후 tag 절차 (별 task)** — bump commit 자체는 본 step 의 commit, 이후 `gitea-release v0.20.1` 명령은 별 task 로 분리 (executor 의 sequential N step 후 user 가 직접 cut). spec §12.1 의 release notes 본문 (한국어 2자 query 지원, FTS5 tokenizer 변경 회귀, 자동 backfill, ingest 성능 감소) 4 단락을 release notes draft 로 미리 작성하여 `docs/release-notes/v0.20.1-draft.md` 에 보관 (선택, plan 의 brief 의 verifier checklist).
- [ ] **10.4 Verify.** `grep -c "^version = \"0.20.1\"" Cargo.toml` → `1`. `cargo build --workspace -j 4` → success + binary `--version` 출력이 `0.20.1`.

**Acceptance criteria:**
- `grep "^version" Cargo.toml | head -1` → `version = "0.20.1"`.
- `cargo build --release -p kebab-cli -j 4 && ./target/release/kebab --version 2>&1` → `kebab 0.20.1` (또는 동급 출력).
- `cargo build --workspace -j 4 2>&1 | tail -3` → success.

---

### Step 11 — final sanity (no commit)

**Implements:** AC §9.3 verifier checklist + plan brief §7. **No commit.**

**Spec sections covered:** §9.3 verifier checklist, §12.2 dogfood verification.

**Implementation outline:**

- [ ] **11.1 Workspace test.** `cargo test --workspace --no-fail-fast -j 1 > /tmp/wstest.out 2>&1` → 모두 pass (baseline 1370+ → 신규 +4 이상). Expected: `0 failed`.
- [ ] **11.2 Clippy.** `cargo clippy --workspace --all-targets -j 4 -- -D warnings` → 0 warning.
- [ ] **11.3 cargo fmt.** `cargo fmt --all --check` → exit 0 (또는 fmt 적용).
- [ ] **11.4 Schema 무결성.** `./target/release/kebab schema --json | jq -e '.wire.schemas | length' 2>&1` → 기존과 동일 (wire schema 추가 0).
- [ ] **11.5 Dogfood smoke.** TempDir KB 의 fresh KB 시나리오:

  ```bash
  rm -rf /tmp/kebab-smoke-v009
  ./target/release/kebab --config /tmp/kebab-smoke-v009/config.toml ingest <한국어 fixture>
  ./target/release/kebab --config /tmp/kebab-smoke-v009/config.toml search '한국' --json | jq '.hits | length'
  # → ≥ 1
  ./target/release/kebab --config /tmp/kebab-smoke-v009/config.toml search '서울' --json | jq '.hits | length'
  # → ≥ 1
  ./target/release/kebab --config /tmp/kebab-smoke-v009/config.toml search '지하철' --json | jq '.hits | length'
  # → ≥ 1
  ```

- [ ] **11.6 기존 KB eager backfill 시나리오.** 별도 디렉토리에 V007 (pre-V009) 시점 SQLite snapshot 보존 사용 가능 시:

  ```bash
  cp -r /tmp/kebab-v007-snapshot /tmp/kebab-v007-test
  ./target/release/kebab --config /tmp/kebab-v007-test/config.toml schema --json 2>&1 | grep -i "backfill"
  # eager backfill info log 가 stderr 에 발화 확인.
  ./target/release/kebab --config /tmp/kebab-v007-test/config.toml search '한국' --json | jq '.hits | length'
  # backfill 완료 후 → ≥ 1
  ```

  V007 snapshot 부재 시 본 verifier 는 best-effort (manual). dogfood corpus 의 재구성 비용을 고려해 spec §12.2 의 권장 절차 따른다.

- [ ] **11.7 git status 확인.** `git status --short` → 모든 의도된 변경이 stage 됨 + untracked file 없음 (또는 의도된 untracked 만 — 예: release-notes draft).

**Acceptance criteria (final checklist):**
- [ ] `kebab search '한국'` (fresh V009 KB) → hit ≥ 1.
- [ ] V009 migration apply 후 기존 V007 KB → eager backfill 자동 시작 → 완료 후 `kebab search '한국'` → hit ≥ 1.
- [ ] `cargo test --workspace --no-fail-fast -j 1` → 모두 pass (baseline +4 이상).
- [ ] `cargo clippy --workspace --all-targets -j 4 -- -D warnings` → clean.
- [ ] `cargo fmt --all --check` → exit 0.
- [ ] (deferred) `cargo deny check` → S2.5 에서 P9 follow-up 으로 분리. 본 PR 의 license 검증은 `cargo tree` 의 SPDX 수동 비교로 대체.
- [ ] README.md / SKILL.md / HANDOFF.md / docs/ARCHITECTURE.md / tasks/HOTFIXES.md 모두 갱신됨.
- [ ] release notes draft (`docs/release-notes/v0.20.1-draft.md`) 작성, 4 단락 본문 (2자 query 지원 / tokenizer 변경 + 영어 회귀 / 자동 backfill / ingest 성능).

---

## 3. Dependencies + sequencing

각 step 의 의존성:

| Step | Title | Blocked by | Blocks | 병렬 가능? |
|------|-------|------------|--------|------------|
| S1 | V009 migration + design §5.5 + CI rename | (none) | S3, S4, S6, S7 | (entry) |
| S2 | lindera dep | (none) | S3 | (entry, S1 과 병렬) |
| S3 | tokenize_korean_morphological + ingest 통합 | S1, S2 | S4, S5, S7 | — |
| S4 | first-boot eager backfill | S1, S3 | S7, S11 | S5/S6 와 병렬 가능 |
| S5 | short_query_hint 제거 + lexical.rs 정리 | (none, S3 면 깔끔) | S7 | S4/S6 와 병렬 가능 |
| S6 | lexical_index_version bump | S1 | S7 | S4/S5 와 병렬 가능 |
| S7 | 신규 test (FTS + search_korean) | S3, S4, S6 | S8, S11 | — |
| S8 | eval golden regenerate | S7 + dogfood KB | S11 | — |
| S9 | docs sync | S1~S8 (의미적) | S11 | — |
| S10 | version bump | S9 | S11 | — |
| S11 | final sanity | S1~S10 | (exit) | — |

**Parallel dispatch 가능 그룹:**

- **Group 1 (entry, parallel):** S1 + S2. 두 step 은 독립. dispatcher 가 sonnet 두 instance 로 병렬 실행 가능.
- **Group 2 (S3 후속, parallel):** S4 + S5 + S6. S3 가 끝난 직후 3 instance 동시 실행. S4 (backfill API + hook) + S5 (hint 제거 + lexical.rs) + S6 (index_version bump) 가 file overlap 0 (서로 다른 module). **주의:** S5 와 S6 은 모두 `crates/kebab-app/src/app.rs` 를 수정함. 병렬 에이전트 실행 시 편집 행 범위를 사전 분리할 것 (S5: line 98, 532, 616; S6: line 991-993).
- **Group 3 (sequential):** S7 → S8. test 가 먼저 들어가야 eval baseline 갱신 후 회귀 0 검증 가능.
- **Group 4 (sequential):** S9 → S10. docs sync 후 version bump.

**총 시퀀스 (worst-case sequential, sub-agent 1 instance):** S1 → S2 → S3 → S4 → S5 → S6 → S7 → S8 → S9 → S10 → S11. 10 commit + 1 verify.

**총 시퀀스 (parallel, sub-agent 3 instance):** {S1, S2} 병렬 → S3 → {S4, S5, S6} 병렬 → S7 → S8 → S9 → S10 → S11. wall-clock 단축 약 30%.

---

## 4. AC verifier 의 actionability

본 plan 의 모든 AC 는 mechanical 검증 가능하도록 작성됨. spec §9.3 의 verifier checklist 도 모두 actionable 한 명령 또는 grep 단위:

| AC type | Verifier 명령 형식 |
|--|--|
| Schema diff-check | `cargo test ...verbatim -j 4` → exit 0 |
| Hit/miss assertion | `cargo test ...hits -j 4` → exit 0 + grep on output |
| Surface 갱신 cascade | `grep -c "<phrase>" <file>` → ≥ 1 |
| Test count growth | `cargo test ... 2>&1 \| grep "test result" \| awk` |
| Binary version | `./target/release/kebab --version` |
| dogfood smoke | `kebab search '한국' --json \| jq '.hits \| length'` → ≥ 1 |

각 step 의 AC 는 별 reviewer subagent (sonnet) 가 mechanical 통과 여부를 판단 가능. 해석 여지 없는 fact statement.

---

## 5. Risk + open questions

### S1 (V009 migration)

- **Risk:** design §5.5 갱신과 V009 SQL block 의 whitespace 미세 차이로 verbatim test fail.
- **Mitigation:** S1.3 의 갱신은 V009 SQL 의 (c) 블록을 design §5.5 에 그대로 paste. fts.rs:407 의 `normalize_ws` 함수가 모든 whitespace 차이 흡수.

### S2 (lindera dep)

- **Risk:** `lindera-dict-ko-dic` 의 dict 다운로드가 build-time 에 실패 (network egress 차단 환경). 또는 정확한 feature flag name 이 spec 의 가정과 다름.
- **Mitigation:** S2.3 의 cargo build 출력 정독 후 dict embed feature name 확정. 실패 시 `build.rs` proactive download 또는 vendored crate 로 후퇴 (별 follow-up).
- **Risk:** lindera version pin 의 transitive dep 가 기존 workspace 와 충돌 (예: `unicode-normalization`, `serde` major version 의 lock 갈등).
- **Mitigation:** `cargo update` 후 `cargo tree --duplicates` 로 회귀 확인.

### S3 (tokenize_korean_morphological)

- **Risk:** lindera ko-dic 의 `'한국어'` segmentation 이 단일 token (`['한국어']`) 으로 나오면 `'한국'` query 0-hit (spec critic R1 finding #3). 본 plan 의 S3.5 test `tokenize_korean_morphological_splits_2char_word` 이 fixture `"한국 문화는 오래되었다"` 에서 hit 한다는 prior-knowledge 가정.
- **Mitigation:** test 실패 시 (a) fixture 를 ko-dic 의 실제 분해 결과에 일치하는 corpus 로 교체 (예: `'한국 문화는 오래되었다'` 가 명시적으로 공백 포함하므로 두 token `['한국', '문화는']` 보장). (b) AC §9.1 의 hit 보장 범위를 spec Appendix B 의 "고유명사 미등록 또는 형태소 경계 일치 시" 로 명시적 좁힘. (c) sub-morpheme 추가 분해 (n-gram supplement) 는 별 follow-up.

### S4 (first-boot eager backfill)

- **Risk:** 거대 KB (예: 100,000 chunk) 의 첫 부팅이 ~5-10 분 hang. 사용자가 hang 으로 오인 + Ctrl-C → partial backfill state.
- **Mitigation:** S4.3 의 `tracing::info!` progress log 가 stderr 에 발화. partial 은 idempotent (re-run 시 이어서 처리). 거대 KB 의 사용자 경험은 별 P5 follow-up (background job + streaming feedback) 으로 미룬다.
- **Risk:** `chunks` 테이블에 row 가 매우 많을 때 단일 transaction commit 이 long-lock → 다른 reader 가 block.
- **Mitigation:** S4.1 body 의 "1000 row 마다 commit" batch 패턴. SQLite WAL mode 와 호환.

### S5 (short_query_hint 제거)

- **Risk:** wire schema 의 `SearchResponse` 가 `short_query_hint` field 를 포함 → removal 이 wire breaking change.
- **Mitigation:** S5.2 의 wire schema grep 으로 사전 확인. 포함 시 struct field 유지 (항상 None) + wire 의 backward-compat 보존.

### S6 (lexical_index_version)

- **Risk:** `crates/kebab-search/tests/lexical.rs:650` 외에도 hard-coded expected 가 다른 test 에 존재.
- **Mitigation:** `grep -rn "lex:" crates/ tests/ --include="*.rs"` 로 사전 확인 + cascade.

### S7 (신규 test)

- **Risk:** end-to-end `search_korean` test 의 fixture markdown 파일이 chunker_version 의존성으로 chunk boundary 가 의도와 다르게 끊겨 한국어 token 이 cross-chunk 분리 → query 의 hit 가 fixture 의 의도와 다름.
- **Mitigation:** fixture text 의 short paragraph 로 단일 chunk 생성 + chunker_version 의 invariant 확인.

### S8 (eval golden regenerate)

- **Risk:** baseline diff 가 거대 (모든 row 의 rank 가 shift) → review 시 spec drift 의심 + revert 압박.
- **Mitigation:** S8.3 의 commit 분리 + spec §11.3 의 의도된 변화 cross-link. revert 결정은 user 의 별 dogfood 후. 본 PR scope 에서 fail-fast 가능 — 그 경우 별 P5 follow-up 으로 분리.

### S9 (docs sync)

- **Risk:** docs/ARCHITECTURE.md 의 crate dependency graph 가 stale 한 형태 (rebuild 필요).
- **Mitigation:** ARCHITECTURE.md 의 graph 가 Mermaid 형식이라 단순 edge add. cosmetic.

### S10 (version bump)

- **Risk:** bump 만 한 commit 이라 spec drift 의심 + 후속 task 의 release notes 미완성.
- **Mitigation:** S10.3 의 release notes draft 작성 (`docs/release-notes/v0.20.1-draft.md`). bump commit 본문에 "spec §12.1 release notes draft 포함" 명시.

### S11 (final sanity)

- **Risk:** dogfood smoke 의 fixture 한국어 corpus 부재 → 11.5 verification 의 hit 검증 stub 형식.
- **Mitigation:** spec §12.2 + handoff §4.2 의 dogfood corpus 경로 (`/build/cache/tmp/v0.20-r5-dogfood/`) 재구성 후 사용. 부재 시 manual best-effort.

### Open questions (executor 에게 위임)

1. **lindera version pin** — `0.32` 또는 `0.34` (cargo search 시점 latest stable) 중 선택. major breaking change 부재 가정.
2. **`lindera-dict-ko-dic` feature flag name** — `embedded-dict`, `ko-dic`, `compress` 중 정확한 이름은 crate metadata 확인 후 결정.
3. **`SearchResponse` 의 `short_query_hint` field 의 wire schema 포함 여부** — S5.2 에서 grep 확인 후 결정.
4. **`lexical.rs::build_match_string()` 의 trigram-specific 분기 보존 여부** — 본 plan 은 보존 선택 (S5.6). executor 가 코드 가독성 측면에서 단순화 판단 시 별 commit 으로 분리.
5. **eval golden regenerate 의 baseline diff size** — S8 의 sanity bound (예: 변경 라인 < 50% of total) 미정. executor 의 dogfood 후 판단.

---

## 6. Cost optimization (model routing)

각 step 의 implementer + reviewer 모델 routing 제안 (memory `feedback_teammate_model_routing.md` 따라):

| Step | Title | Implementer | Reviewer | Rationale |
|------|-------|-------------|----------|-----------|
| S1 | V009 migration + design §5.5 + CI rename | **opus** | sonnet | spec frozen contract (§5.5 verbatim) 의 spirit 보존 필요. trigger CASE expression 의 design 측 갱신은 한 글자 오차로 CI fail. |
| S2 | lindera dep | sonnet | sonnet | dependency 추가만 — mechanical. |
| S3 | tokenize_korean_morphological + ingest 통합 | **opus** | sonnet | lindera API 의 정확한 호출 + ingest pipeline 의 transaction invariant + chunk struct cascade. multi-crate 동시 변경. |
| S4 | first-boot eager backfill | sonnet | sonnet | API design 은 spec 에 명시. batch commit 패턴은 표준. |
| S5 | short_query_hint 제거 + lexical.rs 정리 | sonnet | sonnet | 다중 file cascade 이나 mechanical search + delete. |
| S6 | lexical_index_version bump | sonnet | sonnet | 단일 함수 + test fixture 갱신. |
| S7 | 신규 test | sonnet | sonnet | spec §9.2 의 verbatim 시그니처 따라. fixture text 의 ko-dic 동작 의존이라 dogfood 단계에서 추가 조정 가능. |
| S8 | eval golden regenerate | sonnet | sonnet | baseline regenerate 명령 실행 + commit. |
| S9 | docs sync | sonnet | sonnet | mechanical text edit. README narrow scope. |
| S10 | version bump | sonnet | sonnet | 한 줄 변경. |
| S11 | final sanity | sonnet | sonnet | 검증 명령 실행만. |
| **PR-level final review** | (전체 diff) | — | **opus** | merge 직전 cross-file 일관성 + spec 회귀 미검출 risk. |

- **opus 권장 step**: S1, S3. spec frozen contract + multi-crate 동시 변경.
- **sonnet 충분 step**: S2, S4~S11.
- **PR-level final review**: opus 로 별 reviewer subagent 호출.

---

## 7. Verifier checklist (final)

본 plan 의 모든 step 완료 시 final verifier 가 통과시킬 checklist (spec §9.3 의 확장):

- [ ] `kebab search '한국'` (fresh V009 KB) → hit ≥ 1.
- [ ] `kebab search '서울'` (fresh V009 KB) → hit ≥ 1.
- [ ] `kebab search '지하철'` (fresh V009 KB) → hit ≥ 1.
- [ ] V009 migration apply 후 기존 V007 KB → eager backfill 자동 시작 (stderr info log 발화) → 완료 후 `kebab search '한국'` → hit ≥ 1.
- [ ] `cargo test --workspace --no-fail-fast -j 1` → 모두 pass (baseline +4 이상).
- [ ] `cargo clippy --workspace --all-targets -j 4 -- -D warnings` → clean.
- [ ] `cargo fmt --all --check` → exit 0.
- [ ] (deferred to P9) `cargo deny check` — 본 PR 은 `cargo tree` SPDX 수동 검증으로 대체. P9 follow-up 으로 별 issue.
- [ ] `grep -c "fts5-v009" crates/kebab-app/src/app.rs` → `≥ 1` (lexical_index_version V009 bump).
- [ ] `grep -c "tokenized_korean_text" migrations/V009__fts_korean_morphological.sql` → `≥ 1`.
- [ ] `grep -rn "short_query_hint" crates/ tests/` → 0 production reference (test 또는 doc-comment 1 줄 허용).
- [ ] README.md / SKILL.md / HANDOFF.md / docs/ARCHITECTURE.md / tasks/HOTFIXES.md 모두 갱신됨 (S9 cascade 5 file).
- [ ] `Cargo.toml` workspace `version = "0.20.1"`.
- [ ] release notes draft (`docs/release-notes/v0.20.1-draft.md`) 작성, 4 단락 본문.
- [ ] `cargo build --release -p kebab-cli -j 4 && ./target/release/kebab --config /tmp/kebab-smoke-v009/config.toml search 'token' --json | jq '.hits | length'` → `0` (영어 substring 매칭 회귀 확인, V009 는 whole-token only).
- [ ] `./target/release/kebab --config /tmp/kebab-smoke-v009/config.toml search 'tokenizer' --json | jq '.hits | length'` → `≥ 1` (whole-token 매칭 정상).
- [ ] `./target/release/kebab --config /tmp/kebab-smoke-v009/config.toml search '한국' --mode hybrid --json | jq '.hits | length'` → `≥ 1` (hybrid mode 회귀 확인 — mode flag 실제 이름은 `--mode hybrid` 또는 `--hybrid` 등 확인).

---

## 8. Constraints

1. **No branch change.** 10 commit 모두 `feat/korean-morphological-tokenizer` branch.
2. **Spec frozen.** ACCEPT spec 의 본문 X edit. deviation → `tasks/HOTFIXES.md`.
3. **Wire schema shape 불변.** spec §11.3 따라 hit ordering / snippet content 만 변화. 신규 wire schema 또는 field add 없음.
4. **Regression budget 0.** baseline workspace test pass 수 (≥ 1370) 유지 + 신규 +4 test 추가.
5. **Worker protocol — subagent skip.** executor 는 nested worker spawn 없이 sequential N step.
6. **Length budget.** 500-800 line plan (본 file ≈ 740 line).
7. **Build path.** `export CARGO_TARGET_DIR=/build/out/cargo-target/target`; `-j 4` default, `-j 1` for workspace sanity.
8. **Commit cadence.** 10 commit (S1~S10). S11 verify-only.
9. **Doc sync.** S9 의 5 file cascade 명시. README narrow scope 보존.
10. **Release trigger.** CLAUDE.md `Release / binary version bump` rule 의 두 트리거 모두 hit (도그푸딩 필요 + design §5.5 변경). v0.20.1 patch release 별 task.
11. **cargo-deny deferred.** Appendix D 의 `cargo deny check` 절차는 본 PR scope 외 (P9 follow-up). license 검증은 `cargo tree` SPDX 수동 + lindera/ko-dic 의 fail-fast 정책으로 대체.

---

## Changelog
- 2026-05-28 closure-r1-mp: plan closure verifier (sonnet) ACCEPT verdict + 9 micro-patches 적용 (MP-1: feature flag, MP-2~MP-7: AC actionability, MP-8: parallel safety, MP-9: hybrid/english regression checks).
- 2026-05-28 post-implementation: 11 step + 10 follow-up commit 모두 머지 — total 17 implementation commit + 4 docs polish + 1 spec/plan archive = 22 commit on `feat/korean-morphological-tokenizer`. S3 spec compliance reviewer 가 2 blocker (get_chunk read path + 9 AST chunker cascade) 발견 → fix commit. S7 implementer 가 `MIT_QUERY_CHARS` 3→2 cascade 필수 발견 (정당한 scope expansion). S11 sanity 단계에서 V007 trigram-specific test 3 + corpus_revision test baseline + chunk snapshot 10 의 의도된 회귀 update. PR-level final review (opus) 의 4 minor docs finding (README english regression, HOTFIXES paths, hint schema, SKILL.md) 정정.
- 2026-05-28 dogfood evidence: reference fixture (korea-overview.md + korea-compound.md, DOGFOOD.md §2.1bis) 의 14 scenario verify pass — '한국' 4 hit, '서울' 2 hit, '지하철' 2 hit, '서울특별시' 1 hit (ko-dic 분해 `[서울, 특별시]` 증거). spec Appendix B 의 prior-knowledge 가정이 실측에서 일치. HOTFIXES 2026-05-28 entry 의 dogfood verification 표 + spec Appendix B 의 Empirical verification subsection 으로 cross-link.

---

## 9. References

- **Spec contract:** `docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md` (668 line, ACCEPT, frozen).
- **Critic R1:** `docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec-critic-r1.md` (9 finding — 3 critical + 6 major, 모두 r1c 에서 resolved).
- **Critic R2:** `docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec-critic-r2.md` (traceability matrix + ACCEPT verdict).
- **Parent design:** `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` (§5.5 chunks_fts + §9 version cascade).
- **Handoff:** `docs/superpowers/handoffs/2026-05-28-v0.20.x-c-korean-morphological-tokenizer-handoff.md`.
- **V007 trigram migration:** `migrations/V007__fts_trigram.sql` (2026-05-23 v0.17.0).
- **HOTFIXES:** `tasks/HOTFIXES.md` — 2026-05-22 trigram entry + 2026-05-24 build_match_string entry.
- **FTS tests:** `crates/kebab-store-sqlite/tests/fts.rs:407` (V007 verbatim test → rename target).
- **Lexical search:** `crates/kebab-search/src/lexical.rs:205` (build_match_string).
- **CLI hint:** `crates/kebab-app/src/app.rs:98,532,616` + `crates/kebab-tui/src/{app,search,run}.rs` (cascade).
- **lindera:** https://github.com/lindera-morphology/lindera (MIT OR Apache-2.0).
- **lindera-dict-ko-dic:** https://github.com/lindera-morphology/lindera-dictionary (Apache-2.0, MeCab-ko-dic 기반).
- **CLAUDE.md sections:** §The facade rule (App::open_with_config 의 hook 위치), §Versioning cascade (index_version bump + S10 release trigger), §Naming + paths (kebab- prefix), §Spec contract (design §5.5 + plan deviations → HOTFIXES).
