From e3a9c67781b565709900d922ac54fd9d633a6d79 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 09:49:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(lang-guard):=20=E6=96=B0=E5=A2=9E=E7=AC=AC?= =?UTF-8?q?=E4=B8=83=E6=94=AF=20hook=EF=BC=8C=E5=BC=B7=E5=88=B6=E9=9D=9E?= =?UTF-8?q?=E7=A8=8B=E5=BC=8F=E7=A2=BC=E8=BC=B8=E5=87=BA=E7=B9=81=E4=B8=AD?= =?UTF-8?q?=E7=84=A1=E4=BA=82=E7=A2=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增 `hooks/lang-guard.sh` 與機檢字表 `hooks/simplified.txt`。腳本比照 `comment-scope.sh` 的結構,一樣三種模式:`prompt` 注入規則摘要、無參數掃剛寫入的單一檔案、`sweep [dir]` 掃整個 git 工作區這次改過的檔案。偵測三項:簡體字、亂碼(U+FFFD 與雙重編碼殘骸)、非 UTF-8 編碼。命中走 stderr 並 exit 2,資料不足或找不到 git 一律安靜 exit 0,逃生門 `JSC_LANG_GUARD=off`。 Why:使用者新增的規則是「非程式碼的輸出一律套用繁中無亂碼」,範圍涵蓋程式碼註解、commit 訊息、PR 描述、wiki 頁、對使用者的回報與各種文件。這條規則原本只有 `ste100-guard.sh` 的提示層,模型看得到卻沒有人檢查,寫出簡體字或亂碼不會有任何回饋。提示層擋不住的事,就要有機檢層。 How:簡體字樣式由字表組出,字表是單一真實來源,腳本裡不留第二份;字表讀不到就安靜跳過這一項,不中斷整支腳本。亂碼一律用位元組比對(`LC_ALL=C`),不靠語系的字元範圍,因為 `grep -E` 的字元範圍在不同語系下行為不一致。掃描深度沿用 `comment-scope.sh`:檔案已追蹤就只掃 `git diff HEAD` 的新增行,不翻舊帳。二進位檔只認 NUL 位元組判定,不拿「非可列印字元」當判準,那會把所有含中文的檔案誤判成二進位。字表刻意排除繁體也在用的字(后、台、干、只、里、面、制、志),並跳過 `simplified.txt`、`ste100-guard.sh`、`lang-guard.sh` 三份以簡體字與亂碼為討論對象的檔案,避免整支 hook 每次先抓到自己。 Who:`jsc-hooks` 的 hook 實作層,供 `hooks/hooks.json` 與 `tools/wire-cli.sh` 接線給五個 CLI 使用。 --- hooks/lang-guard.sh | 176 +++++++++++++++++++++++++++++++++++++++++++ hooks/simplified.txt | 109 +++++++++++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100755 hooks/lang-guard.sh create mode 100644 hooks/simplified.txt diff --git a/hooks/lang-guard.sh b/hooks/lang-guard.sh new file mode 100755 index 0000000..2954892 --- /dev/null +++ b/hooks/lang-guard.sh @@ -0,0 +1,176 @@ +#!/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 ;; + 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 diff --git a/hooks/simplified.txt b/hooks/simplified.txt new file mode 100644 index 0000000..5ccc5ab --- /dev/null +++ b/hooks/simplified.txt @@ -0,0 +1,109 @@ +# simplified.txt — 機檢用的簡體字表,一行一個字,UTF-8。 +# 用途:hooks/lang-guard.sh 讀這份字表組出 grep 樣式,判定非程式碼輸出有沒有夾帶簡體字。 +# 這是本存取庫的單一真實來源,jsc-meta 的 tools/ste100-lint.sh 也優先讀這份。 +# 排除原則:只收簡化後才出現的字形,刻意排除繁體也在用的字(后、台、干、只、里、面、制、志)。 +# 加進那些字會讓「太后」「電視台」「干涉」「只有」「公里」「面板」「制度」「志工」全部誤報。 +# 增刪字元前先想清楚:這份字表寧可漏抓,也不可誤報。 +# `#` 開頭的行與空行由讀取端忽略。 +应 +为 +这 +说 +发 +国 +过 +对 +开 +关 +问 +题 +东 +车 +马 +鸟 +龙 +飞 +见 +无 +产 +业 +务 +书 +写 +学 +习 +报 +纸 +认 +识 +证 +际 +网 +络 +计 +划 +实 +现 +给 +条 +约 +级 +组 +织 +变 +换 +电 +脑 +两 +双 +单 +构 +价 +钱 +众 +议 +论 +传 +统 +么 +儿 +们 +从 +来 +时 +间 +长 +门 +闻 +声 +员 +图 +团 +转 +输 +达 +运 +进 +远 +连 +边 +还 +经 +该 +营 +规 +则 +范 +围 +参 +数 +类 +结 +态 +设 +备 +辑 +译 +码 +库 +档