取消 [[頁名]] 與 [[顯示文字|頁名]] 兩種同 wiki 寫法,不再分「同存取庫」與 「跨存取庫」兩條規則。那種寫法只在自己那個 wiki 內解析,寫錯不報錯,畫面上 看起來像普通文字或死連結,巡不到也修不了。 連結寫進頁面前先過 jsc-gitea 的 link-check.sh,結束碼 0 才寫。驗證一律走 API, 不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把 好連結判成壞的。認證失敗回 7,與死連結的 1 分開,免得金鑰一過期就把還在的頁 整批判死。
353 lines
14 KiB
Bash
Executable File
353 lines
14 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
# stage-report.sh — SDLC 階段收尾回報(規劃、分析、實作、維護共用)。
|
||
#
|
||
# 為什麼要有這支腳本:階段結束要回報的東西是固定的——模型閘門判定、工作日誌連結、所有寫入的
|
||
# wiki 連結,實作階段再加上工作目錄與三條分支。寫在技能內文裡靠模型自己記,少一項看不出來;
|
||
# 所以彙整搬到程式層:頁名換成絕對網址、commit 數與推送狀態由 git 現查、沒有工作日誌就當場
|
||
# 警告並把內容存進暫存區,全部由本檔產出,技能只負責把事實餵進來。
|
||
#
|
||
# 用法:
|
||
# stage-report.sh <stage> [選項]
|
||
# <stage> = plan | analyze | implement | maintain
|
||
#
|
||
# 共用選項:
|
||
# --page TYPE:PAGE 本階段寫入的 wiki 頁,可重複(例:--page ANALYZE:ANALYZE_D3F1A2B0)
|
||
# --worklog PAGE 已寫入的工作日誌頁名(例:LOG_D3F1A2B0)
|
||
# --worklog-heading TEXT 日誌條目標題,用來組出導向該條目的錨點連結
|
||
# --pending-file FILE 沒有工作日誌時,要暫存起來的日誌內容檔
|
||
# --log-hash HASH 暫存歸屬的工作日誌 hash(即 LOG_{HASH} 的 HASH)
|
||
#
|
||
# 實作階段選項:
|
||
# --worktree PATH 工作目錄(worktree 路徑)
|
||
# --source-branch NAME 來源分支(本檔自行判定遠端有沒有這一條)
|
||
# --work-branch NAME 工作分支(本檔自行算 commit 數與推送狀態)
|
||
# --pr URL 目標分支的 PR 連結
|
||
# --target-branch NAME 目標分支(省略時等同來源分支)
|
||
#
|
||
# 輸出: 繁體中文 markdown 階段回報,直接貼給使用者。
|
||
# 結束碼: 0=回報完整 1=有警告(缺工作日誌或有連不到的連結) 2=用法錯誤 3=相依工具找不到
|
||
#
|
||
# 連結驗證:
|
||
# 本檔輸出的是一張給人點的 wiki 連結清單,所以印之前先把這些網址交給
|
||
# jsc-gitea/tools/link-check.sh 驗一次,逐列標出「通過、連不到、未驗證」。
|
||
# 驗證走 API 不看網頁狀態碼:私有存取庫的網頁網址對沒帶金鑰的請求一律回 404。
|
||
# 這裡的驗證只註記、不阻擋——頁面在呼叫本檔之前就寫完了,擋下去只會讓使用者連回報都拿不到。
|
||
# 呼叫端該做的事在寫入之前:每個要放進頁面的連結先過 link-check.sh,結束碼 0 才寫入,
|
||
# 有 DEAD 就不寫並回報。本檔是最後一道複查,不是那一道關卡的替代品。
|
||
#
|
||
# 陷阱:
|
||
# - 結束碼 1 是警告,不是阻擋:階段的工作已經做完了,擋下去只會讓使用者拿不到回報。
|
||
# - 缺工作日誌時一定要給 --pending-file 與 --log-hash,否則內容留在對話裡,換一個工作階段就沒了。
|
||
# - 模型閘門那一列只轉述 sdlc-gate.sh report 的結果,不自己判定標籤。
|
||
# - 金鑰失效(link-check.sh 結束碼 7)標成「未驗證」,不標成「連不到」:把金鑰問題寫成死連結,
|
||
# 下一手就會照著去刪還活著的頁。
|
||
set -u
|
||
|
||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||
plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}"
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
用法:
|
||
stage-report.sh <plan|analyze|implement|maintain> [選項]
|
||
--page TYPE:PAGE 本階段寫入的 wiki 頁,可重複
|
||
--worklog PAGE 已寫入的工作日誌頁名
|
||
--worklog-heading TEXT 日誌條目標題(組錨點用)
|
||
--pending-file FILE 沒有工作日誌時要暫存的內容檔
|
||
--log-hash HASH 暫存歸屬的工作日誌 hash
|
||
--worktree PATH 實作:工作目錄
|
||
--source-branch NAME 實作:來源分支
|
||
--work-branch NAME 實作:工作分支
|
||
--target-branch NAME 實作:目標分支(省略時同來源分支)
|
||
--pr URL 實作:PR 連結
|
||
結束碼: 0=完整 1=缺工作日誌(已暫存,警告) 2=用法錯誤 3=相依工具找不到
|
||
EOF
|
||
exit 2
|
||
}
|
||
|
||
# 找相依腳本。兩種版面都要顧到:並排存取庫(開發用)與已安裝 plugin(各版本一個目錄)。
|
||
resolve_dep() { # $1=並排目錄名(gitea、hooks、log) $2=plugin 內的相對路徑
|
||
_name="$1"; _rel="$2"
|
||
for _c in "$plugin_root/../$_name/$_rel" "$plugin_root/../jsc-$_name/$_rel"; do
|
||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
done
|
||
_c=$(ls -d "$plugin_root"/../../"jsc-$_name"/*/"$_rel" \
|
||
"$plugin_root"/../../"$_name"/*/"$_rel" \
|
||
"$HOME"/.claude/plugins/cache/*/"jsc-$_name"/*/"$_rel" 2>/dev/null \
|
||
| sort | tail -n1)
|
||
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
return 1
|
||
}
|
||
|
||
gitea_sh() {
|
||
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
|
||
printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0
|
||
fi
|
||
resolve_dep gitea tools/gitea.sh
|
||
}
|
||
|
||
sdlc_gate_sh() {
|
||
if [ -n "${JSC_HOOKS_DIR:-}" ] && [ -f "$JSC_HOOKS_DIR/sdlc-gate.sh" ]; then
|
||
printf '%s\n' "$JSC_HOOKS_DIR/sdlc-gate.sh"; return 0
|
||
fi
|
||
resolve_dep hooks hooks/sdlc-gate.sh
|
||
}
|
||
|
||
link_check_sh() {
|
||
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/link-check.sh" ]; then
|
||
printf '%s\n' "$JSC_GITEA_TOOLS/link-check.sh"; return 0
|
||
fi
|
||
resolve_dep gitea tools/link-check.sh
|
||
}
|
||
|
||
stage="${1:-}"
|
||
case "$stage" in
|
||
plan|analyze|implement|maintain) shift ;;
|
||
*) usage ;;
|
||
esac
|
||
|
||
pages=''
|
||
worklog=''
|
||
worklog_heading=''
|
||
pending_file=''
|
||
log_hash=''
|
||
worktree=''
|
||
source_branch=''
|
||
work_branch=''
|
||
target_branch=''
|
||
pr_url=''
|
||
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--page) [ "$#" -ge 2 ] || usage; pages="$pages$2
|
||
"; shift 2 ;;
|
||
--worklog) [ "$#" -ge 2 ] || usage; worklog="$2"; shift 2 ;;
|
||
--worklog-heading) [ "$#" -ge 2 ] || usage; worklog_heading="$2"; shift 2 ;;
|
||
--pending-file) [ "$#" -ge 2 ] || usage; pending_file="$2"; shift 2 ;;
|
||
--log-hash) [ "$#" -ge 2 ] || usage; log_hash="$2"; shift 2 ;;
|
||
--worktree) [ "$#" -ge 2 ] || usage; worktree="$2"; shift 2 ;;
|
||
--source-branch) [ "$#" -ge 2 ] || usage; source_branch="$2"; shift 2 ;;
|
||
--work-branch) [ "$#" -ge 2 ] || usage; work_branch="$2"; shift 2 ;;
|
||
--target-branch) [ "$#" -ge 2 ] || usage; target_branch="$2"; shift 2 ;;
|
||
--pr) [ "$#" -ge 2 ] || usage; pr_url="$2"; shift 2 ;;
|
||
*) echo "[jsc][階段回報][ERR]:不認得的選項「$1」。" >&2; usage ;;
|
||
esac
|
||
done
|
||
|
||
GITEA=$(gitea_sh) || { echo '[jsc][階段回報][ERR]:找不到 jsc-gitea/tools/gitea.sh,無法把頁名換成網址。' >&2; exit 3; }
|
||
GATE=$(sdlc_gate_sh) || { echo '[jsc][階段回報][ERR]:找不到 jsc-hooks/hooks/sdlc-gate.sh,無法轉述閘門判定。' >&2; exit 3; }
|
||
|
||
stage_zh() {
|
||
case "$1" in
|
||
plan) echo '規劃' ;;
|
||
analyze) echo '分析' ;;
|
||
implement) echo '實作' ;;
|
||
maintain) echo '維護' ;;
|
||
esac
|
||
}
|
||
|
||
required_tag() {
|
||
case "$1" in
|
||
plan|analyze) echo 'reasoning-max' ;;
|
||
implement) echo 'coding' ;;
|
||
maintain) echo '無(模型 id 可判定即通過)' ;;
|
||
esac
|
||
}
|
||
|
||
# 錨點:Gitea wiki 的標題錨點比照 GitHub——轉小寫、空白換連字號、去掉標點;中文原樣保留。
|
||
anchor_of() {
|
||
printf '%s' "$1" \
|
||
| tr 'A-Z' 'a-z' \
|
||
| sed 's/[][()#。,、:;!?,.:;!?]//g; s/[[:space:]]\{1,\}/-/g'
|
||
}
|
||
|
||
page_url() { # TYPE PAGE -> 絕對網址;查不到就回空字串
|
||
_type="$1"; _page="$2"
|
||
_repo=$("$GITEA" wiki-repo "$_type" 2>/dev/null) || return 1
|
||
"$GITEA" wiki-url "$_repo" "$_page" 2>/dev/null || return 1
|
||
}
|
||
|
||
# ---- 模型能力標籤 ----
|
||
gate_line=$("$GATE" report 2>/dev/null || true)
|
||
gate_stage=$(printf '%s' "$gate_line" | awk '{print $2}')
|
||
gate_tag=$(printf '%s' "$gate_line" | awk '{print $3}')
|
||
gate_model=$(printf '%s' "$gate_line" | awk '{print $4}')
|
||
if [ -z "$gate_line" ]; then
|
||
gate_result="查不到閘門狀態(未上鎖或狀態檔不在),請重跑 sdlc-gate.sh lock $stage"
|
||
elif [ "$gate_stage" != "$stage" ]; then
|
||
gate_result="不符:本階段為 $stage,鎖上的卻是 $gate_stage,請重跑 sdlc-gate.sh lock $stage"
|
||
else
|
||
gate_result="通過(必要標籤 $(required_tag "$stage"),實際模型 ${gate_model:-未知},鎖上時的標籤 ${gate_tag:-無})"
|
||
fi
|
||
|
||
# ---- 工作日誌 ----
|
||
warn=0
|
||
worklog_cell=''
|
||
if [ -n "$worklog" ]; then
|
||
wl_url=$(page_url LOG "$worklog" || true)
|
||
if [ -n "$wl_url" ]; then
|
||
if [ -n "$worklog_heading" ]; then
|
||
worklog_cell="[$worklog_heading]($wl_url#$(anchor_of "$worklog_heading"))"
|
||
else
|
||
worklog_cell="[$worklog]($wl_url)"
|
||
fi
|
||
else
|
||
worklog_cell="$worklog(取不到網址,請確認 LOG 的 wiki 存取庫設定)"
|
||
fi
|
||
else
|
||
warn=1
|
||
worklog_cell='**尚未寫入**'
|
||
fi
|
||
|
||
# ---- 暫存 ----
|
||
pending_note=''
|
||
if [ "$warn" -eq 1 ]; then
|
||
if [ -n "$pending_file" ] && [ -n "$log_hash" ]; then
|
||
PENDING=$(resolve_dep log tools/worklog-pending.sh || true)
|
||
if [ -n "$PENDING" ]; then
|
||
if saved=$("$PENDING" add "$log_hash" "$pending_file" 2>&1); then
|
||
pending_note="已暫存到 $saved,下次 jsc-log:worklog 寫入時一併寫進去。"
|
||
else
|
||
pending_note="暫存失敗:$saved"
|
||
fi
|
||
else
|
||
pending_note='找不到 jsc-log/tools/worklog-pending.sh,這次的內容沒有暫存起來。'
|
||
fi
|
||
else
|
||
pending_note='沒有給 --pending-file 與 --log-hash,這次的內容沒有暫存起來。'
|
||
fi
|
||
fi
|
||
|
||
# ---- 分支資訊(實作階段) ----
|
||
branch_rows=''
|
||
if [ "$stage" = implement ]; then
|
||
wt="${worktree:-}"
|
||
git_in() { # 在工作目錄裡跑 git
|
||
if [ -n "$wt" ]; then git -C "$wt" "$@" 2>/dev/null; else git "$@" 2>/dev/null; fi
|
||
}
|
||
|
||
src_cell='未提供'
|
||
if [ -n "$source_branch" ]; then
|
||
if git_in show-ref --verify --quiet "refs/remotes/origin/$source_branch"; then
|
||
src_cell="origin/$source_branch(遠端)"
|
||
else
|
||
src_cell="$source_branch(**遠端找不到**,只有本機)"
|
||
fi
|
||
fi
|
||
|
||
work_cell='未提供'
|
||
if [ -n "$work_branch" ]; then
|
||
base="origin/$source_branch"
|
||
git_in show-ref --verify --quiet "refs/remotes/origin/$source_branch" || base="$source_branch"
|
||
count=$(git_in rev-list --count "$base..$work_branch" || true)
|
||
[ -n "$count" ] || count='?'
|
||
if git_in show-ref --verify --quiet "refs/remotes/origin/$work_branch"; then
|
||
ahead=$(git_in rev-list --count "origin/$work_branch..$work_branch" || true)
|
||
if [ "${ahead:-0}" = 0 ]; then push_cell='已 push'; else push_cell="尚有 ${ahead} 個 commit 未 push"; fi
|
||
else
|
||
push_cell='**未 push**'
|
||
fi
|
||
work_cell="$work_branch($count commit、$push_cell)"
|
||
fi
|
||
|
||
tgt="${target_branch:-$source_branch}"
|
||
if [ -n "$pr_url" ]; then
|
||
tgt_cell="${tgt:-未提供}(PR $pr_url)"
|
||
else
|
||
tgt_cell="${tgt:-未提供}(**尚無 PR**)"
|
||
warn=1
|
||
fi
|
||
|
||
branch_rows=$(cat <<EOF
|
||
| 工作目錄 | ${worktree:-未提供} |
|
||
| 來源分支 | $src_cell |
|
||
| 工作分支 | $work_cell |
|
||
| 目標分支 | $tgt_cell |
|
||
EOF
|
||
)
|
||
fi
|
||
|
||
# ---- 連結驗證 ----
|
||
# 先把頁名全部解成網址收在 page_rows,再一次餵給 link-check.sh:一頁一次呼叫是白付的往返成本。
|
||
TAB=$(printf '\t')
|
||
page_rows=''
|
||
url_list=''
|
||
if [ -n "$pages" ]; then
|
||
page_rows=$(printf '%s' "$pages" | while IFS= read -r item; do
|
||
[ -n "$item" ] || continue
|
||
type=${item%%:*}
|
||
page=${item#*:}
|
||
url=$(page_url "$type" "$page" || true)
|
||
printf '%s%s%s\n' "$page" "$TAB" "${url:-}"
|
||
done)
|
||
url_list=$(printf '%s\n' "$page_rows" | awk -F"$TAB" 'NF>1 && $2!=""{print $2}')
|
||
fi
|
||
[ -n "$worklog_cell" ] && [ -n "${wl_url:-}" ] && url_list=$(printf '%s\n%s' "$url_list" "$wl_url")
|
||
|
||
LINKCHECK=''
|
||
link_note=''
|
||
dead_urls=''
|
||
skip_urls=''
|
||
if [ -n "$url_list" ]; then
|
||
LINKCHECK=$(link_check_sh || true)
|
||
if [ -z "$LINKCHECK" ]; then
|
||
link_note='找不到 jsc-gitea/tools/link-check.sh,這次的連結沒有驗證過。'
|
||
else
|
||
lc_out=$(printf '%s\n' "$url_list" | "$LINKCHECK" 2>/dev/null)
|
||
lc_rc=$?
|
||
case "$lc_rc" in
|
||
0|1)
|
||
dead_urls=$(printf '%s\n' "$lc_out" | awk -F"$TAB" '$1=="DEAD"{print $2}')
|
||
# SKIP 是「沒有可查的端點」,不影響 link-check.sh 的結束碼,所以分開標、不進警告。
|
||
skip_urls=$(printf '%s\n' "$lc_out" | awk -F"$TAB" '$1=="SKIP"{print $2}')
|
||
;;
|
||
3) link_note='GITEA_HOST 沒設定,連結沒有驗證過;設好變數再跑一次。' ;;
|
||
7) link_note='Gitea 金鑰失效(401、403),連結沒有驗證過。這不代表連結是死的,先處理金鑰。' ;;
|
||
*) link_note="連結驗證沒跑完(link-check.sh 結束碼 $lc_rc)。" ;;
|
||
esac
|
||
fi
|
||
fi
|
||
[ -n "$dead_urls" ] && warn=1
|
||
|
||
verify_cell() { # $1=網址 -> 驗證結果欄
|
||
[ -n "$1" ] || { printf '未驗證'; return 0; }
|
||
if [ -z "$LINKCHECK" ] || [ -n "$link_note" ]; then printf '未驗證'; return 0; fi
|
||
if printf '%s\n' "$dead_urls" | grep -Fxq -- "$1"; then printf '**連不到**'
|
||
elif printf '%s\n' "$skip_urls" | grep -Fxq -- "$1"; then printf '無可查端點'
|
||
else printf '通過'; fi
|
||
}
|
||
|
||
# ---- 輸出 ----
|
||
printf '## 階段回報:%s\n\n' "$(stage_zh "$stage")"
|
||
printf '| 項目 | 內容 |\n| --- | --- |\n'
|
||
printf '| 模型能力標籤 | %s |\n' "$gate_result"
|
||
printf '| 工作日誌 | %s |\n' "$worklog_cell"
|
||
[ -n "$branch_rows" ] && printf '%s\n' "$branch_rows"
|
||
printf '\n### 寫入的 wiki 頁\n\n'
|
||
if [ -n "$page_rows" ]; then
|
||
printf '| 頁面 | 連結 | 連結驗證 |\n| --- | --- | --- |\n'
|
||
printf '%s\n' "$page_rows" | while IFS="$TAB" read -r page url; do
|
||
[ -n "$page" ] || continue
|
||
url=${url:-}
|
||
printf '| %s | %s | %s |\n' "$page" "${url:-(取不到網址)}" "$(verify_cell "$url")"
|
||
done
|
||
else
|
||
printf '本階段沒有寫入任何 wiki 頁。\n'
|
||
fi
|
||
|
||
if [ "$warn" -eq 1 ] || [ -n "$link_note" ]; then
|
||
printf '\n### 警告\n\n'
|
||
[ -n "$worklog" ] || printf -- '- 這個階段還沒有寫入工作日誌,請自行檢查是不是漏了。%s\n' "$pending_note"
|
||
[ "$stage" = implement ] && [ -z "$pr_url" ] && printf -- '- 目標分支還沒有 PR,工作尚未交出去。\n'
|
||
[ -n "$link_note" ] && printf -- '- %s\n' "$link_note"
|
||
if [ -n "$dead_urls" ]; then
|
||
printf -- '- 下列連結連不到,請修好再貼給別人:\n'
|
||
printf '%s\n' "$dead_urls" | while IFS= read -r dead; do
|
||
[ -n "$dead" ] || continue
|
||
printf -- ' - %s\n' "$dead"
|
||
done
|
||
fi
|
||
[ "$warn" -eq 1 ] && exit 1
|
||
fi
|
||
exit 0
|