speccore 5.87.2 → 5.88.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/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +10 -3
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/iteration/list.d.ts.map +1 -1
- package/dist/commands/iteration/list.js +17 -1
- package/dist/commands/iteration/list.js.map +1 -1
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +250 -54
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/spec2doc.d.ts.map +1 -1
- package/dist/commands/spec2doc.js +4 -2
- package/dist/commands/spec2doc.js.map +1 -1
- package/dist/commands/task/new.js +3 -1
- package/dist/commands/task/new.js.map +1 -1
- package/dist/core/global-counters.d.ts.map +1 -1
- package/dist/core/global-counters.js +15 -1
- package/dist/core/global-counters.js.map +1 -1
- package/dist/core/task-paths.d.ts +12 -4
- package/dist/core/task-paths.d.ts.map +1 -1
- package/dist/core/task-paths.js +35 -5
- package/dist/core/task-paths.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,45 @@ const task_utils_1 = require("../../utils/task-utils");
|
|
|
11
11
|
const next_steps_1 = require("../../core/next-steps");
|
|
12
12
|
const readline_1 = require("readline");
|
|
13
13
|
const prompt_builder_1 = require("../../core/prompt-builder");
|
|
14
|
+
/** 将名称转为目录安全的短 slug(2-4 词) */
|
|
15
|
+
function slugify(name) {
|
|
16
|
+
return name
|
|
17
|
+
.replace(/[\u4e00-\u9fff]/g, '') // 去掉中文
|
|
18
|
+
.replace(/[^a-zA-Z0-9\s-]/g, '') // 去特殊字符
|
|
19
|
+
.split(/\s+/)
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.slice(0, 3) // 最多 3 词
|
|
22
|
+
.join('-')
|
|
23
|
+
.toLowerCase() || 'task';
|
|
24
|
+
}
|
|
25
|
+
/** 粒度约束常量 */
|
|
26
|
+
const GRANULARITY_RULES = {
|
|
27
|
+
macro: { label: '粗粒度 (macro)', minHours: 20, maxHours: 80, maxApis: 15, maxTables: 5, maxPages: 5, desc: '每个任务 1-2 周,按业务方向合并' },
|
|
28
|
+
module: { label: '中粒度 (module)', minHours: 12, maxHours: 40, maxApis: 8, maxTables: 3, maxPages: 3, desc: '每个任务 3-5 天,按功能/端拆分' },
|
|
29
|
+
atomic: { label: '细粒度 (atomic)', minHours: 4, maxHours: 24, maxApis: 3, maxTables: 2, maxPages: 1, desc: '每个任务 1-3 天,按接口/表拆分' },
|
|
30
|
+
};
|
|
31
|
+
/** 校验任务工时是否在粒度范围内 */
|
|
32
|
+
function validateGranularity(gran, hours, apiCount, tableCount) {
|
|
33
|
+
const rule = GRANULARITY_RULES[gran];
|
|
34
|
+
const warnings = [];
|
|
35
|
+
if (hours > rule.maxHours)
|
|
36
|
+
warnings.push(`⚠️ 工时 ${hours}h 超出上限 ${rule.maxHours}h → 建议再拆`);
|
|
37
|
+
else if (hours < rule.minHours)
|
|
38
|
+
warnings.push(`⚠️ 工时 ${hours}h 低于下限 ${rule.minHours}h → 建议合并到关联任务`);
|
|
39
|
+
if (apiCount > rule.maxApis)
|
|
40
|
+
warnings.push(`⚠️ 接口 ${apiCount} 个超出上限 ${rule.maxApis} → 建议按业务领域拆分`);
|
|
41
|
+
if (tableCount > rule.maxTables)
|
|
42
|
+
warnings.push(`⚠️ 数据表 ${tableCount} 张超出上限 ${rule.maxTables} → 建议按数据层拆分`);
|
|
43
|
+
return warnings;
|
|
44
|
+
}
|
|
45
|
+
/** 根据团队规模推荐粒度 */
|
|
46
|
+
function recommendGranularity(teamSize) {
|
|
47
|
+
if (teamSize <= 3)
|
|
48
|
+
return 'macro';
|
|
49
|
+
if (teamSize <= 8)
|
|
50
|
+
return 'module';
|
|
51
|
+
return 'atomic';
|
|
52
|
+
}
|
|
14
53
|
function promptUser(question) {
|
|
15
54
|
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
16
55
|
return new Promise(resolve => {
|
|
@@ -20,7 +59,42 @@ function promptUser(question) {
|
|
|
20
59
|
async function detectPlatforms(iterationDir, specified) {
|
|
21
60
|
if (specified)
|
|
22
61
|
return specified.split(',').map(p => p.trim()).filter(Boolean);
|
|
23
|
-
//
|
|
62
|
+
// 1. 优先从 CONSTITUTION.md 读取「对应需求端」配置
|
|
63
|
+
const constitutionPath = (0, path_1.join)('.speccore', 'CONSTITUTION.md');
|
|
64
|
+
if (await (0, fs_extra_1.pathExists)(constitutionPath)) {
|
|
65
|
+
const content = await (0, fs_extra_1.readFile)(constitutionPath, 'utf-8');
|
|
66
|
+
const lines = content.split('\n');
|
|
67
|
+
let headerIdx = -1;
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
if (lines[i].includes('对应需求端')) {
|
|
70
|
+
headerIdx = i;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (headerIdx >= 0) {
|
|
75
|
+
const headers = lines[headerIdx].split('|').map(h => h.trim()).filter(Boolean);
|
|
76
|
+
const platformColIdx = headers.findIndex(h => h.includes('对应需求端'));
|
|
77
|
+
if (platformColIdx >= 0) {
|
|
78
|
+
const platforms = new Set();
|
|
79
|
+
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
80
|
+
const line = lines[i].trim();
|
|
81
|
+
if (!line.startsWith('|') || line.match(/^\|\s*[-:]/))
|
|
82
|
+
continue;
|
|
83
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
84
|
+
if (cells[platformColIdx]) {
|
|
85
|
+
cells[platformColIdx].split(',').forEach((p) => {
|
|
86
|
+
const trimmed = p.trim();
|
|
87
|
+
if (trimmed && !trimmed.startsWith('>'))
|
|
88
|
+
platforms.add(trimmed);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (platforms.size > 0)
|
|
93
|
+
return [...platforms];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 2. 回退:扫描 020-specs/ 子目录
|
|
24
98
|
const specsDir = (0, path_1.join)(iterationDir, '020-specs');
|
|
25
99
|
if (await (0, fs_extra_1.pathExists)(specsDir)) {
|
|
26
100
|
const entries = await (0, fs_extra_1.readdir)(specsDir, { withFileTypes: true });
|
|
@@ -30,7 +104,7 @@ async function detectPlatforms(iterationDir, specified) {
|
|
|
30
104
|
if (platforms.length > 0)
|
|
31
105
|
return platforms;
|
|
32
106
|
}
|
|
33
|
-
return ['web']; //
|
|
107
|
+
return ['web']; // 默认
|
|
34
108
|
}
|
|
35
109
|
async function iterationSplitCommand(options) {
|
|
36
110
|
// ── Prompt 模式 ──
|
|
@@ -63,7 +137,8 @@ async function iterationSplitCommand(options) {
|
|
|
63
137
|
const task = tasks[i];
|
|
64
138
|
// 将 AI JSON 转换为 Section,复用 createTaskFromSection 创建完整目录
|
|
65
139
|
const desc = task.description || task.name || '';
|
|
66
|
-
const
|
|
140
|
+
const scopeArr = Array.isArray(task.scope) ? task.scope : [];
|
|
141
|
+
const scope = scopeArr.join(', ');
|
|
67
142
|
const apis = Array.isArray(task.apis) ? task.apis.join('\n') : '';
|
|
68
143
|
const acs = Array.isArray(task.acceptanceCriteria) ? task.acceptanceCriteria.join('\n') : '';
|
|
69
144
|
let content = desc;
|
|
@@ -73,11 +148,18 @@ async function iterationSplitCommand(options) {
|
|
|
73
148
|
content += `\n\n接口:\n${apis}`;
|
|
74
149
|
if (acs)
|
|
75
150
|
content += `\n\n验收标准:\n${acs}`;
|
|
151
|
+
// 从 scope 提取平台列表(后端 + 前端各端)
|
|
152
|
+
const taskScopePlatforms = [];
|
|
153
|
+
const isBackend = scopeArr.some((s) => /后端|backend/i.test(s));
|
|
154
|
+
const fePlatforms = scopeArr.filter((s) => !/后端|backend/i.test(s)).map((s) => s.trim()).filter(Boolean);
|
|
155
|
+
if (isBackend)
|
|
156
|
+
taskScopePlatforms.push('backend');
|
|
157
|
+
taskScopePlatforms.push(...fePlatforms);
|
|
76
158
|
const section = {
|
|
77
159
|
name: task.name || `Task ${i + 1}`,
|
|
78
160
|
content,
|
|
79
161
|
level: 2,
|
|
80
|
-
platform:
|
|
162
|
+
platform: isBackend ? 'backend' : (fePlatforms[0] || undefined),
|
|
81
163
|
};
|
|
82
164
|
section._complexity = {
|
|
83
165
|
estimatedHours: task.estimatedHours || 8,
|
|
@@ -89,6 +171,9 @@ async function iterationSplitCommand(options) {
|
|
|
89
171
|
wordCount: content.length,
|
|
90
172
|
};
|
|
91
173
|
section._owner = task.owner || '未分配';
|
|
174
|
+
section._taskType = (task.type && ['feature', 'bugfix', 'refactor', 'research'].includes(task.type)) ? task.type : 'feature';
|
|
175
|
+
if (taskScopePlatforms.length > 0)
|
|
176
|
+
section._scopePlatforms = taskScopePlatforms;
|
|
92
177
|
sections.push(section);
|
|
93
178
|
}
|
|
94
179
|
// 检测已有任务 + 冲突处理
|
|
@@ -98,17 +183,77 @@ async function iterationSplitCommand(options) {
|
|
|
98
183
|
logger_1.logger.info(' 使用 --force 强制覆盖');
|
|
99
184
|
return;
|
|
100
185
|
}
|
|
101
|
-
//
|
|
102
|
-
|
|
186
|
+
// --force 清理旧任务(避免新旧叠加编号暴增)
|
|
187
|
+
if (options.force) {
|
|
188
|
+
const tasksRoot = (0, path_1.join)(iterDirFull, '030-tasks');
|
|
189
|
+
if (await (0, fs_extra_1.pathExists)(tasksRoot)) {
|
|
190
|
+
const entries = await (0, fs_extra_1.readdir)(tasksRoot, { withFileTypes: true });
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (entry.isDirectory()) {
|
|
193
|
+
await (0, fs_extra_1.remove)((0, path_1.join)(tasksRoot, entry.name));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
logger_1.logger.info(` 🗑 已清理旧任务目录`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// 确定粒度
|
|
200
|
+
const staffing2 = readStaffing(iterDirFull);
|
|
201
|
+
const teamSize2 = staffing2 ? staffing2.length : 0;
|
|
202
|
+
const granularity = options.granularity || recommendGranularity(teamSize2);
|
|
203
|
+
const granRule = GRANULARITY_RULES[granularity];
|
|
204
|
+
const isInteractive = process.stdin.isTTY; // 非 TTY(管道调用)时自动确认
|
|
205
|
+
logger_1.logger.info(` 📏 粒度: ${granRule.label}${options.granularity ? ' (用户指定)' : ` (${teamSize2} 人团队自动推荐)`}`);
|
|
206
|
+
if (!isInteractive)
|
|
207
|
+
logger_1.logger.info(' ℹ️ 非交互终端,自动确认所有任务');
|
|
208
|
+
// 逐任务交互确认
|
|
209
|
+
const createdSections = [];
|
|
103
210
|
for (let i = 0; i < sections.length; i++) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
211
|
+
const sec = sections[i];
|
|
212
|
+
const complexity = sec._complexity || {};
|
|
213
|
+
const taskType = sec._taskType || 'feature';
|
|
214
|
+
const deps = (tasks[i].dependencies || []);
|
|
215
|
+
const acs = (tasks[i].acceptanceCriteria || []);
|
|
216
|
+
// 展示任务摘要
|
|
217
|
+
logger_1.logger.info(`\n ━━━━ 任务 ${i + 1}/${sections.length} ━━━━`);
|
|
218
|
+
logger_1.logger.info(` 📌 ${sec.name}`);
|
|
219
|
+
logger_1.logger.info(` 🏷 类型: ${taskType} | ⏱ 预估: ${complexity.estimatedHours}h | 🎯 优先级: ${complexity.priority || 'medium'}`);
|
|
220
|
+
if (complexity.apiCount)
|
|
221
|
+
logger_1.logger.info(` 🔌 接口: ${complexity.apiCount} 个 | 🗄 数据表: ${complexity.dbCount || 0} 张`);
|
|
222
|
+
if (deps.length > 0)
|
|
223
|
+
logger_1.logger.info(` 🔗 依赖: ${deps.join(', ')}`);
|
|
224
|
+
if (acs.length > 0) {
|
|
225
|
+
logger_1.logger.info(` ✅ 验收标准:`);
|
|
226
|
+
for (const ac of acs.slice(0, 5))
|
|
227
|
+
logger_1.logger.info(` ${ac}`);
|
|
228
|
+
}
|
|
229
|
+
// 粒度校验
|
|
230
|
+
const warnings = validateGranularity(granularity, complexity.estimatedHours || 8, complexity.apiCount || 0, complexity.dbCount || 0);
|
|
231
|
+
if (warnings.length > 0) {
|
|
232
|
+
for (const w of warnings)
|
|
233
|
+
logger_1.logger.warn(` ${w}`);
|
|
234
|
+
}
|
|
235
|
+
// 交互确认(仅确认,调整应回到 AI 对话重新生成方案)
|
|
236
|
+
if (isInteractive) {
|
|
237
|
+
const answer = await promptUser(` 确认创建?(y/回车确认,n 调整方案):`);
|
|
238
|
+
if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
|
|
239
|
+
logger_1.logger.info(` 💡 如需调整,请告诉 AI:`);
|
|
240
|
+
logger_1.logger.info(` "把 XX 和 YY 合为一个任务" / "ZZ 任务太大,拆成两个" / "修改工时为 Xh"`);
|
|
241
|
+
logger_1.logger.info(` AI 会参考 .speccore/prompts/split-suggestion-${iter}.md 中的规则重新生成`);
|
|
242
|
+
logger_1.logger.info(` 调整后再次执行本命令即可`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const { id: taskId } = await (0, global_counters_1.nextTaskId)(sec.name);
|
|
247
|
+
sec._taskId = taskId;
|
|
248
|
+
await createTaskFromSection(iterDirFull, taskId, sec, allPlatforms, taskType);
|
|
249
|
+
createdSections.push(sec);
|
|
250
|
+
logger_1.logger.info(` ✅ 创建: ${taskId} - [${taskType}] ${sec.name}`);
|
|
251
|
+
}
|
|
252
|
+
if (createdSections.length > 0) {
|
|
253
|
+
await generateImpactGraph(iterDirFull, createdSections, allPlatforms);
|
|
254
|
+
await updateProjectGraph(iterDirFull, createdSections);
|
|
108
255
|
}
|
|
109
|
-
|
|
110
|
-
await updateProjectGraph(iterDirFull, sections);
|
|
111
|
-
logger_1.logger.success(`✅ 创建了 ${sections.length} 个任务(完整目录结构)`);
|
|
256
|
+
logger_1.logger.success(`✅ 创建了 ${createdSections.length}/${sections.length} 个任务(${sections.length - createdSections.length} 个跳过)`);
|
|
112
257
|
}
|
|
113
258
|
else {
|
|
114
259
|
logger_1.logger.warn('AI 返回格式非数组,将作为 Markdown 写入 REQUIREMENT.md');
|
|
@@ -204,12 +349,9 @@ async function iterationSplitCommand(options) {
|
|
|
204
349
|
// 粒度推荐(基于 STAFFING 人数)
|
|
205
350
|
const staffing = readStaffing(iterationDir);
|
|
206
351
|
const teamSize = staffing ? staffing.length : 0;
|
|
207
|
-
const recommendedGranularity = options.granularity ||
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
recommendedGranularity === 'module' ? '\u4e2d\u7c92\u5ea6 (module)' : '\u7ec6\u7c92\u5ea6 (atomic)';
|
|
211
|
-
const granularityHint = recommendedGranularity === 'macro' ? '\u6bcf\u4e2a\u4efb\u52a1 1-2 \u5468\uff0c\u6309\u4e1a\u52a1\u65b9\u5411\u5408\u5e76' :
|
|
212
|
-
recommendedGranularity === 'module' ? '\u6bcf\u4e2a\u4efb\u52a1 3-5 \u5929\uff0c\u6309\u529f\u80fd/\u7aef\u62c6\u5206' : '\u6bcf\u4e2a\u4efb\u52a1 1-3 \u5929\uff0c\u6309\u63a5\u53e3/\u8868\u62c6\u5206';
|
|
352
|
+
const recommendedGranularity = options.granularity || recommendGranularity(teamSize);
|
|
353
|
+
const granularityLabel = GRANULARITY_RULES[recommendedGranularity].label;
|
|
354
|
+
const granularityHint = GRANULARITY_RULES[recommendedGranularity].desc;
|
|
213
355
|
// 构建完整 prompt
|
|
214
356
|
let splitPrompt = buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, staffing, teamSize, granularityLabel, granularityHint);
|
|
215
357
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(promptsDir, `split-suggestion-${iteration}.md`), splitPrompt);
|
|
@@ -275,7 +417,7 @@ async function iterationSplitCommand(options) {
|
|
|
275
417
|
}
|
|
276
418
|
for (const section of approved) {
|
|
277
419
|
const taskId = section._taskId;
|
|
278
|
-
await createTaskFromSection(iterationDir, taskId, section, platforms);
|
|
420
|
+
await createTaskFromSection(iterationDir, taskId, section, platforms, section._taskType);
|
|
279
421
|
}
|
|
280
422
|
spinner.stop(`✅ 创建了 ${approved.length} 个任务`);
|
|
281
423
|
return;
|
|
@@ -335,7 +477,7 @@ async function iterationSplitCommand(options) {
|
|
|
335
477
|
break;
|
|
336
478
|
}
|
|
337
479
|
if (resp?.toLowerCase() === 'y' || resp === '') {
|
|
338
|
-
await createTaskFromSection(iterationDir, taskId, sections[i], platforms);
|
|
480
|
+
await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType);
|
|
339
481
|
created++;
|
|
340
482
|
logger_1.logger.info(` ✅ ${taskId}`);
|
|
341
483
|
}
|
|
@@ -353,7 +495,7 @@ async function iterationSplitCommand(options) {
|
|
|
353
495
|
// Default: create all
|
|
354
496
|
for (let i = 0; i < sections.length; i++) {
|
|
355
497
|
const taskId = sections[i]._taskId;
|
|
356
|
-
await createTaskFromSection(iterationDir, taskId, sections[i], platforms);
|
|
498
|
+
await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType);
|
|
357
499
|
}
|
|
358
500
|
await generateImpactGraph(iterationDir, sections, platforms);
|
|
359
501
|
await generateEnvExample(iterationDir, sections);
|
|
@@ -365,7 +507,7 @@ async function iterationSplitCommand(options) {
|
|
|
365
507
|
// Create tasks(使用预分配的 ID)
|
|
366
508
|
for (let i = 0; i < sections.length; i++) {
|
|
367
509
|
const taskId = sections[i]._taskId;
|
|
368
|
-
await createTaskFromSection(iterationDir, taskId, sections[i], platforms);
|
|
510
|
+
await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType);
|
|
369
511
|
}
|
|
370
512
|
// ── Generate impact graph + risk scores ──
|
|
371
513
|
await generateImpactGraph(iterationDir, sections, platforms);
|
|
@@ -465,15 +607,16 @@ function filterTemplateNoise(sections) {
|
|
|
465
607
|
return true;
|
|
466
608
|
});
|
|
467
609
|
}
|
|
468
|
-
async function createTaskFromSection(iterationDir, taskId, section, allPlatforms) {
|
|
469
|
-
|
|
470
|
-
const
|
|
610
|
+
async function createTaskFromSection(iterationDir, taskId, section, allPlatforms, taskType = 'feature') {
|
|
611
|
+
// taskId 已含 slug(nextTaskId 返回 Task-NNN-slug),直接用
|
|
612
|
+
const taskDir = (0, path_1.join)(iterationDir, '030-tasks', taskType, taskId);
|
|
613
|
+
const taskPlatforms = section._scopePlatforms || (section.platform ? [section.platform] : allPlatforms);
|
|
471
614
|
const complexity = section._complexity || { estimatedHours: 2, priority: 'medium', complexity: 'medium', apiCount: 0, dbCount: 0, pageCount: 0, wordCount: 0 };
|
|
472
615
|
const owner = section._owner || '未分配';
|
|
473
616
|
const today = new Date().toISOString().split('T')[0];
|
|
474
617
|
// ── 1. 元信息目录 ──
|
|
475
618
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '.meta'));
|
|
476
|
-
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'type'),
|
|
619
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'type'), taskType);
|
|
477
620
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'status'), 'todo');
|
|
478
621
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'owner'), owner);
|
|
479
622
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'created-at'), today);
|
|
@@ -605,7 +748,7 @@ ${apiDesc}
|
|
|
605
748
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'TASK.md'), `# ${section.name}
|
|
606
749
|
|
|
607
750
|
## 任务信息
|
|
608
|
-
- 类型:
|
|
751
|
+
- 类型: ${taskType}
|
|
609
752
|
- 状态: 🔲 待开发
|
|
610
753
|
- 优先级: ${complexity.priority}
|
|
611
754
|
- 负责人: ${owner}
|
|
@@ -665,19 +808,26 @@ ${apiDesc}
|
|
|
665
808
|
if (adr) {
|
|
666
809
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '99-artifacts', 'ADR.md'), adr);
|
|
667
810
|
}
|
|
668
|
-
// ── 5. 实现目录 10-backend/ 20-
|
|
811
|
+
// ── 5. 实现目录 10-backend/ 20-frontend/ ──
|
|
669
812
|
for (const platform of taskPlatforms) {
|
|
670
|
-
if (platform
|
|
813
|
+
if (platform === 'backend') {
|
|
814
|
+
// 纯后端任务:直接创建 10-backend/src/ 和 10-backend/tests/
|
|
815
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', 'src'));
|
|
816
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', 'tests'));
|
|
817
|
+
}
|
|
818
|
+
else if (platform.startsWith('后台')) {
|
|
819
|
+
// 后台服务任务:创建 10-backend/{service}/src/ 和 tests/
|
|
671
820
|
const service = platform.replace(/^后台/, '').trim() || 'default';
|
|
672
|
-
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', service
|
|
673
|
-
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', service
|
|
821
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', service, 'src'));
|
|
822
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', service, 'tests'));
|
|
674
823
|
}
|
|
675
824
|
else {
|
|
825
|
+
// 前端任务:创建 20-frontend/{platform}/src/ 和 tests/
|
|
676
826
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '20-frontend', platform, 'src'));
|
|
677
827
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '20-frontend', platform, 'tests'));
|
|
678
828
|
}
|
|
679
829
|
}
|
|
680
|
-
if (!taskPlatforms.some(p => p.startsWith('后台'))) {
|
|
830
|
+
if (!taskPlatforms.some((p) => p === 'backend' || p.startsWith('后台'))) {
|
|
681
831
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', 'src'));
|
|
682
832
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', 'tests'));
|
|
683
833
|
}
|
|
@@ -688,7 +838,16 @@ ${apiDesc}
|
|
|
688
838
|
const testContent = await (0, fs_extra_1.readFile)((0, path_1.join)(taskDir, '99-artifacts', 'TEST.md'), 'utf-8');
|
|
689
839
|
const reviewContent = await (0, fs_extra_1.readFile)((0, path_1.join)(taskDir, '99-artifacts', 'REVIEW.md'), 'utf-8');
|
|
690
840
|
for (const platform of taskPlatforms) {
|
|
691
|
-
if (platform
|
|
841
|
+
if (platform === 'backend') {
|
|
842
|
+
// 纯后端任务:规格写入 10-backend/ 根目录
|
|
843
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'REQ.md'), reqContent);
|
|
844
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'TECH.md'), techContent);
|
|
845
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'TASK.md'), taskContent);
|
|
846
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'TEST.md'), testContent);
|
|
847
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'REVIEW.md'), reviewContent);
|
|
848
|
+
}
|
|
849
|
+
else if (platform.startsWith('后台')) {
|
|
850
|
+
// 后台服务任务:规格写入 10-backend/{service}/
|
|
692
851
|
const service = platform.replace(/^后台/, '').trim() || platform;
|
|
693
852
|
const svcDir = (0, path_1.join)(taskDir, '10-backend', service);
|
|
694
853
|
await (0, fs_extra_1.ensureDir)(svcDir);
|
|
@@ -699,6 +858,7 @@ ${apiDesc}
|
|
|
699
858
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(svcDir, 'REVIEW.md'), reviewContent);
|
|
700
859
|
}
|
|
701
860
|
else {
|
|
861
|
+
// 前端任务:规格写入 20-frontend/{platform}/
|
|
702
862
|
const feDir = (0, path_1.join)(taskDir, '20-frontend', platform);
|
|
703
863
|
await (0, fs_extra_1.ensureDir)(feDir);
|
|
704
864
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(feDir, 'REQ.md'), reqContent);
|
|
@@ -727,7 +887,7 @@ async function updateProjectGraph(iterationDir, sections) {
|
|
|
727
887
|
while (/端端/.test(taskName))
|
|
728
888
|
taskName = taskName.replace('端端', '端');
|
|
729
889
|
if (!content.includes(taskId)) {
|
|
730
|
-
const taskEntry = `| ${taskId} | ${taskName} | feature | 0% | 🔲 待开发 | |\n`;
|
|
890
|
+
const taskEntry = `| ${taskId} | ${taskName} | ${sections[i]._taskType || 'feature'} | 0% | 🔲 待开发 | |\n`;
|
|
731
891
|
content = content.replace('| 任务编号 | 任务名称 | 类型 | 进度 | 状态 | 负责人 |\n| :--- | :--- | :--- | :--- | :--- | :--- |\n', `| 任务编号 | 任务名称 | 类型 | 进度 | 状态 | 负责人 |\n| :--- | :--- | :--- | :--- | :--- | :--- |\n${taskEntry}`);
|
|
732
892
|
}
|
|
733
893
|
}
|
|
@@ -938,7 +1098,8 @@ async function generateImpactGraph(iterationDir, sections, platforms) {
|
|
|
938
1098
|
const taskId = s._taskId || `Task-${String(i + 1).padStart(3, '0')}`;
|
|
939
1099
|
const risk = await (0, risk_scorer_1.scoreRisk)(s.content + s.name, s.name, iterationDir);
|
|
940
1100
|
impact += `| ${taskId}: ${s.name} | ${risk.level} | ${risk.score} | ${risk.tags.join(' ')} | ${risk.reasons.join('; ')} |\n`;
|
|
941
|
-
const
|
|
1101
|
+
const taskType = s._taskType || 'feature';
|
|
1102
|
+
const taskDir = (0, path_1.join)(iterationDir, '030-tasks', taskType, taskId);
|
|
942
1103
|
if (await (0, fs_extra_1.pathExists)(taskDir)) {
|
|
943
1104
|
// 生成风险报告并嵌入 TASK.md(去重:只写一次)
|
|
944
1105
|
const taskMdPath = (0, path_1.join)(taskDir, '00-specs', 'TASK.md');
|
|
@@ -1331,9 +1492,20 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
1331
1492
|
}
|
|
1332
1493
|
p += `\n检测到 ${teamSize} 人团队,推荐粒度: ${granularityLabel}\n\n---\n\n`;
|
|
1333
1494
|
}
|
|
1334
|
-
//
|
|
1495
|
+
// 粒度说明(含硬约束)
|
|
1335
1496
|
p += `## 🎯 拆分粒度: ${granularityLabel}\n\n`;
|
|
1336
1497
|
p += `${granularityHint}\n\n`;
|
|
1498
|
+
p += `### 当前粒度硬约束(必须严格遵守)\n`;
|
|
1499
|
+
if (granularityLabel.includes('粗')) {
|
|
1500
|
+
p += `- 每任务工时: 20-80h(1-2 周)\n- 接口上限: 15 个/任务\n- 数据表上限: 5 张/任务\n- 页面上限: 5 个/任务\n`;
|
|
1501
|
+
}
|
|
1502
|
+
else if (granularityLabel.includes('中')) {
|
|
1503
|
+
p += `- 每任务工时: 12-40h(3-5 天)\n- 接口上限: 8 个/任务\n- 数据表上限: 3 张/任务\n- 页面上限: 3 个/任务\n`;
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
p += `- 每任务工时: 4-24h(1-3 天)\n- 接口上限: 3 个/任务\n- 数据表上限: 2 张/任务\n- 页面上限: 1 个/任务\n`;
|
|
1507
|
+
}
|
|
1508
|
+
p += `\n**超出上限必须再拆,低于下限必须合并。**\n\n`;
|
|
1337
1509
|
p += `用户可通过 --granularity macro|module|atomic 调整全局粒度。\n\n`;
|
|
1338
1510
|
// SpecCore 拆分原则
|
|
1339
1511
|
p += `## ⚙️ SpecCore 拆分原则\n\n`;
|
|
@@ -1346,21 +1518,28 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
1346
1518
|
p += `- execute 时不强依赖其他 Task 的运行时状态\n`;
|
|
1347
1519
|
p += `- 有明确的验收标准(AC 可枚举)\n`;
|
|
1348
1520
|
p += `- 可独立提 PR、独立 review\n\n`;
|
|
1349
|
-
p += `###
|
|
1521
|
+
p += `### 合并规则(优先合并,减少任务数)\n`;
|
|
1350
1522
|
p += `- 同一数据实体的 CRUD → 共享数据模型,合并为 1 个任务\n`;
|
|
1351
1523
|
p += `- 页面 + 对应后端接口 < 5 个 → 前后端强耦合,一人做效率最高\n`;
|
|
1352
1524
|
p += `- 纯配置/文案/样式微调 → 不构成独立工作单元\n`;
|
|
1353
|
-
p += `- 关联紧密的小功能(如列表页 + 详情页)→ 共享路由和状态\n
|
|
1525
|
+
p += `- 关联紧密的小功能(如列表页 + 详情页)→ 共享路由和状态\n`;
|
|
1526
|
+
p += `- **复杂度判断**:如果一个需求章节接口 ≤ 3、数据表 ≤ 1、预估工时 < 粒度下限 → 必须合并到最相关的任务,不单独拆\n`;
|
|
1527
|
+
p += `- **宁少勿多**:任务数越少越好,每个任务应该是真正独立的工作单元。如果两个功能共享数据模型或路由,一人就能做完,不要拆\n\n`;
|
|
1354
1528
|
p += `### 拆分规则\n`;
|
|
1355
|
-
p += `-
|
|
1356
|
-
p += `-
|
|
1357
|
-
p += `-
|
|
1529
|
+
p += `- 超出当前粒度接口上限 → 按业务领域拆\n`;
|
|
1530
|
+
p += `- 超出当前粒度数据表上限 → 按数据层拆\n`;
|
|
1531
|
+
p += `- 超出当前粒度工时上限 → 必须再拆\n`;
|
|
1532
|
+
p += `- 低于当前粒度工时下限 → 合并到关联任务\n`;
|
|
1358
1533
|
p += `- 跨端功能 → 按端拆(后端 1 个 + 每个前端各 1 个)\n`;
|
|
1359
1534
|
p += `- 独立第三方集成(支付/短信/OSS)→ 独立任务\n\n`;
|
|
1360
1535
|
p += `### 依赖关系\n`;
|
|
1361
1536
|
p += `- 基础模块(认证/数据库/配置)优先拆出,作为第一批任务\n`;
|
|
1362
1537
|
p += `- 依赖链深度 ≤ 3\n`;
|
|
1363
1538
|
p += `- 同层级无循环依赖\n\n`;
|
|
1539
|
+
p += `### 总量约束\n`;
|
|
1540
|
+
p += `- 单次迭代总任务数: 3-15 个(超出说明粒度不合适)\n`;
|
|
1541
|
+
p += `- 每个任务必须有明确的 owner(对应 STAFFING 中的成员)\n`;
|
|
1542
|
+
p += `- 高优先级任务排在前面\n\n`;
|
|
1364
1543
|
// 输出格式
|
|
1365
1544
|
p += `## 📤 输出格式\n\n`;
|
|
1366
1545
|
p += `请输出 JSON 数组,每个 Task 包含:\n`;
|
|
@@ -1382,14 +1561,15 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
1382
1561
|
p += ` }\n]\n`;
|
|
1383
1562
|
p += '```\n\n';
|
|
1384
1563
|
// 质量自检
|
|
1385
|
-
p += `## ✅
|
|
1386
|
-
p += `拆分完成后自查:\n`;
|
|
1564
|
+
p += `## ✅ 质量自检(必须全部通过)\n\n`;
|
|
1387
1565
|
p += `□ 每个任务都满足原子任务定义?\n`;
|
|
1388
|
-
p += `□
|
|
1566
|
+
p += `□ 每个任务的 estimatedHours 在当前粒度范围内?(不满足 → 合并或再拆)\n`;
|
|
1389
1567
|
p += `□ 没有循环依赖?\n`;
|
|
1390
1568
|
p += `□ 基础模块排在前面?\n`;
|
|
1391
|
-
p += `□
|
|
1392
|
-
p += `□
|
|
1569
|
+
p += `□ 同领域功能没被过度拆分?(同一数据实体的 CRUD 必须合并)\n`;
|
|
1570
|
+
p += `□ 总任务数在 3-15 个范围内?\n`;
|
|
1571
|
+
p += `□ 每个任务都有明确的 owner 和 acceptanceCriteria?\n`;
|
|
1572
|
+
p += `□ 每个任务都能独立提 PR、独立 review?\n`;
|
|
1393
1573
|
// 自动模式指令
|
|
1394
1574
|
p += `## 🤖 自动模式指令\n\n`;
|
|
1395
1575
|
p += `本拆分在自动模式下执行,请遵循以下原则:\n`;
|
|
@@ -1404,6 +1584,13 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
1404
1584
|
`;
|
|
1405
1585
|
p += `3. **遇阻断就跳过** — 如果某个功能模块信息不足无法拆分,跳过它并在疑问清单中记录\n`;
|
|
1406
1586
|
p += `4. **输出 JSON** — 直接输出拆分结果的 JSON 数组,不要输出其他内容\n`;
|
|
1587
|
+
// 持久指令(用户调整时 AI 可回读此文件)
|
|
1588
|
+
p += `\n## 🔄 调整指令(持久有效)\n\n`;
|
|
1589
|
+
p += `当用户要求调整拆分方案时(如“合并”“拆分”“改工时”“改优先级”):\n`;
|
|
1590
|
+
p += `1. **先重读本文件** — 本文件包含完整的拆分规则、粒度约束、端配置、Spec 上下文\n`;
|
|
1591
|
+
p += `2. **在同一套规则下调整** — 合并/拆分/修改都必须遵守粒度硬约束\n`;
|
|
1592
|
+
p += `3. **重新输出完整 JSON** — 不要只输出修改的部分,输出调整后的完整数组\n`;
|
|
1593
|
+
p += `4. **文件路径**: \`.speccore/prompts/split-suggestion-${iteration}.md\`\n\n`;
|
|
1407
1594
|
return p;
|
|
1408
1595
|
}
|
|
1409
1596
|
/**
|
|
@@ -1533,15 +1720,24 @@ async function detectExistingTasks(iterDir) {
|
|
|
1533
1720
|
// 优先从 030-tasks/ 扫描,兼容旧布局(迭代根目录)
|
|
1534
1721
|
const scanDir = (0, path_1.join)(iterDir, '030-tasks');
|
|
1535
1722
|
const targetDir = (await (0, fs_extra_1.pathExists)(scanDir)) ? scanDir : iterDir;
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1723
|
+
const scanRecursive = async (dir) => {
|
|
1724
|
+
try {
|
|
1725
|
+
const entries = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
|
|
1726
|
+
for (const e of entries) {
|
|
1727
|
+
if (e.isDirectory()) {
|
|
1728
|
+
if (e.name.startsWith('Task-')) {
|
|
1729
|
+
tasks.push(e.name);
|
|
1730
|
+
}
|
|
1731
|
+
else if (!e.name.startsWith('.')) {
|
|
1732
|
+
// 递归扫描类型子目录(feature/bugfix/refactor/research)
|
|
1733
|
+
await scanRecursive((0, path_1.join)(dir, e.name));
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1541
1736
|
}
|
|
1542
1737
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1738
|
+
catch { }
|
|
1739
|
+
};
|
|
1740
|
+
await scanRecursive(targetDir);
|
|
1545
1741
|
return tasks;
|
|
1546
1742
|
}
|
|
1547
1743
|
//# sourceMappingURL=split.js.map
|