speccore 8.3.15 → 8.3.17

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":"AAoEA,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC;IACxC,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAqoD3E;AAi/GD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,eAAe,kCAAkC,CAAC;AAE/D,oBAAoB;AACpB,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAmDrF;AAED,kBAAkB;AAClB,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/F;AAED,iBAAiB;AACjB,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAYjF;AAED,iBAAiB;AACjB,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAI/G;AAED,oCAAoC;AACpC,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,aAAa,EAAE,EACtB,QAAQ,EAAE,YAAY,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,CAmFjB;AAED,2BAA2B;AAC3B,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,aAAa,EACnB,QAAQ,EAAE,aAAa,EAAE,GACxB,OAAO,CAAC,MAAM,CAAC,CAwEjB"}
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"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"}
@@ -461,17 +461,35 @@ async function analyzeCommand(options) {
461
461
  return;
462
462
  }
463
463
  }
464
- // 模糊匹配端名
464
+ // v8.3.16+: 模糊匹配端名,支持多段指定(逗号分隔)
465
465
  if (options.platform) {
466
- const resolved = await (0, platform_registry_1.resolvePlatform)(options.platform);
467
- if (resolved.error) {
468
- logger_1.logger.error(`❌ ${resolved.error}`);
466
+ const allPlatforms = await (0, spec_paths_1.parsePlatformList)();
467
+ const inputPlatforms = options.platform.split(',').map(p => p.trim()).filter(Boolean);
468
+ const resolvedPlatforms = [];
469
+ for (const input of inputPlatforms) {
470
+ const resolved = await (0, platform_registry_1.resolvePlatform)(input);
471
+ if (resolved.error) {
472
+ logger_1.logger.error(`❌ ${resolved.error}`);
473
+ return;
474
+ }
475
+ if (!resolved.exact) {
476
+ logger_1.logger.info(`📍 --platform ${input} → 匹配 ${resolved.resolved}`);
477
+ }
478
+ if (resolved.resolved) {
479
+ resolvedPlatforms.push(resolved.resolved);
480
+ }
481
+ }
482
+ // 过滤只保留在 CONSTITUTION.md 端列表中的端
483
+ const validPlatforms = resolvedPlatforms.filter(p => allPlatforms.includes(p));
484
+ if (validPlatforms.length === 0) {
485
+ logger_1.logger.error(`❌ 指定的端均不在 CONSTITUTION.md 端列表中。可用端: ${allPlatforms.join(', ')}`);
469
486
  return;
470
487
  }
471
- if (!resolved.exact) {
472
- logger_1.logger.info(`📍 --platform ${options.platform} 匹配 ${resolved.resolved}`);
488
+ if (validPlatforms.length < resolvedPlatforms.length) {
489
+ const skipped = resolvedPlatforms.filter(p => !allPlatforms.includes(p));
490
+ logger_1.logger.warn(`⚠️ 跳过不在端列表中的端: ${skipped.join(', ')}`);
473
491
  }
474
- options.platform = resolved.resolved;
492
+ options.platform = validPlatforms.join(',');
475
493
  }
476
494
  // 命令前索引新鲜度检查(非阻塞)
477
495
  // v6.97.0+ 修复:全局分析时不检查迭代级索引,避免误导用户
@@ -656,13 +674,44 @@ async function analyzeCommand(options) {
656
674
  // v8.0.0+: 骨架优先架构 — CLI 预创建所有文件,AI 只覆盖内容
657
675
  const iterDirForSkeleton = await (0, context_1.getIterationDir)(iterForDirs);
658
676
  const specDir = (0, path_1.join)(iterDirForSkeleton, '020-specs');
659
- const platforms = await (0, spec_paths_1.parsePlatformList)();
677
+ let platforms = await (0, spec_paths_1.parsePlatformList)();
660
678
  const iterName = iterForDirs;
679
+ // v8.3.16+: 如果指定了 --platform,只创建指定端的骨架
680
+ if (options.platform) {
681
+ const specifiedPlatforms = options.platform.split(',').map(p => p.trim()).filter(Boolean);
682
+ const filtered = platforms.filter(p => specifiedPlatforms.includes(p));
683
+ if (filtered.length > 0) {
684
+ platforms = filtered;
685
+ logger_1.logger.info(`📍 按 --platform 过滤: 只为 ${platforms.join(', ')} 端创建骨架`);
686
+ }
687
+ else {
688
+ logger_1.logger.warn(`⚠️ --platform 指定的端不在 CONSTITUTION.md 列表中,将使用全部端`);
689
+ }
690
+ }
661
691
  // v7.4.3+ 兼容:先运行一次旧版 sanitize 迁移遗留文件(一次性)
662
692
  await sanitizeSpecDirectories(iterDirForSkeleton);
663
- // v8.0.0+: 计算 manifest 并生成骨架
693
+ // v8.3.17+: 检测新旧结构,自动选择 manifest 生成方式
664
694
  const phase = options.phase;
665
- const manifest = (0, spec_skeleton_1.computeAnalyzeManifest)(platforms, phase, iterName);
695
+ const isLegacy = await (0, spec_paths_1.isLegacySpecDir)(specDir);
696
+ let manifest;
697
+ if (isLegacy) {
698
+ // 旧结构:端平铺(020-specs/{端}/)
699
+ manifest = (0, spec_skeleton_1.computeAnalyzeManifest)(platforms, phase, iterName);
700
+ logger_1.logger.info(`📂 使用旧结构(端平铺): 020-specs/{端}/`);
701
+ }
702
+ else {
703
+ // 新结构:按需求文档名组织(020-specs/{需求名}/)
704
+ const features = await (0, spec_paths_1.parseFeatureList)(iterDirForSkeleton);
705
+ if (features.length > 0) {
706
+ manifest = (0, spec_skeleton_1.computeFeatureBasedAnalyzeManifest)(features, platforms, phase, iterName);
707
+ logger_1.logger.info(`📂 使用新结构(按需求文档组织): ${features.length} 个功能模块`);
708
+ }
709
+ else {
710
+ // 未检测到需求文档,回退到旧结构
711
+ manifest = (0, spec_skeleton_1.computeAnalyzeManifest)(platforms, phase, iterName);
712
+ logger_1.logger.info(`📂 未检测到需求文档,回退到旧结构(端平铺)`);
713
+ }
714
+ }
666
715
  const skeletonResult = await (0, spec_skeleton_1.generateSkeleton)(specDir, manifest);
667
716
  if (skeletonResult.created.length > 0) {
668
717
  logger_1.logger.info(`🦴 已创建 ${skeletonResult.created.length} 个文件骨架: ${skeletonResult.created.join(', ')}`);
@@ -1049,6 +1098,12 @@ async function analyzeCommand(options) {
1049
1098
  await (0, fs_extra_1.ensureDir)(specDir);
1050
1099
  const globalSet = new Set(spec_paths_1.GLOBAL_SPEC_FILES);
1051
1100
  const validPlatforms = new Set([spec_paths_1.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_1.parsePlatformList)())]);
1101
+ // v8.3.17+: 检测是否为新结构(按需求文档组织)
1102
+ const isFeatureBased = !await (0, spec_paths_1.isLegacySpecDir)(specDir);
1103
+ const features = isFeatureBased ? await (0, spec_paths_1.parseFeatureList)(iterDir) : [];
1104
+ if (isFeatureBased && features.length > 0) {
1105
+ logger_1.logger.info(` 📂 新结构检测到 ${features.length} 个功能模块,按需求文档名路由文档`);
1106
+ }
1052
1107
  let skippedCount = 0;
1053
1108
  for (const [filename, content] of Object.entries(docs)) {
1054
1109
  // v8.2.0+: 需求文档写入黄金需求目录 020-specs/requirements/
@@ -1105,6 +1160,50 @@ async function analyzeCommand(options) {
1105
1160
  if (cleanFilename.startsWith(`${spec_paths_1.GLOBAL_SPECS_DIR}/`)) {
1106
1161
  cleanFilename = cleanFilename.slice(`${spec_paths_1.GLOBAL_SPECS_DIR}/`.length);
1107
1162
  }
1163
+ // v8.3.17+: 新结构路由 — 按需求文档名组织的目录结构
1164
+ if (isFeatureBased && cleanFilename.includes('/')) {
1165
+ const parts = cleanFilename.split('/');
1166
+ const firstPart = parts[0];
1167
+ // 全局文档:overview/xxx → 写入 020-specs/overview/
1168
+ if (firstPart === spec_paths_1.GLOBAL_SPECS_DIR) {
1169
+ const targetDir = (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR);
1170
+ await (0, fs_extra_1.ensureDir)(targetDir);
1171
+ const fp = (0, path_1.join)(targetDir, parts.slice(1).join('/'));
1172
+ if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
1173
+ logger_1.logger.info(` ⏭️ 跳过: ${filename}`);
1174
+ continue;
1175
+ }
1176
+ const bk = await (0, task_utils_1.backupWithTimestamp)(fp);
1177
+ if (bk) {
1178
+ backups.push(bk);
1179
+ logger_1.logger.info(` 📦 ${filename} 旧版已备份: ${bk.split('/').pop()}`);
1180
+ }
1181
+ await (0, fs_extra_1.writeFile)(fp, content);
1182
+ count++;
1183
+ continue;
1184
+ }
1185
+ // 功能模块路径:验证第二级是 overview/ 或合法端名
1186
+ if (!validPlatforms.has(firstPart) && parts.length >= 2) {
1187
+ const secondPart = parts[1];
1188
+ if (secondPart === 'overview' || validPlatforms.has(secondPart)) {
1189
+ const targetDir = (0, path_1.join)(specDir, ...parts.slice(0, -1));
1190
+ await (0, fs_extra_1.ensureDir)(targetDir);
1191
+ const fp = (0, path_1.join)(targetDir, parts[parts.length - 1]);
1192
+ if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
1193
+ logger_1.logger.info(` ⏭️ 跳过: ${filename}`);
1194
+ continue;
1195
+ }
1196
+ const bk = await (0, task_utils_1.backupWithTimestamp)(fp);
1197
+ if (bk) {
1198
+ backups.push(bk);
1199
+ logger_1.logger.info(` 📦 ${filename} 旧版已备份: ${bk.split('/').pop()}`);
1200
+ }
1201
+ await (0, fs_extra_1.writeFile)(fp, content);
1202
+ count++;
1203
+ continue;
1204
+ }
1205
+ }
1206
+ }
1108
1207
  // v7.4.0+: 如果 AI 携带了其他未知目录前缀(非端名),剥离前缀只保留文件名
1109
1208
  if (cleanFilename.includes('/')) {
1110
1209
  const prefix = cleanFilename.split('/')[0];
@@ -1123,9 +1222,10 @@ async function analyzeCommand(options) {
1123
1222
  continue;
1124
1223
  }
1125
1224
  // 综合文档写入 overview/ 子目录,端专属文档写入 {端}/ 子目录
1225
+ // v8.3.16+: 多段指定时(如 api,web),不额外加目录前缀,由 AI 返回的路径决定
1126
1226
  const targetDir = globalSet.has(cleanFilename)
1127
1227
  ? (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR)
1128
- : options.platform ? (0, path_1.join)(specDir, options.platform) : specDir;
1228
+ : (options.platform && !options.platform.includes(',')) ? (0, path_1.join)(specDir, options.platform) : specDir;
1129
1229
  await (0, fs_extra_1.ensureDir)(targetDir);
1130
1230
  const fp = (0, path_1.join)(targetDir, cleanFilename);
1131
1231
  if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
@@ -1449,9 +1549,10 @@ async function analyzeCommand(options) {
1449
1549
  skippedCount++;
1450
1550
  continue;
1451
1551
  }
1552
+ // v8.3.16+: 多段指定时(如 api,web),不额外加目录前缀,由 AI 返回的路径决定
1452
1553
  const targetDir = globalSet.has(cleanFilename)
1453
1554
  ? (0, path_1.join)(specDir, spec_paths_1.GLOBAL_SPECS_DIR)
1454
- : options.platform ? (0, path_1.join)(specDir, options.platform) : specDir;
1555
+ : (options.platform && !options.platform.includes(',')) ? (0, path_1.join)(specDir, options.platform) : specDir;
1455
1556
  await (0, fs_extra_1.ensureDir)(targetDir);
1456
1557
  const fp = (0, path_1.join)(targetDir, cleanFilename);
1457
1558
  if (!(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
@@ -1846,6 +1947,20 @@ async function sanitizeSpecDirectories(iterDir) {
1846
1947
  }
1847
1948
  // 白名单校验:非 overview/ 且非端名的目录 → 非法
1848
1949
  if (!validDirs.has(entry)) {
1950
+ // v8.3.17+: 新结构下允许功能模块目录存在
1951
+ const isLegacy = await (0, spec_paths_1.isLegacySpecDir)(specDir);
1952
+ if (!isLegacy) {
1953
+ // 新结构:只归档明显非法的目录名(纯数字、特殊符号等)
1954
+ if (/^\d+$/.test(entry) || /^[\.\-\*_\s]+$/.test(entry)) {
1955
+ const archivedName = `${entry}.invalid-${Date.now()}`;
1956
+ await (0, fs_extra_1.rename)(entryPath, (0, path_1.join)(specDir, archivedName));
1957
+ logger_1.logger.warn(`⚠️ 非法目录已归档: 020-specs/${entry}/ → ${archivedName}/`);
1958
+ illegalDirCount++;
1959
+ }
1960
+ // 其他目录允许存在(功能模块目录)
1961
+ continue;
1962
+ }
1963
+ // 旧结构:非 overview/ 且非端名的目录 → 非法
1849
1964
  const archivedName = `${entry}.invalid-${Date.now()}`;
1850
1965
  await (0, fs_extra_1.rename)(entryPath, (0, path_1.join)(specDir, archivedName));
1851
1966
  logger_1.logger.warn(`⚠️ 非法目录已归档: 020-specs/${entry}/ → ${archivedName}/`);