Files
Leo c2638668e3 ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份
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>
2026-08-20 11:41:46 +08:00

154 lines
7.2 KiB
Python
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 python3
"""
考前指紋:**這一筆數據是打在哪一版 kbdb 上量出來的。**
立它的理由(總管 2026-08-15,出貨管線 preflight 抓到的):
kbdb 的出貨成品比源碼舊 3 顆 commit,其中一顆正是 v7 拆表那顆
⇒ youlin 上那顆 worker 什麼時候變成 v7,取決於出貨走到哪一步。
⇒ **不標指紋就會量出「模型寫錯了」,而真兇是它打到的那台還在跑舊模型。**
兩個獨立來源,**刻意都留**(一個從使用者那條路量、一個從帳號那邊量):
① 行為指紋(不需要任何憑證,走 `acr` = 使用者真的會走的那條路)
建一張拋棄式 sheet,寫一筆 → 回應直接說出它是哪一版:
`no such table: entry_values` → 舊碼(v7 之前)
成功 → 新碼(v7 之後)
🔑 這一支的價值在於**它量的就是受測者會撞到的那個東西**。
② 部署指紋(需要該帳號的 CF token,唯讀)
worker 的 modified_on etag。它答的是「這顆什麼時候被換過」。
⚠️ 兩者都不是 commit sha。**worker 上沒有 sha 可讀**——
所以這裡誠實地記「行為 + 換過的時間」,不假裝知道它是哪一顆 commit。
"""
import argparse
import json
import os
import subprocess
import time
import uuid
def behavior_fingerprint(workdir):
"""走 acr(使用者那條路)問一句:你是 v7 之前還是之後?"""
name = f"xqfp_{uuid.uuid4().hex[:8]}"
out = {"probe_sheet": name}
def run(args):
r = subprocess.run(["acr", "kbdb"] + args, cwd=workdir,
capture_output=True, text=True, timeout=120)
return r.returncode, (r.stdout + r.stderr).strip()
who = subprocess.run(["acr", "whoami"], cwd=workdir,
capture_output=True, text=True, timeout=60).stdout
out["whoami"] = [l.strip() for l in who.splitlines() if l.strip()][:6]
rc, txt = run(["template", "create", name, "--slots", "a,b"])
out["template_create"] = {"exit": rc, "tail": txt[-160:]}
rc, txt = run(["record", "create", name, "--values", "a=1", "--values", "b=2"])
out["record_create"] = {"exit": rc, "tail": txt[-200:]}
if rc == 0:
# 🔴 2026-08-16 這一行我寫錯過一次,留著當警示:
# 原本是「rc==0 ⇒ v7+(關係列模型已上線)」。**寫得進去 ≠ 寫成新形狀。**
# 那天 acr update 之後 record create 又成功了,我就宣告 v7+;
# 實際是 `entry_values` 被 migration 重播**復活**,舊碼照樣往那張表寫
# ⇒ 考卷寫進去的 310 列全在那張死掉的表裡,新模型那邊一列都沒長。
# ⇒ 行為探針只能證明「寫得進去」,**證明不了寫成什麼形狀**。形狀要另外量。
out["kbdb_generation"] = "寫得進去,但**形狀未驗**——要看 shape 那一段才算數"
elif "no such table: entry_values" in txt:
out["kbdb_generation"] = "pre-v7(舊碼碰新庫:worker 還在寫 entry_values"
else:
out["kbdb_generation"] = "未知(寫入失敗但不是拆表那個原因,看 tail)"
return out
def deploy_fingerprint(account_id, token, scripts=("arcrun-kbdb", "arcrun-cypher-executor",
"arcrun-mcp")):
"""worker 上一次被換掉是什麼時候(唯讀)。"""
r = subprocess.run(
["curl", "-s", "--max-time", "25", "-H", f"Authorization: Bearer {token}",
f"https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts"],
capture_output=True, text=True, timeout=60).stdout
try:
data = json.loads(r).get("result", [])
except Exception:
return {"error": "CF API 回應不是 JSON"}
return {s["id"]: {"modified_on": s.get("modified_on"), "etag": (s.get("etag") or "")[:16]}
for s in data if s["id"] in scripts}
# 受測者可能讀到教材的地方。**一個都不能漏,漏掉的那份會安靜地教錯。**
# 2026-08-15 實證:同名的 kbdb-api-wall-guard.sh 有兩份都會開火,
# 只有 repo 那份被修好(efa64d8),全域 skill 那份還在說
# 「exactly three core tables: entries / templates / entry_values」。
MATERIAL_PATHS = [
"~/.claude/skills/arcrun-kbdb-guardrails",
".claude/hooks/kbdb-api-wall-guard.sh",
"arcrun_harness/.claude/skills/arcrun-kbdb-guardrails",
]
# 已經死掉的東西:教材裡出現它**而且沒有同時說它死了**,就是還在當現行做法教。
DEAD_TERMS = ["entry_values"]
TOMBSTONE_MARKS = ["🪦", "廢除", "已死", "0007", "已被推翻", "提議廢掉", "尚未 confirm"]
def materials_fingerprint(paths=MATERIAL_PATHS):
"""教材指紋:這一筆數據是在哪一版教材底下量的。
🔑 判準不是「有沒有提到死掉的東西」——**歷史要留著**
(保留歷史而不是抹掉,那是 efa64d8 自己選的做法,對的)。
判準是「提到它的那一段,有沒有說它死了」。
"""
out = {}
for p in paths:
real = os.path.expanduser(p)
if not os.path.exists(real):
out[p] = {"status": "不存在"}
continue
walk = ([real] if os.path.isfile(real)
else [os.path.join(d, f) for d, _, fs in os.walk(real) for f in fs])
files = []
for f in walk:
if f.endswith((".png", ".jpg", ".pyc", ".bak")):
continue
try:
lines = open(f, encoding="utf-8", errors="ignore").read().splitlines()
except Exception:
continue
stale = []
for i, line in enumerate(lines):
if not any(t in line for t in DEAD_TERMS):
continue
# 墓碑註記可能寫在前後幾行,看一個小範圍再判
#(避免把「刻意保留的歷史」誤報成過期教材)
ctx = "\n".join(lines[max(0, i - 3):i + 4])
if not any(m in ctx for m in TOMBSTONE_MARKS):
stale.append(i + 1)
if stale:
files.append({"file": f.replace(os.path.expanduser("~"), "~"),
"stale_lines": stale[:8]})
out[p] = {"status": "🔴 還在教死掉的東西" if files else "✅ 乾淨", "files": files}
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--workdir", required=True, help="考場資料夾(含 .arcrun.yaml")
ap.add_argument("--materials-only", action="store_true", help="只查教材指紋,什麼都不寫")
ap.add_argument("--account-id", default="")
ap.add_argument("--token-env", default="CLOUDFLARE_API_TOKEN_YOULIN_CC_USE")
ap.add_argument("--no-probe", action="store_true", help="只取部署指紋,不寫任何東西")
a = ap.parse_args()
stamp = {"taken_at": time.strftime("%Y-%m-%dT%H:%M:%S%z")}
if a.account_id and os.environ.get(a.token_env):
stamp["deploy"] = deploy_fingerprint(a.account_id, os.environ[a.token_env])
if not a.no_probe:
stamp["behavior"] = behavior_fingerprint(a.workdir)
print(json.dumps(stamp, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()