# 관리자 RBAC 마일스톤 1 기록

## 마감 요약

마감 일자 2026-10-19. PRD Delivery Milestones 1 행을 `complete` 로 표시했다. 아래 「사용자 몫」 과 「(미검증)」 은 그 표시와 별개로 남아 있다. 게이트 · 리뷰의 세부 근거는 이 노트의 아래 절에 있다.

### 게이트 결과

- 계획 게이트: 상한 3 라운드를 다 쓰고 최종 divergent (`multi-agent`, L2 정족수 실패, L3 divergent). 마지막 라운드 CRITICAL 은 없었다. 2차 처리(사용자 지시 2026-10-19, 마지막 라운드 skip-receipt 진행)에 따라 plan receipt 를 쓰지 않았다. `MCCP_SKIP_RECEIPT=1` 로 쓰려 하면 intent 게이트가 fail-closed(exit 2, intentDecision 없음)로 막았고, `MCCP_SKIP_INTENT_GATE` 는 쓰지 않았다. `.claude/receipts/mccp-plan-codex/admin-rbac-m1.json` 은 없다.
- 구현 게이트: Codex implementation review 1 라운드, `codex_verdict=divergent` (흡수 뒤 재검토 없음). implement receipt `.claude/receipts/mccp-implement-codex/admin-rbac-m1.json` 은 있다. 그 안의 `converged` 필드는 CLI 기준 true 이고, 게이트 판정은 `codex_verdict` 다.
- 체인 검증: 마감 때 `validate --command mccp:prp-implement` 를 다시 돌렸다. rc 2 (`mccp-plan-codex` 누락). 미해소 (사용자 몫 1).
- 계획 Validation 검사 (검사별 실행, 최종 리뷰 R2 기준): `#1`~`#8` · `#10` · `#11` · `#13` 통과. `#9` 붉음 (`FR=5 EX=0`, allow-ip 5행. 계획이 이 파일 집합을 외부 표지로 고정해 개정 요청 3 대기). `#12` 붉음 (허용 집합 밖 3경로: 백로그 · 보고서 · implement receipt. 개정 요청 4 대기).
- 계획 Acceptance 체크박스는 전부 미체크다. 계획서는 동결이라 고치지 않았다.
- 재계수 (검사 `#11` 이 노트 줄과 대조): 03ce `noauth=80 admin51=38`, aff `noauth=80 admin51=88`. admin51 은 정의 개정(R2) 뒤 값이다. 개정 전 값은 03ce 1 · aff 51. 행 704 · 클래스 126. 런타임 대조 `route-compare ok rows=704 runtime=704 express=704 wildcard=70`.
- 소스 무변경. 서브모듈 3곳 워킹트리 clean, HEAD 는 gitlink 와 같다 (server `03ce7146` · admin `146ed4d6` · site `b4200ff2`). 서버 jest · tsc · eslint 는 돌리지 않았다 (아래 「(미검증)」 1).

### code-review (3 라운드)

- 지적 행 수 R1 16 · R2 19 · R3 11 (중복 합산 뒤). R2 는 흡수 15 · 부분 2 · 미해소 2. R3 는 흡수 9 · 이연 1 · 기각 1.
- 주요 흡수: allow-ip 5행 CRITICAL 의 표 정정 · 검사 #10 덤프의 스케줄러 해제 · 외부 env 격리 · `.env` 우회 차단(존재하면 exit 2) · 판정 정의(CarViewAdminGuard · ADMIN_ROLES) · 범위 키 행 단위 · 소비자 칸 경로 앵커 · known-guards · Express 와일드카드 다중집합 · (메서드, 경로) 유일성.
- 미해소 · 이연: 검사 #9 · #12 (위), 「무조건 공개 0」 HIGH (R2, 가드 없는 44행 · 개정 요청 2), 검사 #5 패턴 (개정 요청 5), 검사 #10 음성 분기가 exit 0 만 거른다 (MEDIUM 이연 · 백로그).

### 최종 리뷰 (fable · codex, 라운드 2)

- R1: 지적 8 · 흡수 6 · 부분 흡수 1 · 이연 1 · 기각 0. HIGH 2 중 1 흡수 (codex, 필수 토큰 판정 행). 나머지 1 은 부분 흡수 (fable, 접근 계약 어휘는 README 「어휘 대응」으로 처리하고 계획 본문의 「무조건 공개 0」 정정은 개정 요청 9).
- R2: 지적 4 · 흡수 3 · HIGH 1 미해소. 미해소 HIGH 는 범위 키 선언 공백이다. 자동 칸 465행 (「관리자 전사 · 미선언」 318 · 「미판정 (영업·기업 통과)」 104 · 「열람 토큰」 35 · 「미판정」 8). PRD 성공 지표 「범위 선언 없는 통과 행 0」 은 미충족 (개정 요청 10).
- codex 는 두 라운드 모두 응답했다 (ok).

### 사용자 몫 (코드 밖 · 결정 필요)

1. mccp 체인: `/mccp:prp-implement` 의 validate 가 plan receipt 부재로 rc 2. 2차 처리 5 의 (a) intent 우회 봉인 또는 (b) 현 상태 유지 중 택일이 필요하다. 구현은 (b) 상태로 진행됐다. 노트에 그 선택을 명시한 지시는 없다 (확인 필요).
2. 계획 개정 요청 1~10 (계획서 동결, 본문 미정정): 1 판정 정의 (CarViewAdminGuard · RolesGuard 전체 집합 → admin51 정의), 2 「무조건 공개 0」 주장 (가드 없는 44행), 3 검사 #9 외부 표지 파일 집합 (allow-ip 제외), 4 검사 #12 허용 집합 (백로그 · 보고서 · receipt), 5 검사 #5 패턴 확장, 6 소비자 종류 (앱 · 스케줄러 · 운영 도구) 와 책임자 (R19), 7 레거시 v1 열람 토큰 통과 정책, 8 Corp 가드 (CorpReadonlyGuard · CorpAnalysisViewGuard) 판정 포함 여부 (admin51 변동), 9 접근 계약 어휘를 PRD 여섯 범주로 정렬, 10 범위 키 465행 선언 주체.
3. #96 M8 · M9 의 stg 병합 (PRD R1) 과 병합 뒤 재동결 · 조상 대조. 정책 §1 의 대체 검증 상태다. M2 착수 전 필수.
4. R12 (#96 M8 Outcome 에 swagger UI 닫기 명시). M2 착수 전 필수. R19 (저장소 밖 소비자 · 책임자) 는 M9 전, R14 (운영 ALB 헤더) 는 M5 전.

### (미검증)

1. 서버 jest 기준선 39 · tsc 기준선 23 대조를 M1 에서 돌리지 않았다. 정책 §1 은 기준선 39 대조를 M1 의 몫으로 적었다. 노트는 서버 소스 무변경을 이유로 생략했다. M2 착수 전 필수 (정책 §5.2 · §5.10).
2. 실 stg 병합 뒤 #96 재동결과 조상 대조. 사용자 몫.
3. 접근 계약 여섯 범주 판정의 내용. 게이트는 어휘만 본다. 「판정만」 으로 바꾼 행 (4 · 24행 등) 의 런타임 재생은 없다. 가드 없는 78행의 본문 훑기는 정규식 한 겹이며 서비스 안은 전수 점검하지 않았다.
4. 소비자 칸은 문자열 흔적이다 (호출 증명 아님). 저장소 밖 소비자와 책임자 (R19) 는 미확인.
5. DB 없는 부팅 덤프는 main.ts 의 listen · 스웨거 · 전역 필터를 거치지 않는다. 판정에 영향이 있는지는 미확인. R12 swagger 행은 표지만 있다.
6. 정적 읽기만 한 항목: 레거시 v1 열람 토큰 통과 (`admin-view-token.guard.ts:33-47`), B2C 생년월일 단독 인증 (시도 제한 · 프록시 · WAF 미확인), `/admin/push/:pshId` IDOR (푸시 민감도 미확인), refresh 서명 미검증 (`auth.service.ts:654` · `auth.controller.ts:139`). 실측 없음 (백로그 관찰).
7. 범위 키 465행은 저자 선언이 아니라 자동 칸이다.

### 다음 마일스톤 (M2) 이 알아야 할 것

- M2 착수 조건 (PRD): #96 M8 · M9 병합 sha 가 착수 sha 의 조상 (R1, 사용자 몫), R12 명시. 둘 다 충족 전에는 M2 를 시작하지 않는다.
- 판정표 산출물은 `.claude/meta/2026-10-19-admin-rbac-m1/` 에 있다. `route-contract-table.md` (704행, 생성기 `build-table.mjs` 출력), `annotations.json`, `README.md`, `route-compare.mjs`, `known-guards.mjs`, `express-wildcard.list.txt` (70행), 동결본 2종 (`freeze-03ce7146` · `freeze-aff7ffcc`), `route-runtime-dump.cjs`, 도구 사본 3종 (digest 고정).
- 부팅 단언 (M2) 은 판정표 키와 런타임 메타데이터의 행 단위 대조를 재사용할 수 있다. `route-compare.mjs` 는 표 · 덤프 · Express 를 함께 대조하고 (메서드, 경로) 중복을 막는다.
- 덤프 함정: 외부 env 를 지워도 `.env` 가 있으면 dotenv 가 되살린다 (작업 디렉터리 · 서버 루트 확인, 있으면 exit 2). 스케줄러는 `app.init()` 뒤에 해제한다 (실측 interval 1 · cron 1). Express 와일드카드 70개는 nestjs-pino LoggerModule 미들웨어의 `forRoutes` 다.
- 판정 정의가 개정 요청 1 · 8 로 바뀌면 판정표와 계수가 같이 바뀐다. M2 는 개정 뒤의 판정표 digest 를 기준으로 삼는다.
- 기준선 계수는 PRD 의 「80 · 51」 을 통과 기준으로 하드코딩하지 않는다 (INV4). 현재 값은 03ce 80 · 38, aff 80 · 88 이다.

## 계획 게이트

계획서 `.claude/PRPs/plans/admin-rbac-m1.plan.md` 의 mccp 계획 게이트가 상한 3라운드를 다 쓰고 divergent 로 끝났다. 계획서는 동결했고 고치지 않았다. 리뷰 기록은 `.claude/reviews/plan-review-admin-rbac-m1.md`, 판정은 `.claude/state/plan-review/decision.json`, L3 결과는 `.claude/state/plan-review/l3-findings.json` 에 있다.

| 라운드 | 시각 (UTC) | 계획 해시 | 발견 | 흡수 | 기각 |
|---|---|---|---|---|---|
| R1 | 2026-10-19 01:50 | `sha256:6f9018dfa394…` | 패널 16건 (architect 3 · security 2 · test 6 · invariant 5) · L3 1건 · 자가 1패스 2행 | 18 | 1 |
| R2 | 2026-10-19 02:04 | `sha256:adcdf0790b9f…` | 발견 기록 없음. dispatch 기록만 남았고 원인은 미확인 | — | — |
| R3 | 2026-10-19 02:04 ~ 02:10 | `sha256:adcdf0790b9f…` | 패널 10건 (architect 2 · security 1 · test 4 · invariant 3) · L3 divergent (high 2건) | 0 (계획서 동결) | 0 |

R1 흡수 18건과 기각 1건(security LOW, 덤프가 부팅 환경의 비밀을 싣는다 — 덤프는 git 디렉터리 임시 경로에만 쓰고 커밋하지 않으므로 기각)은 `.claude/state/santa-ledger.md` 에 행으로 남아 있다. 이 파일은 gitignore 대상이다.

R3 의 L2 정족수는 4/4 응답했지만 blocking 4건(architect HIGH · FAIL, test HIGH · FAIL) 때문에 실패했다. 판정은 `review_source=multi-agent`, `review_proof=null`, `forwardCodexVerdict=false`, halt 단계 `5.2e`. L3 Codex 는 실행을 마쳤고 verdict 는 divergent 다.

## 처리 (1차, R3 직후)

1) 정상 receipt 경로가 없어 receipt 를 쓰지 않았다. (이 절은 아래 2차 처리로 대체된다) `--codex-verdict` 단독, `--review-mode` 생략, converged 위장, 감사 우회로 가드를 넘지 않았다.
2) 미해소 지적 12건을 `.claude/plans/codex-findings-backlog.md` 끝에 2026-10-19 날짜로 append 했다. 각 행에 file:line 근거와 이연 사유를 적었다. 최종 목록 9건에 invariant LOW 3건을 더해 12건이다. 마지막 리뷰의 미해소 지적 전부를 적으라는 규칙에 맞추려고 더했다.
3) 보고 문구는 「receipt 없음 — cap 도달, 반영분 미재검증」이다. 가장 무거운 미해소 지적은 architect HIGH 다. 개발 우회 칸이 helper import 기준이라 704행 전부가 `없음`이 되고, 검사 #7 · #10 이 이를 잡지 못한다.
4) 계획서를 고친 뒤 재진입할지는 사용자가 정한다.

## 처리 (2차, 사용자 지시 2026-10-19)

사용자 지시: 「Round 는 최대 3까지만 진행하고 마지막 라운드는 CRITICAL 이상 건이 아닌 이상 skip-receipt 로 진행 (환경 변수도 적용). HIGH 건은 backlog 에 기록해서 다음 plan 작성에 반영」. mccp 한시 규칙보다 우선한다.

1) CRITICAL 유무: 없음. 마지막 라운드(R3)의 최고 등급은 L2 architect · test 의 HIGH 와 L3 의 high 2건이다. FAIL 은 reviewer verdict 이고 등급이 아니다. 그래서 skip-receipt 경로로 갔다.
2) skip 사유 원문: 「사용자 지시 2026-10-19: 라운드 상한 3 의 마지막 라운드는 CRITICAL 이 없으면 skip-receipt 로 진행하고 HIGH 는 backlog 에 남겨 다음 계획에 반영」
3) backlog: `.claude/plans/codex-findings-backlog.md` 의 2026-10-19 행 12건은 1차 처리 2) 에서 이미 적혀 있었다. HIGH 4건(1465 · 1466 · 1467 · 1470행)에 `→ 다음 계획 반영 대상` 표지가 없어 같은 라운드 표지 보강 행 4건을 끝에 append 했다. 기존 행은 고치지 않았다. 다음 계획 작성에 넘기는 HIGH 는 다음과 같다.
   - 개발 우회 판정을 컨트롤러 import 가 아니라 가드 기준으로 (1465 · 1467, L3 plan:101 · architect Task 3 규칙 6)
   - AppModule 부팅 시 DB·Redis·스케줄러·외부 발송 격리 계약 (1466, L3 plan:110-114)
   - #10 음성에 유효 @Roles · 신원 종류 변이 추가 (1470, test plan:265-268)
4) receipt: 쓰지 않았다. `MCCP_SKIP_RECEIPT=1` 을 붙인 `cli.js write --gate mccp-plan-codex --decision admin-rbac-m1 --codex-verdict divergent --skip-reason …` 을 실행했으나 intent 게이트가 fail-closed 로 exit 2 였다 ("gate mccp-plan-codex is in scope for the intent gate but no intentDecision was supplied"). `MCCP_SKIP_RECEIPT` 는 이 검사 앞에서 적용되지 않는다. 우회 env `MCCP_SKIP_INTENT_GATE` 는 쓰지 않았다. 그 경로는 receipt 를 봉인하고 `validate --command mccp:prp-implement` 를 열어, 미해소 HIGH 4건이 남은 계획으로 구현 체인을 연다. 사용자 지시에 없는 가드 우회라 사용자 확인을 기다린다.
   - 확인 결과: `MCCP_SKIP_RECEIPT=1 … preflight --command mccp:prp-implement` → `ok:true · bypassed:true` (env 가 있으면 늘 bypass 로 읽힌다. receipt 가 있다는 증거가 아니다). `validate --command mccp:prp-implement` (env 없음) → `ok:false`, missing `mccp-plan-codex` 「no receipt written」, rc 2.
5) 사용자 결정 필요: (a) 「MCCP_SKIP_INTENT_GATE」 사유를 주고 skip 을 봉인해 구현 체인을 연다, 또는 (b) 계획서를 고치지 않고 두며 HIGH 4건을 다음 계획에 반영한다. 원장이 상한이라 같은 decision 으로 `/mccp:plan` 재진입은 막힌다.

## 구현 기록 (구현 당시 기록, 2026-10-19 · 현재 값은 아래 「## 코드 리뷰」 절)

커밋하지 않았다 (실행 지시: 커밋 전에 별도 세션에서 code-review). 부모 저장소의 변경은 삭제 1건 (`.claude/PRPs/plans/admin-rbac-m1.gates.sh`, 계획 Task 1 의 사이드카 제거) 과 새 디렉터리 `.claude/meta/2026-10-19-admin-rbac-m1/` 뿐이다. 서브모듈 3곳은 건드리지 않았다 (워킹트리 clean). 판정표 생성 · 대조 · 런타임 덤프는 서버 소스를 읽기만 한다.

### 기준선 계수 (정책 문서 §2, 검사 `#11` 이 이 줄을 `--stats` 출력과 대조한다)

```
03ce noauth=80 admin51=38
aff noauth=80 admin51=88
```

- 03ce 는 착수 트리 `03ce7146` (로컬 병합 기준), aff 는 핫픽스 병합 전 `aff7ffcc`. 라운드 2 에서 판정 정의를 고쳐 admin51 이 1 · 51 에서 38 · 88 로 바뀌었다 (아래 「## 코드 리뷰」 라운드 2 참고).
- 행 704 · 클래스 126 · 가드 종류 auth 624 · authz-only 2 · ratelimit 6 · optional 6 · none 66 은 두 트리가 같다.

### 착수 조건 처리 (정책 §2 의 M1 행, 실행 결과)

| 조건 | 처리 | 실행 결과 |
|---|---|---|
| refactor M3 측정 도구 공유 | satisfy-now | 세 파일을 바이트 복사. sha256 이 고정값과 같다 (검사 `#2` 통과). 도구 신규 작성 없음 |
| #96 M9 병합 뒤 재동결 | substitute | 로컬 병합 기준 두 트리 재계산 (위 계수). 실제 stg 병합 뒤 재동결은 (미검증) |
| 런타임 라우트 ↔ 판정표 독립 대조 | satisfy-now | 검사 `#10` 통과 (704 · 704 · Express 704 중 와일드카드 70 은 미들웨어) |
| 기준선 80 · 51 | satisfy-now | 산식으로 재계산. 03ce 는 1 (계획 시점 스크래치 2 와 다름, 아래 「구현 편차」 1) |
| 저장소 밖 소비자 · 책임자 | defer-unverified | 판정표 책임자 칸은 코드 근거 없이 「미확인(R19)」 로 둔다 |

### 검사 결과 (계획서 Validation 블록을 `bash` 로 전부 실행, 모든 검사 번호)

| 검사 | 결과 | 한 줄 |
|---|---|---|
| `#1` 착수 sha 고정 | 통과 | gitlink 3곳 · 두 병합 조상 · 트리 `b2acda28` |
| `#2` 도구 사본 digest | 통과 | 세 파일 sha256 이 고정값과 같고 원본과 cmp 0 |
| `#3` 도구 자가 검사 | 통과 | selftest 가 사본 `clones.mjs` · `route-guards.mjs` 둘 다 호출 |
| `#4` 동결본 재계산 | 통과 | 두 트리 `--list` 가 동결본과 cmp 0, 행 수 704 = handlers |
| `#5` 미지 가드 · 무조건 공개 | 통과 | other 0 (두 트리), `@Public(` · `isPublic` 0 |
| `#6` 컨트롤러 표본 범위 | 통과 | `*.controller.ts` 밖의 `@Controller` 0 |
| `#7` 생성기 출력 · 형상 | 통과 | 재생성 = 커밋 표, 행 키 = 동결본, 빈 칸 0, 어휘 안 |
| `#8` 신원 없음 행 | 통과 | 비 auth 키 집합 cmp 0, 비 auth 행 비고 전부 채움 |
| `#9` 외부 표지 파일 | 통과 | money-public 2 · coocon-bypass 1 · allow-ip-admin 5, 외부 표지 칸과 일치 |
| `#10` 런타임 대조 | 통과 | `route-compare ok rows=704 runtime=704 express=704 wildcard=70`. 음성 두 사본 모두 exit 1 |
| `#11` 기준선 줄 | 통과 | 위 「기준선 계수」 두 줄이 `--stats` 출력과 글자까지 같다 (이 절 작성 뒤 확인) |
| `#12` 범위 · 코드 영향 0 | 붉음 (기대 불일치) | 서브모듈 clean. 허용 집합 밖 경로 세 개: `.claude/plans/codex-findings-backlog.md` (계획 단계에서 mccp 규칙에 따라 커밋된 append), 구현 보고서 `.claude/PRPs/reports/admin-rbac-m1-report.md`, implement receipt `.claude/receipts/mccp-implement-codex/admin-rbac-m1.json` (「구현 편차」 3) |
| `#13` 호스트 경로 | 통과 | 계획서 · meta · 노트에 홈 · 루트 · 사용자 임시 경로 문자열 0 |

### 음성 실행 기록 (구현 때 실행, 검사 본문의 음성 표지 대응)

| 검사 | 사본 | 관찰 |
|---|---|---|
| `#2` | `route-guards.mjs` 끝에 한 글자 추가 | sha256 이 `a86b96eda67cf995…` 로 바뀌어 고정값 `c0eedee9…` 와 다르다 |
| `#3` | `clones.mjs` 를 뺀 사본 | `selftest.sh` exit 1 |
| `#4` | 동결본 첫 행을 뺀 사본 | cmp 가 붉다 (rc 1) |
| `#5` | `MysteryGuard` 한 줄만 둔 임시 저장소 | `byKind.other` 1 |
| `#7` | 판정표 첫 행 범위 키 칸 비움 | 빈 칸 수 1 |
| `#8` | 가드 종류 두 행 가운데 하나를 `auth` 로 바꾼 표 | 비 auth 키 대조 cmp rc 1 |
| `#9` | money-public 의 외부 표지 칸 하나를 `-` 로 | 파일별 대조 1 ≠ 2 로 붉다 |
| `#10` | 표 첫 행 뺀 사본 · 가짜 행 `/__stray` 더한 사본 · Express 전용 `/untracked/*` 더한 덤프 | 셋 다 exit 1 (`check10` 본문이 두 표 사본을 돈다). 탐지 증거는 exit 1 이 아니라 stderr 문구다 (라운드 3 실측, 「## 코드 리뷰」 절) |
| `#11` | 노트 줄 `admin51=51` 을 `50` 으로 바꾼 사본 | 줄 대조 rc 1 |
| `#13` | 홈 경로가 든 파일 | grep rc 0 이라 검사가 붉어진다 (빈 결과 통과 금지 의도대로) |

### 구현 편차 (계획과 다르게 간 자리)

1. 기준선 admin51 은 계획 시점 스크래치 값 2 와 다르다 (03ce). 같은 산식 (Tasks 3 규칙 4) 을 적용하면 1 이다. 1 행은 `apps/api/src/modules/menu/menu.controller.ts:30` (관리자 토큰 · 가드 `AdminJwtAuthGuard` 한 개). 두 번째 행은 이 산식에서 나오지 않았다 (미확인). 노트의 줄은 구현 계수 1 을 적었다. 계획 본문 값은 바꾸지 않았다 (계획서 동결).
2. 개발 우회 칸이 704행 전부 `없음` 이다. 규칙 6 이 컨트롤러 파일의 `helpers/request.helper` import 를 본다. 실제 우회 스위치는 가드 파일 (`auth/user/jwt.guard.ts`, `auth/admin/jwt.guard.ts`, `auth/user/jwt-optional.guard.ts`, `subscribe/subscribe/subscribe.service.ts`) 에 있다. 이 칸은 판정 근거로 쓰지 않는다. 계획 리뷰 기록의 architect HIGH 가 구현에서 그대로 재현됐다 (`.claude/PRPs/plans/admin-rbac-m1.plan.md` 규칙 6 · 리뷰 기록 R1 · 백로그 행 「개발 우회 판정을 가드 기준으로」 → 다음 계획 반영 대상). 검사 `#7` · `#10` 은 이를 잡지 못하는 것이 맞다 (계획 리뷰가 이미 지적). 동결된 계획을 따랐다.
3. 검사 `#12` 허용 집합은 두 경로를 싣지 않는다. (가) `.claude/plans/codex-findings-backlog.md`: 계획 단계의 mccp 게이트 기록이 이 파일에 행을 append 했고 그 커밋 (`97beddf` 이전) 이 `d30f777..HEAD` 에 들어 있다. mccp 한시 규칙이 그 append 를 요구하므로 되돌리지 않았다. (나) `.claude/PRPs/reports/admin-rbac-m1-report.md`: 본문 지시에 따라 구현 보고서를 쓰고 커밋하지 않는다. (다) `.claude/receipts/mccp-implement-codex/admin-rbac-m1.json`: mccp implement receipt (본문 2.5.6). 허용 집합 변경은 계획서 Validation 을 바꾸므로 하지 않았다. 검사 `#12` 는 `set -e` 에서 여기서 멈추므로 `#13` 은 같은 스크립트 실행으로는 도달하지 않는다. `#13` 은 같은 grep 을 따로 돌려 rc 1 (빈 결과) 을 확인했다. 두 경로 모두 코드 영향 0 을 깨지 않는다 (서브모듈 워킹트리 clean, 서버 소스 미변경).
4. 생성기에 `--skeleton` 모드를 추가했다 (주석 초안 JSON 출력). 계획서는 `--stats` 와 표 출력만 적었다. 주석 파일 `annotations.json` 은 이 모드의 출력을 저자가 검토해 둔 것이다. 소비자 칸은 두 프런트 디렉터리에서 경로 문자열을 찾은 결과다 (호출 흔적). 범위 키는 클래스 가드 이름 기준의 선언이다.
5. 외부 Basic 신원 (`ClientBasicAuthGuard`, 23행 `client/client/client.controller.ts`) 의 접근 계약을 「외부 신원 표지」 로 두었다. 계획 규칙 5 는 이 경우를 명시하지 않는다. 같은 범주 안에서 외부 표지 칸에 `외부 Basic (ClientBasicAuthGuard)` 를 적었고 소비자는 `미확인(R19)` 이다. 검사 `#9` 의 파일 셋은 바꾸지 않았다.
6. DB 없는 부팅 (`route-runtime-dump.cjs`): 생성자가 죽지 않도록 `FCM_PRIVATE_KEY` · `FCM_CLIENT_EMAIL` · `FCM_PROJECT_ID` 를 비어 있을 때만 채운다. RSA 키는 실행 때 메모리에서 만들고 파일에 쓰지 않는다. 부팅 중 표준출력 글은 표준오류로 돌렸다. 계획서는 이 부분을 적지 않았다.
7. Express 와일드카드: 덤프에 `/*` 라우트 70개 (메서드별) 가 있다. 원인은 nestjs-pino 로거 미들웨어의 `forRoutes` 이며 덤프의 `middleware` 항목 (forRoutes `/*`) 으로 확인된다. `route-compare.mjs` 는 미들웨어가 선언한 와일드카드 경로에 속한 것만 허용한다 (`Codex 구현 리뷰` F2 에 따라 고쳤다).
8. 계획 Task 1 의 사이드카 `.claude/PRPs/plans/admin-rbac-m1.gates.sh` 는 커밋 없이 워킹트리에서 지웠다 (git 상으로 삭제 한 건).

### 미검증 (이 실행에서 확인하지 않은 것)

- 접근 계약 여섯 범주의 판정 내용 (게이트는 어휘만 본다) — 저자 판정, 리뷰 몫.
- 실제 stg 병합 뒤 #96 재동결과 조상 대조 — 사용자 몫 (정책 §1).
- 저장소 밖 소비자와 책임자 (R19) — 판정표 책임자 칸은 「미확인」.
- 운영 · stg 실 트래픽의 would-deny 와 실제 ALB 헤더 (M5 몫, 이 마일스톤과 무관).
- DB 없는 부팅은 이 저장소 `main.ts` 의 전역 설정을 거치지 않는다 (listen · 스웨거 · 전역 필터 미호출). 그 설정이 판정에 영향을 주는지는 확인하지 않았다.
- 개발 우회 칸의 실제 내용 (편차 2).
- 검사 `#12` 는 보고서 경로 때문에 붉다 (편차 3). 허용 집합 밖 변경은 그 보고서 하나다.

### 실행 순서 편차 (Phase 2.5 가 Phase 3 뒤에 돌았다)

구현 본문은 Phase 2.5 (Implement-Codex) 를 Phase 3 (EXECUTE) 앞에 둔다. 이 실행은 계획 Task 를 먼저 구현한 뒤 2.5 를 돌렸다. 그래서 Codex 는 완성된 산출물을 검토했고, 2.5.2 의 결정 목록은 구현 뒤에 정리했다. 2.5.0 의 봉인 (`codex-policy seal`, `review-rounds seal`, cap 3) 은 구현 전에 하지 않았다. 판정 근거는 아래 「## Codex Implementation Review」 에 있다.

## Codex Implementation Review

- 호출: `node <mccp 플러그인>/scripts/lib/codex-invoke.js adversarial-review` (fail-closed wrapper), `--timeout-ms 900000 --json --impeccable-available`. 초점: 판정표 생성기 · 대조 · 덤프 · 계획 Validation 의 구현 정확성. 서버 소스는 읽기 전용 대상.
- 라운드 수: 1 (상한 3 의 1 라운드). 2 라운드는 열지 않았다 (본문 규칙: 최고 등급이 CRITICAL · HIGH 가 아니므로 escalate 조건 불충족).
- 구조화 판정: `needs-attention` → gate verdict `divergent` (mccp `deriveGateVerdict`, source structured). 소요 67초.
- 합치 결론: 두 medium 지적을 흡수했다. CRITICAL 없음. 판정은 divergent 로 기록한다 (converged 위장 없음).

| 지적 | 등급 | 판정 | 근거 · 처리 |
|---|---|---|---|
| F1 `README.md` 가 노트의 구현 편차 · 기준선 줄을 적었다고 했지만 노트에 없어 검사 `#11` 이 실패 | medium | ACCEPT_NOW (흡수) | 노트에 「## 구현 기록」 과 `03ce noauth=80 admin51=1` · `aff noauth=80 admin51=51` 두 줄을 적었다. `#11` 재실행 통과 |
| F2 `route-compare.mjs` 가 `*` 를 포함한 모든 Express 경로를 미들웨어로 간주해 Express 전용 `GET /untracked/*` 음성을 통과시켰다 (`wildcard=71`) | medium | ACCEPT_NOW (흡수) | 덤프에 `middleware` 항목 (`app.middlewareContainer.getConfigurations()` 의 forRoutes) 을 추가하고, 와일드카드는 미들웨어가 선언한 경로 (`/*`) 와 같을 때만 허용. 같은 음성 입력은 exit 1 (재실행 확인). 흡수 근거는 편차 7 |

- 흡수 뒤 재검토는 하지 않았다 (본문 규칙). 판정은 흡수 전 상태의 divergent 를 그대로 남긴다. 다음 개정 · 별도 세션 code-review 에서 다시 본다.
- 백로그: 이 실행에서 연기된 항목은 없다. 개발 우회 칸 (편차 2) 은 계획 리뷰가 이미 백로그에 올린 HIGH 이므로 새로 append 하지 않았다.
- Implement receipt: `.claude/receipts/mccp-implement-codex/admin-rbac-m1.json`, `codex_verdict=divergent`, `round=1`. receipt 의 `resolution.converged` 는 CLI 가 `true` 로 적었다 (findings 0 기준). 게이트 판정은 `codex_verdict` 필드다. 이 두 필드가 다르다는 사실을 여기 적는다.
- 게이트 read-back (Phase 2.5.7): `receipt validate --command mccp:prp-implement` 는 `MCCP_SKIP_RECEIPT=1` 을 붙여도 rc 2 다 (`missing: mccp-plan-codex, no receipt written`). `preflight` 는 같은 env 에서 `bypassed:true` 로 통과한다. 즉 env 는 preflight 만 우회하고 validate 는 우회하지 않는다. 계획 게이트 receipt 는 없고 intent 게이트 우회 env (`MCCP_SKIP_INTENT_GATE`) 는 사용자 지시로 쓰지 않는다. 따라서 Phase 2.5.7 은 통과하지 못했다 (미해결, 사용자 몫).

## 코드 리뷰 (라운드 1 · 2 · 3, 2026-10-19)

커밋 전 워킹트리 리뷰(code-review · security · typescript 세 출처)의 지적을 판정했다. 중복 지적은 합쳤다 (원 지적 CRITICAL 1 · HIGH 5 · MEDIUM 7 · LOW 3 가 중복 합산 뒤 고유 16건. 아래 행이 그 16건이다). 계획서는 고치지 않았다. 생성물 (`route-contract-table.md`) 과 생성기 · 주석 · 덤프 · 대조 스크립트 · README 를 고쳤다. 도구 사본 (`route-guards.mjs` · `clones.mjs` · `selftest.sh`) 은 digest 고정 (검사 `#2`) 이라 건드리지 않았다.

| 라운드 | 출처 | 심각도 | 지적 | 판정 | 증거 | 층위 |
|---|---|---|---|---|---|---|
| 1 | code-review · security · typescript | CRITICAL | `allow-ip-admin` 5행 (`/admin/ip` 조회 · 등록 · 수정 · 삭제) 이 가드 없는 쓰기인데 「외부 신원 표지」 로 분류돼 있었다 | 흡수 (표 정정). 접근 계약 「신원 없음·선택」, 외부 칸 「외부 호출자 후보 · 소비처 미확인 (R19) · 가드 없음」. 단 검사 `#9` 는 계획서 동결로 이 파일을 외부 표지로 고정해 붉다. 계획 개정 전까지 미해소 | `allow-ip-admin.controller.ts:15-16` 가드 주석 처리, 표 103~107행, 검사 `#9` 실행 「FR=5 EX=0」 | 2 |
| 1 | code-review | HIGH | 검사 `#10` 덤프가 `AppModule` 을 올려 스케줄러 · 외부 env 가 살아 있었다 | 흡수. 외부 연결 env 를 지우고 DB · Redis 를 닫힌 포트로 돌리고, `app.init()` 직후 `SchedulerRegistry` 의 interval · timeout · cron 을 전부 해제. 등록 수를 stderr 에 남긴다 | 반사실: 해제 전 등록 `intervals=1 timeouts=0 crons=1` (`alimtalk` 10초 틱 · 예약 푸시). 수정 뒤 `handlers` · `express` JSON 이 이전 덤프와 같다 (704 · 704). `.env` 없음 확인 | 5 |
| 1 | code-review | HIGH | `kindOf` 부분문자열이 미지 가드를 optional 로 흡수 (`AnalysisTargetGuard` · `PushDetailReaderGuard`) | 흡수 (부분). 도구 kind 칸은 digest 고정이라 못 고친다. 대신 `route-compare.mjs` 에 판정 가드 대조를 넣었고, 판정 가드 행은 접근 계약을 「판정만」 으로 적었다 | 런타임 가드 집합에서 도구 분류 밖 이름 확인 (`AnalysisTargetGuard` 5행, `PushDetailReaderGuard` 1행, 나머지는 rate-limit · body 가드로 기존 분류와 맞다). 옛 표로 대조하면 6건 붉고 새 표는 ok | 6 |
| 1 | code-review · typescript | HIGH | `/admin/push/:pshId` 를 「없으면 비회원」 으로 적어 가렸고, 회원 access 로 임의 `pshId` 를 읽을 수 있다 (IDOR 후보) | 표 부분 흡수 (판정만 · 「IDOR 후보」 비고). IDOR 자체는 서버 코드라 M1(코드 영향 0) 밖이다. 미해소, 백로그 이월 | `push.controller.ts:134-146`, `push.service.ts:378-385` (리뷰 인용, 이 노트에서 재확인하지 않았다). 민감도 미확인 | 1 |
| 1 | security | HIGH | 개발 우회 칸이 704행 전부 「없음」 (실제 스위치는 가드 쪽) | 흡수. 칸을 가드 이름에서 정한다 (`AdminJwtAuthGuard` · `JwtAuthGuard` · `JwtOptionalGuard`). 571행 「가드 측 스위치 (env 미확인)」, 133행 「없음」. env 켜짐 여부는 미확인 | 분포 재계수 (칸 값 2종). 구독 면제 (`subscribe.service.ts:443`) 는 가드 칸에 없어 표시 안 됨 | 6 |
| 1 | code-review | HIGH | 검사 `#12` 가 붉다 (허용 집합 밖 3경로: 백로그 append · 구현 보고서 · implement receipt) | 미해소 (사용자 결정). 허용 집합은 계획서 Validation 에 있어 바꾸지 않았다. 산출 위치 이동은 mccp 기록 규칙과 충돌한다 | 재실행 `out-of-scope` 3행 (`.claude/plans/codex-findings-backlog.md`, `.claude/PRPs/reports/admin-rbac-m1-report.md`, `.claude/receipts/mccp-implement-codex/admin-rbac-m1.json`) | 5 |
| 1 | code-review | MEDIUM | `car-basic` · `car-detail` (B2C, 표 159·160행) 에 신원을 세우는 가드가 없어 전건 403 가능성 | 흡수 (비고). 「이 라우트에 req.admin 을 세우는 가드가 없다 (정적, 전건 403 가능성 · 런타임 미확인)」. 기능 단절 여부는 미확인, 백로그 | `analyze.controller.ts:478-482`, `:527-531`, `auth/admin/car-view-admin.guard.ts:31,94-96` | 1 |
| 1 | code-review | MEDIUM | 핸들러 안 `assertB2cAuth` 로 쿠키를 판정하는 `none` 행이 「신원 없음·선택」 으로 적혔다 | 흡수. 생성기가 `none` 이고 본문에 `assertB2cAuth(` 가 있으면 접근 계약을 「판정만」 으로 두고 비고에 적는다 | 표의 판정만 24행 (authz-only 2 · 판정 가드 6 · 쿠키 판정 16). 근거 `analyze.controller.ts:314`, 본문 탐지 `bodyOf` | 8 |
| 1 | code-review | MEDIUM | 노트 기준선 `03ce admin51=1` 이 계획 시점 값 2 와 다르고 원인이 미확인 | 부분 흡수. 2 를 재현하지 못했다는 증거를 적었다. `AdminJwt`+`RolesGuard` 448행은 전부 `@Roles` 인수가 비어 있지 않다 (빈 `@Roles()` 는 `git grep` 0건). `AdminJwt` 에 역할 가드가 없는 1행이 `menu.controller.ts:30`. 두 번째 행은 미확인 | 항목 수 재계수 (`freeze-03ce7146.list.txt`), `git grep -E '@Roles\(\s*\)'` 0건. 값은 관찰 기록이며 통과 기준이 아니다 (INV4). 검사 `#11` 은 여전히 노트 줄과 `--stats` 출력을 비교만 한다 | 7 |
| 1 | security | MEDIUM | 범위 키 칸이 클래스 가드 한 줄로 정해져 `Corp*` 행 103개가 「관리자 전사 (범위 미적용)」 으로 적혔다 | 흡수. 행 단위 `rowScope` 로 바꿨다. 「범위 미적용」 단정은 없앴다 | 분포: 기업 범위 89 · 미판정 (영업·기업 통과) 35 · 관리자 전사 (키 미선언) 324 · 열람 토큰 35 · 외부 Basic 24 · 없음 66 · 회원 본인 99. 옛 칸 「관리자 전사」 451 → 324 | 1 |
| 1 | security · typescript | MEDIUM | 신원 칸이 복수 신원 (`AdminOptionalGuard` 로 관리자 쿠키 · admin-view 도 세움) 을 회원 토큰 하나로만 적음 | 부분 흡수. 신원 칸 어휘는 검사 `#7` 이 고정하므로 칸 값은 그대로 두고 비고에 「복수 신원 후보」 표지를 넣었다 (AdminOptional 행 전부) | `admin-optional.guard.ts:36,53`. 칸 값 구조 변경은 계획 개정 사항 | 1 |
| 1 | security · typescript | MEDIUM | 소비자 칸이 클래스 접두 일치만 보고 채워졌고 (`/advertisement/increse` 는 관리자 쪽에 문자열이 없다), 빈 접두 컨트롤러 (`organization-admin`) 는 「미확인」 이었다 | 흡수. 경로의 고정 세그먼트가 순서대로 나오면 「(경로 일치)」, 접두만 나오면 「(접두만 · 후보)」. 빈 접두도 경로로 찾는다 | `/advertisement/increse` → 사이트 프런트 (경로 일치). `/auth/fcm-token` → 두 프런트 (접두만 · 후보). `organization-admin` 6행 → 관리자 프런트 (경로 일치) | 2 |
| 1 | security | LOW | 검사 `#10` 이 접근 계약 칸을 런타임과 대조하지 않는다 | 흡수. `route-compare.mjs` 에 두 검사 추가 (판정 가드 행은 「신원 없음·선택」 이 아니어야 함, 관리자 토큰 행은 판정 계산과 계약이 일치해야 함) | 반사실: 옛 표를 새 대조에 넣으면 판정 가드 6행이 붉다. 새 표는 ok (704 · 704 · 704 · wildcard 70). 판정 계산 누락 음성은 따로 돌리지 않았다 | 7 |
| 1 | security | LOW | DB 없는 부팅이 외부 연결 env 를 거르지 않는다 | 흡수 (위 HIGH 스케줄러 행과 같은 수정). env 제거 · 닫힌 포트 · 스케줄러 해제 | 위 HIGH 행의 실측 | 5 |
| 1 | code-review | LOW | 판정표 `converged=true` 와 `codex_verdict=divergent` 가 한 receipt 에 공존 | 기각 (기록 완료). receipt 는 mccp CLI 가 쓰는 산출물이라 이 저장소에서 고치지 않는다 (B1). 「구현 기록」 Codex 절에 converged 는 CLI 기준이고 게이트 판정은 `codex_verdict` 라고 이미 적었다. 사용자 결정 대기 상태도 같은 절에 있다 | `.claude/receipts/mccp-implement-codex/admin-rbac-m1.json`, 「구현 기록 · Codex 절」 | 1 |
| 1 | security | MEDIUM | 요약 없음 · 외부 신원 이름 누락을 「통과」 로 본 항목 (`loss-analysis` 6행 등). 입력 원문이 중간에 잘려 나머지 주장을 확인하지 못했다 | 기각 (근거 불충분). 「요약 없음」 은 비고에 그대로 드러나 있고, 검사 `#8` 은 비고가 비었는지만 본다. 외부 신원 이름 누락은 CRITICAL 행과 같은 표지 칸 문제로 처리했다. 잘린 원문은 다시 받아야 판단할 수 있다 | 표 `loss-analysis.controller.ts` 행 (비고 「요약 없음」 표시) | 2 |
| 2 | code-review | HIGH | CarViewAdminGuard 판정 행이 관리자 신원만으로 분류됐다. 관리자 열람 토큰 6행 (118·119·202·203·255·256) 과 관리자 토큰 4행 (126·127·264·265) | 흡수. 판정 정의에 CarViewAdminGuard 를 넣었다 (`canViewCarWith` 가 403 을 낸다). 12행 전부 「관리자 판정」, 비고에 차량 축 표지. 계획 Task 3 규칙 4 는 동결이라 개정을 계획 개정 요청으로 넘긴다 | 반사실: 옛 표를 새 대조에 넣으면 「관리자 계약 불일치」 (역할 집합 37행과 합쳐 43행, rc 1). 옛 대조기는 옛 표에 rc 0. 새 표 · 새 대조 rc 0. `car-view-admin.guard.ts:27-45` | 1 · 7 |
| 2 | code-review | HIGH | 「무조건 공개 0」 근거가 틀렸다. 가드가 없는 라우트 44행이 판정 없이 「신원 없음·선택」 칸에 있다. 게이트 #5 는 `@Public` 표지만 센다 | 미해소. 계획 NOT Building 345행 · Acceptance 320행 · 검사 #5 는 동결이라 고치지 않았다 (계획 개정 요청, 사용자 결정). 표는 가드 없음 · 판정 없음을 비고에 그대로 드러낸다 | 표 가드 종류 `none` · 접근 계약 「신원 없음·선택」 44행. PRD 143행 (여섯 범주의 하나) | 2 |
| 2 | security | MEDIUM | 역할 전체 집합 (admin · corp · sales) 을 가진 37행이 「관리자 판정」 이었다. 이 역할 제한은 AdminJwtAuthGuard 를 통과한 누구도 거르지 못한다 | 흡수. `ADMIN_ROLES` 를 jwt.guard.ts 에서 읽고, RolesGuard 역할 집합이 전체를 덮으면 판정이 아니다. 37행은 「관리자 신원만」 이 됐다. admin51 은 03ce 1 → 38 (구 정의 값은 노트 기준선 절에 남긴다). 계획 규칙 4 개정 요청에 포함 | `jwt.guard.ts:14` (`ADMIN_ROLES`), `roles.guard.ts:27`. 반사실: 옛 표 + 새 대조에서 역할 전체 집합 행이 「관리자 계약 불일치」 (수는 위 HIGH 와 합산) | 1 |
| 2 | security | MEDIUM | 범위 키가 가드 이름을 먼저 봐서 영업 · 기업 통과 행이 「기업 범위」 로 적혔다 (89행). 관리자 전용 행도 「기업 범위」 로 적혔다 (21행) | 흡수. 영업 · 기업 통과를 가드 이름보다 먼저 판정한다. 관리자 전용 행은 `없음 (관리자 전용 · 범위 키 미선언)`. 「기업 범위」 칸은 없앴다 | 반사실: 옛 칸 「기업 범위 (Corp 가드 선언 …)」 89행 → 새 표 0행. 「미판정 (영업·기업 통과)」 35행은 `sagId · agcId · orgPath 미판정 (영업·기업 통과)` 104행으로 늘었다 | 2 |
| 2 | code-review | MEDIUM | 범위 키 칸이 PRD M1 어휘 (sagId · agcId · orgPath · 없음 · 해당 없음) 를 쓰지 않았고, 소비자 종류 (앱 · 스케줄러 · 운영 도구) 가 없다 | 부분 흡수. 범위 칸을 PRD 어휘로 바꿨다. 소비자 종류는 코드에서 식별하지 못했다 (스케줄러는 HTTP 라우트가 아니고, 앱 · 운영 도구의 호출 표지가 소스에 없다). 이 부분은 미해소로 남기고 사용자 몫 (R19) | 범위 칸 새 어휘 9종 (재계수). 소비자 칸은 프런트 두 갈래 · 「미확인(R19)」 만 있다 | 2 |
| 2 | security | MEDIUM | B2C 생년월일 인증 (`analyze.controller.ts:569`) 은 6자리 생년월일이 유일한 요소이고 시도 제한이 없다. 성공 시 b2c_auth 쿠키로 assertB2cAuth 판정 행 16개가 열린다. 표는 「가드 없음」 만 적었다 | 흡수 (비고 · 노트 한계 · 백로그). 서버 수정은 M1 밖 (코드 영향 0) 이라 보안 핫픽스 항목으로 넘긴다. 프록시 · WAF 의 시도 제한은 확인하지 못했다 (미검증) | 표 569행 비고 · 16행 개수는 `assertB2cAuth` 표 재계수. 리뷰 인용 `analyze.controller.ts:569-620` 은 이 라운드에서 재읽지 않았다 | 2 |
| 2 | code-review | MEDIUM | 외부 신원 표지가 빠졌다. 본인인증 결과 수신 (`mok-auth.controller.ts:21`) 과 알림톡 조회 3행 (`alimtalk.controller.ts:40·51·70`) 이 외부 칸 「-」 이었다 | 흡수. 주석에 외부 칸과 접근 계약 「외부 신원 표지」 를 넣었다. 검사 #9 는 세 파일 고정이라 그대로 두었다 (계획 동결). 「clients/*」 는 반박: `modules/client/` 62행 중 23행은 ClientBasicAuthGuard 로 이미 「외부 신원 표지 · 외부 Basic」 이다. 나머지는 관리자 가드 | 외부 칸이 「-」 였던 행 중 4행 (mok 1 · alimtalk 3) 이 외부 표지가 됐다. `freeze-03ce7146.list.txt` 의 `modules/client/` 행 종류 재계수 | 2 |
| 2 | code-review | MEDIUM | 검사 #10 진단의 방향 라벨이 뒤집혔다 (표에만 · 런타임에만, Express 에만 · 메타데이터에만) | 흡수 (인자 순서 교정, Express 쪽도 같이) | 반사실: 표에서 `GET /live` 행을 뺀 사본. 옛 대조기 「표에만 있음: GET /live」 (실제는 런타임에만). 새 대조기 「런타임에만 있음: GET /live」. 표에 `/__stray` 를 더한 사본은 「표에만 있음: GET /__stray」. 두 사본 모두 rc 1 | 5 |
| 2 | code-review | MEDIUM | 구현 보고서가 검사 #9 를 Pass 로, 개발 우회 칸을 704행 전부 「없음」 으로 적었다 | 흡수. 보고서의 검증 결과 · 편차 · 요약을 현재 상태로 고쳤다 (#9 Red · #12 Red · 개발 우회 「가드 측 스위치」 571 · 「없음」 133). 보고서는 허용 집합 밖이라 검사 #12 의 기존 빨강에 그대로 든다 | `.claude/PRPs/reports/admin-rbac-m1-report.md` 검증 결과 표 | 2 |
| 2 | typescript | MEDIUM | 검사 #10 이 가드 이름 · 가드 종류 · 개발 우회 칸을 런타임과 대조하지 않는다. Corp · Own · Car · AdminOptional 가드 124행을 지운 덤프에서 옛 대조기가 rc 0 이다 | 부분 흡수. 동결본 가드 이름 집합을 런타임 가드 이름과 행 단위로 맞추고, 그에서 나온 가드 종류 · 개발 우회 칸도 대조한다. 범위 · 「판정만」 · 외부 칸 내용은 대조하지 않는다 (미해소, README 한계에 적음) | 반사실: 가드 124행 제거 덤프. 옛 대조기 rc 0 · 새 대조기 rc 1 (「가드 대조 불일치 표 124 · 런타임 124」). 원본 덤프 새 대조기 rc 0 | 6 · 7 |
| 2 | 검사 #10 확장 중 발견 | MEDIUM | 동결본 파서가 `treat-provider-admin.controller.ts:121` 의 `@UseGuards(SuperAdminGuard)` 를 놓쳤다. 그 위 `//*` 주석 줄에서 파서가 멈춘다 | 흡수. 주석 `guardsAdd` 를 `tree` (03ce7146) 에 한정해 보정하고 근거를 비고에 적었다. 도구 `route-guards.mjs` 는 digest 고정이라 고치지 않았다. 같은 형상의 다른 행은 런타임 대조에서 어긋나지 않았다 | 반사실: `guardsAdd` 를 뺀 사본. 새 대조기 rc 1 (「가드 대조」 3줄). 보정 뒤 rc 0 | 5 |
| 2 | code-review | MEDIUM | 레거시 v1 토큰은 req.admin 을 세우지 않고 통과한다 (`admin-view-token.guard.ts:33-47`). 차량 축 판정 행의 가드 표지가 비고에 없었다 | 흡수 (비고 표지). 레거시 표지는 AdminViewTokenGuard 행 35개, 차량 축 표지는 CarViewAdminGuard 행 12개. 레거시 통과 자체의 정책 판단은 계획 개정 요청에 넘긴다 | 표 비고 재계수 (grep). 요청 재생은 하지 않았다 (정적 읽기) | 2 |
| 2 | security · typescript | LOW | 덤프 env 격리가 접두 denylist 라 서버가 읽는 외부 연동 키 (`GOOGLE_CHAT_WEBHOOK_URL` · `MAILER_*` · `CODEF_*` 등) 가 남는다. 모듈 import 가 env 를 먼저 읽을 수 있다 | 흡수. allowlist 만 남기고 AppModule import 전에 지운다. 부팅 결과는 같다 (handlers 704 · express 774 · middleware 1, 스케줄러 해제 intervals=1 timeouts=0 crons=1) | 실측: 새 덤프 704 · 774 · 1. allowlist 밖 키는 셸 값과 무관하게 전부 지워진다 | 5 |
| 2 | typescript | LOW | 빈 판정표 · 빈 덤프가 rc 0 으로 통과한다 | 흡수 (빈 입력은 exit 2) | 반사실: 빈 표 · 빈 덤프. 옛 대조기 「ok rows=0」 rc 0, 새 대조기 rc 2 | 5 |
| 2 | code-review | LOW | 주석 파일의 미사용 키가 조용히 무시된다 | 흡수 (미사용 키가 있으면 생성기 exit 1) | 반사실: 가짜 키 추가 주석에서 rc 1 「미사용 주석 키 1」. 실제 주석은 rc 0 | 6 |
| 2 | code-review | LOW | annotations 의 GET 행 2개 (`allow-ip-admin.controller.ts:23·32`) 비고가 「인증 없는 관리자 쓰기」 였다 | 흡수 (「인증 없는 관리자 조회」 로 고쳤다. 쓰기 표현은 POST · PUT · DELETE 행에만 남는다) | `annotations.json` 두 행 · 판정표 재생성 | 2 |
| 2 | typescript | LOW | 생성기 죽은 코드 (`grepDir` · `execFileSync` import · `CTRL_GLOB`) | 흡수 (삭제). 재생성 표가 같아 검사 #7 cmp 통과 | 검사 #7 | - |
| 2 | code-review | LOW | README 계수 리터럴 (704 · 126 · 가드 분포 · 774 · 571 · 133) 이 검사 없이 적혔다 (INV4) | 흡수. 관측 기록이라고 표시하고 재현 명령을 적었다. 게이트 기준으로 쓰지 않는다. 계수를 게이트로 새로 묶지는 않았다 | README 「알려진 한계」 첫 줄 | 2 |
| 2 | code-review | LOW | 검사 #5 가 `@Public(` 과 `SetMetadata(isPublic` 두 패턴만 본다. 상수 키 · Reflect 우회를 못 잡는다 | 미해소. 검사 본문은 계획서 Validation 안에 있어 계획 개정이 필요하다 (사용자 결정). 현재 `isPublic` 설정자 0 은 grep 으로 다시 확인했다 | 계획 Validation 206행 · `git grep` 설정자 0 | 5 |
| 3 | code-review | MEDIUM | 검사 #10 음성 분기가 exit 0 만 거른다. 없는 표 (rc 1) 와 빈 표 (rc 2) 도 음성 「통과」 가 된다 | 기각 (계획서 동결, 이연). 검사 본문은 계획서 Validation 안에 있어 이번 라운드에서 고치지 않았다. 백로그 append (MEDIUM). 음성 기록은 stderr 실측으로 보강 | 계획서 :266 · :268. 실측: 빈 표 rc 2 「빈 입력」, 없는 표 rc 1 (ENOENT), 표 한 행 뺀 사본 rc 1 「런타임에만 있음: GET /live」, 가짜 행 더한 사본 rc 1 「표에만 있음: GET /__stray」 | 3 · 5 |
| 3 | code-review | MEDIUM | 재동결 절차가 route-compare 의 고정 동결본 (03ce) 때문에 끝까지 돌지 않는다 | 흡수. `route-compare.mjs` 가 세 번째 인자로 동결본을 받는다 (없으면 03ce, 검사 #10 본문은 그대로). README 재동결 명령에 대조 단계와 guardsAdd 한계를 적었다 | 반사실: 기본 실행 rc 0 · 명시 동결본 rc 0. 동결본 가드 이름을 바꾼 사본 rc 1 「가드 대조 불일치 표 31 · 런타임 31」. 없는 동결본 rc 1 (ENOENT, 인자를 읽는다) | 2 |
| 3 | code-review · typescript | LOW | README 「파일」 표의 행 수 81 이 annotations.json 의 rows 키 개수와 다르다 (INV4 리터럴) | 흡수. 리터럴을 지우고 기계 계수 안내로 바꿨다 | README 「파일」 표 · `node -e` 로 센 rows 는 82 (실측) | 2 |
| 3 | code-review | LOW | Express 와일드카드 면제가 경로 문자열만 보고 메서드 · 출처를 보지 않는다. 덤프의 middleware 이름은 해시다 | 흡수 (부분). 면제를 (경로, 메서드) 로 묶는다 (미들웨어 메서드가 ALL 이면 전 메서드). 덤프 middleware 항목이 메서드를 싣는다. 잔여: 같은 경로 · ALL 의 다른 미들웨어는 구분하지 못한다 (README 한계). 편차 7 의 nestjs-pino 귀속은 덤프가 아니라 `app.module.ts:99` 읽기로만 확인된다 | 반사실: 덤프의 미들웨어 메서드를 GET 으로 바꾼 사본 rc 1 「미들웨어에 없는 와일드카드 등록: ACL /*」 (그 밖에 다수). 실제 덤프 rc 0 (wildcard=70) | 6 |
| 3 | code-review | LOW | 노트 「구현 기록」 절이 검사 #9 통과 · 개발 우회 704행 전부 없음으로 적혀 현재 상태와 어긋난다 | 흡수. 절 머리에 「구현 당시 기록」 표지를 달았다. 현재 값은 이 절 (라운드 1 · 2 · 3) 에 있다 | 「## 구현 기록」 절 머리 | 2 |
| 3 | code-review | LOW | 판정표와 덤프가 main.ts 가 붙이는 라우트 (스웨거) 와 전역 미들웨어를 보지 않는다는 경계가 README 에 없다 | 흡수. 「알려진 한계」 에 경계와 R12 「열림 · #96 M8 잔여」 표지를 적었다 | README 「알려진 한계」 · `apps/api/src/main.ts:42` | 6 |
| 3 | code-review | LOW | aff 동결본에는 SuperAdminGuard 보정이 없어 같은 파서 빈틈이 남는다 | 흡수 (문서). 수치 영향 없음 (`@Roles` 가 admin 한 개라 판정 있음이 어느 쪽이든 같다). 「aff 는 정적 계수만」 을 README 한계에 적었다. tree 단일 값 구조라 보정 추가는 하지 않았다 | README 「알려진 한계」 · `freeze-aff7ffcc.list.txt:693` | 6 |
| 3 | security · typescript | MEDIUM | 덤프의 env 격리가 .env 로 우회된다. dotenv 가 import 시점에 cwd · 서버 루트의 .env 를 읽어 지운 키를 되살린다. README 의 「상위 .env 가 실 자격을 싣지 못한다」 는 사실이 아니다 | 흡수 (fail closed). 작업 디렉터리와 서버 루트의 `.env` · `apps/api/.env` 가 하나라도 있으면 exit 2 로 멈춘다. README 문장을 닫힘 규칙으로 고쳤다. 서버 루트 분기는 코드 읽기만 했다 | 수정 전 리뷰 실측: GOOGLE_CHAT · ALIGO 키가 되살아남. 수정 뒤 실측: 작업 디렉터리에 `.env` 를 둔 덤프 rc 2 「.env 가 1개 있다」, 없을 때 rc 0 (704 · 774 · 미들웨어 1) | 5 |
| 3 | security | MEDIUM | CorpReadonlyGuard (조직관리자 쓰기 기본 차단) · CorpAnalysisViewGuard (기업 기능 레벨) 이 판정표에 안 보인다. 범위 분기가 AdminJwt 분기에 가려진 죽은 코드였다 (라운드 2 회귀). 89행 | 흡수. 죽은 범위 분기를 지우고 두 Corp 가드를 비고 칸에 가드 이름과 함께 적었다. 가드 행의 비고에 가드 이름이 없으면 생성기가 exit 1 (검사 #7 이 본다). README 규칙 7 을 고쳤다 | 반사실: 표지 줄을 뺀 생성기 사본 rc 1 「Corp 가드 표지 누락 88행」. 수정 뒤 생성기 rc 0, 표 89행의 비고 칸만 바뀌고 `--stats` 는 불변 (noauth=80 admin51=38) | 2 · 5 |
| 3 | typescript | LOW | judgedOf 가 CarViewAdminGuard 는 세고 같은 근거의 Corp 가드는 안 센다. Corp 가드를 가진 관리자 행이 「관리자 신원만」 으로 남는다 | 기각. 판정 정의는 계획 규칙 4 의 개정이라 사용자 몫이다. 현재 구현은 계획 규칙 4 와 라운드 2 개정 정의를 따른다. 계획 개정 요청 8번으로 올렸다 | `build-table.mjs` judgedOf · `corp-readonly.guard.ts:21-33` · `corp-analysis-view.guard.ts:34-49` | 6 |
| 3 | typescript | LOW | 보고서가 검사 #12 의 허용 집합 밖 경로를 한 곳에서 두 개, 다른 곳에서 세 개로 적었다 (INV4) | 흡수. 보고서 두 곳을 세 경로 (백로그 · 보고서 · implement receipt) 로 맞췄다 | `admin-rbac-m1-report.md` 23행 · 71행 | 2 |

### 검사 결과 (라운드 1 정정 뒤, 같은 Validation 블록 실행)

| 검사 | 결과 | 비고 |
|---|---|---|
| `#1` ~ `#8` | 통과 | `#7` 은 생성기 재실행 결과가 커밋 표와 같다 (`cmp`) |
| `#9` | 붉음 (기대 불일치) | 외부 표지 행 `FR=5 EX=0` (allow-ip). 계획서가 이 파일을 외부 표지로 고정한다. 계획 개정 필요 (CRITICAL 행) |
| `#10` | 통과 | `route-compare ok rows=704 runtime=704 express=704 wildcard=70`. 반례 두 사본은 exit 1 |
| `#11` | 통과 | 노트 줄 `03ce noauth=80 admin51=1` · `aff noauth=80 admin51=51` 그대로 |
| `#12` | 붉음 (기대 불일치) | 허용 집합 밖 3경로 (HIGH 행). 코드 영향 0 은 유지 (서브모듈 워킹트리 clean, 서버 소스 미변경) |
| `#13` | 통과 | 호스트 경로 0 |

- 서버 소스는 건드리지 않았으므로 서버 jest · tsc · eslint 는 돌리지 않았다. 생성기 · 덤프 · 대조 스크립트만 바뀌었다 (node 실행으로 확인).
- 미검증: 판정 가드 행 6개의 「판정만」 내용은 저자 판정이다. `AnalysisTargetGuard` 의 거부 조건은 `analysis-target.guard.ts:36-60` 정적 읽기다. 런타임 재생은 하지 않았다.
- 미검증: `/admin/push/:pshId` 의 IDOR 는 정적 읽기다. 실제 `pshId` 로 재생하지 않았고, 푸시 내용의 민감도도 확인하지 않았다.
- 계획 개정 요청 (사용자 결정): 검사 `#9` 의 외부 표지 파일 셋 (allow-ip 제외) 과 `#12` 허용 집합 (산출 위치) 두 가지다.

### 검사 결과 (라운드 2, 같은 Validation 블록 실행)

| 검사 | 결과 | 비고 |
|---|---|---|
| `#1` ~ `#6` | 통과 | 착수 sha · 도구 digest · 자가 검사 · 동결본 재계산 · 미지 가드 0 · 컨트롤러 범위 (서버 소스 무변경) |
| `#7` | 통과 | 판정표를 재생성해 커밋 대상 `route-contract-table.md` 에 썼다 (cmp 0). 어휘 검사 통과 |
| `#8` | 통과 | 신원 수립 없는 행의 키 집합과 비고 |
| `#9` | 붉음 (라운드 1 과 같다) | 외부 표지 파일 `FR=5 EX=0` (allow-ip 5행). 계획이 이 파일을 외부 표지로 고정하므로 계획 개정이 필요하다 |
| `#10` | 통과 | `route-compare ok rows=704 runtime=704 express=704 wildcard=70`. 가드 대조 0 불일치. 행 뺀 사본 rc 1 「런타임에만 있음: GET /live」, 행 더한 사본 rc 1 「표에만 있음: GET /__stray」 |
| `#11` | 통과 | 노트 줄 `03ce noauth=80 admin51=38` · `aff noauth=80 admin51=88` (aff 는 아카이브 트리로 계산, 03ce 소스로 계산한 82 는 착오였다) |
| `#12` | 붉음 (라운드 1 과 같다) | 허용 집합 밖 3경로 (백로그 · 보고서 · implement receipt). 이번 라운드로 늘어난 경로는 없다 |
| `#13` | 통과 | 호스트 경로 0 |

- 서버 · 관리자 · 사이트 소스는 건드리지 않았다. 건드린 것은 생성기 · 덤프 · 대조 스크립트 · 주석 · 판정표 · README · 노트 · 보고서 · 백로그 한 줄이다. 그래서 서버 jest · tsc · eslint 는 돌리지 않았다 (라운드 1 과 같다).
- 생성기 재실행 결과 CarViewAdminGuard 행은 12개다 (관리자 열람 토큰 6 · 관리자 토큰 4 · 없음 2). 라운드 1 지적은 6행만 들었다. 나머지 4행 (`analyze-admin.controller.ts:166·186`, `client-analyze-admin.controller.ts:164·184`) 도 이번에 「관리자 판정」 으로 바뀌었다.

### 계획 개정 요청 (사용자 결정, 계획서는 동결이라 고치지 않았다)

1. Task 3 규칙 4 (판정 정의): `CarViewAdminGuard` 를 판정에 넣는다. `RolesGuard` 는 역할 집합이 `ADMIN_ROLES` 전체를 덮으면 판정이 아니다. 근거는 위 라운드 2 행 1 · 3.
2. NOT Building 345행 · Acceptance 320행 「무조건 공개 0」 과 검사 #5: 가드 없는 라우트 44행 (판정 없음) 을 어떻게 셀지. 「무조건 공개」 범주를 둘지, 「신원 없음·선택」 으로 두고 주장만 고칠지.
3. 검사 #9 허용 파일 집합 (allow-ip 를 외부 표지에서 빼는 개정, 라운드 1 과 같다).
4. 검사 #12 허용 집합 (백로그 · 보고서 · receipt 산출 위치, 라운드 1 과 같다).
5. 검사 #5 패턴 확장 (`isPublic` 문자열 전역 검사). 현재 값 0.
6. 소비자 종류 (앱 · 스케줄러 · 운영 도구) 와 책임자 칸 (R19, 사용자 몫).
7. 레거시 v1 열람 토큰 통과 (`admin-view-token.guard.ts:33-47`) 의 정책 판단. 신원을 세우지 않는 통과가 의도인지.
8. 판정 정의 (계획 규칙 4) 에 Corp 가드 (`CorpReadonlyGuard` · `CorpAnalysisViewGuard`) 를 넣을지. 두 가드도 거부 응답을 내므로 `CarViewAdminGuard` 와 같은 논리다 (라운드 3 LOW). 넣으면 admin51 이 바뀐다. 정의는 사용자 몫이다.

### 미검증 (라운드 2)

- 판정 정의 개정 (계획 규칙 4) 이 확정되기 전까지 행의 「관리자 판정」 · 「관리자 신원만」 은 저자 판정이다. 검사 #7 · #10 은 어휘와 개정 정의의 일치만 본다.
- 레거시 v1 토큰 · 차량 축 행의 실제 요청 재생은 하지 않았다 (정적 읽기).
- B2C 생년월일 단독 인증의 실제 재생 · 프록시 · WAF 의 시도 제한 여부는 확인하지 못했다. 백로그 항목으로만 남긴다.
- 「clients/*」 외부 표지 주장은 반박했다 (위 행 6). 동결본의 `modules/client/` 62행 중 23행은 외부 Basic 으로 이미 표지됐다.

## 코드 리뷰 라운드 3 (검사 결과, 마지막 라운드)

이 라운드는 상한 3 의 마지막이다. 흡수하지 못한 MEDIUM 1건은 `.claude/plans/codex-findings-backlog.md` 에 append 했다 (검사 #10 음성 분기, 계획서 동결). CRITICAL · HIGH 미해소는 없다. 검사 본문은 실행 순서 때문에 #4 와 #11 을 같은 셸에서 돌렸다 (`TMPD` 를 #4 가 만든다).

| 검사 | 결과 | 비고 |
|---|---|---|
| `#1` ~ `#8` | 통과 | `#7` 은 재생성 표를 커밋 대상 `route-contract-table.md` 에 썼다 (Corp 비고 칸 89행이 바뀌었다). 생성기 rc 0 |
| `#9` | 붉음 (변화 없음) | `FR=5 EX=0` (allow-ip 5행). 계획이 이 파일을 외부 표지로 고정하므로 계획 개정 요청 3번 |
| `#10` | 통과 | `route-compare ok rows=704 runtime=704 express=704 wildcard=70`. 두 음성 사본 rc 1 (표에만 · 런타임에만 문구) |
| `#11` | 통과 | `03ce noauth=80 admin51=38` · `aff noauth=80 admin51=88` |
| `#12` | 붉음 (변화 없음) | 허용 집합 밖 세 경로 (백로그 · 보고서 · implement receipt). 이번 라운드로 늘어난 경로는 없다 |
| `#13` | 통과 | 호스트 경로 0 |

- 서버 소스는 건드리지 않았다. 바뀐 것은 덤프 · 대조 스크립트 · 생성기 · README · 판정표 · 보고서 · 노트 · 백로그다. 서버 tsc · jest · eslint 는 돌리지 않았다 (라운드 1 · 2 와 같다). 대신 `node --check` 와 위 반사실 실행으로 대신 확인했다.
- 계획서는 고치지 않았다. 이 절을 쓸 때는 커밋하지 않았지만, 그 뒤 `d3d26dd` 가 메타 · 노트 · 백로그 · receipt 를 커밋했다 (최종 리뷰 R1 · fable LOW 반영).

### 미검증 (라운드 3)

- 서버 루트 쪽 `.env` 분기 (`route-runtime-dump.cjs` 의 `ROOT` 항목) 는 코드 읽기만 했다. 실측은 작업 디렉터리 쪽만 했다 (임시 디렉터리에 `.env` 를 두고 exit 2).
- 미들웨어 귀속은 `app.module.ts:99` 의 LoggerModule 읽기로만 확인했다. nestjs-pino 의 미들웨어 이름은 해시라 덤프로 확인하지 못했다.
- Corp 가드 행의 「관리자 신원만」 판정은 계획 규칙 4 정의에 따른 결과다 (계획 개정 요청 8번).

## 최종 리뷰 (라운드 1 · 2 / 3, fable · codex, 2026-10-19)

이 절은 마일스톤 끝의 최종 리뷰 라운드 1 이다. 지적 8건 (fable 4 · codex 4, 중복 1쌍 포함) 을 판정했다.
계획서 `.claude/PRPs/plans/admin-rbac-m1.plan.md` 는 동결이라 고치지 않았다. 바꾼 것은 판정표 생성기 · 대조 스크립트 · 주석 · 어휘 대응 · 동결 와일드카드 목록 · 노트다.
판정 요약: 흡수 6 · 부분 흡수 1 · 이연 1 · 기각 0. HIGH 는 2건이며 1건 흡수 (codex) · 1건 미해소 (fable, 계획 본문의 「무조건 공개 0」 주장은 동결이라 사용자 개정 요청 9 몫). CRITICAL 미해소 0.

| 라운드 | 리뷰어 | 심각도 | 지적 | 판정 | 증거 | 층위 |
|---|---|---|---|---|---|---|
| 1 | fable | HIGH | 접근 계약 칸이 PRD M1 여섯 범주 (무조건 공개 · 필수 신원 · 복수 신원 · 자원 한정 토큰 · 서비스 검증 · 외부 신원 표지) 대신 표 어휘를 쓰고, 어휘 대응이 없다. 「무조건 공개 후보」 미명명. 계획 Acceptance · NOT Building 의 「무조건 공개 0」 은 거짓 | 부분 흡수. README 「어휘 대응」 표 (표 어휘 → PRD 어휘, 열람 토큰 행은 신원 종류로 자원 한정 토큰) 를 쓰고, 생성기가 가드 없음 · 요청 빈도 제한만 행 46개의 비고에 「무조건 공개 후보」 를 붙였다. 검사 `#7` 어휘와 계획 본문은 동결이라 칸을 PRD 어휘로 다시 쓰지 않았다. 계획 본문 정정은 개정 요청 2 · 9 (사용자 몫) | `README.md` 「어휘 대응」 · `build-table.mjs` 무조건 공개 후보 표지 · 표 비고 46행 | 2 |
| 1 | fable | MEDIUM | 외부 표지 행의 소비자 칸이 「미확인(R19)」 로 강제돼 코드에 보이는 흔적을 지운다 (알림톡 3행 · mok-auth 결과) | 흡수. 생성기가 외부 행도 소비자 흔적을 적고, 흔적이 없을 때만 「미확인(R19)」 단독. 실측: 100 · 101 · 102 는 「사이트 프런트 (경로 일치) · 외부 수신자 미확인(R19)」, 560 · 561 (PG 승인 · 취소) 은 `PaymentPage.tsx:257-258` 의 ReturnUrl 로 같은 형태. 272–294 (외부 Basic) 는 흔적이 없어 그대로. 부수 두 건도 고쳤다. (가) 경로 앞 경계 없는 부분 문자열 (`/admin/clients/` 가 `/clients` 접두를, `/analyze/b2c/…/health` 가 `/health` 를, `@/features/advertisement` 가 `/advertisement` 를 먹었다). 경로 앞을 따옴표 · 백틱 · `}` 또는 `://호스트` 뒤로 제한. 떨어진 일치는 모두 부분 문자열이었다. (나) 176 의 사이트 흔적은 `App.tsx:266` 의 페이지 라우트 선언이라 비고에 표지 | `build-table.mjs` consumerRow · ANCHOR. 소비자 칸 57행 변경 (외부 6행 + 앵커 규칙으로 바뀐 비외부 51행) | 6 |
| 1 | fable | MEDIUM | 검사 `#9` 가 붉어 `#10`~`#13` 이 같은 Validation 블록으로 도달하지 않는다. 노트가 경로를 적지 않았다 | 흡수 (기록). 계획 블록을 검사별로 쪼개 `#1`~`#13` 을 개별 실행했다 (`#4` 와 `#11` 은 `TMPD` 를 공유하므로 같은 셸). 결과는 아래 「검사 결과 (최종 리뷰 R1)」. 검사 `#9` 파일 집합 (allow-ip 제외) 은 개정 요청 3 으로 M2 착수 전 사용자 몫 | 아래 「검사 결과 (최종 리뷰 R1)」 | 4 |
| 1 | fable | LOW | 노트 254행 「커밋하지 않았다」 가 `d3d26dd` 뒤에도 남았다 | 흡수. 문장을 커밋 사실로 고쳤다 | `git show --stat d3d26dd` (메타 · 노트 · 백로그 · receipt 포함) | 2 |
| 1 | codex | HIGH | 가드 없는 핸들러의 필수 토큰 검사가 판정표에서 사라졌다 (`invoice…:190` 서명 토큰 검증, `auth…:124` 재설정 티켓 검사). 생성기 기본값이 「신원 없음·선택」 이었다 | 흡수. 두 행을 주석으로 「판정만」 으로 고치고 비고에 검사 위치와 PRD 어휘를 적었다 (자원 한정 토큰 · 서비스 검증). 같은 본문 훑기에서 B2C `analyze…:569` (생년월일 서비스 검증 뒤 쿠키 발급) · `:627` (쿠키 판정) 도 「판정만」 으로 바꿨다. 정규식 훑기 15행 · 수작업 1행. 반사실: 주석을 빼면 접근 계약 4행이 「신원 없음·선택」 으로 돌아간다. 검사 `#7` · `#10` 은 이 변화를 잡지 않는다 (어휘와 판정 가드 행만 본다, 추론이고 실측 안 함) | `annotations.json` 4행, 표 접근 계약 칸 4행 변경 | 2 |
| 1 | codex | MEDIUM | 미지 가드가 알려진 신원 가드와 같이 붙으면 kind auth 로 묻혀 「미지 가드 0」 이 통과한다 | 흡수. `known-guards.mjs` (17 이름) 를 두었다. 생성기는 목록 밖 이름에 exit 1, `route-compare.mjs` 는 런타임 이름에 exit 1. 검사 `#5` 본문은 그대로 둔다. 음성 실행은 아래 표 | `known-guards.mjs` · `build-table.mjs` · `route-compare.mjs` | 5 |
| 1 | codex | MEDIUM | Express `/*` 면제가 등록 개수를 보지 않아 같은 (메서드, /*) 를 하나 더 붙여도 exit 0 이었다 (wildcard 71) | 흡수. `express-wildcard.list.txt` (70행 = 35 메서드 × 2 등록) 를 동결하고 다중집합으로 대조한다. 검사 `#10` 본문은 그대로 | `express-wildcard.list.txt` · `route-compare.mjs` (f) · 음성 실행 표 | 5 |
| 1 | codex | MEDIUM | 검사 `#9` 가 정정된 인벤토리 (allow-ip 5행) 를 거부하고 뒤 검사를 멈춘다 | 이연. 계획이 동결이라 검사 본문은 고치지 않았다. fable 의 같은 지적과 묶어 개정 요청 3 (사용자 몫, M2 착수 전) 으로 넘긴다. 검사 결과는 개별 실행으로 기록 | 계획 Validation `#9`, 「검사 결과 (최종 리뷰 R1)」 | 5 |
| 2 | fable | HIGH | 범위 키 선언 공백: 자동 칸 465행 (「없음 (관리자 전용 · 범위 키 미선언)」 318 · 「sagId · agcId · orgPath 미판정 (영업·기업 통과)」 104 · 「없음 (열람 토큰 · 범위 미판정)」 35 · 「미판정」 8) 은 저자 선언이 아니다. 계획 Task 3 규칙 7 의 클래스 주석 경로가 폐기됐는데 미검증 목록과 개정 요청에 없었다 | 공시 · 미해소 (사용자 몫). 채우지는 않았다: 클래스 126개 · 행마다 저자 판정이 필요하고 PRD 선언의 주체는 사용자다. 계획 개정 요청 10 과 「미검증 (최종 리뷰 R2)」 에 올렸다. README 칸 규칙 7 · 100행의 「선언이다」 를 「자동 칸」 으로 고쳤다. 104행 「미판정」 은 PRD Success Metric 의 0 을 충족하지 않는다 | README 칸 규칙 7 · 100행 · 표 범위 키 칸 집계 (awk: 318 · 104 · 35 · 8) | 2 |
| 2 | fable | MEDIUM | 같은 식별 수단 (altCode · 초대 코드) 의 행이 엇갈린다: loss-analysis `invite/:code` 6행 · certify-resume 1행 · treat 본문 altCode 2행 (89 · 110) 이 「신원 없음·선택 · 무조건 공개 후보」 로 남았다 | 흡수. 9행에 `contract: 외부 신원 표지` 와 external 을 주석으로 더했다. 근거: loss `loss-analysis.service.ts:201-203` (초대 코드로 행을 찾고 attempt 가 있으면 assertReminderToken), treat 80 `treat.service.ts:211-213` (validateReminder · checkAlimtalkEntry), treat 89 `treat.service.ts:605` (validateTreatReminder 가 본문 altCode 를 검사), treat 110 `treat.service.ts:769` (정회원 분기). 반사실: 주석을 전부 빼고 표를 생성하면 (이 9행 + 판정만 2행) 계약 칸 11행이 「신원 없음·선택」 으로 돌아가고 커밋 표와 cmp 가 달라 검사 `#7` 이 붉다 (실측). 검사 `#10` 은 관리자 행만 대조하므로 이 변화를 잡지 않는다 | `annotations.json` loss 40 · 45 · 50 · 55 · 60 · 65, treat 80 · 89 · 110 · 표 9행 | 1 |
| 2 | fable | MEDIUM | `/auth/refresh` 가 「무조건 공개 후보」. 서비스 위임 한 줄 핸들러 (`mok-auth:32` decrypt, `auth:139` signout 앱 분기) 도 같은 방식으로 훑어야 한다 | 흡수. `auth:165` 를 `판정만` + 「PRD 어휘: 서비스 검증 (auth.service.ts:642-676)」, `mok-auth:32` 를 `판정만` + 「서비스 검증 (mok-auth.service.ts:53-66, 외부 결과 API 의 resultCode 검사)」 으로 고쳤다. `auth:139` 는 계약을 두고 비고에 「_aT 를 decode 만 해 서명을 보지 않고 그 id 의 refresh 를 비운다 (auth.service.ts:795-807, 백로그)」 를 적었다. 훑기: 가드 없는 78행 (none 66 · ratelimit 6 · optional 6) 을 본문 다섯 줄 창으로 봤다 (한 겹). `mok-auth:21 result` 는 이미 외부 신원 표지다. 반사실: 주석 제거 시 계약 칸이 돌아간다 (위 행과 같은 실측) | `annotations.json` 177 · 184 · 186 · 표 3행 | 1 |
| 2 | fable | MEDIUM | route-compare 의 (메서드, 경로) 유일성: 복수 컨트롤러 음성이 실행되지 않았고 키 · 가드 · Express 대조로는 탐지되지 않는다 | 흡수. `route-compare.mjs` (g) 를 추가했다: 표 쪽과 런타임 쪽 각각에서 (메서드, 경로) 가 두 번 나오면 exit 1. 음성은 「음성 실행 (최종 리뷰 R2)」 표. 표 · 덤프 · Express 를 같이 미러링한 중복 사본에서 구 스크립트는 exit 0, 신 스크립트는 exit 1 이다. 실측 표 · 덤프의 (메서드, 경로) 중복은 0 이다 (검사 `#10` 통과, rows=704). 계획 정책 행 「복수 컨트롤러 반례」 는 이 음성으로 실행됐다 | `route-compare.mjs` (g) · README 「검사 대조 범위」 · 음성 표 | 5 |

### 음성 실행 (최종 리뷰 R1, 흡수 뒤 반사실)

| 대상 | 사본 | 관찰 |
|---|---|---|
| 미지 가드 (생성기) | 동결본 `menu.controller.ts:30` 행에 `MysteryGuard` 추가 | `build-table.mjs` exit 1, 「미지 가드 이름 1: MysteryGuard」 |
| 미지 가드 (대조) | 덤프의 `/live` 핸들러 가드에 `MysteryGuard` 추가 | `route-compare.mjs` exit 1, 「런타임 미지 가드 이름: MysteryGuard」 |
| 와일드카드 | 덤프의 Express 등록에 `GET /*` 추가 (동결본 밖) | exit 1, 「와일드카드 동결본에 없음: GET /*」 |
| 소비자 앵커 (부분 문자열) | 앵커 없이 생성 | 외부 Basic 272–294 가 「관리자 프런트 (접두만 · 후보)」 로 거짓 일치. 앵커를 달자 「미확인(R19)」 로 복귀 |
| 소비자 앵커 (URL) | 「://호스트」 규칙 없이 생성 | 164 (`fetch("http://localhost:7200/analyze/b2c/…")`) 가 「접두만 · 후보」 로 떨어짐. 규칙을 넣자 「경로 일치」 로 복귀 |
| 판정만 주석 | 4행 주석 제거 | 접근 계약 칸 4행이 「신원 없음·선택」 으로 돌아간다. 게이트가 이를 잡는지는 실측하지 않았다 (본문 읽기 추론) |

### 검사 결과 (최종 리뷰 R1)

계획서 Validation 블록을 검사별로 쪼개 같은 본문으로 실행했다. `#4` 와 `#11` 은 같은 셸에서 돌렸다 (`TMPD` 공유).

| 검사 | 결과 | 비고 |
|---|---|---|
| `#1` | 통과 | gitlink 3곳 · 두 병합 조상 · 트리 `b2acda28` |
| `#2` | 통과 | 도구 사본 digest 고정 |
| `#3` | 통과 | 도구 자가 검사 두 줄 |
| `#4` | 통과 | 두 트리 `--list` 가 동결본과 같다 (`#11` 과 같은 셸) |
| `#5` | 통과 | 미지 가드 (other) 0 · `@Public(` · `isPublic` 0 |
| `#6` | 통과 | `*.controller.ts` 밖의 `@Controller` 0 |
| `#7` | 통과 | 생성기 출력 = 커밋 표 (cmp 0) · 행 키 = 동결본 · 어휘 안 |
| `#8` | 통과 | 신원 수립 없는 행의 키 집합과 비고 |
| `#9` | 붉음 (기대 불일치) | `FR=5 EX=0` (allow-ip 5행). 계획 개정 요청 3 |
| `#10` | 통과 | `route-compare ok rows=704 runtime=704 express=704 wildcard=70` · 두 음성 사본 exit 1 |
| `#11` | 통과 | `03ce noauth=80 admin51=38` · `aff noauth=80 admin51=88` (노트 줄과 같다) |
| `#12` | 붉음 (기대 불일치) | 허용 집합 밖 3경로 (백로그 · 보고서 · implement receipt). 이번 라운드로 늘어난 경로는 없다 |
| `#13` | 통과 | 호스트 경로 0 |

### 계획 개정 요청 (최종 리뷰 R1 에서 추가, 사용자 결정)

9. 접근 계약 어휘를 PRD M1 여섯 범주로 맞출지. 대상은 계획 Task 3 규칙 5 · 검사 `#7` 어휘 정규식 · Acceptance 의 「무조건 공개 행 0」 · NOT Building 의 「무조건 공개」 행 0 문장이다. 판정 칸을 PRD 어휘로 다시 쓸지, README 「어휘 대응」 로 충분한지도 사용자가 정한다. 근거: 표 비고의 「무조건 공개 후보」 46행, 판정만 4행 (PRD 밖 판정 가드 · B2C 쿠키 판정 행을 포함한다).

### 미검증 (최종 리뷰 R1)

- 「판정만」 으로 바꾼 4행의 「자원 한정 토큰 · 서비스 검증」 판정 내용은 소스 읽기만 했다. 런타임 재생은 하지 않았다.
- 가드 없는 행의 본문 밖 (서비스 안) 검사는 전수 점검하지 않았다. 본문 훑기는 정규식 한 겹이다.
- 소비자 칸은 문자열 흔적이다. 호출 증명이 아니다 (README 「알려진 한계」).
- 판정 칸의 내용 (접근 계약 여섯 범주) 은 게이트가 어휘만 본다 (계획 Acceptance 의 (미검증)).
- `known-guards.mjs` 목록은 현재 두 동결본의 가드 이름 17개를 덮는다. 새 트리를 재동결할 때 목록을 같이 본다 (README 재동결 절차에 적었다).

### 판정 요약 (최종 리뷰 R2)

흡수 3 (MEDIUM 3건) · 공시 미해소 1 (HIGH 1건, 사용자 몫) · 기각 0. CRITICAL 0.

### 음성 실행 (최종 리뷰 R2, 흡수 뒤 반사실)

| 대상 | 사본 | 관찰 |
|---|---|---|
| (메서드, 경로) 중복 | 표 · 덤프 · Express 에 `GET /live` 를 한 벌 더 미러링 | 구 `route-compare` (HEAD) exit 0 「rows=705」 · 신 스크립트 exit 1 「(메서드, 경로) 중복 (표): GET /live」 · 「(런타임): GET /live」 |
| 계약 주석 되돌림 | 주석 11행 (loss 6 · treat 3 · refresh · decrypt) 을 빼고 표 생성 | 계약 칸 11행이 「신원 없음·선택」 으로 돌아간다. 생성 결과가 커밋 표와 cmp 다르다 → 검사 `#7` 붉음. 검사 `#10` 은 이 11행을 잡지 않는다 (관리자 계약 칸만 대조) |

### 검사 결과 (최종 리뷰 R2)

계획서 Validation 블록을 검사별로 쪼개 같은 본문으로 실행했다. `#4` 와 `#11` 은 같은 셸에서 돌렸다.

| 검사 | 결과 | 비고 |
|---|---|---|
| `#1` | 통과 | 착수 sha · 조상 · 트리 |
| `#2` | 통과 | 도구 사본 digest |
| `#3` | 통과 | 도구 자가 검사 |
| `#4` | 통과 | 동결본 재계산 (`#11` 과 같은 셸) |
| `#5` | 통과 | 미지 가드 0 · `@Public` 0 |
| `#6` | 통과 | `@Controller` 표본 범위 |
| `#7` | 통과 | 생성기 출력 = 커밋 표 (cmp 0) · 키 = 동결본 · 어휘 안 |
| `#8` | 통과 | 신원 수립 없는 행 키 집합 · 비고 |
| `#9` | 붉음 (R1 과 같음) | `allow-ip` 5행 (FR=5 EX=0). 계획 개정 요청 3 |
| `#10` | 통과 | 덤프 재실행 · `route-compare ok rows=704 runtime=704 express=704 wildcard=70` |
| `#11` | 통과 | `03ce noauth=80 admin51=38` · `aff noauth=80 admin51=88` |
| `#12` | 붉음 (R1 과 같음) | 허용 집합 밖 3경로: 백로그 · 보고서 · implement receipt. 이번 라운드로 늘어난 경로는 없다. 보고서 파일은 이 라운드가 만든 것이 아니라 커밋하지 않았다 |
| `#13` | 통과 | 호스트 경로 0 |

### 계획 개정 요청 (최종 리뷰 R2 에서 추가, 사용자 결정)

10. 범위 키 선언 465행 미달. 계획 Task 3 규칙 7 (`admin-rbac-m1.plan.md:102`) 과 Files 표 「`annotations.json` = 클래스 126개의 범위 키 · 소비자」 는 클래스 주석 경로를 요구하지만, 구현은 가드 이름에서 기계로 정한 자동 칸으로 바꿨고 `classes` 는 `{}` 다. 클래스 126개에 저자 판정을 채울지, 자동 칸을 PRD 선언으로 인정할지 사용자가 정한다. 영업 · 기업 통과 104행이 「미판정」 이라 PRD Success Metric 의 0 은 지금 충족하지 않는다.

### 미검증 (최종 리뷰 R2)

- 범위 키 칸 465행은 PRD 의 선언이 아니다. 공시만 했고 내용은 채우지 않았다 (계획 개정 요청 10).
- `refresh` · `decrypt` 를 「판정만」 으로 바꾼 근거는 핸들러 본문과 서비스 본문 한 겹 읽기다. 런타임 재생은 하지 않았다.
- 가드 없는 78행의 서비스 안 검사는 본문 다섯 줄 훑기로만 봤다. 전수 점검하지 않았다.
- `attempt` 가 없으면 검증을 건너뛴다 (certify-resume 80 · loss 40~65 · treat-compare 의 B2C 분기). 같은 구조가 알림톡 check 행에도 있다. 이 구조가 의도인지는 (미검증).
- 초대 코드 · altCode 식별 행의 외부 신원 판정은 소스 읽기로 정했다. 런타임 재생과 실제 외부 호출자 확인은 하지 않았다 (R19).

### 백로그 관찰 (M1 밖, 기록만, 미실측)

- `auth.service.ts:654` 는 `jwtService.decode` 라 refresh 토큰의 서명을 보지 않고 DB 존재 확인에 기댄다 (R1 의 부수 관찰 재확인).
- `auth.controller.ts:139` 앱 분기는 `_aT` 를 decode 만 한 id 로 refresh 를 비운다 (`auth.service.ts:795-807`). 서명 미검증이라 임의 id 의 강제 로그아웃이 가능할 수 있다 (미실측).
- `treat.controller.ts:100` `compare` 는 oddId 만 받고 `treat.service.ts:801` `userReAnalyze` 의 본문 801~830 에 신원 검사가 보이지 않는다. 한 겹 읽기다 (미검증). 가드 없음 · 무조건 공개 후보로 남긴다.
- `treat.service.ts:761-766` treat-compare 의 B2C 분기는 altCode 검사 (`:769`) 전에 이벤트로 넘긴다 (미검증 · 관찰).

## M2 정정

관리자 RBAC M2 (`.claude/PRPs/plans/admin-rbac-m2.plan.md`) 에서 M1 산출의 결함 몇 가지를 정정한다. M1 본문과 M1 검사 본문은 동결이라 고치지 않았고, 정정은 여기와 M2 메타에 적는다.

1. 「무조건 공개 0」 주장은 틀렸다. 가드가 없는 「신원 없음·선택」 행이 35개 있고, 레지스트리 생성 때 범주 우선순위 뒤에 다시 셌다. 범주 분포는 필수 신원 565 · 외부 신원 표지 39 · 자원 한정 토큰 35 · 서비스 검증 30 · 무조건 공개 35 다 (`.claude/meta/2026-10-19-admin-rbac-m2/README.md`).
2. 개발 우회 칸 571행은 자리표시 「가드 측 스위치 (env 미확인)」 였다. M2 에서 신원 가드 소스의 스위치 호출 여부로 다시 정했다. 「있음」 571행 · 「없음」 133행이다. 자리표시 문구는 레지스트리에 없다 (검사 `#5`).
3. R12 (swagger UI 닫기) 의 「M2 착수 전 필수」 표기는 정책 문서 §7 의 채택에 따라 「진행, 완료 표기 금지」 로 바뀌었다. `apps/api/src/main.ts` 의 `setupSwagger` 분기는 그대로 두었고 R12 는 여전히 열려 있다 (미해소).
4. M1 검사 `#10` (대조기 음성 분기) 과 `#12` (허용 집합) 가 붉다는 것은 계획서 Risks 의 기록을 따른다 (이 M2 구현에서 M1 검사를 다시 돌려 확인하지 않았다, 미검증). M2 검사 `#10` 은 역할 변이 rc 1 · 빈 표 rc 2 를 본다. M1 검사 `#12` 의 허용 집합 붉음은 백로그에 이연으로 남겼다.
5. 덤프 `route-runtime-dump.cjs` 에 `scheduler` 필드를 더했다 (등록 수 · 중지 뒤 남은 수). 기존 필드는 그대로다.
