specline 2.0.0 → 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/lib/merge.mjs +7 -3
- 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,171 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
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
|
-
}
|