# [Lv.4] 확장↔서버 계약 정합 remediation (assembleRequestBody → 서버 OHMY_PPD_V2 flat + consent 캡처 + 골든 계약테스트)

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "extension/background/ingest.js"
    - "extension/content.js"
    - "extension/__tests__/**"
    - "server/tests/test_contract_golden.py"
    - "tests/contract/**"
  forbidden_paths:
    - "src/**"
    - ".github/**"
    - "server/routes/**"
    - "server/schemas/**"
    - "server/migrations/**"
    - "extension/manifest.json"
    - "extension/background/crm.js"
  commands: ["npx vitest","pytest","python","git"]
  merge_policy: "none"
  ttl_hours: 16
```
> ★ merge_policy=none. base=현 origin/main. 신규 브랜치. **서버 스키마/라우트 변경 절대 금지** — 서버가 canonical(문서화·frozen). 확장을 서버에 맞춘다. host/key/flag(=별도 후속 TASK-EXT-PLUMBING)는 이 태스크 범위 밖(manifest/crm.js 건드리지 마라).

## 배경 (실측 확증 — task-2848 진단)
확장 `assembleRequestBody`(ingest.js) 가 **중첩 snapshot envelope** 를 내는데 서버 `ConsultationHistoryV1Request`(server/schemas/consultation_history_v1.py) 가 **12 validation error 로 전량 거부**. flag OFF 라 미폭발이나 **파일럿 flag ON 시 첫 저장부터 100% 400/422**. 서버가 문서화된 canonical(`docs/contracts/OHMY_PPD_DATA_CONTRACT_V1.md` §2-1 frozen) → **확장을 서버 flat OHMY_PPD_V2 로 정합**.

## 서버 flat 타깃 (ConsultationHistoryV1Request — 이 형태로 조립)
최상위 평면: `contract_version="OHMY_PPD_V2"` · `reference_type="CUSTOMER"` · `reference_id` · `idempotency_key` · `query_condition{age,gender,insurance_type,plan_id}` · `request_fingerprint` · `dom_selection_hash` · `api_captured_at:int` · `dom_snapshotted_at:int` · `selected_coverages:[{coverage_cd,amount}]` · `per_insurer:[PerInsurerResult]` · `validation:ValidationResult` · `consent:{granted_at(AwareDatetime ISO), consent_version, scope, source}` · `analysis_matrix`(서버 AnalysisMatrix 형태, V2 필수) · `client_version?`. **모델 extra="forbid"** — 여분 키(snapshot/reference 래퍼) 있으면 거부.

## 작업 (Codex 조율 확정 — 5개 불일치 전부)
1. **snapshot 언랩 → 평면**: `assembleRequestBody`(ingest.js) 가 `snapshot.{query_condition, request_fingerprint, dom_selection_hash, api_captured_at, dom_snapshotted_at, selected_coverages, per_insurer, validation}` 를 **최상위 평면**으로 낸다. `snapshot`/`reference` 래퍼 키 제거.
2. **reference 언랩 + reference_type 매핑**: `reference:{reference_type, reference_id}` → 최상위 `reference_type`/`reference_id`. **`matched|created` → `"CUSTOMER"` 로 매핑**(content.js reference_type 산출부 ~1947-1955). id 는 그대로.
3. **consent 객체 캡처(신규 — DATA GAP)**: content.js 동의 흐름(~1859-1865)이 현재 `consent:true` bool 만 보냄. **ConsentPayload 객체** 생성: `granted_at`=동의 시각(AwareDatetime ISO8601, tz 포함) · `consent_version`=상수(예: "v1") · `scope`=상수(예: "consultation_history_ingest") · `source`=상수(예: "extension_popup"). 이 객체를 조립부에 전달 → 최상위 `consent` 로. (bool 아님)
4. **analysis_matrix V2 형태**: content.js analysis_matrix 산출(~1741-1761)을 서버 `AnalysisMatrix`(schemas ~193-252) 형태로 정합. `contract_version="OHMY_PPD_V2"` 이므로 analysis_matrix **필수**(없으면 서버 거부).
5. **contract_version**: `INGEST_CONTRACT_VERSION` 을 서버 리터럴 **`"OHMY_PPD_V2"`** 로(현재 "V2" bare → 거부됨).
6. **idempotency_key**: 서버는 문자열 필드로 받음. 현재 랜덤(ingest.js ~77-86,151) — 재시도 replay 를 위해 **동일 논리요청=동일 key** 보장 확인(캡처 시점 1회 생성·storage.session 고정, 재직렬화 0). reshape 후에도 frozen bytes 재전송 규율 유지.
7. **taint-guard 보존**: opaque 래퍼 unwrap 규율(ingest.js ~88-110,145-149,161-192) 그대로 — 평면화해도 PII 는 unwrap getter 로만, canary sink 0 유지.

## 골든 계약테스트 (파일럿 게이트 — 신설)
8. **골든 fixture** `tests/contract/ohmy_ppd_v2.golden.json` = 정합 후 `assembleRequestBody(대표입력)` 실제 산출 bytes(합성 PII·마스킹, 실고객 0).
9. **확장측**(`extension/__tests__/contract-golden.test.ts`): `assembleRequestBody(대표입력)` == 골든 assert.
10. **서버측**(`server/tests/test_contract_golden.py`): 골든 bytes → `ConsultationHistoryV1Request.model_validate` → **파싱 성공(0 error)** + `FORBIDDEN_CLIENT_FIELDS`·extra 충돌 0 assert. ← 이게 lock. 이후 드리프트 CI 자동 차단.

## 기존 테스트 재작성 (현재 틀린 snapshot 형태 assert 중)
11. `extension/__tests__/prd1-ingest-core.test.ts`(~186-207 tripwire, ~327-336 consent=true, ~350-359 mutation canonical.consent) → **flat + consent 객체** 형태로 수정.
12. `extension/__tests__/prd2-crm-ui.test.ts`(~353-374 reference) → reference_type CUSTOMER 반영 수정.

## 검증 (전부 통과)
- `npx vitest run extension/__tests__/` 전체 PASS(재작성 포함, 회귀 0) — 특히 골든·consent·reference·single-egress·canary sink-spy.
- `pytest server/tests/test_contract_golden.py` PASS(서버가 확장 산출 bytes 파싱 성공).
- 대표입력 왕복 실증: `assembleRequestBody` 출력 → `ConsultationHistoryV1Request.model_validate` → **0 error**(12→0).
- taint-guard canary sink 0 유지, PII 골든에 0.

## 완료 (★ 순서 B 종결)
- 변경 = allowed_resources 내부만. manifest/crm.js/서버스키마 불변.
- **dev6 금지** — dev2. 커밋 → push → finish-task **foreground 1회** → .done 즉시 세션종료. **background+terminal/callback-wait 금지(좀비).** ANU 원격 독립검증·머지.

## goal_assertions (auto-generated)
- `npx vitest run extension/__tests__/`
- `pytest server/tests/test_contract_golden.py`
