# 관리자 RBAC 마일스톤 5 노트

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

- 상태: M5 를 완료로 표시했다 (PRD 마일스톤 5 행 `complete`, plan `.claude/PRPs/plans/admin-rbac-m5.plan.md`). 단 PRD M5 Outcome 의 「실제 대상 · 목록 조건으로 would-deny 를 잰다」는 하지 않았다. 아래 「PRD 편차」 참조.
- 서버: 서브모듈 포인터를 `f9072f75` (접근 감시 모듈 prettier 정렬 · HEAD 출력과 바이트 동일 확인) 로 맞췄다. 서버 워킹트리는 clean 이다. 이전 노트의 「미커밋 4파일」 미검증 항목은 이 커밋으로 정리됐다.
- 게이트: 계획서 Validation 블록 #1 · #2 · #14 · #15 · #16 을 이 마감에서 다시 돌려 통과했다. #3–#13 은 격리 실행이 필요해 「마감 재검증」 절의 결과를 인용한다 (#8 · #9 · #10 · #11 · #12 · #13 통과, 기준 HEAD `d311e0bb` · 부모 `9989d44`).
- 검사 #14: 사이드카 판정을 닫힌 태그 집합(RESOLVED-IN · HIGH · MEDIUM · LOW · FAIL · REJECTED) 으로 넓히고 「RESOLVED-IN 1건 이상」을 더했다. 통과 (rc 0, 새 줄 37건 중 RESOLVED-IN 6). 음성: 닫힌 집합 밖 태그 `NOTE` 한 줄을 넣은 사본에서 BADN 1 · rc 1 을 확인했다.
- 리뷰: 계획 게이트 상한 3라운드 divergent (receipt 없음, 사용자 skip-receipt 지시로 진행). 최종 리뷰 R1 · R2, 구현 뒤 codex 1회의 지적 중 미해소 항목은 전부 backlog 에 있다 (이 마일스톤이 덧붙인 줄 37건: HIGH 9 · MEDIUM 16 · LOW 5 · FAIL 1 · RESOLVED-IN 6). 다음 계획 반영 대상은 「→ 다음 계획 반영 대상」 표지로 표시했다.
- 판정: 「마감 판정」 절 (끝) 참조. 검사 #9 기대 12 수용 · 검사 #14 사이드카 수정 · F2 · C2 backlog · 완료 여부 (편차 명시 뒤 완료).
- PRD 편차 (C2): 범위 감시 헬퍼 `scope-monitor` 의 프로덕션 호출자가 0 이다. 범위 would-deny 는 M5 에서 재지 않았고 측정은 M6 Task 1 몫으로 옮겼다. M6 착수 조건은 「범위 would-deny 측정 완료」 그대로다.
- (미검증): 격리 검사 #3–#13 의 이번 마감 세션 재실행 (앞 절 인용), 실트래픽 수치, 롤백 시험 (계획이 (미검증) 으로 표기), 혼합 신원 감시 시험 (backlog).
- M6 가 알 것:
  1. Task 1 로 범위 감시를 실제 목록 · 단건 경로에 배선하고 would-deny 를 격리 DB 에서 측정한다. 측정이 끝나기 전에는 범위 강제 코드를 쓰지 않는다 (backlog 2026-10-19 HIGH 줄).
  2. `scope-monitor` 의 판정은 orgPath 동치 비교라 현행 부분트리 허용과 어긋난다 (backlog MEDIUM). M6 전제로 prefix 비교를 적는다.
  3. 자기 판정 엔드포인트 `GET /admin/super-admin/permission` 이 rbac.manage 로 분류돼 의도된 차단 12행에 들어 있다 (backlog HIGH). M9 판단 표에서 의도된 차단이 아님을 적는다.
  4. 감시 슬롯은 응답 없는 소켓에서 풀 상한까지 잡힌다 (backlog HIGH 잔여). 연결 수명 설계 몫이다.
  5. 계획 :73 의 「현행 가드에는 최고관리자 가드가 없다」는 사실 오류다 (최고관리자 전용 27행은 SuperAdminGuard 뒤). 본문은 동결이라 다음 계획에서 고친다.

## 계획 게이트

계획서 `.claude/PRPs/plans/admin-rbac-m5.plan.md` 의 mccp 계획 게이트가 상한 3라운드(`ROUNDS=3/3`)를 다 쓰고 divergent 로 끝났다. 수렴하지 못했다. 계획서는 고치지 않았다 (R3 판 해시 `sha256:9da673c5e9bd60b32fc8ada502fe4011ea3c2d01f623573bd0daa878b1a16ccf`, R2 를 흡수한 계획 커밋 `91c6507`). 판정 원문 `decision.json` · `l2.json` · `l3.json` · `l3-findings.json` 은 `.claude/state/plan-review/` 에 있고 gitignore 대상이라, 정본은 리뷰 기록 `.claude/reviews/plan-review-admin-rbac-m5.md` 와 이 노트다.

R3 판정: `review_verdict=divergent` · `review_source=multi-agent` · `review_proof=null` · `forwardCodexVerdict=false` · 사유 `L2 quorum not satisfied: 3 blocking finding(s): test/HIGH, test/HIGH, test/FAIL`. L1 converged (위반 0). L2 응답 4/4 (architect · security · invariant pass, test fail). L3 Codex divergent (raw `needs-attention`, high 1). L3 high 는 `forwardCodexVerdict=false` 라 정족수 판정에 반영되지 않았다.

| 라운드 | 계획 해시 | L2 (4역할) | L3 (Codex) | 처리 |
|---|---|---|---|---|
| R1 | `sha256:f96d2197…` | converged · 정족수 통과 · 지적 14건 (MEDIUM 7 · LOW 7) | divergent (high 1) | `f3fe86b` 에서 계획 반영 |
| R2 | `sha256:4948e925…` | divergent · 정족수 실패 · blocking 4 (architect HIGH · FAIL, test HIGH · FAIL) | divergent (high 2) | `91c6507` 에서 계획 반영 |
| R3 (상한 3/3) | `sha256:9da673c5…` | divergent · 정족수 실패 · blocking 3 (test HIGH 2 · FAIL 1) | divergent (high 1) | 흡수하지 않음 · 아래 상한 처리 |
| 구현 진입 (상한 뒤) | `sha256:9da673c5…` | — | — | 기각 — 사용자 지시 확인(verify-user-directive.sh, 2026-10-19T02:21Z 원문). validate rc 2 (mccp-plan-codex 영수증 부재, skip-receipt 지시로 기록만 함) |

R3 은 R2 흡수 판을 검토한 마지막 라운드다. R3 이후에는 계획을 고치지 않았다.

### 상한 처리

- 사용자 지시 (세션 입력 원문, `verify-user-directive.sh` exit 0 으로 확인): 「Round 는 최대 3까지만 진행하고 마지막 라운드는 CRITICAL 이상 건이 아닌이상 skip-receipt로 진행해줘. (환경 변수도 적용). HIGH건은 backlog에 기록해서 다음 plan 작성에 반영하는 방법으로 진행.」 이 지시가 전역 CLAUDE.md mccp 한시 규칙의 「receipt 없음 → 멈춤」을 대체한다.
- CRITICAL 유무: 없음. R3 의 최고 등급은 HIGH 3건 (L2 test HIGH 2 · L3 codex HIGH 1). 나머지는 MEDIUM 2 · LOW 5 이고, test 패널 판정 FAIL 1 은 위 test HIGH 2건과 MEDIUM 2건을 묶은 판정이라 독립 지적이 아니다.
- receipt: 쓰지 않았다 (plan 게이트 receipt write 시도 금지, intent gate 가 fail-closed 로 막는다). `receipt_path` 없음. 수렴 판정이 아니다. `MCCP_SKIP_INTENT_GATE` 는 쓰지 않았다.
- 구현 진입 검증: `MCCP_SKIP_RECEIPT=1 node <mccp 플러그인 캐시>/mccp/1.33.6/scripts/receipt/cli.js preflight --command mccp:prp-implement --decision admin-rbac-m5 --plan .claude/PRPs/plans/admin-rbac-m5.plan.md` → `"ok": true` · `"bypassed": true` · `"reason": "MCCP_SKIP_RECEIPT"` (exit 0). env 는 그 명령에만 주었다. 설정 파일과 세션에는 두지 않았다.
- 계획서 · `l2.json` · `decision.json` 은 고치지 않았다.

### backlog 로 넘긴 항목

`.claude/plans/codex-findings-backlog.md` 끝에 2026-10-19 항목 11줄을 덧붙였다. 기존 줄은 고치지 않았다. 같은 라운드 항목은 backlog 에 없었으므로 11줄 모두 새로 적었다 (기존 m5 줄 5개는 M4 이월의 RESOLVED-IN 이라 이 라운드와 무관하다).

HIGH 3건 (사유 끝에 `→ 다음 계획 반영 대상`):

1. test HIGH: jest 기준선만 재고 사후 비교 게이트가 없다. 구현 뒤 기존 `*-access.spec` (권한 가드 매트릭스 시험) 과 재생성 카탈로그 시험이 어떤 검사에도 안 돈다 (`.claude/PRPs/plans/admin-rbac-m5.gates.sh:38-44` · `:82-95`, 계획 Task 1).
2. test HIGH: 응답 불변의 근거인 `diff=0` 줄을 스크립트가 자기 보고로 찍는다. before · after row 를 대조하지 않아 `diff=0` 을 항상 찍는 probe 도 검사 #8 을 통과한다 (`.claude/PRPs/plans/admin-rbac-m5.gates.sh:174`, 계획 `:221`).
3. L3 codex HIGH: DB 작업을 취소하지 않고 완료까지 감시 슬롯을 쥐게 해서, DB 작업 두 건이 멈추면 이후 판정이 전부 `skipped` 로 빠진다. 정지 시험은 있으나 복구를 검증하지 않는다 (계획 `:207-209`, `.claude/state/plan-review/l3-findings.json`).

MEDIUM 2 · LOW 5 는 기록만 남겼다. 목록은 backlog 에 있다 (counterfactual 자기 보고 줄 · 신원 해석기 무예외 검사 이름 · PermissionAdapter 제공 모듈 · note 7 과장 · 클래스 표지 잔여 위험 · 슬롯 알람 · 롤백 미시험).

### 구현 진입 시 주의

- 계획 본문은 동결이라 HIGH 3건이 계획에 그대로 남아 있다. 구현 뒤 검사로 확인하기 전에는 이 셋을 해소된 것으로 적지 않는다.
- jest 기준선 대조 게이트가 없으므로, 구현 중에는 기존 가드 시험 (`insuniverse-server/apps/api/src/modules/auth/admin/admin-roles-access.spec.ts` 등) 을 직접 돌려 회귀를 본다.
- 구현 진입은 `MCCP_SKIP_RECEIPT=1` 로만 열린다. 이 상태의 receipt 는 `skipped` 로 찍히므로 수렴 증거가 아니다.

## 구현 (2026-10-19, prp-implement)

### 진입 조건

- 사용자 지시 확인: `verify-user-directive.sh` exit 0 (2026-10-19 02:21, user/external). 라운드 상한 3 의 마지막 라운드는 CRITICAL 이 없으면 skip-receipt 로 진행. 그래서 plan receipt 부재로 멈추지 않았다.
- 착수 sha: server `76e25be7` · admin `146ed4d6` · site `b4200ff2` · 부모 `91c6507a`. 조상 검사 `#1` 통과.
- 커밋하지 않았다 (UI3: 구현 뒤 별도 세션 code-review). 서브모듈 작업 트리에 이 세션이 만들지 않은 변경이 있다: `apps/api/src/modules/auth/permission/permission-admin.service.ts` (줄바꿈 정리 4줄, 구현 시작 시점부터 존재). 건드리지 않았다. 나중 커밋에 싣지 않는다 (정책 §5-7).
- 기준선 (sha 76e25be7, 이 세션에서 재측정): jest 실패 suite 6 · 실패 시험 3 (M4 기준과 같다). tsc 오류 23 줄 (M4 와 같다). Redis 는 `127.0.0.1:6380` 일회용 (treat spec 이 그 주소를 고정).

### 구현한 것 (Task 별)

- Task 1: 기준선 파일 작성 (`.claude/meta/2026-10-19-admin-rbac-m5/jest-baseline.txt`, `tsc-baseline.txt`).
- Task 2: 접근 계약 spec 은 이미 녹색이다. 빈 레지스트리 기대 (`RULE_UNKNOWN_PATH`) 는 맞으므로 고치지 않았다. 「재현 없음」 (backlog 1529).
- Task 3 · 4: `build-catalog.mjs` 개정. 범위 키 열 (`$11`) 에 「최고관리자 전용」 이 드러나는 관리자 행은 `스템.read-super|write-super` 로 나가고 `superOnly` 에 든다. 경로 맵 `route-permission-map.ts` (440행, 키 충돌 시 생성 실패). 골든 `route-map` 행 440. 코드 110 (M4 109), 적재 101 (M4 108), 최고관리자 전용 9 (rbac.manage 1 + -super 8). -super 행 합 27.
- Task 5: 적재 표지 `__seeded__` (ath 0). 권한 집합 조회는 표지 코드 두 개를 거른다. 적재 스크립트는 표지가 있으면 전원 회수 뒤에도 재실행을 거부한다.
- Task 6: 세 인증 가드 `getAllAndOverride`. RolesGuard 는 핸들러 표지만 존중하고, 신원이 없으면 403.
- Task 7: `access-identity.ts` (비throw, 표지 admin · 회원 · view-token · external-basic · invalid · none).
- Task 8: `access-control/` (상수 · 전역 가드 · 인터셉터 · 모듈 · 시험). 최고관리자 조회 본문은 `super-admin.query.ts` 로 옮겼고 `SuperAdminService` 는 위임한다 (동작 불변, 기존 spec 녹색). 어댑터는 감시 연결 핸들 `ACCESS_CONTROL_DB` 하나를 받는다.
- Task 9: `scope-monitor.ts` (인터셉터는 부르지 않는다).
- Task 12: `access-control.mixed.itspec.ts` (8건, 실제 가드 체인, HTTP).
- Task 13: M4 `isolated-check.sh` 를 M5 기대 (`loaded-diff.txt`) · `__seeded__` 제외 · 요약 줄로 개정. M4 `adapter-probe.ts` 생성자 변경 반영.

### 편차 (계획 대비)

1. `api.yml` 검증 목록에 신규 spec 과 개정된 `super-admin.service.spec.ts` 를 더했다.
2. `permission-admin.service.spec.ts` 의 픽스처 코드 `alert-admin.read` 를 `banner-admin.read` 로 바꿨다. M5 분류로 그 코드가 전부 최고관리자 전용이 되어 카탈로그에서 사라졌다 (회귀를 고쳤다).
3. `mixed-identity.sh` 는 격리 MySQL 을 띄우지 않는다 (가드 체인이 스텁 두 개만 쓴다).
4. 감시 집계는 HTTP 엔드포인트가 아니라 `SIGUSR2` 덤프로 읽는다.
5. `permission.adapter.ts` 는 계획의 「ACCESS_CONTROL_DB 제공 모듈」 을 `AccessControlModule` 에 둔다 (backlog 1535 LOW 항목).
6. `isolated-*.sh` 가 쓰는 스키마 덤프는 `security-review` 격리 덤프다 (M4 와 같다). stg 에서 떠진 것인지는 확인하지 못했다 (미검증).

### 게이트 중간 결과

- 검사 #1 · #2 · #3 · #4 · #5 · #6 · #7: 통과 (#5 는 tsc 신규 오류 0, 신규 spec 녹색).
- 검사 #10: 8건 녹색 (`it(` 8, numTotalTests 8).
- 검사 #8 · #9 · #11 · #12 · #13: 격리 실행 중이다. 결과는 아래 「최종 결과」 에 적는다.
- 검사 #14 는 구조상 붉다 (아래 「게이트 설계 결함」).

### 구현 중 실행 기록 (2026-10-19, 이어받은 세션)

- 앞 에이전트가 띄운 격리 probe 는 끝까지 돌았다 (행 6336 · 6336). 그러나 서버가 행 5132 에서 죽었다. 원인: 격리 DB 에 저장 함수 (`GET_STATUS` · `FN_DECRYPT` 등) 가 없어 `GET /admin/order-detail/download` 의 엑셀 오류 경로가 응답 뒤에 헤더를 다시 써 `ERR_HTTP_HEADERS_SENT` 로 프로세스가 죽었다. 두 빌드 모두 같은 지점에서 죽었다 (기준 트리의 기존 동작, M5 무관). 그 뒤 행은 `status=0` 이다.
- 하네스 수정 (`_lib.sh`, 메타 디렉터리 안): (1) 저장 함수 덤프를 스키마 덤프와 같은 디렉터리의 `01-functions.sql` 에서 읽고 (`STG_FUNCTIONS_SQL` 로 덮는다), 스냅샷 덤프에 `--routines` 를 더했다. (2) Redis 를 일회용 `redis:7` 컨테이너로 띄우고 `REDIS_URL` 을 거기로 준다 (죽은 포트 9 는 Redis 명령을 약 4초씩 실패시켰다). 서버 기동 전 `FLUSHALL`. 공유 `dev-redis` 는 건드리지 않았다.
- 서브모듈 정리: `apps/api/src/modules/auth/permission/permission-admin.service.ts` 의 변경은 `throw new ForbiddenException(` 줄바꿈 1건뿐이었다 (3줄 추가 · 1줄 삭제, 의미 변화 없음, `git diff --no-ext-diff` 로 확인). 패치를 scratchpad 에 보관하고 `git checkout --` 로 되돌렸다. 계획 범위 밖이라 커밋에 싣지 않는다.
- 구현 뒤 게이트 실행: `gates-m5` 전체 (검사 #1~#16). 결과는 아래 「최종 결과」에 적는다.

## Codex Implementation Review

- 호출: `node <plugin>/scripts/lib/codex-invoke.js adversarial-review` (fail-closed wrapper). 대상은 구현 워킹트리 diff (`insuniverse-server`, 기준 `76e25be7`). focus 는 2.5.2 의 구현 결정 7건.
- 라운드 수: 1. R2 는 없다. 유일한 지적이 DEFER_TO_BACKLOG 라 2.5.4 의 escalate 조건 (ACCEPT_NOW · HIGH 이상) 이 성립하지 않는다. 원장 `mccp-implement-codex__admin-rbac-m5`, cap 3.
- Codex 판정: `needs-attention` → `codex_verdict=divergent`. class=ok · blocking=false · 71초. 요약: 「Do not ship yet: two stalled database operations can disable permission monitoring indefinitely.」
- 합치 결론: 구현 결정 7건 중 감시 슬롯 복구 하나가 HIGH 로 남았다. 나머지 지적은 없다.
- YAGNI Triage:

  | Finding | Severity | Verdict | Why |
  |---|---|---|---|
  | F1 감시 슬롯이 300 ms 시한 뒤에도 돌아오지 않는다 (`access-control.interceptor.ts:153`). `Promise.race` 는 기다림만 끝내고 inflight 는 평가가 끝나야 줄어든다. 정지한 DB 작업 두 건이면 이후 판정이 전부 skipped | HIGH | DEFER_TO_BACKLOG | 계획 HIGH 「DB 작업을 취소하지 않고 슬롯을 쥔다」(backlog 2026-10-19 줄 · 계획 R3 판정) 와 같은 근원이다. 사용자 지시에 따라 HIGH 는 backlog 로 넘겨 다음 계획에 반영한다. 관찰 모드이고 강제는 꺼져 있어 요청 처리나 권한 판정은 막히지 않는다. 영향은 감시 집계에서 판정이 빠지는 것 (SIGUSR2 의 skipped 로만 보인다) |

- Deferred to backlog: 1건 → `.claude/plans/codex-findings-backlog.md` (아래 「백로그 기록」).
- Open Questions: 없음. auto-CRITICAL 항목 없음.
- Codex 원문 JSON: 구현 저장소 밖이 아니라 `.git` 아래 `mccp/tmp/codex-stdout.json` 에 있다.

### Security Reviewer

> security-reviewer unavailable, skipped (auto-fallback): 이 실행 환경에는 Task 도구가 없어 서브에이전트를 부를 수 없다. 인증 가드 · 권한 판정 변경이므로 PR 전에 보안 리뷰가 따로 필요하다. receipt 의 `security_skipped` 가 `/mccp:pr` 를 막는다.

`SECURITY_SKIPPED_REASON` 을 export 해 2.5.6 receipt write 에 전달한다.

### Design Review

> impeccable silent-skip (reason=no-signal). skill_available=1 · design_signal=0 · 디자인 표면 없음 (서버 전용 변경). 판정 기록은 2.5.6 에 `--impeccable-silent-skip` 으로 전달한다.

- 백로그 기록: 위 F1 을 `.claude/plans/codex-findings-backlog.md` 끝에 한 줄 덧붙였다 (HIGH, 계획 경로). 기존 줄은 고치지 않았다.

## 최종 결과

상태: 구현 완료, 커밋 안 함. 게이트 `admin-rbac-m5.gates.sh` 는 전체 녹색이 아니다. 검사 #9 와 #14 가 붉고, 둘 다 구조적 편차다 (아래 1)·2)). 나머지 검사는 녹색이다.

### 검사별 결과 (run 3 과 개별 실행, 2026-10-19)

| 검사 | 결과 | 근거 |
|---|---|---|
| #1 · #2 · #3 · #4 · #5 · #6 · #7 | 녹색 | run 2 에서 #8 까지 통과 (set -e 가 #9 에서 멈춤). #5 는 신규 spec 과 회귀 spec 녹색, tsc 오류 23 줄 (기준 23 줄, 신규 0) |
| #8 | 녹색 | 행 6336 · 6336 (기준 704 × 신원 9), `status=0` 0 줄, `diff=0`, 헤더 변형 2 줄 (두 값 모두 `same=yes`), `/live` · `/health` 두 빌드 200 |
| #9 | 붉음 (1)) | 반사실 두 변이 0 · unknown 0 · skipped 0 · 미배정 79. 기본 트리의 관리자 일반 would-deny 는 12 (rbac 스템), 기대는 39 (rbac 12 + 최고관리자 전용 27) |
| #10 | 녹색 | jest --json 8 건 통과 (기대 8, 실패 0, 보류 0) |
| #11 | 녹색 | 재실행 거부 (rc 1), 전원 회수 뒤 권한 행 0, 표지 뺀 사본은 복원 |
| #12 | 녹색 | loaded_check=ok, rbac_loaded=0, super_loaded=0 |
| #13 | 녹색 | status_match=yes, 표 부재 라벨 table-absent, absent_as_deny=no |
| #14 | 붉음 (2)) | append-only 는 통과. 새 줄 중 RESOLVED-IN 형식이 아닌 것이 기록 기준으로 12 건 (R3 11 건 + 이번 구현 HIGH 1 건) |
| #15 | 녹색 | 호스트 절대경로 0 · 동그라미 숫자 0 · `.claude` 줄 번호 인용 0 · 토큰 형태 0 |
| #16 | 녹색 | `lint-plan.mjs` rc 0 |

1) 검사 #9: 기대 집합과 관측 지점이 어긋난다. 감시는 판정을 `access-control.interceptor.ts` 의 `intercept` 안에서 기록한다 (`:98`, `:163`). 최고관리자 전용 27 행은 기존 `SuperAdminGuard` 가 가드 단계에서 403 으로 막아 인터셉터에 닿지 않는다. 실측: 기준 빌드에서 `GET /admin/alert/channels` 의 관리자 일반 신원은 403 이다 (rows-before). 평가기 단위로는 이 27 행이 `super-only` deny 가 되도록 짜였다 (`evaluate.ts` (6)). 그러나 관측 집계에는 나타나지 않는다. 해소 방법은 두 가지다. 관측을 가드 단계로 옮기거나, 기대 정의를 「가드 뒤에 닿는 행」으로 바꾸는 것이다. 둘 다 계획 판단이라 이 세션은 고르지 않았다. 최종 리뷰 몫이다. 미검증: 27 행 판정이 실트래픽에서 deny 가 되는지는 평가기 단위 시험으로만 확인했다.

2) 검사 #14: 사이드카는 「이번 마일스톤이 덧붙이는 backlog 줄은 RESOLVED-IN 이다」 를 전제한다. 그러나 R3 판정 때 사용자 지시 「HIGH 는 backlog 에 기록」 에 따라 11 건의 HIGH·MEDIUM·LOW 줄이 이미 붙었고, 이번 구현 HIGH 1 건 (F1) 도 붙였다. 지시와 사이드카 계약이 충돌한다. 사이드카는 고치지 않았다.

### 하네스 수정 (메타 디렉터리 안, `_lib.sh` · `isolated-seed.sh`)

- 저장 함수: 스키마 덤프와 같은 디렉터리의 `01-functions.sql` 을 적재하고 (`STG_FUNCTIONS_SQL` 로 덮는다), 스냅샷에 `--routines` 를 더했다. 없으면 엑셀 다운로드 오류 경로 (`GET /admin/order-detail/download`, admin-super) 가 응답 뒤에 헤더를 다시 써 서버 프로세스가 죽었다 (`ERR_HTTP_HEADERS_SENT`). 기준 트리도 같은 지점에서 죽었다. 이 죽음이 probe 의 `status=0` 1205 건을 만들었다 (첫 실행).
- Redis: 일회용 `redis:7` 컨테이너를 쓰고 `REDIS_URL` 을 거기로 준다. 죽은 포트 9 는 Redis 명령을 약 4 초씩 실패시켰다. 서버 기동 전 `FLUSHALL` 로 두 빌드의 상태를 맞췄다. 공유 `dev-redis` 는 건드리지 않았다.
- 반사실 사본 로더: 생성된 사본이 `.git` 아래에 있어 `mysql2/promise` 를 못 찾았다. `NODE_PATH` 를 서버 `node_modules` 로 주었다 (검사 #11 의 `mutant_rerun_after_wipe` 가 `not-restored` 에서 `restored` 가 됐다).

### 구현 중 수정

- 서브모듈 `permission-admin.service.ts` 의 변경은 `throw new ForbiddenException(` 줄바꿈 한 건뿐이었다 (의미 변화 없음). 패치를 scratchpad 에 보관하고 `git checkout --` 로 되돌렸다. 계획 범위 밖이라 커밋에 싣지 않는다.
- 세 인증 가드가 `getAllAndOverride` 로 바뀌면서 기존 시험의 모의 객체가 깨졌다 (`context.getClass is not a function`, `reflector.getAllAndOverride is not a function`). 네 spec (`principal.spec.ts`, `dev-auth-bypass.spec.ts`, `member-token-kind.spec.ts`, `admin-token-kind.spec.ts`) 의 모의에 `getClass` · `getAllAndOverride` 를 더했다. 실제 Nest 실행 컨텍스트에는 두 메서드가 있으므로 운영 동작은 바뀌지 않는다.

### 검증 (게이트 밖)

- 서버 jest 전체 (`--json`, Redis 는 127.0.0.1:6380 일회용): 213 suites 중 실패 6 (기준 6, 같은 suite), 2470 시험 중 실패 3 (기준 3, 같은 제목). 새 실패 0.
- 기존 `*-access.spec` 11 suites · 111 시험 녹색. 계획 HIGH 「*-access.spec 이 어떤 검사에도 안 돈다」 의 실측 대응이다 (검사로는 아직 없다).
- ESLint (`ESLINT_USE_FLAT_CONFIG=false`, 변경 비-spec 19 파일): 오류 0.
- 빌드: 기준 트리와 M5 트리 모두 `nest build` 로그가 비어 있다 (성공). 검사 #8 이 두 빌드를 기동했다.

### 구현 리뷰 · 영수증 (2.5.x)

- 2.5.0 seal: 정책 봉인 · 라운드 cap 3 (`mccp-implement-codex__admin-rbac-m5`).
- 2.5.3 Codex: `needs-attention` → `divergent`. class=ok · blocking=false · 71 초. 지적 1 건 (HIGH): 감시 슬롯이 300 ms 시한 뒤에도 돌아오지 않는다 (`access-control.interceptor.ts:153`). 2.5.4 판정은 DEFER_TO_BACKLOG 다 (사용자 지시: HIGH 는 backlog). 라운드 1 만 썼고 R2 는 없다.
- 백로그: `.claude/plans/codex-findings-backlog.md` 끝에 HIGH 1 줄 추가.
- 2.5.5 보안 리뷰: `security-reviewer` 호출 불가 (이 실행 환경에 Task 도구가 없다). `SECURITY_SKIPPED_REASON` 을 export 해 receipt 에 `security_skipped` 로 실었다. 이 상태는 `/mccp:pr` 을 막는다. PR 전 보안 리뷰가 따로 필요하다.
- 2.5.5b impeccable: skill_available=1 · design_signal=0 · silent-skip (`no-signal`, 서버 전용 변경).
- 2.5.6 receipt write: 성공 (`.claude/receipts/mccp-implement-codex/admin-rbac-m5.json`). 우회 env 없이 기록되었다. `codex_verdict=divergent`, `security_skipped=true`, `impeccable_silent_skip=no-signal`.
- 2.5.7 validate: exit 2. 사유는 `mccp-plan-codex` 영수증 부재 (skip-receipt 지시). `MCCP_SKIP_RECEIPT=1` 로 `preflight` 는 `bypassed: true` 로 ok 가 되지만 `validate` 는 같은 env 에서도 exit 2 다. 기록만 하고 넘어간다.
- 3.6 디자인 마무리: 생략 (트리거 불충족, `design_signal=0`). 3.7 디자인 근거 검증: 생략 (근거 미캡처).
- Phase 5 보고서 파일은 만들지 않았다. 이 절이 보고다.

### 커밋 · 원격

- 커밋 없음. 원격 push · PR · stg/prod 병합 · 운영 DB · stg DB 쓰기 없음. 격리 DB · Redis 는 모두 일회용이고 종료했다 (`m5-iso-` 접두 컨테이너만 썼다).

### 남은 것 (다음 단계, 사용자 · 최종 리뷰 몫)

1) 검사 #9 의 관측 지점 대 기대 정의 (위 1)). 2) 검사 #14 의 backlog 형식과 사용자 지시의 충돌 (위 2)). 3) F1 슬롯 복구 HIGH (계획 HIGH 와 같은 근원). 4) 보안 리뷰 (`security_skipped` 해소). 5) 커밋 · receipt restamp 는 리뷰 뒤 (메모 `implement-receipt-commit-after-restamp`).

## 코드 리뷰 (code-review R1, 2026-10-19)

지적 원천은 code-review · security · typescript 세 렌즈다. 같은 결함은 한 줄로 합쳤다. 판정 근거는 증거 열에 적었다. 층위는 계획서 저작 체크리스트(WT CLAUDE.md §2)의 층 번호다.

| 라운드 | 출처 | 심각도 | 지적 | 판정 | 증거 | 층위 |
|---|---|---|---|---|---|---|
| R1 | code-review | HIGH | 감시 슬롯이 정지한 DB 작업에 묶여 판정이 전부 skipped 로 빠진다 | 흡수 (부분) · 잔여 backlog | 판정 질의에 `SET SESSION MAX_EXECUTION_TIME = 300` 을 감시 연결마다 건다 (`armWatchConnection`). 격리 MySQL 8.0.42 실측: 상한 없는 질의는 끝나고, 상한을 건 5천만 행 재귀 질의는 ER 3024 로 0.34초에 끊긴다. 시험 「감시 연결마다 질의 시간 상한을 건다」가 설정 연결을 본다 (수정 전 `armWatchConnection` 없음으로 붉음). 잔여: 응답 없는 소켓 정지는 keepalive 에만 의존 → backlog HIGH 줄 | 1 |
| R1 | code-review (추가 발견) | HIGH 근원 | `evaluate` 의존성 생성이 `inflight` 증가 뒤라 던지면 슬롯이 샌다 | 흡수 | `watch()` 에서 `deps()` 를 먼저 만든 뒤 `inflight` 를 올린다 | 1 |
| R1 | code-review | MEDIUM | 검사 #9 기대 39행은 계획 사실 오류다. 최고관리자 전용 27행은 SuperAdminGuard 가 가드 단계에서 403 이라 인터셉터에 닿지 않는다 | 흡수 불가 · backlog | `alert-admin.controller.ts:78` 등 5개 컨트롤러가 `SuperAdminGuard` 를 단다 (27행 확인). 기대를 12 로 맞추는 것은 계획 정정 판정 전까지 하지 않았다. `identities.json` 의 「39행 deny」 문구는 실측 사실로 고쳤다. 검사 #9 는 붉다 | 1 |
| R1 | code-review | MEDIUM | 검사 #12 기대 파일이 생성기 출력의 복사본이다 | 흡수 불가 · backlog | 독립 계산에 코드명 규칙 재구현이 필요하다. 이번 라운드 미흡수 | 6 |
| R1 | code-review | MEDIUM | 관리자 계열 다른 종류 · 형상 불일치 · 판정 준비 실패가 어떤 칸에도 안 남는다 | 흡수 | 가드가 `_aaT` 한 개만 해석한다 (`resolveAdminCookieIdentity`). 관리자 계열 종류와 invalid 는 `identity` 칸에 계수, 판정 준비 실패는 `guard-error` 칸. 시험: 영업 쿠키 · 위조 쿠키 · 예외 주입 (수정 전 4건 붉음) | 2 |
| R1 | code-review | MEDIUM | 클래스 표지 isPublic 이 인증 가드를 끌 수 있다 | 기각 → backlog (사용자 판단) | `getAllAndOverride` 는 계획 Task 6 · 검사 #7 이 요구한 조합이다. 위험 수용은 사용자 몫. 런타임 시험을 더하면 위험한 동작을 고정하게 되어 더하지 않았다 | 1 |
| R1 | code-review | MEDIUM | 감시 가드 단위 시험이 없다. 혼합 시험은 감시 해석기를 안 올린다 | 흡수 (가드 단위) · 체인 편입은 이연 | `access-control.guard.spec.ts` 8건 (HEAD · 종류 칸 · 위조 · 예외 · 비정상 요청). 체인 편입은 backlog 에 넣지 않았다 (범위 밖, 시험 대상이 가드 단위로 닫힘) | 2 |
| R1 | code-review | LOW | 혼합 시험 identityOf 가 열람 토큰 부재를 false 로 접는다 | 기각 (범위 밖) | 감시는 열람 신원을 판정하지 않는다 (M5 직접 영향 없음). 보안 리뷰어 몫으로 넘긴다. 엔드투엔드 악용 경로는 미검증 | 1 |
| R1 | code-review · security | LOW·MEDIUM | HEAD 요청이 GET 행을 못 찾아 unassigned 로 샌다 | 흡수 | `codeForRoute` 가 HEAD 를 GET 으로 정규화. 수정 전 HEAD 시험 붉음, 수정 뒤 초록. Express 실측 근거는 보안 리뷰 (`HEAD /admin/x/:id` → 200) | 1 |
| R1 | code-review | LOW | 인터셉터에 방어 try 와 record 체인 catch 가 없다 | 흡수 | `intercept` 본문을 try 로 감싸고 `record` 체인에 catch 를 달았다. 시험 2건 (수정 전 붉음: 던지는 접근자 · 기록 던짐 → unhandledRejection) | 2 |
| R1 | code-review | LOW | 300 ms 시한이 순차 질의 3건 전체에 걸린다 | 기각 | 거부가 unknown(watch-timeout) 으로 접혀 카운터에 남는다 (완전 맹점 아님). 시한 값은 계획 값이다. 운영 지연은 미검증 | 5 |
| R1 | code-review · security | LOW | SIGUSR2 리스너를 프로덕션에 상시 등록한다 | 기각 | 리스너 등록은 SIGUSR2 의 기본 동작(종료)을 막는다. 조건부 등록은 운영에서 SIGUSR2 를 보내는 쪽(예: 재시작 도구)의 동작을 바꾼다. 사용처 확인은 미검증이라 그대로 두고 편차에 적었다 | 1 |
| R1 | code-review | LOW | 감시 풀 종료 훅 없음 · 편차 미기록 항목 | 부분 기각 · 편차 기록 | 풀 종료 훅은 프로세스 종료가 소켓을 닫으므로 기각. 편차 (a)~(d) 는 아래 편차 7~10 으로 적었다. (a) 외부 발송 env 차단은 실측하지 않았다 (미검증) | 2 |
| R1 | code-review | LOW | 풀 포화 시험이 포화를 재현하지 않는다 | 기각 → backlog (이름) | 시험 이름은 게이트 #5 가 고정한다. 이름 변경은 게이트 수정을 동반하므로 계획 판정 몫. backlog MEDIUM 줄 | 2 |
| R1 | code-review | LOW | 쓰기 계약 변화 (일반 코드 소멸) 를 픽스처 교체로 가렸다 | 편차 기록 · M10 전 해소 | 편차 11 에 적었다. 쓰기 컨트롤러는 어떤 모듈에도 없어 실해 없음 | 6 |
| R1 | code-review | LOW | 구현 노트가 permission-admin.service.ts 변경을 자기모순으로 적었다 | 흡수 | 남의 줄바꿈 변경을 패치로 되돌렸다 (`git apply`, 작업 트리가 구현 시작 시점과 같아졌다). 「구현 중 실행 기록」 서술은 이 절이 정정한다 | 2 |
| R1 | code-review | LOW | 검사 #14 의 backlog 형식이 HIGH 기록 지시와 충돌한다 | 기각 (계획 판정 몫) | 게이트를 지시 형식에 맞춰 느슨하게 바꾸지 않았다 (Goodhart). 검사 #14 는 붉다 (새 줄 20건 모두 RESOLVED-IN 형식이 아니다) | 1 |
| R1 | security | MEDIUM | DEV 우회 시험이 우회가 켜져도 통과한다 (Referer · XFF 없는 위조 요청) | 흡수 | 위조 요청에 `Referer: …/swagger/…` 와 `X-Forwarded-For: 211.48.209.5` 를 달았다. 우회 스위치를 켠 사본에서 그 시험이 붉어짐을 실측 (9건 중 1 red). beforeAll 의 delete 는 사전조건 주석으로 바꿨다 | 5 |
| R1 | security | LOW | scope-monitor 의 ORG 누락 검사가 declared 뒤에 있고 쌍 키를 강제하지 않는다 | 흡수 | ORG 는 declared 와 무관하게 orgPath 누락을 먼저 본다. 쌍 키 (agcId · orgPath) 가 없으면 던진다. 시험 2건 (수정 전 붉음 2건) | 1 |
| R1 | security | LOW | 전역 가드가 모든 요청에서 회원 토큰까지 검증한다 (CPU 증폭) | 흡수 | 가드는 관리자 쿠키만 검증한다. 쿠키가 없으면 검증 없이 none. 편차 7 에 대가를 적었다 | 1 |
| R1 | security | LOW | 전역 가드 · 경로 맵 행동 시험 부재 · 440 키 전수 CI 시험 없음 | 흡수 (가드 시험) · 전수 CI 시험은 이연 | 가드 시험 흡수. 440 키 전수 대조 spec 은 이번에 넣지 않았다 (LOW 이연, 검사 #3 의 행 수 대조가 일부를 본다) | 7 |
| R1 | security | MEDIUM | gate #9 unassigned_admin 에 독립 기대 상한이 없다 | 흡수 불가 · backlog | 미배정 79 과 704 − 440 의 차이가 미실측이다. 이번 라운드 미흡수 | 6 |
| R1 | typescript | MEDIUM | 키별 카운터가 쓰기 전용이다 | 흡수 | 덤프가 `ac_key <키> <건수>` 줄을 낸다 (키는 경로 패턴 · 코드 · 판정 · 사유만). 거부 집합과 합계는 그대로다 | 2 |
| R1 | typescript | MEDIUM | 슬롯 상한 2 에 대기열이 없어 버스트에서 조용히 빠진다 | 흡수 (부분) · 대기열은 backlog | 건너뜀을 분당 한 번 경고로 남겼다 (`logSkipped`). 대기열은 계획 Task 8 · 편차 6 의 설계라 넣지 않았다 → backlog MEDIUM | 1 |
| R1 | typescript | MEDIUM | 혼합 시험이 외부 Basic 가드를 올리지 않는다 | 흡수 | `zz-mixed/client` 경로에 `ClientBasicAuthGuard` 를 올리고 스텁 DrizzleService 를 준다. 시험: Basic 요청 신원은 회원 형상이 아니다 (관리자 쿠키가 같이 와도). 9건 녹색 | 6 |
| R1 | typescript | MEDIUM | 「풀 포화」 시험 이름이 재는 것보다 과장이다 | 기각 → backlog (이름) | 위 R1 code-review LOW 와 같은 항목 | 2 |
| R1 | typescript | MEDIUM | access-identity 가 모든 요청에서 회원 후보를 검증한다 | 흡수 | 위 security LOW (CPU 증폭) 와 같은 항목. 가드가 `resolveAdminCookieIdentity` 를 쓴다. `resolveAccessIdentity` 는 access-identity.spec 와 함께 그대로 둔다 | 1 |
| R1 | typescript | LOW | record 가 unmapped 를 allow 로 센다 | 흡수 | `allow` 만 허용으로 세고 나머지 판정(unknown · unmapped)은 unknown 칸에 사유와 함께 센다. 시험 1건 (수정 전 붉음: allow 1) | 1 |
| R1 | typescript | LOW | 감시 연결 풀이 연결 · 판정 상한 · 집계를 외부에서 볼 방법이 없다 | 흡수 (ac_key) · 나머지 기각 | 키 덤프는 흡수. ACCESS_CONTROL_MODE 가 코드에서 안 읽히는 것은 기각 (M9 가 읽는 상수이고 검사 #6 이 소비한다) | 2 |
| R1 | — | (판정 불가) | 리뷰 목록의 마지막 항목이 원문에서 잘려 있다 | 판정 불가 · 미처리 | 원문이 `apps/ap` 에서 끊겨 내용을 알 수 없다. 리뷰 기록을 다시 받아야 한다 | — |
| R2 | code-review · security | MEDIUM | `judgeRowScope` 가 범위 키 목록이 비면 allow 를 내고, 일부 키만 넘기면 고른 키만 비교해 allow 한다 (두 지적은 같은 결함이라 한 행) | 흡수 | `scope-monitor.ts` 의 `REQUIRED_KEYS` 로 종류별 필수 키를 두고, 키가 비었거나 필수 키가 빠지면 던진다 (`listScopeCondition` 과 같은 규칙). 시험 「범위 키 목록이 비면 단건 판정도 던진다」 「종류의 필수 키가 빠진 호출은 던진다」 수정 전 2건 붉음 (allow 반환), 수정 뒤 scope-monitor.spec 11건 녹색 | 1 |
| R2 | code-review | LOW | 표지가 없는 기존 적재 DB 는 재실행 거부가 성립하지 않는다 | 기각 · 한계 문서화 | `__version__` 행으로는 가릴 수 없다. `drizzle/0036_auth_permission.sql:26-28` 의 DDL 이 그 행을 넣으므로 M4 적재와 DDL 만 적용한 DB 를 가르지 못한다. 표지 `__seeded__` 는 M5 적재만 남긴다. 한계를 `permission-first-version.ts` 적재 표지 주석에 적었다. 운영 적재 없음 (M4 노트 176). 근본 해소는 backlog 1538 (계획 개정 몫) | 2 |
| R2 | security | LOW | ORG 범위를 orgPath 동치로 판정해 현행 부분트리 허용과 어긋난다 | 기각 (M6 몫) | 계획 Task 9 (b) 「원천 비교 하나 · 3의미 분리는 M6」. 현행 부분트리는 `organization.repository.ts:48-54` · `:113-122` (LIKE). 감시 인터셉터는 이 함수를 부르지 않는다 (검사 #6), 응답 영향 0. M6 이 이 함수를 측정 입력으로 쓰기 전에 prefix 비교로 바꿔야 한다 (M6 계획의 전제로 남긴다) | 1 |
| R2 | security | LOW | 격리 서버가 모든 인터페이스에 바인딩된다 (preload 가 포트만 바꾼다) | 흡수 | `_lib.sh` `gk_preload` 가 `listen` 인자에 호스트 `127.0.0.1` 을 끼운다. `gk_bind_check` (ss) 를 `gk_wait_up` 에 연결. 수정 전 같은 preload 로 띄운 스텁이 `*:17201` (실측), 수정 뒤 `127.0.0.1:17202` 와 단언 통과. 704행 probe 는 다시 돌리지 않았다 (미검증) | 1 |
| R2 | typescript | MEDIUM | `armWatchConnection` 이 연결 타입을 bare `Function` 으로 받아 lint 오류 (no-unsafe-function-type) | 흡수 | `access-control.module.ts` 의 `WatchConnection` 좁은 구조형 (`query(sql, cb)` · `destroy()`). 수정 전 22:47 오류 (리뷰 실측), 수정 뒤 `ESLINT_USE_FLAT_CONFIG=false npx eslint` 대상 파일 오류 0. 이 함수가 mysql2 시그니처 변화를 타입으로 잡는다 | 2 |
| R2 | typescript | LOW | 감시 연결에서 SET 이 실패하면 오류 코드 없이 연결을 버린다 | 흡수 | `armWatchConnection` 이 `err.code` 를 풀마다 분당 한 번 `Logger.warn` 으로 남기고 destroy 한다. 시험 「SET 이 실패한 감시 연결은 버리고 경고를 남긴다」 수정 전 붉음 (warn 없음), 수정 뒤 녹색 | 2 |
| R2 | typescript | LOW | 새 spec 의 lint 오류 (`require()` · bare `Function`) | 흡수 | `permission-adapter.spec.ts` 의 `require` 를 `jest.requireActual<typeof import(...)>` 로 바꿨다 (jest.mock 팩토리 안). `public-marker.spec.ts` 의 `ctx` 인자 `Function` 을 `unknown` 으로. 두 파일 eslint 오류 0, 두 spec 녹색 (5건, 시험 이름 불변: gate #5 이름 grep 통과) | 2 |
| R2 | typescript | LOW | `AccessControlNote.label` 이 `string` 이라 오타 칸이 컴파일되고 새 계수 키를 만든다 | 흡수 | `NoteLabel` 닫힌 유니온 (`IdentityLabel` 에서 none · admin 을 뺀 값 + admin-no-id · guard-error). 시험 「계수 칸 이름은 닫힌 집합이다」의 `@ts-expect-error` 는 수정 전 TS2578 (미사용 지시자) 로 붉고, 수정 뒤 tsc 에서 사라진다 | 1 |
| R2 | typescript | LOW | `AccessControlDb = MySql2Database<any>` 가 스키마 제네릭을 지운다 | 흡수 | `access-control.constants.ts` 가 `MySql2Database<typeof schema>` (type-only import). tsc 오류 줄 23 = 기준선 23, 기준선 대조 신규 0. 어댑터의 tnAdmin · tnAuthPermission 열 타입이 검사된다. `super-admin.query.ts` 의 `SqlRunner` `any` 는 건드리지 않았다 (범위 밖) | 1 |

### 편차 (R1 흡수 추가)

7. 가드는 관리자 쿠키 `_aaT` 만 해석한다 (`resolveAdminCookieIdentity`). 계획 Task 7 의 비관리자 칸 가운데 회원 · 외부 Basic 칸은 도입하지 않았다. 열람 토큰은 헤더 존재만 `view-token` 칸으로 계수하고 검증하지 않는다 (최종 리뷰 R1 F6). 관리자 계열 다른 종류와 형상 불일치 · 판정 준비 실패 칸은 `identity` 칸으로 남긴다. 대가: 쿠키 없는 요청에서 회원 토큰 검증이 빠진다 (성능 이득). backlog MEDIUM 줄 (편차).
8. 감시 연결마다 `SET SESSION MAX_EXECUTION_TIME = 300` 을 건다 (`armWatchConnection`). 판정 질의는 SELECT 라 서버가 끊는다. 설정 실패 연결은 버린다. 응답 없는 소켓 정지는 이 장치 밖이다.
9. `judgeRowScope` 는 ORG 신원을 자동 칸 여부보다 먼저 본다. ORG 호출에 agcId · orgPath 쌍 키가 없으면 던진다. 기존 시험 「ORG 신원에 orgPath 가 없으면 deny 다」의 키 인자에 agcId 를 더했다 (의도 불변).
10. `permission-admin.service.ts` 의 남의 줄바꿈 변경을 패치로 되돌려 놓았다. 구현 중 실행 기록의 「git checkout 으로 되돌렸다」는 R1 에서 정정한다. 커밋에는 싣지 않는다 (정책 §5-7).
11. 쓰기 계약 변화: M5 카탈로그 개정으로 alert-admin.read 등 일반 코드가 `-super` 로 바뀌었다. 픽스처를 banner-admin.read 로 바꾼 것은 이 변화를 가린다. 쓰기 컨트롤러는 모듈에 없어 실해는 없다. M10 전 해소 대상이다.
12. 감시 SIGUSR2 리스너는 유지한다. 조건부 등록은 운영 신호 동작을 바꾸므로 기각했다 (운영에서 SIGUSR2 를 보내는 쪽은 미확인, 미검증).
13. 검사 #9 는 계획 사실 오류 때문에 붉다 (R1 판정). 구현을 기대에 맞추지 않았다. 마일스톤은 완료로 적지 않는다.

### 흡수 뒤 검증 (R1, 게이트 밖 포함)

- 흡수 전 붉음 확인: 가드 시험 4건 (HEAD · 종류 칸 · 위조 · 예외 칸), 인터셉터 · 범위 시험 7건, 혼합 DEV 시험 (우회 켠 사본에서 1건 붉음).
- 흡수 뒤: access-control 디렉터리 36건 녹색. 혼합 신원 9건 녹색 (`mixed-identity.sh`, `it(` 9 과 일치). auth + admin/super-admin 28 suite · 350건 녹색. tsc 23 줄 = 기준선 23 줄, 신규 0.
- 게이트 (`gates-m5`): #1~#7 통과 (#7 까지 도달). #8 은 STG_SCHEMA_DUMP 가 없어 실행하지 않았다 (미검증, 격리 스키마 덤프가 이 세션에 없다). #9 · #11 · #12 · #13 도 같은 이유로 미실행 (미검증). #14 는 붉다 (새 줄 20건이 RESOLVED-IN 형식이 아니다). #15 · #16 은 통과 (수동 실행).
- 응답 불변 (검사 #8) 은 이 라운드 변경으로 바뀔 이유가 가드 본문에서 던짐이 없어지고 항상 true 인 점에서 이론상 그대로다. 실측은 미검증이다.
- 커밋하지 않았다 (UI3).

### 흡수 뒤 검증 (R2)

- 흡수 전 붉음 확인: 범위 시험 2건 (allow 반환), SET 실패 경고 시험 1건 (warn 없음), `@ts-expect-error` 1건 (TS2578). 수정 뒤 전부 녹색.
- 수정 뒤: scope-monitor · access-control · permission-adapter · public-marker · evaluate · access-contract · super-admin.service 9 suite 99건 녹색 (jest). 혼합 신원 `mixed-identity.sh` 9건 녹색 (`it(` 9 과 일치).
- tsc 오류 줄 23 = 기준선 23, 기준선 대조 신규 0. 리뷰가 든 ESLint 오류 (access-control 모듈 · 신규 spec 2건) 는 대상 파일 전부 0.
- 게이트: `admin-rbac-m5.gates.sh` 의 #1 · #2 · #3 · #4 · #5 · #6 · #7 rc 0 (실행은 #7 까지 잘라 돌림), #15 · #16 rc 0. #8 · #9 · #10 · #11 · #12 · #13 은 격리 스택 · STG 덤프가 필요해 이번 라운드에 돌리지 않았다 (미검증, 의미 변화 없음). #14 는 이전 라운드와 같이 붉다 (backlog 를 바꾸지 않았다).
- 격리 바인딩: 704행 probe 를 다시 돌리지 않았다. 바인딩 수정은 ss 단언으로 확인했다 (미검증: 704행 probe 재실행 시 단언이 붙는다).
- 커밋하지 않았다 (UI3).

## 최종 리뷰 (fable · codex, 라운드 1/3)

사용자 지시 확인: `verify-user-directive.sh` exit 0. 「Round 는 최대 3 · 마지막 라운드는 CRITICAL 이상만 receipt · HIGH 는 backlog」 원문이 세션 기록에 있다. 이번 라운드는 1/3 이라 상한이 아니다. CRITICAL 0건이라 critical_open 0.

| 라운드 | 리뷰어 | 심각도 | 지적 | 판정 | 증거 | 층위 |
|---|---|---|---|---|---|---|
| F1 | fable | HIGH | 응답 불변 게이트 (검사 #8) 의 마지막 실행이 M5 구현 커밋 cd6e4144 보다 앞선다. 최종 커밋 기준 704행 probe 가 없다 | 미흡수 · backlog | `isolated-probe.sh:4` 가 `STG_SCHEMA_DUMP` 를 요구한다. 이 세션에 설정도 덤프도 없다 (rc 1). 완료 표기는 하지 않는다. 최종 커밋 뒤 재실행이 필요하다 | 2 |
| F2 | fable | HIGH | 자기 판정 엔드포인트 `GET /admin/super-admin/permission` 이 rbac.manage 로 분류되어 「의도된 차단 12행」 에 들어 있다 | 미흡수 · backlog | `high-risk.json:6` · `route-permission-map.ts:236` · `apps/api/src/modules/admin/super-admin-permission.controller.ts:40` (판정 가드를 일부러 달지 않는다). 분류를 바꾸려면 M4 골든 · M1 표 · 검사 #3 · #9 를 함께 바꾸는 계획 변경이라 이번 라운드 범위 밖이다. M9 판단 표에 「자기 판정 행은 의도된 차단이 아니다」 를 넣도록 backlog 로 넘긴다 | 1 |
| F3 | fable | MEDIUM | 검사 #9 기대 39행은 계획 사실 오류다. 최고관리자 전용 27행은 현행 SuperAdminGuard 뒤에 있어 would-deny 가 아니다. 기대는 rbac 행 12 여야 한다 | 흡수 | `gates.sh` 검사 #9: `ROWSUPER` 항을 기대 계산에서 뺐다 (#4 의 ROWSUPER 정의는 그대로). 오프라인 대조: 기대 12행 = `gatekit-m5-cf.txt` 의 wd_set 12행 (cmp 일치), `base_would_deny_admin=12`, `unknown=0`, `skipped=0`. 계획 :73 본문 정정은 backlog | 1 |
| F4 | fable | MEDIUM | PRD M5 Outcome 「실제 목록 조건으로 would-deny 를 잰다」 를 M5 에서 하지 않았다. scope 비교가 orgPath 동치라 부분트리 허용과 어긋난다 | 미흡수 · backlog | `scope-monitor.ts:80` (`String(want) !== String(have)`). 현행 조직 범위는 LIKE 부분트리 (`organization.repository.ts:48-54`, 노트 R2). 측정은 M6 몫 (운용 정책 §2) 이라 M6 Task · 전제로 옮기는 계획 개정이 필요하다 | 2 |
| F5 | fable | LOW | 검사 #7 은 리터럴 `SetMetadata('isPublic'` 만 봐서 상수 키 데코레이터로 우회된다 | 흡수 | `gates.sh` 검사 #7: 가드 네 곳 (`admin/jwt`, `user/jwt`, `user/jwt-optional`, `admin/roles` `.guard.ts`) 밖에서 문자열 `isPublic` 0건. 상수 키 사본 (`const IS_PUBLIC = 'isPublic';`) 자가시험이 걸린다. 오프라인 실행 rc 0 | 8 |
| F6 | fable | LOW | 열람 토큰 헤더로 선 요청이 감시에 보이지 않는다 | 흡수 | `access-control.guard.ts` `hasViewTokenHeader` 분기 → 칸 `view-token` (검증 없음, 헤더 존재만). 시험 「열람 토큰 헤더만 있으면 검증 없이 view-token 칸으로 남긴다」. HEAD 가드에서 붉음 (실측), 수정 뒤 녹색 | 1 |
| F7 | fable | LOW | 검사 #14 는 backlog 형식 (`RESOLVED-IN`) 과 사용자 지시 (HIGH 를 backlog 에 기록) 가 충돌해 구조적으로 붉다 | 기각 (계획 판정 몫) | 게이트를 느슨하게 고치지 않았다 (Goodhart, R1 기각과 같은 판단). 검사 #14 는 붉은 채 둔다. `gates-m5 ok` 는 계획 게이트 정정 전에는 나올 수 없다 | 5 |
| F8 | fable | LOW | README 의 혼합 신원 시험 수 「`it(` 8건」 이 낡았다. 파일은 9건이다 | 흡수 | `.claude/meta/2026-10-19-admin-rbac-m5/README.md` 표 `it(` 9건. `grep -c "^\s*it("` 실측 9 | 2 |
| C1 | codex | HIGH | 정지한 질의가 슬롯을 영구히 쥐어 감시가 조용히 멈춘다 (두 슬롯이 멈추면 이후 요청이 전부 건너뜀) | 부분 흡수 · 잔여 backlog | `access-control.interceptor.ts` 슬롯 해제를 시간 상한 시점으로 옮겼다 (`release()` 한 번만). 시험 「정지한 DB 작업이 있어도 …」 의 끝 단언을 「시간 상한 뒤 평가 재개 · skipped 3」 으로 고쳤다. HEAD 인터셉터에서 그 시험이 붉다 (실측). 잔여: 응답 없는 소켓은 풀 상한 (`WATCH_POOL_LIMIT`, 대기 없음) 까지만 쥐고, 그 뒤 판정은 풀 획득 실패로 unknown 이 된다. 연결 파기 (질의 시간 상한의 클라이언트 측) 는 backlog | 1 |
| C2 | codex | HIGH | 필수 범위 감시가 M5 에 없다 (범위 헬퍼 프로덕션 호출자 0) | 미흡수 · backlog | `.claude/PRPs/plans/admin-rbac-m5.plan.md:214` (NOT Building) 과 검사 #6 (`scope-monitor` 호출 금지). F4 와 같은 뿌리다. 계획 개정 몫 | 2 |
| C3 | codex | MEDIUM | 응답 불변 게이트 (#8) 가 헤더 변형 줄의 `same` 값을 보지 않는다 | 흡수 | `gates.sh` 검사 #8: `header_variant` 줄 2건 모두 `same=yes` 를 요구. 자가시험 (`same=no` 한 줄이 든 사본은 걸림) 오프라인 통과. 실 probe 재실행은 미검증 | 3 |
| C4 | codex | MEDIUM | 혼합 신원 시험 (9건) 에 감시 가드 · 인터셉터가 없어 혼합 신원에서 관측 칸이 섞이는지 검증되지 않는다 | 미흡수 · backlog | `access-control.mixed.itspec.ts:59`. 시험 앱에 감시 체인을 올리려면 `PermissionAdapter` 스텁과 판정 경로가 필요하다. 이번 라운드 범위 밖. 가드 단위 시험 (F6 포함) 은 있다 | 6 |
| R2-1 | fable | MEDIUM | 시험 이름 「정지한 DB 작업이 있어도 동시 실행은 상한을 넘지 않는다」 가 뒷부분 (슬롯 해제) 과 어긋나고, 실제 DB 상한인 풀은 어떤 시험도 돌리지 않는다 | 흡수 | 시험을 둘로 나눴다. 기존 이름은 판정 슬롯 2 만 잰다 (`selectCalls` 2 · `skipped` 3). 슬롯 해제는 「시간 상한이 지나면 멈춘 판정의 슬롯을 돌려준다」로 옮겼고 풀 연결을 쥔 채라는 주석을 달았다. 풀 옵션을 `watchPoolOptions` 로 빼고 「감시 풀은 연결 2 · 대기 없음 · 큐 0 이다」 시험을 더했다. 반사실: `waitForConnections: true` 사본에서 그 시험이 붉다 (1건). 게이트 #5 이름 검사에 두 이름 추가. 계획 Acceptance 와 Task 8(f) 의 문장은 본문 동결이라 backlog | 7 |
| R2-2 | fable | MEDIUM | 검사 #9 · #13 의 반사실 스크립트가 dist 가 있으면 빌드를 건너뛰어 낡은 빌드 (구현 커밋 이전) 를 잰다 | 부분 흡수 | `counterfactual.sh` · `isolated-ddl-axis.sh` 를 무조건 빌드로 고쳤다 (`isolated-probe.sh` 와 같은 형태). 격리 재실행은 STG 덤프가 없어 못 했다 (미검증). backlog 줄 | 5 |
| R2-3 | fable | LOW | 검사 #5 SPECS 에 `access-control.guard.spec.ts` 가 없어 전역 가드 시험을 로컬 게이트가 돌리지 않는다 | 흡수 | SPECS 에 한 줄 추가. 게이트 #5 의 api.yml 대조와 jest 목록 대조가 통과한다 (오프라인 실행, 9 suite 103건 녹색) | 2 |
| R2-4 | fable | LOW | identities.json 과 편차 7 이 「열람 토큰은 계수하지 않는다」로 낡았다 (F6 흡수 뒤) | 흡수 | identities.json 의 열람 토큰 · 영업 · 조직관리자 칸을 고쳤다. 편차 7 을 「헤더 존재만 view-token 칸, 검증 없음」으로 고쳤다 | 2 |
| R2-5 | fable | LOW | 서버 워킹트리에 M5 파일 4개의 포매팅 변경이 커밋 뒤에 생겼다 (23:05, 정책 §5-7 과 불일치) | 흡수 (출처 기록 · 되돌림) | 변경은 80 열 재배치와 쉼표 추가다. 공백을 빼도 HEAD 와 같지 않다. 4 파일 변경을 `.claude/meta/2026-10-19-admin-rbac-m5/foreign-format-r2.patch` 에 보존하고 HEAD 로 되돌렸다. 누가 돌렸는지는 확인하지 못했다 (미검증). 커밋 메모 (R2) 정정 | 2 |
| R2-6 | codex | MEDIUM | 목록 조건 생성기가 종류의 필수 키를 보지 않는다. `{kind:'org-manager', agcId:7}` 에 `['agcId']` 를 주면 기업 경계 없이 `agc_id = ?` 가 나온다 | 흡수 | `listScopeCondition` 도 `REQUIRED_KEYS` 를 본다 (단건 판정과 `missingRequired` 를 공유). 시험 「목록 조건도 종류의 필수 키가 빠지면 던진다」 신설. HEAD 의 `scope-monitor.ts` 에서 그 시험이 붉다 (1건 실측) | 1 |
| R2-7 | codex | MEDIUM | 응답 불변 probe 의 영업 · 조직관리자 신원이 범위 키를 빠뜨렸다 (`sagId` · `agcId`). 실제 로그인 payload 와 다르다 | 부분 흡수 | `probe.mjs` 신원에 `sagId: 3` · `agcId: 1` 을 넣었다. 이 변경 전의 704행 결과 (diff=0 포함) 는 이 두 신원에 대해 무효다. 재실행은 STG 덤프가 없어 못 했다 (미검증). backlog 줄 | 6 |
| R2-8 | codex | MEDIUM | 격리 실행이 cwd 의 dotenv 를 읽어 `env -i` 가 지운 값 (`DEV_AUTH_BYPASS` · 외부 자격증명) 을 되살린다 | 부분 흡수 | `gk_no_dotenv` 를 `gk_serve` 앞에 두어 루트 `.env` 나 `apps/api/.env` 가 있으면 기동하지 않는다 (경로 값은 찍지 않는다). 서버 트리에는 두 파일이 없다. 헬퍼는 오프라인으로 본다 (빈 트리 rc 0 · 루트 `.env` rc 1 · `apps/api/.env` rc 1). 격리 기동 전체는 STG 덤프가 없어 미검증. backlog 줄 | 1 |

### 흡수 뒤 검증 (최종 리뷰 R1)

- 서버 access-control 디렉터리 시험 4 suite 42건 녹색 (`npx jest --config jest.config.js`, `access-control.spec.ts` · `access-control.guard.spec.ts` · `access-identity.spec.ts` · `scope-monitor.spec.ts`). 혼합 시험 파일은 기본 설정이 줍지 않는다 (재실행은 하지 않았다, 가드 본문을 올리지 않으므로 영향 없음 (미검증)).
- 반사실: HEAD 인터셉터를 넣으면 「정지한 DB 작업이 있어도 …」 1건 붉음. HEAD 가드를 넣으면 「열람 토큰 헤더만 있으면 …」 1건 붉음. 둘 다 복원 뒤 cmp 일치.
- tsc: 오류 줄 23 = 기준선 23. 기준선 대조 신규 0.
- ESLint (`ESLINT_USE_FLAT_CONFIG=false`): 변경한 서버 파일 5개 rc 0.
- 게이트 (`gates.sh` 오프라인 부분): 검사 #7 rc 0 (자가시험 포함). 검사 #8 헤더 줄 검사는 정상 사본에서 2 건이고 `same=no` 사본에서 걸린다. 검사 #9 기대 12행 = 기존 반사실 출력 12행 (cmp 일치). 검사 #1 · #2 · #3 · #4 · #5 · #6 은 이번 변경과 무관하다 (직전 실행 결과 유지, 미재실행).
- 미검증: 검사 #8 실 probe (STG_SCHEMA_DUMP 없음), 검사 #9 실 반사실 재실행 (counterfactual.sh), 검사 #10 혼합 재실행, 검사 #11 · #12 · #13 (격리 스택). 검사 #14 는 붉다 (F7).

### 흡수 뒤 검증 (최종 리뷰 R2)

- 서버 jest (`npx jest --config jest.config.js` · 9 spec: access-contract · evaluate · permission-adapter · access-identity · access-control · access-control.guard · scope-monitor · public-marker · super-admin.service): 9 suite 103건 녹색.
- 반사실 (실측): HEAD 의 `scope-monitor.ts` 를 넣으면 「목록 조건도 종류의 필수 키가 빠지면 던진다」 1건이 붉다. `waitForConnections: true` 사본에서 「감시 풀은 연결 2 · 대기 없음 · 큐 0 이다」 1건이 붉다. 두 사본 모두 원본을 cmp 로 되돌렸다.
- tsc (`tsconfig.json`): 오류 줄 23 = 기준선 23, 기준선 대조 신규 0.
- ESLint (`ESLINT_USE_FLAT_CONFIG=false`): 변경한 서버 파일 4개 rc 0.
- 게이트 (`admin-rbac-m5.gates.sh` 를 두 사본으로 나눠 돌림, 계획서 Validation 블록이 같은 스크립트를 부른다): (a) #1–#7 · #15 · #16 rc 0 (`gates-m5 ok`). #14 블록은 뺐다. 붉은 채다 (backlog 형식, 최종 리뷰 R1 F7 기각 유지). (b) #10 rc 0 (혼합 신원 9건 = `it(` 9). #8 · #9 · #11 · #12 · #13 은 `STG_SCHEMA_DUMP` 가 없어 실행하지 않았다 (미검증).
- 격리 재실행 (704행 probe · 반사실 · 두 축 DDL · 적재): 이 세션에 스키마 덤프가 없어 실행하지 못했다 (미검증). 픽스처 · dist 무조건 빌드 · dotenv 차단은 코드로만 고쳤다. backlog 줄 3개.
- 시험 이름 (R2-1): 게이트 #5 의 이름 검사 3개 (기존 1 · 신규 2) 를 통과한다.

### 편차 (R2 흡수 추가)

14. `access-control.module.ts` 의 풀 옵션을 `watchPoolOptions(configService)` 로 뺐다. 값은 그대로다. 풀 설정을 시험이 읽게 하려는 것이고, 게이트 #6 의 `pool_ok` 문자열 검사는 그대로 맞는다.
15. 영업 · 조직관리자 probe 신원에 범위 키를 넣었다 (`sagId` · `agcId`). 이 변경 뒤 704행 재실행 전까지 응답 불변 주장은 이 두 신원에서 미검증이다.

### 커밋 메모

- 서버 워킹트리에 이 라운드가 만들지 않은 변경이 있다. `apps/api/src/modules/auth/permission/permission-admin.service.ts` 의 `requireSuper` 예외 메시지 줄바꿈 (의미 변화 없음, 「`No syntactic changes`」). 정책 §5-7 에 따라 커밋에 싣지 않았다. 이 파일은 워킹트리에 그대로 남았다 (미스테이지).
- 서브모듈 커밋과 부모 커밋은 파일 경로를 명시해서만 스테이지했다 (`git add <path>`, 와일드카드 없음).

### 커밋 메모 (R2)

- 서버 (`insuniverse-server` · feat/admin-rbac): 4 파일만 커밋했다 (`access-control.module.ts` · `access-control.spec.ts` · `scope-monitor.ts` · `scope-monitor.spec.ts`).
- 되돌린 변경: 서버 워킹트리의 `access-control.guard.spec.ts` · `access-control.interceptor.ts` · `access-control.spec.ts` · `permission/permission-admin.service.ts` 포매팅 변경은 `.claude/meta/2026-10-19-admin-rbac-m5/foreign-format-r2.patch` 에 보존한 뒤 HEAD 로 되돌렸다. 이 커밋 뒤 서버 워킹트리는 clean 이다. R1 커밋 메모의 「이 파일은 워킹트리에 그대로 남았다」는 이 항목으로 대체된다.
- 부모: 서브모듈 포인터 · 이 notes · `.claude/PRPs/plans/admin-rbac-m5.gates.sh` · `.claude/meta/2026-10-19-admin-rbac-m5/` 의 5개 (`_lib.sh` · `counterfactual.sh` · `isolated-ddl-axis.sh` · `probe.mjs` · `identities.json`) · patch · backlog 를 파일 경로로만 스테이지했다. 계획서는 고치지 않았다.

## 마감 재검증 (2026-10-19, 최종 HEAD)

- 기준 커밋: 부모 `9989d44` · 서버 `d311e0bb` · admin `146ed4d6` · site `b4200ff2`.
- 실행 위치: 세 서브모듈 워킹트리와 분리된 임시 detached 워크트리 (부모 9989d44 · 서버 d311e0bb · admin 146ed4d6 · site b4200ff2). 이유: 서버 워킹트리 (`feat/admin-rbac`) 에 커밋되지 않은 변경 4파일이 있다 (`apps/api/src/modules/auth/access-control/` 의 `access-control.module.ts` · `access-control.spec.ts` · `scope-monitor.spec.ts` · `scope-monitor.ts`). 이 재검증 세션이 만든 변경이 아니어서 건드리지 않았다 (정책 §5-7). 검사 결과는 HEAD 기준이며, 워킹트리 변경은 반영되지 않았다. 커밋 전에 이 4파일의 출처를 확인해야 한다 (미확인).
- 격리: 스키마 덤프와 저장 함수 SQL 은 합성 스키마 (security-review 격리 스키마, 158표, 10-15). 검사마다 일회용 MySQL 8.0.42 · Redis 7 컨테이너를 띄웠고 실행 뒤 전부 내렸다.
- 실행 순서: 계획서 Validation 블록 전체 1회 (`#1` 부터 순차, `set -e`). 전체 블록은 `#14` 에서 멈췄다 (`#1`–`#13` 통과). 이어서 `#9`–`#16` 을 블록별로 따로 돌렸다.

| 검사 | rc | 핵심 수치 |
|---|---|---|
| #8 | 0 (전체 블록 안에서 통과) | probe 행 6336 = 704 × 9 (before · after 각각 6336), `status=0` 0줄, `diff=0`, 헤더 변형 2줄 모두 `same=yes`, `/live` · `/health` 두 빌드 모두 200 |
| #9 | 0 | `base_would_deny_admin=12`, `mutant_no_record_admin=0`, `mutant_allow_fixed_admin=0`, `unknown_would_deny_admin=0`, `skipped_admin=0`, `unassigned_admin=79`, 기대 집합 대조 일치 |
| #10 | 0 | jest 9 건 통과 · 실패 0 · 보류 0 (`it(` 9 건과 일치) |
| #11 | 0 | `rerun_after_wipe=refused`, `rows_after_wipe=0`, `mutant_rerun_after_wipe=restored` |
| #12 | 0 | `loaded_check=ok`, 적재 행 303 · 합성 역할 3 · 코드 101, `rbac_loaded=0`, `super_loaded=0` |
| #13 | 0 | `status_match=yes` (표 있음 401/200/200, 표 없음 401/200/200), `absent_label=table-absent`, `absent_as_deny=no` |
| #14 | 1 | 이 마일스톤이 덧붙인 backlog 줄 37건 중 `RESOLVED-IN` 형식이 아닌 것 31건 (HIGH · MEDIUM · LOW · FAIL 줄). 앞 절 2) 와 같은 원인 (사이드카 계약과 사용자 지시 충돌). 사이드카는 고치지 않았다 |
| #15 | 0 | 호스트 절대경로 0 · 동그라미 숫자 0 · `.claude` 줄 번호 인용 0 · 토큰 형태 0 |
| #16 | 0 | `lint-plan.mjs` rc 0 |

- 검사 #9 기대식: HEAD 의 사이드카는 rbac 스템 행만 센다 (기대 12). 앞 「최종 결과」 절 1) 의 「기대 39 = rbac 12 + 최고관리자 전용 27」 과 다르다. 어느 커밋에서 기대식이 바뀌었는지는 확인하지 않았다. 최고관리자 전용 27행이 가드 단계에서 403 으로 막힌다는 관측 사실은 그대로다.
- 검사 #14 는 HEAD 에서도 붉다. 사용자 지시 「HIGH 는 backlog 에 기록」 과 사이드카의 「이 마일스톤 줄은 RESOLVED-IN」 전제가 충돌하는 문제이며, 기각하거나 사이드카를 고치는 판단은 최종 리뷰 몫이다.
- 검증하지 않은 것 (미검증): 워킹트리의 미커밋 4파일 내용 (HEAD 와의 차이가 포매팅뿐인지, 의미 있는 편집이 섞였는지). 격리 실행 이외의 실트래픽 수치.
- 임시 워크트리 · 일회용 컨테이너는 검증 뒤 제거했다. 이 절은 커밋하지 않았다.

## 마감 판정 (2026-10-19)

fable · codex 두 리뷰를 받아 메인 세션이 확정했다.

1) 검사 #9 기대 12: 수용 (그대로 둔다). 최고관리자 전용 27행은 현행 `SuperAdminGuard` 가 가드 단계에서 403 으로 막아, would-deny 정의(판정과 현재 가드 결과가 다른 행)에 들지 않는다. 기대 12 는 사이드카(`b35663d`)에 이미 반영됐고 마감 재검증이 그 정의로 통과했다. 계획 :73 의 문구 정정은 계획 본문 동결 때문에 여기 notes 에만 적는다 (backlog 에 기존 줄 있음).

2) 검사 #14: 사이드카를 고쳤다. 이 마일스톤이 덧붙인 backlog 줄은 HIGH · MEDIUM · LOW · FAIL 등 여러 태그를 가지는데 사이드카는 RESOLVED-IN 만 허용해 31건이 붉었다. 원인은 사이드카 계약과 「HIGH 는 backlog 에 기록」 지시의 충돌이다. 태그를 닫힌 집합으로 넓히고 「RESOLVED-IN 1건 이상」을 더했다. append-only 와 계획 경로 조건은 유지했다. 음성은 `NOTE` 태그 한 줄을 넣은 사본에서 rc 1 로 확인했다.

3) backlog (Q3): F2(자기 판정 행 rbac 분류 · backlog 2026-10-19 HIGH) 와 C2(범위 감시 호출자 0 · 2026-10-19 HIGH) 는 이미 「→ 다음 계획 반영 대상」 표지가 달린 줄이 있어 다시 쓰지 않았다. 표지 보강도 필요 없었다. 대신 M6 착수 조건 줄을 덧붙였다 (2026-10-19 HIGH · 「M5 범위 would-deny 측정 미실시 — M6 Task 1 에서 배선 · 측정 후에만 범위 강제」).

4) 완료 여부 (갈림): fable 은 「편차 명시 뒤 완료」, codex 는 「완료 보류」. 메인 세션은 fable 을 택했다. 근거는 계획서 :214 · :326 과 운용 정책 §2 M6 행이 범위 감시 배선 · 측정을 M6 의 몫으로 이미 배정했다는 점이다. 완료 보류는 같은 측정을 M5 로 되돌리는 것이고, 배정된 자리(M6)에서 재는 것이 맞다. codex 의 우려는 M6 착수 조건으로 옮겼다. M6 계획은 Task 1 로 범위 감시를 실제 목록 · 단건 경로에 배선하고 would-deny 를 격리 DB 에서 측정하며, 그 측정이 끝나기 전에는 범위 강제 코드를 쓰지 않는다.

PRD 편차 (C2): PRD M5 Outcome 은 「실제 대상 · 목록 조건으로 would-deny 를 잰다」를 요구한다. M5 는 이를 하지 않았다. 완료 표시는 이 편차를 명시한 채로 한 판정이다. 이 편차를 숨기는 문구는 PRD 에 넣지 않았다 (PRD 는 행 5 의 Status · Plan 만 바꿨다).
