# task-2788+3 — A-0 빌드에서 레거시 실서버 전송 경로 차단 (라이브 실행 전 마지막)

- **팀**: dev5-team (마르둑)
- **레벨**: Lv.2 / same-branch `task/task-2788-dev5` (**PR·merge·배포 없음** — 지시서 준수)
- **작업 시각**: 2026-07-18 22:34 ~ 22:52 (KST)
- **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2788-dev5`
- **커밋**: `3b71666`(U2) · `ab3fe41`(U1) · `f2973fa`(구 주석 정정) · `960d788`(테스트)

---

## S — Situation

task-2788+2 까지 완료되어 A-0 스파이크(`DIRECT_FETCH_SPIKE_RUN`)가 515 PASS·ANU 독립검증까지 통과했다. 회장께 확장을 전달해 **라이브 실행**만 남은 단계다.

## C — Complication

패키징 검토 중 **레거시 경로 동거**가 발견됐다. ohmymanager 페이지에 레거시 "📊 InsuRo로 분석" 버튼(`insuro-helper-btn`)이 여전히 뜨고, 클릭 시 `OHMY_MATRIX_CAPTURED` → `pushMatrixToInsuro()` → `fetch(\`${base}/api/insuro/ohmy-capture\`, {method:"POST"})` 로 **실고객 데이터가 실서버로 POST** 된다.

두 버튼은 화면 **같은 위치(우하단)** 에 뜬다. 회장이 A-0 버튼 대신 이 버튼을 한 번만 잘못 눌러도 **"서버 전송 0" 불변식이 깨진다.** "누르지 마세요"라는 인적 통제에 의존할 수 없다.

## Q — Question

A-0 경로와 +1/+2 보안가드(S1~S5, T1~T3)를 **하나도 훼손하지 않으면서**, 실서버 전송 경로를 **분기(if)로 우회 불가능하게** 구조적으로 제거할 수 있는가? 그리고 그것을 검증하는 테스트가 **실제로 결함을 잡는가**?

## A — Answer

**U1·U2 모두 구조적 제거로 처리했고, 실브라우저에서 직접 관측했다.** 전체 **526 PASS**(515 → +11, **감소 0**). 추가로 **뮤테이션 2건을 직접 주입해 신규 테스트가 실제로 결함을 잡음을 실증**했고(각각 정확히 2건 FAIL), L1 실브라우저에서 **레거시 버튼 미존재 / A-0 버튼 정상 부착 / ohmy-capture 요청 0건**을 눈으로 확인했다.

---

## 구현 내역

### U1. 레거시 캡처 버튼 부착 제거 — `extension/content.js` (이쉬타르)

ohmymanager 분기에서 `initInsuroFloatingButton()` **호출부를 삭제**했다.

```diff
-    // 매트릭스 캡처 + Z-① 플로팅 버튼만 초기화
-    if (document.readyState === "loading") {
-      document.addEventListener("DOMContentLoaded", initInsuroFloatingButton);
-    } else {
-      initInsuroFloatingButton();
-    }
+    // task-2788+3 (U1): A-0 라이브 실행 빌드 — 레거시 "📊 InsuRo로 분석" 버튼을
+    // 부착하지 않는다. 이 버튼이 유일한 OHMY_MATRIX_CAPTURED 송신 진입점이며,
+    // 클릭 시 실고객 데이터가 실서버로 POST 된다. (initInsuroFloatingButton 의
+    // 유일한 호출부였다 → renderInsuroButton / handleInsuroCaptureClick 도달 경로 0.
+    //  스크레이핑 함수는 회귀 위험 때문에 존치.)
```

**설계 판단(팀장)**: 함수 정의를 삭제하지 않고 **호출 경로만 끊었다.** 근거 — 스크레이핑 함수군은 기존 테스트가 `module.exports` 로 의존하므로 삭제 시 회귀 위험이 크다. 지시서도 "관련 스크레이핑 함수 자체는 삭제하지 않아도 되나 도달 경로 0"을 요구한다.

### U2. 실서버 전송 경로 구조 제거 — `extension/background.js` (엔키)

1. **`pushMatrixToInsuro` 함수를 소스에서 통째로 삭제**. 이 함수가 `ohmy-capture` POST 를 담은 유일한 지점이었다.
2. `OHMY_MATRIX_CAPTURED` 핸들러를 **동기 no-op** 으로 교체:

```js
if (msg?.type === "OHMY_MATRIX_CAPTURED") {
  // task-2788+3 (U2): no-op. 서버 전송·탭 생성·카운터 증가 전부 수행하지 않는다.
  sendResponse({ ok: false, error: "A0_SPIKE_BUILD_SERVER_SEND_DISABLED" });
  return true;
}
```

**설계 판단(팀장)**: 지시서의 "분기(if)로 우회 가능하게 두지 말고 전송 코드 자체에 도달 못 하게" 요구를 **함수 삭제**로 이행했다. 플래그·조건문으로 막으면 조건 한 줄만 뒤집혀도 실고객 데이터가 나간다. 전송 코드가 파일에 **존재하지 않으면** 그 실패 모드 자체가 성립하지 않는다. 비동기 IIFE 도 제거해 **지연 전송 여지 0**.

### 추가 1건 (마르둑) — 구 주석 정정

`background.js:54` / `content.js:26` 의 구 주석이 `ohmy-capture` 경로를 **살아있는 것처럼** 기술하고 있었다("신 경로: … 명시 클릭만 활성"). 코드는 죽었는데 주석은 살아있다고 말하는 상태는 다음 작업자를 오도한다. 폐기 사실을 명시하도록 정정(주석 전용, 로직 무변경).

---

## 도달 경로 0 근거 (grep 은 보조 — 도달경로 기준)

### U1 (레거시 버튼)
- `initInsuroFloatingButton` 의 호출부는 삭제된 32-36행이 **유일**했다. `module.exports` 미노출.
- 현재 grep 결과 2건 = **함수 정의(146행) + 주석 언급(36행)**. 호출 형태(`initInsuroFloatingButton(` / `addEventListener(..., initInsuroFloatingButton)`) **0건**.
- 체인: `renderInsuroButton` 의 유일한 호출자는 `refreshInsuroButtonByJwt`, 그 유일한 호출자는 `initInsuroFloatingButton` → **전 체인 도달 0**.
- **독립 교차 증거**: TypeScript 진단이 `'initInsuroFloatingButton' is declared but its value is never read (6133)` 를 보고 — 도구가 도달 0을 독립적으로 확인.

### U2 (실서버 전송)
- `grep -n "fetch(" extension/background.js` → **0건**.
  - ★ 이 값이 처음엔 A-0 파손 의심 신호로 보였으나, 조사 결과 **A-0 는 `doFetch(...)`(주입 가능한 `fetchImpl`, 기본값 `globalThis.fetch`)로 호출**한다(373·382행). 즉 파일 내 유일한 리터럴 `fetch(` 가 삭제된 레거시 것이었다는 뜻으로, **모든 단서가 정합적으로 설명된다.**
- `ohmy-capture` 잔존 3건은 **전부 주석**(비주석 라인 0건 — 테스트로 자동 검증).
- `pushMatrixToInsuro` 함수 정의 **0건**(주석 내 언급 1건뿐).
- TypeScript 진단이 `getInsuroBase` / `getInsuroJwt` / `getInsuroFrontendBase` / `incrementCounter` 를 **never read** 로 보고 → 전송 경로가 실제로 끊겼다는 독립 교차 증거(존치는 회귀 방지 목적, 의도적).

---

## A-0 경로 유지 증거

1. **소스 무손상**: `DIRECT_FETCH_SPIKE_RUN` 핸들러, `validateSpikeRequest` / `isAllowedSpikeUrl`(T2 포트조건 포함) / `isAllowedSpikeSender` / `isWellFormedJwtShape` / `buildSpikeVerdict` / `runDirectFetchSpike`, content.js 의 `attachSpikeButton` / `createSpikeButton` / `runDirectFetchSpikeFromPage` / `token_source`(T1) / `url_entry_age_ms`(T3) — **diff hunk 에 일절 등장하지 않음**.
2. **행동 테스트**: 유효 `DIRECT_FETCH_SPIKE_RUN` 요청 → 게이트 통과 → fetch **1회 호출**(Bearer 헤더 확인) → verdict 수신(`transport_ok:true`, `company_count:1`).
3. **실브라우저**: A-0 버튼(`insuro-a0-spike-btn`, "🔎 A-0 직접조회 진단") 정상 부착 — 스크린샷 확인.

---

## 테스트 (닌기르수)

`extension/__tests__/direct_fetch_spike.test.ts` 에 **11건 추가**(기존 테스트 수정·삭제 0건).

- **O그룹(3건)** — 실제 등록된 background 리스너를 캡처해 `OHMY_MATRIX_CAPTURED` 호출: `fetch` **0회**, `sendResponse` = `{ok:false, error:"A0_SPIKE_BUILD_SERVER_SEND_DISABLED"}`, `chrome.tabs.create` **0회**, 마이크로태스크·타이머 flush 후에도 fetch 0회(**지연 전송 없음**).
- **P그룹(6건)** — 정적: `ohmy-capture` **비주석 라인 0건**(주석 판정 로직 명시), `pushMatrixToInsuro` 정의 0건, `initInsuroFloatingButton` 호출부 0건(정의는 존치 확인).
- **Q그룹(2건)** — A-0 회귀 방지: `DIRECT_FETCH_SPIKE_RUN` 정상 동작, `createSpikeButton` 이 `insuro-a0-spike-btn` 생성·`insuro-helper-btn` 미생성.

### ★ 뮤테이션 검증 (팀장 직접 수행)

닌기르수가 P·Q그룹 뮤테이션 미수행을 **정직하게 보고**하여, 팀장이 직접 결함을 주입해 실증했다. **테스트가 헛도는지 여부는 실패시켜 봐야만 알 수 있다.**

| 뮤테이션 | 결과 |
|---|---|
| **M1**: `ohmy-capture` POST 함수를 비주석 코드로 부활 | **2 FAIL** (P그룹: 비주석 라인 0건 assert + `pushMatrixToInsuro` 정의 0건 assert) → 524/526 |
| **M2**: A-0 버튼 id 를 `insuro-helper-btn` 으로 변경 | **2 FAIL** (신규 Q그룹 + 기존 B그룹) → 524/526 |

두 뮤테이션 모두 **원복 확인**(`git status --short` 빈 출력, `git diff` 빈 출력). 최종 소스는 커밋 상태와 완전 일치.

### 테스트 카운트
```
Test Files  35 passed (35)
      Tests  526 passed (526)
```
**515 → 526 (+11, 감소 0).** 팀장이 `npx vitest run` 을 **직접 독립 재실행**하여 확인.

---

## ★ L1 스모크테스트 결과

- **서버 재시작**: 해당없음 (Chrome Extension — 서버 컴포넌트 없음)
- **API 응답 확인**: 해당없음 (이번 변경의 목적이 **서버 호출 제거**이므로 호출할 API 가 없는 것이 정상. 대신 **네트워크 요청 0건**을 실브라우저에서 관측)
- **스크린샷**: `/home/jay/.cokacdir/workspace/0F7E275A/l1-a0-button.png`

**실행 방식**: Playwright + 실제 Chromium. `page.route` 로 `https://mmlfcp.ohmymanager.com/` 을 로컬 HTML 로 fulfill(외부 네트워크 접속 0) → `location.hostname` 이 실제 ohmymanager 가 되어 content.js 의 ohmy 분기가 **실제로 실행**됨 → 실제 `extension/content.js` 원본 주입.

| 명제 | 결과 | 근거 |
|---|---|---|
| 레거시 버튼 미존재 | **PASS** | `getElementById('insuro-helper-btn')` → `null` |
| A-0 버튼 정상 부착 | **PASS** | `insuro-a0-spike-btn` 존재, "🔎 A-0 직접조회 진단" |
| `ohmy-capture` 요청 0건 | **PASS** | 전체 요청 1건(문서 자체)뿐, ohmy-capture 0건 |
| `OHMY_MATRIX_CAPTURED` 송신 0건 | **PASS** | `sendMessage` 호출 기록 0건 |

**★ 교란요인 배제(핵심)**: `chrome.storage.local` 스텁이 **`exp` 가 미래인 유효 JWT** 를 반환하도록 구성했다(런타임에서 `{"exp":1784386140,"nowSec":1784382540,"isFuture":true}` 재확인). 즉 레거시 버튼 미표시는 **"JWT 가 없어서"가 아니라 "부착 코드 경로를 제거해서"** 임이 증명된다. 이 구분이 없으면 L1 은 아무것도 증명하지 못한다.

**스크린샷 육안 확인(팀장)**: 우하단에 **A-0 버튼(teal) 단 하나만** 렌더. 레거시 버튼은 동일 위치(`right:24px; bottom:24px`, indigo)에 뜨도록 되어 있었으므로, 그 자리에 없다는 것이 곧 미부착의 시각적 증거다.

**브라우저 정리**: `browser.close()` 호출 확인, 잔존 chromium 없음.

---

## MATCH / GAP

### MATCH
- U1 레거시 버튼 부착 제거 — 도달 경로 0 (grep + TS 진단 + 실브라우저 3중 확인)
- U2 실서버 POST 핸들러 no-op — **함수 삭제**로 우회 불가 구조화, 명시 에러코드 `A0_SPIKE_BUILD_SERVER_SEND_DISABLED`
- A-0 경로(`DIRECT_FETCH_SPIKE_RUN`) 동작 유지 — 소스 무손상 + 행동 테스트 + 실브라우저
- +1/+2 보안가드(S1~S5, T1~T3) 무력화 0
- 테스트 추가(fetch 미호출 / ohmy-capture 도달 0 / A-0 유지) + 뮤테이션 실증
- 기존 테스트 전부 유지 — 515 → 526, 감소 0
- 토큰 저장/캐시/로깅 0 · MAIN world 인터셉터 추가 0 · `inject.js`/`manifest.json` 미수정 · 신규 파일 0 · PR/merge/배포 0

### GAP
1. **게이트 지시 vs 지시서 충돌 — 지시서 우선 적용**: 프롬프트 G3 는 "`finish --action pr` 로 PR 생성 → 머지"를 지시하나, task 파일은 `merge_policy: "none"` · "**PR·merge·배포 금지**"를 **불변식**으로 명시한다. **지시서를 따라 PR/머지를 수행하지 않았다.** same-branch 커밋만 존재. 머지 판단은 아누/회장께 위임.
2. **미사용 함수 존치**: `getInsuroBase` / `getInsuroJwt` / `getInsuroFrontendBase` / `incrementCounter` / `initInsuroFloatingButton` / `renderInsuroButton` / `handleInsuroCaptureClick` 가 정의만 남아 TS 6133(never read) 경고 발생. **의도적 판단** — 지시서의 회귀 위험 회피 지침을 따랐고, 경고 자체가 도달 0의 교차 증거로 기능한다. 빌드 실패 아님(경고 레벨).
3. **A-0 버튼 클릭 후 로직까지는 L1 미실행**: 실브라우저 L1 은 버튼 부착·네트워크 0건까지 확인했고, 클릭 후 `runDirectFetchSpikeFromPage`(Resource Timing 기반 URL 복구)는 **실 ohmymanager 세션·실 JWT 가 있어야 의미 있는 관측**이 되므로 수행하지 않았다. 이는 **A-0 라이브 실행(회장 수행) 그 자체**이며 이번 범위가 아니다. 단위 테스트 레벨에서는 Q그룹으로 커버.
4. **범위 외 미착수(지시 준수)**: production 이식 · query key allowlist · 화면값/session owner 대조 · DOM 기반 URL 구성 · 세션전환 상태기계 — 손대지 않음.

**STOP_REPORT 조건 미해당**: 레거시 버튼 제거가 A-0 버튼 동작을 깨뜨리지 않았고(실브라우저 확인), manifest 수정 불필요, 기존 보안가드 완화 불필요.

---

## 머지 판단

- **머지 필요**: **No** — 지시서가 PR·merge·배포를 금지(`merge_policy: "none"`). 아누/회장 판단 대기.
- **브랜치**: `task/task-2788-dev5`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2788-dev5`
- **머지 의견**: 변경은 `extension/content.js`·`extension/background.js`·테스트 3파일로 국한되며 전부 **제거 방향**(기능 추가 0)이라 충돌 위험이 낮다. 526 PASS·뮤테이션 실증·L1 실브라우저 관측까지 완료해 품질은 머지 가능 수준이나, **지시서 불변식에 따라 머지하지 않았다.**

---

## 발견 이슈 및 해결

| 이슈 | 해결 |
|---|---|
| `grep "fetch(" background.js` 0건 → A-0 파손 의심 | 조사 결과 A-0 는 `doFetch`(주입형 `fetchImpl`) 사용. 모든 단서가 설명되는 가설 확인 후 정합 판정. **파손 아님** |
| 구 주석이 폐기된 `ohmy-capture` 경로를 살아있다고 기술 | 팀장이 주석 정정 커밋(`f2973fa`). 로직 무변경 |
| 닌기르수가 P·Q그룹 뮤테이션 미수행 보고 | 팀장이 M1·M2 직접 주입 → 각 2건 FAIL 실증 → 원복 확인 |
| TS 6133 never-read 경고 다수 발생 | 의도적 존치(회귀 방지). 경고 레벨이며 빌드 실패 아님. 도달 0의 교차 증거로 활용 |

---

## ★ 완료 처리 상태 — .done 미생성 (아누 판단 요망)

**작업 자체는 완료·검증되었으나 `finish-task.sh` 가 G4 게이트에서 차단되어 `.done` 이 생성되지 않았다.** 정직하게 기록한다.

### 통과한 게이트
QC Gate **WARN**(7 PASS / 15 SKIP / 1 WARN, FAIL 0) · git_evidence **PASS**(커밋 4건, uncommitted 0) · scope-guard **PASS**(3 files in scope) · GIT-GATE / MERGE-BASE / IMPACT-GATE / CI-PREFLIGHT(tsc·vitest exit=0) **PASS** · GOAL-GATE **PASS**(`npx vitest run`) · FINALIZE-ONLY **merge block 스킵**(merge_policy=none honored — PR/Gemini trigger 미실행, side-effect 0)

### 차단 지점
```
[G4-GATE] fix_loop_count=2 >= max=2 — OWNER_DECISION_REQUIRED
action: ESCALATED_OWNER_DECISION
marker: memory/events/task-2788+3.g4-fix-loop-cap.json
```

### 원인 분석 (코드 품질 결함 아님)
`scripts/gemini_cli_gate_check.py:499-502` 는 **G4 게이트가 호출될 때마다 무조건** `fix_loop_count` 를 +1 한다(게이트 통과 여부·실제 수정 여부와 무관). 즉 이 카운터는 "Gemini 지적에 따른 수정 반복 횟수"가 아니라 **"finish-task.sh 가 G4 까지 도달한 횟수"** 로 동작한다.

본 작업에서 재실행이 2회 발생한 원인은 **전부 인프라 설정 문제**였고 코드 수정은 단 한 번도 유발되지 않았다:

| 회차 | 실패 원인 | 성격 |
|---|---|---|
| 1회 | QC evidence root 가 `/home/jay/workspace` 로 지정 → git_evidence 가 **엉뚱한 repo** 를 검사(커밋 0건, unstaged 589건). 실제 커밋은 InsuRo 워크트리에 4건 존재 | 인프라(인자 누락) |
| 2회 | GOAL-GATE 가 `npx vitest run` 을 workspace cwd 에서 실행 → 프로젝트 부재로 30s 타임아웃(rc=124). 워크트리 cwd 에서는 rc=0 | 인프라(cwd) |

두 원인 모두 **측정으로 확정**했다(같은 명령을 두 cwd 에서 실행해 rc=124 vs rc=0 대조, git_evidence details 의 `resolved_via=env_var dir=/home/jay/workspace` 확인).

### 팀장 판단 — 카운터 임의 리셋 미수행
`task-2788+3.g4-fix-loop-count` 를 리셋하면 `.done` 이 생성될 것으로 보이나 **수행하지 않았다.** 이 게이트는 명시적으로 `OWNER_DECISION_REQUIRED` / "회장 결정 필요"를 선언한다. 오너 결정을 요구하는 hard cap 을 **실행 주체가 스스로 되돌려 자기 완료를 통과시키는 것은 자가승인**이며, 수동 `.done` 생성 금지 원칙과 같은 성격의 위반이다. 판단을 아누/회장께 넘긴다.

**요청 사항**: 위 원인이 인프라 아티팩트임을 인정하신다면 `memory/events/task-2788+3.g4-fix-loop-count` 리셋 후 `finish-task.sh` 재실행(워크트리 cwd + project_path 인자 필수)으로 `.done` 생성이 가능하다.

### ANU callback
- 등록 완료: cron id **`05488351`**, `collector_role=ANU`, `owner_key=c119085addb0f8b7`(**ANU key — self-key 아님**), envelope-only 9필드, **771 bytes**(≤3900), `--once`
- 런처: `dispatch.normal_fallback_callback_helper.launch_callback` → `status=ANU_OWNED_READY`
- `callback_schedule_created`: executor 자가발사 0 — OS-level pickup runner 가 closeout 소유(task-2730 contract)
- 봇 자가 회수검증 미수행(SELF_COLLECTOR_VERIFICATION_FORBIDDEN 준수)

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 엔키 | 백엔드 (U2 background.js) | sonnet | 로직 구현 |
| 이쉬타르 | 프론트엔드 (U1 content.js) | sonnet | 로직 구현 |
| 닌기르수 | 테스터 (테스트 11건 + L1) | sonnet | 테스트 설계·Playwright L1 |
| 마르둑 | 팀장 | opus | 설계·검증·뮤테이션·주석 정정 |

haiku 미사용 (보안 불변식이 걸린 작업으로 판단 정확도 우선).

---

## 수정 파일 목록

| 파일 | 변경 |
|---|---|
| `extension/content.js` | U1 레거시 버튼 부착 제거 + 구 주석 정정 |
| `extension/background.js` | U2 `pushMatrixToInsuro` 삭제 + 핸들러 no-op + 구 주석 정정 |
| `extension/__tests__/direct_fetch_spike.test.ts` | 검증 테스트 11건 추가 |
| `memory/reports/task-2788+3.md` | 본 보고서 |

신규 파일 **0건**(new_file_limit=0 준수). forbidden_paths 접근 **0건**.
※ L1 스크립트(`l1_smoke.mjs`)는 프로젝트 외부인 봇 작업 디렉토리에 생성 — 프로젝트 무접촉.
