Files
kebab/docs/DOGFOOD.md
altair823 2871da14f4 feat(parse-pdf): #232 스캔 PDF 를 페이지 렌더링으로 OCR
`extract_dctdecode_page_image` 는 페이지의 image XObject 중 `/Filter` 가
정확히 DCTDecode 인 것 하나만 받는다. 실제 스캔본에서 흔한 CCITTFaxDecode·
JBIG2Decode·FlateDecode·JPXDecode, `[FlateDecode, DCTDecode]` 같은 체인,
Internet Archive 계열의 "배경 + /ImageMask" 분리 구조가 전부 걸러진다.

텍스트 게이트는 정상 동작했다. `needs_ocr` 판정을 통과했다는 건 "이 페이지는
스캔본이라 OCR 이 필요하다" 고 올바르게 본 것이다. 판정은 맞았고 래스터를 못
꺼냈을 뿐인데, 결과가 조용한 내용 손실이었다 — 색인은 성공으로 끝나고,
검색이 안 되는 시점에야 알게 되며, 그때 원인이 PDF 인코더라는 걸 역추적할
방법이 없다.

페이지를 렌더링한다 (`page_render::PageRenderer`, pdfium). 지원할 필터도,
고를 XObject 도 없고, 벡터와 이미지가 섞인 페이지도 리더가 보는 대로 나온다.
이슈가 지적한 "image XObject 선택이 비결정적" 문제도 이 경로에서는 성립하지
않는다.

이슈는 교체를 권했지만 렌더러 우선 + DCTDecode 폴백으로 갔다. 배포 형태
때문이다 — pdfium 은 공유 라이브러리로만 배포되고 정적 빌드가 없어서,
링크하면 CLAUDE.md 가 규정한 단일 바이너리가 깨진다. 사용자와 상의해 정했다.

  - 런타임 바인딩. 있으면 전 인코딩 커버, 없으면 오늘 동작 + 왜 건너뛰었는지.
  - `[ingest.pdf.ocr] render_library` 로 경로 지정, 비우면 로더 경로 탐색.
  - `kebab doctor` 의 `pdf_render` 가 어느 쪽인지 보고.
  - 바이너리 392.9 → 399.3 MB (+6.4 MB 글루). ldd 에 pdfium 없음.

조용한 손실을 시끄럽게 (이슈 부수 제안 2·3):

`failure_reason` 이 CLI 에서 `..` 로 버려지고 있었다. wire 이벤트는 원인을
구분해 싣는데 사람이 보는 출력이 "no DCTDecode or engine fail" 로 뭉갰다.
이제 no_renderer / render_error / ocr_error 를 구분해 찍는다.
`IngestReport.ocr_skipped_pages` 를 추가하고(additive) 사람용 요약에도
`ocr-skipped N` 으로 낸다 — stderr 한 줄로 흘리면 대량 ingest 에서 지나간다.

parser_version cascade: pdf-text-v1 → pdf-text-v2. 안 올리면 이미 색인된
스캔본에 적용되지 않는다 (파일이 안 바뀌었으니 해시가 같고 Unchanged 로
건너뛴다). 사용자가 --force-reingest 를 떠올려야만 고쳐지는 수정은 고쳐진 게
아니다. 스냅샷 둘이 따라 움직였고 바뀐 것이 파생 식별자뿐임을 확인했다 —
본문 텍스트·inlines·source_span·metadata 는 동일.

구현 중 발견: pdfium 은 동시 사용이 안전하지 않다. 테스트를 병렬로 돌리자
`double free or corruption` 으로 프로세스가 죽었고, `thread_safe` 기능만으로는
부족했다. ingest 는 PDF 를 하나씩 처리하니 오늘은 문제가 없지만 `Arc` 는
공유해도 된다고 광고하는 타입이라, `PageRenderer` 안에 뮤텍스를 두고
`RenderedPdf` 가 문서 수명 동안 잡게 했다 (필드 선언 순서가 load-bearing —
doc 이 guard 보다 먼저 드롭돼야 한다). 지금 비용 0, 병렬화되는 날 메모리
손상 대신 대기가 된다. `set_target_width` 만 주면 긴 스캔에서 pdfium 이 C++
length_error 로 프로세스를 죽여서(exceptions 비활성 빌드라 Err 로 못 받는다)
양변을 set_maximum_* 으로 묶었다. 바인딩도 run 당 1회여야 한다.

실측 (govdocs1-000157-ccitt.pdf, 22쪽 중 1쪽이 CCITT 스캔, gemma3:4b):

                  렌더러 없음                        렌더러 있음
  OCR        ⊘ 건너뜀 — 인코딩을 읽을 수 없다    ✓ 101 chars, 6489ms
  chunk                35                              36
  글자 수            35,994                          36,095
  요약           ocr-skipped 1                        (없음)

렌더링 자체는 여섯 필터 계열 전부 확인 — CCITT / JBIG2 / Flate / JPX /
혼합(DCT+CCITT+JBIG2+Flate) / DCT, 300dpi 페이지당 40~145 ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-17 01:53:46 +09:00

994 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DOGFOOD — kebab 도그푸딩 시나리오
> 본 file 은 kebab 의 모든 기능에 대한 dogfood 시나리오 의 single reference. 새 dogfood request 시 본 file 의 시나리오 list 를 참조. 새 sub-item 또는 새 release 마다 §관련 section 의 시나리오 갱신.
>
> 사용법: 도그푸딩 요청 시 본 doc 의 시나리오 카탈로그 → 적절한 시나리오 list 선정 → 환경 setup (§0) → 시나리오 실행 → bug log → 발견된 bug 즉시 fix cycle.
## 목차
- [§0 환경 + 사전 준비](#0-환경--사전-준비)
- [§1 Ingest](#1-ingest)
- [§2 Search](#2-search)
- [§3 Ask (RAG)](#3-ask-rag)
- [§4 Inspect / Fetch / List](#4-inspect--fetch--list)
- [§5 Version cascade](#5-version-cascade)
- [§6 Wire schema (`--json`)](#6-wire-schema---json)
- [§7 TUI (P9)](#7-tui-p9)
- [§8 MCP (P9-fb-30)](#8-mcp-p9-fb-30)
- [§9 Doctor / Schema / Reset](#9-doctor--schema--reset)
- [§10 Eval (P5)](#10-eval-p5)
- [§11 Edge cases](#11-edge-cases)
- [§12 Bug discovery checklist](#12-bug-discovery-checklist)
- [§13 Reference dogfood corpus](#13-reference-dogfood-corpus)
---
## §0 환경 + 사전 준비
### 0.1 release binary
```bash
export CARGO_TARGET_DIR=/build/out/cargo-target/target
export RELEASE_BIN="${CARGO_TARGET_DIR:-target}/release/kebab"
cargo build --release -p kebab-cli -j 4 2>&1 | tail -5
"$RELEASE_BIN" --version # 기대: kebab X.Y.Z (current workspace version)
```
### 0.2 Ollama endpoint check
```bash
# Remote (사용자 dogfood host)
curl -s --connect-timeout 3 http://192.168.0.47:11434/api/tags | jq -r '.models[]?.name'
# 기대: qwen2.5vl:3b / gemma4:e4b / gemma4:26b / bge-m3:latest / nomic-embed-text:latest
# Local fallback
curl -s --connect-timeout 3 http://localhost:11434/api/tags 2>&1 | head -3
```
### 0.3 isolated KB workspace
```bash
DOGFOOD=/build/cache/tmp/<sub-item>-dogfood
mkdir -p "$DOGFOOD/kb" "$DOGFOOD/data"
# Default config from `kebab init` then customize
HOME=/tmp/dogfood-home XDG_CONFIG_HOME="$DOGFOOD/xdg" "$RELEASE_BIN" init
cp "$DOGFOOD/xdg/kebab/config.toml" "$DOGFOOD/config.toml"
# 사용자 customize: workspace.root, storage.data_dir, models.{embedding,llm}.endpoint, 기능 enable
```
### 0.4 disk + cargo clean policy
memory `feedback-cargo-clean-policy`:
- `/build` avail < 500GB OR target > 200GB 시만 cargo clean.
- 평소 incremental build.
### 0.5 dogfood corpus location
본 doc §13 reference. 표준 corpus = PoC 9 PDF + markdown notes + code samples + image set.
---
## §1 Ingest
### §1.1 Markdown ingest (P1)
**기본**:
```bash
# Test markdown KB
mkdir -p "$DOGFOOD/kb/notes"
cat > "$DOGFOOD/kb/notes/sample.md" <<'EOF'
# Title
본문 paragraph. **bold** + *italic*.
## Heading 2
- list item 1
- list item 2
```rust
fn main() { println!("code block"); }
```
EOF
"$RELEASE_BIN" ingest --config "$DOGFOOD/config.toml" --json | tail -1 | jq '.items[] | {doc_path, kind, block_count, chunk_count}'
```
**verify**:
- `kind` = `"new"` (first ingest) / `"unchanged"` (no change) / `"updated"` (modified).
- `block_count` ≥ heading/paragraph count.
- `chunk_count` ≥ 1.
**scenarios**:
- 1.1.a empty markdown (0 byte) → expected warning + 0 chunks.
- 1.1.b deeply nested heading (h1-h6) → block 모두 capture.
- 1.1.c frontmatter (YAML/TOML) → metadata preserve.
- 1.1.d code fence (Rust/Python/etc.) → code block 으로 chunk.
- 1.1.e mixed inline (link/image/inline-code/strong/em) → inline preserve.
- 1.1.f huge markdown (1 MB+) → walker pass + chunk count delta.
### §1.2 Image ingest (P6)
**Config**:
```toml
[image.ocr]
enabled = true
engine = "ollama-vision"
model = "gemma4:e4b"
endpoint = "http://192.168.0.47:11434"
[image.caption]
enabled = false # opt-in
```
**verify**:
- `*.png` / `*.jpg` / `*.jpeg` 만 ingest target.
- OCR text 가 `Block::ImageRef.ocr.joined` 안.
- `[image.caption].enabled=true` 시 caption 도.
**scenarios**:
- 1.2.a Korean OCR (한국어 scan PNG) → OCR text + search hit.
- 1.2.b English OCR (typed text screenshot) → alnum > 90%.
- 1.2.c photo (자연 사진, OCR 없음) → empty OCR or warning.
- 1.2.d corrupt image → graceful error.
- 1.2.e oversized image (> max_pixels) → downscale.
### §1.3 PDF text ingest (P7-1)
**기본**:
- vector PDF (embedded text) — `PdfTextExtractor` 가 page 별 text 추출.
- 1 `Block::Paragraph` per page (P7-1 invariant).
**verify**:
- `parser_version = "pdf-text-v2"`.
- `chunker_version = "pdf-page-v1"` (또는 `"pdf-page-v1.1"` from v0.20.1).
- `block_count` ≥ page count.
**scenarios**:
- 1.3.a single page vector PDF → 1 block.
- 1.3.b multi-page (10+ pages) → block per page.
- 1.3.c large PDF (50+ pages, 50 MB+) → ingest 시간 + memory monitor.
- 1.3.d encrypted PDF → friendly error wording (`qpdf --decrypt` hint).
- 1.3.e corrupt PDF (truncated bytes) → graceful error.
- 1.3.f PDF with annotations / forms → extract 동작.
### §1.4 PDF scanned OCR ingest (v0.20.0 sub-item 1)
**Config**:
```toml
[pdf.ocr]
enabled = true
always_on = false
model = "qwen2.5vl:3b"
endpoint = "http://192.168.0.47:11434"
valid_ratio_threshold = 0.5
min_char_count = 20
```
**Config (v0.33.0~)**: 위 블록은 `[ingest.pdf.ocr]` 로 옮겨졌고, 스캔본을 제대로 읽으려면 `render_library` 가 필요하다.
```toml
render_library = "/path/to/libpdfium.so"
render_dpi = 300
```
**verify**:
- IngestEvent::PdfOcrStarted / PdfOcrFinished emit.
- `IngestItem.pdf_ocr_pages > 0` for scanned PDF.
- `IngestItem.pdf_ocr_ms_total > 0`.
- CLI printer: `📷 OCR page N...` / `✓ OCR page N (chars chars, msms via ollama-vision)`.
- **렌더러 유무로 갈리는 지점** (issue #232): `render_library` 없이 CCITTFax·JBIG2·Flate·JPX 스캔을 넣으면 `⊘ OCR page N 건너뜀 — 이 페이지의 인코딩은 페이지 렌더러 없이 읽을 수 없다` 가 찍히고, 요약에 `ocr-skipped N`, `--json``ingest_report.ocr_skipped_pages``pdf_ocr_finished.failure_reason = "no_renderer"` 로 나온다. 렌더러를 주면 같은 파일이 실제 텍스트를 낸다.
- `kebab doctor``pdf_render` 가 바인딩된 라이브러리 경로를 보고하는가.
**scenarios** (이전 dogfood report 의 9 시나리오):
- 1.4.a scanned 한국어 (F1 / F2) → OCR text indexed + search hit.
- 1.4.b multi-scanned PDF (5+ files) → chunk_id collision 0 (Bug #3 fix verify).
- 1.4.c always_on=true → vector PDF page 도 dual-block OCR.
- 1.4.d **필터별 커버리지** (issue #232): `corpus/pdf/``scanned-ccitt` / `korean-scan`(JBIG2·DCT) / `scanned-flate` / `scanned-jpx` / `scanned-mixed` 를 렌더러 있음/없음 두 조건으로 색인해 `ocr-skipped` 와 색인된 글자 수를 비교.
- 1.4.d valid_ratio_threshold variation (0.3 / 0.5 / 0.8) → mojibake / 정상 page 분류.
- 1.4.e min_char_count variation (5 / 20 / 100) → 짧은 page OCR 호출.
- 1.4.f DCTDecode-only skip (F6 FlateDecode / F7 CCITTFax) → warning + skip.
- 1.4.g force-reingest (`--force-reingest`) → OCR 재실행.
- 1.4.h cancel handle (Ctrl+C 또는 SIGINT) → graceful abort.
- 1.4.i v0.19 → v0.20 upgrade UX (parser_version 보존 + manual force-reingest 필요).
### §1.5 Code ingest (P10)
**Tier 1 (AST chunkers)** — Rust / Python / TS / JS / Go / Java / Kotlin / C / C++
**Config**:
```toml
[ingest.code]
max_file_bytes = 262144
max_file_lines = 5000
ast_chunk_max_lines = 200
```
**verify per lang**:
- `chunker_version` = `code-{lang}-ast-v1`.
- symbol path correct (file-scope nesting / module-path / class-method nesting).
- `extra_skip_globs` 동작.
**scenarios**:
- 1.5.a Rust crate (workspace + multiple modules) → mod / fn / impl chunks.
- 1.5.b Python package (src/ layout, dotted module path).
- 1.5.c TypeScript/JavaScript (decorators, generators, classes).
- 1.5.d Go (package + struct methods).
- 1.5.e Java/Kotlin (class + inner class + method).
- 1.5.f C (typedef alias unit, header).
- 1.5.g C++ (namespace::Class::method recursive).
- 1.5.h `.h` 파일 (C vs C++ syntax) → tree-sitter-c parse 실패 시 Tier 3 fallback.
**Tier 2 (resource-aware)** — yaml/k8s, dockerfile, manifest (toml/json/xml/groovy)
**verify**:
- k8s YAML: `apiVersion+kind` per resource.
- Dockerfile: whole-file `dockerfile-file-v1`.
- Cargo.toml / package.json / pom.xml: whole-file `manifest-file-v1`.
**scenarios**:
- 1.5.i k8s manifest (multi-resource via `---`).
- 1.5.j Dockerfile (multi-stage build).
- 1.5.k Cargo.toml workspace (members + dependencies).
- 1.5.l invalid YAML → Tier 3 fallback.
**Tier 3 (paragraph fallback)** — shell, non-k8s YAML, AST failure
**verify**:
- `chunker_version = "code-text-paragraph-v1"`.
- Blank-line paragraph segmentation + 80-line / 20-overlap window for oversize.
**scenarios**:
- 1.5.m `.sh` / `.bash` / `.zsh` → paragraph chunks.
- 1.5.n empty file → 0 chunks + warning.
- 1.5.o very long shell script (1000+ lines) → line-window split.
### §1.6 Single-file / stdin ingest (p9-fb-31)
```bash
# Workspace 외부 file
"$RELEASE_BIN" ingest-file ~/Documents/external.md --config "$DOGFOOD/config.toml"
# 기대: _external/<hash>.md 에 copy + ingest.
# stdin
echo "# stdin content" | "$RELEASE_BIN" ingest-stdin --title "from stdin" --config "$DOGFOOD/config.toml"
```
**scenarios**:
- 1.6.a external markdown ingest.
- 1.6.b stdin with `--source-uri` flag.
- 1.6.c .kebabignore matched file → warn + 진행.
- 1.6.d binary file → reject (markdown only).
### §1.7 Incremental ingest
**verify**:
- 첫 ingest 후 다시 ingest → 모두 `unchanged`.
- 일부 file 수정 후 → 해당 file 만 `updated`.
- 새 file 추가 → `new`.
- 삭제된 file → `purged_deleted_files` count.
**scenarios**:
- 1.7.a unchanged path (parser_version + chunker_version + embedding_version match).
- 1.7.b stale file purge.
- 1.7.c `--force-reingest` (force-update path).
### §1.8 Ingest progress (wire `ingest_progress.v1`)
**`--json` mode 의 ndjson stream**:
- `scan_started``scan_completed``[sweep_started → sweep_progress* → sweep_completed]``(asset_started → [pdf_ocr_*]* → asset_finished)+``completed` | `aborted`.
- `sweep_*` (v0.32.1, issue #228) 는 스토어에 이번 스캔이 덮지 않은 경로가 있을 때만 나온다. 첫 ingest 에는 없다.
**verify**:
- ordering invariant (design §2.4a).
- per-asset `idx/total/path/media/result/chunks`.
- aggregate `counts` on `completed` / `aborted`.
- sweep 구간: `sweep_started.total` 이 후보 수와 맞고, `sweep_progress.idx` 가 1..=total 로 연속이며, 취소 없이 완주하면 `sweep_completed.checked == total`.
- sweep 이 끝난 뒤 진행바가 asset 분모·라벨로 돌아오는가 (TTY). sweep 이 같은 바를 빌려 쓰므로 복구가 빠지면 색인 구간 내내 `sweep [..] 4213/21` 로 그려진다.
- ndjson 로그에 `purge` 줄과 `sweep_summary` 가 남는가 (이슈 #228 이전에는 이 구간이 0바이트였다). purge 실패 시 `purge_failed`.
- sweep 중 Ctrl-C 한 번에 실제로 멈추는가. `sweep_completed.checked` 가 예고한 `total` 이 아니라 실제 검사한 수로 나와야 한다.
- `asset_timings``cache_hit` / `cache_miss` / `cache_ms` (v0.32.1, issue #231). `--force-reingest` 로 warm 재색인하면 `cache_miss == 0` 이어야 한다(그냥 재색인하면 변경 없는 문서가 통째로 skip 되어 `asset_timings` 자체가 안 나온다). `cache_ms`**`embed_ms` 에 포함된** 값이라 phase 를 합산할 때 이중 계상하지 말 것. 임베딩 kind 만 세므로 이미지·PDF 위주 코퍼스에서는 캐시가 한 일을 과소 표현한다. code 자산은 `asset_timings` 자체를 emit 하지 않는다.
---
## §2 Search
### §2.1 Lexical search (P2)
```bash
"$RELEASE_BIN" search --config "$DOGFOOD/config.toml" "한국어" --mode lexical --k 10
```
**verify**:
- FTS5 `unicode61` + lindera ko-dic 형태소 분해 column (v0.20.1, V009 migration).
- `chunks_fts` schema (`text`, `heading_path` 별 column) — V009 의 chunks_ai/au trigger 가 `tokenized_korean_text` 를 CASE expression 으로 raw text 앞에 prepend.
- 한국어 2-char query (`한국`, `서울`) 가 chunk 의 ko-dic 분해된 morpheme 또는 explicit 공백 분리된 token 과 일치 시 hit.
- 영어는 V002 의 whole-token 매칭으로 회귀 (`token` query 는 `token` 토큰만 hit, `tokenizer` substring 은 hit X). substring recall 이 필요하면 vector/hybrid mode 권장.
**scenarios**:
- 2.1.a Korean 2-char query (`한국`, `서울` → ≥ 1 hit on Korean wiki fixture).
- 2.1.b Korean compound noun (`한국어`, `서울특별시` → ko-dic 의 형태소 분해 + 단일 noun 동시 매칭).
- 2.1.c English/Korean mixed (`Rust 최적화` → token-AND 두 토큰 모두 hit).
- 2.1.d 1-char query → 0 hit (MIN_QUERY_CHARS = 2 filter, `build_match_string` v0.20.1 갱신).
- 2.1.e English whole-token (`tokenizer` hit, `token``tokenizer` 의 substring 매칭 X — V007 trigram 회귀).
- 2.1.f raw mode escape (`heading_path : <token>`).
- 2.1.g FTS5 phrase query (`"specific phrase"`).
- 2.1.h exclusion (`-token`).
### §2.1bis V009 morphological tokenizer dogfood evidence (v0.20.1)
**Reference corpus** (이 fixture 로 ingest 시 모든 scenario 보장 hit):
```bash
mkdir -p $DOGFOOD/corpus
cat > $DOGFOOD/corpus/korea-overview.md <<'EOF'
# 한국 개요
한국 은 동아시아 의 반도 국가다. 한국 어 는 한반도 의 주요 언어다.
서울 은 한국 의 수도다. 서울 의 지하철 은 1974년 1호선 개통 후
지금까지 23개 노선으로 확장되었다.
## 한국 문화
한국 의 문화 는 오래 된 역사 와 깊은 전통 을 가진다.
EOF
cat > $DOGFOOD/corpus/korea-compound.md <<'EOF'
# 한국어 와 한국문화
한국어 학습 자료. 한국문화 의 핵심 은 정 (情) 이다.
서울특별시 와 부산광역시 는 한국 의 양대 도시다.
EOF
```
**검증 명령** (모두 hit ≥ 1):
```bash
KB="$RELEASE_BIN --config $DOGFOOD/config.toml"
# 한국어 2-char (Bug #8 close, v0.20.1 의 핵심 가치)
$KB search '한국' --mode lexical --json | jq '.hits | length' # ≥ 1
$KB search '서울' --mode lexical --json | jq '.hits | length' # ≥ 1
# 한국어 3-char + compound noun
$KB search '지하철' --mode lexical --json | jq '.hits | length' # ≥ 1
$KB search '한국어' --mode lexical --json | jq '.hits | length' # ≥ 1
$KB search '한국문화' --mode lexical --json | jq '.hits | length' # ≥ 1
$KB search '서울특별시' --mode lexical --json | jq '.hits | length' # ≥ 1
# 영어 whole-token (V002 동작 회귀)
$KB search 'token' --mode lexical --json | jq '.hits | length' # 0 또는 별 token 으로 존재 시 hit
$KB search 'tokenizer' --mode lexical --json | jq '.hits | length' # ≥ 1 if corpus has 'tokenizer' word
```
**예상 snippet (lindera 분해 evidence)**:
- `'한국'` query → "한국 은 동아시아 의 반 도 국가 다" — ko-dic 의 명사 boundary + 조사 분리 확인.
- `'서울'` query → "서울특별시 와" — ko-dic 의 compound `서울특별시``[서울, 특별시]` 분해.
**Known limitation (spec critic R1 #3 acceptance, Option α)**:
- ko-dic 이 compound noun 을 단일 token 으로 저장하는 경우 (예: `한국정부` 가 한 token) → `'한국'` query 는 그 chunk 에 hit X.
- KB 가 영어/code 위주 (예: 사용자 KnowledgeBase 가 React docs) 면 한국어 token 자체 부재로 0 hit 정상.
- N-gram supplement (Option β) 는 v0.21.x P9 follow-up.
### §2.2 Vector search (P3)
```bash
"$RELEASE_BIN" search --config "$DOGFOOD/config.toml" "어떤 문장의 의미적 검색" --mode vector --k 10
```
**verify**:
- `multilingual-e5-small` (384d) or `bge-m3` embedding.
- LanceDB 의 model 별 separate table.
- similarity score normalized.
**scenarios**:
- 2.2.a Korean semantic query.
- 2.2.b English semantic query.
- 2.2.c domain-specific (code semantic).
- 2.2.d cross-lingual (한영 mixed query).
### §2.3 Hybrid search (RRF, P3)
```bash
"$RELEASE_BIN" search --config "$DOGFOOD/config.toml" "query" --mode hybrid --k 10
```
**verify**:
- `fusion_score = [0, 1]` (normalize).
- lexical + vector → RRF fusion.
**scenarios**:
- 2.3.a hybrid 의 lexical-only / vector-only 가 못 잡는 case (RRF win).
- 2.3.b fusion_score ordering.
### §2.4 Search filters (p9-fb-36)
```bash
"$RELEASE_BIN" search --config "$DOGFOOD/config.toml" "query" --tag rust --tag api --lang en --path-glob 'src/**'
```
**verify**:
- `--tag` repeatable (OR within).
- `--lang` ISO code.
- `--path-glob` workspace_path glob.
### §2.4bis Source / provenance filters (`--source` / `--source-type`, v0.29.0)
```bash
# 출처 id 필터 ([[workspace.sources]] 의 id; 단일 root 는 "default").
"$RELEASE_BIN" search --config "$DOGFOOD/config.toml" "query" --source jira --json | jq '.hits | length'
# source_type 필터 (markdown/note/paper/reference/inbox).
"$RELEASE_BIN" search --config "$DOGFOOD/config.toml" "query" --source-type reference,markdown --json
```
**verify**:
- `--source` / `--source-type` repeatable + comma-sep, OR within.
- lexical · vector · hybrid 모든 모드에 동일 적용 (직접 인덱스 컬럼 `documents.source_id` / `source_type`).
- 모르는 값 → silently empty (no error).
- 멀티소스 KB 측정: `--source wiki` 가 개념 질의 오염 회복(MRR 0.780→0.810), `--source jira` 가 incident 0.918→0.975 (HOTFIXES 2026-06-21).
- trust precedence: `[[workspace.sources]]` 의 per-source `trust_level` 가 frontmatter 부재 시 적용 → `--trust-min primary` 와 조합 시 secondary source 배제.
### §2.5 Search pagination (p9-fb-34)
```bash
"$RELEASE_BIN" search "query" --max-tokens 1000 --snippet-chars 200 --json | jq '.next_cursor'
"$RELEASE_BIN" search "query" --cursor "$(...)" --json
```
**verify**:
- `next_cursor` opaque token.
- `corpus_revision` mismatch → `stale_cursor` error.
### §2.6 Bulk search
stdin ndjson — 줄당 하나의 query object (`{"query":"<text>"}` 필수, 나머지 optional):
```bash
printf '%s\n' \
'{"query":"한국","mode":"lexical","k":3}' \
'{"query":"tokenizer","mode":"hybrid"}' \
'{"query":"lindera","mode":"vector","k":5}' \
| "$RELEASE_BIN" search --bulk --json
```
기대: 줄당 `bulk_search_item.v1` (query echo + response 또는 error). `query` 누락 시 그 item 만 `error.v1` (code `invalid_input`, message 에 shape hint), 나머지 query 계속 진행. Cap 100.
---
## §3 Ask (RAG)
### §3.1 Basic ask (P4)
```bash
"$RELEASE_BIN" ask --config "$DOGFOOD/config.toml" "어떤 질문" --json
```
**verify**:
- `grounded` field (boolean).
- `citations` list (chunk references).
- `answer` text.
**scenarios**:
- 3.1.a in-corpus question (grounded=true).
- 3.1.b out-of-corpus question (grounded=false + refusal).
- 3.1.c hallucination check (paraphrase test, fb-41).
### §3.6 응답 언어 자동 매칭 (v0.20.2 Todo #1)
```bash
"$RELEASE_BIN" ask --config "$DOGFOOD/config.toml" "What is the tokenizer?" --hide-citations # 영어 응답 기대
"$RELEASE_BIN" ask --config "$DOGFOOD/config.toml" "토크나이저가 뭐야?" --hide-citations # 한국어 응답 기대
```
기대: query 언어 = response 언어 (`prompt_template_version = "rag-v4"` default). 큰따옴표 직접 인용은 원문 언어 보존. citation `[#번호]` 유지. 한국어 corpus 를 영어로 물으면 LLM 이 근거를 영어로 번역해 답함 (trade-off). `rag-v3` 로 pin 하면 legacy (provenance 라벨 discount 없음) 동작.
### §3.2 Streaming ask (v0.17.1)
```bash
"$RELEASE_BIN" ask "query" --stream
```
**verify**:
- per-event `answer.v1` (delta tokens).
- final event with `verification` + `citations`.
### §3.3 Multi-hop RAG (fb-41 / v0.18.0)
```bash
"$RELEASE_BIN" ask "complex question" --multi-hop --json
```
**verify**:
- decompose → decide → synthesize loop.
- `multi_hop_max_depth` / `multi_hop_max_sub_queries_per_iter` 따름.
### §3.4 NLI verification (fb-41 / v0.18.0)
```toml
[models.nli]
model = "Xenova/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7"
[rag]
nli_threshold = 0.5
```
**verify**:
- `verification.entailment_score`.
- `refusal_reason = "nli_verification_failed"` (low score).
**scenarios**:
- 3.4.a known hallucination case (S7 caffeine) → reject with nli_score < threshold.
- 3.4.b legitimate grounded answer → entailment > 0.5.
- 3.4.c NLI model unavailable → `refusal_reason = "nli_model_unavailable"`.
### §3.5 Ask filters
```bash
"$RELEASE_BIN" ask "query" --tag rust --path-glob 'src/**' --lang en
```
---
## §4 Inspect / Fetch / List
### §4.1 Inspect
```bash
"$RELEASE_BIN" inspect document <doc_id> --json
"$RELEASE_BIN" inspect chunk <chunk_id> --json
```
**verify**:
- `chunk_inspection.v1` schema.
- `canonical_document.parser_version` / `chunker_version`.
### §4.2 Fetch (p9-fb-35)
verbatim chunk / doc / span fetch:
```bash
"$RELEASE_BIN" fetch <chunk_id> --json
"$RELEASE_BIN" fetch <doc_id> --span 12-34 --json
```
**verify**:
- `fetch_result.v1` schema.
- verbatim text (no chunk wrapping).
### §4.3 List
```bash
"$RELEASE_BIN" list documents --json
"$RELEASE_BIN" list chunks --json
```
---
## §5 Version cascade
design §9 cascade rule:
- `parser_version` 변경 → 해당 parser 의 모든 chunk 무효.
- `chunker_version` 변경 → 해당 chunker 의 모든 chunk 무효.
- `embedding_version` 변경 → 모든 embedding 무효.
**verify scenarios**:
- 5.a parser_version bump (예: `pdf-text-v1``pdf-text-v2`) → 자동 invalidation + 다음 ingest 가 재처리.
- 5.b chunker_version bump (예: v0.20.1 의 `pdf-page-v1``pdf-page-v1.1`) → chunk_id 재계산.
- 5.c embedding_version bump (예: `multilingual-e5-small/v1``/v2`) → LanceDB 의 별 table.
- 5.d 동일 asset 의 doc_id 다른 case → `purge_workspace_path_for_parser_bump` cascade.
- 5.e force-reingest 의 user-facing UX.
---
## §6 Wire schema (`--json`)
### §6.1 schemas list
`docs/wire-schema/v1/`:
- `ingest_progress.v1`, `ingest_report.v1`
- `search_hit.v1`, `search_response.v1`, `bulk_search_item.v1`, `bulk_search_response.v1`
- `answer.v1`, `answer_event.v1`
- `chunk_inspection.v1`, `citation.v1`, `doc_summary.v1`
- `doctor.v1`, `schema.v1`, `fetch_result.v1`, `reset_report.v1`
- `error.v1`
### §6.2 verify per schema
각 schema:
```bash
$RELEASE_BIN <subcommand> --json | jq '.schema_version'
# 기대: "<schema>.v1"
```
JSON schema validity:
```bash
jq -e 'has("schema_version")' <output>
ajv-cli validate -s docs/wire-schema/v1/<schema>.schema.json -d <output>
```
### §6.3 wire backward-compat
**verify**:
- additive minor (new field / new enum value) → older consumer 가 graceful.
- breaking change (field removal / type change) → `v2` major bump.
### §6.4 schema_version cascade
`schema.v1` (`kebab schema --json`) output 의 `wire_schemas` field:
- 16+ entry 의 `{name, version, capabilities}`.
---
## §7 TUI (P9)
### §7.1 Launch TUI
```bash
"$RELEASE_BIN" tui --config "$DOGFOOD/config.toml"
```
### §7.2 4 panel verify
- **P9-1 Library**: workspace document tree + recent assets.
- **P9-2 Search**: lexical / vector / hybrid search panel.
- **P9-3 Ask**: question + answer pane + citations.
- **P9-4 Inspect**: chunk / document detail.
### §7.3 keyboard shortcuts
- Tab / Shift+Tab — switch panel.
- Esc — cancel ongoing op.
- q — quit.
- 기타 panel-specific.
### §7.4 scenarios
- 7.4.a library tree navigation.
- 7.4.b search query + result selection → fetch.
- 7.4.c ask question + answer + citation click → inspect.
- 7.4.d cancel mid-ingest (Esc).
- 7.4.e quit + restart → state preserve.
---
## §8 MCP (P9-fb-30)
### §8.1 Launch MCP stdio server
```bash
"$RELEASE_BIN" mcp
# (agent host 가 stdio 로 호출)
```
### §8.2 6 MCP tools
- `search`, `ask`, `schema`, `doctor`, `ingest_file`, `ingest_stdin`.
### §8.3 verify per tool
- input schema (JSON Schema).
- output schema (wire `*.v1`).
- error path (graceful, exit code).
---
## §9 Doctor / Schema / Reset
### §9.1 Doctor (health check)
```bash
"$RELEASE_BIN" doctor --json
```
**verify**:
- `doctor.v1` schema.
- workspace / storage / models / ollama_reachability.
### §9.2 Schema (introspection report)
```bash
"$RELEASE_BIN" schema --json
```
**verify**:
- `schema.v1` schema.
- wire_schemas / capabilities / model_versions / stats.
### §9.3 Reset
```bash
"$RELEASE_BIN" reset --yes
"$RELEASE_BIN" reset --data-only --yes
"$RELEASE_BIN" reset --vector-only --yes
```
**verify**:
- XDG data dirs wipe (irreversible — TTY confirm 또는 `--yes`).
- macOS path collision 회피 (HOTFIXES 2026-05-07).
- `--data-only` 가 config 보호.
---
### config migrate (스키마 마이그레이션, v0.21.1)
```bash
# 옛 스키마 흉내(섹션 누락 + deprecated) 후 migrate.
printf 'schema_version = 1\n\n[workspace]\nroot = "~/MyNotes"\ninclude = ["*.md"]\n\n[search]\ndefault_k = 25\n' \
> "$DOGFOOD/old.toml"
"$RELEASE_BIN" --config "$DOGFOOD/old.toml" config migrate --dry-run # 미리보기, 파일 미수정
"$RELEASE_BIN" --config "$DOGFOOD/old.toml" config migrate # .bak + 빠진 섹션 주석과 함께 추가
"$RELEASE_BIN" --config "$DOGFOOD/old.toml" config migrate # 멱등
"$RELEASE_BIN" --config "$DOGFOOD/old.toml" doctor | grep config_migration # ok 확인
```
기대: dry-run 파일 미수정 → apply 시 `old.toml.bak`(원본 byte-identical) + `[ingest.code]`·`[logging]`·`[pdf.ocr]` 가시화 + 손본 `default_k`/주석 보존 + `workspace.include` 제거 → 재실행 멱등 → doctor `config_migration` ok. v0.21.1 evidence 는 `tasks/HOTFIXES.md` 2026-05-31.
## §10 Eval (P5)
### §10.1 Basic eval run
```bash
KEBAB_EVAL_GOLDEN=/build/dogfood/golden_queries.yaml \
"$RELEASE_BIN" --config "$DOGFOOD/config.toml" eval run --mode hybrid --k 10
```
**verify**:
- golden query suite 의 metrics (MRR / Recall / NDCG).
- regression detection (snapshot 비교).
- `eval aggregate <run_id> --json` 로 metric object 확인.
### §10.2 검색 품질 baseline (v0.20.2 golden suite, spec §4.6)
v0.20.2 dogfood 에서 확립한 baseline. eval `--config` facade 패치로 dogfood KB 를 직접 평가할 수 있게 됨.
**실행 절차**:
```bash
# 1. eval run (hybrid + lexical 각각)
KEBAB_EVAL_GOLDEN=/build/dogfood/golden_queries.yaml \
"$RELEASE_BIN" --config /build/dogfood/config.toml eval run --mode hybrid --k 10 --json \
| tee /build/dogfood/logs/eval-hybrid-$(date +%Y%m%d).json
KEBAB_EVAL_GOLDEN=/build/dogfood/golden_queries.yaml \
"$RELEASE_BIN" --config /build/dogfood/config.toml eval run --mode lexical --k 10 --json \
| tee /build/dogfood/logs/eval-lexical-$(date +%Y%m%d).json
# 2. aggregate
RUN_ID=$(jq -r '.run_id' /build/dogfood/logs/eval-hybrid-$(date +%Y%m%d).json | head -1)
"$RELEASE_BIN" --config /build/dogfood/config.toml eval aggregate "$RUN_ID" --json
```
**v0.20.2 metric baseline** (`/build/dogfood/golden_queries.yaml` 10 query):
| Mode | hit@1 | hit@3 | hit@10 | MRR | recall@10 | empty |
|------|-------|-------|--------|-----|-----------|-------|
| hybrid | 0.7 | **1.0** | 1.0 | **0.833** | 1.0 | 0 |
| lexical | 0.4 | 1.0 | 1.0 | 0.7 | 1.0 | 0 |
**정성 체크리스트**:
- [ ] 한국어 2자 정답 (`'한국'` / `'서울'` 등) 이 hit@3 이내에 등장.
- [ ] `empty_result_rate = 0` — 10개 query 전부 ≥ 1 hit.
- [ ] hybrid MRR ≥ 0.8 (baseline 0.833).
- [ ] lexical MRR ≥ 0.65 (baseline 0.7).
- [ ] `eval compare <run_a> <run_b>` 의 delta MRR 이 ±0.1 이내면 ranking 건강.
**큐레이션 절차 (spec §4.6)**:
- golden answer 는 "note 의 intent 와 가장 가까운 chunk" 가 아닌 "합리적으로 관련된 모든 doc" 포함 권장.
- 코드와 note 가 동시에 정답일 수 있음 — eval 분해 후 vector hit 를 직접 확인해 golden 보완.
- 초기 라벨링 후 `eval aggregate --json` 의 per-query breakdown 으로 false-negative 정정.
- 정정 시 `hit@3` 등 상위 metric 이 0.9→1.0 수준으로 개선되면 curated golden 으로 확정.
**인사이트**:
- hybrid 가 vector 덕분에 top-1 정확도 우위 (0.7 vs lexical 0.4). hit@3 이후는 두 모드 모두 완벽.
- lexical (V009 형태소) 이 짧은 한국어 토큰을 top-3 에 정확히 배치.
- ranking 조정 없이 현재 hybrid RRF 가 baseline 달성 (`[[project_ranking_deferred]]` 결정 유효).
Cross-link: `tasks/HOTFIXES.md` (2026-05-29 — 검색 품질 baseline entry), `/build/dogfood/golden_queries.yaml`, `/build/dogfood/logs/`.
---
## §11 Edge cases
### §11.1 Encrypted PDF
- input: thermal-pos-printer.pdf / thermal-label.pdf (사용자 dogfood corpus).
- expected: `kind: "Error"` + friendly wording (`qpdf --decrypt` hint).
### §11.2 Corrupt file
- input: truncated bytes / invalid magic.
- expected: `kind: "Error"` + graceful error.
### §11.3 Empty file
- input: 0-byte file.
- expected: 0 chunks + warning.
### §11.4 Very large file
- markdown 100 MB+, PDF 100 MB+.
- expected: walker pass (per file type limit), parser graceful.
### §11.5 env variable overrides
```bash
KEBAB_PDF_OCR_ENABLED=true \
KEBAB_OCR_MODEL=qwen2.5vl:7b \
"$RELEASE_BIN" ingest --config "$DOGFOOD/config.toml"
```
**verify per env**:
- `KEBAB_OCR_*` (config schema v5: 공유 OCR 엔진 env — image·pdf 양쪽 적용).
- `KEBAB_IMAGE_OCR_ENABLED` / `KEBAB_PDF_OCR_ENABLED` (미디어별 on/off 토글) + PDF 고유 `KEBAB_PDF_OCR_{ALWAYS_ON,VALID_RATIO_THRESHOLD,MIN_CHAR_COUNT,LANG_HINT}`.
- `KEBAB_MODELS_LLM_*`, `KEBAB_MODELS_EMBEDDING_*`.
- `KEBAB_READONLY` (write-path subcommand 차단).
### §11.6 Cancel handle
- 대형 PDF (metro-korea 58MB) ingest 도중 SIGINT (Ctrl+C).
- expected: graceful abort + `IngestEvent::Aborted` + partial counts.
### §11.7 `--readonly` mode
```bash
KEBAB_READONLY=1 "$RELEASE_BIN" ingest # 기대: refuse
KEBAB_READONLY=1 "$RELEASE_BIN" search "query" # 기대: OK
```
### §11.8 `--quiet` mode
```bash
"$RELEASE_BIN" ingest --quiet # stderr 0
"$RELEASE_BIN" ingest --json # implies quiet
```
### §11.9 `.kebabignore`
```text
# In workspace root
node_modules/
*.tmp
draft/**
```
**verify**:
- ignore patterns 정확히 적용.
- per-directory `.kebabignore` cascading.
### §11.10 Config edge cases
- missing config → fall back to XDG default.
- malformed TOML → `error.v1` with `config_invalid`.
- unknown field → tolerant (forward-compat) 또는 strict (TBD).
- workspace.root path 변수 (`~`, `${XDG_DATA_HOME}`, relative path) 모두 동작.
### §11.11 Concurrent access
- 동일 KB 의 multiple ingest 동시 실행 → SQLite lock 또는 graceful queue.
- ingest 중 search → consistent snapshot.
### §11.12 macOS path collision (HOTFIXES 2026-05-07)
- `config_dir()``data_dir()` 보장.
- legacy `~/Library/...` path 의 자동 migration.
---
## §12 Bug discovery checklist
새 feature 또는 새 release 마다 본 checklist 실행:
### §12.1 Pre-flight
- [ ] release binary build clean.
- [ ] Ollama endpoint reachable + 사용 model pull.
- [ ] isolated KB workspace 분리.
- [ ] config.toml = default + minimal customize.
### §12.2 Ingest path
- [ ] 각 media type 의 baseline (markdown / image / pdf / code / Tier2/3).
- [ ] empty / corrupt / encrypted file.
- [ ] very large file (size limit verify).
- [ ] incremental + force-reingest cycle.
- [ ] env var override.
- [ ] cancel handle mid-ingest.
### §12.3 Search path
- [ ] lexical (한국어 + 영어 + mixed).
- [ ] vector (semantic).
- [ ] hybrid (RRF).
- [ ] filter (tag/lang/path-glob).
- [ ] pagination cursor.
- [ ] bulk search.
### §12.4 Ask path
- [ ] in-corpus question (grounded=true).
- [ ] out-of-corpus question (grounded=false).
- [ ] streaming (`--stream`).
- [ ] multi-hop (`--multi-hop`).
- [ ] NLI verification (known hallucination case).
### §12.5 Surface verify
- [ ] CLI flags (각 subcommand 의 --help + actual behavior).
- [ ] TUI panel + keyboard shortcuts.
- [ ] MCP stdio (each tool).
- [ ] Wire schema (`--json` mode 의 schema_version + jq validity).
### §12.6 Version cascade
- [ ] parser_version bump → 자동 invalidation.
- [ ] chunker_version bump → chunk_id 재계산.
- [ ] embedding_version bump → LanceDB 별 table.
### §12.7 Edge cases
- [ ] env override (각 KEBAB_* env).
- [ ] readonly mode.
- [ ] quiet mode.
- [ ] .kebabignore.
- [ ] config edge cases.
- [ ] concurrent access.
### §12.8 Doc + Wire schema 정합
- [ ] README + HANDOFF + ARCHITECTURE 의 사용자 visible surface 일치.
- [ ] wire schema 의 actual emit field 와 schema.json 일치.
- [ ] error.v1 의 `code` 값이 실제 surface 와 일치.
### §12.9 Bug discovery → immediate fix cycle
사용자 명시: "아무리 작은거여도 발견되면 바로 이어서 그걸 수정하는 작업들을 하도록 해".
bug 발견 시:
1. **immediate stop dogfood** (현 scenario 중단).
2. **bug log**`.omc/reviews/<date>-<sub-item>-dogfood-report.md` 에 다음 fields:
- file:line of root cause.
- reproduction command.
- expected vs actual.
- severity (Critical / Important / Minor).
3. **spec/plan/executor cycle** (size-adapted):
- **small bug** (1-line fix / wording typo / config field rename): simplified spec (1 page) + plan (1-2 step) + executor.
- **large bug** (cross-crate / wire schema / new invariant): full spec/plan/executor cycle (이전 v0.20-sub1-bugfix scale).
4. **fix verify** — workspace test + clippy + dogfood scenario 재확인.
5. **dogfood resume** from where it left off.
---
## §13 Reference dogfood corpus
### §13.1 PDF (9 file, PoC + sub-item 1)
| # | File | Size | Source | Use case |
|---|------|------|--------|----------|
| 1 | scanned_page1.pdf (F1) | 466 KB | `crates/kebab-parse-pdf/tests/fixtures/` | scanned 한국어 일반 (OCR alnum > 85%) |
| 2 | scanned_page2.pdf (F2) | 773 KB | `crates/kebab-parse-pdf/tests/fixtures/` | scanned 받침 intensive (OCR alnum > 70%) |
| 3 | metro-korea.pdf | 58 MB | `/build/cache/pdf-ocr-poc/fixtures/` | real-world 신문 multi-page vector PDF |
| 4 | mojibake.pdf (F4) | 23 KB | `crates/kebab-parse-pdf/tests/fixtures/` | vector PDF (Latin + no ToUnicode CMap) |
| 5 | flate_raw.pdf (F6) | 872 B | `crates/kebab-parse-pdf/tests/fixtures/` | FlateDecode skip path |
| 6 | ccitt.pdf (F7) | 2 KB | `crates/kebab-parse-pdf/tests/fixtures/` | CCITTFax skip path |
| 7 | thermal-pos-printer.pdf | 1.1 MB | `~/paperboy/` | ENG manual PDF (encrypted) |
| 8 | thermal-label.pdf | 2.7 MB | `~/paperboy/` | ENG manual PDF (encrypted) |
| 9 | internals-presentation.pdf | 820 KB | `~/namu-crawler/docs/` | slide deck PDF (vector text) |
### §13.2 PoC ground-truth (alnum e2e)
`/build/cache/pdf-ocr-poc/ground-truth/`:
- `page1.txt` (1489 byte) — F1 ground-truth.
- `page2-batchim.txt` (3282 byte) — F2 ground-truth (받침 intensive).
### §13.3 Markdown / code corpus
(향후 sub-item 별 추가 — `~/Documents/notes/` 또는 별 prepared fixture).
### §13.4 Image corpus
(P6 dogfood — 향후 추가).
---
## 부록 A — 본 doc 의 갱신 정책
- 새 release / sub-item 머지 시 §1 (Ingest) 또는 §2-§9 (관련 section) 갱신.
- 새 dogfood report (`.omc/reviews/<date>-<sub-item>-dogfood-report.md`) 의 scenarios 가 본 doc 의 표준 시나리오 와 align.
- 본 doc 의 시나리오 자체에 새 scenarios 추가 시 PR 안 §12 checklist update.
- HOTFIXES.md 의 deviation 발견 시 본 doc 의 관련 section 의 verify step 강화.
## 부록 B — Reviewer 의 dogfood verify
PR review 시 reviewer 가 본 doc 의 §12 checklist 참조:
- 새 feature 의 변경 surface (CLI flag / wire schema / config field) 가 §1-§9 의 시나리오 cover 하는지.
- new bug discovery 시 §12.9 의 immediate fix cycle 적용.