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
|
@@ -17,56 +17,97 @@ exports.outputNeedsInfo = outputNeedsInfo;
|
|
|
17
17
|
const fs_extra_1 = require("fs-extra");
|
|
18
18
|
const path_1 = require("path");
|
|
19
19
|
const task_utils_1 = require("../utils/task-utils");
|
|
20
|
+
const logger_1 = require("../utils/logger");
|
|
20
21
|
const knowledge_graph_1 = require("./knowledge-graph");
|
|
21
22
|
const context_builder_1 = require("./context-builder");
|
|
23
|
+
const unified_retrieval_1 = require("./unified-retrieval");
|
|
24
|
+
const techStackCache = new Map();
|
|
25
|
+
const constitutionCache = new Map();
|
|
26
|
+
const reqContentCache = new Map();
|
|
27
|
+
const tocCache = new Map();
|
|
28
|
+
/** 通用文件缓存读取 */
|
|
29
|
+
async function cachedRead(cache, filePath, loader) {
|
|
30
|
+
try {
|
|
31
|
+
const st = await (0, fs_extra_1.stat)(filePath);
|
|
32
|
+
const cached = cache.get(filePath);
|
|
33
|
+
if (cached && cached.mtime >= st.mtimeMs) {
|
|
34
|
+
return cached.data;
|
|
35
|
+
}
|
|
36
|
+
const data = await loader();
|
|
37
|
+
cache.set(filePath, { data, mtime: st.mtimeMs, key: filePath });
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// 文件不存在时直接加载(不缓存)
|
|
42
|
+
return loader();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
22
45
|
// ═══════════════════════════════════════════════════════════
|
|
23
46
|
// 上下文加载
|
|
24
47
|
// ═══════════════════════════════════════════════════════════
|
|
25
48
|
/**
|
|
26
|
-
* 从 CONSTITUTION.md
|
|
49
|
+
* 从 CONSTITUTION.md 解析技术栈(带进程缓存)
|
|
27
50
|
*/
|
|
28
51
|
async function loadTechStack(cwd) {
|
|
29
52
|
const constitutionPath = (0, path_1.join)(cwd, '.speccore', 'CONSTITUTION.md');
|
|
30
53
|
if (!await (0, fs_extra_1.pathExists)(constitutionPath))
|
|
31
54
|
return {};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
return cachedRead(techStackCache, constitutionPath, async () => {
|
|
56
|
+
const content = await (0, fs_extra_1.readFile)(constitutionPath, 'utf-8');
|
|
57
|
+
const stack = {};
|
|
58
|
+
// 解析技术栈章节
|
|
59
|
+
const techSection = content.match(/##\s*技术栈[\s\S]*?(?=## |$)/i);
|
|
60
|
+
if (techSection) {
|
|
61
|
+
const section = techSection[0];
|
|
62
|
+
const langMatch = section.match(/语言[::]\s*(.+)/i);
|
|
63
|
+
if (langMatch)
|
|
64
|
+
stack.language = langMatch[1].trim();
|
|
65
|
+
const frameworkMatch = section.match(/框架[::]\s*(.+)/i);
|
|
66
|
+
if (frameworkMatch)
|
|
67
|
+
stack.framework = frameworkMatch[1].trim();
|
|
68
|
+
const dbMatch = section.match(/数据库[::]\s*(.+)/i);
|
|
69
|
+
if (dbMatch)
|
|
70
|
+
stack.database = dbMatch[1].trim();
|
|
71
|
+
const cacheMatch = section.match(/缓存[::]\s*(.+)/i);
|
|
72
|
+
if (cacheMatch)
|
|
73
|
+
stack.cache = cacheMatch[1].trim();
|
|
74
|
+
}
|
|
75
|
+
return stack;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 定位 REQ.md 实际路径(支持新旧结构)
|
|
80
|
+
*/
|
|
81
|
+
async function resolveReqPath(cwd, taskDir) {
|
|
82
|
+
const paths = [
|
|
83
|
+
(0, path_1.join)(cwd, taskDir, '_shared', 'REQ.md'),
|
|
84
|
+
(0, path_1.join)(cwd, taskDir, '00-specs', 'REQ.md'),
|
|
85
|
+
(0, path_1.join)(cwd, taskDir, 'REQ.md'),
|
|
86
|
+
];
|
|
87
|
+
for (const p of paths) {
|
|
88
|
+
if (await (0, fs_extra_1.pathExists)(p))
|
|
89
|
+
return p;
|
|
50
90
|
}
|
|
51
|
-
return
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 加载 REQ.md 内容(带进程缓存)
|
|
95
|
+
*/
|
|
96
|
+
async function loadReqContent(cwd, taskDir) {
|
|
97
|
+
const reqPath = await resolveReqPath(cwd, taskDir);
|
|
98
|
+
if (!reqPath)
|
|
99
|
+
return null;
|
|
100
|
+
return cachedRead(reqContentCache, reqPath, async () => {
|
|
101
|
+
return await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
|
|
102
|
+
});
|
|
52
103
|
}
|
|
53
104
|
/**
|
|
54
|
-
* 从 REQ.md 解析 API
|
|
105
|
+
* 从 REQ.md 解析 API 定义(支持传入已读取的内容,避免重复 I/O)
|
|
55
106
|
*/
|
|
56
|
-
async function loadApiSpecs(cwd, taskDir) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const legacyPath = (0, path_1.join)(cwd, taskDir, 'REQ.md');
|
|
60
|
-
let actualPath = null;
|
|
61
|
-
if (await (0, fs_extra_1.pathExists)(newPath))
|
|
62
|
-
actualPath = newPath;
|
|
63
|
-
else if (await (0, fs_extra_1.pathExists)(oldPath))
|
|
64
|
-
actualPath = oldPath;
|
|
65
|
-
else if (await (0, fs_extra_1.pathExists)(legacyPath))
|
|
66
|
-
actualPath = legacyPath;
|
|
67
|
-
if (!actualPath)
|
|
107
|
+
async function loadApiSpecs(cwd, taskDir, reqContent) {
|
|
108
|
+
const content = reqContent ?? await loadReqContent(cwd, taskDir);
|
|
109
|
+
if (!content)
|
|
68
110
|
return [];
|
|
69
|
-
const content = await (0, fs_extra_1.readFile)(actualPath, 'utf-8');
|
|
70
111
|
const apis = [];
|
|
71
112
|
// 解析 API 表格: | 方法 | 路径 | 说明 | 或 | Method | Path | Description |
|
|
72
113
|
const tableRegex = /\|.*(?:方法|Method).*\|.*(?:路径|Path).*\|.*(?:说明|Description).*\|.*/i;
|
|
@@ -92,22 +133,12 @@ async function loadApiSpecs(cwd, taskDir) {
|
|
|
92
133
|
return apis;
|
|
93
134
|
}
|
|
94
135
|
/**
|
|
95
|
-
* 从 REQ.md
|
|
136
|
+
* 从 REQ.md 解析数据模型(支持传入已读取的内容)
|
|
96
137
|
*/
|
|
97
|
-
async function loadDataModels(cwd, taskDir) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const legacyPath = (0, path_1.join)(cwd, taskDir, 'REQ.md');
|
|
101
|
-
let actualPath = null;
|
|
102
|
-
if (await (0, fs_extra_1.pathExists)(newPath))
|
|
103
|
-
actualPath = newPath;
|
|
104
|
-
else if (await (0, fs_extra_1.pathExists)(oldPath))
|
|
105
|
-
actualPath = oldPath;
|
|
106
|
-
else if (await (0, fs_extra_1.pathExists)(legacyPath))
|
|
107
|
-
actualPath = legacyPath;
|
|
108
|
-
if (!actualPath)
|
|
138
|
+
async function loadDataModels(cwd, taskDir, reqContent) {
|
|
139
|
+
const content = reqContent ?? await loadReqContent(cwd, taskDir);
|
|
140
|
+
if (!content)
|
|
109
141
|
return [];
|
|
110
|
-
const content = await (0, fs_extra_1.readFile)(actualPath, 'utf-8');
|
|
111
142
|
const models = [];
|
|
112
143
|
// 查找所有数据模型表格
|
|
113
144
|
const modelSection = content.match(/(?:数据模型|实体|Entity|Model)[\s\S]*?(?=## |\n##|$)/i);
|
|
@@ -148,14 +179,14 @@ async function loadDataModels(cwd, taskDir) {
|
|
|
148
179
|
return models;
|
|
149
180
|
}
|
|
150
181
|
/**
|
|
151
|
-
* 从 CONSTITUTION.md + REQ.md
|
|
182
|
+
* 从 CONSTITUTION.md + REQ.md 提取业务规则(支持传入已读取的 REQ 内容)
|
|
152
183
|
*/
|
|
153
|
-
async function loadBusinessRules(cwd, taskDir) {
|
|
184
|
+
async function loadBusinessRules(cwd, taskDir, reqContent) {
|
|
154
185
|
const rules = [];
|
|
155
|
-
// 从 CONSTITUTION
|
|
186
|
+
// 从 CONSTITUTION 读取命名规范等(带缓存)
|
|
156
187
|
const constitutionPath = (0, path_1.join)(cwd, '.speccore', 'CONSTITUTION.md');
|
|
157
188
|
if (await (0, fs_extra_1.pathExists)(constitutionPath)) {
|
|
158
|
-
const content = await (0, fs_extra_1.readFile)(constitutionPath, 'utf-8');
|
|
189
|
+
const content = await cachedRead(constitutionCache, constitutionPath, async () => await (0, fs_extra_1.readFile)(constitutionPath, 'utf-8'));
|
|
159
190
|
const namingSection = content.match(/##\s*命名规范[\s\S]*?(?=## |$)/i);
|
|
160
191
|
if (namingSection) {
|
|
161
192
|
const lines = namingSection[0].split('\n');
|
|
@@ -167,11 +198,8 @@ async function loadBusinessRules(cwd, taskDir) {
|
|
|
167
198
|
}
|
|
168
199
|
}
|
|
169
200
|
if (taskDir) {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
const actualReqPath = (await (0, fs_extra_1.pathExists)(reqPath)) ? reqPath : (await (0, fs_extra_1.pathExists)(legacyReqPath)) ? legacyReqPath : null;
|
|
173
|
-
if (actualReqPath) {
|
|
174
|
-
const content = await (0, fs_extra_1.readFile)(actualReqPath, 'utf-8');
|
|
201
|
+
const content = reqContent ?? await loadReqContent(cwd, taskDir);
|
|
202
|
+
if (content) {
|
|
175
203
|
const ruleSection = content.match(/(?:业务规则|约束条件|Constraint)[\s\S]*?(?=## |\n##|$)/i);
|
|
176
204
|
if (ruleSection) {
|
|
177
205
|
const lines = ruleSection[0].split('\n');
|
|
@@ -187,10 +215,15 @@ async function loadBusinessRules(cwd, taskDir) {
|
|
|
187
215
|
}
|
|
188
216
|
/**
|
|
189
217
|
* 读取任务目录中的额外上下文文件(TECH.md / TASK.md / SCHEMA.md / .issues.md 等)
|
|
218
|
+
* 带大小限制,防止 prompt 爆炸
|
|
190
219
|
*/
|
|
191
|
-
async function loadExtraSpecs(cwd, taskDir, platform, iteration) {
|
|
220
|
+
async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
|
|
192
221
|
const extras = [];
|
|
222
|
+
const MAX_PER_FILE = options?.maxCharsPerFile ?? 2000;
|
|
223
|
+
const MAX_TOTAL = options?.maxTotalChars ?? 8000;
|
|
224
|
+
let totalChars = 0;
|
|
193
225
|
const files = [
|
|
226
|
+
{ name: '任务上下文', path: '_shared/CONTEXT.md' },
|
|
194
227
|
{ name: '技术方案', path: '_shared/TECH.md' },
|
|
195
228
|
{ name: '技术方案(旧)', path: '00-specs/TECH.md' },
|
|
196
229
|
{ name: '任务追踪', path: '00-specs/TASK.md' },
|
|
@@ -220,13 +253,150 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration) {
|
|
|
220
253
|
for (const f of files) {
|
|
221
254
|
const fullPath = (0, path_1.join)(cwd, taskDir, f.path);
|
|
222
255
|
if (await (0, fs_extra_1.pathExists)(fullPath)) {
|
|
223
|
-
|
|
256
|
+
let content = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
|
|
224
257
|
// 跳过空文件或纯占位符文件
|
|
225
|
-
if (content.trim().length
|
|
226
|
-
|
|
258
|
+
if (content.trim().length <= 50 || content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL\s*-->$/m)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// 单文件大小限制
|
|
262
|
+
if (content.length > MAX_PER_FILE) {
|
|
263
|
+
content = content.slice(0, MAX_PER_FILE) + `\n\n> ... (已截断,原文件 ${content.length} 字)`;
|
|
264
|
+
}
|
|
265
|
+
// 总大小限制
|
|
266
|
+
if (totalChars + content.length > MAX_TOTAL) {
|
|
267
|
+
const remain = MAX_TOTAL - totalChars;
|
|
268
|
+
if (remain > 200) {
|
|
269
|
+
content = content.slice(0, remain) + `\n\n> ... (已达总上限 ${MAX_TOTAL} 字)`;
|
|
270
|
+
extras.push({ name: f.name, path: f.path, content });
|
|
271
|
+
}
|
|
272
|
+
break; // 总大小超限,停止加载更多文件
|
|
273
|
+
}
|
|
274
|
+
totalChars += content.length;
|
|
275
|
+
extras.push({ name: f.name, path: f.path, content });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return extras;
|
|
279
|
+
}
|
|
280
|
+
// ═══════════════════════════════════════════════════════════
|
|
281
|
+
// 全量兜底读取(检索不足时,读取所有内容)
|
|
282
|
+
// ═══════════════════════════════════════════════════════════
|
|
283
|
+
/** 全量兜底:当统一检索结果不足时,读取任务目录 + 迭代规格 + 关联任务的所有内容 */
|
|
284
|
+
async function loadAllTaskContext(cwd, taskDir, platform, iteration, graph) {
|
|
285
|
+
const extras = [];
|
|
286
|
+
const MAX_PER_FILE = 8000;
|
|
287
|
+
const MAX_TOTAL = 20000;
|
|
288
|
+
let totalChars = 0;
|
|
289
|
+
const seen = new Set();
|
|
290
|
+
const addFile = async (fullPath, name, relPath) => {
|
|
291
|
+
if (seen.has(fullPath))
|
|
292
|
+
return;
|
|
293
|
+
seen.add(fullPath);
|
|
294
|
+
if (!(await (0, fs_extra_1.pathExists)(fullPath)))
|
|
295
|
+
return;
|
|
296
|
+
let content = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
|
|
297
|
+
if (content.trim().length <= 50 || content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL\s*-->$/m))
|
|
298
|
+
return;
|
|
299
|
+
if (content.length > MAX_PER_FILE) {
|
|
300
|
+
content = content.slice(0, MAX_PER_FILE) + `\n\n> ... (已截断,原文件 ${content.length} 字)`;
|
|
301
|
+
}
|
|
302
|
+
if (totalChars + content.length > MAX_TOTAL)
|
|
303
|
+
return;
|
|
304
|
+
totalChars += content.length;
|
|
305
|
+
extras.push({ name, path: relPath, content });
|
|
306
|
+
};
|
|
307
|
+
// 1. 递归扫描任务目录所有 .md / .yaml 文件
|
|
308
|
+
const scanTaskDir = async (dir, prefix) => {
|
|
309
|
+
if (!(await (0, fs_extra_1.pathExists)(dir)))
|
|
310
|
+
return;
|
|
311
|
+
try {
|
|
312
|
+
const items = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
|
|
313
|
+
for (const item of items) {
|
|
314
|
+
if (item.name.startsWith('.') || (0, task_utils_1.isTimestampBackup)(item.name))
|
|
315
|
+
continue;
|
|
316
|
+
const fullPath = (0, path_1.join)(dir, item.name);
|
|
317
|
+
if (item.isDirectory()) {
|
|
318
|
+
if (item.name === 'node_modules' || item.name === '10-backend' || item.name === '20-frontend')
|
|
319
|
+
continue;
|
|
320
|
+
await scanTaskDir(fullPath, `${prefix}${item.name}/`);
|
|
321
|
+
}
|
|
322
|
+
else if (/\.(md|yaml|yml)$/i.test(item.name)) {
|
|
323
|
+
await addFile(fullPath, `${prefix}${item.name}`, `${prefix}${item.name}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch { /* 跳过 */ }
|
|
328
|
+
};
|
|
329
|
+
await scanTaskDir(taskDir, '');
|
|
330
|
+
// 2. 迭代规格 020-specs/ 所有 .md
|
|
331
|
+
if (iteration) {
|
|
332
|
+
const iterDir = (0, path_1.join)(cwd, `Iteration-${iteration}`);
|
|
333
|
+
const specsDir = (0, path_1.join)(iterDir, '020-specs');
|
|
334
|
+
if (await (0, fs_extra_1.pathExists)(specsDir)) {
|
|
335
|
+
try {
|
|
336
|
+
const items = await (0, fs_extra_1.readdir)(specsDir, { withFileTypes: true });
|
|
337
|
+
for (const item of items) {
|
|
338
|
+
if (!item.name.endsWith('.md') || (0, task_utils_1.isTimestampBackup)(item.name))
|
|
339
|
+
continue;
|
|
340
|
+
await addFile((0, path_1.join)(specsDir, item.name), `迭代规格: ${item.name}`, `020-specs/${item.name}`);
|
|
341
|
+
}
|
|
342
|
+
// 各端规格
|
|
343
|
+
if (platform) {
|
|
344
|
+
const platDir = (0, path_1.join)(specsDir, 'platforms', platform);
|
|
345
|
+
if (await (0, fs_extra_1.pathExists)(platDir)) {
|
|
346
|
+
const platItems = await (0, fs_extra_1.readdir)(platDir, { withFileTypes: true });
|
|
347
|
+
for (const item of platItems) {
|
|
348
|
+
if (!item.name.endsWith('.md') || (0, task_utils_1.isTimestampBackup)(item.name))
|
|
349
|
+
continue;
|
|
350
|
+
await addFile((0, path_1.join)(platDir, item.name), `${platform}端规格: ${item.name}`, `020-specs/platforms/${platform}/${item.name}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// features/ 规格
|
|
355
|
+
const featuresDir = (0, path_1.join)(specsDir, 'features');
|
|
356
|
+
if (await (0, fs_extra_1.pathExists)(featuresDir)) {
|
|
357
|
+
const featItems = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
|
|
358
|
+
for (const item of featItems) {
|
|
359
|
+
if (!item.name.endsWith('.md') || (0, task_utils_1.isTimestampBackup)(item.name))
|
|
360
|
+
continue;
|
|
361
|
+
await addFile((0, path_1.join)(featuresDir, item.name), `功能规格: ${item.name}`, `020-specs/features/${item.name}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
227
364
|
}
|
|
365
|
+
catch { /* 跳过 */ }
|
|
228
366
|
}
|
|
229
367
|
}
|
|
368
|
+
// 3. 关联任务的 00-specs/(从知识图谱获取依赖任务)
|
|
369
|
+
if (graph) {
|
|
370
|
+
const relatedIds = [];
|
|
371
|
+
for (const rel of graph.relations) {
|
|
372
|
+
if (rel.type === 'depends_on' || rel.type === 'subtask_of') {
|
|
373
|
+
relatedIds.push(rel.from, rel.to);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const uniqueRelated = [...new Set(relatedIds)];
|
|
377
|
+
if (iteration && uniqueRelated.length > 0) {
|
|
378
|
+
const iterDir = (0, path_1.join)(cwd, `Iteration-${iteration}`);
|
|
379
|
+
const tasksDir = (0, path_1.join)(iterDir, '030-tasks');
|
|
380
|
+
for (const relId of uniqueRelated.slice(0, 3)) {
|
|
381
|
+
const relTaskDir = (0, path_1.join)(tasksDir, relId, '00-specs');
|
|
382
|
+
if (await (0, fs_extra_1.pathExists)(relTaskDir)) {
|
|
383
|
+
try {
|
|
384
|
+
const relItems = await (0, fs_extra_1.readdir)(relTaskDir, { withFileTypes: true });
|
|
385
|
+
for (const item of relItems) {
|
|
386
|
+
if (!item.name.endsWith('.md') || (0, task_utils_1.isTimestampBackup)(item.name))
|
|
387
|
+
continue;
|
|
388
|
+
await addFile((0, path_1.join)(relTaskDir, item.name), `关联任务 ${relId}: ${item.name}`, `030-tasks/${relId}/00-specs/${item.name}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch { /* 跳过 */ }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// 4. 全局文档(核心 2 个)
|
|
397
|
+
const globalDir = (0, path_1.join)(cwd, '.speccore', 'GLOBAL');
|
|
398
|
+
await addFile((0, path_1.join)(globalDir, 'ARCHITECTURE.md'), '全局架构', 'GLOBAL/ARCHITECTURE.md');
|
|
399
|
+
await addFile((0, path_1.join)(globalDir, 'TECH_STACK.md'), '全局技术栈', 'GLOBAL/TECH_STACK.md');
|
|
230
400
|
return extras;
|
|
231
401
|
}
|
|
232
402
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -450,7 +620,7 @@ async function buildGlobalTOC(globalDir) {
|
|
|
450
620
|
return toc;
|
|
451
621
|
}
|
|
452
622
|
/**
|
|
453
|
-
*
|
|
623
|
+
* 加载全局上下文(带 TOC 缓存)
|
|
454
624
|
* 策略:必读的 INDEX.md 直接注入 + 其余文件只给目录,AI 自己 Read
|
|
455
625
|
*/
|
|
456
626
|
async function loadGlobalContext(cwd, _command, _platform) {
|
|
@@ -458,14 +628,21 @@ async function loadGlobalContext(cwd, _command, _platform) {
|
|
|
458
628
|
const ctx = { toc: [] };
|
|
459
629
|
if (!await (0, fs_extra_1.pathExists)(globalDir))
|
|
460
630
|
return ctx;
|
|
461
|
-
// 必读:INDEX.md
|
|
631
|
+
// 必读:INDEX.md 直接注入(带缓存)
|
|
462
632
|
const indexPath = (0, path_1.join)(globalDir, 'INDEX.md');
|
|
463
633
|
if (await (0, fs_extra_1.pathExists)(indexPath)) {
|
|
464
|
-
const content = await (0, fs_extra_1.readFile)(indexPath, 'utf-8');
|
|
634
|
+
const content = await cachedRead(constitutionCache, indexPath, async () => await (0, fs_extra_1.readFile)(indexPath, 'utf-8'));
|
|
465
635
|
ctx.indexSummary = content.slice(0, 1500);
|
|
466
636
|
}
|
|
467
|
-
// 其余:只给目录,AI
|
|
468
|
-
|
|
637
|
+
// 其余:只给目录,AI 自己决定读什么(带缓存)
|
|
638
|
+
const cached = tocCache.get(globalDir);
|
|
639
|
+
if (cached) {
|
|
640
|
+
ctx.toc = cached.data;
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
ctx.toc = await buildGlobalTOC(globalDir);
|
|
644
|
+
tocCache.set(globalDir, { data: ctx.toc, mtime: Date.now(), key: globalDir });
|
|
645
|
+
}
|
|
469
646
|
return ctx;
|
|
470
647
|
}
|
|
471
648
|
/**
|
|
@@ -768,14 +945,63 @@ async function buildPrompt(command, options) {
|
|
|
768
945
|
const cwd = options.cwd || process.cwd();
|
|
769
946
|
const techStack = await loadTechStack(cwd);
|
|
770
947
|
const taskDir = options.taskDir || '';
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
const
|
|
948
|
+
// P0-1: 统一读取 REQ.md,避免 loadApiSpecs/loadDataModels/loadBusinessRules 各读一次
|
|
949
|
+
const reqContent = taskDir ? await loadReqContent(cwd, taskDir) : null;
|
|
950
|
+
const apiSpecs = await loadApiSpecs(cwd, taskDir, reqContent || undefined);
|
|
951
|
+
const dataModels = await loadDataModels(cwd, taskDir, reqContent || undefined);
|
|
952
|
+
const businessRules = await loadBusinessRules(cwd, taskDir, reqContent || undefined);
|
|
953
|
+
// 统一检索层:同时查询文档 RAG + 代码切片 + 知识图谱
|
|
954
|
+
let extraSpecs = [];
|
|
955
|
+
if (taskDir) {
|
|
956
|
+
try {
|
|
957
|
+
const unifiedResult = await (0, unified_retrieval_1.unifiedSearch)(cwd, {
|
|
958
|
+
query: options.task || options.iteration || '',
|
|
959
|
+
iteration: options.iteration,
|
|
960
|
+
taskId: options.task,
|
|
961
|
+
platform: options.platform,
|
|
962
|
+
taskDir,
|
|
963
|
+
});
|
|
964
|
+
if (unifiedResult.documentChunks.length > 0 || unifiedResult.codeSlices.length > 0) {
|
|
965
|
+
extraSpecs = (0, unified_retrieval_1.assembleUnifiedContext)(unifiedResult, { maxTotalChars: 8000, generous: true });
|
|
966
|
+
logger_1.logger?.info?.(` 🔍 统一检索: ${unifiedResult.stats.docChunksFound} 文档块 + ${unifiedResult.stats.codeSlicesFound} 代码切片 | ~${unifiedResult.stats.totalTokensEstimate} tokens`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
catch (e) {
|
|
970
|
+
logger_1.logger?.debug?.('统一检索失败,回退到传统模式:', e);
|
|
971
|
+
}
|
|
972
|
+
// 回退:统一检索失败或结果为空时,用传统截断模式
|
|
973
|
+
if (extraSpecs.length === 0) {
|
|
974
|
+
extraSpecs = await loadExtraSpecs(cwd, taskDir, options.platform, options.iteration, {
|
|
975
|
+
maxCharsPerFile: 2000,
|
|
976
|
+
maxTotalChars: 8000,
|
|
977
|
+
});
|
|
978
|
+
if (extraSpecs.length > 0) {
|
|
979
|
+
logger_1.logger?.info?.(` 📄 传统模式: ${extraSpecs.length} 个参考文档已加载`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
// 稀疏检测 + 全量兜底:检索内容不足时,读取所有内容
|
|
983
|
+
const SPARSE_THRESHOLD = 3000;
|
|
984
|
+
const currentChars = extraSpecs.reduce((sum, s) => sum + s.content.length, 0);
|
|
985
|
+
if (currentChars < SPARSE_THRESHOLD) {
|
|
986
|
+
// 提前加载知识图谱(供全量兜底使用)
|
|
987
|
+
let fallbackGraph = null;
|
|
988
|
+
try {
|
|
989
|
+
fallbackGraph = await (0, knowledge_graph_1.loadKnowledgeGraph)(cwd);
|
|
990
|
+
if (fallbackGraph && await (0, knowledge_graph_1.isGraphStale)(cwd, options.iteration)) {
|
|
991
|
+
fallbackGraph = await (0, knowledge_graph_1.refreshKnowledgeGraph)(cwd, options.iteration);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
catch { /* 图谱不可用时跳过 */ }
|
|
995
|
+
const fullContext = await loadAllTaskContext(cwd, taskDir, options.platform, options.iteration, fallbackGraph || undefined);
|
|
996
|
+
if (fullContext.length > extraSpecs.length) {
|
|
997
|
+
extraSpecs = fullContext;
|
|
998
|
+
logger_1.logger?.info?.(` 📚 全量兜底: ${fullContext.length} 个文件 (检索内容不足 ${currentChars} < ${SPARSE_THRESHOLD})`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
775
1002
|
// 加载全局上下文(智能注入)
|
|
776
1003
|
const globalContext = await loadGlobalContext(cwd, command, options.platform);
|
|
777
1004
|
// 加载知识图谱 → 生成任务关联链(< 500 tokens)
|
|
778
|
-
// 懒加载:如果图谱已过期,自动重建
|
|
779
1005
|
let taskContextStr;
|
|
780
1006
|
if (options.task) {
|
|
781
1007
|
let graph = await (0, knowledge_graph_1.loadKnowledgeGraph)(cwd);
|
|
@@ -820,10 +1046,73 @@ async function buildPrompt(command, options) {
|
|
|
820
1046
|
// ═══════════════════════════════════════════════════════════
|
|
821
1047
|
// Prompt 序列化
|
|
822
1048
|
// ═══════════════════════════════════════════════════════════
|
|
1049
|
+
/**
|
|
1050
|
+
* 粗略估算 token 数(中文 ≈ 1.5 tokens/字,英文 ≈ 0.25 tokens/字符)
|
|
1051
|
+
*/
|
|
1052
|
+
function estimateTokens(text) {
|
|
1053
|
+
let tokens = 0;
|
|
1054
|
+
for (const ch of text) {
|
|
1055
|
+
tokens += ch.charCodeAt(0) > 127 ? 1.5 : 0.25;
|
|
1056
|
+
}
|
|
1057
|
+
return Math.ceil(tokens);
|
|
1058
|
+
}
|
|
823
1059
|
/**
|
|
824
1060
|
* 将 Prompt 序列化为 AI 可读的文本(输出到 stdout)
|
|
1061
|
+
* 带动态裁剪:超出预算时按优先级逐级简化
|
|
825
1062
|
*/
|
|
826
|
-
function formatPrompt(prompt) {
|
|
1063
|
+
function formatPrompt(prompt, maxTokens = 12000) {
|
|
1064
|
+
// 尝试完整构建
|
|
1065
|
+
let result = buildPromptText(prompt);
|
|
1066
|
+
let tokens = estimateTokens(result);
|
|
1067
|
+
if (tokens <= maxTokens)
|
|
1068
|
+
return result;
|
|
1069
|
+
// Level 1: 简化全局上下文(只保留 INDEX.md,去掉 TOC 目录)
|
|
1070
|
+
if (prompt.globalContext) {
|
|
1071
|
+
const slimGlobal = { ...prompt.globalContext, toc: [] };
|
|
1072
|
+
result = buildPromptText({ ...prompt, globalContext: slimGlobal });
|
|
1073
|
+
tokens = estimateTokens(result);
|
|
1074
|
+
if (tokens <= maxTokens) {
|
|
1075
|
+
logger_1.logger?.info?.(` 🪶 Prompt 已简化:隐藏全局目录(-${estimateTokens(formatGlobalContext(prompt.globalContext, prompt.platform))} tokens)`);
|
|
1076
|
+
return result;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
// Level 2: 压缩 extraSpecs(截断到 500 字/文件)
|
|
1080
|
+
if (prompt.extraSpecs.length > 0) {
|
|
1081
|
+
const slimExtras = prompt.extraSpecs.map(s => ({
|
|
1082
|
+
...s,
|
|
1083
|
+
content: s.content.length > 500 ? s.content.slice(0, 500) + '\n> ... (已截断)' : s.content,
|
|
1084
|
+
}));
|
|
1085
|
+
result = buildPromptText({ ...prompt, extraSpecs: slimExtras });
|
|
1086
|
+
tokens = estimateTokens(result);
|
|
1087
|
+
if (tokens <= maxTokens) {
|
|
1088
|
+
logger_1.logger?.info?.(` 🪶 Prompt 已简化:压缩 extraSpecs 至 500 字/文件`);
|
|
1089
|
+
return result;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// Level 3: 移除 taskContext(知识图谱关联链)
|
|
1093
|
+
if (prompt.taskContext) {
|
|
1094
|
+
result = buildPromptText({ ...prompt, taskContext: undefined });
|
|
1095
|
+
tokens = estimateTokens(result);
|
|
1096
|
+
if (tokens <= maxTokens) {
|
|
1097
|
+
logger_1.logger?.info?.(` 🪶 Prompt 已简化:隐藏任务关联链`);
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
// Level 4: 终极简化——只保留核心(技术栈 + API + 指令)
|
|
1102
|
+
const minimalPrompt = {
|
|
1103
|
+
...prompt,
|
|
1104
|
+
extraSpecs: [],
|
|
1105
|
+
taskContext: undefined,
|
|
1106
|
+
globalContext: undefined,
|
|
1107
|
+
dataModels: prompt.dataModels.slice(0, 2),
|
|
1108
|
+
businessRules: prompt.businessRules.slice(0, 3),
|
|
1109
|
+
};
|
|
1110
|
+
result = buildPromptText(minimalPrompt);
|
|
1111
|
+
logger_1.logger?.info?.(` 🪶 Prompt 已极简模式:仅保留技术栈/API/核心指令`);
|
|
1112
|
+
return result;
|
|
1113
|
+
}
|
|
1114
|
+
/** 实际构建 prompt 文本(无裁剪逻辑) */
|
|
1115
|
+
function buildPromptText(prompt) {
|
|
827
1116
|
const lines = [];
|
|
828
1117
|
lines.push('[SPECCORE_PROMPT]');
|
|
829
1118
|
lines.push('');
|