Files
meta/tools/check-delegate.sh
T
jiantw83 8bb1effb93 feat(delegate): 技能委派判定的判準文件、清單與檢核腳本
助理要知道每一支技能能不能交給它代跑,但那份盤點目前只存在於一份待辦文件的表格裡。技能組每天在動,手工盤點只會過期,而且過期得無聲無息——今天就有三件事栽在這上面:照舊版資訊列的待辦白列了、一份文件寫著「8 碼」而工具早就改用 40 碼、一份技能文件描述的路徑家族已經不存在。

判準寫成 references 一份文件,四支技能異動技能共用。內容是判定決策樹的五題與四種結論各自的必填欄位。

判定結果寫成 tools 底下一份 TSV,一支技能一列,十一個欄位。作法沿用設定規格表那一套:註解檔頭說明每一欄的意思與可填值、TAB 分隔、空值寫短橫。技能名分成 domain 與 name 兩欄,形狀跟技能清單工具的輸出一致,比對時直接取前兩欄就好。

初始 35 列依既有盤點種入,來源欄標 seed,代表還沒正式走過決策樹、待複核。技能有異動時就地走一次決策樹再轉成 judged。

檢核腳本比對清單與實際技能:缺列或多列算缺失,必填欄位空著也算缺失——切片交與條件式交沒填「留在人手上的那一半」,助理下一輪會把整支技能當成可交的一路跑完。版本號落後與來源是 seed 都只印提示、不影響結束碼,理由寫進腳本註解:版本號是 domain 層級,同 domain 改一支技能其餘技能也會被標到,當成缺失的話每次發版整個 domain 都亮紅,提示很快就會被當成雜訊忽略。

種入時有一支改判。體檢那一支在既有盤點裡是無條件的切片交,實測確認它呼叫自家腳本走的是帶版本號的路徑,那種路徑進不了允許清單,無人值守的每一輪都會無聲卡在第一支腳本,所以改判條件式交,把條件與條件不成立時的行為都寫進去。留成切片交的話,下一個實作定期健檢的人會看到一列寫著可以交,然後重演一次。

這一輪只建地基。四支技能異動技能還沒引用這份判準與這支腳本,例行稽核也還沒把它排進檢查群組,接線另外一輪做。
2026-09-03 14:00:36 +08:00

226 lines
11 KiB
Bash
Executable File
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 sh
# check-delegate.sh — 檢查委派判定清單 tools/delegate-spec.tsv 有沒有跟本機實際技能對齊。
#
# 用法: check-delegate.sh [<plugins-root>]
# 不給參數就交給 tools/plugins-root.sh 推導根目錄,判準與 list-skills.sh 完全相同。
#
# 檢查四項:
# 1. 一一對應 — 清單一支技能一列,跟 list-skills.sh 的實際技能不多不少,也不重複。
# 2. 欄位齊全 — 11 欄,每一欄非空;四種結論各自的必填欄位都填了,不該填的填 -。
# 3. 欄位可填值 — verdict、way、trigger、recur、origin 只認固定詞彙;next 要指到
# 一支真的存在的技能。指到不存在的技能,助理會一路重試一支叫不出來的東西。
# 4. 版本號與來源 — 只印提示,不影響結束碼,理由見下。
#
# 為什麼要這支: 技能組每天在動,手工盤點只會過期。清單缺一列,助理就永遠不知道那支技能
# 存在;清單多一列,助理會去觸發一支不存在的技能,而且失敗不會自動暫停。這兩件事靠眼睛
# 比對每輪都會漏,交給程式判定才穩。
#
# 為什麼版本號落後只算提示: 版本號是 domain 層級的,同一個 domain 改一支技能,其餘技能的
# 版本號也會一起落後、也會被標成要複判。當成稽核缺失的話,每次發版整個 domain 都亮紅,
# 提示很快就會被當成雜訊忽略,真正要複判的那一支反而看不見。
#
# 為什麼 origin=seed 只算提示: 種入的那一批是依既有盤點填的,還沒正式走過決策樹,本來就
# 知道它待複核。它是「已經有結論、但要再確認一次」,不是「漏填」。列成缺失會讓清單從
# 第一天就是紅的,紅到全部複核完為止,這段期間所有真的缺失都會被蓋掉。
#
# 輸出: 缺失一行一個,格式 {清單路徑}:{domain}/{name}:{說明}(stderr);
# 提示同格式(stdout),讓呼叫端分得開缺失與提示;統計摘要走 stderr。
# 結束碼: 0=清單與實際技能一一對應、必填欄位齊全(可能帶提示,提示不影響這一碼)
# 1=缺列、多列、重複列、欄位空著、欄位值不合法,或 next 指到不存在的技能,
# 清單在 stderr
# 2=用法錯誤(本腳本最多吃一個參數)
# 3=找不到 tools/delegate-spec.tsv、推導不出根目錄,或列不出任何技能——
# **什麼都沒查**,不等於通過
set -u
usage() {
echo 'usage: check-delegate.sh [<plugins-root>]' >&2
exit 2
}
[ "$#" -le 1 ] || usage
if [ "$#" -eq 1 ]; then
[ -n "$1" ] || usage
JSC_PLUGINS_ROOT=${1%/}
export JSC_PLUGINS_ROOT
fi
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
SPEC="$HERE/delegate-spec.tsv"
[ -f "$SPEC" ] || { echo "找不到委派判定清單:$SPEC" >&2; exit 3; }
. "$HERE/plugins-root.sh"
ROOT=$(jsc_plugins_root) || { echo "推導不出技能組工作目錄根位置" >&2; exit 3; }
TMPD=$(mktemp -d) || { echo "無法建立暫存目錄" >&2; exit 3; }
trap 'rm -rf "$TMPD"' EXIT
TAB=$(printf '\t')
# 實際技能清單。真實來源只有 list-skills.sh 一處,本腳本不自己掃 SKILL.md:
# 兩套掃法會各自漂移,屆時分不出是清單錯還是掃法錯。
"$HERE/list-skills.sh" > "$TMPD/raw.txt" 2>/dev/null \
|| { echo "list-skills.sh 列不出技能,先跑 sync-domains.sh:$ROOT" >&2; exit 3; }
cut -f1,2 "$TMPD/raw.txt" | LC_ALL=C sort > "$TMPD/skills.txt"
[ -s "$TMPD/skills.txt" ] || { echo "掃不到任何技能:$ROOT" >&2; exit 3; }
# 各 domain 的現行版本號。domain 名以 plugin.json 的 name 為準,checkout 目錄名只是退路:
# 目錄名是誰 clone 誰決定的,換一台機器就可能叫別的名字。
: > "$TMPD/versions.txt"
for p in "$ROOT"/*/plugin.json; do
[ -f "$p" ] || continue
n=$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$p" | head -n1)
v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$p" | head -n1)
case $n in
jsc-?*) ;;
*) continue ;;
esac
[ -n "$v" ] || continue
printf '%s\t%s\n' "${n#jsc-}" "$v" >> "$TMPD/versions.txt"
done
# 清單資料列:# 開頭的註解與空行都不算。
grep -v '^[[:space:]]*#' "$SPEC" | grep -v '^[[:space:]]*$' > "$TMPD/rows.txt" || true
[ -s "$TMPD/rows.txt" ] || { echo "清單裡一列資料都沒有:$SPEC" >&2; exit 3; }
: > "$TMPD/fail-set.txt"
: > "$TMPD/fail-row.txt"
: > "$TMPD/hint.txt"
report() { # $1=domain/name 或 -,$2=說明
printf '%s:%s:%s\n' "$SPEC" "$1" "$2" >> "$TMPD/fail-set.txt"
}
# --- 1. 一一對應 ---
cut -f1,2 "$TMPD/rows.txt" | LC_ALL=C sort > "$TMPD/keys.txt"
LC_ALL=C uniq -d "$TMPD/keys.txt" > "$TMPD/dup.txt"
LC_ALL=C uniq "$TMPD/keys.txt" > "$TMPD/keys-uniq.txt"
LC_ALL=C comm -23 "$TMPD/skills.txt" "$TMPD/keys-uniq.txt" > "$TMPD/missing.txt"
LC_ALL=C comm -13 "$TMPD/skills.txt" "$TMPD/keys-uniq.txt" > "$TMPD/extra.txt"
while IFS="$TAB" read -r d n; do
[ -n "${d:-}" ] || continue
report "$d/$n" '清單有兩列以上寫同一支技能,只留一列'
done < "$TMPD/dup.txt"
while IFS="$TAB" read -r d n; do
[ -n "${d:-}" ] || continue
report "$d/$n" '這支技能存在,清單缺這一列。沒有判定結果,助理永遠不知道它存在'
done < "$TMPD/missing.txt"
while IFS="$TAB" read -r d n; do
[ -n "${d:-}" ] || continue
report "$d/$n" '清單多這一列,本機沒有這支技能。請刪掉,否則助理會去觸發叫不出來的技能'
done < "$TMPD/extra.txt"
# --- 2~4. 逐列查欄位。多出來的列上面已經報過,這裡跳過,同一個缺陷不印兩遍。 ---
awk -F"$TAB" \
-v SPEC="$SPEC" -v SKILLS="$TMPD/skills.txt" -v VERS="$TMPD/versions.txt" \
-v FAILS="$TMPD/fail-row.txt" -v HINTS="$TMPD/hint.txt" '
function bad(k, m) { printf "%s:%s:%s\n", SPEC, k, m > FAILS }
function hint(k, m) { printf "%s:%s:%s\n", SPEC, k, m > HINTS }
function vlt(a, b, x, y, na, nb, n, i, ai, bi) {
na = split(a, x, "."); nb = split(b, y, ".")
n = (na > nb) ? na : nb
for (i = 1; i <= n; i++) {
ai = (i <= na) ? x[i] + 0 : 0
bi = (i <= nb) ? y[i] + 0 : 0
if (ai < bi) return 1
if (ai > bi) return 0
}
return 0
}
function way_ok(w, t, m, i) {
m = split(w, t, ",")
for (i = 1; i <= m; i++) if (!(t[i] in WAY)) return 0
return (m > 0)
}
function trig_ok(t, e) {
if (t ~ /^at:..*$/) return 1
if (t ~ /^after:..*$/) { e = substr(t, 7); sub(/:.*$/, "", e); return (e in EV) }
return 0
}
BEGIN {
split("full slice cond none", a, " "); for (i in a) VERDICT[a[i]] = 1
split("invoke patrol remind", b, " "); for (i in b) WAY[b[i]] = 1
split("seed judged", c, " "); for (i in c) ORIGIN[c[i]] = 1
split("worklog-written wp-merged stage-entered analyze-completed hook-error session-start session-end", \
d, " "); for (i in d) EV[d[i]] = 1
}
FILENAME == SKILLS { sk[$1 "\t" $2] = 1; next }
FILENAME == VERS { cur[$1] = $2; next }
{
key = $1 "/" $2
if (NF != 11) { bad(key, "這一列有 " NF " 欄,合約規定 11 欄"); next }
if (!(($1 "\t" $2) in sk)) next
verdict = $3; way = $4; slice = $5; human = $6
trig = $7; rec = $8; nxt = $9; ver = $10; org = $11
for (i = 1; i <= 11; i++)
if ($i == "") bad(key, "第 " i " 欄是空的。TSV 沒有空格這種值,不填就寫一個減號")
if (!(verdict in VERDICT)) {
bad(key, "verdict 只認 full、slice、cond、none,實際是「" verdict "」")
next
}
# 不交的那一列只留理由:交出方式、切片、時間點、週期全部要空成減號。
# 留著值等於清單自己在說「不交但這樣交」,下一輪讀的人不知道該信哪一邊。
if (verdict == "none") {
if (way != "-") bad(key, "不交的列,way 要寫 -,實際是「" way "」")
if (slice != "-") bad(key, "不交的列,slice 要寫 -,實際是「" slice "」")
if (trig != "-") bad(key, "不交的列,trigger 要寫 -,實際是「" trig "」")
if (rec != "-") bad(key, "不交的列,recur 要寫 -,實際是「" rec "」")
if (human == "-") bad(key, "不交的列要寫不交的理由,否則下次分不出「判過決定不交」與「還沒判」")
} else {
if (way == "-") bad(key, "交出去的列一定要有交出方式,way 不可以是 -")
else if (!way_ok(way)) bad(key, "way 只認 invoke、patrol、remind,多個用半形逗號隔開,實際是「" way "」")
if (!trig_ok(trig)) bad(key, "trigger 要寫 at:{時間} 或 after:{固定事件名},實際是「" trig "」")
if (rec != "once" && rec !~ /^every:..*$/ && rec !~ /^cron:..*$/)
bad(key, "recur 只認 once、every:{間隔}、cron:{式子},實際是「" rec "」")
}
if (verdict == "full") {
if (slice != "-") bad(key, "全交是整支都交,slice 要寫 -,實際是「" slice "」")
if (human != "-") bad(key, "全交沒有留在人手上的那一半,human 要寫 -,實際是「" human "」")
}
# 切片交與條件式交少了「留在人手上的那一半」,助理下一輪會把整支技能當成可交的一路跑完。
if (verdict == "slice" || verdict == "cond") {
if (slice == "-") bad(key, "交出的那一段沒寫,slice 不可以是 -")
if (human == "-") bad(key, "留在人手上的那一半沒寫,助理下一輪會把整支技能當成可交的一路跑完")
}
if (nxt !~ /^jsc-[a-z0-9-]+:[a-z0-9-]+$/)
bad(key, "next 要寫成 jsc-{domain}:{name},實際是「" nxt "」")
else {
n2 = substr(nxt, 5); sub(/:/, "\t", n2)
if (!(n2 in sk)) bad(key, "next 指到不存在的技能「" nxt "」,助理會一路重試叫不出來的東西")
}
if (!(org in ORIGIN)) bad(key, "origin 只認 seed 與 judged,實際是「" org "」")
else if (org == "seed") hint(key, "origin=seed:依既有盤點種入,還沒走過決策樹,待複核")
if (ver !~ /^[0-9]+(\.[0-9]+)*$/) bad(key, "version 要是 plugin 版本號,實際是「" ver "」")
else if (($1 in cur) && vlt(ver, cur[$1]))
hint(key, "判定時記的版本號 " ver " 落後 jsc-" $1 " 現行的 " cur[$1] ",要複判")
}
' "$TMPD/skills.txt" "$TMPD/versions.txt" "$TMPD/rows.txt"
cat "$TMPD/fail-set.txt" "$TMPD/fail-row.txt" > "$TMPD/fails.txt"
if [ -s "$TMPD/hint.txt" ]; then
LC_ALL=C sort "$TMPD/hint.txt"
fi
rows=$(wc -l < "$TMPD/rows.txt" | tr -d ' ')
skills=$(wc -l < "$TMPD/skills.txt" | tr -d ' ')
hints=$(wc -l < "$TMPD/hint.txt" | tr -d ' ')
if [ -s "$TMPD/fails.txt" ]; then
LC_ALL=C sort "$TMPD/fails.txt" >&2
echo "委派判定清單不符:$SPEC 有 $rows 列,本機有 $skills 支技能,逐項見上方(另有 $hints 則提示,見 stdout)" >&2
exit 1
fi
echo "委派判定清單檢查通過:$SPEC 的 $rows 列對上 $skills 支技能,必填欄位齊全(另有 $hints 則提示,見 stdout)" >&2
exit 0