speccore 6.6.0 → 6.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/.agents/skills/spec-ask/SKILL.md +24 -4
  2. package/README.md +10 -0
  3. package/dist/cli.js +18 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/about.d.ts.map +1 -1
  6. package/dist/commands/about.js +10 -3
  7. package/dist/commands/about.js.map +1 -1
  8. package/dist/commands/analyze.d.ts +3 -0
  9. package/dist/commands/analyze.d.ts.map +1 -1
  10. package/dist/commands/analyze.js +178 -11
  11. package/dist/commands/analyze.js.map +1 -1
  12. package/dist/commands/change.d.ts.map +1 -1
  13. package/dist/commands/change.js +16 -4
  14. package/dist/commands/change.js.map +1 -1
  15. package/dist/commands/doc2spec.d.ts +1 -0
  16. package/dist/commands/doc2spec.d.ts.map +1 -1
  17. package/dist/commands/doc2spec.js +175 -0
  18. package/dist/commands/doc2spec.js.map +1 -1
  19. package/dist/commands/done.js +25 -5
  20. package/dist/commands/done.js.map +1 -1
  21. package/dist/commands/execute.d.ts.map +1 -1
  22. package/dist/commands/execute.js +20 -6
  23. package/dist/commands/execute.js.map +1 -1
  24. package/dist/commands/help.js +32 -5
  25. package/dist/commands/help.js.map +1 -1
  26. package/dist/commands/iteration/create.d.ts.map +1 -1
  27. package/dist/commands/iteration/create.js +40 -15
  28. package/dist/commands/iteration/create.js.map +1 -1
  29. package/dist/commands/iteration/split.d.ts.map +1 -1
  30. package/dist/commands/iteration/split.js +309 -36
  31. package/dist/commands/iteration/split.js.map +1 -1
  32. package/dist/commands/knowledge.d.ts +6 -0
  33. package/dist/commands/knowledge.d.ts.map +1 -0
  34. package/dist/commands/knowledge.js +94 -0
  35. package/dist/commands/knowledge.js.map +1 -0
  36. package/dist/commands/rag-index.d.ts +19 -0
  37. package/dist/commands/rag-index.d.ts.map +1 -0
  38. package/dist/commands/rag-index.js +272 -0
  39. package/dist/commands/rag-index.js.map +1 -0
  40. package/dist/commands/refresh.d.ts +21 -0
  41. package/dist/commands/refresh.d.ts.map +1 -0
  42. package/dist/commands/refresh.js +185 -0
  43. package/dist/commands/refresh.js.map +1 -0
  44. package/dist/commands/sync-global.d.ts.map +1 -1
  45. package/dist/commands/sync-global.js +59 -0
  46. package/dist/commands/sync-global.js.map +1 -1
  47. package/dist/core/ai-context-generator.d.ts +2 -0
  48. package/dist/core/ai-context-generator.d.ts.map +1 -1
  49. package/dist/core/ai-context-generator.js +8 -6
  50. package/dist/core/ai-context-generator.js.map +1 -1
  51. package/dist/core/analyze-engine.d.ts +19 -0
  52. package/dist/core/analyze-engine.d.ts.map +1 -1
  53. package/dist/core/analyze-engine.js +326 -9
  54. package/dist/core/analyze-engine.js.map +1 -1
  55. package/dist/core/ask-engine.d.ts.map +1 -1
  56. package/dist/core/ask-engine.js +134 -4
  57. package/dist/core/ask-engine.js.map +1 -1
  58. package/dist/core/ask-host-ai.d.ts.map +1 -1
  59. package/dist/core/ask-host-ai.js +9 -6
  60. package/dist/core/ask-host-ai.js.map +1 -1
  61. package/dist/core/code-scanner.d.ts +24 -2
  62. package/dist/core/code-scanner.d.ts.map +1 -1
  63. package/dist/core/code-scanner.js +310 -9
  64. package/dist/core/code-scanner.js.map +1 -1
  65. package/dist/core/context-builder.d.ts.map +1 -1
  66. package/dist/core/context-builder.js +28 -9
  67. package/dist/core/context-builder.js.map +1 -1
  68. package/dist/core/decay-detector.d.ts +2 -2
  69. package/dist/core/decay-detector.d.ts.map +1 -1
  70. package/dist/core/decay-detector.js +135 -6
  71. package/dist/core/decay-detector.js.map +1 -1
  72. package/dist/core/git-integration.d.ts +50 -2
  73. package/dist/core/git-integration.d.ts.map +1 -1
  74. package/dist/core/git-integration.js +217 -27
  75. package/dist/core/git-integration.js.map +1 -1
  76. package/dist/core/global-knowledge.d.ts +25 -0
  77. package/dist/core/global-knowledge.d.ts.map +1 -0
  78. package/dist/core/global-knowledge.js +228 -0
  79. package/dist/core/global-knowledge.js.map +1 -0
  80. package/dist/core/index-guard.d.ts +57 -0
  81. package/dist/core/index-guard.d.ts.map +1 -0
  82. package/dist/core/index-guard.js +202 -0
  83. package/dist/core/index-guard.js.map +1 -0
  84. package/dist/core/intent-cache.d.ts +3 -1
  85. package/dist/core/intent-cache.d.ts.map +1 -1
  86. package/dist/core/intent-cache.js +33 -3
  87. package/dist/core/intent-cache.js.map +1 -1
  88. package/dist/core/intent-recognition.d.ts.map +1 -1
  89. package/dist/core/intent-recognition.js +21 -2
  90. package/dist/core/intent-recognition.js.map +1 -1
  91. package/dist/core/knowledge-graph.d.ts +18 -3
  92. package/dist/core/knowledge-graph.d.ts.map +1 -1
  93. package/dist/core/knowledge-graph.js +539 -42
  94. package/dist/core/knowledge-graph.js.map +1 -1
  95. package/dist/core/knowledge-visualizer.d.ts +19 -0
  96. package/dist/core/knowledge-visualizer.d.ts.map +1 -0
  97. package/dist/core/knowledge-visualizer.js +528 -0
  98. package/dist/core/knowledge-visualizer.js.map +1 -0
  99. package/dist/core/next-steps.js +6 -0
  100. package/dist/core/next-steps.js.map +1 -1
  101. package/dist/core/platform-registry.d.ts +5 -0
  102. package/dist/core/platform-registry.d.ts.map +1 -1
  103. package/dist/core/platform-registry.js +52 -0
  104. package/dist/core/platform-registry.js.map +1 -1
  105. package/dist/core/prompt-builder.d.ts +3 -2
  106. package/dist/core/prompt-builder.d.ts.map +1 -1
  107. package/dist/core/prompt-builder.js +361 -72
  108. package/dist/core/prompt-builder.js.map +1 -1
  109. package/dist/core/rag-engine.d.ts +152 -0
  110. package/dist/core/rag-engine.d.ts.map +1 -0
  111. package/dist/core/rag-engine.js +816 -0
  112. package/dist/core/rag-engine.js.map +1 -0
  113. package/dist/core/spec-merger.d.ts +133 -0
  114. package/dist/core/spec-merger.d.ts.map +1 -0
  115. package/dist/core/spec-merger.js +735 -0
  116. package/dist/core/spec-merger.js.map +1 -0
  117. package/dist/core/unified-retrieval.d.ts +141 -0
  118. package/dist/core/unified-retrieval.d.ts.map +1 -0
  119. package/dist/core/unified-retrieval.js +471 -0
  120. package/dist/core/unified-retrieval.js.map +1 -0
  121. package/package.json +1 -1
@@ -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
- const content = await (0, fs_extra_1.readFile)(constitutionPath, 'utf-8');
33
- const stack = {};
34
- // 解析技术栈章节
35
- const techSection = content.match(/##\s*技术栈[\s\S]*?(?=## |$)/i);
36
- if (techSection) {
37
- const section = techSection[0];
38
- const langMatch = section.match(/语言[::]\s*(.+)/i);
39
- if (langMatch)
40
- stack.language = langMatch[1].trim();
41
- const frameworkMatch = section.match(/框架[::]\s*(.+)/i);
42
- if (frameworkMatch)
43
- stack.framework = frameworkMatch[1].trim();
44
- const dbMatch = section.match(/数据库[::]\s*(.+)/i);
45
- if (dbMatch)
46
- stack.database = dbMatch[1].trim();
47
- const cacheMatch = section.match(/缓存[::]\s*(.+)/i);
48
- if (cacheMatch)
49
- stack.cache = cacheMatch[1].trim();
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 stack;
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 newPath = (0, path_1.join)(cwd, taskDir, '_shared', 'REQ.md');
58
- const oldPath = (0, path_1.join)(cwd, taskDir, '00-specs', 'REQ.md');
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 newPath = (0, path_1.join)(cwd, taskDir, '_shared', 'REQ.md');
99
- const oldPath = (0, path_1.join)(cwd, taskDir, '00-specs', 'REQ.md');
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 reqPath = (0, path_1.join)(cwd, taskDir, '00-specs', 'REQ.md');
171
- const legacyReqPath = (0, path_1.join)(cwd, taskDir, 'REQ.md');
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
- const content = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
256
+ let content = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
224
257
  // 跳过空文件或纯占位符文件
225
- if (content.trim().length > 50 && !content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL\s*-->$/m)) {
226
- extras.push({ name: f.name, path: f.path, content });
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
- ctx.toc = await buildGlobalTOC(globalDir);
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
- const apiSpecs = await loadApiSpecs(cwd, taskDir);
772
- const dataModels = await loadDataModels(cwd, taskDir);
773
- const businessRules = await loadBusinessRules(cwd, taskDir);
774
- const extraSpecs = taskDir ? await loadExtraSpecs(cwd, taskDir, options.platform, options.iteration) : [];
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('');