speccore 6.69.0 → 6.71.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +1 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +686 -97
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +262 -0
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +25 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/execute.d.ts +1 -0
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +137 -0
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/iteration/split.d.ts +2 -0
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +414 -56
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/core/change-detection.d.ts +121 -0
- package/dist/core/change-detection.d.ts.map +1 -0
- package/dist/core/change-detection.js +459 -0
- package/dist/core/change-detection.js.map +1 -0
- package/dist/core/knowledge-graph.d.ts +26 -0
- package/dist/core/knowledge-graph.d.ts.map +1 -1
- package/dist/core/knowledge-graph.js +113 -0
- package/dist/core/knowledge-graph.js.map +1 -1
- package/dist/core/pipeline-engine.d.ts +103 -0
- package/dist/core/pipeline-engine.d.ts.map +1 -0
- package/dist/core/pipeline-engine.js +436 -0
- package/dist/core/pipeline-engine.js.map +1 -0
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +18 -3
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/spec-paths.d.ts.map +1 -1
- package/dist/core/spec-paths.js +1 -0
- package/dist/core/spec-paths.js.map +1 -1
- package/package.json +1 -1
|
@@ -47,6 +47,75 @@ const prompt_builder_1 = require("../../core/prompt-builder");
|
|
|
47
47
|
const index_guard_1 = require("../../core/index-guard");
|
|
48
48
|
const spec_paths_1 = require("../../core/spec-paths");
|
|
49
49
|
const questions_1 = require("../../core/questions");
|
|
50
|
+
const pipeline_engine_1 = require("../../core/pipeline-engine");
|
|
51
|
+
/**
|
|
52
|
+
* 将 AI 返回的 scope 简写映射到 CONSTITUTION.md 标准端名
|
|
53
|
+
* v6.69.3+: 解决中文简写(如 "后端"、"admin")导致目录名错误的问题
|
|
54
|
+
*/
|
|
55
|
+
function normalizeScopePlatforms(scopeArr, standardPlatforms) {
|
|
56
|
+
const result = [];
|
|
57
|
+
const used = new Set();
|
|
58
|
+
for (const raw of scopeArr) {
|
|
59
|
+
const s = raw.trim();
|
|
60
|
+
if (!s)
|
|
61
|
+
continue;
|
|
62
|
+
// 1. 如果已经是标准端名,直接使用
|
|
63
|
+
if (standardPlatforms.includes(s)) {
|
|
64
|
+
if (!used.has(s)) {
|
|
65
|
+
result.push(s);
|
|
66
|
+
used.add(s);
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// 2. 后端类简写 → 匹配第一个后端端名
|
|
71
|
+
if (/后端|backend|服务端|server/i.test(s)) {
|
|
72
|
+
const backendPlatform = standardPlatforms.find(p => /-(service|api|server|backend)$/i.test(p) || p.startsWith('后台'));
|
|
73
|
+
if (backendPlatform && !used.has(backendPlatform)) {
|
|
74
|
+
result.push(backendPlatform);
|
|
75
|
+
used.add(backendPlatform);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// 3. 前端类简写 → 模糊匹配标准端名
|
|
80
|
+
const lower = s.toLowerCase();
|
|
81
|
+
const matched = standardPlatforms.find(p => {
|
|
82
|
+
const pl = p.toLowerCase();
|
|
83
|
+
// 直接包含关系:admin-web 包含 admin
|
|
84
|
+
return pl.includes(lower) || lower.includes(pl);
|
|
85
|
+
});
|
|
86
|
+
if (matched && !used.has(matched)) {
|
|
87
|
+
result.push(matched);
|
|
88
|
+
used.add(matched);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// 4. 关键词匹配
|
|
92
|
+
const keywordMap = {
|
|
93
|
+
'h5': ['h5-mobile', 'h5', 'mobile', '移动端'],
|
|
94
|
+
'mobile': ['h5-mobile', 'h5', 'mobile', '移动端'],
|
|
95
|
+
'小程序': ['miniapp', 'mini-app', '小程序'],
|
|
96
|
+
'admin': ['admin-web', 'admin', '后台管理'],
|
|
97
|
+
'web': ['admin-web', 'web', 'h5-mobile'],
|
|
98
|
+
'app': ['app', 'android', 'ios'],
|
|
99
|
+
'安卓': ['android'],
|
|
100
|
+
'ios': ['ios'],
|
|
101
|
+
};
|
|
102
|
+
const candidates = keywordMap[lower];
|
|
103
|
+
if (candidates) {
|
|
104
|
+
const found = standardPlatforms.find(p => candidates.some(c => p.toLowerCase().includes(c.toLowerCase())));
|
|
105
|
+
if (found && !used.has(found)) {
|
|
106
|
+
result.push(found);
|
|
107
|
+
used.add(found);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 5. 无法匹配时保留原始值(后续可能创建非法目录,但至少不丢失信息)
|
|
112
|
+
if (!used.has(s)) {
|
|
113
|
+
result.push(s);
|
|
114
|
+
used.add(s);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result.length > 0 ? result : standardPlatforms;
|
|
118
|
+
}
|
|
50
119
|
/** 将名称转为目录安全的短 slug(2-4 词) */
|
|
51
120
|
function slugify(name) {
|
|
52
121
|
const cleaned = name
|
|
@@ -139,6 +208,62 @@ async function detectPlatforms(iterationDir, specified) {
|
|
|
139
208
|
return ['web']; // 默认
|
|
140
209
|
}
|
|
141
210
|
async function iterationSplitCommand(options) {
|
|
211
|
+
// ── Pipeline 模式 ──
|
|
212
|
+
if (options.pipeline) {
|
|
213
|
+
const iter = options.iteration || await (0, context_1.getDefaultIteration)() || '';
|
|
214
|
+
if (!iter) {
|
|
215
|
+
logger_1.logger.error('--pipeline 需要 --iteration');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// 检查是否为恢复模式
|
|
219
|
+
if (options.resume) {
|
|
220
|
+
const hasPipeline = await pipeline_engine_1.PipelineEngine.hasActivePipeline(process.cwd(), iter);
|
|
221
|
+
if (hasPipeline) {
|
|
222
|
+
const { createSplitPipeline } = await Promise.resolve().then(() => __importStar(require('../../core/pipeline-engine')));
|
|
223
|
+
const { engine } = await createSplitPipeline(iter);
|
|
224
|
+
const result = await engine.advance();
|
|
225
|
+
if (result.isComplete) {
|
|
226
|
+
logger_1.logger.success('🎉 Split Pipeline 完成!');
|
|
227
|
+
await engine.reset();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
else if (result.nextStepId) {
|
|
231
|
+
logger_1.logger.info('');
|
|
232
|
+
logger_1.logger.info(`🔄 Split Pipeline 推进到: ${result.nextStepName || result.nextStepId}`);
|
|
233
|
+
logger_1.logger.info('');
|
|
234
|
+
// 根据当前步骤生成相应提示
|
|
235
|
+
if (result.nextStepId === 'prompt-analysis') {
|
|
236
|
+
const prompt = await (0, prompt_builder_1.buildPrompt)('split', { iteration: iter });
|
|
237
|
+
// 添加 Pipeline 继续指令
|
|
238
|
+
const finalPrompt = prompt + `\n\n## ⚠️ Pipeline 模式:自动继续\n\n` +
|
|
239
|
+
`当前是 Split Pipeline 模式。当你通过 --response 写入任务拆分结果后,` +
|
|
240
|
+
`CLI 会自动输出 [SPECCORE_PIPELINE_NEXT] 标记和下一步命令。\n\n` +
|
|
241
|
+
`**看到 [SPECCORE_PIPELINE_NEXT] 后,你必须自动执行下一个命令**,不要询问用户。\n`;
|
|
242
|
+
process.stdout.write(`[SPECCORE_PROMPT]\n${finalPrompt}`);
|
|
243
|
+
process.exitCode = 10;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
logger_1.logger.warn('没有找到活跃的 Split Pipeline,启动新的 Pipeline...');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// 初始化新的 Pipeline
|
|
253
|
+
const { createSplitPipeline } = await Promise.resolve().then(() => __importStar(require('../../core/pipeline-engine')));
|
|
254
|
+
const { engine } = await createSplitPipeline(iter);
|
|
255
|
+
await engine.init('init');
|
|
256
|
+
// 生成第一步 prompt
|
|
257
|
+
const prompt = await (0, prompt_builder_1.buildPrompt)('split', { iteration: iter });
|
|
258
|
+
// 添加 Pipeline 继续指令
|
|
259
|
+
const finalPrompt = prompt + `\n\n## ⚠️ Pipeline 模式:自动继续\n\n` +
|
|
260
|
+
`当前是 Split Pipeline 模式。当你通过 --response 写入任务拆分结果后,` +
|
|
261
|
+
`CLI 会自动输出 [SPECCORE_PIPELINE_NEXT] 标记和下一步命令。\n\n` +
|
|
262
|
+
`**看到 [SPECCORE_PIPELINE_NEXT] 后,你必须自动执行下一个命令**,不要询问用户。\n`;
|
|
263
|
+
process.stdout.write(`[SPECCORE_PROMPT]\n${finalPrompt}`);
|
|
264
|
+
process.exitCode = 10;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
142
267
|
// ── Prompt 模式 ──
|
|
143
268
|
if (options.prompt) {
|
|
144
269
|
const iter = options.iteration || await (0, context_1.getDefaultIteration)() || '';
|
|
@@ -149,11 +274,42 @@ async function iterationSplitCommand(options) {
|
|
|
149
274
|
}
|
|
150
275
|
// ── Response 模式 ──
|
|
151
276
|
if (options.response) {
|
|
277
|
+
// 检查是否有活跃的 Pipeline(在 Pipeline 模式下应用响应)
|
|
152
278
|
const iter = options.iteration || await (0, context_1.getDefaultIteration)() || '';
|
|
153
279
|
if (!iter) {
|
|
154
280
|
logger_1.logger.error('--response 需要 --iteration');
|
|
155
281
|
return;
|
|
156
282
|
}
|
|
283
|
+
const hasPipeline = await pipeline_engine_1.PipelineEngine.hasActivePipeline(process.cwd(), iter);
|
|
284
|
+
if (hasPipeline) {
|
|
285
|
+
const { createSplitPipeline } = await Promise.resolve().then(() => __importStar(require('../../core/pipeline-engine')));
|
|
286
|
+
const { engine } = await createSplitPipeline(iter);
|
|
287
|
+
await engine.advance();
|
|
288
|
+
const state = await engine.getState();
|
|
289
|
+
if (state?.currentStep === 'done') {
|
|
290
|
+
logger_1.logger.success('🎉 Split Pipeline 完成!');
|
|
291
|
+
await engine.reset();
|
|
292
|
+
}
|
|
293
|
+
else if (state?.currentStep) {
|
|
294
|
+
logger_1.logger.info('');
|
|
295
|
+
logger_1.logger.info(`🔄 Split Pipeline 推进: ${state.currentStep}`);
|
|
296
|
+
logger_1.logger.info('');
|
|
297
|
+
// 生成下一步 prompt
|
|
298
|
+
let nextPrompt;
|
|
299
|
+
if (state.currentStep === 'creation') {
|
|
300
|
+
// 在创建阶段,我们已完成拆分,可以生成创建任务的提示
|
|
301
|
+
nextPrompt = `任务拆分已完成,正在创建任务目录结构...\n\n请继续执行后续操作。`;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const promptResult = await (0, prompt_builder_1.buildPrompt)('split', { iteration: iter });
|
|
305
|
+
nextPrompt = typeof promptResult === 'string' ? promptResult : JSON.stringify(promptResult);
|
|
306
|
+
}
|
|
307
|
+
process.stdout.write(`[SPECCORE_PIPELINE_NEXT]\n${nextPrompt}`);
|
|
308
|
+
process.exitCode = 10;
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// 非 Pipeline 模式的 Response 处理
|
|
157
313
|
const iterDir = (0, path_1.join)('Iteration-' + iter, '030-tasks');
|
|
158
314
|
await (0, fs_extra_1.ensureDir)(iterDir);
|
|
159
315
|
const backups = [];
|
|
@@ -193,18 +349,13 @@ async function iterationSplitCommand(options) {
|
|
|
193
349
|
content += `\n\n接口:\n${apis}`;
|
|
194
350
|
if (acs)
|
|
195
351
|
content += `\n\n验收标准:\n${acs}`;
|
|
196
|
-
//
|
|
197
|
-
const taskScopePlatforms =
|
|
198
|
-
const isBackend = scopeArr.some((s) => /后端|backend/i.test(s));
|
|
199
|
-
const fePlatforms = scopeArr.filter((s) => !/后端|backend/i.test(s)).map((s) => s.trim()).filter(Boolean);
|
|
200
|
-
if (isBackend)
|
|
201
|
-
taskScopePlatforms.push('backend');
|
|
202
|
-
taskScopePlatforms.push(...fePlatforms);
|
|
352
|
+
// v6.69.3+: 将 AI 返回的 scope 简写映射到标准端名
|
|
353
|
+
const taskScopePlatforms = normalizeScopePlatforms(scopeArr, allPlatforms);
|
|
203
354
|
const section = {
|
|
204
355
|
name: task.name || `Task ${i + 1}`,
|
|
205
356
|
content,
|
|
206
357
|
level: 2,
|
|
207
|
-
platform:
|
|
358
|
+
platform: taskScopePlatforms[0] || undefined,
|
|
208
359
|
};
|
|
209
360
|
section._complexity = {
|
|
210
361
|
estimatedHours: task.estimatedHours || 8,
|
|
@@ -603,8 +754,10 @@ async function iterationSplitCommand(options) {
|
|
|
603
754
|
const recommendedGranularity = options.granularity || recommendGranularity(teamSize);
|
|
604
755
|
const granularityLabel = GRANULARITY_RULES[recommendedGranularity].label;
|
|
605
756
|
const granularityHint = GRANULARITY_RULES[recommendedGranularity].desc;
|
|
606
|
-
//
|
|
607
|
-
|
|
757
|
+
// 获取标准端名列表(用于 prompt 注入和 scope 规范化)
|
|
758
|
+
const allPlatforms = await detectPlatforms(iterationDir);
|
|
759
|
+
// 构建完整 prompt(v6.69.3+: 传入标准端名列表,确保 AI 使用正确的端名)
|
|
760
|
+
let splitPrompt = buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, staffing, teamSize, granularityLabel, granularityHint, allPlatforms);
|
|
608
761
|
// 注入全局上下文(INDEX + TOC 目录,AI 自主读取)
|
|
609
762
|
const { loadGlobalContext, formatGlobalContext } = await Promise.resolve().then(() => __importStar(require('../../core/prompt-builder')));
|
|
610
763
|
const globalCtx = await loadGlobalContext(process.cwd(), 'split');
|
|
@@ -1050,9 +1203,18 @@ ${taskPlatforms.map((p) => `| ${subtaskIdMap.get(p)} | ${p} | ${owner} | 🔲
|
|
|
1050
1203
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'created-at'), today);
|
|
1051
1204
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '_shared'));
|
|
1052
1205
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '00-specs'));
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1206
|
+
// v6.70.0+: 优先复制全局 API_CONTRACT.yaml(全局契约是单一真相源)
|
|
1207
|
+
const globalContractPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'API_CONTRACT.yaml');
|
|
1208
|
+
if (await (0, fs_extra_1.pathExists)(globalContractPath)) {
|
|
1209
|
+
const globalContract = await (0, fs_extra_1.readFile)(globalContractPath, 'utf-8');
|
|
1210
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml'), globalContract);
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
// 回退:从 section content 提取 API 生成任务级契约
|
|
1214
|
+
const contractYaml = generateApiContract(section);
|
|
1215
|
+
if (contractYaml) {
|
|
1216
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml'), contractYaml);
|
|
1217
|
+
}
|
|
1056
1218
|
}
|
|
1057
1219
|
// ── 4. 核心规格写入 00-specs/(REQ/TECH/SCHEMA/CHANGELOG) ──
|
|
1058
1220
|
const acItems = generateAcceptanceCriteria(section);
|
|
@@ -1226,6 +1388,20 @@ ${section.content}
|
|
|
1226
1388
|
# 自动拉取: 继承迭代配置
|
|
1227
1389
|
# 远程名称: 继承迭代配置
|
|
1228
1390
|
`);
|
|
1391
|
+
// v6.70.0+: 从 section 提取接口/页面清单用于 TASK.md
|
|
1392
|
+
const apiLines = section.content.split('\n').filter(l => l.includes('| GET') || l.includes('| POST') || l.includes('| PUT') || l.includes('| DELETE') || l.includes('| PATCH'));
|
|
1393
|
+
const apiList = apiLines.length > 0
|
|
1394
|
+
? apiLines.map(l => `- ${l.trim()}`).join('\n')
|
|
1395
|
+
: '- 待补充(从 00-specs/REQ.md 和 TECH.md 提取)';
|
|
1396
|
+
// 前端页面清单(如果是前端端)
|
|
1397
|
+
const pageLines = !isBk ? section.content.split('\n').filter(l => /页面[::]|路由[::]|path[::]|\/\w+/.test(l)) : [];
|
|
1398
|
+
const pageList = pageLines.length > 0
|
|
1399
|
+
? pageLines.map(l => `- ${l.trim()}`).join('\n')
|
|
1400
|
+
: '- 待补充(从 00-specs/REQ.md 和 TECH.md 提取)';
|
|
1401
|
+
// 依赖信息
|
|
1402
|
+
const dependsOn = (section._dependsOn || []).join(', ') || '无';
|
|
1403
|
+
const sharedCap = section._sharedCapability || '无';
|
|
1404
|
+
const crossDesc = section._description || '—';
|
|
1229
1405
|
// TASK.md
|
|
1230
1406
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'TASK.md'), `# ${section.name} — ${platformLabel}
|
|
1231
1407
|
|
|
@@ -1241,6 +1417,38 @@ ${section.content}
|
|
|
1241
1417
|
## 共享规格引用
|
|
1242
1418
|
- REQ.md → ../../../00-specs/REQ.md
|
|
1243
1419
|
- TECH.md → ../../../00-specs/TECH.md
|
|
1420
|
+
- API_CONTRACT.yaml → ../../../_shared/API_CONTRACT.yaml
|
|
1421
|
+
- CONTEXT.md → ../../../_shared/CONTEXT.md
|
|
1422
|
+
|
|
1423
|
+
## 跨端关联
|
|
1424
|
+
- **共享能力**: ${sharedCap}
|
|
1425
|
+
- **依赖任务**: ${dependsOn}
|
|
1426
|
+
- **跨端说明**: ${crossDesc}
|
|
1427
|
+
|
|
1428
|
+
## 工作清单
|
|
1429
|
+
|
|
1430
|
+
### 第一阶段:需求确认
|
|
1431
|
+
- [ ] 阅读 00-specs/REQ.md 确认本任务需求范围
|
|
1432
|
+
- [ ] 阅读 _shared/API_CONTRACT.yaml 确认接口契约
|
|
1433
|
+
- [ ] 阅读 _shared/CONTEXT.md 确认跨端关联
|
|
1434
|
+
|
|
1435
|
+
### 第二阶段:技术方案
|
|
1436
|
+
- [ ] 阅读 00-specs/TECH.md 确认技术方案
|
|
1437
|
+
- [ ] 确认本端涉及的接口/页面清单
|
|
1438
|
+
- [ ] 确认数据模型和字段映射
|
|
1439
|
+
|
|
1440
|
+
### 第三阶段:开发实施
|
|
1441
|
+
- [ ] 按 TECH.md 实施开发
|
|
1442
|
+
- [ ] 编写单元测试/集成测试
|
|
1443
|
+
- [ ] 自测通过
|
|
1444
|
+
|
|
1445
|
+
### 第四阶段:验收交付
|
|
1446
|
+
- [ ] 更新 TASK.md 进度
|
|
1447
|
+
- [ ] 提交代码并关联本任务
|
|
1448
|
+
- [ ] 通知相关端联调
|
|
1449
|
+
|
|
1450
|
+
## ${isBk ? '接口清单' : '页面清单'}
|
|
1451
|
+
${isBk ? apiList : pageList}
|
|
1244
1452
|
|
|
1245
1453
|
## 产出物
|
|
1246
1454
|
| 产出物 | 状态 | 路径 |
|
|
@@ -1280,14 +1488,12 @@ ${section.content}
|
|
|
1280
1488
|
};
|
|
1281
1489
|
// ── 所有端平铺:{端名}/{子任务}/ (v6.49.2+ 统一架构)──
|
|
1282
1490
|
// 不再区分前后端,所有端平铺在任务目录下
|
|
1283
|
-
//
|
|
1491
|
+
// 子任务目录名使用 generateSubtaskId 生成的全局唯一 ID:Task-{num}-{platform}
|
|
1284
1492
|
for (const platform of taskPlatforms) {
|
|
1285
1493
|
const platformDir = (0, path_1.join)(taskDir, platform);
|
|
1286
1494
|
const subtaskId = subtaskIdMap.get(platform);
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const subtaskDirName = `${taskId}-${subtaskSlug}`;
|
|
1290
|
-
const subtaskDir = (0, path_1.join)(platformDir, subtaskDirName);
|
|
1495
|
+
// 子任务目录名 = subtaskId(如 Task-001-booking-service)
|
|
1496
|
+
const subtaskDir = (0, path_1.join)(platformDir, subtaskId);
|
|
1291
1497
|
const subtaskHours = section._hoursByPlatform?.[platform] || Math.ceil(complexity.estimatedHours / taskPlatforms.length);
|
|
1292
1498
|
// 判断是否后端(用于生成不同的文档内容)
|
|
1293
1499
|
const isBk = platform === 'backend' || platform.startsWith('后台') || /-(service|api|server|backend)$/i.test(platform);
|
|
@@ -1301,7 +1507,8 @@ ${section.content}
|
|
|
1301
1507
|
// 从端列表查找第一个后端端名作为 fallback(v6.48.0+)
|
|
1302
1508
|
const allPlatforms = await (0, spec_paths_1.parsePlatformList)();
|
|
1303
1509
|
const fallbackBackend = allPlatforms.find(p => p === 'backend' || p.startsWith('后台') || /-(service|api|server|backend)$/i.test(p)) || 'backend';
|
|
1304
|
-
const
|
|
1510
|
+
const fallbackSubtaskId = subtaskIdMap.get(fallbackBackend) || generateSubtaskId(taskNum, fallbackBackend);
|
|
1511
|
+
const autoSubtaskDir = (0, path_1.join)(taskDir, fallbackBackend, fallbackSubtaskId);
|
|
1305
1512
|
await (0, fs_extra_1.ensureDir)((0, path_1.join)(autoSubtaskDir, '.meta'));
|
|
1306
1513
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'type'), taskType);
|
|
1307
1514
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'status'), 'todo');
|
|
@@ -1372,7 +1579,8 @@ ${section.content}
|
|
|
1372
1579
|
const body = reqContent.replace(/^#[^\n]*\n/, '').trim();
|
|
1373
1580
|
originalDesc = body.length > 500 ? body.slice(0, 500) + '...' : body;
|
|
1374
1581
|
}
|
|
1375
|
-
|
|
1582
|
+
// v6.69.3+: CONTEXT.md 写入 _shared/(符合规范),同时保留 00-specs/ 副本供兼容
|
|
1583
|
+
const contextContent = `# 任务上下文
|
|
1376
1584
|
|
|
1377
1585
|
## 来源追溯
|
|
1378
1586
|
|
|
@@ -1392,12 +1600,25 @@ ${originalDesc || '> 待补充(执行 analyze 后自动生成)'}
|
|
|
1392
1600
|
|
|
1393
1601
|
${relatedTasks.length > 0 ? relatedTasks.join('\n') : '> 暂无关联任务(split 时自动填充)'}
|
|
1394
1602
|
|
|
1603
|
+
## 跨端关联
|
|
1604
|
+
|
|
1605
|
+
> 来自 FUNCTION_MAP.md
|
|
1606
|
+
|
|
1607
|
+
| 属性 | 值 |
|
|
1608
|
+
|:---|:---|
|
|
1609
|
+
| 共享能力 | ${section._sharedCapability || '无'} |
|
|
1610
|
+
| 依赖任务 | ${(section._dependsOn || []).join(', ') || '无'} |
|
|
1611
|
+
| 跨端说明 | ${section._description || '—'} |
|
|
1612
|
+
|
|
1395
1613
|
## 影响范围
|
|
1396
1614
|
|
|
1397
1615
|
| 端 | 影响说明 |
|
|
1398
1616
|
|:---|:---|
|
|
1399
1617
|
${taskPlatforms.map((p) => `| ${p} | 待补充 |`).join('\n')}
|
|
1400
|
-
|
|
1618
|
+
`;
|
|
1619
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'CONTEXT.md'), contextContent);
|
|
1620
|
+
// 兼容:00-specs/ 下也保留一份(部分旧代码可能读取 00-specs/CONTEXT.md)
|
|
1621
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'CONTEXT.md'), contextContent);
|
|
1401
1622
|
// ── 7. 问题追踪 ──
|
|
1402
1623
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.issues.md'), `# ${section.name} - 问题追踪\n\n> 执行过程中发现的问题记录于此。\n\n`);
|
|
1403
1624
|
}
|
|
@@ -2212,7 +2433,7 @@ ${isH5 ? '移动端优先,适配 375/414/768' :
|
|
|
2212
2433
|
/**
|
|
2213
2434
|
* 构建完整的 AI 智能拆分 Prompt(含 SpecCore 理念 + 粒度规则 + 完整上下文)
|
|
2214
2435
|
*/
|
|
2215
|
-
function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents, staffing, teamSize, granularityLabel, granularityHint) {
|
|
2436
|
+
function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents, staffing, teamSize, granularityLabel, granularityHint, standardPlatforms) {
|
|
2216
2437
|
let p = `# SpecCore AI 智能拆分\n\n`;
|
|
2217
2438
|
p += `> 迭代: ${iteration} | 粒度: ${granularityLabel} | 生成: ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
2218
2439
|
// 技术宪法
|
|
@@ -2250,6 +2471,15 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2250
2471
|
}
|
|
2251
2472
|
p += `\n**超出上限必须再拆,低于下限必须合并。**\n\n`;
|
|
2252
2473
|
p += `用户可通过 --granularity macro|module|atomic 调整全局粒度。\n\n`;
|
|
2474
|
+
// v6.69.3+: 注入标准端名列表
|
|
2475
|
+
if (standardPlatforms.length > 0) {
|
|
2476
|
+
p += `## 🖥️ 项目端列表(标准端名)\n\n`;
|
|
2477
|
+
p += `本项目已配置以下端,所有 scope、hoursByPlatform 必须使用这些**标准端名**,禁止使用简写或中文:\n\n`;
|
|
2478
|
+
p += `\`${standardPlatforms.join('`, `')}\`\n\n`;
|
|
2479
|
+
p += `- 后端端名示例: \`booking-service\`, \`room-service\`(不是 "后端"、"backend")\n`;
|
|
2480
|
+
p += `- 前端端名示例: \`admin-web\`, \`h5-mobile\`(不是 "web"、"admin"、"h5")\n`;
|
|
2481
|
+
p += `- **scope 数组必须使用上述标准端名**,否则会导致目录结构错误\n\n`;
|
|
2482
|
+
}
|
|
2253
2483
|
// SpecCore 拆分原则
|
|
2254
2484
|
p += `## ⚙️ SpecCore 拆分原则\n\n`;
|
|
2255
2485
|
p += `SpecCore 核心理念: "Code by Spec, Not by Vibe" — 每个任务必须有对应的 Spec,AI 在 Spec 约束下工作。\n\n`;
|
|
@@ -2328,10 +2558,10 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2328
2558
|
p += ` "topic": "english-slug-for-directory",\n`;
|
|
2329
2559
|
p += ` "type": "feature|bugfix|refactor|research",\n`;
|
|
2330
2560
|
p += ` "reason": "为什么这样拆分",\n`;
|
|
2331
|
-
p += ` "scope": ["
|
|
2561
|
+
p += ` "scope": ["booking-service", "admin-web"],\n`;
|
|
2332
2562
|
p += ` "apis": ["POST /api/auth/login"],\n`;
|
|
2333
2563
|
p += ` "tables": ["users"],\n`;
|
|
2334
|
-
p += ` "hoursByPlatform": { "
|
|
2564
|
+
p += ` "hoursByPlatform": { "booking-service": 8, "admin-web": 8 },\n`;
|
|
2335
2565
|
p += ` "estimatedHours": 16,\n`;
|
|
2336
2566
|
p += ` "priority": "high|medium|low",\n`;
|
|
2337
2567
|
p += ` "dependencies": [],\n`;
|
|
@@ -2351,9 +2581,17 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2351
2581
|
p += `> 同一模块/领域的任务填相同的值,用于粒度校验和任务分组\n`;
|
|
2352
2582
|
p += `> **topic** 必须是英文短横线格式(如 \`user-authentication\`、\`product-crud\`),用于生成任务目录名 Task-NNN-{topic}\n`;
|
|
2353
2583
|
p += `> **sourceFile** 必须填写:该任务对应的 020-specs 源文档路径(如 \`bugs/login-timeout.md\`、\`features/user-auth.md\`、\`refactors/db-pool.md\`),用于在 CONTEXT.md 中生成来源追溯\n`;
|
|
2354
|
-
p += `> **reqContent
|
|
2355
|
-
p += `>
|
|
2356
|
-
p += `>
|
|
2584
|
+
p += `> **reqContent 质量要求(必填,禁止模板化)**:\n`;
|
|
2585
|
+
p += `> - 必须是**具体的、可执行的需求描述**,不是"待补充"或"参考全局文档"\n`;
|
|
2586
|
+
p += `> - 包含:业务规则(含边界条件)、数据模型(字段/类型/约束)、接口清单(方法/路径/参数/响应)\n`;
|
|
2587
|
+
p += `> - 从 020-specs/global/REQUIREMENT.md 和对应端 TECH.md 中提取本任务相关的具体内容\n`;
|
|
2588
|
+
p += `> - 直接写入 00-specs/REQ.md,执行时 AI 不再重新分析需求\n`;
|
|
2589
|
+
p += `> **techContent 质量要求(必填,禁止模板化)**:\n`;
|
|
2590
|
+
p += `> - 必须是**具体的技术实现方案**,不是框架模板\n`;
|
|
2591
|
+
p += `> - 包含:架构设计、核心逻辑伪代码/流程、数据库设计(表结构/索引)、API 详细定义、测试策略\n`;
|
|
2592
|
+
p += `> - 从 020-specs/global/TECH.md 和对应端 TECH.md 中提取本任务相关的技术细节\n`;
|
|
2593
|
+
p += `> - 直接写入 00-specs/TECH.md,执行时 AI 据此直接开发\n`;
|
|
2594
|
+
p += `> **质量红线**:如果 reqContent/techContent 只有标题和占位符(如 "<!-- AI-FILL -->"),视为不合格,必须重新生成\n\n`;
|
|
2357
2595
|
p += `### ⚠️ 工时估算规则(重要)\n\n`;
|
|
2358
2596
|
p += `- **hoursByPlatform**: 按端分别估算工时,key 对应 scope 中的端名称\n`;
|
|
2359
2597
|
p += `- **estimatedHours**: 各端工时总和(仅用于展示,不参与粒度校验)\n`;
|
|
@@ -2679,6 +2917,89 @@ function parseModulePlatforms(content, allPlatforms) {
|
|
|
2679
2917
|
}
|
|
2680
2918
|
return modules;
|
|
2681
2919
|
}
|
|
2920
|
+
/**
|
|
2921
|
+
* v6.70.0+: 解析 FUNCTION_MAP.md 跨端功能映射表
|
|
2922
|
+
* 返回功能单元列表,含涉及端、共享能力、依赖关系
|
|
2923
|
+
*/
|
|
2924
|
+
function parseFunctionMap(content, allPlatforms) {
|
|
2925
|
+
const units = [];
|
|
2926
|
+
const lines = content.split('\n');
|
|
2927
|
+
let inTable = false;
|
|
2928
|
+
let platformColIdx = -1;
|
|
2929
|
+
let sharedCapColIdx = -1;
|
|
2930
|
+
let dependsOnColIdx = -1;
|
|
2931
|
+
let descColIdx = -1;
|
|
2932
|
+
for (const line of lines) {
|
|
2933
|
+
// 检测映射表开始(通过表头特征)
|
|
2934
|
+
if (line.includes('功能单元') && line.includes('涉及端')) {
|
|
2935
|
+
inTable = true;
|
|
2936
|
+
// 解析表头,找到各列索引
|
|
2937
|
+
const headerCells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
2938
|
+
platformColIdx = headerCells.findIndex(c => c.includes('涉及端'));
|
|
2939
|
+
sharedCapColIdx = headerCells.findIndex(c => c.includes('共享能力'));
|
|
2940
|
+
dependsOnColIdx = headerCells.findIndex(c => c.includes('依赖任务'));
|
|
2941
|
+
descColIdx = headerCells.findIndex(c => c.includes('说明'));
|
|
2942
|
+
continue;
|
|
2943
|
+
}
|
|
2944
|
+
if (!inTable)
|
|
2945
|
+
continue;
|
|
2946
|
+
// 检测下一个 ## 标题 → 表格结束
|
|
2947
|
+
if (line.startsWith('## ') && !line.includes('功能映射')) {
|
|
2948
|
+
break;
|
|
2949
|
+
}
|
|
2950
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
2951
|
+
if (cells.length < 3)
|
|
2952
|
+
continue;
|
|
2953
|
+
// 分隔行跳过
|
|
2954
|
+
if (cells.every(c => /^[-:]+$/.test(c)))
|
|
2955
|
+
continue;
|
|
2956
|
+
// 表头行(已经处理过,跳过)
|
|
2957
|
+
if (cells.some(c => c.includes('功能单元')) || cells.some(c => c.includes('涉及端'))) {
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
// 数据行:第2列是功能单元名
|
|
2961
|
+
const unitName = cells[1];
|
|
2962
|
+
if (!unitName || unitName === '#' || unitName === '功能单元')
|
|
2963
|
+
continue;
|
|
2964
|
+
// 解析涉及端
|
|
2965
|
+
let platforms = [];
|
|
2966
|
+
if (platformColIdx >= 0 && platformColIdx < cells.length) {
|
|
2967
|
+
const raw = cells[platformColIdx];
|
|
2968
|
+
if (raw && raw !== '无' && raw !== '—' && raw !== '-') {
|
|
2969
|
+
platforms = raw.split(/[,,]/)
|
|
2970
|
+
.map(p => p.trim())
|
|
2971
|
+
.filter(p => p && allPlatforms.includes(p));
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
// 涉及端为空 → 回退全端(但保留空列表表示纯文档/无开发)
|
|
2975
|
+
if (platforms.length === 0) {
|
|
2976
|
+
platforms = [...allPlatforms];
|
|
2977
|
+
}
|
|
2978
|
+
// 解析共享能力
|
|
2979
|
+
let sharedCapability = '无';
|
|
2980
|
+
if (sharedCapColIdx >= 0 && sharedCapColIdx < cells.length) {
|
|
2981
|
+
const raw = cells[sharedCapColIdx];
|
|
2982
|
+
if (raw && raw !== '无' && raw !== '—' && raw !== '-') {
|
|
2983
|
+
sharedCapability = raw;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
// 解析依赖任务
|
|
2987
|
+
let dependsOn = [];
|
|
2988
|
+
if (dependsOnColIdx >= 0 && dependsOnColIdx < cells.length) {
|
|
2989
|
+
const raw = cells[dependsOnColIdx];
|
|
2990
|
+
if (raw && raw !== '无' && raw !== '—' && raw !== '-') {
|
|
2991
|
+
dependsOn = raw.split(/[,,]/).map(p => p.trim()).filter(Boolean);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
// 解析说明
|
|
2995
|
+
let description = '';
|
|
2996
|
+
if (descColIdx >= 0 && descColIdx < cells.length) {
|
|
2997
|
+
description = cells[descColIdx];
|
|
2998
|
+
}
|
|
2999
|
+
units.push({ name: unitName, platforms, sharedCapability, dependsOn, description });
|
|
3000
|
+
}
|
|
3001
|
+
return units;
|
|
3002
|
+
}
|
|
2682
3003
|
/**
|
|
2683
3004
|
* 尝试模块驱动拆分:从功能模块创建任务目录结构
|
|
2684
3005
|
* 成功返回 true,无功能模块时返回 false(回退到传统流程)
|
|
@@ -2688,50 +3009,80 @@ async function tryModuleDrivenSplit(iteration, iterationDir, options) {
|
|
|
2688
3009
|
const allPlatforms = await detectPlatforms(iterationDir);
|
|
2689
3010
|
// 收集功能模块(含涉及端信息)
|
|
2690
3011
|
const modules = [];
|
|
2691
|
-
// 1. 优先从 global/
|
|
2692
|
-
const
|
|
2693
|
-
let
|
|
2694
|
-
if (await (0, fs_extra_1.pathExists)(
|
|
3012
|
+
// v6.70.0+: 1. 优先从 global/FUNCTION_MAP.md 读取跨端功能映射表
|
|
3013
|
+
const functionMapPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'FUNCTION_MAP.md');
|
|
3014
|
+
let functionMapParsed = false;
|
|
3015
|
+
if (await (0, fs_extra_1.pathExists)(functionMapPath)) {
|
|
2695
3016
|
try {
|
|
2696
|
-
const content = await (0, fs_extra_1.readFile)(
|
|
2697
|
-
const parsed =
|
|
3017
|
+
const content = await (0, fs_extra_1.readFile)(functionMapPath, 'utf-8');
|
|
3018
|
+
const parsed = parseFunctionMap(content, allPlatforms);
|
|
2698
3019
|
if (parsed.length > 0) {
|
|
2699
|
-
for (const
|
|
3020
|
+
for (const u of parsed) {
|
|
2700
3021
|
modules.push({
|
|
2701
|
-
name:
|
|
2702
|
-
slug: slugify(
|
|
3022
|
+
name: u.name,
|
|
3023
|
+
slug: slugify(u.name),
|
|
2703
3024
|
type: 'feature',
|
|
2704
|
-
sourceFile: 'global/
|
|
2705
|
-
platforms:
|
|
3025
|
+
sourceFile: 'global/FUNCTION_MAP.md',
|
|
3026
|
+
platforms: u.platforms,
|
|
3027
|
+
sharedCapability: u.sharedCapability,
|
|
3028
|
+
dependsOn: u.dependsOn,
|
|
3029
|
+
description: u.description,
|
|
2706
3030
|
});
|
|
2707
3031
|
}
|
|
2708
|
-
|
|
2709
|
-
logger_1.logger.info(` 📋 从 global/
|
|
3032
|
+
functionMapParsed = true;
|
|
3033
|
+
logger_1.logger.info(` 📋 从 global/FUNCTION_MAP.md 读取到 ${parsed.length} 个功能单元(严格按映射表拆分)`);
|
|
2710
3034
|
}
|
|
2711
3035
|
}
|
|
2712
|
-
catch {
|
|
3036
|
+
catch (e) {
|
|
3037
|
+
logger_1.logger.debug('解析 FUNCTION_MAP.md 失败:', e);
|
|
3038
|
+
}
|
|
2713
3039
|
}
|
|
2714
|
-
// 2.
|
|
2715
|
-
if (!
|
|
2716
|
-
const
|
|
2717
|
-
|
|
3040
|
+
// 2. 回退:从 global/REQUIREMENT.md 读取功能模块清单(含涉及端)
|
|
3041
|
+
if (!functionMapParsed) {
|
|
3042
|
+
const globalReqPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'REQUIREMENT.md');
|
|
3043
|
+
let modulePlatformsParsed = false;
|
|
3044
|
+
if (await (0, fs_extra_1.pathExists)(globalReqPath)) {
|
|
2718
3045
|
try {
|
|
2719
|
-
const
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
}
|
|
3046
|
+
const content = await (0, fs_extra_1.readFile)(globalReqPath, 'utf-8');
|
|
3047
|
+
const parsed = parseModulePlatforms(content, allPlatforms);
|
|
3048
|
+
if (parsed.length > 0) {
|
|
3049
|
+
for (const m of parsed) {
|
|
3050
|
+
modules.push({
|
|
3051
|
+
name: m.name,
|
|
3052
|
+
slug: slugify(m.name),
|
|
3053
|
+
type: 'feature',
|
|
3054
|
+
sourceFile: 'global/REQUIREMENT.md',
|
|
3055
|
+
platforms: m.platforms,
|
|
3056
|
+
});
|
|
2730
3057
|
}
|
|
3058
|
+
modulePlatformsParsed = true;
|
|
3059
|
+
logger_1.logger.info(` 📋 从 global/REQUIREMENT.md 读取到 ${parsed.length} 个功能模块(含涉及端)`);
|
|
2731
3060
|
}
|
|
2732
3061
|
}
|
|
2733
3062
|
catch { }
|
|
2734
3063
|
}
|
|
3064
|
+
// 3. 回退:读取 features/*/README.md(无涉及端信息,使用全端)
|
|
3065
|
+
if (!modulePlatformsParsed) {
|
|
3066
|
+
const featuresDir = (0, path_1.join)(reqDir, 'features');
|
|
3067
|
+
if (await (0, fs_extra_1.pathExists)(featuresDir)) {
|
|
3068
|
+
try {
|
|
3069
|
+
const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
|
|
3070
|
+
for (const entry of entries) {
|
|
3071
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
3072
|
+
const readmePath = (0, path_1.join)(featuresDir, entry.name, 'README.md');
|
|
3073
|
+
if (await (0, fs_extra_1.pathExists)(readmePath)) {
|
|
3074
|
+
modules.push({
|
|
3075
|
+
name: entry.name, slug: slugify(entry.name), type: 'feature',
|
|
3076
|
+
sourceFile: `features/${entry.name}/README.md`,
|
|
3077
|
+
platforms: [...allPlatforms],
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
catch { }
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
2735
3086
|
}
|
|
2736
3087
|
// 3. 读取类型文档(bugs/refactors/research)
|
|
2737
3088
|
for (const typeDir of ['bugs', 'refactors', 'research']) {
|
|
@@ -2806,6 +3157,13 @@ async function tryModuleDrivenSplit(iteration, iterationDir, options) {
|
|
|
2806
3157
|
section._owner = '未分配';
|
|
2807
3158
|
section._taskId = taskId;
|
|
2808
3159
|
section.functionalUnit = mod.name;
|
|
3160
|
+
// v6.70.0+: 传递 FUNCTION_MAP.md 中的扩展信息
|
|
3161
|
+
if (mod.sharedCapability)
|
|
3162
|
+
section._sharedCapability = mod.sharedCapability;
|
|
3163
|
+
if (mod.dependsOn)
|
|
3164
|
+
section._dependsOn = mod.dependsOn;
|
|
3165
|
+
if (mod.description)
|
|
3166
|
+
section._description = mod.description;
|
|
2809
3167
|
await createTaskFromSection(iterationDir, taskId, section, modPlatforms, mod.type, []);
|
|
2810
3168
|
createdSections.push(section);
|
|
2811
3169
|
logger_1.logger.info(` ✅ 创建: ${taskId} [${mod.type}] — ${mod.name} (${modPlatforms.length} 个端: ${modPlatforms.join(', ')})`);
|