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
@@ -51,6 +51,8 @@ const crypto_1 = require("crypto");
51
51
  const context_1 = require("./context");
52
52
  const task_paths_1 = require("./task-paths");
53
53
  const task_utils_1 = require("../utils/task-utils");
54
+ const logger_1 = require("../utils/logger");
55
+ const kgCache = new Map();
54
56
  // ═══════════════════════════════════════════════
55
57
  // 文件扫描辅助
56
58
  // ═══════════════════════════════════════════════
@@ -102,6 +104,9 @@ async function scanRequirements(iterDir, iterName) {
102
104
  const fullPath = (0, path_1.join)(dir, item.name);
103
105
  const relPath = `${prefix}${item.name}`;
104
106
  if (item.isDirectory()) {
107
+ // sources/ 由 scanUserFiles() 单独处理,避免双重注册
108
+ if (item.name === 'sources')
109
+ continue;
105
110
  await scanDir(fullPath, `${relPath}/`);
106
111
  }
107
112
  else if (item.name.endsWith('.md') && item.name !== 'INDEX.md' && !(0, task_utils_1.isTimestampBackup)(item.name)) {
@@ -118,7 +123,13 @@ async function scanRequirements(iterDir, iterName) {
118
123
  file: `010-requirements/${relPath}`,
119
124
  hash,
120
125
  mtime,
121
- tags: [prefix.includes('features/') ? 'feature' : 'requirement'],
126
+ tags: [
127
+ prefix.includes('features/') ? 'feature'
128
+ : prefix.includes('bugs/') ? 'bug'
129
+ : prefix.includes('refactors/') ? 'refactor'
130
+ : prefix.includes('research/') ? 'research'
131
+ : 'requirement'
132
+ ],
122
133
  });
123
134
  }
124
135
  }
@@ -179,6 +190,74 @@ async function scanSpecs(iterDir) {
179
190
  }
180
191
  }
181
192
  }
193
+ // 扫描 features/ 子目录(按功能模块的分析产出)
194
+ const featuresDir = (0, path_1.join)(specsDir, 'features');
195
+ if (await (0, fs_extra_1.pathExists)(featuresDir)) {
196
+ const featureEntries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
197
+ for (const fe of featureEntries) {
198
+ if (fe.name.startsWith('.') || (0, task_utils_1.isTimestampBackup)(fe.name))
199
+ continue;
200
+ if (!fe.name.endsWith('.md'))
201
+ continue;
202
+ const fullPath = (0, path_1.join)(featuresDir, fe.name);
203
+ const { hash, mtime } = await fileHash(fullPath);
204
+ const title = await extractTitle(fullPath);
205
+ const featureName = fe.name.replace('.md', '');
206
+ entities.push({
207
+ id: `SPEC:features/${featureName}`,
208
+ type: 'spec',
209
+ title: title || featureName,
210
+ file: `020-specs/features/${fe.name}`,
211
+ hash,
212
+ mtime,
213
+ tags: ['feature-spec'],
214
+ });
215
+ // 关联到对应的需求 feature(同名匹配)
216
+ const reqFeaturePath = (0, path_1.join)('010-requirements', 'features', featureName);
217
+ const matchedReq = entities.find(e => e.file?.startsWith(reqFeaturePath));
218
+ if (matchedReq) {
219
+ relations.push({
220
+ from: `SPEC:features/${featureName}`,
221
+ to: matchedReq.id,
222
+ type: 'references',
223
+ });
224
+ }
225
+ }
226
+ }
227
+ // 扫描类型目录(bugs/refactors/research — 扁平文件)
228
+ for (const typeDir of ['bugs', 'refactors', 'research']) {
229
+ const typeDirPath = (0, path_1.join)(specsDir, typeDir);
230
+ if (!(await (0, fs_extra_1.pathExists)(typeDirPath)))
231
+ continue;
232
+ const typeEntries = await (0, fs_extra_1.readdir)(typeDirPath, { withFileTypes: true });
233
+ for (const te of typeEntries) {
234
+ if (!te.isFile() || !te.name.endsWith('.md') || (0, task_utils_1.isTimestampBackup)(te.name))
235
+ continue;
236
+ const fullPath = (0, path_1.join)(typeDirPath, te.name);
237
+ const { hash, mtime } = await fileHash(fullPath);
238
+ const title = await extractTitle(fullPath);
239
+ const slug = te.name.replace('.md', '');
240
+ entities.push({
241
+ id: `SPEC:${typeDir}/${slug}`,
242
+ type: 'spec',
243
+ title: title || slug,
244
+ file: `020-specs/${typeDir}/${te.name}`,
245
+ hash,
246
+ mtime,
247
+ tags: [`${typeDir}-spec`],
248
+ });
249
+ // 关联到对应的需求文件(同名匹配)
250
+ const reqPath = (0, path_1.join)('010-requirements', typeDir, te.name);
251
+ const matchedReq = entities.find(e => e.file === reqPath);
252
+ if (matchedReq) {
253
+ relations.push({
254
+ from: `SPEC:${typeDir}/${slug}`,
255
+ to: matchedReq.id,
256
+ type: 'references',
257
+ });
258
+ }
259
+ }
260
+ }
182
261
  return { entities, relations };
183
262
  }
184
263
  // ═══════════════════════════════════════════════
@@ -232,7 +311,34 @@ async function scanTasks(iterDir) {
232
311
  status = 'in_progress';
233
312
  }
234
313
  }
235
- const { hash, mtime } = taskMdPath ? await fileHash(taskMdPath) : { hash: '', mtime: '' };
314
+ // 回退:从 _shared/REQ.md 提取标题和状态
315
+ if (title === taskId) {
316
+ const reqMdPath = (0, path_1.join)(taskPath, '_shared', 'REQ.md');
317
+ if (await (0, fs_extra_1.pathExists)(reqMdPath)) {
318
+ const reqContent = await (0, fs_extra_1.readFile)(reqMdPath, 'utf-8');
319
+ const reqTitleMatch = reqContent.match(/^#\s+(.+)/m);
320
+ if (reqTitleMatch)
321
+ title = reqTitleMatch[1].trim().slice(0, 80);
322
+ }
323
+ }
324
+ // hash/mtime 回退链:TASK.md → REQ.md → .meta/status
325
+ let hash = '', mtime = '';
326
+ if (taskMdPath) {
327
+ const h = await fileHash(taskMdPath);
328
+ hash = h.hash;
329
+ mtime = h.mtime;
330
+ }
331
+ else {
332
+ const fallbackPaths = [(0, path_1.join)(taskPath, '_shared', 'REQ.md'), (0, path_1.join)(taskPath, '.meta', 'status')];
333
+ for (const fp of fallbackPaths) {
334
+ if (await (0, fs_extra_1.pathExists)(fp)) {
335
+ const h = await fileHash(fp);
336
+ hash = h.hash;
337
+ mtime = h.mtime;
338
+ break;
339
+ }
340
+ }
341
+ }
236
342
  entities.push({
237
343
  id: taskId,
238
344
  type: 'task',
@@ -253,27 +359,43 @@ async function scanTasks(iterDir) {
253
359
  if ((0, task_utils_1.isTimestampBackup)(de.name))
254
360
  continue;
255
361
  const platformTaskMd = (0, path_1.join)(taskPath, de.name, 'TASK.md');
256
- if (!(await (0, fs_extra_1.pathExists)(platformTaskMd)))
257
- continue;
258
- const subContent = await (0, fs_extra_1.readFile)(platformTaskMd, 'utf-8');
259
- let subTitle = `${title} - ${de.name}`;
362
+ let subTitle = `${title} — ${de.name}`;
260
363
  let subStatus = 'pending';
261
364
  let subTaskId = `${taskId}-${de.name}`;
262
- const titleMatch = subContent.match(/^#\s+(.+)/m);
263
- if (titleMatch)
264
- subTitle = titleMatch[1].trim().slice(0, 80);
265
- const statusMatch = subContent.match(/\*\*状态\*\*[:\s]*(.+)/);
266
- if (statusMatch) {
267
- const raw = statusMatch[1].trim();
268
- if (raw.includes('已完成') || raw.includes('completed'))
269
- subStatus = 'completed';
270
- else if (raw.includes('进行中') || raw.includes('in_progress'))
271
- subStatus = 'in_progress';
365
+ if (await (0, fs_extra_1.pathExists)(platformTaskMd)) {
366
+ const subContent = await (0, fs_extra_1.readFile)(platformTaskMd, 'utf-8');
367
+ const titleMatch = subContent.match(/^#\s+(.+)/m);
368
+ if (titleMatch)
369
+ subTitle = titleMatch[1].trim().slice(0, 80);
370
+ const statusMatch = subContent.match(/\*\*状态\*\*[:\s]*(.+)/);
371
+ if (statusMatch) {
372
+ const raw = statusMatch[1].trim();
373
+ if (raw.includes('已完成') || raw.includes('completed'))
374
+ subStatus = 'completed';
375
+ else if (raw.includes('进行中') || raw.includes('in_progress'))
376
+ subStatus = 'in_progress';
377
+ }
378
+ const subIdMatch = subContent.match(/子任务 ID\*\*[:\s]*`(Task-[^`]+)`/);
379
+ if (subIdMatch)
380
+ subTaskId = subIdMatch[1];
381
+ }
382
+ // hash 回退:TASK.md → src/ 目录文件 → 空
383
+ let subHash = { hash: '', mtime: '' };
384
+ if (await (0, fs_extra_1.pathExists)(platformTaskMd)) {
385
+ subHash = await fileHash(platformTaskMd);
386
+ }
387
+ else {
388
+ const srcDir = (0, path_1.join)(taskPath, de.name, 'src');
389
+ if (await (0, fs_extra_1.pathExists)(srcDir)) {
390
+ try {
391
+ const srcFiles = await (0, fs_extra_1.readdir)(srcDir);
392
+ if (srcFiles.length > 0) {
393
+ subHash = await fileHash((0, path_1.join)(srcDir, srcFiles[0]));
394
+ }
395
+ }
396
+ catch { /* 忽略 */ }
397
+ }
272
398
  }
273
- const subIdMatch = subContent.match(/子任务 ID\*\*[:\s]*`(Task-[^`]+)`/);
274
- if (subIdMatch)
275
- subTaskId = subIdMatch[1];
276
- const subHash = await fileHash(platformTaskMd);
277
399
  entities.push({
278
400
  id: subTaskId,
279
401
  type: 'subtask',
@@ -331,6 +453,240 @@ async function scanUserFiles(iterDir) {
331
453
  return entities;
332
454
  }
333
455
  // ═══════════════════════════════════════════════
456
+ // 源码文件扫描(从 code-index 缓存读取)
457
+ // ═══════════════════════════════════════════════
458
+ async function scanSourceFiles(cwd) {
459
+ const entities = [];
460
+ const relations = [];
461
+ // 从 code-index 缓存读取
462
+ const indexPath = (0, path_1.join)(cwd, '.speccore', 'cache', 'code-structure.json');
463
+ if (!(await (0, fs_extra_1.pathExists)(indexPath)))
464
+ return { entities, relations };
465
+ try {
466
+ const index = JSON.parse(await (0, fs_extra_1.readFile)(indexPath, 'utf-8'));
467
+ const files = index.files || [];
468
+ // 只取核心文件(每个模块 top 3)
469
+ const moduleGroups = new Map();
470
+ for (const f of files) {
471
+ const key = `${f.endpoint || 'common'}:${f.module || 'root'}`;
472
+ if (!moduleGroups.has(key))
473
+ moduleGroups.set(key, []);
474
+ moduleGroups.get(key).push(f);
475
+ }
476
+ const coreFiles = [];
477
+ for (const group of moduleGroups.values()) {
478
+ group.sort((a, b) => (b.exports?.length || 0) - (a.exports?.length || 0));
479
+ coreFiles.push(...group.slice(0, 3));
480
+ }
481
+ // 限制总数,避免图谱过大
482
+ const maxFiles = Math.min(coreFiles.length, 50);
483
+ const selectedFiles = coreFiles.slice(0, maxFiles);
484
+ for (const f of selectedFiles) {
485
+ const id = `SRC:${f.path.replace(/\//g, '-').replace(/\.[^.]+$/, '')}`;
486
+ entities.push({
487
+ id,
488
+ type: 'source-file',
489
+ title: f.path.split('/').pop() || f.path,
490
+ file: f.path,
491
+ hash: '',
492
+ mtime: new Date(f.lastModified || 0).toISOString(),
493
+ endpoint: f.endpoint || 'common',
494
+ module: f.module || 'root',
495
+ language: f.language || 'unknown',
496
+ exports: (f.exports || []).slice(0, 10),
497
+ imports: (f.imports || []).slice(0, 15),
498
+ apis: f.apis || [],
499
+ tags: ['source', f.endpoint || 'common', f.module || 'root'],
500
+ });
501
+ }
502
+ // 构建 import 关系
503
+ const entityMap = new Map(entities.map(e => [e.id, e]));
504
+ for (const f of selectedFiles) {
505
+ const fromId = `SRC:${f.path.replace(/\//g, '-').replace(/\.[^.]+$/, '')}`;
506
+ for (const imp of (f.imports || [])) {
507
+ // 查找被 import 的文件
508
+ const impBasename = imp.replace(/\.[^.]+$/, '').replace(/.*[\/\\]/, '');
509
+ for (const other of selectedFiles) {
510
+ if (other.path === f.path)
511
+ continue;
512
+ const otherBasename = other.path.replace(/\.[^.]+$/, '').replace(/.*[\/\\]/, '');
513
+ if (imp.includes(otherBasename) || imp.includes(other.path)) {
514
+ const toId = `SRC:${other.path.replace(/\//g, '-').replace(/\.[^.]+$/, '')}`;
515
+ if (entityMap.has(toId) && !relations.some(r => r.from === fromId && r.to === toId && r.type === 'imports')) {
516
+ relations.push({ from: fromId, to: toId, type: 'imports' });
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ // 构建模块依赖关系
523
+ const moduleDeps = new Map();
524
+ for (const f of selectedFiles) {
525
+ const fromMod = `${f.endpoint || 'common'}:${f.module || 'root'}`;
526
+ for (const imp of (f.imports || [])) {
527
+ for (const other of selectedFiles) {
528
+ if (other.path === f.path)
529
+ continue;
530
+ const otherBasename = other.path.replace(/\.[^.]+$/, '').replace(/.*[\/\\]/, '');
531
+ if (imp.includes(otherBasename)) {
532
+ const toMod = `${other.endpoint || 'common'}:${other.module || 'root'}`;
533
+ if (toMod !== fromMod) {
534
+ if (!moduleDeps.has(fromMod))
535
+ moduleDeps.set(fromMod, new Set());
536
+ moduleDeps.get(fromMod).add(toMod);
537
+ }
538
+ }
539
+ }
540
+ }
541
+ }
542
+ // 添加模块依赖关系(用模块名作为虚拟节点)
543
+ for (const [fromMod, toMods] of moduleDeps) {
544
+ for (const toMod of toMods) {
545
+ if (!relations.some(r => r.from === fromMod && r.to === toMod && r.type === 'module_depends')) {
546
+ relations.push({ from: fromMod, to: toMod, type: 'module_depends' });
547
+ }
548
+ }
549
+ }
550
+ // 构建 Git 共变关系
551
+ const correlations = index.correlations || [];
552
+ for (const corr of correlations) {
553
+ const filesInCorr = corr.files || [];
554
+ for (let i = 0; i < filesInCorr.length; i++) {
555
+ for (let j = i + 1; j < filesInCorr.length; j++) {
556
+ const fromId = `SRC:${filesInCorr[i].replace(/\//g, '-').replace(/\.[^.]+$/, '')}`;
557
+ const toId = `SRC:${filesInCorr[j].replace(/\//g, '-').replace(/\.[^.]+$/, '')}`;
558
+ if (entityMap.has(fromId) && entityMap.has(toId)) {
559
+ if (!relations.some(r => r.from === fromId && r.to === toId && r.type === 'co_changes')) {
560
+ relations.push({ from: fromId, to: toId, type: 'co_changes' });
561
+ }
562
+ }
563
+ }
564
+ }
565
+ }
566
+ }
567
+ catch {
568
+ // 索引不存在或解析失败,跳过
569
+ }
570
+ return { entities, relations };
571
+ }
572
+ // ═══════════════════════════════════════════════
573
+ // 全局层扫描(.speccore/GLOBAL/)
574
+ // ═══════════════════════════════════════════════
575
+ async function scanGlobalDocs(cwd) {
576
+ const entities = [];
577
+ const relations = [];
578
+ const globalDir = (0, path_1.join)(cwd, '.speccore', 'GLOBAL');
579
+ if (!(await (0, fs_extra_1.pathExists)(globalDir)))
580
+ return { entities, relations };
581
+ const scanDir = async (dir, prefix) => {
582
+ const items = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
583
+ for (const item of items) {
584
+ if (item.name.startsWith('.') || (0, task_utils_1.isTimestampBackup)(item.name))
585
+ continue;
586
+ const fullPath = (0, path_1.join)(dir, item.name);
587
+ const relPath = `${prefix}${item.name}`;
588
+ if (item.isDirectory()) {
589
+ if (item.name === '_template' || item.name === 'RULES')
590
+ continue;
591
+ await scanDir(fullPath, `${relPath}/`);
592
+ }
593
+ else if (item.name.endsWith('.md') && item.name !== 'INDEX.md') {
594
+ const { hash, mtime } = await fileHash(fullPath);
595
+ const title = await extractTitle(fullPath);
596
+ const tags = ['global'];
597
+ if (prefix.includes('synthesis/'))
598
+ tags.push('synthesis');
599
+ else if (prefix.includes('platforms/')) {
600
+ tags.push('platform');
601
+ tags.push(prefix.split('platforms/')[1]?.split('/')[0] || '');
602
+ }
603
+ else if (prefix.includes('baselines/'))
604
+ tags.push('baseline');
605
+ else if (prefix.includes('projects/'))
606
+ tags.push('project');
607
+ entities.push({
608
+ id: `GLOBAL:${relPath.replace(/\.md$/, '').replace(/\//g, '-')}`,
609
+ type: 'global-doc',
610
+ title: title || item.name.replace('.md', ''),
611
+ file: `.speccore/GLOBAL/${relPath}`,
612
+ hash,
613
+ mtime,
614
+ tags,
615
+ });
616
+ }
617
+ }
618
+ };
619
+ await scanDir(globalDir, '');
620
+ return { entities, relations };
621
+ }
622
+ // ═══════════════════════════════════════════════
623
+ // 任务层详细规格扫描(Task-xxx/{platform}/REQ.md, TECH.md 等)
624
+ // ═══════════════════════════════════════════════
625
+ const TASK_SPEC_FILES = ['REQ.md', 'TECH.md', 'SCHEMA.md', 'CHANGELOG.md', 'TEST.md', 'TASK.md'];
626
+ async function scanTaskSpecs(iterDir) {
627
+ const entities = [];
628
+ const relations = [];
629
+ const tasksDir = (0, path_1.join)(iterDir, '030-tasks');
630
+ if (!(await (0, fs_extra_1.pathExists)(tasksDir)))
631
+ return { entities, relations };
632
+ const findTaskDirs = async (dir) => {
633
+ const results = [];
634
+ try {
635
+ const entries = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
636
+ for (const e of entries) {
637
+ const p = (0, path_1.join)(dir, e.name);
638
+ if (e.isDirectory()) {
639
+ if (e.name.startsWith('Task-'))
640
+ results.push(p);
641
+ else if (!e.name.startsWith('.') && e.name !== 'node_modules') {
642
+ results.push(...await findTaskDirs(p));
643
+ }
644
+ }
645
+ }
646
+ }
647
+ catch { /* 跳过 */ }
648
+ return results;
649
+ };
650
+ const taskDirs = await findTaskDirs(tasksDir);
651
+ for (const taskDir of taskDirs) {
652
+ const taskId = taskDir.split('/').pop() || '';
653
+ const subDirs = ['_shared', '00-specs'];
654
+ try {
655
+ const entries = await (0, fs_extra_1.readdir)(taskDir, { withFileTypes: true });
656
+ for (const e of entries) {
657
+ if (e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('0') && e.name !== '_shared' && e.name !== '99-artifacts' && !(0, task_utils_1.isTimestampBackup)(e.name)) {
658
+ subDirs.push(e.name);
659
+ }
660
+ }
661
+ }
662
+ catch { /* 跳过 */ }
663
+ for (const subDir of subDirs) {
664
+ const specDir = (0, path_1.join)(taskDir, subDir);
665
+ if (!(await (0, fs_extra_1.pathExists)(specDir)))
666
+ continue;
667
+ for (const specFile of TASK_SPEC_FILES) {
668
+ const specPath = (0, path_1.join)(specDir, specFile);
669
+ if (!(await (0, fs_extra_1.pathExists)(specPath)))
670
+ continue;
671
+ const { hash, mtime } = await fileHash(specPath);
672
+ const title = await extractTitle(specPath);
673
+ const specType = specFile.replace('.md', '').toLowerCase();
674
+ entities.push({
675
+ id: `TSPEC:${taskId}-${subDir}-${specType}`,
676
+ type: 'task-spec',
677
+ title: title || `${taskId}/${subDir}/${specFile}`,
678
+ file: `030-tasks/**/${taskId}/${subDir}/${specFile}`,
679
+ hash,
680
+ mtime,
681
+ tags: ['task-spec', specType, subDir === '_shared' ? 'shared' : subDir],
682
+ });
683
+ relations.push({ from: `TSPEC:${taskId}-${subDir}-${specType}`, to: taskId, type: 'elaborates' });
684
+ }
685
+ }
686
+ }
687
+ return { entities, relations };
688
+ }
689
+ // ═══════════════════════════════════════════════
334
690
  // 关联推断
335
691
  // ═══════════════════════════════════════════════
336
692
  async function inferRelations(entities, iterDir) {
@@ -351,6 +707,26 @@ async function inferRelations(entities, iterDir) {
351
707
  }
352
708
  }
353
709
  }
710
+ // 需求 → 任务:内容关键词匹配(当编号匹配失败时回退)
711
+ // 从需求标题中提取 ≥3 字符的特征关键词,匹配任务标题
712
+ const matchedPairs = new Set(relations.map(r => `${r.from}:${r.to}`));
713
+ for (const req of reqs) {
714
+ const reqKeywords = req.title
715
+ .split(/[-_\s]+/)
716
+ .filter(w => w.length >= 3);
717
+ if (reqKeywords.length === 0)
718
+ continue;
719
+ for (const task of tasks) {
720
+ const pairKey = `${task.id}:${req.id}`;
721
+ if (matchedPairs.has(pairKey))
722
+ continue;
723
+ const hasMatch = reqKeywords.some(kw => task.title.includes(kw));
724
+ if (hasMatch) {
725
+ relations.push({ from: task.id, to: req.id, type: 'implements' });
726
+ matchedPairs.add(pairKey);
727
+ }
728
+ }
729
+ }
354
730
  // 规格 → 需求:读取 spec 文件内容,提取 REQ-xxx 引用
355
731
  for (const spec of specs) {
356
732
  const specPath = (0, path_1.join)(iterDir, spec.file);
@@ -367,6 +743,60 @@ async function inferRelations(entities, iterDir) {
367
743
  }
368
744
  catch { /* 文件不存在或无法读取,跳过 */ }
369
745
  }
746
+ // 任务 → 需求 / 任务 → 规格:读取任务目录下的 REQ.md 和 TECH.md
747
+ for (const task of tasks) {
748
+ const taskAbsDir = (0, path_1.join)(iterDir, task.file);
749
+ const reqPaths = [
750
+ (0, path_1.join)(taskAbsDir, '_shared', 'REQ.md'),
751
+ (0, path_1.join)(taskAbsDir, '00-specs', 'REQ.md'),
752
+ (0, path_1.join)(taskAbsDir, 'REQ.md'),
753
+ ];
754
+ for (const reqPath of reqPaths) {
755
+ if (!(await (0, fs_extra_1.pathExists)(reqPath)))
756
+ continue;
757
+ try {
758
+ const content = await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
759
+ // 提取 REQ-xxx 引用 → task implements req
760
+ const reqRefs = content.match(/REQ-\d+/g);
761
+ if (reqRefs) {
762
+ for (const ref of [...new Set(reqRefs)]) {
763
+ if (entityMap.has(ref) && !relations.some(r => r.from === task.id && r.to === ref && r.type === 'implements')) {
764
+ relations.push({ from: task.id, to: ref, type: 'implements' });
765
+ }
766
+ }
767
+ }
768
+ // 提取 SPEC:xxx 引用 → task references spec
769
+ const specRefs = content.match(/SPEC:[\w\/\-]+/g);
770
+ if (specRefs) {
771
+ for (const ref of [...new Set(specRefs)]) {
772
+ if (entityMap.has(ref) && !relations.some(r => r.from === task.id && r.to === ref && r.type === 'references')) {
773
+ relations.push({ from: task.id, to: ref, type: 'references' });
774
+ }
775
+ }
776
+ }
777
+ }
778
+ catch { /* 跳过 */ }
779
+ break; // 只读第一个存在的
780
+ }
781
+ // 读取 API_CONTRACT.yaml 解析依赖
782
+ const contractPath = (0, path_1.join)(taskAbsDir, '_shared', 'API_CONTRACT.yaml');
783
+ if (await (0, fs_extra_1.pathExists)(contractPath)) {
784
+ try {
785
+ const content = await (0, fs_extra_1.readFile)(contractPath, 'utf-8');
786
+ // 提取依赖的任务引用: dependsOn: Task-xxx 或 # Depends on Task-xxx
787
+ const depRefs = content.match(/(?:dependsOn|depends on|依赖)\s*[::]\s*(Task-\S+)/gi);
788
+ if (depRefs) {
789
+ for (const raw of depRefs) {
790
+ const m = raw.match(/(Task-\S+)/i);
791
+ if (m && entityMap.has(m[1]) && !relations.some(r => r.from === task.id && r.to === m[1] && r.type === 'depends_on')) {
792
+ relations.push({ from: task.id, to: m[1], type: 'depends_on' });
793
+ }
794
+ }
795
+ }
796
+ }
797
+ catch { /* 跳过 */ }
798
+ }
799
+ }
370
800
  return relations;
371
801
  }
372
802
  // ═══════════════════════════════════════════════
@@ -381,7 +811,7 @@ async function buildKnowledgeGraph(cwd, iteration) {
381
811
  iteration: iterName,
382
812
  entities: {},
383
813
  relations: [],
384
- stats: { requirements: 0, specs: 0, tasks: 0, subtasks: 0, userFiles: 0, relations: 0 },
814
+ stats: { requirements: 0, specs: 0, tasks: 0, subtasks: 0, userFiles: 0, sourceFiles: 0, globalDocs: 0, taskSpecs: 0, relations: 0 },
385
815
  };
386
816
  if (!iterDir || !(await (0, fs_extra_1.pathExists)(iterDir)))
387
817
  return graph;
@@ -390,21 +820,42 @@ async function buildKnowledgeGraph(cwd, iteration) {
390
820
  const specResult = await scanSpecs(iterDir);
391
821
  const taskResult = await scanTasks(iterDir);
392
822
  const userFiles = await scanUserFiles(iterDir);
393
- // 合并实体
823
+ const sourceResult = await scanSourceFiles(cwd);
824
+ const globalResult = await scanGlobalDocs(cwd);
825
+ const taskSpecResult = await scanTaskSpecs(iterDir);
826
+ // 合并实体(处理 ID 冲突:同名实体用路径前缀去重)
394
827
  const allEntities = [
395
828
  ...reqResult.entities,
396
829
  ...specResult.entities,
397
830
  ...taskResult.entities,
398
831
  ...userFiles,
832
+ ...sourceResult.entities,
833
+ ...globalResult.entities,
834
+ ...taskSpecResult.entities,
399
835
  ];
836
+ const idRemap = new Map();
400
837
  for (const e of allEntities) {
838
+ let finalId = e.id;
839
+ if (graph.entities[e.id]) {
840
+ finalId = `${e.id}@${e.file.replace(/\//g, '-')}`;
841
+ idRemap.set(e.id, finalId);
842
+ }
843
+ e.id = finalId;
401
844
  graph.entities[e.id] = e;
402
845
  }
846
+ // 同步更新已有关系中的旧 ID
847
+ const remapId = (id) => idRemap.get(id) || id;
848
+ for (const r of [...reqResult.relations, ...specResult.relations, ...taskResult.relations, ...taskSpecResult.relations]) {
849
+ r.from = remapId(r.from);
850
+ r.to = remapId(r.to);
851
+ }
403
852
  // 合并关系
404
853
  graph.relations = [
405
854
  ...reqResult.relations,
406
855
  ...specResult.relations,
407
856
  ...taskResult.relations,
857
+ ...sourceResult.relations,
858
+ ...taskSpecResult.relations,
408
859
  ...(await inferRelations(allEntities, iterDir)),
409
860
  ];
410
861
  // 统计
@@ -414,6 +865,9 @@ async function buildKnowledgeGraph(cwd, iteration) {
414
865
  tasks: taskResult.entities.filter(e => e.type === 'task').length,
415
866
  subtasks: taskResult.entities.filter(e => e.type === 'subtask').length,
416
867
  userFiles: userFiles.length,
868
+ sourceFiles: sourceResult.entities.length,
869
+ globalDocs: globalResult.entities.length,
870
+ taskSpecs: taskSpecResult.entities.length,
417
871
  relations: graph.relations.length,
418
872
  };
419
873
  return graph;
@@ -427,13 +881,24 @@ async function saveKnowledgeGraph(cwd, graph) {
427
881
  return filePath;
428
882
  }
429
883
  /** 加载已存在的知识图谱 */
884
+ /**
885
+ * 加载知识图谱(带进程缓存)
886
+ * 文件 mtime 未变时直接返回缓存,避免重复 JSON 解析
887
+ */
430
888
  async function loadKnowledgeGraph(cwd) {
431
889
  const filePath = (0, path_1.join)(cwd, '.speccore', 'cache', 'knowledge-graph.json');
432
890
  if (!(await (0, fs_extra_1.pathExists)(filePath)))
433
891
  return null;
434
892
  try {
893
+ const st = await (0, fs_extra_1.stat)(filePath);
894
+ const cached = kgCache.get(filePath);
895
+ if (cached && cached.mtime >= st.mtimeMs) {
896
+ return cached.graph;
897
+ }
435
898
  const content = await (0, fs_extra_1.readFile)(filePath, 'utf-8');
436
- return JSON.parse(content);
899
+ const graph = JSON.parse(content);
900
+ kgCache.set(filePath, { graph, mtime: st.mtimeMs });
901
+ return graph;
437
902
  }
438
903
  catch {
439
904
  return null;
@@ -443,18 +908,38 @@ async function loadKnowledgeGraph(cwd) {
443
908
  function getTaskContext(graph, taskId) {
444
909
  const entity = graph.entities[taskId];
445
910
  if (!entity) {
446
- return { requirement: null, siblingSubtasks: [], parentTask: null };
911
+ return { requirement: null, siblingSubtasks: [], parentTask: null, relatedSpecs: [], dependsOn: [] };
447
912
  }
448
913
  let requirement = null;
449
914
  let parentTask = null;
450
- const siblingSubtasks = [];
451
- // 找上游需求(直接 implements 或通过父任务间接关联)
452
- for (const rel of graph.relations) {
453
- if (rel.from === taskId && rel.type === 'implements') {
454
- requirement = graph.entities[rel.to] || null;
915
+ const relatedSpecs = [];
916
+ const dependsOn = [];
917
+ // 预建索引:避免多次遍历全量关系
918
+ const parentIndex = new Map();
919
+ for (const e of Object.values(graph.entities)) {
920
+ if (e.type === 'subtask' && e.parentTaskId) {
921
+ const list = parentIndex.get(e.parentTaskId) || [];
922
+ list.push(e);
923
+ parentIndex.set(e.parentTaskId, list);
455
924
  }
456
- if (rel.from === taskId && rel.type === 'subtask_of') {
457
- parentTask = graph.entities[rel.to] || null;
925
+ }
926
+ // 找上游需求、关联规格、依赖任务
927
+ for (const rel of graph.relations) {
928
+ if (rel.from === taskId) {
929
+ if (rel.type === 'implements')
930
+ requirement = graph.entities[rel.to] || null;
931
+ if (rel.type === 'references') {
932
+ const spec = graph.entities[rel.to];
933
+ if (spec)
934
+ relatedSpecs.push(spec);
935
+ }
936
+ if (rel.type === 'depends_on') {
937
+ const dep = graph.entities[rel.to];
938
+ if (dep)
939
+ dependsOn.push(dep);
940
+ }
941
+ if (rel.type === 'subtask_of')
942
+ parentTask = graph.entities[rel.to] || null;
458
943
  }
459
944
  }
460
945
  // 子任务没有直接 implements 关系 → 通过父任务找上游需求
@@ -466,22 +951,25 @@ function getTaskContext(graph, taskId) {
466
951
  }
467
952
  }
468
953
  }
469
- // 如果是子任务,找兄弟子任务
954
+ // 如果是子任务,找兄弟子任务(用索引,O(1))
955
+ const siblingSubtasks = [];
470
956
  const parentId = entity.parentTaskId || parentTask?.id;
471
957
  if (parentId) {
472
- for (const e of Object.values(graph.entities)) {
473
- if (e.type === 'subtask' && e.parentTaskId === parentId) {
474
- siblingSubtasks.push(e);
475
- }
476
- }
958
+ const siblings = parentIndex.get(parentId);
959
+ if (siblings)
960
+ siblingSubtasks.push(...siblings);
477
961
  }
478
- return { requirement, siblingSubtasks, parentTask };
962
+ return { requirement, siblingSubtasks, parentTask, relatedSpecs, dependsOn };
479
963
  }
480
964
  // ═══════════════════════════════════════════════
481
965
  // 自动更新机制
482
966
  // ═══════════════════════════════════════════════
483
- /** 递归获取目录下所有文件的最新 mtime */
484
- async function getLatestMtime(dir) {
967
+ /**
968
+ * 递归获取目录下所有文件的最新 mtime
969
+ * 限制:每个目录最多检查 MAX_FILES_PER_DIR 个文件,避免大型项目阻塞
970
+ */
971
+ const MAX_FILES_PER_DIR = 100;
972
+ async function getLatestMtime(dir, fileCount = { value: 0 }) {
485
973
  if (!(await (0, fs_extra_1.pathExists)(dir)))
486
974
  return 0;
487
975
  let latest = 0;
@@ -489,23 +977,26 @@ async function getLatestMtime(dir) {
489
977
  for (const item of items) {
490
978
  if (item.name.startsWith('.') || (0, task_utils_1.isTimestampBackup)(item.name))
491
979
  continue;
980
+ if (fileCount.value >= MAX_FILES_PER_DIR)
981
+ break;
492
982
  const fullPath = (0, path_1.join)(dir, item.name);
493
983
  const st = await (0, fs_extra_1.stat)(fullPath);
494
984
  if (item.isDirectory()) {
495
985
  if (item.name === 'node_modules' || item.name === 'cache')
496
986
  continue;
497
- const subLatest = await getLatestMtime(fullPath);
987
+ const subLatest = await getLatestMtime(fullPath, fileCount);
498
988
  if (subLatest > latest)
499
989
  latest = subLatest;
500
990
  }
501
991
  else {
992
+ fileCount.value++;
502
993
  if (st.mtimeMs > latest)
503
994
  latest = st.mtimeMs;
504
995
  }
505
996
  }
506
997
  return latest;
507
998
  }
508
- /** 检查知识图谱是否已过期 */
999
+ /** 检查知识图谱是否已过期(带 500ms 超时保护) */
509
1000
  async function isGraphStale(cwd, iteration) {
510
1001
  const graph = await loadKnowledgeGraph(cwd);
511
1002
  if (!graph)
@@ -522,7 +1013,13 @@ async function isGraphStale(cwd, iteration) {
522
1013
  (0, path_1.join)(iterDir, '030-tasks'),
523
1014
  ];
524
1015
  for (const dir of dirsToCheck) {
1016
+ // 超时保护:单目录扫描超过 200ms 就放弃
1017
+ const start = Date.now();
525
1018
  const latestMtime = await getLatestMtime(dir);
1019
+ if (Date.now() - start > 200) {
1020
+ logger_1.logger.debug(`isGraphStale: ${dir} 扫描超时,假设未过期`);
1021
+ continue;
1022
+ }
526
1023
  if (latestMtime > graphTime)
527
1024
  return true;
528
1025
  }