speccore 5.87.2 → 5.89.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 +289 -56
- 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,56 @@ 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
|
+
/** 校验任务工时是否在粒度范围内(按单人 max 工时计算) */
|
|
32
|
+
function validateGranularity(gran, hoursByPlatform, apiCount, tableCount) {
|
|
33
|
+
const rule = GRANULARITY_RULES[gran];
|
|
34
|
+
const warnings = [];
|
|
35
|
+
const platformEntries = Object.entries(hoursByPlatform);
|
|
36
|
+
const maxPerPerson = platformEntries.length > 0 ? Math.max(...platformEntries.map(([, h]) => h)) : 0;
|
|
37
|
+
const totalHours = platformEntries.reduce((sum, [, h]) => sum + h, 0);
|
|
38
|
+
const maxPlatform = platformEntries.length > 0 ? platformEntries.reduce((a, b) => (b[1] > a[1] ? b : a))[0] : '';
|
|
39
|
+
if (maxPerPerson > rule.maxHours) {
|
|
40
|
+
warnings.push(`⚠️ 单人最大工时 ${maxPerPerson}h(${maxPlatform})超出上限 ${rule.maxHours}h → 建议再拆`);
|
|
41
|
+
}
|
|
42
|
+
else if (maxPerPerson < rule.minHours) {
|
|
43
|
+
warnings.push(`⚠️ 单人最大工时 ${maxPerPerson}h(${maxPlatform})低于下限 ${rule.minHours}h → 建议合并到关联任务`);
|
|
44
|
+
}
|
|
45
|
+
if (apiCount > rule.maxApis)
|
|
46
|
+
warnings.push(`⚠️ 接口 ${apiCount} 个超出上限 ${rule.maxApis} → 建议按业务领域拆分`);
|
|
47
|
+
if (tableCount > rule.maxTables)
|
|
48
|
+
warnings.push(`⚠️ 数据表 ${tableCount} 张超出上限 ${rule.maxTables} → 建议按数据层拆分`);
|
|
49
|
+
// 返回额外信息供展示
|
|
50
|
+
if (platformEntries.length > 1) {
|
|
51
|
+
const breakdown = platformEntries.map(([p, h]) => `${p}:${h}h`).join(' + ');
|
|
52
|
+
warnings.unshift(`ℹ️ 工时分布: ${breakdown} = ${totalHours}h(max per person: ${maxPerPerson}h)`);
|
|
53
|
+
}
|
|
54
|
+
return warnings;
|
|
55
|
+
}
|
|
56
|
+
/** 根据团队规模推荐粒度 */
|
|
57
|
+
function recommendGranularity(teamSize) {
|
|
58
|
+
if (teamSize <= 3)
|
|
59
|
+
return 'macro';
|
|
60
|
+
if (teamSize <= 8)
|
|
61
|
+
return 'module';
|
|
62
|
+
return 'atomic';
|
|
63
|
+
}
|
|
14
64
|
function promptUser(question) {
|
|
15
65
|
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
16
66
|
return new Promise(resolve => {
|
|
@@ -20,7 +70,42 @@ function promptUser(question) {
|
|
|
20
70
|
async function detectPlatforms(iterationDir, specified) {
|
|
21
71
|
if (specified)
|
|
22
72
|
return specified.split(',').map(p => p.trim()).filter(Boolean);
|
|
23
|
-
//
|
|
73
|
+
// 1. 优先从 CONSTITUTION.md 读取「对应需求端」配置
|
|
74
|
+
const constitutionPath = (0, path_1.join)('.speccore', 'CONSTITUTION.md');
|
|
75
|
+
if (await (0, fs_extra_1.pathExists)(constitutionPath)) {
|
|
76
|
+
const content = await (0, fs_extra_1.readFile)(constitutionPath, 'utf-8');
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
let headerIdx = -1;
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
if (lines[i].includes('对应需求端')) {
|
|
81
|
+
headerIdx = i;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (headerIdx >= 0) {
|
|
86
|
+
const headers = lines[headerIdx].split('|').map(h => h.trim()).filter(Boolean);
|
|
87
|
+
const platformColIdx = headers.findIndex(h => h.includes('对应需求端'));
|
|
88
|
+
if (platformColIdx >= 0) {
|
|
89
|
+
const platforms = new Set();
|
|
90
|
+
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
91
|
+
const line = lines[i].trim();
|
|
92
|
+
if (!line.startsWith('|') || line.match(/^\|\s*[-:]/))
|
|
93
|
+
continue;
|
|
94
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
95
|
+
if (cells[platformColIdx]) {
|
|
96
|
+
cells[platformColIdx].split(',').forEach((p) => {
|
|
97
|
+
const trimmed = p.trim();
|
|
98
|
+
if (trimmed && !trimmed.startsWith('>'))
|
|
99
|
+
platforms.add(trimmed);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (platforms.size > 0)
|
|
104
|
+
return [...platforms];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 2. 回退:扫描 020-specs/ 子目录
|
|
24
109
|
const specsDir = (0, path_1.join)(iterationDir, '020-specs');
|
|
25
110
|
if (await (0, fs_extra_1.pathExists)(specsDir)) {
|
|
26
111
|
const entries = await (0, fs_extra_1.readdir)(specsDir, { withFileTypes: true });
|
|
@@ -30,7 +115,7 @@ async function detectPlatforms(iterationDir, specified) {
|
|
|
30
115
|
if (platforms.length > 0)
|
|
31
116
|
return platforms;
|
|
32
117
|
}
|
|
33
|
-
return ['web']; //
|
|
118
|
+
return ['web']; // 默认
|
|
34
119
|
}
|
|
35
120
|
async function iterationSplitCommand(options) {
|
|
36
121
|
// ── Prompt 模式 ──
|
|
@@ -63,7 +148,8 @@ async function iterationSplitCommand(options) {
|
|
|
63
148
|
const task = tasks[i];
|
|
64
149
|
// 将 AI JSON 转换为 Section,复用 createTaskFromSection 创建完整目录
|
|
65
150
|
const desc = task.description || task.name || '';
|
|
66
|
-
const
|
|
151
|
+
const scopeArr = Array.isArray(task.scope) ? task.scope : [];
|
|
152
|
+
const scope = scopeArr.join(', ');
|
|
67
153
|
const apis = Array.isArray(task.apis) ? task.apis.join('\n') : '';
|
|
68
154
|
const acs = Array.isArray(task.acceptanceCriteria) ? task.acceptanceCriteria.join('\n') : '';
|
|
69
155
|
let content = desc;
|
|
@@ -73,14 +159,22 @@ async function iterationSplitCommand(options) {
|
|
|
73
159
|
content += `\n\n接口:\n${apis}`;
|
|
74
160
|
if (acs)
|
|
75
161
|
content += `\n\n验收标准:\n${acs}`;
|
|
162
|
+
// 从 scope 提取平台列表(后端 + 前端各端)
|
|
163
|
+
const taskScopePlatforms = [];
|
|
164
|
+
const isBackend = scopeArr.some((s) => /后端|backend/i.test(s));
|
|
165
|
+
const fePlatforms = scopeArr.filter((s) => !/后端|backend/i.test(s)).map((s) => s.trim()).filter(Boolean);
|
|
166
|
+
if (isBackend)
|
|
167
|
+
taskScopePlatforms.push('backend');
|
|
168
|
+
taskScopePlatforms.push(...fePlatforms);
|
|
76
169
|
const section = {
|
|
77
170
|
name: task.name || `Task ${i + 1}`,
|
|
78
171
|
content,
|
|
79
172
|
level: 2,
|
|
80
|
-
platform:
|
|
173
|
+
platform: isBackend ? 'backend' : (fePlatforms[0] || undefined),
|
|
81
174
|
};
|
|
82
175
|
section._complexity = {
|
|
83
176
|
estimatedHours: task.estimatedHours || 8,
|
|
177
|
+
hoursByPlatform: (task.hoursByPlatform && typeof task.hoursByPlatform === 'object') ? task.hoursByPlatform : {},
|
|
84
178
|
priority: task.priority || 'medium',
|
|
85
179
|
complexity: task.risk === 'high' ? 'high' : task.risk === 'low' ? 'low' : 'medium',
|
|
86
180
|
apiCount: (task.apis || []).length,
|
|
@@ -89,6 +183,9 @@ async function iterationSplitCommand(options) {
|
|
|
89
183
|
wordCount: content.length,
|
|
90
184
|
};
|
|
91
185
|
section._owner = task.owner || '未分配';
|
|
186
|
+
section._taskType = (task.type && ['feature', 'bugfix', 'refactor', 'research'].includes(task.type)) ? task.type : 'feature';
|
|
187
|
+
if (taskScopePlatforms.length > 0)
|
|
188
|
+
section._scopePlatforms = taskScopePlatforms;
|
|
92
189
|
sections.push(section);
|
|
93
190
|
}
|
|
94
191
|
// 检测已有任务 + 冲突处理
|
|
@@ -98,17 +195,92 @@ async function iterationSplitCommand(options) {
|
|
|
98
195
|
logger_1.logger.info(' 使用 --force 强制覆盖');
|
|
99
196
|
return;
|
|
100
197
|
}
|
|
101
|
-
//
|
|
102
|
-
|
|
198
|
+
// --force 清理旧任务(避免新旧叠加编号暴增)
|
|
199
|
+
if (options.force) {
|
|
200
|
+
const tasksRoot = (0, path_1.join)(iterDirFull, '030-tasks');
|
|
201
|
+
if (await (0, fs_extra_1.pathExists)(tasksRoot)) {
|
|
202
|
+
const entries = await (0, fs_extra_1.readdir)(tasksRoot, { withFileTypes: true });
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (entry.isDirectory()) {
|
|
205
|
+
await (0, fs_extra_1.remove)((0, path_1.join)(tasksRoot, entry.name));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
logger_1.logger.info(` 🗑 已清理旧任务目录`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 确定粒度
|
|
212
|
+
const staffing2 = readStaffing(iterDirFull);
|
|
213
|
+
const teamSize2 = staffing2 ? staffing2.length : 0;
|
|
214
|
+
const granularity = options.granularity || recommendGranularity(teamSize2);
|
|
215
|
+
const granRule = GRANULARITY_RULES[granularity];
|
|
216
|
+
const isInteractive = process.stdin.isTTY; // 非 TTY(管道调用)时自动确认
|
|
217
|
+
logger_1.logger.info(` 📏 粒度: ${granRule.label}${options.granularity ? ' (用户指定)' : ` (${teamSize2} 人团队自动推荐)`}`);
|
|
218
|
+
if (!isInteractive)
|
|
219
|
+
logger_1.logger.info(' ℹ️ 非交互终端,自动确认所有任务');
|
|
220
|
+
// 逐任务交互确认
|
|
221
|
+
const createdSections = [];
|
|
103
222
|
for (let i = 0; i < sections.length; i++) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
223
|
+
const sec = sections[i];
|
|
224
|
+
const complexity = sec._complexity || {};
|
|
225
|
+
const taskType = sec._taskType || 'feature';
|
|
226
|
+
const deps = (tasks[i].dependencies || []);
|
|
227
|
+
const acs = (tasks[i].acceptanceCriteria || []);
|
|
228
|
+
// 展示任务摘要
|
|
229
|
+
logger_1.logger.info(`\n ━━━━ 任务 ${i + 1}/${sections.length} ━━━━`);
|
|
230
|
+
logger_1.logger.info(` 📌 ${sec.name}`);
|
|
231
|
+
logger_1.logger.info(` 🏷 类型: ${taskType} | 🎯 优先级: ${complexity.priority || 'medium'}`);
|
|
232
|
+
// 按端展示工时分布
|
|
233
|
+
const hbp = complexity.hoursByPlatform || {};
|
|
234
|
+
const hbpEntries = Object.entries(hbp);
|
|
235
|
+
if (hbpEntries.length > 0) {
|
|
236
|
+
const breakdown = hbpEntries.map(([p, h]) => `${p}:${h}h`).join(' + ');
|
|
237
|
+
const maxPerPerson = Math.max(...hbpEntries.map(([, h]) => h));
|
|
238
|
+
logger_1.logger.info(` ⏱ 工时: ${breakdown} = ${complexity.estimatedHours}h(max per person: ${maxPerPerson}h)`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
logger_1.logger.info(` ⏱ 预估: ${complexity.estimatedHours}h`);
|
|
242
|
+
}
|
|
243
|
+
if (complexity.apiCount)
|
|
244
|
+
logger_1.logger.info(` 🔌 接口: ${complexity.apiCount} 个 | 🗄 数据表: ${complexity.dbCount || 0} 张`);
|
|
245
|
+
if (deps.length > 0)
|
|
246
|
+
logger_1.logger.info(` 🔗 依赖: ${deps.join(', ')}`);
|
|
247
|
+
if (acs.length > 0) {
|
|
248
|
+
logger_1.logger.info(` ✅ 验收标准:`);
|
|
249
|
+
for (const ac of acs.slice(0, 5))
|
|
250
|
+
logger_1.logger.info(` ${ac}`);
|
|
251
|
+
}
|
|
252
|
+
// 粒度校验(按单人 max 工时)
|
|
253
|
+
const warnings = validateGranularity(granularity, hbp, complexity.apiCount || 0, complexity.dbCount || 0);
|
|
254
|
+
if (warnings.length > 0) {
|
|
255
|
+
for (const w of warnings) {
|
|
256
|
+
if (w.startsWith('ℹ️'))
|
|
257
|
+
logger_1.logger.info(` ${w}`);
|
|
258
|
+
else
|
|
259
|
+
logger_1.logger.warn(` ${w}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// 交互确认(仅确认,调整应回到 AI 对话重新生成方案)
|
|
263
|
+
if (isInteractive) {
|
|
264
|
+
const answer = await promptUser(` 确认创建?(y/回车确认,n 调整方案):`);
|
|
265
|
+
if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
|
|
266
|
+
logger_1.logger.info(` 💡 如需调整,请告诉 AI:`);
|
|
267
|
+
logger_1.logger.info(` "把 XX 和 YY 合为一个任务" / "ZZ 任务太大,拆成两个" / "修改工时为 Xh"`);
|
|
268
|
+
logger_1.logger.info(` AI 会参考 .speccore/prompts/split-suggestion-${iter}.md 中的规则重新生成`);
|
|
269
|
+
logger_1.logger.info(` 调整后再次执行本命令即可`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const { id: taskId } = await (0, global_counters_1.nextTaskId)(sec.name, tasks[i].topic);
|
|
274
|
+
sec._taskId = taskId;
|
|
275
|
+
await createTaskFromSection(iterDirFull, taskId, sec, allPlatforms, taskType);
|
|
276
|
+
createdSections.push(sec);
|
|
277
|
+
logger_1.logger.info(` ✅ 创建: ${taskId} - [${taskType}] ${sec.name}`);
|
|
278
|
+
}
|
|
279
|
+
if (createdSections.length > 0) {
|
|
280
|
+
await generateImpactGraph(iterDirFull, createdSections, allPlatforms);
|
|
281
|
+
await updateProjectGraph(iterDirFull, createdSections);
|
|
108
282
|
}
|
|
109
|
-
|
|
110
|
-
await updateProjectGraph(iterDirFull, sections);
|
|
111
|
-
logger_1.logger.success(`✅ 创建了 ${sections.length} 个任务(完整目录结构)`);
|
|
283
|
+
logger_1.logger.success(`✅ 创建了 ${createdSections.length}/${sections.length} 个任务(${sections.length - createdSections.length} 个跳过)`);
|
|
112
284
|
}
|
|
113
285
|
else {
|
|
114
286
|
logger_1.logger.warn('AI 返回格式非数组,将作为 Markdown 写入 REQUIREMENT.md');
|
|
@@ -204,12 +376,9 @@ async function iterationSplitCommand(options) {
|
|
|
204
376
|
// 粒度推荐(基于 STAFFING 人数)
|
|
205
377
|
const staffing = readStaffing(iterationDir);
|
|
206
378
|
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';
|
|
379
|
+
const recommendedGranularity = options.granularity || recommendGranularity(teamSize);
|
|
380
|
+
const granularityLabel = GRANULARITY_RULES[recommendedGranularity].label;
|
|
381
|
+
const granularityHint = GRANULARITY_RULES[recommendedGranularity].desc;
|
|
213
382
|
// 构建完整 prompt
|
|
214
383
|
let splitPrompt = buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, staffing, teamSize, granularityLabel, granularityHint);
|
|
215
384
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(promptsDir, `split-suggestion-${iteration}.md`), splitPrompt);
|
|
@@ -275,7 +444,7 @@ async function iterationSplitCommand(options) {
|
|
|
275
444
|
}
|
|
276
445
|
for (const section of approved) {
|
|
277
446
|
const taskId = section._taskId;
|
|
278
|
-
await createTaskFromSection(iterationDir, taskId, section, platforms);
|
|
447
|
+
await createTaskFromSection(iterationDir, taskId, section, platforms, section._taskType);
|
|
279
448
|
}
|
|
280
449
|
spinner.stop(`✅ 创建了 ${approved.length} 个任务`);
|
|
281
450
|
return;
|
|
@@ -335,7 +504,7 @@ async function iterationSplitCommand(options) {
|
|
|
335
504
|
break;
|
|
336
505
|
}
|
|
337
506
|
if (resp?.toLowerCase() === 'y' || resp === '') {
|
|
338
|
-
await createTaskFromSection(iterationDir, taskId, sections[i], platforms);
|
|
507
|
+
await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType);
|
|
339
508
|
created++;
|
|
340
509
|
logger_1.logger.info(` ✅ ${taskId}`);
|
|
341
510
|
}
|
|
@@ -353,7 +522,7 @@ async function iterationSplitCommand(options) {
|
|
|
353
522
|
// Default: create all
|
|
354
523
|
for (let i = 0; i < sections.length; i++) {
|
|
355
524
|
const taskId = sections[i]._taskId;
|
|
356
|
-
await createTaskFromSection(iterationDir, taskId, sections[i], platforms);
|
|
525
|
+
await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType);
|
|
357
526
|
}
|
|
358
527
|
await generateImpactGraph(iterationDir, sections, platforms);
|
|
359
528
|
await generateEnvExample(iterationDir, sections);
|
|
@@ -365,7 +534,7 @@ async function iterationSplitCommand(options) {
|
|
|
365
534
|
// Create tasks(使用预分配的 ID)
|
|
366
535
|
for (let i = 0; i < sections.length; i++) {
|
|
367
536
|
const taskId = sections[i]._taskId;
|
|
368
|
-
await createTaskFromSection(iterationDir, taskId, sections[i], platforms);
|
|
537
|
+
await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType);
|
|
369
538
|
}
|
|
370
539
|
// ── Generate impact graph + risk scores ──
|
|
371
540
|
await generateImpactGraph(iterationDir, sections, platforms);
|
|
@@ -465,15 +634,16 @@ function filterTemplateNoise(sections) {
|
|
|
465
634
|
return true;
|
|
466
635
|
});
|
|
467
636
|
}
|
|
468
|
-
async function createTaskFromSection(iterationDir, taskId, section, allPlatforms) {
|
|
469
|
-
|
|
470
|
-
const
|
|
637
|
+
async function createTaskFromSection(iterationDir, taskId, section, allPlatforms, taskType = 'feature') {
|
|
638
|
+
// taskId 已含 slug(nextTaskId 返回 Task-NNN-slug),直接用
|
|
639
|
+
const taskDir = (0, path_1.join)(iterationDir, '030-tasks', taskType, taskId);
|
|
640
|
+
const taskPlatforms = section._scopePlatforms || (section.platform ? [section.platform] : allPlatforms);
|
|
471
641
|
const complexity = section._complexity || { estimatedHours: 2, priority: 'medium', complexity: 'medium', apiCount: 0, dbCount: 0, pageCount: 0, wordCount: 0 };
|
|
472
642
|
const owner = section._owner || '未分配';
|
|
473
643
|
const today = new Date().toISOString().split('T')[0];
|
|
474
644
|
// ── 1. 元信息目录 ──
|
|
475
645
|
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'),
|
|
646
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'type'), taskType);
|
|
477
647
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'status'), 'todo');
|
|
478
648
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'owner'), owner);
|
|
479
649
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'created-at'), today);
|
|
@@ -605,7 +775,7 @@ ${apiDesc}
|
|
|
605
775
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'TASK.md'), `# ${section.name}
|
|
606
776
|
|
|
607
777
|
## 任务信息
|
|
608
|
-
- 类型:
|
|
778
|
+
- 类型: ${taskType}
|
|
609
779
|
- 状态: 🔲 待开发
|
|
610
780
|
- 优先级: ${complexity.priority}
|
|
611
781
|
- 负责人: ${owner}
|
|
@@ -665,19 +835,26 @@ ${apiDesc}
|
|
|
665
835
|
if (adr) {
|
|
666
836
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '99-artifacts', 'ADR.md'), adr);
|
|
667
837
|
}
|
|
668
|
-
// ── 5. 实现目录 10-backend/ 20-
|
|
838
|
+
// ── 5. 实现目录 10-backend/ 20-frontend/ ──
|
|
669
839
|
for (const platform of taskPlatforms) {
|
|
670
|
-
if (platform
|
|
840
|
+
if (platform === 'backend') {
|
|
841
|
+
// 纯后端任务:直接创建 10-backend/src/ 和 10-backend/tests/
|
|
842
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', 'src'));
|
|
843
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', 'tests'));
|
|
844
|
+
}
|
|
845
|
+
else if (platform.startsWith('后台')) {
|
|
846
|
+
// 后台服务任务:创建 10-backend/{service}/src/ 和 tests/
|
|
671
847
|
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
|
|
848
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', service, 'src'));
|
|
849
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', service, 'tests'));
|
|
674
850
|
}
|
|
675
851
|
else {
|
|
852
|
+
// 前端任务:创建 20-frontend/{platform}/src/ 和 tests/
|
|
676
853
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '20-frontend', platform, 'src'));
|
|
677
854
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '20-frontend', platform, 'tests'));
|
|
678
855
|
}
|
|
679
856
|
}
|
|
680
|
-
if (!taskPlatforms.some(p => p.startsWith('后台'))) {
|
|
857
|
+
if (!taskPlatforms.some((p) => p === 'backend' || p.startsWith('后台'))) {
|
|
681
858
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', 'src'));
|
|
682
859
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '10-backend', 'tests'));
|
|
683
860
|
}
|
|
@@ -688,7 +865,16 @@ ${apiDesc}
|
|
|
688
865
|
const testContent = await (0, fs_extra_1.readFile)((0, path_1.join)(taskDir, '99-artifacts', 'TEST.md'), 'utf-8');
|
|
689
866
|
const reviewContent = await (0, fs_extra_1.readFile)((0, path_1.join)(taskDir, '99-artifacts', 'REVIEW.md'), 'utf-8');
|
|
690
867
|
for (const platform of taskPlatforms) {
|
|
691
|
-
if (platform
|
|
868
|
+
if (platform === 'backend') {
|
|
869
|
+
// 纯后端任务:规格写入 10-backend/ 根目录
|
|
870
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'REQ.md'), reqContent);
|
|
871
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'TECH.md'), techContent);
|
|
872
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'TASK.md'), taskContent);
|
|
873
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'TEST.md'), testContent);
|
|
874
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '10-backend', 'REVIEW.md'), reviewContent);
|
|
875
|
+
}
|
|
876
|
+
else if (platform.startsWith('后台')) {
|
|
877
|
+
// 后台服务任务:规格写入 10-backend/{service}/
|
|
692
878
|
const service = platform.replace(/^后台/, '').trim() || platform;
|
|
693
879
|
const svcDir = (0, path_1.join)(taskDir, '10-backend', service);
|
|
694
880
|
await (0, fs_extra_1.ensureDir)(svcDir);
|
|
@@ -699,6 +885,7 @@ ${apiDesc}
|
|
|
699
885
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(svcDir, 'REVIEW.md'), reviewContent);
|
|
700
886
|
}
|
|
701
887
|
else {
|
|
888
|
+
// 前端任务:规格写入 20-frontend/{platform}/
|
|
702
889
|
const feDir = (0, path_1.join)(taskDir, '20-frontend', platform);
|
|
703
890
|
await (0, fs_extra_1.ensureDir)(feDir);
|
|
704
891
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(feDir, 'REQ.md'), reqContent);
|
|
@@ -727,7 +914,7 @@ async function updateProjectGraph(iterationDir, sections) {
|
|
|
727
914
|
while (/端端/.test(taskName))
|
|
728
915
|
taskName = taskName.replace('端端', '端');
|
|
729
916
|
if (!content.includes(taskId)) {
|
|
730
|
-
const taskEntry = `| ${taskId} | ${taskName} | feature | 0% | 🔲 待开发 | |\n`;
|
|
917
|
+
const taskEntry = `| ${taskId} | ${taskName} | ${sections[i]._taskType || 'feature'} | 0% | 🔲 待开发 | |\n`;
|
|
731
918
|
content = content.replace('| 任务编号 | 任务名称 | 类型 | 进度 | 状态 | 负责人 |\n| :--- | :--- | :--- | :--- | :--- | :--- |\n', `| 任务编号 | 任务名称 | 类型 | 进度 | 状态 | 负责人 |\n| :--- | :--- | :--- | :--- | :--- | :--- |\n${taskEntry}`);
|
|
732
919
|
}
|
|
733
920
|
}
|
|
@@ -938,7 +1125,8 @@ async function generateImpactGraph(iterationDir, sections, platforms) {
|
|
|
938
1125
|
const taskId = s._taskId || `Task-${String(i + 1).padStart(3, '0')}`;
|
|
939
1126
|
const risk = await (0, risk_scorer_1.scoreRisk)(s.content + s.name, s.name, iterationDir);
|
|
940
1127
|
impact += `| ${taskId}: ${s.name} | ${risk.level} | ${risk.score} | ${risk.tags.join(' ')} | ${risk.reasons.join('; ')} |\n`;
|
|
941
|
-
const
|
|
1128
|
+
const taskType = s._taskType || 'feature';
|
|
1129
|
+
const taskDir = (0, path_1.join)(iterationDir, '030-tasks', taskType, taskId);
|
|
942
1130
|
if (await (0, fs_extra_1.pathExists)(taskDir)) {
|
|
943
1131
|
// 生成风险报告并嵌入 TASK.md(去重:只写一次)
|
|
944
1132
|
const taskMdPath = (0, path_1.join)(taskDir, '00-specs', 'TASK.md');
|
|
@@ -1331,9 +1519,21 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
1331
1519
|
}
|
|
1332
1520
|
p += `\n检测到 ${teamSize} 人团队,推荐粒度: ${granularityLabel}\n\n---\n\n`;
|
|
1333
1521
|
}
|
|
1334
|
-
//
|
|
1522
|
+
// 粒度说明(含硬约束)
|
|
1335
1523
|
p += `## 🎯 拆分粒度: ${granularityLabel}\n\n`;
|
|
1336
1524
|
p += `${granularityHint}\n\n`;
|
|
1525
|
+
p += `### 当前粒度硬约束(必须严格遵守)\n`;
|
|
1526
|
+
p += `> ⚠️ 工时约束按 **max(各端工时)** 计算,即单个开发人员的实际工作量,不是所有端的总和\n\n`;
|
|
1527
|
+
if (granularityLabel.includes('粗')) {
|
|
1528
|
+
p += `- 每人工时: 20-80h(1-2 周)\n- 接口上限: 15 个/任务\n- 数据表上限: 5 张/任务\n- 页面上限: 5 个/任务\n`;
|
|
1529
|
+
}
|
|
1530
|
+
else if (granularityLabel.includes('中')) {
|
|
1531
|
+
p += `- 每人工时: 12-40h(3-5 天)\n- 接口上限: 8 个/任务\n- 数据表上限: 3 张/任务\n- 页面上限: 3 个/任务\n`;
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
p += `- 每人工时: 4-24h(1-3 天)\n- 接口上限: 3 个/任务\n- 数据表上限: 2 张/任务\n- 页面上限: 1 个/任务\n`;
|
|
1535
|
+
}
|
|
1536
|
+
p += `\n**超出上限必须再拆,低于下限必须合并。**\n\n`;
|
|
1337
1537
|
p += `用户可通过 --granularity macro|module|atomic 调整全局粒度。\n\n`;
|
|
1338
1538
|
// SpecCore 拆分原则
|
|
1339
1539
|
p += `## ⚙️ SpecCore 拆分原则\n\n`;
|
|
@@ -1346,34 +1546,43 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
1346
1546
|
p += `- execute 时不强依赖其他 Task 的运行时状态\n`;
|
|
1347
1547
|
p += `- 有明确的验收标准(AC 可枚举)\n`;
|
|
1348
1548
|
p += `- 可独立提 PR、独立 review\n\n`;
|
|
1349
|
-
p += `###
|
|
1549
|
+
p += `### 合并规则(优先合并,减少任务数)\n`;
|
|
1350
1550
|
p += `- 同一数据实体的 CRUD → 共享数据模型,合并为 1 个任务\n`;
|
|
1351
1551
|
p += `- 页面 + 对应后端接口 < 5 个 → 前后端强耦合,一人做效率最高\n`;
|
|
1352
1552
|
p += `- 纯配置/文案/样式微调 → 不构成独立工作单元\n`;
|
|
1353
|
-
p += `- 关联紧密的小功能(如列表页 + 详情页)→ 共享路由和状态\n
|
|
1553
|
+
p += `- 关联紧密的小功能(如列表页 + 详情页)→ 共享路由和状态\n`;
|
|
1554
|
+
p += `- **复杂度判断**:如果一个需求章节接口 ≤ 3、数据表 ≤ 1、预估工时 < 粒度下限 → 必须合并到最相关的任务,不单独拆\n`;
|
|
1555
|
+
p += `- **宁少勿多**:任务数越少越好,每个任务应该是真正独立的工作单元。如果两个功能共享数据模型或路由,一人就能做完,不要拆\n\n`;
|
|
1354
1556
|
p += `### 拆分规则\n`;
|
|
1355
|
-
p += `-
|
|
1356
|
-
p += `-
|
|
1357
|
-
p += `-
|
|
1557
|
+
p += `- 超出当前粒度接口上限 → 按业务领域拆\n`;
|
|
1558
|
+
p += `- 超出当前粒度数据表上限 → 按数据层拆\n`;
|
|
1559
|
+
p += `- 超出当前粒度工时上限 → 必须再拆\n`;
|
|
1560
|
+
p += `- 低于当前粒度工时下限 → 合并到关联任务\n`;
|
|
1358
1561
|
p += `- 跨端功能 → 按端拆(后端 1 个 + 每个前端各 1 个)\n`;
|
|
1359
1562
|
p += `- 独立第三方集成(支付/短信/OSS)→ 独立任务\n\n`;
|
|
1360
1563
|
p += `### 依赖关系\n`;
|
|
1361
1564
|
p += `- 基础模块(认证/数据库/配置)优先拆出,作为第一批任务\n`;
|
|
1362
1565
|
p += `- 依赖链深度 ≤ 3\n`;
|
|
1363
1566
|
p += `- 同层级无循环依赖\n\n`;
|
|
1567
|
+
p += `### 总量约束\n`;
|
|
1568
|
+
p += `- 单次迭代总任务数: 3-15 个(超出说明粒度不合适)\n`;
|
|
1569
|
+
p += `- 每个任务必须有明确的 owner(对应 STAFFING 中的成员)\n`;
|
|
1570
|
+
p += `- 高优先级任务排在前面\n\n`;
|
|
1364
1571
|
// 输出格式
|
|
1365
1572
|
p += `## 📤 输出格式\n\n`;
|
|
1366
1573
|
p += `请输出 JSON 数组,每个 Task 包含:\n`;
|
|
1367
1574
|
p += '```json\n';
|
|
1368
1575
|
p += `[\n {\n`;
|
|
1369
1576
|
p += ` "id": "Task-001",\n`;
|
|
1370
|
-
p += ` "name": "
|
|
1577
|
+
p += ` "name": "任务名称(中文)",\n`;
|
|
1578
|
+
p += ` "topic": "english-slug-for-directory",\n`;
|
|
1371
1579
|
p += ` "type": "feature|bugfix|refactor|research",\n`;
|
|
1372
1580
|
p += ` "reason": "为什么这样拆分",\n`;
|
|
1373
1581
|
p += ` "scope": ["后端", "admin"],\n`;
|
|
1374
1582
|
p += ` "apis": ["POST /api/auth/login"],\n`;
|
|
1375
1583
|
p += ` "tables": ["users"],\n`;
|
|
1376
|
-
p += ` "
|
|
1584
|
+
p += ` "hoursByPlatform": { "后端": 8, "admin": 8 },\n`;
|
|
1585
|
+
p += ` "estimatedHours": 16,\n`;
|
|
1377
1586
|
p += ` "priority": "high|medium|low",\n`;
|
|
1378
1587
|
p += ` "dependencies": [],\n`;
|
|
1379
1588
|
p += ` "acceptanceCriteria": ["AC1: ..."],\n`;
|
|
@@ -1381,15 +1590,23 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
1381
1590
|
p += ` "owner": "建议负责人"\n`;
|
|
1382
1591
|
p += ` }\n]\n`;
|
|
1383
1592
|
p += '```\n\n';
|
|
1593
|
+
p += `> **topic** 必须是英文短横线格式(如 \`user-authentication\`、\`product-crud\`),用于生成任务目录名 Task-NNN-{topic}\n\n`;
|
|
1594
|
+
p += `### ⚠️ 工时估算规则(重要)\n\n`;
|
|
1595
|
+
p += `- **hoursByPlatform**: 按端分别估算工时,key 对应 scope 中的端名称\n`;
|
|
1596
|
+
p += `- **estimatedHours**: 各端工时总和(仅用于展示,不参与粒度校验)\n`;
|
|
1597
|
+
p += `- **粒度校验用 max(各端工时)**:衡量「一个开发人员实际干多少」,不是总和\n`;
|
|
1598
|
+
p += `- 例:后端 8h + admin 8h = total 16h,但 per-person max = 8h,按 8h 判断粒度\n`;
|
|
1599
|
+
p += `- 同一功能的前后端各端工作必须在一个原子任务里,不要按端拆分任务\n\n`;
|
|
1384
1600
|
// 质量自检
|
|
1385
|
-
p += `## ✅
|
|
1386
|
-
p += `拆分完成后自查:\n`;
|
|
1601
|
+
p += `## ✅ 质量自检(必须全部通过)\n\n`;
|
|
1387
1602
|
p += `□ 每个任务都满足原子任务定义?\n`;
|
|
1388
|
-
p += `□
|
|
1603
|
+
p += `□ 每个任务的 estimatedHours 在当前粒度范围内?(不满足 → 合并或再拆)\n`;
|
|
1389
1604
|
p += `□ 没有循环依赖?\n`;
|
|
1390
1605
|
p += `□ 基础模块排在前面?\n`;
|
|
1391
|
-
p += `□
|
|
1392
|
-
p += `□
|
|
1606
|
+
p += `□ 同领域功能没被过度拆分?(同一数据实体的 CRUD 必须合并)\n`;
|
|
1607
|
+
p += `□ 总任务数在 3-15 个范围内?\n`;
|
|
1608
|
+
p += `□ 每个任务都有明确的 owner 和 acceptanceCriteria?\n`;
|
|
1609
|
+
p += `□ 每个任务都能独立提 PR、独立 review?\n`;
|
|
1393
1610
|
// 自动模式指令
|
|
1394
1611
|
p += `## 🤖 自动模式指令\n\n`;
|
|
1395
1612
|
p += `本拆分在自动模式下执行,请遵循以下原则:\n`;
|
|
@@ -1404,6 +1621,13 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
1404
1621
|
`;
|
|
1405
1622
|
p += `3. **遇阻断就跳过** — 如果某个功能模块信息不足无法拆分,跳过它并在疑问清单中记录\n`;
|
|
1406
1623
|
p += `4. **输出 JSON** — 直接输出拆分结果的 JSON 数组,不要输出其他内容\n`;
|
|
1624
|
+
// 持久指令(用户调整时 AI 可回读此文件)
|
|
1625
|
+
p += `\n## 🔄 调整指令(持久有效)\n\n`;
|
|
1626
|
+
p += `当用户要求调整拆分方案时(如“合并”“拆分”“改工时”“改优先级”):\n`;
|
|
1627
|
+
p += `1. **先重读本文件** — 本文件包含完整的拆分规则、粒度约束、端配置、Spec 上下文\n`;
|
|
1628
|
+
p += `2. **在同一套规则下调整** — 合并/拆分/修改都必须遵守粒度硬约束\n`;
|
|
1629
|
+
p += `3. **重新输出完整 JSON** — 不要只输出修改的部分,输出调整后的完整数组\n`;
|
|
1630
|
+
p += `4. **文件路径**: \`.speccore/prompts/split-suggestion-${iteration}.md\`\n\n`;
|
|
1407
1631
|
return p;
|
|
1408
1632
|
}
|
|
1409
1633
|
/**
|
|
@@ -1533,15 +1757,24 @@ async function detectExistingTasks(iterDir) {
|
|
|
1533
1757
|
// 优先从 030-tasks/ 扫描,兼容旧布局(迭代根目录)
|
|
1534
1758
|
const scanDir = (0, path_1.join)(iterDir, '030-tasks');
|
|
1535
1759
|
const targetDir = (await (0, fs_extra_1.pathExists)(scanDir)) ? scanDir : iterDir;
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1760
|
+
const scanRecursive = async (dir) => {
|
|
1761
|
+
try {
|
|
1762
|
+
const entries = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
|
|
1763
|
+
for (const e of entries) {
|
|
1764
|
+
if (e.isDirectory()) {
|
|
1765
|
+
if (e.name.startsWith('Task-')) {
|
|
1766
|
+
tasks.push(e.name);
|
|
1767
|
+
}
|
|
1768
|
+
else if (!e.name.startsWith('.')) {
|
|
1769
|
+
// 递归扫描类型子目录(feature/bugfix/refactor/research)
|
|
1770
|
+
await scanRecursive((0, path_1.join)(dir, e.name));
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1541
1773
|
}
|
|
1542
1774
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1775
|
+
catch { }
|
|
1776
|
+
};
|
|
1777
|
+
await scanRecursive(targetDir);
|
|
1545
1778
|
return tasks;
|
|
1546
1779
|
}
|
|
1547
1780
|
//# sourceMappingURL=split.js.map
|