Files
kebab/docs/components/store/README.md
altair823 5e6d977281 docs: CLAUDE 3구역 정책 + DOCS.md 지도 + component README task-spec 링크 정리
doc-reorg(C) Phase 4b+5:
- CLAUDE.md: doc-list 에 DOCS.md/CHANGELOG 추가 + zones(living/계약/증거) 정책 +
  "새 plan/handoff/per-feature spec 누적 금지(결정→HOTFIXES, 구조→ARCHITECTURE,
  릴리스→CHANGELOG)" 명문화. §Spec contract 재작성(task spec 삭제 반영: 유일
  frozen 계약 = 설계 doc, 구현 진실 = 코드). stale per-task-spec 참조 정정.
- docs/components/*/README.md (12): 삭제될 tasks/p*/ task-spec markdown 링크
  제거/plain-text 화 + 흡수된 crate(kebab-normalize→parse-md::normalize,
  kebab-parse-types→parse-md::types) 참조 정정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 12:46:29 +00:00

8.7 KiB

Store

persistence layer 두 백엔드 — SQLite (메타 + lexical FTS + jobs + chat) + LanceDB (per-model vector 테이블). 둘이 협조해 partial-write Lance 행이 caller 에 노출되지 않게 two-phase 보장.

구성 crate

Crate 역할
kebab-store-sqlite DocumentStore + JobRepo + ChatSessionRepo + asset writer + FTS5 lexical 쿼리 + eval 저장. refinery migration runner.
kebab-store-vector VectorStore LanceDB 어댑터. per-model chunk_embeddings_<model>_<dim>.lance/ 테이블. SQLite 와 two-phase write 협조.

구조

classDiagram
    class DocumentStore {
        <<trait kebab-core>>
        put_asset(a)
        put_document(d)
        put_blocks(doc, blocks)
        put_chunks(doc, chunks)
        get_document(id)
        list_documents(filter)
    }
    class VectorStore {
        <<trait kebab-core>>
        ensure_table(model, dim) IndexId
        upsert(recs)
        search(query_vec, k, filters) Vec~VectorHit~
        delete_by_chunk_ids(ids)
    }
    class JobRepo {
        <<trait kebab-core>>
        create / update_progress / finish / list
    }
    class ChatSessionRepo {
        <<trait kebab-core>>
        create_session / get_session / list_sessions
        delete_session / append_turn / list_turns
    }
    class SqliteStore {
        +open(cfg) Self
        +run_migrations()
        +put_asset_with_bytes(asset, bytes)
        +corpus_revision() u64
        +bump_corpus_revision()
        +stale_chunk_ids_at(workspace_path)
        +rebuild_chunks_fts()
    }
    class LanceVectorStore {
        +new(cfg, Arc~SqliteStore~) Self
        -tokio::Runtime current-thread
    }
    DocumentStore <|.. SqliteStore
    JobRepo <|.. SqliteStore
    ChatSessionRepo <|.. SqliteStore
    VectorStore <|.. LanceVectorStore
    LanceVectorStore --> SqliteStore : two-phase coord

Data flow — two-phase upsert (write 경로)

flowchart LR
    Caller["kebab-app"]
    PendingIns["1. SQLite INSERT<br/>embedding_records<br/>status='pending'"]
    LanceWrite["2. Lance MergeInsert<br/>(keyed on chunk_id)"]
    CommitFlip["3. SQLite UPDATE<br/>status='committed'"]
    SearchJoin["search() join<br/>WHERE status='committed'<br/>(pending 행 안 보임)"]
    Caller --> PendingIns --> LanceWrite --> CommitFlip
    SearchJoin -.- CommitFlip
    Crash["프로세스 crash<br/>(phase 2 또는 phase 3 사이)"]
    Retry["다음 upsert 가<br/>idempotent 재시도<br/>(MergeInsert 가 chunk_id 로 dedupe)"]
    LanceWrite -.crash.-> Crash --> Retry

Data flow — re-ingest orphan cleanup (delete 경로)

flowchart LR
    Bytes["asset bytes 변경<br/>(같은 workspace_path)"]
    Purge["purge_orphan_at_workspace_path<br/>(SQLite 의 stale chunk_id 수집)"]
    LanceDel["VectorStore::delete_by_chunk_ids<br/>(LanceDB 행 삭제)"]
    Reingest["새 doc_id / chunk_id 로 정상 ingest"]
    Bytes --> Purge --> LanceDel --> Reingest

주요 type / trait / 함수

SqliteStore (kebab-store-sqlite::store):

  • SqliteStore::open(&kebab_config::Config) -> Result<Self>data_dir/sqlite 파일 열고 Mutex<Connection> 으로 wrap.
  • SqliteStore::run_migrations()refinerymigrations/V001..V005 적용.
  • put_asset_with_bytes(asset, bytes) — content-addressable 저장 ({asset_dir}/<aa>/<bb>/<full_hex> blob), assets.workspace_path UNIQUE 충돌 시 purge_orphan_at_workspace_path 후 재삽입 (HOTFIXES P7-3).
  • corpus_revision() -> u64 / bump_corpus_revision() -> Result<u64>kv 테이블의 monotonic counter (p9-fb-19, search cache invalidation key).
  • stale_chunk_ids_at(&WorkspacePath) -> Vec<ChunkId> — re-ingest 시 LanceDB 에서 지울 행 식별.
  • rebuild_chunks_fts() — FTS5 virtual table 풀 리빌드 (corruption 또는 스키마 변경 시).

SQLite migrations (migrations/):

  • V001 — full P1 schema: assets, documents, document_tags, blocks, chunks, embedding_records, jobs, ingest_runs, answers, eval_runs, eval_query_results.
  • V002chunks_fts (FTS5 virtual, unicode61 remove_diacritics 2) + chunks_ai/ad/au sync triggers. 본문은 frozen 설계 §5.5 verbatim.
  • V003embedding_records.status lifecycle marker (pending/committed) + idx_embed_status. P3-3 two-phase write 의 SQLite 측.
  • V004kv (key TEXT PK, value TEXT) + corpus_revision = '0' seed (p9-fb-19).
  • V005chat_sessions + chat_turns (FK ON DELETE CASCADE) + idx_chat_turns_session (p9-fb-17). spec PR 의 V004 가 p9-fb-19 의 kv 와 충돌해서 V005 로 시프트.

LanceVectorStore (kebab-store-vector::store):

  • LanceVectorStore::new(&Config, Arc<SqliteStore>) -> Result<Self>Arc<SqliteStore> 공유로 two-phase coord.
  • VectorStore trait 구현. 모든 메서드 안에서 private current-thread tokio::runtime::Runtimeblock_on (sync trait → async LanceDB 브리지).
  • 테이블 명: chunk_embeddings_<sanitized_model>_<dim>.lance/ (예: chunk_embeddings_multilingual-e5-small_384.lance/). 모델/차원 변경 시 자동 새 테이블 = embeddings 분리.
  • delete_by_chunk_ids(&[ChunkId]) — default impl 빈 no-op (P7-3 follow-up 으로 LanceVectorStore 만 override).

외부 의존

  • kebab-store-sqlitekebab-core + kebab-config, rusqlite (bundled feature, libsqlite3 시스템 dep 회피), refinery, serde_json, time, blake3, globset.
  • kebab-store-vectorkebab-core + kebab-config + kebab-store-sqlite, lancedb, arrow (Lance schema), tokio (current-thread runtime).
  • 외부 서비스: 없음. 모든 저장 on-disk.

핵심 결정

  • rusqlite bundled feature. : 시스템 libsqlite3 의존을 single-binary 약속이 거부. bundled 가 SQLite source 를 같이 빌드 → kebab 깔면 추가 install 0.

  • per-model Lance 테이블. : 임베딩 모델 swap 시 이전 모델의 vector 가 그대로 남아 있어야 (re-embed 유예) + 같은 chunk 가 두 모델로 동시에 indexed 가능. 테이블 명에 <model>_<dim> 인코딩 → EmbeddingModelId 변경 = 새 테이블, 기존은 read-only 유지.

  • two-phase write (SQLite first, Lance second, status flip). : LanceDB 는 transactional commit 없음 (MergeInsert 도 read-after-write 보장 안 함). SQLite 의 embedding_records.status 가 truth 역할 — searchWHERE status='committed' 로 join 해서 partial-write Lance 행 보이지 않음. 프로세스 crash 후 재 ingest 가 idempotent (Lance MergeInsert 가 chunk_id dedupe).

  • delete_by_chunk_ids 의 default no-op + Lance override. : VectorStore trait 의 default impl 가 빈 no-op 이라 test fake / 미래 다른 backend 가 default 그대로 컴파일됨 (behavioural breaking change 없음). Lance 만 override.

  • byte-edit re-ingest 시 SQLite + Lance 양쪽 stale 청소. : P7-3 의 hot bug — assets.workspace_path UNIQUE 와 upsert_asset_rowON CONFLICT(asset_id) gap 때문에 byte 가 바뀐 같은 path 의 자산이 ingest 실패. purge_orphan_at_workspace_path 가 SQLite 측 stale chunk_id 수집, follow-up PR 이 VectorStore::delete_by_chunk_ids 추가해 LanceDB 에서도 같은 chunk_id 삭제. 둘 다 안 청소하면 orphan vector 누적.

  • chat session storage = V005 (V004 와 충돌). : spec PR 가 chat_sessions 를 V004 로 잡았는데 같은 시점 p9-fb-19 가 kv 테이블을 V004 로 가져감. refinery 는 numeric 충돌 = hard fail. p9-fb-17 가 V005 로 시프트, HOTFIXES 기록.

  • SqliteStoreJobRepo + ChatSessionRepo 도 구현. : 모두 같은 SQLite 파일 안에 있고 같은 Mutex<Connection> 공유. 별 crate 로 분리하면 connection 두 개 필요 → SQLite 가 single-writer 라 contention 만 늘어남. trait 별로는 분리, impl 은 한 store.

  • FTS5 본문 spec verbatim + CI diff-check. : tokenizer (unicode61 remove_diacritics 2) + trigger 본문이 frozen 설계 §5.5 와 byte-for-byte 동일해야 wire 동작 일치. CI 가 diff 검출.

관련 spec / HOTFIXES

  • frozen 설계 §5.1 (meta), §5.2 (assets), §5.3 (documents), §5.4 (blocks), §5.5 (chunks + FTS5), §5.6 (embedding_records two-phase), §5.7 (jobs/ingest_runs/answers/eval_runs), §5.7a (chat_sessions/turns), §6.3 (lance table naming), §7.2 (DocumentStore/VectorStore/JobRepo/ChatSessionRepo): docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
  • task specs: 삭제됨(2026-06-27 doc-reorg) — 설계는 frozen 계약, 동작은 tasks/HOTFIXES.md, 상세 git history.
  • HOTFIXES (P7-3 storage UNIQUE bug + delete_by_chunk_ids follow-up, V004→V005 시프트): tasks/HOTFIXES.md