# task-2900 보고서 — 소식지 검토 Phase 3b-1: 교차모델(GPT) 2차 검증 파이프라인 추가

- 팀: dev1-team (헤르메스) / 팀원: 불칸(백엔드), 아르고스(테스터)
- base: origin/main `a914594` (Phase 3a 머지본)
- 브랜치: `task/task-2900-dev1` / worktree: `/home/jay/projects/InsuRo/.worktrees/task-2900-dev1`
- 레벨: Lv.3 / merge_policy: **none** (머지·배포는 ANU 소관)

## S (Situation)
현 소식지 검토 파이프라인 T3b는 **같은 모델(Claude)이 자기 출력을 자기검증** → 자기 환각을 못 잡는 상관오류 한계. 회장 260805 승인 = **다른 엔진(GPT/OpenAI)으로 2차 교차검증** 추가. 서로 다른 아키텍처가 같은 숫자·보험사명을 똑같이 오독할 확률은 낮으므로, 두 모델 불일치 = 고신뢰 의심신호를 사람에게 제시.

## C (Complication)
- 자기검증만으로는 상관오류(같은 맹점) 방어 불가.
- 교차검증기(외부 GPT)는 지연·불안정·키 부재 가능 → 파이프라인을 절대 중단시키면 안 됨.
- AI/GPT는 발행 승인권 0 — 숫자·보험사명·날짜 자동수정 절대 금지, **플래그만**.
- Phase 3b-2(UI)와 플래그 스키마 계약 고정 필요.

## Q (Question)
server/ 만 수정하여, 결정론9규칙+T3b 자기검증 직후 GPT 교차검증 패스를 best-effort로 추가하고, 불일치를 기존 validation_flags에 **병합(append)**하되 기존 구조·라우팅을 재구현 없이 재사용할 수 있는가?

## A (Answer)
가능. 순수 additive 2파일 + 신규 테스트 1파일.

### 생성/수정 파일
| 파일 | 구분 | 라인 |
|---|---|---|
| `server/newsletter_cross_verify.py` | 신규 | 196 |
| `server/newsletter_review.py` | 배선만(+20) | 477→497 |
| `server/tests/test_newsletter_cross_verify.py` | 신규 테스트 | 337 (14 케이스) |

### 구현 요약
1. **newsletter_cross_verify.py** — `async def cross_verify(source_text, extracted, model_version) -> list[dict]`
   - OpenAI Chat Completions 직접 호출(httpx AsyncClient, timeout 30s, `OPENAI_API_KEY` env, 모델 `gpt-4o`). **codex CLI subprocess 미사용.**
   - **확증편향 방지**: user 프롬프트에 **원문(source_text)만** 전달. 1차 추출값(extracted)은 GPT에 절대 노출하지 않음 → GPT가 독립 재판독 후 `{"insurer":{"value","quote"},...}` 반환.
   - 비교: `_normalize`(strip+공백축약) 후 불일치 시만 플래그. insurer 불일치=HIGH, title/body는 MED이나 숫자 토큰(`\d+`) 불일치 시 HIGH 승격.
   - **extractive-only**: 어떤 필드값도 수정/생성 안 함. 일치 시 빈 리스트.
   - **graceful degradation**: 키 부재 / 타임아웃 / HTTPStatusError / HTTPError / JSON 파싱 실패 / 최종 except → `cross_model_unavailable` INFO 1건만 반환, 예외 밖으로 안 던짐.
   - 네트워크 호출부 `_call_openai` 분리 → 결정론 테스트용 monkeypatch 가능.
2. **newsletter_review.py 배선** — `run_review_pipeline`에 `do_cross_verify: bool=True`, `cross_verify_fn=None` 주입 파라미터 추가. step2(AI 자기검증) 직후·step3(grounding) 앞에 step2.5 삽입. `flags = flags + list(cross_flags or [])` **append만**, 기존 flags 순서/구조 불변. 배선부 자체도 try/except 이중 방어. HIGH 불일치 시 기존 advance_to_pending_review 라우팅이 그대로 pending_review 처리(별도 라우팅 재구현 0).
   - `transition_review_status`/`ALLOWED_TRANSITIONS`/예외계층(1~229행)·`*_extracted` UPDATE 불변.

### 플래그 스키마 (Phase 3b-2 UI 계약 고정)
- 불일치: `{"rule":"cross_model_disagree","level":"HIGH|MED","field":"insurer|title|body","message":"교차검증(GPT)과 1차 추출 불일치","detail":{"primary_value","verifier_value","source_quote","verifier":"gpt-4o"}}`
- 미가용: `{"rule":"cross_model_unavailable","level":"INFO","field":"_meta","message":"교차검증 미완(검증기 미가용)","detail":{"reason":"..."}}`

## 테스트 결과
- 신규 `test_newsletter_cross_verify.py`: **14 passed** (불일치/레벨판정 3 · 일치/정규화 2 · graceful 5 · extractive-only 불변 2 · 파이프라인 병합 2).
- 소식지 관련 기존: `test_newsletter_review.py`+`test_newsletter_validation.py` **260 passed** (회귀 0).
- 전체 회귀: **1198 passed, 1 failed**.
  - 유일 실패 = `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset`. **변경 전 베이스라인에서도 동일 실패** (작업 시작 시 clean base에서 `1 failed, 1168 passed`로 사전 측정). 원인 = worktree .env 오염(EXT_ORIGIN 미설정) — 본 변경(newsletter_cross_verify.py + newsletter_review.py)과 무관. 메모리에 회귀 아님으로 독립확정된 기존 이슈.

## L1 스모크테스트 결과 (실제 프로세스 실행)
- 서버 재시작: **해당없음** (프로덕션 서버는 ANU 배포 소관, merge_policy=none). 대신 실제 파이썬 프로세스로 함수 end-to-end 실행하여 실동작 관찰.
- API 응답 확인: **해당없음** (OpenAI 실호출은 worktree .env에 `OPENAI_API_KEY` 부재 → 실왕복 불가. 단, 이 미가용 경로가 곧 프로덕션 초기 경로이므로 실측 가치 높음).
- 스크린샷: 해당없음 (백엔드 순수 로직).
- **L1-A** (실프로세스, 키 없음): `cross_verify` → `cross_model_unavailable` INFO 정확히 1건, 예외 0. 프로덕션이 키 프로비저닝 전까지 맞닥뜨릴 경로 실증.
- **L1-B** (실프로세스, `_call_openai`만 스텁·비교로직 실제): 보험사 불일치=HIGH, 본문 숫자 3000↔5000 불일치=HIGH 승격, 제목 일치=플래그 없음 → 실제 비교/승격 로직 정상.
- **L1-C** (실프로세스, `run_review_pipeline` 배선): 기존 플래그(insurer_not_whitelisted, truncated, insurer_ungrounded) 전부 보존 + cross_model_disagree append 병합, HIGH 불일치→기존 라우팅으로 `pending_review` 전이, UPDATE validation_flags에 cross 플래그 반영 확인.

## 발견 이슈 및 해결
- Pyright: server/ 모듈의 `import newsletter_*` 미해결 경고 → 기존 sys.path 관례(newsletter_validation도 동일 표시), 런타임 정상(import OK). 실제 문제 아님.
- `cross_verify`의 `model_version` 인자 미사용 경고 → 의도적(1차 추출 모델 버전, 향후 확장 대비 시그니처 고정). docstring에 명시. 무해.
- 프로덕션 버그 발견: 없음.

## 머지 판단
- **머지 필요**: Yes — 단, **머지·배포는 ANU 소관** (task merge_policy=none, 순서 B). 팀은 push + PR open + .done 까지.
- **브랜치**: `task/task-2900-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2900-dev1`
- **머지 의견**: 순수 additive 2파일 + 테스트. 기존 상태기계·검증규칙·*_extracted 불변, 회귀 0(CORS 1건=기존 .env 이슈 독립확정). ANU 독립검증(clean /tmp worktree) 권장. 머지 후 후속 = `OPENAI_API_KEY` 서버 env 프로비저닝(없어도 graceful 동작) + 서버 재시작. Phase 3b-2(검토 UI, src/ 만) 별도 위임 대기.

## 모델 사용 기록
- 불칸(백엔드): sonnet — cross_verify 모듈+배선 구현.
- 아르고스(테스터): sonnet — 결정론 테스트 14케이스.
- 팀장(헤르메스): opus — 설계/검토/통합/L1 실측. haiku 미사용.

## 커밋
- `641b1f8` 불칸: cross_verify 모듈 + 배선
- `3b31f02` 아르고스: 결정론 테스트 14케이스

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


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

