Files
kebab/docs/components
altair823 e76e909f56 chore: PR #238 회차 1 리뷰 반영 — render_dpi 가 동작하지 않았다
리뷰가 이 PR 의 핵심을 무너뜨리는 결함을 잡았다.

1) render_dpi 가 아무 일도 안 하고 있었다 (HIGH)

   `set_maximum_*` 만 걸었는데, pdfium-render 에서 maximum 은 초과할 때만
   줄이는 클램프이고 스케일이 아니다. 타깃도 배율도 없으면 스케일 1.0 —
   1 pt → 1 px, 즉 **72 DPI** 로 렌더된다. 300 을 주든 1200 을 주든 산출물이
   같았다. 렌더가 실패하지 않으니 도그푸딩도 통과해 버렸다.

   실측 (govdocs1-000157-ccitt.pdf 5쪽):
     maximum_* 만 (초안)        621×801 px    72 DPI
     target + maximum_* (수정)  1588×2048 px  184 DPI

   같은 뿌리로 종횡비도 깨져 있었다. 클램프만 걸리는 경로는
   `do_maintain_aspect_ratio = false` 라 가로·세로가 독립적으로 잘린다.
   600×800pt 페이지를 600px 예산으로 렌더하면 600×600 으로 세로가 25%
   눌린 채 나왔고, 긴 변만 보던 테스트는 초록불이었다.

   `render_dpi_changes_the_rendered_size` 와
   `the_pixel_budget_is_respected_without_distorting_the_page` 로 고정했다.
   `set_target_width` 한 줄을 되돌리면 둘 다 실패하는 것을 확인했다.

   덧붙여 render_dpi 는 **요청**이고 max_pixels 가 이긴다. PDF 기본값
   2048 이면 A4 는 175 DPI 언저리에서 잘린다. 기본값 300 이 그대로 나오지
   않는다는 뜻이라 config·README·SMOKE 문구를 실제와 맞췄다.

2) /MediaBox 를 직접 파싱하고 있었다 (MEDIUM)

   `/MediaBox` 는 상속 속성이고 대부분의 생산자가 `/Pages` 노드에 한 번만
   쓴다. lopdf 0.32 에는 상속 해석 헬퍼가 없어서 그런 PDF 는 전부 조용히
   A4 폴백을 탔다. `/UserUnit` 도 미반영이었다.

   pdfium 이 이미 페이지 크기를 안다. 거기서 받으니 40여 줄이 사라지고
   상속·UserUnit 문제가 함께 없어졌으며, kebab-app 이 lopdf 딕셔너리를
   뒤지던 레이어링도 정리됐다.

3) 렌더러가 있으면 오히려 손해 보는 경우가 있었다 (MEDIUM)

   페이지 하나만 렌더에 실패하면 곧장 skip 이었고 DCTDecode 경로를 시도하지
   않았다. "렌더러 우선 + 폴백" 이 렌더러 유무 수준에서만 성립했던 것이다.
   페이지 단위 폴백을 넣었다.

4) 렌더러를 설정한 사용자에게 틀린 지시가 나갔다 (MEDIUM)

   pdfium 이 PDF 자체를 못 열면 모든 페이지가 no_renderer 로 보고되면서
   "render_library 를 지정하라" 고 안내했다. `unopenable_pdf` 로 갈랐다.

5) ⊘ 줄 수와 ocr-skipped 카운트가 안 맞았다 (MEDIUM)

   카운트는 래스터 실패만 세는데 OCR 엔진 실패도 화면에는 똑같이 ⊘ 로
   찍혔다. 사유를 라벨에 적어 둘을 구분한다 — 이 구분이 바로 아래 도그푸딩
   에서 실제로 값을 했다.

6) 잔가지 (LOW)

   docs 의 pdf-text-v1 잔재 3곳, doctor hint 의 줄 이음이 무너져 생긴 여백.

정답 있는 한국어 스캔으로 인식률을 쟀다 (CCITT 3건, qwen2.5vl:3b):

  namu-beulenda…   8쪽  CER 15.65%
  namu-bihaengdae  8쪽  CER 12.55%
  namu-gu-anoli    6쪽  CER 15.08%

전 페이지 OCR 성공, 건너뜀 0. 수정 전에는 세 문서 모두 본문 0 자였다.

엔진 선택이 결과를 가른다는 것도 알게 됐다. 처음에는 이 머신에 있던
gemma3:4b 로 쟀는데 래스터는 정상인데 출력이 원문과 무관한 환각이었고,
해상도가 올라가자 밀집 한국어 페이지에서 180초 타임아웃이 났다. 범용
멀티모달 모델은 OCR 엔진이 아니다 — 이때 5번의 새 라벨이 "래스터 없음"이
아니라 "OCR 엔진 실패"로 찍어 줘서 원인이 바로 갈렸다.

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

Components

책임 단위 그룹별 contributor 향 상세. 사용자 향 grand picture 는 README.md, 상위 crate 의존 그래프 + 디렉토리 구조 + locked-in 결정은 docs/ARCHITECTURE.md, 진척도는 HANDOFF.md, per-task spec 은 tasks/INDEX.md.

각 그룹 페이지는 동일 템플릿: 구성 crate / 구조 다이어그램 / data flow 다이어그램 / 주요 type / 외부 의존 / 핵심 결정 (HOTFIXES + spec 의 "왜") / 관련 spec / HOTFIXES.

그룹 wiring

12 그룹 간 호출/의존 흐름. 점선 = Foundation 이 모두에 의존. UI 는 App facade 만 통해 다른 그룹 도달.

flowchart TB
    subgraph Surfaces ["UI surface"]
        UI["UI<br/>(cli + tui)"]
    end
    subgraph Orchestration ["orchestration"]
        AppFacade["App facade<br/>(kebab-app)"]
        RAG["RAG"]
        Eval["Eval"]
    end
    subgraph IngestPipe ["ingest pipeline"]
        Source["Source"]
        Parse["Parse"]
        NormChunk["Normalize+Chunk"]
    end
    subgraph IndexQuery ["index + retrieval"]
        Embed["Embed"]
        Store["Store"]
        Search["Search"]
    end
    subgraph Generation ["generation"]
        LLM["LLM"]
    end
    Foundation["Foundation<br/>(core + parse-types + config)"]

    UI --> AppFacade
    AppFacade --> Source --> Parse --> NormChunk
    NormChunk --> Store
    NormChunk --> Embed --> Store
    AppFacade --> Embed
    AppFacade --> Search
    Search --> Store
    Search --> Embed
    AppFacade --> RAG
    RAG --> Search
    RAG --> LLM
    RAG --> Store
    AppFacade --> Eval
    Eval --> AppFacade
    Eval --> Store

    Foundation -.-> Source
    Foundation -.-> Parse
    Foundation -.-> NormChunk
    Foundation -.-> Embed
    Foundation -.-> Store
    Foundation -.-> Search
    Foundation -.-> LLM
    Foundation -.-> RAG
    Foundation -.-> AppFacade
    Foundation -.-> UI
    Foundation -.-> Eval

그룹 목록

그룹 역할 페이지
Foundation 도메인 type + 설정 + parser IR. 모든 crate 의 zero-dep 토대. foundation/
Source 워크스페이스 walk + .kebabignore + BLAKE3 checksum → RawAsset. source/
Parse bytes → ParsedBlock (md) 또는 CanonicalDocument (pdf/image). OCR + caption 어댑터. parse/
Normalize+Chunk ParsedBlockCanonicalDocument lift (markdown only) + 모든 미디어 → Vec<Chunk> (md/pdf 변종 chunker). normalize-chunk/
Store SQLite (V001-V005, FTS5, jobs, chat sessions) + LanceDB (per-model vector 테이블) two-phase write. store/
Embed Embedder trait + fastembed-rs 어댑터 (multilingual-e5-small 384d). embed/
Search lexical (FTS5 BM25) + vector (ANN) + hybrid (RRF) — Retriever trait 3 변종. search/
LLM LanguageModel trait + Ollama HTTP 어댑터 (gemma4:e4b default). streaming + cancel-safe. llm/
RAG retrieve → gate → pack → generate → cite-validate → persist 9 stage pipeline. multi-turn 지원. rag/
App facade kebab-app — 모든 UI binary 의 유일한 진입점. *_with_config companion 패턴. app-facade/
UI kebab-cli (--json wire envelope) + kebab-tui (4 패널 + Mode machine + cheatsheet). ui/
Eval golden query 회귀 평가 + run-vs-run compare. must_contain rule-based. eval/

진입 가이드

처음 읽는다면 (의존성 따라 bottom-up):

  1. Foundation — 다른 모든 페이지가 참조하는 type 정의. AssetId / DocumentId / Chunk / Citation / 5 version 등.
  2. Source → Parse → Normalize+Chunk → Store — ingest pipeline 흐름.
  3. Embed → Search — retrieval.
  4. LLM → RAG — generation.
  5. App facade — 위 전부 wiring.
  6. UI — facade 위.
  7. Eval — 독립.

특정 작업 별 진입:

  • 새 미디어 타입 추가 (예: epub) — Parse → Normalize+Chunk → Store (chunker_version) → App facade (라우팅).
  • 새 retrieval 모드 — Search → App facade (mode dispatch) → UI (--mode flag).
  • 새 LLM 어댑터kebab-coreLanguageModel trait 구현 + 새 kebab-llm-<provider> crate → App facade (config provider switch).
  • TUI 신규 pane — UI 만. Mode + Theme + InputBuffer 재사용.

다이어그램 제약

각 그룹 페이지의 다이어그램은 mermaid (Gitea / GitHub 자동 렌더). 페이지 별 최소 2개 — 구조 (type/trait/struct 관계) + data flow (입출력 흐름). 실제 코드와 시그니처 일치 — 작성 시 crates/kebab-<name>/src/lib.rs 직접 읽음 (추측 금지).