# Milestone Closure — 4-treat-payments-messaging

## Milestone

- ID         : 4-treat-payments-messaging
- Name       : `01-treat-providers.md` + `03-payments.md` + `04-messaging.md`
- PRD        : `.claude/prds/insuniverse-docs-40-runtime-vendor-legal.prd.md` (Delivery Milestones 4행)
- Plan       : `.claude/plans/insuniverse-docs-40-runtime-vendor-legal-m4.plan.md`
- Status     : done
- Closed at  : 2026-08-26T08:13:31.090Z
- Closed by  : /mccp:milestone-close (run_id=d9f11c8a-ba78-492d-8ae2-5da2bde430ef)
- HEAD       : `a528631d4e4e30a917a8c92ce1fd8bd67c44bfdf`

> **id 가 `4` 가 아닌 이유.** `.claude/milestone-closures/4.md` 는 `ops-alert-system` 마일스톤 4 의
> closure 이고 git 추적 중이다. 행 번호만으로 이름을 지으면 PRD 가 다른 마일스톤끼리 파일명이 충돌해
> 앞의 closure 를 덮어쓴다. 이 저장소의 앞선 세 건(`1-04-runtime-topology` · `2-retention-billing-monitoring`
> · `3-02-failure-semantics`)이 이미 `<행>-<주제>` 규약을 쓰고 있어 그것을 따른다.

## Acceptance Condition

사용자가 `/goal` 에 넘긴 조건 원문:

> `.claude/plans/insuniverse-docs-40-runtime-vendor-legal-m4.gates.mjs all` 이 16/16 을 유지하고,
> PRD Delivery Milestones 4행이 complete 이며, closure 문서와 provenance 행이 선다.
> `#15` 는 종료 후 모드를 추가해 complete 를 요구하도록 고치되 종료 전 주장은 살린다. 또는 20 turn 후 중단

이 조건에 `#15` 항이 붙은 이유는 진입 전 실측에서 **전이 자체가 하네스를 깨는 것**이 확인됐기 때문이다.
lock 을 걸기 전에 `all` 을 한 번 돌려 baseline 16/16 을 잡고, `#15b` 가 4행을 `in-progress` 로 못박는
것을 소스에서 확인한 뒤 사용자에게 보고했다.

## Goal Loop Result

4항 전건 충족.

### 0) 진입 전 실측 — 왜 하네스 수정이 조건에 들어왔는가

`gates.mjs` 이전 판 `#15b` 의 마지막 두 줄:

```js
if (/\bcomplete\b/.test(row)) die(`#15b FAIL: complete 전이는 milestone-close 소유`);
if (!/\bin-progress\b/.test(row)) die(`#15b FAIL: Status 가 in-progress 가 아니다 — ${row.slice(0, 140)}`);
```

이 검사의 주장은 **"구현 창이 종료 전이를 가로채지 않았다"** 이고 그 자체는 옳다. 문제는 그 주장이
`in-progress` 라는 **값**에 결박돼 있어서, milestone-close 가 자기 일을 하는 순간 검사가 죽는다는
것이다. 저자는 같은 종류의 일회용성을 한 번 잡았다 — 같은 함수 주석이 *"커밋하는 순간 `all` 이 영구히
15/16 이 되고 … M5 의 `#0a` 가 M4 에서 죽는다"* 라며 커밋 전/후 두 모드로 갈랐다. **종료 전/후 축은
같은 자리에 남아 있었다.**

귀결이 이월이 아니라 차단인 이유는 `#0a` 다 — M4 의 `#0a` 가 선행 하네스 M1·M2·M3 의 `all` 을 rc 0
으로 요구하고, M5 하네스가 같은 구조를 쓰면 M4 `all` 이 15/16 인 채로 M5 전건이 서지 못한다.

부수 사실 하나 더 — milestone-close 는 PRD 에 **두 줄**을 쓴다(4행 전이 + provenance 행). 두 줄은 서로
다른 절에 있으므로 이전 판 `#15a`(hunk 전건이 `## Delivery Milestones` 범위 안)는 미커밋 상태에서도
`절 범위 밖 변경` 으로 죽는다.

### 1) `#15` 종료 후 모드 — 추가됐고 종료 전 주장은 살아 있다

`#15c` 를 신설해 **종료 여부를 PRD 의 자기 주장이 아니라 파일 바이트 해시로 판정**한다.

| 조건 | 판정 |
|---|---|
| `## Milestone Closure Provenance` 표에 `4-treat-payments-messaging` 행이 정확히 1개 | 아니면 exit 1 |
| 그 행 `Verdict` 가 `done` | 아니면 exit 1 |
| 그 행 `Closure` 셀이 `.claude/milestone-closures/4-treat-payments-messaging.md` | 아니면 exit 1 |
| 그 파일이 실재 | 아니면 exit 1 (행만 손으로 적는 것이 종료가 되지 않게) |
| 그 파일의 raw sha256 == 행의 `sha256` 셀 | 아니면 exit 1 |
| 행이 0개인데 closure 파일은 있음 | exit 1 (부분 종료 금지, 양방향) |

`closed` 가 정해지면 `#15b` 가 **양방향**으로 문다 — 종료 전 `complete` 는 구현 창이 milestone-close 의
몫을 가로챈 것이고, 종료 후 `in-progress` 는 closure 가 섰는데 표가 안 따라온 것이다. 둘 다 실패다.
`#15a` 의 허용 절 범위도 `closed` 에 따라 하나(`## Delivery Milestones`)와 둘(+ `## Milestone Closure
Provenance`)로 갈리고, 기대 변경 행 수가 1 과 2 로 갈린다.

`hash-markdown` 이 raw sha256 과 같은 값을 낸다는 것을 먼저 실측했다 — `plan_hash` 는 구조 해시라
원시 sha256 과 다르지만 `hash-markdown` 은 다르다:

```
$ node .../receipt/cli.js hash-markdown .claude/milestone-closures/3-02-failure-semantics.md
sha256:89de6f133ca2f3dff770e759cc340e27519026bfeb640f968e994977ef463bfc
$ node -e "…createHash('sha256').update(readFileSync(…))…"
sha256:89de6f133ca2f3dff770e759cc340e27519026bfeb640f968e994977ef463bfc
```

따라서 하네스는 `node:crypto` 하나로 재계산한다. `node:crypto` 는 `#11c` 허용 목록에 이미 있던
빌트인이라 **INV1(신규 의존 0) 은 그대로**다. 지정자 계수만 4 → 5 로 늘고 `#11c` 는 그 수를 리터럴로
주장하지 않는다.

**반사실 3건 — 실행해 확인했다.**

| # | 만든 상태 | 관측 |
|---|---|---|
| 1 | 패치 후, PRD 4행 `in-progress`, closure 없음 | `#15c ok [종료 전] provenance 행 0건` · `#15b ok [커밋 후 · 종료 전] … Status == in-progress · complete 아님` · rc 0 |
| 2 | PRD 4행 `complete`, closure·provenance 없음 | `#15b FAIL: complete 전이는 milestone-close 소유` · rc 1 — **종료 전 주장이 살아 있다** |
| 3 | PRD 4행 `complete`, provenance 행 sha256 을 한 글자 틀리게 | `#15c FAIL: sha256 불일치` · rc 1 — 행을 손으로 적는 것으로 종료가 되지 않는다 |

### 2) PRD Delivery Milestones 4행 — `complete`

```
| 4 | `01-treat-providers.md` + `03-payments.md` + `04-messaging.md` | 벤더별 연동 정책과 플랫폼 차이. `push.scheduler` 수용 | complete | `.claude/plans/insuniverse-docs-40-runtime-vendor-legal-m4.plan.md` |
```

PRD 꼬리의 `*Status: DRAFT*` 행은 그대로 둔다 — 마일스톤 5·6 이 `pending` 이라 PRD 자체는 아직 종료가
아니다. M3 closure 가 M1·M2·M3 세 하네스에서 `PRD` 상수 사용처를 열거해 *"어느 하네스도 `Delivery
Milestones` 표 행이나 PRD 꼬리의 `*Status:` 토큰을 읽지 않는다"* 를 확인했고, M4 하네스만 표 행을
읽는다. 그 자리가 이번에 두 모드로 갈렸다.

### 3) closure 문서와 provenance 행 — 섰다

이 문서와 PRD `## Milestone Closure Provenance` 표의 네 번째 행. 계획서 본문에는 아무것도 쓰지 않는다
(아래 §계획서 본문 무편집).

### 4) `all` 16/16 — 유지

`node .claude/plans/insuniverse-docs-40-runtime-vendor-legal-m4.gates.mjs all` 최종 출력 (rc 0):

```
#0a ok 선행 하네스 완주 — M1 15/15 · M2 13/13 · M3 14/14
#0b ok 선행 5문서 워킹트리 변경 0
#0c ok 동결 3트리 포인터 전건이 manifest repos[].head 이고 상위 체크아웃에서 commit 으로 열린다
#0 ok vendor-behaviors.yaml 벤더 8종 == VENDOR_KEYWORDS 8원소
OK — 8개 파일, 위반 0 (--strict-refs)
#1 ok 8개 파일 위반 0 (--strict-refs)
#2a ok 층 집합 불변식 3종 (비어있지 않음 · 5키 부분집합 · exception 은 non-treat 에만) · 관측 계수 35칸 · 리더가 건너뛴 형제 키 34개
#2b ok 계획서 축 2 표 재파싱 10행 == 하네스 상수 표 10행 (5열 전건 일치)
#2c ok 선행 5문서 id 74종과 교집합 0 (계획서 문면은 '4문서' — 실측 집합은 5다)
#2d ok 설정 블록 벤더성 키 12종 전건 귀속 — 인덱스 8 · 축2 1 · NOT Building 0 · 04 사각절 3 {s3 user mailer} · 미귀속 0
#2d 사각: 이 검사는 축 2 완전성을 판정하지 않는다 — 축 2 10건 중 configuration.ts 앵커 보유는 3건뿐이고 나머지 7건 {treat-provider:__pool__:routing payment:__env__:mode messaging:fcm:token-lifecycle messaging:fcm:exception messaging:push.scheduler:cron messaging:aligo:local-mock messaging:__bridge__:platform} 은 이 경로로 검증되지 않는다. 설정 블록을 쓰지 않는 연동과 블록 키·유닛 디렉터리 이름이 어긋나는 연동(chatbot↔libs/chat · google↔libs/youtube · cfn↔libs/cloudfront · mok)도 열거에서 빠진다
#2 ok 문서별 정책 수 01=23 03=11 04=11
#3a ok status 유도 일치 45/45 · data-dependent-unknown 3≥1 · conditional-unknown 42≥1
#3b ok unknown_reason 보유 45/45
#3c ok 본문 배치 자격 0건 == 본문 정책 0건 (3문서)
#4 ok 01=axis:treat 5 · 03={smartro,toss} · 04={aligo} · 합집합 8 == vendors 키 8 · 쌍별 교집합 0
#5a ok 스캔 대상 24파일 · D/E 적중 분해 both 3 db 0 env 29 none 13 · 토큰 db=[findEnabledPool · getSetting( · tn_treat_setting · enable_treat_car · enable_inhouse_car_canary] env=[process.env. · configService.get[<T>]( · this.config.get[<T>](]
#5b none — messaging:aligo:exception ← insuniverse-server/libs/aligo/src/aligo.exception.ts
#5b none — treat-provider:codef:fallback ← insuniverse-server/apps/api/src/modules/treat/dispatch/treat-dispatcher.service.ts
#5b none — treat-provider:coocon:fallback ← insuniverse-server/apps/api/src/modules/treat/dispatch/treat-dispatcher.service.ts
#5b none — treat-provider:coocon:normalization ← insuniverse-server/libs/coocon/src/coocon.mapper.ts
#5b none — treat-provider:hyphen:fallback ← insuniverse-server/apps/api/src/modules/treat/dispatch/treat-dispatcher.service.ts
#5b none — treat-provider:hyphen:normalization ← insuniverse-server/libs/hyphen/src/hyphen.mapper.ts
#5b none — treat-provider:inhouse:fallback ← insuniverse-server/apps/api/src/modules/treat/dispatch/treat-dispatcher.service.ts
#5b none — treat-provider:octover:fallback ← insuniverse-server/apps/api/src/modules/treat/dispatch/treat-dispatcher.service.ts
#5b none — treat-provider:octover:normalization ← insuniverse-server/libs/octover/src/octover.mapper.ts
#5b none — payment:smartro:exception ← insuniverse-server/libs/pg/src/smartro/smartro.exception.ts
#5b none — payment:toss:exception ← insuniverse-server/libs/pg/src/toss/toss.exception.ts
#5b none — messaging:fcm:exception ← insuniverse-server/apps/api/src/modules/fcm/fcm.exception.ts
#5b none — messaging:push.scheduler:cron ← insuniverse-server/apps/api/src/modules/push/push.scheduler.ts
#5b ok none 판정 13건 전건이 스캔 파일 목록을 출력에 냈다
#5 ok 재유도 == 대조표 7열 전건 45/45 · 유도 밖 행 0 (열: 정책 id · 벤더 · 층 · 인덱스 값 · 게이트 종류 · 미판정 종류 · 닫을 경로)
#5c ok 계획서 축 2 기대값 대비 편차 4건 · 전건이 미판정 종류를 보존한다 — treat-provider:__pool__:membership 계획 db → 재유도 both · treat-provider:__pool__:routing 계획 db → 재유도 both · treat-provider:__car__:scope 계획 db → 재유도 both · messaging:fcm:token-lifecycle 계획 none → 재유도 env
#6a ok 봉쇄 목록 9값 (인덱스 timeout 5 · TTL 3 · 캐시 1)
#6 ok 3문서 등장 0건
#7a ok 벤더별 진입 형태 표 5행 == axis:treat 집합
#7 ok 풀 축 산문의 앵커 3건 전건 동결 트리 해석 가능
#8 ok anchor tokens 81 / bad 0 — insuniverse-server 77 · insuniverse-site 4 · insuniverse-admin 0
#9 ok 3문서 연혁 행 01=9 03=2 04=5 · SHA 전건 docs/_meta/index/commits.server.jsonl 실재 · 행 0개 문서 없음
#10 ok 문서별 강제 지점 == 적용 범위 == 예외·폴백 == 프론트매터 (01 23 · 03 11 · 04 11)
#11a ok --help rc=0 · 서브커맨드 목록 출력
#11b ok 알 수 없는 서브커맨드가 이름을 담고 exit 1
#11c ok 의존 지정자 5건 전건 node 빌트인 또는 doc-kit lint (UI8)
#12 ok 배치표 담당 04-messaging 1종 {push.scheduler} == 문서 스케줄러 정책 1건
#13 ok 코드값 리터럴 0건 · 검사 패턴 3종
#13 사각: 패턴은 표본이고 범위가 아니다 — toss·smartro·aligo 코드는 codes/vendor.yaml pattern 밖이라(관측 10) 대문자 상수형 표본으로만 잡히고, 소문자·숫자만으로 된 벤더 코드나 한글 사유 문구로 옮겨 적은 값은 이 검사가 보지 못한다
#14a ok 설정 블록 벤더성 키 12종 · 키워드 집합 8원소 · 차집합 {fcm, mailer, s3, user}
#14 ok 차집합 4건 전건이 04-messaging 본문에 사각으로 기술됨
#14b ok 플랫폼 차이 표 2행 계수 == 동결 site 재계수 (부분 문자열 매칭 — 단어 경계로는 밑줄 접두 토큰이 0건으로 떨어진다)
#15c ok [종료 후] provenance 행 1건 · Verdict done · closure 실재 · sha256 일치 sha256:<이 문서의 해시 — 자기 참조라 본문에 못 싣는다. PRD provenance 표의 sha256 셀이 정본>
#15a ok [미커밋] PRD diff hunk 2건 전건이 ## Delivery Milestones + ## Milestone Closure Provenance 범위 안 · 변경 행 2개 (기대 2)
#15b ok [미커밋 · 종료 후] 대상 행이 4행 · Plan 셀 == 이 계획서 경로 · Status == complete · in-progress 아님
#15 ok (모드 미커밋 · 종료 후)
all ok 16/16 통과
```

`#0a` 가 M1 15/15 · M2 13/13 · M3 14/14 회귀와 선행 5문서 워킹트리 불변을 선행 검사하므로, 이 출력은
이 창이 앞선 세 마일스톤의 산출물을 건드리지 않았다는 진술을 함께 진다. `#15` 는 커밋 전이라 미커밋
모드이며 종료 후 모드로 붙었다 — 변경 두 줄이 `## Delivery Milestones` 와 `## Milestone Closure
Provenance` 두 절에 각각 하나씩임을 물었다.

### 5) 네 모드 조합 전건 실측 — M5 가 볼 자리는 마지막 행이다

`#15` 는 이제 (커밋 전/후) × (종료 전/후) 네 상태를 가진다. **넷을 전부 만들어 돌렸다.**

| 커밋 | 종료 | PRD 4행 | 관측 | rc |
|---|---|---|---|---|
| 커밋 후 | 종료 전 | in-progress | `#15c ok [종료 전] provenance 행 0건` · `#15b ok [커밋 후 · 종료 전]` | 0 |
| 미커밋 | 종료 전 | complete | `#15b FAIL: complete 전이는 milestone-close 소유` | 1 |
| 미커밋 | 종료 후 | complete | `#15a ok [미커밋] … 변경 행 2개 (기대 2)` · `#15b ok [미커밋 · 종료 후]` | 0 |
| **커밋 후** | **종료 후** | **complete** | `#15a ok [커밋 후] PRD 워킹트리 diff 0건` · `#15b ok [커밋 후 · 종료 후] … Status == complete` | **0** |

마지막 행이 이 종료 커밋 이후의 정지 상태이고 **M5 하네스의 `#0a` 가 M4 `all` 을 돌릴 때 보는 상태**다.
그 자리에서 `all ok 16/16 통과` 를 확인했다 — 이 마일스톤 종료가 다음 마일스톤을 막지 않는다.

**이 설계가 새로 만든 뻣뻣함 하나를 적어 둔다.** 종료 후 미커밋 모드는 변경 행을 정확히 2개로 요구한다
(4행 전이 + provenance 행). 종료 후에 그 두 절 중 한쪽만 손대면 커밋하기 전까지 `#15b FAIL: 변경 행이
1개다 (기대 2)` 로 떨어진다. 종료 뒤 그 두 자리가 다시 움직일 일이 없다고 보고 fail-closed 쪽을 택했다.

## 계획서 본문 무편집 — receipt `plan_hash` 재검증

`hash-plan` 재계산값과 두 receipt 의 `plan_hash` 가 같다:

```
live plan_hash                       : sha256:8f94905be762e7368b8decce140ee5f27872cfb61aaf16b9323ac2ce589f69ca
mccp-plan-codex/...-m4.json          : sha256:8f94905be762e7368b8decce140ee5f27872cfb61aaf16b9323ac2ce589f69ca
mccp-implement-codex/...-m4.json     : sha256:8f94905be762e7368b8decce140ee5f27872cfb61aaf16b9323ac2ce589f69ca
```

계획서 §Acceptance 의 "착수 후 계획서 본문이 편집되지 않았다" 가 이것으로 닫힌다. 이 closure 는
계획서에 provenance 를 찍지 않고 PRD 표에 한 행을 더한다 — 계획서에 절을 덧붙이면 위 두 receipt 가
동시에 `stale` 로 떨어지고 `stale` 에는 우회 env 가 없다.

## 이 closure 가 남기는 갈림 — 계획서 §Validation 인용과 라이브 출력이 한 줄 어긋난다

**하네스 파일을 고쳤고 계획서 본문은 고치지 않았다.** 두 receipt 의 앵커는 계획서 본문이므로
`plan_hash` 는 그대로지만, 계획서 §Validation `# 기대:` 주석이 `#15` 의 출력을 **세 줄**로 적고 있고
지금은 네 줄이다(`#15c` 신설).

| 자리 | 계획서 문면 (446~457행) | 라이브 |
|---|---|---|
| `#15` 기대 줄 수 | 세 줄 (`#15a` · `#15b` · `#15`) | 네 줄 (`#15c` · `#15a` · `#15b` · `#15`) |
| `#15b` 기대 문면 | `Status == in-progress` | 종료 전 `in-progress` / 종료 후 `complete` 로 갈림 |
| `#15` 음성 케이스 | `Status 를 complete 로 바꾸면 #15b FAIL: complete 전이는 milestone-close 소유` | **그대로 유효** — 위 반사실 2 가 재현했다 |
| `#11c` 기대 | `의존 지정자 <k>건` (플레이스홀더) | `5건` — 계획서가 수를 리터럴로 적지 않아 어긋나지 않는다 |

이 갈림을 계획서를 고쳐 없애지 않는다. 그것이 `plan_hash` 를 움직여 두 receipt 를 stale 로 만들고,
봉인된 판정을 사후에 옮기는 일이 된다. **문면과 라이브가 갈렸다는 사실을 여기 남기는 것이 정본이다.**

## 남는 `(미검증)` — 이 closure 가 닫지 않는 것

계획서 §Acceptance 의 `(미검증)` 3건은 그대로 열려 있다.

- 축 2 가 코드 전용 정책을 하나도 빠뜨리지 않았다 — 열린 집합이라 게이트로 닫히지 않는다.
  `#2d` 출력이 자기 사각을 자백한다(축 2 10건 중 `configuration.ts` 앵커 보유 3건, 나머지 7건 미검증).
- PRD Evidence `[구-SHA]` 3행의 재측정 3갈래 — `.claude/plans/codex-findings-backlog.md` 이월 행. 사람 확인.
- Q17·Q40 이외의 온보딩 질문 중 이 세 문서 담당분이 문서만으로 답해지는가 — 유지보수자 판정.

그 밖에:

- **Codex 축은 통째로 안 돌았다.** implement receipt 가 `codex_skipped: false` 인데
  `codex_skip_reason: "codex_disabled"` · `codex_disabled: true` · `resolution.codex_verdict: "skipped"` 다.
  `MCCP_CODEX_DISABLED` 가 전역 설정에 살아 있고 하류 게이트도 같은 경로로 skip 된다. 이 closure 의
  하네스 수정도 Codex·L2 패널 리뷰를 받지 않았다 — backlog 이월 대상이다.
- `#15c` 는 closure 문서의 **내용**을 판정하지 않는다. 실재와 바이트 해시만 본다. 종료 acceptance 가
  실제로 충족됐는가는 이 본문이 지며 그것은 사람 판정이다. 그 사각을 하네스 출력에도 주석으로 적었다.
- 이 문서는 **자기 해시를 본문에 실을 수 없다**. 아래 `all` 인용의 `#15c` 행에서 sha256 값만 가린
  이유이며, 정본은 PRD provenance 표의 `sha256` 셀이다.
- `/mccp:code-review`(PR 모드) · `/mccp:pr` 은 이 closure 로 닫히지 않는다.

## Provenance

- Lock run_id        : `d9f11c8a-ba78-492d-8ae2-5da2bde430ef`
- Lock owner session : `unknown` (`CLAUDE_SESSION_ID` 미설정 — 최소 사양 모드)
- PRD source         : `.claude/prds/insuniverse-docs-40-runtime-vendor-legal.prd.md`
- Plan source        : `.claude/plans/insuniverse-docs-40-runtime-vendor-legal-m4.plan.md`
- Gate harness       : `.claude/plans/insuniverse-docs-40-runtime-vendor-legal-m4.gates.mjs` (이 창에서 `#15` 수정)
- Detection signal   : `{"row":4,"name":"`01-treat-providers.md` + `03-payments.md` + `04-messaging.md`","plan":".claude/plans/insuniverse-docs-40-runtime-vendor-legal-m4.plan.md","status":"in-progress"}`
- mccp version       : 1.32.1
- Grammar response   : `goal-done:M4 벤더 연동 3문서 종료`
