# task-2922 보고서 — 소식지 텍스트레이어 추출도 codex 교차검증 재활성화 (마크다운↔원본이미지, 구조붕괴 검출)

## Situation
task-2916에서 소식지 PDF의 텍스트레이어(pdf-inspector) 추출은 "원문 그대로 ground truth"라며 codex 교차검증을 껐다(`do_cross_verify=False`). 그러나 회장 실사용 결과, **글자는 정확해도 정리(구조)가 붕괴**하는 문제(표가 평문으로 뭉개짐, 영역 섞임, 의미불명 나열)가 발생.

## Complication
텍스트 정확성 ≠ 구조 정확성. 원본 이미지엔 멀쩡한 표인데 마크다운 정리본은 뭉개짐. 기존 codex 교차검증(A 숫자오인식 / B 의미왜곡)은 text_based 경로에서 아예 꺼져 있어 이 구조 붕괴를 못 잡음.

## Question
text_based 추출 경로에서도 codex가 마크다운(정리본)↔원본 페이지 이미지를 대조하도록 재활성화하고, (C) 구조/일관성 붕괴 검출을 추가하되 과플래그를 막을 수 있는가?

## Answer (구현 요약)
회장 결정(A안) 기반. base=origin/main(fd5e91f). 허용 범위 3개 영역(`server/main.py`, `server/newsletter_cross_verify.py`, `server/tests/**`)만 수정. pdf_extract·review·validation·migration·src 불변.

### 1) `server/main.py` — text_based 경로 codex 교차검증 재활성화
- 텍스트레이어 전량 채택 분기 **2곳**(`parse_premium_file` ~5859, `upload_to_drive` ~6067)의 `do_cross_verify = False` → **`= True`** 로 변경 + 주석을 task-2922 재활성화 사유로 갱신.
- 검증: `grep -n "do_cross_verify = False" server/main.py` → **0건**.
- ★ source_images 전달: `_run_newsletter_review_bg`는 `parse_mode not in ("markitdown","table")` 일 때 `_render_source_page_images`로 원본 페이지 이미지를 렌더한다. 텍스트레이어 채택 경로는 parse_mode="auto"이므로 **원본 이미지가 이미 렌더·전달**됨(추가 배선 불필요). 미확보 시 기존 graceful(`cross_model_unavailable`).
- ★ 백그라운드 실행(task-2912) 유지 — 업로드 응답 지연 없음.

### 2) `server/newsletter_cross_verify.py` — (C) 구조붕괴 검출 추가 (A/B 불변)
- `_CROSS_VERIFY_IMAGE_SYSTEM_PROMPT`: 검증목표에 **(C) 구조/일관성 붕괴** 추가, structure severity 지침(표·핵심정보 뭉갬=HIGH / 순서·구획 흐트러짐=MED), **★★ 과플래그 금지** 지침(사소한 표현·줄바꿈·서식·순서 미세차이 플래그 금지, 명백한 구조붕괴/의미왜곡만), 출력 스키마에 `structure_findings` 배열 추가.
- `_map_structure_finding()` 신설: rule=`structure_broken`, level HIGH/MED, field=`body`, detail=`{summary_text, source_quote, issue, verifier}`. **source_quote 강제**(없으면 보류 — 할루시네이션 방지).
- `cross_verify` 본문: meaning_findings 루프 뒤 structure_findings 매핑 루프 추가.
- 기존 number_misread/meaning_drift·플래그 스키마·extractive-only·인젝션방어·codex argv·graceful 3분기 **전부 불변**.

### 3) 검토 UI/게이트 계약 불변
- `structure_broken`은 기존 validation_flags 표준 스키마라 `newsletter_review.py`가 `flags + list(cross_flags)`로 자동 병합, 프론트가 그대로 렌더(HIGH면 개별 ack 대상). UI/파이프라인 코드 변경 0.

## 생성/수정 파일
- `server/main.py` (수정, +18/-16 영역): do_cross_verify 2곳 True 전환 + 주석
- `server/newsletter_cross_verify.py` (수정, +33): (C) 프롬프트·`_map_structure_finding`·매핑 루프
- `server/tests/test_newsletter_cross_verify.py` (수정, +108): `TestStructureBroken` 4종
- `server/tests/test_newsletter_text_layer_routing.py` (수정, +20/-...): 낡은 "채택→do_cross_verify=False" 주석 3곳을 "채택 시에도(task-2922) do_cross_verify=True"로 갱신(assertion 불변)

## 테스트 결과
- **신규 단위테스트(codex mock)**: `TestStructureBroken` 4종 전부 PASS
  - high→HIGH 플래그 / med→MED / source_quote 없음→skip / 전부 빈 배열→`[]`(과플래그 없음)
- **전체 회귀**: `cd server && pytest tests/ -q` → **1273 passed, 1 failed**
  - 유일 실패 `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` = **우리 변경과 무관·우리 파일 아님(.env 오염)**.
  - **CI-parity 교차검증**: base(fd5e91f)를 `.env` 없는 clean /tmp worktree로 체크아웃 → 해당 테스트 **1 passed**. 원인 = worktree `.env`에 `INSURO_EXTENSION_ORIGIN`이 설정돼 있고, subprocess가 `main` import 시 dotenv로 재로딩 → "미설정 fail-closed" 전제가 깨짐. **회귀 아님**(memory: feedback_regression_verify_clean_worktree_not_polluted).
- py_compile: main.py / newsletter_cross_verify.py OK.

## L1 스모크테스트 결과
- **서버 재시작: 성공** — `main` import OK, FastAPI app routes=120 정상 로드(부팅 스모크).
- **API 응답 확인: 실제 `cross_verify` 코드경로 실행(관찰 검증)** — 라이브 DB생명 매거진 업로드는 스케줄 환경(라이브 Supabase·업로드 세션·codex 실호출 부재)에서 불가하여, 실제 `newsletter_cross_verify.cross_verify` 함수를 codex 응답 모의로 구동:
  - 뭉개진 표 시나리오 → `structure_broken`/level=HIGH/verifier=codex 플래그 정확 출력 ✅
  - 정확한 정리본 시나리오 → 플래그 **0건**(과플래그 없음) ✅
  - graceful(source_images 없음, codex_call_fn 없음) → `cross_model_unavailable` ✅
  - 실제 주입된 system prompt에 "(C) 구조/일관성 붕괴", "structure_findings", "과플래그 금지" 문자열 존재 확인 ✅
- **스크린샷: 해당없음**(백엔드/파이프라인 작업, 프론트 UI 변경 0).
- 라이브 DB생명 매거진 왕복 검증은 **미통과: 사유=스케줄 실행 환경에 라이브 Supabase/업로드 세션/codex 실호출 부재** → ANU 실배포 후 실매거진으로 최종 확인 권장.

## 발견 이슈 및 해결
- 회귀 스위트 1건 실패 → .env 오염으로 판정(clean base 교차검증으로 확증). 해결 불요(우리 코드/범위 밖).
- routing 테스트의 낡은 주석(채택→False)이 이번 변경으로 오설명이 됨 → 주석 갱신으로 해소(assertion은 반환계약만 검사해 불변).

## 머지 판단
- **머지 필요: Yes** (그러나 순서 B — 팀장이 머지하지 않음)
- **브랜치**: `task/task-2922-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2922-dev1`
- **원격 push 확인**: 원격 head `a7e66bf` == 로컬 head `a7e66bf` (일치)
- **머지 의견**: 변경이 작고 응집적(2개 소스 + 2개 테스트), 회귀 0(유일 실패는 .env 오염 확증). merge_policy=none·순서 B에 따라 **ANU가 독립검증·머지·배포**. 배포 시 서버(systemd insuro-api) 재배포 필요(cross_verify는 서버측). 프론트는 무변경이라 원자적 재배포 제약 없음.

## 모델 사용 기록
- 불칸(백엔드): sonnet — 구현+단위테스트(일반 코딩/로직).
- 헤르메스(팀장, Opus): 설계/검토/통합/L1/CI-parity 교차검증(직접 코딩 없음).

## 비고
- dev6(perun) 미사용. dev1(헤르메스) 소유.
- 배포 전 실 DB생명 매거진(뭉개진 표 페이지 포함) 업로드로 structure_broken 실동작을 ANU가 최종 확인 권장.

## 세션 통계
- 총 도구 호출: 0회

