# task-2802 (TRACK_A) — 확장 production 이식 A1~A6 보고서

- **팀**: dev1-team (헤르메스)
- **레벨**: Lv.4 프로그램 하위 Track (게이트 Lv.2 적용)
- **브랜치**: `task/task-2802-dev1`
- **worktree**: `/home/jay/projects/InsuRo/.worktrees/task-2802-dev1`
- **base**: `task/task-2800-dev1` (A7 완료분, 커밋 3개 **보존 확인**)
- **작성일**: 2026-07-20

---

## S (Situation)

계약 PR(`contract/ohmy-ppd-v1`)이 확정한 OHMY_PPD V1 규약 위에서, ohmymanager 보험료 데이터를
**로컬에서만** 조회·계산·미리보기하는 확장 경로가 필요했다. A7(활성 서버 write 경로 제거)은
task-2800에서 이미 완료된 상태였고, 본 task 범위는 **A1~A6**이다.

## C (Complication)

1. 직전 판본 지시서에 **잘못된 지시 2건**이 있었다(지시서 자체가 정정 표기): "DOM primary / resource
   timing fallback"과 "`idempotency_key` 생성". 전자는 실 DOM에 `plan_id`가 없어 구조적으로 불가능,
   후자는 A에 저장 액션이 없어 자기모순이다.
2. A4가 요구하는 "현재 화면 조건" 대조에서 **어떤 값을 DOM에서 읽을 수 있는지가 미확정**이었다.
3. 서버 전송·저장 버튼·PR·merge가 전부 미승인이라, **"만들지 않는 것"을 증명**해야 했다.

## Q (Question)

계약을 위반하지 않으면서 A1~A6을 구현하고, **금지사항이 실제로 부재함**을 증명할 수 있는가?

## A (Answer)

구현 완료. 커밋 5개, **테스트 152개 전부 통과**(기존 41 → 152, 회귀 0), 실제 Chromium L1 통과.
구현 중 **계약 위반 소지 3건을 자체 발견해 수정**했다(아래 "발견 이슈 및 해결").

---

## 착수 전 게이트

### 계약 문서 SHA-256 (착수 시 + 종료 시 2회 확인, 모두 일치)
- `docs/contracts/INSURO_LV4_PROGRAM_CONTRACTS_V1.md`
  → `c7fe00a75606f94de218f27f65f44315bae97ef993be1d7b217ea322febca873` ✅ 일치
- `docs/contracts/OHMY_PPD_DATA_CONTRACT_V1.md`
  → `fc081a76da0c9213faa7cee1a64d183981b348e42ac788d81466dd9d3e720986` ✅ 일치
- 계약 커밋 `bb1c219` 를 **merge·cherry-pick 하지 않음**(읽기 전용 `git show` 참조만).

### G1 설계 게이트 — 타 track 중첩 0
- Track A(본 task) allowed = `extension/**` / Track B(task-2801) allowed = `server/**`,`supabase/migrations/**`
- **양방향 forbidden_paths 상호 명시 확인**: task-2801이 `extension/**` 을 금지, 본 task가 `server/**`·`supabase/**` 를 금지.
- 공통설정 3종(`package.json`,`vitest.config.ts`,`tsconfig.json`) **미수정 확인**.

### 진단브랜치 격리
`task-2787` / `task-2787+2` / `task-2788` 브랜치를 **merge·cherry-pick·파일 열람·코드 복사 전부 하지 않음.**
Resource Timing 접근은 **계약 문서 §9 + 지시서 A5 서술만으로** 재구현했다(개념 수준 3~5줄 설명만 참조).

---

## A1~A7 각 근거

### A1. 사용자 명시 클릭 기반 background 직접 조회 — ✅
- `content.js` ohmymanager 분기의 조기 `return` 을 **버튼 부착만** 하도록 교체(`initOhmyPpdPreviewButton()`).
- 앵커 우선순위 `#bojang_lists` → `#companyInfo` → `#premium_lists`, 없으면 조용히 종료. 중복 부착 방지.
- SPA 폴링은 **DOM 조회만**(10초/500ms), 네트워크 요청 0.
- **클릭 전 네트워크 0 실증(L1)**: 페이지 로드 후 `ppdMessages: []`, `/api/ProductPremiums` resource entry `0`.
- MAIN world 주입·인터셉터·`chrome.scripting` **신규 추가 0**(정적 테스트로 증명).

### A2. 토큰 취급 (계약 §AMENDMENT-1) — ✅
- 토큰은 **클릭 핸들러 함수 스코프에서 `location.search` 로부터 1회만** 읽는다(`readTokenFromSearch`).
- 저장·캐시 0: 전역/모듈 변수 할당 0, `chrome.storage`/`localStorage`/`sessionStorage` 0.
- 로깅 0: 신규 코드(L500+) `console.*` **0건**. L1에서 콘솔 전체에 JWT·`consultantid` 문자열 **부재 확인**(`tokenInConsole: false`).
- `token_source` = `"URL_QUERY"` | `"ABSENT"` 기록. **ABSENT → 즉시 fail-closed**(`TOKEN_ABSENT`), 쿠키·storage 우회 조립 0.
- 거부 경로에서 토큰 잔존 0: background 거부 응답을 JSON 직렬화해 토큰 문자열 부재를 단언(뮤테이션으로 검증력 확인).

### A3. URL allowlist + sender 검증 — ✅
`background.js` `isAllowedProductPremiumsUrl()` / `isAllowedSender()`:
- `protocol==="https:"` · `hostname==="mmlfcp.ohmymanager.com"`(정확일치) · `pathname==="/api/ProductPremiums"`(정확일치) · 포트 생략/443만 · userinfo 금지
- **query key allowlist** = `age`,`gender`,`insurance_type`,`plan_id` 정확 4개. 미지 키·중복 키·누락·길이 위반·`age` 범위(0~120) 위반 전부 거부.
- **sender 검증**: `sender.id === chrome.runtime.id` **및** origin 이 `https://mmlfcp.ohmymanager.com`.
- 게이트 실패 시 **`fetch` 함수 자체를 호출하지 않음**(테스트로 fetch mock 호출 0회 단언).

### A4. stale URL · 계정전환 차단 — ✅ (결정항목 3건 확정)

지시서가 "실사 후 확정"으로 남긴 3개 항목을 실사로 확정했다.

**결정 1 — "현재 화면 조건"의 DOM 소스 (fixture 실사 결과)**

| 값 | 판정 | 근거 |
|---|---|---|
| `gender` | **READABLE** | `#gender` select, `value` = `M`/`F`, 라벨 남/여 |
| `insurance_type` | **READABLE** | `#selInsuranceType` select, `value` = `LF`(생손보)/`F`(손보) |
| `age` | **NOT_READABLE** | 화면에 `#birth_date`(생년월일 8자리)만 존재. 나이 필드·환산 로직 0 |
| `plan_id` | **NOT_READABLE** | 화면에 `#user_plan_name`(표시용 이름)만 존재. plan_id 개념 0 |

→ **대조 대상 = `gender`,`insurance_type` 2개.** `age`/`plan_id` 는 대조 제외하되
`dom_cross_check` 에 `UNAVAILABLE_NO_DOM_SOURCE` 로 **명시 기록**(조용한 통과 0).
지시서 규정("읽을 수 없는 항목은 대조 대상에서 제외하고 그 사실을 보고")에 따른 축소이며 임의 확대 없음.

**결정 2 — "마지막 조회 시각" 기준 시점**
세션 메모리 `lastConsumedEntryStartTime`(초기 `-Infinity`). 선택 entry 의 `startTime` 이 이 값보다
**커야만** 진행하고, background 조회 **성공 후에만** 갱신한다. → 동일 entry 재사용이 구조적으로 차단됨.

**결정 3 — `local_owner_observation_key` 산출**
`SHA-256(sessionSalt + ":" + consultantid)` hex. `sessionSalt` 는 모듈 로드 시 `crypto.getRandomValues` 로 1회 생성.
- **메모리 전용**: storage 0 · 전송 0(background 메시지에 미포함) · 로그 0.
- `consultantid` **원문 저장·출력 0**(해시 함수 스코프에서만 사용). 반환값에 원문 부재를 테스트로 단언.
- **`external_subject_key` 생성 0**(V1 미사용, §2-1-a-0).
- 전환 판단은 토큰 원문 비교가 아니라 **owner key 비교**로 수행.

**게이트 3종 전부 통과해야 fetch 진행**. 하나라도 불일치 → 메시지 전송 자체를 하지 않음(fail-closed)
+ 로컬 verdict 기록 + 안내 문구. owner 변경/stale 감지 시 **미리보기·스냅샷 즉시 폐기 + `INVALIDATED`**.

> **저장 버튼은 만들지 않았다.** 비활성화 로직도 없다(A6-a: integration 소유).
> A 는 `getPreviewState()` 로 **`VALID`|`INVALIDATED` 상태를 노출하는 의무까지**만 수행한다.

### A5. resource timing primary — ✅
- `performance.getEntriesByType("resource")` 에서 `/api/ProductPremiums` entry 중 `startTime` **최대**를 선택.
- entry URL 의 쿼리를 그대로 사용(**URL 우회 조립 0** — DOM 값으로 URL을 만들지 않음).
- entry 미발견 → 네트워크 0으로 즉시 종료 + **"조회하기를 먼저 눌러주세요"** (L1 실증).
- DOM 은 교차검증용으로만 사용. **인터셉터 복귀 0.**
- `performance` 주입 가능 → 예외 발생 시 null 흡수.

### A6. 로컬 계산·3축 교차검증·미리보기 — ✅
- 계산식 `api_premium × (dom_amount_parsed ÷ api_coverage_amount)`. 분모는 **`coverage_amount` 고정**,
  `guide_coverage_amount` 는 함수 시그니처에서 **아예 배제**해 계산 혼입을 구조적으로 차단.
- 선택 담보 = `#bojang_lists input[type=checkbox][data-cd]:checked` 만. **저장플랜 사용 0.**
- 행 편집가능 input 개수≠1 또는 id≠`input_<cd>` → 가입금액 **신뢰 금지**(`PARSE_FAIL`).
- **fail-closed 11코드** 전부 배선 + **사유별 카운트 출력**(`validation.failure_counts`) — 조용한 통과 0.
- **3축 교차검증**: `scaled_value` / `screen_cell_premium` / `screen_selected_total`(vs 화면 상단 총액) 3개를 모두 보관.
  허용오차 = 비교 건수 × 1원, `tolerance_basis: "PROVISIONAL_COUNT_TIMES_1WON"` 그대로 포함.
  초과 시 해당 보험사·담보 **차단**(`OUT_OF_TOLERANCE`), 한쪽 조용히 채택 0.
- **`WITHIN_TOLERANCE` ≠ `VALIDATED`** — 코드 주석 + **UI 문구까지** 반영(아래 이슈 3 참조).
- **고객 표시값 = DOM 셀 값(`screen_sum`)**. 반올림 규칙 미확정이므로 `scaled_value` 는 검증 전용, 표시값 승격 0.

**`ImmutablePreviewSnapshotV1`** (계약 §2-3-c) — 포함 필드:
`contract_version`("OHMY_PPD_V1") · `query_condition{age,gender,insurance_type,plan_id}` ·
`request_fingerprint` · `dom_selection_hash` · `api_captured_at` · `dom_snapshotted_at` ·
`selected_coverages[]` · `per_insurer[]` · `validation{overall_status,failure_codes,failure_counts,tolerance_basis}` ·
`client_version`. `deepFreeze` 로 동결.

**미포함(검증 완료)**: `idempotency_key` · `reference_type`/`reference_id` · `consent` ·
`fa_account_id` · `external_subject_key` · `local_owner_observation_key` · API 원본 응답 전체.
→ 소스 전수 grep **0건**, 스냅샷 JSON 직렬화 문자열 단언으로 이중 확인.

**서버 전송 코드 0**: `content.js` 실코드에 `fetch(`·`XMLHttpRequest`·`sendBeacon` **0건**(주석만 존재).
`chrome.runtime.sendMessage` 실호출 지점은 2곳뿐 — 기존 `SET_INSURO_JWT` 릴레이(L65, 본 작업 무관)와
신규 `OHMY_PPD_FETCH_V1` **조회**(L906). 저장/전송 메시지 신규 추가 0.

### A7. — 이번 범위 아님, **되돌리지 않음** ✅
- base 의 A7 커밋 3개(`4869c10`,`3649c5f`,`dfbd7f5`) **보존 확인**(`git log | grep -c task-2800` = 3).
- A7 3중 증명 테스트 `a7-write-path-blocked.test.ts` **12/12 통과 유지**.

---

## 뮤테이션 검증 (vacuous 테스트 방지)

`track-a-mutation.test.ts` — 각 뮤턴트마다 **"원본 통과 / 변종 실패"를 둘 다 단언**:

| # | 결함 주입 | 결과 |
|---|---|---|
| 1 | `isAllowedProductPremiumsUrl` → `return true` | 원본은 `https://evil.com/...` 거부, 변종은 허용 → **검출됨** |
| 2 | 거부 응답 `code` 를 `token` 으로 치환 | 원본 응답에 토큰 부재, 변종에 노출 → **검출됨** |
| 3 | `content.js` 에 `fetch("https://insuro.biz/x",{method:"POST"})` 주입 | 원본 위반 0건, 변종에서 `fetch(` 검출 → **검출됨** |

---

## vitest 카운트

| 파일 | 개수 | 비고 |
|---|---|---|
| a7-write-path-blocked.test.ts | 12 | 기존 (A7 3중 증명) |
| host-matching.test.ts | 19 | 기존 |
| matrix-detection.test.ts | 10 | 기존 |
| track-a-gates.test.ts | 65 | **신규** |
| track-a-preview.test.ts | 34 | **신규** |
| track-a-mutation.test.ts | 12 | **신규** |

**착수 시 41 → 종료 시 152 (Test Files 6 passed, Tests 152 passed). 기존 테스트 감소 0.**

---

## L1 스모크테스트 결과 (필수 기록)

pytest/vitest PASS ≠ 실동작이므로, **실제 Chromium**에서 content.js 를
`https://mmlfcp.ohmymanager.com` 오리진으로 구동해 관측했다.
(스크립트는 저장소 밖 `/tmp/l1-task2802/l1.cjs` — 커밋 대상 0)

- **서버 재시작**: 해당없음 (Chrome 확장 — 기동할 서버 없음)
- **API 응답 확인**: Playwright 라우트로 `/api/ProductPremiums` 를 실제 응답시켜
  **진짜 resource timing entry 1건 생성** → A5 복구 경로가 실동작함을 확인 (`step3_entriesAfterRealFetch: 1`)
- **스크린샷**: `/home/jay/workspace/memory/reports/task-2802-l1-preview.png` (1280x720, 30,539 bytes)

관측 결과:

| 단계 | 기대 | 실측 | 판정 |
|---|---|---|---|
| 로드 직후 | 버튼만 부착, 네트워크 0 | `btnPresent:true`, `ppdMessages:[]`, entry 0 | ✅ |
| entry 없이 클릭 | fail-closed 안내, 전송 0 | `"조회하기를 먼저 눌러주세요"`, `ppdMessages:[]` | ✅ |
| 정상 클릭 | 조회 1회 + 미리보기 렌더 | `["OHMY_PPD_FETCH_V1"]`, 메리츠 32,000원 / 삼성 33,500원 | ✅ |
| 같은 entry 재클릭 | stale 차단 | `"조회하기를 다시 눌러주세요"` | ✅ |
| 화면 gender M→F 변경 후 클릭 | 조건 불일치 차단 + 무효화 | `"조회하기를 다시 눌러주세요"`, `previewState:"INVALIDATED"` | ✅ |
| 콘솔 토큰 유출 | 0 | `tokenInConsole:false` | ✅ |
| 허용 외 네트워크 | 0 | `nonAllowedNetwork:[]` | ✅ |

**계산 검산(수기 대조)**: 메리츠 DEATH `12000×(200,000,000÷100,000,000)=24,000` + CANCER `8000×1=8,000` = **32,000**
→ 화면 셀 합 `24,000+8,000=32,000` 과 일치(3축 축1=축2). 삼성 `26,000+7,500=33,500` 동일 일치.

---

## 발견 이슈 및 해결

작업 중 **3건의 계약 위반 소지를 자체 발견해 전부 해결**했다.

<details><summary>이슈 1 — A4 교차검증 fail-open (심각, 아르고스 발견 → 팀장 수정)</summary>

- **증상**: `crossCheckConditions` 가 정규화 실패(미지 인코딩) 시 `ok:true` 로 통과.
  DOM `gender="X"` + URL `gender="M"` 이 조용히 통과함을 테스트로 실증.
- **왜 문제인가**: A2/A3/A5 는 전부 fail-closed 인데 A4-(1)만 fail-open — 계약 §3-5
  "하나라도 불일치 → fail-closed" 와 어긋난다. 이 설계 지시를 낸 것은 팀장(본인)이다.
- **해결**: 정규화 실패 시 **원문 비교로 폴백**하도록 변경. 양쪽 원문이 모두 존재하는데 다르면
  fail-closed(`MISMATCH`), 같으면 `MATCH_RAW_UNKNOWN_ENCODING` 으로 통과, **값 자체가 없을 때만**
  `UNAVAILABLE_NO_VALUE` 로 대조 제외. 양쪽이 같은 인코딩을 쓰는 정상 경로는 오탐 0.
- **검증**: 기존 테스트가 옛 fail-open 동작을 단언해 **의도적으로 1건 실패** → 새 동작에 맞춰 갱신 + 케이스 2개 추가.
</details>

<details><summary>이슈 2 — 계약 enum 밖 코드명 사용 (팀장 발견 → 수정)</summary>

- **증상**: `recordVerdict({ code: "ENTRY_NOT_FOUND" })` — `ENTRY_NOT_FOUND` 는 계약 §4-1~§4-3 enum에 없다.
- **왜 문제인가**: 계약이 "임의 코드명 생성 금지, A·B 동일 문자열 사용"을 명시. 로컬 verdict라 전송되진
  않지만 코드명 규약이 무너지면 integration에서 A·B 코드 대조가 깨진다.
- **해결**: entry 미발견은 "아직 조회를 안 누른" **UX 선행조건**이지 계약상 실패가 아니므로,
  `code: null` + 별도 `ux_state: "ENTRY_NOT_FOUND"` 필드로 분리.
  → `code` 필드에 남은 값이 전부 계약 enum임을 grep으로 전수 확인.
</details>

<details><summary>이슈 3 — UI가 WITHIN_TOLERANCE 를 "검증됨"으로 승격 (L1 스크린샷에서 발견 → 수정)</summary>

- **증상**: 미리보기 배지가 `"검증됨(허용오차 이내)"`. 계약 §4는 **`WITHIN_TOLERANCE ≠ VALIDATED`** 를 명시.
- **왜 문제인가**: 코드 주석에는 구분을 적어두고 **사용자에게는 "검증됨"으로 표시**하면, 계약이 금지한
  승격이 UI 층에서 그대로 일어난다. vitest만 봤다면 못 잡았을 결함 — **L1 스크린샷이 아니었으면 통과했을 것.**
- **해결**: `"(허용오차 이내 · 최종 검증 전)"` 로 교체. 겸사 필드 구분자 누락(`메리츠화재32,000원`)도 수정.
- **검증**: L1 재실행 → `"메리츠화재 32,000원 (허용오차 이내 · 최종 검증 전)"` 확인.
</details>

<details><summary>이슈 4 — NEGATIVE_INVALID 도달 불가 분기 (아르고스 발견 → 팀장 수정)</summary>

- **증상**: `api_coverage_amount <= 0` 을 한 덩어리로 `ZERO_BASELINE` 처리 → 음수 기준금액에서
  계약 enum `NEGATIVE_INVALID` 가 **구조적으로 도달 불가**.
- **해결**: 음수 → `NEGATIVE_INVALID`, 0 → `ZERO_BASELINE`, 누락/비수치 → `ZERO_BASELINE` 로 분리. 테스트 2건 추가.
</details>

<details><summary>이슈 5 — background.js 주석이 설계 근거를 오기술 (팀장 발견 → 수정)</summary>

- **증상**: "content script 가 직접 fetch 할 수 없는 케이스(CORS/CSP)를 위해 background 위임" 이라고 서술.
- **왜 문제인가**: 계약 §9는 background 직접조회가 **아키텍처 결정**(MAIN world 인터셉터 전면 제거의 대체)이지
  CORS 우회 fallback이 아니다. 이 주석을 믿은 후속 개발자가 "CORS만 풀리면 content에서 직접 fetch"로
  되돌릴 위험이 있다 — 즉 **금지된 아키텍처로의 회귀 유인**.
- **해결**: 계약 §9 근거와 "되돌리지 말 것" 경고를 명시한 주석으로 교체.
</details>

**미해결(범위 외)**: `getInsuroJwt`(background.js:23) 미사용 경고. **A7(task-2800)에서 호출부가
제거되며 발생한 기존 이슈**로, base HEAD에도 존재함을 `git show HEAD:` 로 확인. 본 작업이 만든 것이
아니고 최소 diff 원칙에 따라 유지.

---

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|---|---|---|---|
| `extension/background.js` | `OHMY_PPD_FETCH_V1` 핸들러 + `isAllowedSender`/`classifyToken`/`isAllowedProductPremiumsUrl` (+137줄) | `grep "OHMY_PPD_FETCH_V1\|isAllowedProductPremiumsUrl"` OK | **verified** |
| `extension/content.js` | A1/A2/A4/A5/A6 전체 (+1039줄) | `grep "insuro-ppd-preview-btn\|findProductPremiumsResourceEntry\|ImmutablePreviewSnapshotV1"` OK | **verified** |
| `extension/__tests__/track-a-gates.test.ts` | 거부 분기·토큰 미유출 65건 (신규) | vitest 65 passed | **verified** |
| `extension/__tests__/track-a-preview.test.ts` | 계산·3축·스냅샷 34건 (신규) | vitest 34 passed | **verified** |
| `extension/__tests__/track-a-mutation.test.ts` | 뮤테이션·정적증명 12건 (신규) | vitest 12 passed | **verified** |

**planned 항목 0건.** `extension/manifest.json` **미수정**(권한 확대 불필요 — `*://*.ohmymanager.com/*`
host_permission 이 이미 존재). 신규 파일 **정확히 3개**(`new_file_limit: 3` 준수).

---

## MATCH / GAP / UNKNOWN

### MATCH (계약과 일치 확인)
- A1 클릭 전 네트워크 0 · A2 토큰 무저장/무로깅/ABSENT fail-closed · A3 allowlist+sender 전 분기
- A4 3중 게이트 fail-closed · owner key 메모리 전용·전송 0 · `VALID`/`INVALIDATED` 노출
- A5 resource timing primary · 우회 조립 0 · A6 계산식/3축/fail-closed 11코드/스냅샷 필드
- 금지사항 전부 부재(서버 전송·저장 버튼·`idempotency_key`·`reference`·`consent`·`fa_account_id`·`external_subject_key`)

### GAP (관측 불가로 축소 진행 — 지시서 규정에 따름)
1. **`age`·`plan_id` DOM 대조 불가** — 화면에 소스가 없음(실사 확정). 대조 제외 + `UNAVAILABLE_NO_DOM_SOURCE` 기록.
   → **integration 또는 실 페이지 실사에서 소스가 발견되면 대조 대상에 추가 필요.**
2. **`gender`/`insurance_type` URL 인코딩 미실증** — 실 API URL이 `M`/`F`를 쓰는지 숫자를 쓰는지 미확인.
   정규화 매핑 + 원문 폴백으로 방어했으나, **실 URL 1건만 관측되면 확정 가능**.
   background 의 값 검증도 형식 검증만 적용(코드에 GAP 주석 명시).
3. **화면 상단 회사 총액(3축의 축3) 셀렉터 미확정** — fixture·저장소 어디에도 해당 요소가 없다.
   현재 항상 `SCREEN_CELL_MISSING` 으로 기록하고 축1·축2만 비교한다(조용한 생략 0).
   → **실 DOM 셀렉터 확보 시 축3 활성화 필요.**
4. `guide_coverage_amount` vs `coverage_amount` 불일치에 대응하는 전용 enum이 계약에 없어
   `DIFFERS_ACROSS_COMPANIES` 로 처리(코드에 해석 판단임을 명시). **계약 소유자 확인 권장.**

### UNKNOWN (판단 보류 — 아누/회장 확인 요청)
1. `ImmutablePreviewSnapshotV1` 의 **명시적 JSON 스키마 블록이 계약에 없다.** §2-3-c의 정의
   ("§2-1 payload − reference/consent/idempotency_key")로 역산했으며, 특히
   `contract_version`·`client_version` 이 snapshot 레벨 소속인지는 계약에 직접 서술이 없다.
   (지시서가 `contract_version` 은 snapshot 포함으로 명시했으므로 그에 따름.)
2. `classifyOverallStatus` 는 `BLOCKED` 를 반환하지 않는다 — `BLOCKED` 는 스냅샷 조립 **이전** 게이트에서
   결정되기 때문(계약 "BLOCKED 면 조립 자체를 하지 않는다"). **integration 기대와 일치하는지 확인 필요.**

---

## 머지 판단

- **머지 필요**: **No — 이번 범위 아님**
- **브랜치**: `task/task-2802-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2802-dev1`
- **머지 의견**:
  지시서가 **PR 생성·main merge·배포·실고객 write·production 활성화·A↔B 실연결을 전부 미승인**으로
  명시했다. 따라서 **PR을 생성하지 않았고 머지도 하지 않았다.** 브랜치 커밋까지가 본 task의 산출물이다.
  계약 merge 이후 적용될 순서(계약 PR → B merge → A를 `B_MERGE_SHA` 위로 rebase·전체 재검증 → A merge
  → integration)에 따라, **A는 B 머지 후 rebase + 전체 재검증이 선행되어야 한다.**
  충돌 가능성: Track B와 파일 중첩 0이므로 낮음. 단 `vitest.config.ts` 등 공통설정이 B에서 바뀌면
  A의 테스트 include 경로가 영향받을 수 있으므로 rebase 시 재실행 필수.

---

## 커밋

```
d5e03e2 [task-2802] 헤르메스: L1 스모크 결과 반영 — 미리보기 배지 문구 계약 정합 + 필드 구분자
f443582 [task-2802] 아르고스+헤르메스: 검증 테스트 3종 추가 + A4 fail-open 결함 수정 + NEGATIVE_INVALID 분류 정정
a681cc2 [task-2802] 이리스: content.js A6 구현
4074e6f [task-2802] 이리스: content.js A1/A2/A4/A5 구현
777ec57 [task-2802] 불칸: background.js OHMY_PPD_FETCH_V1 핸들러 추가
```

diff (base 대비): **5 files changed, 2672 insertions(+), 1 deletion(-)**

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 아테나 | 계약 문서 발췌 / DOM 실사 / UX 문구 | **sonnet** | 분석 작업 — haiku 금지 규정 준수 |
| 아르고스 | 코드 실사 / 검증 테스트 작성 | **sonnet** | |
| 불칸 | background.js 구현 | **sonnet** | |
| 이리스 | content.js A1~A6 구현 | **sonnet** | 2단계 순차 위임(A1/A2/A4/A5 → A6) |
| 헤르메스(팀장) | 설계 판단·게이트·이슈 수정·L1 | opus | 직접 코딩은 팀원 결함 수정 5건에 한정 |

**haiku 사용 0건.**

---

## 셀프 QC 8항목

1. **영향 파일**: `extension/{content.js,background.js,__tests__/*}` 5개. manifest·공통설정·타 track 영향 0.
2. **엣지 케이스**: 토큰 부재/형식오류, entry 부재/파싱실패, 미지 인코딩, 중복/누락 쿼리키, 0·음수·비수치 금액, DOM 중복, API 중복, 화면 셀 부재 — 전부 테스트 커버.
3. **지시 일치**: A1~A6 구현, A7 미개입, PR·merge·서버전송 미수행. 지시서 정정판(A5 resource timing primary, `idempotency_key` 생성 금지) 반영.
4. **에러 처리·보안**: 전 게이트 fail-closed, 토큰·PII 미유출 실증, `textContent` 사용(XSS 방지), 자동 재시도 0.
5. **테스트 경로 커버**: 152개, 뮤테이션 3종으로 검증력 자체를 확인.
6. **발견 이슈 해결**: 5건 발견 → 5건 해결. 미해결 1건은 기존 이슈(범위 외, 사유 명시).
7. **아키텍처 원칙**: 순수 함수 분리 + 의존성 주입(performance/document/crypto)으로 테스트 가능성 확보. 중복 로직 0.
8. **인터페이스 문서화**: 신규 메시지 프로토콜 `OHMY_PPD_FETCH_V1`(요청/응답/실패코드)을 본 보고서와 코드 주석에 기록.

**Evidence**: vitest 152/152, L1 실브라우저 7항목 관측표, 스크린샷, grep 전수 결과, 뮤테이션 3종.

---

## 최종 검증 (완료 처리 시점, 2026-07-20 01:50)

### goal_assertions `npx vitest run` — 전체 스위트 대조 측정

지시서의 `goal_assertions` 는 extension 만이 아닌 **저장소 전체 스위트**다. 대조군(base)과 함께 측정했다.

| 대상 | Test Files | Tests | 소요 | exit |
|---|---|---|---|---|
| **본 브랜치** `task/task-2802-dev1` | 38 | **535 passed** | 6.98s | 0 |
| **대조군(base)** `task/task-2800-dev1` (`/tmp/a7check`) | 35 | **424 passed** | 6.27s | 0 |

**차이 = +3 files / +111 tests = 신규 테스트 정확히 그 수(65+34+12=111).**
→ **전체 스위트 회귀 0**을 대조군으로 실증.

### `[GOAL-GATE] TIMEOUT` — 하네스 경로 이슈 (제품 결함 아님)

finish-task.sh 의 GOAL-GATE 가 `TIMEOUT (fail-closed)` 로 차단했다. 원인을 단정하지 않고 좁혀서 확인했다.

- `goal_assertion_exec_isolated()` 는 `timeout 30s bash -c "$cmd"` 를 **finish-task.sh 의 cwd**에서 실행한다.
- 그 cwd 는 `/home/jay/workspace` 이고, 이 디렉토리에는 **`package.json`·`node_modules` 가 없다**(확인함).
  → `npx vitest run` 이 npm 레지스트리에서 vitest 를 받으려다 30초를 넘겨 `rc=124`.

**결정적 재현 (동일 명령, cwd만 변경)**:
```
cwd=/home/jay/workspace                        → rc=124 (timeout)
cwd=<worktree task-2802-dev1>                  → rc=0
```
→ 원인은 **실행 디렉토리**이며, goal assertion 자체는 올바른 위치에서 **PASS(535/535)**.
관측된 단서(30초 상한 · workspace 에 package.json 부재 · 올바른 cwd 에서 6.98초 완주)를 모두 설명한다.

> **아누 확인 요청**: GOAL-GATE 가 `PROJECT_PATH` 를 cwd 로 삼지 않는 것은 InsuRo 처럼
> workspace 밖에 있는 프로젝트 전반에 재현될 하네스 이슈로 보인다. 본 task 범위 밖이라 수정하지 않았다.

### 완료 처리 상태

| 항목 | 결과 |
|---|---|
| QC 게이트 | **WARN** (비차단) — 8 PASS / 12 SKIP / 3 WARN, **FAIL 0** |
| `git_evidence` | **PASS** — task-2802 커밋 5건 인식, uncommitted 0 |
| `l1_smoketest_check` | **PASS** |
| scope-guard | **PASS** (6 files in scope) |
| MERGE-BASE / IMPACT / CI-PREFLIGHT / G4 | 전부 PASS |
| 머지 | **미실행** — `merge_policy=none` honored, `.finalize-only` 생성, `.merge-done` 미생성 |
| PR | **미생성** (지시서 미승인) |
| `.done` | 생성됨 |

**WARN 3건 (전부 비차단, 사유 명시)**
1. `tdd_check` — 구현이 테스트보다 먼저 수정됨(TDD 순서 위반). 사실이다. 계약·실사 확정이 선행되어야
   테스트 기대값을 쓸 수 있는 구조였고, 대신 **뮤테이션 검증 3종**으로 테스트의 검증력 자체를 증명했다.
2. `scope_check` — audit-trail 에 `/tmp` 소재 임시 스크립트(스모크·L1)가 잡혀 "Unexpected" 로 표시.
   **저장소 커밋 대상 0**이며 `git status` 로 저장소 청결을 확인했다.
3. `claude_md_check` — `design/CLAUDE.md` 310줄. **타 팀 파일이며 본 작업과 무관**(수정 금지 대상).

### 완료 처리 중 관측된 이슈 (정직 기록)

1. **`.done` 이 QC 통과 이전(01:14)에 이미 존재**했고, done-watcher 가 30분 경과 기준으로
   `.done.escalated`(`stale_done_unacknowledged`)를 생성했다(01:44). `.done` 내용은 task-timer end
   페이로드와 동일하고 `end_time` 이 파일 mtime 과 일치한다. **다만 무엇이 그 시점에 timer end 를
   호출했는지는 로그에서 특정하지 못했다** — 본인이 호출한 바 없으며, 추측으로 단정하지 않고 그대로 보고한다.
   현 시점 기준 QC·게이트는 모두 통과했으므로 `.done` 내용 자체는 유효하다.
2. **`PROJECT_PATH` 환경변수는 finish-task.sh 에 전달되지 않는다** — 스크립트가 18행에서 빈 값으로
   초기화하고 위치 인자로만 받는다. 3번째 위치 인자로 전달해야 QC 의 `git_evidence` 가 올바른
   저장소를 본다(전달해도 `merge_policy=none` 이면 머지 블록은 스킵됨을 resolver 로 사전 확인 후 실행).
3. **worktree 를 `worktree_manager.py` 로 만들지 않아** `task-timers.json` 에 `worktree_path` 가 비어
   있고, 이 때문에 `git_evidence` 가 workspace 로 fallback 해 "커밋 0건"으로 최초 FAIL 했다.
   본 task 는 base 를 `task/task-2800-dev1` 로 고정해야 해서 수동 생성이 불가피했다.
   → **후속 개선 제안**: 지정 base 가 필요한 경우에도 timers 에 worktree 경로가 기록되도록 보완 필요.


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

