speccore 8.3.276 → 8.3.278

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.
@@ -640,8 +640,10 @@ async function iterationTaskifyCommand(options) {
640
640
  }
641
641
  const extracted = extractJsonArray(options.response);
642
642
  // 尝试解析 AI 返回的 JSON Task 列表
643
+ let jsonParsed = false;
643
644
  try {
644
645
  const tasks = JSON.parse(extracted.json);
646
+ jsonParsed = true;
645
647
  if (Array.isArray(tasks)) {
646
648
  const allPlatforms = await detectPlatforms(iterDirFull);
647
649
  const sections = [];
@@ -716,8 +718,12 @@ async function iterationTaskifyCommand(options) {
716
718
  section.functionalUnit = task.functionalUnit;
717
719
  if (task.reason)
718
720
  section._reason = task.reason;
719
- if (taskScopePlatforms.length > 0)
721
+ if (taskScopePlatforms.length > 0) {
720
722
  section._scopePlatforms = taskScopePlatforms;
723
+ // v8.3.277: 标记 AI 显式声明了 scope(用于区分「推断出的端」与「AI 明确意图」,
724
+ // 显式 scope 不自动补后端,推断 scope 保留自动补后端)
725
+ section._explicitScope = true;
726
+ }
721
727
  sections.push(section);
722
728
  }
723
729
  // v8.3.223+: 过滤骨架质量标准标题泄漏为功能单元名(spec-skeleton 注入的引导标题
@@ -980,8 +986,25 @@ async function iterationTaskifyCommand(options) {
980
986
  }
981
987
  // 逐任务交互确认
982
988
  const createdSections = [];
989
+ const moduleFilter = (unit) => {
990
+ if (scopeModules.length === 0)
991
+ return true;
992
+ const u = (unit || '').toLowerCase();
993
+ return scopeModules.some((m) => {
994
+ const mf = m.toLowerCase();
995
+ return u === mf || u.includes(mf) || mf.includes(u);
996
+ });
997
+ };
998
+ let filteredCount = 0;
983
999
  for (let i = 0; i < sections.length; i++) {
984
1000
  const sec = sections[i];
1001
+ // v8.3.277: --modules 指定范围重建时,只创建匹配模块的任务,其余跳过(避免重复)
1002
+ const secUnit = sec.functionalUnit || sec.name || '';
1003
+ if (!moduleFilter(secUnit)) {
1004
+ filteredCount++;
1005
+ logger_1.logger.info(` ⏭️ 跳过「${sec.name}」(不属于 --modules 指定范围,已保留旧任务)`);
1006
+ continue;
1007
+ }
985
1008
  const complexity = sec._complexity || {};
986
1009
  const taskType = sec._taskType || 'feature';
987
1010
  const deps = (tasks[i].dependencies || []);
@@ -1032,7 +1055,7 @@ async function iterationTaskifyCommand(options) {
1032
1055
  // 输出下一步操作标记:引导宿主 AI 填充任务级 00-specs/ 文档(analyze --task 已删除)
1033
1056
  outputAnalyzeTaskHints(iter, createdSections);
1034
1057
  }
1035
- logger_1.logger.success(`✅ 创建了 ${createdSections.length}/${sections.length} 个任务(${sections.length - createdSections.length} 个跳过)`);
1058
+ logger_1.logger.success(`✅ 创建了 ${createdSections.length}/${sections.length - filteredCount} 个任务(${sections.length - createdSections.length - filteredCount} 个跳过)`);
1036
1059
  // v8.3.160+: 检测是否有待拆分的功能模块
1037
1060
  if (extracted.pending) {
1038
1061
  logger_1.logger.info('');
@@ -1054,14 +1077,25 @@ async function iterationTaskifyCommand(options) {
1054
1077
  await (0, fs_extra_1.writeFile)(reqPath, options.response);
1055
1078
  }
1056
1079
  }
1057
- catch {
1058
- const reqPath = (0, path_1.join)(iterDir, 'REQUIREMENT.md');
1059
- const bk = await (0, task_utils_1.backupWithTimestamp)(reqPath);
1060
- if (bk) {
1061
- backups.push(bk);
1062
- logger_1.logger.info(` 📦 已备份: ${bk.split('/').pop()}`);
1080
+ catch (e) {
1081
+ if (!jsonParsed) {
1082
+ // JSON 解析失败 → 回退 Markdown(保留原始输出)
1083
+ const reqPath = (0, path_1.join)(iterDir, 'REQUIREMENT.md');
1084
+ const bk = await (0, task_utils_1.backupWithTimestamp)(reqPath);
1085
+ if (bk) {
1086
+ backups.push(bk);
1087
+ logger_1.logger.info(` 📦 已备份: ${bk.split('/').pop()}`);
1088
+ }
1089
+ await (0, fs_extra_1.writeFile)(reqPath, options.response);
1090
+ }
1091
+ else {
1092
+ // 任务创建过程异常 → 必须显式报错,禁止静默吞掉冒充成功
1093
+ logger_1.logger.error(`❌ 任务创建失败: ${e?.message || e}`);
1094
+ if (backups.length > 0)
1095
+ logger_1.logger.info(` 📦 已备份文件仍保留,可回滚: ${backups.join(', ')}`);
1096
+ process.exitCode = 1;
1097
+ return;
1063
1098
  }
1064
- await (0, fs_extra_1.writeFile)(reqPath, options.response);
1065
1099
  }
1066
1100
  // v7.4.0+: 从 AI 输出中提取疑问并持久化
1067
1101
  const extractedQs = (0, questions_1.extractQuestionsFromText)(options.response);
@@ -2350,8 +2384,12 @@ ${!isBk && docSet.hasComponentTree ? '| COMPONENT_TREE.md | ⏳ | ./COMPONENT_TR
2350
2384
  }
2351
2385
  // 确保至少有 backend 子任务(AI 未输出 backend 时自动补充,仅非 research)
2352
2386
  // v6.48.0+:从 CONSTITUTION.md 端列表查找后端端名,不再硬编码 'backend'
2387
+ // v8.3.277: 仅当 AI 未显式声明 scope(_scopePlatforms 缺失/为空)时才自动补充;
2388
+ // scope 已显式给出时尊重 AI 意图,不制造幽灵后端子任务
2353
2389
  const hasAnyBackend = taskPlatforms.some((p) => p === 'backend' || p.startsWith('后台') || /-(service|api|server|backend)$/i.test(p));
2354
- if (!isResearch && !hasAnyBackend) {
2390
+ // AI 显式声明 scope 时尊重意图,不自动补后端;推断出的 scope 保留自动补充
2391
+ const shouldAutoBackend = !section._explicitScope;
2392
+ if (!isResearch && !hasAnyBackend && shouldAutoBackend) {
2355
2393
  // 从端列表查找第一个后端端名作为 fallback(v6.48.0+)
2356
2394
  const allPlatforms = await (0, spec_paths_1.parsePlatformList)();
2357
2395
  const fallbackBackend = allPlatforms.find(p => p === 'backend' || p.startsWith('后台') || /-(service|api|server|backend)$/i.test(p)) || 'backend';
@@ -2478,6 +2516,8 @@ ${taskPlatforms.map((p) => `| ${p} | 待补充 |`).join('\n')}
2478
2516
  }
2479
2517
  async function updateProjectGraph(iterationDir, sections) {
2480
2518
  const graphPath = (0, path_1.join)(iterationDir, '000-overview', 'PROJECT_GRAPH.md');
2519
+ // 防护:000-overview/ 可能尚不存在(全新迭代首次拆分),写前先建目录
2520
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(iterationDir, '000-overview'));
2481
2521
  let content = '';
2482
2522
  if (await (0, fs_extra_1.pathExists)(graphPath)) {
2483
2523
  content = await (0, fs_extra_1.readFile)(graphPath, 'utf-8');
@@ -3082,7 +3122,9 @@ async function readMatrixPlatforms(iterationDir, featureName) {
3082
3122
  return [];
3083
3123
  const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
3084
3124
  for (const e of entries) {
3085
- if (!e.isDirectory() || !e.name.includes(featureName))
3125
+ // v8.3.277: 排除时间戳备份目录——备份目录名仍包含 featureName,
3126
+ // 读到旧版 _matrix.md 会返回过期的端覆盖声明
3127
+ if (!e.isDirectory() || !e.name.includes(featureName) || (0, task_utils_1.isTimestampBackup)(e.name))
3086
3128
  continue;
3087
3129
  const matrixPath = (0, path_1.join)(featuresDir, e.name, '_matrix.md');
3088
3130
  if (!(await (0, fs_extra_1.pathExists)(matrixPath)))
@@ -4674,7 +4716,8 @@ async function tryModuleDrivenSplit(iteration, iterationDir, options) {
4674
4716
  try {
4675
4717
  const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
4676
4718
  for (const entry of entries) {
4677
- if (entry.isDirectory() && !entry.name.startsWith('.')) {
4719
+ // v8.3.277: 排除时间戳备份目录(unitize overwrite 产生的 xxx-20260924083058)
4720
+ if (entry.isDirectory() && !entry.name.startsWith('.') && !(0, task_utils_1.isTimestampBackup)(entry.name)) {
4678
4721
  const readmePath = await (0, feature_source_1.resolveUnitSourcePath)((0, path_1.join)(featuresDir, entry.name));
4679
4722
  if (await (0, fs_extra_1.pathExists)(readmePath)) {
4680
4723
  let unitPlatforms = [...allPlatforms];