Files
kebab/docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec-critic-r1.md
altair823 8c56ef3010 docs(superpowers): v0.20.x C 한국어 morphological tokenizer spec + plan artifacts
본 commit 은 v0.20.x C task (Bug #8 — 한국어 2자 query 0-hit) 의
4-stage workflow artifact 5 파일을 archive:

- spec.md (668 line, status=accepted): Option A/B/C 비교 + lindera
  Path A (영어 substring 회귀 인정) 결정 + 12 section + 4 Appendix
  (B segmentation evidence, C cost evidence, D license evidence).
- spec-critic-r1.md: 3 critical + 6 major finding (NEEDS_REWRITE).
- spec-critic-r2.md: r1c rewrite 후 traceability matrix (ACCEPT).
- plan.md (750 line, status=accepted): 11 step + dependencies +
  cost optimization routing + 9 closure micro-patches 적용.
- plan-closure-r1.md: traceability matrix + 9 MP 의 origin.

이 artifact 들은 implementation 머지 후 frozen reference. 후속
deviation 은 tasks/HOTFIXES.md 가 source of truth.

Workflow stage:
1. spec drafter (omc team writer, opus)
2. spec critic R1 (omc team critic, opus) — NEEDS_REWRITE
3. spec rewriter r1c (omc team writer, opus) — 7 item fix
4. spec closure R2 (in-process verifier, sonnet) — ACCEPT
5. plan drafter (omc team planner, opus)
6. plan closure (in-process verifier, sonnet) — ACCEPT + 9 MP
7. subagent-driven-development implementation (11 step + 5 follow-up
   + 1 docs polish = 17 commit)
8. PR-level final code review (in-process code-reviewer, opus) —
   Approved with notes (4 minor docs finding, merge as-is)

Branch: feat/korean-morphological-tokenizer
Version: 0.20.1
2026-05-28 12:53:31 +00:00

505 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Spec critic round 1 — 한국어 morphological tokenizer
**Verdict**: NEEDS_REWRITE
**Reviewed by**: critic R1
**Reviewed at**: 2026-05-28
**Target spec**: `docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md`
본 critic round 1 의 결론을 먼저 적자면 **NEEDS_REWRITE** 다. 본 spec 은
방향 (lindera + 별 column pre-tokenize) 자체는 합리적이나, 다음 세 가지 critical
결함이 동시에 존재해 그대로 implementation 으로 넘기면 silent regression /
contractual 모순 / silent stale 데이터 손해 가 동시에 발생할 위험이 있다:
1. `tokenize='trigram'``'unicode61'` 전환을 "English 변경 없음" 으로 선언한
§3 + §9.2 와, 실제 SQLite FTS5 의 substring 매칭 동작 사이의 직접 모순.
2. §9.1 AC + §12.1 release notes 의 "기존 KB 자동 backfill (re-ingest 불필요)"
claim 과, §8.2 의 lazy backfill (`tokenized_korean_text = NULL` 유지) 설계
사이의 직접 모순.
3. unicode61 의 CJK tokenization 동작 (한 syllable run = 단일 token) 에 대한
spec 의 전제와, lindera ko-dic 의 segmentation 결과가 일치하지 않을
가능성 — §9.1 AC 의 2-char `'한국'` query 가 `'한국어'` chunk 에서 hit
한다는 보장이 깨질 수 있음.
이 셋은 모두 design / behavior surface 변경을 요구해 stylistic 수준 fix 로
해소 불가. 아래 finding 별 상세.
---
## Substantive findings
### Finding #1: English substring 회귀 — spec 의 self-contradiction (CRITICAL)
- **Severity**: critical
- **Location**: §3 Non-Goals (line 50), §4.1 표 의 "English 영향: 변경 없음"
(line 66), §6.3 (line 192), §9.2 test `fts_v009_english_substring_retained`
(line 271-275)
- **Issue**:
V007 (`tokenize='trigram'`) 의 핵심 trade-off 는 "**영어 lexical 도 substring
매칭으로 이동**" 이라고 design §5.5 line 1069-1070 + V007 migration line 13-15
+ HANDOFF.md line 42 가 모두 명시. 즉 V007 의 `'token'` query 가
`'tokenizer'` chunk 를 hit 시키는 동작은 trigram 의 결과로 도입된 신
behavior.
V009 spec 은 `tokenize='unicode61'` 로 복귀하면서 §3 / §4.1 / §9 모두에서
"English 변경 없음" 을 claim 하지만 이는 FTS5 의 동작과 정면 모순:
- `unicode61` 은 whole-token (word) 매칭. `'token'` query 는 `'token'`
token 만 매칭하지 `'tokenizer'` 는 매칭 X.
-`tokenized_korean_text` column 은 한국어 morpheme 분해 결과만 채우므로
English token 의 substring 매칭은 어떤 column 에서도 복원되지 않음.
§9.2 의 test 는 이 모순을 그대로 노출:
```rust
#[test]
fn fts_v009_english_substring_retained() {
// Fixture: "the tokenizer normalizes whitespace" chunk.
// Query: "token" → hit (substring of "tokenizer").
}
```
이 test 는 unicode61 위에서 **logically impossible** — 절대 통과 불가.
test 가 통과한다면 그건 unicode61 이 아니라 다른 tokenizer 가 실제 적용
중이라는 뜻이고, test 가 fail 하면 §3 의 "변경 없음" claim 이 위약.
- **Suggested fix**: 두 가지 중 하나로 spec 갱신 필요:
- **Option A** (회귀 인정): §3 Non-Goals 에서 "V007 trigram 의 substring
매칭 유지" 제거. 새 표현: "English lexical 은 unicode61 의 whole-token
매칭으로 환원 — V002 (pre-v0.17.0) 와 동일." §4.1 의 "English 영향: 변경
없음" → "회귀 (substring → whole-token), V002 동일." Release notes
(§12.1) 에 명시. test 이름 `fts_v009_english_whole_token_only` 로 rename
+ assertion 반전 (`'token'` → 0-hit on `'tokenizer'` chunk).
- **Option B** (dual-tokenizer 도입): English 의 substring 매칭을 유지하려면
chunks_fts 의 `text` column 은 trigram 유지, 별 column (예: `kor_text`)
에 unicode61 + morpheme-pre-tokenize. 단일 chunks_fts 의 column 별
tokenizer 가 FTS5 에서 지원 안 되므로 (한 tokenizer per virtual table),
실질적으로 dual virtual table = Option B (Bigram supplement) 와 같은
아키텍처 복잡도 → spec 의 §4.2 권장 근거 자체가 흔들림.
선택 자체는 architect 의 결정 사항이지만, **현재 spec 의 self-contradiction 은
NEEDS_REWRITE trigger**.
---
### Finding #2: 기존 KB 의 자동 backfill claim 위약 (CRITICAL)
- **Severity**: critical
- **Location**: §5.1 backfill INSERT (line 148-154), §8.2 (line 230-233),
§9.1 (line 244) "현재 0 hit → 예상 hit", §9.3 verifier checklist
(line 300), §12.1 release notes (line 365)
- **Issue**:
§12.1 release notes 의 마지막 항목 `"기존 KB 의 자동 backfill (재-ingest
불필요)"` 와 §8.2 의 다음 두 문장 사이에 직접 모순:
> 2. `tokenized_korean_text` 는 초기에 NULL (기존 chunks 에 형태소 분해 불가).
> 3. **Lazy backfill**: 향후 ingest 시 기존 chunks 를 `--force-reingest` 로
> 재처리하면 tokenized_korean_text 채워짐 (선택 사항, user 가 원하면).
§5.1 의 backfill INSERT block 도 마찬가지로 `CASE WHEN tokenized_korean_text
IS NOT NULL THEN ... ELSE text END` — 기존 chunks 는 항상 ELSE branch 를
타서 raw `text` 만 chunks_fts 에 들어감.
Raw `text``'한국문화는오래되었다'` 같은 한국어 chunk 일 때, unicode61
은 CJK character run 을 단일 token 으로 봐서 token = `'한국문화는오래되었다'`.
2-char `'한국'` query 의 unicode61 tokenized form = `'한국'` token → FTS5
는 token 일치만 매칭하므로 0-hit.
즉 V009 migration 이 적용된 **기존 KB 의 한국어 2-char query 는 여전히
0-hit**. 사용자가 `kebab ingest --force-reingest` 를 명시적으로 호출하기
전까지 §9.1 AC 의 "현재 0 hit → 예상 hit" 가 충족 안 됨. v0.17.0 trigram
adoption (V007) 의 "자동 backfill 로 즉시 효과" 사용자 기대 와 정반대.
- **Suggested fix**: 두 가지 중 하나로 spec 갱신 필요:
- **Option A** (eager backfill 채택): V009 migration 본체 내부 또는 첫
`kebab` invocation 의 booting hook 에서 모든 기존 chunks 에 대해
lindera tokenize → `tokenized_korean_text` UPDATE → chunks_fts re-index.
Migration 시점이라 Rust helper 호출이 어려우면 (refinery 는 raw SQL 만
실행), V009 는 schema 만 변경 + `kebab-app` 의 first-boot hook 또는
`kebab reindex-korean` subcommand 로 backfill. §10 위험 표에
"backfill 시간 (KB 크기 비례, 1만 chunk 당 ~30-60s) 동안 search 가
부분 결과 반환" 추가.
- **Option B** (lazy backfill 명시): §12.1 release notes 에서 "자동
backfill" 표현 삭제 → "기존 KB 는 `kebab ingest --force-reingest` 후에
2-char Korean query 활성화" 로 정직하게 표기. §9.1 AC 도 "fresh KB
(V009 이후 ingest) 시나리오에 한정" 으로 scope 좁힘. dogfood
instructions 명시.
- **Cross-link**: §9.3 verifier checklist `[ ] Ingest 후 chunks.tokenized_korean_text
가 모든 한국어 chunk 에 채워짐` 도 "기존 chunk" / "신규 ingest" 분기 모호
— 갱신 필요.
---
### Finding #3: unicode61 CJK tokenization 의 sub-morpheme 매칭 보장 부재 (CRITICAL)
- **Severity**: critical
- **Location**: §9.1 query scenario 1 (line 241-243), §6.2 (line 178-184),
§3 Goals 첫 항목 (line 43)
- **Issue**:
§9.1 의 첫 AC:
```
1. `kebab search '한국'` (2자)
- 예상 hit: Korean wiki 의 "한국어", "한국 문화" 등 포함 chunk.
```
이 시나리오의 hit 보장은 다음 두 가지에 모두 의존:
(a) 별 `tokenized_korean_text` column 에 lindera ko-dic 의 segmentation
결과 (공백 구분) 가 저장 + FTS5 unicode61 이 공백을 token 경계로 인식.
(b) lindera ko-dic 이 `"한국어"` 를 `["한국", "어"]` 로 분해 (즉
sub-morpheme level 분해).
(a) 는 spec 설계상 성립. (b) 는 ko-dic 의 사전 정의에 달림. ko-dic 은
실제로는 `"한국어"` 를 **단일 명사로 등록** 한 경우가 일반적이며, 그렇다면
lindera 의 출력은 `["한국어"]` 한 token. 이때 unicode61 의 tokenization
이후 chunks_fts 의 token 은 `'한국어'` 이고, `'한국'` query token 과는
**다른 token** → 0-hit.
§6.2 의 `'한국문화는오래되었다'` 예시도 `['한국', '문화', '는', '오래', '되',
'었다']` 식 segmentation 을 가정하나, 이게 실제 ko-dic 출력인지 spec drafter
가 검증한 evidence 가 없음. ko-dic 의 명사구 등록 정책상 `'한국문화'`,
`'한국문화는'` 등이 단일 entry 일 수 있고, 그러면 `'한국'` query 매칭은
실패.
본 critic 은 lindera ko-dic 의 실제 output 을 직접 실행해 검증할 수단이
없으므로 (PR 단계의 spike), spec 이 이 검증을 implementation 시점으로
미루는 것은 "구현 후 동작 안 하면 알게 됨" 형태의 design risk. AC §9.1 의
hit 보장 이 design level 에서 사라짐.
- **Suggested fix**: spec drafter 가 spec 단계에서 다음 두 검증을 evidence
로 첨부:
- **검증 1**: 호스트 머신에서 lindera-cli + lindera-dict-ko-dic 으로
`'한국어'`, `'한국문화는오래되었다'`, `'서울특별시'`, `'지하철은
빠르다'` 4-5 가지 fixture 에 대한 실제 tokenization 결과를 spec
appendix 에 기록.
- **검증 2**: 만일 (예상대로) `'한국어'` 가 `['한국어']` 단일 token 으로
나온다면, AC §9.1 의 "한국어 포함 chunk hit" claim 을 삭제하거나, 또는
**N-gram supplement** (1자/2자 sub-token 도 추가 emit) 같은 추가 design
을 §6.2 에 추가. 이 추가는 §4.1 권장 (Option A 의 simplicity) 의 근거
자체를 약화시킴.
이 finding 은 §9.1 AC 의 의미 자체가 implementation-validatable 아닌
상태로 남는다는 점에서 NEEDS_REWRITE.
---
### Finding #4: V007 CI diff-check (`fts_v007_matches_design_section_5_5_verbatim`) 의 운명 미명시 (MAJOR)
- **Severity**: major
- **Location**: §5.3 (line 162-166), 기존 test
`crates/kebab-store-sqlite/tests/fts.rs:407`
- **Issue**:
§5.3 은 신규 test `fts_v009_matches_design_section_5_5_verbatim` 추가만
명시. 기존 `fts_v007_matches_design_section_5_5_verbatim` 의 운명은 침묵.
기존 test 는 `migrations/V007__fts_trigram.sql` 의 `§5.5 verbatim block` 을
design §5.5 의 verbatim 과 일치 비교. design §5.5 가 V009 의 unicode61 +
형태소 column 으로 다시 쓰여지면, **V007 test 가 즉시 fail** —
`migrations/V007__fts_trigram.sql` 의 `tokenize='trigram'` 이 design 의
`tokenize='unicode61'` 와 안 맞으므로.
세 가지 가능한 처리 중 spec 이 어느 쪽을 선택해야 하는지 명시 필요:
1. **Rename + replace**: `fts_v007_matches_design_section_5_5_verbatim` 를
`fts_v009_matches_design_section_5_5_verbatim` 로 rename 하고
migration_block 추출 대상도 V009 로 변경. V007 은 "historical replay 만,
더 이상 design 와 매칭 X" 가 됨. → fts.rs:402-405 의 comment 와 호환
가능 (V002 가 이미 유사 처리).
2. **Two tests**: V007 test 는 그대로 두되 design 비교를 끊고, 신규 V009
test 가 design 비교 담당. V007 test 는 syntactic correctness 만
확인 (migration 파일 존재 + DDL parse) 수준으로 축소.
3. **Delete V007 test**: V002 는 fts.rs comment 에서 "더 이상 design 와
비교 안 함" 으로 표현됐는데, V007 도 동일 운명 명시.
- **Suggested fix**: §5.3 에 위 3 가지 중 권장 옵션 명시 + `tests/fts.rs`
편집 범위 명시. PR scope 에 포함.
---
### Finding #5: chunks 의 트리거 + ingest 파이프라인 순서의 race + double-index 가능성 (MAJOR)
- **Severity**: major
- **Location**: §5.1 chunks_ai trigger (line 121-128), §6.2 pre-tokenize
순서 (line 179-184)
- **Issue**:
§6.2 의 ingest 흐름:
```
1. Chunk 생성 후 → 2. lindera tokenize → 3. Chunk row INSERT 시 tokenized_korean_text pre-fill
```
이 순서가 보장되면 chunks_ai trigger 가 fire 할 때 `new.tokenized_korean_text`
가 이미 채워져 있어 CASE 의 NOT NULL branch 로 정상 indexing.
하지만 §8.2 lazy backfill flow 는:
- 기존 chunk: INSERT 시 tokenized = NULL → chunks_ai 가 ELSE branch
(raw text 만 index) → 이후 user 가 `--force-reingest` 또는 background job
으로 UPDATE chunks SET tokenized_korean_text = '...' → chunks_au 가 DELETE
+ INSERT (CASE 의 NOT NULL branch).
이 두 path 가 같은 chunk 에 대해 동시 발생할 가능성 (예: 동일 chunk_id 가
서로 다른 ingest run 에서 reprocess) 의 race 명시 없음. 또한:
- chunks_au 는 DELETE + INSERT 패턴이라 trigger 가 atomic 한 transaction
안에 실행되긴 함. 그러나 spec §6.2 단계 2 ("lindera tokenize") 와 단계
3 ("INSERT") 가 다른 transaction 이면, 단계 2 실패 시 chunks 는 row 없이
남거나 NULL tokenized 로 INSERT — recovery 정책 없음.
- chunks_ai 의 INSERT 가 `(chunk_id, doc_id, heading_path, text)` 4-column.
V007 migration 의 verbatim block 과 일치하므로 CI diff-check 가 통과하려면
signature 일치해야 함 — 하지만 §5.1 의 trigger body 가 4-column 으로는
맞아도 VALUES 부 는 CASE expression 으로 raw text 와 다름 → CI diff-check
의 "verbatim" 의미가 column 단위인지 statement 단위인지 명확화 필요.
- **Suggested fix**:
- §6.2 단계 명시: lindera tokenize 는 chunk row INSERT 와 **동일
transaction** 안에서 (Rust 측에서 string 계산 후 단일 INSERT). NULL
backfill 경로 외에는 chunks_ai trigger 가 항상 CASE 의 NOT NULL branch
를 타는 invariant 보장.
- CI diff-check 의 "verbatim" 정의 (whitespace-normalized string compare
of the §5.5 block) 가 CASE expression 까지 포함하는지 명시. V007 의
fts.rs:407 test 는 string compare 라 CASE 추가는 verbatim 변경 → design
§5.5 도 같이 변경 필요.
- `tokenize_korean_morphological()` 의 실패 처리 (예: lindera dict load
fail) — fallback (NULL) vs error propagation 정책. spec 침묵.
---
### Finding #6: storage / binary 비용 추정의 evidence 부재 (MAJOR)
- **Severity**: major
- **Location**: §4.1 표 (line 62 "DB 크기 +20-30%"), §10.2 (line 316-317
"Dict 7-10 MB, binary +5-10 MB"), §10.3 (line 322 "Ingest +10-20%")
- **Issue**:
세 estimate 모두 spec 내부에 측정 / 근거 / 참조 없음. 다음 의심점:
1. **DB 크기 +20-30%**: chunks 에 추가되는 `tokenized_korean_text` column
본문 = 한국어 chunk 의 segmented form (대략 +1× 원문) + chunks_fts 가
index 하는 text 가 `tokenized_korean_text || ' ' || text` = 2× Korean
text. Korean-heavy KB (한국어 wiki 위주) 면 chunks 테이블 본체 +50-80% +
chunks_fts shadow ~+100% Korean part 만 — 합산 "+20-30%" 은 영문 위주
KB 에서나 성립할 보수적 lower bound. 한국어 위주 dogfood KB 에서는
훨씬 클 가능성.
2. **Dict size 7-10 MB compressed**: lindera-dict-ko-dic crate 의 실제
uncompressed dict size 는 30-50 MB 수준 (FST + matrix + connection cost
table). Cargo crate 의 packed size 가 ~20-30 MB. Release binary 에
embed (include_bytes!) 시 +20-30 MB 가 정상 추정. "+5-10 MB" 은 LTO 와
strip 의 산술 misapplied 가능성 — dict 자체는 strip 대상 아님.
3. **Ingest +10-20%**: chunk creation 자체가 ingest 의 일부분이고, lindera
tokenize 는 chunk text 길이 비례. 1000-char chunk 당 5-20 ms 라는
§10.3 추정도 lindera benchmark 출처 없음. 대형 PDF (수백 chunk) ingest
에서는 누적 latency 가 +30-50% 가능성 — 별 mitigation 필요.
- **Suggested fix**: spec drafter 가 다음 measurement 을 spec appendix 에
첨부 (PR 단계에서 spike branch 로 측정 가능):
- lindera-ko-dic 의 uncompressed size + cargo packed size + release
binary 의 strip 후 size delta.
- 한국어 wiki fixture KB (예: dogfood-p10b/) 에 V009 적용 전후 SQLite
파일 size delta.
- 같은 fixture 의 `kebab ingest` total time 전후 비교 (warm + cold both).
evidence 없으면 §4.1 의 Option A 선택 근거 (storage 비용 표) 자체가 부실 →
post-merge 에서 "예상보다 큼" 발견 시 design rollback 위험.
---
### Finding #7: search result ordering 변화 / eval baseline drift 미주소 (MAJOR)
- **Severity**: major
- **Location**: §11 (line 332-352)
- **Issue**:
V007 trigram → V009 unicode61 + 형태소 전환은 **token boundary** 자체가 다름
(3-gram vs whole-word vs morpheme). 같은 query 의 BM25 raw score 분포가
완전히 다른 분포로 이동 → hybrid/RRF rank 도 (rank-based 라 어느 정도
완화되긴 하나) hit 의 ordering 이 V007 와 다름. 같은 chunk 가 V007 에서는
rank 3, V009 에서는 rank 7 같은 변동이 자연스러움.
§11.3 의 "search_response.v1 변경 없음 (내부 FTS 구현은 wire-invisible)"
은 schema shape level 에서는 맞음. 그러나 **wire content** (hits[] 의
ordering + snippet) 가 변화하는 사실은 명시되지 않음. 특히:
1. **Eval baseline regression**: `crates/kebab-eval/` 의 goldens.csv 의 expected
chunk_id sequence 가 V007 기준이면, V009 적용 후 fail 가능. spec §11 은
eval runner 의 `config_snapshot_json` 이 `index_version` bump 를 picks up
한다고만 명시 (이는 옳음) — 그러나 baseline regenerate 책임 / 시점이
명시 안 됨. PR scope 의 일부인지 eval P5 follow-up 인지 모호.
2. **search cache (p9-fb-19) 의 corpus_revision invalidation**: §5.2 가
"increment" 만 제시. 그런데 design §9 의 corpus_revision 정의 (table
line 1523) 는 "ingest commit 발생 (ANY new/updated)" — V009 schema
migration 은 ingest commit 이 아님. V009 migration 본체가 직접
corpus_revision 을 +1 해 주지 않으면, 다음 ingest 까지 LRU cache 가
여전히 V007 token 기반 결과를 반환할 위험. 본 spec 은 V009 migration
tail 에 `UPDATE kv SET v = v + 1 WHERE k = 'corpus_revision'` 같은
문장의 명시가 없음.
3. **MCP / TUI / CLI 의 surface 영향 의식 부재**: README.md line 88 +
integrations/claude-code/kebab/SKILL.md line 63 모두 V007 의 substring
매칭 + 3-char hint 를 user-facing 표현으로 명시. V009 는 두 표현 모두
stale (substring 매칭 없어짐 + 2-char query 작동). 본 spec §7.3 은
`short_query_hint` 의 "제거 또는 조건 변경" 만 vague 하게 언급 — README
+ SKILL.md + HANDOFF.md 의 정확한 갱신 범위 미정.
- **Suggested fix**:
- §11.3 갱신: "wire schema 의 shape 은 unchanged 하나, hit ordering /
snippet 내용은 V007 와 다름. eval golden baseline 재생성 PR scope 에
포함."
- §5.2 갱신: V009 migration 의 마지막 statement 로 `UPDATE kv …
corpus_revision` 명시. (또는 첫 ingest 가 자동으로 bump 한다는 정책을
근거로 의도적 생략 명시.)
- §7.3 갱신: `short_query_hint` 의 운명 명확화 (제거 권장 — 2-char 가
유효 query 가 됨). README.md / SKILL.md / HANDOFF.md 의 갱신 범위
명시 (CLAUDE.md §wire schema cascade 의 "shipped integration 동시 갱신"
rule 에 따른 SKILL.md 갱신은 필수).
---
### Finding #8: `disable_korean_morphological` config 노브의 surface + cascade 정의 누락 (MAJOR)
- **Severity**: major
- **Location**: §6.3 (line 193)
- **Issue**:
§6.3 마지막 줄:
> Advanced user 는 `config.toml [rag]` 에 `disable_korean_morphological = true`
> 로 opt-out (legacy unicode61 fallback, V009 migration 후에도 원문 text 만
> FTS index).
다음이 모두 미정:
1. **Config schema 추가**: `kebab-config` crate 의 `[rag]` section 정의 +
default value + serde 처리. 갱신 PR scope 인지 별 PR 인지.
2. **위치 적합성**: `[rag]` 보다는 `[search]` 또는 `[index.fts]` 가
semantic 자연 — `[rag]` 는 retrieval-augmented generation 측 (LLM /
prompt) 노브. 명명 review 필요.
3. **cascade 의미**: opt-out 시 새 chunk INSERT 도 `tokenized_korean_text =
NULL` → 한국어 2-char query 의 hit 가 사라짐. 사용자 기대는 "V007 의
trigram substring 매칭으로 fallback" 일 수도, "V002 의 unicode61 만" 일
수도 있는데 spec 은 후자 (unicode61 only). 이게 의도라면 명시 + 사용자
가이드. trigram fallback 은 V009 migration 이 trigram 을 이미 drop 했으므로
불가능 — 그러면 disable 의 의미는 "한국어 lexical 으로 0-hit 으로 복귀"
에 가까움. 사용자 가치 불명확.
4. **eval runner 영향**: opt-out 시 `lexical_index_version` 값이 달라져야 함
(예: `fts5-v009-no-morpho`) — 그렇지 않으면 같은 index_version 으로 두
다른 동작이 공존하여 eval baseline reproducibility 깨짐. spec 침묵.
- **Suggested fix**: §6.3 의 disable 노브를 다음 중 하나로 명시화:
- **Option A** (drop disable 노브): default-enabled 한 가지만 — 사용자
선택권 없음. binary 의 dict 비용은 모두 부담. simplicity 우선.
- **Option B** (build-time feature 만): `fts_korean_morphological` cargo
feature 로 build-time off → release binary 별도 컷 (영문 전용 사용자용).
runtime 노브 제거. 명료성 ↑.
- **Option C** (현재 spec 유지하되 보강): config 위치 명확화 (`[search]`
권장), default 명시, opt-out 시 index_version suffix 명시, eval
config_snapshot 의 영향 명시.
---
### Finding #9: lindera 의 license + dict source 의 검증 evidence 부재 (MAJOR)
- **Severity**: major
- **Location**: §6.1 (line 175-176), §10.1 (line 308-312)
- **Issue**:
§6.1 + §10.1 모두 "lindera = MIT/Apache-2.0 dual", "lindera-dict-ko-dic =
Apache-2.0 (Korean dict, Google search engine dict 기반)" 라 단정. evidence /
참조 URL / commit SHA 없음.
- ko-dic 은 MeCab-ko-dic (KAIST 기반) 의 fork 인 경우가 많고, 라이센스는
Creative Commons (CC BY-SA) + Apache-2.0 dual 또는 별 라이센스 가능성.
spec 의 "Google search engine dict 기반" 표현은 출처 불분명 — Google
사전이 별도로 존재하지 않거나 misattribution 가능.
- kebab workspace 의 `cargo deny` / `licenses.toml` 의 allow-list 갱신 PR
scope 인지 침묵. Apache-2.0 만 dual-licensed 된 dict 가 아니면 reject 위험.
- **Suggested fix**: §10.1 에 다음 evidence 추가:
- lindera crate 의 정확한 라이센스 SPDX (예: `MIT OR Apache-2.0`) +
Cargo.toml `license` field 인용.
- lindera-dict-ko-dic 의 정확한 SPDX + GitHub repo URL + dict 의 upstream
source (예: MeCab-ko-dic 의 commit) cross-link.
- workspace 의 `deny.toml` license allow-list 갱신 필요 여부 (Apache-2.0
이 이미 allow 면 OK, CC BY-SA 면 추가 필요).
---
## Stylistic / clarity findings (no rewrite required)
- §1 Summary 의 "기존 trigram 의 장점 (영어 substring 매칭, 부분 매칭 지원)
을 보존" — finding #1 의 contradiction 기원이므로 같은 PR 에서 표현 정리.
- §4.1 표의 "Migration cascade" row 의 Option A = "V009 (index_version
bump)" — index_version 만 bump 인지, corpus_revision / schema_version 도
같이 bump 인지 모호. design §9 표 와 매핑 명시 권장.
- §2.1 line 22 "trigram bucket 이 없어" — trigram tokenizer 가 "bucket" 이라는
용어를 안 쓰므로 "3-character gram 의 최소 길이 미만" 같은 표현 권장.
Reader 가 SQLite FTS5 docs 와 직접 비교 가능.
- §6.3 "default 로 feature 포함" — cargo feature 의 default 처리 방식 (예:
`[features] default = ["fts_korean_morphological"]`) 의 정확한 표기 권장.
- §11.1 의 index_version 문자열 `"fts5-v009-korean-morphological"` 의
source-of-truth 위치 (예: `kebab_store_sqlite::FTS_INDEX_VERSION` 상수 또는
`lexical_index_version()` 함수) 명시. `kebab_store_vector::INDEX_VERSION_STR`
과 별 lexical 측 상수가 어디서 정의되는지 spec 침묵.
- §12.1 Release notes 항목은 사용자 도그푸딩 영향에 영향이 큼에도 표현이
bullet point 1줄씩 — CLAUDE.md §Release / binary version bump 의 "친절하고
자세하게 풀어서 설명" 정책 과 미스매치. release notes draft 단계에서
보강 권장.
- §9.1 의 dogfood fixture KB / corpus 미명시 (Finding #7 의 일부) — minor
rewording 으로 reproducibility 명시 가능.
---
## Verdict rationale
세 가지 critical finding (English substring 회귀의 self-contradiction, 기존
KB backfill 의 silent breakage, unicode61 + lindera 의 sub-morpheme 매칭
보장 부재) 와 여섯 가지 major finding (CI guard rename, trigger race,
storage evidence, ordering / cache invalidation, config 노브, license
evidence) 가 동시에 존재. 이 중 finding #1 / #2 는 spec 의 self-contradiction
이라 implementation 시점에서 발견 시 design 자체를 재논의해야 함 — round
1c 단계에서 spec 갱신이 비용 효율적.
minor finding 들만이면 ACCEPT 가능했으나, critical 의 존재로 NEEDS_REWRITE.
---
## Recommended r1c rewrite scope
다음 7 가지를 같은 commit 의 spec rewrite 에 포함 권장:
1. **§3 + §4.1 + §9.2: English regression 명시** — `unicode61` 의 whole-token
매칭으로 환원 사실 인정. Non-Goals 의 "trigram 의 substring 매칭 유지"
조항 삭제 또는 별 column dual-tokenize 설계 추가. §9.2 의
`fts_v009_english_substring_retained` test 를 의도에 맞게 rename +
재작성 (또는 dual-tokenize 채택 시 그대로 유지).
2. **§5.1 + §8.2 + §9.1 + §12.1: 기존 KB backfill 정책 결단** — eager
backfill (V009 migration 또는 first-boot hook) 채택 명시, 또는 lazy 명시
+ release notes / AC 표현 일치. 둘 다 채택 시 trade-off matrix 추가.
3. **§6.2 + §9.1: lindera ko-dic segmentation evidence** — spike branch
결과를 appendix 로 첨부 (`'한국어'`, `'서울특별시'`, `'지하철은 빠르다'`,
`'한국문화는오래되었다'`, `'rust 최적화'` 5-6 fixture 의 실제 tokenize
output). AC §9.1 의 hit 보장 이 fixture 와 일치하는지 cross-check.
4. **§5.3 + §6.2: CI diff-check 및 trigger semantics 명시** — V007 verbatim
test 의 rename / replace / delete 중 선택. trigger 의 CASE expression
포함한 verbatim block 의 design 측 갱신 범위 명시. ingest pipeline 의
"lindera tokenize + chunk INSERT 단일 transaction" invariant 추가.
5. **§4.1 + §10.2 + §10.3: 비용 evidence** — dict size, binary delta, SQLite
file delta, ingest latency delta 의 실측 첨부.
6. **§11 + §7.3 + Surface cascade**: corpus_revision bump 의 V009 migration
tail 명시. eval golden 재생성 책임 명시. `short_query_hint` 의 운명
명시 (제거 권장). README.md / integrations/claude-code/kebab/SKILL.md /
HANDOFF.md 갱신 범위를 PR scope 에 explicit list.
7. **§6.3: config 노브 정리** — `disable_korean_morphological` 의 위치 /
default / disable 시의 fallback 의미 / `lexical_index_version` 영향
명시. 또는 노브 자체를 spec 에서 삭제 (Option A).
위 7 항목이 closure 되면 critic round 2 에서 ACCEPT 가능.