speccore 8.3.162 → 8.3.164

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":"AAwNA,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;AA+ID,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA09BzF"}
1
+ {"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../../src/commands/iteration/split.ts"],"names":[],"mappings":"AAwNA,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;AA+ID,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmjCzF"}
@@ -505,6 +505,14 @@ async function iterationSplitCommand(options) {
505
505
  const iterDir = (0, path_1.join)(iterDirFull, '030-tasks');
506
506
  await (0, fs_extra_1.ensureDir)(iterDir);
507
507
  const backups = [];
508
+ // v8.3.163+: AI 上下文中禁用 --force(质量门禁和安全限制不可被 AI 自行绕过)
509
+ const isAiCtx = !process.stdout.isTTY;
510
+ if (options.force && isAiCtx) {
511
+ logger_1.logger.error('❌ AI 上下文中禁止使用 --force 参数');
512
+ logger_1.logger.info(' 质量门禁(任务数限制、拆分粒度校验)不可被 AI 自行绕过');
513
+ logger_1.logger.info(' 如需强制继续,请在交互式终端中执行此命令');
514
+ return;
515
+ }
508
516
  // v8.3.160+: 智能提取 JSON(支持 JSON 后附带 [PENDING] / [SPECCORE_EXEC] 标记)
509
517
  function extractJsonArray(text) {
510
518
  const result = { json: text };
@@ -674,20 +682,29 @@ async function iterationSplitCommand(options) {
674
682
  if (sections.length > MAX_TASKS_HARD && options.force) {
675
683
  logger_1.logger.warn(` ⚠️ 任务数 ${sections.length} 超出建议上限 ${MAX_TASKS_HARD},--force 已启用,继续...`);
676
684
  }
677
- // 🚨 逐功能单元校验:每个功能单元拆出的任务数不超过 3 个
678
- const MAX_TASKS_PER_UNIT = 3;
679
- const unitTaskCount = {};
685
+ // v8.3.162+: 逐功能单元×端校验:每个功能单元的每个端最多 3 个任务
686
+ // 拆分策略:功能单元先按端拆分,每个端内的子任务最多 3 个
687
+ const MAX_TASKS_PER_UNIT_PLATFORM = 3;
688
+ const unitPlatformTaskCount = {};
680
689
  let missingFunctionalUnit = 0;
681
- // 按 functionalUnit 分组统计(AI 在 JSON 中标注所属功能单元)
690
+ // 按 functionalUnit × platform 分组统计
682
691
  for (let i = 0; i < sections.length; i++) {
683
692
  const task = tasks[i];
684
693
  const unitName = task.functionalUnit;
694
+ const scopes = Array.isArray(task.scope) ? task.scope : [];
685
695
  if (!unitName) {
686
696
  missingFunctionalUnit++;
687
- unitTaskCount['__missing__'] = (unitTaskCount['__missing__'] || 0) + 1;
697
+ // 未标注功能单元的任务,按 scope 分别计入 __missing__
698
+ for (const platform of scopes) {
699
+ const key = `__missing__|${platform}`;
700
+ unitPlatformTaskCount[key] = (unitPlatformTaskCount[key] || 0) + 1;
701
+ }
688
702
  }
689
703
  else {
690
- unitTaskCount[unitName] = (unitTaskCount[unitName] || 0) + 1;
704
+ for (const platform of scopes) {
705
+ const key = `${unitName}|${platform}`;
706
+ unitPlatformTaskCount[key] = (unitPlatformTaskCount[key] || 0) + 1;
707
+ }
691
708
  }
692
709
  }
693
710
  // functionalUnit 缺失警告
@@ -696,23 +713,24 @@ async function iterationSplitCommand(options) {
696
713
  logger_1.logger.warn(` 💡 AI 未按约束填写功能单元,校验可能不准确`);
697
714
  logger_1.logger.warn(` 💡 建议重新执行 split,确保 Prompt 包含 functionalUnit 要求`);
698
715
  }
699
- // 检查每个功能单元的任务数
716
+ // 检查每个功能单元×端的任务数
700
717
  let hasOverSplit = false;
701
- for (const [unitName, count] of Object.entries(unitTaskCount)) {
702
- if (count > MAX_TASKS_PER_UNIT) {
718
+ for (const [key, count] of Object.entries(unitPlatformTaskCount)) {
719
+ if (count > MAX_TASKS_PER_UNIT_PLATFORM) {
703
720
  if (!hasOverSplit) {
704
- logger_1.logger.error(`\n ❌ 检测到过度拆分!某些功能单元拆出过多任务:`);
721
+ logger_1.logger.error(`\n ❌ 检测到过度拆分!某些功能单元在单个端内拆出过多任务:`);
705
722
  hasOverSplit = true;
706
723
  }
707
- const displayName = unitName === '__missing__' ? '(未标注功能单元)' : unitName;
708
- logger_1.logger.error(` 📌 "${displayName}" 拆出了 ${count} 个任务(上限 ${MAX_TASKS_PER_UNIT})`);
724
+ const [unitName, platform] = key.split('|');
725
+ const displayUnit = unitName === '__missing__' ? '(未标注功能单元)' : unitName;
726
+ logger_1.logger.error(` 📌 "${displayUnit}" 在 "${platform}" 端拆出了 ${count} 个任务(上限 ${MAX_TASKS_PER_UNIT_PLATFORM})`);
709
727
  }
710
728
  }
711
729
  if (hasOverSplit) {
712
- logger_1.logger.error(` 💡 核心原则:一个功能单元默认 1 个任务,最多 3 个`);
730
+ logger_1.logger.error(` 💡 核心原则:功能单元先按端拆分,每个端内默认 1 个任务,最多 3 个`);
713
731
  logger_1.logger.error(` 🔧 建议操作:`);
714
732
  logger_1.logger.error(` 1. 重新执行 split,并告诉 AI:"任务太多,请合并相关功能"`);
715
- logger_1.logger.error(` 2. 或者手动编辑 .speccore/prompts/split-suggestion-${iter}.md,明确要求"每个功能单元最多拆 2 个任务"`);
733
+ logger_1.logger.error(` 2. 或者手动编辑 .speccore/prompts/split-suggestion-${iter}.md,明确要求"每个功能单元的每个端最多拆 2 个任务"`);
716
734
  logger_1.logger.error(` 3. 如果确实需要这么多任务,使用 --force 跳过检查(不推荐)`);
717
735
  if (!options.force) {
718
736
  logger_1.logger.info('\n ℹ️ 如需强制继续,添加 --force 参数');
@@ -720,6 +738,76 @@ async function iterationSplitCommand(options) {
720
738
  }
721
739
  logger_1.logger.warn(' ⚠️ --force 已启用,继续创建所有任务...');
722
740
  }
741
+ // v8.3.164+: 内容质量校验 — 拦截空壳任务(reqContent/techContent 实质内容不足)
742
+ // 问题:AI 返回的 reqContent/techContent 虽然有长度,但全是 AI-FILL 占位符和模板
743
+ const MIN_SUBSTANTIVE_CHARS = 80; // 实质内容最低字符数
744
+ const MAX_EMPTY_SHELL_RATIO = 0.5; // 空壳任务占比上限
745
+ let emptyShellCount = 0;
746
+ const emptyShellTasks = [];
747
+ for (let i = 0; i < sections.length; i++) {
748
+ const sec = sections[i];
749
+ const reqContent = sec._reqContent;
750
+ const techContent = sec._techContent;
751
+ // 计算实质内容字符数(排除 HTML 注释、AI-FILL 标记、空白行、markdown 格式符号)
752
+ const calcSubstantive = (text) => {
753
+ if (!text || text.trim().length === 0)
754
+ return 0;
755
+ // 去除 HTML 注释
756
+ let cleaned = text.replace(/<!--[\s\S]*?-->/g, '');
757
+ // 去除 AI-FILL 标记行
758
+ cleaned = cleaned.replace(/AI-FILL[\s\S]*?$/gm, '');
759
+ // 去除 markdown 标题符号
760
+ cleaned = cleaned.replace(/^#+\s*/gm, '');
761
+ // 去除表格分隔符
762
+ cleaned = cleaned.replace(/^[\|\-:\s]+$/gm, '');
763
+ // 去除空白行
764
+ cleaned = cleaned.replace(/^\s*$/gm, '');
765
+ // 去除 markdown 列表符号
766
+ cleaned = cleaned.replace(/^[-*+]\s+/gm, '');
767
+ // 去除代码块标记
768
+ cleaned = cleaned.replace(/^```\w*$/gm, '');
769
+ // 去除纯英文占位符(如 "TBD", "TODO", "待补充" 等)
770
+ cleaned = cleaned.replace(/\b(TBD|TODO|FIXME|HACK|XXX|待补充|待实现|待填写|待完善|待确认|待定)\b/gi, '');
771
+ // 计算剩余实质字符(中文字符 + 英文单词)
772
+ const cnChars = (cleaned.match(/[\u4e00-\u9fa5]/g) || []).length;
773
+ const enWords = (cleaned.match(/[a-zA-Z]{2,}/g) || []).length;
774
+ return cnChars + enWords;
775
+ };
776
+ const reqSubstantive = calcSubstantive(reqContent);
777
+ const techSubstantive = calcSubstantive(techContent);
778
+ // 判定为空壳:req 和 tech 都缺乏实质内容
779
+ if (reqSubstantive < MIN_SUBSTANTIVE_CHARS && techSubstantive < MIN_SUBSTANTIVE_CHARS) {
780
+ emptyShellCount++;
781
+ emptyShellTasks.push(`"${sec.name}" (req=${reqSubstantive}, tech=${techSubstantive})`);
782
+ }
783
+ }
784
+ const emptyRatio = sections.length > 0 ? emptyShellCount / sections.length : 0;
785
+ if (emptyShellCount > 0) {
786
+ if (emptyRatio > MAX_EMPTY_SHELL_RATIO) {
787
+ logger_1.logger.error(`\n ❌ 内容质量拦截:${emptyShellCount}/${sections.length} 个任务为空壳(占比 ${Math.round(emptyRatio * 100)}%,上限 ${Math.round(MAX_EMPTY_SHELL_RATIO * 100)}%)`);
788
+ logger_1.logger.error(` 💡 空壳任务特征:reqContent/techContent 全是 AI-FILL 占位符、模板内容或 "待补充"`);
789
+ logger_1.logger.error(` 🔧 建议操作:`);
790
+ logger_1.logger.error(` 1. 重新执行 split,并告诉 AI:"请为每个任务生成完整的需求规格和技术方案,不要只输出骨架"`);
791
+ logger_1.logger.error(` 2. 或者检查 analyze 阶段的 020-specs/ 是否有足够内容供 split 提取`);
792
+ for (const t of emptyShellTasks.slice(0, 5))
793
+ logger_1.logger.error(` • ${t}`);
794
+ if (emptyShellTasks.length > 5)
795
+ logger_1.logger.error(` ... 还有 ${emptyShellTasks.length - 5} 个`);
796
+ if (!options.force) {
797
+ logger_1.logger.info('\n ℹ️ 如需强制继续,添加 --force 参数');
798
+ return;
799
+ }
800
+ logger_1.logger.warn(' ⚠️ --force 已启用,继续创建空壳任务...');
801
+ }
802
+ else {
803
+ logger_1.logger.warn(`\n ⚠️ 内容质量警告:${emptyShellCount}/${sections.length} 个任务为空壳`);
804
+ for (const t of emptyShellTasks.slice(0, 3))
805
+ logger_1.logger.warn(` • ${t}`);
806
+ if (emptyShellTasks.length > 3)
807
+ logger_1.logger.warn(` ... 还有 ${emptyShellTasks.length - 3} 个`);
808
+ logger_1.logger.warn(` 💡 这些任务将在 execute 阶段依赖 AI 现场生成内容,可能增加开发时间`);
809
+ }
810
+ }
723
811
  // 交互模式判断:显式 --interactive 或 stdin 是 TTY(--force 时跳过交互,直接执行)
724
812
  const isInteractive = (options.interactive || process.stdin.isTTY) && !options.force;
725
813
  // 非交互模式:显示任务总览摘要