feat(comment-scope): 新增 sweep 模式,掃整個 git 工作區
What:`hooks/comment-scope.sh` 由兩種模式變三種,新增 `sweep [dir]`:掃整個 git 工作區這次改過的所有檔案,命中就把報告與最多三行證據送到 stderr 並 exit 2,找不到 git 就安靜 exit 0。`prompt` 與無參數單檔掃描兩個模式的判定邏輯一行不動。 Why:只有 claude 有 PostToolUse,拿得到「剛剛寫了哪個檔」。codex、copilot、antigravity、kiro 四個 CLI 都沒有 post-tool 事件,註解範圍檢查在那邊只剩規則提示,違規註解寫進去了不會有人叫。改掃整個工作區的 git diff,時機晚一點,涵蓋範圍一樣。 How:單檔判定抽成 `scan_file()`,兩種掃描模式共用同一份禁止樣式與白名單,不會各自漂移。`sweep` 以 `git rev-parse --show-toplevel` 找庫根,所以在子目錄跑也掃得到整個庫;逐檔報告累積在暫存檔再一次輸出,因為迴圈跑在管線的子行程裡,變數帶不回本 shell。 Who:`jsc-hooks` 的 hook 實作層,供 `tools/wire-cli.sh` 接線給 codex、copilot、antigravity、kiro 四個 CLI 使用。
This commit is contained in:
+87
-41
@@ -4,8 +4,13 @@
|
||||
# 專案代號、客戶名稱這類無法用樣式判定的,交給 jsc-review:code-review 第 2 組人工審查。
|
||||
#
|
||||
# 用法:
|
||||
# comment-scope.sh prompt # UserPromptSubmit:注入規則摘要
|
||||
# comment-scope.sh # PostToolUse:掃描剛寫入的檔案,命中就發警告
|
||||
# comment-scope.sh prompt 注入規則摘要(UserPromptSubmit 或規則檔取文字用)
|
||||
# comment-scope.sh 掃描剛寫入的單一檔案(PostToolUse)
|
||||
# comment-scope.sh sweep [dir] 掃描整個工作區這次改過的所有檔案(沒有 post-tool hook 的 CLI 用)
|
||||
#
|
||||
# 為什麼要有 sweep:只有 claude 接得到 PostToolUse,逐檔精準掃得到。codex 只有每輪結束的
|
||||
# notify、kiro 只有 userPromptSubmit、copilot 與 antigravity 只有包裝別名,這四個都拿不到
|
||||
# 「剛剛寫了哪個檔」,只能改成掃整個工作區的 git diff。時機晚一點,涵蓋範圍一樣。
|
||||
#
|
||||
# 輸入相容:
|
||||
# Claude: PostToolUse 的 stdin JSON,取 tool_input.file_path。
|
||||
@@ -13,7 +18,7 @@
|
||||
# 兩者都取不到就安靜降級(exit 0)。
|
||||
#
|
||||
# 掃描範圍:檔案在 git 工作區內就只掃 `git diff HEAD` 的新增行,不翻舊帳;
|
||||
# 不在 git 內或檔案尚未追蹤才整檔掃描。
|
||||
# 不在 git 內或檔案尚未追蹤才整檔掃描。sweep 一律只看 git diff。
|
||||
#
|
||||
# 結束碼:0=沒命中或資料不足;2=命中,訊息走 stderr 交回模型自行修正(不擋寫入,檔案已經寫好了)。
|
||||
# 逃生門:JSC_COMMENT_SCOPE=off。
|
||||
@@ -29,35 +34,41 @@ if [ "${1:-}" = "prompt" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
read_stdin 2>/dev/null || STDIN_JSON=""
|
||||
file=$(json_str file_path 2>/dev/null || true)
|
||||
[ -n "$file" ] || file="${JSC_CHANGED_FILE:-}"
|
||||
[ -n "$file" ] && [ -f "$file" ] || exit 0
|
||||
hit() { # $1=樣式 $2=說明;命中就把說明與最多三行證據印到 stdout
|
||||
m=$(printf '%s\n' "$cleaned" | grep -nE "$1" | head -n 3)
|
||||
[ -n "$m" ] || return 0
|
||||
printf ' %s\n' "$2"
|
||||
printf '%s\n' "$m" | sed 's/^/ /'
|
||||
}
|
||||
|
||||
# 非程式碼檔不受本規則限制:markdown、純文字、資料檔沒有「程式碼註解」。
|
||||
case "$file" in
|
||||
*.md|*.markdown|*.txt|*.rst|*.json|*.csv|*.tsv|*.svg|*.lock|*.log|*COMMIT_EDITMSG) exit 0 ;;
|
||||
esac
|
||||
# 二進位檔跳過。只認 NUL 位元組——拿「非可列印字元」當判準會把所有含中文的檔案誤判成二進位。
|
||||
raw=$(head -c 1024 "$file" 2>/dev/null | wc -c)
|
||||
txt=$(head -c 1024 "$file" 2>/dev/null | LC_ALL=C tr -d '\000' | wc -c)
|
||||
[ "$raw" = "$txt" ] || exit 0
|
||||
scan_file() { # $1=檔案路徑;命中就把報告印到 stdout 並回傳 1,沒命中回傳 0
|
||||
f=$1
|
||||
[ -f "$f" ] || return 0
|
||||
|
||||
dir=$(dirname -- "$file")
|
||||
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 &&
|
||||
git -C "$dir" ls-files --error-unmatch -- "$file" >/dev/null 2>&1; then
|
||||
lines=$(git -C "$dir" diff HEAD -- "$file" 2>/dev/null | sed -n 's/^+[^+]/&/p' | cut -c2-)
|
||||
[ -n "$lines" ] || exit 0
|
||||
else
|
||||
lines=$(cat "$file" 2>/dev/null)
|
||||
fi
|
||||
# 非程式碼檔不受本規則限制:markdown、純文字、資料檔沒有「程式碼註解」。
|
||||
case "$f" in
|
||||
*.md|*.markdown|*.txt|*.rst|*.json|*.csv|*.tsv|*.svg|*.lock|*.log|*COMMIT_EDITMSG) return 0 ;;
|
||||
esac
|
||||
# 二進位檔跳過。只認 NUL 位元組——拿「非可列印字元」當判準會把所有含中文的檔案誤判成二進位。
|
||||
raw=$(head -c 1024 "$f" 2>/dev/null | wc -c)
|
||||
txt=$(head -c 1024 "$f" 2>/dev/null | LC_ALL=C tr -d '\000' | wc -c)
|
||||
[ "$raw" = "$txt" ] || return 0
|
||||
|
||||
# 只留註解行:行首註解符號,或行中出現 // 與 # 的行尾註解。
|
||||
comments=$(printf '%s\n' "$lines" | grep -E '^[[:space:]]*(//|#|--|\*|/\*|<!--|;|%)|[[:space:]](//|#)[[:space:]]' || true)
|
||||
[ -n "$comments" ] || exit 0
|
||||
d=$(dirname -- "$f")
|
||||
if git -C "$d" rev-parse --is-inside-work-tree >/dev/null 2>&1 &&
|
||||
git -C "$d" ls-files --error-unmatch -- "$f" >/dev/null 2>&1; then
|
||||
lines=$(git -C "$d" diff HEAD -- "$f" 2>/dev/null | sed -n 's/^+[^+]/&/p' | cut -c2-)
|
||||
[ -n "$lines" ] || return 0
|
||||
else
|
||||
lines=$(cat "$f" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# 白名單先剪掉,再比對禁止樣式。剪掉而不是整行放行——同一行可能一半合規、一半違規。
|
||||
cleaned=$(printf '%s\n' "$comments" | sed -E \
|
||||
# 只留註解行:行首註解符號,或行中出現 // 與 # 的行尾註解。
|
||||
comments=$(printf '%s\n' "$lines" | grep -E '^[[:space:]]*(//|#|--|\*|/\*|<!--|;|%)|[[:space:]](//|#)[[:space:]]' || true)
|
||||
[ -n "$comments" ] || return 0
|
||||
|
||||
# 白名單先剪掉,再比對禁止樣式。剪掉而不是整行放行——同一行可能一半合規、一半違規。
|
||||
cleaned=$(printf '%s\n' "$comments" | sed -E \
|
||||
-e 's#SPDX-License-Identifier:[^[:space:]]*##g' \
|
||||
-e 's#CVE-[0-9]{4}-[0-9]+##g' \
|
||||
-e 's#(RFC|ISO|IEEE|ANSI|ECMA|UTF|SHA|MD|AES|RSA|HMAC|PBKDF|TLS|SSL|HTTP|BIG|EUC|JIS|GB|RS|IPV|X)-?[0-9]+(-[0-9]+)?##g' \
|
||||
@@ -66,14 +77,7 @@ cleaned=$(printf '%s\n' "$comments" | sed -E \
|
||||
-e 's#https?://[^[:space:]]*[{<][^[:space:]]*##g' \
|
||||
-e 's#[0-9]{4}[-/][0-9]{1,2}[-/][0-9]{1,2}##g')
|
||||
|
||||
hit() { # $1=樣式 $2=說明;命中就把說明與最多三行證據印到 stdout
|
||||
m=$(printf '%s\n' "$cleaned" | grep -nE "$1" | head -n 3)
|
||||
[ -n "$m" ] || return 0
|
||||
printf ' %s\n' "$2"
|
||||
printf '%s\n' "$m" | sed 's/^/ /'
|
||||
}
|
||||
|
||||
out=$(
|
||||
out=$(
|
||||
hit '(^|[^[:alnum:]_/])#[0-9]+' '議題編號(#123)'
|
||||
hit '(^|[^[:alnum:]_])![0-9]+' 'PR、MR 編號(!45)'
|
||||
hit '[A-Z]{2,6}-[0-9]{1,6}' '工作包、故事、驗收、測試案例、變更單、議題編號(前綴加流水號)'
|
||||
@@ -89,14 +93,56 @@ out=$(
|
||||
hit '([Gg]enerated (with|by)|Co-Authored-By|本檔(案)?由|AI (產生|生成|撰寫))' '產生來源署名'
|
||||
hit '預估[[:space:]]*[0-9]+[[:space:]]*(小時|分鐘|人日|人天|天)|[0-9]+[[:space:]]*(人日|人天|工時)' '工時估算'
|
||||
hit '(規格書|需求書|準則|規範|清單|guidelines)[^。]{0,8}第[[:space:]]*[0-9]+|(規格書|需求書|SRS)[^。]{0,6}[0-9]+(\.[0-9]+)+' '規格文件章節、稽核檢查項編號'
|
||||
)
|
||||
)
|
||||
|
||||
[ -n "$out" ] || exit 0
|
||||
|
||||
{
|
||||
printf '[jsc] 程式碼註解夾帶了文件相關資訊,請就地修正:%s\n' "$file"
|
||||
[ -n "$out" ] || return 0
|
||||
printf '%s\n' "$f"
|
||||
printf '%s\n' "$out"
|
||||
return 1
|
||||
}
|
||||
|
||||
advice() {
|
||||
printf ' 修法:把編號指向的內容搬進註解,然後刪掉編號。搬不動就代表那件事不該用註解表達。\n'
|
||||
printf ' 規則正文與白名單見 jsc-review 的 references/comment-scope.md。誤判時用 JSC_COMMENT_SCOPE=off 關閉。\n'
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "sweep" ]; then
|
||||
target=${2:-.}
|
||||
[ -d "$target" ] || exit 0
|
||||
root=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null) || exit 0
|
||||
[ -n "$root" ] || exit 0
|
||||
changed=$(git -C "$root" diff --name-only HEAD 2>/dev/null)
|
||||
[ -n "$changed" ] || exit 0
|
||||
|
||||
# 報告累積在暫存檔:迴圈跑在管線的子行程裡,變數帶不回來。
|
||||
tmp=${TMPDIR:-/tmp}/jsc-comment-scope.$$
|
||||
: > "$tmp" 2>/dev/null || exit 0
|
||||
printf '%s\n' "$changed" | while IFS= read -r rel; do
|
||||
[ -n "$rel" ] || continue
|
||||
scan_file "$root/$rel" >> "$tmp" 2>/dev/null
|
||||
done
|
||||
if [ -s "$tmp" ]; then
|
||||
{
|
||||
printf '[jsc] 工作區有註解夾帶文件相關資訊,請就地修正:\n'
|
||||
sed 's/^/ /' "$tmp"
|
||||
advice
|
||||
} >&2
|
||||
rm -f "$tmp"
|
||||
exit 2
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
read_stdin 2>/dev/null || STDIN_JSON=""
|
||||
file=$(json_str file_path 2>/dev/null || true)
|
||||
[ -n "$file" ] || file="${JSC_CHANGED_FILE:-}"
|
||||
[ -n "$file" ] || exit 0
|
||||
|
||||
report=$(scan_file "$file") && exit 0
|
||||
{
|
||||
printf '[jsc] 程式碼註解夾帶了文件相關資訊,請就地修正:\n'
|
||||
printf '%s\n' "$report"
|
||||
advice
|
||||
} >&2
|
||||
exit 2
|
||||
|
||||
Reference in New Issue
Block a user