# task-1648.1 완료 보고서

## SCQA

**S**: InsuWiki 검토 시스템 Phase 1 Week 2 작업이다. Week 1(task-1647.1)에서 4역할 Custom Claims 기반 인증 인프라가 구축되어 머지 완료된 상태이며, 이를 기반으로 상태 머신, 검토 제도, 경량 수정 면제, 감사 로그를 구현해야 한다.

**C**: 현재 문서 상태 관리가 없어 모든 문서가 즉시 공개된다. 제이회장님 1인 승인 병목이 존재하며, 보험 콘텐츠의 무검증 수정이 가능하다. 특히 고위험 콘텐츠(법규/판례/약관)의 숫자·부정어 변조에 대한 방어가 없다.

**Q**: CF 서버 사이드 상태 머신과 다중 검토자 시스템으로 1인 병목을 해소하고, 7조건 경량 면제로 검토자 부담을 최소화할 수 있는가?

**A**: 18개 파일(생성 14 + 수정 4) 변경, 테스트 115건 전체 통과로 구현 완료. 7개 상태 전이 로직, 2단계 승인(고위험: reviewer+admin 순서 무관), 경량 수정 7조건 AND 판정(Levenshtein ≤20 + 숫자 위치 보존 비교), 라운드 로빈 배정, 감사 로그 이중화(Firestore + Cloud Logging). 마아트 독립 검증에서 HIGH 3건 + MEDIUM 3건 발견 후 전수 수정 완료.

## 작업 내용

### 상태 머신 (CF 서버 사이드)
- `VALID_TRANSITIONS` 테이블: 7개 상태(draft, in_review, approved, rejected, revision_requested, needs_re_review, published) 간 10개 합법 전이 정의
- `assessRiskLevel()`: sourceType 기반 리스크 판정 (court_ruling/regulation/policy_pdf → high)
- `canApprove()`: 저위험=reviewer 1명, 고위험=reviewer+admin 2단계 (순서 무관)
- `onReviewCreate` CF: reviews 서브컬렉션 onCreate 트리거, 상태 전이 + 감사 로그
- `onDocumentUpdateReview` CF: approved/published 문서 수정 시 needs_re_review 자동 전환

### 경량 수정 면제 로직
- 7조건 AND: (1) Levenshtein ≤ 20자 (bandwidth 제한 O(n)), (2) 숫자/금액/날짜/비율 위치 보존 비교, (3) 부정어 빈도 불변, (4) URL 불변, (5) 법규/약관 인용 불변, (6) 고위험 카테고리 아님, (7) 7일 내 면제 < 3회
- CF 서버사이드 판정 (클라이언트 우회 불가)

### 검토 제도
- `reviews` 서브컬렉션: `documents/{docId}/reviews/{reviewId}`
- 자기 검토 금지: contributorIds + authorId 이중 검증 (API + assignReviewer)
- 검토자 배정: 라운드 로빈 + 연속 배정 제한 + 풀 최소 3명 체크
- `review.comment`: 반려/수정요청 시 사유 필수 입력 검증
- `collectionGroup('reviews')` 인덱스 추가

### 감사 로그
- `documents/{docId}/auditLogs` 서브컬렉션
- CF에서만 쓰기 (Security Rules deny all client writes)
- Cloud Logging 이중화 (structured log: `functions.logger.info('AUDIT_LOG', ...)`)
- `auditLogs` 복합 인덱스 추가 (action + createdAt)

### API
- `POST /api/wiki/entries/{id}/review`: verifyReviewer 인증 → decision/comment 검증 → 자기 검토 금지 → in_review 상태 확인 → reviews 서브컬렉션 생성

## 산출물 파일

### 생성 (14개)
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/types/review.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/reviewStateMachine.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/auditLog.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/lightweightEditExemption.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/reviewerAssignment.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/reviewOnCreate.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/onDocumentUpdate.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/reviewStateMachine.test.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/auditLog.test.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/lightweightEditExemption.test.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/reviewerAssignment.test.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/reviewOnCreate.test.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/onDocumentUpdate.test.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/nextapp/src/app/api/wiki/entries/[id]/review/route.ts`

### 수정 (4개)
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/index.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/nextapp/src/types/firestore.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/nextapp/src/lib/auth-middleware.ts`
- `/home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/firestore.indexes.json`

## 테스트 결과
- `reviewStateMachine.test.ts`: 34/34 통과 (합법 전이 10건, 불법 전이 6건, transitionStatus 3건, assessRiskLevel 6건, getRequiredApprovals 2건, canApprove 7건)
- `auditLog.test.ts`: 4/4 통과
- `lightweightEditExemption.test.ts`: 41/41 통과 (7조건 경계값 + swap 탐지 + 통합)
- `reviewerAssignment.test.ts`: 23/23 통과
- `reviewOnCreate.test.ts`: 7/7 통과
- `onDocumentUpdate.test.ts`: 6/6 통과
- **전체**: 311/312 (실패 1건은 기존 youtubeWhisper 타임아웃, 본 작업 범위 외)
- TDD 순서: 테스트 먼저 작성 → 구현 (audit-trail 검증 PASS)

## 발견 이슈 및 해결

### 자체 해결 (7건)

1. **canApprove 순서 의존 버그** — 고위험 2단계 승인에서 admin 먼저 승인 시 reviewer 승인으로 approved 불가. `canApprove`를 역할 기반 검증으로 리팩토링 (existingReviews에 reviewerRole 포함, 순서 무관).
2. **[HIGH-1] 숫자 위치 교환 미탐지** — `hasNoNumericChange`가 토큰 sort 후 비교하여 "1,000↔500" swap이 면제 통과. sort 제거하여 위치 보존 비교로 변경.
3. **[HIGH-2] assignReviewer authorId 미포함** — contributorIds에 authorId 없을 경우 자기 검토 배정 가능. `AssignReviewerParams`에 `authorId?` 추가, contributorIds에 자동 병합.
4. **[HIGH-3] auditLogs 복합 인덱스 누락** — `action` + `createdAt` 복합 쿼리에 인덱스 필요. `firestore.indexes.json`에 추가.
5. **[MEDIUM-4] HIGH_RISK_CATEGORIES 데드 코드** — `types/review.ts`의 `['practice']`와 `lightweightEditExemption.ts`의 `['court_ruling', 'regulation', 'policy_pdf']` 이름 충돌. types 파일의 데드 코드 제거.
6. **[MEDIUM-5] 합법 전이 테스트 커버리지 60%** — 10건 중 6건만 테스트. 누락 4건 추가 (revision_requested→draft, revision_requested→in_review, needs_re_review→in_review, published→needs_re_review).
7. **[MEDIUM-6] AuditLogEntry.action 타입 불일치** — functions는 `AuditAction` union, nextapp는 `string`. nextapp에 `AuditAction` 타입 추가 및 동기화.

### 범위 외 미해결 (2건)
1. **Security Rules에서 리뷰 생성 시 문서 상태 미검증** — Rules에 get() 추가 필요하나 10회 제한에 근접. API route + CF에서 3계층 방어 중이므로 LOW. 불필요한 review 누적 가능성은 있으나 상태 전이 자체는 차단됨.
2. **⚠️ 기존 테스트 실패 1건** — `youtubeWhisper.test.ts` OPENAI_API_KEY 타임아웃 (본 작업 범위 외, 기존부터 존재)

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: task/task-1648.1-dev1
- **워크트리 경로**: /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1
- **머지 의견**: 테스트 115건 통과, 마아트 HIGH 3건 전수 수정, TDD 순서 준수. Week 1 산출물과 충돌 없음 (머지된 main 기반 worktree). 배포 시 `firebase deploy --only functions,firestore:indexes` 필요.

## 모델 사용 기록
- 불칸-A: Types + 상태 머신 + 리스크 판정 + 감사 로그 / sonnet
- 불칸-B: 경량 수정 면제 7조건 + 검토자 배정 / sonnet
- 불칸-C: CF 트리거 + API 라우트 + 미들웨어 + 인덱스 / sonnet
- 마아트: 독립 QC 검증 / sonnet
- 헤르메스(팀장): canApprove 버그 수정, 마아트 이슈 6건 수정, 설계/통합/보고서 / opus

## QC 자동 검증 결과

```json
{
  "tdd_check": "PASS",
  "data_integrity": "PASS",
  "spec_compliance": "PASS",
  "test_result": "311/312 (기존 실패 1건)"
}
```

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

### 수정 파일 목록
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/reviewOnCreate.test.ts: 5회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/reviewStateMachine.ts: 4회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/nextapp/src/types/firestore.ts: 4회 (Edit)
- bash_cmd: 4회 (Bash)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/auditLog.test.ts: 3회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/reviewStateMachine.test.ts: 3회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/reviewOnCreate.ts: 3회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/reviewerAssignment.ts: 3회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/firestore.indexes.json: 2회 (Edit)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/lightweightEditExemption.test.ts: 2회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/onDocumentUpdate.test.ts: 2회 (Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/lightweightEditExemption.ts: 2회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/types/review.ts: 2회 (Edit, Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/__tests__/reviewerAssignment.test.ts: 1회 (Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/auditLog.ts: 1회 (Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/index.ts: 1회 (Edit)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/functions/src/onDocumentUpdate.ts: 1회 (Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/nextapp/src/app/api/wiki/entries/[id]/review/route.ts: 1회 (Write)
- /home/jay/projects/insuwiki/.worktrees/task-1648.1-dev1/nextapp/src/lib/auth-middleware.ts: 1회 (Edit)
- /home/jay/workspace/memory/reports/task-1648.1.md: 1회 (Write)
- /home/jay/workspace/memory/tasks/task-1648.1.md: 1회 (dispatch)

### 도구 사용 현황
- Edit: 23회
- Write: 19회
- Bash: 4회
- dispatch: 1회

