speccore 8.3.264 → 8.3.267

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":"analyze.d.ts","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"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"}
@@ -714,6 +714,25 @@ async function analyzeCommand(options) {
714
714
  return;
715
715
  }
716
716
  }
717
+ else if (options.autoUnits || options.resumeUnits) {
718
+ // v8.3.266+: 缓存与磁盘 features/ 权威源对照——旧缓存可能含 unitize 重切前的假单元
719
+ // (实测会议项目 23 个缓存单元中 6 个在 features/ 不存在),调度循环会给假单元生成
720
+ // Prompt 写到 features 锚点之外的目录,白写且 taskify/Phase 2 均不读。
721
+ const diskUnits = await extractIterationUnits(iterDir);
722
+ const cacheNames = new Set(units.map((u) => u.name));
723
+ const diskNames = new Set(diskUnits.map((u) => u.name));
724
+ const mismatched = diskUnits.length > 0
725
+ && (diskUnits.length !== units.length || [...diskNames].some((n) => !cacheNames.has(n)));
726
+ if (mismatched) {
727
+ units = diskUnits;
728
+ await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(unitsCachePath));
729
+ await (0, fs_extra_1.writeFile)(unitsCachePath, JSON.stringify(units, null, 2));
730
+ logger_1.logger.warn(`⚠️ 单元清单与磁盘 features/ 目录不一致,已刷新: ${units.length} 个`);
731
+ for (const u of units) {
732
+ logger_1.logger.info(` ${u.id}: ${u.name} (${u.source}#L${u.sourceRange.startLine})`);
733
+ }
734
+ }
735
+ }
717
736
  // Step 2: 汇总模式(--consolidate)
718
737
  if (options.consolidate) {
719
738
  const analyses = await loadUnitAnalyses(iter);
@@ -771,14 +790,33 @@ async function analyzeCommand(options) {
771
790
  // CLI 每轮推进状态机:校验产出 → 已就绪自动标记并推进,未就绪输出该单元 prompt(exitCode 10)
772
791
  // → 宿主 AI 落盘 → 再次调用本命令 → CLI 校验通过自动推进下一个单元,直到全部完成
773
792
  if (options.autoUnits || options.resumeUnits) {
774
- const loopResult = await runUnitDispatchLoop(iter, iterDir, units);
775
- if (loopResult.finished) {
776
- await printUnitLoopSummary(iter, iterDir, units, loopResult);
777
- return;
793
+ // v8.3.266: Qoder 环境非 TTY 下优先 SDK 派单——CLI 进程内直接调起子 Agent
794
+ // (独立上下文、acceptEdits 直接写文件),校验通过后自动推进下一单元,宿主全程
795
+ // 不参与;SDK 不可用/失败时回退标记模式(exitCode 10 宿主接力)。
796
+ // 收敛性:每轮单元状态必然前进(done/failed)或 SDK 失败退出,不会死循环。
797
+ const sdkSupported = !process.stdout.isTTY && (0, agent_adapter_1.isSdkDispatchSupported)();
798
+ const maxDispatchRounds = units.length * 3 + 5; // 防御性上限(每单元最多 3 次尝试 + 余量)
799
+ let dispatchCount = 0;
800
+ while (true) {
801
+ const loopResult = await runUnitDispatchLoop(iter, iterDir, units);
802
+ if (loopResult.finished) {
803
+ await printUnitLoopSummary(iter, iterDir, units, loopResult);
804
+ return;
805
+ }
806
+ const fallbackPrompt = `${loopResult.prompt}\n\n## 完成后继续\n\n落盘后运行以下命令,CLI 将校验产出并自动推进到下一个单元:\n\n\`\`\`bash\nspeccore analyze --resume-units -I ${iter}\n\`\`\`\n`;
807
+ if (!sdkSupported || !(await trySdkDispatchUnit(loopResult.prompt, iter, loopResult.step.name))) {
808
+ // 回退标记模式(非 Qoder 宿主 / TTY / SDK 失败均走此路径)
809
+ await emitAiPrompt(iter, fallbackPrompt, { mode: 'resume-units-loop' });
810
+ process.exitCode = 10;
811
+ return;
812
+ }
813
+ if (++dispatchCount >= maxDispatchRounds) {
814
+ logger_1.logger.warn(`⚠️ SDK 派单轮次超限(${maxDispatchRounds}),退出调度循环`);
815
+ process.exitCode = 10;
816
+ return;
817
+ }
778
818
  }
779
- await emitAiPrompt(iter, `${loopResult.prompt}\n\n## 完成后继续\n\n落盘后运行以下命令,CLI 将校验产出并自动推进到下一个单元:\n\n\`\`\`bash\nspeccore analyze --resume-units -I ${iter}\n\`\`\`\n`, { mode: 'resume-units-loop' });
780
- process.exitCode = 10;
781
- return;
819
+ // 理论不可达:while(true) 内所有路径均 return
782
820
  }
783
821
  // 3c. --unit: 指定单元(支持逗号分隔多个,如 M-01,M-03)
784
822
  if (options.unit) {
@@ -1765,6 +1803,8 @@ async function analyzeCommand(options) {
1765
1803
  const clarifyBlocks = parseClarifyMarkers(options.apply);
1766
1804
  if (clarifyBlocks.size > 0 && !isGlobalScope) {
1767
1805
  logger_1.logger.info(`📋 检测到 ${clarifyBlocks.size} 个澄清文档,先写入...`);
1806
+ // v8.3.267: 提前取真实功能单元名集合(只取一次),供幽灵路由判定
1807
+ const validFeatures = new Set(iterDir ? await (0, spec_paths_2.parseFeatureList)(iterDir) : []);
1768
1808
  for (const [filename, content] of clarifyBlocks) {
1769
1809
  const { parseClarifiedRequirement, buildClarifiedHeader } = await Promise.resolve().then(() => __importStar(require('../core/requirement-clarifier')));
1770
1810
  const { content: cleaned } = parseClarifiedRequirement(content);
@@ -1773,7 +1813,25 @@ async function analyzeCommand(options) {
1773
1813
  // 解析写入路径:支持新格式(overview/、{feature}/overview/)和旧格式(requirements/)
1774
1814
  let fp;
1775
1815
  if (filename.includes('/') || filename.includes('\\')) {
1776
- fp = (0, path_1.join)(iterDir, '020-specs', filename);
1816
+ // v8.3.267: 幽灵路由归一化——AI 常把「文档 slug」当 feature-name 生成
1817
+ // [CLARIFY:overview/overview/CLARIFY.md]、[CLARIFY:admin-web/overview/CLARIFY.md]
1818
+ // 等标记,原样落地会产出 overview/overview/、admin-web/ 等幽灵目录。
1819
+ // 首段不是真实功能单元时:
1820
+ // 「overview」→ 全局澄清 020-specs/overview/CLARIFY.md
1821
+ // 其余文档 slug → 文档级黄金需求 020-specs/requirements/{slug}-clarified.md
1822
+ const seg = filename.split(/[\\/]/)[0];
1823
+ const isUnitOverview = /^[^\\/]+\/overview\/CLARIFY\.md$/i.test(filename);
1824
+ if (isUnitOverview && seg === 'overview') {
1825
+ fp = (0, path_1.join)(iterDir, '020-specs', 'overview', 'CLARIFY.md');
1826
+ logger_1.logger.warn(` ↪ ${filename} 首段「overview」为全局澄清,已归一化到 020-specs/overview/CLARIFY.md`);
1827
+ }
1828
+ else if (isUnitOverview && !validFeatures.has(seg)) {
1829
+ fp = (0, path_1.join)(iterDir, '020-specs', 'requirements', `${seg}-clarified.md`);
1830
+ logger_1.logger.warn(` ↪ ${filename} 首段「${seg}」非功能单元,已路由到 requirements/${seg}-clarified.md(文档级黄金需求)`);
1831
+ }
1832
+ else {
1833
+ fp = (0, path_1.join)(iterDir, '020-specs', filename);
1834
+ }
1777
1835
  }
1778
1836
  else {
1779
1837
  // 兼容旧格式:无路径前缀的文件名写入 requirements/
@@ -6359,12 +6417,26 @@ async function extractIterationUnits(iterDir) {
6359
6417
  const featuresDir = (0, path_1.join)(reqDir, 'features');
6360
6418
  if (await (0, fs_extra_1.pathExists)(featuresDir)) {
6361
6419
  const entries = await (0, fs_extra_2.readdir)(featuresDir, { withFileTypes: true });
6362
- const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_'));
6420
+ // v8.3.267: 过滤 overview- 前缀的全局章节目录(unitize 误切产物,
6421
+ // 如「核心业务规则跨端通用」),防止假单元混入调度循环
6422
+ const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_') && !e.name.startsWith('overview-'));
6363
6423
  for (const dir of dirs) {
6364
6424
  const readmePath = await (0, feature_source_1.resolveUnitSourcePath)((0, path_1.join)(featuresDir, dir.name));
6365
6425
  let content = '';
6366
6426
  if (await (0, fs_extra_1.pathExists)(readmePath)) {
6367
- content = (await (0, fs_extra_2.readFile)(readmePath, 'utf-8')).slice(0, 500);
6427
+ // v8.3.267: 图片引用可能落在 500 字符截断之外,显式保留引用行,
6428
+ // 保证单元上下文的图片清单完整(AI 可据此 Read 原型图)
6429
+ const full = await (0, fs_extra_2.readFile)(readmePath, 'utf-8');
6430
+ content = full.slice(0, 500);
6431
+ const imgRefs = new Set();
6432
+ const imgRe = /!\[[^\]]*\]\(([^)\s]+)\)/g;
6433
+ let m;
6434
+ while ((m = imgRe.exec(full)) !== null)
6435
+ imgRefs.add(m[1]);
6436
+ const outside = [...imgRefs].filter(r => !content.includes(r));
6437
+ if (outside.length > 0) {
6438
+ content += `\n\n<!-- 原文图片引用(截断外保留)-->\n${outside.map(r => `![图片](${r})`).join('\n')}`;
6439
+ }
6368
6440
  }
6369
6441
  units.push({
6370
6442
  id: `M-${String(idCounter++).padStart(2, '0')}`,
@@ -6470,7 +6542,9 @@ async function loadUnitAnalyses(iteration) {
6470
6542
  async function detectPendingUnits(iteration, allUnits) {
6471
6543
  const completed = await loadUnitAnalyses(iteration);
6472
6544
  const completedIds = new Set(completed.map(a => a.id));
6473
- const iterDir = `Iteration-${iteration.replace(/^Iteration-/, '')}`;
6545
+ // v8.3.266+: 用绝对路径迭代目录替代相对路径拼接——cwd 不在迭代父目录时旧写法会指向错误路径,
6546
+ // 导致 unitHasSkeletonOutput 兜底检查全部误判为骨架(路径不存在)
6547
+ const iterDir = await (0, context_1.getIterationDir)(iteration);
6474
6548
  const pending = [];
6475
6549
  for (const u of allUnits) {
6476
6550
  if (!completedIds.has(u.id)) {
@@ -6864,6 +6938,7 @@ async function buildUnitLoopPrompt(iterDir, iter, unit, allUnits, attempt, lastE
6864
6938
  约束:
6865
6939
  - 每个文件内容不少于 200 字符,禁止输出空模板或骨架占位
6866
6940
  - 写入前先创建目录;写完后回复一行:\`DONE ${unit.name}\`
6941
+ - 若上下文中含「需求图片/原型」或 \`![...](...)\` 图片引用,请先用 Read 工具打开图片文件查看,再结合图片信息撰写规格;不要忽略图片提示
6867
6942
  `;
6868
6943
  if (attempt > 1 && lastError) {
6869
6944
  extra += `
@@ -6876,6 +6951,89 @@ async function buildUnitLoopPrompt(iterDir, iter, unit, allUnits, attempt, lastE
6876
6951
  }
6877
6952
  return base + extra;
6878
6953
  }
6954
+ /**
6955
+ * v8.3.266: 调度循环 SDK 派单——Qoder 环境非 TTY 下由 CLI 直接调起子 Agent
6956
+ * (独立上下文、acceptEdits 直接写文件),返回 true 表示派单成功(含自动落盘)。
6957
+ */
6958
+ async function trySdkDispatchUnit(prompt, iter, unitName) {
6959
+ try {
6960
+ const agentCtx = {
6961
+ subagent: 'requirement-analyzer',
6962
+ iteration: iter,
6963
+ contextBudget: 6000,
6964
+ contextType: 'incremental',
6965
+ cwd: (0, task_utils_1.findProjectRoot)() || process.cwd(),
6966
+ };
6967
+ const dispatchResult = await (0, agent_adapter_1.dispatchSubagent)(agentCtx, prompt);
6968
+ if (!dispatchResult || !dispatchResult.success) {
6969
+ logger_1.logger.debug('[analyze] SDK 派单未成功,回退标记模式');
6970
+ return false;
6971
+ }
6972
+ // v8.3.266: 防御空转——success 但既没写文件也没返回可落盘内容,视为无效派单,
6973
+ // 立即回退标记模式(否则循环会白白耗尽该单元 3 次尝试并误标 failed)
6974
+ if (!dispatchResult.content && (!dispatchResult.filesChanged || dispatchResult.filesChanged.length === 0)) {
6975
+ logger_1.logger.warn('[analyze] SDK 派单无产出(未写文件且无回传内容),回退标记模式');
6976
+ return false;
6977
+ }
6978
+ logger_1.logger.info(`[analyze] 调度循环子 Agent SDK 派单成功: ${unitName}`);
6979
+ // v8.3.196+ 自动落盘:子 Agent 未直接写文件时,解析其返回的 JSON/[DOC:] 内容
6980
+ // 并路由到该单元的 overview/ 目录(applyDocContent 无单元路由,此处专用落盘)
6981
+ if (dispatchResult.content) {
6982
+ const autoApplied = await applyUnitLoopDocs(dispatchResult.content, iter, unitName);
6983
+ if (autoApplied > 0) {
6984
+ process.stdout.write(`✅ 已自动写入 ${autoApplied} 个文档到 020-specs/${sanitizeUnitDirName(unitName)}/overview/\n`);
6985
+ }
6986
+ }
6987
+ if (dispatchResult.filesChanged && dispatchResult.filesChanged.length > 0) {
6988
+ process.stdout.write(`📄 文件变更: ${dispatchResult.filesChanged.join(', ')}\n`);
6989
+ }
6990
+ return true;
6991
+ }
6992
+ catch (e) {
6993
+ logger_1.logger.debug(`[analyze] SDK 派单异常: ${e.message},回退标记模式`);
6994
+ return false;
6995
+ }
6996
+ }
6997
+ /**
6998
+ * v8.3.266: 把子 Agent 回传的 JSON/[DOC:] 内容写入指定单元的 overview/ 目录。
6999
+ * 文档名归一化:analyze prompt 用 [DOC:REQ.md],落盘用 REQUIREMENT.md(validateUnitSpec 校验名单)。
7000
+ */
7001
+ async function applyUnitLoopDocs(content, iter, unitName) {
7002
+ const trimmed = content.trim();
7003
+ if (!trimmed)
7004
+ return 0;
7005
+ const iterDir = await (0, context_1.getIterationDir)(iter);
7006
+ const unitDir = (0, path_1.join)(iterDir, '020-specs', sanitizeUnitDirName(unitName), 'overview');
7007
+ let docs = {};
7008
+ if (trimmed.startsWith('{')) {
7009
+ try {
7010
+ const parsed = JSON.parse(trimmed);
7011
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
7012
+ docs = parsed;
7013
+ }
7014
+ catch { /* 回退 [DOC:] 标记 */ }
7015
+ }
7016
+ if (Object.keys(docs).length === 0) {
7017
+ for (const [name, c] of parseDocMarkers(trimmed))
7018
+ docs[name] = c;
7019
+ }
7020
+ if (Object.keys(docs).length === 0)
7021
+ return 0;
7022
+ const NAME_MAP = { 'REQ.md': 'REQUIREMENT.md' };
7023
+ let applied = 0;
7024
+ for (const [filename, docContent] of Object.entries(docs)) {
7025
+ if (typeof docContent !== 'string' || !docContent.trim())
7026
+ continue;
7027
+ const baseName = (filename.split('/').pop() || filename).trim();
7028
+ const normalized = NAME_MAP[baseName] || baseName;
7029
+ if (!normalized.endsWith('.md') || normalized.startsWith('.'))
7030
+ continue;
7031
+ await (0, fs_extra_1.ensureDir)(unitDir);
7032
+ await (0, fs_extra_1.writeFile)((0, path_1.join)(unitDir, normalized), docContent);
7033
+ applied++;
7034
+ }
7035
+ return applied;
7036
+ }
6879
7037
  /** 单元分析调度循环:构建步骤清单并推进一轮 */
6880
7038
  async function runUnitDispatchLoop(iter, iterDir, units) {
6881
7039
  const steps = units.map((u) => ({
@@ -6905,9 +7063,49 @@ async function printUnitLoopSummary(iter, iterDir, units, result) {
6905
7063
  logger_1.logger.success(`🎉 所有 ${s.done.length} 个功能单元分析完成!`);
6906
7064
  }
6907
7065
  logger_1.logger.info(` 下一步: 汇总统一报告 speccore analyze --prompt -I ${iter} --consolidate`);
7066
+ // v8.3.266+: 循环只保证 overview/ 3 份,端级文档({feature}/{端}/)由 Phase 2 负责——
7067
+ // 此前完成提示不提 Phase 2,用户以为分析已闭环,taskify 时端级内容仍是骨架
7068
+ const platformSk = await countPlatformSkeletons(iterDir);
7069
+ if (platformSk.total > 0 && platformSk.skeleton > 0) {
7070
+ logger_1.logger.warn(`⚠️ 端级文档仍有 ${platformSk.skeleton}/${platformSk.total} 份为骨架(overview/ 之外)`);
7071
+ logger_1.logger.info(` Phase 2 生成各端专属文档: speccore analyze --prompt -I ${iter} --phase 2`);
7072
+ }
6908
7073
  // v8.3.234+: 执行完成质量报告(成功/失败清单 + 产出物评分)落盘 QUALITY_AUDIT.md
6909
7074
  await buildAndSaveQualityReport(iterDir, iter, units);
6910
7075
  }
7076
+ /**
7077
+ * v8.3.266+: 统计 020-specs/{feature}/{端}/ 下仍为骨架的 .md 文件数。
7078
+ * 用于调度循环完成后引导 Phase 2(调度循环只保证 overview/ 3 份落盘)。
7079
+ */
7080
+ async function countPlatformSkeletons(iterDir) {
7081
+ let skeleton = 0;
7082
+ let total = 0;
7083
+ try {
7084
+ const specDir = (0, path_1.join)(iterDir, '020-specs');
7085
+ if (!(await (0, fs_extra_1.pathExists)(specDir)))
7086
+ return { skeleton, total };
7087
+ const features = await (0, spec_paths_2.parseFeatureList)(iterDir);
7088
+ const platforms = await (0, spec_paths_2.parsePlatformList)();
7089
+ for (const feature of features) {
7090
+ for (const platform of platforms) {
7091
+ const pDir = (0, path_1.join)(specDir, feature, platform);
7092
+ if (!(await (0, fs_extra_1.pathExists)(pDir)))
7093
+ continue;
7094
+ const entries = await (0, fs_extra_2.readdir)(pDir, { withFileTypes: true }).catch(() => []);
7095
+ for (const entry of entries) {
7096
+ if (!entry.name.endsWith('.md') || !entry.isFile())
7097
+ continue;
7098
+ total++;
7099
+ const content = (await (0, fs_extra_2.readFile)((0, path_1.join)(pDir, entry.name), 'utf-8').catch(() => '')).trim();
7100
+ if (!content || content.includes('SPEC-SKELETON') || content.length < 200)
7101
+ skeleton++;
7102
+ }
7103
+ }
7104
+ }
7105
+ }
7106
+ catch { /* 统计失败不阻断主流程 */ }
7107
+ return { skeleton, total };
7108
+ }
6911
7109
  /**
6912
7110
  * v8.3.234+: 生成并落盘「功能单元分析质量报告」——成功/失败清单 + 每单元产出物评分。
6913
7111
  * 落盘到 020-specs/QUALITY_AUDIT.md;无可评单元时返回 null。