# [Lv.3] 소식지 검토 Workflow Phase 1 — 스키마(마이그레이션 017) + 상태전이 함수 (T1+T2)

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "server/migrations/**"
    - "server/main.py"
    - "server/routes/**"
    - "server/schemas/**"
    - "server/newsletter_review.py"
    - "server/tests/**"
    - "docs/**"
  forbidden_paths:
    - ".github/**"
    - "extension/**"
    - "src/**"
  commands: ["pytest","python3","git"]
  merge_policy: "none"
  ttl_hours: 20
```
> ★ merge_policy=none. base=현 origin/main. **Phase 1 = 백엔드 척추만**(스키마+상태기계). 검토 UI·자동검증 워커는 Phase 2/3(별도). 3문서: `memory/plans/newsletter-review-workflow_260805/`.

## 배경 (회장 승인 MVP — 미팅 3사이클)
소식지 PDF/PPT 업로드 → AI 추출(보험사명·제목·본문)이 **검토 없이 바로 발행**됨. **사람 발행 전 검토·교정 게이트**를 넣는다. Phase 1은 그 토대(스키마+상태기계). 단일소스: `memory/plans/newsletter-review-workflow_260805/plan.md`.

## 선행 (봇이 먼저 파악)
- **소식지 저장 테이블 특정**: 현재 소식지는 Supabase에 저장(`file_type='newsletter'`), 추출은 `ai_queue` 경유. main.py/routes/마이그레이션에서 **실제 소식지 테이블명**(전용 테이블 or contents/documents 공용)을 먼저 찾아 확정. 공용 테이블이면 컬럼은 공용에 추가하되 로직은 newsletter로 scope.

## T1 — 마이그레이션 017 (+backfill)
- `server/migrations/017_newsletter_review_workflow.sql` 신규. **모든 신규 컬럼 NULLABLE + DEFAULT**(하위호환, 기존 행 안 깨짐).
- 소식지 테이블에 컬럼 추가:
  - `review_status` text (enum-like: uploaded/extracting/extracted/auto_validated/pending_review/editing/approved/published/rejected/extract_failed) DEFAULT 'published'(기존행 보호)
  - `source_file_ref` jsonb (원본 파일 위치 + `sha256`)
  - `extract_mode` text, `model_version` text
  - `insurer_extracted`/`insurer_edited`, `title_extracted`/`title_edited`, `body_extracted`/`body_edited` (각 분리)
  - `validation_flags` jsonb DEFAULT '[]', `field_confidence` jsonb DEFAULT '{}' (내부보존·UI 미노출)
  - `reviewer_id`, `reviewed_at`, `approved_at`, `published_at`
- 신규 테이블 `newsletter_review_events` (append-only 감사): id, doc_id(FK), from_status, to_status, actor, action, presented jsonb(제시된 grounding·flags), diff jsonb, ts. **UPDATE/DELETE 금지(append-only)** — RLS/트리거로 보장.
- **backfill**: 기존 발행분 → `review_status='published'`, `extract_mode='legacy'`, `*_edited`=현재값 복사, `*_extracted`=NULL, `validation_flags`='[]', `field_confidence`='{}'. review_events에 genesis는 **단일 batch 이벤트 1건**으로.
- ⚠️ 마이그레이션 **적용은 회장 rollout**(Supabase SQL editor). 봇은 SQL 작성 + idempotent(`IF NOT EXISTS`/`ADD COLUMN IF NOT EXISTS`) 보장.

## T2 — 상태전이 함수 (fail-closed)
- `server/newsletter_review.py` 신규: `transition_review_status(doc_id, event, actor, ...)` **단일 중앙 함수**.
  - **허용 전이표**를 코드 상수로 고정, 부적합 전이=예외.
  - ★ **published fail-closed**: `→published`는 이전 상태 `approved` AND `reviewer_id NOT NULL`(사람)일 때만. **AI/워커는 published 절대 불가.**
  - `editing→approved` 직행 금지(재검증 경로 강제), reject/re-extract 루프 허용.
  - 모든 전이 = 소식지 테이블 status 갱신 + `newsletter_review_events` append를 **같은 DB 트랜잭션**에서(이벤트 없는 상태변경 원천 차단).
- **기존 흐름 회귀 0**: 기존 manual/직접발행 경로는 `extract_mode='manual'`로 게이트 우회(상태기계 미진입). AI 추출 업로드만 uploaded→extracting 진입. 기존 소식지 CRUD·발행 e2e 불변.

## 검증
- `pytest server/tests/` — 신규:
  - 상태전이 매트릭스 전수(허용/금지). **음성 케이스 필수**: AI/워커가 published 불가 / editing→approved 직행 금지 / reject·re-extract 루프.
  - 전이마다 review_events 정확히 1건 append(동일 트랜잭션, 롤백 시 둘 다 롤백).
  - 마이그레이션 017 idempotent·nullable(기존행 backfill 정합).
  - 기존 manual 경로 회귀 0.
- 마이그레이션 SQL은 로컬 검증(구문/idempotent) + 적용은 회장 rollout 명시.

## 완료 (★ 순서 B)
- 변경 = server/ 만. src/·extension·.github 불변.
- **dev6 금지** — dev1. 커밋 → push → finish-task **foreground 1회** → .done 즉시 종료. **background wait 금지.** ANU 검증·머지·마이그레이션 rollout 안내는 ANU. **Phase 2(자동검증 워커+grounding)·Phase 3(검토 UI)는 이 Phase 머지 후 별도 위임.**

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