# task-2968 — InsuRo 증권분석 Phase 3 MVP (신규설계 비교)

- **팀**: dev2-team (오딘)
- **일자**: 2026-08-17
- **레벨**: Lv.3 / critical
- **브랜치**: `task/task-2968-dev2` (base `923944c9`)
- **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2968-dev2`
- **머지**: ★ **HOLD** — ANU 검증 + 회장 보고 후 (task 완료조건 명시)

---

## S (상황)

Phase 2(담보 grouping 지식DB 5테이블 + confirmed-only 게이트)가 프로덕션 배포 완료된 상태에서,
설계사 전용 **신규설계 비교** 기능을 신설하는 Phase 3 MVP. codex↔ANU 설계 합의 + 회장 Go.
원칙은 confirmed-only(정확도 우선)와 금소법(추정을 사실로 오도 금지).

## C (문제)

구조 선행 파악에서 task 전제를 흔드는 사실 2건이 나왔다.

1. **담보별 보험료는 "없는" 게 아니라 "버려지는" 것** — `CoverageRecord.premium_won`
   (`server/policy_extract/models.py:38`)은 vision/textlayer 추출 양쪽에서 실제로 채워지지만
   (`vision.py:437`, `textlayer.py:427/455/646`), `policy_normalizer.build_analysis_table()`
   (`policy_normalizer.py:407-413`)이 `amount_manwon`만 읽고 **구조적으로 폐기**한다.
   `CoverageRow`는 7필드뿐이고 보험료 필드가 아예 없다.
2. **기존 코드에 이미 "추정 금지" 규약 존재** — `CoverageAnalysisTable.tsx:5-16`이 금소법 근거로
   "추정 금지, 없으면 정직하게 `-`/`UNKNOWN`"을 명문화. task의 "추정치 노출" 지시와 표면상 충돌.

## Q (질문)

담보별 기존 보험료를 추정할 것인가, 만들지 않을 것인가?

## A (답변/조치)

**"추정 배관은 만들되, 근거 없는 수치는 만들지 않는다."**
`premium`을 객체(`value_won`/`is_estimate`/`granularity`/`basis`/`summable`)로 반환해 사실↔추정을
**라벨이 아니라 구조로** 구분했다. MVP 입력에는 배분 근거가 없으므로 담보별은 `UNAVAILABLE`,
보험료는 **상품 단위 사실값**만 표기한다. 이는 task의 "배분 불가 시 coarse/상품 단위로만 표기" 분기와 일치한다.

**기각한 대안 — 가입금액 비례 배분**: 만원당 보험료가 담보 종류별로 수배~수십배 차이(사망 vs 진단비 vs 실손)라
정밀해 보이는 허위 수치를 생산한다. task의 "억지 배분 금지"에 직접 위배 → 구현하지 않음.

---

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/server/new_design_comparison/models.py | 요청/응답 Pydantic 모델, EstimatedPremium, group_label_ko | grep "EstimatedPremium" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/server/new_design_comparison/linear_scale.py | 선형환산 어댑터 + 거부조건 7종 + 경고 2종 | grep "BASELINE_AMOUNT_UNKNOWN" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/server/new_design_comparison/engine.py | 게이트 위임 + group_id 조인 + cells 검증 + label_ko | grep "gate_for_official_comparison" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/server/new_design_comparison/__init__.py | 패키지 초기화 | grep "new_design_comparison" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/server/routes/new_design_comparison_v1.py | 라우터 (silson 패턴) | grep "NEW_DESIGN_COMPARISON_PATH" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/server/main.py | import 1줄 + include_router 1줄 (총 2줄 삽입) | grep "new_design_comparison_v1_router" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/server/tests/test_new_design_comparison.py | 유닛 38건 | pytest 38 passed | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/src/pages/NewDesignComparison.tsx | 페이지 + 입력 + 토글 | grep "new-design-comparison-page" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/src/components/policy/NewDesignComparisonTable.tsx | 비교표 + 비교불가 리포트 | grep "official-comparison-table" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/src/config/routes.ts | lazy import + 라우트 엔트리 (분석&도구) | grep "/new-design-comparison" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/docs/new-design-comparison-contract.md | API 계약 (확정 문서) | grep "confirmed-only" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2968-dev2/docs/new-design-comparison-ux-rules.md | 금소법 표기 규칙 (미미르) | grep "추정치" OK | verified |

planned 항목 0건.

---

## 테스트 결과

| 항목 | 수치 | 근거 |
|---|---|---|
| 신규 유닛 | **38 passed** | `pytest tests/test_new_design_comparison.py -q` |
| 전체 회귀 | **2074 passed, 1 failed, 2 skipped** | `pytest tests/ --ignore=gdrive_sync --ignore=ingest_fcpa` |
| task-2968 기인 신규 실패 | **0건** | 아래 인과 실험 |
| npm run build | **PASS** (`✓ built in 12.21s`) | `dist/assets/NewDesignComparison-*.js` 청크 생성 |
| Phase 2 로직 변경 | **0줄** | `git diff 923944c9..HEAD -- policy_normalizer.py comparison_gate.py grouping_bridge.py` → 빈 출력 |

### 회귀 실패 1건 규명 (인과 실험으로 확정)
`test_cors_fail_closed_when_ext_origin_unset` 1건 실패. **task-2968 코드와 무관**하며 원인은
worktree 로컬 `.env`(git 미추적, `worktree_manager`가 복사) 오염이다.

- `.env`에 `INSURO_EXTENSION_ORIGIN` 존재 → `main.py`의 `load_dotenv()`가 파일에서 읽어
  테스트의 env 격리(`env.pop`)를 무력화
- `env -u INSURO_EXTENSION_ORIGIN` 으로는 **여전히 실패** (환경변수가 아니라 파일이 원인)
- **`.env` 파일을 치우고 재실행 → `1 passed`**, 복원 후 다시 실패 → 인과 확정
- 헤임달이 base 커밋 clean worktree에 같은 `.env`를 넣어도 동일 실패함을 별도 확인

> 정정: 최초 "pre-existing" 표현은 부정확하다. 정확히는 **환경오염이며 base에서도 재현된다**.

---

## L1 스모크테스트

- **서버 재시작**: 성공 — worktree 코드로 uvicorn 기동 (포트 8099/8098, 프로덕션 8001 무영향 확인)
- **API 응답 확인**:
  - 라우트 등록: `openapi.json`에 `/api/insuro/new-design-comparison` (POST) 존재
  - 무인증 호출 → **HTTP 401** (인증 게이트 정상 작동)
  - 인증 스텁 후 실제 HTTP POST → **HTTP 200 (0.005s)**, 응답 키 `official_comparison`/`unknown_report`/`meta`
  - ★ 실제 응답: `official rows: 0 / unknown rows: 3`, counts `UNKNOWN:3` — 아래 **미해결 1** 참조
  - "누락 0건" 불변식 실측 성립: 입력 담보 3건 = 제외 3건
  - ※ 인증만 스텁(`verify_jwt`/`_verify_incar_member`). JWT_SECRET 부재 + Supabase 프로필 조회 불가 환경이라
    실토큰 발급 불가. 라우팅/Pydantic 검증/엔진/직렬화는 실제 ASGI 스택을 그대로 통과했다.
- **스크린샷** (★ 성격을 반드시 구분해서 볼 것):
  - `/home/jay/workspace/teams/dev2/task-2968-01-official-comparison.png`
  - `/home/jay/workspace/teams/dev2/task-2968-02-unknown-report.png`
  - `/home/jay/workspace/teams/dev2/task-2968-03-estimate-label.png`
  - ★★ **이 3장은 실 백엔드 왕복이 아니라 Playwright route-intercept 픽스처로 렌더한 UI 검증이다.**
    현재 백엔드는 (a) CONFIRMED 행을 만들지 못하고(미해결 1) (b) `is_estimate=true`를 설계상 생성하지 않으므로,
    UI가 그 상태들을 올바로 렌더할 수 있는지는 픽스처로만 검증 가능하다. 의도적 선택이며,
    **실 API 왕복 증거는 위 curl 200 (official rows 0)** 이 별도로 존재한다.
    (독립 검증관 마아트가 이 불일치를 지적했고, 지적이 타당하여 표기를 분리했다)
  - 실 브라우저에서 확인된 것: 라우트 접근, AuthGuard 통과, 표 렌더, 토글 2종 동작,
    추정치 뱃지, `미가입` 렌더, 비교불가 리포트 3행 + 사유 문구, disclaimers 3종
  - 콘솔 에러 15건은 전부 가짜 세션으로 인한 Supabase 401(사이드바/구독 조회)이며 신규 페이지와 무관
- **브라우저 정리**: `browser_close` 완료, 잔존 Playwright 렌더러 0 (남은 chrome 2개는 유휴 MCP 서버, 0% CPU)

---

## 게이트

- **G1 Codex 사전 검증**: **PASS** (`pass:true`, critical=False, risks 6). 결과: `/home/jay/projects/InsuRo/memory/events/task-2968.codex-gate`
  - HIGH 4종을 계약서에 반영: 다건 증권 열 귀속 명시 / CONFIRMED-modality 별도 사유 / 추정치 합계·정렬 금지(`summable`) / 선형환산 적용가능성 가드
- **G2 마아트 독립 검증**: **NEEDS WORK** → 지적 2건 수정 후 재확인 (아래)
- **sanitize**: 외부 AI(Codex)에 전달한 것은 task 파일 + 설계 요약. PII(주민번호/연락처/API키/계좌/증권번호) 미포함.
  L1 페이로드의 피보험자명은 `홍**` 마스킹 값 사용. PII net-new 0건.

### 마아트 지적 처리
| 지적 | 판정 | 조치 |
|---|---|---|
| 미선언 `column_id`가 `cells`에 있으면 조용히 소실 (대원칙 3 위반) | 타당 | 400 `UNKNOWN_COLUMN_ID`로 명시 거부. 테스트 2건 추가 |
| NEW_ONLY 행 담보명에 내부 `group_id` 원문 노출 (금소법) | 타당 | 응답에 `group_label_ko` 추가(taxonomy 읽기 전용), 프론트 폴백을 한글 라벨→"미확인 담보"로 교체 |
| L1 스크린샷이 실 API 응답과 모순 | **타당 (팀장 기록 결함)** | 픽스처 렌더임을 본 보고서·체크리스트에 명시 분리. 실 왕복 증거(curl 200)를 별도 제시 |
| E1 심각도가 "대부분"이 아니라 "100%" | 타당 | 아래 미해결 1에 100%로 정정 반영 |

---

## 발견 이슈 및 해결

| # | 이슈 | 상태 |
|---|---|---|
| 1 | 원본 `main.py:7906-7935`는 baseline<=0에서 보험료를 **0으로 폴백** — "공짜"라는 거짓 진술 | 해결 — 신규 어댑터는 `None` + `BASELINE_AMOUNT_UNKNOWN`. 테스트로 `!= 0` 명시 assert |
| 2 | 같은 `group_id`에 신규 담보 2건 이상 매칭 시 두 번째가 누락될 뻔함 | 해결 — 개별 entry 단위 매칭으로 수정. 3건 동시 매칭 테스트로 검증 |
| 3 | `target_amount_manwon == 0` → "0원" 표시가 "무료" 오인 유발 | 해결 — `TARGET_AMOUNT_ZERO_NOT_ENROLLED` 경고 + UI에서 `미가입` 렌더 |
| 4 | 미선언 `column_id` 셀 데이터 조용한 소실 | 해결 — 400 거부 (마아트 지적) |
| 5 | 내부 코드명(`TX_SURGERY`) 담보명 노출 | 해결 — `group_label_ko` 한글 라벨 (마아트 지적) |
| 6 | 회귀 실패 1건의 "pre-existing" 프레이밍 부정확 | 해결 — 인과 실험으로 `.env` 오염 확정, 표현 정정 |
| 7 | L1 스크린샷 성격을 체크리스트에 미표기 | 해결 — 픽스처/실왕복 분리 표기 |

### ★ 미해결 1 (범위 외 — 회장·ANU 판단 필요): 기존 증권 담보가 CONFIRMED에 **100% 도달 불가**

`CoverageRow`에는 `clause_id`도 `axes`도 없다(7필드 전수 확인, grep 0건). `lookup_group()`은 둘 중
하나가 있어야 confirmed/learned_rule/auto_map 경로에 도달하며, 없으면 **경로③ cold start →
`status="unclassified"` → `UNKNOWN`** 으로 결정론적으로 고정된다.

- 따라서 기존 증권 측 담보는 지식DB 내용과 **무관하게** 공식 비교표에 절대 들어가지 못한다.
- 실측: L1 curl 응답이 `official rows: 0`, 전 항목 `UNKNOWN` 제외였다.
- 마아트가 CONFIRMED 매핑을 인위 시드해도 existing 측은 여전히 제외됨을 독립 재현했다.
- 결과적으로 실사용 시 공식비교표는 사실상 **신규 견적 단독 나열**에 가깝다.

**왜 이번에 못 고치는가**: 해결하려면 `analysis_table`에 `clause_id`/`axes`를 실어야 하고, 이는
`policy_normalizer.build_analysis_table()` 수정을 요구한다. task 지시가 "증권분석/Phase2 로직 무변경"으로
명시적으로 금지한 범위다. 임의 확장하지 않고 보고한다.

**제안 (Phase 3.1)**: `analysis_table`에 담보별 `clause_id` 배선. 이걸 하면
(a) existing 측 CONFIRMED 도달이 가능해지고, (b) `CoverageRecord.premium_won`도 함께 실어주면
**담보별 기존 보험료 추정 자체가 불필요**해진다. 본 API는 그 배선이 생기면 스키마 변경 없이 받도록
`existing_premium_hints` 통로를 이미 열어두었다.

### 미해결 2 (경미): PolicyAnalysis → 비교화면 데이터 인계 배선 없음
현재는 JSON 붙여넣기 입력. 저장된 분석 이력을 선택해 넘기는 UI 흐름은 신규 기능이라 MVP 범위에서 제외했다.
(`ConsultationHistoryView`의 `?record=` 딥링크 패턴이 선례로 활용 가능)

---

## trip-wire 5종 실측

| 항목 | 실측값 |
|---|---|
| Critical7 | 0 |
| PII net-new | 0 |
| 회귀 실패 (task-2968 기인) | 0 |
| forbidden_paths 침범 | 0 (`src/**`·`server/**` 외 변경 없음, Phase2 3파일 diff 0줄) |
| nonce | task-2968 |

---

## 머지 판단

- **머지 필요**: Yes — 단 **HOLD** (task 완료조건: ANU 검증 + 회장 보고 후)
- **브랜치**: `task/task-2968-dev2`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2968-dev2`
- **머지 의견**: 코드 품질·계약 준수·회귀 안전성은 확보됐다(회귀 0, 빌드 PASS, Phase2 무변경,
  적대적 검증에서 대원칙 위반 1건 발견 후 수정 완료). 다만 **미해결 1** 때문에 지금 머지·배포해도
  설계사가 얻는 실사용 가치는 제한적이다. "코드는 머지 가능, 기능은 Phase 3.1 배선이 있어야 발현"이
  정확한 상태다. 머지 여부와 Phase 3.1 착수 여부를 함께 판단해 주시기를 요청한다.

## 모델 사용 기록

| 팀원 | 모델 | 비고 |
|---|---|---|
| 토르 (백엔드) | sonnet | API/엔진/어댑터/테스트 |
| 프레이야 (프론트) | sonnet | 라우트/페이지/표 |
| 미미르 (UX) | sonnet | 금소법 표기 규칙 문서 (전략/문서 작업이라 haiku 금지 규칙 준수) |
| 헤임달 (QA) | sonnet | 회귀 독립재현 + 적대적 검증 |
| 마아트 (독립검증) | sonnet | critical 레벨 필수 |
| 구조 파악 explorer ×2 | sonnet | 읽기 전용 |
| 오딘 (팀장) | opus | 설계/계약 고정/게이트/L1/통합 — 직접 코딩 0 |

haiku 미사용.

---

## PR 상태 (★ 미완 항목 — 정직 보고)

**PR 미생성.** task 완료조건의 "PR"을 충족하지 못했다.

- 로컬 브랜치 `task/task-2968-dev2` 에 커밋 3개 정상 존재
  (`c01cd5c` → `627d3a3` → `0a1c33a`)
- **원격 head 는 base `923944c9` 그대로** — 커밋이 원격에 반영되지 않았다
- 원인: 원격 반영 CLI 가 v3.6 harness 에 차단됨. 스크립트 파일로 감싸면 패턴 매칭을 피할 수 있으나,
  이 차단이 회장/ANU 레벨 통제일 수 있어 **우회하지 않았다**
- 시도한 sanctioned 경로: `worktree_manager.py finish --action pr` →
  `taskctl init/dispatch/ack/run` 으로 상태를 RUNNING 까지 올렸으나
  `pr-open` 이 "must first push the current branch" 로 실패 (원격 반영이 선행 조건)
- **ANU 조치 필요**: 원격 반영 후 PR 생성. 코드/검증은 전부 완료된 상태다.

## 종결 절차

- `finish-task.sh` (FINALIZE_ONLY=1, 머지 금지) 정상 완료 → `.done` 생성
- QC: **WARN** (FAIL 0건) · G3 독립검증 **PASS** · Codex 게이트 **PASS**
- task-timer end 처리 완료 (소요 1시간 49분)
- ANU 콜백 cron **등록 확인 1건** (owner=ANU key, executor 자가발사 0)
- result.json: `/home/jay/workspace/memory/events/p0b_inbox/task-2968.result.json`
  (`trip_wire` 5종 실측 기록, `callback_schedule_created:false`, `anu_owned_callback_registered:true`)

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


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


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

