speccore 6.5.1 → 6.10.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 +6 -0
- package/.agents/skills/spec-reindex/SKILL.md +40 -0
- package/.agents/skills/speccore-router/SKILL.md +9 -0
- package/dist/cli.js +9 -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 +2 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +141 -0
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/change.d.ts.map +1 -1
- package/dist/commands/change.js +3 -0
- 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/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 +38 -10
- 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 +125 -9
- package/dist/commands/iteration/split.js.map +1 -1
- 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.js +41 -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 +113 -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 +235 -5
- 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 +29 -7
- 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 +149 -8
- 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/knowledge-graph.d.ts +7 -1
- package/dist/core/knowledge-graph.d.ts.map +1 -1
- package/dist/core/knowledge-graph.js +265 -32
- package/dist/core/knowledge-graph.js.map +1 -1
- package/dist/core/platform-registry.d.ts +7 -2
- package/dist/core/platform-registry.d.ts.map +1 -1
- package/dist/core/platform-registry.js +57 -4
- 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 +231 -71
- 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/reindex-engine.js +5 -5
- package/dist/core/reindex-engine.js.map +1 -1
- package/dist/core/task-paths.d.ts +2 -2
- package/dist/core/task-paths.d.ts.map +1 -1
- package/dist/core/task-paths.js +6 -4
- package/dist/core/task-paths.js.map +1 -1
- package/dist/core/unified-retrieval.d.ts +140 -0
- package/dist/core/unified-retrieval.d.ts.map +1 -0
- package/dist/core/unified-retrieval.js +466 -0
- package/dist/core/unified-retrieval.js.map +1 -0
- package/package.json +1 -1
|
@@ -20,9 +20,25 @@ const logger_1 = require("../utils/logger");
|
|
|
20
20
|
// 常量
|
|
21
21
|
// ═══════════════════════════════════════════════════════════
|
|
22
22
|
const CACHE_PATH = '.speccore/local/intent-cache.json';
|
|
23
|
-
const CACHE_VERSION = '1.
|
|
23
|
+
const CACHE_VERSION = '1.1'; // 升级到 1.1 支持 normalizedInput
|
|
24
24
|
const MAX_ENTRIES = 200;
|
|
25
25
|
const FUZZY_THRESHOLD = 2; // 编辑距离阈值
|
|
26
|
+
/** 停用词表 */
|
|
27
|
+
const NORMALIZE_STOP_WORDS = new Set([
|
|
28
|
+
'的', '与', '和', '及', '在', '中', '对', '为', '是', '有', '从', '到',
|
|
29
|
+
'the', 'a', 'an', 'and', 'or', 'to', 'of', 'in', 'for', 'on', 'with', 'by',
|
|
30
|
+
'请', '帮我', '给我', '一下', '一个', '这个', '那个',
|
|
31
|
+
]);
|
|
32
|
+
/** 将输入归一化为语义键(去停用词 + 排序) */
|
|
33
|
+
function normalizeInput(input) {
|
|
34
|
+
const tokens = input
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^\u4e00-\u9fa5a-z0-9\s]/g, ' ')
|
|
37
|
+
.split(/\s+/)
|
|
38
|
+
.filter(t => t.length >= 2 && !NORMALIZE_STOP_WORDS.has(t))
|
|
39
|
+
.sort();
|
|
40
|
+
return tokens.slice(0, 6).join('|'); // 取前6个关键词,用 | 分隔
|
|
41
|
+
}
|
|
26
42
|
// ═══════════════════════════════════════════════════════════
|
|
27
43
|
// 缓存读写
|
|
28
44
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -54,7 +70,7 @@ async function saveCache(cache) {
|
|
|
54
70
|
// 公共 API
|
|
55
71
|
// ═══════════════════════════════════════════════════════════
|
|
56
72
|
/**
|
|
57
|
-
*
|
|
73
|
+
* 查询缓存:精确 → 归一化语义 → 编辑距离模糊
|
|
58
74
|
*/
|
|
59
75
|
async function getCachedIntent(input) {
|
|
60
76
|
const cache = await loadCache();
|
|
@@ -67,7 +83,20 @@ async function getCachedIntent(input) {
|
|
|
67
83
|
logger_1.logger.info(`💾 意图缓存命中(精确): "${input.slice(0, 30)}..." → ${exact.result.summary.slice(0, 30)}`);
|
|
68
84
|
return exact.result;
|
|
69
85
|
}
|
|
70
|
-
// 2.
|
|
86
|
+
// 2. 归一化语义匹配("分析一下登录" ≈ "分析登录功能")
|
|
87
|
+
const normalized = normalizeInput(input);
|
|
88
|
+
if (normalized) {
|
|
89
|
+
for (const entry of Object.values(cache.entries)) {
|
|
90
|
+
if (entry.normalizedInput === normalized) {
|
|
91
|
+
entry.hitCount++;
|
|
92
|
+
entry.lastUsed = new Date().toISOString();
|
|
93
|
+
await saveCache(cache);
|
|
94
|
+
logger_1.logger.info(`💾 意图缓存命中(语义): "${input.slice(0, 30)}..." ≈ "${entry.input.slice(0, 30)}..."`);
|
|
95
|
+
return entry.result;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 3. 模糊匹配(编辑距离 ≤ 2,且长度比例合理)
|
|
71
100
|
// 防止短字符串误匹配:如"补充测试"匹配"补充分析"(编辑距离 2 但语义完全不同)
|
|
72
101
|
for (const [cachedInput, entry] of Object.entries(cache.entries)) {
|
|
73
102
|
const dist = levenshtein(input, cachedInput);
|
|
@@ -92,6 +121,7 @@ async function cacheIntent(input, result, source = 'host-ai') {
|
|
|
92
121
|
const cache = await loadCache();
|
|
93
122
|
cache.entries[input] = {
|
|
94
123
|
input,
|
|
124
|
+
normalizedInput: normalizeInput(input),
|
|
95
125
|
result,
|
|
96
126
|
source,
|
|
97
127
|
hitCount: 1,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intent-cache.js","sourceRoot":"","sources":["../../src/core/intent-cache.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;
|
|
1
|
+
{"version":3,"file":"intent-cache.js","sourceRoot":"","sources":["../../src/core/intent-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -41,14 +41,20 @@ export declare function buildKnowledgeGraph(cwd: string, iteration?: string): Pr
|
|
|
41
41
|
/** 保存知识图谱到 cache 目录 */
|
|
42
42
|
export declare function saveKnowledgeGraph(cwd: string, graph: KnowledgeGraph): Promise<string>;
|
|
43
43
|
/** 加载已存在的知识图谱 */
|
|
44
|
+
/**
|
|
45
|
+
* 加载知识图谱(带进程缓存)
|
|
46
|
+
* 文件 mtime 未变时直接返回缓存,避免重复 JSON 解析
|
|
47
|
+
*/
|
|
44
48
|
export declare function loadKnowledgeGraph(cwd: string): Promise<KnowledgeGraph | null>;
|
|
45
49
|
/** 获取指定任务的关联链(上游需求 + 关联子任务) */
|
|
46
50
|
export declare function getTaskContext(graph: KnowledgeGraph, taskId: string): {
|
|
47
51
|
requirement: GraphEntity | null;
|
|
48
52
|
siblingSubtasks: GraphEntity[];
|
|
49
53
|
parentTask: GraphEntity | null;
|
|
54
|
+
relatedSpecs: GraphEntity[];
|
|
55
|
+
dependsOn: GraphEntity[];
|
|
50
56
|
};
|
|
51
|
-
/**
|
|
57
|
+
/** 检查知识图谱是否已过期(带 500ms 超时保护) */
|
|
52
58
|
export declare function isGraphStale(cwd: string, iteration?: string): Promise<boolean>;
|
|
53
59
|
/** 刷新知识图谱(重建 + 保存)—— 供命令完成后调用 */
|
|
54
60
|
export declare function refreshKnowledgeGraph(cwd: string, iteration?: string): Promise<KnowledgeGraph | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knowledge-graph.d.ts","sourceRoot":"","sources":["../../src/core/knowledge-graph.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"knowledge-graph.d.ts","sourceRoot":"","sources":["../../src/core/knowledge-graph.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyBH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACtC,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,YAAY,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,YAAY,CAAC;CAC/E;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAmeD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,cAAc,CAAC,CAkEzB;AAED,uBAAuB;AACvB,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM5F;AAED,iBAAiB;AACjB;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAkBpF;AAED,+BAA+B;AAC/B,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG;IACrE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,WAAW,EAAE,CAAC;IAC/B,UAAU,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,SAAS,EAAE,WAAW,EAAE,CAAC;CAC1B,CAwDA;AAiCD,gCAAgC;AAChC,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA6BpF;AAED,iCAAiC;AACjC,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAShC"}
|
|
@@ -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
|
// ═══════════════════════════════════════════════
|
|
@@ -104,11 +106,13 @@ async function scanRequirements(iterDir, iterName) {
|
|
|
104
106
|
if (item.isDirectory()) {
|
|
105
107
|
await scanDir(fullPath, `${relPath}/`);
|
|
106
108
|
}
|
|
107
|
-
else if (item.name.endsWith('.md') && item.name !== 'INDEX.md') {
|
|
109
|
+
else if (item.name.endsWith('.md') && item.name !== 'INDEX.md' && !(0, task_utils_1.isTimestampBackup)(item.name)) {
|
|
108
110
|
const { hash, mtime } = await fileHash(fullPath);
|
|
109
111
|
const title = await extractTitle(fullPath);
|
|
110
112
|
const content = await (0, fs_extra_1.readFile)(fullPath, 'utf-8').catch(() => '');
|
|
111
|
-
|
|
113
|
+
// 用路径前缀确保 ID 唯一性,避免不同文件引用同一 REQ-xxx 时覆盖
|
|
114
|
+
const pathPrefix = relPath.replace(/\.md$/, '').replace(/\//g, '-');
|
|
115
|
+
const reqId = extractReqId(content, pathPrefix);
|
|
112
116
|
entities.push({
|
|
113
117
|
id: reqId,
|
|
114
118
|
type: 'requirement',
|
|
@@ -116,7 +120,13 @@ async function scanRequirements(iterDir, iterName) {
|
|
|
116
120
|
file: `010-requirements/${relPath}`,
|
|
117
121
|
hash,
|
|
118
122
|
mtime,
|
|
119
|
-
tags: [
|
|
123
|
+
tags: [
|
|
124
|
+
prefix.includes('features/') ? 'feature'
|
|
125
|
+
: prefix.includes('bugs/') ? 'bug'
|
|
126
|
+
: prefix.includes('refactors/') ? 'refactor'
|
|
127
|
+
: prefix.includes('research/') ? 'research'
|
|
128
|
+
: 'requirement'
|
|
129
|
+
],
|
|
120
130
|
});
|
|
121
131
|
}
|
|
122
132
|
}
|
|
@@ -177,6 +187,74 @@ async function scanSpecs(iterDir) {
|
|
|
177
187
|
}
|
|
178
188
|
}
|
|
179
189
|
}
|
|
190
|
+
// 扫描 features/ 子目录(按功能模块的分析产出)
|
|
191
|
+
const featuresDir = (0, path_1.join)(specsDir, 'features');
|
|
192
|
+
if (await (0, fs_extra_1.pathExists)(featuresDir)) {
|
|
193
|
+
const featureEntries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
|
|
194
|
+
for (const fe of featureEntries) {
|
|
195
|
+
if (fe.name.startsWith('.') || (0, task_utils_1.isTimestampBackup)(fe.name))
|
|
196
|
+
continue;
|
|
197
|
+
if (!fe.name.endsWith('.md'))
|
|
198
|
+
continue;
|
|
199
|
+
const fullPath = (0, path_1.join)(featuresDir, fe.name);
|
|
200
|
+
const { hash, mtime } = await fileHash(fullPath);
|
|
201
|
+
const title = await extractTitle(fullPath);
|
|
202
|
+
const featureName = fe.name.replace('.md', '');
|
|
203
|
+
entities.push({
|
|
204
|
+
id: `SPEC:features/${featureName}`,
|
|
205
|
+
type: 'spec',
|
|
206
|
+
title: title || featureName,
|
|
207
|
+
file: `020-specs/features/${fe.name}`,
|
|
208
|
+
hash,
|
|
209
|
+
mtime,
|
|
210
|
+
tags: ['feature-spec'],
|
|
211
|
+
});
|
|
212
|
+
// 关联到对应的需求 feature(同名匹配)
|
|
213
|
+
const reqFeaturePath = (0, path_1.join)('010-requirements', 'features', featureName);
|
|
214
|
+
const matchedReq = entities.find(e => e.file?.startsWith(reqFeaturePath));
|
|
215
|
+
if (matchedReq) {
|
|
216
|
+
relations.push({
|
|
217
|
+
from: `SPEC:features/${featureName}`,
|
|
218
|
+
to: matchedReq.id,
|
|
219
|
+
type: 'references',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// 扫描类型目录(bugs/refactors/research — 扁平文件)
|
|
225
|
+
for (const typeDir of ['bugs', 'refactors', 'research']) {
|
|
226
|
+
const typeDirPath = (0, path_1.join)(specsDir, typeDir);
|
|
227
|
+
if (!(await (0, fs_extra_1.pathExists)(typeDirPath)))
|
|
228
|
+
continue;
|
|
229
|
+
const typeEntries = await (0, fs_extra_1.readdir)(typeDirPath, { withFileTypes: true });
|
|
230
|
+
for (const te of typeEntries) {
|
|
231
|
+
if (!te.isFile() || !te.name.endsWith('.md') || (0, task_utils_1.isTimestampBackup)(te.name))
|
|
232
|
+
continue;
|
|
233
|
+
const fullPath = (0, path_1.join)(typeDirPath, te.name);
|
|
234
|
+
const { hash, mtime } = await fileHash(fullPath);
|
|
235
|
+
const title = await extractTitle(fullPath);
|
|
236
|
+
const slug = te.name.replace('.md', '');
|
|
237
|
+
entities.push({
|
|
238
|
+
id: `SPEC:${typeDir}/${slug}`,
|
|
239
|
+
type: 'spec',
|
|
240
|
+
title: title || slug,
|
|
241
|
+
file: `020-specs/${typeDir}/${te.name}`,
|
|
242
|
+
hash,
|
|
243
|
+
mtime,
|
|
244
|
+
tags: [`${typeDir}-spec`],
|
|
245
|
+
});
|
|
246
|
+
// 关联到对应的需求文件(同名匹配)
|
|
247
|
+
const reqPath = (0, path_1.join)('010-requirements', typeDir, te.name);
|
|
248
|
+
const matchedReq = entities.find(e => e.file === reqPath);
|
|
249
|
+
if (matchedReq) {
|
|
250
|
+
relations.push({
|
|
251
|
+
from: `SPEC:${typeDir}/${slug}`,
|
|
252
|
+
to: matchedReq.id,
|
|
253
|
+
type: 'references',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
180
258
|
return { entities, relations };
|
|
181
259
|
}
|
|
182
260
|
// ═══════════════════════════════════════════════
|
|
@@ -248,6 +326,8 @@ async function scanTasks(iterDir) {
|
|
|
248
326
|
continue;
|
|
249
327
|
if (de.name.startsWith('.') || de.name.startsWith('0') || de.name === '_shared' || de.name === '99-artifacts')
|
|
250
328
|
continue;
|
|
329
|
+
if ((0, task_utils_1.isTimestampBackup)(de.name))
|
|
330
|
+
continue;
|
|
251
331
|
const platformTaskMd = (0, path_1.join)(taskPath, de.name, 'TASK.md');
|
|
252
332
|
if (!(await (0, fs_extra_1.pathExists)(platformTaskMd)))
|
|
253
333
|
continue;
|
|
@@ -329,11 +409,12 @@ async function scanUserFiles(iterDir) {
|
|
|
329
409
|
// ═══════════════════════════════════════════════
|
|
330
410
|
// 关联推断
|
|
331
411
|
// ═══════════════════════════════════════════════
|
|
332
|
-
function inferRelations(entities) {
|
|
412
|
+
async function inferRelations(entities, iterDir) {
|
|
333
413
|
const relations = [];
|
|
334
414
|
const reqs = entities.filter(e => e.type === 'requirement');
|
|
335
415
|
const tasks = entities.filter(e => e.type === 'task');
|
|
336
416
|
const specs = entities.filter(e => e.type === 'spec');
|
|
417
|
+
const entityMap = new Map(entities.map(e => [e.id, e]));
|
|
337
418
|
// 需求 → 任务:按编号匹配(REQ-001 → Task-001)
|
|
338
419
|
for (const req of reqs) {
|
|
339
420
|
const reqNum = req.id.match(/\d+/)?.[0];
|
|
@@ -346,8 +427,76 @@ function inferRelations(entities) {
|
|
|
346
427
|
}
|
|
347
428
|
}
|
|
348
429
|
}
|
|
349
|
-
// 规格 →
|
|
350
|
-
|
|
430
|
+
// 规格 → 需求:读取 spec 文件内容,提取 REQ-xxx 引用
|
|
431
|
+
for (const spec of specs) {
|
|
432
|
+
const specPath = (0, path_1.join)(iterDir, spec.file);
|
|
433
|
+
try {
|
|
434
|
+
const content = await (0, fs_extra_1.readFile)(specPath, 'utf-8');
|
|
435
|
+
const reqRefs = content.match(/REQ-\d+/g);
|
|
436
|
+
if (reqRefs) {
|
|
437
|
+
for (const ref of [...new Set(reqRefs)]) {
|
|
438
|
+
if (entityMap.has(ref)) {
|
|
439
|
+
relations.push({ from: spec.id, to: ref, type: 'specifies' });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch { /* 文件不存在或无法读取,跳过 */ }
|
|
445
|
+
}
|
|
446
|
+
// 任务 → 需求 / 任务 → 规格:读取任务目录下的 REQ.md 和 TECH.md
|
|
447
|
+
for (const task of tasks) {
|
|
448
|
+
const taskAbsDir = (0, path_1.join)(iterDir, task.file);
|
|
449
|
+
const reqPaths = [
|
|
450
|
+
(0, path_1.join)(taskAbsDir, '_shared', 'REQ.md'),
|
|
451
|
+
(0, path_1.join)(taskAbsDir, '00-specs', 'REQ.md'),
|
|
452
|
+
(0, path_1.join)(taskAbsDir, 'REQ.md'),
|
|
453
|
+
];
|
|
454
|
+
for (const reqPath of reqPaths) {
|
|
455
|
+
if (!(await (0, fs_extra_1.pathExists)(reqPath)))
|
|
456
|
+
continue;
|
|
457
|
+
try {
|
|
458
|
+
const content = await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
|
|
459
|
+
// 提取 REQ-xxx 引用 → task implements req
|
|
460
|
+
const reqRefs = content.match(/REQ-\d+/g);
|
|
461
|
+
if (reqRefs) {
|
|
462
|
+
for (const ref of [...new Set(reqRefs)]) {
|
|
463
|
+
if (entityMap.has(ref) && !relations.some(r => r.from === task.id && r.to === ref && r.type === 'implements')) {
|
|
464
|
+
relations.push({ from: task.id, to: ref, type: 'implements' });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// 提取 SPEC:xxx 引用 → task references spec
|
|
469
|
+
const specRefs = content.match(/SPEC:[\w\/\-]+/g);
|
|
470
|
+
if (specRefs) {
|
|
471
|
+
for (const ref of [...new Set(specRefs)]) {
|
|
472
|
+
if (entityMap.has(ref) && !relations.some(r => r.from === task.id && r.to === ref && r.type === 'references')) {
|
|
473
|
+
relations.push({ from: task.id, to: ref, type: 'references' });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch { /* 跳过 */ }
|
|
479
|
+
break; // 只读第一个存在的
|
|
480
|
+
}
|
|
481
|
+
// 读取 API_CONTRACT.yaml 解析依赖
|
|
482
|
+
const contractPath = (0, path_1.join)(taskAbsDir, '_shared', 'API_CONTRACT.yaml');
|
|
483
|
+
if (await (0, fs_extra_1.pathExists)(contractPath)) {
|
|
484
|
+
try {
|
|
485
|
+
const content = await (0, fs_extra_1.readFile)(contractPath, 'utf-8');
|
|
486
|
+
// 提取依赖的任务引用: dependsOn: Task-xxx 或 # Depends on Task-xxx
|
|
487
|
+
const depRefs = content.match(/(?:dependsOn|depends on|依赖)\s*[::]\s*(Task-\S+)/gi);
|
|
488
|
+
if (depRefs) {
|
|
489
|
+
for (const raw of depRefs) {
|
|
490
|
+
const m = raw.match(/(Task-\S+)/i);
|
|
491
|
+
if (m && entityMap.has(m[1]) && !relations.some(r => r.from === task.id && r.to === m[1] && r.type === 'depends_on')) {
|
|
492
|
+
relations.push({ from: task.id, to: m[1], type: 'depends_on' });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch { /* 跳过 */ }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
351
500
|
return relations;
|
|
352
501
|
}
|
|
353
502
|
// ═══════════════════════════════════════════════
|
|
@@ -371,22 +520,35 @@ async function buildKnowledgeGraph(cwd, iteration) {
|
|
|
371
520
|
const specResult = await scanSpecs(iterDir);
|
|
372
521
|
const taskResult = await scanTasks(iterDir);
|
|
373
522
|
const userFiles = await scanUserFiles(iterDir);
|
|
374
|
-
//
|
|
523
|
+
// 合并实体(处理 ID 冲突:同名实体用路径前缀去重)
|
|
375
524
|
const allEntities = [
|
|
376
525
|
...reqResult.entities,
|
|
377
526
|
...specResult.entities,
|
|
378
527
|
...taskResult.entities,
|
|
379
528
|
...userFiles,
|
|
380
529
|
];
|
|
530
|
+
const idRemap = new Map();
|
|
381
531
|
for (const e of allEntities) {
|
|
532
|
+
let finalId = e.id;
|
|
533
|
+
if (graph.entities[e.id]) {
|
|
534
|
+
finalId = `${e.id}@${e.file.replace(/\//g, '-')}`;
|
|
535
|
+
idRemap.set(e.id, finalId);
|
|
536
|
+
}
|
|
537
|
+
e.id = finalId;
|
|
382
538
|
graph.entities[e.id] = e;
|
|
383
539
|
}
|
|
540
|
+
// 同步更新已有关系中的旧 ID
|
|
541
|
+
const remapId = (id) => idRemap.get(id) || id;
|
|
542
|
+
for (const r of [...reqResult.relations, ...specResult.relations, ...taskResult.relations]) {
|
|
543
|
+
r.from = remapId(r.from);
|
|
544
|
+
r.to = remapId(r.to);
|
|
545
|
+
}
|
|
384
546
|
// 合并关系
|
|
385
547
|
graph.relations = [
|
|
386
548
|
...reqResult.relations,
|
|
387
549
|
...specResult.relations,
|
|
388
550
|
...taskResult.relations,
|
|
389
|
-
...inferRelations(allEntities),
|
|
551
|
+
...(await inferRelations(allEntities, iterDir)),
|
|
390
552
|
];
|
|
391
553
|
// 统计
|
|
392
554
|
graph.stats = {
|
|
@@ -408,13 +570,24 @@ async function saveKnowledgeGraph(cwd, graph) {
|
|
|
408
570
|
return filePath;
|
|
409
571
|
}
|
|
410
572
|
/** 加载已存在的知识图谱 */
|
|
573
|
+
/**
|
|
574
|
+
* 加载知识图谱(带进程缓存)
|
|
575
|
+
* 文件 mtime 未变时直接返回缓存,避免重复 JSON 解析
|
|
576
|
+
*/
|
|
411
577
|
async function loadKnowledgeGraph(cwd) {
|
|
412
578
|
const filePath = (0, path_1.join)(cwd, '.speccore', 'cache', 'knowledge-graph.json');
|
|
413
579
|
if (!(await (0, fs_extra_1.pathExists)(filePath)))
|
|
414
580
|
return null;
|
|
415
581
|
try {
|
|
582
|
+
const st = await (0, fs_extra_1.stat)(filePath);
|
|
583
|
+
const cached = kgCache.get(filePath);
|
|
584
|
+
if (cached && cached.mtime >= st.mtimeMs) {
|
|
585
|
+
return cached.graph;
|
|
586
|
+
}
|
|
416
587
|
const content = await (0, fs_extra_1.readFile)(filePath, 'utf-8');
|
|
417
|
-
|
|
588
|
+
const graph = JSON.parse(content);
|
|
589
|
+
kgCache.set(filePath, { graph, mtime: st.mtimeMs });
|
|
590
|
+
return graph;
|
|
418
591
|
}
|
|
419
592
|
catch {
|
|
420
593
|
return null;
|
|
@@ -424,35 +597,95 @@ async function loadKnowledgeGraph(cwd) {
|
|
|
424
597
|
function getTaskContext(graph, taskId) {
|
|
425
598
|
const entity = graph.entities[taskId];
|
|
426
599
|
if (!entity) {
|
|
427
|
-
return { requirement: null, siblingSubtasks: [], parentTask: null };
|
|
600
|
+
return { requirement: null, siblingSubtasks: [], parentTask: null, relatedSpecs: [], dependsOn: [] };
|
|
428
601
|
}
|
|
429
602
|
let requirement = null;
|
|
430
603
|
let parentTask = null;
|
|
431
|
-
const
|
|
432
|
-
|
|
604
|
+
const relatedSpecs = [];
|
|
605
|
+
const dependsOn = [];
|
|
606
|
+
// 预建索引:避免多次遍历全量关系
|
|
607
|
+
const parentIndex = new Map();
|
|
608
|
+
for (const e of Object.values(graph.entities)) {
|
|
609
|
+
if (e.type === 'subtask' && e.parentTaskId) {
|
|
610
|
+
const list = parentIndex.get(e.parentTaskId) || [];
|
|
611
|
+
list.push(e);
|
|
612
|
+
parentIndex.set(e.parentTaskId, list);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// 找上游需求、关联规格、依赖任务
|
|
433
616
|
for (const rel of graph.relations) {
|
|
434
|
-
if (rel.from === taskId
|
|
435
|
-
|
|
617
|
+
if (rel.from === taskId) {
|
|
618
|
+
if (rel.type === 'implements')
|
|
619
|
+
requirement = graph.entities[rel.to] || null;
|
|
620
|
+
if (rel.type === 'references') {
|
|
621
|
+
const spec = graph.entities[rel.to];
|
|
622
|
+
if (spec)
|
|
623
|
+
relatedSpecs.push(spec);
|
|
624
|
+
}
|
|
625
|
+
if (rel.type === 'depends_on') {
|
|
626
|
+
const dep = graph.entities[rel.to];
|
|
627
|
+
if (dep)
|
|
628
|
+
dependsOn.push(dep);
|
|
629
|
+
}
|
|
630
|
+
if (rel.type === 'subtask_of')
|
|
631
|
+
parentTask = graph.entities[rel.to] || null;
|
|
436
632
|
}
|
|
437
|
-
|
|
438
|
-
|
|
633
|
+
}
|
|
634
|
+
// 子任务没有直接 implements 关系 → 通过父任务找上游需求
|
|
635
|
+
if (!requirement && parentTask) {
|
|
636
|
+
for (const rel of graph.relations) {
|
|
637
|
+
if (rel.from === parentTask.id && rel.type === 'implements') {
|
|
638
|
+
requirement = graph.entities[rel.to] || null;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
439
641
|
}
|
|
440
642
|
}
|
|
441
|
-
//
|
|
643
|
+
// 如果是子任务,找兄弟子任务(用索引,O(1))
|
|
644
|
+
const siblingSubtasks = [];
|
|
442
645
|
const parentId = entity.parentTaskId || parentTask?.id;
|
|
443
646
|
if (parentId) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
}
|
|
647
|
+
const siblings = parentIndex.get(parentId);
|
|
648
|
+
if (siblings)
|
|
649
|
+
siblingSubtasks.push(...siblings);
|
|
449
650
|
}
|
|
450
|
-
return { requirement, siblingSubtasks, parentTask };
|
|
651
|
+
return { requirement, siblingSubtasks, parentTask, relatedSpecs, dependsOn };
|
|
451
652
|
}
|
|
452
653
|
// ═══════════════════════════════════════════════
|
|
453
654
|
// 自动更新机制
|
|
454
655
|
// ═══════════════════════════════════════════════
|
|
455
|
-
/**
|
|
656
|
+
/**
|
|
657
|
+
* 递归获取目录下所有文件的最新 mtime
|
|
658
|
+
* 限制:每个目录最多检查 MAX_FILES_PER_DIR 个文件,避免大型项目阻塞
|
|
659
|
+
*/
|
|
660
|
+
const MAX_FILES_PER_DIR = 100;
|
|
661
|
+
async function getLatestMtime(dir, fileCount = { value: 0 }) {
|
|
662
|
+
if (!(await (0, fs_extra_1.pathExists)(dir)))
|
|
663
|
+
return 0;
|
|
664
|
+
let latest = 0;
|
|
665
|
+
const items = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
|
|
666
|
+
for (const item of items) {
|
|
667
|
+
if (item.name.startsWith('.') || (0, task_utils_1.isTimestampBackup)(item.name))
|
|
668
|
+
continue;
|
|
669
|
+
if (fileCount.value >= MAX_FILES_PER_DIR)
|
|
670
|
+
break;
|
|
671
|
+
const fullPath = (0, path_1.join)(dir, item.name);
|
|
672
|
+
const st = await (0, fs_extra_1.stat)(fullPath);
|
|
673
|
+
if (item.isDirectory()) {
|
|
674
|
+
if (item.name === 'node_modules' || item.name === 'cache')
|
|
675
|
+
continue;
|
|
676
|
+
const subLatest = await getLatestMtime(fullPath, fileCount);
|
|
677
|
+
if (subLatest > latest)
|
|
678
|
+
latest = subLatest;
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
fileCount.value++;
|
|
682
|
+
if (st.mtimeMs > latest)
|
|
683
|
+
latest = st.mtimeMs;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return latest;
|
|
687
|
+
}
|
|
688
|
+
/** 检查知识图谱是否已过期(带 500ms 超时保护) */
|
|
456
689
|
async function isGraphStale(cwd, iteration) {
|
|
457
690
|
const graph = await loadKnowledgeGraph(cwd);
|
|
458
691
|
if (!graph)
|
|
@@ -461,23 +694,23 @@ async function isGraphStale(cwd, iteration) {
|
|
|
461
694
|
const iterDir = await (0, context_1.getIterationDir)(iterName);
|
|
462
695
|
if (!iterDir)
|
|
463
696
|
return false;
|
|
464
|
-
const { stat } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
|
|
465
697
|
const graphTime = new Date(graph.generated).getTime();
|
|
466
|
-
//
|
|
698
|
+
// 递归检查目录下所有文件的最新 mtime(目录 mtime 只在增删文件时变)
|
|
467
699
|
const dirsToCheck = [
|
|
468
700
|
(0, path_1.join)(iterDir, '010-requirements'),
|
|
469
701
|
(0, path_1.join)(iterDir, '020-specs'),
|
|
470
702
|
(0, path_1.join)(iterDir, '030-tasks'),
|
|
471
703
|
];
|
|
472
704
|
for (const dir of dirsToCheck) {
|
|
473
|
-
|
|
705
|
+
// 超时保护:单目录扫描超过 200ms 就放弃
|
|
706
|
+
const start = Date.now();
|
|
707
|
+
const latestMtime = await getLatestMtime(dir);
|
|
708
|
+
if (Date.now() - start > 200) {
|
|
709
|
+
logger_1.logger.debug(`isGraphStale: ${dir} 扫描超时,假设未过期`);
|
|
474
710
|
continue;
|
|
475
|
-
try {
|
|
476
|
-
const st = await stat(dir);
|
|
477
|
-
if (st.mtimeMs > graphTime)
|
|
478
|
-
return true;
|
|
479
711
|
}
|
|
480
|
-
|
|
712
|
+
if (latestMtime > graphTime)
|
|
713
|
+
return true;
|
|
481
714
|
}
|
|
482
715
|
return false;
|
|
483
716
|
}
|