speccore 6.6.0 → 6.11.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 (121) hide show
  1. package/.agents/skills/spec-ask/SKILL.md +24 -4
  2. package/README.md +10 -0
  3. package/dist/cli.js +18 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/about.d.ts.map +1 -1
  6. package/dist/commands/about.js +10 -3
  7. package/dist/commands/about.js.map +1 -1
  8. package/dist/commands/analyze.d.ts +3 -0
  9. package/dist/commands/analyze.d.ts.map +1 -1
  10. package/dist/commands/analyze.js +178 -11
  11. package/dist/commands/analyze.js.map +1 -1
  12. package/dist/commands/change.d.ts.map +1 -1
  13. package/dist/commands/change.js +16 -4
  14. package/dist/commands/change.js.map +1 -1
  15. package/dist/commands/doc2spec.d.ts +1 -0
  16. package/dist/commands/doc2spec.d.ts.map +1 -1
  17. package/dist/commands/doc2spec.js +175 -0
  18. package/dist/commands/doc2spec.js.map +1 -1
  19. package/dist/commands/done.js +25 -5
  20. package/dist/commands/done.js.map +1 -1
  21. package/dist/commands/execute.d.ts.map +1 -1
  22. package/dist/commands/execute.js +20 -6
  23. package/dist/commands/execute.js.map +1 -1
  24. package/dist/commands/help.js +32 -5
  25. package/dist/commands/help.js.map +1 -1
  26. package/dist/commands/iteration/create.d.ts.map +1 -1
  27. package/dist/commands/iteration/create.js +40 -15
  28. package/dist/commands/iteration/create.js.map +1 -1
  29. package/dist/commands/iteration/split.d.ts.map +1 -1
  30. package/dist/commands/iteration/split.js +309 -36
  31. package/dist/commands/iteration/split.js.map +1 -1
  32. package/dist/commands/knowledge.d.ts +6 -0
  33. package/dist/commands/knowledge.d.ts.map +1 -0
  34. package/dist/commands/knowledge.js +94 -0
  35. package/dist/commands/knowledge.js.map +1 -0
  36. package/dist/commands/rag-index.d.ts +19 -0
  37. package/dist/commands/rag-index.d.ts.map +1 -0
  38. package/dist/commands/rag-index.js +272 -0
  39. package/dist/commands/rag-index.js.map +1 -0
  40. package/dist/commands/refresh.d.ts +21 -0
  41. package/dist/commands/refresh.d.ts.map +1 -0
  42. package/dist/commands/refresh.js +185 -0
  43. package/dist/commands/refresh.js.map +1 -0
  44. package/dist/commands/sync-global.d.ts.map +1 -1
  45. package/dist/commands/sync-global.js +59 -0
  46. package/dist/commands/sync-global.js.map +1 -1
  47. package/dist/core/ai-context-generator.d.ts +2 -0
  48. package/dist/core/ai-context-generator.d.ts.map +1 -1
  49. package/dist/core/ai-context-generator.js +8 -6
  50. package/dist/core/ai-context-generator.js.map +1 -1
  51. package/dist/core/analyze-engine.d.ts +19 -0
  52. package/dist/core/analyze-engine.d.ts.map +1 -1
  53. package/dist/core/analyze-engine.js +326 -9
  54. package/dist/core/analyze-engine.js.map +1 -1
  55. package/dist/core/ask-engine.d.ts.map +1 -1
  56. package/dist/core/ask-engine.js +134 -4
  57. package/dist/core/ask-engine.js.map +1 -1
  58. package/dist/core/ask-host-ai.d.ts.map +1 -1
  59. package/dist/core/ask-host-ai.js +9 -6
  60. package/dist/core/ask-host-ai.js.map +1 -1
  61. package/dist/core/code-scanner.d.ts +24 -2
  62. package/dist/core/code-scanner.d.ts.map +1 -1
  63. package/dist/core/code-scanner.js +310 -9
  64. package/dist/core/code-scanner.js.map +1 -1
  65. package/dist/core/context-builder.d.ts.map +1 -1
  66. package/dist/core/context-builder.js +28 -9
  67. package/dist/core/context-builder.js.map +1 -1
  68. package/dist/core/decay-detector.d.ts +2 -2
  69. package/dist/core/decay-detector.d.ts.map +1 -1
  70. package/dist/core/decay-detector.js +135 -6
  71. package/dist/core/decay-detector.js.map +1 -1
  72. package/dist/core/git-integration.d.ts +50 -2
  73. package/dist/core/git-integration.d.ts.map +1 -1
  74. package/dist/core/git-integration.js +217 -27
  75. package/dist/core/git-integration.js.map +1 -1
  76. package/dist/core/global-knowledge.d.ts +25 -0
  77. package/dist/core/global-knowledge.d.ts.map +1 -0
  78. package/dist/core/global-knowledge.js +228 -0
  79. package/dist/core/global-knowledge.js.map +1 -0
  80. package/dist/core/index-guard.d.ts +57 -0
  81. package/dist/core/index-guard.d.ts.map +1 -0
  82. package/dist/core/index-guard.js +202 -0
  83. package/dist/core/index-guard.js.map +1 -0
  84. package/dist/core/intent-cache.d.ts +3 -1
  85. package/dist/core/intent-cache.d.ts.map +1 -1
  86. package/dist/core/intent-cache.js +33 -3
  87. package/dist/core/intent-cache.js.map +1 -1
  88. package/dist/core/intent-recognition.d.ts.map +1 -1
  89. package/dist/core/intent-recognition.js +21 -2
  90. package/dist/core/intent-recognition.js.map +1 -1
  91. package/dist/core/knowledge-graph.d.ts +18 -3
  92. package/dist/core/knowledge-graph.d.ts.map +1 -1
  93. package/dist/core/knowledge-graph.js +539 -42
  94. package/dist/core/knowledge-graph.js.map +1 -1
  95. package/dist/core/knowledge-visualizer.d.ts +19 -0
  96. package/dist/core/knowledge-visualizer.d.ts.map +1 -0
  97. package/dist/core/knowledge-visualizer.js +528 -0
  98. package/dist/core/knowledge-visualizer.js.map +1 -0
  99. package/dist/core/next-steps.js +6 -0
  100. package/dist/core/next-steps.js.map +1 -1
  101. package/dist/core/platform-registry.d.ts +5 -0
  102. package/dist/core/platform-registry.d.ts.map +1 -1
  103. package/dist/core/platform-registry.js +52 -0
  104. package/dist/core/platform-registry.js.map +1 -1
  105. package/dist/core/prompt-builder.d.ts +3 -2
  106. package/dist/core/prompt-builder.d.ts.map +1 -1
  107. package/dist/core/prompt-builder.js +361 -72
  108. package/dist/core/prompt-builder.js.map +1 -1
  109. package/dist/core/rag-engine.d.ts +152 -0
  110. package/dist/core/rag-engine.d.ts.map +1 -0
  111. package/dist/core/rag-engine.js +816 -0
  112. package/dist/core/rag-engine.js.map +1 -0
  113. package/dist/core/spec-merger.d.ts +133 -0
  114. package/dist/core/spec-merger.d.ts.map +1 -0
  115. package/dist/core/spec-merger.js +735 -0
  116. package/dist/core/spec-merger.js.map +1 -0
  117. package/dist/core/unified-retrieval.d.ts +141 -0
  118. package/dist/core/unified-retrieval.d.ts.map +1 -0
  119. package/dist/core/unified-retrieval.js +471 -0
  120. package/dist/core/unified-retrieval.js.map +1 -0
  121. package/package.json +1 -1
@@ -0,0 +1,735 @@
1
+ "use strict";
2
+ /**
3
+ * spec-merger — Section 级局部更新引擎
4
+ *
5
+ * 统一处理三种场景的局部合并:
6
+ * 1. analyze --task --sync → 任务分析结果局部回写 020-specs/
7
+ * 2. change → 需求变更后局部更新受影响的 specs
8
+ * 3. sync-global → 迭代 specs 局部合并到全局层
9
+ *
10
+ * 核心原则:只更新受影响的部分,其余内容不动
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.splitMarkdownSections = splitMarkdownSections;
14
+ exports.patchSection = patchSection;
15
+ exports.extractSection = extractSection;
16
+ exports.extractKeywords = extractKeywords;
17
+ exports.mergeSpecContent = mergeSpecContent;
18
+ exports.findAffectedFeatures = findAffectedFeatures;
19
+ exports.findAffectedSpecSections = findAffectedSpecSections;
20
+ exports.syncTaskToSpecs = syncTaskToSpecs;
21
+ exports.syncIterationToGlobal = syncIterationToGlobal;
22
+ exports.mergeNewRequirementsOnArchive = mergeNewRequirementsOnArchive;
23
+ const fs_extra_1 = require("fs-extra");
24
+ const path_1 = require("path");
25
+ const logger_1 = require("../utils/logger");
26
+ // ═══════════════════════════════════════════════
27
+ // Markdown Section 拆分/重组
28
+ // ═══════════════════════════════════════════════
29
+ /**
30
+ * 将 Markdown 文档按 H2 标题拆分为段落数组
31
+ *
32
+ * 标题前的内容(前言)作为第一个元素返回(heading 为空字符串)
33
+ */
34
+ function splitMarkdownSections(content) {
35
+ const lines = content.split('\n');
36
+ const sections = [];
37
+ let cur = { heading: '', level: 0, content: '', raw: '' };
38
+ for (const line of lines) {
39
+ const hMatch = line.match(/^(#{1,6})\s+(.+)/);
40
+ if (hMatch && hMatch[1].length <= 2) {
41
+ // 只按 H1/H2 拆分(更细粒度可后续扩展)
42
+ if (cur.heading || cur.content) {
43
+ cur.raw = (cur.heading ? `#${' #'.repeat(cur.level - 1)} ${cur.heading}\n` : '') + cur.content;
44
+ sections.push(cur);
45
+ }
46
+ cur = { heading: hMatch[2].trim(), level: hMatch[1].length, content: '', raw: '' };
47
+ }
48
+ else {
49
+ cur.content += (cur.content ? '\n' : '') + line;
50
+ }
51
+ }
52
+ // 推入最后一段
53
+ if (cur.heading || cur.content) {
54
+ cur.raw = (cur.heading ? `#${' #'.repeat(cur.level - 1)} ${cur.heading}\n` : '') + cur.content;
55
+ sections.push(cur);
56
+ }
57
+ return sections;
58
+ }
59
+ /**
60
+ * 替换文档中指定标题的段落,不存在则追加
61
+ *
62
+ * @param docContent 原始完整文档
63
+ * @param sectionHeading 要替换的 H2 标题(精确匹配或包含匹配)
64
+ * @param newContent 新的段落内容(不含标题行)
65
+ * @returns 替换后的完整文档
66
+ */
67
+ function patchSection(docContent, sectionHeading, newContent) {
68
+ const sections = splitMarkdownSections(docContent);
69
+ let patched = false;
70
+ for (const sec of sections) {
71
+ if (sec.heading === sectionHeading || sec.heading.includes(sectionHeading)) {
72
+ sec.content = newContent.endsWith('\n') ? newContent : newContent + '\n';
73
+ patched = true;
74
+ break;
75
+ }
76
+ }
77
+ if (!patched) {
78
+ // 追加新段落
79
+ sections.push({
80
+ heading: sectionHeading,
81
+ level: 2,
82
+ content: newContent.endsWith('\n') ? newContent : newContent + '\n',
83
+ raw: `## ${sectionHeading}\n${newContent}`,
84
+ });
85
+ }
86
+ return sections.map(s => {
87
+ if (s.level === 0)
88
+ return s.content.trimEnd();
89
+ return `${'#'.repeat(s.level)} ${s.heading}\n${s.content.trimEnd()}`;
90
+ }).join('\n\n');
91
+ }
92
+ /**
93
+ * 从文档中提取指定标题的段落内容
94
+ */
95
+ function extractSection(docContent, sectionHeading) {
96
+ const sections = splitMarkdownSections(docContent);
97
+ for (const sec of sections) {
98
+ if (sec.heading === sectionHeading || sec.heading.includes(sectionHeading)) {
99
+ return sec.content.trim();
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ // ═══════════════════════════════════════════════
105
+ // 关键词提取
106
+ // ═══════════════════════════════════════════════
107
+ /** 中文分词常用停用词(过滤用) */
108
+ const STOP_WORDS = new Set([
109
+ '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一',
110
+ '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着',
111
+ '没有', '看', '好', '自己', '这', '他', '她', '它', '们', '那', '些',
112
+ '什么', '怎么', '如何', '为什么', '可以', '需要', '使用', '通过', '进行',
113
+ '功能', '模块', '系统', '管理', '服务', '实现', '设计', '开发',
114
+ ]);
115
+ /**
116
+ * 从文本中提取特征关键词(≥3 字符,过滤停用词)
117
+ */
118
+ function extractKeywords(text) {
119
+ const chinese = text.match(/[\u4e00-\u9fa5]{3,}/g) || [];
120
+ const english = text.match(/[a-zA-Z]{3,}/g) || [];
121
+ const keywords = [...chinese, ...english]
122
+ .filter(w => !STOP_WORDS.has(w))
123
+ .map(w => w.toLowerCase());
124
+ return [...new Set(keywords)];
125
+ }
126
+ // ═══════════════════════════════════════════════
127
+ // 功能模块级合并
128
+ // ═══════════════════════════════════════════════
129
+ /**
130
+ * 将源文档中的相关段落合并到目标文档
131
+ *
132
+ * 匹配策略:
133
+ * 1. 标题精确匹配 → 直接替换
134
+ * 2. 标题关键词重叠 → 替换(重叠率 > 阈值)
135
+ * 3. 无匹配 → 追加到目标文档末尾
136
+ *
137
+ * @param sourceContent 源文档(新分析结果)
138
+ * @param targetContent 目标文档(现有 spec)
139
+ * @param keywords 用于匹配的关键词集合
140
+ * @returns 合并后的文档
141
+ */
142
+ function mergeSpecContent(sourceContent, targetContent, keywords) {
143
+ const sourceSections = splitMarkdownSections(sourceContent);
144
+ const targetSections = splitMarkdownSections(targetContent);
145
+ const patchedSections = [];
146
+ const kwLower = keywords.map(k => k.toLowerCase());
147
+ for (const src of sourceSections) {
148
+ if (!src.heading || src.level === 0)
149
+ continue; // 跳过前言
150
+ // 关键词匹配打分
151
+ const srcKw = extractKeywords(src.heading + ' ' + src.content.slice(0, 200));
152
+ let matchScore = 0;
153
+ for (const kw of kwLower) {
154
+ if (srcKw.some(sk => sk.includes(kw) || kw.includes(sk))) {
155
+ matchScore++;
156
+ }
157
+ }
158
+ if (matchScore === 0 && kwLower.length > 0)
159
+ continue; // 不相关的段落,跳过
160
+ // 在目标中找匹配段落
161
+ let bestMatch = -1;
162
+ let bestScore = 0;
163
+ for (let i = 0; i < targetSections.length; i++) {
164
+ const tgt = targetSections[i];
165
+ if (!tgt.heading)
166
+ continue;
167
+ // 精确匹配
168
+ if (tgt.heading === src.heading) {
169
+ bestMatch = i;
170
+ bestScore = 100;
171
+ break;
172
+ }
173
+ // 包含匹配
174
+ if (tgt.heading.includes(src.heading) || src.heading.includes(tgt.heading)) {
175
+ bestMatch = i;
176
+ bestScore = 80;
177
+ break;
178
+ }
179
+ // 关键词重叠匹配
180
+ const tgtKw = extractKeywords(tgt.heading);
181
+ let overlap = 0;
182
+ for (const sk of srcKw) {
183
+ if (tgtKw.some(tk => tk.includes(sk) || sk.includes(tk))) {
184
+ overlap++;
185
+ }
186
+ }
187
+ if (overlap > bestScore) {
188
+ bestScore = overlap;
189
+ bestMatch = i;
190
+ }
191
+ }
192
+ if (bestMatch >= 0 && bestScore > 0) {
193
+ // 替换已有段落
194
+ targetSections[bestMatch].content = src.content;
195
+ patchedSections.push(targetSections[bestMatch].heading);
196
+ }
197
+ else if (matchScore > 0) {
198
+ // 追加新段落
199
+ targetSections.push(src);
200
+ patchedSections.push(src.heading);
201
+ }
202
+ }
203
+ const content = targetSections.map(s => {
204
+ if (s.level === 0)
205
+ return s.content.trimEnd();
206
+ return `${'#'.repeat(s.level)} ${s.heading}\n${s.content.trimEnd()}`;
207
+ }).join('\n\n');
208
+ return { content, patchedSections };
209
+ }
210
+ // ═══════════════════════════════════════════════
211
+ // 影响范围查询
212
+ // ═══════════════════════════════════════════════
213
+ /**
214
+ * 根据关键词查找受影响的功能模块 spec 文件
215
+ *
216
+ * 扫描 020-specs/features/ 目录,按文件名和内容匹配关键词
217
+ */
218
+ async function findAffectedFeatures(iterDir, keywords) {
219
+ const featuresDir = (0, path_1.join)(iterDir, '020-specs', 'features');
220
+ if (!(await (0, fs_extra_1.pathExists)(featuresDir)))
221
+ return [];
222
+ const results = [];
223
+ const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
224
+ for (const entry of entries) {
225
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
226
+ continue;
227
+ const featureName = entry.name.replace('.md', '');
228
+ const specPath = (0, path_1.join)('020-specs', 'features', entry.name);
229
+ // 文件名匹配
230
+ const nameKw = extractKeywords(featureName);
231
+ let score = 0;
232
+ for (const kw of keywords) {
233
+ if (featureName.includes(kw) || nameKw.some(nk => nk.includes(kw) || kw.includes(nk))) {
234
+ score += 2;
235
+ }
236
+ }
237
+ // 内容匹配(轻量:只读前 500 字符)
238
+ const content = await (0, fs_extra_1.readFile)((0, path_1.join)(featuresDir, entry.name), 'utf-8');
239
+ const header = content.slice(0, 500);
240
+ for (const kw of keywords) {
241
+ if (header.includes(kw))
242
+ score += 1;
243
+ }
244
+ if (score > 0) {
245
+ // 推导对应的需求源路径
246
+ const reqPath = `010-requirements/features/${featureName}/README.md`;
247
+ results.push({ name: featureName, specPath, reqPath, confidence: score });
248
+ }
249
+ }
250
+ return results.sort((a, b) => b.confidence - a.confidence);
251
+ }
252
+ /**
253
+ * 根据关键词查找受影响的顶层 spec 文件(TECH.md / TEST.md 等)
254
+ *
255
+ * 返回需要 patch 的 section 名称列表
256
+ */
257
+ async function findAffectedSpecSections(iterDir, keywords) {
258
+ const specFiles = ['TECH.md', 'TEST.md', 'RISK.md', 'DEPS.md', 'MONITOR.md'];
259
+ const results = [];
260
+ for (const filename of specFiles) {
261
+ const filePath = (0, path_1.join)(iterDir, '020-specs', filename);
262
+ if (!(await (0, fs_extra_1.pathExists)(filePath)))
263
+ continue;
264
+ const content = await (0, fs_extra_1.readFile)(filePath, 'utf-8');
265
+ const sections = splitMarkdownSections(content);
266
+ const matched = [];
267
+ for (const sec of sections) {
268
+ if (!sec.heading)
269
+ continue;
270
+ const secKw = extractKeywords(sec.heading + ' ' + sec.content.slice(0, 200));
271
+ for (const kw of keywords) {
272
+ if (secKw.some(sk => sk.includes(kw) || kw.includes(sk))) {
273
+ matched.push(sec.heading);
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ if (matched.length > 0) {
279
+ results.push({ file: filename, sections: matched });
280
+ }
281
+ }
282
+ return results;
283
+ }
284
+ // ═══════════════════════════════════════════════
285
+ // 高层 API:任务级局部回写
286
+ // ═══════════════════════════════════════════════
287
+ /**
288
+ * 将任务分析结果局部回写到迭代级 020-specs/
289
+ *
290
+ * 流程:
291
+ * 1. 从任务标题/REQ.md 提取关键词
292
+ * 2. 查找受影响的功能模块 spec 文件
293
+ * 3. 对每个受影响文件执行 section 级 merge
294
+ * 4. 对顶层 spec 文件(TECH.md 等)执行 section 级 patch
295
+ *
296
+ * @param iterDir 迭代目录绝对路径
297
+ * @param taskDir 任务目录绝对路径
298
+ * @param taskName 任务名称(用于关键词提取)
299
+ * @returns 合并结果摘要
300
+ */
301
+ async function syncTaskToSpecs(iterDir, taskDir, taskName) {
302
+ const result = { filesUpdated: 0, changes: [] };
303
+ // 1. 提取关键词
304
+ const keywords = extractKeywords(taskName);
305
+ // 补充:从 _shared/REQ.md 提取更多关键词
306
+ const reqMdPath = (0, path_1.join)(taskDir, '_shared', 'REQ.md');
307
+ if (await (0, fs_extra_1.pathExists)(reqMdPath)) {
308
+ const reqContent = await (0, fs_extra_1.readFile)(reqMdPath, 'utf-8');
309
+ const reqKw = extractKeywords(reqContent.slice(0, 1000));
310
+ keywords.push(...reqKw);
311
+ }
312
+ const uniqueKw = [...new Set(keywords)];
313
+ if (uniqueKw.length === 0) {
314
+ logger_1.logger.warn(' ⚠️ 无法提取有效关键词,跳过局部回写');
315
+ return result;
316
+ }
317
+ logger_1.logger.info(` 🔑 关键词: ${uniqueKw.slice(0, 6).join(', ')}${uniqueKw.length > 6 ? ` ...等${uniqueKw.length}个` : ''}`);
318
+ // 2. 查找受影响的功能模块
319
+ const affectedFeatures = await findAffectedFeatures(iterDir, uniqueKw);
320
+ // 3. 对每个受影响的功能模块执行 merge
321
+ for (const feature of affectedFeatures) {
322
+ const specFullPath = (0, path_1.join)(iterDir, feature.specPath);
323
+ const sourceContent = await readTaskSpecContent(taskDir, feature.name, uniqueKw);
324
+ if (!sourceContent) {
325
+ result.changes.push({ file: feature.specPath, sectionsPatched: [], action: 'skipped' });
326
+ continue;
327
+ }
328
+ let targetContent = '';
329
+ if (await (0, fs_extra_1.pathExists)(specFullPath)) {
330
+ targetContent = await (0, fs_extra_1.readFile)(specFullPath, 'utf-8');
331
+ }
332
+ const { content, patchedSections } = mergeSpecContent(sourceContent, targetContent, uniqueKw);
333
+ if (patchedSections.length > 0) {
334
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(iterDir, '020-specs', 'features'));
335
+ await (0, fs_extra_1.writeFile)(specFullPath, content, 'utf-8');
336
+ result.filesUpdated++;
337
+ result.changes.push({ file: feature.specPath, sectionsPatched: patchedSections, action: targetContent ? 'patched' : 'created' });
338
+ logger_1.logger.info(` ✅ ${feature.specPath}: 更新 ${patchedSections.length} 个段落 [${patchedSections.join(', ')}]`);
339
+ }
340
+ else {
341
+ result.changes.push({ file: feature.specPath, sectionsPatched: [], action: 'skipped' });
342
+ }
343
+ }
344
+ // 4. 对顶层 spec 文件执行 section 级 patch
345
+ const affectedSections = await findAffectedSpecSections(iterDir, uniqueKw);
346
+ for (const { file, sections } of affectedSections) {
347
+ const specFullPath = (0, path_1.join)(iterDir, '020-specs', file);
348
+ const taskSpecContent = await readTaskSpecByFilename(taskDir, file);
349
+ if (!taskSpecContent)
350
+ continue;
351
+ const existingContent = await (0, fs_extra_1.pathExists)(specFullPath)
352
+ ? await (0, fs_extra_1.readFile)(specFullPath, 'utf-8')
353
+ : '';
354
+ let updated = existingContent;
355
+ const patched = [];
356
+ for (const sectionName of sections) {
357
+ const newSectionContent = extractSection(taskSpecContent, sectionName);
358
+ if (newSectionContent) {
359
+ updated = patchSection(updated, sectionName, newSectionContent);
360
+ patched.push(sectionName);
361
+ }
362
+ }
363
+ if (patched.length > 0) {
364
+ await (0, fs_extra_1.writeFile)(specFullPath, updated, 'utf-8');
365
+ result.filesUpdated++;
366
+ result.changes.push({ file: `020-specs/${file}`, sectionsPatched: patched, action: existingContent ? 'patched' : 'created' });
367
+ logger_1.logger.info(` ✅ 020-specs/${file}: 更新 ${patched.length} 个段落 [${patched.join(', ')}]`);
368
+ }
369
+ }
370
+ return result;
371
+ }
372
+ // ═══════════════════════════════════════════════
373
+ // 高层 API:迭代级同步到全局
374
+ // ═══════════════════════════════════════════════
375
+ /**
376
+ * 将迭代级 spec 文件局部合并到全局层
377
+ *
378
+ * 与 syncTaskToSpecs 类似,但方向是 iteration → global
379
+ *
380
+ * @param iterDir 迭代目录绝对路径
381
+ * @param globalDir 全局 .speccore 目录绝对路径
382
+ * @param keywords 可选的过滤关键词(只同步相关模块)
383
+ */
384
+ async function syncIterationToGlobal(iterDir, globalDir, keywords) {
385
+ const result = { filesUpdated: 0, changes: [] };
386
+ // 扫描迭代 020-specs/ 下的所有 spec 文件
387
+ const iterSpecsDir = (0, path_1.join)(iterDir, '020-specs');
388
+ if (!(await (0, fs_extra_1.pathExists)(iterSpecsDir)))
389
+ return result;
390
+ const entries = await (0, fs_extra_1.readdir)(iterSpecsDir, { withFileTypes: true });
391
+ for (const entry of entries) {
392
+ if (entry.isDirectory() && entry.name === 'features') {
393
+ // 功能模块:逐个 merge 到全局
394
+ const featureEntries = await (0, fs_extra_1.readdir)((0, path_1.join)(iterSpecsDir, 'features'), { withFileTypes: true });
395
+ for (const fe of featureEntries) {
396
+ if (!fe.isFile() || !fe.name.endsWith('.md'))
397
+ continue;
398
+ // 关键词过滤
399
+ if (keywords && keywords.length > 0) {
400
+ const feKw = extractKeywords(fe.name);
401
+ const hasMatch = keywords.some(kw => feKw.some(fk => fk.includes(kw) || kw.includes(fk)) || fe.name.includes(kw));
402
+ if (!hasMatch)
403
+ continue;
404
+ }
405
+ const iterContent = await (0, fs_extra_1.readFile)((0, path_1.join)(iterSpecsDir, 'features', fe.name), 'utf-8');
406
+ const globalFeatureDir = (0, path_1.join)(globalDir, 'GLOBAL', '020-specs', 'features');
407
+ const globalPath = (0, path_1.join)(globalFeatureDir, fe.name);
408
+ const globalContent = (await (0, fs_extra_1.pathExists)(globalPath))
409
+ ? await (0, fs_extra_1.readFile)(globalPath, 'utf-8')
410
+ : '';
411
+ const mergeKw = keywords || extractKeywords(fe.name);
412
+ const { content, patchedSections } = mergeSpecContent(iterContent, globalContent, mergeKw);
413
+ if (patchedSections.length > 0 || !globalContent) {
414
+ await (0, fs_extra_1.ensureDir)(globalFeatureDir);
415
+ await (0, fs_extra_1.writeFile)(globalPath, content, 'utf-8');
416
+ result.filesUpdated++;
417
+ result.changes.push({
418
+ file: `GLOBAL/020-specs/features/${fe.name}`,
419
+ sectionsPatched: patchedSections,
420
+ action: globalContent ? 'patched' : 'created',
421
+ });
422
+ }
423
+ }
424
+ }
425
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
426
+ // 顶层文件:section 级 merge
427
+ if (keywords && keywords.length > 0) {
428
+ const fileKw = extractKeywords(entry.name);
429
+ const hasMatch = keywords.some(kw => fileKw.some(fk => fk.includes(kw) || kw.includes(fk)));
430
+ if (!hasMatch)
431
+ continue;
432
+ }
433
+ const iterContent = await (0, fs_extra_1.readFile)((0, path_1.join)(iterSpecsDir, entry.name), 'utf-8');
434
+ const globalSpecDir = (0, path_1.join)(globalDir, 'GLOBAL', '020-specs');
435
+ const globalPath = (0, path_1.join)(globalSpecDir, entry.name);
436
+ const globalContent = (await (0, fs_extra_1.pathExists)(globalPath))
437
+ ? await (0, fs_extra_1.readFile)(globalPath, 'utf-8')
438
+ : '';
439
+ const mergeKw = keywords || extractKeywords(entry.name);
440
+ const { content, patchedSections } = mergeSpecContent(iterContent, globalContent, mergeKw);
441
+ if (patchedSections.length > 0 || !globalContent) {
442
+ await (0, fs_extra_1.ensureDir)(globalSpecDir);
443
+ await (0, fs_extra_1.writeFile)(globalPath, content, 'utf-8');
444
+ result.filesUpdated++;
445
+ result.changes.push({
446
+ file: `GLOBAL/020-specs/${entry.name}`,
447
+ sectionsPatched: patchedSections,
448
+ action: globalContent ? 'patched' : 'created',
449
+ });
450
+ }
451
+ }
452
+ }
453
+ return result;
454
+ }
455
+ // ═══════════════════════════════════════════════
456
+ // 内部辅助
457
+ // ═══════════════════════════════════════════════
458
+ /** 读取任务目录中与功能模块相关的 spec 内容 */
459
+ async function readTaskSpecContent(taskDir, featureName, keywords) {
460
+ // 优先从 _shared/TECH.md 提取
461
+ const techPath = (0, path_1.join)(taskDir, '_shared', 'TECH.md');
462
+ if (await (0, fs_extra_1.pathExists)(techPath)) {
463
+ const content = await (0, fs_extra_1.readFile)(techPath, 'utf-8');
464
+ // 尝试提取与 featureName 相关的段落
465
+ const sectionContent = extractSection(content, featureName);
466
+ if (sectionContent)
467
+ return `## ${featureName}\n${sectionContent}`;
468
+ // 回退:提取包含关键词的段落
469
+ const sections = splitMarkdownSections(content);
470
+ const relevant = sections.filter(s => {
471
+ if (!s.heading)
472
+ return false;
473
+ return keywords.some(kw => s.heading.includes(kw) || s.content.slice(0, 200).includes(kw));
474
+ });
475
+ if (relevant.length > 0) {
476
+ return relevant.map(s => `## ${s.heading}\n${s.content.trim()}`).join('\n\n');
477
+ }
478
+ }
479
+ // 回退:读取 _shared/REQ.md
480
+ const reqPath = (0, path_1.join)(taskDir, '_shared', 'REQ.md');
481
+ if (await (0, fs_extra_1.pathExists)(reqPath)) {
482
+ const content = await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
483
+ return content;
484
+ }
485
+ return null;
486
+ }
487
+ /** 按文件名读取任务目录中对应的 spec 文件 */
488
+ async function readTaskSpecByFilename(taskDir, filename) {
489
+ // 优先 _shared/,回退 99-artifacts/
490
+ const paths = [
491
+ (0, path_1.join)(taskDir, '_shared', filename),
492
+ (0, path_1.join)(taskDir, '00-specs', filename),
493
+ (0, path_1.join)(taskDir, '99-artifacts', filename),
494
+ ];
495
+ for (const p of paths) {
496
+ if (await (0, fs_extra_1.pathExists)(p))
497
+ return (0, fs_extra_1.readFile)(p, 'utf-8');
498
+ }
499
+ return null;
500
+ }
501
+ // ═══════════════════════════════════════════════
502
+ // 高层 API:归档时新增需求合并回原文档
503
+ // ═══════════════════════════════════════════════
504
+ /**
505
+ * 归档时将新增需求局部合并回原需求文档
506
+ *
507
+ * 流程:
508
+ * 1. 扫描迭代下所有任务,找出「新增需求」任务
509
+ * (通过 .meta/ 标记、CHANGE_SUMMARY.md 记录、或任务名含"新增"特征)
510
+ * 2. 读取任务的 _shared/REQ.md
511
+ * 3. 按关键词匹配原需求文档(010-requirements/features/ 或 converted/)
512
+ * 4. 用 mergeSpecContent() 做 section 级合并
513
+ * 5. 未匹配到原文档的,追加到 features/{slug}/README.md
514
+ */
515
+ async function mergeNewRequirementsOnArchive(iterDir) {
516
+ const result = { filesUpdated: 0, changes: [] };
517
+ const tasksDir = (0, path_1.join)(iterDir, '030-tasks');
518
+ if (!(await (0, fs_extra_1.pathExists)(tasksDir)))
519
+ return result;
520
+ // 扫描所有任务目录(含类型子目录)
521
+ const taskDirs = await scanTaskDirs(tasksDir);
522
+ // 读取 CHANGE_SUMMARY.md 获取新增需求记录
523
+ const changeSummaryPath = (0, path_1.join)(iterDir, '020-specs', 'CHANGE_SUMMARY.md');
524
+ let changeSummaryContent = '';
525
+ if (await (0, fs_extra_1.pathExists)(changeSummaryPath)) {
526
+ changeSummaryContent = await (0, fs_extra_1.readFile)(changeSummaryPath, 'utf-8');
527
+ }
528
+ for (const { taskId, taskDir } of taskDirs) {
529
+ // 检查是否是「新增需求」任务
530
+ const isNewReq = await detectNewRequirement(taskDir, taskId, changeSummaryContent);
531
+ if (!isNewReq)
532
+ continue;
533
+ // 读取任务的 REQ.md
534
+ const reqMdPath = (0, path_1.join)(taskDir, '_shared', 'REQ.md');
535
+ if (!(await (0, fs_extra_1.pathExists)(reqMdPath)))
536
+ continue;
537
+ const reqContent = await (0, fs_extra_1.readFile)(reqMdPath, 'utf-8');
538
+ // 提取关键词
539
+ const titleMatch = reqContent.match(/^#\s+(.+)/m);
540
+ const taskTitle = titleMatch ? titleMatch[1].trim() : taskId;
541
+ const keywords = extractKeywords(taskTitle + ' ' + reqContent.slice(0, 500));
542
+ if (keywords.length === 0)
543
+ continue;
544
+ // 查找匹配的原需求文档
545
+ const matchedOriginal = await findMatchingOriginalRequirement(iterDir, keywords);
546
+ if (matchedOriginal) {
547
+ // 合并到原文档
548
+ const originalContent = await (0, fs_extra_1.readFile)(matchedOriginal.path, 'utf-8');
549
+ const { content, patchedSections } = mergeSpecContent(reqContent, originalContent, keywords);
550
+ if (patchedSections.length > 0) {
551
+ await (0, fs_extra_1.writeFile)(matchedOriginal.path, content, 'utf-8');
552
+ result.filesUpdated++;
553
+ result.changes.push({
554
+ file: matchedOriginal.relativePath,
555
+ sectionsPatched: patchedSections,
556
+ action: 'patched',
557
+ });
558
+ logger_1.logger.info(` ✅ ${taskId} → ${matchedOriginal.relativePath}: 合并 ${patchedSections.length} 个段落`);
559
+ }
560
+ else {
561
+ result.changes.push({ file: matchedOriginal.relativePath, sectionsPatched: [], action: 'skipped' });
562
+ }
563
+ }
564
+ else {
565
+ // 未匹配到原文档 → 追加到 features/{slug}/README.md
566
+ const slug = slugify(taskTitle);
567
+ const featureDir = (0, path_1.join)(iterDir, '010-requirements', 'features', slug);
568
+ const featurePath = (0, path_1.join)(featureDir, 'README.md');
569
+ if (!(await (0, fs_extra_1.pathExists)(featurePath))) {
570
+ await (0, fs_extra_1.ensureDir)(featureDir);
571
+ await (0, fs_extra_1.writeFile)(featurePath, reqContent, 'utf-8');
572
+ result.filesUpdated++;
573
+ result.changes.push({
574
+ file: `010-requirements/features/${slug}/README.md`,
575
+ sectionsPatched: ['(新建)'],
576
+ action: 'created',
577
+ });
578
+ logger_1.logger.info(` ✨ ${taskId} → 新建 features/${slug}/README.md`);
579
+ }
580
+ else {
581
+ // 已存在 → section 级合并
582
+ const existing = await (0, fs_extra_1.readFile)(featurePath, 'utf-8');
583
+ const { content, patchedSections } = mergeSpecContent(reqContent, existing, keywords);
584
+ if (patchedSections.length > 0) {
585
+ await (0, fs_extra_1.writeFile)(featurePath, content, 'utf-8');
586
+ result.filesUpdated++;
587
+ result.changes.push({
588
+ file: `010-requirements/features/${slug}/README.md`,
589
+ sectionsPatched: patchedSections,
590
+ action: 'patched',
591
+ });
592
+ logger_1.logger.info(` ✅ ${taskId} → features/${slug}/README.md: 合并 ${patchedSections.length} 个段落`);
593
+ }
594
+ }
595
+ }
596
+ }
597
+ return result;
598
+ }
599
+ // ═══════════════════════════════════════════════
600
+ // 归档合并 — 内部辅助
601
+ // ═══════════════════════════════════════════════
602
+ /** 递归扫描任务目录(支持类型子目录) */
603
+ async function scanTaskDirs(tasksDir) {
604
+ const results = [];
605
+ const entries = await (0, fs_extra_1.readdir)(tasksDir, { withFileTypes: true });
606
+ for (const entry of entries) {
607
+ if (!entry.isDirectory())
608
+ continue;
609
+ if (entry.name.startsWith('.'))
610
+ continue;
611
+ const fullPath = (0, path_1.join)(tasksDir, entry.name);
612
+ if (entry.name.startsWith('Task-')) {
613
+ results.push({ taskId: entry.name, taskDir: fullPath });
614
+ }
615
+ else {
616
+ // 类型子目录(feature/bugfix/refactor/...)→ 递归扫描
617
+ try {
618
+ const subEntries = await (0, fs_extra_1.readdir)(fullPath, { withFileTypes: true });
619
+ for (const sub of subEntries) {
620
+ if (sub.isDirectory() && sub.name.startsWith('Task-')) {
621
+ results.push({ taskId: sub.name, taskDir: (0, path_1.join)(fullPath, sub.name) });
622
+ }
623
+ }
624
+ }
625
+ catch { }
626
+ }
627
+ }
628
+ return results;
629
+ }
630
+ /** 检测任务是否为「新增需求」(通过多种信号判断) */
631
+ async function detectNewRequirement(taskDir, taskId, changeSummaryContent) {
632
+ // 信号 1:CHANGE_SUMMARY.md 中记录了该任务为「新增」
633
+ if (changeSummaryContent) {
634
+ const pattern = new RegExp(`${taskId}.*(?:新增|新增需求|added|new)`, 'i');
635
+ if (pattern.test(changeSummaryContent))
636
+ return true;
637
+ }
638
+ // 信号 2:.meta/ 目录中有 change-source 标记
639
+ const changeSourcePath = (0, path_1.join)(taskDir, '.meta', 'change-source');
640
+ if (await (0, fs_extra_1.pathExists)(changeSourcePath)) {
641
+ const content = await (0, fs_extra_1.readFile)(changeSourcePath, 'utf-8');
642
+ if (content.includes('new') || content.includes('新增'))
643
+ return true;
644
+ }
645
+ // 信号 3:任务名/REQ.md 标题含「新增」特征
646
+ const reqMdPath = (0, path_1.join)(taskDir, '_shared', 'REQ.md');
647
+ if (await (0, fs_extra_1.pathExists)(reqMdPath)) {
648
+ const content = await (0, fs_extra_1.readFile)(reqMdPath, 'utf-8');
649
+ const titleMatch = content.match(/^#\s+(.+)/m);
650
+ if (titleMatch) {
651
+ const title = titleMatch[1];
652
+ if (/^新增|新增需求|^\[?new\]?/i.test(title))
653
+ return true;
654
+ }
655
+ }
656
+ return false;
657
+ }
658
+ /** 在 010-requirements/ 中查找与关键词匹配的原需求文档 */
659
+ async function findMatchingOriginalRequirement(iterDir, keywords) {
660
+ const candidates = [];
661
+ const reqDir = (0, path_1.join)(iterDir, '010-requirements');
662
+ if (!(await (0, fs_extra_1.pathExists)(reqDir)))
663
+ return null;
664
+ // 扫描 features/ 目录
665
+ const featuresDir = (0, path_1.join)(reqDir, 'features');
666
+ if (await (0, fs_extra_1.pathExists)(featuresDir)) {
667
+ const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
668
+ for (const entry of entries) {
669
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
670
+ continue;
671
+ const readmePath = (0, path_1.join)(featuresDir, entry.name, 'README.md');
672
+ if (!(await (0, fs_extra_1.pathExists)(readmePath)))
673
+ continue;
674
+ let score = 0;
675
+ const nameKw = extractKeywords(entry.name);
676
+ for (const kw of keywords) {
677
+ if (entry.name.includes(kw) || nameKw.some(nk => nk.includes(kw) || kw.includes(nk))) {
678
+ score += 2;
679
+ }
680
+ }
681
+ // 内容匹配(轻量)
682
+ const content = await (0, fs_extra_1.readFile)(readmePath, 'utf-8');
683
+ const header = content.slice(0, 500);
684
+ for (const kw of keywords) {
685
+ if (header.includes(kw))
686
+ score += 1;
687
+ }
688
+ if (score > 0) {
689
+ candidates.push({
690
+ path: readmePath,
691
+ relativePath: `010-requirements/features/${entry.name}/README.md`,
692
+ score,
693
+ });
694
+ }
695
+ }
696
+ }
697
+ // 扫描 converted/ 目录
698
+ const convertedDir = (0, path_1.join)(reqDir, 'converted');
699
+ if (await (0, fs_extra_1.pathExists)(convertedDir)) {
700
+ const entries = await (0, fs_extra_1.readdir)(convertedDir, { withFileTypes: true });
701
+ for (const entry of entries) {
702
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
703
+ continue;
704
+ const filePath = (0, path_1.join)(convertedDir, entry.name);
705
+ let score = 0;
706
+ const nameKw = extractKeywords(entry.name.replace('.md', ''));
707
+ for (const kw of keywords) {
708
+ if (entry.name.includes(kw) || nameKw.some(nk => nk.includes(kw) || kw.includes(nk))) {
709
+ score += 2;
710
+ }
711
+ }
712
+ if (score > 0) {
713
+ candidates.push({
714
+ path: filePath,
715
+ relativePath: `010-requirements/converted/${entry.name}`,
716
+ score,
717
+ });
718
+ }
719
+ }
720
+ }
721
+ if (candidates.length === 0)
722
+ return null;
723
+ candidates.sort((a, b) => b.score - a.score);
724
+ return candidates[0];
725
+ }
726
+ /** 简单的 slugify(中文转拼音太复杂,用关键词拼接) */
727
+ function slugify(text) {
728
+ // 提取英文单词和数字
729
+ const english = text.match(/[a-zA-Z0-9]+/g) || [];
730
+ if (english.length > 0)
731
+ return english.join('-').toLowerCase().slice(0, 40);
732
+ // 回退:取前 20 个字符(去空格和特殊字符)
733
+ return text.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '-').slice(0, 20) || 'new-feature';
734
+ }
735
+ //# sourceMappingURL=spec-merger.js.map