speccore 6.72.0 → 6.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,10 @@ const resolver_1 = require("../core/resolver");
48
48
  const global_counters_1 = require("../core/global-counters");
49
49
  const inbox_1 = require("../core/inbox");
50
50
  const index_guard_1 = require("../core/index-guard");
51
+ // v6.73.0+ 变更驱动工作流 v2
52
+ const change_inbox_1 = require("../core/change-inbox");
53
+ const change_parser_1 = require("../core/change-parser");
54
+ const ai_impact_analyzer_1 = require("../core/ai-impact-analyzer");
51
55
  /**
52
56
  * 解析任务目录基础路径:优先 030-tasks/,兼容旧布局
53
57
  */
@@ -393,213 +397,13 @@ async function changeCommand(options) {
393
397
  options.desc = options.input;
394
398
  }
395
399
  if (!options.task && !options.global) {
396
- // ── 智能匹配 / 新增需求(含 inbox + 附件 + 澄清)──
397
- if (!options.desc && !options.file && options.noInbox) {
398
- logger_1.logger.error('请提供变更描述或附件。用法: speccore change "描述" --file=xxx.md');
400
+ // v6.73.0+ 变更驱动工作流 v2
401
+ // --prompt/--response 模式保持向后兼容
402
+ if (options.prompt || options.response) {
403
+ await processChangeLegacy(options);
399
404
  return;
400
405
  }
401
- const iteration = await (0, context_1.getDefaultIteration)(options.iteration);
402
- if (!iteration) {
403
- logger_1.logger.error('未找到活跃迭代。请先运行: speccore iteration create --name <名称>');
404
- return;
405
- }
406
- // ── 1. 加载附件 ──
407
- const allFiles = [];
408
- // 1a. 扫描 inbox(默认启用)
409
- if (!options.noInbox) {
410
- await (0, inbox_1.ensureInboxDir)();
411
- const inboxResult = await (0, inbox_1.scanInbox)({ reprocess: options.reprocess });
412
- (0, inbox_1.logInboxScan)(inboxResult);
413
- const actionable = [...inboxResult.newFiles, ...inboxResult.modifiedFiles];
414
- allFiles.push(...actionable);
415
- }
416
- // 1b. 加载 --file 指定的文件
417
- if (options.file) {
418
- const { readFile: rf, stat: st } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
419
- const filePaths = options.file.split(',').map(f => f.trim());
420
- for (const fp of filePaths) {
421
- const absPath = (0, path_1.join)(process.cwd(), fp);
422
- if (!await (0, fs_extra_1.pathExists)(absPath)) {
423
- logger_1.logger.warn(`⚠️ 文件不存在: ${fp}`);
424
- continue;
425
- }
426
- const fileStat = await st(absPath);
427
- const name = fp.split('/').pop() || fp;
428
- const ext = name.split('.').pop()?.toLowerCase() || '';
429
- let type = 'other';
430
- if (['md', 'txt', 'markdown', 'json', 'yaml', 'yml', 'csv'].includes(ext))
431
- type = 'text';
432
- else if (['xlsx', 'xls'].includes(ext))
433
- type = 'excel';
434
- else if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext))
435
- type = 'image';
436
- let content = '';
437
- if (type === 'text') {
438
- content = await rf(absPath, 'utf-8');
439
- }
440
- else if (type === 'excel') {
441
- try {
442
- const XLSX = require('xlsx');
443
- const wb = XLSX.readFile(absPath);
444
- const sheets = [];
445
- for (const sn of wb.SheetNames) {
446
- sheets.push(`## Sheet: ${sn}\n${XLSX.utils.sheet_to_csv(wb.Sheets[sn])}`);
447
- }
448
- content = sheets.join('\n\n');
449
- }
450
- catch {
451
- content = `[Excel 解析失败]`;
452
- }
453
- }
454
- else if (type === 'image') {
455
- content = `[图片文件: ${absPath}]`;
456
- }
457
- else {
458
- try {
459
- content = await rf(absPath, 'utf-8');
460
- }
461
- catch {
462
- content = `[无法读取]`;
463
- }
464
- }
465
- allFiles.push({ name, path: absPath, size: fileStat.size, mtime: fileStat.mtime.toISOString(), type, content });
466
- logger_1.logger.info(` 📎 ${name} (${fileStat.size > 1024 ? (fileStat.size / 1024).toFixed(1) + 'KB' : fileStat.size + 'B'})`);
467
- }
468
- }
469
- // ── 2. 构建澄清上下文 ──
470
- const desc = options.desc ? normalizeDescription(options.desc) : '';
471
- const iterDir = await (0, context_1.getIterationDir)(iteration);
472
- const taskBase = await resolveTaskBase(iterDir);
473
- const allTasks = await (0, state_1.scanTasks)(iteration);
474
- const taskDetails = await buildTaskDetails(taskBase);
475
- // ── 3. Prompt 模式:输出澄清 Prompt 到 stdout ──
476
- if (options.prompt) {
477
- const promptText = (0, inbox_1.buildClarifyPrompt)(desc || '(从附件分析需求)', allFiles, taskDetails);
478
- logger_1.logger.info('[SPECCORE_PROMPT]');
479
- process.stdout.write(promptText);
480
- return;
481
- }
482
- // ── 4. Response 模式:解析 AI 澄清结果 ──
483
- let clarifiedIntent;
484
- let clarifiedDesc = desc;
485
- let clarifiedTasks = [];
486
- if (options.response) {
487
- const parsed = (0, inbox_1.parseClarifyResponse)(options.response);
488
- if (parsed) {
489
- clarifiedIntent = parsed.intent;
490
- clarifiedDesc = parsed.structuredDesc || desc;
491
- // 从 impactReport 中提取直接影响的任务 ID
492
- clarifiedTasks = parsed.impactReport?.directTasks?.map(t => t.id) || [];
493
- (0, inbox_1.logClarifyResult)(parsed);
494
- }
495
- else {
496
- logger_1.logger.warn('⚠️ AI 澄清结果解析失败,使用本地分析');
497
- }
498
- }
499
- // ── 5. 本地意图检测(无 AI 澄清时) ──
500
- const intent = clarifiedIntent || detectIntent(desc || allFiles.map(f => f.content).join(' '));
501
- // ── 6. 新增需求 ──
502
- if (intent === 'new') {
503
- logger_1.logger.info('🆕 检测到新增需求意图');
504
- const newDesc = clarifiedDesc || allFiles.map(f => f.name + ': ' + f.content.slice(0, 200)).join('\n');
505
- // 传递澄清结果给 handleNewRequirement,持久化到 REQ.md
506
- const parsed = options.response ? (0, inbox_1.parseClarifyResponse)(options.response) : null;
507
- const clarifyOutput = parsed ? { structuredDesc: parsed.structuredDesc, keyPoints: parsed.keyPoints, acceptanceCriteria: parsed.acceptanceCriteria } : undefined;
508
- await handleNewRequirement(newDesc, iteration, clarifyOutput);
509
- // 标记 inbox 文件已处理
510
- if (allFiles.length > 0) {
511
- await (0, inbox_1.markProcessed)(allFiles, 'new', []);
512
- }
513
- return;
514
- }
515
- // ── 7. 变更:全量影响分析 ──
516
- let impactReport;
517
- if (clarifiedTasks.length > 0) {
518
- // 使用 AI 澄清结果中的匹配任务构造影响报告
519
- const directTasks = clarifiedTasks.map(tid => {
520
- const task = allTasks.find(t => t.id === tid);
521
- return { id: tid, name: task?.name || tid, status: 'unknown', level: 'direct', reason: 'AI 澄清匹配', affectedFiles: [], needReExecute: true, needRegression: false };
522
- }).filter(m => allTasks.some(t => t.id === m.id));
523
- impactReport = { directTasks, indirectTasks: [], unaffectedTasks: [] };
524
- }
525
- else {
526
- // 本地全量影响分析
527
- const matchDesc = desc || allFiles.map(f => f.content).join(' ');
528
- impactReport = await analyzeImpact(matchDesc, iterDir, taskBase);
529
- }
530
- const hasImpact = impactReport.directTasks.length > 0 || impactReport.indirectTasks.length > 0;
531
- if (!hasImpact) {
532
- logger_1.logger.warn('未匹配到受影响任务。请指定 --task 或检查变更描述/附件。');
533
- logger_1.logger.info('💡 如果是新增需求,请确保描述以"新增/加/创建"开头');
534
- return;
535
- }
536
- // 展示影响分析报告
537
- (0, inbox_1.logImpactReport)(impactReport);
538
- logger_1.logger.info('');
539
- // 对所有直接影响任务应用变更
540
- const changeDesc = clarifiedDesc || desc;
541
- const affectedIds = [];
542
- for (const m of impactReport.directTasks) {
543
- const taskOpts = { ...options, task: m.id, desc: changeDesc };
544
- await applyTaskChange(taskOpts, iteration);
545
- affectedIds.push(m.id);
546
- }
547
- // v6.72.0+: 对间接影响任务标记为 needs-rework(需回归验证)
548
- for (const m of impactReport.indirectTasks) {
549
- const indirectTaskDir = (0, path_1.join)(taskBase, m.id);
550
- const metaStatusPath = (0, path_1.join)(indirectTaskDir, '.meta', 'status');
551
- const legacyStatusPath = (0, path_1.join)(indirectTaskDir, '.task-status');
552
- const taskMdPath = (0, path_1.join)(indirectTaskDir, '00-specs', 'TASK.md');
553
- const now = new Date().toISOString().split('T')[0];
554
- try {
555
- if (await (0, fs_extra_1.pathExists)(metaStatusPath)) {
556
- const currentStatus = (await (0, fs_extra_1.readFile)(metaStatusPath, 'utf-8')).trim();
557
- if (currentStatus === 'done') {
558
- await (0, fs_extra_1.writeFile)(metaStatusPath, 'needs-rework');
559
- logger_1.logger.info(` 📌 ${m.id} 状态从 done 回退为 needs-rework(间接影响)`);
560
- }
561
- }
562
- else if (await (0, fs_extra_1.pathExists)(legacyStatusPath)) {
563
- const currentStatus = (await (0, fs_extra_1.readFile)(legacyStatusPath, 'utf-8')).trim();
564
- if (currentStatus === 'done') {
565
- await (0, fs_extra_1.writeFile)(legacyStatusPath, 'needs-rework');
566
- logger_1.logger.info(` 📌 ${m.id} 状态从 done 回退为 needs-rework(间接影响)`);
567
- }
568
- }
569
- // 在 TASK.md 追加间接影响说明
570
- if (await (0, fs_extra_1.pathExists)(taskMdPath)) {
571
- let content = await (0, fs_extra_1.readFile)(taskMdPath, 'utf-8');
572
- const indirectNote = `| ${now} | 间接影响 | 上游任务变更,需回归验证 | SpecCore |\n`;
573
- if (!content.includes('间接影响')) {
574
- content = content.replace(/(\| :--- \| :--- \| :--- \| :--- \|)/, `$1\n${indirectNote}`);
575
- await (0, fs_extra_1.writeFile)(taskMdPath, content);
576
- }
577
- }
578
- }
579
- catch { /* 忽略失败 */ }
580
- }
581
- // 标记 inbox 文件已处理
582
- if (allFiles.length > 0) {
583
- await (0, inbox_1.markProcessed)(allFiles, 'change', affectedIds);
584
- }
585
- // ── 8. 持久化澄清结果:迭代级 CHANGE_SUMMARY.md ──
586
- await writeChangeSummary(iterDir, changeDesc, impactReport, affectedIds);
587
- logger_1.logger.info('');
588
- logger_1.logger.success(`✅ 变更已应用到 ${affectedIds.length} 个任务`);
589
- logger_1.logger.info(` 📄 变更摘要: 020-specs/CHANGE_SUMMARY.md`);
590
- logger_1.logger.info('');
591
- logger_1.logger.info('💡 下一步:');
592
- for (const id of affectedIds) {
593
- logger_1.logger.info(` speccore analyze --task=${id} --sync # 局部回写受影响的 specs`);
594
- }
595
- logger_1.logger.info(` speccore execute --task=${affectedIds.join(',')} --force # 重新执行`);
596
- // 自动刷新知识图谱(v6.49.10+)
597
- try {
598
- const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../core/knowledge-graph')));
599
- await refreshKnowledgeGraph(process.cwd(), iteration);
600
- logger_1.logger.info('🧠 知识图谱已刷新');
601
- }
602
- catch { }
406
+ await processChangeV2(options);
603
407
  return;
604
408
  }
605
409
  if (!options.desc) {
@@ -1025,4 +829,527 @@ async function syncToAnalysis(iteration, taskId, desc) {
1025
829
  }
1026
830
  logger_1.logger.info(` → 已同步到 ANALYSIS.md`);
1027
831
  }
832
+ // ═══════════════════════════════════════════════════════════════
833
+ // v6.73.0+ 变更驱动工作流 v2
834
+ // ═══════════════════════════════════════════════════════════════
835
+ /**
836
+ * v6.72.0 及之前版本的变更处理流程(向后兼容)
837
+ * 用于 --prompt / --response 模式
838
+ */
839
+ async function processChangeLegacy(options) {
840
+ if (!options.desc && !options.file && options.noInbox) {
841
+ logger_1.logger.error('请提供变更描述或附件。用法: speccore change "描述" --file=xxx.md');
842
+ return;
843
+ }
844
+ const iteration = await (0, context_1.getDefaultIteration)(options.iteration);
845
+ if (!iteration) {
846
+ logger_1.logger.error('未找到活跃迭代。请先运行: speccore iteration create --name <名称>');
847
+ return;
848
+ }
849
+ // ── 1. 加载附件 ──
850
+ const allFiles = [];
851
+ // 1a. 扫描 inbox(默认启用)
852
+ if (!options.noInbox) {
853
+ await (0, inbox_1.ensureInboxDir)();
854
+ const inboxResult = await (0, inbox_1.scanInbox)({ reprocess: options.reprocess });
855
+ (0, inbox_1.logInboxScan)(inboxResult);
856
+ const actionable = [...inboxResult.newFiles, ...inboxResult.modifiedFiles];
857
+ allFiles.push(...actionable);
858
+ }
859
+ // 1b. 加载 --file 指定的文件
860
+ if (options.file) {
861
+ const { readFile: rf, stat: st } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
862
+ const filePaths = options.file.split(',').map(f => f.trim());
863
+ for (const fp of filePaths) {
864
+ const absPath = (0, path_1.join)(process.cwd(), fp);
865
+ if (!await (0, fs_extra_1.pathExists)(absPath)) {
866
+ logger_1.logger.warn(`⚠️ 文件不存在: ${fp}`);
867
+ continue;
868
+ }
869
+ const fileStat = await st(absPath);
870
+ const name = fp.split('/').pop() || fp;
871
+ const ext = name.split('.').pop()?.toLowerCase() || '';
872
+ let type = 'other';
873
+ if (['md', 'txt', 'markdown', 'json', 'yaml', 'yml', 'csv'].includes(ext))
874
+ type = 'text';
875
+ else if (['xlsx', 'xls'].includes(ext))
876
+ type = 'excel';
877
+ else if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext))
878
+ type = 'image';
879
+ let content = '';
880
+ if (type === 'text') {
881
+ content = await rf(absPath, 'utf-8');
882
+ }
883
+ else if (type === 'excel') {
884
+ try {
885
+ const XLSX = require('xlsx');
886
+ const wb = XLSX.readFile(absPath);
887
+ const sheets = [];
888
+ for (const sn of wb.SheetNames) {
889
+ sheets.push(`## Sheet: ${sn}\n${XLSX.utils.sheet_to_csv(wb.Sheets[sn])}`);
890
+ }
891
+ content = sheets.join('\n\n');
892
+ }
893
+ catch {
894
+ content = `[Excel 解析失败]`;
895
+ }
896
+ }
897
+ else if (type === 'image') {
898
+ content = `[图片文件: ${absPath}]`;
899
+ }
900
+ else {
901
+ try {
902
+ content = await rf(absPath, 'utf-8');
903
+ }
904
+ catch {
905
+ content = `[无法读取]`;
906
+ }
907
+ }
908
+ allFiles.push({ name, path: absPath, size: fileStat.size, mtime: fileStat.mtime.toISOString(), type, content });
909
+ logger_1.logger.info(` 📎 ${name} (${fileStat.size > 1024 ? (fileStat.size / 1024).toFixed(1) + 'KB' : fileStat.size + 'B'})`);
910
+ }
911
+ }
912
+ // ── 2. 构建澄清上下文 ──
913
+ const desc = options.desc ? normalizeDescription(options.desc) : '';
914
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
915
+ const taskBase = await resolveTaskBase(iterDir);
916
+ const allTasks = await (0, state_1.scanTasks)(iteration);
917
+ const taskDetails = await buildTaskDetails(taskBase);
918
+ // ── 3. Prompt 模式 ──
919
+ if (options.prompt) {
920
+ const promptText = (0, inbox_1.buildClarifyPrompt)(desc || '(从附件分析需求)', allFiles, taskDetails);
921
+ logger_1.logger.info('[SPECCORE_PROMPT]');
922
+ process.stdout.write(promptText);
923
+ return;
924
+ }
925
+ // ── 4. Response 模式 ──
926
+ let clarifiedIntent;
927
+ let clarifiedDesc = desc;
928
+ let clarifiedTasks = [];
929
+ if (options.response) {
930
+ const parsed = (0, inbox_1.parseClarifyResponse)(options.response);
931
+ if (parsed) {
932
+ clarifiedIntent = parsed.intent;
933
+ clarifiedDesc = parsed.structuredDesc || desc;
934
+ clarifiedTasks = parsed.impactReport?.directTasks?.map(t => t.id) || [];
935
+ (0, inbox_1.logClarifyResult)(parsed);
936
+ }
937
+ else {
938
+ logger_1.logger.warn('⚠️ AI 澄清结果解析失败,使用本地分析');
939
+ }
940
+ }
941
+ // ── 5. 本地意图检测 ──
942
+ const intent = clarifiedIntent || detectIntent(desc || allFiles.map(f => f.content).join(' '));
943
+ // ── 6. 新增需求 ──
944
+ if (intent === 'new') {
945
+ logger_1.logger.info('🆕 检测到新增需求意图');
946
+ const newDesc = clarifiedDesc || allFiles.map(f => f.name + ': ' + f.content.slice(0, 200)).join('\n');
947
+ const parsed = options.response ? (0, inbox_1.parseClarifyResponse)(options.response) : null;
948
+ const clarifyOutput = parsed ? { structuredDesc: parsed.structuredDesc, keyPoints: parsed.keyPoints, acceptanceCriteria: parsed.acceptanceCriteria } : undefined;
949
+ await handleNewRequirement(newDesc, iteration, clarifyOutput);
950
+ if (allFiles.length > 0) {
951
+ await (0, inbox_1.markProcessed)(allFiles, 'new', []);
952
+ }
953
+ return;
954
+ }
955
+ // ── 7. 变更:全量影响分析 ──
956
+ let impactReport;
957
+ if (clarifiedTasks.length > 0) {
958
+ const directTasks = clarifiedTasks.map(tid => {
959
+ const task = allTasks.find(t => t.id === tid);
960
+ return { id: tid, name: task?.name || tid, status: 'unknown', level: 'direct', reason: 'AI 澄清匹配', affectedFiles: [], needReExecute: true, needRegression: false };
961
+ }).filter(m => allTasks.some(t => t.id === m.id));
962
+ impactReport = { directTasks, indirectTasks: [], unaffectedTasks: [] };
963
+ }
964
+ else {
965
+ const matchDesc = desc || allFiles.map(f => f.content).join(' ');
966
+ impactReport = await analyzeImpact(matchDesc, iterDir, taskBase);
967
+ }
968
+ const hasImpact = impactReport.directTasks.length > 0 || impactReport.indirectTasks.length > 0;
969
+ if (!hasImpact) {
970
+ logger_1.logger.warn('未匹配到受影响任务。请指定 --task 或检查变更描述/附件。');
971
+ logger_1.logger.info('💡 如果是新增需求,请确保描述以"新增/加/创建"开头');
972
+ return;
973
+ }
974
+ (0, inbox_1.logImpactReport)(impactReport);
975
+ logger_1.logger.info('');
976
+ const changeDesc = clarifiedDesc || desc;
977
+ const affectedIds = [];
978
+ for (const m of impactReport.directTasks) {
979
+ const taskOpts = { ...options, task: m.id, desc: changeDesc };
980
+ await applyTaskChange(taskOpts, iteration);
981
+ affectedIds.push(m.id);
982
+ }
983
+ // 间接影响任务
984
+ for (const m of impactReport.indirectTasks) {
985
+ const indirectTaskDir = (0, path_1.join)(taskBase, m.id);
986
+ const metaStatusPath = (0, path_1.join)(indirectTaskDir, '.meta', 'status');
987
+ const legacyStatusPath = (0, path_1.join)(indirectTaskDir, '.task-status');
988
+ const taskMdPath = (0, path_1.join)(indirectTaskDir, '00-specs', 'TASK.md');
989
+ const now = new Date().toISOString().split('T')[0];
990
+ try {
991
+ if (await (0, fs_extra_1.pathExists)(metaStatusPath)) {
992
+ const currentStatus = (await (0, fs_extra_1.readFile)(metaStatusPath, 'utf-8')).trim();
993
+ if (currentStatus === 'done') {
994
+ await (0, fs_extra_1.writeFile)(metaStatusPath, 'needs-rework');
995
+ logger_1.logger.info(` 📌 ${m.id} 状态从 done 回退为 needs-rework(间接影响)`);
996
+ }
997
+ }
998
+ else if (await (0, fs_extra_1.pathExists)(legacyStatusPath)) {
999
+ const currentStatus = (await (0, fs_extra_1.readFile)(legacyStatusPath, 'utf-8')).trim();
1000
+ if (currentStatus === 'done') {
1001
+ await (0, fs_extra_1.writeFile)(legacyStatusPath, 'needs-rework');
1002
+ logger_1.logger.info(` 📌 ${m.id} 状态从 done 回退为 needs-rework(间接影响)`);
1003
+ }
1004
+ }
1005
+ if (await (0, fs_extra_1.pathExists)(taskMdPath)) {
1006
+ let content = await (0, fs_extra_1.readFile)(taskMdPath, 'utf-8');
1007
+ const indirectNote = `| ${now} | 间接影响 | 上游任务变更,需回归验证 | SpecCore |\n`;
1008
+ if (!content.includes('间接影响')) {
1009
+ content = content.replace(/(\| :--- \| :--- \| :--- \| :--- \|)/, `$1\n${indirectNote}`);
1010
+ await (0, fs_extra_1.writeFile)(taskMdPath, content);
1011
+ }
1012
+ }
1013
+ }
1014
+ catch { /* 忽略 */ }
1015
+ }
1016
+ if (allFiles.length > 0) {
1017
+ await (0, inbox_1.markProcessed)(allFiles, 'change', affectedIds);
1018
+ }
1019
+ await writeChangeSummary(iterDir, changeDesc, impactReport, affectedIds);
1020
+ logger_1.logger.info('');
1021
+ logger_1.logger.success(`✅ 变更已应用到 ${affectedIds.length} 个任务`);
1022
+ logger_1.logger.info(` 📄 变更摘要: 020-specs/CHANGE_SUMMARY.md`);
1023
+ logger_1.logger.info('');
1024
+ logger_1.logger.info('💡 下一步:');
1025
+ for (const id of affectedIds) {
1026
+ logger_1.logger.info(` speccore analyze --task=${id} --sync`);
1027
+ }
1028
+ logger_1.logger.info(` speccore execute --task=${affectedIds.join(',')} --force`);
1029
+ try {
1030
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../core/knowledge-graph')));
1031
+ await refreshKnowledgeGraph(process.cwd(), iteration);
1032
+ logger_1.logger.info('🧠 知识图谱已刷新');
1033
+ }
1034
+ catch { }
1035
+ }
1036
+ /**
1037
+ * 收集变更输入来源
1038
+ * 优先级: --desc > --file > --dir > --inbox > 默认变更收件箱
1039
+ */
1040
+ async function collectChangeInputs(options) {
1041
+ const entries = [];
1042
+ let fromInbox = false;
1043
+ // 1. 直接描述 → 包装为虚拟文件条目
1044
+ if (options.desc) {
1045
+ entries.push({
1046
+ name: 'inline-desc.md',
1047
+ path: '<inline>',
1048
+ size: options.desc.length,
1049
+ mtime: new Date().toISOString(),
1050
+ type: 'text',
1051
+ content: options.desc,
1052
+ });
1053
+ }
1054
+ // 2. --file 指定文件
1055
+ if (options.file) {
1056
+ const filePaths = options.file.split(',').map(f => f.trim());
1057
+ for (const fp of filePaths) {
1058
+ const entry = await (0, change_inbox_1.loadChangeFile)(fp);
1059
+ if (entry)
1060
+ entries.push(entry);
1061
+ }
1062
+ }
1063
+ // 3. --dir 指定目录
1064
+ if (options.dir) {
1065
+ const dirEntries = await (0, change_inbox_1.loadChangeFilesFromDir)(options.dir);
1066
+ entries.push(...dirEntries);
1067
+ }
1068
+ // 4. --inbox 或默认扫描变更收件箱
1069
+ if (options.inbox || (!options.desc && !options.file && !options.dir)) {
1070
+ await (0, change_inbox_1.ensureChangeInboxDir)();
1071
+ const inboxResult = await (0, change_inbox_1.scanChangeInbox)({ reprocess: options.reprocess });
1072
+ (0, change_inbox_1.logChangeInboxScan)(inboxResult);
1073
+ const actionable = [...inboxResult.newFiles, ...inboxResult.modifiedFiles];
1074
+ if (actionable.length > 0) {
1075
+ entries.push(...actionable);
1076
+ fromInbox = true;
1077
+ }
1078
+ }
1079
+ return { entries, fromInbox };
1080
+ }
1081
+ /**
1082
+ * 变更驱动工作流 v2 主流程
1083
+ */
1084
+ async function processChangeV2(options) {
1085
+ const { entries, fromInbox } = await collectChangeInputs(options);
1086
+ if (entries.length === 0) {
1087
+ logger_1.logger.error('请提供变更描述、文件或放入 .speccore/changes/pending/');
1088
+ logger_1.logger.info('用法: speccore change "描述"');
1089
+ logger_1.logger.info(' speccore change --file change.md');
1090
+ logger_1.logger.info(' speccore change --inbox');
1091
+ return;
1092
+ }
1093
+ const iteration = await (0, context_1.getDefaultIteration)(options.iteration);
1094
+ if (!iteration) {
1095
+ logger_1.logger.error('未找到活跃迭代。请先运行: speccore iteration create --name <名称>');
1096
+ return;
1097
+ }
1098
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
1099
+ const taskBase = await resolveTaskBase(iterDir);
1100
+ const allTasks = await (0, state_1.scanTasks)(iteration);
1101
+ logger_1.logger.info(`📋 共 ${entries.length} 个变更项待处理`);
1102
+ logger_1.logger.info('');
1103
+ const processedEntries = [];
1104
+ const allAffectedIds = [];
1105
+ let changeCounter = 0;
1106
+ for (let i = 0; i < entries.length; i++) {
1107
+ const entry = entries[i];
1108
+ changeCounter++;
1109
+ const changeId = `Change-${String(changeCounter).padStart(3, '0')}`;
1110
+ logger_1.logger.info(`⏳ 处理 [${i + 1}/${entries.length}] ${entry.name}`);
1111
+ try {
1112
+ // Step 1: 解析变更文件
1113
+ const changeRequest = (0, change_parser_1.parseChangeFile)(entry.name, entry.content);
1114
+ // Step 2: 意图分类(如果未从文件中解析出)
1115
+ if (changeRequest.type === 'unknown' || changeRequest.category === 'unknown') {
1116
+ const inferred = (0, change_parser_1.inferCategoryFromDescription)(changeRequest.description);
1117
+ changeRequest.type = options.newFlag ? 'new' : inferred.type;
1118
+ changeRequest.category = inferred.category;
1119
+ }
1120
+ if (options.newFlag) {
1121
+ changeRequest.type = 'new';
1122
+ }
1123
+ logger_1.logger.info(` 🔍 意图: ${changeRequest.type === 'new' ? '🆕 新增' : '🔄 变更'} / ${changeRequest.category}`);
1124
+ // Step 3: 新增需求 → 走新增流程
1125
+ if (changeRequest.type === 'new') {
1126
+ await handleNewRequirementV2(changeRequest, iteration);
1127
+ processedEntries.push(entry);
1128
+ continue;
1129
+ }
1130
+ // Step 4: 变更 → AI 影响分析
1131
+ const spinner = new logger_1.Spinner('语义检索 + 影响分析...');
1132
+ spinner.start();
1133
+ const analysis = await (0, ai_impact_analyzer_1.aiImpactAnalysis)(changeRequest.description, changeRequest.category, iteration, allTasks.map(t => ({ id: t.id, name: t.name, status: t.status })), { withCode: options.withCode, useLlm: !options.auto });
1134
+ spinner.stop('影响分析完成');
1135
+ // Step 5: 展示分析结果
1136
+ logAiImpactReport(analysis);
1137
+ // Step 6: 应用变更到任务
1138
+ const affectedIds = await applyAiImpactToTasks(analysis, changeRequest, options, iteration, taskBase);
1139
+ allAffectedIds.push(...affectedIds);
1140
+ // Step 7: 生成 CHANGE_TODO.md
1141
+ if (analysis.taskImpacts.direct.length > 0 || analysis.globalImpacts.length > 0) {
1142
+ const todoContent = (0, ai_impact_analyzer_1.generateChangeTodo)(changeRequest.description, changeRequest.category, analysis, changeId);
1143
+ const todoPath = (0, path_1.join)(iterDir, '020-specs', `${changeId}-TODO.md`);
1144
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(iterDir, '020-specs'));
1145
+ await (0, fs_extra_1.writeFile)(todoPath, todoContent);
1146
+ logger_1.logger.info(` 📝 已生成: 020-specs/${changeId}-TODO.md`);
1147
+ }
1148
+ // Step 8: 更新 CHANGE_SUMMARY.md
1149
+ const impactReport = {
1150
+ directTasks: analysis.taskImpacts.direct.map(t => ({
1151
+ id: t.taskId,
1152
+ name: t.taskName,
1153
+ status: t.status,
1154
+ level: 'direct',
1155
+ reason: t.matchedContext,
1156
+ affectedFiles: t.files,
1157
+ needReExecute: true,
1158
+ needRegression: false,
1159
+ })),
1160
+ indirectTasks: analysis.taskImpacts.indirect.map(t => ({
1161
+ id: t.taskId,
1162
+ name: t.taskName,
1163
+ status: t.status,
1164
+ level: 'indirect',
1165
+ reason: t.matchedContext,
1166
+ affectedFiles: t.files,
1167
+ needReExecute: false,
1168
+ needRegression: true,
1169
+ })),
1170
+ unaffectedTasks: [],
1171
+ };
1172
+ await writeChangeSummary(iterDir, changeRequest.description, impactReport, affectedIds);
1173
+ // Step 9: 标记已处理
1174
+ processedEntries.push(entry);
1175
+ if (fromInbox) {
1176
+ await (0, change_inbox_1.markChangeProcessed)([entry], 'change', affectedIds, changeId);
1177
+ }
1178
+ logger_1.logger.info(` ✅ ${changeId} 处理完成`);
1179
+ logger_1.logger.info('');
1180
+ }
1181
+ catch (e) {
1182
+ logger_1.logger.error(` ❌ ${entry.name} 处理失败: ${e.message}`);
1183
+ if (fromInbox) {
1184
+ await (0, change_inbox_1.markChangeFailed)(entry.name, e.message);
1185
+ }
1186
+ }
1187
+ }
1188
+ // Step 10: 归档/清理原始文件
1189
+ if (fromInbox && processedEntries.length > 0) {
1190
+ let strategy = 'archive';
1191
+ if (options.keep)
1192
+ strategy = 'keep';
1193
+ if (options.deleteAfterProcess)
1194
+ strategy = 'delete';
1195
+ if (options.archiveStrategy)
1196
+ strategy = options.archiveStrategy;
1197
+ await (0, change_inbox_1.archiveProcessedFiles)(processedEntries, strategy);
1198
+ }
1199
+ // 输出总结
1200
+ logger_1.logger.info('');
1201
+ logger_1.logger.success(`✅ 变更处理完成: ${processedEntries.length}/${entries.length}`);
1202
+ if (allAffectedIds.length > 0) {
1203
+ logger_1.logger.info(` 📌 影响任务: ${[...new Set(allAffectedIds)].join(', ')}`);
1204
+ }
1205
+ logger_1.logger.info('');
1206
+ logger_1.logger.info('💡 下一步:');
1207
+ logger_1.logger.info(' speccore status # 查看当前迭代状态');
1208
+ logger_1.logger.info(' speccore analyze --global --withCode # 刷新全局层');
1209
+ logger_1.logger.info(' speccore execute --task <Task-XXX> --force # 重新执行受影响任务');
1210
+ // 自动刷新知识图谱
1211
+ try {
1212
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../core/knowledge-graph')));
1213
+ await refreshKnowledgeGraph(process.cwd(), iteration);
1214
+ logger_1.logger.info('🧠 知识图谱已刷新');
1215
+ }
1216
+ catch { }
1217
+ }
1218
+ /**
1219
+ * 展示 AI 影响分析报告
1220
+ */
1221
+ function logAiImpactReport(analysis) {
1222
+ logger_1.logger.info('');
1223
+ logger_1.logger.info('📊 AI 影响分析:');
1224
+ logger_1.logger.info('┌─────────────────────────────────────┐');
1225
+ if (analysis.taskImpacts.direct.length > 0) {
1226
+ logger_1.logger.info('│ 🔴 直接影响:');
1227
+ for (const t of analysis.taskImpacts.direct) {
1228
+ logger_1.logger.info(`│ ${t.taskId} ${t.taskName} [${(t.score * 100).toFixed(0)}%]`);
1229
+ }
1230
+ }
1231
+ if (analysis.taskImpacts.indirect.length > 0) {
1232
+ logger_1.logger.info('│ 🟡 间接影响:');
1233
+ for (const t of analysis.taskImpacts.indirect) {
1234
+ logger_1.logger.info(`│ ${t.taskId} ${t.taskName} [${(t.score * 100).toFixed(0)}%]`);
1235
+ }
1236
+ }
1237
+ if (analysis.codeImpacts.length > 0) {
1238
+ logger_1.logger.info('│ 💻 代码级变更:');
1239
+ for (const c of analysis.codeImpacts.slice(0, 3)) {
1240
+ logger_1.logger.info(`│ ${c.file}`);
1241
+ }
1242
+ if (analysis.codeImpacts.length > 3) {
1243
+ logger_1.logger.info(`│ ... 等 ${analysis.codeImpacts.length} 个文件`);
1244
+ }
1245
+ }
1246
+ if (analysis.globalImpacts.length > 0) {
1247
+ logger_1.logger.info('│ 🌍 全局层建议:');
1248
+ for (const g of analysis.globalImpacts) {
1249
+ logger_1.logger.info(`│ ${g.artifact}`);
1250
+ }
1251
+ }
1252
+ logger_1.logger.info('└─────────────────────────────────────┘');
1253
+ }
1254
+ /**
1255
+ * 将 AI 影响分析结果应用到任务
1256
+ */
1257
+ async function applyAiImpactToTasks(analysis, changeRequest, options, iteration, taskBase) {
1258
+ const affectedIds = [];
1259
+ // 处理直接影响任务
1260
+ for (const task of analysis.taskImpacts.direct) {
1261
+ const taskOpts = { ...options, task: task.taskId, desc: changeRequest.description };
1262
+ await applyTaskChange(taskOpts, iteration);
1263
+ affectedIds.push(task.taskId);
1264
+ }
1265
+ // 处理间接影响任务(标记为 need-review)
1266
+ for (const task of analysis.taskImpacts.indirect) {
1267
+ const indirectTaskDir = (0, path_1.join)(taskBase, task.taskId);
1268
+ const metaStatusPath = (0, path_1.join)(indirectTaskDir, '.meta', 'status');
1269
+ const legacyStatusPath = (0, path_1.join)(indirectTaskDir, '.task-status');
1270
+ const taskMdPath = (0, path_1.join)(indirectTaskDir, '00-specs', 'TASK.md');
1271
+ const now = new Date().toISOString().split('T')[0];
1272
+ try {
1273
+ if (await (0, fs_extra_1.pathExists)(metaStatusPath)) {
1274
+ const currentStatus = (await (0, fs_extra_1.readFile)(metaStatusPath, 'utf-8')).trim();
1275
+ if (currentStatus === 'done') {
1276
+ await (0, fs_extra_1.writeFile)(metaStatusPath, 'needs-rework');
1277
+ logger_1.logger.info(` 📌 ${task.taskId} 状态从 done 回退为 needs-rework(间接影响)`);
1278
+ }
1279
+ }
1280
+ else if (await (0, fs_extra_1.pathExists)(legacyStatusPath)) {
1281
+ const currentStatus = (await (0, fs_extra_1.readFile)(legacyStatusPath, 'utf-8')).trim();
1282
+ if (currentStatus === 'done') {
1283
+ await (0, fs_extra_1.writeFile)(legacyStatusPath, 'needs-rework');
1284
+ logger_1.logger.info(` 📌 ${task.taskId} 状态从 done 回退为 needs-rework(间接影响)`);
1285
+ }
1286
+ }
1287
+ if (await (0, fs_extra_1.pathExists)(taskMdPath)) {
1288
+ let content = await (0, fs_extra_1.readFile)(taskMdPath, 'utf-8');
1289
+ const indirectNote = `| ${now} | 间接影响 | 上游任务变更,需回归验证 | SpecCore |\n`;
1290
+ if (!content.includes('间接影响')) {
1291
+ content = content.replace(/(\| :--- \| :--- \| :--- \| :--- \|)/, `$1\n${indirectNote}`);
1292
+ await (0, fs_extra_1.writeFile)(taskMdPath, content);
1293
+ }
1294
+ }
1295
+ }
1296
+ catch { /* 忽略 */ }
1297
+ }
1298
+ return affectedIds;
1299
+ }
1300
+ /**
1301
+ * 新增需求处理 v2(增强版)
1302
+ */
1303
+ async function handleNewRequirementV2(changeRequest, iteration) {
1304
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
1305
+ const taskBase = await resolveTaskBase(iterDir);
1306
+ await (0, fs_extra_1.ensureDir)(taskBase);
1307
+ const { id: taskId } = await (0, global_counters_1.nextTaskId)();
1308
+ const taskName = changeRequest.title || changeRequest.description.replace(/^(新增?|加|创建|实现|做)/, '').replace(/[::]/g, '').trim() || taskId;
1309
+ const taskDir = (0, path_1.join)(taskBase, taskId);
1310
+ const specsDir = (0, path_1.join)(taskDir, '00-specs');
1311
+ await (0, fs_extra_1.ensureDir)(specsDir);
1312
+ const now = new Date().toISOString().split('T')[0];
1313
+ const tx = new transaction_1.FileTransaction();
1314
+ // 构建 REQ.md
1315
+ let reqContent = `# ${taskName}\n\n`;
1316
+ reqContent += `## 需求描述\n\n${changeRequest.description}\n\n`;
1317
+ if (changeRequest.acceptanceCriteria.length > 0) {
1318
+ reqContent += `## 验收标准\n\n`;
1319
+ for (const c of changeRequest.acceptanceCriteria) {
1320
+ reqContent += `- [ ] ${c}\n`;
1321
+ }
1322
+ reqContent += '\n';
1323
+ }
1324
+ else {
1325
+ reqContent += `## 验收标准\n\n- [ ] 功能正常\n`;
1326
+ }
1327
+ reqContent += `## 分析记录\n\n- 分析时间: ${now}\n- 分析方式: AI 变更驱动工作流 v2\n`;
1328
+ tx.write((0, path_1.join)(specsDir, 'REQ.md'), reqContent);
1329
+ tx.write((0, path_1.join)(specsDir, 'CHANGELOG.md'), `# 变更记录\n\n| 时间 | 版本 | 变更内容 | 变更人 |\n| :--- | :--- | :--- | :--- |\n| ${now} | v1.0 | 初始创建 | SpecCore |\n`);
1330
+ tx.write((0, path_1.join)(specsDir, 'TASK.md'), `# ${taskName}\n\n- 状态: 待开发\n- 优先级: ${changeRequest.priority}\n- 类型: feature\n\n## 变更履历\n\n| 时间 | 版本 | 变更内容 | 变更人 |\n| :--- | :--- | :--- | :--- |\n| ${now} | v1.0 | 创建任务 | SpecCore |\n`);
1331
+ await tx.commit();
1332
+ // 追加到 REQUIREMENT.md
1333
+ const reqPath = (0, path_1.join)(iterDir, '020-specs', 'REQUIREMENT.md');
1334
+ if (await (0, fs_extra_1.pathExists)(reqPath)) {
1335
+ let content = await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
1336
+ content += `\n\n## ${taskName}\n\n${changeRequest.description}\n`;
1337
+ await (0, fs_extra_1.writeFile)(reqPath, content);
1338
+ }
1339
+ // 更新 PROJECT_GRAPH.md
1340
+ const graphPath = (0, path_1.join)(iterDir, '000-overview', 'PROJECT_GRAPH.md');
1341
+ if (await (0, fs_extra_1.pathExists)(graphPath)) {
1342
+ let content = await (0, fs_extra_1.readFile)(graphPath, 'utf-8');
1343
+ content += `| ${taskId} | ${taskName} | feature | pending |\n`;
1344
+ await (0, fs_extra_1.writeFile)(graphPath, content);
1345
+ }
1346
+ logger_1.logger.success(` ✅ 新任务已创建: ${taskId}`);
1347
+ logger_1.logger.info(` 📄 ${taskId}/00-specs/REQ.md`);
1348
+ // 刷新知识图谱
1349
+ try {
1350
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../core/knowledge-graph')));
1351
+ await refreshKnowledgeGraph(process.cwd(), iteration);
1352
+ }
1353
+ catch { }
1354
+ }
1028
1355
  //# sourceMappingURL=change.js.map