speccore 8.3.253 → 8.3.254

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.
@@ -0,0 +1,4676 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.iterationTaskifyCommand = iterationTaskifyCommand;
37
+ const fs_extra_1 = require("fs-extra");
38
+ const path_1 = require("path");
39
+ const logger_1 = require("../../utils/logger");
40
+ const context_1 = require("../../core/context");
41
+ const feature_source_1 = require("../../core/feature-source");
42
+ const risk_scorer_1 = require("../../core/risk-scorer");
43
+ const global_counters_1 = require("../../core/global-counters");
44
+ const task_utils_1 = require("../../utils/task-utils");
45
+ const next_steps_1 = require("../../core/next-steps");
46
+ const readline_1 = require("readline");
47
+ const prompt_builder_1 = require("../../core/prompt-builder");
48
+ const index_guard_1 = require("../../core/index-guard");
49
+ const spec_paths_1 = require("../../core/spec-paths");
50
+ const spec_skeleton_1 = require("../../core/spec-skeleton");
51
+ const questions_1 = require("../../core/questions");
52
+ const pipeline_engine_1 = require("../../core/pipeline-engine");
53
+ const agent_adapter_1 = require("../../core/agent-adapter");
54
+ const git_integration_1 = require("../../core/git-integration");
55
+ const cleanup_1 = require("../cleanup");
56
+ const unified_config_1 = require("../../core/unified-config");
57
+ const ui_verify_1 = require("../../core/ui-verify");
58
+ // v8.3.125+: 公共同义词扩展模块(从 split.ts 提取,供全链路使用)
59
+ const synonyms_1 = require("../../utils/synonyms");
60
+ const unit_context_assembler_1 = require("../../core/unit-context-assembler");
61
+ /**
62
+ * 将 AI 返回的 scope 简写映射到 CONSTITUTION.md 标准端名
63
+ * v6.69.3+: 解决中文简写(如 "后端"、"admin")导致目录名错误的问题
64
+ */
65
+ function normalizeScopePlatforms(scopeArr, standardPlatforms) {
66
+ const result = [];
67
+ const used = new Set();
68
+ for (const raw of scopeArr) {
69
+ const s = raw.trim();
70
+ if (!s)
71
+ continue;
72
+ // 1. 如果已经是标准端名,直接使用
73
+ if (standardPlatforms.includes(s)) {
74
+ if (!used.has(s)) {
75
+ result.push(s);
76
+ used.add(s);
77
+ }
78
+ continue;
79
+ }
80
+ // 2. 后端类简写 → 匹配第一个后端端名
81
+ if (/后端|backend|服务端|server|api/i.test(s)) {
82
+ const backendPlatform = standardPlatforms.find(p => /-(service|api|server|backend)$/i.test(p) || p.startsWith('后台'));
83
+ if (backendPlatform && !used.has(backendPlatform)) {
84
+ result.push(backendPlatform);
85
+ used.add(backendPlatform);
86
+ }
87
+ continue;
88
+ }
89
+ // 3. 前端类简写 → 模糊匹配标准端名
90
+ const lower = s.toLowerCase();
91
+ const matched = standardPlatforms.find(p => {
92
+ const pl = p.toLowerCase();
93
+ // 直接包含关系:admin-web 包含 admin
94
+ return pl.includes(lower) || lower.includes(pl);
95
+ });
96
+ if (matched && !used.has(matched)) {
97
+ result.push(matched);
98
+ used.add(matched);
99
+ continue;
100
+ }
101
+ // 4. 关键词匹配
102
+ const keywordMap = {
103
+ 'h5': ['h5-mobile', 'h5', 'mobile', '移动端'],
104
+ 'mobile': ['h5-mobile', 'h5', 'mobile', '移动端'],
105
+ '小程序': ['miniapp', 'mini-app', '小程序'],
106
+ 'admin': ['admin-web', 'admin', '后台管理'],
107
+ 'web': ['admin-web', 'web', 'h5-mobile'],
108
+ 'frontend': ['admin-web', 'h5-mobile', 'frontend'],
109
+ 'app': ['app', 'android', 'ios'],
110
+ '安卓': ['android'],
111
+ 'ios': ['ios'],
112
+ };
113
+ const candidates = keywordMap[lower];
114
+ if (candidates) {
115
+ const found = standardPlatforms.find(p => candidates.some(c => p.toLowerCase().includes(c.toLowerCase())));
116
+ if (found && !used.has(found)) {
117
+ result.push(found);
118
+ used.add(found);
119
+ continue;
120
+ }
121
+ }
122
+ // 5. v7.4.2+: 无法匹配时警告并跳过,不保留非标准端名
123
+ logger_1.logger.warn(` ⚠️ 非标准端名 "${s}",无法映射到 CONSTITUTION.md 标准端名 ${standardPlatforms.join('/')}`);
124
+ logger_1.logger.warn(` 已跳过该端,请检查 AI 输出的 scope 字段`);
125
+ }
126
+ return result.length > 0 ? result : standardPlatforms;
127
+ }
128
+ /** 将名称转为目录安全的短 slug(2-4 词)
129
+ * v8.3.77+: 优先提取英文/数字片段,不再直接去掉中文
130
+ */
131
+ function slugify(name) {
132
+ // 优先提取连续的英文/数字片段(如 "User管理" → "User")
133
+ const latinMatches = name.match(/[a-zA-Z0-9]+/g);
134
+ if (latinMatches && latinMatches.length > 0) {
135
+ return latinMatches.slice(0, 3).join('-').toLowerCase();
136
+ }
137
+ // 完全没有英文/数字 → 生成短 hash 作为 fallback
138
+ const hash = Math.abs(name.split('').reduce((a, c) => a * 31 + c.charCodeAt(0), 7)).toString(36);
139
+ return hash.slice(0, 6);
140
+ }
141
+ /** 从 AI 返回的任务中提取 topic,多层 fallback(v8.3.77+) */
142
+ function extractTopic(task) {
143
+ // 1. 优先使用 AI 提供的合法 topic(英文短横线格式)
144
+ if (task.topic && /^[a-z0-9-]+$/.test(task.topic)) {
145
+ return task.topic;
146
+ }
147
+ // 2. 尝试从 functionalUnit 提取英文片段
148
+ if (task.functionalUnit) {
149
+ const latin = task.functionalUnit.match(/[a-zA-Z0-9]+/g);
150
+ if (latin && latin.length > 0) {
151
+ return latin.slice(0, 3).join('-').toLowerCase();
152
+ }
153
+ }
154
+ // 3. 尝试从 name 提取英文片段
155
+ if (task.name) {
156
+ const latin = task.name.match(/[a-zA-Z0-9]+/g);
157
+ if (latin && latin.length > 0) {
158
+ return latin.slice(0, 3).join('-').toLowerCase();
159
+ }
160
+ }
161
+ // 4. fallback 到 hash
162
+ return slugify(task.name || task.functionalUnit || 'task');
163
+ }
164
+ /**
165
+ * 生成子任务全局唯一 ID
166
+ * 格式: Task-{父任务ID}-{端名}
167
+ * 例: Task-001-booking-service, Task-001-h5-mobile
168
+ */
169
+ function generateSubtaskId(parentTaskId, platform) {
170
+ // v6.49.5+ / v8.3.10+:确定性格式 {taskId}-{platform},保证全项目唯一
171
+ // 因为每个任务每个端只有一个子任务,所以 {taskId}-{platform} 已经唯一
172
+ return `Task-${parentTaskId}-${platform}`;
173
+ }
174
+ /** 拆分约束常量(简化版:每个功能单元按涉及的端拆分,每端1个子任务) */
175
+ const SPLIT_RULES = {
176
+ maxTasksPerIteration: 20, // 单次迭代总任务数上限
177
+ maxTasksPerUnit: 3, // 每个功能单元最多任务数
178
+ };
179
+ function promptUser(question) {
180
+ const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
181
+ return new Promise(resolve => {
182
+ rl.question(`${question} `, answer => { rl.close(); resolve(answer.trim()); });
183
+ });
184
+ }
185
+ async function detectPlatforms(iterationDir, specified) {
186
+ if (specified)
187
+ return specified.split(',').map(p => p.trim()).filter(Boolean);
188
+ // 1. 优先从 CONSTITUTION.md「端列表」章节读取(v6.46.0+ 显式声明)
189
+ const platforms = await (0, spec_paths_1.parsePlatformList)();
190
+ if (platforms.length > 0)
191
+ return platforms;
192
+ // v8.3.32 修复:默认不再硬编码 ['web'],返回空数组并提示用户配置
193
+ logger_1.logger.warn(`⚠️ CONSTITUTION.md 中未找到「端列表」或工程标识表格`);
194
+ logger_1.logger.warn(` 请先在 CONSTITUTION.md 中配置「项目信息」表格(含工程标识列)`);
195
+ logger_1.logger.warn(` 或在 .speccore/PROJECT.yaml 中配置 platforms 字段`);
196
+ return [];
197
+ }
198
+ async function preSplitGate(iterationDir) {
199
+ const items = [];
200
+ const specDir = (0, path_1.join)(iterationDir, '020-specs');
201
+ // 检查全局 FUNCTION_MAP.md(警告级)
202
+ // v8.3.220+: 降级为 warn——功能单元的权威来源是 010-requirements/features/ 目录(doc2spec --split 产出),
203
+ // FUNCTION_MAP 仅作「功能×端映射」参考。split 的 prompt 构建已有多级回退(FUNCTION_MAP → ANALYSIS.md → features/)。
204
+ const funcMapPath = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR, 'FUNCTION_MAP.md');
205
+ const funcMapExists = await (0, fs_extra_1.pathExists)(funcMapPath);
206
+ let funcMapIsSkeleton = false;
207
+ if (funcMapExists) {
208
+ const content = await (0, fs_extra_1.readFile)(funcMapPath, 'utf-8');
209
+ funcMapIsSkeleton = content.includes('<!-- SPEC-SKELETON -->') || content.trim().length < 100;
210
+ }
211
+ items.push({
212
+ path: funcMapPath,
213
+ name: 'FUNCTION_MAP.md',
214
+ purpose: '功能单元×端映射参考(权威来源为 010-requirements/features/ 目录)',
215
+ impact: '缺失或不完整时仅作提示,split 以 features/ 目录为准拆分,不影响任务生成',
216
+ exists: funcMapExists,
217
+ isSkeleton: funcMapIsSkeleton,
218
+ level: 'warn',
219
+ });
220
+ // 检查全局 REQUIREMENT.md(阻塞级——taskify 的核心需求上下文,骨架则无法拆分)
221
+ const reqPath = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR, 'REQUIREMENT.md');
222
+ const reqExists = await (0, fs_extra_1.pathExists)(reqPath);
223
+ let reqIsSkeleton = false;
224
+ if (reqExists) {
225
+ const content = await (0, fs_extra_1.readFile)(reqPath, 'utf-8');
226
+ reqIsSkeleton = content.includes('<!-- SPEC-SKELETON -->') || content.trim().length < 100;
227
+ }
228
+ items.push({
229
+ path: reqPath,
230
+ name: 'REQUIREMENT.md',
231
+ purpose: '本迭代的完整需求规格,包含功能模块清单和验收标准(taskify 拆分任务的核心依据)',
232
+ impact: '缺失或仍为骨架时,AI 拆分缺少需求上下文,会拆出无意义任务(如把「写作要求」当功能模块)',
233
+ exists: reqExists,
234
+ isSkeleton: reqIsSkeleton,
235
+ level: 'block',
236
+ });
237
+ // v8.3.221+: 检查全局 ANALYSIS.md(阻塞级——功能分析,骨架说明 analyze 未落盘)
238
+ // v8.3.248+: 模块级回退——v8.3.238 后 analyze 指示 AI 把 ANALYSIS.md 写在 {feature}/overview/ 下
239
+ // (全局 overview 只留索引类文档),若仍硬性要求全局文件,新流程将永远阻塞无法拆分。
240
+ // 全局缺失/骨架时,任一功能模块的 overview/ANALYSIS.md 非骨架即视为分析已落盘。
241
+ const analysisGatePath = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR, 'ANALYSIS.md');
242
+ let analysisGateExists = await (0, fs_extra_1.pathExists)(analysisGatePath);
243
+ let analysisGateIsSkeleton = false;
244
+ if (analysisGateExists) {
245
+ const ac = await (0, fs_extra_1.readFile)(analysisGatePath, 'utf-8');
246
+ analysisGateIsSkeleton = ac.includes('<!-- SPEC-SKELETON -->') || ac.trim().length < 100;
247
+ }
248
+ if (!analysisGateExists || analysisGateIsSkeleton) {
249
+ try {
250
+ const fList = await (0, spec_paths_1.parseFeatureList)(iterationDir);
251
+ for (const f of fList) {
252
+ const fAnalysisPath = (0, path_1.join)(specDir, f, 'overview', 'ANALYSIS.md');
253
+ if (!(await (0, fs_extra_1.pathExists)(fAnalysisPath)))
254
+ continue;
255
+ const c = await (0, fs_extra_1.readFile)(fAnalysisPath, 'utf-8');
256
+ if (!c.includes('<!-- SPEC-SKELETON -->') && c.trim().length >= 100) {
257
+ analysisGateExists = true;
258
+ analysisGateIsSkeleton = false;
259
+ break;
260
+ }
261
+ }
262
+ }
263
+ catch { /* 回退失败保持原判定 */ }
264
+ }
265
+ items.push({
266
+ path: analysisGatePath,
267
+ name: 'ANALYSIS.md',
268
+ purpose: '需求功能分析(功能点清单 F-xx),taskify 拆分任务的输入之一',
269
+ impact: '仍为骨架说明 analyze 阶段未落盘,拆分会遗漏功能点',
270
+ exists: analysisGateExists,
271
+ isSkeleton: analysisGateIsSkeleton,
272
+ level: 'block',
273
+ });
274
+ // 检查功能模块级文档(警告级)
275
+ const features = await (0, spec_paths_1.parseFeatureList)(iterationDir);
276
+ for (const feature of features) {
277
+ const featureReqPath = (0, path_1.join)(specDir, feature, 'overview', 'REQUIREMENT.md');
278
+ const featureReqExists = await (0, fs_extra_1.pathExists)(featureReqPath);
279
+ let featureReqIsSkeleton = false;
280
+ if (featureReqExists) {
281
+ const content = await (0, fs_extra_1.readFile)(featureReqPath, 'utf-8');
282
+ featureReqIsSkeleton = content.includes('<!-- SPEC-SKELETON -->') || content.trim().length < 100;
283
+ }
284
+ items.push({
285
+ path: featureReqPath,
286
+ name: `${feature}/overview/REQUIREMENT.md`,
287
+ purpose: `功能模块「${feature}」的完整需求(合并各端视角)`,
288
+ impact: `缺失时,execute 阶段 AI 读不到「${feature}」的详细需求,可能实现不完整`,
289
+ exists: featureReqExists,
290
+ isSkeleton: featureReqIsSkeleton,
291
+ level: 'warn',
292
+ });
293
+ }
294
+ const blockers = items.filter(i => i.level === 'block' && (!i.exists || i.isSkeleton));
295
+ const warnings = items.filter(i => i.level === 'warn' && (!i.exists || i.isSkeleton));
296
+ return { pass: blockers.length === 0, blockers, warnings };
297
+ }
298
+ function printPreSplitReport(iterationDir, blockers, warnings) {
299
+ logger_1.logger.info('');
300
+ logger_1.logger.info('╔══════════════════════════════════════════════════════════════╗');
301
+ logger_1.logger.info('║ Split 前置检查报告 ║');
302
+ logger_1.logger.info('╚══════════════════════════════════════════════════════════════╝');
303
+ if (blockers.length > 0) {
304
+ logger_1.logger.error(`\n🔴 阻塞项 (${blockers.length} 个) — 必须修复后才能拆分:`);
305
+ for (const b of blockers) {
306
+ logger_1.logger.error(`\n 📄 ${b.name}`);
307
+ logger_1.logger.error(` 路径: ${(0, path_1.relative)(iterationDir, b.path)}`);
308
+ logger_1.logger.error(` 作用: ${b.purpose}`);
309
+ logger_1.logger.error(` 影响: ${b.impact}`);
310
+ if (!b.exists) {
311
+ logger_1.logger.error(` 状态: ❌ 文件缺失`);
312
+ }
313
+ else if (b.isSkeleton) {
314
+ logger_1.logger.error(` 状态: ⚠️ 仍为骨架(待 AI 填充)`);
315
+ }
316
+ }
317
+ }
318
+ if (warnings.length > 0) {
319
+ logger_1.logger.warn(`\n🟡 警告项 (${warnings.length} 个) — 建议修复:`);
320
+ for (const w of warnings) {
321
+ logger_1.logger.warn(`\n 📄 ${w.name}`);
322
+ logger_1.logger.warn(` 路径: ${(0, path_1.relative)(iterationDir, w.path)}`);
323
+ logger_1.logger.warn(` 作用: ${w.purpose}`);
324
+ logger_1.logger.warn(` 影响: ${w.impact}`);
325
+ if (!w.exists) {
326
+ logger_1.logger.warn(` 状态: ❌ 文件缺失`);
327
+ }
328
+ else if (w.isSkeleton) {
329
+ logger_1.logger.warn(` 状态: ⚠️ 仍为骨架(待 AI 填充)`);
330
+ }
331
+ }
332
+ }
333
+ logger_1.logger.info('');
334
+ logger_1.logger.info('💡 修复建议:');
335
+ logger_1.logger.info(' 运行 speccore analyze -I <迭代名> --apply');
336
+ logger_1.logger.info(' 让 AI 补充缺失的分析产物,然后再执行 split');
337
+ }
338
+ async function iterationTaskifyCommand(options) {
339
+ // ── Pipeline 模式 ──
340
+ if (options.pipeline) {
341
+ const iter = options.iteration || await (0, context_1.getDefaultIteration)() || '';
342
+ if (!iter) {
343
+ logger_1.logger.error('--pipeline 需要 --iteration');
344
+ return;
345
+ }
346
+ // 检查是否为恢复模式
347
+ if (options.resume) {
348
+ const hasPipeline = await pipeline_engine_1.PipelineEngine.hasActivePipeline(process.cwd(), iter);
349
+ if (hasPipeline) {
350
+ const { createSplitPipeline } = await Promise.resolve().then(() => __importStar(require('../../core/pipeline-engine')));
351
+ const { engine } = await createSplitPipeline(iter);
352
+ // v8.3.160+: 默认步骤隔离模式
353
+ const result = await engine.advance();
354
+ if (result.isComplete) {
355
+ logger_1.logger.success('🎉 Split Pipeline 完成!');
356
+ await engine.reset();
357
+ return;
358
+ }
359
+ else if (result.nextStepId) {
360
+ logger_1.logger.info('');
361
+ logger_1.logger.info(`🔄 Split Pipeline 推进到: ${result.nextStepName || result.nextStepId}`);
362
+ logger_1.logger.info('');
363
+ // v8.3.160+: 步骤隔离模式输出标记 + 子 Agent 激活
364
+ const nextCmd = `speccore iteration taskify -I ${iter} --resume --pipeline`;
365
+ const prompt = await (0, prompt_builder_1.buildPrompt)('split', {
366
+ iteration: iter,
367
+ // v8.3.160+: 传递步骤级上下文控制
368
+ contextType: result.contextType,
369
+ contextBudget: result.contextBudget,
370
+ });
371
+ const output = [
372
+ `[SPECCORE_STEP_DONE]`,
373
+ `步骤 "${result.nextStepName || result.nextStepId}" 已完成。`,
374
+ ``,
375
+ `[SPECCORE_NEXT_STEP]`,
376
+ `下一步: ${result.nextStepName || result.nextStepId}`,
377
+ `请在同一会话继续执行: ${nextCmd}`,
378
+ ``,
379
+ `[SPECCORE_CONTEXT_SNAPSHOT]`,
380
+ JSON.stringify(result.contextSnapshot || {}),
381
+ ``,
382
+ // v8.3.160+: 子 Agent 激活标记
383
+ ...(result.subagent ? [
384
+ `[SPECCORE_SESSION_AGENT: ${result.subagent}]`,
385
+ `[SPECCORE_CONTEXT_BUDGET: ${result.contextBudget || 12000}]`,
386
+ `[SPECCORE_CONTEXT_TYPE: ${result.contextType || 'full'}]`,
387
+ ``,
388
+ ] : []),
389
+ `--- 下一步 Prompt ---`,
390
+ typeof prompt === 'string' ? prompt : JSON.stringify(prompt),
391
+ ].join('\n');
392
+ process.stdout.write(output);
393
+ process.exitCode = 10;
394
+ return;
395
+ // 根据当前步骤生成相应提示
396
+ if (result.nextStepId === 'prompt-analysis') {
397
+ const prompt = await (0, prompt_builder_1.buildPrompt)('split', { iteration: iter });
398
+ // 添加 Pipeline 继续指令
399
+ // v8.3.160+: 步骤隔离模式,每步完成后需新会话继续
400
+ const finalPrompt = prompt + `\n\n## ⚠️ Pipeline 模式:步骤隔离\n\n` +
401
+ `当前是 Split Pipeline 模式。当你通过 --response 写入任务拆分结果后,` +
402
+ `CLI 会自动输出 [SPECCORE_STEP_DONE] 标记,提示你在新会话中执行下一步。\n\n` +
403
+ `**看到 [SPECCORE_STEP_DONE] 后,请在同一会话继续执行下一步命令**。\n`;
404
+ process.stdout.write(`[SPECCORE_PROMPT]\n${finalPrompt}`);
405
+ process.exitCode = 10;
406
+ return;
407
+ }
408
+ }
409
+ }
410
+ else {
411
+ logger_1.logger.warn('没有找到活跃的 Split Pipeline,启动新的 Pipeline...');
412
+ }
413
+ }
414
+ // 初始化新的 Pipeline
415
+ const { createSplitPipeline } = await Promise.resolve().then(() => __importStar(require('../../core/pipeline-engine')));
416
+ const { engine } = await createSplitPipeline(iter);
417
+ await engine.init('init');
418
+ // 生成第一步 prompt
419
+ const prompt = await (0, prompt_builder_1.buildPrompt)('split', { iteration: iter });
420
+ // 添加 Pipeline 继续指令
421
+ // v8.3.160+: 步骤隔离模式,每步完成后需新会话继续
422
+ const finalPrompt = prompt + `\n\n## ⚠️ Pipeline 模式:步骤隔离\n\n` +
423
+ `当前是 Split Pipeline 模式。当你通过 --response 写入任务拆分结果后,` +
424
+ `CLI 会自动输出 [SPECCORE_STEP_DONE] 标记,提示你在新会话中执行下一步。\n\n` +
425
+ `**看到 [SPECCORE_STEP_DONE] 后,请在同一会话继续执行下一步命令**。\n`;
426
+ process.stdout.write(`[SPECCORE_PROMPT]\n${finalPrompt}`);
427
+ process.exitCode = 10;
428
+ return;
429
+ }
430
+ // ── Prompt 模式 ──
431
+ if (options.prompt) {
432
+ const iter = options.iteration || await (0, context_1.getDefaultIteration)() || '';
433
+ const prompt = await (0, prompt_builder_1.buildPrompt)('split', { iteration: iter });
434
+ // v8.3.167+: 检测功能模块数量,大项目启用分批拆分
435
+ let featureBatchHint = '';
436
+ try {
437
+ const iterDirForSplit = await (0, context_1.getIterationDir)(iter);
438
+ if (iterDirForSplit) {
439
+ const { parseFeatureList } = await Promise.resolve().then(() => __importStar(require('../../core/spec-paths')));
440
+ const featureList = await parseFeatureList(iterDirForSplit);
441
+ if (featureList.length > 2) {
442
+ logger_1.logger.info(`📦 检测到 ${featureList.length} 个功能模块,启用分批拆分模式`);
443
+ featureBatchHint = `\n\n## 📦 功能模块分批拆分(v8.3.167+)\n\n`;
444
+ featureBatchHint += `当前迭代包含 ${featureList.length} 个功能模块:${featureList.join('、')}\n\n`;
445
+ featureBatchHint += `**分批策略**:\n`;
446
+ featureBatchHint += `- 本次只拆分 **1-2 个功能模块**(优先拆分基础模块:认证/配置/数据库)\n`;
447
+ featureBatchHint += `- 确保每个模块的拆分质量(任务定义清晰、依赖关系明确)\n`;
448
+ featureBatchHint += `- 剩余模块在 JSON 输出后追加续批标记\n\n`;
449
+ featureBatchHint += `**续批标记格式**:\n`;
450
+ featureBatchHint += `\`\`\`\n[PENDING: 模块A, 模块B, 模块C]\n`;
451
+ featureBatchHint += `[SPECCORE_EXEC: speccore iteration taskify --prompt -I ${iter}]\n`;
452
+ featureBatchHint += `\`\`\`\n\n`;
453
+ featureBatchHint += `**重要**:不要一次性拆分所有模块,确保当前批次质量后再续批。\n`;
454
+ }
455
+ }
456
+ }
457
+ catch { /* 忽略功能模块检测失败 */ }
458
+ // v8.3.166+: 输出 subagent 标记(task-decomposer)
459
+ // v8.3.169+: 尝试通过 Qoder SDK 直接调度子 Agent
460
+ const promptText = (0, prompt_builder_1.formatPrompt)(prompt) + featureBatchHint;
461
+ let llmHandled = false;
462
+ let aiProvider = 'host';
463
+ try {
464
+ const { loadConfigWithMeta } = await Promise.resolve().then(() => __importStar(require('../../core/unified-config')));
465
+ const { config } = await loadConfigWithMeta();
466
+ aiProvider = config.settings.ai?.provider || 'host';
467
+ }
468
+ catch { /* 默认 host */ }
469
+ // v8.3.196+: 用自配 LLM 生成任务 JSON,成功设 llmHandled(供 llm 优先 / auto 回退复用)
470
+ const tryLlmTaskify = async () => {
471
+ try {
472
+ const { callLlm, resolveAiProviders } = await Promise.resolve().then(() => __importStar(require('../../core/ask-llm')));
473
+ const providers = await resolveAiProviders('taskify');
474
+ const llmContent = await callLlm('你是任务拆解专家。请按 prompt 要求拆分任务,只输出 JSON 数组格式的任务列表,不要输出 JSON 以外的文字。', promptText, providers, 4000);
475
+ if (llmContent) {
476
+ options.response = llmContent;
477
+ llmHandled = true;
478
+ return true;
479
+ }
480
+ }
481
+ catch (e) {
482
+ logger_1.logger.debug(`[split] 自配 LLM 生成失败: ${e.message}`);
483
+ }
484
+ return false;
485
+ };
486
+ // llm 优先模式:先用自配 LLM,失败回退宿主
487
+ if (aiProvider === 'llm') {
488
+ if (!(await tryLlmTaskify())) {
489
+ logger_1.logger.warn('⚠️ [llm] 自配 LLM 未生成任务,回退宿主模式');
490
+ }
491
+ }
492
+ if (!llmHandled && !process.stdout.isTTY) {
493
+ try {
494
+ const agentCtx = {
495
+ subagent: 'task-decomposer',
496
+ iteration: iter || '',
497
+ contextBudget: 12000,
498
+ contextType: 'full',
499
+ cwd: process.cwd(),
500
+ };
501
+ const dispatchResult = await (0, agent_adapter_1.dispatchSubagent)(agentCtx, promptText);
502
+ if (dispatchResult && dispatchResult.success) {
503
+ logger_1.logger.info(`[split] 子 Agent task-decomposer SDK 调度成功`);
504
+ process.stdout.write(`\n[SPECCORE_RESULT] 子 Agent task-decomposer 通过 Qoder SDK 完成执行\n`);
505
+ if (dispatchResult.filesChanged && dispatchResult.filesChanged.length > 0) {
506
+ process.stdout.write(`📄 文件变更: ${dispatchResult.filesChanged.join(', ')}\n`);
507
+ }
508
+ // v8.3.212+: SDK 直连真闭环——返回内容自动落盘(复用 --response 流程,设 llmHandled 避免重复输出 prompt)
509
+ if (dispatchResult.content && dispatchResult.content.trim()) {
510
+ options.response = dispatchResult.content;
511
+ llmHandled = true;
512
+ }
513
+ else {
514
+ process.stdout.write(`\n> 请审查结果后手动 speccore iteration taskify --response 写入任务\n`);
515
+ process.exitCode = 0;
516
+ return;
517
+ }
518
+ }
519
+ // 宿主 SDK 调度失败(success=false)→ auto 时回退自配 LLM
520
+ if (aiProvider === 'auto')
521
+ await tryLlmTaskify();
522
+ }
523
+ catch (e) {
524
+ logger_1.logger.debug(`[split] SDK 调度尝试失败: ${e.message}`);
525
+ // 宿主异常 → auto 时回退自配 LLM
526
+ if (aiProvider === 'auto')
527
+ await tryLlmTaskify();
528
+ }
529
+ }
530
+ if (!llmHandled) {
531
+ const output = [
532
+ `[SPECCORE_SESSION_AGENT: task-decomposer]`,
533
+ `[SPECCORE_CONTEXT_BUDGET: 12000]`,
534
+ `[SPECCORE_CONTEXT_TYPE: full]`,
535
+ ``,
536
+ promptText,
537
+ ].join('\n');
538
+ process.stdout.write(output);
539
+ process.exitCode = 10;
540
+ return;
541
+ }
542
+ }
543
+ // ── Response 模式 ──
544
+ if (options.response) {
545
+ // 检查是否有活跃的 Pipeline(在 Pipeline 模式下应用响应)
546
+ const iter = options.iteration || await (0, context_1.getDefaultIteration)() || '';
547
+ if (!iter) {
548
+ logger_1.logger.error('--response 需要 --iteration');
549
+ return;
550
+ }
551
+ const hasPipeline = await pipeline_engine_1.PipelineEngine.hasActivePipeline(process.cwd(), iter);
552
+ if (hasPipeline) {
553
+ const { createSplitPipeline } = await Promise.resolve().then(() => __importStar(require('../../core/pipeline-engine')));
554
+ const { engine } = await createSplitPipeline(iter);
555
+ // v8.3.160+: 默认步骤隔离模式
556
+ const stepResult = await engine.advance();
557
+ const state = await engine.getState();
558
+ if (state?.currentStep === 'done') {
559
+ logger_1.logger.success('🎉 Split Pipeline 完成!');
560
+ await engine.reset();
561
+ }
562
+ else if (state?.currentStep) {
563
+ logger_1.logger.info('');
564
+ logger_1.logger.info(`🔄 Split Pipeline 推进: ${state.currentStep}`);
565
+ logger_1.logger.info('');
566
+ // 生成下一步 prompt
567
+ let nextPrompt;
568
+ if (state.currentStep === 'creation') {
569
+ // 在创建阶段,我们已完成拆分,可以生成创建任务的提示
570
+ nextPrompt = `任务拆分已完成,正在创建任务目录结构...\n\n请继续执行后续操作。`;
571
+ }
572
+ else {
573
+ // v8.3.160+: 传递步骤级上下文控制,确保 Pipeline 配置的 budget/type 生效
574
+ const promptResult = await (0, prompt_builder_1.buildPrompt)('split', {
575
+ iteration: iter,
576
+ contextType: stepResult.contextType,
577
+ contextBudget: stepResult.contextBudget,
578
+ });
579
+ nextPrompt = typeof promptResult === 'string' ? promptResult : JSON.stringify(promptResult);
580
+ }
581
+ // v8.3.160+: 步骤隔离模式输出标记 + 子 Agent 激活
582
+ const nextCmd = `speccore iteration taskify -I ${iter} --resume --pipeline`;
583
+ const output = [
584
+ `[SPECCORE_STEP_DONE]`,
585
+ `步骤 "${state.currentStep}" 已完成。`,
586
+ ``,
587
+ `[SPECCORE_NEXT_STEP]`,
588
+ `下一步: ${state.currentStep}`,
589
+ `请在同一会话继续执行: ${nextCmd}`,
590
+ ``,
591
+ `[SPECCORE_CONTEXT_SNAPSHOT]`,
592
+ JSON.stringify(stepResult.contextSnapshot || {}),
593
+ ``,
594
+ // v8.3.160+: 子 Agent 激活标记
595
+ ...(stepResult.subagent ? [
596
+ `[SPECCORE_SESSION_AGENT: ${stepResult.subagent}]`,
597
+ `[SPECCORE_CONTEXT_BUDGET: ${stepResult.contextBudget || 12000}]`,
598
+ `[SPECCORE_CONTEXT_TYPE: ${stepResult.contextType || 'full'}]`,
599
+ ``,
600
+ ] : []),
601
+ `--- 下一步 Prompt ---`,
602
+ nextPrompt,
603
+ ].join('\n');
604
+ process.stdout.write(output);
605
+ process.exitCode = 10;
606
+ }
607
+ return;
608
+ }
609
+ // 非 Pipeline 模式的 Response 处理
610
+ // v8.3.4+ 修复:使用 getIterationDir 解析正确路径,避免 Iteration- 前缀重复
611
+ const iterDirFull = await (0, context_1.getIterationDir)(iter);
612
+ const iterDir = (0, path_1.join)(iterDirFull, '030-tasks');
613
+ await (0, fs_extra_1.ensureDir)(iterDir);
614
+ const backups = [];
615
+ // v8.3.163+: AI 上下文中禁用 --force(质量门禁和安全限制不可被 AI 自行绕过)
616
+ const isAiCtx = !process.stdout.isTTY;
617
+ if (options.force && isAiCtx) {
618
+ logger_1.logger.error('❌ AI 上下文中禁止使用 --force 参数');
619
+ logger_1.logger.info(' 质量门禁(任务数限制、拆分粒度校验)不可被 AI 自行绕过');
620
+ logger_1.logger.info(' 如需强制继续,请在交互式终端中执行此命令');
621
+ return;
622
+ }
623
+ // v8.3.160+: 智能提取 JSON(支持 JSON 后附带 [PENDING] / [SPECCORE_EXEC] 标记)
624
+ function extractJsonArray(text) {
625
+ const result = { json: text };
626
+ // 提取 [PENDING: ...]
627
+ const pendingMatch = text.match(/\[PENDING:\s*([^\]]+)\]/);
628
+ if (pendingMatch)
629
+ result.pending = pendingMatch[1].trim();
630
+ // 提取 [SPECCORE_EXEC: ...]
631
+ const execMatch = text.match(/\[SPECCORE_EXEC:\s*([^\]]+)\]/);
632
+ if (execMatch)
633
+ result.exec = execMatch[1].trim();
634
+ // 提取 JSON 数组(从第一个 [ 到最后一个 ])
635
+ const jsonMatch = text.match(/(\[[\s\S]*\])/);
636
+ if (jsonMatch)
637
+ result.json = jsonMatch[1];
638
+ return result;
639
+ }
640
+ const extracted = extractJsonArray(options.response);
641
+ // 尝试解析 AI 返回的 JSON Task 列表
642
+ try {
643
+ const tasks = JSON.parse(extracted.json);
644
+ if (Array.isArray(tasks)) {
645
+ const allPlatforms = await detectPlatforms(iterDirFull);
646
+ const sections = [];
647
+ for (let i = 0; i < tasks.length; i++) {
648
+ const task = tasks[i];
649
+ // 📌 核心原则校验:同一功能单元内的任务是否应该合并
650
+ // 如果 AI 把同一个功能单元拆成过多任务,给出警告
651
+ if (i > 0) {
652
+ const prevTask = tasks[i - 1];
653
+ const sameUnit = (task.functionalUnit && prevTask.functionalUnit &&
654
+ task.functionalUnit === prevTask.functionalUnit);
655
+ if (sameUnit) {
656
+ logger_1.logger.warn(` ⚠️ 同一功能单元 "${task.functionalUnit}" 已有多个任务:`);
657
+ logger_1.logger.warn(` Task ${i}: ${prevTask.name}`);
658
+ logger_1.logger.warn(` Task ${i + 1}: ${task.name}`);
659
+ logger_1.logger.warn(` 💡 建议:同一功能单元的不同子模块应该尽量合并为一个任务`);
660
+ }
661
+ }
662
+ // 将 AI JSON 转换为 Section,复用 createTaskFromSection 创建完整目录
663
+ const desc = task.description || task.name || '';
664
+ const scopeArr = Array.isArray(task.scope) ? task.scope : [];
665
+ const scope = scopeArr.join(', ');
666
+ const apis = Array.isArray(task.apis) ? task.apis.join('\n') : '';
667
+ const acs = Array.isArray(task.acceptanceCriteria) ? task.acceptanceCriteria.join('\n') : '';
668
+ let content = desc;
669
+ if (scope)
670
+ content += `\n\n范围: ${scope}`;
671
+ if (apis)
672
+ content += `\n\n接口:\n${apis}`;
673
+ if (acs)
674
+ content += `\n\n验收标准:\n${acs}`;
675
+ // v6.69.3+: 将 AI 返回的 scope 简写映射到标准端名
676
+ const taskScopePlatforms = normalizeScopePlatforms(scopeArr, allPlatforms);
677
+ const section = {
678
+ name: task.name || `Task ${i + 1}`,
679
+ content,
680
+ level: 2,
681
+ platform: taskScopePlatforms[0] || undefined,
682
+ };
683
+ section._complexity = {
684
+ estimatedHours: task.estimatedHours || 8,
685
+ priority: task.priority || 'medium',
686
+ };
687
+ section._owner = task.owner || '未分配';
688
+ // v8.3.239+: 任务类型收敛为 4 种(与 task-paths.ts TASK_TYPES 对齐)——
689
+ // security 按 feature 交付(安全加固是完整交付,威胁分析归入 TECH.md 安全章节),
690
+ // performance 按 refactor 执行(性能优化本质是重构)
691
+ const rawType = task.type;
692
+ let taskType = (rawType && ['feature', 'bugfix', 'refactor', 'research'].includes(rawType)) ? rawType : 'feature';
693
+ if (rawType === 'security') {
694
+ taskType = 'feature';
695
+ logger_1.logger.info(` 🔒 任务「${task.name}」类型 security 收敛为 feature(安全加固按完整交付)`);
696
+ }
697
+ if (rawType === 'performance') {
698
+ taskType = 'refactor';
699
+ logger_1.logger.info(` ⚡ 任务「${task.name}」类型 performance 收敛为 refactor(性能优化按重构执行)`);
700
+ }
701
+ section._taskType = taskType;
702
+ // 保存 topic slug,用于生成任务目录名(v8.3.77+: 多层提取)
703
+ section._topic = extractTopic(task);
704
+ // 保存 AI 生成的实际内容(用于写入 REQ.md / TECH.md)
705
+ if (task.reqContent)
706
+ section._reqContent = task.reqContent;
707
+ if (task.techContent)
708
+ section._techContent = task.techContent;
709
+ // v7.4.0+: 保存 AI 生成的 DEV_GUIDE 内容
710
+ if (task.devGuideContent)
711
+ section._devGuideContent = task.devGuideContent;
712
+ if (task.sourceFile)
713
+ section._sourceFile = task.sourceFile;
714
+ if (task.functionalUnit)
715
+ section.functionalUnit = task.functionalUnit;
716
+ if (task.reason)
717
+ section._reason = task.reason;
718
+ if (taskScopePlatforms.length > 0)
719
+ section._scopePlatforms = taskScopePlatforms;
720
+ sections.push(section);
721
+ }
722
+ // v8.3.223+: 过滤骨架质量标准标题泄漏为功能单元名(spec-skeleton 注入的引导标题
723
+ // 「📋 质量标准({docName})」被 AI 当功能单元,产出垃圾任务名如 Task-NNN-requirement-md)
724
+ const rubricFiltered = sections.filter(s => {
725
+ const name = (s.functionalUnit || s.name || '').trim();
726
+ return !/^📋\s*质量标准/.test(name) && !/^质量标准\s*([^)]*)$/.test(name);
727
+ });
728
+ if (rubricFiltered.length < sections.length) {
729
+ const removed = sections.length - rubricFiltered.length;
730
+ logger_1.logger.warn(` 🧹 已过滤 ${removed} 个「质量标准」骨架标题伪功能单元(AI 未删除骨架引导标题)`);
731
+ logger_1.logger.warn(' 请检查 AI 拆分结果是否覆盖真实功能单元(参考 FUNCTION_MAP.md)');
732
+ sections.length = 0;
733
+ sections.push(...rubricFiltered);
734
+ if (sections.length === 0) {
735
+ logger_1.logger.error(' ❌ 过滤后无有效功能单元,拆分中止。请重新拆分并告诉 AI:不要将「质量标准」骨架标题作为功能单元');
736
+ return;
737
+ }
738
+ }
739
+ // v8.3.4+: 校验所有任务的 scope 是否只含标准端名
740
+ const invalidScopes = [];
741
+ for (const sec of sections) {
742
+ const scopes = sec._scopePlatforms || [];
743
+ for (const s of scopes) {
744
+ if (!allPlatforms.includes(s))
745
+ invalidScopes.push(`任务 "${sec.name}" 的 scope "${s}"`);
746
+ }
747
+ }
748
+ if (invalidScopes.length > 0) {
749
+ logger_1.logger.error(`❌ 发现 ${invalidScopes.length} 个非标准端名,拆分被拒绝:`);
750
+ for (const msg of invalidScopes)
751
+ logger_1.logger.error(` ${msg}`);
752
+ logger_1.logger.error(` 标准端名: ${allPlatforms.join(', ')}`);
753
+ logger_1.logger.info(` 💡 请告诉 AI:scope 必须使用 CONSTITUTION.md 标准端名,禁止 api/web/backend 等简写`);
754
+ return;
755
+ }
756
+ // v8.3.4+: 功能点覆盖校验 — 对比 FUNCTION_MAP.md 检查是否有遗漏
757
+ const funcMapPath = (0, path_1.join)(iterDirFull, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'FUNCTION_MAP.md');
758
+ if (await (0, fs_extra_1.pathExists)(funcMapPath)) {
759
+ try {
760
+ const fmContent = await (0, fs_extra_1.readFile)(funcMapPath, 'utf-8');
761
+ const funcUnits = parseFunctionMap(fmContent, allPlatforms);
762
+ if (funcUnits.length > 0) {
763
+ const aiUnits = new Set();
764
+ for (const t of tasks) {
765
+ const unit = t.functionalUnit || t.name;
766
+ if (unit)
767
+ aiUnits.add(unit.trim());
768
+ }
769
+ const missingUnits = [];
770
+ for (const u of funcUnits) {
771
+ // 模糊匹配:如果 AI 任务中没有功能单元名包含或被包含于 FUNCTION_MAP 中的名称
772
+ const matched = [...aiUnits].some(aiu => aiu.toLowerCase().includes(u.name.toLowerCase()) ||
773
+ u.name.toLowerCase().includes(aiu.toLowerCase()));
774
+ if (!matched)
775
+ missingUnits.push(u.name);
776
+ }
777
+ if (missingUnits.length > 0) {
778
+ logger_1.logger.warn(`\n ⚠️ 功能点覆盖警告:${missingUnits.length} 个功能单元可能被遗漏`);
779
+ for (const mu of missingUnits.slice(0, 5))
780
+ logger_1.logger.warn(` 📌 ${mu}`);
781
+ if (missingUnits.length > 5)
782
+ logger_1.logger.warn(` ... 还有 ${missingUnits.length - 5} 个`);
783
+ logger_1.logger.warn(` 💡 建议检查:这些功能点是否已合并到其他任务中?`);
784
+ logger_1.logger.warn(` 💡 如确需补充,重新执行 split 并告诉 AI:"补充以下功能点: ${missingUnits.slice(0, 3).join(', ')}"`);
785
+ }
786
+ }
787
+ }
788
+ catch { }
789
+ }
790
+ // 检测已有任务 + 冲突处理
791
+ const existingTasks = await detectExistingTasks(iterDirFull);
792
+ if (existingTasks.length > 0 && !options.force) {
793
+ logger_1.logger.warn(` ⚠️ 已有 ${existingTasks.length} 个任务: ${existingTasks.slice(0, 5).join(', ')}...`);
794
+ logger_1.logger.info(' 使用 --force 强制覆盖');
795
+ return;
796
+ }
797
+ // --force 清理旧任务(避免新旧叠加编号暴增)
798
+ if (options.force) {
799
+ const tasksRoot = (0, path_1.join)(iterDirFull, '030-tasks');
800
+ if (await (0, fs_extra_1.pathExists)(tasksRoot)) {
801
+ const entries = await (0, fs_extra_1.readdir)(tasksRoot, { withFileTypes: true });
802
+ for (const entry of entries) {
803
+ if (entry.isDirectory()) {
804
+ await (0, fs_extra_1.remove)((0, path_1.join)(tasksRoot, entry.name));
805
+ }
806
+ }
807
+ logger_1.logger.info(` 🗑 已清理旧任务目录`);
808
+ }
809
+ }
810
+ // 🚨 全局任务数硬限制(安全网:防止 AI 输出爆炸)
811
+ const MAX_TASKS_HARD = SPLIT_RULES.maxTasksPerIteration;
812
+ if (sections.length > MAX_TASKS_HARD && !options.force) {
813
+ logger_1.logger.error(`\n ❌ 任务数爆炸!AI 输出了 ${sections.length} 个任务(上限 ${MAX_TASKS_HARD})`);
814
+ logger_1.logger.error(` 💡 这说明拆分粒度过细,必须合并。请告诉 AI:"任务太多,请合并相关功能,总数控制在 ${MAX_TASKS_HARD} 以内"`);
815
+ logger_1.logger.error(` 🔧 如确实需要,使用 --force 跳过检查(不推荐)`);
816
+ logger_1.logger.info('\n ℹ️ 如需强制继续,添加 --force 参数');
817
+ return;
818
+ }
819
+ if (sections.length > MAX_TASKS_HARD && options.force) {
820
+ logger_1.logger.warn(` ⚠️ 任务数 ${sections.length} 超出建议上限 ${MAX_TASKS_HARD},--force 已启用,继续...`);
821
+ }
822
+ // v8.3.162+: 逐功能单元×端校验:每个功能单元的每个端最多 3 个任务
823
+ // 拆分策略:功能单元先按端拆分,每个端内的子任务最多 3 个
824
+ const MAX_TASKS_PER_UNIT_PLATFORM = 3;
825
+ const unitPlatformTaskCount = {};
826
+ let missingFunctionalUnit = 0;
827
+ // 按 functionalUnit × platform 分组统计
828
+ for (let i = 0; i < sections.length; i++) {
829
+ const task = tasks[i];
830
+ const unitName = task.functionalUnit;
831
+ const scopes = Array.isArray(task.scope) ? task.scope : [];
832
+ if (!unitName) {
833
+ missingFunctionalUnit++;
834
+ // 未标注功能单元的任务,按 scope 分别计入 __missing__
835
+ for (const platform of scopes) {
836
+ const key = `__missing__|${platform}`;
837
+ unitPlatformTaskCount[key] = (unitPlatformTaskCount[key] || 0) + 1;
838
+ }
839
+ }
840
+ else {
841
+ for (const platform of scopes) {
842
+ const key = `${unitName}|${platform}`;
843
+ unitPlatformTaskCount[key] = (unitPlatformTaskCount[key] || 0) + 1;
844
+ }
845
+ }
846
+ }
847
+ // functionalUnit 缺失警告
848
+ if (missingFunctionalUnit > sections.length * 0.5) {
849
+ logger_1.logger.warn(` ⚠️ ${missingFunctionalUnit}/${sections.length} 个任务缺少 functionalUnit 字段`);
850
+ logger_1.logger.warn(` 💡 AI 未按约束填写功能单元,校验可能不准确`);
851
+ logger_1.logger.warn(` 💡 建议重新执行 split,确保 Prompt 包含 functionalUnit 要求`);
852
+ }
853
+ // 检查每个功能单元×端的任务数
854
+ let hasOverSplit = false;
855
+ for (const [key, count] of Object.entries(unitPlatformTaskCount)) {
856
+ if (count > MAX_TASKS_PER_UNIT_PLATFORM) {
857
+ if (!hasOverSplit) {
858
+ logger_1.logger.error(`\n ❌ 检测到过度拆分!某些功能单元在单个端内拆出过多任务:`);
859
+ hasOverSplit = true;
860
+ }
861
+ const [unitName, platform] = key.split('|');
862
+ const displayUnit = unitName === '__missing__' ? '(未标注功能单元)' : unitName;
863
+ logger_1.logger.error(` 📌 "${displayUnit}" 在 "${platform}" 端拆出了 ${count} 个任务(上限 ${MAX_TASKS_PER_UNIT_PLATFORM})`);
864
+ }
865
+ }
866
+ if (hasOverSplit) {
867
+ logger_1.logger.error(` 💡 核心原则:功能单元先按端拆分,每个端内默认 1 个任务,最多 3 个`);
868
+ logger_1.logger.error(` 🔧 建议操作:`);
869
+ logger_1.logger.error(` 1. 重新执行 split,并告诉 AI:"任务太多,请合并相关功能"`);
870
+ logger_1.logger.error(` 2. 或者手动编辑 .speccore/prompts/split-suggestion-${iter}.md,明确要求"每个功能单元的每个端最多拆 2 个任务"`);
871
+ logger_1.logger.error(` 3. 如果确实需要这么多任务,使用 --force 跳过检查(不推荐)`);
872
+ if (!options.force) {
873
+ logger_1.logger.info('\n ℹ️ 如需强制继续,添加 --force 参数');
874
+ return;
875
+ }
876
+ logger_1.logger.warn(' ⚠️ --force 已启用,继续创建所有任务...');
877
+ }
878
+ // v8.3.164+: 内容质量校验 — 拦截空壳任务(reqContent/techContent 实质内容不足)
879
+ // 问题:AI 返回的 reqContent/techContent 虽然有长度,但全是 AI-FILL 占位符和模板
880
+ const MIN_SUBSTANTIVE_CHARS = 80; // 实质内容最低字符数
881
+ const MAX_EMPTY_SHELL_RATIO = 0.5; // 空壳任务占比上限
882
+ let emptyShellCount = 0;
883
+ const emptyShellTasks = [];
884
+ for (let i = 0; i < sections.length; i++) {
885
+ const sec = sections[i];
886
+ const reqContent = sec._reqContent;
887
+ const techContent = sec._techContent;
888
+ // 计算实质内容字符数(排除 HTML 注释、AI-FILL 标记、空白行、markdown 格式符号)
889
+ const calcSubstantive = (text) => {
890
+ if (!text || text.trim().length === 0)
891
+ return 0;
892
+ // 去除 HTML 注释
893
+ let cleaned = text.replace(/<!--[\s\S]*?-->/g, '');
894
+ // 去除 AI-FILL 标记行
895
+ cleaned = cleaned.replace(/AI-FILL[\s\S]*?$/gm, '');
896
+ // 去除 markdown 标题符号
897
+ cleaned = cleaned.replace(/^#+\s*/gm, '');
898
+ // 去除表格分隔符
899
+ cleaned = cleaned.replace(/^[\|\-:\s]+$/gm, '');
900
+ // 去除空白行
901
+ cleaned = cleaned.replace(/^\s*$/gm, '');
902
+ // 去除 markdown 列表符号
903
+ cleaned = cleaned.replace(/^[-*+]\s+/gm, '');
904
+ // 去除代码块标记
905
+ cleaned = cleaned.replace(/^```\w*$/gm, '');
906
+ // 去除纯英文占位符(如 "TBD", "TODO", "待补充" 等)
907
+ cleaned = cleaned.replace(/\b(TBD|TODO|FIXME|HACK|XXX|待补充|待实现|待填写|待完善|待确认|待定)\b/gi, '');
908
+ // 计算剩余实质字符(中文字符 + 英文单词)
909
+ const cnChars = (cleaned.match(/[\u4e00-\u9fa5]/g) || []).length;
910
+ const enWords = (cleaned.match(/[a-zA-Z]{2,}/g) || []).length;
911
+ return cnChars + enWords;
912
+ };
913
+ const reqSubstantive = calcSubstantive(reqContent);
914
+ const techSubstantive = calcSubstantive(techContent);
915
+ // 判定为空壳:req 和 tech 都缺乏实质内容
916
+ if (reqSubstantive < MIN_SUBSTANTIVE_CHARS && techSubstantive < MIN_SUBSTANTIVE_CHARS) {
917
+ emptyShellCount++;
918
+ emptyShellTasks.push(`"${sec.name}" (req=${reqSubstantive}, tech=${techSubstantive})`);
919
+ }
920
+ }
921
+ const emptyRatio = sections.length > 0 ? emptyShellCount / sections.length : 0;
922
+ if (emptyShellCount > 0) {
923
+ if (emptyRatio > MAX_EMPTY_SHELL_RATIO) {
924
+ logger_1.logger.error(`\n ❌ 内容质量拦截:${emptyShellCount}/${sections.length} 个任务为空壳(占比 ${Math.round(emptyRatio * 100)}%,上限 ${Math.round(MAX_EMPTY_SHELL_RATIO * 100)}%)`);
925
+ logger_1.logger.error(` 💡 空壳任务特征:reqContent/techContent 全是 AI-FILL 占位符、模板内容或 "待补充"`);
926
+ logger_1.logger.error(` 🔧 建议操作:`);
927
+ logger_1.logger.error(` 1. 重新执行 split,并告诉 AI:"请为每个任务生成完整的需求规格和技术方案,不要只输出骨架"`);
928
+ logger_1.logger.error(` 2. 或者检查 analyze 阶段的 020-specs/ 是否有足够内容供 split 提取`);
929
+ for (const t of emptyShellTasks.slice(0, 5))
930
+ logger_1.logger.error(` • ${t}`);
931
+ if (emptyShellTasks.length > 5)
932
+ logger_1.logger.error(` ... 还有 ${emptyShellTasks.length - 5} 个`);
933
+ if (!options.force) {
934
+ logger_1.logger.info('\n ℹ️ 如需强制继续,添加 --force 参数');
935
+ return;
936
+ }
937
+ logger_1.logger.warn(' ⚠️ --force 已启用,继续创建空壳任务...');
938
+ }
939
+ else {
940
+ logger_1.logger.warn(`\n ⚠️ 内容质量警告:${emptyShellCount}/${sections.length} 个任务为空壳`);
941
+ for (const t of emptyShellTasks.slice(0, 3))
942
+ logger_1.logger.warn(` • ${t}`);
943
+ if (emptyShellTasks.length > 3)
944
+ logger_1.logger.warn(` ... 还有 ${emptyShellTasks.length - 3} 个`);
945
+ logger_1.logger.warn(` 💡 这些任务将在 execute 阶段依赖 AI 现场生成内容,可能增加开发时间`);
946
+ }
947
+ }
948
+ // 交互模式判断:显式 --interactive 或 stdin 是 TTY(--force 时跳过交互,直接执行)
949
+ const isInteractive = (options.interactive || process.stdin.isTTY) && !options.force;
950
+ // 非交互模式:显示任务总览摘要
951
+ if (!isInteractive) {
952
+ logger_1.logger.info(`\n 📋 任务总览(共 ${sections.length} 个):`);
953
+ const unitSummary = {};
954
+ for (let i = 0; i < tasks.length; i++) {
955
+ const unit = tasks[i].functionalUnit || '(未标注)';
956
+ if (!unitSummary[unit])
957
+ unitSummary[unit] = [];
958
+ unitSummary[unit].push(tasks[i].name || `Task ${i + 1}`);
959
+ }
960
+ for (const [unit, names] of Object.entries(unitSummary)) {
961
+ logger_1.logger.info(` 📌 ${unit} (${names.length} 个):`);
962
+ for (const n of names)
963
+ logger_1.logger.info(` - ${n}`);
964
+ }
965
+ logger_1.logger.info('');
966
+ }
967
+ // 逐任务交互确认
968
+ const createdSections = [];
969
+ for (let i = 0; i < sections.length; i++) {
970
+ const sec = sections[i];
971
+ const complexity = sec._complexity || {};
972
+ const taskType = sec._taskType || 'feature';
973
+ const deps = (tasks[i].dependencies || []);
974
+ const acs = (tasks[i].acceptanceCriteria || []);
975
+ // 展示任务摘要
976
+ logger_1.logger.info(`\n ━━━━ 任务 ${i + 1}/${sections.length} ━━━━`);
977
+ logger_1.logger.info(` 📌 ${sec.name}`);
978
+ logger_1.logger.info(` 🏷 类型: ${taskType} | 🎯 优先级: ${complexity.priority || 'medium'}`);
979
+ if (complexity.estimatedHours)
980
+ logger_1.logger.info(` ⏱ 预估: ${complexity.estimatedHours}h`);
981
+ if (deps.length > 0)
982
+ logger_1.logger.info(` 🔗 依赖: ${deps.join(', ')}`);
983
+ if (acs.length > 0) {
984
+ logger_1.logger.info(` ✅ 验收标准:`);
985
+ for (const ac of acs.slice(0, 5))
986
+ logger_1.logger.info(` ${ac}`);
987
+ }
988
+ // 交互确认(仅确认,调整应回到 AI 对话重新生成方案)
989
+ if (isInteractive) {
990
+ const answer = await promptUser(` 确认创建?(y/回车确认,n 调整方案):`);
991
+ if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
992
+ logger_1.logger.info(` 💡 如需调整,请告诉 AI:`);
993
+ logger_1.logger.info(` "把 XX 和 YY 合为一个任务" / "ZZ 任务太大,拆成两个" / "修改工时为 Xh"`);
994
+ logger_1.logger.info(` AI 会参考 .speccore/prompts/split-suggestion-${iter}.md 中的规则重新生成`);
995
+ logger_1.logger.info(` 调整后再次执行本命令即可`);
996
+ return;
997
+ }
998
+ }
999
+ // 使用保存的 topic slug 生成任务ID
1000
+ // v8.3.33: 优先使用 functionalUnit 作为 topic,使目录名更有意义
1001
+ const taskTopic = sec.functionalUnit || sec._topic || slugify(sec.name);
1002
+ const { id: taskId } = await (0, global_counters_1.nextTaskId)(sec.name, taskTopic);
1003
+ sec._taskId = taskId;
1004
+ await createTaskFromSection(iterDirFull, taskId, sec, allPlatforms, taskType, sections);
1005
+ createdSections.push(sec);
1006
+ logger_1.logger.info(` ✅ 创建: ${taskId} - [${taskType}] ${sec.name}`);
1007
+ }
1008
+ if (createdSections.length > 0) {
1009
+ await generateImpactGraph(iterDirFull, createdSections, allPlatforms);
1010
+ await updateProjectGraph(iterDirFull, createdSections);
1011
+ // 生成任务总览报告 → 000-overview/TASK_SUMMARY.md
1012
+ const summaryMd = await generateTaskSummary(iterDirFull, tasks, createdSections);
1013
+ logger_1.logger.info(` 📊 任务总览 → 000-overview/task-summaries/TASK_SUMMARY-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 16)}.md`);
1014
+ // 输出报告到 stdout,供宿主 AI 展示给用户
1015
+ process.stdout.write('\n[SPECCORE_TASK_SUMMARY]\n');
1016
+ process.stdout.write(summaryMd);
1017
+ process.stdout.write('\n[/SPECCORE_TASK_SUMMARY]\n');
1018
+ // 输出下一步操作标记:引导宿主 AI 对每个 Task 执行 analyze --task
1019
+ outputAnalyzeTaskHints(iter, createdSections);
1020
+ }
1021
+ logger_1.logger.success(`✅ 创建了 ${createdSections.length}/${sections.length} 个任务(${sections.length - createdSections.length} 个跳过)`);
1022
+ // v8.3.160+: 检测是否有待拆分的功能模块
1023
+ if (extracted.pending) {
1024
+ logger_1.logger.info('');
1025
+ logger_1.logger.info(`📦 还有未拆分的功能模块: ${extracted.pending}`);
1026
+ logger_1.logger.info(` 继续拆分: speccore iteration taskify --prompt -I ${iter}`);
1027
+ if (extracted.exec) {
1028
+ process.stdout.write(`\n[SPECCORE_EXEC: ${extracted.exec}]\n`);
1029
+ }
1030
+ }
1031
+ }
1032
+ else {
1033
+ logger_1.logger.warn('AI 返回格式非数组,将作为 Markdown 写入 REQUIREMENT.md');
1034
+ const reqPath = (0, path_1.join)(iterDir, 'REQUIREMENT.md');
1035
+ const bk = await (0, task_utils_1.backupWithTimestamp)(reqPath);
1036
+ if (bk) {
1037
+ backups.push(bk);
1038
+ logger_1.logger.info(` 📦 已备份: ${(0, path_1.basename)(bk)}`);
1039
+ }
1040
+ await (0, fs_extra_1.writeFile)(reqPath, options.response);
1041
+ }
1042
+ }
1043
+ catch {
1044
+ const reqPath = (0, path_1.join)(iterDir, 'REQUIREMENT.md');
1045
+ const bk = await (0, task_utils_1.backupWithTimestamp)(reqPath);
1046
+ if (bk) {
1047
+ backups.push(bk);
1048
+ logger_1.logger.info(` 📦 已备份: ${bk.split('/').pop()}`);
1049
+ }
1050
+ await (0, fs_extra_1.writeFile)(reqPath, options.response);
1051
+ }
1052
+ // v7.4.0+: 从 AI 输出中提取疑问并持久化
1053
+ const extractedQs = (0, questions_1.extractQuestionsFromText)(options.response);
1054
+ if (extractedQs.length > 0) {
1055
+ await (0, questions_1.writeQuestions)({ command: 'split', scope: iter }, extractedQs);
1056
+ }
1057
+ logger_1.logger.success('✅ 任务已创建');
1058
+ // 备份汇总
1059
+ if (backups.length > 0) {
1060
+ logger_1.logger.info('');
1061
+ logger_1.logger.info(`📦 备份文件 (${backups.length} 个):`);
1062
+ for (const bp of backups) {
1063
+ logger_1.logger.info(` ${bp}`);
1064
+ }
1065
+ logger_1.logger.info(' 💡 如不再需要可手动删除');
1066
+ }
1067
+ // 自动刷新知识图谱(v6.49.10+)
1068
+ try {
1069
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../../core/knowledge-graph')));
1070
+ await refreshKnowledgeGraph(process.cwd(), iter);
1071
+ logger_1.logger.info('🧠 知识图谱已刷新');
1072
+ }
1073
+ catch { }
1074
+ return;
1075
+ }
1076
+ // 命令前索引新鲜度检查(非阻塞)
1077
+ await (0, index_guard_1.warnIfIndexStale)(process.cwd(), 'split', options.iteration);
1078
+ const spinner = new logger_1.Spinner('Splitting requirements into tasks');
1079
+ spinner.start();
1080
+ try {
1081
+ const iteration = await (0, context_1.getDefaultIteration)(options.iteration);
1082
+ if (!iteration) {
1083
+ spinner.fail('No active iteration found. Please specify --iteration or create one first.');
1084
+ return;
1085
+ }
1086
+ const iterationDir = await (0, context_1.getIterationDir)(iteration);
1087
+ // v8.3.19+: Split 前置检查 — 确保分析产物完整后再拆分
1088
+ if (!options.prompt && !options.response) {
1089
+ const { pass, blockers, warnings } = await preSplitGate(iterationDir);
1090
+ if (!pass || warnings.length > 0) {
1091
+ spinner.stop();
1092
+ printPreSplitReport(iterationDir, blockers, warnings);
1093
+ if (!pass) {
1094
+ if (!options.interactive && !options.strict) {
1095
+ logger_1.logger.info('');
1096
+ logger_1.logger.info('🤖 自动模式:请先补充分析产物后再拆分');
1097
+ logger_1.logger.info(' 命令: speccore analyze -I <迭代名> --apply');
1098
+ }
1099
+ else {
1100
+ logger_1.logger.error('\n⛔ 拆分被拒绝:存在阻塞项,请先补充分析产物');
1101
+ }
1102
+ return;
1103
+ }
1104
+ // 只有警告,没有阻塞
1105
+ if (options.interactive || options.strict) {
1106
+ const proceed = await promptUser('\n仍要继续拆分?[y/N] ');
1107
+ if (!proceed || proceed.toLowerCase() !== 'y') {
1108
+ logger_1.logger.info('已取消拆分');
1109
+ return;
1110
+ }
1111
+ }
1112
+ else {
1113
+ logger_1.logger.info('\n🤖 自动模式:跳过警告,继续拆分');
1114
+ }
1115
+ spinner.start();
1116
+ }
1117
+ }
1118
+ // v6.76.0+: 变更检测 — 检查 020-specs/ 是否比 Task/ 更新
1119
+ if (!options.ignoreSpecsUpdate && !options.prompt && !options.response) {
1120
+ const { detectSpecChangesBeforeSplit, printChangeDetection } = await Promise.resolve().then(() => __importStar(require('../../core/change-detector')));
1121
+ const changeResult = await detectSpecChangesBeforeSplit(iterationDir);
1122
+ printChangeDetection(changeResult, '拆分前检测');
1123
+ }
1124
+ // ── v6.49.13+: 模块驱动拆分 — CLI 按功能模块×端创建任务目录,AI 只填充内容 ──
1125
+ if (!options.prompt && !options.response) {
1126
+ const moduleDrivenResult = await tryModuleDrivenSplit(iteration, iterationDir, options);
1127
+ if (moduleDrivenResult) {
1128
+ return;
1129
+ }
1130
+ }
1131
+ // ── 1. 检查 ANALYSIS.md + AI 智能拆分建议 ──
1132
+ // v8.3.249+: reqContent2/reqContent2Aggregated 提升到块外声明,供下方实际拆分源复用
1133
+ let reqContent2 = '';
1134
+ let reqContent2Aggregated = false;
1135
+ const analysisPathNew = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'ANALYSIS.md');
1136
+ const analysisPathOld = (0, path_1.join)(iterationDir, '020-specs', 'ANALYSIS.md');
1137
+ const analysisPath = await (0, fs_extra_1.pathExists)(analysisPathNew) ? analysisPathNew : analysisPathOld;
1138
+ if (await (0, fs_extra_1.pathExists)(analysisPath)) {
1139
+ const analysis = await (0, fs_extra_1.readFile)(analysisPath, 'utf-8');
1140
+ const blockerLines = analysis.split(/\r?\n/).filter(l => l.includes('🔴') || l.includes('🚫') || l.toLowerCase().includes('blocker'));
1141
+ if (blockerLines.length > 0) {
1142
+ spinner.stop();
1143
+ logger_1.logger.warn(`\n⚠️ ANALYSIS.md 检测到 ${blockerLines.length} 个阻断项:`);
1144
+ for (const line of blockerLines.slice(0, 5)) {
1145
+ logger_1.logger.warn(` ${line.trim().slice(0, 80)}`);
1146
+ }
1147
+ // 自动模式不询问用户,直接继续;交互模式才确认
1148
+ if (options.interactive || options.strict) {
1149
+ const proceed = await promptUser('\n仍要继续拆分?[y/N] ');
1150
+ if (!proceed || proceed.toLowerCase() !== 'y') {
1151
+ logger_1.logger.info('已取消拆分');
1152
+ return;
1153
+ }
1154
+ }
1155
+ else {
1156
+ logger_1.logger.info(' ℹ️ 自动模式:跳过阻断项确认,继续拆分(疑问记录到 .speccore/questions/)');
1157
+ }
1158
+ spinner.start();
1159
+ }
1160
+ // 生成 AI 智能拆分上下文(含完整 Spec 上下文 + 粒度规则)
1161
+ const promptsDir = (0, path_1.join)('.speccore', 'prompts');
1162
+ await (0, fs_extra_1.ensureDir)(promptsDir);
1163
+ // 读取 CONSTITUTION.md(技术宪法)
1164
+ const constitutionPath = (0, path_1.join)('.speccore', 'CONSTITUTION.md');
1165
+ let constitutionContent = '';
1166
+ if (await (0, fs_extra_1.pathExists)(constitutionPath)) {
1167
+ constitutionContent = await (0, fs_extra_1.readFile)(constitutionPath, 'utf-8');
1168
+ }
1169
+ // 读取 020-specs/ 全部文件(完整上下文)
1170
+ const specDir2 = (0, path_1.join)(iterationDir, '020-specs');
1171
+ // REQUIREMENT.md 从 overview/ 读取
1172
+ const reqPath2 = (0, path_1.join)(specDir2, spec_paths_1.GLOBAL_SPECS_DIR, 'REQUIREMENT.md');
1173
+ if (await (0, fs_extra_1.pathExists)(reqPath2)) {
1174
+ reqContent2 = await (0, fs_extra_1.readFile)(reqPath2, 'utf-8');
1175
+ }
1176
+ // v8.3.160+: 如果全局 REQUIREMENT.md 为空/骨架,从各功能模块 overview/REQUIREMENT.md 聚合
1177
+ // v8.3.249+: 增加陈旧检测——聚合文档不含当前 features 单元名时视为过期,强制重新聚合。
1178
+ // 此前仅按空/骨架判断,旧聚合文档与新切分单元不同步时 taskify 用陈旧数据拆分,
1179
+ // 漏掉新单元(--modules 指定新单元也匹配不到任何 section,反而全量拆分)。
1180
+ let reqContent2IsStale = false;
1181
+ if (reqContent2 && reqContent2.trim().length >= 200 && !reqContent2.includes('<!-- SPEC-SKELETON -->')) {
1182
+ const featuresAll = await (0, spec_paths_1.parseFeatureList)(iterationDir);
1183
+ for (const feature of featuresAll) {
1184
+ if (!reqContent2.includes(feature)) {
1185
+ reqContent2IsStale = true;
1186
+ break;
1187
+ }
1188
+ }
1189
+ }
1190
+ if (!reqContent2 || reqContent2.trim().length < 200 || reqContent2.includes('<!-- SPEC-SKELETON -->') || reqContent2IsStale) {
1191
+ const featureReqParts = [];
1192
+ const features = await (0, spec_paths_1.parseFeatureList)(iterationDir);
1193
+ for (const feature of features) {
1194
+ const featureReqPath = (0, path_1.join)(specDir2, feature, 'overview', 'REQUIREMENT.md');
1195
+ if (await (0, fs_extra_1.pathExists)(featureReqPath)) {
1196
+ const content = await (0, fs_extra_1.readFile)(featureReqPath, 'utf-8');
1197
+ if (content.trim().length > 100 && !content.includes('<!-- SPEC-SKELETON -->')) {
1198
+ featureReqParts.push(`# ${feature}\n\n${content.trim()}`);
1199
+ }
1200
+ }
1201
+ }
1202
+ if (featureReqParts.length > 0) {
1203
+ reqContent2 = featureReqParts.join('\n\n---\n\n');
1204
+ reqContent2Aggregated = true;
1205
+ }
1206
+ }
1207
+ const specContents = [];
1208
+ // v8.3.21+: 读取全局层文档(overview/ 下)
1209
+ const overviewDir = (0, path_1.join)(specDir2, spec_paths_1.GLOBAL_SPECS_DIR);
1210
+ if (await (0, fs_extra_1.pathExists)(overviewDir)) {
1211
+ const overviewFiles = await (0, fs_extra_1.readdir)(overviewDir, { withFileTypes: true });
1212
+ for (const f of overviewFiles) {
1213
+ if (!f.name.endsWith('.md'))
1214
+ continue;
1215
+ const fp = (0, path_1.join)(overviewDir, f.name);
1216
+ const content = await (0, fs_extra_1.readFile)(fp, 'utf-8');
1217
+ if (content.trim().length > 50 && !content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL/m)) {
1218
+ specContents.push({ name: f.name, content });
1219
+ }
1220
+ }
1221
+ }
1222
+ // v8.3.21+: 读取各功能模块下的文档(overview/ + 各端/)
1223
+ const features = await (0, spec_paths_1.parseFeatureList)(iterationDir);
1224
+ for (const feature of features) {
1225
+ const featureDir = (0, path_1.join)(specDir2, feature);
1226
+ if (!await (0, fs_extra_1.pathExists)(featureDir))
1227
+ continue;
1228
+ const entries = await (0, fs_extra_1.readdir)(featureDir, { withFileTypes: true });
1229
+ for (const entry of entries) {
1230
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
1231
+ continue;
1232
+ const subDir = (0, path_1.join)(featureDir, entry.name);
1233
+ const subFiles = await (0, fs_extra_1.readdir)(subDir, { withFileTypes: true });
1234
+ for (const sf of subFiles) {
1235
+ if (!sf.name.endsWith('.md'))
1236
+ continue;
1237
+ const fp = (0, path_1.join)(subDir, sf.name);
1238
+ const content = await (0, fs_extra_1.readFile)(fp, 'utf-8');
1239
+ if (content.trim().length > 50 && !content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL/m)) {
1240
+ specContents.push({ name: `${feature}/${entry.name}/${sf.name}`, content });
1241
+ }
1242
+ }
1243
+ }
1244
+ }
1245
+ // 读取类型文档分析(020-specs/{bugs,refactors,research}/)
1246
+ for (const typeDir of ['bugs', 'refactors', 'research']) {
1247
+ const typeDirPath = (0, path_1.join)(specDir2, typeDir);
1248
+ if (!(await (0, fs_extra_1.pathExists)(typeDirPath)))
1249
+ continue;
1250
+ const typeEntries = await (0, fs_extra_1.readdir)(typeDirPath, { withFileTypes: true });
1251
+ for (const te of typeEntries) {
1252
+ if (!te.isFile() || !te.name.endsWith('.md'))
1253
+ continue;
1254
+ const fp = (0, path_1.join)(typeDirPath, te.name);
1255
+ const content = await (0, fs_extra_1.readFile)(fp, 'utf-8');
1256
+ if (content.trim().length > 50 && !content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL/m)) {
1257
+ specContents.push({ name: `${typeDir}/${te.name}`, content });
1258
+ }
1259
+ }
1260
+ }
1261
+ // 获取标准端名列表(用于 prompt 注入和 scope 规范化)
1262
+ const allPlatforms = await detectPlatforms(iterationDir);
1263
+ // v6.76.0+: 扫描已有 Task 结构(增量拆分)
1264
+ const existingTaskStructure = await scanExistingTaskStructure(iterationDir);
1265
+ // 构建完整 prompt(v6.69.3+: 传入标准端名列表,确保 AI 使用正确的端名)
1266
+ // v6.76.0+: 传入过滤条件,限制拆分范围;传入已有 Task 结构,支持增量拆分
1267
+ const modulesFilter = options.modules ? options.modules.split(',').map(m => m.trim()).filter(Boolean) : undefined;
1268
+ const platformsFilter = options.platforms ? options.platforms.split(',').map(p => p.trim()).filter(Boolean) : undefined;
1269
+ // v8.3.160+: 普通模式下默认使用 task-decomposer,full 上下文类型
1270
+ let splitPrompt = await buildSplitPrompt(iteration, constitutionContent, reqContent2, specContents, allPlatforms, modulesFilter, platformsFilter, existingTaskStructure, iterationDir, 'task-decomposer', 'full');
1271
+ // 注入全局上下文(INDEX + TOC 目录,AI 自主读取)
1272
+ const { loadGlobalContext, formatGlobalContext } = await Promise.resolve().then(() => __importStar(require('../../core/prompt-builder')));
1273
+ const globalCtx = await loadGlobalContext(process.cwd(), 'split');
1274
+ if (globalCtx.indexSummary || globalCtx.toc.length > 0) {
1275
+ splitPrompt += '\n' + formatGlobalContext(globalCtx) + '\n';
1276
+ }
1277
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(promptsDir, `split-suggestion-${iteration}.md`), splitPrompt);
1278
+ logger_1.logger.info(` 🤖 AI 拆分建议 → .speccore/prompts/split-suggestion-${iteration}.md`);
1279
+ logger_1.logger.info(` 📜 上下文: CONSTITUTION + REQUIREMENT + ${specContents.length} 个 Spec 文档`);
1280
+ }
1281
+ else {
1282
+ logger_1.logger.info(' ℹ️ 未找到 ANALYSIS.md,建议先运行 speccore analyze');
1283
+ }
1284
+ const reqFileNew = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, options.file || 'REQUIREMENT.md');
1285
+ const reqFileOld = (0, path_1.join)(iterationDir, '020-specs', options.file || 'REQUIREMENT.md');
1286
+ const reqFile = await (0, fs_extra_1.pathExists)(reqFileNew) ? reqFileNew : reqFileOld;
1287
+ if (!(await (0, fs_extra_1.pathExists)(reqFile))) {
1288
+ spinner.fail(`Requirement file not found: ${reqFile}`);
1289
+ return;
1290
+ }
1291
+ const content = await (0, fs_extra_1.readFile)(reqFile, 'utf-8');
1292
+ // v8.3.249+: 聚合后的最新需求内容(含新切分单元)优先于陈旧聚合文档作为实际拆分源
1293
+ const finalContent = (reqContent2Aggregated && reqContent2) ? reqContent2 : content;
1294
+ let sections = extractSections(finalContent, options.sections);
1295
+ // v6.76.0+: 按功能模块过滤
1296
+ if (options.modules) {
1297
+ const moduleFilters = options.modules.split(',').map(m => m.trim()).filter(Boolean);
1298
+ const beforeCount = sections.length;
1299
+ sections = sections.filter(s => moduleFilters.some(f => s.name.includes(f)));
1300
+ if (sections.length < beforeCount) {
1301
+ logger_1.logger.info(` 🎯 模块过滤: ${beforeCount} → ${sections.length}(只拆分: ${moduleFilters.join(', ')})`);
1302
+ }
1303
+ if (sections.length === 0) {
1304
+ spinner.fail(`没有匹配 "${options.modules}" 的功能模块,请检查模块名称`);
1305
+ return;
1306
+ }
1307
+ }
1308
+ const platforms = await detectPlatforms(iterationDir, options.platforms);
1309
+ // ── 冲突检测: 检查是否已有 Task 目录 ──
1310
+ const existingTasks = await detectExistingTasks(iterationDir);
1311
+ if (existingTasks.length > 0 && !options.force) {
1312
+ spinner.stop();
1313
+ logger_1.logger.warn(`\n⚠️ 检测到已有 ${existingTasks.length} 个任务: ${existingTasks.slice(0, 5).join(', ')}${existingTasks.length > 5 ? '...' : ''}`);
1314
+ logger_1.logger.info(' 重新 split 会覆盖 TASK.md 中的进度和状态。');
1315
+ logger_1.logger.info(' 使用 --force 强制覆盖,或手动删除不需要的任务。\n');
1316
+ return;
1317
+ }
1318
+ // ── 智能分析: 复杂度 + 优先级 + 工时 ──
1319
+ const complexities = sections.map(s => estimateSectionComplexity(s));
1320
+ for (let i = 0; i < sections.length; i++) {
1321
+ sections[i]._complexity = complexities[i];
1322
+ }
1323
+ // ── STAFFING 人员排期 ──
1324
+ const staffing = readStaffing(iterationDir);
1325
+ if (staffing) {
1326
+ logger_1.logger.info(` 👥 STAFFING: ${staffing.map(m => `${m.name}(${m.platforms.join(',')})`).join(', ')}`);
1327
+ for (const section of sections) {
1328
+ if (!section._owner) {
1329
+ section._owner = autoAssign(section, platforms, staffing);
1330
+ }
1331
+ }
1332
+ }
1333
+ else {
1334
+ logger_1.logger.info(' ℹ️ 未找到 STAFFING.md,人员分配默认为"未分配"');
1335
+ }
1336
+ logger_1.logger.info(`Found ${sections.length} sections to split`);
1337
+ logger_1.logger.info(`Platforms: ${platforms.join(', ')}`);
1338
+ // ── 预分配任务 ID(确保所有路径使用计数器,避免编号重复) ──
1339
+ for (const section of sections) {
1340
+ const topic = slugify(section.name);
1341
+ const { id: taskId } = await (0, global_counters_1.nextTaskId)(section.name, topic);
1342
+ section._taskId = taskId;
1343
+ }
1344
+ // ── Strict mode: preview + confirm each task's split plan ──
1345
+ if (options.strict) {
1346
+ const approved = await strictSplitPreview(sections, platforms, iterationDir);
1347
+ if (approved.length === 0) {
1348
+ spinner.stop('已取消,未创建任何任务');
1349
+ return;
1350
+ }
1351
+ for (const section of approved) {
1352
+ const taskId = section._taskId;
1353
+ await createTaskFromSection(iterationDir, taskId, section, platforms, section._taskType, approved);
1354
+ }
1355
+ spinner.stop(`✅ 创建了 ${approved.length} 个任务`);
1356
+ // 自动刷新知识图谱(v6.49.10+)
1357
+ try {
1358
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../../core/knowledge-graph')));
1359
+ await refreshKnowledgeGraph(process.cwd(), iteration);
1360
+ logger_1.logger.info('🧠 知识图谱已刷新');
1361
+ }
1362
+ catch { }
1363
+ return;
1364
+ }
1365
+ if (options.dryRun) {
1366
+ spinner.stop('Dry run complete - no files created');
1367
+ for (const section of sections) {
1368
+ logger_1.logger.info(` Would create: ${section.name}`);
1369
+ }
1370
+ return;
1371
+ }
1372
+ // ── Interactive mode: preview → adjust → confirm → create ──
1373
+ if (options.interactive) {
1374
+ spinner.stop('任务预览');
1375
+ logger_1.logger.info('');
1376
+ // 依赖关系预览
1377
+ const semanticDeps = detectSemanticDependencies(sections);
1378
+ if (semanticDeps.size > 0) {
1379
+ logger_1.logger.info('🔗 任务间依赖关系:\n');
1380
+ for (const [from, targets] of semanticDeps) {
1381
+ logger_1.logger.info(` ${from} 依赖 → ${targets.join(', ')}`);
1382
+ }
1383
+ logger_1.logger.info('');
1384
+ }
1385
+ logger_1.logger.info(`📋 共 ${sections.length} 个任务将被创建:\n`);
1386
+ for (let i = 0; i < sections.length; i++) {
1387
+ const taskId = sections[i]._taskId;
1388
+ const contentPreview = sections[i].content?.split(/\r?\n/)[0]?.slice(0, 60) || '';
1389
+ const c = complexities[i];
1390
+ const owner = sections[i]._owner || '未分配';
1391
+ const deps = semanticDeps.get(taskId);
1392
+ logger_1.logger.info(` ${taskId} → ${sections[i].name}`);
1393
+ if (contentPreview)
1394
+ logger_1.logger.info(` ${contentPreview}`);
1395
+ logger_1.logger.info(` 优先级: ${c.priority} | 工时: ${c.estimatedHours}h | 👤 ${owner}`);
1396
+ if (deps)
1397
+ logger_1.logger.info(` 🔗 依赖: ${deps.join(', ')}`);
1398
+ logger_1.logger.info(` 平台: ${platforms.join(', ')}`);
1399
+ logger_1.logger.info('');
1400
+ }
1401
+ logger_1.logger.info('💡 你可以:');
1402
+ logger_1.logger.info(' [y] 确认创建全部 [n] 逐一确认 [q] 取消');
1403
+ logger_1.logger.info('');
1404
+ const answer = await promptUser('确认创建?');
1405
+ if (answer?.toLowerCase() === 'q') {
1406
+ logger_1.logger.info('已取消');
1407
+ return;
1408
+ }
1409
+ if (answer?.toLowerCase() === 'n') {
1410
+ logger_1.logger.info('进入逐一确认模式...');
1411
+ let created = 0;
1412
+ for (let i = 0; i < sections.length; i++) {
1413
+ const taskId = sections[i]._taskId;
1414
+ const resp = await promptUser(` 创建 ${taskId} - ${sections[i].name}? [y/n/q]`);
1415
+ if (resp?.toLowerCase() === 'q') {
1416
+ logger_1.logger.info(`已取消,剩余 ${sections.length - i} 个任务未创建`);
1417
+ break;
1418
+ }
1419
+ if (resp?.toLowerCase() === 'y' || resp === '') {
1420
+ await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType, sections);
1421
+ created++;
1422
+ logger_1.logger.info(` ✅ ${taskId}`);
1423
+ }
1424
+ else {
1425
+ logger_1.logger.info(` ⏭️ 跳过 ${sections[i].name}`);
1426
+ }
1427
+ }
1428
+ spinner.stop(`创建了 ${created}/${sections.length} 个任务`);
1429
+ if (created > 0) {
1430
+ await generateImpactGraph(iterationDir, sections.slice(0, created), platforms);
1431
+ await updateProjectGraph(iterationDir, sections.slice(0, created));
1432
+ // 自动刷新知识图谱(v6.49.10+)
1433
+ try {
1434
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../../core/knowledge-graph')));
1435
+ await refreshKnowledgeGraph(process.cwd(), iteration);
1436
+ logger_1.logger.info('🧠 知识图谱已刷新');
1437
+ }
1438
+ catch { }
1439
+ }
1440
+ return;
1441
+ }
1442
+ // Default: create all
1443
+ for (let i = 0; i < sections.length; i++) {
1444
+ const taskId = sections[i]._taskId;
1445
+ await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType, sections);
1446
+ }
1447
+ await generateImpactGraph(iterationDir, sections, platforms);
1448
+ await generateEnvExample(iterationDir, sections);
1449
+ await updateProjectGraph(iterationDir, sections);
1450
+ spinner.stop(`✅ 创建了 ${sections.length} 个任务`);
1451
+ (0, next_steps_1.showNextSteps)('split');
1452
+ // v8.3.14+: split 后自动清理旧的时间戳备份(保留 7 天内的)
1453
+ await (0, cleanup_1.cleanupByType)({
1454
+ cwd: process.cwd(),
1455
+ types: ['timestampBackups'],
1456
+ days: 7,
1457
+ silent: true,
1458
+ });
1459
+ // 自动刷新知识图谱(v6.49.10+)
1460
+ try {
1461
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../../core/knowledge-graph')));
1462
+ await refreshKnowledgeGraph(process.cwd(), iteration);
1463
+ logger_1.logger.info('🧠 知识图谱已刷新');
1464
+ }
1465
+ catch { }
1466
+ return;
1467
+ }
1468
+ // Create tasks(使用预分配的 ID)
1469
+ for (let i = 0; i < sections.length; i++) {
1470
+ const taskId = sections[i]._taskId;
1471
+ await createTaskFromSection(iterationDir, taskId, sections[i], platforms, sections[i]._taskType, sections);
1472
+ }
1473
+ // ── Generate impact graph + risk scores ──
1474
+ await generateImpactGraph(iterationDir, sections, platforms);
1475
+ // ── Generate .env.example for the iteration ──
1476
+ await generateEnvExample(iterationDir, sections);
1477
+ // Update PROJECT_GRAPH.md
1478
+ await updateProjectGraph(iterationDir, sections);
1479
+ // 生成任务总览报告
1480
+ const summaryMd = await generateTaskSummary(iterationDir, [], sections);
1481
+ logger_1.logger.info(`📊 任务总览 → 000-overview/task-summaries/`);
1482
+ spinner.stop(`Created ${sections.length} tasks from requirements`);
1483
+ (0, next_steps_1.showNextSteps)('split');
1484
+ // v8.3.14+: split 后自动清理旧的时间戳备份(保留 7 天内的)
1485
+ await (0, cleanup_1.cleanupByType)({
1486
+ cwd: process.cwd(),
1487
+ types: ['timestampBackups'],
1488
+ days: 7,
1489
+ silent: true,
1490
+ });
1491
+ // 自动刷新知识图谱(v6.49.10+)
1492
+ try {
1493
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../../core/knowledge-graph')));
1494
+ await refreshKnowledgeGraph(process.cwd(), iteration);
1495
+ logger_1.logger.info('🧠 知识图谱已刷新');
1496
+ }
1497
+ catch { }
1498
+ }
1499
+ catch (error) {
1500
+ spinner.fail(`Split failed: ${error}`);
1501
+ throw error;
1502
+ }
1503
+ }
1504
+ function extractSections(content, sectionFilter) {
1505
+ const sections = [];
1506
+ let currentPlatform;
1507
+ const lines = content.split(/\r?\n/);
1508
+ let currentSection = null;
1509
+ let currentContent = [];
1510
+ for (const line of lines) {
1511
+ const headerMatch = line.match(/^(#{2,4})\s+(.+)/);
1512
+ if (headerMatch) {
1513
+ if (currentSection) {
1514
+ currentSection.content = currentContent.join('\n');
1515
+ sections.push(currentSection);
1516
+ }
1517
+ currentSection = {
1518
+ name: headerMatch[2].trim(),
1519
+ content: '',
1520
+ level: headerMatch[1].length
1521
+ };
1522
+ // 检测 "## {X}端需求" 父章节,子章节继承此平台
1523
+ const platformMatch = currentSection.name.match(/^(.+)端需求$/);
1524
+ if (platformMatch) {
1525
+ currentPlatform = platformMatch[1];
1526
+ currentSection = null; // 容器章节本身不作为 Task
1527
+ continue;
1528
+ }
1529
+ else if (currentSection.level === 2) {
1530
+ currentPlatform = undefined; // 新的 ## 章节重置平台
1531
+ }
1532
+ currentSection.platform = currentPlatform;
1533
+ while (/端端/.test(currentSection.name))
1534
+ currentSection.name = currentSection.name.replace('端端', '端');
1535
+ currentContent = [];
1536
+ }
1537
+ else if (currentSection) {
1538
+ currentContent.push(line);
1539
+ }
1540
+ }
1541
+ if (currentSection) {
1542
+ currentSection.content = currentContent.join('\n');
1543
+ sections.push(currentSection);
1544
+ }
1545
+ // Filter sections if specified
1546
+ if (sectionFilter) {
1547
+ return sections.filter(s => {
1548
+ const filters = sectionFilter.split(',').map(f => f.trim());
1549
+ return filters.some(f => s.name.includes(f));
1550
+ });
1551
+ }
1552
+ // Filter template noise: skip empty/template placeholder sections
1553
+ return filterTemplateNoise(sections);
1554
+ }
1555
+ const TEMPLATE_PATTERNS = [
1556
+ // Section types that should NOT become separate tasks
1557
+ /^\d+\.\d+\s*(背景|目标|范围)\s*$/,
1558
+ /^\d+\.\d+\s*(性能|安全|兼容性)\s*$/,
1559
+ /^\d+\.\s*(需求概述|功能需求|非功能需求|验收标准|附录)\s*$/,
1560
+ /^功能模块[一二三四五]\s*$/,
1561
+ // Structural PRD headings (not functional requirements)
1562
+ /^功能优先级$/,
1563
+ /^范围边界$/,
1564
+ /^依赖关系$/,
1565
+ /^术语表$/,
1566
+ /^业务规则$/,
1567
+ /^非功能要求?$/,
1568
+ /^原型参考$/,
1569
+ /^版本历史$/,
1570
+ /^项目概述$/,
1571
+ /^BDD 验收标准$/,
1572
+ // 背景/概述/架构类(非功能内容)
1573
+ /项目背景/,
1574
+ /项目目标/,
1575
+ /系统概述/,
1576
+ /系统架构/,
1577
+ /技术架构/,
1578
+ /整体架构/,
1579
+ /名词解释/,
1580
+ /术语定义/,
1581
+ /参考文献/,
1582
+ /修订记录/,
1583
+ /变更历史/,
1584
+ /文档说明/,
1585
+ /编写说明/,
1586
+ /阅读指南/,
1587
+ /目录$/,
1588
+ // 抽象约束类(不是具体功能)
1589
+ /^约束条件$/,
1590
+ /^假设条件$/,
1591
+ /^设计原则$/,
1592
+ /^总体目标$/,
1593
+ /^建设目标$/,
1594
+ /^业务背景$/,
1595
+ /^技术背景$/,
1596
+ // v8.3.223+: 骨架质量标准引导标题(spec-skeleton 注入,AI 填充时可能未删除)不应成为任务
1597
+ /^📋\s*质量标准/,
1598
+ /^质量标准\s*([^)]*)$/,
1599
+ ];
1600
+ function filterTemplateNoise(sections) {
1601
+ return sections.filter(s => {
1602
+ // Skip sections matching template patterns
1603
+ for (const pattern of TEMPLATE_PATTERNS) {
1604
+ if (pattern.test(s.name))
1605
+ return false;
1606
+ }
1607
+ // Skip sections with effectively empty content (< 20 meaningful chars = no real substance)
1608
+ const meaningful = (s.content || '').replace(/[\s\n>#*-|]/g, '').length;
1609
+ if (meaningful < 20)
1610
+ return false;
1611
+ // Skip sections without API tables (structural headings)
1612
+ return true;
1613
+ });
1614
+ }
1615
+ // v8.3.4+: 从迭代目录路径提取迭代名(如 Iteration-011-meeting-upgrade → 011-meeting-upgrade)
1616
+ function extractIterationName(iterationDir) {
1617
+ const base = (0, path_1.basename)(iterationDir) || '';
1618
+ return base.replace(/^Iteration-/, '');
1619
+ }
1620
+ /** 生成带实际配置值的 git-config 内容(v8.3.118+) */
1621
+ function buildGitConfigContent(config, platformLabel, taskType, branchTypes) {
1622
+ const branchType = taskType === 'bugfix' ? 'bugfix' : taskType === 'refactor' ? 'refactor' : taskType === 'research' ? 'research' : 'feature';
1623
+ // v8.3.118+: 从全局 branch_types 读取对应类型的前缀/后缀/源分支
1624
+ const typeDef = branchTypes?.[branchType];
1625
+ const hasPrefix = typeDef?.prefix !== undefined && typeDef.prefix !== '';
1626
+ const hasSuffix = typeDef?.suffix !== undefined && typeDef.suffix !== '';
1627
+ const hasSource = typeDef?.source !== undefined && typeDef.source !== '';
1628
+ // v8.3.6+: 空值用注释表示,避免 loadSubtaskGitConfig 误把 "(空)" 当实际值
1629
+ const fmt = (key, val, def) => val !== undefined && val !== '' && String(val) !== 'false'
1630
+ ? `${key}: ${val}`
1631
+ : `# ${key}: ${def}`;
1632
+ // v8.3.118+: 前缀/后缀/源分支优先从 branch_types 读取,其次从 gitConfig 读取
1633
+ const effectiveSource = hasSource ? typeDef.source : (config.defaultBranch || '');
1634
+ return `# 子任务级 Git 配置(${platformLabel})
1635
+ # ─────────────────────────────────────────────────────────────────────────────
1636
+ # 配置优先级:子任务 > 迭代级 > 全局 CONSTITUTION.md > 默认值
1637
+ # 未配置的字段自动从上级继承,无需全部填写
1638
+ # ─────────────────────────────────────────────────────────────────────────────
1639
+
1640
+ # === 分支类型与命名(v8.3.120+) ===
1641
+ # 分支名格式: {类型}/{前缀}{任务名}{后缀}
1642
+ # {类型}/ = 类型自带(如 hotfix/、feature/)
1643
+ # {前缀} = 任务名前的前缀(可选,包含分隔符),如 api-、backend_、v2.
1644
+ # {任务名} = 子任务全局唯一 ID,如 Task-001-booking-service
1645
+ # {后缀} = 任务名后的后缀(可选,包含分隔符),如 -urgent、_review、.beta
1646
+ #
1647
+ # ⚠️ 注意:前缀/后缀的值请包含你想用的分隔符,代码不再自动添加任何符号
1648
+ #
1649
+ # 示例 1(- 分隔): 前缀=api- 后缀=-urgent → hotfix/api-Task-001-urgent
1650
+ # 示例 2(_ 分隔): 前缀=api_ 后缀=_urgent → hotfix/api_Task-001_urgent
1651
+ # 示例 3(无分隔): 前缀=api 后缀=urgent → hotfix/apiTask-001urgent
1652
+
1653
+ # 分支类型: 覆盖默认 feature(可选值: feature / bugfix / hotfix / release / support)
1654
+ 分支类型: ${branchType}
1655
+
1656
+ # 前缀: 任务名前的前缀(可选,包含分隔符),如 api-、backend_、v2.
1657
+ ${hasPrefix ? `前缀: ${typeDef.prefix}` : `# 前缀: api-`}
1658
+
1659
+ # 后缀: 任务名后的后缀(可选,包含分隔符),如 -urgent、_review
1660
+ ${hasSuffix ? `后缀: ${typeDef.suffix}` : `# 后缀: -urgent`}
1661
+
1662
+ # === 源分支(从哪个分支创建)===
1663
+ # 默认从全局配置的 default_base 创建,如需覆盖请取消注释:
1664
+ ${fmt('源分支', effectiveSource, 'main')}
1665
+
1666
+ # === 保护分支(禁止直接推送)===
1667
+ ${config.protectedBranches.map(b => `# - ${b}`).join('\n')}
1668
+
1669
+ # === 高级配置(一般不修改)===
1670
+ # 分支前缀(旧语义,兼容保留)
1671
+ ${fmt('分支前缀', config.branchPrefix, '无')}
1672
+
1673
+ # 分支格式: 自定义分支名模板(支持变量: {type} {prefix} {taskId} {name} {date} {hash4})
1674
+ ${fmt('分支格式', config.branchFormat, '{type}/{prefix}{taskId}')}
1675
+
1676
+ # 自动拉取: 创建分支前是否自动 git pull(true / false)
1677
+ ${fmt('自动拉取', config.autoPull ? 'true' : '', 'false')}
1678
+
1679
+ # 远程名称: 远程仓库名称(默认 origin)
1680
+ ${fmt('远程名称', config.remoteName, 'origin')}
1681
+ `;
1682
+ }
1683
+ async function createTaskFromSection(iterationDir, taskId, section, allPlatforms, taskType = 'feature', allSections) {
1684
+ // taskId 已含 slug(nextTaskId 返回 Task-NNN-slug),直接用
1685
+ const taskDir = (0, path_1.join)(iterationDir, '030-tasks', taskType, taskId);
1686
+ const iterationName = extractIterationName(iterationDir);
1687
+ // v8.3.118+: 读取全局 branch_types,用于生成子任务 git-config
1688
+ const projectConfig = await (0, unified_config_1.loadProjectConfig)();
1689
+ const branchTypes = projectConfig.git.branch_types;
1690
+ // v8.3.36: 提前加载 spec 内容,用于平台推断 + 任务级文件填充
1691
+ const specContents = await loadSpecContents(iterationDir);
1692
+ // 确定任务涉及的端:优先使用 AI 标注的 _scopePlatforms,否则从 020-specs 推断
1693
+ let taskPlatforms;
1694
+ if (section._scopePlatforms && section._scopePlatforms.length > 0) {
1695
+ taskPlatforms = section._scopePlatforms;
1696
+ }
1697
+ else if (section.platform) {
1698
+ taskPlatforms = [section.platform];
1699
+ }
1700
+ else {
1701
+ // v8.3.125+: 使用三层匹配推断平台(替代脆弱的字符串包含匹配)
1702
+ taskPlatforms = [];
1703
+ const sectionFeatureName = section.name.replace(/^\d+(\.\d+)*\s*/, '').trim();
1704
+ for (const platform of allPlatforms) {
1705
+ const bestMatch = findBestSpecMatch(specContents, sectionFeatureName, platform, 'TECH.md');
1706
+ if (bestMatch && bestMatch.score >= 20) {
1707
+ taskPlatforms.push(platform);
1708
+ }
1709
+ }
1710
+ // v8.3.185+: Spec 匹配失败时,用六层端推断机制(显式声明→_matrix→目录→关键词→AI)
1711
+ if (taskPlatforms.length === 0) {
1712
+ const { inferFeaturePlatforms } = await Promise.resolve().then(() => __importStar(require('../../core/platform-inference')));
1713
+ const featureDir = (0, path_1.join)(iterationDir, '010-requirements', 'features', sectionFeatureName);
1714
+ const inference = await inferFeaturePlatforms({
1715
+ featureName: sectionFeatureName,
1716
+ featureDir,
1717
+ allPlatforms,
1718
+ });
1719
+ taskPlatforms = inference.platforms;
1720
+ if (inference.needsConfirm || inference.source === 'ai-required') {
1721
+ logger_1.logger.warn(` ⚠️ 功能「${sectionFeatureName}」端推断置信度 ${inference.confidence}(${inference.source}),建议人工确认 _matrix.md`);
1722
+ }
1723
+ }
1724
+ }
1725
+ const complexity = section._complexity || { estimatedHours: 8, priority: 'medium' };
1726
+ const owner = section._owner || '未分配';
1727
+ const today = new Date().toISOString().split('T')[0];
1728
+ // v8.3.32 修复:020-specs 缺失时给出明确警告
1729
+ const hasSpecContent = Object.keys(specContents).length > 0;
1730
+ if (!hasSpecContent) {
1731
+ logger_1.logger.warn(` ⚠️ 未找到 020-specs/ 分析文档,任务级 REQ.md/TECH.md 将生成空模板`);
1732
+ logger_1.logger.warn(` 建议先运行: speccore analyze -I ${iterationName} --auto`);
1733
+ }
1734
+ // 预生成所有端的子任务 ID(保证同一端在所有文件中 ID 一致)
1735
+ const taskNum = taskId.replace(/^Task-/, '');
1736
+ const subtaskIdMap = new Map();
1737
+ for (const p of taskPlatforms) {
1738
+ subtaskIdMap.set(p, generateSubtaskId(taskNum, p));
1739
+ }
1740
+ // ── 1. 任务目录指引(功能模块分组) ──
1741
+ await (0, fs_extra_1.ensureDir)(taskDir);
1742
+ const isResearch = taskType === 'research';
1743
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, 'README.md'), isResearch
1744
+ ? `# ${section.name}
1745
+
1746
+ > 技术调研任务 — 产出调研文档,不产出代码
1747
+
1748
+ ## 目录结构
1749
+
1750
+ \`\`\`
1751
+ ${taskId}/
1752
+ ├── .meta/ ← 任务元信息(feature/type/status/owner)
1753
+ ├── README.md ← 本文件(调研任务说明)
1754
+ ├── _shared/ ← 共享上下文(CONTEXT.md)
1755
+ ├── 00-specs/ ← 核心规格(REQ.md/TECH.md)
1756
+ ├── RESEARCH.md ← 调研报告
1757
+ ├── COMPARISON.md ← 方案对比
1758
+ └── .issues.md ← 问题追踪
1759
+ \`\`\`
1760
+
1761
+ ## AI 执行时读取规则
1762
+
1763
+ 运行 \`speccore execute -t ${taskId}\` 时:
1764
+ - 读取 \`00-specs/REQ.md\` — 调研目标
1765
+ - 读取 \`_shared/CONTEXT.md\` — 任务上下文
1766
+ - 产出写入 \`RESEARCH.md\` 和 \`COMPARISON.md\`
1767
+ `
1768
+ : `# ${section.name}
1769
+
1770
+ > 功能模块分组 — 聚合相关子任务,共享规格与契约
1771
+
1772
+ ## 目录结构
1773
+
1774
+ \`\`\`
1775
+ ${taskId}/
1776
+ ├── .meta/ ← 任务元信息(feature/type/status/owner)
1777
+ ├── README.md ← 本文件
1778
+ ├── _shared/ ← 共享契约(API_CONTRACT.yaml + CONTEXT.md)
1779
+ ├── 00-specs/ ← 模块级核心规格(REQ/TECH/SCHEMA/CHANGELOG)
1780
+ ├── {服务名}/ ← 后端服务(如 booking-service,v6.49.3+ 平铺架构)
1781
+ │ └── {taskId}-{子任务}/ ← 执行单元(.meta/TASK.md + 规格文档)
1782
+ ├── {端名}/ ← 前端端(如 h5-mobile/admin-web,v6.49.3+ 平铺架构)
1783
+ │ └── {taskId}-{子任务}/ ← 执行单元(.meta/TASK.md + 规格文档 + 前端设计)
1784
+ \`\`\`
1785
+
1786
+ ## 子任务列表
1787
+
1788
+ | 子任务 ID | 所属端/服务 | 负责人 | 状态 |
1789
+ | :--- | :--- | :--- | :--- |
1790
+ ${taskPlatforms.map((p) => `| ${subtaskIdMap.get(p)} | ${p} | ${owner} | 🔲 待开发 |`).join('\n')}
1791
+
1792
+ ## AI 执行时读取规则
1793
+
1794
+ 运行 \`speccore execute -t ${taskId} --platform {端}\` 时:
1795
+
1796
+ ### 必读(自动嵌入)
1797
+ - \`00-specs/REQ.md\` — 模块需求描述
1798
+ - \`00-specs/TECH.md\` — 模块技术方案
1799
+ - \`_shared/CONTEXT.md\` — 任务上下文(来源 + 关联)
1800
+ - \`_shared/API_CONTRACT.yaml\` — API 契约
1801
+ - \`{platform}/{子任务}/TASK.md\` — 子任务详情
1802
+ - \`.meta/type\` + \`.meta/status\` — 子任务元信息
1803
+
1804
+ ### 参考(按需读取)
1805
+ - \`020-specs/\` 下的迭代全局文档
1806
+ - \`.speccore/GLOBAL/\` 下的全局知识库
1807
+
1808
+ ### 代码输出位置
1809
+ - AI 生成的代码写入 **CONSTITUTION.md/PROJECT.yaml 中声明的源码路径**,不写入迭代目录
1810
+ - 子任务目录只存放规格文档(TASK.md/TEST.md/RISK.md 等),不存放代码
1811
+ `);
1812
+ // ── 3. 任务级 .meta/(功能单元标识)+ 共享契约 + 核心规格目录 ──
1813
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '.meta'));
1814
+ const taskFeatureName = section.functionalUnit || section.name || '未分类';
1815
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'feature'), taskFeatureName);
1816
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'type'), taskType);
1817
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'status'), 'todo');
1818
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'owner'), owner);
1819
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'created-at'), today);
1820
+ // v8.3.5+: 写入预估工时到 .meta/estimated-hours,供 plan/状态面板读取
1821
+ const taskHours = section._complexity?.estimatedHours || complexity.estimatedHours || 8;
1822
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.meta', 'estimated-hours'), String(taskHours));
1823
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '_shared'));
1824
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(taskDir, '00-specs'));
1825
+ // v6.70.0+: 优先复制全局 API_CONTRACT.yaml(全局契约是单一真相源)
1826
+ const globalContractPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'API_CONTRACT.yaml');
1827
+ if (await (0, fs_extra_1.pathExists)(globalContractPath)) {
1828
+ const globalContract = await (0, fs_extra_1.readFile)(globalContractPath, 'utf-8');
1829
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml'), globalContract);
1830
+ }
1831
+ else {
1832
+ // 回退:从 section content 提取 API 生成任务级契约
1833
+ const contractYaml = generateApiContract(section);
1834
+ if (contractYaml) {
1835
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml'), contractYaml);
1836
+ }
1837
+ }
1838
+ // ── 4. 核心规格写入 00-specs/(REQ/TECH/SCHEMA/CHANGELOG) ──
1839
+ const acItems = generateAcceptanceCriteria(section);
1840
+ const aiReqContent = section._reqContent;
1841
+ // v8.3.160+: 提升阈值,确保内容质量(200/300 字符起用)
1842
+ // v8.3.160+: 优先从功能模块级 overview/REQUIREMENT.md 提取,回退全局 REQUIREMENT.md
1843
+ const bestReqMatch = findBestSpecMatch(specContents, section.name, undefined, 'REQUIREMENT.md');
1844
+ const analyzeReqContent = bestReqMatch && bestReqMatch.score >= 20
1845
+ ? extractRelevantSection(specContents[bestReqMatch.key], section.name, '需求 功能 业务规则 异常场景 验收标准')
1846
+ : extractRelevantSection(specContents['REQUIREMENT.md'] || '', section.name, '需求 功能 业务规则 异常场景 验收标准');
1847
+ if (aiReqContent && aiReqContent.length > 200) {
1848
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'REQ.md'), `# ${section.name}\n\n${aiReqContent}\n\n## 验收标准\n\n${acItems}\n`);
1849
+ }
1850
+ else if (analyzeReqContent && analyzeReqContent.length > 300) {
1851
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'REQ.md'), `# ${section.name}\n\n> 来源: analyze → REQUIREMENT.md(自动提取)\n\n${analyzeReqContent}\n\n## 验收标准\n\n${acItems}\n`);
1852
+ }
1853
+ else if (section.content && section.content.trim().length > 200) {
1854
+ // v8.3.160+: 第三级回退 — section.content 有实质内容时直接使用
1855
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'REQ.md'), `# ${section.name}\n\n> 来源: split 阶段 section 原始内容\n\n${section.content.trim()}\n\n## 验收标准\n\n${acItems}\n`);
1856
+ }
1857
+ else {
1858
+ // 最终回退:精简骨架 + section.content(即使很短也放入,方便 AI execute 阶段参考)
1859
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'REQ.md'), `# ${section.name}\n\n## 需求描述\n\n${section.content || '(待补充,请根据功能单元描述填充)'}\n\n## 验收标准\n\n${acItems}\n`);
1860
+ }
1861
+ const apiLines = section.content.split(/\r?\n/).filter(l => l.includes('| GET') || l.includes('| POST') || l.includes('| PUT') || l.includes('| DELETE') || l.includes('| PATCH'));
1862
+ const apiDesc = apiLines.length > 0 ? apiLines.map(l => `- ${l.trim()}`).join('\n') : '- 待补充(从 REQ.md 提取接口列表)';
1863
+ const aiTechContent = section._techContent;
1864
+ // 从 analyze TECH.md 提取本任务相关内容
1865
+ const specTechContent = extractTaskTechContent(specContents, section);
1866
+ // v8.3.0+: 从 analyze ANALYSIS.md 补充功能分析内容
1867
+ // v8.3.160+: 优先从功能模块级 overview/ANALYSIS.md 提取,回退全局 ANALYSIS.md
1868
+ const bestAnalysisMatch = findBestSpecMatch(specContents, section.name, undefined, 'ANALYSIS.md');
1869
+ const analyzeAnalysisContent = bestAnalysisMatch && bestAnalysisMatch.score >= 20
1870
+ ? extractRelevantSection(specContents[bestAnalysisMatch.key], section.name, '功能分析 业务流程 数据流 决策逻辑 业务规则')
1871
+ : extractRelevantSection(specContents['ANALYSIS.md'] || '', section.name, '功能分析 业务流程 数据流 决策逻辑 业务规则');
1872
+ // v8.3.160+: 提升阈值,确保内容质量(200/150 字符起用)
1873
+ // TECH.md: 优先 AI 生成 → 回退 analyze 提取 → 回退 section 内容提取 → 回退模板
1874
+ if (aiTechContent && aiTechContent.length > 200) {
1875
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'TECH.md'), `# ${section.name} - 技术方案\n\n${aiTechContent}\n`);
1876
+ }
1877
+ else if (specTechContent && specTechContent.length > 150) {
1878
+ let techBody = `> 来源: analyze → TECH.md(自动提取)\n\n${specTechContent}`;
1879
+ // 追加 ANALYSIS.md 功能分析(如有)
1880
+ if (analyzeAnalysisContent && analyzeAnalysisContent.length > 100) {
1881
+ techBody += `\n\n---\n\n## 功能分析补充\n\n> 来源: analyze → ANALYSIS.md(自动提取)\n\n${analyzeAnalysisContent}`;
1882
+ }
1883
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'TECH.md'), `# ${section.name} - 技术方案\n\n${techBody}\n`);
1884
+ }
1885
+ else {
1886
+ // v8.3.160+: 第三级回退 — 从 section.content 提取技术相关内容
1887
+ const sectionTechExtract = extractRelevantSection(section.content, section.name, '接口 数据 模型 字段 表结构 技术方案 架构 流程');
1888
+ if (sectionTechExtract && sectionTechExtract.length > 100) {
1889
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'TECH.md'), `# ${section.name} - 技术方案\n\n> 来源: split 阶段 section 原始内容(技术相关段落自动提取)\n\n${sectionTechExtract}\n\n## 接口设计\n${apiDesc}\n`);
1890
+ }
1891
+ else {
1892
+ // 最终回退:精简骨架模板(保留接口信息,减少 AI-FILL 占位符数量)
1893
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'TECH.md'), `# ${section.name} - 技术方案
1894
+
1895
+ > ⚠️ 本文档由 split 自动生成框架,内容待 AI execute 阶段填充。
1896
+ > 参考:_shared/CONTEXT.md、00-specs/REQ.md、020-specs/overview/TECH.md
1897
+
1898
+ ## 1. 方案概述
1899
+ <!-- AI-FILL: 简述本任务的业务背景和技术目标 -->
1900
+
1901
+ ## 2. 接口设计
1902
+ ${apiDesc}
1903
+
1904
+ ### 统一响应格式
1905
+ \`\`\`json
1906
+ { "code": 0, "message": "success", "data": {} }
1907
+ \`\`\`
1908
+
1909
+ ## 3. 数据模型
1910
+ <!-- AI-FILL: 分析接口参数,设计 Entity/DTO/VO -->
1911
+
1912
+ ## 4. 核心逻辑
1913
+ <!-- AI-FILL: 描述关键业务流程和边界条件 -->
1914
+
1915
+ ## 5. 测试策略
1916
+ - 单元测试覆盖核心 Service 逻辑
1917
+ - 接口测试覆盖正常/异常/边界
1918
+ - 自动化测试通过后方可提 PR
1919
+ `);
1920
+ }
1921
+ }
1922
+ // v7.4.0+: DEV_GUIDE.md 写入
1923
+ // v8.3.0+: 三级回退 — AI split 生成 → analyze DEV_GUIDE.md 提取 → 结构化模板
1924
+ const aiDevGuideContent = section._devGuideContent;
1925
+ const analyzeDevGuideContent = extractTaskDevGuideContent(specContents, section, taskPlatforms);
1926
+ // v8.3.160+: 提升阈值,确保内容质量(200/300 字符起用)
1927
+ if (aiDevGuideContent && aiDevGuideContent.length > 200) {
1928
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'DEV_GUIDE.md'), `# ${section.name} - 开发者实现指南\n\n${aiDevGuideContent}\n`);
1929
+ }
1930
+ else if (analyzeDevGuideContent && analyzeDevGuideContent.length > 300) {
1931
+ // v8.3.0+: 从 analyze DEV_GUIDE.md 提取本任务相关内容
1932
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'DEV_GUIDE.md'), `# ${section.name} - 开发者实现指南\n\n> 任务: ${taskId} | ${section.name}\n> 本文档面向开发者,提供可执行的实现指导。\n> 来源: analyze → DEV_GUIDE.md(自动提取)\n\n${analyzeDevGuideContent}\n\n## 全局开发指南引用\n- 环境/规范/联调 → ../../../overview/DEV_GUIDE.md\n`);
1933
+ }
1934
+ else {
1935
+ // v8.3.160+: 第三级回退 — 从 section.content 提取开发相关内容
1936
+ const sectionDevExtract = extractRelevantSection(section.content, section.name, '步骤 实现 改造 文件 函数 类 接口 验证 测试');
1937
+ if (sectionDevExtract && sectionDevExtract.length > 100) {
1938
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'DEV_GUIDE.md'), `# ${section.name} - 开发者实现指南\n\n> 任务: ${taskId} | ${section.name}
1939
+ > 本文档面向开发者,提供可执行的实现指导。
1940
+ > 来源: split 阶段 section 原始内容(开发相关段落自动提取)
1941
+
1942
+ ${sectionDevExtract}
1943
+
1944
+ ## 全局开发指南引用
1945
+ - 环境/规范/联调 → ../../../overview/DEV_GUIDE.md
1946
+ `);
1947
+ }
1948
+ else {
1949
+ // 最终回退:结构化骨架模板(AI execute 阶段填充)
1950
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'DEV_GUIDE.md'), `# ${section.name} - 开发者实现指南\n\n> 任务: ${taskId} | ${section.name}\n> 本文档面向开发者,提供可执行的实现指导。\n\n## 1. 本任务改造范围\n\n<!-- AI-FILL: execute 阶段根据 REQ.md 和 TECH.md 填充 -->\n| 类型 | 文件/目录 | 说明 |\n| :--- | :--- | :--- |\n| 新增 | | |\n| 修改 | | |\n| 删除 | | |\n\n## 2. 实施步骤(按依赖排序)\n\n<!-- AI-FILL: Step-by-step 开发步骤,具体到文件/函数级 -->\n- **Step 1**: ...(为什么先做,依赖什么)\n- **Step 2**: ...(依赖 Step 1 的什么产出)\n- ...\n\n## 3. 关键代码指引\n\n<!-- AI-FILL: 核心逻辑的伪代码或改造前后对比 -->\n\n### 3.1 核心改造点 A\n- **文件**: \`xxx.ts\`\n- **改造**: ...\n- **代码示例**:\n \`\`\`typescript\n // 改造后\n \`\`\`\n\n## 4. 接口契约\n\n<!-- AI-FILL: 本任务涉及的前后接口对照 -->\n| 接口 | 路径 | 后端实现 | 前端调用 | 状态 |\n| :--- | :--- | :--- | :--- | :--- |\n\n## 5. 每步验证\n\n<!-- AI-FILL: 每步改完怎么验证 -->\n| 步骤 | 验证命令/操作 | 通过标准 |\n| :--- | :--- | :--- |\n\n## 6. 回滚方案\n\n<!-- AI-FILL: 改坏了怎么回退 -->\n- 数据库: ...\n- 代码: ...\n\n## 7. 已知坑点\n\n<!-- AI-FILL: 常见坑点及解决方式 -->\n- ⚠️ **坑 1**: ...(解决方式)\n- ⚠️ **坑 2**: ...(解决方式)\n\n## 全局开发指南引用\n- 环境/规范/联调 → ../../../overview/DEV_GUIDE.md\n`);
1951
+ }
1952
+ }
1953
+ if (section.content.match(/数据库|数据表|表结构|DDL|ALTER|建表|索引/)) {
1954
+ // v8.3.239+: 三层匹配提取 DDL(兼容 {feature}/overview/TECH.md 嵌套键,旧扁平键取不到 DDL)
1955
+ const bestTechMatch = findBestSpecMatch(specContents, section.name, undefined, 'TECH.md');
1956
+ const schemaMaterial = extractRelevantSection((bestTechMatch && bestTechMatch.score >= 20 ? specContents[bestTechMatch.key] : specContents['TECH.md']) || '', section.name, 'DDL 建表 表结构 索引 CREATE TABLE');
1957
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'SCHEMA.md'), generateSchemaTemplate(section, schemaMaterial));
1958
+ }
1959
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'CHANGELOG.md'), `# ${section.name} - 变更记录
1960
+
1961
+ | 时间 | 版本 | 变更内容 | 变更人 |
1962
+ | :--- | :--- | :--- | :--- |
1963
+ | ${today} | v1.0 | 初始创建 | CLI |
1964
+ `);
1965
+ // API_CONTRACT.yaml 统一放 _shared/
1966
+ // v8.3.239+: 全局契约是单一真相源——已写入时不再覆盖(保留 rate limit/幂等性/版本策略),
1967
+ // section 提取版仅作兜底(全局契约不存在时)
1968
+ if (apiLines.length > 0 && !(await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml')))) {
1969
+ // 从 section 内容提取每个 API 的请求/响应字段
1970
+ const sectionContent = section.content || '';
1971
+ const apiFieldMap = {};
1972
+ for (const l of apiLines) {
1973
+ const parts = l.split('|').map(p => p.trim()).filter(Boolean);
1974
+ const path = parts[1] || '/api/unknown';
1975
+ const apiIdx = sectionContent.indexOf(path);
1976
+ if (apiIdx >= 0) {
1977
+ const after = sectionContent.slice(apiIdx, apiIdx + 800);
1978
+ const fields = [];
1979
+ const fieldRegex = /[`']([a-zA-Z]\w{1,25})[`']\s*[((]?[::]?\s*(?:类型[::])?\s*([\u4e00-\u9fa5A-Za-z]+)/g;
1980
+ let fm;
1981
+ while ((fm = fieldRegex.exec(after)) !== null) {
1982
+ if (!['api', 'data', 'code', 'message', 'msg', 'status', 'ok', 'err'].includes(fm[1].toLowerCase())) {
1983
+ fields.push(`${fm[1]}: ${fm[2]}`);
1984
+ }
1985
+ }
1986
+ if (fields.length > 0)
1987
+ apiFieldMap[path] = fields.slice(0, 10);
1988
+ }
1989
+ }
1990
+ const contracts = apiLines.map(l => {
1991
+ const parts = l.split('|').map(p => p.trim()).filter(Boolean);
1992
+ const method = (parts[0] || 'GET').toUpperCase();
1993
+ const path = parts[1] || '/api/unknown';
1994
+ const desc = parts[2] || path;
1995
+ let yaml = ` ${path}:\n ${method}:\n summary: "${desc}"`;
1996
+ if (apiFieldMap[path]) {
1997
+ yaml += `\n fields:`;
1998
+ for (const f of apiFieldMap[path]) {
1999
+ yaml += `\n - "${f}"`;
2000
+ }
2001
+ }
2002
+ yaml += `\n responses:\n "200":\n description: Success`;
2003
+ return yaml;
2004
+ }).join('\n');
2005
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml'), `# ${section.name} - API Contract\n# Auto-generated from split\n\npaths:\n${contracts}\n`);
2006
+ }
2007
+ // ── 4. 子任务目录(按任务类型差异化) ──
2008
+ // 预加载执行产出模板材料(每个子任务都需要)
2009
+ // v8.3.239+: 材料提取统一走三层匹配——兼容 overview/ 扁平键与 {feature}/overview/ 嵌套键
2010
+ // (新结构下 RISK/DEPS/MONITOR 在功能模块目录,旧结构下在 overview/ 平铺)
2011
+ const extractMaterial = (docName) => {
2012
+ const best = findBestSpecMatch(specContents, section.name, undefined, docName);
2013
+ if (best && best.score >= 20) {
2014
+ return extractRelevantSection(specContents[best.key], section.name);
2015
+ }
2016
+ return extractRelevantSection(specContents[docName] || '', section.name);
2017
+ };
2018
+ const testMaterial = extractMaterial('TEST.md');
2019
+ // v8.3.239+: 分析层 9 份名单无 REVIEW/DEPLOY,提取恒空 → 不再提取,走内容感知模板
2020
+ // (generateReviewChecklist/generateDeployChecklist 保留 material 参数,未来补提取源即可恢复)
2021
+ const riskMaterial = extractMaterial('RISK.md');
2022
+ const depsMaterial = extractMaterial('DEPS.md');
2023
+ const monitorMaterial = extractMaterial('MONITOR.md');
2024
+ // v8.3.239+: 子任务文档按类型裁剪——bugfix 轻量(修缺陷无需部署/监控/前端设计),
2025
+ // refactor 中等(保留风险与组件树,跳过路由/样式/部署/监控),feature 全套
2026
+ const docSet = {
2027
+ hasRisk: taskType !== 'bugfix',
2028
+ hasMonitor: taskType === 'feature',
2029
+ hasDeploy: taskType === 'feature',
2030
+ hasFeDesign: taskType === 'feature', // ROUTES/STATE/STYLE_GUIDE
2031
+ hasComponentTree: taskType !== 'bugfix', // feature/refactor 前端影响面
2032
+ hasAdr: taskType !== 'bugfix',
2033
+ };
2034
+ // ── 4a. research 类型:不创建平台/子任务目录,直接产出调研文档 ──
2035
+ if (isResearch) {
2036
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, 'RESEARCH.md'), `# ${section.name} — 调研报告
2037
+
2038
+ ## 调研目标
2039
+ ${section.content}
2040
+
2041
+ ## 调研结果
2042
+ <!-- AI-FILL: 调研发现、技术选型分析 -->
2043
+
2044
+ ## 推荐方案
2045
+ <!-- AI-FILL: 推荐方案及理由 -->
2046
+
2047
+ ## 风险与建议
2048
+ <!-- AI-FILL -->
2049
+ `);
2050
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, 'COMPARISON.md'), `# ${section.name} — 方案对比
2051
+
2052
+ | 维度 | 方案 A | 方案 B | 方案 C |
2053
+ |:---|:---|:---|:---|
2054
+ | 技术栈 | | | |
2055
+ | 开发成本 | | | |
2056
+ | 性能 | | | |
2057
+ | 可维护性 | | | |
2058
+ | 社区生态 | | | |
2059
+
2060
+ ## 综合评价
2061
+ <!-- AI-FILL -->
2062
+ `);
2063
+ }
2064
+ else {
2065
+ // ── 4b. feature/bugfix/refactor:端平铺 → 子任务(v6.49.0+ 统一架构)──
2066
+ // 创建子任务的通用函数
2067
+ const createSubtask = async (subtaskDir, subtaskId, platformName, platformLabel, isBk, hours) => {
2068
+ // .meta/
2069
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(subtaskDir, '.meta'));
2070
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, '.meta', 'type'), taskType);
2071
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, '.meta', 'status'), 'todo');
2072
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, '.meta', 'owner'), owner);
2073
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, '.meta', 'created-at'), today);
2074
+ // v8.3.5+: 子任务工时(按端均分或保持父任务工时)
2075
+ const subHours = complexity.estimatedHours || 8;
2076
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, '.meta', 'estimated-hours'), String(subHours));
2077
+ // 功能单元标识(v6.49.2+):默认取 section 的 functionalUnit 或 section.name
2078
+ const featureName = section.functionalUnit || section.name || '未分类';
2079
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, '.meta', 'feature'), featureName);
2080
+ // v8.3.4+: git-config 自动填充 — 读取迭代级/全局级实际配置值写入
2081
+ // v8.3.118+: 传入全局 branch_types,让子任务 git-config 自动填充前缀/后缀/源分支
2082
+ const gitConfig = (0, git_integration_1.loadGitConfig)(iterationName);
2083
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, '.meta', 'git-config'), buildGitConfigContent(gitConfig, platformLabel, section.type, branchTypes));
2084
+ // v6.70.0+: 从 section 提取接口/页面清单用于 TASK.md
2085
+ const apiLines = section.content.split(/\r?\n/).filter(l => l.includes('| GET') || l.includes('| POST') || l.includes('| PUT') || l.includes('| DELETE') || l.includes('| PATCH'));
2086
+ const apiList = apiLines.length > 0
2087
+ ? apiLines.map(l => `- ${l.trim()}`).join('\n')
2088
+ : '- 待补充(从 00-specs/REQ.md 和 TECH.md 提取)';
2089
+ // 前端页面清单(如果是前端端)
2090
+ const pageLines = !isBk ? section.content.split(/\r?\n/).filter(l => /页面[::]|路由[::]|path[::]|\/\w+/.test(l)) : [];
2091
+ const pageList = pageLines.length > 0
2092
+ ? pageLines.map(l => `- ${l.trim()}`).join('\n')
2093
+ : '- 待补充(从 00-specs/REQ.md 和 TECH.md 提取)';
2094
+ // 依赖信息
2095
+ const dependsOn = (section._dependsOn || []).join(', ') || '无';
2096
+ const sharedCap = section._sharedCapability || '无';
2097
+ const crossDesc = section._description || '—';
2098
+ // TASK.md
2099
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'TASK.md'), `# ${section.name} — ${platformLabel}
2100
+
2101
+ ## 子任务信息
2102
+ - **子任务 ID**: \`${subtaskId}\`
2103
+ - **所属模块**: \`${taskId}\`
2104
+ - **功能单元**: ${featureName}
2105
+ - **端**: ${platformName}
2106
+ - **负责人**: ${owner}
2107
+ - **状态**: 待开发
2108
+ - **预计耗时**: ${hours}h
2109
+
2110
+ ## 共享规格引用
2111
+ - REQ.md → ../../../00-specs/REQ.md
2112
+ - TECH.md → ../../../00-specs/TECH.md
2113
+ - DEV_GUIDE.md → ../../../00-specs/DEV_GUIDE.md
2114
+ - API_CONTRACT.yaml → ../../../_shared/API_CONTRACT.yaml
2115
+ - CONTEXT.md → ../../../_shared/CONTEXT.md
2116
+ - 全局开发指南 → ../../../overview/DEV_GUIDE.md
2117
+
2118
+ ## 跨端关联
2119
+ - **共享能力**: ${sharedCap}
2120
+ - **依赖任务**: ${dependsOn}
2121
+ - **跨端说明**: ${crossDesc}
2122
+
2123
+ ## 本任务改造范围(代码级)
2124
+
2125
+ <!-- AI-FILL: execute 阶段根据 DEV_GUIDE.md 和 TECH.md 填充具体文件 -->
2126
+ | 类型 | 文件/目录 | 说明 |
2127
+ | :--- | :--- | :--- |
2128
+ | 新增 | | |
2129
+ | 修改 | | |
2130
+ | 删除 | | |
2131
+
2132
+ ## 工作清单
2133
+
2134
+ ### 第一阶段:需求确认
2135
+ - [ ] 阅读 00-specs/REQ.md 确认本任务需求范围
2136
+ - [ ] 阅读 00-specs/DEV_GUIDE.md 确认改造范围和顺序
2137
+ - [ ] 阅读 _shared/API_CONTRACT.yaml 确认接口契约
2138
+ - [ ] 阅读 _shared/CONTEXT.md 确认跨端关联
2139
+
2140
+ ### 第二阶段:技术方案
2141
+ - [ ] 阅读 00-specs/TECH.md 确认技术方案
2142
+ ${isBk ? `- [ ] 设计本端负责的接口(路径/方法/参数/响应/错误码)
2143
+ - [ ] 设计数据模型(Entity/DTO/VO/表结构)
2144
+ - [ ] 设计业务逻辑流程和边界条件` : `- [ ] 设计本端负责的页面(路由/组件/状态)
2145
+ - [ ] 确认 API 调用链(调哪些后端接口)
2146
+ - [ ] 设计字段映射和表单校验规则`}
2147
+
2148
+ ### 第三阶段:开发实施(按依赖排序)
2149
+ ${isBk ? `- [ ] 实现数据库变更(如有)— 先改表结构
2150
+ - [ ] 实现 Entity/Repository 层 — 依赖表结构
2151
+ - [ ] 实现 Service/UseCase 层 — 依赖 Repository
2152
+ - [ ] 实现 Controller/Handler 层 — 依赖 Service
2153
+ - [ ] 实现单元测试(覆盖核心逻辑和边界条件)
2154
+ - [ ] 自测通过(接口通、数据对、边界覆盖)` : `- [ ] 确认后端接口已就绪(依赖后端先完成)
2155
+ - [ ] 实现 API 调用封装(请求/错误处理/重试)
2156
+ - [ ] 实现状态管理(全局状态 + 本地状态)
2157
+ - [ ] 实现页面组件和路由配置
2158
+ - [ ] 实现表单校验和字段映射
2159
+ - [ ] 实现 UI 测试(关键流程覆盖)
2160
+ - [ ] 自测通过(页面渲染、交互响应、API 连通)`}
2161
+
2162
+ ### 第四阶段:验收交付
2163
+ - [ ] 更新 TASK.md 进度
2164
+ - [ ] 提交代码并关联本任务
2165
+ - [ ] 通知相关端联调
2166
+
2167
+ ## ${isBk ? '接口清单' : '页面清单'}
2168
+ ${isBk ? apiList : pageList}
2169
+
2170
+ ## 产出物
2171
+ | 产出物 | 状态 | 路径 | 说明 |
2172
+ | :--- | :--- | :--- | :--- |
2173
+ | TASK.md | ✅ | ./TASK.md | 本文件 |
2174
+ | TEST.md | ⏳ | ./TEST.md | ${isBk ? '单元测试 + 接口测试' : 'UI 测试 + 组件测试'} |
2175
+ ${docSet.hasRisk ? '| RISK.md | ⏳ | ./RISK.md | 风险评估 |\n' : ''}| DEPS.md | ⏳ | ./DEPS.md | 依赖清单 |
2176
+ ${docSet.hasMonitor ? '| MONITOR.md | ⏳ | ./MONITOR.md | 监控指标 |\n' : ''}| REVIEW.md | ⏳ | ./REVIEW.md | 评审清单 |
2177
+ ${docSet.hasDeploy ? '| DEPLOY.md | ⏳ | ./DEPLOY.md | 部署检查 |\n' : ''}| ERROR_CODES.md | ⏳ | ./ERROR_CODES.md | 错误码定义 |
2178
+ ${!isBk && docSet.hasComponentTree ? '| COMPONENT_TREE.md | ⏳ | ./COMPONENT_TREE.md | 组件树 |\n' : ''}${!isBk && docSet.hasFeDesign ? '| ROUTES.md | ⏳ | ./ROUTES.md | 路由设计 |\n| STATE.md | ⏳ | ./STATE.md | 状态管理 |\n| STYLE_GUIDE.md | ⏳ | ./STYLE_GUIDE.md | 样式规范 |\n' : ''}> ⚠️ 代码输出位置:AI 生成的代码写入 CONSTITUTION.md/PROJECT.yaml 中声明的「源码路径」,
2179
+ > 不写入迭代目录。子任务目录只存放规格文档。
2180
+
2181
+ ## 变更履历
2182
+ | 时间 | 变更内容 | 变更人 |
2183
+ | :--- | :--- | :--- |
2184
+ | ${today} | 创建子任务 | CLI |
2185
+ `);
2186
+ // 前端设计文档(仅非 backend 端,按任务类型裁剪:bugfix 跳过,refactor 仅组件树)
2187
+ if (!isBk && docSet.hasComponentTree) {
2188
+ const feContent = extractTaskTechContent(specContents, section, platformName);
2189
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'COMPONENT_TREE.md'), generateComponentTree(section, platformName, feContent));
2190
+ if (docSet.hasFeDesign) {
2191
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'ROUTES.md'), generateRoutesDoc(section, platformName, feContent));
2192
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'STATE.md'), generateStateDoc(section, platformName, feContent));
2193
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'STYLE_GUIDE.md'), generateStyleGuide(section, platformName, feContent));
2194
+ }
2195
+ }
2196
+ // v8.3.60+: 自动生成 VERIFY_SPEC.yaml(从源码路由扫描,所有前端端均生成)
2197
+ if (!isBk) {
2198
+ try {
2199
+ const projectConfig = await (0, unified_config_1.loadProjectConfig)();
2200
+ const platformConfig = projectConfig.platforms.find(p => p.name === platformName);
2201
+ const codePath = platformConfig?.code_path
2202
+ ? ((0, path_1.isAbsolute)(platformConfig.code_path)
2203
+ ? platformConfig.code_path
2204
+ : (0, path_1.join)(process.cwd(), platformConfig.code_path))
2205
+ : null;
2206
+ if (codePath && await (0, fs_extra_1.pathExists)(codePath)) {
2207
+ const routeResult = await (0, ui_verify_1.scanRoutes)(codePath);
2208
+ if (routeResult.routes.length > 0) {
2209
+ const baseUrl = 'http://localhost:3000'; // 默认值,开发时调整
2210
+ const spec = (0, ui_verify_1.generateSpecFromRoutes)(routeResult.routes, {
2211
+ baseUrl,
2212
+ platform: platformName,
2213
+ name: `${section.name} - ${platformName} UI 验证`,
2214
+ });
2215
+ await (0, ui_verify_1.writeVerifySpec)(spec, (0, path_1.join)(subtaskDir, 'VERIFY_SPEC.yaml'));
2216
+ logger_1.logger.info(` 📄 VERIFY_SPEC.yaml 已自动生成(${routeResult.routes.length} 个场景)`);
2217
+ }
2218
+ }
2219
+ }
2220
+ catch {
2221
+ // 静默失败:源码不可用或扫描失败时不阻断 split 流程
2222
+ }
2223
+ }
2224
+ // 执行产出文档(按任务类型裁剪:bugfix 无 RISK/MONITOR/DEPLOY,refactor 无 MONITOR/DEPLOY)
2225
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'TEST.md'), generateTestOutline(section, testMaterial));
2226
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'REVIEW.md'), generateReviewChecklist(section));
2227
+ if (docSet.hasDeploy)
2228
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'DEPLOY.md'), generateDeployChecklist(section));
2229
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'ERROR_CODES.md'), generateErrorCodes(section));
2230
+ if (docSet.hasRisk)
2231
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'RISK.md'), generateRiskTemplate(section, riskMaterial));
2232
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'DEPS.md'), generateDepsTemplate(section, depsMaterial));
2233
+ if (docSet.hasMonitor)
2234
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'MONITOR.md'), generateMonitorTemplate(section, monitorMaterial));
2235
+ // v8.0.0+: 预创建 REQ.md 和 TECH.md 骨架文件
2236
+ // AI 用 Write 覆盖这些文件,不再自行创建路径(消除路径错误)
2237
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'REQ.md'), `${spec_skeleton_1.SKELETON_MARKER}\n# ${section.name} — ${platformLabel}\n\n> 本文件由 CLI 预创建,请用 Write 工具覆盖为专业需求规格。\n\n## 功能描述\n<!-- AI-FILL: 本端的需求描述 -->\n\n## 验收标准\n<!-- AI-FILL: Given/When/Then 格式 -->\n\n## 业务规则\n<!-- AI-FILL -->\n\n## 边界条件\n<!-- AI-FILL -->\n\n## 异常场景\n<!-- AI-FILL -->\n`);
2238
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'TECH.md'), `${spec_skeleton_1.SKELETON_MARKER}\n# ${section.name} — ${platformLabel} 技术方案\n\n> 本文件由 CLI 预创建,请用 Write 工具覆盖为专业技术方案。\n\n## 接口定义\n<!-- AI-FILL: 请求/响应结构体 -->\n\n## 数据模型\n<!-- AI-FILL: 字段/类型/约束 -->\n\n## 核心逻辑\n<!-- AI-FILL -->\n\n## 依赖说明\n<!-- AI-FILL -->\n`);
2239
+ if (docSet.hasAdr) {
2240
+ const adr = generateAdr(section);
2241
+ if (adr) {
2242
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(subtaskDir, 'ADR.md'), adr);
2243
+ }
2244
+ }
2245
+ };
2246
+ // ── 所有端平铺:{端名}/{子任务}/ (v6.49.2+ 统一架构)──
2247
+ // 不再区分前后端,所有端平铺在任务目录下
2248
+ // 子任务目录名使用 generateSubtaskId 生成的全局唯一 ID:Task-{num}-{platform}
2249
+ for (const platform of taskPlatforms) {
2250
+ const platformDir = (0, path_1.join)(taskDir, platform);
2251
+ const subtaskId = subtaskIdMap.get(platform);
2252
+ // 子任务目录名 = subtaskId(如 Task-001-booking-service)
2253
+ const subtaskDir = (0, path_1.join)(platformDir, subtaskId);
2254
+ const subtaskHours = complexity.estimatedHours || 8;
2255
+ // 判断是否后端(用于生成不同的文档内容)
2256
+ const isBk = platform === 'backend' || platform.startsWith('后台') || /-(service|api|server|backend)$/i.test(platform);
2257
+ await createSubtask(subtaskDir, subtaskId, platform, isBk ? '后端' : platform, isBk, subtaskHours);
2258
+ }
2259
+ }
2260
+ // 确保至少有 backend 子任务(AI 未输出 backend 时自动补充,仅非 research)
2261
+ // v6.48.0+:从 CONSTITUTION.md 端列表查找后端端名,不再硬编码 'backend'
2262
+ const hasAnyBackend = taskPlatforms.some((p) => p === 'backend' || p.startsWith('后台') || /-(service|api|server|backend)$/i.test(p));
2263
+ if (!isResearch && !hasAnyBackend) {
2264
+ // 从端列表查找第一个后端端名作为 fallback(v6.48.0+)
2265
+ const allPlatforms = await (0, spec_paths_1.parsePlatformList)();
2266
+ const fallbackBackend = allPlatforms.find(p => p === 'backend' || p.startsWith('后台') || /-(service|api|server|backend)$/i.test(p)) || 'backend';
2267
+ const fallbackSubtaskId = subtaskIdMap.get(fallbackBackend) || generateSubtaskId(taskNum, fallbackBackend);
2268
+ const autoSubtaskDir = (0, path_1.join)(taskDir, fallbackBackend, fallbackSubtaskId);
2269
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(autoSubtaskDir, '.meta'));
2270
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'type'), taskType);
2271
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'status'), 'todo');
2272
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'owner'), owner);
2273
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'created-at'), today);
2274
+ // v8.3.5+: 子任务工时
2275
+ const autoHours = complexity.estimatedHours || 8;
2276
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'estimated-hours'), String(autoHours));
2277
+ // 功能单元标识(v6.49.2+)
2278
+ const featureName = section.functionalUnit || section.name || '未分类';
2279
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'feature'), featureName);
2280
+ // v8.3.4+: git-config 自动填充
2281
+ // v8.3.118+: 传入全局 branch_types
2282
+ const gitConfig = (0, git_integration_1.loadGitConfig)(iterationName);
2283
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, '.meta', 'git-config'), buildGitConfigContent(gitConfig, '后端', section.type, branchTypes));
2284
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, 'TASK.md'), `# ${section.name} — ${fallbackBackend}(自动补充)
2285
+
2286
+ ## 子任务信息
2287
+ - **子任务 ID**: \`${taskId}-${fallbackBackend}-auto\`
2288
+ - **所属模块**: \`${taskId}\`
2289
+ - **功能单元**: ${featureName}
2290
+ - **端**: ${fallbackBackend}
2291
+ - **负责人**: ${owner}
2292
+ - **状态**: 待开发
2293
+
2294
+ ## 共享规格引用
2295
+ - REQ.md → ../../../00-specs/REQ.md
2296
+ - TECH.md → ../../../00-specs/TECH.md
2297
+ `);
2298
+ // 自动补充的后端也需要执行产出文档(按任务类型裁剪)
2299
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, 'TEST.md'), generateTestOutline(section, testMaterial));
2300
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, 'REVIEW.md'), generateReviewChecklist(section));
2301
+ if (docSet.hasDeploy)
2302
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, 'DEPLOY.md'), generateDeployChecklist(section));
2303
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, 'ERROR_CODES.md'), generateErrorCodes(section));
2304
+ if (docSet.hasRisk)
2305
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, 'RISK.md'), generateRiskTemplate(section, riskMaterial));
2306
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, 'DEPS.md'), generateDepsTemplate(section, depsMaterial));
2307
+ if (docSet.hasMonitor)
2308
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(autoSubtaskDir, 'MONITOR.md'), generateMonitorTemplate(section, monitorMaterial));
2309
+ taskPlatforms.push(fallbackBackend);
2310
+ subtaskIdMap.set(fallbackBackend, `${taskId}-${fallbackBackend}-auto`);
2311
+ }
2312
+ // ─ 5. 任务上下文 CONTEXT.md ─
2313
+ const sourceFile = section._sourceFile || '';
2314
+ const topic = section._topic || slugify(section.name);
2315
+ // 推导 010-requirements 源路径
2316
+ const reqSourcePath = sourceFile
2317
+ ? `010-requirements/${sourceFile}`
2318
+ : `010-requirements/${taskType === 'feature' ? 'features' : taskType === 'bugfix' ? 'bugs' : taskType === 'refactor' ? 'refactors' : 'research'}/${topic}.md`;
2319
+ // 推导 020-specs 分析文档路径
2320
+ const specSourcePath = sourceFile
2321
+ ? `020-specs/${sourceFile}`
2322
+ : `020-specs/${taskType === 'feature' ? 'features' : taskType === 'bugfix' ? 'bugs' : taskType === 'refactor' ? 'refactors' : 'research'}/${topic}.md`;
2323
+ // 任务类型中文标签
2324
+ const typeLabels = { feature: '功能开发', bugfix: '缺陷修复', refactor: '重构优化', research: '技术调研' };
2325
+ const typeLabel = typeLabels[taskType] || taskType;
2326
+ // 收集同迭代的其它任务(用于关联关系)
2327
+ const relatedTasks = allSections
2328
+ ? allSections
2329
+ .filter((s) => s !== section)
2330
+ .slice(0, 5)
2331
+ .map((s, idx) => {
2332
+ const sId = s._taskId || `Task-${String(allSections.indexOf(s) + 1).padStart(3, '0')}`;
2333
+ const sType = s._taskType || 'feature';
2334
+ return `- \`${sId}\` (${s.name}) — ${typeLabels[sType] || sType}`;
2335
+ })
2336
+ : [];
2337
+ // 提取原始描述摘要(从 REQ.md 内容截取前 500 字)
2338
+ const reqMdPath = (0, path_1.join)(taskDir, '00-specs', 'REQ.md');
2339
+ let originalDesc = '';
2340
+ if (await (0, fs_extra_1.pathExists)(reqMdPath)) {
2341
+ const reqContent = await (0, fs_extra_1.readFile)(reqMdPath, 'utf-8');
2342
+ // 跳过标题行,取正文前 500 字
2343
+ const body = reqContent.replace(/^#[^\n]*\n/, '').trim();
2344
+ originalDesc = body.length > 500 ? body.slice(0, 500) + '...' : body;
2345
+ }
2346
+ // v6.69.3+: CONTEXT.md 写入 _shared/(符合规范),同时保留 00-specs/ 副本供兼容
2347
+ const contextContent = `# 任务上下文
2348
+
2349
+ ## 来源追溯
2350
+
2351
+ | 属性 | 值 |
2352
+ |:---|:---|
2353
+ | 任务类型 | ${typeLabel}(\`${taskType}\`) |
2354
+ | 需求文档 | \`${reqSourcePath}\` |
2355
+ | 分析文档 | \`${specSourcePath}\` |
2356
+ | 功能单元 | ${section.functionalUnit || section.name} |
2357
+ | 拆分原因 | ${section._reason || '—'} |
2358
+
2359
+ ## 原始描述
2360
+
2361
+ ${originalDesc || '> 待补充(执行 analyze 后自动生成)'}
2362
+
2363
+ ## 关联任务
2364
+
2365
+ ${relatedTasks.length > 0 ? relatedTasks.join('\n') : '> 暂无关联任务(split 时自动填充)'}
2366
+
2367
+ ## 跨端关联
2368
+
2369
+ > 来自 FUNCTION_MAP.md(功能×端映射参考)
2370
+
2371
+ | 属性 | 值 |
2372
+ |:---|:---|
2373
+ | 共享能力 | ${section._sharedCapability || '无'} |
2374
+ | 依赖任务 | ${(section._dependsOn || []).join(', ') || '无'} |
2375
+ | 跨端说明 | ${section._description || '—'} |
2376
+
2377
+ ## 影响范围
2378
+
2379
+ | 端 | 影响说明 |
2380
+ |:---|:---|
2381
+ ${taskPlatforms.map((p) => `| ${p} | 待补充 |`).join('\n')}
2382
+ `;
2383
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'CONTEXT.md'), contextContent);
2384
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '00-specs', 'CONTEXT.md'), contextContent);
2385
+ // ── 7. 问题追踪 ──
2386
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.issues.md'), `# ${section.name} - 问题追踪\n\n> 执行过程中发现的问题记录于此。\n\n`);
2387
+ }
2388
+ async function updateProjectGraph(iterationDir, sections) {
2389
+ const graphPath = (0, path_1.join)(iterationDir, '000-overview', 'PROJECT_GRAPH.md');
2390
+ let content = '';
2391
+ if (await (0, fs_extra_1.pathExists)(graphPath)) {
2392
+ content = await (0, fs_extra_1.readFile)(graphPath, 'utf-8');
2393
+ }
2394
+ for (let i = 0; i < sections.length; i++) {
2395
+ const taskId = sections[i]._taskId || `Task-${String(i + 1).padStart(3, '0')}`;
2396
+ let taskName = sections[i].name;
2397
+ while (/端端/.test(taskName))
2398
+ taskName = taskName.replace('端端', '端');
2399
+ if (!content.includes(taskId)) {
2400
+ const taskEntry = `| ${taskId} | ${taskName} | ${sections[i]._taskType || 'feature'} | 0% | 🔲 待开发 | |\n`;
2401
+ content = content.replace('| 任务编号 | 任务名称 | 类型 | 进度 | 状态 | 负责人 |\n| :--- | :--- | :--- | :--- | :--- | :--- |\n', `| 任务编号 | 任务名称 | 类型 | 进度 | 状态 | 负责人 |\n| :--- | :--- | :--- | :--- | :--- | :--- |\n${taskEntry}`);
2402
+ }
2403
+ }
2404
+ await (0, fs_extra_1.writeFile)(graphPath, content);
2405
+ }
2406
+ /**
2407
+ * 根据需求内容自动生成测试用例框架
2408
+ */
2409
+ function generateTestOutline(section, material) {
2410
+ // v8.3.239+: 阈值统一 200 字符,低质量提取片段不再替换内容感知模板
2411
+ if (material && material.trim().length > 200) {
2412
+ return `# ${section.name} — 测试用例\n\n> 来源: analyze → TEST.md(自动提取)\n\n${material.trim()}\n`;
2413
+ }
2414
+ const name = section.name;
2415
+ const content = section.content || '';
2416
+ const isBackend = section.platform?.startsWith('后台') || false;
2417
+ const hasApi = content.includes('/api/') || content.includes('接口');
2418
+ const hasDb = content.includes('数据表') || content.includes('数据库') || content.includes('表');
2419
+ // v6.79.0+: 从 content 提取 API 表格生成针对性测试用例
2420
+ const apiRows = extractApiRowsFromContent(content);
2421
+ let outline = `# ${name} — 测试用例\n\n`;
2422
+ outline += `> 自动生成于 split,请在编码后补充具体用例\n\n`;
2423
+ outline += `## 1. 单元测试\n\n`;
2424
+ if (isBackend && hasApi && apiRows.length > 0) {
2425
+ outline += `| 用例 | 接口 | 输入 | 预期 | 状态 |\n`;
2426
+ outline += `| :--- | :--- | :--- | :--- | :--- |\n`;
2427
+ for (const row of apiRows.slice(0, 6)) {
2428
+ outline += `| ${row.method} ${row.path} | ${row.path} | 正常参数 | 200 | ⬜ |\n`;
2429
+ outline += `| ${row.method} ${row.path} 异常 | ${row.path} | 非法参数 | 400/500 | ⬜ |\n`;
2430
+ }
2431
+ }
2432
+ else if (isBackend && hasApi) {
2433
+ outline += `| 用例 | 接口 | 输入 | 预期 | 状态 |\n`;
2434
+ outline += `| :--- | :--- | :--- | :--- | :--- |\n`;
2435
+ outline += `| 正常请求 | | | 200 | ⬜ |\n`;
2436
+ outline += `| 参数校验 | | | 400 | ⬜ |\n`;
2437
+ outline += `| 未授权 | | | 401 | ⬜ |\n`;
2438
+ }
2439
+ else {
2440
+ outline += `| 用例 | 场景 | 输入 | 预期 | 状态 |\n`;
2441
+ outline += `| :--- | :--- | :--- | :--- | :--- |\n`;
2442
+ outline += `| 正常渲染 | 默认 | | | ⬜ |\n`;
2443
+ outline += `| 空数据 | 无数据 | | | ⬜ |\n`;
2444
+ }
2445
+ if (hasDb) {
2446
+ outline += `\n## 2. 数据库测试\n\n`;
2447
+ outline += `| 用例 | 表 | 操作 | 预期 | 状态 |\n`;
2448
+ outline += `| :--- | :--- | :--- | :--- | :--- |\n`;
2449
+ outline += `| 事务回滚 | | INSERT/UPDATE | 异常时回滚 | ⬜ |\n`;
2450
+ outline += `| 唯一约束 | | INSERT 重复 | 约束冲突 | ⬜ |\n`;
2451
+ }
2452
+ outline += `\n## 3. 集成测试 / E2E\n\n`;
2453
+ outline += `| 用例 | 流程 | 预期 | 状态 |\n`;
2454
+ outline += `| :--- | :--- | :--- | :--- |\n`;
2455
+ outline += `| 正常流程 | 从头到尾走通 | 成功 | ⬜ |\n`;
2456
+ outline += `| 异常流程 | 中断/超时 | 优雅降级 | ⬜ |\n`;
2457
+ outline += `| 并发 | 多用户同时操作 | 无数据错乱 | ⬜ |\n`;
2458
+ outline += `\n## 4. 性能 / 安全\n\n`;
2459
+ outline += `| 用例 | 指标 | 阈值 | 状态 |\n`;
2460
+ outline += `| :--- | :--- | :--- | :--- |\n`;
2461
+ outline += `| 响应时间 | P99 | < 500ms | ⬜ |\n`;
2462
+ outline += `| 并发容量 | QPS | 满足预期 | ⬜ |\n`;
2463
+ outline += `\n> ⬜ 待编写 | ✅ 通过 | ❌ 失败 | ➖ 不适用\n`;
2464
+ return outline;
2465
+ }
2466
+ /** v6.79.0+: 从任务内容中提取 API 表格行 */
2467
+ function extractApiRowsFromContent(content) {
2468
+ const rows = [];
2469
+ const lines = content.split(/\r?\n/);
2470
+ for (const line of lines) {
2471
+ const m = line.match(/\|\s*(GET|POST|PUT|DELETE|PATCH)\s*\|\s*([^|]+)\|/i);
2472
+ if (m) {
2473
+ rows.push({ method: m[1].toUpperCase(), path: m[2].trim(), desc: '' });
2474
+ }
2475
+ }
2476
+ return rows;
2477
+ }
2478
+ /**
2479
+ * 根据需求内容自动生成代码审查清单
2480
+ */
2481
+ function generateReviewChecklist(section, material) {
2482
+ // v8.3.239+: 阈值统一 200 字符(分析层暂无 REVIEW 提取源,material 恒空,走内容感知模板)
2483
+ if (material && material.trim().length > 200) {
2484
+ return `# ${section.name} — Code Review Checklist\n\n> 来源: analyze → REVIEW.md(自动提取)\n\n${material.trim()}\n`;
2485
+ }
2486
+ const name = section.name;
2487
+ const content = section.content || '';
2488
+ const hasApi = content.includes('/api/') || content.includes('接口');
2489
+ const hasDb = content.includes('数据库') || content.includes('表');
2490
+ const hasBatch = content.includes('批量') || content.includes('导出');
2491
+ const hasAuth = content.includes('权限') || content.includes('角色') || content.includes('认证');
2492
+ const isBackend = section.platform?.startsWith('后台') || false;
2493
+ let checklist = `# ${name} — Code Review Checklist\n\n`;
2494
+ checklist += `> 自动生成于 split,请在提交 PR 前逐项确认\n\n`;
2495
+ checklist += `## 功能正确性\n\n`;
2496
+ checklist += `- [ ] 需求覆盖完整,无遗漏\n`;
2497
+ checklist += `- [ ] 边界条件处理(空值、极值、特殊字符)\n`;
2498
+ checklist += `- [ ] 错误码统一\n\n`;
2499
+ checklist += `## 代码质量\n\n`;
2500
+ checklist += `- [ ] 零 ` + '`any`' + ` 类型\n`;
2501
+ checklist += `- [ ] 无 console.log 残留\n`;
2502
+ checklist += `- [ ] 命名清晰、见名知义\n`;
2503
+ checklist += `- [ ] 无重复代码(>3 次提取为函数)\n\n`;
2504
+ if (isBackend) {
2505
+ checklist += `## 后端专项\n\n`;
2506
+ checklist += `- [ ] 接口幂等性\n`;
2507
+ checklist += `- [ ] 参数校验(@Valid / DTO)\n`;
2508
+ checklist += `- [ ] 防 SQL 注入\n`;
2509
+ checklist += `- [ ] 日志脱敏(密码/手机号不打日志)\n`;
2510
+ if (hasDb) {
2511
+ checklist += `- [ ] 数据库事务边界正确\n`;
2512
+ checklist += `- [ ] 索引是否匹配查询条件\n`;
2513
+ }
2514
+ if (hasBatch) {
2515
+ checklist += `- [ ] 批量操作有上限限制\n`;
2516
+ checklist += `- [ ] 大数据量分页处理\n`;
2517
+ }
2518
+ if (hasAuth) {
2519
+ checklist += `- [ ] 权限校验在每个接口入口(不是中间件漏掉)\n`;
2520
+ }
2521
+ checklist += `\n`;
2522
+ }
2523
+ else {
2524
+ checklist += `## 前端专项\n\n`;
2525
+ checklist += `- [ ] 组件拆分合理(>200 行考虑拆分)\n`;
2526
+ checklist += `- [ ] 无 XSS 漏洞(v-html 审查)\n`;
2527
+ checklist += `- [ ] 响应式适配\n`;
2528
+ checklist += `- [ ] 加载态 / 空态 / 错误态 / 边界态(四态齐全)\n\n`;
2529
+ }
2530
+ checklist += `## 测试\n\n`;
2531
+ checklist += `- [ ] 核心路径有单元测试\n`;
2532
+ checklist += `- [ ] 参照 \`TEST.md\` 逐项验证\n`;
2533
+ checklist += `- [ ] \`speccore validate --task=${name}\` 通过\n\n`;
2534
+ checklist += `## 自查确认\n\n`;
2535
+ checklist += `- [ ] 已在本地完整跑通\n`;
2536
+ checklist += `- [ ] 相关的 \`REQ.md\` 已更新(如有变化)\n`;
2537
+ checklist += `- [ ] PR 描述写清楚了「做了什么 + 怎么测」\n`;
2538
+ return checklist;
2539
+ }
2540
+ /**
2541
+ * 严格模式:预览拆分方案,逐 section 确认
2542
+ */
2543
+ async function strictSplitPreview(sections, platforms, iterationDir) {
2544
+ const ask = (q) => {
2545
+ process.stdout.write(q);
2546
+ return new Promise((resolve) => {
2547
+ process.stdin.resume();
2548
+ process.stdin.once('data', (data) => {
2549
+ process.stdin.pause();
2550
+ resolve(data.toString().split(/\r?\n/)[0].trim());
2551
+ });
2552
+ });
2553
+ };
2554
+ logger_1.logger.info('\n╔══════════════════════════════════════════╗');
2555
+ logger_1.logger.info('║ 🔍 Strict Split — 预览拆分方案 ║');
2556
+ logger_1.logger.info('╚══════════════════════════════════════════╝\n');
2557
+ logger_1.logger.info(`检测到 ${sections.length} 个章节,${platforms.length} 个端: ${platforms.join(', ')}\n`);
2558
+ const approved = [];
2559
+ for (let i = 0; i < sections.length; i++) {
2560
+ const s = sections[i];
2561
+ const taskId = s._taskId || `Task-${String(i + 1).padStart(3, '0')}`;
2562
+ // Determine target directory
2563
+ const target = s.platform || platforms.join(' + ');
2564
+ logger_1.logger.info(`── ${taskId}: ${s.name} ──`);
2565
+ logger_1.logger.info(` 端: ${target}`);
2566
+ logger_1.logger.info(` 内容: ${(s.content || '').slice(0, 60).replace(/\n/g, ' ')}...`);
2567
+ const answer = (await ask(` → 保留?[y]确认 [e]编辑名称 [a]分配 [N]跳过 [q]取消: `)).toLowerCase();
2568
+ if (answer === 'q') {
2569
+ logger_1.logger.info(' ❌ 取消全部\n');
2570
+ approved.length = 0;
2571
+ break;
2572
+ }
2573
+ if (answer === 'a') {
2574
+ const owner = await ask(` → 分配给谁?(如需要多端,用逗号分隔: 张三(后台),李四(Web)): `);
2575
+ if (owner) {
2576
+ // Store owner info for later use
2577
+ s._owner = owner;
2578
+ logger_1.logger.info(` 👤 负责人: ${owner}`);
2579
+ }
2580
+ approved.push(s);
2581
+ logger_1.logger.info(` ✅ 保留`);
2582
+ }
2583
+ else if (answer === 'e') {
2584
+ const newName = await ask(` → 新名称: `);
2585
+ if (newName) {
2586
+ s.name = newName;
2587
+ logger_1.logger.info(` 📝 已改名: ${newName}`);
2588
+ }
2589
+ approved.push(s);
2590
+ }
2591
+ else if (answer === 'y' || answer === 'yes') {
2592
+ approved.push(s);
2593
+ logger_1.logger.info(` ✅ 保留`);
2594
+ }
2595
+ else {
2596
+ logger_1.logger.info(` ⏭️ 跳过`);
2597
+ }
2598
+ logger_1.logger.info('');
2599
+ }
2600
+ if (approved.length === 0)
2601
+ return [];
2602
+ logger_1.logger.info(`\n 将创建 ${approved.length}/${sections.length} 个任务`);
2603
+ const confirm = await ask(' 确认创建?[y/N] ');
2604
+ logger_1.logger.info('\n✅ 确认创建...\n');
2605
+ (0, next_steps_1.showNextSteps)('split');
2606
+ return approved;
2607
+ }
2608
+ /**
2609
+ * 生成任务间影响关系图 + 风险评分
2610
+ */
2611
+ async function generateImpactGraph(iterationDir, sections, platforms) {
2612
+ const deps = [];
2613
+ const sectionApis = sections.map((s, i) => {
2614
+ const taskId = s._taskId || `Task-${String(i + 1).padStart(3, '0')}`;
2615
+ const apis = (s.content.match(/\/api\/[a-zA-Z0-9\/-]+/g) || []).map(a => a.trim());
2616
+ return { name: taskId, apis };
2617
+ });
2618
+ for (let i = 0; i < sectionApis.length; i++) {
2619
+ for (let j = 0; j < sectionApis.length; j++) {
2620
+ if (i === j)
2621
+ continue;
2622
+ for (const api of sectionApis[j].apis) {
2623
+ if (sections[i].content.includes(api)) {
2624
+ deps.push({ from: sectionApis[i].name, fromName: sections[i].name, to: sectionApis[j].name, toName: sections[j].name, reason: api });
2625
+ break;
2626
+ }
2627
+ }
2628
+ }
2629
+ }
2630
+ const seen = new Set();
2631
+ const uniqueDeps = deps.filter(d => { const k = d.from + d.to; if (seen.has(k))
2632
+ return false; seen.add(k); return true; });
2633
+ let impact = '# IMPACT.md\n\n> auto-generated by split\n\n## Risk Scores\n\n| Task | Risk | Score | Tags | Reasons |\n| :--- | :--- | ---: | :--- | :--- |\n';
2634
+ for (let i = 0; i < sections.length; i++) {
2635
+ const s = sections[i];
2636
+ const taskId = s._taskId || `Task-${String(i + 1).padStart(3, '0')}`;
2637
+ const risk = await (0, risk_scorer_1.scoreRisk)(s.content + s.name, s.name, iterationDir);
2638
+ impact += `| ${taskId}: ${s.name} | ${risk.level} | ${risk.score} | ${risk.tags.join(' ')} | ${risk.reasons.join('; ')} |\n`;
2639
+ const taskType = s._taskType || 'feature';
2640
+ const taskDir = (0, path_1.join)(iterationDir, '030-tasks', taskType, taskId);
2641
+ if (await (0, fs_extra_1.pathExists)(taskDir)) {
2642
+ // 生成风险报告并嵌入 00-specs/TECH.md(去重:只写一次)
2643
+ const taskMdPath = (await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '00-specs', 'TECH.md')))
2644
+ ? (0, path_1.join)(taskDir, '00-specs', 'TECH.md')
2645
+ : (0, path_1.join)(taskDir, '_shared', 'TECH.md');
2646
+ const riskReport = (0, risk_scorer_1.generateRiskReport)(risk);
2647
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '.risk'), riskReport);
2648
+ if (await (0, fs_extra_1.pathExists)(taskMdPath)) {
2649
+ let taskMd = await (0, fs_extra_1.readFile)(taskMdPath, 'utf-8');
2650
+ if (!taskMd.includes('## 风险评估')) {
2651
+ taskMd += '\n\n## 风险评估\n\n' + riskReport.replace('# 风险评估\n\n', '');
2652
+ await (0, fs_extra_1.writeFile)(taskMdPath, taskMd);
2653
+ }
2654
+ }
2655
+ }
2656
+ }
2657
+ // 语义依赖检测
2658
+ const semanticDeps = detectSemanticDependencies(sections);
2659
+ impact += '\n## Dependencies\n\n';
2660
+ if (uniqueDeps.length > 0 || semanticDeps.size > 0) {
2661
+ impact += '| Consumer | -> | Producer | 类型 |\n| :--- | :---: | :--- | :--- |\n';
2662
+ // API 依赖
2663
+ for (const d of uniqueDeps)
2664
+ impact += `| ${d.from}: ${d.fromName.slice(0, 20)} | → | ${d.to}: ${d.toName.slice(0, 20)} | API: \`${d.reason}\` |\n`;
2665
+ // 语义依赖
2666
+ for (const [from, targets] of semanticDeps) {
2667
+ for (const target of targets) {
2668
+ impact += `| ${from} | → | ${target} | 语义推断 |\n`;
2669
+ }
2670
+ }
2671
+ impact += '\n> Consumer tasks must wait for Producer tasks, or pre-define API contracts.\n';
2672
+ }
2673
+ else {
2674
+ impact += 'No task dependencies detected — all tasks can be developed in parallel.\n';
2675
+ }
2676
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(iterationDir, 'IMPACT.md'), impact);
2677
+ logger_1.logger.info(`\nImpact analysis: ${iterationDir}/IMPACT.md`);
2678
+ }
2679
+ function generateSchemaTemplate(section, material) {
2680
+ const name = section.name;
2681
+ if (material && material.trim().length > 200) {
2682
+ return `# ${name} — Database Schema\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
2683
+ }
2684
+ return `# ${name} — Database Schema
2685
+
2686
+ > Auto-generated. Fill in DDL before development.
2687
+
2688
+ ## Tables
2689
+
2690
+ | Table | Purpose | Engine | Charset |
2691
+ | :--- | :--- | :--- | :--- |
2692
+ | | | InnoDB | utf8mb4 |
2693
+
2694
+ ## DDL
2695
+
2696
+ \`\`\`sql
2697
+ -- TODO: Write CREATE TABLE statements
2698
+
2699
+ \`\`\`
2700
+
2701
+ ## Indexes
2702
+
2703
+ | Table | Index | Columns | Type |
2704
+ | :--- | :--- | :--- | :--- |
2705
+ | | | | BTREE |
2706
+
2707
+ ## Migration Plan
2708
+
2709
+ - [ ] Dev: Write DDL in local
2710
+ - [ ] Review: DBA reviews schema changes
2711
+ - [ ] Stage: Run migration on staging
2712
+ - [ ] Prod: Run migration during deployment window
2713
+
2714
+ ## Rollback
2715
+
2716
+ \`\`\`sql
2717
+ -- TODO: Write rollback DDL
2718
+ \`\`\`
2719
+ `;
2720
+ }
2721
+ function generateDeployChecklist(section, material) {
2722
+ // v8.3.239+: 阈值统一 200 字符(分析层暂无 DEPLOY 提取源,material 恒空,走内容感知模板)
2723
+ if (material && material.trim().length > 200) {
2724
+ return `# ${section.name} — 部署检查清单\n\n> 来源: analyze → DEPLOY.md(自动提取)\n\n${material.trim()}\n`;
2725
+ }
2726
+ const name = section.name;
2727
+ const hasDb = section.content.match(/数据库|数据表|DDL|ALTER/) !== null;
2728
+ return `# ${name} — Deployment Checklist
2729
+
2730
+ ## Pre-Deploy
2731
+
2732
+ - [ ] All tests pass (\`speccore verify\`)
2733
+ - [ ] Code review approved (REVIEW.md all checked)
2734
+ - [ ] PR merged to main
2735
+ - [ ] CI/CD pipeline green
2736
+
2737
+ ${hasDb ? '- [ ] DB migration script ready and reviewed\n- [ ] DB backup taken before migration\n' : ''}
2738
+ ## Deploy Steps
2739
+
2740
+ 1. [ ] Merge to release branch
2741
+ 2. [ ] Tag version: \`git tag vX.Y.Z\`
2742
+ 3. [ ] Deploy to staging
2743
+ 4. [ ] Smoke test on staging
2744
+ 5. [ ] Deploy to production
2745
+ ${hasDb ? '6. [ ] Run DB migration\n7. [ ] Verify data integrity\n' : ''}
2746
+ ## Post-Deploy
2747
+
2748
+ - [ ] Monitor error logs (first 30 min)
2749
+ - [ ] Monitor performance metrics
2750
+ - [ ] Run \`speccore archive --task=${name}\`
2751
+
2752
+ ## Rollback Plan
2753
+
2754
+ - [ ] \`git revert\` the merge commit
2755
+ ${hasDb ? '- [ ] Run rollback DDL from SCHEMA.md\n' : ''}- [ ] Notify team on rollback
2756
+ `;
2757
+ }
2758
+ async function generateEnvExample(iterationDir, sections) {
2759
+ const envPath = (0, path_1.join)(iterationDir, '.env.example');
2760
+ let env = '# Environment Variables — ' + iterationDir + '\n';
2761
+ env += '# Copy to .env and fill in values\n\n';
2762
+ const needs = new Set();
2763
+ for (const s of sections) {
2764
+ const c = s.content + s.name;
2765
+ if (c.match(/Redis|缓存/))
2766
+ needs.add('REDIS_URL=redis://localhost:6379');
2767
+ if (c.match(/Kafka|MQ|消息队列/))
2768
+ needs.add('KAFKA_BROKERS=localhost:9092');
2769
+ if (c.match(/MySQL|数据库|JDBC|数据表/))
2770
+ needs.add('DB_URL=jdbc:mysql://localhost:3306/db\nDB_USER=root\nDB_PASS=');
2771
+ if (c.match(/OSS|对象存储|S3|文件上传/))
2772
+ needs.add('OSS_ENDPOINT=https://oss.example.com\nOSS_KEY=\nOSS_SECRET=');
2773
+ if (c.match(/支付|微信|支付宝|wechat|alipay/))
2774
+ needs.add('PAYMENT_API_KEY=\nPAYMENT_SECRET=');
2775
+ if (c.match(/短信|SMS|验证码/))
2776
+ needs.add('SMS_API_KEY=\nSMS_SECRET=');
2777
+ if (c.match(/邮件|email|smtp/))
2778
+ needs.add('SMTP_HOST=smtp.example.com\nSMTP_PORT=587\nSMTP_USER=\nSMTP_PASS=');
2779
+ if (c.match(/token|JWT|OAuth|鉴权|登录/))
2780
+ needs.add('JWT_SECRET=\nTOKEN_EXPIRE=3600');
2781
+ }
2782
+ if (needs.size === 0) {
2783
+ needs.add('# No extra environment variables detected.');
2784
+ needs.add('# Add required variables here.');
2785
+ }
2786
+ env += [...needs].join('\n') + '\n';
2787
+ await (0, fs_extra_1.writeFile)(envPath, env);
2788
+ logger_1.logger.info(`Env example: ${iterationDir}/.env.example`);
2789
+ }
2790
+ async function injectTechFromAnalysis(iterationDir, taskDir, sectionName) {
2791
+ const analysisPathNew = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'ANALYSIS.md');
2792
+ const analysisPathOld = (0, path_1.join)(iterationDir, '020-specs', 'ANALYSIS.md');
2793
+ const analysisPath = await (0, fs_extra_1.pathExists)(analysisPathNew) ? analysisPathNew : analysisPathOld;
2794
+ if (!(await (0, fs_extra_1.pathExists)(analysisPath)))
2795
+ return;
2796
+ const analysis = await (0, fs_extra_1.readFile)(analysisPath, 'utf-8');
2797
+ // Extract relevant tech stack section
2798
+ const techSection = analysis.match(/### 技术选型[\s\S]*?(?=###|$)/);
2799
+ const dbSection = analysis.match(/### 数据库变更[\s\S]*?(?=###|$)/);
2800
+ const depSection = analysis.match(/### 接口依赖[\s\S]*?(?=###|$)/);
2801
+ if (!techSection && !dbSection && !depSection)
2802
+ return;
2803
+ const techPath = (await (0, fs_extra_1.pathExists)((0, path_1.join)(taskDir, '00-specs', 'TECH.md')))
2804
+ ? (0, path_1.join)(taskDir, '00-specs', 'TECH.md')
2805
+ : (0, path_1.join)(taskDir, '_shared', 'TECH.md');
2806
+ let tech = await (0, fs_extra_1.readFile)(techPath, 'utf-8');
2807
+ const note = '\n\n> 以下内容自动从 ANALYSIS.md 注入\n\n';
2808
+ if (techSection && !tech.includes(techSection[0].trim())) {
2809
+ tech += note + techSection[0].trim() + '\n';
2810
+ }
2811
+ if (dbSection && !tech.includes(dbSection[0].trim())) {
2812
+ tech += dbSection[0].trim() + '\n';
2813
+ }
2814
+ if (depSection && !tech.includes(depSection[0].trim())) {
2815
+ tech += depSection[0].trim() + '\n';
2816
+ }
2817
+ await (0, fs_extra_1.writeFile)(techPath, tech);
2818
+ }
2819
+ function generateApiContract(section) {
2820
+ const lines = (section.content || '').split(/\r?\n/);
2821
+ const apis = [];
2822
+ for (const line of lines) {
2823
+ const match = line.match(/\|\s*(GET|POST|PUT|DELETE|PATCH)\s*\|\s*(\/[^\s|]+)\s*\|\s*(.*)/i);
2824
+ if (match) {
2825
+ apis.push({ method: match[1].toUpperCase(), path: match[2].trim(), desc: (match[3] || '').trim() });
2826
+ }
2827
+ }
2828
+ if (apis.length === 0)
2829
+ return '';
2830
+ // 从 section 内容提取每个 API 的请求/响应字段
2831
+ const fullContent = section.content || '';
2832
+ const apiFieldMap = {};
2833
+ for (const api of apis) {
2834
+ const apiIdx = fullContent.indexOf(api.path);
2835
+ if (apiIdx >= 0) {
2836
+ const after = fullContent.slice(apiIdx, apiIdx + 800);
2837
+ const fields = [];
2838
+ const fieldRegex = /[`']([a-zA-Z]\w{1,25})[`']\s*[((]?[::]?\s*(?:类型[::])?\s*([\u4e00-\u9fa5A-Za-z]+)/g;
2839
+ let fm;
2840
+ while ((fm = fieldRegex.exec(after)) !== null) {
2841
+ if (!['api', 'data', 'code', 'message', 'msg', 'status', 'ok', 'err'].includes(fm[1].toLowerCase())) {
2842
+ fields.push(`${fm[1]}: ${fm[2]}`);
2843
+ }
2844
+ }
2845
+ if (fields.length > 0)
2846
+ apiFieldMap[api.path] = fields.slice(0, 10);
2847
+ }
2848
+ }
2849
+ let yaml = `# ${section.name} — API Contract
2850
+ # Auto-generated from REQ.md
2851
+
2852
+ openapi: "3.0.0"
2853
+ info:
2854
+ title: "${section.name}"
2855
+ version: "1.0.0"
2856
+
2857
+ paths:
2858
+ `;
2859
+ for (const api of apis) {
2860
+ const tag = api.path.split('/')[2] || 'default';
2861
+ yaml += ` ${api.path}:
2862
+ ${api.method.toLowerCase()}:
2863
+ tags: [${tag}]
2864
+ summary: "${api.desc}"
2865
+ `;
2866
+ // 添加字段定义
2867
+ if (apiFieldMap[api.path]) {
2868
+ yaml += ` fields:
2869
+ `;
2870
+ for (const f of apiFieldMap[api.path]) {
2871
+ yaml += ` - "${f}"
2872
+ `;
2873
+ }
2874
+ }
2875
+ yaml += ` responses:
2876
+ "200":
2877
+ description: Success
2878
+ `;
2879
+ if (api.method === 'POST' || api.method === 'PUT') {
2880
+ yaml += ` "400":
2881
+ description: Bad Request
2882
+ `;
2883
+ }
2884
+ if (api.method === 'DELETE') {
2885
+ yaml += ` "404":
2886
+ description: Not Found
2887
+ `;
2888
+ }
2889
+ }
2890
+ return yaml;
2891
+ }
2892
+ function generateErrorCodes(section) {
2893
+ let md = `# ${section.name} — Error Codes\n\n> Auto-generated\n\n`;
2894
+ md += `| Code | HTTP | Message | Description |\n`;
2895
+ md += `| :--- | :--- | :--- | :--- |\n`;
2896
+ const content = section.content || '';
2897
+ const module = section.name.replace(/[^\w]/g, '_').toUpperCase();
2898
+ md += `| ${module}_001 | 400 | 参数校验失败 | 请求参数不符合规范 |\n`;
2899
+ md += `| ${module}_002 | 404 | 资源不存在 | 请求的资源未找到 |\n`;
2900
+ md += `| ${module}_003 | 500 | 服务器内部错误 | 未预期的服务异常 |\n`;
2901
+ if (content.includes('权限') || content.includes('RBAC')) {
2902
+ md += `| ${module}_004 | 403 | 无操作权限 | 当前用户权限不足 |\n`;
2903
+ }
2904
+ if (content.includes('创建') || content.includes('POST')) {
2905
+ md += `| ${module}_005 | 409 | 资源冲突 | 重复创建或状态冲突 |\n`;
2906
+ }
2907
+ return md;
2908
+ }
2909
+ function generateAdr(section) {
2910
+ const content = section.content || '';
2911
+ // Only generate ADR if tech decisions are mentioned
2912
+ const hasTech = content.match(/Spring|Vue|React|MySQL|Redis|Kafka|微服务|单体|REST|gRPC/);
2913
+ if (!hasTech)
2914
+ return '';
2915
+ const now = new Date().toISOString().split('T')[0];
2916
+ let adr = `# ADR: ${section.name}\n\n`;
2917
+ adr += `- **日期**: ${now}\n`;
2918
+ adr += `- **状态**: 提议中\n\n`;
2919
+ adr += `## 决策\n\n`;
2920
+ const techStack = content.match(/(Spring|Vue|React|MySQL|Redis|Kafka|微服务|单体|REST|gRPC)[^\n]*/g);
2921
+ if (techStack) {
2922
+ adr += `基于任务需求,技术选型如下:\n\n`;
2923
+ for (const t of [...new Set(techStack)]) {
2924
+ adr += `- ${t.trim()}\n`;
2925
+ }
2926
+ }
2927
+ adr += `\n## 备选方案\n\n- _待补充_\n`;
2928
+ adr += `\n## 后果\n\n- _待补充_\n`;
2929
+ return adr;
2930
+ }
2931
+ // ── AC 自动生成 ──
2932
+ function generateAcceptanceCriteria(section) {
2933
+ const lines = section.content.split(/\r?\n/);
2934
+ let acs = '';
2935
+ let acNum = 1;
2936
+ for (const line of lines) {
2937
+ const trimmed = line.trim();
2938
+ if (trimmed.startsWith('|') && (trimmed.includes('GET') || trimmed.includes('POST') || trimmed.includes('PUT') || trimmed.includes('DELETE') || trimmed.includes('PATCH'))) {
2939
+ const parts = trimmed.split('|').map(p => p.trim()).filter(Boolean);
2940
+ const method = parts[0] || '';
2941
+ const path = parts[1] || '';
2942
+ const desc = parts[2] || path;
2943
+ acs += `- [ ] AC-${acNum++}: \`${method} ${path}\` — ${desc}\n`;
2944
+ }
2945
+ if (trimmed.startsWith('-') && (trimmed.includes('规则') || trimmed.includes('校验') || trimmed.includes('必须'))) {
2946
+ acs += `- [ ] AC-${acNum++}: ${trimmed.replace(/^- /, '')}\n`;
2947
+ }
2948
+ }
2949
+ if (acNum === 1) {
2950
+ acs = `- [ ] AC-1: 功能实现与需求描述一致\n- [ ] AC-2: 异常输入有合理的错误处理\n- [ ] AC-3: 核心逻辑有单元测试覆盖\n`;
2951
+ acNum = 4;
2952
+ }
2953
+ acs += `- [ ] AC-${acNum++}: 代码审查通过(REVIEW.md 全部已确认)\n`;
2954
+ acs += `- [ ] AC-${acNum++}: 部署清单完成(DEPLOY.md 全部已确认)\n`;
2955
+ return acs;
2956
+ }
2957
+ // 风险评估
2958
+ function generateRiskTemplate(section, material) {
2959
+ if (material && material.trim().length > 200) {
2960
+ return `# ${section.name} — 风险评估\n\n> 来源: analyze → RISK.md(自动提取)\n\n${material.trim()}\n`;
2961
+ }
2962
+ return `# ${section.name} — 风险评估\n\n> split | ${new Date().toISOString().split('T')[0]}\n\n## 风险矩阵\n\n| 风险 | 可能 | 影响 | 缓解 |\n| :--- | :--- | :--- | :--- |\n| 兼容性 | 中 | 高 | 版本号+测试 |\n| 性能 | 低 | 中 | 压测+索引 |\n| 依赖故障 | 低 | 高 | 降级方案 |\n\n## 回滚\n\n1. 触发: 线上错误率 > 1%\n2. 步骤: git revert → 重部署\n3. 验证: 冒烟测试 + 监控\n`;
2963
+ }
2964
+ // 依赖清单
2965
+ function generateDepsTemplate(section, material) {
2966
+ if (material && material.trim().length > 200) {
2967
+ return `# ${section.name} — 依赖清单\n\n> 来源: analyze → DEPS.md(自动提取)\n\n${material.trim()}\n`;
2968
+ }
2969
+ return `# ${section.name} — 依赖清单\n\n## 上游依赖\n\n| 服务 | 版本 | 用途 | SLA |\n| :--- | :--- | :--- | :--- |\n| _待补充_ | — | — | — |\n\n## 下游影响\n\n| 服务 | 影响 | 通知 |\n| :--- | :--- | :--- |\n| _待补充_ | — | — |\n`;
2970
+ }
2971
+ // 监控指标
2972
+ function generateMonitorTemplate(section, material) {
2973
+ if (material && material.trim().length > 200) {
2974
+ return `# ${section.name} — 监控\n\n> 来源: analyze → MONITOR.md(自动提取)\n\n${material.trim()}\n`;
2975
+ }
2976
+ return `# ${section.name} — 监控\n\n## 关键指标\n\n| 指标 | 阈值 | 级别 |\n| :--- | :--- | :--- |\n| 成功率 | <99.9% | P1 |\n| P99延迟 | >1000ms | P2 |\n| 错误率 | >0.1% | P0 |\n\n## 关键日志\n\n- 请求入口 (traceId)\n- 业务异常 (上下文)\n- 外部调用 (耗时)\n`;
2977
+ }
2978
+ // ═══════════════════════════════════════════════
2979
+ // 从 analyze 产出中提取任务相关内容
2980
+ // ═══════════════════════════════════════════════
2981
+ /** 加载迭代级 020-specs/ 文档内容(包括 global/ 全局文档 + 根目录回退 + 各端子目录文档) */
2982
+ async function loadSpecContents(iterationDir) {
2983
+ const specs = {};
2984
+ const specDir = (0, path_1.join)(iterationDir, '020-specs');
2985
+ if (!(await (0, fs_extra_1.pathExists)(specDir)))
2986
+ return specs;
2987
+ // v8.3.21+: 读取全局文档(overview/ 下)
2988
+ const overviewDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
2989
+ if (await (0, fs_extra_1.pathExists)(overviewDir)) {
2990
+ const overviewFiles = await (0, fs_extra_1.readdir)(overviewDir, { withFileTypes: true });
2991
+ for (const f of overviewFiles) {
2992
+ if (!f.name.endsWith('.md'))
2993
+ continue;
2994
+ const fp = (0, path_1.join)(overviewDir, f.name);
2995
+ const content = await (0, fs_extra_1.readFile)(fp, 'utf-8');
2996
+ if (content.trim().length > 50 && !content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL/m)) {
2997
+ specs[f.name] = content;
2998
+ }
2999
+ }
3000
+ }
3001
+ // v8.3.21+: 读取各功能模块下的文档
3002
+ const features = await (0, spec_paths_1.parseFeatureList)(iterationDir);
3003
+ for (const feature of features) {
3004
+ const featureDir = (0, path_1.join)(specDir, feature);
3005
+ if (!await (0, fs_extra_1.pathExists)(featureDir))
3006
+ continue;
3007
+ const entries = await (0, fs_extra_1.readdir)(featureDir, { withFileTypes: true });
3008
+ for (const entry of entries) {
3009
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
3010
+ continue;
3011
+ const subDir = (0, path_1.join)(featureDir, entry.name);
3012
+ const subFiles = await (0, fs_extra_1.readdir)(subDir, { withFileTypes: true });
3013
+ for (const sf of subFiles) {
3014
+ if (!sf.name.endsWith('.md'))
3015
+ continue;
3016
+ const fp = (0, path_1.join)(subDir, sf.name);
3017
+ const content = await (0, fs_extra_1.readFile)(fp, 'utf-8');
3018
+ if (content.trim().length > 50 && !content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL/m)) {
3019
+ specs[`${feature}/${entry.name}/${sf.name}`] = content;
3020
+ }
3021
+ }
3022
+ }
3023
+ }
3024
+ return specs;
3025
+ }
3026
+ // ═══════════════════════════════════════════════
3027
+ // v8.3.125+: 三层匹配 — 精确 + 关键词交集 + 文档标题
3028
+ // ═══════════════════════════════════════════════
3029
+ // v8.3.125+: extractNormalizedKeywords 和 expandSynonyms 已提取到 ../../utils/synonyms
3030
+ /** 第二层:关键词集合交集得分 */
3031
+ function scoreKeywordOverlap(taskName, docPath) {
3032
+ const taskKws = (0, synonyms_1.expandSynonyms)((0, synonyms_1.extractNormalizedKeywords)(taskName));
3033
+ const pathKws = (0, synonyms_1.expandSynonyms)((0, synonyms_1.extractNormalizedKeywords)(docPath));
3034
+ let score = 0;
3035
+ for (const tkw of taskKws) {
3036
+ for (const pkw of pathKws) {
3037
+ if (tkw === pkw || tkw.includes(pkw) || pkw.includes(tkw)) {
3038
+ score += 10;
3039
+ break;
3040
+ }
3041
+ }
3042
+ }
3043
+ return score;
3044
+ }
3045
+ /** 第三层:文档标题匹配得分 */
3046
+ function scoreHeadingMatch(taskName, docContent) {
3047
+ const taskKws = (0, synonyms_1.expandSynonyms)((0, synonyms_1.extractNormalizedKeywords)(taskName));
3048
+ const headingRegex = /^(#{1,3})\s+(.+)$/gm;
3049
+ let score = 0;
3050
+ let match;
3051
+ while ((match = headingRegex.exec(docContent)) !== null) {
3052
+ const headingKws = (0, synonyms_1.expandSynonyms)((0, synonyms_1.extractNormalizedKeywords)(match[2]));
3053
+ for (const tkw of taskKws) {
3054
+ for (const hkw of headingKws) {
3055
+ if (tkw === hkw || tkw.includes(hkw) || hkw.includes(tkw)) {
3056
+ score += 8;
3057
+ break;
3058
+ }
3059
+ }
3060
+ }
3061
+ }
3062
+ return score;
3063
+ }
3064
+ /** 三层匹配:找到与任务最匹配的文档 key
3065
+ * @returns 最佳匹配的 {key, score},无匹配返回 null
3066
+ */
3067
+ function findBestSpecMatch(specContents, taskName, platform, docType) {
3068
+ const entries = Object.entries(specContents);
3069
+ if (entries.length === 0 || !taskName)
3070
+ return null;
3071
+ const candidates = [];
3072
+ for (const [key, content] of entries) {
3073
+ // 如果指定了 docType,过滤不匹配的文档类型
3074
+ if (docType && !key.endsWith(docType))
3075
+ continue;
3076
+ let score = 0;
3077
+ // 第一层:精确匹配(字符串包含,最高权重)
3078
+ const normalizedTask = taskName.toLowerCase().replace(/\s+/g, '');
3079
+ const normalizedKey = key.toLowerCase().replace(/[\/\-_\.]/g, '');
3080
+ if (normalizedKey.includes(normalizedTask) || normalizedTask.includes(normalizedKey)) {
3081
+ score += 100;
3082
+ }
3083
+ // 第二层:关键词集合交集
3084
+ score += scoreKeywordOverlap(taskName, key);
3085
+ // 第三层:文档标题匹配
3086
+ score += scoreHeadingMatch(taskName, content);
3087
+ // 平台匹配加分(如果指定了 platform)
3088
+ if (platform) {
3089
+ const pLower = platform.toLowerCase();
3090
+ if (key.toLowerCase().includes(`/${pLower}/`) || key.toLowerCase().startsWith(`${pLower}/`)) {
3091
+ score += 20;
3092
+ }
3093
+ }
3094
+ if (score > 0)
3095
+ candidates.push({ key, score });
3096
+ }
3097
+ if (candidates.length === 0)
3098
+ return null;
3099
+ candidates.sort((a, b) => b.score - a.score);
3100
+ return candidates[0];
3101
+ }
3102
+ /** 从完整文档中提取与任务名相关的段落 */
3103
+ function extractRelevantSection(fullContent, taskName, sectionHint) {
3104
+ if (!fullContent || !taskName)
3105
+ return '';
3106
+ const lines = fullContent.split(/\r?\n/);
3107
+ const nameKeywords = new Set();
3108
+ for (const m of taskName.matchAll(/[a-zA-Z]+/g)) {
3109
+ if (m[0].length > 1)
3110
+ nameKeywords.add(m[0].toLowerCase());
3111
+ }
3112
+ for (const m of taskName.matchAll(/[\u4e00-\u9fff]+/g)) {
3113
+ if (m[0].length >= 2)
3114
+ nameKeywords.add(m[0]);
3115
+ }
3116
+ if (sectionHint) {
3117
+ for (const m of sectionHint.matchAll(/[a-zA-Z]+/g)) {
3118
+ if (m[0].length > 1)
3119
+ nameKeywords.add(m[0].toLowerCase());
3120
+ }
3121
+ for (const m of sectionHint.matchAll(/[\u4e00-\u9fff]+/g)) {
3122
+ if (m[0].length >= 2)
3123
+ nameKeywords.add(m[0]);
3124
+ }
3125
+ }
3126
+ if (nameKeywords.size === 0)
3127
+ return '';
3128
+ const kwArray = [...nameKeywords];
3129
+ // 按 Markdown 标题拆分,查找包含关键词的完整段落
3130
+ const sections = [];
3131
+ let cur = { heading: '', level: 0, content: [], headingLine: '' };
3132
+ for (const line of lines) {
3133
+ const hm = line.match(/^(#{1,4})\s+(.+)/);
3134
+ if (hm) {
3135
+ if (cur.heading || cur.content.length > 0)
3136
+ sections.push({ heading: cur.heading, level: cur.level, content: cur.content.join('\n'), headingLine: cur.headingLine });
3137
+ cur = { heading: hm[2].trim(), level: hm[1].length, content: [], headingLine: line };
3138
+ }
3139
+ else {
3140
+ cur.content.push(line);
3141
+ }
3142
+ }
3143
+ if (cur.heading || cur.content.length > 0)
3144
+ sections.push({ heading: cur.heading, level: cur.level, content: cur.content.join('\n'), headingLine: cur.headingLine });
3145
+ // 匹配:标题或内容包含关键词
3146
+ const matched = [];
3147
+ for (const sec of sections) {
3148
+ const text = (sec.heading + ' ' + sec.content).toLowerCase();
3149
+ const hit = kwArray.some(kw => text.includes(kw));
3150
+ if (hit && (sec.heading || sec.content.trim())) {
3151
+ const prefix = '#'.repeat(Math.max(sec.level + 1, 2));
3152
+ matched.push(`${prefix} ${sec.heading}\n${sec.content.trim()}`);
3153
+ }
3154
+ }
3155
+ if (matched.length > 0)
3156
+ return matched.join('\n\n');
3157
+ // 回退:提取包含关键词的连续行
3158
+ const fallback = [];
3159
+ for (const line of lines) {
3160
+ const lower = line.toLowerCase();
3161
+ if (kwArray.some(kw => lower.includes(kw)))
3162
+ fallback.push(line);
3163
+ }
3164
+ return fallback.length > 0 ? fallback.join('\n') : '';
3165
+ }
3166
+ /** 从 TECH.md 提取前端平台相关内容 */
3167
+ function extractFrontendContent(techContent, taskName, platform) {
3168
+ if (!techContent)
3169
+ return '';
3170
+ // 先提取该前端平台相关的顶层段落(如 "H5移动端"、"后台管理端")
3171
+ const platformLabels = {
3172
+ 'h5': ['H5', 'h5', '移动端', 'mobile'],
3173
+ 'admin': ['admin', '后台', '管理端', 'Admin'],
3174
+ 'web': ['Web', 'web', '前端', '桌面'],
3175
+ 'app': ['App', 'app', '客户端'],
3176
+ 'miniapp': ['小程序', 'miniapp'],
3177
+ };
3178
+ const platformKws = platformLabels[platform] || [platform];
3179
+ const platformSection = extractRelevantSection(techContent, taskName, platformKws.join(' '));
3180
+ if (platformSection)
3181
+ return platformSection;
3182
+ // 回退:用任务名匹配
3183
+ return extractRelevantSection(techContent, taskName);
3184
+ }
3185
+ /** 从 specContents 提取任务级 TECH 内容(优先读取对应端的文档)
3186
+ {feature}/{platform}/TECH.md key 为 {feature}/{platform}/TECH.md
3187
+ * v8.3.125+: 使用三层匹配(精确+关键词+标题)替代字符串查找
3188
+ */
3189
+ function extractTaskTechContent(specContents, section, platform) {
3190
+ // v8.3.125+: 使用三层匹配找到最佳 TECH.md
3191
+ const bestMatch = findBestSpecMatch(specContents, section.name, platform, 'TECH.md');
3192
+ if (bestMatch && bestMatch.score >= 20) {
3193
+ return extractRelevantSection(specContents[bestMatch.key], section.name);
3194
+ }
3195
+ // 回退:尝试从全局 TECH.md 提取(overview/TECH.md 或根目录 TECH.md)
3196
+ const techMd = specContents['TECH.md'] || specContents['overview/TECH.md'];
3197
+ if (!techMd)
3198
+ return '';
3199
+ if (platform && platform !== 'backend') {
3200
+ return extractFrontendContent(techMd, section.name, platform);
3201
+ }
3202
+ // 后端/共享:提取技术方案、架构、接口设计、数据模型等
3203
+ const techSection = extractRelevantSection(techMd, section.name, '技术方案 架构 接口设计 数据模型 模块设计');
3204
+ return techSection;
3205
+ }
3206
+ /** v8.3.0+: 从 analyze DEV_GUIDE.md 提取任务级开发指南内容
3207
+ {feature}/{platform}/TECH.md key 为 {feature}/{platform}/DEV_GUIDE.md
3208
+ * v8.3.125+: 使用三层匹配替代字符串查找
3209
+ */
3210
+ function extractTaskDevGuideContent(specContents, section, taskPlatforms) {
3211
+ const results = [];
3212
+ // v8.3.125+: 对每个端使用三层匹配找最佳 DEV_GUIDE.md
3213
+ for (const platform of taskPlatforms) {
3214
+ const bestMatch = findBestSpecMatch(specContents, section.name, platform, 'DEV_GUIDE.md');
3215
+ if (bestMatch && bestMatch.score >= 20) {
3216
+ const extracted = extractRelevantSection(specContents[bestMatch.key], section.name);
3217
+ if (extracted && extracted.trim().length > 50) {
3218
+ results.push(`## ${platform} 端开发指南\n\n${extracted.trim()}`);
3219
+ }
3220
+ }
3221
+ }
3222
+ // v8.3.171+: 从全局 DEV_GUIDE.md 提取(唯一路径 overview/DEV_GUIDE.md)
3223
+ if (results.length === 0) {
3224
+ const globalDevGuide = specContents['overview/DEV_GUIDE.md'];
3225
+ if (globalDevGuide) {
3226
+ const extracted = extractRelevantSection(globalDevGuide, section.name, '改造范围 实施步骤 接口契约 验证方式 回滚 坑点');
3227
+ if (extracted && extracted.trim().length > 50) {
3228
+ results.push(`## 全局开发指南(本任务相关)\n\n${extracted.trim()}`);
3229
+ }
3230
+ }
3231
+ }
3232
+ return results.join('\n\n');
3233
+ }
3234
+ // 前端专属:组件树
3235
+ function generateComponentTree(section, platform, material) {
3236
+ if (material && material.trim().length > 200) {
3237
+ return `# ${section.name} — 组件树 (${platform})\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
3238
+ }
3239
+ return `# ${section.name} — 组件树 (${platform})
3240
+
3241
+ > split | ${new Date().toISOString().split('T')[0]}
3242
+
3243
+ ## 页面结构
3244
+ <!-- 从 analyze TECH.md 自动提取,若无内容则待补充 -->
3245
+
3246
+ ## 组件清单
3247
+ | 组件 | 路径 | 类型 | 状态 |
3248
+ | :--- | :--- | :--- | :--- |
3249
+ | _待补充_ | — | — | — |
3250
+
3251
+ ## 共享组件
3252
+ | 组件 | 来源 | 用途 |
3253
+ | :--- | :--- | :--- |
3254
+ | _待补充_ | — | — |
3255
+ `;
3256
+ }
3257
+ // 前端专属:路由
3258
+ function generateRoutesDoc(section, platform, material) {
3259
+ if (material && material.trim().length > 200) {
3260
+ return `# ${section.name} — 路由设计 (${platform})\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
3261
+ }
3262
+ return `# ${section.name} — 路由设计 (${platform})
3263
+
3264
+ > split | ${new Date().toISOString().split('T')[0]}
3265
+
3266
+ ## 路由表
3267
+ | 路径 | 组件 | 权限 | 参数 |
3268
+ | :--- | :--- | :--- | :--- |
3269
+ | _待AI分析_ | — | — | — |
3270
+
3271
+ ## 导航结构
3272
+ <!-- AI-FILL: 面包屑 / 侧栏 / Tab -->
3273
+ `;
3274
+ }
3275
+ // 前端专属:状态管理
3276
+ function generateStateDoc(section, platform, material) {
3277
+ if (material && material.trim().length > 200) {
3278
+ return `# ${section.name} — 状态管理 (${platform})\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
3279
+ }
3280
+ return `# ${section.name} — 状态管理 (${platform})
3281
+
3282
+ > split | ${new Date().toISOString().split('T')[0]}
3283
+
3284
+ ## 全局状态
3285
+ | Store | 字段 | 类型 | 持久化 |
3286
+ | :--- | :--- | :--- | :--- |
3287
+ | _待AI分析_ | — | — | — |
3288
+
3289
+ ## 组件状态
3290
+ | 组件 | 状态 | 来源 |
3291
+ | :--- | :--- | :--- |
3292
+ | _待补充_ | — | — |
3293
+
3294
+ ## 数据流
3295
+ <!-- AI-FILL: 父→子 props / 子→父 emit / store -->
3296
+ `;
3297
+ }
3298
+ // 前端专属:样式规范
3299
+ function generateStyleGuide(section, platform, material) {
3300
+ if (material && material.trim().length > 200) {
3301
+ return `# ${section.name} — 样式规范 (${platform})\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
3302
+ }
3303
+ const isH5 = platform.includes('h5') || platform.includes('mobile');
3304
+ const isMiniapp = platform.includes('miniapp') || platform.includes('小程序');
3305
+ const styleCtx = isH5 ? '移动端 H5,注意触控交互和移动适配' :
3306
+ isMiniapp ? '小程序,遵守平台组件规范' : '桌面 Web';
3307
+ return `# ${section.name} — 样式规范 (${platform})
3308
+
3309
+ > ${styleCtx}
3310
+
3311
+ ## 设计 Token
3312
+ | Token | 值 | 用途 |
3313
+ | :--- | :--- | :--- |
3314
+ | --color-primary | #1677FF | 主色 |
3315
+ | --spacing-unit | 8px | 间距单位 |
3316
+ | --radius | ${isH5 ? '12' : '8'}px | 圆角 |
3317
+
3318
+ ## 响应式断点
3319
+ ${isH5 ? '移动端优先,适配 375/414/768' :
3320
+ isMiniapp ? '小程序 rpx 自适应' : '>=1920 / 1440 / 1024 / 768'}
3321
+
3322
+ ## 动画
3323
+ - 页面切换: 300ms ease-in-out
3324
+ - 加载态: 骨架屏优先
3325
+ `;
3326
+ }
3327
+ /**
3328
+ * 构建完整的 AI 智能拆分 Prompt(含 SpecCore 理念 + 粒度规则 + 完整上下文)
3329
+ */
3330
+ async function buildSplitPrompt(iteration, constitutionContent, reqContent, specContents, standardPlatforms, modulesFilter, platformsFilter, existingTasks, iterationDir,
3331
+ // v8.3.160+: 子 Agent 角色和上下文类型,用于动态调整 Prompt
3332
+ subagent, contextType) {
3333
+ // v8.3.160+: 接入 agent-adapter,注入子 Agent 角色定义和上下文裁剪策略
3334
+ const agentCtx = {
3335
+ subagent: subagent || 'task-decomposer',
3336
+ iteration,
3337
+ contextBudget: contextType === 'incremental' ? 5000 : 8000,
3338
+ contextType: contextType || 'full',
3339
+ cwd: iterationDir || process.cwd(),
3340
+ };
3341
+ const agentContextText = await agent_adapter_1.defaultAdapter.prepareContext(agentCtx);
3342
+ let p = `# SpecCore AI 智能拆分\n\n`;
3343
+ p += `> 迭代: ${iteration} | 子 Agent: ${agentCtx.subagent} | 生成: ${new Date().toISOString().split('T')[0]}\n\n`;
3344
+ // 注入子 Agent 上下文(角色定义 + 上下文裁剪策略)
3345
+ p += agentContextText + '\n\n';
3346
+ // v6.76.0+: 拆分范围限制
3347
+ if (modulesFilter?.length || platformsFilter?.length) {
3348
+ p += `## 🎯 拆分范围限制(用户指定)\n\n`;
3349
+ if (modulesFilter?.length) {
3350
+ p += `- **只拆分以下功能模块**: ${modulesFilter.join('、')}\n`;
3351
+ p += `- 如果需求文档中有其他功能模块,**完全忽略**,不要拆分\n`;
3352
+ }
3353
+ if (platformsFilter?.length) {
3354
+ p += `- **只拆分涉及以下端的功能模块**: ${platformsFilter.join('、')}\n`;
3355
+ p += `- 如果某个功能模块不涉及这些端,**跳过不拆**\n`;
3356
+ p += `- 每个任务的 scope 只能包含这些端,不要引入其他端\n`;
3357
+ }
3358
+ p += `\n`;
3359
+ }
3360
+ // v6.76.0+: 增量拆分 — 已有 Task 结构
3361
+ if (existingTasks && existingTasks.size > 0) {
3362
+ p += `## 📂 已有 Task 结构(增量拆分模式)\n\n`;
3363
+ p += `本次拆分为**增量模式**:以下 Task 已存在,如果新拆分的功能单元与已有 Task 匹配,**复用该 Task ID,只追加新端**。\n\n`;
3364
+ for (const [taskName, platforms] of existingTasks.entries()) {
3365
+ p += `- **${taskName}**: 已有端 [${platforms.join(', ')}]\n`;
3366
+ }
3367
+ p += `\n`;
3368
+ p += `### 增量拆分规则\n`;
3369
+ p += `- 如果功能单元与已有 Task 的 functionalUnit 相同 → **复用该 Task ID**,scope 追加新端\n`;
3370
+ p += `- 如果功能单元是全新的 → 生成新的 Task ID(延续现有编号)\n`;
3371
+ p += `- 已有端不要重复拆分,只拆分**新增的端**\n`;
3372
+ p += `- 已有 Task 的 _shared/ 目录保持不变,新端目录平铺追加到任务根目录下(如 \`{端名}/{taskId}-{端名}/\`)\n`;
3373
+ p += `- API 契约(_shared/API_CONTRACT.yaml)需要补充新端涉及的接口\n\n`;
3374
+ }
3375
+ // v8.3.160+: 路径引用模式 —— 不再把文档全文塞进 Prompt,让 AI 通过 Read 工具按需读取
3376
+ p += `## 📂 文档清单(请按需 Read)\n\n`;
3377
+ p += `> 💡 **路径引用模式**:以下文档已就绪,请根据当前任务需要选择性 Read,不要一次性加载所有文件。\n\n`;
3378
+ p += `### 必读文档\n\n`;
3379
+ p += `1. \`CONSTITUTION.md\` — 技术宪法(端列表、命名规范、技术栈)\n`;
3380
+ p += `2. \`010-requirements/INDEX.md\` — 需求全貌索引\n`;
3381
+ p += `3. \`010-requirements/converted/*.md\` — 转换后的需求文档\n`;
3382
+ p += `4. \`020-specs/overview/REQUIREMENT.md\` — 黄金需求文档\n`;
3383
+ p += `5. \`020-specs/overview/TECH.md\` — 全局技术方案\n`;
3384
+ p += `6. \`020-specs/overview/ANALYSIS.md\` — 功能分析\n`;
3385
+ p += `7. \`020-specs/overview/FUNCTION_MAP.md\` — 功能单元与端的映射(参考,权威来源为 010-requirements/features/ 目录)\n`;
3386
+ if (await (0, fs_extra_1.pathExists)((0, path_1.join)(iterationDir || '', '020-specs', 'overview', 'INTERACTION_MAP.md'))) {
3387
+ p += `8. \`020-specs/overview/INTERACTION_MAP.md\` — 跨端交互时序\n`;
3388
+ }
3389
+ if (await (0, fs_extra_1.pathExists)((0, path_1.join)(iterationDir || '', '020-specs', 'overview', 'DEV_GUIDE.md'))) {
3390
+ p += `9. \`020-specs/overview/DEV_GUIDE.md\` — 开发指南\n`;
3391
+ }
3392
+ p += `\n`;
3393
+ p += `### 按功能模块读取(020-specs/{功能模块}/)\n\n`;
3394
+ p += `对于每个功能模块,按需 Read:\n`;
3395
+ for (const spec of specContents) {
3396
+ p += `- \`020-specs/${spec.name}\`\n`;
3397
+ }
3398
+ p += `\n`;
3399
+ p += `> 📖 **读取策略**:\n`;
3400
+ p += `> - 先 Read 010-requirements/features/ 目录了解功能单元划分(权威来源),FUNCTION_MAP.md 作功能×端映射参考\n`;
3401
+ p += `> - 按功能单元逐个处理,每次只 Read 当前单元相关的文档\n`;
3402
+ p += `> - reqContent 从 REQUIREMENT.md 和对应功能模块的需求文档提取\n`;
3403
+ p += `> - techContent 从 TECH.md 和对应功能模块的技术规格提取\n`;
3404
+ p += `> - devGuideContent 从 DEV_GUIDE.md 提取\n\n`;
3405
+ p += `---\n\n`;
3406
+ // v6.69.3+: 注入标准端名列表
3407
+ if (standardPlatforms.length > 0) {
3408
+ p += `## 🖥️ 项目端列表(标准端名 — 权威来源)\n\n`;
3409
+ p += `本项目已配置以下端,所有 scope 必须使用这些**标准端名**,禁止使用简写或中文:\n\n`;
3410
+ p += `\`${standardPlatforms.join('`, `')}\`\n\n`;
3411
+ p += `- ⛔ **禁止使用以下非标准端名**: \`api\`、\`web\`、\`backend\`、\`frontend\`、\`admin\`、\`h5\`、\"后端\"、\"前端\"\n`;
3412
+ p += `- ✅ **必须使用 CONSTITUTION.md 中的标准端名**作为子任务目录名\n`;
3413
+ p += `- scope 数组和子任务目录名必须完全匹配上述标准端名\n\n`;
3414
+ }
3415
+ // v7.4.2+: 功能单元精准上下文(CLI 提取,AI 拆分)
3416
+ if (iterationDir) {
3417
+ try {
3418
+ const funcMapPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'FUNCTION_MAP.md');
3419
+ let funcUnitsInjected = false;
3420
+ if (await (0, fs_extra_1.pathExists)(funcMapPath)) {
3421
+ const funcMapContent = await (0, fs_extra_1.readFile)(funcMapPath, 'utf-8');
3422
+ const funcUnits = parseFunctionMap(funcMapContent, standardPlatforms);
3423
+ if (funcUnits.length > 0) {
3424
+ p += `## 🧩 功能单元上下文(CLI 从 FUNCTION_MAP.md 精准提取)\n\n`;
3425
+ p += `> 以下是从 FUNCTION_MAP.md 中为每个功能单元提取的相关上下文。\n`;
3426
+ p += `> 拆分时必须基于这些功能单元,每个功能单元对应 1~3 个任务。\n\n`;
3427
+ for (const unit of funcUnits) {
3428
+ const unitCtx = await (0, unit_context_assembler_1.extractOverviewContext)(iterationDir, unit.name, unit.platforms);
3429
+ p += `### 📌 功能单元: ${unit.name}\n`;
3430
+ p += `- 涉及端: ${unit.platforms.join(', ')}\n`;
3431
+ if (unit.sharedCapability && unit.sharedCapability !== '无')
3432
+ p += `- 共享能力: ${unit.sharedCapability}\n`;
3433
+ if (unit.dependsOn && unit.dependsOn.length > 0)
3434
+ p += `- 依赖: ${unit.dependsOn.join(', ')}\n`;
3435
+ if (unit.description)
3436
+ p += `- 说明: ${unit.description}\n`;
3437
+ p += `\n${unitCtx}\n\n---\n\n`;
3438
+ }
3439
+ funcUnitsInjected = true;
3440
+ }
3441
+ }
3442
+ // v7.5.0+: FUNCTION_MAP.md 不存在或为空时,从 ANALYSIS.md 提取功能单元清单
3443
+ if (!funcUnitsInjected) {
3444
+ const analysisPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'ANALYSIS.md');
3445
+ if (await (0, fs_extra_1.pathExists)(analysisPath)) {
3446
+ const analysisContent = await (0, fs_extra_1.readFile)(analysisPath, 'utf-8');
3447
+ const analysisUnits = parseAnalysisFunctionalUnits(analysisContent, standardPlatforms);
3448
+ if (analysisUnits.length > 0) {
3449
+ p += `## 🧩 功能单元清单(CLI 从 ANALYSIS.md 提取,v7.5.0+)\n\n`;
3450
+ p += `> 以下是从 ANALYSIS.md 功能点清单表中提取的 **全部功能单元**(F-xx 编号)。\n`;
3451
+ p += `> ⚠️ **你必须为每个功能单元创建至少 1 个任务,不允许遗漏任何功能单元。**\n`;
3452
+ p += `> 可以按合并规则将相关功能单元合并为同一个任务,但必须确保所有功能单元都被覆盖。\n\n`;
3453
+ p += `| 编号 | 功能单元 | 涉及端 | 核心功能 |\n`;
3454
+ p += `|:---|:---|:---|:---|\n`;
3455
+ for (const u of analysisUnits) {
3456
+ p += `| ${u.id} | ${u.name} | ${u.platforms.join(', ')} | ${u.description || '-'} |\n`;
3457
+ }
3458
+ p += `\n**拆分要求**:\n`;
3459
+ p += `- 每个功能单元必须在某个任务的 \`functionalUnit\` 字段中被引用\n`;
3460
+ p += `- 合并时,在任务描述中列出所有合并的功能单元编号(如 F-02, F-03)\n`;
3461
+ p += `- 不允许将不同端的功能单元合并到同一个任务\n\n`;
3462
+ }
3463
+ }
3464
+ // v8.3.220+: FUNCTION_MAP 和 ANALYSIS 都为空/骨架时,回退到 features/ 目录(功能单元权威来源)
3465
+ if (!funcUnitsInjected) {
3466
+ const featureDirs = await (0, spec_paths_1.parseFeatureList)(iterationDir);
3467
+ if (featureDirs.length > 0) {
3468
+ p += `## 🧩 功能单元清单(CLI 从 010-requirements/features/ 目录提取,v8.3.220+)\n\n`;
3469
+ p += `> 以下是从 features/ 目录读取的 **全部功能单元**(权威来源)。\n`;
3470
+ p += `> ⚠️ **你必须为每个功能单元创建至少 1 个任务,不允许遗漏任何功能单元。**\n\n`;
3471
+ p += `| 功能单元 |\n`;
3472
+ p += `|:---|\n`;
3473
+ for (const fn of featureDirs) {
3474
+ p += `| ${fn} |\n`;
3475
+ }
3476
+ p += `\n**拆分要求**:\n`;
3477
+ p += `- 每个功能单元必须在某个任务的 \`functionalUnit\` 字段中被引用\n`;
3478
+ p += `- 涉及端从 CONSTITUTION.md 端列表判断,或从 features/{单元}/.meta/source 读取\n\n`;
3479
+ funcUnitsInjected = true;
3480
+ }
3481
+ }
3482
+ }
3483
+ }
3484
+ catch { }
3485
+ }
3486
+ // SpecCore 拆分原则
3487
+ p += `## ⚙️ SpecCore 拆分原则\n\n`;
3488
+ p += `SpecCore 核心理念: "Code by Spec, Not by Vibe" — 每个任务必须有对应的 Spec,AI 在 Spec 约束下工作。\n\n`;
3489
+ p += `### 原子任务定义\n`;
3490
+ p += `一个原子任务 = 一个开发者在指定粒度内可独立完成的、有明确验收标准的最小工作单元。\n`;
3491
+ p += `判定标准(全部满足):\n`;
3492
+ p += `- 有独立的输入/输出(API 接口 / 页面 / 数据表)\n`;
3493
+ p += `- _shared/ + {端}/TASK.md 能独立写满(REQ.md + TECH.md + 各端子任务)\n`;
3494
+ p += `- execute 时不强依赖其他 Task 的运行时状态\n`;
3495
+ p += `- 有明确的验收标准(AC 可枚举)\n`;
3496
+ p += `- 可独立提 PR、独立 review\n\n`;
3497
+ p += `### 合并规则(优先合并,减少任务数)\n`;
3498
+ p += `- 同一数据实体的 CRUD → 共享数据模型,合并为 1 个任务\n`;
3499
+ p += `- 页面 + 对应后端接口 < 5 个 → 前后端强耦合,一人做效率最高\n`;
3500
+ p += `- 纯配置/文案/样式微调 → 不构成独立工作单元\n`;
3501
+ p += `- 关联紧密的小功能(如列表页 + 详情页)→ 共享路由和状态\n`;
3502
+ p += `- **复杂度判断**:如果一个需求章节接口 ≤ 3、数据表 ≤ 1、预估工时 < 粒度下限 → 必须合并到最相关的任务,不单独拆\n`;
3503
+ p += `- **宁少勿多**:任务数越少越好,每个任务应该是真正独立的工作单元。如果两个功能共享数据模型或路由,一人就能做完,不要拆\n\n`;
3504
+ p += `### 拆分规则\n`;
3505
+ p += `- 超出当前粒度接口上限 → 按业务领域拆\n`;
3506
+ p += `- 超出当前粒度数据表上限 → 按数据层拆\n`;
3507
+ p += `- 超出当前粒度工时上限 → 必须再拆\n`;
3508
+ p += `- 低于当前粒度工时下限 → 合并到关联任务\n`;
3509
+ p += `- 跨端功能 → 按端拆(后端 1 个 + 每个前端各 1 个)\n`;
3510
+ p += `- 独立第三方集成(支付/短信/OSS)→ 独立任务\n\n`;
3511
+ // 聚合度分析(v6.43.0+)
3512
+ p += `### 🔍 功能聚合度分析(重要 — 拆分前必须执行)\n\n`;
3513
+ p += `对 REQUIREMENT.md 中的每个功能模块,判断它是「聚合的」还是「单端的」:\n\n`;
3514
+ p += `**聚合功能**(涉及多个端):\n`;
3515
+ p += `- 判定标准:功能的「涉及端」列包含 2 个以上端,或功能描述中涉及多端交互\n`;
3516
+ p += `- 拆分策略:**按端拆分**,每个端一个独立 Task\n`;
3517
+ p += ` - 例:「用户登录」涉及 h5 + backend + admin → 拆成 3 个 Task:\n`;
3518
+ p += ` - Task-NNN-h5-login(h5 端登录页面 + 交互)\n`;
3519
+ p += ` - Task-NNN-backend-login(登录接口 + 鉴权 + 数据模型)\n`;
3520
+ p += ` - Task-NNN-admin-login(管理端登录入口)\n`;
3521
+ p += ` - 错误示例:Task-116-approval-scheduler(跨端功能未拆分)\n`;
3522
+ p += ` - ✅ 正确做法:Task-116-approval-backend + Task-117-approval-admin + Task-118-approval-h5\n`;
3523
+ p += ` - 共享契约:\`_shared/API_CONTRACT.yaml\` 定义跨端接口\n`;
3524
+ p += ` - 每个 Task 的 scope 只包含该端,不要跨端\n\n`;
3525
+ p += `**单端功能**(只涉及一个端):\n`;
3526
+ p += `- 直接生成 1 个 Task,scope 只包含该端\n`;
3527
+ p += `- 但仍需检查隐含跨端依赖(如 admin 页面需要 backend 提供新接口)\n`;
3528
+ p += `- 如有隐含依赖,在 dependencies 中标注,或在 scope 中加入对应端\n\n`;
3529
+ p += `**判断流程**:\n`;
3530
+ p += `1. 读 overview/REQUIREMENT.md 的功能模块清单 → 查看「涉及端」列\n`;
3531
+ p += `2. 读 overview/ANALYSIS.md → 确认跨端关联和数据流向\n`;
3532
+ p += `3. 读 overview/TECH.md → 了解整体架构中的端交互\n`;
3533
+ p += `4. 对每个功能模块判定聚合度,决定拆分策略\n\n`;
3534
+ // 类型文档 1:1 映射规则
3535
+ p += `### 类型文档拆分规则(bugs/refactors/research)\n`;
3536
+ p += `与 feature 不同,类型文档采用 **1:1 映射**——每个文档直接对应一个任务,不拆分不合并:\n`;
3537
+ p += `- \`bugs/xxx.md\` → 1 个 bugfix 任务(一个 bug 就是一个任务,不要把多个 bug 合并)\n`;
3538
+ p += `- \`refactors/xxx.md\` → 1 个 refactor 任务\n`;
3539
+ p += `- \`research/xxx.md\` → 1 个 research 任务\n`;
3540
+ p += `- \`features/xxx.md\` → 按功能单元拆分规则处理(可能 1~3 个 feature 任务)\n`;
3541
+ p += `- **sourceFile 必须填写**:类型文档任务的 sourceFile 就是该文档的相对路径(如 \`bugs/login-timeout.md\`)\n\n`;
3542
+ p += `### 依赖关系\n`;
3543
+ p += `- 基础模块(认证/数据库/配置)优先拆出,作为第一批任务\n`;
3544
+ p += `- 依赖链深度 ≤ 3\n`;
3545
+ p += `- 同层级无循环依赖\n\n`;
3546
+ p += `### 总量约束(功能单元基准)\n`;
3547
+ p += `- 核心原则:以需求的功能单元为基准拆分,而非需求文档的章节划分\n`;
3548
+ p += `- 每个功能单元默认 1 个任务,最多 3 个\n`;
3549
+ p += `- 单次迭代总任务数**不得超过 20 个**(超出说明粒度过细,必须合并)\n`;
3550
+ p += `- 每个任务必须有明确的 owner(对应 STAFFING 中的成员)\n`;
3551
+ p += `- 高优先级任务排在前面\n`;
3552
+ p += `- 没有实质性功能内容的章节(如背景、概述、架构、术语等)不能作为拆分依据\n\n`;
3553
+ // v8.3.160+: 根据 subagent 动态调整本步骤任务指令
3554
+ if (subagent === 'dependency-analyst') {
3555
+ p += `## 🔗 本步骤任务:依赖关系分析\n\n`;
3556
+ p += `你是 **依赖关系分析师**。你的职责是分析任务间的依赖关系,不需要做拆分或工时估算。\n\n`;
3557
+ p += `**输入**:上一步生成的任务列表(含 id、name、functionalUnit、scope)\n`;
3558
+ p += `**输出**:为每个任务补充 \`dependencies\` 字段,标注该任务依赖哪些其他任务(填写 task id)\n\n`;
3559
+ p += `### 依赖判定规则\n\n`;
3560
+ p += `- **数据依赖**:Task-B 需要 Task-A 创建的数据表/实体 → B 依赖 A\n`;
3561
+ p += `- **API 依赖**:Task-B 调用 Task-A 提供的接口 → B 依赖 A\n`;
3562
+ p += `- **状态依赖**:Task-B 需要 Task-A 完成后的某个业务状态 → B 依赖 A\n`;
3563
+ p += `- **配置依赖**:Task-B 需要 Task-A 完成后的配置/环境 → B 依赖 A\n`;
3564
+ p += `- 依赖链深度 ≤ 3,发现循环依赖必须标注并建议解耦方案\n`;
3565
+ p += `- 同层级任务之间尽量无依赖,可并行执行\n\n`;
3566
+ }
3567
+ else if (subagent === 'effort-estimator') {
3568
+ p += `## ⏱️ 本步骤任务:工时估算与排期\n\n`;
3569
+ p += `你是 **工时估算专家**。你的职责是为每个任务估算工时并确定优先级,不需要重新拆分。\n\n`;
3570
+ p += `**输入**:带依赖关系的任务列表(含 id、name、scope、dependencies)\n`;
3571
+ p += `**输出**:为每个任务补充 \`estimatedHours\`、\`priority\`、\`owner\` 字段\n\n`;
3572
+ p += `### 估算规则\n\n`;
3573
+ p += `- 单个任务工时控制在 **2h-8h** 之间\n`;
3574
+ p += `- 超过 8h 必须拆分为多个任务(返回修改建议,不要硬估)\n`;
3575
+ p += `- 低于 2h 建议合并到关联任务\n`;
3576
+ p += `- **高优先级**:基础模块(认证/数据库/配置)、被多个任务依赖的任务\n`;
3577
+ p += `- **中优先级**:核心业务功能\n`;
3578
+ p += `- **低优先级**:独立第三方集成、纯 UI 调整\n`;
3579
+ p += `- 考虑技术风险和不确定性,预留 20% 缓冲\n\n`;
3580
+ }
3581
+ else {
3582
+ // task-decomposer 默认
3583
+ p += `## 📋 本步骤任务:任务拆分与内容生成\n\n`;
3584
+ p += `你是 **任务拆分专家**。你的职责是将功能模块拆分为原子级开发任务,并为每个任务生成完整的内容。\n\n`;
3585
+ p += `**核心要求**:\n`;
3586
+ p += `- 按 FUNCTION_MAP.md 中的功能单元拆分\n`;
3587
+ p += `- 每个功能单元涉及的端,各生成一个子任务(scope 只包含该端)\n`;
3588
+ p += `- 为每个任务生成完整的 reqContent、techContent、devGuideContent\n`;
3589
+ p += `- **内容必须从 020-specs/ 对应文档中读取并提取**,禁止输出模板化占位符\n`;
3590
+ p += `- 如果某个文档内容不足,标注「基于现有信息推断」而非留空\n\n`;
3591
+ }
3592
+ // 输出格式
3593
+ p += `## 📤 输出格式\n\n`;
3594
+ p += `请输出 JSON 数组,每个 Task 包含:\n`;
3595
+ p += '```json\n';
3596
+ p += `[\n {\n`;
3597
+ p += ` "id": "Task-001",\n`;
3598
+ p += ` "functionalUnit": "所属功能单元/影响域(必填!见下方类型规则)",\n`;
3599
+ p += ` "name": "任务名称(中文)",\n`;
3600
+ p += ` "topic": "english-slug-for-directory",\n`;
3601
+ p += ` "type": "feature|bugfix|refactor|research|security|performance",\n`;
3602
+ p += ` "reason": "为什么这样拆分",\n`;
3603
+ p += ` "scope": ["booking-service", "admin-web"],\n`;
3604
+ p += ` "priority": "high|medium|low",\n`;
3605
+ p += ` "dependencies": [],\n`;
3606
+ p += ` "acceptanceCriteria": ["AC1: ..."],\n`;
3607
+ p += ` "risk": "low|medium|high",\n`;
3608
+ p += ` "owner": "建议负责人",\n`;
3609
+ p += ` "sourceFile": "来源文档路径(如 bugs/login-timeout.md、features/user-auth.md)",\n`;
3610
+ p += ` "reqContent": "需求描述内容(Markdown 格式,写入 REQ.md)",\n`;
3611
+ p += ` "techContent": "技术方案内容(Markdown 格式,写入 TECH.md)",\n`;
3612
+ p += ` "devGuideContent": "开发者实现指南(Markdown 格式,写入 DEV_GUIDE.md):实现步骤、关键代码示例、与存量功能集成、注意事项"\n`;
3613
+ p += ` }\n]\n`;
3614
+ p += '```\n\n';
3615
+ p += `> **functionalUnit 必须填写**:根据任务类型语义不同:\n`;
3616
+ p += `> - feature → 功能模块名(如:用户管理、订单系统、支付模块)\n`;
3617
+ p += `> - bugfix → 受影响组件/流程(如:登录流程、支付回调、数据同步)\n`;
3618
+ p += `> - refactor → 重构目标范围(如:数据库层、API网关、状态管理)\n`;
3619
+ p += `> - security → 安全修复范围(如:SQL注入防护、XSS修复、鉴权加固)\n`;
3620
+ p += `> - performance → 性能优化目标(如:查询优化、缓存策略、并发提升)\n`;
3621
+ p += `> - research → 研究主题(如:WebSocket方案、缓存策略)\n`;
3622
+ p += `> 同一模块/领域的任务填相同的值,用于粒度校验和任务分组\n`;
3623
+ p += `> **topic 必填且必须是英文短横线格式**:\n`;
3624
+ p += `> - 用于生成任务目录名 \`Task-NNN-{topic}\`,如 \`Task-001-user-auth\`\n`;
3625
+ p += `> - 从功能单元名提取英文关键词拼接,如:\n`;
3626
+ p += `> - "用户认证" → \`user-authentication\`\n`;
3627
+ p += `> - "订单CRUD" → \`order-crud\`\n`;
3628
+ p += `> - "支付模块" → \`payment-module\`\n`;
3629
+ p += `> - 格式:全小写、短横线连接,如 \`user-auth\`、\`order-list\`\n`;
3630
+ p += `> - ⛔ 禁止纯中文、禁止空值、禁止驼峰命名、禁止下划线\n`;
3631
+ p += `> - 如果功能单元名完全没有英文可提取,用简写英文意译(如"会议室预订" → \`room-booking\`)\n`;
3632
+ p += `> **sourceFile** 必须填写:该任务对应的 020-specs 源文档路径(如 \`bugs/login-timeout.md\`、\`features/user-auth.md\`、\`refactors/db-pool.md\`),用于在 CONTEXT.md 中生成来源追溯\n`;
3633
+ p += `> **scope 强制约束(违反则拆分无效)**:\n`;
3634
+ p += `> - scope 数组中的每个值必须是上面「项目端列表」中的标准端名之一\n`;
3635
+ p += `> - ⛔ 禁止出现 api、web、backend、frontend、server、admin、h5、mobile、pc、app 等非标准名称\n`;
3636
+ p += `> - ⛔ 禁止出现中文端名(如"后端"、"前端"、"移动端")\n`;
3637
+ p += `> - 每个任务按 scope 中的端创建子任务目录,目录名 = 标准端名\n`;
3638
+ p += `> - 如果 scope 含非标准端名,整个 JSON 视为不合格,必须重新生成\n`;
3639
+ p += `> **reqContent 质量要求(必填,禁止模板化)**:\n`;
3640
+ p += `> - 必须是**具体的、可执行的需求描述**,不是"待补充"或"参考全局文档"\n`;
3641
+ p += `> - 包含:业务规则(含边界条件)、数据模型(字段/类型/约束)、接口清单(方法/路径/参数/响应)\n`;
3642
+ p += `> - 从 020-specs/overview/REQUIREMENT.md 和 020-specs/{功能模块}/{对应端}/TECH.md 中提取本任务相关的具体内容\n`;
3643
+ p += `> - 直接写入 00-specs/REQ.md,执行时 AI 不再重新分析需求\n`;
3644
+ p += `> **techContent 质量要求(必填,禁止模板化)**:\n`;
3645
+ p += `> - 必须是**具体的技术实现方案**,不是框架模板\n`;
3646
+ p += `> - 包含:架构设计、核心逻辑伪代码/流程、数据库设计(表结构/索引)、API 详细定义、测试策略\n`;
3647
+ p += `> - 从 020-specs/overview/TECH.md 和 020-specs/{功能模块}/{对应端}/TECH.md 中提取本任务相关的技术细节\n`;
3648
+ p += `> - 直接写入 00-specs/TECH.md,执行时 AI 据此直接开发\n`;
3649
+ p += `> **质量红线**:如果 reqContent/techContent 只有标题和占位符(如 "<!-- AI-FILL -->"),视为不合格,必须重新生成\n\n`;
3650
+ // v8.3.4+: 强制约束专节(类似 analyze Phase 2)
3651
+ p += `### ⛔ 强制约束(违反则拆分无效)\n\n`;
3652
+ p += `- **禁止跳过任何功能模块** — 即使某个模块信息不足,也必须基于已有信息拆分并填充 reqContent/techContent,标注「基于现有信息推断」而非留空\n`;
3653
+ p += `- **禁止输出空内容** — reqContent、techContent、devGuideContent 每个字段长度必须 ≥200 字符,不允许写「待补充」「TODO」「参考全局文档」\n`;
3654
+ p += `- **禁止省略字段** — JSON 中必须包含 reqContent、techContent、devGuideContent 三个字段,缺一不可\n`;
3655
+ p += `- **内容必须从 analyze 产物提取** — reqContent 从 REQUIREMENT.md 提取,techContent 从 TECH.md 提取,devGuideContent 从 DEV_GUIDE.md 提取\n`;
3656
+ p += `- **如果 token 不足**:优先保证 reqContent 完整,其次是 techContent,最后是 devGuideContent\n\n`;
3657
+ p += `### ⚠️ 拆分规则(重要)\n\n`;
3658
+ p += `- **按功能单元拆分,不按技术层拆分**:每个任务必须对应一个业务功能(如「预订管理」「签到」「审批」),不要按技术层分组(如「后端改进」「前端修复」)\n`;
3659
+ p += `- ❌ 错误示例:Task-129-backend-improve、Task-130-admin-fix(按技术层分组)\n`;
3660
+ p += `- ✅ 正确示例:Task-129-booking-crud、Task-130-checkin-flow、Task-131-approval-management(按功能单元分组)\n`;
3661
+ p += `- **topic 必须反映功能单元**:每个任务的 topic(目录名后缀)必须唯一且反映业务功能,禁止所有任务都用同一个后缀(如 -impl)\n`;
3662
+ p += `- **scope 必须使用标准端名**:禁止使用 api/web/backend/frontend 等非标准名称\n`;
3663
+ p += `- **一个功能单元默认 1 个任务**,最多拆成 3 个\n`;
3664
+ p += `- **按 scope 涉及的端创建子任务**:每个端 1 个子任务目录\n\n`;
3665
+ // 质量自检
3666
+ p += `## ✅ 质量自检(必须全部通过)\n\n`;
3667
+ p += `□ 每个任务都满足原子任务定义?\n`;
3668
+ p += `□ **每个任务按功能单元命名,不是按技术层**?(禁止 backend-improve/admin-fix 这类名称)\n`;
3669
+ p += `□ **每个 topic 唯一且反映业务功能**?(禁止全部用 -impl)\n`;
3670
+ p += `□ **scope 只包含 CONSTITUTION.md 标准端名**?(禁止 api/web/backend/frontend)\n`;
3671
+ p += `□ 没有循环依赖?\n`;
3672
+ p += `□ 基础模块排在前面?\n`;
3673
+ p += `□ 同功能单元内的任务没被过度拆分?(每个功能单元 ≤ 3 个任务)\n`;
3674
+ p += `□ 总任务数不超过 20 个?\n`;
3675
+ p += `□ 没有把非功能章节(背景/概述/架构/术语)作为拆分依据?\n`;
3676
+ p += `□ 每个任务都有明确的 owner 和 acceptanceCriteria?\n`;
3677
+ p += `□ 每个任务都能独立提 PR、独立 review?\n`;
3678
+ // 自动模式指令
3679
+ p += `## 🤖 自动模式指令\n\n`;
3680
+ p += `本拆分在自动模式下执行,请遵循以下原则:\n`;
3681
+ p += `1. **不要询问用户** — 按你的最佳判断直接拆分,不要请求确认或澄清\n`;
3682
+ p += `2. **有疑问就记录** — 如果对需求理解、技术选型、任务边界有疑问,不要停下来问,而是:\n`;
3683
+ p += ` - 按你的最佳判断继续拆分\n`;
3684
+ p += ` - 将疑问写入 \`.speccore/questions/split-{迭代名}-{日期}.md\`\n`;
3685
+ p += ` - 格式: \`## 疑问 N
3686
+ - 问题描述
3687
+ - 你的判断
3688
+ - 建议后续动作\`
3689
+ `;
3690
+ p += `3. **禁止跳过任何模块** — 即使某个功能模块信息不足,也必须基于已有信息拆分并填充内容,宁可标注「基于现有信息推断:需要补充 xxx」也不许留空或省略\n`;
3691
+ p += `4. **输出 JSON** — 直接输出拆分结果的 JSON 数组,不要输出其他内容\n`;
3692
+ // 持久指令(用户调整时 AI 可回读此文件)
3693
+ p += `\n## 🔄 调整指令(持久有效)\n\n`;
3694
+ p += `当用户要求调整拆分方案时(如“合并”“拆分”“改工时”“改优先级”):\n`;
3695
+ p += `1. **先重读本文件** — 本文件包含完整的拆分规则、粒度约束、端配置、Spec 上下文\n`;
3696
+ p += `2. **在同一套规则下调整** — 合并/拆分/修改都必须遵守粒度硬约束\n`;
3697
+ p += `3. **重新输出完整 JSON** — 不要只输出修改的部分,输出调整后的完整数组\n`;
3698
+ p += `4. **文件路径**: \`.speccore/prompts/split-suggestion-${iteration}.md\`\n\n`;
3699
+ return p;
3700
+ }
3701
+ /**
3702
+ * 读取迭代的 STAFFING.md 人员排期配置
3703
+ */
3704
+ function readStaffing(iterationDir) {
3705
+ try {
3706
+ const staffingPath = (0, path_1.join)(iterationDir, 'STAFFING.md');
3707
+ if (!require('fs').existsSync(staffingPath))
3708
+ return null;
3709
+ const content = require('fs').readFileSync(staffingPath, 'utf-8');
3710
+ const members = [];
3711
+ // 解析表格: | 张三 | 后台 | 70% |
3712
+ const lines = content.split(/\r?\n/);
3713
+ for (const line of lines) {
3714
+ if (!line.startsWith('|') || line.includes(':---'))
3715
+ continue;
3716
+ const cols = line.split('|').map((c) => c.trim()).filter(Boolean);
3717
+ if (cols.length >= 3 && cols[0] !== '人员' && cols[0] !== '成员') {
3718
+ const capacity = parseInt(cols[2] || '100') || 100;
3719
+ members.push({
3720
+ name: cols[0],
3721
+ platforms: (cols[1] || '').split(/[,,]/).map((p) => p.trim()),
3722
+ capacity,
3723
+ });
3724
+ }
3725
+ }
3726
+ return members.length > 0 ? members : null;
3727
+ }
3728
+ catch {
3729
+ return null;
3730
+ }
3731
+ }
3732
+ /**
3733
+ * 根据平台和负荷自动推荐负责人
3734
+ */
3735
+ function autoAssign(section, platforms, staffing) {
3736
+ const targetPlatform = section.platform || '';
3737
+ if (!targetPlatform || !staffing.length)
3738
+ return '未分配';
3739
+ // 找到匹配平台的、负荷最低的人
3740
+ let best = null;
3741
+ let bestLoad = Infinity;
3742
+ for (const m of staffing) {
3743
+ const platformMatch = m.platforms.some(p => targetPlatform.includes(p) || p.includes(targetPlatform));
3744
+ if (platformMatch && m.capacity < bestLoad) {
3745
+ best = m;
3746
+ bestLoad = m.capacity;
3747
+ }
3748
+ }
3749
+ return best ? best.name : '未分配';
3750
+ }
3751
+ /**
3752
+ * 分析章节复杂度,决定优先级和工时(简化版)
3753
+ */
3754
+ function estimateSectionComplexity(section) {
3755
+ const content = section.content || '';
3756
+ const name = section.name || '';
3757
+ const full = `${name}\n${content}`;
3758
+ // 粗略估算工时(仅用于展示)
3759
+ const wordCount = full.length;
3760
+ const estimatedHours = wordCount > 800 ? 16 : wordCount > 300 ? 8 : 4;
3761
+ // 优先级
3762
+ let priority = 'medium';
3763
+ if (full.includes('核心') || full.includes('基础') || full.includes('安全')) {
3764
+ priority = 'high';
3765
+ }
3766
+ else if (wordCount < 200) {
3767
+ priority = 'low';
3768
+ }
3769
+ return { estimatedHours, priority };
3770
+ }
3771
+ /**
3772
+ * 语义依赖检测: 比字符串匹配更准确的任务间关系
3773
+ */
3774
+ function detectSemanticDependencies(sections) {
3775
+ const deps = new Map();
3776
+ // 关键词对: [from, to]
3777
+ const semanticPairs = [
3778
+ [/订单|支付|交易/, /用户|登录|认证|鉴权/, '需要用户模块'],
3779
+ [/管理|后台|admin/, /用户|登录|认证/, '需要登录鉴权'],
3780
+ [/列表|查询|搜索/, /数据库|表|DDL|schema/, '依赖数据表'],
3781
+ [/页面|界面|UI|表单/, /API|接口|后端/, '依赖后端接口'],
3782
+ [/统计|报表|dashboard/, /列表|查询|数据/, '依赖数据查询'],
3783
+ [/通知|消息|推送|email/, /用户|人员|member/, '依赖用户数据'],
3784
+ [/文件|上传|下载|附件/, /存储|oss|s3|bucket/, '依赖存储服务'],
3785
+ [/审批|审核|workflow/, /用户|角色|权限/, '依赖用户角色'],
3786
+ ];
3787
+ for (let i = 0; i < sections.length; i++) {
3788
+ const si = sections[i];
3789
+ const siContent = `${si.name}\n${si.content || ''}`;
3790
+ const taskDeps = [];
3791
+ for (let j = 0; j < sections.length; j++) {
3792
+ if (i === j)
3793
+ continue;
3794
+ const sj = sections[j];
3795
+ const sjContent = `${sj.name}\n${sj.content || ''}`;
3796
+ // 语义匹配
3797
+ for (const [fromPat, toPat, reason] of semanticPairs) {
3798
+ if (fromPat.test(siContent) && toPat.test(sjContent)) {
3799
+ const depLabel = `${sections[j]._taskId || `Task-${String(j + 1).padStart(3, '0')}`}(${sj.name.slice(0, 10)})`;
3800
+ if (!taskDeps.includes(depLabel))
3801
+ taskDeps.push(depLabel);
3802
+ break; // 每对只匹配一次
3803
+ }
3804
+ }
3805
+ }
3806
+ if (taskDeps.length > 0) {
3807
+ deps.set(sections[i]._taskId || `Task-${String(i + 1).padStart(3, '0')}`, taskDeps);
3808
+ }
3809
+ }
3810
+ return deps;
3811
+ }
3812
+ async function detectExistingTasks(iterDir) {
3813
+ const tasks = [];
3814
+ // 优先从 030-tasks/ 扫描
3815
+ const scanDir = (0, path_1.join)(iterDir, '030-tasks');
3816
+ const targetDir = (await (0, fs_extra_1.pathExists)(scanDir)) ? scanDir : iterDir;
3817
+ const scanRecursive = async (dir) => {
3818
+ try {
3819
+ const entries = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
3820
+ for (const e of entries) {
3821
+ if (e.isDirectory()) {
3822
+ if (e.name.startsWith('Task-')) {
3823
+ tasks.push(e.name);
3824
+ }
3825
+ else if (!e.name.startsWith('.')) {
3826
+ // 递归扫描类型子目录(feature/bugfix/refactor/research)
3827
+ await scanRecursive((0, path_1.join)(dir, e.name));
3828
+ }
3829
+ }
3830
+ }
3831
+ }
3832
+ catch { }
3833
+ };
3834
+ await scanRecursive(targetDir);
3835
+ return tasks;
3836
+ }
3837
+ /**
3838
+ * v8.3.24+: 扫描已有 Task 的端结构
3839
+ * 返回每个 Task 已有的端目录(端平铺结构: {端名}/)
3840
+ */
3841
+ async function scanExistingTaskStructure(iterDir) {
3842
+ const result = new Map();
3843
+ const tasks = await detectExistingTasks(iterDir);
3844
+ const standardPlatforms = await (0, spec_paths_1.parsePlatformList)();
3845
+ for (const taskName of tasks) {
3846
+ // 递归查找实际任务目录(支持 030-tasks/{type}/Task-xxx/ 布局)
3847
+ const tasksRoot = (0, path_1.join)(iterDir, '030-tasks');
3848
+ let taskDir = (0, path_1.join)(tasksRoot, taskName);
3849
+ if (!(await (0, fs_extra_1.pathExists)(taskDir))) {
3850
+ // 可能在类型子目录下
3851
+ try {
3852
+ const entries = await (0, fs_extra_1.readdir)(tasksRoot, { withFileTypes: true });
3853
+ for (const e of entries) {
3854
+ if (e.isDirectory() && !e.name.startsWith('.')) {
3855
+ const candidate = (0, path_1.join)(tasksRoot, e.name, taskName);
3856
+ if (await (0, fs_extra_1.pathExists)(candidate)) {
3857
+ taskDir = candidate;
3858
+ break;
3859
+ }
3860
+ }
3861
+ }
3862
+ }
3863
+ catch { }
3864
+ }
3865
+ if (!(await (0, fs_extra_1.pathExists)(taskDir)))
3866
+ continue;
3867
+ const platforms = [];
3868
+ // v8.3.24+: 扫描端平铺结构 — 任务目录下的合法端名子目录
3869
+ try {
3870
+ const entries = await (0, fs_extra_1.readdir)(taskDir, { withFileTypes: true });
3871
+ for (const e of entries) {
3872
+ if (e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_') && standardPlatforms.includes(e.name)) {
3873
+ platforms.push(e.name);
3874
+ }
3875
+ }
3876
+ }
3877
+ catch { }
3878
+ if (platforms.length > 0) {
3879
+ result.set(taskName, platforms);
3880
+ }
3881
+ }
3882
+ return result;
3883
+ }
3884
+ /**
3885
+ * 生成任务总览报告 → 000-overview/TASK_SUMMARY.md
3886
+ * 包含:任务名、功能单元、人工工时、AI工时、优先级、依赖、风险
3887
+ */
3888
+ async function generateTaskSummary(iterationDir, tasks, createdSections) {
3889
+ const today = new Date().toISOString().split('T')[0];
3890
+ let md = `# 任务总览报告\n\n`;
3891
+ md += `> 生成时间: ${today} | 任务总数: ${createdSections.length}\n\n`;
3892
+ // 汇总表
3893
+ md += `## 任务清单\n\n`;
3894
+ md += `| # | 任务名 | 功能单元 | 人工工时 | AI工时 | 优先级 | 依赖 | 风险 |\n`;
3895
+ md += `| :--- | :--- | :--- | ---: | ---: | :---: | :--- | :---: |\n`;
3896
+ let totalHumanHours = 0;
3897
+ let totalAiHours = 0;
3898
+ for (let i = 0; i < createdSections.length; i++) {
3899
+ const task = tasks[i] || {};
3900
+ const sec = createdSections[i];
3901
+ const taskId = sec._taskId || `Task-${String(i + 1).padStart(3, '0')}`;
3902
+ const name = sec.name || task.name || '';
3903
+ const unit = task.functionalUnit || '(未标注)';
3904
+ const estimatedHours = task.estimatedHours || sec._complexity?.estimatedHours || 0;
3905
+ // AI 工时 = estimatedHours 中标注为 AI 可自动完成的部分(目前全部算 AI 工时)
3906
+ const aiHours = Math.round(estimatedHours * 0.7); // 预估 70% 可由 AI 完成
3907
+ const humanHours = estimatedHours - aiHours;
3908
+ const priority = task.priority || sec._complexity?.priority || 'medium';
3909
+ const deps = (task.dependencies || []).join(', ') || '-';
3910
+ const risk = task.risk || 'medium';
3911
+ totalHumanHours += humanHours;
3912
+ totalAiHours += aiHours;
3913
+ const priorityIcon = priority === 'high' ? '🔴' : priority === 'low' ? '🟢' : '🟡';
3914
+ const riskIcon = risk === 'high' ? '🔴' : risk === 'low' ? '🟢' : '🟡';
3915
+ md += `| ${taskId} | ${name} | ${unit} | ${humanHours}h | ${aiHours}h | ${priorityIcon} ${priority} | ${deps} | ${riskIcon} ${risk} |\n`;
3916
+ }
3917
+ // 汇总
3918
+ md += `\n## 工时汇总\n\n`;
3919
+ md += `| 指标 | 值 |\n| :--- | ---: |\n`;
3920
+ md += `| 人工总工时 | **${totalHumanHours}h** |\n`;
3921
+ md += `| AI总工时 | **${totalAiHours}h** |\n`;
3922
+ md += `| 总预估工时 | **${totalHumanHours + totalAiHours}h** |\n`;
3923
+ md += `| AI 占比 | **${totalHumanHours + totalAiHours > 0 ? Math.round(totalAiHours / (totalHumanHours + totalAiHours) * 100) : 0}%** |\n`;
3924
+ // 功能单元分布
3925
+ const unitCounts = {};
3926
+ for (const task of tasks.slice(0, createdSections.length)) {
3927
+ const unit = task.functionalUnit || '(未标注)';
3928
+ unitCounts[unit] = (unitCounts[unit] || 0) + 1;
3929
+ }
3930
+ md += `\n## 功能单元分布\n\n`;
3931
+ md += `| 功能单元 | 任务数 |\n| :--- | ---: |\n`;
3932
+ for (const [unit, count] of Object.entries(unitCounts)) {
3933
+ md += `| ${unit} | ${count} |\n`;
3934
+ }
3935
+ // 写入文件(带时间戳,放单独子目录)
3936
+ const overviewDir = (0, path_1.join)(iterationDir, '000-overview', 'task-summaries');
3937
+ await (0, fs_extra_1.ensureDir)(overviewDir);
3938
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 16); // 2026-08-13T14-30
3939
+ const summaryPath = (0, path_1.join)(overviewDir, `TASK_SUMMARY-${timestamp}.md`);
3940
+ await (0, fs_extra_1.writeFile)(summaryPath, md);
3941
+ return md;
3942
+ }
3943
+ /**
3944
+ * 输出 [SPECCORE_NEXT_STEPS] 标记,引导宿主 AI 对每个 Task 执行 analyze --task
3945
+ * split 完成后调用,生成任务级 00-specs/ 文档
3946
+ */
3947
+ function outputAnalyzeTaskHints(iteration, sections) {
3948
+ if (sections.length === 0)
3949
+ return;
3950
+ const iterFlag = iteration ? ` -I ${iteration}` : '';
3951
+ let md = `\n## 🧠 下一步:任务级 Spec 分析\n\n`;
3952
+ md += `拆分完成!每个 Task 需要执行 AI 分析,生成任务级 00-specs/ 文档(REQ.md、TECH.md、TASK.md、SCHEMA.md)。\n\n`;
3953
+ md += `AI 会 Read global/ 和 {端}/ 的全局 spec 作为参考,根据本 Task 的原子范围生成针对性分析。\n\n`;
3954
+ md += `### 执行命令\n\n`;
3955
+ for (const sec of sections) {
3956
+ const taskId = sec._taskId || sec.name;
3957
+ md += `\`\`\`bash
3958
+ speccore analyze --task ${taskId}${iterFlag}
3959
+ \`\`\`
3960
+
3961
+ `;
3962
+ }
3963
+ md += `> 自动模式可逐个执行,或告诉 AI:“对所有新建 Task 执行 analyze --task”\n`;
3964
+ process.stdout.write('\n[SPECCORE_NEXT_STEPS]\n');
3965
+ process.stdout.write(md);
3966
+ process.stdout.write('\n[/SPECCORE_NEXT_STEPS]\n');
3967
+ }
3968
+ // ── v6.49.14+: 模块驱动拆分 — 从 overview/REQUIREMENT.md 读取涉及端 ──
3969
+ /**
3970
+ * 从 overview/REQUIREMENT.md 解析功能模块清单的「涉及端」列
3971
+ * 支持两种格式:
3972
+ * 1. 单表格含「涉及端」列(功能模块 | 涉及端 | ...)
3973
+ * 2. 按端分节(### X.X ... — {platform} 后跟表格)
3974
+ * 返回模块列表及其涉及的标准端名
3975
+ */
3976
+ function parseModulePlatforms(content, allPlatforms) {
3977
+ // v7.4.2+: 先尝试原始格式(单表格含「涉及端」列)
3978
+ const tableResult = parseModulePlatformsTable(content, allPlatforms);
3979
+ if (tableResult.length > 0)
3980
+ return tableResult;
3981
+ // v7.4.2+: 回退到按端分节格式
3982
+ return parseModulePlatformsBySection(content, allPlatforms);
3983
+ }
3984
+ /**
3985
+ * v7.4.2+: 解析单表格含「涉及端」列的格式
3986
+ */
3987
+ function parseModulePlatformsTable(content, allPlatforms) {
3988
+ const modules = [];
3989
+ const lines = content.split(/\r?\n/);
3990
+ let inFeatureTable = false;
3991
+ let platformColIdx = -1;
3992
+ for (const line of lines) {
3993
+ if (line.includes('功能模块清单')) {
3994
+ inFeatureTable = true;
3995
+ continue;
3996
+ }
3997
+ if (!inFeatureTable)
3998
+ continue;
3999
+ if (line.startsWith('## ') && !line.includes('功能模块清单'))
4000
+ break;
4001
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
4002
+ if (cells.length < 2)
4003
+ continue;
4004
+ if (cells.some(c => c.includes('涉及端'))) {
4005
+ platformColIdx = cells.findIndex(c => c.includes('涉及端'));
4006
+ continue;
4007
+ }
4008
+ if (cells.every(c => /^[-:]+$/.test(c)))
4009
+ continue;
4010
+ const moduleName = cells[1];
4011
+ if (!moduleName || moduleName === '#' || moduleName === '模块')
4012
+ continue;
4013
+ if (platformColIdx >= 0 && platformColIdx < cells.length) {
4014
+ const raw = cells[platformColIdx];
4015
+ if (raw && raw !== '_待 AI 标注_' && raw !== '—' && raw !== '-') {
4016
+ const platforms = raw.split(/[,,]/)
4017
+ .map(p => p.trim())
4018
+ .filter(p => p && allPlatforms.includes(p));
4019
+ if (platforms.length > 0) {
4020
+ modules.push({ name: moduleName, platforms });
4021
+ continue;
4022
+ }
4023
+ }
4024
+ }
4025
+ modules.push({ name: moduleName, platforms: [...allPlatforms] });
4026
+ }
4027
+ return modules;
4028
+ }
4029
+ /**
4030
+ * v7.4.2+: 解析按端分节的 REQUIREMENT.md 格式
4031
+ * 匹配: ### 2.1 后端 — booking-service 或 ### 2.1 前端 — admin-web
4032
+ * 从标题提取端名,从后续表格提取模块名,建立模块→端的映射
4033
+ */
4034
+ function parseModulePlatformsBySection(content, allPlatforms) {
4035
+ const lines = content.split(/\r?\n/);
4036
+ // 模块名 → 涉及端列表 的映射
4037
+ const modulePlatformMap = new Map();
4038
+ let currentPlatform = '';
4039
+ let inTable = false;
4040
+ for (const line of lines) {
4041
+ // 检测按端分节标题: ### 2.1 后端 — booking-service
4042
+ const sectionMatch = line.match(/^###\s+[\d.]+\s+.*[—\-–]\s*(.+)$/);
4043
+ if (sectionMatch) {
4044
+ const rawPlatform = sectionMatch[1].trim();
4045
+ // 匹配标准端名
4046
+ currentPlatform = allPlatforms.find(p => rawPlatform.includes(p)) || '';
4047
+ inTable = false;
4048
+ continue;
4049
+ }
4050
+ // 非当前端标题下的 ## 标题 → 重置
4051
+ if (line.startsWith('## ') && !line.startsWith('### ')) {
4052
+ currentPlatform = '';
4053
+ inTable = false;
4054
+ continue;
4055
+ }
4056
+ if (!currentPlatform)
4057
+ continue;
4058
+ // 检测表格开始(表头行含「功能模块」)
4059
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
4060
+ if (cells.length >= 2 && cells.some(c => c.includes('功能模块'))) {
4061
+ inTable = true;
4062
+ continue;
4063
+ }
4064
+ if (!inTable)
4065
+ continue;
4066
+ // 分隔行跳过
4067
+ if (cells.every(c => /^[-:]+$/.test(c)))
4068
+ continue;
4069
+ // 表头行跳过
4070
+ if (cells.some(c => c === '功能模块' || c === '模块'))
4071
+ continue;
4072
+ // 数据行:第1列或第2列是模块名
4073
+ const moduleName = cells[0] && cells[0] !== '功能模块' ? cells[0] : (cells[1] || '');
4074
+ if (!moduleName || moduleName.length < 2)
4075
+ continue;
4076
+ // 跳过状态标记行
4077
+ if (moduleName.startsWith('#'))
4078
+ continue;
4079
+ // 添加到映射
4080
+ const existing = modulePlatformMap.get(moduleName) || [];
4081
+ if (!existing.includes(currentPlatform)) {
4082
+ existing.push(currentPlatform);
4083
+ }
4084
+ modulePlatformMap.set(moduleName, existing);
4085
+ }
4086
+ // 转换为结果数组
4087
+ const modules = [];
4088
+ for (const [name, platforms] of modulePlatformMap) {
4089
+ modules.push({ name, platforms: platforms.length > 0 ? platforms : [...allPlatforms] });
4090
+ }
4091
+ return modules;
4092
+ }
4093
+ /**
4094
+ * v6.70.0+: 解析 FUNCTION_MAP.md 跨端功能映射表
4095
+ * 支持两种格式:
4096
+ * 1. Markdown 表格(功能单元 | 涉及端 | ...)
4097
+ * 2. 树形格式(├── 模块名 ... platform1 ✅ | platform2 ✅)
4098
+ * 返回功能单元列表,含涉及端、共享能力、依赖关系
4099
+ */
4100
+ function parseFunctionMap(content, allPlatforms) {
4101
+ // v7.4.2+: 先尝试表格格式
4102
+ const tableResult = parseFunctionMapTable(content, allPlatforms);
4103
+ if (tableResult.length > 0)
4104
+ return tableResult;
4105
+ // v7.4.2+: 回退到树形格式解析
4106
+ return parseFunctionMapTree(content, allPlatforms);
4107
+ }
4108
+ /**
4109
+ * v7.4.2+: 解析 FUNCTION_MAP.md 表格格式
4110
+ */
4111
+ function parseFunctionMapTable(content, allPlatforms) {
4112
+ const units = [];
4113
+ const lines = content.split(/\r?\n/);
4114
+ let inTable = false;
4115
+ let platformColIdx = -1;
4116
+ let sharedCapColIdx = -1;
4117
+ let dependsOnColIdx = -1;
4118
+ let descColIdx = -1;
4119
+ for (const line of lines) {
4120
+ if (line.includes('功能单元') && line.includes('涉及端')) {
4121
+ inTable = true;
4122
+ const headerCells = line.split('|').map(c => c.trim()).filter(Boolean);
4123
+ platformColIdx = headerCells.findIndex(c => c.includes('涉及端'));
4124
+ sharedCapColIdx = headerCells.findIndex(c => c.includes('共享能力'));
4125
+ dependsOnColIdx = headerCells.findIndex(c => c.includes('依赖任务'));
4126
+ descColIdx = headerCells.findIndex(c => c.includes('说明'));
4127
+ continue;
4128
+ }
4129
+ if (!inTable)
4130
+ continue;
4131
+ if (line.startsWith('## ') && !line.includes('功能映射'))
4132
+ break;
4133
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
4134
+ if (cells.length < 3)
4135
+ continue;
4136
+ if (cells.every(c => /^[-:]+$/.test(c)))
4137
+ continue;
4138
+ if (cells.some(c => c.includes('功能单元')) || cells.some(c => c.includes('涉及端')))
4139
+ continue;
4140
+ const unitName = cells[1];
4141
+ if (!unitName || unitName === '#' || unitName === '功能单元')
4142
+ continue;
4143
+ let platforms = [];
4144
+ if (platformColIdx >= 0 && platformColIdx < cells.length) {
4145
+ const raw = cells[platformColIdx];
4146
+ if (raw && raw !== '无' && raw !== '—' && raw !== '-') {
4147
+ platforms = raw.split(/[,,]/).map(p => p.trim()).filter(p => p && allPlatforms.includes(p));
4148
+ }
4149
+ }
4150
+ if (platforms.length === 0)
4151
+ platforms = [...allPlatforms];
4152
+ let sharedCapability = '无';
4153
+ if (sharedCapColIdx >= 0 && sharedCapColIdx < cells.length) {
4154
+ const raw = cells[sharedCapColIdx];
4155
+ if (raw && raw !== '无' && raw !== '—' && raw !== '-')
4156
+ sharedCapability = raw;
4157
+ }
4158
+ let dependsOn = [];
4159
+ if (dependsOnColIdx >= 0 && dependsOnColIdx < cells.length) {
4160
+ const raw = cells[dependsOnColIdx];
4161
+ if (raw && raw !== '无' && raw !== '—' && raw !== '-') {
4162
+ dependsOn = raw.split(/[,,]/).map(p => p.trim()).filter(Boolean);
4163
+ }
4164
+ }
4165
+ let description = '';
4166
+ if (descColIdx >= 0 && descColIdx < cells.length)
4167
+ description = cells[descColIdx];
4168
+ units.push({ name: unitName, platforms, sharedCapability, dependsOn, description });
4169
+ }
4170
+ return units;
4171
+ }
4172
+ /**
4173
+ * v7.4.2+: 解析 FUNCTION_MAP.md 树形格式
4174
+ * 匹配: ├── 会议室 CRUD .............. room-service ✅ | admin-web ✅
4175
+ * 提取: 模块名="会议室 CRUD", 涉及端=[room-service, admin-web]
4176
+ */
4177
+ function parseFunctionMapTree(content, allPlatforms) {
4178
+ const units = [];
4179
+ const lines = content.split(/\r?\n/);
4180
+ // 树形节点正则:匹配 ├── 或 └── 开头的行
4181
+ const treeNodeRe = /[├└]──\s*/;
4182
+ // 分隔点正则:匹配连续 .... 或 ———
4183
+ const separatorRe = /\s*[.·]{3,}\s*/;
4184
+ for (const line of lines) {
4185
+ if (!treeNodeRe.test(line))
4186
+ continue;
4187
+ // 提取树节点后的文本
4188
+ const afterTree = line.replace(/.*[├└]──\s*/, '').trim();
4189
+ if (!afterTree)
4190
+ continue;
4191
+ // 跳过分类型节点(如 ├── 🏢 会议室管理),这些是父级分类
4192
+ // 父级节点通常没有 ... 分隔符和平台标记
4193
+ if (!separatorRe.test(afterTree) && !afterTree.includes('✅') && !afterTree.includes('❌') && !afterTree.includes('⚠️')) {
4194
+ continue; // 这是分类父节点,跳过
4195
+ }
4196
+ // 分割模块名和平台部分
4197
+ const parts = afterTree.split(separatorRe);
4198
+ if (parts.length < 2)
4199
+ continue;
4200
+ // 模块名:去除 emoji 和空格
4201
+ let unitName = parts[0].replace(/[🏢📅✅💰🔔👥📊⚙️🔑🛡️]/g, '').trim();
4202
+ if (!unitName || unitName.length < 2)
4203
+ continue;
4204
+ // 平台部分:提取所有标准端名
4205
+ const platformPart = parts.slice(1).join(' ');
4206
+ const platforms = [];
4207
+ for (const p of allPlatforms) {
4208
+ if (platformPart.includes(p)) {
4209
+ platforms.push(p);
4210
+ }
4211
+ }
4212
+ // 如果没匹配到标准端名,尝试匹配常见缩写
4213
+ if (platforms.length === 0) {
4214
+ // 回退全端
4215
+ platforms.push(...allPlatforms);
4216
+ }
4217
+ units.push({ name: unitName, platforms, sharedCapability: '无', dependsOn: [], description: '' });
4218
+ }
4219
+ return units;
4220
+ }
4221
+ /**
4222
+ * v7.5.0+: 从 ANALYSIS.md 提取功能单元清单
4223
+ * 支持两种格式:
4224
+ * A) 按端分节: ### X.X 端名(platform-name)— N 个页面 + | F-01 | 功能名 | ...
4225
+ * B) 按优先级分节: ### P0 — 必修 + 表格含「涉及端」列(v7.5.1+ 新增)
4226
+ */
4227
+ function parseAnalysisFunctionalUnits(content, allPlatforms) {
4228
+ const units = [];
4229
+ const lines = content.split(/\r?\n/);
4230
+ let currentPlatforms = [];
4231
+ let platformColIdx = -1; // 「涉及端」列索引,-1 表示未检测到
4232
+ for (let i = 0; i < lines.length; i++) {
4233
+ const line = lines[i];
4234
+ // 检测 ### 标题行,提取当前端名
4235
+ if (line.startsWith('### ')) {
4236
+ const prevPlatforms = [...currentPlatforms]; // 保留上一轮端信息
4237
+ currentPlatforms = [];
4238
+ // 匹配括号内的端名: ### 1.1 后台管理端(admin-web)
4239
+ const parenMatch = line.match(/[((]([a-zA-Z][\w-]*)[))]/);
4240
+ if (parenMatch) {
4241
+ const name = parenMatch[1];
4242
+ const matched = allPlatforms.filter(p => p === name || p.includes(name) || name.includes(p));
4243
+ if (matched.length > 0)
4244
+ currentPlatforms.push(...matched);
4245
+ }
4246
+ // 回退:从标题文本匹配端名
4247
+ if (currentPlatforms.length === 0) {
4248
+ for (const p of allPlatforms) {
4249
+ if (line.includes(p)) {
4250
+ currentPlatforms.push(p);
4251
+ }
4252
+ }
4253
+ }
4254
+ // 后端服务检测
4255
+ if (currentPlatforms.length === 0 && /后端|服务|backend|service/i.test(line)) {
4256
+ currentPlatforms.push(...allPlatforms.filter(p => /service|server|api|backend/i.test(p)));
4257
+ }
4258
+ // v7.5.1: 标题未匹配到端时,保留上一轮的 currentPlatforms(优先级分节场景)
4259
+ if (currentPlatforms.length === 0) {
4260
+ currentPlatforms = prevPlatforms;
4261
+ }
4262
+ // 遇到新章节,重置列索引(下一张表可能结构不同)
4263
+ platformColIdx = -1;
4264
+ continue;
4265
+ }
4266
+ // 解析表格行
4267
+ if (!line.startsWith('|'))
4268
+ continue;
4269
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
4270
+ if (cells.length < 2)
4271
+ continue;
4272
+ // 分隔行跳过
4273
+ if (line.match(/^\|\s*[-:]/))
4274
+ continue;
4275
+ // 检测表头行:查找「涉及端」/「端」列
4276
+ if (platformColIdx === -1) {
4277
+ const idx = cells.findIndex(c => c === '涉及端' || c === '端' || c.includes('涉及端') || c.includes('涉及工程'));
4278
+ if (idx >= 0) {
4279
+ platformColIdx = idx;
4280
+ continue; // 表头行本身不是数据
4281
+ }
4282
+ }
4283
+ // 检测 F-xx 编号行(支持 | F-01 | 或 | # | 格式)
4284
+ const idMatch = cells[0]?.match(/^F-(\d+)$/i);
4285
+ if (!idMatch)
4286
+ continue;
4287
+ const id = `F-${idMatch[1]}`;
4288
+ const name = cells[1]?.replace(/[*_`]/g, '').trim();
4289
+ if (!name || name.length < 2)
4290
+ continue;
4291
+ // 提取描述(核心功能列,通常是第3或第4列)
4292
+ const desc = cells.length >= 4 ? cells[3]?.trim() || '' : '';
4293
+ // v7.5.1+: 优先从「涉及端」列提取平台(格式 B)
4294
+ let rowPlatforms = [];
4295
+ if (platformColIdx >= 0 && cells[platformColIdx]) {
4296
+ const rawPlatform = cells[platformColIdx];
4297
+ // 按逗号、+、顿号、空格分割
4298
+ const parts = rawPlatform.split(/[,+、;\s]+/).map(s => s.trim()).filter(Boolean);
4299
+ for (const part of parts) {
4300
+ // 精确匹配
4301
+ const exact = allPlatforms.find(p => p === part);
4302
+ if (exact) {
4303
+ rowPlatforms.push(exact);
4304
+ continue;
4305
+ }
4306
+ // 模糊匹配(部分名包含)
4307
+ const fuzzy = allPlatforms.filter(p => p.includes(part) || part.includes(p));
4308
+ if (fuzzy.length > 0) {
4309
+ rowPlatforms.push(...fuzzy);
4310
+ continue;
4311
+ }
4312
+ // 中文端名映射(如 "后台管理端" → admin-web)
4313
+ // 尝试从 CONSTITUTION 的「对应需求端」列匹配(此处简化:跳过中文值)
4314
+ }
4315
+ // 去重
4316
+ rowPlatforms = [...new Set(rowPlatforms)];
4317
+ }
4318
+ // 回退优先级:涉及端列 → 章节标题 → 全部端
4319
+ const platforms = rowPlatforms.length > 0
4320
+ ? rowPlatforms
4321
+ : (currentPlatforms.length > 0 ? [...currentPlatforms] : [...allPlatforms]);
4322
+ units.push({ name, platforms, description: desc, id });
4323
+ }
4324
+ // 去重(按名称)
4325
+ const seen = new Set();
4326
+ return units.filter(u => {
4327
+ if (seen.has(u.name))
4328
+ return false;
4329
+ seen.add(u.name);
4330
+ return true;
4331
+ });
4332
+ }
4333
+ /**
4334
+ * 尝试模块驱动拆分:从功能模块创建任务目录结构
4335
+ * 成功返回 true,无功能模块时返回 false(回退到传统流程)
4336
+ */
4337
+ async function tryModuleDrivenSplit(iteration, iterationDir, options) {
4338
+ const reqDir = (0, path_1.join)(iterationDir, '010-requirements');
4339
+ const allPlatforms = await detectPlatforms(iterationDir);
4340
+ // 收集功能模块(含涉及端信息)
4341
+ const modules = [];
4342
+ // v6.70.0+: 1. 优先从 overview/FUNCTION_MAP.md 读取跨端功能映射表
4343
+ const functionMapPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'FUNCTION_MAP.md');
4344
+ let functionMapParsed = false;
4345
+ if (await (0, fs_extra_1.pathExists)(functionMapPath)) {
4346
+ try {
4347
+ const content = await (0, fs_extra_1.readFile)(functionMapPath, 'utf-8');
4348
+ const parsed = parseFunctionMap(content, allPlatforms);
4349
+ if (parsed.length > 0) {
4350
+ for (const u of parsed) {
4351
+ modules.push({
4352
+ name: u.name,
4353
+ slug: slugify(u.name),
4354
+ type: 'feature',
4355
+ sourceFile: 'overview/FUNCTION_MAP.md',
4356
+ platforms: u.platforms,
4357
+ sharedCapability: u.sharedCapability,
4358
+ dependsOn: u.dependsOn,
4359
+ description: u.description,
4360
+ });
4361
+ }
4362
+ functionMapParsed = true;
4363
+ logger_1.logger.info(` 📋 从 overview/FUNCTION_MAP.md 读取到 ${parsed.length} 个功能单元(严格按映射表拆分)`);
4364
+ }
4365
+ }
4366
+ catch (e) {
4367
+ logger_1.logger.debug('解析 FUNCTION_MAP.md 失败:', e);
4368
+ }
4369
+ }
4370
+ // 2. 回退:从 overview/REQUIREMENT.md 读取功能模块清单(含涉及端)
4371
+ if (!functionMapParsed) {
4372
+ const globalReqPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'REQUIREMENT.md');
4373
+ let modulePlatformsParsed = false;
4374
+ if (await (0, fs_extra_1.pathExists)(globalReqPath)) {
4375
+ try {
4376
+ const content = await (0, fs_extra_1.readFile)(globalReqPath, 'utf-8');
4377
+ const parsed = parseModulePlatforms(content, allPlatforms);
4378
+ if (parsed.length > 0) {
4379
+ for (const m of parsed) {
4380
+ modules.push({
4381
+ name: m.name,
4382
+ slug: slugify(m.name),
4383
+ type: 'feature',
4384
+ sourceFile: 'overview/REQUIREMENT.md',
4385
+ platforms: m.platforms,
4386
+ });
4387
+ }
4388
+ modulePlatformsParsed = true;
4389
+ logger_1.logger.info(` 📋 从 overview/REQUIREMENT.md 读取到 ${parsed.length} 个功能模块(含涉及端)`);
4390
+ }
4391
+ }
4392
+ catch { }
4393
+ }
4394
+ // 2.5 v7.5.0+: 回退:从 ANALYSIS.md 提取功能单元清单(按端分节的 F-xx 表格)
4395
+ if (!modulePlatformsParsed) {
4396
+ const analysisPath = (0, path_1.join)(iterationDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'ANALYSIS.md');
4397
+ if (await (0, fs_extra_1.pathExists)(analysisPath)) {
4398
+ try {
4399
+ const content = await (0, fs_extra_1.readFile)(analysisPath, 'utf-8');
4400
+ const parsed = parseAnalysisFunctionalUnits(content, allPlatforms);
4401
+ if (parsed.length > 0) {
4402
+ for (const u of parsed) {
4403
+ modules.push({
4404
+ name: u.name,
4405
+ slug: slugify(u.name),
4406
+ type: 'feature',
4407
+ sourceFile: 'ANALYSIS.md',
4408
+ platforms: u.platforms,
4409
+ description: u.description,
4410
+ });
4411
+ }
4412
+ modulePlatformsParsed = true;
4413
+ logger_1.logger.info(` 📋 从 ANALYSIS.md 提取到 ${parsed.length} 个功能单元(F-xx 编号表)`);
4414
+ }
4415
+ }
4416
+ catch { }
4417
+ }
4418
+ }
4419
+ // 3. 回退:读取 features/*/SOURCE.md(v8.3.226+,兼容旧 README.md)
4420
+ // v8.3.249+: 优先读 _matrix.md 端声明(unitize 切分时写入),替代全端兜底——
4421
+ // 此前全部配全端,单端功能(如仅 booking-service)也会拆出 4 个子任务
4422
+ if (!modulePlatformsParsed) {
4423
+ const featuresDir = (0, path_1.join)(reqDir, 'features');
4424
+ if (await (0, fs_extra_1.pathExists)(featuresDir)) {
4425
+ try {
4426
+ const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
4427
+ for (const entry of entries) {
4428
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
4429
+ const readmePath = await (0, feature_source_1.resolveUnitSourcePath)((0, path_1.join)(featuresDir, entry.name));
4430
+ if (await (0, fs_extra_1.pathExists)(readmePath)) {
4431
+ let unitPlatforms = [...allPlatforms];
4432
+ const matrixPath = (0, path_1.join)(featuresDir, entry.name, '_matrix.md');
4433
+ if (await (0, fs_extra_1.pathExists)(matrixPath)) {
4434
+ const mc = await (0, fs_extra_1.readFile)(matrixPath, 'utf-8');
4435
+ const m = mc.match(/^platforms:\s*\n((?:\s+-\s+\S+\s*\n?)+)/m);
4436
+ if (m) {
4437
+ const declared = m[1].split('\n')
4438
+ .map(l => l.replace(/^\s+-\s+/, '').trim()).filter(Boolean);
4439
+ if (declared.length > 0) {
4440
+ unitPlatforms = declared.filter(p => allPlatforms.includes(p));
4441
+ }
4442
+ }
4443
+ }
4444
+ modules.push({
4445
+ name: entry.name, slug: slugify(entry.name), type: 'feature',
4446
+ sourceFile: `features/${entry.name}/${(0, path_1.basename)(readmePath)}`,
4447
+ platforms: unitPlatforms,
4448
+ });
4449
+ }
4450
+ }
4451
+ }
4452
+ }
4453
+ catch { }
4454
+ }
4455
+ }
4456
+ }
4457
+ // 3. 读取类型文档(bugs/refactors/research)
4458
+ for (const typeDir of ['bugs', 'refactors', 'research']) {
4459
+ const typeDirPath = (0, path_1.join)(reqDir, typeDir);
4460
+ if (!(await (0, fs_extra_1.pathExists)(typeDirPath)))
4461
+ continue;
4462
+ try {
4463
+ const entries = await (0, fs_extra_1.readdir)(typeDirPath, { withFileTypes: true });
4464
+ for (const entry of entries) {
4465
+ if (entry.isFile() && entry.name.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(entry.name)) {
4466
+ const slug = slugify(entry.name.replace('.md', ''));
4467
+ const taskType = typeDir === 'bugs' ? 'bugfix' : typeDir;
4468
+ modules.push({
4469
+ name: entry.name.replace('.md', ''), slug, type: taskType,
4470
+ sourceFile: `${typeDir}/${entry.name}`,
4471
+ platforms: [...allPlatforms],
4472
+ });
4473
+ }
4474
+ }
4475
+ }
4476
+ catch { }
4477
+ }
4478
+ if (modules.length === 0) {
4479
+ return false; // 无功能模块,回退到传统流程
4480
+ }
4481
+ // v8.3.249+: --modules 过滤——模块驱动拆分此前完全忽略该选项,
4482
+ // 指定单个单元时仍全量拆分(FUNCTION_MAP 全部单元照拆)
4483
+ if (options.modules) {
4484
+ const moduleFilters = options.modules.split(',').map(m => m.trim()).filter(Boolean);
4485
+ const beforeCount = modules.length;
4486
+ const filtered = modules.filter(mod => moduleFilters.some(f => mod.name.includes(f) || mod.slug.includes(f)));
4487
+ if (filtered.length === 0) {
4488
+ logger_1.logger.warn(`没有匹配 "${options.modules}" 的功能模块(可用: ${modules.slice(0, 5).map(m => m.name).join(', ')}${modules.length > 5 ? '...' : ''}),回退传统流程`);
4489
+ return false;
4490
+ }
4491
+ modules.length = 0;
4492
+ modules.push(...filtered);
4493
+ logger_1.logger.info(` 🎯 模块过滤: ${beforeCount} → ${filtered.length}(只拆分: ${moduleFilters.join(', ')})`);
4494
+ }
4495
+ // 检测已有任务
4496
+ const existingTasks = await detectExistingTasks(iterationDir);
4497
+ if (existingTasks.length > 0 && !options.force && !options.prune) {
4498
+ logger_1.logger.warn(` ⚠️ 已有 ${existingTasks.length} 个任务: ${existingTasks.slice(0, 5).join(', ')}...`);
4499
+ logger_1.logger.info(' 使用 --force 强制覆盖 或 --prune 智能清理不匹配任务');
4500
+ return true; // 已处理,不继续传统流程
4501
+ }
4502
+ // --force 清理所有旧任务
4503
+ if (options.force) {
4504
+ const tasksRoot = (0, path_1.join)(iterationDir, '030-tasks');
4505
+ if (await (0, fs_extra_1.pathExists)(tasksRoot)) {
4506
+ const entries = await (0, fs_extra_1.readdir)(tasksRoot, { withFileTypes: true });
4507
+ for (const entry of entries) {
4508
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
4509
+ await (0, fs_extra_1.remove)((0, path_1.join)(tasksRoot, entry.name));
4510
+ }
4511
+ }
4512
+ logger_1.logger.info(` 🗑️ 已清理旧任务目录`);
4513
+ }
4514
+ }
4515
+ // v6.72.0+: --prune 智能清理不匹配的旧任务
4516
+ if (options.prune && !options.force && existingTasks.length > 0) {
4517
+ const tasksRoot = (0, path_1.join)(iterationDir, '030-tasks');
4518
+ const validFeatureNames = new Set(modules.map(m => m.name));
4519
+ const archiveDir = (0, path_1.join)(tasksRoot, '.archive', new Date().toISOString().split('T')[0]);
4520
+ let prunedCount = 0;
4521
+ for (const taskName of existingTasks) {
4522
+ const taskDir = (0, path_1.join)(tasksRoot, taskName);
4523
+ const featurePath = (0, path_1.join)(taskDir, '.meta', 'feature');
4524
+ let featureName = '';
4525
+ if (await (0, fs_extra_1.pathExists)(featurePath)) {
4526
+ featureName = (await (0, fs_extra_1.readFile)(featurePath, 'utf-8')).trim();
4527
+ }
4528
+ // 如果功能单元不在当前 FUNCTION_MAP.md 中,移动到 archive
4529
+ if (featureName && !validFeatureNames.has(featureName)) {
4530
+ await (0, fs_extra_1.ensureDir)(archiveDir);
4531
+ const { rename } = await Promise.resolve().then(() => __importStar(require('fs-extra')));
4532
+ await rename(taskDir, (0, path_1.join)(archiveDir, taskName));
4533
+ prunedCount++;
4534
+ logger_1.logger.info(` 🗑️ 已归档旧任务: ${taskName} (功能单元: ${featureName})`);
4535
+ }
4536
+ }
4537
+ if (prunedCount > 0) {
4538
+ logger_1.logger.info(` 📦 共归档 ${prunedCount} 个不匹配任务到 ${(0, path_1.relative)(tasksRoot, archiveDir)}`);
4539
+ }
4540
+ else {
4541
+ logger_1.logger.info(` ✅ 无需要清理的旧任务(所有任务功能单元均匹配)`);
4542
+ }
4543
+ }
4544
+ logger_1.logger.info(`\n📦 模块驱动拆分: ${modules.length} 个功能模块`);
4545
+ logger_1.logger.info(` 全局端列表: ${allPlatforms.join(', ')}`);
4546
+ // 逐模块创建任务目录(按模块各自的涉及端)
4547
+ const createdSections = [];
4548
+ for (const mod of modules) {
4549
+ const { id: taskId } = await (0, global_counters_1.nextTaskId)(mod.name, mod.slug);
4550
+ const modPlatforms = mod.platforms.length > 0 ? mod.platforms : [...allPlatforms];
4551
+ // v8.3.249+: section.content 从 sourceFile(SOURCE.md 等)读取——此前硬编码空串,
4552
+ // 导致 REQ.md 四级回退全部落空(aiReq/analyzeReq/短 section.content 均不满足),
4553
+ // 最终写出「待补充」骨架,execute 阶段拿不到有价值的需求文档
4554
+ let sectionContent = '';
4555
+ if (mod.sourceFile) {
4556
+ try {
4557
+ const srcPath = (0, path_1.join)(iterationDir, '010-requirements', mod.sourceFile);
4558
+ if (await (0, fs_extra_1.pathExists)(srcPath)) {
4559
+ sectionContent = await (0, fs_extra_1.readFile)(srcPath, 'utf-8');
4560
+ }
4561
+ }
4562
+ catch { /* 读取失败保持空 */ }
4563
+ }
4564
+ const section = {
4565
+ name: mod.name,
4566
+ content: sectionContent,
4567
+ level: 2,
4568
+ };
4569
+ section._topic = mod.slug;
4570
+ section._taskType = mod.type;
4571
+ section._sourceFile = mod.sourceFile;
4572
+ section._scopePlatforms = modPlatforms;
4573
+ section._complexity = {
4574
+ estimatedHours: 8,
4575
+ priority: 'medium',
4576
+ };
4577
+ section._owner = '未分配';
4578
+ section._taskId = taskId;
4579
+ section.functionalUnit = mod.name;
4580
+ // v6.70.0+: 传递 FUNCTION_MAP.md 中的扩展信息
4581
+ if (mod.sharedCapability)
4582
+ section._sharedCapability = mod.sharedCapability;
4583
+ if (mod.dependsOn)
4584
+ section._dependsOn = mod.dependsOn;
4585
+ if (mod.description)
4586
+ section._description = mod.description;
4587
+ await createTaskFromSection(iterationDir, taskId, section, modPlatforms, mod.type, []);
4588
+ createdSections.push(section);
4589
+ logger_1.logger.info(` ✅ 创建: ${taskId} [${mod.type}] — ${mod.name} (${modPlatforms.length} 个端: ${modPlatforms.join(', ')})`);
4590
+ }
4591
+ // 生成任务总览
4592
+ if (createdSections.length > 0) {
4593
+ await generateImpactGraph(iterationDir, createdSections, allPlatforms);
4594
+ logger_1.logger.info(`\n 📊 创建了 ${createdSections.length} 个任务(每端一个子任务)`);
4595
+ // 生成内容填充提示
4596
+ const fillPrompt = await buildContentFillingPrompt(iteration, iterationDir, createdSections, allPlatforms);
4597
+ const promptsDir = (0, path_1.join)('.speccore', 'prompts');
4598
+ await (0, fs_extra_1.ensureDir)(promptsDir);
4599
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(promptsDir, `split-content-${iteration}.md`), fillPrompt);
4600
+ logger_1.logger.info(` 📝 内容填充提示 → .speccore/prompts/split-content-${iteration}.md`);
4601
+ }
4602
+ logger_1.logger.success(`✅ 模块驱动拆分完成: ${createdSections.length} 个任务`);
4603
+ // 自动刷新知识图谱
4604
+ try {
4605
+ const { refreshKnowledgeGraph } = await Promise.resolve().then(() => __importStar(require('../../core/knowledge-graph')));
4606
+ await refreshKnowledgeGraph(process.cwd(), iteration);
4607
+ logger_1.logger.info('🧠 知识图谱已刷新');
4608
+ }
4609
+ catch { }
4610
+ return true;
4611
+ }
4612
+ // v8.3.223+: 本地 assembleUnitContext 副本已收敛到 core/unit-context-assembler.ts 的
4613
+ // extractOverviewContext(此前两套实现并存、已漂移),本文件统一引用该实现
4614
+ /**
4615
+ * 生成内容填充 Prompt — AI 为预创建的任务填充 REQ.md/TECH.md
4616
+ * v7.4.2+: 每个功能单元注入精准上下文,而非让 AI 自己从全量文档中查找
4617
+ */
4618
+ async function buildContentFillingPrompt(iteration, iterationDir, sections, allPlatforms) {
4619
+ let p = `# 任务内容填充(模块驱动拆分)\n\n`;
4620
+ p += `> 迭代: ${iteration} | 任务数: ${sections.length} | 端: ${allPlatforms.join(', ')}\n\n`;
4621
+ p += `## 说明\n\n`;
4622
+ p += `CLI 已按功能单元×端创建了任务目录结构,并为每个功能单元从分析文档中提取了相关上下文。\n`;
4623
+ p += `你的任务是为每个子任务填充 REQ.md 和 TECH.md。\n\n`;
4624
+ p += `## 全局上下文\n\n`;
4625
+ p += `1. Read .speccore/CONSTITUTION.md — 项目配置\n`;
4626
+ p += `2. Read 020-specs/overview/ANALYSIS.md — 迭代综合分析报告\n\n`;
4627
+ p += `## 任务清单(含精准上下文)\n\n`;
4628
+ for (const sec of sections) {
4629
+ const taskId = sec._taskId || sec.name;
4630
+ const sourceFile = sec._sourceFile || '';
4631
+ const featureName = sec.functionalUnit || sec.name;
4632
+ const modPlatforms = sec._scopePlatforms || allPlatforms;
4633
+ p += `### ${taskId} — ${sec.name}\n`;
4634
+ p += `- 功能单元: ${featureName}\n`;
4635
+ p += `- 涉及端: ${modPlatforms.join(', ')}\n`;
4636
+ if (sourceFile)
4637
+ p += `- 来源: 010-requirements/${sourceFile}\n`;
4638
+ p += `- 子任务目录: ${modPlatforms.map(pl => `${pl}/`).join(', ')}\n`;
4639
+ p += `- 需要填充:\n`;
4640
+ for (const platform of modPlatforms) {
4641
+ p += ` - ${platform}/*/REQ.md — 子任务需求规格\n`;
4642
+ p += ` - ${platform}/*/TECH.md — 子任务技术方案\n`;
4643
+ }
4644
+ // v7.4.2+: 注入该功能单元的精准上下文
4645
+ const unitCtx = await (0, unit_context_assembler_1.extractOverviewContext)(iterationDir, featureName, modPlatforms);
4646
+ p += `\n#### 📎 功能单元「${featureName}」相关上下文\n\n${unitCtx}\n\n`;
4647
+ p += `\n`;
4648
+ }
4649
+ p += `## 填充规则\n\n`;
4650
+ p += `1. 先 Read 子任务目录下的 TASK.md(已有基本信息)和 .meta/feature(功能单元名)\n`;
4651
+ p += `2. REQ.md 和 TECH.md 已由 CLI 预创建(含骨架标记 + 质量标准),请用 Write 工具直接覆盖为专业内容\n`;
4652
+ p += `3. REQ.md: 基于上方提供的「功能单元相关上下文」,撰写本子任务的需求规格\n`;
4653
+ p += `4. TECH.md: 基于上下文中的接口定义和数据模型,细化本子任务的技术方案\n`;
4654
+ p += `5. 同一功能模块的各端子任务要保持 API 契约一致(前后端接口签名必须匹配)\n`;
4655
+ p += `6. 每个文件必须有实质性专业内容(最低 500 字符 / 30 行),禁止占位符残留\n`;
4656
+ p += `7. REQ.md 必须 ≥ 50 行,TECH.md 必须 ≥ 80 行\n`;
4657
+ p += `8. 自检:写入完成后检查行数和内容完整性,不达标需补全\n\n`;
4658
+ // v8.1.0+: 注入子任务文档质量标准
4659
+ p += `## 📋 子任务文档质量标准\n\n`;
4660
+ p += `### REQ.md 必含内容\n`;
4661
+ p += (0, spec_skeleton_1.buildQualityRubRIC)('REQ.md');
4662
+ p += `\n### TECH.md 必含内容\n`;
4663
+ p += (0, spec_skeleton_1.buildQualityRubRIC)('TECH.md');
4664
+ p += `\n### 前后端契约一致性(关键)\n\n`;
4665
+ p += `- 后端 TECH.md 的接口签名(路径/方法/参数/响应)必须与前端 TECH.md 的 API 调用清单完全匹配\n`;
4666
+ p += `- 数据模型字段名、类型、必填性必须前后端一致\n`;
4667
+ p += `- 状态枚举值必须前后端对齐\n\n`;
4668
+ // v8.3.171+: 简化路径约束
4669
+ p += `## ⚠️ 约束\n\n`;
4670
+ p += `- 文件已预创建,只覆盖已有文件,不要创建新文件或新目录\n`;
4671
+ p += `- 不要修改 .meta/ 下的文件\n`;
4672
+ p += `- 不要修改 TASK.md(已由 CLI 生成)\n`;
4673
+ p += '\n' + (0, questions_1.buildAutoModeInstruction)('split', iteration) + '\n';
4674
+ return p;
4675
+ }
4676
+ //# sourceMappingURL=taskify.js.map