切片交的那幾支,助理現在只會提醒不會動手,因為沒有東西記得下「那一段唯讀盤點到底要跑什麼」。清單加第十二欄記具體指令,種入那一支有值就拿它當動作、沒值才退回只提醒。 只認三種寫法。一行指令,路徑一律寫成代入點開頭,由種入那一支代進字面絕對根目錄;指令裡不可以出現金錢符號或波浪號,那兩種在無人值守那一輪解不出來也進不了允許清單,會被靜靜擋掉。要標未接線就寫理由,留白的話下一輪分不出是刻意還是漏填。沒有唯讀入口的寫減號。 觸發型與不交的列一律填減號。觸發的意思是呼叫整支技能,這一欄填了指令會讓種入那一支改拿指令當動作,於是整支交出降級成只跑一支腳本,該寫的頁一頁都不會寫,而且看起來完全正常。這條由檢核腳本擋。 逐項回各存放庫核對,不照抄既有盤點的措辭,因而抓到五處對不上實際腳本的地方。 最要緊的一處:既有盤點點名的那支工具腳本要三個參數,其中兩個無人值守那一輪根本拿不到,而且它是唯一會去問遠端的一方。改用同一件事的本機那一半,而那個子命令剛好在閘門腳本「免讀標準輸入」的清單裡。這一點非確認不可:同一支腳本的另一個子命令不在那份清單上,標準輸入是管線又沒人關閉時會一直等,實測會無限卡住。腳本自己的註解就記著這個坑,說工具腳本轉呼叫時曾經整支卡死——那正是先前心跳斷掉的同一種死法。 另一處:既有盤點點名的同步子命令會寫檔,不是唯讀。那一列剛好是觸發型所以填減號、衝突沒落地,但措辭本身是錯的。 七項標成未接線,理由都是要連網要金鑰。連網那一種一過期就讓那一項每輪失敗,或每輪靜靜回報沒事——後者更難查;而純本機讀取本來一輪都不會失敗。先填會連網的,等於用一批每輪報錯的項目把真的發現蓋掉。 還記下一件接線那天要注意的事:查遠端版本那一支永遠回成功,查不到就安靜放行,所以金鑰失效時它會靜靜回報沒有新版。接線要用另一個子命令,那個有「查不出來」這第三種結論。 檢核腳本從四項檢查改成五項。填錯欄位、指到不存在的腳本、用了認不得的代入點都算缺失;標未接線只印提示,那是判過知道還沒接,不是漏填;指到的 domain 本機沒裝也只提示,那是機器少裝一套不是清單填錯,算缺失會讓半套機器每輪亮紅。 五支技能異動流程一併改:寫清單時要問到並寫下這一欄,重判時要一併重問,刪除時要檢查別列有沒有指到一起刪掉的腳本,一次改多支時要注意跨存放庫搬腳本是這一欄最容易過期的地方。
293 lines
15 KiB
Bash
Executable File
293 lines
15 KiB
Bash
Executable File
#!/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. 欄位齊全 — 12 欄,每一欄非空;四種結論各自的必填欄位都填了,不該填的填 -。
|
||
# 3. 欄位可填值 — verdict、way、trigger、recur、origin 只認固定詞彙;next 要指到
|
||
# 一支真的存在的技能。指到不存在的技能,助理會一路重試一支叫不出來的東西。
|
||
# 4. 唯讀指令 — probe 的填法與那一支腳本在不在,判準見下。
|
||
# 5. 版本號與來源 — 只印提示,不影響結束碼,理由見下。
|
||
#
|
||
# 為什麼要這支: 技能組每天在動,手工盤點只會過期。清單缺一列,助理就永遠不知道那支技能
|
||
# 存在;清單多一列,助理會去觸發一支不存在的技能,而且失敗不會自動暫停。這兩件事靠眼睛
|
||
# 比對每輪都會漏,交給程式判定才穩。
|
||
#
|
||
# 為什麼版本號落後只算提示: 版本號是 domain 層級的,同一個 domain 改一支技能,其餘技能的
|
||
# 版本號也會一起落後、也會被標成要複判。當成稽核缺失的話,每次發版整個 domain 都亮紅,
|
||
# 提示很快就會被當成雜訊忽略,真正要複判的那一支反而看不見。
|
||
#
|
||
# 為什麼 origin=seed 只算提示: 種入的那一批是依既有盤點填的,還沒正式走過決策樹,本來就
|
||
# 知道它待複核。它是「已經有結論、但要再確認一次」,不是「漏填」。列成缺失會讓清單從
|
||
# 第一天就是紅的,紅到全部複核完為止,這段期間所有真的缺失都會被蓋掉。
|
||
#
|
||
# probe 什麼算缺失、什麼只算提示: 缺失的判準只有一條——這一列照著跑,無人值守那一輪會出事。
|
||
# 四種算缺失:
|
||
# 一、way 含 invoke 或這一列不交,probe 卻填了指令。種入那一支會改拿指令當待辦簿的動作,
|
||
# 整支交出於是變成只跑一支腳本,那支技能該寫的頁一頁都不會寫,而且看起來完全正常。
|
||
# 二、way 只有 patrol 或 remind,probe 卻是減號。那幾列的動作原本一律退回只提醒,
|
||
# 等於判過要交、實際什麼都沒交出去。接不上就寫 pending 並講明理由,別留減號。
|
||
# 三、指令裡有金錢符號或波浪號,或用了 root、cli、repo 以外的代入點。前者在無人值守
|
||
# 那一輪解不出來也進不了允許清單,後者代不進去會原樣送進指令,兩種都是靜靜失敗。
|
||
# 四、指令指到的腳本在本機不存在。助理每一輪都會叫不到它,而失敗不會自動暫停。
|
||
# 兩種只算提示:
|
||
# 一、pending:{理由}。入口還沒接上是判過的結論,跟 origin=seed 同一類,不是漏填。
|
||
# 二、指令指到的 domain 本機沒有裝。那是這台機器少裝一個 domain,不是清單填錯——
|
||
# 算成缺失的話,只裝半套的機器每一輪都亮紅,真的填錯反而看不見。
|
||
#
|
||
# 輸出: 缺失一行一個,格式 {清單路徑}:{domain}/{name}:{說明}(stderr);
|
||
# 提示同格式(stdout),讓呼叫端分得開缺失與提示;統計摘要走 stderr。
|
||
# 結束碼: 0=清單與實際技能一一對應、必填欄位齊全(可能帶提示,提示不影響這一碼)
|
||
# 1=缺列、多列、重複列、欄位空著、欄位值不合法、next 指到不存在的技能,
|
||
# 或 probe 填錯欄位、指到不存在的腳本、用了認不得的代入點,清單在 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 誰決定的,換一台機器就可能叫別的名字。第三欄記那個目錄,
|
||
# probe 要拿它把 {root}/jsc-{domain} 換成本機的實際位置,才驗得到腳本在不在。
|
||
: > "$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\t%s\n' "${n#jsc-}" "$v" "${p%/plugin.json}" >> "$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~5. 逐列查欄位。多出來的列上面已經報過,這裡跳過,同一個缺陷不印兩遍。 ---
|
||
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
|
||
}
|
||
# probe 裡認不得的代入點,回傳第一個;全部認得回傳空字串。認不得的代入點代不進去,
|
||
# 會原樣送進指令,於是那一輪安安靜靜跑錯東西。
|
||
function ph_bad(s, t, p) {
|
||
t = s
|
||
while (match(t, /\{[^}]*\}/)) {
|
||
p = substr(t, RSTART + 1, RLENGTH - 2)
|
||
if (!(p in PH)) return p
|
||
t = substr(t, RSTART + RLENGTH)
|
||
}
|
||
return ""
|
||
}
|
||
# probe 的第一個非環境變數指派詞,也就是腳本路徑本身。
|
||
function script_of(s, n, a, i) {
|
||
n = split(s, a, " ")
|
||
for (i = 1; i <= n; i++) {
|
||
if (a[i] ~ /^[A-Za-z_][A-Za-z0-9_]*=/) continue
|
||
return a[i]
|
||
}
|
||
return ""
|
||
}
|
||
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
|
||
split("root cli repo", e, " "); for (i in e) PH[e[i]] = 1
|
||
}
|
||
FILENAME == SKILLS { sk[$1 "\t" $2] = 1; next }
|
||
FILENAME == VERS { cur[$1] = $2; ddir[$1] = $3; next }
|
||
{
|
||
key = $1 "/" $2
|
||
if (NF != 12) { bad(key, "這一列有 " NF " 欄,合約規定 12 欄"); 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; probe = $12
|
||
|
||
for (i = 1; i <= 12; 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] ",要複判")
|
||
|
||
# --- probe:那一段唯讀盤點實際要跑的指令。判準見檔頭「什麼算缺失、什麼只算提示」。 ---
|
||
if (verdict == "none" || way ~ /(^|,)invoke(,|$)/) {
|
||
if (probe != "-")
|
||
bad(key, "way 含 invoke 或這一列不交,probe 要寫 -,實際是「" probe "」。填了指令,種入那一支會改拿指令當動作,整支交出就變成只跑一支腳本")
|
||
} else if (probe == "-") {
|
||
bad(key, "way 只有 patrol 或 remind,probe 不可以是 -:沒有指令的那幾筆一律退回只提醒,等於判過要交卻什麼都沒交出去。接不上就寫 pending:{理由}")
|
||
}
|
||
|
||
if (probe ~ /^pending:/) {
|
||
if (probe == "pending:") bad(key, "pending 後面要寫不接的理由,留白的話下一輪分不出是刻意還是漏填")
|
||
else hint(key, "probe=" probe ":唯讀盤點的入口還沒接上,待接線")
|
||
} else if (probe != "-") {
|
||
if (probe ~ /[$~]/)
|
||
bad(key, "probe 裡有金錢符號或波浪號,實際是「" probe "」。那兩種寫法在無人值守那一輪解不出來,也進不了允許清單,會被靜靜擋掉")
|
||
p2 = ph_bad(probe)
|
||
if (p2 != "")
|
||
bad(key, "probe 用了認不得的代入點「{" p2 "}」,只認 {root}、{cli}、{repo}。代不進去的字面值會原樣送進指令")
|
||
sp = script_of(probe)
|
||
if (sp !~ /^\{root\}\/jsc-[a-z0-9-]+\/..*$/)
|
||
bad(key, "probe 的腳本路徑要寫成 {root}/jsc-{domain}/… ,實際是「" sp "」")
|
||
else {
|
||
pd = sp; sub(/^\{root\}\/jsc-/, "", pd); sub(/\/.*$/, "", pd)
|
||
rest = sp; sub(/^\{root\}\/jsc-[a-z0-9-]+\//, "", rest)
|
||
if (!(pd in ddir))
|
||
hint(key, "probe 指到的 jsc-" pd " 本機沒有裝,這一列的腳本這一次驗不到")
|
||
else if (system("test -f '\''" ddir[pd] "/" rest "'\''") != 0)
|
||
bad(key, "probe 指到的腳本不存在:" ddir[pd] "/" rest "。助理每一輪都會叫不到它,而失敗不會自動暫停")
|
||
}
|
||
}
|
||
}
|
||
' "$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
|