---
task_id: task-3071
scope: task
type: report
team: dev3-team (다그다)
repo: Jeon-Jonghyuk/InsuRo
pr: 283
status: PR OPEN (draft) · 머지 금지 · ANU 판단 대기
created: 2026-08-30
---

# task-3071 — 약관 수술분류표 파싱 + 정액 판정 기반 구축 (Phase 1)

- **PR**: **#283 (draft, OPEN)** · base `bd0c5b4` → head `a74d4658`
- **브랜치**: `task-3071-dev3`
- **명세 sha256(앞16)**: `8d0fdd81b624bbce` (일치 확인)
- **3문서**: `memory/plans/policy-surgery-table_260830/` 3건 정독 후 착수

## 봉투 한 줄

실약관 2사 4구간 **351행 중 339행(96.6%) 종 확정** · 전수 스캔 **오적재 0건** ·
회귀 3090→**3218 passed(+128) 실패 0** · 변이 **16종 전부 사망** ·
**미해결 5건은 아래 §7 에 분리 명시**(머지 불필요 아님).

---

## 1. 무엇을 만들었나

계획서 §3 의 단계적 판단구조를 **재료 계층까지** 세웠다. 판정 엔진은 후속이다.

| 산출물 | 파일 | 역할 |
|---|---|---|
| 약관 표 추출 | `server/policy_grouping/surgery_table.py` | 별표의 1-5종 수술분류표를 행 단위 구조화 |
| 저장소 DDL | `supabase/migrations/20260830T000010_task3071_coverage_surgery_class.sql` | 분류표 행 저장(제출까지) |
| 서류 추출기 | `server/claim_extract/medical_doc.py` · `medical_prompt.py` | 진단서·수술확인서 → KCD·수술명·입퇴원일·진단확정여부 |
| 증권 1차 탐색 | `server/claim_extract/policy_candidates.py` | [1단계] 담보 후보 좁히기 |
| 매칭 + HITL | `server/claim_extract/surgery_match.py` | [2][3][4]단계 + 조건 판단 |
| 테스트 3종 | `server/tests/test_{surgery_table,medical_doc,surgery_match}_task3071.py` | 128건 |

**호출부 배선 없음 (ACTIVE=false).** 라이브러리만이며 화면 동작 변화 0건이다.

## 2. ★ 실측 커버리지 (팀장이 재측정 — 팀원 자기보고 승계 안 함)

pdfplumber 0.11.9 · 메리츠 `meritz_2607_20260713.pdf`(1767p) · 삼성 `samsung_…_20260701.pdf`(1474p)

| 대상 | 행 | 종 확정 | 미확정 |
|---|---|---|---|
| 메리츠 별표43 p817–820 | 94 | 90 (95.7%) | multi_value 4 |
| 메리츠 별표6 p1432–1435 | 94 | 90 (95.7%) | multi_value 4 |
| 삼성 p1363–1366 | 102 | 99 (97.1%) | unreadable 3 |
| 삼성 p1402–1403 | 61 | 60 (98.4%) | unreadable 1 |
| **합계** | **351** | **339 (96.6%)** | **12** |

### 미확정 12건은 파서 결함이 아니다 (교차검증으로 확정)

같은 수술을 두 회사가 다르게 표현한다:

- **메리츠** = 부모·자식을 **한 셀에 합침** → `'1 3'`·`'1 2'`·`'3 2 1'`·`'5 3'` (다값)
- **삼성** = **부모행 공란 + 자식행 보유** (`11` 공란 / `11-1`=1 / `11-2`=3)

두 회사에서 미확정으로 잡힌 항목이 **정확히 같은 수술**(사지 절단수술 11번,
사지골·사지관절 관혈수술 13번, 내시경 수술 88번, 관혈적 악성신생물 근치수술 1번)이라
구조적 원인이 교차 확인됐다. **삼성 쪽은 정보 손실이 0**이다(자식행이 값을 가짐).
메리츠 쪽은 결정론적으로 분해할 수 없어 값을 고르지 않고 사람에게 넘긴다.

### 오적재 0건 (전수 스캔)

- 메리츠 1,767p 표 **1,870개 중 17개만** 헤더 게이트 통과. 행 생성 페이지 = 정확히 그 8쪽
- 삼성 1,474p 표 **1,115개 중 16개만** 통과
- 인접 별표42(`['구분','대 상 질 병','분류\n번호']`)·ADRG 코드표 **전부 탈락**

### 선 없는 표 실측 (체크리스트 §1 요구)

- 회사마다 "선"의 정체가 다르다: 메리츠 p817 = stroke line 49개 / 삼성 p1363 = **line 2개 + rect 449개**
  (삼성 표 경계는 선이 아니라 **채운 사각형**). pdfplumber 기본 `edges` 전략이 둘 다 처리한다.
- `line/rect/curve` 전부 제거 → `extract_tables()` 0개 → 행 **0**
- text-strategy 강제 → 표는 잡히나 헤더가 `['【별표4','3】','','']` 로 깨져 게이트 탈락 → 행 **0**
- **어느 경우에도 지어낸 행 0건.** `"수술분류표 행이 0건입니다"` 경고만 남는다 (fail-closed)

## 3. ★ 환각 금지 — 코드 수준 증거

| 금지 항목 | 구현 | 봉인 |
|---|---|---|
| 종 분류 모델 추론 | LLM 호출 **0건**, 전 경로 결정론적 | import 전량이 `re`/`unicodedata`/`dataclasses`/`typing` |
| 다값 셀 값 택일 | `multi_value` + `class_value=None` + 원문 보존 | 첫값·최댓값·최솟값 각각을 반증하는 테스트 |
| 유사도 자동 확정 | **기계 자동확정 = 정규화 후 완전일치 1곳뿐** | 리터럴 대입 지점 수를 세는 테스트 |
| 사람 판정을 auto 로 표기 | `advisor_confirmed` 분리 값 | `"auto" not in decision` 단언 |
| 경증제외 추정 | 근거 없으면 `needs_review` | 발췌는 있어도 `excerpt_page` 없으면 `needs_review` |
| KCD 표기 보정 | 원문 그대로 | 점(.) 제거 변이 → 3 failed |
| `certainty` 기본 `final` | 기본값 `unknown` | 4개 실패경로 전부 `!= "final"` |

`difflib`/`SequenceMatcher`/`rapidfuzz`/`fuzz` 문자열 **소스 0건**.

### 팀장이 통합 중 발견·수정한 정직성 결함 1건

아네의 초안은 **설계사가 직접 고른 결과에도 `auto_confirmed`** 를 붙였다. 사람의 판정을
기계의 자동 판정으로 적는 것은 **판정 근거에 대한 거짓 표기**다(값이 맞아도 출처가 틀림).
→ `advisor_confirmed` 를 별도 값으로 분리하고, "사람이 고른 결과에 auto 표기가 붙지
않는다"는 봉인 테스트를 추가했다. 자동확정 대입 지점은 2곳 → **1곳**으로 줄었다.

## 4. 약관 전문 미저장 (전면 반려 조건)

분류표 **행만** 구조화한다. 조항 본문(`clause_text`)을 다루는 코드 경로가 없다.
`server/routes/policy_terms_v1.py:23-24` 설계 원칙 유지. 해당 파일 **무수정**.

## 5. 검증

- **회귀**: base `bd0c5b4` 실측 **3090 passed / 5 skipped** → head **3218 passed / 5 skipped**
  → **+128, 실패 0, skip 증가 0**
- **변이 16종 전부 사망**: 다값 첫값 택일(6 failed) / forward-fill 제거(1) / 헤더 게이트 느슨화(4) /
  1-7종 차단 제거(2) / `surgery_name_raw` 정규화(1) / `certainty` 기본 `final`(1) /
  `_map_certainty` 실패시 `final`(1) / KCD 점 제거(3) / 입원일수 양끝포함 제거(5) /
  입원일수 실패를 `(0,None)`(1) / `cross_check_kcd` 침묵(1) / PII 스크럽 무력화(1) /
  `class_state=="ok"` 가드 제거(4) / 부분일치 자동확정 허용(6) / 정규화가 괄호 제거(3) /
  근거 없는 후보 허용(1). 주입 전 `원문 in 소스` 확인 후 전부 원복 검증.
- **DDL**: 로컬 docker Postgres 16 에 실제 적용해 CHECK·UNIQUE·RLS·upsert 멱등성 실증.
  **프로덕션 접근 0건.**
- **CI**: 필수 8종 **전부 success** (`cancel-kill-switch`·`qc-check`·`hidden-path-audit`·
  `lock-in-check`·`merge-safety-check`·`gemini-review-gate`·`ci/guard`·`guard`).
  `ci`/`diagnostic`/`e2e-test` 는 진행 중(필수 아님).

## 6. 개인정보

- 결과 모델 필드에 환자명·주민번호·주소·연락처·의료기관명·의사명·면허번호 **0개**
- 프롬프트가 8종 PII 를 "키 자체를 만들지 마라"로 금지
- 자유서술에 PII 패턴이 섞여 오면 **부분 마스킹이 아니라 필드를 통째로 버리고** 경고
- 로그에 KCD·질병명·수술명·날짜 **미출력**(봉인 테스트 존재)
- 실물 샘플 `/home/jay/private/claim_samples/` **repo 복사 0건, 열람 0건**
- 커밋 전 스캔: 시크릿 0건, `.pyc`/`dist` 스테이징 0건

**커밋에 남은 PII 패턴 2건은 합성 음성입력이다** — `홍길동`(표준 플레이스홀더),
`900101-1234567`(**주민번호 검증숫자 불일치: 기대 8 ≠ 실제 7 → 실재 불가**),
`010-1234-5678`(표준 예시번호). 셋 다 **PII 스크럽이 작동하는지 확인하는 테스트 입력**이며,
제거하면 그 테스트가 **no-op** 이 된다. 실환자 데이터 0건.

---

## 7. ★★★ ANU 판단 요청 사항 (분리 명시)

### 7-1. 명세 전제 반증 2건 — 판단 필요

**(a) 계획서 §2-4 "약관 별표가 한글 수술명 목록"은 절반만 맞다.**
메리츠 약관에는 **두 분류체계가 공존**한다.
- **별표43·별표6 = `1-5종 수술분류표Ⅱ`** → `구분|수술명|수술 종류`. 한글 이름 기반 ✅ (이번 대상)
- **별표30 = `1-7종 수술분류표`** → 약관 제2조가 판정 기준을 **ADRG 수술코드(KDRG v4.4)** 로 정의.
  한글 수술명 컬럼은 있으나 그것은 **ADRG 그룹 라벨**이지 수술확인서에 적히는 수술명이 아니다.
  **수술확인서에는 수술코드가 없다**(맥락노트 §C 확인).
→ 이번 파서는 별표30 을 **대상에서 제외**하고, 해당 담보는 `unsupported_scheme` 으로
   **정직하게 판정 불가**를 반환한다. 계획서 §3-2 가 EDI 코드 경로를 제외한 것과 같은 근거다.
**★ 판단 요청: 1-7종 수술비 담보를 실제로 쓰는 고객이 있다면 Phase 2 에서 ADRG 경로가 필요하다.
별표30 자체는 4열 표로 정상 추출 가능하다(p781~). 착수 여부 지시 바란다.**

**(b) 계획서 §3-1(7) "질병수술비의 `경증 제외` 조건"의 실제 형태가 다르다.**
- **메리츠: `경증`+`제외` 근접 0건** — 이 상품엔 조건 자체가 없다
- **삼성: 87건. 단, `경증질병 제외` 가 담보명(특약명) 안에 있다**
  (예: `질병 상급종합병원 수술비(경증질병 제외) 특별약관`)
  → **증권만 봐도 알 수 있다.** [1단계] 증권 1차 탐색에서 잡히므로 단계적 구조와 맞물린다
- 실제 "경증" 판정은 별도 경증 정의표가 아니라 **제5조 ②의 KCD 코드범위 제외목록**이다
  (`F04~F99`·`N96~N98`·`O00~O99`·`Q00~Q99`·`E66`·`K00~K08`·`I84`·`K60~K62,K64`·`N39.3,N39.4,R32`·
  `H00,B30,H60,L84,B00`)
→ 진단서 KCD 와 **기계적 대조가 가능**하다. 추정이 필요 없다.
**★ 판단 요청: 이 목록은 상품마다 다르므로 코드 상수로 박으면 안 된다. 약관 텍스트에서
파싱해 근거(page)와 함께 저장하는 경로를 Phase 2 로 올릴지 지시 바란다. 현재는 근거 없으면
`needs_review` 로 떨어진다(안전측).**

### 7-2. 미실증 3건 — 정직하게 남김

1. **실물 서류 2종 Vision 실호출 미실행.** 샘플이 repo 밖(권한 700)이고 CLI 실호출은 테스트
   범위 밖이라 합성 Static 클라이언트로만 검증했다. `AnuMedicalVisionClient` 는 t3067 의
   `AnuReceiptVisionClient` 와 동일 스테이징/CLI 경로에 프롬프트만 교체한 형태다.
   **★ 실서류 1건 실호출 검증이 필요하다**(계획서 §6-1 항목 — 미충족).
2. **파서↔매처 실물 결선 1회 실행 미검증.** 아네는 순환의존 방지를 위해 duck typing 으로
   받았다. 속성명이 어긋나면 `getattr` 기본값 때문에 **조용히 후보 0건**이 될 수 있다.
   계약 필드명은 팀장이 고정했고 양쪽 다 준수하나, **실물 1회 실행으로만 최종 확인된다.**
3. **`coverage_surgery_class` 적재 후 실조회 HIT 미검증**(계획서 §6-3). DDL 이 프로덕션에
   미적용이라 구조적으로 불가하다. **ANU 적용 후 재측정 필요.**

### 7-3. 설계 판단 3건 — 승인/변경 지시 필요

1. **입원일수 = 양끝 포함**(01-05~01-10 → 6일)으로 확정했다. 약관마다 기준이 다를 수 있어
   상수 `INPATIENT_DAYS_COUNTS_BOTH_ENDS` + 주석에 명시하고, 값이 나와도 항상
   `needs_manual=["inpatient_days"]` 를 붙인다(지급일수로 확정하지 않음).
2. **`절제술`↔`적출술` 을 자동 확정으로 승격하지 않았다.** 두 표기가 같은 수술인지는 약관마다
   다를 수 있어 후보 제시까지만 한다.
3. **`scheme` 스티키 승계.** 별표 2번째 페이지 이후엔 `1-5종` 문자열이 실제로 없어(메리츠
   p818/819 실측) 승계 없이는 뒷 페이지를 못 막는다. 승계는 추정이므로
   `stats['pages_with_scheme_marker']` 로 **직접 관측 페이지 수를 따로 신고**한다.

### 7-4. 인프라 관찰 1건 (범위 밖)

`hidden-path-audit` 체크는 `forbidden_paths` 를 **실제로 대조하지 않는다** —
변경 파일 수를 세고 `echo PASS` 만 한다(`.github/workflows/taskctl-ci.yml:75-105`).
이번 PR 은 통과했으나 **그 통과가 범위 준수의 증거가 되지 못한다.** 기존 관찰의 재확인이다.

---

## 8. 종결 상태

- **PR #283 draft OPEN. 머지하지 않았다.** 프로덕션 DDL 적용 0건. 프로덕션 데이터 변경 0건.
- `CI_WATCH_HANDOFF`: `memory/events/task-3071.ci-watch-handoff.json` (16필드, 필수 12 포함)
- `gh` 호출은 전부 `GH_TOKEN="$BOT_GITHUB_TOKEN"`(App 설치 토큰) 사용. 회장 개인 PAT 미사용.
- **"머지 불필요·미해결 없음" 아님.** 미해결 5건을 §7-2·§7-3 에 명시했다.

## dev3-team 추가 항목

- **코드 품질**: 팀원 3인 병렬(루/브리짓/아네), 공유 worktree + 파일 명시 add 로 커밋 귀속 혼입 차단.
- **버그 발견/수정**: 1건 — 사람 판정을 `auto_confirmed` 로 표기하던 정직성 결함(§3 말미).
  검증 중 `88-1` 행 누락 의심이 있었으나 **조회 필터 문제로 확인**되어 오보하지 않았다(실제 정상 추출).
