# task-2904 — 소식지 교차검증 2차엔진 OpenAI API → codex CLI 교체

## Situation
소식지 검토 워크플로우의 2차 교차검증(숫자 오인식 + 의미 왜곡 더블체크)이 `server/newsletter_cross_verify.py`에서 OpenAI API(`_call_openai_chat_completions`, httpx → api.openai.com, gpt-4o, `OPENAI_API_KEY`)로 구현되어 있었다.

## Complication
회장 지시(2026-08-05): 2차 엔진을 **새 외부 API가 아니라 우리 시스템의 codex cc(codex CLI)로** 교체. 이유 — 새 API 키 불필요 + 외부 API 신규 연동/데이터 egress 우려 회피(내부 도구 사용). 단, **로직·플래그 스키마·파이프라인 배선·검증 초점(숫자 오인식+의미 왜곡)은 전부 그대로**, **호출 메커니즘만 교체**.

## Question
기존 계약(플래그 스키마, extractive-only, graceful, 인용 span 강제, insurer/date 제외)을 100% 보존하면서 2차 검증 호출부만 OpenAI httpx → codex CLI subprocess로 안전하게 교체할 수 있는가?

## Answer
가능했다. 코드에 이미 주입 이음새(`openai_call_fn`)가 있어 호출부만 갈아끼웠다. 수정은 **server/ 2파일만**(수술 범위 준수).

### 변경 내역
1. **`_call_codex_cli` 신규(async)** — `codex exec -s read-only --skip-git-repo-check --ephemeral -C /tmp -o <tmpfile> -` 형태.
   - 프롬프트는 **stdin**으로 주입(`asyncio.create_subprocess_exec`), 최종 메시지는 `-o <tmpfile>`로 캡처(stdout 잡텍스트 회피).
   - **clean cwd(/tmp)** 실행으로 프로젝트 CLAUDE.md/무거운 설정 로딩 회피(InfoKeyword claude CLI 교훈), **read-only 샌드박스**로 모델의 쉘 실행/파일쓰기 차단, **60s 타임아웃**.
2. **OpenAI 경로 완전 제거** — `_call_openai_chat_completions`·`httpx`·`_OPENAI_CHAT_COMPLETIONS_URL`·`OPENAI_API_KEY` 의존 삭제. 주입 이음새는 유지(`openai_call_fn` → `codex_call_fn`으로 개명, 테스트 주입성 보존).
3. **`detail.verifier` 값** `"gpt-4o"` → `"codex"` — 신설 `VERIFIER_LABEL = "codex"` 상수. `DEFAULT_MODEL = None`(codex 기본 모델 사용, 별도 `-m` 미전달).
4. **graceful 유지** — codex 미설치(PATH 없음)/비정상 종료/타임아웃/JSON 파싱 실패 → 파이프라인 중단 없이 `cross_model_unavailable`(INFO) 1건. `codex_call_fn` 미주입 + codex PATH 부재 시 subprocess 자체를 만들지 않음(테스트/CI subprocess 0회 보장).
5. **`_parse_json` 관대화** — 코드펜스 제거 + 첫 `{`~마지막 `}` 블록 추출 fallback(앞뒤 잡텍스트 방어).

### 불변 확인 (건드리지 않음)
- 플래그 스키마(`number_misread`/`meaning_drift`/`cross_model_unavailable`), extractive-only·플래그only, 인용 span(source_quote) 강제, 검증 초점(숫자·의미; insurer·date·title 제외).
- `server/newsletter_review.py`(run_review_pipeline 배선), `main.py`, `newsletter_validation.py`, `migrations`, `src` 전부 미접촉. newsletter_review는 `cv(source_text, body, model_version)` 3-위치인자 호출 → 시그니처 변경 안전(외부 결합 grep 0건 확인).

## 생성/수정 파일
- `server/newsletter_cross_verify.py` (수정 — 전체 재작성, OpenAI→codex)
- `server/tests/test_newsletter_cross_verify.py` (수정 — codex subprocess mock으로 전환, httpx mock 제거)
- 커밋: `0760a30` `[task-2904] 불칸: 소식지 교차검증 2차엔진 OpenAI API→codex CLI(codex exec) 교체`
- 브랜치: `task/task-2904-dev1` (push 확인: remote==local 0760a30)

## 테스트 결과
- **대상 테스트**: `pytest server/tests/test_newsletter_cross_verify.py` → **16 passed** (codex subprocess mock으로 기존 16 케이스 동등 통과: 숫자오류→number_misread HIGH / 원문없는주장→meaning_drift HIGH / 뉘앙스차→MED / 정확→[] / codex 미가용·예외·파싱실패→cross_model_unavailable / body 불변 / insurer·date·title 미생성 / subprocess 0회 증명)
- **전체 회귀**: `pytest server/tests/` → **1200 passed, 1 failed**
  - 유일 실패 `test_cors_fail_closed_when_ext_origin_unset` = **회귀 아님(.env 오염 아티팩트)**. 근거:
    1. clean base(84b3d0b) /tmp worktree(.env 없음)에서 **동일 테스트 PASS**.
    2. task worktree만 실패 — 유일 차이는 worktree **루트 `.env`**(파일럿 아티팩트, worktree_manager 복사). 루트 `.env`에 `INSURO_EXTENSION_ORIGIN`(chrome-extension origin) 존재 → main.py가 dotenv로 상위 .env 로드 → CORS가 확장 origin 노출 → fail-closed 깨짐.
    3. 본 작업 diff(newsletter_cross_verify + 테스트)는 CORS/main._cors_allow_origins와 **완전 무관**.
  - → **실질 회귀 0**.
- grep 검증: 프로덕션·테스트 파일에서 `httpx`/`OPENAI_API_KEY`/`_call_openai`/`openai_call_fn` **0건**, `codex`/`VERIFIER_LABEL`/`_call_codex_cli`/`shutil.which`/`codex_call_fn` 반영 확인.

## L1 스모크테스트 결과 (실제 codex CLI subprocess, mock 없음)
- **테스트 결과(L1 증거)**: `pytest server/tests/test_newsletter_cross_verify.py` → **16 passed**; 전체 `pytest server/tests/` → **1200 passed** (CORS 1건은 worktree 루트 .env 오염, clean base 통과로 회귀 아님 확정).
- **서버 재시작**: 코드 정제 작업(모듈 단위 subprocess) — API 라우트 미변경이라 서버 재시작 불요.
- **API 응답 확인**: 파이프라인 배선 불변·라우트 미변경 — curl 라우트 검증 대신 아래 실제 subprocess 실호출로 대체 검증.
- **실호출 검증**(DIRECT-WORKFLOW 4.8 "subprocess 작업" 경로):
  - 실제 `cross_verify()` 실호출(codex_call_fn 미주입 → 진짜 codex exec):
    - **숫자오류 케이스** (원문 3,000만원 → 정리본 30,000만원): `number_misread` HIGH 1건, `source_quote="지급보험금 한도는 3,000만원입니다."`, `verifier="codex"`, 환각 플래그 0. **소요 3.9s**.
    - **정확한 정리 케이스** (표현만 정리, 사실 동일): `[]` (오탐 0). **소요 11.4s**.
  - codex exec 자체 sanity: clean cwd /tmp에서 짧은 JSON 프롬프트 → `{"number":"3,000만원"}` 초 단위 응답(rc=0, 6.8s) 실측.
  - **판정**: codex CLI 2차 검증이 실제로 동작 — 숫자오류 검출 + 정확본 무플래그 양방향 확인.

## 발견 이슈 및 해결
- **Pyright unreachable(210/230행)**: `_map_number_finding/_map_meaning_finding`의 `if not isinstance(finding, dict): return None` 방어 가드가 `finding: dict` 타입힌트 때문에 정적 unreachable로 표시됨. **base 원본에도 동일하게 존재하던 무해한 런타임 방어**(JSON 파싱 결과 비-dict 방어). 신규 버그 아님 → 유지.
- **CORS 테스트 실패 오분류 방지**: 초기 env-변수 오염 가설이 틀렸음을 확인(변수 .env/shell 부재에도 실패). 원인을 끝까지 좁혀 **worktree 루트 .env 파일**(dotenv 상위 로드)임을 clean base 대조로 확정.

## 모델 사용 기록
- **불칸(백엔드)**: sonnet — 구현(2파일 수정+커밋+grep 검증). 정당성: 명세가 완전 확정된 surgical 코딩 작업.
- 팀장(헤르메스, Opus): 설계/분석/검증/L1/종결만 수행(직접 코딩 없음).

## 머지 판단
- **머지 필요**: Yes (단, **ANU가 머지** — 본 task merge_policy=none, 완료 순서 B)
- **브랜치**: `task/task-2904-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2904-dev1`
- **base**: `origin/main` = 84b3d0b (#183)
- **머지 의견**: server/ 2파일 순수 교체(호출 메커니즘만). 대상 16/16 + 실질 회귀 0 + L1 실호출 양방향 통과. 외부 결합 0(newsletter_review 3-위치인자 호출 안전). CORS 1건은 .env 오염(회귀 아님). ANU 독립검증(clean worktree) → Codex 사전 반증리뷰 → 머지 → 서버 배포(git pull + 재시작) 순으로 후속 권장.

## ANU 후속 (회장 지시 순서)
1. ANU 독립검증 + 머지
2. Codex CC 사전 리뷰(최종본 반증 검토) → 회장 보고
3. 배포(서버 git pull + 재시작; 웹앱 CF Pages는 본 변경 무관)

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

