# notes: 삼성화재 계정 인테이크 M1

계획서 `.claude/plans/samsung-account-intake-m1.plan.md` 의 구현 창 기록.
**리뷰 절을 계획서 본문이 아니라 여기에 둔다** — 계획서를 고치면 `plan_hash` 가 바뀌어
`mccp-plan-codex` receipt 가 stale 이 되고 `/mccp:pr` 이 막힌다.

## Codex Implementation Review

- 호출: `node …/scripts/lib/codex-invoke.js adversarial-review` (fail-closed Bash wrapper, v0.2.2)
- **> Codex skipped per MCCP_CODEX_DISABLED=1** — 봉인된 운영자 정책(`codex-policy.json`
  `codex_disabled=true`). wrapper 가 `{"classification":"disabled","blocking":false,"durationMs":1}`
  로 short-circuit 했다. spawn 없음. 라운드 원장 `mccp-implement-codex__samsung-account-intake-m1`
  은 cap=1 / rounds=0 으로 봉인됐고 이 창에서 소진하지 않았다.
- 라운드 수: 0 (Codex), 1 (security-reviewer)
- 합치 결론: **Codex 교차확증 없음.** 계획 게이트의 `mccp-plan-codex` 도 divergent(미수렴)였고
  이 창도 Codex 를 부르지 못했다. 이 마일스톤은 **교차 모델 확증을 한 번도 받지 못했다.**
  `MCCP_SKIP_INTENT_GATE` 감사 우회로 구현만 열었다(사용자 명시 지시).

### 구현 시점 신규 결정 (계획서가 미리 못 박지 않은 것)

1. 미복원 사유 분류를 `resign()` 안에 인라인 — 별도 helper 로 빼지 않는다
2. `restoreSubscribe` 를 3번째 인자 `opts` 객체로 (DTO 필드 아님) — 계획서가 못 박음
3. `useAt=0` 의 `subId == null` 행을 unsubscribe 루프에서 제외하고 사유로 재분류
4. `baseline.json` 필드 형상은 `gates.mjs baseline` 서브커맨드가 이미 동결
5. `ALLOWED_OPTIN_CALLERS` 는 M1 에서 빈 배열

### Security Reviewer

`mccp:security-reviewer` 를 `review proposed implementation: privilege escalation /
authorization boundary / billing-state mutation reachable by a lower-privileged role` 로 호출.
**CRITICAL·HIGH 0건.** 계획서의 위협 모델 주장 3건이 코드 증거로 확증됐다.

| Q | 주장 | 판정 | 증거 |
|---|---|---|---|
| Q1 | corp 가 `opts.restoreSubscribe=true` 를 켤 수 없다 | **확증** | `.resign(` 3곳 전건 2인자. 컨트롤러 `:459` `resign(body, req['admin'])`, `accountUpdateUpload` `:1060`·`:1063` 은 서비스가 만든 객체 리터럴에 `useAt` 하드코딩. 서비스 메서드는 Nest 메타데이터 바인딩 대상이 아니라 3번째 위치인자를 요청이 채울 기제가 **구조적으로** 없다 |
| Q2 | 전역 파이프가 두 번째 겹이다 | **확증** | `validation.pipe.ts:13-21` `whitelist:true`·`forbidNonWhitelisted:true`, `app.module.ts:188` `APP_PIPE` 루트 등록. `ResignAgencyFcDto` `:125-145` 는 전 필드 데코레이트 — 무데코 DTO 예외 경로가 아니다. 본문에 `restoreSubscribe` 를 실으면 **400** |
| Q3 | `useAt=1` 해지 제거가 가드를 약화시키지 않는다 | **확증** | `assertSeatLowerBound` 는 `:804` 에서 **이미** `useAt===0` 조건부라 `useAt=1` 에 원래 안 걸린다. `subscribeService.unsubscribe` `:422-437` 는 자체 인가·seat 판정이 0건인 순수 상태변경 — 제거로 사라지는 검사가 없다 |
| Q5 | select 두 칸 추가가 기존 소비처로 새지 않는다 | **확증** | 소비처 3곳 전건 값을 HTTP 로 직렬화하지 않는다(컨트롤러 `:459`·`:478-490` 이 반환값 버림). `sub_disable_completed_at` 은 이미 엑셀·`payment.dto.ts:32`·`refund.dto.ts:12` 로 admin/corp 에 노출된 칸이라 새 노출 부류가 아니다 |

### YAGNI Triage

| Finding | Severity | Verdict | Why |
|---|---|---|---|
| F1 `assertMembersWritableByAdmin` 을 `useAt` 분기 **밖**에 유지 — 이것이 실제 테넌트 경계 | HIGH | ACCEPT_NOW | 계획서 설계와 일치. 구현 시 `:802` 위치를 그대로 둔다. 어기면 corp 가 남의 테넌트 회원을 만진다 |
| F2 seat **상한** 판정이 저장소 전역에 0건 — 되살리기가 정원 초과를 만들 수 있다 | MEDIUM | DEFER_TO_BACKLOG | 기존 결함이고 이 diff 가 키우지 않는다. M1 에 `restoreSubscribe=true` 호출자가 **0** 이라 도달 불가(관측 13·§NOT Building). 이미 있는 subscribe/upload 되살리기 경로도 같은 구멍을 갖는다 — 이 창에서 고치면 범위가 그 경로까지 번진다 |
| F3 `opts` 타입을 `ResignAgencyFcDto` 로 구조적 만족 불가하게(브랜드 타입) | LOW | REJECT_YAGNI | 세 번째 겹이 필요 없다. 1) 게이트 `#9` 가 `apps` 전역 `.resign(` 를 전수로 훑어 3인자 호출을 이름으로 낸다 — 기계 검사가 이미 있다. 2) `{restoreSubscribe?: boolean}` 은 전 필드 optional 인 **weak type** 이라 공통 속성이 없는 `ResignAgencyFcDto` 를 넘기면 TS 가 이미 거부한다. **이 2)는 주장이므로 구현 중 `tsc` 로 실측해 아래 「반사실·실측」에 적는다** |
| F4 `skipped[].memId` 로그 위생 | LOW | REJECT_YAGNI | `memId` 는 내부 PK 이고 `assertMembersWritableByAdmin` 이 corp 의 `dto.ids` 를 자기 테넌트로 이미 좁힌다 — 교차 테넌트 열거가 안 열린다. M1 에 반환값 소비처가 0 이라 로그 자리 자체가 없다(관측 19) |

- Deferred to backlog: 1건 → `.claude/plans/codex-findings-backlog.md`
- **Open Questions**: 계획 게이트가 divergent 로 닫혔고 이 창도 Codex 를 못 불렀다 —
  교차 모델 확증 부재 · severity HIGH · `DIVERGENT_UNRESOLVED`
- Codex session 참조: 없음 (미발화)

### Design Review

`> impeccable silent-skip: no-signal` — `impeccable-detect --mode implement` 가
`skill_available=1 design_signal=0 silent_skip=1 reason=no-signal`. 이 마일스톤은 서버 서비스·
저장소·spec 만 건드리는 백엔드 전용 변경이라 렌더 표면이 0이다. Phase 3.6·3.7 은 no-op.

## 반사실·실측

흡수·구현을 주장으로 남기지 않는다(CLAUDE.md §4). 수정 전/후로 검사를 돌려 결과가 뒤집히는지 확인한 기록.

| # | 반사실 조작 | 기대 | 관측 |
|---|---|---|---|
| `#7` | select 에서 `memId`·`subDisableCompletedAt` 두 칸 제거 | FAIL | `#7 FAIL: select 에 memId 부재` rc=1 → 복원 시 `#7 ok` rc=0 |
| `#4` | `skipped.push` 의 `'expired'` 를 주석으로만 이동 | FAIL | `#4 FAIL: 사유 어휘가 코드(주석 제외)에 없다 — expired` rc=1 → 복원 시 rc=0 |
| `#9` | `accountUpdateUpload` 의 `useIds` 호출에 3번째 인자 `{restoreSubscribe:true}` 추가 | FAIL | `#9 FAIL: 허용 목록 밖 opt-in 전달 1곳 — api/src/modules/fc/agency/fc-agency.service.ts:if` rc=1 |
| `#9` | `ResignAgencyFcDto` 에 `restoreSubscribe?: boolean` 추가 | FAIL | `#9 FAIL: DTO 에 restoreSubscribe 가 노출됐다 — corp 가 본문으로 켤 수 있다` rc=1 |
| `#10` | 핀(`4d798f55`)의 서비스 사본으로 같은 spec 실행 | 붉음 6 · 초록 1 | 정확히 그 분포. 초록은 `stop: useAt=0 은 전건 해지한다` 하나 |
| F3 | 컨트롤러에서 `resign(body, req['admin'], body)` | 컴파일 거부 | `agency-fc-admin.controller.ts(459,59): error TS2559: Type 'ResignAgencyFcDto' has no properties in common with type '{ restoreSubscribe?: boolean | undefined; }'` |

### 구현 중 발견한 게이트 결함 2건 (둘 다 층위 2 — 검사가 스스로를 못 falsify)

1. **`gates.mjs` `stripComments` 가 코드를 삼켰다.** 블록 주석을 먼저 지우는데 이 저장소의 관례
   주석 `//*` 안에도 `/*` 가 있어, 그 지점부터 다음 `*/` 까지가 블록으로 먹혔다. 실측:
   `fc-agency.service.ts` 81,701자 중 **36,715자(45%)** 소실 → 사유 어휘 **0/4**, `.resign(` **3→2**.
   `#4` 가 이 때문에 붉게 떨어졌고, 같은 함수를 쓰는 `#9` 는 **호출처 하나를 못 보고 있었다**(사각지대).
   행 주석을 먼저 지우도록 순서를 바꿔 5,227자만 소실 · 4/4 · 3곳으로 정정. `#9` 출력이 `2곳`→`3곳`.
2. **내가 넣은 주석과 타입이 검사를 무력화했다.** 1) select 주석에 `memId:` 를 적어 `#7` 이
   주석만으로 통과했다(`select-shape` 는 주석을 안 걷는다) — 문구 수정. 2) `skipped` 의 `reason` 을
   리터럴 union 으로 좁히자 사유 4종이 **타입 선언**에 남아 `#4` 반사실이 통과했다 — union 을 걷고
   `reason: string` 으로. 오타는 spec 의 `toEqual` 이 잡는다.

## 게이트 완주 출력

`.claude/plans/samsung-account-intake-m1.plan.md` §Validation 블록을 계획서에서 **추출해** 1회 완주
(손으로 옮겨 적지 않았다). 전체 rc=0. 아래는 구현 후 코드 리뷰 흡수까지 반영한 **최종** 실행이다.

```
#1 ok  pin=4d798f55 · base=a5faba63 · jest{suites:4,tests:27} tsc{files:4}
#2 ok  failing==baseline (4스위트/27테스트) · 신규 붉음 0
#3 ok  restore 축 1건 통과
#4 ok  미복원 사유 5건 통과
#4 ok  사유 어휘 5종이 skipped.push 인자에 실재
#5 ok  stop 축 2건 통과
#5 ok  fc/subscribe 5스위트(기준선 4+1) 초록
#6 ok  tsc 에러 파일 4건 전건 baseline 안 · fc/agency·subscribe 0건
#7 ok  s.<col> 인용 6종 ⊆ 서브쿼리 칼럼 7종 · select 에 memId·subDisableCompletedAt 실재
#8 ok  기준 커밋 대비 변경 7경로 전건 표 안(표 9행) · 브랜치 신규 첨부 의심 0건 · PRD 1행 status=in-progress
#9 ok  DTO 에 restoreSubscribe 부재 · apps 전역 .resign( 3곳 전건 2인자 이하 · 허용 호출처 0곳
#10 ok  수정 전 서비스로 붉음 7건(판별) · 초록 1건(중지 경로 불변)
#11 ok  사각지대 7축 — drizzle 스텁 — 저장소 질의가 jest 에서 실행되지 않는다 / 스텁 test 가 select 형상을 관측하지 못한다 / 라이브 DB 미대조 / 반환값이 M1 에서 소비되지 않는다 / 부모 저장소의 미스테이징 신규 파일은 스테이징 전까지 안 보인다 / baseline.json 변조는 게이트가 막지 않는다 / 서브모듈 push 미수행 · §NOT Building 이름 대조 7/7
```

- `#8` 의 `변경 7경로` = `gates.mjs` · 이 notes · `codex-findings-backlog.md` ·
  `baseline.json`(스테이징) + 서브모듈 3파일(repository · service · 신규 spec). 이 커밋이 담는 집합과
  같다. implement receipt 와 PRPs 리포트는 부모 미추적이라 `#8` 범위 밖이다(설계된 사각지대,
  `#11` 5번째 축) — 아래 §이 창에서 커밋하지 않는 미추적 산출물 참조.
- `#5` 의 `5스위트` 는 리터럴이 아니라 기준선 4 + 신규 1 유도값이다.

## 구현 후 코드 리뷰 흡수 (`/mccp:code-review` 로컬 모드 · 2026-09-16)

계획서에 붙이지 않는다 — `plan_hash` 가 바뀌면 plan-codex receipt 가 stale 이 된다.
층위는 CLAUDE.md §2 체크리스트 번호.

| # | 심각도 | 층위 | 지적 | 판정 |
|---|---|---|---|---|
| 1 | CRITICAL | 8 | 평문 비밀번호·PII 파일 5건(xlsx 2 · tsv 2 · png 1)이 워크트리에 미추적이고 `.gitignore` 에도 없다. `git check-ignore` rc=1 ×3, TSV 헤더 `로그인아이디|회원명|비밀번호|연락처`, 158행·7,788행. Task 1 이 "착수 전 미추적 산출물을 커밋하라" 고 지시해 `git add -A` 가 자연스러운 다음 동작인데 그 경로에서는 게이트가 안 돈다 | ABSORBED — 5건 전부 저장소 **밖** `~/work/samsung-intake-attachments/`(`chmod 700`)으로 이동. 이동 후 미추적은 mccp 산출물 3건뿐. `.gitignore` 등재는 **못 했다** — §Files to Change 9행 밖이라 `#8` 이 거부하고 계획서를 고치면 `plan_hash` 가 깨진다. 재발 방지는 별도 건 |
| 2 | HIGH | 7 | `#9` 봉인의 허용목록 id 가 호출처를 식별하지 못한다. 400자 역방향 정규식이 `if (…) {` 를 물어 두 호출처 모두 `fc-agency.service.ts:if` 를 냈다(실측). M5 가 그 id 를 올리면 같은 파일의 **모든** `if` 블록 안 3인자 `.resign(` 이 함께 열린다 — 봉인이 겨냥한 corp 도달 경로가 통째로 | ABSORBED — `enclosingFn()` 신설(브레이스 역방향 스캔 + 머리 형태 검증 + 블록 키워드 배제). 반사실: 수정 전 `fn=if` ×2 → 수정 후 `fn=accountUpdateUpload` ×2. 3인자를 주입하면 `#9 FAIL: … :accountUpdateUpload` rc=1(실행 확인). 이름을 못 뽑으면(`?`) 그 자리에서 `fail()` — fail-closed |
| 3 | MEDIUM | 7 | `skipped` 의 `reason` 을 게이트 편의로 `string` 까지 넓혔다. 고칠 자리는 코드가 아니라 `#4 vocab` 이다 | ABSORBED — `vocab` 이 파일 전체가 아니라 `skipped.push(` **인자 원문**만 보도록(`callArgs` 재사용), 코드는 리터럴 union 5종으로 복원. 반사실: `'expired'` 를 push 에서 빼고 union 선언에만 남기면 구판은 통과(구멍), 신판은 `#4 FAIL: … expired` rc=1(실행 확인) |
| 4 | MEDIUM | 6 | `dto.ids` 중 `tn_member` 에 행이 없는 id 가 `applied`·`skipped` 어디에도 안 남는다. `assertMembersWritableByAdmin` 은 `role!=='corp'` 면 즉시 return 이라 내부 관리자 경로에서 안 걸러진다 | ABSORBED — 사유 `'not-found'` 신설 + 루프 뒤 미조회 id 보정. spec it 1건 추가, `REASONS` 5종, `#10` 판별 목록에도 등재(붉음 6→7) |
| 5 | MEDIUM | 1 | 서브모듈 변경이 미커밋이라 지금 부모를 커밋하면 코드가 하나도 안 실린다 | ABSORBED — 서브모듈에서 먼저 커밋. 포인터 bump 는 `#1`(핀 동결)·`#8`(창 범위)이 이 창 밖으로 설계한 축이라 **다음 창**이고, push 는 사람 스위치(CLAUDE.md §4) |
| 6 | LOW | 2 | notes 의 완주 기록이 이미 낡았다(`4경로` → 실제 6, `rc=2` → 실제 1) | ABSORBED — 이 절과 위 펜스를 최종 실행 출력으로 재생성 |
| 7 | LOW | 1 | `sql<number>`s.sub_id`` 가 leftJoin 의 null 을 감춘다 — `resign` 의 null 가드가 타입상 죽은 코드로 보인다 | **부분 흡수** — 정직한 선언으로 고치자 `fc-agency.service.ts(1533,9): TS2322` 이 떴다. 두 번째 소비처 `processUnsubscription` 이 null 을 무가드로 넘기는 **같은 유형의 잠복 결함**이고, 고치려면 `handleFailedUnsubscriptions` 의 인덱스 결합까지 손대야 해 이 창 밖이다. 선언은 되돌리고 `resign` 안에서만 지역 주석으로 좁혔다(양쪽에 사유 주석). backlog `samsung-m1-subid-nullable-lie` 로 이월 |

### 이 흡수가 계획서 `# 기대:` 문면을 낡게 만든 자리 3곳

계획서는 `plan_hash` 결박이라 고칠 수 없다. 숨기지 않고 여기 적는다(CLAUDE.md §1).

| 검사 | 계획서 `# 기대:` | 실제 출력 |
|---|---|---|
| `#4` | `미복원 4종 4건 통과` · `사유 어휘 4종이 주석 밖 코드에 실재` | `미복원 사유 5건 통과` · `사유 어휘 5종이 skipped.push 인자에 실재` |
| `#8` | (계수 무기입) | `변경 6경로 … 표 9행` |
| `#10` | `붉음 6건(판별) · 초록 1건` | `붉음 7건(판별) · 초록 1건` |

라벨 `미복원 4종` 의 리터럴 계수는 이번에 `미복원 사유` 로 걷었다 — 계수는 `want.length` 에서만 나온다(INV4).

### 이 창에서 커밋하지 않는 미추적 산출물

`.claude/plans/samsung-account-intake-m1.baseline.json` 만 §Files to Change 에 있고 커밋한다.
`.claude/receipts/mccp-implement-codex/samsung-account-intake-m1.json` 과
`.claude/PRPs/reports/samsung-account-intake-m1-report.md` 는 표 밖이라 커밋하면 `#8` 이 떨어진다 —
backlog `samsung-m1-receipt-path-out-of-table` 이 이미 이름으로 든 자리다. 선례대로 별도
`chore(evidence)` 커밋으로 낸다(`7cdc046`).

## Phase 4 나머지 축

| Level | 명령 | 결과 |
|---|---|---|
| Static | `npx tsc --noEmit -p tsconfig.json` | rc=1, 에러 4파일 19건 — **전건 기준선 안**, `modules/fc|subscribe` 0건 (`#6`) |
| Lint | (없음) | `insuniverse-server/package.json` 에 lint 스크립트가 0건. 이 저장소에 lint 축 자체가 없다 |
| Unit | `npx jest --silent` | 38스위트 34통과/4실패 · 242테스트 215통과/27실패 — **기준선과 동일 집합**, 신규 붉음 0 (`#2`) |
| Build | `npx nest build` | **rc=0**. `dist/` 는 서브모듈 `.gitignore:45` 라 `#8` 범위 밖 |
| Integration | N/A | 되살리기 경로에 M1 호출자가 0곳이라 통합 대상이 없다. 라이브 대조는 마일스톤 5 |
