specline 1.2.2 → 1.3.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.
- package/README.md +15 -7
- package/cli.mjs +260 -29
- package/package.json +1 -1
- package/templates/.cursor/agents/specline-spec-creator.md +23 -5
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a1-covers-ref.sh +125 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a2-a3-reverse.sh +171 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/c1-exception.sh +71 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/c2-vague.sh +60 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/common.sh +68 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/d1-cycle.sh +149 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/d3-type-file.sh +260 -0
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +776 -159
- package/templates/.cursor/skills/specline-apply-change/SKILL.md +4 -0
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +76 -594
- package/templates/.cursor/skills/specline-pipeline/references/error-recovery-details.md +49 -0
- package/templates/.cursor/skills/specline-pipeline/references/event-log-spec.md +59 -0
- package/templates/.cursor/skills/specline-pipeline/references/pipeline-state-schema.md +87 -0
- package/templates/.cursor/skills/specline-pipeline/templates/subagent-prompts.md +221 -0
- package/templates/specline/config.yaml +44 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# d3-type-file.sh - D3: Type-File 一致性检测
|
|
4
|
+
#
|
|
5
|
+
# 检测 tasks.md 中每个任务的 Type 字段与 Files 字段的扩展名是否一致。
|
|
6
|
+
# 通过 source 加载,定义 run_d3_type_file() 函数。
|
|
7
|
+
#
|
|
8
|
+
# 依赖:
|
|
9
|
+
# - common.sh(提供 semantic_warn / semantic_info 和全局计数器)
|
|
10
|
+
# - 环境变量 TASKS_FILE(由 gate_semantic 设置)
|
|
11
|
+
#
|
|
12
|
+
# 兼容性: bash 3.2+ (macOS 默认 bash)
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
# ===== Type -> 期望扩展名匹配函数(兼容 bash 3.2,无关联数组)=====
|
|
17
|
+
#
|
|
18
|
+
# is_extension_for_type <type> <extension>
|
|
19
|
+
# 返回: 0 = 匹配, 1 = 不匹配
|
|
20
|
+
is_extension_for_type() {
|
|
21
|
+
local t="$1"
|
|
22
|
+
local e="$2"
|
|
23
|
+
|
|
24
|
+
case "$t" in
|
|
25
|
+
frontend)
|
|
26
|
+
case "$e" in
|
|
27
|
+
tsx|jsx|css|scss|less|html|vue|svelte) return 0 ;;
|
|
28
|
+
*) return 1 ;;
|
|
29
|
+
esac
|
|
30
|
+
;;
|
|
31
|
+
backend)
|
|
32
|
+
case "$e" in
|
|
33
|
+
py|go|rs|java|rb|php) return 0 ;;
|
|
34
|
+
*) return 1 ;;
|
|
35
|
+
esac
|
|
36
|
+
;;
|
|
37
|
+
infra)
|
|
38
|
+
case "$e" in
|
|
39
|
+
yaml|yml|tf|toml) return 0 ;;
|
|
40
|
+
*) return 1 ;;
|
|
41
|
+
esac
|
|
42
|
+
;;
|
|
43
|
+
db)
|
|
44
|
+
case "$e" in
|
|
45
|
+
sql|prisma) return 0 ;;
|
|
46
|
+
*) return 1 ;;
|
|
47
|
+
esac
|
|
48
|
+
;;
|
|
49
|
+
config)
|
|
50
|
+
case "$e" in
|
|
51
|
+
json|yaml|yml|toml|cfg|env) return 0 ;;
|
|
52
|
+
*) return 1 ;;
|
|
53
|
+
esac
|
|
54
|
+
;;
|
|
55
|
+
docs)
|
|
56
|
+
case "$e" in
|
|
57
|
+
md|rst|txt) return 0 ;;
|
|
58
|
+
*) return 1 ;;
|
|
59
|
+
esac
|
|
60
|
+
;;
|
|
61
|
+
*)
|
|
62
|
+
return 1
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# ===== 特殊文件名匹配 =====
|
|
68
|
+
# infra 类型下的无扩展名特殊文件
|
|
69
|
+
is_infra_special_name() {
|
|
70
|
+
local name="$1"
|
|
71
|
+
local lc_name
|
|
72
|
+
lc_name=$(echo "$name" | tr '[:upper:]' '[:lower:]')
|
|
73
|
+
case "$lc_name" in
|
|
74
|
+
dockerfile|docker-compose|docker-compose.yml|docker-compose.yaml) return 0 ;;
|
|
75
|
+
*) return 1 ;;
|
|
76
|
+
esac
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ===== check_file_against_type =====
|
|
80
|
+
# 参数: $1 = task_id, $2 = type, $3 = file_path
|
|
81
|
+
# 返回: 0 = 一致, 1 = 不匹配
|
|
82
|
+
check_file_against_type() {
|
|
83
|
+
local task_id="$1"
|
|
84
|
+
local task_type="$2"
|
|
85
|
+
local file_path="$3"
|
|
86
|
+
|
|
87
|
+
# 提取扩展名
|
|
88
|
+
local ext=""
|
|
89
|
+
local basename
|
|
90
|
+
basename=$(basename "$file_path")
|
|
91
|
+
|
|
92
|
+
# 判断是否有扩展名(文件名包含点号且点号不在开头)
|
|
93
|
+
case "$basename" in
|
|
94
|
+
*.*)
|
|
95
|
+
ext="${basename##*.}"
|
|
96
|
+
ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
|
|
97
|
+
;;
|
|
98
|
+
*)
|
|
99
|
+
ext=""
|
|
100
|
+
;;
|
|
101
|
+
esac
|
|
102
|
+
|
|
103
|
+
# --- 特殊规则 1: infra 类型下,无扩展名或特殊名称文件 ---
|
|
104
|
+
if [ "$task_type" = "infra" ]; then
|
|
105
|
+
if is_infra_special_name "$basename"; then
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
# 也检查扩展名以 yml/yaml 结尾的 docker-compose 变体
|
|
109
|
+
local lc_basename
|
|
110
|
+
lc_basename=$(echo "$basename" | tr '[:upper:]' '[:lower:]')
|
|
111
|
+
case "$lc_basename" in
|
|
112
|
+
docker-compose.yml|docker-compose.yaml) return 0 ;;
|
|
113
|
+
esac
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# --- 特殊规则 2: .ts + server/ 路径 + Type: backend -> 一致 ---
|
|
117
|
+
if [ "$ext" = "ts" ] && [ "$task_type" = "backend" ]; then
|
|
118
|
+
local lc_path
|
|
119
|
+
lc_path=$(echo "$file_path" | tr '[:upper:]' '[:lower:]')
|
|
120
|
+
case "$lc_path" in
|
|
121
|
+
*server/*|*/server|*server*) return 0 ;;
|
|
122
|
+
esac
|
|
123
|
+
# 如果 .ts 文件不在 server/ 路径下且 Type 是 backend,不算匹配
|
|
124
|
+
# 继续执行下面的通用检查(.ts 不在 backend 列表中,会失败)
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# --- 特殊规则 3: db 类型下,路径包含 migration 或 schema 关键词 -> 一致 ---
|
|
128
|
+
if [ "$task_type" = "db" ]; then
|
|
129
|
+
local lc_path
|
|
130
|
+
lc_path=$(echo "$file_path" | tr '[:upper:]' '[:lower:]')
|
|
131
|
+
case "$lc_path" in
|
|
132
|
+
*migration*|*schema*) return 0 ;;
|
|
133
|
+
esac
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# --- 特殊规则 4: config 类型下,.env.xxx 变体文件(如 .env.example)-> 一致 ---
|
|
137
|
+
if [ "$task_type" = "config" ]; then
|
|
138
|
+
local lc_basename
|
|
139
|
+
lc_basename=$(echo "$basename" | tr '[:upper:]' '[:lower:]')
|
|
140
|
+
case "$lc_basename" in
|
|
141
|
+
.env|.env.*|*.env) return 0 ;;
|
|
142
|
+
esac
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# --- 通用规则: 检查扩展名 ---
|
|
146
|
+
if [ -z "$ext" ]; then
|
|
147
|
+
return 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
if is_extension_for_type "$task_type" "$ext"; then
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
return 1
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# ===== run_d3_type_file =====
|
|
158
|
+
# 主入口函数,由 gate_semantic 在 source 后调用
|
|
159
|
+
run_d3_type_file() {
|
|
160
|
+
if [ -z "${TASKS_FILE:-}" ] || [ ! -f "$TASKS_FILE" ]; then
|
|
161
|
+
echo "[WARN] TASKS_FILE 未设置或不存在,跳过 D3 检查" >&2
|
|
162
|
+
return 0
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
local all_pass=true
|
|
166
|
+
local skipped_count=0
|
|
167
|
+
local mismatch_count=0
|
|
168
|
+
|
|
169
|
+
# 用 awk 解析 tasks.md,输出结构化的任务信息
|
|
170
|
+
# 输出格式:
|
|
171
|
+
# FILE|<task_id>|<type>|<file_path>
|
|
172
|
+
# NOFILES|<task_id>|<type>
|
|
173
|
+
local parsed_data
|
|
174
|
+
parsed_data=$(awk '
|
|
175
|
+
/^## [0-9]+\./ {
|
|
176
|
+
# 新任务开始前,处理上一个任务
|
|
177
|
+
if (task_id != "" && has_type == 1 && has_files == 0) {
|
|
178
|
+
print "NOFILES|" task_id "|" task_type
|
|
179
|
+
}
|
|
180
|
+
# 提取新任务编号
|
|
181
|
+
task_id = $2
|
|
182
|
+
gsub(/\..*/, "", task_id)
|
|
183
|
+
task_type = ""
|
|
184
|
+
has_type = 0
|
|
185
|
+
has_files = 0
|
|
186
|
+
}
|
|
187
|
+
/\*\*Type\*\*:/ {
|
|
188
|
+
task_type = $0
|
|
189
|
+
sub(/.*\*\*Type\*\*:[ \t]*/, "", task_type)
|
|
190
|
+
sub(/[ \t]+$/, "", task_type)
|
|
191
|
+
has_type = 1
|
|
192
|
+
}
|
|
193
|
+
/\*\*Files\*\*:/ {
|
|
194
|
+
has_files = 1
|
|
195
|
+
files_str = $0
|
|
196
|
+
sub(/.*\*\*Files\*\*:[ \t]*/, "", files_str)
|
|
197
|
+
# 分割逗号分隔的文件列表
|
|
198
|
+
n = split(files_str, file_list, /,[ \t]*/)
|
|
199
|
+
for (i = 1; i <= n; i++) {
|
|
200
|
+
gsub(/^[ \t]+|[ \t]+$/, "", file_list[i])
|
|
201
|
+
if (file_list[i] != "") {
|
|
202
|
+
print "FILE|" task_id "|" task_type "|" file_list[i]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
END {
|
|
207
|
+
# 处理最后一个任务
|
|
208
|
+
if (task_id != "" && has_type == 1 && has_files == 0) {
|
|
209
|
+
print "NOFILES|" task_id "|" task_type
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
' "$TASKS_FILE")
|
|
213
|
+
|
|
214
|
+
# 逐行处理解析结果
|
|
215
|
+
while IFS= read -r line; do
|
|
216
|
+
[ -z "$line" ] && continue
|
|
217
|
+
|
|
218
|
+
local field1 field2 field3 field4
|
|
219
|
+
field1=$(echo "$line" | cut -d'|' -f1)
|
|
220
|
+
field2=$(echo "$line" | cut -d'|' -f2)
|
|
221
|
+
field3=$(echo "$line" | cut -d'|' -f3)
|
|
222
|
+
field4=$(echo "$line" | cut -d'|' -f4)
|
|
223
|
+
|
|
224
|
+
case "$field1" in
|
|
225
|
+
FILE)
|
|
226
|
+
local tid="$field2"
|
|
227
|
+
local ttype="$field3"
|
|
228
|
+
local fpath="$field4"
|
|
229
|
+
|
|
230
|
+
if ! check_file_against_type "$tid" "$ttype" "$fpath"; then
|
|
231
|
+
semantic_warn "D3" "任务 ${tid} (Type: ${ttype}) 的 Files 可能不匹配 — ${fpath}"
|
|
232
|
+
all_pass=false
|
|
233
|
+
mismatch_count=$((mismatch_count + 1))
|
|
234
|
+
fi
|
|
235
|
+
;;
|
|
236
|
+
NOFILES)
|
|
237
|
+
local tid="$field2"
|
|
238
|
+
local ttype="$field3"
|
|
239
|
+
semantic_info "D3" "任务 ${tid} 无 Files 字段,跳过 Type-File 一致性检查"
|
|
240
|
+
skipped_count=$((skipped_count + 1))
|
|
241
|
+
;;
|
|
242
|
+
esac
|
|
243
|
+
done <<< "$parsed_data"
|
|
244
|
+
|
|
245
|
+
# 输出汇总信息
|
|
246
|
+
if [ "$all_pass" = true ]; then
|
|
247
|
+
echo "[OK] D3 Type-File 一致性检查全部通过"
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
return 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# 如果直接执行此脚本(非 source),运行检查
|
|
254
|
+
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
255
|
+
if [ -z "${TASKS_FILE:-}" ]; then
|
|
256
|
+
echo "用法: TASKS_FILE=<path> bash $0"
|
|
257
|
+
exit 1
|
|
258
|
+
fi
|
|
259
|
+
run_d3_type_file
|
|
260
|
+
fi
|