speccore 8.1.2 → 8.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cli.js +24 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +38 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +1046 -135
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/change.d.ts.map +1 -1
- package/dist/commands/change.js +28 -0
- package/dist/commands/change.js.map +1 -1
- package/dist/commands/clarify.d.ts +6 -0
- package/dist/commands/clarify.d.ts.map +1 -1
- package/dist/commands/clarify.js +172 -14
- package/dist/commands/clarify.js.map +1 -1
- package/dist/commands/done.d.ts.map +1 -1
- package/dist/commands/done.js +312 -3
- package/dist/commands/done.js.map +1 -1
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +112 -2
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +111 -14
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/pattern.d.ts +2 -0
- package/dist/commands/pattern.d.ts.map +1 -1
- package/dist/commands/pattern.js +43 -0
- package/dist/commands/pattern.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +99 -1
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/workspace.d.ts +20 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +142 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/core/ask-engine.d.ts.map +1 -1
- package/dist/core/ask-engine.js +18 -0
- package/dist/core/ask-engine.js.map +1 -1
- package/dist/core/intent-recognition.d.ts +7 -1
- package/dist/core/intent-recognition.d.ts.map +1 -1
- package/dist/core/intent-recognition.js +33 -3
- package/dist/core/intent-recognition.js.map +1 -1
- package/dist/core/knowledge-graph.d.ts +6 -1
- package/dist/core/knowledge-graph.d.ts.map +1 -1
- package/dist/core/knowledge-graph.js +268 -7
- package/dist/core/knowledge-graph.js.map +1 -1
- package/dist/core/pattern-detector.d.ts +26 -0
- package/dist/core/pattern-detector.d.ts.map +1 -0
- package/dist/core/pattern-detector.js +256 -0
- package/dist/core/pattern-detector.js.map +1 -0
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +40 -7
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/requirement-clarifier.d.ts +25 -2
- package/dist/core/requirement-clarifier.d.ts.map +1 -1
- package/dist/core/requirement-clarifier.js +205 -9
- package/dist/core/requirement-clarifier.js.map +1 -1
- package/dist/core/spec-skeleton.d.ts.map +1 -1
- package/dist/core/spec-skeleton.js +125 -25
- package/dist/core/spec-skeleton.js.map +1 -1
- package/dist/core/unit-context-assembler.d.ts +50 -0
- package/dist/core/unit-context-assembler.d.ts.map +1 -0
- package/dist/core/unit-context-assembler.js +442 -0
- package/dist/core/unit-context-assembler.js.map +1 -0
- package/dist/core/verify-engine.d.ts +4 -3
- package/dist/core/verify-engine.d.ts.map +1 -1
- package/dist/core/verify-engine.js +623 -24
- package/dist/core/verify-engine.js.map +1 -1
- package/dist/core/workspace-manager.d.ts +62 -0
- package/dist/core/workspace-manager.d.ts.map +1 -0
- package/dist/core/workspace-manager.js +175 -0
- package/dist/core/workspace-manager.js.map +1 -0
- package/package.json +1 -1
package/dist/commands/analyze.js
CHANGED
|
@@ -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
|
|
83
|
-
|
|
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
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
429
|
-
for (const f of
|
|
430
|
-
requirements.push((0, path_1.join)(
|
|
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
|
-
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
481
|
-
if (options.
|
|
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
|
-
|
|
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 (
|
|
622
|
+
if (needsClarify) {
|
|
494
623
|
logger_1.logger.info('');
|
|
495
|
-
logger_1.logger.info(`📋 ${
|
|
496
|
-
logger_1.logger.info('
|
|
497
|
-
logger_1.logger.info('
|
|
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
|
-
//
|
|
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
|
-
|
|
872
|
-
const
|
|
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(` 📝
|
|
1081
|
+
logger_1.logger.info(` 📝 黄金需求已写入: 020-specs/requirements/${reqFilePath.replace(/^converted\//, '')}`);
|
|
881
1082
|
count++;
|
|
882
1083
|
continue;
|
|
883
1084
|
}
|
|
@@ -1022,6 +1223,227 @@ async function analyzeCommand(options) {
|
|
|
1022
1223
|
// fallback to single-file mode
|
|
1023
1224
|
}
|
|
1024
1225
|
}
|
|
1226
|
+
// v8.2.0+: [DOC:xxx] 标记解析 —— 统一报告自动拆分
|
|
1227
|
+
// AI 输出格式: [DOC:REQUIREMENT.md]...内容...[DOC:TECH.md]...内容...
|
|
1228
|
+
const docBlocks = parseDocMarkers(options.apply);
|
|
1229
|
+
if (docBlocks.size > 1) {
|
|
1230
|
+
// 将 [DOC:xxx] 格式转换为 JSON 格式复用现有写入逻辑
|
|
1231
|
+
const docs = {};
|
|
1232
|
+
for (const [docName, content] of docBlocks) {
|
|
1233
|
+
docs[docName] = content;
|
|
1234
|
+
}
|
|
1235
|
+
// 构造一个 faux JSON 字符串让后面的 JSON 解析逻辑处理
|
|
1236
|
+
options.apply = JSON.stringify(docs);
|
|
1237
|
+
// 重新进入 JSON 处理分支(递归一次)
|
|
1238
|
+
try {
|
|
1239
|
+
const docs = JSON.parse(options.apply);
|
|
1240
|
+
let count = 0;
|
|
1241
|
+
if (isTaskLevel && taskDir) {
|
|
1242
|
+
const targetSubDir = options.platform && !isTaskLevel ? options.platform : '00-specs';
|
|
1243
|
+
const taskSpecDir = (0, path_1.join)(taskDir, targetSubDir);
|
|
1244
|
+
await (0, fs_extra_1.ensureDir)(taskSpecDir);
|
|
1245
|
+
for (const [filename, content] of Object.entries(docs)) {
|
|
1246
|
+
const fp = (0, path_1.join)(taskSpecDir, filename);
|
|
1247
|
+
if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
|
|
1248
|
+
logger_1.logger.info(` ⏭️ 跳过: ${filename}`);
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
const bk = await (0, task_utils_1.backupWithTimestamp)(fp);
|
|
1252
|
+
if (bk) {
|
|
1253
|
+
backups.push(bk);
|
|
1254
|
+
logger_1.logger.info(` 📦 ${filename} 旧版已备份: ${bk.split('/').pop()}`);
|
|
1255
|
+
}
|
|
1256
|
+
await (0, fs_extra_1.writeFile)(fp, content);
|
|
1257
|
+
count++;
|
|
1258
|
+
}
|
|
1259
|
+
const platformLabel = options.platform ? `/${options.platform}` : '';
|
|
1260
|
+
logger_1.logger.success(`✅ ${count} 个 Spec 文档已写入 ${options.task}${platformLabel}/(任务级,迭代基线不变)`);
|
|
1261
|
+
}
|
|
1262
|
+
else if (isGlobalScope) {
|
|
1263
|
+
const globalBaseDir = (0, path_1.join)(process.cwd(), '.speccore', 'GLOBAL');
|
|
1264
|
+
await (0, fs_extra_1.ensureDir)(globalBaseDir);
|
|
1265
|
+
const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
|
|
1266
|
+
for (const [filename, content] of Object.entries(docs)) {
|
|
1267
|
+
if (filename.startsWith('PATTERNS/')) {
|
|
1268
|
+
const patternsDir = (0, path_1.join)(process.cwd(), '.speccore', 'PATTERNS');
|
|
1269
|
+
await (0, fs_extra_1.ensureDir)(patternsDir);
|
|
1270
|
+
const patternFile = filename.slice('PATTERNS/'.length);
|
|
1271
|
+
const fp = (0, path_1.join)(patternsDir, patternFile);
|
|
1272
|
+
let existing = '';
|
|
1273
|
+
if (await (0, fs_extra_1.pathExists)(fp))
|
|
1274
|
+
existing = await (0, fs_extra_2.readFile)(fp, 'utf-8');
|
|
1275
|
+
const merged = existing ? `${existing}\n\n---\n\n${content}` : content;
|
|
1276
|
+
await (0, fs_extra_1.writeFile)(fp, merged);
|
|
1277
|
+
logger_1.logger.info(` 🧩 PATTERN 已追加: ${patternFile}`);
|
|
1278
|
+
count++;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
let targetDir;
|
|
1282
|
+
let targetFilename;
|
|
1283
|
+
if (filename.includes('/')) {
|
|
1284
|
+
const parts = filename.split('/');
|
|
1285
|
+
if (parts[0] === 'platforms' || parts[0] === 'requirements') {
|
|
1286
|
+
targetDir = (0, path_1.join)(globalBaseDir, ...parts.slice(0, -1));
|
|
1287
|
+
targetFilename = parts[parts.length - 1];
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
targetDir = (0, path_1.join)(globalBaseDir, 'platforms', parts[0]);
|
|
1291
|
+
targetFilename = parts[parts.length - 1];
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
else if (filename === 'REQUIREMENT.md') {
|
|
1295
|
+
targetDir = (0, path_1.join)(globalBaseDir, 'requirements');
|
|
1296
|
+
targetFilename = filename;
|
|
1297
|
+
}
|
|
1298
|
+
else if (globalSet.has(filename)) {
|
|
1299
|
+
targetDir = (0, path_1.join)(globalBaseDir, 'overview');
|
|
1300
|
+
targetFilename = filename;
|
|
1301
|
+
}
|
|
1302
|
+
else {
|
|
1303
|
+
targetDir = (0, path_1.join)(globalBaseDir, 'overview');
|
|
1304
|
+
targetFilename = filename;
|
|
1305
|
+
}
|
|
1306
|
+
await (0, fs_extra_1.ensureDir)(targetDir);
|
|
1307
|
+
const fp = (0, path_1.join)(targetDir, targetFilename);
|
|
1308
|
+
if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
|
|
1309
|
+
logger_1.logger.info(` ⏭️ 跳过: ${filename}`);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const bk = await (0, task_utils_1.backupWithTimestamp)(fp);
|
|
1313
|
+
if (bk) {
|
|
1314
|
+
backups.push(bk);
|
|
1315
|
+
logger_1.logger.info(` 📦 ${filename} 旧版已备份: ${bk.split('/').pop()}`);
|
|
1316
|
+
}
|
|
1317
|
+
await (0, fs_extra_1.writeFile)(fp, content);
|
|
1318
|
+
count++;
|
|
1319
|
+
}
|
|
1320
|
+
logger_1.logger.success(`✅ ${count} 个全局文档已写入 .speccore/GLOBAL/`);
|
|
1321
|
+
}
|
|
1322
|
+
else {
|
|
1323
|
+
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
1324
|
+
await (0, fs_extra_1.ensureDir)(specDir);
|
|
1325
|
+
const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
|
|
1326
|
+
const validPlatforms = new Set([spec_paths_1.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_1.parsePlatformList)())]);
|
|
1327
|
+
let skippedCount = 0;
|
|
1328
|
+
for (const [filename, content] of Object.entries(docs)) {
|
|
1329
|
+
// v8.2.0+: 黄金需求目录支持
|
|
1330
|
+
if (filename.startsWith('020-specs/requirements/')) {
|
|
1331
|
+
const reqFilePath = filename.slice('020-specs/requirements/'.length);
|
|
1332
|
+
const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
|
|
1333
|
+
const fp = (0, path_1.join)(goldenDir, reqFilePath);
|
|
1334
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(fp));
|
|
1335
|
+
let finalContent = content;
|
|
1336
|
+
if (reqFilePath.startsWith('clarified-') && !content.startsWith('---')) {
|
|
1337
|
+
finalContent = (0, requirement_clarifier_1.buildClarifiedHeader)(reqFilePath) + content;
|
|
1338
|
+
}
|
|
1339
|
+
await (0, fs_extra_1.writeFile)(fp, finalContent);
|
|
1340
|
+
logger_1.logger.info(` 📝 黄金需求已写入: 020-specs/requirements/${reqFilePath}`);
|
|
1341
|
+
count++;
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
// 向后兼容:010-requirements/ 自动重定向到黄金需求目录
|
|
1345
|
+
if (filename.startsWith('010-requirements/')) {
|
|
1346
|
+
const reqFilePath = filename.slice('010-requirements/'.length);
|
|
1347
|
+
const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
|
|
1348
|
+
const fp = (0, path_1.join)(goldenDir, reqFilePath.replace(/^converted\//, ''));
|
|
1349
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(fp));
|
|
1350
|
+
let finalContent = content;
|
|
1351
|
+
if (reqFilePath.startsWith('converted/clarified-') && !content.startsWith('---')) {
|
|
1352
|
+
finalContent = (0, requirement_clarifier_1.buildClarifiedHeader)(reqFilePath) + content;
|
|
1353
|
+
}
|
|
1354
|
+
await (0, fs_extra_1.writeFile)(fp, finalContent);
|
|
1355
|
+
logger_1.logger.info(` 📝 黄金需求已写入: 020-specs/requirements/${reqFilePath.replace(/^converted\//, '')}`);
|
|
1356
|
+
count++;
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (filename.startsWith('PATTERNS/')) {
|
|
1360
|
+
const patternsDir = (0, path_1.join)(process.cwd(), '.speccore', 'PATTERNS');
|
|
1361
|
+
await (0, fs_extra_1.ensureDir)(patternsDir);
|
|
1362
|
+
const patternFile = filename.slice('PATTERNS/'.length);
|
|
1363
|
+
const fp = (0, path_1.join)(patternsDir, patternFile);
|
|
1364
|
+
let existing = '';
|
|
1365
|
+
if (await (0, fs_extra_1.pathExists)(fp))
|
|
1366
|
+
existing = await (0, fs_extra_2.readFile)(fp, 'utf-8');
|
|
1367
|
+
const merged = existing ? `${existing}\n\n---\n\n${content}` : content;
|
|
1368
|
+
await (0, fs_extra_1.writeFile)(fp, merged);
|
|
1369
|
+
logger_1.logger.info(` 🧩 PATTERN 已追加: ${patternFile}`);
|
|
1370
|
+
count++;
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
let cleanFilename = filename;
|
|
1374
|
+
if (cleanFilename.startsWith(`${spec_paths_1.GLOBAL_SPECS_DIR}/`)) {
|
|
1375
|
+
cleanFilename = cleanFilename.slice(`${spec_paths_1.GLOBAL_SPECS_DIR}/`.length);
|
|
1376
|
+
}
|
|
1377
|
+
if (cleanFilename.includes('/')) {
|
|
1378
|
+
const prefix = cleanFilename.split('/')[0];
|
|
1379
|
+
if (!validPlatforms.has(prefix)) {
|
|
1380
|
+
logger_1.logger.warn(` ⚠️ 文件名含未知目录前缀 "${prefix}/",已自动剥离: ${cleanFilename}`);
|
|
1381
|
+
cleanFilename = cleanFilename.split('/').pop();
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const platformDir = cleanFilename.includes('/') ? cleanFilename.split('/')[0] : null;
|
|
1385
|
+
if (platformDir && !validPlatforms.has(platformDir)) {
|
|
1386
|
+
logger_1.logger.warn(` ⚠️ 跳过非法端目录: ${platformDir}(文件: ${cleanFilename})`);
|
|
1387
|
+
skippedCount++;
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
const targetDir = globalSet.has(cleanFilename)
|
|
1391
|
+
? (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR)
|
|
1392
|
+
: options.platform ? (0, path_1.join)(specDir, options.platform) : specDir;
|
|
1393
|
+
await (0, fs_extra_1.ensureDir)(targetDir);
|
|
1394
|
+
const fp = (0, path_1.join)(targetDir, cleanFilename);
|
|
1395
|
+
if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
|
|
1396
|
+
logger_1.logger.info(` ⏭️ 跳过: ${filename}`);
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
const bk = await (0, task_utils_1.backupWithTimestamp)(fp);
|
|
1400
|
+
if (bk) {
|
|
1401
|
+
backups.push(bk);
|
|
1402
|
+
logger_1.logger.info(` 📦 ${filename} 旧版已备份: ${bk.split('/').pop()}`);
|
|
1403
|
+
}
|
|
1404
|
+
await (0, fs_extra_1.writeFile)(fp, content);
|
|
1405
|
+
count++;
|
|
1406
|
+
}
|
|
1407
|
+
if (skippedCount > 0) {
|
|
1408
|
+
logger_1.logger.warn(`⚠️ 共跳过 ${skippedCount} 个非法目录的文档,请检查 AI 输出是否包含非端名目录`);
|
|
1409
|
+
}
|
|
1410
|
+
logger_1.logger.success(`✅ ${count} 个 Spec 文档已写入 020-specs/`);
|
|
1411
|
+
await sanitizeSpecDirectories(iterDir);
|
|
1412
|
+
}
|
|
1413
|
+
// v8.2.0+: 单元分析结果保存到缓存(供 --consolidate 汇总使用)
|
|
1414
|
+
if (options.unit && options.iteration) {
|
|
1415
|
+
try {
|
|
1416
|
+
const unitsCachePath = (0, path_1.join)(process.cwd(), exports.UNITS_CACHE_DIR, options.iteration, 'units.json');
|
|
1417
|
+
let unitName = options.unit;
|
|
1418
|
+
if (await (0, fs_extra_1.pathExists)(unitsCachePath)) {
|
|
1419
|
+
const allUnits = JSON.parse(await (0, fs_extra_2.readFile)(unitsCachePath, 'utf-8'));
|
|
1420
|
+
const matched = allUnits.find(u => u.id === options.unit);
|
|
1421
|
+
if (matched)
|
|
1422
|
+
unitName = matched.name;
|
|
1423
|
+
}
|
|
1424
|
+
const unitAnalysis = {
|
|
1425
|
+
id: options.unit,
|
|
1426
|
+
name: unitName,
|
|
1427
|
+
req: docs['REQ.md'] || docs['REQUIREMENT.md'] || '',
|
|
1428
|
+
tech: docs['TECH.md'] || docs['ANALYSIS.md'] || '',
|
|
1429
|
+
test: docs['TEST.md'] || '',
|
|
1430
|
+
risk: docs['RISK.md'] || '',
|
|
1431
|
+
timestamp: new Date().toISOString(),
|
|
1432
|
+
};
|
|
1433
|
+
await saveUnitAnalysis(options.iteration, unitAnalysis);
|
|
1434
|
+
logger_1.logger.info(`💾 单元分析 ${options.unit} 已缓存(供 --consolidate 使用)`);
|
|
1435
|
+
}
|
|
1436
|
+
catch (e) {
|
|
1437
|
+
logger_1.logger.debug('单元分析缓存失败(非关键):', e);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
printBackupSummary();
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
catch (e) {
|
|
1444
|
+
logger_1.logger.warn('⚠️ [DOC:xxx] 解析后写入失败,回退到单文件模式');
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1025
1447
|
// 单文件模式
|
|
1026
1448
|
if (isTaskLevel && taskDir) {
|
|
1027
1449
|
// 任务级:写 Task/00-specs/(v6.44.0+)
|
|
@@ -1265,7 +1687,7 @@ async function analyzeCommand(options) {
|
|
|
1265
1687
|
}
|
|
1266
1688
|
catch { /* ignore */ }
|
|
1267
1689
|
}
|
|
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);
|
|
1690
|
+
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
1691
|
process.stdout.write(`[SPECCORE_PROMPT]\n${prompt}`);
|
|
1270
1692
|
// v7.2.0+: 全局分析完成后输出下一步引导
|
|
1271
1693
|
if (options.scope === 'global') {
|
|
@@ -1895,13 +2317,37 @@ async function detectGlobalLayerProgress() {
|
|
|
1895
2317
|
missing.push('Layer 2: platforms/_shared/_ASSOCIATION.md + _MODULES.md');
|
|
1896
2318
|
}
|
|
1897
2319
|
}
|
|
1898
|
-
// Layer 3:
|
|
2320
|
+
// Layer 3: 检查各端功能模块深入文档(v8.3.0+ 修复:不再把 _MODULES.md 当 Layer 3 产物)
|
|
1899
2321
|
if (completedLayer >= 2) {
|
|
1900
|
-
|
|
1901
|
-
|
|
2322
|
+
try {
|
|
2323
|
+
const platformsDir = (0, path_1.join)(globalDir, 'platforms');
|
|
2324
|
+
const entries = await (0, fs_extra_2.readdir)(platformsDir, { withFileTypes: true });
|
|
2325
|
+
const platformDirs = entries.filter(e => e.isDirectory() && e.name !== '_shared').map(e => e.name);
|
|
2326
|
+
if (platformDirs.length === 0) {
|
|
2327
|
+
missing.push('Layer 3: platforms/{端}/modules/*.md(功能模块深入文档)');
|
|
2328
|
+
}
|
|
2329
|
+
else {
|
|
2330
|
+
const moduleDocChecks = await Promise.all(platformDirs.map(async (d) => {
|
|
2331
|
+
const modulesDir = (0, path_1.join)(platformsDir, d, 'modules');
|
|
2332
|
+
if (!await (0, fs_extra_1.pathExists)(modulesDir))
|
|
2333
|
+
return false;
|
|
2334
|
+
const files = await (0, fs_extra_2.readdir)(modulesDir).catch(() => []);
|
|
2335
|
+
return files.some(f => f.endsWith('.md'));
|
|
2336
|
+
}));
|
|
2337
|
+
if (moduleDocChecks.every(Boolean)) {
|
|
2338
|
+
completedLayer = 3;
|
|
2339
|
+
}
|
|
2340
|
+
else {
|
|
2341
|
+
for (const [i, d] of platformDirs.entries()) {
|
|
2342
|
+
if (!moduleDocChecks[i]) {
|
|
2343
|
+
missing.push(`Layer 3: platforms/${d}/modules/*.md(功能模块深入文档)`);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
1902
2348
|
}
|
|
1903
|
-
|
|
1904
|
-
missing.push('Layer 3: platforms/
|
|
2349
|
+
catch {
|
|
2350
|
+
missing.push('Layer 3: platforms/{端}/modules/*.md(功能模块深入文档)');
|
|
1905
2351
|
}
|
|
1906
2352
|
}
|
|
1907
2353
|
// Layer 4 子层检测
|
|
@@ -1910,10 +2356,28 @@ async function detectGlobalLayerProgress() {
|
|
|
1910
2356
|
const overviewDir = (0, path_1.join)(globalDir, 'overview');
|
|
1911
2357
|
const requirementsDir = (0, path_1.join)(globalDir, 'requirements');
|
|
1912
2358
|
const completedSubLayers = [];
|
|
1913
|
-
// 4a:
|
|
2359
|
+
// 4a: 产品文档(全局 + 各端,v8.3.0+ 修复:检查各端 REQUIREMENT.md)
|
|
1914
2360
|
const hasReq = await (0, fs_extra_1.pathExists)((0, path_1.join)(requirementsDir, 'REQUIREMENT.md'));
|
|
1915
|
-
|
|
2361
|
+
let allPlatformReqsExist = true;
|
|
2362
|
+
try {
|
|
2363
|
+
const platformsDir = (0, path_1.join)(globalDir, 'platforms');
|
|
2364
|
+
const entries = await (0, fs_extra_2.readdir)(platformsDir, { withFileTypes: true });
|
|
2365
|
+
const platformDirs = entries.filter(e => e.isDirectory() && e.name !== '_shared').map(e => e.name);
|
|
2366
|
+
const platformReqChecks = await Promise.all(platformDirs.map(p => (0, fs_extra_1.pathExists)((0, path_1.join)(requirementsDir, p, 'REQUIREMENT.md'))));
|
|
2367
|
+
allPlatformReqsExist = platformReqChecks.every(Boolean);
|
|
2368
|
+
for (const [i, p] of platformDirs.entries()) {
|
|
2369
|
+
if (!platformReqChecks[i]) {
|
|
2370
|
+
missing.push(`Layer 4a: requirements/${p}/REQUIREMENT.md(${p} 产品视角需求)`);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
catch { /* ignore */ }
|
|
2375
|
+
if (hasReq && allPlatformReqsExist) {
|
|
1916
2376
|
completedSubLayers.push('4a');
|
|
2377
|
+
}
|
|
2378
|
+
else if (!hasReq) {
|
|
2379
|
+
missing.push('Layer 4a: requirements/REQUIREMENT.md(全局需求总纲)');
|
|
2380
|
+
}
|
|
1917
2381
|
// 4b: 全局技术核心文档
|
|
1918
2382
|
const hasCoreTech = await (0, fs_extra_1.pathExists)((0, path_1.join)(overviewDir, 'ARCHITECTURE.md'))
|
|
1919
2383
|
&& await (0, fs_extra_1.pathExists)((0, path_1.join)(overviewDir, 'FUNCTION_MAP.md'));
|
|
@@ -1924,15 +2388,42 @@ async function detectGlobalLayerProgress() {
|
|
|
1924
2388
|
|| await (0, fs_extra_1.pathExists)((0, path_1.join)(overviewDir, 'DATA_FLOW.md'));
|
|
1925
2389
|
if (hasExtTech)
|
|
1926
2390
|
completedSubLayers.push('4c');
|
|
1927
|
-
// 4d:
|
|
2391
|
+
// 4d: 各端技术文档(v8.3.0+ 修复:每个端都要有,不再用 some(Boolean))
|
|
1928
2392
|
try {
|
|
1929
2393
|
const platformsDir = (0, path_1.join)(globalDir, 'platforms');
|
|
1930
2394
|
const entries = await (0, fs_extra_2.readdir)(platformsDir, { withFileTypes: true });
|
|
1931
2395
|
const platformDirs = entries.filter(e => e.isDirectory() && e.name !== '_shared').map(e => e.name);
|
|
1932
|
-
const
|
|
1933
|
-
|
|
1934
|
-
|
|
2396
|
+
const missingPlatformDocs = [];
|
|
2397
|
+
for (const d of platformDirs) {
|
|
2398
|
+
const isBackend = /service|server|api|backend/i.test(d);
|
|
2399
|
+
if (isBackend) {
|
|
2400
|
+
const hasApi = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'API_INVENTORY.md'));
|
|
2401
|
+
const hasData = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'DATA_MODEL.md'));
|
|
2402
|
+
if (!hasApi)
|
|
2403
|
+
missingPlatformDocs.push(`platforms/${d}/API_INVENTORY.md`);
|
|
2404
|
+
if (!hasData)
|
|
2405
|
+
missingPlatformDocs.push(`platforms/${d}/DATA_MODEL.md`);
|
|
2406
|
+
}
|
|
2407
|
+
else {
|
|
2408
|
+
const hasUi = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'UI_FLOW.md'));
|
|
2409
|
+
const hasApiMap = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'API_CALL_MAP.md'));
|
|
2410
|
+
const hasState = await (0, fs_extra_1.pathExists)((0, path_1.join)(platformsDir, d, 'STATE_MANAGEMENT.md'));
|
|
2411
|
+
if (!hasUi)
|
|
2412
|
+
missingPlatformDocs.push(`platforms/${d}/UI_FLOW.md`);
|
|
2413
|
+
if (!hasApiMap)
|
|
2414
|
+
missingPlatformDocs.push(`platforms/${d}/API_CALL_MAP.md`);
|
|
2415
|
+
if (!hasState)
|
|
2416
|
+
missingPlatformDocs.push(`platforms/${d}/STATE_MANAGEMENT.md`);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
if (missingPlatformDocs.length === 0 && platformDirs.length > 0) {
|
|
1935
2420
|
completedSubLayers.push('4d');
|
|
2421
|
+
}
|
|
2422
|
+
else {
|
|
2423
|
+
for (const doc of missingPlatformDocs) {
|
|
2424
|
+
missing.push(`Layer 4d: ${doc}`);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
1936
2427
|
}
|
|
1937
2428
|
catch { /* ignore */ }
|
|
1938
2429
|
if (completedSubLayers.length === 4) {
|
|
@@ -2111,7 +2602,7 @@ async function buildLayer3ModuleContext(projectRoot) {
|
|
|
2111
2602
|
return ctx.join('\n\n---\n\n');
|
|
2112
2603
|
}
|
|
2113
2604
|
// ── buildMultiDocPrompt: 多文档协议 ──
|
|
2114
|
-
async function buildMultiDocPrompt(command, ctx, options) {
|
|
2605
|
+
async function buildMultiDocPrompt(command, ctx, options, clarifyCtx) {
|
|
2115
2606
|
const iter = ctx.iteration || '当前迭代';
|
|
2116
2607
|
const task = ctx.task ? ` — ${ctx.task}` : '';
|
|
2117
2608
|
const taskType = ctx.type || 'feature';
|
|
@@ -2119,6 +2610,9 @@ async function buildMultiDocPrompt(command, ctx, options) {
|
|
|
2119
2610
|
const isTask = ctx.scope === 'task' || !!ctx.task;
|
|
2120
2611
|
const isGlobal = ctx.scope === 'global';
|
|
2121
2612
|
const autoMode = ctx.autoMode || false;
|
|
2613
|
+
// v8.3.0+: 从 clarifyCtx 提取需求澄清状态
|
|
2614
|
+
const needsClarify = clarifyCtx?.needsClarify ?? false;
|
|
2615
|
+
const clarifyTargets = clarifyCtx?.clarifyTargets ?? [];
|
|
2122
2616
|
// global 范围: 从源码反推需求 + 生成技术栈配置
|
|
2123
2617
|
if (isGlobal) {
|
|
2124
2618
|
// v6.74.0+: 流式全局分析模式
|
|
@@ -3167,38 +3661,46 @@ sequenceDiagram
|
|
|
3167
3661
|
`# 开发者实现指南
|
|
3168
3662
|
|
|
3169
3663
|
> ${iter} | ${now}
|
|
3170
|
-
>
|
|
3664
|
+
> 本文档面向开发者,提供**可执行的实现指导**——开发者读完知道改哪些文件、先改什么后改什么、具体怎么改、改完怎么验证。
|
|
3171
3665
|
|
|
3172
3666
|
## 写作要求
|
|
3173
3667
|
|
|
3174
3668
|
### 迭代级 DEV_GUIDE.md(020-specs/overview/)
|
|
3175
|
-
|
|
3176
|
-
-
|
|
3177
|
-
-
|
|
3178
|
-
-
|
|
3179
|
-
-
|
|
3180
|
-
-
|
|
3181
|
-
-
|
|
3669
|
+
面向全迭代,提供全局实施指导:
|
|
3670
|
+
- **分支策略**:从哪个分支切 feature 分支,合并到哪里,分支命名规范
|
|
3671
|
+
- **改造范围清单**:本迭代所有需要修改的文件(新增/修改/删除),按类型分类
|
|
3672
|
+
- **改造顺序与依赖**:按依赖关系排列的实施步骤,说明每步为什么先做
|
|
3673
|
+
- **关键实现步骤(代码级)**:具体到文件/函数级的改造方式,包含改造前后对比
|
|
3674
|
+
- **接口契约(前后端对照)**:所有接口的路径、后端实现位置、前端调用位置、状态
|
|
3675
|
+
- **每步验证方式**:每步改完用什么命令/操作验证,通过标准是什么
|
|
3676
|
+
- **回滚方案**:数据库变更的 DOWN migration、代码回滚方式、配置回滚方式
|
|
3677
|
+
- **常见坑点**:已知陷阱及解决方式
|
|
3182
3678
|
|
|
3183
3679
|
### 端级 DEV_GUIDE.md(020-specs/{端}/)
|
|
3184
|
-
|
|
3185
|
-
-
|
|
3186
|
-
-
|
|
3187
|
-
-
|
|
3188
|
-
-
|
|
3189
|
-
-
|
|
3680
|
+
面向本端开发者:
|
|
3681
|
+
- **本端改造范围**:本端需要修改的文件清单
|
|
3682
|
+
- **本端实施步骤(按依赖排序)**:具体到文件/函数的开发顺序
|
|
3683
|
+
- **代码级指引**:改哪几行、改什么、改造后的代码片段
|
|
3684
|
+
- **与后端联调**:接口状态、联调顺序、Mock 方案
|
|
3685
|
+
- **验证方式**:本端特有的验证命令/操作和通过标准
|
|
3686
|
+
- **本端坑点**:该端特有的陷阱
|
|
3190
3687
|
|
|
3191
3688
|
### 任务级 DEV_GUIDE.md(Task/00-specs/)
|
|
3192
|
-
|
|
3193
|
-
-
|
|
3194
|
-
-
|
|
3195
|
-
-
|
|
3196
|
-
-
|
|
3197
|
-
-
|
|
3689
|
+
面向具体任务开发者,最细粒度:
|
|
3690
|
+
- **本任务改造范围**:该任务涉及的具体文件(新增/修改/删除)
|
|
3691
|
+
- **实施步骤(按依赖排序)**:Step-by-step,具体到文件/函数级
|
|
3692
|
+
- **关键代码指引**:核心改造点的文件位置、改造方式、改造后代码示例
|
|
3693
|
+
- **接口契约**:本任务涉及的前后接口对照表
|
|
3694
|
+
- **每步验证**:每步改完怎么验证(命令/操作 + 通过标准)
|
|
3695
|
+
- **回滚方案**:本任务改坏了怎么回退
|
|
3696
|
+
- **已知坑点**:常见坑点及解决方式
|
|
3198
3697
|
|
|
3199
3698
|
## 质量要求
|
|
3200
3699
|
- 必须是**可执行的实现指导**,不是抽象概念
|
|
3201
|
-
-
|
|
3700
|
+
- **改造范围**必须列出具体文件路径(相对项目根目录)
|
|
3701
|
+
- **改造顺序**必须说明依赖关系(为什么 A 必须在 B 之前)
|
|
3702
|
+
- **代码示例**必须具体到文件/函数/行号级别
|
|
3703
|
+
- **验证方式**必须是可执行的命令或操作(不是"测试通过"这种空话)
|
|
3202
3704
|
- 基于 020-specs/ 中已有的分析文档,不做重复分析
|
|
3203
3705
|
- 补充技术文档中未涉及的实现细节
|
|
3204
3706
|
- 如果涉及存量功能,说明复用方式和集成点
|
|
@@ -3246,15 +3748,19 @@ sequenceDiagram
|
|
|
3246
3748
|
if (perDocStatus.nextUnfilled) {
|
|
3247
3749
|
const nextDocName = perDocStatus.nextUnfilled.docName;
|
|
3248
3750
|
const nextDocPlatform = perDocStatus.nextUnfilled.platform || null;
|
|
3249
|
-
//
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3751
|
+
// v8.2.0+: 功能单元聚焦模式不强制单文档过滤,让 AI 一次输出完整单元分析
|
|
3752
|
+
const isUnitMode = !!(options?.unit || options?.consolidate || options?.extractUnits);
|
|
3753
|
+
if (!isUnitMode) {
|
|
3754
|
+
// 过滤 taskDocs 只保留下一个未填充的文档(默认逐文档推进模式)
|
|
3755
|
+
if (nextDocPlatform && perDocStatus.nextUnfilled.category === 'platform') {
|
|
3756
|
+
// Phase 2 文档(端专属)
|
|
3757
|
+
taskDocs = taskDocs.filter(([n]) => n === nextDocName);
|
|
3758
|
+
ctx.platform = nextDocPlatform;
|
|
3759
|
+
}
|
|
3760
|
+
else {
|
|
3761
|
+
// Phase 1 文档(overview)
|
|
3762
|
+
taskDocs = taskDocs.filter(([n]) => n === nextDocName);
|
|
3763
|
+
}
|
|
3258
3764
|
}
|
|
3259
3765
|
logger_1.logger.info(`🦴 骨架进度: ${perDocStatus.filledCount}/${perDocStatus.totalCount} 已填充,下一个: ${perDocStatus.nextUnfilled.relPath}`);
|
|
3260
3766
|
// v8.1.0+: 校验上一个已填充文档的质量
|
|
@@ -3412,6 +3918,57 @@ sequenceDiagram
|
|
|
3412
3918
|
techDoc[1] = `# 技术架构(跨端全局)\n\n> ${iter}\n\n## 写作要求\n撰写整体技术架构,覆盖所有端的交互关系:\n- 系统整体分层设计(各端在架构中的位置)\n- 跨端交互协议(前端↔后端通信方式、数据流向)\n- 中间件选型(缓存、消息队列、网关等)\n- 数据库整体设计(核心表结构、ER 关系)\n- 技术栈选型及理由\n`;
|
|
3413
3919
|
}
|
|
3414
3920
|
let prompt = `\n# 任务: ${command}${task} (${taskDocs.length}个文档 · ${isTask ? `类型:${taskType}` : '迭代全量'}${ctx.phase ? ` · Phase ${ctx.phase}` : ''})\n\n`;
|
|
3921
|
+
// v8.3.0+: 如果检测到需求质量不足,在 prompt 开头注入强制澄清阶段
|
|
3922
|
+
if (needsClarify && !isGlobal && !isTask && !ctx.phase) {
|
|
3923
|
+
const now = new Date().toISOString();
|
|
3924
|
+
prompt += `## 🚨 Phase 0: 需求澄清(强制前置 — 未完成则禁止进入 Phase 1)\n\n`;
|
|
3925
|
+
prompt += `检测到以下需求文档质量不足,**必须先完成专业化整理,才能继续技术分析**:\n\n`;
|
|
3926
|
+
for (const t of clarifyTargets) {
|
|
3927
|
+
prompt += `- \`${t.path.split('/').slice(-2).join('/')}\`(质量: ${t.level.toUpperCase()})\n`;
|
|
3928
|
+
}
|
|
3929
|
+
prompt += `\n### 澄清执行步骤(必须按顺序完成)\n\n`;
|
|
3930
|
+
prompt += `**Step 1**: 读取上述每个需求文档的原始内容\n`;
|
|
3931
|
+
prompt += `**Step 2**: 将口语化/非专业描述整理为 PRD 级专业文档\n`;
|
|
3932
|
+
prompt += `**Step 3**: 补充:验收标准(AC)、功能边界、业务规则、异常处理、数据模型\n`;
|
|
3933
|
+
prompt += `**Step 4**: 输出澄清后的文档,使用以下标记格式:\n`;
|
|
3934
|
+
prompt += `\`\`\`
|
|
3935
|
+
[CLARIFY:requirements/clarified-{feature-name}.md]
|
|
3936
|
+
---
|
|
3937
|
+
source: "原始文档路径"
|
|
3938
|
+
clarified-at: "${now}"
|
|
3939
|
+
status: "clarified"
|
|
3940
|
+
---
|
|
3941
|
+
|
|
3942
|
+
# {功能名称}
|
|
3943
|
+
|
|
3944
|
+
## 背景与目标
|
|
3945
|
+
...
|
|
3946
|
+
|
|
3947
|
+
## 用户故事
|
|
3948
|
+
...
|
|
3949
|
+
|
|
3950
|
+
## 功能规格
|
|
3951
|
+
...
|
|
3952
|
+
|
|
3953
|
+
## 验收标准(AC)
|
|
3954
|
+
- [ ] ...
|
|
3955
|
+
|
|
3956
|
+
## 非功能需求
|
|
3957
|
+
...
|
|
3958
|
+
|
|
3959
|
+
## 依赖与约束
|
|
3960
|
+
...
|
|
3961
|
+
\`\`\`
|
|
3962
|
+
`;
|
|
3963
|
+
prompt += `**Step 5**: 用 Read 工具验证文件已正确写入 \`{迭代}/020-specs/requirements/clarified-{feature-name}.md\`\n`;
|
|
3964
|
+
prompt += `**Step 6**: 基于澄清后的需求继续 Phase 1 分析\n\n`;
|
|
3965
|
+
prompt += `### ⚠️ 重要提醒\n\n`;
|
|
3966
|
+
prompt += `- **如果输出中没有 [CLARIFY:xxx] 标记的澄清文档,--apply 阶段将拒绝写入所有分析结果**\n`;
|
|
3967
|
+
prompt += `- CLI 会在接收 --apply 时先解析 [CLARIFY:xxx] 标记,写入 020-specs/requirements/,然后才处理 [DOC:xxx] 标记\n`;
|
|
3968
|
+
prompt += `- 不要跳过此步骤,不要假设需求已经够清晰\n`;
|
|
3969
|
+
prompt += `- 如果原始描述不完整,在澄清文档中标注「待补充」而不是自行编造\n\n`;
|
|
3970
|
+
prompt += `---\n\n`;
|
|
3971
|
+
}
|
|
3415
3972
|
// v7.2.0+: 迭代分析代码关联 — 注入结构化数据和语义定位上下文
|
|
3416
3973
|
if (!isGlobal && ctx.withCode && ctx.iteration && ctx.iteration !== 'GLOBAL') {
|
|
3417
3974
|
try {
|
|
@@ -3529,24 +4086,37 @@ sequenceDiagram
|
|
|
3529
4086
|
try {
|
|
3530
4087
|
const iterDirForCtx = await (0, context_1.getIterationDir)(iter);
|
|
3531
4088
|
if (iterDirForCtx) {
|
|
3532
|
-
//
|
|
3533
|
-
const reqDir = (0, path_1.join)(iterDirForCtx, '010-requirements');
|
|
4089
|
+
// v8.2.0+: 注入 PRD 内容(优先从黄金需求目录 020-specs/requirements/ 读取)
|
|
3534
4090
|
let prdContent = '';
|
|
3535
|
-
const
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
4091
|
+
const goldenReqDir = (0, path_1.join)(iterDirForCtx, '020-specs', 'requirements');
|
|
4092
|
+
if (await (0, fs_extra_1.pathExists)(goldenReqDir)) {
|
|
4093
|
+
// 黄金需求目录存在,只读这里(唯一依据)
|
|
4094
|
+
const files = (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readdirSync(goldenReqDir).filter((f) => f.endsWith('.md')).slice(0, 10);
|
|
4095
|
+
for (const f of files) {
|
|
4096
|
+
const content = await (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readFile((0, path_1.join)(goldenReqDir, f), 'utf-8');
|
|
4097
|
+
prdContent += `\n### ${f}\n${content.slice(0, 2000)}\n`;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
else {
|
|
4101
|
+
// 回退:从 010-requirements/ 读取原始需求
|
|
4102
|
+
const reqDir = (0, path_1.join)(iterDirForCtx, '010-requirements');
|
|
4103
|
+
const prdSources = (0, path_1.join)(reqDir, 'sources');
|
|
4104
|
+
const prdConverted = (0, path_1.join)(reqDir, 'converted');
|
|
4105
|
+
const prdFeatures = (0, path_1.join)(reqDir, 'features');
|
|
4106
|
+
for (const dir of [prdSources, prdConverted, prdFeatures]) {
|
|
4107
|
+
if (await (0, fs_extra_1.pathExists)(dir)) {
|
|
4108
|
+
const files = (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readdirSync(dir).filter((f) => f.endsWith('.md')).slice(0, 5);
|
|
4109
|
+
for (const f of files) {
|
|
4110
|
+
const content = await (await Promise.resolve().then(() => __importStar(require('fs-extra')))).readFile((0, path_1.join)(dir, f), 'utf-8');
|
|
4111
|
+
prdContent += `\n### ${f}\n${content.slice(0, 2000)}\n`;
|
|
4112
|
+
}
|
|
3544
4113
|
}
|
|
3545
4114
|
}
|
|
3546
4115
|
}
|
|
3547
4116
|
if (prdContent.length > 0) {
|
|
3548
4117
|
prompt += `\n## 📎 需求文档原文(PRD,写作核心输入)\n\n`;
|
|
3549
|
-
|
|
4118
|
+
const sourceLabel = await (0, fs_extra_1.pathExists)(goldenReqDir) ? '020-specs/requirements/(黄金需求,已澄清)' : '010-requirements/(原始需求)';
|
|
4119
|
+
prompt += `> 以下是 ${sourceLabel} 下的需求文档,这是你撰写专业分析文档的核心输入。所有分析必须基于这些内容,不要臆造。\n`;
|
|
3550
4120
|
prompt += prdContent.slice(0, 5000);
|
|
3551
4121
|
prompt += `\n\n`;
|
|
3552
4122
|
}
|
|
@@ -3641,24 +4211,48 @@ sequenceDiagram
|
|
|
3641
4211
|
prompt += `- Read 020-specs/overview/ANALYSIS.md → 分析报告\n`;
|
|
3642
4212
|
prompt += `- Read 020-specs/overview/TECH.md → 整体技术架构\n`;
|
|
3643
4213
|
prompt += `- Read 020-specs/overview/RISK.md、DEPS.md、REVIEW.md、MONITOR.md(如存在)\n\n`;
|
|
3644
|
-
prompt += `### Step 2:
|
|
3645
|
-
prompt += `根据全局上下文,为 PLATFORMS.md
|
|
3646
|
-
prompt +=
|
|
4214
|
+
prompt += `### Step 2: 为每个端撰写专属文档(v8.3.1+ 强制四文档)\n`;
|
|
4215
|
+
prompt += `根据全局上下文,为 PLATFORMS.md 中的**每个端**分别撰写以下 **4 份文档**,缺一不可:\n\n`;
|
|
4216
|
+
prompt += `**1. {端}/TECH.md — 该端专属技术方案(必须对齐 overview/TECH.md 架构)**\n`;
|
|
4217
|
+
prompt += ` - 该端的分层架构、模块划分、核心接口设计(路径/方法/参数/响应/状态码)\n`;
|
|
4218
|
+
prompt += ` - 数据库表结构(字段/类型/索引/约束,如适用)\n`;
|
|
4219
|
+
prompt += ` - 业务规则实现(含边界条件和异常流)\n`;
|
|
3647
4220
|
prompt += ` - ⚠️ **必须包含「业务-代码映射」章节**:在 TECH.md 末尾添加表格,列出本端涉及的业务模块及其对应的代码实体(文件/表/API/组件等),关系类型由你根据技术栈自主决定(如 api_controller、uses_table、page、component、route、middleware、interceptor、gateway 等)\n`;
|
|
3648
4221
|
prompt += ` - 表格格式:| 业务模块 | 代码实体 | 关系类型 | 说明 |\n`;
|
|
3649
4222
|
prompt += ` - 示例:| 会议室档案 | backend/RoomController.java | api_controller | REST 控制器 |\n`;
|
|
3650
|
-
prompt += ` - 示例:| 会议室档案 | admin-web/src/pages/RoomList.vue | page | 列表页 |\n`;
|
|
3651
|
-
prompt +=
|
|
3652
|
-
prompt +=
|
|
4223
|
+
prompt += ` - 示例:| 会议室档案 | admin-web/src/pages/RoomList.vue | page | 列表页 |\n\n`;
|
|
4224
|
+
prompt += `**2. {端}/TEST.md — 该端专属测试计划**\n`;
|
|
4225
|
+
prompt += ` - 覆盖该端所有功能模块的测试用例(含前置条件、步骤、预期结果)\n`;
|
|
4226
|
+
prompt += ` - 边界值和异常输入测试\n`;
|
|
4227
|
+
prompt += ` - 必须覆盖 REQUIREMENT.md 中该端的验收标准\n\n`;
|
|
4228
|
+
prompt += `**3. {端}/UI_SPEC.md — 该端专属 UI 规格(仅前端端需要)**\n`;
|
|
4229
|
+
prompt += ` - 页面结构与路由(每个页面的路径、入口、权限)\n`;
|
|
4230
|
+
prompt += ` - 组件清单和字段→UI 映射\n`;
|
|
4231
|
+
prompt += ` - 状态枚举和错误处理策略\n\n`;
|
|
4232
|
+
prompt += `**4. {端}/DEV_GUIDE.md — 该端开发者实现指南(v8.3.1+ 新增强制要求)**\n`;
|
|
4233
|
+
prompt += ` - ⛔ **禁止只写框架/占位符** — 必须是可执行的实现指导\n`;
|
|
4234
|
+
prompt += ` - **本端改造范围清单**:列出本端所有需要新增/修改/删除的文件(相对项目根目录的具体路径)\n`;
|
|
4235
|
+
prompt += ` - **本端实施步骤(按依赖排序)**:Step-by-step,具体到文件/函数级,说明每步为什么先做\n`;
|
|
4236
|
+
prompt += ` - **代码级指引**:改哪几行、改什么、改造后的关键代码片段\n`;
|
|
4237
|
+
prompt += ` - **与后端联调**:接口状态、联调顺序、Mock 方案(表格格式)\n`;
|
|
4238
|
+
prompt += ` - **验证方式**:本端特有的验证命令/操作和通过标准(表格格式)\n`;
|
|
4239
|
+
prompt += ` - **本端坑点**:已知陷阱及解决方式\n`;
|
|
4240
|
+
prompt += ` - **回滚方案**:代码回滚方式、配置回滚方式\n\n`;
|
|
3653
4241
|
prompt += `### Step 3: 一致性检查\n`;
|
|
3654
4242
|
prompt += `- 各端 TECH.md 的技术选型必须与 overview/TECH.md 一致\n`;
|
|
3655
4243
|
prompt += `- UI_SPEC.md 的字段映射必须与后端 API 响应字段一一对应\n`;
|
|
3656
4244
|
prompt += `- TEST.md 必须覆盖 REQUIREMENT.md 中该端的验收标准\n\n`;
|
|
4245
|
+
prompt += `### Step 3: 自检 — 确保所有文档已实质填充\n`;
|
|
4246
|
+
prompt += `生成完成后,逐个检查每个文档:\n`;
|
|
4247
|
+
prompt += `- 如果文档中仍含有 \`<!-- SPEC-SKELETON -->\` 标记或空表格 → **必须重新填充**\n`;
|
|
4248
|
+
prompt += `- 如果「改造范围」表格只有表头没有数据行 → **必须补充具体文件路径**\n`;
|
|
4249
|
+
prompt += `- 如果「验证方式」表格只有表头 → **必须补充可执行的命令/操作**\n`;
|
|
4250
|
+
prompt += `- DEV_GUIDE.md 必须有至少 3 个具体文件路径和 3 个可执行验证步骤\n\n`;
|
|
3657
4251
|
prompt += `### 写入方式\n`;
|
|
3658
4252
|
prompt += `**Pipeline 模式**:一次 --apply 写入所有端的文档(推荐)\n`;
|
|
3659
|
-
prompt += `speccore analyze --apply '{"TECH.md":"...","TEST.md":"...","UI_SPEC.md":"..."}' -I ${iter} --platform all\n\n`;
|
|
4253
|
+
prompt += `speccore analyze --apply '{"TECH.md":"...","TEST.md":"...","UI_SPEC.md":"...","DEV_GUIDE.md":"..."}' -I ${iter} --platform all\n\n`;
|
|
3660
4254
|
prompt += `**或者逐端写入**(每端一次 --apply):\n`;
|
|
3661
|
-
prompt += `speccore analyze --apply '{"TECH.md":"...","TEST.md":"...","UI_SPEC.md":"..."}' -I ${iter} --platform {端名}\n\n`;
|
|
4255
|
+
prompt += `speccore analyze --apply '{"TECH.md":"...","TEST.md":"...","UI_SPEC.md":"...","DEV_GUIDE.md":"..."}' -I ${iter} --platform {端名}\n\n`;
|
|
3662
4256
|
}
|
|
3663
4257
|
else {
|
|
3664
4258
|
// v6.61.0+: 一次性生成所有文档(global/ + {端}/)
|
|
@@ -3698,7 +4292,14 @@ sequenceDiagram
|
|
|
3698
4292
|
prompt += ` - **边界处理**:如果文档对某功能描述不完整,标注"文档未充分描述",不要自行脑补完整方案\n`;
|
|
3699
4293
|
prompt += ` - **交叉验证**:每写一个功能点,回头检查需求文档中是否有对应描述,没有则删除\n`;
|
|
3700
4294
|
prompt += `6. 每个文档都要具体内容(禁止"待填充")\n`;
|
|
3701
|
-
prompt += `7.
|
|
4295
|
+
prompt += `7. **辅助文档强制要求(v8.3.1+ 修复空模板问题)**:以下文档常被 AI 遗漏或敷衍,必须同等重视:\n`;
|
|
4296
|
+
prompt += ` - **FUNCTION_MAP.md**:功能单元 × 端映射表,必须用 Markdown 表格,每行一个功能单元,不允许合并。表头:| # | 功能单元 | 涉及端 | 全局对比 | 共享能力 | 依赖任务 | 说明 |\n`;
|
|
4297
|
+
prompt += ` - **INTERACTION_MAP.md**:每个功能单元一个 Mermaid sequenceDiagram,展示跨端交互时序,箭头标注接口路径\n`;
|
|
4298
|
+
prompt += ` - **DEV_GUIDE.md(迭代级)**:全局实施指导,必须包含:分支策略、改造范围清单(具体到文件路径)、改造顺序与依赖、代码级指引(含改造前后对比)、接口契约对照表、每步验证方式、回滚方案、常见坑点\n`;
|
|
4299
|
+
prompt += ` - **MONITOR.md**:必须按 Fatal/Critical/Warning/Info 四级定义告警规则,不能只列指标名称\n`;
|
|
4300
|
+
prompt += ` - **REVIEW.md**:安全检查必须逐接口列出鉴权需求,不能笼统写"需要鉴权"\n`;
|
|
4301
|
+
prompt += ` ⛔ **自检规则**:生成完成后检查每个文档,如果仍含 \`<!-- SPEC-SKELETON -->\` 或空表格只有表头 → 必须重新填充\n`;
|
|
4302
|
+
prompt += `8. **端发现(重要)**:先确定项目有哪些端,再按端组织文档\n`;
|
|
3702
4303
|
prompt += ` - 第 1 步:Read .speccore/CONSTITUTION.md\n`;
|
|
3703
4304
|
prompt += ` - 第 2 步:从「## 端列表」章节提取端名(这是全局权威来源)\n`;
|
|
3704
4305
|
prompt += ` - 第 3 步:如果没有「端列表」章节,从「对应端」列提取\n`;
|
|
@@ -3706,7 +4307,7 @@ sequenceDiagram
|
|
|
3706
4307
|
prompt += ` - 第 5 步:将发现的端列表写入 020-specs/PLATFORMS.md\n`;
|
|
3707
4308
|
// v6.70.0+: REQUIREMENT.md 以产品视角撰写(不按端分章节)
|
|
3708
4309
|
// v6.99.0+: 丰富需求文档章节要求
|
|
3709
|
-
prompt += `
|
|
4310
|
+
prompt += `9. **REQUIREMENT.md 写作风格(重要)**:全局需求文档必须以产品/用户视角撰写\n`;
|
|
3710
4311
|
prompt += ` - **按业务场景/用户旅程组织章节**,不按端分章节(如"H5端需求"、"后端需求")\n`;
|
|
3711
4312
|
prompt += ` - 文档结构必须包含(如需求文档中有相关信息):\n`;
|
|
3712
4313
|
prompt += ` - **产品愿景**:本迭代要解决的核心问题和目标价值(1-2段)\n`;
|
|
@@ -3724,7 +4325,7 @@ sequenceDiagram
|
|
|
3724
4325
|
prompt += ` - 端的信息只在「功能模块清单」表格中标注,正文不区分端\n`;
|
|
3725
4326
|
// v6.49.14+: 功能模块清单必须含涉及端列 + 来源链接
|
|
3726
4327
|
// v6.71.3+: 增加「与全局层对比」列
|
|
3727
|
-
prompt += `
|
|
4328
|
+
prompt += `10. **功能模块清单(重要)**:写入 overview/REQUIREMENT.md 时,功能模块清单表格必须包含以下列\n`;
|
|
3728
4329
|
prompt += ` - 表格格式:| # | 功能模块 | 涉及端 | 全局对比 | 来源 | 说明 |\n`;
|
|
3729
4330
|
prompt += ` - 「涉及端」:每个模块标注需要**新开发工作**的端(标准端名,逗号分隔)\n`;
|
|
3730
4331
|
prompt += ` - 「涉及」= 该端需要写新接口/新页面/新逻辑\n`;
|
|
@@ -4272,6 +4873,16 @@ async function buildClarifyPhasePrompt(iteration) {
|
|
|
4272
4873
|
if (qualityReports.length > 0) {
|
|
4273
4874
|
await (0, requirement_clarifier_1.writeClarifyReport)(iterDir, qualityReports);
|
|
4274
4875
|
}
|
|
4876
|
+
// v8.2.0+: 提取功能单元,支持单元化澄清(解决注意力漂移)
|
|
4877
|
+
let allDocContent = '';
|
|
4878
|
+
for (const p of docPaths) {
|
|
4879
|
+
try {
|
|
4880
|
+
allDocContent += await (0, fs_extra_2.readFile)(p, 'utf-8') + '\n\n';
|
|
4881
|
+
}
|
|
4882
|
+
catch { /* ignore */ }
|
|
4883
|
+
}
|
|
4884
|
+
const extractedUnits = (0, requirement_clarifier_1.extractUnitsFromText)(allDocContent);
|
|
4885
|
+
const hasMultipleUnits = extractedUnits.length >= 2;
|
|
4275
4886
|
// v6.84.0+: 从 AGENTS 规范数据库动态加载角色
|
|
4276
4887
|
let prompt = `\n# 任务: 需求专业化(Phase 0: 需求澄清,v6.84.0+)\n\n`;
|
|
4277
4888
|
const projectRoot = (0, task_utils_1.findProjectRoot)() || process.cwd();
|
|
@@ -4332,6 +4943,33 @@ async function buildClarifyPhasePrompt(iteration) {
|
|
|
4332
4943
|
prompt += `3. \`010-requirements/converted/*.md\` — 已转换文档\n`;
|
|
4333
4944
|
prompt += `4. \`010-requirements/features/*/README.md\` — 功能级补充\n`;
|
|
4334
4945
|
prompt += `5. \`010-requirements/prototypes/\` — 原型文件\n\n`;
|
|
4946
|
+
// v8.2.0+: 如果检测到多个功能单元,启用单元化澄清模式
|
|
4947
|
+
if (hasMultipleUnits) {
|
|
4948
|
+
prompt += `## ⚠️ 功能单元拆分澄清模式(v8.2.0+)\n\n`;
|
|
4949
|
+
prompt += `检测到需求文档包含 ${extractedUnits.length} 个功能单元。为避免注意力漂移,请**按单元逐个澄清**:\n\n`;
|
|
4950
|
+
prompt += `### 功能单元清单\n\n`;
|
|
4951
|
+
for (const u of extractedUnits) {
|
|
4952
|
+
prompt += `- **${u.id}**: ${u.name}\n`;
|
|
4953
|
+
}
|
|
4954
|
+
prompt += `\n`;
|
|
4955
|
+
prompt += `### 单元化澄清规则\n\n`;
|
|
4956
|
+
prompt += `1. **逐单元处理**:每次只深入分析一个功能单元,不要跨单元混合\n`;
|
|
4957
|
+
prompt += `2. **使用 [UNIT:xxx] 标记**:每个单元的澄清结果用 \`[UNIT:单元ID]\` 开头分隔\n`;
|
|
4958
|
+
prompt += `3. **关联上下文**:分析当前单元时,参考相关单元的摘要(保持逻辑一致性)\n`;
|
|
4959
|
+
prompt += `4. **全局约束共享**:术语表、接口契约在所有单元间统一\n`;
|
|
4960
|
+
prompt += `5. **优先核心单元**:如果单元过多(>5),先处理核心功能单元\n\n`;
|
|
4961
|
+
prompt += `### 输出格式示例\n\n`;
|
|
4962
|
+
prompt += `\`\`\`\n`;
|
|
4963
|
+
for (const u of extractedUnits.slice(0, 2)) {
|
|
4964
|
+
prompt += `[UNIT:${u.id}]\n`;
|
|
4965
|
+
prompt += `## ${u.name}\n`;
|
|
4966
|
+
prompt += `(该单元的专业化澄清内容...)\n\n`;
|
|
4967
|
+
}
|
|
4968
|
+
if (extractedUnits.length > 2) {
|
|
4969
|
+
prompt += `...(其余 ${extractedUnits.length - 2} 个单元依此类推)\n`;
|
|
4970
|
+
}
|
|
4971
|
+
prompt += `\`\`\`\n\n`;
|
|
4972
|
+
}
|
|
4335
4973
|
prompt += `---\n\n`;
|
|
4336
4974
|
prompt += `# 📋 五步迭代澄清流程\n\n`;
|
|
4337
4975
|
prompt += `> 本阶段要求你先以专业角色深度分析,再生成改写版,与用户反复迭代确认,最终写入。\n\n`;
|
|
@@ -4420,11 +5058,11 @@ async function buildClarifyPhasePrompt(iteration) {
|
|
|
4420
5058
|
prompt += ` - 记录取舍:每轮迭代记录「采纳/拒绝/待确认」的决策\n\n`;
|
|
4421
5059
|
prompt += `## Step 5: 确认写入\n\n`;
|
|
4422
5060
|
prompt += `当用户确认 "满意,可以写入" 后:\n\n`;
|
|
4423
|
-
prompt += `1. 将最终版 PRD 写入 \`
|
|
4424
|
-
prompt += `2. 同时生成 \`
|
|
5061
|
+
prompt += `1. 将最终版 PRD 写入 \`020-specs/requirements/clarified-{源文件名}-{日期}.md\`(黄金需求目录)\n`;
|
|
5062
|
+
prompt += `2. 同时生成 \`020-specs/requirements/clarified-{源文件名}-{日期}-diff.md\` 保存最终对比报告\n`;
|
|
4425
5063
|
prompt += `3. 使用以下命令写入:\n\n`;
|
|
4426
5064
|
prompt += `\`\`\`bash\n`;
|
|
4427
|
-
prompt += `speccore analyze --apply '{"
|
|
5065
|
+
prompt += `speccore analyze --apply '{"020-specs/requirements/clarified-xxx.md":"...","020-specs/requirements/clarified-xxx-diff.md":"..."}' -I ${iteration}\n`;
|
|
4428
5066
|
prompt += `\`\`\`\n\n`;
|
|
4429
5067
|
prompt += `> 注意:写入后 CLI 会自动推进到需求确认阶段。\n`;
|
|
4430
5068
|
return await injectGraphSummary(prompt);
|
|
@@ -4432,7 +5070,7 @@ async function buildClarifyPhasePrompt(iteration) {
|
|
|
4432
5070
|
async function buildConfirmCheckPrompt(iteration) {
|
|
4433
5071
|
const iterDir = await (0, context_1.getIterationDir)(iteration);
|
|
4434
5072
|
const reportPath = (0, path_1.join)(iterDir, '010-requirements', 'CLARIFY_REPORT.md');
|
|
4435
|
-
const
|
|
5073
|
+
const goldenDir = (0, path_1.join)(iterDir, '020-specs', 'requirements');
|
|
4436
5074
|
let prompt = `\n# 任务: 需求确认(Phase 0: 确认检查,v6.83.0+)\n\n`;
|
|
4437
5075
|
if (await (0, fs_extra_1.pathExists)(reportPath)) {
|
|
4438
5076
|
const report = await (0, fs_extra_2.readFile)(reportPath, 'utf-8');
|
|
@@ -4442,11 +5080,11 @@ async function buildConfirmCheckPrompt(iteration) {
|
|
|
4442
5080
|
prompt += `\n\n... (报告共 ${report.length} 字符,已截断)\n`;
|
|
4443
5081
|
}
|
|
4444
5082
|
}
|
|
4445
|
-
//
|
|
5083
|
+
// v8.2.0+: 检查黄金需求目录是否有 diff 文件
|
|
4446
5084
|
let diffFiles = [];
|
|
4447
|
-
if (await (0, fs_extra_1.pathExists)(
|
|
5085
|
+
if (await (0, fs_extra_1.pathExists)(goldenDir)) {
|
|
4448
5086
|
try {
|
|
4449
|
-
const entries = await (0, fs_extra_2.readdir)(
|
|
5087
|
+
const entries = await (0, fs_extra_2.readdir)(goldenDir);
|
|
4450
5088
|
diffFiles = entries.filter(f => f.includes('-diff.md'));
|
|
4451
5089
|
}
|
|
4452
5090
|
catch { /* ignore */ }
|
|
@@ -4454,13 +5092,13 @@ async function buildConfirmCheckPrompt(iteration) {
|
|
|
4454
5092
|
if (diffFiles.length > 0) {
|
|
4455
5093
|
prompt += `\n## 已生成的对比报告\n\n`;
|
|
4456
5094
|
for (const f of diffFiles) {
|
|
4457
|
-
prompt += `- \`
|
|
5095
|
+
prompt += `- \`020-specs/requirements/${f}\` — 需求澄清对比记录\n`;
|
|
4458
5096
|
}
|
|
4459
5097
|
prompt += '\n';
|
|
4460
5098
|
}
|
|
4461
5099
|
prompt += `## 确认检查清单\n\n`;
|
|
4462
5100
|
prompt += `请逐项确认:\n\n`;
|
|
4463
|
-
prompt += `- [ ] clarified-*.md 已写入 \`
|
|
5101
|
+
prompt += `- [ ] clarified-*.md 已写入 \`020-specs/requirements/\`(黄金需求目录)\n`;
|
|
4464
5102
|
prompt += `- [ ] 验收标准可测试、可量化\n`;
|
|
4465
5103
|
prompt += `- [ ] 功能边界明确(不做什么)\n`;
|
|
4466
5104
|
prompt += `- [ ] 业务流程完整(含异常分支)\n`;
|
|
@@ -4484,4 +5122,277 @@ async function buildConfirmCheckPrompt(iteration) {
|
|
|
4484
5122
|
prompt += `\`\`\`\n`;
|
|
4485
5123
|
return await injectGraphSummary(prompt);
|
|
4486
5124
|
}
|
|
5125
|
+
// ================================================================
|
|
5126
|
+
// v8.2.0+: 功能单元聚焦分析辅助函数(解决注意力漂移+空模板问题)
|
|
5127
|
+
// ================================================================
|
|
5128
|
+
/** 解析 [DOC:filename] 标记,将统一报告拆分为多文档 Map */
|
|
5129
|
+
function parseDocMarkers(text) {
|
|
5130
|
+
const result = new Map();
|
|
5131
|
+
const markerRegex = /\[DOC:([^\]]+)\]/g;
|
|
5132
|
+
let match;
|
|
5133
|
+
const positions = [];
|
|
5134
|
+
while ((match = markerRegex.exec(text)) !== null) {
|
|
5135
|
+
positions.push({ name: match[1].trim(), index: match.index });
|
|
5136
|
+
}
|
|
5137
|
+
for (let i = 0; i < positions.length; i++) {
|
|
5138
|
+
const start = positions[i].index + `[DOC:${positions[i].name}]`.length;
|
|
5139
|
+
const end = i < positions.length - 1 ? positions[i + 1].index : text.length;
|
|
5140
|
+
const content = text.slice(start, end).trim();
|
|
5141
|
+
result.set(positions[i].name, content);
|
|
5142
|
+
}
|
|
5143
|
+
return result;
|
|
5144
|
+
}
|
|
5145
|
+
/** 解析 [CLARIFY:filename] 标记,提取需求澄清文档 Map — v8.3.0+ */
|
|
5146
|
+
function parseClarifyMarkers(text) {
|
|
5147
|
+
const result = new Map();
|
|
5148
|
+
const markerRegex = /\[CLARIFY:([^\]]+)\]/g;
|
|
5149
|
+
let match;
|
|
5150
|
+
const positions = [];
|
|
5151
|
+
while ((match = markerRegex.exec(text)) !== null) {
|
|
5152
|
+
positions.push({ name: match[1].trim(), index: match.index });
|
|
5153
|
+
}
|
|
5154
|
+
for (let i = 0; i < positions.length; i++) {
|
|
5155
|
+
const start = positions[i].index + `[CLARIFY:${positions[i].name}]`.length;
|
|
5156
|
+
const end = i < positions.length - 1 ? positions[i + 1].index : text.length;
|
|
5157
|
+
const content = text.slice(start, end).trim();
|
|
5158
|
+
result.set(positions[i].name, content);
|
|
5159
|
+
}
|
|
5160
|
+
return result;
|
|
5161
|
+
}
|
|
5162
|
+
exports.UNITS_CACHE_DIR = '.speccore/cache/unit-analysis';
|
|
5163
|
+
/** 从需求文档提取功能单元清单 */
|
|
5164
|
+
async function extractIterationUnits(iterDir) {
|
|
5165
|
+
const reqDir = (0, path_1.join)(iterDir, '010-requirements');
|
|
5166
|
+
const units = [];
|
|
5167
|
+
let idCounter = 1;
|
|
5168
|
+
if (!await (0, fs_extra_1.pathExists)(reqDir))
|
|
5169
|
+
return units;
|
|
5170
|
+
const reqFiles = [];
|
|
5171
|
+
async function scanDir(dir) {
|
|
5172
|
+
const entries = await (0, fs_extra_2.readdir)(dir, { withFileTypes: true });
|
|
5173
|
+
for (const entry of entries) {
|
|
5174
|
+
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
5175
|
+
if (entry.isDirectory())
|
|
5176
|
+
await scanDir(fullPath);
|
|
5177
|
+
else if (entry.name.endsWith('.md'))
|
|
5178
|
+
reqFiles.push(fullPath);
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
await scanDir(reqDir);
|
|
5182
|
+
for (const file of reqFiles) {
|
|
5183
|
+
const content = await (0, fs_extra_2.readFile)(file, 'utf-8');
|
|
5184
|
+
const lines = content.split('\n');
|
|
5185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5186
|
+
const line = lines[i];
|
|
5187
|
+
const headingMatch = line.match(/^#{2,3}\s+(.+)$/);
|
|
5188
|
+
if (!headingMatch)
|
|
5189
|
+
continue;
|
|
5190
|
+
const name = headingMatch[1].trim();
|
|
5191
|
+
// 跳过通用标题
|
|
5192
|
+
const skipKeywords = ['需求概述', '术语表', '附录', '测试策略', '参考资料', '目录', '引言', '背景'];
|
|
5193
|
+
if (skipKeywords.some(k => name.includes(k)))
|
|
5194
|
+
continue;
|
|
5195
|
+
// 收集后续 1-15 行作为内容摘要
|
|
5196
|
+
let descLines = [];
|
|
5197
|
+
for (let j = i + 1; j < Math.min(i + 16, lines.length); j++) {
|
|
5198
|
+
const l = lines[j];
|
|
5199
|
+
if (l.match(/^#{1,3}\s+/))
|
|
5200
|
+
break;
|
|
5201
|
+
if (l.trim())
|
|
5202
|
+
descLines.push(l.trim());
|
|
5203
|
+
}
|
|
5204
|
+
units.push({
|
|
5205
|
+
id: `M-${String(idCounter++).padStart(2, '0')}`,
|
|
5206
|
+
name,
|
|
5207
|
+
source: (0, path_1.relative)(reqDir, file),
|
|
5208
|
+
sourceRange: { startLine: i + 1, endLine: i + 1 + descLines.length },
|
|
5209
|
+
content: descLines.join('\n').slice(0, 500),
|
|
5210
|
+
});
|
|
5211
|
+
if (idCounter > 30)
|
|
5212
|
+
break; // 最多识别 30 个功能单元
|
|
5213
|
+
}
|
|
5214
|
+
if (idCounter > 30)
|
|
5215
|
+
break;
|
|
5216
|
+
}
|
|
5217
|
+
return units;
|
|
5218
|
+
}
|
|
5219
|
+
/** 保存单元分析结果到缓存 */
|
|
5220
|
+
async function saveUnitAnalysis(iteration, analysis) {
|
|
5221
|
+
const cacheDir = (0, path_1.join)(process.cwd(), exports.UNITS_CACHE_DIR, iteration);
|
|
5222
|
+
await (0, fs_extra_1.ensureDir)(cacheDir);
|
|
5223
|
+
const fp = (0, path_1.join)(cacheDir, `${analysis.id}.json`);
|
|
5224
|
+
await (0, fs_extra_1.writeFile)(fp, JSON.stringify(analysis, null, 2));
|
|
5225
|
+
}
|
|
5226
|
+
/** 读取已完成的单元分析 */
|
|
5227
|
+
async function loadUnitAnalyses(iteration) {
|
|
5228
|
+
const cacheDir = (0, path_1.join)(process.cwd(), exports.UNITS_CACHE_DIR, iteration);
|
|
5229
|
+
if (!await (0, fs_extra_1.pathExists)(cacheDir))
|
|
5230
|
+
return [];
|
|
5231
|
+
const files = (await (0, fs_extra_2.readdir)(cacheDir)).filter(f => f.endsWith('.json'));
|
|
5232
|
+
const results = [];
|
|
5233
|
+
for (const f of files) {
|
|
5234
|
+
try {
|
|
5235
|
+
const content = await (0, fs_extra_2.readFile)((0, path_1.join)(cacheDir, f), 'utf-8');
|
|
5236
|
+
results.push(JSON.parse(content));
|
|
5237
|
+
}
|
|
5238
|
+
catch { /* skip */ }
|
|
5239
|
+
}
|
|
5240
|
+
return results.sort((a, b) => a.id.localeCompare(b.id));
|
|
5241
|
+
}
|
|
5242
|
+
/** 检测未完成的单元分析 */
|
|
5243
|
+
async function detectPendingUnits(iteration, allUnits) {
|
|
5244
|
+
const completed = await loadUnitAnalyses(iteration);
|
|
5245
|
+
const completedIds = new Set(completed.map(a => a.id));
|
|
5246
|
+
return allUnits.filter(u => !completedIds.has(u.id));
|
|
5247
|
+
}
|
|
5248
|
+
/** 汇总所有单元分析为统一报告(带 [DOC:xxx] 标记) */
|
|
5249
|
+
async function consolidateUnitAnalyses(iteration, units, analyses) {
|
|
5250
|
+
const lines = [];
|
|
5251
|
+
lines.push(`# ${iteration} 迭代 Spec 统一报告`);
|
|
5252
|
+
lines.push(`> 生成时间: ${new Date().toISOString()}`);
|
|
5253
|
+
lines.push(`> 共 ${analyses.length}/${units.length} 个功能单元已完成分析`);
|
|
5254
|
+
lines.push('');
|
|
5255
|
+
// 汇总 REQUIREMENT.md
|
|
5256
|
+
lines.push('[DOC:REQUIREMENT.md]');
|
|
5257
|
+
lines.push(`# ${iteration} 需求规格`);
|
|
5258
|
+
lines.push('');
|
|
5259
|
+
lines.push('## 功能模块清单');
|
|
5260
|
+
lines.push('| 单元ID | 模块名称 | 需求来源 |');
|
|
5261
|
+
lines.push('|:---|:---|:---|');
|
|
5262
|
+
for (const u of units) {
|
|
5263
|
+
lines.push(`| ${u.id} | ${u.name} | ${u.source}#L${u.sourceRange.startLine} |`);
|
|
5264
|
+
}
|
|
5265
|
+
lines.push('');
|
|
5266
|
+
for (const a of analyses) {
|
|
5267
|
+
lines.push(`### ${a.id} ${a.name}`);
|
|
5268
|
+
lines.push(a.req.split('\n').slice(0, 5).join('\n'));
|
|
5269
|
+
lines.push('');
|
|
5270
|
+
}
|
|
5271
|
+
lines.push('');
|
|
5272
|
+
// 汇总 ANALYSIS.md
|
|
5273
|
+
lines.push('[DOC:ANALYSIS.md]');
|
|
5274
|
+
lines.push(`# ${iteration} 架构分析`);
|
|
5275
|
+
lines.push('');
|
|
5276
|
+
for (const a of analyses) {
|
|
5277
|
+
lines.push(`## ${a.id} ${a.name}`);
|
|
5278
|
+
lines.push(a.tech);
|
|
5279
|
+
lines.push('');
|
|
5280
|
+
}
|
|
5281
|
+
lines.push('');
|
|
5282
|
+
// 汇总 FUNCTION_MAP.md
|
|
5283
|
+
lines.push('[DOC:FUNCTION_MAP.md]');
|
|
5284
|
+
lines.push(`# ${iteration} 功能映射`);
|
|
5285
|
+
lines.push('');
|
|
5286
|
+
lines.push('| 单元ID | 功能模块 | 涉及端 | 优先级 |');
|
|
5287
|
+
lines.push('|:---|:---|:---|:---|');
|
|
5288
|
+
for (const a of analyses) {
|
|
5289
|
+
lines.push(`| ${a.id} | ${a.name} | ${(a.tech.match(/端[::]\s*(.+)/)?.[1] || '待定')} | P1 |`);
|
|
5290
|
+
}
|
|
5291
|
+
lines.push('');
|
|
5292
|
+
// 汇总 TECH.md
|
|
5293
|
+
lines.push('[DOC:TECH.md]');
|
|
5294
|
+
lines.push(`# ${iteration} 技术方案`);
|
|
5295
|
+
lines.push('');
|
|
5296
|
+
for (const a of analyses) {
|
|
5297
|
+
lines.push(`## ${a.id} ${a.name}`);
|
|
5298
|
+
lines.push(a.tech);
|
|
5299
|
+
lines.push('');
|
|
5300
|
+
}
|
|
5301
|
+
lines.push('');
|
|
5302
|
+
// 汇总 TEST.md
|
|
5303
|
+
lines.push('[DOC:TEST.md]');
|
|
5304
|
+
lines.push(`# ${iteration} 测试方案`);
|
|
5305
|
+
lines.push('');
|
|
5306
|
+
for (const a of analyses) {
|
|
5307
|
+
lines.push(`## ${a.id} ${a.name}`);
|
|
5308
|
+
lines.push(a.test);
|
|
5309
|
+
lines.push('');
|
|
5310
|
+
}
|
|
5311
|
+
lines.push('');
|
|
5312
|
+
// 汇总 RISK.md
|
|
5313
|
+
lines.push('[DOC:RISK.md]');
|
|
5314
|
+
lines.push(`# ${iteration} 风险评估`);
|
|
5315
|
+
lines.push('');
|
|
5316
|
+
for (const a of analyses) {
|
|
5317
|
+
if (a.risk) {
|
|
5318
|
+
lines.push(`## ${a.id} ${a.name}`);
|
|
5319
|
+
lines.push(a.risk);
|
|
5320
|
+
lines.push('');
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
lines.push('');
|
|
5324
|
+
return lines.join('\n');
|
|
5325
|
+
}
|
|
5326
|
+
/** 构建单个功能单元的深度分析 Prompt */
|
|
5327
|
+
async function buildUnitAnalysisPrompt(iterDir, iteration, unit, allUnits) {
|
|
5328
|
+
// 使用智能上下文组装引擎(三层上下文模型)
|
|
5329
|
+
const unitRef = { id: unit.id, name: unit.name, content: unit.content };
|
|
5330
|
+
const allUnitRefs = allUnits.map(u => ({ id: u.id, name: u.name, content: u.content }));
|
|
5331
|
+
const ctxResult = await (0, unit_context_assembler_1.assembleUnitContext)(iterDir, iteration, unitRef, allUnitRefs, {
|
|
5332
|
+
maxTokens: 6000,
|
|
5333
|
+
maxRelatedUnits: 3,
|
|
5334
|
+
maxCodeFiles: 5,
|
|
5335
|
+
useKnowledgeGraph: true,
|
|
5336
|
+
});
|
|
5337
|
+
const prompt = `
|
|
5338
|
+
# 功能单元深度分析: ${unit.id} ${unit.name}
|
|
5339
|
+
|
|
5340
|
+
## 迭代信息
|
|
5341
|
+
- 迭代: ${iteration}
|
|
5342
|
+
- 单元ID: ${unit.id}
|
|
5343
|
+
- 来源: ${unit.source}#L${unit.sourceRange.startLine}
|
|
5344
|
+
- 全迭代共 ${allUnits.length} 个功能单元
|
|
5345
|
+
- 上下文预算: ${ctxResult.stats.estimatedTokens} tokens (核心${ctxResult.stats.coreChars}字符 + 关联${ctxResult.stats.relatedUnitChars + ctxResult.stats.codeChars}字符 + 全局${ctxResult.stats.globalChars}字符)
|
|
5346
|
+
|
|
5347
|
+
## 需求上下文
|
|
5348
|
+
${ctxResult.context}
|
|
5349
|
+
|
|
5350
|
+
## 分析要求
|
|
5351
|
+
|
|
5352
|
+
请对「${unit.name}」进行深度分析,输出以下文档(使用 [DOC:xxx] 标记分隔):
|
|
5353
|
+
|
|
5354
|
+
### [DOC:REQ.md] 需求规格
|
|
5355
|
+
- 功能描述(用户故事格式)
|
|
5356
|
+
- 输入/输出定义
|
|
5357
|
+
- 业务规则(R-XX 编号)
|
|
5358
|
+
- 异常场景
|
|
5359
|
+
|
|
5360
|
+
### [DOC:TECH.md] 技术方案
|
|
5361
|
+
- 数据模型/接口设计
|
|
5362
|
+
- 核心算法/逻辑
|
|
5363
|
+
- 端分工(backend/web/admin 等)
|
|
5364
|
+
- 依赖服务
|
|
5365
|
+
|
|
5366
|
+
### [DOC:TEST.md] 测试方案
|
|
5367
|
+
- 验收标准(Given-When-Then)
|
|
5368
|
+
- 边界值
|
|
5369
|
+
- 异常路径
|
|
5370
|
+
|
|
5371
|
+
### [DOC:RISK.md] 风险评估(如适用)
|
|
5372
|
+
- 技术风险
|
|
5373
|
+
- 业务风险
|
|
5374
|
+
- 缓解方案
|
|
5375
|
+
|
|
5376
|
+
## 输出格式
|
|
5377
|
+
\`\`\`
|
|
5378
|
+
[DOC:REQ.md]
|
|
5379
|
+
(内容...)
|
|
5380
|
+
|
|
5381
|
+
[DOC:TECH.md]
|
|
5382
|
+
(内容...)
|
|
5383
|
+
|
|
5384
|
+
[DOC:TEST.md]
|
|
5385
|
+
(内容...)
|
|
5386
|
+
|
|
5387
|
+
[DOC:RISK.md]
|
|
5388
|
+
(内容...)
|
|
5389
|
+
\`\`\`
|
|
5390
|
+
|
|
5391
|
+
## 重要提示
|
|
5392
|
+
- 只分析当前功能单元「${unit.name}」,不要涉及其他模块
|
|
5393
|
+
- 每个文档必须有实质内容,不能是空模板
|
|
5394
|
+
- 使用具体的技术术语,避免泛泛而谈
|
|
5395
|
+
`;
|
|
5396
|
+
return prompt.trim();
|
|
5397
|
+
}
|
|
4487
5398
|
//# sourceMappingURL=analyze.js.map
|