# task-2902 — 소식지 검토 GPT 교차모델 더블체크 백엔드 (v2)

- 팀: dev1-team (헤르메스) · 담당 구현: 불칸(백엔드, sonnet)
- 프로젝트: InsuRo · 브랜치: `task/task-2902-dev1`
- 워크트리: `/home/jay/projects/InsuRo/.worktrees/task-2902-dev1`
- base: `4cf8dd5`(origin/main, task-2901 UI 머지 후) — server/ 무충돌

## Situation
소식지 1차 추출(Claude Opus/Sonnet)이 사람 검토 없이 곧바로 발행되던 구조를 막기 위한 검토 workflow 중, 자동검증 단계가 결정론 9규칙(newsletter_validation) + AI 자기검증(같은 계열 모델)까지만 있었다. 같은 계열 모델의 자기검증은 상관오류·자기보증 위험이 있다.

## Complication
회장 260805 초점 교정(verbatim): 보험사명은 파일명, 기준월/개정일은 사용자 직접 지정 → 이미 신뢰 가능하므로 **교차검증 불필요**. 정말 검증할 것은 1차 추출 모델이 (A) **숫자를 잘못 인식/전사**(자릿수·단위·부착항목 오류), (B) **본문을 다른 의미로 왜곡** 정리한 두 가지. 이를 **다른 엔진(GPT)**이 원문 대조로 더블체크해야 한다. 보험사명·날짜 교차검증은 초점 분산이므로 금지.

## Question
server/ 만 수정(src·main·migration·validation·.github 불변)하면서, extractive-only·플래그only·graceful를 지키고 기존 파이프라인 회귀 0으로 GPT 교차검증을 배선할 수 있는가?

## Answer
가능. 두 파일 + 테스트로 구현, clean-worktree(CI parity) 회귀 0 확인.

### 생성/수정 파일
| 파일 | 종류 | 라인 |
|---|---|---|
| `server/newsletter_cross_verify.py` | 신규 | 251 |
| `server/newsletter_review.py` | 수정(배선만 +12줄) | 490 |
| `server/tests/test_newsletter_cross_verify.py` | 신규 | 456 |

### 핵심 설계
- **`async def cross_verify(source_text, extracted_body, model_version, *, openai_call_fn=None, api_key=None, model="gpt-4o", timeout=30.0) -> list[dict]`**
  - insurer/title/date 파라미터 **없음**(초점=body와 그 안의 숫자만).
  - **httpx async로 OpenAI Chat Completions 직접 호출**(`response_format=json_object`). codex CLI subprocess 미사용.
  - **확증편향 방지**: system prompt가 GPT에게 "정리본은 정답이 아니라 검사 대상 — 원문에서 먼저 숫자·사실을 독립 추출한 뒤 대조"하도록 강제. `model_version`(1차 추출 모델명)을 프롬프트에 회의 컨텍스트로 주입.
  - **인용 span 강제**: finding의 `source_quote`가 비면 그 플래그 **보류(생성 안 함)** — 할루시네이션 방지.
  - **extractive-only**: 어떤 값도 수정/생성 안 함(플래그만).
  - **graceful**: 키 부재(네트워크 호출 0 즉시 반환)/타임아웃(30s)/호출예외/JSON 파싱실패 → `cross_model_unavailable` INFO 1건만, 예외 절대 전파 안 함.
- **배선(`run_review_pipeline`)**: step2(AI 자기검증) 직후·step3(grounding) 전에 삽입(→ 플래그가 step4 UPDATE의 `validation_flags`에 병합). `do_cross_verify: bool=True`, `cross_verify_fn=None`(테스트 주입) 추가. cross_verify 실패해도 `logger.exception` 후 파이프라인·상태전이 정상(pending_review 도달). 기존 `transition_review_status`/`ALLOWED_TRANSITIONS`/예외계층/`advance_to_pending_review` **일절 미수정**.

### 플래그 스키마 (UI task-2901과 계약 고정)
- `number_misread` (level HIGH, field body, detail: summary_value/source_quote/issue/verifier)
- `meaning_drift` (level HIGH=의미뒤바뀜·없는주장 / MED=뉘앙스, detail: summary_text/source_quote/issue/verifier)
- `cross_model_unavailable` (level INFO, field _meta, detail.reason)

## 테스트 결과
- **신규** `test_newsletter_cross_verify.py`: **16 passed, 0 failed** (숫자자릿수/단위오류→number_misread HIGH / 원문없는주장→meaning_drift HIGH / 뉘앙스→MED / 정확정리→[] / 키없음·예외·파싱실패→cross_model_unavailable / 인용없음→보류 / insurer·date 플래그 미생성 / 배선 병합 / body 원본 불변 / graceful).
- **회귀** `test_newsletter_validation.py` + `test_newsletter_review.py`: **260 passed, 0 failed**.
- **★ canonical(clean /tmp worktree, CI parity, .env 오염 배제)**: newsletter 3파일 **276 passed, 0 failed** (0.65s, hang 없음).

## L1 스모크테스트 결과 (필수)
- **서버 재시작**: 불필요 (순수 검증 모듈 — HTTP 라우트 신규 없음, 파이프라인 내부 함수).
- **API 응답 확인**: HTTP 라우트 미신설이므로 curl 대신 **실제 모듈 런타임 실행**(mock 아님, `env -u OPENAI_API_KEY`)으로 대체 — 전부 통과:
  - L1-1 키없음 → `cross_model_unavailable` INFO(네트워크 호출 0) ✅
  - L1-2 주입 fn 숫자오인식 → `number_misread` HIGH, detail.verifier=gpt-4o, 프롬프트에 model_version 반영 확인 ✅
  - L1-3 인용 없는 finding → 플래그 0(보류) ✅
  - L1-4 호출 예외 → `cross_model_unavailable`(전파 안 함) ✅
  - **파이프라인 end-to-end**: `run_review_pipeline(cross_verify_fn=<number_misread>)` → flags·UPDATE.validation_flags에 병합, `body_extracted` 원본 불변, `review_status=pending_review`; cross_verify_fn 예외에도 graceful하게 pending_review 도달 ✅
- **스크린샷**: 미해당(백엔드 검증 모듈 — 브라우저 UI 없음). L1 증거는 위 런타임 실행 로그 + `276 passed` 테스트 결과로 갈음.

## 발견 이슈 및 해결
- **전체 `pytest server/tests/`(1201개) 실행 시 hang + CORS 실패 1건** — 둘 다 **pre-existing, 내 변경 무관**으로 확정:
  - `test_cors_fail_closed_when_ext_origin_unset` 실패는 worktree-root `.env`에 `INSURO_EXTENSION_ORIGIN`이 있어 main.py `load_dotenv(parents[1]/.env)`가 import 시점에 재로드→테스트 env.pop 무력화(메모리 다수 선행 태스크 2896/2897/2899에 "회귀 아님" 박제). **clean /tmp worktree(.env 미복사)에서 동일 테스트 `1 passed`로 pre-existing 확정.** 해당 테스트는 cross_verify 미참조.
  - 전체 스위트 hang은 무관한 네트워크 의존 테스트 파일(실 네트워크 호출)에서 발생 — newsletter 3파일 단독 실행은 0.65s no-hang. cross_verify는 키 부재 시 네트워크 호출 0 graceful 가드가 있어 hang 유발 불가.
- 진단 경고(pyright): reportMissingImports(server/ sys.path 미포함 — 기존 코드 동일 패턴), 방어적 isinstance unreachable(런타임 리스트 요소 방어, 무해) — 기능 결함 아님.

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: `task/task-2902-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2902-dev1`
- **머지 의견**: server/ 3파일 순수 additive(배선은 신규 파라미터 기본값으로 기존 호출 무손상). extractive-only·graceful·플래그only·pending_review 라우팅 재사용으로 fail-closed 불변. clean-worktree 회귀 0(276 GREEN). 유일 실패(CORS)·hang은 pre-existing 환경 이슈로 독립 확정. **단, 외부 유료 엔진(OpenAI gpt-4o) 신규 도입**이므로 서버 rollout 시 `OPENAI_API_KEY` 설정·비용/rate limit 정책은 회장/ANU 확인 권장(코드 자체는 키 부재 시 graceful).

## 모델 사용 기록
- 불칸(백엔드): sonnet (구현 + model_version 프롬프트 개선). haiku 미사용.
- 헤르메스(팀장, opus): 설계/검토/L1 스모크/clean-worktree 회귀 판정/보고.

## 종결 산출물
- 커밋: `919f2ef`(HEAD) ← `7dd0224` ← base `4cf8dd5`. push 완료 + 원격 head==로컬 HEAD 검증(919f2ef).
- **PR #183** open(머지 X): https://github.com/Jeon-Jonghyuk/InsuRo/pull/183
- finish-task FINALIZE_ONLY: `.done` + `.finalize-only` 마커 + timer end + result.json(owner_pickup_required=true, ANU 소유 closeout).

## 후속 (ANU/회장)
- ANU: 독립검증(clean worktree)·PR 리뷰·머지.
- rollout: 서버 `git pull`+재시작 시 `OPENAI_API_KEY` 환경변수 설정(미설정 시 cross_model_unavailable INFO로 안전 degrade).

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

