What:新增 references/behaviors.md,一支技能一節,共七支技能。每節五列,記下觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象。新增 tools/check-behaviors.sh,比對 skills/ 與這份清單。skill-new、skill-update、skill-delete、skillset-update 加上同步更新清單的步驟。skill-check 把這支腳本併進第一組稽核。 Why:技能驗證原本沒有基準,稽核只能靠眼睛比對 SKILL.md。十個 domain 每輪都要重做一遍,還會漏掉。行為清單當基準,技能改了、清單沒跟著改,就是漂移。漂移交給程式判定才穩。 How:腳本檢查節數、節名、節序、每節一張表、五個欄位齊全、內容欄非空。退出碼 0 代表相符,1 代表不符,2 代表用法錯誤,3 代表找不到清單或找不到 skills 目錄。domain 名以 plugin.json 的 name 為準,checkout 目錄名只是退路。四支異動技能在同一個 PR 內改清單,並照退出碼分流。 Who:屬於「技能行為清單」這件需求,提供技能驗證的參考基準。
172 lines
7.1 KiB
Bash
Executable File
172 lines
7.1 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
# check-behaviors.sh — 檢查一個 domain 的技能行為清單 references/behaviors.md 有沒有跟 skills/ 對齊。
|
||
#
|
||
# 用法: check-behaviors.sh <domain-path>
|
||
#
|
||
# 檢查六項(格式合約見 jsc-meta references/guidelines.md 的「技能行為清單」一節):
|
||
# 1. 標題 — 第一行是「# jsc-{domain} 技能行為清單」,檔案不得有 UTF-8 BOM。
|
||
# 2. 節對技能 — 每支 skills/*/SKILL.md 一個「## {技能名}」節,名稱與目錄名逐字相同,不多不少。
|
||
# 3. 節順序 — 節的排列照技能目錄名的字典序(LC_ALL=C)。
|
||
# 4. 表格 — 每節恰好一張表,表頭是「| 項目 | 內容 |」。
|
||
# 5. 五個欄位 — 依序為 觸發時機、關鍵步驟、外部呼叫、完成條件、可驗證跡象,不多不少。
|
||
# 6. 內容 — 每一列的「內容」欄不得空白。
|
||
#
|
||
# 為什麼要這支: 技能改了行為、清單沒跟著改,兩邊就漂移。漂移靠眼睛比對,10 個 domain 每次稽核
|
||
# 都要重做一遍,還會漏。這六項的輸入輸出固定,交給程式判定才穩。
|
||
#
|
||
# 輸出: 一行一個不合格項目,格式 {behaviors.md 路徑}:{技能名或 -}:{說明}(stderr);
|
||
# 通過時在 stderr 印一行摘要。stdout 不印東西。
|
||
# 結束碼: 0=行為清單與 skills/ 相符,五個欄位齊全且內容欄非空
|
||
# 1=不符:缺節、多節、順序不對、表格不對、缺欄位或欄位空白,清單在 stderr
|
||
# 2=用法錯誤(本腳本只吃一個參數)
|
||
# 3=找不到 {domain-path}/references/behaviors.md,或找不到 {domain-path}/skills/,
|
||
# 或 skills/ 底下一支 SKILL.md 都沒有——**什麼都沒查**,不等於通過
|
||
set -u
|
||
|
||
usage() {
|
||
echo 'usage: check-behaviors.sh <domain-path>' >&2
|
||
exit 2
|
||
}
|
||
|
||
[ "$#" -eq 1 ] || usage
|
||
DOMAIN=${1%/}
|
||
[ -n "$DOMAIN" ] || usage
|
||
|
||
SKILLS="$DOMAIN/skills"
|
||
DOC="$DOMAIN/references/behaviors.md"
|
||
[ -d "$DOMAIN" ] || { echo "找不到 domain 路徑:$DOMAIN" >&2; exit 3; }
|
||
[ -d "$SKILLS" ] || { echo "找不到 skills/:$SKILLS" >&2; exit 3; }
|
||
[ -f "$DOC" ] || { echo "找不到行為清單:$DOC" >&2; exit 3; }
|
||
|
||
TMPD=$(mktemp -d) || { echo "無法建立暫存目錄" >&2; exit 3; }
|
||
trap 'rm -rf "$TMPD"' EXIT
|
||
|
||
TAB=$(printf '\t')
|
||
BOM=$(printf '\357\273\277')
|
||
FIELDS='觸發時機 關鍵步驟 外部呼叫 完成條件 可驗證跡象'
|
||
|
||
fail=0
|
||
report() { # $1=技能名或 -,$2=說明
|
||
printf '%s:%s:%s\n' "$DOC" "$1" "$2" >&2
|
||
fail=1
|
||
}
|
||
|
||
# 技能清單: skills/ 底下帶 SKILL.md 的目錄名,字典序。
|
||
for d in "$SKILLS"/*/; do
|
||
[ -f "${d}SKILL.md" ] || continue
|
||
n=${d%/}
|
||
echo "${n##*/}"
|
||
done | LC_ALL=C sort > "$TMPD/skills.txt"
|
||
[ -s "$TMPD/skills.txt" ] || { echo "skills/ 底下沒有任何 SKILL.md:$SKILLS" >&2; exit 3; }
|
||
|
||
# 解析 behaviors.md,攤平成三種記錄:
|
||
# SEC<TAB>{節名} 一個「## 」標題
|
||
# HDR<TAB>{節名} 一列表頭「| 項目 | 內容 |」
|
||
# ROW<TAB>{節名}<TAB>{項目}<TAB>{內容} 一列資料(分隔列不算)
|
||
awk '
|
||
function trim(s) { gsub(/^[ \t]+/, "", s); gsub(/[ \t]+$/, "", s); return s }
|
||
BEGIN { FS = "|"; sec = "-" }
|
||
{ sub(/\r$/, "") }
|
||
/^## / {
|
||
sec = trim(substr($0, 4))
|
||
printf "SEC\t%s\n", sec
|
||
next
|
||
}
|
||
/^[ \t]*\|/ {
|
||
item = trim($2)
|
||
body = ""
|
||
for (i = 3; i < NF; i++) body = (body == "" ? $i : body "|" $i)
|
||
body = trim(body)
|
||
if (item ~ /^[-: ]+$/ && body ~ /^[-: ]*$/) next
|
||
if (item == "項目" && body == "內容") { printf "HDR\t%s\n", sec; next }
|
||
printf "ROW\t%s\t%s\t%s\n", sec, item, body
|
||
}
|
||
' "$DOC" > "$TMPD/parsed.txt"
|
||
|
||
# --- 1. 標題 ---
|
||
first=$(head -1 "$DOC" | tr -d '\r')
|
||
case $first in
|
||
"$BOM"*) report - '檔頭帶 UTF-8 BOM,請改存無 BOM'; first=${first#"$BOM"} ;;
|
||
esac
|
||
# domain 名以 plugin.json 的 name 為準,checkout 目錄名只是退路。
|
||
# 目錄名是誰 clone 誰決定的,同一個 repo 換一台機器就可能叫別的名字,
|
||
# 拿它當唯一來源會在別人的工作區誤報一次「標題不符」。
|
||
dom=$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$DOMAIN/plugin.json" 2>/dev/null | head -n1)
|
||
if [ -z "$dom" ]; then
|
||
dom=$(cd "$DOMAIN" 2>/dev/null && pwd) || dom=$DOMAIN
|
||
dom=${dom##*/}
|
||
fi
|
||
dom=${dom#jsc-}
|
||
want_title="# jsc-$dom 技能行為清單"
|
||
[ "$first" = "$want_title" ] || report - "第一行要是「$want_title」,實際是「$first」"
|
||
|
||
# --- 2. 節對技能 ---
|
||
awk -F"$TAB" '$1 == "SEC" { print $2 }' "$TMPD/parsed.txt" > "$TMPD/sections.txt"
|
||
LC_ALL=C sort "$TMPD/sections.txt" > "$TMPD/sections-sorted.txt"
|
||
|
||
LC_ALL=C uniq -d "$TMPD/sections-sorted.txt" | while IFS= read -r s; do
|
||
[ -n "$s" ] && printf '%s:%s:%s\n' "$DOC" "$s" '同一支技能出現多個節,只留一節' >&2
|
||
done
|
||
if [ -n "$(LC_ALL=C uniq -d "$TMPD/sections-sorted.txt")" ]; then fail=1; fi
|
||
|
||
LC_ALL=C uniq "$TMPD/sections-sorted.txt" > "$TMPD/sections-uniq.txt"
|
||
LC_ALL=C comm -23 "$TMPD/skills.txt" "$TMPD/sections-uniq.txt" > "$TMPD/missing.txt"
|
||
LC_ALL=C comm -13 "$TMPD/skills.txt" "$TMPD/sections-uniq.txt" > "$TMPD/extra.txt"
|
||
|
||
while IFS= read -r s; do
|
||
[ -n "$s" ] || continue
|
||
report "$s" "skills/$s/SKILL.md 存在,行為清單缺這一節,請補「## $s」"
|
||
done < "$TMPD/missing.txt"
|
||
|
||
while IFS= read -r s; do
|
||
[ -n "$s" ] || continue
|
||
report "$s" "行為清單多這一節,skills/ 底下沒有這支技能,請移除或改名"
|
||
done < "$TMPD/extra.txt"
|
||
|
||
# --- 3. 節順序 ---
|
||
if ! cmp -s "$TMPD/sections.txt" "$TMPD/sections-sorted.txt"; then
|
||
report - '節的排列不是技能目錄名的字典序,請重排'
|
||
fi
|
||
|
||
# --- 4~6. 逐節查表格與五個欄位 ---
|
||
while IFS= read -r name; do
|
||
[ -n "$name" ] || continue
|
||
grep -q "^$name\$" "$TMPD/missing.txt" && continue
|
||
|
||
hdr=$(awk -F"$TAB" -v s="$name" '$1 == "HDR" && $2 == s' "$TMPD/parsed.txt" | wc -l)
|
||
hdr=$((hdr + 0))
|
||
if [ "$hdr" -eq 0 ]; then
|
||
report "$name" '這一節沒有表頭「| 項目 | 內容 |」,五個欄位無從判讀'
|
||
continue
|
||
fi
|
||
[ "$hdr" -eq 1 ] || report "$name" "這一節有 $hdr 張表,合約規定恰好一張"
|
||
|
||
awk -F"$TAB" -v s="$name" '$1 == "ROW" && $2 == s { print $3 "\t" $4 }' \
|
||
"$TMPD/parsed.txt" > "$TMPD/rows.txt"
|
||
|
||
rows=$(wc -l < "$TMPD/rows.txt")
|
||
rows=$((rows + 0))
|
||
[ "$rows" -eq 5 ] || report "$name" "表格有 $rows 列,合約規定 5 列:$FIELDS"
|
||
|
||
i=0
|
||
while IFS="$TAB" read -r item body; do
|
||
i=$((i + 1))
|
||
[ "$i" -le 5 ] || { report "$name" "第 $i 列「$item」是多的,合約只收 5 列"; continue; }
|
||
want=$(echo "$FIELDS" | cut -d' ' -f"$i")
|
||
[ "$item" = "$want" ] || report "$name" "第 $i 列的項目要是「$want」,實際是「$item」"
|
||
[ -n "$body" ] || report "$name" "「$item」的內容欄空白,請補實際行為"
|
||
done < "$TMPD/rows.txt"
|
||
done < "$TMPD/skills.txt"
|
||
|
||
# --- 節裡以外的表格 ---
|
||
if awk -F"$TAB" '$2 == "-" { found = 1 } END { exit found ? 0 : 1 }' "$TMPD/parsed.txt"; then
|
||
report - '有表格落在任何「## 」節之外,請搬進所屬技能的節裡'
|
||
fi
|
||
|
||
if [ "$fail" -eq 0 ]; then
|
||
echo "行為清單檢查通過:$DOC 對上 $(wc -l < "$TMPD/skills.txt" | tr -d ' ') 支技能,五個欄位齊全" >&2
|
||
else
|
||
echo "行為清單檢查不符:$DOC 與 $SKILLS 對不起來,逐項見上方" >&2
|
||
fi
|
||
exit $fail
|