# 별칭 dense 별도 벡터 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Steps use checkbox (`- [ ]`).

**Goal:** `chunk.aliases`를 별도 dense 벡터(sentinel chunk_id `{orig}#alias`)로 색인해, dense(e5)가 별칭 순수 신호로 설명형 패러프레이즈를 잡게 한다. 본문 벡터 불변(회귀 안전).

**Architecture:** `ingest.expansion.embed_aliases`(default off) on 이면 별칭을 e5 passage 임베딩 → sentinel chunk_id VectorRecord upsert. VectorRetriever 가 sentinel hit 을 원본 chunk_id 로 strip + dedup(2채널 유지, wire 무변경). purge 가 sentinel 벡터도 정리.

**Tech Stack:** Rust 2024, fastembed e5, LanceVectorStore(MergeInsert keyed on chunk_id), kebab-core/config/app/search.

**빌드 규약:** `CARGO_TARGET_DIR=/build/out/cargo-target/target`, `-j 4`. 결과 redirect + `echo "EXIT=$?"` 후 커밋. `cargo|grep` 금지. 브랜치 `feat/doc-side-expansion`(같은 PR).

**참조 spec:** `docs/superpowers/specs/2026-05-30-dense-alias-vectors-design.md`

---

## File Structure

| 파일 | 역할 | Task |
|------|------|------|
| `crates/kebab-core/src/ids.rs` | `ALIAS_SUFFIX` 상수 + `strip_alias_suffix` 헬퍼 | 1 |
| `crates/kebab-config/src/lib.rs` | `IngestExpansionCfg.embed_aliases` + env | 2 |
| `crates/kebab-app/src/lib.rs` | ingest 별칭 임베딩 + sentinel VectorRecord + purge sentinel | 3 |
| `crates/kebab-search/src/vector.rs` | VectorRetriever sentinel strip + dedup + overfetch↑ | 4 |
| `docs/`, dogfood | 측정 + 문서 | 5 |

---

## Task 1: `ALIAS_SUFFIX` + `strip_alias_suffix` (kebab-core)

**Files:** Modify `crates/kebab-core/src/ids.rs` (+ `lib.rs` re-export)

- [ ] **Step 1: 실패 테스트** — `ids.rs` `#[cfg(test)] mod tests` 에:

```rust
    #[test]
    fn strip_alias_suffix_roundtrip() {
        assert_eq!(strip_alias_suffix("abc123#alias"), "abc123");
        assert_eq!(strip_alias_suffix("abc123"), "abc123"); // 접미 없으면 그대로
        assert_eq!(ALIAS_SUFFIX, "#alias");
    }
```

- [ ] **Step 2: 실패 확인** — `CARGO_TARGET_DIR=/build/out/cargo-target/target cargo test -p kebab-core strip_alias_suffix -j 4 > /tmp/dv-t1.log 2>&1; echo "EXIT=$?"` → 컴파일 실패.

- [ ] **Step 3: 구현** — `ids.rs` 상단(pub 영역)에:

```rust
/// 별칭 dense 벡터의 sentinel chunk_id 접미. 본문 벡터(원본 chunk_id)와
/// 별칭 벡터(`{orig}#alias`)를 LanceDB(chunk_id 키)에서 공존시킨다. ChunkId 는
/// blake3 hex(영숫자)라 `#` 미포함 → 충돌 없음. 설계 spec dense-alias-vectors §3.2.
pub const ALIAS_SUFFIX: &str = "#alias";

/// sentinel 별칭 chunk_id 에서 원본 chunk_id 를 복원. 접미 없으면 그대로.
pub fn strip_alias_suffix(id: &str) -> &str {
    id.strip_suffix(ALIAS_SUFFIX).unwrap_or(id)
}
```

`crates/kebab-core/src/lib.rs` 의 `ids` re-export 에 `ALIAS_SUFFIX, strip_alias_suffix` 추가
(`pub use ids::{... , ALIAS_SUFFIX, strip_alias_suffix};` — 기존 `pub use ids::{...}` 목록에 삽입).

- [ ] **Step 4: 통과** — `cargo test -p kebab-core strip_alias_suffix -j 4` EXIT=0.

- [ ] **Step 5: 커밋** — `git add crates/kebab-core && git commit -m "feat(core): ALIAS_SUFFIX + strip_alias_suffix (dense alias vectors)"`

---

## Task 2: config `embed_aliases`

**Files:** Modify `crates/kebab-config/src/lib.rs`

- [ ] **Step 1: 실패 테스트** — `#[cfg(test)] mod tests` 에:

```rust
    #[test]
    fn embed_aliases_defaults_off() {
        assert!(!Config::defaults().ingest.expansion.embed_aliases);
    }

    #[test]
    fn embed_aliases_env_override() {
        let mut cfg = Config::defaults();
        let env: std::collections::HashMap<String, String> =
            [("KEBAB_INGEST_EXPANSION_EMBED_ALIASES".to_string(), "true".to_string())]
                .into_iter().collect();
        cfg.apply_env(&env);
        assert!(cfg.ingest.expansion.embed_aliases);
    }
```

- [ ] **Step 2: 실패 확인** — `cargo test -p kebab-config embed_aliases -j 4 > /tmp/dv-t2.log 2>&1; echo "EXIT=$?"` → 컴파일 실패.

- [ ] **Step 3: 구현** — `IngestExpansionCfg` struct 에 필드(기존 `prompt_version` 다음):

```rust
    /// 별칭을 dense 벡터로도 색인(별도 sentinel chunk_id). default off.
    /// `enabled`(별칭 생성)와 별개 축 — 둘 다 on 이어야 dense 별칭. 설계 spec
    /// dense-alias-vectors §3.3.
    pub embed_aliases: bool,
```

`impl Default for IngestExpansionCfg` 에 `embed_aliases: false,` 추가. `apply_env` 에:

```rust
            "KEBAB_INGEST_EXPANSION_EMBED_ALIASES" => {
                self.ingest.expansion.embed_aliases = parse_bool(v)
            }
```

- [ ] **Step 4: 통과** — `cargo test -p kebab-config -j 4` EXIT=0 (신규 2 + 기존).

- [ ] **Step 5: 커밋** — `git add crates/kebab-config && git commit -m "feat(config): ingest.expansion.embed_aliases flag (default off)"`

---

## Task 3: ingest 별칭 임베딩 + sentinel VectorRecord + purge

**Files:** Modify `crates/kebab-app/src/lib.rs` (embed 블록 ~1309, purge 함수)

- [ ] **Step 1: 구현 (embed 블록)** — `if !chunks.is_empty()` 블록(현재 body inputs/records 생성)을 확장. body records 생성 후 별칭 records 를 추가로 만들어 같은 `upsert` 에 합친다:

기존 body 임베딩(`let inputs = chunks.iter().map(|c| EmbeddingInput{text: c.text.as_str(), ...})` → `vectors` → `records`)은 **그대로**. `vec_store.upsert(&records)` **직전**에 추가:

```rust
            // dense 별칭(별도 벡터, sentinel chunk_id). embed_aliases on +
            // 별칭 있는 청크만. 본문 records 는 위에서 이미 생성됨(불변).
            let mut all_records = records;
            if app.config.ingest.expansion.embed_aliases {
                let alias_chunks: Vec<&kebab_core::Chunk> = chunks
                    .iter()
                    .filter(|c| c.aliases.as_deref().is_some_and(|a| !a.is_empty()))
                    .collect();
                if !alias_chunks.is_empty() {
                    let alias_inputs: Vec<EmbeddingInput<'_>> = alias_chunks
                        .iter()
                        .map(|c| EmbeddingInput {
                            text: c.aliases.as_deref().unwrap(),
                            kind: EmbeddingKind::Document,
                        })
                        .collect();
                    let alias_vectors = emb
                        .embed(&alias_inputs)
                        .context("Embedder::embed (alias vectors)")?;
                    for (c, v) in alias_chunks.iter().zip(alias_vectors) {
                        let alias_chunk_id = kebab_core::ChunkId(format!(
                            "{}{}",
                            c.chunk_id.0,
                            kebab_core::ALIAS_SUFFIX
                        ));
                        all_records.push(VectorRecord {
                            embedding_id: kebab_core::id_for_embedding(
                                &alias_chunk_id, &model_id, &model_version, dimensions,
                            ),
                            chunk_id: alias_chunk_id,
                            vector: v,
                            doc_id: canonical.doc_id.clone(),
                            text: c.aliases.clone().unwrap_or_default(),
                            heading_path: c.heading_path.clone(),
                            model_id: model_id.clone(),
                            model_version: model_version.clone(),
                            dimensions,
                        });
                    }
                }
            }
            vec_store.upsert(&all_records).context("VectorStore::upsert")?;
```

(기존 `vec_store.upsert(&records)` 줄은 위 `upsert(&all_records)` 로 대체 — 중복 upsert 금지.)

- [ ] **Step 2: 구현 (purge sentinel)** — `purge_vector_orphans_for_workspace_path` 의 `delete_by_chunk_ids(&stale)` 를, stale + sentinel 을 함께 지우도록:

```rust
    let mut to_delete = stale.clone();
    to_delete.extend(stale.iter().map(|id| format!("{}{}", id, kebab_core::ALIAS_SUFFIX)));
    vec_store
        .delete_by_chunk_ids(&to_delete)
        .context("VectorStore::delete_by_chunk_ids (orphan vector cleanup)")?;
```

그리고 `sweep_deleted_files` 의 `purge_deleted_workspace_path` 후 `vec.delete_by_chunk_ids(&chunk_ids)`(있는 곳)도 동일하게 `{id}#alias` 를 포함하도록 확장(해당 위치 `grep -n "delete_by_chunk_ids" crates/kebab-app/src/lib.rs` 로 모두 찾아 sentinel 추가).

- [ ] **Step 3: 빌드 + 회귀** — `CARGO_TARGET_DIR=/build/out/cargo-target/target cargo build -p kebab-app -j 4 > /tmp/dv-t3.log 2>&1; echo "EXIT=$?"` EXIT=0. `cargo test -p kebab-app -j 4` EXIT=0(embed_aliases off 라 기존 무영향).

- [ ] **Step 4: 커밋** — `git add crates/kebab-app/src/lib.rs && git commit -m "feat(app): 별칭 dense 별도 벡터 색인 + purge (sentinel)"`

---

## Task 4: VectorRetriever sentinel strip + dedup

**Files:** Modify `crates/kebab-search/src/vector.rs`

- [ ] **Step 1: 실패 테스트** — `crates/kebab-search/tests/` 의 기존 vector 테스트 패턴 확인(`ls crates/kebab-search/tests/ && grep -rln "VectorRetriever" crates/kebab-search/tests/`). store 에 body + `{orig}#alias` 벡터를 넣고, 별칭 벡터에 가까운 쿼리로 검색 시 결과가 **원본 chunk_id** 1개(중복 없음)인지 검증:

```rust
#[test]
fn alias_vector_hit_strips_to_original_and_dedupes() {
    // store 에 chunk "c1" body 벡터 + "c1#alias" 별칭 벡터. 쿼리가 둘 다 매칭.
    // 결과: 원본 "c1" 1개 (sentinel strip + dedup).
    // (기존 vector 테스트 헬퍼로 store fixture 구성 — 벡터/임베딩 mock 패턴 따름.)
    let hits = retr.search(&q).unwrap();
    let c1 = hits.iter().filter(|h| h.chunk_id.0 == "c1").count();
    assert_eq!(c1, 1, "body+alias 둘 다 매칭해도 원본 chunk_id 1개로 dedup");
    assert!(!hits.iter().any(|h| h.chunk_id.0.ends_with("#alias")),
        "sentinel chunk_id 가 결과에 노출되면 안 된다");
}
```

> 정확한 store fixture(벡터 upsert + embed mock)는 기존 `tests/` 의 VectorRetriever 테스트 패턴을 따른다.

- [ ] **Step 2: 실패 확인** — `cargo test -p kebab-search alias_vector_hit -j 4 > /tmp/dv-t4.log 2>&1; echo "EXIT=$?"` → 실패(현재 sentinel 노출 + 중복).

- [ ] **Step 3: 구현** — `vector.rs` `search()`:
  (a) `VECTOR_OVERFETCH_MULTIPLIER` 를 `2` → `3` (별칭 벡터로 dedup 후 k 미달 방지).
  (b) raw_hits 순회 루프에서 strip + dedup. 기존:
  ```rust
        let candidate_ids: Vec<&str> = raw_hits.iter().map(|h| h.chunk_id.0.as_str()).collect();
        let hydration = hydrate_chunks(&self.sqlite, &candidate_ids)...;
        ...
        for hit in raw_hits {
            let Some(meta) = hydration.get(hit.chunk_id.0.as_str()) else { continue; };
            rank = rank.saturating_add(1);
            hits.push(build_hit(hit, meta, rank, ...)?);
            if hits.len() >= k { break; }
        }
  ```
  를 다음으로(원본 id 로 hydrate + seen dedup, build_hit 에 strip 된 chunk_id 반영):
  ```rust
        // sentinel 별칭 hit 을 원본 chunk_id 로 strip 해 hydrate.
        let candidate_ids: Vec<&str> = raw_hits
            .iter()
            .map(|h| kebab_core::strip_alias_suffix(h.chunk_id.0.as_str()))
            .collect();
        let hydration = hydrate_chunks(&self.sqlite, &candidate_ids)
            .context("kb-search vector: hydrate chunk metadata")?;
        ...
        let model_id = self.embed.model_id();
        let mut hits: Vec<SearchHit> = Vec::with_capacity(k.min(raw_hits.len()));
        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
        let mut rank: u32 = 0;
        for mut hit in raw_hits {
            let orig = kebab_core::strip_alias_suffix(hit.chunk_id.0.as_str()).to_string();
            if !seen.insert(orig.clone()) {
                continue; // 같은 원본이 body+alias 둘 다 → 첫(높은 score) 유지
            }
            let Some(meta) = hydration.get(orig.as_str()) else { continue; };
            // build_hit 이 원본 chunk_id 를 쓰도록 hit 의 chunk_id 를 strip 본으로 교체.
            hit.chunk_id = kebab_core::ChunkId(orig);
            rank = rank.saturating_add(1);
            hits.push(build_hit(hit, meta, rank, &self.index_version, &model_id, self.snippet_chars)?);
            if hits.len() >= k { break; }
        }
  ```
  (`raw_hits` 가 `Vec<VectorHit>` 라 `for mut hit` 가능. `VectorHit.chunk_id` 가 `pub` 인지 확인 — `crates/kebab-core/src/vector.rs:24`. pub 아니면 build_hit 시그니처에 override chunk_id 인자 추가.)

- [ ] **Step 4: 통과 + 회귀** — `cargo test -p kebab-search -j 4 > /tmp/dv-t4.log 2>&1; echo "EXIT=$?"` EXIT=0 (신규 + 기존 vector/hybrid).

- [ ] **Step 5: 커밋** — `git add crates/kebab-search/src/vector.rs crates/kebab-search/tests && git commit -m "feat(search): VectorRetriever sentinel 별칭 strip + dedup"`

---

## Task 4.5: V0XX — embedding_records FK 제거 (breaking) + CASCADE 대체

**배경 (spec §3.5)**: sentinel chunk_id 는 chunks 에 없어 `embedding_records.chunk_id REFERENCES
chunks(chunk_id) ON DELETE CASCADE`(V001:100) FK 를 위반(SQLite 787) → ingest 에러. SQLite 는 ALTER
로 FK 못 지워 테이블 재생성. CASCADE 사라지면 orphan 정리를 명시 DELETE 로 대체.

**Files:** Create `migrations/V010__drop_embedding_records_fk.sql` (또는 현재 최신 번호+1 확인:
`ls migrations/` → 최신이 V010__chunk_aliases.sql 이면 **V011**), Modify `crates/kebab-store-sqlite/src/documents.rs`(put_chunks), `crates/kebab-store-sqlite/src/store.rs`(purge 경로)

- [ ] **Step 1: 최신 migration 번호 확인** — `ls migrations/`. doc-side expansion 이 V010__chunk_aliases.sql
  을 추가했으므로 신규는 **V011**. 파일명 `V011__drop_embedding_records_fk.sql`.

- [ ] **Step 2: migration 작성** — `embedding_records` 를 FK 없이 재생성(V003 의 status/vector_committed
  컬럼 + 모든 인덱스 보존). FK 외 스키마는 동일:

```sql
-- V011__drop_embedding_records_fk.sql — embedding_records.chunk_id FK 제거.
-- sentinel chunk_id({orig}#alias, chunks 에 없는 id) 벡터를 허용하기 위함
-- (설계 spec 2026-05-30-dense-alias-vectors-design.md §3.5-1). SQLite 는 ALTER
-- 로 FK 제거 불가 → 테이블 재생성. status/vector_committed(V003) + 인덱스 보존.
-- CASCADE 제거분은 put_chunks/purge 의 명시 DELETE 로 대체(§3.5-2).
PRAGMA foreign_keys=OFF;

CREATE TABLE embedding_records_new (
  embedding_id   TEXT PRIMARY KEY,
  chunk_id       TEXT NOT NULL,                 -- FK 제거 (was REFERENCES chunks ON DELETE CASCADE)
  model_id       TEXT NOT NULL,
  model_version  TEXT NOT NULL,
  dimensions     INTEGER NOT NULL,
  lance_table    TEXT NOT NULL,
  created_at     TEXT NOT NULL,
  status         TEXT NOT NULL DEFAULT 'pending',
  vector_committed INTEGER NOT NULL DEFAULT 0,
  UNIQUE(chunk_id, model_id, model_version, dimensions)
);
INSERT INTO embedding_records_new
  SELECT embedding_id, chunk_id, model_id, model_version, dimensions,
         lance_table, created_at, status, vector_committed
    FROM embedding_records;
DROP TABLE embedding_records;
ALTER TABLE embedding_records_new RENAME TO embedding_records;
CREATE INDEX idx_embed_chunk  ON embedding_records(chunk_id);
CREATE INDEX idx_embed_model  ON embedding_records(model_id, model_version, dimensions);
CREATE INDEX idx_embed_status ON embedding_records(status);

PRAGMA foreign_keys=ON;

UPDATE kv SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE key = 'corpus_revision';
```

> ⚠️ `chunks_bd_tombstone_embeddings` trigger(V003)는 그대로 유지. FK 제거 후 tombstone 이 실제 보존됨
> (CASCADE 가 즉시 안 지움) — 명시 DELETE(Step 3)가 정리를 담당.

- [ ] **Step 3: CASCADE 대체 — 명시 DELETE** — chunk 삭제 경로에서 embedding_records 를 명시 정리.
  `crates/kebab-store-sqlite/src/documents.rs` `put_chunks`(DELETE-then-INSERT, 라인 101 `DELETE FROM
  chunks WHERE doc_id=?` 직전/직후): 해당 doc 의 chunk_id + `{id}#alias` embedding_records 삭제:
```rust
  // CASCADE 제거(V011) 대체: 이 doc 의 chunk 임베딩 레코드를 명시 정리.
  // 원본 + sentinel({id}#alias) 둘 다. (별칭 벡터는 chunks FK 가 없어 자동 정리 안 됨.)
  tx.execute(
      "DELETE FROM embedding_records WHERE chunk_id IN \
       (SELECT chunk_id FROM chunks WHERE doc_id=?1 \
        UNION SELECT chunk_id||'#alias' FROM chunks WHERE doc_id=?1)",
      params![doc.0],
  ).map_err(StoreError::from)?;
```
  (이 DELETE 는 `DELETE FROM chunks` **전에** 실행 — chunks 가 지워지면 서브쿼리가 빈 결과.)
  `crates/kebab-store-sqlite/src/store.rs` 의 `purge_orphan_at_workspace_path`(라인 ~631 `DELETE FROM
  documents`)·`purge_deleted_workspace_path` 도 동일하게, chunks 삭제 전 수집한 chunk_id + sentinel 을
  `DELETE FROM embedding_records WHERE chunk_id IN (...)` 로 정리. (`grep -n "DELETE FROM documents\|DELETE
  FROM chunks" crates/kebab-store-sqlite/src/store.rs` 로 경로 확인.)

- [ ] **Step 4: 테스트** — `crates/kebab-store-sqlite/tests/` 에:
  - sentinel chunk_id embedding_records INSERT 가 FK 위반 없이 성공(V011 후).
  - put_chunks 재호출 시 기존 embedding_records(원본+sentinel) 정리 → orphan 0.
  Run: `CARGO_TARGET_DIR=/build/out/cargo-target/target cargo test -p kebab-store-sqlite -j 4 > /tmp/dv-t45.log 2>&1; echo "EXIT=$?"` EXIT=0 + 기존 corpus_revision baseline(V011 bump 로 +1) 갱신 필요 시 갱신.

- [ ] **Step 5: 커밋** — `git add migrations/V011__drop_embedding_records_fk.sql crates/kebab-store-sqlite && git commit -m "feat(store): V011 embedding_records FK 제거 + CASCADE 대체 명시 DELETE (sentinel 별칭 벡터)"`

---

## Task 4.6: filter_chunks sentinel strip

**배경 (spec §3.5-3)**: `filter_chunks`(filters.rs:81)가 `embedding_records er JOIN chunks c ON
c.chunk_id=er.chunk_id WHERE er.status='committed'` 로 LanceDB 후보를 필터. sentinel chunk_id 는 chunks
JOIN 에서 버려져 VectorRetriever strip 이전에 탈락. sentinel candidate 를 원본으로 strip 해 JOIN 통과시킴.

**Files:** Modify `crates/kebab-store-sqlite/src/filters.rs` (`filter_chunks`)

- [ ] **Step 1: 실패 테스트** — committed 원본 chunk 의 sentinel candidate(`{orig}#alias`)가
  filter_chunks 결과에 (원본 또는 sentinel 로) 통과하는지. (기존 filters 테스트 패턴 따라.)

- [ ] **Step 2: 구현** — `filter_chunks(chunk_ids, filters)` 가 candidate `chunk_ids` 중 sentinel
  (`#alias` 접미)을 **원본으로 strip 해 IN-list/JOIN** 에 넣되, **반환은 입력 candidate 형태(sentinel
  유지)** 로 — VectorRetriever 가 그 sentinel 을 받아 strip+dedup(Task 4)하기 때문. 즉:
  - IN-list 바인딩: 각 candidate 를 `strip_alias_suffix` 한 원본 chunk_id 로 JOIN(committed 판정은
    원본 chunk 기준). 원본이 committed 면 그 candidate(원본 or sentinel) 통과.
  - 반환: 통과한 **원본 candidate 문자열 그대로**(sentinel 포함) — store.search 가 그대로 VectorRetriever 로.
  - 구현 주의: 현재 `er.chunk_id IN (?)` 가 candidate 직접 매칭. sentinel 은 embedding_records 에는
    있으나(V011 후) chunks JOIN 실패. 두 방법 중 택1 — (a) JOIN 을 `c.chunk_id = strip(er.chunk_id)` 로
    (SQL 에서 `#alias` 제거: `replace(er.chunk_id,'#alias','')` 또는 `rtrim`), 또는 (b) Rust 에서
    candidate 를 원본으로 strip 해 IN-list 구성 후, 결과를 원본 candidate 와 매핑해 반환. **(b) 권장**
    (SQL replace 보다 명확). `kebab_core::strip_alias_suffix` 사용.

- [ ] **Step 3: 테스트 통과 + 회귀** — `cargo test -p kebab-store-sqlite -p kebab-search -j 4 > /tmp/dv-t46.log 2>&1; echo "EXIT=$?"` EXIT=0.

- [ ] **Step 4: 커밋** — `git add crates/kebab-store-sqlite/src/filters.rs && git commit -m "feat(store): filter_chunks sentinel 별칭 candidate strip (committed 통과)"`

---

## Task 5: 측정 + 문서

- [ ] **Step 1: clippy** — `cargo clippy --workspace --all-targets -j 4 -- -D warnings > /tmp/dv-clippy.log 2>&1; echo "EXIT=$?"` EXIT=0.

- [ ] **Step 2: 측정** — `.kebabignore`(topics 만) 재작성 → release 빌드 → `KEBAB_INGEST_EXPANSION_ENABLED=true KEBAB_INGEST_EXPANSION_EMBED_ALIASES=true kebab ingest --force-reingest`(topics 재임베딩, 별칭 벡터 생성, ~32분) → `KEBAB_EVAL_GOLDEN=... kebab eval run --mode hybrid --k 50` → `eval variants`. **Read 로 값 확인(추측 금지).**
  - **효과**: 영어 설명형(mvcc/raft) `recall@50` 0→양수 회복? concat PoC(6/0/2/0.25) 대비 개선?
  - **회귀**: body 벡터 불변이라 명사형/단일쿼리 회귀 0 확인. 측정 후 `.kebabignore` 삭제.

- [ ] **Step 3: 문서** — `tasks/HOTFIXES.md` dated entry(lexical 별칭 + dense 별칭 측정 표), README Configuration(`embed_aliases` off 기본), ARCHITECTURE(별칭 dense sentinel 벡터), HANDOFF.

- [ ] **Step 4: 커밋** — `git add tasks/HOTFIXES.md README.md docs/ARCHITECTURE.md HANDOFF.md && git commit -m "docs: dense 별칭 측정 결과 + 문서 동기화"`

---

## Self-Review

- **Spec 커버리지**: §3.2 sentinel→Task1. §3.3 config→Task2, ingest embed→Task3, retriever dedup→Task4, purge→Task3. §5 측정→Task5. §7 테스트→각 Task. ✅
- **Placeholder**: Task4 Step1 store fixture 는 "기존 패턴 따름"으로 위임(단언 핵심 명시). VectorHit.chunk_id pub 여부는 "확인 후 분기" 지시. 나머지 완성 코드. ✅
- **타입 일관성**: `ALIAS_SUFFIX`/`strip_alias_suffix`(Task1, kebab_core) ↔ ingest(Task3)·retriever(Task4) 사용. `embed_aliases`(Task2 config) ↔ ingest(Task3). VectorRecord 필드(Task3) = 기존 body records 와 동일 구조. ✅

---

## Execution Handoff

OMC teammate(sequential single-team). Task1·2=sonnet(작은), Task3·4=opus(임베딩/retriever 핵심). Task3/4 후 code-reviewer(opus, sentinel dedup·purge 정확성·회귀). Task5 측정은 main 세션 직접.
