#!/usr/bin/env bash
# AI Review Runner — 用 Claude Code CLI 跑 PR 审查
#
# 优势（vs 直接调 API）：
#   - 不用管 API key 轮换、SDK 依赖、prompt caching
#   - skills 自动加载（.agents/skills/code-review/SKILL.md）
#   - CLI 自带的 thinking / 缓存 / 结构化输出已就绪
#
# 用法：
#   PR_NUMBER=123 AI_REVIEW_MODE=hard-rules-block bash scripts/ops/ai-review-runner.sh
#
# 必填环境变量：
#   PR_NUMBER         PR 编号
#   AI_REVIEW_MODE    hard-rules-block | batch-summary | release-risk
#   GITEA_API_TOKEN   Gitea token（PR 读 + 评论写）
#
# 可选环境变量：
#   GITEA_BASE_URL        默认 http://43.130.59.228
#   GITEA_OWNER           默认 FFAIWorkspace
#   GITEA_REPO            默认 workspace
#   AI_REVIEW_DRY_RUN     非空时不写回 Gitea，只打印
#   AI_REVIEW_DUMP_PROMPT 非空时只打印最终 PROMPT 然后 exit 0，不调 claude（本地 prompt 调试）
#   AI_REVIEW_FORCE       非空时跳过 diff hash 幂等短路（强制重跑）
#
# 退出码：
#   0  无硬阻断问题
#   1  hard-rules-block 模式发现 BLOCK
#   2  脚本错误
#
# PRD 合理性审核（按需）：
#   PR diff 含 docs/modules/**/01-prd.md 时，自动注入 prompts/prd-rationality.md 到主 prompt
#   严重度阈值在该文件里强约束（只产 hard_block / risk，最多 5 条），降低 P2/P3 噪音

set -euo pipefail

PR_NUMBER="${PR_NUMBER:?必填}"
AI_REVIEW_MODE="${AI_REVIEW_MODE:?必填}"
GITEA_API_TOKEN="${GITEA_API_TOKEN:?必填}"
GITEA_BASE_URL="${GITEA_BASE_URL:-http://43.130.59.228}"
GITEA_OWNER="${GITEA_OWNER:-FFAIWorkspace}"
GITEA_REPO="${GITEA_REPO:-workspace}"

case "$AI_REVIEW_MODE" in
  hard-rules-block|batch-summary|release-risk) ;;
  *) echo "❌ AI_REVIEW_MODE 必须是 hard-rules-block / batch-summary / release-risk" >&2; exit 2 ;;
esac

command -v claude >/dev/null || { echo "❌ claude CLI 未安装"; exit 2; }
command -v jq >/dev/null     || { echo "❌ jq 未安装"; exit 2; }

# shellcheck source=./_ai_review_common.sh
source "$(dirname "$0")/_ai_review_common.sh"

api() {
  local method="$1" path="$2"
  shift 2
  curl -fsS -X "$method" \
    -H "Authorization: token $GITEA_API_TOKEN" \
    -H "Content-Type: application/json" \
    "$GITEA_BASE_URL/api/v1${path}" "$@"
}

echo "[info] 拉取 PR #$PR_NUMBER"
PR_JSON=$(api GET "/repos/$GITEA_OWNER/$GITEA_REPO/pulls/$PR_NUMBER")
PR_TITLE=$(echo "$PR_JSON" | jq -r .title)
TARGET_BRANCH=$(echo "$PR_JSON" | jq -r .base.ref)
CURRENT_HEAD_SHA=$(echo "$PR_JSON" | jq -r .head.sha)
# PR description body：作者写下的 rationale / 跟 PRD 的差异声明 / 已知风险
# 缺它会让 review 反复指控作者已经解释清楚的事（#373）
# null/missing → jq 的 // "" 兜底为空串；空 body 时下游 PR_BODY_CONTEXT 整段不注入
PR_BODY=$(echo "$PR_JSON" | jq -r '.body // ""')
# 防御性截断：超大 PR body 会挤占 prompt 预算
# bash 的 ${#var} 与 ${var:0:N} 在 UTF-8 locale（CI 默认）下按**字符**，C locale 下按字节
# 走字符语义反而更稳：${var:0:N} 不会切坏 UTF-8 多字节序列、命名也直观
# 实测项目 PR body 多在 2-8K 字符，12000 字符阈值覆盖绝大多数；长尾留给作者自己拆段
PR_BODY_LEN=${#PR_BODY}
if [ "$PR_BODY_LEN" -gt 12000 ]; then
  PR_BODY="${PR_BODY:0:12000}

[…PR body 超过 12000 字符已截断，原长 $PR_BODY_LEN]"
fi
# 仓库已由 actions/checkout@v4 fetch-depth=0 完整 checkout 在 cwd → 不再 API 拉 diff
# 三点（merge-base 起算）= PR review 标准语义；两点会把 base 推进的提交带回来
# 落 tmpfile 而非 bash 变量，避免大 PR 把整坨 diff 装进进程内存
git fetch origin "$TARGET_BRANCH" --quiet 2>/dev/null || true
DIFF_TMP=$(mktemp)
trap 'rm -f "$DIFF_TMP"' EXIT
git diff "origin/${TARGET_BRANCH}...HEAD" 2>/dev/null > "$DIFF_TMP" || true
PR_DIFF_BYTES=$(wc -c < "$DIFF_TMP")
CURRENT_DIFF_HASH=$(md5sum < "$DIFF_TMP" | cut -d' ' -f1)
echo "[info] PR: $PR_TITLE → $TARGET_BRANCH (diff ${PR_DIFF_BYTES} bytes, hash=${CURRENT_DIFF_HASH:0:12}, agent 自探查)"

# PRD 合理性审核（按需启用）：PR diff 含 docs/modules/**/01-prd.md → 注入 PRD 审核 prompt 片段
# 严重度阈值 + 维度优先级在 prompts/prd-rationality.md 里强约束（只产 hard_block / risk，最多 5 条）
PRD_REVIEW_CONTEXT=""
PRD_FILES=$(git diff --name-only "origin/${TARGET_BRANCH}...HEAD" 2>/dev/null \
  | grep -E '^docs/modules/.+/01-prd\.md$' || true)
if [ -n "$PRD_FILES" ]; then
  PRD_COUNT=$(echo "$PRD_FILES" | wc -l)
  echo "[info] 检测到 $PRD_COUNT 份 PRD 改动，注入 PRD 合理性审核 context"
  PRD_PROMPT_FILE="$(dirname "$0")/prompts/prd-rationality.md"
  if [ ! -f "$PRD_PROMPT_FILE" ]; then
    echo "❌ PRD 审核 prompt 文件缺失: $PRD_PROMPT_FILE" >&2
    exit 2
  fi
  PRD_FILES_LIST=$(echo "$PRD_FILES" | sed 's/^/  - /')
  # 用 awk 做安全替换（sed 对含 / 路径不友好；awk 把整段 placeholder 行整行替换）
  PRD_REVIEW_CONTEXT=$(awk -v rep="$PRD_FILES_LIST" '
    /^__PRD_FILES__$/ { print rep; next }
    { print }
  ' "$PRD_PROMPT_FILE")
  # 包一层换行 + 分隔，避免和上下段贴死
  PRD_REVIEW_CONTEXT=$'\n'"$PRD_REVIEW_CONTEXT"$'\n'
fi

# State header：从上一次 ai-review-runner 评论末尾解析 base64 编码的 JSON state
# 让本轮 review 知道上一轮 head SHA + open findings，避免重复指控（#259 痛点）
# 第一次 review 拉不到 → PREV_* 全空 → prompt 走完整 review 流程
echo "[info] 查找上一轮 state header（增量 review 去重）"
PREV_COMMENTS_JSON=$(api GET "/repos/$GITEA_OWNER/$GITEA_REPO/issues/$PR_NUMBER/comments" 2>/dev/null || echo "[]")
PREV_STATE_B64=$(echo "$PREV_COMMENTS_JSON" | jq -r '
  [.[] | select(.body | contains("ai-review-runner 生成"))]
  | sort_by(.created_at)
  | (last // {}).body // ""
' | { grep -oE '<!-- ai-review-state-b64: [A-Za-z0-9+/=]+ -->' || true; } | tail -1 \
  | sed -E 's/<!-- ai-review-state-b64: ([A-Za-z0-9+/=]+) -->/\1/')
PREV_HEAD_SHA=""
PREV_REVIEW_COUNT=0
PREV_OPEN_FINDINGS_JSON="[]"
PREV_DIFF_HASH=""
if [ -n "$PREV_STATE_B64" ]; then
  PREV_STATE=$(echo "$PREV_STATE_B64" | base64 -d 2>/dev/null || echo "{}")
  PREV_HEAD_SHA=$(echo "$PREV_STATE" | jq -r '.last_head_sha // ""')
  PREV_REVIEW_COUNT=$(echo "$PREV_STATE" | jq -r '.review_count // 0')
  PREV_OPEN_FINDINGS_JSON=$(echo "$PREV_STATE" | jq -c '.open_findings // []')
  PREV_DIFF_HASH=$(echo "$PREV_STATE" | jq -r '.last_diff_hash // ""')
  echo "[info]   上一轮 head=$PREV_HEAD_SHA, review_count=$PREV_REVIEW_COUNT, open_findings=$(echo "$PREV_OPEN_FINDINGS_JSON" | jq 'length') 项, diff_hash=${PREV_DIFF_HASH:0:12}"
else
  echo "[info]   无上一轮 state（首次 review）"
fi
NEW_REVIEW_COUNT=$((PREV_REVIEW_COUNT + 1))

# 幂等短路（替代删 synchronize 的方案）：PR diff 没变 → 跳过 claude CLI，秒级 success exit
# 触发场景：merge base into PR / rebase / 无内容 force-push / draft toggle 触发的 ready_for_review
# 例外：手动 /ai-review 或 workflow_dispatch 强制重跑（人为意图），不短路
if [ -n "$PREV_DIFF_HASH" ] && [ "$CURRENT_DIFF_HASH" = "$PREV_DIFF_HASH" ] && \
   [ -z "${AI_REVIEW_FORCE:-}" ] && \
   [ "${GITHUB_EVENT_NAME:-}" != "issue_comment" ] && \
   [ "${GITHUB_EVENT_NAME:-}" != "workflow_dispatch" ]; then
  echo "[info] PR diff hash 未变化（${CURRENT_DIFF_HASH:0:12}），跳过本轮 review（幂等短路）"
  echo "[info]   若需强制重跑：评论 /ai-review，或 workflow_dispatch，或 export AI_REVIEW_FORCE=1"
  exit 0
fi

case "$AI_REVIEW_MODE" in
  hard-rules-block)
    MODE_DESC="**hard-rules-block** (PR → $TARGET_BRANCH)：只关注硬规则违反——契约面文档缺失、迁移文件 > 1、明显逻辑错、关键测试缺失、组织隔离/权限漏洞、SQL 注入等。软规则（命名/可读性/重构）一律 severity=suggestion，不要返回 hard_block。verdict=block 当且仅当存在 hard_block finding。" ;;
  batch-summary)
    MODE_DESC="**batch-summary** (PR → $TARGET_BRANCH)：聚焦本批次合并到 staging 后的整体风险——跨模块影响、数据迁移合理性、上线连锁问题。不再深究 develop 已审过的细节。" ;;
  release-risk)
    MODE_DESC="**release-risk** (PR → $TARGET_BRANCH)：聚焦发布风险——回滚预案、灰度策略、不可逆改动、上线时机、监控告警。不审代码细节（L3 已审过）。重点：迁移能否回滚、env 变量是否完整、灰度比例、回退机制。" ;;
esac

# PR description body 上下文（#373）：作者已在 PR body 写过的 rationale
# 不喂给 LLM = 每轮 review 重复指控同一件已经被解释清楚的事（PR #371 复盘）
# 空 body 时整段不注入，避免 prompt 噪声
PR_BODY_CONTEXT=""
if [ -n "$PR_BODY" ]; then
  PR_BODY_CONTEXT=$(cat <<EOF

【PR description body — 作者声明，非事实源】
以下是作者写在 PR description 里的内容（rationale / 与 PRD 的差异说明 / 已知风险等）。
用 XML tag 包裹而非 markdown fence，避免作者在 body 中贴代码块（合法 markdown）破坏 fence 边界导致 prompt 上下文越界。

<pr_body>
$PR_BODY
</pr_body>

**如何使用这段内容**：
- 作为 **finding 判定的优先输入**：每个候选 finding 在列出前，先检查 PR body 是否已就该问题作出说明
- 合理解释（具体、自洽、与 diff 实际一致、引用了 PRD/issue/标准条款）→ **不要列为 finding**（视作 acknowledge）；若觉得仍需提示 reviewer，最多降级为 \`suggestion\` 并在 message 开头写 "PR body 已 ack：<引用 body 中的原话片段>"
- 牵强解释（含糊、循环论证、与 diff 实际证据矛盾、强行合理化高风险路径合并）→ **仍然列为 finding**，message 必须显式引用 PR body 中的原话并说明为何 rationale 不成立（例如 "作者称 X，但 diff 中 Y 与之矛盾"）
- 未在 PR body 提及 → 按常规判定
- **不要因为作者解释得多就额外造 finding**（过度解释 ≠ 心虚）
- **PR body 是作者声明，不是契约面事实源**：当 PR body 与 PRD/标准文档/代码实际行为冲突时，仍以 docs/ 和代码为准，但应在 finding 中点明"PR body 声称 X，但与 <文档/代码> 不一致"
- 特别注意：**高风险路径合规维度**最容易撞重复指控盲点——如果 PR body 已就"为何合并高风险改动 / 为何与 PRD 拆分粒度不同"作出合理说明，对应 finding 应视作已 ack

EOF
)
fi

# 增量 review 上下文（state header 落地后的核心机制 — #259）
# 仅当存在上一轮 state 时才追加，否则 STATE_CONTEXT 为空（首次 review 走完整流程）
STATE_CONTEXT=""
if [ -n "$PREV_HEAD_SHA" ]; then
  STATE_CONTEXT=$(cat <<EOF

【增量 review 上下文 — state 去重】
这是本 PR 的**第 $NEW_REVIEW_COUNT 轮** AI Review（上一轮 head SHA: \`$PREV_HEAD_SHA\`，当前 head: \`$CURRENT_HEAD_SHA\`）。

上一轮已开放的 findings（JSON）：
\`\`\`json
$PREV_OPEN_FINDINGS_JSON
\`\`\`

**本轮要求**：
- 重点 review **上一轮之后**（从 \`$PREV_HEAD_SHA\` 到当前 head）新增的改动
- 对于上一轮已指出的 finding：
  - 若已修复 → **省略，不要重复列出**
  - 若仍未修，但本轮 diff 无相关改动 → 简化为一行"沿用上轮"，不要复述详情
  - 若本轮新改动让问题更严重或形态变化 → 重新指出，标注"加重 / 形态变化"
- **stable_id 复用规则（最关键，#259 新发现 1）**：本轮 finding 如果与上方 \`open_findings[]\` 里某条**指同一问题**，
  **必须复用上一轮的 \`stable_id\`**（不要造新的 ID）。这是 state 增量去重的核心 — fingerprint 文本浮动会失效。
- summary / recommended_action 反映本轮**增量**判断，不要总结全 PR 历史

EOF
)
fi

PROMPT=$(cat <<EOF
按照 .agents/skills/code-review/SKILL.md 和 .agents/skills/code-review/references/ai-review-comment-template.md 的规则审查这个 PR，结论按 schema 输出。

审查模式：$MODE_DESC

PR 元信息：
- PR #$PR_NUMBER：$PR_TITLE
- base 分支（diff 起点）：origin/$TARGET_BRANCH
- 当前 head SHA：$CURRENT_HEAD_SHA
- diff 总量：${PR_DIFF_BYTES} bytes
$PR_BODY_CONTEXT
【探查方式 — agent 模式】
仓库已 checkout 到当前工作目录。**不在 prompt 里给你 diff**，你用工具自己探查。建议路径：

1. 看清单：先跑 \`git diff --stat origin/$TARGET_BRANCH...HEAD\` 看哪些文件改了 / 改了多少行（**三点**：从 merge-base 起算，PR review 标准语义）
2. 按文件取 diff：\`git diff origin/$TARGET_BRANCH...HEAD -- <file>\` 看具体改动
3. 看上下文：Read 改动文件本身，理解改动行**周围**的代码（被调用方、相邻函数、相关常量）
4. 对照契约（**最重要**）：改动涉及 API / schema / state machine / UI 规格时，Read \`docs/modules/{module}/\` 下对应文档（\`07-api.md\` / \`06-data-model.md\` / \`04-state-machine.md\` / \`05-ui-interaction-spec.md\` / \`01-prd.md\`）验证一致性
5. 跳过：lock 文件 / generated / dist / *.snapshot / 二进制 / 大段 JSON 数据资源
6. >500 行改动的文件：优先看 hot path / 核心逻辑，不要一行行抠

$(ai_review_emit_global_checks)

$STATE_CONTEXT
$PRD_REVIEW_CONTEXT
输出要求（强约束）：
- summary 必须 ≤200 字，一句话讲完评审核心结论（不要复述 PR 内容）
- dimensions 至少返回 7 个核心维度：契约面 / 数据迁移 / 代码逻辑 / 测试覆盖 / 安全/权限 / 文档一致性 / 高风险路径合规。每个返回 ok / warn / block / n/a。模式特定维度（release-risk 加"回滚预案"等）可追加在后面。
- **verdict 五级语义**（按严重性递增，#259 新发现 2 加 should_fix 让 risk 类有压力）：
  - \`pass\`: 无 findings 或仅 suggestion
  - \`pass_with_risk\`: 有 risk findings 但作者可以不修（如代码风格、可读性）
  - \`should_fix\`: 有 risk findings 且**强烈建议修**（影响可维护性 / 上线风险 / 文档漂移），但非合并阻断项；作者应在 PR description 中明确响应
  - \`needs_fix\`: 多个 risk 或单个需要明确响应的问题，建议合并前修
  - \`block\`: 存在 hard_block finding（契约缺失 / SQL 注入 / 权限漏洞等合并阻断）
- findings 按 severity 分级：hard_block 仅给真正阻断合并的问题；risk 给需要关注但不阻断的；suggestion 给软规则
- **每个 finding 必须含 stable_id**：kebab-case，3-64 字符，命名 \`<category-slug>-<问题短描述>\`（如 \`contract-07api-missing-verify\`、\`xlsx-dep-missing\`）。同一问题在同 PR 跨多轮 review 必须复用同一 ID（详见 STATE_CONTEXT 里的复用规则）。
- recommended_action 必须 ≤200 字，明确告诉作者要不要回应、做什么（例："无需修改，已可合并" / "请补 .env.example 中 XYZ 变量后重新触发 review"）
- 引用文件/行号时尽量精确（用 \`path/to/file.ts:42\` 格式），方便作者跳转
EOF
)

# 本地 prompt 调试入口（#373 引入）：只 dump 最终 prompt 不调 claude，秒级验证 PR body 注入
# CI 守卫：拒绝在 CI 环境使用，避免误设导致 review job 以「无 finding 成功」结束（绕过 review）
if [ -n "${AI_REVIEW_DUMP_PROMPT:-}" ]; then
  if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ] || [ -n "${GITEA_ACTIONS:-}" ]; then
    echo "❌ AI_REVIEW_DUMP_PROMPT 仅限本地使用，在 CI 环境会跳过真实 review" >&2
    exit 2
  fi
  echo "===== PROMPT (AI_REVIEW_DUMP_PROMPT — 仅本地调试，不是 review 结论) ====="
  echo "$PROMPT"
  echo "===== END PROMPT ====="
  exit 0
fi

# JSON Schema 驱动的结构化输出：claude CLI 把 schema 推给 API 做硬校验，
# 模型物理上拿不到"输出非法 JSON"的选项；不再需要 jq fallback / recover-json 启发式
# （见 .learnings/ERRORS/ERR-20260510-004 — 自由文本 JSON + 中文引号未转义事故）
# 结构详见 .agents/skills/code-review/references/ai-review-comment-template.md
# Schema 单源：本 runner + scripts/dev/ai-review-local.sh 共用 ai-review-schema.json
SCHEMA=$(cat "$(dirname "$0")/ai-review-schema.json")

echo "[info] 调用 claude CLI（agent 模式，只读工具白名单） ..."
RESPONSE=$(echo "$PROMPT" | claude --print \
  --output-format json \
  --json-schema "$SCHEMA" \
  --permission-mode auto \
  --allowedTools "${AI_REVIEW_ALLOWED_TOOLS[@]}" 2>&1) || {
  echo "❌ claude CLI 失败:"
  echo "$RESPONSE"
  exit 2
}

# claude --output-format json 把 structured_output 字段塞在 envelope 里
JSON=$(echo "$RESPONSE" | jq -c '.structured_output // empty')
if [ -z "$JSON" ]; then
  echo "❌ structured_output 字段缺失（schema 不通过或 CLI 异常）"
  echo "--- envelope 摘要 ---"
  echo "$RESPONSE" | jq -r '{subtype, is_error, api_error_status, stop_reason, num_turns} | to_entries[] | "\(.key)=\(.value)"' 2>/dev/null \
    || echo "$RESPONSE" | head -20
  if [ -n "${AI_REVIEW_DRY_RUN:-}" ]; then
    echo "⚠️ dry-run 期间软失败，CI 不阻断"
    exit 0
  fi
  exit 2
fi

VERDICT=$(echo "$JSON" | jq -r .verdict)
SUMMARY=$(echo "$JSON" | jq -r '.summary // ""')
ACTION=$(echo "$JSON" | jq -r '.recommended_action // ""')
FINDINGS_COUNT=$(echo "$JSON" | jq '.findings | length')
DIM_COUNT=$(echo "$JSON" | jq '.dimensions | length')
echo "[info] verdict=$VERDICT, $DIM_COUNT dimensions, $FINDINGS_COUNT findings"

# 渲染评论（结构详见 .agents/skills/code-review/references/ai-review-comment-template.md）
case "$VERDICT" in
  pass)           EMOJI="✅" ;;
  pass_with_risk) EMOJI="⚠️" ;;
  should_fix)     EMOJI="🔶" ;;  # #259 新发现 2：四级 verdict 区分"强烈建议修但不阻断"
  needs_fix)      EMOJI="🔧" ;;
  block)          EMOJI="❌" ;;
  *)              EMOJI="❓" ;;
esac

COMMENT="## $EMOJI AI Review ($AI_REVIEW_MODE) — $VERDICT"$'\n\n'
COMMENT+="**一句话**：$SUMMARY"$'\n\n'

# 评审维度表
COMMENT+="### 评审维度"$'\n\n'
COMMENT+="| 维度 | 结论 | 备注 |"$'\n'
COMMENT+="|---|---|---|"$'\n'
while IFS= read -r line; do
  COMMENT+="$line"$'\n'
done < <(echo "$JSON" | jq -r '
  .dimensions[] |
  ( if .status == "ok"    then "✓"
    elif .status == "warn"  then "⚠️"
    elif .status == "block" then "🚫"
    else "—"
    end
  ) as $emoji |
  # note 字段含换行会破坏 markdown 表格行 → 替换成空格
  ((.note // "—") | gsub("\n"; " ")) as $note |
  "| \(.name) | \($emoji) \(.status) | \($note) |"
')

# 发现按 severity 分级，零发现也展示（structured，便于未来 #259 统计）
COMMENT+=$'\n### 发现\n\n'
for sev in hard_block risk suggestion; do
  COUNT=$(echo "$JSON" | jq --arg sev "$sev" '[.findings[] | select(.severity == $sev)] | length')
  case "$sev" in
    hard_block) LABEL="🚫 **硬阻断**" ;;
    risk)       LABEL="⚠️ **风险**" ;;
    suggestion) LABEL="💡 **建议**" ;;
  esac
  if [ "$COUNT" -eq 0 ]; then
    COMMENT+="- $LABEL：0 项"$'\n'
    continue
  fi
  COMMENT+="- $LABEL：$COUNT 项"$'\n'
  while IFS= read -r line; do
    COMMENT+="$line"$'\n'
  done < <(echo "$JSON" | jq -r --arg sev "$sev" '
    .findings[]
    | select(.severity == $sev)
    | "  - **[\(.category)]** \(if .file then "(`\(.file)\(if .line then ":\(.line)" else "" end)`) " else "" end)\(.message)"
  ')
done

# 给作者的 action
COMMENT+=$'\n### 给作者的 action\n\n'
COMMENT+="$ACTION"$'\n'

COMMENT+=$'\n---\n_由 ai-review-runner 生成 · Claude Code CLI · 跟踪 [#171](http://43.130.59.228/FFAIWorkspace/workspace/issues/171)_\n'

# State header（#259 增量 review 去重）：把本轮 head SHA + open findings 编码进 HTML 注释
# 末尾追加 → 下轮 runner 抓"last comment by ai-review-runner 末尾的 base64 块"做增量上下文
# stable_id 是去重的真依据（#259 新发现 1）；category/severity/msg_short 留作人类 debug 时辨认
NEW_STATE_JSON=$(echo "$JSON" | jq -c \
  --arg sha "$CURRENT_HEAD_SHA" \
  --arg hash "$CURRENT_DIFF_HASH" \
  --argjson count "$NEW_REVIEW_COUNT" \
  '{last_head_sha: $sha, last_diff_hash: $hash, review_count: $count, open_findings: [.findings[] | {stable_id, category, severity, msg_short: (.message | .[0:80])}]}')
NEW_STATE_B64=$(echo -n "$NEW_STATE_JSON" | base64 -w0)
COMMENT+=$'\n<!-- ai-review-state-b64: '"$NEW_STATE_B64"$' -->\n'

# Dry-run 期间在评论顶端加横幅，提示观察期不阻断、便于 PR 阅读者识别
# （之前是 dry-run 不发评论，要看输出得点进 CI 日志层层翻——观察期成本太高）
if [ -n "${AI_REVIEW_DRY_RUN:-}" ]; then
  BANNER='> 🔬 **DRY RUN（观察期）** — 本评论由 AI 自动生成，**当前不阻断合并**。
> 评估准确率达标后会去 dry-run 进 required check（跟踪：issue #171）。

'
  COMMENT="${BANNER}${COMMENT}"
fi

echo "[info] 写回 Gitea PR 评论（dry-run=${AI_REVIEW_DRY_RUN:-no}）"
api POST "/repos/$GITEA_OWNER/$GITEA_REPO/issues/$PR_NUMBER/comments" \
  -d "$(jq -nc --arg body "$COMMENT" '{body: $body}')" >/dev/null

# hard-rules-block 模式：发现硬阻断 → exit 1（dry-run 期间例外，仅日志提示）
if [ "$AI_REVIEW_MODE" = "hard-rules-block" ] && [ "$VERDICT" = "block" ]; then
  if [ -n "${AI_REVIEW_DRY_RUN:-}" ]; then
    echo "⚠️  dry-run 期间不阻断（实际 verdict=block，转正后此处会 exit 1）"
  else
    echo "❌ 硬阻断问题，PR 不允许合并"
    exit 1
  fi
fi

echo "✅ AI Review 完成"
