docs(tasks): P6-4 image ingest wiring task spec #35

Merged
altair823 merged 2 commits from spec/p6-4-image-ingest-wiring into main 2026-05-02 07:16:37 +00:00
Owner

요약

P6 phase 의 미완 구간 — kebab-app::ingest 가 markdown 만 dispatch 해서 P6-1/2/3 라이브러리가 CLI 에서 보이지 않음 — 을 별도 component task 로 spec 화. P6-1/2/3 의 frozen contract 는 그대로 두고 wiring 만 본 task 의 contract 로 진행.

본 PR 은 spec only — 구현은 별도 PR (feat/p6-4-image-ingest-wiring) 로 진행 예정.

사용자 brainstorming 반영 결정

  • 청킹: 옵션 A — kebab-chunk::md_heading_v1 에 image-only document 분기 추가, 단일 합성 청크 emit.
  • 청크 텍스트 포맷: (β) plain concatenation — <alt>\n\n<ocr.joined>\n\n<caption.text>, 라벨 없음, 빈 부분 drop.
  • 실패 정책: (b) Lenient — ImageExtractor::extract 성공이면 doc 저장, OCR/caption 부분 실패는 Provenance Warning + errors 카운터 미증가.
  • LM 인스턴스: ingest 세션당 1회 빌드, &dyn LanguageModel 공유.
  • Citation: 기존 Region { x, y, w, h } source span. xywh fragment URI 표기는 v1 미적용.
  • HEIC / RAW: MediaType::Other → 기존 markdown-only 와 동일하게 skipped+=1.
  • 책 / 스캔 PDF: P6-4 scope 외 — P7 PDF 라인이 책임 (text-embed PDF 는 pdfium 직접 추출, 스캔본은 P7 내부에서 페이지 렌더 → P6-2 OCR 호출).
  • P6-5 (image-scale-hardening): 미시작 — 사용자 시나리오가 "다이어그램 / 스크린샷 / 카메라 사진" 으로 좁아져 불필요.

변경 파일

파일 변경
tasks/p6/p6-4-image-ingest-wiring.md 신규
tasks/INDEX.md P6 "3 components" → "4 components"

Test plan (spec 자체 검증)

  • Placeholder / TBD / TODO 없음 (self-review)
  • 내부 일관성 (alt fallback contract 가 P6-1 와 chunker 측에서 일치)
  • Scope 단일 PR 분량 (kebab-app dispatch + kebab-chunk 분기 + 통합 테스트)
  • Ambiguity 의도적 중복 (실패 카운터 정책이 Behavior contract + Risks 양쪽 명시)

후속

본 spec 머지 후 feat/p6-4-image-ingest-wiring 브랜치로 implementation PR 진행 (kebab-app deps 추가 + dispatch 분기 + chunker image-only 분기 + 통합 테스트 + SMOKE.md 갱신).

🤖 Generated with Claude Code

## 요약 P6 phase 의 미완 구간 — `kebab-app::ingest` 가 markdown 만 dispatch 해서 P6-1/2/3 라이브러리가 CLI 에서 보이지 않음 — 을 별도 component task 로 spec 화. P6-1/2/3 의 frozen contract 는 그대로 두고 wiring 만 본 task 의 contract 로 진행. 본 PR 은 **spec only** — 구현은 별도 PR (`feat/p6-4-image-ingest-wiring`) 로 진행 예정. ## 사용자 brainstorming 반영 결정 - **청킹**: 옵션 A — `kebab-chunk::md_heading_v1` 에 image-only document 분기 추가, 단일 합성 청크 emit. - **청크 텍스트 포맷**: (β) plain concatenation — `<alt>\n\n<ocr.joined>\n\n<caption.text>`, 라벨 없음, 빈 부분 drop. - **실패 정책**: (b) Lenient — `ImageExtractor::extract` 성공이면 doc 저장, OCR/caption 부분 실패는 Provenance Warning + `errors` 카운터 미증가. - **LM 인스턴스**: ingest 세션당 1회 빌드, `&dyn LanguageModel` 공유. - **Citation**: 기존 `Region { x, y, w, h }` source span. xywh fragment URI 표기는 v1 미적용. - **HEIC / RAW**: `MediaType::Other` → 기존 markdown-only 와 동일하게 `skipped+=1`. - **책 / 스캔 PDF**: P6-4 scope 외 — P7 PDF 라인이 책임 (text-embed PDF 는 pdfium 직접 추출, 스캔본은 P7 내부에서 페이지 렌더 → P6-2 OCR 호출). - **P6-5 (image-scale-hardening)**: 미시작 — 사용자 시나리오가 \"다이어그램 / 스크린샷 / 카메라 사진\" 으로 좁아져 불필요. ## 변경 파일 | 파일 | 변경 | |---|---| | [tasks/p6/p6-4-image-ingest-wiring.md](tasks/p6/p6-4-image-ingest-wiring.md) | 신규 | | [tasks/INDEX.md](tasks/INDEX.md) | P6 \"3 components\" → \"4 components\" | ## Test plan (spec 자체 검증) - [x] Placeholder / TBD / TODO 없음 (self-review) - [x] 내부 일관성 (alt fallback contract 가 P6-1 와 chunker 측에서 일치) - [x] Scope 단일 PR 분량 (kebab-app dispatch + kebab-chunk 분기 + 통합 테스트) - [x] Ambiguity 의도적 중복 (실패 카운터 정책이 Behavior contract + Risks 양쪽 명시) ## 후속 본 spec 머지 후 `feat/p6-4-image-ingest-wiring` 브랜치로 implementation PR 진행 (kebab-app deps 추가 + dispatch 분기 + chunker image-only 분기 + 통합 테스트 + SMOKE.md 갱신). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
altair823 added 1 commit 2026-05-02 07:02:22 +00:00
P6-1 / P6-2 / P6-3 의 라이브러리 파이프라인 (`ImageExtractor`,
`OllamaVisionOcr`, `apply_caption`) 이 모두 머지되어 있지만
`kebab-app::ingest_with_config` 의 dispatch 가 markdown 만 처리하므로
CLI 에서 이미지 자산이 색인되지 않는 미완 구간 존재. 본 spec 은
그 wiring 을 별도 component task 로 잡아 P6-1/2/3 의 frozen contract
는 보존하고 통합만 본 task 의 contract 로 진행되게 한다.

핵심 결정 (사용자 brainstorming 반영):
- 청킹 옵션 A — `kebab-chunk::md_heading_v1` 에 image-only document
  분기 추가, 단일 합성 청크 emit.
- 청크 텍스트 포맷 (β) — `<alt>\n\n<ocr.joined>\n\n<caption.text>`
  plain concatenation. 라벨 없음. 빈 부분 drop.
- 실패 정책 (b) Lenient — extract 성공이면 doc 저장, OCR/caption
  부분 실패는 Provenance Warning + `errors` 카운터 미증가.
- LM 인스턴스 — ingest 세션당 1회 빌드, `&dyn LanguageModel` 공유.
- 책 / 스캔 PDF — P6-4 scope 외, P7 PDF 라인이 책임.
- P6-5 (image-scale-hardening) 미시작 — 사용자 시나리오가
  \"다이어그램 / 스크린샷 / 카메라 사진\" 으로 좁아져 불필요.

INDEX.md: P6 \"3 components\" → \"4 components\".

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.
claude-reviewer-01 requested changes 2026-05-02 07:05:30 +00:00
claude-reviewer-01 left a comment
Member

회차 1 — spec 의 큰 그림은 견고합니다. brainstorming 의 결정 (옵션 A 청킹 / β 청크 텍스트 / Lenient 실패 정책 / LM 1회 빌드 / P6-5 미시작 / 책 P7 이관) 모두 정확히 반영되었고, Risks/notes 섹션의 "determinism stress" 처럼 P6-1 contract 와의 일관성까지 짚어 둔 점도 좋습니다.

머지 전에 정리해야 할 actionable 항목들:

사실 오류 (반드시 수정):

  • Chunk 구조체의 section_label 필드가 실재하지 않고, span 필드는 source_spans: Vec<SourceSpan> 단수가 아니라 복수 — implementation 이 spec 그대로 따르면 컴파일 실패.

컨벤션 불일치:

  • Allowed deps 가 kebab-app 의 현재 Cargo.toml 과 일치 안 함 (kebab-search, kebab-llm, kebab-rag 등 이미 deps 인 항목 누락).
  • Forbidden deps 가 추상적 한 줄 — P6-1/2/3 컨벤션처럼 kebab-tui, kebab-desktop, kebab-eval 명시 필요.

완성도 보강:

  • LM construction 절이 caption 만 다룸 — OllamaVisionOcr 도 동일 1회 빌드 정책 명시.
  • max_parallel_extractors 가 image dispatch 에서 어떻게 honor 되는지 한 줄 명시 (5000장 ingest 시간 추정 가능하게).
  • Definition of Done 이 spec PR / implementation PR 항목 섞임 — 분할 권장.

Cosmetic:

  • is_image_only_document 의 P6-1 contract 와의 관계 doc-comment 한 줄 (defensive 의도 명시).

칭찬 — "Out of scope" 절이 8개 항목을 명시적으로 호명하면서 각각 "왜 본 task scope 가 아닌지 + 어떤 후속 task / phase 에서 다뤄지는지" 까지 짚어 둔 점이 좋습니다. P+ 에서 누군가 "왜 P6-4 가 long-OCR splitting 안 함?" 의문이 들 때 spec 만 보고 답이 나옵니다.

회차 1 — spec 의 큰 그림은 견고합니다. brainstorming 의 결정 (옵션 A 청킹 / β 청크 텍스트 / Lenient 실패 정책 / LM 1회 빌드 / P6-5 미시작 / 책 P7 이관) 모두 정확히 반영되었고, Risks/notes 섹션의 \"determinism stress\" 처럼 P6-1 contract 와의 일관성까지 짚어 둔 점도 좋습니다. 머지 전에 정리해야 할 actionable 항목들: **사실 오류 (반드시 수정)**: - `Chunk` 구조체의 `section_label` 필드가 실재하지 않고, span 필드는 `source_spans: Vec<SourceSpan>` 단수가 아니라 복수 — implementation 이 spec 그대로 따르면 컴파일 실패. **컨벤션 불일치**: - Allowed deps 가 `kebab-app` 의 현재 Cargo.toml 과 일치 안 함 (`kebab-search`, `kebab-llm`, `kebab-rag` 등 이미 deps 인 항목 누락). - Forbidden deps 가 추상적 한 줄 — P6-1/2/3 컨벤션처럼 `kebab-tui`, `kebab-desktop`, `kebab-eval` 명시 필요. **완성도 보강**: - LM construction 절이 caption 만 다룸 — `OllamaVisionOcr` 도 동일 1회 빌드 정책 명시. - `max_parallel_extractors` 가 image dispatch 에서 어떻게 honor 되는지 한 줄 명시 (5000장 ingest 시간 추정 가능하게). - Definition of Done 이 spec PR / implementation PR 항목 섞임 — 분할 권장. **Cosmetic**: - `is_image_only_document` 의 P6-1 contract 와의 관계 doc-comment 한 줄 (defensive 의도 명시). 칭찬 — \"Out of scope\" 절이 8개 항목을 명시적으로 호명하면서 각각 \"왜 본 task scope 가 아닌지 + 어떤 후속 task / phase 에서 다뤄지는지\" 까지 짚어 둔 점이 좋습니다. P+ 에서 누군가 \"왜 P6-4 가 long-OCR splitting 안 함?\" 의문이 들 때 spec 만 보고 답이 나옵니다.
@@ -0,0 +30,4 @@
- `kebab-chunk` (image-document branch in `md-heading-v1`)
- `kebab-store-sqlite`, `kebab-store-vector`, `kebab-embed-local`
- `anyhow`, `serde_json`, `tracing`

Allowed dependencies 리스트가 kebab-app 의 현재 Cargo.toml 과 정확히 일치하지 않습니다 — kebab-search (이미 deps), kebab-llm (이미 deps), kebab-rag (이미 deps), kebab-embed (이미 deps) 가 빠져 있습니다. 본 task 가 그들을 새로 import 하지 않더라도 spec 의 "Allowed" 는 "이 crate 가 의존할 수 있는 surface 의 전체 그림" 으로 P6-1/P6-2/P6-3 가 사용한 컨벤션입니다.

수정 권장:

- `kebab-core`
- `kebab-config`
- `kebab-source-fs`
- `kebab-parse-md` (이미 의존, 본 task 변경 없음)
- `kebab-parse-types`
- `kebab-normalize`
- `kebab-chunk` (image-document branch in `md-heading-v1`)
- `kebab-store-sqlite`, `kebab-store-vector`
- `kebab-search`
- `kebab-embed`, `kebab-embed-local`
- `kebab-llm`, `kebab-llm-local`
- `kebab-rag`
- `kebab-parse-image` (NEW — this task adds it)
- `anyhow`, `serde_json`, `tracing`

그 후 "NEW" 라벨로 본 task 가 실제 추가하는 것 (kebab-parse-image) 을 강조하면 reader 가 변경점을 즉시 파악합니다.

Allowed dependencies 리스트가 `kebab-app` 의 현재 Cargo.toml 과 정확히 일치하지 않습니다 — `kebab-search` (이미 deps), `kebab-llm` (이미 deps), `kebab-rag` (이미 deps), `kebab-embed` (이미 deps) 가 빠져 있습니다. 본 task 가 그들을 새로 import 하지 않더라도 spec 의 "Allowed" 는 "이 crate 가 의존할 수 있는 surface 의 전체 그림" 으로 P6-1/P6-2/P6-3 가 사용한 컨벤션입니다. 수정 권장: ``` - `kebab-core` - `kebab-config` - `kebab-source-fs` - `kebab-parse-md` (이미 의존, 본 task 변경 없음) - `kebab-parse-types` - `kebab-normalize` - `kebab-chunk` (image-document branch in `md-heading-v1`) - `kebab-store-sqlite`, `kebab-store-vector` - `kebab-search` - `kebab-embed`, `kebab-embed-local` - `kebab-llm`, `kebab-llm-local` - `kebab-rag` - `kebab-parse-image` (NEW — this task adds it) - `anyhow`, `serde_json`, `tracing` ``` 그 후 "NEW" 라벨로 본 task 가 실제 추가하는 것 (`kebab-parse-image`) 을 강조하면 reader 가 변경점을 즉시 파악합니다.
@@ -0,0 +32,4 @@
- `anyhow`, `serde_json`, `tracing`
## Forbidden dependencies

Forbidden dependencies 가 "New crates outside the existing kebab-app dep set" 한 줄로 추상적입니다. P6-1/P6-2/P6-3 모두 명시적 forbidden list 를 둡니다. 본 task 도 같은 컨벤션으로 명시 권장:

## Forbidden dependencies

- `kebab-tui`, `kebab-desktop` (P9 미시작 — UI crate 가 ingest 를 호출하면 layering 위반)
- `kebab-eval` (cycle 위험 — eval 이 ingest 를 호출하므로 그 반대는 금지)
- 본 task 가 신설하는 자체 image extractor / OCR / caption 로직 — 모두 `kebab-parse-image` 에 이미 있음. `kebab-app` 안에 image-specific 비즈니스 로직 추가 금지.

이러면 implementation PR 리뷰 시 "왜 이 import 가 들어왔지?" 가 명확해집니다.

Forbidden dependencies 가 "New crates outside the existing kebab-app dep set" 한 줄로 추상적입니다. P6-1/P6-2/P6-3 모두 명시적 forbidden list 를 둡니다. 본 task 도 같은 컨벤션으로 명시 권장: ``` ## Forbidden dependencies - `kebab-tui`, `kebab-desktop` (P9 미시작 — UI crate 가 ingest 를 호출하면 layering 위반) - `kebab-eval` (cycle 위험 — eval 이 ingest 를 호출하므로 그 반대는 금지) - 본 task 가 신설하는 자체 image extractor / OCR / caption 로직 — 모두 `kebab-parse-image` 에 이미 있음. `kebab-app` 안에 image-specific 비즈니스 로직 추가 금지. ``` 이러면 implementation PR 리뷰 시 "왜 이 import 가 들어왔지?" 가 명확해집니다.
@@ -0,0 +67,4 @@
}
fn is_image_only_document(doc: &CanonicalDocument) -> bool {
doc.blocks.len() == 1

(작은 명확성) is_image_only_document 의 doc-comment 에 P6-1 contract 와의 관계를 한 줄 추가 권장 — 현재는 reader 가 "P6-1 이 이미 항상 단일 ImageRef block 만 emit 하는데 왜 chunker 에서 다시 검사?" 의문이 들 수 있습니다.

/// Returns true iff `doc` is an image-only document (single `ImageRef`
/// block). P6-1's `ImageExtractor` already guarantees this shape today
/// — the predicate exists as a defensive guard against (a) future task
/// that introduces multi-block image documents, and (b) accidental
/// route-through of a `[Block::Heading, Block::ImageRef, ...]` shape
/// that would look image-ish but should still flow through the
/// markdown chunker.
fn is_image_only_document(doc: &CanonicalDocument) -> bool { ... }

사소하지만 P+ 에서 image-embedded-in-markdown 같은 케이스가 합류했을 때 가드의 의도가 자명해집니다.

(작은 명확성) `is_image_only_document` 의 doc-comment 에 P6-1 contract 와의 관계를 한 줄 추가 권장 — 현재는 reader 가 "P6-1 이 이미 항상 단일 ImageRef block 만 emit 하는데 왜 chunker 에서 다시 검사?" 의문이 들 수 있습니다. ```rust /// Returns true iff `doc` is an image-only document (single `ImageRef` /// block). P6-1's `ImageExtractor` already guarantees this shape today /// — the predicate exists as a defensive guard against (a) future task /// that introduces multi-block image documents, and (b) accidental /// route-through of a `[Block::Heading, Block::ImageRef, ...]` shape /// that would look image-ish but should still flow through the /// markdown chunker. fn is_image_only_document(doc: &CanonicalDocument) -> bool { ... } ``` 사소하지만 P+ 에서 image-embedded-in-markdown 같은 케이스가 합류했을 때 가드의 의도가 자명해집니다.
@@ -0,0 +127,4 @@
6. If `config.image.caption.enabled`:
- Build the LM **once per ingest session** (not per asset) — see "LM construction" below.
- Call `apply_caption(&*llm, &bytes, block, lang_hint, &cfg, &mut events)`.
- Failure → same lenient policy as OCR: warn, log, continue. `block.caption` stays `None`.

Behavior contract 의 ingest dispatch 절이 config.indexing.max_parallel_extractors 와의 관계를 명시하지 않습니다. spec Out of scope 에서 "기존 knob 그대로 적용" 이라 했지만, 실제로 본 task 가 image dispatch 에서 이 knob 을 어떻게 honor 하는지 정확히 안 보입니다.

현재 kebab-app 의 markdown 경로가 이 knob 을 어떻게 쓰는지 (rayon? tokio? 단순 sequential?) 빠르게 확인해서 spec 에 한 줄 추가 권장:

- 병렬화: image branch 는 markdown branch 와 동일한 worker pool 을 공유하며, 자산 디스패치 단위로 `max_parallel_extractors` 를 그대로 honor 한다. OCR/caption HTTP 호출이 blocking I/O 라 worker 1개당 동시 1요청.

또는 "markdown 경로가 현재 sequential 이므로 image 도 sequential" 임을 명시. reader 가 "5000장 PNG ingest 시간 추정" 같은 산수를 spec 만 보고 가능하게.

Behavior contract 의 ingest dispatch 절이 `config.indexing.max_parallel_extractors` 와의 관계를 명시하지 않습니다. spec Out of scope 에서 "기존 knob 그대로 적용" 이라 했지만, 실제로 본 task 가 image dispatch 에서 이 knob 을 어떻게 honor 하는지 정확히 안 보입니다. 현재 `kebab-app` 의 markdown 경로가 이 knob 을 어떻게 쓰는지 (rayon? tokio? 단순 sequential?) 빠르게 확인해서 spec 에 한 줄 추가 권장: ``` - 병렬화: image branch 는 markdown branch 와 동일한 worker pool 을 공유하며, 자산 디스패치 단위로 `max_parallel_extractors` 를 그대로 honor 한다. OCR/caption HTTP 호출이 blocking I/O 라 worker 1개당 동시 1요청. ``` 또는 "markdown 경로가 현재 sequential 이므로 image 도 sequential" 임을 명시. reader 가 "5000장 PNG ingest 시간 추정" 같은 산수를 spec 만 보고 가능하게.
@@ -0,0 +142,4 @@
- `reqwest::blocking::Client` inside `OllamaLanguageModel` is internally `Arc`-shared, so reusing one instance across hundreds of asset iterations is correct and cheap.
### Chunking (kebab-chunk md-heading-v1)

LM construction 절이 caption 의 OllamaLanguageModel 만 다룹니다. OllamaVisionOcrreqwest::blocking::Client 를 내부에 가진 동일 구조라서 ingest session 당 1회 빌드해야 합니다 (자산마다 Client 새로 만들면 connection pool 낭비). 한 줄 추가 권장:

- The OCR path constructs `OllamaVisionOcr::new(&cfg)` **once per ingest invocation** under the same logic as the caption LM — same `reqwest::blocking::Client` Arc-share property applies. The two adapters share the same Ollama endpoint by default (`config.image.ocr.endpoint` falls back to `models.llm.endpoint`) so a single host typically serves both.

LM construction 섹션 제목도 "LM / OCR engine construction" 정도로 일반화 권장.

LM construction 절이 caption 의 `OllamaLanguageModel` 만 다룹니다. `OllamaVisionOcr` 도 `reqwest::blocking::Client` 를 내부에 가진 동일 구조라서 ingest session 당 1회 빌드해야 합니다 (자산마다 Client 새로 만들면 connection pool 낭비). 한 줄 추가 권장: ``` - The OCR path constructs `OllamaVisionOcr::new(&cfg)` **once per ingest invocation** under the same logic as the caption LM — same `reqwest::blocking::Client` Arc-share property applies. The two adapters share the same Ollama endpoint by default (`config.image.ocr.endpoint` falls back to `models.llm.endpoint`) so a single host typically serves both. ``` LM construction 섹션 제목도 "LM / OCR engine construction" 정도로 일반화 권장.
@@ -0,0 +149,4 @@
- `chunk.block_ids = vec![block.common.block_id.clone()]`.
- `chunk.heading_path = vec![]` (image documents have no heading hierarchy).
- `chunk.section_label = None`.
- `chunk.source_span = block.common.source_span.clone()` (the `Region { x, y, w, h }` from P6-1).

사실 오류입니다 — kebab_core::Chunk 구조체에 section_label 필드가 없고, span 필드는 source_span 단수가 아니라 source_spans: Vec<SourceSpan> 입니다 (crates/kebab-core/src/chunk.rs:16-26):

pub struct Chunk {
    pub chunk_id: ChunkId,
    pub doc_id: DocumentId,
    pub block_ids: Vec<BlockId>,
    pub text: String,
    pub heading_path: Vec<String>,
    pub source_spans: Vec<SourceSpan>,
    pub token_estimate: usize,
    pub chunker_version: ChunkerVersion,
    pub policy_hash: String,
}

spec 의 두 줄을 수정 권장:

- `chunk.section_label = None`.
+ (line 삭제 — section_label 필드 없음)

- `chunk.source_span = block.common.source_span.clone()`
+ `chunk.source_spans = vec![block.common.source_span.clone()]`

implementation PR 에서 이 spec 을 그대로 따르면 컴파일 실패합니다. spec contract 가 frozen 되기 전에 잡는 게 좋습니다.

사실 오류입니다 — `kebab_core::Chunk` 구조체에 `section_label` 필드가 없고, span 필드는 `source_span` 단수가 아니라 `source_spans: Vec<SourceSpan>` 입니다 (`crates/kebab-core/src/chunk.rs:16-26`): ```rust pub struct Chunk { pub chunk_id: ChunkId, pub doc_id: DocumentId, pub block_ids: Vec<BlockId>, pub text: String, pub heading_path: Vec<String>, pub source_spans: Vec<SourceSpan>, pub token_estimate: usize, pub chunker_version: ChunkerVersion, pub policy_hash: String, } ``` spec 의 두 줄을 수정 권장: ``` - `chunk.section_label = None`. + (line 삭제 — section_label 필드 없음) - `chunk.source_span = block.common.source_span.clone()` + `chunk.source_spans = vec![block.common.source_span.clone()]` ``` implementation PR 에서 이 spec 을 그대로 따르면 컴파일 실패합니다. spec contract 가 frozen 되기 전에 잡는 게 좋습니다.
@@ -0,0 +195,4 @@
The opt-in real-Ollama integration test from P6-2 / P6-3 stays inside `kebab-parse-image`; this task's integration tests use wiremock so `cargo test --workspace` stays hermetic.
## Definition of Done

Definition of Done 항목들이 spec PR (본 PR) 과 implementation PR 의 항목을 섞어 두었습니다. spec 자체는 "문서가 작성되고 INDEX 가 갱신됨" 으로 끝나야 머지 가능 — cargo test, kebab ingest, SMOKE.md 수정 같은 항목은 implementation PR 의 DoD 입니다.

분할 권장:

## Definition of Done

### Spec PR (this PR)

- [ ] `tasks/p6/p6-4-image-ingest-wiring.md` 작성 완료 + self-review
- [ ] `tasks/INDEX.md` "P6 — 4 components" 반영
- [ ] PR 본문에 design §3.4, §6.1, §9.1 링크

### Implementation PR (follow-up)

- [ ] `cargo check --workspace` passes
- [ ] `cargo test --workspace --no-fail-fast -j 1` passes
- [ ] `cargo clippy --workspace --all-targets -- -D warnings` passes
- [ ] `kebab ingest` against TempDir KB (1 markdown + 1 PNG) → `scanned 2 / new 2 / errors 0`
- [ ] `kebab search --mode lexical "<OCR text>"` returns image chunk
- [ ] `kebab inspect doc <image_doc_id>` shows non-empty `block.ocr` / `block.caption`
- [ ] `docs/SMOKE.md` includes image-fixture step

spec 이 머지 가능한 단위인지 implementation 후에야 가능한지 reader 가 한 번에 인지 가능.

Definition of Done 항목들이 spec PR (본 PR) 과 implementation PR 의 항목을 섞어 두었습니다. spec 자체는 "문서가 작성되고 INDEX 가 갱신됨" 으로 끝나야 머지 가능 — `cargo test`, `kebab ingest`, SMOKE.md 수정 같은 항목은 implementation PR 의 DoD 입니다. 분할 권장: ``` ## Definition of Done ### Spec PR (this PR) - [ ] `tasks/p6/p6-4-image-ingest-wiring.md` 작성 완료 + self-review - [ ] `tasks/INDEX.md` "P6 — 4 components" 반영 - [ ] PR 본문에 design §3.4, §6.1, §9.1 링크 ### Implementation PR (follow-up) - [ ] `cargo check --workspace` passes - [ ] `cargo test --workspace --no-fail-fast -j 1` passes - [ ] `cargo clippy --workspace --all-targets -- -D warnings` passes - [ ] `kebab ingest` against TempDir KB (1 markdown + 1 PNG) → `scanned 2 / new 2 / errors 0` - [ ] `kebab search --mode lexical "<OCR text>"` returns image chunk - [ ] `kebab inspect doc <image_doc_id>` shows non-empty `block.ocr` / `block.caption` - [ ] `docs/SMOKE.md` includes image-fixture step ``` spec 이 머지 가능한 단위인지 implementation 후에야 가능한지 reader 가 한 번에 인지 가능.
altair823 added 1 commit 2026-05-02 07:07:22 +00:00
- Allowed dependencies 를 kebab-app 의 현재 Cargo.toml 과 정합되도록
  보강 (kebab-search / kebab-llm / kebab-rag / kebab-embed 누락 추가).
  본 task 가 새로 추가하는 deps 인 `kebab-parse-image` 만 \"NEW\"
  라벨로 강조.
- Forbidden dependencies 를 추상적 한 줄에서 명시 리스트로 교체:
  `kebab-tui` / `kebab-desktop` (UI layering), `kebab-eval` (cycle),
  본 crate 안 image-specific 비즈니스 로직 (kebab-parse-image 가 이미
  처리). P6-1/2/3 spec 의 컨벤션과 통일.
- Public surface 의 `Chunk` 사실 오류 정정:
  • `chunk.section_label = None` 줄 삭제 (필드 없음)
  • `chunk.source_span = ...` → `chunk.source_spans = vec![...]`
    (실제 필드명 + Vec 타입 반영)
  • `token_estimate` / `policy_hash` 채움 정책 추가.
- LM construction 절을 \"LM / OCR engine construction\" 으로 일반화
  + OCR 어댑터도 ingest session 당 1회 빌드 정책 명시.
- Behavior contract 에 \"Parallelism\" 새 절 추가 — 현재 markdown
  branch 가 sequential 임 + 본 task 도 동일 + 5000장 OCR 시간
  추정치까지 명시. 책 P7 이관 신호와 일관.
- Definition of Done 을 spec PR (이 PR — 모두 완료된 항목) 과
  implementation PR (후속) 으로 분할. spec PR 의 머지 가능 시점
  명확.
- `is_image_only_document` 의 doc-comment 추가 — P6-1 contract 가
  이미 단일 ImageRef block 보장하지만 chunker 측 가드의 defensive
  의도 명시.

본 PR 은 spec only — implementation 은 후속 PR.
claude-reviewer-01 approved these changes 2026-05-02 07:08:28 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 의 7개 항목 (Allowed deps 정합화 / Forbidden deps 명시 / Chunk 사실 오류 정정 / OCR adapter 1회 빌드 / Parallelism 절 / DoD 분할 / defensive guard doc) 모두 정확히 반영되었습니다. 추가 actionable 한 지적이 보이지 않습니다.

전체 회차 요약:

  • 회차 1: 사실 오류 + 컨벤션 + 완성도 보강 7건.
  • 회차 2: APPROVE.

본 spec 은 contract 단위로 머지 가능합니다. 사용자가 Gitea UI 에서 머지해 주세요:

#35

"Delete branch after merge" 체크 권장. 로컬 브랜치는 머지 후 git fetch -p + git branch -d spec/p6-4-image-ingest-wiring 로 정리.

머지 후 후속: implementation PR (feat/p6-4-image-ingest-wiring 브랜치) 로 spec 의 "Implementation PR" DoD 항목 (kebab-app dispatch + kebab-chunk 분기 + 통합 테스트 + SMOKE.md) 진행.

회차 2 — 회차 1 의 7개 항목 (Allowed deps 정합화 / Forbidden deps 명시 / Chunk 사실 오류 정정 / OCR adapter 1회 빌드 / Parallelism 절 / DoD 분할 / defensive guard doc) 모두 정확히 반영되었습니다. 추가 actionable 한 지적이 보이지 않습니다. 전체 회차 요약: - 회차 1: 사실 오류 + 컨벤션 + 완성도 보강 7건. - 회차 2: APPROVE. 본 spec 은 contract 단위로 머지 가능합니다. 사용자가 Gitea UI 에서 머지해 주세요: http://gitea.altair823.xyz/altair823-org/kebab/pulls/35 \"Delete branch after merge\" 체크 권장. 로컬 브랜치는 머지 후 `git fetch -p` + `git branch -d spec/p6-4-image-ingest-wiring` 로 정리. 머지 후 후속: implementation PR (`feat/p6-4-image-ingest-wiring` 브랜치) 로 spec 의 \"Implementation PR\" DoD 항목 (kebab-app dispatch + kebab-chunk 분기 + 통합 테스트 + SMOKE.md) 진행.
@@ -0,0 +184,4 @@
- When `config.image.ocr.enabled = false` AND `config.image.caption.enabled = false`, the image is still extracted, stored, and chunked — the user gets EXIF + dimensions + filename indexed, with empty OCR / caption.
- The synthesized chunk text falls back to just the filename. Lexical search on filenames still works; vector search produces a best-effort embedding from a one-line input.
### Failure semantics summary

(칭찬) Parallelism 새 절이 "현재 markdown 도 sequential" 사실 + "5000장 × per-call latency 의 4-7시간 범위" 추정을 구체적 수치로 박은 게 좋습니다. brainstorming 단계에서 사용자가 "책은 PDF 로 가겠다" 결정한 배경을 spec 만 보고도 다시 인지할 수 있어 미래 reader 가 "왜 P6-5 가 없지?" 의문이 들 때 답이 한 줄로 나옵니다.

(칭찬) Parallelism 새 절이 "현재 markdown 도 sequential" 사실 + "5000장 × per-call latency 의 4-7시간 범위" 추정을 구체적 수치로 박은 게 좋습니다. brainstorming 단계에서 사용자가 "책은 PDF 로 가겠다" 결정한 배경을 spec 만 보고도 다시 인지할 수 있어 미래 reader 가 "왜 P6-5 가 없지?" 의문이 들 때 답이 한 줄로 나옵니다.
@@ -0,0 +212,4 @@
| integration | Determinism: ingest the same PNG twice → identical `doc_id`, `chunk_id` (P1 idempotency contract holds) | inline |
| integration | OCR Ollama returns 503 → asset still indexed; `block.ocr = None`; provenance has Warning event; `errors` counter NOT incremented; ingest returns Ok | wiremock |
| integration | Caption Ollama returns 503 → asset still indexed; `block.caption = None`; provenance Warning; `errors` not incremented | wiremock |
| integration | `image.ocr.enabled = false` AND `image.caption.enabled = false` → image still indexed; chunk text = filename only | inline |

(칭찬) chunk.source_spans / token_estimate / policy_hash 의 정확한 필드명 + 채움 정책이 한 자리에 모였습니다. 특히 policy_hash 가 markdown 과 동일 값을 공유한다는 명시는 "policy 변경 시 image chunk 도 함께 invalidate" 라는 §4.2 ID recipe 의 의도를 implementation 단계에서 그대로 따라가게 만드는 가드입니다.

(칭찬) `chunk.source_spans` / `token_estimate` / `policy_hash` 의 정확한 필드명 + 채움 정책이 한 자리에 모였습니다. 특히 `policy_hash` 가 markdown 과 동일 값을 공유한다는 명시는 "policy 변경 시 image chunk 도 함께 invalidate" 라는 §4.2 ID recipe 의 의도를 implementation 단계에서 그대로 따라가게 만드는 가드입니다.

(칭찬) Definition of Done 의 spec PR / implementation PR 분할 + 본 PR 의 spec 항목은 모두 [x] 로 자체 마킹된 상태가 좋습니다. reviewer 가 "이 PR 머지 가능 여부" 와 "후속 PR 에서 검증할 항목" 을 한눈에 분리할 수 있고, implementation PR 작성자가 spec PR 의 항목을 다시 처리하지 않게 막아 줍니다.

(칭찬) Definition of Done 의 spec PR / implementation PR 분할 + 본 PR 의 spec 항목은 모두 [x] 로 자체 마킹된 상태가 좋습니다. reviewer 가 "이 PR 머지 가능 여부" 와 "후속 PR 에서 검증할 항목" 을 한눈에 분리할 수 있고, implementation PR 작성자가 spec PR 의 항목을 다시 처리하지 않게 막아 줍니다.
altair823 merged commit 5453829195 into main 2026-05-02 07:16:37 +00:00
altair823 deleted branch spec/p6-4-image-ingest-wiring 2026-05-02 07:16:38 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#35