---
spec_id: gemini-evidence-gate
type: spec
created: 2026-05-06
updated: 2026-05-06
authors: [페룬, 회장]
status: active
related_tasks: [task-2454, task-2460, task-2463, task-2464, task-2465]
related_specs:
  - memory/specs/incident-2026-05-05-timer-merge-gemini.md
  - memory/specs/incident-2026-05-05-gemini-trigger.md
  - memory/specs/break-glass-procedure.md
---

# Gemini Evidence Gate — Phase 3 Spec

## 0. 한 줄 요약

PR 머지 차단 게이트는 **Gemini Code Assist App이 GitHub에 박제한 evidence(review/comment/check-run)** 만 신뢰한다. Gemini API 호출 0, secret 의존 0, neutral 자동 통과 0.

## 1. 배경

### 1.1 사고 연혁
- **task-2454 / PR #24**: Gemini 리뷰 0건 + `.merge-done` 마커 silent 생성 + 회장 토큰 수동 머지 → 부적절한 코드가 main 진입.
- **task-2460 보고서** §A: `gemini_review_gate.py:144-146, 271-280`이 `GEMINI_API_KEY` 부재/호출 실패 시 `conclusion="neutral"` → GitHub branch protection이 neutral을 success로 취급 → 자동 머지 통과. **silent corruption 사고의 핵심 결함**.
- **task-2464 조사**: Gemini App 트리거 메커니즘 분석. App은 webhook 기반이며 review/comment를 GitHub에 실제 박제. 우리 시스템이 "API 호출"이 아닌 "박제된 evidence"를 신뢰해야 한다는 결론.

### 1.2 회장 직접 설계 (2026-05-06)
> "Gemini API 방식은 사용하지 않는다. 우리는 Gemini Code Assist App만 사용한다.
> GEMINI_API_KEY 기반 호출은 제거한다.
> 대신 우리 시스템은 **GitHub에 실제로 남은 Gemini App evidence를 검증**한다."

## 2. 원칙 (절대 규칙)

1. **Gemini API 호출 0건** — `urllib`/`requests`/`google.generativeai` 등 어떤 형태로도 Gemini API endpoint 호출 금지. GitHub API(gh CLI)만 사용.
2. **secret 의존 0건** — `GEMINI_API_KEY` 환경변수 참조 금지 (코드/workflow 어디든).
3. **App evidence만 신뢰** — `gemini-code-assist[bot]` 발행 review/comment/check-run만 인정.
4. **HOLD(neutral)도 머지 차단** — neutral conclusion 발행 금지. 모든 비-PASS 상태는 GitHub check `failure`로 매핑.
5. **App 죽으면 모든 PR BLOCK** — 의도된 동작. silent pass 절대 금지. Emergency hotfix는 break-glass 절차로만.

## 3. 아키텍처

### 3.1 데이터 흐름
```
PR push → GitHub webhook → Gemini Code Assist App
                              ↓ (App이 GitHub에 박제)
                        review / comment / check-run
                              ↓
CI workflow → gemini_review_gate.py → gemini_evidence_verify.evaluate_gate()
                              ↓
                        GitHub API (gh api)로 evidence 조회
                              ↓
                        PASS / HOLD / BLOCK 판정
                              ↓
                        GitHub check run 발행 (success | failure)
                              ↓
                        Branch protection → merge 허용/차단
```

### 3.2 코드 구조

| 파일 | 역할 |
|------|------|
| `scripts/gemini_evidence_verify.py` | 핵심 평가 함수 `evaluate_gate(pr, sha, repo)`. GitHub API 조회 + 판정 + audit log. |
| `scripts/gemini_review_gate.py` | CLI wrapper. `evaluate_gate` 호출 + GitHub check run 발행. |
| `.github/workflows/ci.yml` | `gemini-review-gate` job + `phase3-merge-gate` job (둘 다 같은 wrapper 호출). |
| `tests/phase3_evidence_gate/` | 회귀 테스트 38건 (T1~T16 + TU1~TU3 series). |

### 3.3 두 check name이 공존하는 이유 (회장 명시 §8.1)
- GitHub branch protection ruleset이 check 이름으로 묶여있어 즉시 변경 시 보호 규칙 흔들림.
- 본 task는 **로직 단일화**까지만. `gemini-review-gate`와 `phase3-merge-gate` 둘 다 같은 `evaluate_gate` 호출 → 결과 동일.
- 체크명 통합은 **별도 후속 task**에서 안정화 후 진행.

## 4. Evidence 분류

### 4.1 Primary evidence (1건+ 있어야 PASS 가능)
| 타입 | 조회 endpoint | stale 판정 |
|------|---------------|-----------|
| review | `GET /repos/{repo}/pulls/{pr}/reviews` | `commit_id` ≠ head_sha 면 stale |
| review_comment | `GET /repos/{repo}/pulls/{pr}/comments` | `commit_id` ≠ head_sha 면 stale |
| issue_comment | `GET /repos/{repo}/issues/{pr}/comments` | `created_at` < `head_pushed_at` 면 stale (★ force-push 회피 차단) |

**필터**: `user.login == "gemini-code-assist[bot]"`

issue comment는 GitHub schema상 commit_id가 없으나, **force-push 시나리오에서 옛 review를 회피하고 옛 comment만으로 PASS받는 경로를 차단**하기 위해 `created_at` 기준 stale 판정을 적용한다 (회장 절대 규칙 §0 — silent pass 금지).
- `head_pushed_at`을 알 수 없거나 comment의 `created_at`을 알 수 없는 경우는 **보수적으로 stale=True** (의심스러우면 BLOCK).

### 4.2 Secondary evidence (보조 — audit log만)
| 타입 | 조회 endpoint | stale 판정 |
|------|---------------|-----------|
| check_run | `GET /repos/{repo}/commits/{sha}/check-runs` | head_sha 기반 조회이므로 항상 valid |

**필터**: `app.slug == "gemini-code-assist"` (★ 정확 매칭. name prefix 허용 안 함 — 다른 봇/수동 check 차단).

**secondary evidence는 audit log 기록만, 단독 PASS 판정 근거로는 사용 안 함**. 이유: review/comment 없이 check-run만으로 "리뷰가 진행됐다"는 증거가 약함. 회장 명시 §3.6에서 "본 task에서 명시"하라고 한 부분 — 본 spec에서 결정.

## 5. 판정 로직 (3-state)

### 5.1 결정 트리
```
1. high_severity_hits 1건+ ?
   └─ YES → BLOCK (reason: "high severity matched: {patterns}")
   └─ NO ↓

2. primary evidence 1건+ 있고 모두 stale ?
   └─ YES → BLOCK (reason: "all evidence stale (SHA mismatch)")
   └─ NO ↓

3. valid primary evidence (non-stale) 1건+ ?
   └─ YES → PASS (reason: "valid evidence found: N item(s)")
   └─ NO ↓

4. (현재 시각 - head_pushed_at) < 300초 ?
   └─ YES → HOLD (reason: "no evidence yet, elapsed Ns < 300s timeout")
   └─ NO  → BLOCK (reason: "evidence timeout (5min exceeded)")
```

### 5.2 GitHub check conclusion 매핑
| state | conclusion | exit code | 머지 가능? |
|-------|-----------|-----------|----------|
| PASS  | `success` | 0 | ✅ 가능 |
| HOLD  | `failure` ★ | 1 | ❌ 차단 (재시도 cron 등록) |
| BLOCK | `failure` | 1 | ❌ 차단 |

★ **neutral 절대 금지**: GitHub branch protection이 neutral을 success로 취급하므로, neutral 발행 = 자동 머지 통로 = task-2454 사고 재현. 본 게이트는 neutral 사용 안 함.

### 5.3 timeout 정책
- **기준점**: head SHA 마지막 push 시각.
  - 1차: `gh api repos/{repo}/pulls/{pr}` 의 `head.repo.pushed_at` (실제 push 시각에 가장 가까움)
  - 2차 fallback: `gh api repos/{repo}/commits/{sha}` 의 `commit.committer.date` (PR 조회 실패 시)
  - 이유: `committer.date`만 사용하면 오래된 commit을 force-push할 때 즉시 timeout BLOCK 발생 (committer.date는 commit이 작성된 시각이지 push 시각이 아님).
- **5분 (300초)**: 회장 확정 §8.2.
- **force-push 시 타이머 리셋**: 새 SHA의 push 시각 기준으로 다시 카운트.
- **재조정**: 1주 운영 후 3~7분 범위에서 재조정 (별도 task).
- **자동 재평가**: HOLD 상태에서 5분 경과 후 evidence가 늦게 도착해도 자동 재평가되지 않음 (현 구현 제약). 별도 후속 task에서 schedule cron 또는 webhook 기반 재평가 추가 예정.

## 6. High Severity 매칭

### 6.1 차단 패턴 4종 (확정 — 회장 §8.3)
| 종류 | 패턴 | 예시 |
|------|------|------|
| 이모지 | `🔴` 또는 `❌` (substring) | "🔴 SQL injection" |
| 명시 severity | `severity\s*:\s*(high\|critical)` (case-insensitive) | "Severity: HIGH" |
| 대문자 키워드 | `\b(BLOCKING\|CRITICAL\|MUST FIX)\b` (대문자 정확 매칭) | "MUST FIX: race condition" |
| h2 헤더 | `^\s*##\s+(High\|Critical\|Blocking)\b` (multiline) | "## High\n..." |

### 6.2 code block 제외
- fenced code block (```` ``` ... ``` ````)와 inline code (`` `code` ``)는 매칭 전 제거.
- 이유: 코드 예시에서 `BLOCKING`이라는 단어가 등장해도 차단하지 않기 위함.
- 구현: `strip_code_blocks(body)` 헬퍼 → high severity 매칭은 stripped body에 대해서만.

### 6.3 보조 신호 (단독 차단 금지)
| 패턴 | 동작 |
|------|------|
| `\bsecurity\b` | audit log 기록만 (`supplementary_signals`) |
| `\bdata loss\b` | audit log 기록만 |
| `\bregression\b` | audit log 기록만 |

이유: false positive 위험 (예: "no security issues"). 단독 차단 시 정상 PR이 차단됨. 추세 분석용으로만.

## 7. Audit Log

### 7.1 위치
`memory/audit/gemini-gate-{YYYYMMDD}.jsonl` — append-only, 일자별 파일.

### 7.2 레코드 필드
```json
{
  "timestamp": "2026-05-06T03:14:15Z",
  "pr": 29,
  "sha": "abc123def...",
  "repo": "JonghyukJeon/dev_workspace",
  "state": "pass|hold|block",
  "reason": "...",
  "elapsed_seconds": 42,
  "primary_count": 2,
  "valid_primary_count": 2,
  "secondary_count": 1,
  "high_severity_hits": [],
  "supplementary_signals": ["security"]
}
```

### 7.3 사용처
- 디버깅 (왜 특정 PR이 차단/통과되었는지 추적)
- 추세 분석 (보조 신호 빈도, timeout 분포 등 — 1주 운영 후 재조정 근거)
- 사고 조사 (incident response 시 이력 확인)

## 8. App 다운 시 운영

### 8.1 24h 끊김 알림 (★ 본 spec에서 명시, 구현은 후속 task)
- 별도 cron job: `scripts/gemini_app_health_check.py` (구현 후속).
- 마지막 24시간 동안 `gemini-code-assist[bot]`이 어떤 PR에도 review/comment 발행 안 했으면 회장 Telegram 알림.
- 알림 채널: **Telegram 단일** (Email 추가 안 함 — 회장 §8.5).

### 8.2 Emergency hotfix
- App이 백엔드 다운으로 review를 못 다는 24h+ 상황에서 critical security hotfix가 필요한 경우.
- **break-glass 절차** (별도 spec: `memory/specs/break-glass-procedure.md`) 사용.
- 회장 전용. 월 3회 / 분기 5회 hard cap. audit log 필수.

### 8.3 의도된 동작
- **App 죽으면 모든 PR BLOCK = 정상**. silent pass로 흐르지 않는다.
- 가용성 trade-off는 break-glass로 controlled.

## 9. 회귀 테스트

### 9.1 테스트 케이스 (T1~T16 + TU1~TU3, 38건)
- T1~T16: 통합 시나리오 — `evaluate_gate` 행동 검증.
- TU1: `strip_code_blocks` 단위 (4건).
- TU2: `match_high_severity` 단위 (10건 — 4종 패턴 + edge case).
- TU3: `match_supplementary` 단위 (8건).

핵심 보장:
- **T12**: `GEMINI_API_KEY` 누락 상태에서도 정상 동작 (의존 0).
- **T15**: 소스 코드에 Gemini API endpoint 0건 (정적 검증).
- **T16**: HOLD 상태에서 GitHub conclusion이 `failure`로 매핑 (neutral 절대 금지).

### 9.2 외부 의존
모든 테스트는 `mock_gh_api` fixture로 GitHub API 호출을 stub. 실제 `gh api` / network 호출 0.

## 10. 비-목표

- 본 spec에서는 다루지 않음:
  - check 이름 통합 (별도 후속 task에서 안정화 후)
  - timeout 5분 → 3~7분 재조정 (1주 운영 후 별도 task)
  - `scripts/gemini_app_health_check.py` 구현 (별도 task — 본 spec에서는 명시만)
  - `tools/ai-image-gen/`, `output/` 등 다른 Gemini API 사용 코드 (별개 시스템 — 본 spec은 Phase 3 review gate만)

## 11. 변경 이력

| 날짜 | 변경 | 관련 task |
|------|------|----------|
| 2026-05-06 | 최초 작성. evidence-only 게이트 도입. | task-2465 |
