What:簡體字表由 102 字擴充到 1006 字;lang-guard.sh 的跳過清單補上 jsc-meta 的 references/ste100.md 與 tools/ste100-lint.sh。 Why:102 字的舊表覆蓋率太低——拿 21 個常見簡體詞實測只抓到 4 個,「简体字」 「举办」「觉得」「战争」「医药」這類全部漏掉,規則等於形同虛設。跳過清單則是 對稱性問題:規則文件與機檢工具本身會列舉簡體字當範例,不跳過就每次先抓到自己。 How:字表逐字檢查「簡化後才出現的字形」,剔除正體也在用的字(后、台、干、只、 里、面、制、志、出、斗、丑、了、卷、咸、云、留、言、短、困、陷、游、封、吞), 再拿 jsc 全部 11 個存取庫共 221 個檔案的既有正體中文語料回歸驗證,誤報為零。 檔頭寫明排除原則與「改完必須拿語料重驗」的要求。跳過清單比照 ste100-lint.sh 的做法。 Who:非程式碼輸出的繁中無亂碼檢查。
179 lines
8.8 KiB
Bash
Executable File
179 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
# lang-guard.sh — 非程式碼輸出一律繁體中文、UTF-8、無亂碼、無簡體字(hook > prompt 的強制層)。
|
||
# 規則正文的唯一來源:jsc-meta 的 references/ste100.md。本腳本只實作可用樣式判定的三項:
|
||
# 簡體字、亂碼、非 UTF-8 編碼;用詞、標點、語氣那些判不出來的交給 ste100-guard.sh 的提示層。
|
||
#
|
||
# 用法:
|
||
# lang-guard.sh prompt 注入規則摘要(UserPromptSubmit 或規則檔取文字用)
|
||
# lang-guard.sh 掃描剛寫入的單一檔案(PostToolUse)
|
||
# lang-guard.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。
|
||
# 其他 CLI: 環境變數 JSC_CHANGED_FILE。
|
||
# 兩者都取不到就安靜降級(exit 0)。
|
||
#
|
||
# 掃描範圍跟 comment-scope.sh 有兩點刻意不同,不要照抄那支的判斷:
|
||
# 1. 三項檢查都掃整個檔案,不是只掃註解行。程式碼的任何位置都不該出現簡體字或亂碼——
|
||
# 字串常值、識別字、資料內容一樣算輸出。comment-scope.sh 只掃註解,是因為它抓的是
|
||
# 註解夾帶文件編號,那件事只發生在註解裡。
|
||
# 2. markdown 與純文字檔要掃。它們正是「非程式碼輸出」的主場:README、wiki 頁、PR 描述、
|
||
# commit 訊息都是這類檔案。comment-scope.sh 刻意跳過 .md,因為那裡沒有程式碼註解。
|
||
#
|
||
# 掃描深度:檔案在 git 工作區內且已追蹤,就只掃 `git diff HEAD` 的新增行,不翻舊帳;
|
||
# 不在 git 內或檔案尚未追蹤才整檔掃描。sweep 一律只看 git diff。
|
||
#
|
||
# 結束碼:0=沒命中或資料不足;2=命中,訊息走 stderr 交回模型自行修正(不擋寫入,檔案已經寫好了)。
|
||
# 逃生門:JSC_LANG_GUARD=off。
|
||
set -u
|
||
|
||
. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/lib.sh" 2>/dev/null || true
|
||
|
||
[ "${JSC_LANG_GUARD:-on}" = "off" ] && exit 0
|
||
|
||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||
WORDLIST="$HERE/simplified.txt"
|
||
|
||
if [ "${1:-}" = "prompt" ]; then
|
||
echo "[jsc] 所有非程式碼輸出一律繁體中文、UTF-8、無亂碼、無簡體字。適用範圍:程式碼註解、commit 訊息、PR 描述與標題、wiki 頁、對使用者的回報、README 與各種文件、錯誤訊息與日誌文字。程式碼本身的關鍵字、識別字、API 欄位名維持原樣,但其中的中文一樣要是繁體。"
|
||
echo "[jsc] 送出前自我檢查:1)簡體字(例:应、为、这、说、发、国)一律換成繁體;2)替代字元「U+FFFD」與雙重編碼亂碼(Ã、â 開頭的怪序列)代表編碼壞掉,重寫那段而不是保留;3)檔案一律存成 UTF-8,不加 BOM。規則正文見 jsc-meta 的 references/ste100.md。"
|
||
exit 0
|
||
fi
|
||
|
||
# 簡體字樣式:由 hooks/simplified.txt 組出,字表是單一真實來源,腳本裡不留第二份。
|
||
# 讀不到字表就回傳 1,呼叫端安靜跳過這一項,不中斷整支腳本——少抓一項,好過整支 hook 死掉。
|
||
simplified_pattern() {
|
||
[ -f "$WORDLIST" ] || return 1
|
||
_p=$(sed -e 's/#.*//' -e 's/[[:space:]]//g' "$WORDLIST" 2>/dev/null \
|
||
| grep -v '^$' | tr '\n' '|' | sed 's/|$//')
|
||
[ -n "$_p" ] || return 1
|
||
printf '%s' "$_p"
|
||
}
|
||
|
||
# 亂碼樣式一律用位元組比對(LC_ALL=C),不靠語系的字元範圍:
|
||
# 替代字元 U+FFFD(EF BF BD)本身;
|
||
# � ——「U+FFFD 的 UTF-8 位元組再被當成 Latin-1 讀一次」的雙重編碼殘骸;
|
||
# U+00C0–U+00FF 的字後面緊接 U+0080–U+00BF 的字(ä、æ¸、è©、ç”),這是 UTF-8 被當成
|
||
# Latin-1 讀一次再存回 UTF-8 的固定長相。中文的 UTF-8 前導位元組落在 E4–E9,被誤讀後
|
||
# 就變成 ä–é 開頭、後面接 U+0080–U+00BF 的兩三個字,所以前導字要收整個 Latin-1 字母段,
|
||
# 只收「Ã」會漏掉最常見的中文亂碼;
|
||
# â 後面接任何非 ASCII 字(’、“),引號與破折號被雙重編碼時的典型長相。
|
||
# 用位元組比對是因為 grep -E 的字元範圍在不同語系下行為不一致,位元組範圍到哪都一樣。
|
||
# 誤報防線:正常的西歐文字(câmara、crème、naïve)重音字後面接的是 ASCII 字母,不會命中。
|
||
mojibake_pattern() {
|
||
_fffd=$(printf '\357\277\275')
|
||
_lo=$(printf '\200'); _hi=$(printf '\277')
|
||
_c3=$(printf '\303'); _c2=$(printf '\302') # U+00C0–U+00FF 與 U+0080–U+00BF 的前導位元組
|
||
_bx=$(printf '\303\242') # 「â」
|
||
_l2=$(printf '\302'); _h2=$(printf '\364')
|
||
printf '%s|%s|%s[%s-%s]%s[%s-%s]|%s[%s-%s]' \
|
||
"$_fffd" '�' \
|
||
"$_c3" "$_lo" "$_hi" "$_c2" "$_lo" "$_hi" \
|
||
"$_bx" "$_l2" "$_h2"
|
||
}
|
||
|
||
hit() { # $1=樣式 $2=說明;命中就把說明與最多三行證據印到 stdout
|
||
m=$(printf '%s\n' "$lines" | LC_ALL=C grep -nE "$1" 2>/dev/null | head -n 3)
|
||
[ -n "$m" ] || return 0
|
||
printf ' %s\n' "$2"
|
||
printf '%s\n' "$m" | sed 's/^/ /'
|
||
}
|
||
|
||
scan_file() { # $1=檔案路徑;命中就把報告印到 stdout 並回傳 1,沒命中回傳 0
|
||
f=$1
|
||
[ -f "$f" ] || return 0
|
||
|
||
# 產生檔與壓縮輸出跳過:內容不是人寫的,抓到也沒有人要改。
|
||
case "$f" in
|
||
*.lock|*.min.js|*.min.css|*.map) return 0 ;;
|
||
esac
|
||
# 字表與兩支語言規則腳本跳過:這幾份檔案裡的簡體字與亂碼樣本是「被討論的對象」,
|
||
# 不是被使用。同一個道理,jsc-meta 的 ste100-lint.sh 也跳過 references/ste100.md。
|
||
# 不跳過的話,這支 hook 每次都會先抓到自己,訊號全被自己的噪音蓋掉。
|
||
case "$f" in
|
||
*/simplified.txt|simplified.txt) return 0 ;;
|
||
*/ste100-guard.sh|ste100-guard.sh) return 0 ;;
|
||
*/lang-guard.sh|lang-guard.sh) return 0 ;;
|
||
*/references/ste100.md|ste100.md) return 0 ;;
|
||
*/ste100-lint.sh|ste100-lint.sh) 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
|
||
|
||
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
|
||
|
||
out=$(
|
||
_sp=$(simplified_pattern) && hit "$_sp" '簡體字,改成繁體'
|
||
hit "$(mojibake_pattern)" '亂碼:替代字元或雙重編碼殘骸,重寫這一段'
|
||
# 編碼檢查沒有行號可指,命中就整檔報一行。沒有 iconv 就跳過這一項。
|
||
if command -v iconv >/dev/null 2>&1; then
|
||
printf '%s\n' "$lines" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1 \
|
||
|| printf ' %s\n' '非 UTF-8 編碼,整檔轉存成 UTF-8(不加 BOM)'
|
||
fi
|
||
)
|
||
|
||
[ -n "$out" ] || return 0
|
||
printf '%s\n' "$f"
|
||
printf '%s\n' "$out"
|
||
return 1
|
||
}
|
||
|
||
advice() {
|
||
printf ' 修法:簡體字換成對應繁體字;亂碼那段重打,不要留著半壞的字元;檔案存成 UTF-8。\n'
|
||
printf ' 規則正文見 jsc-meta 的 references/ste100.md,字表在 hooks/simplified.txt。誤判時用 JSC_LANG_GUARD=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-lang-guard.$$
|
||
: > "$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
|