specline 1.2.1 → 1.3.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.
@@ -6,7 +6,7 @@
6
6
  # specline-pipeline-gate.sh <phase> --change <change-name>
7
7
  #
8
8
  # Phases:
9
- # new | list | artifacts | spec | build | lint | test-unit | test-integration | test-e2e | archive | status
9
+ # new | list | artifacts | spec | semantic | build | lint | test-unit | test-integration | test-e2e | detect-modules | bind | archive | status
10
10
  #
11
11
  # Exit codes:
12
12
  # 0 = 通过
@@ -18,6 +18,7 @@ set -euo pipefail
18
18
  PHASE="${1:-}"
19
19
  CHANGE=""
20
20
  EXECUTE_ARCHIVE=""
21
+ POSITIONAL_ARGS=()
21
22
 
22
23
  # 遍历所有参数,不依赖位置
23
24
  shift # 跳过 PHASE
@@ -32,6 +33,7 @@ while [ $# -gt 0 ]; do
32
33
  shift
33
34
  ;;
34
35
  *)
36
+ POSITIONAL_ARGS+=("$1")
35
37
  shift
36
38
  ;;
37
39
  esac
@@ -81,6 +83,425 @@ find_spec_file() {
81
83
  find "$PROJECT_ROOT/specline/changes/$CHANGE/specs" -name "spec.md" 2>/dev/null | head -1
82
84
  }
83
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
+
84
505
  # ===== Phase Handlers =====
85
506
 
86
507
  gate_new() {
@@ -255,14 +676,30 @@ gate_spec() {
255
676
  fi
256
677
  pass "Scenario 数量: $scenario_count"
257
678
 
258
- # 6. WHEN/THEN 配对检查
259
- local when_count then_count
260
- when_count=$(grep -c "\*\*WHEN\*\*" "$spec_file" || echo "0")
261
- then_count=$(grep -c "\*\*THEN\*\*" "$spec_file" || echo "0")
262
- if [ "$when_count" -ne "$then_count" ]; then
263
- fail "WHEN/THEN 数量不匹配。WHEN: $when_count, THEN: $then_count"
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}"
264
701
  fi
265
- pass "WHEN/THEN 配对检查通过 ($when_count )"
702
+ pass "WHEN/THEN 语义检查通过 (每个 Scenario 至少 1 WHEN + 1 THEN)"
266
703
 
267
704
  # 7. review.json 状态检查(如果存在)
268
705
  local review_file
@@ -345,28 +782,44 @@ gate_spec() {
345
782
  }
346
783
 
347
784
  gate_build() {
348
- # TypeScript 编译检查(如果存在 tsconfig.json)
349
- if [ -f "$PROJECT_ROOT/tsconfig.json" ]; then
350
- echo "正在检查 TypeScript 编译..."
351
- if ! npx tsc --noEmit 2>&1; then
352
- fail "TypeScript 编译失败"
353
- fi
354
- pass "TypeScript 编译通过"
785
+ load_project_config
786
+
787
+ local module_count
788
+ module_count=$(echo "$MODULES_JSON" | jq 'length')
789
+
790
+ if [ "$module_count" -eq 0 ]; then
791
+ echo "⚠️ 未检测到项目模块,跳过 build 命令"
792
+ else
793
+ local i=0
794
+ while [ "$i" -lt "$module_count" ]; do
795
+ local path lang
796
+ path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
797
+ lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
798
+ build_module "$path" "$lang"
799
+ i=$((i + 1))
800
+ done
801
+ pass "模块 build 检查通过 ($module_count 个模块)"
355
802
  fi
356
803
 
357
- # Python 语法检查
358
- echo "正在检查 Python 语法..."
359
- local py_dirs=""
360
- for d in agent server scripts; do
361
- if [ -d "$PROJECT_ROOT/$d" ]; then
362
- py_dirs="$py_dirs $d"
363
- fi
364
- done
365
- if [ -n "$py_dirs" ]; then
366
- if ! python -m compileall -q $py_dirs 2>&1; then
367
- fail "Python 语法错误"
804
+ # Agent 产出 JSON 验证(task-result.json files_changed / files)
805
+ if [ -n "$CHANGE" ]; then
806
+ local task_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/task-result.json"
807
+ if [ -f "$task_result" ]; then
808
+ echo "正在验证 task-result.json 声明的文件..."
809
+ local missing_files=""
810
+ while IFS= read -r f; do
811
+ [ -z "$f" ] && continue
812
+ if [ ! -f "$PROJECT_ROOT/$f" ]; then
813
+ missing_files="${missing_files}
814
+ - $f"
815
+ fi
816
+ done < <(jq -r '(.files_changed // .files // [])[]?' "$task_result" 2>/dev/null)
817
+
818
+ if [ -n "$missing_files" ]; then
819
+ fail "task-result.json 声明的文件不存在:${missing_files}"
820
+ fi
821
+ pass "task-result.json 文件验证通过"
368
822
  fi
369
- pass "Python 语法检查通过"
370
823
  fi
371
824
 
372
825
  # 单元测试文件存在性检查(Testable=true 任务)
@@ -384,7 +837,7 @@ gate_build() {
384
837
  while IFS='|' read -r task_id file_path; do
385
838
  if [ -z "$file_path" ]; then
386
839
  missing_files="${missing_files}
387
- 任务 $task_id: 未在 Files 列表中声明 tests/unit/ 或 tests/models/ 下的测试文件"
840
+ 任务 $task_id: 未在 Files 列表中声明测试文件(支持 tests/unit/、tests/models/、*_test.go、*.test.ts 等)"
388
841
  continue
389
842
  fi
390
843
 
@@ -394,10 +847,9 @@ gate_build() {
394
847
  continue
395
848
  fi
396
849
 
397
- # 语法检查
398
850
  case "$file_path" in
399
851
  *.py)
400
- if ! python -m py_compile "$PROJECT_ROOT/$file_path" 2>&1; then
852
+ if ! python3 -m py_compile "$PROJECT_ROOT/$file_path" 2>&1; then
401
853
  syntax_errors="${syntax_errors}
402
854
  任务 $task_id: $file_path (Python 语法错误)"
403
855
  fi
@@ -420,15 +872,19 @@ gate_build() {
420
872
  files_line = $0
421
873
  gsub(/.*\*\*Files\*\*:[ \t]*/, "", files_line)
422
874
  split(files_line, paths, /,[ \t]*/)
423
- has_unit_test = 0
875
+ has_test = 0
424
876
  for (i in paths) {
425
877
  gsub(/^[ \t]+|[ \t]+$/, "", paths[i])
426
- if (paths[i] ~ /^tests\/(unit|models)\//) {
878
+ if (paths[i] ~ /^tests\/(unit|models)\// ||
879
+ paths[i] ~ /_test\.go$/ ||
880
+ paths[i] ~ /\.test\.(ts|tsx|js|jsx)$/ ||
881
+ paths[i] ~ /\.spec\.(ts|tsx|js|jsx)$/ ||
882
+ paths[i] ~ /^src\/.*\/tests\.rs$/) {
427
883
  print task_id "|" paths[i]
428
- has_unit_test = 1
884
+ has_test = 1
429
885
  }
430
886
  }
431
- if (has_unit_test == 0) {
887
+ if (has_test == 0) {
432
888
  print task_id "|"
433
889
  }
434
890
  }
@@ -454,26 +910,23 @@ gate_build() {
454
910
  }
455
911
 
456
912
  gate_lint() {
457
- # Python lint (ruff)
458
- if command -v ruff &>/dev/null; then
459
- echo "正在检查 Python 代码规范..."
460
- if ! ruff check "$PROJECT_ROOT" --quiet 2>&1; then
461
- fail "Python lint 失败"
462
- fi
463
- pass "Python lint 通过"
464
- else
465
- echo "⚠️ ruff 未安装,跳过 Python lint"
466
- fi
913
+ load_project_config
467
914
 
468
- # JS/TS lint (eslint)
469
- if [ -f "$PROJECT_ROOT/package.json" ]; then
470
- if command -v npx &>/dev/null; then
471
- echo "正在检查 JS/TS 代码规范..."
472
- if ! npx eslint "$PROJECT_ROOT" --max-warnings 0 --quiet 2>&1; then
473
- fail "JS/TS lint 失败"
474
- fi
475
- pass "JS/TS lint 通过"
476
- fi
915
+ local module_count
916
+ module_count=$(echo "$MODULES_JSON" | jq 'length')
917
+
918
+ if [ "$module_count" -eq 0 ]; then
919
+ echo "⚠️ 未检测到项目模块,跳过 lint"
920
+ else
921
+ local i=0
922
+ while [ "$i" -lt "$module_count" ]; do
923
+ local path lang
924
+ path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
925
+ lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
926
+ lint_module "$path" "$lang"
927
+ i=$((i + 1))
928
+ done
929
+ pass "模块 lint 检查通过 ($module_count 个模块)"
477
930
  fi
478
931
 
479
932
  # code-review.json error 计数(位于 change 的 .tmp/ 目录下)
@@ -492,7 +945,7 @@ gate_lint() {
492
945
  }
493
946
 
494
947
  # ===== 测试框架自动检测 =====
495
- # 优先级:.pipeline-state.json 中的 test_framework > 项目配置文件检测 > 默认 pytest
948
+ # 优先级:.pipeline-state.json > test-code-result.json > MODULES_JSON 推导 > 无兜底
496
949
  detect_test_framework() {
497
950
  framework="" test_cmd="" coverage_cmd=""
498
951
 
@@ -505,99 +958,148 @@ detect_test_framework() {
505
958
  fi
506
959
  fi
507
960
 
508
- # 2. 如果状态文件没有,从项目配置文件检测
961
+ # 2. 从 test-code-result.json 读取
962
+ if [ -z "$framework" ] && [ -n "$CHANGE" ]; then
963
+ local test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
964
+ if [ -f "$test_result" ]; then
965
+ framework=$(jq -r '.test_framework // empty' "$test_result" 2>/dev/null)
966
+ test_cmd=$(jq -r '.test_cmd // empty' "$test_result" 2>/dev/null)
967
+ fi
968
+ fi
969
+
970
+ # 3. 从 MODULES_JSON 推导
509
971
  if [ -z "$framework" ]; then
510
- if [ -f "$PROJECT_ROOT/package.json" ]; then
511
- if grep -q '"jest"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
512
- framework="jest"
513
- elif grep -q '"vitest"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
514
- framework="vitest"
515
- elif grep -q '"mocha"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
516
- framework="mocha"
517
- fi
518
- elif [ -f "$PROJECT_ROOT/go.mod" ]; then
519
- framework="go-test"
520
- elif [ -f "$PROJECT_ROOT/Cargo.toml" ]; then
521
- framework="cargo-test"
522
- elif [ -f "$PROJECT_ROOT/pom.xml" ] || [ -f "$PROJECT_ROOT/build.gradle" ]; then
523
- framework="junit"
972
+ load_project_config
973
+ local module_count
974
+ module_count=$(echo "$MODULES_JSON" | jq 'length')
975
+ if [ "$module_count" -gt 0 ]; then
976
+ local lang
977
+ lang=$(echo "$MODULES_JSON" | jq -r '.[0].language')
978
+ case "$lang" in
979
+ go) framework="go-test" ;;
980
+ python) framework="pytest" ;;
981
+ rust) framework="cargo-test" ;;
982
+ java) framework="junit" ;;
983
+ kotlin) framework="junit" ;;
984
+ typescript|javascript)
985
+ local mod_path
986
+ mod_path=$(echo "$MODULES_JSON" | jq -r '.[0].path')
987
+ local mod_dir
988
+ mod_dir=$(module_absolute_path "$mod_path")
989
+ if [ -f "$mod_dir/package.json" ]; then
990
+ if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
991
+ framework="vitest"
992
+ elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
993
+ framework="jest"
994
+ elif grep -q '"mocha"' "$mod_dir/package.json" 2>/dev/null; then
995
+ framework="mocha"
996
+ fi
997
+ fi
998
+ ;;
999
+ esac
524
1000
  fi
525
1001
  fi
526
1002
 
527
- # 3. 默认兜底
1003
+ # 4. 无兜底 pytest — 检测失败时 framework 为空
528
1004
  if [ -z "$framework" ]; then
529
- framework="pytest"
1005
+ echo "⚠️ 未检测到测试框架"
1006
+ return 0
530
1007
  fi
531
1008
 
532
- # 根据框架确定命令
533
- case "$framework" in
534
- jest)
535
- test_cmd="npx jest"
536
- coverage_cmd="npx jest --coverage"
537
- ;;
538
- vitest)
539
- test_cmd="npx vitest run"
540
- coverage_cmd="npx vitest run --coverage"
541
- ;;
542
- mocha)
543
- test_cmd="npx mocha"
544
- coverage_cmd="npx nyc mocha"
545
- ;;
546
- go-test)
547
- test_cmd="go test"
548
- coverage_cmd="go test -cover"
549
- ;;
550
- cargo-test)
551
- test_cmd="cargo test"
552
- coverage_cmd="cargo tarpaulin 2>/dev/null || cargo test" # tarpaulin 可能未安装
553
- ;;
554
- junit)
555
- if [ -f "$PROJECT_ROOT/pom.xml" ]; then
556
- test_cmd="mvn test"
557
- coverage_cmd="mvn jacoco:report"
558
- else
559
- test_cmd="gradle test"
560
- coverage_cmd="gradle jacocoTestReport"
561
- fi
562
- ;;
563
- pytest|*)
564
- test_cmd="pytest"
565
- coverage_cmd="pytest --cov --cov-fail-under=80"
566
- ;;
567
- esac
1009
+ # 根据框架确定命令(test_cmd 可能已由 JSON 提供)
1010
+ if [ -z "$test_cmd" ]; then
1011
+ case "$framework" in
1012
+ jest)
1013
+ test_cmd="npx jest"
1014
+ coverage_cmd="npx jest --coverage"
1015
+ ;;
1016
+ vitest)
1017
+ test_cmd="npx vitest run"
1018
+ coverage_cmd="npx vitest run --coverage"
1019
+ ;;
1020
+ mocha)
1021
+ test_cmd="npx mocha"
1022
+ coverage_cmd="npx nyc mocha"
1023
+ ;;
1024
+ go-test)
1025
+ test_cmd="go test ./..."
1026
+ coverage_cmd="go test -cover ./..."
1027
+ ;;
1028
+ cargo-test)
1029
+ test_cmd="cargo test"
1030
+ coverage_cmd="cargo tarpaulin 2>/dev/null || cargo test"
1031
+ ;;
1032
+ junit)
1033
+ local mod_path mod_dir
1034
+ mod_path=$(echo "$MODULES_JSON" | jq -r '.[0].path // "."')
1035
+ mod_dir=$(module_absolute_path "$mod_path")
1036
+ if [ -f "$mod_dir/pom.xml" ]; then
1037
+ test_cmd="mvn test"
1038
+ coverage_cmd="mvn jacoco:report"
1039
+ else
1040
+ test_cmd="gradle test"
1041
+ coverage_cmd="gradle jacocoTestReport"
1042
+ fi
1043
+ ;;
1044
+ pytest)
1045
+ test_cmd="pytest"
1046
+ coverage_cmd="pytest --cov --cov-fail-under=80"
1047
+ ;;
1048
+ *)
1049
+ test_cmd=""
1050
+ coverage_cmd=""
1051
+ ;;
1052
+ esac
1053
+ fi
568
1054
 
569
- echo "检测到测试框架: $framework (命令: $test_cmd)"
1055
+ if [ -n "$framework" ] && [ -n "$test_cmd" ]; then
1056
+ echo "检测到测试框架: $framework (命令: $test_cmd)"
1057
+ fi
570
1058
  }
571
1059
 
572
1060
  gate_test_unit() {
573
1061
  echo "正在执行单元测试..."
574
- detect_test_framework
1062
+ load_project_config
575
1063
 
576
- # 确定测试目录
577
- local test_dir=""
578
- for d in "$PROJECT_ROOT/tests/unit" "$PROJECT_ROOT/tests" "$PROJECT_ROOT/__tests__" "$PROJECT_ROOT/test"; do
579
- if [ -d "$d" ]; then
580
- test_dir="$d"
581
- break
582
- fi
583
- done
1064
+ local test_result=""
1065
+ if [ -n "$CHANGE" ]; then
1066
+ test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
1067
+ fi
584
1068
 
585
- if [ -z "$test_dir" ]; then
586
- echo "⚠️ 未找到测试目录,尝试用框架默认命令运行..."
587
- if ! eval "$test_cmd" 2>&1; then
588
- fail "单元测试失败(无测试目录且框架命令执行失败)"
1069
+ if [ -n "$test_result" ] && [ -f "$test_result" ]; then
1070
+ local test_cmd
1071
+ test_cmd=$(jq -r '.test_cmd // empty' "$test_result" 2>/dev/null)
1072
+
1073
+ verify_test_result_files "$test_result" "test_files"
1074
+
1075
+ if [ -n "$test_cmd" ]; then
1076
+ echo "执行 Agent 声明的 test_cmd: $test_cmd"
1077
+ if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
1078
+ fail "单元测试失败"
1079
+ fi
1080
+ else
1081
+ echo "⚠️ test-code-result.json 无 test_cmd,回退到模块默认命令"
1082
+ run_tests_by_modules "unit"
589
1083
  fi
590
1084
  else
591
- echo "测试目录: $test_dir"
592
- if ! eval "$test_cmd \"$test_dir\" -v 2>&1"; then
593
- fail "单元测试失败"
1085
+ local module_count
1086
+ module_count=$(echo "$MODULES_JSON" | jq 'length')
1087
+ if [ "$module_count" -eq 0 ]; then
1088
+ echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过单元测试"
1089
+ write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
1090
+ pass "单元测试已跳过"
1091
+ return 0
594
1092
  fi
1093
+ run_tests_by_modules "unit"
595
1094
  fi
596
1095
 
597
- # 覆盖率检查(非阻塞,警告即可——覆盖率的深入分析由 test-runner agent 负责)
598
- echo "正在检查覆盖率..."
599
- if ! eval "$coverage_cmd \"$test_dir\" 2>&1"; then
600
- echo "⚠️ 覆盖率检查未通过(不阻塞,由 test-runner agent 深入分析)"
1096
+ # 覆盖率检查(非阻塞)
1097
+ detect_test_framework
1098
+ if [ -n "${coverage_cmd:-}" ]; then
1099
+ echo "正在检查覆盖率..."
1100
+ if ! (cd "$PROJECT_ROOT" && eval "$coverage_cmd" 2>&1); then
1101
+ echo "⚠️ 覆盖率检查未通过(不阻塞,由 test-runner agent 深入分析)"
1102
+ fi
601
1103
  fi
602
1104
 
603
1105
  write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
@@ -606,53 +1108,78 @@ gate_test_unit() {
606
1108
 
607
1109
  gate_test_integration() {
608
1110
  echo "正在执行集成测试..."
609
- detect_test_framework
1111
+ load_project_config
1112
+
1113
+ local test_result=""
1114
+ if [ -n "$CHANGE" ]; then
1115
+ test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
1116
+ fi
1117
+
1118
+ if [ -n "$test_result" ] && [ -f "$test_result" ]; then
1119
+ local test_cmd
1120
+ test_cmd=$(jq -r '.integration_test_cmd // empty' "$test_result" 2>/dev/null)
610
1121
 
611
- local test_dir=""
612
- for d in "$PROJECT_ROOT/tests/integration" "$PROJECT_ROOT/__tests__/integration"; do
613
- if [ -d "$d" ]; then
614
- test_dir="$d"
615
- break
1122
+ if jq -e '.integration_test_files | length > 0' "$test_result" &>/dev/null; then
1123
+ verify_test_result_files "$test_result" "integration_test_files"
616
1124
  fi
617
- done
618
1125
 
619
- if [ -z "$test_dir" ]; then
620
- echo "⚠️ 无集成测试目录,跳过"
621
- else
622
- echo "测试目录: $test_dir"
623
- if ! eval "$test_cmd \"$test_dir\" -v 2>&1"; then
624
- fail "集成测试失败"
1126
+ if [ -n "$test_cmd" ]; then
1127
+ echo "执行 Agent 声明的 integration_test_cmd: $test_cmd"
1128
+ if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
1129
+ fail "集成测试失败"
1130
+ fi
1131
+ else
1132
+ run_tests_by_modules "integration"
625
1133
  fi
626
- # 覆盖率
627
- if ! eval "$coverage_cmd \"$test_dir\" 2>&1"; then
628
- echo "⚠️ 覆盖率检查未通过(不阻塞)"
1134
+ else
1135
+ local module_count
1136
+ module_count=$(echo "$MODULES_JSON" | jq 'length')
1137
+ if [ "$module_count" -eq 0 ]; then
1138
+ echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过集成测试"
1139
+ else
1140
+ run_tests_by_modules "integration"
629
1141
  fi
630
1142
  fi
1143
+
631
1144
  write_gate_passed "phases.test.sub_phases.integration.gates.test_integration_gate"
632
1145
  pass "集成测试通过"
633
1146
  }
634
1147
 
635
1148
  gate_test_e2e() {
636
1149
  echo "正在执行 E2E 测试..."
637
- detect_test_framework
1150
+ load_project_config
1151
+
1152
+ local test_result=""
1153
+ if [ -n "$CHANGE" ]; then
1154
+ test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
1155
+ fi
1156
+
1157
+ if [ -n "$test_result" ] && [ -f "$test_result" ]; then
1158
+ local test_cmd
1159
+ test_cmd=$(jq -r '.e2e_test_cmd // empty' "$test_result" 2>/dev/null)
638
1160
 
639
- local test_dir=""
640
- for d in "$PROJECT_ROOT/tests/e2e" "$PROJECT_ROOT/__tests__/e2e" "$PROJECT_ROOT/e2e"; do
641
- if [ -d "$d" ]; then
642
- test_dir="$d"
643
- break
1161
+ if jq -e '.e2e_test_files | length > 0' "$test_result" &>/dev/null; then
1162
+ verify_test_result_files "$test_result" "e2e_test_files"
644
1163
  fi
645
- done
646
1164
 
647
- if [ -z "$test_dir" ]; then
648
- echo "⚠️ 无 E2E 测试目录,跳过"
1165
+ if [ -n "$test_cmd" ]; then
1166
+ echo "执行 Agent 声明的 e2e_test_cmd: $test_cmd"
1167
+ if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
1168
+ fail "E2E 测试失败"
1169
+ fi
1170
+ else
1171
+ run_tests_by_modules "e2e"
1172
+ fi
649
1173
  else
650
- echo "测试目录: $test_dir"
651
- if ! eval "$test_cmd \"$test_dir\" -v 2>&1"; then
652
- fail "E2E 测试失败"
1174
+ local module_count
1175
+ module_count=$(echo "$MODULES_JSON" | jq 'length')
1176
+ if [ "$module_count" -eq 0 ]; then
1177
+ echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过 E2E 测试"
1178
+ else
1179
+ run_tests_by_modules "e2e"
653
1180
  fi
654
- # E2E 通常不需要覆盖率检查
655
1181
  fi
1182
+
656
1183
  write_gate_passed "phases.test.sub_phases.e2e.gates.test_e2e_gate"
657
1184
  pass "E2E 测试通过"
658
1185
  }
@@ -785,6 +1312,89 @@ gate_bind() {
785
1312
  echo "✅ 已绑定 session '$session_id' → pipeline '$target_change'"
786
1313
  }
787
1314
 
1315
+ # ===== Semantic Gate — 跨文件语义检查 =====
1316
+ gate_semantic() {
1317
+ if [ -z "$CHANGE" ]; then
1318
+ fail "需要 --change <name>"
1319
+ fi
1320
+
1321
+ # 定位 spec.md 和 tasks.md
1322
+ local spec_file tasks_file
1323
+ spec_file=$(find_spec_file)
1324
+ tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
1325
+
1326
+ if [ ! -f "$spec_file" ] || [ ! -f "$tasks_file" ]; then
1327
+ fail "spec.md 或 tasks.md 不存在,无法执行语义检查"
1328
+ fi
1329
+
1330
+ local checks_dir="$SCRIPT_DIR/specline-pipeline-gate-checks"
1331
+ local common_sh="$checks_dir/common.sh"
1332
+
1333
+ if [ ! -f "$common_sh" ]; then
1334
+ fail "common.sh 不存在: $common_sh"
1335
+ fi
1336
+
1337
+ # source common.sh 初始化计数器
1338
+ source "$common_sh"
1339
+
1340
+ # 设定文件路径环境变量,供各检查脚本使用
1341
+ export SPEC_FILE="$spec_file"
1342
+ export TASKS_FILE="$tasks_file"
1343
+
1344
+ # 依次执行 6 项语义检查
1345
+ local check_scripts=(
1346
+ "a1-covers-ref.sh"
1347
+ "d1-cycle.sh"
1348
+ "c1-exception.sh"
1349
+ "c2-vague.sh"
1350
+ "a2-a3-reverse.sh"
1351
+ "d3-type-file.sh"
1352
+ )
1353
+
1354
+ local check_functions=(
1355
+ "run_a1_covers_ref"
1356
+ "run_d1_cycle"
1357
+ "run_c1_exception"
1358
+ "run_c2_vague"
1359
+ "run_a2_a3_reverse"
1360
+ "run_d3_type_file"
1361
+ )
1362
+
1363
+ local i=0
1364
+ for script in "${check_scripts[@]}"; do
1365
+ local script_path="$checks_dir/$script"
1366
+ if [ -f "$script_path" ]; then
1367
+ source "$script_path"
1368
+ if declare -f "${check_functions[$i]}" > /dev/null 2>&1; then
1369
+ "${check_functions[$i]}"
1370
+ fi
1371
+ else
1372
+ echo "⚠️ 检查脚本不存在,跳过: $script"
1373
+ fi
1374
+ i=$((i + 1))
1375
+ done
1376
+
1377
+ # 汇总结果
1378
+ local total_issues=$((SEMANTIC_ERRORS + SEMANTIC_WARNINGS + SEMANTIC_INFOS))
1379
+
1380
+ echo ""
1381
+ echo "========== Semantic Gate 汇总 =========="
1382
+ echo " ❌ 错误: $SEMANTIC_ERRORS"
1383
+ echo " ⚠️ 警告: $SEMANTIC_WARNINGS"
1384
+ echo " ℹ️ 信息: $SEMANTIC_INFOS"
1385
+ echo " 总计: $total_issues"
1386
+ echo "========================================="
1387
+
1388
+ if [ "$SEMANTIC_ERRORS" -gt 0 ]; then
1389
+ echo ""
1390
+ echo "❌ Semantic Gate 未通过:发现 $SEMANTIC_ERRORS 个错误"
1391
+ exit 1
1392
+ fi
1393
+
1394
+ write_gate_passed "phases.spec.gates.semantic_gate"
1395
+ pass "✅ Semantic Gate 全部通过"
1396
+ }
1397
+
788
1398
  # ===== 分派 =====
789
1399
 
790
1400
  case "$PHASE" in
@@ -800,6 +1410,9 @@ case "$PHASE" in
800
1410
  spec)
801
1411
  gate_spec
802
1412
  ;;
1413
+ semantic)
1414
+ gate_semantic "$@"
1415
+ ;;
803
1416
  build)
804
1417
  gate_build
805
1418
  ;;
@@ -816,7 +1429,11 @@ case "$PHASE" in
816
1429
  gate_test_e2e
817
1430
  ;;
818
1431
  bind)
819
- gate_bind "$2" "$3"
1432
+ gate_bind "${POSITIONAL_ARGS[0]:-}" "${POSITIONAL_ARGS[1]:-}"
1433
+ ;;
1434
+ detect-modules)
1435
+ load_project_config
1436
+ echo "$MODULES_JSON"
820
1437
  ;;
821
1438
  archive)
822
1439
  gate_archive "$@"
@@ -826,7 +1443,7 @@ case "$PHASE" in
826
1443
  ;;
827
1444
  *)
828
1445
  echo "未知 phase: $PHASE"
829
- echo "可用: new | list | artifacts | spec | build | lint | test-unit | test-integration | test-e2e | archive | status"
1446
+ echo "可用: new | list | artifacts | spec | semantic | build | lint | test-unit | test-integration | test-e2e | detect-modules | bind | archive | status"
830
1447
  exit 2
831
1448
  ;;
832
1449
  esac