Files
system-dev-template/template/.claude/hooks/jdd-format-guard.sh
T
Leo 2f5d9f3bb2 feat(W2 Phase 2-3): JDD 文件範本+八條封路 hook+還清兩件舊債
SDD: docs/3-specs/jdd-dual-profile(active)。編號 task 26/33 完成,Phase 4-5 未開工。

■ Phase 2 JDD 文件範本(orchestrator profile)
範本形狀對齊「實際跑出來的那兩份」(總管已寫的 root.md 15 卡、journeys.md J-1 九站),
不是照規格憑空造:
- 卡片是巢狀 bullet(`- **P1** 🟢 …` + 子項放來源/對帳),非規格畫的平行文字行
- 站點索引**巢狀 bullet 不用表格**(表格會把層級壓平,看不出從屬)
- 兩份都保留「這卷還缺什麼(誠實記)」收尾段——規格沒有,但那是防假綠的地方
新增:root.md / journeys.md / sprint.md / triage-map.md 四範本(add-if-missing,
填了就永不覆蓋)+ plugin-load-order.md(W3 插槽,框架不發明平行外掛格式)

■ Phase 3 封路 hook(八條規則落六支檔)
- role-guard(J1+J2+J3)★命門:考生不能改考卷。六組實測含「考題藏在別的 md 裡」也擋
- jdd-format-guard(J4+J5+J8):紅卡缺對帳日/任務缺站號/PM 文件混技術名詞
- station-done-guard(J6):收工判準是站的考題全綠,不是任務全關
- regression-scope(J7):動實作 → 列出要重考哪幾題(只提醒不擋)
- install-artifact-guard(S1):實例不改機制
- orchestrator-scope-guard(S4):總管不進成員 repo 動實作(從實例上收進框架,
  路徑清單改由實例自填,範本零專名)
掛載鏈依「範圍大的擋在前」:改機制 → 角色 → 位置 → 格式 → 既有三支

■ 還清兩件舊債
- update.sh 檔案清單改讀 manifest(舊硬編降為抓不到來源時的 fallback)
  ——install/update 兩份手抄清單漂移的根因全修
- CLAUDE.md 界標補植:舊實例全文原封包進本地區、框架區重鋪、原檔備份、冪等
  ——解開「沒界標⇒不敢覆蓋⇒框架改的憲法永遠送不到既有實例」這個死結

■ 修掉三個自己造的問題(實測抓出來的,不是想出來的)
- jdd-format-guard 誤擋真實 journeys.md 的「這卷還缺什麼」自述段
  → 排除法改**正面圈定**(只掃卡片本體與站內文),說明區/自述段/索引自然不在範圍
- install-artifact-guard 把 pre-write-guard.sh 也擋了——而它的錯誤訊息正叫人去改那支
  → 使用者自訂插槽列為最優先放行
- check-legacy-paths 用 HEAD 當基準會**自我弱化**:改成清單驅動後保護範圍 35→29 條
  → 基準改指最後一次真正發佈的版本

■ 實測(全部貼過輸出)
- G2 考生改考卷:6/6,含 orchestrator 寫 code/engineer 改考題/考題藏別處
- G4 憲法分流:兩環境重裝,總管版技術軌關鍵字 0、成員版上游指針 8,界標 4/4
- G6 實例改機制:4/4,含框架開發標記放行與自訂插槽放行
- G7 CI 擋實例名:注入違規 → 指出檔案行號 exit 1
- G3 進度以站計量:起牀推「J-1 已點亮 2/9 站」、收工列未亮站並禁用任務數當理由
- 回歸考、界標補植冪等、orchestrator-scope-guard 四組:全通

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:26:56 +08:00

168 lines
7.2 KiB
Bash
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.
#!/bin/bash
# jdd-format-guard.sh — PM 軌文件的格式閘(JDD 規則 J4+J5+J8)
#
# 掛 PreToolUsematcher: Write|Edit|MultiEdit)。
#
# J4 root.md 的 🔴 卡缺【要驗證 + 對帳日】 → 擋
# 為什麼:紅卡=賭注。賭注沒有「賭輸了怎麼知道」和「什麼時候結算」,
# 就會永遠是「還在做」,永遠不必認賠——那不是賭注,是藉口。
#
# J5 tasks.md **新增**的任務缺站號 → 擋
# 為什麼:sprint 的單位是站。任務不掛站,就沒有人答得出
# 「這個任務不做,哪一站會掛?」——那正是認領流程唯一的問題。
#
# J8 root.mdjourneys.md 出現技術名詞 → 擋
# 為什麼:這兩份是給不懂技術的人讀的。技術是達成手段,寫進各專案自己的規格。
#
# ── J8 的關鍵細節:只掃「卡片/站的本體」,其餘一概不掃 ──────────
# 踩過兩次同一類坑(第二次是本閘自己被真實文件抓包):
# ① 文件開頭的規矩說明裡寫「各 repo 的規格」→ 被自己的自檢抓到
# ② 文件結尾「這卷還缺什麼」的自述裡提到跨 repo 鏈路 → 又被抓到
# 兩者都不是卡片內容,是**文件在講自己**。
# 閘要是連這些都掃,人就只能把說明寫得不清不楚來換綠燈,本末倒置。
#
# 第一版用「排除法」(跳過 > 引言、註解、標題)——不夠,因為自述段是普通條列。
# 改用**正面圈定**:只掃真正的內容體
# · root.md `- **P<n>**` 卡片行 它底下的縮排子項
# · journeys.md `#### S<n>` 站標題以下、到下一個標題之前的內文
# 自述段、說明區、索引表因為不在這兩種範圍裡,自然就不會被掃到——
# 不必為它們一個個開例外。
#
# 誠實限制:只認字面與行首形狀。
# 「把技術概念用白話包裝起來」它看不出來(那要人讀);
# 用 bash 繞道改檔也擋不到。價值是擋掉明顯的格式錯誤與手滑,不是技術防偽。
set -uo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0
INPUT="$(cat)"
FILE_PATH="$(sdt_file_path "$INPUT")"
[ -z "$FILE_PATH" ] && exit 0
REL="$(sdt_rel_path "$FILE_PATH")"
CONTENT="$(sdt_write_content "$INPUT")"
[ -z "$CONTENT" ] && exit 0
TECH_TERMS='API|SDK|CLI|MCP|WASM|endpoint|schema|webhook|resturl|JSON|YAML|SQL|資料庫|後端|前端|部署|repo|commit|branch'
# 正面圈定要掃的行(見上方說明):
# root.md → `- **P<n>**` 卡片行 其縮排子項
# journeys.md → `#### S<n>` 站標題以下到下一個標題之前的內文
card_body_only() { # $1=root|journeys
if [ "$1" = "root" ]; then
awk '
/<!--/ { inc=1 } inc { if (/-->/) inc=0; next }
/^[[:space:]]*-[[:space:]]*\*\*P[0-9]+\*\*/ { incard=1; print NR "\t" $0; next }
incard && /^[[:space:]]+[-*]/ { print NR "\t" $0; next } # 卡片的縮排子項
{ incard=0 }
'
else
awk '
/<!--/ { inc=1 } inc { if (/-->/) inc=0; next }
/^####[[:space:]]*S[0-9]+/ { instation=1; next } # 進入某一站
/^#/ { instation=0; next } # 任何標題結束該站
instation && /^[[:space:]]*>/ { next } # 站內引言仍不掃
instation && /^[[:space:]]*$/ { next }
instation { print NR "\t" $0 }
'
fi
}
fail() { # $1=規則 $2=標題 $3=細節
cat >&2 <<EOF
❌ BLOCKED by jdd-format-guard(規則 $1
$2
檔案:$REL
$3
EOF
exit 2
}
# ── J4:root.md 的紅卡必須附【要驗證 + 對帳日】────────
if printf '%s' "$REL" | grep -q 'root\.md$'; then
# 逐張紅卡檢查:紅卡行之後、下一張卡之前,要出現「對帳日」
MISSING="$(printf '%s' "$CONTENT" | awk '
/^[[:space:]]*-[[:space:]]*\*\*P[0-9]+\*\*/ {
if (pending != "" && !found) { print pending }
found = 0
if ($0 ~ /🔴/) { pending = NR "\t" $0 } else { pending = "" }
next
}
pending != "" && /對帳日/ { found = 1 }
END { if (pending != "" && !found) print pending }
')"
if [ -n "$MISSING" ]; then
fail "J4" "紅卡(🔴)少了【要驗證 + 對帳日】。" \
" 下列紅卡沒有對帳行:
$(printf '%s' "$MISSING" | sed 's/^/ 行 /' | cut -c1-120)
補成這樣:
- **P9** 🔴 <一句白話>。
- 【要驗證:<能用真實數字或事實判真假的判準> | 對帳日 YYYY-MM-DD】
為什麼擋:紅卡是賭注。沒有判準和結算日的賭注,永遠不必認賠——
那不是賭注,是把「還沒做到」講得像「正在做」。"
fi
fi
# ── J8root.mdjourneys.md 不准出現技術名詞 ────────
case "$REL" in
*root.md|*journeys.md)
KIND="journeys"; printf '%s' "$REL" | grep -q 'root\.md$' && KIND="root"
HITS="$(printf '%s' "$CONTENT" | card_body_only "$KIND" | grep -inE "$TECH_TERMS" | head -8 || true)"
if [ -n "$HITS" ]; then
fail "J8" "PM 軌文件出現技術名詞。" \
" 命中(只掃卡片本體,已排除說明區/註解/標題):
$(printf '%s' "$HITS" | cut -c1-120 | sed 's/^/ /')
怎麼修:
· 把它翻成「使用者感覺得到的事」——例如不是「呼叫 API 取得資料」,
而是「我按下去之後,畫面上出現我的東西」
· 真的必須談技術 → 那句話屬於各專案自己的規格,不屬於這裡
為什麼擋:這兩份文件唯一的讀者是「不懂技術但要點頭或搖頭的人」。
出現一個他看不懂的詞,他就沒辦法判斷這張卡是不是他的意思。"
fi
;;
esac
# ── J5:tasks.md 新增的任務必須掛站號 ─────────────────
if printf '%s' "$REL" | grep -q 'tasks\.md$'; then
# 只看這次要寫入的內容裡「長得像新任務」的行
NEWTASKS="$(printf '%s' "$CONTENT" | grep -nE '^[[:space:]]*-[[:space:]]*\[[ x~!🔄]\][[:space:]]*[0-9]+\.[0-9]+' || true)"
if [ -n "$NEWTASKS" ]; then
# 站號標注:任務行本身或緊接的子項出現 S<n> / S-<n> / 「服務:S…」
NOSTATION="$(printf '%s' "$CONTENT" | awk '
/^[[:space:]]*-[[:space:]]*\[[ x~!🔄]\][[:space:]]*[0-9]+\.[0-9]+/ {
if (pending != "" && !found) print pending
pending = NR "\t" $0; found = 0
if ($0 ~ /S-?[0-9]+/) found = 1
next
}
pending != "" && /S-?[0-9]+/ { found = 1 }
/^[[:space:]]*$/ { if (pending != "" && !found) { print pending; pending=""; found=0 } }
END { if (pending != "" && !found) print pending }
')"
if [ -n "$NOSTATION" ]; then
fail "J5" "新增的任務沒有標注它服務哪一站。" \
" 下列任務缺站號:
$(printf '%s' "$NOSTATION" | cut -c1-120 | sed 's/^/ 行 /')
補成這樣:
- [ ] 1.1 <任務描述>
- 服務:S3(我按一次就裝到自己的地方)
- 驗收:<客觀可驗證的完成標準>
為什麼擋:sprint 的單位是站。任務不掛站,就沒有人答得出認領流程唯一的問題——
「這個任務不做,指定站的考題會掛嗎?」答不出來,這個任務就沒有理由在這一期做。"
fi
fi
fi
exit 0