feat(req-test-gate): 集成 Harness Engineering 工程约束方法论

将项目级的 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>
This commit is contained in:
2026-03-26 11:34:42 +10:30
parent e3924e6b2b
commit b9c808cce0
13 changed files with 1333 additions and 14 deletions

View File

@@ -0,0 +1,120 @@
# Convention Detection Script Template
泛化的约定检测脚本模板,基于 new-ai-proj `check-modal-safety.sh` 提取。用于检测代码中违反编码约定的模式。
## 使用方法
1. 复制下方脚本到 `scripts/check-{NAME}.sh`
2. 替换所有 `{PLACEHOLDER}` 为项目特定值
3. 运行 `chmod +x scripts/check-{NAME}.sh`
4. 验证:`./scripts/check-{NAME}.sh` 输出 PASSED
## 脚本模板
```bash
#!/usr/bin/env bash
#
# {CONVENTION_NAME} Check — detects {WHAT_IT_DETECTS} in {TARGET_SCOPE}.
#
# {PROBLEM_DESCRIPTION}
#
# Usage:
# ./scripts/check-{NAME}.sh # Check and report
# ./scripts/check-{NAME}.sh --ci # Exit code 1 on violations
#
set -euo pipefail
TARGET_DIR="$(cd "$(dirname "$0")/../{TARGET_DIRECTORY}" && pwd)"
CI_MODE=false
[[ "${1:-}" == "--ci" ]] && CI_MODE=true
violations=0
# ── detection logic ─────────────────────────────────────────────────
# Strategy A: Simple grep pattern (file-level)
# Find files matching a bad pattern, optionally excluding false positives.
#
# while IFS= read -r file; do
# rel_path="${file#"$TARGET_DIR"/}"
# echo "WARNING: $rel_path — {VIOLATION_MESSAGE}"
# violations=$((violations + 1))
# done < <(grep -rl '{BAD_PATTERN}' "$TARGET_DIR" --include='{FILE_GLOB}' 2>/dev/null \
# | while read f; do grep -L '{FALSE_POSITIVE_PATTERN}' "$f" 2>/dev/null; done)
# Strategy B: Line-level with context (multi-condition)
# Find a primary pattern, check nearby lines for secondary pattern.
#
# while IFS= read -r file; do
# while IFS= read -r line_num; do
# # Check context around the match
# has_mitigation=$(sed -n "${line_num},$((line_num + {CONTEXT_LINES}))p" "$file" \
# | grep -c '{MITIGATION_PATTERN}' || true)
# if [[ "$has_mitigation" -eq 0 ]]; then
# # Additional condition: check if problematic follow-up exists
# after_end=$((line_num + {LOOKAHEAD_LINES}))
# has_problem=$(sed -n "$((line_num + 1)),${after_end}p" "$file" \
# | grep -cE '{FOLLOW_UP_BAD_PATTERN}' || true)
# if [[ "$has_problem" -gt 0 ]]; then
# rel_path="${file#"$TARGET_DIR"/}"
# echo "WARNING: $rel_path:$line_num — {VIOLATION_MESSAGE}"
# violations=$((violations + 1))
# fi
# fi
# done < <(grep -n '{PRIMARY_PATTERN}' "$file" | cut -d: -f1)
# done < <(grep -rl '{PRIMARY_PATTERN}' "$TARGET_DIR" --include='{FILE_GLOB}' 2>/dev/null)
# ── results ─────────────────────────────────────────────────────────
if [[ "$violations" -gt 0 ]]; then
echo ""
echo "Found $violations {CONVENTION_NAME} violation(s)."
echo "Fix: {FIX_GUIDANCE}"
echo "Docs: see CLAUDE.md '{CLAUDE_MD_SECTION}'"
$CI_MODE && exit 1
else
echo "{CONVENTION_NAME} check PASSED."
fi
```
## Placeholder 说明
| Placeholder | 含义 | 示例 |
|-------------|------|------|
| `{NAME}` | 脚本短名 | `modal-safety`, `sql-injection` |
| `{CONVENTION_NAME}` | 约定显示名 | `Modal safety`, `SQL injection prevention` |
| `{WHAT_IT_DETECTS}` | 检测内容 | `potential modal overlap patterns` |
| `{TARGET_SCOPE}` | 目标范围描述 | `frontend code`, `Go handlers` |
| `{TARGET_DIRECTORY}` | 扫描目录 | `frontend/src`, `backend` |
| `{PROBLEM_DESCRIPTION}` | 问题描述 | `Ant Design Modal.success is non-blocking...` |
| `{FILE_GLOB}` | 文件匹配 | `*.tsx`, `*.go` |
| `{PRIMARY_PATTERN}` | 主匹配模式 | `Modal\.\(success\|info\)` |
| `{MITIGATION_PATTERN}` | 缓解模式(有则排除) | `onOk` |
| `{FOLLOW_UP_BAD_PATTERN}` | 后续违规模式 | `set\w*Open\(true\)` |
| `{CONTEXT_LINES}` | 上下文行数 | `25` |
| `{LOOKAHEAD_LINES}` | 前瞻行数 | `20` |
| `{BAD_PATTERN}` | Strategy A 主匹配模式 | `console\.log` |
| `{FALSE_POSITIVE_PATTERN}` | Strategy A 排除模式(匹配到则非违规) | `// eslint-disable` |
| `{VIOLATION_MESSAGE}` | 违规消息 | `Modal without onOk followed by setState` |
| `{FIX_GUIDANCE}` | 修复指导 | `add onOk callback to Modal` |
| `{CLAUDE_MD_SECTION}` | CLAUDE.md 章节名 | `Frontend: Modal 安全规则` |
## 两种检测策略
### Strategy A: 文件级检测
适用于:整个文件不应出现某模式(或出现即违规)。
例子:
- `console.log` 在生产代码中
- `TODO` / `FIXME` 计数
- 缺少 license header
### Strategy B: 行级 + 上下文检测
适用于:模式本身不违规,需要检查周围上下文。
例子:
- `Modal.success()` 不违规,但后面紧跟 `setState` 才违规
- `db.Exec()` 不违规,但参数用字符串拼接才违规
- `fmt.Println()` 不违规,但在 handler 中输出敏感数据才违规

View File

@@ -0,0 +1,102 @@
# GATES.md Template
项目质量门禁文档模板。放置于 `docs/quality/GATES.md`
## 使用方法
1. 复制下方模板到项目的 `docs/quality/GATES.md`
2. 替换 `{PLACEHOLDER}` 为项目特定值
3. 根据项目实际情况增删 Gate
## 模板
```markdown
# Quality Gates
Quality gates are automated checks that code must pass before it can be merged or deployed.
## Gate Overview
| Gate | Name | Where | What it catches |
|------|------|-------|-----------------|
| 0 | Pre-commit | Local (husky) | Formatting, lint errors |
| 1 | CI Lint | {CI_SYSTEM} | {LINT_TOOLS} |
| 2 | Convention & Architecture | {CI_SYSTEM} | New violations of established rules |
| 3 | Unit Tests | CI / local | Logic bugs, regressions |
| 4 | Security Scan | CI | Dependency vulnerabilities |
| 5 | Build | CI | Compilation errors |
## Gate 0: Pre-commit (Local)
Runs automatically on every `git commit` via husky + lint-staged.
{FRONTEND_SECTION — include if applicable}
**Frontend** (`{FRONTEND_GLOBS}`):
- {LINT_TOOL} with {LINT_FLAGS}
- {FORMATTER}
{BACKEND_SECTION — include if applicable}
**Backend** (`{BACKEND_GLOBS}`):
- {FORMAT_TOOL} (auto-format)
- {ANALYSIS_TOOL} (static analysis)
**Bypass** (emergency only): `git commit --no-verify`
## Gate 1: CI Lint
Runs on pull requests via {CI_SYSTEM}.
- **Backend**: {BACKEND_LINT_COMMANDS}
- **Frontend**: {FRONTEND_LINT_COMMANDS}
## Gate 2: Convention & Architecture Checks
Runs on pull requests via {CI_SYSTEM}.
{LIST_EACH_CHECK_SCRIPT}
- `./scripts/check-{name}.sh check` — {description}
See project CLAUDE.md and docs/architecture/ for rule details.
## Gates 3-5: Testing, Security, Build
These gates are enforced via the `/req test` workflow:
1. **Gate 3**: Unit test generation and execution
2. **Gate 4**: Security regression scan
3. **Gate 5**: E2E smoke test + final report
See the dev-test skill documentation for details.
## Local Commands
\`\`\`bash
# Gate 0: Format & Lint
{LOCAL_LINT_COMMANDS}
# Gate 2: Convention checks
{LOCAL_CHECK_COMMANDS}
# Gate 3: Unit tests
{LOCAL_TEST_COMMANDS}
\`\`\`
```
## Placeholder 说明
| Placeholder | 含义 | 示例 |
|-------------|------|------|
| `{CI_SYSTEM}` | CI 系统名称 | `Gitea Actions`, `GitHub Actions` |
| `{LINT_TOOLS}` | lint 工具列表 | `golangci-lint, ESLint, TypeScript errors` |
| `{FRONTEND_GLOBS}` | 前端文件 glob | `*.ts, *.tsx`, `*.vue` |
| `{BACKEND_GLOBS}` | 后端文件 glob | `*.go` |
| `{LINT_TOOL}` | lint 工具 | `ESLint` |
| `{LINT_FLAGS}` | lint 参数 | `--fix --max-warnings 0` |
| `{FORMATTER}` | 格式化工具 | `Prettier`, `gofmt` |
## 注意事项
- **此文档的 Gate 编号是项目级的**(描述项目基础设施层级),与 req-test-gate 技能的 Gate 0-5需求级测试门禁是两套独立体系
- Gate 0-2 由 harness engineering 方法论管理pre-commit → CI lint → ratchet/约定检测)
- Gate 3-5 由 `/req test` 流程管理(单元测试 → 安全扫描 → 构建验证)
- 两者互补,不要混淆编号

View File

@@ -0,0 +1,135 @@
# Pre-commit Configuration Template
husky + lint-staged 配置模板三种变体Node.js 项目、Go 项目、Monorepo。
---
## 变体 1: Node.js 项目React / Vue / 纯 TS
### 安装
```bash
npm install -D husky lint-staged prettier eslint
npx husky init
echo "npx lint-staged" > .husky/pre-commit
```
### package.json
```json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix --max-warnings 0",
"prettier --write"
],
"*.{css,less,scss}": [
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
```
---
## 变体 2: Go 项目
### 安装
```bash
# Go 项目需要 package.json 来驱动 husky
npm init -y
npm install -D husky lint-staged
npx husky init
echo "npx lint-staged" > .husky/pre-commit
```
### package.json
```json
{
"lint-staged": {
"*.go": [
"gofmt -s -w",
"bash -c 'go vet ./...'"
]
}
}
```
### 可选golangci-lint更严格
```json
{
"lint-staged": {
"*.go": [
"gofmt -s -w",
"bash -c 'go vet ./...'",
"bash -c 'golangci-lint run --fix'"
]
}
}
```
---
## 变体 3: MonorepoGo + Node.js
### 安装(根目录)
```bash
npm install -D husky lint-staged
npx husky init
echo "npx lint-staged" > .husky/pre-commit
```
### package.json根目录
```json
{
"lint-staged": {
"frontend/**/*.{ts,tsx}": [
"bash -c 'cd frontend && npx eslint --fix --max-warnings 0'",
"bash -c 'cd frontend && npx prettier --write'"
],
"frontend/**/*.{css,less,json}": [
"bash -c 'cd frontend && npx prettier --write'"
],
"backend/**/*.go": [
"gofmt -s -w",
"bash -c 'cd backend && go vet ./...'"
]
}
}
```
### 注意事项
- `bash -c 'cd xxx && ...'` 确保命令在正确的子目录中执行
- lint-staged 传递的文件路径是相对于根目录的,部分工具(如 `go vet`)需要在子目录执行
- 如果前端和后端各自有 `package.json`lint-staged 仍然在根目录配置
---
## 验证配置
```bash
# 修改一个文件后测试 pre-commit hook
echo "// test" >> some-file.ts
git add some-file.ts
git commit -m "test: verify pre-commit hook"
# 应该看到 lint-staged 执行 ESLint + Prettier
# 如果有 lint 错误commit 会被阻止
```
## 常见问题
| 问题 | 原因 | 解决 |
|------|------|------|
| lint-staged 不执行 | `.husky/pre-commit` 内容不对 | 确认内容为 `npx lint-staged` |
| ESLint 报错太多 | 旧项目首次启用 | 先 `npx eslint --fix` 全量修复,再启用 hook |
| gofmt 修改后 commit 内容不一致 | lint-staged 自动 format 后需要 re-stage | lint-staged 自动处理,无需手动 |
| Monorepo 路径错误 | lint-staged 传递根目录相对路径 | 使用 `bash -c 'cd sub && ...'` 包装 |

View File

@@ -0,0 +1,189 @@
# 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/` |