feat(tools): 新增發現合併與變更註解兩支腳本

- 合併、去重、排序原本寫在兩支技能的步驟文字裡,各寫一份就會各自演化。改由腳本執行,並定下六欄 TSV 的回報格式。格式不對就擋下並指出是第幾列,不猜、不放行、不自行補欄。
- 另一支腳本列出本次改到的註解行,清理範圍不再靠肉眼判讀。
- 註解樣式與非程式碼副檔名清單沿用註解範圍腳本的同一份,不另立第二套判準。markdown 的標題行開頭就是井字號,判準不一致就會把整份文件當成註解。
This commit is contained in:
2026-08-31 11:07:05 +08:00
parent 168113e429
commit a0bd488ac8
2 changed files with 217 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env sh
# changed-comments.sh — 列出本次變更裡新增或修改的註解行,供 comment-cleanup 決定清理範圍。
#
# 用法:
# changed-comments.sh 比對工作區與 HEAD(git diff HEAD),即尚未提交的變更
# changed-comments.sh <base> 比對基準版本(git diff {base}...HEAD),實作完成時用
# 一律在要檢查的存取庫目錄下執行;本腳本不吃路徑參數,也不切換目錄。
#
# 輸出(TSV,一列一行註解,三欄,欄位之間一個 tab,只走 stdout):
# 1 檔案 路徑,相對存取庫根目錄
# 2 行號 該註解行在新版檔案裡的行號,正整數
# 3 內容 該行原文;行內的 tab 換成一個空白,欄位才不會錯位
#
# 結束碼:
# 0 有結果,至少印出一列
# 1 沒有結果,本次變更沒有新增或修改的註解行(呼叫端回報「無發現」)
# 2 參數或環境錯誤:參數超過一個、目前目錄不在 git 工作區、基準版本無效、缺 git 或 awk
#
# 判準的來源:註解行樣式與「不受本規則限制的非程式碼副檔名」兩份判準,都沿用 jsc-hooks 的
# hooks/comment-scope.sh(scan_file 裡的那兩份),一字不差地照抄。那支 hook 是規則實作的
# 正本,這裡只是把同一份判準搬到 git diff 上;哪天正本改了樣式,這裡要跟著改,不得各自演化。
# 跳過非程式碼檔不是可有可無的:markdown 的標題行開頭就是 #,不跳過就會把整份文件當成註解。
set -u
usage() {
echo '用法:changed-comments.sh [base](不給基準時比對工作區與 HEAD)' >&2
exit 2
}
[ "$#" -le 1 ] || usage
for cmd in git awk; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "錯誤:環境缺 $cmd,無法列出變更的註解行。" >&2
exit 2
}
done
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
echo '錯誤:目前目錄不在 git 工作區內,取不到變更範圍。' >&2
exit 2
}
# git diff 的失敗一律原樣往上帶:判不出範圍卻回「無發現」,等於把環境問題講成沒有東西要清。
if [ "$#" -eq 1 ]; then
base=$1
case "$base" in
-*) usage ;;
esac
git rev-parse --verify --quiet "$base^{commit}" >/dev/null 2>&1 || {
echo "錯誤:基準版本「$base」不是有效的 commit。" >&2
exit 2
}
DIFF=$(git diff --no-color -U0 "$base...HEAD" 2>&1) || {
printf '%s\n' "$DIFF" >&2
exit 2
}
else
DIFF=$(git diff --no-color -U0 HEAD 2>&1) || {
printf '%s\n' "$DIFF" >&2
exit 2
}
fi
# 樣式走環境變數交給 awk:走 -v 的話 awk 會先解釋字串裡的跳脫序列,樣式中的 \* 會被吃成
# 量詞 *,整條 ERE 的意思就變了。ENVIRON 不做這層處理,樣式進到 awk 時與正本一字不差。
COMMENT_RE='^[[:space:]]*(//|#|--|\*|/\*|<!--|;|%)|[[:space:]](//|#)[[:space:]]'
OUT=$(printf '%s\n' "$DIFF" | COMMENT_RE="$COMMENT_RE" awk '
BEGIN { OFS = "\t"; re = ENVIRON["COMMENT_RE"]; skip = 1 }
# 非程式碼檔沒有「程式碼註解」,整檔跳過(副檔名清單同 comment-scope.sh)。
function skipped(p) {
return (p ~ /\.(md|markdown|txt|rst|json|csv|tsv|svg|lock|log)$/ || p ~ /COMMIT_EDITMSG$/)
}
/^\+\+\+ / {
f = substr($0, 5) # 去掉開頭的「+++ 」
sub(/^b\//, "", f)
skip = (f == "" || f == "/dev/null" || skipped(f))
next
}
# 區塊標頭 @@ -a,b +c,d @@:第三欄的 +c 就是這一段在新版檔案裡的起始行號。
/^@@ / {
t = $3
sub(/^\+/, "", t)
sub(/,.*$/, "", t)
ln = t + 0
next
}
/^\+/ {
if (skip || ln < 1) next
line = substr($0, 2)
if (line ~ re) {
out = line
gsub(/\t/, " ", out)
print f, ln, out
}
ln++ # 只有新增行會佔掉新版檔案的行號,刪除行不會
}
')
[ -n "$OUT" ] || exit 1
printf '%s\n' "$OUT"
exit 0
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env sh
# merge-findings.sh — 合併多組審查發現,依位置去重,再依嚴重度排序。
# 用法:merge-findings.sh [發現檔…]
# 給檔名就讀那幾個檔,不給檔名就讀 stdin。code-review 六組與 api-doc 兩個面向
# 共用這一支,兩支技能都不再自己描述合併與排序規則。
#
# 輸入格式(TSV,一列一筆發現,六欄,欄位之間一個 tab):
# 1 file 檔案路徑,相對專案根目錄,不得留空
# 2 line 行號,正整數
# 3 severity 嚴重度,只收 高、中、低
# 4 group 組別或面向代號,例如 2 或 1 狀態碼
# 5 item 發現名稱,壞味道名稱或稽核項目名稱
# 6 detail 一句證據加上建議修法
# 空白列、開頭是 # 的列、第一欄為「無發現」的列一律略過。
#
# 輸出(TSV,同樣六欄,一個位置一列,印到 stdout):
# file 與 line 相同的發現合併成一列。
# severity 取該位置最高的一級。group、item、detail 依輸入順序以「|」相連。
# 排序:嚴重度 高 → 中 → 低;同一級再依 file 字典序、line 數值遞增。
#
# 結束碼:
# 0 合併成功,至少印出一列
# 1 沒有任何發現(輸入沒有可用的資料列),呼叫端回報「無發現」
# 2 參數或環境錯誤(找不到輸入檔,或缺 awk、sort、cut)
# 3 輸入格式錯誤(欄數不是 6、行號不是正整數,或嚴重度不在 高、中、低)
#
# 護欄:
# 格式錯誤一律 exit 3 並在 stderr 指出是第幾個檔的第幾列,不猜、不放行、不自行補欄。
# 錯誤訊息一律印繁中到 stderr,正常輸出只走 stdout。
set -u
for cmd in awk sort cut; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "錯誤:環境缺 $cmd,無法合併發現。" >&2
exit 2
}
done
for f in "$@"; do
case "$f" in
-*) echo "用法:merge-findings.sh [發現檔…](不給檔名時讀 stdin)" >&2; exit 2 ;;
esac
[ -f "$f" ] || { echo "錯誤:找不到輸入檔 $f。請確認路徑後重試。" >&2; exit 2; }
done
TAB=$(printf '\t')
TMP="${TMPDIR:-/tmp}/jsc-merge-findings.$$"
trap 'rm -f "$TMP" "$TMP.sorted"' EXIT INT TERM
# 第一欄先放排序用的嚴重度序位,排完再切掉。
awk -F"$TAB" '
BEGIN { OFS = "\t"; n = 0; bad = 0 }
{
sub(/\r$/, "", $0)
if ($0 ~ /^[ \t]*$/) next
if ($0 ~ /^#/) next
if ($1 == "無發現") next
where = (FILENAME == "" || FILENAME == "-") ? "stdin" : FILENAME
if (NF != 6) {
printf("錯誤:%s 第 %d 列有 %d 欄,應為 6 欄。\n", where, FNR, NF) > "/dev/stderr"
bad = 1; next
}
if ($1 == "") {
printf("錯誤:%s 第 %d 列的檔案路徑是空的。\n", where, FNR) > "/dev/stderr"
bad = 1; next
}
if ($2 !~ /^[0-9]+$/ || $2 + 0 < 1) {
printf("錯誤:%s 第 %d 列的行號「%s」不是正整數。\n", where, FNR, $2) > "/dev/stderr"
bad = 1; next
}
if ($3 != "高" && $3 != "中" && $3 != "低") {
printf("錯誤:%s 第 %d 列的嚴重度「%s」不在 高、中、低。\n", where, FNR, $3) > "/dev/stderr"
bad = 1; next
}
rank = ($3 == "高") ? 1 : (($3 == "中") ? 2 : 3)
key = $1 SUBSEP $2
if (!(key in seen)) {
seen[key] = 1
order[++n] = key
kfile[key] = $1; kline[key] = $2; krank[key] = rank
kgroup[key] = $4; kitem[key] = $5; kdetail[key] = $6
} else {
if (rank < krank[key]) krank[key] = rank
kgroup[key] = kgroup[key] "|" $4
kitem[key] = kitem[key] "|" $5
kdetail[key] = kdetail[key] "|" $6
}
}
END {
if (bad) exit 3
if (n == 0) exit 1
for (i = 1; i <= n; i++) {
k = order[i]
sev = (krank[k] == 1) ? "高" : ((krank[k] == 2) ? "中" : "低")
print krank[k], kfile[k], kline[k], sev, kgroup[k], kitem[k], kdetail[k]
}
}
' "$@" > "$TMP"
rc=$?
[ "$rc" -eq 0 ] || exit "$rc"
LC_ALL=C sort -t"$TAB" -k1,1n -k2,2 -k3,3n "$TMP" > "$TMP.sorted" || {
echo "錯誤:排序失敗。" >&2
exit 2
}
cut -f2- "$TMP.sorted" || {
echo "錯誤:輸出失敗。" >&2
exit 2
}
exit 0