# task-2880 보고서 — [Lv.4] 생보 3대질병 Phase A: 확장 생보 캡처 + 서버 life_matrix 계약

## Situation
손보 복합설계 저장 계약에 **생보 3대질병 진단비(암·뇌혈관질환·허혈성심장질환)** matrix 비교를 얹는다. Phase A = **캡처·저장 계약**까지(표시·차액계산은 Phase B 별도). 생보 캡처는 손보와 완전 동일 — 오마이매니저 상단 생손보유형 드롭다운(`#selInsuranceType`, F=손보/LF=생보) 값만 다르다.

## Complication
- 손보=`analysis_matrix`, 생보=`life_matrix` **별도 슬롯**(병합 절대 금지, 회장 설계 결정).
- 클라이언트가 손보+생보를 **한 페이로드**로 저장(단일 idempotency/reference/consent, 사후매칭 금지).
- **골든 계약(확장 OHMY_PPD_V2 ↔ 서버) 원자적 동기** 필수.
- 스코프 제약: `extension/background.js`(메시지 라우터)와 `extension/manifest.json`은 allowed 밖.

## Question
allowed_resources 내에서, ① 확장이 생손보유형을 읽어 슬롯 라우팅 + 단일 페이로드 누적하고 ② 서버가 life_matrix를 Optional로 수용하며 ③ 기존 손보 골든/테스트를 회귀 없이 보존하려면?

## Answer (구현 요약)

### A. 확장 (프레이야) — 생보 캡처 라우팅 + storage.session 누적
- `extension/content.js` (+185/-5):
  - `isSelInsuranceTypeLife(doc)` — `#selInsuranceType`를 기존 `normalizeInsuranceType`으로 정규화해 LF(생보) 판정하는 순수 함수.
  - `captureLifeMatrixToSession(doc)` — LF일 때만 `scrapeMmlfcpMatrix()` **재사용**(신규 스크래퍼 0, 담보명 하드코딩 0)해 `{insurers, coverages}`를 `chrome.storage.session`(전용 키 `insuroLifeMatrixAccumulatorV1`, `INGEST_STORAGE_KEY`와 충돌 없음)에 누적.
  - "🧬 생보 3대질병 이어서 캡처" 버튼(명시 클릭에서만 캡처, 조용한 자동 캡처 금지) + 사용자 노출 문구(에러코드 노출 금지 관례).
  - `handleConsentSaveClick`을 async 전환 — 저장 직전 누적 life_matrix가 있으면 `snapshot.life_matrix`로 **조건부 첨부** 후 즉시 clear(소비 후 재사용 방지). `hasSessionStorageApi()` 가드로 구 sync mock 테스트 회귀 0.
- `extension/background/ingest.js` (+10):
  - `assembleRequestBody`에서 `snapshot.life_matrix`를 `analysis_matrix`와 동일 taint-guard(`createOpaqueValue`)로 감싸고, **null/undefined면 canonical 키 자체 생략**(payment_years 관례) → 손보-only 페이로드 byte 불변.
  - ★ **background.js 무수정 유지**: background.js가 `snapshot` 전체를 그대로 `assembleRequestBody`에 전달하므로 `snapshot.life_matrix`가 별도 배선 없이 통과.

### B. 서버 (토르) — life_matrix 계약 수용
- `server/schemas/consultation_history_v1.py` (+33): 최상위 요청에 `life_matrix: AnalysisMatrix | None = Field(default=None)` 추가(**AnalysisMatrix 재사용**, 신규 클래스 0). cross-field validator 확장 — V1→life_matrix도 반드시 None(frozen 보존), V2→optional(손보-only 정상). request_fingerprint 4요소 불변.
- `server/migrations/016_consultation_history_life_matrix.sql` (신규): `ADD COLUMN IF NOT EXISTS life_matrix JSONB`(014 analysis_matrix와 동형, additive, 신규테이블 0) + ok_only 뷰에 life_matrix 추가(WHERE 절 불변). ★ **015는 이미 존재**(rls_route_only)하여 **016으로 조정**.
- `server/routes/consultation_history_v1.py` (+6): `_ALLOWED_RESPONSE_FIELDS`에 life_matrix 추가(GET/LIST 응답), INSERT persist(analysis_matrix와 동일, 별도 슬롯). **ok_only/record_status 필터는 손보(overall_status) 기준 그대로** — life_matrix 검증 실패가 손보-OK 레코드를 숨기지 않음.

## 생성/수정 파일
- `extension/content.js` (수정, +185/-5)
- `extension/background/ingest.js` (수정, +10)
- `extension/__tests__/life-matrix-routing.test.ts` (신규, 24 tests)
- `server/schemas/consultation_history_v1.py` (수정, +33)
- `server/routes/consultation_history_v1.py` (수정, +6)
- `server/migrations/016_consultation_history_life_matrix.sql` (신규)
- `server/tests/test_consultation_history_life_matrix.py` (신규, 22 tests)

## 테스트 결과
- **확장 vitest**: `Test Files 23 passed / Tests 498 passed` (golden 포함, 회귀 0).
- **서버 pytest**: `898 passed, 1 failed` — 신규 22건 포함(baseline 876 → 898, 정확히 +22, 기존 763+ 감소 0).
  - 골든: `test_contract_golden.py` **3 passed** (life_matrix None → Optional 통과, 재생성 0).
  - 신규: `test_consultation_history_life_matrix.py` **22 passed**.
- **골든 재생성 회피**: 성공. life_matrix null 시 canonical 키 생략 → `tests/contract/ohmy_ppd_v2.golden.json` byte 불변. 확장/서버 골든 파일 수정 0.

## 발견 이슈 및 해결
- **pytest 1건 실패(`test_cors_fail_closed_when_ext_origin_unset`)는 회귀 아님 — pilot .env 오염 아티팩트로 실증 확정**:
  - clean base 워크트리(be40a02, .env 없음) → **PASS**
  - task 워크트리(pilot .env 복사됨) → FAIL
  - clean base + 동일 pilot .env → **동일 FAIL** (apples-to-apples: task-2880 코드 없이도 재현)
  - 해당 테스트 파일은 task-2880 diff에 없음(우리 미수정, diff로 대조). 원인 = worktree_manager가 복사한 pilot .env가 EXT origin을 set → "ext_origin_unset" 전제 위반. **memory doctrine feedback_regression_verify_clean_worktree_not_polluted_260726 정확히 일치.**
- **교차 shape 정합 확인**: `scrapeMmlfcpMatrix().matrix.{insurers[{code,name}], coverages[{cd,name,amount,premiums:{code→값}}]}`가 서버 `AnalysisMatrix`(MatrixInsurer{name,code}/MatrixCoverage{cd,name,amount,premiums}) 계약과 정합. premiums 키=insurer code로 서버 validator 통과. life는 골든 검증된 손보 scrape를 재사용하므로 shape 신뢰 가능.

## 미해결 / Phase B·QA 이관
- **생보 셀렉터 live 확인 미실시**: 실 오마이매니저 생보 화면 접속 불가(자격증명/네트워크 부재). 대체 검증 = `isSelInsuranceTypeLife` LF/F/부재/대소문자 단위테스트 + 기존 `matrix-detection.test.ts`의 실 DOM fixture(`#selInsuranceType` value="LF" 포함). 재사용 함수(scrapeMmlfcpMatrix)·판정 헬퍼(normalizeInsuranceType) 모두 기존 실증 셀렉터 → 신규 회귀 위험 낮음. **헤임달/실 브라우저 라이브 QA 권장(ANU 판단)**.
- **생보 포함 결합 골든 미추가**: 골든 재생성 회피(None 생략)로 인해 손보+생보 결합 골든은 없음. 서버 life_matrix 파싱 테스트 + 확장 canonical 조립 테스트로 각각 검증됨. 결합 골든 추가는 Phase B 검토.
- **supabase/migrations 미러 미생성**: allowed_resources(`server/migrations/**`) 밖. 014/015는 미러 존재 → 배포 시 016 미러 필요 여부 ANU 판단.
- **owner 전환 시 storage.session 잔존 life_matrix 엣지케이스**: `invalidatePreviewState`와 미연동(Phase A 최소범위). 계정 전환 시 잔존 가능 — Phase B 검토.

## 머지 판단
- **머지 필요**: Yes (단, merge_policy=none → 머지는 **ANU** 권한)
- **브랜치**: `task/task-2880-dev2` (커밋 `6cccc3f`, base be40a02)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2880-dev2`
- **머지 의견**: allowed_resources 내부만 변경(background.js/manifest 무수정 실증). vitest 498/pytest 898 PASS, 유일 실패는 pilot .env 오염(base 동일 재현으로 회귀 0 실증). 골든 재생성 회피 성공. 양측 life_matrix shape 정합. 생보 live 확인은 라이브 QA로 이관 권장.

## L1 스모크테스트 결과
- 서버 재시작: 해당없음(스키마/계약 additive — Phase A는 캡처·저장·계약 수용까지, 실 서버 fire는 Phase B 배포 대상). 대신 계약 정합을 **스키마 레벨 실행 검증**으로 대체:
  - API 응답 확인: 해당없음(live 저장 흐름은 Phase B). 대신 `ConsultationHistoryV1Request.model_validate` 실행 테스트로 life_matrix 있음/없음/V1거부 22건 실행 PASS(test_consultation_history_life_matrix.py).
  - 확장 canonical 조립: `assembleRequestBody` 실행으로 life_matrix 조건부 결선 + 골든 byte 불변 실행 검증(life-matrix-routing.test.ts 24건 + contract-golden.test.ts 7건 실행 PASS).
  - 스크린샷: 해당없음(생보 화면 live 접속 불가 — 위 "생보 셀렉터 live 확인 미실시" 참조).
- 판정: 코드 실행 기반 계약 검증(vitest 498 + pytest 22 신규) 실제 실행 통과. 실 브라우저/실 서버 fire는 Phase A 범위 밖(캡처·계약까지) — 라이브 E2E는 Phase B/헤임달 QA로 이관.

## 모델 사용 기록
- 토르(백엔드): sonnet — 서버 스키마/routes/migration/tests.
- 프레이야(프론트): sonnet — 확장 content.js/ingest.js/tests.
- 팀장(오딘, opus): 직접 코딩 없음. 구조 파악·설계·통합·검증(스코프/골든/shape 정합/pre-existing 독립 실증)만 수행.

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


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


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

