# [Lv.2] 소식지 승인 서버측 게이트 — HIGH 플래그 개별 확인(ack) 강제 + 감사로그 (fail-closed)

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "server/routes/newsletter_review_v1.py"
    - "server/newsletter_review.py"
    - "server/tests/**"
  forbidden_paths:
    - ".github/**"
    - "extension/**"
    - "src/**"
    - "server/migrations/**"
    - "server/main.py"
    - "server/newsletter_cross_verify.py"
    - "server/newsletter_validation.py"
  commands: ["pytest","python3","git"]
  merge_policy: "none"
  ttl_hours: 14
```
> ★ base=현 origin/main. 근거: agent 미팅 4사이클(로키 P0-A) — 현재 승인 게이트가 **클라이언트 disable 뿐** → `POST /transition→approved` API 직접호출로 미검토 발행 우회 가능(금소법 구멍). 미팅 기록: `memory/meetings/2026-08-06-newsletter-review-detail-ui.md`.

## 배경
검토 상세 UI는 발행 전 사람이 HIGH 플래그(숫자 오독·의미 왜곡)를 확인해야 승인. 그러나 서버(`server/routes/newsletter_review_v1.py`)의 approve 전이는 **HIGH ack를 전혀 검사하지 않음**. 서버에서 fail-closed로 강제해야 함(클라 disable은 우회 가능).

## ★ 공유 계약 — 플래그 키 & ack (프론트 태스크와 100% 동일, 정확히 이 알고리즘)
- **flag_key**(결정론): 각 validation_flag 객체에서
  ```python
  import hashlib
  def flag_key(f: dict) -> str:
      d = f.get("detail") or {}
      basis = "|".join([
          str(f.get("rule","")), str(f.get("field","")),
          str(d.get("source_quote") or d.get("summary_value") or d.get("summary_text") or ""),
          str(f.get("message","")),
      ])
      return hashlib.sha256(basis.encode("utf-8")).hexdigest()[:16]
  ```
  프론트도 GET `/review`가 준 동일 flag 객체로 같은 값을 계산 → 일치.
- **HIGH 대상**: `level == "HIGH"` 인 플래그만 ack 필수. (MED/INFO는 ack 불요.)
- **approve 요청 바디 확장**: `acknowledgements: [{ "flag_key": str, "attested": true }]` (attested=사람이 "원문에서 직접 대조 확인함"). 
- **재추출/편집으로 flag 세트 변경 시**: flag_key가 달라져 옛 ack 자동 무효(별도 무효화 불요).

## 작업
1. **approve 서버 게이트** (`newsletter_review_v1.py` 의 to==`approved` 전이 경로):
   - 현재 레코드의 `validation_flags` 에서 **HIGH flag_key 집합** 계산.
   - 요청 acknowledgements 의 `attested==true` flag_key 집합이 **HIGH 집합을 전부 포함**하지 않으면 → **fail-closed 거부**(HTTP 409 또는 422, `detail="미확인 HIGH 플래그: N건"`). transition_review_status(상태전이) 호출 **전에** 거부(흔적 0).
   - HIGH 0건이면 ack 없이 승인 허용.
   - ★ 이 검사는 **서버에서 무조건** — 클라가 뭘 보내든 서버가 HIGH 집합을 자체 계산해 대조.
2. **감사로그**: 승인 성공 시 이벤트(newsletter_review_events 또는 transition presented/diff)에 **actor·ts·확인한 flag_key 목록·attested 여부** 기록(금소법 §44/§45 입증책임 대비). 기존 감사 이벤트 구조 재사용.
3. `newsletter_review.py` 의 상태기계(published fail-closed 3중잠금)·전이표는 **불변**. approve의 HIGH-ack 검사만 route 계층에 추가.

## 제약
- routes + newsletter_review(필요시 헬퍼만) + 테스트. main.py·validation·cross_verify·migration·src 불변. published fail-closed·기존 전이 회귀 0.

## 검증
- `pytest server/tests/` 회귀 0. 신규: HIGH 2건 중 1건만 ack → approve **거부(409/422)** / 전부 ack → 승인 / HIGH 0건 → ack 없이 승인 / 클라가 가짜 flag_key 보내도 서버 HIGH 집합 기준으로 거부 / 감사로그에 flag_key·attested 기록 / flag_key 알고리즘이 위 스펙과 정확히 일치(고정 픽스처로).

## 완료 (★ 순서 B)
- server/ 만. **dev6 금지** — dev1(헤르메스, 검토 API·상태기계 원저자). 커밋 → push → **★ push 확인** → finish-task foreground 1회 → .done. ANU 독립검증·머지·배포는 ANU.

## goal_assertions (auto-generated)
- `pytest server/tests/`
