#!/bin/bash
# Stop / SubagentStop hook: 收尾前把**最后一轮**的 mockup 机检结果取回来。
#
# ── 为什么需要它 (2026-09-07, owner 拍 ai-ds-lab decision-queue Q6 的
#    `fix-last-round-blindspot`) ──────────────────────────────────────────────
# `post-figma-write.sh` 是 **PostToolUse + 后台跑 + 下一轮注入**（它头注释 B1 段逐字
# 「本轮**发起**, 下一轮**注入上一轮的结果**」，理由是整文件实测 27s、同步会把 hook
# 变成长阻塞）。这个形态有一个结构性盲区：
#
#   🔴 **agent 报 done 那一轮就是最后一轮 ⇒ 那一轮发起的 run 的结果没有「下一轮」可注入。**
#
# 而 §M-LIFECYCLE 第②时点要守的**恰恰是**「报 done 前必须跑写后机检」
# ⇒ 最后一次写操作的机检结果，在正常收尾路径上**恒不可见**。
# 亲验记录（挂载面四查 + 那 4 个静默不跑的前件）在 ai-ds-lab `docs/decision-queue.md` Q6。
#
# ⇒ 本 hook 不动挂载面、不加规则、不需要 Figma 凭据进 CI，只改**取结果的时点**。
#
# ── 契约取自活源 + 实测 (2026-09-09 复核，订正 09-07 那版两处) ─────────────────
#   · 阻塞机制是**退出码 2**，消息取 **stderr**（docs 逐字：a hook that blocks by exiting 2 routes
#     the same way as `reason`）。JSON `{"decision":"block","reason":…}` 对 Stop **同样有效**
#     （docs 逐字：`decision: "block"` prevents Claude from stopping）—— 09-07 写「那不是这个事件
#     的形态」是错的；本 hook 仍用 exit 2，两者等价。
#   · 输入**有** `stop_hook_active` 字段（docs 逐字：true when Claude Code is already continuing
#     as a result of a stop hook）。2026-09-09 用 `claude -p` + 探针 hook 实测 Claude Code 2.1.251：
#     Stop 触发两次、首次 false、续行 true、exit 2 被尊重（模型按 stderr 续跑）。
#     09-07 写「没有这个字段」是错的。⛔ 但本 hook **仍不拿它防循环**：它只说明「这是续行」，
#     不说明「该不该再挡」（续行里再写一次 Figma 就该再挡一次）。防循环靠下面两条。
#   · 「harness 真的触发 Stop 并尊重 exit 2」已由上述无头探针观测到（⛔ 此前登记为「未观测」）。
#
# ── 防循环（两条独立的belt，⛔ 一条都不能省）────────────────────────────────────
#   1. **消费一次**：注入过的 `result.txt` 立刻 `mv` 成 `result.consumed.txt`
#      —— 与 post-figma-write.sh 用的是同一个协议，⛔ 没另造一套状态。
#      （第 ③ 段的 `unchecked-writes` 同理 mv 成 `.consumed`。）
#   2. **有界升级**：still-pending 的情形会重复触发（等超时 ⇒ exit 2 ⇒ 又停 ⇒ 又等），
#      所以最多阻塞 MAX_BLOCKS 次，之后放行并留下**响亮的** systemMessage。
#      ⛔ 刻意不做成无限等：无限等 = 把「结果没取到」变成「session 卡死」，那是更坏的失败。
#
# 真源: tvu-design-system/docs/internal/mockup-conventions.md §M-LIFECYCLE
# 注册: .claude/settings.json hooks.Stop + hooks.SubagentStop
# 自测: .claude/hooks/stop-figma-conformance.selftest.sh（造故障 + 阴性对照）

set -u

WAIT_SECS="${TVU_STOP_CONFORMANCE_WAIT:-45}"
MAX_BLOCKS="${TVU_STOP_CONFORMANCE_MAX_BLOCKS:-3}"

input=$(cat)

session_id=$(printf '%s' "$input" | python3 -c "
import json, sys
try:
    print(json.load(sys.stdin).get('session_id') or 'nosession')
except Exception:
    print('nosession')
" 2>/dev/null || echo nosession)

STATE_DIR="${TMPDIR:-/tmp}/tvu-mockup-conformance/${session_id}"

# 本 session 一次 Figma 写都没有 ⇒ 没有状态目录 ⇒ 静默放行。
# ⛔ 别在这里「兜底提醒一句」：那会让每个与 Figma 无关的 session 收尾时都被打扰，
#    而打扰的代价是训练人忽略这条 hook（本仓已登记过「恒红 = 零信息」）。
[ -d "$STATE_DIR" ] || exit 0

# ⚠️ 读计数器**不用** `$(cat f || echo 0)`：f 不在时 cat 什么都不印、`||` 补一个 0 ——
#    看着没事，但 f 存在且为空时会得到空串，而 `-ge` 拿空串当场报错。
#    （同族坑已在 ai-ds-lab Q7 §4.2 第 2 条登记：`grep -c … || echo 0` ⇒ 变量成了 `0\n0`。）
blocks=0
[ -s "$STATE_DIR/stop-blocks" ] && blocks=$(tr -dc '0-9' < "$STATE_DIR/stop-blocks")
[ -n "$blocks" ] || blocks=0

emit_and_block() {
  # exit 2 的消息取 stderr（活源契约）
  printf '%s\n' "$1" >&2
  echo $((blocks + 1)) > "$STATE_DIR/stop-blocks"
  exit 2
}

# ── ① 有没有还在跑的 run：等它，但**有上限** ──────────────────────────────────
pid=""
[ -s "$STATE_DIR/pending.pid" ] && pid=$(tr -dc '0-9' < "$STATE_DIR/pending.pid")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
  if [ "$blocks" -ge "$MAX_BLOCKS" ]; then
    python3 -c "
import json
print(json.dumps({'systemMessage': '⚠️ mockup 机检仍在后台跑，已达阻塞上限 ${MAX_BLOCKS} 次 ⇒ 放行收尾。⛔ 别把这次收尾读作「验过了」—— report 在 ${STATE_DIR}/'}, ensure_ascii=False))
"
    exit 0
  fi
  waited=0
  while [ "$waited" -lt "$WAIT_SECS" ]; do
    kill -0 "$pid" 2>/dev/null || break
    sleep 1
    waited=$((waited + 1))
  done
  if kill -0 "$pid" 2>/dev/null; then
    emit_and_block "⛔ §M-LIFECYCLE 第②时点未闭环：本轮发起的 mockup 机检**还在跑**（pid ${pid}，已等 ${waited}s / 上限 ${WAIT_SECS}s）。
⚠️ 这正是 Q6 那个盲区：报 done 那一轮的机检结果没有「下一轮」可注入。
下一步（⛔ 别报 done）：读 ${STATE_DIR}/ 下最新的 report-*.json 与 run-*.log，逐行看 scope 与 checkedUnits。
⚠️ 判读纪律：exit=0 **不等于**验过了 —— scope=file 的两条（library-binding / connector）是全文件口径；checkedUnits=0 的那条**分母是空的**，它的绿不可解读。
（本 hook 最多阻塞 ${MAX_BLOCKS} 次，之后会放行 —— ⛔ 放行不代表验过。）"
  fi
fi

# ── ② 有没有没人看过的结果：注入并挡一次收尾 ─────────────────────────────────
if [ -s "$STATE_DIR/result.txt" ]; then
  result=$(cat "$STATE_DIR/result.txt")
  # 消费一次（与 post-figma-write.sh 同协议）⇒ 下次 Stop 不会再拿同一份挡
  mv "$STATE_DIR/result.txt" "$STATE_DIR/result.consumed.txt" 2>/dev/null || true
  emit_and_block "⛔ §M-LIFECYCLE 第②时点：**最后一轮**的 mockup 机检结果还没有人看过。

══ 后台机检结果（真实执行，非提醒）══
${result}
══ 结果结束 ══

⚠️ 判读纪律：exit=0 **不等于**验过了 —— 逐行看 scope 与 checkedUnits。
   scope=file 的两条（library-binding / connector）是**全文件口径**，--node 缩不了它们；
   checkedUnits=0 的那条**分母是空的**，它的绿不可解读。
⇒ 有 FINDINGS 就当场修完再收尾；确认全绿再报 done。"
fi

# ── ③ 写了却一次机检都没跑（post-figma-write.sh 因缺 fileKey / 未定位仓库而没发起）────
#    2026-09-09 补：这是同一条规则（第②时点）的另一个盲区 —— 没贴过 URL 的 session 可以写一整轮
#    而 ①② 都没东西可挡。消费一次（mv 成 .consumed）⇒ 只挡一次，不会把 session 卡死。
if [ -s "$STATE_DIR/unchecked-writes" ]; then
  n=$(wc -l < "$STATE_DIR/unchecked-writes" | tr -dc '0-9')
  [ -n "$n" ] || n=0
  mv "$STATE_DIR/unchecked-writes" "$STATE_DIR/unchecked-writes.consumed" 2>/dev/null || true
  emit_and_block "⛔ §M-LIFECYCLE 第②时点：本 session 有 ${n} 次 Figma 写操作**没有任何机检跑过**（post-figma-write 因缺 fileKey 或未定位仓库而没发起）。
下一步（⛔ 别报 done）：贴一次该文件的 figma.com/design/<key> URL 让后续写操作自动机检，或手工跑
  pnpm audit:mockup-conformance --file <key> --node <触碰节点>   （每个触碰节点各跑一次）
明细：${STATE_DIR}/unchecked-writes.consumed（每行 = 一次写的时间与原因）。
⚠️ 本提醒只挡一次 —— 放行不代表验过。"
fi

# 没有 pending、也没有未消费的结果、也没有未机检的写 ⇒ 该看的都看过了，放行。
exit 0
