# 관리자 RBAC 마일스톤 8b 노트

## 마감 요약 (2026-10-21)

- **판정: PRD 마일스톤 8 = complete.** 마감 기준(메인 세션 확정)은 PRD 8 행 Outcome 과 증명 8 행의 코드 몫이다. 항목별 근거는 아래와 같다.
  1. 식별자 · 권한 버전 클레임: 발급 4지점이 계정 조회 앞에서 버전을 읽고 `pv` 를 싣는다 (검사 #3).
  2. 불일치 시 재해석 · Set-Cookie 재발급: `admin-token-version.spec.ts` 의 (c) 시험. 파일 40 시험 녹색 (검사 #4).
  3. 재발급 뒤 재해석 0: 같은 spec 의 (d) 시험 (재발급받은 토큰의 다음 요청은 원천 행을 읽지 않는다).
  4. 쓰기 대응표 행마다 상승 spec: `permission-bump-sites.spec.ts` 13행 (B01–B13), 행마다 식별자 · 상승 순서 · 거절 단언 (검사 #5 · #6).
  5. 재해석 중 동시 변경 spec: 격리 MySQL 두 연결 경합 3 시나리오 녹색 (검사 #7, 컨테이너 `m8b-iso-` 접두로 띄우고 내렸다).
  6. 범위 키 요청마다 원천 대조 (`(범위)` 시험 두 개), 구 토큰 이행기 (`(h)` · `(r)` 시험), `role` 클레임 유지 (`(i)` 시험이 구 서버 판정 함수 `ADMIN_ROLES.includes(payload.role)` 로 재발급 토큰을 통과시킨다). 모두 `admin-token-version.spec.ts`.
  7. 보완 계획의 PRD 미달 A·B·D (백로그 1710 · 1711 · 1713) 는 해소됐다. D 는 PRD 결정 로그 정정으로 닫았고 행 문구는 그대로다. C (백로그 1712) 는 검사 음성 기록으로 닫았고, 음성 한 건 (검사 #8) 이 아래 「게이트 결과」 에 남는다.
- **PRD 미달 목록: 없음.** 미해소 HIGH 는 없다. 백로그 1720 (검사 #14 허용 목록) 행은 최종 리뷰 R1 의 허용 목록 판단으로 해소됐으나 행 문구를 다시 쓰지 않았다 (`## 최종 리뷰` 행 6). 다음 계획이 이 행을 닫는다.
- **게이트 결과 (2026-10-21 재실행, 사이드카 `admin-rbac-m8b.gates.sh`): `GATES_RC=0`.** 검사 #1–#18 녹색. 주요 수치는 `START_SHAS_OK=1` · `JEST_NARROW passed=53 failed=0` · `JEST_BASELINE suites=72 total=753 failed=0` · `TSC_ERRORS=23` · `RACE_ITSPEC passed=3 failed=0` · `BUMP_SPEC_ROWS=13` · `SCOPE_OUT_OF_SCOPE=0` · `HIGH_CARRYOVER_LINES=70 MISS=0` · `HOST_ABS_PATHS=0` · `USER_DIRECTIVE_VERIFIED=1`.
- **이번 마감에서 한 음성 재확인** (사본 실행, 원본은 되돌렸고 `cmp` 와 `git status` 가 빈 것을 확인):
  - 검사 #9: 타입 probe 를 넣으면 tsc 가 25 를 낸다 (23 + 2). 상승 인자에 `DrizzleService` 를 넘기면 `TS2345` (DrizzleTransactionType 아님). 좁힘은 작동한다. probe 파일은 지웠다.
  - 검사 #13: 런북 헤딩을 지우면 헤딩 개수가 0 이 나와 게이트가 붉다.
  - 검사 #16: 번호 1698 의 RESOLVED-IN 행을 뺀 백로그 사본에서 `RESOLVED_IN_MISS 1698` rc 1. 원본은 rc 0.
  - 검사 #8 (미재현): `principal.spec.ts` 의 `AdminJwtAuthGuard` 생성자 넷째 인자를 뺀 사본은 36/36 녹색이었다. 원인은 `jest.config.js` 의 `ts-jest` 가 `isolatedModules: true` 라 생성자 인자 개수를 jest 시점에 보지 않고, 그 경로가 넷째 인자를 쓰지 않기 때문이다. 검사 #8 은 회귀 게이트로는 유효하지만 「가드 생성자 스텁 불일치를 잡는다」는 음성 주장은 근거가 없다. 음성 대상을 정하거나 주장을 지우는 일을 다음 계획이 맡는다 (MEDIUM, 백로그 행은 만들지 않았다).
- **검사 #11 주의**: 행 8 지문 (`f2959fd085c3df27`) 은 행 전체 문자열을 잰다. 마감에서 행 8 의 Status 와 Plan 칸을 바꿨으므로 이 검사는 이제 붉다. Outcome 칸 (세 번째 열) 지문은 마감 전후 같다 (`d6aa356b5a049fe3`). 다음 계획이 지문 대상을 Outcome 칸으로 좁힌다. 게이트 스크립트는 고치지 않았다.
- **code-review (구현 리뷰)**: 2 라운드 11행. HIGH 2 (검사 #6 줄 나눔 → 흡수, 검사 #14 허용 목록 → 이월 1720, 최종 리뷰 R1 에서 해소), MEDIUM 4 · LOW 5 (모두 흡수). 상세는 `## 코드 리뷰` 절.
- **구현 Codex 리뷰**: 라운드 2 (1 needs-attention, 2 approve). 상한 3 의 마지막 라운드는 쓰지 않았다.
- **최종 리뷰 라운드 요약**: `[{"r":1,"found":7,"absorbed":7,"high_absorbed":0,"high_open":0,"codex":"ok"}]` (fable MEDIUM 2 · LOW 2, codex MEDIUM 3, 모두 흡수). 라운드 2 는 돌리지 않았다. 흡수 뒤 재실행이 녹색이고 HIGH 와 MEDIUM 이 열려 있지 않아 루프를 끝냈다.
- **계획 게이트**: `converged` (L1 · L2 4/3 · L3 Codex 수렴). receipt 는 `.claude/receipts/mccp-plan-codex/admin-rbac-m8b.json`. 구현 receipt 는 `.claude/receipts/mccp-implement-codex/admin-rbac-m8b.json`.
- **(미검증) · 사용자 몫**
  1. R7 `ath_id` NULL 내부 관리자 운영 수 (미검증).
  2. R8 운영 env `JWT_ADMIN_ACCESS_TOKEN_EXPIRY` 설정 여부와 값 (미검증). 쿠키 maxAge 12시간은 코드 사실로만 적었다.
  3. DDL 0036 (`tn_auth_permission` · `perm_version`) 의 운영 · stg 적용 (미검증). 미적용이면 쓰기는 살고 회수는 꺼진다. 그 상태는 `skipped` warn 으로 드러난다.
  4. 혼재 배포 삭제 런북 (백로그 1698) 의 운영 적용 (사용자 몫). 구 서버 토큰은 sentinel 상승으로 회수할 수 없다.
  5. 구 서버 실기동 검증 (미검증). 혼재 spec 은 같은 판정 함수가 재발급 토큰을 통과시키는지만 본다.
  6. 실제 DB 롤백 실측 (미검증). 행별 spec 은 스텁 계약만 증명한다.
  7. 보안 리뷰어 공백: 구현 receipt 가 `security_skipped` 다. 인증 · 토큰 경로이므로 사람이 본다.
  8. 행 8 정정 (캐시 없음, 요청마다 DB 판정) 과 검사 #14 허용 목록 확대 (보고서 · mccp receipt 경로) 는 최종 리뷰의 판단이므로 사람이 확인한다.
  9. 실 jsonwebtoken · drizzle 결과 형상 (`affectedRows` 위치) 은 단위 spec 두 형상만 확인했다 (실 DB 미확인).
  10. 회수 밖의 자격 변경 (비밀번호 · 재설정 · 푸시 · refresh 칸) 은 버전을 올리지 않는다 (Risks 9).
  11. 관측 경로 `access-control/access-identity.ts` 와 `authority/authority.service.ts:73` 의 decode 는 버전을 보지 않는다 (NOT Building, 마일스톤 9 조건).
- **다음 마일스톤이 알아야 할 것**
  1. 마일스톤 9 착수 조건은 「M6 · M7 · M8 완료」다. M8 은 이제 complete 다. M6 은 PRD 표에서 아직 in-progress 라 M9 는 막혀 있다.
  2. 행 8 의 권한 판정은 요청마다 DB 원천에서 읽는다. 캐시와 재해석 기록은 없다. 전역 버전 1개만 가드가 매 요청 읽는다.
  3. 가드 생성자가 바뀌었다 (`AdminJwtAuthGuard` 에 `DrizzleService` · `ConfigService`, `AdminOptionalGuard` 에 `DrizzleService`). 새 spec 은 이 형상을 따른다. jest 설정이 `isolatedModules: true` 라 생성자 인자 타입 불일치는 jest 가 보지 않는다. 런타임에서 쓰이는 인자만 실패로 드러난다 (위 검사 #8 항목).
  4. 검사 #11 지문 대상 (위) · 검사 #8 음성 · 서브모듈 쪽 범위 검사 공백 (검사 #14 가 `insuniverse-*` 를 뺌) 은 다음 계획에서 닫는다.
  5. 백로그 1720 은 다음 계획에서 닫는다.

## 계획 게이트

- 판정: `converged` · source `hybrid` · forwardCodexVerdict=true.
  - L1 converged.
  - L2 converged: 정족수 4/3 (4 렌즈 전부 pass, blocking 0).
  - L3 converged: Codex raw approve, findings 0.
- 최종 계획 해시: `sha256:d7169732…8988` (reviewed_plan_hash). 계획서 `.claude/PRPs/plans/admin-rbac-m8b.plan.md` 는 이 해시 이후 고치지 않았다. `## 리뷰 라운드 기록` 절도 동결 상태로 둔다. 수렴 사실은 이 노트에 적는다.
- receipt: `.claude/receipts/mccp-plan-codex/admin-rbac-m8b.json` (head `c13c768`). `seal-converged.sh 8b` 로 봉인했고 `VALIDATE_EXIT=0` 이다.
- 리뷰 원문: `.claude/reviews/plan-review-admin-rbac-m8b.md`.

### intent gate 우회 사유

- 최초 write 가 intent gate 에 막혔다 (`intent_gate_verdict=incomplete`, `intent_section_present=false`). 계획서에 `## User Intent` 절(UI1~UI7)이 있는데도 게이트가 절을 읽지 못했다. 원인은 확인하지 않았다 (미확인).
- `seal-converged.sh` 가 감사 우회 사유를 달아 같은 write 를 한 번 더 불렀다. receipt 에 `intent_gate_force_override=true` 로 남는다.
- 우회 사유 원문: 「hybrid 패널과 L3 Codex 가 모두 수렴해 intent 판정 대상 지적이 없다 — mccp 1.33.6 hybrid write 의 intent gate 결함 우회」.

### 라운드별 결과

| 라운드 | L2 | L3 | 판정 | 층위 |
|---|---|---|---|---|
| R0 자가 1패스 (게이트 전) | 해당 없음 | 해당 없음 | 해당 없음 | 1 · 2 · 3 · 5 |
| 1 (`6c6d39e7`) | divergent · HIGH 포함 (정족수 미달) | divergent · high 1 · medium 1 | divergent | 1 · 2 · 3 · 4 · 5 · 6 |
| 2 (`ff6c2253`) | divergent · HIGH 포함 | divergent · HIGH 1 | divergent | 1 · 2 · 3 · 6 · 7 |
| 3 (`d7169732`, 최종) | converged · 4/4 pass · blocking 0 | converged · findings 0 | converged | 2 · 5 · 6 |

- 라운드 1·2 의 L2·L3 원판정 파일은 다음 라운드가 덮어써 남지 않았다. 위 표의 L2·L3 열은 그 라운드의 「리뷰 라운드 기록」 행(지적 A·B 열)에서 재구성했다.
- 라운드 1·2 는 수렴하지 않았고 그 뒤 계획서를 고쳤다. 판정은 divergent 로 적었다.
- 층위 열은 그 라운드에서 흡수·기각한 지적의 층 번호다. 라운드 3 은 흡수한 지적이 없고, 아래 잔존 지적의 층 번호다.

### 최종 라운드에 남은 지적 (흡수 없음)

라운드 3 은 수렴했지만 계획서를 고치지 않았다. 아래 일곱 건은 blocking 이 아니다 (MEDIUM 3 · LOW 4). 수렴 판정과 별개로 남겨 둔다.

1. security MEDIUM (층위 5): 검사 `#17` 이 `git diff` 를 `--no-ext-diff` 없이 돌려 difft 출력에서 `^+` 줄을 못 찾는다. PRD · 백로그 추가 줄 검사가 항상 0 을 낼 수 있다.
2. invariant MEDIUM (층위 6): 검사 `#14` 는 `git status` 만 본다. 기준 커밋 `c8da87c` 뒤에 이미 커밋된 허용 밖 경로는 보지 않는다.
3. invariant MEDIUM (층위 2): INV2 (부모 gitlink 동결) 를 받치는 검사가 없다. `#14` 는 `insuniverse-*` 경로를 뺀다.
4. security LOW (층위 6): `#17` 이 리뷰 기록 파일을 스캔하지 않는다.
5. test LOW (층위 6): 행별 spec 의 외부 drizzle 스텁에 `update` 가 생기면 bump 가 바깥 객체로 가도 `#6` 이 통과한다.
6. test LOW (층위 6): `#6` 블록이 마지막 `it(` 뒤 파일 끝까지 이어진다. B13 블록에 공용 헬퍼의 단언이 섞일 수 있다.
7. invariant LOW (층위 6): `#6` 블록 범위가 `describe` · `afterEach` 뒤로도 번진다. 6 과 같은 원인이다.

### 미검증

- 실제 롤백 · 운영 회수는 계획서가 밝힌 대로 미검증이다 (Acceptance · Risks 의 (미검증) 항목). 검사가 본 것은 스텁 수준 계약뿐이다.

## 착수 기록 (구현, 2026-10-20)

- 착수 sha: 부모 `c8da87c`, 서버 `288506ce`, 어드민 `1301b664`, 사이트 `fc8872a1`. 서브모듈 HEAD 세 개가 이 값과 같다 (게이트 `#1`).
- 선행 병합 여덟 개 (`ea3c0359` · `a3016d35` · `aff7ffcc` · `ccf1594b` · `16141f29` · `1de1a6d2` · `466bd611` · `55d5f699`) 가 서버 HEAD 의 조상이다 (`merge-base --is-ancestor`, rc 0).
- 사용자 지시 확인: `verify-user-directive.sh` exit 0 (세션 기록 2026-10-19T02:21:22Z, user/external). 원문은 옮기지 않는다. 이 확인으로 plan receipt 부재 상태에서 구현에 들어갔다 (전역 CLAUDE.md mccp 규칙 대체).
- 착수 시점 (구현 끝까지) 은 커밋 없이 워킹트리 미커밋 변경으로 남겼다 (사용자 지시: 별도 세션에서 code-review 후 커밋). 이후 커밋: 서버 `1ba6c4eb` · `100a9535` · `6ef1b4c9`, 부모 `83f6310`. 이 항목은 그 뒤 기록이다 (아래 「코드 리뷰」 · 「최종 리뷰」 가 현재 상태다).

## 구현 내용

- 상승 행별 spec (`insuniverse-server/apps/api/src/modules/auth/permission/permission-bump-sites.spec.ts`): 대응표 13행 (B01–B13) 각각 실제 저장소 메서드를 부른다. 행마다 (a) 업무 쓰기 → 상승 → 감사 순서 한 줄 (b) 상승 갱신 행 0 이면 `bump-not-applied` 거절 (c) 커밋 목록 빈 배열. 스텁 tx 는 쓰기를 await 시점에 기록한다 (drizzle 의 QueryPromise 와 같다). 스텁 수준 계약이며 실제 롤백은 증명하지 않는다 (미검증).
- 격리 경합 실측 (`insuniverse-server/apps/api/src/modules/auth/admin/admin-token-race.race.itspec.ts`): 격리 MySQL 두 연결. R1 (행 읽기 전 삭제 + 버전 상승) → 두 요청 모두 거부. R2 (행 읽기 뒤 행 변경 + 버전 상승) → 첫 요청은 옛 버전 3 · 옛 행, 다음 요청은 새 버전 4 · 새 행. R3 (상승과 쓰기를 연 채 커밋 전) → 옛 버전과 옛 행을 함께 본다.
- PRD 결정 로그에 「2026-10-20 정정 (admin-rbac-m8b)」 절 한 개. 행 8 문구는 바꾸지 않았다 (지문 `f2959fd085c3df27` 일치).
- CI 목록 (`insuniverse-server/.github/workflows/api.yml`) 에 `permission-bump-sites.spec.ts` 한 줄. 경합 itspec 은 넣지 않았다 (Docker 필요).

## 구현 뒤 음성 관찰 (사본에서 실행)

| 검사 | 음성 입력 (사본) | 관찰 |
|---|---|---|
| 행별 spec B01 | 관리자 갱신의 상승 호출 한 줄을 주석 처리 | B01 붉음 (성공 순서 줄 `['update:tnAdmin', 'bump', 'audit']` 불일치). 사본은 원래대로 되돌렸고 git diff 가 비었다 |
| 행별 spec B08 | 퇴사 갱신의 `await` 를 뺌 | B08 붉음. 스텁을 await 시점 기록으로 바꾼 뒤에 잡힌다 (구현 리뷰 R1 의 지적을 흡수한 결과) |
| 경합 순서 뒤집기 (정정) | 가드 판정에서 행 읽기를 버전 읽기보다 앞에 둠 (`admin-token-version.ts` 사본, 2회 실행) | R1 붉음 (Received pv 3 · alpha, `toBeNull` 깨짐), R2 붉음 (첫 요청 pv 4, 기대 3), R3 녹색. 원본은 되돌렸고 `cmp` 가 같고 git diff 가 비었다. 이전 기록 「R2 붉음, R1 · R3 녹색」 은 재현되지 않아 정정했다 (코드 리뷰 R1). R3 은 MySQL 읽기 격리를 보는 시험이라 판정 순서 음성의 대조 대상이 아니다 (읽기 순서와 무관하게 녹색이다) |
| 경합 R3 실패 뒤 정리 | R3 단언을 강제로 실패시키고 뒤따르는 R4 (행 초기화 · 단언 한 개) 를 붙인 임시 사본 | 고치기 전: R3 붉음, R4 50006 ms 뒤 붉음 (beforeEach 의 행 초기화가 연결 B 의 행 락에 막혀 시간 초과). 고친 뒤 (`try`/`finally` 로 `rollback`): R3 붉음, R4 녹색 8 ms. 실제 파일은 세 건 녹색이다 |
| 1697 반사실 | 계획 시점 관측 (`insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts` 재발급 서명기에 `expiresIn` 추가) | 계획서 「계획 시점 음성 관찰」 과 같다. 이번 실행에서 재관측하지 않았다 |

## 혼재 배포 삭제 런북

이 절은 백로그 1698 의 런북 몫이다. 운영 적용은 사용자 몫이고 이 저장소에서 실행하지 않았다.

- 조건 (a): 구 서버 전부 내린 뒤 관리자 계정을 삭제한다. 구 서버는 권한 버전 이전 배포다. 구 서버가 떠 있는 동안의 관리자 삭제는 sentinel 상승을 하지 않는다 (구 서버 삭제 경로는 이 저장소 밖이다).
- 조건 (b): 구 서버로 되돌리는 롤백은 sentinel 상승으로 되돌릴 수 없다. 구 서버 가드 (`16141f29` 의 `jwt.guard.ts`) 는 `pv` 를 읽지 않고 (0건), 계정 행도 읽지 않는다. 신원은 토큰의 `principalKind` 로만 정한다. 그래서 구 서버가 받은 관리자 토큰은 계정이 삭제된 뒤에도 만료까지 통과한다.
- 토큰 만료 길이는 환경 변수 `JWT_ADMIN_ACCESS_TOKEN_EXPIRY` 가 정하고 (`insuniverse-server/apps/api/src/config/configuration.ts:94`, 코드 기본값 없음), 운영 값은 R8 미검증이다. 로그인 쿠키의 `maxAge` 12시간은 쿠키 수명이며 토큰 만료의 상한이 아니다.
- sentinel 상승은 신 서버 안에서만 회수 수단이 된다. 구 서버 토큰의 회수 수단 (서명 키 교체 · 만료 대기) 은 사용자 판단이며 (미검증) 이다.
- 잔여 위험: 구 서버가 떠 있는 동안 관리자를 삭제하면 신 서버의 빠른 경로가 삭제된 신원을 만료까지 통과시킬 수 있다. 분석 열람 경로도 포함된다.
- 코드 측 고정: 관리자 삭제 상승 (B02) 과 기업 · 권한 그룹 삭제 상승 (B04 · B06) 을 행별 spec 이 본다.

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

1. 실행 순서. 명령 본문은 구현 Codex 게이트 (Phase 2.5, 게이트 상한 3) 를 Phase 3 구현 앞에 둔다. 이 실행은 Phase 3 을 먼저 돌렸고, 그 뒤 Phase 2.5.0 (정책 봉인 · 라운드 봉인, cap 3) 과 구현 Codex 라운드 1 을 돌렸다. 구현 뒤 리뷰라서 2.5.1 의 「계획과 같은가」 판정은 이 결과에 쓰지 않는다.
2. 두 신규 spec 파일이 편집 뒤 디스크에서 포맷이 바뀐 것을 확인했다 (원인 미확인). 코드 리뷰 R1 에서 검사 #6 이 13행 중 7행 붉은 것이 이 포맷 변경 때문임을 확인했다. 흡수: 줄이 나뉜 여덟 자리에 `// prettier-ignore` 한 줄을 달아 한 줄 형태를 고정했다 (주석 안에 설명을 같이 쓰면 prettier 가 무시 표지로 읽지 않는다, 실측).
3. 보고서 경로와 구현 receipt 경로. 명령 본문은 `.claude/PRPs/reports/admin-rbac-m8b-report.md` 에 보고서를, `.claude/receipts/mccp-implement-codex/admin-rbac-m8b.json` 에 구현 receipt 를 쓴다. 계획 검사 `#14` 의 허용 목록에는 두 경로가 모두 없다. 코드 리뷰 R1 실측 (`#14` 블록 재실행): 두 경로 모두 OUT_OF_SCOPE 로 찍히고 rc 1 이다. 검사 본문과 허용 목록은 계획 해시에 묶여 바꾸지 않았다 (사용자 판단).
4. 계획서는 손대지 않았다 (동결). 구현 리뷰 기록은 이 노트에 둔다.

## Codex 구현 리뷰 (라운드 1 of 3)

- 호출: `codex-invoke.js adversarial-review` (fail-closed Bash wrapper), 대상은 서브모듈 워킹트리 미커밋 변경 (부모 diff 는 gitlink 와 PRD · api.yml 뿐이라 focus 에 서브모듈 경로를 적었다).
- 판정: `needs-attention` (구조화 verdict, 결정 판정 아님 · 코드 검토). 지적 1건, medium.
- 지적 (흡수): 스텁이 `set()` · `where()` 시점에 쓰기를 기록해서, 업무 쓰기의 `await` 를 빼도 순서 단언이 녹색이었다 (B08 로 재현). 흡수: 쓰기를 `await` 시점에 기록하는 thenable 로 바꿨다. 반사실: 퇴사 갱신의 `await` 를 뺀 사본에서 B08 붉음, 원본 복원 후 13/13 녹색.
- CRITICAL · HIGH 없음. 같은 리뷰가 확인한 것: R1~R3 훅 위치와 기대값이 판정 순서 계약과 맞는다.
- 리뷰어 샌드박스의 jest 캐시 쓰기 오류는 리뷰어 환경의 일이며 이 실행의 jest 는 녹색으로 돌았다.

## Codex Implementation Review

- 호출: `codex-invoke.js adversarial-review` (fail-closed Bash wrapper). 라운드 상한 3 (정책 봉인 cap=3, 라운드 원장 키 `mccp-implement-codex__admin-rbac-m8b`).
- 라운드 수: 2 (라운드 1 needs-attention · 라운드 2 approve). 상한 3 의 마지막 라운드는 쓰지 않았다.
- 합치 결론: 라운드 1 의 medium 1 건 (스텁이 await 누락을 못 잡음) 을 흡수했고 라운드 2 가 approve 했다. CRITICAL · HIGH 없음.
- YAGNI 분류 (구현 결정): 새 헬퍼 없음 (spec 내부 스텁 2 개).
- 계획 밖 파일 변경 한 개: 서버 `permission-admin.service.spec.ts` (커밋 `1ba6c4eb`, 인라인 상승 시험 추가 · 코드 리뷰 R2 흡수). 계획 Files to Change 표에 없다. 검사 `#14` 는 서브모듈 경로를 보지 않아 이 이탈을 막지 않는다. CI 목록 (api.yml) 에 이미 있는 spec 이라 위험은 낮다 (최종 리뷰 R1 의 LOW 정정).
- 이월 (DEFER_TO_BACKLOG): 백로그 1720 (검사 `#14` HIGH) 한 건. 코드 리뷰 R1 이 append 했다 (커밋 `83f6310`). 허용 목록 판단은 최종 리뷰 R1 에서 흡수했다 (아래 「최종 리뷰」 행 6).
- Open Questions: 없음 (auto-CRITICAL 항목 없음).
- Security Reviewer: unavailable, skipped (auto-fallback): 이 실행은 서브에이전트 도구가 없어 security-reviewer 를 부르지 못했다. 인증 · 토큰 경로를 건드리므로 이 공백은 사용자 · 최종 리뷰 몫으로 남긴다. 이 항목 때문에 구현 receipt 에 security_skipped 가 붙는다.
- Design Review: impeccable 스킬은 열렸으나 설계 신호 0 (`no-signal`, silent-skip). 변경은 서버 spec 과 CI 목록뿐이라 화면 설계 대상이 아니다.
- Design Finish · Design Grounding: 설계 트리거 미충족으로 생략 (3.6 · 3.7 no-op).
- 구현 Codex 리뷰 원문은 저장소 밖 Git 디렉터리 아래 (`mccp/tmp`) 에 두었다. 이 노트에는 판정만 옮겼다.

## 코드 리뷰

범위: 부모 `.claude/notes` · 서버 spec 두 개 · PRD 정정 절. 출처는 code-review 레인 두 개 (code-review · typescript · security 로 나뉜 지적을 같은 원인끼리 합쳤다). 층위는 리뷰 체크리스트의 층 번호다.

| 라운드 | 출처 | 심각도 | 지적 | 판정 | 증거 | 층위 |
|---|---|---|---|---|---|---|
| 1 | code-review · typescript | HIGH | 검사 #6 이 13행 중 7행 붉다 (B01 B05 B06 B10–B13 이 줄 나눔으로 awk 에 안 잡힌다). 보고서 「#6 녹색」 과 노트 「같은 검사를 통과한다」 는 거짓이었다 | 흡수 | 고치기 전 `#5`·`#6` 블록 실행: `SPEC_ASSERT_MISS B01` rc 1. 여덟 자리에 `// prettier-ignore` 한 줄 (설명은 그 윗줄). 고친 뒤 `#5`·`#6` rc 0 · `BUMP_SPEC_ROWS=13` · jest 13/13 녹색 · prettier --check 통과 | 2 |
| 1 | code-review · typescript | HIGH | 검사 #14 가 현재 워킹트리에서 붉다. 허용 목록에 보고서 경로와 구현 receipt 경로가 없고, 보고서의 「GATES_RC=0」 과 「#14 녹색 시점 0건」 은 낡았다 | 미해소 (사용자 판단) · 기록만 고침 | `#14` 블록 실행: `OUT_OF_SCOPE .claude/PRPs/reports/admin-rbac-m8b-report.md` 와 `OUT_OF_SCOPE .claude/receipts/mccp-implement-codex/admin-rbac-m8b.json`, rc 1. 허용 목록은 계획 해시에 묶여 고치지 않았다. 보고서 · 마감 요약 · 편차 3 을 현재 상태로 고쳤다. 이월 줄은 백로그 append | 2 |
| 1 | code-review | MEDIUM | PRD 정정 절이 구현에 없는 「재해석 기록 키」 를 구현 사실처럼 적는다 | 흡수 | PRD 에서 「재해석 기록 키다」 1건 → 0건. 「재해석 기록은 두지 않는다」 로 고쳤다. 행 8 문구 불변 (`#11` 지문 `f2959fd085c3df27` 일치 · 정정 절 1건) | 1 |
| 1 | code-review | LOW | R3 시험이 단언 실패로 끝나면 연결 B 의 트랜잭션이 열린 채 남아 뒤 시험의 행 초기화가 락에 막힌다 | 흡수 | 고치기 전 임시 사본 (R3 강제 실패 + R4 추가): R4 50006 ms 붉음 (락 대기 시간 초과). 고친 뒤 (`try` / `finally` 의 `rollback`): R4 녹색 8 ms, R3 는 여전히 붉다 (강제 실패가 정상 동작). 실제 파일 세 건 녹색 · prettier 통과 | 4 |
| 1 | security · typescript | LOW | 판정 순서 뒤집기 음성 기록 「R2 붉음, R1 · R3 녹색」 이 재현되지 않는다 (R1 도 붉다) | 흡수 (기록 정정) | 사본 2회 실행: R1 붉음 (Received pv 3 · alpha), R2 붉음 (첫 요청 pv 4, 기대 3), R3 녹색. 음성 표 · 보고서 음성 줄을 고쳤다. 원본 복원 뒤 `cmp` 일치 · git diff 빔 | 2 |
| 1 | typescript | LOW | 음성 표의 「R3 녹색」 이 순서 음성과 같은 급 증거로 읽힌다. R3 은 MySQL 읽기 격리를 보는 시험이라 읽기 순서와 무관하게 녹색이다 | 흡수 (기록 명시) | 음성 표 행에 「R3 은 판정 순서 음성의 대조 대상이 아니다」 를 적었다 (사본 실행 두 번 모두 R3 녹색) | 6 |
| 2 | code-review | MEDIUM | 역할 권한 편집의 인라인 버전 상승 (`permission-admin.service.ts` 의 `permVersion + 1`) 에 검사가 없다. spec 은 update 를 기록만 하고 단언하지 않는다 | 흡수 | `permission-admin.service.spec.ts` 에 「권한 쓰기는 같은 트랜잭션에서 버전을 올린다」 시험 추가 (update 가 `permVersion` 을 싣고, 실행 순서가 delete → insert → update → read-sentinel → insert). 반사실: 인라인 상승 아홉 줄을 지운 사본에서 그 시험 붉음 (1 failed / 12), 원본 복원 뒤 `cmp` 일치 · 12/12 녹색. 검사 #5 · #6 은 넓히지 않았다 (13행 대응표 밖이고 계획서 동결). 음성 자리는 spec 행 하나로 고정 | 2 |
| 2 | code-review | MEDIUM | 게이트 #15 가 작업 트리 백로그의 R1 행 (1720, 「검사 #14」 HIGH) 때문에 붉다 (`MISS 1720`). 계획서 본문은 해시로 동결돼 그 번호를 넣을 수 없다 | 흡수 (검사 전제 고침) | `admin-rbac-m8b.gates.sh` 의 #15 가 번호 집합을 계획 커밋 `c13c768` 의 백로그에서 센다 (`git show`). 실측: 작업 트리는 71행, 계획 커밋은 70행 · `HIGH_CARRYOVER_LINES=70 MISS=0` rc 0. 음성: 번호 1697 을 지운 계획서 사본에서 `MISS 1697` · `MISS=1`. 1720 행은 다음 계획 몫으로 백로그에 그대로 둔다 (다시 쓰지 않음) | 6 |
| 2 | code-review | LOW | jest race 설정의 드리즐 스텁 매퍼가 제거되지 않는다. base 의 `^drizzle/.*$` 키가 먼저 매치되어 스텁이 이긴다 | 흡수 | `#7` 의 race 설정 생성부가 base 의 `^drizzle/` 키를 뺀 뒤 병합한다. 노드로 첫 매치를 비교: 고치기 전 `^drizzle/.*$` → `test/jest-drizzle-stub.js`, 고친 뒤 `^drizzle/(.*)$` → `drizzle/$1`. 검사 #7 재실행: `RACE_ITSPEC passed=3 failed=0 pending=0` (녹색 유지) | 2 |
| 2 | typescript | LOW | 계획 관측의 호출 사이트 파일 수 「9파일」 은 실측과 다르다 (상승 호출 파일은 7개) | 흡수 (기록 정정) | 계획서는 동결이라 고치지 않는다. `grep -rl "bumpPermissionVersion(tx)"` (spec 제외) 실측 7파일 · 합계 13 은 맞다. 정정은 이 표에 적는다 | 1 |
| 2 | typescript | MEDIUM | (B10) · (B11) spec 은 호출자가 준 tx 분기를 덮지 않는다. M9 가 강제를 켜면 이 분기가 실제 경로가 된다 | 흡수 | `permission-bump-sites.spec.ts` 의 (B10) · (B11) 블록에 stub tx 를 직접 넘기는 시험 추가 (순서 `update:tnMember` → `bump`, 상승 0행이면 `bump-not-applied` 거절, `drizzle.transaction` 재호출 없음). 반사실: `agency-fc.repository.ts` 의 두 상승을 감싸는 쪽으로만 옮긴 사본에서 (B10) · (B11) 붉음 (2 failed / 13), 원본 복원 뒤 `cmp` 일치 · git diff 빔 · 13/13 녹색. 검사 #6 은 여전히 `BUMP_SPEC_ROWS=13` · 행마다 세 단언 통과 | 6 |

## 마감 요약 (2026-10-20, 구현)

- 게이트 결과: 사이드카 `.claude/PRPs/plans/admin-rbac-m8b.gates.sh` 종료 코드 0. 검사 번호별 결과는 보고서 `.claude/PRPs/reports/admin-rbac-m8b-report.md` 의 검증 표.
- 검사 `#14` 는 코드 리뷰 R1 때 허용 목록 밖 두 경로 (보고서 · 구현 receipt) 로 붉었다. 최종 리뷰 R1 에서 두 경로를 허용 목록에 더하고 범위를 기준 커밋 뒤 커밋분까지 넓혔다 (아래 「최종 리뷰」 행 6). 이 마감 요약의 「종료 코드 0」 은 코드 리뷰 R1 이전의 값이다. 현재 값은 「최종 리뷰」 의 재실행 기록이 정본이다.
- 구현 receipt 는 codex_verdict=converged (라운드 2 approve) 와 security_skipped 로 봉인했다. 보안 리뷰어 공백은 최종 fable · codex 리뷰가 다시 본다.
- 구현 변경은 커밋했다 (서버 `1ba6c4eb` · `100a9535` · `6ef1b4c9`, 부모 `83f6310`). 다음 단계: 최종 리뷰 R1 반영 커밋 (이 절이 들어 있는 커밋) 뒤 최종 fable · codex 리뷰.
- 미검증 항목은 보고서 「미검증 · 사용자 몫」 과 같다.

## 최종 리뷰

리뷰어: fable · codex (메인 세션이 전달한 최종 리뷰 지적). 라운드 상한 3 (사용자 지시 · skip-receipt 규칙). 이 절은 라운드 1 의 판정과 그 반영 뒤 검사 재실행 기록이다. 층위는 리뷰 체크리스트의 층 번호다.

### 라운드 1 (1/3)

| 라운드 | 리뷰어 | 심각도 | 지적 | 판정 | 증거 | 층위 |
|---|---|---|---|---|---|---|
| 1 | fable | MEDIUM | 행별 spec 의 스텁 트랜잭션이 중첩 · 분리된 두 번째 트랜잭션을 구분하지 못해 「같은 트랜잭션 상승」 을 증명하지 않는다 (`permission-bump-sites.spec.ts`) | 흡수 | 스텁 `transaction` 이 두 번째 호출에서 `same-transaction-violated` 를 던진다 (서버 `b2c476a8`). 반사실: `admin.repository.ts` 의 상승 한 곳을 `this.drizzle.transaction(...)` 으로 감싼 사본에서 (B01) 붉음 (1 failed / 13), 원본 복원 뒤 `cmp` 일치 · git diff 빔 · 13/13 녹색 | 6 |
| 1 | fable | MEDIUM | 검사 `#17` 의 PRD · 백로그 팔이 부모의 `diff.external=difft` 때문에 항상 0 이다 | 흡수 | `git diff --no-ext-diff` 로 고쳤다. 같은 입력 실측: 외부 diff 의 `+` 줄 0, `--no-ext-diff` 의 `+` 줄 13. 재실행 `HOST_ABS_PATHS=0` (실제 누출은 없었다) | 5 |
| 1 | fable | LOW | 이 절 바깥 기록 네 건: 계획 밖 spec (`1ba6c4eb`) 을 「없음」 이라 적음 · 「이월 없음」 인데 백로그 1720 을 append 함 · 「커밋 없음」 문구 · 보고서 「#15 붉음」 | 흡수 (기록 정정) | 앞 절의 「계획 밖 파일 변경」 · 「이월」 · 「착수 시점 상태」 · 「마감 요약」 행을 현재 상태로 고쳤다. 보고서 편차 · 변경 파일 표도 고쳤다. 서브모듈 쪽 범위 이탈을 보는 검사는 여전히 없다 (LOW, 기록만) | 1 |
| 1 | fable | LOW | 검사 `#1` 이 착수 sha 와 서브모듈 HEAD 의 등호를 요구해 커밋 뒤 첫 검사에서 멈춘다 | 흡수 | 등호를 `merge-base --is-ancestor` 로 바꿨다. 음성: 없는 sha `deadbeef00` 은 rc 128. 양성: `288506ce` rc 0. 재실행 `START_SHAS_OK=1` · 조상 8줄 | 5 |
| 1 | codex | MEDIUM | `permission-admin.service.spec.ts` 의 버전 상승 시험이 값의 존재만 본다. `+ 1` 을 `+ 0` 으로 바꿔도 통과한다 | 흡수 | `MySqlDialect().sqlToQuery` 로 식을 렌더링해 끝이 `+ 1` 인지 단언했다 (서버 `b2c476a8`). 반사실: service 의 `+ 1` 을 `+ 0` 으로 바꾼 사본에서 「같은 트랜잭션에서 버전을 올린다」 붉음 (1 failed / 12), 원본 복원 뒤 `cmp` 일치 · 12/12 녹색 | 2 |
| 1 | codex | MEDIUM | 검사 `#14` 가 워킹트리만 보아 커밋된 허용 밖 경로가 커밋하는 순간 사라진다. 커밋분에 보고서 · mccp receipt 가 들어 있다 | 흡수 (판단 기록) | 범위를 기준 커밋 `c8da87c` 뒤 커밋분 + 워킹트리로 넓혔다. 허용 목록에 `.claude/PRPs/reports/admin-rbac-m8b-report.md` 와 `.claude/receipts/mccp-(plan\|implement)-codex/admin-rbac-m8b.json` 을 더했다. 근거: 둘 다 M1–M7 과 같은 mccp 기록 채널이고 plan receipt 와 같은 형식이다. 허용 목록 확대가 판단 쟁점이므로 다음 최종 리뷰가 다시 본다. 재실행 `SCOPE_OUT_OF_SCOPE=0` | 6 |
| 1 | codex | MEDIUM | 검사 `#1` 이 커밋 상태에서 실행되지 않는다 (fable LOW 와 같은 원인이라 한 번에 닫힌다) | 흡수 | 위 fable LOW 행의 조상 검사 변경 | 5 |

- 흡수 합계: 7건 (fable MEDIUM 2 · fable LOW 2 · codex MEDIUM 3). 기각 0. HIGH 0. CRITICAL 0. codex 의 `#1` 지적은 fable LOW 의 `#1` 지적과 원인이 같아 한 수정으로 닫았다.
- 미해소 HIGH · MEDIUM 0. 백로그 append 없음. 백로그 1720 (검사 `#14` HIGH) 은 다시 쓰지 않는다. 그 전제 (허용 목록 밖) 는 이 라운드의 허용 목록 판단으로 해소됐다 (행 6).
- 서브모듈 범위 검사 공백 (LOW, 기록만): 검사 `#14` 는 `insuniverse-*` 경로를 빼므로 서브모듈 안의 범위 이탈 (`1ba6c4eb` 의 spec) 을 보지 않는다. 다음 계획에서 검토한다.

### 라운드 1 반영 뒤 검사 재실행

- 실행: 사이드카 `.claude/PRPs/plans/admin-rbac-m8b.gates.sh`, 서버 커밋 `b2c476a8` 뒤 · 부모 커밋 전 워킹트리에서 한 번 돌렸다. 부모 커밋은 이 절과 게이트 스크립트를 싣기만 하므로 검사 대상은 같다.
- 결과: `GATES_RC=0`. 검사 #1–#18 전부 녹색.
  - `JEST_NARROW passed=53 failed=0` · `JEST_BASELINE suites=72 total=753 failed=0` · `TSC_ERRORS=23` · `RACE_ITSPEC passed=3 failed=0 pending=0`
  - `BUMP_SPEC_ROWS=13` · `BUMP_SITES=13` · `BUMP_NEAR_TX_BAD=0` · `SIGNIN_VERSION_SITES=4 PV_SITES=4` · `CI_SPEC_ENTRIES=1/1` · `REISSUE_SIGNER_OPTIONS=0 EXP_SPEC=1` · `RUNBOOK_1698=1/1/4`
  - `SCOPE_OUT_OF_SCOPE=0` · `HIGH_CARRYOVER_LINES=70 MISS=0` · `CARRYOVER_HANDLED=6 APPLIED=6` · `HOST_ABS_PATHS=0` · `USER_DIRECTIVE_VERIFIED=1`

### 미검증 (이 라운드 범위 밖)

- 최종 리뷰 자체의 결과 (round 2 이후) 는 이 절에 없다. 이 절은 R1 반영 기록이다.
- 앞 절의 미검증 항목 (실제 롤백 · 운영 회수 · R7 · R8) 은 그대로 사용자 몫이다.
