Files
kebab/docs/superpowers/plans/2026-06-24-pdf-page-v1.2-oversize-split.md
altair823 ed8ab7cdbe feat(chunk): pdf-page-v1.2 — PDF 페이지 oversize 분할 + 공유 oversize 모듈
md-heading-v2(PR #209)의 oversize 분할을 PDF 청커에도 적용. v1.1 의 chunk_page 는
문장/문단 경계로만 잘라서 경계 없는 거대 페이지(빽빽한 scanned page 한 줄 OCR)가
통째로 한 청크 → strict 임베더 실패. v1.2 는 2-tier: tier-1(문장/문단 greedy +
overlap) 후 segment 가 max_chunk_tokens 초과면 tier-2 가 공유 text_pieces 로
재분할 → 모든 PDF 청크 ≤ 예산.

- 신규 공유 모듈 crate::oversize (text_pieces/char_pieces/BYTES_PER_TOKEN) — md 와
  PDF 가 공유(단일 진실 공급원). md-heading-v2 는 호출만, 출력 byte-identical(md
  라벨·동작 불변, parity 테스트 전부 통과).
- PdfPageV1Chunker { max_chunk_tokens } + policy_hash budget fold(md 동형) +
  pdf_chunker_from_config(kebab-app). 신규 config 키 없음.
- 분할 조각 chunk_id 는 #c{segment_start}s{i}(미분할 단일 segment 는 bare
  #c{segment_start} 유지 → 공통 경우 v1.1 동일).
- Page span: 분할 조각은 부모 segment 의 char 범위를 그대로 가짐(segment-granular,
  md 동형). per-piece narrowing 은 text_pieces 의 줄 구분자 소실로 drift 하는
  버그라 코드 리뷰 후 제거 — 회귀 테스트
  oversize_pdf_page_with_newlines_splits_without_span_drift 로 잠금.
- chunker_version v1.1→v1.2 → 다음 ingest 에서 PDF 1회 자동 재청크(md/code 무영향).

검증: kebab-chunk lib 93 pass(span 회귀 포함), kebab-app green, clippy 0. 도그푸딩
(실험 KB, scanned PDF + arctic@Lemonade, budget 200): 625/625 errors=0,
scanned_page1 1→3 청크·scanned_page2 3→7 청크(둘 다 pdf-page-v1.2), 전 코퍼스
10215 청크 전부 ≤200. 버전 bump 은 follow-up 들과 함께 배치 릴리스에서 일괄.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 05:47:09 +00:00

4.8 KiB

title, created, status, extends, follows, contract_sections, design_doc_change
title created status extends follows contract_sections design_doc_change
pdf-page-v1.2 — PDF 페이지 oversize 청크 분할 (shared oversize module) 2026-06-24 implemented tasks/p7/ (pdf-page-v1), HOTFIXES 2026-05-27 (pdf-page-v1.1) docs/superpowers/plans/2026-06-24-md-heading-v2-oversize-split.md
§3.5 Chunk
§4.2 chunk_id recipe
§7.2 Chunker
§9 versioning
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=3crates/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<String> 만으로 복구 불가). → 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.1pdf-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 와 함께 배치 릴리스에서 일괄.