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.
@@ -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