set -euo pipefail
WT="$(git rev-parse --show-toplevel)"
cd "$WT"
META=.claude/meta/2026-10-19-admin-rbac-m1
PLAN=.claude/PRPs/plans/admin-rbac-m1.plan.md
TABLE="$META/route-contract-table.md"
NOTE=.claude/notes/admin-rbac-m1.md
SCR="$(git rev-parse --git-dir)/m1-gates"
mkdir -p "$SCR"
PARENT="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"
SRC="$PARENT/.worktrees/security-structure-research/.claude/worktrees/refactor-prd/.claude/_meta/2026-10-06-refactor-duplication-bottleneck"
PIN_SRV=03ce7146964b404c95303499bd9f1e4066ccfb81
PIN_AFF=aff7ffcc146c37cbb974330f43388784101692a4
PIN_HOTFIX=a3016d3515d17a9fbc8dcbc9588ee4ebc91a8e09
PIN_ADM=146ed4d662fb76edab1112833f4d980bdf20176f
PIN_SITE=b4200ff2e630c098b16dc5ecc2430374b10c093b
PIN_TREE=b2acda288312268c190613318176c265f1b59db7
PIN_BASE=d30f7775c437f77138e78cd2eff9edc64af441e1
DIG_ROUTE=c0eedee9bc740afe07089f639a23931908beeddf231e1f5cfc651093c5128c12
DIG_CLONES=1a6f779eabceb9689d5a0b4c37c68e17c5529cb5b8ef1211f3d1340c532975bf
DIG_SELFTEST=ddd36f3d26750f196f7c06980f5db548a0df868ca97afbd529df9689e8f1b509
HOST_A="$(printf '/%s/' home)"
HOST_B="$(printf '/%s/' root)"
HOST_C="$(printf '/%s/' Users)"
HOST_D="$(printf '/tmp/%s' claude)"
TMPD=""
cleanup() { if [ -n "$TMPD" ]; then rm -rf "$TMPD"; fi; }
trap cleanup EXIT
jget() { node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const j=JSON.parse(s);process.stdout.write(String(eval(process.argv[1])))})' "$1"; }

# 검사 #1: 착수 sha 고정 — gitlink 3곳, 선행 병합 조상, 트리
# 기대: gitlink 3곳이 서브모듈 HEAD 와 같고, 두 병합 sha 가 server 착수 sha 의 조상이며 트리가 b2acda28
# 음성: prod 고정 sha 5a9a7106 을 조상 판정에 넣으면 rc 1 (계획 시점 실행)
test "$(git -C insuniverse-server rev-parse HEAD)" = "$PIN_SRV"
test "$(git -C insuniverse-admin rev-parse HEAD)" = "$PIN_ADM"
test "$(git -C insuniverse-site rev-parse HEAD)" = "$PIN_SITE"
test "$(git ls-tree HEAD insuniverse-server | awk '{print $3}')" = "$PIN_SRV"
test "$(git ls-tree HEAD insuniverse-admin | awk '{print $3}')" = "$PIN_ADM"
test "$(git ls-tree HEAD insuniverse-site | awk '{print $3}')" = "$PIN_SITE"
git -C insuniverse-server merge-base --is-ancestor "$PIN_HOTFIX" "$PIN_SRV"
git -C insuniverse-server merge-base --is-ancestor "$PIN_AFF" "$PIN_SRV"
test "$(git -C insuniverse-server rev-parse "$PIN_SRV^{tree}")" = "$PIN_TREE"
echo "check1 ok"

# 검사 #2: 도구 사본 — 세 파일이 원본과 바이트 같고 고정 digest 와 같다
# 기대: 사본 세 개의 sha256 이 고정값과 같고 원본과 cmp 가 0
# 음성: route-guards.mjs 한 바이트를 바꾼 사본에서 sha256 검사가 붉다 (구현 때 실행해 기록)
for f in route-guards.mjs clones.mjs selftest.sh; do cmp -s "$SRC/$f" "$META/$f"; done
echo "$DIG_ROUTE  $META/route-guards.mjs" | sha256sum -c --status
echo "$DIG_CLONES  $META/clones.mjs" | sha256sum -c --status
echo "$DIG_SELFTEST  $META/selftest.sh" | sha256sum -c --status
echo "check2 ok"

# 검사 #3: 도구 자가 검사 — 사본의 selftest 가 두 줄 모두 통과한다
# 기대: 출력에 ok clones 줄과 ok route-guards 줄이 있고 exit 0
# 음성: selftest 가 부르는 clones.mjs 를 빠뜨린 사본에서 exit 1 (구현 때 실행해 기록)
bash "$META/selftest.sh" > "$SCR/selftest.out" 2>&1
grep -q '^ok clones' "$SCR/selftest.out"
grep -q '^ok route-guards' "$SCR/selftest.out"
echo "check3 ok"

# 검사 #4: 동결본 재계산 — 두 트리의 --list 가 동결본과 바이트 같고 행 수가 도구의 handlers 와 같다
# 기대: 03ce7146 작업 트리와 aff7ffcc 아카이브의 재계산이 동결본과 같다. 행 수가 JSON handlers 와 같다
# 음성: 동결본에서 한 행을 뺀 사본은 cmp 에서 붉다 (구현 때 실행해 기록)
TMPD="$(mktemp -d "${TMPDIR:-/tmp}/m1-aff.XXXXXX")"
git -C insuniverse-server archive "$PIN_AFF" | tar -x -C "$TMPD"
git -C "$TMPD" init -q
git -C "$TMPD" add -A
node "$META/route-guards.mjs" "$TMPD" --list > "$SCR/freeze-aff.check"
node "$META/route-guards.mjs" "$WT/insuniverse-server" --list > "$SCR/freeze-03.check"
cmp "$SCR/freeze-aff.check" "$META/freeze-aff7ffcc.list.txt"
cmp "$SCR/freeze-03.check" "$META/freeze-03ce7146.list.txt"
test "$(wc -l < "$META/freeze-03ce7146.list.txt")" = "$(node "$META/route-guards.mjs" "$WT/insuniverse-server" | jget 'j.handlers')"
echo "check4 ok"

# 검사 #5: 미지 가드 0 · 무조건 공개 0 — 두 트리의 other 가 0, @Public 표지와 isPublic 설정자가 운영 소스에 없다
# 기대: 두 트리의 byKind.other 가 0. 운영 소스의 @Public( 와 isPublic 설정자 수가 0 (spec 제외)
# 음성: MysteryGuard 만 단 임시 저장소에서 other 가 1 이 되고 이 검사는 붉다 (계획 시점 기록은 없음, 구현 때 실행)
test "$(node "$META/route-guards.mjs" "$WT/insuniverse-server" | jget 'j.byKind.other||0')" = 0
test "$(node "$META/route-guards.mjs" "$TMPD" | jget 'j.byKind.other||0')" = 0
PUB_RC=0
git -C insuniverse-server grep -n -E "@Public\(|SetMetadata\(['\"]isPublic" -- apps libs > "$SCR/pub.txt" || PUB_RC=$?
test "$PUB_RC" -le 1
test "$(awk '!/\.spec\.ts:/' "$SCR/pub.txt" | wc -l)" = 0
echo "check5 ok"

# 검사 #6: 도구 표본 범위 — 운영 소스의 @Controller 선언이 *.controller.ts 밖에 없다 (spec 제외)
# 기대: *.controller.ts 가 아닌 운영 파일의 @Controller 선언 파일 수 0
# 음성: spec 을 빼지 않고 세면 spec 2건이 잡힌다 (계획 시점 기록 — 두 건이 spec)
CTL_RC=0
git -C insuniverse-server grep -l -E '@Controller\(' -- apps > "$SCR/ctl.txt" || CTL_RC=$?
test "$CTL_RC" -le 1
test "$(awk '!/\.spec\.ts$/ && !/\.controller\.ts$/' "$SCR/ctl.txt" | wc -l)" = 0
echo "check6 ok"

# 검사 #7: 판정표 생성 · 형상 — 생성기 출력이 커밋 표와 같고, 행 키가 동결본과 같고, 칸이 비지 않고 어휘 안이다
# 기대: build-table 재생성이 커밋 표와 cmp 0. 행 키 집합이 동결본과 같다. 칸 빈 값 0. 접근 계약 · 신원 종류가 어휘 안
# 음성: 한 행의 범위 키 칸을 비운 사본에서 빈 칸 수가 1 이 되고 이 검사는 붉다 (구현 때 실행해 기록)
node "$META/build-table.mjs" "$WT/insuniverse-server" "$META/freeze-03ce7146.list.txt" "$META/annotations.json" > "$SCR/table.md"
cmp "$SCR/table.md" "$TABLE"
awk -F'|' '/^\| [0-9]+ \|/ { v = $3; gsub(/ /, "", v); print v }' "$TABLE" | sort > "$SCR/keys-table.txt"
awk '{ print $1 }' "$META/freeze-03ce7146.list.txt" | sort > "$SCR/keys-freeze.txt"
cmp "$SCR/keys-table.txt" "$SCR/keys-freeze.txt"
test "$(awk -F'|' '/^\| [0-9]+ \|/ { for (i = 3; i <= 14; i++) { v = $i; gsub(/^ +| +$/, "", v); if (v == "" || v ~ /미분류/) b++ } } END { print b + 0 }' "$TABLE")" = 0
test "$(awk -F'|' '/^\| [0-9]+ \|/ { v = $7; gsub(/^ +| +$/, "", v); if (v !~ /^(관리자 판정|관리자 신원만|회원 신원|외부 신원 표지|판정만|신원 없음·선택)$/) b++ } END { print b + 0 }' "$TABLE")" = 0
test "$(awk -F'|' '/^\| [0-9]+ \|/ { v = $8; gsub(/^ +| +$/, "", v); if (v !~ /^(관리자 토큰|관리자 열람 토큰|회원 토큰|외부 Basic|없음)$/) b++ } END { print b + 0 }' "$TABLE")" = 0
echo "check7 ok"

# 검사 #8: 신원 수립 없는 행 — 가드 종류가 auth 아닌 행의 키 집합이 동결본과 같고, 그 행마다 비고가 채워졌다
# 기대: 동결본 비 auth 키 집합 == 표 비 auth 키 집합 (행 단위 대조). 비 auth 행의 비고 칸이 비지 않는다
# 음성: 두 행의 가드 종류를 맞바꾼 사본에서 키 대조가 붉다 (구현 때 실행해 기록)
awk '$3 != "auth" { print $1 }' "$META/freeze-03ce7146.list.txt" | sort > "$SCR/nonauth-freeze.txt"
awk -F'|' '/^\| [0-9]+ \|/ { g = $6; gsub(/ /, "", g); v = $3; gsub(/ /, "", v); if (g != "auth") print v }' "$TABLE" | sort > "$SCR/nonauth-table.txt"
cmp "$SCR/nonauth-freeze.txt" "$SCR/nonauth-table.txt"
test -s "$SCR/nonauth-table.txt"
test "$(awk -F'|' '/^\| [0-9]+ \|/ { g = $6; gsub(/ /, "", g); n = $14; gsub(/^ +| +$/, "", n); if (g != "auth" && n == "") b++ } END { print b + 0 }' "$TABLE")" = 0
echo "check8 ok"

# 검사 #9: 저장소 밖 호출자 후보 — 결제 콜백 · 클라이언트 콜백 · 사무실 IP 파일의 행이 모두 외부 신원 표지다
# 기대: 세 파일 각각 동결본 행 수가 1 이상이고, 같은 수가 외부 표지 칸과 접근 계약 칸에 있다
# 음성: 외부 표지 칸을 지운 사본에서 개수 대조가 붉다 (구현 때 실행해 기록)
for p in money-public coocon-bypass allow-ip-admin; do
  FR="$(awk -v p="/$p.controller.ts:" 'index($1, p) > 0' "$META/freeze-03ce7146.list.txt" | wc -l)"
  test "$FR" -ge 1
  EX="$(awk -F'|' -v p="/$p.controller.ts:" '/^\| [0-9]+ \|/ { f = $3; gsub(/ /, "", f); x = $13; gsub(/^ +| +$/, "", x); c = $7; gsub(/^ +| +$/, "", c); if (index(f, p) > 0 && x != "-" && x != "" && c == "외부 신원 표지") n++ } END { print n + 0 }' "$TABLE")"
  test "$FR" = "$EX"
done
echo "check9 ok"

# 검사 #10: 런타임 대조 — DB 없이 부팅한 Nest 메타데이터 집합이 표와 같고, 표에서 한 행을 뺀 · 한 행을 더한 사본은 붉다
# 기대: route-compare 가 exit 0. Express 등록 집합도 메타데이터 집합과 같다 (/live · /health 포함)
# 음성: 첫 행을 뺀 사본과 가짜 행을 더한 사본 모두 route-compare 가 exit 1 이다 (이 검사 본문이 매번 실행)
(cd insuniverse-server && TS_NODE_TRANSPILE_ONLY=1 TS_NODE_PROJECT=apps/api/tsconfig.app.json SERVER_ROOT="$WT/insuniverse-server" node -r ts-node/register -r tsconfig-paths/register "$WT/$META/route-runtime-dump.cjs" > "$SCR/runtime.json" 2> "$SCR/runtime.err")
node "$META/route-compare.mjs" "$TABLE" "$SCR/runtime.json"
awk 'BEGIN { d = 0 } /^\| [0-9]+ \|/ && d == 0 { d = 1; next } { print }' "$TABLE" > "$SCR/t-minus.md"
if node "$META/route-compare.mjs" "$SCR/t-minus.md" "$SCR/runtime.json" > /dev/null 2>&1; then echo "check10 minus-copy passed (bad)"; exit 1; fi
{ cat "$TABLE"; echo "| 999 | stray.controller.ts:1 | GET | /__stray | - | - | 관리자 토큰 | 없음 | - | - | - | - | - |"; } > "$SCR/t-plus.md"
if node "$META/route-compare.mjs" "$SCR/t-plus.md" "$SCR/runtime.json" > /dev/null 2>&1; then echo "check10 plus-copy passed (bad)"; exit 1; fi
echo "check10 ok"

# 검사 #11: 기준선 재계수 — 두 트리의 noauth 와 admin51 을 산식으로 재계산하고, 노트에 그 줄이 글자까지 같다
# 기대: 노트에 「03ce noauth=N admin51=M」 과 「aff noauth=N admin51=M」 줄이 있다. 값은 --stats 출력이다
# 음성: 노트의 한 값을 바꾼 사본에서 줄 대조가 붉다 (구현 때 실행해 기록)
test -s "$NOTE"
node "$META/build-table.mjs" "$WT/insuniverse-server" "$META/freeze-03ce7146.list.txt" --stats | sed 's/^/03ce /' > "$SCR/stats.txt"
node "$META/build-table.mjs" "$TMPD" "$META/freeze-aff7ffcc.list.txt" --stats | sed 's/^/aff /' >> "$SCR/stats.txt"
test "$(wc -l < "$SCR/stats.txt")" = 2
while IFS= read -r line; do grep -q -F -x -- "$line" "$NOTE"; done < "$SCR/stats.txt"
cat "$SCR/stats.txt"
echo "check11 ok"

# 검사 #12: 코드 영향 0 · 변경 범위 — 서브모듈 워킹트리가 깨끗하고, 기준 이후 부모 변경이 허용 집합 안이다
# 기대: 서브모듈 세 곳 status 가 비고, 변경 경로가 허용 접두 안이다 (mccp 게이트 기록 reviews · state 는 뺀다)
# 음성: 허용 밖 경로 한 줄을 넣은 목록에서 필터가 그 줄을 남긴다 (계획 시점 실행 — 필터의 관측, 구현 때 재확인)
for s in insuniverse-server insuniverse-admin insuniverse-site; do test -z "$(git -C "$s" status --porcelain)"; done
{ git diff --name-only "$PIN_BASE" HEAD; git status --porcelain --untracked-files=all | cut -c4-; } > "$SCR/changed.txt"
awk 'NF && !(index($0, "CLAUDE.md") == 1 || index($0, ".claude/templates/plan-kit/") == 1 || index($0, ".claude/meta/") == 1 || index($0, ".claude/PRPs/plans/") == 1 || $0 == ".claude/notes/admin-rbac-m1.md" || index($0, ".claude/reviews/") == 1 || index($0, ".claude/state/") == 1)' "$SCR/changed.txt" > "$SCR/out-of-scope.txt"
test ! -s "$SCR/out-of-scope.txt"
echo "check12 ok"

# 검사 #13: 호스트 경로 금지 — 계획서 · 메타 · 노트에 홈 · 루트 · 사용자 임시 경로 문자열이 없다
# 기대: 검색 결과 0 건. 입력이 없으면 grep rc 2 로 붉다 (빈 입력을 통과시키지 않는다)
# 음성: 계획서 사본 한 줄에 홈 경로 패턴을 넣으면 rc 0 이고 검사가 붉다 (구현 때 실행해 기록)
HRC=0
grep -r -I -l -F -e "$HOST_A" -e "$HOST_B" -e "$HOST_C" -e "$HOST_D" "$PLAN" "$META" "$NOTE" > "$SCR/host.txt" || HRC=$?
test "$HRC" = 1
test ! -s "$SCR/host.txt"
echo "check13 ok"
echo "admin-rbac-m1 gates ok"
