set -euo pipefail
WT="$(git rev-parse --show-toplevel)"
cd "$WT"
S=insuniverse-server
PLAN=.claude/PRPs/plans/admin-rbac-m2.plan.md
M1=.claude/meta/2026-10-19-admin-rbac-m1
META=.claude/meta/2026-10-19-admin-rbac-m2
NOTE=.claude/notes/admin-rbac-m2.md
BACKLOG=.claude/plans/codex-findings-backlog.md
ACD=$S/apps/api/src/modules/auth/access-contract
REG=$ACD/access-contract.registry.ts
EXC=$ACD/access-contract.exceptions.ts
SCR="$(git rev-parse --git-dir)/m2-gates"
mkdir -p "$SCR"
BASE=7287e38
PRE=03ce7146964b404c95303499bd9f1e4066ccfb81
HOST_A="$(printf '/%s/' home)"
HOST_B="$(printf '/%s/' root)"
HOST_C="$(printf '/%s/' Users)"
HOST_D="$(printf '/tmp/%s' claude)"
HOST_PAT=(-e "$HOST_A" -e "$HOST_B" -e "$HOST_C" -e "$HOST_D")
JG='AnalysisTargetGuard|PushDetailReaderGuard'
SW_RE='isDevAuthBypassEnabled|isLocalRequest|isDevSwaggerRequest'
SPECS="apps/api/src/pino.spec.ts apps/api/src/modules/alimtalk/alimtalk-reminder.spec.ts apps/api/src/modules/alimtalk/alimtalk-reminder-attempt.spec.ts apps/api/src/modules/alimtalk/alimtalk.repository.spec.ts apps/api/src/modules/loss-analysis/loss-analysis.repository.spec.ts apps/api/src/modules/loss-analysis/loss-analysis.service.spec.ts apps/api/src/modules/loss-analysis/loss-analysis.case-matrix.spec.ts apps/api/src/modules/treat/dispatch/treat-queue.service.spec.ts apps/api/src/modules/treat/dispatch/treat-reservation.service.spec.ts libs/inhouse/src/inhouse.service.spec.ts apps/api/src/modules/auth/admin/admin-roles-access.spec.ts apps/api/src/modules/auth/user/dev-auth-bypass.spec.ts apps/api/src/modules/auth/access-contract/access-contract.spec.ts"
GEN() { node "$META/build-registry.mjs" "$1" "$2" "$M1/route-contract-table.md" "$M1/annotations.json" "${@:3}"; }
DUMP() { (cd "$WT/$S" && TS_NODE_TRANSPILE_ONLY=1 TS_NODE_PROJECT=apps/api/tsconfig.app.json SERVER_ROOT="$PWD" node -r ts-node/register -r tsconfig-paths/register "$WT/$M1/route-runtime-dump.cjs" 2>/dev/null); }
EXPECT_RC() { set +e; "${@:2}" > /dev/null 2>&1; r=$?; set -e; test "$r" = "$1"; }

# 검사 #1: 착수 sha · 선행 병합 조상 · 트리 · prod 고정의 음성
# 기대: 착수 sha 의 트리가 b2acda28, 두 선행 병합이 착수 sha 의 조상, admin · site HEAD 가 착수 sha 와 같다
# 음성: prod 고정 5a9a7106 은 객체가 있고 착수 sha 의 조상이 아니다 (계획 시점 실행, rc 1 이 음성 관찰)
test "$(git -C $S rev-parse $PRE^{tree})" = b2acda288312268c190613318176c265f1b59db7
git -C $S merge-base --is-ancestor a3016d3515d17a9fbc8dcbc9588ee4ebc91a8e09 $PRE
git -C $S merge-base --is-ancestor aff7ffcc146c37cbb974330f43388784101692a4 $PRE
git -C $S merge-base --is-ancestor $PRE HEAD
test "$(git -C insuniverse-admin rev-parse HEAD)" = 146ed4d662fb76edab1112833f4d980bdf20176f
test "$(git -C insuniverse-site rev-parse HEAD)" = b4200ff2e630c098b16dc5ecc2430374b10c093b
git -C $S cat-file -e 5a9a7106^{commit}
EXPECT_RC 1 git -C $S merge-base --is-ancestor 5a9a7106 $PRE

# 검사 #2: 레지스트리 재현 · 키 집합 · Express 스택 · 행별 가드 집합 (런타임 덤프 기준)
# 기대: 생성기 출력이 커밋 파일과 cmp 0. gate-compare.mjs 가 exit 0 (레지스트리 키 = 덤프 handlers 키 = 덤프 express 키(경로에 * 없음), 행마다 guards 가 덤프 런타임 guards 와 같음, 행 수 = handlers 수, 중복 0)
# 음성: 첫 행을 뺀 사본과 SuperAdminGuard 를 AdminJwtAuthGuard 로 바꾼 사본에서 gate-compare.mjs 가 exit 1 이다 (아래 두 줄이 실행한다)
DUMP > "$SCR/dump.json"
GEN "$S" "$M1/freeze-03ce7146.list.txt" > "$SCR/reg.ts"
cmp "$SCR/reg.ts" "$REG"
node "$META/gate-compare.mjs" "$SCR/dump.json" "$REG"
sed '0,/^  { method:/{/^  { method:/d}' "$REG" > "$SCR/reg-drop.ts"
sed "0,/'SuperAdminGuard'/s//'AdminJwtAuthGuard'/" "$REG" > "$SCR/reg-guard.ts"
EXPECT_RC 1 node "$META/gate-compare.mjs" "$SCR/dump.json" "$SCR/reg-drop.ts"
EXPECT_RC 1 node "$META/gate-compare.mjs" "$SCR/dump.json" "$SCR/reg-guard.ts"

# 검사 #3: 여섯 범주 — 범주 값이 PRD 여섯 범주 안에 있고, 범주 칸 수가 행 수와 같다
# 기대: 범주 값 가운데 categories.txt 밖의 값이 0. 범주 칸 수 = 행 수
# 음성: 첫 「서비스 검증」 범주 칸을 「기타」 로 바꾼 사본에서 밖의 값이 1 이상이다 (아래 두 줄이 실행한다)
CAT_OUT() { grep -o "category: '[^']*'" "$1" | sed "s/^category: '\(.*\)'$/\1/" | sort -u | grep -vxF -f "$META/categories.txt" || true; }
test -z "$(CAT_OUT "$REG")"
test "$(grep -c "category: '" "$REG" || true)" = "$(grep -c '^  { method:' "$REG" || true)"
sed "0,/category: '서비스 검증'/s//category: '기타'/" "$REG" > "$SCR/reg-cat.ts"
test -n "$(CAT_OUT "$SCR/reg-cat.ts")"

# 검사 #4: 가드 명시 목록 · 판정 가드 행 — 목록 밖 가드 이름은 exit 1, 판정 가드 행은 전부 서비스 검증이고 수가 동결본과 같다
# 기대: --check-guards exit 0. 판정 가드 줄 수(동결본)가 1 이상이고 레지스트리에서 그 행 수와 같다. 그 행의 범주는 전부 서비스 검증
# 음성: 동결본 사본의 첫 RolesGuard 를 MysteryGuard 로 바꾸면 --check-guards 가 exit 1 이다 (아래 두 줄이 실행한다)
GEN "$S" "$M1/freeze-03ce7146.list.txt" --check-guards > /dev/null
N_FROZEN="$(grep -cE "$JG" "$M1/freeze-03ce7146.list.txt" || true)"
test "$N_FROZEN" -ge 1
test "$(grep -cE "guards: \[[^]]*($JG)" "$REG" || true)" = "$N_FROZEN"
test "$(grep -E "guards: \[[^]]*($JG)" "$REG" | grep -vc "category: '서비스 검증'" || true)" = 0
sed '0,/RolesGuard/s//MysteryGuard/' "$M1/freeze-03ce7146.list.txt" > "$SCR/freeze-mystery.txt"
EXPECT_RC 1 GEN "$S" "$SCR/freeze-mystery.txt" --check-guards

# 검사 #5: 개발 우회 칸 — 자리표시 0건, 스위치 함수 정의 3개, 「있음」 행 수가 소스 grep 기대치와 같고, 스위치 제거 사본에서 생성기 출력이 바뀐다
# 기대: 「env 미확인」 0. 스위치 정의 3. 「있음」 행 수 = 스위치를 부르는 신원 가드 클래스를 가진 행 수 (독립 grep 으로 센 클래스 집합 기준)
# 음성: 신원 가드 트리의 사본에서 스위치 이름을 지우고 생성기를 다시 돌리면 「있음」 행 수가 바뀐다 (아래 세 줄이 실행한다)
SWC() { local f; for f in $(grep -rlE "$SW_RE" "$1/apps/api/src/modules/auth" --include='*.guard.ts' || true); do grep -oE 'export class [A-Za-z]+' "$f" | awk '{print $3}'; done | sort -u | paste -sd'|' -; }
SW_CLS="$(SWC "$S")"
test -n "$SW_CLS"
test "$(grep -cE "export function ($SW_RE)\(" "$S/apps/api/src/helpers/request.helper.ts" || true)" = 3
test "$(grep -c 'env 미확인' "$REG" || true)" = 0
test "$(grep -cE "guards: \[[^]]*($SW_CLS)" "$REG" || true)" = "$(grep -c "devBypass: '있음'" "$REG" || true)"
rm -rf "$SCR/neg" && mkdir -p "$SCR/neg/apps/api/src/modules" && cp -r "$S/apps/api/src/modules/auth" "$SCR/neg/apps/api/src/modules/"
sed -i -E "s/($SW_RE)/XX/g" "$SCR/neg/apps/api/src/modules/auth/user/jwt.guard.ts"
GEN "$S" "$M1/freeze-03ce7146.list.txt" > "$SCR/reg-pos.ts"
GEN "$SCR/neg" "$M1/freeze-03ce7146.list.txt" > "$SCR/reg-neg.ts"
N0="$(grep -c "devBypass: '있음'" "$SCR/reg-pos.ts" || true)"
N1="$(grep -c "devBypass: '있음'" "$SCR/reg-neg.ts" || true)"
test "$N0" != "$N1"

# 검사 #6: 부팅 단언 spec — 판정 · 모드 · 음성 · 실제 라우터 · 실제 AppModule 시험이 녹색이고, 그 이름의 테스트가 실제로 통과했다
# 기대: jest 실패 0. 통과한 테스트 가운데 전체 이름에 「음성」, 「실제 라우터」, 「실제 AppModule」 이 각각 1개 이상
# 음성: 위반이 나야 통과하는 음성 케이스는 spec 안의 단언이다. 이 블록은 이름의 통과 여부를 JSON 으로 본다 (음성 사본 없음, 기록 아님)
rm -f "$SCR/jest6.json"
(cd "$S" && npx jest apps/api/src/modules/auth/access-contract --runInBand --forceExit --json --outputFile="$SCR/jest6.json") || true
node -e 'const r=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); const t=r.testResults.flatMap((f)=>f.assertionResults); const ok=(s)=>t.some((a)=>a.status==="passed"&&a.fullName.includes(s)); if(r.numFailedTests!==0||!ok("음성")||!ok("실제 라우터")||!ok("실제 AppModule")) process.exit(1)' "$SCR/jest6.json"

# 검사 #7: 호출 경로 — 부팅 단언 이름이 나오는 파일은 main.ts · 단언 모듈 · spec 셋이고, main.ts 의 호출 줄은 1이다
# 기대: 파일 집합이 기대 목록과 diff 0. main.ts 의 호출 줄 1
# 음성: 호출 줄을 뺀 main.ts 사본에서 호출 줄 수가 0 이다 (아래 두 줄이 실행한다)
printf '%s\n' apps/api/src/main.ts apps/api/src/modules/auth/access-contract/access-contract.assert.ts apps/api/src/modules/auth/access-contract/access-contract.spec.ts | sort > "$SCR/call-exp.txt"
grep -rl 'assertAccessContracts' "$S/apps/api/src" | sed "s#^$S/##" | sort > "$SCR/call-got.txt"
diff "$SCR/call-exp.txt" "$SCR/call-got.txt"
test "$(grep -c 'await assertAccessContracts(app);' "$S/apps/api/src/main.ts" || true)" = 1
sed '/await assertAccessContracts(app);/d' "$S/apps/api/src/main.ts" > "$SCR/main-neg.ts"
test "$(grep -c 'await assertAccessContracts(app);' "$SCR/main-neg.ts" || true)" = 0

# 검사 #8: 예외표 — golden 과 같고, 경로 항목은 판정표 경로 열에 있고, 문서 경로 이름은 swagger 설정과 @nestjs/swagger 가 만드는 경로와 같다
# 기대: 예외 항목 목록이 golden 과 diff 0. 경로 항목 전부가 판정표 경로 열에 있다. swagger.ts 의 설정 이름이 swagger 이고, 라이브러리가 /swagger-json · /swagger/swagger-ui-init.js 를 만드는 자리가 있다
# 음성: golden 에 없는 항목 /health/detail 을 더한 사본과 어긋난 사본에서 diff 가 0 이 아니다 (아래 두 줄이 실행한다)
grep -oE "\{ method: '[^']*', (path|prefix): '[^']*' \}" "$EXC" > "$SCR/exc.txt"
diff "$SCR/exc.txt" "$META/exceptions.golden.txt"
grep -oE "path: '[^']*'" "$SCR/exc.txt" | sed "s/path: '\(.*\)'/\1/" | { grep -vxF -e /swagger -e /swagger-json -e /swagger-yaml || true; } | while read -r p; do grep -qF "| $p |" "$M1/route-contract-table.md"; done
test "$(grep -cF "SwaggerModule.setup('swagger'" "$S/apps/api/src/swagger.ts" || true)" = 1
test "$(grep -cF '${finalPath}-json' "$S/node_modules/@nestjs/swagger/dist/swagger-module.js" || true)" -ge 1
test "$(grep -cF '${finalPath}/swagger-ui-init.js' "$S/node_modules/@nestjs/swagger/dist/swagger-module.js" || true)" -ge 1
cp "$META/exceptions.golden.txt" "$SCR/golden-plus.txt" && echo "{ method: 'GET', path: '/health/detail' }" >> "$SCR/golden-plus.txt"
! diff -q "$SCR/exc.txt" "$SCR/golden-plus.txt" > /dev/null
sed "s/prefix: '\/swagger\/'/prefix: '\/swagger'/" "$SCR/exc.txt" > "$SCR/exc-loose.txt"
! diff -q "$SCR/exc-loose.txt" "$META/exceptions.golden.txt" > /dev/null

# 검사 #9: IP 5 라우트 — 정정 라벨과 가드 부재
# 기대: 레지스트리의 allow-ip 행 수가 판정표의 allow-ip 행 수와 같고(1 이상), 같은 수의 정정 라벨 행이 있고, 컨트롤러에 가드 줄이 0
# 음성: 가드 주석을 푼 사본에서 가드 줄이 1 이상이다 (아래 두 줄이 실행한다)
A_REG="$(grep -c 'allow-ip-admin' "$REG" || true)"
A_TBL="$(grep -c 'allow-ip-admin' "$M1/route-contract-table.md" || true)"
test "$A_REG" -ge 1
test "$A_REG" = "$A_TBL"
test "$(grep -c '외부 호출자 후보 · 소비처 미확인 (R19)' "$REG" || true)" = "$A_REG"
GUARD_LINES() { grep -cE '^\s*@UseGuards' "$1" || true; }
test "$(GUARD_LINES "$S/apps/api/src/modules/allow-ip/allow-ip-admin.controller.ts")" = 0
sed -E 's#^(\s*)//\s*(@UseGuards)#\1\2#' "$S/apps/api/src/modules/allow-ip/allow-ip-admin.controller.ts" > "$SCR/allow-neg.ts"
test "$(GUARD_LINES "$SCR/allow-neg.ts")" -ge 1

# 검사 #10: 대조기 음성 보강 — 원본 rc 0, 역할 변이 rc 1, 빈 표 rc 2
# 기대: 원본 표 rc 0. charge-requests/process 행의 유효 @Roles 칸(9번째 열)을 바꾼 사본 rc 1. 빈 표 rc 2
# 음성: 이 검사 자체가 음성이다 (변이 사본이 rc 0 이면 붉다)
node "$M1/route-compare.mjs" "$M1/route-contract-table.md" "$SCR/dump.json" "$M1/freeze-03ce7146.list.txt" > /dev/null
awk -F'|' 'BEGIN{OFS="|"} /charge-requests\/process/ {$9=" admin, sales "} {print}' "$M1/route-contract-table.md" > "$SCR/t-role.md"
EXPECT_RC 1 node "$M1/route-compare.mjs" "$SCR/t-role.md" "$SCR/dump.json" "$M1/freeze-03ce7146.list.txt"
: > "$SCR/t-empty.md"
EXPECT_RC 2 node "$M1/route-compare.mjs" "$SCR/t-empty.md" "$SCR/dump.json" "$M1/freeze-03ce7146.list.txt"

# 검사 #11: 덤프 격리 — 중지 전에 등록이 있었고, 중지 뒤 남은 것이 0이다
# 기대: registered 합계 1 이상 (0 이면 이 검사는 빈 주장이다), leftAfterStop 0
# 음성: leftAfterStop 을 1 로 바꾼 덤프 사본에서 검사 식이 실패한다 (아래 두 줄이 실행한다)
SCHED_OK() { node -e 'const s=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).scheduler; const r=s.registered; if(!(r.intervals+r.timeouts+r.crons>=1 && s.leftAfterStop===0)) process.exit(1)' "$1"; }
SCHED_OK "$SCR/dump.json"
sed 's/"leftAfterStop": 0/"leftAfterStop": 1/' "$SCR/dump.json" > "$SCR/dump-neg.json"
EXPECT_RC 1 SCHED_OK "$SCR/dump-neg.json"

# 검사 #12: 범위 키 출처 — 저자 선언 0, 자동 출처 표지 행 수가 레지스트리 행 수와 같다
# 기대: scopeSource 가 author 인 행 0. auto 행 수가 행 수와 같다
# 음성: 첫 행의 scopeSource 를 author 로 바꾼 사본에서 author 행이 1 이다 (아래 두 줄이 실행한다)
AUTHOR_N() { grep -c "scopeSource: 'author'" "$1" || true; }
test "$(AUTHOR_N "$REG")" = 0
test "$(grep -c "scopeSource: 'auto'" "$REG" || true)" = "$(grep -c '^  { method:' "$REG" || true)"
sed "0,/scopeSource: 'auto'/s//scopeSource: 'author'/" "$REG" > "$SCR/reg-author.ts"
test "$(AUTHOR_N "$SCR/reg-author.ts")" = 1

# 검사 #13: CI 편입 · 현행 spec 녹색 — 13 파일이 api.yml 목록에 있고 같은 목록의 jest 가 exit 0
# 기대: 13 파일 모두 api.yml 에 있다. jest exit 0 (treat · dispatch 는 빈 포트 redis)
# 음성: 목록에서 admin-roles-access 를 뺀 사본에서 누락 목록이 비지 않다 (아래 두 줄이 실행한다)
CI_MISSING() { local f; for f in $SPECS; do grep -qF "$f" "$1" || echo "$f"; done; }
test -z "$(CI_MISSING "$S/.github/workflows/api.yml")"
grep -vF "admin-roles-access.spec.ts" "$S/.github/workflows/api.yml" > "$SCR/api-neg.yml"
test -n "$(CI_MISSING "$SCR/api-neg.yml")"
SPECS_NOAPP="$(echo $SPECS | tr ' ' '\n' | grep -vxF apps/api/src/modules/auth/access-contract/access-contract.spec.ts | paste -sd' ' -)"
(cd "$S" && npm test -- --runInBand $SPECS_NOAPP)
(cd "$S" && npm test -- --runInBand --forceExit apps/api/src/modules/auth/access-contract/access-contract.spec.ts)

# 검사 #16: 생성기 — 알 수 없는 플래그는 exit 2 · 판정만 행이 서비스 검증이 아니면 exit 1 · 판정만 수 = 서비스 검증 수 · 다중 신원 표지 수 = 두 선택 신원 가드를 함께 단 행 수
# 기대: 플래그 거부 rc 2. 판정만 수 ≥ 1 이고 판정만 중 서비스 검증 수와 같다. 레지스트리의 multiIdentity true 행 수가 두 선택 신원 가드를 함께 단 행 수와 같다
# 음성: 규칙 7 을 무조건 공개로 바꾼 생성기 사본에서 exit 1 이다 (아래 세 줄이 실행한다)
EXPECT_RC 2 GEN "$S" "$M1/freeze-03ce7146.list.txt" --check-categories
ST16="$(GEN "$S" "$M1/freeze-03ce7146.list.txt" --stats 2>&1 1>/dev/null)"
J16="$(echo "$ST16" | grep -oE 'judged_only=[0-9]+' | cut -d= -f2)"
JS16="$(echo "$ST16" | grep -oE 'judged_only_service=[0-9]+' | cut -d= -f2)"
test -n "$J16" && test "$J16" -ge 1 && test "$J16" = "$JS16"
test "$(grep -c 'multiIdentity: true' "$REG" || true)" = "$(grep -E "guards: \[[^]]*AdminOptionalGuard" "$REG" | grep -c "guards: \[[^]]*JwtOptionalGuard" || true)"
rm -rf "$SCR/gen-neg" && mkdir -p "$SCR/gen-neg" && cp "$META/build-registry.mjs" "$META/categories.txt" "$SCR/gen-neg/"
sed 's/"category": "서비스 검증", "contract": "판정만"/"category": "무조건 공개", "contract": "판정만"/' "$META/category-map.json" > "$SCR/gen-neg/category-map.json"
EXPECT_RC 1 node "$SCR/gen-neg/build-registry.mjs" "$S" "$M1/freeze-03ce7146.list.txt" "$M1/route-contract-table.md" "$M1/annotations.json"

# 검사 #14: 변경 집합 · 호스트 경로 — 기준 커밋 이후 부모 변경과 미추적 파일이 허용 집합 안, 계획 · 노트 · 메타 · 레지스트리 · 서버 서브모듈 diff 에 호스트 경로 0
# 기대: 허용 밖 0. 호스트 패턴 4종 0 (서브모듈 diff 는 기준 03ce7146 대비 커밋 · 미커밋 · 미추적 전부)
# 음성: 허용 밖 경로 한 줄을 넣은 목록에서 필터가 그 줄을 남긴다 (아래 세 줄이 실행한다)
ALLOW='^(\.claude/(PRPs/plans/admin-rbac-m2\.plan\.md|meta/2026-10-19-admin-rbac-m2/|notes/admin-rbac-m[12]\.md|plans/codex-findings-backlog\.md|meta/2026-10-19-admin-rbac-m1/route-runtime-dump\.cjs|PRPs/reports/admin-rbac-m1-report\.md|reviews/plan-review-admin-rbac-m2\.md|receipts/mccp-implement-codex/admin-rbac-m2\.json)|insuniverse-server/?$)'
( git diff --name-only "$BASE" --ignore-submodules=none; git ls-files --others --exclude-standard ) | sort -u > "$SCR/changed.txt"
set +e; grep -Ev "$ALLOW" "$SCR/changed.txt" > "$SCR/outside.txt"; set -e
test ! -s "$SCR/outside.txt"
printf '%s\n' insuniverse-admin/src/x.ts | grep -Ev "$ALLOW" > "$SCR/outside-neg.txt"
test -s "$SCR/outside-neg.txt"
test "$(grep -rcF "${HOST_PAT[@]}" "$PLAN" "$NOTE" "$META" "$REG" 2>/dev/null | awk -F: '{s+=$2} END{print s+0}')" = 0
test "$(git diff --no-ext-diff --cached | grep -cF "${HOST_PAT[@]}" || true)" = 0
test "$( (git -C $S diff --no-ext-diff "$PRE"; (cd "$S" && git ls-files --others --exclude-standard | xargs -r cat)) | grep -cF "${HOST_PAT[@]}" || true)" = 0

# 검사 #15: 이월 기록 — 처리한 줄마다 RESOLVED-IN 이 하나씩, 기준 커밋 이후 백로그 기존 줄은 삭제되지 않았다
# 기대: 백로그의 RESOLVED-IN 행 수가 계획 처리표의 RESOLVED 행 수와 같고, 기준 커밋 대비 삭제 줄 0
# 음성: 기존 줄 하나를 지운 사본의 diff 에 삭제 줄이 1 이상 생긴다 (아래 세 줄이 실행한다)
test "$(grep -c 'RESOLVED-IN | .claude/PRPs/plans/admin-rbac-m2.plan.md |' "$BACKLOG" || true)" = "$(grep -c '^| .* | RESOLVED | ' "$PLAN" || true)"
test "$(git diff --no-ext-diff "$BASE" -- "$BACKLOG" | grep -c '^-[^-]' || true)" = 0
RESOLVED_CHECKS="$(grep 'RESOLVED-IN | .claude/PRPs/plans/admin-rbac-m2.plan.md |' "$BACKLOG" | grep -oE '#[0-9]+' | sort -u)"
test -n "$RESOLVED_CHECKS"
for c in $RESOLVED_CHECKS; do grep -qE "^\| $c \| 통과 \|" "$NOTE"; done
git show "$BASE:$BACKLOG" > "$SCR/backlog-base.md"
sed '5d' "$SCR/backlog-base.md" > "$SCR/backlog-neg.md"
test "$(git diff --no-ext-diff --no-index --unified=0 "$SCR/backlog-base.md" "$SCR/backlog-neg.md" | grep -c '^-[^-]' || true)" -ge 1
