#!/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 的回應裡,要嘛沒有。**這是事實,不是判讀。** # # ── 兩條路的分別(D81,leo 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_`。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