Files
ISEP/hooks/kbdb-evidence-stamp.sh
Leo 7de1ad6be6 補上「用錯的路去證明一件事」那一格的閘(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>
2026-08-27 21:38:17 +08:00

144 lines
7.3 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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