P6-1/P6-2/P6-3 의 라이브러리 (`ImageExtractor`, `OllamaVisionOcr`,
`apply_caption`) 가 그동안 CLI 에서 보이지 않던 미완 구간을 완성.
이제 `kebab ingest` 가 markdown 외에 이미지 자산을 end-to-end 로
색인하고, `kebab search` / `kebab ask` 가 OCR 텍스트 + caption 으로
이미지를 매칭/인용한다.
## kebab-app
- `[dependencies]` 에 `kebab-parse-image` 추가.
- `ingest_with_config` 진입 시 `image.ocr.enabled` / `image.caption.enabled`
플래그에 따라 `OllamaVisionOcr` / `OllamaLanguageModel` 을 **ingest
세션당 1회** 빌드. 자산 루프에서 trait object 로 공유.
reqwest::blocking::Client 의 내부 Arc 덕분에 알로케이션 비용은
자산 수와 무관.
- 두 어댑터 + ImageExtractor 를 한 묶음으로 `ImagePipeline` 구조체에
담아 `ingest_one_asset` 매개변수 폭증 차단 (clippy::too_many_arguments
대응).
- `ingest_one_asset` 의 markdown-only 가드를 `match media_type` 으로
교체 — Markdown 은 기존 경로, Image(_) 는 새 `ingest_one_image_asset`
로 분기, PDF/Audio/Other 는 종전대로 skipped.
- 신규 `ingest_one_image_asset`:
- bytes 읽기 → `ImageExtractor::extract` (실패 시 caller 가 errors+=1)
- `apply_ocr` (Lenient — 실패 시 ProvenanceKind::Warning 이벤트 +
`IngestItem.warnings` 에 \"ocr_failed: ...\", `block.ocr` 는 None
유지)
- `apply_caption` (동일 Lenient 정책)
- 기존 `MdHeadingV1Chunker` 호출 — 청커는 이미 `Block::ImageRef` 를
단일 청크로 emit
- 기존 persist + embed 시퀀스 그대로 (markdown 과 byte-identical)
- `lang_hint_from_doc` — `Lang(\"und\")` 또는 빈 문자열을 None 으로
매핑 (image-pipeline 어댑터의 build_prompt 가 \"und\" 를 silent drop
하지 않도록 caller 측에서 미리).
## kebab-chunk
- `render_block_text` 의 `Block::ImageRef` 분기를 P6-4 (β) plain
concat 정책으로 교체 — `[alt, ocr.joined, caption.text]` 를 `\\n\\n`
로 join, 빈 부분은 drop. alt 가 비면 `src` 의 basename 으로 fallback
(P6-1 contract 의 defensive guard).
- 신규 unit 테스트 `image_ref_p6_4_plain_concat_drops_empty_parts` —
alt-only / alt+ocr / alt+caption / alt+ocr+caption / 빈 alt → src
fallback 다섯 케이스 모두 검증.
- 기존 `image_ref_emits_own_chunk_zero_tokens` 그대로 통과 — 청커의
per-block dispatch 는 변경 없음, text 렌더링만 갱신.
## 통합 테스트 (kebab-app/tests/image_pipeline.rs)
wiremock 으로 Ollama 를 stub. 5건:
1. OCR-only happy path — 1 PNG + ocr.enabled → 1 doc + 1 chunk emit,
`block.ocr.joined` 가 mock 의 \"Hello World 2026\".
2. OCR + caption 동시 활성 — 두 필드 모두 채워지고 chunk text 에
alt + ocr + caption 세 부분 모두 포함.
3. Lenient 실패 검증 — OCR 503 시 자산은 indexed (kind=New),
`errors=0`, ProvenanceKind::Warning attributed to \"kb-app\",
`IngestItem.warnings` 에 \"ocr_failed:\" 노트.
4. 양쪽 비활성 — `image.ocr.enabled=false && image.caption.enabled=false`
여도 자산은 chunk 1개로 indexed (chunk text=filename), EXIF +
dimensions 그대로 채워짐.
5. 결정성 (re-ingest) — 동일 PNG 두 번 ingest 시 두 번째는
`Updated` + 동일 `doc_id`.
## SMOKE.md
`kebab search --mode lexical \"Hello World\"` 단계를 명령 시퀀스에
추가. `[image.ocr]` / `[image.caption]` config 절 예시 + ingest 시간
추정 (자산당 ~5-10초) 추가. \"책은 P7 PDF 라인으로\" 가이드를 검증
체크리스트 와 \"알려진 동작\" 양쪽에 박음.
## 실 Ollama 통합 검증
192.168.0.47 + gemma4:e4b 기준:
```
$ kebab --config /tmp/kebab-smoke/config.toml ingest
scanned 2 new 2 updated 0 skipped 0 errors 0 (18395 ms)
$ kebab inspect doc <image_doc_id>
parser_version: image-meta-v1
blocks: [{
alt: \"hello.png\",
ocr: \"Hello World 2026\",
caption: \"The image displays the text \\\"Hello World 2026\\\" in a large, black, sans-serif font.\"
}]
$ kebab --json ask \"Hello World 텍스트가 어디에 있나?\" --mode hybrid
grounded: true
citations: [{marker: \"[1]\", doc_path: \"hello.png\"}]
```
## 검증
- `cargo test --workspace --no-fail-fast -j 1` — 전부 pass
- `cargo clippy --workspace --all-targets -- -D warnings` — pass
- `cargo test -p kebab-chunk image_ref` — 2 pass (P1-5 회귀 + P6-4
신규 unit)
- `cargo test -p kebab-app --test image_pipeline` — 5 pass
## 의존성 경계
- `kebab-app` 이 `kebab-parse-image` 추가 — spec Allowed dep 그대로.
- 새 forbidden 침범 없음 (기존 `kebab-tui` / `kebab-desktop` /
`kebab-eval` 미참조 유지).
- 본 task 가 신설하는 image-specific 비즈니스 로직 0줄 — 모두
`kebab-parse-image` 에 위임.
`tasks/p6/p6-4-image-ingest-wiring.md` status: planned → completed.
contract: docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
sections: §3.4 ImageRefBlock, §6.1 ingest pipeline, §7.2
Extractor/Chunker traits, §9.1 image extraction policy.
kebab — Local-first Knowledge Base
상태: P0–P4 구현 완료 (31 component task 중 17 완료) + 3건 post-merge hotfix 적용.
kebab index/kebab search --mode {lexical,vector,hybrid}/kebab ask모두 실 동작. 다음 단계 = P5 (eval suite). 자세한 진행 상황은 tasks/INDEX.md, 머지 후 발견된 버그와 fix는 tasks/HOTFIXES.md.
kebab 는 개인용 로컬 knowledge base + RAG 도구다. Markdown / PDF / 이미지 / 음성을 한 곳에 색인하고, 의미 검색 + citation 포함 LLM 답변을 단일 binary 로 제공한다. 모든 추론은 로컬 (Ollama / fastembed / whisper.cpp) 에서 돌아간다.
대상 하드웨어: M4 48GB MacBook 1대, 사용자 1명.
무엇인가
| 명령 | 동작 | 상태 |
|---|---|---|
kebab init |
XDG 경로에 데이터 디렉토리 + config.toml 생성 | ✅ P0 |
kebab ingest [<path>] |
Markdown 색인 (idempotent). PDF/이미지/음성은 P6+. | ✅ P3-5 |
kebab search --mode {lexical,vector,hybrid} "<query>" |
검색 — citation 포함, hybrid는 RRF fusion | ✅ P3-5 |
kebab list docs |
색인된 문서 목록 | ✅ P3-5 |
kebab inspect doc <id> / kebab inspect chunk <id> |
raw record 보기 | ✅ P3-5 |
kebab ask "<query>" |
RAG 답변 + 근거 인용. 근거 부족 시 거절. Ollama 필요. | ✅ P4-3 |
kebab doctor |
설정/모델/DB 헬스 체크 | ✅ P0 |
kebab eval run / compare |
golden query 회귀 측정 | ⏳ P5 |
기계 친화 모드: 모든 명령에 --json 플래그. 출력은 frozen wire schema v1 (schema_version 필드 항상 포함, 예: ingest_report.v1, search_hit.v1, answer.v1, doctor.v1).
핵심 결정 (lock 됨)
| 결정 | 값 |
|---|---|
| 언어 | Rust 2024 (resolver=3, edition 2024) |
| repo | Cargo workspace (single repo, 함수 호출 기반 모듈러 모놀리스) |
| 원본 저장 | filesystem + blake3 content-addressable copy (대용량은 reference + checksum) |
| metadata | SQLite + FTS5 (lexical search) |
| vector | LanceDB (embedded, model 별 분리 table) |
| Markdown parser | pulldown-cmark |
| embedding | fastembed-rs (multilingual-e5-small, 384d) |
| LLM | Ollama HTTP (default qwen2.5:7b-instruct ─ 사용자 환경에 맞춰 gemma4:26b 등으로 교체 가능) |
| 음성 ASR | whisper.cpp (via whisper-rs) — P8 |
| OCR | Tesseract (default) + macOS Apple Vision sidecar (feature gate) — P6 |
| TUI | Ratatui + crossterm — P9 |
| Desktop | Tauri 2 + pdfjs-dist (native PDF render backend 금지) — P9 |
| citation 형식 | URI fragment (path#L12-L34, W3C Media Fragments) |
| ID 생성 | blake3(canonical_json(tuple))[..32] hex |
| RRF fusion_score | [0, 1] 정규화 — 2 / (k_rrf + 1) 로 나눠 mode 간 비교 가능 (post-merge hotfix) |
| layout | XDG (~/.local/share/kebab/, ~/.config/kebab/, …) |
전체는 docs/superpowers/specs/2026-04-27-kebab-final-form-design.md 참조.
의존성 그래프
kebab-cli, kebab-tui, kebab-desktop
└─> kebab-app
├─> kebab-source-fs
├─> kebab-parse-md / kebab-parse-pdf / kebab-parse-image / kebab-parse-audio
│ └─> kebab-parse-types
├─> kebab-normalize
│ └─> kebab-parse-types
├─> kebab-chunk
├─> kebab-store-sqlite
├─> kebab-store-vector
├─> kebab-embed-local (kebab-embed trait crate)
├─> kebab-search
├─> kebab-llm-local (kebab-llm trait crate)
├─> kebab-rag
├─> kebab-eval
└─> kebab-config
└─> kebab-core (모두 의존)
UI → store/llm/parse 직접 의존 금지. 모든 user-facing 진입은 kebab-app facade 만 통한다 (design §8). kebab-cli 가 --config <path> flag 를 honor 하려면 kebab_app::*_with_config(cfg, …) companion 을 통해 Config 을 명시적으로 thread 하는 패턴 — 자세한 이유는 tasks/HOTFIXES.md 의 --config 항목.
Phase 로드맵
| Phase | 내용 | 핵심 산출 crate | 선행 | 상태 |
|---|---|---|---|---|
| P0 | Workspace 뼈대 + 도메인 계약 + ID recipe | kebab-core, kebab-parse-types, kebab-config, kebab-app, kebab-cli |
– | ✅ 완료 |
| P1 | Markdown ingestion (walk → parse → chunk → SQLite) | kebab-source-fs, kebab-parse-md, kebab-normalize, kebab-chunk, kebab-store-sqlite |
P0 | ✅ 완료 |
| P2 | SQLite FTS5 lexical 검색 + citation | kebab-search (lexical) |
P1 | ✅ 완료 |
| P3 | Local embedding + LanceDB + hybrid (RRF) + kebab-app wiring | kebab-embed, kebab-embed-local, kebab-store-vector, kebab-search |
P2 | ✅ 완료 |
| P4 | Local LLM + RAG + grounded answer | kebab-llm, kebab-llm-local, kebab-rag |
P3 | ✅ 완료 |
| P5 | Golden query / regression eval | kebab-eval |
P4 | ⏳ 다음 |
| P6 | 이미지 ingestion (OCR + caption) | kebab-parse-image |
P5 | ⏳ |
| P7 | PDF text + page citation | kebab-parse-pdf |
P5 | ⏳ |
| P8 | 음성 transcription + timestamp citation | kebab-parse-audio |
P5 | ⏳ |
| P9 | TUI + desktop app | kebab-tui, kebab-desktop |
P5 | ⏳ |
P0P5 직렬. P6P9 P5 이후 병렬 가능.
각 phase 는 component-level 단위로 더 분해되어 있다 (총 31 component task — P3-5 app-wiring 추가). 자세한 분해는 tasks/INDEX.md. 머지 후 발견된 버그/fix 의 dated 로그는 tasks/HOTFIXES.md.
디렉토리 구조
kebab/
├── README.md # 이 파일
├── kebab_local_rust_report.md # 최초 설계 보고서 (방향성 + 근거)
├── docs/
│ ├── superpowers/
│ │ ├── specs/
│ │ │ └── 2026-04-27-kebab-final-form-design.md # frozen design (12 sections)
│ │ └── plans/
│ │ └── 2026-04-27-task-decomposition.md # task 분해 implementation plan
│ ├── SMOKE.md # 로컬 워크스페이스에 직접 돌려보는 절차
│ └── 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-3 # (3)
│ ├── p7/p7-1, p7-2 # (2)
│ ├── p8/p8-1, p8-2 # (2)
│ └── p9/p9-1 … p9-5 # (5)
├── crates/
│ ├── kebab-core/ kebab-parse-types/ kebab-config/ # 도메인 + 설정 (P0)
│ ├── kebab-source-fs/ # 워크스페이스 walk + checksum (P1-1)
│ ├── kebab-parse-md/ # Markdown frontmatter + blocks (P1-2/3)
│ ├── kebab-normalize/ # ParsedBlock → CanonicalDocument (P1-4)
│ ├── kebab-chunk/ # heading-aware chunker (P1-5)
│ ├── kebab-store-sqlite/ # SQLite + FTS5 (V001/V002/V003) (P1-6, P2-1, P3-3)
│ ├── 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)
│ ├── kebab-llm/ kebab-llm-local/ # LanguageModel trait + Ollama adapter (P4-1, P4-2)
│ ├── kebab-rag/ # RAG pipeline (P4-3)
│ ├── kebab-app/ # facade (P0 시그니처 + P3-5 본체)
│ └── kebab-cli/ # binary (P0 → 핫픽스로 --config flag wiring 강화)
├── migrations/ # SQLite refinery V001/V002/V003
└── fixtures/ # 테스트 fixture 트리
빌드 + 실행
# build
cargo build --release
# 첫 실행 — XDG 경로에 config.toml 생성
./target/release/kebab init
# config 손보고
${EDITOR:-vi} ~/.config/kebab/config.toml
# 색인
./target/release/kebab ingest
# 검색
./target/release/kebab search "Markdown chunking 규칙" --mode hybrid
# 질문 (Ollama 필요)
./target/release/kebab ask "내 KB 설계에서 저장소 전략은?"
워크스페이스를 격리해서 직접 돌려보는 패턴은 docs/SMOKE.md 참조 — --config <path> 로 임시 디렉토리에 격리된 KB 를 만들 수 있다.
비-목표 (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+
외부 AI 통합
kebab 의 --json 모드 + frozen wire schema v1 은 외부 자동화의 stable contract. 가능한 통합:
- Claude Code / Codex skill — 얇은 wrapper (
kebab search --json/kebab ask --json호출). ~50 lines. - MCP server —
kebab-mcpbinary (stdio JSON-RPC) 가kebab-appfacade 를 1:1 노출. Claude Desktop / Cursor / Zed 등 공유. - HTTP wrapper —
kebab serve --bind 127.0.0.1:7711(P+, local-only 가치 깨므로 신중).
기여 / 작업 흐름
이 repo 는 단일 사용자 프로젝트지만 spec 변경 절차는 명문화되어 있다.
- frozen design 변경 —
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md가 단일 contract. 변경 시 영향 받는 component task 모두 동시 갱신 필요. PR 1개로 묶기. - 새 component task 추가 —
tasks/_template.md복사 후tasks/p<phase>/p<phase>-<n>-<name>.md생성.contract_sections에 design doc 섹션 명시.Allowed/Forbidden dependencies는 design §8 module-boundary 표 따름. - 구현 — component task 1개당 sub-agent 1세션 권장.
cargo test -p <crate>+ DoD 체크리스트 통과. PR 으로 머지. - 버전 변경 —
parser_version/chunker_version/embedding_version등 변경은 design §9 의 cascade rule 따름. 영향 받는 record 는 재처리 필요. - post-merge 핫픽스 — 머지 후 발견된 버그는 tasks/HOTFIXES.md 에 dated entry 추가 + 영향 받는 task spec 의
Risks/notes에 cross-link 한 줄 추가. 원래 spec 본문은 frozen 으로 두고 HOTFIXES.md 가 live source of truth.
라이선스
미정 (frozen design 에는 MIT OR Apache-2.0 가 workspace.package 의 license 필드로 권장됨; P0 lock 시 결정).
참고
- 최초 설계 보고서: kebab_local_rust_report.md
- Frozen design: docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
- Task 분해 plan: docs/superpowers/plans/2026-04-27-task-decomposition.md
- Task 인덱스: tasks/INDEX.md
- Post-merge 핫픽스 로그: tasks/HOTFIXES.md
- Smoke recipe: docs/SMOKE.md