# task-2801 (TRACK_B) — 서버·CRM 기반 V1 ingest 구현 보고서

- **팀**: dev6-team (페룬/스바로그/벨레스)
- **레벨**: Lv.4 프로그램 하위 Track (게이트 Lv.2 지시)
- **저장소**: InsuRo `/home/jay/projects/InsuRo`
- **브랜치**: `task/task-2801-dev6` (worktree `/home/jay/projects/InsuRo/.worktrees/task-2801-dev6`)
- **base**: `cc7476bfbd9f6b3ae3c99f9a7136cc9f94cd3ada` (= origin/main, 지시서 확정값과 일치)
- **최종 커밋**: `0acbd9107d188c6558b27eafcc72246f759ce44c`
- **범위**: 개발까지만. **PR·merge·배포·실고객 write·production 활성화 전부 미착수**

---

## S (Situation)

InsuRo Lv.4 복합설계 프로그램의 Track B(서버·CRM)로서, 계약 §2 envelope 을 수신하는 **신규 V1 ingest endpoint 1개**를 만들고, 기존 legacy write endpoint 4개를 **default fail-closed** 로 차단해야 했다. Track A(확장) 산출물 없이 mock payload 만으로 전 경로가 독립 검증되어야 한다.

## C (Complication)

1. 기존 `ohmy_capture_history` 모델은 `matrix`/`capture_hash` 중심이라 계약 §2 envelope(`reference_type/id`·`idempotency_key`·`body_sha256`·`consent`·`rollout_*`)을 **수용 불가**.
2. 멱등성 unique scope 가 회장 정정으로 바뀌었고(조회조건 단독 판단 금지), **재직렬화 금지·raw bytes 비교** 규약이 붙어 일반적인 Pydantic 핸들러 패턴을 쓸 수 없다.
3. `FEATURE_DISABLED` 는 "body 를 읽지 않고 403" 이어야 하는데 FastAPI 는 핸들러 진입 전 body 를 파싱한다.
4. 저장소에 `FEATURE_DISABLED` 관례·machine-readable error_code 규약이 **존재하지 않아** 신설이 필요했다.

## Q (Question)

계약을 위반하지 않으면서, Track A 없이 독립 검증 가능하고, 기존 674개 테스트를 회귀시키지 않는 구현이 가능한가?

## A (Answer)

**가능했고 완료했다.** 신규 파일 7개(limit 8 이내), 신규 회귀 **0건**, L1 실서버 검증 통과. 다만 **계약 밖 임의 에러코드 2건을 발견해 제거**했다(아래 "발견 이슈 및 해결").

---

## B0. 착수 전 실사 3건 (선행조건)

| # | 실사 항목 | 결과 | 근거 |
|---|---|---|---|
| 1 | `customers` 기준 tenant 소속 검증 가능 여부 | **가능 (MATCH)** | `customers.agent_id` 컬럼 존재 (`supabase/migrations/20260308182257_*.sql:14-16`), RLS `auth.uid() = agent_id` (:95). 기존 코드도 `server/main.py:7499-7502` 에서 `.eq("agent_id", user_id)` 로 동일 검증 수행 중 |
| 2 | 새 envelope 저장 모델 부재 | **확인 (재사용 불가)** | `ohmy_capture_history` 컬럼 전수 조사 결과 요청 10개 필드(`reference_type/id`·`idempotency_key`·`body_sha256`·`query_condition`·`selected_coverages`·`per_insurer`·`validation`·`consent`·`rollout_stage`·`rollout_batch_id`) **전부 부재** → 신규 테이블 신설 |
| 3 | `FEATURE_DISABLED` 의 deserialize 0 방식 | **① middleware 채택** | 아래 상세 |

**계약 SHA-256 재확인 (착수 시 실행, 정확히 일치)**
```
c7fe00a75606f94de218f27f65f44315bae97ef993be1d7b217ea322febca873  INSURO_LV4_PROGRAM_CONTRACTS_V1.md
fc081a76da0c9213faa7cee1a64d183981b348e42ac788d81466dd9d3e720986  OHMY_PPD_DATA_CONTRACT_V1.md
```
계약 커밋 `bb1c219…` 은 **읽기 전용 참조만** 했고 merge·cherry-pick **0**.

**추가 확인**: `fa_account_id` 라는 명칭은 저장소에 존재하지 않으며, 실제 파생 경로는 `Depends(verify_jwt)` → `_verify_incar_member(payload) -> user_id: str` 이다. 개념은 동일하며 이 값을 `fa_account_id` 로 사용했다.

---

## 산출물 (신규 7 / 수정 2)

**신규 (7 — new_file_limit 8 준수)**
- `server/schemas/consultation_history_v1.py` (217줄) — Pydantic 모델 + 에러 enum
- `server/routes/consultation_history_v1.py` (362줄) — `POST /api/insuro/consultation-history/v1`
- `server/utils/legacy_write_guard.py` (171줄) — pure ASGI 차단 미들웨어
- `supabase/migrations/20260719120000_consultation_history_v1.sql` (127줄) — 정식 배포 경로
- `server/migrations/013_consultation_history_v1.sql` (127줄) — 관례상 미러
- `server/tests/test_consultation_history_v1.py` — B1/B2/B3/B5 검증
- `server/tests/test_legacy_write_disabled.py` — B4 3중 증명

**수정 (2)**
- `server/main.py` — router include(:319) + `app.add_middleware(LegacyWriteGuardMiddleware)`(:385, **최외곽**)
- `server/tests/test_composite_ingest.py` — B4 차단에 따른 flag 활성 경로 전환

**경로 준수**: 전 변경이 `server/**` · `supabase/migrations/**` 내부. 금지 경로(`extension/**`·`src/**`·`package.json`·`tsconfig.json`·`vitest.config.ts`·`docs/contracts/**`·`.github/**`·`.env*`) 수정 **0**.

---

## B1. ingest API (계약 §2 envelope)

신규 V1 endpoint **1개**만 개설: `POST /api/insuro/consultation-history/v1`. legacy write 4개는 `FEATURE_DISABLED` 유지.

**멱등성 — raw bytes 규약 4항목 준수**
```python
raw_body: bytes = await request.body()      # ① 요청 body 전체 raw bytes
body_sha256 = hashlib.sha256(raw_body).hexdigest()   # ③ SHA-256 보관
```
Pydantic 파싱은 **이 이후**에 수행되며, 해시는 재직렬화 값이 아닌 원문 bytes 에서만 계산(② 정규화 0 / ④ 재조립 0).

**DB unique scope** (지시서 확정값 그대로)
```sql
CREATE UNIQUE INDEX idx_consultation_history_v1_idempotency
  ON consultation_history_v1(fa_account_id, operation_scope, idempotency_key);
```
`reference_type`·`reference_id`·`contract_version` 은 unique scope에 **미포함** → body hash 안에서 검증 → 같은 키로 대상이 바뀌면 `IDEMPOTENCY_CONFLICT`.

**거부 규칙**: `fa_account_id` 수신 → `CLIENT_AUTHORITY_FIELD_FORBIDDEN`/400(예외 없음) · `external_subject_key`/`local_owner_observation_key` 수신 → 거부 · `reference_type` V1 허용값 `"CUSTOMER"` 단독(`"CONSULTATION"` 도 `REFERENCE_SHAPE_INVALID`/400).

## B2. 신원·tenant

`fa_account_id` 는 **인증 세션에서만** 파생, payload 수신 시 무조건 거부.
```python
tenant_check = (sb.table("customers").select("id")
    .eq("id", parsed.reference_id).eq("agent_id", fa_account_id).maybe_single().execute())
# 불일치 → TENANT_REFERENCE_MISMATCH / 403 / 저장 0 · 조회 0
```
`external_subject_key` 는 **V1 범위 밖** — 생성·저장·매핑 **미구현**(§2-1-a-0 준수). 미검증 값 HMAC 생성 **0**.

#### B2 결정항목 (실사 후 확정)
| # | 결정 | 확정 내용 |
|---|---|---|
| 1 | live-write 차단 메커니즘 | **pure ASGI middleware** + env flag `OHMY_LEGACY_WRITE_ENABLED` default-OFF·fail-closed |
| 2 | live write 간주 범위 | 재실사로 확인된 **write 계열 4개**(POST) |
| 3 | 기배포 구버전 확장 가정 | **회수 불가 전제** — 요청 도달은 계속 발생 가능. 따라서 "요청 0" 이 아니라 accepted write 0 / DB write 0 / payload 잔존 0 을 증명 대상으로 삼음 |
| 4 | 차단 성립 증거 | 정적+동적+뮤테이션 3중 + **L1 실서버 curl 실증** (아래) |

## B3. 데이터 모델

`consultation_history_v1` 신규 테이블. 조회조건·선택담보+가입금액·보험사별 결과·`validation`·동의 4필드·감사 로그(`created_at`/`updated_at`/`deleted_at`) 포함. **원문(JWT·consultantid·API 원본) 컬럼 0.**

- **`REQUIRES_REVIEW`**: `record_status` 로 quarantine 격리. `consultation_history_v1_ok_only` DB 뷰 + `build_customer_visible_query()` 헬퍼로 **쿼리 레벨 강제**(고객 UI 노출 0 · 조합 계산 입력 0).
- **`BLOCKED`**: Pydantic 허용값을 `OK`/`REQUIRES_REVIEW` 2종으로 제한 → 스키마 검증에서 거부, **DB 접근 이전 차단 = 저장 0**.

**롤백 식별 2종**
```sql
rollout_stage TEXT NOT NULL CHECK (rollout_stage IN ('PILOT','PRODUCTION')),
rollout_batch_id TEXT NOT NULL,
CREATE INDEX ... (rollout_stage);  CREATE INDEX ... (rollout_batch_id);
```
둘 다 **서버가 저장 시점 생성**(클라이언트 입력 0), NOT NULL, 인덱스 존재.

## B4. legacy write 차단 — 3중 증명 + 재실사

**재실사 결과 (지시서와 정확히 일치)**
```
7225 POST /api/insuro/composite-design/ingest
7403 POST /api/insuro/ohmy-capture
7612 GET  /api/insuro/ohmy-capture-history
7667 GET  /api/insuro/ohmy-capture-history/{capture_id}
7727 POST /api/insuro/ohmy-capture/{capture_id}/link
7858 POST /api/insuro/ohmy-capture-history/{capture_id}/reassign
```

**채택 방식 = ① middleware (body 처리 전 403)**. `BaseHTTPMiddleware` 가 아닌 **pure ASGI** 로 구현해 차단 시 `receive()` 를 **한 번도 호출하지 않는다** → body 가 애플리케이션 계층에 도달하지 않음이 구조적으로 성립. 기존 핸들러 무수정이라 회귀 위험도 최소.

미들웨어 스택 실측(최외곽 우선): `['LegacyWriteGuardMiddleware', 'BaseHTTPMiddleware', 'CORSMiddleware']` → 우리 가드가 **최외곽**.

**증명 3중**
1. **정적** — `BLOCKED_WRITE_ROUTES` 가 write4 정확히 커버, `is_blocked()` 가 read2 에 False (fullmatch + method 검사로 `/ohmy-capture` vs `/ohmy-capture-history` 오탐 방지)
2. **동적** — 403 + `error_code: FEATURE_DISABLED`, insert/upsert/update 호출 **0회**, receive spy 미호출, caplog 마커 미포함, flag 8종 fail-closed, 503/404 미반환
3. **뮤테이션** — `_legacy_write_enabled` 를 True 로 monkeypatch 하면 403 이 **사라짐**(가드가 실제 동작 중임을 역증명) + 대조군 통과

**보장 3가지**: accepted write 0(2xx 0건) / DB write 0 / queue·payload log 0.

**read2 tenant 경계**: `.eq("user_id", user_id)` 로 항상 인증 사용자 스코프. 다만 **MagicMock 은 실제 필터링을 수행하지 않으므로 DB/RLS 레벨 실증은 unit 범위 밖** — 테스트 파일에 한계를 주석 명시(억지 통과 없음). → **UNKNOWN 으로 보고**.

## B5. 실패 enum

5종 구현: `FEATURE_DISABLED`403 · `IDEMPOTENCY_CONFLICT`409 · `TENANT_REFERENCE_MISMATCH`403 · `CLIENT_AUTHORITY_FIELD_FORBIDDEN`400 · `REFERENCE_SHAPE_INVALID`400.
`TENANT_MAPPING_MISSING`/`EXTERNAL_SUBJECT_MISSING` 은 `V1_RESERVED_UNUSED_ERROR_CODES` 상수로만 예약, **어떤 경로에서도 반환 0**(테스트로 assert).

---

## 발견 이슈 및 해결

| # | 이슈 | 심각도 | 해결 |
|---|---|---|---|
| 1 | 팀원이 **계약 밖 에러코드 `BLOCKED_STATUS_NOT_ACCEPTED`(400) 신설** | **High (계약 위반)** | 팀장이 계약 §4-3-a 원문 대조로 적발. `overall_status` 를 `Literal["OK","REQUIRES_REVIEW"]` 로 제한해 스키마 단계 거부로 전환. grep 0건 확인 |
| 2 | 동일 유형 **`CONTRACT_VERSION_UNSUPPORTED`(400)** | **High (계약 위반)** | 동일 원칙 적용 — `contract_version: Literal["OHMY_PPD_V1"]`(계약 §2-1:49행 값 직접 grep 확인). 프로덕션 코드 grep 0건 |
| 3 | B4 미들웨어로 `test_composite_ingest::test_ingest_returns_ok` 회귀(403) | Medium | "403 기대"로 바꾸지 않고 **flag 활성 경로로 전환** — 원래 핸들러 커버리지 보존 + flag enable 경로 동시 증명 |
| 4 | 팀원 보고의 **회귀 원인 귀속 오류** (신규 실패를 1건으로 보고했으나 실측 6건) | Medium | 팀장이 기준선 독립 재실행으로 전수 대조 → gdrive 2건=worktree `.env` 존재, CostCircuitBreaker 3건=기준선 공통 flaky 로 판별 |
| 5 | migration 미러 2파일이 헤더 주석 1줄만 상이 | Low | 테스트가 1행 제외 비교하도록 이미 구현됨(확인 완료) |

**미해결 (범위 외 사유 명시)**
- `VALIDATION_ERROR`/`MALFORMED_JSON_BODY` 는 계약 enum 밖이나 **도메인 실패 코드가 아닌 프레임워크 레벨 malformed-body 처리(422)** 이므로 유지 판단. 어떤 HTTP API 든 필요한 계층이며 §4 enum 의 규율 대상이 아니라고 해석했다. **이 해석은 ANU 재검토 대상으로 남긴다.**
- 지시서 문구 "동일 키인데 `reference_type`/`contract_version` 변경 → `IDEMPOTENCY_CONFLICT`" 는 V1 에서 **도달 불가 경로**다. 두 필드가 단일 허용값이라 멱등 조회 이전에 스키마 단계에서 먼저 거부된다. **저장 0 안전 속성은 동일하게 보장**되나 스펙-구현 불일치이므로 보고한다. (`reference_id` 변경 → `IDEMPOTENCY_CONFLICT` 는 정상 도달·검증됨)

---

## 테스트 결과 (팀장 독립 재실행)

`.env` 유무를 통제해 기준선과 **동일 조건**으로 대조(worktree 는 `.env` 가 복사되어 있어 gdrive 테스트를 오염시키므로, 별도 clean worktree 로 검증).

| 조건 | failed | passed |
|---|---|---|
| 기준선 `cc7476b` (run1) | 17 | 657 |
| 기준선 `cc7476b` (run2) | 20 | 654 |
| **task-2801 최종** | **17** | **725** |

**실패 집합 차집합 (결정적 판정)**
```
우리에게만 있는 실패 (신규 회귀): (없음)
기준선에만 있는 실패            : CostCircuitBreaker × 3   ← 양쪽 공통 flaky
```
→ **신규 회귀 0건. 신규 통과 +68건.**

잔존 17건은 전부 기준선에서도 실패하는 pre-existing (`test_security_patch` 11건은 타 task worktree 하드코딩 경로 참조, `test_keyword_pool_refresh` 4건, `test_main` 2건). CostCircuitBreaker 3건은 **양쪽에서 실행마다 흔들리는 flaky** 로 실증 확인(기준선 run2 실패·우리 run 통과).

**goal_assertion `python3 -m pytest server/tests -q` 실행 완료.** 신규 테스트 함수 71개.

---

## ★ L1 스모크테스트 결과

**pytest PASS ≠ 실동작**이므로 실제 uvicorn 서버를 기동해 curl 로 검증했다.

- **서버 재시작**: **성공** (`uvicorn main:app` 127.0.0.1:8791 / 8792, `Application startup complete`)
- **API 응답 확인**: **성공** — 아래 curl 실측
  ```
  [flag OFF]
  POST /api/insuro/composite-design/ingest            -> 403 {"error_code":"FEATURE_DISABLED"}
  POST /api/insuro/ohmy-capture                       -> 403 {"error_code":"FEATURE_DISABLED"}
  POST /api/insuro/ohmy-capture/abc123/link           -> 403 {"error_code":"FEATURE_DISABLED"}
  POST /api/insuro/ohmy-capture-history/abc123/reassign -> 403 {"error_code":"FEATURE_DISABLED"}
  GET  /api/insuro/ohmy-capture-history               -> 401  (차단 대상 아님, 인증계층 도달)
  GET  /api/insuro/ohmy-capture-history/abc123        -> 401  (동일)
  POST /api/insuro/consultation-history/v1            -> 403 FEATURE_DISABLED (default-OFF)

  [V1 flag ON]
  POST /api/insuro/consultation-history/v1            -> 401 "Missing or invalid authorization"
       → FEATURE_DISABLED 통과 = 라우트 실제 도달·인증 강제 증명
  POST /api/insuro/ohmy-capture                       -> 403 여전히 차단 (독립 flag 증명)
  ```
- **payload 원문 로깅 0 실증**: 고유 마커 `L1_MARKER_MUST_NOT_BE_LOGGED_7c4e` 를 body 에 담아 차단 요청 → 서버 로그 내 **출현 0회**. 로그에는 경로·메서드만 기록:
  `legacy_write_guard: blocked request method=POST path=/api/insuro/composite-design/ingest`
- **OpenAPI 등록 확인**: `/api/insuro/consultation-history/v1 ['post']`
- **스크린샷**: 해당없음 (서버 전용 백엔드 task, 프론트 변경 0)
- **브라우저 정리**: 해당없음 (Playwright 미사용). uvicorn 프로세스는 종료 확인(잔존 0), 임시 worktree 2개 제거 완료.

---

## 절대 제약 준수 확인

- 실고객 데이터 write **0** — 전 테스트 MagicMock, L1 은 403/401 거부 경로만 호출(성공 write 0건)
- production endpoint 활성화 **0** — V1·legacy 양쪽 default-OFF
- Track A 없이 **독립 검증 성립** — extension 산출물 의존 테스트 0
- PR·merge·배포 **미착수** (지시서상 미승인)
- 계약 파일 수정 **0**, 계약 커밋 merge/cherry-pick **0**

---

## 게이트

- **G1 설계**: 통과. affected_files 전부 `server/**`·`supabase/migrations/**`. Track A(task-2800, `extension/background.js`·`content.js` 2파일)와 **중첩 0** 실측. migration serial_only 충돌 **0**(진행 중 타 worktree 4곳 migration 변경 0 확인).
- **G2 구현**: 통과. 팀 테스터(벨레스) 기능 테스트 71개 + 팀장 독립 재실행 + 뮤테이션 검증.
- **G3 머지**: **미수행 — 지시서상 PR·merge 회장 미승인**. `--action pr` 실행하지 않음.

## 머지 판단
- **머지 필요**: **No (현 시점)** — 회장 승인 게이트. 계약 PR merge → B first merge 순서가 예정되어 있으나 지금은 브랜치 커밋까지가 범위.
- **브랜치**: `task/task-2801-dev6`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2801-dev6`
- **머지 의견**: 신규 회귀 0, L1 실서버 검증 통과, 신규 파일 7개로 기존 코드 침습 최소(main.py 3줄 추가). 머지 자체의 기술적 위험은 낮다. **단, 머지 시 legacy write 4개가 즉시 default 차단**되므로 이를 호출하는 기배포 구버전 확장이 403 을 받게 된다 — 이는 계약이 의도한 동작이나 **운영 영향이 있는 변경이므로 회장 승인이 필수**다.

## MATCH / GAP / UNKNOWN

- **MATCH**: 계약 SHA 2건 · B4 write4/read2 재실사 6경로 · base commit · tenant 검증 가능성 · 멱등 unique scope · rollout 2필드 · 에러 5종 매핑 · FEATURE_DISABLED 403 고정 · deserialize 0 · payload 로깅 0
- **GAP**: 기존 `ohmy_capture_history` 재사용 불가(신규 테이블로 해소) · 저장소에 error_code 규약 부재(신설) · `fa_account_id` 명칭 부재(`user_id` 로 대응) · 지시서의 `reference_type`/`contract_version` 멱등 충돌 경로 도달 불가
- **UNKNOWN**: read2 tenant 경계의 **DB/RLS 레벨 실증**(unit mock 범위 밖 — 실 DB 통합테스트 필요) · `VALIDATION_ERROR`/`MALFORMED_JSON_BODY` 유지 판단의 계약 적합성(ANU 재검토 요청)

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 스바로그A | 백엔드(코어) | sonnet | 실사·구현·2회 계약 수정 |
| 스바로그B | 백엔드(미들웨어) | sonnet | ASGI guard |
| 벨레스 | 테스터 | sonnet | 검증 71개 + 회귀 수정 |
| 페룬(팀장) | 설계·검토·통합 | opus | main.py 통합 3줄, 계약 위반 2건 적발, 독립 재검증 |

haiku 미사용(전 작업이 계약 해석·설계 판단을 요구). 라다(프론트)·모코시(UX)는 **본 task 가 서버 전용이라 미소집**. 디자인 작업 없음 → 디자인팀 호출 불필요.

---

## ★ 완료 차단 (.done 미생성) — ANU 판단 요청

`finish-task.sh` 는 QC 전 항목 통과 후 **GOAL-GATE 에서 fail-closed 차단**되었다. `.done` 을 **수동 생성하지 않았다**(금지 규정 준수).

```
[GOAL-GATE] FAIL: python3 -m pytest server/tests -q
[GOAL-GATE] BLOCKED: goal_assertions FAIL (fail-closed)
```

### 원인 분석 (3가지 단서 전부를 설명하는 가설)

goal_assertion `python3 -m pytest server/tests -q` 는 **Track B 작업과 무관하게 불충족**이다. 근거 3건:

1. **기준선에서 이미 실패** — 지시서가 확정한 base `cc7476b` 를 깨끗한 worktree 에 체크아웃해 충분한 시간(600s)으로 실행한 결과 **exit code = 1**. 즉 **Track B 착수 이전부터** 이 assertion 은 통과 불가였다. (기준선 17건 실패: `test_security_patch` 11 · `test_keyword_pool_refresh` 4 · `test_main` 2)
2. **게이트 실행 경로 불일치** — `goal_assertion_exec_isolated` 는 `bash -c "$cmd"` 를 **호출 시점 cwd** 에서 실행한다. finish-task.sh 는 `/home/jay/workspace` 에서 돌았고 **거기엔 `server/tests` 가 존재하지 않는다**(InsuRo 저장소 상대경로이므로). 실측 확인.
3. **타임아웃 부족** — `GOAL_CMD_TIMEOUT` 기본 30초. 올바른 디렉토리에서 실행해도 전체 스위트는 **약 120초** 소요 → `rc=124` TIMEOUT fail-closed. 실측 확인.

→ 세 단서 모두 "내 변경이 테스트를 깨뜨렸다"로는 설명되지 않는다. 실제로 **신규 회귀는 0건**이며(기준선 대비 실패 집합 차집합 공집합), 오히려 **신규 통과 +68건**이다.

### ANU 에게 요청하는 판단

- 본 차단은 **Track B 산출물의 결함이 아니다.** 지시서의 `goal_assertions` 가 저장소의 **기존 깨진 테스트 17건** 때문에 원천적으로 충족 불가하며, 여기에 게이트의 cwd·timeout 문제가 겹쳤다.
- 선택지: (a) goal_assertion 을 신규 테스트 범위로 한정(예: `pytest server/tests/test_consultation_history_v1.py server/tests/test_legacy_write_disabled.py server/tests/test_composite_ingest.py -q` → **74 passed, failed 0** 실측) (b) 기존 17건 수리를 별도 task 로 분리 (c) 회장 판단으로 예외 승인
- **어느 경우든 `.done` 수동 생성은 하지 않았다.** 산출물(커밋 7개·보고서·result.json)은 전부 남겼다.

### 현재 마커 상태
- `.done` — **미생성** (GOAL-GATE 차단)
- `.qc-result` — 생성 (`qc_result: WARN`, FAIL 0)
- `.finalize-only` — 생성 (merge_policy=none 준수, **merge 미실행**)
- `.merge-done` — **미생성** (정상 — 회장 미승인)
- `main` 브랜치 — `cc7476b` 그대로, **오염 0**
