# 작업 보고: task-2943

- 팀: dev2-team (오딘/Odin)
- 지시서: `memory/tasks/dispatch-insuro-phase1a-policy-extract-260813.md` (sha256 `452e9a40…dac71c` 대조 일치)
- 작업 repo: `/home/jay/projects/InsuRo` — worktree `.worktrees/task-2943-dev2`, branch `task/task-2943-dev2`, base `ba63045`(main)
- **로컬 커밋 `2a2ddf2` (원격 push = harness hook 차단 → ANU가 push/머지). merge_policy=tiered**

## 1. 작업 내용

InsuRo 보장분석(Phase 1a) 백엔드: 실증권(PDF/JPG) → 담보 추출 → 대구분 정규화 → task-2938 grouping 엔진 연결 → 보장분석 표 데이터 구조.

### 생성 파일 (전부 신규, 3,847줄)
| 파일 | 줄수 | 내용 |
|---|---:|---|
| `server/policy_extract/models.py` | 210 | `CoverageRecord`/`PolicyHeader`/`ExtractResult`/`ExtractProvenance`, 추출 시점 실명·증권번호 마스킹, 금액 단위 환산(만원/천원/억원/원) |
| `server/policy_extract/detect.py` | 46 | 텍스트레이어 유무 판별(`TEXTLAYER_MIN_CHARS=50`), 페이지별 판정 |
| `server/policy_extract/textlayer.py` | 745 | pymupdf **좌표(bbox) 기반** 파서. profile A(`field_block`) / profile B(`table_stream`) — 회사명 아닌 **라벨 시그니처로 분기** |
| `server/policy_extract/vision.py` | 544 | **아누시스템 vision 어댑터(신규)**. claude CLI 무학습·stateless, 다중 이미지 단일 호출 |
| `server/policy_extract/grouping_bridge.py` | 147 | task-2938 지식DB 경유 grouping. **이름매칭 금지**, cold start → 미분류 + 설계사 질문 큐 |
| `server/policy_extract/__init__.py` | 119 | `extract_policy(path/paths, vision_client=)` — textlayer→vision fallback(`mixed`) |
| `server/policy_normalizer.py` | 461 | 대구분 10종 **순서 고정**, 테이블 주도 규칙(`COARSE_RULES` 11종), 보장분석 표 구조 |
| 테스트 4종 | 1,575 | textlayer/vision/normalizer/grouping_bridge |

### 제약 준수
- `server/policy_grouping/**`, `server/policy_analyzer.py`, `server/anu_provider.py` **무수정**(git status 확인). 읽기/import 재사용만.
- `납입잔여개월` = `policy_analyzer.calculate_remaining_payment` **import 재사용**(중복 구현 0).
- 허용 경로(`server/policy_extract/**`, `server/policy_normalizer.py`, `server/tests/**`) 밖 파일 0건.
- Phase 1b(표 렌더/프린트/검수 UI) 미착수 — 범위 밖.

## 2. 아누시스템 vision 어댑터 (신규 · 실호출 검증)

`anu_provider.generate`는 **텍스트 전용(이미지 불가)** 이고 수정 금지 대상이라, `policy_extract/vision.py`에서 CLI를 직접 호출(패턴은 `anu_provider._run_claude_cli_async` 준수).
- 검증된 호출법: 이미지를 임시 디렉터리에 `page_N.png`로 스테이징 → **cwd=그 디렉터리**, 프롬프트는 **stdin**, `claude -p --model <tier> --allowedTools Read`. (cwd 밖 절대경로=권한 거부, `--allowedTools` 뒤 위치인자=프롬프트 삼킴 — 둘 다 실측 확인)
- 이미지 PDF는 fitz로 200dpi 렌더. `tempfile.mkdtemp` + `finally` 정리(PII 잔존 0).
- 무학습: 매 호출 stateless, 학습은 지식DB에만 축적.
- 실패/타임아웃/파싱실패 = **예외 없이** `coverages=[]` + warnings.

## 3. 실증권 5건 추출 결과 (4형식 전부 성공 · 마스킹)

| 파일 | 형식 | method | 담보 | warnings | 헤더 |
|---|---|---|---:|---:|---|
| 현대해상 어린이보험 13p | textlayer A | textlayer | **56** | 6(전부 UI텍스트 false-positive, 담보 손실 0) | 상품명·월보험료(원문에 피보험자/증권번호 라벨 자체 없음) |
| 메리츠화재 담보내용 1p | textlayer B | textlayer | **16** | 0 | 완전(`9Z***`형 마스킹, 월 58,000원) |
| 교보생명 보험증권 4p(이미지) | vision 실호출 | vision | **9** | 0 | 완전(계약일 포함), `100,000천원`→10,000만원 환산 실증 |
| 우체국암보험 JPG 2장(한 증권) | vision 실호출(2파일 1콜) | vision | **10** | 0 | 부분(이미지에 보험사명 없음 → None, 파일명 추론 안 함) |

계 91담보. vision 2건 모두 **실제 CLI 호출**(SKIP 없음, 20~50초/건).

## 4. 강연우 정답 대조 (`강연우(아들) 가입현황 260115.pdf`)

정답 시트를 **좌표로 파싱**해 열(권장합계 / 현대해상 종합보험 / 현대해상 종합+실손)을 분리 후 대조. 우리가 보유한 증권은 **종합보험 1건뿐**이라 그 열에 한정(두 번째 열 `종합+실손 2021-6` 증권은 **미제공** → 제외, 일치율에 포함 안 함).

- **대구분 순서**: 정답 시트 등장 순서 = `COARSE_ORDER` **완전 일치**(실손→진단비→치료비→수술비→일당→장애→요양→사망→운전→생활).
- **(B) 값 기준 recall(이름 무관) = 17/23 = 73.9%**
  - 미매칭 6건 **전부 실손 대구분**. 근본원인: 13p PDF 원문 전체 grep 결과 실손의료비 담보가 **아예 없음**(유일한 "실손" 토큰은 배상책임의 "1억원한도 실손보상" 설명) → **추출 결함 아님, 원문 부재**.
  - 실손 제외 시 17/17 = 100%(진단비 7/7·치료비 1/1·수술비 3/3·일당 3/3·장애 2/2·생활 1/1). **조건부 수치이며 실손 갭을 덮는 값이 아님.**
- **대구분별 건수**(우리/정답): 실손 0/4 · 진단비 20/7 · 치료비 3/1 · 수술비 19/3 · 일당 6/3 · 장애 3/2 · 요양 0/0 · 사망 0/0 · 운전 0/0 · 생활 1/1 · 미분류 4/–. 우리 쪽이 많은 것은 정답 시트가 "주요 담보" 축약이고 우리는 56건 전량 나열이기 때문.
- **(C) 감사표**: 정답 17행 전부 같은 대구분 내 금액 일치 후보 1개 이상 확인(예: 일상생활배상책임 10,000 ↔ `무배당일상생활중배상책임Ⅲ(가족)(갱신형)담보` 10,000). **자동 grouping 아님 — 지식DB cold start이므로 설계사 확인 큐.**
- 리포트/하네스: `teams/dev2/task-2943/golden_report.md`, `verify_golden.py` (repo 밖, PII 격리)

## 5. UNKNOWN 정책 (금소법)

91담보 전수 점검 **위반 0건**.
- 메리츠 `이륜자동차운전중상해부보장` = 원문에 명시된 **진짜 0** → 0 유지(UNKNOWN 아님).
- 우체국 10건 premium = 이미지에 담보별 보험료 미표기 → **전건 UNKNOWN**(0 채움 금지, 총액 억지배분 없음).
- 셀 3분류(`금액` / `"-"`(담보 없음) / `"UNKNOWN"`(값 불명)) 구분 동작 확인. 합산은 **하나라도 None이면 합계 UNKNOWN**.
- 계산 전부 결정적 순수 함수(AI 호출 0).

## 6. 이름매칭 금지 원칙

**위반 0건.** `policy_normalizer.normalize()`는 오직 `payout_condition`(증권 명시 지급사유) 패턴만 입력으로 쓰고, 지급사유가 비면 담보명 단독 판정 금지 → `미분류` + `needs_advisor` + `reason="no_payout_evidence"`.
`.name` 사용 2곳은 (a) coarse 확정 **후** 표시 행 병합 키, (b) 설계사 질문 화면 라벨뿐. grouping 판정 입력 0건.
회귀 테스트로 고정: `test_normalize_name_never_used_for_decision`, `test_grouping_bridge_module_never_reads_coverage_name_for_decision`, `test_same_name_different_clause_id_does_not_force_same_group`.
grouping cold start 실측: 56담보 전건 `unclassified` + 질문 큐 56건 등록(설계 의도대로).

## 7. 테스트 결과

- 신규 84건 통과 (textlayer 20 / vision 24 / normalizer 24 / grouping_bridge 16 계열)
- **전체 회귀: `1542 passed, 3 skipped, 0 failed` (170s)** — 팀장 직접 실행
- skip 3건 = `INSURO_POLICY_FIXTURE_DIR` 게이트(실증권 스팟체크) + 기존 폰트 skip. env 지정 시 20 passed로 재확인.

## 8. 개발 중 발견·수정한 결함 (실증권 실측으로만 잡힌 것들)

1. **profile A 담보명 컬럼 역순 함정** — 담보명이 별도 컬럼이라 평문 순서상 페이지 뒤에 몰림. 좌표 페어링으로 해결. 페이지 로컬 zip은 카운트가 같아도(5=5) **조용히 오정렬**됨을 실측 확인 → 문서 전체 연속 스트림으로 재설계.
2. **갱신형 담보 `보장기간` 별도 행 밀림** — `payout_condition`에 지급사유 대신 `"보장기간 2023.03.16~…"`이 들어가고 실제 지급사유가 손실. → `배상책임Ⅲ(가족)` 복구, **가입금액 10,000만원**(정답 시트 일상생활배상책임과 일치).
3. **금액 단위 `천원`/`억원`/`원` 미지원** — 생보(교보) 증권 전건 UNKNOWN 되던 문제. `fullmatch` 앵커링으로 `"1억5,000만원"` 같은 복합표기가 부분매칭돼 **틀린 값을 조용히 반환**하던 잠재 버그도 함께 제거(모호=UNKNOWN 원칙).
4. **vision 페이지 경계 컬럼 헤더 유실** — JPG 2장을 개별 호출해 2번째 장의 `지급금액` 열을 보험료로 오분류. **한 증권=한 호출**(`extract_pages`)로 해소, 실측 재확인.
5. **운전 대구분 과분류** — `자동차사고(스쿨존내교통사고)담보`("스쿨존내 교통사고로 인한 상해")를 운전자책임 담보로 오분류. 규칙 정교화(교통사고처리지원금/벌금·합의금/변호사선임 결합만) → 운전 0건으로 정답 시트와 일치, 해당 담보는 미분류(설계사 확인).
6. **[Critical] 실증권 PII가 테스트에 리터럴 커밋** — QA(헤임달)가 실제 증권번호 `6A158280`·실보험료·실담보 값 조합이 "합성 fixture"로 위장돼 있음을 발견. 전량 합성값 치환. 추가로 팀장이 **실명(강혁/이고은/강연우)** 이 fixture 파일명/tmp 파일명으로 남은 것을 발견 → 보험사명 glob(`*메리츠*.pdf`)로 교체. 최종 `grep` 실명·증권번호 **0건**.

## 9. 한계 / 회장님 결정 필요

1. **실손 갭(recall 73.9%의 전부)** — 정답 시트 종합보험 열의 실손 4행이 제공된 PDF 원문에 없음. ① 별도 실손 특약 증권이 미제공인지 ② 시트 작성 시점 차이인지 **확인 필요**(단정하지 않음).
2. **정답 시트 두 번째 열(현대해상 종합+실손 2021-6) 증권 미제공** → 대조 제외.
3. **세부 행 자동 grouping 불가** — 지식DB cold start + axes(약관 정독) 유입 경로 미연결(Phase 2). 현재는 전건 설계사 질문 큐. 이름매칭으로 메우는 것은 최상위 금지 원칙 위반이라 하지 않음.
4. **원격 push 차단** — harness hook이 `git push`를 차단. 로컬 커밋 `2a2ddf2`만 존재 → **ANU가 push + PR + 머지**(tiered).
5. profile A 헤더에 피보험자/증권번호/계약일 없음 = 원문에 라벨 자체가 없음(None 유지, 임의 추론 안 함). 실사용 시 UI에서 설계사 입력 보완 필요.
6. vision LLM 응답 편차 관측(반복 호출 시 `amount_raw` 공백 사례 1회) — 오분류가 아니라 UNKNOWN 방향 실패라 안전선은 유지되나, 설계사 검수 단계 필수라는 기존 스펙을 재확인.
7. pyright 잔여 경고(fitz 타입 스텁 유래 다수 + 미사용 변수 몇 건)는 기능 영향 없음. CLI pyright 기준으로는 vision/`__init__` 0 errors.

## 10. 비고
- 위임 방식: 팀장 직접 코딩 0 — 설계·계약서 작성·검증만. 구현은 Sonnet 팀원 6회 위임(토르 4·미미르 1·프레이야 1·헤임달 1).
- 계약서: `teams/dev2/task-2943-contract.md`
- 실증권 원본/추출결과 repo 유입 0. 검증 산출물은 `teams/dev2/task-2943/`(repo 밖).
