speccore 8.3.2 → 8.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../../src/commands/iteration/split.ts"],"names":[],"mappings":"AAsIA,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAuBD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+xBzF"}
1
+ {"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../../src/commands/iteration/split.ts"],"names":[],"mappings":"AAsIA,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAoCD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+0BzF"}
@@ -162,16 +162,28 @@ async function detectPlatforms(iterationDir, specified) {
162
162
  const platforms = await (0, spec_paths_1.parsePlatformList)();
163
163
  if (platforms.length > 0)
164
164
  return platforms;
165
- // 2. 回退:扫描 020-specs/ 子目录(排除 global/ 等非端目录)
165
+ // 2. 回退:扫描 020-specs/ 子目录(排除 global/ 等非端目录 + 常见 AI 简写垃圾目录)
166
166
  const specsDir = (0, path_1.join)(iterationDir, '020-specs');
167
167
  if (await (0, fs_extra_1.pathExists)(specsDir)) {
168
168
  const entries = await (0, fs_extra_1.readdir)(specsDir, { withFileTypes: true });
169
- const knownNonPlatformDirs = new Set(['sources', 'assets', 'prototypes', 'converted', 'features', 'bugs', 'refactors', 'research', 'staging', 'platforms', 'snapshots', spec_paths_1.GLOBAL_SPECS_DIR]);
170
- const platforms = entries
169
+ // v8.3.4+: 增加常见 AI 简写过滤,避免 api/web 等错误目录被当成端名
170
+ const knownNonPlatformDirs = new Set([
171
+ 'sources', 'assets', 'prototypes', 'converted', 'features', 'bugs', 'refactors', 'research',
172
+ 'staging', 'platforms', 'snapshots', 'overview', 'global', spec_paths_1.GLOBAL_SPECS_DIR,
173
+ // 常见 AI 简写垃圾目录(必须从标准端名映射,不能直接用)
174
+ 'api', 'web', 'backend', 'frontend', 'server', 'mobile', 'admin', 'h5', 'pc', 'app',
175
+ ]);
176
+ const rawPlatforms = entries
171
177
  .filter((e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.') && !knownNonPlatformDirs.has(e.name))
172
178
  .map((e) => e.name);
173
- if (platforms.length > 0)
174
- return platforms;
179
+ // 二次过滤:用 normalizeScopePlatforms 排除任何残留的非标准端名
180
+ if (rawPlatforms.length > 0) {
181
+ const standardPlatforms = await (0, spec_paths_1.parsePlatformList)();
182
+ if (standardPlatforms.length > 0) {
183
+ return normalizeScopePlatforms(rawPlatforms, standardPlatforms);
184
+ }
185
+ return rawPlatforms;
186
+ }
175
187
  }
176
188
  return ['web']; // 默认
177
189
  }
@@ -278,15 +290,15 @@ async function iterationSplitCommand(options) {
278
290
  return;
279
291
  }
280
292
  // 非 Pipeline 模式的 Response 处理
281
- const iterDir = (0, path_1.join)('Iteration-' + iter, '030-tasks');
293
+ // v8.3.4+ 修复:使用 getIterationDir 解析正确路径,避免 Iteration- 前缀重复
294
+ const iterDirFull = await (0, context_1.getIterationDir)(iter);
295
+ const iterDir = (0, path_1.join)(iterDirFull, '030-tasks');
282
296
  await (0, fs_extra_1.ensureDir)(iterDir);
283
297
  const backups = [];
284
298
  // 尝试解析 AI 返回的 JSON Task 列表
285
299
  try {
286
300
  const tasks = JSON.parse(options.response);
287
301
  if (Array.isArray(tasks)) {
288
- // 获取迭代根目录(createTaskFromSection 需要迭代根路径)
289
- const iterDirFull = await (0, context_1.getIterationDir)(iter);
290
302
  const allPlatforms = await detectPlatforms(iterDirFull);
291
303
  const sections = [];
292
304
  for (let i = 0; i < tasks.length; i++) {
@@ -351,6 +363,57 @@ async function iterationSplitCommand(options) {
351
363
  section._scopePlatforms = taskScopePlatforms;
352
364
  sections.push(section);
353
365
  }
366
+ // v8.3.4+: 校验所有任务的 scope 是否只含标准端名
367
+ const invalidScopes = [];
368
+ for (const sec of sections) {
369
+ const scopes = sec._scopePlatforms || [];
370
+ for (const s of scopes) {
371
+ if (!allPlatforms.includes(s))
372
+ invalidScopes.push(`任务 "${sec.name}" 的 scope "${s}"`);
373
+ }
374
+ }
375
+ if (invalidScopes.length > 0) {
376
+ logger_1.logger.error(`❌ 发现 ${invalidScopes.length} 个非标准端名,拆分被拒绝:`);
377
+ for (const msg of invalidScopes)
378
+ logger_1.logger.error(` ${msg}`);
379
+ logger_1.logger.error(` 标准端名: ${allPlatforms.join(', ')}`);
380
+ logger_1.logger.info(` 💡 请告诉 AI:scope 必须使用 CONSTITUTION.md 标准端名,禁止 api/web/backend 等简写`);
381
+ return;
382
+ }
383
+ // v8.3.4+: 功能点覆盖校验 — 对比 FUNCTION_MAP.md 检查是否有遗漏
384
+ const funcMapPath = (0, path_1.join)(iterDirFull, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR, 'FUNCTION_MAP.md');
385
+ if (await (0, fs_extra_1.pathExists)(funcMapPath)) {
386
+ try {
387
+ const fmContent = await (0, fs_extra_1.readFile)(funcMapPath, 'utf-8');
388
+ const funcUnits = parseFunctionMap(fmContent, allPlatforms);
389
+ if (funcUnits.length > 0) {
390
+ const aiUnits = new Set();
391
+ for (const t of tasks) {
392
+ const unit = t.functionalUnit || t.name;
393
+ if (unit)
394
+ aiUnits.add(unit.trim());
395
+ }
396
+ const missingUnits = [];
397
+ for (const u of funcUnits) {
398
+ // 模糊匹配:如果 AI 任务中没有功能单元名包含或被包含于 FUNCTION_MAP 中的名称
399
+ const matched = [...aiUnits].some(aiu => aiu.toLowerCase().includes(u.name.toLowerCase()) ||
400
+ u.name.toLowerCase().includes(aiu.toLowerCase()));
401
+ if (!matched)
402
+ missingUnits.push(u.name);
403
+ }
404
+ if (missingUnits.length > 0) {
405
+ logger_1.logger.warn(`\n ⚠️ 功能点覆盖警告:${missingUnits.length} 个功能单元可能被遗漏`);
406
+ for (const mu of missingUnits.slice(0, 5))
407
+ logger_1.logger.warn(` 📌 ${mu}`);
408
+ if (missingUnits.length > 5)
409
+ logger_1.logger.warn(` ... 还有 ${missingUnits.length - 5} 个`);
410
+ logger_1.logger.warn(` 💡 建议检查:这些功能点是否已合并到其他任务中?`);
411
+ logger_1.logger.warn(` 💡 如确需补充,重新执行 split 并告诉 AI:"补充以下功能点: ${missingUnits.slice(0, 3).join(', ')}"`);
412
+ }
413
+ }
414
+ }
415
+ catch { }
416
+ }
354
417
  // 检测已有任务 + 冲突处理
355
418
  const existingTasks = await detectExistingTasks(iterDirFull);
356
419
  if (existingTasks.length > 0 && !options.force) {
@@ -2261,7 +2324,8 @@ async function loadSpecContents(iterationDir) {
2261
2324
  }
2262
2325
  // 2. 读取各端子目录文档(如 admin/TECH.md、h5/TECH.md 等)
2263
2326
  const entries = await (0, fs_extra_1.readdir)(specDir, { withFileTypes: true });
2264
- const knownNonPlatformDirs = new Set(['sources', 'assets', 'prototypes', 'converted', 'features', 'bugs', 'refactors', 'research', 'staging', 'platforms', 'snapshots', spec_paths_1.GLOBAL_SPECS_DIR]);
2327
+ // v8.3.4+: 增加 overview 过滤,防止 analyze 全局文档目录被误当成端目录
2328
+ const knownNonPlatformDirs = new Set(['sources', 'assets', 'prototypes', 'converted', 'features', 'bugs', 'refactors', 'research', 'staging', 'platforms', 'snapshots', 'overview', 'global', spec_paths_1.GLOBAL_SPECS_DIR]);
2265
2329
  for (const e of entries) {
2266
2330
  if (e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.') && !knownNonPlatformDirs.has(e.name)) {
2267
2331
  const platform = e.name;
@@ -2710,6 +2774,12 @@ async function buildSplitPrompt(iteration, constitutionContent, reqContent, spec
2710
2774
  p += `> 同一模块/领域的任务填相同的值,用于粒度校验和任务分组\n`;
2711
2775
  p += `> **topic** 必须是英文短横线格式(如 \`user-authentication\`、\`product-crud\`),用于生成任务目录名 Task-NNN-{topic}\n`;
2712
2776
  p += `> **sourceFile** 必须填写:该任务对应的 020-specs 源文档路径(如 \`bugs/login-timeout.md\`、\`features/user-auth.md\`、\`refactors/db-pool.md\`),用于在 CONTEXT.md 中生成来源追溯\n`;
2777
+ p += `> **scope 强制约束(违反则拆分无效)**:\n`;
2778
+ p += `> - scope 数组中的每个值必须是上面「项目端列表」中的标准端名之一\n`;
2779
+ p += `> - ⛔ 禁止出现 api、web、backend、frontend、server、admin、h5、mobile、pc、app 等非标准名称\n`;
2780
+ p += `> - ⛔ 禁止出现中文端名(如"后端"、"前端"、"移动端")\n`;
2781
+ p += `> - 每个任务按 scope 中的端创建子任务目录,目录名 = 标准端名\n`;
2782
+ p += `> - 如果 scope 含非标准端名,整个 JSON 视为不合格,必须重新生成\n`;
2713
2783
  p += `> **reqContent 质量要求(必填,禁止模板化)**:\n`;
2714
2784
  p += `> - 必须是**具体的、可执行的需求描述**,不是"待补充"或"参考全局文档"\n`;
2715
2785
  p += `> - 包含:业务规则(含边界条件)、数据模型(字段/类型/约束)、接口清单(方法/路径/参数/响应)\n`;
@@ -2721,6 +2791,13 @@ async function buildSplitPrompt(iteration, constitutionContent, reqContent, spec
2721
2791
  p += `> - 从 020-specs/overview/TECH.md 和对应端 TECH.md 中提取本任务相关的技术细节\n`;
2722
2792
  p += `> - 直接写入 00-specs/TECH.md,执行时 AI 据此直接开发\n`;
2723
2793
  p += `> **质量红线**:如果 reqContent/techContent 只有标题和占位符(如 "<!-- AI-FILL -->"),视为不合格,必须重新生成\n\n`;
2794
+ // v8.3.4+: 强制约束专节(类似 analyze Phase 2)
2795
+ p += `### ⛔ 强制约束(违反则拆分无效)\n\n`;
2796
+ p += `- **禁止跳过任何功能模块** — 即使某个模块信息不足,也必须基于已有信息拆分并填充 reqContent/techContent,标注「基于现有信息推断」而非留空\n`;
2797
+ p += `- **禁止输出空内容** — reqContent、techContent、devGuideContent 每个字段长度必须 ≥200 字符,不允许写「待补充」「TODO」「参考全局文档」\n`;
2798
+ p += `- **禁止省略字段** — JSON 中必须包含 reqContent、techContent、devGuideContent 三个字段,缺一不可\n`;
2799
+ p += `- **内容必须从 analyze 产物提取** — reqContent 从 REQUIREMENT.md 提取,techContent 从 TECH.md 提取,devGuideContent 从 DEV_GUIDE.md 提取\n`;
2800
+ p += `- **如果 token 不足**:优先保证 reqContent 完整,其次是 techContent,最后是 devGuideContent\n\n`;
2724
2801
  p += `### ⚠️ 拆分规则(重要)\n\n`;
2725
2802
  p += `- **按功能单元拆分,不按技术层拆分**:每个任务必须对应一个业务功能(如「预订管理」「签到」「审批」),不要按技术层分组(如「后端改进」「前端修复」)\n`;
2726
2803
  p += `- ❌ 错误示例:Task-129-backend-improve、Task-130-admin-fix(按技术层分组)\n`;
@@ -2754,7 +2831,7 @@ async function buildSplitPrompt(iteration, constitutionContent, reqContent, spec
2754
2831
  - 你的判断
2755
2832
  - 建议后续动作\`
2756
2833
  `;
2757
- p += `3. **遇阻断就跳过**如果某个功能模块信息不足无法拆分,跳过它并在疑问清单中记录\n`;
2834
+ p += `3. **禁止跳过任何模块**即使某个功能模块信息不足,也必须基于已有信息拆分并填充内容,宁可标注「基于现有信息推断:需要补充 xxx」也不许留空或省略\n`;
2758
2835
  p += `4. **输出 JSON** — 直接输出拆分结果的 JSON 数组,不要输出其他内容\n`;
2759
2836
  // 持久指令(用户调整时 AI 可回读此文件)
2760
2837
  p += `\n## 🔄 调整指令(持久有效)\n\n`;