|
| 1 | +#!/usr/bin/env bash |
| 2 | +# 质量审计脚本 —— 跑 4 类检查防漂移 |
| 3 | +# |
| 4 | +# 1. 静态校验:JSON parse / SKILL.md frontmatter / symlink / hook 可执行性 |
| 5 | +# 2. Installer 功能:17 款工具装 / 卸载 / 幂等 |
| 6 | +# 3. 上游对齐:hooks 3 文件 + brainstorm scripts 3 文件 + 14 翻译 skill 结构层级 |
| 7 | +# 4. 交叉引用:README → docs/ 链接 + skill 间引用 + bootstrap 注入路径 |
| 8 | +# |
| 9 | +# 用法: |
| 10 | +# bash scripts/audit.sh # 跑全部,FAIL > 0 时 exit 1 |
| 11 | +# bash scripts/audit.sh --quick # 跳过 installer 功能测试 |
| 12 | +# bash scripts/audit.sh --no-upstream # 跳过上游对齐(CI 没 upstream remote 时) |
| 13 | +# |
| 14 | +# CI 默认在 PR + push to main 跑,发现漂移立刻拦下。 |
| 15 | + |
| 16 | +ROOT="$(cd "$(dirname "$0")/.." && pwd)" |
| 17 | +cd "$ROOT" |
| 18 | + |
| 19 | +QUICK=0 |
| 20 | +NO_UPSTREAM=0 |
| 21 | +for arg in "$@"; do |
| 22 | + case "$arg" in |
| 23 | + --quick) QUICK=1 ;; |
| 24 | + --no-upstream) NO_UPSTREAM=1 ;; |
| 25 | + esac |
| 26 | +done |
| 27 | + |
| 28 | +PASS=0; FAIL=0; WARN=0 |
| 29 | +declare -a FAILURES=() |
| 30 | +declare -a WARNINGS=() |
| 31 | +INSTALLER="$ROOT/bin/superpowers-zh.js" |
| 32 | + |
| 33 | +ok() { PASS=$((PASS+1)); } |
| 34 | +bad() { FAIL=$((FAIL+1)); FAILURES+=("$1"); echo " ❌ $1"; } |
| 35 | +warn() { WARN=$((WARN+1)); WARNINGS+=("$1"); echo " ⚠️ $1"; } |
| 36 | +hdr() { echo ""; echo "=== $1 ==="; } |
| 37 | + |
| 38 | +# 确保有 upstream remote(CI 上需要 fetch) |
| 39 | +ensure_upstream() { |
| 40 | + if [ "$NO_UPSTREAM" = "1" ]; then return 1; fi |
| 41 | + if ! git ls-remote --exit-code upstream HEAD >/dev/null 2>&1; then |
| 42 | + if git remote get-url upstream >/dev/null 2>&1; then |
| 43 | + git fetch upstream main --depth=50 --quiet 2>/dev/null || return 1 |
| 44 | + else |
| 45 | + git remote add upstream https://github.com/obra/superpowers.git 2>/dev/null |
| 46 | + git fetch upstream main --depth=50 --quiet 2>/dev/null || return 1 |
| 47 | + fi |
| 48 | + fi |
| 49 | + return 0 |
| 50 | +} |
| 51 | + |
| 52 | +#============================================================================== |
| 53 | +hdr "Category 1: 静态校验" |
| 54 | +#============================================================================== |
| 55 | + |
| 56 | +# 1a. JSON parse |
| 57 | +while IFS= read -r f; do |
| 58 | + if node -e "JSON.parse(require('fs').readFileSync('$f','utf8'))" 2>/dev/null; then |
| 59 | + ok |
| 60 | + else |
| 61 | + bad "JSON parse failure: $f" |
| 62 | + fi |
| 63 | +done < <(find . -name "*.json" \ |
| 64 | + -not -path "./node_modules/*" \ |
| 65 | + -not -path "./.git/*" \ |
| 66 | + -not -path "./tests/*/node_modules/*") |
| 67 | + |
| 68 | +# 1b. SKILL.md frontmatter 完整性 |
| 69 | +for f in skills/*/SKILL.md; do |
| 70 | + if ! head -1 "$f" | grep -q '^---$'; then |
| 71 | + bad "No frontmatter: $f" |
| 72 | + continue |
| 73 | + fi |
| 74 | + fm=$(sed -n '/^---$/,/^---$/p' "$f" | head -20) |
| 75 | + for field in name description; do |
| 76 | + if ! echo "$fm" | grep -q "^${field}:"; then |
| 77 | + bad "Missing frontmatter field '$field': $f" |
| 78 | + fi |
| 79 | + done |
| 80 | + ok |
| 81 | +done |
| 82 | + |
| 83 | +# 1c. Symlink 解析 |
| 84 | +while IFS= read -r l; do |
| 85 | + if [ -e "$l" ]; then ok; else bad "Broken symlink: $l"; fi |
| 86 | +done < <(find . -type l -not -path "./node_modules/*" -not -path "./.git/*") |
| 87 | + |
| 88 | +# 1d. Hook 脚本可执行权限 |
| 89 | +for f in hooks/session-start hooks/run-hook.cmd; do |
| 90 | + if [ -x "$f" ]; then ok; else bad "Not executable: $f"; fi |
| 91 | +done |
| 92 | + |
| 93 | +#============================================================================== |
| 94 | +if [ "$QUICK" != "1" ]; then |
| 95 | +hdr "Category 2: Installer 功能测试(17 款工具)" |
| 96 | +#============================================================================== |
| 97 | + |
| 98 | +declare -a TOOLS=(claude cursor codex kiro deerflow trae antigravity vscode openclaw windsurf gemini aider opencode qwen hermes claw copilot) |
| 99 | + |
| 100 | +for tool in "${TOOLS[@]}"; do |
| 101 | + TMP=$(mktemp -d) |
| 102 | + pushd "$TMP" >/dev/null |
| 103 | + |
| 104 | + if ! node "$INSTALLER" --tool "$tool" >/dev/null 2>&1; then |
| 105 | + bad "Installer: $tool 安装失败" |
| 106 | + popd >/dev/null |
| 107 | + rm -rf "$TMP" |
| 108 | + continue |
| 109 | + fi |
| 110 | + |
| 111 | + # 幂等:再装一遍不应炸 |
| 112 | + if ! node "$INSTALLER" --tool "$tool" >/dev/null 2>&1; then |
| 113 | + bad "Installer: $tool 二次安装失败(幂等性破坏)" |
| 114 | + popd >/dev/null |
| 115 | + rm -rf "$TMP" |
| 116 | + continue |
| 117 | + fi |
| 118 | + |
| 119 | + if ! node "$INSTALLER" --uninstall >/dev/null 2>&1; then |
| 120 | + bad "Installer: $tool 卸载失败" |
| 121 | + else |
| 122 | + ok |
| 123 | + fi |
| 124 | + |
| 125 | + popd >/dev/null |
| 126 | + rm -rf "$TMP" |
| 127 | +done |
| 128 | + |
| 129 | +else |
| 130 | +echo "" |
| 131 | +echo "[--quick 跳过 installer 功能测试]" |
| 132 | +fi |
| 133 | + |
| 134 | +#============================================================================== |
| 135 | +hdr "Category 3: 上游对齐" |
| 136 | +#============================================================================== |
| 137 | + |
| 138 | +if ! ensure_upstream; then |
| 139 | + warn "无法访问 upstream,跳过对齐检查(CI 上请确保有网络)" |
| 140 | +else |
| 141 | + # 3a. Hooks 3 文件 + cursor manifest |
| 142 | + for f in hooks/session-start hooks/hooks.json hooks/run-hook.cmd hooks/hooks-cursor.json; do |
| 143 | + d=$(diff <(git show upstream/main:$f 2>/dev/null) "$f" 2>/dev/null | wc -l | tr -d ' ') |
| 144 | + if [ "$d" = "0" ]; then ok; else bad "Hooks 漂移: $f ($d 行)"; fi |
| 145 | + done |
| 146 | + |
| 147 | + # 3b. Brainstorm scripts 3 文件 |
| 148 | + for f in skills/brainstorming/scripts/server.cjs \ |
| 149 | + skills/brainstorming/scripts/start-server.sh \ |
| 150 | + skills/brainstorming/scripts/stop-server.sh; do |
| 151 | + d=$(diff <(git show upstream/main:$f 2>/dev/null) "$f" 2>/dev/null | wc -l | tr -d ' ') |
| 152 | + if [ "$d" = "0" ]; then ok; else bad "Brainstorm script 漂移: $(basename $f) ($d 行)"; fi |
| 153 | + done |
| 154 | + |
| 155 | + # 3c. 14 翻译 skill 结构层级(H1-H4 标题数) |
| 156 | + declare -a SKILLS=(brainstorming dispatching-parallel-agents executing-plans \ |
| 157 | + finishing-a-development-branch receiving-code-review requesting-code-review \ |
| 158 | + subagent-driven-development systematic-debugging test-driven-development \ |
| 159 | + using-git-worktrees using-superpowers verification-before-completion \ |
| 160 | + writing-plans writing-skills) |
| 161 | + |
| 162 | + for s in "${SKILLS[@]}"; do |
| 163 | + up=$(git show upstream/main:skills/$s/SKILL.md 2>/dev/null | grep -cE '^#{1,4} ' || echo 0) |
| 164 | + our=$(grep -cE '^#{1,4} ' "skills/$s/SKILL.md" 2>/dev/null || echo 0) |
| 165 | + diff=$((up - our)) |
| 166 | + abs=${diff#-} |
| 167 | + # 允许 3 个 header 差异(翻译造成的合并/拆分小幅波动) |
| 168 | + if [ "$abs" -le "3" ]; then |
| 169 | + ok |
| 170 | + else |
| 171 | + warn "Skill 结构漂移: ${s} (上游 H=${up}, 我们 H=${our}) -- 可能 v5.1.0 没跟,或主动扩写" |
| 172 | + fi |
| 173 | + done |
| 174 | + |
| 175 | + # 3d. requesting-code-review/code-reviewer.md 结构(v5.1.0 self-contained) |
| 176 | + up=$(git show upstream/main:skills/requesting-code-review/code-reviewer.md 2>/dev/null | grep -cE '^#{1,3} ' || echo 0) |
| 177 | + our=$(grep -cE '^#{1,3} ' skills/requesting-code-review/code-reviewer.md) |
| 178 | + diff=$((up - our)) |
| 179 | + abs=${diff#-} |
| 180 | + if [ "$abs" -le "2" ]; then |
| 181 | + ok |
| 182 | + else |
| 183 | + bad "code-reviewer.md 结构漂移 (上游 v5.1.0 self-contained, H=${up}; 我们 H=${our})" |
| 184 | + fi |
| 185 | +fi |
| 186 | + |
| 187 | +#============================================================================== |
| 188 | +hdr "Category 4: 交叉引用完整性" |
| 189 | +#============================================================================== |
| 190 | + |
| 191 | +# 4a. README → docs/ 链接 |
| 192 | +BROKEN=0 |
| 193 | +while IFS= read -r link; do |
| 194 | + link=${link#(}; link=${link%)} |
| 195 | + if [ -f "$link" ]; then ok; else |
| 196 | + bad "README 链接断: $link" |
| 197 | + BROKEN=$((BROKEN+1)) |
| 198 | + fi |
| 199 | +done < <(grep -oE '\(docs/README\.[a-z-]+\.md\)' README.md) |
| 200 | + |
| 201 | +# 4b. Skill 间引用(superpowers:xxx) |
| 202 | +while IFS= read -r line; do |
| 203 | + skill_file=$(echo "$line" | cut -d: -f1) |
| 204 | + refs=$(echo "$line" | grep -oE '\bsuperpowers:[a-z-]+\b' | sort -u) |
| 205 | + for ref in $refs; do |
| 206 | + name=${ref#superpowers:} |
| 207 | + if [ -d "skills/$name" ]; then ok; else |
| 208 | + src=$(basename $(dirname "$skill_file")) |
| 209 | + bad "Skill 引用断: $src 引用了不存在的 skills/$name" |
| 210 | + fi |
| 211 | + done |
| 212 | +done < <(grep -rln 'superpowers:' skills/*/SKILL.md 2>/dev/null | \ |
| 213 | + xargs -I{} grep -H 'superpowers:' {} 2>/dev/null) |
| 214 | + |
| 215 | +# 4c. 装完后 .claude/skills/using-superpowers/SKILL.md 路径必须存在(hook 依赖) |
| 216 | +TMP=$(mktemp -d) |
| 217 | +pushd "$TMP" >/dev/null |
| 218 | +if node "$INSTALLER" --tool claude >/dev/null 2>&1; then |
| 219 | + if [ -f "$TMP/.claude/skills/using-superpowers/SKILL.md" ]; then |
| 220 | + ok |
| 221 | + else |
| 222 | + bad "装完后 .claude/skills/using-superpowers/SKILL.md 不存在(hook 会找不到)" |
| 223 | + fi |
| 224 | +fi |
| 225 | +popd >/dev/null |
| 226 | +rm -rf "$TMP" |
| 227 | + |
| 228 | +#============================================================================== |
| 229 | +echo "" |
| 230 | +echo "==========================================" |
| 231 | +echo "📊 审计结果" |
| 232 | +echo "==========================================" |
| 233 | +echo "✅ PASS: $PASS" |
| 234 | +echo "⚠️ WARN: $WARN" |
| 235 | +echo "❌ FAIL: $FAIL" |
| 236 | + |
| 237 | +if [ "$WARN" -gt 0 ]; then |
| 238 | + echo "" |
| 239 | + echo "Warnings(不阻塞):" |
| 240 | + for w in "${WARNINGS[@]}"; do echo " ⚠️ $w"; done |
| 241 | +fi |
| 242 | + |
| 243 | +if [ "$FAIL" -gt 0 ]; then |
| 244 | + echo "" |
| 245 | + echo "Failures(必须修):" |
| 246 | + for f in "${FAILURES[@]}"; do echo " ❌ $f"; done |
| 247 | + echo "" |
| 248 | + echo "❌ Audit 失败:$FAIL 个 P0 问题。看 README 「质量审计」段了解每项含义。" |
| 249 | + exit 1 |
| 250 | +fi |
| 251 | + |
| 252 | +echo "" |
| 253 | +echo "✅ Audit 通过" |
| 254 | +exit 0 |
0 commit comments