speccore 6.76.1 → 6.77.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.
- package/.agents/skills/spec-analyze/SKILL.md +174 -4
- package/.agents/skills/spec-change/SKILL.md +156 -20
- package/.agents/skills/spec-doc2spec/SKILL.md +121 -4
- package/.agents/skills/spec-execute/SKILL.md +148 -5
- package/.agents/skills/spec-iteration-create/SKILL.md +128 -9
- package/.agents/skills/spec-plan/SKILL.md +123 -4
- package/.agents/skills/spec-spec2doc/SKILL.md +126 -4
- package/.agents/skills/spec-split/SKILL.md +214 -4
- package/.agents/skills/spec-task-create/SKILL.md +156 -9
- package/.agents/skills/speccore-router/SKILL.md +21 -1
- package/dist/cli.js +22 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +198 -14
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/clarify.d.ts +11 -0
- package/dist/commands/clarify.d.ts.map +1 -0
- package/dist/commands/clarify.js +138 -0
- package/dist/commands/clarify.js.map +1 -0
- package/dist/commands/execute.d.ts +1 -0
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +15 -0
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/iteration/split.d.ts +3 -1
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +125 -153
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/update.js +0 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/core/ask-engine.d.ts.map +1 -1
- package/dist/core/ask-engine.js +3 -2
- package/dist/core/ask-engine.js.map +1 -1
- package/dist/core/change-detector.d.ts +25 -0
- package/dist/core/change-detector.d.ts.map +1 -0
- package/dist/core/change-detector.js +170 -0
- package/dist/core/change-detector.js.map +1 -0
- package/dist/core/requirement-clarifier.d.ts +41 -0
- package/dist/core/requirement-clarifier.d.ts.map +1 -0
- package/dist/core/requirement-clarifier.js +181 -0
- package/dist/core/requirement-clarifier.js.map +1 -0
- package/package.json +1 -1
- package/.agents/skills/spec-dev/SKILL.md +0 -10
- package/.agents/skills/spec-synthesize/SKILL.md +0 -10
|
@@ -142,45 +142,11 @@ function generateSubtaskId(parentTaskId, platform) {
|
|
|
142
142
|
// 因为每个任务每个端只有一个子任务,所以 {taskId}-{platform} 已经唯一
|
|
143
143
|
return `Task-${parentTaskId}-${platform}`;
|
|
144
144
|
}
|
|
145
|
-
/**
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
atomic: { label: '细粒度 (atomic)', minHours: 4, maxHours: 24, maxApis: 3, maxTables: 2, maxPages: 1, desc: '每个任务 1-3 天,按接口/表拆分' },
|
|
145
|
+
/** 拆分约束常量(简化版:每个功能单元按涉及的端拆分,每端1个子任务) */
|
|
146
|
+
const SPLIT_RULES = {
|
|
147
|
+
maxTasksPerIteration: 20, // 单次迭代总任务数上限
|
|
148
|
+
maxTasksPerUnit: 3, // 每个功能单元最多任务数
|
|
150
149
|
};
|
|
151
|
-
/** 校验任务工时是否在粒度范围内(按单人 max 工时计算) */
|
|
152
|
-
function validateGranularity(gran, hoursByPlatform, apiCount, tableCount) {
|
|
153
|
-
const rule = GRANULARITY_RULES[gran];
|
|
154
|
-
const warnings = [];
|
|
155
|
-
const platformEntries = Object.entries(hoursByPlatform);
|
|
156
|
-
const maxPerPerson = platformEntries.length > 0 ? Math.max(...platformEntries.map(([, h]) => h)) : 0;
|
|
157
|
-
const totalHours = platformEntries.reduce((sum, [, h]) => sum + h, 0);
|
|
158
|
-
const maxPlatform = platformEntries.length > 0 ? platformEntries.reduce((a, b) => (b[1] > a[1] ? b : a))[0] : '';
|
|
159
|
-
if (maxPerPerson > rule.maxHours) {
|
|
160
|
-
warnings.push(`⚠️ 单人最大工时 ${maxPerPerson}h(${maxPlatform})超出上限 ${rule.maxHours}h → 建议再拆`);
|
|
161
|
-
}
|
|
162
|
-
else if (maxPerPerson < rule.minHours) {
|
|
163
|
-
warnings.push(`⚠️ 单人最大工时 ${maxPerPerson}h(${maxPlatform})低于下限 ${rule.minHours}h → 建议合并到关联任务`);
|
|
164
|
-
}
|
|
165
|
-
if (apiCount > rule.maxApis)
|
|
166
|
-
warnings.push(`⚠️ 接口 ${apiCount} 个超出上限 ${rule.maxApis} → 建议按业务领域拆分`);
|
|
167
|
-
if (tableCount > rule.maxTables)
|
|
168
|
-
warnings.push(`⚠️ 数据表 ${tableCount} 张超出上限 ${rule.maxTables} → 建议按数据层拆分`);
|
|
169
|
-
// 返回额外信息供展示
|
|
170
|
-
if (platformEntries.length > 1) {
|
|
171
|
-
const breakdown = platformEntries.map(([p, h]) => `${p}:${h}h`).join(' + ');
|
|
172
|
-
warnings.unshift(`ℹ️ 工时分布: ${breakdown} = ${totalHours}h(max per person: ${maxPerPerson}h)`);
|
|
173
|
-
}
|
|
174
|
-
return warnings;
|
|
175
|
-
}
|
|
176
|
-
/** 根据团队规模推荐粒度 */
|
|
177
|
-
function recommendGranularity(teamSize) {
|
|
178
|
-
if (teamSize <= 3)
|
|
179
|
-
return 'macro';
|
|
180
|
-
if (teamSize <= 8)
|
|
181
|
-
return 'module';
|
|
182
|
-
return 'atomic';
|
|
183
|
-
}
|
|
184
150
|
function promptUser(question) {
|
|
185
151
|
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
186
152
|
return new Promise(resolve => {
|
|
@@ -359,16 +325,10 @@ async function iterationSplitCommand(options) {
|
|
|
359
325
|
};
|
|
360
326
|
section._complexity = {
|
|
361
327
|
estimatedHours: task.estimatedHours || 8,
|
|
362
|
-
hoursByPlatform: (task.hoursByPlatform && typeof task.hoursByPlatform === 'object') ? task.hoursByPlatform : {},
|
|
363
328
|
priority: task.priority || 'medium',
|
|
364
|
-
complexity: task.risk === 'high' ? 'high' : task.risk === 'low' ? 'low' : 'medium',
|
|
365
|
-
apiCount: (task.apis || []).length,
|
|
366
|
-
dbCount: (task.tables || []).length,
|
|
367
|
-
pageCount: 0,
|
|
368
|
-
wordCount: content.length,
|
|
369
329
|
};
|
|
370
330
|
section._owner = task.owner || '未分配';
|
|
371
|
-
section._taskType = (task.type && ['feature', 'bugfix', 'refactor', 'research'].includes(task.type)) ? task.type : 'feature';
|
|
331
|
+
section._taskType = (task.type && ['feature', 'bugfix', 'refactor', 'research', 'security', 'performance'].includes(task.type)) ? task.type : 'feature';
|
|
372
332
|
// 保存 topic slug,用于生成任务目录名
|
|
373
333
|
section._topic = task.topic || slugify(task.name || `Task ${i + 1}`);
|
|
374
334
|
// 保存 AI 生成的实际内容(用于写入 REQ.md / TECH.md)
|
|
@@ -406,13 +366,8 @@ async function iterationSplitCommand(options) {
|
|
|
406
366
|
logger_1.logger.info(` 🗑 已清理旧任务目录`);
|
|
407
367
|
}
|
|
408
368
|
}
|
|
409
|
-
// 确定粒度
|
|
410
|
-
const staffing2 = readStaffing(iterDirFull);
|
|
411
|
-
const teamSize2 = staffing2 ? staffing2.length : 0;
|
|
412
|
-
const granularity = options.granularity || recommendGranularity(teamSize2);
|
|
413
|
-
const granRule = GRANULARITY_RULES[granularity];
|
|
414
369
|
// 🚨 全局任务数硬限制(安全网:防止 AI 输出爆炸)
|
|
415
|
-
const MAX_TASKS_HARD =
|
|
370
|
+
const MAX_TASKS_HARD = SPLIT_RULES.maxTasksPerIteration;
|
|
416
371
|
if (sections.length > MAX_TASKS_HARD && !options.force) {
|
|
417
372
|
logger_1.logger.error(`\n ❌ 任务数爆炸!AI 输出了 ${sections.length} 个任务(上限 ${MAX_TASKS_HARD})`);
|
|
418
373
|
logger_1.logger.error(` 💡 这说明拆分粒度过细,必须合并。请告诉 AI:"任务太多,请合并相关功能,总数控制在 ${MAX_TASKS_HARD} 以内"`);
|
|
@@ -471,7 +426,6 @@ async function iterationSplitCommand(options) {
|
|
|
471
426
|
}
|
|
472
427
|
// 交互模式判断:显式 --interactive 或 stdin 是 TTY(--force 时跳过交互,直接执行)
|
|
473
428
|
const isInteractive = (options.interactive || process.stdin.isTTY) && !options.force;
|
|
474
|
-
logger_1.logger.info(` 📏 粒度: ${granRule.label}${options.granularity ? ' (用户指定)' : ` (${teamSize2} 人团队自动推荐)`}`);
|
|
475
429
|
// 非交互模式:显示任务总览摘要
|
|
476
430
|
if (!isInteractive) {
|
|
477
431
|
logger_1.logger.info(`\n 📋 任务总览(共 ${sections.length} 个):`);
|
|
@@ -501,19 +455,8 @@ async function iterationSplitCommand(options) {
|
|
|
501
455
|
logger_1.logger.info(`\n ━━━━ 任务 ${i + 1}/${sections.length} ━━━━`);
|
|
502
456
|
logger_1.logger.info(` 📌 ${sec.name}`);
|
|
503
457
|
logger_1.logger.info(` 🏷 类型: ${taskType} | 🎯 优先级: ${complexity.priority || 'medium'}`);
|
|
504
|
-
|
|
505
|
-
const hbp = complexity.hoursByPlatform || {};
|
|
506
|
-
const hbpEntries = Object.entries(hbp);
|
|
507
|
-
if (hbpEntries.length > 0) {
|
|
508
|
-
const breakdown = hbpEntries.map(([p, h]) => `${p}:${h}h`).join(' + ');
|
|
509
|
-
const maxPerPerson = Math.max(...hbpEntries.map(([, h]) => h));
|
|
510
|
-
logger_1.logger.info(` ⏱ 工时: ${breakdown} = ${complexity.estimatedHours}h(max per person: ${maxPerPerson}h)`);
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
458
|
+
if (complexity.estimatedHours)
|
|
513
459
|
logger_1.logger.info(` ⏱ 预估: ${complexity.estimatedHours}h`);
|
|
514
|
-
}
|
|
515
|
-
if (complexity.apiCount)
|
|
516
|
-
logger_1.logger.info(` 🔌 接口: ${complexity.apiCount} 个 | 🗄 数据表: ${complexity.dbCount || 0} 张`);
|
|
517
460
|
if (deps.length > 0)
|
|
518
461
|
logger_1.logger.info(` 🔗 依赖: ${deps.join(', ')}`);
|
|
519
462
|
if (acs.length > 0) {
|
|
@@ -521,16 +464,6 @@ async function iterationSplitCommand(options) {
|
|
|
521
464
|
for (const ac of acs.slice(0, 5))
|
|
522
465
|
logger_1.logger.info(` ${ac}`);
|
|
523
466
|
}
|
|
524
|
-
// 粒度校验(按单人 max 工时)
|
|
525
|
-
const warnings = validateGranularity(granularity, hbp, complexity.apiCount || 0, complexity.dbCount || 0);
|
|
526
|
-
if (warnings.length > 0) {
|
|
527
|
-
for (const w of warnings) {
|
|
528
|
-
if (w.startsWith('ℹ️'))
|
|
529
|
-
logger_1.logger.info(` ${w}`);
|
|
530
|
-
else
|
|
531
|
-
logger_1.logger.warn(` ${w}`);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
467
|
// 交互确认(仅确认,调整应回到 AI 对话重新生成方案)
|
|
535
468
|
if (isInteractive) {
|
|
536
469
|
const answer = await promptUser(` 确认创建?(y/回车确认,n 调整方案):`);
|
|
@@ -615,6 +548,12 @@ async function iterationSplitCommand(options) {
|
|
|
615
548
|
return;
|
|
616
549
|
}
|
|
617
550
|
const iterationDir = await (0, context_1.getIterationDir)(iteration);
|
|
551
|
+
// v6.76.0+: 变更检测 — 检查 020-specs/ 是否比 Task/ 更新
|
|
552
|
+
if (!options.ignoreSpecsUpdate && !options.prompt && !options.response) {
|
|
553
|
+
const { detectSpecChangesBeforeSplit, printChangeDetection } = await Promise.resolve().then(() => __importStar(require('../../core/change-detector')));
|
|
554
|
+
const changeResult = await detectSpecChangesBeforeSplit(iterationDir);
|
|
555
|
+
printChangeDetection(changeResult, '拆分前检测');
|
|
556
|
+
}
|
|
618
557
|
// ── v6.49.13+: 模块驱动拆分 — CLI 按功能模块×端创建任务目录,AI 只填充内容 ──
|
|
619
558
|
if (!options.prompt && !options.response) {
|
|
620
559
|
const moduleDrivenResult = await tryModuleDrivenSplit(iteration, iterationDir, options);
|
|
@@ -748,16 +687,15 @@ async function iterationSplitCommand(options) {
|
|
|
748
687
|
}
|
|
749
688
|
}
|
|
750
689
|
}
|
|
751
|
-
// 粒度推荐(基于 STAFFING 人数)
|
|
752
|
-
const staffing = readStaffing(iterationDir);
|
|
753
|
-
const teamSize = staffing ? staffing.length : 0;
|
|
754
|
-
const recommendedGranularity = options.granularity || recommendGranularity(teamSize);
|
|
755
|
-
const granularityLabel = GRANULARITY_RULES[recommendedGranularity].label;
|
|
756
|
-
const granularityHint = GRANULARITY_RULES[recommendedGranularity].desc;
|
|
757
690
|
// 获取标准端名列表(用于 prompt 注入和 scope 规范化)
|
|
758
691
|
const allPlatforms = await detectPlatforms(iterationDir);
|
|
692
|
+
// v6.76.0+: 扫描已有 Task 结构(增量拆分)
|
|
693
|
+
const existingTaskStructure = await scanExistingTaskStructure(iterationDir);
|
|
759
694
|
// 构建完整 prompt(v6.69.3+: 传入标准端名列表,确保 AI 使用正确的端名)
|
|
760
|
-
|
|
695
|
+
// v6.76.0+: 传入过滤条件,限制拆分范围;传入已有 Task 结构,支持增量拆分
|
|
696
|
+
const modulesFilter = options.modules ? options.modules.split(',').map(m => m.trim()).filter(Boolean) : undefined;
|
|
697
|
+
const platformsFilter = options.platforms ? options.platforms.split(',').map(p => p.trim()).filter(Boolean) : undefined;
|
|
698
|
+
let splitPrompt = buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, allPlatforms, modulesFilter, platformsFilter, existingTaskStructure, options.devGuide);
|
|
761
699
|
// 注入全局上下文(INDEX + TOC 目录,AI 自主读取)
|
|
762
700
|
const { loadGlobalContext, formatGlobalContext } = await Promise.resolve().then(() => __importStar(require('../../core/prompt-builder')));
|
|
763
701
|
const globalCtx = await loadGlobalContext(process.cwd(), 'split');
|
|
@@ -766,7 +704,6 @@ async function iterationSplitCommand(options) {
|
|
|
766
704
|
}
|
|
767
705
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(promptsDir, `split-suggestion-${iteration}.md`), splitPrompt);
|
|
768
706
|
logger_1.logger.info(` 🤖 AI 拆分建议 → .speccore/prompts/split-suggestion-${iteration}.md`);
|
|
769
|
-
logger_1.logger.info(` 📏 推荐粒度: ${granularityLabel}${options.granularity ? ' (用户指定)' : ` (基于 ${teamSize} 人团队自动推荐)`}`);
|
|
770
707
|
logger_1.logger.info(` 📜 上下文: CONSTITUTION + REQUIREMENT + ${specContents.length} 个 Spec 文档`);
|
|
771
708
|
}
|
|
772
709
|
else {
|
|
@@ -778,10 +715,19 @@ async function iterationSplitCommand(options) {
|
|
|
778
715
|
return;
|
|
779
716
|
}
|
|
780
717
|
const content = await (0, fs_extra_1.readFile)(reqFile, 'utf-8');
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
718
|
+
let sections = extractSections(content, options.sections);
|
|
719
|
+
// v6.76.0+: 按功能模块过滤
|
|
720
|
+
if (options.modules) {
|
|
721
|
+
const moduleFilters = options.modules.split(',').map(m => m.trim()).filter(Boolean);
|
|
722
|
+
const beforeCount = sections.length;
|
|
723
|
+
sections = sections.filter(s => moduleFilters.some(f => s.name.includes(f)));
|
|
724
|
+
if (sections.length < beforeCount) {
|
|
725
|
+
logger_1.logger.info(` 🎯 模块过滤: ${beforeCount} → ${sections.length}(只拆分: ${moduleFilters.join(', ')})`);
|
|
726
|
+
}
|
|
727
|
+
if (sections.length === 0) {
|
|
728
|
+
spinner.fail(`没有匹配 "${options.modules}" 的功能模块,请检查模块名称`);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
785
731
|
}
|
|
786
732
|
const platforms = await detectPlatforms(iterationDir, options.platforms);
|
|
787
733
|
// ── 冲突检测: 检查是否已有 Task 目录 ──
|
|
@@ -870,7 +816,7 @@ async function iterationSplitCommand(options) {
|
|
|
870
816
|
logger_1.logger.info(` ${taskId} → ${sections[i].name}`);
|
|
871
817
|
if (contentPreview)
|
|
872
818
|
logger_1.logger.info(` ${contentPreview}`);
|
|
873
|
-
logger_1.logger.info(` 优先级: ${c.priority} | 工时: ${c.estimatedHours}h |
|
|
819
|
+
logger_1.logger.info(` 优先级: ${c.priority} | 工时: ${c.estimatedHours}h | 👤 ${owner}`);
|
|
874
820
|
if (deps)
|
|
875
821
|
logger_1.logger.info(` 🔗 依赖: ${deps.join(', ')}`);
|
|
876
822
|
logger_1.logger.info(` 平台: ${platforms.join(', ')}`);
|
|
@@ -1111,7 +1057,7 @@ async function createTaskFromSection(iterationDir, taskId, section, allPlatforms
|
|
|
1111
1057
|
taskPlatforms = allPlatforms;
|
|
1112
1058
|
}
|
|
1113
1059
|
}
|
|
1114
|
-
const complexity = section._complexity || { estimatedHours:
|
|
1060
|
+
const complexity = section._complexity || { estimatedHours: 8, priority: 'medium' };
|
|
1115
1061
|
const owner = section._owner || '未分配';
|
|
1116
1062
|
const today = new Date().toISOString().split('T')[0];
|
|
1117
1063
|
// 加载迭代级 analyze 产出(020-specs/),用于填充任务级文件
|
|
@@ -1494,7 +1440,7 @@ ${isBk ? apiList : pageList}
|
|
|
1494
1440
|
const subtaskId = subtaskIdMap.get(platform);
|
|
1495
1441
|
// 子任务目录名 = subtaskId(如 Task-001-booking-service)
|
|
1496
1442
|
const subtaskDir = (0, path_1.join)(platformDir, subtaskId);
|
|
1497
|
-
const subtaskHours =
|
|
1443
|
+
const subtaskHours = complexity.estimatedHours || 8;
|
|
1498
1444
|
// 判断是否后端(用于生成不同的文档内容)
|
|
1499
1445
|
const isBk = platform === 'backend' || platform.startsWith('后台') || /-(service|api|server|backend)$/i.test(platform);
|
|
1500
1446
|
await createSubtask(subtaskDir, subtaskId, platform, isBk ? '后端' : platform, isBk, subtaskHours);
|
|
@@ -1557,7 +1503,7 @@ ${isBk ? apiList : pageList}
|
|
|
1557
1503
|
? `020-specs/${sourceFile}`
|
|
1558
1504
|
: `020-specs/${taskType === 'feature' ? 'features' : taskType === 'bugfix' ? 'bugs' : taskType === 'refactor' ? 'refactors' : 'research'}/${topic}.md`;
|
|
1559
1505
|
// 任务类型中文标签
|
|
1560
|
-
const typeLabels = { feature: '功能开发', bugfix: '缺陷修复', refactor: '重构优化', research: '技术调研' };
|
|
1506
|
+
const typeLabels = { feature: '功能开发', bugfix: '缺陷修复', refactor: '重构优化', research: '技术调研', security: '安全修复', performance: '性能优化' };
|
|
1561
1507
|
const typeLabel = typeLabels[taskType] || taskType;
|
|
1562
1508
|
// 收集同迭代的其它任务(用于关联关系)
|
|
1563
1509
|
const relatedTasks = allSections
|
|
@@ -2433,9 +2379,38 @@ ${isH5 ? '移动端优先,适配 375/414/768' :
|
|
|
2433
2379
|
/**
|
|
2434
2380
|
* 构建完整的 AI 智能拆分 Prompt(含 SpecCore 理念 + 粒度规则 + 完整上下文)
|
|
2435
2381
|
*/
|
|
2436
|
-
function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents,
|
|
2382
|
+
function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents, standardPlatforms, modulesFilter, platformsFilter, existingTasks, devGuide) {
|
|
2437
2383
|
let p = `# SpecCore AI 智能拆分\n\n`;
|
|
2438
|
-
p += `> 迭代: ${iteration} |
|
|
2384
|
+
p += `> 迭代: ${iteration} | 生成: ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
2385
|
+
// v6.76.0+: 拆分范围限制
|
|
2386
|
+
if (modulesFilter?.length || platformsFilter?.length) {
|
|
2387
|
+
p += `## 🎯 拆分范围限制(用户指定)\n\n`;
|
|
2388
|
+
if (modulesFilter?.length) {
|
|
2389
|
+
p += `- **只拆分以下功能模块**: ${modulesFilter.join('、')}\n`;
|
|
2390
|
+
p += `- 如果需求文档中有其他功能模块,**完全忽略**,不要拆分\n`;
|
|
2391
|
+
}
|
|
2392
|
+
if (platformsFilter?.length) {
|
|
2393
|
+
p += `- **只拆分涉及以下端的功能模块**: ${platformsFilter.join('、')}\n`;
|
|
2394
|
+
p += `- 如果某个功能模块不涉及这些端,**跳过不拆**\n`;
|
|
2395
|
+
p += `- 每个任务的 scope 只能包含这些端,不要引入其他端\n`;
|
|
2396
|
+
}
|
|
2397
|
+
p += `\n`;
|
|
2398
|
+
}
|
|
2399
|
+
// v6.76.0+: 增量拆分 — 已有 Task 结构
|
|
2400
|
+
if (existingTasks && existingTasks.size > 0) {
|
|
2401
|
+
p += `## 📂 已有 Task 结构(增量拆分模式)\n\n`;
|
|
2402
|
+
p += `本次拆分为**增量模式**:以下 Task 已存在,如果新拆分的功能单元与已有 Task 匹配,**复用该 Task ID,只追加新端**。\n\n`;
|
|
2403
|
+
for (const [taskName, platforms] of existingTasks.entries()) {
|
|
2404
|
+
p += `- **${taskName}**: 已有端 [${platforms.join(', ')}]\n`;
|
|
2405
|
+
}
|
|
2406
|
+
p += `\n`;
|
|
2407
|
+
p += `### 增量拆分规则\n`;
|
|
2408
|
+
p += `- 如果功能单元与已有 Task 的 functionalUnit 相同 → **复用该 Task ID**,scope 追加新端\n`;
|
|
2409
|
+
p += `- 如果功能单元是全新的 → 生成新的 Task ID(延续现有编号)\n`;
|
|
2410
|
+
p += `- 已有端不要重复拆分,只拆分**新增的端**\n`;
|
|
2411
|
+
p += `- 已有 Task 的 _shared/ 目录保持不变,新端目录追加到 10-backend/ 或 20-frontend/ 下\n`;
|
|
2412
|
+
p += `- API 契约(_shared/API_CONTRACT.yaml)需要补充新端涉及的接口\n\n`;
|
|
2413
|
+
}
|
|
2439
2414
|
// 技术宪法
|
|
2440
2415
|
if (constitutionContent) {
|
|
2441
2416
|
p += `## 📜 技术宪法 (CONSTITUTION.md)\n\n${constitutionContent.slice(0, 3000)}\n\n---\n\n`;
|
|
@@ -2446,35 +2421,19 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2446
2421
|
for (const spec of specContents) {
|
|
2447
2422
|
p += `## 📜 ${spec.name}\n\n${spec.content.slice(0, 3000)}\n\n---\n\n`;
|
|
2448
2423
|
}
|
|
2449
|
-
//
|
|
2450
|
-
if (
|
|
2451
|
-
p += `##
|
|
2452
|
-
p +=
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
p +=
|
|
2457
|
-
}
|
|
2458
|
-
// 粒度说明(含硬约束)
|
|
2459
|
-
p += `## 🎯 拆分粒度: ${granularityLabel}\n\n`;
|
|
2460
|
-
p += `${granularityHint}\n\n`;
|
|
2461
|
-
p += `### 当前粒度硬约束(必须严格遵守)\n`;
|
|
2462
|
-
p += `> ⚠️ 工时约束按 **max(各端工时)** 计算,即单个开发人员的实际工作量,不是所有端的总和\n\n`;
|
|
2463
|
-
if (granularityLabel.includes('粗')) {
|
|
2464
|
-
p += `- 每人工时: 20-80h(1-2 周)\n- 接口上限: 15 个/任务\n- 数据表上限: 5 张/任务\n- 页面上限: 5 个/任务\n`;
|
|
2465
|
-
}
|
|
2466
|
-
else if (granularityLabel.includes('中')) {
|
|
2467
|
-
p += `- 每人工时: 12-40h(3-5 天)\n- 接口上限: 8 个/任务\n- 数据表上限: 3 张/任务\n- 页面上限: 3 个/任务\n`;
|
|
2468
|
-
}
|
|
2469
|
-
else {
|
|
2470
|
-
p += `- 每人工时: 4-24h(1-3 天)\n- 接口上限: 3 个/任务\n- 数据表上限: 2 张/任务\n- 页面上限: 1 个/任务\n`;
|
|
2424
|
+
// v6.69.3+: 注入标准端名列表
|
|
2425
|
+
if (standardPlatforms.length > 0) {
|
|
2426
|
+
p += `## 🖥️ 项目端列表(标准端名)\n\n`;
|
|
2427
|
+
p += `本项目已配置以下端,所有 scope 必须使用这些**标准端名**,禁止使用简写或中文:\n\n`;
|
|
2428
|
+
p += `\`${standardPlatforms.join('`, `')}\`\n\n`;
|
|
2429
|
+
p += `- 后端端名示例: \`booking-service\`, \`room-service\`(不是 "后端"、"backend")\n`;
|
|
2430
|
+
p += `- 前端端名示例: \`admin-web\`, \`h5-mobile\`(不是 "web"、"admin"、"h5")\n`;
|
|
2431
|
+
p += `- **scope 数组必须使用上述标准端名**,否则会导致目录结构错误\n\n`;
|
|
2471
2432
|
}
|
|
2472
|
-
p += `\n**超出上限必须再拆,低于下限必须合并。**\n\n`;
|
|
2473
|
-
p += `用户可通过 --granularity macro|module|atomic 调整全局粒度。\n\n`;
|
|
2474
2433
|
// v6.69.3+: 注入标准端名列表
|
|
2475
2434
|
if (standardPlatforms.length > 0) {
|
|
2476
2435
|
p += `## 🖥️ 项目端列表(标准端名)\n\n`;
|
|
2477
|
-
p += `本项目已配置以下端,所有 scope
|
|
2436
|
+
p += `本项目已配置以下端,所有 scope 必须使用这些**标准端名**,禁止使用简写或中文:\n\n`;
|
|
2478
2437
|
p += `\`${standardPlatforms.join('`, `')}\`\n\n`;
|
|
2479
2438
|
p += `- 后端端名示例: \`booking-service\`, \`room-service\`(不是 "后端"、"backend")\n`;
|
|
2480
2439
|
p += `- 前端端名示例: \`admin-web\`, \`h5-mobile\`(不是 "web"、"admin"、"h5")\n`;
|
|
@@ -2556,13 +2515,9 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2556
2515
|
p += ` "functionalUnit": "所属功能单元/影响域(必填!见下方类型规则)",\n`;
|
|
2557
2516
|
p += ` "name": "任务名称(中文)",\n`;
|
|
2558
2517
|
p += ` "topic": "english-slug-for-directory",\n`;
|
|
2559
|
-
p += ` "type": "feature|bugfix|refactor|research",\n`;
|
|
2518
|
+
p += ` "type": "feature|bugfix|refactor|research|security|performance",\n`;
|
|
2560
2519
|
p += ` "reason": "为什么这样拆分",\n`;
|
|
2561
2520
|
p += ` "scope": ["booking-service", "admin-web"],\n`;
|
|
2562
|
-
p += ` "apis": ["POST /api/auth/login"],\n`;
|
|
2563
|
-
p += ` "tables": ["users"],\n`;
|
|
2564
|
-
p += ` "hoursByPlatform": { "booking-service": 8, "admin-web": 8 },\n`;
|
|
2565
|
-
p += ` "estimatedHours": 16,\n`;
|
|
2566
2521
|
p += ` "priority": "high|medium|low",\n`;
|
|
2567
2522
|
p += ` "dependencies": [],\n`;
|
|
2568
2523
|
p += ` "acceptanceCriteria": ["AC1: ..."],\n`;
|
|
@@ -2570,13 +2525,15 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2570
2525
|
p += ` "owner": "建议负责人",\n`;
|
|
2571
2526
|
p += ` "sourceFile": "来源文档路径(如 bugs/login-timeout.md、features/user-auth.md)",\n`;
|
|
2572
2527
|
p += ` "reqContent": "需求描述内容(Markdown 格式,写入 REQ.md)",\n`;
|
|
2573
|
-
p += ` "techContent": "技术方案内容(Markdown 格式,写入 TECH.md)"\n`;
|
|
2528
|
+
p += ` "techContent": "技术方案内容(Markdown 格式,写入 TECH.md)"${devGuide ? `,\n "devGuideContent": "开发者实现指南(Markdown 格式,写入 DEV_GUIDE.md):实现步骤、关键代码示例、与存量功能集成、注意事项"` : ''}\n`;
|
|
2574
2529
|
p += ` }\n]\n`;
|
|
2575
2530
|
p += '```\n\n';
|
|
2576
2531
|
p += `> **functionalUnit 必须填写**:根据任务类型语义不同:\n`;
|
|
2577
2532
|
p += `> - feature → 功能模块名(如:用户管理、订单系统、支付模块)\n`;
|
|
2578
2533
|
p += `> - bugfix → 受影响组件/流程(如:登录流程、支付回调、数据同步)\n`;
|
|
2579
2534
|
p += `> - refactor → 重构目标范围(如:数据库层、API网关、状态管理)\n`;
|
|
2535
|
+
p += `> - security → 安全修复范围(如:SQL注入防护、XSS修复、鉴权加固)\n`;
|
|
2536
|
+
p += `> - performance → 性能优化目标(如:查询优化、缓存策略、并发提升)\n`;
|
|
2580
2537
|
p += `> - research → 研究主题(如:WebSocket方案、缓存策略)\n`;
|
|
2581
2538
|
p += `> 同一模块/领域的任务填相同的值,用于粒度校验和任务分组\n`;
|
|
2582
2539
|
p += `> **topic** 必须是英文短横线格式(如 \`user-authentication\`、\`product-crud\`),用于生成任务目录名 Task-NNN-{topic}\n`;
|
|
@@ -2592,16 +2549,15 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2592
2549
|
p += `> - 从 020-specs/global/TECH.md 和对应端 TECH.md 中提取本任务相关的技术细节\n`;
|
|
2593
2550
|
p += `> - 直接写入 00-specs/TECH.md,执行时 AI 据此直接开发\n`;
|
|
2594
2551
|
p += `> **质量红线**:如果 reqContent/techContent 只有标题和占位符(如 "<!-- AI-FILL -->"),视为不合格,必须重新生成\n\n`;
|
|
2595
|
-
p += `### ⚠️
|
|
2596
|
-
p += `-
|
|
2597
|
-
p += `-
|
|
2598
|
-
p += `-
|
|
2599
|
-
p += `-
|
|
2600
|
-
p += `- 同一功能的前后端各端工作必须在一个原子任务里,不要按端拆分任务\n\n`;
|
|
2552
|
+
p += `### ⚠️ 拆分规则(重要)\n\n`;
|
|
2553
|
+
p += `- **一个功能单元默认 1 个任务**,最多拆成 3 个\n`;
|
|
2554
|
+
p += `- **按 scope 涉及的端创建子任务**:每个端 1 个子任务目录\n`;
|
|
2555
|
+
p += `- **不要按端拆分任务**:同一功能的前后端各端工作在一个任务里,按端拆分子任务\n`;
|
|
2556
|
+
p += `- **用户不满意时可手动拆分**:CLI 提供简单的默认拆分,复杂拆分由用户手动调整\n\n`;
|
|
2601
2557
|
// 质量自检
|
|
2602
2558
|
p += `## ✅ 质量自检(必须全部通过)\n\n`;
|
|
2603
2559
|
p += `□ 每个任务都满足原子任务定义?\n`;
|
|
2604
|
-
p += `□ 每个任务的
|
|
2560
|
+
p += `□ 每个任务的 scope 只包含必要的端?\n`;
|
|
2605
2561
|
p += `□ 没有循环依赖?\n`;
|
|
2606
2562
|
p += `□ 基础模块排在前面?\n`;
|
|
2607
2563
|
p += `□ 同功能单元内的任务没被过度拆分?(每个功能单元 ≤ 3 个任务)\n`;
|
|
@@ -2683,35 +2639,24 @@ function autoAssign(section, platforms, staffing) {
|
|
|
2683
2639
|
return best ? best.name : '未分配';
|
|
2684
2640
|
}
|
|
2685
2641
|
/**
|
|
2686
|
-
*
|
|
2642
|
+
* 分析章节复杂度,决定优先级和工时(简化版)
|
|
2687
2643
|
*/
|
|
2688
2644
|
function estimateSectionComplexity(section) {
|
|
2689
2645
|
const content = section.content || '';
|
|
2690
2646
|
const name = section.name || '';
|
|
2691
2647
|
const full = `${name}\n${content}`;
|
|
2692
|
-
//
|
|
2693
|
-
const apiCount = (full.match(/\/api\/|API|接口|endpoint|POST|GET|PUT|DELETE/gi) || []).length;
|
|
2694
|
-
const dbCount = (full.match(/数据库|表|DDL|schema|model|entity|索引|字段/gi) || []).length;
|
|
2695
|
-
const pageCount = (full.match(/页面|表单|列表|详情|弹窗|modal|dialog/gi) || []).length;
|
|
2648
|
+
// 粗略估算工时(仅用于展示)
|
|
2696
2649
|
const wordCount = full.length;
|
|
2697
|
-
|
|
2698
|
-
let complexity = 'medium';
|
|
2699
|
-
let score = apiCount * 3 + dbCount * 2 + pageCount;
|
|
2700
|
-
if (score <= 3 && wordCount < 200)
|
|
2701
|
-
complexity = 'low';
|
|
2702
|
-
else if (score >= 10 || wordCount > 800)
|
|
2703
|
-
complexity = 'high';
|
|
2704
|
-
// 工时预估
|
|
2705
|
-
const estimatedHours = complexity === 'high' ? 16 : complexity === 'medium' ? 8 : 4;
|
|
2650
|
+
const estimatedHours = wordCount > 800 ? 16 : wordCount > 300 ? 8 : 4;
|
|
2706
2651
|
// 优先级
|
|
2707
2652
|
let priority = 'medium';
|
|
2708
|
-
if (
|
|
2653
|
+
if (full.includes('核心') || full.includes('基础') || full.includes('安全')) {
|
|
2709
2654
|
priority = 'high';
|
|
2710
2655
|
}
|
|
2711
|
-
else if (
|
|
2656
|
+
else if (wordCount < 200) {
|
|
2712
2657
|
priority = 'low';
|
|
2713
2658
|
}
|
|
2714
|
-
return {
|
|
2659
|
+
return { estimatedHours, priority };
|
|
2715
2660
|
}
|
|
2716
2661
|
/**
|
|
2717
2662
|
* 语义依赖检测: 比字符串匹配更准确的任务间关系
|
|
@@ -2779,6 +2724,39 @@ async function detectExistingTasks(iterDir) {
|
|
|
2779
2724
|
await scanRecursive(targetDir);
|
|
2780
2725
|
return tasks;
|
|
2781
2726
|
}
|
|
2727
|
+
/**
|
|
2728
|
+
* v6.76.0+: 扫描已有 Task 的端结构
|
|
2729
|
+
* 返回每个 Task 已有的端目录(10-backend/{端}, 20-frontend/{端})
|
|
2730
|
+
*/
|
|
2731
|
+
async function scanExistingTaskStructure(iterDir) {
|
|
2732
|
+
const result = new Map();
|
|
2733
|
+
const tasks = await detectExistingTasks(iterDir);
|
|
2734
|
+
for (const taskName of tasks) {
|
|
2735
|
+
const taskDir = (0, path_1.join)(iterDir, '030-tasks', taskName);
|
|
2736
|
+
if (!(await (0, fs_extra_1.pathExists)(taskDir)))
|
|
2737
|
+
continue;
|
|
2738
|
+
const platforms = [];
|
|
2739
|
+
// 扫描 10-backend/ 和 20-frontend/ 子目录
|
|
2740
|
+
for (const category of ['10-backend', '20-frontend']) {
|
|
2741
|
+
const catDir = (0, path_1.join)(taskDir, category);
|
|
2742
|
+
if (!(await (0, fs_extra_1.pathExists)(catDir)))
|
|
2743
|
+
continue;
|
|
2744
|
+
try {
|
|
2745
|
+
const entries = await (0, fs_extra_1.readdir)(catDir, { withFileTypes: true });
|
|
2746
|
+
for (const e of entries) {
|
|
2747
|
+
if (e.isDirectory() && !e.name.startsWith('.')) {
|
|
2748
|
+
platforms.push(`${category}/${e.name}`);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
catch { }
|
|
2753
|
+
}
|
|
2754
|
+
if (platforms.length > 0) {
|
|
2755
|
+
result.set(taskName, platforms);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return result;
|
|
2759
|
+
}
|
|
2782
2760
|
/**
|
|
2783
2761
|
* 生成任务总览报告 → 000-overview/TASK_SUMMARY.md
|
|
2784
2762
|
* 包含:任务名、功能单元、人工工时、AI工时、优先级、依赖、风险
|
|
@@ -3175,13 +3153,7 @@ async function tryModuleDrivenSplit(iteration, iterationDir, options) {
|
|
|
3175
3153
|
section._scopePlatforms = modPlatforms;
|
|
3176
3154
|
section._complexity = {
|
|
3177
3155
|
estimatedHours: 8,
|
|
3178
|
-
hoursByPlatform: {},
|
|
3179
3156
|
priority: 'medium',
|
|
3180
|
-
complexity: 'medium',
|
|
3181
|
-
apiCount: 0,
|
|
3182
|
-
dbCount: 0,
|
|
3183
|
-
pageCount: 0,
|
|
3184
|
-
wordCount: 0,
|
|
3185
3157
|
};
|
|
3186
3158
|
section._owner = '未分配';
|
|
3187
3159
|
section._taskId = taskId;
|