# task-2806 — TRACK_A_AGE_CROSSCHECK: age 근사식 제거 + 실사값 박제

- **작업 ID**: task-2806 / **팀**: dev1-team (헤르메스)
- **브랜치**: `task/task-2802-dev1` (worktree `/home/jay/projects/InsuRo/.worktrees/task-2802-dev1`)
- **추가 커밋**: `39c896a` — **정확히 1개** (PR 0 · main merge 0)
- **작성일**: 2026-07-20

> ℹ️ 지시서가 지정한 보고서 경로 `trackA-age-crosscheck.md` 는 **task-2805 의 보고서가 이미 점유**하고 있고,
> 그 문서는 **task-2805 가 실행 도중 취소된 사고 기록(아누/회장 판단 대기)** 을 담고 있다.
> 덮어쓰면 미해결 사고 기록이 소실되므로 **보존**하고 본 보고서를 `task-2806.md` 로 저장했다. (하단 GAP 6 참조)

---

## S (Situation)

2026-07-20 회장 라이브 실사에서 URL `query_condition.age` 가 **만나이가 아니라 보험나이**임이 확인됐다
(DOB `19830809` / 조회일 2026-07-20 → 만나이 42, URL `age=43`, 화면 "보험나이 : 43세").
A4 게이트가 `age` 를 대조하지 않아 stale `age=42` entry 가 통과할 수 있다는 결함이 지적됐다.

## C (Complication) — ★ 착수 시점 실제 상태는 지시서 전제와 달랐다

지시서는 "`domCrossCheck.age = UNAVAILABLE_NO_DOM_SOURCE` 라서 대조를 안 한다"를 전제했으나,
**브랜치 실코드를 읽어보니 선행 커밋 `2d0c4c1`(task-2805)이 age 교차검증·tie-break·OBSERVED-1 주석·실사 fixture 를 이미 반영**한 상태였다.

그러나 그 구현은 **task-2806 이 명시적으로 금지한 근사식을 그대로 쓰고 있었다**:

```js
const totalDays = (now.getTime() - birth.getTime()) / MS_PER_DAY_INSURANCE_AGE;
const exactYears = totalDays / 365.25;
return Math.round(exactYears);
```

주석에는 `src/pages/Calculators.tsx` 의 `calcInsuranceAge()` 와 **"동일하게 맞춘다"** 고 적혀 있었다 —
지시서가 "참조하지 말라"고 못 박은 바로 그 파일이며, **이것이 결함의 직접적 유입 경로**였다.

즉 **본 작업의 실질 결함은 "age 대조 부재"가 아니라 "대조는 하는데 산출식이 틀린 것"** 이었다.
게이트가 겉보기에 동작하므로 **표면 통과 ≠ 의도대로 동작** 의 전형이다.

착수 전 근사식 오차를 직접 재현했다 (DOB `19830809`):

```
2026-02-08 rule=42 approx365.25=43   ★MISMATCH
2026-02-09 rule=43 approx365.25=43
2026-07-20 rule=43 approx365.25=43
2026-08-09 rule=43 approx365.25=43
2027-02-08 rule=43 approx365.25=44   ★MISMATCH
2027-02-09 rule=44 approx365.25=44
```

ANU 가 지목한 두 날짜에서 정확히 재현됐다. **6개월 경계 하루 전 조기 올림 → 보험나이 +1 → 보험료 전면 오류.**

## Q (Question)

근사식을 월 단위 규칙으로 교체하되, **근사식이 되살아나면 반드시 실패하는 회귀 방어** 를 어떻게 박제하는가?

## A (Answer) — 수행 내역

### 1. 보험나이 공식 교체 (`extension/content.js`, 이리스)

```js
let age = today.getFullYear() - y;
const birthdayPassedThisYear =
  today.getMonth() > m - 1 || (today.getMonth() === m - 1 && today.getDate() >= d);
if (!birthdayPassedThisYear) age -= 1;

const lastBirthday = new Date(y + age, m - 1, d);
const targetMonthIndex = lastBirthday.getMonth() + 6;
const halfYearYear  = lastBirthday.getFullYear() + Math.floor(targetMonthIndex / 12);
const halfYearMonth = targetMonthIndex % 12;
const lastDayOfTargetMonth = new Date(halfYearYear, halfYearMonth + 1, 0).getDate();
const halfYearDate = Math.min(lastBirthday.getDate(), lastDayOfTargetMonth);
const halfYearMark = new Date(halfYearYear, halfYearMonth, halfYearDate);

return age + (today.getTime() >= halfYearMark.getTime() ? 1 : 0);
```

- **월말일 클램프 확정**: `8/31 + 6개월 = 2/31`(부존재) → **해당 월 말일로 클램프**(평년 2/28 · 윤년 2/29).
  `new Date(y, m+6, d)` 직접 사용 시 다음 달 롤오버가 생기므로, 목표 연/월을 먼저 확정한 뒤
  `new Date(y, m+1, 0).getDate()` 로 말일을 구해 `Math.min` 클램프.
- **정확히 6개월이면 올림**(`>=`).
- `MS_PER_DAY_INSURANCE_AGE` 상수 제거(타 참조 없음 grep 확인).
- 기준일을 연/월/일로 정규화 → 시:분:초 유입에도 결과 불변.
- **보존**: 8자리 검증 / 부존재 날짜(20260231) 되짚기 → null / 미래 DOB → null / 시그니처·export.

### 2. 주석 정정 — 결함 유입 경로 차단

`Calculators.tsx` 와 "동일하게 맞춘다" 문장을 삭제하고 **참조 금지 + 근사 금지 사유(실측 불일치 2건)** 를 명시.
`OBSERVED-1` 블록(단일 표본 경고 포함)은 **원문 그대로 보존**(grep 3건 확인).

### 3. 회귀 테스트 11건 박제 (`extension/__tests__/track-a-gates.test.ts`, 아르고스)

기존 인라인 Vitest/JSDOM 스타일 유지, 신규 fixture 파일 생성 0.

- **6개월 경계 5건** (DOB `19830809`): `2026-02-08`→42 · `2026-02-09`→43 · `2026-08-09`→43 · `2027-02-08`→43 · `2027-02-09`→44
  (`2026-07-20`→43 은 기존 테스트에 존재하여 중복 추가 안 함)
- **월말 클램프 4건** (DOB `19830831`): 평년 `2027-02-27`→43 / `2027-02-28`→44, 윤년 `2028-02-28`→44 / `2028-02-29`→45
- **생일 당일/직전 2건**: `2026-08-08`→43 · `2026-08-09`→43
- 각 케이스에 산출 근거와 "근사식이면 N(회귀)" 주석 명시.
- 기존 테스트 삭제·완화·skip·xfail **0건**.

### 4. startTime 동률 tie-break

선행 커밋의 `entry.startTime >= best.startTime`(배열 뒤쪽 우선) **유지 확인**, 해당 테스트 존치.

---

## 검증

### vitest 원문 (전 → 후)

**전 (baseline, 착수 시점)**
```
 Test Files  6 passed (6)
      Tests  184 passed (184)
```
> ⚠️ 지시서의 "173 passed 유지"는 task-2805 이전 수치다. 착수 시점 실제 baseline 은 **184** 이며,
> 이 값을 감소 판정 기준으로 삼았다.

**후**
```
 Test Files  6 passed (6)
      Tests  195 passed (195)
```
**184 → 195 (+11), 감소 0.**

### ★ 뮤테이션 검증 — 테스트에 실제로 이빨이 있는가

"테스트가 통과했다"만으로는 방어 성립을 증명하지 못하므로, **근사식을 일부러 되돌려** 회귀 탐지를 실증했다:

```
× "19830809" @ 2026-02-08 → 42 ... 근사식이면 43(회귀) — 회귀 탐지용
× "19830809" @ 2027-02-08 → 43 ... 근사식이면 44(회귀) — 회귀 탐지용
× 평년 클램프 당일: 2027-02-28 → 44
× 윤년 클램프 당일: 2028-02-29 → 45
   Tests  4 failed | 99 passed (103)
```

**ANU 가 지목한 2개 날짜 + 클램프 경계 2건에서 정확히 실패**했고, 이후 원복해 195 passed 재확인.
→ 근사식 재유입 시 반드시 차단된다.

---

## L1 스모크테스트 결과

**JSDOM 단위테스트만으로는 실동작 확인이 아니므로, 실제 Chrome 엔진에서 `content.js` 원본을 구동**했다.
(로컬 http 서버 `127.0.0.1:8791` + Playwright, 2026-07-20 실사 DOM 재현: `#birth_date=19830809`, `gender=M`, `selInsuranceType=F`)

- **서버 재시작**: 해당없음 (브라우저 확장 content script — 서버 컴포넌트 없음). 대신 정적 http 서버 기동 후 실 브라우저 로드.
- **API 응답 확인**: 해당없음 (네트워크 요청 없는 순수 DOM/계산 경로). 대신 실 브라우저 평가 결과:

```json
{
 "dom":   { "gender":"M", "insurance_type":"F", "age":43 },
 "match": { "ok":true,  "mismatchField":null,
            "dom_cross_check":{"plan_id":"UNAVAILABLE_NO_DOM_SOURCE",
                               "age":"MATCH","gender":"MATCH","insurance_type":"MATCH"} },
 "stale": { "ok":false, "mismatchField":"age",
            "dom_cross_check":{"plan_id":"UNAVAILABLE_NO_DOM_SOURCE",
                               "age":"MISMATCH","gender":"MATCH","insurance_type":"MATCH"} },
 "bound": ["2026-02-08=42","2026-02-09=43","2026-07-20=43",
           "2026-08-09=43","2027-02-08=43","2027-02-09=44"]
}
```

핵심 3가지가 실 브라우저에서 실증됐다:
1. `#birth_date=19830809` → **보험나이 43** 산출 (만나이 42 아님)
2. 실사 URL `age=43` → 전 필드 MATCH, **게이트 통과**(`ok:true`)
3. **stale `age=42` → `ok:false`, `mismatchField:"age"` — fail-closed 실증** ← 본 작업의 목표 결함이 실제로 차단됨

- **콘솔 에러**: `favicon.ico` 404 1건뿐 (하네스 무관). 스크립트 에러 0.
- **스크린샷**: `/home/jay/workspace/memory/reports/assets/task-2806-l1.png` (43,342 bytes)
- **브라우저 정리**: `browser_close` 호출 완료. 잔존 chrome 프로세스는 MCP 서버 상주(CPU ≤1.3%)이며 좀비 렌더러 없음.

---

## MATCH / GAP

**MATCH**
- age DOM 교차검증 존재 + 월 단위 규칙 산출 (일수 나눗셈 근사 제거)
- 불일치 → fail-closed (기존 실패코드 체계 사용, 신규 코드 발명 0)
- `#birth_date` 부재·형식불량 → `UNAVAILABLE_NO_VALUE` (기존 3상태 패턴 유지)
- startTime 동률 결정적 tie-break + 테스트
- 실사값 fixture 박제 / 경계 회귀 테스트 / OBSERVED-1 주석 보존
- 기존 테스트 감소 0 (184→195), skip·xfail·완화 0
- 추가 커밋 정확히 1개, PR 0, merge 0, `src/**`·`server/**` 미수정

**GAP / 유의**
1. **baseline 수치 불일치**: 지시서 "173 passed"는 task-2805 이전 값. 실제 baseline 184 기준으로 판정했다.
2. **지시서 전제와 실코드 괴리**: 지시서가 지목한 `UNAVAILABLE_NO_DOM_SOURCE` 상태는 task-2805 에서 이미 해소돼 있었다. 본 작업의 실질 기여는 **산출식 교체 + 회귀 방어** 다.
3. **`#birth_date` 부재 시 통과 여부** — 지시서는 "없다고 통과시키지 말 것"이라 했으나, 기존 정책(계약 §3-5 3상태 패턴)은 `UNAVAILABLE_NO_VALUE` 를 단독 차단 사유로 두지 않는다. **기존 정책 유지를 택했고 정책 변경은 하지 않았다.** 정책 자체를 바꾸려면 별건 승인이 필요하다 — **아누 판단 요청**.
4. **`src/pages/Calculators.tsx::calcInsuranceAge()` 에 동일 근사 결함이 잔존할 가능성이 높다.** 범위 밖이라 미수정. **별도 백로그 등재 필요**(사용자 노출 계산기이므로 우선순위 높음).
5. OBSERVED-1 은 여전히 **단일 표본** 이다. 다른 상품·경계일에서 재검증되지 않았다.
6. **★ task-2805 취소 사고 미해결**: `trackA-age-crosscheck.md` 에 따르면 task-2805 는 `.cancelled`(12:04:29) 이후 12:14:42 에 커밋 `2d0c4c1` 을 남겼다. 본 작업(task-2806)은 그 커밋 위에 쌓였다. **task-2806 자체는 취소 마커가 없어 정상 진행**했으나, 선행 커밋의 취급은 아누/회장 판단 대상이다.

---

## 머지 판단

- **머지 필요**: **No** — 지시서가 `merge_policy: "none"`, `PR 생성 0 · main merge 0` 을 명시.
- **브랜치**: `task/task-2802-dev1`
- **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2802-dev1`
- **머지 의견**: 커밋만 남기고 대기. ANU 재실행/독립검증 대상.

## 모델 사용 기록

- 이리스(프론트엔드) — `sonnet` — content.js 공식 교체
- 아르고스(테스터) — `sonnet` — 경계/클램프 회귀 테스트
- 헤르메스(팀장) — `opus` — 결함 진단, 뮤테이션 검증, L1, 통합/커밋
- haiku 미사용 (정확성이 핵심인 날짜 경계 로직이라 sonnet 이상 적용)

## 발견 이슈 및 해결

| 이슈 | 조치 |
|---|---|
| task-2805 구현이 금지 근사식 사용 | 월 단위 규칙으로 교체 (해결) |
| 주석이 `Calculators.tsx` 참조를 권장 → 결함 유입 경로 | 참조 금지 + 근사 금지 사유 명시로 교체 (해결) |
| 근사식 재유입 방어 부재 | 경계 회귀 테스트 11건 + 뮤테이션으로 이빨 실증 (해결) |
| `Calculators.tsx` 동일 결함 의심 | 범위 밖 — 백로그 등재 요청 (미해결, 사유 명시) |
| `#birth_date` 부재 시 통과 정책 | 기존 정책 유지, 변경은 별건 승인 필요 (아누 판단 요청) |
| 보고서 경로 충돌 (task-2805 취소 기록 보존) | 덮어쓰지 않고 `task-2806.md` 로 분리 저장 (해결) |

---

## ★ 완료 처리 결과 — `.done` 미생성 (scope_guard 차단)

`finish-task.sh task-2806 dev1 /home/jay/projects/InsuRo/.worktrees/task-2802-dev1` 실행 결과:

| 단계 | 결과 |
|---|---|
| QC gate | **WARN(통과)** — 6 PASS / 0 FAIL |
| `git_evidence` | **PASS** (커밋 1건 + uncommitted 0) |
| `l1_smoketest_check` | **PASS** |
| `tdd_check` | WARN (구현→테스트 순서) |
| scope_guard | **FAIL → 머지 차단 + `.escalate` 생성** |
| `.done` | **미생성** |

### scope_guard 위반은 false positive 로 판단

- 위반 지목: `extension/background.js` (allowed paths 밖)
- **그러나 task-2806 커밋 `39c896a` 는 `content.js` + `track-a-gates.test.ts` 2개만 변경**했다.
- scope guard 는 `merge-base(origin/main, HEAD)..HEAD` 를 diff 하므로, 지시서 지시대로 기존 브랜치에 **이어서 커밋**하면 선행 커밋(task-2800/2802)이 전부 diff 에 포함된다.
- `background.js` 최종 변경 커밋: `777ec57 [task-2802]`, `4869c10 [task-2800]` — 본 작업과 무관.
- **선례 일치**: `task-2804` 도 동일한 `background.js` scope_guard false positive 로 차단·escalate 됐다.

### 우회하지 않은 이유

- `worktree-base.json` 은 `merge-base` **실패 시에만** 적용되는 fallback 이라 정당한 override 경로가 없다.
- `FINISH_TASK_WORKTREE_STRICT` 비활성화는 **안전 게이트 무력화**이므로 수행하지 않았다.
- **수동 `.done` 생성은 하지 않았다.**
- 지시서가 merge 를 금지하므로 **머지 차단 자체는 의도와 일치**한다. 문제는 `.done` 미생성뿐이다.

### 1차 시도 실패 원인 (기록)

`PROJECT_PATH` 미지정 시 QC evidence root 가 `/home/jay/workspace` 로 잡혀 `git_evidence` FAIL
("커밋 0건", "38 unstaged" — 이는 workspace 자체의 오염이지 본 작업과 무관).
task-timers.json 에 task-2806 의 `worktree_path` 가 없어 자동 인식이 실패한 것이 원인.
worktree 경로를 3번째 인자로 명시해 PASS.

**→ 아누 판단 요청 항목**: scope_guard false positive 확인 후 `.done` 처리 여부.

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

