# task-2947 계약서 — InsuRo 실손보험 세대 지식화

- 팀: 개발2팀(오딘)
- base: `origin/main` = `50d1bc0` (task-2943 Phase 1a 머지본)
- worktree: `/home/jay/projects/InsuRo-worktrees/task-2947` (branch `task-2947-silson-generation`)
- 회장 지시 원문 요지: **"실손은 증권별이 아니라 '몇 세대인지'가 핵심"**

---

## 0. 소스 실측 (팀장 확인)

| 항목 | 지시서 | 실측 | 근거 |
|---|---|---|---|
| 전체 이미지 | 19장 | **18장** | `find` 18 + 원본 `실비,실손.zip` 엔트리 18 (교차확인) |
| 기준표 | 2장 | 2장 | `5세대vs유병자vs노후실손.png`, `변천사&보장안하는손해.jpg` |
| KB총정리 | 16장 | 16장 | `KakaoTalk_...321.png` + `_01`~`_15` |

→ **합격 기준은 "18장 전부 OCR, 누락 0"** 으로 확정한다(2+16=18). 지시서의 19는 오기.

---

## 1. 최상위 불변 원칙 (위반 시 머지 불가)

1. **vision 무학습** — `policy_extract/vision.py`의 아누(claude CLI) 호출 규약을 **재사용만** 한다.
   기존 `policy_extract/*`, `policy_grouping/*`, `policy_normalizer.py`, `policy_analyzer.py`
   **수정 절대 금지**(git diff에 나타나면 실패). 새 패키지에서 import 해서 쓰기만 한다.
2. **계산은 결정적(deterministic)** — 세대 판별은 **날짜 비교만** 한다. LLM 호출 없음, 난수 없음,
   같은 입력 → 항상 같은 출력. vision은 *지식 구축 1회성 OCR*에만 쓰였고, **런타임 판별 경로에는
   절대 들어가지 않는다**.
3. **이름 매칭 금지 원칙 승계** — 담보 "이름" 문자열로 세대를 추정하지 않는다.
   세대의 유일한 근거는 **보장개시일(날짜)** 이다.
4. **UNKNOWN 정책** — 날짜가 없거나 파싱 불가면 **추측 금지**, `generation=None` +
   `reason` 코드 반환. 환각·기본값 채우기 금지.
5. **PII/이미지 격리** — 원본 이미지를 repo 안으로 복사·커밋 **금지**. OCR 원문은
   `/home/jay/workspace/teams/dev2/task-2947/ocr/` (repo 밖)에만 둔다.
   repo에는 **OCR에서 도출된 지식 데이터(JSON)와 코드**만 들어간다.

---

## 2. 산출물 (worktree 기준 경로)

```
server/silson/                      ← 신규 패키지 (순수 추가)
├── __init__.py                     공개 API 재수출
├── models.py                       GenerationProfile / GenerationVerdict 등 dataclass
├── data/silson_generations.json    ★ 지식 데이터 (OCR 도출, 단일 소스)
├── knowledge.py                    JSON 로더 + 조회 API (하드코딩 금지, JSON이 단일 소스)
├── classify.py                     ★ 보장개시일 → 세대 판별 (결정적)
└── analysis_bridge.py              보장분석(policy_normalizer/analyzer) 연결 인터페이스
server/migrations/018_silson_generation_knowledge.sql   지식DB 테이블
server/tests/test_silson_classify.py
server/tests/test_silson_knowledge.py
server/tests/test_silson_analysis_bridge.py
```

---

## 3. 세대 판별 룰 (결정적)

입력: `coverage_start_date` (보장개시일, `YYYY-MM-DD`). 없으면 호출자가 증권
`PolicyHeader.contract_date`를 넘길 수 있으나 **그 경우 provenance에
`date_source="contract_date"` 로 표기**(보장개시일과 다를 수 있음을 명시).

경계(회장 지시 6개, **좌폐구간 `[start, end)`** — 경계일 당일은 새 세대):

| # | 세대키 | 구간 | 통칭 |
|---|---|---|---|
| 1 | `GEN1_PRE_2003_10` | ~ 2003-09-30 | 1세대 (초기 구실손) |
| 2 | `GEN1_2003_10` | 2003-10-01 ~ 2009-07-31 | 1세대 (표준화 이전) |
| 3 | `GEN2_STD1_2009_08` | 2009-08-01 ~ 2012-12-31 | 2세대 표준화 Ⅰ |
| 4 | `GEN2_STD2_2013_01` | 2013-01-01 ~ 2015-08-31 | 2세대 표준화 Ⅱ |
| 5 | `GEN2_STD3_2015_09` | 2015-09-01 ~ 2017-03-31 | 2세대 표준화 Ⅲ |
| 6 | `GEN3_2017_04` | 2017-04-01 ~ 2021-06-30 | 3세대 착한실손 |
| 7 | `GEN4_2021_07` | 2021-07-01 ~ (현재) | 4세대 |
| 8 | `GEN5` | **시행일 미확정** | 5세대 |

- **5세대는 날짜로 판별하지 않는다.** OCR 기준표에 시행일이 확정 표기돼 있지 않으면
  `classify_generation()`은 **절대 GEN5를 반환하지 않고**, GEN5 프로필은 *조회 전용*
  (`status="announced"` 또는 OCR 표기 그대로)으로만 지식DB에 존재한다.
- **노후실손 / 유병력자(유병자)실손은 날짜 파생이 아니다.** 별도 상품군이므로
  `classify_generation(date, product_kind=...)` 의 **명시 입력**이 있을 때만 반환한다.
  명시 입력이 없으면 일반 실손 세대로 판별하고 `caveats`에 "노후/유병력자 여부 미확인"을 남긴다.
- 경계일 정확히 그 날 = **새 세대**(좌폐). 테스트로 6개 경계 × (전일/당일) 12케이스 고정.
- 날짜 파싱 실패/미래일자/1900년 이전 → `generation=None`, `reason` 코드
  (`missing_date` / `unparsable_date` / `out_of_range`).

---

## 4. 세대별 지식 스키마 (`silson_generations.json`)

각 세대 레코드 필수 필드 (회장 지시 항목 전부):

```jsonc
{
  "key": "GEN3_2017_04",
  "display_name": "3세대(착한실손)",
  "period": {"start": "2017-04-01", "end": "2021-06-30"},
  "insurance_term": "...",          // 보험기간
  "renewal_cycle": "...",           // 갱신주기
  "reenrollment_cycle": "...",      // 재가입주기
  "coverage_structure": "...",      // 담보구성(기본형/특약 분리 등)
  "deductible": {"급여": "...", "비급여": "...", "특약": "..."},  // 자기부담(공제)
  "payout_rate": {"급여": "...", "비급여": "..."},                // 보장률
  "waiting_period": "...",          // 면책기간
  "upper_grade_room": "...",        // 상급병실
  "coverage_limit": "...",          // 가입금액(한도)
  "excluded_coverages": ["치매", "치질", "한방", "치과", "정신과", "해외" ...],
  "notes": ["..."],
  "sources": ["변천사&보장안하는손해.jpg", "KB_p07"]   // ★ 출처 추적 필수
}
```

- **모든 값은 OCR에서 도출**한다. OCR에 없는 값은 `null` + `notes`에 "원문 미기재".
  구현자가 자기 지식으로 채우면 **환각 위반**(금소법 리스크).
- `sources` 없는 필드는 금지 — 세대별 매트릭스 검증의 근거가 된다.

---

## 5. 지식DB 저장 + 보장분석 연결

- **테이블** `silson_generation_knowledge` (migration 018):
  `key`(PK) / `display_name` / `period_start` / `period_end` / `attributes`(jsonb) /
  `excluded_coverages`(jsonb) / `sources`(jsonb) / `revision` / `updated_at`.
  task-2938 `policy_grouping` 지식DB와 **동일한 store 패턴**(Protocol + InMemory + Supabase)을
  따르되, **기존 파일은 수정하지 않고** 새 모듈에 구현한다.
- **보장분석 연결 인터페이스** (`analysis_bridge.py`):
  ```python
  def attach_generation(header: PolicyHeader, *, coverage_start_date: str | None = None,
                        product_kind: str | None = None) -> GenerationVerdict
  def describe_for_analysis(verdict: GenerationVerdict) -> dict  # 표/UI용 평탄화
  ```
  `policy_normalizer.COARSE_ORDER`의 `"실손"` 섹션에 붙일 메타를 반환할 뿐,
  **normalizer/analyzer 자체는 건드리지 않는다**(호출자가 조립).

---

## 6. 검증 (합격 기준)

1. **OCR 커버리지**: 18/18 이미지 처리, 누락 0. OCR 산출물에 파일명 전수 기재.
2. **세대 매트릭스 대조**: `변천사&보장안하는손해.jpg` + `5세대vs유병자vs노후실손.png`
   기준표와 JSON 지식데이터를 항목 단위로 대조 → **일치/불일치/원문미기재** 3분류 집계.
   불일치 0이 합격. (원문미기재는 `null`로 남기고 불일치가 아님.)
3. **경계 테스트**: 6경계 × 전일/당일 12케이스 전부 PASS.
4. **회귀**: `server/tests` 전체 PASS, 신규 실패 0.
5. **금지경로 grep**: `git diff --stat` 에 `policy_extract/`, `policy_grouping/`,
   `policy_normalizer.py`, `policy_analyzer.py` **수정 0줄**.
6. **PII/이미지**: `git status`에 이미지 파일 0건.

---

## 7. 완료 조건

- `/home/jay/workspace/memory/events/task-2947.done`
- `/home/jay/workspace/memory/reports/task-2947.md`
- ANU 콜백(세대수 · OCR건수 · 검증정확도, 3900 bytes 이내)
