将项目级的 Ratchet/约定检测方法论融入 req-test-gate 技能, 通过 /req 流程三个节点自动触发(dev 环境检测、cr 约定建议、test Gate 0B), 无需手动记忆执行。 新增文档:harness-engineering.md、ratchet-pattern.md、convention-flow.md、 project-bootstrap.md 及 4 个模板(ratchet/convention 脚本、GATES.md、pre-commit)。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
190 lines
5.5 KiB
Markdown
190 lines
5.5 KiB
Markdown
# Ratchet Script Template
|
||
|
||
泛化的 ratchet 脚本模板,基于 new-ai-proj `check-architecture.sh` 提取。
|
||
|
||
## 使用方法
|
||
|
||
1. 复制下方脚本到 `scripts/check-{NAME}.sh`
|
||
2. 替换所有 `{PLACEHOLDER}` 为项目特定值
|
||
3. 运行 `chmod +x scripts/check-{NAME}.sh`
|
||
4. 运行 `./scripts/check-{NAME}.sh baseline` 记录首次基线
|
||
5. 提交脚本和基线文件
|
||
|
||
## 脚本模板
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
#
|
||
# {DESCRIPTION} Ratchet — ensures new code doesn't increase {WHAT} violations.
|
||
#
|
||
# Usage:
|
||
# ./scripts/check-{NAME}.sh check # CI / pre-push: fail if violations increased
|
||
# ./scripts/check-{NAME}.sh baseline # Record current counts (commit the result)
|
||
# ./scripts/check-{NAME}.sh report # Pretty-print current vs baseline
|
||
#
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
BASELINE_FILE="$SCRIPT_DIR/.{NAME}-baseline.json"
|
||
TARGET_DIR="$SCRIPT_DIR/../{TARGET_DIRECTORY}"
|
||
|
||
# ── rule definitions ────────────────────────────────────────────────
|
||
# Add/remove rules as needed. Each rule has:
|
||
# - A name (used as JSON key)
|
||
# - A type: "ratchet" (baseline can decrease) or "wall" (must be 0)
|
||
# - A count function
|
||
|
||
RULES=({RULE_NAMES}) # e.g. ("rule_one" "rule_two" "rule_three")
|
||
TYPES=({RULE_TYPES}) # e.g. ("ratchet" "ratchet" "wall")
|
||
|
||
# ── counting functions ──────────────────────────────────────────────
|
||
|
||
count_rule() {
|
||
local rule="$1"
|
||
case "$rule" in
|
||
{RULE_NAME_1})
|
||
# Example: count files in {DIR} that import {PACKAGE}
|
||
grep -rl '{PATTERN_1}' "$TARGET_DIR/{SUBDIR_1}/" 2>/dev/null | wc -l | tr -d ' '
|
||
;;
|
||
{RULE_NAME_2})
|
||
grep -rl '{PATTERN_2}' "$TARGET_DIR/{SUBDIR_2}/" 2>/dev/null | wc -l | tr -d ' '
|
||
;;
|
||
# Add more rules...
|
||
*)
|
||
echo "0"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ── helpers ─────────────────────────────────────────────────────────
|
||
|
||
current_counts() {
|
||
echo "{"
|
||
local first=true
|
||
for rule in "${RULES[@]}"; do
|
||
local count
|
||
count=$(count_rule "$rule")
|
||
$first || echo ","
|
||
printf ' "%s": %s' "$rule" "${count:-0}"
|
||
first=false
|
||
done
|
||
echo ""
|
||
echo "}"
|
||
}
|
||
|
||
json_val() {
|
||
local key="$1" file="$2"
|
||
grep "\"${key}\"" "$file" | head -1 | sed 's/[^0-9]//g'
|
||
}
|
||
|
||
json_val_str() {
|
||
local key="$1" json="$2"
|
||
echo "$json" | grep "\"${key}\"" | head -1 | sed 's/[^0-9]//g'
|
||
}
|
||
|
||
# ── commands ────────────────────────────────────────────────────────
|
||
|
||
cmd_baseline() {
|
||
echo "Recording {NAME} baseline..."
|
||
current_counts > "$BASELINE_FILE"
|
||
echo "Baseline written to $BASELINE_FILE"
|
||
cat "$BASELINE_FILE"
|
||
}
|
||
|
||
cmd_report() {
|
||
if [[ ! -f "$BASELINE_FILE" ]]; then
|
||
echo "No baseline found. Run: $0 baseline"
|
||
exit 1
|
||
fi
|
||
|
||
local current
|
||
current=$(current_counts)
|
||
|
||
printf "\n%-35s %10s %10s %8s\n" "Rule" "Baseline" "Current" "Status"
|
||
printf "%-35s %10s %10s %8s\n" "---" "---" "---" "---"
|
||
|
||
for i in "${!RULES[@]}"; do
|
||
local rule="${RULES[$i]}"
|
||
local type="${TYPES[$i]}"
|
||
local base cur status
|
||
|
||
base=$(json_val "$rule" "$BASELINE_FILE")
|
||
cur=$(json_val_str "$rule" "$current")
|
||
|
||
if [[ "$type" == "wall" ]]; then
|
||
[[ "$cur" -eq 0 ]] && status="PASS" || status="FAIL"
|
||
else
|
||
[[ "$cur" -le "$base" ]] && status="PASS" || status="FAIL"
|
||
fi
|
||
|
||
printf "%-35s %10d %10d %8s\n" "$rule" "$base" "$cur" "$status"
|
||
done
|
||
echo ""
|
||
}
|
||
|
||
cmd_check() {
|
||
if [[ ! -f "$BASELINE_FILE" ]]; then
|
||
echo "ERROR: No baseline found. Run: $0 baseline"
|
||
exit 1
|
||
fi
|
||
|
||
local current
|
||
current=$(current_counts)
|
||
local failed=0
|
||
|
||
for i in "${!RULES[@]}"; do
|
||
local rule="${RULES[$i]}"
|
||
local type="${TYPES[$i]}"
|
||
local base cur
|
||
|
||
base=$(json_val "$rule" "$BASELINE_FILE")
|
||
cur=$(json_val_str "$rule" "$current")
|
||
|
||
if [[ "$type" == "wall" ]]; then
|
||
if [[ "$cur" -ne 0 ]]; then
|
||
echo "FAIL [hard wall] $rule: expected 0, got $cur"
|
||
failed=1
|
||
fi
|
||
else
|
||
if [[ "$cur" -gt "$base" ]]; then
|
||
echo "FAIL [ratchet] $rule: baseline=$base, current=$cur (+$((cur - base)))"
|
||
failed=1
|
||
fi
|
||
fi
|
||
done
|
||
|
||
if [[ "$failed" -eq 1 ]]; then
|
||
echo ""
|
||
echo "{NAME} check FAILED. New violations detected."
|
||
echo "If this is intentional, update the baseline: $0 baseline"
|
||
exit 1
|
||
fi
|
||
|
||
echo "{NAME} check PASSED."
|
||
}
|
||
|
||
# ── main ────────────────────────────────────────────────────────────
|
||
|
||
case "${1:-check}" in
|
||
baseline) cmd_baseline ;;
|
||
report) cmd_report ;;
|
||
check) cmd_check ;;
|
||
*)
|
||
echo "Usage: $0 {check|baseline|report}"
|
||
exit 1
|
||
;;
|
||
esac
|
||
```
|
||
|
||
## Placeholder 说明
|
||
|
||
| Placeholder | 含义 | 示例 |
|
||
|-------------|------|------|
|
||
| `{NAME}` | 脚本短名 | `architecture`, `import-rules` |
|
||
| `{DESCRIPTION}` | 一句话描述 | `Architecture`, `Import Boundary` |
|
||
| `{TARGET_DIRECTORY}` | 扫描目标目录(相对于 scripts/) | `backend`, `frontend/src` |
|
||
| `{RULE_NAMES}` | Bash 数组,规则名列表 | `"handler_db" "route_db"` |
|
||
| `{RULE_TYPES}` | Bash 数组,规则类型列表 | `"ratchet" "wall"` |
|
||
| `{PATTERN_N}` | grep 匹配模式 | `"my-pkg/database"` |
|
||
| `{SUBDIR_N}` | 目标子目录 | `handlers/`, `routes/` |
|