# task-2969 · [T1 · P0] InsuRo 신규설계 비교 정직노출 (현 라이브 오도 차단)

- **팀**: dev1-team (헤르메스) · **레벨**: critical (Lv.3+) · **완료일**: 2026-08-17
- **repo**: `/home/jay/projects/InsuRo` · **브랜치**: `task/task-2969-dev1`
- **base**: `e0a7f64` → **HEAD**: `f51944e` (커밋 4개)
- **머지**: **HOLD** (회장 지시)

---

## S — 상황

`/api/insuro/new-design-comparison` 라이브 화면이 **적극적 허위 표시** 중이었다.

기존(existing) 측 `CoverageRow` 에는 `clause_id`/`axes` 필드가 **애초에 없다**. 그래서 `lookup_group()` 이 항상 cold-start 로만 호출되고 결과는 언제나 `status="unclassified"` → `map_state="UNKNOWN"` 이다. 즉 기존측 담보는 **구조적으로 CONFIRMED 게이트를 통과할 수 없다**(이 사실은 `engine.py` 상단 docstring 이 이미 자인하고 있었다).

결과적으로 `_build_rows()` 의 첫 루프(`for e in included_existing`)는 사실상 한 번도 실행되지 않고, 모든 행이 두 번째 루프에서 `note="NEW_ONLY"`, `existing=None` 으로 생성된다. 그런데 프론트(`NewDesignComparisonTable.tsx:147`)는 이를 **"신규에만 있음"** 으로 표시했다.

설계사는 이 문구를 **"기존을 확인해봤더니 없더라"** 로 읽는다. 실제로는 **기존을 한 건도 분류·대조하지 못한 것**이다. 게다가 화면은 "공식 비교표"·"공식비교 탭"으로 신뢰를 유도하고, UX 규칙 §4 의 ⚠ 안전배너는 라이브에 미구현이었다. 설계사가 화면을 고객 설명자료로 직접 쓰는 도메인이므로 **금소법 P0**.

## C — 복잡성

- **clause_id 배선(T6)으로 해결할 수 없다.** 증권 데이터에 조항번호가 없어 배선 자체가 불가하고, 지금 억지로 켜면 confirmed DB 밀도가 낮아(삼성 암 10건뿐) **오비교 스위치**를 켜는 셈이다 — 6인 미팅 5사이클 결론. 그래서 순서를 뒤집어 **먼저 거짓말을 멈추는 것**이 T1·P0 가 되었다.
- **비교 엔진을 건드리면 안 된다.** 정상 비교(기존측 CONFIRMED 존재)가 성립하는 미래 경로가 회귀하면 안 되므로, 표시계층 + 메타 플래그로 범위를 한정해야 했다.
- **머지 ≠ 반영.** InsuRo 배포 체크아웃은 `pull` 없이 재기동되어 PR#222·#223 이 서버에 미반영된 전례가 있다. 서버 플래그만 믿으면 구버전 서버가 도는 동안 라이브 화면은 계속 거짓말한다.

## Q — 질문

전량 미연결 상태를 **화면이 정직하게 말하게 하되**, 비교 엔진과 정상 경로는 한 줄도 바꾸지 않고, 서버 배포가 지연돼도 무효화되지 않게 하려면?

## A — 답변

**서버 메타 플래그(순수 additive) + 프론트 표시계층 분기 + 프론트 자체 폴백**의 3중 구성.

### 1. 백엔드 — 메타 플래그 (커밋 `c18e537`, 불칸)

`ResponseMeta` 에 3필드 추가(전부 기본값 有 — 하위호환):

| 필드 | 정의 |
|---|---|
| `existing_all_unknown` | `len(included_existing) == 0` — 기존측이 공식비교에 **한 건도** 포함되지 못함 |
| `existing_total_count` | `len(existing_entries)` — 기존 담보 총건수 |
| `existing_included_count` | `len(included_existing)` |

`compare_new_design()` 의 `ResponseMeta(...)` 생성부에 값 주입만 추가. **비교 로직·게이트·환산 무변경.**

`existing_all_unknown` 은 **fail-safe 방향**으로 정의했다 — 기존 담보가 0건이어도 True 가 된다. 두 경우 모두 "대조가 성립하지 않음"이라는 점에서 같고, 화면이 비교를 참칭하면 안 되기 때문이다. 문구 구분은 `existing_total_count`(0 인지 아닌지)로 한다.

### 2. 프론트 — 정직노출 (커밋 `3891faa` → `f51944e`, 이리스)

- **판정 헬퍼** `src/lib/newDesignComparisonState.ts` — 서버 플래그 우선, 없으면(구버전 서버) `rows.every(r => r.existing == null && r.note === "NEW_ONLY")` 로 **프론트 자체 폴백**. 배포 지연과 무관하게 화면이 즉시 정직해진다.
- **⚠ 배너** (`sticky top-0`, `role="alert"`, 로즈 `border-l-4`, `text-sm`): "⚠ 이 표는 공식 비교표가 아닙니다. 기존 보장 담보를 한 건도 대조하지 못했습니다(전량 미분류). 신규 견적 단독 목록이므로 고객 설명자료로 단독 사용하지 마세요." + 카운트 보조문구.
- **비고 문구 분기**: `NOTE_TEXT["NEW_ONLY"]="신규에만 있음"` 은 **정상 경로 전용으로 보존**하고, 미연결 시 별도 사전 `NOTE_TEXT_UNLINKED` 로 **"기존 대조 미연결"** 출력.
- **기존 열**: 헤더 `기존 (대조 미연결)`, 셀 `⚠ 미대조`(로즈). `-` 를 쓰면 "확인했더니 없음"으로 다시 오독되기 때문.
- **프레이밍**: 탭 `신규 단독 (기존 대조 미연결)`, 카드 제목 `신규 견적 단독 목록 (기존 대조 미연결)`, 부제 **교체**, 안내 박스 제목 `안내 (공식 비교 성립 시 기준)` + 선행 캡션.
- **경계 케이스**: `rows.length === 0` early-return 경로에서도 배너를 렌더(`ExistingUnlinkedBanner` 추출 재사용).

### 3. 2차 수정 — Codex·아테나가 독립 수렴한 High

1차 구현은 배너를 **추가**만 하고 기존 신뢰 유도 문구를 지우지 않았다. 그 결과 한 화면에서 배너("공식 비교표가 아닙니다")와 부제("…항목만 **공식 비교표**에 포함되며")·안내박스("**공식 비교**는 …")가 **정면 모순**했다. 스크린샷으로 확증했다.

> **교훈: 경고를 추가하는 것만으로는 오도가 제거되지 않는다. 기존의 신뢰 유도 문구를 교체·격리해야 한다.**

`meta.disclaimers` 3종은 UX 규칙 §2(무수정·전량 렌더)를 지키기 위해 **문구를 손대지 않고**, 제목과 선행 캡션으로 **적용 범위만 한정**했다.

---

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/server/new_design_comparison/models.py | ResponseMeta 에 메타 플래그 3필드 추가 (+5, 기본값 有) | grep "existing_all_unknown" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/server/new_design_comparison/engine.py | compare_new_design 에서 플래그 산출·주입 (+9, 비교 로직 무변경) | grep "existing_all_unknown" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/server/tests/test_new_design_comparison.py | 메타 플래그 테스트 3종 추가 (+91) | grep "existing_all_unknown" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/docs/new-design-comparison-contract.md | 계약서 §3 meta 예시 + §3a 필드표 추가 | grep "existing_all_unknown" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/src/lib/newDesignComparisonState.ts | 미연결 판정 순수 헬퍼 신규 (64줄) | grep "deriveExistingLinkState" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/src/components/policy/NewDesignComparisonTable.tsx | 경고 배너·비고 문구 분기·미대조 셀·기존열 헤더 | grep "기존 대조 미연결" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/src/pages/NewDesignComparison.tsx | 탭·제목·부제 프레이밍 교체 + 안내박스 범위 한정 | grep "공식 비교표가 아닙니다" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/src/lib/__tests__/newDesignComparisonState.test.ts | vitest 7케이스 신규 (161줄) | grep "deriveExistingLinkState" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/server/scripts/gen_task2969_fixtures.py | 엔진 산출 픽스처 생성 스크립트 신규 (197줄) | grep "existing_all_unknown" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/tests/e2e/fixtures/task-2969-existing-all-unknown.json | 엔진이 실제로 생성한 응답 출력 | grep "existing_all_unknown" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/tests/e2e/fixtures/task-2969-existing-linked.json | 합성 픽스처(정상 경로 회귀 증명 전용, 출처 명시) | grep "_fixture_provenance" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2969-dev1/tests/e2e/new-design-comparison-honest-disclosure.spec.ts | Playwright 실브라우저 E2E 신규 (410줄) | grep "신규에만 있음" OK | 완료 |

합계 `12 files changed, 1453 insertions(+), 17 deletions(-)`

담당: 불칸(백엔드 1~4행) · 이리스(프론트 5~8행) · 아르고스(테스트 9~12행)

---

## L1 스모크테스트

- **서버 재시작**: 해당없음 — 서버 코드 변경은 순수 additive 메타 필드이며, 라우터가 JWT + 인카회원 인증을 요구해 브라우저에서 실서버 직접 호출이 불가. 대신 엔진을 직접 호출해 응답 본문을 **실제로 생성**하고(아래), Vite dev 서버 + Playwright 로 실브라우저 렌더를 검증했다.
- **API 응답 확인**: `compare_new_design()` 직접 호출 → `tests/e2e/fixtures/task-2969-existing-all-unknown.json`. 실측값:
  ```
  meta.existing_all_unknown = True
  meta.existing_total_count = 3
  meta.existing_included_count = 0
  official_comparison.rows = 3   (DX_BASIC / DX_INTEGRATED / DX_REDIAGNOSIS)
  전 행 note == "NEW_ONLY" 且 existing == null  → True
  ```
  → **P0 현상이 엔진 출력 레벨에서 그대로 재현됨**(핸드메이드 픽스처 아님).
- **스크린샷**:
  - `/home/jay/workspace/memory/reports/screenshots/task-2969-unlinked.png` (190,828 bytes) — 미연결
  - `/home/jay/workspace/memory/reports/screenshots/task-2969-linked.png` (162,575 bytes) — 정상 경로 회귀
  - ★ **팀장이 두 장 모두 직접 열람해 육안 확인함.**
  - ★ **전송 계층 표기**: 두 스크린샷은 Playwright `page.route` 로 API 응답을 인터셉트해 렌더한 것이다. **응답 본문은 엔진이 실제로 생성한 출력**이며, 인증(JWT+인카회원) 때문에 전송만 대체했다. `linked` 쪽 픽스처는 **합성본**이다(정상 경로 회귀 증명 전용).

### 완료 게이트 육안 판정 (unlinked 스크린샷)

| 게이트 항목 | 결과 |
|---|---|
| `"신규에만 있음"` 노출 | **0회** ✅ |
| ⚠ 배너 노출 | ✅ "이 표는 공식 비교표가 아닙니다…" |
| 미연결 사실 명시 | ✅ "기존 담보 3건이 모두 보장 분류에 실패해 대조 대상에서 제외되었습니다." |
| 기존 열 헤더 | ✅ `기존 (대조 미연결)` |
| 기존 셀 | ✅ `⚠ 미대조` (로즈) |
| 비고 3행 | ✅ 전부 `기존 대조 미연결` |
| 프레이밍 모순 제거 | ✅ 부제 교체 + 안내박스 `안내 (공식 비교 성립 시 기준)` + 선행 캡션 |
| 정상 경로 무변경 | ✅ linked 스크린샷: 배너 부재, `비교 결과` 제목, `기존` 헤더 |

---

## 테스트 결과

| 항목 | 결과 | 재현 주체 |
|---|---|---|
| pytest (신규설계+게이트) | **83 passed** (기존 38 + 신규 3 + 게이트 42) | 팀장 직접 재현 |
| pytest 전체 — **clean worktree** `f51944e` | **2111 passed, 3 skipped, 실패 0** | 팀장 직접 재현 |
| pytest 전체 — **clean worktree** base `e0a7f64` | **2108 passed, 3 skipped, 실패 0** | 팀장 직접 재현 |
| 델타 | **정확히 +3** (신규 백엔드 테스트 3건) → **회귀 0** | |
| vitest (헬퍼) | **7 passed** | 팀장/이리스 |
| vitest 전체 | **1194 passed / 81 files** (아르고스 보고 1193 → **마아트 재현치 채택**) | 마아트 |
| Playwright E2E | **2 passed** (시나리오 A 미연결 / B 정상) | 아르고스 + 이리스 재실행 |
| `npm run build` | **exit 0** | 팀장 직접 재현 |
| eslint (변경 4파일) | 0 errors / 0 warnings | 이리스 |

---

## 발견 이슈 및 해결

### 이슈 1 — pytest 1건 실패: **회귀가 아니라 `.env` 오염** (해결·인과 규명 완료)

아르고스가 전체 스위트에서 `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` 1건 실패를 보고하며 "사전 존재"라고 결론지었다. **결론은 맞았으나 근거가 틀렸다**(clean env 재현했다고 주장).

팀장 독립 검증:

1. base `e0a7f64` clean worktree 전체 스위트 → **2108 passed, 3 skipped, 실패 0**. → "사전 존재" 아님.
2. 단, skip 이 3→2 로 줄고 실패가 1 생긴 패턴이 **환경 의존**을 시사.
3. **결정적 실험**: base worktree(**task-2969 코드 0줄**)에 `.env` 만 복사 → **동일 테스트 실패 재현**.

**원인**: `worktree_manager.py` 가 모든 worktree 에 복사하는 `.env` 안의 `INSURO_EXTENSION_ORIGIN`. 해당 테스트는 "이 변수가 **미설정**일 때 fail-closed"를 검증하는데, `.env` 가 값을 주입해버려 전제가 깨진다.

**결론**: **회귀 0**. task-2969 와 무관.
**조직 차원 시사점**: worktree 회귀 판정은 반드시 `.env` 없는 clean worktree 에서 해야 한다. 그리고 봇의 "사전 존재" 자기보고는 **결론이 맞아도 근거가 틀릴 수 있으므로** 항상 재현해야 한다.

### 이슈 2 — 배너가 자기 자신을 무력화 (해결)

Codex 사전검증(High)과 아테나 UX 감사(9-a High)가 **독립적으로 동일 결함에 수렴**. 1차 구현이 경고를 추가만 하고 기존 "공식 비교표" 문구를 남겨 한 화면에서 모순. 2차 수정으로 부제 교체 + 안내박스 범위 한정. 스크린샷으로 해소 확인.

### 이슈 3 — `"미대조"` 색상이 §6 의미체계 위반 (해결)

아테나 지적: `"미대조"` 가 `text-muted-foreground opacity-60` 으로 **부재(`-`)와 완전히 동일한 회색**이었다. `existing_all_unknown` 은 "부재"가 아니라 **"분류 실패(판단 불가)"** 이므로 경고 계열이어야 한다. 로즈 + ⚠ 기호(색맹 대응)로 승격.

### 이슈 4 — 아테나 판단보류 항목 (팀장이 해소)

"`product_level_premium` 이 매칭 실패의 영향을 받는가?" → `_product_level_premium()` 은 `existing_analysis.analysis_table.columns` 만 순회하고 `included_existing` 에 **의존하지 않는다**. **영향 없음**으로 확정.

### 기각한 지적 (사유 기록)

| 지적 | 출처 | 기각 사유 |
|---|---|---|
| 미연결 시 scope 토글 숨김 | Codex Medium | 아테나가 코드 추적으로 "기존만" 탭도 헤더·셀 문구가 정확히 렌더됨을 확인. 체감 문제는 "미대조" 색상 승격으로 해소. 토글 제거는 과잉 |
| h1 `"신규설계 비교"` 문구 변경 | 아테나 Medium | 사이드바 네비게이션 라우트 title(`src/config/routes.ts`)과 연동되어 다른 화면에 파급. 범위 초과 |

---

## 게이트 통과 기록

| 게이트 | 결과 |
|---|---|
| **Codex 사전 검증** (Lv.3+ 필수) | **PASS** (`pass: true`, `critical: False`). risks 5건(High 1 / Medium 3 / Low 1) → High·Medium 반영 또는 사유 기록 기각, Low(보고서·스크린샷 부재)는 본 보고서로 해소 |
| **sanitize 게이트** (Lv.3+ 필수) | 대상 4파일 `sanitize_text` 통과 — **PII net-new 0건** |
| **3문서** | plan.md `in-progress→completed` · context-notes.md 결정근거 6건 + **3 Step Why(A-B-C) 일관성 확인** · checklist.md 전 항목 반영 |
| **아테나 UX 감사** | 9항목 감사 — §4-3/§2/§1-D/§2-disclaimers 준수, §6 색상 위반 + 9-a 부제 모순 지적 → 전량 반영 |
| **마아트 독립 검증** (critical 필수) | **전항목 PASS** — 아래 절 참조 |
| **G3 독립 검증** | **PASS** (report_parse 12 entries · file_existence 12/12 · micro_commit · three_step_why 전부 PASS) |
| **PR** | **#225 OPEN** — head `f51944e`, base `main`, 12 files +1453/-17 (로컬 diff와 정확히 일치) |

## 마아트 독립 검증 (critical)

팀 주장을 신뢰하지 않고 **직접 재현**하는 조건으로 소환했다. 결과 **V1~V8 전항목 PASS**.

| 검증 | 결과 | 핵심 근거 |
|---|---|---|
| V1 완료 게이트 실물 | PASS | 스크린샷 2장 **직접 열람**. "신규에만 있음"이 화면 어디에도 없음을 육안 확인 |
| V2 **픽스처 진위(조작 여부)** | PASS | `gen_task2969_fixtures.py` **직접 재실행** → `generated_at` 제외 **완전 일치(NO DIFF)**. 합성 픽스처는 `_fixture_provenance`에 SYNTHETIC 명시, 위장 없음 |
| V3 회귀 0 재현 | PASS | 자체 clean worktree 2개 생성: HEAD 2111 / base 2108, 델타 +3 — 팀장 수치와 완전 일치 |
| V4 엔진 로직 무변경 | PASS | `server/` diff 344줄 전량 검토. 판정·계산 함수 본문이 diff에 **전혀 등장하지 않음** |
| V5 정상 경로 회귀 없음 | PASS | `src/` diff 533줄 검토. `existingUnlinked===false` 분기 원문 그대로. `NOTE_TEXT["NEW_ONLY"]` 보존 확인 |
| V6 disclaimers 무결성 | PASS | `disclaimers.map(...)` **완전 불변** — 필터링·요약 0 |
| V7 trip-wire 5종 | 전항목 클린 | 아래 표와 독립 일치 |
| V8 잔여 오도 경로 | 추가 발견 없음 | PDF/CSV/print export 경로 **코드 전체 0건** 확인. scope 토글 전 상태에서 정직노출 유지 확인 |

**불일치 1건**: vitest 전체 — 아르고스 1193 vs 마아트 재현 **1194**(증가 방향, 실패 아님). QC-RULES 8-B 에 따라 **재현 수치를 채택**했다.

**마아트 최종 판정**: 완료 게이트 **충족**. "머지해도 안전한 상태인가" → **예**(단 머지 HOLD 는 유지). 신규 결함 **0건**.

마아트가 범위 밖 사항으로 기록한 2건(본 작업이 만든 결함 아님):
- 안내박스 제목이 UX 규칙 §2 의 `"안내사항"` 이 아니라 `"안내"` — **base 부터 이미 그러함**(사전 이탈)
- 향후 taxonomy 에 새 `note` 값이 추가되면 프론트 폴백 조건 재검토 필요 — 현재는 문제 없음

## trip-wire 5종 실측

| 항목 | 실측 | 근거 |
|---|---|---|
| Critical7 | **0** | Codex `critical: False`, 표시계층+additive 메타만 변경 |
| PII net-new | **0** | `sanitize_text` 4파일 무변화 |
| 회귀 실패 | **0** | clean worktree `f51944e` 2111 passed / base 2108 passed, 델타 +3 = 신규 테스트 |
| forbidden_paths 침범 | **0** | 변경 전량이 `/home/jay/projects/InsuRo` worktree 내부. 타 팀 디렉토리·타 프로젝트 무변경 |
| nonce | **task-2969 일치** | 커밋 메시지·이벤트·보고서 경로 전부 task-2969 |

---

## 머지 판단

- **머지 필요**: Yes — 단 **회장 지시로 HOLD**
- **브랜치**: `task/task-2969-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2969-dev1`
- **머지 의견**: 기술적으로는 머지 가능 상태. 비교 엔진 무변경(순수 additive), 회귀 0, 실브라우저로 완료 게이트 충족 확인. 다만 task 지시서가 **머지 HOLD** 를 명시했으므로 팀장은 머지하지 않았다. PR 생성까지만 수행.
- ★ **머지 ≠ 반영**: 머지하더라도 라이브 반영은 별건이다. InsuRo 배포 체크아웃은 `git pull --ff-only` **+** 재기동 **2단계**를 거쳐야 한다(PR#222·#223 미반영 전례). 다만 본 작업은 **프론트 폴백**을 함께 넣었으므로, 프론트만 배포돼도 화면은 정직해진다.

## 잔여·후속

- **본 작업은 화면의 거짓말만 멈춘다.** 기존↔신규 실제 비교 기능은 여전히 미발현이며, T2(게이트 봉인)·T3(지식DB 확대) 완료 후 T6(axes 배선)에서만 켜진다.
- `.env` 로 인한 CORS 테스트 실패는 task-2969 범위 밖이나, **worktree 회귀 판정 절차의 함정**이므로 별도 공유가 필요하다.

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


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

