# 라운드 6 종료 — 이월 4항목이 모두 닫혔다

직전 QA(`devlog/_fin/260726_agbrowse_qa` §7)가 남긴 이월 4항목을 닫는 것이 이
라운드의 범위였다. 닫는 과정에서 새 결함 3건이 나와 함께 처리했다.

## 1. 결과

| WP | 항목 | 출처 | 처분 | 커밋 |
|----|------|------|------|------|
| WP1 | Chromium 리비전 불일치 | 이월 1 | 해결 | `e35e8d9` |
| WP2 | `context-dry-run --file` 크래시 (Q13 잔여) | 이월 2 | 해결 | `3fecaf0`+`cb5bc78` |
| WP2b | 예측자 사본 → 정본 호출 | WP2에서 파생 | 해결 | `0d2a683` |
| WP3 | Camoufox 레인 동작 확인 (Q6) | 이월 3 | 검증 완료 | `0c876a5` |
| WP3b | CI 설치 안티패턴 + README stealth 상충 | WP1/WP3에서 파생 | 해결 | `cdd484a` |
| WP5 | 브라우저 레인 실패가 확보한 본문을 버림 | **신규** | 해결 | `7dc6d4c` |
| WP6 | 레인 실패와 자체 버그 구분 | WP5 A-gate | 해결 | `29ac072` |
| WP7 | 감시하는 척하던 CI 잡 | WP3b A-gate | 해결 | `ebf5b16` |
| WP8 | Q6 수정에 회귀 테스트 없음 | 라운드 7 후보 → 당김 | 해결 | `0725823` |
| WP9 | 후보 발견 단계가 도달 불가 | WP8 A-gate | 해결 | `c71507b` |
| WP10 | 발견 단계의 경계 + 타입 방어 | WP9 A-gate | 해결 | `b81553e` |
| WP11 | camoufox 레인 오류 처리 + abort 배선 | 120 §5 후속 1 | 해결 | `cd934d0` |
| WP12 | 입력 오류가 `internal.unhandled`로 오분류 + 수치 옵션 상한 | 130 §5 후속 | 해결 | `84d8816` |
| WP13 | 요약이 결과 아닌 레인을 보고 + 사람 경로에 errorCode 부재 | 120·140 §5 후속 | 해결 | `d0f04cb` |

이월 4번("다른 결함 모양 — 동시성·자원 고갈·깨진 프로바이더 DOM")은 이번에도
건드리지 않았다. 범위를 넓히는 대신 1~3번을 실제로 닫는 데 썼다.

### 1.1 라운드가 한 번 더 이어졌다

§4의 이월 목록 4번(Q6 회귀 테스트 부재)을 다음 라운드로 미루는 대신 당겨서 WP8로
처리했고, 그 A-gate에서 나온 것이 WP9, 또 그 A-gate에서 나온 것이 WP10, WP10이
남긴 후속에서 WP11이, 다시 그 후속에서 WP12와 WP13이 열렸다. 여섯 work-phase가 연쇄로 이어졌고 **각각 실제 결함을
잡았다.**

| WP | 잡은 것 | 성격 |
|----|---------|------|
| WP8 | `strong_ok` 가드가 죽어 있어 camoufox가 매 fetch마다 실행 | 설치 환경에서 **6.1초/fetch** |
| WP9 | Phase 1d가 도달 불가 + 그 안의 `ranked.slice` `TypeError` | 기능 하나가 통째로 죽어 있었다 |
| WP10 | 발견 attempt가 `weak_ok`로 기록 — 미채점 URL을 통과처럼 표기 | 트레이스가 거짓을 말함 |
| WP11 | camoufox 레인만 `.catch(() => null)` + `signal` 미배선 | 우리 버그가 흔적 없이 사라짐 |
| WP12 | 입력 오류 전체가 `internal.unhandled`, `--timeout-ms` 상한 없음 | 오타에 "버그 신고하세요" |
| WP13 | 요약이 마지막 attempt를 결과로 보고 | 성공한 fetch가 `browser_required`로 요약됨 |

**회귀 테스트를 쓰려다 프로덕션 결함을 여섯 잡았다.** WP8은 "테스트가 없다"는 항목이었지
"결함이 있다"는 항목이 아니었다.

### 1.2 사용자에게 보이는 수정과 문서 전용을 구분한다

**동작이 바뀐 것:**

- WP1 — 오버라이드 없이 통합 스위트가 돈다. 침묵하던 트랜스포트 테스트 15개가
  실제로 실행된다.
- WP2 — `web-ai context-dry-run --prompt hi --file <path>`가 죽지 않는다.
- WP5 — Chrome이 없어도 `agbrowse fetch <url>`이 읽은 것을 돌려주고 exit 0으로
  끝난다. 기본 사용법에서 밟던 크래시다.
- WP6 — 레인 실패는 기록하고 계속 가되, 우리 코드의 결함은 삼키지 않는다.
- WP8 — camoufox가 필요 없을 때 스폰하지 않는다.
- WP9 — 후보 발견이 실제로 동작한다.
- WP10 — 트레이스가 미채점 URL을 통과처럼 적지 않는다.
- WP11 — camoufox 레인이 우리 버그를 숨기지 않고, 데드라인이 터지면 스폰하지 않는다.
- WP12 — 오타와 거부가 각각 `input.*` / `safety.*`로 보고되고, 큰 `--timeout-ms`가 크래시하지 않는다.
- WP13 — trace 요약이 반환된 레인을 말하고, `--json` 없이도 오류의 성격이 보인다.

**문서·인프라 정리:** WP2b(중복 제거), WP3(검증 기록), WP3b(CI + README),
WP7(죽은 잡).

## 2. 이번 라운드가 가르쳐 준 것

### 2.1 미검증을 방어하는 근거가 두 번 연속 틀렸다

WP3의 040 문서 §2에 표로 남겼다. "격리할 방법이 없다"와 "브라우저 빌드를 받을 수
없다" 둘 다 실측 앞에서 무너졌다. 두 번째 근거는 리뷰어가 제시한 추정을 내가 확인
없이 인용해 결론 근거로 승격시킨 것이다.

**규칙으로 적으면** — 미검증을 정당화하는 근거도 검증 대상이다. "할 수 없다"는
주장은 "해 봤다"만큼의 증거를 요구한다.

### 2.2 뮤테이션과 리그레션은 다른 질문에 답한다

WP6에서 뮤테이션 6종이 전부 RED였는데도 리그레션 하나가 게이트를 통과했다. 원격
서버의 깨진 `Location` 헤더가 fetch 전체를 크래시시키는 결함이었고, 리뷰어가
HEAD와 비교해 찾았다.

- 뮤테이션: **내가 쓴 코드가 지켜지는가**
- 리그레션: **이전에 되던 것이 아직 되는가**

같은 구멍이 한 라운드에 세 번 나왔다. M5/M6(오류 타입 한 종류만 테스트),
M9(공백 검사가 막던 루프를 테스트 안 함), 그리고 위 리그레션. 전부 "고친 것"은
테스트했는데 "그 고침이 부수적으로 지키던 것"은 테스트하지 않은 모양이다. 셋 다
이번에 닫았다 — M9는 리뷰어가 "선택적 개선"으로 남긴 것을 같은 사이클에서 채웠고,
뮤테이션이 이제 RED다.

### 2.3 리뷰어의 제안도 검증 대상이다

WP6에서 리뷰어가 "TypeError는 rethrow"를 제안했다. 실측해 보니 Node의 `fetch`가
ENOTFOUND/ECONNREFUSED를 `TypeError`로 던져서, 그대로 넣었으면 죽은 호스트명 하나에
크래시하는 새 결함이 됐을 것이다. 판정 기준을 `타입 ∧ cause === undefined`로 바꿔
피했다.

반대 방향도 한 번 있었다. WP3 A-gate에서 리뷰어가 올린 블로커가 리뷰어 자신의
재현 스니펫에 있던 `browserSession:'none'` 때문이었고, 실행으로 반증했다. 리뷰어가
받아들이고 판정을 GO로 바꿨다.

**양쪽 모두 실행이 갈랐다.** 누가 말했는지가 아니라 무엇이 재현되는지가 기준이다.

### 2.4 결함을 볼 때 저장소 안에 이미 옳게 하는 곳을 찾는다

직전 라운드의 규칙이 이번에도 그대로 통했다.

| 결함 | 이미 옳던 곳 |
|------|--------------|
| WP2b 예측자 사본 | 같은 저장소의 정본 `hasContextPackaging` |
| WP5 브라우저 레인 rethrow | 형제 catch 5곳 (`:124` `:217` `:265` `:404` `:440`) |
| WP3b README stealth | 이미 실려 있는 203.1 TLS impersonation |
| WP8 죽은 `strong_ok` 가드 | 바로 아래 `:427` `:454`가 채점 후 `best.verdict`를 봄 |
| WP9 Phase 1d 언랩 | 같은 파일 `resultFromReaderCandidate`(`:641`)의 `scored.candidate` |

WP5는 특히 선명하다. 여섯 곳 중 다섯이 같은 형태였고 하나만 달랐다. 설계 판단을
새로 할 필요가 없었다. **다섯 번 연속으로 정답이 같은 파일 안에 있었다.**

### 2.5 도달 불가 코드는 조용히 썩는다

WP9가 Phase 1d를 살리자 그 안에서 `ranked.slice is not a function`이 즉시 터졌다.
`rankDiscoveredCandidates`는 배열이 아니라 객체를 반환하는데, **아무도 그 줄을 실행해
본 적이 없어서** 아무도 몰랐다. 유닛 테스트는 두 함수를 직접 import해 통과하고 있었다.

**"안 돌려본 표면은 미검증"이 함수 단위가 아니라 파이프라인 단위로도 필요하다.**
함수가 초록인 것과 그 함수가 제품에서 불리는 것은 다른 명제다.

### 2.6 같은 실수가 부호만 바꿔 두 번

`chooseBestReaderCandidate`는 채점 래퍼를 돌려준다. 본문은 `.candidate.text`에,
`verdict`는 래퍼에 있다. 두 결함이 이 구분을 서로 반대로 놓쳤다.

| 위치 | 잘못 읽은 것 | 결과 |
|------|--------------|------|
| camoufox 가드 (WP8) | raw 후보에서 `verdict` | 항상 `undefined` → 조건 **항상 참** → 매번 실행 |
| Phase 1d (WP9) | 래퍼에서 `text` | 항상 `undefined` → 조건 **항상 거짓** → 전혀 실행 안 됨 |

둘 다 `tsc`를 그냥 통과했다. WP10이 `ScoredReaderCandidate` typedef를 넣어 뒤쪽(Phase 1d)
방향을 막았다 — 다만 그 방어는 아직 게이트에 걸려 있지 않다(120 §2.1).

## 3. 게이트

오버라이드 **없이** 전부 통과한다. WP1 이후 `AGBROWSE_CHROMIUM_EXECUTABLE_PATH`가
필요 없다.

```
npm run test:unit          → 156 files / 1683 tests
npm run test:integration   → 22 files / 217 tests
npm run test:e2e           → 1 file / 1 test
npm run typecheck          → 0
npm run docs:drift         → 164
npm run docs:counts        → 76
```

통합 스위트가 193 → 235, 유닛이 1683 → 1693으로 늘었다. 라운드 중 추가한 회귀가
52건이다. 위 블록은 WP10 시점 값이고 WP13 이후 최종 수치는 **integration 235 /
unit 1693**이다.

`npm run typecheck:checkjs`는 109건 실패하는데 이 라운드 이전부터의 baseline이다
(리뷰어가 stash 대조로 확인, 120 §2.1).

## 4. 라운드 7로 넘기는 것

### 4.1 사용자 판단이 필요한 것

1. **`~/Library/Caches/camoufox` 656MB.** WP3 검증이 남겼다. 남기면 다음 라운드에서
   재설치 없이 검증할 수 있고, 지우면 디스크가 돌아온다. 임의로 지우지 않았다
   (040 §3.1).
2. **camoufox를 선택적 의존성으로 선언할지.** 레인이 동작함은 확인됐고 README
   표기도 정리했지만, `package.json`에 선언할지는 제품 결정이다(040 §6).
3. **`AGBROWSE_ALERT_WEBHOOK` 시크릿.** 저장소 설정에 남아 있다면 이제 아무도
   쓰지 않는다. 시크릿 삭제는 QA 권한 밖이다(080 §3).

### 4.2 다음 라운드 작업 후보

4. ~~Q6 수정에 회귀 테스트가 없다~~ → WP8에서 닫았다. "설계 변경이 필요하다"던
   진단 자체가 틀렸다 — 주입 선례가 같은 파일에 이미 있었다(100 §2).
5. **레인 내부 catch도 프로그래밍 오류를 삼킨다.** WP6은 스케줄러 6곳을 고쳤고,
   `tls-fetch.mjs`·`metadata.mjs`·`browser-session.mjs`의 레인 내부 catch는
   그대로다. `tls-fetch.mjs:118`의 `new URL(location)`이 안전한 것은 그 catch에
   **의존한** 안전이다(070 §5.1). camoufox 레인도 여기 해당한다(120 §5).
6. **이월 4번이 그대로 남았다.** 동시성, 자원 고갈, 깨진 프로바이더 DOM. 이번
   방법(플래그 변형·경계 탐색·실경로 실행)이 잘 드러내는 모양은 다 훑었다.
7. **WP8~WP10이 남긴 여섯 항목** — camoufox `signal` 미전달과 `.catch` 삼킴,
   `RawReaderCandidate` typedef, `adaptive-fetch/` checkJs 편입,
   `summarizeAttempts`의 `verdict=discovered` 노출, M-I/M-L, Phase 1d → search
   배선. 전부 120 §5에 근거와 함께 있다. 그중 첫 항목(camoufox `signal` + `.catch`)은
   **WP11에서 닫았다**(130 문서). 나머지 다섯은 그대로다. WP11이 새로 남긴 것은
   N3 뮤턴트 미고정과 `fetcher.mjs:47`의 `AbortSignal.timeout` RangeError 두 건이다.
   → 그 RangeError는 **WP12에서 닫았다**(140 문서). 쫓다가 입력 오류 전체가
   `internal.unhandled`로 오분류되던 더 넓은 결함이 나왔다.

## 5. 커밋

| 커밋 | 내용 |
|------|------|
| `e35e8d9` | WP1 — `playwright-core` 1.59.1 고정 |
| `3fecaf0` | WP2 — 컨텍스트 가드가 `--file`을 통과시키던 것 |
| `cb5bc78` | WP2 — 약한 증거 등급을 실패로 취급하던 것 |
| `0d2a683` | WP2b — 예측자 사본 제거 |
| `0c876a5` | WP3 — camoufox 검증 기록 |
| `7dc6d4c` | WP5 — 브라우저 레인 실패가 본문을 버리던 것 |
| `cdd484a` | WP3b — CI 설치 제거 + README 용어 정리 |
| `29ac072` | WP6 — 레인 실패와 자체 버그 구분 |
| `ebf5b16` | WP7 — 죽은 `live-drift` 잡 제거 |
| `0725823` | WP8 — camoufox 회귀 핀 + 죽은 `strong_ok` 가드 |
| `c71507b` | WP9 — Phase 1d 도달 가능화 + `ranked.slice` |
| `b81553e` | WP10 — 발견 경계 확정 + 타입 방어 |
| `cd934d0` | WP11 — camoufox 레인 오류 처리 + abort 예산 |
| `84d8816` | WP12 — 입력 오류 분류 + 수치 옵션 상한 |
| `d0f04cb` | WP13 — 요약 선택 기준 + 사람 경로 errorCode |
