리뷰 두 건에서 나온 지적을 반영한다. 1) 실패 양상이 바뀐 것을 다루지 않았다 (MEDIUM) `chunk_id` 로 행을 찾던 때는 shadow 정렬이 어긋나도 느릴 뿐 정확했다. rowid 로 찾으면 어긋난 순간 `chunks_ad` 가 남의 문서 shadow 행을 지우고 아무 오류도 내지 않는다. 즉 이 PR 은 실패 양상을 "느림"에서 "조용한 오삭제"로 바꿨는데, 그 불변식이 눈에 안 보이는 상태였다. `kebab doctor` 에 `fts_shadow` 점검을 넣었다. 전수 대조는 60만 chunk 에서 33초라 doctor 앞에 둘 수 없어 rowid 범위 앞뒤 200행씩만 본다 — 실측 10 ms 이고, 현실적인 드리프트가 취하는 전면 재번호는 잡는다. 표본이라는 사실을 detail 에 적어 정렬 증명으로 읽히지 않게 했다. `SqliteStore::fts_shadow_misaligned_sample` 이 질의를 들고 있다. 2) VACUUM 위험을 과장했다 (정정) 초안이 "VACUUM 이 rowid 를 다시 매길 수 있고 그러면 정렬이 깨진다"고 단정했다. 실제로 재보니 다시 매기지 않았다 — 실제 KB 사본(60만 chunk, 문서 3,000건을 지워 rowid 에 구멍을 낸 뒤)과 소형 합성 DB 양쪽에서 VACUUM 후 전수 대조 불일치가 0 이었다 (sqlite 3.53.4). SQLite 문서가 "다시 매길 수 있다"고 적은 것은 보장이 없다는 뜻이지 실제로 그렇게 한다는 뜻이 아니다. 문구를 실측대로 고쳤다. 남는 실제 경로는 앞으로 `chunks` 를 테이블 재작성 방식으로 바꾸는 마이그레이션이다. V016 주석에 "그런 마이그레이션은 repopulate 를 같이 돌려야 한다"는 울타리를 박았다. 3) 같은 실측치를 파일마다 다르게 적었다 (MEDIUM) 삭제 시간이 커밋 메시지·HOTFIXES 는 2.0초, 마이그레이션 주석·테스트 독스트링·설계 문서는 0.73초였다. 0.73초는 손으로 마이그레이션한 사본을 따뜻한 캐시에서 잰 값이고 2.0초는 릴리스 바이너리가 마이그레이션한 새 사본에서 잰 값이다. 보수적인 2.0초로 통일했다. '한국' hit 수도 15,837(문서 200건 삭제 후) 과 15,977(전체 코퍼스) 이 섞여 있어 15,977 로 통일했다. 4) docs/ARCHITECTURE.md 디렉토리 트리가 V001..V015 로 멈춰 있었다 (MEDIUM) V016 까지로 갱신. README 는 손대지 않는다 — 새 서브커맨드·플래그·config 키·`--json` 필드가 없다. 5) 잔가지 (LOW) `:=` 검사가 번들 SQLite 의 FTS5 idxStr 인코딩에 기대는 것을 assert 메시지에 적었다 (rusqlite 를 올린 직후 실패하면 거기부터 보라는 뜻). 가상 테이블은 항상 `SCAN` 으로 찍히므로 `SEARCH` 로 대체 검사할 방법이 없다는 것도 독스트링에 남겼다. `kb index --rebuild-fts` 라는 옛 이름 + 존재하지 않는 명령 참조 두 곳을 지웠다. `fts_v016_shadow_probe_detects_forced_drift` 로 탐지 자체를 시험한다 — 어긋난 shadow 행을 억지로 만들어 점검이 잡는지 본다. 잡지 못하는 점검은 없느니만 못하다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
25 KiB
Architecture
kebab 의 내부 구조 — crate 의존성, 디렉토리, 핵심 기술 결정. 사용자 사용법은 README.md, 진척도는 HANDOFF.md, frozen 설계 계약은 docs/superpowers/specs/2026-04-27-kebab-final-form-design.md, 머지 후 발견된 deviation 은 tasks/HOTFIXES.md.
한 줄
Cargo workspace, 함수 호출 기반 모듈러 모놀리스. UI binary (kebab-cli, 미래 kebab-desktop) 가 facade crate (kebab-app) 만 참조. 도메인 / 파이프라인 / 저장소 / 외부 어댑터가 명확한 boundary 로 분리.
핵심 기술 결정 (lock 됨)
| 결정 | 값 |
|---|---|
| 언어 | Rust 2024 (resolver=3, edition 2024) |
| repo | Cargo workspace (single repo, 함수 호출 기반 모듈러 모놀리스) |
| 원본 저장 | filesystem + blake3 content-addressable copy (대용량은 reference + checksum) |
| metadata | SQLite + FTS5 (lexical search + v0.20.1 한국어 형태소 tokenizer via lindera-ko-dic) |
| vector | LanceDB (embedded, model 별 분리 table) |
| Markdown parser | pulldown-cmark. frontmatter 에 title 없으면 첫 H1 → H2 → 첫 paragraph 80 자 → 파일명 순으로 자동 채움 (parser_version = md-frontmatter-v2, 기존 doc 도 다음 ingest 에서 갱신) |
| embedding | fastembed-rs (multilingual-e5-large, 1024d, v0.18.0부터 default 업그레이드). opt-in 대안: Ollama /api/embed (snowflake-arctic-embed-l-v2.0 등). arctic = 설명형 query recall 보강 (v0.26.0, 아래 결정표) |
| 한국어 형태소분석 | lindera-ko-dic (FTS5 외부 tokenizer, v0.20.1) — 2자 이상 한국어 query 지원 |
| LLM | Ollama HTTP (default gemma4:e4b ─ OCR / caption 와 family 통일. 사용자가 더 큰 variant gemma4:26b 등으로 override 가능) |
| 음성 ASR | whisper.cpp (via whisper-rs) — P8 보류, 시스템 dep brainstorm 후 |
| OCR (image) | OcrEngine trait, 2 백엔드: ollama-vision (default, gemma4:e4b) / paddle-onnx (v0.27.0 — PP-OCRv5 ONNX in-process via ort =2.0.0-rc.9, DBNet det + CTC rec, 후처리 min-area rect/unclip pure-Rust, Python 런타임 0). engine 선택은 [image.ocr] engine, 팩토리는 kebab-app::build_image_ocr_engine. e2e CER 0.005 / 큰 페이지 <4초. (HOTFIXES P6-2, 2026-06-04) |
| OCR (PDF, v0.20.0+) | Ollama vision LM (default qwen2.5vl:3b) — post-extract enrichment via kebab-app::pdf_ocr_apply (H-1 resolution). DCTDecode-only v1 (FlateDecode/CCITTFax skip + warning). family asymmetry vs image OCR: PoC alnum 94.79% (qwen2.5vl) >> 27% (gemma4:e4b 받침), 본 단계에서 PDF OCR 만 qwen2.5vl. |
| Image caption | Ollama vision LM, runtime gate image.caption.enabled (default OFF) |
| RAG groundedness 검증 | kebab-nli 의 mDeBERTa-v3 XNLI 가 (packed_chunks, generated_answer) entailment 검사 (fb-41). [rag] nli_threshold > 0 (default 0 = disabled, production 권장 0.5) 일 때 활성 — 미달 시 refusal_reason = nli_verification_failed (LLM self-judge ceiling 보완). 첫 호출 시 ~280 MB ONNX 자동 다운로드 |
| PDF parser | lopdf per-page 텍스트 + scanned-page image extract (page_image::extract_dctdecode_page_image, v0.20.0). chunker_version = "pdf-page-v1" 하드코딩 (HOTFIXES P7-3). parser_version = "pdf-text-v1" 보존 (v0.20 OCR 후에도) — provenance event 로 OCR 사용 차별화. force-reingest 가 v0.19 indexed scanned PDF 의 재처리에 필요. |
| code parser | tree-sitter + tree-sitter-rust / tree-sitter-python / tree-sitter-typescript / tree-sitter-javascript / tree-sitter-go / tree-sitter-java / tree-sitter-kotlin-ng — parser-side (kebab-parse-code), chunker-side 아님 (design §6.3). chunker versions: Rust = code-rust-ast-v1, Python = code-python-ast-v1, TypeScript = code-ts-ast-v1, JavaScript = code-js-ast-v1, Go = code-go-ast-v1, Java = code-java-ast-v1, Kotlin = code-kotlin-ast-v1. (v0.32.0 #220: 9개 언어 chunker 가 단일 CodeAstV1Chunker 로 통합 — for_lang(lang) 가 per-lang chunker_version 라벨을 verbatim 매핑. chunker 는 tree-sitter 미사용·lang 은 SourceSpan::Code 데이터에서 흐르므로 9개 struct 차이는 VERSION_LABEL 문자열뿐이었음 → chunk_id byte-identical.) ast_chunk_max_lines = 200 상수 고정 (HOTFIXES 2026-05-19 — Chunker trait 이 per-medium config 미노출). Kotlin grammar 은 tree-sitter-kotlin-ng 사용 — bare tree-sitter-kotlin 은 tree-sitter 0.21–0.23 에 고착되어 있어 사용 불가. Tier 2 (p10-2): YAML/k8s → serde_yaml_ng + k8s-manifest-resource-v1 (apiVersion+kind per resource), Dockerfile → dockerfile-file-v1 (whole-file), Cargo.toml/go.mod/.json/.xml/.groovy → manifest-file-v1 (whole-file). Tier 2 chunkers live in kebab-chunk; no tree-sitter grammar needed (structure from file type, not AST). Tier 3 (p10-3): shell scripts (.sh/.bash/.zsh) direct → code-text-paragraph-v1 (blank-line paragraph segmentation + 80-line / 20-overlap line-window for oversize). Same chunker also serves as fallback when Tier 1/2 emit 0 chunks or Err — non-k8s YAML / invalid YAML / AST extractor failures all picked up. symbol = None; lang preserved from input doc. Tier 1 family complete (p10-1D): C (tree-sitter-c, code-c-ast-v1, .c/.h) + C++ (tree-sitter-cpp, code-cpp-ast-v1, .cpp/.cc/.cxx/.hpp/.hh/.hxx). C symbol = function name only; C++ symbol = namespace::Class::method (recursive nesting). .h 가 C++ syntax 만나면 tree-sitter-c parse 실패 → Tier 3 fallback. |
| symbol path 형식 | workspace path → module path: Python = dotted prefix (kebab_eval.metrics.compute_mrr), TypeScript/JavaScript = slash-style prefix (src/Foo.Foo.search), Go = package.Func / package.(*Receiver).Method, Java/Kotlin = com.foo.Foo.bar (패키지+클래스+메서드/필드), C = 함수명, C++ = namespace::Class::method. Rust 1A-2 는 file-scope nesting 만 (workspace prefix 없음, 비일관 수용 — HOTFIXES 2026-05-20). code chunk 은 citation.kind = "code" + citation.lang + symbol + line range, SearchHit 에 code_lang + repo(.git walk-up 디렉토리명) backfill. |
| Desktop | Tauri 2 + pdfjs-dist (native PDF render backend 금지) — P9-5 |
| 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) |
제거됨 (v0.25.0, HOTFIXES 2026-06-03) — 색인-시 청크당 LLM 별칭 생성 + 별칭 검색 채널을 완전히 제거. 별칭 ROI 음수(cross-lingual 은 e5-large 단독으로 충분, 기여는 설명형 +2 그룹뿐인데 대가가 청크당 색인-시 LLM). V013 마이그레이션이 chunk_aliases_fts + chunks.aliases DROP. 기존 KB 의 잔존 별칭 벡터는 검색 시 strip_alias_suffix 로 본문 chunk 에 매핑(graceful)되거나 kebab reset 으로 정리. spec: docs/superpowers/specs/2026-06-03-remove-doc-expansion-spec.md. |
|
파생물 캐시 derivation_cache (V012, v0.21.0) |
비싼 ingest 파생물(embedding 벡터)을 청크 내용 해시 키로 SQLite 에 캐싱 → 재색인 시 내용 불변 청크는 재계산 skip. cache_key = blake3(kind ‖ text_blake3 ‖ version_key)[:32]; version_key 에 model/dimensions 포함 → §9 cascade 와 정합(버전 bump 시 자동 miss). 위치 기반 chunk_id 와 달리 내용이 같으면 문서·위치 무관 동일 키. 순수 가산 — corpus_revision bump 안 함, 손상/삭제돼도 정확성 영향 0(miss → 재계산). search/ask 는 kebab.sqlite+lancedb 만으로 동작하므로 외부 서버 색인 후 DB 만 복사하는 이식 워크플로 가능 (HOTFIXES 2026-05-31). namespace-agnostic 스키마(kind TEXT + payload BLOB). 현재 kind = embedding + ocr + caption — ocr/caption 은 v0.31.0(#217)에서 이미지/PDF OCR·caption 산출물을 소스 바이트 키로 캐싱(full-struct serde payload)해 비싼 비전 엔진(paddle ONNX / ollama-vision)을 §9 버전-캐스케이드에서 분리. (별칭 LLM 캐싱 kind 는 v0.25.0 에서 제거.) |
| provenance 출처 필터 (v0.29.0) | 혼합 출처 KB 의 레버 = 질의 시 출처 필터링 (전역 trust 가중 아님). config [[workspace.sources]](각 id/root/trust_level/source_type) → documents.source_id 컬럼(V014, additive·DEFAULT 'default') stamp + 검색 --source <id> / --source-type <type>(lexical+vector 두 site, OR). 단일 root 는 implicit default source 로 정규화(config v3→v4 step_3_to_4 미러). per-source trust/type 는 frontmatter 부재 시 기본값(우선순위 frontmatter > source 기본값 > Primary). 전역 trust 곱셈가중(weighted-RRF)은 반증 — A/B 에서 θ=0.85 만으로 incident MRR 0.918→0.340 절벽(점수 압축), 작은 오염 잡으려다 큰 개선 버리는 see-saw 라 빌드 안 함. 필터는 see-saw 없음. (HOTFIXES 2026-06-21) |
| layout | XDG (~/.local/share/kebab/, ~/.config/kebab/, …) |
전체 frozen 설계는 docs/superpowers/specs/2026-04-27-kebab-final-form-design.md 12 sections 참조.
핵심 구현 불변식 (durable invariants)
삭제된 task spec(p1-5/p3-3/p4-3/p7-2)에서 추출 + 현재 코드 대조 검증 (2026-06-27 doc-reorg). 코드가 진실이며 아래는 비자명한 불변식의 요약.
VectorStore upsert 순서/원자성
kebab-store-vector의 upsert는 SQLite-first, Lance-second 두 단계 쓰기로 구현되며, embedding_records 테이블의 3-상태 마커(pending/committed/tombstone)로 crash 재조정을 보장한다. Phase 1: status='pending', vector_committed=0으로 모든 행을 SQLite에 INSERT OR REPLACE (단일 tx). Phase 2: Lance MergeInsert (chunk_id 키). Phase 3: 성공 시 status='committed'로 UPDATE (WHERE status='pending'). Phase 2-3 사이 crash 시 행은 pending으로 남고 다음 upsert가 자동 재시도(Lance는 chunk_id로 dedup). search는 embedding_records와 JOIN 시 WHERE status='committed' 필터를 적용 → 부분 쓰기 Lance 고아 행이 검색에 절대 노출 안 됨. tombstone은 chunk 삭제 GC 재조정용 예약(구현 P+). (V003 마이그레이션, store.rs crash-recovery 주석.)
RAG score-gate + context budget
- Score gate (
config.rag.score_gate, default 0.30): 검색 top-1 점수가 임계값 미만이면 LLM 호출 없이 즉시 refusal —refusal_reason = ScoreGate,grounded = false, 답변에 임계 미만 후보 상위 3개를 나열, citations 는 그 후보들(unmarked). - Context packing budget:
max_context_tokens(default 8000) − (시스템 프롬프트 + 질문 토큰 + 64). 청크를 순서대로 fetch 해[#{marker}] source={source_id} trust={trust} doc={path} heading={heading_path} span={citation}헤더 + 본문으로 pack, 예산 초과 시 중단(단 gate 통과 청크 ≥1 은 항상 pack). 토큰 추정 =chars/4. - Completion budget(LLM max_tokens):
llm.context_tokens() − (시스템+질문 + 256 reserve), 최소 64 — packing budget 과 독립 계산(설계 §6.4). - Drift: 헤더에
source=/trust=필드 추가됨(p9-fb-32 rag-provenance-label); 원본 p4-3 spec 은doc=/heading=/span=만.
Markdown 청킹 우선순위 (md-heading-v2)
Phase 1 (v1-동등): 제목 경계 우선(절대) → 코드 블록 절대 불분할 → 표 단일 청크 원칙 → 긴 섹션 문단 분할 → heading_path 전파 → source_spans 병합. Phase 2 (oversize 후처리): config.ingest.chunking.max_chunk_tokens 초과 청크를 라인(→UTF-8 char) 경계로 분할, 조각은 원본의 block_ids/source_spans/heading_path 공유(블록-단위 인용) + #seg{i} 접미사로 id 구분. 예산은 policy_hash 에 8바이트로 fold → §9 cascade. (원본 p1-5 는 v1; v2 oversize 후처리는 v0.30.0 label-bump — HOTFIXES 2026-06-24.)
chunk_id 충돌 회피 (split-key variant)
한 source block 이 여러 chunk 로 분할될 때(PDF 1 page → N page-chunk, code 1 unit → M AST chunk) 동일 block_ids 로 chunk_id(= blake3(doc, chunker_version, block_ids, policy_hash))가 충돌하는 걸, policy_hash 슬롯을 chunk 별로 파라미터화해 회피한다. pdf-page-v1 = {base_policy_hash}#c{segment_start}[s{i}](segment_start = pre-overlap 경계, 단조증가), code-ast-v1 = {base_policy_hash}#L{line_start}, md-heading-v2 = #seg{i}. 단일 조각은 접미사 생략. 원본 base_policy_hash 는 Chunk.policy_hash 에 보존(audit). (설계 §4.2 collision 회피.)
PDF OCR 엔진 선택 (PoC 근거)
text-detect-first + vision LLM fallback(always-on 아님 — latency 측정 후 always-on 초기안 철회). single-binary 원칙(CLAUDE.md 코어)상 native-dep 엔진(Tesseract/EasyOCR/Paddle-py 런타임) 배제 → Ollama-hosted qwen2.5vl:3b 채택: scanned 한글 PoC alnum 94.79%(Tesseract 86.96% / EasyOCR 89.76% / gemma4:e4b 27% 대비 우위). v0.27.0+ 는 paddle-onnx(in-process ONNX, native Python 런타임 0)도 선택지. 상세 측정표는 git history(삭제된 v0.20 handoff).
crate 의존성 그래프
그룹 단위 view + 컴포넌트별 상세는 docs/components/.
flowchart TB
subgraph UI ["UI binary"]
cli["kebab-cli"]
mcp["kebab-mcp<br/>(P9-FB-30)"]
desktop["kebab-desktop<br/>(P9-5)"]
end
app["kebab-app<br/>(facade)"]
subgraph Ingest ["ingest pipeline"]
srcfs["kebab-source-fs"]
pmd["kebab-parse-md"]
ppdf["kebab-parse-pdf"]
pimg["kebab-parse-image"]
paud["kebab-parse-audio<br/>(P8 보류)"]
pcode["kebab-parse-code<br/>(P10-1A-2 + P10-1B + P10-1C-Go + P10-1C-JK + P10-2 + P10-3 + P10-1D)"]
chunk["kebab-chunk"]
end
subgraph Persist ["persistence"]
sqlite["kebab-store-sqlite"]
vector["kebab-store-vector"]
end
subgraph Adapters ["traits + adapters"]
embedlocal["kebab-embed-local<br/>(fastembed, default)"]
embedollama["kebab-embed-ollama<br/>(Ollama /api/embed, opt-in)"]
llmlocal["kebab-llm-local<br/>(Ollama)"]
search["kebab-search"]
rag["kebab-rag"]
nli["kebab-nli<br/>(NLI verifier, fb-41)"]
end
eval["kebab-eval"]
config["kebab-config"]
core["kebab-core<br/>(domain types)"]
cli --> app
mcp --> app
desktop --> app
app --> srcfs
app --> pmd
app --> ppdf
app --> pimg
app --> paud
app --> pcode
app --> chunk
app --> sqlite
app --> vector
app --> embedlocal
app --> embedollama
app --> llmlocal
app --> search
app --> rag
app --> eval
app --> config
pmd --> core
ppdf --> core
pimg --> core
paud --> core
pcode --> core
embedlocal --> core
embedollama --> core
embedollama --> config
llmlocal --> core
rag --> search
rag --> sqlite
rag --> nli
app --> nli
nli --> config
search --> sqlite
search --> vector
eval --> app
config --> core
sqlite --> core
vector --> core
chunk --> core
search --> core
rag --> core
srcfs --> core
eval --> core
UI → store/llm/parse 직접 의존 금지. 모든 user-facing 진입은 kebab-app facade 만 통한다 (frozen 설계 §8). kebab-cli 가 --config <path> flag 를 honor 하려면 kebab_app::*_with_config(cfg, …) companion 을 통해 Config 을 명시적으로 thread 하는 패턴 — 자세한 이유는 tasks/HOTFIXES.md 의 --config 항목.
kebab-parse-code 의 외부 tree-sitter grammar crate 의존: P10-1A-2 에서 tree-sitter-rust 추가, P10-1B 에서 tree-sitter-python / tree-sitter-typescript / tree-sitter-javascript 추가, P10-1C-Go 에서 tree-sitter-go 추가, P10-1C-JK 에서 tree-sitter-java / tree-sitter-kotlin-ng 추가, P10-1D 에서 tree-sitter-c / tree-sitter-cpp 추가. 모두 kebab-parse-code 에만 격리 (facade 룰 — UI crate / chunker 가 직접 import 금지). Kotlin 은 tree-sitter-kotlin-ng 사용 (bare tree-sitter-kotlin 은 tree-sitter 0.21–0.23 에 고착 — 사용 불가). v0.18.0+ 부터 kebab-source-fs 는 자체 code_meta 모듈 (lang detect + skip helpers + BUILTIN_BLACKLIST) 을 보유, kebab-parse-code 와 분리 (refactor 2026-05-26). v0.19.0 부터 kebab-parse-md 가 kebab-parse-types (parser intermediate types) + kebab-normalize (CanonicalDocument lift) 두 crate 를 흡수 — 24 → 22 crates, design §3.7b 재작성 (HOTFIXES 2026-05-26). v0.20.1 부터 kebab-search 가 lindera-ko-dic 를 의존해 한국어 FTS5 형태소 tokenizer 지원 — V009 migration 으로 2자 이상 한국어 query 매칭 (Bug #8 closure). pure re-export shim 이던 kebab-embed / kebab-llm (trait 은 이미 kebab-core 소유, mock + test helper 만 보유) 를 kebab-core 의 default-OFF mock feature 로 흡수 — 22 → 20 crates, trait surface · 동작 불변 (test-only + import-rename churn).
임베딩 백엔드 결정표 (v0.26.0)
| provider | 모델 | pooling / prefix | 위치 | 언제 |
|---|---|---|---|---|
fastembed (기본) |
multilingual-e5-large |
mean / query:·passage: |
in-process (onnxruntime) | 기본. 모든 호스트 |
ollama |
snowflake-arctic-embed2 등 |
모델 태그로 추론 / arctic=query:·무접두어 |
원격 HTTP (/api/embed) |
GPU 서버 위임, 측정에 쓴 경로 그대로 재현 |
arctic-embed-l-v2.0 채택 근거: 별칭(doc-side expansion) 제거(v0.25.0) 후 설명형
query 의 recall 보강책. 측정(HOTFIXES 2026-06-03 arctic entry — 도그푸딩 store 는 large_data/out/kebab-dogfood/)에서
arctic = recall@10 130/132 (e5 대비 +7, 색인 1회·per-query 0·LLM 0, 용어 무손실).
Ollama 백엔드(provider = "ollama")로 arctic 모델 사용. e5 → arctic 전환은
embedding_version cascade (모델별 벡터 상이) → 재색인 필요. 기본값 e5 유지라 기존
사용자 무영향. 자세한 내용: tasks/HOTFIXES.md 2026-06-03 arctic entry.
디렉토리 구조
kebab/
├── README.md # 사용자 첫 stop (사용법 / Quick start / Mermaid)
├── HANDOFF.md # 진척도 (phase status / 다음 task)
├── kebab_local_rust_report.md # 최초 설계 보고서 (방향성 + 근거)
├── docs/
│ ├── ARCHITECTURE.md # 이 파일
│ ├── SMOKE.md # 로컬 워크스페이스 직접 돌려보는 절차
│ ├── superpowers/
│ │ ├── specs/
│ │ │ └── 2026-04-27-kebab-final-form-design.md # frozen design (12 sections)
│ │ └── plans/
│ │ └── 2026-04-27-task-decomposition.md # task 분해 implementation plan
│ └── wire-schema/v1/ # JSON Schema 7 (citation, search_hit, answer, …)
├── tasks/
│ ├── INDEX.md # phase 인덱스 + component task 트리
│ ├── HOTFIXES.md # post-merge dated fix 로그
│ ├── _template.md # task spec 작성 템플릿
│ ├── phase-0-skeleton.md … phase-9-ui.md # phase epic (high-level)
│ ├── p0/p0-1-skeleton.md # component task (1)
│ ├── p1/p1-1 … p1-6 # (6)
│ ├── p2/p2-1, p2-2 # (2)
│ ├── p3/p3-1 … p3-5 # (5 — p3-5 = app-wiring, post-spec 추가)
│ ├── p4/p4-1 … p4-3 # (3)
│ ├── p5/p5-1, p5-2 # (2)
│ ├── p6/p6-1 … p6-4 # (4 — p6-4 = image-ingest-wiring 후속 추가)
│ ├── p7/p7-1 … p7-3 # (3 — p7-3 = pdf-ingest-wiring 후속 추가)
│ ├── p8/p8-1, p8-2 # (2 — 보류)
│ └── p9/p9-1 … p9-5 # (5)
├── crates/
│ ├── 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, 단일 CodeAstV1Chunker) + 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-1*, P10-2, P10-3, v0.32.0 #220 통합)
│ │ └── src/
│ │ ├── code_ast_v1.rs # Tier 1: 단일 CodeAstV1Chunker — for_lang(lang) 로 9개 언어(rust/python/ts/js/go/java/kotlin/c/cpp)의 per-lang chunker_version 라벨 매핑 (v0.32.0 #220, 이전 9개 code_*_ast_v1.rs 통합)
│ │ ├── k8s_manifest_resource_v1.rs # Tier 2 (p10-2): YAML multi-doc, apiVersion+kind per resource
│ │ ├── dockerfile_file_v1.rs # Tier 2 (p10-2): whole-file Dockerfile
│ │ ├── 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). src/derivation_cache.rs = derivation_cache 테이블 저장소 (V012, v0.21.0)
│ ├── kebab-search/ # Lexical + Vector + Hybrid retriever (P2-2, P3-4)
│ ├── kebab-embed-local/ # fastembed Embedder adapter (P3-2; trait lives in kebab-core)
│ ├── kebab-embed-ollama/ # Ollama /api/embed Embedder, opt-in provider=ollama (arctic 경로, v0.26.0)
│ ├── kebab-store-vector/ # LanceDB VectorStore (P3-3, P7-3 follow-up)
│ ├── kebab-llm-local/ # Ollama LanguageModel adapter (P4-2; trait lives in kebab-core)
│ ├── kebab-rag/ # RAG pipeline (P4-3)
│ ├── kebab-nli/ # NLI verifier (mDeBERTa-v3 XNLI, fb-41 PR-9a/9b/9c-1)
│ ├── kebab-eval/ # golden query runner + metrics (P5-1, P5-2)
│ ├── kebab-parse-image/ # ImageExtractor + OCR (ollama-vision + paddle-onnx ONNX) + 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 본체). src/derivation_payload.rs = 캐시 payload 인코딩 (v0.21.0)
│ ├── kebab-mcp/ # stdio MCP server — tools: schema, doctor, search, bulk_search, ask, fetch, ingest_file, ingest_stdin (P9-FB-30)
│ └── kebab-cli/ # binary (P0 → 핫픽스로 --config flag wiring 강화)
├── migrations/ # SQLite refinery V001..V016 (V012 = derivation_cache v0.21.0, V013 = drop chunk_aliases v0.25.0, V014 = documents.source_id v0.29.0, V015 = drop chat_sessions v0.31.0, V016 = chunks_fts rowid 삭제 #229)
└── fixtures/ # 테스트 fixture 트리
외부 AI 통합
--json 플래그 가 모든 명령에 붙어 frozen wire schema v1 (schema_version 항상 포함) 을 출력. 외부 도구는 wire 만 의존하면 됨:
- Claude Code / Codex skill — 얇은 wrapper (
kebab search --json/kebab ask --json호출). ~50 lines. - MCP server —
kebab를 stdio MCP server 로 wrap. 모든 LLM client 가 자동으로 사용. - HTTP wrapper —
kebab serve --bind 127.0.0.1:7711(P+, local-only 가치 깨므로 신중).
wire schema 자체는 docs/wire-schema/v1/.
비-목표 (frozen design §11 / §0)
- 다중 사용자 SaaS, K8s 배포, 원격 vector DB
- enterprise RBAC/ABAC, 실시간 협업
- 모든 파일 포맷의 완벽한 parsing
- agent 가 임의로 파일을 수정하는 자동화
- multi-workspace (P+ 후순위)
- LLM-as-judge eval (rule-based
must_contain만) - visual embedding (CLIP) — P+
- desktop app
kebab://protocol handler — P+