speccore 7.4.1 → 7.4.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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../../src/commands/iteration/split.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../../src/commands/iteration/split.ts"],"names":[],"mappings":"AAmIA,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAuBD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+xBzF"}
|
|
@@ -68,7 +68,7 @@ function normalizeScopePlatforms(scopeArr, standardPlatforms) {
|
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
70
|
// 2. 后端类简写 → 匹配第一个后端端名
|
|
71
|
-
if (/后端|backend|服务端|server/i.test(s)) {
|
|
71
|
+
if (/后端|backend|服务端|server|api/i.test(s)) {
|
|
72
72
|
const backendPlatform = standardPlatforms.find(p => /-(service|api|server|backend)$/i.test(p) || p.startsWith('后台'));
|
|
73
73
|
if (backendPlatform && !used.has(backendPlatform)) {
|
|
74
74
|
result.push(backendPlatform);
|
|
@@ -95,6 +95,7 @@ function normalizeScopePlatforms(scopeArr, standardPlatforms) {
|
|
|
95
95
|
'小程序': ['miniapp', 'mini-app', '小程序'],
|
|
96
96
|
'admin': ['admin-web', 'admin', '后台管理'],
|
|
97
97
|
'web': ['admin-web', 'web', 'h5-mobile'],
|
|
98
|
+
'frontend': ['admin-web', 'h5-mobile', 'frontend'],
|
|
98
99
|
'app': ['app', 'android', 'ios'],
|
|
99
100
|
'安卓': ['android'],
|
|
100
101
|
'ios': ['ios'],
|
|
@@ -108,11 +109,9 @@ function normalizeScopePlatforms(scopeArr, standardPlatforms) {
|
|
|
108
109
|
continue;
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
// 5.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
used.add(s);
|
|
115
|
-
}
|
|
112
|
+
// 5. v7.4.2+: 无法匹配时警告并跳过,不保留非标准端名
|
|
113
|
+
logger_1.logger.warn(` ⚠️ 非标准端名 "${s}",无法映射到 CONSTITUTION.md 标准端名 ${standardPlatforms.join('/')}`);
|
|
114
|
+
logger_1.logger.warn(` 已跳过该端,请检查 AI 输出的 scope 字段`);
|
|
116
115
|
}
|
|
117
116
|
return result.length > 0 ? result : standardPlatforms;
|
|
118
117
|
}
|
|
@@ -703,7 +702,7 @@ async function iterationSplitCommand(options) {
|
|
|
703
702
|
// v6.76.0+: 传入过滤条件,限制拆分范围;传入已有 Task 结构,支持增量拆分
|
|
704
703
|
const modulesFilter = options.modules ? options.modules.split(',').map(m => m.trim()).filter(Boolean) : undefined;
|
|
705
704
|
const platformsFilter = options.platforms ? options.platforms.split(',').map(p => p.trim()).filter(Boolean) : undefined;
|
|
706
|
-
let splitPrompt = buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, allPlatforms, modulesFilter, platformsFilter, existingTaskStructure);
|
|
705
|
+
let splitPrompt = await buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, allPlatforms, modulesFilter, platformsFilter, existingTaskStructure, iterationDir);
|
|
707
706
|
// 注入全局上下文(INDEX + TOC 目录,AI 自主读取)
|
|
708
707
|
const { loadGlobalContext, formatGlobalContext } = await Promise.resolve().then(() => __importStar(require('../../core/prompt-builder')));
|
|
709
708
|
const globalCtx = await loadGlobalContext(process.cwd(), 'split');
|
|
@@ -2446,7 +2445,7 @@ ${isH5 ? '移动端优先,适配 375/414/768' :
|
|
|
2446
2445
|
/**
|
|
2447
2446
|
* 构建完整的 AI 智能拆分 Prompt(含 SpecCore 理念 + 粒度规则 + 完整上下文)
|
|
2448
2447
|
*/
|
|
2449
|
-
function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents, standardPlatforms, modulesFilter, platformsFilter, existingTasks) {
|
|
2448
|
+
async function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents, standardPlatforms, modulesFilter, platformsFilter, existingTasks, iterationDir) {
|
|
2450
2449
|
let p = `# SpecCore AI 智能拆分\n\n`;
|
|
2451
2450
|
p += `> 迭代: ${iteration} | 生成: ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
2452
2451
|
// v6.76.0+: 拆分范围限制
|
|
@@ -2490,21 +2489,40 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2490
2489
|
}
|
|
2491
2490
|
// v6.69.3+: 注入标准端名列表
|
|
2492
2491
|
if (standardPlatforms.length > 0) {
|
|
2493
|
-
p += `## 🖥️
|
|
2492
|
+
p += `## 🖥️ 项目端列表(标准端名 — 权威来源)\n\n`;
|
|
2494
2493
|
p += `本项目已配置以下端,所有 scope 必须使用这些**标准端名**,禁止使用简写或中文:\n\n`;
|
|
2495
2494
|
p += `\`${standardPlatforms.join('`, `')}\`\n\n`;
|
|
2496
|
-
p += `-
|
|
2497
|
-
p += `-
|
|
2498
|
-
p += `-
|
|
2495
|
+
p += `- ⛔ **禁止使用以下非标准端名**: \`api\`、\`web\`、\`backend\`、\`frontend\`、\`admin\`、\`h5\`、\"后端\"、\"前端\"\n`;
|
|
2496
|
+
p += `- ✅ **必须使用 CONSTITUTION.md 中的标准端名**作为子任务目录名\n`;
|
|
2497
|
+
p += `- scope 数组和子任务目录名必须完全匹配上述标准端名\n\n`;
|
|
2499
2498
|
}
|
|
2500
|
-
//
|
|
2501
|
-
if (
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2499
|
+
// v7.4.2+: 功能单元精准上下文(CLI 提取,AI 拆分)
|
|
2500
|
+
if (iterationDir) {
|
|
2501
|
+
try {
|
|
2502
|
+
const funcMapPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'FUNCTION_MAP.md');
|
|
2503
|
+
if (await (0, fs_extra_1.pathExists)(funcMapPath)) {
|
|
2504
|
+
const funcMapContent = await (0, fs_extra_1.readFile)(funcMapPath, 'utf-8');
|
|
2505
|
+
const funcUnits = parseFunctionMap(funcMapContent, standardPlatforms);
|
|
2506
|
+
if (funcUnits.length > 0) {
|
|
2507
|
+
p += `## 🧩 功能单元上下文(CLI 从分析文档精准提取)\n\n`;
|
|
2508
|
+
p += `> 以下是从 FUNCTION_MAP.md / REQUIREMENT.md / _INDEX.md / _MODULES.md 中为每个功能单元提取的相关上下文。\n`;
|
|
2509
|
+
p += `> 拆分时必须基于这些功能单元,每个功能单元对应 1~3 个任务。\n\n`;
|
|
2510
|
+
for (const unit of funcUnits) {
|
|
2511
|
+
const unitCtx = await assembleUnitContext(iterationDir, unit.name, unit.platforms);
|
|
2512
|
+
p += `### 📌 功能单元: ${unit.name}\n`;
|
|
2513
|
+
p += `- 涉及端: ${unit.platforms.join(', ')}\n`;
|
|
2514
|
+
if (unit.sharedCapability && unit.sharedCapability !== '无')
|
|
2515
|
+
p += `- 共享能力: ${unit.sharedCapability}\n`;
|
|
2516
|
+
if (unit.dependsOn && unit.dependsOn.length > 0)
|
|
2517
|
+
p += `- 依赖: ${unit.dependsOn.join(', ')}\n`;
|
|
2518
|
+
if (unit.description)
|
|
2519
|
+
p += `- 说明: ${unit.description}\n`;
|
|
2520
|
+
p += `\n${unitCtx}\n\n---\n\n`;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
catch { }
|
|
2508
2526
|
}
|
|
2509
2527
|
// SpecCore 拆分原则
|
|
2510
2528
|
p += `## ⚙️ SpecCore 拆分原则\n\n`;
|
|
@@ -2618,14 +2636,19 @@ function buildSplitPrompt(iteration, constitutionContent, reqContent, specConten
|
|
|
2618
2636
|
p += `> - 直接写入 00-specs/TECH.md,执行时 AI 据此直接开发\n`;
|
|
2619
2637
|
p += `> **质量红线**:如果 reqContent/techContent 只有标题和占位符(如 "<!-- AI-FILL -->"),视为不合格,必须重新生成\n\n`;
|
|
2620
2638
|
p += `### ⚠️ 拆分规则(重要)\n\n`;
|
|
2639
|
+
p += `- **按功能单元拆分,不按技术层拆分**:每个任务必须对应一个业务功能(如「预订管理」「签到」「审批」),不要按技术层分组(如「后端改进」「前端修复」)\n`;
|
|
2640
|
+
p += `- ❌ 错误示例:Task-129-backend-improve、Task-130-admin-fix(按技术层分组)\n`;
|
|
2641
|
+
p += `- ✅ 正确示例:Task-129-booking-crud、Task-130-checkin-flow、Task-131-approval-management(按功能单元分组)\n`;
|
|
2642
|
+
p += `- **topic 必须反映功能单元**:每个任务的 topic(目录名后缀)必须唯一且反映业务功能,禁止所有任务都用同一个后缀(如 -impl)\n`;
|
|
2643
|
+
p += `- **scope 必须使用标准端名**:禁止使用 api/web/backend/frontend 等非标准名称\n`;
|
|
2621
2644
|
p += `- **一个功能单元默认 1 个任务**,最多拆成 3 个\n`;
|
|
2622
|
-
p += `- **按 scope 涉及的端创建子任务**:每个端 1 个子任务目录\n`;
|
|
2623
|
-
p += `- **不要按端拆分任务**:同一功能的前后端各端工作在一个任务里,按端拆分子任务\n`;
|
|
2624
|
-
p += `- **用户不满意时可手动拆分**:CLI 提供简单的默认拆分,复杂拆分由用户手动调整\n\n`;
|
|
2645
|
+
p += `- **按 scope 涉及的端创建子任务**:每个端 1 个子任务目录\n\n`;
|
|
2625
2646
|
// 质量自检
|
|
2626
2647
|
p += `## ✅ 质量自检(必须全部通过)\n\n`;
|
|
2627
2648
|
p += `□ 每个任务都满足原子任务定义?\n`;
|
|
2628
|
-
p += `□
|
|
2649
|
+
p += `□ **每个任务按功能单元命名,不是按技术层**?(禁止 backend-improve/admin-fix 这类名称)\n`;
|
|
2650
|
+
p += `□ **每个 topic 唯一且反映业务功能**?(禁止全部用 -impl)\n`;
|
|
2651
|
+
p += `□ **scope 只包含 CONSTITUTION.md 标准端名**?(禁止 api/web/backend/frontend)\n`;
|
|
2629
2652
|
p += `□ 没有循环依赖?\n`;
|
|
2630
2653
|
p += `□ 基础模块排在前面?\n`;
|
|
2631
2654
|
p += `□ 同功能单元内的任务没被过度拆分?(每个功能单元 ≤ 3 个任务)\n`;
|
|
@@ -2912,38 +2935,46 @@ speccore analyze --task ${taskId}${iterFlag}
|
|
|
2912
2935
|
// ── v6.49.14+: 模块驱动拆分 — 从 overview/REQUIREMENT.md 读取涉及端 ──
|
|
2913
2936
|
/**
|
|
2914
2937
|
* 从 overview/REQUIREMENT.md 解析功能模块清单的「涉及端」列
|
|
2938
|
+
* 支持两种格式:
|
|
2939
|
+
* 1. 单表格含「涉及端」列(功能模块 | 涉及端 | ...)
|
|
2940
|
+
* 2. 按端分节(### X.X ... — {platform} 后跟表格)
|
|
2915
2941
|
* 返回模块列表及其涉及的标准端名
|
|
2916
2942
|
*/
|
|
2917
2943
|
function parseModulePlatforms(content, allPlatforms) {
|
|
2944
|
+
// v7.4.2+: 先尝试原始格式(单表格含「涉及端」列)
|
|
2945
|
+
const tableResult = parseModulePlatformsTable(content, allPlatforms);
|
|
2946
|
+
if (tableResult.length > 0)
|
|
2947
|
+
return tableResult;
|
|
2948
|
+
// v7.4.2+: 回退到按端分节格式
|
|
2949
|
+
return parseModulePlatformsBySection(content, allPlatforms);
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* v7.4.2+: 解析单表格含「涉及端」列的格式
|
|
2953
|
+
*/
|
|
2954
|
+
function parseModulePlatformsTable(content, allPlatforms) {
|
|
2918
2955
|
const modules = [];
|
|
2919
2956
|
const lines = content.split('\n');
|
|
2920
2957
|
let inFeatureTable = false;
|
|
2921
2958
|
let platformColIdx = -1;
|
|
2922
2959
|
for (const line of lines) {
|
|
2923
|
-
// 检测功能模块清单表格开始
|
|
2924
2960
|
if (line.includes('功能模块清单')) {
|
|
2925
2961
|
inFeatureTable = true;
|
|
2926
2962
|
continue;
|
|
2927
2963
|
}
|
|
2928
2964
|
if (!inFeatureTable)
|
|
2929
2965
|
continue;
|
|
2930
|
-
|
|
2931
|
-
if (line.startsWith('## ') && !line.includes('功能模块清单')) {
|
|
2966
|
+
if (line.startsWith('## ') && !line.includes('功能模块清单'))
|
|
2932
2967
|
break;
|
|
2933
|
-
}
|
|
2934
2968
|
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
2935
2969
|
if (cells.length < 2)
|
|
2936
2970
|
continue;
|
|
2937
|
-
// 表头行 → 找到「涉及端」列索引
|
|
2938
2971
|
if (cells.some(c => c.includes('涉及端'))) {
|
|
2939
2972
|
platformColIdx = cells.findIndex(c => c.includes('涉及端'));
|
|
2940
2973
|
continue;
|
|
2941
2974
|
}
|
|
2942
|
-
// 分隔行跳过
|
|
2943
2975
|
if (cells.every(c => /^[-:]+$/.test(c)))
|
|
2944
2976
|
continue;
|
|
2945
|
-
|
|
2946
|
-
const moduleName = cells[1]; // 第2列通常是模块名
|
|
2977
|
+
const moduleName = cells[1];
|
|
2947
2978
|
if (!moduleName || moduleName === '#' || moduleName === '模块')
|
|
2948
2979
|
continue;
|
|
2949
2980
|
if (platformColIdx >= 0 && platformColIdx < cells.length) {
|
|
@@ -2958,16 +2989,93 @@ function parseModulePlatforms(content, allPlatforms) {
|
|
|
2958
2989
|
}
|
|
2959
2990
|
}
|
|
2960
2991
|
}
|
|
2961
|
-
// 涉及端为空或无法解析 → 回退全端
|
|
2962
2992
|
modules.push({ name: moduleName, platforms: [...allPlatforms] });
|
|
2963
2993
|
}
|
|
2964
2994
|
return modules;
|
|
2965
2995
|
}
|
|
2996
|
+
/**
|
|
2997
|
+
* v7.4.2+: 解析按端分节的 REQUIREMENT.md 格式
|
|
2998
|
+
* 匹配: ### 2.1 后端 — booking-service 或 ### 2.1 前端 — admin-web
|
|
2999
|
+
* 从标题提取端名,从后续表格提取模块名,建立模块→端的映射
|
|
3000
|
+
*/
|
|
3001
|
+
function parseModulePlatformsBySection(content, allPlatforms) {
|
|
3002
|
+
const lines = content.split('\n');
|
|
3003
|
+
// 模块名 → 涉及端列表 的映射
|
|
3004
|
+
const modulePlatformMap = new Map();
|
|
3005
|
+
let currentPlatform = '';
|
|
3006
|
+
let inTable = false;
|
|
3007
|
+
for (const line of lines) {
|
|
3008
|
+
// 检测按端分节标题: ### 2.1 后端 — booking-service
|
|
3009
|
+
const sectionMatch = line.match(/^###\s+[\d.]+\s+.*[—\-–]\s*(.+)$/);
|
|
3010
|
+
if (sectionMatch) {
|
|
3011
|
+
const rawPlatform = sectionMatch[1].trim();
|
|
3012
|
+
// 匹配标准端名
|
|
3013
|
+
currentPlatform = allPlatforms.find(p => rawPlatform.includes(p)) || '';
|
|
3014
|
+
inTable = false;
|
|
3015
|
+
continue;
|
|
3016
|
+
}
|
|
3017
|
+
// 非当前端标题下的 ## 标题 → 重置
|
|
3018
|
+
if (line.startsWith('## ') && !line.startsWith('### ')) {
|
|
3019
|
+
currentPlatform = '';
|
|
3020
|
+
inTable = false;
|
|
3021
|
+
continue;
|
|
3022
|
+
}
|
|
3023
|
+
if (!currentPlatform)
|
|
3024
|
+
continue;
|
|
3025
|
+
// 检测表格开始(表头行含「功能模块」)
|
|
3026
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
3027
|
+
if (cells.length >= 2 && cells.some(c => c.includes('功能模块'))) {
|
|
3028
|
+
inTable = true;
|
|
3029
|
+
continue;
|
|
3030
|
+
}
|
|
3031
|
+
if (!inTable)
|
|
3032
|
+
continue;
|
|
3033
|
+
// 分隔行跳过
|
|
3034
|
+
if (cells.every(c => /^[-:]+$/.test(c)))
|
|
3035
|
+
continue;
|
|
3036
|
+
// 表头行跳过
|
|
3037
|
+
if (cells.some(c => c === '功能模块' || c === '模块'))
|
|
3038
|
+
continue;
|
|
3039
|
+
// 数据行:第1列或第2列是模块名
|
|
3040
|
+
const moduleName = cells[0] && cells[0] !== '功能模块' ? cells[0] : (cells[1] || '');
|
|
3041
|
+
if (!moduleName || moduleName.length < 2)
|
|
3042
|
+
continue;
|
|
3043
|
+
// 跳过状态标记行
|
|
3044
|
+
if (moduleName.startsWith('#'))
|
|
3045
|
+
continue;
|
|
3046
|
+
// 添加到映射
|
|
3047
|
+
const existing = modulePlatformMap.get(moduleName) || [];
|
|
3048
|
+
if (!existing.includes(currentPlatform)) {
|
|
3049
|
+
existing.push(currentPlatform);
|
|
3050
|
+
}
|
|
3051
|
+
modulePlatformMap.set(moduleName, existing);
|
|
3052
|
+
}
|
|
3053
|
+
// 转换为结果数组
|
|
3054
|
+
const modules = [];
|
|
3055
|
+
for (const [name, platforms] of modulePlatformMap) {
|
|
3056
|
+
modules.push({ name, platforms: platforms.length > 0 ? platforms : [...allPlatforms] });
|
|
3057
|
+
}
|
|
3058
|
+
return modules;
|
|
3059
|
+
}
|
|
2966
3060
|
/**
|
|
2967
3061
|
* v6.70.0+: 解析 FUNCTION_MAP.md 跨端功能映射表
|
|
3062
|
+
* 支持两种格式:
|
|
3063
|
+
* 1. Markdown 表格(功能单元 | 涉及端 | ...)
|
|
3064
|
+
* 2. 树形格式(├── 模块名 ... platform1 ✅ | platform2 ✅)
|
|
2968
3065
|
* 返回功能单元列表,含涉及端、共享能力、依赖关系
|
|
2969
3066
|
*/
|
|
2970
3067
|
function parseFunctionMap(content, allPlatforms) {
|
|
3068
|
+
// v7.4.2+: 先尝试表格格式
|
|
3069
|
+
const tableResult = parseFunctionMapTable(content, allPlatforms);
|
|
3070
|
+
if (tableResult.length > 0)
|
|
3071
|
+
return tableResult;
|
|
3072
|
+
// v7.4.2+: 回退到树形格式解析
|
|
3073
|
+
return parseFunctionMapTree(content, allPlatforms);
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* v7.4.2+: 解析 FUNCTION_MAP.md 表格格式
|
|
3077
|
+
*/
|
|
3078
|
+
function parseFunctionMapTable(content, allPlatforms) {
|
|
2971
3079
|
const units = [];
|
|
2972
3080
|
const lines = content.split('\n');
|
|
2973
3081
|
let inTable = false;
|
|
@@ -2976,10 +3084,8 @@ function parseFunctionMap(content, allPlatforms) {
|
|
|
2976
3084
|
let dependsOnColIdx = -1;
|
|
2977
3085
|
let descColIdx = -1;
|
|
2978
3086
|
for (const line of lines) {
|
|
2979
|
-
// 检测映射表开始(通过表头特征)
|
|
2980
3087
|
if (line.includes('功能单元') && line.includes('涉及端')) {
|
|
2981
3088
|
inTable = true;
|
|
2982
|
-
// 解析表头,找到各列索引
|
|
2983
3089
|
const headerCells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
2984
3090
|
platformColIdx = headerCells.findIndex(c => c.includes('涉及端'));
|
|
2985
3091
|
sharedCapColIdx = headerCells.findIndex(c => c.includes('共享能力'));
|
|
@@ -2989,47 +3095,33 @@ function parseFunctionMap(content, allPlatforms) {
|
|
|
2989
3095
|
}
|
|
2990
3096
|
if (!inTable)
|
|
2991
3097
|
continue;
|
|
2992
|
-
|
|
2993
|
-
if (line.startsWith('## ') && !line.includes('功能映射')) {
|
|
3098
|
+
if (line.startsWith('## ') && !line.includes('功能映射'))
|
|
2994
3099
|
break;
|
|
2995
|
-
}
|
|
2996
3100
|
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
2997
3101
|
if (cells.length < 3)
|
|
2998
3102
|
continue;
|
|
2999
|
-
// 分隔行跳过
|
|
3000
3103
|
if (cells.every(c => /^[-:]+$/.test(c)))
|
|
3001
3104
|
continue;
|
|
3002
|
-
|
|
3003
|
-
if (cells.some(c => c.includes('功能单元')) || cells.some(c => c.includes('涉及端'))) {
|
|
3105
|
+
if (cells.some(c => c.includes('功能单元')) || cells.some(c => c.includes('涉及端')))
|
|
3004
3106
|
continue;
|
|
3005
|
-
}
|
|
3006
|
-
// 数据行:第2列是功能单元名
|
|
3007
3107
|
const unitName = cells[1];
|
|
3008
3108
|
if (!unitName || unitName === '#' || unitName === '功能单元')
|
|
3009
3109
|
continue;
|
|
3010
|
-
// 解析涉及端
|
|
3011
3110
|
let platforms = [];
|
|
3012
3111
|
if (platformColIdx >= 0 && platformColIdx < cells.length) {
|
|
3013
3112
|
const raw = cells[platformColIdx];
|
|
3014
3113
|
if (raw && raw !== '无' && raw !== '—' && raw !== '-') {
|
|
3015
|
-
platforms = raw.split(/[,,]/)
|
|
3016
|
-
.map(p => p.trim())
|
|
3017
|
-
.filter(p => p && allPlatforms.includes(p));
|
|
3114
|
+
platforms = raw.split(/[,,]/).map(p => p.trim()).filter(p => p && allPlatforms.includes(p));
|
|
3018
3115
|
}
|
|
3019
3116
|
}
|
|
3020
|
-
|
|
3021
|
-
if (platforms.length === 0) {
|
|
3117
|
+
if (platforms.length === 0)
|
|
3022
3118
|
platforms = [...allPlatforms];
|
|
3023
|
-
}
|
|
3024
|
-
// 解析共享能力
|
|
3025
3119
|
let sharedCapability = '无';
|
|
3026
3120
|
if (sharedCapColIdx >= 0 && sharedCapColIdx < cells.length) {
|
|
3027
3121
|
const raw = cells[sharedCapColIdx];
|
|
3028
|
-
if (raw && raw !== '无' && raw !== '—' && raw !== '-')
|
|
3122
|
+
if (raw && raw !== '无' && raw !== '—' && raw !== '-')
|
|
3029
3123
|
sharedCapability = raw;
|
|
3030
|
-
}
|
|
3031
3124
|
}
|
|
3032
|
-
// 解析依赖任务
|
|
3033
3125
|
let dependsOn = [];
|
|
3034
3126
|
if (dependsOnColIdx >= 0 && dependsOnColIdx < cells.length) {
|
|
3035
3127
|
const raw = cells[dependsOnColIdx];
|
|
@@ -3037,15 +3129,62 @@ function parseFunctionMap(content, allPlatforms) {
|
|
|
3037
3129
|
dependsOn = raw.split(/[,,]/).map(p => p.trim()).filter(Boolean);
|
|
3038
3130
|
}
|
|
3039
3131
|
}
|
|
3040
|
-
// 解析说明
|
|
3041
3132
|
let description = '';
|
|
3042
|
-
if (descColIdx >= 0 && descColIdx < cells.length)
|
|
3133
|
+
if (descColIdx >= 0 && descColIdx < cells.length)
|
|
3043
3134
|
description = cells[descColIdx];
|
|
3044
|
-
}
|
|
3045
3135
|
units.push({ name: unitName, platforms, sharedCapability, dependsOn, description });
|
|
3046
3136
|
}
|
|
3047
3137
|
return units;
|
|
3048
3138
|
}
|
|
3139
|
+
/**
|
|
3140
|
+
* v7.4.2+: 解析 FUNCTION_MAP.md 树形格式
|
|
3141
|
+
* 匹配: ├── 会议室 CRUD .............. room-service ✅ | admin-web ✅
|
|
3142
|
+
* 提取: 模块名="会议室 CRUD", 涉及端=[room-service, admin-web]
|
|
3143
|
+
*/
|
|
3144
|
+
function parseFunctionMapTree(content, allPlatforms) {
|
|
3145
|
+
const units = [];
|
|
3146
|
+
const lines = content.split('\n');
|
|
3147
|
+
// 树形节点正则:匹配 ├── 或 └── 开头的行
|
|
3148
|
+
const treeNodeRe = /[├└]──\s*/;
|
|
3149
|
+
// 分隔点正则:匹配连续 .... 或 ———
|
|
3150
|
+
const separatorRe = /\s*[.·]{3,}\s*/;
|
|
3151
|
+
for (const line of lines) {
|
|
3152
|
+
if (!treeNodeRe.test(line))
|
|
3153
|
+
continue;
|
|
3154
|
+
// 提取树节点后的文本
|
|
3155
|
+
const afterTree = line.replace(/.*[├└]──\s*/, '').trim();
|
|
3156
|
+
if (!afterTree)
|
|
3157
|
+
continue;
|
|
3158
|
+
// 跳过分类型节点(如 ├── 🏢 会议室管理),这些是父级分类
|
|
3159
|
+
// 父级节点通常没有 ... 分隔符和平台标记
|
|
3160
|
+
if (!separatorRe.test(afterTree) && !afterTree.includes('✅') && !afterTree.includes('❌') && !afterTree.includes('⚠️')) {
|
|
3161
|
+
continue; // 这是分类父节点,跳过
|
|
3162
|
+
}
|
|
3163
|
+
// 分割模块名和平台部分
|
|
3164
|
+
const parts = afterTree.split(separatorRe);
|
|
3165
|
+
if (parts.length < 2)
|
|
3166
|
+
continue;
|
|
3167
|
+
// 模块名:去除 emoji 和空格
|
|
3168
|
+
let unitName = parts[0].replace(/[🏢📅✅💰🔔👥📊⚙️🔑🛡️]/g, '').trim();
|
|
3169
|
+
if (!unitName || unitName.length < 2)
|
|
3170
|
+
continue;
|
|
3171
|
+
// 平台部分:提取所有标准端名
|
|
3172
|
+
const platformPart = parts.slice(1).join(' ');
|
|
3173
|
+
const platforms = [];
|
|
3174
|
+
for (const p of allPlatforms) {
|
|
3175
|
+
if (platformPart.includes(p)) {
|
|
3176
|
+
platforms.push(p);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
// 如果没匹配到标准端名,尝试匹配常见缩写
|
|
3180
|
+
if (platforms.length === 0) {
|
|
3181
|
+
// 回退全端
|
|
3182
|
+
platforms.push(...allPlatforms);
|
|
3183
|
+
}
|
|
3184
|
+
units.push({ name: unitName, platforms, sharedCapability: '无', dependsOn: [], description: '' });
|
|
3185
|
+
}
|
|
3186
|
+
return units;
|
|
3187
|
+
}
|
|
3049
3188
|
/**
|
|
3050
3189
|
* 尝试模块驱动拆分:从功能模块创建任务目录结构
|
|
3051
3190
|
* 成功返回 true,无功能模块时返回 false(回退到传统流程)
|
|
@@ -3242,7 +3381,7 @@ async function tryModuleDrivenSplit(iteration, iterationDir, options) {
|
|
|
3242
3381
|
await generateImpactGraph(iterationDir, createdSections, allPlatforms);
|
|
3243
3382
|
logger_1.logger.info(`\n 📊 创建了 ${createdSections.length} 个任务(每端一个子任务)`);
|
|
3244
3383
|
// 生成内容填充提示
|
|
3245
|
-
const fillPrompt = buildContentFillingPrompt(iteration, iterationDir, createdSections, allPlatforms);
|
|
3384
|
+
const fillPrompt = await buildContentFillingPrompt(iteration, iterationDir, createdSections, allPlatforms);
|
|
3246
3385
|
const promptsDir = (0, path_1.join)('.speccore', 'prompts');
|
|
3247
3386
|
await (0, fs_extra_1.ensureDir)(promptsDir);
|
|
3248
3387
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(promptsDir, `split-content-${iteration}.md`), fillPrompt);
|
|
@@ -3258,22 +3397,152 @@ async function tryModuleDrivenSplit(iteration, iterationDir, options) {
|
|
|
3258
3397
|
catch { }
|
|
3259
3398
|
return true;
|
|
3260
3399
|
}
|
|
3400
|
+
/**
|
|
3401
|
+
* v7.4.2+: 按功能单元从各分析文档中精准提取相关上下文
|
|
3402
|
+
* CLI 做计算(提取),AI 做内容(填充)
|
|
3403
|
+
*/
|
|
3404
|
+
async function assembleUnitContext(iterationDir, unitName, platforms) {
|
|
3405
|
+
const specsBase = (0, path_1.join)(iterationDir, '020-specs');
|
|
3406
|
+
const overviewDir = (0, path_1.join)(specsBase, spec_paths_1.GLOBAL_SPECS_DIR);
|
|
3407
|
+
const ctx = [];
|
|
3408
|
+
// 关键词拆分:将功能单元名拆成多个搜索词(如「预订管理」→「预订」「管理」)
|
|
3409
|
+
const keywords = unitName.split(/(?<=[\u4e00-\u9fff])(?=[\u4e00-\u9fff])/).filter(k => k.length >= 1);
|
|
3410
|
+
// 也加入英文关键词(如 booking、checkin)
|
|
3411
|
+
const slugKeywords = slugify(unitName).split('-').filter(k => k.length >= 3);
|
|
3412
|
+
const allKeywords = [...keywords, ...slugKeywords];
|
|
3413
|
+
const matchLine = (line) => allKeywords.some(k => line.includes(k));
|
|
3414
|
+
// 1. 从 overview/REQUIREMENT.md 提取相关章节
|
|
3415
|
+
const reqPath = (0, path_1.join)(overviewDir, 'REQUIREMENT.md');
|
|
3416
|
+
if (await (0, fs_extra_1.pathExists)(reqPath)) {
|
|
3417
|
+
try {
|
|
3418
|
+
const content = await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
|
|
3419
|
+
const lines = content.split('\n');
|
|
3420
|
+
const relevant = [];
|
|
3421
|
+
let inRelevantSection = false;
|
|
3422
|
+
for (const line of lines) {
|
|
3423
|
+
if (line.startsWith('## ') || line.startsWith('### ')) {
|
|
3424
|
+
inRelevantSection = matchLine(line);
|
|
3425
|
+
}
|
|
3426
|
+
if (inRelevantSection || (line.startsWith('|') && matchLine(line))) {
|
|
3427
|
+
relevant.push(line);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
if (relevant.length > 0) {
|
|
3431
|
+
ctx.push(`### 📋 需求规格(REQUIREMENT.md 相关片段)\n\`\`\`\n${relevant.slice(0, 60).join('\n')}\n\`\`\``);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
catch { }
|
|
3435
|
+
}
|
|
3436
|
+
// 2. 从 platforms/_shared/_MODULES.md 提取相关模块
|
|
3437
|
+
const modulesPath = (0, path_1.join)(specsBase, 'platforms', '_shared', '_MODULES.md');
|
|
3438
|
+
if (await (0, fs_extra_1.pathExists)(modulesPath)) {
|
|
3439
|
+
try {
|
|
3440
|
+
const content = await (0, fs_extra_1.readFile)(modulesPath, 'utf-8');
|
|
3441
|
+
const lines = content.split('\n');
|
|
3442
|
+
const relevant = [];
|
|
3443
|
+
let inRelevantBlock = false;
|
|
3444
|
+
let blockLines = [];
|
|
3445
|
+
for (const line of lines) {
|
|
3446
|
+
if (line.startsWith('## ')) {
|
|
3447
|
+
// 保存上一个块
|
|
3448
|
+
if (inRelevantBlock && blockLines.length > 0) {
|
|
3449
|
+
relevant.push(...blockLines);
|
|
3450
|
+
}
|
|
3451
|
+
inRelevantBlock = matchLine(line);
|
|
3452
|
+
blockLines = [line];
|
|
3453
|
+
}
|
|
3454
|
+
else if (inRelevantBlock) {
|
|
3455
|
+
blockLines.push(line);
|
|
3456
|
+
}
|
|
3457
|
+
else if (line.startsWith('|') && matchLine(line)) {
|
|
3458
|
+
relevant.push(line);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
if (inRelevantBlock && blockLines.length > 0)
|
|
3462
|
+
relevant.push(...blockLines);
|
|
3463
|
+
if (relevant.length > 0) {
|
|
3464
|
+
ctx.push(`### 🧩 功能模块(_MODULES.md 相关片段)\n\`\`\`\n${relevant.slice(0, 50).join('\n')}\n\`\`\``);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
catch { }
|
|
3468
|
+
}
|
|
3469
|
+
// 3. 从各端 _INDEX.md 提取相关接口/页面
|
|
3470
|
+
for (const platform of platforms) {
|
|
3471
|
+
const indexPath = (0, path_1.join)(specsBase, 'platforms', platform, '_INDEX.md');
|
|
3472
|
+
if (await (0, fs_extra_1.pathExists)(indexPath)) {
|
|
3473
|
+
try {
|
|
3474
|
+
const content = await (0, fs_extra_1.readFile)(indexPath, 'utf-8');
|
|
3475
|
+
const lines = content.split('\n');
|
|
3476
|
+
const relevant = [];
|
|
3477
|
+
let inRelevantSection = false;
|
|
3478
|
+
for (const line of lines) {
|
|
3479
|
+
if (line.startsWith('## ') || line.startsWith('### ')) {
|
|
3480
|
+
inRelevantSection = matchLine(line);
|
|
3481
|
+
}
|
|
3482
|
+
if (inRelevantSection || (line.startsWith('|') && matchLine(line))) {
|
|
3483
|
+
relevant.push(line);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
if (relevant.length > 0) {
|
|
3487
|
+
ctx.push(`### 🖥️ ${platform} 端相关(_INDEX.md 片段)\n\`\`\`\n${relevant.slice(0, 40).join('\n')}\n\`\`\``);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
catch { }
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
// 4. 从 overview/FUNCTION_MAP.md 提取本单元行 + 依赖单元
|
|
3494
|
+
const funcMapPath = (0, path_1.join)(overviewDir, 'FUNCTION_MAP.md');
|
|
3495
|
+
if (await (0, fs_extra_1.pathExists)(funcMapPath)) {
|
|
3496
|
+
try {
|
|
3497
|
+
const content = await (0, fs_extra_1.readFile)(funcMapPath, 'utf-8');
|
|
3498
|
+
const lines = content.split('\n');
|
|
3499
|
+
const matchedRows = lines.filter(l => l.startsWith('|') && matchLine(l));
|
|
3500
|
+
if (matchedRows.length > 0) {
|
|
3501
|
+
ctx.push(`### 🗺️ 功能映射(FUNCTION_MAP.md)\n\`\`\`\n${matchedRows.join('\n')}\n\`\`\``);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
catch { }
|
|
3505
|
+
}
|
|
3506
|
+
// 5. 从 overview/TECH.md 提取相关架构信息
|
|
3507
|
+
const techPath = (0, path_1.join)(overviewDir, 'TECH.md');
|
|
3508
|
+
if (await (0, fs_extra_1.pathExists)(techPath)) {
|
|
3509
|
+
try {
|
|
3510
|
+
const content = await (0, fs_extra_1.readFile)(techPath, 'utf-8');
|
|
3511
|
+
const lines = content.split('\n');
|
|
3512
|
+
const relevant = [];
|
|
3513
|
+
let inRelevantSection = false;
|
|
3514
|
+
for (const line of lines) {
|
|
3515
|
+
if (line.startsWith('## ') || line.startsWith('### ')) {
|
|
3516
|
+
inRelevantSection = matchLine(line);
|
|
3517
|
+
}
|
|
3518
|
+
if (inRelevantSection)
|
|
3519
|
+
relevant.push(line);
|
|
3520
|
+
}
|
|
3521
|
+
if (relevant.length > 0) {
|
|
3522
|
+
ctx.push(`### 🏗️ 技术架构相关(TECH.md 片段)\n\`\`\`\n${relevant.slice(0, 40).join('\n')}\n\`\`\``);
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
catch { }
|
|
3526
|
+
}
|
|
3527
|
+
if (ctx.length === 0) {
|
|
3528
|
+
return `> 功能单元「${unitName}」未在分析文档中找到直接相关内容,请根据全局文档推断。`;
|
|
3529
|
+
}
|
|
3530
|
+
return ctx.join('\n\n');
|
|
3531
|
+
}
|
|
3261
3532
|
/**
|
|
3262
3533
|
* 生成内容填充 Prompt — AI 为预创建的任务填充 REQ.md/TECH.md
|
|
3534
|
+
* v7.4.2+: 每个功能单元注入精准上下文,而非让 AI 自己从全量文档中查找
|
|
3263
3535
|
*/
|
|
3264
|
-
function buildContentFillingPrompt(iteration, iterationDir, sections, allPlatforms) {
|
|
3536
|
+
async function buildContentFillingPrompt(iteration, iterationDir, sections, allPlatforms) {
|
|
3265
3537
|
let p = `# 任务内容填充(模块驱动拆分)\n\n`;
|
|
3266
3538
|
p += `> 迭代: ${iteration} | 任务数: ${sections.length} | 端: ${allPlatforms.join(', ')}\n\n`;
|
|
3267
3539
|
p += `## 说明\n\n`;
|
|
3268
|
-
p += `CLI
|
|
3540
|
+
p += `CLI 已按功能单元×端创建了任务目录结构,并为每个功能单元从分析文档中提取了相关上下文。\n`;
|
|
3269
3541
|
p += `你的任务是为每个子任务填充 REQ.md 和 TECH.md。\n\n`;
|
|
3270
|
-
p += `##
|
|
3542
|
+
p += `## 全局上下文\n\n`;
|
|
3271
3543
|
p += `1. Read .speccore/CONSTITUTION.md — 项目配置\n`;
|
|
3272
|
-
p += `2. Read 020-specs/overview/
|
|
3273
|
-
p +=
|
|
3274
|
-
p += `4. Read 020-specs/overview/TECH.md — 迭代综合技术架构\n`;
|
|
3275
|
-
p += `5. Read 020-specs/{端}/TECH.md — 各端专属技术方案\n\n`;
|
|
3276
|
-
p += `## 任务清单\n\n`;
|
|
3544
|
+
p += `2. Read 020-specs/overview/ANALYSIS.md — 迭代综合分析报告\n\n`;
|
|
3545
|
+
p += `## 任务清单(含精准上下文)\n\n`;
|
|
3277
3546
|
for (const sec of sections) {
|
|
3278
3547
|
const taskId = sec._taskId || sec.name;
|
|
3279
3548
|
const sourceFile = sec._sourceFile || '';
|
|
@@ -3290,15 +3559,20 @@ function buildContentFillingPrompt(iteration, iterationDir, sections, allPlatfor
|
|
|
3290
3559
|
p += ` - ${platform}/*/REQ.md — 子任务需求规格\n`;
|
|
3291
3560
|
p += ` - ${platform}/*/TECH.md — 子任务技术方案\n`;
|
|
3292
3561
|
}
|
|
3562
|
+
// v7.4.2+: 注入该功能单元的精准上下文
|
|
3563
|
+
const unitCtx = await assembleUnitContext(iterationDir, featureName, modPlatforms);
|
|
3564
|
+
p += `\n#### 📎 功能单元「${featureName}」相关上下文\n\n${unitCtx}\n\n`;
|
|
3293
3565
|
p += `\n`;
|
|
3294
3566
|
}
|
|
3295
3567
|
p += `## 填充规则\n\n`;
|
|
3296
3568
|
p += `1. 先 Read 子任务目录下的 TASK.md(已有基本信息)和 .meta/feature(功能单元名)\n`;
|
|
3297
|
-
p += `2. REQ.md:
|
|
3298
|
-
p += `3. TECH.md:
|
|
3569
|
+
p += `2. REQ.md: 基于上方提供的「功能单元相关上下文」,撰写本子任务的需求规格(验收标准、业务规则、边界条件)\n`;
|
|
3570
|
+
p += `3. TECH.md: 基于上下文中的接口定义和数据模型,细化本子任务的技术方案\n`;
|
|
3299
3571
|
p += `4. 用 Write 工具直接写入对应路径\n`;
|
|
3300
|
-
p += `5. 同一功能模块的各端子任务要保持 API
|
|
3301
|
-
p += `6. 禁止产出垃圾内容——每个文件必须有实质性专业内容\n
|
|
3572
|
+
p += `5. 同一功能模块的各端子任务要保持 API 契约一致(前后端接口签名必须匹配)\n`;
|
|
3573
|
+
p += `6. 禁止产出垃圾内容——每个文件必须有实质性专业内容\n`;
|
|
3574
|
+
p += `7. REQ.md 必须包含:功能描述、验收标准(Given/When/Then)、业务规则、边界条件、异常场景\n`;
|
|
3575
|
+
p += `8. TECH.md 必须包含:接口定义(请求/响应结构体)、数据模型(字段/类型/约束)、核心算法/逻辑、依赖说明\n\n`;
|
|
3302
3576
|
p += `## ⚠️ 绝对禁止\n\n`;
|
|
3303
3577
|
p += `- 不要创建新目录 — 目录已由 CLI 创建\n`;
|
|
3304
3578
|
p += `- 不要修改 .meta/ 下的文件\n`;
|