#!/bin/bash # PreToolUse hook — 影子表警察:抓 D91 的指紋(宣告 schema 在一邊,資料倒進 JSON 團在另一邊) # # 🚫 **本檔目前是死的:沒有註冊進 .claude/settings.json,不會被執行。** # leo 2026-08-15:「**我是要你提案封鎖方式,不是要你就去做,急着做造成太多疊牀架屋了。**」 # ⇒ 總管在討論「疊牀架屋是病」的同時去疊了這一層。已測過(該擋兩條擋、六條不該擋全放), # 但**要不要掛由 leo 裁**——它只在「過渡期止血」這一個用途上值得存在。 # ⇒ 真正的封鎖方式是**刪掉 metadata_json 與 entry_type 兩個欄位**(減,不是加), # 提案見 matrix/arcrun 的 pending-changes.md。 # # ⚠️ 這支是**過渡期的止血帶,不是答案**(leo 2026-08-15 定調)。 # 真正的解是「**刪掉那兩個欄位,讓儲存層沒有位置放壞東西**」——提案在 matrix/arcrun 的 pending-changes.md。 # 偵測永遠落後一步:D91 就是從「多開一張表」變異成「JSON 團」的。 # 在收窄的 API 出來之前,這支至少擋得住已知的那個形狀。**別因為有閘就以為安全。** # # 【為什麼 kbdb-api-wall-guard 抓不到】 # 那支守著 D38「三張表打天下,永不加表」,而 D91 **完全沒有違反它**——一張表都沒加。 # 它做的是:seed 一張宣告 6 個欄位的 template,然後把資料倒進 metadata_json。 # ⇒ **一張沒人管得到的影子表,而表數是三。現有的閘數表,而病不在表數上。** # # 【病徵】kbdb/migrations/0004_execution_log_template.sql 自己的註解就寫著 # 「template 這裡只負責 schema 文件化⋯⋯結構化欄位打包進 metadata_json」 # ⇒ 那張宣告了 6 個欄位的 template,沒有任何東西讀它。 # 【擴散】同註解:「recipe_stat 早已示範這個模式合法」 # ⇒ 活著的 7 種 entry_type 裡有 6 種是這形狀——不是角落,是多數。 # # 攔的是**形狀不是關鍵字**:型別標籤與 JSON 團打包必須**同時**出現才命中 # (今天已有四次閘因為比對指令形狀而誤攔,不再犯)。註解、測試、文件一律放行。 # 豁免:該行尾加 `shadow-table-ok`(留痕,commit 說明理由)。 # # 全文:頂層 system-dev/wiki/decisions-summary.md D91、D92。 exec python3 -c ' import json, re, sys try: d = json.load(sys.stdin) except Exception: sys.exit(0) if d.get("tool_name") not in ("Write", "Edit", "MultiEdit"): sys.exit(0) i = d.get("tool_input") or {} path = i.get("file_path") or "" if not path: sys.exit(0) parts = [i.get("content") or "", i.get("new_string") or ""] for e in (i.get("edits") or []): parts.append(e.get("new_string") or "") body = "\n".join(p for p in parts if p) if not body: sys.exit(0) low = path.lower() if re.search(r"\.(md|txt|json|yaml|yml)$", low): sys.exit(0) if re.search(r"(^|/)(wiki|docs|tests?)/", low): sys.exit(0) if re.search(r"(test|spec)\.[a-z]+$|_test\.go$", low): sys.exit(0) if not re.search(r"\.(ts|js|mjs|sql|go)$", low): sys.exit(0) live = [] for line in body.splitlines(): if "shadow-table-ok" in line: continue s = re.sub(r"--.*$", "", line) s = re.sub(r"//.*$", "", s) if re.match(r"\s*[*#]", s): continue live.append(s) live = "\n".join(live) # A:把型別標籤釘在 entries 上 a = len(re.findall(r"""entry_type\s*[:=]\s*[\x27"][a-z_]+[\x27"]""", live)) # B:結構化欄位被打包進 JSON 團 b = len(re.findall(r"""metadata_json[^\n]{0,80}(JSON\.stringify|json_object|\{)|(JSON\.stringify|json_object)\([^\n]{0,80}metadata_json""", live)) # C:seed 一張宣告了欄位的 template c = len(re.findall(r"""INSERT\s+(OR\s+IGNORE\s+)?INTO\s+templates\b[^;]{0,200}slots_json|slots_json\s*[:=]""", live, re.I | re.S)) if not ((a and b) or (c and b)): sys.exit(0) sys.stderr.write(f"""❌ BLOCKED — 影子表警察(D91 的指紋) 檔案:{path} 命中:型別標籤 {a} 處/JSON 團打包 {b} 處/template 宣告 {c} 處 你正在寫的形狀是:**宣告一個 schema,然後把資料倒進 metadata_json**。 那會做出一張**沒人管得到的影子表**——表數還是三,但欄位從此改不動: 要加一欄,得同時改「一個沒人讀的宣告」和「一段打包程式」, 而**沒有任何機制會告訴你它們已經對不上了**。 leo 2026-08-15:「這每個都是一個 template⋯⋯**以後你不能修改 fields,因為表禁止動**, 如果要多一欄,要不就是說要改表,**要不就是全塞在能塞的格子裡**。」 先回答這三題,答不出來就不是這樣寫: ① 這些被打包的欄位,**誰會讀 schema 宣告**?沒有人讀 ⇒ 那個宣告是裝飾品 ② 要加第八個欄位時,**有什麼東西會擋住「只改了一半」**? ③ 這批資料的形狀是**我們決定**的,還是**使用者的想法**決定的? 使用者決定 ⇒ 不准有 schema,走 v-table (Arcrun#133 的判準:這張表要改的時候,誰付 migration 的代價?) 真的有理由這樣寫(例:那條路徑的寫入放大有實測數字,且已在票上記錄) ⇒ 該行尾加 `shadow-table-ok`,並在 commit 說明理由。 ⚠️ 本閘是過渡期止血帶。真正的解是刪掉 metadata_json 與 entry_type(減,不是加)。 參考:system-dev/wiki/decisions-summary.md **D91**(病徵與擴散)、**D92**(同族形狀) """) sys.exit(2) '