#!/bin/bash # role-guard.sh — 角色封路:誰能寫什麼(JDD 規則 J1+J2+J3) # # 掛 PreToolUse(matcher: Write|Edit|MultiEdit)。 # # ── 為什麼三條規則合成一支 hook ───────────────────────── # J1 orchestrator 不准寫 code # J2 orchestrator 不准寫技術軌文件(任務池/規格) # J3 engineer 不准改考卷(旅程/根文件/任何考題) # 三條都是「同一個 hook 事件 × 同一份身分判定 × 同一張路徑表」。 # 拆三支=解析三次 JSON、三處維護同一張表,改一條規則要記得改三個檔。 # 規則編號保留在程式碼與錯誤訊息裡,追溯不會少。 # # ── J3 是命門 ─────────────────────────────────────── # 考生不能改考卷。這條沒守住,整套 PM 軌就是裝飾品: # 考題沒過的人,只要改一下考題就「過了」,而且沒有人會發現。 # 所以 J3 攔的不只是檔案,還包括 md 檔裡的 Gherkin 區塊—— # 考題常常就住在別的文件裡。 # # 誠實限制:擋的是「直接寫檔」這個語法層動作。 # 用 bash 繞道(sed -i / cat > / python 改檔)擋不到。 # 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱不可能繞過。 set -uo pipefail HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/role-lib.sh . "$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")" ROLE="$(sdt_role)" # 身分矩陣先驗(成員 repo × orchestrator 這格不存在) sdt_assert_identity || exit 2 # ── 路徑分類 ─────────────────────────────────────── is_code_path() { case "$1" in src/*|*/src/*) return 0 ;; *.py|*.ts|*.tsx|*.js|*.jsx|*.go|*.rs|*.java|*.rb|*.c|*.h|*.cpp|*.sh|*.bash|*.zsh) return 0 ;; *.sql|*.vue|*.svelte|*.swift|*.kt|*.php) return 0 ;; esac return 1 } is_tech_track_doc() { # 技術軌文件:任務池與規格 case "$1" in *tasks.md|*requirements.md|*design.md) return 0 ;; esac return 1 } is_exam_paper() { # 考卷:旅程/根文件/獨立考題檔 case "$1" in *journeys.md|*root.md|*.feature) return 0 ;; esac return 1 } # md 檔內含 Gherkin 區塊?(考題常寄住在別的文件裡) content_has_gherkin() { local c c="$(sdt_write_content "$INPUT")" [ -z "$c" ] && return 1 # ```gherkin 圍籬,或 journeys.md 的考題行式(- **G-1.1**),或 Given/When/Then 三件成組 printf '%s' "$c" | grep -qiE '```[[:space:]]*gherkin' && return 0 printf '%s' "$c" | grep -qE '^\s*-?\s*\*\*G-[0-9]+\.[0-9]+\*\*' && return 0 if printf '%s' "$c" | grep -qE '^\s*-?\s*Given ' \ && printf '%s' "$c" | grep -qE '^\s*-?\s*When ' \ && printf '%s' "$c" | grep -qE '^\s*-?\s*Then '; then return 0 fi return 1 } block() { # $1=規則編號 $2=標題 $3=正確做法(多行) cat >&2 <