# task-2981-r2 · 인슈로 — 담보명 1:1 매칭 정정 + 가입제안서 파싱(1c)

- **task_id**: task-2981 (r2 라운드 · 타이머 `task-2981+2`)
- **팀**: dev2-team (오딘)
- **레벨**: Lv.3 (critical)
- **repo**: `/home/jay/projects/InsuRo` · worktree `/home/jay/projects/InsuRo-worktrees/task-2981-r2`
- **branch**: `task/task-2981-r2-dev2` · **commit `092abeb`** · base `c3434bb`
- **머지**: **HOLD** (ANU 독립검증 후)

---

## S — 상황

r1(PR #233)은 담보↔약관조항 결합을 **담보코드 완전일치**로 구현해 머지됐다(main `c3434bb`).
그러나 ① 증권 파서가 `coverage.code` 를 채운 적이 없어 **전건 `unresolved`** 였고,
② 회장님 실측 정정(2026-08-20, codex 합일)으로 **담보코드는 같은 보험사도 상품마다 달라
매칭 기준 자체가 무의미**함이 확정됐다. 아울러 가입제안서 파싱(1c)은 r1에서 샘플 부재로 보류였으나
회장님이 실제 샘플 PDF를 제공하셨다.

## C — 복잡성

- 결합 근거를 **담보명**으로 옮기면, 회사 간 grouping 게이트(t2970/2974/2976/2977/2979)가
  오랫동안 지켜온 **name-match 금지 불변식**과 정면 충돌하는 것처럼 보인다.
- "제한적 fuzzy"를 유사도 threshold로 구현하면 완료조건인 **"오매칭 0"을 증명할 방법이 없다**.
- 제안서 표는 **부모(합계)-자식 계층**을 갖고 있어, 단순 합산 시 조용한 과대추정이 발생한다.

## Q — 질문

담보명으로 결합하면서도 게이트 봉인을 침해하지 않고, **오매칭 0을 실측으로 증명**할 수 있는가?

## A — 답변

**계층을 분리**했다. 담보명 매칭은 *같은 세트 내 문서 ↔ 자기 약관* 조인(= 데이터 링킹)에만 쓰고,
**회사 간 grouping 결정에는 담보명이 단 한 줄도 유입되지 않는다**(`lookup_group()` 입력은 종전대로
clause_id/axes/clause_text). 봉인 경로 변경 **0건**.

그리고 **"제한적 fuzzy"를 유사도 점수가 아니라 규칙 기반 정본화 + 완전일치로 구현**했다.
2단 모두 `==` 완전일치이고 정규화 강도만 다르다. 그 결과 오매칭 위험이 *확률*이 아니라
**"정규화 규칙이 지운 문자"라는 열거 가능한 유한 목록**이 되어, 테스트로 고정·감사할 수 있다.

---

## 수정 파일별 검증 상태

| 파일 | 변경 | 검증 | status |
|---|---|---|---|
| /home/jay/projects/InsuRo-worktrees/task-2981-r2/server/policy_extract/terms_binding.py | 코드결합 폐기 → 담보명 2단 완전일치 | 회귀 86 passed · probe 16/16 · A1~A12 | verified |
| /home/jay/projects/InsuRo-worktrees/task-2981-r2/server/policy_extract/proposal_parser.py | 신규 — 제안서 파서 + 계층행 | 19 passed · 실샘플 35행 실측 | verified |
| /home/jay/projects/InsuRo-worktrees/task-2981-r2/server/policy_analysis_table.py | 결합판정 헬퍼 통일 · 죽은 폴백 제거 | 24 passed · L1 실배선 왕복 | verified |
| /home/jay/projects/InsuRo-worktrees/task-2981-r2/server/policy_extract/models.py | CoverageRecord.code 제거 | 소비자 grep 0 · 전체회귀 | verified |
| /home/jay/projects/InsuRo-worktrees/task-2981-r2/server/tests/test_coverage_name_binding_task2981r2.py | 신규 회귀 스위트 | 실행 통과 | verified |
| /home/jay/projects/InsuRo-worktrees/task-2981-r2/server/tests/test_coverage_terms_binding_task2981.py | 이름 매칭 체계로 재작성 | 실행 통과 | verified |
| /home/jay/projects/InsuRo-worktrees/task-2981-r2/server/tests/test_analyze_policy_terms_set_task2981.py | ImportError 복구 | 실행 통과 | verified |
| /home/jay/projects/InsuRo-worktrees/task-2981-r2/server/tests/test_proposal_parser_task2981r2.py | 신규 | 실행 통과 | verified |

---

## 핵심 성과

### 1. 팀장 적대 검증이 팀원 테스트가 놓친 오매칭 **7건**을 잡아냈다

토르A 라운드1은 자체 테스트 **49 passed**, 봉인 **200 passed** 였다. 그럼에도 팀장이 직접 고안한
**정상 포맷 위조값**으로 7건이 뚫렸다:

| 증권 담보명 | 약관 담보명 | 라운드1 |
|---|---|---|
| `실손의료비(급여)` | `실손의료비(비급여)` | **오결합** ★금소법 리스크 최대 |
| `암진단비(1형)` | `암진단비(2형)` | **오결합** |
| `수술비(1-5종)` | `수술비(6-10종)` | **오결합** (실샘플에 10행 실재) |
| `입원일당(1급)` | `입원일당(3급)` | **오결합** |
| `일반상해사망[기본계약]` | `일반상해사망[추가특약]` | **오결합** |
| `수술비1-5종` | `수술비15종` | **오결합** (하이픈 제거) |

**근본 원인 — 화이트리스트 폴라리티가 거꾸로였다.** 라운드1은 "괄호 내용을 일단 다 지우고
보존토큰이 있으면 봐준다"(allow-by-default)였는데, **의미를 담은 괄호 내용의 공간은 무한**하다.
→ **deny-by-default 전환**(제거해도 안전함이 *실측된* 내용만 열거) + 범위표기 구두점 보호로 전건 봉인.

### 2. 실제 약관 PDF로 종단 실측 — "왜 안 붙는가"를 원인별로 분리

실제 메리츠 약관(`terms_source/meritz_2607_20260713.pdf`) → **299개 조항** 분할 성공.

- **원인 A(진짜 결함, 수정함)**: 299/299 조항이 전부 `보장특별약관` 접미어로 끝난다.
  약관 문서에만 붙는 표기라 안 벗기면 **실제 약관과 영원히 결합 불가**였다 → 후행 접미어 제거 규칙 추가.
- **원인 B(정상 동작, 유지)**: 약관은 `(통합간편가입)` 292건, 제안서는 `(건강가입)` 33건 =
  **다른 인수 변형(= 다른 상품)**. 결합 **0이 정답**이며, 이걸 붙이면 그게 오매칭이다.

실측 결과:
- 실제 제안서(34) ↔ 실제 약관(299): **결합 0 · 오매칭 0** (전건 `no_name_match`)
- **파생 대조군 299/299 결합 성공** — 실제 조항명에서 문서 접미어만 뗀 입력으로,
  매칭 기구 자체가 실데이터에서 작동함을 증명 (※ 독립 문서 간 조인이 아니라 **파생 대조군**임을 명시)

### 3. 제안서 파싱(1c) — 조용한 26% 과대계상을 잡았다

- 실측: **35행 / 한도 33 / 보험료 34 / 합계 136,411원**
- ★ 교차검증에서 발견: 행 보험료 단순 합산 = **172,101** vs 문서 실제 **136,411**.
  차이 35,690 = `암진단및치료비` **합계 행**이 자식 2건(33,400+2,290)과 중복 계상된 것.
- → `row_kind`(leaf/aggregate/child) + `parent_index` + **`premium_reconciled` 자가검증 불변식** 도입.
  `sum(비-aggregate) == total_premium == 136,411`, `reconciled=True` 실측 확인.
- ★ 자식 행 이름이 `┗` 트리문자로 시작해 **담보명 매칭이 깨지던 것**도 함께 수정(원본은 `raw` 보존).

### 4. ANU 지적사항 처리
`policy_analysis_table.py` 의 `except TypeError` 폴백은 `product_id` 가 keyword-only 필수라
**도달 불가 죽은 코드**이자 되살아나면 구멍②(상품 스코프 우회)가 재개방되는 부비트랩이었다 → **제거**.

---

## L1 스모크테스트

- **서버 재시작**: **성공** — worktree 코드를 별도 포트(8099)로 실제 기동.
  `/api/status` → `{"status":"ok","sha":"092abebbb5b17a3f...","ready":true,"db":"ok"}` (200 OK)
  ※ 라이브(8001)는 `sha=49c17a92` 로 **미배포 상태 유지**(건드리지 않음)
- **API 응답 확인**:
  - `POST /api/insuro/analyze-policy` (빈 요청 / 증권+약관 세트) → **HTTP 401** `Missing or invalid authorization`.
    500 아님 = 라우팅·import·배선 무손. **서명 JWT 위조는 금지이므로 인증 우회 시도 안 함.**
  - 인증 아래 실제 결합 배선은 **실서버 환경(.env·실 DB)에서 직접 왕복**:
    `bind_terms_to_coverages()` → 34건 반환, `('unresolved','no_name_match','UNKNOWN')`.
    반환 사유가 `terms_not_ingested` 가 **아니라는 점**이 핵심 — 약관 조회가 성공했다는 뜻이다.
  - 교차 확인: 해당 약관은 실 DB에 **적재되어 있음**(product id `38c796b7-094b-472e-aacb-5ebcfcd712b0`).
    즉 파일읽기 → sha256 → 상품조회 → 조항분할(299) → 결합까지 **전 경로가 실주행**했다.
- **스크린샷**: 해당없음 (백엔드 전용 변경 · 프론트 무변경)

---

## 회귀 / 게이트 실측

| 항목 | 결과 |
|---|---|
| 전체 회귀 (`server/tests`) | base **2380 passed, 3 skipped** → **2451 passed, 4 skipped** · **실패 0** |
| 봉인 회귀 6종 | **200 passed** |
| 봉인 경로 변경 | **0건** (comparison_gate/axes/content_hash/mapper/taxonomy/knowledge/pipeline/seed_cancer5/hitl/grouping_bridge + seal 테스트 7종) |
| 팀장 적대 probe 1차 | **OK=16 / HOLE=0** |
| 팀장 적대 probe 2차 (A1~A12) | **HOLE=1** — A8(`뇌·심장질환진단비`↔`뇌심장질환진단비`)만이며 **설계상 의도한 병합**(순수 구분자) |
| 오매칭 (실데이터) | **0** |
| Codex G1 게이트 | **PASS** (`"pass": true`, critical=False) |
| PII net-new | **0** (추가 2,053줄 스캔) · repo 내 PDF **0건** |

### QC 검증기 지적 — 전건 오탐 확인
- `red-team`: `terms_binding` HIGH 등급 지적 없음(critical 표기 6건은 전부 `re.compile(...)` 정적 패턴 — 기록된 알려진 오탐),
  `proposal_parser` 11건도 `re.compile` 5건 + `"가입담보"`/`"보험료(원)"` 같은 **한글 표 헤더 상수** 6건.
  실제 취약점 **0건**. `policy_analysis_table` 은 `passed: true`.
- `code-validator`: workflow/code quality/security/dependencies 전부 ✅.
  유일한 ❌ 는 "Execution Test: Output is not valid JSON" — **라이브러리 모듈을 CLI로 실행해 JSON을
  기대하는 하네스 기대 불일치**이며(returncode 0), 코드 결함이 아니다.

---

## trip-wire 5종 (실측)

| trip-wire | 실측 | 통과 |
|---|---|---|
| Critical7 | 0 | ✅ |
| PII net-new | 0 | ✅ |
| 회귀 실패 | 0 | ✅ |
| forbidden_paths 침범 | 0 | ✅ |
| nonce | task-2981 | ✅ |

---

## 미달성 / 회장님 결정 필요 (정직 고지)

1. **★ 완료조건 중 "기존측 UNKNOWN 탈출 발현"은 미달성이다.**
   보유 약관은 **통합간편가입** 변형, 제공된 제안서는 **건강가입(건강고지형)** 변형 —
   **문서쌍 자체가 다른 상품**이다. 결합 0은 버그가 아니라 **올바른 fail-closed** 이며,
   여기서 결합을 만들어내는 것이야말로 회장님이 막으라고 하신 오매칭이다.
   → **필요한 것**: 이 제안서에 맞는 **건강가입 약관 PDF**, 또는 보유 약관(통합간편가입)에 맞는 증권/제안서.
     둘 중 하나만 오면 발현까지 실측 가능하다.
2. **증권(policy) 파서 경로는 미검증** — 증권 샘플이 없어 제안서로만 실측했다.
3. **제안서 파서는 메리츠 1개 레이아웃에 한해 실증** — 지시서의 "샘플 축적 후 단계 확장·과설계 금지" 준수.
   Codex가 골든 샘플 2~3종을 권고했으나 지시서 범위 밖이라 **한계로 명시**한다.
4. A8 유형(`·` 순수 구분자 병합)은 설계상 허용된 예외다. `·` 를 의미 구분자로 쓰는 상품이
   발견되면 재검토가 필요하다(현재까지 샘플·probe 에서 발견되지 않음).

## 모델 사용 기록

| 역할 | 담당 | 모델 | 비고 |
|---|---|---|---|
| 팀장 | 오딘 | opus | 구조 파악 · 설계 판단 · **적대 검증** · 종단 실측 · 통합 |
| 백엔드 A(3라운드) | 토르 | sonnet | 담보명 매칭 · 배선 · 회귀 |
| 백엔드 B(2라운드) | 토르 | sonnet | 제안서 파서 · 계층행 |

haiku 미사용. 페르소나 고정 예외: 백엔드 2건을 토르 인스턴스 2개로 병렬 수행(역할 동일·**파일 무중첩**).
프레이야/미미르/헤임달 미소집 — 프론트/UX 변경 0, 테스트는 각 구현자와 팀장 적대검증이 담당.
