# notes · insuniverse-docs-30-contracts-limits — 마일스톤 5

계획서: `.claude/plans/insuniverse-docs-30-contracts-limits-m5.plan.md`
하네스: `.claude/plans/insuniverse-docs-30-contracts-limits-m5.gates.mjs`
산출물: `docs/03-contracts/05-external-client-api.md`

**리뷰 기록은 이 파일이 든다.** 계획 본문에 쓰면 `mccp-plan-codex` receipt 의 `plan_hash` 가
움직여 봉인이 stale 로 떨어진다.

## Codex Implementation Review

- 호출: `node …/scripts/lib/codex-invoke.js adversarial-review` (fail-closed Bash wrapper, v0.2.2)
- 라운드 수: 0
- 합치 결론: **Codex 는 이 게이트에서 발화하지 않았다.** `MCCP_CODEX_DISABLED=1` 이 전역
  `~/.claude/settings.json` 에 박혀 있어 wrapper 가 `classification=disabled` 로 단락했다
  (실측 프로브: `{"ok":true,"stdout":"","stderr":"","durationMs":6,"classification":"disabled","blocking":false,"advisory":false}`).
  지적 0건은 **없다는 관측이 아니라 묻지 못했다는 기록**이다. receipt 의
  `codex_verdict=skipped` 가 그 사실을 든다 — 하류 dedupe 는 `converged` 밖의 값에
  fail-closed 이므로 `/mccp:pr` 시점의 PR-Codex 가 그대로 발화한다.

> Codex skipped per MCCP_CODEX_DISABLED=1

- YAGNI Triage:

  | Finding | Severity | Verdict | Why |
  |---|---|---|---|
  | (없음) | — | — | Codex 미발화. 판정할 지적이 0건이다 |

- Deferred to backlog: 0 → `.claude/plans/codex-findings-backlog.md`
- Open Questions: 계획서 §Open Questions 네 항목이 그대로 열려 있다 — 전건 severity LOW.
  자동 CRITICAL 목록(비밀 노출 · 데이터 손실 · 되돌릴 수 없는 마이그레이션 · 인증 우회 ·
  외부 목적지 변경 · 암호키 취급) 해당 0건.
- Codex session 참조: 없음 (미발화)

### Security Reviewer

이 마일스톤의 산출은 `docs/**` 문서 1건과 `.claude/**` 하네스·기록 3건이다. 인증·암호·비밀·입력
검증·주입·SSRF·경로 순회·권한 상승 어느 축도 건드리지 않으며, 실행 코드 경로를 만들지 않는다.
문서가 **서술하는** 대상이 대외 자격 증명 관문이지만, 이 마일스톤이 만드는 것은 그 관문을 읽어
기록한 산문이고 관문 자체는 동결 SHA 의 읽기 전용 blob 이다. Phase 2.5.5 의 보안 리뷰 트리거
조건(security-sensitive areas)에 해당하지 않아 호출하지 않았다 — 호출 실패로 인한 auto-fallback
이 아니므로 `security_skipped` 도 세우지 않는다.

### Design Review

`impeccable-detect --mode implement` 가 `design_signal=false` · `silent_skip=true`
(`reason=no-signal`) 를 냈다. 이 마일스톤은 렌더링 표면을 만들지도 고치지도 않는다
(`docs/**` 1건 · `.claude/**` 3건). receipt 에 `impeccable_silent_skip=true` 로 실린다.
계획 단계 탐지기도 같은 값을 냈다(계획서 §Codex Adversarial Review) — 마일스톤 4 와 달리 두
단계의 신호가 갈리지 않는다.

### 구현 시점에 새로 선 결정 (2.5.2)

Codex 가 발화했다면 이 목록이 focus 였다. 발화하지 않았으므로 **묻지 못한 결정 목록**으로
남긴다.

| # | 결정 | 대안 | 선택 이유 |
|---|---|---|---|
| D1 | `entry_prefixes` 분모를 **대외 관문 클래스 이름을 `@UseGuards(...)` 인자로 다는 컨트롤러**로 잡았다 | `modules/client/` 디렉터리 트리 · `clients` 접두 문자열 | 계획서 G3·G4 가 둘 다 반증했다. 디렉터리로 잡으면 디렉터리 밖 진입점(G4)이 사라지고, 접두 문자열로 잡으면 내부 관리 표면(`admin/clients`)이 섞인다. 관문 데코레이터만이 두 오류를 동시에 피한다 |
| D2 | 상수 표 세 건을 **`export const <이름> = { code: '<값>', message: … }` 형태의 최상위 선언**으로 파싱하고 칸 이름(`code`/`resultCode`)을 함께 뽑았다 | 코드 인덱스 `codes/*.yaml` 의 값 목록 | 계획서 Task 2 가 인덱스를 집합·계수 용도로 금지한다 — `source_sha` 가 manifest head 셋과 다르다. 칸 이름을 함께 뽑는 것은 `client:error-table-is-separate` 의 하중이 **값이 아니라 칸 이름의 갈림**이기 때문이다 |
| D3 | 검사 `#7` 의 내부 전용 코드값 분모를 `(내부 두 표) \ (대외 표)` **값 차집합**으로 계산했다 | 내부 두 표의 값 전체 | 후자는 `E0001` 을 봉쇄해 마일스톤 5행 Outcome 이 지목한 코드를 문서가 쓸 수 없게 만든다(G14). 차집합이 그 충돌을 푸는 유일한 분모다 |
| D4 | 검사 `#10` 의 순도 축을 **서술 행 전체에서 `[0-9]` 를 세는** 것으로 뒀다. 목록 마커도 표 구분선도 예외로 두지 않았다 | 마커·구분선을 먼저 벗기고 남은 산문만 보기 | 계획서 167행이 *"기획 절은 코드 심볼도 숫자도 없이 쓴다"* 로 못박았다. 예외를 두면 순서 목록을 타고 한도 숫자가 들어온다 — 이 문서에서 가장 새기 쉬운 값이 그것이다. 대가는 기획·부록 절에 순서 목록과 표를 못 쓴다는 것이고, 두 절 다 실제로 쓰지 않는다 |
| D5 | 하네스를 `.mjs` 단일 파일로 두고 계획서 §Validation 은 인라인 펜스로 `# 검사 #N` 마커만 든다 | 계획서에 검사 본문 인라인 | CLAUDE.md §1 의 200행 규약이고 같은 PRD 의 m2·m3·m4 선례 3건이다 |

## 실측 기록 — 구현 시점 (2026-08-28)

동결 기준점은 manifest `repos[].head` 이며 server `4d798f55` · site `767d1e44` · admin `fb001e1d` 다.
워크트리 서브모듈 3종 HEAD 가 그 값과 전건 일치한다(`git submodule status` 실측).

### 계획서 §GROUND 대조 — 전건 확인

| 관측 | 계획서 | 구현 시점 실측 | 판정 |
|---|---|---|---|
| G3 | `modules/client/` 아홉 컨트롤러 중 대외 관문은 하나 | 아홉 건 전수 열거 확인. 여덟이 `admin/`·`admin-view/` 접두이거나 라우트 0개, `client.controller.ts` 만 `ClientBasicAuthGuard` | 확인 |
| G4 | 디렉터리 밖 진입점 하나 더 | `treat/coocon-bypass.controller.ts` 가 같은 관문 · 접두 `clients/coocon` · 인터셉터도 필터도 없음 · 한도 초과를 `code`·`message` 로 던짐 | 확인 |
| G5 | 껍데기 둘 | `ClientAnalyzeController`·`ClientSettlementController` 둘 다 모듈 `controllers` 등록 · HTTP 데코레이터 0 · 관문은 내부 세션 관문 | 확인 |
| G7 | `E0001` 3중 의미 | `api.constant.ts:16`·`admin.constant.ts:62`·`client.constant.ts:29` 세 행 번호가 동결 blob 에서 그대로 맞음 | 확인 |
| G8 | 이름 충돌 — 내부↔대외 둘, 내부↔내부 셋 중 하나는 값까지 같음 | 내부↔대외 `NOT_FOUND_ORDER`(대외 `E0201` / 내부 `E0208`) · `ANALYSIS_FAILED`(대외 `E0301` / 내부 `E0305`) 둘. 내부↔내부 `UNAUTHORIZED`·`NOT_FOUND_REQUIRED_PARAMETER`·`NOT_FOUND_FILE` 셋이고 마지막은 값까지 같음 | 확인 |
| G10 | 요구되지 않는 옵션 실재 | 선언 24 · 결속 21 · 차집합 `CCT015`·`CCT016`·`CCT017` | 확인 |
| G17 | `staged-only` 실재 | `client.service.ts:45`·`:436` 두 행의 blame 이 `releases.yaml` server `unreached`(64건) 안에 있음 | 확인 |
| G18 | 나머지 앵커 전건 prod 도달 | 앵커 46건 중 위 둘만 미도달, 나머지 44건 도달 | 확인 |
| G19 | 소비자가 저장소 밖 | site 0건 · admin 0건. admin 의 `/admin/clients` 는 대외 계정 **관리** 화면이며 대외 접두 호출이 아님 | 확인 |
| G21 | golden 은 문서 생성으로 안 움직임 | 봉인 시점 `cmp -s` 바이트 동일 (검사 `#1` seal) | 확인 |

**계획서 문면이 실측과 어긋난 자리는 0건이었다.** 계획 단계 GROUND 가 이미 동결 blob 을 읽고
쓰였기 때문이며, 이 마일스톤에서 새로 발견한 사실은 아래 표의 셋뿐이다.

### 계획서가 몰랐던 자리 (구현 시점 신규)

| # | 관측 | 이 문서에 미친 영향 |
|---|---|---|
| N1 | `resultCode`·`CCT001` 같은 토큰이 **소비자 저장소에서 다른 이름공간과 충돌**한다. site 의 `resultCode` 는 결제 게이트웨이 쿼리 파라미터이고, admin 의 `CCT001` 은 알림 채널 어휘(`"문자"`)다 | 검사 `#13` 의 호출 측 토큰을 **접두로 정규화한 대외 경로**(`clients/search`·`clients/analyze`·`clients/coocon/scrap`)로 좁혔다. 코드값이나 칸 이름을 토큰으로 쓰면 site·admin 칸이 거짓 `소비` 로 뒤집힌다 |
| N2 | 대외 라우트는 컨트롤러 접두와 메서드 경로가 **따로 선언**돼 server 소스에 완전 경로 문자열이 실재하지 않는다 | 검사 `#13` 의 server 칸은 선언 측 토큰(관문 클래스 이름)으로, site·admin 칸은 호출 측 토큰(완전 경로)으로 나눠 판정한다. 한 토큰군으로 세 열을 다 보려 하면 server 가 0건이 된다 |
| N3 | `admin.constant.ts` 는 CRLF 파일이다 | `blobText` 가 CRLF 를 정규화한다. 정규화하지 않으면 토큰 대조가 줄 끝에서만 어긋난다 |

### 게이트 실행 관측

- `--help` rc 0 · `nosuchcheck` rc 1 (`알 수 없는 서브커맨드: nosuchcheck`) — 양성 먼저, 그다음 음성.
- `--selfprobe` 추출기 16종 전건 `empty=reject mutate=reject`.
- 검사 `#4` 앵커 47건(고유 46) · 본문 인용 43건(고유 위치 31) 전건 해석.
- 검사 `#8` 6축 원소 92건 양방향 일치 · 검사 `#9` 주장 4건 원소 16건 일치.
- 검사 `#10` 서술 66행에서 코드 심볼 0 · 숫자 0.
- 검사 `#13` 27칸 실재 일치 — server 전건 `강제 지점`, site·admin 전건 `—`.
- 검사 `#14` 진입점 2 · 겉모양 동반 1 · 결속 21건 고유 21건 · 비결속 2건.

## 음성 프로브 관측 (2026-08-28)

계획서 §Validation 이 `(구현 시 실측)` 으로 남긴 자리를 실제로 뒤집어 본 기록이다. 스크립트는
원본을 백업하고 `finally` 와 `process.on("exit")` 양쪽에서 복원하며, 임시 파일은 등록부로 추적해
실패 경로에서도 지운다(하네스 `die` 가 `finally` 를 건너뛴 선례 때문).

| 검사 | 변이 | rc | 관측된 첫 행 |
|---|---|---|---|
| `#3` | 신설 문서를 glob 밖 이름으로 옮김 | 1 | `#3 FAIL: lint 대상 집합에 신설 문서가 없다 1건 — docs/03-contracts/05-external-client-api.md` |
| `#4` | 앵커 행 번호를 파일 길이 밖으로 | 1 | `#4 FAIL: 토큰 맵 ↔ 문서 앵커 집합 불일치 — 문서에만: [...client.controller.ts:99999] / 토큰 맵에만: [...:71]` |
| `#4` | 앵커를 토큰 맵 밖 행으로 이동 | 1 | 같은 형태 (`...interceptor.ts:26` vs `:25`) |
| `#5` | 미배포 정책 앵커를 도달 행으로 | 1 | `#5 FAIL: 1건 — staged-only 인데 앵커 blame 이 도달 집합에 있다 ...client.service.ts:73` |
| `#5` | `unknown_reason` 삭제 | 1 | `#5 FAIL: 1건 — unknown 접미인데 사유가 없다 client:quota-values-are-contract-data` |
| `#6` | 본문에 게이트 이름 1개 | 1 | `#6 FAIL: 게이트 이름 누출 1건 — TREAT_PROVIDER (...:373)` |
| `#7` | 본문에 내부 전용 코드값 1개 | 1 | `#7 FAIL: 형제 축 값 누출 1건 — 내부 전용 코드값 E0208 (...:373)` |
| `#7` | 본문에 수치 정책 상수명 1개 | 1 | `#7 FAIL: 형제 축 값 누출 1건 — 상수명 MAX_MONEY_AMOUNT (...:373)` |
| `#8` | 결속 집합에서 라우트 1개 삭제 | 1 | `#8 FAIL: 1건 — client_routes 문서에 없는 소스값 clients/analyze/notice` |
| `#8` | 결속 집합에 없는 옵션값 추가 | 1 | `#8 FAIL: 1건 — option_codes 소스에 없는 문서값 CCT099` |
| `#9` | 이름 충돌 1건 삭제 | 1 | `#9 FAIL: 1건 — name-across-tables 실측에 있는데 문서에 없다 ANALYSIS_FAILED` |
| `#9` | 실측에 없는 원소 추가 | 1 | `#9 FAIL: 1건 — option-declared-unrouted 문서에 있는데 실측에 없다 CCT014` |
| `#10` | 기획 절에 아라비아 숫자 | 1 | `#10 FAIL: 기획 절 코드 심볼 1건 — 숫자 리터럴 "2" (...:124)` |
| `#10` | 기획 절에 코드 스팬 | 1 | `#10 FAIL: 기획 절 코드 심볼 2건 — 코드 스팬 · 파일 확장자 (...:137)` |
| `#11` | 표만 complete, 기대는 in-progress | 1 | `#11 FAIL: 상태가 기대와 다르다 기대=in-progress 실측=complete` |
| `#12` | 하네스 사본에서 라우트 추출기 0건 거부 제거 | 1 | `parseRouteOptionMap … mutate=accept` 행 + `--selfprobe FAIL: 1건` |
| `#13` | site 칸을 `소비` 로 날조 | 1 | `#13 FAIL: 1건 — client:surface-entry-points × site '소비' — 실측은 '—' 다 (동결 트리 0파일, 행 292)` |
| `#13` | 프론트매터에 없는 표 행 추가 | 1 | `#13 FAIL: 1건 — 프론트매터에 없는 표 행 client:nosuch` |
| `#15` | 연혁 SHA 한 자리 변경 | 1 | `#15 FAIL: 1건 — 0b800ee8 를 커밋으로 해석한 저장소가 0곳이다` |
| `#15` | 연혁 날짜 1칸 변경 | 1 | `#15 FAIL: 1건 — eed6bf4f 의 날짜가 2026-06-30 인데 표는 2026-06-29 다` |
| `#15` | `last_verified` 를 head 접두 밖 값으로 | 1 | `#15 FAIL: 1건 — last_verified 'deadbee' 가 manifest head 0건에 걸린다` |
| `#1` | golden 스냅샷에 1바이트 추가 (봉인 모드) | 1 | `#1 FAIL: golden 변동` + `106: now="" snap="x"` — verify.sh 완주 뒤 바이트 대조에서 떨어진다 |

**변이 22건 전건 rc 1 · 뒤집히지 않은 변이 0건.** 프로브 뒤 워킹트리에 남은 변경은 이 마일스톤의 산출물뿐이다 — 신설 파일은 미추적, 기존 추적 파일 중 diff 가 있는 것은 PRD 하나(Task 6 상태 전환)다. `golden.txt` 와 스냅샷은 sha256 동일.

### 프로브가 드러낸 하네스 성질 둘

- **`#4` 의 행 번호 변이는 "행 초과" 가 아니라 "토큰 맵 불일치" 로 떨어진다.** 앵커 집합이
  토큰 맵과 양방향으로 묶여 있어 행 번호를 하나만 바꿔도 그 이전에 전단사가 깨진다. 두 축이
  겹쳐 있는 것이며 사각지대가 아니다 — 다만 실패 메시지가 계획서 문면(`앵커 행 초과 …`)과
  다르므로 여기 적어 둔다.
- **`#14` 는 음성 프로브를 만들 수 없다.** 그 검사의 입력이 전부 동결 blob 이고, 진입점을 하나
  더 만들려면 서브모듈 커밋을 새로 써야 한다(INV2 위반). 문서 쪽 변이로는 뒤집히지 않는다 —
  이 검사는 문서를 보지 않고 소스만 보기 때문이다. `(미실측)` 으로 남긴다.
- **`#1` 의 findings 축도 음성 프로브를 만들지 않았다.** `docs/_meta/findings/30-contracts-limits.md`
  를 만들면 `check1` 의 존재 확인보다 **먼저** `verify.sh` 의 golden 대조가 `findings.files` 키로
  붉어져, 관측되는 실패가 계획서 문면(`findings … 개설됐다`)이 아니라 verify 실패가 된다. 두
  게이트가 같은 사실을 겹쳐 잡는 것이며, 굳이 실측하려면 `docs/_meta/` 에 잔여물을 남길 위험을
  진다. `(미실측)` 으로 남기고 그 이유를 여기 적는다.

## 로컬 코드 리뷰 라운드 (2026-08-28)

`/mccp:code-review` Local Review Mode — 미커밋 변경 9건 대상. Reviewer B 는 서지 않았다
(`MCCP_CODEX_DISABLED=1`). 지적 9건 전건 흡수, 기각 0건.

| 라운드 | A | B | 층위 | 흡수 | 기각 |
|---|---|---|---|---|---|
| R1 | Local Review Mode (실행 대조 포함) | 미발화 (`MCCP_CODEX_DISABLED=1`) | 1 · 2 · 6 · 7 · 8 | 9 | 0 |

| # | 층위 | 지적 | 흡수 | 반사실 |
|---|---|---|---|---|
| H1 | 1 · 2 | 문서 기획 절이 *"각 표 안에서 값이 유일하다"* 고 적었는데 거짓이다. 내부 두 표에 중복이 실재한다 | 문장을 실측대로 고치고 `#14` 에 네 번째 축(표 안 값 유일성)을 넣었다 | 하네스 사본에서 대외 표를 중복 있는 내부 표로 바꾸면 `#14 FAIL: 대외 표에 코드값 중복이 2건이다` |
| M1 | 2 | 원장 D4 가 적은 마커 분리를 `#10` 이 하지 않는다 | D4 를 코드가 하는 일로 고쳐 적었다. 코드는 그대로 — 계획서 167행이 예외 없는 축을 요구한다 | (기록 정정이라 반사실 없음) |
| M2 | 6 | `parseControllerMeta` 가 데코레이터 첫 매치만 읽어, 메서드 레벨 대외 관문이 표면에서 빠진다 | `matchAll` 로 전건 수집 · 한 파일 컨트롤러/클래스 둘 이상이면 거부 | 클래스 레벨 관리자 관문 + 메서드 레벨 대외 관문 입력에서 `guards` 가 둘을 다 든다. 한 파일 두 컨트롤러 입력은 `reject` |
| M3 | 8 | `check-all` 의 `ORDER` 를 어느 게이트도 안 문다 — 검사를 얹어도 `#2` 는 초록인 채 봉인이 건너뛴다 | `#2` 에 `ORDER ∪ {check12}` ↔ 디스패치 양방향 대조 추가 | 사본에서 `ORDER` 의 `check15` 를 지우면 `#2 FAIL: 디스패치에만: [check15]` |
| M4 | — | `#13` 이 행마다 전 트리 grep 을 돌려 27회 실행 | 저장소당 1회로 올렸다 | 실측 16.5초 → 1.35초 |
| M5 | 1 | 예외·폴백 *"관리자 관문을 다는 컨트롤러"* 행의 근거가 대외 관문 행을 가리킨다 | 앵커 `client-admin.controller.ts:27` 을 신설하고 근거와 강제 지점 서술을 그리로 옮겼다 | `#4` 앵커 47 → 48 · 토큰 맵 양방향 유지 |
| M6 | 6 | 상수 표 셋이 경로 리터럴이라 네 번째 표가 생기면 문서의 *"내부 표는 둘"* 이 조용히 거짓이 된다 | `constantTables()` 가 동결 트리 `constants/` 를 재열거해 리터럴 셋과 양방향 대조 | 사본 리터럴에 유령 파일을 더하면 `#14 FAIL: 상수 표 집합 불일치` |
| L1 | — | `clientControllers()` 가 파일당 `parseControllerMeta` 를 두 번 부른다 | 한 번으로 | (성능) |
| L2 | 6 | `parseStatusTokenMilestone` 이 `*Status:` 행의 **첫** `마일스톤 N` 을 집어, 같은 행의 `마일스톤 1~5` 가 앞으로 오면 다른 번호를 읽는다 | 선두 절(`<상태> — 마일스톤 N`)에 결박 | 선두 절 없는 행은 `reject`, 정상 형태는 `5` |
| L3 | — | 리포트 §Files Changed 계수 둘이 실제와 다르다 | 실측값으로 정정 | `wc -l` 대조 |

**흡수 뒤 봉인 실행**: `check-all --expect complete` rc 0 (`#1` seal · `verify.sh` `check PASS` 포함).
`#4` 앵커 48건(고유 47) · `#10` 서술 68행 · `#14` 네 축.

### 이 라운드가 남긴 사각지대

- `#14` 의 네 번째 축도 **음성 프로브가 문서 쪽에서 만들어지지 않는다.** 입력이 전부 동결
  blob 이라 `#14` 의 앞 세 축과 같은 제약이다. 하네스 사본에서 표 지정을 바꿔 뒤집었고 그
  관측을 위 표에 적었다 — 서브모듈을 건드리지 않는 유일한 경로다(INV2).
- M2 의 새 거부(한 파일 컨트롤러 둘)는 **동결 트리에 해당 파일이 0건**이라 실행 경로에서
  한 번도 타지 않는다. 뒤집힘은 합성 입력으로만 확인했다.
