--- title: "pdf-page-v1.2 — PDF 페이지 oversize 청크 분할 (shared oversize module)" created: 2026-06-24 status: implemented extends: tasks/p7/ (pdf-page-v1), HOTFIXES 2026-05-27 (pdf-page-v1.1) follows: docs/superpowers/plans/2026-06-24-md-heading-v2-oversize-split.md contract_sections: [§3.5 Chunk, §4.2 chunk_id recipe, §7.2 Chunker, §9 versioning] design_doc_change: none --- # pdf-page-v1.2 — PDF oversize 청크 분할 ## 문제 md-heading-v2(PR #209)가 markdown/이미지-OCR 텍스트의 예산 초과 청크를 분할하게 했지만, **PDF 는 별도 청커 `pdf-page-v1.1`** 을 쓴다. v1.1 의 `chunk_page` 는 문장(`.?!`)/문단(`\n\n`) 경계로만 자르므로, **경계 없는 거대 페이지**(빽빽한 scanned page 가 한 줄로 OCR 된 경우)는 통째로 한 청크가 되어 strict 임베더(AMD Lemonade)에서 임베드 실패 — md 와 동일한 hole 이 PDF 에 잔존했다(v1.1 module doc 에 "accepted limit" 으로 명시돼 있었음). ## 설계 — 공유 모듈 + 2-tier ### 1. 공유 `crate::oversize` 모듈 md-heading-v2 의 분할 primitive `text_pieces`(줄 경계)·`char_pieces`(UTF-8 char 경계)·`BYTES_PER_TOKEN=3` 을 `crates/kebab-chunk/src/oversize.rs` 로 추출, `pub(crate)` 로 두 청커가 공유. md-heading-v2 는 이제 `crate::oversize::` 를 호출 — **출력 byte-identical**(md parity 테스트 전부 통과, md 라벨 불변). 단일 진실 공급원(DRY)으로 세 번째 복사 방지. ### 2. pdf-page-v1.2 의 2-tier - **Tier 1**(기존): `chunk_page` 의 문장/문단 greedy + overlap. 경계 풍부한 페이지는 그대로 — chunk_id 안정. - **Tier 2**(신규): tier-1 이 내준 segment slice 가 `max_chunk_tokens`(공유 config, byte/3, default 4000)를 넘으면 `crate::oversize::text_pieces` 로 재분할 → **모든 PDF 청크 ≤ 예산** 보장(char fallback 이 경계 없는 페이지도 bound). - `PdfPageV1Chunker { max_chunk_tokens }`(이전 unit struct), `policy_hash()` 에 budget fold(md 와 동일 8-LE-byte append), `pdf_chunker_from_config`(kebab-app)로 config 주입. 신규 config 키 없음 — `max_chunk_tokens` 공유. ### 3. chunk_id sub-piece 스킴 분할 조각은 동일 page block_id 공유 → id 충돌. 기존 `#c{segment_start}` 접미사를 tier-2 에서 `#c{segment_start}s{i}` 로 확장(i = segment 내 0-based). **단일(미분할) segment 는 bare `#c{segment_start}` 유지** → 미분할 PDF 청크의 hash 컴포넌트는 v1.1 과 동일(공통 경우 churn 최소). `segment_start` 가 segment-unique + `i` 가 segment 내 unique → 페이지 전역 고유. ### 4. Page source_span — segment-granular (중요한 설계 결정) 분할 조각의 `SourceSpan::Page` char 범위를 **per-piece 로 정밀 narrow 하려던 최초 구현은 버그였다**: `text_pieces` 가 줄 분할 시 piece 사이의 `'\n'` 구분자를 **소실**(각 split 경계에서 consume, 인접 piece 어디에도 없음)시키므로, piece char 수를 합산하는 running offset 이 줄 경계마다 1씩 **earlier 로 drift**(코드 리뷰 실증: `"aaaa\nbbbb\ncccc"` → drift 2). 게다가 한 줄이 char-split 되면 그 piece 들 사이엔 구분자가 없어 "boundary 당 +1" 보정도 불가능(`Vec` 만으로 복구 불가). → **md-heading-v2 와 동일하게 모든 sub-piece 가 부모 segment 의 `char_start..seg_char_end` 를 그대로 갖는다**(segment-granular). citation 은 올바른 페이지 영역을 가리키며 **절대 drift 하지 않는다**(per-piece 보다 coarse 하나 never wrong). 미분할 단일 piece 는 정확히 segment span → v1.1 과 동일. ### 5. 버전 cascade `VERSION_LABEL` `pdf-page-v1.1` → **`pdf-page-v1.2`** → 다음 plain `kebab ingest` 에서 PDF 자산 1회 자동 재청크(skip-check mismatch). markdown/code 무영향. `max_chunk_tokens` 는 이미 `ingest_config_signature` 공통 prefix 에 있어 budget 변경 시 PDF 재색인이 작동(단 v1.1 은 값을 무시했음 → 이제 실효). ## 검증 단위 테스트(`crates/kebab-chunk/src/pdf_page_v1.rs`): `oversize_pdf_page_splits` (경계 없는 페이지 → tier-2 char-split, 각 ≤ budget), `oversize_pdf_page_with_newlines_splits_without_span_drift`(줄 포함 페이지 — span drift 회귀 잠금), `non_oversize_pdf_unchanged`(미분할 = v1.1 동일), `policy_hash_matches_md_heading_v2_for_identical_policy_and_budget`, `budget_in_policy_hash`. `crate::oversize` roundtrip 테스트 2종. md parity 테스트 전부 통과(md 무변경). kebab-chunk lib 93 pass / kebab-app green / clippy `-D warnings` 0. 도그푸딩(실험 KB, scanned PDF + arctic@Lemonade): [HOTFIXES 2026-06-24 pdf-page-v1.2 entry 참조]. ## 버전 `Cargo.toml` workspace version: minor bump(사용자-visible — 빽빽한 scanned PDF 가 분할되어 검색 hit 분리 + strict 임베더 호환). follow-up #1/#2/#3 와 함께 배치 릴리스에서 일괄.