# task-2849 — 확장↔서버 계약 정합 remediation (assembleRequestBody → 서버 OHMY_PPD_V2 flat + consent 캡처 + 골든 계약테스트)

- 팀: dev2-team (오딘)
- 레벨: Lv.4 / merge_policy: none (ANU 원격 독립검증·머지)
- 프로젝트: InsuRo
- worktree: `/home/jay/projects/InsuRo/.worktrees/task-2849-dev2`  브랜치 `task/task-2849-dev2`
- base: origin/main `50bb4d8`

## Situation
확장 `assembleRequestBody`(extension/background/ingest.js)가 **중첩 snapshot/reference 래퍼 + consent bool + contract_version="V2"**를 내는데, 서버 canonical `ConsultationHistoryV1Request`(server/schemas/consultation_history_v1.py, extra="forbid")가 **12 validation error로 전량 거부**. flag OFF라 미폭발이나 파일럿 flag ON 시 첫 저장부터 100% 400/422.

## Complication
서버 스키마는 문서화된 canonical(frozen) — 확장을 서버 flat OHMY_PPD_V2에 맞춰야 함. 5개 불일치(snapshot 언랩, reference 언랩+CUSTOMER 매핑, consent 객체화, analysis_matrix V2 형태, contract_version 리터럴)를 정합하고 골든 계약테스트로 lock.

### ★ 실측으로 발견한 결정적 설계 제약 (task 지시서와 상이 — 정직 보고)
`extension/background.js`는 **allowed_resources 밖(수정 불가)**이며, `INSURO_CONSENT_SAVE_V1` 핸들러에서 `if (consent !== true)` **bool 게이트**를 걸고 `{snapshot, analysis_matrix, consent, reference}` **4개 필드만** `assembleRequestBody`로 전달한다.
- 지시서 항목3은 "content.js가 ConsentPayload 객체를 조립부에 전달"을 요구하나, content.js가 객체를 보내면 background bool 게이트가 막는다.
- → **consent 객체(ConsentPayload)는 `assembleRequestBody` 내부에서 생성**하도록 설계 결정. content.js는 계속 `consent:true`(bool) 발신 → background 게이트 통과 → assembleRequestBody가 `consent===true`일 때 `{granted_at, consent_version, scope, source}` 객체를 최상위 canonical.consent에 주입. background.js 불변, 서버는 정상 ConsentPayload 객체 수신 — 목표(12→0) 달성.

## Question → Answer (수행 내역)

### 백엔드 (토르) — assembleRequestBody flat 재조립 + 골든/서버 계약테스트
- `extension/background/ingest.js` (370 lines):
  - `INGEST_CONTRACT_VERSION`: `"V2"` → `"OHMY_PPD_V2"`
  - 신규 상수: `CONSENT_VERSION="v1"`, `CONSENT_SCOPE="consultation_history_ingest"`, `CONSENT_SOURCE="extension_popup"`
  - canonical 완전 flat화(`snapshot`/`reference` 래퍼 제거), 최상위 평면 필드로 재구성
  - `snapshot.contract_version`(PPD_CONTRACT_VERSION="OHMY_PPD_V1")은 canonical에서 제외 (서버 필드 아님, extra=forbid 대비)
  - consent 객체 내부 생성(`consent===true`일 때 granted_at=`new Date().toISOString()` tz-aware Z)
  - `client_version`은 문자열일 때만 키 삽입(undefined 시 stableStringify 파손 방지)
  - taint-guard(createOpaqueValue/unwrap) 전 구간 보존, idempotency_key 1회 생성/frozen bytes 재전송 규율 유지
- `extension/__tests__/prd1-ingest-core.test.ts` (619 lines): flat + consent 객체 형태로 재작성(tripwire contract_version="OHMY_PPD_V2", consent 객체 assert, mutation)
- `tests/contract/ohmy_ppd_v2.golden.json` (85 lines, 신규): 정합 후 assembleRequestBody 실 산출 canonical(합성 PII·마스킹, 실고객 0)
- `extension/__tests__/contract-golden.test.ts` (139 lines, 신규): `crypto.randomUUID`/`Date` 고정 stub → assembleRequestBody(대표입력) == 골든 byte/구조 동일 assert (7 tests)
- `server/tests/test_contract_golden.py` (82 lines, 신규): 골든 → `ConsultationHistoryV1Request.model_validate` 파싱 성공(0 error) + FORBIDDEN_CLIENT_FIELDS 부재 + extra 충돌 0 assert (드리프트 CI lock)

### 프론트엔드 (프레이야) — content.js 입력 정합
- `extension/content.js` (2448 lines):
  - `buildAnalysisMatrix` → 서버 `AnalysisMatrix` 형태 `{insurers:[{name,code}], coverages:[{cd,name,amount,premiums:{code:int}}]}`
    - coverage name: selected_coverages/coverage_results에 담보명 없음 확인 → coverage_cd 폴백(순수함수 계약 유지, DOM 미참조)
    - null/미가용 premium: 키 자체 제외(서버 int-only). 값 `Math.round` 정수 보장. premiums 빈 담보는 배열에서 제외(min_length=1 위반 방지)
    - insurers.name: `company_name || company_code` 폴백
  - `buildMatchedReference`/`buildCreatedReference`: `reference_type` `"matched"|"created"` → **`"CUSTOMER"`** (서버 ReferenceType 단독값). 로컬 소비처(isSaveUnlocked 등) reference_type 문자열 미의존 확인 → 별도 분리 불필요
- `extension/__tests__/prd2-crm-ui.test.ts` (491 lines): reference_type CUSTOMER 반영(8곳)

## 검증 결과 (전부 실행·통과)

### 확장 vitest 전체
```
npx vitest run extension/__tests__/
→ Test Files 17 passed (17)  |  Tests 387 passed (387)
```
골든·consent·reference·single-egress·canary sink-spy 포함 회귀 0.

### 서버 계약테스트
```
python3 -m pytest server/tests/test_contract_golden.py -q
→ 3 passed
```

### ★ L1 스모크테스트 (실동작 왕복 실증)
- 서버 재시작: **해당없음** (contract-shaping 태스크 — flag OFF, live POST 없음. 실 검증은 실 서버 Pydantic 모델에 실 산출 bytes를 통과시키는 왕복 증명)
- API 응답 확인: **왕복 실증** — 골든(=assembleRequestBody 실 산출) → 실 `ConsultationHistoryV1Request.model_validate` → **PARSE OK, 0 error** (기존 12 error → 0):
  ```
  PARSE OK — contract_version=OHMY_PPD_V2 reference_type=CUSTOMER
  consent granted_at=2026-01-01 00:00:00+00:00 version=v1
  analysis_matrix insurers=2 coverages=2
  FORBIDDEN fields present in golden?: []
  ```
  이것은 pytest 모킹이 아니라 **실 서버 스키마가 실 확장 산출물을 검증**하는 종단 계약 증명.
- 스크린샷: 해당없음 (UI 변경 없음 — 전송 페이로드 형태 정합)

## 발견 이슈 및 해결
1. **background.js bool 게이트 제약** (위 Complication) → consent 객체를 assembleRequestBody 내부 생성으로 해소. background.js 불변 유지.
2. **content.js reference_type "matched"/"created" 실 발신 확인** → 프레이야가 CUSTOMER로 정합. 토르 골든/서버 테스트와 일관(reference_type=CUSTOMER).
3. **Pyright "schemas... could not be resolved"** → 정적분석 한계(sys.path.insert 미실행). 기존 server/tests 관례(test_consultation_history_get.py 등 동일 `sys.path.insert(0, ..)`)와 일치, pytest 런타임 통과. 조치 불필요.
4. **content.js line191 `_lastMmlfcpTrace`, line2137 unused** → PR#111(e086e33) 기존 코드. 우리 diff 무관, 범위 밖.

## 변경 파일 (7개 — 전부 allowed_resources 내부, forbidden 변경 0)
- extension/background/ingest.js
- extension/content.js
- extension/__tests__/prd1-ingest-core.test.ts
- extension/__tests__/prd2-crm-ui.test.ts
- extension/__tests__/contract-golden.test.ts (신규)
- tests/contract/ohmy_ppd_v2.golden.json (신규)
- server/tests/test_contract_golden.py (신규)

`extension/background.js`, `extension/background/crm.js`, `extension/manifest.json`, `server/schemas/**`, `server/routes/**`, `server/migrations/**` — 전부 불변.

## 모델 사용 기록
- 토르(백엔드): sonnet — ingest.js 재조립 + 골든/서버 계약테스트 (계약 로직, haiku 부적합)
- 프레이야(프론트): sonnet — content.js matrix/reference 정합 (서버 스키마 정합 로직)
- 미미르/헤임달: 미소환 (UX/전용 QA 불필요 — 계약 정합 + 골든 lock으로 검증 충족)
- 팀장(오딘, Opus): 설계/제약 실측/분배/통합검증만. 직접 코딩 0.

## 머지 판단
- 머지 필요: **Yes** (그러나 merge_policy=none — 팀장/봇 머지 금지)
- 브랜치: `task/task-2849-dev2` (origin push 완료, HEAD a301e2a)
- 워크트리: `/home/jay/projects/InsuRo/.worktrees/task-2849-dev2`
- 머지 의견: 확장 387 + 서버 3 tests 전부 PASS, 왕복 12→0 실증, scope 100% 준수, background.js 불변. **ANU 원격 독립검증 후 머지** 권장. 서버 스키마 canonical 불변 확인됨.

## Closeout 경위 (순서 B — 봇=push+.done, 검증·머지=ANU)
- **최종 상태**: origin push ✓ / `.done`(qc_result=WARN) ✓ / `.qc-done`+`.qc-result`(blocking FAIL 0) ✓ / `.scope-guard-done`+scope-diff(7 허용파일 정확 일치) ✓ / `.finalize-only`(merge_policy=none, merge_executed=false) ✓ / 타이머 status=completed ↔ .done **CONSISTENT** ✓ / hang 프로세스 0.
- finish-task QC 게이트 통과: data_integrity=PASS, git_evidence=PASS, file_check=PASS, l1_smoketest_check=PASS, critical_gap/spec_compliance/duplicate_check=PASS, WARN(tdd/scope/claude_md, 비차단), 나머지 SKIP. overall=WARN.
- **발생한 운영 이슈 2건 (자체 해결, memory 기존 doctrine 확인)**:
  1. 팀 CLAUDE.md 지시대로 보고서 직후 수동 `task-timer.py end` 호출 → **조기 `.done` 생성**으로 finish-task 멱등 스킵(callback 미실행). memory `feedback_finish_task_worktree_p0b_inbox_and_manual_timer_end`가 경고한 함정 재현. → 타이머를 `in_progress`로 리셋 + 조기 마커 정리 후 finish-task를 **worktree cwd + FINISH_TASK_WORKTREE_STRICT=1**로 재실행하여 git_evidence까지 정상 통과.
  2. FINALIZE_ONLY finish-task가 **`terminal_state_callback.py emit`에서 hang**(memory `feedback_bot_finish_foreground_no_wait_anu_merges`가 규정한 좀비 지점). QC/scope는 이미 통과(.qc-done 존재). → terminal-callback wait를 포기하고, QC 통과 후 canonical `task-timer.py end`로 `.done`을 생성(정당 closeout 지점)하고 세션 종료. trap_EXIT가 남긴 false-crash 마커(CRASH_NO_EXIT_CODE, exit_code=0, dev4/vishnu 라벨은 CHAIR-AUTH-2712 템플릿 하드코딩 버그)는 오탐이므로 정리.
- **ANU 후속(disk-marker polling + OS-level pickup)**: `.done` 감지 → owner-proof → 독립 재검증 + 머지 + ANU normal callback(ANU key). 봇은 self-key callback 자가등록 금지 규율 준수(executor self-register 0).

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

