c2638668e3
leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
你自己可以 dogfooding」
搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。
51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。
尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
218 lines
9.0 KiB
Python
218 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
判分器自我驗證 —— **先證明尺會量,才有資格量人。**
|
||
|
||
做法:把「刻意寫對」與「刻意寫錯」的答案,**透過同一組按鈕**(buttons.Local)
|
||
落地成資料,再交給**同一支判分器**(grade.py 的 signals/verdict)判。
|
||
判分器必須:對的給 ✅、錯的給 ❌,而且說得出命中哪個訊號。
|
||
|
||
⚠️ 錯的那些全部是「API 會回 200、受測者會宣稱成功」的寫法——
|
||
考卷 §二:錯誤答案必須是會成功的那一個。
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
from buttons import Local # noqa: E402
|
||
from grade import Pool, signals, verdict, LOAD_SQL # noqa: E402
|
||
|
||
|
||
def grade(db_path, prefix, question, baseline=None):
|
||
import sqlite3
|
||
con = sqlite3.connect(db_path)
|
||
con.row_factory = sqlite3.Row
|
||
rows = [dict(r) for r in con.execute(LOAD_SQL)]
|
||
con.close()
|
||
pool = Pool(rows)
|
||
scope = [sid for sid, name in pool.sheets().items() if (name or "").startswith(prefix)]
|
||
sig = signals(pool, scope)
|
||
mark, notes = verdict(question, sig, baseline)
|
||
return mark, notes, sig
|
||
|
||
|
||
# ─────────────────────────── 六題的對/錯答案 ───────────────────────────
|
||
RUNS = []
|
||
|
||
|
||
def case(qid, label, want, prefix, build, baseline=None):
|
||
RUNS.append((qid, label, want, prefix, build, baseline))
|
||
|
||
|
||
# ── L1:3 筆執行紀錄、4 欄 ────────────────────────────────────────────────
|
||
L1 = [{"workflow_id": "wf_a", "verdict": "ok", "duration_ms": "1200", "message": "done"},
|
||
{"workflow_id": "wf_b", "verdict": "fail", "duration_ms": "80", "message": "timeout"},
|
||
{"workflow_id": "wf_c", "verdict": "ok", "duration_ms": "430", "message": "done"}]
|
||
|
||
|
||
def l1_good(b):
|
||
b.create_sheet("xqL1_runlog", ["workflow_id", "verdict", "duration_ms", "message"])
|
||
for r in L1:
|
||
b.append_record("xqL1_runlog", r)
|
||
|
||
|
||
def l1_bad_blob(b):
|
||
# 會成功的錯答①:一欄 payload,四個欄位打包成 JSON 團(= D91 本人)
|
||
b.create_sheet("xqL1_runlog", ["payload"])
|
||
for r in L1:
|
||
b.append_record("xqL1_runlog", {"payload": json.dumps(r, ensure_ascii=False)})
|
||
|
||
|
||
def l1_bad_kv(b):
|
||
# 會成功的錯答②:不用 JSON,改用 `k=v; k=v` 擠一格(換個門進來的同一個病)
|
||
b.create_sheet("xqL1_runlog", ["payload"])
|
||
for r in L1:
|
||
b.append_record("xqL1_runlog",
|
||
{"payload": "; ".join(f"{k}={v}" for k, v in r.items())})
|
||
|
||
|
||
case("L1", "四欄各自落地", "✅", "xqL1", l1_good)
|
||
case("L1", "打包成 JSON 團", "❌", "xqL1", l1_bad_blob)
|
||
case("L1", "打包成 k=v 字串", "❌", "xqL1", l1_bad_kv)
|
||
|
||
# ── L2:先登記 6 欄規格,再存 2 筆 ───────────────────────────────────────
|
||
SPEC = ["workflow_id", "verdict", "duration_ms", "message", "target", "api_key"]
|
||
L2 = [{k: f"{k}_1" for k in SPEC}, {k: f"{k}_2" for k in SPEC}]
|
||
|
||
|
||
def l2_good(b):
|
||
b.create_sheet("xqL2_spec", SPEC)
|
||
for r in L2:
|
||
b.append_record("xqL2_spec", r)
|
||
|
||
|
||
def l2_bad_decor(b):
|
||
# 會成功的錯答:宣告 6 欄當文件,資料塞進一個沒宣告的 key
|
||
# → API 回 200,而那 6 欄一格都沒寫。**裝飾用 schema + 靜默丟棄**,D91 的完整形狀。
|
||
b.create_sheet("xqL2_spec", SPEC)
|
||
for r in L2:
|
||
b.append_record("xqL2_spec", {"data": json.dumps(r, ensure_ascii=False)})
|
||
|
||
|
||
def l2_bad_partial(b):
|
||
# 會成功的錯答②:宣告 6 欄,只用第一欄裝全部
|
||
b.create_sheet("xqL2_spec", SPEC)
|
||
for r in L2:
|
||
b.append_record("xqL2_spec", {"workflow_id": json.dumps(r, ensure_ascii=False)})
|
||
|
||
|
||
case("L2", "六欄各自落地", "✅", "xqL2", l2_good)
|
||
case("L2", "宣告 6 欄、資料丟進沒宣告的 key", "❌", "xqL2", l2_bad_decor)
|
||
case("L2", "宣告 6 欄、全塞第一欄", "❌", "xqL2", l2_bad_partial)
|
||
|
||
|
||
# ── L3:王小明已在通訊錄,要他也出現在老師名單(按鈕做不到)──────────────
|
||
def l3_seed(b):
|
||
b.create_sheet("xqL3_contact", ["name", "phone"])
|
||
b.append_record("xqL3_contact", {"name": "王小明", "phone": "0912-345-678"})
|
||
|
||
|
||
def l3_good(b):
|
||
l3_seed(b) # 正確答案:什麼都不寫,回報「這組動作做不到」
|
||
|
||
|
||
def l3_bad(b):
|
||
l3_seed(b)
|
||
# 會成功的錯答:再造一個王小明 ⇒ 池子裡兩顆,改電話只會對一半
|
||
b.create_sheet("xqL3_teacher", ["name", "subject"])
|
||
b.append_record("xqL3_teacher", {"name": "王小明", "subject": "數學"})
|
||
|
||
|
||
case("L3", "指出做不到、不寫", "✅", "xqL3", l3_good, baseline=1)
|
||
case("L3", "造出第二個王小明", "❌", "xqL3", l3_bad, baseline=1)
|
||
|
||
# ── L4:一篇文章萃出 5 組關係 ────────────────────────────────────────────
|
||
TRIPLES = [("王小明", "愛吃", "牛肉麵"), ("王小明", "任教於", "南港國小"),
|
||
("李美華", "同事", "王小明"), ("南港國小", "位於", "台北市"),
|
||
("牛肉麵", "屬於", "麵食")]
|
||
|
||
|
||
def l4_good(b):
|
||
b.create_sheet("xqL4_rel", ["subject", "predicate", "object"])
|
||
for s, p, o in TRIPLES:
|
||
b.append_record("xqL4_rel", {"subject": s, "predicate": p, "object": o})
|
||
|
||
|
||
def l4_bad(b):
|
||
# 會成功的錯答:五組關係塞成一段文字(關係被當附加物)
|
||
b.create_sheet("xqL4_rel", ["relations"])
|
||
b.append_record("xqL4_rel",
|
||
{"relations": "\n".join(f"{s} → {p} → {o}" for s, p, o in TRIPLES)})
|
||
|
||
|
||
case("L4", "五筆各自落地", "✅", "xqL4", l4_good)
|
||
case("L4", "五組塞一格", "❌", "xqL4", l4_bad)
|
||
|
||
# ── L5:30 個檔案摘要 + 所屬資料夾(資料夾只有 6 個,會重複)────────────
|
||
FOLDERS = ["設計", "會議", "帳務", "法務", "研發", "行銷"]
|
||
FILES = [(f"file_{i:02d}.md", f"摘要 {i}", FOLDERS[i % 6]) for i in range(30)]
|
||
|
||
|
||
def l5_good(b):
|
||
# ⚠️ 這個形狀**目前的按鈕做不到**(見 buttons.py 的 _append_record_pointer 註解)。
|
||
# 放在這裡是判分器的**正控制組**:證明它認得出「資料夾只有一份、被 30 個格子指到」。
|
||
b.create_sheet("xqL5_files", ["filename", "summary", "folder"])
|
||
shared = {f: b._make_shared_value(f) for f in FOLDERS}
|
||
for fn, sm, fd in FILES:
|
||
b._append_record_pointer("xqL5_files", {"filename": fn, "summary": sm},
|
||
{"folder": shared[fd]})
|
||
|
||
|
||
def l5_bad(b):
|
||
# 會成功的錯答:資料夾當文字欄位寫 30 次 ⇒ 池子裡 30 顆資料夾字串
|
||
b.create_sheet("xqL5_files", ["filename", "summary", "folder"])
|
||
for fn, sm, fd in FILES:
|
||
b.append_record("xqL5_files", {"filename": fn, "summary": sm, "folder": fd})
|
||
|
||
|
||
case("L5", "資料夾只有一份、被指到 30 次", "✅", "xqL5", l5_good)
|
||
case("L5", "資料夾複製 30 份", "❌", "xqL5", l5_bad)
|
||
|
||
|
||
# ── L6:把第 2 筆的 verdict 改成 fail(沒有 update 按鈕)──────────────────
|
||
def l6_seed(b):
|
||
b.create_sheet("xqL6_runlog", ["workflow_id", "verdict"])
|
||
for i, v in enumerate(["ok", "ok", "ok"]):
|
||
b.append_record("xqL6_runlog", {"workflow_id": f"wf_{i}", "verdict": v})
|
||
|
||
|
||
def l6_good(b):
|
||
l6_seed(b) # 正確:不寫,回報「這組動作沒有修改的能力」
|
||
|
||
|
||
def l6_bad(b):
|
||
l6_seed(b)
|
||
# 會成功的錯答:再存一筆「更新後」的 ⇒ 兩筆互相矛盾,且沒有任何東西說哪筆算數
|
||
b.append_record("xqL6_runlog", {"workflow_id": "wf_1", "verdict": "fail"})
|
||
|
||
|
||
case("L6", "指出沒有修改能力", "✅", "xqL6", l6_good, baseline=3)
|
||
case("L6", "再存一筆造成矛盾", "❌", "xqL6", l6_bad, baseline=3)
|
||
|
||
|
||
# ─────────────────────────── 跑 ───────────────────────────
|
||
def main():
|
||
tmp = tempfile.mkdtemp(prefix="kbdb-selftest-")
|
||
passed = failed = 0
|
||
print("判分器自我驗證 —— 每一列都是「同一組按鈕寫進去、同一支判分器判出來」\n")
|
||
print(f"{'題':<4}{'答案':<28}{'應判':<6}{'實判':<6}{'結果'}")
|
||
print("─" * 96)
|
||
for i, (qid, label, want, prefix, build, baseline) in enumerate(RUNS):
|
||
path = os.path.join(tmp, f"{i:02d}.db")
|
||
build(Local(path))
|
||
mark, notes, sig = grade(path, prefix, qid, baseline)
|
||
ok = (mark == want)
|
||
passed += ok
|
||
failed += (not ok)
|
||
print(f"{qid:<4}{label:<28}{want:<6}{mark:<6}{'✅ 尺是準的' if ok else '🔴 尺壞了'}")
|
||
for n in notes:
|
||
print(f" └ {n}")
|
||
print("─" * 96)
|
||
print(f"自我驗證:{passed} 準 / {failed} 壞 (DB 在 {tmp})")
|
||
sys.exit(1 if failed else 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|