feat(app): OCR/caption derivation 캐시 — 비전 산출물을 버전-캐스케이드에서 분리 #217

Merged
altair823 merged 9 commits from feat/ocr-caption-cache into main 2026-06-25 01:47:32 +00:00
Owner

요약

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 없음). 이미지/페이지 바이트를 키로.
  • OCR/caption payload (kebab-app): OcrText/ModelCaption 전체 구조체 serde 인코딩 — 문자열-stub 대신 full struct 라 block 레벨까지 byte-identical.
  • b1 이미지 OCR / b2 이미지 caption: ingest_one_image_assetapply_ocr/apply_caption 호출을 캐시 래핑. 히트=block 설정+엔진 스킵(provenance 미재현 §3.6), 미스=실행 후 성공 시만 캐시. caption 은 enabled 토글 존중.
  • b3 PDF 페이지 OCR: PdfOcrOpts 에 캐시 핸들(Option<Arc<SqliteStore>>)+버전키 추가, per-page engine.recognize 래핑. parse-crate 의존 무변경(캐시는 kebab-app 내부에만). hit/miss 가 page-mutation 단일 출처 공유.
  • version key: OCR=엔진 name/version+score_thresh/unclip_ratio/max_boxes(R3 — 출력 형성 파라미터 선폴딩), caption=provider+prompt_template_version. 캐스케이드 변경 시 키 변경→미스.

검증

  • mock-engine 결정적 게이트(CI 레인, no model): apply_ocr_to_pdf_pages 2회 호출 → 엔진 recognize 1회만(히트=미호출). deliberate-break 시 실제 fail 입증.
  • paddle 실엔진 이미지 게이트(#[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 입증.
  • byte-identical: full-struct serde 무손실, hit=fresh 동일, provenance 미재현(wire/게이트에 없음), 네임스페이스 분리. clippy --workspace --all-targets 0.

비범위 / 후속

  • markdown 패리티 게이트: PR2 가 markdown 경로 무수정이라 N/A.
  • GPU ollama-vision 도그푸딩(§4.4)·paddle 게이트 코퍼스(§4.2 R2): release/정리 단계에서 GPU 박스 스왑과 함께 수행(lemonade 무조건 복구). 코드 diff 외 도그푸딩 의무.
  • 버전 bump: 새 CLI/wire/migration 없고 출력 불변이라 patch — release 단계 처리.

시험 항목 (Test Plan)

  • clippy --workspace --all-targets 0
  • cargo test -p kebab-app --test ocr_caption_cache (mock 게이트 green)
  • cargo test -p kebab-app --test paddle_image_cache -- --ignored (실엔진 b1, created_at 판별자)
  • derivation payload round-trip + derivation_cache_key_bytes 단위테스트

Assisted-by: Claude Code

## 요약 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 없음). 이미지/페이지 바이트를 키로. - **OCR/caption payload** (kebab-app): `OcrText`/`ModelCaption` 전체 구조체 serde 인코딩 — 문자열-stub 대신 full struct 라 block 레벨까지 byte-identical. - **b1 이미지 OCR** / **b2 이미지 caption**: `ingest_one_image_asset` 의 `apply_ocr`/`apply_caption` 호출을 캐시 래핑. 히트=block 설정+엔진 스킵(provenance 미재현 §3.6), 미스=실행 후 성공 시만 캐시. caption 은 enabled 토글 존중. - **b3 PDF 페이지 OCR**: `PdfOcrOpts` 에 캐시 핸들(`Option<Arc<SqliteStore>>`)+버전키 추가, per-page `engine.recognize` 래핑. **parse-crate 의존 무변경**(캐시는 kebab-app 내부에만). hit/miss 가 page-mutation 단일 출처 공유. - **version key**: OCR=엔진 name/version+score_thresh/unclip_ratio/max_boxes(R3 — 출력 형성 파라미터 선폴딩), caption=provider+prompt_template_version. 캐스케이드 변경 시 키 변경→미스. ## 검증 - **mock-engine 결정적 게이트**(CI 레인, no model): `apply_ocr_to_pdf_pages` 2회 호출 → 엔진 `recognize` 1회만(히트=미호출). deliberate-break 시 실제 fail 입증. - **paddle 실엔진 이미지 게이트**(`#[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 입증. - byte-identical: full-struct serde 무손실, hit=fresh 동일, provenance 미재현(wire/게이트에 없음), 네임스페이스 분리. clippy `--workspace --all-targets` 0. ## 비범위 / 후속 - markdown 패리티 게이트: PR2 가 markdown 경로 무수정이라 N/A. - **GPU ollama-vision 도그푸딩**(§4.4)·paddle 게이트 코퍼스(§4.2 R2): release/정리 단계에서 GPU 박스 스왑과 함께 수행(lemonade 무조건 복구). 코드 diff 외 도그푸딩 의무. - 버전 bump: 새 CLI/wire/migration 없고 출력 불변이라 **patch** — release 단계 처리. ## 시험 항목 (Test Plan) - [ ] clippy --workspace --all-targets 0 - [ ] cargo test -p kebab-app --test ocr_caption_cache (mock 게이트 green) - [ ] cargo test -p kebab-app --test paddle_image_cache -- --ignored (실엔진 b1, created_at 판별자) - [ ] derivation payload round-trip + derivation_cache_key_bytes 단위테스트 Assisted-by: Claude Code
altair823 added 8 commits 2026-06-25 01:32:56 +00:00
claude-reviewer-01 requested changes 2026-06-25 01:33:54 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — subagent-driven 개발로 task 리뷰(b1/b3 full + b2 self) + whole-branch 최종 리뷰(READY TO MERGE)를 거쳤고, 그 검증 사실 + 잔여 actionable nit 1건. REQUEST_CHANGES(nit 반영 후 APPROVE).

검증 완료 (재확인)

  • byte-identical (hard gate): full-struct serde(OcrText 4필드 / ModelCaption 3필드) 무손실 round-trip → 캐시 히트가 block.ocr/block.caption/페이지 텍스트를 fresh 와 동일 재구성. cold-cache 첫 인덱싱 무변경. b1/b2 는 히트 시 엔진 호출 자체 스킵, b3 는 hit/miss 가 page-mutation 단일 출처 공유(provenance push 만 !was_cache_hit 게이트).
  • provenance 미재현(§3.6): 캐시 히트 시 OcrApplied/CaptionApplied 이벤트 push 안 함 — wire 스키마/게이트에 없어 byte-identical 무영향.
  • version-key 캐스케이드 안전: 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 키가 kind 0x00 폴딩으로 disjoint.
  • 테스트 유효성(둘 다 실제 SQL 대비 재검증): mock 게이트는 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 입증.
  • parse-crate 경계: PDF 캐시는 kebab-app 의 pdf_ocr_apply.rs 에만(parse 크레이트 store 의존 0). PdfOcrOpts 리터럴 6+1 갱신(None=오늘 동작).
  • clippy --workspace --all-targets 0, 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 방어 코드 — 올바르므로 유지.
회차 1 — subagent-driven 개발로 task 리뷰(b1/b3 full + b2 self) + whole-branch 최종 리뷰(READY TO MERGE)를 거쳤고, 그 검증 사실 + 잔여 actionable nit 1건. **REQUEST_CHANGES**(nit 반영 후 APPROVE). ## 검증 완료 (재확인) - **byte-identical (hard gate)**: full-struct serde(`OcrText` 4필드 / `ModelCaption` 3필드) 무손실 round-trip → 캐시 히트가 `block.ocr`/`block.caption`/페이지 텍스트를 fresh 와 동일 재구성. cold-cache 첫 인덱싱 무변경. b1/b2 는 히트 시 엔진 호출 자체 스킵, b3 는 hit/miss 가 page-mutation 단일 출처 공유(provenance push 만 `!was_cache_hit` 게이트). - **provenance 미재현(§3.6)**: 캐시 히트 시 OcrApplied/CaptionApplied 이벤트 push 안 함 — wire 스키마/게이트에 없어 byte-identical 무영향. - **version-key 캐스케이드 안전**: `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` 키가 `kind` 0x00 폴딩으로 disjoint. - **테스트 유효성(둘 다 실제 SQL 대비 재검증)**: mock 게이트는 `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 입증. - **parse-crate 경계**: PDF 캐시는 kebab-app 의 pdf_ocr_apply.rs 에만(parse 크레이트 store 의존 0). PdfOcrOpts 리터럴 6+1 갱신(`None`=오늘 동작). - clippy `--workspace --all-targets` 0, 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 방어 코드 — 올바르므로 유지.
altair823 added 1 commit 2026-06-25 01:34:32 +00:00
claude-reviewer-01 approved these changes 2026-06-25 01:34:45 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 actionable nit 반영 확인. APPROVE.

  • b3 PDF per-page 캐시 GET 에러에 tracing::debug! 추가(inspect_err, 141dfbf): swallow 된 GET 에러를 노출 → b1/b2(? propagate)와 관측성 대칭. degrade-to-MISS 동작은 불변(출력 무영향). clippy --all-targets 0.

머지 차단 0건. byte-identical(full-struct serde, hit=fresh 동일, provenance 미재현) + 두 테스트 유효성(call_count row-독립 + created_at 불변 판별자, 실제 put/touch SQL 대비 검증, break→fail 입증) + parse 경계 모두 확인. APPROVE.

회차 2 — 회차 1 actionable nit 반영 확인. **APPROVE.** - b3 PDF per-page 캐시 GET 에러에 `tracing::debug!` 추가(`inspect_err`, `141dfbf`): swallow 된 GET 에러를 노출 → b1/b2(`?` propagate)와 관측성 대칭. degrade-to-MISS 동작은 불변(출력 무영향). clippy `--all-targets` 0. 머지 차단 0건. byte-identical(full-struct serde, hit=fresh 동일, provenance 미재현) + 두 테스트 유효성(call_count row-독립 + created_at 불변 판별자, 실제 put/touch SQL 대비 검증, break→fail 입증) + parse 경계 모두 확인. APPROVE.
altair823 merged commit cfffbf60ad into main 2026-06-25 01:47:32 +00:00
altair823 deleted branch feat/ocr-caption-cache 2026-06-25 01:47:33 +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#217