Files
kebab/docs/SMOKE.md
altair823 3a57cab1eb fix(kebab-store-sqlite): purge stale assets row on workspace_path orphan + smoke
P7-3 통합 테스트가 노출한 storage 레이어 버그 fix.
`assets.workspace_path` 의 UNIQUE 제약과 `upsert_asset_row` 의
`ON CONFLICT(asset_id)` 만 처리하던 gap 사이 — byte 가 변경된 자산
re-ingest 시 새 asset_id 가 같은 workspace_path 에서 secondary UNIQUE
충돌. md / image / pdf 모두 영향.

Fix:
- 새 helper `purge_orphan_at_workspace_path` 가 같은 `workspace_path`
  의 *다른* `asset_id` 를 발견하면 documents → assets 순서로 sweep.
  documents 의 ON DELETE RESTRICT 회피 + CASCADE 로 blocks / chunks /
  embedding_records 정리. copied 모드면 storage_path 의 byte 파일도
  best-effort 삭제.
- `put_asset_with_bytes` 의 두 분기 (copy / reference) + `DocumentStore
  ::put_asset` 모두 호출.
- 회귀 테스트 `put_asset_with_bytes_sweeps_workspace_path_orphan` (이전
  의 "UPSERT 실패시 orphan 청소" 테스트가 더 이상 doable 하지 않으므로
  대체).
- `re_ingest_edited_pdf_produces_new_doc_id` integration `#[ignore]` 해제 →
  9 통합 테스트 모두 default 로 통과.

Vector store orphan 은 별도 P+ task — LanceDB 가 SQLite cascade 와 무관하게
운영되므로 stale chunk_id vector 가 디스크에 남음. 검색에는 영향 없음 (search 가
SQLite join 통해 surface).

Smoke 검증 (release binary, markdown 2 + image 1 + PDF 2):
- doctor pass
- 첫 ingest: 5 new
- list docs: 5 docs all media types
- search lexical "pdf-page-v1 chunker" → whitepaper.pdf hit
- search hybrid → cross-media 결과
- inspect doc PDF: parser_version=pdf-text-v1, blocks 가 SourceSpan::Page
- 동일 byte re-ingest: 5 updated, 0 errors (P1 idempotency)
- byte 수정 후 re-ingest: 1 new (해당 PDF) + 4 updated, 0 errors (storage fix)
- corrupt PDF 추가: errors+=1 + IngestItem.error 메시지 정확, 다른 자산 영향 0
- 정리 후 다시 ingest: errors=0
- RAG ask: PDF 인용 + `citations[].citation` 에 `kind: "page"` + `page: <N>` +
  `path: <pdf_path>` 정확히 노출

운영 fixture 보조:
- `crates/kebab-parse-pdf/examples/gen_smoke_pdf.rs` — `cargo run --release
  --example gen_smoke_pdf -p kebab-parse-pdf -- <out.pdf> <text-pages>` 로
  reportlab/qpdf 없이 in-tree PDF 생성.
- `crates/kebab-parse-image/examples/gen_smoke_png.rs` — 동일 방식의 PNG
  fixture 생성.
- SMOKE.md 가 두 example 사용법 + 갱신된 HOTFIXES 동작 (byte 수정 시
  errors+=1 → new+=1) 반영.

HOTFIXES `2026-05-02 P7-3` entry 가 \"deferred\" → \"fixed in same PR\" 로
업데이트, vector store orphan caveat 만 남음.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:41:23 +00:00

12 KiB
Raw Blame History

title, date
title date
kebab 스모크 실행 가이드 2026-05-01

kebab 스모크 실행 가이드

P3-5 머지 후 (kebab-app::ingest / search / list / inspect 와이어링) 부터, 그리고 P4-3 머지 후 (kebab ask 와이어링) 부터 사용자가 자기 설치본을 직접 검증할 수 있다. 이 문서는 사용자 환경 (~/.config/kebab/, ~/.local/share/kebab/) 을 건드리지 않고 임시 디렉토리에 격리된 KB 를 띄워 전체 파이프라인을 1세션 안에 한 번 돌리는 절차다.

준비

빌드:

cargo build --release -p kebab-cli   # debug 도 무방. 디버그가 더 빠르게 빌드됨.

원격 Ollama (선택, kebab ask 만 필요):

# Mac 등 별도 호스트에서
OLLAMA_HOST=0.0.0.0:11434 ollama serve
ollama pull gemma4:26b           # 또는 qwen2.5:32b 등 — 자세한 비교는 README

본 머신에서 reachability 검증:

curl http://<host>:11434/api/tags

{"models": [...]} 가 나오면 네트워크 + 방화벽 OK.

격리된 워크스페이스 생성

mkdir -p /tmp/kebab-smoke/{workspace,data}
cat > /tmp/kebab-smoke/workspace/intro.md <<'EOF'
---
title: 인사말
tags: [demo]
lang: ko
---
# 안녕

이 문서는 스모크 테스트 fixture 다.
EOF

여러 파일을 시드하고 싶으면 본인 KB 일부를 cp -r 으로 복사해도 좋다 (다음 절차는 6개 markdown 가정).

격리된 config

/tmp/kebab-smoke/config.toml:

schema_version = 1

[workspace]
root = "/tmp/kebab-smoke/workspace"
include = ["**/*.md"]
exclude = [".git/**", "node_modules/**", ".obsidian/**"]

[storage]
data_dir = "/tmp/kebab-smoke/data"
sqlite = "{data_dir}/kebab.sqlite"
vector_dir = "{data_dir}/lancedb"
asset_dir = "{data_dir}/assets"
artifact_dir = "{data_dir}/artifacts"
model_dir = "{data_dir}/models"
runs_dir = "{data_dir}/runs"
copy_threshold_mb = 100

[indexing]
max_parallel_extractors = 2
max_parallel_embeddings = 1
watch_filesystem = false

[chunking]
target_tokens = 500
overlap_tokens = 80
respect_markdown_headings = true
chunker_version = "md-heading-v1"

[models.embedding]
provider = "fastembed"               # "none" 으로 두면 lexical-only — Ollama 불필요
model = "multilingual-e5-small"
version = "v1"
dimensions = 384
batch_size = 64

[models.llm]
provider = "ollama"
model = "gemma4:26b"                 # 사용자 환경에 맞춰 교체
context_tokens = 16384
endpoint = "http://192.168.0.47:11434"
temperature = 0.2
seed = 42

[search]
default_k = 10
hybrid_fusion = "rrf"
rrf_k = 60
snippet_chars = 220

[rag]
prompt_template_version = "rag-v1"
score_gate = 0.05                    # RRF 정규화 후 [0, 1] 범위라 default 그대로 OK
explain_default = false
max_context_tokens = 6000

KEBAB_* 환경변수로 override 가능 (KEBAB_MODELS_LLM_MODEL=qwen2.5:32b kebab … 등). 자세한 키 목록은 crates/kebab-config/src/lib.rsapply_env 매치 암.

명령 시퀀스

KEBAB() { ./target/debug/kebab --config /tmp/kebab-smoke/config.toml "$@"; }

KB doctor                                          # 1. health check
KB ingest                                          # 2. 워크스페이스 색인 (markdown + image)
KB list docs                                       # 3. 색인 결과 목록 (markdown + image 모두 표시)
KB search --mode lexical "코루틴" --k 3            # 4. lexical 검색
KB search --mode vector "memory safety" --k 3      # 5. vector 검색
KB search --mode hybrid "Cargo workspace" --k 3    # 6. hybrid 검색
KB search --mode lexical "Hello World" --k 3       # 7. image OCR 텍스트 검색 (P6-4)
KB inspect chunk <chunk_id>                        # 8. raw chunk 보기
KB ask "이 KB 안에서 ..." --mode hybrid --k 5     # 9. RAG 답변 (Ollama 필요)
KB --json ask "..." --mode hybrid                  # 10. 기계 친화 출력 검증

P6-4 이미지 ingestion 옵션

config.toml 에 다음 절을 추가하면 kebab ingest**/*.png / **/*.jpg 등 이미지 자산도 함께 색인합니다 (텍스트만 색인하려면 생략):

[workspace]
include = ["**/*.md", "**/*.png", "**/*.jpg"]

[image.ocr]
enabled = true                        # vision LM 으로 이미지 안 텍스트 전사
engine = "ollama-vision"
model = "gemma4:e4b"                  # 사용자 환경의 비전 모델
endpoint = "http://192.168.0.47:11434"  # 비우면 models.llm.endpoint fallback
languages = ["eng", "kor"]
max_pixels = 1600                     # long-edge cap

[image.caption]
enabled = true                        # vision LM 으로 한 문장 객관 설명 생성
max_pixels = 768
prompt_template_version = "caption-v1"

이미지 자산 한 장당 OCR 1 호출 + Caption 1 호출 → ~3-6초 (gemma4:e4b 기준). 다이어그램 / 카메라 사진 / 스크린샷 위주 워크스페이스에 권장. 책 / 스캔본은 P7 PDF 라인으로.

P7-3 PDF ingestion

config.toml[workspace] include**/*.pdf 를 추가하면 kebab ingest 가 텍스트 PDF 자산도 색인합니다. 외부 service 의존 없음 — kebab-parse-pdf 가 lopdf 로 페이지 단위 텍스트 추출, kebab-chunk::PdfPageV1Chunker 가 페이지 경계를 절대 넘지 않는 chunk 생성.

[workspace]
include = ["**/*.md", "**/*.pdf"]

PDF 한 권당 페이지 수만큼 (또는 페이지 텍스트가 길면 그 이상의) chunk 가 한 transaction 안에서 commit. 검색 결과의 chunk.source_spans[0]Page { page, char_start, char_end } 형태라 인용 시 페이지 번호가 그대로 사용 가능. kebab ask --jsoncitations[].citationkind: "page" + page: <N> + path: <pdf_path> 로 노출.

테스트 fixture 가 필요할 때는 두 example 바이너리를 사용 — reportlab / qpdf 같은 시스템 dep 없이 in-tree 로 PDF / PNG 생성:

cargo run --release --example gen_smoke_pdf -p kebab-parse-pdf -- \
  /tmp/kebab-smoke/workspace/whitepaper.pdf "page one body" "page two body"

cargo run --release --example gen_smoke_png -p kebab-parse-image -- \
  /tmp/kebab-smoke/workspace/diagram.png
kebab --config /tmp/kebab-smoke/config.toml ingest
kebab --config /tmp/kebab-smoke/config.toml search --mode hybrid "<본문 단어>"
kebab --config /tmp/kebab-smoke/config.toml inspect doc "<pdf_doc_id>"
kebab --config /tmp/kebab-smoke/config.toml ask "<PDF 본문에 관한 질문>" --json

암호화 PDF (예: DRM 책) → errors+=1, error 필드에 qpdf --decrypt 안내. 빈/스캔 페이지 (텍스트 추출 실패) → 0 chunk + Provenance::Warning ("scanned candidate"). v1 에서는 검색 불가, P+ scanned-PDF OCR fallback 까지 대기.

수정된 PDF 를 같은 path 에 다시 배치하면 purge_orphan_at_workspace_path 가 옛 doc / chunks / embeddings 를 sweep 하고 새 byte 가 새 doc_id 로 색인됨 — IngestReport 에 그 자산만 new+=1 로 분류 (다른 자산은 updated). HOTFIXES 2026-05-02 P7-3 참조.

각 명령은 0 종료 코드면 정상. kebab ask 는 거절 시 종료 코드 1 (RefusalSignal) — 의도된 동작.

검증 체크리스트

  • kebab doctor--config path 를 honor 하고 그 안의 storage.data_dir 를 출력 (XDG default 가 아님).
  • kebab ingest idempotent — 두 번째 실행이 new=0 updated=N.
  • kebab list docs 출력에 frontmatter 의 title 이 아닌 deterministic doc_id (32-hex) + workspace_path 가 보임.
  • kebab search --mode hybridfusion_score[0, 1] 범위 (top-1 종종 1.0 — 두 retriever 모두 rank 1 일 때).
  • kebab ask JSON 응답에 model.id 가 config 의 모델 (gemma4:26b 등) 과 일치, embedding.id = multilingual-e5-small, citations[].marker[1] / [2] 형식 (square-bracketed bare index).
  • 코퍼스에 없는 주제로 kebab askrefusal_reason: "llm_self_judge" (또는 no_chunks / score_gate) + grounded: false.
  • (P6-4) image.ocr.enabled = true 로 PNG 자산을 ingest 하면 kebab list docs 가 markdown 옆에 image doc 도 출력 (workspace_path*.png). kebab inspect doc <image_doc_id>block.ocr.joined 가 vision LM 의 OCR 결과 (예: 스크린샷 안의 텍스트). kebab search --mode lexical "<OCR text>" 가 그 image chunk 를 반환하면 wiring 정상.
  • OCR / caption 부분 실패는 errors 카운터 미증가 — kebab inspect doc <id> 의 Provenance Warning 이벤트 또는 --debug 로그에서만 확인.
  • (P7-3) *.pdf 자산을 워크스페이스에 두면 kebab ingest 출력에 PDF 도 new 카운터에 포함. kebab inspect doc <pdf_doc_id>parser_version = "pdf-text-v1" + 페이지마다 Block::Paragraph + SourceSpan::Page { page, char_start, char_end }. 본문에 등장하는 단어로 kebab search --mode hybrid 시 PDF chunk 가 결과에 포함되고 source_span.kind = "page" 면 wiring 정상. 암호화 PDF 는 errors+=1 로 분류되며 error 필드에 qpdf --decrypt 안내 보존. 빈/스캔 페이지 (PDF 가 텍스트를 추출하지 못한 페이지) 는 0 chunk + Provenance::Warning ("scanned candidate") 로 표시 — P+ scanned-PDF OCR fallback 까지는 검색 불가.

정리

rm -rf /tmp/kebab-smoke/data        # 데이터만 날리고 다시 ingest 가능
rm -rf /tmp/kebab-smoke              # 통째로 정리

~/.config/kebab/~/.local/share/kebab/ 는 한 번도 터치되지 않는다 (--config flag 가 정확히 honor 되는 경우 — P3-5 hotfix 이후 보장).

알려진 동작

  • kebab ingest 시 fastembed 모델 다운로드 (~470MB) — data_dir/models/fastembed/ 에 캐시.
  • kebab ask 응답 시간 = LLM 토큰 throughput 에 종속. M4 Pro 48GB + gemma4:26b 기준 답변 50100 토큰에 2055초.
  • --config path 가 존재하지 않거나 malformed 면 kebab doctor 가 hard fail (defaults 가 silently mask 하지 않게 하는 hotfix 동작).
  • 매 CLI invocation 마다 fastembed 모델 init 비용 (~4초) — process-level 캐시 부재 때문. P9 TUI 진입 시 AppOnceLock 으로 세션 동안 한 번만 init.
  • (P6-4) image.ocr.enabled = true + image.caption.enabled = true 인 워크스페이스에 PNG 가 N장 있으면 ingest 시간 ≈ markdown_time + N × (OCR + Caption latency). gemma4:e4b + 192.168.0.47 로 자산당 ~5-10초. 다수의 책 페이지를 이미지로 넣지 말 것 — 책은 P7 PDF 라인 사용 권장.
  • (P7-3) config.chunking.chunker_version 는 markdown 만 represent — PDF 자산은 pdf-page-v1 하드코딩. config.tomlchunker_version = "md-heading-v1" 을 봐도 PDF 는 영향 안 받음. HOTFIXES 2026-05-02 P7-3 entry 참조 (P+ chunker registry task 까지 유지).
  • (P7-3) 한 PDF 가 N 페이지면 kebab ingest 가 N 개 (또는 그 이상의, 페이지 길면 multi-chunk) 의 chunk 를 한 transaction 안에서 commit. 500 페이지 책 → 500+ chunk 한 번에 → embedding throughput 가 bottleneck. 임베딩 활성 워크스페이스에서 큰 PDF 를 처음 ingest 하면 분-단위 시간 + WAL 크기 증가 가능 — P+ 스케일 hardening task 까지 정상 동작이지만 비용은 측정 가능.
  • (P7-3) 동일 path 에 byte 가 다른 PDF 를 두 번째 ingest 하면 purge_orphan_at_workspace_path 가 옛 doc / chunks / embeddings 를 sweep 하고 새 byte 가 새 doc_id 로 색인됨. IngestReport 에 그 자산만 new+=1 (다른 자산은 updated). LanceDB 는 별도 store 라 옛 vector 가 잔존하지만 검색에는 영향 없음 (SQLite join 으로 surface 안 됨) — 디스크 cleanup 은 P+.

자세한 history 와 발견된 버그는 tasks/HOTFIXES.md 참조.