2f5d9f3bb2
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>
168 lines
7.2 KiB
Bash
168 lines
7.2 KiB
Bash
#!/bin/bash
|
||
# jdd-format-guard.sh — PM 軌文件的格式閘(JDD 規則 J4+J5+J8)
|
||
#
|
||
# 掛 PreToolUse(matcher: Write|Edit|MultiEdit)。
|
||
#
|
||
# J4 root.md 的 🔴 卡缺【要驗證 + 對帳日】 → 擋
|
||
# 為什麼:紅卡=賭注。賭注沒有「賭輸了怎麼知道」和「什麼時候結算」,
|
||
# 就會永遠是「還在做」,永遠不必認賠——那不是賭注,是藉口。
|
||
#
|
||
# J5 tasks.md **新增**的任務缺站號 → 擋
|
||
# 為什麼:sprint 的單位是站。任務不掛站,就沒有人答得出
|
||
# 「這個任務不做,哪一站會掛?」——那正是認領流程唯一的問題。
|
||
#
|
||
# J8 root.md/journeys.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
|
||
|
||
# ── J8:root.md/journeys.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
|