補上「用錯的路去證明一件事」那一格的閘(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
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# 管什麼: 記下「這個 session 看到的每一筆 KBDB 資料,是從哪一條路拿到的」——搜尋出來的,還是走產品檢索路徑拿到的。
# 為什麼: 2026-08-27 inkstone/Arcrun#167 comment 4865,總管把 kbdb_search 的輸出當成「產品檢索壞了」的證據,退回一張已經做完的票、重派一條線。錯的不是那句話,是那條路。
# 誤觸時怎麼關: 這支不擋任何東西,只留檔。要停用就從 hooks.json 拿掉它(配套的 search-is-not-proof-guard 會跟著失效,見該檔)。
#
# kbdb-evidence-stamp.sh — 證據的出處登記處(PostToolUse: 各 kbdb_* 工具)
#
# ── 為什麼是「出處」而不是「措辭」───────────────────────────────────
# leo 2026-08-17 證偽過文字層:那天在文字層封路的閘 **8 次誤攔、0 次正確攔截**,
# 且方向穩定——紅線寫得越細,命中關鍵字的機率越高 ⇒ **那些閘在懲罰謹慎**。
# leo 原話:「自然語言的變體是無限的,blacklist 永遠追不完。
# 封路哲學之所以有效,是因為它封的是**動作**——動作有限且可枚舉。」
#
# ⇒ 所以本閘不看你怎麼說,看你**引用的那筆資料是從哪支工具掉出來的**。
# 資料值跟動作一樣是有限且可枚舉的:一個 entry id、一個 `kb://` 來源位址,
# 它要嘛出現在 kbdb_search 的回應裡,要嘛沒有。**這是事實,不是判讀。**
#
# ── 兩條路的分別(D81leo 2026-08-13 親自裁定)────────────────────
# `system-dev/docs/2-architecture/decisions/D81-vector-only-for-entity-normalize.md`
# Graph(三元組/entities → **找到該找的東西** ← 產品檢索路徑
# LLM Wiki(卡片文本) → **拉到對的內容** ← 產品檢索路徑
# 向量/關鍵字搜尋 → **協助 entities normalize****輔助,不是檢索主路徑**
#
# 🔴 **`kbdb_search` 本身完全合法**,這支不會讓它變難用——
# subagent-wiki-guard.sh 還明文要求「查任何東西之前先用它查 wiki」。
# leo 的話是「不能拿它當**產品檢索路徑**」,不是「不准呼叫」。
# ⇒ 本檔只登記出處,判斷留給 search-is-not-proof-guard.sh,而那支的觸發條件
# 窄到「你把搜尋結果的原始值貼進票或派工單裡當證據」才會響。
#
# ── 平台依據(查證過,不是推測)─────────────────────────────────────
# PostToolUse 的 stdin payload 帶得到工具的**輸出**
# `@anthropic-ai/claude-code/sdk.d.ts:54-59`
# export type PostToolUseHookInput = BaseHookInput & {
# hook_event_name: 'PostToolUse'; tool_name: string;
# tool_input: unknown; tool_response: unknown; };
# 實跑的那顆(Caskroom 2.1.220)內建說明同樣寫著
# `"tool_response": { "success": true } // PostToolUse only`。
#
# ── fail-closed:這支失效的時候要出聲,不准靜默 ──────────────────────
# `kbdb-asked-stamp.sh` 檔頭記著 leo 2026-08-08 的原話:
# 「你自己寫了 hook,結果**一次次改都沒產生用途**,問題太大」。
# ⇒ 所以 payload 裡**根本沒有 `tool_response` 這個欄位**時(=平台換了介面),
# 本檔走 exit 2 把話講到 stderr 上(PostToolUse 的 exit 2 不會擋工具,
# 只會把訊息交給 Claude 看)。**寧可吵一次,也不要變成一支活著卻沒作用的閘。**
# 「回應讀得到但裡面沒有指紋」是正常情況(搜尋 0 筆),不出聲。
set -uo pipefail
INPUT=$(cat)
OUT=$(printf '%s' "$INPUT" | python3 -c '
import json, os, re, sys
try:
d = json.load(sys.stdin)
except Exception:
print("SKIP"); raise SystemExit
tool = d.get("tool_name") or ""
sid = d.get("session_id") or "nosid"
# 這一族才登記;別的工具直接走人
if "kbdb_" not in tool:
print("SKIP"); raise SystemExit
# 🔴 D81 的兩條產品檢索路徑 vs 輔助工具。判斷只看工具名,不看內容。
WALKED = ("kbdb_get_record", "kbdb_graph_neighbors", "kbdb_query", "kbdb_get_map")
if any(w in tool for w in WALKED):
kind = "walked"
elif "kbdb_search" in tool:
kind = "search"
else:
print("SKIP"); raise SystemExit # 寫入類工具(create_*)不是證據來源
if "tool_response" not in d: # ← 平台換介面了,本閘等於死了
print("NOFIELD"); raise SystemExit
# 回應可能是 dict、list、字串、或 MCP 的 content blocks——一律壓成字串再找指紋。
# 這樣就不必猜它今天是哪一種包裝。
def flatten(x, acc):
if isinstance(x, str):
acc.append(x)
elif isinstance(x, dict):
for v in x.values(): flatten(v, acc)
elif isinstance(x, (list, tuple)):
for v in x: flatten(v, acc)
elif x is not None:
acc.append(str(x))
parts = []
flatten(d.get("tool_response"), parts)
try:
parts.append(json.dumps(d.get("tool_response"), ensure_ascii=False))
except Exception:
pass
blob = "\n".join(parts)
# ── 指紋只取兩類,兩類都是「值」不是「措辭」───────────────────────
# ① entry id`e_<uuid>`。36 字元的 UUID,實質上不會誤撞。
# ② 來源位址:`kb://…`。它在 metadata_json 那串裡,而**跳脫前後長得一樣**
# (貼進 markdown 會變成 \"…\",但 kb:// 那段本身不變)⇒ 貼過去也認得出來。
# 兩類都是可枚舉的資料值,正好對上 leo 8-17「動作有限且可枚舉,文字不是」。
ids = set(re.findall(r"\be_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", blob))
srcs = set(re.findall(r"kb://[^\"\\\s,}\]]{6,}", blob))
fps = sorted(ids | srcs)
path = "/tmp/.kbdb-evidence-%s-%s" % (kind, sid)
if fps:
try:
with open(path, "a", encoding="utf-8") as f:
for x in fps:
f.write(x + "\n")
os.chmod(path, 0o600)
except Exception:
print("WRITEFAIL"); raise SystemExit
print("OK %s %d" % (kind, len(fps)))
' 2>/dev/null) || OUT="PYFAIL"
case "${OUT:-}" in
NOFIELD)
cat >&2 <<'EOF'
⚠️ kbdb-evidence-stamp 失效了:這次的 PostToolUse payload **沒有 `tool_response` 欄位**。
它靠這個欄位登記「這筆資料是從哪條路拿到的」,欄位不見 =
`search-is-not-proof-guard.sh` 從現在起**永遠不會響**(而且不會有人發現)。
依據(本閘寫的時候查證過):
@anthropic-ai/claude-code/sdk.d.ts:54-59 PostToolUseHookInput 帶 `tool_response`
⇒ 平台介面變了。開一張票去修這兩支,不要當作沒看到——
一支活著卻沒作用的閘,比沒有閘更糟:它會讓人以為這一格有人守。
EOF
exit 2 ;;
PYFAIL|WRITEFAIL)
cat >&2 <<'EOF'
⚠️ kbdb-evidence-stamp 這次沒登記成功(python3 或 /tmp 寫入失敗)。
後果:這一次的 kbdb 呼叫不會被 `search-is-not-proof-guard.sh` 看見。
不影響你手上的工作,但**這一格暫時沒人守**——請自己確認一次
「我等一下要下的結論,走的是不是我實際查過的那條路」。
EOF
exit 2 ;;
esac
exit 0