feat(expansion): doc-side expansion 별칭 개별 dense 벡터 + 파생물 캐시(V012)

별칭을 줄별 개별 dense 벡터(sentinel `{chunk}#alias#N`)로 색인하고
boilerplate 청크는 별칭 생성을 skip. 묶음 1벡터 방식은 평균화로 특정
표현이 희석돼 오히려 회귀(13/18)했던 것을 폐기. 변형 일관성 14/18 →
16/18, mean_spread@10 0.222 → 0.111 (나무위키 ~1000 문서 CS corpus).
`kebab-core::strip_alias_suffix` 가 suffix 형과 per-alias 형 둘 다 처리.

파생물 캐시(V012): embedding 벡터 + 별칭 LLM 결과를 청크 내용 해시
키로 캐싱해 재색인 시 내용 불변 청크의 재계산을 skip. cache_key =
blake3(kind ‖ text_blake3 ‖ version_key)[:32], version_key 에
model/prompt/dimensions 포함 → §9 cascade 와 정합(버전 bump 시 자동
miss). 측정: 정답 3개 cold 1879s → warm 13s ≈ 145배. 순수 가산이라
corpus_revision bump 없음. search/ask 는 kebab.sqlite+lancedb 만으로
동작 → 외부 서버 색인 후 DB 만 복사하는 이식 워크플로 가능.

V012 schema migration + 신규 surface 로 workspace version 0.20.2 →
0.21.0 (minor) bump. README/HANDOFF/ARCHITECTURE/HOTFIXES sync.
known limitation: stack·svm 설명형 2개 잔존 + grounded 판정이 부분
인용을 grounded 로 오분류(후속 후보).

측정 상세: docs/superpowers/handoffs/2026-05-31-namu-wiki-alias-cache-study.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 08:24:04 +00:00
parent 0282a81c67
commit a8fd76499c
18 changed files with 1000 additions and 71 deletions

View File

@@ -32,6 +32,8 @@ Cargo workspace, 함수 호출 기반 모듈러 모놀리스. UI binary (`kebab-
| citation 형식 | URI fragment (`path#L12-L34` / `path#p=12` / `path#xywh=0,0,100,50`, W3C Media Fragments) |
| ID 생성 | `blake3(canonical_json(tuple))[..32]` hex |
| RRF fusion_score | `[0, 1]` 정규화 — `2 / (k_rrf + 1)` 로 나눠 mode 간 비교 가능 (post-merge hotfix) |
| doc-side expansion 별칭 (v0.21.0) | 색인 시 LLM 이 청크별 "같은 의미 다른 표현" 별칭 생성. 별칭은 줄별 **개별 dense 벡터**(sentinel `{chunk}#alias#N`)로 색인하고 본문 벡터는 그대로 둠 (묶음 1벡터는 평균화로 희석 → 회귀, HOTFIXES 2026-05-31). boilerplate 청크는 별칭 skip. 검색 시 별칭 hit 는 `kebab-core::strip_alias_suffix` 로 원본 chunk_id 에 매핑. `[ingest.expansion]` default off (opt-in, 청크당 LLM 비용). |
| 파생물 캐시 `derivation_cache` (V012, v0.21.0) | 비싼 ingest 파생물(embedding 벡터 / 별칭 LLM 결과)을 청크 **내용 해시** 키로 SQLite 에 캐싱 → 재색인 시 내용 불변 청크는 재계산 skip. `cache_key = blake3(kind ‖ text_blake3 ‖ version_key)[:32]`; version_key 에 model/prompt/dimensions 포함 → §9 cascade 와 정합(버전 bump 시 자동 miss). 위치 기반 `chunk_id` 와 달리 내용이 같으면 문서·위치 무관 동일 키. 순수 가산 — `corpus_revision` bump 안 함, 손상/삭제돼도 정확성 영향 0(miss → 재계산). search/ask 는 `kebab.sqlite`+`lancedb` 만으로 동작하므로 외부 서버 색인 후 DB 만 복사하는 이식 워크플로 가능 (HOTFIXES 2026-05-31). |
| layout | XDG (`~/.local/share/kebab/`, `~/.config/kebab/`, …) |
전체 frozen 설계는 [docs/superpowers/specs/2026-04-27-kebab-final-form-design.md](superpowers/specs/2026-04-27-kebab-final-form-design.md) 12 sections 참조.
@@ -162,7 +164,7 @@ kebab/
│ ├── p8/p8-1, p8-2 # (2 — 보류)
│ └── p9/p9-1 … p9-5 # (5)
├── crates/
│ ├── kebab-core/ kebab-config/ # 도메인 + 설정 (P0)
│ ├── kebab-core/ kebab-config/ # 도메인 + 설정 (P0). kebab-core/src/derivation.rs = 파생물 캐시 키 순수 함수 (blake3 내용 해시, v0.21.0)
│ ├── kebab-source-fs/ # 워크스페이스 walk + checksum (P1-1)
│ ├── kebab-parse-md/ # Markdown frontmatter + blocks + types + ParsedBlock → CanonicalDocument lift (P1-2/3/4 — v0.19.0 흡수)
│ ├── kebab-chunk/ # heading-aware + pdf-page-v1 + code-*-ast-v1 (Tier 1) + k8s-manifest-resource-v1 + dockerfile-file-v1 + manifest-file-v1 + tier2_shared (P10-2) + code-text-paragraph-v1 (P10-3) chunker (P1-5, P7-2, P10-1A-2, P10-1B, P10-1C-Go, P10-1C-JK, P10-2, P10-3, P10-1D)
@@ -175,7 +177,7 @@ kebab/
│ │ ├── manifest_file_v1.rs # Tier 2 (p10-2): whole-file Cargo.toml / go.mod / .json / .xml / .groovy
│ │ ├── code_text_paragraph_v1.rs # Tier 3 (p10-3): blank-line paragraph + 80/20 line-window fallback
│ │ └── tier2_shared.rs # Tier 2 (p10-2): shared oversize fallback + Chunk builder helpers
│ ├── kebab-store-sqlite/ # SQLite + FTS5 (V001/V002/V003) (P1-6, P2-1, P3-3)
│ ├── kebab-store-sqlite/ # SQLite + FTS5 (V001/V002/V003) (P1-6, P2-1, P3-3). src/derivation_cache.rs = derivation_cache 테이블 저장소 (V012, v0.21.0)
│ ├── kebab-search/ # Lexical + Vector + Hybrid retriever (P2-2, P3-4)
│ ├── kebab-embed/ kebab-embed-local/ # Embedder trait + fastembed adapter (P3-1, P3-2)
│ ├── kebab-store-vector/ # LanceDB VectorStore (P3-3, P7-3 follow-up)
@@ -186,11 +188,11 @@ kebab/
│ ├── kebab-parse-image/ # ImageExtractor + Ollama OCR + caption (P6)
│ ├── kebab-parse-pdf/ # lopdf per-page text extractor (P7-1)
│ ├── kebab-parse-code/ # tree-sitter AST extractors: Rust (P10-1A-2), Python + TypeScript + JavaScript (P10-1B), Go (P10-1C-Go), Java + Kotlin (P10-1C-JK — java.rs + kotlin.rs), C + C++ (P10-1D — c.rs + cpp.rs); chunker lives in kebab-chunk
│ ├── kebab-app/ # facade (P0 시그니처 + P3-5/P6-4/P7-3 본체)
│ ├── kebab-app/ # facade (P0 시그니처 + P3-5/P6-4/P7-3 본체). src/expansion.rs = 별칭 생성, src/derivation_payload.rs = 캐시 payload 인코딩 (v0.21.0)
│ ├── kebab-tui/ # Ratatui shell + Library 패널 (P9-1)
│ ├── kebab-mcp/ # stdio MCP server — tools: schema, doctor, search, ask (P9-FB-30)
│ └── kebab-cli/ # binary (P0 → 핫픽스로 --config flag wiring 강화)
├── migrations/ # SQLite refinery V001/V002/V003
├── migrations/ # SQLite refinery V001..V012 (V012 = derivation_cache, v0.21.0)
└── fixtures/ # 테스트 fixture 트리
```

View File

@@ -0,0 +1,108 @@
# 나무위키 대규모 측정 — doc-side expansion 별칭 효과 + 파생물 캐시
> 2026-05-31. Phase 2 doc-side expansion(별칭) 의 효과를 실사용 규모(한국어 나무위키
> corpus)로 검증하고, 그 과정에서 드러난 별칭 생성 비용 문제를 "내용 해시 기반 파생물
> 캐시"로 해결한 기록. 선행: `2026-05-30-phase2-doc-expansion-kickoff.md`,
> 설계: `../specs/2026-05-30-dense-alias-vectors-design.md`,
> `../specs/2026-05-31-derivation-cache-design.md`.
## 1. 출발 질문 (사용자 제기)
측정을 진행하며 사용자가 던진 질문들이 설계를 단계적으로 교정했다:
1. **"테스트 모수가 너무 적지 않나? 더 넓게(대규모, 영+한 혼합) 테스트하자."**
→ 기존 8~32개 golden 으로는 "변형 일관성 개선"이 우연인지 실재인지 판단 불가.
2. **"실사용은 약 2천 개 한국어 위키 문서다."** + 기존 크롤링한 나무위키 parquet
(`/build/cache/namu-crawler/pages.parquet`, 119만 문서) 제공.
→ 측정 corpus 를 실사용에 맞춤. 노이즈는 크게, 별칭은 정답 문서에만(비용).
3. **"정답과 주제가 완전히 다르면(야구·게임) 검색이 너무 쉬워 별칭 효과가 과소평가된다.
실사용은 한 개발조직 위키 = 유사 주제 밀집이다."**
→ 노이즈를 정답과 같은 분야(CS/IT)로 교체. 진짜 어려운 "유사 경쟁" 환경 구성.
4. **"대조군(정답 없는 질문)도 측정하자."** → false-positive(별칭이 노이즈를 grounded
answer 로 끌어오는지) 검증.
5. **"별칭 벡터 생성이 너무 오래 걸린다(18문서 2.5시간). 캐싱이 절실하다 — 별칭뿐 아니라
비용 큰 모든 데이터에."** → 내용 해시 기반 파생물 캐시 설계·구현.
6. **"비싼 계산을 외부 CPU ollama 서버에서 하고 결과 DB 파일만 가져오고 싶다. 가능한가?"**
→ KB 이식성 검증.
## 2. corpus 구축
- 소스: 나무위키 덤프 119만 문서(`pages.parquet`, redirect 제외 완료).
- **노이즈 979개**: 본문 3k~30k자 + "분류" 헤더에 CS 키워드(컴퓨터공학·프로그래밍·알고리즘
…)가 있는 문서 ~70% 정밀도로 필터 → 무작위 샘플(CCleaner·LLaMA·SQL·멀티스레딩 등).
정답과 같은 임베딩 공간(유사 주제 밀집)이라 현실적 난이도.
- **정답 18개**: 명확한 CS 개념(경사하강법·TCP·정렬·이진탐색·뮤텍스·정규표현식 …),
전부 한국어 문서 → 영어 변형은 자동으로 cross-lingual(영→한) 시나리오.
- **변환 핵심 교훈**: nawiki `text_extracted` 는 **개행 0**인 한 덩어리라 md 청커(단락
경계 분할)가 거대 청크(4000+토큰)를 만들어 e5 512토큰 한계에서 잘렸다. → `html`
컬럼을 pandoc(`-f html -t markdown_strict-raw_html`)으로 변환 + base64/링크 정제 →
헤딩·단락 구조 복원 → 청크 중앙값 272토큰으로 정상화.
- golden: 변형 18그룹 × 4변형(한국어 용어 / 영어 용어 / 동의어·약어 / 설명형) + 대조군 10
(`/build/dogfood/namu_golden.yaml`).
## 3. 측정 결과
### 3.1 변형 일관성 (search run, hybrid k=50)
| 구성 | fully_consistent | A(MisRanked) | B(Missing) | mean_spread@10 |
|------|------------------|--------------|------------|----------------|
| baseline (별칭 off) | 14/18 | 2 | 2 | 0.222 |
| 별도-벡터 (별칭 묶음 1벡터) | 13/18 | 2 | 3 | 0.278 (악화) |
| **개선 (별칭 개별 벡터 + boilerplate skip)** | **16/18** | 1 | 1 | **0.111** |
- baseline 약점은 **전부 "설명형" 변형**(용어·약어·영어는 18그룹 전부 완벽). 자연어 설명이
문서 전문용어와 어휘가 멀어 벡터 검색이 못 잡음 = "어휘 격차".
- **별도-벡터(묶음)가 오히려 악화**한 원인 진단: ① 청크당 별칭 8개를 줄바꿈으로 묶어 한
벡터로 임베딩 → 평균화로 특정 표현 **희석** ② 나무위키 메뉴(boilerplate) 청크에도 별칭
생성 → 18문서 공통 노이즈.
- **개선판**: 별칭을 줄별 **개별 sentinel 벡터**(`{orig}#alias#N`) + boilerplate 청크 skip.
→ linked_list·sorting 회복, tcp 회귀 복구. 남은 약점은 stack·svm 설명형 2개.
### 3.2 대조군 (RAG run, refusal_correctness)
- refusal 0.6 (대조군 10개 중 6개 정상 거부, 4개 grounded).
- **false-positive 4개(graphql·oauth·react·grpc)의 인용 출처는 전부 노이즈 본문**
(GitHub_Mobile·API·Svelte), **별칭 sentinel 인용 0** → 별칭이 false-positive 를
유발하지 않음(별칭 무죄). 게다가 answer 는 "근거에서 찾을 수 없다"고 정직히 거부했는데
grounded 판정이 "부분 언급 인용 있음"을 grounded 로 오분류 → 실제 refusal 은 0.6 보다 높음.
(kebab grounded/refusal 판정의 별도 개선 여지 — HOTFIXES 후보.)
### 3.3 정답 RAG
- 변형 72개 중 대부분 grounded=True + 정답 문서 다수 인용(sort 28·linked_list 23 등). 양호.
## 4. 파생물 캐시 (V012)
별칭 18문서 재생성 2.5시간이 근본 병목. `chunk_id``ordinal+span`(위치) 기반이라
chunk_id 캐싱은 중간 수정 시 무력 → **청크 text 내용 해시**를 키로 한 범용 캐시 설계.
- `derivation_cache(cache_key, kind, payload, created_at, last_used_at)` (SQLite, V012).
- `cache_key = blake3(kind ‖ text_blake3 ‖ version_key)`. version_key 에 model/prompt/
dimensions 포함 → §9 cascade 와 정합(버전 bump 시 자동 miss).
- 적용: embedding(본문 + 별칭 벡터 양쪽) + 별칭 LLM. korean_tokens 는 우선순위 낮아 보류.
- **측정: 정답 3개 cold 1879초(31분) → warm 13초 ≈ 145배.** 18문서 환산 시 2.5h → ~80s.
derivation_cache 1237 엔트리(alias 140 + embedding 1097).
## 5. KB 이식성 (외부 계산 워크플로)
- `storage_path`(asset 절대경로)는 search/ask 경로에서 **사용처 0** — 저장·재처리에서만.
- **search/ask 는 `kebab.sqlite` + `lancedb` 만으로 동작**(asset 불필요).
- 실증: 원본 KB 와 다른 경로로 복사한 portable KB(asset 제외)의 search 결과가 score·순서·
문서까지 **완전 동일**.
- 결론 워크플로:
```
[외부 CPU ollama 서버] 같은 corpus + 같은 e5 모델/버전 + 같은 parser/chunker/embedding 버전
kebab ingest → 별칭 LLM + embedding (비싼 계산, 캐시 워밍)
↓ kebab.sqlite(+derivation_cache) + lancedb/ 만 복사
[로컬] kebab search/ask → 계산 0. 증분 수정 시 외부 캐시가 머신 독립적으로 히트.
```
## 6. 결정 / 후속
- **채택**: 별칭 개별 sentinel 벡터 + boilerplate skip(효과·안전 입증) + 파생물 캐시(V012).
- **보류**: stack·svm 설명형 2그룹 추가 개선, korean_tokens 캐시, 이식용 캐시 export/import
명령, 별칭 default-on 여부(현재 off-by-default, 실사용 관찰 후 재결정).
- **별도 이슈**: grounded/refusal 판정이 부분 인용을 grounded 로 오분류 — 정직한 거부가
false-positive 로 집계됨.
- 측정 데이터: corpus `/build/dogfood/corpus/markdown/namu-wiki/`,
golden `/build/dogfood/namu_golden.yaml`, 로그 `/build/dogfood/logs/`.

View File

@@ -0,0 +1,155 @@
# 내용 해시 기반 파생물 캐시 (Derivation Cache)
> 작성 2026-05-31. 비용 큰 ingest 파생물(embedding 벡터 / LLM 별칭 / 한국어 형태소)을
> 청크 **내용 해시** 키로 캐싱해, 문서 갱신·재색인 시 변경되지 않은 청크의 재계산을 없앤다.
## 1. 문제
현재 kebab ingest 는 **doc 단위 skip**(`try_skip_unchanged`, lib.rs:894)만 한다. 변경된
문서는 모든 청크를 재파싱·재청킹·재임베딩·재별칭한다(`put_chunks` 가 doc 의 청크를
통째 DELETE 후 재INSERT — documents.rs:113, embedding/alias/tokens 무조건 재계산).
측정 증거: 정답 18개 문서의 별칭 재생성에 **2.5시간**(gemma LLM, doc 당 ~39청크).
embedding 도 전체 재계산. 문서 한 줄만 고쳐도 동일 비용이 든다. 실사용(나무위키
~2천 문서) 시 재색인이 비현실적으로 느리다.
`chunk_id``id_for_block``ordinal + span`(ids.rs:160) 때문에 **위치 기반**이라,
chunk_id 를 캐시 키로 쓰면 중간 수정 시 뒤 청크가 전부 무효화된다 → 캐시 키는
**청크 text 의 내용 해시**여야 위치와 무관하게 재사용된다.
## 2. 목표 / 비목표
**목표**
- ingest 시 청크별로 (embedding, alias, korean_tokens) 를 내용 해시로 캐싱.
- 캐시 히트 시 비싼 계산(embedder.embed / LLM.generate / lindera tokenize)을 건너뜀.
- 모델/프롬프트/토크나이저 버전을 캐시 키에 포함 → §9 version cascade 와 정합
(버전 변경 시 자동 cache miss → 재계산).
- 별칭뿐 아니라 비용 큰 파생물 전반에 동일 메커니즘.
**비목표**
- 청크 단위 diff (put_chunks 의 전체 DELETE/INSERT 는 그대로 둔다 — chunks 행 재생성은
싸다). 캐시는 *계산*만 절감한다.
- chunk_id 생성 방식 변경 (위치 기반 유지 — frozen 동작).
- doc 단위 skip(`try_skip_unchanged`) 변경 (그대로, 캐시와 독립).
## 3. 설계
### 3.1 캐시 키
```
cache_key = blake3_hex( kind || 0x00 || text_blake3 || 0x00 || version_key )[:32]
```
- `text_blake3` = blake3(chunk.text 의 NFC 정규화 UTF-8 bytes).
- `kind` ∈ { "embedding", "alias", "korean_tokens" }.
- `version_key` (kind 별, 버전 변경 시 캐시 무효화):
- embedding: `{model_id}|{model_version}|{dimensions}`
- alias: `{prompt_version}|{max_aliases_per_chunk}|{model}` (model="" 면 LLM 기본)
- korean_tokens: `{tokenizer_version}` (현재 lindera 고정 → 상수 "lindera-v1";
추후 토크나이저 교체 시 bump)
text 내용이 같고 버전이 같으면 문서·위치·chunk_id 와 무관하게 동일 cache_key.
### 3.2 저장소 — SQLite `derivation_cache` 테이블
신규 마이그레이션 `V012__derivation_cache.sql`:
```sql
CREATE TABLE derivation_cache (
cache_key TEXT PRIMARY KEY, -- §3.1
kind TEXT NOT NULL, -- 'embedding' | 'alias' | 'korean_tokens'
payload BLOB NOT NULL, -- kind 별 인코딩 (§3.3)
created_at TEXT NOT NULL,
last_used_at TEXT NOT NULL -- LRU 정리용
);
CREATE INDEX idx_dcache_kind ON derivation_cache(kind);
CREATE INDEX idx_dcache_last_used ON derivation_cache(last_used_at);
```
- `corpus_revision` 은 bump 하지 않는다 — 캐시 테이블 추가는 기존 데이터 무효화가
아니다(순수 가산). 단 V012 자체는 schema migration 이라 release bump 트리거(§Versioning).
### 3.3 payload 인코딩
- embedding: `dimensions × f32` little-endian 바이트열 (1024×4 = 4096 B/청크).
- alias: 별칭 묶음 문자열의 UTF-8 (현행 `chunk.aliases` 와 동일 형식 — 줄바꿈 join).
- korean_tokens: 토큰 문자열 UTF-8.
### 3.4 ingest 흐름 변경 (kebab-app lib.rs)
각 파생물 생성 직전에 캐시를 조회한다. 의사코드:
```rust
// --- 별칭 (lib.rs ~1259) ---
if expansion.enabled {
for chunk in &mut chunks {
let key = cache_key("alias", &chunk.text, &alias_version_key);
if let Some(p) = cache.get(&key)? { // 히트
chunk.aliases = Some(String::from_utf8(p)?);
} else if is_nav_boilerplate(chunk) { // (기존 skip 규칙 유지)
chunk.aliases = None;
} else { // 미스 → LLM
chunk.aliases = generator.generate(chunk);
if let Some(a) = &chunk.aliases { cache.put(&key, "alias", a.as_bytes())?; }
}
}
}
// --- embedding (lib.rs ~1309) ---
// 1) 각 청크 cache_key 계산 → 히트/미스 분리
// 2) 미스 청크만 emb.embed(&miss_inputs) (배치 축소)
// 3) 미스 결과를 캐시에 put
// 4) 히트 vector + 미스 vector 를 합쳐 VectorRecord 생성 → lance upsert
// (별칭 dense 벡터도 동일하게 alias text 의 embedding 을 캐시; 별칭 개별 벡터는
// 각 별칭 문자열 text 로 embedding cache_key 재사용 → 별칭 임베딩도 캐시 적중)
// --- korean_tokens (chunker 내부 또는 호출부) ---
// tokenize 직전 cache 조회, 미스만 lindera 호출.
```
핵심: **embedding 캐시는 청크 본문 + 별칭 문자열 양쪽에 적용**된다. 별칭 dense 벡터도
"같은 별칭 문자열"이면 재사용된다(별칭 LLM 캐시 + 별칭 임베딩 캐시 2중 절감).
### 3.5 무효화 / 정리
- **버전 무효화**: version_key 가 cache_key 에 포함 → model/prompt/tokenizer 버전이 bump
되면 새 키가 되어 자동 miss(옛 엔트리는 고아). §9 cascade 와 자동 정합.
- **고아 정리**: `kebab doctor` 또는 ingest 종료 시, `last_used_at` 이 N일(기본 30) 지난
엔트리를 삭제하는 경량 GC. 또는 테이블 행수가 임계(기본 50만) 초과 시 LRU 삭제.
(정리 정책은 plan 에서 상수화; 초기엔 30일 TTL 만.)
- 캐시는 **순수 성능 레이어** — 손상/삭제되어도 정확성 영향 없음(miss → 재계산).
`kebab reset` 시 함께 비워진다(같은 sqlite).
### 3.6 정확성 보장
- 캐시 히트가 재계산과 **동일 결과**임을 보장하는 근거: embedding/LLM/tokenize 는 같은
입력(text) + 같은 버전에서 결정적이어야 한다. embedding(e5, temperature 무관) ✓.
LLM 별칭은 `temperature=0.0, seed=0`(config) 라 사실상 결정적 — 단 LLM 비결정성은
"캐시가 첫 생성 결과를 고정"하는 것이라 오히려 일관성↑(허용).
- 버전 키 누락이 가장 위험한 실패 모드(옛 모델 벡터 재사용). version_key 에 모든
cascade 인자를 넣고, 테스트로 "버전 변경 → cache miss" 를 고정한다.
## 4. 컴포넌트 / 파일
- `migrations/V012__derivation_cache.sql` — 신규 테이블.
- `kebab-core``derivation_cache_key(kind, text, version_key) -> String` 순수 함수
(도메인, 다른 crate 의존 없음). text NFC 정규화 + blake3.
- `kebab-store-sqlite``DerivationCache` 저장소: `get(key) -> Option<Vec<u8>>`,
`put(key, kind, payload)`, `touch(keys)`(last_used 갱신), `gc(ttl_days)`.
`DocumentStore` 또는 별도 trait.
- `kebab-app` lib.rs ingest hook — 별칭/embedding 캐시 조회·저장 통합. embedding 미스
배치 분리 로직.
- `kebab-chunk` — korean_tokens 캐시(선택, 우선순위 낮음 — embedding/LLM 이 주 비용).
## 5. Allowed / forbidden deps
- `kebab-core` 의 키 함수는 순수(blake3 + unicode-normalization 만). 다른 kebab-* 금지.
- 캐시 저장소는 `kebab-store-sqlite`. UI crate 직접 접근 금지(facade 경유).
- `kebab-app` 만 캐시를 오케스트레이션(ingest 경로).
## 6. 측정 / 검증
- 동일 corpus 2회 ingest: 1회차(cold) vs 2회차(warm, 전부 캐시 히트) 시간 비교.
warm 재색인이 별칭 LLM 0회·embedding 0회여야(로그로 hit/miss 카운트 노출).
- 정답 18 문서 별칭: cold 2.5h → warm ~수십초(캐시 히트) 목표.
- golden eval: warm 재색인 후 variant 16/18 + refusal 동일(결과 불변 = 캐시 정확성).
- 버전 bump 시뮬: prompt_version 변경 → 별칭 전부 miss(재계산) 확인.
## 7. Risks / notes
- LLM 별칭의 미세한 비결정성: 캐시가 첫 결과를 고정하므로 재현성은 오히려 향상.
단 "더 나은 별칭" 재생성을 원하면 prompt_version bump 로 무효화.
- payload BLOB 크기: embedding 4KB/청크 × 캐시 엔트리. 50만 엔트리 ≈ 2GB. TTL/LRU 로 관리.
- V012 는 schema migration → release version bump 트리거(CLAUDE.md §Versioning).
- 본 설계는 frozen design contract(§9 versioning)의 *의미*를 바꾸지 않는다(캐시는 그
위의 성능 레이어). design 문서 수정 불필요; cascade 안전성만 version_key 로 보장.