補上「用錯的路去證明一件事」那一格的閘(inkstone/ISEP#30 → comment 4879)

擋的是**證據的出處**,不是措辭。

判準(兩個條件同時成立才響):
  ① 要送出去的那份東西(票上的留言/派工單)裡,貼了一個值
     ——entry id 或 `kb://` 來源位址——而它這個 session 只在
     `kbdb_search` 的回應裡出現過
  ② 這個 session 從沒用產品檢索路徑(graph/wiki 內容/query)拿過那筆

為什麼不比對措辭:驗收條件第 4 條寫死不准,而 leo 2026-08-17 已經證偽過
文字層——那天 8 次誤攔、0 次正確攔截,且方向穩定:紅線寫得越細,命中
關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎。值跟動作一樣有限且可枚舉,措辭不是。

🔴 `kbdb_search` 一點都沒有變難用:查 wiki、找 record_id、看某筆在不在,
全部照放。它只在「把搜尋輸出貼出去當產品檢索壞掉的證據」那一刻才響。

實測 31 條,A 群(不該擋)11 條、B 群(該擋)5 條、C 群訊息 6 條、
D 群登記處 9 條。真跡重演=inkstone/Arcrun#167 comment 4865 那則退回,
原文照貼會被擋;走過一次真路徑之後同一份留言就放行。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 21:33:32 +08:00
parent a4eb17df48
commit 43dc93fdda
7 changed files with 625 additions and 6 deletions
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
# 管什麼: 把 kbdb_search 掉出來的原始值當證據,貼進票或派工單裡,而那筆資料從沒走過產品檢索路徑時,擋一次。
# 為什麼: 2026-08-27 inkstone/Arcrun#167 comment 4865——總管用 kbdb_search 的輸出判定「兩個缺陷都還在」,退回一張已經做完、有測試、東西在 main 的票,還重派一條線。半小時後自己更正(comment 4877):那條路本來就不會有那個欄位。
# 誤觸時怎麼關: 指令或派工單裡加 `search-is-evidence: <一句理由>` 重送(留痕);同一 session 只擋一次。
#
# search-is-not-proof-guard.sh — 你判斷的那條路,和你走的那條路,是同一條嗎?
# PreToolUse: Bash / Agent / Task
#
# ── 這一格為什麼原本沒有閘(inkstone/ISEP#30 comment 4879 的原話)──────
# 「所有現有的閘防的是**危險動作**:推 main、推 prod、寫金鑰、打真庫、開重複票。
# 而『用 kbdb_search 去證明某條路壞了』——不危險、不寫檔、不推 main。
# **它只是錯的方法。** ⇒ 閘的座標系裡沒有這一維。」
#
# ── 判準:出處,不是措辭 ────────────────────────────────────────────
# 🔴 驗收條件第 4 條寫死:**不要用關鍵字比對我的措辭**。
# leo 2026-08-17 已經證偽過文字層——那天 **8 次誤攔、0 次正確攔截**,
# 而且方向穩定:紅線寫得越細,命中關鍵字的機率越高 ⇒ **在懲罰謹慎**。
#
# 所以這支一個字都不看。它只問一件機械問題:
#
# 你正要送出去的這份東西裡,有沒有一個**只在 kbdb_search 的回應裡出現過**
# 的值(entry id / `kb://` 來源位址)?而那筆資料,這個 session 從來沒有
# 用產品檢索路徑(graph / wiki 內容)拿過?
#
# ⇒ 兩個條件都成立,才代表「我在拿輔助工具的輸出,當產品檢索路徑的證據」。
# 這是**資料的出處**,不是我的用詞。值跟動作一樣有限且可枚舉。
#
# ── D81:哪條才是產品檢索路徑(leo 2026-08-13 親自裁定)──────────────
# `system-dev/docs/2-architecture/decisions/D81-vector-only-for-entity-normalize.md`
# 「你會用 **graph 找到該找的東西**,用 **LLM wiki 去拉到對的內容**
# 所以全量向量化沒意義⋯⋯**向量的目的是用來協助把 entities normalize**。」
# ⇒ 主路徑=graph + wiki 內容;向量/關鍵字搜尋=**輔助**。
#
# 🔴 **這支不會讓 `kbdb_search` 變難用。** 它是合法的基本盤工具,
# subagent-wiki-guard.sh 還明文要求「查任何東西之前先用它查 wiki」。
# 查一筆資料在不在、找 record_id、翻 wiki 找判例——**全部照放**,
# 因為那些用途不會把 entry id 貼進票裡當證據。
#
# ── 真跡(本閘的規格,Arcrun#167 comment 4865 原文照貼)───────────────
# $ kbdb_search(q="小果 AFTEE 詐貸") → count 5
# { "id": "e_b77f23a1-b467-4aa4-883c-58a051ab8e66", … }
# "metadata_json": "{\"source\":\"kb://小果被AFTEE詐貸.pdf#8\", … }"
# ⇒ 那則退回裡**同時**有 entry id 和 `kb://` 位址,兩類指紋都命中,
# 而整個 session 沒有任何一次 graphwiki 內容的呼叫。
# ⇒ 半小時後 comment 4877:「上一則退回是錯的;真正的缺口是那支工具 MCP 沒有」。
# **那半小時裡,一條線被停掉、一張做完的票被退回、一份工單被重寫。**
#
# ── 誠實邊界(不補,補了就變成文字層)────────────────────────────────
# 1. **改寫過的證據抓不到**——「我看到出處還是 `../` 開頭」這種轉述沒有指紋。
# 但房規本來就要求貼原始輸出(規則四之一、派工鐵律之三),
# 而貼了原始輸出就會被抓到。**不貼的那條路由別的閘管,不歸這支。**
# 2. **只認 KBDB 這一族**。別的「錯的驗證路徑」(拿 portal 畫面當 MCP 的證據——
# 08-27 同一天的另一半)這支看不到,因為那不經過工具呼叫。
# 3. 想不出乾淨判準的地方就留白,不硬做。
# `.claude/branch-holds.md` 檔頭那句:**永遠在響的警報,等於訓練人忽略這個警報。**
set -uo pipefail
INPUT=$(cat)
SID=$(printf '%s' "$INPUT" | python3 -c "
import sys, json
try: print(json.load(sys.stdin).get('session_id','') or 'nosid')
except Exception: print('nosid')
" 2>/dev/null || echo nosid)
SEARCH="/tmp/.kbdb-evidence-search-$SID"
# 這個 session 從來沒用搜尋拿過任何東西 ⇒ 這支沒有立場說話,最便宜的出口放在最前面
[ -s "$SEARCH" ] || exit 0
WARNED="/tmp/.search-is-not-proof-warned-$SID"
[ -f "$WARNED" ] && exit 0 # 同一 session 只擋一次,不鬼打牆
# 🔴 env 要 export,不能掛在 `printf` 前面——那樣只餵到 printfpython3 收不到
export SEARCH_FILE="$SEARCH"
export WALKED_FILE="/tmp/.kbdb-evidence-walked-$SID"
HIT=$(printf '%s' "$INPUT" | python3 -c '
import json, os, re, sys
try:
d = json.load(sys.stdin)
except Exception:
raise SystemExit # 讀不到 payload 就不擋(那不是使用者的錯)
tool = d.get("tool_name") or ""
ti = d.get("tool_input") or {}
if not isinstance(ti, dict):
raise SystemExit
# ── ① 這是不是一個「往外送」的動作?──────────────────────────────
# 往外送 = 這份文字會變成別人的前提:貼進票、關票、記裁決、或發包給另一條線。
# 只在自己腦袋裡看搜尋結果,永遠不會走到這裡。
body, kind = "", ""
if tool in ("Agent", "Task"):
body = ti.get("prompt") or ""
kind = "派工單"
elif tool == "Bash":
cmd = ti.get("command") or ""
if not cmd:
raise SystemExit
outward = False
if re.search(r"\bticket\s+(say|close|decide|new|subtask|handback)\b", cmd):
outward = True
if re.search(r"issues/\d+(/comments)?\b", cmd) and re.search(r"\b(POST|PATCH)\b", cmd):
outward = True
if not outward:
raise SystemExit
body, kind = cmd, "票上的留言"
# 內文常常放在 -F <檔> 裡,指令本身看不到 ⇒ 跟 comment-carries-task-guard 同一招
m = re.search(r"[\s](?:-F|--file)[\s]+(\S+)", cmd)
if m and os.path.isfile(m.group(1)):
try:
body += "\n" + open(m.group(1), encoding="utf-8", errors="replace").read()
except Exception:
pass
else:
raise SystemExit
if not body:
raise SystemExit
# 逃生口(留痕:它會留在指令歷史/派工單上)
if "search-is-evidence:" in body:
raise SystemExit
def load(p):
try:
with open(p, encoding="utf-8") as f:
return {ln.strip() for ln in f if ln.strip()}
except Exception:
return set()
searched = load(os.environ["SEARCH_FILE"])
walked = load(os.environ["WALKED_FILE"])
# ── ② 只在搜尋裡出現過、真路徑沒走過的那些值 ─────────────────────
# 走過真路徑的(graph / wiki 內容 / query)從候選裡扣掉——
# **搜完再去走一次真路徑,本來就是對的做法,不該被罰。**
only_search = searched - walked
if not only_search:
raise SystemExit
# ── ③ 這些值有沒有真的被貼進要送出去的那份東西裡 ──────────────────
hits = sorted(v for v in only_search if v in body)
if not hits:
raise SystemExit
print(kind + "\t" + "\t".join(hits[:3]))
' 2>/dev/null) || exit 0
[ -n "${HIT:-}" ] || exit 0
KIND=$(printf '%s' "$HIT" | cut -f1)
VALS=$(printf '%s' "$HIT" | cut -f2- | tr '\t' '\n' | sed 's/^/ /')
date +%s > "$WARNED"
cat >&2 <<EOF
🧭 你正要把 **kbdb_search 掉出來的原始值**,當成證據送進${KIND}裡。
被貼過去的值(這個 session 只在 kbdb_search 的回應裡出現過):
$VALS
而這個 session **沒有用產品檢索路徑拿過這幾筆**——
graph\`kbdb_graph_neighbors\`)/wiki 內容(\`kbdb_get_record\`)/\`kbdb_query\` 一次都沒有。
【D81leo 2026-08-13 親自裁定】
「你會用 **graph 找到該找的東西**,用 **LLM wiki 去拉到對的內容**⋯⋯
**向量的目的是用來協助把 entities normalize**。」
⇒ 主路徑=graph + wiki 內容。**搜尋是輔助,不是產品檢索路徑。**
🔴 這不是說 \`kbdb_search\` 不能用——它是合法的基本盤工具,查 wiki 本來就該用它。
不能做的是**拿它的輸出,去斷定產品檢索那條路是好是壞**。
【它花掉過多少(inkstone/Arcrun#1672026-08-27)】
12:34 comment 4865 用 kbdb_search 的輸出判定「兩個缺陷都還在」→ 退回一張
**已經做完、有測試、東西在 main** 的票,並重派一條線
13:08 comment 4877 總管自己更正:「上一則退回是錯的;
真正的缺口是**那支工具 MCP 根本沒有**」
⇒ 真相是那次修的是另一支工具的欄位,\`kbdb_search\` 本來就不會有它。
**在用一條錯的路,反覆確認一件錯的事**——而真正的缺口一直沒被看到。
【送出前先回答這一題】
**你要下結論的那條路,和你剛才實際走的那條路,是同一條嗎?**
(08-27 那則退回自己就寫著:「我當時驗的是 portal 的來源顯示,
但這張票的驗收條件是 MCP client 問『原文在哪』。**兩條不同的路。**」)
【出路,挑一條】
① 走一次真的那條路再下結論 —— \`kbdb_graph_neighbors\` / \`kbdb_get_record\` / \`kbdb_query\`
走過之後同一筆資料就不再命中本閘(走過的會從候選裡扣掉)。
② **那條路的工具根本不存在** ⇒ **那個才是你要回報的東西**(4877 就是這樣收的)。
照實寫「產品檢索路徑上少一支工具」,不要改用搜尋去補位再下判斷。
③ 真的只是引用一筆資料、不是在斷定檢索好壞
⇒ 加 \`search-is-evidence: <一句理由>\` 重送(留痕)。
同一 session 只擋這一次。
EOF
exit 2