feat(app): OCR/caption derivation 캐시 — 비전 산출물을 버전-캐스케이드에서 분리 #217
Reference in New Issue
Block a user
Delete Branch "feat/ocr-caption-cache"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
OCR·caption 중간 산출물을 derivation cache 에 캐싱한다(PR1 의 임베딩 캐시 전면화에 이어 (b)). 이미지 OCR·이미지 caption·PDF 페이지 OCR 결과를 소스 바이트 + OCR/caption 버전키로 캐싱 — 재인덱싱이나 chunker/embedding 버전 bump 시 비싼 비전 엔진(paddle ONNX / ollama-vision)을 재실행하지 않고 캐시된 텍스트를 재사용한다. 이로써 버전-캐스케이드에서 비전 작업을 분리(chunker bump 가 더는 OCR 을 재실행 안 함)하고, ollama-vision 의 잔여 비결정성을 첫 결과로 박제(재현성↑)한다. 출력은 byte-identical — 캐시 히트가
block.ocr/block.caption/페이지 텍스트를 전체 구조체 serde 로 동일 재구성.설계: docs/superpowers/specs/2026-06-24-cache-everywhere-ocr-caption-design.md §3
계획: docs/superpowers/plans/2026-06-25-pr2-ocr-caption-cache.md
변경
derivation_cache_key_bytes(kebab-core): 바이트 입력용 키 변형(NFC 없음). 이미지/페이지 바이트를 키로.OcrText/ModelCaption전체 구조체 serde 인코딩 — 문자열-stub 대신 full struct 라 block 레벨까지 byte-identical.ingest_one_image_asset의apply_ocr/apply_caption호출을 캐시 래핑. 히트=block 설정+엔진 스킵(provenance 미재현 §3.6), 미스=실행 후 성공 시만 캐시. caption 은 enabled 토글 존중.PdfOcrOpts에 캐시 핸들(Option<Arc<SqliteStore>>)+버전키 추가, per-pageengine.recognize래핑. parse-crate 의존 무변경(캐시는 kebab-app 내부에만). hit/miss 가 page-mutation 단일 출처 공유.검증
apply_ocr_to_pdf_pages2회 호출 → 엔진recognize1회만(히트=미호출). deliberate-break 시 실제 fail 입증.#[ignore], AVX): b1 을 실엔진 end-to-end 검증.created_at불변 판별자 — row-count 단독은 INSERT-OR-REPLACE 로 false-green(미스 재-OCR 도 같은 키 덮어써 카운트 유지)이라, HIT(touch-only→created_at 고정) vs MISS(re-put→새 created_at)로 판별. break→fail 입증.--workspace --all-targets0.비범위 / 후속
시험 항목 (Test Plan)
Assisted-by: Claude Code
회차 1 — subagent-driven 개발로 task 리뷰(b1/b3 full + b2 self) + whole-branch 최종 리뷰(READY TO MERGE)를 거쳤고, 그 검증 사실 + 잔여 actionable nit 1건. REQUEST_CHANGES(nit 반영 후 APPROVE).
검증 완료 (재확인)
OcrText4필드 /ModelCaption3필드) 무손실 round-trip → 캐시 히트가block.ocr/block.caption/페이지 텍스트를 fresh 와 동일 재구성. cold-cache 첫 인덱싱 무변경. b1/b2 는 히트 시 엔진 호출 자체 스킵, b3 는 hit/miss 가 page-mutation 단일 출처 공유(provenance push 만!was_cache_hit게이트).score_thresh/unclip_ratio/max_boxes가 출력 형성하지만 engine_version(blake3 det‖rec‖dict)엔 미포함 → version key 에 선폴딩(R3). caption=provider+prompt. 캐스케이드 변경→키 변경→미스. image/pdf 가ocr_cache_version_key공유.ocr/caption/embedding키가kind0x00 폴딩으로 disjoint.call_count(row-독립, INSERT-OR-REPLACE 면역) — 히트=엔진 미호출. paddle 게이트는created_at불변 판별자 — row-count 단독은 INSERT-OR-REPLACE 로 false-green(미스 재-OCR 도 같은 키 덮어써 카운트 유지)이라, put=created_at 갱신 / touch=last_used_at 만 → 히트는 created_at 고정으로 판별. 양쪽 deliberate-break→fail 입증.None=오늘 동작).--workspace --all-targets0, mock 게이트 green.Actionable (nit 1건, 반영 요청)
[nit] b3 PDF per-page 캐시 GET 에러가 silent swallow — b1/b2 와 비대칭.
crates/kebab-app/src/pdf_ocr_apply.rs(per-page cache_hit 블록). PDF 경로는.derivation_cache_get(key).ok().flatten()로 GET 에러를 MISS 로 degrade(per-page 회복력 — 의도적·문서화됨). b1/b2 는?로 propagate. 정합성은 OK 이나, mid-PDF 에 SQLite 손상 read 가 신호 없이 재-OCR 됨.tracing::debug!한 줄로 swallow 된 GET 에러를 노출하면 관측성↑. (출력 무영향, 비차단이나 cleanup 가치.)비-actionable (유지)
else if let Some(produced) = &block.ocr가드: 성공 후 block.ocr 은 항상 Some 이라 현재 unreachable 이지만, None 캐싱 방지의 future-proof 방어 코드 — 올바르므로 유지.회차 2 — 회차 1 actionable nit 반영 확인. APPROVE.
tracing::debug!추가(inspect_err,141dfbf): swallow 된 GET 에러를 노출 → b1/b2(?propagate)와 관측성 대칭. degrade-to-MISS 동작은 불변(출력 무영향). clippy--all-targets0.머지 차단 0건. byte-identical(full-struct serde, hit=fresh 동일, provenance 미재현) + 두 테스트 유효성(call_count row-독립 + created_at 불변 판별자, 실제 put/touch SQL 대비 검증, break→fail 입증) + parse 경계 모두 확인. APPROVE.