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.
- package/.agents/skills/spec-ask/SKILL.md +24 -4
- package/README.md +10 -0
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/about.d.ts.map +1 -1
- package/dist/commands/about.js +10 -3
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/analyze.d.ts +3 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +178 -11
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/change.d.ts.map +1 -1
- package/dist/commands/change.js +16 -4
- package/dist/commands/change.js.map +1 -1
- package/dist/commands/doc2spec.d.ts +1 -0
- package/dist/commands/doc2spec.d.ts.map +1 -1
- package/dist/commands/doc2spec.js +175 -0
- package/dist/commands/doc2spec.js.map +1 -1
- package/dist/commands/done.js +25 -5
- package/dist/commands/done.js.map +1 -1
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +20 -6
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/help.js +32 -5
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/iteration/create.d.ts.map +1 -1
- package/dist/commands/iteration/create.js +40 -15
- package/dist/commands/iteration/create.js.map +1 -1
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +309 -36
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/knowledge.d.ts +6 -0
- package/dist/commands/knowledge.d.ts.map +1 -0
- package/dist/commands/knowledge.js +94 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/rag-index.d.ts +19 -0
- package/dist/commands/rag-index.d.ts.map +1 -0
- package/dist/commands/rag-index.js +272 -0
- package/dist/commands/rag-index.js.map +1 -0
- package/dist/commands/refresh.d.ts +21 -0
- package/dist/commands/refresh.d.ts.map +1 -0
- package/dist/commands/refresh.js +185 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/commands/sync-global.d.ts.map +1 -1
- package/dist/commands/sync-global.js +59 -0
- package/dist/commands/sync-global.js.map +1 -1
- package/dist/core/ai-context-generator.d.ts +2 -0
- package/dist/core/ai-context-generator.d.ts.map +1 -1
- package/dist/core/ai-context-generator.js +8 -6
- package/dist/core/ai-context-generator.js.map +1 -1
- package/dist/core/analyze-engine.d.ts +19 -0
- package/dist/core/analyze-engine.d.ts.map +1 -1
- package/dist/core/analyze-engine.js +326 -9
- package/dist/core/analyze-engine.js.map +1 -1
- package/dist/core/ask-engine.d.ts.map +1 -1
- package/dist/core/ask-engine.js +134 -4
- package/dist/core/ask-engine.js.map +1 -1
- package/dist/core/ask-host-ai.d.ts.map +1 -1
- package/dist/core/ask-host-ai.js +9 -6
- package/dist/core/ask-host-ai.js.map +1 -1
- package/dist/core/code-scanner.d.ts +24 -2
- package/dist/core/code-scanner.d.ts.map +1 -1
- package/dist/core/code-scanner.js +310 -9
- package/dist/core/code-scanner.js.map +1 -1
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +28 -9
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/decay-detector.d.ts +2 -2
- package/dist/core/decay-detector.d.ts.map +1 -1
- package/dist/core/decay-detector.js +135 -6
- package/dist/core/decay-detector.js.map +1 -1
- package/dist/core/git-integration.d.ts +50 -2
- package/dist/core/git-integration.d.ts.map +1 -1
- package/dist/core/git-integration.js +217 -27
- package/dist/core/git-integration.js.map +1 -1
- package/dist/core/global-knowledge.d.ts +25 -0
- package/dist/core/global-knowledge.d.ts.map +1 -0
- package/dist/core/global-knowledge.js +228 -0
- package/dist/core/global-knowledge.js.map +1 -0
- package/dist/core/index-guard.d.ts +57 -0
- package/dist/core/index-guard.d.ts.map +1 -0
- package/dist/core/index-guard.js +202 -0
- package/dist/core/index-guard.js.map +1 -0
- package/dist/core/intent-cache.d.ts +3 -1
- package/dist/core/intent-cache.d.ts.map +1 -1
- package/dist/core/intent-cache.js +33 -3
- package/dist/core/intent-cache.js.map +1 -1
- package/dist/core/intent-recognition.d.ts.map +1 -1
- package/dist/core/intent-recognition.js +21 -2
- package/dist/core/intent-recognition.js.map +1 -1
- package/dist/core/knowledge-graph.d.ts +18 -3
- package/dist/core/knowledge-graph.d.ts.map +1 -1
- package/dist/core/knowledge-graph.js +539 -42
- package/dist/core/knowledge-graph.js.map +1 -1
- package/dist/core/knowledge-visualizer.d.ts +19 -0
- package/dist/core/knowledge-visualizer.d.ts.map +1 -0
- package/dist/core/knowledge-visualizer.js +528 -0
- package/dist/core/knowledge-visualizer.js.map +1 -0
- package/dist/core/next-steps.js +6 -0
- package/dist/core/next-steps.js.map +1 -1
- package/dist/core/platform-registry.d.ts +5 -0
- package/dist/core/platform-registry.d.ts.map +1 -1
- package/dist/core/platform-registry.js +52 -0
- package/dist/core/platform-registry.js.map +1 -1
- package/dist/core/prompt-builder.d.ts +3 -2
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +361 -72
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/rag-engine.d.ts +152 -0
- package/dist/core/rag-engine.d.ts.map +1 -0
- package/dist/core/rag-engine.js +816 -0
- package/dist/core/rag-engine.js.map +1 -0
- package/dist/core/spec-merger.d.ts +133 -0
- package/dist/core/spec-merger.d.ts.map +1 -0
- package/dist/core/spec-merger.js +735 -0
- package/dist/core/spec-merger.js.map +1 -0
- package/dist/core/unified-retrieval.d.ts +141 -0
- package/dist/core/unified-retrieval.d.ts.map +1 -0
- package/dist/core/unified-retrieval.js +471 -0
- package/dist/core/unified-retrieval.js.map +1 -0
- 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: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
/**
|
|
484
|
-
|
|
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
|
}
|