specline 1.4.0 → 2.0.1

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.
Files changed (75) hide show
  1. package/README.md +132 -125
  2. package/adapters/claude/deploy.json +12 -0
  3. package/adapters/claude/hooks/hooks.json +12 -0
  4. package/adapters/claude/hooks.json +12 -0
  5. package/adapters/claude/orchestration.md +17 -0
  6. package/adapters/codex/agent.toml.hbs +7 -0
  7. package/adapters/codex/deploy.json +12 -0
  8. package/adapters/codex/hooks.json +12 -0
  9. package/adapters/codex/orchestration.md +18 -0
  10. package/adapters/cursor/deploy.json +12 -0
  11. package/adapters/cursor/hooks.json +9 -0
  12. package/adapters/cursor/orchestration.md +17 -0
  13. package/adapters/opencode/deploy.json +12 -0
  14. package/adapters/opencode/orchestration.md +18 -0
  15. package/adapters/opencode/plugin.js +10 -0
  16. package/cli.mjs +161 -558
  17. package/core/agents/specline-backend-dev.yaml +45 -0
  18. package/core/agents/specline-code-reviewer.yaml +67 -0
  19. package/core/agents/specline-config-dev.yaml +50 -0
  20. package/core/agents/specline-config-reviewer.yaml +70 -0
  21. package/core/agents/specline-explore-assistant.yaml +79 -0
  22. package/core/agents/specline-frontend-dev.yaml +45 -0
  23. package/core/agents/specline-spec-creator.yaml +58 -0
  24. package/core/agents/specline-spec-reviewer.yaml +58 -0
  25. package/core/agents/specline-test-runner.yaml +62 -0
  26. package/core/agents/specline-test-writer.yaml +67 -0
  27. package/core/bootstrap/using-specline.md +14 -0
  28. package/core/gates/pipeline-gate-checks/a1-covers-ref.sh +125 -0
  29. package/core/gates/pipeline-gate-checks/a2-a3-reverse.sh +171 -0
  30. package/core/gates/pipeline-gate-checks/c1-exception.sh +71 -0
  31. package/core/gates/pipeline-gate-checks/c2-vague.sh +60 -0
  32. package/core/gates/pipeline-gate-checks/common.sh +68 -0
  33. package/core/gates/pipeline-gate-checks/d1-cycle.sh +149 -0
  34. package/core/gates/pipeline-gate-checks/d3-type-file.sh +260 -0
  35. package/core/gates/pipeline-gate.sh +1456 -0
  36. package/core/hooks/session-start.sh +259 -0
  37. package/core/skills/specline-apply-change/SKILL.md +197 -0
  38. package/core/skills/specline-archive-change/SKILL.md +173 -0
  39. package/core/skills/specline-explore/SKILL.md +504 -0
  40. package/core/skills/specline-knowledge/SKILL.md +539 -0
  41. package/core/skills/specline-pipeline/SKILL.md +604 -0
  42. package/core/skills/specline-pipeline/references/error-recovery-details.md +49 -0
  43. package/core/skills/specline-pipeline/references/event-log-spec.md +59 -0
  44. package/core/skills/specline-pipeline/references/pipeline-state-schema.md +87 -0
  45. package/core/skills/specline-pipeline/templates/subagent-prompts.md +397 -0
  46. package/core/skills/specline-propose/SKILL.md +186 -0
  47. package/core/skills/specline-quickfix/SKILL.md +289 -0
  48. package/core/templates/AGENTS.md.hbs +5 -0
  49. package/core/templates/specline/config.yaml +15 -0
  50. package/lib/deploy-claude.mjs +80 -0
  51. package/lib/deploy-codex.mjs +77 -0
  52. package/lib/deploy-opencode.mjs +93 -0
  53. package/lib/deploy.mjs +668 -0
  54. package/lib/gate.mjs +103 -0
  55. package/lib/hash.mjs +13 -0
  56. package/lib/hook.mjs +105 -0
  57. package/lib/init.mjs +122 -0
  58. package/lib/lock.mjs +99 -0
  59. package/lib/merge.mjs +188 -0
  60. package/lib/paths.mjs +40 -0
  61. package/lib/platforms.mjs +74 -0
  62. package/lib/render-agents.mjs +88 -0
  63. package/lib/render.mjs +126 -0
  64. package/lib/sync.mjs +253 -0
  65. package/lib/tty-select.mjs +89 -0
  66. package/package.json +4 -1
  67. package/templates/.cursor/README.md +18 -0
  68. package/templates/.cursor/agents/specline-code-reviewer.md +18 -2
  69. package/templates/.cursor/agents/specline-spec-creator.md +51 -2
  70. package/templates/.cursor/agents/specline-test-runner.md +10 -1
  71. package/templates/.cursor/agents/specline-test-writer.md +58 -7
  72. package/templates/.cursor/hooks/specline-pipeline-gate-checks/a2-a3-reverse.sh +1 -1
  73. package/templates/.cursor/hooks/specline-pipeline-gate.sh +118 -0
  74. package/templates/.cursor/skills/specline-pipeline/SKILL.md +10 -4
  75. package/templates/.cursor/skills/specline-propose/SKILL.md +3 -3
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # a1-covers-ref.sh — A1: Covers 引用存在性验证
4
+ #
5
+ # 验证 tasks.md 中每个任务的 Covers 字段引用的 Requirement 名称和 Scenario
6
+ # 名称在 spec.md 中实际存在。
7
+ #
8
+ # 兼容 bash 3.2+(macOS 默认版本),不使用关联数组(declare -A)。
9
+ #
10
+ # 依赖 common.sh 中定义的:
11
+ # - semantic_error(code, msg)
12
+ # - semantic_warn(code, msg)
13
+ # - semantic_info(code, msg)
14
+ # - SEMANTIC_ERRORS / SEMANTIC_WARNINGS / SEMANTIC_INFOS 全局计数器
15
+ #
16
+ # 环境变量:
17
+ # SPEC_FILE — spec.md 的路径
18
+ # TASKS_FILE — tasks.md 的路径
19
+
20
+ # 确保正确处理多字节 UTF-8 字符(中文 Scenario/Requirement 名称)
21
+ export LC_ALL="${LC_ALL:-zh_CN.UTF-8}"
22
+
23
+ run_a1_covers_ref() {
24
+ # ==== 输入校验 ====
25
+ if [ ! -f "${SPEC_FILE:-}" ]; then
26
+ semantic_error "A1" "spec.md 不存在: ${SPEC_FILE:-未设置}"
27
+ return
28
+ fi
29
+
30
+ if [ ! -f "${TASKS_FILE:-}" ]; then
31
+ semantic_error "A1" "tasks.md 不存在: ${TASKS_FILE:-未设置}"
32
+ return
33
+ fi
34
+
35
+ # ==== 临时文件(存储 Requirement 和 Scenario 名称集合) ====
36
+ # 兼容 bash 3.2,不使用 declare -A 关联数组
37
+ local _req_file _scen_file
38
+ _req_file=$(mktemp) || { semantic_error "A1" "无法创建临时文件"; return; }
39
+ _scen_file=$(mktemp) || { rm -f "$_req_file"; semantic_error "A1" "无法创建临时文件"; return; }
40
+
41
+ # ==== 1. 从 spec.md 提取 Requirement 和 Scenario 名称 ====
42
+ local current_req="" scen_name=""
43
+
44
+ while IFS= read -r line; do
45
+ if [[ "$line" =~ ^###[[:space:]]+Requirement:[[:space:]]+(.+)$ ]]; then
46
+ current_req="${BASH_REMATCH[1]}"
47
+ current_req=$(echo "$current_req" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
48
+ echo "$current_req" >> "$_req_file"
49
+ elif [[ "$line" =~ ^####[[:space:]]+Scenario:[[:space:]]+(.+)$ ]]; then
50
+ scen_name="${BASH_REMATCH[1]}"
51
+ scen_name=$(echo "$scen_name" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
52
+ if [ -n "$current_req" ]; then
53
+ echo "${current_req}|${scen_name}" >> "$_scen_file"
54
+ fi
55
+ fi
56
+ done < "$SPEC_FILE"
57
+
58
+ # ==== 2. 从 tasks.md 解析 Covers 引用 ====
59
+ local task_num=0
60
+ local covers_content req_name scenarios_str split_list
61
+ covers_content=""; req_name=""; scenarios_str=""; split_list=""
62
+
63
+ while IFS= read -r line; do
64
+ # 追踪任务编号(从 "## N." 标题行)
65
+ if [[ "$line" =~ ^##[[:space:]]+([0-9]+)\. ]]; then
66
+ task_num="${BASH_REMATCH[1]}"
67
+ continue
68
+ fi
69
+
70
+ # 跳过非 Covers 行
71
+ if [[ "$line" != *"**Covers**:"* ]]; then
72
+ continue
73
+ fi
74
+
75
+ # 提取 Covers 内容
76
+ covers_content=$(echo "$line" \
77
+ | sed 's/.*\*\*Covers\*\*:[[:space:]]*//' \
78
+ | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
79
+
80
+ # 格式检查:必须有 "Requirement:" 前缀
81
+ if [[ ! "$covers_content" =~ Requirement: ]]; then
82
+ semantic_warn "A1" "任务 $task_num 的 Covers 行格式不规范,跳过该任务的引用验证"
83
+ continue
84
+ fi
85
+
86
+ # 提取 Requirement 名称(Requirement: 之后到第一个分隔符之前)
87
+ req_name=$(echo "$covers_content" \
88
+ | sed -n 's/.*Requirement:[[:space:]]*//p' \
89
+ | sed 's/[[:space:]]*[,,、].*//' \
90
+ | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
91
+
92
+ if [ -z "$req_name" ]; then
93
+ semantic_warn "A1" "任务 $task_num 的 Covers 行缺少 Requirement 名称,跳过该任务的引用验证"
94
+ continue
95
+ fi
96
+
97
+ # 校验 Requirement 存在性
98
+ if ! grep -qxF "$req_name" "$_req_file" 2>/dev/null; then
99
+ semantic_error "A1" "Covers 引用不存在: 任务 $task_num 引用了不存在的 Requirement \"$req_name\""
100
+ fi
101
+
102
+ # 提取并校验 Scenario 名称列表
103
+ if [[ "$covers_content" =~ Scenario:[[:space:]]*(.+)$ ]]; then
104
+ scenarios_str="${BASH_REMATCH[1]}"
105
+ scenarios_str=$(echo "$scenarios_str" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
106
+
107
+ # 拆分 Scenario 名称(分隔符:、 , ,)
108
+ split_list=$(echo "$scenarios_str" \
109
+ | sed 's/[、,]/\'$'\n''/g' \
110
+ | sed 's/,[[:space:]]*/\'$'\n''/g')
111
+
112
+ while IFS= read -r scen_name; do
113
+ scen_name=$(echo "$scen_name" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
114
+ [ -z "$scen_name" ] && continue
115
+
116
+ if ! grep -qxF "${req_name}|${scen_name}" "$_scen_file" 2>/dev/null; then
117
+ semantic_error "A1" "Covers 引用不存在: 任务 $task_num 引用了不存在的 Scenario \"$scen_name\"(在 Requirement \"$req_name\" 下)"
118
+ fi
119
+ done <<< "$split_list"
120
+ fi
121
+ done < "$TASKS_FILE"
122
+
123
+ # ==== 清理临时文件 ====
124
+ rm -f "$_req_file" "$_scen_file"
125
+ }
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # a2-a3-reverse.sh — A2/A3 反向覆盖验证
4
+ #
5
+ # 从 spec.md 提取所有 Requirement 和 Scenario 名称,
6
+ # 与 tasks.md 中 Covers 字段引用的名称做交叉比对,
7
+ # 输出未被任何任务覆盖的 Requirement 和 Scenario(INFO 级别)。
8
+ #
9
+ # 依赖环境变量:
10
+ # SPEC_FILE — spec.md 文件路径
11
+ # TASKS_FILE — tasks.md 文件路径
12
+ # SPEC_REVIEW_FILE — 可选,spec-review.json 路径(交叉验证)
13
+ #
14
+ # 使用方式:
15
+ # source a2-a3-reverse.sh
16
+ # SPEC_FILE=... TASKS_FILE=... run_a2_a3_reverse
17
+
18
+ run_a2_a3_reverse() {
19
+ local spec_file="${SPEC_FILE:-}"
20
+ local tasks_file="${TASKS_FILE:-}"
21
+ local review_file="${SPEC_REVIEW_FILE:-}"
22
+
23
+ # 验证输入文件
24
+ if [ -z "$spec_file" ] || [ ! -f "$spec_file" ]; then
25
+ echo "ERROR: spec.md 文件不存在或未通过 SPEC_FILE 指定: ${spec_file:-未设置}" >&2
26
+ return 1
27
+ fi
28
+ if [ -z "$tasks_file" ] || [ ! -f "$tasks_file" ]; then
29
+ echo "ERROR: tasks.md 文件不存在或未通过 TASKS_FILE 指定: ${tasks_file:-未设置}" >&2
30
+ return 1
31
+ fi
32
+
33
+ # =========================================
34
+ # Step 1: 从 spec.md 提取所有 Requirement 和 Scenario 名称
35
+ # =========================================
36
+
37
+ local tmp_spec_reqs
38
+ tmp_spec_reqs=$(mktemp) || return 1
39
+ local tmp_spec_scens
40
+ tmp_spec_scens=$(mktemp) || return 1
41
+ local tmp_tasks_reqs
42
+ tmp_tasks_reqs=$(mktemp) || return 1
43
+ local tmp_tasks_scens
44
+ tmp_tasks_scens=$(mktemp) || return 1
45
+
46
+ _a2a3_cleanup() {
47
+ rm -f "${tmp_spec_reqs:-}" "${tmp_spec_scens:-}" "${tmp_tasks_reqs:-}" "${tmp_tasks_scens:-}"
48
+ }
49
+ trap _a2a3_cleanup RETURN
50
+
51
+ # 从 spec.md 解析 Requirement 和 Scenario
52
+ local current_req=""
53
+ while IFS= read -r line; do
54
+ if [[ "$line" =~ ^###[[:space:]]+Requirement:[[:space:]]+(.+)$ ]]; then
55
+ current_req="${BASH_REMATCH[1]}"
56
+ current_req="${current_req%"${current_req##*[![:space:]]}"}"
57
+ echo "$current_req" >> "$tmp_spec_reqs"
58
+ elif [[ "$line" =~ ^####[[:space:]]+Scenario:[[:space:]]+(.+)$ ]]; then
59
+ local scen="${BASH_REMATCH[1]}"
60
+ scen="${scen%"${scen##*[![:space:]]}"}"
61
+ if [ -n "$current_req" ]; then
62
+ printf '%s\t%s\n' "$current_req" "$scen" >> "$tmp_spec_scens"
63
+ fi
64
+ fi
65
+ done < "$spec_file"
66
+
67
+ # =========================================
68
+ # Step 2: 从 tasks.md 提取 Covers 引用的 Requirement 和 Scenario
69
+ # =========================================
70
+
71
+ while IFS= read -r line; do
72
+ if [[ "$line" =~ \*\*Covers\*\*[[:space:]]*:[[:space:]]*(.+) ]]; then
73
+ local covers_content="${BASH_REMATCH[1]}"
74
+
75
+ # 提取 Requirement 名称
76
+ if [[ "$covers_content" =~ Requirement:[[:space:]]*([^,,]+) ]]; then
77
+ local task_req="${BASH_REMATCH[1]}"
78
+ task_req="${task_req%"${task_req##*[![:space:]]}"}"
79
+ echo "$task_req" >> "$tmp_tasks_reqs"
80
+ fi
81
+
82
+ # 提取 Scenario 名称
83
+ if [[ "$covers_content" =~ Scenario:[[:space:]]*(.+)$ ]]; then
84
+ local scenarios_str="${BASH_REMATCH[1]}"
85
+ scenarios_str="${scenarios_str%"${scenarios_str##*[![:space:]]}"}"
86
+
87
+ # 用 、或 , 或 ,分割 Scenario 名称
88
+ local cleaned="${scenarios_str//、/ }"
89
+ cleaned="${cleaned//,/,}"
90
+ cleaned="${cleaned//,/ }"
91
+ cleaned="${cleaned//\// }"
92
+
93
+ for item in $cleaned; do
94
+ item="${item#"${item%%[![:space:]]*}"}"
95
+ item="${item%"${item##*[![:space:]]}"}"
96
+ if [ -n "$item" ]; then
97
+ echo "$item" >> "$tmp_tasks_scens"
98
+ fi
99
+ done
100
+ fi
101
+ fi
102
+ done < "$tasks_file"
103
+
104
+ # =========================================
105
+ # Step 3: 计算差集 — 未被覆盖的 Requirement 和 Scenario
106
+ # =========================================
107
+
108
+ sort -u "$tmp_spec_reqs" -o "$tmp_spec_reqs" 2>/dev/null || true
109
+ sort -u "$tmp_spec_scens" -o "$tmp_spec_scens" 2>/dev/null || true
110
+ sort -u "$tmp_tasks_reqs" -o "$tmp_tasks_reqs" 2>/dev/null || true
111
+ sort -u "$tmp_tasks_scens" -o "$tmp_tasks_scens" 2>/dev/null || true
112
+
113
+ local uncovered_req_count=0
114
+ local uncovered_scen_count=0
115
+
116
+ # 查找未被覆盖的 Requirement
117
+ if [ -s "$tmp_spec_reqs" ]; then
118
+ while IFS= read -r req; do
119
+ if ! grep -qxF "$req" "$tmp_tasks_reqs" 2>/dev/null; then
120
+ semantic_info "A2/A3" "Requirement \"${req}\" 不被任何任务覆盖"
121
+ uncovered_req_count=$((uncovered_req_count + 1))
122
+ fi
123
+ done < "$tmp_spec_reqs"
124
+ fi
125
+
126
+ # 查找未被覆盖的 Scenario
127
+ if [ -s "$tmp_spec_scens" ]; then
128
+ while IFS=$'\t' read -r req scen; do
129
+ if ! grep -qxF "$scen" "$tmp_tasks_scens" 2>/dev/null; then
130
+ semantic_info "A2/A3" "Scenario \"${scen}\"(Requirement: \"${req}\")不被任何任务覆盖"
131
+ uncovered_scen_count=$((uncovered_scen_count + 1))
132
+ fi
133
+ done < "$tmp_spec_scens"
134
+ fi
135
+
136
+ # =========================================
137
+ # Step 4: 全部覆盖时的汇总信息
138
+ # =========================================
139
+ if [ "$uncovered_req_count" -eq 0 ] && [ "$uncovered_scen_count" -eq 0 ]; then
140
+ semantic_info "A2/A3" "所有 Requirement 和 Scenario 均被 Covers 覆盖"
141
+ fi
142
+
143
+ # =========================================
144
+ # Step 5: 与 spec-review.json 交叉验证(如果存在)
145
+ # =========================================
146
+ if [ -z "$review_file" ]; then
147
+ local spec_dir
148
+ spec_dir=$(dirname "$spec_file")
149
+ if [ -f "${spec_dir}/spec-review.json" ]; then
150
+ review_file="${spec_dir}/spec-review.json"
151
+ fi
152
+ fi
153
+
154
+ if [ -n "$review_file" ] && [ -f "$review_file" ]; then
155
+ local review_reqs_covered
156
+ local review_reqs_total
157
+ review_reqs_covered=$(jq -r '.coverage.requirements_covered // "N/A"' "$review_file" 2>/dev/null || echo "N/A")
158
+ review_reqs_total=$(jq -r '.coverage.requirements_total // "N/A"' "$review_file" 2>/dev/null || echo "N/A")
159
+
160
+ if [ "$review_reqs_covered" != "N/A" ] && [ "$review_reqs_total" != "N/A" ]; then
161
+ local spec_total_reqs
162
+ local spec_covered_reqs
163
+ spec_total_reqs=$(wc -l < "$tmp_spec_reqs" | tr -d '[:space:]')
164
+ spec_covered_reqs=$((spec_total_reqs - uncovered_req_count))
165
+
166
+ if [ "$spec_covered_reqs" -ne "$review_reqs_covered" ] || [ "$spec_total_reqs" -ne "$review_reqs_total" ]; then
167
+ semantic_info "A2/A3" "与 spec-review.json 差异: A2 发现 ${spec_total_reqs} 个 Requirement(${spec_covered_reqs} 被覆盖),spec-review.json 报告 ${review_reqs_total} 个(${review_reqs_covered} 被覆盖)"
168
+ fi
169
+ fi
170
+ fi
171
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # c1-exception.sh — C1: 异常场景覆盖率检测
4
+ #
5
+ # 检查 spec.md 中每个 Requirement 是否至少包含一个异常/错误场景。
6
+ # 使用 SPEC_FILE 环境变量定位 spec 文件(由 gate_semantic 设置)。
7
+ #
8
+ # 异常关键词(不区分大小写):
9
+ # 错误|失败|异常|超时|无效|不存在|未找到|拒绝|过期|冲突|超出|不允许|未授权|已存在
10
+ #
11
+ # Usage(由 gate_semantic source 后调用):
12
+ # run_c1_exception
13
+
14
+ run_c1_exception() {
15
+ # 检查 spec 文件是否存在
16
+ if [ ! -f "$SPEC_FILE" ]; then
17
+ return 0
18
+ fi
19
+
20
+ # 检查是否有 Requirement 区块
21
+ local req_count
22
+ req_count=$(grep -c '^### Requirement:' "$SPEC_FILE" 2>/dev/null || true)
23
+ req_count="${req_count:-0}"
24
+ if [ "$req_count" = "0" ]; then
25
+ semantic_warn "C1" "未找到任何 Requirement 区块,跳过异常场景覆盖率检查"
26
+ return 0
27
+ fi
28
+
29
+ # 异常关键词(不区分大小写,用 grep -iE 扩展正则)
30
+ local keywords="错误|失败|异常|超时|无效|不存在|未找到|拒绝|过期|冲突|超出|不允许|未授权|已存在"
31
+
32
+ # 用 awk 按 Requirement 分组 Scenario 标题
33
+ # 输出格式: 需求名称|场景标题1|场景标题2|...
34
+ # 将 awk 输出捕获到变量,再用 here-string 逐行处理,避免管道 subshell 导致计数器丢失
35
+ local grouped
36
+ grouped=$(awk '
37
+ /^### Requirement:/ {
38
+ if (current_req != "") print current_req "|" scenarios
39
+ current_req = $0
40
+ sub(/^### Requirement: /, "", current_req)
41
+ scenarios = ""
42
+ }
43
+ /^#### Scenario:/ {
44
+ s = $0
45
+ sub(/^#### Scenario: /, "", s)
46
+ if (scenarios != "") scenarios = scenarios "|"
47
+ scenarios = scenarios s
48
+ }
49
+ END {
50
+ if (current_req != "") print current_req "|" scenarios
51
+ }
52
+ ' "$SPEC_FILE")
53
+
54
+ # 逐行处理每个 Requirement
55
+ while IFS='|' read -r req_name scenarios_str; do
56
+ [ -z "$req_name" ] && continue
57
+
58
+ # 统计该 Requirement 的 Scenario 数量
59
+ local scenario_count
60
+ if [ -z "$scenarios_str" ]; then
61
+ scenario_count=0
62
+ else
63
+ scenario_count=$(echo "$scenarios_str" | awk -F'|' '{print NF}')
64
+ fi
65
+
66
+ # 检查 Scenario 标题集合中是否包含异常关键词
67
+ if ! echo "$scenarios_str" | grep -qiE "$keywords" 2>/dev/null; then
68
+ semantic_warn "C1" "Requirement \"${req_name}\" 缺少异常场景(${scenario_count} 个 Scenario 中无异常/错误场景关键词)"
69
+ fi
70
+ done <<< "$grouped"
71
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # c2-vague.sh — C2 检查:模糊需求检测
4
+ #
5
+ # 扫描 spec.md 中不可量化的模糊表述(如"足够快""适当"等),
6
+ # 跳过代码块(```)内内容,对匹配到的模糊词输出 WARNING。
7
+ #
8
+ # 依赖:common.sh(提供 semantic_warn 函数和 SEMANTIC_WARNINGS 计数器)
9
+ # 环境变量:SPEC_FILE — 指向 spec.md 的路径
10
+
11
+ # 加载共享工具函数(允许独立测试时直接 source)
12
+ if [ -z "${SEMANTIC_WARNINGS:-}" ]; then
13
+ COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ if [ -f "$COMMON_DIR/common.sh" ]; then
15
+ source "$COMMON_DIR/common.sh"
16
+ fi
17
+ fi
18
+
19
+ run_c2_vague() {
20
+ if [ -z "${SPEC_FILE:-}" ] || [ ! -f "$SPEC_FILE" ]; then
21
+ echo "⚠️ [WARNING] (C2) SPEC_FILE 未设置或文件不存在,跳过模糊需求检测" >&2
22
+ return 0
23
+ fi
24
+
25
+ # 模糊词正则模式
26
+ # 性能良好|足够快|适当[的地]|合理[的地]|必要时|等等|较好[的地]|
27
+ # 尽量|较大规模|可能[的地]|若干|一定[的地]|一般[的地]|基本的|少量的
28
+ local VAGUE_PATTERN='性能良好|足够快|适当[的地]|合理[的地]|必要时|等等|较好[的地]|尽量|较大规模|可能[的地]|若干|一定[的地]|一般[的地]|基本的|少量的'
29
+
30
+ local in_codeblock=0
31
+ local line_num=0
32
+
33
+ while IFS= read -r line; do
34
+ line_num=$((line_num + 1))
35
+
36
+ # 检测代码块边界:以 ``` 开头(忽略后面的语言标记)
37
+ if [[ "$line" =~ ^\`\`\` ]]; then
38
+ if [ "$in_codeblock" -eq 0 ]; then
39
+ in_codeblock=1
40
+ else
41
+ in_codeblock=0
42
+ fi
43
+ continue
44
+ fi
45
+
46
+ # 跳过代码块内部的行
47
+ if [ "$in_codeblock" -eq 1 ]; then
48
+ continue
49
+ fi
50
+
51
+ # 对普通文本行执行模糊词匹配
52
+ if echo "$line" | grep -qE "$VAGUE_PATTERN" 2>/dev/null; then
53
+ # 提取上下文(去除首尾空白)
54
+ local context
55
+ context=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
56
+
57
+ semantic_warn "C2" "模糊需求表述: 第 ${line_num} 行 \"${context}\""
58
+ fi
59
+ done < "$SPEC_FILE"
60
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # common.sh — Specline Pipeline Gate 语义检查共享工具
4
+ #
5
+ # 提供:
6
+ # - 全局计数器: SEMANTIC_ERRORS, SEMANTIC_WARNINGS, SEMANTIC_INFOS
7
+ # - 严重度报告函数: semantic_error(), semantic_warn(), semantic_info()
8
+ # - 文件定位函数: find_spec_file(), find_tasks_file()
9
+ #
10
+ # 兼容性: bash 3.2+ (macOS 默认 bash)
11
+
12
+ set -euo pipefail
13
+
14
+ # ===== 全局计数器 =====
15
+ SEMANTIC_ERRORS=${SEMANTIC_ERRORS:-0}
16
+ SEMANTIC_WARNINGS=${SEMANTIC_WARNINGS:-0}
17
+ SEMANTIC_INFOS=${SEMANTIC_INFOS:-0}
18
+
19
+ # ===== 严重度报告函数 =====
20
+
21
+ # semantic_error <code> <message>
22
+ # 输出 ERROR 级别消息到 stderr,并增加 SEMANTIC_ERRORS 计数
23
+ semantic_error() {
24
+ local code="$1"
25
+ local msg="$2"
26
+ echo "❌ [ERROR] (${code}) ${msg}" >&2
27
+ SEMANTIC_ERRORS=$((SEMANTIC_ERRORS + 1))
28
+ }
29
+
30
+ # semantic_warn <code> <message>
31
+ # 输出 WARNING 级别消息到 stdout,并增加 SEMANTIC_WARNINGS 计数
32
+ semantic_warn() {
33
+ local code="$1"
34
+ local msg="$2"
35
+ echo "⚠️ [WARNING] (${code}) ${msg}"
36
+ SEMANTIC_WARNINGS=$((SEMANTIC_WARNINGS + 1))
37
+ }
38
+
39
+ # semantic_info <code> <message>
40
+ # 输出 INFO 级别消息到 stdout,并增加 SEMANTIC_INFOS 计数
41
+ semantic_info() {
42
+ local code="$1"
43
+ local msg="$2"
44
+ echo "ℹ️ [INFO] (${code}) ${msg}"
45
+ SEMANTIC_INFOS=$((SEMANTIC_INFOS + 1))
46
+ }
47
+
48
+ # ===== 文件定位函数 =====
49
+
50
+ # find_spec_file
51
+ # 在 specline/changes/$CHANGE/specs/ 下查找 spec.md
52
+ find_spec_file() {
53
+ if [ -z "${CHANGE:-}" ]; then
54
+ echo ""
55
+ return
56
+ fi
57
+ find "${PROJECT_ROOT:-.}/specline/changes/${CHANGE}/specs" -name "spec.md" 2>/dev/null | head -1
58
+ }
59
+
60
+ # find_tasks_file
61
+ # 返回 tasks.md 路径
62
+ find_tasks_file() {
63
+ if [ -z "${CHANGE:-}" ]; then
64
+ echo ""
65
+ return
66
+ fi
67
+ echo "${PROJECT_ROOT:-.}/specline/changes/${CHANGE}/tasks.md"
68
+ }
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # d1-cycle.sh — D1: 依赖环路检测
4
+ #
5
+ # 使用 awk 实现三色 DFS 检测 tasks.md 中任务依赖关系是否形成环路。
6
+ # 核心逻辑在 awk 中执行,避免 bash 版本兼容性问题(macOS bash 3.2)。
7
+ #
8
+ # 用法:
9
+ # export TASKS_FILE=/path/to/tasks.md
10
+ # source d1-cycle.sh && run_d1_cycle
11
+ #
12
+ # 环境变量:
13
+ # TASKS_FILE — tasks.md 文件路径
14
+
15
+ # 确保计数器变量已定义(兼容独立 source 运行场景)
16
+ : "${SEMANTIC_ERRORS:=0}"
17
+ : "${SEMANTIC_WARNINGS:=0}"
18
+ : "${SEMANTIC_INFOS:=0}"
19
+
20
+ run_d1_cycle() {
21
+ local tasks_file="${TASKS_FILE:-}"
22
+
23
+ if [ -z "$tasks_file" ] || [ ! -f "$tasks_file" ]; then
24
+ echo "⚠️ D1: TASKS_FILE 未设置或文件不存在,跳过依赖环路检测" >&2
25
+ return 0
26
+ fi
27
+
28
+ # ──────────────────────────────────────────────
29
+ # 使用 awk 完成全部检测逻辑:
30
+ # 1. 解析 Depends 行构建邻接表
31
+ # 2. 三色 DFS 环路检测
32
+ # 3. 输出 "SELF:task_id" 或 "CYCLE:path" 行
33
+ # ──────────────────────────────────────────────
34
+ local result
35
+ result=$(awk '
36
+ # 解析任务编号
37
+ /^## / {
38
+ task = $2
39
+ gsub(/\..*/, "", task)
40
+ }
41
+ # 解析 Depends 行
42
+ /\*\*Depends\*\*:/ {
43
+ deps = $0
44
+ sub(/.*\*\*Depends\*\*:[ \t]*/, "", deps)
45
+ gsub(/^[ \t]+|[ \t]+$/, "", deps)
46
+ if (deps ~ /\(none\)/) {
47
+ deps = ""
48
+ }
49
+ # 清理依赖列表:去空格,过滤非数字字符(防止意外格式变化)
50
+ gsub(/[ \t]/, "", deps)
51
+ gsub(/[^0-9,]/, "", deps)
52
+ gsub(/,/, " ", deps)
53
+ # 去掉多余的连续空格
54
+ gsub(/ +/, " ", deps)
55
+ gsub(/^ | $/, "", deps)
56
+ adj[task] = deps
57
+ }
58
+ END {
59
+ if (length(adj) == 0) exit 0
60
+
61
+ # 收集所有任务 ID
62
+ for (t in adj) {
63
+ all_tasks[t] = 1
64
+ }
65
+
66
+ errors = 0
67
+
68
+ # 对每个任务执行 DFS(使用参数传递路径,自动处理回溯)
69
+ for (t in all_tasks) {
70
+ if (!(t in color) || color[t] == 0) {
71
+ dfs(t, "")
72
+ }
73
+ }
74
+ }
75
+
76
+ function dfs(node, path, neighbors_str, n, i, arr, cnt, new_path, path_idx) {
77
+ color[node] = 1
78
+ new_path = path (path == "" ? "" : " ") node
79
+
80
+ neighbors_str = adj[node]
81
+
82
+ cnt = split(neighbors_str, arr, " ")
83
+ for (i = 1; i <= cnt; i++) {
84
+ n = arr[i]
85
+ if (n == "") continue
86
+
87
+ # 自引用
88
+ if (n == node) {
89
+ print "SELF:" node
90
+ errors++
91
+ continue
92
+ }
93
+
94
+ # 检查颜色
95
+ if (n in color && color[n] == 1) {
96
+ # 发现环路:从 new_path 中提取从 n 开始的路径
97
+ path_idx = index(" " new_path " ", " " n " ")
98
+ if (path_idx > 0) {
99
+ cycle_seg = substr(new_path, path_idx)
100
+ } else {
101
+ cycle_seg = new_path
102
+ }
103
+ gsub(/ /, " → ", cycle_seg)
104
+ print "CYCLE:" cycle_seg " → " n
105
+ errors++
106
+ } else if (!(n in color) || color[n] == 0) {
107
+ dfs(n, new_path)
108
+ }
109
+ }
110
+
111
+ color[node] = 2
112
+ }
113
+ ' "$tasks_file" 2>&1)
114
+
115
+ # ──────────────────────────────────────────────
116
+ # 处理 awk 输出,调用 semantic_error
117
+ # ──────────────────────────────────────────────
118
+ if [ -z "$result" ]; then
119
+ echo "✅ D1 依赖环路检测通过(无环路)"
120
+ return 0
121
+ fi
122
+
123
+ local line
124
+ while IFS= read -r line; do
125
+ [ -z "$line" ] && continue
126
+ case "$line" in
127
+ SELF:*)
128
+ local self_task="${line#SELF:}"
129
+ if type -t semantic_error &>/dev/null; then
130
+ semantic_error "D1" "依赖环路检测: 任务 ${self_task} 自引用"
131
+ else
132
+ echo "❌ [ERROR] (D1) 依赖环路检测: 任务 ${self_task} 自引用" >&2
133
+ ((SEMANTIC_ERRORS++))
134
+ fi
135
+ ;;
136
+ CYCLE:*)
137
+ local cycle_path="${line#CYCLE:}"
138
+ if type -t semantic_error &>/dev/null; then
139
+ semantic_error "D1" "依赖环路检测: 发现环路 ${cycle_path}"
140
+ else
141
+ echo "❌ [ERROR] (D1) 依赖环路检测: 发现环路 ${cycle_path}" >&2
142
+ ((SEMANTIC_ERRORS++))
143
+ fi
144
+ ;;
145
+ esac
146
+ done <<< "$result"
147
+
148
+ return 0
149
+ }