Files
meta/tools/check-behaviors.sh
T
jiantw83 2e237b7674 feat(behaviors): 新增技能行為清單與檢查腳本
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:屬於「技能行為清單」這件需求,提供技能驗證的參考基準。
2026-08-31 13:37:04 +08:00

172 lines
7.1 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-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