Files
ISEP/scripts/kbdb-live-exam/report.py
T
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

114 lines
4.7 KiB
Python
Raw 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-動作清單考卷.md` §七點五):**每格至少跑三次**,
記成 `n/3`,不寫單一結果——一次的差異一律當雜訊。
兩種來源:
本機乾跑 report.py --sqlite a.db b.db c.db
正式考試 report.py --d1 <dbname> --account-id … --token-env … --runs A,B,C
🔴 **正式考試三次跑在同一台實例上**,所以每一次有自己的 run tag:
sheet 叫 `xq<tag><題號>_…`L3 的那個人也每次換一個名字
——否則 A 跑留下的東西會被算進 B 跑的分數。
"""
import argparse
import os
import sqlite3
import sys
from collections import defaultdict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from grade import Pool, signals, verdict, RUBRIC, LOAD_SQL, load_d1 # noqa: E402
QUESTIONS = ["L1", "L2", "L3", "L4", "L5", "L6"]
# 每一次考試自己的人名(L3 的 entity_once 是**整池**判定,不換名字會跨 run 互相污染)
RUN_PERSON = {"": "王小明", "A": "王小明", "B": "陳大文", "C": "林志明"}
def load_rows(source, dbname="", account_id="", token=""):
if source.endswith(".db"):
con = sqlite3.connect(source)
con.row_factory = sqlite3.Row
rows = [dict(r) for r in con.execute(LOAD_SQL)]
con.close()
return rows
return load_d1(dbname, account_id, token)
def grade_rows(rows, qid, tag=""):
pool = Pool(rows)
prefix = f"xq{tag}{qid}"
scope = [sid for sid, name in pool.sheets().items()
if (name or "").startswith(prefix)]
sig = signals(pool, scope)
rule = dict(RUBRIC[qid])
if "entity_once" in rule:
rule = {**rule, "entity_once": RUN_PERSON.get(tag, "王小明")}
# verdict 讀的是 RUBRIC,這裡暫時換掉那一格再換回來(不改共用狀態的語意)
saved = RUBRIC[qid]
RUBRIC[qid] = rule
try:
mark, notes = verdict(qid, sig, None)
finally:
RUBRIC[qid] = saved
return mark, notes, sig
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--sqlite", nargs="*", default=[])
ap.add_argument("--d1", default="")
ap.add_argument("--account-id", default="")
ap.add_argument("--token-env", default="CLOUDFLARE_API_TOKEN_YOULIN_CC_USE")
ap.add_argument("--runs", default="", help="正式考試的 run tag,逗號分隔,如 A,B,C")
a = ap.parse_args()
jobs = [] # (label, rows, tag)
if a.sqlite:
for p in a.sqlite:
jobs.append((os.path.basename(os.path.dirname(p)) or os.path.basename(p),
load_rows(p), ""))
if a.d1:
rows = load_rows("d1", a.d1, a.account_id, os.environ.get(a.token_env, ""))
print(f"(從 D1 讀到 {len(rows)} 顆 entry —— 判分只看落地資料,不看受測者自述)")
for tag in [t.strip() for t in a.runs.split(",") if t.strip()]:
jobs.append((f"run {tag}", rows, tag))
tally = defaultdict(list)
for label, rows, tag in jobs:
print(f"\n════ {label} ════")
# 🔴 生死檢查,**必須在計分之前**(2026-08-16 實撞):
# L3/L6 的正確答案是「什麼都不寫」——所以一個**完全沒動手**的受測者
# 會在那兩題拿到 ✅。那不是答對,那是沒考。
# 實例:run A 自述六題全做完、還報出每一筆內容,而 D1 裡一筆都沒有。
# ⇒ 先問「這次到底有沒有寫進任何東西」,沒有就整場作廢,不給任何 ✅。
wrote = sum(grade_rows(rows, q, tag)[2]["records"] for q in ("L1", "L2", "L4", "L5"))
if wrote == 0:
print(" 🔴 這一場作廢:四個「該寫東西」的題目加起來一筆都沒落地。")
print(" (L3/L6 的正解是不寫 ⇒ 沒動手的人會假性通過那兩題,不予計分)")
for q in QUESTIONS:
tally[q].append("作廢")
continue
for q in QUESTIONS:
mark, notes, sig = grade_rows(rows, q, tag)
tally[q].append(mark)
print(f" {q} {mark} {RUBRIC[q]['title']}")
for n in notes:
print(f" └ {n}")
n = len(jobs)
print(f"\n════ 記分板(n={n},只有跨全部樣本一致的差異才算訊號)════")
print(f"{'題':<5}{'✅':<5}{'◐':<5}{'❌':<5}{'作廢':<5} 標題")
for q in QUESTIONS:
m = tally[q]
print(f"{q:<5}{m.count(chr(9989)):<5}{m.count(chr(9680)):<5}{m.count(chr(10060)):<5}{m.count('作廢'):<5} {RUBRIC[q]['title']}")
if __name__ == "__main__":
main()