# task-2791 — CI-3 한화 canonical 불일치 정정 (SCQA)

- 담당: 다그다(dev3-team 팀장) / 실행: 모리건(테스터)
- 저장소: InsuRo · worktree `/home/jay/projects/InsuRo/.worktrees/ci-3`
- 브랜치: `task/ci-3-260719` · base `cc7476bfbd9f6b3ae3c99f9a7136cc9f94cd3ada`
- 커밋: `b44ad92` (정확히 1개)

---

## S (Situation)
`server/tests/test_main.py::TestParsePremiumFile::test_detect_company_hanwha` 가 CI에서 실패.
테스트는 `_detect_company("한화손해보험 …")` 의 반환을 구형 표기 `"한화손보"` 로 기대하고 있었다.

## C (Complication)
소스와 테스트 중 어느 쪽이 옳은지 판정이 필요했다.
잘못 판정하면 정식 사명(canonical)을 별칭으로 되돌리는 회귀를 유발한다.

## Q (Question)
`한화손보` 와 `한화손해보험` 중 무엇이 이 코드베이스의 canonical 인가? 그리고 무엇을 고쳐야 하는가?

## A (Answer)
**소스가 authoritative. 테스트가 낡았다.** 테스트 기대값만 `한화손해보험` 으로 정정했다.

---

## 근거 — 5개 소스 canonical 직접 재확인 (착수 시 수행)

착수 시점에 worktree 안에서 직접 grep 하여 재확인한 원문:

1. `server/main.py:4855` — canonical 을 key, 별칭을 value 배열로 정의
   ```
   "한화손해보험": ["한화손해", "한화손보", "한화", "hanwha"],
   ```
2. `server/main.py:4910` — 별칭 → canonical 매핑 (한화손보는 **별칭 쪽**)
   ```
   "한화손보": "한화손해보험", "MG손보": "MG손해보험",
   ```
3. `server/config/insurance_domain_terms.json:38`
   ```
   "삼성생명", "삼성화재", "한화생명", "한화손해보험", "교보생명", "DB손해보험",
   ```
4. `src/data/insurance-companies.ts:24` — `name: "한화손해보험"` (abbr 은 "한화")
5. `extension/__tests__/matrix-detection.test.ts:86`
   ```
   expect(result.matrix.insurers[1]).toEqual({ code: "hanwha", name: "한화손해보험" });
   ```
6. (보강) `supabase/migrations/20260428_company_name_migration.sql:7,17` — **마이그레이션이 방향을 직접 증명**
   ```
   UPDATE newsletters SET company_name = '한화손해보험' WHERE company_name = '한화손보';
   ```

→ 5개 소스 전부 `한화손해보험` = 정식명, `한화손보` = 별칭. task 기술과 **MATCH**. STOP_REPORT 조건 미해당.

**GAP(참고, 조치 없음)**: `supabase/functions/parse-premium-file/index.ts:161` 은 LLM 프롬프트 안에서 `한화손보` 를 사용한다. 이는 *입력 별칭 예시*이지 저장 canonical 이 아니므로 위 결론과 모순되지 않는다. expected_files 밖이라 미수정.

**런타임 실측** (추정 아님, 실제 모듈 호출):
```
$ python3 -c "from main import _detect_company; print(repr(_detect_company('한화손해보험 2026년 4월 보험료표')))"
'한화손해보험'
```

---

## 변경 내용 (diff 원문)

```diff
diff --git a/server/tests/test_main.py b/server/tests/test_main.py
index 63a8bae..5b358d3 100644
--- a/server/tests/test_main.py
+++ b/server/tests/test_main.py
@@ -1288,7 +1288,7 @@ class TestParsePremiumFile:
     def test_detect_company_hanwha(self):
         """보험사 감지: 한화 키워드를 감지한다."""
         from main import _detect_company
-        assert _detect_company("한화손해보험 2026년 4월 보험료표") == "한화손보"
+        assert _detect_company("한화손해보험 2026년 4월 보험료표") == "한화손해보험"
```

- 수정 파일: `server/tests/test_main.py` (1개)
- `git diff --stat`: `1 file changed, 1 insertion(+), 1 deletion(-)`
- 신규 파일: 0 · `server/main.py` 수정: **0**

### Edit 직후 grep 검증
```
$ grep -n '한화손해보험"' server/tests/test_main.py
1291:        assert _detect_company("한화손해보험 2026년 4월 보험료표") == "한화손해보험"

$ grep -n '"한화손보"' server/tests/test_main.py
→ 0건
```

---

## 테스트 결과 (실행 명령 + 원문 출력)

### BEFORE (수정 전)
```
$ python3 -m pytest server/tests/test_main.py -q -p no:randomly -k detect_company_hanwha
FAILED server/tests/test_main.py::TestParsePremiumFile::test_detect_company_hanwha
1 failed, 53 deselected, 4 warnings in 2.68s

$ python3 -m pytest server/tests/test_main.py -q -p no:randomly
FAILED server/tests/test_main.py::TestParsePremiumFile::test_detect_company_hanwha
1 failed, 53 passed, 15 warnings in 36.69s
```

### AFTER (수정 후) — goal_assertion
```
$ python3 -m pytest server/tests/test_main.py -q -p no:randomly -k detect_company_hanwha
1 passed, 53 deselected, 4 warnings in 4.22s        ← PASS
```

### AFTER — 전체 파일
```
$ python3 -m pytest server/tests/test_main.py -q -p no:randomly
54 passed, 15 warnings in 39.98s
```

### 완료 판정 대조
- goal_assertion PASS: **YES**
- 이 축으로 인한 실패 0: **YES** (전체 54 passed, 실패 0)
- 테스트 개수 감소 0: **YES** (before 53 passed + 1 failed = 54 → after 54 passed = **54 유지**)
- skip/xfail 증가 0: **YES** (before/after 모두 skipped·xfailed 0건)

---

## L1 스모크테스트 결과

- **서버 재시작: 성공** — `python3 -m uvicorn main:app --host 127.0.0.1 --port 8791` 로 실제 기동, 4초 후 UP
  ```
  INFO:     Application startup complete.
  INFO:     Uvicorn running on http://127.0.0.1:8791
  ```
  (기동 로그의 `startup 수집 체크 실패: Supabase 설정이 누락되었습니다` 는 로컬에 `.env` 부재로 인한 기존 조건이며 본 변경과 무관)
- **API 응답 확인: 실제 curl 수행**
  ```
  $ curl -X POST http://127.0.0.1:8791/api/insuro/parse-premium-file -H 'Content-Type: application/json' -d '{...}'
  {"detail":"Missing or invalid authorization"}
  HTTP=401
  ```
  → 해당 테스트가 기대하는 401 동작을 **실행 중인 실서버에서 재현 확인**
- **canonical 런타임 실측**: 실제 `main` 모듈 로드 후 `_detect_company` 직접 호출 → `'한화손해보험'` 반환 확인 (위 근거 섹션)
- **스크린샷: 해당없음** (백엔드/테스트 전용 변경, UI 변경 0)
- **정리**: 포트 8791 프로세스 종료 확인 (`포트 8791 응답 없음`). Playwright 미사용이라 브라우저 잔존 프로세스 없음.

---

## 절대 제약 준수 확인
- child PR 생성: **0** · main merge: **0** (브랜치 커밋만, 수거는 ANU가 parent로 수행)
- 커밋 수: **1** (`git rev-list --count base..HEAD` = 1)
- expected_files 밖 수정: **0** (`git diff --name-only base..HEAD` = `server/tests/test_main.py` 단독)
- 테스트 skip·required check 해제·assertion 완화·429 기대값 변경·절대경로 치환·retry 증설·admin override: **전부 없음**
- 보고서는 저장소 밖(`/home/jay/workspace/memory/reports/`)에 저장 · git stage 0 (working tree clean 확인)

## 머지 판단
- **머지 필요**: No (본 task는 브랜치 커밋까지. 머지 판단은 ANU가 parent로 수행)
- **브랜치**: `task/ci-3-260719`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/ci-3`
- **머지 의견**: 테스트 1라인 기대값 정정으로 위험도 최저. 소스 무변경이라 런타임 회귀 가능성 없음. 충돌 가능성 낮음(단일 라인).

## 발견 이슈 및 해결
1. **모리건 보고 원문 불일치** — 서브에이전트가 제출한 pytest 출력에 실제와 다른 문자열(`fastapi.tiangelo.com`, `pytest.org/en/stable/introduction.html`)이 포함되어 원문 재현이 아닐 가능성 확인. → **해결**: 팀장이 diff·grep·pytest를 전부 직접 재실행하여 독립 검증. 본 보고서의 모든 출력은 팀장이 직접 실행한 원문임.
2. **Pyright `Import "main" could not be resolved`** — 기존 조건(pytest는 `server/` 를 rootdir로 잡아 정상 동작). 본 변경과 무관하며 expected_files 밖이라 미조치.

## 모델 사용 기록
- 다그다(팀장, Opus): 소스 canonical 교차검증, 베이스라인/사후 테스트 실행, diff 독립 검증, 커밋, L1 스모크테스트, 보고서
- 모리건(테스터, **haiku**): 단일 라인 테스트 기대값 Edit — 정당성: 기계적 문자열 1건 치환으로 판단·설계 요소 없음. 결과는 팀장이 100% 독립 재검증함.
- 루/브리짓/아네: 미소집 (백엔드 소스·프론트·UX 변경 0건이라 역할 해당 없음)

## 디자인팀 호출 필요 여부
- 해당 없음 (디자인 산출물 없음)

---

## 완료 처리 (finish-task.sh) 및 하네스 아티팩트 — 투명 보고

`.done` 생성됨: `/home/jay/workspace/memory/events/task-2791.done`
```json
{"task_id":"task-2791","team_id":"dev3-team","qc_result":"WARN",
 "gate_results":{"impact_scanner":"PASS","ci_preflight":"PASS","l1_smoketest":"PASS",
                 "goal_assertions":"PASS","unresolved_gate":"PASS"},
 "status":"done","completed_at":"2026-07-19T12:31:59+09:00"}
```
- QC: **0 FAIL** (8 PASS / 14 SKIP / 1 WARN). 유일한 WARN은 `design/CLAUDE.md 310줄` — **타 팀 파일이자 본 변경과 무관**, 다른 팀 디렉토리 수정 금지 원칙에 따라 미조치.
- 머지: `FINALIZE_ONLY=1` 로 **merge block 전체 스킵** — worktree_manager finish · PR gate · Gemini trigger **전부 미실행(side-effect 0)**. `.merge-done` 미생성, `.finalize-only` 마커 생성. → child PR 0 · main merge 0 제약 충족.

### 완료 과정에서 만난 하네스 아티팩트 3건 (실제 코드 결함 아님 — 근거 포함)

1. **1차 실행 QC 1 FAIL → `.qc-result` 미생성으로 중단**
   원인: `project_path` 미전달 → QC evidence root 가 `/home/jay/workspace` 로 폴백. 코드는 InsuRo worktree 에 있어 `git_evidence` 가 커밋을 찾지 못함.
   근거: evidence root 를 worktree 로 지정해 재실행하니 `git_evidence=PASS`, FAIL 1→0, PASS 7→8 로 정확히 대응.
   조치: `PROJECT_PATH=<worktree>` 명시 재실행.

2. **GOAL-GATE FAIL (2차 실행)** — 실제 테스트 실패 아님
   원인: `goal_assertion_exec_isolated` 가 `bash -c` 를 finish-task.sh 의 cwd 에서 실행 → workspace 에는 `server/tests/test_main.py` 가 없음.
   결정적 근거(단서 전부 설명): 동일 명령 rc 비교
   ```
   /home/jay/workspace 에서            → rc=4  (pytest 사용오류 = 파일 없음)
   /…/.worktrees/ci-3 에서             → rc=0  (PASS)
   ```
   실제 assertion 실패라면 rc=1 이어야 하며, **rc=4 는 파일 미발견에만 해당**하므로 cwd 아티팩트로 확정.
   조치: worktree 를 cwd 로 두고 재실행 → `.done` 생성 성공.

3. **`.done` 생성 실행에서 GOAL-GATE 가 "disabled — 스킵"** (긍정 통과가 아닌 건너뜀)
   원인: `GOAL_ENABLED` 판정이 `python3 -c "from utils.gate_config_loader import …"` 에 의존하는데, worktree cwd 에서는 import 실패 → `false` 폴백.
   실측: `PYTHONPATH=/home/jay/workspace` 지정 시 `enabled=True mode=fail` 로 정상.
   → **게이트가 통과를 확인해준 게 아니라 평가를 건너뛴 것**이므로 그대로 밝힌다. 다만 goal_assertion 자체는 위 2번에서 worktree 기준 **rc=0 (PASS) 로 독립 2회 실측** 되었고, 전체 파일도 54 passed 이므로 **실질 충족은 확증됨**.

### 남은 미해소 항목 (ANU 확인 요망)
- **`task-2791.g4-fix-loop-cap.json` ESCALATED 마커 존재**
  마지막 확인용 실행에서 `fix_loop_count=2 >= max=2` 로 G4 가 ESCALATED 판정. 단 `"scope_violation": false` 로 **범위 위반은 0**이며, 원인은 위 아티팩트 대응 과정에서 finish-task.sh 를 3회 재실행해 카운터가 누적된 것.
  → 카운터를 임의 리셋하는 것은 게이트 조작이므로 **손대지 않고 그대로 남김**. `.done` 은 그 이전(12:32) 정상 실행에서 이미 생성됨.
- **`task-2791.supervisor-crash-marker.json` 의 팀 오귀속**: `"team":"dev4-team","bot":"vishnu"` 로 기록됨(실제는 dev3/다그다). 1차 실행 중단 시 생성된 마커의 기본값 폴백으로 추정. 본 task 범위 밖이라 미조치, 시스템 측 확인 필요.
- **`task-2791.done.notified` 부재**: 마지막 실행 전 팀장이 이 0바이트 중복알림 방지 마커를 삭제함. 재생성은 완료 마커 수동 조작 오해 소지가 있어 하지 않음. 영향은 ANU 알림 1회 중복 가능성뿐(무해).

### 최종 저장소 상태 재확인
```
$ git rev-list --count cc7476b..HEAD   → 1
$ git diff --name-only cc7476b..HEAD   → server/tests/test_main.py
$ git status --porcelain               → (clean)
```

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

