#!/usr/bin/env sh # check-delegate.sh — 檢查委派判定清單 tools/delegate-spec.tsv 有沒有跟本機實際技能對齊。 # # 用法: check-delegate.sh [] # 不給參數就交給 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 []' >&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