#!/usr/bin/env bash set -euo pipefail # dev-flow 收尾检查 + index 派生 # 检查模式: check-dev-flow.sh → 对 openspec/changes// 执行三项收尾检查 # 索引模式: 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 或显式 # 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 '' "$decision"; then echo "✓ [2] 备选已记录或显式声明未记录" else echo "✗ [2] decision.md 缺少 ## Alternatives considered,也没有 显式豁免" 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 | --all | --index" >&2 echo " 对单个活跃 change 执行三项收尾检查" >&2 echo " --all 检查所有活跃 change" >&2 echo " --index 扫描归档生成 devflow/index.md" >&2 exit 2 ;; *) check_change "$1" ;; esac