speccore 8.1.2 → 8.3.2

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 (74) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +24 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/analyze.d.ts +38 -0
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +1107 -135
  7. package/dist/commands/analyze.js.map +1 -1
  8. package/dist/commands/change.d.ts.map +1 -1
  9. package/dist/commands/change.js +28 -0
  10. package/dist/commands/change.js.map +1 -1
  11. package/dist/commands/clarify.d.ts +6 -0
  12. package/dist/commands/clarify.d.ts.map +1 -1
  13. package/dist/commands/clarify.js +172 -14
  14. package/dist/commands/clarify.js.map +1 -1
  15. package/dist/commands/done.d.ts.map +1 -1
  16. package/dist/commands/done.js +312 -3
  17. package/dist/commands/done.js.map +1 -1
  18. package/dist/commands/execute.d.ts.map +1 -1
  19. package/dist/commands/execute.js +112 -2
  20. package/dist/commands/execute.js.map +1 -1
  21. package/dist/commands/iteration/split.d.ts.map +1 -1
  22. package/dist/commands/iteration/split.js +111 -14
  23. package/dist/commands/iteration/split.js.map +1 -1
  24. package/dist/commands/pattern.d.ts +2 -0
  25. package/dist/commands/pattern.d.ts.map +1 -1
  26. package/dist/commands/pattern.js +43 -0
  27. package/dist/commands/pattern.js.map +1 -1
  28. package/dist/commands/plan.d.ts.map +1 -1
  29. package/dist/commands/plan.js +99 -1
  30. package/dist/commands/plan.js.map +1 -1
  31. package/dist/commands/workspace.d.ts +20 -0
  32. package/dist/commands/workspace.d.ts.map +1 -0
  33. package/dist/commands/workspace.js +142 -0
  34. package/dist/commands/workspace.js.map +1 -0
  35. package/dist/core/ask-engine.d.ts.map +1 -1
  36. package/dist/core/ask-engine.js +18 -0
  37. package/dist/core/ask-engine.js.map +1 -1
  38. package/dist/core/intent-recognition.d.ts +7 -1
  39. package/dist/core/intent-recognition.d.ts.map +1 -1
  40. package/dist/core/intent-recognition.js +33 -3
  41. package/dist/core/intent-recognition.js.map +1 -1
  42. package/dist/core/knowledge-graph.d.ts +6 -1
  43. package/dist/core/knowledge-graph.d.ts.map +1 -1
  44. package/dist/core/knowledge-graph.js +268 -7
  45. package/dist/core/knowledge-graph.js.map +1 -1
  46. package/dist/core/pattern-detector.d.ts +26 -0
  47. package/dist/core/pattern-detector.d.ts.map +1 -0
  48. package/dist/core/pattern-detector.js +256 -0
  49. package/dist/core/pattern-detector.js.map +1 -0
  50. package/dist/core/prompt-builder.d.ts.map +1 -1
  51. package/dist/core/prompt-builder.js +40 -7
  52. package/dist/core/prompt-builder.js.map +1 -1
  53. package/dist/core/questions.js +1 -1
  54. package/dist/core/questions.js.map +1 -1
  55. package/dist/core/requirement-clarifier.d.ts +25 -2
  56. package/dist/core/requirement-clarifier.d.ts.map +1 -1
  57. package/dist/core/requirement-clarifier.js +205 -9
  58. package/dist/core/requirement-clarifier.js.map +1 -1
  59. package/dist/core/spec-skeleton.d.ts.map +1 -1
  60. package/dist/core/spec-skeleton.js +125 -25
  61. package/dist/core/spec-skeleton.js.map +1 -1
  62. package/dist/core/unit-context-assembler.d.ts +50 -0
  63. package/dist/core/unit-context-assembler.d.ts.map +1 -0
  64. package/dist/core/unit-context-assembler.js +442 -0
  65. package/dist/core/unit-context-assembler.js.map +1 -0
  66. package/dist/core/verify-engine.d.ts +4 -3
  67. package/dist/core/verify-engine.d.ts.map +1 -1
  68. package/dist/core/verify-engine.js +623 -24
  69. package/dist/core/verify-engine.js.map +1 -1
  70. package/dist/core/workspace-manager.d.ts +62 -0
  71. package/dist/core/workspace-manager.d.ts.map +1 -0
  72. package/dist/core/workspace-manager.js +175 -0
  73. package/dist/core/workspace-manager.js.map +1 -0
  74. package/package.json +1 -1
@@ -33,7 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.UNITS_CACHE_DIR = void 0;
36
37
  exports.analyzeCommand = analyzeCommand;
38
+ exports.extractIterationUnits = extractIterationUnits;
39
+ exports.saveUnitAnalysis = saveUnitAnalysis;
40
+ exports.loadUnitAnalyses = loadUnitAnalyses;
41
+ exports.detectPendingUnits = detectPendingUnits;
42
+ exports.consolidateUnitAnalyses = consolidateUnitAnalyses;
43
+ exports.buildUnitAnalysisPrompt = buildUnitAnalysisPrompt;
37
44
  /**
38
45
  * analyze — 统一分析命令
39
46
  *
@@ -61,6 +68,7 @@ const index_guard_1 = require("../core/index-guard");
61
68
  const spec_paths_1 = require("../core/spec-paths");
62
69
  const spec_skeleton_1 = require("../core/spec-skeleton");
63
70
  const unified_retrieval_1 = require("../core/unified-retrieval");
71
+ const unit_context_assembler_1 = require("../core/unit-context-assembler");
64
72
  const pipeline_engine_1 = require("../core/pipeline-engine");
65
73
  const change_detection_1 = require("../core/change-detection");
66
74
  const streaming_analyzer_1 = require("../core/streaming-analyzer");
@@ -79,8 +87,12 @@ async function analyzeCommand(options) {
79
87
  if (options.scope === 'global' && !options.iteration) {
80
88
  options.iteration = 'GLOBAL';
81
89
  }
82
- // ── --full / --phase 模式: 委托给 synthesizeCommand(原 synthesize 命令) ──
83
- if (options.full || options.phase) {
90
+ // ── --full 模式: 委托给 synthesizeCommand(原 synthesize 命令) ──
91
+ // v8.3.1+ 修复:--phase 不再委托给 synthesize,analyze 的 Phase 1/2 由 analyze 自己处理
92
+ // 原因:analyze --phase 2 应该生成端级文档({端}/TECH.md/DEV_GUIDE.md),
93
+ // 而 synthesize --phase 2 是跨端关系提取(CROSS_PLATFORM.md/ARCHITECTURE.md),
94
+ // 两者完全不同,委托会导致用户永远无法执行 analyze 的 Phase 2
95
+ if (options.full) {
84
96
  const { synthesizeCommand } = await Promise.resolve().then(() => __importStar(require('./synthesize')));
85
97
  return synthesizeCommand({
86
98
  iteration: options.iteration,
@@ -351,6 +363,103 @@ async function analyzeCommand(options) {
351
363
  logger_1.logger.info(` speccore reindex # 完整重建索引`);
352
364
  return;
353
365
  }
366
+ // ── v8.2.0+: 功能单元聚焦分析模式(--extract-units / --unit / --consolidate / --resume-units)──
367
+ if (options.extractUnits || options.unit || options.consolidate || options.resumeUnits) {
368
+ const iter = options.iteration || await (0, context_1.getDefaultIteration)();
369
+ if (!iter) {
370
+ logger_1.logger.error('请指定迭代: -I <iteration>');
371
+ return;
372
+ }
373
+ const iterDir = await (0, context_1.getIterationDir)(iter);
374
+ // Step 1: 提取功能单元清单
375
+ let units = [];
376
+ const unitsCachePath = (0, path_1.join)(process.cwd(), exports.UNITS_CACHE_DIR, iter, 'units.json');
377
+ if (await (0, fs_extra_1.pathExists)(unitsCachePath)) {
378
+ try {
379
+ units = JSON.parse(await (0, fs_extra_2.readFile)(unitsCachePath, 'utf-8'));
380
+ logger_1.logger.info(`📋 已加载功能单元清单: ${units.length} 个`);
381
+ }
382
+ catch { /* fall through to re-extract */ }
383
+ }
384
+ if (units.length === 0 || options.extractUnits) {
385
+ units = await extractIterationUnits(iterDir);
386
+ await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(unitsCachePath));
387
+ await (0, fs_extra_1.writeFile)(unitsCachePath, JSON.stringify(units, null, 2));
388
+ logger_1.logger.info(`📋 功能单元清单已提取: ${units.length} 个`);
389
+ for (const u of units) {
390
+ logger_1.logger.info(` ${u.id}: ${u.name} (${u.source}#L${u.sourceRange.startLine})`);
391
+ }
392
+ if (options.extractUnits) {
393
+ logger_1.logger.info('');
394
+ logger_1.logger.info('📋 下一步: 逐个分析功能单元');
395
+ logger_1.logger.info(` speccore analyze --prompt -I ${iter} --unit M-01`);
396
+ return;
397
+ }
398
+ }
399
+ // Step 2: 汇总模式(--consolidate)
400
+ if (options.consolidate) {
401
+ const analyses = await loadUnitAnalyses(iter);
402
+ if (analyses.length === 0) {
403
+ logger_1.logger.warn('⚠️ 尚未完成任何单元分析,请先运行 --unit 模式');
404
+ logger_1.logger.info(` speccore analyze --prompt -I ${iter} --unit M-01`);
405
+ return;
406
+ }
407
+ const report = await consolidateUnitAnalyses(iter, units, analyses);
408
+ if (options.prompt) {
409
+ process.stdout.write(`[SPECCORE_PROMPT]\n${report}\n\n## 操作选项\n\n**选项 A:确认通过,写入所有文档**\n\`\`\`bash\nspeccore analyze --apply '[DOC:...]' -I ${iter}\n\`\`\`\n`);
410
+ process.exitCode = 10;
411
+ return;
412
+ }
413
+ if (options.apply) {
414
+ // 直接通过 --apply 的 [DOC:xxx] 处理逻辑写入
415
+ logger_1.logger.info(`📋 汇总报告已生成,共 ${analyses.length}/${units.length} 个单元`);
416
+ return;
417
+ }
418
+ logger_1.logger.info(`📋 汇总报告(${analyses.length}/${units.length} 个单元)`);
419
+ logger_1.logger.info(' 使用 --prompt 查看完整报告,或 --apply 直接写入');
420
+ return;
421
+ }
422
+ // Step 3: 断点续跑(--resume-units)
423
+ let targetUnit;
424
+ if (options.resumeUnits) {
425
+ const pending = await detectPendingUnits(iter, units);
426
+ if (pending.length === 0) {
427
+ logger_1.logger.success('🎉 所有功能单元分析已完成!');
428
+ logger_1.logger.info(' 运行 --consolidate 汇总为统一报告');
429
+ logger_1.logger.info(` speccore analyze --prompt -I ${iter} --consolidate`);
430
+ return;
431
+ }
432
+ targetUnit = pending[0];
433
+ logger_1.logger.info(`🔄 断点续跑: ${targetUnit.id} ${targetUnit.name}(剩余 ${pending.length} 个)`);
434
+ }
435
+ // Step 4: 分析单个单元(--unit M-01)
436
+ if (options.unit) {
437
+ targetUnit = units.find(u => u.id === options.unit);
438
+ if (!targetUnit) {
439
+ logger_1.logger.error(`未找到功能单元: ${options.unit}`);
440
+ logger_1.logger.info(` 可用单元: ${units.map(u => u.id).join(', ')}`);
441
+ return;
442
+ }
443
+ }
444
+ if (targetUnit && options.prompt) {
445
+ const prompt = await buildUnitAnalysisPrompt(iterDir, iter, targetUnit, units);
446
+ process.stdout.write(`[SPECCORE_PROMPT]\n${prompt}`);
447
+ process.exitCode = 10;
448
+ return;
449
+ }
450
+ if (targetUnit && options.apply) {
451
+ // AI 输出的是 [DOC:xxx] 格式,由 --apply 的 parseDocMarkers 处理
452
+ logger_1.logger.info(`✅ 功能单元 ${targetUnit.id} 分析结果已接收`);
453
+ logger_1.logger.info(' 结果已通过 --apply 的 [DOC:xxx] 解析写入');
454
+ return;
455
+ }
456
+ if (targetUnit) {
457
+ logger_1.logger.info(`📋 功能单元: ${targetUnit.id} ${targetUnit.name}`);
458
+ logger_1.logger.info(` 来源: ${targetUnit.source}#L${targetUnit.sourceRange.startLine}`);
459
+ logger_1.logger.info(' 使用 --prompt 生成分析 Prompt,或 --apply 写入分析结果');
460
+ return;
461
+ }
462
+ }
354
463
  // 模糊匹配端名
355
464
  if (options.platform) {
356
465
  const resolved = await (0, platform_registry_1.resolvePlatform)(options.platform);
@@ -368,6 +477,9 @@ async function analyzeCommand(options) {
368
477
  if (options.scope !== 'global') {
369
478
  await (0, index_guard_1.warnIfIndexStale)(process.cwd(), 'analyze', options.iteration);
370
479
  }
480
+ // v8.3.0+: 需求澄清检测状态(跨代码块共享)
481
+ let needsClarify = false;
482
+ const clarifyTargets = [];
371
483
  // 备份追踪
372
484
  const backups = [];
373
485
  const printBackupSummary = () => {
@@ -417,86 +529,102 @@ async function analyzeCommand(options) {
417
529
  const reqDir = (0, path_1.join)(iterDir, '010-requirements');
418
530
  const specDir = (0, path_1.join)(iterDir, '020-specs');
419
531
  await (0, fs_extra_1.ensureDir)(specDir);
420
- // 收集需求文档
532
+ // v8.2.0+: 需求收集优先从黄金需求目录 020-specs/requirements/ 读取
533
+ // 该目录存放经过 clarify 后的专业需求,作为分析和后续步骤的唯一依据
421
534
  const requirements = [];
422
- const reqIndex = (0, path_1.join)(reqDir, 'INDEX.md');
423
- if (await (0, fs_extra_1.pathExists)(reqIndex))
424
- requirements.push(reqIndex);
425
- const convDir = (0, path_1.join)(reqDir, 'converted');
426
- if (await (0, fs_extra_1.pathExists)(convDir)) {
535
+ const goldenReqDir = (0, path_1.join)(specDir, 'requirements');
536
+ const hasGoldenDocs = await (0, fs_extra_1.pathExists)(goldenReqDir);
537
+ if (hasGoldenDocs) {
427
538
  try {
428
- const files = await (0, fs_extra_2.readdir)(convDir);
429
- for (const f of files.filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f)))
430
- requirements.push((0, path_1.join)(convDir, f));
539
+ const goldenFiles = await (0, fs_extra_2.readdir)(goldenReqDir);
540
+ for (const f of goldenFiles.filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f))) {
541
+ requirements.push((0, path_1.join)(goldenReqDir, f));
542
+ }
431
543
  }
432
544
  catch { }
433
545
  }
434
- const reqRoot = (0, path_1.join)(reqDir, 'REQUIREMENT.md');
435
- if (await (0, fs_extra_1.pathExists)(reqRoot))
436
- requirements.push(reqRoot);
437
- // 收集 features/*/README.md
438
- const featuresDir = (0, path_1.join)(reqDir, 'features');
439
- if (await (0, fs_extra_1.pathExists)(featuresDir)) {
440
- try {
441
- const featureEntries = await (0, fs_extra_2.readdir)(featuresDir, { withFileTypes: true });
442
- for (const fe of featureEntries) {
443
- if (fe.isDirectory() && !fe.name.startsWith('.')) {
444
- const readmePath = (0, path_1.join)(featuresDir, fe.name, 'README.md');
445
- if (await (0, fs_extra_1.pathExists)(readmePath))
446
- requirements.push(readmePath);
546
+ // 如果黄金需求目录为空,回退到原始需求目录 010-requirements/
547
+ if (requirements.length === 0) {
548
+ const reqIndex = (0, path_1.join)(reqDir, 'INDEX.md');
549
+ if (await (0, fs_extra_1.pathExists)(reqIndex))
550
+ requirements.push(reqIndex);
551
+ const convDir = (0, path_1.join)(reqDir, 'converted');
552
+ if (await (0, fs_extra_1.pathExists)(convDir)) {
553
+ try {
554
+ const files = await (0, fs_extra_2.readdir)(convDir);
555
+ for (const f of files.filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f)))
556
+ requirements.push((0, path_1.join)(convDir, f));
557
+ }
558
+ catch { }
559
+ }
560
+ const reqRoot = (0, path_1.join)(reqDir, 'REQUIREMENT.md');
561
+ if (await (0, fs_extra_1.pathExists)(reqRoot))
562
+ requirements.push(reqRoot);
563
+ // 收集 features/*/README.md
564
+ const featuresDir = (0, path_1.join)(reqDir, 'features');
565
+ if (await (0, fs_extra_1.pathExists)(featuresDir)) {
566
+ try {
567
+ const featureEntries = await (0, fs_extra_2.readdir)(featuresDir, { withFileTypes: true });
568
+ for (const fe of featureEntries) {
569
+ if (fe.isDirectory() && !fe.name.startsWith('.')) {
570
+ const readmePath = (0, path_1.join)(featuresDir, fe.name, 'README.md');
571
+ if (await (0, fs_extra_1.pathExists)(readmePath))
572
+ requirements.push(readmePath);
573
+ }
447
574
  }
448
575
  }
576
+ catch { }
449
577
  }
450
- catch { }
451
- }
452
- // 收集 staging/ 下的分类文档(doc2spec --classify 产物,带 type frontmatter)
453
- const stagingDir = (0, path_1.join)(reqDir, 'staging');
454
- if (await (0, fs_extra_1.pathExists)(stagingDir)) {
455
- try {
456
- const stagingFiles = await (0, fs_extra_2.readdir)(stagingDir);
457
- for (const f of stagingFiles.filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f))) {
458
- requirements.push((0, path_1.join)(stagingDir, f));
578
+ // 收集 staging/ 下的分类文档
579
+ const stagingDir = (0, path_1.join)(reqDir, 'staging');
580
+ if (await (0, fs_extra_1.pathExists)(stagingDir)) {
581
+ try {
582
+ const stagingFiles = await (0, fs_extra_2.readdir)(stagingDir);
583
+ for (const f of stagingFiles.filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f))) {
584
+ requirements.push((0, path_1.join)(stagingDir, f));
585
+ }
459
586
  }
587
+ catch { }
460
588
  }
461
- catch { }
462
- }
463
- // 收集类型目录下的文档(bugs/, refactors/, research/ — 扁平 .md 文件)
464
- for (const typeDir of ['bugs', 'refactors', 'research']) {
465
- const typeDirPath = (0, path_1.join)(reqDir, typeDir);
466
- if (!(await (0, fs_extra_1.pathExists)(typeDirPath)))
467
- continue;
468
- try {
469
- const typeFiles = await (0, fs_extra_2.readdir)(typeDirPath);
470
- for (const f of typeFiles.filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f))) {
471
- requirements.push((0, path_1.join)(typeDirPath, f));
589
+ // 收集类型目录下的文档
590
+ for (const typeDir of ['bugs', 'refactors', 'research']) {
591
+ const typeDirPath = (0, path_1.join)(reqDir, typeDir);
592
+ if (!(await (0, fs_extra_1.pathExists)(typeDirPath)))
593
+ continue;
594
+ try {
595
+ const typeFiles = await (0, fs_extra_2.readdir)(typeDirPath);
596
+ for (const f of typeFiles.filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f))) {
597
+ requirements.push((0, path_1.join)(typeDirPath, f));
598
+ }
472
599
  }
600
+ catch { }
473
601
  }
474
- catch { }
602
+ }
603
+ else {
604
+ logger_1.logger.info(` 📋 使用黄金需求目录: 020-specs/requirements/ (${requirements.length} 个文档)`);
475
605
  }
476
606
  if (requirements.length === 0) {
477
607
  logger_1.logger.warn('未找到需求文档,请先导入: speccore doc2spec');
478
608
  return;
479
609
  }
480
- // v6.76.0+: --clarify 模式下检测需求文档专业度
481
- if (options.clarify) {
610
+ // v8.3.0+: 需求澄清作为必须过程(除非 --skip-clarify
611
+ if (!options.skipClarify) {
482
612
  const { detectProfessionalLevel } = await Promise.resolve().then(() => __importStar(require('../core/requirement-clarifier')));
483
- let lowQualityCount = 0;
484
613
  for (const reqPath of requirements) {
485
614
  const reqContent = await (0, fs_extra_2.readFile)(reqPath, 'utf-8');
486
615
  const level = detectProfessionalLevel(reqContent);
487
616
  if (level !== 'high') {
488
- lowQualityCount++;
617
+ needsClarify = true;
618
+ clarifyTargets.push({ path: reqPath, level });
489
619
  logger_1.logger.warn(` ⚠️ 需求文档质量${level.toUpperCase()}: ${reqPath.replace(iterDir + '/', '')}`);
490
- logger_1.logger.info(` 💡 建议: speccore clarify --from "${reqPath}" --to ${iter}`);
491
620
  }
492
621
  }
493
- if (lowQualityCount > 0) {
622
+ if (needsClarify) {
494
623
  logger_1.logger.info('');
495
- logger_1.logger.info(`📋 ${lowQualityCount}/${requirements.length} 个需求文档需要澄清整理`);
496
- logger_1.logger.info(' 选项 1: 先执行 clarify 整理需求,再重新 analyze');
497
- logger_1.logger.info(' 选项 2: 继续使用当前文档分析(加 --force 跳过检测)');
624
+ logger_1.logger.info(`📋 ${clarifyTargets.length}/${requirements.length} 个需求文档需要澄清整理`);
625
+ logger_1.logger.info(' 将在分析流程中自动注入需求澄清阶段(Phase 0)');
626
+ logger_1.logger.info(' 如需跳过: speccore analyze -I ${iter} --skip-clarify');
498
627
  logger_1.logger.info('');
499
- // 不阻断,但在 prompt 中注入澄清指令
500
628
  }
501
629
  }
502
630
  // 【v6.40.2 修复】--auto 不再跳过 AI,而是自动生成 prompt 让宿主 AI 执行专业分析
@@ -752,6 +880,63 @@ async function analyzeCommand(options) {
752
880
  return;
753
881
  }
754
882
  }
883
+ // v8.3.0+: 解析 [CLARIFY:xxx] 标记 — 需求澄清是强制前置步骤
884
+ const clarifyBlocks = parseClarifyMarkers(options.apply);
885
+ if (clarifyBlocks.size > 0 && !isGlobalScope) {
886
+ logger_1.logger.info(`📋 检测到 ${clarifyBlocks.size} 个澄清文档,先写入黄金需求目录...`);
887
+ const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
888
+ await (0, fs_extra_1.ensureDir)(goldenDir);
889
+ for (const [filename, content] of clarifyBlocks) {
890
+ const { parseClarifiedRequirement, buildClarifiedHeader } = await Promise.resolve().then(() => __importStar(require('../core/requirement-clarifier')));
891
+ const { content: cleaned } = parseClarifiedRequirement(content);
892
+ const header = buildClarifiedHeader(filename);
893
+ const finalContent = header + cleaned;
894
+ const fp = (0, path_1.join)(goldenDir, (0, path_1.basename)(filename));
895
+ await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(fp));
896
+ await (0, fs_extra_1.writeFile)(fp, finalContent);
897
+ logger_1.logger.info(` ✅ 澄清文档已写入: ${fp.replace(process.cwd() + '/', '')}`);
898
+ }
899
+ logger_1.logger.info('');
900
+ }
901
+ // v8.3.0+: 需求澄清验证 — 如果需求质量不足但未澄清,拒绝写入
902
+ if (!isGlobalScope && !isTaskLevel && !options.skipClarify) {
903
+ const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
904
+ const hasClarifiedDocs = await (0, fs_extra_1.pathExists)(goldenDir) && (await (0, fs_extra_2.readdir)(goldenDir)).some((f) => f.endsWith('.md'));
905
+ if (!hasClarifiedDocs && clarifyBlocks.size === 0) {
906
+ // 重新检测需求质量
907
+ const { detectProfessionalLevel } = await Promise.resolve().then(() => __importStar(require('../core/requirement-clarifier')));
908
+ const reqDir = (0, path_1.join)(iterDir, '010-requirements');
909
+ const requirements = [];
910
+ const reqIndex = (0, path_1.join)(reqDir, 'INDEX.md');
911
+ if (await (0, fs_extra_1.pathExists)(reqIndex))
912
+ requirements.push(reqIndex);
913
+ const convDir = (0, path_1.join)(reqDir, 'converted');
914
+ if (await (0, fs_extra_1.pathExists)(convDir)) {
915
+ const files = await (0, fs_extra_2.readdir)(convDir);
916
+ for (const f of files.filter((f) => f.endsWith('.md')))
917
+ requirements.push((0, path_1.join)(convDir, f));
918
+ }
919
+ let lowQualityCount = 0;
920
+ for (const reqPath of requirements) {
921
+ const reqContent = await (0, fs_extra_2.readFile)(reqPath, 'utf-8');
922
+ const level = detectProfessionalLevel(reqContent);
923
+ if (level !== 'high')
924
+ lowQualityCount++;
925
+ }
926
+ if (lowQualityCount > 0) {
927
+ logger_1.logger.error('❌ 需求澄清未完成,拒绝写入分析结果');
928
+ logger_1.logger.error(` ${lowQualityCount} 个需求文档质量不足,必须先澄清为 PRD`);
929
+ logger_1.logger.info('');
930
+ logger_1.logger.info('解决方式:');
931
+ logger_1.logger.info(' 1. 在 AI 输出中先包含 [CLARIFY:requirements/xxx.md] 标记的澄清文档');
932
+ logger_1.logger.info(' 2. 或先手动运行: speccore clarify --from "<需求文件>" --to ' + options.iteration + ' --prompt');
933
+ logger_1.logger.info('');
934
+ logger_1.logger.info(' 如需跳过澄清(不推荐):');
935
+ logger_1.logger.info(` speccore analyze --apply ... -I ${options.iteration} --skip-clarify`);
936
+ return;
937
+ }
938
+ }
939
+ }
755
940
  // 支持 JSON 多文档写入
756
941
  if (options.apply.startsWith('{')) {
757
942
  try {
@@ -865,19 +1050,35 @@ async function analyzeCommand(options) {
865
1050
  const validPlatforms = new Set([spec_paths_1.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_1.parsePlatformList)())]);
866
1051
  let skippedCount = 0;
867
1052
  for (const [filename, content] of Object.entries(docs)) {
868
- // v6.80.0+: 010-requirements/ 路径处理(clarify 结果写入)
1053
+ // v8.2.0+: 需求文档写入黄金需求目录 020-specs/requirements/
1054
+ // 支持 010-requirements/ 路径(向后兼容,自动重定向)
1055
+ if (filename.startsWith('020-specs/requirements/')) {
1056
+ const reqFilePath = filename.slice('020-specs/requirements/'.length);
1057
+ const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
1058
+ const fp = (0, path_1.join)(goldenDir, reqFilePath);
1059
+ await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(fp));
1060
+ let finalContent = content;
1061
+ if (reqFilePath.startsWith('clarified-') && !content.startsWith('---')) {
1062
+ finalContent = (0, requirement_clarifier_1.buildClarifiedHeader)(reqFilePath) + content;
1063
+ }
1064
+ await (0, fs_extra_1.writeFile)(fp, finalContent);
1065
+ logger_1.logger.info(` 📝 黄金需求已写入: 020-specs/requirements/${reqFilePath}`);
1066
+ count++;
1067
+ continue;
1068
+ }
1069
+ // v6.80.0+: 向后兼容 — 010-requirements/ 路径自动重定向到黄金需求目录
869
1070
  if (filename.startsWith('010-requirements/')) {
870
1071
  const reqFilePath = filename.slice('010-requirements/'.length);
871
- const reqDir = (0, path_1.join)(iterDir, '010-requirements');
872
- const fp = (0, path_1.join)(reqDir, reqFilePath);
1072
+ // v8.2.0+: clarify 结果写入 020-specs/requirements/ 而非 010-requirements/converted/
1073
+ const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
1074
+ const fp = (0, path_1.join)(goldenDir, reqFilePath.replace(/^converted\//, ''));
873
1075
  await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(fp));
874
- // 为 clarified 文件添加头部元信息
875
1076
  let finalContent = content;
876
1077
  if (reqFilePath.startsWith('converted/clarified-') && !content.startsWith('---')) {
877
1078
  finalContent = (0, requirement_clarifier_1.buildClarifiedHeader)(reqFilePath) + content;
878
1079
  }
879
1080
  await (0, fs_extra_1.writeFile)(fp, finalContent);
880
- logger_1.logger.info(` 📝 需求文档已写入: ${reqFilePath}`);
1081
+ logger_1.logger.info(` 📝 黄金需求已写入: 020-specs/requirements/${reqFilePath.replace(/^converted\//, '')}`);
881
1082
  count++;
882
1083
  continue;
883
1084
  }
@@ -942,6 +1143,60 @@ async function analyzeCommand(options) {
942
1143
  logger_1.logger.warn(`⚠️ 共跳过 ${skippedCount} 个非法目录的文档,请检查 AI 输出是否包含非端名目录`);
943
1144
  }
944
1145
  logger_1.logger.success(`✅ ${count} 个 Spec 文档已写入 020-specs/`);
1146
+ // v8.3.1+: 空模板检测 — 写入后立即检查是否有文档仍为骨架状态
1147
+ const emptySkeletonDocs = [];
1148
+ const emptyTableDocs = [];
1149
+ try {
1150
+ const { readdir, readFile } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
1151
+ const specDir = (0, path_1.join)(iterDir, '020-specs');
1152
+ const checkDir = async (dir, prefix = '') => {
1153
+ const entries = await readdir(dir, { withFileTypes: true });
1154
+ for (const entry of entries) {
1155
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
1156
+ const fullPath = (0, path_1.join)(dir, entry.name);
1157
+ if (entry.isDirectory()) {
1158
+ await checkDir(fullPath, relPath);
1159
+ }
1160
+ else if (entry.name.endsWith('.md')) {
1161
+ const content = await readFile(fullPath, 'utf-8');
1162
+ if (content.includes('<!-- SPEC-SKELETON -->')) {
1163
+ emptySkeletonDocs.push(relPath);
1164
+ }
1165
+ // 检测空表格:| 表头 | 表头 | 后没有数据行
1166
+ const tableMatches = content.match(/\|[^\n]+\|\n\|[-:\s|]+\|/g);
1167
+ if (tableMatches) {
1168
+ for (const table of tableMatches) {
1169
+ const tableEnd = content.indexOf(table) + table.length;
1170
+ const afterTable = content.slice(tableEnd, tableEnd + 200);
1171
+ // 如果表格后面紧跟空行或另一个表格/标题,说明没有数据行
1172
+ if (!afterTable.trim().startsWith('|')) {
1173
+ emptyTableDocs.push(relPath);
1174
+ break;
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+ }
1180
+ };
1181
+ await checkDir(specDir);
1182
+ if (emptySkeletonDocs.length > 0) {
1183
+ logger_1.logger.warn('');
1184
+ logger_1.logger.warn(`⚠️ 检测到 ${emptySkeletonDocs.length} 个文档仍为骨架状态(含 <!-- SPEC-SKELETON -->):`);
1185
+ for (const d of emptySkeletonDocs)
1186
+ logger_1.logger.warn(` ❌ ${d}`);
1187
+ logger_1.logger.info(' 💡 建议重新执行 analyze 补充这些文档');
1188
+ }
1189
+ if (emptyTableDocs.length > 0) {
1190
+ logger_1.logger.warn('');
1191
+ logger_1.logger.warn(`⚠️ 检测到 ${emptyTableDocs.length} 个文档含空表格(只有表头无数据):`);
1192
+ for (const d of emptyTableDocs)
1193
+ logger_1.logger.warn(` ❌ ${d}`);
1194
+ logger_1.logger.info(' 💡 建议重新执行 analyze 补充表格数据');
1195
+ }
1196
+ }
1197
+ catch (e) {
1198
+ logger_1.logger.debug(`空模板检测失败(非关键): ${e.message}`);
1199
+ }
945
1200
  // v6.90.0+: 事后校验——检测并清理 AI 绕过 --apply 创建的非法目录/文件
946
1201
  await sanitizeSpecDirectories(iterDir);
947
1202
  // v7.4.0+: 从 AI 输出中提取疑问并持久化
@@ -1022,6 +1277,227 @@ async function analyzeCommand(options) {
1022
1277
  // fallback to single-file mode
1023
1278
  }
1024
1279
  }
1280
+ // v8.2.0+: [DOC:xxx] 标记解析 —— 统一报告自动拆分
1281
+ // AI 输出格式: [DOC:REQUIREMENT.md]...内容...[DOC:TECH.md]...内容...
1282
+ const docBlocks = parseDocMarkers(options.apply);
1283
+ if (docBlocks.size > 1) {
1284
+ // 将 [DOC:xxx] 格式转换为 JSON 格式复用现有写入逻辑
1285
+ const docs = {};
1286
+ for (const [docName, content] of docBlocks) {
1287
+ docs[docName] = content;
1288
+ }
1289
+ // 构造一个 faux JSON 字符串让后面的 JSON 解析逻辑处理
1290
+ options.apply = JSON.stringify(docs);
1291
+ // 重新进入 JSON 处理分支(递归一次)
1292
+ try {
1293
+ const docs = JSON.parse(options.apply);
1294
+ let count = 0;
1295
+ if (isTaskLevel && taskDir) {
1296
+ const targetSubDir = options.platform && !isTaskLevel ? options.platform : '00-specs';
1297
+ const taskSpecDir = (0, path_1.join)(taskDir, targetSubDir);
1298
+ await (0, fs_extra_1.ensureDir)(taskSpecDir);
1299
+ for (const [filename, content] of Object.entries(docs)) {
1300
+ const fp = (0, path_1.join)(taskSpecDir, filename);
1301
+ if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
1302
+ logger_1.logger.info(` ⏭️ 跳过: ${filename}`);
1303
+ continue;
1304
+ }
1305
+ const bk = await (0, task_utils_1.backupWithTimestamp)(fp);
1306
+ if (bk) {
1307
+ backups.push(bk);
1308
+ logger_1.logger.info(` 📦 ${filename} 旧版已备份: ${bk.split('/').pop()}`);
1309
+ }
1310
+ await (0, fs_extra_1.writeFile)(fp, content);
1311
+ count++;
1312
+ }
1313
+ const platformLabel = options.platform ? `/${options.platform}` : '';
1314
+ logger_1.logger.success(`✅ ${count} 个 Spec 文档已写入 ${options.task}${platformLabel}/(任务级,迭代基线不变)`);
1315
+ }
1316
+ else if (isGlobalScope) {
1317
+ const globalBaseDir = (0, path_1.join)(process.cwd(), '.speccore', 'GLOBAL');
1318
+ await (0, fs_extra_1.ensureDir)(globalBaseDir);
1319
+ const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
1320
+ for (const [filename, content] of Object.entries(docs)) {
1321
+ if (filename.startsWith('PATTERNS/')) {
1322
+ const patternsDir = (0, path_1.join)(process.cwd(), '.speccore', 'PATTERNS');
1323
+ await (0, fs_extra_1.ensureDir)(patternsDir);
1324
+ const patternFile = filename.slice('PATTERNS/'.length);
1325
+ const fp = (0, path_1.join)(patternsDir, patternFile);
1326
+ let existing = '';
1327
+ if (await (0, fs_extra_1.pathExists)(fp))
1328
+ existing = await (0, fs_extra_2.readFile)(fp, 'utf-8');
1329
+ const merged = existing ? `${existing}\n\n---\n\n${content}` : content;
1330
+ await (0, fs_extra_1.writeFile)(fp, merged);
1331
+ logger_1.logger.info(` 🧩 PATTERN 已追加: ${patternFile}`);
1332
+ count++;
1333
+ continue;
1334
+ }
1335
+ let targetDir;
1336
+ let targetFilename;
1337
+ if (filename.includes('/')) {
1338
+ const parts = filename.split('/');
1339
+ if (parts[0] === 'platforms' || parts[0] === 'requirements') {
1340
+ targetDir = (0, path_1.join)(globalBaseDir, ...parts.slice(0, -1));
1341
+ targetFilename = parts[parts.length - 1];
1342
+ }
1343
+ else {
1344
+ targetDir = (0, path_1.join)(globalBaseDir, 'platforms', parts[0]);
1345
+ targetFilename = parts[parts.length - 1];
1346
+ }
1347
+ }
1348
+ else if (filename === 'REQUIREMENT.md') {
1349
+ targetDir = (0, path_1.join)(globalBaseDir, 'requirements');
1350
+ targetFilename = filename;
1351
+ }
1352
+ else if (globalSet.has(filename)) {
1353
+ targetDir = (0, path_1.join)(globalBaseDir, 'overview');
1354
+ targetFilename = filename;
1355
+ }
1356
+ else {
1357
+ targetDir = (0, path_1.join)(globalBaseDir, 'overview');
1358
+ targetFilename = filename;
1359
+ }
1360
+ await (0, fs_extra_1.ensureDir)(targetDir);
1361
+ const fp = (0, path_1.join)(targetDir, targetFilename);
1362
+ if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
1363
+ logger_1.logger.info(` ⏭️ 跳过: ${filename}`);
1364
+ continue;
1365
+ }
1366
+ const bk = await (0, task_utils_1.backupWithTimestamp)(fp);
1367
+ if (bk) {
1368
+ backups.push(bk);
1369
+ logger_1.logger.info(` 📦 ${filename} 旧版已备份: ${bk.split('/').pop()}`);
1370
+ }
1371
+ await (0, fs_extra_1.writeFile)(fp, content);
1372
+ count++;
1373
+ }
1374
+ logger_1.logger.success(`✅ ${count} 个全局文档已写入 .speccore/GLOBAL/`);
1375
+ }
1376
+ else {
1377
+ const specDir = (0, path_1.join)(iterDir, '020-specs');
1378
+ await (0, fs_extra_1.ensureDir)(specDir);
1379
+ const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
1380
+ const validPlatforms = new Set([spec_paths_1.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_1.parsePlatformList)())]);
1381
+ let skippedCount = 0;
1382
+ for (const [filename, content] of Object.entries(docs)) {
1383
+ // v8.2.0+: 黄金需求目录支持
1384
+ if (filename.startsWith('020-specs/requirements/')) {
1385
+ const reqFilePath = filename.slice('020-specs/requirements/'.length);
1386
+ const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
1387
+ const fp = (0, path_1.join)(goldenDir, reqFilePath);
1388
+ await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(fp));
1389
+ let finalContent = content;
1390
+ if (reqFilePath.startsWith('clarified-') && !content.startsWith('---')) {
1391
+ finalContent = (0, requirement_clarifier_1.buildClarifiedHeader)(reqFilePath) + content;
1392
+ }
1393
+ await (0, fs_extra_1.writeFile)(fp, finalContent);
1394
+ logger_1.logger.info(` 📝 黄金需求已写入: 020-specs/requirements/${reqFilePath}`);
1395
+ count++;
1396
+ continue;
1397
+ }
1398
+ // 向后兼容:010-requirements/ 自动重定向到黄金需求目录
1399
+ if (filename.startsWith('010-requirements/')) {
1400
+ const reqFilePath = filename.slice('010-requirements/'.length);
1401
+ const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
1402
+ const fp = (0, path_1.join)(goldenDir, reqFilePath.replace(/^converted\//, ''));
1403
+ await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(fp));
1404
+ let finalContent = content;
1405
+ if (reqFilePath.startsWith('converted/clarified-') && !content.startsWith('---')) {
1406
+ finalContent = (0, requirement_clarifier_1.buildClarifiedHeader)(reqFilePath) + content;
1407
+ }
1408
+ await (0, fs_extra_1.writeFile)(fp, finalContent);
1409
+ logger_1.logger.info(` 📝 黄金需求已写入: 020-specs/requirements/${reqFilePath.replace(/^converted\//, '')}`);
1410
+ count++;
1411
+ continue;
1412
+ }
1413
+ if (filename.startsWith('PATTERNS/')) {
1414
+ const patternsDir = (0, path_1.join)(process.cwd(), '.speccore', 'PATTERNS');
1415
+ await (0, fs_extra_1.ensureDir)(patternsDir);
1416
+ const patternFile = filename.slice('PATTERNS/'.length);
1417
+ const fp = (0, path_1.join)(patternsDir, patternFile);
1418
+ let existing = '';
1419
+ if (await (0, fs_extra_1.pathExists)(fp))
1420
+ existing = await (0, fs_extra_2.readFile)(fp, 'utf-8');
1421
+ const merged = existing ? `${existing}\n\n---\n\n${content}` : content;
1422
+ await (0, fs_extra_1.writeFile)(fp, merged);
1423
+ logger_1.logger.info(` 🧩 PATTERN 已追加: ${patternFile}`);
1424
+ count++;
1425
+ continue;
1426
+ }
1427
+ let cleanFilename = filename;
1428
+ if (cleanFilename.startsWith(`${spec_paths_1.GLOBAL_SPECS_DIR}/`)) {
1429
+ cleanFilename = cleanFilename.slice(`${spec_paths_1.GLOBAL_SPECS_DIR}/`.length);
1430
+ }
1431
+ if (cleanFilename.includes('/')) {
1432
+ const prefix = cleanFilename.split('/')[0];
1433
+ if (!validPlatforms.has(prefix)) {
1434
+ logger_1.logger.warn(` ⚠️ 文件名含未知目录前缀 "${prefix}/",已自动剥离: ${cleanFilename}`);
1435
+ cleanFilename = cleanFilename.split('/').pop();
1436
+ }
1437
+ }
1438
+ const platformDir = cleanFilename.includes('/') ? cleanFilename.split('/')[0] : null;
1439
+ if (platformDir && !validPlatforms.has(platformDir)) {
1440
+ logger_1.logger.warn(` ⚠️ 跳过非法端目录: ${platformDir}(文件: ${cleanFilename})`);
1441
+ skippedCount++;
1442
+ continue;
1443
+ }
1444
+ const targetDir = globalSet.has(cleanFilename)
1445
+ ? (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR)
1446
+ : options.platform ? (0, path_1.join)(specDir, options.platform) : specDir;
1447
+ await (0, fs_extra_1.ensureDir)(targetDir);
1448
+ const fp = (0, path_1.join)(targetDir, cleanFilename);
1449
+ if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
1450
+ logger_1.logger.info(` ⏭️ 跳过: ${filename}`);
1451
+ continue;
1452
+ }
1453
+ const bk = await (0, task_utils_1.backupWithTimestamp)(fp);
1454
+ if (bk) {
1455
+ backups.push(bk);
1456
+ logger_1.logger.info(` 📦 ${filename} 旧版已备份: ${bk.split('/').pop()}`);
1457
+ }
1458
+ await (0, fs_extra_1.writeFile)(fp, content);
1459
+ count++;
1460
+ }
1461
+ if (skippedCount > 0) {
1462
+ logger_1.logger.warn(`⚠️ 共跳过 ${skippedCount} 个非法目录的文档,请检查 AI 输出是否包含非端名目录`);
1463
+ }
1464
+ logger_1.logger.success(`✅ ${count} 个 Spec 文档已写入 020-specs/`);
1465
+ await sanitizeSpecDirectories(iterDir);
1466
+ }
1467
+ // v8.2.0+: 单元分析结果保存到缓存(供 --consolidate 汇总使用)
1468
+ if (options.unit && options.iteration) {
1469
+ try {
1470
+ const unitsCachePath = (0, path_1.join)(process.cwd(), exports.UNITS_CACHE_DIR, options.iteration, 'units.json');
1471
+ let unitName = options.unit;
1472
+ if (await (0, fs_extra_1.pathExists)(unitsCachePath)) {
1473
+ const allUnits = JSON.parse(await (0, fs_extra_2.readFile)(unitsCachePath, 'utf-8'));
1474
+ const matched = allUnits.find(u => u.id === options.unit);
1475
+ if (matched)
1476
+ unitName = matched.name;
1477
+ }
1478
+ const unitAnalysis = {
1479
+ id: options.unit,
1480
+ name: unitName,
1481
+ req: docs['REQ.md'] || docs['REQUIREMENT.md'] || '',
1482
+ tech: docs['TECH.md'] || docs['ANALYSIS.md'] || '',
1483
+ test: docs['TEST.md'] || '',
1484
+ risk: docs['RISK.md'] || '',
1485
+ timestamp: new Date().toISOString(),
1486
+ };
1487
+ await saveUnitAnalysis(options.iteration, unitAnalysis);
1488
+ logger_1.logger.info(`💾 单元分析 ${options.unit} 已缓存(供 --consolidate 使用)`);
1489
+ }
1490
+ catch (e) {
1491
+ logger_1.logger.debug('单元分析缓存失败(非关键):', e);
1492
+ }
1493
+ }
1494
+ printBackupSummary();
1495
+ return;
1496
+ }
1497
+ catch (e) {
1498
+ logger_1.logger.warn('⚠️ [DOC:xxx] 解析后写入失败,回退到单文件模式');
1499
+ }
1500
+ }
1025
1501
  // 单文件模式
1026
1502
  if (isTaskLevel && taskDir) {
1027
1503
  // 任务级:写 Task/00-specs/(v6.44.0+)
@@ -1265,7 +1741,7 @@ async function analyzeCommand(options) {
1265
1741
  }
1266
1742
  catch { /* ignore */ }
1267
1743
  }
1268
- const prompt = await buildMultiDocPrompt('analyze', { iteration: iter, task: options.task, type: options.type, scope: options.scope, withCode: options.withCode, platform: options.platform, phase: options.phase, autoMode: options.auto }, options);
1744
+ const prompt = await buildMultiDocPrompt('analyze', { iteration: iter, task: options.task, type: options.type, scope: options.scope, withCode: options.withCode, platform: options.platform, phase: options.phase, autoMode: options.auto }, options, needsClarify ? { needsClarify, clarifyTargets } : undefined);
1269
1745
  process.stdout.write(`[SPECCORE_PROMPT]\n${prompt}`);
1270
1746
  // v7.2.0+: 全局分析完成后输出下一步引导
1271
1747
  if (options.scope === 'global') {
@@ -1895,13 +2371,37 @@ async function detectGlobalLayerProgress() {
1895
2371
  missing.push('Layer 2: platforms/_shared/_ASSOCIATION.md + _MODULES.md');
1896
2372
  }
1897
2373
  }
1898
- // Layer 3: 检查 _MODULES.md
2374
+ // Layer 3: 检查各端功能模块深入文档(v8.3.0+ 修复:不再把 _MODULES.md 当 Layer 3 产物)
1899
2375
  if (completedLayer >= 2) {
1900
- if (await (0, fs_extra_1.pathExists)((0, path_1.join)(globalDir, 'platforms', '_shared', '_MODULES.md'))) {
1901
- completedLayer = 3;
2376
+ try {
2377
+ const platformsDir = (0, path_1.join)(globalDir, 'platforms');
2378
+ const entries = await (0, fs_extra_2.readdir)(platformsDir, { withFileTypes: true });
2379
+ const platformDirs = entries.filter(e => e.isDirectory() && e.name !== '_shared').map(e => e.name);
2380
+ if (platformDirs.length === 0) {
2381
+ missing.push('Layer 3: platforms/{端}/modules/*.md(功能模块深入文档)');
2382
+ }
2383
+ else {
2384
+ const moduleDocChecks = await Promise.all(platformDirs.map(async (d) => {
2385
+ const modulesDir = (0, path_1.join)(platformsDir, d, 'modules');
2386
+ if (!await (0, fs_extra_1.pathExists)(modulesDir))
2387
+ return false;
2388
+ const files = await (0, fs_extra_2.readdir)(modulesDir).catch(() => []);
2389
+ return files.some(f => f.endsWith('.md'));
2390
+ }));
2391
+ if (moduleDocChecks.every(Boolean)) {
2392
+ completedLayer = 3;
2393
+ }
2394
+ else {
2395
+ for (const [i, d] of platformDirs.entries()) {
2396
+ if (!moduleDocChecks[i]) {
2397
+ missing.push(`Layer 3: platforms/${d}/modules/*.md(功能模块深入文档)`);
2398
+ }
2399
+ }
2400
+ }
2401
+ }
1902
2402
  }
1903
- else {
1904
- missing.push('Layer 3: platforms/_shared/_MODULES.md(功能模块候选清单)');
2403
+ catch {
2404
+ missing.push('Layer 3: platforms/{端}/modules/*.md(功能模块深入文档)');
1905
2405
  }
1906
2406
  }
1907
2407
  // Layer 4 子层检测
@@ -1910,10 +2410,28 @@ async function detectGlobalLayerProgress() {
1910
2410
  const overviewDir = (0, path_1.join)(globalDir, 'overview');
1911
2411
  const requirementsDir = (0, path_1.join)(globalDir, 'requirements');
1912
2412
  const completedSubLayers = [];
1913
- // 4a: 产品文档
2413
+ // 4a: 产品文档(全局 + 各端,v8.3.0+ 修复:检查各端 REQUIREMENT.md)
1914
2414
  const hasReq = await (0, fs_extra_1.pathExists)((0, path_1.join)(requirementsDir, 'REQUIREMENT.md'));
1915
- if (hasReq)
2415
+ let allPlatformReqsExist = true;
2416
+ try {
2417
+ const platformsDir = (0, path_1.join)(globalDir, 'platforms');
2418
+ const entries = await (0, fs_extra_2.readdir)(platformsDir, { withFileTypes: true });
2419
+ const platformDirs = entries.filter(e => e.isDirectory() && e.name !== '_shared').map(e => e.name);
2420
+ const platformReqChecks = await Promise.all(platformDirs.map(p => (0, fs_extra_1.pathExists)((0, path_1.join)(requirementsDir, p, 'REQUIREMENT.md'))));
2421
+ allPlatformReqsExist = platformReqChecks.every(Boolean);
2422
+ for (const [i, p] of platformDirs.entries()) {
2423
+ if (!platformReqChecks[i]) {
2424
+ missing.push(`Layer 4a: requirements/${p}/REQUIREMENT.md(${p} 产品视角需求)`);
2425
+ }
2426
+ }
2427
+ }
2428
+ catch { /* ignore */ }
2429
+ if (hasReq && allPlatformReqsExist) {
1916
2430
  completedSubLayers.push('4a');
2431
+ }
2432
+ else if (!hasReq) {
2433
+ missing.push('Layer 4a: requirements/REQUIREMENT.md(全局需求总纲)');
2434
+ }
1917
2435
  // 4b: 全局技术核心文档
1918
2436
  const hasCoreTech = await (0, fs_extra_1.pathExists)((0, path_1.join)(overviewDir, 'ARCHITECTURE.md'))
1919
2437
  && await (0, fs_extra_1.pathExists)((0, path_1.join)(overviewDir, 'FUNCTION_MAP.md'));
@@ -1924,15 +2442,42 @@ async function detectGlobalLayerProgress() {
1924
2442
  || await (0, fs_extra_1.pathExists)((0, path_1.join)(overviewDir, 'DATA_FLOW.md'));
1925
2443
  if (hasExtTech)
1926
2444
  completedSubLayers.push('4c');
1927
- // 4d: 各端技术文档
2445
+ // 4d: 各端技术文档(v8.3.0+ 修复:每个端都要有,不再用 some(Boolean))
1928
2446
  try {
1929
2447
  const platformsDir = (0, path_1.join)(globalDir, 'platforms');
1930
2448
  const entries = await (0, fs_extra_2.readdir)(platformsDir, { withFileTypes: true });
1931
2449
  const platformDirs = entries.filter(e => e.isDirectory() && e.name !== '_shared').map(e => e.name);
1932
- const hasPlatformDoc = platformDirs.length > 0 && (await Promise.all(platformDirs.map(async (d) => (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'API_INVENTORY.md'))
1933
- || (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'UI_FLOW.md'))))).some(Boolean);
1934
- if (hasPlatformDoc)
2450
+ const missingPlatformDocs = [];
2451
+ for (const d of platformDirs) {
2452
+ const isBackend = /service|server|api|backend/i.test(d);
2453
+ if (isBackend) {
2454
+ const hasApi = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'API_INVENTORY.md'));
2455
+ const hasData = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'DATA_MODEL.md'));
2456
+ if (!hasApi)
2457
+ missingPlatformDocs.push(`platforms/${d}/API_INVENTORY.md`);
2458
+ if (!hasData)
2459
+ missingPlatformDocs.push(`platforms/${d}/DATA_MODEL.md`);
2460
+ }
2461
+ else {
2462
+ const hasUi = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'UI_FLOW.md'));
2463
+ const hasApiMap = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'API_CALL_MAP.md'));
2464
+ const hasState = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'STATE_MANAGEMENT.md'));
2465
+ if (!hasUi)
2466
+ missingPlatformDocs.push(`platforms/${d}/UI_FLOW.md`);
2467
+ if (!hasApiMap)
2468
+ missingPlatformDocs.push(`platforms/${d}/API_CALL_MAP.md`);
2469
+ if (!hasState)
2470
+ missingPlatformDocs.push(`platforms/${d}/STATE_MANAGEMENT.md`);
2471
+ }
2472
+ }
2473
+ if (missingPlatformDocs.length === 0 && platformDirs.length > 0) {
1935
2474
  completedSubLayers.push('4d');
2475
+ }
2476
+ else {
2477
+ for (const doc of missingPlatformDocs) {
2478
+ missing.push(`Layer 4d: ${doc}`);
2479
+ }
2480
+ }
1936
2481
  }
1937
2482
  catch { /* ignore */ }
1938
2483
  if (completedSubLayers.length === 4) {
@@ -2111,7 +2656,7 @@ async function buildLayer3ModuleContext(projectRoot) {
2111
2656
  return ctx.join('\n\n---\n\n');
2112
2657
  }
2113
2658
  // ── buildMultiDocPrompt: 多文档协议 ──
2114
- async function buildMultiDocPrompt(command, ctx, options) {
2659
+ async function buildMultiDocPrompt(command, ctx, options, clarifyCtx) {
2115
2660
  const iter = ctx.iteration || '当前迭代';
2116
2661
  const task = ctx.task ? ` — ${ctx.task}` : '';
2117
2662
  const taskType = ctx.type || 'feature';
@@ -2119,6 +2664,9 @@ async function buildMultiDocPrompt(command, ctx, options) {
2119
2664
  const isTask = ctx.scope === 'task' || !!ctx.task;
2120
2665
  const isGlobal = ctx.scope === 'global';
2121
2666
  const autoMode = ctx.autoMode || false;
2667
+ // v8.3.0+: 从 clarifyCtx 提取需求澄清状态
2668
+ const needsClarify = clarifyCtx?.needsClarify ?? false;
2669
+ const clarifyTargets = clarifyCtx?.clarifyTargets ?? [];
2122
2670
  // global 范围: 从源码反推需求 + 生成技术栈配置
2123
2671
  if (isGlobal) {
2124
2672
  // v6.74.0+: 流式全局分析模式
@@ -3167,38 +3715,46 @@ sequenceDiagram
3167
3715
  `# 开发者实现指南
3168
3716
 
3169
3717
  > ${iter} | ${now}
3170
- > 本文档面向开发者,提供具体实现步骤、代码模式和最佳实践。
3718
+ > 本文档面向开发者,提供**可执行的实现指导**——开发者读完知道改哪些文件、先改什么后改什么、具体怎么改、改完怎么验证。
3171
3719
 
3172
3720
  ## 写作要求
3173
3721
 
3174
3722
  ### 迭代级 DEV_GUIDE.md(020-specs/overview/)
3175
- - **技术栈与选型决策**:每个技术点的选型理由、替代方案对比
3176
- - **代码分层规范**:目录结构、各层职责、代码组织方式
3177
- - **通用设计模式**:Repository 模式、Service 模式、策略模式等具体实现
3178
- - **跨端数据流**:请求从入口到数据库的完整链路、数据转换规则
3179
- - **错误处理策略**:全局错误码、异常分类、降级策略
3180
- - **性能优化基线**:缓存策略、数据库优化、并发控制
3181
- - **安全基线**:鉴权流程、数据校验、敏感信息处理
3723
+ 面向全迭代,提供全局实施指导:
3724
+ - **分支策略**:从哪个分支切 feature 分支,合并到哪里,分支命名规范
3725
+ - **改造范围清单**:本迭代所有需要修改的文件(新增/修改/删除),按类型分类
3726
+ - **改造顺序与依赖**:按依赖关系排列的实施步骤,说明每步为什么先做
3727
+ - **关键实现步骤(代码级)**:具体到文件/函数级的改造方式,包含改造前后对比
3728
+ - **接口契约(前后端对照)**:所有接口的路径、后端实现位置、前端调用位置、状态
3729
+ - **每步验证方式**:每步改完用什么命令/操作验证,通过标准是什么
3730
+ - **回滚方案**:数据库变更的 DOWN migration、代码回滚方式、配置回滚方式
3731
+ - **常见坑点**:已知陷阱及解决方式
3182
3732
 
3183
3733
  ### 端级 DEV_GUIDE.md(020-specs/{端}/)
3184
- - **端技术栈**:框架、库、工具链及选型理由
3185
- - **目录结构规范**:该端的代码组织方式
3186
- - **核心流程实现**:关键业务流程的伪代码/示例代码
3187
- - **API 调用模式**:请求封装、错误处理、重试策略
3188
- - **状态管理**:全局状态设计、与后端同步策略
3189
- - **端特定最佳实践**:该端特有的性能优化、安全策略
3734
+ 面向本端开发者:
3735
+ - **本端改造范围**:本端需要修改的文件清单
3736
+ - **本端实施步骤(按依赖排序)**:具体到文件/函数的开发顺序
3737
+ - **代码级指引**:改哪几行、改什么、改造后的代码片段
3738
+ - **与后端联调**:接口状态、联调顺序、Mock 方案
3739
+ - **验证方式**:本端特有的验证命令/操作和通过标准
3740
+ - **本端坑点**:该端特有的陷阱
3190
3741
 
3191
3742
  ### 任务级 DEV_GUIDE.md(Task/00-specs/)
3192
- - **任务概述**:该任务在整体功能中的位置和职责
3193
- - **实现步骤**:Step-by-step 的开发步骤
3194
- - **关键代码示例**:核心逻辑的伪代码或代码片段
3195
- - **与存量功能的集成**:如何与已有代码交互、复用哪些模块
3196
- - **测试策略**:单元测试、集成测试的具体写法
3197
- - **注意事项**:常见坑点、边界条件、调试技巧
3743
+ 面向具体任务开发者,最细粒度:
3744
+ - **本任务改造范围**:该任务涉及的具体文件(新增/修改/删除)
3745
+ - **实施步骤(按依赖排序)**:Step-by-step,具体到文件/函数级
3746
+ - **关键代码指引**:核心改造点的文件位置、改造方式、改造后代码示例
3747
+ - **接口契约**:本任务涉及的前后接口对照表
3748
+ - **每步验证**:每步改完怎么验证(命令/操作 + 通过标准)
3749
+ - **回滚方案**:本任务改坏了怎么回退
3750
+ - **已知坑点**:常见坑点及解决方式
3198
3751
 
3199
3752
  ## 质量要求
3200
3753
  - 必须是**可执行的实现指导**,不是抽象概念
3201
- - 包含具体的代码示例(伪代码或关键代码片段)
3754
+ - **改造范围**必须列出具体文件路径(相对项目根目录)
3755
+ - **改造顺序**必须说明依赖关系(为什么 A 必须在 B 之前)
3756
+ - **代码示例**必须具体到文件/函数/行号级别
3757
+ - **验证方式**必须是可执行的命令或操作(不是"测试通过"这种空话)
3202
3758
  - 基于 020-specs/ 中已有的分析文档,不做重复分析
3203
3759
  - 补充技术文档中未涉及的实现细节
3204
3760
  - 如果涉及存量功能,说明复用方式和集成点
@@ -3246,15 +3802,19 @@ sequenceDiagram
3246
3802
  if (perDocStatus.nextUnfilled) {
3247
3803
  const nextDocName = perDocStatus.nextUnfilled.docName;
3248
3804
  const nextDocPlatform = perDocStatus.nextUnfilled.platform || null;
3249
- // 过滤 taskDocs 只保留下一个未填充的文档
3250
- if (nextDocPlatform && perDocStatus.nextUnfilled.category === 'platform') {
3251
- // Phase 2 文档(端专属)
3252
- taskDocs = taskDocs.filter(([n]) => n === nextDocName);
3253
- ctx.platform = nextDocPlatform;
3254
- }
3255
- else {
3256
- // Phase 1 文档(overview)
3257
- taskDocs = taskDocs.filter(([n]) => n === nextDocName);
3805
+ // v8.2.0+: 功能单元聚焦模式不强制单文档过滤,让 AI 一次输出完整单元分析
3806
+ const isUnitMode = !!(options?.unit || options?.consolidate || options?.extractUnits);
3807
+ if (!isUnitMode) {
3808
+ // 过滤 taskDocs 只保留下一个未填充的文档(默认逐文档推进模式)
3809
+ if (nextDocPlatform && perDocStatus.nextUnfilled.category === 'platform') {
3810
+ // Phase 2 文档(端专属)
3811
+ taskDocs = taskDocs.filter(([n]) => n === nextDocName);
3812
+ ctx.platform = nextDocPlatform;
3813
+ }
3814
+ else {
3815
+ // Phase 1 文档(overview)
3816
+ taskDocs = taskDocs.filter(([n]) => n === nextDocName);
3817
+ }
3258
3818
  }
3259
3819
  logger_1.logger.info(`🦴 骨架进度: ${perDocStatus.filledCount}/${perDocStatus.totalCount} 已填充,下一个: ${perDocStatus.nextUnfilled.relPath}`);
3260
3820
  // v8.1.0+: 校验上一个已填充文档的质量
@@ -3412,6 +3972,57 @@ sequenceDiagram
3412
3972
  techDoc[1] = `# 技术架构(跨端全局)\n\n> ${iter}\n\n## 写作要求\n撰写整体技术架构,覆盖所有端的交互关系:\n- 系统整体分层设计(各端在架构中的位置)\n- 跨端交互协议(前端↔后端通信方式、数据流向)\n- 中间件选型(缓存、消息队列、网关等)\n- 数据库整体设计(核心表结构、ER 关系)\n- 技术栈选型及理由\n`;
3413
3973
  }
3414
3974
  let prompt = `\n# 任务: ${command}${task} (${taskDocs.length}个文档 · ${isTask ? `类型:${taskType}` : '迭代全量'}${ctx.phase ? ` · Phase ${ctx.phase}` : ''})\n\n`;
3975
+ // v8.3.0+: 如果检测到需求质量不足,在 prompt 开头注入强制澄清阶段
3976
+ if (needsClarify && !isGlobal && !isTask && !ctx.phase) {
3977
+ const now = new Date().toISOString();
3978
+ prompt += `## 🚨 Phase 0: 需求澄清(强制前置 — 未完成则禁止进入 Phase 1)\n\n`;
3979
+ prompt += `检测到以下需求文档质量不足,**必须先完成专业化整理,才能继续技术分析**:\n\n`;
3980
+ for (const t of clarifyTargets) {
3981
+ prompt += `- \`${t.path.split('/').slice(-2).join('/')}\`(质量: ${t.level.toUpperCase()})\n`;
3982
+ }
3983
+ prompt += `\n### 澄清执行步骤(必须按顺序完成)\n\n`;
3984
+ prompt += `**Step 1**: 读取上述每个需求文档的原始内容\n`;
3985
+ prompt += `**Step 2**: 将口语化/非专业描述整理为 PRD 级专业文档\n`;
3986
+ prompt += `**Step 3**: 补充:验收标准(AC)、功能边界、业务规则、异常处理、数据模型\n`;
3987
+ prompt += `**Step 4**: 输出澄清后的文档,使用以下标记格式:\n`;
3988
+ prompt += `\`\`\`
3989
+ [CLARIFY:requirements/clarified-{feature-name}.md]
3990
+ ---
3991
+ source: "原始文档路径"
3992
+ clarified-at: "${now}"
3993
+ status: "clarified"
3994
+ ---
3995
+
3996
+ # {功能名称}
3997
+
3998
+ ## 背景与目标
3999
+ ...
4000
+
4001
+ ## 用户故事
4002
+ ...
4003
+
4004
+ ## 功能规格
4005
+ ...
4006
+
4007
+ ## 验收标准(AC)
4008
+ - [ ] ...
4009
+
4010
+ ## 非功能需求
4011
+ ...
4012
+
4013
+ ## 依赖与约束
4014
+ ...
4015
+ \`\`\`
4016
+ `;
4017
+ prompt += `**Step 5**: 用 Read 工具验证文件已正确写入 \`{迭代}/020-specs/requirements/clarified-{feature-name}.md\`\n`;
4018
+ prompt += `**Step 6**: 基于澄清后的需求继续 Phase 1 分析\n\n`;
4019
+ prompt += `### ⚠️ 重要提醒\n\n`;
4020
+ prompt += `- **如果输出中没有 [CLARIFY:xxx] 标记的澄清文档,--apply 阶段将拒绝写入所有分析结果**\n`;
4021
+ prompt += `- CLI 会在接收 --apply 时先解析 [CLARIFY:xxx] 标记,写入 020-specs/requirements/,然后才处理 [DOC:xxx] 标记\n`;
4022
+ prompt += `- 不要跳过此步骤,不要假设需求已经够清晰\n`;
4023
+ prompt += `- 如果原始描述不完整,在澄清文档中标注「待补充」而不是自行编造\n\n`;
4024
+ prompt += `---\n\n`;
4025
+ }
3415
4026
  // v7.2.0+: 迭代分析代码关联 — 注入结构化数据和语义定位上下文
3416
4027
  if (!isGlobal && ctx.withCode && ctx.iteration && ctx.iteration !== 'GLOBAL') {
3417
4028
  try {
@@ -3529,24 +4140,37 @@ sequenceDiagram
3529
4140
  try {
3530
4141
  const iterDirForCtx = await (0, context_1.getIterationDir)(iter);
3531
4142
  if (iterDirForCtx) {
3532
- // 1. 注入 PRD 内容(010-requirements/ 下的需求文档)
3533
- const reqDir = (0, path_1.join)(iterDirForCtx, '010-requirements');
4143
+ // v8.2.0+: 注入 PRD 内容(优先从黄金需求目录 020-specs/requirements/ 读取)
3534
4144
  let prdContent = '';
3535
- const prdSources = (0, path_1.join)(reqDir, 'sources');
3536
- const prdConverted = (0, path_1.join)(reqDir, 'converted');
3537
- const prdFeatures = (0, path_1.join)(reqDir, 'features');
3538
- for (const dir of [prdSources, prdConverted, prdFeatures]) {
3539
- if (await (0, fs_extra_1.pathExists)(dir)) {
3540
- const files = (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readdirSync(dir).filter((f) => f.endsWith('.md')).slice(0, 5);
3541
- for (const f of files) {
3542
- const content = await (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readFile((0, path_1.join)(dir, f), 'utf-8');
3543
- prdContent += `\n### ${f}\n${content.slice(0, 2000)}\n`;
4145
+ const goldenReqDir = (0, path_1.join)(iterDirForCtx, '020-specs', 'requirements');
4146
+ if (await (0, fs_extra_1.pathExists)(goldenReqDir)) {
4147
+ // 黄金需求目录存在,只读这里(唯一依据)
4148
+ const files = (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readdirSync(goldenReqDir).filter((f) => f.endsWith('.md')).slice(0, 10);
4149
+ for (const f of files) {
4150
+ const content = await (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readFile((0, path_1.join)(goldenReqDir, f), 'utf-8');
4151
+ prdContent += `\n### ${f}\n${content.slice(0, 2000)}\n`;
4152
+ }
4153
+ }
4154
+ else {
4155
+ // 回退:从 010-requirements/ 读取原始需求
4156
+ const reqDir = (0, path_1.join)(iterDirForCtx, '010-requirements');
4157
+ const prdSources = (0, path_1.join)(reqDir, 'sources');
4158
+ const prdConverted = (0, path_1.join)(reqDir, 'converted');
4159
+ const prdFeatures = (0, path_1.join)(reqDir, 'features');
4160
+ for (const dir of [prdSources, prdConverted, prdFeatures]) {
4161
+ if (await (0, fs_extra_1.pathExists)(dir)) {
4162
+ const files = (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readdirSync(dir).filter((f) => f.endsWith('.md')).slice(0, 5);
4163
+ for (const f of files) {
4164
+ const content = await (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readFile((0, path_1.join)(dir, f), 'utf-8');
4165
+ prdContent += `\n### ${f}\n${content.slice(0, 2000)}\n`;
4166
+ }
3544
4167
  }
3545
4168
  }
3546
4169
  }
3547
4170
  if (prdContent.length > 0) {
3548
4171
  prompt += `\n## 📎 需求文档原文(PRD,写作核心输入)\n\n`;
3549
- prompt += `> 以下是 010-requirements/ 下的需求文档,这是你撰写专业分析文档的核心输入。所有分析必须基于这些内容,不要臆造。\n`;
4172
+ const sourceLabel = await (0, fs_extra_1.pathExists)(goldenReqDir) ? '020-specs/requirements/(黄金需求,已澄清)' : '010-requirements/(原始需求)';
4173
+ prompt += `> 以下是 ${sourceLabel} 下的需求文档,这是你撰写专业分析文档的核心输入。所有分析必须基于这些内容,不要臆造。\n`;
3550
4174
  prompt += prdContent.slice(0, 5000);
3551
4175
  prompt += `\n\n`;
3552
4176
  }
@@ -3641,24 +4265,55 @@ sequenceDiagram
3641
4265
  prompt += `- Read 020-specs/overview/ANALYSIS.md → 分析报告\n`;
3642
4266
  prompt += `- Read 020-specs/overview/TECH.md → 整体技术架构\n`;
3643
4267
  prompt += `- Read 020-specs/overview/RISK.md、DEPS.md、REVIEW.md、MONITOR.md(如存在)\n\n`;
3644
- prompt += `### Step 2: 为每个端撰写专属文档\n`;
3645
- prompt += `根据全局上下文,为 PLATFORMS.md 中的**每个端**分别撰写:\n`;
3646
- prompt += `- **{端}/TECH.md**:该端专属技术方案(必须对齐 overview/TECH.md 架构)\n`;
4268
+ prompt += `### Step 2: 为每个端撰写专属文档(v8.3.1+ 强制四文档)\n`;
4269
+ prompt += `根据全局上下文,为 PLATFORMS.md 中的**每个端**分别撰写以下 **4 份文档**,缺一不可:\n\n`;
4270
+ prompt += `**1. {端}/TECH.md — 该端专属技术方案(必须对齐 overview/TECH.md 架构)**\n`;
4271
+ prompt += ` - 该端的分层架构、模块划分、核心接口设计(路径/方法/参数/响应/状态码)\n`;
4272
+ prompt += ` - 数据库表结构(字段/类型/索引/约束,如适用)\n`;
4273
+ prompt += ` - 业务规则实现(含边界条件和异常流)\n`;
3647
4274
  prompt += ` - ⚠️ **必须包含「业务-代码映射」章节**:在 TECH.md 末尾添加表格,列出本端涉及的业务模块及其对应的代码实体(文件/表/API/组件等),关系类型由你根据技术栈自主决定(如 api_controller、uses_table、page、component、route、middleware、interceptor、gateway 等)\n`;
3648
4275
  prompt += ` - 表格格式:| 业务模块 | 代码实体 | 关系类型 | 说明 |\n`;
3649
4276
  prompt += ` - 示例:| 会议室档案 | backend/RoomController.java | api_controller | REST 控制器 |\n`;
3650
- prompt += ` - 示例:| 会议室档案 | admin-web/src/pages/RoomList.vue | page | 列表页 |\n`;
3651
- prompt += `- **{端}/TEST.md**:该端专属测试计划\n`;
3652
- prompt += `- **{端}/UI_SPEC.md**:该端专属 UI 规格(仅前端端需要)\n\n`;
4277
+ prompt += ` - 示例:| 会议室档案 | admin-web/src/pages/RoomList.vue | page | 列表页 |\n\n`;
4278
+ prompt += `**2. {端}/TEST.md — 该端专属测试计划**\n`;
4279
+ prompt += ` - 覆盖该端所有功能模块的测试用例(含前置条件、步骤、预期结果)\n`;
4280
+ prompt += ` - 边界值和异常输入测试\n`;
4281
+ prompt += ` - 必须覆盖 REQUIREMENT.md 中该端的验收标准\n\n`;
4282
+ prompt += `**3. {端}/UI_SPEC.md — 该端专属 UI 规格(仅前端端需要)**\n`;
4283
+ prompt += ` - 页面结构与路由(每个页面的路径、入口、权限)\n`;
4284
+ prompt += ` - 组件清单和字段→UI 映射\n`;
4285
+ prompt += ` - 状态枚举和错误处理策略\n\n`;
4286
+ prompt += `**4. {端}/DEV_GUIDE.md — 该端开发者实现指南(v8.3.1+ 新增强制要求)**\n`;
4287
+ prompt += ` - ⛔ **禁止只写框架/占位符** — 必须是可执行的实现指导\n`;
4288
+ prompt += ` - **本端改造范围清单**:列出本端所有需要新增/修改/删除的文件(相对项目根目录的具体路径)\n`;
4289
+ prompt += ` - **本端实施步骤(按依赖排序)**:Step-by-step,具体到文件/函数级,说明每步为什么先做\n`;
4290
+ prompt += ` - **代码级指引**:改哪几行、改什么、改造后的关键代码片段\n`;
4291
+ prompt += ` - **与后端联调**:接口状态、联调顺序、Mock 方案(表格格式)\n`;
4292
+ prompt += ` - **验证方式**:本端特有的验证命令/操作和通过标准(表格格式)\n`;
4293
+ prompt += ` - **本端坑点**:已知陷阱及解决方式\n`;
4294
+ prompt += ` - **回滚方案**:代码回滚方式、配置回滚方式\n\n`;
3653
4295
  prompt += `### Step 3: 一致性检查\n`;
3654
4296
  prompt += `- 各端 TECH.md 的技术选型必须与 overview/TECH.md 一致\n`;
3655
4297
  prompt += `- UI_SPEC.md 的字段映射必须与后端 API 响应字段一一对应\n`;
3656
4298
  prompt += `- TEST.md 必须覆盖 REQUIREMENT.md 中该端的验收标准\n\n`;
4299
+ prompt += `### Step 3: 自检 — 确保所有文档已实质填充\n`;
4300
+ prompt += `生成完成后,逐个检查每个文档:\n`;
4301
+ prompt += `- 如果文档中仍含有 \`<!-- SPEC-SKELETON -->\` 标记或空表格 → **必须重新填充**\n`;
4302
+ prompt += `- 如果「改造范围」表格只有表头没有数据行 → **必须补充具体文件路径**\n`;
4303
+ prompt += `- 如果「验证方式」表格只有表头 → **必须补充可执行的命令/操作**\n`;
4304
+ prompt += `- DEV_GUIDE.md 必须有至少 3 个具体文件路径和 3 个可执行验证步骤\n\n`;
3657
4305
  prompt += `### 写入方式\n`;
3658
4306
  prompt += `**Pipeline 模式**:一次 --apply 写入所有端的文档(推荐)\n`;
3659
- prompt += `speccore analyze --apply '{"TECH.md":"...","TEST.md":"...","UI_SPEC.md":"..."}' -I ${iter} --platform all\n\n`;
4307
+ prompt += `speccore analyze --apply '{"TECH.md":"...","TEST.md":"...","UI_SPEC.md":"...","DEV_GUIDE.md":"..."}' -I ${iter} --platform all\n\n`;
3660
4308
  prompt += `**或者逐端写入**(每端一次 --apply):\n`;
3661
- prompt += `speccore analyze --apply '{"TECH.md":"...","TEST.md":"...","UI_SPEC.md":"..."}' -I ${iter} --platform {端名}\n\n`;
4309
+ prompt += `speccore analyze --apply '{"TECH.md":"...","TEST.md":"...","UI_SPEC.md":"...","DEV_GUIDE.md":"..."}' -I ${iter} --platform {端名}\n\n`;
4310
+ prompt += `### ⛔ 强制约束(违反则分析无效)\n`;
4311
+ prompt += `- **禁止省略任何一份文档** — 4 份文档(TECH.md + TEST.md + UI_SPEC.md + DEV_GUIDE.md)缺一不可\n`;
4312
+ prompt += `- **禁止输出空表格** — 每个 Markdown 表格必须有至少 1 行数据,只有表头的表格视为未完成\n`;
4313
+ prompt += `- **禁止输出占位符** — 不允许写「待填充」、「TODO」、「...」、「xxx」等占位内容\n`;
4314
+ prompt += `- **DEV_GUIDE.md 最低标准**:改造范围表格 ≥3 行、实施步骤 ≥3 步、验证方式表格 ≥3 行、坑点 ≥2 条\n`;
4315
+ prompt += `- **如果 token 不足**:优先保证 DEV_GUIDE.md 完整,其他文档可精简但不可空\n`;
4316
+ prompt += `- **自检必做**:写入前用 Read 工具检查每个文档,确认不含 \`<!-- SPEC-SKELETON -->\` 且所有表格有数据行\n\n`;
3662
4317
  }
3663
4318
  else {
3664
4319
  // v6.61.0+: 一次性生成所有文档(global/ + {端}/)
@@ -3698,7 +4353,14 @@ sequenceDiagram
3698
4353
  prompt += ` - **边界处理**:如果文档对某功能描述不完整,标注"文档未充分描述",不要自行脑补完整方案\n`;
3699
4354
  prompt += ` - **交叉验证**:每写一个功能点,回头检查需求文档中是否有对应描述,没有则删除\n`;
3700
4355
  prompt += `6. 每个文档都要具体内容(禁止"待填充")\n`;
3701
- prompt += `7. **端发现(重要)**:先确定项目有哪些端,再按端组织文档\n`;
4356
+ prompt += `7. **辅助文档强制要求(v8.3.1+ 修复空模板问题)**:以下文档常被 AI 遗漏或敷衍,必须同等重视:\n`;
4357
+ prompt += ` - **FUNCTION_MAP.md**:功能单元 × 端映射表,必须用 Markdown 表格,每行一个功能单元,不允许合并。表头:| # | 功能单元 | 涉及端 | 全局对比 | 共享能力 | 依赖任务 | 说明 |\n`;
4358
+ prompt += ` - **INTERACTION_MAP.md**:每个功能单元一个 Mermaid sequenceDiagram,展示跨端交互时序,箭头标注接口路径\n`;
4359
+ prompt += ` - **DEV_GUIDE.md(迭代级)**:全局实施指导,必须包含:分支策略、改造范围清单(具体到文件路径)、改造顺序与依赖、代码级指引(含改造前后对比)、接口契约对照表、每步验证方式、回滚方案、常见坑点\n`;
4360
+ prompt += ` - **MONITOR.md**:必须按 Fatal/Critical/Warning/Info 四级定义告警规则,不能只列指标名称\n`;
4361
+ prompt += ` - **REVIEW.md**:安全检查必须逐接口列出鉴权需求,不能笼统写"需要鉴权"\n`;
4362
+ prompt += ` ⛔ **自检规则**:生成完成后检查每个文档,如果仍含 \`<!-- SPEC-SKELETON -->\` 或空表格只有表头 → 必须重新填充\n`;
4363
+ prompt += `8. **端发现(重要)**:先确定项目有哪些端,再按端组织文档\n`;
3702
4364
  prompt += ` - 第 1 步:Read .speccore/CONSTITUTION.md\n`;
3703
4365
  prompt += ` - 第 2 步:从「## 端列表」章节提取端名(这是全局权威来源)\n`;
3704
4366
  prompt += ` - 第 3 步:如果没有「端列表」章节,从「对应端」列提取\n`;
@@ -3706,7 +4368,7 @@ sequenceDiagram
3706
4368
  prompt += ` - 第 5 步:将发现的端列表写入 020-specs/PLATFORMS.md\n`;
3707
4369
  // v6.70.0+: REQUIREMENT.md 以产品视角撰写(不按端分章节)
3708
4370
  // v6.99.0+: 丰富需求文档章节要求
3709
- prompt += `8. **REQUIREMENT.md 写作风格(重要)**:全局需求文档必须以产品/用户视角撰写\n`;
4371
+ prompt += `9. **REQUIREMENT.md 写作风格(重要)**:全局需求文档必须以产品/用户视角撰写\n`;
3710
4372
  prompt += ` - **按业务场景/用户旅程组织章节**,不按端分章节(如"H5端需求"、"后端需求")\n`;
3711
4373
  prompt += ` - 文档结构必须包含(如需求文档中有相关信息):\n`;
3712
4374
  prompt += ` - **产品愿景**:本迭代要解决的核心问题和目标价值(1-2段)\n`;
@@ -3724,7 +4386,7 @@ sequenceDiagram
3724
4386
  prompt += ` - 端的信息只在「功能模块清单」表格中标注,正文不区分端\n`;
3725
4387
  // v6.49.14+: 功能模块清单必须含涉及端列 + 来源链接
3726
4388
  // v6.71.3+: 增加「与全局层对比」列
3727
- prompt += `9. **功能模块清单(重要)**:写入 overview/REQUIREMENT.md 时,功能模块清单表格必须包含以下列\n`;
4389
+ prompt += `10. **功能模块清单(重要)**:写入 overview/REQUIREMENT.md 时,功能模块清单表格必须包含以下列\n`;
3728
4390
  prompt += ` - 表格格式:| # | 功能模块 | 涉及端 | 全局对比 | 来源 | 说明 |\n`;
3729
4391
  prompt += ` - 「涉及端」:每个模块标注需要**新开发工作**的端(标准端名,逗号分隔)\n`;
3730
4392
  prompt += ` - 「涉及」= 该端需要写新接口/新页面/新逻辑\n`;
@@ -4272,6 +4934,16 @@ async function buildClarifyPhasePrompt(iteration) {
4272
4934
  if (qualityReports.length > 0) {
4273
4935
  await (0, requirement_clarifier_1.writeClarifyReport)(iterDir, qualityReports);
4274
4936
  }
4937
+ // v8.2.0+: 提取功能单元,支持单元化澄清(解决注意力漂移)
4938
+ let allDocContent = '';
4939
+ for (const p of docPaths) {
4940
+ try {
4941
+ allDocContent += await (0, fs_extra_2.readFile)(p, 'utf-8') + '\n\n';
4942
+ }
4943
+ catch { /* ignore */ }
4944
+ }
4945
+ const extractedUnits = (0, requirement_clarifier_1.extractUnitsFromText)(allDocContent);
4946
+ const hasMultipleUnits = extractedUnits.length >= 2;
4275
4947
  // v6.84.0+: 从 AGENTS 规范数据库动态加载角色
4276
4948
  let prompt = `\n# 任务: 需求专业化(Phase 0: 需求澄清,v6.84.0+)\n\n`;
4277
4949
  const projectRoot = (0, task_utils_1.findProjectRoot)() || process.cwd();
@@ -4332,6 +5004,33 @@ async function buildClarifyPhasePrompt(iteration) {
4332
5004
  prompt += `3. \`010-requirements/converted/*.md\` — 已转换文档\n`;
4333
5005
  prompt += `4. \`010-requirements/features/*/README.md\` — 功能级补充\n`;
4334
5006
  prompt += `5. \`010-requirements/prototypes/\` — 原型文件\n\n`;
5007
+ // v8.2.0+: 如果检测到多个功能单元,启用单元化澄清模式
5008
+ if (hasMultipleUnits) {
5009
+ prompt += `## ⚠️ 功能单元拆分澄清模式(v8.2.0+)\n\n`;
5010
+ prompt += `检测到需求文档包含 ${extractedUnits.length} 个功能单元。为避免注意力漂移,请**按单元逐个澄清**:\n\n`;
5011
+ prompt += `### 功能单元清单\n\n`;
5012
+ for (const u of extractedUnits) {
5013
+ prompt += `- **${u.id}**: ${u.name}\n`;
5014
+ }
5015
+ prompt += `\n`;
5016
+ prompt += `### 单元化澄清规则\n\n`;
5017
+ prompt += `1. **逐单元处理**:每次只深入分析一个功能单元,不要跨单元混合\n`;
5018
+ prompt += `2. **使用 [UNIT:xxx] 标记**:每个单元的澄清结果用 \`[UNIT:单元ID]\` 开头分隔\n`;
5019
+ prompt += `3. **关联上下文**:分析当前单元时,参考相关单元的摘要(保持逻辑一致性)\n`;
5020
+ prompt += `4. **全局约束共享**:术语表、接口契约在所有单元间统一\n`;
5021
+ prompt += `5. **优先核心单元**:如果单元过多(>5),先处理核心功能单元\n\n`;
5022
+ prompt += `### 输出格式示例\n\n`;
5023
+ prompt += `\`\`\`\n`;
5024
+ for (const u of extractedUnits.slice(0, 2)) {
5025
+ prompt += `[UNIT:${u.id}]\n`;
5026
+ prompt += `## ${u.name}\n`;
5027
+ prompt += `(该单元的专业化澄清内容...)\n\n`;
5028
+ }
5029
+ if (extractedUnits.length > 2) {
5030
+ prompt += `...(其余 ${extractedUnits.length - 2} 个单元依此类推)\n`;
5031
+ }
5032
+ prompt += `\`\`\`\n\n`;
5033
+ }
4335
5034
  prompt += `---\n\n`;
4336
5035
  prompt += `# 📋 五步迭代澄清流程\n\n`;
4337
5036
  prompt += `> 本阶段要求你先以专业角色深度分析,再生成改写版,与用户反复迭代确认,最终写入。\n\n`;
@@ -4420,11 +5119,11 @@ async function buildClarifyPhasePrompt(iteration) {
4420
5119
  prompt += ` - 记录取舍:每轮迭代记录「采纳/拒绝/待确认」的决策\n\n`;
4421
5120
  prompt += `## Step 5: 确认写入\n\n`;
4422
5121
  prompt += `当用户确认 "满意,可以写入" 后:\n\n`;
4423
- prompt += `1. 将最终版 PRD 写入 \`010-requirements/converted/clarified-{源文件名}-{日期}.md\`\n`;
4424
- prompt += `2. 同时生成 \`010-requirements/converted/clarified-{源文件名}-{日期}-diff.md\` 保存最终对比报告\n`;
5122
+ prompt += `1. 将最终版 PRD 写入 \`020-specs/requirements/clarified-{源文件名}-{日期}.md\`(黄金需求目录)\n`;
5123
+ prompt += `2. 同时生成 \`020-specs/requirements/clarified-{源文件名}-{日期}-diff.md\` 保存最终对比报告\n`;
4425
5124
  prompt += `3. 使用以下命令写入:\n\n`;
4426
5125
  prompt += `\`\`\`bash\n`;
4427
- prompt += `speccore analyze --apply '{"010-requirements/converted/clarified-xxx.md":"...","010-requirements/converted/clarified-xxx-diff.md":"..."}' -I ${iteration}\n`;
5126
+ prompt += `speccore analyze --apply '{"020-specs/requirements/clarified-xxx.md":"...","020-specs/requirements/clarified-xxx-diff.md":"..."}' -I ${iteration}\n`;
4428
5127
  prompt += `\`\`\`\n\n`;
4429
5128
  prompt += `> 注意:写入后 CLI 会自动推进到需求确认阶段。\n`;
4430
5129
  return await injectGraphSummary(prompt);
@@ -4432,7 +5131,7 @@ async function buildClarifyPhasePrompt(iteration) {
4432
5131
  async function buildConfirmCheckPrompt(iteration) {
4433
5132
  const iterDir = await (0, context_1.getIterationDir)(iteration);
4434
5133
  const reportPath = (0, path_1.join)(iterDir, '010-requirements', 'CLARIFY_REPORT.md');
4435
- const convertedDir = (0, path_1.join)(iterDir, '010-requirements', 'converted');
5134
+ const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
4436
5135
  let prompt = `\n# 任务: 需求确认(Phase 0: 确认检查,v6.83.0+)\n\n`;
4437
5136
  if (await (0, fs_extra_1.pathExists)(reportPath)) {
4438
5137
  const report = await (0, fs_extra_2.readFile)(reportPath, 'utf-8');
@@ -4442,11 +5141,11 @@ async function buildConfirmCheckPrompt(iteration) {
4442
5141
  prompt += `\n\n... (报告共 ${report.length} 字符,已截断)\n`;
4443
5142
  }
4444
5143
  }
4445
- // v6.83.0+: 检查是否有 diff 文件
5144
+ // v8.2.0+: 检查黄金需求目录是否有 diff 文件
4446
5145
  let diffFiles = [];
4447
- if (await (0, fs_extra_1.pathExists)(convertedDir)) {
5146
+ if (await (0, fs_extra_1.pathExists)(goldenDir)) {
4448
5147
  try {
4449
- const entries = await (0, fs_extra_2.readdir)(convertedDir);
5148
+ const entries = await (0, fs_extra_2.readdir)(goldenDir);
4450
5149
  diffFiles = entries.filter(f => f.includes('-diff.md'));
4451
5150
  }
4452
5151
  catch { /* ignore */ }
@@ -4454,13 +5153,13 @@ async function buildConfirmCheckPrompt(iteration) {
4454
5153
  if (diffFiles.length > 0) {
4455
5154
  prompt += `\n## 已生成的对比报告\n\n`;
4456
5155
  for (const f of diffFiles) {
4457
- prompt += `- \`converted/${f}\` — 需求澄清对比记录\n`;
5156
+ prompt += `- \`020-specs/requirements/${f}\` — 需求澄清对比记录\n`;
4458
5157
  }
4459
5158
  prompt += '\n';
4460
5159
  }
4461
5160
  prompt += `## 确认检查清单\n\n`;
4462
5161
  prompt += `请逐项确认:\n\n`;
4463
- prompt += `- [ ] clarified-*.md 已写入 \`010-requirements/converted/\`\n`;
5162
+ prompt += `- [ ] clarified-*.md 已写入 \`020-specs/requirements/\`(黄金需求目录)\n`;
4464
5163
  prompt += `- [ ] 验收标准可测试、可量化\n`;
4465
5164
  prompt += `- [ ] 功能边界明确(不做什么)\n`;
4466
5165
  prompt += `- [ ] 业务流程完整(含异常分支)\n`;
@@ -4484,4 +5183,277 @@ async function buildConfirmCheckPrompt(iteration) {
4484
5183
  prompt += `\`\`\`\n`;
4485
5184
  return await injectGraphSummary(prompt);
4486
5185
  }
5186
+ // ================================================================
5187
+ // v8.2.0+: 功能单元聚焦分析辅助函数(解决注意力漂移+空模板问题)
5188
+ // ================================================================
5189
+ /** 解析 [DOC:filename] 标记,将统一报告拆分为多文档 Map */
5190
+ function parseDocMarkers(text) {
5191
+ const result = new Map();
5192
+ const markerRegex = /\[DOC:([^\]]+)\]/g;
5193
+ let match;
5194
+ const positions = [];
5195
+ while ((match = markerRegex.exec(text)) !== null) {
5196
+ positions.push({ name: match[1].trim(), index: match.index });
5197
+ }
5198
+ for (let i = 0; i < positions.length; i++) {
5199
+ const start = positions[i].index + `[DOC:${positions[i].name}]`.length;
5200
+ const end = i < positions.length - 1 ? positions[i + 1].index : text.length;
5201
+ const content = text.slice(start, end).trim();
5202
+ result.set(positions[i].name, content);
5203
+ }
5204
+ return result;
5205
+ }
5206
+ /** 解析 [CLARIFY:filename] 标记,提取需求澄清文档 Map — v8.3.0+ */
5207
+ function parseClarifyMarkers(text) {
5208
+ const result = new Map();
5209
+ const markerRegex = /\[CLARIFY:([^\]]+)\]/g;
5210
+ let match;
5211
+ const positions = [];
5212
+ while ((match = markerRegex.exec(text)) !== null) {
5213
+ positions.push({ name: match[1].trim(), index: match.index });
5214
+ }
5215
+ for (let i = 0; i < positions.length; i++) {
5216
+ const start = positions[i].index + `[CLARIFY:${positions[i].name}]`.length;
5217
+ const end = i < positions.length - 1 ? positions[i + 1].index : text.length;
5218
+ const content = text.slice(start, end).trim();
5219
+ result.set(positions[i].name, content);
5220
+ }
5221
+ return result;
5222
+ }
5223
+ exports.UNITS_CACHE_DIR = '.speccore/cache/unit-analysis';
5224
+ /** 从需求文档提取功能单元清单 */
5225
+ async function extractIterationUnits(iterDir) {
5226
+ const reqDir = (0, path_1.join)(iterDir, '010-requirements');
5227
+ const units = [];
5228
+ let idCounter = 1;
5229
+ if (!await (0, fs_extra_1.pathExists)(reqDir))
5230
+ return units;
5231
+ const reqFiles = [];
5232
+ async function scanDir(dir) {
5233
+ const entries = await (0, fs_extra_2.readdir)(dir, { withFileTypes: true });
5234
+ for (const entry of entries) {
5235
+ const fullPath = (0, path_1.join)(dir, entry.name);
5236
+ if (entry.isDirectory())
5237
+ await scanDir(fullPath);
5238
+ else if (entry.name.endsWith('.md'))
5239
+ reqFiles.push(fullPath);
5240
+ }
5241
+ }
5242
+ await scanDir(reqDir);
5243
+ for (const file of reqFiles) {
5244
+ const content = await (0, fs_extra_2.readFile)(file, 'utf-8');
5245
+ const lines = content.split('\n');
5246
+ for (let i = 0; i < lines.length; i++) {
5247
+ const line = lines[i];
5248
+ const headingMatch = line.match(/^#{2,3}\s+(.+)$/);
5249
+ if (!headingMatch)
5250
+ continue;
5251
+ const name = headingMatch[1].trim();
5252
+ // 跳过通用标题
5253
+ const skipKeywords = ['需求概述', '术语表', '附录', '测试策略', '参考资料', '目录', '引言', '背景'];
5254
+ if (skipKeywords.some(k => name.includes(k)))
5255
+ continue;
5256
+ // 收集后续 1-15 行作为内容摘要
5257
+ let descLines = [];
5258
+ for (let j = i + 1; j < Math.min(i + 16, lines.length); j++) {
5259
+ const l = lines[j];
5260
+ if (l.match(/^#{1,3}\s+/))
5261
+ break;
5262
+ if (l.trim())
5263
+ descLines.push(l.trim());
5264
+ }
5265
+ units.push({
5266
+ id: `M-${String(idCounter++).padStart(2, '0')}`,
5267
+ name,
5268
+ source: (0, path_1.relative)(reqDir, file),
5269
+ sourceRange: { startLine: i + 1, endLine: i + 1 + descLines.length },
5270
+ content: descLines.join('\n').slice(0, 500),
5271
+ });
5272
+ if (idCounter > 30)
5273
+ break; // 最多识别 30 个功能单元
5274
+ }
5275
+ if (idCounter > 30)
5276
+ break;
5277
+ }
5278
+ return units;
5279
+ }
5280
+ /** 保存单元分析结果到缓存 */
5281
+ async function saveUnitAnalysis(iteration, analysis) {
5282
+ const cacheDir = (0, path_1.join)(process.cwd(), exports.UNITS_CACHE_DIR, iteration);
5283
+ await (0, fs_extra_1.ensureDir)(cacheDir);
5284
+ const fp = (0, path_1.join)(cacheDir, `${analysis.id}.json`);
5285
+ await (0, fs_extra_1.writeFile)(fp, JSON.stringify(analysis, null, 2));
5286
+ }
5287
+ /** 读取已完成的单元分析 */
5288
+ async function loadUnitAnalyses(iteration) {
5289
+ const cacheDir = (0, path_1.join)(process.cwd(), exports.UNITS_CACHE_DIR, iteration);
5290
+ if (!await (0, fs_extra_1.pathExists)(cacheDir))
5291
+ return [];
5292
+ const files = (await (0, fs_extra_2.readdir)(cacheDir)).filter(f => f.endsWith('.json'));
5293
+ const results = [];
5294
+ for (const f of files) {
5295
+ try {
5296
+ const content = await (0, fs_extra_2.readFile)((0, path_1.join)(cacheDir, f), 'utf-8');
5297
+ results.push(JSON.parse(content));
5298
+ }
5299
+ catch { /* skip */ }
5300
+ }
5301
+ return results.sort((a, b) => a.id.localeCompare(b.id));
5302
+ }
5303
+ /** 检测未完成的单元分析 */
5304
+ async function detectPendingUnits(iteration, allUnits) {
5305
+ const completed = await loadUnitAnalyses(iteration);
5306
+ const completedIds = new Set(completed.map(a => a.id));
5307
+ return allUnits.filter(u => !completedIds.has(u.id));
5308
+ }
5309
+ /** 汇总所有单元分析为统一报告(带 [DOC:xxx] 标记) */
5310
+ async function consolidateUnitAnalyses(iteration, units, analyses) {
5311
+ const lines = [];
5312
+ lines.push(`# ${iteration} 迭代 Spec 统一报告`);
5313
+ lines.push(`> 生成时间: ${new Date().toISOString()}`);
5314
+ lines.push(`> 共 ${analyses.length}/${units.length} 个功能单元已完成分析`);
5315
+ lines.push('');
5316
+ // 汇总 REQUIREMENT.md
5317
+ lines.push('[DOC:REQUIREMENT.md]');
5318
+ lines.push(`# ${iteration} 需求规格`);
5319
+ lines.push('');
5320
+ lines.push('## 功能模块清单');
5321
+ lines.push('| 单元ID | 模块名称 | 需求来源 |');
5322
+ lines.push('|:---|:---|:---|');
5323
+ for (const u of units) {
5324
+ lines.push(`| ${u.id} | ${u.name} | ${u.source}#L${u.sourceRange.startLine} |`);
5325
+ }
5326
+ lines.push('');
5327
+ for (const a of analyses) {
5328
+ lines.push(`### ${a.id} ${a.name}`);
5329
+ lines.push(a.req.split('\n').slice(0, 5).join('\n'));
5330
+ lines.push('');
5331
+ }
5332
+ lines.push('');
5333
+ // 汇总 ANALYSIS.md
5334
+ lines.push('[DOC:ANALYSIS.md]');
5335
+ lines.push(`# ${iteration} 架构分析`);
5336
+ lines.push('');
5337
+ for (const a of analyses) {
5338
+ lines.push(`## ${a.id} ${a.name}`);
5339
+ lines.push(a.tech);
5340
+ lines.push('');
5341
+ }
5342
+ lines.push('');
5343
+ // 汇总 FUNCTION_MAP.md
5344
+ lines.push('[DOC:FUNCTION_MAP.md]');
5345
+ lines.push(`# ${iteration} 功能映射`);
5346
+ lines.push('');
5347
+ lines.push('| 单元ID | 功能模块 | 涉及端 | 优先级 |');
5348
+ lines.push('|:---|:---|:---|:---|');
5349
+ for (const a of analyses) {
5350
+ lines.push(`| ${a.id} | ${a.name} | ${(a.tech.match(/端[::]\s*(.+)/)?.[1] || '待定')} | P1 |`);
5351
+ }
5352
+ lines.push('');
5353
+ // 汇总 TECH.md
5354
+ lines.push('[DOC:TECH.md]');
5355
+ lines.push(`# ${iteration} 技术方案`);
5356
+ lines.push('');
5357
+ for (const a of analyses) {
5358
+ lines.push(`## ${a.id} ${a.name}`);
5359
+ lines.push(a.tech);
5360
+ lines.push('');
5361
+ }
5362
+ lines.push('');
5363
+ // 汇总 TEST.md
5364
+ lines.push('[DOC:TEST.md]');
5365
+ lines.push(`# ${iteration} 测试方案`);
5366
+ lines.push('');
5367
+ for (const a of analyses) {
5368
+ lines.push(`## ${a.id} ${a.name}`);
5369
+ lines.push(a.test);
5370
+ lines.push('');
5371
+ }
5372
+ lines.push('');
5373
+ // 汇总 RISK.md
5374
+ lines.push('[DOC:RISK.md]');
5375
+ lines.push(`# ${iteration} 风险评估`);
5376
+ lines.push('');
5377
+ for (const a of analyses) {
5378
+ if (a.risk) {
5379
+ lines.push(`## ${a.id} ${a.name}`);
5380
+ lines.push(a.risk);
5381
+ lines.push('');
5382
+ }
5383
+ }
5384
+ lines.push('');
5385
+ return lines.join('\n');
5386
+ }
5387
+ /** 构建单个功能单元的深度分析 Prompt */
5388
+ async function buildUnitAnalysisPrompt(iterDir, iteration, unit, allUnits) {
5389
+ // 使用智能上下文组装引擎(三层上下文模型)
5390
+ const unitRef = { id: unit.id, name: unit.name, content: unit.content };
5391
+ const allUnitRefs = allUnits.map(u => ({ id: u.id, name: u.name, content: u.content }));
5392
+ const ctxResult = await (0, unit_context_assembler_1.assembleUnitContext)(iterDir, iteration, unitRef, allUnitRefs, {
5393
+ maxTokens: 6000,
5394
+ maxRelatedUnits: 3,
5395
+ maxCodeFiles: 5,
5396
+ useKnowledgeGraph: true,
5397
+ });
5398
+ const prompt = `
5399
+ # 功能单元深度分析: ${unit.id} ${unit.name}
5400
+
5401
+ ## 迭代信息
5402
+ - 迭代: ${iteration}
5403
+ - 单元ID: ${unit.id}
5404
+ - 来源: ${unit.source}#L${unit.sourceRange.startLine}
5405
+ - 全迭代共 ${allUnits.length} 个功能单元
5406
+ - 上下文预算: ${ctxResult.stats.estimatedTokens} tokens (核心${ctxResult.stats.coreChars}字符 + 关联${ctxResult.stats.relatedUnitChars + ctxResult.stats.codeChars}字符 + 全局${ctxResult.stats.globalChars}字符)
5407
+
5408
+ ## 需求上下文
5409
+ ${ctxResult.context}
5410
+
5411
+ ## 分析要求
5412
+
5413
+ 请对「${unit.name}」进行深度分析,输出以下文档(使用 [DOC:xxx] 标记分隔):
5414
+
5415
+ ### [DOC:REQ.md] 需求规格
5416
+ - 功能描述(用户故事格式)
5417
+ - 输入/输出定义
5418
+ - 业务规则(R-XX 编号)
5419
+ - 异常场景
5420
+
5421
+ ### [DOC:TECH.md] 技术方案
5422
+ - 数据模型/接口设计
5423
+ - 核心算法/逻辑
5424
+ - 端分工(backend/web/admin 等)
5425
+ - 依赖服务
5426
+
5427
+ ### [DOC:TEST.md] 测试方案
5428
+ - 验收标准(Given-When-Then)
5429
+ - 边界值
5430
+ - 异常路径
5431
+
5432
+ ### [DOC:RISK.md] 风险评估(如适用)
5433
+ - 技术风险
5434
+ - 业务风险
5435
+ - 缓解方案
5436
+
5437
+ ## 输出格式
5438
+ \`\`\`
5439
+ [DOC:REQ.md]
5440
+ (内容...)
5441
+
5442
+ [DOC:TECH.md]
5443
+ (内容...)
5444
+
5445
+ [DOC:TEST.md]
5446
+ (内容...)
5447
+
5448
+ [DOC:RISK.md]
5449
+ (内容...)
5450
+ \`\`\`
5451
+
5452
+ ## 重要提示
5453
+ - 只分析当前功能单元「${unit.name}」,不要涉及其他模块
5454
+ - 每个文档必须有实质内容,不能是空模板
5455
+ - 使用具体的技术术语,避免泛泛而谈
5456
+ `;
5457
+ return prompt.trim();
5458
+ }
4487
5459
  //# sourceMappingURL=analyze.js.map