Files
sdlc/tools/stage-report.sh
T
jiantw83 f4e489ceb4 feat(link): 連結一律寫成 [文字](絕對網址),寫入前先驗證連得到
取消 [[頁名]] 與 [[顯示文字|頁名]] 兩種同 wiki 寫法,不再分「同存取庫」與
「跨存取庫」兩條規則。那種寫法只在自己那個 wiki 內解析,寫錯不報錯,畫面上
看起來像普通文字或死連結,巡不到也修不了。

連結寫進頁面前先過 jsc-gitea 的 link-check.sh,結束碼 0 才寫。驗證一律走 API,
不看網頁狀態碼:私有存取庫的網頁網址對未登入請求一律回 404,拿狀態碼判會把
好連結判成壞的。認證失敗回 7,與死連結的 1 分開,免得金鑰一過期就把還在的頁
整批判死。
2026-09-02 14:27:18 +08:00

353 lines
14 KiB
Bash
Executable File
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.
#!/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