specline 1.2.2 → 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.
- package/cli.mjs +240 -11
- 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 +25 -1
- package/templates/specline/config.yaml +44 -0
|
@@ -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
|
+
}
|
|
@@ -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
|