diff --git a/.github/workflows/adversary-gate.yml b/.github/workflows/adversary-gate.yml
index b0f28d6..c3f0359 100644
--- a/.github/workflows/adversary-gate.yml
+++ b/.github/workflows/adversary-gate.yml
@@ -77,11 +77,19 @@ jobs:
echo "merge_group:adversary check run 已写回 success(EXPECTED_SKIP)"
- name: 预检 PR 是否含 specs/** 变更(gh + github.token)
+ # 2026-08-31 精化(AC-14 豁免谓词确定性派生):specs/** 命中只统计
+ # **可审计 spec 体**——变更路径所属 specs/
/ 在 PR head 上存在
+ # spec.md(adversary 目标契约 specs//{spec.md,suite/,run-suite.sh}
+ # 的判别面)。specs/ 下无 spec.md 的目录(如 test-freeze 的 MANIFEST
+ # 派生哈希账本)是机器可校验资产而非红队审计对象——其完整性由
+ # 自身哈希链/签名执法,红队无面可攻。判定仍由 diff 路径集 + head 树
+ # 确定性派生(禁人工打标);API 失败负向断言不变(fail-closed)。
if: github.event_name == 'pull_request'
id: specspr
env:
GH_TOKEN: ${{ github.token }}
PR_API: "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}"
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
set +e
@@ -93,10 +101,60 @@ jobs:
echo "has_specs=true" >> "$GITHUB_OUTPUT"
echo "::warning::取 PR files 失败(负向断言:视为 spec 变更)"
else
- HASSPECS=$(echo "$FILES" | python3 -c "import json,sys;files=json.load(sys.stdin);print('true' if any(f.startswith('specs/') for f in files) else 'false')")
- echo "has_specs=$HASSPECS" >> "$GITHUB_OUTPUT"
+ SPECS_HITS=$(echo "$FILES" | python3 -c "import json,sys;files=json.load(sys.stdin);print('\n'.join(f for f in files if f.startswith('specs/')))")
+ if [[ -z "$SPECS_HITS" ]]; then
+ echo "has_specs=false" >> "$GITHUB_OUTPUT"
+ else
+ # 逐变更 specs 目录核验可审计性(head 树上 specs//spec.md 存在)
+ AUDITABLE=0; ADJACENT_DIRS=""
+ for d in $(echo "$SPECS_HITS" | sed 's|^specs/||' | cut -d/ -f1 | sort -u); do
+ [[ -n "$d" ]] || continue
+ if gh api "repos/${{ github.repository }}/contents/specs/$d/spec.md?ref=$HEAD_SHA" >/dev/null 2>&1; then
+ AUDITABLE=1
+ else
+ ADJACENT_DIRS="$ADJACENT_DIRS $d"
+ fi
+ done
+ # specs/ 根下直挂文件(无目录层)→ fail-closed 视为可审计(红队判别面外不放行)
+ if echo "$SPECS_HITS" | grep -qv '^specs/[^/]*/'; then AUDITABLE=1; fi
+ if [[ $AUDITABLE -eq 1 ]]; then
+ echo "has_specs=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "has_specs=adjacent" >> "$GITHUB_OUTPUT"
+ echo "adjacent_dirs=$(echo $ADJACENT_DIRS)" >> "$GITHUB_OUTPUT"
+ echo "specs 邻接变更(无可审计 spec 体):$ADJACENT_DIRS"
+ fi
+ fi
fi
+ - name: specs 邻接变更(无 spec.md 审计体)——写 success check run 放行
+ # EXPECTED_SKIP(AC-14):specs/** 命中但全部属无 spec.md 的目录
+ # (MANIFEST/README 类派生资产)——确定性派生豁免,非人工打标。
+ if: steps.specspr.outputs.has_specs == 'adjacent'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ SUMMARY="specs/** 邻接变更但无可审计 spec 体(${{ steps.specspr.outputs.adjacent_dirs }} 无 spec.md,adversary 目标契约不成立):EXPECTED_SKIP=True(AC-14 确定性派生豁免——哈希账本/MANIFEST 类资产由自身哈希链执法)"
+ python3 - "$SUMMARY" > "$RUNNER_TEMP/check_body.json" <<'PYEOF3'
+ import json, sys, datetime as dt
+ summary = sys.argv[1]
+ json.dump({
+ "name": "adversary",
+ "head_sha": "${{ github.event.pull_request.head.sha }}",
+ "status": "completed",
+ "conclusion": "success",
+ "completed_at": dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "output": {"title": "adversary: skipped (specs-adjacent, no auditable spec body)", "summary": summary},
+ }, sys.stdout)
+ PYEOF3
+ curl -fsS -X POST \
+ -H "Authorization: Bearer $GH_TOKEN" \
+ -H "Accept: application/vnd.github+json" \
+ "https://api.github.com/repos/${{ github.repository }}/check-runs" \
+ -d @"$RUNNER_TEMP/check_body.json" \
+ && echo "specs 邻接变更:adversary check run 已写回 success(EXPECTED_SKIP)"
+
- name: 非 specs PR——写 success check run 并放行(github.token)
if: steps.specspr.outputs.has_specs == 'false'
env: