# task-2815 — Track A: Gemini 지적 1·2번 마이크로픽스 (PR #111)

- **작업 ID**: task-2815 (logical ID: `TRACKA_GEMINI_MICROFIX`)
- **팀**: dev1-team (헤르메스)
- **리포/브랜치**: `/home/jay/projects/InsuRo` → `task/task-2802-dev1` (PR #111)
- **완료 커밋**: `ac39249`(구현), `767eebd`(테스트) — push 완료 `174ed08..767eebd`

---

## SCQA

### Situation (상황)
PR #111에 Gemini 리뷰 지적 3건이 제기되었다. 3번(`readCompanyTopTotal` companyCode 대소문자 완화)은 회장 판단으로 **거부 확정**이며, 1·2번만 반영 대상이다. 둘 다 "현재 동작은 이미 옳지만 방어/진단을 더한다" 성격으로, 기존 fail-closed 동작을 바꾸면 안 되는 제약이 있었다.

### Complication (문제)
1. **작업 1** — `parseJwtPayload`가 `atob` 단독 디코딩을 사용. 각 바이트를 latin1 문자로 만들기 때문에, 현재 소비 필드(`consultantid`)는 ASCII라 무해하지만 **향후 한글 필드를 읽으면 값이 깨진다.**
2. **작업 2** — `chrome.runtime.lastError`를 읽지 않아 콘솔에 "Unchecked runtime.lastError" 경고가 남고, 실패 사유 문자열을 확보하지 못한다. 단 **분기 결과를 바꾸면 안 된다.**
3. 지시서가 조건부 경고를 걸었다: `recordVerdict`에 `reason` 키를 넣기 전에 스키마가 `extra="forbid"` 성격인지 먼저 확인할 것.
4. 작업 착수 시 worktree가 origin과 divergent(12 ahead / 56 behind) 상태였다 — 유실 위험.

### Question (질문)
기존 fail-closed 동작과 202건 테스트를 단 1건도 깨뜨리지 않으면서, `extension/` 밖 변경 0건으로 두 마이크로픽스를 반영할 수 있는가?

### Answer (결론)
**가능했고, 반영 완료.** 207 passed (기존 202 + 신규 5, 회귀 0건). `extension/` 밖 변경 0건. 실제 Chrome 147 엔진에서 결함 재현과 수정 실효를 모두 증명했다.

---

## 수행 내용

### 사전 조치 — worktree divergent 해소 (유실 0건 증명)
착수 시 worktree HEAD(`4d77929`)와 origin(`174ed08`)이 갈라져 있었다. 커밋 메시지는 같은데 해시가 달라 origin이 rebase된 상태로 판단되었다. **추정으로 reset하지 않고** `git cherry origin/task/task-2802-dev1 HEAD`로 검증한 결과 12건 **전부 `-`**(동등 패치가 이미 origin에 존재) → 로컬 고유 작업 0건임을 확인한 뒤 origin으로 동기화했다.

### G1 설계 게이트 — `reason` 키 허용 판정
지시서의 조건부 경고를 해소하기 위해 직접 확인:
- `recordVerdict`(content.js L532)는 `Object.assign({ts:Date.now()}, partial)` — **자유형식, `extra="forbid"` 아님**
- `reason` 키는 이미 기존 코드 **L1002·L1131에서 사용 중**
- 기존 테스트(`track-a-preview.test.ts` L833)의 타입 정의에도 `reason?: string`이 **정식 존재**
- verdict는 **로컬 진단용**이며, 계약 §2-1 `ImmutablePreviewSnapshotV1`(서버 전송 payload)과 별개 객체

→ `reason` 삽입은 계약 위반이 아님. 지시서대로 반영.

### 작업 1 — `parseJwtPayload` UTF-8 안전 디코딩 (content.js L563-564)
```diff
-    const json = atob(base64);
+    const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
+    const json = new TextDecoder("utf-8").decode(bytes);
     const payload = JSON.parse(json);
```
`try { ... } catch (_) { return null; }` 구조 유지 → 디코딩 실패 시 여전히 `null`(fail-closed 불변).

### 작업 2 — `chrome.runtime.lastError` 명시 확인 (content.js L1079-1088)
```diff
     (response) => {
-      if (!response || response.ok !== true) {
+      // chrome.runtime.lastError 는 읽어야 콘솔 경고가 사라진다. 읽기만 하고
+      // 판정은 기존 가드에 그대로 맡긴다(동작 변경 금지).
+      const transportError = chrome.runtime.lastError
+        ? chrome.runtime.lastError.message || "unknown"
+        : null;
+      if (transportError || !response || response.ok !== true) {
         const code = (response && response.code) || "TRANSPORT_ERROR";
-        recordVerdict({ code, token_source: "URL_QUERY" });
+        recordVerdict({ code, token_source: "URL_QUERY", reason: transportError });
         setOhmyPpdStatus(userMessageForOhmyPpdCode(code));
         return;
       }
```
유일한 논리 추가는 `transportError ||` — "lastError가 있는데 response도 온" 경우를 실패로 처리. 그 외 분기 결과 불변.

### 신규 테스트 5건 (`extension/__tests__/track-a-microfix.test.ts`, 191줄)
1. 한글 payload → `name`이 정확히 `"전종혁"`, `consultantid`도 정상
2. 기존 ASCII payload → 회귀 없음
3. 깨진 base64 → `null` (fail-closed 유지)
4. `lastError` mock → `TRANSPORT_ERROR` verdict + `reason` 일치 + 사용자 안내 문구 노출
5. `lastError` 없는 정상 response → `previewState=VALID`, `code=null`, "조회 완료" (성공 경로 회귀 없음)

---

## 검증 결과

### 테스트 (지시서 필수 조건)
```
npx vitest run extension/__tests__/
Test Files  7 passed (7)
     Tests  207 passed (207)
```
- **Baseline 202건 → 207건. 기존 202건 중 깨진 것 0건.** (보험나이 경계·뮤테이션 테스트 포함 전부 통과)
- baseline은 착수 직후 별도 측정하여 기준선으로 확정한 값이다.

### 뮤테이션 검증 (표면 통과 배제)
신규 테스트가 실질 검증력을 갖는지 확인하기 위해, 구현을 각각 되돌려 실행:
- **뮤테이션 1** (`TextDecoder` → `atob` 원복) → 한글 테스트 **1건만 정확히 FAIL**, 나머지 4 passed
- **뮤테이션 2** (`transportError` 가드 제거) → lastError 테스트 **1건만 정확히 FAIL**, 나머지 4 passed
- 두 뮤테이션 모두 `git checkout`으로 원복 확인 완료

→ 테스트가 "그냥 통과하는" 것이 아니라 대상 결함을 실제로 잡아냄이 증명됨.

### `extension/` 밖 변경 0건 증명
```
git diff --stat origin/task/task-2802-dev1..HEAD   (push 전 기준)
 extension/__tests__/track-a-microfix.test.ts | 191 +++++++++++++++
 extension/content.js                         |  12 +-
 2 files changed, 200 insertions(+), 3 deletions(-)
```
`server/**`, `src/**`, `docs/contracts/**`, `.github/**`, `extension/background.js`, `extension/manifest.json` **변경 0건**.

### 빌드
- `node --check extension/content.js` → 문법 OK (실제 배포되는 파일)
- `npm run build` → **성공** (`✓ built in 11.81s`, PWA precache 172 entries)
- 참고: `extension/`은 vite 빌드 대상이 아니다(MV3 정적 JS). 빌드는 회귀 확인 목적.

---

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

- **서버 재시작**: 해당없음 (브라우저 확장 content script — 서버 컴포넌트 아님)
- **API 응답 확인**: 해당없음 (Track A 불변식상 서버 전송 코드 추가 금지)
- **스크린샷**: `/home/jay/workspace/memory/reports/task-2815-l1-smoke.png` (35,290 bytes)

### 실행 내용 — jsdom mock이 아닌 실제 브라우저 엔진 검증
vitest는 jsdom 환경이므로, 실제 Chrome 엔진의 `atob`/`TextDecoder` 동작을 별도로 확인했다.
Playwright로 실제 브라우저를 띄우고 **배포될 `content.js` L555-570의 `parseJwtPayload`를 그대로** 실행:

| 항목 | 결과 |
|---|---|
| 엔진 | Chrome/147.0.0.0 (실제 브라우저) |
| 한글 `name` 복원 | `"전종혁"` ✅ 정확 |
| `consultantid` | `"c-1"` ✅ |
| ASCII 회귀 | ✅ 없음 |
| 깨진 base64 → null | ✅ fail-closed 유지 |
| 콘솔 에러 | **0건** |

### 대조군 — 결함 실재 증명
동일 브라우저에서 변경 **전** 방식(`atob` 단독)을 실행한 결과:
- 변경 전: `name = "ì „ì¢…í˜"` ← **한글 깨짐 (결함 실재 확인)**
- 변경 후: `name = "전종혁"` ← 정확 복원

→ Gemini 지적 1번이 실제 결함이었고 수정이 실효함을 실브라우저에서 증명.

- **브라우저 정리**: `browser_close` 호출 완료

---

## 발견 이슈 및 해결

| 이슈 | 처리 |
|---|---|
| worktree가 origin과 divergent (12 ahead / 56 behind) | `git cherry`로 로컬 고유 커밋 0건 증명 후 동기화. **유실 0건** |
| 지시서의 `reason` 키 조건부 경고 | 코드·테스트 양쪽 교차 확인으로 허용 판정 (위 G1 섹션). 스키마 위반 아님 |
| 신규 테스트에 미사용 import(`beforeEach`) | 제거 후 grep 검증 + 재실행(207 passed) |

### 범위 밖 발견 (미조치, 의도적)
`isJwtValid`(content.js L85 부근)에도 **동일한 `atob` → `JSON.parse` 패턴**이 남아 있다.
- **미조치 사유**: 지시서가 `parseJwtPayload` 단일 함수만 명시했고, `isJwtValid`는 만료(`exp`) 검증 목적이라 ASCII 숫자 필드만 읽으므로 현재 실동작 영향이 없다. 지시 범위를 넘지 않기 위해 손대지 않았다.
- **판단 요청**: 동일 성격의 선제 방어를 원하면 별건 task로 처리 권장.

---

## 지시서 금지사항 준수 확인

- 3번 지적(`readCompanyTopTotal` 대소문자 완화) 반영 **안 함** ✅ (grep으로 해당 라인 불변 확인)
- 테스트 skip·assertion 완화·required check 변경 **없음** ✅
- 서버 전송 코드 추가 **없음** ✅ (Track A 불변식 유지)
- rebase·force push·브랜치 재생성 **없음** ✅ (일반 커밋 2건 + fast-forward push만)
- **PR 신규 생성 안 함** ✅ — PR #111(OPEN)에 두 커밋 자동 반영 확인

---

## 머지 판단

- **머지 필요**: **No** (이 task 범위 아님)
- **브랜치**: `task/task-2802-dev1` (기존 브랜치, 신규 생성 안 함)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2802-dev1`
- **머지 의견**: 본 task의 완료 조건은 "PR #111에 커밋 push"까지다. PR #111은 task-2802 트랙 소유이므로 머지 판단은 해당 트랙/아누에게 위임한다. 코드 관점에서는 위험도 낮음 — 동작 변경 없는 방어적 개선, 207 passed, `extension/` 밖 영향 0.

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 이리스 | 프론트엔드 구현 (content.js 2건) | **sonnet** | 코딩 작업 기본값 |
| 아르고스 | 테스터 (신규 테스트 5건) | **sonnet** | 테스트 설계 판단 필요, haiku 부적합 |
| 헤르메스(팀장) | 설계 게이트·검증·통합 | opus | 직접 코딩 없음 (미사용 import 1줄 정리만) |

- haiku 사용 0건.
- 불칸(백엔드)·아테나(UX/UI) 미소집 — `extension/content.js` 단일 파일 프론트 작업이라 역할 해당 없음(페르소나 고정 규칙 준수).

---

## 셀프 QC (8항목)

1. 지시 범위 준수 — ✅ `extension/` 2파일만, 밖 0건
2. 금지사항 위반 — ✅ 없음 (위 섹션 개별 확인)
3. 테스트 통과 — ✅ 207/207, 기존 202건 회귀 0
4. 실동작 검증 — ✅ 실제 Chrome 147에서 결함 재현 + 수정 실효 증명
5. 파일 실재 확인 — ✅ 191줄, 35KB 스크린샷 (0byte 아님)
6. Edit 후 grep 검증 — ✅ `TextDecoder`(L564), `transportError`(L1082/1085/1087) 전부 확인
7. 커밋/푸시 — ✅ 2커밋, fast-forward push, PR #111 반영 확인
8. 보고서 작성 — ✅ 본 파일

---

## 완료 처리 결과 — ★ `.done` 미생성 (아누 판단 요청)

### finish-task.sh 실행 방식과 그 근거
`FINALIZE_ONLY=1 bash scripts/finish-task.sh task-2815 dev1 <worktree>` 로 실행했다.

task md의 `merge_policy: tiered` 때문에 기본 실행 시 merge block에 진입해 `worktree_manager finish` + PR gate가 동작한다. 그러나 **PR #111은 task-2802 트랙 소유**이고, 본 task의 완료 조건은 "커밋 push까지"이며 지시서는 머지를 요구하지 않았다. 타 트랙 소유 PR의 머지는 되돌리기 어려운 outward-facing 행위이므로 **executor 권한 밖으로 판단**해 merge block만 차단했다. QC·검증 단계는 전부 정상 수행했다.

### QC 결과
**WARN(통과)** — `file_check` PASS, `data_integrity` PASS, `schema_contract` PASS

### scope_guard 차단 — false positive 판정
scope_guard가 18건 위반을 보고하며 머지를 차단하고 `.escalate`를 생성했다. **이는 오탐이며, 근거는 다음과 같다:**

| 항목 | 값 |
|---|---|
| scope_guard diff base | `merge-base(origin/main,HEAD)` = `097134f` |
| 해당 구간 커밋 수 | **14건** |
| 그중 task-2815 소유 | **2건** |
| task-2815 커밋이 바꾼 파일 | `extension/content.js`, `extension/__tests__/track-a-microfix.test.ts` **(2개뿐)** |

위반으로 지목된 파일의 **실제 소유 커밋**:
- `server/main.py` → `69ee0a3 [task-2801] 페룬`
- `extension/background.js` → `fbdb53a [task-2802] 불칸`
- `docs/contracts/**`, `server/**`, `.github/**`, `supabase/**` → 전부 선행 task 커밋 소유

**즉 scope_guard는 브랜치 누적 diff를 보기 때문에 선행 13커밋의 변경을 본 task 것으로 오귀속했다.** 지시서가 "새 브랜치 만들지 말고 이 브랜치 위에 커밋하라"고 명시했으므로 이 오탐은 **구조적으로 필연**이다.

**선례 일치**: task-2804·task-2806이 동일 브랜치에서 동일한 scope_guard false positive로 차단·escalate됐다 (선례 2건).

### 조치
- **override 시도 안 함** — `FINISH_TASK_WORKTREE_STRICT` 비활성화 등 안전게이트 무력화는 수행하지 않았다.
- **수동 `.done` 생성 안 함.**
- 머지 차단 자체는 무해하다 — 본 task는 애초에 머지하지 않기로 결정했으므로 의도와 일치한다. **실질 영향은 `.done` 미생성뿐이다.**
- 생성된 마커: `.escalate`, `.scope-violation.json`, `.qc-done`, `.qc-result`, `.failure-envelope.json`, `.result.json`

### 아누 판단 요청 사항
1. scope_guard false positive에 대한 `.done` 수동 승인 여부 (선례 task-2804/2806과 동일 처리)
2. 기존 브랜치 누적 커밋 위에서 작업할 때 scope_guard가 커밋 소유권 기준으로 판정하도록 개선할지 (선례 3건 누적 — 구조적 이슈)

---

## 비고

- **버그 유무**: 신규 버그 없음. 기존 결함 1건(UTF-8 디코딩) 수정.
- 두 변경 모두 **기존 fail-closed 동작을 보존**한다. 작업 2의 `transportError ||` 추가는 실패 방향으로만 엄격해지므로 안전 측 변경이다.
- **지시서 완료조건 1~3번은 전부 충족**됐다 (push 완료, `extension/` 밖 0건 증명, PR 신규 생성 안 함). 4번(보고+콜백) 중 보고서·result.json 기록 완료.

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

