#!/bin/bash
# UserPromptSubmit hook（lab 专用）：在 lab 的 cwd 里收到**消费产品的设计任务**时当场退回。
#
# ── 为什么有这个 hook（⛔ 这一段是它存在的唯一理由，别删）─────────────────────────
# owner 2026-09-10 拍定三条（`docs/2026-09-09-ds-slim-optimize-review.md` §8.1）：
#   1. 设计执行归 TVU Pack 项目；lab 只做设计系统的**审查与优化**。
#   2. 消费产品的设计任务 **⛔ 别记录在 lab 和 DS 上**（逐字）——只能记在那个消费仓里。
#   3. 已经混进来的按第 2 条清掉。
# 那条边界当时**只写在文档里** ⇒ 按本仓纪律「一条纪律如果没有承载它的栏位，它就只会靠自觉
# 执行 —— 而自觉的方差是一个数量级」（`AGENTS §26`），必须落成机制。本文件就是那个栏位。
#
# ── 判据是怎么选出来的（⛔ 不是拍脑袋，有读数）───────────────────────────────────
# 量具：`probes/consumer-task-boundary/measure-classifier-fp.mjs`
# 语料：lab cwd 下 250 份 session transcript / **452 条人写 prompt**（已排 tool_result /
#       system-reminder / task-notification / hook 注入 —— 排除面本身是订正过的，见该量具注释）
#
#   | 通道 | 命中 | 精确率 | 召回 | 结论 |
#   |---|---|---|---|---|
#   | 臂 B 流水线唤醒词          | 3 | **3/3**，误报 **0/452** | 3/3 | ✅ 挂阻塞 |
#   | 臂 A Figma URL + 写动词    | 0 | —                       | **0/3** | ⚠️ 本语料零命中 |
#
# 🔴 **臂 A 是复用 DS `.claude/hooks/detect-figma-task.sh` 那条已验过的分类器，
#    而它对本用途召回 = 0** —— 三条真阳一条都没贴 Figma URL（走 Jira 单号 + 唤醒词）。
#    ⇒ 臂 A 在本语料上**两臂都不响 = 零信息**（`AGENTS §25`：只在故障臂红的才是判别器）。
#    ⇒ 保留它，因为它抓的失效形态（贴着 Figma URL 要改 mockup）是真实的；
#    ⛔ **但不得把它算进「本机制已验证」的证据里。**
#
# ── 如实登记的覆盖边界（这是边界，⛔ 不是 TODO）─────────────────────────────────
# ① **召回上限**：判据钉的是「流水线入口的措辞」。有人不提唤醒词、直接说「帮我画个 X 弹窗」
#    ⇒ **本 hook 不会响**。⛔ 别把它读成「边界已经守住了」。
# ② 判定面 = prompt 文本。任务从**子代理 / 跨 session 续跑 / 文件内容**进来的形态不覆盖。
# ③ 本 hook ⛔ 不删任何东西、⛔ 不改任何文件 —— 只退回。清理仍是人的动作。
# ④ 逃生口 `LAB_CONSUMER_TASK_ACK=1` 只允许**人**设置。逐字照 owner 2026-08-12 对
#    `VISUAL_COMMIT_APPROVED` 的裁定形态：「**AI 仍不得自设本变量**」。
# ⑥ 🔴 **已实测的误报形态（2026-09-10 接线后当场造出来的，⛔ 不是推测）：臂 B 会锁住
#    「审查这条 hook 本身」的 session。** 实测 prompt 逐字
#    「接 ai-ds-lab 的 DS 审查线。上一轮把「@TVU 设计全流程」这条唤醒词加进了拦截 hook 的
#     臂 B，请核一下那条判据的召回上限。只回 OK。」⇒ **`num_turns 0`，当场退回**。
#    ⚠️ 这正是臂 B2 被降为告警的那条理由（「lab 审查 session 会写下这几个字 ⇒ 挂阻塞会把
#    lab 自己锁在门外」），而**它当时没有被应用到臂 B 上**。
#    ⇒ **⛔ 刻意不改判据** —— 「加豁免 / 降判据」是 `decision-queue.md` 的 **C4**（放宽任何闸），
#      自审自批 = 一条不能失败的控制 ⇒ agent 不自拍。
#    ⇒ **缓解（实测过，⛔ 不是设想）**：退回时 `result` 逐字回显 `Original prompt: <原文>`
#      ⇒ 原话不丢、不用重打；文案本身点名逃生口。代价 = 一次退回 + 由**人**设一个环境变量。
#    ⇒ 重开条件：这个形态真的开始烦人（≥3 次），或有人量出「审查意图」可机检的判据。
# ⑤ 🔴 **已登记的判据空档：Figma→code 方向不拦。** 臂 A 逐字复用 DS 分类器，而它为了分流
#    到另一条协议**刻意排除**了 code-output 信号（`还原/实现/vue/react/html…`）。对 lab 的边界
#    而言那同样是消费产品的活，⇒ 这是一个**空档**。⛔ 刻意不补，理由有两条：
#    （a）臂 A 在 452 条语料上召回 0 ⇒ 它的精确率**根本没被测过**，往阻塞档加未测宽度
#         正是本仓警告过的「过度上闸 = 训练绕过」；
#    （b）lab 审查 `code-conventions` / `framework-parity` 那族闸时，prompt 里出现
#         「Figma URL + vue/react」是**正常审查语料**，补上去会真误伤。
#    ⇒ 要补它，先量那个形态在语料里的分布，⛔ 别直接加正则。
#
# ── 两层验证（⛔ 别把上层的绿读成下层的绿）──────────────────────────────────────
# 第 1 层 **脚本自己的分支**：`.claude/hooks/reject-consumer-design-task.selftest.sh`
#   14/14，离线喂 JSON。⛔ 它**不证明** harness 会尊重 exit 2。
# 第 2 层 **harness 契约**：`probes/consumer-task-boundary/harness-contract-probe.sh`
#   9/9（读数 `runs/consumer-task-boundary/harness-contract.json`）。三臂变量隔离实测：
#   `exit 2` ⇒ 模型零调用（`num_turns 0` / `input_tokens 0`）；stderr 到达用户；
#   `additionalContext` 真进模型上下文；且 result 逐字回显 `Original prompt:`
#   ⇒ 🔴 **prompt ⛔ 没有被擦掉** —— 这一条推翻了 lab 自己
#   `docs/2026-09-09-ds-slim-optimize-review.md` §3.1 那句「exit 2 会擦掉用户 prompt」（-p 口径）。
# 第 3 层 **接线后在 lab 真 cwd 上的三臂**（owner 2026-09-10 拍定接线，`.claude/settings.json`）：
#   对照 `OK/NO-WARN`（num_turns 1）· 臂B2 `OK/GOT-WARN`（注入进了、不挡）· 臂B `num_turns 0`（真挡）。
# ⚠️ 三臂**进程退出码全 0** ⇒ ⛔ 别拿 exit code 当验收，判别力在 `num_turns` / `result` 上。
# ⚠️ 逃生口 `LAB_CONSUMER_TASK_ACK=1` 的两半各有读数、但**未合并实测**：脚本分支由 selftest 验，
#    「环境变量进得了 hook 进程」由 harness 探针的 `PROBE_ARM` 三臂切换实证。
#    ⛔ AI 不得自设该变量 ⇒ 合并那一格刻意留给人跑。
#    🟢 **owner 2026-09-15 拍定：这一格【不单独跑】，用到时自然就验了** ⇒ 该待办已销账。
#    判据（⛔ 不是省事）：① 两半各自都有读数，且**两半之间没有任何认变量名的机制**
#    ⇒ 合并的残余不确定性极低；② 失效形态是**当场可见、非破坏性的** —— 真坏了就是
#    「人设了变量却仍被挡」，发现即修，⛔ 不会静默错到别处去。
#    ⇒ **成本（一次 session 重启）> 收益。**
#    ⚠️ **⛔ 但这不等于「验过了」** —— 谁第一次真用到这个逃生口，那一次就是合并实测：
#    **若设了变量仍被挡，⛔ 别绕过（⛔ 别改这个脚本、⛔ 别 --no-verify），回来修这里。**
# ⚠️ 上述读数的环境名：Claude Code **2.1.251**（nvm）· `claude -p` 无头模式。
#    ⛔ 交互 TUI 模式未测（「会不会清掉输入框里已打的字」在 -p 下结构上没有对应物）。

set -u

# ⚠️ stdin 必须**先读干**再判逃生口 —— 反过来会让写端拿到 SIGPIPE
# （自测里表现为 `BrokenPipeError`）。这不是噪音，是 hook 提前退出的真实副作用。
raw=$(cat)

# 逃生口：只有人能设。⛔ AI 不得自设（同 VISUAL_COMMIT_APPROVED 的裁定形态）。
if [ "${LAB_CONSUMER_TASK_ACK:-0}" = "1" ]; then
  exit 0
fi

# 取 prompt 文本。⛔ fail closed：解析不出就**放行**（hook 不该把 lab 自己锁死），
# 但把「解析不出」显式说出来 —— 静默放行才是这条纪律最容易复发的形态。
prompt=$(printf '%s' "$raw" | python3 -c '
import json, sys
try:
    d = json.load(sys.stdin)
except Exception:
    print("__TVU_HOOK_PARSE_FAIL__")
    sys.exit(0)
p = d.get("prompt")
if p is None:
    print("__TVU_HOOK_NO_PROMPT_FIELD__")
else:
    print(p)
' 2>/dev/null || printf '__TVU_HOOK_PARSE_FAIL__')

case "$prompt" in
  __TVU_HOOK_PARSE_FAIL__*|__TVU_HOOK_NO_PROMPT_FIELD__*)
    printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"⚠️ lab 的 reject-consumer-design-task hook 取不到 prompt 字段（harness 契约可能变了）⇒ 本轮未做边界判定。⛔ 别把「没报」读成「不是消费产品设计任务」。"}}\n'
    exit 0
    ;;
esac

# ── 臂 B（有读数背书的那条）：消费产品设计流水线的入口措辞 ────────────────────────
armB=0
if printf '%s' "$prompt" | grep -qiE '(@?TVU[[:space:]]*设计全流程|tvu-design-pipeline|tvu-design-mockup|design-qa-loop|design-walkthrough|Stage[[:space:]]*0[[:space:]]*[–—-][[:space:]]*8)'; then
  armB=1
fi

# ── 臂 B2（弱档，⛔ 刻意不阻塞）：**裸**「设计全流程」，即不带 TVU 前缀 ─────────────
# 语料读数：裸串与臂 B 在 452 条上**不可区分**（同 3 个 session、同 0 误报）⇒ 单看读数
# 它够格挂阻塞。⛔ **但仍降为告警档**，理由是一个**语料量不到**的形态：
# 往后任何讨论「这条 hook / 这条边界」的 lab 审查 session 都会写下「设计全流程」四个字
# ⇒ 挂阻塞会把 lab 自己锁在门外，进而训练出「习惯性设 `LAB_CONSUMER_TASK_ACK=1`」——
# 而那正是本仓逐字警告过的「**过度上闸不只费时间，是训练绕过**」（`decision-queue.md` C1 判据）。
# ⇒ 分档依据 = **可测的判别力**，⛔ 不是「宁严勿松」的直觉。
armB2=0
if [ "$armB" = "0" ] && printf '%s' "$prompt" | grep -q '设计全流程'; then
  armB2=1
fi

# ── 臂 A（形态真实、本语料零命中 ⇒ ⛔ 不算已验证）：Figma URL + mockup-write 动词，
#    且无 code-output 信号。两条正则**逐字取自** DS `detect-figma-task.sh`，⛔ 不重造。
armA=0
if printf '%s' "$prompt" | grep -qE 'figma\.com/(design|file)/'; then
  mw=0; co=0
  printf '%s' "$prompt" | grep -qiE '(画|加|改|调|更新|同步|推|设计|走查|角色测试|persona|journey|UX ?交付|交付说明|注释|效果图|设计稿)' && mw=1
  printf '%s' "$prompt" | grep -qiE '(还原|实现|翻译|做成代码|组件代码|生成代码|\bcode\b|html|vue|react)' && co=1
  if [ "$mw" = "1" ] && [ "$co" = "0" ]; then armA=1; fi
fi

if [ "$armB" = "0" ] && [ "$armA" = "0" ]; then
  # 弱档：注入告警，⛔ 不阻塞。⚠️ 它**不构成**「边界已守住」的证据。
  if [ "$armB2" = "1" ]; then
    printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"⚠️ 本轮 prompt 里出现了「设计全流程」，而当前 cwd 是 ai-ds-lab（只做 DS 的审查与优化）。若这确实是**消费产品的设计任务**，⛔ 当场退回并说明，别在 lab 或 DS 留需求内容 / 单号 / 竞品资料（owner 2026-09-10 拍定，真源 docs/2026-09-09-ds-slim-optimize-review.md §8.1）。若只是在**讨论/审查**这条流水线，照常继续。"}}\n'
  fi
  exit 0
fi

which=""
[ "$armB" = "1" ] && which="${which}臂B(流水线唤醒词) "
[ "$armA" = "1" ] && which="${which}臂A(FigmaURL+写动词) "

# 阻塞：exit 2 ⇒ prompt 不进模型，stderr 给用户看。
cat >&2 <<EOF
⛔ 退回：这看起来是**消费产品的设计任务**，而当前 cwd 是 ai-ds-lab（DS 的体检科）。

命中通道：${which}

owner 2026-09-10 拍定（逐字）：
  1. 设计执行归 TVU Pack 项目；lab 只做设计系统的**审查与优化**。
  2. 消费产品的设计任务 ⛔ 别记录在 lab 和 DS 上 —— 只能记在那个消费仓里。

请这样做：
  · 到那个消费产品的仓库 cwd 里重新发起（设计流水线 skill 在那边照样能用）。
  · ⛔ 不要在 ai-ds-lab 或 tvu-design-system 留需求内容 / 单号 / 竞品资料。
  · 如果你要的其实是「审查 DS 的某条规则 / 某道闸」，换个说法重发即可。

真源：docs/2026-09-09-ds-slim-optimize-review.md §8.1
逃生口：确实要在这里做 ⇒ 由**人**设 LAB_CONSUMER_TASK_ACK=1 后重发（⛔ AI 不得自设）。
EOF
exit 2
