# task-2980 Phase 2 — 인슈위키 유튜브 Sonnet 요약 + Drive 업로드 + 공개 카드

- **작업 ID**: task-2980 (Phase 2) · timer id `task-2980.2`
- **팀**: dev1-team (헤르메스)
- **작성**: 2026-08-20
- **worktree**: `/home/jay/.worktrees/task-2980-dev1` · 브랜치 `task/task-2980-dev1` · HEAD `1b8d0eb`
- **판정**: 기능 완료 · **머지 HOLD**(ANU 독립검증 대기) · **보안 에스컬레이션 1건 회장 결정 필요**

---

## S — 상황

Phase 1 에서 Whisper 전사는 실증 성공(18,308자)했으나 **Gemini API 키 폐기**로 요약이 0건(`summarized 0`, HTTP 400 `API_KEY_INVALID`)이었고, 커밋된 `.pyc` 안의 평문 시크릿까지 겹쳐 머지가 HOLD 된 상태였다. 회장 결정(2026-08-20)으로 요약을 외부 Gemini 대신 **아누 시스템 내부 Claude Sonnet CLI** 로 전환하고, 전사·요약 파일을 Drive 에 올린 뒤 **인슈위키 공개 카드**로 등록하는 것이 Phase 2 의 범위다.

## C — 갈등

1. Gemini 키는 재발급 없이는 살아나지 않고, `nextapp/.env.local` 에도 같은 무효값이 들어 있어 경로를 바꿔도 실패는 동일하다.
2. 요약뿐 아니라 **임베딩**(`text-embedding-004`)도 같은 Gemini 키에 묶여 있다. 기존 `main.py` 는 임베딩 호출이 try/except 밖이라 임베딩이 죽으면 **영상 전체가 failed** 로 떨어져 Drive 업로드·카드 등록까지 통째로 유실된다.
3. 공개 카드의 `attachments[]` 는 `fileId` 를 요구하는데 기존 `upload_markdown()` 은 webViewLink(str)만 반환한다.
4. Phase 1 잔여 블로커(`.pyc` 평문 시크릿)를 해소하지 않으면 기능이 통과해도 머지가 계속 막힌다.

## Q — 질문

Gemini 의존을 요약 경로에서 제거하면서, 임베딩 실패가 카드 등록을 막지 않고, 재실행에도 중복이 0인 공개 카드 등록을 실제 영상으로 실증할 수 있는가?

## A — 답변

**할 수 있고, 실제 영상 1건으로 전 구간 실측 통과했다.**

영상 `JRp2zoZAZ9E`(인스킹 ins-king, 46분58초) 기준 E2E:

- Whisper 전사 **18,288자** (`whisper_stt`)
- **Claude Sonnet 요약** 성공 → `summaryStatus: done`
- 임베딩은 **예상대로 실패**(Gemini 400)했으나 파이프라인은 **중단 없이 계속 진행** ← 설계 의도대로 동작
- Drive 업로드 2건(`_요약`, `_전사`) webViewLink 확보
- `documents/youtube_JRp2zoZAZ9E` 공개 카드 등록 (`visibility='public'`, content 5,844자, attachments 1건)
- 파이프라인 최종 집계: `처리: 1, 스킵: 39, 실패: 0, 임베딩실패: 1, 카드등록실패: 0`

---

## 3 Step Why

- **1st Why — 왜 이 설계가 필요한가?** → **A**: Gemini 키 폐기로 요약이 전면 중단됐고, 키 수명에 파이프라인이 묶여 있는 한 같은 사고가 재발한다.
- **2nd Why — 왜 A(내부 Claude CLI 전환)가 최선인가?** → **B**: 내부 CLI 는 API 키가 불필요해 외부 의존을 **영구 제거**한다. 게다가 `knowledge_extractor_v2._call_claude` 라는 검증된 사내 선례가 이미 있어 신규 위험이 낮다. (Codex 사전 리뷰에 "이 설계의 대안은 무엇이며 왜 이 접근이 최선인가"를 포함해 검증 — 재실행 `pass: true`)
- **3rd Why — 왜 B가 대안보다 나은가?** → **C**: 대안인 "Gemini 키 재발급"은 블로커를 뒤로 미룰 뿐이고, "다른 외부 LLM API 도입"은 신규 키·과금·심사를 추가하면서 같은 취약점 구조를 유지한다. B만이 실패 원인 자체를 제거한다.

A-B-C 일관성 확인됨.

---

## 수정 파일별 검증 상태

| 파일 | 변경 요지 | 검증 방법 | 상태 |
|---|---|---|---|
| /home/jay/.worktrees/task-2980-dev1/scripts/youtube-pipeline/youtube_pipeline/summarizer.py | generate_summary 를 Gemini → 로컬 claude CLI sonnet 으로 교체, 방어로직 이식 | 실제 CLI 1회 호출(973자·16.7초) + grep 으로 genai 잔존 0 확인 | PASS |
| /home/jay/.worktrees/task-2980-dev1/scripts/youtube-pipeline/youtube_pipeline/drive_uploader.py | UploadResult·upload_markdown_with_id 신설(fileId·file_name), 라벨 Claude Sonnet, 쿼리 이스케이프 | 단위테스트 + E2E 실업로드 2건 + 재업로드 동일 fileId | PASS |
| /home/jay/.worktrees/task-2980-dev1/scripts/youtube-pipeline/youtube_pipeline/firestore_writer.py | save_public_document 신규(문서ID youtube_&lt;videoId&gt; 멱등) | Firestore 실조회 + 재호출 delta 0 | PASS |
| /home/jay/.worktrees/task-2980-dev1/scripts/youtube-pipeline/youtube_pipeline/main.py | 임베딩 격리, 카드 등록 배선, documentId 기록, 첨부 uploadedAt 타입 | E2E 실행 로그 + Firestore 실조회 | PASS |
| /home/jay/.worktrees/task-2980-dev1/scripts/youtube-pipeline/youtube_pipeline/config.py | CLAUDE_* / DOCUMENT_AUTHOR_* 설정 추가 | grep 반영 확인 + E2E 기동 | PASS |
| /home/jay/.worktrees/task-2980-dev1/scripts/youtube-pipeline/tests/test_summarizer.py | Gemini 목킹 → Claude CLI 기반으로 갱신 | pytest 83 passed | PASS |
| /home/jay/.worktrees/task-2980-dev1/scripts/youtube-pipeline/tests/test_drive_uploader.py | 이스케이프·파일명 정합 테스트 추가 | pytest 83 passed | PASS |
| /home/jay/.worktrees/task-2980-dev1/scripts/youtube-pipeline/tests/test_main_degraded_guard.py | 신규 배선(upload_markdown_with_id·save_public_document) 반영 | pytest 83 passed | PASS |
| /home/jay/.worktrees/task-2980-dev1/.gitignore | pyc/pycache/pytest_cache 무시 규칙 +6줄 | git ls-files pyc 카운트 0 | PASS |

---

## L1 스모크테스트

- **서버 재시작**: 해당없음 — 본 작업은 상시 데몬이 아니라 크론 구동 배치 파이프라인이다. 대신 **파이프라인 프로세스를 실제로 기동**해 전 구간을 돌렸다(`python3 -m youtube_pipeline.main`, MAX_VIDEOS_PER_RUN=1). 의존 서비스인 Whisper GPU 는 재시작 없이 가동 상태 확인(`/v1/health` → `device: cuda`, `nvidia-smi` 로 교차확인: GTX 1060 6GB, driver 580.173.02).
- **API 응답 확인**:
  - Whisper 헬스 — `curl -s http://localhost:8200/v1/health` → `{"status":"ok","device":"cuda","gpu_memory_used_mb":91,...}`
  - Gemini 실호출(블로커 실증) — HTTP 400 `reason: "API_KEY_INVALID"` (키 sha256[:12]=`c50d0068dcff`)
  - Firestore 실조회 — `documents/youtube_JRp2zoZAZ9E` → `EXISTS: True`, `visibility: 'public'`, `content len: 5844`, `attachments count: 1`, `uploadedAt TYPE: DatetimeWithNanoseconds`
  - Drive 실조회 — fileId `1IE6Lz_...` 의 실제 파일명이 카드 첨부 name 과 일치(`MATCH: True`)
  - 요약 엔진 실호출 — Claude Sonnet 이 973자 한국어 보험 요약 반환, `has_numeric_warnings=True`
- **스크린샷**: 해당없음 (UI 변경 0건 — 백엔드 파이프라인 전용. 카드는 기존 `documents` 렌더러가 소비)

E2E 파이프라인 최종 로그:
```
처리 완료: 2주간 한시특판 무조건!! 한손 간병인보험 20만원 (요양병원 365일 국내유일~) (whisper_stt, 18288자)
파이프라인 완료 — 채널: 2, 영상: 40 (처리: 1, 스킵: 39, 실패: 0, 임베딩실패: 1, 카드등록실패: 0)
```

---

## 멱등성 실측

동일 videoId 로 재호출한 결과:

```
BEFORE youtube_* docs: 1 | createdAt: 2026-08-19 18:11:31.181000+00:00
returned doc_id: youtube_JRp2zoZAZ9E
AFTER  youtube_* docs: 1 | createdAt: 2026-08-19 18:11:31.181000+00:00
DELTA: 0 | createdAt preserved: True
Drive re-upload file_id: 1IE6Lz_Q7QYUP1R7bn9BgL_j54KSdEvDI | same as attachment: True
```

카드 중복 0 · `createdAt` 보존 · Drive 중복 업로드 0(동일 fileId 반환).

---

## 게이트 결과

- **Codex G1 설계 게이트**: 1차 `pass: false`(task 명세만 읽어 구현에 이미 있는 설계를 미비로 판정) → 실제 구현 파일을 `--affected-files` 로 넘겨 재실행 → **`pass: true`**
- **회귀**: `pytest tests/ -q` → **83 passed, 0 failed** (Phase 1 기준선 71 → 신규 테스트 12건 추가)
- **qc_verify**: test_runner PASS · l1_smoketest_check PASS · planned_check/data_integrity/critical_gap/spec_compliance/duplicate_check PASS
- **sanitize 게이트**: Codex 호출 시 PII 마스킹 1건 적용 확인(`PII 마스킹 완료: 1건 감지`)

### Codex 지적 처리 내역

| 지적 | 심각도 | 판정 | 처리 |
|---|---|---|---|
| Drive 첨부 파일이 공개 권한 미설정 | critical | 부분수용 | 기존 첨부 문서 6건도 동일 모델(링크는 Drive 계정 경유) — 본 작업이 만든 결함 아님. 플랫폼 차원 결정 필요로 회장 보고 |
| documents 중복 방지 키 없음 | high | 오탐 | 문서ID youtube_&lt;videoId&gt; 로 이미 구현. delta 0 실측 |
| 단계별 재진입 규칙 없음 | high | 오탐 | state.py 상태머신 존재(discovered→transcribed→summarized) |
| 임베딩 예외가 카드 등록 차단 | high | 오탐 | try/except 격리 구현. E2E 에서 임베딩 실패에도 카드 등록 성공 실증 |
| get_active_channels doc_id 불일치 | high | 오탐 | main.py 는 youtube_api 판을 쓰고 그쪽이 _doc_id 를 채움. 일치 |
| Drive 쿼리 작은따옴표 미이스케이프 | medium | 수용 | 수정. 대기 큐에 따옴표 포함 제목 2건 실재 확인 |
| 첨부 uploadedAt 문자열 타입 | medium | 수용 | datetime 객체로 수정 + 기존 카드 1건 backfill |
| 카드 content 에 메타헤더 포함 | medium | 의도적 선택 | 자체 완결형 마크다운이 카드 본문으로 적절. 검색 품질 영향은 후속 관찰 |
| 모델 라벨 추적성 약함 | low | 미조치 | bounded fix budget 초과. 후속 |

---

## trip-wire 5종 (실측)

| trip-wire | 실측값 | 근거 |
|---|---|---|
| Critical7 | 0 | Codex 게이트 재실행 pass=true, critical 잔여 0(권한 이슈는 플랫폼 기존 동작으로 재분류) |
| PII net-new | 0 | fd88601..HEAD 추가행 대상 AIza·GOCSPX·1//·ya29·sk-·PEM/주민번호/연락처 패턴 스캔 0건 |
| 회귀 실패 | 0 | pytest 83 passed, 0 failed |
| forbidden_paths 침범 | 0 | 변경은 insuwiki worktree 내부 + workspace memory 산출물뿐. 타 팀 디렉토리 미접촉 |
| nonce | task-2980 | 발사 task_id 와 일치 |

---

## ★ 보안 에스컬레이션 (회장·ANU 결정 필요)

아르고스 감사 결과, 커밋된 `.pyc` 에서 시크릿 4종이 평문으로 발견됐다. **`.py` 소스 grep 은 0건**이었고 컴파일 산출물에만 남아 있었다.

- untrack 완료: 추적 `.pyc` **36 → 0**, `.gitignore` 규칙 추가 (커밋 `bbfdc41`)
- **그러나 untrack 은 HEAD 이후 노출만 막는다.** 과거 커밋(`d8aa4c1`, 2026-04-08)에는 원문이 그대로 남아 있다.
- 값 동일성 대조 결과 **3종이 현재도 라이브**:
  1. `YOUTUBE_API_KEY` (sha256[:12] `a89c386ab5e3`)
  2. `DRIVE_CLIENT_SECRET` (`17bb444d7e36`)
  3. `DRIVE_REFRESH_TOKEN` (`f3300debddf9`) — client secret 재발급 시 함께 무효화되므로 재인증 플로우 재실행 필요
  - `GEMINI_API_KEY`(`c50d0068dcff`)는 이미 폐기 확인 → 로테이션 불요
  - 4번째 값(`eed7bdd40256`, AIza 형식 40자)은 활성 env 3곳에 없어 **출처 불명·미확인**
- **권고**: 위 3종 **키 로테이션**. 히스토리 완전 제거는 `filter-repo`/BFG + 전 팀 재클론이 필요한 파괴적 작업이라 **승인 없이 미실행**.
- **고지**: 아르고스가 최초 탐지 단계에서 키 원문을 자신의 도구 출력에 1회 노출했다(이후 sha256 지문 방식으로 전환, 임시파일 삭제). 해당 순간의 원문이 서브에이전트 transcript 에 남아 있을 가능성이 있어 사실대로 보고하며, 이는 로테이션 필요성을 더 뒷받침한다.

---

## 알려진 잔여 사항 (정직 고지)

1. **임베딩 미복구** — Gemini 키 폐기로 `generate_embedding` 실패 지속. 설계상 격리했으므로 카드 등록은 정상이나, `insurance_chunks` 저장은 건너뛴다(`embeddingFailed: True`, `embedding` 길이 0). **벡터 검색 품질은 이 영상에 대해 미반영 상태.** 키 로테이션 또는 대체 임베딩 도입이 후속 과제.
2. **Drive 첨부 접근 권한** — 카드는 `visibility='public'` 이지만 첨부 Drive 파일은 링크 공개 설정을 하지 않았다. 기존 첨부 문서 6건도 동일 모델이라 본 작업이 만든 회귀는 아니나, 위키 사용자가 첨부를 열 수 있어야 한다면 플랫폼 차원의 결정이 필요하다. **무단으로 외부 공개 전환하지 않았다.**
3. **정적분석 LOW** — `main.py:422` 에서 pyright 가 `transcript_upload` possibly-unbound 를 경고한다. 실제로는 `is_title_only` 분기가 `continue` 하므로 도달 불가능한 경로다(코드 확인 완료). 런타임 결함 아님, 향후 리팩터링 시 방어 권장.
4. **`git_evidence` FAIL 은 교차repo 오탐** — 검증기가 `/home/jay/workspace` 를 뒤졌으나(해당 repo 내 task-2980 커밋 0건) 실제 작업은 insuwiki worktree 에 있다(커밋 3건: `bbfdc41`, `f898a1b`, `1b8d0eb`). 자가 해소하지 않고 근거만 박제한다 — 판정은 ANU 권한.
5. **`scope_check` WARN(13 vs 11)** — 초과 3건은 `.gitignore`(보안 조치)와 테스트 2건으로 전부 설명 가능하다.
6. **커밋 혼입** — 불칸의 `summarizer.py`/`config.py`/`drive_uploader.py`/`firestore_writer.py` 변경이 동시 작업 중 아르고스의 `git add -A` 에 휩쓸려 `bbfdc41`(".pyc untrack" 메시지)에 함께 실렸다. 내용은 정상이나 **리뷰 시 커밋 메시지와 diff 범위가 불일치**함을 인지해야 한다.
7. **라이브 Drive 이스케이프 미검증** — 따옴표 포함 제목의 이스케이프는 mock 기반 단위테스트로 쿼리 문자열 무결성만 실증했다. 실제 Drive API 라이브 호출 검증은 다음 크론 주기에서 이뤄진다.

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 불칸 | 백엔드 구현 (2회 디스패치) | sonnet | 로직 구현 — haiku 부적합 |
| 아르고스 | 보안감사·테스터 | sonnet | 시크릿 판정은 분석 작업 — haiku 금지 대상 |
| 이리스 | 프론트엔드 | 미소집 | UI 변경 0건 |
| 아테나 | UX/UI | 미소집 | 화면 산출물 없음 |
| 헤르메스(팀장) | 설계·분배·검증·통합 | opus | 직접 코딩 없음. E2E 실측·게이트·판정만 수행 |

haiku 사용 0건.

---

## 커밋

| sha | 내용 |
|---|---|
| bbfdc41 | 아르고스: 커밋된 .pyc untrack + .gitignore 규칙 추가 (+ 불칸 4파일 혼입) |
| f898a1b | 불칸: 요약엔진 Claude Sonnet 교체 + 공개카드 등록 배선 |
| 1b8d0eb | 불칸: Codex 게이트 medium 2건 수정 + 첨부 파일명 정합 |

브랜치 `task/task-2980-dev1`, 원격 미반영(하네스 차단 규칙 준수 — 원격 반영은 ANU 가 Git Data API 로 수행).

---

## 결론

Phase 2 완료조건을 **전부 실측 충족**했다. Gemini 의존은 요약 경로에서 제거됐고, 임베딩 실패와 무관하게 공개 카드가 등록되며, 재실행 멱등성도 실측으로 확인했다. Phase 1 잔여 블로커였던 `.pyc` 평문 시크릿도 HEAD 기준으로 해소했다.

다만 **① 라이브 키 3종 로테이션**과 **② Drive 첨부 공개 권한 정책**은 회장/ANU 결정 사항이며, **머지는 HOLD** 상태로 ANU 독립검증을 기다린다.

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


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


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


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


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


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


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


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


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


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

