speccore 8.3.121 → 8.3.127

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 (144) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/about.d.ts.map +1 -1
  4. package/dist/commands/about.js +16 -0
  5. package/dist/commands/about.js.map +1 -1
  6. package/dist/commands/analyze.d.ts +1 -0
  7. package/dist/commands/analyze.d.ts.map +1 -1
  8. package/dist/commands/analyze.js +198 -70
  9. package/dist/commands/analyze.js.map +1 -1
  10. package/dist/commands/change.js +14 -7
  11. package/dist/commands/change.js.map +1 -1
  12. package/dist/commands/context.js +25 -31
  13. package/dist/commands/context.js.map +1 -1
  14. package/dist/commands/deploy.d.ts.map +1 -1
  15. package/dist/commands/deploy.js +6 -1
  16. package/dist/commands/deploy.js.map +1 -1
  17. package/dist/commands/done.js +29 -28
  18. package/dist/commands/done.js.map +1 -1
  19. package/dist/commands/execute.d.ts.map +1 -1
  20. package/dist/commands/execute.js +281 -20
  21. package/dist/commands/execute.js.map +1 -1
  22. package/dist/commands/import.js +1 -1
  23. package/dist/commands/import.js.map +1 -1
  24. package/dist/commands/init.d.ts +1 -0
  25. package/dist/commands/init.d.ts.map +1 -1
  26. package/dist/commands/init.js +513 -28
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/commands/iteration/create.js +1 -1
  29. package/dist/commands/iteration/split.d.ts.map +1 -1
  30. package/dist/commands/iteration/split.js +158 -70
  31. package/dist/commands/iteration/split.js.map +1 -1
  32. package/dist/commands/iteration-from-global.js +1 -2
  33. package/dist/commands/iteration-from-global.js.map +1 -1
  34. package/dist/commands/pattern.js +30 -16
  35. package/dist/commands/pattern.js.map +1 -1
  36. package/dist/commands/pipeline.d.ts.map +1 -1
  37. package/dist/commands/pipeline.js +15 -4
  38. package/dist/commands/pipeline.js.map +1 -1
  39. package/dist/commands/retro.d.ts.map +1 -1
  40. package/dist/commands/retro.js +15 -8
  41. package/dist/commands/retro.js.map +1 -1
  42. package/dist/commands/status-panel.js +1 -1
  43. package/dist/commands/status-panel.js.map +1 -1
  44. package/dist/commands/sync.js +15 -8
  45. package/dist/commands/sync.js.map +1 -1
  46. package/dist/commands/trace.js +27 -24
  47. package/dist/commands/trace.js.map +1 -1
  48. package/dist/commands/update-env-configs.d.ts.map +1 -1
  49. package/dist/commands/update-env-configs.js +33 -5
  50. package/dist/commands/update-env-configs.js.map +1 -1
  51. package/dist/commands/update.d.ts.map +1 -1
  52. package/dist/commands/update.js +35 -0
  53. package/dist/commands/update.js.map +1 -1
  54. package/dist/commands/verify.d.ts +1 -1
  55. package/dist/commands/verify.d.ts.map +1 -1
  56. package/dist/commands/verify.js +25 -0
  57. package/dist/commands/verify.js.map +1 -1
  58. package/dist/core/ai-context-generator.js +35 -22
  59. package/dist/core/ai-context-generator.js.map +1 -1
  60. package/dist/core/ai-impact-analyzer.d.ts.map +1 -1
  61. package/dist/core/ai-impact-analyzer.js +1 -1
  62. package/dist/core/ai-impact-analyzer.js.map +1 -1
  63. package/dist/core/analyze-engine.d.ts +2 -1
  64. package/dist/core/analyze-engine.d.ts.map +1 -1
  65. package/dist/core/analyze-engine.js +217 -53
  66. package/dist/core/analyze-engine.js.map +1 -1
  67. package/dist/core/ask-engine.d.ts.map +1 -1
  68. package/dist/core/ask-engine.js +31 -13
  69. package/dist/core/ask-engine.js.map +1 -1
  70. package/dist/core/change-detection.d.ts.map +1 -1
  71. package/dist/core/change-detection.js +21 -5
  72. package/dist/core/change-detection.js.map +1 -1
  73. package/dist/core/code-scanner.d.ts +12 -3
  74. package/dist/core/code-scanner.d.ts.map +1 -1
  75. package/dist/core/code-scanner.js +376 -10
  76. package/dist/core/code-scanner.js.map +1 -1
  77. package/dist/core/context-builder.d.ts +2 -1
  78. package/dist/core/context-builder.d.ts.map +1 -1
  79. package/dist/core/context-builder.js +104 -25
  80. package/dist/core/context-builder.js.map +1 -1
  81. package/dist/core/deploy/engine.d.ts.map +1 -1
  82. package/dist/core/deploy/engine.js +21 -6
  83. package/dist/core/deploy/engine.js.map +1 -1
  84. package/dist/core/git-integration.d.ts +5 -1
  85. package/dist/core/git-integration.d.ts.map +1 -1
  86. package/dist/core/git-integration.js +52 -15
  87. package/dist/core/git-integration.js.map +1 -1
  88. package/dist/core/knowledge-graph.d.ts +2 -0
  89. package/dist/core/knowledge-graph.d.ts.map +1 -1
  90. package/dist/core/knowledge-graph.js +246 -232
  91. package/dist/core/knowledge-graph.js.map +1 -1
  92. package/dist/core/name-validator.js +1 -1
  93. package/dist/core/name-validator.js.map +1 -1
  94. package/dist/core/pipeline-test.d.ts.map +1 -1
  95. package/dist/core/pipeline-test.js +7 -4
  96. package/dist/core/pipeline-test.js.map +1 -1
  97. package/dist/core/prompt-builder.d.ts +3 -0
  98. package/dist/core/prompt-builder.d.ts.map +1 -1
  99. package/dist/core/prompt-builder.js +629 -77
  100. package/dist/core/prompt-builder.js.map +1 -1
  101. package/dist/core/rag-engine.d.ts +10 -5
  102. package/dist/core/rag-engine.d.ts.map +1 -1
  103. package/dist/core/rag-engine.js +134 -16
  104. package/dist/core/rag-engine.js.map +1 -1
  105. package/dist/core/spec-paths.d.ts +42 -2
  106. package/dist/core/spec-paths.d.ts.map +1 -1
  107. package/dist/core/spec-paths.js +207 -11
  108. package/dist/core/spec-paths.js.map +1 -1
  109. package/dist/core/structured-extractor.d.ts +67 -0
  110. package/dist/core/structured-extractor.d.ts.map +1 -1
  111. package/dist/core/structured-extractor.js +1298 -23
  112. package/dist/core/structured-extractor.js.map +1 -1
  113. package/dist/core/test-config.d.ts +1 -1
  114. package/dist/core/test-config.d.ts.map +1 -1
  115. package/dist/core/test-config.js +85 -65
  116. package/dist/core/test-config.js.map +1 -1
  117. package/dist/core/ui-verify/report-generator.d.ts.map +1 -1
  118. package/dist/core/ui-verify/report-generator.js +7 -3
  119. package/dist/core/ui-verify/report-generator.js.map +1 -1
  120. package/dist/core/ui-verify/smoke-engine.js +7 -1
  121. package/dist/core/ui-verify/smoke-engine.js.map +1 -1
  122. package/dist/core/unified-config.d.ts +43 -3
  123. package/dist/core/unified-config.d.ts.map +1 -1
  124. package/dist/core/unified-config.js +133 -13
  125. package/dist/core/unified-config.js.map +1 -1
  126. package/dist/core/unified-retrieval.d.ts.map +1 -1
  127. package/dist/core/unified-retrieval.js +41 -15
  128. package/dist/core/unified-retrieval.js.map +1 -1
  129. package/dist/core/unit-context-assembler.js +2 -2
  130. package/dist/core/unit-context-assembler.js.map +1 -1
  131. package/dist/core/validator.d.ts.map +1 -1
  132. package/dist/core/validator.js +59 -44
  133. package/dist/core/validator.js.map +1 -1
  134. package/dist/core/verify-engine.d.ts +9 -2
  135. package/dist/core/verify-engine.d.ts.map +1 -1
  136. package/dist/core/verify-engine.js +274 -108
  137. package/dist/core/verify-engine.js.map +1 -1
  138. package/dist/core/verify-report-aggregator.js +9 -20
  139. package/dist/core/verify-report-aggregator.js.map +1 -1
  140. package/dist/utils/synonyms.d.ts +24 -0
  141. package/dist/utils/synonyms.d.ts.map +1 -0
  142. package/dist/utils/synonyms.js +114 -0
  143. package/dist/utils/synonyms.js.map +1 -0
  144. package/package.json +1 -1
@@ -4,6 +4,7 @@ exports.loadGlobalContext = loadGlobalContext;
4
4
  exports.formatGlobalContext = formatGlobalContext;
5
5
  exports.buildPrompt = buildPrompt;
6
6
  exports.formatPrompt = formatPrompt;
7
+ exports.extractInfoGaps = extractInfoGaps;
7
8
  exports.parseAiResponse = parseAiResponse;
8
9
  exports.outputNeedsInfo = outputNeedsInfo;
9
10
  exports.processMarkdownContent = processMarkdownContent;
@@ -22,8 +23,12 @@ const logger_1 = require("../utils/logger");
22
23
  const knowledge_graph_1 = require("./knowledge-graph");
23
24
  const context_builder_1 = require("./context-builder");
24
25
  const spec_paths_1 = require("./spec-paths");
25
- const rag_engine_1 = require("./rag-engine");
26
+ const context_1 = require("./context");
26
27
  const unified_retrieval_1 = require("./unified-retrieval");
28
+ // v8.3.122+: execute 时深入读取关联源码
29
+ const code_scanner_1 = require("./code-scanner");
30
+ // v8.3.125+: 同义词扩展,解决中英文/缩写关键词匹配失败
31
+ const synonyms_1 = require("../utils/synonyms");
27
32
  // v8.3.94+: 视觉模型引擎( specs 图片理解)
28
33
  const vision_engine_1 = require("./vision-engine");
29
34
  // v6.93.0+: Prompt 插件系统
@@ -246,6 +251,8 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
246
251
  { name: '已知问题', path: '.issues.md' },
247
252
  ];
248
253
  // 加载迭代级设计文档(020-specs/)—— 填补迭代层上下文断裂
254
+ // v8.3.126+: feature/platform 端级规格改为阅读清单模式(省 Token + AI 按需 Read)
255
+ const readingListItems = [];
249
256
  if (iteration) {
250
257
  const iterDir = (0, path_1.join)(cwd, `Iteration-${iteration}`);
251
258
  files.push({ name: '迭代设计文档', path: (0, path_1.join)(iterDir, '020-specs', 'DESIGN.md') });
@@ -255,7 +262,7 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
255
262
  files.push({ name: `迭代overview/${f}`, path: (0, path_1.join)(overviewDir, f) });
256
263
  }
257
264
  if (platform) {
258
- // v8.3.21+: 扫描 020-specs/{feature}/{platform}/ 下的 .md 文件
265
+ // v8.3.126+: 扫描 020-specs/{feature}/{platform}/ 下的 .md 文件,改为阅读清单
259
266
  const specsDir = (0, path_1.join)(iterDir, '020-specs');
260
267
  try {
261
268
  const features = await (0, spec_paths_1.parseFeatureList)(iterDir);
@@ -265,11 +272,11 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
265
272
  const entries = await (0, fs_extra_1.readdir)(featurePlatDir, { withFileTypes: true });
266
273
  for (const entry of entries) {
267
274
  const isMd = entry.name.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(entry.name);
268
- const isHtml = (entry.name.endsWith('.html') || entry.name.endsWith('.htm')) && !(0, task_utils_1.isTimestampBackup)(entry.name);
269
- if (isMd || isHtml) {
270
- files.push({
271
- name: `${feature}-${platform}端规格`,
275
+ if (isMd) {
276
+ readingListItems.push({
272
277
  path: (0, path_1.join)(featurePlatDir, entry.name),
278
+ feature,
279
+ name: entry.name,
273
280
  });
274
281
  }
275
282
  }
@@ -300,32 +307,17 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
300
307
  files.unshift({ name: `${platform}端组件树`, path: (0, path_1.join)(platform, firstSub, 'COMPONENT_TREE.md') }, { name: `${platform}端路由`, path: (0, path_1.join)(platform, firstSub, 'ROUTES.md') }, { name: `${platform}端状态管理`, path: (0, path_1.join)(platform, firstSub, 'STATE.md') });
301
308
  }
302
309
  }
303
- // 回退: 旧结构 10-backend/{服务}/ 或 20-frontend/{端}/
304
- const isBk = platform === 'backend' || platform.startsWith('后台');
305
- const categoryDir = isBk ? '10-backend' : '20-frontend';
306
- const serviceName = isBk && platform === 'backend' ? 'api' : platform;
307
- const legacyBase = (0, path_1.join)(cwd, taskDir, categoryDir, serviceName);
308
- if (subtaskDirsList.length === 0) {
309
- try {
310
- if (await (0, fs_extra_1.pathExists)(legacyBase)) {
311
- const entries = await (0, fs_extra_1.readdir)(legacyBase, { withFileTypes: true });
312
- const legacySubs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name);
313
- if (legacySubs.length > 0) {
314
- files.unshift({ name: `${platform}端子任务(旧)`, path: (0, path_1.join)(categoryDir, serviceName, legacySubs[0], 'TASK.md') });
315
- }
316
- }
317
- }
318
- catch { /* ignore */ }
319
- }
310
+ // v8.3.121+: 已移除 10-backend/20-frontend 旧结构回退,端平铺结构 {platform}/{subtask}/ 为标准
320
311
  }
321
312
  for (const f of files) {
322
313
  const fullPath = (0, path_1.join)(cwd, taskDir, f.path);
323
314
  seenPaths.add(fullPath);
324
315
  if (await (0, fs_extra_1.pathExists)(fullPath)) {
325
316
  let rawContent = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
326
- // v8.3.92+: HTML 原型文件提取文本内容
317
+ // v8.3.122+: HTML 原型文件保留完整内容(CSS/JS/结构),不再提取纯文本
318
+ // 原型包含视觉效果和交互逻辑,AI 需要完整解析
327
319
  const isHtml = fullPath.endsWith('.html') || fullPath.endsWith('.htm');
328
- let content = isHtml ? (0, rag_engine_1.extractHtmlText)(rawContent) : rawContent;
320
+ let content = rawContent;
329
321
  // v8.3.93+: Markdown 链接自动展开 + 图片提取
330
322
  if (fullPath.endsWith('.md')) {
331
323
  content = await processMarkdownContent(content, fullPath, seenPaths, visionConfig, {
@@ -363,9 +355,9 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
363
355
  seenPaths.add(fullPath);
364
356
  try {
365
357
  let rawContent = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
366
- // v8.3.92+: HTML 原型文件提取文本内容
358
+ // v8.3.122+: HTML 原型文件保留完整内容(CSS/JS/结构),不再提取纯文本
367
359
  const isHtml = fullPath.endsWith('.html') || fullPath.endsWith('.htm');
368
- let content = isHtml ? (0, rag_engine_1.extractHtmlText)(rawContent) : rawContent;
360
+ let content = rawContent;
369
361
  // v8.3.93+: Markdown 链接自动展开 + 图片提取
370
362
  if (fullPath.endsWith('.md')) {
371
363
  content = await processMarkdownContent(content, fullPath, seenPaths, visionConfig, {
@@ -391,6 +383,61 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
391
383
  }
392
384
  catch { /* 忽略读取失败的文件 */ }
393
385
  }
386
+ // v8.3.126+: 将 020-specs/{feature}/{platform}/ 端级规格转为阅读清单
387
+ if (readingListItems.length > 0) {
388
+ const listLines = [
389
+ '## 📚 迭代层端级规格阅读清单',
390
+ '> 以下文档与当前任务相关,已改为阅读清单模式(节省 Token)。',
391
+ '> 如需深入了解某个功能模块的规格,请按需 Read 对应文件。',
392
+ '',
393
+ ];
394
+ for (const item of readingListItems) {
395
+ try {
396
+ const raw = await (0, fs_extra_1.readFile)(item.path, 'utf-8');
397
+ const lines = raw.split(/\r?\n/);
398
+ // 提取第一个 # 标题
399
+ const titleLine = lines.find(l => l.startsWith('#')) || '';
400
+ const title = titleLine.replace(/^#+\s*/, '').trim() || item.name;
401
+ // 提取 ## 标签(前 5 个二级标题)
402
+ const sections = lines
403
+ .filter(l => l.startsWith('## ') && !l.includes('<!--'))
404
+ .slice(0, 5)
405
+ .map(l => l.replace(/^##\s*/, '').trim());
406
+ // 首段摘要(标题后第一个非空段落,最多 120 字)
407
+ let summary = '';
408
+ let foundTitle = false;
409
+ for (const l of lines) {
410
+ if (l.startsWith('#')) {
411
+ foundTitle = true;
412
+ continue;
413
+ }
414
+ if (foundTitle && l.trim()) {
415
+ summary = l.trim().slice(0, 120);
416
+ if (l.trim().length > 120)
417
+ summary += '...';
418
+ break;
419
+ }
420
+ }
421
+ const relPath = (0, path_1.relative)(cwd, item.path);
422
+ listLines.push(`### ${item.feature}/${item.name}`);
423
+ listLines.push(`- **文件**: \`${relPath}\``);
424
+ listLines.push(`- **标题**: ${title}`);
425
+ if (sections.length > 0)
426
+ listLines.push(`- **章节**: ${sections.join(' | ')}`);
427
+ if (summary)
428
+ listLines.push(`- **摘要**: ${summary}`);
429
+ listLines.push('');
430
+ }
431
+ catch { /* 忽略读取失败 */ }
432
+ }
433
+ listLines.push('---');
434
+ listLines.push('💡 **使用方式**: 如果预加载的任务规格不足以理解实现细节,请打开对应文件查看完整规格。');
435
+ extras.push({
436
+ name: '📚 迭代层端级规格阅读清单',
437
+ path: 'reading-list.md',
438
+ content: listLines.join('\n'),
439
+ });
440
+ }
394
441
  return extras;
395
442
  }
396
443
  /**
@@ -483,9 +530,9 @@ async function loadAllTaskContext(cwd, taskDir, platform, iteration, graph) {
483
530
  if (!(await (0, fs_extra_1.pathExists)(fullPath)))
484
531
  return;
485
532
  let rawContent = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
486
- // v8.3.92+: HTML 原型文件提取文本内容
533
+ // v8.3.122+: HTML 原型文件保留完整内容(CSS/JS/结构),不再提取纯文本
487
534
  const isHtml = fullPath.endsWith('.html') || fullPath.endsWith('.htm');
488
- let content = isHtml ? (0, rag_engine_1.extractHtmlText)(rawContent) : rawContent;
535
+ let content = rawContent;
489
536
  // v8.3.93+: Markdown 链接自动展开 + 图片提取
490
537
  if (fullPath.endsWith('.md')) {
491
538
  content = await processMarkdownContent(content, fullPath, seen, visionConfig, {
@@ -504,7 +551,7 @@ async function loadAllTaskContext(cwd, taskDir, platform, iteration, graph) {
504
551
  };
505
552
  // 1. 递归扫描任务目录所有 .md / .yaml 文件
506
553
  // 排除自检/审查/产出阶段文件(这些在代码生成后的 verify 阶段才需要)
507
- // 排除整个 10-backend/ 和 20-frontend/ 旧大类目录 + 00-specs/ _shared/ 等非代码目录
554
+ // 排除非代码目录(保留 10-backend/20-frontend 排除项以兼容旧项目数据)
508
555
  const CODEGEN_EXCLUDE_DIRS = new Set(['node_modules', '10-backend', '20-frontend', '00-specs', '_shared', '99-artifacts', '.meta']);
509
556
  const CODEGEN_EXCLUDE_FILES = new Set(['test.md', 'schema.md', 'review.md', 'changelog.md', 'deploy.md', '.issues.md']);
510
557
  const scanTaskDir = async (dir, prefix) => {
@@ -519,6 +566,9 @@ async function loadAllTaskContext(cwd, taskDir, platform, iteration, graph) {
519
566
  if (item.isDirectory()) {
520
567
  if (CODEGEN_EXCLUDE_DIRS.has(item.name))
521
568
  continue;
569
+ // v8.3.125+: 如果有 platform,根目录下只扫描匹配的端 + _shared
570
+ if (platform && prefix === '' && item.name !== '_shared' && item.name !== platform)
571
+ continue;
522
572
  await scanTaskDir(fullPath, `${prefix}${item.name}/`);
523
573
  }
524
574
  else if (/\.(md|yaml|yml|html|htm)$/i.test(item.name)) {
@@ -997,7 +1047,9 @@ function buildSplitInstruction() {
997
1047
  ' - 哪些功能需要跨端协作?',
998
1048
  ' - 数据如何在端之间流转?',
999
1049
  '',
1000
- '4. **Read `020-specs/{端名}/TECH.md`**(每个端都要读)→ 了解各端技术方案',
1050
+ '4. **Read `020-specs/{功能模块}/{端名}/TECH.md`**(按功能模块×端组合读取)→ 了解各端技术方案',
1051
+ ' - 先读 FUNCTION_MAP.md 获取功能模块清单',
1052
+ ' - 再按 `020-specs/{功能模块}/{端名}/TECH.md` 读取该功能在各端的技术方案',
1001
1053
  ' - 各端有哪些接口/页面?',
1002
1054
  ' - 各端技术栈和架构约束?',
1003
1055
  ' - **据此确定每个任务涉及哪些端**',
@@ -1201,6 +1253,25 @@ function getInstruction(command, context) {
1201
1253
  '2. **有疑问就记录** — 如果对需求理解、技术选型有疑问,按最佳判断实现,并将疑问写入 `.speccore/questions/execute-{任务名}-{日期}-*.md`',
1202
1254
  '3. **遇阻断就跳过** — 如果某个功能信息不足无法实现,跳过它并在疑问清单中记录',
1203
1255
  '4. **直接输出代码** — 按 JSON 格式输出文件列表,不要输出多余解释',
1256
+ '',
1257
+ '## 🔍 信息充足性自检(v8.3.124+)',
1258
+ '',
1259
+ '生成代码前,必须先评估当前上下文是否充足:',
1260
+ '1. **检查 API 契约**:所有接口的入参、出参、错误码是否都已明确?',
1261
+ '2. **检查数据模型**:Entity 的字段类型、约束、关系是否完整?',
1262
+ '3. **检查依赖服务**:是否需要调用其他 Service/Module,但其接口未提供?',
1263
+ '4. **检查公共模块**:是否需要使用通用工具类、拦截器、中间件,但未提供其签名?',
1264
+ '5. **检查前端映射**:UI 字段→API 字段的映射是否完整?',
1265
+ '',
1266
+ '如果存在信息缺口,在代码文件的注释中标注 `[INFO_GAP: 具体缺什么]`,例如:',
1267
+ '- `// [INFO_GAP: 缺少 UserService.findById 的返回类型定义]`',
1268
+ '- `// [INFO_GAP: 不知道 OrderStatus 枚举的完整取值]`',
1269
+ '- `// [INFO_GAP: 缺少支付回调接口的签名]`',
1270
+ '',
1271
+ '**要求**:',
1272
+ '- 每发现一个缺口就标注一个 `[INFO_GAP: ...]`',
1273
+ '- 不要假设缺失的信息「应该就是这样」,必须标注',
1274
+ '- INFO_GAP 标注不影响代码生成,但为后续补充提供精确目标',
1204
1275
  ].join('\n');
1205
1276
  case 'analyze':
1206
1277
  return [
@@ -1247,7 +1318,7 @@ function getInstruction(command, context) {
1247
1318
  * 构建完整的 SpecCore Prompt
1248
1319
  */
1249
1320
  async function buildPrompt(command, options) {
1250
- const cwd = options.cwd || process.cwd();
1321
+ const cwd = options.cwd || (0, task_utils_1.findProjectRoot)() || process.cwd();
1251
1322
  const techStack = await loadTechStack(cwd);
1252
1323
  const taskDir = options.taskDir || '';
1253
1324
  // P0-1: 统一读取 REQ.md,避免 loadApiSpecs/loadDataModels/loadBusinessRules 各读一次
@@ -1257,21 +1328,49 @@ async function buildPrompt(command, options) {
1257
1328
  const businessRules = await loadBusinessRules(cwd, taskDir, reqContent || undefined);
1258
1329
  // 统一检索层:同时查询文档 RAG + 代码切片 + 知识图谱
1259
1330
  let extraSpecs = [];
1260
- if (taskDir) {
1331
+ // v8.3.123+: analyze/split 阶段也需要统一检索,从迭代需求提取查询词
1332
+ let searchQuery = options.task || options.iteration || '';
1333
+ if (reqContent) {
1334
+ const titleMatch = reqContent.match(/^#\s+(.+)$/m);
1335
+ if (titleMatch)
1336
+ searchQuery = titleMatch[1].trim();
1337
+ }
1338
+ else if ((command === 'analyze' || command === 'split') && options.iteration) {
1261
1339
  try {
1262
- // v8.2.0+: 从 REQ.md 提取标题作为更精准的查询词
1263
- let searchQuery = options.task || options.iteration || '';
1264
- if (reqContent) {
1265
- const titleMatch = reqContent.match(/^#\s+(.+)$/m);
1266
- if (titleMatch)
1267
- searchQuery = titleMatch[1].trim();
1340
+ const iterDir = await (0, context_1.getIterationDir)(options.iteration);
1341
+ if (iterDir) {
1342
+ const reqIndex = (0, path_1.join)(iterDir, '010-requirements', 'INDEX.md');
1343
+ if (await (0, fs_extra_1.pathExists)(reqIndex)) {
1344
+ const idxContent = await (0, fs_extra_1.readFile)(reqIndex, 'utf-8');
1345
+ const titleMatch = idxContent.match(/^#\s+(.+)$/m);
1346
+ if (titleMatch)
1347
+ searchQuery = titleMatch[1].trim();
1348
+ }
1349
+ else {
1350
+ // 尝试读取第一个需求文档
1351
+ const reqDir = (0, path_1.join)(iterDir, '010-requirements');
1352
+ const reqFiles = (await (0, fs_extra_1.readdir)(reqDir).catch(() => []))
1353
+ .filter(f => f.endsWith('.md') && !f.startsWith('.'));
1354
+ if (reqFiles.length > 0) {
1355
+ const firstReq = await (0, fs_extra_1.readFile)((0, path_1.join)(reqDir, reqFiles[0]), 'utf-8');
1356
+ const titleMatch = firstReq.match(/^#\s+(.+)$/m);
1357
+ if (titleMatch)
1358
+ searchQuery = titleMatch[1].trim();
1359
+ }
1360
+ }
1268
1361
  }
1362
+ }
1363
+ catch { /* 忽略需求读取失败 */ }
1364
+ }
1365
+ const shouldSearchUnified = !!taskDir || command === 'analyze' || command === 'split';
1366
+ if (shouldSearchUnified && options.iteration) {
1367
+ try {
1269
1368
  const unifiedResult = await (0, unified_retrieval_1.unifiedSearch)(cwd, {
1270
1369
  query: searchQuery,
1271
1370
  iteration: options.iteration,
1272
1371
  taskId: options.task,
1273
1372
  platform: options.platform,
1274
- taskDir,
1373
+ taskDir: taskDir || undefined,
1275
1374
  });
1276
1375
  if (unifiedResult.documentChunks.length > 0 || unifiedResult.codeSlices.length > 0) {
1277
1376
  extraSpecs = (0, unified_retrieval_1.assembleUnifiedContext)(unifiedResult, { maxTotalChars: 8000, generous: true });
@@ -1281,8 +1380,8 @@ async function buildPrompt(command, options) {
1281
1380
  catch (e) {
1282
1381
  logger_1.logger?.debug?.('统一检索失败,回退到传统模式:', e);
1283
1382
  }
1284
- // 回退:统一检索失败或结果为空时,用传统截断模式
1285
- if (extraSpecs.length === 0) {
1383
+ // 回退:统一检索失败或结果为空时,用传统截断模式(仅 taskDir 存在时)
1384
+ if (extraSpecs.length === 0 && taskDir) {
1286
1385
  extraSpecs = await loadExtraSpecs(cwd, taskDir, options.platform, options.iteration, {
1287
1386
  maxCharsPerFile: 2000,
1288
1387
  maxTotalChars: 8000,
@@ -1291,25 +1390,383 @@ async function buildPrompt(command, options) {
1291
1390
  logger_1.logger?.info?.(` 📄 传统模式: ${extraSpecs.length} 个参考文档已加载`);
1292
1391
  }
1293
1392
  }
1294
- // 稀疏检测 + 全量兜底:检索内容不足时,读取所有内容
1295
- const SPARSE_THRESHOLD = 3000;
1296
- const currentChars = extraSpecs.reduce((sum, s) => sum + s.content.length, 0);
1297
- if (currentChars < SPARSE_THRESHOLD) {
1298
- // 提前加载知识图谱(供全量兜底使用)
1299
- let fallbackGraph = null;
1300
- try {
1301
- fallbackGraph = await (0, knowledge_graph_1.loadKnowledgeGraph)(cwd);
1302
- if (fallbackGraph && await (0, knowledge_graph_1.isGraphStale)(cwd, options.iteration)) {
1303
- fallbackGraph = await (0, knowledge_graph_1.refreshKnowledgeGraph)(cwd, options.iteration);
1393
+ // 稀疏检测 + 全量兜底:检索内容不足时,读取所有内容(仅 taskDir 存在时)
1394
+ if (taskDir) {
1395
+ const SPARSE_THRESHOLD = 3000;
1396
+ const currentChars = extraSpecs.reduce((sum, s) => sum + s.content.length, 0);
1397
+ if (currentChars < SPARSE_THRESHOLD) {
1398
+ let fallbackGraph = null;
1399
+ try {
1400
+ fallbackGraph = await (0, knowledge_graph_1.loadFreshKnowledgeGraph)(cwd, options.iteration);
1401
+ }
1402
+ catch { /* 图谱不可用时跳过 */ }
1403
+ const fullContext = await loadAllTaskContext(cwd, taskDir, options.platform, options.iteration, fallbackGraph || undefined);
1404
+ if (fullContext.length > extraSpecs.length) {
1405
+ extraSpecs = fullContext;
1406
+ logger_1.logger?.info?.(` 📚 全量兜底: ${fullContext.length} 个文件 (检索内容不足 ${currentChars} < ${SPARSE_THRESHOLD})`);
1304
1407
  }
1305
1408
  }
1306
- catch { /* 图谱不可用时跳过 */ }
1307
- const fullContext = await loadAllTaskContext(cwd, taskDir, options.platform, options.iteration, fallbackGraph || undefined);
1308
- if (fullContext.length > extraSpecs.length) {
1309
- extraSpecs = fullContext;
1310
- logger_1.logger?.info?.(` 📚 全量兜底: ${fullContext.length} 个文件 (检索内容不足 ${currentChars} < ${SPARSE_THRESHOLD})`);
1409
+ }
1410
+ }
1411
+ // v8.3.122+: execute / analyze 时深入读取关联源码(完整文件内容,不只是切片)
1412
+ // v8.3.122++: 同时注入知识图谱任务上下文,帮助 AI 理解代码关联关系
1413
+ if ((command === 'execute' || command === 'analyze') && searchQuery) {
1414
+ try {
1415
+ // v8.3.124+: execute 阶段优先注入结构化事实卡片(structured-data.json)
1416
+ // 这是最高优先级的改进:用结构化卡片替代原始源码,Token 效率提升 10-20 倍
1417
+ const structuredDataPath = (0, path_1.join)(cwd, '.speccore', 'cache', 'structured-data.json');
1418
+ let structuredCardsInjected = false;
1419
+ if (await (0, fs_extra_1.pathExists)(structuredDataPath)) {
1420
+ try {
1421
+ const sdContent = await (0, fs_extra_1.readFile)(structuredDataPath, 'utf-8');
1422
+ const structured = JSON.parse(sdContent);
1423
+ // v8.3.125+: 使用同义词扩展,解决 "登录" 与 "auth" 等跨语言/缩写不匹配
1424
+ const queryWords = [...(0, synonyms_1.expandSynonyms)((0, synonyms_1.extractNormalizedKeywords)(searchQuery))];
1425
+ const matchedApis = [];
1426
+ const matchedEntities = [];
1427
+ const matchedComponents = [];
1428
+ const matchedRoutes = [];
1429
+ const matchedDtos = []; // v8.3.126+
1430
+ const matchedServices = []; // v8.3.126+
1431
+ for (const [platform, data] of Object.entries(structured.endpoints || {})) {
1432
+ // API 匹配
1433
+ for (const api of data.apis || []) {
1434
+ const text = `${api.path || ''} ${api.handler || ''} ${api.description || ''}`.toLowerCase();
1435
+ if (queryWords.some(qw => text.includes(qw)))
1436
+ matchedApis.push({ ...api, platform });
1437
+ }
1438
+ // Entity 匹配
1439
+ for (const entity of data.entities || []) {
1440
+ const text = `${entity.name || ''} ${entity.tableName || ''} ${entity.description || ''}`.toLowerCase();
1441
+ if (queryWords.some(qw => text.includes(qw)))
1442
+ matchedEntities.push({ ...entity, platform });
1443
+ }
1444
+ // Component 匹配
1445
+ for (const comp of data.components || []) {
1446
+ const text = `${comp.name || ''} ${comp.description || ''}`.toLowerCase();
1447
+ if (queryWords.some(qw => text.includes(qw)))
1448
+ matchedComponents.push({ ...comp, platform });
1449
+ }
1450
+ // Route 匹配
1451
+ for (const route of data.routes || []) {
1452
+ const text = `${route.path || ''} ${route.component || ''}`.toLowerCase();
1453
+ if (queryWords.some(qw => text.includes(qw)))
1454
+ matchedRoutes.push({ ...route, platform });
1455
+ }
1456
+ }
1457
+ // v8.3.126+: 全局 DTO 匹配(跨平台)
1458
+ for (const dto of structured.dtos || []) {
1459
+ const text = `${dto.name || ''} ${dto.fields?.map((f) => f.name).join(' ') || ''}`.toLowerCase();
1460
+ if (queryWords.some(qw => text.includes(qw)))
1461
+ matchedDtos.push(dto);
1462
+ }
1463
+ // v8.3.126+: 全局 Service 匹配(跨平台)
1464
+ for (const svc of structured.services || []) {
1465
+ const text = `${svc.name || ''} ${svc.methods?.map((m) => m.name).join(' ') || ''}`.toLowerCase();
1466
+ if (queryWords.some(qw => text.includes(qw)))
1467
+ matchedServices.push(svc);
1468
+ }
1469
+ const cardLines = ['## 📇 结构化代码事实卡片(自动提取)'];
1470
+ cardLines.push('> 以下信息从代码扫描工具提取的结构化数据生成,信息密度高于原始源码。如需查看具体实现,参考下方源码文件。\n');
1471
+ if (matchedApis.length > 0) {
1472
+ cardLines.push(`### API 接口 (${matchedApis.length} 个匹配)`);
1473
+ for (const api of matchedApis.slice(0, 8)) {
1474
+ cardLines.push(`\n**${api.method || 'GET'} ${api.path || '/'}** — \`${api.handler}\``);
1475
+ cardLines.push(`- 📁 文件: \`${api.filePath}\`:${api.line}`);
1476
+ // v8.3.124+: 优先展示详细参数信息(含类型、装饰器、校验规则)
1477
+ if (api.parameterDetails && api.parameterDetails.length > 0) {
1478
+ const paramLines = api.parameterDetails.map((p) => {
1479
+ let s = ` - \`${p.name}\`${p.type ? `:\`${p.type}\`` : ''}${p.required === false ? ' (可选)' : ''}`;
1480
+ if (p.decorators && p.decorators.length > 0)
1481
+ s += ` 装饰:${p.decorators.map((d) => `\`${d}\``).join(',')}`;
1482
+ if (p.validationRules && p.validationRules.length > 0)
1483
+ s += ` 校验:${p.validationRules.map((v) => `\`${v}\``).join(',')}`;
1484
+ return s;
1485
+ });
1486
+ cardLines.push(`- 📥 参数:`);
1487
+ cardLines.push(...paramLines.slice(0, 6));
1488
+ if (paramLines.length > 6)
1489
+ cardLines.push(` ...等${paramLines.length}个参数`);
1490
+ }
1491
+ else if (api.parameters && api.parameters.length > 0) {
1492
+ cardLines.push(`- 📥 参数: ${api.parameters.join(', ')}`);
1493
+ }
1494
+ if (api.dtoRef)
1495
+ cardLines.push(`- 📦 DTO: \`${api.dtoRef}\``);
1496
+ if (api.responseType)
1497
+ cardLines.push(`- 📤 返回: \`${api.responseType}\``);
1498
+ if (api.authDecorators && api.authDecorators.length > 0) {
1499
+ cardLines.push(`- 🔐 鉴权: ${api.authDecorators.map((d) => `\`${d}\``).join(', ')}`);
1500
+ }
1501
+ if (api.decorators && api.decorators.length > 0)
1502
+ cardLines.push(`- 🏷️ 路由装饰器: ${api.decorators.map((d) => `\`${d}\``).join(', ')}`);
1503
+ // v8.3.125+: Service 调用链
1504
+ if (api.serviceCalls && api.serviceCalls.length > 0) {
1505
+ const callStrs = api.serviceCalls.slice(0, 5).map((c) => `\`${c.service}.${c.method}\``);
1506
+ cardLines.push(`- 🔗 调用链: ${callStrs.join(' → ')}${api.serviceCalls.length > 5 ? ` ...等${api.serviceCalls.length}个` : ''}`);
1507
+ }
1508
+ if (api.description)
1509
+ cardLines.push(`- 📝 说明: ${api.description}`);
1510
+ }
1511
+ }
1512
+ if (matchedEntities.length > 0) {
1513
+ cardLines.push(`\n### 数据实体 (${matchedEntities.length} 个匹配)`);
1514
+ for (const entity of matchedEntities.slice(0, 6)) {
1515
+ cardLines.push(`\n**${entity.name}**${entity.tableName ? ` (表: \`${entity.tableName}\`)` : ''}`);
1516
+ cardLines.push(`- 📁 文件: \`${entity.filePath}\`:${entity.line}`);
1517
+ if (entity.fields && entity.fields.length > 0) {
1518
+ const fieldStrs = entity.fields.slice(0, 8).map((f) => {
1519
+ let s = `\`${f.name}\`: ${f.type}`;
1520
+ if (f.isPrimaryKey)
1521
+ s += ' [PK]';
1522
+ if (f.nullable)
1523
+ s += ' ?';
1524
+ if (f.defaultValue !== undefined)
1525
+ s += ` =${f.defaultValue}`;
1526
+ // v8.3.124+: 展示列配置和校验规则
1527
+ if (f.columnOptions && Object.keys(f.columnOptions).length > 0) {
1528
+ const opts = Object.entries(f.columnOptions).map(([k, v]) => `${k}:${v}`).join(',');
1529
+ s += ` {${opts}}`;
1530
+ }
1531
+ if (f.validationRules && f.validationRules.length > 0)
1532
+ s += ` [${f.validationRules.map((v) => `\`${v}\``).join(',')}]`;
1533
+ return s;
1534
+ });
1535
+ cardLines.push(`- 🏗️ 字段: ${fieldStrs.join(', ')}${entity.fields.length > 8 ? ` ...等${entity.fields.length}个` : ''}`);
1536
+ }
1537
+ if (entity.indexes && entity.indexes.length > 0) {
1538
+ const idxStrs = entity.indexes.map((idx) => {
1539
+ let s = `${idx.fields.join('+')}`;
1540
+ if (idx.unique)
1541
+ s += '(唯一)';
1542
+ if (idx.name)
1543
+ s = `${idx.name}:${s}`;
1544
+ return s;
1545
+ });
1546
+ cardLines.push(`- 📇 索引: ${idxStrs.join('; ')}`);
1547
+ }
1548
+ if (entity.constraints && entity.constraints.length > 0) {
1549
+ cardLines.push(`- ⛓️ 约束: ${entity.constraints.map((c) => `${c.type}(${c.fields.join('+')})`).join('; ')}`);
1550
+ }
1551
+ if (entity.relations && entity.relations.length > 0) {
1552
+ cardLines.push(`- 🔗 关系: ${entity.relations.map((r) => `${r.type} → ${r.target}`).join(', ')}`);
1553
+ }
1554
+ }
1555
+ }
1556
+ if (matchedComponents.length > 0) {
1557
+ cardLines.push(`\n### 前端组件 (${matchedComponents.length} 个匹配)`);
1558
+ for (const comp of matchedComponents.slice(0, 6)) {
1559
+ cardLines.push(`\n**${comp.name}** — \`${comp.filePath}\`:${comp.line}`);
1560
+ if (comp.props && comp.props.length > 0)
1561
+ cardLines.push(`- Props: ${comp.props.join(', ')}`);
1562
+ }
1563
+ }
1564
+ if (matchedRoutes.length > 0) {
1565
+ cardLines.push(`\n### 页面路由 (${matchedRoutes.length} 个匹配)`);
1566
+ for (const route of matchedRoutes.slice(0, 6)) {
1567
+ cardLines.push(`- \`${route.path}\` → ${route.component || 'unknown'}${route.lazy ? ' (lazy)' : ''}`);
1568
+ }
1569
+ }
1570
+ // v8.3.126+: DTO 定义展示
1571
+ if (matchedDtos.length > 0) {
1572
+ cardLines.push(`\n### DTO 定义 (${matchedDtos.length} 个匹配)`);
1573
+ for (const dto of matchedDtos.slice(0, 6)) {
1574
+ cardLines.push(`\n**${dto.name}** — \`${dto.filePath}\`:${dto.line}`);
1575
+ if (dto.fields && dto.fields.length > 0) {
1576
+ const fieldStrs = dto.fields.slice(0, 10).map((f) => {
1577
+ let s = `\`${f.name}\`: ${f.type}`;
1578
+ if (f.required === false)
1579
+ s += ' (可选)';
1580
+ if (f.validationRules && f.validationRules.length > 0)
1581
+ s += ` [${f.validationRules.map((v) => `\`${v}\``).join(',')}]`;
1582
+ return s;
1583
+ });
1584
+ cardLines.push(`- 🏷️ 字段: ${fieldStrs.join(', ')}${dto.fields.length > 10 ? ` ...等${dto.fields.length}个` : ''}`);
1585
+ }
1586
+ }
1587
+ }
1588
+ // v8.3.126+: Service 定义展示
1589
+ if (matchedServices.length > 0) {
1590
+ cardLines.push(`\n### Service 定义 (${matchedServices.length} 个匹配)`);
1591
+ for (const svc of matchedServices.slice(0, 6)) {
1592
+ cardLines.push(`\n**${svc.name}** — \`${svc.filePath}\`:${svc.line}`);
1593
+ if (svc.injects && svc.injects.length > 0) {
1594
+ cardLines.push(`- 💉 注入: ${svc.injects.map((i) => `\`${i}\``).join(', ')}`);
1595
+ }
1596
+ if (svc.methods && svc.methods.length > 0) {
1597
+ const methodStrs = svc.methods.slice(0, 8).map((m) => {
1598
+ const params = m.parameters?.map((p) => `${p.name}${p.type ? `:${p.type}` : ''}`).join(', ') || '';
1599
+ return `\`${m.name}(${params})${m.returnType ? ": " + m.returnType : ''}\``;
1600
+ });
1601
+ cardLines.push(`- ⚙️ 方法: ${methodStrs.join(', ')}${svc.methods.length > 8 ? ` ...等${svc.methods.length}个` : ''}`);
1602
+ }
1603
+ }
1604
+ }
1605
+ if (matchedApis.length > 0 || matchedEntities.length > 0 || matchedComponents.length > 0 || matchedRoutes.length > 0 || matchedDtos.length > 0 || matchedServices.length > 0) {
1606
+ extraSpecs.push({
1607
+ name: '📇 结构化代码事实卡片',
1608
+ path: 'structured-data-cards.md',
1609
+ content: cardLines.join('\n'),
1610
+ });
1611
+ const cardChars = cardLines.join('\n').length;
1612
+ logger_1.logger?.info?.(` 📇 结构化卡片: ${matchedApis.length} API + ${matchedEntities.length} Entity + ${matchedComponents.length} Component + ${matchedRoutes.length} Route + ${matchedDtos.length} DTO + ${matchedServices.length} Service (${Math.round(cardChars / 1000)}K 字符)`);
1613
+ structuredCardsInjected = true;
1614
+ }
1615
+ }
1616
+ catch { /* 结构化数据解析失败不阻断 */ }
1617
+ }
1618
+ const codeMatches = await (0, code_scanner_1.findRelevantCode)(searchQuery, 15, undefined, options.iteration, options.task);
1619
+ if (codeMatches.length > 0) {
1620
+ // v8.3.124+: 如果已注入结构化卡片,减少原始源码的文件数和长度
1621
+ const sourceMaxFiles = structuredCardsInjected ? 8 : 15;
1622
+ const sourceMaxBytes = structuredCardsInjected ? 60000 : 100000;
1623
+ const sourceContents = await (0, code_scanner_1.readRelevantSource)(codeMatches, sourceMaxBytes, sourceMaxFiles);
1624
+ const sourceKeys = Object.keys(sourceContents);
1625
+ if (sourceKeys.length > 0) {
1626
+ // v8.3.122+: execute 时注入知识图谱任务上下文
1627
+ // v8.3.125+: 自动检查过期并刷新
1628
+ if (command === 'execute' && options.iteration && options.task) {
1629
+ try {
1630
+ const kg = await (0, knowledge_graph_1.loadFreshKnowledgeGraph)(cwd, options.iteration);
1631
+ if (kg) {
1632
+ const ctx = (0, knowledge_graph_1.getFullTaskContext)(kg, options.task);
1633
+ const ctxLines = ['## 🔗 任务关联上下文(知识图谱)', ''];
1634
+ if (ctx.requirement)
1635
+ ctxLines.push(`- **上游需求**: ${ctx.requirement.title} (${ctx.requirement.file})`);
1636
+ if (ctx.parentTask)
1637
+ ctxLines.push(`- **父任务**: ${ctx.parentTask.title}`);
1638
+ if (ctx.dependsOn.length > 0)
1639
+ ctxLines.push(`- **依赖任务**(需先完成): ${ctx.dependsOn.map(d => d.title).join(', ')}`);
1640
+ if (ctx.downstreamTasks.length > 0)
1641
+ ctxLines.push(`- **下游任务**(依赖本任务): ${ctx.downstreamTasks.map(d => d.title).join(', ')}`);
1642
+ if (ctx.relatedSpecs.length > 0) {
1643
+ ctxLines.push(`- **关联规格文档**:`);
1644
+ for (const spec of ctx.relatedSpecs) {
1645
+ ctxLines.push(` - ${spec.title}: \`${spec.file}\``);
1646
+ }
1647
+ }
1648
+ if (ctx.dependencyChain.length > 0) {
1649
+ ctxLines.push(`- **依赖链路**: ${ctx.dependencyChain.map(c => c.taskName).join(' → ')}`);
1650
+ }
1651
+ extraSpecs.push({
1652
+ name: '🔗 任务关联上下文',
1653
+ path: 'kg-context.md',
1654
+ content: ctxLines.join('\n'),
1655
+ });
1656
+ }
1657
+ }
1658
+ catch { /* 知识图谱加载失败不阻断 */ }
1659
+ }
1660
+ // v8.3.123+: analyze 时注入知识图谱迭代级上下文
1661
+ // v8.3.125+: 自动检查过期并刷新
1662
+ if (command === 'analyze' && options.iteration) {
1663
+ try {
1664
+ const kg = await (0, knowledge_graph_1.loadFreshKnowledgeGraph)(cwd, options.iteration);
1665
+ if (kg) {
1666
+ const compactCtx = (0, context_builder_1.buildCompactContext)(kg, {});
1667
+ if (compactCtx) {
1668
+ extraSpecs.push({
1669
+ name: '🧠 知识图谱摘要',
1670
+ path: 'kg-summary.md',
1671
+ content: compactCtx,
1672
+ });
1673
+ }
1674
+ }
1675
+ }
1676
+ catch { /* 知识图谱加载失败不阻断 */ }
1677
+ }
1678
+ // 按匹配得分排序注入源码,并附加匹配原因
1679
+ const sortedMatches = [...codeMatches].sort((a, b) => b.score - a.score);
1680
+ for (const match of sortedMatches.slice(0, sourceMaxFiles)) {
1681
+ const content = sourceContents[match.file];
1682
+ if (!content)
1683
+ continue;
1684
+ const reasons = [];
1685
+ if (match.score >= 50)
1686
+ reasons.push(`@spec 注释直接关联 (score:${match.score})`);
1687
+ else if (match.score >= 40)
1688
+ reasons.push(`知识图谱 source-file 实体关联 (score:${match.score})`);
1689
+ else if (match.score >= 35)
1690
+ reasons.push(`业务模块 codeEntities 关联 (score:${match.score})`);
1691
+ else if (match.score >= 25)
1692
+ reasons.push(`Spec 技术关键词匹配 (score:${match.score})`);
1693
+ else
1694
+ reasons.push(`关键词匹配 (score:${match.score})`);
1695
+ if (match.exports.length > 0)
1696
+ reasons.push(`导出: ${match.exports.join(', ')}`);
1697
+ if (match.apis.length > 0)
1698
+ reasons.push(`API: ${match.apis.join(', ')}`);
1699
+ extraSpecs.push({
1700
+ name: `🔍 ${(0, path_1.basename)(match.file)}`,
1701
+ path: match.file,
1702
+ content: `## 关联源码: ${match.file}\n> **匹配原因**: ${reasons.join(' | ')}\n\n\`\`\`${match.file.split('.').pop() || 'ts'}\n${content}\n\`\`\``,
1703
+ });
1704
+ }
1705
+ logger_1.logger?.info?.(` 💻 深入源码: ${sourceKeys.length} 个完整文件已注入 (${Math.round(Object.values(sourceContents).reduce((a, c) => a + c.length, 0) / 1000)}K 字符)`);
1706
+ // v8.3.125+: 可选阅读清单 — 让宿主 AI 按需深入读取未预加载的关联文件
1707
+ // 解决 "8文件/60KB 限制导致大型功能读不全" 的问题
1708
+ const unreadMatches = sortedMatches.slice(sourceMaxFiles);
1709
+ if (unreadMatches.length > 0) {
1710
+ const optionalLines = [
1711
+ '## 📖 可选阅读清单(宿主 AI 按需读取)',
1712
+ '> 以下文件与当前任务高度相关,但因 Token 预算限制未完整预加载到上下文中。',
1713
+ '> 如果你的 IDE(Cursor / Claude Code / Windsurf)支持文件读取,可按需打开以下文件深入理解业务逻辑:',
1714
+ '',
1715
+ ];
1716
+ for (const match of unreadMatches.slice(0, 15)) {
1717
+ const reasons = [];
1718
+ if (match.score >= 50)
1719
+ reasons.push('@spec 注释直接关联');
1720
+ else if (match.score >= 40)
1721
+ reasons.push('知识图谱关联');
1722
+ else if (match.score >= 35)
1723
+ reasons.push('业务模块关联');
1724
+ else if (match.score >= 25)
1725
+ reasons.push('技术关键词匹配');
1726
+ else
1727
+ reasons.push('关键词匹配');
1728
+ if (match.exports.length > 0)
1729
+ reasons.push(`导出 ${match.exports.join(', ')}`);
1730
+ if (match.apis.length > 0)
1731
+ reasons.push(`API ${match.apis.join(', ')}`);
1732
+ optionalLines.push(`- \`${match.file}\` — ${reasons.join(' | ')} (score:${match.score})`);
1733
+ }
1734
+ if (unreadMatches.length > 15) {
1735
+ optionalLines.push(`\n... 还有 ${unreadMatches.length - 15} 个关联文件未列出`);
1736
+ }
1737
+ optionalLines.push('\n---');
1738
+ optionalLines.push('💡 **使用方式**: 如果上述预加载源码不足以理解实现细节,请打开对应文件查看完整代码。');
1739
+ extraSpecs.push({
1740
+ name: '📖 可选阅读清单',
1741
+ path: 'optional-reading.md',
1742
+ content: optionalLines.join('\n'),
1743
+ });
1744
+ logger_1.logger?.info?.(` 📖 可选阅读清单: ${unreadMatches.length} 个关联文件(宿主 AI 可按需读取)`);
1745
+ }
1746
+ }
1747
+ }
1748
+ }
1749
+ catch (e) {
1750
+ logger_1.logger?.debug?.('深入源码读取失败:', e);
1751
+ }
1752
+ }
1753
+ // v8.3.125+: execute 时注入 INFO_GAP 自动补充上下文(如果存在)
1754
+ if (command === 'execute' && options.task) {
1755
+ try {
1756
+ const supplementPath = (0, path_1.join)(cwd, '.speccore', 'cache', `info-gap-supplement-${options.task}.md`);
1757
+ if (await (0, fs_extra_1.pathExists)(supplementPath)) {
1758
+ const supplementContent = await (0, fs_extra_1.readFile)(supplementPath, 'utf-8');
1759
+ if (supplementContent.trim().length > 0) {
1760
+ extraSpecs.push({
1761
+ name: '📎 INFO_GAP 自动补充上下文',
1762
+ path: supplementPath,
1763
+ content: `## 📎 上一轮执行发现的信息缺口补充\n\n${supplementContent}`,
1764
+ });
1765
+ logger_1.logger?.info?.(` 📎 已注入 INFO_GAP 补充上下文`);
1766
+ }
1311
1767
  }
1312
1768
  }
1769
+ catch { /* 忽略读取失败 */ }
1313
1770
  }
1314
1771
  // v6.72.0+: execute 时注入 CONSISTENCY_CHECK.md(前后端一致性校验)
1315
1772
  if (command === 'execute' && taskDir) {
@@ -1341,11 +1798,7 @@ async function buildPrompt(command, options) {
1341
1798
  // 加载知识图谱 → 生成任务关联链(< 500 tokens)
1342
1799
  let taskContextStr;
1343
1800
  if (options.task) {
1344
- let graph = await (0, knowledge_graph_1.loadKnowledgeGraph)(cwd);
1345
- const stale = await (0, knowledge_graph_1.isGraphStale)(cwd, options.iteration);
1346
- if (stale) {
1347
- graph = await (0, knowledge_graph_1.refreshKnowledgeGraph)(cwd, options.iteration);
1348
- }
1801
+ const graph = await (0, knowledge_graph_1.loadFreshKnowledgeGraph)(cwd, options.iteration);
1349
1802
  if (graph) {
1350
1803
  taskContextStr = (0, context_builder_1.buildCompactContext)(graph, {
1351
1804
  taskId: options.task,
@@ -1363,7 +1816,7 @@ async function buildPrompt(command, options) {
1363
1816
  if (command === 'execute') {
1364
1817
  const projectInfoMap = await (0, spec_paths_1.parseProjectInfo)();
1365
1818
  if (projectInfoMap.size > 0) {
1366
- const lines = ['## 📂 工程路径(代码输出位置)', '', '| 工程标识 | 工程类型 | 源码路径 | 对应端 |', '| :--- | :--- | :--- | :--- |'];
1819
+ const lines = ['## 📂 工程路径(代码输出位置)', '', '| 工程标识 | 工程类型 | 源码路径 | 对应需求端 |', '| :--- | :--- | :--- | :--- |'];
1367
1820
  for (const [identifier, info] of projectInfoMap) {
1368
1821
  lines.push(`| ${identifier} | ${info.projectType || '-'} | \`${info.srcPath}\` | ${info.platform} |`);
1369
1822
  }
@@ -1462,16 +1915,75 @@ function formatPrompt(prompt, maxTokens = 12000) {
1462
1915
  return result;
1463
1916
  }
1464
1917
  }
1465
- // Level 2: 压缩 extraSpecs(截断到 500 字/文件)
1918
+ // Level 2: 分层预算控制 extraSpecs(v8.3.125+ unit-context-assembler)
1919
+ // 优先级: P1(结构化卡片/源码/INFO_GAP) > P2(KG上下文) > P3(全局规范/一致性检查)
1466
1920
  if (prompt.extraSpecs.length > 0) {
1467
- const slimExtras = prompt.extraSpecs.map(s => ({
1468
- ...s,
1469
- content: s.content.length > 500 ? s.content.slice(0, 500) + '\n> ... (已截断)' : s.content,
1470
- }));
1471
- result = buildPromptText({ ...prompt, extraSpecs: slimExtras });
1921
+ const BUDGET_P1 = 10000; // 高优先级: 结构化卡片、关联源码、INFO_GAP补充
1922
+ const BUDGET_P2 = 5000; // 中优先级: 知识图谱上下文、任务关联链
1923
+ const BUDGET_P3 = 3000; // 低优先级: 全局规范、一致性检查、项目路径
1924
+ function getPriority(spec) {
1925
+ const n = spec.name;
1926
+ if (n.includes('结构化代码事实') || n.includes('INFO_GAP') || n.includes('关联源码'))
1927
+ return 1;
1928
+ if (n.includes('知识图谱') || n.includes('任务关联上下文'))
1929
+ return 2;
1930
+ return 3; // CONSISTENCY_CHECK、工程路径、全局规范等
1931
+ }
1932
+ const sorted = [...prompt.extraSpecs].sort((a, b) => getPriority(a) - getPriority(b));
1933
+ const budgeted = [];
1934
+ let usedP1 = 0, usedP2 = 0, usedP3 = 0;
1935
+ for (const spec of sorted) {
1936
+ const p = getPriority(spec);
1937
+ const len = spec.content.length;
1938
+ let keep = true;
1939
+ let content = spec.content;
1940
+ if (p === 1) {
1941
+ if (usedP1 + len > BUDGET_P1) {
1942
+ const remain = Math.max(0, BUDGET_P1 - usedP1);
1943
+ if (remain < 200) {
1944
+ keep = false;
1945
+ }
1946
+ else {
1947
+ content = content.slice(0, remain) + '\n> ... (P1预算截断)';
1948
+ }
1949
+ }
1950
+ if (keep)
1951
+ usedP1 += content.length;
1952
+ }
1953
+ else if (p === 2) {
1954
+ if (usedP2 + len > BUDGET_P2) {
1955
+ const remain = Math.max(0, BUDGET_P2 - usedP2);
1956
+ if (remain < 200) {
1957
+ keep = false;
1958
+ }
1959
+ else {
1960
+ content = content.slice(0, remain) + '\n> ... (P2预算截断)';
1961
+ }
1962
+ }
1963
+ if (keep)
1964
+ usedP2 += content.length;
1965
+ }
1966
+ else {
1967
+ if (usedP3 + len > BUDGET_P3) {
1968
+ const remain = Math.max(0, BUDGET_P3 - usedP3);
1969
+ if (remain < 200) {
1970
+ keep = false;
1971
+ }
1972
+ else {
1973
+ content = content.slice(0, remain) + '\n> ... (P3预算截断)';
1974
+ }
1975
+ }
1976
+ if (keep)
1977
+ usedP3 += content.length;
1978
+ }
1979
+ if (keep)
1980
+ budgeted.push({ ...spec, content });
1981
+ }
1982
+ result = buildPromptText({ ...prompt, extraSpecs: budgeted });
1472
1983
  tokens = estimateTokens(result);
1473
1984
  if (tokens <= maxTokens) {
1474
- logger_1.logger?.info?.(` 🪶 Prompt 已简化:压缩 extraSpecs 至 500 字/文件`);
1985
+ const removed = prompt.extraSpecs.length - budgeted.length;
1986
+ logger_1.logger?.info?.(` 🪶 Prompt 已简化:分层预算控制 (P1:${usedP1}/${BUDGET_P1} P2:${usedP2}/${BUDGET_P2} P3:${usedP3}/${BUDGET_P3}${removed > 0 ? ` 移除${removed}项` : ''})`);
1475
1987
  return result;
1476
1988
  }
1477
1989
  }
@@ -1601,15 +2113,31 @@ function buildPromptText(prompt) {
1601
2113
  /**
1602
2114
  * 解析 AI 返回的 JSON 文件列表
1603
2115
  */
2116
+ /** v8.3.124+: 解析 AI 响应中的 INFO_GAP 标记 */
2117
+ function extractInfoGaps(response) {
2118
+ const gaps = [];
2119
+ const regex = /\[INFO_GAP:\s*([^\]]+)\]/g;
2120
+ let match;
2121
+ while ((match = regex.exec(response)) !== null) {
2122
+ gaps.push(match[1].trim());
2123
+ }
2124
+ return [...new Set(gaps)]; // 去重
2125
+ }
1604
2126
  function parseAiResponse(response) {
2127
+ // 先提取 INFO_GAP(即使 JSON 解析失败也能捕获)
2128
+ const infoGaps = extractInfoGaps(response);
1605
2129
  // 尝试从响应中提取 JSON
1606
2130
  const jsonMatch = response.match(/\{[\s\S]*"files"[\s\S]*\}/);
1607
- if (!jsonMatch)
2131
+ if (!jsonMatch) {
2132
+ // 没有 JSON,但有 INFO_GAP,返回空文件列表 + gaps
2133
+ if (infoGaps.length > 0)
2134
+ return { files: [], infoGaps };
1608
2135
  return null;
2136
+ }
1609
2137
  try {
1610
2138
  const parsed = JSON.parse(jsonMatch[0]);
1611
2139
  if (parsed.files && Array.isArray(parsed.files)) {
1612
- return parsed;
2140
+ return { files: parsed.files, infoGaps: infoGaps.length > 0 ? infoGaps : undefined };
1613
2141
  }
1614
2142
  }
1615
2143
  catch {
@@ -1807,9 +2335,13 @@ async function expandMarkdownLinks(content, baseDir, seenPaths, maxDepth = 2, cu
1807
2335
  if (links.length === 0)
1808
2336
  return content;
1809
2337
  const expansions = [];
2338
+ // v8.3.125+: 收集外链资源,供宿主 AI 按需读取
2339
+ const externalLinks = [];
1810
2340
  for (const link of links) {
1811
- if (isExternalLink(link.path))
2341
+ if (isExternalLink(link.path)) {
2342
+ externalLinks.push(link);
1812
2343
  continue;
2344
+ }
1813
2345
  const resolved = resolveLinkPath(baseDir, link.path);
1814
2346
  if (seenPaths.has(resolved))
1815
2347
  continue;
@@ -1821,9 +2353,9 @@ async function expandMarkdownLinks(content, baseDir, seenPaths, maxDepth = 2, cu
1821
2353
  if (!st.isFile())
1822
2354
  continue;
1823
2355
  let linkContent = await (0, fs_extra_1.readFile)(resolved, 'utf-8');
1824
- // HTML 文件提取文本
2356
+ // v8.3.122+: HTML 原型文件保留完整内容,不再提取纯文本
1825
2357
  if (resolved.endsWith('.html') || resolved.endsWith('.htm')) {
1826
- linkContent = (0, rag_engine_1.extractHtmlText)(linkContent);
2358
+ // 保留原始 HTML(包含 CSS/JS/结构),让 AI 完整解析原型
1827
2359
  }
1828
2360
  if (linkContent.trim().length <= 30)
1829
2361
  continue;
@@ -1835,7 +2367,16 @@ async function expandMarkdownLinks(content, baseDir, seenPaths, maxDepth = 2, cu
1835
2367
  }
1836
2368
  catch { /* 忽略读取失败的链接文件 */ }
1837
2369
  }
1838
- const expanded = content + expansions.join('');
2370
+ let expanded = content + expansions.join('');
2371
+ // v8.3.125+: 在内容末尾附加外链资源清单,提示宿主 AI 按需读取
2372
+ if (externalLinks.length > 0) {
2373
+ const externalSection = [
2374
+ '\n\n---',
2375
+ '📎 **外链资源清单**(CLI 无法直接获取内容,宿主 AI 可按需访问):',
2376
+ ...externalLinks.map(l => `- [${l.text || '链接'}](${l.path})`),
2377
+ ].join('\n');
2378
+ expanded += externalSection;
2379
+ }
1839
2380
  // 递归展开新内容中的链接(深度 + 1)
1840
2381
  if (currentDepth + 1 < maxDepth) {
1841
2382
  return expandMarkdownLinks(expanded, baseDir, seenPaths, maxDepth, currentDepth + 1, maxChars);
@@ -1850,9 +2391,12 @@ async function inlineMarkdownImages(content, baseDir, seenPaths, visionConfig, m
1850
2391
  const inlines = [];
1851
2392
  let visionCallCount = 0;
1852
2393
  const maxVisionCalls = visionConfig?.maxImagesPerPrompt ?? 10;
2394
+ // v8.3.125+: 收集外链图片,供宿主 AI 按需查看
2395
+ const externalImages = [];
1853
2396
  for (const img of images) {
1854
2397
  if (isExternalLink(img.path)) {
1855
- inlines.push(`\n<!-- 图片: ${img.alt || '无描述'} | URL: ${img.path} -->`);
2398
+ externalImages.push(img);
2399
+ inlines.push(`\n<!-- 外链图片: ${img.alt || '无描述'} | URL: ${img.path} -->`);
1856
2400
  continue;
1857
2401
  }
1858
2402
  const resolved = resolveLinkPath(baseDir, img.path);
@@ -1902,6 +2446,14 @@ async function inlineMarkdownImages(content, baseDir, seenPaths, visionConfig, m
1902
2446
  catch { /* ignore */ }
1903
2447
  }
1904
2448
  }
2449
+ // v8.3.125+: 在内容末尾附加外链图片清单,提示宿主 AI 按需查看
2450
+ if (externalImages.length > 0) {
2451
+ inlines.push([
2452
+ '\n\n---',
2453
+ '🖼️ **外链图片清单**(CLI 无法直接获取内容,宿主 AI 可按需查看):',
2454
+ ...externalImages.map(img => `- ${img.alt || '无描述'}: ${img.path}`),
2455
+ ].join('\n'));
2456
+ }
1905
2457
  return content + inlines.join('');
1906
2458
  }
1907
2459
  /**