speccore 8.3.247 → 8.3.248

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.
@@ -67,6 +67,7 @@ const feature_source_1 = require("../core/feature-source");
67
67
  const analyze_engine_1 = require("../core/analyze-engine");
68
68
  const fs_extra_2 = require("fs-extra");
69
69
  const prompt_builder_1 = require("../core/prompt-builder");
70
+ const spec_paths_1 = require("../core/spec-paths");
70
71
  // v8.3.160+: 子 Agent 适配层(analyze 自定义 Prompt 中手动注入)
71
72
  // v8.3.169+: 新增 dispatchSubagent 支持 Qoder SDK 直接调度
72
73
  const agent_adapter_1 = require("../core/agent-adapter");
@@ -74,7 +75,7 @@ const questions_1 = require("../core/questions");
74
75
  const platform_registry_1 = require("../core/platform-registry");
75
76
  const index_guard_1 = require("../core/index-guard");
76
77
  const unified_config_1 = require("../core/unified-config");
77
- const spec_paths_1 = require("../core/spec-paths");
78
+ const spec_paths_2 = require("../core/spec-paths");
78
79
  const spec_skeleton_1 = require("../core/spec-skeleton");
79
80
  const unified_retrieval_1 = require("../core/unified-retrieval");
80
81
  const unit_context_assembler_1 = require("../core/unit-context-assembler");
@@ -260,7 +261,7 @@ async function applyDocContent(content, iter) {
260
261
  let applied = 0;
261
262
  // v8.3.237+: 与 --apply 主通道一致——有功能模块的项目才拦截(内容类归 {feature}/overview/,
262
263
  // 全局 overview/REQUIREMENT.md 由 split 聚合),无功能模块的老项目放行
263
- const featureCount = (await (0, spec_paths_1.parseFeatureList)(iterDir)).length;
264
+ const featureCount = (await (0, spec_paths_2.parseFeatureList)(iterDir)).length;
264
265
  for (const [filename, docContent] of Object.entries(docs)) {
265
266
  if (typeof docContent !== 'string' || !docContent.trim())
266
267
  continue;
@@ -814,7 +815,7 @@ async function analyzeCommand(options) {
814
815
  }
815
816
  // v8.3.16+: 模糊匹配端名,支持多段指定(逗号分隔)
816
817
  if (options.platform) {
817
- const allPlatforms = await (0, spec_paths_1.parsePlatformList)();
818
+ const allPlatforms = await (0, spec_paths_2.parsePlatformList)();
818
819
  const inputPlatforms = options.platform.split(',').map(p => p.trim()).filter(Boolean);
819
820
  const resolvedPlatforms = [];
820
821
  for (const input of inputPlatforms) {
@@ -923,7 +924,7 @@ async function analyzeCommand(options) {
923
924
  }
924
925
  // 新路径两级澄清产物({feature}/overview/CLARIFY.md)与切分目录一一对应,优先级最高
925
926
  const clarifyUnitDocs = await collectUnitClarifyDocs(specDir);
926
- const featureUnits = await (0, spec_paths_1.parseFeatureList)(iterDir);
927
+ const featureUnits = await (0, spec_paths_2.parseFeatureList)(iterDir);
927
928
  if (featureUnits.length > 0 && (clarifyUnitDocs.length > 0 || unitLevel.length > 0)) {
928
929
  // 有切分锚点:按单元优先读入一一对应的澄清文档,跳过冗余的文档级
929
930
  const legacyUnitName = (f) => (0, path_1.basename)(f).replace(/^unit-/, '').replace(/\.md$/, '');
@@ -953,7 +954,7 @@ async function analyzeCommand(options) {
953
954
  // features/ 单元名决定 020-specs 骨架,黄金需求文件名决定内容来源,
954
955
  // 两者不对齐时 taskify 无法把澄清内容挂接到对应单元
955
956
  try {
956
- const featureUnits = await (0, spec_paths_1.parseFeatureList)(iterDir);
957
+ const featureUnits = await (0, spec_paths_2.parseFeatureList)(iterDir);
957
958
  if (featureUnits.length > 0) {
958
959
  const goldenBases = (await (0, fs_extra_2.readdir)(goldenReqDir))
959
960
  .filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f))
@@ -1116,7 +1117,7 @@ async function analyzeCommand(options) {
1116
1117
  // v8.0.0+: 骨架优先架构 — CLI 预创建所有文件,AI 只覆盖内容
1117
1118
  const iterDirForSkeleton = await (0, context_1.getIterationDir)(iterForDirs);
1118
1119
  const specDir = (0, path_1.join)(iterDirForSkeleton, '020-specs');
1119
- let platforms = await (0, spec_paths_1.parsePlatformList)();
1120
+ let platforms = await (0, spec_paths_2.parsePlatformList)();
1120
1121
  const iterName = iterForDirs;
1121
1122
  // v8.3.16+: 如果指定了 --platform,只创建指定端的骨架
1122
1123
  if (options.platform) {
@@ -1133,7 +1134,7 @@ async function analyzeCommand(options) {
1133
1134
  // v8.3.171+: 删除 sanitize 自动调用(保留为 speccore migrate 工具)
1134
1135
  // v8.3.21+: 按需求文档名组织(020-specs/{需求名}/)
1135
1136
  const phase = options.phase;
1136
- const features = await (0, spec_paths_1.parseFeatureList)(iterDirForSkeleton);
1137
+ const features = await (0, spec_paths_2.parseFeatureList)(iterDirForSkeleton);
1137
1138
  let manifest;
1138
1139
  if (features.length > 0) {
1139
1140
  manifest = (0, spec_skeleton_1.computeFeatureBasedAnalyzeManifest)(features, platforms, phase, iterName);
@@ -1526,10 +1527,10 @@ ${singlePrompt}`);
1526
1527
  if (stepId === 'phase1-prompt') {
1527
1528
  // 有功能模块的项目只生成全局索引;老项目生成全部全局文档。
1528
1529
  // CLARIFY.md 由 clarify 阶段产出,不在本步骤检查范围(skip clarify 时不会卡住)。
1529
- const featureCount = (await (0, spec_paths_1.parseFeatureList)(iterDirForCheck)).length;
1530
+ const featureCount = (await (0, spec_paths_2.parseFeatureList)(iterDirForCheck)).length;
1530
1531
  checkDocs = (featureCount > 0 ? spec_skeleton_1.PHASE1_INDEX_DOC_NAMES : spec_skeleton_1.PHASE1_DOC_NAMES)
1531
1532
  .filter((n) => n !== 'CLARIFY.md');
1532
- baseRel = spec_paths_1.GLOBAL_SPECS_DIR;
1533
+ baseRel = spec_paths_2.GLOBAL_SPECS_DIR;
1533
1534
  }
1534
1535
  else {
1535
1536
  const m = stepId.match(/^feature-(.+)-prompt$/);
@@ -1803,7 +1804,7 @@ ${singlePrompt}`);
1803
1804
  // v8.3.237+: 拦截条件化为「有功能模块的项目」——内容类归 {feature}/overview/,
1804
1805
  // 全局 overview/REQUIREMENT.md 由 split 阶段从功能模块聚合生成,AI 不应直接写。
1805
1806
  // 无功能模块的老项目放行(内容类唯一容器就是 overview/)。
1806
- const featureCount = iterDir ? (await (0, spec_paths_1.parseFeatureList)(iterDir)).length : 0;
1807
+ const featureCount = iterDir ? (await (0, spec_paths_2.parseFeatureList)(iterDir)).length : 0;
1807
1808
  if (featureCount > 0) {
1808
1809
  for (const filename of Object.keys(docs)) {
1809
1810
  if (filename === 'overview/REQUIREMENT.md' || filename === 'REQUIREMENT.md') {
@@ -1843,7 +1844,7 @@ ${singlePrompt}`);
1843
1844
  // 全局级:写入 .speccore/GLOBAL/(与 platforms/ 同级,非迭代目录)
1844
1845
  const globalBaseDir = (0, path_1.join)(_projectRoot, '.speccore', 'GLOBAL');
1845
1846
  await (0, fs_extra_1.ensureDir)(globalBaseDir);
1846
- const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
1847
+ const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
1847
1848
  for (const [filename, content] of Object.entries(docs)) {
1848
1849
  // PATTERNS/ 文件特殊处理 → 写入 .speccore/PATTERNS/
1849
1850
  if (filename.startsWith('PATTERNS/')) {
@@ -1943,10 +1944,10 @@ ${singlePrompt}`);
1943
1944
  // v6.69.2+: 增加端名白名单校验,防止 AI 创建非法目录
1944
1945
  const specDir = (0, path_1.join)(iterDir, '020-specs');
1945
1946
  await (0, fs_extra_1.ensureDir)(specDir);
1946
- const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
1947
- const validPlatforms = new Set([spec_paths_1.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_1.parsePlatformList)())]);
1947
+ const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
1948
+ const validPlatforms = new Set([spec_paths_2.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_2.parsePlatformList)())]);
1948
1949
  // v8.3.21+: 按需求文档名组织
1949
- const features = await (0, spec_paths_1.parseFeatureList)(iterDir);
1950
+ const features = await (0, spec_paths_2.parseFeatureList)(iterDir);
1950
1951
  if (features.length > 0) {
1951
1952
  logger_1.logger.info(` 📂 检测到 ${features.length} 个功能模块,按需求文档名路由文档`);
1952
1953
  }
@@ -2002,16 +2003,16 @@ ${singlePrompt}`);
2002
2003
  }
2003
2004
  // v7.4.0+: 文件名归一化——剥离 AI 可能携带的 overview/ 前缀,防止嵌套目录
2004
2005
  let cleanFilename = filename;
2005
- if (cleanFilename.startsWith(`${spec_paths_1.GLOBAL_SPECS_DIR}/`)) {
2006
- cleanFilename = cleanFilename.slice(`${spec_paths_1.GLOBAL_SPECS_DIR}/`.length);
2006
+ if (cleanFilename.startsWith(`${spec_paths_2.GLOBAL_SPECS_DIR}/`)) {
2007
+ cleanFilename = cleanFilename.slice(`${spec_paths_2.GLOBAL_SPECS_DIR}/`.length);
2007
2008
  }
2008
2009
  // v8.3.21+: 按需求文档名组织的目录结构路由
2009
2010
  if (cleanFilename.includes('/')) {
2010
2011
  const parts = cleanFilename.split('/');
2011
2012
  const firstPart = parts[0];
2012
2013
  // 全局文档:overview/xxx → 写入 020-specs/overview/
2013
- if (firstPart === spec_paths_1.GLOBAL_SPECS_DIR) {
2014
- const targetDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
2014
+ if (firstPart === spec_paths_2.GLOBAL_SPECS_DIR) {
2015
+ const targetDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
2015
2016
  await (0, fs_extra_1.ensureDir)(targetDir);
2016
2017
  const fp = (0, path_1.join)(targetDir, parts.slice(1).join('/'));
2017
2018
  if (!options.merge && !(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
@@ -2075,7 +2076,7 @@ ${singlePrompt}`);
2075
2076
  // 如果 AI 输出 {端}/{文件},尝试回退到功能模块路由
2076
2077
  let targetDir;
2077
2078
  if (globalSet.has(cleanFilename)) {
2078
- targetDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
2079
+ targetDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
2079
2080
  }
2080
2081
  else if (platformDir && validPlatforms.has(platformDir)) {
2081
2082
  // 检查是否应归入功能模块(骨架文件已按功能模块创建)
@@ -2083,7 +2084,7 @@ ${singlePrompt}`);
2083
2084
  try {
2084
2085
  const specEntries = await (0, fs_extra_2.readdir)(specDir, { withFileTypes: true });
2085
2086
  for (const entry of specEntries) {
2086
- if (!entry.isDirectory() || entry.name === spec_paths_1.GLOBAL_SPECS_DIR || validPlatforms.has(entry.name))
2087
+ if (!entry.isDirectory() || entry.name === spec_paths_2.GLOBAL_SPECS_DIR || validPlatforms.has(entry.name))
2087
2088
  continue;
2088
2089
  const platformInFeature = (0, path_1.join)(specDir, entry.name, platformDir);
2089
2090
  if (await (0, fs_extra_1.pathExists)(platformInFeature)) {
@@ -2291,7 +2292,7 @@ ${singlePrompt}`);
2291
2292
  }
2292
2293
  if (!isGlobalScope) {
2293
2294
  // v8.3.237+: 与 JSON 通道一致——有功能模块的项目拦截,无功能模块的老项目放行
2294
- const featureCount = iterDir ? (await (0, spec_paths_1.parseFeatureList)(iterDir)).length : 0;
2295
+ const featureCount = iterDir ? (await (0, spec_paths_2.parseFeatureList)(iterDir)).length : 0;
2295
2296
  if (featureCount > 0) {
2296
2297
  for (const docName of docBlocks.keys()) {
2297
2298
  if (docName === 'overview/REQUIREMENT.md' || docName === 'REQUIREMENT.md') {
@@ -2342,7 +2343,7 @@ ${singlePrompt}`);
2342
2343
  else if (isGlobalScope) {
2343
2344
  const globalBaseDir = (0, path_1.join)(_projectRoot, '.speccore', 'GLOBAL');
2344
2345
  await (0, fs_extra_1.ensureDir)(globalBaseDir);
2345
- const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
2346
+ const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
2346
2347
  for (const [filename, content] of Object.entries(docs)) {
2347
2348
  if (filename.startsWith('PATTERNS/')) {
2348
2349
  const patternsDir = (0, path_1.join)(_projectRoot, '.speccore', 'PATTERNS');
@@ -2403,8 +2404,8 @@ ${singlePrompt}`);
2403
2404
  else {
2404
2405
  const specDir = (0, path_1.join)(iterDir, '020-specs');
2405
2406
  await (0, fs_extra_1.ensureDir)(specDir);
2406
- const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
2407
- const validPlatforms = new Set([spec_paths_1.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_1.parsePlatformList)())]);
2407
+ const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
2408
+ const validPlatforms = new Set([spec_paths_2.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_2.parsePlatformList)())]);
2408
2409
  let skippedCount = 0;
2409
2410
  for (const [filename, content] of Object.entries(docs)) {
2410
2411
  if (filename.startsWith('020-specs/requirements/')) {
@@ -2453,8 +2454,8 @@ ${singlePrompt}`);
2453
2454
  continue;
2454
2455
  }
2455
2456
  let cleanFilename = filename;
2456
- if (cleanFilename.startsWith(`${spec_paths_1.GLOBAL_SPECS_DIR}/`)) {
2457
- cleanFilename = cleanFilename.slice(`${spec_paths_1.GLOBAL_SPECS_DIR}/`.length);
2457
+ if (cleanFilename.startsWith(`${spec_paths_2.GLOBAL_SPECS_DIR}/`)) {
2458
+ cleanFilename = cleanFilename.slice(`${spec_paths_2.GLOBAL_SPECS_DIR}/`.length);
2458
2459
  }
2459
2460
  if (cleanFilename.includes('/')) {
2460
2461
  const prefix = cleanFilename.split('/')[0];
@@ -2473,7 +2474,7 @@ ${singlePrompt}`);
2473
2474
  // 若 cleanFilename 含合法端名前缀(如 api/TECH.md),写入对应端目录
2474
2475
  let targetDir;
2475
2476
  if (globalSet.has(cleanFilename)) {
2476
- targetDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
2477
+ targetDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
2477
2478
  }
2478
2479
  else if (platformDir && validPlatforms.has(platformDir)) {
2479
2480
  targetDir = (0, path_1.join)(specDir, platformDir);
@@ -2588,7 +2589,7 @@ ${singlePrompt}`);
2588
2589
  else if (!hasClarifyMarkers) {
2589
2590
  // 迭代级:写 020-specs/overview/(迭代综合文档,v6.78.0+)
2590
2591
  const specDir = (0, path_1.join)(iterDir, '020-specs');
2591
- const globalDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
2592
+ const globalDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
2592
2593
  await (0, fs_extra_1.ensureDir)(globalDir);
2593
2594
  const iterAnalysisPath = (0, path_1.join)(globalDir, 'ANALYSIS.md');
2594
2595
  if (await (0, task_utils_1.shouldOverwrite)(iterAnalysisPath, !!options.interactive)) {
@@ -2633,7 +2634,7 @@ ${singlePrompt}`);
2633
2634
  // ── v6.64.0+: Phase 1 完成后自动触发 Phase 2(仅多端项目,非 Pipeline 模式)──
2634
2635
  if (!options.phase && !isTaskLevel && options.iteration) {
2635
2636
  // 检查是否有多个端
2636
- const platforms = await (0, spec_paths_1.parsePlatformList)();
2637
+ const platforms = await (0, spec_paths_2.parsePlatformList)();
2637
2638
  // 只有多端项目(≥2 个端)才需要分两阶段:
2638
2639
  // - Phase 1: 生成综合文档(overview/REQUIREMENT.md、ANALYSIS.md、DEPS.md)
2639
2640
  // - Phase 2: 生成各端专属文档({端}/TECH.md、TEST.md、UI_SPEC.md)
@@ -2726,9 +2727,9 @@ async function sanitizeSpecDirectories(iterDir) {
2726
2727
  const specDir = (0, path_1.join)(iterDir, '020-specs');
2727
2728
  if (!await (0, fs_extra_1.pathExists)(specDir))
2728
2729
  return;
2729
- const platforms = await (0, spec_paths_1.parsePlatformList)();
2730
- const validDirs = new Set([spec_paths_1.GLOBAL_SPECS_DIR, ...platforms]);
2731
- const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
2730
+ const platforms = await (0, spec_paths_2.parsePlatformList)();
2731
+ const validDirs = new Set([spec_paths_2.GLOBAL_SPECS_DIR, ...platforms]);
2732
+ const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
2732
2733
  const entries = await (0, fs_extra_2.readdir)(specDir);
2733
2734
  let illegalDirCount = 0;
2734
2735
  let orphanFileCount = 0;
@@ -2739,17 +2740,17 @@ async function sanitizeSpecDirectories(iterDir) {
2739
2740
  if (entryStat.isDirectory()) {
2740
2741
  // 检测遗留的 global/ 目录(v6.78.0+ 已改名为 overview/)
2741
2742
  if (entry === 'global') {
2742
- logger_1.logger.warn(`⚠️ 检测到遗留 global/ 目录(v6.78.0+ 已改名为 ${spec_paths_1.GLOBAL_SPECS_DIR}/)`);
2743
+ logger_1.logger.warn(`⚠️ 检测到遗留 global/ 目录(v6.78.0+ 已改名为 ${spec_paths_2.GLOBAL_SPECS_DIR}/)`);
2743
2744
  // 将 global/ 中的文件迁移到 overview/
2744
2745
  const globalFiles = await (0, fs_extra_2.readdir)(entryPath);
2745
- const overviewDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
2746
+ const overviewDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
2746
2747
  await (0, fs_extra_1.ensureDir)(overviewDir);
2747
2748
  for (const f of globalFiles) {
2748
2749
  const src = (0, path_1.join)(entryPath, f);
2749
2750
  const dest = (0, path_1.join)(overviewDir, f);
2750
2751
  if (!await (0, fs_extra_1.pathExists)(dest)) {
2751
2752
  await (0, fs_extra_1.rename)(src, dest);
2752
- logger_1.logger.info(` 📦 迁移: global/${f} → ${spec_paths_1.GLOBAL_SPECS_DIR}/${f}`);
2753
+ logger_1.logger.info(` 📦 迁移: global/${f} → ${spec_paths_2.GLOBAL_SPECS_DIR}/${f}`);
2753
2754
  }
2754
2755
  }
2755
2756
  // 删除空的 global/ 目录
@@ -2781,12 +2782,12 @@ async function sanitizeSpecDirectories(iterDir) {
2781
2782
  else if (entryStat.isFile()) {
2782
2783
  // v8.3.11+: 根目录散落的文件 → 检查是否应归入 overview/(不限于 .md)
2783
2784
  if (globalSet.has(entry)) {
2784
- const overviewDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
2785
+ const overviewDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
2785
2786
  await (0, fs_extra_1.ensureDir)(overviewDir);
2786
2787
  const dest = (0, path_1.join)(overviewDir, entry);
2787
2788
  if (!await (0, fs_extra_1.pathExists)(dest)) {
2788
2789
  await (0, fs_extra_1.rename)(entryPath, dest);
2789
- logger_1.logger.warn(`⚠️ 散落文件已归位: 020-specs/${entry} → ${spec_paths_1.GLOBAL_SPECS_DIR}/${entry}`);
2790
+ logger_1.logger.warn(`⚠️ 散落文件已归位: 020-specs/${entry} → ${spec_paths_2.GLOBAL_SPECS_DIR}/${entry}`);
2790
2791
  orphanFileCount++;
2791
2792
  }
2792
2793
  else {
@@ -2806,9 +2807,9 @@ async function sanitizeSpecDirectories(iterDir) {
2806
2807
  if (illegalDirCount > 0)
2807
2808
  logger_1.logger.warn(` ⚠️ ${illegalDirCount} 个非法目录已归档(AI 绕过 --apply 创建)`);
2808
2809
  if (orphanFileCount > 0)
2809
- logger_1.logger.warn(` ⚠️ ${orphanFileCount} 个散落文件已归位到 ${spec_paths_1.GLOBAL_SPECS_DIR}/`);
2810
+ logger_1.logger.warn(` ⚠️ ${orphanFileCount} 个散落文件已归位到 ${spec_paths_2.GLOBAL_SPECS_DIR}/`);
2810
2811
  if (legacyGlobalMigrated)
2811
- logger_1.logger.info(` 📦 遗留 global/ 已迁移到 ${spec_paths_1.GLOBAL_SPECS_DIR}/`);
2812
+ logger_1.logger.info(` 📦 遗留 global/ 已迁移到 ${spec_paths_2.GLOBAL_SPECS_DIR}/`);
2812
2813
  logger_1.logger.info(` ✅ 当前合法目录: ${Array.from(validDirs).join(', ')}`);
2813
2814
  }
2814
2815
  }
@@ -2818,7 +2819,7 @@ async function sanitizeSpecDirectories(iterDir) {
2818
2819
  async function generateIterationSpecDocs(iteration) {
2819
2820
  const iterDir = await (0, context_1.getIterationDir)(iteration);
2820
2821
  const specDir = (0, path_1.join)(iterDir, '020-specs');
2821
- const globalDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
2822
+ const globalDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
2822
2823
  await (0, fs_extra_1.ensureDir)(globalDir);
2823
2824
  const now = new Date().toISOString().split('T')[0];
2824
2825
  // 综合文档模板 → 写入 global/ 子目录(v6.41.0+)
@@ -3049,6 +3050,9 @@ function parseArgv(options) {
3049
3050
  [['--output', '-o'], (v) => { options.output = v; }],
3050
3051
  [['--depth'], (v) => { options.depth = v; }],
3051
3052
  [['--feature'], (v) => { options.feature = v; }],
3053
+ // v8.3.248+: --unit 指定功能单元(支持逗号分隔多个),此前多单元 prompt 指示的
3054
+ // 「analyze --unit {id} --apply」提交命令因未注册该选项而静默失效
3055
+ [['--unit'], (v) => { options.unit = v; }],
3052
3056
  [['--phase'], (v) => { options.phase = v; }],
3053
3057
  [['--out-dir'], (v) => { options.outDir = v; }],
3054
3058
  ];
@@ -4410,10 +4414,10 @@ async function buildMultiDocPrompt(command, ctx, options, clarifyCtx) {
4410
4414
  };
4411
4415
  // 4a: 追加各前端端的需求文档
4412
4416
  if (subLayerTarget === '4a') {
4413
- const allPlatforms = await (0, spec_paths_1.parsePlatformList)();
4417
+ const allPlatforms = await (0, spec_paths_2.parsePlatformList)();
4414
4418
  const frontendPlatforms = allPlatforms.filter(p => {
4415
4419
  const pt = (async () => { try {
4416
- const m = await (0, spec_paths_1.parsePlatformTypes)();
4420
+ const m = await (0, spec_paths_2.parsePlatformTypes)();
4417
4421
  return m.get(p);
4418
4422
  }
4419
4423
  catch {
@@ -5029,11 +5033,11 @@ sequenceDiagram
5029
5033
  const iterDir = await (0, context_1.getIterationDir)(ctx.iteration);
5030
5034
  if (iterDir) {
5031
5035
  const specDir = (0, path_1.join)(iterDir, '020-specs');
5032
- const platforms = await (0, spec_paths_1.parsePlatformList)();
5036
+ const platforms = await (0, spec_paths_2.parsePlatformList)();
5033
5037
  // v8.3.237+: 有功能模块的项目用功能模块 manifest(内容类骨架在 {feature}/overview/,
5034
5038
  // 全局 overview/ 只含索引类),避免旧 manifest 把 overview/REQUIREMENT.md 当
5035
5039
  // 「下一个待填充」但 --apply 又按路由表拦截 → 单文档模式死锁。
5036
- const features = await (0, spec_paths_1.parseFeatureList)(iterDir);
5040
+ const features = await (0, spec_paths_2.parseFeatureList)(iterDir);
5037
5041
  const manifest = features.length > 0
5038
5042
  ? (0, spec_skeleton_1.computeFeatureBasedAnalyzeManifest)(features, platforms, undefined, ctx.iteration)
5039
5043
  : (0, spec_skeleton_1.computeAnalyzeManifest)(platforms, undefined, ctx.iteration);
@@ -5082,14 +5086,14 @@ sequenceDiagram
5082
5086
  // v8.1.1+: DOC_MATRIX 交叉校验 — 检查骨架清单之外是否有遗漏的文档
5083
5087
  // v8.3.237+: 有功能模块的项目全局 overview/ 只应有索引类文档,
5084
5088
  // 内容类(REQUIREMENT.md 等)归 {feature}/overview/,不得在此补建骨架
5085
- const hasFeatures = (await (0, spec_paths_1.parseFeatureList)(iterDir)).length > 0;
5089
+ const hasFeatures = (await (0, spec_paths_2.parseFeatureList)(iterDir)).length > 0;
5086
5090
  const expectedGlobalDocs = (hasFeatures
5087
5091
  ? spec_skeleton_1.PHASE1_INDEX_DOC_NAMES
5088
5092
  : GLOBAL_DOCS)
5089
5093
  .filter(d => includeDocs.includes(d));
5090
5094
  const missingDocs = [];
5091
5095
  for (const doc of expectedGlobalDocs) {
5092
- const docPath = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR, doc);
5096
+ const docPath = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR, doc);
5093
5097
  if (!await (0, fs_extra_1.pathExists)(docPath)) {
5094
5098
  missingDocs.push(doc);
5095
5099
  }
@@ -5340,7 +5344,7 @@ status: "clarified"
5340
5344
  }
5341
5345
  // ── v6.67.0+: 在 prompt 开头就明确完整工作流程 ──
5342
5346
  if (!ctx.phase && !isTask) {
5343
- const platforms = await (0, spec_paths_1.parsePlatformList)();
5347
+ const platforms = await (0, spec_paths_2.parsePlatformList)();
5344
5348
  if (platforms.length >= 2) {
5345
5349
  prompt += `## 📋 完整工作流程(两阶段分析)\n\n`;
5346
5350
  prompt += `本项目检测到有 ${platforms.length} 个端(${platforms.join('、')}),需要分两阶段执行:\n\n`;
@@ -5362,10 +5366,10 @@ status: "clarified"
5362
5366
  try {
5363
5367
  const iterDir = await (0, context_1.getIterationDir)(iter);
5364
5368
  if (iterDir) {
5365
- const platforms = await (0, spec_paths_1.parsePlatformList)();
5369
+ const platforms = await (0, spec_paths_2.parsePlatformList)();
5366
5370
  const phase = options?.phase;
5367
5371
  // v8.3.34: 按需求文档名组织,prompt 中的文件清单也要用功能模块结构
5368
- const features = await (0, spec_paths_1.parseFeatureList)(iterDir);
5372
+ const features = await (0, spec_paths_2.parseFeatureList)(iterDir);
5369
5373
  const manifest = (0, spec_skeleton_1.computeFeatureBasedAnalyzeManifest)(features, platforms, phase, iter);
5370
5374
  const specDir = (0, path_1.join)(iterDir, '020-specs');
5371
5375
  prompt += (0, spec_skeleton_1.buildSkeletonFileList)(manifest, specDir);
@@ -5636,8 +5640,19 @@ status: "clarified"
5636
5640
  // v8.3.160+: 大项目按端分段读取,避免一次性加载全量结构化数据
5637
5641
  prompt += ` - 先 Read \`.speccore/cache/structured-data.json\` 的 stats 部分(了解各端规模)\n`;
5638
5642
  prompt += ` - **按需读取各端分段文件**:\`.speccore/cache/structured-data.{端名}.json\`(只读取本功能模块涉及的端)\n`;
5639
- prompt += ` - 再按需 Read 各端源码中与本功能模块相关的关键文件(从 CONSTITUTION.md 获取源码路径)\n`;
5640
- prompt += ` - 重点关注:接口定义、数据模型、业务逻辑实现、状态管理\n\n`;
5643
+ // v8.3.248+: 直接注入各端工程路径表(此前仅提示 AI「从 CONSTITUTION.md 获取源码路径」,
5644
+ // AI 需自行翻表定位;现在直接给出,减少一步间接查找、杜绝瞎猜)
5645
+ try {
5646
+ const projectInfoMap = await (0, spec_paths_1.parseProjectInfo)();
5647
+ if (projectInfoMap.size > 0) {
5648
+ prompt += ` - **各端工程路径**(结合源码分析时按此定位,CONSTITUTION.md 工程表权威):\n`;
5649
+ for (const [identifier, info] of projectInfoMap) {
5650
+ prompt += ` - \`${identifier}\`(${info.projectType || '-'})→ \`${info.srcPath}\`\n`;
5651
+ }
5652
+ }
5653
+ }
5654
+ catch { /* 路径解析失败不阻断分析 */ }
5655
+ prompt += ` - 再按需 Read 各端源码中与本功能模块相关的关键文件,重点关注:接口定义、数据模型、业务逻辑实现、状态管理\n\n`;
5641
5656
  }
5642
5657
  prompt += `3. 读取迭代需求文档(按功能模块组织):\n`;
5643
5658
  prompt += ` a. 先读 010-requirements/INDEX.md — 获取功能模块清单\n`;
@@ -5729,7 +5744,7 @@ status: "clarified"
5729
5744
  prompt += ` - INTERACTION_MAP.md 是前后端开发者的共同参考,补全产品文档中隐含的技术交互\n`;
5730
5745
  // 注入工程类型信息(v6.49.0+)
5731
5746
  // v8.3.160+: 精简为路径引用,具体维度让 AI 从 CONSTITUTION.md 读取
5732
- const platformTypes = await (0, spec_paths_1.parsePlatformTypes)();
5747
+ const platformTypes = await (0, spec_paths_2.parsePlatformTypes)();
5733
5748
  if (platformTypes.size > 0) {
5734
5749
  prompt += `8. **工程类型识别**:CONSTITUTION.md 已配置各端的工程类型,请 Read 后据此生成针对性内容\n`;
5735
5750
  prompt += ` | 工程标识 | 工程类型 |\n`;
@@ -5915,7 +5930,7 @@ status: "clarified"
5915
5930
  }
5916
5931
  else {
5917
5932
  prompt += `> 🎉 这是最后一个骨架文件!填充后所有迭代级分析文档将完整。\n`;
5918
- const platforms = await (0, spec_paths_1.parsePlatformList)();
5933
+ const platforms = await (0, spec_paths_2.parsePlatformList)();
5919
5934
  const hasPlatformUnfilled = perDocStatus.unfilled.some(p => p.includes('/') && !p.startsWith('overview/'));
5920
5935
  if (platforms.length >= 2 && hasPlatformUnfilled) {
5921
5936
  prompt += `> 下一步: 生成各端专属文档 → \`speccore analyze --prompt -I ${iter} --phase 2\`\n`;
@@ -5990,8 +6005,8 @@ status: "clarified"
5990
6005
  // ── v6.74.0+: 流式全局分析 Prompt 生成 ──
5991
6006
  async function buildStreamingGlobalPrompt(command, ctx, options) {
5992
6007
  const iter = ctx.iteration || '当前迭代';
5993
- const platforms = await (0, spec_paths_1.parsePlatformList)();
5994
- const platformTypes = await (0, spec_paths_1.parsePlatformTypes)();
6008
+ const platforms = await (0, spec_paths_2.parsePlatformList)();
6009
+ const platformTypes = await (0, spec_paths_2.parsePlatformTypes)();
5995
6010
  // 确定当前 Phase
5996
6011
  let currentPhase = 'phase0-scan';
5997
6012
  if (options?.streamingPhase) {
@@ -6094,7 +6109,7 @@ async function buildStreamingGlobalPrompt(command, ctx, options) {
6094
6109
  // ── v6.69.0+: 契约先行 Prompt 生成(增强策略一)──
6095
6110
  async function buildContractFirstPrompt(iteration) {
6096
6111
  const iterDir = await (0, context_1.getIterationDir)(iteration);
6097
- const globalDir = (0, path_1.join)(iterDir, '020-specs', spec_paths_1.GLOBAL_SPECS_DIR);
6112
+ const globalDir = (0, path_1.join)(iterDir, '020-specs', spec_paths_2.GLOBAL_SPECS_DIR);
6098
6113
  let prompt = `\n# 任务: 跨端 API 契约定义(契约先行阶段)\n\n`;
6099
6114
  prompt += `## 背景\n\n`;
6100
6115
  prompt += `Phase 1 迭代分析已完成。现在需要在各端开始专属技术方案分析之前,**先定义跨端 API 契约**。\n\n`;
@@ -6174,7 +6189,7 @@ async function buildContractFirstPrompt(iteration) {
6174
6189
  // ── v6.49.9+: 扫描平铺的端目录,返回所有子任务目录路径 ──
6175
6190
  async function getSubtaskDirs(taskDir) {
6176
6191
  const result = [];
6177
- const platformList = await (0, spec_paths_1.parsePlatformList)();
6192
+ const platformList = await (0, spec_paths_2.parsePlatformList)();
6178
6193
  for (const platform of platformList) {
6179
6194
  const platDir = (0, path_1.join)(taskDir, platform);
6180
6195
  if (!(await (0, fs_extra_1.pathExists)(platDir)))