# A↔B Integration Gate 계획 (제출물 ⑤)

**상태**: DRAFT — 회장 승인 전. **integration task dispatch 금지.**
**작성**: 아누 2026-07-19 KST
**상위 계약 (canonical = `docs/contracts/` 두 파일뿐. `memory/specs` 는 source of truth 아님)**: `docs/contracts/INSURO_LV4_PROGRAM_CONTRACTS_V1.md` (커밋 `c063f39d743cc3981d171467bf1a0ffc19cf39a3`, SHA-256 `4649531bca9d59d47b20cb5ff482fabcc2f669fd99395c019ebf13cd2d2414f7`)

## ★ 저장소·경로 기준 (모호성 제거)
- **대상 저장소**: **InsuRo** (`/home/jay/projects/InsuRo`, remote `origin`)
- 본 문서의 **모든 상대경로는 InsuRo 저장소 루트 기준**이다. 예: `docs/contracts/…` = `<InsuRo>/docs/contracts/…`
- 계약 PR merge 전에는 해당 파일이 **`main` 에 없다** — 브랜치 `contract/ohmy-ppd-v1` 에만 존재한다. 확인 방법:
  ```bash
  git -C /home/jay/projects/InsuRo show <계약커밋>:docs/contracts/INSURO_LV4_PROGRAM_CONTRACTS_V1.md | sha256sum
  ```
- ⚠️ **`/home/jay/workspace` 는 대상 저장소가 아니다.** 본 지시서 파일 자체가 거기 있을 뿐이며, **경로 해석 기준이 아니다.**

> **이 문서가 규정하는 것**: A(확장)와 B(서버·CRM)를 **언제·무엇을 근거로** 연결해도 되는지.

## ★ 보장 대상 정의 (회장 정정 2026-07-19 — 표현 오류 수정)

**"실고객 데이터가 서버로 가는 코드 경로 0"** 이라는 이전 표현은 **틀렸다. 폐기한다.**
**이미 배포된 구버전 확장은 회수 불가이므로 요청 *시도* 자체는 계속 발생할 수 있다.** 요청 도달을 0으로 만들 수단은 없다.

**따라서 보장 대상은 "요청 0" 이 아니라 아래 3가지다:**

| 보장 항목 | 의미 |
|---|---|
| **accepted write 0** | 요청이 와도 **수용되지 않는다**. 2xx 성공 응답 0 |
| **DB write 0** | 어떤 테이블에도 레코드·부분행 생성 0 |
| **queue / payload log 0** | 큐·재시도 버퍼·임시저장·**원문 로깅** 어디에도 payload 잔존 0 |

**B 의 증명 의무**: 구버전 확장 요청을 **payload 를 저장·큐잉·원문 로깅하지 않고** `FEATURE_DISABLED`(HTTP **403 고정**) 로 **fail-closed** 처리함을 증명한다.
→ **"payload 를 읽어 검증한 뒤 거부"도 불충분**하다. 거부 경로에서 payload 가 **로그·에러객체·큐**에 남으면 위반이다.
→ feature OFF 시 **애플리케이션 모델 deserialize 자체를 하지 않는다**(비즈니스 검증·DB·queue·cache·temp·APM 원문 기록 0).

**★ 전송계층 버퍼는 구분한다**: ASGI/웹서버의 **전송계층 일시 버퍼**는 불가피하며 이를 0 으로 만들라는 요구는 **구현 불가능**이다.
→ 보장·증명 대상은 **"payload 원문이 우리가 관측·보존하는 시스템(DB·queue·cache·temp·로그·APM)에 남지 않음"** 이다.

---

## G-0. 왜 별도 task 인가
A(신버전) 는 **전송 코드 미포함**, B 는 **endpoint default disabled(FEATURE_DISABLED fail-closed)** 로 각각 완료된다.
→ **"실제로 수용되는 write"는 어느 track 에도 없다.** 그것을 여는 유일한 지점이 **integration task** 이며, 여기가 **실고객 데이터가 처음 저장되는 순간**이다. 그래서 분리하고 게이트를 건다.
→ 그 사이 **구버전 확장의 요청 시도는 계속될 수 있으나**, 위 3보장(accepted write 0 / DB 0 / queue·log 0)으로 무해화된다.

---

## G-1. 진입 조건 (전부 충족해야 integration task 착수)

| # | 조건 | 검증 주체·방법 |
|---|---|---|
| 1 | **B merge 완료** (서버측 default-disable 반영) | ANU: merge SHA 확인 + B4 3중 증명 재확인 |
| 2 | **A merge 완료** (extension POST 제거) | ANU: merge SHA 확인 + A7 3중 증명 재확인 · **merge lane 정상 해제 확인**(계약 §P-3-b) |
| 3 | 두 track **전체 테스트 독립 재실행 PASS** | ANU 직접 실행 (봇 self-report 불신) |
| 4 | 두 track **뮤테이션 검증 통과** (차단 되돌리면 실패) | ANU: 결과 확인 |
| 5 | 계약 문서 **SHA-256 무변동** | ANU: 착수 시 대조 |
| 6 | A·B 가 **동일 계약 SHA 기준**임 확인 | ANU: 각 base SHA 대조 |
| 7 | **A4·B2 결정항목 표 STOP-clear** — 실사로 전부 확정되었고 미확정 잔여 0 | ANU: 각 보고서의 결정항목 근거 확인 |
| 8 | **★ B0 실사 선행조건 3건 STOP-clear** — ① `customers` 기준 tenant 소속 검증 가능 ② 신규 저장모델 설계 확정 ③ `FEATURE_DISABLED` 핸들러 진입 전 차단 구조 확정 | ANU: B 보고서 근거 확인 (※ `reference_type` 은 계약이 `CUSTOMER` 단독 확정 — gate 재논의 대상 아님) |
| 9 | **★ `PendingRequestBytesPolicy` 확정** (아래 G-1-a) | ANU: 결정 근거 확인 |
| 10 | **★ 테스트 계정 allowlist 권한 근거 확정** (아래 G-1-b) | ANU: 결정 근거 확인 |
| 11 | **회장 승인** | 명시적 승인 |

### ★ G-1-a. `PendingRequestBytesPolicy` (integration 착수 전 필수 결정)
> `request_body_bytes` 는 **고객 reference 와 보험 설계 결과**를 담는다. **무기한 로컬 보관 금지.**

| 결정할 것 | 비고 |
|---|---|
| **보관 위치** — memory-only vs encrypted session storage | **권장 MVP = 현재 페이지 세션 메모리 전용** |
| **최대 보관 시간** | 명시할 것 |
| **즉시 삭제 시점** | **성공 · 취소 · 계정전환** 시 즉시 폐기 |
| **브라우저 재시작 후 재사용 허용 여부** | **권장 = 불허**(reload·종료 후에는 **새 저장 액션**으로 처리) |

### ★ G-1-b. 테스트 계정 allowlist 권한 근거
- 판단 기준 = **서버 인증 세션의 `fa_account_id`** **단독**.
- ⚠️ **클라이언트의 pilot flag·`rollout_stage` 주장으로 열면 절대 안 된다.** 클라이언트 주장으로 게이트를 열면 파일럿 경계가 무의미해진다.

**하나라도 미충족 → 착수 금지.**

---

## G-2. Integration task 범위 (이것만)

### 포함
1. **명시적 "상담 히스토리에 저장" 버튼** — 사용자가 로컬 미리보기를 **보고 나서** 누르는 단 하나의 액션.
2. **동의 획득 → payload 조립(계약 §2-1) → B ingest 호출.**
   - ★ Track A 산출물 = **`ImmutablePreviewSnapshotV1`**(reference·consent·idempotency_key **미포함**)
   - ★ **integration 이 첫 저장 클릭 때** snapshot + `reference` + `consent` + `idempotency_key` 를 합쳐 **`request_body_bytes` 를 정확히 1회 생성**
   - ★ 재시도 = **그 bytes 그대로 재전송**(재생성·재조립 금지)
3. **저장 버튼 UI 소유** — 버튼 추가·활성/비활성 제어는 **integration 책임**. **A 가 노출한 `INVALIDATED` 상태에서 반드시 잠근다.**
4. **B 의 endpoint 를 테스트 계정 한정으로만 enable** (전체 개방 아님).
5. 실패 시 사용자 안내 + **조용한 재시도 금지**(중복 생성 방지).

### 제외 (절대)
- 조회 즉시 자동 전송 · 동의 없는 저장 · `REQUIRES_REVIEW`/`BLOCKED` 를 정상 결과로 저장
- production 전체 개방 · 실고객 대상 확대 · C(UI) 실데이터 연결

---

## G-3. 게이트 통과 기준 (integration task 완료 판정)

| 축 | 통과 기준 |
|---|---|
| **동의** | 동의 없이 전송되는 경로 **0** (테스트로 증명) |
| **★ external_subject_key** | **V1 범위 밖**(계약 §2-1-a-0). 클라이언트 전송 **거부** · 서버 생성/저장 **0**. 외부 매핑은 `EXTERNAL_SUBJECT_BINDING` task |
| **★ 권한 필드 차단** | 클라이언트가 `fa_account_id` 전송 시 **`CLIENT_AUTHORITY_FIELD_FORBIDDEN`/400·저장 0**(예외 없음) |
| **★ reference shape** | `reference_type`+`reference_id` **단일 쌍**. **V1 허용값 = `"CUSTOMER"` 단독**. 누락·두 종류 동시·`CONSULTATION` 포함 enum 위반 → `REFERENCE_SHAPE_INVALID`/400 |
| **★ V1 미사용 enum** | `TENANT_MAPPING_MISSING`·`EXTERNAL_SUBJECT_MISSING` **반환 0**(발생 경로 없어야 정상) |
| **★ tenant authority** | **`fa_account_id` 단독**(서버 인증세션 파생. **클라이언트 payload 에 없음**). `consultantid` 로 권한·tenant 결정 **0** |
| **★ reference tenant** | **`(reference_type, reference_id)` 조합**이 인증 tenant 소속 검증. 불일치 → **`TENANT_REFERENCE_MISMATCH`** · **저장 0 · 조회 0** |
| **★ 멱등 동작** | 동일키+**동일 payload bytes**(body SHA-256 일치) → **기존 결과 반환·저장 0** / 동일키+**다른 payload bytes**(SHA-256 불일치) → **`IDEMPOTENCY_CONFLICT`(409)·저장 0** |
| **★ FEATURE_DISABLED** | HTTP **403 고정**. deserialize 미실행 · DB·queue·cache·temp 0 · **로그/APM 에 payload 원문 0** |
| **★ 키 구분** | 계정전환 판정은 **`local_owner_observation_key`** 로만(메모리 전용·전송 0·서버 수신/저장 0). **`external_subject_key` 는 V1 미사용** — 생성·저장 0. 미검증 값 HMAC 금지 |
| **★ REQUIRES_REVIEW** | 정상 상담 히스토리에 저장 **금지** → **별도 quarantine/review 상태**. **고객용 UI 노출 건수 0** · **조합 계산 입력 포함 건수 0**(= 조합 계산에서 항상 제외) |
| **★ BLOCKED** | **전송 0 · 저장 0** (클라이언트에서 전송 자체가 차단) |
| **tenant 격리** | 저장 레코드 소유자가 로그인 FA 계정과 100% 일치. 교차 저장 **0** |
| **★ 멱등성** | DB unique scope = **`(fa_account_id, operation_scope, idempotency_key)`**. `reference_*`·`contract_version` 은 **body hash 로 검증** → **같은 key 로 이 값이 바뀌면 반드시 `IDEMPOTENCY_CONFLICT`** |
| **★ 멱등키 소유** | `idempotency_key` 는 **integration 이 저장 클릭 순간 생성**(Track A 생성 금지) |
| **★ bytes 보관 책임** | integration 이 **저장 클릭 시점 raw body bytes 를 보관**하고 재시도 시 **그 원본을 재전송**(재조립 0). 정규화 없음·SHA-256 비교. 재직렬화로 bytes 변하면 `IDEMPOTENCY_CONFLICT` = **의도된 fail-closed** |
| **★ consent 감사** | `granted_at`·`consent_version`·`scope`·`source` 전부 저장. identity 는 payload 주장 아닌 **서버 인증세션** 기록 |
| **금지필드** | 저장된 레코드에 JWT 원문·`consultantid` 원문·API 원본 응답 **0** |
| **계정전환** | owner 변경 후 이전 pending payload **전송 0**. 2단 증명: **① A 가 `INVALIDATED` 상태 노출**(A4) **② integration 이 그 상태에서 저장 버튼 잠금**(integration 소유) |
| **read 경계** | B4 read 2개(`/ohmy-capture-history`)에서 **설계사 간 교차조회 0** (통합 후에도 유지 확인) |
| **롤백 가능성** | **`rollout_stage`(`PILOT`\|`PRODUCTION`, NULL 저장거부) + `rollout_batch_id`(서버 생성·배치별 상이)** 존재 + **인덱스**. 책임 = Track B B3. 없으면 통과 불가 |
| **범위** | **테스트 계정 외 저장 0** |

**뮤테이션**: 위 게이트를 의도적으로 무력화하면 **테스트가 실패**해야 함.

---

## G-4. 파일럿 (게이트 통과 후, 별도 승인)

1. **대상**: 회장님 + 동의한 소수 FA (명시적 목록)
2. **기간·건수 상한** 명시
3. **관측**: `token_source` 분포(★ 다른 FA 는 `ABSENT` 일 수 있음 — 미검증 항목) · 실패 코드 분포 · tenant 오염 0 · 중복 0
   - **★ 최소 표본수 (회장 확정)**: **서로 다른 FA 10명** 또는 **유효 세션 30건** 에 도달하기 전에는 **비율 판정 금지.**
     - 그 전에는 **raw count 와 진입경로별 분포만** 보고한다. "몇 %" 라고 말하지 않는다.
     - **같은 사람의 반복 시도보다 distinct FA / distinct session 기준을 우선**한다(1명이 20번 눌러도 표본 1명).
   - 표본 충족 후 기준: `ABSENT` **30% 초과** → 확대 중단 + 진입경로 재설계 보고 / 10~30% → 경고·원인 분류 / 10% 이하 → 정상.
   - ⚠️ 이 임계값은 **관측 데이터가 없는 상태의 잠정 기준**이며, 파일럿 1차 결과로 재조정한다.
4. **중단 조건(즉시 롤백)**: tenant 오염 1건 · 동의 없는 저장 1건 · 금지필드 저장 1건 · `BLOCKED` 우회 1건
5. 파일럿 결과 보고 → **회장 승인 후에만** 운영 활성화

---

## G-5. 롤백 계획
- **서버측**: **신규 V1 ingest endpoint** 를 **`FEATURE_DISABLED` 로 즉시 복귀**. 1차 차단선. (legacy write 4개는 애초에 계속 disabled)
- **확장측**: 저장 버튼 비활성화 배포.
- **데이터**: 파일럿 저장분은 **`rollout_stage='PILOT'` + `rollout_batch_id=<해당 회차>`** 로 태깅 → **회차 단위 선별 삭제 가능**(필드는 Track B B3).
- ⚠️ **확장은 즉시 회수 불가**(사용자 브라우저에 설치됨) → **서버측 차단이 실질 롤백 수단**임을 전제로 설계.

---

## G-6. 미검증·유보 (정직 기록)
- **다른 FA 의 `token_source`** 가 `URL_QUERY` 인지 **미검증** → 파일럿 1순위 관측 항목
- 반올림 규칙 미확정(화면 셀 값 사용으로 회피 중)
- 기배포 구버전 확장의 실제 write 시도 여부 → **B4 에서 실사 후 확정**
- C(계산엔진·UI) 실데이터 연결은 **본 게이트 이후 별도 계획**
