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

## 마감 요약

- 마감 판정: **in-progress**. PRD Delivery Milestones 의 6 행 Status 를 `in-progress` 로 두었다. Outcome · 선행 · 증명 중 미달 항목이 있어 complete 로 올리지 않는다. 최종 리뷰의 달성 판정 (이 노트 「달성 판정 (정본)」) 과 같다.
- 마감 기준 커밋: 부모 `0485234` (게이트 재실행 시점), 서버 `53a3e865`, 관리자 `146ed4d6`, 사이트 `b4200ff2`. 세 서브모듈 워킹트리는 깨끗하다.
- 사용자 지시: `verify-user-directive.sh` exit 0, 세션 기록 원문 2026-10-19T02:21:22Z 확인. 계획 receipt 부재 (skip-receipt) 로 진행한 것은 이 지시에 따른 것이다.

### 게이트 결과 (2026-10-20 재실행, 부모 HEAD 기준)

- `bash .claude/PRPs/plans/admin-rbac-m6.gates.sh` 는 줄 89 (`test -f synthetic-tenants.sql`) 에서 멈췄다. 검사 #1–#4 는 통과했다. `set -e` 라 #5 이후는 이번 재실행에서 돌지 않았다.
- 정본 표는 이 노트의 「검사 결과 요약」 이다 (이전 실행값). 요약: 통과 #1 · #2 · #3 · #4 · #6 · #7 · #11 · #12 · #14 · #15. 붉음 #5 (측정 산출 없음) · #8 (여섯 핸들러 중 money · analyze · agency-fc-admin 에 범위 호출 없음, 편차 C) · #9 (ON9 pipefail 결함, 측정 sha 조상 성립 불가, 편차 E) · #10 (응답 본문 해시 휘발 필드, `cmp` 검사 명령 결함) · #13 (날짜 식 결함).
- 범위 단위 spec 과 격리 MySQL 경합 itspec (8.0.42) 은 구현 · 리뷰 단계에서 녹색이었다. 이번 마감에서는 다시 돌리지 않았다 (미검증).

### 코드 리뷰 (2 라운드)

- R1: 26행. 흡수 10 · 부분 흡수 6 · 기각 1 · 이연 9. HIGH 흡수 2 (조직 이동 잠금 뒤 재읽기, 소속 지정 판정의 tx 전달), HIGH 미해소 3 (측정 산출 순서, 응답 불변 증거, 단건 판정 연결).
- R2: 7행. 흡수 6 · 부분 흡수 1 (강제 OFF 경로 하위 추가 잠금은 백로그 MEDIUM 행). HIGH 없음.

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

라운드 요약: [{"r":1,"found":11,"absorbed":4,"high_absorbed":1,"high_open":4,"codex":"ok"},{"r":2,"found":5,"absorbed":3,"high_absorbed":0,"high_open":0,"codex":"ok"}]

- 달성 판정 (정본, 위 「최종 리뷰」 절): 미달. HIGH 미해소 4 (머니 · 분석 목록, 조직관리자 지정 잠금, 조직 라우트 판정 분리, 측정 산출 부재) 는 백로그 HIGH 행이 맡는다.

### PRD 미달 목록

아래 7건은 이 마감에서 백로그 2026-10-20 HIGH 행으로 추가했다 (`→ 다음 계획 반영 대상 · 보완 계획 admin-rbac-m6b`).

1) 목록 · count · 집계 · 내보내기가 같은 범위 조건을 받는다 — 미달. 머니 · 분석 (I5–I8) 강제 경로가 없고, 집계 · 내보내기 일치 spec 도 없다.
2) 단건 3의미 판정 — 미달. `judgeSingleScope` 가 핸들러에 연결되지 않았다 (호출처 0).
3) 쓰기 검사와 변경을 같은 트랜잭션으로 — 미달. 조직관리자 지정은 조직 행을 잠그지 않고, 조직 라우트 판정은 쓰기 트랜잭션 밖에 있으며, 소속 지정은 담당 영업 행을 잠그지 않는다.
4) 신규 엔드포인트 범위 선언 강제 (M2 단언 활성화) — 미달. 계획 · 노트 어디에도 없었다 (최종 R1 fable).
5) 선행 「M5 범위 would-deny 측정」 — 미달. 측정 산출 (`synthetic-tenants.sql` · `identities-scope.json` · `scope-would-deny.json` · `scope-measure.mjs`) 이 없다 (편차 A · B).
6) 증명: 응답 불변 (검사 #10) — 미달. 최종 커밋 기준 증거가 없고, 본문 해시에 휘발 필드 규칙이 없다.
7) 조인표 — 미검증. 이행표 I1–I9 의 조인 열 다수에 「(미검증)」 표지가 붙어 있다.

게이트 결함 (ON9 pipefail · #13 날짜 식 · #10 `cmp` 명령) 은 PRD 항목이 아니라 계획 검사의 결함이라 새 행을 만들지 않았다. 기존 HIGH · MEDIUM 행이 맡는다.

### (미검증) 과 사용자 몫

- (미검증) 강제 ON 실트래픽 would-deny (측정 산출 없음).
- (미검증) 응답 불변 probe 재실행 (최종 커밋 기준) · 격리 MySQL 경합 시험 재실행 · Redis 필요 시험 (`treat-queue` · `treat-reservation`).
- (미검증) money · analyze 강제 경로 (편차 C) · 조직 이동 목적지 판정 원자성 (편차 D) · I8 「소유자 또는 관리자 열람」 의미 (잠정).
- 사용자 몫: R5 영업 · 기업 범위 예외 (책임자 · 기한 · 잔여 위험, 예외 행 미작성) · R19 소비처 확인 책임자 · R7 NULL 계정 · 운영 DB 조회 · 조직 쓰기 · 소속 지정의 영업 업무 실사용 (Open Questions) · 강제 ON 체크리스트 (정책 §4: DDL 두 축 · 적재 · NULL 0 · would-deny 판단 표 · R2 · R5 · R19 · M7 번들 · R16).

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

1) 열림 조건: M7 은 M5 와 옛 번들 403 확인만 조건이라 M6 과 무관하다. M8 은 M4 · M7 · R7 · R8 · R18 이라 M6 과 무관하다. M9 (M6 · M7 · M8 완료) 와 M10 (M4 · M6 · M8 · M9 완료) 은 M6 이 완료되어야 열린다.
2) 착수 sha (정책 §1 과 앞 마일스톤 노트): 서버 `f9072f75` · 관리자 `146ed4d6` · 사이트 `b4200ff2` · 부모 `ddaa46a1`. 구현 커밋은 서버 `53a3e865`.
3) stg 동기화가 M7 전에 필요하다. 실측: 로컬 `origin/stg` (`530bf62a`) 는 로컬 병합 `03ce7146` 의 조상이 아니다. 메모 기준으로 #96 은 stg 에 병합되어 있고 RBAC 브랜치와는 `main.ts` 한 곳이 충돌한다. 통합한 뒤 새 착수 sha 로 조상 검사와 M1 재동결을 다시 한다.
4) 측정 설계: 목록 응답 열에 범위 소유 키가 없다 (I1 `getAllAgencyList` 실측, `sagId` 없음). 신원별 기대는 역할 가드에서 도출하고, 행 소유 키는 원천 조인으로 얻는다. 응답 불변 해시는 휘발 키 (health 지연 · 엑셀 zip 시각) 를 계획에 명시한다.
5) 게이트 결함 3건 (ON9 · #13 날짜 식 · #10 `cmp`) 과 gates 본문 digest 고정은 m6b 계획에서 고친다.

## 계획 게이트

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

약어 풀이: R1 ~ R3 은 계획 판의 검토 차수, L1 은 계획서 형식 검사, L2 는 4역할 리뷰 패널 (architect · security · test · invariant), L3 는 Codex 교차 리뷰다.

| 라운드 | 계획 해시 | L2 (4역할) | L3 (Codex) | 처리 |
|---|---|---|---|---|
| R1 | `sha256:b66a4de7…` | divergent · 정족수 실패 · blocking 10건 | divergent | `7d67b25` 에서 계획 반영 |
| R2 | `sha256:1646e545…` | divergent · 정족수 실패 · blocking 6건 | divergent | `fc10c87` 에서 계획 반영 |
| R3 (상한 3/3) | `sha256:dfd12380…` | converged · 정족수 통과 (4/4 pass) · 지적 10건 (MEDIUM 6 · LOW 4) | divergent (high 2) | 흡수하지 않음 · 아래 상한 처리 |
| 구현 진입 (상한 뒤) | `sha256:dfd12380…` | — | — | skip-receipt (`MCCP_SKIP_RECEIPT=1`, preflight ok) · plan receipt 는 이 단계에서 쓰지 않음 |
| 판정 · 구현 진입 receipt 부재 | `sha256:dfd12380…` | — | — | 기각 — 사용자 지시 확인(verify-user-directive.sh, 2026-10-19T02:21Z 원문). 상한 뒤 receipt 는 divergent 봉인으로 대체 · 커밋 전 점검 |

R3 판정: `review_verdict=divergent` · `review_source=hybrid` · `forwardCodexVerdict=true` · 사유 `L1+L2 converged but L3 (Codex) returned divergent`. L1 converged, L2 converged (4/4 pass), L3 divergent (raw `needs-attention`, high 2).

### 상한 처리

- 사용자 지시 (세션 입력 원문, `verify-user-directive.sh` exit 0 으로 확인, 2026-10-19T02:21Z): 「Round 는 최대 3까지만 진행하고 마지막 라운드는 CRITICAL 이상 건이 아닌이상 skip-receipt로 진행해줘. (환경 변수도 적용). HIGH건은 backlog에 기록해서 다음 plan 작성에 반영하는 방법으로 진행.」 이 지시가 전역 CLAUDE.md mccp 한시 규칙의 「receipt 없음 → 멈춤」을 대체한다.
- CRITICAL 유무: 없음. 라운드 3 의 최고 등급은 HIGH 2건 (L3 codex). 나머지는 L2 MEDIUM 6 · LOW 4 다.
- receipt: 이 단계에서 쓰지 않았다. plan 게이트 receipt write 는 시도하지 않았다 (intent gate 가 fail-closed 로 막는다).
- 이전 단계가 남긴 receipt: `.claude/receipts/mccp-plan-codex/admin-rbac-m6.json` 은 상한 봉인 (`seal-divergent-hybrid.sh 6`, `VALIDATE_EXIT=0`) 이 만든 파일이다. 이 단계는 그 파일을 고치지 않고 커밋에만 싣는다. 읽을 때 주의할 세 가지:
  1. `codex_verdict` · `review_verdict` 는 divergent 인데 `resolution.converged` 는 true 로 찍혀 있다. 수렴 증거로 읽으면 안 된다.
  2. `intent_gate_verdict` 는 incomplete 다. 봉인 시 intent gate 가 막아 감사 우회 사유 (`INTENT_OVERRIDE=1`) 로 다시 썼다.
  3. `receipt_hash` 는 `sha256:7842f688…` 이다.
- 구현 진입 검증: `MCCP_SKIP_RECEIPT=1 node <mccp 플러그인 캐시>/mccp/1.33.6/scripts/receipt/cli.js preflight --command mccp:prp-implement --decision admin-rbac-m6 --plan .claude/PRPs/plans/admin-rbac-m6.plan.md` 가 `"ok": true` · `"bypassed": true` · `"reason": "MCCP_SKIP_RECEIPT"` 로 exit 0 이다. env 는 그 명령에만 주었다. `MCCP_SKIP_INTENT_GATE` 는 쓰지 않았다.
- `.claude/state/fix-task.md` 가 `escalate: true` 로 생겼다 (verdict `codex_divergent`, 만료 2026-10-27). santa-loop 실행을 요구하는 상태다. gitignore 대상이며 이 단계에서 지우지 않았다. 처리 방식은 사용자가 정한다.
- 계획서 · `l2.json` · `decision.json` 은 고치지 않았다.

### backlog 로 넘긴 항목

`.claude/plans/codex-findings-backlog.md` 끝의 2026-10-20 항목 12줄이 라운드 3 의 미해소 지적 전부다 (L3 codex 2 · L2 MEDIUM 6 · L2 LOW 4, CRITICAL 0). 이 항목들은 이전 단계에서 이미 덧붙여져 있었고 이 단계에서 다시 쓰지 않았다. HIGH 2건은 사유 끝에 `→ 다음 계획 반영 대상` 이 이미 있어 표지 줄을 더 붙이지 않았다.

HIGH 2건:

1. L3 codex: 측정 선행 조건이 정상 역할 가드와 충돌한다 (계획 :163-166). 계획은 측정 신원 전부에 200 을 요구하는데, I4 `getSubscribeFailedList` 는 `@Roles('admin', 'sales')` 이고 I7 `getPaymentList` 는 상속이라 corp 신원은 403 이 정상이다.
2. L3 codex: 응답 행에 범위 판정용 소유 키 (sagId · agcId · orgPath) 가 없다 (계획 :164-166). `judgeRowScope` 는 키가 없으면 deny 라 정상 담당 행도 범위 밖으로 집계되고, 검사 #5 는 정상 담당 행의 allow 를 요구하지 않아 잘못된 측정을 통과시킨다.

MEDIUM 6: org_path null 행과 scope-condition 의 like 불일치 (architect) · 목록·count 「이것만 쓴다」 주장과 검사 #8 범위 (architect) · 조직 이동 목적지 부모 판정과 부모 행 잠금 (security) · 경쟁 안전 시험이 잠금 제거에 안 깨짐 (test) · OFF 경로 인자 불변 시험 부재 (test) · 측정 54셀 자기 보고 (invariant).

LOW 4: 구분자 형식 출처가 둘 (architect · security) · 「1」 → 「/1/」 단일 세그먼트 허용 (security) · 검사 #8 음성의 관측 기록 (test) · 검사 #9 스킵 술어의 server_sha 신뢰 (invariant).

### 구현 진입 시 주의

- 계획 본문은 동결이라 HIGH 2건이 계획에 그대로 남아 있다. 구현 뒤 검사로 확인하기 전에는 두 건을 해소된 것으로 적지 않는다.
- 이 구현 진입은 수렴 증거가 없다. `MCCP_SKIP_RECEIPT` 로 열렸고 receipt 는 divergent 다.
- 다음 계획은 backlog 의 `→ 다음 계획 반영 대상` 항목을 입력으로 쓴다. 핵심은 측정 신원별 기대 상태를 실제 역할 가드에서 도출하는 것과, 응답 행의 소유 키를 해석하는 측정 어댑터를 명시하는 것이다.

## 구현 (implement 단계)

- 착수 sha: 서버 `f9072f75` · 관리자 `146ed4d6` · 사이트 `b4200ff2` · 부모 `ddaa46a1` (계획과 같다). 커밋은 하지 않았다 (사용자 지시: implement 뒤 커밋 전에 별도 세션에서 code-review). 워킹트리 변경은 서버 서브모듈과 부모의 메타 산출·백로그·노트다.
- 계획 게이트는 상한 3/3 에서 divergent 로 끝났고 receipt 는 쓰지 않았다 (사용자 지시 2026-10-19, `verify-user-directive.sh` exit 0 · 원문 확인). implement 단계는 `MCCP_SKIP_RECEIPT` 로만 진입했다.
- 라운드 원장은 이 구현 단계에서 `mccp-implement-codex__admin-rbac-m6` 로 봉인했다 (cap 3, enforce). 계획 게이트 원장과 섞지 않는다.

### 완료한 Task

| Task | 상태 | 근거 |
|---|---|---|
| 1 기준선 | 완료 | `jest-baseline.txt` (실패 suite 6 · 실패 시험 3 · 착수 jest 25초 실측), `tsc-baseline.txt` (23 줄), `prior-specs-baseline.txt` (20 줄, `super-admin.seed.mysql.spec.ts` 가 MySQL 없이 붉다 — 기준선에 그대로 둔다) |
| 2 이행표 | 완료 | `scope-transition-table.md`: I1–I9 행 (핸들러 9 항목 · 실재 확인), AW 행 15 (`@AllowOrgWrite()` 사용처 수와 같다). 조인 열은 「(미검증)」 표지가 붙은 자리가 많다 |
| 3 합성 테넌트 · 신원 | 미완 | 시드 SQL · 범위 신원 파일을 쓰지 않았다. 실행 없는 산출을 만들지 않으려고 뺐다 (아래 편차 A) |
| 4 측정 하네스 · 산출 | 미완 | 하네스 · `scope-would-deny.json` 을 만들지 않았다 (편차 A · B) |
| 5 범위 정책 함수 | 완료 | `scope-monitor.ts` (접두 경계 orgPath · 절 목록 · 단건 3의미 함수) · `scope-condition.ts` (drizzle 어댑터) · `scope-monitor.spec.ts` 21건 녹색 |
| 6 목록 · count · 집계 · 내보내기 | 부분 | I1 (`getAllAgencyList`) · I4 (`getSubscribeFailedList`, 목록과 count 가 같은 where) · I2 (조직 목록 · 상세 · 생성 · 수정 · 이동 · 삭제, 영업 담당 기업 검사) 는 상수 ON 경로 구현. I5–I8 (money · analyze) 은 미구현 (편차 C) |
| 7 단건 · 일괄 · 소속 · 조직 쓰기 | 부분 | I3 `assignOrg` · `setOrgManager` 를 `runMemberOrgWrite` (ON 일 때만 drizzle.transaction) · `lockMembersForOrgAssign` (FOR UPDATE) · `countMembersInSalesScope` 로 바꿨다. 일괄은 범위 밖이 하나라도 있으면 403 (쓰기 0회). 조직 이동은 `moveSubtree(lock)` 로 대상과 목적지 부모 행을 FOR UPDATE 한다 (편차 D) |
| 8 강제 상수 · ON 시험 | 완료 (상수 · 시험) | `SCOPE_ENFORCE_SALES_CORP = false` 한 줄 (access-control.constants.ts). `scope-enforce.spec.ts` · `scope-handlers.spec.ts` (jest.mock ON/OFF) 녹색 · 경합 itspec 두 개 (격리 MySQL 두 연결) 녹색. `api.yml` 편입은 하지 않았다 (편차 E) |
| 9 응답 불변 probe | 실행 중 | M6 probe 사본 (`isolated-probe.sh` · `probe.mjs`) 을 쓰고 격리 스택에서 착수 빌드 · 워킹트리 빌드 · 착수 재실행 세 번을 돌린다. 결과 (행 수 · cmp) 는 아래 「검사 결과」 에 적는다 |
| 10 기록 · 백로그 | 부분 | 백로그에 RESOLVED-IN 1건 (1576) 과 HIGH 3건 (구현 중 재현 · 게이트 결함) 을 append. 이 노트 |

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

- **A. 측정 산출을 만들지 않았다 (Task 3 · 4).** 계획의 HIGH (`backlog 2026-10-19 …` 의 「응답 행에 범위 판정용 소유 키가 없다」) 가 구현 중 그대로 재현된다. I1 `getAllAgencyList` 의 응답 열은 `agcId · agcCorporateName · agcCorporateCode` 뿐이고 `sagId` 가 없다. 조직 목록 · 구독 실패 목록도 같다. 그래서 `judgeRowScope` 로 응답 행을 세면 전 행이 deny 로 찍힌다 (행 소유 키 부재). 측정 신원별 기대 상태 (I4 corp 신원 403) 도 계획의 「200」 규칙과 충돌한다 (두 번째 HIGH). 이 두 조건을 고치지 않고 산출을 만들면 가짜 수치가 된다. 산출은 만들지 않았고, 검사 `#5` 는 붉다.
- **B. 측정 신원 6종의 발급과 합성 시드를 하지 않았다.** 위 A 의 이유로 시드 · 신원 파일을 실행 없이 쓰지 않았다.
- **C. I5–I8 (money-admin · analyze-admin) 의 강제 경로는 없다.** 계획 Files 표는 이 두 컨트롤러만 고치라고 했는데, 목록 · 내보내기 · 단건을 저장소 조건으로 바꾸려면 money · analyze 서비스와 저장소를 고쳐야 한다. 그 파일들은 Files 표에 없다. 상수가 OFF 라 런타임 영향은 없다. **상수를 ON 으로 켜기 전에 반드시 필요하다 (M9 조건).** 그 전까지 영업 신원은 money · analyze 목록을 전사로 본다 (현행과 같다).
- **D. 조직 이동 검사의 원자성이 불완전하다.** 목적지 부모 판정 (`parentPath.startsWith(orgPath)` · 기업 일치) 은 서비스에서 트랜잭션 밖에 있고, `moveSubtree(lock=true)` 는 그 뒤 대상과 부모 행만 잠근다. 검사와 잠금 사이의 경합은 막지 않는다 (백로그 MEDIUM 행 「목적지 부모 판정」 과 같다). 검사 `#8` 은 이 사실을 보지 못한다.
- **E. `api.yml` 편입 · 측정 산출 커밋 · 상수 도입 커밋의 sha 는 이번 단계에서 하지 않았다.** 커밋 금지 (사용자 지시) 이므로 검사 `#9` 의 「측정 산출의 server_sha 가 상수 도입 커밋의 진짜 조상」 은 구조상 붉다 (측정 산출이 없고, 상수 커밋도 없다).
- **F. 게이트 명령 결함 두 개를 고쳤다.** (1) 검사 `#8` 의 race 실행 `--testRegex` 는 기본 설정의 `testMatch` 와 함께 쓸 수 없어 Validation Error 로 멈춘다 → `--testMatch '**/*.itspec.ts' --testPathPattern 'race\.itspec\.ts$'` 로 바꿨다 (필터 의미는 같다). (2) 검사 `#13` 의 날짜 조건 (`2026-10-19` 만) 은 R3 줄 (2026-10-20) 때문에 계획 시점 관측과 어긋난다. 고치지 않았다 (백로그 HIGH 행으로 넘긴다).
- **G. 경합 itspec 의 설정.** 기본 `jest.config.js` 의 drizzle 스텁을 피하려고 `jest.mock('drizzle/schema', requireActual)` 를 쓴다 (`agency-fc-policy.spec.ts` 와 같은 방식). 격리 DB 는 `m6-iso-mysql` 일회용 컨테이너 (mysql 8.0.42) 다. 환경 변수가 없으면 건너뛰지 않고 붉다.
- **H. Phase 2.5.0 을 나중에 밟았다.** 첫 Codex 호출이 「round cap reached (mccp-plan-codex__admin-rbac-m6)」 로 거부됐다. 2.5.0 의 `codex-policy seal` · `review-rounds seal --gate mccp-implement-codex` 를 밟지 않은 것이 원인이다. 봉인 뒤 다시 호출했다 (원장 `mccp-implement-codex__admin-rbac-m6`, cap 3).

- **I. 응답 본문 해시의 휘발 필드 (Task 9).** 첫 probe 실행의 착수 빌드와 워킹트리 빌드 사이에 112행이 달랐다. 원인은 두 가지다. (1) `/health` · `/health/detail` 의 `db.ms · redis.ms` 지연 시간 (실행마다 바뀐다). (2) 엑셀 · 이진 다운로드 본문의 파일 안 생성 시각 (zip 항목 시각). 그래서 probe 사본에서 `ms · durationMs · elapsedMs` 를 휘발 키로 빼고, 이진 본문은 형식과 바이트 수로만 해시한다 (정규화 본문 해시는 JSON 과 텍스트에만 적용). 이 완화는 응답 내용 대조를 이진 본문에서 약하게 만든다. 이진 본문의 내용 차이는 이 probe 가 잡지 못한다 (미검증).

### 검사 결과 (2026-10-20, 커밋 없는 워킹트리)

검사 본문 (`admin-rbac-m6.gates.sh`) 은 아래 줄 수준으로 확인했다. 전체 실행 결과는 「검사 결과 요약」 의 표가 정본이다.

- `#1` · `#2` · `#3` · `#4` · `#6` · `#11` · `#12` · `#14` · `#15`: 사전 실행에서 통과 (아래 요약이 정본) (`#11` 은 기준선 파일을 같은 절차로 만들었다).
- `#7`: scope-monitor.spec.ts 21/21.
- `#8`: 강제 ON 시험 (scope-enforce 4 · scope-handlers 5) 과 경합 itspec 3 은 녹색. 그러나 money · analyze · agency-fc-admin 컨트롤러 파일에 범위 함수 호출이 없어 호출 검사가 붉다 (편차 C · 소속 쓰기는 서비스에 있다).
- `#5` · `#9` · `#10`: 아래 「검사 결과 요약」 참고.

### 미검증 (사용자 몫 · 이월)

- (미검증) 강제 ON 의 실트래픽 would-deny (측정 산출이 없다 · 편차 A).
- (미검증) I8 「소유자 또는 관리자 열람」 의미는 잠정이다 (사용자 확인 대상).
- (미검증) money · analyze 강제 경로 (편차 C) · 조직 이동 검사 원자성 (편차 D).
- (미검증) 백로그 1484 · 1489 잔여 · 1574 · 1560 · 1563 · 1564 · 1577 는 이 구현에서 다루지 않았다 (계획 이월 표와 같다).
- (미검증) 롤백은 M6 커밋 되돌리기다 (커밋 전이라 아직 없다).

## Codex Implementation Review

- 호출: `node <mccp 플러그인 캐시>/scripts/lib/codex-invoke.js adversarial-review` (fail-closed Bash wrapper). focus 는 서버 서브모듈의 변경 파일 목록을 가리켰다 (부모 diff 는 gitlink 뿐이라 워킹트리를 읽는다).
- 라운드 수: 1 (상한 3 · 원장 `mccp-implement-codex__admin-rbac-m6`). 첫 호출은 원장을 봉인하지 않아 cap 거부로 멈췄고, 2.5.0 봉인 뒤 다시 호출했다. 거부된 호출은 라운드로 치지 않는다.
- 판정: `needs-attention` → `CODEX_VERDICT=divergent`. CRITICAL 없음. HIGH 3.
- 합치 결론: OFF 경로의 응답·SQL 회귀는 정적 검토에서 찾지 못했다 (실행 검증은 probe 가 본다). ON 활성화는 보류해야 한다.
- YAGNI Triage:
  | Finding | Severity | Verdict | Why |
  |---|---|---|---|
  | F1 조직 이동: FOR UPDATE 가 ID 만 읽고 결과를 버려, 잠금 대기 뒤에도 옛 경로·부모로 순환 판정과 경로 재작성을 한다 | HIGH | DEFER_TO_BACKLOG | ON 전용 경로. 잠금 뒤 최신 행 재읽기와 순환 재검증은 계획 Task 7 의 범위를 넘는다. 다음 계획이 맡는다 |
  | F2 소속 쓰기: 잠그는 것은 tn_member 뿐이고 담당 영업 (tn_agency.sag_id) · 목적지 조직 판정은 잠기지 않은 별도 조회다 | HIGH | DEFER_TO_BACKLOG | 같은 계열. 기업 행 잠금과 모든 판정의 tx 전달은 다음 계획 (상수 ON 전 필수) |
  | F3 단건 판정 (judgeSingleScope) 이 핸들러에 연결되지 않았다 (analyze detailList · agency-fc getDetail · money 지갑 목록 · 내보내기) | HIGH | DEFER_TO_BACKLOG | 편차 C 와 같다. 기록된 미구현이며 OFF 대비 신규 회귀가 아니다. ON 활성화 (M9) 전에 반드시 닫는다 |
- Deferred to backlog: 3 → `.claude/plans/codex-findings-backlog.md` 의 2026-10-20 HIGH 줄 (`→ 다음 계획 반영 대상`).
- Open Questions: 없음 (auto-CRITICAL 목록에 해당하는 항목 없음). DIVERGENT_UNRESOLVED 로 표시한다 (F1–F3 는 backlog 로 넘겼다).
- Security Reviewer: Task 도구가 이 실행에 없어 `security-reviewer` 호출 불가 → `> security-reviewer unavailable, skipped (auto-fallback): Task 도구 없음 (workflow 하위 에이전트)` 로 기록하고 `SECURITY_SKIPPED_REASON` 을 내보내 receipt 에 실었다. 인증 · 범위 경계를 건드리므로 리뷰 단계에서 보안 리뷰가 필요하다.
- Impeccable: 설계 신호 없음 (`no-signal`, silent skip). 서버 변경은 UI 파일이 아니다.
- Codex session 참조: `codex-invoke` 라운드 1 (stdout 은 워킹트리 기준 `mccp/tmp/codex-m6.stdout`).

## 검사 결과 요약 (검사 본문을 번호별로 따로 돌렸다, 2026-10-20 · 커밋 없음)

| 검사 | 결과 | 사유 |
|---|---|---|
| #1 착수 sha · 선행 조상 | 통과 | |
| #2 기준선 헤더 · failed-count | 통과 | |
| #3 생성기 drift · 경로 맵 | 통과 | |
| #4 이행표 I1–I9 실재 | 통과 | |
| #5 측정 산출 셀 54 · 하네스 결속 | **붉음** | 측정 산출 (`scope-would-deny.json` 등) 을 만들지 않았다 (편차 A · B). 백로그 HIGH 행 |
| #6 AW 행 수 = 사용처 15 | 통과 | |
| #7 scope-monitor.spec 21/21 · 이름 조각 | 통과 | |
| #8 강제 ON 시험 · 경합 itspec · 여섯 핸들러 호출 | **붉음** | 시험 9건 · 경합 3건은 녹색. 호출 검사는 `agency-fc-admin.controller.ts` · `money-admin.controller.ts` · `analyze-admin.controller.ts` 에서 붉다 (편차 C · 소속 쓰기는 서비스에 있다). 검사 명령은 편차 F 로 고쳤다 |
| #9 상수 OFF · 측정 sha 조상 | **붉음** | 상수 OFF 한 줄 · 정의 1곳 · `true` 0건은 통과 (`ON9` 줄에서 멈춘다: `grep` 무일치 `rc 1` 이 `pipefail` 로 전파되는 게이트 결함, 백로그 HIGH 행). 조상 검사는 커밋 금지 때문에 성립하지 않는다 (편차 E) |
| #10 응답 불변 (probe 재실행 · cmp) | **붉음** | 상태 코드는 세 실행 (착수 · 워킹트리 · 착수 재실행) 에서 전부 같다 (6336행, status=0 없음, 헤더 변형 4행 같음). 본문 해시는 같은 빌드 안에서도 실행마다 달라진다 (착수 대 재실행 40행 차이). 응답 본문의 비결정성 (편차 I) 이 남아 cmp 가 깨진다 |
| #11 앞 마일스톤 권한 spec 통과 집합 | 통과 | 기준선은 같은 절차로 만들었다 (`super-admin.seed.mysql.spec.ts` 가 MySQL 없이 실패하는 것도 기준선에 있다) |
| #12 인터셉터 범위 함수 0 · 가드 `return true` | 통과 | |
| #13 백로그 append 전용 · 형식 · RESOLVED-IN | **붉음** | 추가 줄 중 2026-10-20 줄이 `2026-10-19` 만 허용하는 식에 걸린다 (게이트 결함, 백로그 HIGH 행). 추가 줄 자체는 append 전용이다 |
| #14 호스트 경로 · 동그라미 · 줄 번호 · 토큰 | 통과 | |
| #15 plan-kit lint | 통과 | |

## 구현 영수증 (implement receipt)

- 경로: `.claude/receipts/mccp-implement-codex/admin-rbac-m6.json`. `validate --command mccp:prp-implement` 는 `MCCP_SKIP_RECEIPT=1` 를 붙여 exit 0 이다.
- 필드: `resolution.codex_verdict = divergent` · `meta.security_skipped = true` (Task 도구 없음) · `meta.impeccable_silent_skip = true` (no-signal) · `round = 1` · `plan_hash = sha256:dfd12380…` (계획 게이트 라운드 3 판과 같다, 계획서는 그 뒤 바뀌지 않았다).
- 주의: 같은 파일의 `resolution.converged` 는 `true` 다. 발견 사항이 0 이라 CLI 가 계산한 값이다. 수렴 증거로 읽으면 안 된다. 판정은 `codex_verdict` 로 읽는다.
- 쓰기는 계획 게이트 영수증 (`.claude/receipts/mccp-plan-codex/admin-rbac-m6.json`) 이 있어서 `MCCP_SKIP_RECEIPT` 없이 됐다. 그래도 validate 는 지시대로 env 를 붙여 돌렸다 (`skipped: false` 가 찍혔다).

## 편차 추가 (검사 결과에서 나온 것)

- **I. 응답 본문 해시의 휘발 필드 (Task 9).** 위에 적었다. 같은 빌드 두 번 사이에도 40행이 달라 검사 `#10` 의 cmp 가 구조상 깨진다. 해결은 응답 본문의 시각 · 지연 · 생성 시각을 가려내는 것이다. 이 구현에서는 하지 않았다 (백로그 행으로 넘긴다).
- **J. 게이트 결함 두 개 (백로그 HIGH 행).** (1) 검사 `#9` 의 `ON9=$(grep … | wc -l)` 은 무일치 때 `pipefail` 로 멈춘다. (2) 검사 `#13` 의 날짜 식.
- **K. 검사 `#10` 을 게이트 안에서 다시 돌리지 않았다.** 같은 probe 를 이번 실행에서 세 번 돌렸다 (착수 · 워킹트리 · 착수 재실행). 게이트 안의 네 번째 실행은 같은 이유로 cmp 가 깨질 것이다. 그래서 시간을 쓰지 않았다.

## 코드 리뷰

커밋 없는 워킹트리에서 라운드 1/3 을 처리했다 (2026-10-20). 계획서는 고치지 않았다. 판정 기준은 흡수 · 부분 흡수 · 이연 · 기각 넷이다. 흡수 항목은 수정 전 검사가 붉고 수정 뒤 녹색임을 실행으로 확인했다 (반사실). 층위는 WT CLAUDE.md §2 의 번호다 (1 설계 결함 · 2 주장과 검사 불일치 · 3 명령과 게이트 혼동 · 6 게이트가 보는 대상 · 7 검사 자신의 주장 · 8 검사 사각지대, 「—」 은 형식 · 정리 항목).

| 라운드 | 출처 | 심각도 | 지적 | 판정 | 증거 | 층위 |
|---|---|---|---|---|---|---|
| R1 | code-review | HIGH | 조직 이동: 잠금 대기 뒤에도 옛 경로로 하위 전체를 다시 쓴다 (순환 경로 생성 가능). security MEDIUM 과 같은 지적 | 흡수 (목적지 부모의 범위 판정은 기존 백로그 MEDIUM 행) | `organization.race.itspec.ts` 「낡은 스냅샷 이동」·「낡은 스냅샷 순환」 — 수정 전 2건 붉음, 수정 뒤 3/3 녹색 (격리 `m6-iso-mysql` 8.0.42). 잠금 뒤 재읽기와 순환 재판정은 `organization.repository.ts` `moveSubtree` | 1 |
| R1 | security | HIGH | 소속 지정 ON: 트랜잭션 안에서 판정 조회가 풀 연결을 하나 더 요구한다 (목적지 조직 · 기업 판정이 tx 를 받지 않음). 동시 요청에서 API 전체가 대기한다 | 흡수 | `scope-enforce.spec.ts` 「연결 1개」 2건. 목적지 판정 인자에서 tx 를 빼면 붉음 (반사실 실측), 되돌리면 녹색. tx 는 `assertOrgBelongsToAgency` · `findOrgById` · `countMembersInCorpScope` 까지 전달 | 1 |
| R1 | code-review | HIGH | 측정 산출 (Task 3·4) 없이 강제 경로 (Task 6–8) 를 구현했다. 계획의 순서 전제가 깨졌고 해소 결정이 없다 | 이연 (백로그 HIGH 행, 다음 계획 반영) | 편차 A·B, 계획 :30, `.claude/meta/2026-10-19-admin-rbac-m6/` 에 측정 산출 없음 | 1 |
| R1 | code-review | HIGH | 응답 불변 증거 부족: 같은 빌드 재실행에서도 행 파일이 40행 다르다 (download 류 · 시각 기반 행) | 이연 (기존 백로그 HIGH 행과 같다. 이번 라운드에 probe 를 다시 돌리지 않았다, 격리 스택 필요, 미검증). 정적 대조: 이번 수정은 OFF 경로의 쿼리 인자와 호출 순서를 바꾸지 않는다 (`scope` 는 null, `tx` 는 undefined, `moveSubtree` 는 lock=false 의 기존 순서) | `rows-before-rerun.txt` 대 `rows-before.txt` 40행 차이, `probe.mjs` 60–72 | 3 |
| R1 | code-review | HIGH | 단건 3의미와 I5–I8 (money · analyze 목록 · 내보내기 · 단건) 이 제품 경로에 연결되지 않았다 | 이연 (기존 백로그 HIGH 행 「단건 판정 …」 과 같다. M9 전 필수) | `judgeSingleScope` 호출처 0 (`scope-monitor.ts`), 편차 C | 1 |
| R1 | code-review | MEDIUM | 소속 쓰기: 담당 영업 판정 근거 (tn_agency) 행을 잠그지 않는다 | 부분 흡수: 잠금 전 사전 판정과 판정의 tx 전달을 넣었다. tn_agency 행 잠금은 기존 백로그 HIGH 행 (「소속 쓰기 … 잠기지 않은 별도 조회」) 에 남긴다 | `scope-enforce.spec.ts` 「사전 판정」·「연결 1개」 | 1 |
| R1 | code-review | MEDIUM | 구현 영수증 (`.claude/receipts/mccp-implement-codex/admin-rbac-m6.json`) 이 divergent 를 converged 로 기록하고 Codex 지적 3건을 비운다 | 이연 (백로그 MEDIUM 행). 손으로 고치면 `receipt_hash` 가 깨진다. `converged` 는 `findings=[]` 에서 CLI 가 계산한 값이고 정본 판정은 `codex_verdict=divergent` 다. 재작성은 mccp write 흐름으로만 한다 | 영수증 14행 | 2 |
| R1 | code-review | MEDIUM | probe 요약 `after_sha` 가 `before_sha` 와 같아 두 빌드를 구별하지 못한다 | 이연 (백로그 MEDIUM 행). 이번 라운드에 probe 를 재실행하지 않았다 | `probe-summary.txt` 4행 | 3 |
| R1 | code-review · typescript | MEDIUM | 신규 강제 spec 이 CI spec 목록에 없다 | 흡수. `api.yml` 목록에 `scope-enforce`·`scope-handlers`·`scope-condition`·`organization-admin.scope` 4건 추가. 목록 전체를 로컬 jest 로 돌렸다: 32 suite 중 30 통과, 2 실패 (`treat-queue`·`treat-reservation` 의 실 Redis 시험, `MaxRetriesPerRequestError`). 이번 수정과 무관한 환경 실패로 보지만 Redis 를 띄우고 재실행하지 않았다 (미검증) | `.github/workflows/api.yml` 88–92행 | 2 |
| R1 | code-review · security | MEDIUM | I2 조직 라우트 (6개) 의 영업 판정 시험이 없다 | 부분 흡수. `organization-admin.scope.spec.ts` 6건 (담당 밖 목록 거부 · 담당 양성 · 담당 없음 403 · 담당 밖 상세·수정·이동 거부 · 양성 · OFF 불변). 생성 · 삭제 · 상세 라우트 개별 음성은 이연 | 신규 spec 6/6 녹색 | 2 |
| R1 | code-review | MEDIUM | 「목록·count·집계·내보내기가 같은 절」 시험은 같은 함수를 두 번 불러 비교할 뿐이다 | 흡수 (이름을 실제 검증 범위로 고쳤다. 집계 · 내보내기 시험은 아직 없다) | `scope-handlers.spec.ts` 「범위 절 생성기는 같은 입력에 같은 절 목록을 돌려준다」 | 7 |
| R1 | code-review | MEDIUM | 경합 itspec 이 서비스 경로를 타지 않고 저장소 함수를 직접 부른다 | 이연 (백로그 MEDIUM 행). 서비스 배선을 지나는 격리 시험이 필요하다 | `agency-fc.race.itspec.ts` 40행, `organization.race.itspec.ts` 62–86행 | 6 |
| R1 | code-review | MEDIUM | drizzle 절 어댑터와 `withScope` 의 실제 SQL 을 어떤 시험도 만들지 않는다 | 부분 흡수. `scope-condition.spec.ts` 가 eq · prefix · AND 렌더를 고정한다 (`sag_id = ?`, `org_path like ?` 와 `%` 값). `withScope` (구독 실패 저장소) 의 SQL 시험은 이연 | 신규 spec 5/5 녹색 | 6 |
| R1 | typescript | MEDIUM | 빈 절 목록은 조건 없음 (전건 조회) 이 되고, 접두 값은 와일드카드를 그대로 LIKE 로 넘긴다 | 흡수. 빈 절 목록은 던지고, 접두 값은 `ORG_PATH_RE` 를 통과해야 한다. 반사실: 빈 절 가드를 `return undefined` 로 바꾸면 붉음 1건, 접두 검사를 빼면 음성 시험이 붉음 1건 (실측), 되돌리면 녹색 | `scope-condition.ts`, `scope-condition.spec.ts` | 1 |
| R1 | typescript | MEDIUM | 범위 인자가 선택형이라 빼먹으면 조용히 전건 조회가 된다 | 흡수. 시그니처를 `readonly ScopeClause[] \| null` 로 필수화했다 (`null` = 강제 OFF 또는 영업 밖 신원, 명시). 호출 4곳과 컨트롤러 2곳을 바꿨다. tsc 는 기준선 23건과 같다 | `tsc-after-r1.txt` (기준선 대비 같은 파일 · 같은 개수) | 1 |
| R1 | security | MEDIUM | 소속 지정이 범위 판정 전에 임의 회원 행을 FOR UPDATE 로 잠근다 (memIds 상한 없음) | 부분 흡수. 잠금 전에 범위 밖 여부를 먼저 본다 (범위 밖이 섞이면 트랜잭션을 열지 않는다). 상한 (`ArrayMaxSize`) 은 OFF 응답을 바꾸고 상한 값이 사용자 결정이라 이연 (백로그 MEDIUM 행) | 「사전 판정」 시험, `agency-fc.dto.ts` 249–271행 | 1 |
| R1 | security | LOW | 구독 엑셀 내보내기가 범위 절을 받지 않는다 | 흡수 (한계를 주석으로 명시. 라우트가 admin 전용이라 지금은 도달하지 못하며, 역할 가드를 넓히면 함께 붙여야 한다) | `subscribe-failed.service.ts` `downloadSubscribeList` 위 주석 | 1 |
| R1 | code-review | LOW | 음성 시험의 목 판정이 인자와 무관하게 0 을 돌려준다 | 흡수. 목을 sagId 조건부로 바꿨다. 인자를 빼면 양성 시험이 붉다 | `scope-enforce.spec.ts` 「음성」 | 8 |
| R1 | code-review | LOW | 영업 신원의 sagId 가 없으면 500 이 된다 | 흡수. 목록 두 컨트롤러가 403 으로 감싼다 (ON 에서만) | `scope-handlers.spec.ts` 「담당 영업 키가 없는」 | 1 |
| R1 | code-review | LOW | `subtreeOrgIds` JSDoc 이 `getAgencySagId` 위에 붙어 있었다 | 흡수 | `organization.repository.ts` 주석 위치 | — |
| R1 | code-review · security | LOW | LIKE 는 ci 콜레이션에서 대소문자를 무시하고 JS 판정은 구분한다 | 이연 (LOW). 접두 값은 신원에 저장된 orgPath 이고 `ORG_PATH_RE` 를 통과한 값뿐이다 (ULID 대문자). 콜레이션 BINARY 전환은 SQL · 인덱스 변경이라 별도 검증이 필요하다 | `scope-condition.ts`, `scope-monitor.ts` `ORG_PATH_RE` | 1 |
| R1 | code-review | LOW | 변경 파일 다수가 prettier --check 에 걸린다 | 부분 흡수. 신규 파일 7개와 원래 정렬된 파일 6개를 정렬했다. 원래 정렬이 안 된 5개 (`agency-admin.controller.ts`·`agency-admin.service.ts`·`agency.repository.ts`·`organization.repository.ts`·`organization.service.ts`) 는 diff 범위를 지키려 건드리지 않았다 | `prettier --check` | — |
| R1 | security | LOW | race itspec 이 root 로 DROP · CREATE DATABASE 를 실행하고 대상이 격리 DB 인지 확인하지 않는다 | 이연 (LOW). 포트 오설정 시 공유 DB 에 DDL 이 나갈 수 있다. 게이트 명령 변경이 필요해 계획 개정 때 함께 처리 | `organization.race.itspec.ts`·`agency-fc.race.itspec.ts` 접속 블록 | 1 |
| R1 | security | LOW | 응답 불변 probe 가 휘발 키를 넓게 빼고 이진 본문은 크기만 본다 | 이연. 기존 백로그 HIGH 행 (검사 #10 해시 규칙) 과 같은 계열이다 | `probe.mjs` 46–72행 | 8 |
| R1 | security | LOW | M6 이 OFF 로 커밋되면 영업의 교차 테넌트 쓰기가 열려 있다. orgPermissionUpload 가 저장소를 직접 부른다 | 기각. 정책 §4 의 의도된 OFF 이고 ON 체크리스트가 이미 있다. orgPermissionUpload 는 라우트가 `@Roles('admin', 'corp')` 라 영업이 도달하지 못한다 (`agency-fc-admin.controller.ts` 386행 근처). 편차 목록에는 없지만 영업 우회는 아니다 | 정책 §4, 라우트 역할 | — |
| R1 | typescript | MEDIUM | 지적문 끝이 잘려 있다 (OFF 시험 항목의 I3 부분이 끊김) | 부분 흡수. 보이는 부분 (I4 OFF 시험) 을 추가했다. I2 · I3 OFF 시험은 기존 백로그 MEDIUM 행 「OFF 경로 저장소 인자 불변」 과 같다 | `scope-handlers.spec.ts` 「OFF: 구독 실패 목록」, `organization-admin.scope.spec.ts` OFF 1건 | 2 |
| R2 | code-review | MEDIUM | 하위 추가가 이동 잠금을 우회할 수 있다. create 가 부모를 잠금 없이 읽어 옛 경로로 자식을 넣는다 (추론, 실행 재현 전) | 부분 흡수. 강제 ON 경로는 흡수: 부모 행을 FOR UPDATE 로 잠근 뒤 다시 읽어 경로를 계산한다 (`lock` 인자, moveSubtree 와 같은 규약). 강제 OFF 경로는 잠금이 없어 같은 경합이 남는다 (정책 §4 의 OFF 불변). 백로그 MEDIUM 행 「하위 추가 · OFF 잠금」 으로 옮김 | `organization.race.itspec.ts` 「하위 추가 경합」: 수정 전 `done` (기대 `blocked`) 으로 붉음 (실측), 수정 뒤 4/4 녹색 (격리 `m6-iso-mysql` 8.0.42). `organization.repository.ts` create 의 lock 분기, `organization.service.ts` create 가 `SCOPE_ENFORCE_SALES_CORP` 를 넘긴다 | 1 |
| R2 | code-review | LOW | 기업 번호 · 담당 sagId 가 숫자가 아니면 NaN 이 SQL 로 나가 500 이 된다 (조직 라우트 ON 경로, 소속 쓰기 ON 경로 같은 형태) | 흡수. 숫자가 아닌 값은 저장소를 부르기 전에 403. 소속 쓰기 쪽도 같은 검사 | `organization-admin.scope.spec.ts` 「기업 번호가 숫자가 아니면 SQL 전에 403」, `scope-enforce.spec.ts` 「숫자가 아닌 sagId」. 검사 줄을 빼면 각각 붉음 1건 (반사실 실측), 되돌리면 녹색 | 1 |
| R2 | code-review | LOW | assignOrg · setOrgManager 의 admin 인자가 선택형이라 빠지면 판정이 전부 건너뛰어진다 (fail-open) | 흡수. admin 을 필수 인자로 두고, 신원 (role) 이 없으면 판정 없이 통과하지 않고 403. 현행 호출처 2곳은 모두 `req['admin']` 을 넘긴다 | `scope-enforce.spec.ts` 「신원 없음」. 가드 줄을 빼면 붉음 1건 (반사실 실측), 되돌리면 녹색. `tsc` 는 기준선 23건과 같은 파일 · 같은 개수 | 1 |
| R2 | security | MEDIUM | 소속 지정 · 조직관리자 지정은 corp 검사를 잠금 뒤에야 한다. 존재하지 않는 id 를 실으면 gap lock 이 걸린다 (ON 경로) | 흡수. 잠금 전 사전 판정에 corp 검사를 더했다 (잠금 없는 읽기). 범위 밖 id 가 하나라도 있으면 트랜잭션을 열지 않는다. 판정 대상 id 수를 셀 때 없는 id 는 0 으로 세어 같이 막힌다. `ArrayMaxSize` 상한은 R1 백로그 행 그대로 | `scope-enforce.spec.ts` 「사전 판정 (corp)」: 사전 판정 줄을 빼면 붉음 1건 (반사실 실측), 되돌리면 녹색. 트랜잭션 · 잠금 호출 0회를 본다 | 1 |
| R2 | typescript | MEDIUM | 잠금 뒤 재판정이 던진 404 · 400 을 서비스 catch 의 mysqlErrorWrapper 가 500 (code 없음) 으로 바꾼다 | 흡수. 서비스 move 의 catch 가 HttpException 을 그대로 낸다 (mysqlErrorWrapper 앞). 저장소 시험은 `rejects.toBeInstanceOf(BadRequestException)` 로 좁혔다 | `organization.service.move.spec.ts` (신규, CI 목록에 추가): 분기를 빼면 500 으로 붉음 (반사실 실측: `expected BadRequestException, received InternalServerErrorException`), 되돌리면 400 녹색. `organization.race.itspec.ts` 「낡은 스냅샷 순환」 은 `BadRequestException` 으로 좁힘 | 2 |
| R2 | typescript | LOW | 신규 `assertSalesOrg` 가 `as any` 를 복제하고, 판정에 상세 조회 (계약 조인 포함) 를 쓴다 | 흡수. `as any` 제거. 조직 행만 읽는 `getAgcId` 를 서비스에 더해 영업 판정에 쓴다 (핸들러 본문의 상세 조회는 그대로) | `organization-admin.scope.spec.ts` 「상세 · 수정 · 이동 판정은 조직 행만 읽는다」. 옛 경로 (`getDetail`) 로 바꾸면 붉음 1건 (반사실 실측), 현행이면 녹색 | — |
| R2 | typescript | LOW | `SCOPE_COLUMN` 이 M6 변경 뒤 죽은 export 가 됐다 (열 이름 정본이 둘이 된다) | 흡수 (삭제). 사용처 0 을 grep 으로 확인한 뒤 지웠다 (정의 한 줄 외 참조 없음). `ScopeKey` 타입은 남긴다 | `apps/api/src` · `libs` grep 에서 `SCOPE_COLUMN` 0건. `tsc` 기준선 23건과 같은 파일 · 같은 개수 | — |

판정 집계 (R1, 표 26행): 흡수 10 · 부분 흡수 6 · 기각 1 · 이연 9. HIGH 흡수 2 (조직 이동 · 소속 지정 풀). HIGH 미해소 3 (측정 산출 순서 · 응답 불변 증거 · 단건 연결, 모두 백로그 HIGH 행). CRITICAL 0. 지적 26건은 중복을 합친 뒤 세었다 (원문 지적은 셋 · 둘 전부 같은 항목을 가리키는 것이 많다).

### 검사 재실행 (R1 뒤, 커밋 없음)

- 개별 실행 (게이트 본문을 검사 블록별로 잘라 돌렸다): `#4` · `#6` · `#7` · `#11` · `#12` · `#14` · `#15` 통과.
- `#8`: 시험 파일 2 (jest 15건 — 실패 0) 와 격리 경합 시험 5건 (`organization.race` 3 · `agency-fc.race` 2) 은 녹색이다. 게이트 본문의 마지막 호출 검사 (money · analyze 컨트롤러) 는 편차 C 로 붉다. 이번 라운드에서 바뀌지 않았다.
- `#5` · `#9` · `#10` · `#13`: 이번 라운드에서 다시 돌리지 않았다. `#5` 는 측정 산출 부재, `#9` 는 편차 E, `#10` 은 위 응답 불변 행, `#13` 은 같은 날짜 줄이 BAD 식에 걸린다 (기존 원인).
- `tsc --noEmit`: 23건, 기준선 `tsc-baseline.txt` 와 같은 파일 · 같은 개수 (`tsc-after-r1.txt`).
- `prettier --check`: 정렬한 13개는 통과.
- 격리 컨테이너 `m6-iso-mysql` (mysql 8.0.42, 127.0.0.1:13320) 은 이 라운드의 실행이 만들었고, 끝나면 그 이름만 내린다.

### 미검증 (이번 라운드)

- 응답 불변 probe 재실행 (격리 스택, 편차 I 와 같음).
- Redis 가 필요한 `treat-queue` · `treat-reservation` 시험 (실 Redis 미기동).
- `withScope` (구독 실패 저장소) 와 집계 · 내보내기 경로의 SQL 시험.

## 최종 리뷰

최종 리뷰 라운드 1/3 (fable · codex, 2026-10-20). 리뷰 지적 11건 (fable 5 · codex 6). 판정은 흡수 · 이연 · 기각 셋이다. 코드는 바꾸지 않았다 (서버 서브모듈 변경 없음). 흡수는 기록 정정이다. 판정 근거가 된 게이트 실행은 아래 「최종 리뷰 뒤 검사」 에 적었다.

### 달성 판정 (정본)

- M6 은 PRD Outcome 을 달성하지 못했다. 측정 산출 (검사 #5), 핸들러 호출 (검사 #8), 측정 sha 조상 (검사 #9), 응답 불변 (검사 #10), 형식 (검사 #13) 이 붉다. 이 판정은 이 절이 정본이다.
- PRD 148행 상태는 `pending` 그대로다 (확인함). M6 을 완료로 표시하지 않는다. M9 영역 2 활성화의 입력으로 쓰지 않는다 (강제 상수는 OFF 로 남는다).
- 다음 계획의 입력은 백로그 `2026-10-20` 행 중 `최종 R1` 표시가 붙은 것과, 아래 표가 「이연 (기존 행)」 으로 가리키는 행이다.

### 최종 리뷰 지적 판정표

| 라운드 | 리뷰어 | 심각도 | 지적 | 판정 | 증거 | 층위 |
|---|---|---|---|---|---|---|
| 최종 R1 | fable | HIGH | PRD M6 Outcome 미달성. 게이트 4개가 붉은 채 커밋됐고 완료로 올리면 안 된다 | 흡수 (달성 아님 판정을 이 절에 고정, PRD 148행 pending 유지 확인) | 「검사 결과 요약」 #5 · #8 · #9 · #10 · #13 붉음, PRD 148행 `pending` | 2 |
| 최종 R1 | fable | MEDIUM | PRD Outcome 「신규 엔드포인트 범위 선언 강제 (M2 단언 활성화)」 가 계획 · 노트 · NOT Building 어디에도 없다 | 이연 (백로그 2026-10-20 MEDIUM 행, 다음 계획 반영) | PRD 148행, `access-contract.assert.ts` 에 scope 참조 0건, `access-contract.registry.ts:31-32` 는 scope 열을 가진다 | 2 |
| 최종 R1 | fable | MEDIUM | 검사 #8 의 여섯 컨트롤러 호출 루프 (`gates.sh:191-193`) 는 I3 판정이 서비스에 있어 편차 C 를 닫아도 붉다 | 이연 (백로그 2026-10-20 MEDIUM 행. 게이트 본문 개정은 계획 변경이라 이 판에서 손대지 않는다) | `gates.sh:191-193`, `agency-fc-admin.controller.ts` 의 범위 함수 호출 0건 (grep), I3 판정은 `fc-agency.service.ts` 의 `assertMembersWritableBySales` | 5 |
| 최종 R1 | fable | LOW | 구현 보고서가 커밋 전 상태 문장 (커밋 없음 · 63/63 · 경합 3/3) 을 싣는다 | 흡수 (보고서 머리에 커밋 상태와 정본 위치를 정정) | `.claude/PRPs/reports/admin-rbac-m6-report.md` 6행 | — |
| 최종 R1 | fable | LOW | 「검사 결과」 뒤 노트 180행이 정렬 안 된 5개 파일을 건드리지 않았다고 적었으나 `0bf113aa` 에 형식 재줄바꿈이 남아 있다 | 흡수 (정정 기록. 로컬 이력은 다시 쓰지 않는다) | `git diff --numstat` 공백 무시 전후 `organization.repository.ts` 116/33 → 103/20, `organization.service.ts` 는 줄 분리 hunk (`-190` · `-240`) 가 공백 무시에도 남는다 | 1 |
| 최종 R1 | codex | HIGH | 머니 · 분석 관리자 라우트가 범위 밖 기록을 막지 않는다. `money-admin.controller.ts:125` 목록은 범위 인자가 없고 `analyze-admin.controller.ts:101` 은 기업 신원만 본다 | 이연 (백로그 2026-10-20 HIGH 행, 다음 계획 반영). 단건 판정 행 (1611) 과 별개인 목록 · 내보내기 행을 추가했다. 상수 OFF 라 현행 노출은 M6 이전과 같다 | `money-admin.controller.ts:125`, `analyze-admin.controller.ts:101`, 편차 C | 1 |
| 최종 R1 | codex | HIGH | 소속 · 조직관리자 지정 (`setOrgManager`) 이 조직 행을 잠그지 않고 경로를 읽는다. 조직 이동과 경합하면 판정 뒤 대상이 부분트리 밖으로 나간다 | 이연 (백로그 2026-10-20 HIGH 행, 1609 · 1610 과 같은 계열) | `fc-agency.service.ts:736` (잠금은 `lockMembersForOrgAssign` 의 회원 행뿐) | 1 |
| 최종 R1 | codex | HIGH | 조직 라우트 쓰기 (생성 · 수정 · 이동 · 삭제) 가 영업 판정 (`assertSalesOrg`) 을 쓰기 트랜잭션 밖에서 하고, 쓰기 쪽은 담당 영업 (sagId) 을 잠그거나 다시 보지 않는다 | 이연 (백로그 2026-10-20 HIGH 행, 1610 과 같은 계열의 조직 라우트 쪽) | `organization-admin.controller.ts` 의 `assertSalesOrg` 호출 136 · 153 · 174 · 194행 | 1 |
| 최종 R1 | codex | HIGH | 측정 산출 (Task 3 · 4) 없이 강제 경로를 구현했다 | 이연 (기존 백로그 HIGH 행 1608 · 1613, 편차 A · B. 새 행 없음) | `test -f synthetic-tenants.sql` 이 검사 #5 에서 멈춘다 (아래 「최종 리뷰 뒤 검사」) | 2 |
| 최종 R1 | codex | MEDIUM | 응답 불변 증거가 최종 커밋을 검증하지 않는다. `probe-summary.txt` 의 after_sha 는 착수 sha 8자이고, 행 파일은 착수 대 워킹트리 22행, 착수 대 재실행 40행이 다르다 | 흡수 (최종 커밋의 응답 불변은 미검증으로 고정) + 이연 (백로그 2026-10-20 MEDIUM 행, 기존 1615 · 편차 I 와 같은 계열). 증거 파일은 고치지 않는다 | `probe-summary.txt:6`, `diff rows-before.txt rows-after.txt` 22행, `diff rows-before.txt rows-before-rerun.txt` 40행 | 3 |
| 최종 R1 | codex | MEDIUM | 검사 #9 의 ON9 줄 (`gates.sh:201`) 은 무일치 때 파이프라인 rc 1 로 멈추고, 검사 #10 의 `grep -q "cmp -s" isolated-probe.sh` (`gates.sh:252`) 는 스크립트에 cmp 가 없어 항상 붉다 | 이연 (ON9 는 기존 1612, `gates.sh:252` 는 백로그 2026-10-20 MEDIUM 행 신규). 게이트 본문 수정은 계획 변경이라 이 판에서 손대지 않는다 | `gates.sh:201` (실측 `ON9=$(… \| wc -l)` 의 대입 rc 1), `gates.sh:252`, `isolated-probe.sh` 의 cmp 0건 | 4 · 5 |
| 최종 R2 | fable | MEDIUM | 이행표 I2 · I3 행이 구현된 영업 소유 관계 (`sagId` · `tn_agency.sag_id`) 를 빠뜨린다. 범위 키가 `agcId + orgPath` 뿐이라 M9 입력이 조직 · 회원 자원의 영업 조인을 놓친다 | 흡수 (I2 6행 · I3 2행의 범위 키에 `sagId`, 조인에 `tn_agency.sag_id` (agc_id 경유) 를 더하고 정정 줄을 적었다). 검사 #4 · #6 은 파일 · 핸들러 · AW 행 수만 세므로 이 불일치는 게이트가 잡지 못했다 (실측 확인) | `scope-transition-table.md` 14–21행 (정정 줄 포함), `organization-admin.controller.ts:72–79` (`assertSalesAgency`), `agency-fc.repository.ts` `countMembersInSalesScope` (`tn_agency.sag_id`) | 2 |
| 최종 R2 | fable | LOW | 강제 ON 하위 추가의 잠금 재읽기 404 가 `mysqlErrorWrapper` 를 거쳐 500 (code null) 이 된다. R2 의 move 수정은 형제 호출처인 create 에 적용되지 않았다 | 흡수 (create catch 에 `HttpException` 분기를 move 와 같은 자리에 두었다. 커밋 `53a3e865`). 반사실: 분기를 뺀 사본에서 spec 이 붉다 (`InternalServerErrorException` 실측), 되돌리면 녹색. 인가 우회 아님 | `organization.service.ts:106–107`, `organization.repository.ts:398` (잠금 재읽기 404), `organization.service.create.spec.ts` | 6 |
| 최종 R2 | codex | MEDIUM | 위와 같은 결함 (create 의 잠금 뒤 404 가 500 으로 바뀐다). 심각도는 리뷰어가 갈렸다 (fable LOW · codex MEDIUM) | 흡수 (위 fable LOW 와 같은 수정으로 닫는다). 심각도는 codex 판정을 따랐다: ON 경로에서 응답 상태가 틀리고 move 에서 같은 분기를 이미 고쳤으므로 형제 호출처도 같은 수준으로 본다. 두 행은 한 수정의 두 판정이다 | 같음 | 6 |
| 최종 R2 | fable | LOW | `prior-specs-baseline.txt` 에 `base-sha` 헤더가 없어 검사 #11 의 「착수 기준선과 같다」 를 확인할 수 없다 (검사 #2 는 헤더 있는 두 파일만 대조) | 이연 (검사 개정은 다음 계획 몫). 실질 위험은 작다: 기준 spec 20개 가운데 M6 이 바꾼 모듈을 import 하는 것은 `access-control.spec.ts` 1개다 (리뷰어 실측). 백로그 행은 만들지 않았다 (LOW). 다음 계획이 이 파일을 기준선으로 쓰기 전에 헤더와 대조 검사를 추가한다 | `prior-specs-baseline.txt:1`, `admin-rbac-m6.gates.sh` 검사 #11 블록 | 7 |
| 최종 R2 | fable | LOW | R1 기각 근거 (240행) 의 「게이트 개정 금지」 가 편차 F (검사 #8 명령 변경, 180행 근처) 와 어긋난다. 구현 영수증의 `plan_hash` 는 계획서 `.md` 만 덮어 gates 변경이 receipt 체인에 안 보인다 | 흡수 (기록 정정: 240행에 R2 정정 문장을 덧붙였다. 편차 F 는 구현 커밋에서 이미 공시됐다). 이연: gates 본문의 digest 고정은 다음 계획이 한다. 이 판에서 digest 를 만들지 않았다 | `admin-rbac-m6.gates.sh` 180행 (편차 F), `.claude/receipts/mccp-implement-codex/admin-rbac-m6.json` `plan_hash` | 2 |

판정 집계: 흡수 4 (fable 3 · codex 1, 그중 HIGH 1) · 이연 7 (fable 2 · codex 5) · 기각 0. CRITICAL 0. HIGH 미해소 4 (codex 머니 · 분석 목록, 조직관리자 지정 잠금, 조직 라우트 판정 분리, 측정 산출 부재 · 모두 백로그 HIGH 행).

### 기각 근거

- 없음. 11건 모두 실측 또는 파일 인용으로 확인했다. 이연 항목은 계획 본문이나 게이트 본문을 바꿔야 하므로 다음 계획이 맡는다 (계획서 동결, 게이트 개정 금지). R2 정정: 「게이트 개정 금지」 는 편차 F 외의 게이트 본문 개정에 대한 말이다. 편차 F 는 구현 커밋에서 이미 공시됐고, 나머지 게이트 개정은 다음 계획 몫이다.

### 최종 리뷰 뒤 검사

- 코드 변경이 없어 모듈 테스트와 타입 검사는 다시 돌리지 않았다 (대상 변경 없음).
- `bash .claude/PRPs/plans/admin-rbac-m6.gates.sh` 를 워킹트리 루트에서 실행했다. 검사 #1 · #2 · #3 · #4 를 지나 `줄 89 (test -f synthetic-tenants.sql)` 에서 멈췄다 (rc 1). `set -e` 라 뒤 검사는 돌지 않았다. 검사별 결과 표는 위 「검사 결과 요약」 이 정본이고, 이번 판에서는 바뀐 것이 없다.
- 검사 #9 의 ON9 줄 (`gates.sh:201`) 을 따로 확인했다. 무일치 `grep` 은 `wc -l` 이 0 을 내도 파이프라인이 rc 1 이라 `ON9=$(…)` 대입이 rc 1 이다.

### 이 판에서 고치지 않은 것 (사유)

- 검사 본문 (`gates.sh`) 은 계획의 Validation 이다. 계획서 · 게이트 본문은 동결이라 고치지 않았다. 결함은 백로그 2026-10-20 행으로 넘겼다.
- 머니 · 분석 강제 경로 (편차 C) 와 조직 라우트 · 조직관리자 지정의 잠금 · 판정 분리는 ON 전용 설계 변경이다. 한 함수만 고치면 같은 계열 호출처가 남아 판정을 한 곳에서 못 닫는다 (형제 호출처 원칙). 다음 계획에서 판정 위치를 함께 정한다.
- 측정 산출과 응답 불변 probe 재실행은 격리 스택이 필요하다. 이번 판에서 돌리지 않았다 (미검증).

### 최종 리뷰 R2 · 집계 · 검사

- 최종 리뷰 라운드 2/3 (fable · codex, 같은 날). 지적 5건 (fable 4 · codex 1). 중복 1건 (create 404 → 500) 을 한 수정으로 닫았다.
- 판정 집계: 흡수 3 (scope 이행표 · create 잠금 404 · 기록 정정. create 는 리뷰어 2명이 같은 항목을 올려 2행) · 이연 1 (prior-specs 헤더, 다음 계획) · 기각 0. HIGH 0 · CRITICAL 0. HIGH 흡수 0 이므로 루프는 여기서 끝난다 (운용 정책 · 상한 3 의 종료 조건). R1 에서 넘긴 HIGH 미해소 4건은 백로그 `2026-10-20` HIGH 행이 그대로 맡는다.
- 게이트 구현 커밋 (`53a3e865`, 서버 서브모듈) 뒤 실행: `bash .claude/PRPs/plans/admin-rbac-m6.gates.sh` 는 R1 과 같은 지점 (줄 89, `test -f synthetic-tenants.sql`) 에서 멈춘다. 검사 #1–#4 통과, #5 붉음 (측정 산출 부재, 백로그 HIGH 1608 · 1613). #8 race itspec 은 격리 MySQL 이 필요해 이번 판에서 돌리지 않았다 (미검증).
- 모듈 검사: 조직 spec 3개 (`organization.service.create.spec.ts` · `organization.service.move.spec.ts` · `organization-admin.scope.spec.ts`) 10건 녹색. 타입 검사 `npx tsc --noEmit` 오류 23건 = 기준선 23 (신규 0). 새 spec 파일은 prettier 통과.
- 이 판에서 고치지 않은 것: 검사 #5 (측정 산출) · #8 (여섯 핸들러 호출) · #10 (응답 불변 probe) 은 R1 에서 넘긴 대로다. prior-specs 헤더와 gates digest 는 게이트 개정이라 다음 계획에서 한다.
