Files
git-learn/scripts/check-dev-flow.sh
zhuyongxin 24a71e78f1 Harden dev-flow: pre-authorization marker, alternatives log, check script
- Record rejected answers in decision.md at the moment they are rejected

- Allow explicit pre-authorized Build via a pre-authorized marker line

- Land scripts/check-dev-flow.sh and derive devflow/index.md from archives

- Add dev-flow process artifacts (decisions, prd) for open changes
2026-09-18 18:23:18 +08:00

142 lines
5.1 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# dev-flow 收尾检查 + index 派生
# 检查模式: check-dev-flow.sh <slug> → 对 openspec/changes/<slug>/ 执行三项收尾检查
# 索引模式: check-dev-flow.sh --index → 扫描 openspec/changes/archive/ 与 openspec/archive/ 生成 devflow/index.md
# 全量模式: check-dev-flow.sh --all → 对所有活跃 change 执行检查
#
# 三项收尾检查:
# 1. decision.md 存在(豁免需显式: decision.md 或提交信息中写明)
# 2. 含 ## Alternatives considered 或显式 <!-- alternatives-not-recorded -->
# 3. ## Verification 无"已确认 X"类不可重跑结论
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CHANGES_ROOT="$PROJECT_ROOT/openspec/changes"
fail() { echo "✗ $1" >&2; exit 1; }
check_change() {
local slug="$1"
local dir="$CHANGES_ROOT/$slug"
local decision="$dir/decision.md"
local errors=0
[ -d "$dir" ] || fail "change 目录不存在: openspec/changes/$slug"
echo "== dev-flow 收尾检查: $slug =="
# 1. decision.md 存在
if [ ! -f "$decision" ]; then
echo "✗ [1] 缺少 openspec/changes/$slug/decision.md (纯机械改动可豁免,但豁免必须显式写在提交信息里)"
errors=$((errors+1))
else
echo "✓ [1] decision.md 存在"
# 2. Alternatives 强制
if grep -q '^## Alternatives considered' "$decision" || grep -q '<!-- alternatives-not-recorded -->' "$decision"; then
echo "✓ [2] 备选已记录或显式声明未记录"
else
echo "✗ [2] decision.md 缺少 ## Alternatives considered,也没有 <!-- alternatives-not-recorded --> 显式豁免"
errors=$((errors+1))
fi
# 3. Verification 无不可重跑结论
local vtext
vtext="$(awk '/^## Verification/{flag=1;next} /^## /{flag=0} flag' "$decision")"
if echo "$vtext" | grep -qE '已确认|已验证[^,。;(]|测试通过$'; then
echo "✗ [3] ## Verification 含不可重跑的结论(如\"已确认 X 存在\")——改为可重跑的命令或明确的人工步骤"
errors=$((errors+1))
elif [ -z "$vtext" ]; then
echo "✗ [3] 缺少 ## Verification 小节"
errors=$((errors+1))
else
echo "✓ [3] Verification 通过"
fi
fi
if [ "$errors" -gt 0 ]; then
echo "== $slug: $errors 项未通过 =="
return 1
fi
echo "== $slug: 全部通过 =="
return 0
}
gen_index() {
local out="$PROJECT_ROOT/devflow/index.md"
echo "# devflow 索引" > "$out"
echo "" >> "$out"
echo "> 本文件由 scripts/check-dev-flow.sh --index 派生生成,**不要手写**。" >> "$out"
echo "> 存储层是 openspec/archive/ 下的归档目录;此处只是浏览视图。" >> "$out"
echo "" >> "$out"
local found=0
local rows=()
local arch_dir
# 两个历史归档位置都扫:openspec/changes/archive/ 与 openspec/archive/
for arch_dir in "$CHANGES_ROOT/archive" "$PROJECT_ROOT/openspec/archive"; do
[ -d "$arch_dir" ] || continue
local d
while IFS= read -r d; do
d="${d%/}"
[ -d "$d" ] || continue
local name; name="$(basename "$d")"
local title; title=""
local date; date=""
local rel; rel="${d#"$PROJECT_ROOT"/}"
# 标题取 decision.md 首行,否则 proposal.md 首个 # 标题
if [ -f "$d/decision.md" ]; then
title="$(head -1 "$d/decision.md" | sed 's/^# *//; s/^[[:space:]]*//; s/[[:space:]]*$//' || true)"
elif [ -f "$d/proposal.md" ]; then
title="$(grep -m1 '^# ' "$d/proposal.md" | sed 's/^# *//' || true)"
fi
# 日期取目录名前缀 YYYY-MM-DD
if [[ "$name" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2}) ]]; then
date="${BASH_REMATCH[1]}"
fi
rows+=("| ${date:-—} | ${title:-$name} | ${rel} |")
found=$((found+1))
done < <(find "$arch_dir" -mindepth 1 -maxdepth 1 -type d | sort)
done
# 写表格(带表头)
{
echo "| 日期 | 标题 | 归档位置 |"
echo "|---|---|---|"
if [ ${#rows[@]} -gt 0 ]; then
printf '%s\n' "${rows[@]}"
fi
} >> "$out"
echo "[index] 生成 $found 个归档条目 → devflow/index.md"
}
case "${1:-}" in
--index)
gen_index
;;
--all)
rc=0
for d in "$CHANGES_ROOT"/*/; do
d="${d%/}"
[ -d "$d" ] || continue
name="$(basename "$d")"
[ "$name" = "archive" ] && continue
check_change "$name" || rc=1
done
exit $rc
;;
"")
echo "用法: check-dev-flow.sh <slug> | --all | --index" >&2
echo " <slug> 对单个活跃 change 执行三项收尾检查" >&2
echo " --all 检查所有活跃 change" >&2
echo " --index 扫描归档生成 devflow/index.md" >&2
exit 2
;;
*)
check_change "$1"
;;
esac