specline 1.3.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -125
- package/adapters/claude/deploy.json +12 -0
- package/adapters/claude/hooks/hooks.json +12 -0
- package/adapters/claude/hooks.json +12 -0
- package/adapters/claude/orchestration.md +17 -0
- package/adapters/codex/agent.toml.hbs +7 -0
- package/adapters/codex/deploy.json +12 -0
- package/adapters/codex/hooks.json +12 -0
- package/adapters/codex/orchestration.md +18 -0
- package/adapters/cursor/deploy.json +12 -0
- package/adapters/cursor/hooks.json +9 -0
- package/adapters/cursor/orchestration.md +17 -0
- package/adapters/opencode/deploy.json +12 -0
- package/adapters/opencode/orchestration.md +18 -0
- package/adapters/opencode/plugin.js +10 -0
- package/cli.mjs +161 -558
- package/core/agents/specline-backend-dev.yaml +45 -0
- package/core/agents/specline-code-reviewer.yaml +67 -0
- package/core/agents/specline-config-dev.yaml +50 -0
- package/core/agents/specline-config-reviewer.yaml +70 -0
- package/core/agents/specline-explore-assistant.yaml +79 -0
- package/core/agents/specline-frontend-dev.yaml +45 -0
- package/core/agents/specline-spec-creator.yaml +58 -0
- package/core/agents/specline-spec-reviewer.yaml +58 -0
- package/core/agents/specline-test-runner.yaml +62 -0
- package/core/agents/specline-test-writer.yaml +67 -0
- package/core/bootstrap/using-specline.md +14 -0
- package/core/gates/pipeline-gate-checks/a1-covers-ref.sh +125 -0
- package/core/gates/pipeline-gate-checks/a2-a3-reverse.sh +171 -0
- package/core/gates/pipeline-gate-checks/c1-exception.sh +71 -0
- package/core/gates/pipeline-gate-checks/c2-vague.sh +60 -0
- package/core/gates/pipeline-gate-checks/common.sh +68 -0
- package/core/gates/pipeline-gate-checks/d1-cycle.sh +149 -0
- package/core/gates/pipeline-gate-checks/d3-type-file.sh +260 -0
- package/core/gates/pipeline-gate.sh +1456 -0
- package/core/hooks/session-start.sh +259 -0
- package/core/skills/specline-apply-change/SKILL.md +197 -0
- package/core/skills/specline-archive-change/SKILL.md +173 -0
- package/core/skills/specline-explore/SKILL.md +504 -0
- package/core/skills/specline-knowledge/SKILL.md +539 -0
- package/core/skills/specline-pipeline/SKILL.md +604 -0
- package/core/skills/specline-pipeline/references/error-recovery-details.md +49 -0
- package/core/skills/specline-pipeline/references/event-log-spec.md +59 -0
- package/core/skills/specline-pipeline/references/pipeline-state-schema.md +87 -0
- package/core/skills/specline-pipeline/templates/subagent-prompts.md +397 -0
- package/core/skills/specline-propose/SKILL.md +186 -0
- package/core/skills/specline-quickfix/SKILL.md +289 -0
- package/core/templates/AGENTS.md.hbs +5 -0
- package/core/templates/specline/config.yaml +15 -0
- package/lib/deploy-claude.mjs +80 -0
- package/lib/deploy-codex.mjs +77 -0
- package/lib/deploy-opencode.mjs +93 -0
- package/lib/deploy.mjs +668 -0
- package/lib/gate.mjs +103 -0
- package/lib/hash.mjs +13 -0
- package/lib/hook.mjs +105 -0
- package/lib/init.mjs +122 -0
- package/lib/lock.mjs +99 -0
- package/lib/merge.mjs +184 -0
- package/lib/paths.mjs +40 -0
- package/lib/platforms.mjs +74 -0
- package/lib/render-agents.mjs +88 -0
- package/lib/render.mjs +126 -0
- package/lib/sync.mjs +253 -0
- package/lib/tty-select.mjs +89 -0
- package/package.json +4 -1
- package/templates/.cursor/README.md +18 -0
- package/templates/.cursor/agents/specline-code-reviewer.md +63 -4
- package/templates/.cursor/agents/specline-spec-creator.md +120 -1
- package/templates/.cursor/agents/specline-spec-reviewer.md +21 -2
- package/templates/.cursor/agents/specline-test-runner.md +10 -1
- package/templates/.cursor/agents/specline-test-writer.md +58 -7
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a2-a3-reverse.sh +1 -1
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +118 -0
- package/templates/.cursor/skills/specline-apply-change/SKILL.md +26 -0
- package/templates/.cursor/skills/specline-archive-change/SKILL.md +24 -0
- package/templates/.cursor/skills/specline-explore/SKILL.md +17 -0
- package/templates/.cursor/skills/specline-knowledge/SKILL.md +539 -0
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +102 -3
- package/templates/.cursor/skills/specline-pipeline/templates/subagent-prompts.md +32 -0
- package/templates/.cursor/skills/specline-propose/SKILL.md +34 -3
- package/templates/.cursor/skills/specline-quickfix/SKILL.md +26 -0
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# specline-pipeline-gate.sh — 确定性门禁脚本(零 LLM 参与)
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# specline-pipeline-gate.sh <phase> --change <change-name>
|
|
7
|
+
#
|
|
8
|
+
# Phases:
|
|
9
|
+
# new | list | artifacts | spec | semantic | build | lint | test-unit | test-integration | test-e2e | detect-modules | bind | archive | status
|
|
10
|
+
#
|
|
11
|
+
# Exit codes:
|
|
12
|
+
# 0 = 通过
|
|
13
|
+
# 1 = 失败
|
|
14
|
+
# 2 = 输入参数错误
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
PHASE="${1:-}"
|
|
19
|
+
CHANGE=""
|
|
20
|
+
EXECUTE_ARCHIVE=""
|
|
21
|
+
POSITIONAL_ARGS=()
|
|
22
|
+
|
|
23
|
+
# 遍历所有参数,不依赖位置
|
|
24
|
+
shift # 跳过 PHASE
|
|
25
|
+
while [ $# -gt 0 ]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--change)
|
|
28
|
+
CHANGE="$2"
|
|
29
|
+
shift 2
|
|
30
|
+
;;
|
|
31
|
+
--execute)
|
|
32
|
+
EXECUTE_ARCHIVE="--execute"
|
|
33
|
+
shift
|
|
34
|
+
;;
|
|
35
|
+
*)
|
|
36
|
+
POSITIONAL_ARGS+=("$1")
|
|
37
|
+
shift
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
done
|
|
41
|
+
# ===== 项目根目录 =====
|
|
42
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
43
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
44
|
+
|
|
45
|
+
# ===== 状态文件 =====
|
|
46
|
+
if [ -n "$CHANGE" ]; then
|
|
47
|
+
STATE_FILE="$PROJECT_ROOT/specline/changes/$CHANGE/.pipeline-state.json"
|
|
48
|
+
else
|
|
49
|
+
STATE_FILE=""
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ===== 辅助函数 =====
|
|
53
|
+
now_iso8601() {
|
|
54
|
+
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
write_gate_passed() {
|
|
58
|
+
local gate_path="$1" # e.g., "phases.spec.gates.spec_gate"
|
|
59
|
+
if [ -n "$STATE_FILE" ] && [ -f "$STATE_FILE" ]; then
|
|
60
|
+
local time
|
|
61
|
+
time=$(now_iso8601)
|
|
62
|
+
jq --arg time "$time" \
|
|
63
|
+
".updated_at = \$time | .${gate_path} = { \"passed\": true, \"run_at\": \$time }" \
|
|
64
|
+
"$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
|
65
|
+
fi
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fail() {
|
|
69
|
+
echo "❌ $1" >&2
|
|
70
|
+
exit 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pass() {
|
|
74
|
+
echo "✅ $1"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ===== 获取 Spec 文件路径 =====
|
|
78
|
+
find_spec_file() {
|
|
79
|
+
if [ -z "$CHANGE" ]; then
|
|
80
|
+
echo ""
|
|
81
|
+
return
|
|
82
|
+
fi
|
|
83
|
+
find "$PROJECT_ROOT/specline/changes/$CHANGE/specs" -name "spec.md" 2>/dev/null | head -1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
MODULES_JSON=""
|
|
87
|
+
|
|
88
|
+
detect_project_modules() {
|
|
89
|
+
# 扫描 maxdepth 2 的语言标记文件,输出 JSON 数组
|
|
90
|
+
# 格式: [{"path":"backend/","language":"go"},{"path":"frontend/","language":"typescript"}]
|
|
91
|
+
# 排除: node_modules/, .git/, vendor/, dist/
|
|
92
|
+
|
|
93
|
+
local modules='[]'
|
|
94
|
+
|
|
95
|
+
while IFS= read -r marker; do
|
|
96
|
+
[ -z "$marker" ] && continue
|
|
97
|
+
local dir
|
|
98
|
+
dir=$(dirname "$marker")
|
|
99
|
+
local rel_dir="${dir#$PROJECT_ROOT/}"
|
|
100
|
+
[ "$rel_dir" = "$dir" ] && rel_dir="."
|
|
101
|
+
[ "$rel_dir" != "." ] && rel_dir="${rel_dir}/"
|
|
102
|
+
|
|
103
|
+
case "$(basename "$marker")" in
|
|
104
|
+
go.mod)
|
|
105
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"go"}]')
|
|
106
|
+
;;
|
|
107
|
+
Cargo.toml)
|
|
108
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"rust"}]')
|
|
109
|
+
;;
|
|
110
|
+
pyproject.toml|setup.cfg|requirements.txt)
|
|
111
|
+
local exists
|
|
112
|
+
exists=$(echo "$modules" | jq --arg p "$rel_dir" '[.[] | select(.path == $p and .language == "python")] | length')
|
|
113
|
+
if [ "$exists" = "0" ]; then
|
|
114
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"python"}]')
|
|
115
|
+
fi
|
|
116
|
+
;;
|
|
117
|
+
package.json)
|
|
118
|
+
if ! grep -q '"workspaces"' "$marker" 2>/dev/null; then
|
|
119
|
+
local lang="javascript"
|
|
120
|
+
[ -f "${dir}/tsconfig.json" ] && lang="typescript"
|
|
121
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" --arg l "$lang" '. + [{"path":$p,"language":$l}]')
|
|
122
|
+
fi
|
|
123
|
+
;;
|
|
124
|
+
pom.xml)
|
|
125
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"java"}]')
|
|
126
|
+
;;
|
|
127
|
+
build.gradle|build.gradle.kts)
|
|
128
|
+
local exists
|
|
129
|
+
exists=$(echo "$modules" | jq --arg p "$rel_dir" '[.[] | select(.path == $p)] | length')
|
|
130
|
+
if [ "$exists" = "0" ]; then
|
|
131
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"kotlin"}]')
|
|
132
|
+
fi
|
|
133
|
+
;;
|
|
134
|
+
esac
|
|
135
|
+
done < <(find "$PROJECT_ROOT" -maxdepth 2 \
|
|
136
|
+
\( -name "go.mod" -o -name "package.json" -o -name "Cargo.toml" \
|
|
137
|
+
-o -name "pyproject.toml" -o -name "setup.cfg" -o -name "requirements.txt" \
|
|
138
|
+
-o -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" \) \
|
|
139
|
+
-not -path "*/node_modules/*" -not -path "*/.git/*" \
|
|
140
|
+
-not -path "*/vendor/*" -not -path "*/dist/*" -not -path "*/.tmp*/*" -not -path "*/.tmp-*" 2>/dev/null)
|
|
141
|
+
|
|
142
|
+
echo "$modules"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
load_project_config() {
|
|
146
|
+
local config_file="$PROJECT_ROOT/specline/config.yaml"
|
|
147
|
+
MODULES_JSON=""
|
|
148
|
+
|
|
149
|
+
if [ -f "$config_file" ] && grep -q '^project:' "$config_file" 2>/dev/null; then
|
|
150
|
+
if grep -q '^ - path:' "$config_file" 2>/dev/null; then
|
|
151
|
+
MODULES_JSON=$(awk '
|
|
152
|
+
/^ modules:/ { in_modules=1; next }
|
|
153
|
+
/^ [a-z]/ && !/^ / { in_modules=0 }
|
|
154
|
+
/^[a-z]/ { in_modules=0 }
|
|
155
|
+
in_modules && /^ - path:/ {
|
|
156
|
+
if (path != "") printf ","
|
|
157
|
+
path=$NF; gsub(/["'\'']/, "", path)
|
|
158
|
+
printf "{\"path\":\"%s\"", path
|
|
159
|
+
}
|
|
160
|
+
in_modules && /^ language:/ {
|
|
161
|
+
lang=$NF; gsub(/["'\'']/, "", lang)
|
|
162
|
+
printf ",\"language\":\"%s\"}", lang
|
|
163
|
+
}
|
|
164
|
+
' "$config_file")
|
|
165
|
+
if [ -n "$MODULES_JSON" ]; then
|
|
166
|
+
MODULES_JSON="[${MODULES_JSON}]"
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
if [ -z "$MODULES_JSON" ] || [ "$MODULES_JSON" = "[]" ]; then
|
|
172
|
+
MODULES_JSON=$(detect_project_modules)
|
|
173
|
+
fi
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module_absolute_path() {
|
|
177
|
+
local rel_path="$1"
|
|
178
|
+
if [ "$rel_path" = "." ] || [ "$rel_path" = "./" ]; then
|
|
179
|
+
echo "$PROJECT_ROOT"
|
|
180
|
+
else
|
|
181
|
+
echo "$PROJECT_ROOT/${rel_path%/}"
|
|
182
|
+
fi
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module_has_eslint_config() {
|
|
186
|
+
local mod_dir="$1"
|
|
187
|
+
local f
|
|
188
|
+
for f in eslint.config.js eslint.config.mjs eslint.config.cjs .eslintrc.js .eslintrc.cjs .eslintrc.json; do
|
|
189
|
+
[ -f "$mod_dir/$f" ] && return 0
|
|
190
|
+
done
|
|
191
|
+
return 1
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
build_module() {
|
|
195
|
+
local rel_path="$1"
|
|
196
|
+
local lang="$2"
|
|
197
|
+
local mod_dir
|
|
198
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
199
|
+
|
|
200
|
+
case "$lang" in
|
|
201
|
+
go)
|
|
202
|
+
echo "正在 build Go 模块: $rel_path"
|
|
203
|
+
if ! (cd "$mod_dir" && go build ./...); then
|
|
204
|
+
fail "Go build 失败 ($rel_path)"
|
|
205
|
+
fi
|
|
206
|
+
;;
|
|
207
|
+
typescript)
|
|
208
|
+
echo "正在 build TypeScript 模块: $rel_path"
|
|
209
|
+
if ! (cd "$mod_dir" && npx tsc --noEmit); then
|
|
210
|
+
fail "TypeScript 编译失败 ($rel_path)"
|
|
211
|
+
fi
|
|
212
|
+
;;
|
|
213
|
+
javascript)
|
|
214
|
+
echo "ℹ️ JavaScript 模块 $rel_path 无 compile 步骤,跳过 build"
|
|
215
|
+
;;
|
|
216
|
+
python)
|
|
217
|
+
echo "正在检查 Python 语法: $rel_path"
|
|
218
|
+
if ! (cd "$mod_dir" && python3 -m compileall -q .); then
|
|
219
|
+
fail "Python 语法错误 ($rel_path)"
|
|
220
|
+
fi
|
|
221
|
+
;;
|
|
222
|
+
rust)
|
|
223
|
+
echo "正在 build Rust 模块: $rel_path"
|
|
224
|
+
if ! (cd "$mod_dir" && cargo build); then
|
|
225
|
+
fail "Rust build 失败 ($rel_path)"
|
|
226
|
+
fi
|
|
227
|
+
;;
|
|
228
|
+
java)
|
|
229
|
+
echo "正在 build Java 模块: $rel_path"
|
|
230
|
+
if ! (cd "$mod_dir" && mvn compile -q); then
|
|
231
|
+
fail "Java build 失败 ($rel_path)"
|
|
232
|
+
fi
|
|
233
|
+
;;
|
|
234
|
+
kotlin)
|
|
235
|
+
echo "正在 build Kotlin 模块: $rel_path"
|
|
236
|
+
if [ -f "$mod_dir/build.gradle.kts" ] || [ -f "$mod_dir/build.gradle" ]; then
|
|
237
|
+
if ! (cd "$mod_dir" && ./gradlew compileKotlin -q 2>/dev/null || gradle compileKotlin -q); then
|
|
238
|
+
fail "Kotlin build 失败 ($rel_path)"
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
;;
|
|
242
|
+
*)
|
|
243
|
+
echo "⚠️ 未知语言 '$lang' ($rel_path),跳过 build"
|
|
244
|
+
;;
|
|
245
|
+
esac
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lint_module() {
|
|
249
|
+
local rel_path="$1"
|
|
250
|
+
local lang="$2"
|
|
251
|
+
local mod_dir
|
|
252
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
253
|
+
|
|
254
|
+
case "$lang" in
|
|
255
|
+
go)
|
|
256
|
+
echo "正在 lint Go 模块: $rel_path"
|
|
257
|
+
if command -v go &>/dev/null; then
|
|
258
|
+
if ! (cd "$mod_dir" && go vet ./...); then
|
|
259
|
+
fail "Go vet 失败 ($rel_path)"
|
|
260
|
+
fi
|
|
261
|
+
if command -v golangci-lint &>/dev/null; then
|
|
262
|
+
if ! (cd "$mod_dir" && golangci-lint run ./...); then
|
|
263
|
+
fail "golangci-lint 失败 ($rel_path)"
|
|
264
|
+
fi
|
|
265
|
+
fi
|
|
266
|
+
else
|
|
267
|
+
echo "⚠️ go 未安装,跳过 Go lint ($rel_path)"
|
|
268
|
+
fi
|
|
269
|
+
;;
|
|
270
|
+
typescript|javascript)
|
|
271
|
+
if module_has_eslint_config "$mod_dir"; then
|
|
272
|
+
echo "正在 lint JS/TS 模块: $rel_path"
|
|
273
|
+
if command -v npx &>/dev/null; then
|
|
274
|
+
if ! (cd "$mod_dir" && npx eslint . --quiet); then
|
|
275
|
+
fail "ESLint 失败 ($rel_path)"
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
else
|
|
279
|
+
echo "ℹ️ 模块 $rel_path 无 eslint 配置,跳过 JS/TS lint"
|
|
280
|
+
fi
|
|
281
|
+
;;
|
|
282
|
+
python)
|
|
283
|
+
if command -v ruff &>/dev/null; then
|
|
284
|
+
echo "正在 lint Python 模块: $rel_path"
|
|
285
|
+
if ! (cd "$mod_dir" && ruff check . --quiet); then
|
|
286
|
+
fail "Python lint 失败 ($rel_path)"
|
|
287
|
+
fi
|
|
288
|
+
else
|
|
289
|
+
echo "⚠️ ruff 未安装,跳过 Python lint ($rel_path)"
|
|
290
|
+
fi
|
|
291
|
+
;;
|
|
292
|
+
rust)
|
|
293
|
+
echo "正在 lint Rust 模块: $rel_path"
|
|
294
|
+
if ! (cd "$mod_dir" && cargo clippy -- -D warnings 2>/dev/null || cargo clippy); then
|
|
295
|
+
fail "Rust clippy 失败 ($rel_path)"
|
|
296
|
+
fi
|
|
297
|
+
;;
|
|
298
|
+
java|kotlin)
|
|
299
|
+
echo "ℹ️ Java/Kotlin lint 暂未集成 ($rel_path),跳过"
|
|
300
|
+
;;
|
|
301
|
+
*)
|
|
302
|
+
echo "⚠️ 未知语言 '$lang' ($rel_path),跳过 lint"
|
|
303
|
+
;;
|
|
304
|
+
esac
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
run_default_unit_test() {
|
|
308
|
+
local rel_path="$1"
|
|
309
|
+
local lang="$2"
|
|
310
|
+
local mod_dir
|
|
311
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
312
|
+
|
|
313
|
+
case "$lang" in
|
|
314
|
+
go)
|
|
315
|
+
echo "正在执行 Go 单元测试: $rel_path"
|
|
316
|
+
if ! (cd "$mod_dir" && go test ./...); then
|
|
317
|
+
fail "Go 单元测试失败 ($rel_path)"
|
|
318
|
+
fi
|
|
319
|
+
;;
|
|
320
|
+
python)
|
|
321
|
+
echo "正在执行 Python 单元测试: $rel_path"
|
|
322
|
+
if ! (cd "$mod_dir" && pytest); then
|
|
323
|
+
fail "Python 单元测试失败 ($rel_path)"
|
|
324
|
+
fi
|
|
325
|
+
;;
|
|
326
|
+
typescript|javascript)
|
|
327
|
+
if [ -f "$mod_dir/package.json" ]; then
|
|
328
|
+
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
329
|
+
echo "正在执行 Vitest 单元测试: $rel_path"
|
|
330
|
+
if ! (cd "$mod_dir" && npx vitest run); then
|
|
331
|
+
fail "Vitest 单元测试失败 ($rel_path)"
|
|
332
|
+
fi
|
|
333
|
+
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
334
|
+
echo "正在执行 Jest 单元测试: $rel_path"
|
|
335
|
+
if ! (cd "$mod_dir" && npx jest); then
|
|
336
|
+
fail "Jest 单元测试失败 ($rel_path)"
|
|
337
|
+
fi
|
|
338
|
+
else
|
|
339
|
+
echo "⚠️ 模块 $rel_path 未检测到 vitest/jest,跳过单元测试"
|
|
340
|
+
fi
|
|
341
|
+
fi
|
|
342
|
+
;;
|
|
343
|
+
rust)
|
|
344
|
+
echo "正在执行 Rust 单元测试: $rel_path"
|
|
345
|
+
if ! (cd "$mod_dir" && cargo test); then
|
|
346
|
+
fail "Rust 单元测试失败 ($rel_path)"
|
|
347
|
+
fi
|
|
348
|
+
;;
|
|
349
|
+
java)
|
|
350
|
+
echo "正在执行 Java 单元测试: $rel_path"
|
|
351
|
+
if [ -f "$mod_dir/pom.xml" ]; then
|
|
352
|
+
if ! (cd "$mod_dir" && mvn test -q); then
|
|
353
|
+
fail "Java 单元测试失败 ($rel_path)"
|
|
354
|
+
fi
|
|
355
|
+
fi
|
|
356
|
+
;;
|
|
357
|
+
kotlin)
|
|
358
|
+
echo "正在执行 Kotlin 单元测试: $rel_path"
|
|
359
|
+
if [ -f "$mod_dir/build.gradle.kts" ] || [ -f "$mod_dir/build.gradle" ]; then
|
|
360
|
+
if ! (cd "$mod_dir" && ./gradlew test -q 2>/dev/null || gradle test -q); then
|
|
361
|
+
fail "Kotlin 单元测试失败 ($rel_path)"
|
|
362
|
+
fi
|
|
363
|
+
fi
|
|
364
|
+
;;
|
|
365
|
+
*)
|
|
366
|
+
echo "⚠️ 未知语言 '$lang' ($rel_path),跳过单元测试"
|
|
367
|
+
;;
|
|
368
|
+
esac
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
run_default_integration_test() {
|
|
372
|
+
local rel_path="$1"
|
|
373
|
+
local lang="$2"
|
|
374
|
+
local mod_dir
|
|
375
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
376
|
+
|
|
377
|
+
case "$lang" in
|
|
378
|
+
go)
|
|
379
|
+
echo "正在执行 Go 集成测试: $rel_path"
|
|
380
|
+
if ! (cd "$mod_dir" && go test ./...); then
|
|
381
|
+
fail "Go 集成测试失败 ($rel_path)"
|
|
382
|
+
fi
|
|
383
|
+
;;
|
|
384
|
+
python)
|
|
385
|
+
if [ -d "$mod_dir/tests/integration" ]; then
|
|
386
|
+
echo "正在执行 Python 集成测试: $rel_path"
|
|
387
|
+
if ! (cd "$mod_dir" && pytest tests/integration -v); then
|
|
388
|
+
fail "Python 集成测试失败 ($rel_path)"
|
|
389
|
+
fi
|
|
390
|
+
else
|
|
391
|
+
echo "ℹ️ 模块 $rel_path 无 tests/integration/,跳过集成测试"
|
|
392
|
+
fi
|
|
393
|
+
;;
|
|
394
|
+
typescript|javascript)
|
|
395
|
+
if [ -d "$mod_dir/tests/integration" ]; then
|
|
396
|
+
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
397
|
+
if ! (cd "$mod_dir" && npx vitest run tests/integration); then
|
|
398
|
+
fail "Vitest 集成测试失败 ($rel_path)"
|
|
399
|
+
fi
|
|
400
|
+
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
401
|
+
if ! (cd "$mod_dir" && npx jest tests/integration); then
|
|
402
|
+
fail "Jest 集成测试失败 ($rel_path)"
|
|
403
|
+
fi
|
|
404
|
+
fi
|
|
405
|
+
else
|
|
406
|
+
echo "ℹ️ 模块 $rel_path 无 tests/integration/,跳过集成测试"
|
|
407
|
+
fi
|
|
408
|
+
;;
|
|
409
|
+
rust)
|
|
410
|
+
if [ -d "$mod_dir/tests" ]; then
|
|
411
|
+
if ! (cd "$mod_dir" && cargo test --test '*' 2>/dev/null || cargo test); then
|
|
412
|
+
fail "Rust 集成测试失败 ($rel_path)"
|
|
413
|
+
fi
|
|
414
|
+
fi
|
|
415
|
+
;;
|
|
416
|
+
*)
|
|
417
|
+
echo "ℹ️ 语言 '$lang' 集成测试默认命令未定义 ($rel_path),跳过"
|
|
418
|
+
;;
|
|
419
|
+
esac
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
run_default_e2e_test() {
|
|
423
|
+
local rel_path="$1"
|
|
424
|
+
local lang="$2"
|
|
425
|
+
local mod_dir
|
|
426
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
427
|
+
|
|
428
|
+
case "$lang" in
|
|
429
|
+
python)
|
|
430
|
+
if [ -d "$mod_dir/tests/e2e" ]; then
|
|
431
|
+
if ! (cd "$mod_dir" && pytest tests/e2e -v); then
|
|
432
|
+
fail "Python E2E 测试失败 ($rel_path)"
|
|
433
|
+
fi
|
|
434
|
+
else
|
|
435
|
+
echo "ℹ️ 模块 $rel_path 无 tests/e2e/,跳过 E2E 测试"
|
|
436
|
+
fi
|
|
437
|
+
;;
|
|
438
|
+
typescript|javascript)
|
|
439
|
+
if [ -d "$mod_dir/tests/e2e" ] || [ -d "$mod_dir/e2e" ]; then
|
|
440
|
+
local e2e_dir="tests/e2e"
|
|
441
|
+
[ -d "$mod_dir/e2e" ] && e2e_dir="e2e"
|
|
442
|
+
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
443
|
+
if ! (cd "$mod_dir" && npx vitest run "$e2e_dir"); then
|
|
444
|
+
fail "Vitest E2E 测试失败 ($rel_path)"
|
|
445
|
+
fi
|
|
446
|
+
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
447
|
+
if ! (cd "$mod_dir" && npx jest "$e2e_dir"); then
|
|
448
|
+
fail "Jest E2E 测试失败 ($rel_path)"
|
|
449
|
+
fi
|
|
450
|
+
fi
|
|
451
|
+
else
|
|
452
|
+
echo "ℹ️ 模块 $rel_path 无 E2E 测试目录,跳过"
|
|
453
|
+
fi
|
|
454
|
+
;;
|
|
455
|
+
*)
|
|
456
|
+
echo "ℹ️ 语言 '$lang' E2E 测试默认命令未定义 ($rel_path),跳过"
|
|
457
|
+
;;
|
|
458
|
+
esac
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
verify_test_result_files() {
|
|
462
|
+
local result_file="$1"
|
|
463
|
+
local files_key="$2"
|
|
464
|
+
local missing=""
|
|
465
|
+
|
|
466
|
+
while IFS= read -r f; do
|
|
467
|
+
[ -z "$f" ] && continue
|
|
468
|
+
if [ ! -f "$PROJECT_ROOT/$f" ]; then
|
|
469
|
+
missing="${missing}
|
|
470
|
+
- $f"
|
|
471
|
+
fi
|
|
472
|
+
done < <(jq -r --arg key "$files_key" '.[$key][]?' "$result_file" 2>/dev/null)
|
|
473
|
+
|
|
474
|
+
if [ -n "$missing" ]; then
|
|
475
|
+
fail "${files_key} 中的测试文件不存在:${missing}"
|
|
476
|
+
fi
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
run_tests_by_modules() {
|
|
480
|
+
local test_kind="$1"
|
|
481
|
+
local count
|
|
482
|
+
count=$(echo "$MODULES_JSON" | jq 'length')
|
|
483
|
+
|
|
484
|
+
if [ "$count" -eq 0 ]; then
|
|
485
|
+
echo "⚠️ 未检测到项目模块,跳过${test_kind}测试"
|
|
486
|
+
return 0
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
local i=0
|
|
490
|
+
while [ "$i" -lt "$count" ]; do
|
|
491
|
+
local path lang
|
|
492
|
+
path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
|
|
493
|
+
lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
|
|
494
|
+
|
|
495
|
+
case "$test_kind" in
|
|
496
|
+
unit) run_default_unit_test "$path" "$lang" ;;
|
|
497
|
+
integration) run_default_integration_test "$path" "$lang" ;;
|
|
498
|
+
e2e) run_default_e2e_test "$path" "$lang" ;;
|
|
499
|
+
esac
|
|
500
|
+
|
|
501
|
+
i=$((i + 1))
|
|
502
|
+
done
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# ===== Phase Handlers =====
|
|
506
|
+
|
|
507
|
+
gate_new() {
|
|
508
|
+
if [ -z "$CHANGE" ]; then
|
|
509
|
+
fail "需要 --change <name>"
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
local change_dir="$PROJECT_ROOT/specline/changes/$CHANGE"
|
|
513
|
+
|
|
514
|
+
if [ -d "$change_dir" ]; then
|
|
515
|
+
echo "⚠️ Change '$CHANGE' 已存在"
|
|
516
|
+
exit 0
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
mkdir -p "$change_dir/specs"
|
|
520
|
+
mkdir -p "$change_dir/.tmp"
|
|
521
|
+
|
|
522
|
+
# 写入 .specline.yaml
|
|
523
|
+
cat > "$change_dir/.specline.yaml" << YAML
|
|
524
|
+
schema: spec-driven
|
|
525
|
+
created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
526
|
+
YAML
|
|
527
|
+
|
|
528
|
+
# 初始化 .pipeline-state.json
|
|
529
|
+
cat > "$change_dir/.pipeline-state.json" << 'JSON'
|
|
530
|
+
{
|
|
531
|
+
"version": 1,
|
|
532
|
+
"change_name": "CHANGE_NAME_PLACEHOLDER",
|
|
533
|
+
"created_at": "CREATED_AT_PLACEHOLDER",
|
|
534
|
+
"updated_at": "CREATED_AT_PLACEHOLDER",
|
|
535
|
+
"current_phase": "spec",
|
|
536
|
+
"current_step": "spec-creator",
|
|
537
|
+
"phases": {
|
|
538
|
+
"spec": { "status": "in_progress", "retry_count": 0, "sub_phases": {}, "gates": { "spec_gate": { "passed": null }, "human_gate_1": { "passed": null } } },
|
|
539
|
+
"coding": { "status": "pending", "tasks": [], "sub_phases": {}, "gates": { "build_gate": { "passed": null } } },
|
|
540
|
+
"code_review": { "status": "pending", "retry_count": 0, "gates": { "lint_gate": { "passed": null }, "human_gate_2": { "passed": null } } },
|
|
541
|
+
"test": { "status": "pending", "framework": null, "sub_phases": { "unit": { "status": "pending", "gates": { "test_unit_gate": { "passed": null } } }, "integration": { "status": "pending", "gates": { "test_integration_gate": { "passed": null } } }, "e2e": { "status": "pending", "gates": { "test_e2e_gate": { "passed": null } } } } },
|
|
542
|
+
"archive": { "status": "pending", "gates": { "human_gate_3": { "passed": null }, "archive_gate": { "passed": null } } }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
JSON
|
|
546
|
+
|
|
547
|
+
# 用实际值替换占位符
|
|
548
|
+
local now
|
|
549
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
550
|
+
# 跨平台 sed(macOS/Linux 兼容)
|
|
551
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
552
|
+
sed -i '' "s/CHANGE_NAME_PLACEHOLDER/$CHANGE/g" "$change_dir/.pipeline-state.json"
|
|
553
|
+
sed -i '' "s/CREATED_AT_PLACEHOLDER/$now/g" "$change_dir/.pipeline-state.json"
|
|
554
|
+
else
|
|
555
|
+
sed -i "s/CHANGE_NAME_PLACEHOLDER/$CHANGE/g" "$change_dir/.pipeline-state.json"
|
|
556
|
+
sed -i "s/CREATED_AT_PLACEHOLDER/$now/g" "$change_dir/.pipeline-state.json"
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
echo "✅ Change '$CHANGE' 已创建: $change_dir"
|
|
560
|
+
echo " .specline.yaml + .pipeline-state.json + specs/"
|
|
561
|
+
|
|
562
|
+
write_gate_passed "phases.spec.gates.spec_gate"
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
gate_list() {
|
|
566
|
+
local changes_dir="$PROJECT_ROOT/specline/changes"
|
|
567
|
+
local json_output=false
|
|
568
|
+
|
|
569
|
+
if [ "${1:-}" = "--json" ]; then
|
|
570
|
+
json_output=true
|
|
571
|
+
fi
|
|
572
|
+
|
|
573
|
+
if [ ! -d "$changes_dir" ]; then
|
|
574
|
+
if $json_output; then
|
|
575
|
+
echo '[]'
|
|
576
|
+
else
|
|
577
|
+
echo "(无活跃 change)"
|
|
578
|
+
fi
|
|
579
|
+
exit 0
|
|
580
|
+
fi
|
|
581
|
+
|
|
582
|
+
if $json_output; then
|
|
583
|
+
echo "["
|
|
584
|
+
local first=true
|
|
585
|
+
for f in "$changes_dir"/*/.pipeline-state.json; do
|
|
586
|
+
[ -f "$f" ] || continue
|
|
587
|
+
# 跳过 archive/
|
|
588
|
+
if echo "$f" | grep -q "/archive/"; then continue; fi
|
|
589
|
+
local dir name phase
|
|
590
|
+
dir=$(dirname "$f")
|
|
591
|
+
name=$(basename "$dir")
|
|
592
|
+
phase=$(jq -r '.current_phase // "unknown"' "$f" 2>/dev/null)
|
|
593
|
+
if [ "$first" = true ]; then first=false; else echo ","; fi
|
|
594
|
+
echo " {\"name\":\"$name\",\"phase\":\"$phase\"}"
|
|
595
|
+
done
|
|
596
|
+
echo "]"
|
|
597
|
+
else
|
|
598
|
+
for f in "$changes_dir"/*/.pipeline-state.json; do
|
|
599
|
+
[ -f "$f" ] || continue
|
|
600
|
+
if echo "$f" | grep -q "/archive/"; then continue; fi
|
|
601
|
+
local dir name phase
|
|
602
|
+
dir=$(dirname "$f")
|
|
603
|
+
name=$(basename "$dir")
|
|
604
|
+
phase=$(jq -r '.current_phase // "unknown"' "$f" 2>/dev/null)
|
|
605
|
+
echo " $name (phase: $phase)"
|
|
606
|
+
done
|
|
607
|
+
fi
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
gate_artifacts() {
|
|
611
|
+
if [ -z "$CHANGE" ]; then
|
|
612
|
+
fail "需要 --change <name>"
|
|
613
|
+
fi
|
|
614
|
+
|
|
615
|
+
local dir="$PROJECT_ROOT/specline/changes/$CHANGE"
|
|
616
|
+
local json_output=false
|
|
617
|
+
|
|
618
|
+
if [ "${1:-}" = "--json" ]; then
|
|
619
|
+
json_output=true
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
local has_proposal=false has_design=false has_tasks=false has_specs=false
|
|
623
|
+
|
|
624
|
+
[ -f "$dir/proposal.md" ] && has_proposal=true
|
|
625
|
+
[ -f "$dir/design.md" ] && has_design=true
|
|
626
|
+
[ -f "$dir/tasks.md" ] && has_tasks=true
|
|
627
|
+
[ -d "$dir/specs" ] && [ -n "$(find "$dir/specs" -name 'spec.md' 2>/dev/null)" ] && has_specs=true
|
|
628
|
+
|
|
629
|
+
if $json_output; then
|
|
630
|
+
echo "{"
|
|
631
|
+
echo " \"proposal\": $has_proposal,"
|
|
632
|
+
echo " \"design\": $has_design,"
|
|
633
|
+
echo " \"tasks\": $has_tasks,"
|
|
634
|
+
echo " \"specs\": $has_specs"
|
|
635
|
+
echo "}"
|
|
636
|
+
else
|
|
637
|
+
echo "Artifacts for '$CHANGE':"
|
|
638
|
+
echo " proposal.md: $has_proposal"
|
|
639
|
+
echo " design.md: $has_design"
|
|
640
|
+
echo " tasks.md: $has_tasks"
|
|
641
|
+
echo " spec.md: $has_specs"
|
|
642
|
+
fi
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
gate_spec() {
|
|
646
|
+
local spec_file
|
|
647
|
+
spec_file=$(find_spec_file)
|
|
648
|
+
|
|
649
|
+
if [ -z "$spec_file" ] || [ ! -f "$spec_file" ]; then
|
|
650
|
+
fail "spec.md 不存在。请确保 spec-creator 已生成 spec 文件。"
|
|
651
|
+
fi
|
|
652
|
+
|
|
653
|
+
# 1. H1 含 "Specification"
|
|
654
|
+
if ! grep -q "^# .* Specification" "$spec_file"; then
|
|
655
|
+
fail "标题格式错误:H1 必须包含 'Specification' 关键词"
|
|
656
|
+
fi
|
|
657
|
+
|
|
658
|
+
# 2. 含 Purpose 章节
|
|
659
|
+
if ! grep -q "^## Purpose" "$spec_file"; then
|
|
660
|
+
fail "缺少 ## Purpose 章节"
|
|
661
|
+
fi
|
|
662
|
+
|
|
663
|
+
# 3. 含 Requirements 章节
|
|
664
|
+
if ! grep -q "^## Requirements" "$spec_file"; then
|
|
665
|
+
fail "缺少 ## Requirements 章节"
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
# 4. 至少 1 个 Requirement
|
|
669
|
+
local req_count
|
|
670
|
+
req_count=$(grep -c "^### Requirement:" "$spec_file" || echo "0")
|
|
671
|
+
if [ "$req_count" -lt 1 ]; then
|
|
672
|
+
fail "至少需要 1 个 Requirement,当前: $req_count"
|
|
673
|
+
fi
|
|
674
|
+
pass "Requirements 数量: $req_count"
|
|
675
|
+
|
|
676
|
+
# 5. 每个 Requirement 至少 1 个 Scenario(简化检查:Scenario 总数 >= Requirement 总数)
|
|
677
|
+
local scenario_count
|
|
678
|
+
scenario_count=$(grep -c "^#### Scenario:" "$spec_file" || echo "0")
|
|
679
|
+
if [ "$scenario_count" -lt "$req_count" ]; then
|
|
680
|
+
fail "每个 Requirement 至少需要 1 个 Scenario。Requirement: $req_count, Scenario: $scenario_count"
|
|
681
|
+
fi
|
|
682
|
+
pass "Scenario 数量: $scenario_count"
|
|
683
|
+
|
|
684
|
+
# 6. WHEN/THEN 语义检查(每个 Scenario 至少 1 WHEN + 1 THEN)
|
|
685
|
+
local bad_scenarios=""
|
|
686
|
+
local current_scenario="" has_when=0 has_then=0
|
|
687
|
+
|
|
688
|
+
while IFS= read -r line; do
|
|
689
|
+
if [[ "$line" =~ ^####\ Scenario: ]]; then
|
|
690
|
+
if [ -n "$current_scenario" ] && { [ "$has_when" -eq 0 ] || [ "$has_then" -eq 0 ]; }; then
|
|
691
|
+
bad_scenarios="${bad_scenarios}\n - ${current_scenario} (WHEN=${has_when}, THEN=${has_then})"
|
|
692
|
+
fi
|
|
693
|
+
current_scenario="${line#*Scenario: }"
|
|
694
|
+
has_when=0; has_then=0
|
|
695
|
+
fi
|
|
696
|
+
[[ "$line" == *'**WHEN**'* ]] && ((has_when++)) || true
|
|
697
|
+
[[ "$line" == *'**THEN**'* ]] && ((has_then++)) || true
|
|
698
|
+
done < "$spec_file"
|
|
699
|
+
|
|
700
|
+
if [ -n "$current_scenario" ] && { [ "$has_when" -eq 0 ] || [ "$has_then" -eq 0 ]; }; then
|
|
701
|
+
bad_scenarios="${bad_scenarios}\n - ${current_scenario} (WHEN=${has_when}, THEN=${has_then})"
|
|
702
|
+
fi
|
|
703
|
+
|
|
704
|
+
if [ -n "$bad_scenarios" ]; then
|
|
705
|
+
fail "以下 Scenario 缺少 WHEN 或 THEN:${bad_scenarios}"
|
|
706
|
+
fi
|
|
707
|
+
pass "WHEN/THEN 语义检查通过 (每个 Scenario 至少 1 WHEN + 1 THEN)"
|
|
708
|
+
|
|
709
|
+
# 7. review.json 状态检查(如果存在)
|
|
710
|
+
local review_file
|
|
711
|
+
review_file="$(dirname "$spec_file")/spec-review.json"
|
|
712
|
+
if [ -f "$review_file" ]; then
|
|
713
|
+
local review_status
|
|
714
|
+
review_status=$(jq -r '.status' "$review_file" 2>/dev/null || echo "missing")
|
|
715
|
+
if [ "$review_status" != "approved" ]; then
|
|
716
|
+
fail "spec-review.json 审核未通过 (status: $review_status)"
|
|
717
|
+
fi
|
|
718
|
+
pass "审核状态: approved"
|
|
719
|
+
|
|
720
|
+
# 7b. 检查 coverage(所有 Requirement 和 Scenario 被 task 的 Covers 覆盖)
|
|
721
|
+
local cov_req_total cov_req_covered
|
|
722
|
+
cov_req_total=$(jq -r '.coverage.requirements_total' "$review_file" 2>/dev/null || echo "0")
|
|
723
|
+
cov_req_covered=$(jq -r '.coverage.requirements_covered' "$review_file" 2>/dev/null || echo "0")
|
|
724
|
+
if [ "$cov_req_covered" -lt "$cov_req_total" ]; then
|
|
725
|
+
fail "Requirement 覆盖不全: $cov_req_covered/$cov_req_total"
|
|
726
|
+
fi
|
|
727
|
+
pass "Requirement 覆盖率: $cov_req_covered/$cov_req_total"
|
|
728
|
+
else
|
|
729
|
+
pass "审核状态: 无 spec-review.json(跳过审核检查)"
|
|
730
|
+
fi
|
|
731
|
+
|
|
732
|
+
# 8. 检查 tasks.md 是否存在且含完整的 Type/Depends/Covers/Files 标注
|
|
733
|
+
local tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
|
|
734
|
+
if [ ! -f "$tasks_file" ]; then
|
|
735
|
+
fail "tasks.md 不存在"
|
|
736
|
+
fi
|
|
737
|
+
pass "tasks.md 存在"
|
|
738
|
+
|
|
739
|
+
# 9. 检查每个任务标注完整性
|
|
740
|
+
local task_count type_count deps_count covers_count files_count
|
|
741
|
+
task_count=$(grep -c '^## ' "$tasks_file" || echo "0")
|
|
742
|
+
type_count=$(grep -c '\*\*Type\*\*:' "$tasks_file" || echo "0")
|
|
743
|
+
deps_count=$(grep -c '\*\*Depends\*\*:' "$tasks_file" || echo "0")
|
|
744
|
+
covers_count=$(grep -c '\*\*Covers\*\*:' "$tasks_file" || echo "0")
|
|
745
|
+
files_count=$(grep -c '\*\*Files\*\*:' "$tasks_file" || echo "0")
|
|
746
|
+
|
|
747
|
+
if [ "$type_count" -lt "$task_count" ] || [ "$deps_count" -lt "$task_count" ] || \
|
|
748
|
+
[ "$covers_count" -lt "$task_count" ] || [ "$files_count" -lt "$task_count" ]; then
|
|
749
|
+
fail "tasks.md 标注不完整:任务=$task_count, Type=$type_count, Depends=$deps_count, Covers=$covers_count, Files=$files_count"
|
|
750
|
+
fi
|
|
751
|
+
pass "tasks.md 标注完整性检查通过 ($task_count 个任务)"
|
|
752
|
+
|
|
753
|
+
# 10. Testable 字段校验
|
|
754
|
+
local testable_count
|
|
755
|
+
testable_count=$(grep -c '\*\*Testable\*\*:' "$tasks_file" || echo "0")
|
|
756
|
+
|
|
757
|
+
if [ "$testable_count" -eq 0 ]; then
|
|
758
|
+
echo "⚠️ Testable 标注缺失(向后兼容模式:缺失字段的任务将被视为 Testable: false)"
|
|
759
|
+
elif [ "$testable_count" -gt 0 ] && [ "$testable_count" -lt "$task_count" ]; then
|
|
760
|
+
local missing_testable_tasks
|
|
761
|
+
missing_testable_tasks=$(awk '
|
|
762
|
+
/^## / {
|
|
763
|
+
if (in_task && !has_testable) missing = missing (missing ? ", " : "") prev_task
|
|
764
|
+
prev_task = $2; gsub(/\..*/, "", prev_task)
|
|
765
|
+
in_task = 1; has_testable = 0
|
|
766
|
+
}
|
|
767
|
+
/\*\*Testable\*\*:/ { has_testable = 1 }
|
|
768
|
+
END {
|
|
769
|
+
if (in_task && !has_testable) missing = missing (missing ? ", " : "") prev_task
|
|
770
|
+
print missing
|
|
771
|
+
}' "$tasks_file")
|
|
772
|
+
echo "⚠️ Testable 标注不完整:任务=$task_count, Testable=$testable_count(缺失任务: $missing_testable_tasks;缺失字段的任务将被视为 Testable: false)"
|
|
773
|
+
else
|
|
774
|
+
pass "Testable 标注完整性检查通过 ($testable_count/$task_count)"
|
|
775
|
+
fi
|
|
776
|
+
|
|
777
|
+
# 11. 至少 1 个任务无依赖
|
|
778
|
+
local independent_count
|
|
779
|
+
independent_count=$(grep -c '\*\*Depends\*\*: (none)' "$tasks_file" || echo "0")
|
|
780
|
+
if [ "$independent_count" -lt 1 ]; then
|
|
781
|
+
fail "至少需要 1 个无依赖任务 (Depends: none),当前: $independent_count"
|
|
782
|
+
fi
|
|
783
|
+
pass "无依赖任务数: $independent_count"
|
|
784
|
+
|
|
785
|
+
write_gate_passed "phases.spec.gates.spec_gate"
|
|
786
|
+
pass "Spec Gate 全部通过"
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
gate_build() {
|
|
790
|
+
load_project_config
|
|
791
|
+
|
|
792
|
+
local module_count
|
|
793
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
794
|
+
|
|
795
|
+
if [ "$module_count" -eq 0 ]; then
|
|
796
|
+
echo "⚠️ 未检测到项目模块,跳过 build 命令"
|
|
797
|
+
else
|
|
798
|
+
local i=0
|
|
799
|
+
while [ "$i" -lt "$module_count" ]; do
|
|
800
|
+
local path lang
|
|
801
|
+
path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
|
|
802
|
+
lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
|
|
803
|
+
build_module "$path" "$lang"
|
|
804
|
+
i=$((i + 1))
|
|
805
|
+
done
|
|
806
|
+
pass "模块 build 检查通过 ($module_count 个模块)"
|
|
807
|
+
fi
|
|
808
|
+
|
|
809
|
+
# Agent 产出 JSON 验证(task-result.json files_changed / files)
|
|
810
|
+
if [ -n "$CHANGE" ]; then
|
|
811
|
+
local task_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/task-result.json"
|
|
812
|
+
if [ -f "$task_result" ]; then
|
|
813
|
+
echo "正在验证 task-result.json 声明的文件..."
|
|
814
|
+
local missing_files=""
|
|
815
|
+
while IFS= read -r f; do
|
|
816
|
+
[ -z "$f" ] && continue
|
|
817
|
+
if [ ! -f "$PROJECT_ROOT/$f" ]; then
|
|
818
|
+
missing_files="${missing_files}
|
|
819
|
+
- $f"
|
|
820
|
+
fi
|
|
821
|
+
done < <(jq -r '(.files_changed // .files // [])[]?' "$task_result" 2>/dev/null)
|
|
822
|
+
|
|
823
|
+
if [ -n "$missing_files" ]; then
|
|
824
|
+
fail "task-result.json 声明的文件不存在:${missing_files}"
|
|
825
|
+
fi
|
|
826
|
+
pass "task-result.json 文件验证通过"
|
|
827
|
+
fi
|
|
828
|
+
fi
|
|
829
|
+
|
|
830
|
+
# 单元测试文件存在性检查(Testable=true 任务)
|
|
831
|
+
local tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
|
|
832
|
+
if [ -f "$tasks_file" ]; then
|
|
833
|
+
local testable_true_count
|
|
834
|
+
testable_true_count=$(grep -c '\*\*Testable\*\*:.*true' "$tasks_file" || echo "0")
|
|
835
|
+
|
|
836
|
+
if [ "$testable_true_count" -gt 0 ]; then
|
|
837
|
+
echo "正在检查 $testable_true_count 个 Testable=true 任务的单元测试文件..."
|
|
838
|
+
|
|
839
|
+
local missing_files=""
|
|
840
|
+
local syntax_errors=""
|
|
841
|
+
|
|
842
|
+
while IFS='|' read -r task_id file_path; do
|
|
843
|
+
if [ -z "$file_path" ]; then
|
|
844
|
+
missing_files="${missing_files}
|
|
845
|
+
任务 $task_id: 未在 Files 列表中声明测试文件(支持 tests/unit/、tests/models/、*_test.go、*.test.ts 等)"
|
|
846
|
+
continue
|
|
847
|
+
fi
|
|
848
|
+
|
|
849
|
+
if [ ! -f "$PROJECT_ROOT/$file_path" ]; then
|
|
850
|
+
missing_files="${missing_files}
|
|
851
|
+
任务 $task_id: $file_path"
|
|
852
|
+
continue
|
|
853
|
+
fi
|
|
854
|
+
|
|
855
|
+
case "$file_path" in
|
|
856
|
+
*.py)
|
|
857
|
+
if ! python3 -m py_compile "$PROJECT_ROOT/$file_path" 2>&1; then
|
|
858
|
+
syntax_errors="${syntax_errors}
|
|
859
|
+
任务 $task_id: $file_path (Python 语法错误)"
|
|
860
|
+
fi
|
|
861
|
+
;;
|
|
862
|
+
*.ts|*.tsx)
|
|
863
|
+
if ! npx tsc --noEmit "$PROJECT_ROOT/$file_path" 2>&1; then
|
|
864
|
+
syntax_errors="${syntax_errors}
|
|
865
|
+
任务 $task_id: $file_path (TypeScript 语法错误)"
|
|
866
|
+
fi
|
|
867
|
+
;;
|
|
868
|
+
esac
|
|
869
|
+
done < <(awk '
|
|
870
|
+
/^## / {
|
|
871
|
+
task_id = $2; gsub(/\..*/, "", task_id)
|
|
872
|
+
testable = ""; files_line = ""
|
|
873
|
+
}
|
|
874
|
+
/\*\*Testable\*\*:.*true/ { testable = "true" }
|
|
875
|
+
/\*\*Files\*\*:/ {
|
|
876
|
+
if (testable == "true") {
|
|
877
|
+
files_line = $0
|
|
878
|
+
gsub(/.*\*\*Files\*\*:[ \t]*/, "", files_line)
|
|
879
|
+
split(files_line, paths, /,[ \t]*/)
|
|
880
|
+
has_test = 0
|
|
881
|
+
for (i in paths) {
|
|
882
|
+
gsub(/^[ \t]+|[ \t]+$/, "", paths[i])
|
|
883
|
+
if (paths[i] ~ /^tests\/(unit|models)\// ||
|
|
884
|
+
paths[i] ~ /_test\.go$/ ||
|
|
885
|
+
paths[i] ~ /\.test\.(ts|tsx|js|jsx)$/ ||
|
|
886
|
+
paths[i] ~ /\.spec\.(ts|tsx|js|jsx)$/ ||
|
|
887
|
+
paths[i] ~ /^src\/.*\/tests\.rs$/) {
|
|
888
|
+
print task_id "|" paths[i]
|
|
889
|
+
has_test = 1
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (has_test == 0) {
|
|
893
|
+
print task_id "|"
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
' "$tasks_file")
|
|
898
|
+
|
|
899
|
+
if [ -n "$missing_files" ]; then
|
|
900
|
+
fail "单元测试文件缺失:${missing_files}"
|
|
901
|
+
fi
|
|
902
|
+
|
|
903
|
+
if [ -n "$syntax_errors" ]; then
|
|
904
|
+
fail "单元测试文件语法错误:${syntax_errors}"
|
|
905
|
+
fi
|
|
906
|
+
|
|
907
|
+
pass "单元测试文件存在性检查通过"
|
|
908
|
+
else
|
|
909
|
+
echo "ℹ️ 无 Testable=true 任务,跳过单元测试文件检查"
|
|
910
|
+
fi
|
|
911
|
+
fi
|
|
912
|
+
|
|
913
|
+
write_gate_passed "phases.coding.gates.build_gate"
|
|
914
|
+
pass "Build Gate 全部通过"
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
gate_lint() {
|
|
918
|
+
load_project_config
|
|
919
|
+
|
|
920
|
+
local module_count
|
|
921
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
922
|
+
|
|
923
|
+
if [ "$module_count" -eq 0 ]; then
|
|
924
|
+
echo "⚠️ 未检测到项目模块,跳过 lint"
|
|
925
|
+
else
|
|
926
|
+
local i=0
|
|
927
|
+
while [ "$i" -lt "$module_count" ]; do
|
|
928
|
+
local path lang
|
|
929
|
+
path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
|
|
930
|
+
lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
|
|
931
|
+
lint_module "$path" "$lang"
|
|
932
|
+
i=$((i + 1))
|
|
933
|
+
done
|
|
934
|
+
pass "模块 lint 检查通过 ($module_count 个模块)"
|
|
935
|
+
fi
|
|
936
|
+
|
|
937
|
+
# code-review.json error 计数(位于 change 的 .tmp/ 目录下)
|
|
938
|
+
local review_file="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/code-review.json"
|
|
939
|
+
if [ -f "$review_file" ]; then
|
|
940
|
+
local error_count
|
|
941
|
+
error_count=$(jq '[.findings[] | select(.severity=="error")] | length' "$review_file" 2>/dev/null || echo "0")
|
|
942
|
+
if [ "$error_count" -gt 0 ]; then
|
|
943
|
+
fail "code-review.json 中发现 $error_count 个 error,必须修复"
|
|
944
|
+
fi
|
|
945
|
+
pass "Review errors: 0"
|
|
946
|
+
fi
|
|
947
|
+
|
|
948
|
+
write_gate_passed "phases.code_review.gates.lint_gate"
|
|
949
|
+
pass "Lint Gate 全部通过"
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
# ===== 测试框架自动检测 =====
|
|
953
|
+
# 优先级:.pipeline-state.json > test-code-result.json > MODULES_JSON 推导 > 无兜底
|
|
954
|
+
detect_test_framework() {
|
|
955
|
+
framework="" test_cmd="" coverage_cmd=""
|
|
956
|
+
|
|
957
|
+
# 1. 先尝试从状态文件读取 test-writer 的检测结果
|
|
958
|
+
if [ -f "$STATE_FILE" ]; then
|
|
959
|
+
local recorded
|
|
960
|
+
recorded=$(jq -r '.phases.test.framework // empty' "$STATE_FILE" 2>/dev/null)
|
|
961
|
+
if [ -n "$recorded" ]; then
|
|
962
|
+
framework="$recorded"
|
|
963
|
+
fi
|
|
964
|
+
fi
|
|
965
|
+
|
|
966
|
+
# 2. 从 test-code-result.json 读取
|
|
967
|
+
if [ -z "$framework" ] && [ -n "$CHANGE" ]; then
|
|
968
|
+
local test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
969
|
+
if [ -f "$test_result" ]; then
|
|
970
|
+
framework=$(jq -r '.test_framework // empty' "$test_result" 2>/dev/null)
|
|
971
|
+
test_cmd=$(jq -r '.test_cmd // empty' "$test_result" 2>/dev/null)
|
|
972
|
+
fi
|
|
973
|
+
fi
|
|
974
|
+
|
|
975
|
+
# 3. 从 MODULES_JSON 推导
|
|
976
|
+
if [ -z "$framework" ]; then
|
|
977
|
+
load_project_config
|
|
978
|
+
local module_count
|
|
979
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
980
|
+
if [ "$module_count" -gt 0 ]; then
|
|
981
|
+
local lang
|
|
982
|
+
lang=$(echo "$MODULES_JSON" | jq -r '.[0].language')
|
|
983
|
+
case "$lang" in
|
|
984
|
+
go) framework="go-test" ;;
|
|
985
|
+
python) framework="pytest" ;;
|
|
986
|
+
rust) framework="cargo-test" ;;
|
|
987
|
+
java) framework="junit" ;;
|
|
988
|
+
kotlin) framework="junit" ;;
|
|
989
|
+
typescript|javascript)
|
|
990
|
+
local mod_path
|
|
991
|
+
mod_path=$(echo "$MODULES_JSON" | jq -r '.[0].path')
|
|
992
|
+
local mod_dir
|
|
993
|
+
mod_dir=$(module_absolute_path "$mod_path")
|
|
994
|
+
if [ -f "$mod_dir/package.json" ]; then
|
|
995
|
+
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
996
|
+
framework="vitest"
|
|
997
|
+
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
998
|
+
framework="jest"
|
|
999
|
+
elif grep -q '"mocha"' "$mod_dir/package.json" 2>/dev/null; then
|
|
1000
|
+
framework="mocha"
|
|
1001
|
+
fi
|
|
1002
|
+
fi
|
|
1003
|
+
;;
|
|
1004
|
+
esac
|
|
1005
|
+
fi
|
|
1006
|
+
fi
|
|
1007
|
+
|
|
1008
|
+
# 4. 无兜底 pytest — 检测失败时 framework 为空
|
|
1009
|
+
if [ -z "$framework" ]; then
|
|
1010
|
+
echo "⚠️ 未检测到测试框架"
|
|
1011
|
+
return 0
|
|
1012
|
+
fi
|
|
1013
|
+
|
|
1014
|
+
# 根据框架确定命令(test_cmd 可能已由 JSON 提供)
|
|
1015
|
+
if [ -z "$test_cmd" ]; then
|
|
1016
|
+
case "$framework" in
|
|
1017
|
+
jest)
|
|
1018
|
+
test_cmd="npx jest"
|
|
1019
|
+
coverage_cmd="npx jest --coverage"
|
|
1020
|
+
;;
|
|
1021
|
+
vitest)
|
|
1022
|
+
test_cmd="npx vitest run"
|
|
1023
|
+
coverage_cmd="npx vitest run --coverage"
|
|
1024
|
+
;;
|
|
1025
|
+
mocha)
|
|
1026
|
+
test_cmd="npx mocha"
|
|
1027
|
+
coverage_cmd="npx nyc mocha"
|
|
1028
|
+
;;
|
|
1029
|
+
go-test)
|
|
1030
|
+
test_cmd="go test ./..."
|
|
1031
|
+
coverage_cmd="go test -cover ./..."
|
|
1032
|
+
;;
|
|
1033
|
+
cargo-test)
|
|
1034
|
+
test_cmd="cargo test"
|
|
1035
|
+
coverage_cmd="cargo tarpaulin 2>/dev/null || cargo test"
|
|
1036
|
+
;;
|
|
1037
|
+
junit)
|
|
1038
|
+
local mod_path mod_dir
|
|
1039
|
+
mod_path=$(echo "$MODULES_JSON" | jq -r '.[0].path // "."')
|
|
1040
|
+
mod_dir=$(module_absolute_path "$mod_path")
|
|
1041
|
+
if [ -f "$mod_dir/pom.xml" ]; then
|
|
1042
|
+
test_cmd="mvn test"
|
|
1043
|
+
coverage_cmd="mvn jacoco:report"
|
|
1044
|
+
else
|
|
1045
|
+
test_cmd="gradle test"
|
|
1046
|
+
coverage_cmd="gradle jacocoTestReport"
|
|
1047
|
+
fi
|
|
1048
|
+
;;
|
|
1049
|
+
pytest)
|
|
1050
|
+
test_cmd="pytest"
|
|
1051
|
+
coverage_cmd="pytest --cov --cov-fail-under=80"
|
|
1052
|
+
;;
|
|
1053
|
+
*)
|
|
1054
|
+
test_cmd=""
|
|
1055
|
+
coverage_cmd=""
|
|
1056
|
+
;;
|
|
1057
|
+
esac
|
|
1058
|
+
fi
|
|
1059
|
+
|
|
1060
|
+
if [ -n "$framework" ] && [ -n "$test_cmd" ]; then
|
|
1061
|
+
echo "检测到测试框架: $framework (命令: $test_cmd)"
|
|
1062
|
+
fi
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
gate_test_unit() {
|
|
1066
|
+
echo "正在执行单元测试..."
|
|
1067
|
+
load_project_config
|
|
1068
|
+
|
|
1069
|
+
local test_result=""
|
|
1070
|
+
if [ -n "$CHANGE" ]; then
|
|
1071
|
+
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1072
|
+
fi
|
|
1073
|
+
|
|
1074
|
+
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1075
|
+
local test_cmd
|
|
1076
|
+
test_cmd=$(jq -r '.test_cmd // empty' "$test_result" 2>/dev/null)
|
|
1077
|
+
|
|
1078
|
+
verify_test_result_files "$test_result" "test_files"
|
|
1079
|
+
|
|
1080
|
+
if [ -n "$test_cmd" ]; then
|
|
1081
|
+
echo "执行 Agent 声明的 test_cmd: $test_cmd"
|
|
1082
|
+
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1083
|
+
fail "单元测试失败"
|
|
1084
|
+
fi
|
|
1085
|
+
else
|
|
1086
|
+
echo "⚠️ test-code-result.json 无 test_cmd,回退到模块默认命令"
|
|
1087
|
+
run_tests_by_modules "unit"
|
|
1088
|
+
fi
|
|
1089
|
+
else
|
|
1090
|
+
local module_count
|
|
1091
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1092
|
+
if [ "$module_count" -eq 0 ]; then
|
|
1093
|
+
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过单元测试"
|
|
1094
|
+
write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
|
|
1095
|
+
pass "单元测试已跳过"
|
|
1096
|
+
return 0
|
|
1097
|
+
fi
|
|
1098
|
+
run_tests_by_modules "unit"
|
|
1099
|
+
fi
|
|
1100
|
+
|
|
1101
|
+
# 覆盖率检查(非阻塞)
|
|
1102
|
+
detect_test_framework
|
|
1103
|
+
if [ -n "${coverage_cmd:-}" ]; then
|
|
1104
|
+
echo "正在检查覆盖率..."
|
|
1105
|
+
if ! (cd "$PROJECT_ROOT" && eval "$coverage_cmd" 2>&1); then
|
|
1106
|
+
echo "⚠️ 覆盖率检查未通过(不阻塞,由 test-runner agent 深入分析)"
|
|
1107
|
+
fi
|
|
1108
|
+
fi
|
|
1109
|
+
|
|
1110
|
+
write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
|
|
1111
|
+
pass "单元测试通过"
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
gate_test_integration() {
|
|
1115
|
+
echo "正在执行集成测试..."
|
|
1116
|
+
load_project_config
|
|
1117
|
+
|
|
1118
|
+
local test_result=""
|
|
1119
|
+
if [ -n "$CHANGE" ]; then
|
|
1120
|
+
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1121
|
+
fi
|
|
1122
|
+
|
|
1123
|
+
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1124
|
+
local test_cmd
|
|
1125
|
+
test_cmd=$(jq -r '.integration_test_cmd // empty' "$test_result" 2>/dev/null)
|
|
1126
|
+
|
|
1127
|
+
if jq -e '.integration_test_files | length > 0' "$test_result" &>/dev/null; then
|
|
1128
|
+
verify_test_result_files "$test_result" "integration_test_files"
|
|
1129
|
+
fi
|
|
1130
|
+
|
|
1131
|
+
if [ -n "$test_cmd" ]; then
|
|
1132
|
+
echo "执行 Agent 声明的 integration_test_cmd: $test_cmd"
|
|
1133
|
+
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1134
|
+
fail "集成测试失败"
|
|
1135
|
+
fi
|
|
1136
|
+
else
|
|
1137
|
+
run_tests_by_modules "integration"
|
|
1138
|
+
fi
|
|
1139
|
+
else
|
|
1140
|
+
local module_count
|
|
1141
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1142
|
+
if [ "$module_count" -eq 0 ]; then
|
|
1143
|
+
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过集成测试"
|
|
1144
|
+
else
|
|
1145
|
+
run_tests_by_modules "integration"
|
|
1146
|
+
fi
|
|
1147
|
+
fi
|
|
1148
|
+
|
|
1149
|
+
write_gate_passed "phases.test.sub_phases.integration.gates.test_integration_gate"
|
|
1150
|
+
pass "集成测试通过"
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
gate_test_e2e() {
|
|
1154
|
+
echo "正在执行 E2E 测试..."
|
|
1155
|
+
load_project_config
|
|
1156
|
+
|
|
1157
|
+
local test_result=""
|
|
1158
|
+
if [ -n "$CHANGE" ]; then
|
|
1159
|
+
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1160
|
+
fi
|
|
1161
|
+
|
|
1162
|
+
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1163
|
+
local test_cmd
|
|
1164
|
+
test_cmd=$(jq -r '.e2e_test_cmd // empty' "$test_result" 2>/dev/null)
|
|
1165
|
+
|
|
1166
|
+
if jq -e '.e2e_test_files | length > 0' "$test_result" &>/dev/null; then
|
|
1167
|
+
verify_test_result_files "$test_result" "e2e_test_files"
|
|
1168
|
+
fi
|
|
1169
|
+
|
|
1170
|
+
if [ -n "$test_cmd" ]; then
|
|
1171
|
+
echo "执行 Agent 声明的 e2e_test_cmd: $test_cmd"
|
|
1172
|
+
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1173
|
+
fail "E2E 测试失败"
|
|
1174
|
+
fi
|
|
1175
|
+
else
|
|
1176
|
+
run_tests_by_modules "e2e"
|
|
1177
|
+
fi
|
|
1178
|
+
else
|
|
1179
|
+
local module_count
|
|
1180
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1181
|
+
if [ "$module_count" -eq 0 ]; then
|
|
1182
|
+
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过 E2E 测试"
|
|
1183
|
+
else
|
|
1184
|
+
run_tests_by_modules "e2e"
|
|
1185
|
+
fi
|
|
1186
|
+
fi
|
|
1187
|
+
|
|
1188
|
+
write_gate_passed "phases.test.sub_phases.e2e.gates.test_e2e_gate"
|
|
1189
|
+
pass "E2E 测试通过"
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
gate_archive() {
|
|
1193
|
+
if [ -z "$CHANGE" ]; then
|
|
1194
|
+
fail "需要 --change <name>"
|
|
1195
|
+
fi
|
|
1196
|
+
|
|
1197
|
+
# 如果传了 --execute,执行实际归档动作
|
|
1198
|
+
if [ -n "$EXECUTE_ARCHIVE" ]; then
|
|
1199
|
+
local src_dir="$PROJECT_ROOT/specline/changes/$CHANGE"
|
|
1200
|
+
local archive_dir="$PROJECT_ROOT/specline/changes/archive"
|
|
1201
|
+
local date_prefix
|
|
1202
|
+
date_prefix=$(date -u +"%Y-%m-%d")
|
|
1203
|
+
local dest="$archive_dir/${date_prefix}-${CHANGE}"
|
|
1204
|
+
|
|
1205
|
+
if [ ! -d "$src_dir" ]; then
|
|
1206
|
+
fail "Change '$CHANGE' 不存在: $src_dir"
|
|
1207
|
+
fi
|
|
1208
|
+
|
|
1209
|
+
# 检查基本文件
|
|
1210
|
+
if [ ! -f "$src_dir/proposal.md" ]; then
|
|
1211
|
+
fail "缺少 proposal.md"
|
|
1212
|
+
fi
|
|
1213
|
+
if [ ! -f "$src_dir/tasks.md" ]; then
|
|
1214
|
+
fail "缺少 tasks.md"
|
|
1215
|
+
fi
|
|
1216
|
+
|
|
1217
|
+
# 同步 delta specs 到主 specs
|
|
1218
|
+
if [ -d "$src_dir/specs" ]; then
|
|
1219
|
+
echo "正在同步 delta specs 到 specline/specs/..."
|
|
1220
|
+
cp -r "$src_dir/specs/"* "$PROJECT_ROOT/specline/specs/" 2>/dev/null || true
|
|
1221
|
+
fi
|
|
1222
|
+
|
|
1223
|
+
# 移动到归档
|
|
1224
|
+
mkdir -p "$archive_dir"
|
|
1225
|
+
if [ -d "$dest" ]; then
|
|
1226
|
+
fail "归档目标已存在: $dest"
|
|
1227
|
+
fi
|
|
1228
|
+
|
|
1229
|
+
mv "$src_dir" "$dest"
|
|
1230
|
+
echo "✅ 已归档到: $dest"
|
|
1231
|
+
|
|
1232
|
+
# 临时文件(.tmp/)随 change 目录一起归档,无需单独清理
|
|
1233
|
+
|
|
1234
|
+
# 更新状态文件
|
|
1235
|
+
if [ -f "$dest/.pipeline-state.json" ]; then
|
|
1236
|
+
local now
|
|
1237
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1238
|
+
# macOS sed 兼容
|
|
1239
|
+
sed -i '' "s/\"current_phase\": \"[^\"]*\"/\"current_phase\": \"archived\"/g" "$dest/.pipeline-state.json" 2>/dev/null || true
|
|
1240
|
+
fi
|
|
1241
|
+
|
|
1242
|
+
# 清理所有绑定到该 change 的 session
|
|
1243
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
1244
|
+
if [ -f "$bindings_file" ]; then
|
|
1245
|
+
jq --arg change "$CHANGE" \
|
|
1246
|
+
'with_entries(select(.value.change != $change))' \
|
|
1247
|
+
"$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
1248
|
+
echo "✅ 已清理 pipeline '$CHANGE' 的所有 session 绑定"
|
|
1249
|
+
fi
|
|
1250
|
+
|
|
1251
|
+
exit 0
|
|
1252
|
+
fi
|
|
1253
|
+
|
|
1254
|
+
# 验证模式(原有逻辑,路径改为 specline)
|
|
1255
|
+
local archive_dir="$PROJECT_ROOT/specline/changes/archive"
|
|
1256
|
+
local found
|
|
1257
|
+
found=$(find "$archive_dir" -maxdepth 1 -type d -name "*$CHANGE" 2>/dev/null | head -1)
|
|
1258
|
+
|
|
1259
|
+
if [ -z "$found" ]; then
|
|
1260
|
+
fail "归档目录不存在: $archive_dir/*$CHANGE"
|
|
1261
|
+
fi
|
|
1262
|
+
|
|
1263
|
+
if [ ! -f "$found/proposal.md" ]; then
|
|
1264
|
+
fail "归档目录缺少 proposal.md"
|
|
1265
|
+
fi
|
|
1266
|
+
if [ ! -f "$found/tasks.md" ]; then
|
|
1267
|
+
fail "归档目录缺少 tasks.md"
|
|
1268
|
+
fi
|
|
1269
|
+
|
|
1270
|
+
write_gate_passed "phases.archive.gates.archive_gate"
|
|
1271
|
+
pass "Archive Gate 全部通过"
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
gate_status() {
|
|
1275
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
1276
|
+
echo '{"status":"no_pipeline","message":"未找到流水线状态文件"}'
|
|
1277
|
+
exit 0
|
|
1278
|
+
fi
|
|
1279
|
+
|
|
1280
|
+
jq '{
|
|
1281
|
+
change: .change_name,
|
|
1282
|
+
phase: .current_phase,
|
|
1283
|
+
step: .current_step,
|
|
1284
|
+
tasks: .phases.coding.tasks | map({id: .id, type: .type, status: .status, batch: .batch}),
|
|
1285
|
+
progress: {
|
|
1286
|
+
spec: .phases.spec.status,
|
|
1287
|
+
coding: .phases.coding.status,
|
|
1288
|
+
code_review: .phases.code_review.status,
|
|
1289
|
+
test: .phases.test.status,
|
|
1290
|
+
archive: .phases.archive.status
|
|
1291
|
+
}
|
|
1292
|
+
}' "$STATE_FILE"
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
gate_bind() {
|
|
1296
|
+
local session_id="$1"
|
|
1297
|
+
local target_change="$2"
|
|
1298
|
+
|
|
1299
|
+
if [ -z "$session_id" ] || [ -z "$target_change" ]; then
|
|
1300
|
+
fail "需要 <session_id> <change_name>"
|
|
1301
|
+
fi
|
|
1302
|
+
|
|
1303
|
+
local state_file="$PROJECT_ROOT/specline/changes/$target_change/.pipeline-state.json"
|
|
1304
|
+
if [ ! -f "$state_file" ]; then
|
|
1305
|
+
fail "Change '$target_change' 不存在"
|
|
1306
|
+
fi
|
|
1307
|
+
|
|
1308
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
1309
|
+
[ ! -f "$bindings_file" ] && echo '{}' > "$bindings_file"
|
|
1310
|
+
|
|
1311
|
+
local now
|
|
1312
|
+
now=$(now_iso8601)
|
|
1313
|
+
jq --arg sid "$session_id" --arg change "$target_change" --arg time "$now" \
|
|
1314
|
+
'.[$sid] = {"change": $change, "bound_at": $time}' \
|
|
1315
|
+
"$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
1316
|
+
|
|
1317
|
+
echo "✅ 已绑定 session '$session_id' → pipeline '$target_change'"
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
# ===== Semantic Gate — 跨文件语义检查 =====
|
|
1321
|
+
gate_semantic() {
|
|
1322
|
+
if [ -z "$CHANGE" ]; then
|
|
1323
|
+
fail "需要 --change <name>"
|
|
1324
|
+
fi
|
|
1325
|
+
|
|
1326
|
+
# 定位 spec.md 和 tasks.md
|
|
1327
|
+
local spec_file tasks_file
|
|
1328
|
+
spec_file=$(find_spec_file)
|
|
1329
|
+
tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
|
|
1330
|
+
|
|
1331
|
+
if [ ! -f "$spec_file" ] || [ ! -f "$tasks_file" ]; then
|
|
1332
|
+
fail "spec.md 或 tasks.md 不存在,无法执行语义检查"
|
|
1333
|
+
fi
|
|
1334
|
+
|
|
1335
|
+
local checks_dir="$SCRIPT_DIR/pipeline-gate-checks"
|
|
1336
|
+
local common_sh="$checks_dir/common.sh"
|
|
1337
|
+
|
|
1338
|
+
if [ ! -f "$common_sh" ]; then
|
|
1339
|
+
fail "common.sh 不存在: $common_sh"
|
|
1340
|
+
fi
|
|
1341
|
+
|
|
1342
|
+
# source common.sh 初始化计数器
|
|
1343
|
+
source "$common_sh"
|
|
1344
|
+
|
|
1345
|
+
# 设定文件路径环境变量,供各检查脚本使用
|
|
1346
|
+
export SPEC_FILE="$spec_file"
|
|
1347
|
+
export TASKS_FILE="$tasks_file"
|
|
1348
|
+
|
|
1349
|
+
# 依次执行 6 项语义检查
|
|
1350
|
+
local check_scripts=(
|
|
1351
|
+
"a1-covers-ref.sh"
|
|
1352
|
+
"d1-cycle.sh"
|
|
1353
|
+
"c1-exception.sh"
|
|
1354
|
+
"c2-vague.sh"
|
|
1355
|
+
"a2-a3-reverse.sh"
|
|
1356
|
+
"d3-type-file.sh"
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
local check_functions=(
|
|
1360
|
+
"run_a1_covers_ref"
|
|
1361
|
+
"run_d1_cycle"
|
|
1362
|
+
"run_c1_exception"
|
|
1363
|
+
"run_c2_vague"
|
|
1364
|
+
"run_a2_a3_reverse"
|
|
1365
|
+
"run_d3_type_file"
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
local i=0
|
|
1369
|
+
for script in "${check_scripts[@]}"; do
|
|
1370
|
+
local script_path="$checks_dir/$script"
|
|
1371
|
+
if [ -f "$script_path" ]; then
|
|
1372
|
+
source "$script_path"
|
|
1373
|
+
if declare -f "${check_functions[$i]}" > /dev/null 2>&1; then
|
|
1374
|
+
"${check_functions[$i]}"
|
|
1375
|
+
fi
|
|
1376
|
+
else
|
|
1377
|
+
echo "⚠️ 检查脚本不存在,跳过: $script"
|
|
1378
|
+
fi
|
|
1379
|
+
i=$((i + 1))
|
|
1380
|
+
done
|
|
1381
|
+
|
|
1382
|
+
# 汇总结果
|
|
1383
|
+
local total_issues=$((SEMANTIC_ERRORS + SEMANTIC_WARNINGS + SEMANTIC_INFOS))
|
|
1384
|
+
|
|
1385
|
+
echo ""
|
|
1386
|
+
echo "========== Semantic Gate 汇总 =========="
|
|
1387
|
+
echo " ❌ 错误: $SEMANTIC_ERRORS"
|
|
1388
|
+
echo " ⚠️ 警告: $SEMANTIC_WARNINGS"
|
|
1389
|
+
echo " ℹ️ 信息: $SEMANTIC_INFOS"
|
|
1390
|
+
echo " 总计: $total_issues"
|
|
1391
|
+
echo "========================================="
|
|
1392
|
+
|
|
1393
|
+
if [ "$SEMANTIC_ERRORS" -gt 0 ]; then
|
|
1394
|
+
echo ""
|
|
1395
|
+
echo "❌ Semantic Gate 未通过:发现 $SEMANTIC_ERRORS 个错误"
|
|
1396
|
+
exit 1
|
|
1397
|
+
fi
|
|
1398
|
+
|
|
1399
|
+
write_gate_passed "phases.spec.gates.semantic_gate"
|
|
1400
|
+
pass "✅ Semantic Gate 全部通过"
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
# ===== 分派 =====
|
|
1404
|
+
|
|
1405
|
+
case "$PHASE" in
|
|
1406
|
+
new)
|
|
1407
|
+
gate_new
|
|
1408
|
+
;;
|
|
1409
|
+
list)
|
|
1410
|
+
gate_list "$@"
|
|
1411
|
+
;;
|
|
1412
|
+
artifacts)
|
|
1413
|
+
gate_artifacts "$@"
|
|
1414
|
+
;;
|
|
1415
|
+
spec)
|
|
1416
|
+
gate_spec
|
|
1417
|
+
;;
|
|
1418
|
+
semantic)
|
|
1419
|
+
gate_semantic "$@"
|
|
1420
|
+
;;
|
|
1421
|
+
build)
|
|
1422
|
+
gate_build
|
|
1423
|
+
;;
|
|
1424
|
+
lint)
|
|
1425
|
+
gate_lint
|
|
1426
|
+
;;
|
|
1427
|
+
test-unit)
|
|
1428
|
+
gate_test_unit
|
|
1429
|
+
;;
|
|
1430
|
+
test-integration)
|
|
1431
|
+
gate_test_integration
|
|
1432
|
+
;;
|
|
1433
|
+
test-e2e)
|
|
1434
|
+
gate_test_e2e
|
|
1435
|
+
;;
|
|
1436
|
+
bind)
|
|
1437
|
+
gate_bind "${POSITIONAL_ARGS[0]:-}" "${POSITIONAL_ARGS[1]:-}"
|
|
1438
|
+
;;
|
|
1439
|
+
detect-modules)
|
|
1440
|
+
load_project_config
|
|
1441
|
+
echo "$MODULES_JSON"
|
|
1442
|
+
;;
|
|
1443
|
+
archive)
|
|
1444
|
+
gate_archive "$@"
|
|
1445
|
+
;;
|
|
1446
|
+
status)
|
|
1447
|
+
gate_status
|
|
1448
|
+
;;
|
|
1449
|
+
*)
|
|
1450
|
+
echo "未知 phase: $PHASE"
|
|
1451
|
+
echo "可用: new | list | artifacts | spec | semantic | build | lint | test-unit | test-integration | test-e2e | detect-modules | bind | archive | status"
|
|
1452
|
+
exit 2
|
|
1453
|
+
;;
|
|
1454
|
+
esac
|
|
1455
|
+
|
|
1456
|
+
exit 0
|