# 관리자 RBAC 마일스톤 1 검사 본문. 계획서 Validation 블록은 이 파일 호출 한 줄이다.
# 워크트리 루트를 기준으로 돈다. 검사는 순서대로 돌고 첫 실패에서 멈춘다.
set -euo pipefail
WT="$(git rev-parse --show-toplevel)"
cd "$WT"
META=/home/madsc/work/insuniverse-temp/.git/worktrees/admin-rbac/m1-sim/meta
PLAN=.claude/PRPs/plans/admin-rbac-m1.plan.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
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 와 같고, a3016d35 와 aff7ffcc 가 server HEAD 의 조상이며, server HEAD 의 트리가 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 가 두 줄 모두 통과한다
# 기대: 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 — 두 트리의 분류에서 other 가 0 이다 (도구가 모르는 가드 이름은 other 로 센다)
# 기대: 두 트리의 byKind.other 가 없거나 0
# 음성: 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
echo "check5 ok"

# 검사 #6: 도구 표본 범위 — 운영 소스에서 @Controller 선언이 *.controller.ts 밖에 0 이다 (spec 제외)
# 기대: 운영 파일 중 *.controller.ts 가 아닌 @Controller 선언 파일 수 0
# 음성: spec 을 빼지 않고 세면 samsung-intake 의 spec 2건이 잡힌다 (계획 시점 실행)
OUTSIDE="$(git -C insuniverse-server grep -l -E '@Controller\(' -- apps | grep -v -E '\.spec\.ts$' | grep -v -E '\.controller\.ts$' | wc -l || true)"
test "$OUTSIDE" = 0
echo "check6 ok"

# 검사 #7: 판정표 행 수 · 빈칸 — 표 행 수가 동결본 행 수와 같고, 빈칸과 미분류 칸이 0 이다
# 기대: 표 데이터 행 수 == 03ce7146 동결본 행 수. 비고 칸을 뺀 열 2~12 의 빈칸 · 미분류 0
# 음성: 한 행의 범위 키 칸을 비운 사본에서 빈칸 수가 1 이 되고 이 검사는 붉다 (구현 때 실행해 기록)
TABLE="$META/route-contract-table.md"
ROWS="$(grep -c -E '^\| [0-9]+ \|' "$TABLE" || true)"
test "$ROWS" = "$(wc -l < "$META/freeze-03ce7146.list.txt")"
BLANK="$(awk -F'|' '/^\| [0-9]+ \|/ { for (i = 2; i <= 12; i++) { v = $i; gsub(/^ +| +$/, "", v); if (v == "" || v ~ /미분류/) b++ } } END { print b + 0 }' "$TABLE")"
test "$BLANK" = 0
echo "check7 ok"

# 검사 #8: 신원 수립 가드 없는 행 — 동결본에서 센 수와 표에서 센 수가 같고, 그 행마다 접근 계약과 비고가 적혔다
# 기대: 동결본의 kind 가 auth 가 아닌 행 수 == 표의 가드 종류가 auth 가 아닌 행 수. 그 행의 접근 계약 · 비고 빈칸 0
# 음성: 한 행의 비고를 지운 사본에서 빈 비고 칸이 1 이 되고 이 검사는 붉다 (구현 때 실행해 기록)
NONAUTH_FREEZE="$(awk '$3 != "auth"' "$META/freeze-03ce7146.list.txt" | wc -l)"
NONAUTH_TABLE="$(awk -F'|' '/^\| [0-9]+ \|/ { v = $6; gsub(/ /, "", v); if (v != "auth") n++ } END { print n + 0 }' "$TABLE")"
test "$NONAUTH_FREEZE" = "$NONAUTH_TABLE"
NAMED_BAD="$(awk -F'|' '/^\| [0-9]+ \|/ { v = $6; gsub(/ /, "", v); if (v != "auth") { c = $7; n = $13; gsub(/^ +| +$/, "", c); gsub(/^ +| +$/, "", n); if (c == "" || n == "") b++ } } END { print b + 0 }' "$TABLE")"
test "$NAMED_BAD" = 0
echo "check8 ok"

# 검사 #9: 저장소 밖 소비자 후보 — 결제 승인 · 취소 콜백, clients 콜백, 사무실 IP 우회 5라우트가 「외부 신원 표지」로 이름 붙었다
# 기대: money-public 의 :12 와 :53 행이 외부 표지 행이다. allow-ip-admin 행 수(동결본)와 외부 표지 행 수가 같다
# 음성: 「외부 신원 표지」 칸을 지운 사본에서 money-public 행 수가 0 이 되고 이 검사는 붉다 (구현 때 실행해 기록)
MONEY_EXT="$(grep -c -E 'money-public\.controller\.ts:(12|53) .*외부 신원 표지' "$TABLE" || true)"
test "$MONEY_EXT" = 2
ALLOW_ROWS="$(awk '/allow-ip-admin\.controller\.ts:/' "$META/freeze-03ce7146.list.txt" | wc -l)"
ALLOW_EXT="$(grep -c -E 'allow-ip-admin\.controller\.ts:[0-9]+ .*외부 신원 표지' "$TABLE" || true)"
test "$ALLOW_ROWS" = "$ALLOW_EXT"
echo "check9 ok"

# 검사 #10: 런타임 라우트 ↔ 판정표 독립 대조 — DB 없이 부팅한 Express 스택의 메서드 · 경로 집합이 표와 같다
# 기대: route-compare.mjs 가 exit 0 이고 불일치 0. /live 와 /health 를 포함한다
# 음성: 표에서 한 행을 뺀 사본을 주면 route-compare.mjs 가 exit 1 이다 (구현 때 실행해 기록)
node "$META/route-runtime-dump.mjs" > "$SCR/runtime.json"
node "$META/route-compare.mjs" "$TABLE" "$SCR/runtime.json"
echo "check10 ok"

# 검사 #11: 기준선 재계수 — 신원 수립 없는 행 수가 표와 동결본에서 같은 산식으로 나오고, 51 정의 값을 출력한다
# 기대: 표의 가드 종류 비 auth 수 == 동결본 비 auth 수 (검사 #8 과 같은 산식). 51 정의 행 수는 출력만 한다
# 음성: 「유효 @Roles」 칸을 비운 사본에서 51 정의 행 수가 줄고 출력이 달라진다 (구현 때 실행해 기록)
ADM51="$(awk -F'|' '/^\| [0-9]+ \|/ { g = $6; t = $8; r = $9; gsub(/ /, "", g); gsub(/^ +| +$/, "", t); gsub(/^ +| +$/, "", r); if (g == "auth" && t == "관리자 토큰" && r == "없음") n++ } END { print n + 0 }' "$TABLE")"
echo "baseline: noauth_freeze=$NONAUTH_FREEZE noauth_table=$NONAUTH_TABLE admin_no_roles=$ADM51"
echo "check11 ok"

# 검사 #12: 코드 영향 0 · 변경 범위 — 서브모듈 워킹트리가 깨끗하고, 이 마일스톤의 부모 변경이 허용 집합 안에 있다
# 기대: 서브모듈 세 곳의 status 가 비어 있고, PIN_BASE..HEAD 의 변경 경로가 허용 접두 안에 있다
# 음성: 허용 밖 경로 한 줄을 넣은 목록에서 필터가 그 줄을 남긴다 (계획 시점 실행 — 필터 자체의 관측)
for s in insuniverse-server insuniverse-admin insuniverse-site; do
  test -z "$(git -C "$s" status --porcelain)"
done
OUT_OF_SCOPE="$(git diff --name-only "$PIN_BASE" HEAD | grep -v -E '^(CLAUDE\.md|\.claude/templates/plan-kit/|\.claude/meta/|\.claude/PRPs/plans/|\.claude/notes/admin-rbac-m1\.md)' || true)"
test -z "$OUT_OF_SCOPE"
echo "check12 ok"

# 검사 #13: 호스트 절대경로 금지 — 계획서와 M1 산출물에 호스트 홈 경로 문자열이 없다
# 기대: 검색 결과 0 건
# 음성: 계획 시점 실행에서 계획서 사본 한 줄에 호스트 경로를 넣으면 1 건이 잡힌다
HOSTHITS="$(grep -r -I -l -F "$(printf '/%s/' home)" "$PLAN" "$META" .claude/notes/admin-rbac-m1.md 2>/dev/null | wc -l || true)"
test "$HOSTHITS" = 0
echo "check13 ok"
echo "admin-rbac-m1 gates ok"
