speccore 5.71.0 → 5.74.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.
Files changed (120) hide show
  1. package/.agents/skills/spec-ask/SKILL.md +10 -0
  2. package/.agents/skills/speccore-router/SKILL.md +10 -0
  3. package/README.md +30 -8
  4. package/dist/cli.js +18 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/about.d.ts.map +1 -1
  7. package/dist/commands/about.js +8 -1
  8. package/dist/commands/about.js.map +1 -1
  9. package/dist/commands/analyze.d.ts +3 -0
  10. package/dist/commands/analyze.d.ts.map +1 -1
  11. package/dist/commands/analyze.js +120 -21
  12. package/dist/commands/analyze.js.map +1 -1
  13. package/dist/commands/ask.d.ts +1 -1
  14. package/dist/commands/ask.d.ts.map +1 -1
  15. package/dist/commands/ask.js +12 -5
  16. package/dist/commands/ask.js.map +1 -1
  17. package/dist/commands/change.d.ts +5 -0
  18. package/dist/commands/change.d.ts.map +1 -1
  19. package/dist/commands/change.js +588 -22
  20. package/dist/commands/change.js.map +1 -1
  21. package/dist/commands/code-index.d.ts +18 -0
  22. package/dist/commands/code-index.d.ts.map +1 -0
  23. package/dist/commands/code-index.js +126 -0
  24. package/dist/commands/code-index.js.map +1 -0
  25. package/dist/commands/dashboard.d.ts.map +1 -1
  26. package/dist/commands/dashboard.js +10 -40
  27. package/dist/commands/dashboard.js.map +1 -1
  28. package/dist/commands/dev.d.ts +1 -0
  29. package/dist/commands/dev.d.ts.map +1 -1
  30. package/dist/commands/dev.js +133 -17
  31. package/dist/commands/dev.js.map +1 -1
  32. package/dist/commands/doc2spec.js +4 -4
  33. package/dist/commands/doc2spec.js.map +1 -1
  34. package/dist/commands/execute.d.ts +2 -0
  35. package/dist/commands/execute.d.ts.map +1 -1
  36. package/dist/commands/execute.js +243 -51
  37. package/dist/commands/execute.js.map +1 -1
  38. package/dist/commands/help.js +17 -7
  39. package/dist/commands/help.js.map +1 -1
  40. package/dist/commands/init.d.ts.map +1 -1
  41. package/dist/commands/init.js +8 -1
  42. package/dist/commands/init.js.map +1 -1
  43. package/dist/commands/iteration/create.js +38 -6
  44. package/dist/commands/iteration/create.js.map +1 -1
  45. package/dist/commands/iteration/split.js +70 -12
  46. package/dist/commands/iteration/split.js.map +1 -1
  47. package/dist/commands/lifecycle.d.ts.map +1 -1
  48. package/dist/commands/lifecycle.js +47 -23
  49. package/dist/commands/lifecycle.js.map +1 -1
  50. package/dist/commands/plan.d.ts +20 -0
  51. package/dist/commands/plan.d.ts.map +1 -1
  52. package/dist/commands/plan.js +54 -39
  53. package/dist/commands/plan.js.map +1 -1
  54. package/dist/commands/retro.d.ts +9 -0
  55. package/dist/commands/retro.d.ts.map +1 -1
  56. package/dist/commands/retro.js +61 -15
  57. package/dist/commands/retro.js.map +1 -1
  58. package/dist/commands/status-panel.js +33 -6
  59. package/dist/commands/status-panel.js.map +1 -1
  60. package/dist/commands/validate.d.ts.map +1 -1
  61. package/dist/commands/validate.js +25 -1
  62. package/dist/commands/validate.js.map +1 -1
  63. package/dist/commands/verify.d.ts +22 -0
  64. package/dist/commands/verify.d.ts.map +1 -0
  65. package/dist/commands/verify.js +158 -0
  66. package/dist/commands/verify.js.map +1 -0
  67. package/dist/commands/welcome.d.ts.map +1 -1
  68. package/dist/commands/welcome.js +5 -9
  69. package/dist/commands/welcome.js.map +1 -1
  70. package/dist/core/ai-context-generator.js +2 -2
  71. package/dist/core/ai-context-generator.js.map +1 -1
  72. package/dist/core/analyze-engine.d.ts +19 -0
  73. package/dist/core/analyze-engine.d.ts.map +1 -1
  74. package/dist/core/analyze-engine.js +225 -13
  75. package/dist/core/analyze-engine.js.map +1 -1
  76. package/dist/core/ask-engine.d.ts.map +1 -1
  77. package/dist/core/ask-engine.js +100 -10
  78. package/dist/core/ask-engine.js.map +1 -1
  79. package/dist/core/capabilities.js +3 -3
  80. package/dist/core/code-index-markdown.d.ts +7 -0
  81. package/dist/core/code-index-markdown.d.ts.map +1 -0
  82. package/dist/core/code-index-markdown.js +160 -0
  83. package/dist/core/code-index-markdown.js.map +1 -0
  84. package/dist/core/code-scanner.d.ts +58 -3
  85. package/dist/core/code-scanner.d.ts.map +1 -1
  86. package/dist/core/code-scanner.js +406 -24
  87. package/dist/core/code-scanner.js.map +1 -1
  88. package/dist/core/git-integration.d.ts +10 -0
  89. package/dist/core/git-integration.d.ts.map +1 -1
  90. package/dist/core/git-integration.js +99 -2
  91. package/dist/core/git-integration.js.map +1 -1
  92. package/dist/core/inbox.d.ts +120 -0
  93. package/dist/core/inbox.d.ts.map +1 -0
  94. package/dist/core/inbox.js +377 -0
  95. package/dist/core/inbox.js.map +1 -0
  96. package/dist/core/intent-cache.d.ts.map +1 -1
  97. package/dist/core/intent-cache.js +8 -2
  98. package/dist/core/intent-cache.js.map +1 -1
  99. package/dist/core/intent-recognition.d.ts.map +1 -1
  100. package/dist/core/intent-recognition.js +4 -3
  101. package/dist/core/intent-recognition.js.map +1 -1
  102. package/dist/core/next-steps.js +1 -1
  103. package/dist/core/next-steps.js.map +1 -1
  104. package/dist/core/plan-html.js +3 -3
  105. package/dist/core/prompt-builder.d.ts +7 -0
  106. package/dist/core/prompt-builder.d.ts.map +1 -1
  107. package/dist/core/prompt-builder.js +52 -9
  108. package/dist/core/prompt-builder.js.map +1 -1
  109. package/dist/core/resolver.d.ts +52 -0
  110. package/dist/core/resolver.d.ts.map +1 -0
  111. package/dist/core/resolver.js +257 -0
  112. package/dist/core/resolver.js.map +1 -0
  113. package/dist/core/state.d.ts.map +1 -1
  114. package/dist/core/state.js +46 -14
  115. package/dist/core/state.js.map +1 -1
  116. package/dist/core/verify-engine.d.ts +64 -0
  117. package/dist/core/verify-engine.d.ts.map +1 -0
  118. package/dist/core/verify-engine.js +560 -0
  119. package/dist/core/verify-engine.js.map +1 -0
  120. package/package.json +1 -1
@@ -44,6 +44,331 @@ const fs_extra_1 = require("fs-extra");
44
44
  const path_1 = require("path");
45
45
  const transaction_1 = require("../core/transaction");
46
46
  const state_1 = require("../core/state");
47
+ const resolver_1 = require("../core/resolver");
48
+ const global_counters_1 = require("../core/global-counters");
49
+ const inbox_1 = require("../core/inbox");
50
+ /**
51
+ * 解析任务目录基础路径:优先 030-tasks/,兼容旧布局
52
+ */
53
+ async function resolveTaskBase(iterDir) {
54
+ const tasksDir = (0, path_1.join)(iterDir, '030-tasks');
55
+ return (await (0, fs_extra_1.pathExists)(tasksDir)) ? tasksDir : iterDir;
56
+ }
57
+ /**
58
+ * 从 CHANGELOG.md 提取最新版本号并递增
59
+ */
60
+ async function nextVersion(changelogPath) {
61
+ if (await (0, fs_extra_1.pathExists)(changelogPath)) {
62
+ const content = await (0, fs_extra_1.readFile)(changelogPath, 'utf-8');
63
+ const versions = content.match(/v(\d+\.\d+)/g);
64
+ if (versions && versions.length > 0) {
65
+ const last = versions[versions.length - 1].replace('v', '');
66
+ const [major, minor] = last.split('.').map(Number);
67
+ return `v${major}.${minor + 1}`;
68
+ }
69
+ }
70
+ return 'v1.1';
71
+ }
72
+ /**
73
+ * 检测意图:变更已有需求 vs 新增需求
74
+ */
75
+ function detectIntent(desc) {
76
+ const lower = desc.replace(/\s+/g, '');
77
+ if (/^(新增?|加|添|创建|增加|实现|做[一个]*)/.test(lower))
78
+ return 'new';
79
+ return 'change';
80
+ }
81
+ /**
82
+ * 全量影响分析:读取每个任务的 REQ/TECH/TASK/status,分类为直接/间接/无影响
83
+ */
84
+ async function analyzeImpact(desc, iterDir, taskBase) {
85
+ const keywords = extractKeywords(desc);
86
+ const { readdir: rd } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
87
+ // 读取依赖图
88
+ const graphPath = (0, path_1.join)(iterDir, '000-overview', 'PROJECT_GRAPH.md');
89
+ const graphContent = await (0, fs_extra_1.pathExists)(graphPath) ? await (0, fs_extra_1.readFile)(graphPath, 'utf-8') : '';
90
+ // 收集所有任务详情
91
+ let entries = [];
92
+ try {
93
+ entries = await rd(taskBase, { withFileTypes: true });
94
+ }
95
+ catch {
96
+ return { directTasks: [], indirectTasks: [], unaffectedTasks: [] };
97
+ }
98
+ const allImpacts = [];
99
+ for (const entry of entries) {
100
+ if (!entry.isDirectory() || !entry.name.startsWith('Task-'))
101
+ continue;
102
+ const taskId = entry.name;
103
+ const taskDir = (0, path_1.join)(taskBase, taskId);
104
+ // 读取任务状态
105
+ let status = 'unknown';
106
+ const statusPath1 = (0, path_1.join)(taskDir, '.task-status');
107
+ const statusPath2 = (0, path_1.join)(taskDir, '.meta', 'status');
108
+ if (await (0, fs_extra_1.pathExists)(statusPath1))
109
+ status = (await (0, fs_extra_1.readFile)(statusPath1, 'utf-8')).trim();
110
+ else if (await (0, fs_extra_1.pathExists)(statusPath2))
111
+ status = (await (0, fs_extra_1.readFile)(statusPath2, 'utf-8')).trim();
112
+ // 读取任务名称
113
+ let name = taskId;
114
+ const reqPath = (0, path_1.join)(taskDir, '00-specs', 'REQ.md');
115
+ const legacyReqPath = (0, path_1.join)(taskDir, 'REQ.md');
116
+ const actualReqPath = (await (0, fs_extra_1.pathExists)(reqPath)) ? reqPath : (await (0, fs_extra_1.pathExists)(legacyReqPath)) ? legacyReqPath : null;
117
+ if (actualReqPath) {
118
+ const reqContent = await (0, fs_extra_1.readFile)(actualReqPath, 'utf-8');
119
+ const nameMatch = reqContent.match(/^#\s+(.+)$/m);
120
+ if (nameMatch)
121
+ name = nameMatch[1];
122
+ }
123
+ // 关键词匹配评分
124
+ let score = 0;
125
+ let matchedFiles = [];
126
+ const filesToCheck = [
127
+ { path: actualReqPath || '', label: 'REQ.md' },
128
+ { path: (0, path_1.join)(taskDir, '00-specs', 'TECH.md'), label: 'TECH.md' },
129
+ { path: (0, path_1.join)(taskDir, '00-specs', 'TASK.md'), label: 'TASK.md' },
130
+ ];
131
+ for (const fc of filesToCheck) {
132
+ if (!fc.path || !await (0, fs_extra_1.pathExists)(fc.path))
133
+ continue;
134
+ const content = await (0, fs_extra_1.readFile)(fc.path, 'utf-8');
135
+ for (const kw of keywords) {
136
+ if (content.includes(kw)) {
137
+ score++;
138
+ if (!matchedFiles.includes(fc.label))
139
+ matchedFiles.push(fc.label);
140
+ }
141
+ }
142
+ }
143
+ if (score > 0) {
144
+ allImpacts.push({
145
+ id: taskId,
146
+ name,
147
+ status,
148
+ level: 'direct',
149
+ reason: `关键词命中 ${score} 处 [${matchedFiles.join(', ')}]`,
150
+ affectedFiles: matchedFiles,
151
+ needReExecute: true,
152
+ needRegression: false,
153
+ });
154
+ }
155
+ else {
156
+ allImpacts.push({
157
+ id: taskId,
158
+ name,
159
+ status,
160
+ level: 'none',
161
+ reason: '',
162
+ affectedFiles: [],
163
+ needReExecute: false,
164
+ needRegression: false,
165
+ });
166
+ }
167
+ }
168
+ // 依赖图分析:直接受影响任务的上下游 → 间接影响
169
+ const directIds = new Set(allImpacts.filter(t => t.level === 'direct').map(t => t.id));
170
+ const indirectIds = new Set();
171
+ for (const directId of directIds) {
172
+ const deps = findDependentTasks(graphContent, directId);
173
+ for (const dep of deps) {
174
+ if (!directIds.has(dep)) {
175
+ indirectIds.add(dep);
176
+ }
177
+ }
178
+ // 反向查找:谁依赖了直接受影响的任务
179
+ const reverseDeps = findReverseDependencies(graphContent, directId);
180
+ for (const rd of reverseDeps) {
181
+ if (!directIds.has(rd)) {
182
+ indirectIds.add(rd);
183
+ }
184
+ }
185
+ }
186
+ // 分类结果
187
+ const directTasks = [];
188
+ const indirectTasks = [];
189
+ const unaffectedTasks = [];
190
+ for (const t of allImpacts) {
191
+ if (directIds.has(t.id)) {
192
+ directTasks.push(t);
193
+ }
194
+ else if (indirectIds.has(t.id)) {
195
+ // 补充间接影响原因
196
+ const depReasons = [];
197
+ for (const directId of directIds) {
198
+ const deps = findDependentTasks(graphContent, directId);
199
+ const rdeps = findReverseDependencies(graphContent, directId);
200
+ if (deps.includes(t.id))
201
+ depReasons.push(`依赖 ${directId}`);
202
+ if (rdeps.includes(t.id))
203
+ depReasons.push(`${directId} 依赖本任务`);
204
+ }
205
+ indirectTasks.push({
206
+ ...t,
207
+ level: 'indirect',
208
+ reason: depReasons.join('; ') || '间接关联',
209
+ needReExecute: false,
210
+ needRegression: t.status === 'done',
211
+ });
212
+ }
213
+ else {
214
+ unaffectedTasks.push(t);
215
+ }
216
+ }
217
+ return { directTasks, indirectTasks, unaffectedTasks };
218
+ }
219
+ /**
220
+ * 反向查找依赖:找出哪些任务依赖了指定任务
221
+ */
222
+ function findReverseDependencies(graphContent, taskId) {
223
+ const deps = [];
224
+ // Mermaid 格式: A[Task-001] --> B[Task-002] 表示 Task-001 → Task-002
225
+ const mermaidPattern = new RegExp(`\\[${taskId}\\]\\s*-->\\s*\\[(Task-\\d+)\\]`);
226
+ const reverseMermaidPattern = new RegExp(`\\[(Task-\\d+)\\]\\s*-->\\s*\\[${taskId}\\]`);
227
+ for (const line of graphContent.split('\n')) {
228
+ // 正向:taskId 指向别人
229
+ const m = line.match(mermaidPattern);
230
+ if (m && m[1] !== taskId && !deps.includes(m[1]))
231
+ deps.push(m[1]);
232
+ // 反向:别人指向 taskId
233
+ const rm = line.match(reverseMermaidPattern);
234
+ if (rm && rm[1] !== taskId && !deps.includes(rm[1]))
235
+ deps.push(rm[1]);
236
+ }
237
+ return deps;
238
+ }
239
+ /**
240
+ * 构建任务详情(用于澄清 Prompt 上下文)
241
+ */
242
+ async function buildTaskDetails(taskBase) {
243
+ const { readdir: rd } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
244
+ let entries = [];
245
+ try {
246
+ entries = await rd(taskBase, { withFileTypes: true });
247
+ }
248
+ catch {
249
+ return [];
250
+ }
251
+ const details = [];
252
+ for (const entry of entries) {
253
+ if (!entry.isDirectory() || !entry.name.startsWith('Task-'))
254
+ continue;
255
+ const taskId = entry.name;
256
+ const taskDir = (0, path_1.join)(taskBase, taskId);
257
+ // 状态
258
+ let status = 'unknown';
259
+ const statusPath1 = (0, path_1.join)(taskDir, '.task-status');
260
+ const statusPath2 = (0, path_1.join)(taskDir, '.meta', 'status');
261
+ if (await (0, fs_extra_1.pathExists)(statusPath1))
262
+ status = (await (0, fs_extra_1.readFile)(statusPath1, 'utf-8')).trim();
263
+ else if (await (0, fs_extra_1.pathExists)(statusPath2))
264
+ status = (await (0, fs_extra_1.readFile)(statusPath2, 'utf-8')).trim();
265
+ // REQ 摘要(取前 300 字符)
266
+ let reqSummary = '';
267
+ const reqPath = (0, path_1.join)(taskDir, '00-specs', 'REQ.md');
268
+ const legacyReqPath = (0, path_1.join)(taskDir, 'REQ.md');
269
+ const actualReqPath = (await (0, fs_extra_1.pathExists)(reqPath)) ? reqPath : (await (0, fs_extra_1.pathExists)(legacyReqPath)) ? legacyReqPath : null;
270
+ if (actualReqPath) {
271
+ const content = await (0, fs_extra_1.readFile)(actualReqPath, 'utf-8');
272
+ reqSummary = content.slice(0, 300).replace(/\n+/g, ' ').trim();
273
+ }
274
+ // TECH 摘要
275
+ let techSummary = '';
276
+ const techPath = (0, path_1.join)(taskDir, '00-specs', 'TECH.md');
277
+ if (await (0, fs_extra_1.pathExists)(techPath)) {
278
+ const content = await (0, fs_extra_1.readFile)(techPath, 'utf-8');
279
+ techSummary = content.slice(0, 200).replace(/\n+/g, ' ').trim();
280
+ }
281
+ // 名称
282
+ let name = taskId;
283
+ if (reqSummary) {
284
+ const nameMatch = reqSummary.match(/^#\s+(.+)/);
285
+ if (nameMatch)
286
+ name = nameMatch[1];
287
+ }
288
+ details.push({ id: taskId, name, reqSummary, techSummary, status, dependencies: [] });
289
+ }
290
+ // 从依赖图补充 dependencies
291
+ return details;
292
+ }
293
+ /**
294
+ * 提取关键词(2字以上的中文/英文词)
295
+ */
296
+ function extractKeywords(desc) {
297
+ const normalized = normalizeDescription(desc);
298
+ const chinese = normalized.match(/[\u4e00-\u9fa5]{2,}/g) || [];
299
+ const english = normalized.match(/[a-zA-Z]{2,}/g) || [];
300
+ return [...new Set([...chinese, ...english])];
301
+ }
302
+ /**
303
+ * 新增需求一站式处理:创建任务 → 追加需求 → 更新依赖图 → 引导分析
304
+ * 澄清结果(需求分析)持久化到 REQ.md
305
+ */
306
+ async function handleNewRequirement(desc, iteration, clarifyResult) {
307
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
308
+ const taskBase = await resolveTaskBase(iterDir);
309
+ await (0, fs_extra_1.ensureDir)(taskBase);
310
+ const { id: taskId } = await (0, global_counters_1.nextTaskId)();
311
+ const taskName = desc.replace(/^(新增?|加|创建|实现|做)/, '').replace(/[::]/g, '').trim() || taskId;
312
+ const taskDir = (0, path_1.join)(taskBase, taskId);
313
+ const specsDir = (0, path_1.join)(taskDir, '00-specs');
314
+ await (0, fs_extra_1.ensureDir)(specsDir);
315
+ const now = new Date().toISOString().split('T')[0];
316
+ const tx = new transaction_1.FileTransaction();
317
+ // 构建结构化 REQ.md(澄清 = 需求分析,结果持久化)
318
+ const structuredDesc = clarifyResult?.structuredDesc || desc;
319
+ const keyPoints = clarifyResult?.keyPoints || [];
320
+ const acceptanceCriteria = clarifyResult?.acceptanceCriteria || [];
321
+ let reqContent = `# ${taskName}\n\n`;
322
+ reqContent += `## 需求描述\n\n${structuredDesc}\n\n`;
323
+ reqContent += `## 原始输入\n\n${desc}\n\n`;
324
+ if (keyPoints.length > 0) {
325
+ reqContent += `## 功能要点\n\n`;
326
+ for (const p of keyPoints) {
327
+ reqContent += `- ${p}\n`;
328
+ }
329
+ reqContent += '\n';
330
+ }
331
+ if (acceptanceCriteria.length > 0) {
332
+ reqContent += `## 验收标准\n\n`;
333
+ for (const c of acceptanceCriteria) {
334
+ reqContent += `- [ ] ${c}\n`;
335
+ }
336
+ reqContent += '\n';
337
+ }
338
+ else {
339
+ reqContent += `## 验收标准\n\n- [ ] 功能正常\n`;
340
+ }
341
+ reqContent += `## 分析记录\n\n- 分析时间: ${now}\n- 分析方式: ${clarifyResult ? 'AI 澄清' : '本地分析'}\n`;
342
+ tx.write((0, path_1.join)(specsDir, 'REQ.md'), reqContent);
343
+ // 创建 CHANGELOG.md
344
+ tx.write((0, path_1.join)(specsDir, 'CHANGELOG.md'), `# 变更记录\n\n| 时间 | 版本 | 变更内容 | 变更人 |\n| :--- | :--- | :--- | :--- |\n| ${now} | v1.0 | 初始创建 | SpecCore |\n`);
345
+ // 创建 TASK.md
346
+ tx.write((0, path_1.join)(specsDir, 'TASK.md'), `# ${taskName}\n\n- 状态: 待开发\n- 优先级: medium\n- 类型: feature\n\n## 变更履历\n\n| 时间 | 变更内容 | 变更人 |\n| :--- | :--- | :--- |\n| ${now} | 创建任务 | SpecCore |\n`);
347
+ await tx.commit();
348
+ // 追加到 REQUIREMENT.md
349
+ const reqPath = (0, path_1.join)(iterDir, '020-specs', 'REQUIREMENT.md');
350
+ if (await (0, fs_extra_1.pathExists)(reqPath)) {
351
+ let content = await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
352
+ content += `\n\n## ${taskName}\n\n${desc}\n`;
353
+ await (0, fs_extra_1.writeFile)(reqPath, content);
354
+ }
355
+ // 更新 PROJECT_GRAPH.md
356
+ const graphPath = (0, path_1.join)(iterDir, '000-overview', 'PROJECT_GRAPH.md');
357
+ if (await (0, fs_extra_1.pathExists)(graphPath)) {
358
+ let content = await (0, fs_extra_1.readFile)(graphPath, 'utf-8');
359
+ // 直接使用已生成的 taskId,避免重复计数 bug
360
+ content += `| ${taskId} | ${taskName} | feature | pending |\n`;
361
+ await (0, fs_extra_1.writeFile)(graphPath, content);
362
+ }
363
+ logger_1.logger.info('');
364
+ logger_1.logger.success(`✅ 新任务已创建: ${taskId}`);
365
+ logger_1.logger.info(` 📄 ${taskId}/00-specs/REQ.md`);
366
+ logger_1.logger.info(` 📄 ${taskId}/00-specs/TASK.md`);
367
+ logger_1.logger.info('');
368
+ logger_1.logger.info('💡 下一步:');
369
+ logger_1.logger.info(` speccore analyze --task=${taskId} # 分析技术方案`);
370
+ logger_1.logger.info(` speccore execute --task=${taskId} --force # 执行任务`);
371
+ }
47
372
  const readline_1 = require("readline");
48
373
  function promptUser(question) {
49
374
  const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
@@ -57,7 +382,169 @@ async function changeCommand(options) {
57
382
  options.desc = options.input;
58
383
  }
59
384
  if (!options.task && !options.global) {
60
- logger_1.logger.error('请指定 Task 或使用 --global。用法: speccore change "变更描述" --task=<Task>');
385
+ // ── 智能匹配 / 新增需求(含 inbox + 附件 + 澄清)──
386
+ if (!options.desc && !options.file && options.noInbox) {
387
+ logger_1.logger.error('请提供变更描述或附件。用法: speccore change "描述" --file=xxx.md');
388
+ return;
389
+ }
390
+ const iteration = await (0, context_1.getDefaultIteration)(options.iteration);
391
+ if (!iteration) {
392
+ logger_1.logger.error('未找到活跃迭代。请先运行: speccore iteration create --name <名称>');
393
+ return;
394
+ }
395
+ // ── 1. 加载附件 ──
396
+ const allFiles = [];
397
+ // 1a. 扫描 inbox(默认启用)
398
+ if (!options.noInbox) {
399
+ await (0, inbox_1.ensureInboxDir)();
400
+ const inboxResult = await (0, inbox_1.scanInbox)({ reprocess: options.reprocess });
401
+ (0, inbox_1.logInboxScan)(inboxResult);
402
+ const actionable = [...inboxResult.newFiles, ...inboxResult.modifiedFiles];
403
+ allFiles.push(...actionable);
404
+ }
405
+ // 1b. 加载 --file 指定的文件
406
+ if (options.file) {
407
+ const { readFile: rf, stat: st } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
408
+ const filePaths = options.file.split(',').map(f => f.trim());
409
+ for (const fp of filePaths) {
410
+ const absPath = (0, path_1.join)(process.cwd(), fp);
411
+ if (!await (0, fs_extra_1.pathExists)(absPath)) {
412
+ logger_1.logger.warn(`⚠️ 文件不存在: ${fp}`);
413
+ continue;
414
+ }
415
+ const fileStat = await st(absPath);
416
+ const name = fp.split('/').pop() || fp;
417
+ const ext = name.split('.').pop()?.toLowerCase() || '';
418
+ let type = 'other';
419
+ if (['md', 'txt', 'markdown', 'json', 'yaml', 'yml', 'csv'].includes(ext))
420
+ type = 'text';
421
+ else if (['xlsx', 'xls'].includes(ext))
422
+ type = 'excel';
423
+ else if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext))
424
+ type = 'image';
425
+ let content = '';
426
+ if (type === 'text') {
427
+ content = await rf(absPath, 'utf-8');
428
+ }
429
+ else if (type === 'excel') {
430
+ try {
431
+ const XLSX = require('xlsx');
432
+ const wb = XLSX.readFile(absPath);
433
+ const sheets = [];
434
+ for (const sn of wb.SheetNames) {
435
+ sheets.push(`## Sheet: ${sn}\n${XLSX.utils.sheet_to_csv(wb.Sheets[sn])}`);
436
+ }
437
+ content = sheets.join('\n\n');
438
+ }
439
+ catch {
440
+ content = `[Excel 解析失败]`;
441
+ }
442
+ }
443
+ else if (type === 'image') {
444
+ content = `[图片文件: ${absPath}]`;
445
+ }
446
+ else {
447
+ try {
448
+ content = await rf(absPath, 'utf-8');
449
+ }
450
+ catch {
451
+ content = `[无法读取]`;
452
+ }
453
+ }
454
+ allFiles.push({ name, path: absPath, size: fileStat.size, mtime: fileStat.mtime.toISOString(), type, content });
455
+ logger_1.logger.info(` 📎 ${name} (${fileStat.size > 1024 ? (fileStat.size / 1024).toFixed(1) + 'KB' : fileStat.size + 'B'})`);
456
+ }
457
+ }
458
+ // ── 2. 构建澄清上下文 ──
459
+ const desc = options.desc ? normalizeDescription(options.desc) : '';
460
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
461
+ const taskBase = await resolveTaskBase(iterDir);
462
+ const allTasks = await (0, state_1.scanTasks)(iteration);
463
+ const taskDetails = await buildTaskDetails(taskBase);
464
+ // ── 3. Prompt 模式:输出澄清 Prompt 到 stdout ──
465
+ if (options.prompt) {
466
+ const promptText = (0, inbox_1.buildClarifyPrompt)(desc || '(从附件分析需求)', allFiles, taskDetails);
467
+ logger_1.logger.info('[SPECCORE_PROMPT]');
468
+ process.stdout.write(promptText);
469
+ return;
470
+ }
471
+ // ── 4. Response 模式:解析 AI 澄清结果 ──
472
+ let clarifiedIntent;
473
+ let clarifiedDesc = desc;
474
+ let clarifiedTasks = [];
475
+ if (options.response) {
476
+ const parsed = (0, inbox_1.parseClarifyResponse)(options.response);
477
+ if (parsed) {
478
+ clarifiedIntent = parsed.intent;
479
+ clarifiedDesc = parsed.structuredDesc || desc;
480
+ // 从 impactReport 中提取直接影响的任务 ID
481
+ clarifiedTasks = parsed.impactReport?.directTasks?.map(t => t.id) || [];
482
+ (0, inbox_1.logClarifyResult)(parsed);
483
+ }
484
+ else {
485
+ logger_1.logger.warn('⚠️ AI 澄清结果解析失败,使用本地分析');
486
+ }
487
+ }
488
+ // ── 5. 本地意图检测(无 AI 澄清时) ──
489
+ const intent = clarifiedIntent || detectIntent(desc || allFiles.map(f => f.content).join(' '));
490
+ // ── 6. 新增需求 ──
491
+ if (intent === 'new') {
492
+ logger_1.logger.info('🆕 检测到新增需求意图');
493
+ const newDesc = clarifiedDesc || allFiles.map(f => f.name + ': ' + f.content.slice(0, 200)).join('\n');
494
+ // 传递澄清结果给 handleNewRequirement,持久化到 REQ.md
495
+ const parsed = options.response ? (0, inbox_1.parseClarifyResponse)(options.response) : null;
496
+ const clarifyOutput = parsed ? { structuredDesc: parsed.structuredDesc, keyPoints: parsed.keyPoints, acceptanceCriteria: parsed.acceptanceCriteria } : undefined;
497
+ await handleNewRequirement(newDesc, iteration, clarifyOutput);
498
+ // 标记 inbox 文件已处理
499
+ if (allFiles.length > 0) {
500
+ await (0, inbox_1.markProcessed)(allFiles, 'new', []);
501
+ }
502
+ return;
503
+ }
504
+ // ── 7. 变更:全量影响分析 ──
505
+ let impactReport;
506
+ if (clarifiedTasks.length > 0) {
507
+ // 使用 AI 澄清结果中的匹配任务构造影响报告
508
+ const directTasks = clarifiedTasks.map(tid => {
509
+ const task = allTasks.find(t => t.id === tid);
510
+ return { id: tid, name: task?.name || tid, status: 'unknown', level: 'direct', reason: 'AI 澄清匹配', affectedFiles: [], needReExecute: true, needRegression: false };
511
+ }).filter(m => allTasks.some(t => t.id === m.id));
512
+ impactReport = { directTasks, indirectTasks: [], unaffectedTasks: [] };
513
+ }
514
+ else {
515
+ // 本地全量影响分析
516
+ const matchDesc = desc || allFiles.map(f => f.content).join(' ');
517
+ impactReport = await analyzeImpact(matchDesc, iterDir, taskBase);
518
+ }
519
+ const hasImpact = impactReport.directTasks.length > 0 || impactReport.indirectTasks.length > 0;
520
+ if (!hasImpact) {
521
+ logger_1.logger.warn('未匹配到受影响任务。请指定 --task 或检查变更描述/附件。');
522
+ logger_1.logger.info('💡 如果是新增需求,请确保描述以"新增/加/创建"开头');
523
+ return;
524
+ }
525
+ // 展示影响分析报告
526
+ (0, inbox_1.logImpactReport)(impactReport);
527
+ logger_1.logger.info('');
528
+ // 对所有直接影响任务应用变更
529
+ const changeDesc = clarifiedDesc || desc;
530
+ const affectedIds = [];
531
+ for (const m of impactReport.directTasks) {
532
+ const taskOpts = { ...options, task: m.id, desc: changeDesc };
533
+ await applyTaskChange(taskOpts, iteration);
534
+ affectedIds.push(m.id);
535
+ }
536
+ // 标记 inbox 文件已处理
537
+ if (allFiles.length > 0) {
538
+ await (0, inbox_1.markProcessed)(allFiles, 'change', affectedIds);
539
+ }
540
+ // ── 8. 持久化澄清结果:迭代级 CHANGE_SUMMARY.md ──
541
+ await writeChangeSummary(iterDir, changeDesc, impactReport, affectedIds);
542
+ logger_1.logger.info('');
543
+ logger_1.logger.success(`✅ 变更已应用到 ${affectedIds.length} 个任务`);
544
+ logger_1.logger.info(` 📄 变更摘要: 020-specs/CHANGE_SUMMARY.md`);
545
+ logger_1.logger.info('');
546
+ logger_1.logger.info('💡 下一步:');
547
+ logger_1.logger.info(` speccore execute --task=${affectedIds.join(',')} --force # 重新执行`);
61
548
  return;
62
549
  }
63
550
  if (!options.desc) {
@@ -78,16 +565,24 @@ async function changeCommand(options) {
78
565
  spinner.fail('未找到活跃迭代。请先运行: speccore iteration create --name <名称>');
79
566
  return;
80
567
  }
81
- // 短 Task ID 支持: Task-001 Task-001-订单管理
568
+ // 短 Task ID 支持: 使用统一 resolver 解析
82
569
  if (options.task) {
83
- const tasks = await (0, state_1.scanTasks)(iteration);
84
- const exact = tasks.find(t => t.id === options.task);
85
- if (!exact) {
86
- const prefix = tasks.filter(t => t.id.startsWith(options.task));
87
- if (prefix.length === 1) {
88
- logger_1.logger.info(`📎 Task 短名匹配: ${options.task} → ${prefix[0].id}`);
89
- options.task = prefix[0].id;
570
+ const taskResult = await (0, resolver_1.resolveTask)(options.task, iteration);
571
+ if (taskResult.exact && taskResult.value) {
572
+ if (taskResult.matchType !== 'exact') {
573
+ const hint = (0, resolver_1.formatResolveResult)(taskResult, 'Task');
574
+ if (hint)
575
+ logger_1.logger.info(hint);
90
576
  }
577
+ options.task = taskResult.value.id;
578
+ }
579
+ else if (taskResult.candidates.length > 1) {
580
+ logger_1.logger.warn(taskResult.hint || '找到多个匹配任务,请指定更精确的名称');
581
+ return;
582
+ }
583
+ else {
584
+ logger_1.logger.warn(taskResult.hint || `Task "${options.task}" 未找到`);
585
+ return;
91
586
  }
92
587
  }
93
588
  if (options.dryRun) {
@@ -124,12 +619,33 @@ async function changeCommand(options) {
124
619
  }
125
620
  else {
126
621
  await applyTaskChange(options, iteration);
622
+ // 任务级变更也记录到 CHANGE_SUMMARY.md
623
+ if (options.task && iteration) {
624
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
625
+ const taskImpact = {
626
+ id: options.task,
627
+ name: options.task,
628
+ status: 'changed',
629
+ level: 'direct',
630
+ reason: options.desc || '任务级变更',
631
+ affectedFiles: ['REQ.md', 'CHANGELOG.md', 'TASK.md'],
632
+ needReExecute: true,
633
+ needRegression: false,
634
+ };
635
+ await writeChangeSummary(iterDir, options.desc || '任务级变更', { directTasks: [taskImpact], indirectTasks: [], unaffectedTasks: [] }, [options.task]);
636
+ }
127
637
  }
128
638
  spinner.stop('需求变更已生效');
129
639
  logger_1.logger.info('');
640
+ if (options.task) {
641
+ logger_1.logger.info(` 📄 变更摘要: 020-specs/CHANGE_SUMMARY.md`);
642
+ }
130
643
  logger_1.logger.info('下一步:');
131
644
  logger_1.logger.info(' 1. 运行 speccore validate --task=' + (options.task || '') + ' 验证完整性');
132
645
  logger_1.logger.info(' 2. 检查受影响的下游任务是否需要回归');
646
+ if (options.task) {
647
+ logger_1.logger.info(` 3. 重新执行变更任务: speccore execute --task=${options.task} --force`);
648
+ }
133
649
  }
134
650
  catch (error) {
135
651
  spinner.fail(`变更失败: ${error}`);
@@ -151,7 +667,9 @@ async function dryRunChange(options, iteration) {
151
667
  logger_1.logger.info('');
152
668
  logger_1.logger.info(`变更描述: ${options.desc}`);
153
669
  if (iteration) {
154
- const taskDir = (0, path_1.join)(process.cwd(), iteration, options.task || '');
670
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
671
+ const taskBase = await resolveTaskBase(iterDir);
672
+ const taskDir = (0, path_1.join)(taskBase, options.task || '');
155
673
  logger_1.logger.info('| 文件 | 影响描述 |');
156
674
  logger_1.logger.info('| :--- | :--- |');
157
675
  logger_1.logger.info(`| ${options.task}/00-specs/REQ.md | 需求变更 |`);
@@ -160,7 +678,7 @@ async function dryRunChange(options, iteration) {
160
678
  logger_1.logger.info(`| ${options.task}/_shared/API_CONTRACT.yaml | 接口契约可能需更新 |`);
161
679
  // 查找受影响的依赖任务
162
680
  if (await (0, fs_extra_1.pathExists)(taskDir)) {
163
- const graphPath = (0, path_1.join)(process.cwd(), iteration, '000-overview', 'PROJECT_GRAPH.md');
681
+ const graphPath = (0, path_1.join)(iterDir, '000-overview', 'PROJECT_GRAPH.md');
164
682
  if (await (0, fs_extra_1.pathExists)(graphPath)) {
165
683
  const content = await (0, fs_extra_1.readFile)(graphPath, 'utf-8');
166
684
  const deps = findDependentTasks(content, options.task || '');
@@ -180,37 +698,40 @@ async function applyTaskChange(options, iteration) {
180
698
  logger_1.logger.error('请指定 --task');
181
699
  return;
182
700
  }
183
- const taskDir = (0, path_1.join)(process.cwd(), iteration, '030-tasks', options.task);
701
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
702
+ const taskBase = await resolveTaskBase(iterDir);
703
+ const taskDir = (0, path_1.join)(taskBase, options.task);
184
704
  if (!await (0, fs_extra_1.pathExists)(taskDir)) {
185
705
  logger_1.logger.error(`任务目录不存在: ${taskDir}`);
186
706
  return;
187
707
  }
188
708
  const tx = new transaction_1.FileTransaction();
189
709
  const now = new Date().toISOString().split('T')[0];
710
+ const ver = await nextVersion((0, path_1.join)(taskDir, '00-specs', 'CHANGELOG.md'));
190
711
  // 更新 REQ.md(事务保护)
191
712
  const reqPath = (0, path_1.join)(taskDir, '00-specs', 'REQ.md');
192
713
  if (await (0, fs_extra_1.pathExists)(reqPath)) {
193
714
  let content = await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
194
- const changeNote = `\n## 变更记录\n\n| ${now} | v1.1 | ${options.desc} | SpecCore |\n`;
715
+ const changeNote = `\n## 变更记录\n\n| ${now} | ${ver} | ${options.desc} | SpecCore |\n`;
195
716
  tx.write(reqPath, content + changeNote);
196
717
  }
197
718
  // 更新 CHANGELOG.md(事务保护)
198
719
  const changelogPath = (0, path_1.join)(taskDir, '00-specs', 'CHANGELOG.md');
199
720
  if (await (0, fs_extra_1.pathExists)(changelogPath)) {
200
721
  let content = await (0, fs_extra_1.readFile)(changelogPath, 'utf-8');
201
- const changeEntry = `| ${now} | v1.1 | ${options.desc} | SpecCore |\n`;
722
+ const changeEntry = `| ${now} | ${ver} | ${options.desc} | SpecCore |\n`;
202
723
  const updated = content.replace(/(\| :--- \| :--- \| :--- \| :--- \|)/, `$1\n${changeEntry}`);
203
724
  tx.write(changelogPath, updated);
204
725
  }
205
726
  else {
206
727
  // 如果没有 CHANGELOG.md,创建一个
207
- tx.write(changelogPath, `# 变更记录\n\n| 时间 | 版本 | 变更内容 | 变更人 |\n| :--- | :--- | :--- | :--- |\n| ${now} | v1.1 | ${options.desc} | SpecCore |\n`);
728
+ tx.write(changelogPath, `# 变更记录\n\n| 时间 | 版本 | 变更内容 | 变更人 |\n| :--- | :--- | :--- | :--- |\n| ${now} | ${ver} | ${options.desc} | SpecCore |\n`);
208
729
  }
209
730
  // 更新 TASK.md 变更履历(事务保护)
210
731
  const taskMdPath = (0, path_1.join)(taskDir, '00-specs', 'TASK.md');
211
732
  if (await (0, fs_extra_1.pathExists)(taskMdPath)) {
212
733
  let content = await (0, fs_extra_1.readFile)(taskMdPath, 'utf-8');
213
- const changeEntry = `| ${now} | v1.1 | 需求变更: ${options.desc} | SpecCore |\n`;
734
+ const changeEntry = `| ${now} | ${ver} | 需求变更: ${options.desc} | SpecCore |\n`;
214
735
  const updated = content.replace(/(\| :--- \| :--- \| :--- \| :--- \|)/, `$1\n${changeEntry}`);
215
736
  tx.write(taskMdPath, updated);
216
737
  }
@@ -224,7 +745,7 @@ async function applyTaskChange(options, iteration) {
224
745
  const ftaskPath = (0, path_1.join)(frontendDir, pd.name, 'TASK.md');
225
746
  if (await (0, fs_extra_1.pathExists)(ftaskPath)) {
226
747
  let content = await (0, fs_extra_1.readFile)(ftaskPath, 'utf-8');
227
- const changeEntry = `| ${now} | v1.1 | 需求变更: ${options.desc} | SpecCore |\n`;
748
+ const changeEntry = `| ${now} | ${ver} | 需求变更: ${options.desc} | SpecCore |\n`;
228
749
  const updated = content.replace(/(\| :--- \| :--- \| :--- \| :--- \|)/, `$1\n${changeEntry}`);
229
750
  tx.write(ftaskPath, updated);
230
751
  }
@@ -247,11 +768,11 @@ async function applyTaskChange(options, iteration) {
247
768
  logger_1.logger.info(` ${options.task}/00-specs/REQ.md`);
248
769
  logger_1.logger.info(` ${options.task}/00-specs/CHANGELOG.md`);
249
770
  logger_1.logger.info(` ${options.task}/00-specs/TASK.md`);
250
- // ── 联动更新上层文档 ──
251
- if (options.requirement && options.task) {
771
+ // ── 联动更新上层文档(默认启用,--no-sync 禁用)──
772
+ if (options.requirement !== false && options.task) {
252
773
  await syncToRequirement(iteration, options.task, options.desc);
253
774
  }
254
- if (options.analysis && options.task) {
775
+ if (options.analysis !== false && options.task) {
255
776
  await syncToAnalysis(iteration, options.task, options.desc);
256
777
  }
257
778
  }
@@ -320,11 +841,56 @@ function normalizeDescription(desc) {
320
841
  // 无法识别,原样返回但去语气词
321
842
  return desc.replace(/[了啦啊呢嗯哦哈]$/, '').trim();
322
843
  }
844
+ /**
845
+ * 同步变更到 REQUIREMENT.md(迭代聚合需求文档)
846
+ */
847
+ /**
848
+ * 将澄清结果(需求分析)持久化到迭代级 CHANGE_SUMMARY.md
849
+ */
850
+ async function writeChangeSummary(iterDir, changeDesc, impactReport, affectedIds) {
851
+ const summaryPath = (0, path_1.join)(iterDir, '020-specs', 'CHANGE_SUMMARY.md');
852
+ const now = new Date().toISOString().split('T')[0];
853
+ let content = '';
854
+ if (await (0, fs_extra_1.pathExists)(summaryPath)) {
855
+ content = await (0, fs_extra_1.readFile)(summaryPath, 'utf-8');
856
+ }
857
+ else {
858
+ content = `# 变更摘要\n\n> 需求变更的分析记录,由 speccore change 自动生成\n\n`;
859
+ }
860
+ // 追加本次变更记录
861
+ content += `\n---\n\n## ${now} — ${changeDesc}\n\n`;
862
+ // 直接影响
863
+ if (impactReport.directTasks.length > 0) {
864
+ content += `### 🔴 直接影响(需修改 Spec + 重新执行)\n\n`;
865
+ for (const t of impactReport.directTasks) {
866
+ content += `- **${t.id}** ${t.name} — ${t.reason}\n`;
867
+ if (t.affectedFiles.length > 0) {
868
+ content += ` - 受影响文件: ${t.affectedFiles.join(', ')}\n`;
869
+ }
870
+ }
871
+ content += '\n';
872
+ }
873
+ // 间接影响
874
+ if (impactReport.indirectTasks.length > 0) {
875
+ content += `### 🟡 间接影响(需回归验证)\n\n`;
876
+ for (const t of impactReport.indirectTasks) {
877
+ content += `- **${t.id}** ${t.name} — ${t.reason}\n`;
878
+ }
879
+ content += '\n';
880
+ }
881
+ // 无影响
882
+ if (impactReport.unaffectedTasks.length > 0) {
883
+ content += `### 🟢 无影响\n\n`;
884
+ content += impactReport.unaffectedTasks.map(t => t.id).join(', ') + '\n\n';
885
+ }
886
+ content += `**已应用变更的任务**: ${affectedIds.join(', ')}\n`;
887
+ await (0, fs_extra_1.writeFile)(summaryPath, content);
888
+ }
323
889
  /**
324
890
  * 同步变更到 REQUIREMENT.md(迭代聚合需求文档)
325
891
  */
326
892
  async function syncToRequirement(iteration, taskId, desc) {
327
- const iterDir = `Iteration-${iteration}`;
893
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
328
894
  const reqPath = (0, path_1.join)(iterDir, '020-specs', 'REQUIREMENT.md');
329
895
  if (!(await (0, fs_extra_1.pathExists)(reqPath))) {
330
896
  logger_1.logger.warn(` ⚠️ REQUIREMENT.md 不存在,跳过同步`);
@@ -356,7 +922,7 @@ async function syncToRequirement(iteration, taskId, desc) {
356
922
  * 同步变更到 ANALYSIS.md(技术方案文档)
357
923
  */
358
924
  async function syncToAnalysis(iteration, taskId, desc) {
359
- const iterDir = `Iteration-${iteration}`;
925
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
360
926
  const analysisPath = (0, path_1.join)(iterDir, '020-specs', 'ANALYSIS.md');
361
927
  if (!(await (0, fs_extra_1.pathExists)(analysisPath))) {
362
928
  logger_1.logger.warn(` ⚠️ ANALYSIS.md 不存在,跳过同步。请先运行 speccore analyze`);