#!/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()