specline 2.0.1 → 2.0.2
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/core/agents/specline-spec-creator.yaml +16 -0
- package/core/agents/specline-spec-reviewer.yaml +14 -2
- package/core/skills/specline-pipeline/SKILL.md +63 -9
- package/package.json +1 -1
- package/templates/.cursor/README.md +0 -18
- package/templates/.cursor/agents/specline-backend-dev.md +0 -47
- package/templates/.cursor/agents/specline-code-reviewer.md +0 -110
- package/templates/.cursor/agents/specline-config-dev.md +0 -52
- package/templates/.cursor/agents/specline-config-reviewer.md +0 -79
- package/templates/.cursor/agents/specline-explore-assistant.md +0 -81
- package/templates/.cursor/agents/specline-frontend-dev.md +0 -47
- package/templates/.cursor/agents/specline-spec-creator.md +0 -376
- package/templates/.cursor/agents/specline-spec-reviewer.md +0 -144
- package/templates/.cursor/agents/specline-test-runner.md +0 -107
- package/templates/.cursor/agents/specline-test-writer.md +0 -170
- package/templates/.cursor/hooks/specline-agent-guard.sh +0 -131
- package/templates/.cursor/hooks/specline-auto-format.sh +0 -12
- package/templates/.cursor/hooks/specline-phase-guard.sh +0 -201
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a1-covers-ref.sh +0 -125
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a2-a3-reverse.sh +0 -171
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/c1-exception.sh +0 -71
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/c2-vague.sh +0 -60
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/common.sh +0 -68
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/d1-cycle.sh +0 -149
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/d3-type-file.sh +0 -260
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +0 -1569
- package/templates/.cursor/hooks/specline-reminder.sh +0 -147
- package/templates/.cursor/hooks/specline-session-start.sh +0 -259
- package/templates/.cursor/hooks/specline-shell-guard.sh +0 -18
- package/templates/.cursor/hooks.json +0 -46
- package/templates/.cursor/skills/specline-apply-change/SKILL.md +0 -197
- package/templates/.cursor/skills/specline-archive-change/SKILL.md +0 -173
- package/templates/.cursor/skills/specline-explore/SKILL.md +0 -504
- package/templates/.cursor/skills/specline-knowledge/SKILL.md +0 -539
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +0 -616
- package/templates/.cursor/skills/specline-pipeline/references/error-recovery-details.md +0 -49
- package/templates/.cursor/skills/specline-pipeline/references/event-log-spec.md +0 -59
- package/templates/.cursor/skills/specline-pipeline/references/pipeline-state-schema.md +0 -87
- package/templates/.cursor/skills/specline-pipeline/templates/subagent-prompts.md +0 -253
- package/templates/.cursor/skills/specline-propose/SKILL.md +0 -186
- package/templates/.cursor/skills/specline-quickfix/SKILL.md +0 -265
- package/templates/specline/config.yaml +0 -64
|
@@ -1,1569 +0,0 @@
|
|
|
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
|
-
# macOS sed 兼容
|
|
551
|
-
sed -i '' "s/CHANGE_NAME_PLACEHOLDER/$CHANGE/g" "$change_dir/.pipeline-state.json"
|
|
552
|
-
sed -i '' "s/CREATED_AT_PLACEHOLDER/$now/g" "$change_dir/.pipeline-state.json"
|
|
553
|
-
|
|
554
|
-
echo "✅ Change '$CHANGE' 已创建: $change_dir"
|
|
555
|
-
echo " .specline.yaml + .pipeline-state.json + specs/"
|
|
556
|
-
|
|
557
|
-
write_gate_passed "phases.spec.gates.spec_gate"
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
gate_list() {
|
|
561
|
-
local changes_dir="$PROJECT_ROOT/specline/changes"
|
|
562
|
-
local json_output=false
|
|
563
|
-
|
|
564
|
-
if [ "${1:-}" = "--json" ]; then
|
|
565
|
-
json_output=true
|
|
566
|
-
fi
|
|
567
|
-
|
|
568
|
-
if [ ! -d "$changes_dir" ]; then
|
|
569
|
-
if $json_output; then
|
|
570
|
-
echo '[]'
|
|
571
|
-
else
|
|
572
|
-
echo "(无活跃 change)"
|
|
573
|
-
fi
|
|
574
|
-
exit 0
|
|
575
|
-
fi
|
|
576
|
-
|
|
577
|
-
if $json_output; then
|
|
578
|
-
echo "["
|
|
579
|
-
local first=true
|
|
580
|
-
for f in "$changes_dir"/*/.pipeline-state.json; do
|
|
581
|
-
[ -f "$f" ] || continue
|
|
582
|
-
# 跳过 archive/
|
|
583
|
-
if echo "$f" | grep -q "/archive/"; then continue; fi
|
|
584
|
-
local dir name phase
|
|
585
|
-
dir=$(dirname "$f")
|
|
586
|
-
name=$(basename "$dir")
|
|
587
|
-
phase=$(jq -r '.current_phase // "unknown"' "$f" 2>/dev/null)
|
|
588
|
-
if [ "$first" = true ]; then first=false; else echo ","; fi
|
|
589
|
-
echo " {\"name\":\"$name\",\"phase\":\"$phase\"}"
|
|
590
|
-
done
|
|
591
|
-
echo "]"
|
|
592
|
-
else
|
|
593
|
-
for f in "$changes_dir"/*/.pipeline-state.json; do
|
|
594
|
-
[ -f "$f" ] || continue
|
|
595
|
-
if echo "$f" | grep -q "/archive/"; then continue; fi
|
|
596
|
-
local dir name phase
|
|
597
|
-
dir=$(dirname "$f")
|
|
598
|
-
name=$(basename "$dir")
|
|
599
|
-
phase=$(jq -r '.current_phase // "unknown"' "$f" 2>/dev/null)
|
|
600
|
-
echo " $name (phase: $phase)"
|
|
601
|
-
done
|
|
602
|
-
fi
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
gate_artifacts() {
|
|
606
|
-
if [ -z "$CHANGE" ]; then
|
|
607
|
-
fail "需要 --change <name>"
|
|
608
|
-
fi
|
|
609
|
-
|
|
610
|
-
local dir="$PROJECT_ROOT/specline/changes/$CHANGE"
|
|
611
|
-
local json_output=false
|
|
612
|
-
|
|
613
|
-
if [ "${1:-}" = "--json" ]; then
|
|
614
|
-
json_output=true
|
|
615
|
-
fi
|
|
616
|
-
|
|
617
|
-
local has_proposal=false has_design=false has_tasks=false has_specs=false
|
|
618
|
-
|
|
619
|
-
[ -f "$dir/proposal.md" ] && has_proposal=true
|
|
620
|
-
[ -f "$dir/design.md" ] && has_design=true
|
|
621
|
-
[ -f "$dir/tasks.md" ] && has_tasks=true
|
|
622
|
-
[ -d "$dir/specs" ] && [ -n "$(find "$dir/specs" -name 'spec.md' 2>/dev/null)" ] && has_specs=true
|
|
623
|
-
|
|
624
|
-
if $json_output; then
|
|
625
|
-
echo "{"
|
|
626
|
-
echo " \"proposal\": $has_proposal,"
|
|
627
|
-
echo " \"design\": $has_design,"
|
|
628
|
-
echo " \"tasks\": $has_tasks,"
|
|
629
|
-
echo " \"specs\": $has_specs"
|
|
630
|
-
echo "}"
|
|
631
|
-
else
|
|
632
|
-
echo "Artifacts for '$CHANGE':"
|
|
633
|
-
echo " proposal.md: $has_proposal"
|
|
634
|
-
echo " design.md: $has_design"
|
|
635
|
-
echo " tasks.md: $has_tasks"
|
|
636
|
-
echo " spec.md: $has_specs"
|
|
637
|
-
fi
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
gate_spec() {
|
|
641
|
-
local spec_file
|
|
642
|
-
spec_file=$(find_spec_file)
|
|
643
|
-
|
|
644
|
-
if [ -z "$spec_file" ] || [ ! -f "$spec_file" ]; then
|
|
645
|
-
fail "spec.md 不存在。请确保 spec-creator 已生成 spec 文件。"
|
|
646
|
-
fi
|
|
647
|
-
|
|
648
|
-
# 1. H1 含 "Specification"
|
|
649
|
-
if ! grep -q "^# .* Specification" "$spec_file"; then
|
|
650
|
-
fail "标题格式错误:H1 必须包含 'Specification' 关键词"
|
|
651
|
-
fi
|
|
652
|
-
|
|
653
|
-
# 2. 含 Purpose 章节
|
|
654
|
-
if ! grep -q "^## Purpose" "$spec_file"; then
|
|
655
|
-
fail "缺少 ## Purpose 章节"
|
|
656
|
-
fi
|
|
657
|
-
|
|
658
|
-
# 3. 含 Requirements 章节
|
|
659
|
-
if ! grep -q "^## Requirements" "$spec_file"; then
|
|
660
|
-
fail "缺少 ## Requirements 章节"
|
|
661
|
-
fi
|
|
662
|
-
|
|
663
|
-
# 4. 至少 1 个 Requirement
|
|
664
|
-
local req_count
|
|
665
|
-
req_count=$(grep -c "^### Requirement:" "$spec_file" || echo "0")
|
|
666
|
-
if [ "$req_count" -lt 1 ]; then
|
|
667
|
-
fail "至少需要 1 个 Requirement,当前: $req_count"
|
|
668
|
-
fi
|
|
669
|
-
pass "Requirements 数量: $req_count"
|
|
670
|
-
|
|
671
|
-
# 5. 每个 Requirement 至少 1 个 Scenario(简化检查:Scenario 总数 >= Requirement 总数)
|
|
672
|
-
local scenario_count
|
|
673
|
-
scenario_count=$(grep -c "^#### Scenario:" "$spec_file" || echo "0")
|
|
674
|
-
if [ "$scenario_count" -lt "$req_count" ]; then
|
|
675
|
-
fail "每个 Requirement 至少需要 1 个 Scenario。Requirement: $req_count, Scenario: $scenario_count"
|
|
676
|
-
fi
|
|
677
|
-
pass "Scenario 数量: $scenario_count"
|
|
678
|
-
|
|
679
|
-
# 6. WHEN/THEN 语义检查(每个 Scenario 至少 1 WHEN + 1 THEN)
|
|
680
|
-
local bad_scenarios=""
|
|
681
|
-
local current_scenario="" has_when=0 has_then=0
|
|
682
|
-
|
|
683
|
-
while IFS= read -r line; do
|
|
684
|
-
if [[ "$line" =~ ^####\ Scenario: ]]; then
|
|
685
|
-
if [ -n "$current_scenario" ] && { [ "$has_when" -eq 0 ] || [ "$has_then" -eq 0 ]; }; then
|
|
686
|
-
bad_scenarios="${bad_scenarios}\n - ${current_scenario} (WHEN=${has_when}, THEN=${has_then})"
|
|
687
|
-
fi
|
|
688
|
-
current_scenario="${line#*Scenario: }"
|
|
689
|
-
has_when=0; has_then=0
|
|
690
|
-
fi
|
|
691
|
-
[[ "$line" == *'**WHEN**'* ]] && ((has_when++)) || true
|
|
692
|
-
[[ "$line" == *'**THEN**'* ]] && ((has_then++)) || true
|
|
693
|
-
done < "$spec_file"
|
|
694
|
-
|
|
695
|
-
if [ -n "$current_scenario" ] && { [ "$has_when" -eq 0 ] || [ "$has_then" -eq 0 ]; }; then
|
|
696
|
-
bad_scenarios="${bad_scenarios}\n - ${current_scenario} (WHEN=${has_when}, THEN=${has_then})"
|
|
697
|
-
fi
|
|
698
|
-
|
|
699
|
-
if [ -n "$bad_scenarios" ]; then
|
|
700
|
-
fail "以下 Scenario 缺少 WHEN 或 THEN:${bad_scenarios}"
|
|
701
|
-
fi
|
|
702
|
-
pass "WHEN/THEN 语义检查通过 (每个 Scenario 至少 1 WHEN + 1 THEN)"
|
|
703
|
-
|
|
704
|
-
# 7. review.json 状态检查(如果存在)
|
|
705
|
-
local review_file
|
|
706
|
-
review_file="$(dirname "$spec_file")/spec-review.json"
|
|
707
|
-
if [ -f "$review_file" ]; then
|
|
708
|
-
local review_status
|
|
709
|
-
review_status=$(jq -r '.status' "$review_file" 2>/dev/null || echo "missing")
|
|
710
|
-
if [ "$review_status" != "approved" ]; then
|
|
711
|
-
fail "spec-review.json 审核未通过 (status: $review_status)"
|
|
712
|
-
fi
|
|
713
|
-
pass "审核状态: approved"
|
|
714
|
-
|
|
715
|
-
# 7b. 检查 coverage(所有 Requirement 和 Scenario 被 task 的 Covers 覆盖)
|
|
716
|
-
local cov_req_total cov_req_covered
|
|
717
|
-
cov_req_total=$(jq -r '.coverage.requirements_total' "$review_file" 2>/dev/null || echo "0")
|
|
718
|
-
cov_req_covered=$(jq -r '.coverage.requirements_covered' "$review_file" 2>/dev/null || echo "0")
|
|
719
|
-
if [ "$cov_req_covered" -lt "$cov_req_total" ]; then
|
|
720
|
-
fail "Requirement 覆盖不全: $cov_req_covered/$cov_req_total"
|
|
721
|
-
fi
|
|
722
|
-
pass "Requirement 覆盖率: $cov_req_covered/$cov_req_total"
|
|
723
|
-
else
|
|
724
|
-
pass "审核状态: 无 spec-review.json(跳过审核检查)"
|
|
725
|
-
fi
|
|
726
|
-
|
|
727
|
-
# 8. 检查 tasks.md 是否存在且含完整的 Type/Depends/Covers/Files 标注
|
|
728
|
-
local tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
|
|
729
|
-
if [ ! -f "$tasks_file" ]; then
|
|
730
|
-
fail "tasks.md 不存在"
|
|
731
|
-
fi
|
|
732
|
-
pass "tasks.md 存在"
|
|
733
|
-
|
|
734
|
-
# 9. 检查每个任务标注完整性
|
|
735
|
-
local task_count type_count deps_count covers_count files_count
|
|
736
|
-
task_count=$(grep -c '^## ' "$tasks_file" || echo "0")
|
|
737
|
-
type_count=$(grep -c '\*\*Type\*\*:' "$tasks_file" || echo "0")
|
|
738
|
-
deps_count=$(grep -c '\*\*Depends\*\*:' "$tasks_file" || echo "0")
|
|
739
|
-
covers_count=$(grep -c '\*\*Covers\*\*:' "$tasks_file" || echo "0")
|
|
740
|
-
files_count=$(grep -c '\*\*Files\*\*:' "$tasks_file" || echo "0")
|
|
741
|
-
|
|
742
|
-
if [ "$type_count" -lt "$task_count" ] || [ "$deps_count" -lt "$task_count" ] || \
|
|
743
|
-
[ "$covers_count" -lt "$task_count" ] || [ "$files_count" -lt "$task_count" ]; then
|
|
744
|
-
fail "tasks.md 标注不完整:任务=$task_count, Type=$type_count, Depends=$deps_count, Covers=$covers_count, Files=$files_count"
|
|
745
|
-
fi
|
|
746
|
-
pass "tasks.md 标注完整性检查通过 ($task_count 个任务)"
|
|
747
|
-
|
|
748
|
-
# 10. Testable 字段校验
|
|
749
|
-
local testable_count
|
|
750
|
-
testable_count=$(grep -c '\*\*Testable\*\*:' "$tasks_file" || echo "0")
|
|
751
|
-
|
|
752
|
-
if [ "$testable_count" -eq 0 ]; then
|
|
753
|
-
echo "⚠️ Testable 标注缺失(向后兼容模式:缺失字段的任务将被视为 Testable: false)"
|
|
754
|
-
elif [ "$testable_count" -gt 0 ] && [ "$testable_count" -lt "$task_count" ]; then
|
|
755
|
-
local missing_testable_tasks
|
|
756
|
-
missing_testable_tasks=$(awk '
|
|
757
|
-
/^## / {
|
|
758
|
-
if (in_task && !has_testable) missing = missing (missing ? ", " : "") prev_task
|
|
759
|
-
prev_task = $2; gsub(/\..*/, "", prev_task)
|
|
760
|
-
in_task = 1; has_testable = 0
|
|
761
|
-
}
|
|
762
|
-
/\*\*Testable\*\*:/ { has_testable = 1 }
|
|
763
|
-
END {
|
|
764
|
-
if (in_task && !has_testable) missing = missing (missing ? ", " : "") prev_task
|
|
765
|
-
print missing
|
|
766
|
-
}' "$tasks_file")
|
|
767
|
-
echo "⚠️ Testable 标注不完整:任务=$task_count, Testable=$testable_count(缺失任务: $missing_testable_tasks;缺失字段的任务将被视为 Testable: false)"
|
|
768
|
-
else
|
|
769
|
-
pass "Testable 标注完整性检查通过 ($testable_count/$task_count)"
|
|
770
|
-
fi
|
|
771
|
-
|
|
772
|
-
# 11. 至少 1 个任务无依赖
|
|
773
|
-
local independent_count
|
|
774
|
-
independent_count=$(grep -c '\*\*Depends\*\*: (none)' "$tasks_file" || echo "0")
|
|
775
|
-
if [ "$independent_count" -lt 1 ]; then
|
|
776
|
-
fail "至少需要 1 个无依赖任务 (Depends: none),当前: $independent_count"
|
|
777
|
-
fi
|
|
778
|
-
pass "无依赖任务数: $independent_count"
|
|
779
|
-
|
|
780
|
-
write_gate_passed "phases.spec.gates.spec_gate"
|
|
781
|
-
pass "Spec Gate 全部通过"
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
# ===== 对外接口契约签名检查 =====
|
|
785
|
-
# 以 tasks.md 为决策源头:有 test-writer 任务才需要契约,有契约才检查签名存在性
|
|
786
|
-
check_contract_signatures() {
|
|
787
|
-
local tasks_file="$1"
|
|
788
|
-
local design_file="$PROJECT_ROOT/specline/changes/$CHANGE/design.md"
|
|
789
|
-
|
|
790
|
-
# 1. 判断是否需要契约:检查 tasks.md 末尾「测试文件归属」表格中是否有 specline-test-writer 负责的集成/E2E 测试
|
|
791
|
-
local has_test_writer=false
|
|
792
|
-
if grep -q 'specline-test-writer' "$tasks_file" 2>/dev/null; then
|
|
793
|
-
has_test_writer=true
|
|
794
|
-
fi
|
|
795
|
-
|
|
796
|
-
if [ "$has_test_writer" = false ]; then
|
|
797
|
-
echo "ℹ️ tasks.md 中无 specline-test-writer 负责的测试任务,跳过契约检查"
|
|
798
|
-
return 0
|
|
799
|
-
fi
|
|
800
|
-
|
|
801
|
-
# 2. 有 test-writer 任务 → 设计文档必须有契约章节
|
|
802
|
-
if ! grep -q '^## 对外接口契约' "$design_file" 2>/dev/null; then
|
|
803
|
-
fail "tasks.md 中存在 specline-test-writer 负责的集成/E2E 测试任务,但 design.md 缺少「对外接口契约」章节。请运行 /specline-pipeline propose 重新生成,或手动补充"
|
|
804
|
-
fi
|
|
805
|
-
|
|
806
|
-
echo "正在检查对外接口契约签名..."
|
|
807
|
-
|
|
808
|
-
local contract_errors=""
|
|
809
|
-
|
|
810
|
-
# 3. 解析契约章节中的 CLI 命令,检查命令注册代码是否存在
|
|
811
|
-
local in_cli_section=false
|
|
812
|
-
while IFS= read -r line; do
|
|
813
|
-
# 检测章节边界
|
|
814
|
-
if echo "$line" | grep -q '^### CLI'; then
|
|
815
|
-
in_cli_section=true
|
|
816
|
-
continue
|
|
817
|
-
elif echo "$line" | grep -q '^### HTTP'; then
|
|
818
|
-
in_cli_section=false
|
|
819
|
-
continue
|
|
820
|
-
fi
|
|
821
|
-
|
|
822
|
-
if [ "$in_cli_section" = true ] && echo "$line" | grep -q '^|.*|.*|.*|.*|.*|$'; then
|
|
823
|
-
local cli_cmd
|
|
824
|
-
cli_cmd=$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}')
|
|
825
|
-
# 跳过表头行
|
|
826
|
-
if [ "$cli_cmd" != "命令" ] && [ -n "$cli_cmd" ]; then
|
|
827
|
-
# grep 搜索命令注册(CLI 命令通常在 specline-pipeline-gate.sh 或其他 .sh 文件中有 case 分支)
|
|
828
|
-
if ! grep -rq "$cli_cmd" "$PROJECT_ROOT" --include="*.sh" --include="*.py" --include="*.ts" --include="*.go" 2>/dev/null; then
|
|
829
|
-
contract_errors="${contract_errors}
|
|
830
|
-
CLI 命令 '$cli_cmd' 在契约中定义,但未在代码中找到注册(搜索了 .sh/.py/.ts/.go 文件)"
|
|
831
|
-
fi
|
|
832
|
-
fi
|
|
833
|
-
fi
|
|
834
|
-
done < <(awk '/^## 对外接口契约/,/^## [^对]/' "$design_file" 2>/dev/null)
|
|
835
|
-
|
|
836
|
-
# 4. 解析契约章节中的 HTTP 端点,检查路由注册代码是否存在
|
|
837
|
-
local in_http_section=false
|
|
838
|
-
while IFS= read -r line; do
|
|
839
|
-
if echo "$line" | grep -q '^### HTTP'; then
|
|
840
|
-
in_http_section=true
|
|
841
|
-
continue
|
|
842
|
-
elif echo "$line" | grep -qE '^### (模块导出|CLI)'; then
|
|
843
|
-
in_http_section=false
|
|
844
|
-
continue
|
|
845
|
-
fi
|
|
846
|
-
|
|
847
|
-
if [ "$in_http_section" = true ] && echo "$line" | grep -q '^|.*|.*|.*|.*|.*|$'; then
|
|
848
|
-
local http_path
|
|
849
|
-
http_path=$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $3); print $3}')
|
|
850
|
-
# 跳过表头行
|
|
851
|
-
if [ "$http_path" != "路径" ] && [ -n "$http_path" ]; then
|
|
852
|
-
# 去掉路径参数中的动态段(如 /api/users/:id → /api/users/ 前缀匹配)
|
|
853
|
-
local static_prefix
|
|
854
|
-
static_prefix=$(echo "$http_path" | sed 's/:[a-zA-Z_][a-zA-Z0-9_]*/FINDANY/' | sed 's/\/FINDANY//g')
|
|
855
|
-
if ! grep -rq "$static_prefix" "$PROJECT_ROOT" --include="*.py" --include="*.ts" --include="*.tsx" --include="*.go" --include="*.rs" 2>/dev/null; then
|
|
856
|
-
contract_errors="${contract_errors}
|
|
857
|
-
HTTP 路径 '$http_path' 在契约中定义,但未在代码中找到路由注册(搜索了 .py/.ts/.tsx/.go/.rs 文件)"
|
|
858
|
-
fi
|
|
859
|
-
fi
|
|
860
|
-
fi
|
|
861
|
-
done < <(awk '/^## 对外接口契约/,/^## [^对]/' "$design_file" 2>/dev/null)
|
|
862
|
-
|
|
863
|
-
# 5. 解析契约章节中的模块导出
|
|
864
|
-
local in_exports_section=false
|
|
865
|
-
while IFS= read -r line; do
|
|
866
|
-
if echo "$line" | grep -q '^### 模块导出'; then
|
|
867
|
-
in_exports_section=true
|
|
868
|
-
continue
|
|
869
|
-
elif echo "$line" | grep -qE '^## [^对]' && [ "$in_exports_section" = true ]; then
|
|
870
|
-
break
|
|
871
|
-
fi
|
|
872
|
-
|
|
873
|
-
if [ "$in_exports_section" = true ] && echo "$line" | grep -q '^|.*|.*|.*|.*|$'; then
|
|
874
|
-
local export_file export_name
|
|
875
|
-
export_file=$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}')
|
|
876
|
-
export_name=$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $3); print $3}')
|
|
877
|
-
# 跳过表头行
|
|
878
|
-
if [ "$export_file" != "模块文件" ] && [ -n "$export_file" ] && [ -n "$export_name" ]; then
|
|
879
|
-
if [ ! -f "$PROJECT_ROOT/$export_file" ]; then
|
|
880
|
-
contract_errors="${contract_errors}
|
|
881
|
-
模块文件 '$export_file' 在契约中定义,但文件不存在"
|
|
882
|
-
elif ! grep -q "export.*$export_name\b\|func $export_name\b\|def $export_name\b\|pub fn $export_name\b" "$PROJECT_ROOT/$export_file" 2>/dev/null; then
|
|
883
|
-
contract_errors="${contract_errors}
|
|
884
|
-
导出符号 '$export_name' 在契约中定义,但未在 '$export_file' 中找到对应声明"
|
|
885
|
-
fi
|
|
886
|
-
fi
|
|
887
|
-
fi
|
|
888
|
-
done < <(awk '/^## 对外接口契约/,/^## [^对]/' "$design_file" 2>/dev/null)
|
|
889
|
-
|
|
890
|
-
if [ -n "$contract_errors" ]; then
|
|
891
|
-
fail "对外接口契约签名不一致:${contract_errors}"
|
|
892
|
-
fi
|
|
893
|
-
|
|
894
|
-
pass "对外接口契约签名检查通过"
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
gate_build() {
|
|
898
|
-
load_project_config
|
|
899
|
-
|
|
900
|
-
local module_count
|
|
901
|
-
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
902
|
-
|
|
903
|
-
if [ "$module_count" -eq 0 ]; then
|
|
904
|
-
echo "⚠️ 未检测到项目模块,跳过 build 命令"
|
|
905
|
-
else
|
|
906
|
-
local i=0
|
|
907
|
-
while [ "$i" -lt "$module_count" ]; do
|
|
908
|
-
local path lang
|
|
909
|
-
path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
|
|
910
|
-
lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
|
|
911
|
-
build_module "$path" "$lang"
|
|
912
|
-
i=$((i + 1))
|
|
913
|
-
done
|
|
914
|
-
pass "模块 build 检查通过 ($module_count 个模块)"
|
|
915
|
-
fi
|
|
916
|
-
|
|
917
|
-
# Agent 产出 JSON 验证(task-result.json files_changed / files)
|
|
918
|
-
if [ -n "$CHANGE" ]; then
|
|
919
|
-
local task_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/task-result.json"
|
|
920
|
-
if [ -f "$task_result" ]; then
|
|
921
|
-
echo "正在验证 task-result.json 声明的文件..."
|
|
922
|
-
local missing_files=""
|
|
923
|
-
while IFS= read -r f; do
|
|
924
|
-
[ -z "$f" ] && continue
|
|
925
|
-
if [ ! -f "$PROJECT_ROOT/$f" ]; then
|
|
926
|
-
missing_files="${missing_files}
|
|
927
|
-
- $f"
|
|
928
|
-
fi
|
|
929
|
-
done < <(jq -r '(.files_changed // .files // [])[]?' "$task_result" 2>/dev/null)
|
|
930
|
-
|
|
931
|
-
if [ -n "$missing_files" ]; then
|
|
932
|
-
fail "task-result.json 声明的文件不存在:${missing_files}"
|
|
933
|
-
fi
|
|
934
|
-
pass "task-result.json 文件验证通过"
|
|
935
|
-
fi
|
|
936
|
-
fi
|
|
937
|
-
|
|
938
|
-
# 单元测试文件存在性检查(Testable=true 任务)
|
|
939
|
-
local tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
|
|
940
|
-
if [ -f "$tasks_file" ]; then
|
|
941
|
-
local testable_true_count
|
|
942
|
-
testable_true_count=$(grep -c '\*\*Testable\*\*:.*true' "$tasks_file" || echo "0")
|
|
943
|
-
|
|
944
|
-
if [ "$testable_true_count" -gt 0 ]; then
|
|
945
|
-
echo "正在检查 $testable_true_count 个 Testable=true 任务的单元测试文件..."
|
|
946
|
-
|
|
947
|
-
local missing_files=""
|
|
948
|
-
local syntax_errors=""
|
|
949
|
-
|
|
950
|
-
while IFS='|' read -r task_id file_path; do
|
|
951
|
-
if [ -z "$file_path" ]; then
|
|
952
|
-
missing_files="${missing_files}
|
|
953
|
-
任务 $task_id: 未在 Files 列表中声明测试文件(支持 tests/unit/、tests/models/、*_test.go、*.test.ts 等)"
|
|
954
|
-
continue
|
|
955
|
-
fi
|
|
956
|
-
|
|
957
|
-
if [ ! -f "$PROJECT_ROOT/$file_path" ]; then
|
|
958
|
-
missing_files="${missing_files}
|
|
959
|
-
任务 $task_id: $file_path"
|
|
960
|
-
continue
|
|
961
|
-
fi
|
|
962
|
-
|
|
963
|
-
case "$file_path" in
|
|
964
|
-
*.py)
|
|
965
|
-
if ! python3 -m py_compile "$PROJECT_ROOT/$file_path" 2>&1; then
|
|
966
|
-
syntax_errors="${syntax_errors}
|
|
967
|
-
任务 $task_id: $file_path (Python 语法错误)"
|
|
968
|
-
fi
|
|
969
|
-
;;
|
|
970
|
-
*.ts|*.tsx)
|
|
971
|
-
if ! npx tsc --noEmit "$PROJECT_ROOT/$file_path" 2>&1; then
|
|
972
|
-
syntax_errors="${syntax_errors}
|
|
973
|
-
任务 $task_id: $file_path (TypeScript 语法错误)"
|
|
974
|
-
fi
|
|
975
|
-
;;
|
|
976
|
-
esac
|
|
977
|
-
done < <(awk '
|
|
978
|
-
/^## / {
|
|
979
|
-
task_id = $2; gsub(/\..*/, "", task_id)
|
|
980
|
-
testable = ""; files_line = ""
|
|
981
|
-
}
|
|
982
|
-
/\*\*Testable\*\*:.*true/ { testable = "true" }
|
|
983
|
-
/\*\*Files\*\*:/ {
|
|
984
|
-
if (testable == "true") {
|
|
985
|
-
files_line = $0
|
|
986
|
-
gsub(/.*\*\*Files\*\*:[ \t]*/, "", files_line)
|
|
987
|
-
split(files_line, paths, /,[ \t]*/)
|
|
988
|
-
has_test = 0
|
|
989
|
-
for (i in paths) {
|
|
990
|
-
gsub(/^[ \t]+|[ \t]+$/, "", paths[i])
|
|
991
|
-
if (paths[i] ~ /^tests\/(unit|models)\// ||
|
|
992
|
-
paths[i] ~ /_test\.go$/ ||
|
|
993
|
-
paths[i] ~ /\.test\.(ts|tsx|js|jsx)$/ ||
|
|
994
|
-
paths[i] ~ /\.spec\.(ts|tsx|js|jsx)$/ ||
|
|
995
|
-
paths[i] ~ /^src\/.*\/tests\.rs$/) {
|
|
996
|
-
print task_id "|" paths[i]
|
|
997
|
-
has_test = 1
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
if (has_test == 0) {
|
|
1001
|
-
print task_id "|"
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
' "$tasks_file")
|
|
1006
|
-
|
|
1007
|
-
if [ -n "$missing_files" ]; then
|
|
1008
|
-
fail "单元测试文件缺失:${missing_files}"
|
|
1009
|
-
fi
|
|
1010
|
-
|
|
1011
|
-
if [ -n "$syntax_errors" ]; then
|
|
1012
|
-
fail "单元测试文件语法错误:${syntax_errors}"
|
|
1013
|
-
fi
|
|
1014
|
-
|
|
1015
|
-
pass "单元测试文件存在性检查通过"
|
|
1016
|
-
else
|
|
1017
|
-
echo "ℹ️ 无 Testable=true 任务,跳过单元测试文件检查"
|
|
1018
|
-
fi
|
|
1019
|
-
fi
|
|
1020
|
-
|
|
1021
|
-
# 对外接口契约检查(以 tasks.md 为决策源头)
|
|
1022
|
-
if [ -f "$tasks_file" ]; then
|
|
1023
|
-
check_contract_signatures "$tasks_file"
|
|
1024
|
-
fi
|
|
1025
|
-
|
|
1026
|
-
write_gate_passed "phases.coding.gates.build_gate"
|
|
1027
|
-
pass "Build Gate 全部通过"
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
gate_lint() {
|
|
1031
|
-
load_project_config
|
|
1032
|
-
|
|
1033
|
-
local module_count
|
|
1034
|
-
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1035
|
-
|
|
1036
|
-
if [ "$module_count" -eq 0 ]; then
|
|
1037
|
-
echo "⚠️ 未检测到项目模块,跳过 lint"
|
|
1038
|
-
else
|
|
1039
|
-
local i=0
|
|
1040
|
-
while [ "$i" -lt "$module_count" ]; do
|
|
1041
|
-
local path lang
|
|
1042
|
-
path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
|
|
1043
|
-
lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
|
|
1044
|
-
lint_module "$path" "$lang"
|
|
1045
|
-
i=$((i + 1))
|
|
1046
|
-
done
|
|
1047
|
-
pass "模块 lint 检查通过 ($module_count 个模块)"
|
|
1048
|
-
fi
|
|
1049
|
-
|
|
1050
|
-
# code-review.json error 计数(位于 change 的 .tmp/ 目录下)
|
|
1051
|
-
local review_file="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/code-review.json"
|
|
1052
|
-
if [ -f "$review_file" ]; then
|
|
1053
|
-
local error_count
|
|
1054
|
-
error_count=$(jq '[.findings[] | select(.severity=="error")] | length' "$review_file" 2>/dev/null || echo "0")
|
|
1055
|
-
if [ "$error_count" -gt 0 ]; then
|
|
1056
|
-
fail "code-review.json 中发现 $error_count 个 error,必须修复"
|
|
1057
|
-
fi
|
|
1058
|
-
pass "Review errors: 0"
|
|
1059
|
-
fi
|
|
1060
|
-
|
|
1061
|
-
write_gate_passed "phases.code_review.gates.lint_gate"
|
|
1062
|
-
pass "Lint Gate 全部通过"
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
# ===== 测试框架自动检测 =====
|
|
1066
|
-
# 优先级:.pipeline-state.json > test-code-result.json > MODULES_JSON 推导 > 无兜底
|
|
1067
|
-
detect_test_framework() {
|
|
1068
|
-
framework="" test_cmd="" coverage_cmd=""
|
|
1069
|
-
|
|
1070
|
-
# 1. 先尝试从状态文件读取 test-writer 的检测结果
|
|
1071
|
-
if [ -f "$STATE_FILE" ]; then
|
|
1072
|
-
local recorded
|
|
1073
|
-
recorded=$(jq -r '.phases.test.framework // empty' "$STATE_FILE" 2>/dev/null)
|
|
1074
|
-
if [ -n "$recorded" ]; then
|
|
1075
|
-
framework="$recorded"
|
|
1076
|
-
fi
|
|
1077
|
-
fi
|
|
1078
|
-
|
|
1079
|
-
# 2. 从 test-code-result.json 读取
|
|
1080
|
-
if [ -z "$framework" ] && [ -n "$CHANGE" ]; then
|
|
1081
|
-
local test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1082
|
-
if [ -f "$test_result" ]; then
|
|
1083
|
-
framework=$(jq -r '.test_framework // empty' "$test_result" 2>/dev/null)
|
|
1084
|
-
test_cmd=$(jq -r '.test_cmd // empty' "$test_result" 2>/dev/null)
|
|
1085
|
-
fi
|
|
1086
|
-
fi
|
|
1087
|
-
|
|
1088
|
-
# 3. 从 MODULES_JSON 推导
|
|
1089
|
-
if [ -z "$framework" ]; then
|
|
1090
|
-
load_project_config
|
|
1091
|
-
local module_count
|
|
1092
|
-
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1093
|
-
if [ "$module_count" -gt 0 ]; then
|
|
1094
|
-
local lang
|
|
1095
|
-
lang=$(echo "$MODULES_JSON" | jq -r '.[0].language')
|
|
1096
|
-
case "$lang" in
|
|
1097
|
-
go) framework="go-test" ;;
|
|
1098
|
-
python) framework="pytest" ;;
|
|
1099
|
-
rust) framework="cargo-test" ;;
|
|
1100
|
-
java) framework="junit" ;;
|
|
1101
|
-
kotlin) framework="junit" ;;
|
|
1102
|
-
typescript|javascript)
|
|
1103
|
-
local mod_path
|
|
1104
|
-
mod_path=$(echo "$MODULES_JSON" | jq -r '.[0].path')
|
|
1105
|
-
local mod_dir
|
|
1106
|
-
mod_dir=$(module_absolute_path "$mod_path")
|
|
1107
|
-
if [ -f "$mod_dir/package.json" ]; then
|
|
1108
|
-
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
1109
|
-
framework="vitest"
|
|
1110
|
-
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
1111
|
-
framework="jest"
|
|
1112
|
-
elif grep -q '"mocha"' "$mod_dir/package.json" 2>/dev/null; then
|
|
1113
|
-
framework="mocha"
|
|
1114
|
-
fi
|
|
1115
|
-
fi
|
|
1116
|
-
;;
|
|
1117
|
-
esac
|
|
1118
|
-
fi
|
|
1119
|
-
fi
|
|
1120
|
-
|
|
1121
|
-
# 4. 无兜底 pytest — 检测失败时 framework 为空
|
|
1122
|
-
if [ -z "$framework" ]; then
|
|
1123
|
-
echo "⚠️ 未检测到测试框架"
|
|
1124
|
-
return 0
|
|
1125
|
-
fi
|
|
1126
|
-
|
|
1127
|
-
# 根据框架确定命令(test_cmd 可能已由 JSON 提供)
|
|
1128
|
-
if [ -z "$test_cmd" ]; then
|
|
1129
|
-
case "$framework" in
|
|
1130
|
-
jest)
|
|
1131
|
-
test_cmd="npx jest"
|
|
1132
|
-
coverage_cmd="npx jest --coverage"
|
|
1133
|
-
;;
|
|
1134
|
-
vitest)
|
|
1135
|
-
test_cmd="npx vitest run"
|
|
1136
|
-
coverage_cmd="npx vitest run --coverage"
|
|
1137
|
-
;;
|
|
1138
|
-
mocha)
|
|
1139
|
-
test_cmd="npx mocha"
|
|
1140
|
-
coverage_cmd="npx nyc mocha"
|
|
1141
|
-
;;
|
|
1142
|
-
go-test)
|
|
1143
|
-
test_cmd="go test ./..."
|
|
1144
|
-
coverage_cmd="go test -cover ./..."
|
|
1145
|
-
;;
|
|
1146
|
-
cargo-test)
|
|
1147
|
-
test_cmd="cargo test"
|
|
1148
|
-
coverage_cmd="cargo tarpaulin 2>/dev/null || cargo test"
|
|
1149
|
-
;;
|
|
1150
|
-
junit)
|
|
1151
|
-
local mod_path mod_dir
|
|
1152
|
-
mod_path=$(echo "$MODULES_JSON" | jq -r '.[0].path // "."')
|
|
1153
|
-
mod_dir=$(module_absolute_path "$mod_path")
|
|
1154
|
-
if [ -f "$mod_dir/pom.xml" ]; then
|
|
1155
|
-
test_cmd="mvn test"
|
|
1156
|
-
coverage_cmd="mvn jacoco:report"
|
|
1157
|
-
else
|
|
1158
|
-
test_cmd="gradle test"
|
|
1159
|
-
coverage_cmd="gradle jacocoTestReport"
|
|
1160
|
-
fi
|
|
1161
|
-
;;
|
|
1162
|
-
pytest)
|
|
1163
|
-
test_cmd="pytest"
|
|
1164
|
-
coverage_cmd="pytest --cov --cov-fail-under=80"
|
|
1165
|
-
;;
|
|
1166
|
-
*)
|
|
1167
|
-
test_cmd=""
|
|
1168
|
-
coverage_cmd=""
|
|
1169
|
-
;;
|
|
1170
|
-
esac
|
|
1171
|
-
fi
|
|
1172
|
-
|
|
1173
|
-
if [ -n "$framework" ] && [ -n "$test_cmd" ]; then
|
|
1174
|
-
echo "检测到测试框架: $framework (命令: $test_cmd)"
|
|
1175
|
-
fi
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
gate_test_unit() {
|
|
1179
|
-
echo "正在执行单元测试..."
|
|
1180
|
-
load_project_config
|
|
1181
|
-
|
|
1182
|
-
local test_result=""
|
|
1183
|
-
if [ -n "$CHANGE" ]; then
|
|
1184
|
-
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1185
|
-
fi
|
|
1186
|
-
|
|
1187
|
-
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1188
|
-
local test_cmd
|
|
1189
|
-
test_cmd=$(jq -r '.test_cmd // empty' "$test_result" 2>/dev/null)
|
|
1190
|
-
|
|
1191
|
-
verify_test_result_files "$test_result" "test_files"
|
|
1192
|
-
|
|
1193
|
-
if [ -n "$test_cmd" ]; then
|
|
1194
|
-
echo "执行 Agent 声明的 test_cmd: $test_cmd"
|
|
1195
|
-
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1196
|
-
fail "单元测试失败"
|
|
1197
|
-
fi
|
|
1198
|
-
else
|
|
1199
|
-
echo "⚠️ test-code-result.json 无 test_cmd,回退到模块默认命令"
|
|
1200
|
-
run_tests_by_modules "unit"
|
|
1201
|
-
fi
|
|
1202
|
-
else
|
|
1203
|
-
local module_count
|
|
1204
|
-
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1205
|
-
if [ "$module_count" -eq 0 ]; then
|
|
1206
|
-
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过单元测试"
|
|
1207
|
-
write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
|
|
1208
|
-
pass "单元测试已跳过"
|
|
1209
|
-
return 0
|
|
1210
|
-
fi
|
|
1211
|
-
run_tests_by_modules "unit"
|
|
1212
|
-
fi
|
|
1213
|
-
|
|
1214
|
-
# 覆盖率检查(非阻塞)
|
|
1215
|
-
detect_test_framework
|
|
1216
|
-
if [ -n "${coverage_cmd:-}" ]; then
|
|
1217
|
-
echo "正在检查覆盖率..."
|
|
1218
|
-
if ! (cd "$PROJECT_ROOT" && eval "$coverage_cmd" 2>&1); then
|
|
1219
|
-
echo "⚠️ 覆盖率检查未通过(不阻塞,由 test-runner agent 深入分析)"
|
|
1220
|
-
fi
|
|
1221
|
-
fi
|
|
1222
|
-
|
|
1223
|
-
write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
|
|
1224
|
-
pass "单元测试通过"
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
gate_test_integration() {
|
|
1228
|
-
echo "正在执行集成测试..."
|
|
1229
|
-
load_project_config
|
|
1230
|
-
|
|
1231
|
-
local test_result=""
|
|
1232
|
-
if [ -n "$CHANGE" ]; then
|
|
1233
|
-
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1234
|
-
fi
|
|
1235
|
-
|
|
1236
|
-
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1237
|
-
local test_cmd
|
|
1238
|
-
test_cmd=$(jq -r '.integration_test_cmd // empty' "$test_result" 2>/dev/null)
|
|
1239
|
-
|
|
1240
|
-
if jq -e '.integration_test_files | length > 0' "$test_result" &>/dev/null; then
|
|
1241
|
-
verify_test_result_files "$test_result" "integration_test_files"
|
|
1242
|
-
fi
|
|
1243
|
-
|
|
1244
|
-
if [ -n "$test_cmd" ]; then
|
|
1245
|
-
echo "执行 Agent 声明的 integration_test_cmd: $test_cmd"
|
|
1246
|
-
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1247
|
-
fail "集成测试失败"
|
|
1248
|
-
fi
|
|
1249
|
-
else
|
|
1250
|
-
run_tests_by_modules "integration"
|
|
1251
|
-
fi
|
|
1252
|
-
else
|
|
1253
|
-
local module_count
|
|
1254
|
-
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1255
|
-
if [ "$module_count" -eq 0 ]; then
|
|
1256
|
-
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过集成测试"
|
|
1257
|
-
else
|
|
1258
|
-
run_tests_by_modules "integration"
|
|
1259
|
-
fi
|
|
1260
|
-
fi
|
|
1261
|
-
|
|
1262
|
-
write_gate_passed "phases.test.sub_phases.integration.gates.test_integration_gate"
|
|
1263
|
-
pass "集成测试通过"
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
gate_test_e2e() {
|
|
1267
|
-
echo "正在执行 E2E 测试..."
|
|
1268
|
-
load_project_config
|
|
1269
|
-
|
|
1270
|
-
local test_result=""
|
|
1271
|
-
if [ -n "$CHANGE" ]; then
|
|
1272
|
-
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1273
|
-
fi
|
|
1274
|
-
|
|
1275
|
-
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1276
|
-
local test_cmd
|
|
1277
|
-
test_cmd=$(jq -r '.e2e_test_cmd // empty' "$test_result" 2>/dev/null)
|
|
1278
|
-
|
|
1279
|
-
if jq -e '.e2e_test_files | length > 0' "$test_result" &>/dev/null; then
|
|
1280
|
-
verify_test_result_files "$test_result" "e2e_test_files"
|
|
1281
|
-
fi
|
|
1282
|
-
|
|
1283
|
-
if [ -n "$test_cmd" ]; then
|
|
1284
|
-
echo "执行 Agent 声明的 e2e_test_cmd: $test_cmd"
|
|
1285
|
-
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1286
|
-
fail "E2E 测试失败"
|
|
1287
|
-
fi
|
|
1288
|
-
else
|
|
1289
|
-
run_tests_by_modules "e2e"
|
|
1290
|
-
fi
|
|
1291
|
-
else
|
|
1292
|
-
local module_count
|
|
1293
|
-
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1294
|
-
if [ "$module_count" -eq 0 ]; then
|
|
1295
|
-
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过 E2E 测试"
|
|
1296
|
-
else
|
|
1297
|
-
run_tests_by_modules "e2e"
|
|
1298
|
-
fi
|
|
1299
|
-
fi
|
|
1300
|
-
|
|
1301
|
-
write_gate_passed "phases.test.sub_phases.e2e.gates.test_e2e_gate"
|
|
1302
|
-
pass "E2E 测试通过"
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
gate_archive() {
|
|
1306
|
-
if [ -z "$CHANGE" ]; then
|
|
1307
|
-
fail "需要 --change <name>"
|
|
1308
|
-
fi
|
|
1309
|
-
|
|
1310
|
-
# 如果传了 --execute,执行实际归档动作
|
|
1311
|
-
if [ -n "$EXECUTE_ARCHIVE" ]; then
|
|
1312
|
-
local src_dir="$PROJECT_ROOT/specline/changes/$CHANGE"
|
|
1313
|
-
local archive_dir="$PROJECT_ROOT/specline/changes/archive"
|
|
1314
|
-
local date_prefix
|
|
1315
|
-
date_prefix=$(date -u +"%Y-%m-%d")
|
|
1316
|
-
local dest="$archive_dir/${date_prefix}-${CHANGE}"
|
|
1317
|
-
|
|
1318
|
-
if [ ! -d "$src_dir" ]; then
|
|
1319
|
-
fail "Change '$CHANGE' 不存在: $src_dir"
|
|
1320
|
-
fi
|
|
1321
|
-
|
|
1322
|
-
# 检查基本文件
|
|
1323
|
-
if [ ! -f "$src_dir/proposal.md" ]; then
|
|
1324
|
-
fail "缺少 proposal.md"
|
|
1325
|
-
fi
|
|
1326
|
-
if [ ! -f "$src_dir/tasks.md" ]; then
|
|
1327
|
-
fail "缺少 tasks.md"
|
|
1328
|
-
fi
|
|
1329
|
-
|
|
1330
|
-
# 同步 delta specs 到主 specs
|
|
1331
|
-
if [ -d "$src_dir/specs" ]; then
|
|
1332
|
-
echo "正在同步 delta specs 到 specline/specs/..."
|
|
1333
|
-
cp -r "$src_dir/specs/"* "$PROJECT_ROOT/specline/specs/" 2>/dev/null || true
|
|
1334
|
-
fi
|
|
1335
|
-
|
|
1336
|
-
# 移动到归档
|
|
1337
|
-
mkdir -p "$archive_dir"
|
|
1338
|
-
if [ -d "$dest" ]; then
|
|
1339
|
-
fail "归档目标已存在: $dest"
|
|
1340
|
-
fi
|
|
1341
|
-
|
|
1342
|
-
mv "$src_dir" "$dest"
|
|
1343
|
-
echo "✅ 已归档到: $dest"
|
|
1344
|
-
|
|
1345
|
-
# 临时文件(.tmp/)随 change 目录一起归档,无需单独清理
|
|
1346
|
-
|
|
1347
|
-
# 更新状态文件
|
|
1348
|
-
if [ -f "$dest/.pipeline-state.json" ]; then
|
|
1349
|
-
local now
|
|
1350
|
-
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1351
|
-
# macOS sed 兼容
|
|
1352
|
-
sed -i '' "s/\"current_phase\": \"[^\"]*\"/\"current_phase\": \"archived\"/g" "$dest/.pipeline-state.json" 2>/dev/null || true
|
|
1353
|
-
fi
|
|
1354
|
-
|
|
1355
|
-
# 清理所有绑定到该 change 的 session
|
|
1356
|
-
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
1357
|
-
if [ -f "$bindings_file" ]; then
|
|
1358
|
-
jq --arg change "$CHANGE" \
|
|
1359
|
-
'with_entries(select(.value.change != $change))' \
|
|
1360
|
-
"$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
1361
|
-
echo "✅ 已清理 pipeline '$CHANGE' 的所有 session 绑定"
|
|
1362
|
-
fi
|
|
1363
|
-
|
|
1364
|
-
exit 0
|
|
1365
|
-
fi
|
|
1366
|
-
|
|
1367
|
-
# 验证模式(原有逻辑,路径改为 specline)
|
|
1368
|
-
local archive_dir="$PROJECT_ROOT/specline/changes/archive"
|
|
1369
|
-
local found
|
|
1370
|
-
found=$(find "$archive_dir" -maxdepth 1 -type d -name "*$CHANGE" 2>/dev/null | head -1)
|
|
1371
|
-
|
|
1372
|
-
if [ -z "$found" ]; then
|
|
1373
|
-
fail "归档目录不存在: $archive_dir/*$CHANGE"
|
|
1374
|
-
fi
|
|
1375
|
-
|
|
1376
|
-
if [ ! -f "$found/proposal.md" ]; then
|
|
1377
|
-
fail "归档目录缺少 proposal.md"
|
|
1378
|
-
fi
|
|
1379
|
-
if [ ! -f "$found/tasks.md" ]; then
|
|
1380
|
-
fail "归档目录缺少 tasks.md"
|
|
1381
|
-
fi
|
|
1382
|
-
|
|
1383
|
-
write_gate_passed "phases.archive.gates.archive_gate"
|
|
1384
|
-
pass "Archive Gate 全部通过"
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
gate_status() {
|
|
1388
|
-
if [ ! -f "$STATE_FILE" ]; then
|
|
1389
|
-
echo '{"status":"no_pipeline","message":"未找到流水线状态文件"}'
|
|
1390
|
-
exit 0
|
|
1391
|
-
fi
|
|
1392
|
-
|
|
1393
|
-
jq '{
|
|
1394
|
-
change: .change_name,
|
|
1395
|
-
phase: .current_phase,
|
|
1396
|
-
step: .current_step,
|
|
1397
|
-
tasks: .phases.coding.tasks | map({id: .id, type: .type, status: .status, batch: .batch}),
|
|
1398
|
-
progress: {
|
|
1399
|
-
spec: .phases.spec.status,
|
|
1400
|
-
coding: .phases.coding.status,
|
|
1401
|
-
code_review: .phases.code_review.status,
|
|
1402
|
-
test: .phases.test.status,
|
|
1403
|
-
archive: .phases.archive.status
|
|
1404
|
-
}
|
|
1405
|
-
}' "$STATE_FILE"
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
gate_bind() {
|
|
1409
|
-
local session_id="$1"
|
|
1410
|
-
local target_change="$2"
|
|
1411
|
-
|
|
1412
|
-
if [ -z "$session_id" ] || [ -z "$target_change" ]; then
|
|
1413
|
-
fail "需要 <session_id> <change_name>"
|
|
1414
|
-
fi
|
|
1415
|
-
|
|
1416
|
-
local state_file="$PROJECT_ROOT/specline/changes/$target_change/.pipeline-state.json"
|
|
1417
|
-
if [ ! -f "$state_file" ]; then
|
|
1418
|
-
fail "Change '$target_change' 不存在"
|
|
1419
|
-
fi
|
|
1420
|
-
|
|
1421
|
-
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
1422
|
-
[ ! -f "$bindings_file" ] && echo '{}' > "$bindings_file"
|
|
1423
|
-
|
|
1424
|
-
local now
|
|
1425
|
-
now=$(now_iso8601)
|
|
1426
|
-
jq --arg sid "$session_id" --arg change "$target_change" --arg time "$now" \
|
|
1427
|
-
'.[$sid] = {"change": $change, "bound_at": $time}' \
|
|
1428
|
-
"$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
1429
|
-
|
|
1430
|
-
echo "✅ 已绑定 session '$session_id' → pipeline '$target_change'"
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
# ===== Semantic Gate — 跨文件语义检查 =====
|
|
1434
|
-
gate_semantic() {
|
|
1435
|
-
if [ -z "$CHANGE" ]; then
|
|
1436
|
-
fail "需要 --change <name>"
|
|
1437
|
-
fi
|
|
1438
|
-
|
|
1439
|
-
# 定位 spec.md 和 tasks.md
|
|
1440
|
-
local spec_file tasks_file
|
|
1441
|
-
spec_file=$(find_spec_file)
|
|
1442
|
-
tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
|
|
1443
|
-
|
|
1444
|
-
if [ ! -f "$spec_file" ] || [ ! -f "$tasks_file" ]; then
|
|
1445
|
-
fail "spec.md 或 tasks.md 不存在,无法执行语义检查"
|
|
1446
|
-
fi
|
|
1447
|
-
|
|
1448
|
-
local checks_dir="$SCRIPT_DIR/specline-pipeline-gate-checks"
|
|
1449
|
-
local common_sh="$checks_dir/common.sh"
|
|
1450
|
-
|
|
1451
|
-
if [ ! -f "$common_sh" ]; then
|
|
1452
|
-
fail "common.sh 不存在: $common_sh"
|
|
1453
|
-
fi
|
|
1454
|
-
|
|
1455
|
-
# source common.sh 初始化计数器
|
|
1456
|
-
source "$common_sh"
|
|
1457
|
-
|
|
1458
|
-
# 设定文件路径环境变量,供各检查脚本使用
|
|
1459
|
-
export SPEC_FILE="$spec_file"
|
|
1460
|
-
export TASKS_FILE="$tasks_file"
|
|
1461
|
-
|
|
1462
|
-
# 依次执行 6 项语义检查
|
|
1463
|
-
local check_scripts=(
|
|
1464
|
-
"a1-covers-ref.sh"
|
|
1465
|
-
"d1-cycle.sh"
|
|
1466
|
-
"c1-exception.sh"
|
|
1467
|
-
"c2-vague.sh"
|
|
1468
|
-
"a2-a3-reverse.sh"
|
|
1469
|
-
"d3-type-file.sh"
|
|
1470
|
-
)
|
|
1471
|
-
|
|
1472
|
-
local check_functions=(
|
|
1473
|
-
"run_a1_covers_ref"
|
|
1474
|
-
"run_d1_cycle"
|
|
1475
|
-
"run_c1_exception"
|
|
1476
|
-
"run_c2_vague"
|
|
1477
|
-
"run_a2_a3_reverse"
|
|
1478
|
-
"run_d3_type_file"
|
|
1479
|
-
)
|
|
1480
|
-
|
|
1481
|
-
local i=0
|
|
1482
|
-
for script in "${check_scripts[@]}"; do
|
|
1483
|
-
local script_path="$checks_dir/$script"
|
|
1484
|
-
if [ -f "$script_path" ]; then
|
|
1485
|
-
source "$script_path"
|
|
1486
|
-
if declare -f "${check_functions[$i]}" > /dev/null 2>&1; then
|
|
1487
|
-
"${check_functions[$i]}"
|
|
1488
|
-
fi
|
|
1489
|
-
else
|
|
1490
|
-
echo "⚠️ 检查脚本不存在,跳过: $script"
|
|
1491
|
-
fi
|
|
1492
|
-
i=$((i + 1))
|
|
1493
|
-
done
|
|
1494
|
-
|
|
1495
|
-
# 汇总结果
|
|
1496
|
-
local total_issues=$((SEMANTIC_ERRORS + SEMANTIC_WARNINGS + SEMANTIC_INFOS))
|
|
1497
|
-
|
|
1498
|
-
echo ""
|
|
1499
|
-
echo "========== Semantic Gate 汇总 =========="
|
|
1500
|
-
echo " ❌ 错误: $SEMANTIC_ERRORS"
|
|
1501
|
-
echo " ⚠️ 警告: $SEMANTIC_WARNINGS"
|
|
1502
|
-
echo " ℹ️ 信息: $SEMANTIC_INFOS"
|
|
1503
|
-
echo " 总计: $total_issues"
|
|
1504
|
-
echo "========================================="
|
|
1505
|
-
|
|
1506
|
-
if [ "$SEMANTIC_ERRORS" -gt 0 ]; then
|
|
1507
|
-
echo ""
|
|
1508
|
-
echo "❌ Semantic Gate 未通过:发现 $SEMANTIC_ERRORS 个错误"
|
|
1509
|
-
exit 1
|
|
1510
|
-
fi
|
|
1511
|
-
|
|
1512
|
-
write_gate_passed "phases.spec.gates.semantic_gate"
|
|
1513
|
-
pass "✅ Semantic Gate 全部通过"
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
# ===== 分派 =====
|
|
1517
|
-
|
|
1518
|
-
case "$PHASE" in
|
|
1519
|
-
new)
|
|
1520
|
-
gate_new
|
|
1521
|
-
;;
|
|
1522
|
-
list)
|
|
1523
|
-
gate_list "$@"
|
|
1524
|
-
;;
|
|
1525
|
-
artifacts)
|
|
1526
|
-
gate_artifacts "$@"
|
|
1527
|
-
;;
|
|
1528
|
-
spec)
|
|
1529
|
-
gate_spec
|
|
1530
|
-
;;
|
|
1531
|
-
semantic)
|
|
1532
|
-
gate_semantic "$@"
|
|
1533
|
-
;;
|
|
1534
|
-
build)
|
|
1535
|
-
gate_build
|
|
1536
|
-
;;
|
|
1537
|
-
lint)
|
|
1538
|
-
gate_lint
|
|
1539
|
-
;;
|
|
1540
|
-
test-unit)
|
|
1541
|
-
gate_test_unit
|
|
1542
|
-
;;
|
|
1543
|
-
test-integration)
|
|
1544
|
-
gate_test_integration
|
|
1545
|
-
;;
|
|
1546
|
-
test-e2e)
|
|
1547
|
-
gate_test_e2e
|
|
1548
|
-
;;
|
|
1549
|
-
bind)
|
|
1550
|
-
gate_bind "${POSITIONAL_ARGS[0]:-}" "${POSITIONAL_ARGS[1]:-}"
|
|
1551
|
-
;;
|
|
1552
|
-
detect-modules)
|
|
1553
|
-
load_project_config
|
|
1554
|
-
echo "$MODULES_JSON"
|
|
1555
|
-
;;
|
|
1556
|
-
archive)
|
|
1557
|
-
gate_archive "$@"
|
|
1558
|
-
;;
|
|
1559
|
-
status)
|
|
1560
|
-
gate_status
|
|
1561
|
-
;;
|
|
1562
|
-
*)
|
|
1563
|
-
echo "未知 phase: $PHASE"
|
|
1564
|
-
echo "可用: new | list | artifacts | spec | semantic | build | lint | test-unit | test-integration | test-e2e | detect-modules | bind | archive | status"
|
|
1565
|
-
exit 2
|
|
1566
|
-
;;
|
|
1567
|
-
esac
|
|
1568
|
-
|
|
1569
|
-
exit 0
|