# task-2914 보고서 — 소식지 승인 서버측 게이트(HIGH ack 강제 · fail-closed) + 감사로그

## Situation
소식지 발행 전 검토는 상세 UI에서 사람이 HIGH 플래그(숫자 오독·의미 왜곡)를 확인해야 승인하도록 설계됐다. 그러나 서버(`newsletter_review_v1.py`)의 approve 전이는 **HIGH ack를 전혀 검사하지 않아**, 클라이언트 disable을 우회하는 `POST /transition→approved` 직접호출로 미검토 발행이 가능했다(금소법 구멍, agent 미팅 로키 P0-A).

## Complication
클라이언트측 disable만으로는 API 직접호출을 막을 수 없다. 서버가 자체적으로 HIGH 집합을 계산해 fail-closed로 강제해야 한다. 단, `newsletter_review.py`의 published fail-closed 3중잠금·전이표는 불변 유지, main/validation/cross_verify/migration/src도 불변이어야 한다.

## Question
검토 상태기계와 전이표를 건드리지 않으면서, route 계층에서만 HIGH-ack 게이트를 서버 권위로 강제하고 승인 이력을 감사로그에 남기려면?

## Answer (구현)
route 계층에 approve HIGH-ack 게이트 추가 + `newsletter_review.py`에 순수 헬퍼만 추가. 상태기계·전이표·예외계층은 불변.

### 수정 파일 (server/ 만, 딱 3개)
1. `server/newsletter_review.py` — 파일 끝에 순수 헬퍼만 추가(상태기계 불변):
   - `flag_key(f)`: 프론트 태스크와 100% 동일한 결정론 알고리즘(sha256 앞 16자). basis = `rule|field|(detail.source_quote or summary_value or summary_text or "")|message`.
   - `high_flag_keys(flags)`: `level=="HIGH"` 플래그의 flag_key 집합(서버 자체 계산).
   - `import hashlib` 추가.
2. `server/routes/newsletter_review_v1.py`:
   - `TransitionBody`에 `acknowledgements: list[dict] | None = None` 필드 확장.
   - 현재 레코드 select에 `validation_flags` 추가.
   - `current` 계산 직후·`transition_review_status` 호출 **전에** approve 게이트: `to_status=="approved"`면 서버가 `validation_flags`에서 HIGH flag_key 집합을 자체 계산 → `attested==true`인 ack 집합이 HIGH를 전부 포함하지 않으면 **422 `UNACKNOWLEDGED_HIGH_FLAGS`** (`detail="미확인 HIGH 플래그: N건"`)로 RPC 호출 전 거부(흔적 0). HIGH 0건이면 ack 없이 승인.
   - 통과 시 `presented={"high_ack":{"high_flag_keys":[...정렬...],"acknowledgements":[{flag_key,attested}...]}}`를 `transition_review_status(..., presented=approve_presented)`로 전달 → 기존 RPC review_events 구조(p_presented jsonb)에 확인 flag_key·attested 기록(actor·ts는 기존 이벤트 구조가 기록). 금소법 §44/§45 입증책임 대비.
3. `server/tests/test_newsletter_review_api.py` — `class TestApproveHighAckGate` 7종 추가(기존 테스트 불변).

## 테스트 결과
- 신규 7종 전부 PASS: HIGH 2중 1ack→422·흔적0 / 전부 ack→200·rpc1 / HIGH0→무-ack 승인 / 가짜 flag_key→서버 HIGH집합 기준 422 / attested=false 미인정→422 / 감사로그 flag_key·attested 기록 / flag_key 알고리즘 정확성(sha256 basis 재계산 교차검증, source_quote 우선순위·summary_text-only·detail={} 3케이스).
- **회귀 0** (CI-parity clean worktree, `.env` 없음): `pytest server/tests/` → **1249 passed, 0 failed** (base 1242 + 신규 7).

### 발견 이슈 및 해결 — consultation CORS 테스트 실패 오탐 배제
worktree 최초 전체 스위트에서 `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` 1건 실패. **독립 검증 결과 이번 작업 회귀 아님**:
- 원인: `main.py:54` `load_dotenv(../.env)`가 worktree_manager가 복사한 `.env`(1행 `INSURO_EXTENSION_ORIGIN=chrome-extension://...`)를 로드. 해당 CORS 테스트는 subprocess에서 env var를 pop하지만 load_dotenv가 `.env`에서 재주입 → chrome-extension origin이 CORS에 포함되어 fail-closed 단언 실패.
- 이 테스트는 **subprocess 격리** 구조라 내 인프로세스 코드/테스트가 영향 줄 수 없음. 내 변경은 newsletter 3파일뿐(consultation 무관).
- 증명: pristine base(42df0bd, `.env` 없음) 전체 스위트 = 1242 passed·0 failed. clean `.env`-free CI-parity worktree(내 브랜치 HEAD) = 1249 passed·0 failed. → 실패는 100% worktree `.env` 오염 아티팩트.
- 메모리 규칙 `feedback_regression_verify_clean_worktree_not_polluted` / `feedback_e2e_failure_pre_existing_claim_verify_ci_log` 적용.

## L1 스모크테스트 결과
실제 `main.app`(FastAPI) + 실제 route + 실제 flag_key 헬퍼로 실 HTTP 요청 검증(mock 재구현 아님):
- 서버 재시작: 해당없음(TestClient로 실제 app 객체 in-process 기동 — 프로덕션 라우트/헬퍼 그대로 실행)
- API 응답 확인:
  - `[1]` 부족 ack → **422 "미확인 HIGH 플래그: 1건", rpc_calls=0**(흔적 0)
  - `[2]` 전부 ack → **200, rpc=1, 감사로그 high_flag_keys=['123c93…','611329…'] 기록**
  - `[3]` 가짜 flag_key → **422**(서버 자체 HIGH집합 기준), rpc=0
  - `[4]` HIGH 0건 → **200**(무-ack 승인), rpc=1
- 스크린샷: 해당없음(백엔드 API — HTTP 응답/감사필드로 검증)

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: task/task-2914-dev1
- **워크트리 경로**: /home/jay/projects/InsuRo/.worktrees/task-2914-dev1
- **머지 의견**: server/ 3파일만 수정. 상태기계·전이표·published fail-closed 불변. 신규 7종 PASS + clean worktree 회귀 0(1249 passed). consultation 실패는 `.env` 오염 아티팩트로 배제 완료. 실 app L1 4시나리오 통과. 충돌 위험 낮음(프론트 태스크와 flag_key 알고리즘 공유 계약 일치).

## 모델 사용 기록
- 불칸(백엔드, 코드 구현): sonnet
- 아르고스(테스터, 테스트 작성/검증): sonnet
- 팀장(헤르메스): 설계·게이트 판정·회귀 독립검증·L1 스모크(직접 코딩 없음)
- haiku 미사용.

## 비고
- flag_key 알고리즘은 프론트 태스크와 100% 동일(공유 계약). 재추출/편집으로 flag 세트 변경 시 flag_key가 달라져 옛 ack 자동 무효.
- 후속(ANU): 독립검증·PR/Gemini 리뷰·머지·배포는 ANU 소관(순서 B).

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

