#!/usr/bin/env bash
# capability_effect_probe.sh — 선언된 `writes:` 가 **참인지** 관측한다 (정체성 ① 블로커 (d)).
#
# ─────────────────────────────────────────────────────────────────────────────
# 왜 지어졌나 — 등록 바가 «형식» 만 재고 «진위» 를 안 잰다
# ─────────────────────────────────────────────────────────────────────────────
# `capability_registry_check.sh` 헤더가 직접 적어놨다:
#
#   "🟥 `writes:` 축은 검증 불가 — 그리고 그게 이 파일에서 실제로 터졌다.
#    2026-08-11, 이 검사기를 통과한 capability(`writes: read-only` 선언)의 진입점이
#    정리 트랩 결함으로 레포의 `scripts/` 를 rm -rf 했다. M1–M5 를 전부 통과한 채로.
#    구조 처방(미구축): 샌드박스에서 M4 를 돌려 쓰기 시도를 관측하는 것."
#
# 이 파일이 그 구조 처방이다. `ship_readiness_gate.md` 정체성 ① 의 블로커 (d) —
# *"초록이 파괴 행위에 선행할 수 있는 검사기는 초록 정체성이 아니다"* — 를 겨눈다.
#
# ─────────────────────────────────────────────────────────────────────────────
# 이 프로브가 **증명하지 않는** 것 (과잉주장 금지)
# ─────────────────────────────────────────────────────────────────────────────
# · **한 번의 실행만 본다.** 조건부로 쓰는 capability(특정 입력에서만 쓴다)는 그 입력이
#   M4 쌍에 없으면 안 걸린다. 이건 «이 실행에서 안 썼다» 이지 «절대 안 쓴다» 가 아니다.
# · **샌드박스 밖은 못 본다.** 절대경로로 다른 데를 쓰면 이 프로브는 놓친다.
#   ⟹ 그래서 **카나리아를 샌드박스 밖에도 심는다**(아래 OUTSIDE_CANARY). 그게 지워지면
#      «샌드박스 밖 쓰기» 로 잡힌다 — 완전하진 않지만 조용하진 않다.
# · **네트워크·프로세스 부작용은 안 본다.** 파일시스템 축 전용이다.
# · 🟥 **사본은 `HEAD` 기준이다**(`git archive HEAD`). 후보 파일이 워킹트리에만 있고 커밋 전이면,
#   프로브가 재는 트리는 **후보 상태가 아니다.** 등록 시점에 커밋된 상태를 재는 것이 정상
#   경로이나, 개발 중 호출하면 이 어긋남이 조용히 생긴다 — 그래서 여기 적는다.
# · 🟥 **사본에 `.git` 이 없다 — 이건 의도이고 대가가 있다.** 2026-08-16 이전에는 `git worktree`
#   를 썼는데, 워크트리는 실물 레포의 `.git` 을 **공유**하므로 `read-only` 를 선언한 진입점이
#   `git config --local core.pager` 로 **그 레포에 영속 코드실행을 심고도 VERIFIED** 를 받았다
#   (보안 패스 [S], 손으로 재현). 지금은 `.git` 이 아예 없다. 대가: **git 을 실제로 쓰는
#   capability 는 여기서 못 돈다 → UNVERIFIABLE.** 미측정을 통과로 렌더하는 것보다 낫다.
# · 🟥 **`.git` 외의 메타데이터 채널은 여전히 안 본다.** 이 수리는 «프로브가 스스로 열어 준
#   통로» 하나를 닫은 것이지 «부작용 채널 전부» 를 닫은 게 아니다.
# · 🟥 **절대경로 감시는 «홈 엔트리 목록» 수준이다.** 홈 **하위 디렉토리 내부**의 변경
#   (예: `$HOME/Documents/x`)이나 `/tmp` 의 남의 파일 수정은 여전히 못 본다.
#   전수 스냅샷은 비용이 크고 홈이 늘 시끄러워 오탐이 난다 — 그래서 **엔트리 목록**만 본다.
#   이건 «가장 흔한 표적을 막는다» 이지 «절대경로 쓰기를 막는다» 가 아니다.
#   2026-08-16 자기 공격으로 이 층을 찾았고, 그 위 층은 여전히 열려 있다고 적어 둔다.
# · 실행 불가는 **PASS 가 아니다** — `UNVERIFIABLE`(3) 로 낸다. 미측정을 0 으로 렌더하지 않는다.
#
# 사용법
#   capability_effect_probe.sh <capfile> [<capfile> ...]
#   capability_effect_probe.sh --self-test
#
# exit code
#   0  VERIFIED        선언과 관측이 일치
#   1  VIOLATION       선언보다 **넓게** 썼다 (read-only 선언인데 썼다 등)
#   3  UNVERIFIABLE    돌릴 수 없었다 — 판정 아님(미측정)
#   10 HARNESS_ERROR   capfile 파손·전제 파손
set -o pipefail
set -f

RC_OK=0; RC_VIOLATION=1; RC_UNVERIFIABLE=3; RC_HARNESS=10

# 🟥 **계기 전제 프리플라이트** (2026-08-18, 넓은-질문 탈상관 팔이 known-pair 로 재현).
#   `_snapshot` 의 파일 축은 `… | xargs -0 shasum -a 256 2>/dev/null` 이다. `shasum` 이 없거나
#   죽으면 before/after 가 **둘 다 빈 문자열**이 되어 `changed=0` → **`✅ VERIFIED · 관측=no-write`**.
#   재현(같은 capfile, PATH 만 다름): 정상 `🟥 VIOLATION rc=1` / shasum=127 `✅ VERIFIED rc=0`.
#   ⇒ 이 파일이 헤더에서 겨눈 「미측정을 0 으로 렌더」의 자기 판본이었고, `UNVERIFIABLE` 조차
#     아니라 **VERIFIED** 였다. 계기가 죽으면 판정을 내지 않는다.
#   ★`2>/dev/null` 은 남긴다 — 개별 파일 권한 오류로 전체가 죽으면 그게 더 나쁘다.
#     막는 지점은 **전제**(도구 부재)이지 개별 항목 실패가 아니다.
#   ⚠️ **폴백이 필요하다**(cross-family 지목 #3): `shasum` 은 macOS/perl 계열 관행이고
#     Linux/BusyBox 소비자는 `sha256sum` 만 있는 경우가 흔하다. 이 파일은 `package.json
#     files[]` 로 **출하되므로** shasum 강제는 과차단이다.
HASHER=""
if   command -v shasum    >/dev/null 2>&1; then HASHER="shasum -a 256"
elif command -v sha256sum >/dev/null 2>&1; then HASHER="sha256sum"
else
  echo "❌ HARNESS_ERROR — \`shasum\` 도 \`sha256sum\` 도 PATH 에 없다." >&2
  echo "   스냅샷의 해시 축이 죽어 거짓 \`VERIFIED\` 가 난다. 판정하지 않고 중단한다." >&2
  exit "$RC_HARNESS"
fi

# writes 축 순서 (좁음 → 넓음). 관측이 선언보다 넓으면 VIOLATION.
ORDER_writes="read-only write-local write-remote"

_idx() { local v="$1" i=0 x; for x in $ORDER_writes; do [ "$x" = "$v" ] && { printf '%d' "$i"; return; }; i=$((i+1)); done; printf '%d' -1; }

_key() {  # $1=capfile $2=key
  sed -n "s/^[[:space:]]*$2:[[:space:]]*//p" "$1" 2>/dev/null | head -1 \
    | sed 's/[[:space:]]*$//'
}

# `GIT_META_WATCH` = 격리 클론 안에서 **추가로** 해싱할 경로들(코드실행으로 이어지는 git
# 메타데이터 표면). 비어 있으면 종전과 동일하게 트리만 잰다.
GIT_META_WATCH=""

# 🟥 **`GIT_META_NAMEONLY` 는 폐기했다(2026-08-16, cross-family 지목 + 손 재현).**
# 한때 `.git/objects` 를 «이름만» 세는 별도 목록으로 뒀다 — 근거는 「객체 파일명이 곧 내용
# 해시라 이름 집합이 안 바뀌면 내용도 안 바뀐다」였다. **그 추론은 정직한 액터에만 참이다.**
# 재현: `writes: read-only` 로 선언한 진입점이 기존 loose object 하나에
# `printf CORRUPT > "$p"` 를 하면 이름 집합은 그대로라 **`✅ VERIFIED · 관측=no-write`** 가 났다.
# 「같은 이름, 다른 내용」을 못 본다. 팩파일 수동 덮어쓰기도 같은 얼굴이다.
# ⇒ 이름만 세는 건 **정직성을 가정하는 계기**이고, 이 프로브의 대상은 정확히 그 가정이 깨진
#    경우다. 되돌리고, 대신 **비용 쪽을 고친다**(아래 배치 해싱).
#
# 비용 이력(적어 둔다 — 같은 실수를 두 번 하지 않게): 처음엔 파일당 `shasum` 프로세스라
# 2871개 × before/after × 양·음 arm 으로 **2분 타임아웃**이 났다. 원인은 `.git/objects` 가
# 아니라 **파일당 프로세스 스폰**이었다. `xargs` 배치로 한 번에 넘기면 같은 대상을 재면서
# 비용이 떨어진다 — 정확도를 깎아 비용을 맞추려던 게 잘못된 트레이드였다.

_snapshot() {  # $1=dir → "file <path>" + "sha  path" + dir/기타 축 (정렬)
  # 🟥 2026-09-02 (B1 «빈 값 양변») — 종전엔 대상 디렉터리가 없거나 find 가 죽으면 출력이
  #    «빈 문자열»이었고, before/after 둘 다 비면 `[ "$before" = "$after" ]` 가 참이라
  #    changed=0 → «부작용 없음» 으로 접혔다. 빈 샌드박스(정당한 빈 출력)와 구분이 없었다.
  #    ⇒ 첫 줄에 «무엇을 찍었나»를, 실패는 SNAPSHOT_ERROR 로 **값 안에** 남긴다. 호출부가 검사한다.
  if [ ! -d "$1" ]; then printf 'SNAPSHOT_ERROR nodir %s\n' "$1"; return 1; fi
  printf 'snapshot-of %s\n' "$1"
  local _flist; _flist=$(mktemp "${TMPDIR:-/tmp}/fh_flist.XXXXXX")
  {
    # `.git` 은 통째로 제외한다 — `git status` 류가 `.git/index` 를 정당하게 갱신하므로
    # 전체를 재면 읽기 전용 capability 가 거짓 VIOLATION 을 받는다. 대신 아래에서
    # **실행으로 이어지는 표면만** 골라 넣는다.
    find "$1" -type f -not -path '*/.git/*' 2>/dev/null || printf 'SNAPSHOT_ERROR find-rc %s\n' "$?"
    local w
    for w in $GIT_META_WATCH; do
      [ -e "$w" ] || continue
      # 🟥 **경로 소속을 확인한다(2026-08-16).** 초판은 `$1` 과 무관하게 감시면을 **항상**
      #   붙였다. `GIT_META_WATCH` 는 **샌드박스 안** 경로이므로, 바깥 카나리아를 잴 때
      #   `_snapshot "$outside"` 가 **샌드박스의 `.git` 을 같이 재는** 상태였다.
      #   증상: 샌드박스 안 git 메타를 건드린 capability 가 VIOLATION 은 맞게 받되
      #   이유가 **«샌드박스 밖이 바뀌었다»** 로 찍혔다 — 판정은 맞고 **처방이 오라우팅**된다.
      #   레인이 `rc=1` 만 단언하고 **이유를 안 쟀기 때문에** 여태 안 보였다
      #   ([[feedback_metric_measures_presence_not_relation]]).
      case "$w" in "$1"/*|"$1") ;; *) continue ;; esac
      find "$w" -type f 2>/dev/null
    done
  } | LC_ALL=C sort -u > "$_flist"
  # 🟥 **이름 목록 축 — 해시와 별도로 낸다** (2026-08-18, cross-family 지목 #1).
  #   해시 축만 두면 «해시가 실패한 파일»과 «없는 파일»이 **구분 불가**다. 실측 반례 둘:
  #     ⓐ `shasum` 이 존재하지만 exit 1 인 shim  → 해시 0줄 → changed=0 → ✅ VERIFIED
  #     ⓑ **가짜 도구 없이도** `touch f && chmod 000 f` → 그 파일 해시만 실패 →
  #        `2>/dev/null` 이 삼킴 → ✅ VERIFIED
  #   ⇒ 이름 목록은 **해시 도구와 무관하게** 생성/삭제를 잡는다. 두 축은 서로의 폴백이 아니라
  #     **다른 것을 잰다**(이름=존재 · 해시=내용). 둘 다 필요하다.
  LC_ALL=C sed 's/^/file /' "$_flist"
  tr '\n' '\0' < "$_flist" \
    | xargs -0 -n 400 $HASHER 2>/dev/null \
    | LC_ALL=C sort
  rm -f "$_flist"
  # 🟥 **디렉토리 존재 축** (2026-08-17). 위 블록은 `-type f` 라 **파일만** 센다 — 빈
  #   디렉토리는 관측면에 아예 없었고, `mkdir -p uploads outputs tmp` 만 하는 capability 가
  #   `writes: read-only` 선언으로 **✅ VERIFIED** 를 받았다(known-pair 3-arm 실측).
  #   git 은 빈 디렉토리를 추적하지 않으므로 격리 클론 샌드박스에도 없다 — 즉 이건 warm
  #   cache 가 아니라 **관측면의 구멍**이다(원정 §2 의 진단을 이 실측이 정정했다).
  #   ★존재만 센다, mtime 은 안 센다 — 안쪽 파일이 바뀔 때마다 디렉토리 mtime 이 움직여
  #    읽기전용 capability 가 거짓 VIOLATION 을 받는다.
  #   ★`.git` 제외 경계는 위 파일 축과 **같다**. 갈라지면 한쪽만 고쳐지는 구조가 된다.
  find "$1" -type d -not -path '*/.git/*' -not -path '*/.git' 2>/dev/null \
    | LC_ALL=C sort | sed 's/^/dir  /'
  # 🟥 **비정규 노드 축** (2026-08-17, cross-family codex/gpt-5.5 지목 → 실측 확인).
  #   `-type f` 도 `-type d` 도 symlink·fifo·소켓·디바이스 노드를 안 센다. 실측:
  #     entry `ln -sf /etc/passwd leaked_link` + writes: read-only  → ✅ VERIFIED
  #     entry `mkfifo backdoor_pipe`          + writes: read-only  → ✅ VERIFIED
  #   위 디렉토리 축을 더한 **뒤에도** 둘 다 통과했다 — 같은 결함 클래스가 남아 있었다.
  #   ★symlink 는 **대상까지** 잰다. 존재만 세면 같은 경로를 다른 곳으로 다시 거는
  #    `ln -sf other link` 가 무변화로 보인다 — 그게 이 축에서 가장 위험한 형태다.
  #   ★`stat -f`/`-printf` 를 안 쓴다 — 전자는 BSD 전용, 후자는 GNU 전용이라 한쪽 CI 에서
  #    조용히 죽는다([[feedback_wiring_surfaces_hidden_failures]] 의 실측 클래스).
  #    `-type` 술어와 `readlink` 만 쓰면 양쪽에서 돈다.
  find "$1" -type l -not -path '*/.git/*' 2>/dev/null | LC_ALL=C sort \
    | while IFS= read -r _l; do
        printf 'link  %s -> %s\n' "$_l" "$(readlink "$_l" 2>/dev/null)"
      done
  find "$1" -not -path '*/.git/*' -not -path '*/.git' \
       ! -type f ! -type d ! -type l 2>/dev/null \
    | LC_ALL=C sort | sed 's/^/node  /'
  # ⚠️ 명명된 한계: 개행이 든 파일명은 `find` 출력에서 쪼개진다. 이 프로브가 도입될 때부터
  #    있던 성질이고 이번에 안 고쳤다 — 고치려면 `find -print0` 로 전 구간을 널 구분해야 한다.
}

# ★정리 트랩 — 신호/중단에도 temp 가 안 남게 한다(cross-family 지목).
#   2026-08-16 이전에는 여기에 `git worktree remove` + `worktree prune` 분기가 있었다.
#   샌드박스가 워크트리가 아니게 되면서(§격리) **둘 다 지웠다**. `prune` 은 특히 지워야 했다 —
#   그건 **그 레포의 모든** stale 워크트리 등록을 청소하므로, 병렬 세션이 워크트리 규율을 쓰는
#   레포에서 **남의 등록을 건드릴 수 있었다**(보안 패스 [B]).
_PROBE_WT_PARENT=""; _PROBE_SANDBOX=""
_cleanup_probe() {
  [ -n "$_PROBE_SANDBOX" ] && rm -rf "$_PROBE_SANDBOX"
  [ -n "$_PROBE_WT_PARENT" ] && rm -rf "$_PROBE_WT_PARENT"
  _PROBE_WT_PARENT=""; _PROBE_SANDBOX=""
}
trap _cleanup_probe EXIT INT TERM

probe_one() {  # $1=capfile → rc
  local cap="$1"
  [ -r "$cap" ] || { printf '❌ capfile 도달 불가: %s\n' "$cap" >&2; return "$RC_HARNESS"; }

  local declared entry pos_args neg_args
  declared=$(_key "$cap" writes)
  entry=$(_key "$cap" entry)
  pos_args=$(_key "$cap" calibration_positive_args)
  neg_args=$(_key "$cap" calibration_negative_args)

  [ -n "$declared" ] || { printf '❌ %s: writes 축 선언 없음 — 검증 대상이 아니다\n' "$cap" >&2; return "$RC_HARNESS"; }
  [ "$(_idx "$declared")" -ge 0 ] || { printf '❌ %s: writes 값이 enum 밖: %s\n' "$cap" "$declared" >&2; return "$RC_HARNESS"; }
  [ -n "$entry" ] || { printf '❌ %s: entry 없음\n' "$cap" >&2; return "$RC_HARNESS"; }

  # `requires_cwd` 가 있으면 그 레포의 **`.git` 없는 파일 사본**을 샌드박스로 쓴다.
  # 실물 트리에서 돌리면 «파괴를 관측하려다 파괴하는» 꼴이고, 빈 임시디렉토리에서
  # 돌리면 진입점이 전제를 못 찾아 UNVERIFIABLE 로만 끝난다(실측: 실물 capfile 10 중 4).
  #
  # 🟥 **리셋 먼저, 편집 나중.** 사본을 만든 뒤에 픽스처를 얹는다. 순서를 뒤집으면
  #    사본 생성이 tracked 편집을 되돌려 프로브가 조용히 공허해진다
  #    (2026-08-16 병렬 세션 실측: 같은 순서 문제로 한 레인이 두 번 장식이 됐다).
  local req_cwd wt=""
  req_cwd=$(_key "$cap" requires_cwd)
  # ★`SELF` 를 여기서도 푼다. 초판은 `capability_registry_check.sh` 에만 넣고 이쪽에 안 넣어서,
  #   프로브가 `-d "SELF/.git"` 을 보고 «git 레포 아님» 으로 판정 → 빈 mktemp 샌드박스 →
  #   진입점 rc=127 → **UNVERIFIABLE** 이 났다. 등록 검사기는 통과시키는데 프로브만 막는
  #   형태라 원인이 안 보인다. **반쪽-픽스 전파 경계**의 세 번째 재현이고
  #   ([[feedback_half_fix_propagation_boundary]]), 계기가 잡았다.
  #   해석은 검사기와 **같은 규칙**이어야 한다 — 다르면 두 축이 서로 다른 트리를 잰다.
  if [ "$req_cwd" = "SELF" ]; then
    req_cwd="$(git -C "$(dirname "$cap")" rev-parse --show-toplevel 2>/dev/null)"
    if [ -z "$req_cwd" ]; then
      printf '⚠️  UNVERIFIABLE %s — requires_cwd: SELF 인데 선언이 git 레포 안에 없다\n' "$cap" >&2
      return "$RC_UNVERIFIABLE"
    fi
  fi

  local sandbox outside wt_parent=""
  # ★`-d .git` 이 아니라 **git 자신에게 묻는다**. 워크트리·submodule 에서 `.git` 은 **파일**이라
  #   `-d` 가 거짓이 되고, 그러면 이 프로브만 빈 mktemp 로 떨어져 같은 선언이
  #   등록 검사기에선 VERIFIED · 프로브에선 UNVERIFIABLE 로 갈린다(cross-family 지목, 손 재현:
  #   일반 레포 rc=0 / 워크트리 rc=3, 동일 capfile). 검사기는 이미 `rev-parse` 를 쓴다 —
  #   두 계기가 다른 규칙을 들면 그게 이 레포가 이름 붙인 divergent-normalizer 다.
  local _is_repo=""
  [ -n "$req_cwd" ] && _is_repo="$(git -C "$req_cwd" rev-parse --show-toplevel 2>/dev/null)"
  if [ -n "$_is_repo" ]; then
    # 🟥 **`git worktree` 를 쓰지 않는다 — 워크트리는 정의상 실물 레포의 `.git` 을 공유한다.**
    # 2026-08-16 보안 패스가 지목하고 거버너가 손으로 재현했다: `writes: read-only` 를 선언한
    # 진입점이 워크트리 안에서 `git config --local core.pager "sh -c <cmd>"` 를 심으면
    #   · 그 config 는 **실물 레포**에 쓰인다(공유 `.git`)
    #   · `_snapshot` 은 샌드박스 **디렉토리 트리**만 해싱하므로 `.git/` 은 구조적으로 안 보인다
    #   · `$outside` 카나리아도 `$HOME` 감시도 이 채널을 못 본다
    # → 프로브는 `✅ VERIFIED … 관측=no-write` 를 찍고, 그 레포의 다음 `git log` 가 공격자
    #   명령을 실행한다. **M6 가 «위반 없음» 이라고 적는 바로 그 실행이 위반을 저지른다.**
    # 이건 헤더의 명명된 잔여(«절대경로로 다른 데를 쓰면 놓친다»)와 **다르다**: 그건 capability 가
    # 고른 경로이고, 이건 **프로브가 스스로 열어 준 통로**다.
    #
    # 처방(2026-08-16 2차) — **격리된 로컬 클론**. 초판은 `git archive` 로 `.git` 없는 사본을
    # 만들었는데, 그러면 안전하지만 **git 을 쓰는 capability 전부가 영구히 등록 불가**가 된다
    # (실측: `qasp-dev:new-code-anchor-scan` 이 rc=127 → UNVERIFIABLE → REJECTED).
    # 엣지가 아니라 **한 부류 통째의 배제**이고, 이 레포 자신의 규율이 그걸 금한다 —
    # *"만족할 수 없는 게이트는 엄격한 게이트가 아니라 우회 훈련기다."*
    #
    # `git clone --local --no-hardlinks` 는 **자기 `.git` 을 가진 독립 레포**를 만든다:
    #   · 진입점의 git 명령이 실제로 돈다(측정 가능해진다)
    #   · 쓰기는 **클론 안**에 갇힌다 — 실물 레포의 `.git` 은 공유되지 않는다
    #   · `--no-hardlinks` 로 오브젝트를 복사한다(하드링크는 같은 inode 를 공유한다)
    # 🟥 그리고 **`origin` 을 지운다** — 클론의 origin 은 소스 경로를 가리키므로
    #    `git push origin` 이 **실물 레포로 되돌아가는 통로**가 된다. 우발 경로를 닫는다
    #    (절대경로를 직접 쓰는 공격은 헤더의 기존 명명 잔여이지 이 통로가 아니다).
    wt_parent=$(mktemp -d) || return "$RC_HARNESS"
    _PROBE_WT_PARENT="$wt_parent"      # ★등록을 mktemp **직후**에 (누수 수리)
    wt="$wt_parent/clone"
    if ! git clone --local --no-hardlinks -q "$req_cwd" "$wt" >/dev/null 2>&1; then
      printf '⚠️  UNVERIFIABLE %s — 격리 클론을 못 만들었다(%s)\n' "$cap" "$req_cwd" >&2
      return "$RC_UNVERIFIABLE"
    fi
    git -C "$wt" remote remove origin >/dev/null 2>&1 || true
    # 🟥 **alternates 를 지운다** (2026-08-16 cross-family 지목, 거버너 손 재현).
    # `--no-hardlinks` 는 오브젝트를 **복사**하지만, **소스가 이미 alternates 를 갖고 있으면
    # 클론이 그 alternates 를 상속한다** — 즉 클론의 오브젝트 저장소가 **샌드박스 밖 경로**를
    # 참조하게 된다. 내 초판 격리 측정은 alternates 없는 소스만 봐서 이 채널을 못 봤다.
    # 재현: 소스에 `objects/info/alternates` 를 심고 `--local --no-hardlinks` 클론 →
    #       클론의 alternates 에 그 경로가 그대로 남는다.
    # 지운 뒤 **오브젝트가 실제로 다 있는지**는 아래 단언이 본다 — 지우기만 하고 깨진 채로
    # 돌리면 진입점이 이상하게 실패하고 그게 «위반 없음» 으로 렌더될 수 있다.
    rm -f "$wt/.git/objects/info/alternates" 2>/dev/null || true
    # 격리 전제를 **단언한다** — 하나라도 어긋나면 판정하지 않는다(측정 대신 침묵).
    if [ ! -d "$wt/.git" ] || [ -n "$(git -C "$wt" remote 2>/dev/null)" ] \
       || [ -f "$wt/.git/objects/info/alternates" ]; then
      printf '❌ %s: 격리 전제 파손(.git 부재 · origin 잔존 · alternates 잔존). 판정하지 않는다\n' "$cap" >&2
      return "$RC_HARNESS"
    fi
    # alternates 를 지우고도 HEAD 트리가 온전한가 — 아니면 «격리했지만 못 쓰는» 사본이다.
    if ! git -C "$wt" rev-parse --verify -q HEAD >/dev/null 2>&1 \
       || ! git -C "$wt" cat-file -e HEAD^{tree} 2>/dev/null; then
      printf '⚠️  UNVERIFIABLE %s — alternates 제거 후 오브젝트가 불완전하다(소스가 alternates 에 의존)\n' "$cap" >&2
      return "$RC_UNVERIFIABLE"
    fi
    sandbox="$wt"
    _PROBE_SANDBOX="$wt"
    # 🟥 **코드실행으로 이어지는 git 메타데이터 표면**을 스냅샷 대상에 넣는다.
    #    `.git` 전체를 넣지 않는 이유: `git status` 류가 `.git/index` 를 정당하게 갱신하므로
    #    전체를 재면 **읽기 전용 capability 가 거짓 VIOLATION** 을 받는다. 그래서 «나중에
    #    명령을 실행시킬 수 있는» 표면만 고른다 — config(core.pager·alias) · hooks · refs.
    #   ★`info/attributes` 와 `modules/` 도 본다(cross-family 지목). 전자는 `filter`/`clean`/
    #     `smudge` 속성으로 **체크아웃·add 시 임의 명령을 실행**시킬 수 있고, 후자는 submodule
    #     의 실제 gitdir 라 그 안의 config·hooks 가 같은 실행 표면이다. config·hooks·refs 만
    #     보면 그 둘로 새는 경로가 감시 밖에 남는다.
    # 🟥 `.git/objects` 추가(2026-08-16). 이 축이 빠져 있는 동안 **`writes: read-only` 로
    #   거짓 선언해도 VERIFIED 가 나왔다** — pmh-dev 자기선언 작업에서 실측으로 지목됐다
    #   (`git merge-tree --write-tree` 가 objects 2143→2144, 스냅샷은 그걸 안 봄).
    #   즉 M6 는 «선언 진위» 를 잰다고 하면서 **쓰기의 한 종류를 구조적으로 못 보고 있었다.**
    #   객체 생성은 읽기가 아니다 — 그래서 감시면에 넣는다. `.git/index` 는 여전히 제외다
    #   (`git status` 류가 정당하게 갱신하므로 넣으면 거짓 VIOLATION 이 된다).
    #   `.git/objects` 는 **내용까지** 해싱한다 — 이름만 세면 「같은 이름, 다른 내용」
    #   (기존 객체 덮어쓰기)을 못 본다(§_snapshot 의 폐기 기록 참조). 비용은 배치 해싱으로 낸다.
    GIT_META_WATCH="$wt/.git/config $wt/.git/hooks $wt/.git/packed-refs $wt/.git/refs $wt/.git/info/attributes $wt/.git/modules $wt/.git/objects"
  else
    sandbox=$(mktemp -d) || return "$RC_HARNESS"
    _PROBE_SANDBOX="$sandbox"
  fi
  outside=$(mktemp -d) || return "$RC_HARNESS"
  printf 'canary-inside\n'  > "$sandbox/CANARY_INSIDE"
  printf 'canary-outside\n' > "$outside/CANARY_OUTSIDE"

  # 🟥 **홈 디렉토리 카나리아** — 2026-08-16 실측: 초판은 임시 카나리아 하나만 봐서
  #    `echo x > $HOME/...` 이 **VERIFIED 로 통과**했고 홈에 파일이 실제로 생겼다.
  #    (d) 가 말하는 결함을 프로브 자신이 갖고 있었던 것이다. 절대경로 쓰기의 가장 흔한
  #    표적이 홈이므로 여기에도 감시를 둔다. ⚠️ 완전하지 않다 — 아래 §잔여 참조.
  local home_before home_after
  home_before=$(ls -A "$HOME" 2>/dev/null | LC_ALL=C sort | shasum -a 256 | cut -d" " -f1)
  mkdir -p "$sandbox/scripts"; printf 'x\n' > "$sandbox/scripts/CANARY_SCRIPTS"

  local before after out rc
  before=$(_snapshot "$sandbox")
  # 🟥 스냅샷은 **절대 비지 않는다** — 디렉토리 축이 최소한 루트 자신을 찍기 때문이다.
  #   비었다면 `find` 조차 실패한 것이고, 그건 관측이 아니라 계기 사망이다.
  if [ -z "$before" ]; then
    echo "  ❌ HARNESS_ERROR — 사전 스냅샷이 비었다($sandbox). 관측면이 죽었다." >&2
    return "$RC_HARNESS"
  fi
  local out_before; out_before=$(_snapshot "$outside")

  # 실행 — **양 arm 을 같은 샌드박스 정책으로** 돌린다(2026-08-16 cross-family 지목).
  # 초판은 positive 만 돌렸는데, 등록 검사기 주석은 «M4 를 관측한다» 고 말했다 —
  # 구현과 주장이 어긋났고, **negative arm 에서만 쓰는 capability 가 통과**했다.
  # 실패는 VIOLATION 이 아니다 — 못 잰 것이다.
  #
  # 🟥 **args 를 검문한다** (2026-08-16 보안 패스 [A], 거버너 재현). `eval "$entry $pos_args"`
  # 는 `entry` 가 M1 을 통과하는 순수 argv 여도 **args 하나로 탈출한다**:
  # `calibration_positive_args: x; touch /tmp/PWNED` 가
  # 샌드박스 밖 `/tmp` 에 쓰고도 `✅ VERIFIED · 관측=no-write` 를 받았다.
  # 등록기(`capability_registry_check.sh`)에는 `_validate_arm_args` 가 이미 있었고
  # **프로브에만 없었다** — 그런데 헤더 usage 가 standalone 호출을 광고하고 files[] 로 단독
  # 출하되므로, 등록기 경유 M4 검문에 의존할 수 없다. 형제가 가진 가드를 이쪽에도 세운다.
  _reject_arm_args() {   # $1=args $2=arm 이름 → 0=통과 1=거부
    case "$1" in
      *'|'*|*';'*|*'&'*|*'>'*|*'<'*|*'`'*|*'$('*|*$'\n'*)
        printf '🟥 VIOLATION %s — %s arm args 에 셸 메타문자(인젝션 표면): %s\n' "$cap" "$2" "$1"; return 1 ;;
      *'../'*)
        printf '🟥 VIOLATION %s — %s arm args 가 상위 경로로 탈출한다: %s\n' "$cap" "$2" "$1"; return 1 ;;
    esac
    return 0
  }
  _reject_arm_args "$pos_args" positive || return "$RC_VIOLATION"
  _reject_arm_args "$neg_args" negative || return "$RC_VIOLATION"

  # ⚠️ `eval` 은 **남긴다**, 그리고 그 이유가 측정으로 갈렸다. 처음엔 지적의 처방대로
  # `set -- $entry $args; "$@"` 로 바꿨는데, 그러면 **셸 형태 진입점의 의미가 깨진다**:
  # `bash -c 'git tag X; …'` 가 따옴표를 잃고 조각나 bash 가 `unexpected EOF` 로 rc=2 를 내고,
  # 아무것도 안 바뀌었으니 프로브가 **`✅ VERIFIED`** 를 찍었다(실측). 즉 처방을 그대로
  # 적용하면 [A] 를 닫으면서 «실행 실패를 무위반으로 렌더» 하는 더 조용한 결함이 생긴다.
  # 이 프로브의 자기 self-test 조차 `entry: bash -c '…'` 를 쓴다.
  # **닫아야 할 것은 args 였다** — 위 `_reject_arm_args` 가 그걸 닫는다. entry 자체는 이
  # 도구가 «실행해서 관측하겠다» 고 선언한 대상이라 실행이 목적이지 결함이 아니다.
  local out2 rc2
  out=$(cd "$sandbox" && eval "$entry $pos_args" 2>&1); rc=$?
  out2=""; rc2=0
  if [ -n "$neg_args" ]; then
    out2=$(cd "$sandbox" && eval "$entry $neg_args" 2>&1); rc2=$?
    out="$out
$out2"
    [ "$rc2" -gt "$rc" ] && rc="$rc2"
  fi

  after=$(_snapshot "$sandbox")
  local out_after; out_after=$(_snapshot "$outside")

  # 🟥 스냅샷 «실패»는 «같음»이 아니다 — 양쪽이 같이 죽으면 종전엔 changed=0 이었다.
  case "$before$after$out_before$out_after" in
    *"\nSNAPSHOT_ERROR "*|"SNAPSHOT_ERROR "*)   # 줄머리 센티널만 — 파일명에 SNAPSHOT_ERROR 가 들어가도 안 걸린다(codex B8)
      _cleanup_probe; rm -rf "$outside"
      printf '🟥 HARNESS_ERROR %s — 스냅샷 실패(SNAPSHOT_ERROR). «변화 없음»이 아니라 «못 쟀다»\n' "$cap"
      return "$RC_HARNESS" ;;
  esac
  local changed outside_changed
  changed=$([ "$before" = "$after" ] && echo 0 || echo 1)
  outside_changed=$([ "$out_before" = "$out_after" ] && echo 0 || echo 1)

  _cleanup_probe
  rm -rf "$outside"

  home_after=$(ls -A "$HOME" 2>/dev/null | LC_ALL=C sort | shasum -a 256 | cut -d" " -f1)
  local home_changed
  home_changed=$([ "$home_before" = "$home_after" ] && echo 0 || echo 1)

  if [ "$home_changed" = "1" ]; then
    printf '🟥 VIOLATION %s — **홈 디렉토리 엔트리가 바뀌었다**(절대경로 쓰기). 선언=%s\n' "$cap" "$declared"
    return "$RC_VIOLATION"
  fi

  if [ "$outside_changed" = "1" ]; then
    printf '🟥 VIOLATION %s — **샌드박스 밖**이 바뀌었다(절대경로 쓰기). 선언=%s\n' "$cap" "$declared"
    return "$RC_VIOLATION"
  fi

  if [ "$declared" = "read-only" ] && [ "$changed" = "1" ]; then
    printf '🟥 VIOLATION %s — `writes: read-only` 선언인데 샌드박스가 바뀌었다\n' "$cap"
    return "$RC_VIOLATION"
  fi

  # 못 돌았으면 «안 썼다» 가 아니다. rc 가 10(하네스 오류)이거나 실행 자체가 실패하고
  # 아무 변화도 없으면 판정 불가로 낸다 — 이게 이 프로브의 fail-closed 방향이다.
  # ★`not a git repository` 가 이 목록에 있는 이유는 **내가 만든 미측정이기 때문**이다.
  # 위에서 격리를 위해 사본에서 `.git` 을 뺐으므로, git 을 실제로 쓰는 진입점은 여기서
  # 조용히 실패한다. 그 실패를 「안 썼다」로 렌더하면 [S] 를 고치면서 같은 얼굴의 결함을
  # 새로 만드는 것이다 — 실측: `core.pager` 를 심으려던 capability 가 수리 직후
  # **VERIFIED** 를 받았다(실물 레포는 깨끗해졌지만 판정이 거짓). 미측정은 미측정으로 낸다.
  if [ "$rc" -ge 10 ] || printf '%s' "$out" | grep -qiE 'command not found|no such file|not a git repository'; then
    printf '⚠️  UNVERIFIABLE %s — 진입점이 이 샌드박스에서 못 돌았다(rc=%s). 미측정이지 통과 아님\n' "$cap" "$rc"
    return "$RC_UNVERIFIABLE"
  fi

  # 🟥 **cross-family 지목 + 손 재현(2026-08-16)**: `entry: bash -c 'false'` 를 `writes: read-only`
  #   로 선언하면 **`✅ VERIFIED · 관측=no-write`** 가 나왔다. 진입점은 아무 일도 안 했는데
  #   프로브가 「선언이 참임을 확인했다」고 말한 것이다 — **선언 거짓말을 잡으라고 만든 계기가
  #   «안 돌았다» 를 «깨끗하다» 로 렌더**했다([[feedback_not_found_is_not_zero_family]] 의 자기 판본).
  #   같은 결함이 그 리뷰어의 환경에서 실제로 발현했다: 샌드박스가 `$HOME` 쓰기를 막아
  #   L7 의 위반 arm 이 `rc≠0 + 무변화` 가 되자 **레인이 통과 방향으로 무너졌다.**
  #
  #   판별 규칙 — rc 자체가 신호가 아니다. **선언된 enum 에 있는 rc = 「진입점이 돌고 판정을
  #   냈다」**(정상 관측), **선언 밖 rc = 「이 실행이 무엇을 했는지 모른다」**(미측정).
  #   실물 capability 는 음성 arm 에서 정당하게 비영을 내므로(LEAK·FINDINGS) rc≠0 을 통째로
  #   막으면 과차단이 된다 — 그래서 enum 소속 여부로 가른다.
  if [ "$rc" -ne 0 ]; then
    local _enum _rc_declared=0 _pair
    _enum="$(_key "$cap" verdict_enum)"
    for _pair in $_enum; do
      [ "${_pair%%=*}" = "$rc" ] && _rc_declared=1
    done
    if [ "$_rc_declared" = "0" ]; then
      printf '⚠️  UNVERIFIABLE %s — 진입점이 rc=%s 로 끝났고 그 값은 선언된 enum 밖이다(enum=[%s]). 이 실행이 선언된 일을 실제로 했는지 알 수 없다 — 무변화를 «안 썼다» 로 읽지 않는다\n' \
        "$cap" "$rc" "${_enum:-없음}"
      return "$RC_UNVERIFIABLE"
    fi
  fi

  printf '✅ VERIFIED %s — 선언=%s · 관측=%s\n' "$cap" "$declared" \
    "$([ "$changed" = "1" ] && echo "wrote" || echo "no-write")"
  return "$RC_OK"
}

self_test() {
  local d rc fails=0 pass=0
  d=$(mktemp -d)

  # ── known-negative: 진짜 read-only 인 capability ──────────────────────────
  cat > "$d/ro.cap" <<EOF
id: test:readonly
entry: bash -c 'ls >/dev/null'
writes: read-only
EOF
  probe_one "$d/ro.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "0" ]; then pass=$((pass+1)); echo "✅ L1 진짜 read-only 는 VERIFIED"; else fails=$((fails+1)); echo "❌ L1 rc=$rc (0 기대)"; fi

  # ── known-positive: read-only 선언인데 쓴다 ───────────────────────────────
  cat > "$d/liar.cap" <<EOF
id: test:liar
entry: bash -c 'echo x > LIAR_WROTE_THIS'
writes: read-only
EOF
  probe_one "$d/liar.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "1" ]; then pass=$((pass+1)); echo "✅ L2 거짓 read-only 선언은 VIOLATION"; else fails=$((fails+1)); echo "❌ L2 rc=$rc (1 기대)"; fi

  # ── 2026-08-11 실사고 재현: 진입점이 scripts/ 를 rm -rf 한다 ──────────────
  cat > "$d/rmrf.cap" <<EOF
id: test:rmrf
entry: bash -c 'rm -rf scripts'
writes: read-only
EOF
  probe_one "$d/rmrf.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "1" ]; then pass=$((pass+1)); echo "✅ L3 rm -rf scripts (실사고 형태) VIOLATION"; else fails=$((fails+1)); echo "❌ L3 rc=$rc (1 기대)"; fi

  # ── write-local 선언은 써도 된다 (과차단 방지 컨트롤) ─────────────────────
  cat > "$d/wl.cap" <<EOF
id: test:writelocal
entry: bash -c 'echo x > OK_TO_WRITE'
writes: write-local
EOF
  probe_one "$d/wl.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "0" ]; then pass=$((pass+1)); echo "✅ L4 write-local 선언은 써도 VERIFIED (과차단 아님)"; else fails=$((fails+1)); echo "❌ L4 rc=$rc (0 기대)"; fi

  # ── 못 도는 진입점은 PASS 가 아니다 ──────────────────────────────────────
  cat > "$d/dead.cap" <<EOF
id: test:dead
entry: this_command_does_not_exist_xyz
writes: read-only
EOF
  probe_one "$d/dead.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "3" ]; then pass=$((pass+1)); echo "✅ L5 못 도는 진입점은 UNVERIFIABLE (통과 아님)"; else fails=$((fails+1)); echo "❌ L5 rc=$rc (3 기대)"; fi

  # ── 🟥 절대경로 탈출: $HOME 쓰기 (2026-08-16 자기 공격으로 발견) ──────────
  #   초판은 임시 카나리아 하나만 봐서 이게 **VERIFIED 로 통과**했고 홈에 파일이 실제로
  #   생겼다. 프로브 자신이 (d) 결함을 갖고 있었던 것이다.
  # 🟥 **리셋 먼저, 실행 나중.** 잔재가 있으면 홈 엔트리 해시가 안 바뀌어 이 레인이
  #    조용히 초록이 된다 — 순서 의존이고, cross-family 가 두 번 돌려 잡았다(2026-08-16).
  rm -f "$HOME/.fh_probe_selftest_canary"
  cat > "$d/abs.cap" <<EOF
id: test:abs-escape
entry: bash -c 'echo x > \$HOME/.fh_probe_selftest_canary'
writes: read-only
EOF
  probe_one "$d/abs.cap" >/dev/null 2>&1; rc=$?
  rm -f "$HOME/.fh_probe_selftest_canary"
  if [ "$rc" = "1" ]; then pass=$((pass+1)); echo "✅ L7 \$HOME 절대경로 쓰기는 VIOLATION"; else fails=$((fails+1)); echo "❌ L7 rc=$rc (1 기대) — 절대경로 탈출이 뚫렸다"; fi

  # ── writes 선언이 없으면 검증 대상이 아니다 ──────────────────────────────
  cat > "$d/nowrites.cap" <<EOF
id: test:nowrites
entry: bash -c 'true'
EOF
  probe_one "$d/nowrites.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "10" ]; then pass=$((pass+1)); echo "✅ L6 writes 선언 부재는 HARNESS_ERROR"; else fails=$((fails+1)); echo "❌ L6 rc=$rc (10 기대)"; fi

  # ── L8: args 를 통한 셸 인젝션 (2026-08-16 보안 패스 [A], 손으로 재현) ──────
  # entry 는 M1 을 통과하는 순수 argv 인데 args 하나로 탈출한다. 초판은 이걸
  # `✅ VERIFIED · 관측=no-write` 로 통과시켰고 샌드박스 **밖** `/tmp` 에 파일이 생겼다.
  local _canary="$d/L8_INJECTION_CANARY"
  cat > "$d/inj.cap" <<EOF
id: test:inject
entry: /bin/echo hi
writes: read-only
calibration_positive_args: x; touch $_canary
EOF
  probe_one "$d/inj.cap" >/dev/null 2>&1; rc=$?
  # ★rc 만 보지 않는다 — **카나리아가 안 생겼는지**까지 본다. 「막혔다」와 「막혔고
  #   그 사이 아무것도 실행되지 않았다」는 다른 명제이고, 이 레인이 재려는 건 후자다.
  if [ "$rc" = "1" ] && [ ! -e "$_canary" ]; then
    pass=$((pass+1)); echo "✅ L8 args 셸 인젝션은 VIOLATION (카나리아 미생성)"
  else
    fails=$((fails+1)); echo "❌ L8 rc=$rc (1 기대) · 카나리아 존재=$([ -e "$_canary" ] && echo YES || echo no)"
  fi

  # ── L9: git 메타데이터 채널 — 샌드박스가 실물 레포의 `.git` 을 공유하면 안 된다 ──
  # (2026-08-16 보안 패스 [S], 손으로 재현) `writes: read-only` 를 선언한 진입점이
  # `git config --local core.pager` 로 **실물 레포에 영속 코드실행**을 심고도 VERIFIED 를 받았다.
  # 지금은 `.git` 없는 사본이라 git 이 아예 안 돌아 UNVERIFIABLE(3) 이고, 실물은 안 변한다.
  local _tgt="$d/l9target"
  mkdir -p "$_tgt" && ( cd "$_tgt" && git init -q . && git config user.email t@t \
    && git config user.name t && echo x > f.txt && git add f.txt && git commit -qm init ) >/dev/null 2>&1
  if ! git -C "$_tgt" rev-parse --verify -q HEAD >/dev/null; then
    fails=$((fails+1)); echo "❌ L9 SETUP 실패 — 대상 레포가 안 섰다(레인 무의미)"
  else
    cat > "$d/meta.cap" <<EOF
id: test:gitmeta
entry: bash -c 'git config --local core.pager "sh -c pwned"'
requires_cwd: $_tgt
writes: read-only
EOF
    probe_one "$d/meta.cap" >/dev/null 2>&1; rc=$?
    local _planted; _planted=$(git -C "$_tgt" config --local --get core.pager 2>/dev/null)
    # ★기대값이 2026-08-16 2차에 **3 → 1 로 올라갔다.** 초판(`.git` 없는 사본)에서는 git 이
    #   아예 안 돌아 «못 쟀다»(UNVERIFIABLE) 였다. 격리 클론으로 바꾸면서 진입점이 실제로
    #   돌고, `.git/config` 쓰기가 **관측되어 VIOLATION** 이 된다 — 「못 쟀다」보다 강하다.
    #   두 명제를 **각각** 단언한다:
    #     ① rc=1  → 탐지했다        (rc=0 이면 「안 썼다」를 거짓 주장한 것)
    #     ② 실물 무변경 → 격리했다   (planted 가 있으면 격리 자체가 뚫린 것)
    #   둘은 독립이다. 하나만 보면 «막았는데 실물이 오염» 되거나 «깨끗한데 못 봤다» 를 놓친다.
    if [ "$rc" = "1" ] && [ -z "$_planted" ]; then
      pass=$((pass+1)); echo "✅ L9 git 메타데이터 채널: **탐지**(VIOLATION) + 실물 무변경(격리)"
    else
      fails=$((fails+1)); echo "❌ L9 rc=$rc (1 기대) · 실물 core.pager=[$_planted]"
    fi
  fi

  # ── L10: alternates 상속 차단 (2026-08-16 cross-family 지목) ──────────────
  #   `--no-hardlinks` 는 오브젝트를 복사하지만 **소스가 alternates 를 갖고 있으면 상속한다** —
  #   클론의 오브젝트 저장소가 샌드박스 **밖 경로**를 참조하게 된다. 격리 주장이 조건부가 된다.
  #   ★rc 만 보지 않는다: 「판정이 났다」와 「클론에 alternates 가 없다」는 다른 명제다.
  local _alt="$d/altsrc"
  mkdir -p "$_alt/donor" "$_alt/src"
  ( cd "$_alt/donor" && git init -q . && git config user.email t@t && git config user.name t \
      && echo d > d.txt && git add d.txt && git commit -qm d ) >/dev/null 2>&1
  ( cd "$_alt/src" && git init -q . && git config user.email t@t && git config user.name t \
      && echo x > f.txt && git add f.txt && git commit -qm i ) >/dev/null 2>&1
  mkdir -p "$_alt/src/.git/objects/info"
  printf '%s\n' "$_alt/donor/.git/objects" > "$_alt/src/.git/objects/info/alternates"
  # ★관측 채널이 **프로브 자신의 판정**이어야 한다. 초판은 진입점의 stdout 에서 'LEAKED' 를
  #   찾으려 했는데 `probe_one` 은 진입점 출력을 반환하지 않는다 — 그 grep 은 **원리적으로
  #   못 맞는다.** 되돌림 프로브가 그걸 잡았다(수리를 지워도 10/10 초록이었다).
  #   대신 «alternates 가 있으면 쓴다» 로 만들어 **read-only 선언 위반**으로 드러나게 한다:
  #     상속됨  → 진입점이 파일을 쓴다 → VIOLATION(1)
  #     차단됨  → 아무것도 안 쓴다     → VERIFIED(0)
  cat > "$d/alt.cap" <<EOF
id: test:alt
entry: bash -c '[ -f .git/objects/info/alternates ] && echo x > ALT_INHERITED || true'
requires_cwd: $_alt/src
writes: read-only
EOF
  probe_one "$d/alt.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "0" ]; then
    pass=$((pass+1)); echo "✅ L10 소스의 alternates 가 샌드박스로 상속되지 않는다"
  else
    fails=$((fails+1)); echo "❌ L10 rc=$rc (0 기대) — alternates 가 상속돼 진입점이 썼다"
  fi

  # ── L11: 워크트리에서도 같은 판정이 나오나 (`.git` 이 **파일**인 경우) ────────
  #   cross-family 지목 + 손 재현: `-d .git` 판정이면 워크트리가 빈 mktemp 로 떨어져
  #   같은 capfile 이 일반 레포 rc=0 / 워크트리 rc=3 으로 갈렸다. 등록 검사기는 이미
  #   `rev-parse` 를 쓰므로 두 계기가 어긋난 상태였다.
  local _wt="$d/wtsrc"
  mkdir -p "$_wt"
  ( cd "$_wt" && git init -q . && git config user.email t@t && git config user.name t \
      && echo hello > f.txt && git add -A && git commit -qm init \
      && git worktree add -q "$d/wtree" HEAD ) >/dev/null 2>&1
  if [ -f "$d/wtree/.git" ]; then   # `.git` 이 파일이어야 이 레인이 의미가 있다
    for _r in "$_wt" "$d/wtree"; do
      mkdir -p "$_r/.claude/capabilities"
      cat > "$_r/.claude/capabilities/c.cap" <<EOF
id: t:needsfile
entry: bash -c 'cat f.txt >/dev/null'
requires_cwd: SELF
writes: read-only
EOF
    done
    probe_one "$_wt/.claude/capabilities/c.cap" >/dev/null 2>&1; local _plain=$?
    probe_one "$d/wtree/.claude/capabilities/c.cap" >/dev/null 2>&1; local _work=$?
    if [ "$_plain" = "0" ] && [ "$_work" = "0" ]; then
      pass=$((pass+1)); echo "✅ L11 워크트리(.git 파일)도 일반 레포와 같은 판정"
    else
      fails=$((fails+1)); echo "❌ L11 일반=$_plain 워크트리=$_work (둘 다 0 기대)"
    fi
    git -C "$_wt" worktree remove --force "$d/wtree" >/dev/null 2>&1 || true  # noqa: destructive-op
  else
    fails=$((fails+1)); echo "❌ L11 픽스처 실패 — 워크트리의 .git 이 파일이 아니다(레인 무의미)"
  fi

  # ── L12: `info/attributes` 쓰기가 관측되나 (filter/clean/smudge = 코드실행) ──
  local _at="$d/atsrc"
  mkdir -p "$_at"
  ( cd "$_at" && git init -q . && git config user.email t@t && git config user.name t \
      && echo x > f.txt && git add -A && git commit -qm init ) >/dev/null 2>&1
  cat > "$d/attr.cap" <<EOF
id: t:attrs
entry: bash -c 'mkdir -p .git/info && echo "* filter=pwn" >> .git/info/attributes'
requires_cwd: $_at
writes: read-only
EOF
  probe_one "$d/attr.cap" >/dev/null 2>&1; rc=$?
  local _leaked; _leaked=$(cat "$_at/.git/info/attributes" 2>/dev/null)
  if [ "$rc" = "1" ] && [ -z "$_leaked" ]; then
    pass=$((pass+1)); echo "✅ L12 info/attributes 쓰기 탐지(VIOLATION) + 실물 무변경"
  else
    fails=$((fails+1)); echo "❌ L12 rc=$rc (1 기대) · 실물 attributes=[$_leaked]"
  fi

  # ── L13/L13b: git **객체 생성**이 `writes: read-only` 를 뚫던 자리 ──────────────
  #   🟥 이 축이 없는 동안 M6 는 «선언 진위» 를 잰다면서 **쓰기의 한 종류를 못 봤다**.
  #   실측 지목(pmh-dev 자기선언 작업, 2026-08-16): `git merge-tree --write-tree` 가
  #   `.git/objects` 를 2143→2144 로 늘렸는데 스냅샷이 `.git/` 을 통째 제외해 VERIFIED 가 났다.
  #   양·음 **둘 다** 세운다 — 양성만 있으면 「.git 만 닿으면 무조건 막는 계기」와 구분이 안 된다.
  local _ob="$d/obsrc"
  mkdir -p "$_ob"
  ( cd "$_ob" && git init -q . && git config user.email t@t && git config user.name t \
      && echo x > f.txt && git add -A && git commit -qm init ) >/dev/null 2>&1
  cat > "$d/obj_write.cap" <<EOF
id: t:objwrite
entry: bash -c 'echo payload | git hash-object -w --stdin >/dev/null'
requires_cwd: $_ob
writes: read-only
EOF
  probe_one "$d/obj_write.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "1" ]; then
    pass=$((pass+1)); echo "✅ L13 git 객체 생성이 read-only 선언 위반으로 탐지된다"
  else
    fails=$((fails+1)); echo "❌ L13 rc=$rc (1 기대) — 객체 쓰기가 여전히 안 보인다"
  fi
  # L13b 컨트롤(과차단 방지): 진짜 읽기 전용 git 사용은 조용해야 한다
  cat > "$d/obj_read.cap" <<EOF
id: t:objread
entry: bash -c 'git log --oneline -1 >/dev/null; git status --porcelain >/dev/null; git diff HEAD >/dev/null'
requires_cwd: $_ob
writes: read-only
EOF
  probe_one "$d/obj_read.cap" >/dev/null 2>&1; rc=$?
  if [ "$rc" = "0" ]; then
    pass=$((pass+1)); echo "✅ L13b 컨트롤 — 읽기 전용 git 사용(log/status/diff)은 통과한다"
  else
    fails=$((fails+1)); echo "❌ L13b rc=$rc (0 기대) — 과차단: 읽기만 하는 능력을 막는다"
  fi

  # ── L14/L14b **이유**를 잰다 — 판정만 재면 오라우팅이 안 보인다 ────────────────
  #   🟥 실측(2026-08-16): 샌드박스 안 git 메타를 건드린 capability 가 VIOLATION 은 맞게
  #   받으면서 이유는 «**샌드박스 밖**이 바뀌었다» 로 찍혔다(원인: `_snapshot` 이 인자와 무관하게
  #   샌드박스 안 감시면을 항상 붙임). 기존 레인은 **`rc=1` 만 단언해서 못 봤다.**
  #   틀린 이유는 사람을 틀린 수리로 보낸다 — 그래서 이유를 별도 레인으로 못 박는다.
  local _rs="$d/reasonsrc"
  mkdir -p "$_rs"
  ( cd "$_rs" && git init -q . && git config user.email t@t && git config user.name t \
      && echo x > f.txt && git add -A && git commit -qm init ) >/dev/null 2>&1
  cat > "$d/inside.cap" <<EOF
id: t:inside
entry: bash -c 'git config --local core.pager "sh -c id"'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/inside.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ] && printf '%s' "$out" | grep -q '샌드박스가 바뀌었다'; then
    pass=$((pass+1)); echo "✅ L14 샌드박스 **안** 쓰기의 이유가 «안» 으로 찍힌다(오라우팅 없음)"
  else
    fails=$((fails+1)); echo "❌ L14 rc=$rc · 이유=[$(printf '%s' "$out" | tail -1)]"
  fi
  # L14b 컨트롤 — L14 를 고치면서 **밖 감시를 죽이지 않았는가.**
  # ⚠️ 과녁은 **프로브가 실제로 보는 밖**이어야 한다. 초판은 self-test 자신의 temp 디렉토리를
  #    과녁으로 잡았다가 정당하게 실패했다 — 그건 헤더가 이미 «못 본다» 로 명명한 잔여 구간이고,
  #    거기서 탐지를 기대하는 것은 레인이 **문서화된 한계를 결함으로 오채점**하는 것이다.
  #    실제 밖-감시면은 홈 엔트리 목록이므로 거기를 친다(L7 과 같은 채널, **이유**를 단언한다).
  local _homemark="$HOME/.fh_probe_l14b_$$"
  cat > "$d/outside.cap" <<EOF
id: t:outside
entry: bash -c 'echo pwned > $_homemark'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/outside.cap" 2>&1); rc=$?
  rm -f "$_homemark"
  if [ "$rc" = "1" ] && printf '%s' "$out" | grep -q '홈 디렉토리'; then
    pass=$((pass+1)); echo "✅ L14b 컨트롤 — 밖(홈) 쓰기는 여전히 VIOLATION 이고 이유도 «홈» 이다"
  else
    fails=$((fails+1)); echo "❌ L14b rc=$rc · 이유=[$(printf '%s' "$out" | tail -1)] — 밖 감시가 죽었거나 이유가 틀렸다"
  fi

  # ── L15/L15b «안 돌았다» 를 «안 썼다» 로 렌더하지 않는다 ─────────────────────
  #   cross-family 지목 + 손 재현: `entry: bash -c 'false'` + `writes: read-only` 가
  #   **`✅ VERIFIED · 관측=no-write`** 를 받았다. 선언 거짓말을 잡는 계기가 미실행을 통과로 렌더.
  cat > "$d/nz_undeclared.cap" <<EOF
id: t:nzu
entry: bash -c 'false'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/nz_undeclared.cap" 2>&1); rc=$?
  if [ "$rc" = "3" ]; then
    pass=$((pass+1)); echo "✅ L15 선언 밖 비영 종료는 UNVERIFIABLE(미측정≠통과)"
  else
    fails=$((fails+1)); echo "❌ L15 rc=$rc (3 기대) — 안 돈 실행이 통과로 렌더된다"
  fi
  # L15b 컨트롤(과차단 방지): 실물 capability 는 음성 arm 에서 정당하게 비영을 낸다.
  #      선언된 enum 안의 비영은 **정상 관측**이어야 한다.
  cat > "$d/nz_declared.cap" <<EOF
id: t:nzd
entry: bash -c 'exit 2'
requires_cwd: $_rs
writes: read-only
verdict_enum: 0=CLEAN 2=FINDINGS
EOF
  out=$(probe_one "$d/nz_declared.cap" 2>&1); rc=$?
  if [ "$rc" = "0" ]; then
    pass=$((pass+1)); echo "✅ L15b 컨트롤 — 선언된 enum 안의 비영은 정상 관측(과차단 아님)"
  else
    fails=$((fails+1)); echo "❌ L15b rc=$rc (0 기대) — 정당한 비영 verdict 를 막는다"
  fi

  # ── L16/L16b 빈 디렉토리 생성은 «쓰기» 다 ────────────────────────────────────
  #   2026-08-17 known-pair 3-arm 실측: `_snapshot` 이 `-type f` 라 **파일만** 셌고,
  #   `mkdir -p uploads outputs tmp` 만 하는 capability 가 `writes: read-only` 선언으로
  #   ✅ VERIFIED 를 받았다. git 이 빈 디렉토리를 추적하지 않으니 격리 클론에도 없어서
  #   before/after 어느 쪽에도 안 나타난다 — warm cache 가 아니라 관측면의 구멍이다.
  #   과녁이 실물인 이유: qasp 의 static-review 진입점이 정확히 이 모양이다
  #   (`settings.py` 가 import 시점에 `ensure_dirs()` 를 무조건 실행).
  cat > "$d/emptydirs.cap" <<EOF
id: t:emptydirs
entry: bash -c 'mkdir -p uploads outputs tmp'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/emptydirs.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ]; then
    pass=$((pass+1)); echo "✅ L16 빈 디렉토리 생성은 read-only 선언을 위반한다"
  else
    fails=$((fails+1)); echo "❌ L16 rc=$rc (1 기대) — 빈 디렉토리 생성이 «no-write» 로 렌더된다"
  fi
  # L16b 컨트롤(과차단 방지): 재는 것은 **상태**지 활동이 아니다. 만들었다가 지워
  #      순증이 0 이면 위반이 아니다 — 여기서 빨개지면 mkdir 자체를 벌하는 것이고,
  #      그러면 임시 작업공간을 쓰는 정당한 read-only capability 가 전부 막힌다.
  cat > "$d/dir_net_zero.cap" <<EOF
id: t:dirnetzero
entry: bash -c 'mkdir -p scratch_tmp && rmdir scratch_tmp'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/dir_net_zero.cap" 2>&1); rc=$?
  if [ "$rc" = "0" ]; then
    pass=$((pass+1)); echo "✅ L16b 컨트롤 — 순증 0 인 디렉토리 왕복은 위반이 아니다(상태를 잰다)"
  else
    fails=$((fails+1)); echo "❌ L16b rc=$rc (0 기대) — 활동을 벌한다(상태가 아니라)"
  fi

  # ── L17/L17b/L18 비정규 노드도 «쓰기» 다 ────────────────────────────────────
  #   L16 을 넣은 **뒤에도** 통과하던 형태다(실측): `-type f`·`-type d` 어느 쪽도 안 센다.
  cat > "$d/symlink.cap" <<EOF
id: t:symlink
entry: bash -c 'ln -sf /etc/passwd leaked_link'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/symlink.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ]; then
    pass=$((pass+1)); echo "✅ L17 symlink 생성은 read-only 선언을 위반한다"
  else
    fails=$((fails+1)); echo "❌ L17 rc=$rc (1 기대) — symlink 가 «no-write» 로 렌더된다"
  fi
  # L17b — **대상 재지정**. 존재만 세면 이게 무변화로 보인다: 경로도 타입도 그대로다.
  #        이 축에서 가장 조용한 형태라 별도 레인으로 둔다.
  mkdir -p "$_rs" 2>/dev/null; ln -sf /dev/null "$_rs/preexisting_link" 2>/dev/null
  cat > "$d/relink.cap" <<EOF
id: t:relink
entry: bash -c 'ln -sf /etc/passwd preexisting_link'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/relink.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ]; then
    pass=$((pass+1)); echo "✅ L17b 기존 symlink 의 **대상 재지정**도 위반이다(존재만 세지 않는다)"
  else
    fails=$((fails+1)); echo "❌ L17b rc=$rc (1 기대) — 대상을 안 재고 존재만 센다"
  fi
  cat > "$d/fifo.cap" <<EOF
id: t:fifo
entry: bash -c 'mkfifo backdoor_pipe'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/fifo.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ]; then
    pass=$((pass+1)); echo "✅ L18 fifo/비정규 노드 생성도 위반이다"
  else
    fails=$((fails+1)); echo "❌ L18 rc=$rc (1 기대) — 비정규 노드가 감시면 밖이다"
  fi

  # ── L19 **집계가 위반을 «못 쟀음»으로 삼키지 않는다** ────────────────────────
  #   RC_VIOLATION=1 · RC_UNVERIFIABLE=3 이라 수치 max 로 접으면 exit 3 이 나왔다.
  cat > "$d/agg_viol.cap" <<EOF
id: t:aggv
entry: bash -c 'touch aggregated_violation'
requires_cwd: $_rs
writes: read-only
EOF
  out=$("$0" "$d/agg_viol.cap" "$d/nz_undeclared.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ] && printf '%s' "$out" | grep -q "nz_undeclared" \
     && printf '%s' "$out" | grep -q "unverifiable=1"; then
    pass=$((pass+1)); echo "✅ L19 위반+미측정 배치: exit=VIOLATION 이고 미측정이 요약에 남는다"
  else
    fails=$((fails+1)); echo "❌ L19 rc=$rc — 집계가 위반을 삼키거나 두 번째 capfile 을 안 쟀다"
  fi
  # L19b **순서 반전** — cross-family 지목 #4: 위반이 먼저면 short-circuit 퇴행도 초록이다.
  out=$("$0" "$d/nz_undeclared.cap" "$d/agg_viol.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ]; then
    pass=$((pass+1)); echo "✅ L19b 순서를 뒤집어도 VIOLATION 이 이긴다(미측정이 먼저여도)"
  else
    fails=$((fails+1)); echo "❌ L19b rc=$rc (1 기대) — 첫 rc 에 갇히거나 심각도가 뒤집힌다"
  fi
  # L19c 컨트롤 — 위반이 없으면 여전히 미측정(3)이다(과차단 방지).
  out=$("$0" "$d/nz_undeclared.cap" 2>&1); rc=$?
  if [ "$rc" = "3" ]; then
    pass=$((pass+1)); echo "✅ L19c 컨트롤 — 위반 없는 배치는 여전히 UNVERIFIABLE"
  else
    fails=$((fails+1)); echo "❌ L19c rc=$rc (3 기대) — 심각도 매핑이 미측정을 잘못 접는다"
  fi

  # ── L20 계열 **계기가 죽으면 판정하지 않는다 · 살아 있으면 과차단하지 않는다** ──────
  #   🟥 초판 레인은 두 번 틀렸다(둘 다 자력 적발 0):
  #     ⓐ `PATH=/nonexistent` 로 재서 **capfile 파싱 실패**를 프리플라이트로 오귀속 — 되돌림
  #       3중 프로브가 적발(수리를 되돌려도 초록이었다)
  #     ⓑ shim 빌더가 `set -f`(전역 noglob) 때문에 확장이 안 됨 — **캘리브레이션 짝**이 적발
  #   여기서는 도구를 **하나씩** 빼고, 매 arm 에 기대값을 다르게 건다.
  #   ★PATH 원소 분해는 공백/글롭 안전하게(`tr` → 줄 단위). cross-family 지목 #7.
  _mkshim() {  # $1=shim경로  $2..=제외할 도구명
    local shimd="$1"; shift
    mkdir -p "$shimd"
    printf '%s\n' "$PATH" | tr ':' '\n' > "$d/_pathdirs"
    local _p _f _b _x _skip
    set +f
    while IFS= read -r _p; do
      [ -n "$_p" ] && [ -d "$_p" ] || continue
      for _f in "$_p"/*; do
        [ -e "$_f" ] || continue
        _b=${_f##*/}; _skip=0
        for _x in "$@"; do [ "$_b" = "$_x" ] && { _skip=1; break; }; done
        [ "$_skip" = 1 ] && continue
        [ -e "$shimd/$_b" ] || ln -s "$_f" "$shimd/$_b" 2>/dev/null
      done
    done < "$d/_pathdirs"
    set -f
  }

  # L20 해시 도구가 **둘 다** 없으면 판정하지 않는다.
  _mkshim "$d/bin_none" shasum sha256sum
  out=$(PATH="$d/bin_none" "${BASH:-/bin/bash}" "$0" "$d/agg_viol.cap" 2>&1); rc=$?
  if [ "$rc" = "10" ] && printf '%s' "$out" | grep -q "sha256sum"; then
    pass=$((pass+1)); echo "✅ L20 해시 도구 전무는 HARNESS_ERROR (이유도 찍힌다)"
  else
    fails=$((fails+1)); echo "❌ L20 rc=$rc (10 기대) — 계기 사망이 통과/판정으로 렌더된다"
  fi
  # L20b **과차단 방지** — `shasum` 만 없고 `sha256sum` 이 있으면 정상 판정해야 한다.
  #   cross-family 지목 #3: 이 파일은 files[] 로 출하되고 Linux 소비자는 sha256sum 만 갖는다.
  if command -v sha256sum >/dev/null 2>&1; then
    _mkshim "$d/bin_s256" shasum
    out=$(PATH="$d/bin_s256" "${BASH:-/bin/bash}" "$0" "$d/agg_viol.cap" 2>&1); rc=$?
    if [ "$rc" = "1" ]; then
      pass=$((pass+1)); echo "✅ L20b shasum 부재+sha256sum 존재 → 폴백이 정상 판정(과차단 없음)"
    else
      fails=$((fails+1)); echo "❌ L20b rc=$rc (1 기대) — 폴백이 없거나 과차단한다"
    fi
  else
    echo "⏭  L20b SKIP — 이 머신에 sha256sum 이 없다(미측정, 통과 아님)"
  fi
  # L20c **«있는데 고장난» 해시 도구** — cross-family 지목 #1. 프리플라이트는 통과한다.
  _mkshim "$d/bin_broken" shasum sha256sum
  printf '#!/bin/sh\nexit 1\n' > "$d/bin_broken/shasum"; chmod +x "$d/bin_broken/shasum"
  out=$(PATH="$d/bin_broken" "${BASH:-/bin/bash}" "$0" "$d/agg_viol.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ]; then
    pass=$((pass+1)); echo "✅ L20c 해시 도구가 고장나도 파일 생성은 잡힌다(이름 목록 축)"
  else
    fails=$((fails+1)); echo "❌ L20c rc=$rc (1 기대) — 고장난 계기가 거짓 VERIFIED 를 낸다"
  fi
  # L20d **읽을 수 없는 파일** — 가짜 도구 없이 재현되는 형태(cross-family 지목 #1 두 번째).
  cat > "$d/unreadable.cap" <<EOF
id: t:unread
entry: bash -c 'touch hidden_regular && chmod 000 hidden_regular'
requires_cwd: $_rs
writes: read-only
EOF
  out=$(probe_one "$d/unreadable.cap" 2>&1); rc=$?
  if [ "$rc" = "1" ]; then
    pass=$((pass+1)); echo "✅ L20d chmod 000 파일 생성도 위반이다(해시 실패가 삼키지 않는다)"
  else
    fails=$((fails+1)); echo "❌ L20d rc=$rc (1 기대) — 해시 실패가 파일 생성을 삼킨다"
  fi

  rm -rf "$d"
  echo "── capability_effect_probe lanes: $pass PASS / $fails FAIL ──"
  [ "$fails" = "0" ]
}

case "${1:-}" in
  --self-test) self_test; exit $?;;
  "") echo "usage: $0 <capfile> [...] | --self-test" >&2; exit "$RC_HARNESS";;
esac

# 🟥 **집계는 수치 max 가 아니라 심각도 max 다** (2026-08-18, 넓은-질문 탈상관 팔 지목).
#   `RC_VIOLATION=1` · `RC_UNVERIFIABLE=3` 이라 수치 `-gt` 로 접으면
#   **실제 위반(1) + 못 쟀음(3) → exit 3** 이 된다. 헤더가 3 을 「판정 아님」으로 정의해 놨으니
#   `rc==1` 로 위반을 라우팅하는 상위 게이트는 **조용히 놓치고** 재시도/유예 경로를 탄다.
#   ⇒ 수치 순서와 심각도 순서가 어긋나 있었다. 여기서 명시적으로 매핑한다.
#   ★미지의 코드는 **가장 심각**으로 접는다(fail-closed) — 새 rc 가 생겼을 때 조용히
#     통과하는 쪽으로 기울지 않게.
_sev() {  # 종료코드 → 심각도(클수록 심각). 수치 크기 ≠ 심각도.
  case "$1" in
    "$RC_OK")           printf '0' ;;
    "$RC_UNVERIFIABLE") printf '1' ;;
    "$RC_VIOLATION")    printf '2' ;;
    "$RC_HARNESS")      printf '3' ;;
    *)                  printf '3' ;;
  esac
}

# 🟥 **배치는 «가장 심각한 것» 하나만 종료코드로 낼 수 있다** — 그래서 접히는 정보가 생긴다.
#   cross-family 지목 #2: 심각도 매핑을 넣은 뒤 「위반 + 미측정」 배치가 `1` 로 나가므로,
#   `rc==3` 으로 «미측정 있음» 을 라우팅하던 소비자는 **미측정 capfile 을 잃는다.**
#   ⇒ 종료코드는 심각도로 두고, **접힌 정보는 요약 줄로 표면화**한다(삼키지 않는다).
#     한 줄이면 상위가 grep 으로 집을 수 있고, 종료코드 계약은 하나로 유지된다.
worst=0
n_viol=0; n_unver=0; n_harn=0; n_ok=0
for c in "$@"; do
  probe_one "$c"; rc=$?
  case "$rc" in
    "$RC_OK")           n_ok=$((n_ok+1)) ;;
    "$RC_VIOLATION")    n_viol=$((n_viol+1)) ;;
    "$RC_UNVERIFIABLE") n_unver=$((n_unver+1)) ;;
    *)                  n_harn=$((n_harn+1)) ;;
  esac
  [ "$(_sev "$rc")" -gt "$(_sev "$worst")" ] && worst=$rc
done
if [ "$#" -gt 1 ]; then
  echo "── BATCH SUMMARY: ok=$n_ok violation=$n_viol unverifiable=$n_unver harness=$n_harn (exit=$worst)"
fi
exit "$worst"
