docs(tasks): P7-3 pdf ingest wiring task spec #39

Merged
altair823 merged 2 commits from spec/p7-3-pdf-ingest-wiring into main 2026-05-02 09:19:56 +00:00
Owner

요약

P7-1 (PdfTextExtractor) + P7-2 (PdfPageV1Chunker) 라이브러리는 머지됐지만 kebab-app::ingestMediaType::Pdf 를 dispatch 하지 않아 CLI 에서 PDF 가 보이지 않는 상태. P7-3 가 그 와이어링을 다룸. P6-4 image wiring 패턴과 평행 — 두 가지 구조적 차이만 존재:

  1. PDF chunking 은 별도 chunker (pdf-page-v1) 사용. P6-4 가 md-heading-v1 안에 image-only branch 를 추가했던 것과 다름.
  2. PDF 한 자산이 수십~수백 chunk 생성 가능 (페이지당 1+). embedder loop 가 자산당 수백 chunk 까지 안전해야 함.

핵심 결정 (spec 본문 추출)

  • Dispatch: ingest_one_assetMediaType::Pdf arm 추가 → 새 private fn ingest_one_pdf_asset (P6-4 의 ingest_one_image_asset 평행).
  • Chunker selection: PDF 는 PdfPageV1Chunker 하드코딩, md 는 MdHeadingV1Chunker 그대로. compile-time match 기반 — 추후 chunker 가 늘면 registry 화 (P+).
  • config.chunking.chunker_version deviation: 현재 단일값 필드라 markdown 만 represents. PDF 는 무시 + 하드코딩 — implementation 시점에 HOTFIXES entry. 사용자 혼란 방지용 ingest 시작 시 tracing::info! 또는 향후 chunker registry task 까지 TODO.
  • Encrypted PDF / corrupt PDF: errors+=1. P7-1 의 qpdf --decrypt 안내 메시지 verbatim 보존.
  • 빈 페이지 (scanned candidate): asset 인덱싱, 빈 페이지 0 chunk, P7-1 emit Warning 그대로 통과. 향후 OCR fallback 별도 task.
  • Determinism stress: extract → chunk 사이 now() 추가 호출 금지 (P6-4 와 동일).

테스트 plan (11개, implementation PR 에서)

  • 3-page text PDF → 1 doc + 3 chunk + Page span 검증
  • re-ingest determinism (P1 idempotency)
  • encrypted PDF / corrupt PDF → errors+=1 + remediation message
  • mixed page (scanned candidate page 2) → asset stored + Warning event
  • inspect doc → per-page Block::Paragraph + SourceSpan::Page
  • hybrid search across mixed corpus → PDF chunk hit
  • RAG ask → Citation::source_span 이 Page
  • IngestReport invariant (scanned == new + updated + skipped + errors)
  • 50-page long PDF → 50+ chunks
  • smoke 업데이트 (PDF fixture 추가)

디자인 매핑

§3.4 SourceSpan::Page, §3.5 Chunk, §6.1 ingest pipeline, §7.2 Extractor/Chunker traits, §9.2 PDF text extraction.

INDEX 반영

P7 — 2 componentsP7 — 3 components.

Out of scope (spec 본문)

  • 스캔 PDF OCR fallback (별도 P+ task — P6-2 OllamaVisionOcr 재사용 + PDF 페이지 raster)
  • 멀티-컬럼 reading order / 표 / 수식 / 폼 필드 / 북마크
  • 본문 다국어 (CID 폰트 의존)
  • per-medium chunker_version config 분리 (P+ chunker registry task)
  • 자산당 parallelism (수백-페이지 책 의 embedding throughput hardening)
  • 진행상황 reporting / 새 wire schema bump

Test plan

  • spec 작성 + self-review (placeholder / 모순 / 모호성 / scope)
  • INDEX.md P7 components 갱신
  • design §3.4 §3.5 §6.1 §9.2 링크
  • deviation 명시 (chunker_version 무시)
  • 사용자 검토 후 implementation PR (feat/p7-3-pdf-ingest-wiring) 으로 follow-up
## 요약 P7-1 (`PdfTextExtractor`) + P7-2 (`PdfPageV1Chunker`) 라이브러리는 머지됐지만 `kebab-app::ingest` 가 `MediaType::Pdf` 를 dispatch 하지 않아 CLI 에서 PDF 가 보이지 않는 상태. P7-3 가 그 와이어링을 다룸. P6-4 image wiring 패턴과 평행 — 두 가지 구조적 차이만 존재: 1. PDF chunking 은 **별도** chunker (`pdf-page-v1`) 사용. P6-4 가 `md-heading-v1` 안에 image-only branch 를 추가했던 것과 다름. 2. PDF 한 자산이 **수십~수백** chunk 생성 가능 (페이지당 1+). embedder loop 가 자산당 수백 chunk 까지 안전해야 함. ## 핵심 결정 (spec 본문 추출) - **Dispatch**: `ingest_one_asset` 에 `MediaType::Pdf` arm 추가 → 새 private fn `ingest_one_pdf_asset` (P6-4 의 `ingest_one_image_asset` 평행). - **Chunker selection**: PDF 는 `PdfPageV1Chunker` 하드코딩, md 는 `MdHeadingV1Chunker` 그대로. compile-time `match` 기반 — 추후 chunker 가 늘면 registry 화 (P+). - **`config.chunking.chunker_version` deviation**: 현재 단일값 필드라 markdown 만 represents. PDF 는 무시 + 하드코딩 — implementation 시점에 HOTFIXES entry. 사용자 혼란 방지용 ingest 시작 시 `tracing::info!` 또는 향후 chunker registry task 까지 TODO. - **Encrypted PDF / corrupt PDF**: `errors+=1`. P7-1 의 `qpdf --decrypt` 안내 메시지 verbatim 보존. - **빈 페이지 (scanned candidate)**: asset 인덱싱, 빈 페이지 0 chunk, P7-1 emit Warning 그대로 통과. 향후 OCR fallback 별도 task. - **Determinism stress**: extract → chunk 사이 `now()` 추가 호출 금지 (P6-4 와 동일). ## 테스트 plan (11개, implementation PR 에서) - 3-page text PDF → 1 doc + 3 chunk + Page span 검증 - re-ingest determinism (P1 idempotency) - encrypted PDF / corrupt PDF → errors+=1 + remediation message - mixed page (scanned candidate page 2) → asset stored + Warning event - inspect doc → per-page Block::Paragraph + SourceSpan::Page - hybrid search across mixed corpus → PDF chunk hit - RAG ask → Citation::source_span 이 Page - IngestReport invariant (`scanned == new + updated + skipped + errors`) - 50-page long PDF → 50+ chunks - smoke 업데이트 (PDF fixture 추가) ## 디자인 매핑 §3.4 `SourceSpan::Page`, §3.5 Chunk, §6.1 ingest pipeline, §7.2 Extractor/Chunker traits, §9.2 PDF text extraction. ## INDEX 반영 `P7 — 2 components` → `P7 — 3 components`. ## Out of scope (spec 본문) - 스캔 PDF OCR fallback (별도 P+ task — P6-2 OllamaVisionOcr 재사용 + PDF 페이지 raster) - 멀티-컬럼 reading order / 표 / 수식 / 폼 필드 / 북마크 - 본문 다국어 (CID 폰트 의존) - per-medium `chunker_version` config 분리 (P+ chunker registry task) - 자산당 parallelism (수백-페이지 책 의 embedding throughput hardening) - 진행상황 reporting / 새 wire schema bump ## Test plan - [x] spec 작성 + self-review (placeholder / 모순 / 모호성 / scope) - [x] INDEX.md P7 components 갱신 - [x] design §3.4 §3.5 §6.1 §9.2 링크 - [x] deviation 명시 (chunker_version 무시) - [ ] 사용자 검토 후 implementation PR (`feat/p7-3-pdf-ingest-wiring`) 으로 follow-up
altair823 added 1 commit 2026-05-02 09:07:37 +00:00
P7-1 (`PdfTextExtractor`) + P7-2 (`PdfPageV1Chunker`) 의 라이브러리는
완성됐지만 `kebab-app::ingest` 가 `MediaType::Pdf` 를 dispatch 하지 않아
CLI 에서 PDF 가 보이지 않는 상태. P7-3 이 그 와이어링을 다룬다 — P6-4
의 image wiring 패턴과 평행.

핵심 결정 (spec 본문):
- 새 private fn `ingest_one_pdf_asset` (P6-4 의 `ingest_one_image_asset`
  와 평행). `ingest_one_asset` match 에 `MediaType::Pdf` arm 추가.
- per-medium chunker 선택: PDF 는 `PdfPageV1Chunker` 하드코딩 (md 는
  `MdHeadingV1Chunker` 그대로). `config.chunking.chunker_version` 은 PDF
  ingest 에서 무시 (deviation, HOTFIXES 추가 예정).
- encrypted PDF / corrupt PDF → `errors+=1` + `IngestItem.error` 에 P7-1
  의 `qpdf --decrypt` 안내 그대로 보존.
- 빈/scanned candidate 페이지 → asset 인덱싱, 빈 페이지 0 chunk, P7-1
  emit 한 `Provenance::Warning` 그대로 통과. 향후 OCR fallback 까지는
  검색 불가 (out of scope).
- determinism stress: extract → chunk 사이에 `now()` 추가 호출 금지
  (P6-4 와 동일 invariant).
- 11 통합 테스트 + smoke 업데이트 (별도 implementation PR).

`tasks/INDEX.md` P7 components 2 → 3 반영.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-02 09:09:48 +00:00
claude-reviewer-01 left a comment
Member

회차 1 — spec scope / 모호성 / coverage 갭 4건. 가장 중요한 건 RAG ask 테스트 scope 이탈 (P4-3 책임 영역 침범 + wiremock+RAG 인프라 신설 비용). 그 외 byte-edit re-ingest 케이스 추가, embedding-fail 행 모호성 해소, 사전 운영 jump (skipped → 이동) 명시. 전체 contract 자체는 견고 — determinism stress / chunker dispatch / SourceSpan::Page 첫 저장 노출 / chunker_version deviation 정직성 모두 좋음.

회차 1 — spec scope / 모호성 / coverage 갭 4건. 가장 중요한 건 RAG ask 테스트 scope 이탈 (P4-3 책임 영역 침범 + wiremock+RAG 인프라 신설 비용). 그 외 byte-edit re-ingest 케이스 추가, embedding-fail 행 모호성 해소, 사전 운영 jump (skipped → 이동) 명시. 전체 contract 자체는 견고 — determinism stress / chunker dispatch / SourceSpan::Page 첫 저장 노출 / chunker_version deviation 정직성 모두 좋음.
@@ -0,0 +137,4 @@
| Per-page text extraction Err (panic absorbed by `catch_unwind`) | unchanged | yes | `Provenance::Warning` (page N "scanned candidate") — emitted by P7-1, propagated through |
| Empty page (no `/Contents` stream) | unchanged | yes | `Provenance::Warning` (page N "empty (scanned candidate)") — emitted by P7-1 |
| Embedding call fails for a chunk | bubbles up via existing markdown path's error handling | partial — depends on existing behaviour | n/a |

(nit / 명시성) Dispatch 절이 "new PDF branch (this task)" 만 적고, 이전에는 PDF 가 _ arm 에서 Skipped 으로 처리됐다는 사실 을 명시하지 않습니다. 본 PR 머지 후 IngestReport.skipped 카운터가 PDF 자산 수만큼 줄어들고 (MediaType::Audio | MediaType::Other 만 남음) scanned/new/updated 가 그만큼 늘어나는 운영 변화가 있습니다.

Why: P5 eval / 통합 테스트 / 사용자가 ingest 횟수 비교 시 "왜 갑자기 skipped 가 줄었지?" 의 원인을 spec 에서 바로 찾을 수 있어야 함.

How to apply: 첫 번째 bullet 직후 한 줄 추가:

- 이전 동작: `MediaType::Pdf` 는 `_` (default) arm 에서 `Skipped` 분류. 본 task 머지 후 PDF 자산 N 개당 `skipped` 카운터가 N 만큼 줄고 `scanned`/`new`/`updated` 가 그만큼 늘어난다 — 운영자에게는 "전체 ingest 통계가 한 번 jump" 로 보임.
(nit / 명시성) Dispatch 절이 "new PDF branch (this task)" 만 적고, **이전에는 PDF 가 `_` arm 에서 `Skipped` 으로 처리됐다는 사실** 을 명시하지 않습니다. 본 PR 머지 후 `IngestReport.skipped` 카운터가 PDF 자산 수만큼 줄어들고 (`MediaType::Audio | MediaType::Other` 만 남음) `scanned`/`new`/`updated` 가 그만큼 늘어나는 운영 변화가 있습니다. Why: P5 eval / 통합 테스트 / 사용자가 ingest 횟수 비교 시 "왜 갑자기 skipped 가 줄었지?" 의 원인을 spec 에서 바로 찾을 수 있어야 함. How to apply: 첫 번째 bullet 직후 한 줄 추가: ``` - 이전 동작: `MediaType::Pdf` 는 `_` (default) arm 에서 `Skipped` 분류. 본 task 머지 후 PDF 자산 N 개당 `skipped` 카운터가 N 만큼 줄고 `scanned`/`new`/`updated` 가 그만큼 늘어난다 — 운영자에게는 "전체 ingest 통계가 한 번 jump" 로 보임. ```
@@ -0,0 +153,4 @@
## Test plan
| kind | description | fixture / data |

(칭찬) config.chunking.chunker_version deviation 을 spec 에 미리 못 박은 게 정직합니다. 본 task 가 머지된 직후 사용자가 config.tomlchunker_version = "md-heading-v1" 을 보고 "PDF 도 같이 바뀌나?" 라고 묻을 때 정확한 답 ("PDF 는 무시, hard-coded pdf-page-v1") 가 spec 에 박혀 있고, HOTFIXES entry + 향후 chunker registry task 까지 명확히 path 가 그려진 상태. 이런 "문서화 가능한 작은 거짓말" 을 task 단위에서 받아들이는 자세 + path 명시가 워크스페이스의 실용주의 ("Ship working software, document deviations honestly") 와 정렬.

(칭찬) `config.chunking.chunker_version` deviation 을 spec 에 미리 못 박은 게 정직합니다. 본 task 가 머지된 직후 사용자가 `config.toml` 의 `chunker_version = "md-heading-v1"` 을 보고 "PDF 도 같이 바뀌나?" 라고 묻을 때 정확한 답 ("PDF 는 무시, hard-coded `pdf-page-v1`") 가 spec 에 박혀 있고, HOTFIXES entry + 향후 chunker registry task 까지 명확히 path 가 그려진 상태. 이런 "문서화 가능한 작은 거짓말" 을 task 단위에서 받아들이는 자세 + path 명시가 워크스페이스의 실용주의 ("Ship working software, document deviations honestly") 와 정렬.
@@ -0,0 +158,4 @@
| integration | TempDir KB + 1 small text PDF (3 pages) → `kebab ingest` produces 1 doc + 3 chunks; each chunk's `source_spans[0]` is `Page { page: i, .. }`; chunks stored + embedded | `kebab-app/tests/pdf_pipeline.rs` (new), in-memory PDF via `lopdf` builder (mirrors `kebab-parse-pdf::tests::common`) |
| integration | Re-ingest same PDF → identical `doc_id` and identical `chunk_id` set (P1 idempotency contract) | inline |
| integration | Encrypted PDF → asset NOT stored; `errors+=1`; `IngestItem.error` mentions `qpdf` / `decrypt` | inline (lopdf builder + fake `/Encrypt` trailer) |
| integration | Corrupt header PDF → asset NOT stored; `errors+=1`; error message mentions PDF parse failure | inline |

(칭찬) Determinism stress 절이 P6-4 와 동일한 invariant (now() 추가 호출 금지, extract → chunk 사이) 를 명시적으로 못 박은 게 좋습니다. "chunking 은 pure function of CanonicalDocument 이므로 structural 한 제약" 라는 표현이 "왜 now() 호출이 추가되면 안 되는지" 의 근본 원인 (Provenance event 의 at 타임스탬프가 group 단위로 공유되는 invariant) 을 정확히 짚습니다. 미래 reader 가 "이 invariant 가 왜 있더라" 를 spec 만 읽고도 알 수 있는 상태.

(칭찬) Determinism stress 절이 P6-4 와 동일한 invariant (`now()` 추가 호출 금지, extract → chunk 사이) 를 명시적으로 못 박은 게 좋습니다. "chunking 은 pure function of CanonicalDocument 이므로 structural 한 제약" 라는 표현이 "왜 now() 호출이 추가되면 안 되는지" 의 근본 원인 (Provenance event 의 `at` 타임스탬프가 group 단위로 공유되는 invariant) 을 정확히 짚습니다. 미래 reader 가 "이 invariant 가 왜 있더라" 를 spec 만 읽고도 알 수 있는 상태.
@@ -0,0 +166,4 @@
| integration | `IngestReport` invariant `scanned == new + updated + skipped + errors` holds when ingesting a mixed corpus including a corrupt PDF | inline |
| integration | Long PDF (50 pages × ~1.5 KB body each = ~75 KB) produces ≥50 chunks (≥1 per page); embedding loop completes; storage round-trips | inline |
| smoke | Update `docs/SMOKE.md` so the runbook ingests at least one PDF fixture and verifies search-by-page-text works + `inspect doc` shows `SourceSpan::Page` | docs change |

(nit) PdfPageV1Chunker::chunk Err 행이 "would only fire on P7-1 contract drift" 만 표현. 사실은 defensive validation 이라 미래에 누군가 잘못된 doc 을 넘겨도 (예: P+ chunker registry task 의 routing bug) 즉시 잡힙니다. 이 측면도 한 줄 명시하면 spec 가 frozen 된 후에도 chunker 의 가드 의도가 정확히 전달됩니다.

How to apply: 행의 비고를 "P7-1 contract drift OR future routing bug — defensive validation either way" 로 보강.

(nit) `PdfPageV1Chunker::chunk Err` 행이 "would only fire on P7-1 contract drift" 만 표현. 사실은 *defensive validation* 이라 미래에 누군가 잘못된 doc 을 넘겨도 (예: P+ chunker registry task 의 routing bug) 즉시 잡힙니다. 이 측면도 한 줄 명시하면 spec 가 frozen 된 후에도 chunker 의 가드 의도가 정확히 전달됩니다. How to apply: 행의 비고를 "P7-1 contract drift OR future routing bug — defensive validation either way" 로 보강.
@@ -0,0 +169,4 @@
The opt-in real-Ollama integration tests stay in `kebab-llm-local` / `kebab-parse-image`. P7-3 adds zero LM dependency — PDF text extraction is local-only, so there is no equivalent hermetic-vs-real split to manage.
## Definition of Done

(nit / 모호성) Failure semantics 표의 마지막 행 "Embedding call fails for a chunk → bubbles up via existing markdown path's error handling / partial — depends on existing behaviour" 가 implementation 시점에 결정을 미루는 모호한 표현입니다. md path 의 현재 동작을 한 줄로 명시하거나, "P7-3 implementation 이 결정 + HOTFIXES 기록" 식으로 명시.

Why: spec 은 frozen contract 라 "depends on existing behaviour" 같은 회피 표현은 미래 reader 가 디버깅 시 무엇을 기대해야 할지 결정 못 하게 만듭니다. md path 가 어떻게 동작하는지 (PR diff 본문에는 fail-fast 인지 partial 인지) 한 줄 명시 권장.

How to apply: crates/kebab-app/src/lib.rs 의 markdown path 를 한 번 읽고 (embed_chunks 또는 유사 호출 부근) 다음 형식으로 채우기 — 예: "chunk 단위 embedding fail → 해당 chunk 의 vector 만 누락, doc 는 stored, IngestItemKind::Error 가 아닌 Updated 또는 New 로 분류; IngestReport.errors 는 unchanged. 정상 동작."

(nit / 모호성) Failure semantics 표의 마지막 행 "Embedding call fails for a chunk → bubbles up via existing markdown path's error handling / partial — depends on existing behaviour" 가 implementation 시점에 결정을 미루는 모호한 표현입니다. md path 의 현재 동작을 한 줄로 명시하거나, "P7-3 implementation 이 결정 + HOTFIXES 기록" 식으로 명시. Why: spec 은 frozen contract 라 "depends on existing behaviour" 같은 회피 표현은 미래 reader 가 디버깅 시 무엇을 기대해야 할지 결정 못 하게 만듭니다. md path 가 어떻게 동작하는지 (PR diff 본문에는 fail-fast 인지 partial 인지) 한 줄 명시 권장. How to apply: `crates/kebab-app/src/lib.rs` 의 markdown path 를 한 번 읽고 (`embed_chunks` 또는 유사 호출 부근) 다음 형식으로 채우기 — 예: "chunk 단위 embedding fail → 해당 chunk 의 vector 만 누락, doc 는 stored, IngestItemKind::Error 가 아닌 Updated 또는 New 로 분류; `IngestReport.errors` 는 unchanged. 정상 동작."
@@ -0,0 +175,4 @@
- [ ] `tasks/p7/p7-3-pdf-ingest-wiring.md` 작성 + self-review (placeholder / 모순 / 모호성 / scope)
- [ ] `tasks/INDEX.md` "P7 — 3 components" 반영
- [ ] PR 본문에 design §3.4, §3.5, §6.1, §9.2 링크

(칭찬) Storage / wire effects 절이 "SourceSpan::Page 가 store 에 처음 저장된다" 는 사실을 명시한 게 중요합니다. P1-6 시점에 polymorphic source_span_kind/payload 컬럼이 만들어졌지만 Page variant 가 실제로 그 컬럼을 사용하는 건 P7-3 가 처음 — 즉 P1-6 의 polymorphic 약속이 실전에서 검증되는 첫 PR 이라는 의미입니다. "any downstream reader that did not handle this variant is exposed" 한 줄이 미래에 inspect / search JSON 출력 디버깅 시 정확한 출발점을 알려줍니다.

(칭찬) Storage / wire effects 절이 "`SourceSpan::Page` 가 store 에 처음 저장된다" 는 사실을 명시한 게 중요합니다. P1-6 시점에 polymorphic source_span_kind/payload 컬럼이 만들어졌지만 `Page` variant 가 실제로 그 컬럼을 사용하는 건 P7-3 가 처음 — 즉 P1-6 의 polymorphic 약속이 실전에서 검증되는 첫 PR 이라는 의미입니다. "any downstream reader that did not handle this variant is exposed" 한 줄이 미래에 `inspect` / `search` JSON 출력 디버깅 시 정확한 출발점을 알려줍니다.
@@ -0,0 +189,4 @@
- [ ] `kebab ask "<query about PDF body>"` produces an `Answer` whose `Citation` includes a `Page` span
- [ ] HOTFIXES entry written if any spec deviation surfaces during implementation
- [ ] `docs/SMOKE.md` includes a PDF-fixture step

(coverage gap) Re-ingest 테스트는 "동일 byte" 케이스만 명시. md / image path 가 모두 검증하는 "수정된 PDF re-ingest → updated 카운터 증가" 케이스가 빠져있습니다. P1 idempotency contract 의 다른 axis (updated == 1 for edited content) 가 PDF path 에서도 동작하는지 implementation 수준에서 한 번 잠가줘야 합니다.

Why: PDF doc_id 는 (workspace_path, asset_id, parser_version) 의 함수인데 asset_id 는 id_for_asset(blake3_full_hex) 로 byte-기반. byte 가 바뀌면 asset_id 도 바뀌고 따라서 doc_id 도 바뀝니다 → 새 row, new+=1. 만약 그렇지 않다면 (workspace_path 만 바뀌어도 asset_id 보존되는 path 가 있다면) updated 분기가 동작해야 함. 본 invariant 가 PDF 에서도 깨지지 않는지 시각화하는 게 중요.

How to apply: Test plan 표 한 줄 추가:

| integration | Edit a PDF (replace bytes) and re-ingest → new doc_id (different blake3) → `new+=1` for the new doc; old doc unaffected | inline |

또는 "동일 path 같은 asset_id 다른 parser_version" 같은 P+ 시나리오는 이번 task 에 포함시키지 않아도 OK — byte 수정 케이스만 추가.

(coverage gap) Re-ingest 테스트는 "동일 byte" 케이스만 명시. md / image path 가 모두 검증하는 "수정된 PDF re-ingest → updated 카운터 증가" 케이스가 빠져있습니다. P1 idempotency contract 의 다른 axis (`updated == 1` for edited content) 가 PDF path 에서도 동작하는지 implementation 수준에서 한 번 잠가줘야 합니다. Why: PDF doc_id 는 `(workspace_path, asset_id, parser_version)` 의 함수인데 asset_id 는 `id_for_asset(blake3_full_hex)` 로 byte-기반. byte 가 바뀌면 asset_id 도 바뀌고 따라서 doc_id 도 바뀝니다 → 새 row, `new+=1`. 만약 그렇지 않다면 (workspace_path 만 바뀌어도 asset_id 보존되는 path 가 있다면) updated 분기가 동작해야 함. 본 invariant 가 PDF 에서도 깨지지 않는지 시각화하는 게 중요. How to apply: Test plan 표 한 줄 추가: ``` | integration | Edit a PDF (replace bytes) and re-ingest → new doc_id (different blake3) → `new+=1` for the new doc; old doc unaffected | inline | ``` 또는 "동일 path 같은 asset_id 다른 parser_version" 같은 P+ 시나리오는 이번 task 에 포함시키지 않아도 OK — byte 수정 케이스만 추가.
@@ -0,0 +195,4 @@
- **Scanned PDF OCR fallback** — empty/extract-failed pages stay searchable=false in v1. A future task ("P+ scanned-PDF-ocr") routes those pages through P6-2's `OllamaVisionOcr` after rasterising the page via a PDF renderer (e.g. `mupdf-rs`, `pdfium-render`). Excluded here because (a) it requires a new system / Rust dep we don't have yet, and (b) v1 user scenario is text-embed PDFs (papers, exported reports).
- **Multi-column reading order / table extraction / formula detection / form-field extraction / bookmark or outline ingestion** — all deferred to future PDF-layout task. P7-1 already lists these as out of scope and the wiring inherits.
- **Body multilingual via CID font support** — handled at the parser layer (P7-1). UTF-16BE Title metadata works today; non-Latin body text depends on the PDF's font CID mapping.
- **Per-medium `chunker_version` config** — current `Config::chunking.chunker_version` is single-valued and serves markdown only. PDF ingest ignores it (hard-codes `pdf-page-v1`). A future P+ task either splits the field per medium or introduces a chunker registry. Logged as a deviation in `tasks/HOTFIXES.md` once implementation lands.

(suggestion / scope) RAG kebab ask + mock LLM (wiremock) 테스트가 P7-3 scope 안에서 가장 비용이 큰 항목입니다. 새로운 테스트 인프라 (wiremock 으로 LLM 통신 모킹) 가 kebab-app 통합 테스트에 처음 들어오는 것이고, 이건 P4-3 (RAG pipeline) 의 책임 영역을 P7-3 가 떠안는 형국. 본 task 의 진짜 invariant 는 "검색 결과 chunk 의 source_spans[0]Page 인지" 이지 "RAG 응답이 그걸 cite 하는지" 가 아닙니다 — RAG 가 chunk citation 을 잘 propagate 하는 건 P4-3 의 계약.

Why: P7-3 implementation PR 의 surface area 를 키워서 review 비용 + RAG 모킹 디버깅 (P6 / P7 implementation 동안 wiremock + RAG 의 multi-turn 행동을 한 번도 fixture 화 한 적 없음) 가 같이 늘어납니다. 단순 kebab search --mode hybrid 가 PDF chunk + Page span 을 돌려준다는 검증으로 본 task 의 invariant 는 충분.

How to apply: 해당 행을 두 옵션 중 하나로 변경:

  • (A, 추천) RAG ask 테스트를 out of scope 로 옮기고 "P9 (UI) 또는 별도 P+ task 가 PDF citation 을 RAG 답변에 surfacing 하는 것을 검증" 로 표시.
  • (B) RAG ask 테스트를 유지하되 "opt-in 실제 Ollama 통합 테스트" 로 분리해 hermetic 본 PR 통합 테스트 수트 외부에 둠.

결정 후 hotfixes 추적 부담 없이 갈 수 있는 (A) 가 P6-4 실제 구현 PR 의 scope 와 더 평행.

(suggestion / scope) RAG `kebab ask` + mock LLM (wiremock) 테스트가 P7-3 scope 안에서 가장 비용이 큰 항목입니다. 새로운 테스트 인프라 (wiremock 으로 LLM 통신 모킹) 가 `kebab-app` 통합 테스트에 처음 들어오는 것이고, 이건 P4-3 (RAG pipeline) 의 책임 영역을 P7-3 가 떠안는 형국. 본 task 의 진짜 invariant 는 "검색 결과 chunk 의 `source_spans[0]` 가 `Page` 인지" 이지 "RAG 응답이 그걸 cite 하는지" 가 아닙니다 — RAG 가 chunk citation 을 잘 propagate 하는 건 P4-3 의 계약. Why: P7-3 implementation PR 의 surface area 를 키워서 review 비용 + RAG 모킹 디버깅 (P6 / P7 implementation 동안 wiremock + RAG 의 multi-turn 행동을 한 번도 fixture 화 한 적 없음) 가 같이 늘어납니다. 단순 `kebab search --mode hybrid` 가 PDF chunk + Page span 을 돌려준다는 검증으로 본 task 의 invariant 는 충분. How to apply: 해당 행을 두 옵션 중 하나로 변경: - (A, 추천) RAG ask 테스트를 out of scope 로 옮기고 "P9 (UI) 또는 별도 P+ task 가 PDF citation 을 RAG 답변에 surfacing 하는 것을 검증" 로 표시. - (B) RAG ask 테스트를 유지하되 "opt-in 실제 Ollama 통합 테스트" 로 분리해 hermetic 본 PR 통합 테스트 수트 외부에 둠. 결정 후 hotfixes 추적 부담 없이 갈 수 있는 (A) 가 P6-4 실제 구현 PR 의 scope 와 더 평행.
altair823 added 1 commit 2026-05-02 09:11:21 +00:00
- RAG `kebab ask` 통합 테스트 → out of scope 로 이동. RAG citation 라운드트립
  검증은 P4-3 의 책임 영역이고 PDF chunk 도 Citation shape 동일. wiremock+RAG
  인프라를 P7-3 통합 테스트에 신설하는 비용이 본 task 의 invariant 와
  비례하지 않음.
- byte-edit re-ingest 케이스 추가. 동일 byte (P1 idempotency) 외에 byte
  수정 → 새 doc_id → `new+=1` 시나리오를 명시적으로 잠금.
- "Embedding call fails" 행을 `Embedder::embed Err → errors+=1, doc + chunks
  rows 는 이미 저장됨, 재실행 시 재시도" 로 명시. md path 코드 (lib.rs:621+)
  와 일치.
- Dispatch 절에 "이전엔 PDF 가 Skipped 였음 → 머지 후 skipped→scanned 이동"
  운영 jump 한 줄 추가.
- `PdfPageV1Chunker::chunk Err` 비고를 "P7-1 contract drift OR future
  routing bug — defensive validation either way" 로 정확화.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-02 09:11:51 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 지적 5건 (RAG 테스트 out-of-scope 이동, byte-edit re-ingest 케이스 추가, embedding-fail 행 명시, dispatch 운영 jump 표시, chunker validate err 비고 정확화) 모두 반영. spec 머지 가능.

회차 2 — 회차 1 지적 5건 (RAG 테스트 out-of-scope 이동, byte-edit re-ingest 케이스 추가, embedding-fail 행 명시, dispatch 운영 jump 표시, chunker validate err 비고 정확화) 모두 반영. spec 머지 가능.
@@ -0,0 +139,4 @@
| Empty page (no `/Contents` stream) | unchanged | yes | `Provenance::Warning` (page N "empty (scanned candidate)") — emitted by P7-1 |
| `Embedder::embed(...)` Err (any chunk) | `errors+=1` | yes (doc + chunk rows already written before embed call — see below) | n/a |
The embedding call sits *after* `put_document` / `put_blocks` / `put_chunks` in `kebab-app::ingest_one_asset` (markdown path, lines 615+), so a failed embed leaves doc + chunk rows on disk while no vector exists. This is consistent with the markdown path and accepted as v1 behaviour — re-running `kebab ingest` re-attempts the embed for any chunk whose `embedding_id` is missing from the vector store. Whole-asset rollback on embed-fail is a P+ task (atomic ingest transaction).

(칭찬) Dispatch 절에 "이전엔 PDF 가 Skipped 였음 → 머지 후 skipped→scanned 이동" 의 운영 jump 한 줄을 정확히 수치화 ("N PDF files reports skipped decreasing by N") 한 게 좋습니다. P5 eval / smoke / 사용자가 머지 직후 ingest 통계 변화를 보고 "왜 갑자기 숫자가 바뀌지" 의 답을 spec 만 봐도 즉시 찾을 수 있는 상태.

(칭찬) Dispatch 절에 "이전엔 PDF 가 Skipped 였음 → 머지 후 skipped→scanned 이동" 의 운영 jump 한 줄을 정확히 수치화 ("N PDF files reports `skipped` decreasing by N") 한 게 좋습니다. P5 eval / smoke / 사용자가 머지 직후 ingest 통계 변화를 보고 "왜 갑자기 숫자가 바뀌지" 의 답을 spec 만 봐도 즉시 찾을 수 있는 상태.
@@ -0,0 +166,4 @@
| integration | Mixed page PDF (page 1 text, page 2 empty / scanned, page 3 text) → asset stored; 2 chunks (pages 1 + 3); `doc.provenance.events` contains exactly 1 `Warning` for page 2 marked "scanned candidate" | inline |
| integration | `kebab inspect doc <pdf_doc_id>` returns the PDF `CanonicalDocument` with per-page `Block::Paragraph` and `SourceSpan::Page` intact | inline |
| integration | Hybrid search across mixed corpus (1 markdown + 1 PDF) returns the PDF chunk for a query whose terms appear only in the PDF body | inline (real `multilingual-e5-small` embedding) |
| integration | `IngestReport` invariant `scanned == new + updated + skipped + errors` holds when ingesting a mixed corpus including a corrupt PDF | inline |

(칭찬) PdfPageV1Chunker::chunk Err 비고를 "P7-1 contract drift OR future routing bug — defensive validation either way" 로 정확화. 미래 chunker registry task 가 routing bug 를 만들 때 "왜 chunker 가 거부 했지?" 를 spec 에서 즉시 찾을 수 있게 됐습니다.

(칭찬) `PdfPageV1Chunker::chunk Err` 비고를 "P7-1 contract drift OR future routing bug — defensive validation either way" 로 정확화. 미래 chunker registry task 가 routing bug 를 만들 때 "왜 chunker 가 거부 했지?" 를 spec 에서 즉시 찾을 수 있게 됐습니다.
@@ -0,0 +172,4 @@
The opt-in real-Ollama integration tests stay in `kebab-llm-local` / `kebab-parse-image`. P7-3 adds zero LM dependency — PDF text extraction is local-only, so there is no equivalent hermetic-vs-real split to manage.
## Definition of Done

(칭찬) Embedding fail 행을 "Embedder::embed Err → errors+=1, doc + chunks rows 는 이미 저장됨, 재실행 시 재시도" 로 정확화 + md path 코드 위치 (lib.rs` 615+) 명시. "depends on existing behaviour" 같은 회피 표현이 사라지고 spec 가 frozen 후에도 미래 reader 가 "부분 저장 + 재실행 retry" 라는 v1 정책의 정확한 의도를 알 수 있는 상태. 추가로 "Whole-asset rollback on embed-fail 은 P+ task" 라는 향후 path 까지 표시한 게 정직.

(칭찬) Embedding fail 행을 "`Embedder::embed Err → errors+=1, doc + chunks rows 는 이미 저장됨, 재실행 시 재시도" 로 정확화 + md path 코드 위치 (`lib.rs` 615+) 명시. "depends on existing behaviour" 같은 회피 표현이 사라지고 spec 가 frozen 후에도 미래 reader 가 "부분 저장 + 재실행 retry" 라는 v1 정책의 정확한 의도를 알 수 있는 상태. 추가로 "Whole-asset rollback on embed-fail 은 P+ task" 라는 향후 path 까지 표시한 게 정직.
@@ -0,0 +193,4 @@
- [ ] `docs/SMOKE.md` includes a PDF-fixture step
## Out of scope

(칭찬) byte-edit re-ingest 케이스를 P1 idempotency contract 의 다른 axis 로 명시한 게 좋습니다. "identical bytes → identical IDs" 와 "different bytes → different IDs" 두 invariant 가 한 표 안에 나란히 있어야 P1 의 byte-기반 ID 약속이 PDF path 에서도 유지되는 게 확인됩니다. "orphan handling 은 P+" 한 줄까지 단 게 정직 — 이번 PR 의 scope 가 어디서 끝나는지 명확히 표시.

(칭찬) byte-edit re-ingest 케이스를 P1 idempotency contract 의 다른 axis 로 명시한 게 좋습니다. "identical bytes → identical IDs" 와 "different bytes → different IDs" 두 invariant 가 한 표 안에 나란히 있어야 P1 의 byte-기반 ID 약속이 PDF path 에서도 유지되는 게 확인됩니다. "orphan handling 은 P+" 한 줄까지 단 게 정직 — 이번 PR 의 scope 가 어디서 끝나는지 명확히 표시.
@@ -0,0 +212,4 @@
- **Encrypted-PDF error message is the only operator-visible signal.** The `kebab ingest` summary shows `errors=1` and the `IngestItem.error` message; a user who didn't read the full output may miss the `qpdf --decrypt` hint. Acceptable in v1 — a future `kebab inspect ingest <run_id>` (P9) renders structured per-asset errors. For now, ensure the test asserts the hint is preserved verbatim from P7-1's bail string.
- **Determinism stress with `now()` calls.** Same constraint as P6-4 — extract → chunk pipeline must not insert wall-clock reads between steps. P7-1 emits its own `now()` once for all per-page Provenance events; the PDF wiring branch must add no further `now()`s inside the per-asset path.
- **`pdf-page-v1` per-chunk hash variant `#c{char_start}` is opaque.** Downstream tools comparing `chunk_id`s by exact match work fine (it's still a deterministic blake3 input). Tools attempting to derive a stable position from the `chunk_id` alone would fail — they must read `chunk.source_spans[0].char_start`. Documented in P7-2's HOTFIXES entry; cross-referenced here for findability.
- **HEIC / RAW image and PDF/A subspecies share `MediaType::Other`.** PDF/A is detected by `kebab-source-fs` as `MediaType::Pdf` (header is still `%PDF-`); PDF subtype variants do not branch separately. Acceptable for v1.

(칭찬) RAG ask 검증을 out-of-scope 절에 "P4-3 책임 영역, PDF chunk 도 Citation shape 동일, wiremock+RAG 인프라 신설은 P7-3 invariant 와 비례하지 않음" 으로 옮긴 게 정확한 추론입니다. 본 task 가 검증해야 하는 invariant 는 "PDF chunk 가 search 에서 Page span 으로 나온다" 까지이고, RAG citation 라운드트립은 어떤 chunk 든 변하지 않는 P4-3 의 계약 — out-of-scope 절에 captured 한 추론이 미래 reviewer 가 "왜 P7-3 가 RAG 까지 가지 않았지" 를 묻을 때 답이 됩니다.

(칭찬) RAG ask 검증을 out-of-scope 절에 "P4-3 책임 영역, PDF chunk 도 Citation shape 동일, wiremock+RAG 인프라 신설은 P7-3 invariant 와 비례하지 않음" 으로 옮긴 게 정확한 추론입니다. 본 task 가 검증해야 하는 invariant 는 "PDF chunk 가 search 에서 Page span 으로 나온다" 까지이고, RAG citation 라운드트립은 *어떤 chunk* 든 변하지 않는 P4-3 의 계약 — out-of-scope 절에 captured 한 추론이 미래 reviewer 가 "왜 P7-3 가 RAG 까지 가지 않았지" 를 묻을 때 답이 됩니다.
altair823 merged commit 1986e9e026 into main 2026-05-02 09:19:56 +00:00
altair823 deleted branch spec/p7-3-pdf-ingest-wiring 2026-05-02 09:19:57 +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#39