본 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
505 lines
27 KiB
Markdown
505 lines
27 KiB
Markdown
# 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 가능.
|