sillyspec 3.21.0 → 3.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.21.0",
3
+ "version": "3.22.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -1,87 +1,163 @@
1
- import { readFileSync, existsSync } from 'fs'
2
-
3
- /**
4
- * 归一化清单/allowed_paths 中的路径项:
5
- * - 去反引号、去行内括号注释(`src/foo.js (新增)` / `src/foo.js(说明)`)
6
- * - 统一为正斜杠、去首尾空白与尾部斜杠
7
- * @param {string} raw
8
- * @returns {string}
9
- */
10
- function normalizeEntry(raw) {
11
- if (!raw) return ''
12
- return raw
13
- .replace(/`/g, '')
14
- .replace(/\s*([^)]*)\s*$/, '')
15
- .replace(/\s*\([^)]*\)\s*$/, '')
16
- .replace(/\\/g, '/')
17
- .replace(/\/+$/, '')
18
- .trim()
19
- }
20
-
21
- /**
22
- * design.md 解析文件变更清单。兼容两种真实写法:
23
- * 表格:`| 操作 | 文件路径 | 说明 |`(brainstorm 模板默认)
24
- * 分类列表:`### 新增文件` / `### 修改文件` / `### 不修改文件` 下的 `- path`
25
- * 始终忽略 `.sillyspec/` 内的路径与占位符;「不修改/保留」子段下的路径会被排除。
26
- * @param {string} designMdPath - design.md 文件路径
27
- * @returns {Set<string>} 文件路径集合(相对路径,如 "src/worktree.js")
28
- */
29
- export function parseFileChangeList(designMdPath) {
30
- const result = new Set()
31
-
32
- if (!designMdPath || !existsSync(designMdPath)) return result
33
-
34
- const content = readFileSync(designMdPath, 'utf8').replace(/\r\n/g, '\n')
35
-
36
- // 定位"文件变更清单"标题
37
- const sectionRegex = /^#{2,3}\s*文件变更清单/m
38
- const sectionMatch = content.match(sectionRegex)
39
- if (!sectionMatch) return result
40
-
41
- // 从标题后开始,截取到下一个 ## 标题或文件末尾
42
- const afterSection = content.slice(sectionMatch.index + sectionMatch[0].length)
43
- const nextSectionMatch = afterSection.match(/^##\s/m)
44
- const relevantContent = nextSectionMatch
45
- ? afterSection.slice(0, nextSectionMatch.index)
46
- : afterSection
47
-
48
- const isExcluded = (p) => !p || p === '—' || p === '-' || p.startsWith('.sillyspec/')
49
-
50
- const lines = relevantContent.split('\n')
51
- let headerSkipped = false
52
- // 分类列表的当前子段模式:include(新增/修改/删除)或 exclude(不修改/保留)
53
- let listMode = 'include'
54
-
55
- for (const line of lines) {
56
- // 分类列表子标题:### 新增文件 / ### 修改文件 / ### 不修改文件
57
- const subHeader = line.match(/^###\s+(.+?)\s*$/)
58
- if (subHeader) {
59
- listMode = /不修改|不变|保留|无变更|未变更|不改动/.test(subHeader[1]) ? 'exclude' : 'include'
60
- continue
61
- }
62
-
63
- // 表格行
64
- if (line.startsWith('|')) {
65
- if (/^\|[-:\s|]+\|$/.test(line)) continue // 分隔行
66
- const cells = line.split('|').slice(1, -1) // 去掉首尾空元素
67
- if (cells.length < 2) continue
68
- if (!headerSkipped) { headerSkipped = true; continue } // 跳过表头
69
-
70
- const filePath = normalizeEntry(cells[1])
71
- if (isExcluded(filePath)) continue
72
- result.add(filePath)
73
- continue
74
- }
75
-
76
- // 分类列表项:`- path` / `- \`path\``
77
- const listItem = line.match(/^\s*-\s+(.+)/)
78
- if (listItem) {
79
- const filePath = normalizeEntry(listItem[1])
80
- if (isExcluded(filePath)) continue
81
- if (listMode === 'exclude') result.delete(filePath)
82
- else result.add(filePath)
83
- }
84
- }
85
-
86
- return result
87
- }
1
+ import { readFileSync, existsSync } from 'fs'
2
+
3
+ /**
4
+ * 路径项归一化:去反引号、去行内括号注释(`src/foo.js (新增)` / `src/foo.js(说明)`)、
5
+ * 统一为正斜杠、去首尾空白与尾部斜杠。
6
+ * design 清单 cell 与 task allowed_paths 共用此归一化(两处写法都可能带注释/反引号)。
7
+ * @param {string} raw
8
+ * @returns {string}
9
+ */
10
+ export function normalizePath(raw) {
11
+ if (!raw) return ''
12
+ return raw
13
+ .replace(/`/g, '')
14
+ .replace(/\s*([^)]*)\s*$/, '')
15
+ .replace(/\s*\([^)]*\)\s*$/, '')
16
+ .replace(/\\/g, '/')
17
+ .replace(/\/+$/, '')
18
+ .trim()
19
+ }
20
+
21
+ /**
22
+ * glob 正则:双星(globstar)跨零或多段目录(含零段,bash 语义)、
23
+ * 单星 单段([^/]*)、问号 单字符([^/]),其余转义。
24
+ * pattern 不含通配符时返回 false(调用方先用完全相等判断)。
25
+ * 不支持字符类(design/allowed_paths 实际不写,遇方括号按字面转义)。
26
+ * @param {string} str
27
+ * @param {string} pattern
28
+ * @returns {boolean}
29
+ */
30
+ export function globMatch(str, pattern) {
31
+ if (!/[*?]/.test(pattern)) return false
32
+ let re = '^'
33
+ for (let i = 0; i < pattern.length; i++) {
34
+ const ch = pattern[i]
35
+ if (ch === '*') {
36
+ if (pattern[i + 1] === '*') {
37
+ // ** 跨零或多段:吞掉紧随的 /,输出 (?:.*/)? 让 src/**/a.js 也能匹配 src/a.js
38
+ if (pattern[i + 2] === '/') { re += '(?:.*/)?'; i += 2 }
39
+ else { re += '.*'; i++ }
40
+ } else {
41
+ re += '[^/]*'
42
+ }
43
+ } else if (ch === '?') {
44
+ re += '[^/]'
45
+ } else {
46
+ re += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&')
47
+ }
48
+ }
49
+ re += '$'
50
+ try { return new RegExp(re).test(str) } catch { return false }
51
+ }
52
+
53
+ /**
54
+ * 双向容差匹配:design 清单文件 vs task allowed_paths(或脏文件 vs 清单)。
55
+ * 命中条件(任一):完全相等 / 目录前缀包含(双向)/ glob 通配(双向)。
56
+ * @param {string} a
57
+ * @param {string} b
58
+ * @returns {boolean}
59
+ */
60
+ export function pathMatches(a, b) {
61
+ a = normalizePath(a)
62
+ b = normalizePath(b)
63
+ if (!a || !b) return false
64
+ if (a === b) return true
65
+ if (a.startsWith(b + '/') || b.startsWith(a + '/')) return true
66
+ if (globMatch(a, b) || globMatch(b, a)) return true
67
+ return false
68
+ }
69
+
70
+ /**
71
+ * 「文件变更清单」章节标题同义词(与 src/stage-contract.js 的识别集对齐,
72
+ * 避免两个校验器对「有没有清单」给出矛盾结论)。
73
+ */
74
+ const FILE_LIST_SECTION_RE = /^#{2,3}\s*(文件变更清单|变更文件清单|文件清单|File Changes|Files to Change)/im
75
+
76
+ /**
77
+ * exclude 子标题词集:「不修改/暂缓/保留」类 —— 其下路径从清单移除,不强制 task 覆盖。
78
+ * 用排除集而非白名单:容忍「改动/变更/涉及」等近义 include 写法。
79
+ */
80
+ const EXCLUDE_SUBSECTION_RE = /不修改|不变|保留|无变更|未变更|不改动|暂不|暂缓|暂定|待定|后续|未改|无需|不涉及/
81
+
82
+ function isPlaceholder(p) {
83
+ return !p || p === '—' || p === '-' || /^(n\/?a|无|none|-+)$/i.test(p)
84
+ }
85
+
86
+ /** 表头列定位:cell 整体匹配路径类词 */
87
+ function isPathHeaderCell(c) {
88
+ return /^(文件路径|文件名|文件|路径|filepath|filename|file\s*path|path)$/i.test(c)
89
+ }
90
+
91
+ /**
92
+ * 从 design.md 解析文件变更清单。兼容两种真实写法:
93
+ * ① 表格:`| 操作 | 文件路径 | 说明 |`(brainstorm 模板默认)
94
+ * ② 分类列表:`### 新增文件` / `### 修改文件` / `### 不修改文件` 下的 `- path`
95
+ * 表头列顺序自适应(定位「文件/路径/file/path」列,列顺序写反时不会把操作名当路径);
96
+ * 忽略 `.sillyspec/` 与占位符(`—`/`-`/`N/A`/`无`);「不修改/暂缓」子段下的路径会被排除;
97
+ * CRLF 容错。
98
+ * @param {string} designMdPath - design.md 文件路径
99
+ * @returns {Set<string>} 文件路径集合(相对路径,如 "src/worktree.js")
100
+ */
101
+ export function parseFileChangeList(designMdPath) {
102
+ const result = new Set()
103
+ if (!designMdPath || !existsSync(designMdPath)) return result
104
+
105
+ const content = readFileSync(designMdPath, 'utf8').replace(/\r\n/g, '\n')
106
+
107
+ const sectionMatch = content.match(FILE_LIST_SECTION_RE)
108
+ if (!sectionMatch) return result
109
+
110
+ // 从标题后开始,截取到下一个 ## 标题或文件末尾
111
+ const afterSection = content.slice(sectionMatch.index + sectionMatch[0].length)
112
+ const nextSectionMatch = afterSection.match(/^##\s/m)
113
+ const relevantContent = nextSectionMatch
114
+ ? afterSection.slice(0, nextSectionMatch.index)
115
+ : afterSection
116
+
117
+ const lines = relevantContent.split('\n')
118
+ let headerSkipped = false
119
+ let pathColIdx = 1 // 默认第 2 列;解析表头后可重定位
120
+ let listMode = 'include' // include | exclude(分类列表子段)
121
+
122
+ for (const line of lines) {
123
+ // 分类列表子标题:### 新增文件 / ### 修改文件 / ### 不修改文件
124
+ const subHeader = line.match(/^###\s+(.+?)\s*$/)
125
+ if (subHeader) {
126
+ listMode = EXCLUDE_SUBSECTION_RE.test(subHeader[1]) ? 'exclude' : 'include'
127
+ continue
128
+ }
129
+
130
+ // 表格行
131
+ if (line.startsWith('|')) {
132
+ if (/^\|[-:\s|]+\|$/.test(line)) continue // 分隔行
133
+ const cells = line.split('|').slice(1, -1).map(c => c.trim())
134
+ if (cells.length < 2) continue
135
+
136
+ if (!headerSkipped) {
137
+ headerSkipped = true
138
+ // 表头列定位:找「文件/路径/file/path」列,找不到保持默认第 2 列
139
+ const idx = cells.findIndex(isPathHeaderCell)
140
+ if (idx >= 0) pathColIdx = idx
141
+ continue
142
+ }
143
+
144
+ const filePath = normalizePath(cells[pathColIdx] || '')
145
+ // 列定位兜底:取到纯操作词(表头未命中且列顺序异常)→ 跳过,避免把「修改」当路径
146
+ if (/^(新增|修改|删除|重命名|new|modify|update|delete|create|rename)$/i.test(filePath)) continue
147
+ if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
148
+ result.add(filePath)
149
+ continue
150
+ }
151
+
152
+ // 分类列表项:`- path` / `- \`path\``
153
+ const listItem = line.match(/^\s*-\s+(.+)/)
154
+ if (listItem) {
155
+ const filePath = normalizePath(listItem[1])
156
+ if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
157
+ if (listMode === 'exclude') result.delete(filePath)
158
+ else result.add(filePath)
159
+ }
160
+ }
161
+
162
+ return result
163
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { join } from 'path'
10
10
  import { existsSync, readFileSync } from 'fs'
11
- import { parseFileChangeList } from './change-list.js'
11
+ import { parseFileChangeList, pathMatches } from './change-list.js'
12
12
 
13
13
  function escapeRegex(s) {
14
14
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
@@ -60,13 +60,13 @@ function readProposalContext(specDir, changeName) {
60
60
 
61
61
  /**
62
62
  * 判断单个脏文件是否命中 design.md 声明的文件清单。
63
- * 支持精确匹配 + 目录前缀匹配(design 可能写目录、脏文件是目录下文件,或反之)。
63
+ * 复用 change-list.js pathMatches:完全相等 / 目录前缀(双向)/ glob 通配(双向),
64
+ * 与 plan-postcheck 的文件覆盖对账共用同一套匹配语义;并修复了 design 清单含 glob
65
+ * (如 `src/stages/scan/*.md`)时纯前缀匹配漏召回的问题。
64
66
  */
65
67
  function fileHitsList(dirtyFile, fileList) {
66
- if (fileList.has(dirtyFile)) return true
67
68
  for (const declared of fileList) {
68
- if (declared === dirtyFile) continue
69
- if (dirtyFile.startsWith(declared + '/') || declared.startsWith(dirtyFile + '/')) return true
69
+ if (pathMatches(dirtyFile, declared)) return true
70
70
  }
71
71
  return false
72
72
  }
@@ -361,30 +361,48 @@ function parseWavesFromPlan(planContent) {
361
361
  const lines = planContent.split('\n')
362
362
  let currentWave = null
363
363
  let currentTask = null
364
+ // light/none plan.md 用 `## Tasks`(无 `## Wave N`)包任务,需识别为隐式任务区,
365
+ // 让其中的 task checkbox 能被收进惰性创建的隐式 Wave(见下方 taskMatch 分支)。
366
+ let inImplicitTaskSection = false
364
367
 
365
368
  for (const line of lines) {
366
369
  const waveMatch = line.match(/^#+\s*Wave\s+(\d+)/i)
367
370
  if (waveMatch) {
368
371
  currentWave = { index: parseInt(waveMatch[1]), tasks: [] }
369
372
  currentTask = null
373
+ inImplicitTaskSection = false
370
374
  waves.push(currentWave)
371
375
  continue
372
376
  }
373
377
 
374
- // 遇到任何非 Wave 的标题行(## 自检、## 备注 等)时退出当前 Wave 段,
375
- // 避免「## 自检」段里的 - [x] checkbox 被误当 task 定义解析(导致
376
- // Contract 校验报 task id 重复/不连续)。详见 docs/sillyspec/plan-postcheck-self-check-checkbox-false-dup.md
377
- if (/^#+\s/.test(line)) {
378
+ // 任何非 Wave 的标题行:
379
+ // 1) 退出当前显式 Wave 段,避免「## 自检」段里的 - [x] checkbox 被误当 task 定义解析
380
+ // (导致 Contract 校验报 task id 重复/不连续,详见 docs/sillyspec/plan-postcheck-self-check-checkbox-false-dup.md
381
+ // 2) 识别 light/none 的 `## Tasks`/`## 任务` 任务区(无 Wave 标题),置位隐式任务区标志
382
+ const headingMatch = line.match(/^#+\s+(.+?)\s*$/)
383
+ if (headingMatch) {
378
384
  currentWave = null
379
385
  currentTask = null
386
+ const headingText = headingMatch[1].trim().toLowerCase()
387
+ inImplicitTaskSection = /^(tasks?|任务)$/.test(headingText)
380
388
  continue
381
389
  }
382
390
 
383
- if (!currentWave) continue
384
-
385
391
  const taskMatch = line.match(/^[-*]\s*\[[ x]\]\s*(.+)/)
386
392
  if (taskMatch) {
387
393
  const taskNoMatch = taskMatch[1].match(/\btask-(\d+)\b/i)
394
+ // full plan.md:task 必须在显式 Wave 段内(currentWave 非 null),正常收容。
395
+ // light/none plan.md(无 Wave 标题,任务在 `## Tasks` 下):在隐式任务区内,遇含
396
+ // task-XX 编号的 checkbox 时惰性创建隐式 Wave 收容,否则 validatePlanForExecute 会报
397
+ // "没有找到 checkbox task"。详见 docs/sillyspec/plan-light-needs-wave-heading.md
398
+ // 不收的情况:非任务区(## 自检/## 验收 等)的 checkbox,或任务区内无 task-XX 编号的 checkbox。
399
+ if (!currentWave) {
400
+ if (!inImplicitTaskSection || !taskNoMatch) continue
401
+ const nextIndex = waves.length === 0 ? 1 : (waves[waves.length - 1].index || waves.length) + 1
402
+ currentWave = { index: nextIndex, tasks: [], implicit: true }
403
+ currentTask = null
404
+ waves.push(currentWave)
405
+ }
388
406
  currentTask = {
389
407
  index: taskNoMatch ? parseInt(taskNoMatch[1], 10) : null,
390
408
  name: taskMatch[1].trim(),
@@ -549,12 +567,12 @@ ${taskSummary}
549
567
  4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流
550
568
 
551
569
  ### Wave 开始前
552
- 1. 读取 design.md 的「编码铁律」章节(如果存在),严格遵守
570
+ 1. 读取 design.md 的「非目标」与「兼容策略」章节(如存在),确保子代理不超范围、不破坏旧逻辑
553
571
  2. 读取 plan.md 了解全局任务划分和依赖关系
554
572
  3. 确认本 Wave 的输入/输出契约(前置 Wave 产出了什么,本 Wave 需要消费什么)
555
573
  4. 检查前置 Wave 的产出是否完整(文件是否存在、测试是否通过)
556
574
  5. **上下文分层加载**:
557
- - 🔥 热上下文:design.md 编码铁律 + 当前 Wave 任务(必须加载)
575
+ - 🔥 热上下文:design.md 非目标/兼容策略 + 当前 Wave 任务(必须加载)
558
576
  - 🌡️ 温上下文:CONVENTIONS.md + ARCHITECTURE.md(需要时加载)
559
577
  - ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
560
578
  ${contractInjection}
@@ -12,7 +12,7 @@
12
12
  import { existsSync, readFileSync, readdirSync } from 'fs'
13
13
  import { join as pJoin } from 'path'
14
14
  import jsYaml from 'js-yaml'
15
- import { parseFileChangeList } from '../change-list.js'
15
+ import { parseFileChangeList, pathMatches } from '../change-list.js'
16
16
 
17
17
  // ═══════════════════════════════════════════════════════════════
18
18
  // 解析工具(从 plan.js 迁移)
@@ -337,38 +337,8 @@ export function validateCrossTaskContracts(changeDir) {
337
337
  return { ok: errors.length === 0, errors, warnings }
338
338
  }
339
339
 
340
- // ── 路径匹配工具(与 change-list.js 的 normalizeEntry 同源语义)─────────
341
- // allowed_paths 真实写法常带行内注释(`src/worktree.js (新增)`),匹配前必须归一化。
342
- function normalizePath(raw) {
343
- if (!raw) return ''
344
- return raw
345
- .replace(/`/g, '')
346
- .replace(/\s*([^)]*)\s*$/, '')
347
- .replace(/\s*\([^)]*\)\s*$/, '')
348
- .replace(/\\/g, '/')
349
- .replace(/\/+$/, '')
350
- .trim()
351
- }
352
-
353
- function globMatch(str, pattern) {
354
- if (!pattern.includes('*')) return false
355
- const re = '^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*') + '$'
356
- try { return new RegExp(re).test(str) } catch { return false }
357
- }
358
-
359
- /**
360
- * 双向容差匹配:design 清单文件 vs task allowed_paths
361
- * 命中条件(任一):完全相等 / 目录前缀包含(双向)/ glob 通配(双向)
362
- */
363
- function pathMatches(designFile, allowedPath) {
364
- const a = normalizePath(designFile)
365
- const b = normalizePath(allowedPath)
366
- if (!a || !b) return false
367
- if (a === b) return true
368
- if (a.startsWith(b + '/') || b.startsWith(a + '/')) return true
369
- if (globMatch(a, b) || globMatch(b, a)) return true
370
- return false
371
- }
340
+ // 路径容差匹配(normalizePath / globMatch / pathMatches)复用自 change-list.js
341
+ // design 清单解析与 allowed_paths 对账共用同一套匹配语义,避免两处逻辑漂移。
372
342
 
373
343
  /**
374
344
  * design.md 文件变更清单 → tasks allowed_paths 覆盖对账
@@ -380,8 +350,10 @@ function pathMatches(designFile, allowedPath) {
380
350
  * 在 plan-postcheck(execute 前)确定性拦截:design 清单中每个源码文件必须被
381
351
  * 至少一个 task 的 allowed_paths 覆盖(前缀 / glob 容差匹配)。
382
352
  *
383
- * fail-open 边界(不阻断):design.md 不存在、无清单章节、解析为空、无 tasks/、
384
- * allowed_paths —— 保持与 none/light 级别及老变更向后兼容。
353
+ * fail-open 边界(不阻断):design.md 不存在、无 task 卡片(none 级别)、
354
+ * task 卡片均无 allowed_paths(由 validatePlanFeasibility 把关)。
355
+ * 阻断边界:有 task 卡片但 design 缺清单章节(light/full 已生成 task → design 必有清单)、
356
+ * 清单文件未被任何 task 的 allowed_paths 覆盖。
385
357
  *
386
358
  * @param {string} changeDir - 变更目录
387
359
  * @returns {{ ok: boolean, errors: string[], warnings: string[], designFiles: string[], uncovered: string[] }}
@@ -395,18 +367,26 @@ export function validateDesignFileCoverage(changeDir) {
395
367
  return { ok: true, errors, warnings, designFiles: [], uncovered: [] }
396
368
  }
397
369
 
398
- const designFiles = [...parseFileChangeList(designPath)]
399
- if (designFiles.length === 0) {
400
- warnings.push('design.md 未找到「文件变更清单」或清单为空,跳过文件覆盖对账')
370
+ const tasksDir = pJoin(changeDir, 'tasks')
371
+ const taskFiles = existsSync(tasksDir)
372
+ ? readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))
373
+ : []
374
+
375
+ // 无 task 卡片(plan_level=none 或老变更)→ 无对账对象,fail-open
376
+ if (taskFiles.length === 0) {
401
377
  return { ok: true, errors, warnings, designFiles: [], uncovered: [] }
402
378
  }
403
379
 
404
- const tasksDir = pJoin(changeDir, 'tasks')
405
- if (!existsSync(tasksDir)) {
406
- return { ok: true, errors, warnings, designFiles, uncovered: [] }
380
+ const designFiles = [...parseFileChangeList(designPath)]
381
+ if (designFiles.length === 0) {
382
+ // 走到 plan-postcheck 说明已生成 task 卡片(light/full),brainstorm 模板规定清单必填。
383
+ // 无清单 = design↔execute 偏差温床(覆盖对账无从对起),阻断,不让它静默放过。
384
+ errors.push(
385
+ 'design.md 缺少「文件变更清单」章节(或清单解析为空),无法做文件覆盖对账。' +
386
+ '该章节在 brainstorm 模板中为必填;请在 design.md 补上完整的文件变更清单(列出本次新增/修改/删除的源码文件)后重试。'
387
+ )
388
+ return { ok: false, errors, warnings, designFiles: [], uncovered: [] }
407
389
  }
408
-
409
- const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))
410
390
  const allAllowed = []
411
391
  for (const file of taskFiles) {
412
392
  const content = readFileSync(pJoin(tasksDir, file), 'utf8')
@@ -114,10 +114,10 @@ export const definition = {
114
114
  ### 自动探针(必须先执行)
115
115
  在检查前,依次运行以下三个探针,将结果作为验证输入:
116
116
 
117
- **探针 1:未实现标记扫描**
118
- 在项目源码目录中搜索未实现标记:
117
+ **探针 1:未实现标记扫描(仅变更文件)**
118
+ 只扫描本次变更涉及的文件,不要全项目扫描——历史 TODO 与本次变更无关,徒增噪音与 token。变更文件 = design.md「文件变更清单」列出的源码文件(你已在「加载规范」步骤读取 design.md,glob 路径需展开为具体文件):
119
119
  \`\`\`bash
120
- grep -rn "尚未实现\|TODO\|FIXME\|HACK\|XXX" <源码目录>/ --include="*.java" --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" --include="*.py"
120
+ grep -n "尚未实现\|TODO\|FIXME\|HACK\|XXX" <design 清单中的源码文件>
121
121
  \`\`\`
122
122
  记录每个匹配的文件、行号和内容。
123
123