# task-3021 — 유튜브 파이프라인 pull 안전화 (Sonnet 요약 이식 + lastCrawledAt 가드)

**레벨**: Lv.2 · **팀**: dev2-team (오딘) · **회장 승인 완료** (2026-08-26)
**저장소**: `Jeon-Jonghyuk/InsuWiki` (`/home/jay/projects/insuwiki`, 기본 브랜치 **master**)

## 목표
지금 `git pull` 을 하면 **4.5개월 백로그가 소실되고 요약도 전량 실패**한다.
이 태스크는 **pull 을 안전하게 만드는 것**이 목표다. pull·타이머 재가동은 하지 마라.

---

## ① Sonnet 요약 이식 (본수정)

### 배경 — 회장 결정은 이미 났다
**요약에 Gemini 를 쓰지 않는다**(2026-08-20 회장 확정). 전환 코드도 이미 작성됐다.
그런데 **머지되지 않아 master 는 아직 Gemini 2.5 Flash 를 쓴다.**
그 키는 라이브 프로브 **HTTP 400 (사망)** 이고 `config.py:18` 평문 커밋 기본값과 sha256 동일하다.
→ **요약 전량 실패 → wiki 카드 0건.** 이것이 카드 0건의 실제 원인이다.

### ★★ 브랜치를 통째로 머지하지 마라 (가장 중요)
전환 코드는 `task/task-2980-dev1` 에 있으나 **2026-07-28 분기라 한 달 낡았다.**
그 사이 master 의 `summarizer.py` 가 두 번 크게 바뀌었다:
```
t2989  임베딩을 Gemini → 아누 로컬 임베딩 서비스(768차원) 로 전환
t3001  요약 규격화 + wiki 카드 자동 생성
```
브랜치를 그대로 머지하면 **이 둘이 되돌아간다**(merge-tree 충돌 11곳).
★ 2026-08-25 PR #256 에서 ANU 가 낸 사고와 **동일 유형**이다(낡은 base 로 최신을 덮음).

**반드시 현행 `origin/master` 를 base 로 새 브랜치를 만들고, Sonnet 호출부만 재적용하라.**

### 이식 대상 (브랜치 → 현행 master)
```
가져올 것:  _call_claude_cli · _call_with_backoff · _build_length_retry_prompt
            _MAX_PROMPT_CHARS · _MAX_STDOUT_CHARS · _TRUNCATE_STDOUT_CHARS
            config.CLAUDE_CLI_PATH · config.CLAUDE_SUMMARY_MODEL
대체할 것:  master 의 _init_genai · _call_gemini_with_backoff
```

### ★ 손대지 말 것 (건드리면 회귀다)
```
generate_embedding · _embed_endpoint · _extract_vector · _fail   ← t2989 로컬 임베딩. 그대로 둔다
★ 브랜치의 generate_embedding 은 Gemini 를 쓴다. 절대 가져오지 마라
generate_summary 의 t3001 요약 규격(분량·형식)              ← 회장 확정. 호출부만 바꾼다
```
분량 보증 로직이 브랜치와 master 양쪽에 있다면 **master(t3001) 쪽을 정본으로 삼아라.**
판단이 서지 않으면 진행하지 말고 ANU 에 물어라.

### 마무리
- `GEMINI_API_KEY` 참조를 요약 경로에서 **0건**으로 만든다
- `config.py:18` **평문 커밋 기본값을 제거**한다(하드코딩 기본값 금지, `os.getenv` 만)
- 이 키는 이미 사망했으나 평문 커밋 자체가 위생 문제다. **값을 로그·보고서에 쓰지 마라**

---

## ② lastCrawledAt 전진 가드

```
main.py:403   if new_videos and doc_id:
                  # lastCrawledAt 갱신
```
**성공 여부를 보지 않는다.** 요약·카드 생성이 실패해도 커서가 전진한다.
지금 두 채널 모두 `2026-04-11` 에 멈춰 있고, 이 상태로 pull 하면 첫 실행에서
**커서가 현재로 점프해 4.5개월 백로그가 영구 소실**된다.

- `task/task-2980-dev1` 에 `channel_deferred` 가드가 있으나 **master 에는 없다**(심볼 0건)
- **처리 실패 건이 하나라도 있으면 커서를 전진시키지 않는다**(fail-closed)
- 어떤 조건에서 전진/보류하는지 **로그로 남겨라**

---

## ★ 하지 말 것
- **`git pull` 금지** — ANU 가 머지 후 직접 한다
- **타이머 재가동 금지** — `insuwiki-youtube-pipeline.timer` = inactive 유지
- **`task/task-2980-dev1` 브랜치 통째 머지 금지**
- fail-closed 판정 완화 (전사 없으면 카드 생성 안 함 — 옳은 동작이다)
- `firestore_writer.py` · `drive_uploader.py` 로직 변경

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "scripts/youtube-pipeline/youtube_pipeline/summarizer.py"
    - "scripts/youtube-pipeline/youtube_pipeline/config.py"
    - "scripts/youtube-pipeline/youtube_pipeline/main.py"
    - "scripts/youtube-pipeline/tests/**"
    - "memory/reports/task-3021.md"
  forbidden_paths:
    - "scripts/youtube-pipeline/youtube_pipeline/firestore_writer.py"
    - "scripts/youtube-pipeline/youtube_pipeline/drive_uploader.py"
    - "scripts/youtube-pipeline/youtube_pipeline/transcriber.py"
    - ".github/workflows/**"
    - "functions/**"
    - "nextapp/**"
  commands: ["bash", "python3", "pytest", "gh"]
  merge_policy: "tiered"
  ttl_hours: 24
```

## 검증 (실측 강제)
1. **Sonnet 요약 실호출 1건** — 실제 전사 텍스트로 요약을 생성해 **글자수와 형식**을 제시하라.
   "테스트 통과"가 아니라 **생성된 요약 본문의 실측치**를 보고하라
2. **Gemini 잔존 0건** — 요약 경로에서 `GEMINI_API_KEY` 참조 0건임을 grep 으로 증명
3. **임베딩 무회귀** — `generate_embedding` 이 여전히 로컬 서비스(768차원)를 쓰는지 확인.
   변경 전후 **함수 본문 diff 0줄**이어야 한다
4. **커서 가드** — 실패 건이 섞인 상황에서 `lastCrawledAt` 이 **전진하지 않는지** 실측.
   성공만 있는 경우엔 **전진하는지**도 확인(양방향)
5. **봉인** — 가드를 제거하는 변이와 Gemini 복귀 변이를 각각 만들어 테스트가 **FAIL 하는지**
   확인하고 복원하라. 변이가 no-op 이 아님을 `assert` 로 먼저 증명하라
6. `pytest` 회귀 유지 (**base 재측정 기준선**. 명세 숫자를 믿지 말고 base 에서 직접 재라)

## 완료 조건 (DoD)
**현행 master 기준 브랜치에서 요약이 Sonnet CLI 로 실제 생성되고, 임베딩·요약규격에 회귀가 없으며,
처리 실패 시 `lastCrawledAt` 이 전진하지 않음이 실측으로 증명된다.**

## ★ 세션 조기 종료 대책
- **시작 즉시 작업 브랜치 + 드래프트 PR 을 먼저 열어라.** 마지막 긴 응답에 보고를 몰지 마라
- 단계마다 진행 기록을 PR 본문 또는 보고서에 갱신하라
- ★ **ANU 명세 전제가 틀렸다고 판단되면 구현을 진행하지 말고 반증을 먼저 기록한 뒤 보고하라.**
  (t3018 에서 봇이 ANU 의 "큐 적체" 전제를 반증한 것이 옳았다)

## 운영 계약
- `origin/master`(=`290c003`) 기준으로 시작. **브랜치 base 를 반드시 확인하라**
- ★ `gh` 호출 시 `GH_TOKEN="$BOT_GITHUB_TOKEN"` 주입 필수 (회장 개인 PAT 금지 — 감사기록 오염)
- 워크플로우 `/home/jay/workspace/prompts/DIRECT-WORKFLOW.md` · QC `/home/jay/workspace/teams/shared/QC-RULES.md`
- `WORKSPACE_ROOT=/home/jay/workspace` · `CHAT_ID=6937032012` · 수집자 key `ANU_KEY=c119085addb0f8b7`
- 완료 경로는 `finish-task.sh` 실행이 유일하다. 수동 `.done` 금지.
- ★ PR Check `validate` 는 이 저장소에서 **상시 적색**이다(`scripts/*.ts` tsc, firebase-admin 미설치).
  변경 파일과 교집합이 없으면 선재로 판정하고 그 근거를 보고서에 적어라

## 보고
**PR 생성까지가 범위다. 머지는 ANU 가 한다. 자동 머지 금지.**
`memory/reports/task-3021.md` 작성 후 표준 완료 콜백 등록. 콜백 프롬프트 **UTF-8 3900 bytes 이하**.
★ 봉투 첫 줄에 **"Sonnet 요약 실호출 성공 여부 + 임베딩 회귀 0 여부"** 를 담아라.