# task-2848 보고서 — G-3 크로스레이어 계약 테스트 (골든 fixture)

- 작업 ID: task-2848
- 팀: dev3-team (다그다)
- 레벨: Lv.3 / test-only / merge_policy=none
- 상태: **BLOCKED — P0 크로스레이어 계약 드리프트 실측 확증. 회장/아누 결정 필요.**
- 결론: **task 명세대로는 정직하게 완료 불가.** 명세 step 3("골든 fixture → 서버 파서 → 파싱 성공 assert")이 **현 origin/main에서 경험적으로 거짓**. 이 task가 잡으려던 "치명 갭"이 **실재하며 이미 깨져 있음**을 증명함.

---

## S (Situation)
InsuRo 복합설계+CRM 연동(consultation-history v1 ingest)이 main 머지 완료, 파일럿 직전. 헤임달(보안QA) 미팅에서 최대 갭으로 지목된 것:
> 확장이 실제 조립하는 요청 bytes ↔ 서버 파서(`parse_and_validate_body`) 정합을 검증하는 실행 테스트가 **어느 계층에도 없음.** 확장 suite·서버 suite 가 각자 "계약 이해"로 그린일 뿐. → "이게 없으면 파일럿 금지."

본 task는 그 크로스레이어 계약 테스트를 골든 fixture로 만들어 CI에서 드리프트를 잡게 하는 것.

## C (Complication)
계약 테스트 설계를 위해 **양쪽 실물을 origin/main에서 직접 실측**한 결과, 확장 조립 출력과 서버 파서 스키마가 **구조적으로 비호환**임을 확인함. 즉 정합을 "고정(lock)"하려던 테스트가, 정합이 **애초에 존재하지 않음**을 드러냄.

### 실측 재현 (읽기 전용, 소스 변경 0)
1. origin/main `extension/background/ingest.js`의 실제 `assembleRequestBody(대표 model-A 입력)` 호출 → 실제 요청 bytes(canonical) 산출.
2. 그 bytes를 origin/main `server/schemas/consultation_history_v1.py`의 `ConsultationHistoryV1Request.model_validate(...)`(= 라우트 `parse_and_validate_body`가 호출하는 바로 그 함수)에 그대로 투입.

### 결과
- 확장 산출 canonical 최상위 키: `['analysis_matrix', 'consent', 'contract_version', 'idempotency_key', 'reference', 'snapshot']`
- 서버 파싱: **PARSE FAILED — 12 validation errors.**

| 확장이 보내는 것 | 서버가 기대하는 것 | 오류 |
|---|---|---|
| `contract_version: "V2"` (envelope 버전) | `Literal["OHMY_PPD_V1","OHMY_PPD_V2"]` | literal_error |
| `reference: {reference_type, reference_id}` (객체) | 최상위 `reference_type` + `reference_id` (평면) | missing ×2 + extra_forbidden(`reference`) |
| `snapshot.{query_condition, request_fingerprint, dom_selection_hash, api_captured_at, dom_snapshotted_at, validation, ...}` (중첩) | 최상위 평면 필드 | missing ×6 + extra_forbidden(`snapshot`) |
| `consent: true` (boolean) | `ConsentPayload` 객체(granted_at/consent_version/scope/source) | model_type |
| `analysis_matrix` (최상위, 이건 형태 OK) | 최상위 `analysis_matrix` | extra_forbidden(모델이 extra="forbid"라 나머지 미인식 필드와 함께 거부) |

추가로, 서버 **라우트 핸들러**는 Pydantic 이전 단계(step ③)에서 `raw_data.get("reference_type")`/`reference_id`를 **최상위**에서 읽어 없으면 `REFERENCE_SHAPE_INVALID`(400)로 선제 거부한다. 확장은 이를 `snapshot`이 아닌 `reference` 객체 안에 넣으므로, **Pydantic에 도달하기도 전에 400**으로 떨어진다. 라우트는 `raw_data.get("snapshot")`을 **어디서도 언랩하지 않는다**(grep 확인).

### 이것이 stale/미머지 문제가 아님을 교차 확인
- 동일 엔드포인트: 확장 `INGEST_PATH` == 라우트 `@router.post(INGEST_PATH)` == `/api/insuro/consultation-history/v1`. (다른 엔드포인트로 가는 것 아님 — 확장의 single egress choke는 이 경로 하나.)
- 확장 브랜치 전수 확인(`task-2831-dev1`, `origin/task-2836-dev1`=PR-D2b, `task-2843-dev2`, `task-2847-dev2`): **전부 동일하게** `snapshot` 래핑 + `consent: consent === true`(bool) + `contract_version: INGEST_CONTRACT_VERSION("V2")`. 정합된 평면 envelope를 내는 브랜치는 **하나도 없음.**
- 즉 드리프트는 origin이 뒤처져서가 아니라 **코드베이스 전반에 체계적으로 존재**한다.

### 왜 지금까지 안 잡혔나 (미팅 진단과 정확히 일치)
확장 vitest는 `assembleRequestBody`가 "확장이 이해한 계약(snapshot envelope)"을 낸다고 그린, 서버 pytest는 "서버가 이해한 계약(flat envelope)"을 받는다고 그린. **두 계층이 서로 다른 계약을 각자 이해한 채 각자 그린.** ingest flag가 OFF라 실 왕복이 한 번도 없었으므로 프로덕션에서도 안 터짐. 파일럿에서 flag ON 하는 순간 **첫 저장 요청부터 400/422로 전량 실패**할 상태.

## Q (Question)
test-only(소스 로직 변경 금지) 범위에서 "골든 fixture → 서버 파싱 성공 assert" 테스트를 정직하게 통과시킬 방법이 없다:
- 골든 fixture 수기 작성 금지(명세) — 실제 함수 산출물이어야 함 → 실제 산출물은 파싱 실패.
- 서버 라우트/스키마/확장 로직 변경 금지(범위) → 형태를 맞출 수 없음.
- 파싱 성공을 assert하는 테스트를 쓰면 **거짓 그린**(표면 통과 ≠ 동작). QC 원칙 위반.

## A (Answer / 권고)
**팀에 코딩 위임을 보류하고 회장/아누께 에스컬레이션.** (거짓 그린 테스트를 짜게 하는 것은 부적절. 소스 정합은 test-only 범위 밖이며 팀장 직접코딩 금지 원칙에도 해당.)

### 권고 조치 (택1, 아누 조율)
1. **[권장] 정합 remediation task 신설(선행)** — 확장 `assembleRequestBody` 출력 형태 ↔ 서버 envelope를 **한쪽 canonical로 통일**. 계약 단일소스: `docs/contracts/OHMY_PPD_DATA_CONTRACT_V1.md` + `INSURO_LV4_PROGRAM_CONTRACTS_V1.md`, `memory/plans/insuro-composite-design/prD1-ingest-contract-260722.md`. 어느 쪽이 진실 계약인지(평면 envelope vs snapshot envelope)는 **회장/아누 판정 사항**. 정합 완료 후 **본 task-2848의 골든 계약 테스트를 그 위에 얹어 lock** → 이후 드리프트는 CI가 잡음.
2. **[대안] 본 task를 "드리프트 sentinel"로 재정의** — 확장측 테스트(assembleRequestBody==golden)는 그대로 PASS로 두고, 서버측 테스트는 "**현재 확장 출력은 서버가 거부한다**(파싱 실패 + 정확한 12 불일치)"를 assert하는 실패-감시 테스트로 명시 작성. 정합 remediation 머지 시 이 테스트가 뒤집히도록 xfail/TODO 표식. → 단, 이는 명세 step 3(파싱 성공 assert)과 반대이므로 **명세 개정 승인 필요**(D-SPEC-EXACTNESS).

두 경로 모두 **파일럿은 정합 remediation 전까지 hard-block** 되어야 함("이게 없으면 파일럿 금지" → 이제 "정합 깨짐 확증"이므로 파일럿 금지 유지).

---

## 생성/수정 파일
- 소스 변경 **없음**(진단 전용). 실측 재현 스크립트는 `/tmp/task2848_probe/`(throwaway, 워크스페이스 밖 — read-only 검증용).
- 본 보고서만 생성.

## L1 스모크테스트 결과
- 서버 재시작: 해당없음(코드 변경 0, 진단 전용)
- API 응답 확인: **로컬 스키마 검증으로 대체** — 실제 `assembleRequestBody` 산출 bytes를 `ConsultationHistoryV1Request.model_validate`에 투입 → 12 validation errors 관측(위 표). 이것이 서버 라우트 ④단계와 동일 경로.
- 스크린샷: 해당없음(백엔드 스키마 검증)

## 인지 검증 메모
- 추측 아님: 양쪽 실물을 origin/main에서 직접 실행/투입해 관측. `assembleRequestBody`가 POST 본문 빌더임도 코드 경로로 확인(`sendIngest`가 stored blob bytes=조립 bytes를 재직렬화 0으로 그대로 POST).
- 대안 가설 배제: (a)중간 변환 계층 — `sendIngest`/라우트 모두 변환 없음 확인. (b)다른 엔드포인트 — 경로 동일 확인. (c)stale origin — 확장 브랜치 전수 동일 구조 확인.

## 완료 규약
- **.done 생성 안 함**(blocker). finish-task.sh 미실행. PR 미생성.
- 회장/아누 결정 후: 권고 1 채택 시 remediation task 선행 → 본 task 재개.

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

