# task-2810 / CI-4B — keyword_pool red 4건 정상화

- **팀**: dev6-team (페룬)
- **레벨**: Lv.2 · 검증레벨 normal
- **일시**: 2026-07-20
- **브랜치**: `task/task-2810-ci4b-dev6` (base `cc7476bfbd9f6b3ae3c99f9a7136cc9f94cd3ada`)
- **워크트리**: `/home/jay/workspace/.worktrees/task-2810-ci4b-dev6`
- **커밋**: `cd38731` (정확히 1개)

---

## S (Situation)
insuro `server/tests/test_keyword_pool_refresh.py` 에서 4건이 red 상태로 main CI 를 막고 있었다.
red 4건은 모두 `is_blocked("보험 …") is True` 를 기대하는 테스트였다.

## C (Complication)
`server/scripts/keyword_pool_refresh.py::is_blocked()` 는 키워드에 `"보험"` 문자열이 포함되면
blocklist 검사·브랜드 검사 이전에 **무조건 `False` 를 반환**한다(소스 119~134행, early return).

```python
if "보험" in kw_normalized:
    return False
```

즉 `"보험 채용"`·`"보험 시험"`·`"보험 사기"`·`"대출 보험"` 은 **통과(False)가 소스의 의도된 동작**이다.
회장 2026-07-19 결정: 이 면제는 **의도된 보험 도메인 정책**이며, 위 4건은 정당한 보험 주제다.
→ **소스가 옳고 테스트가 낡았다.**

## Q (Question)
소스를 건드리지 않고, (1) red 4건을 정상화하면서 (2) 보험 도메인 허용 계약과
(3) 일반 noise 차단 계약을 **동시에 유지**할 수 있는가?

## A (Answer)
red 4건의 **패턴 검증은 그대로 유지하되 보험 문맥만 제거**했다.
소스 수정 0건, 테스트 파일 1개만 수정, 커밋 1개.

---

## 수정 파일 (정확히 1개)
| 파일 | 변경 |
|---|---|
| `server/tests/test_keyword_pool_refresh.py` | +74 / -4 |

⚠️ `server/scripts/keyword_pool_refresh.py` **수정 0건** (아래 검증 원문 참조)

---

## 변경 상세

### 1. 교체 4건 — 보험 문맥 제거 (패턴 검증은 유지)
| 테스트 | 기존 (red) | 교체 후 | 패턴 | 실측 |
|---|---|---|---|---|
| `test_blocks_recruitment_keyword` | `보험 채용` | `채용 공고` | `채용\|연봉\|시험` | True |
| `test_blocks_exam_keyword` | `보험 시험` | `공무원 시험` | `채용\|연봉\|시험` | True |
| `test_blocks_fraud_keyword` | `보험 사기` | `전세 사기` | `사기\|먹튀\|고소` | True |
| `test_blocks_loan_keyword` | `대출 보험` | `대출 금리 비교` | `대출\|카드\|주식` | True |

**근거**: 각 교체 키워드는 원 테스트가 검증하던 **동일 패턴에 그대로 매칭**된다.
특히 `사기` 카테고리는 Codex 지적(카테고리 이탈 금지)에 따라 **`전세 사기`로 `사기` 계열을 유지**했다.

### 2. 사기 계열 보조 3건 추가 (`사기|먹튀|고소` 커버리지 보강)
`투자 사기` · `먹튀 사이트` · `고소 방법` → 전부 True 실측 확인.

### 3. 경계 테스트 5건 보존 (`TestIsBlockedInsuranceExemptionBoundary` 신규)
`"보험"` 면제가 **오타·공백·영문 표기로는 발동하지 않음**을 지키는 계약.
`ded17c1` 을 cherry-pick 하지 않고 **직접 재작성 + 실측 재검증**했다.

| assert | 의미 | 실측 |
|---|---|---|
| `is_blocked("보협 마약") is True` | `보험` 오타 → 면제 미발동 | True |
| `is_blocked("보 험 마약") is True` | 공백 삽입 → 면제 미발동 | True |
| `is_blocked("insurance 마약") is True` | 영문 표기 → 면제 미발동 | True |
| `is_blocked("마약 단속") is True` | 보험 문맥 없음 | True |
| `is_blocked("살인 사건") is True` | 보험 문맥 없음 | True |

### 4. 보험 도메인 허용 계약 4건 (`TestIsBlockedInsuranceDomainAllowed` 신규)
`보험 채용`·`보험 시험`·`보험 사기`·`대출 보험` → 전부 통과(False) 확인.

### 5. 제거 대상 2건 — 부재 확인
`is_blocked("보험 마약") is False` / `is_blocked("보험 살인") is False`
→ **base `cc7476b` 에 애초에 존재하지 않았다.** (그 2건은 제외 지시된 초안 `ded17c1` 에만 있었음)
따라서 "제거" 는 **신규 추가 금지**로 이행했고, 결과적으로 파일 내 0건이다(grep 원문 참조).
`"버그가 아니라 현행 정책의 특성화"` 취지 docstring 도 0건.

### 6. 미해결 공백 — 주석 1줄만
```python
# NOTE: "보험" 문자열이 blocklist 전체를 우회하는 정책 공백은 미해결이며 CI-4c(백로그) 소관.
```

---

## ★ 주장하지 않는 것 (명시)
**"정책 공백 해결 완료" 를 주장하지 않는다.**
`"보험"` 문자열 하나가 blocklist 전체를 우회하는 문제는 **미해결**이며 **CI-4c(백로그) 소관**이다.
예: `보험 마약`·`보험 살인`·`보험 코스피` 는 여전히 통과한다 — 본 작업 범위 밖이며 고치지 않았다.
문서·주석 어디에도 해결된 것처럼 서술하지 않았다.

---

## 테스트 결과

### pytest 실행 원문 (수정 전 — base `cc7476b`)
```
========================= 4 failed, 34 passed in 0.26s =========================
FAILED server/tests/test_keyword_pool_refresh.py::TestIsBlockedPattern::test_blocks_recruitment_keyword
FAILED server/tests/test_keyword_pool_refresh.py::TestIsBlockedPattern::test_blocks_exam_keyword
FAILED server/tests/test_keyword_pool_refresh.py::TestIsBlockedPattern::test_blocks_fraud_keyword
FAILED server/tests/test_keyword_pool_refresh.py::TestIsBlockedPattern::test_blocks_loan_keyword
```

### pytest 실행 원문 (수정 후)
```
$ python3 -m pytest server/tests/test_keyword_pool_refresh.py -q -p no:randomly
server/tests/test_keyword_pool_refresh.py .............................. [ 60%]
....................                                                     [100%]
============================== 50 passed in 0.15s ==============================
```

### 테스트 카운트 전후
| | 전 | 후 | 증감 |
|---|---|---|---|
| collected | 38 | 50 | **+12** |
| passed | 34 | 50 | +16 |
| failed | **4** | **0** | -4 |
| skip / xfail | 0 | 0 | **0** |

감소 0건. skip/xfail 증가 0건 (grep `skip\|xfail` → 0건).

---

## L1 스모크테스트 결과
pytest PASS ≠ 실동작 확인이므로, **실제 모듈 + 실제 프로덕션 시드 데이터**로 파이프라인을 직접 실행했다.

- **서버 재시작**: 해당없음 (서버 없는 배치 스크립트 · 테스트 전용 변경)
- **API 응답 확인**: 해당없음 (HTTP 엔드포인트 아님)
- **스크린샷**: 해당없음 (UI 없음)
- **실행 로그**: `/home/jay/workspace/memory/reports/artifacts/task-2810-l1-smoke.txt`

### 실행 내용 및 결과 (원문 요약)
```
=== L1: 실제 모듈 + 실제 시드 데이터 파이프라인 실행 ===
실제 시드 로드: 30개 (그룹 6개)          ← config/keyword-seeds.json 실파일
통과 30개 / 차단 0개                      ← 보험 시드 오차단 0
카테고리 분류 샘플: [('암보험','암/건강'), ('종신보험','생명/종신'), ...]

=== L1: noise 유입 시뮬레이션 ===
BLOCK 채용 공고 / 공무원 시험 / 전세 사기 / 투자 사기 / 먹튀 사이트 / 고소 방법
BLOCK 대출 금리 비교 / 마약 단속 / 살인 사건 / 보협 마약 / 보 험 마약
BLOCK insurance 마약 / 코스피 전망 / 비트코인 시세
noise 차단률: 14/14

=== L1: 보험 도메인 허용 계약 ===
PASS 보험 채용 / 보험 시험 / 보험 사기 / 대출 보험 / 암보험 비교 / 실손보험 갱신
보험 도메인 통과율: 6/6

L1 판정: PASS
```

**L1 통과** — 실제 모듈을 임포트해 실제 시드 30개를 흘려보냈고, 보험 시드 오차단 0건 ·
noise 14/14 차단 · 보험 도메인 6/6 통과를 관측했다.

---

## 완료 판정 대조 (MATCH / GAP)
| 판정 기준 | 결과 | 근거 |
|---|---|---|
| `pytest … -q -p no:randomly` → 0 failed | **MATCH** | `50 passed` 원문 |
| 보존 5건이 실제 assert 로 존재 | **MATCH** | `TestIsBlockedInsuranceExemptionBoundary` 5개 메서드 · diff 확인 |
| 제거 2건이 존재하지 않음 | **MATCH** | `grep -c "보험 마약\|보험 살인"` → **0** |
| 사기 카테고리가 `사기` 계열로 검증됨 | **MATCH** | `전세 사기` + `투자 사기` (+`먹튀 사이트`·`고소 방법`) |
| 테스트 개수 감소 0 | **MATCH** | 38 → 50 (+12) |
| skip/xfail 증가 0 | **MATCH** | `grep -c "skip\|xfail"` → **0** |
| 소스 파일 수정 0 | **MATCH** | `git diff cc7476b -- server/scripts/keyword_pool_refresh.py \| wc -l` → **0** |
| 커밋 정확히 1개 | **MATCH** | `git rev-list --count cc7476b..HEAD` → **1** |
| PR 생성 0 · main merge 0 | **MATCH** | worktree keep · PR 미생성 |
| `ded17c1` cherry-pick 0 | **MATCH** | base 에서 신규 분기 · 경계테스트 직접 재작성 |

**GAP: 없음.**

### 검증 원문
```
$ git status --short
 M server/tests/test_keyword_pool_refresh.py          ← 테스트 1파일만

$ git diff cc7476bfbd9f6b3ae3c99f9a7136cc9f94cd3ada -- server/scripts/keyword_pool_refresh.py | wc -l
0                                                     ← 소스 수정 0

$ grep -c "보험 마약\|보험 살인" server/tests/test_keyword_pool_refresh.py
0
$ grep -c "skip\|xfail" server/tests/test_keyword_pool_refresh.py
0

$ git rev-list --count cc7476bfbd9f6b3ae3c99f9a7136cc9f94cd3ada..HEAD
1
```

---

## 발견 이슈 및 해결
1. **제거 대상 2건이 base 에 부재** — 지시서는 `보험 마약`/`보험 살인` 테스트 제거를 요구했으나
   `cc7476b` 에는 존재하지 않았다(제외 지시된 `ded17c1` 초안에만 존재).
   → 제거 대신 **신규 추가 금지**로 이행하고 grep 0건으로 부재를 증명. 보고서에 명시.
2. **경계 테스트 출처** — `ded17c1` cherry-pick 금지 제약 하에서 계약만 가져와야 했다.
   → 5개 assert 를 **직접 재작성**하고, 작성 **전에** 실제 모듈로 5건 전부 True 실측 후 반영.
3. **G3 게이트 지시와 task 절대제약 충돌** — 게이트 지시는 `--action pr` 이나
   task 는 `PR 생성 0 · main merge 0` 을 절대제약으로 명시.
   → **task 절대제약 우선** 적용(worktree keep, PR 미생성). 아누 판단 필요 — 아래 머지 판단 참조.

---

## 머지 판단
- **머지 필요**: **No (팀장 자체 머지 금지)** — task 절대제약 `PR 생성 0 · main merge 0`
- **브랜치**: `task/task-2810-ci4b-dev6`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2810-ci4b-dev6`
- **베이스**: `cc7476bfbd9f6b3ae3c99f9a7136cc9f94cd3ada`
- **커밋**: `cd38731` (1개)
- **머지 의견**:
  테스트 파일 1개만 변경 · 소스 무수정 · 50 passed 0 failed · 테스트 수 순증(+12) ·
  skip/xfail 0 · L1 PASS. **다른 팀 작업과 파일 겹침 없음**(테스트 파일 단독).
  충돌 위험 낮음. 다만 **머지 권한은 아누(개발실장)에게 위임**한다 — 본 브랜치가 main 에 들어가면
  CI-unblock 의 마지막 조각이 풀린다.

---

## 모델 사용 기록
| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 페룬(팀장) | 설계·실측검증·통합·독립검증 | Opus 4.8 | 직접 코딩 0 |
| 벨레스 | 테스터 — 테스트 파일 수정 | **sonnet** | 테스트 코드 수정, haiku 미사용 |

스바로그(백엔드)·라다(프론트)·모코시(UX/UI) 는 **미소집** — 소스 수정 금지 · UI 무관 작업이므로 불필요.
디자인 작업 없음 → 디자인팀 호출 불필요.

## 인지 검증 기록 (팀원 보고를 믿기 전에 의심)
벨레스 보고를 그대로 수용하지 않고 팀장이 **독립 재실행**했다:
- `git status --short` 재확인 → 변경 파일 1개
- `git diff cc7476b -- server/scripts/…` → 0줄 (소스 무수정 직접 증명)
- 금지 문자열 grep 2종 → 각 0건
- pytest 재실행 → `50 passed` 직접 관측
- `git diff` **전문 육안 확인** → assert 가 실제 존재하며 문자열만 있는 게 아님을 확인
- 교체·보존 키워드 24개를 **코드 작성 전에** 실제 모듈로 사전 실측 (추정 0)

---

## ★ 완료 상태: G4 게이트 ESCALATED — `.done` 미생성 (회장 결정 필요)

**작업 산출물은 green 이나, finish-task.sh 가 G4 게이트에서 차단되어 `.done` 이 생성되지 않았다.**
`.done` 을 수동 생성하지 않았다(수동 생성 금지 원칙 준수).

### 차단 사유
```
[G4-GATE] ESCALATED — scope_violation 또는 fix_loop_cap. .done 차단.
{
  "gate_trigger": "fix_loop_cap",
  "risks": [{"severity":"critical",
             "description":"fix_loop_count=2 >= max=2 — OWNER_DECISION_REQUIRED"}],
  "suggestions": ["회장 결정 필요 (fix_loop hard cap 도달)"],
  "action": "ESCALATED_OWNER_DECISION",
  "marker_path": "/home/jay/workspace/memory/events/task-2810.g4-fix-loop-cap.json"
}
```
- `scope_violation`: **false** (범위 위반 아님)
- 차단 원인은 **오직 fix_loop_count 카운터**

### 카운터가 소진된 경위 (코드 결함 아님)
`scripts/gemini_cli_gate_check.py:499-502` 는 QC 를 통과해 G4 단계에 도달한
**모든 finish-task.sh 실행마다 카운터를 +1** 한다(코드 수정 횟수가 아니라 **게이트 실행 횟수**).

본 작업에서 카운터 2회는 **전부 인프라 진단 재실행**으로 소진되었다. 코드 재수정 0회:
1. **1회차(14:00)** — 1차 실행이 `git_evidence FAIL` 로 중단. 원인: finish-task.sh 에 project_path 미전달 →
   evidence root 가 `/home/jay/workspace` 로 잡혀 worktree 의 커밋을 못 찾음
   (`COMMIT_EXISTS: task-2810 커밋 0건`). → project_path 를 worktree 로 전달해 해소.
2. **2회차(14:01)** — `GOAL-GATE FAIL`. 원인: goal assertion 이 `bash -c` 로 **호출자 cwd** 에서 실행되는데
   `/home/jay/workspace` 에는 `server/tests/` 가 없어 `collected 0 items`.
   → worktree 를 cwd 로 두고 재실행해 해소(동일 명령이 worktree 에서는 `50 passed`).
3. **3회차** — 위 2건이 모두 해소된 상태였으나, 카운터가 이미 2/2 라 **hard cap 선검사에서 즉시 차단**
   (`gemini_cli_gate_check.py:421`, cap 검사가 Gemini 호출보다 먼저 수행됨).

즉 **3회차는 실제로는 통과 가능한 상태였으나 카운터 소진으로 평가 자체가 차단**되었다.

### 요청 사항 (아누/회장 판단 필요)
- `task-2810.g4-fix-loop-count` (현재 `2`) 리셋 승인 후 finish-task.sh 재실행, 또는
- 게이트 예외 승인으로 `.done` 생성 경로 개방

**팀장이 카운터를 자체 리셋하지 않은 이유**: 게이트가 `OWNER_DECISION_REQUIRED` 를 명시하며,
자체 리셋은 거버넌스 게이트 우회에 해당한다. 선례도 일관되지 않다
(cap 도달 5건 중 `.done` 도달은 task-2791 1건뿐, 나머지 3건은 미생성).
→ **판단을 위임**한다.

### 이 차단이 산출물 품질과 무관함을 보이는 증거
| 항목 | 상태 |
|---|---|
| QC 게이트 | **통과** (`qc_result: WARN`, `.qc-done` 생성됨) |
| pytest | **50 passed, 0 failed** (차단 이후 재확인) |
| goal assertion | worktree cwd 에서 **PASS** (`50 passed`) |
| scope_violation | **false** |
| 커밋 | `cd38731` 1개, worktree clean |
| merge | `merge_policy=none` → `.finalize-only` 생성, **머지 미실행**(의도대로) |

### 미완료 항목 (정직 기재)
- `.done` **미생성**
- `task-2810.terminal-result.json` **미작성** (G4 exit 로 terminal result 단계 미도달)
- task-timer end **미호출** (finish-task.sh 내부 단계라 도달 못 함)

---

## 비고
- 버그: **없음**
- 미해결(의도적): `"보험"` blocklist 우회 정책 공백 → **CI-4c 백로그 소관**. 본 작업에서 해결 주장 안 함.
- 부수 관찰(본 작업 무관, 참고):
  - `task-2810.supervisor-crash-marker.json` 의 팀 귀속이 `dev4-team`/`vishnu` 로 **오기재**됨
    (실제 dev6-team/페룬). 크래시 마커 작성기의 팀 해석 버그로 보이며 별도 확인 권장.
  - QC `duplicate_check` WARN(유사도 100%)는 `task-2810.md` 와 `ci-4b-replacement.md` 가
    **의도적 동일 사본**이기 때문 (지시서 요구 경로 + 훅 요구 경로 양쪽 충족). 결함 아님.
  - `git stash` 76개 누적 경고 관측 (workspace 공통 이슈).

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


---

## 회수·검증 및 G4 리셋 판정 (2026-07-20, 회장 위임 판단)

### 독립 재검증
| 항목 | 주장 | 실측 | 판정 |
|---|---|---|---|
| pytest | 0 failed / 50개 | `50 passed in 0.16s`, skip/xfail 0 | ✅ 일치 |
| 수정 범위 | 테스트 1파일, 소스 0 | `cd38731`: `server/tests/test_keyword_pool_refresh.py` +74/-4 단일 | ✅ 일치 |
| 커밋 | 1개 | 1개, base `cc7476b`, worktree clean | ✅ 일치 |

### G4 fix_loop_cap 판정 — **리셋 승인**

근거 3건:

1. **카운터 의미 결함(코드 확인)** — `scripts/gemini_cli_gate_check.py` L499-502 의 증가 코드는
   `if not gate_pass` 블록 **바깥**에 있어 **무조건 실행**된다.
   즉 `fix_loop_count` 는 fix 반복이 아니라 **G4 평가 도달 횟수**를 센다. 이름·cap 취지와 불일치.

2. **실제 gate 실패 0회** — `_save_g4_marker` 는 `not gate_pass` 일 때만 호출된다.
   task-2810 에는 `.g4-soft`·`.g4-fail` 마커가 **존재하지 않고** 3회차의 `.g4-fix-loop-cap.json` 만 있었다.
   → 카운터를 소진한 1·2회차는 **모두 `gate_pass=True`**. 이 task 코드로 G4 가 실패한 적은 **한 번도 없다**.

3. **cap 선검사 순서** — cap 검사(L421)가 Gemini 평가(L455)보다 앞서므로 3회차는 코드 평가 자체가 없었다.
   따라서 이 escalation 은 코드 품질에 대한 신호값이 **0**이다.

→ 인프라 오탐. 팀의 "코드 결함 아님" 주장은 사실이며, 팀이 자체 리셋을 거부하고 위임한 판단도 적절했다.

### 조치 결과
- `task-2810.g4-fix-loop-count` 2 → 0 리셋, stale `.g4-fix-loop-cap.json` 은 `events/archive/` 로 보존 이관
- 판정 근거 박제: `memory/events/task-2810.g4-fix-loop-reset-approval.json`
- worktree cwd 에서 `finish-task.sh task-2810 dev6 <worktree>` 재실행 → **완주**

```
[G4-GATE] PASS 또는 soft (PR open 허용).   fix_loop_count=1 / max=2
[OK] finish-task.sh completed for task-2810
```

- **`.done` 생성됨** — `status=done`, gate 5/5 PASS
  (impact_scanner / ci_preflight / l1_smoketest / **goal_assertions** / unresolved_gate)
  → `goal_assertions` 가 PASS 로 전환된 것은 PROJECT_PATH 가 worktree 로 정상 전달된 결과로,
    팀이 지목한 인프라 원인 진단이 옳았음을 사후 확인해 준다.
- `merge_policy=none` honored → **PR 0 / merge 0 유지**, merge block 미진입 (의도된 finalize-only)

### 잔여 (본 task 범위 외)
- **시스템 결함**: G4 카운터 무조건 증가(L499-502)는 `not gate_pass` 블록 안으로 옮기거나
  `eval_count` 로 개명해야 한다. 동일 오탐이 task-2566 / 2729+1 / 2729+10 / 2788+3 / 2791 에서 반복 관측됨. → 별도 task 필요
- **`보험` blocklist 우회 정책 공백**: CI-4c 백로그 소관. 본 task 에서 해결 주장 없음.
