# TRACK_A (Track A) — 확장 production 이식 (로컬 전용, 서버 전송 OFF)

> **★ 개발 dispatch 승인됨(2026-07-19). PR·merge·배포·실고객 write·production 활성화는 여전히 미승인.**

## ★ ID 규약 (숫자 task ID 하드코딩 금지)
- **logical ID = `TRACK_A`** — 본 문서의 영구 식별자.
- **`<ALLOCATED_TASK_ID>`** = dispatch 시점에 **dispatch 시스템이 실제로 할당한 숫자 ID**로 치환한다.
  - ⚠️ ANU 가 미리 고른 번호·브랜치 선점은 **예약이 아니다**(2026-07-19 실증: 선점한 `task-2798` 이 타 task 에 할당됨).
  - 치환은 **등록 성공 후 실제 할당값을 조회해서** 수행한다. 추측 금지.
- **`<TEAM>`** = dispatch 시 확정되는 팀 슬러그(예: `dev5`).
- 브랜치·worktree·보고서 경로 모두 위 치환 결과를 사용한다.

## 레벨
Lv.4 프로그램의 하위 Track. **PR·merge·배포·운영활성화 전부 회장 승인 게이트.**

## base (★ 확정 — A7 완료분 위에서 이어서)
- **분기 기준**: **`task/task-2800-dev1`** (= A7 완료 상태, ANU 독립검증 `npx vitest run extension/__tests__/` **41 passed** 확인)
- worktree: 신규 생성. **A7 커밋 3개를 되돌리지 마라.**
- **계약 문서는 merge 안 된 브랜치에서 읽는다**:
  ```bash
  git -C /home/jay/projects/InsuRo show bb1c219d0451cf3798aff624226b4f25d194f5d9:docs/contracts/INSURO_LV4_PROGRAM_CONTRACTS_V1.md | sha256sum
  ```
  → "단일 소스" SHA-256 과 일치해야 함. 다르면 STOP_REPORT.
- ⚠️ 계약 커밋을 브랜치에 merge·cherry-pick 금지 — 읽기 전용 참조.

## ★ 이번 범위 = **A1~A6 만**
**A7 은 이미 완료됐다**(활성 write 경로 제거 + 3중 증명). 다시 하지 마라.


## ★ 저장소·경로 기준 (모호성 제거)
- **대상 저장소**: **InsuRo** (`/home/jay/projects/InsuRo`, remote `origin`)
- 본 문서의 **모든 상대경로는 InsuRo 저장소 루트 기준**이다. 예: `docs/contracts/…` = `<InsuRo>/docs/contracts/…`
- 계약 PR merge 전에는 해당 파일이 **`main` 에 없다** — 브랜치 `contract/ohmy-ppd-v1` 에만 존재한다. 확인 방법:
  ```bash
  git -C /home/jay/projects/InsuRo show <계약커밋>:docs/contracts/INSURO_LV4_PROGRAM_CONTRACTS_V1.md | sha256sum
  ```
- ⚠️ **`/home/jay/workspace` 는 대상 저장소가 아니다.** 본 지시서 파일 자체가 거기 있을 뿐이며, **경로 해석 기준이 아니다.**

## 단일 소스 (저장소 내부 canonical — memory 사본 아님)
- `docs/contracts/INSURO_LV4_PROGRAM_CONTRACTS_V1.md` (SHA-256 `c7fe00a75606f94de218f27f65f44315bae97ef993be1d7b217ea322febca873`)
- `docs/contracts/OHMY_PPD_DATA_CONTRACT_V1.md` (SHA-256 `fc081a76da0c9213faa7cee1a64d183981b348e42ac788d81466dd9d3e720986`)
- **착수 시 두 파일의 SHA-256 을 재확인**. 다르면 **STOP_REPORT**.

## expected_files
```yaml
allowed_resources:
  paths:
    - "extension/content.js"
    - "extension/background.js"
    - "extension/manifest.json"
    - "extension/__tests__/**"
  forbidden_paths:
    - "server/**"          # Track B 영역 — 중첩 0
    - "supabase/**"        # Track B 영역
    - "src/**"
    - "package.json"       # 공통설정 — 양 track 수정 금지
    - "vitest.config.ts"   # 공통설정
    - "tsconfig.json"      # 공통설정
    - "docs/contracts/**"  # 계약 = 읽기 전용
    - ".github/**"
    - ".env*"
  new_file_limit: 3
  commands: ["git","npm","node","npx"]
  merge_policy: "none"
```
```yaml
external_control_artifacts:
  # ★ 아래는 **InsuRo 저장소 밖**(/home/jay/workspace)의 control-plane 산출물이다.
  #   - git stage · commit 대상 **0**
  #   - effective diff 산정 대상 **0**  (expected_files 위반 판정에 포함되지 않는다)
  #   - 절대경로로만 접근한다. 저장소 상대경로로 해석 금지.
  - /home/jay/workspace/memory/reports/<ALLOCATED_TASK_ID>-trackA.md
```
**타 track 중첩**: Track B(`server/`·`supabase/`)와 **0**. 공통설정 3개는 양쪽 수정 금지.

## ★ 진단브랜치 격리 (절대)
`task/task-2787-dev5` · `task/task-2787+2-dev5` · `task/task-2788-dev5` →
**merge · cherry-pick · rebase 이식 · 수동 복사 · 부분 포팅 · 코드 스니펫 붙여넣기 전부 금지.**
지식 전달은 **위 계약 문서를 통해서만**. 해당 브랜치 파일을 열어 코드를 옮기는 행위 자체를 금지.

## ★ 이번 dispatch 의 범위 (회장 확정 2026-07-19)
**개발까지만.** 아래는 **전부 미승인**이며 착수 금지:
- PR 생성 · main merge · 배포 · 실고객 write · production 활성화 · A↔B 실연결

**merge 순서(계약 merge 이후 적용될 계획 — 지금은 참고용)**:
계약 PR merge → B first merge → A 를 `B_MERGE_SHA` 위로 rebase·전체 재검증 → A merge → integration
※ **지금은 어느 것도 실행하지 않는다.** 브랜치 커밋까지가 이번 범위.

### ★ 봇 실행 안전 프로토콜 (필수)
1. **Edit·commit·finish 직전마다 cancel 마커 재확인** — `.cancelled` 존재 시 즉시 중단
2. **동일 worktree·branch 를 다른 task 가 쓰고 있지 않은지** 착수 시 확인
3. **`.done` 자동 마커는 완료 증거가 아니다** — ANU 독립검증이 권위. 산출물(커밋·보고서)을 반드시 남길 것
4. 실패·중단 시에도 **보고서를 남기고** ANU callback 발사


## 구현 요구

### A1. 사용자 명시 클릭 기반 background 직접 조회
- ohmymanager 페이지 버튼 **명시 클릭 시에만** 동작. 클릭 전 A-0 관련 네트워크 요청 **0**.
- content(ISOLATED) → 현재 URL 에서 **토큰 1회 읽기** → background 로 전달 → `fetch(API, {headers:{Authorization:"Bearer "+token}})`.
- **MAIN world 주입·fetch/XHR 인터셉터 신규 추가 절대 금지.**

### A2. 토큰 취급 (계약 §AMENDMENT-1)
- **저장·캐시·로깅·서버전송 0.** 함수 스코프 1회 사용 후 폐기.
- `token_source: "URL_QUERY" | "ABSENT"` 기록. **ABSENT → fail-closed**(과거 토큰 재사용·우회 조립 금지).
- 실패·거부 경로에서 토큰이 **에러객체·재시도큐·verdict·로그에 잔존 0**.

### A3. URL allowlist (fetch 전 게이트)
`protocol=https` · `hostname === "mmlfcp.ohmymanager.com"`(정확일치) · `pathname === "/api/ProductPremiums"`(정확일치) · **포트 = 생략 또는 443**.
+ **query key allowlist**: 허용 키만(`age`,`gender`,`insurance_type`,`plan_id` 등 실사 후 확정). **미지 키·중복 키·비정상 길이·enum 위반·나이범위 위반 거부.**
+ **sender 검증**: `sender.id === chrome.runtime.id` **및** `sender.url`/`sender.origin` 의 origin 이 `https://mmlfcp.ohmymanager.com`.
불일치 → **fetch 함수 자체를 호출하지 않고** 거부 코드 반환.

### A4. ★ 계정전환·stale URL 차단 (계약 §3-4, §3-5)
fetch 전 **아래 전부 일치**해야 진행. 하나라도 불일치 → **fail-closed** + "조회하기를 다시 눌러주세요":
- resource URL 의 `age/gender/insurance_type/plan_id` == **현재 화면 조건**
- resource entry 생성시각 **> 현재 세션 마지막 조회 시각**
- 현재 `local_owner_observation_key` == 조회 당시 `local_owner_observation_key`
**owner 변경 감지 시 (A 의 책임 범위)**: 이전 응답·계산·미리보기·**preview snapshot 즉시 폐기** + 미리보기 상태를 **`INVALIDATED`** 로 표시(계약 §3-4-a 상태 정의: `VALID` | `INVALIDATED`).
> ⚠️ **"저장 버튼 비활성화"는 A 의 책임이 아니다** — A 에는 저장 버튼이 없고 추가도 금지다(A6-a).
> **버튼 비활성화는 integration task 소유**이며, integration 은 **A 가 노출한 `INVALIDATED` 상태를 근거로** 버튼을 잠근다.
> A 는 **상태를 정확히 노출할 의무**까지, integration 은 **그 상태를 UI 에 강제할 의무**를 진다.
전환 판단은 **토큰 원문 비교가 아니라 `local_owner_observation_key` 비교**로 한다(같은 사용자도 토큰은 갱신되므로 토큰 비교는 오탐). **V1 에서 `external_subject_key` 는 쓰지 않는다.**

**★ 값 구분 (계약 §2-1-d — 혼동 시 보안사고)**:

| 값 | 소재 | 신뢰 | 전송 |
|---|---|---|---|
| **`local_owner_observation_key`** | 클라이언트 **메모리 전용** | **비신뢰**(관측용) | **전송 0** |
| **`external_subject_key`** | — | **V1 미사용**(§2-1-a-0) | 생성·전송 **금지** |

→ 계정전환 탐지는 **`local_owner_observation_key`** 로 한다. 이 값은 **payload 에 넣지 않는다.**

#### ★ A4 결정항목 — 실사 후 확정 (정정판)

> **`plan_id` 소스는 이미 확정됐다 → resource timing URL 쿼리**(A5). **더 이상 STOP 조건이 아니다.**
> 직전 판본의 "plan_id DOM 소스를 확정 못 하면 즉시 STOP" 은 **잘못된 지시였으므로 삭제한다.**

| # | 결정할 것 | 근거로 삼을 것 |
|---|---|---|
| 1 | **"현재 화면 조건" 의 DOM 소스** — resource URL 값과 대조할 화면측 값을 어디서 읽는가 | 실제 화면 실사. **읽을 수 없는 항목은 대조 대상에서 제외하고 그 사실을 보고** |
| 2 | **"마지막 조회 시각" 기준 시점** | 관측으로 확정 |
| 3 | **`local_owner_observation_key` 산출 방법** | 메모리 전용·전송 0(§2-1-d) |

**STOP 조건 (이것만)**: resource timing 으로도 URL 복구가 **구조적으로 불가능**함이 실증될 때. 그 외에는 **관측 가능한 범위로 축소해 진행하고 GAP 을 보고**하라 — 미확정 항목이 있다고 전체를 STOP 하지 마라.


### A5. ★ URL 구성 — **resource timing 이 primary** (정정 2026-07-19)

> **이전 지시서 오류 정정**: 직전 판본은 "DOM 1순위 / resource timing fallback" 이라고 썼다. **틀렸다.**
> **A-0 스파이크(task-2788)가 Resource Timing API 만으로 URL 복구를 이미 실증했고, 회장 라이브 실행으로 확인됐다**:
> `url_construction: "SOLVED"` · `token_source: "URL_QUERY"` · `http_status: 200` · `company_count: 10`
> resource timing entry 의 URL 에 **`age`·`gender`·`insurance_type`·`plan_id` 쿼리가 전부 들어 있다.**
> 실 DOM 에는 `plan_id` 요소가 없다(fixture 실사 0건) — **DOM primary 는 애초에 불가능한 지시였다.**

- **primary = `performance.getEntriesByType("resource")`** 에서 `/api/ProductPremiums` entry 복구 → 그 URL 의 쿼리를 사용.
  참조 구현: `task/task-2788-dev5` 의 `content.js` `findProductPremiumsEntryFromResourceTiming()` — **방식만 참조, 코드 복사 금지.**
- **DOM 은 교차검증용**: 화면에서 읽을 수 있는 값(`age`·`gender`·`insurance_type` 등)은 resource URL 값과 **대조**해 불일치 시 fail-closed(A4).
- ⚠️ **인터셉터(MAIN world fetch/XHR 후킹) 복귀 금지** — resource timing 은 인터셉터가 아니다.
- resource entry 미발견(버퍼 유실·SPA clear·첫 조회 전) → **"조회하기를 먼저 눌러주세요" UX 로 흡수.** 우회 조립 금지.

### A6. 로컬 분석·검증·미리보기 (여기까지만)
계약 §1 계산 + 3축 교차검증(계산값 / 화면 셀 / 화면 상단 총액) → **로컬 미리보기**.
**서버 전송 코드 미포함(추가 금지).** 실제 전송은 **A↔B 통합 task 에서만**.

#### ★ A6-a. payload builder 구조까지만 — **`idempotency_key` 생성 금지** (holistic 검토 정정 2026-07-19)

> **이전 초안 오류**: "저장 액션 1회 단위로 `idempotency_key` 생성"을 A 에 지시했다. **그런데 A 에는 저장 액션이 없다.**
> 없는 것을 단위로 삼으라는 지시라, dev 는 미리보기 시 / 조회 시 / 페이지 로드 시 생성하거나 **가상의 저장 버튼을 미리 추가**하게 된다(= A 의 "서버 전송 코드 미포함" 불변식 침해).

**★ Track A 의 산출물 = `ImmutablePreviewSnapshotV1`** (계약 §2-3-c — 회장 지시 3번)
- 내용: 조회조건·선택담보·보험사별 계산·검증결과 등 **미리보기 확정 데이터**
- **미포함**: `reference` · `consent` · `idempotency_key` — 미리보기 시점에 **아직 존재하지 않는 값들**이다
- **★ A 는 최종 `request_body_bytes` 를 만들지 않는다.** (만들 수 없다 — 위 3개가 없으므로)
- **`idempotency_key` 생성 금지.**

**Integration task 의 책임 (A 범위 밖)**: 첫 저장 클릭 시 `ImmutablePreviewSnapshotV1` + **`reference` + `consent` + `idempotency_key`** 를 합쳐 **`request_body_bytes` 를 정확히 1회 생성** → 재시도 시 **그 bytes 를 그대로 재사용**(재생성 금지).

**필드 규약 (계약 §2-1)**:
- **`reference_type` + `reference_id`** — **integration 이 채운다.** A 는 snapshot 에 넣지 않는다. (V1 허용값 = `"CUSTOMER"` 단독)
- `contract_version` 은 snapshot 에 포함 / **`consent{granted_at,consent_version,scope,source}` 는 integration 이 채운다.**
- **`fa_account_id` 를 payload 에 넣지 않는다**(계약 §2-1-b). 넣으면 서버가 `CLIENT_AUTHORITY_FIELD_FORBIDDEN`/400 으로 거부한다.
- **`external_subject_key` 는 V1 범위 밖**(§2-1-a-0) — 생성·전송 금지.
- **`BLOCKED` 상태면 payload 조립 자체를 하지 않는다.** 실패 코드는 **local verdict / UI 에만** 기록(계약 §4-4-a). **payload 에 넣으려고 조립하면 계약 위반.**

### A7. ✅ **완료됨 (task-2800, 커밋 3개)** — 이번 범위 아님. 되돌리지 말 것.
<details><summary>원 요구사항(참고)</summary>

### (완료) 기존 활성 write 경로 구조적 차단
`origin/main` 에 **살아있는** extension 서버 POST(`background.js` 의 `fetch(\`${base}/api/insuro/ohmy-capture\`, POST)`)와 레거시 캡처 버튼을 **제거**한다.
**증명 3중 (전부 0)**:
1. **정적**: 서버 POST **함수 정의 0** · 해당 경로로의 **호출 도달경로 0**
2. **동적**: 실행 중 **`insuro.biz` write 요청 0**(행동 테스트로 관측)
3. **뮤테이션**: 차단을 의도적으로 되돌리면 **테스트가 실패**해야 함
※ **방식만 참조**(A-0 브랜치에서 검증된 접근) — **코드 복사 금지**.
</details>

## 검증
- 단위+행동 테스트: A2~A5 각 거부 분기 · 토큰 미유출(전 경로) · 클릭 전 A-0 네트워크 0 · A7 3중 증명
- **뮤테이션 검증 필수**(결함 재주입 시 실패 확인)
- **ANU 가 `npx vitest run` 전체 독립 재실행**. 기존 테스트 감소 금지.

## goal_assertions
- `npx vitest run`

## STOP_REPORT 조건
계약 문서 SHA-256 불일치 · A4 차단이 구조적으로 불가 · 서버 전송 없이 미리보기 불가 · 진단브랜치 코드가 필요 · manifest 권한 확대 필요 · 공통설정 파일 수정 필요

## 금지
서버 전송 코드 추가 · 토큰 저장/캐시/로깅 · 자동 실행 · MAIN world 인터셉터 · 진단브랜치 이식(수동 복사 포함) · Track B 영역 수정 · 공통설정 수정 · **PR·merge·배포·운영활성화**(전부 회장 승인)

## 산출물 & 보고
1. 브랜치 커밋. **PR 생성도 회장 승인 후.**
2. 보고서는 **`/home/jay/workspace/memory/reports/<ALLOCATED_TASK_ID>-trackA.md`** 에 쓴다(저장소 커밋 대상 아님). 내용: diff / A1~A7 각 근거 / **A7 3중 증명 결과** / 뮤테이션 결과 / vitest 카운트 / MATCH·GAP·UNKNOWN
3. 봇 종료 전 ANU callback(envelope만·UTF-8 ≤3900B, collector=ANU key c119085addb0f8b7)