speccore 6.38.0 → 6.40.0

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.
@@ -1642,13 +1642,65 @@ async function detectPlatformsFromConstitution() {
1642
1642
  * 用于 analyze --auto 模式,替代 init 创建的空模板。
1643
1643
  */
1644
1644
  async function generateSpecsFromRequirements(reqPaths, iteration, specDir) {
1645
- // 1. 读取所有需求内容
1645
+ // 1. 【增强】读取所有需求内容,并按端分类
1646
+ const platforms = await detectPlatformsFromConstitution(); // 【提前定义】
1646
1647
  const allContent = [];
1648
+ const platformFileMap = {}; // platform -> [file paths]
1649
+ const unclassifiedFiles = []; // 无法分类的文件
1647
1650
  for (const p of reqPaths) {
1648
1651
  if (await (0, fs_extra_1.pathExists)(p)) {
1649
- allContent.push(await (0, fs_extra_1.readFile)(p, 'utf-8'));
1652
+ const content = await (0, fs_extra_1.readFile)(p, 'utf-8');
1653
+ // 【新增】尝试从文件路径或内容中推断所属的端
1654
+ const inferredPlatform = inferPlatformFromPathOrContent(p, content, platforms);
1655
+ if (inferredPlatform) {
1656
+ // ✅ 端专属文档:不加入全局内容,只记录到平台映射
1657
+ if (!platformFileMap[inferredPlatform]) {
1658
+ platformFileMap[inferredPlatform] = [];
1659
+ }
1660
+ platformFileMap[inferredPlatform].push(p);
1661
+ logger_1.logger.info(` 📄 检测到 ${p} 属于 ${inferredPlatform} 端(端专属文档)`);
1662
+ }
1663
+ else {
1664
+ // ❓ 无法推断,检查是否为跨端通用文档
1665
+ const fileName = p.split(/[/\\]/).pop() || '';
1666
+ const isGlobalDoc = fileName.toUpperCase().includes('REQUIREMENT') ||
1667
+ fileName.toUpperCase().includes('INDEX') ||
1668
+ fileName.toUpperCase().includes('PRD');
1669
+ if (isGlobalDoc) {
1670
+ // ✅ 跨端通用文档:加入全局内容
1671
+ allContent.push(content);
1672
+ logger_1.logger.info(` 📄 ${fileName} 识别为跨端通用文档`);
1673
+ }
1674
+ else {
1675
+ // ⚠️ 无法分类的非通用文档
1676
+ unclassifiedFiles.push({ path: p, content });
1677
+ logger_1.logger.warn(` ⚠️ 无法自动识别 ${p} 所属的端`);
1678
+ }
1679
+ }
1650
1680
  }
1651
1681
  }
1682
+ // 【新增】如果有无法分类的文件,给出处理建议
1683
+ if (unclassifiedFiles.length > 0) {
1684
+ logger_1.logger.info('');
1685
+ logger_1.logger.info(` ❓ 发现 ${unclassifiedFiles.length} 个文档无法自动识别所属端`);
1686
+ logger_1.logger.info(` 💡 可用端列表: ${platforms.join(', ')}`);
1687
+ logger_1.logger.info('');
1688
+ for (const file of unclassifiedFiles) {
1689
+ const fileName = file.path.split(/[/\\]/).pop() || file.path;
1690
+ logger_1.logger.info(` 📄 ${fileName}`);
1691
+ // 检查是否有全局的 REQUIREMENT.md 或 INDEX.md,这些通常是跨端的
1692
+ if (fileName.toUpperCase().includes('REQUIREMENT') ||
1693
+ fileName.toUpperCase().includes('INDEX') ||
1694
+ fileName.toUpperCase().includes('PRD')) {
1695
+ logger_1.logger.info(` → 假设为跨端通用文档,内容将合并到全局分析中`);
1696
+ }
1697
+ else {
1698
+ logger_1.logger.info(` → 未指定端,将在生成端专属文档时使用占位符`);
1699
+ logger_1.logger.info(` → 后续可运行: speccore ask "将 ${fileName} 标注为 [端名] 端"`);
1700
+ }
1701
+ }
1702
+ logger_1.logger.info('');
1703
+ }
1652
1704
  const fullContent = allContent.join('\n\n---\n\n');
1653
1705
  if (fullContent.trim().length < 20) {
1654
1706
  logger_1.logger.warn(' ⚠️ 需求文档内容过少,无法生成有效 Spec 文件');
@@ -1661,8 +1713,26 @@ async function generateSpecsFromRequirements(reqPaths, iteration, specDir) {
1661
1713
  const businessRules = extractBusinessRules(fullContent);
1662
1714
  const uiPatterns = extractUIPatterns(fullContent);
1663
1715
  const archImpact = await analyzeArchitectureImpact(fullContent);
1664
- const platforms = await detectPlatformsFromConstitution();
1716
+ // const platforms 已在前面定义
1665
1717
  const now = new Date().toISOString().split('T')[0];
1718
+ // 3. 【新增】按端分割需求内容,为每个端单独提取专属信息
1719
+ // 【修复】不仅要分割全局内容,还要合并端专属文件的内容
1720
+ const platformContents = splitContentByPlatform(fullContent, platforms);
1721
+ // 合并端专属文件的内容
1722
+ for (const [platform, filePaths] of Object.entries(platformFileMap)) {
1723
+ let platformContent = platformContents[platform] || '';
1724
+ for (const filePath of filePaths) {
1725
+ const content = await (0, fs_extra_1.readFile)(filePath, 'utf-8');
1726
+ if (platformContent) {
1727
+ platformContent += '\n\n---\n\n' + content;
1728
+ }
1729
+ else {
1730
+ platformContent = content;
1731
+ }
1732
+ }
1733
+ platformContents[platform] = platformContent;
1734
+ }
1735
+ logger_1.logger.info(` 🔍 已按端分割需求内容: ${Object.keys(platformContents).length} 个端有专属内容`);
1666
1736
  // 3. 生成各 Spec 文件
1667
1737
  const files = [];
1668
1738
  // ── 全局文档(跨端通用)──
@@ -1703,7 +1773,7 @@ async function generateSpecsFromRequirements(reqPaths, iteration, specDir) {
1703
1773
  const platformDir = (0, path_1.join)(specDir, platform);
1704
1774
  await (0, fs_extra_1.ensureDir)(platformDir);
1705
1775
  // TECH.md — 该端技术方案
1706
- const techContent = buildTechSpecForPlatform(iteration, now, apis, dataModels, archImpact, platform, features, uiPatterns);
1776
+ const techContent = buildTechSpecForPlatform(iteration, now, apis, dataModels, archImpact, platform, features, uiPatterns, platformContents);
1707
1777
  await (0, fs_extra_1.writeFile)((0, path_1.join)(platformDir, 'TECH.md'), techContent);
1708
1778
  // TEST.md — 该端测试计划
1709
1779
  const testContent = buildTestSpecForPlatform(iteration, now, features, apis, platform);
@@ -1970,7 +2040,7 @@ function buildTechSpec(iter, now, apis, models, archImpact, platforms, features,
1970
2040
  /**
1971
2041
  * 生成指定端的技术方案(该端专属内容)
1972
2042
  */
1973
- function buildTechSpecForPlatform(iter, now, apis, models, archImpact, platform, features, uiPatterns) {
2043
+ function buildTechSpecForPlatform(iter, now, apis, models, archImpact, platform, features, uiPatterns, platformContents) {
1974
2044
  let md = `# ${platform} 端技术方案\n\n> 迭代: ${iter} | 端: ${platform} | 生成: ${now}\n\n`;
1975
2045
  if (isBackendPlatform(platform)) {
1976
2046
  // 后端专属内容
@@ -2003,7 +2073,12 @@ function buildTechSpecForPlatform(iter, now, apis, models, archImpact, platform,
2003
2073
  platformPages.forEach(p => { md += `| ${p.name} | \`${p.route}\` | ${p.desc} |\n`; });
2004
2074
  }
2005
2075
  else {
2076
+ // 【增强】添加智能填充提示
2006
2077
  md += `_待补充:从需求中提取 ${platform} 端的页面清单。_\n`;
2078
+ md += `\n> 💡 **如何填充**:\n`;
2079
+ md += `> 1. 运行 \`speccore ask "为 ${platform} 端补充页面结构和组件设计"\`\n`;
2080
+ md += `> 2. AI 会读取 010-requirements/ 中「${platform} 端需求」章节,自动提取页面清单\n`;
2081
+ md += `> 3. 或手动编辑此文件,参考需求文档中的功能描述\n`;
2007
2082
  }
2008
2083
  md += `\n## 2. 组件设计\n\n`;
2009
2084
  const platformComponents = uiPatterns.components.filter(c => c.page.includes(platform) || c.type.includes(platform));
@@ -2384,4 +2459,131 @@ function guessModule(apiPath, features) {
2384
2459
  }
2385
2460
  return segments[1] || '—';
2386
2461
  }
2462
+ // ============================================================
2463
+ // 【新增】按端分割需求内容
2464
+ // ============================================================
2465
+ /**
2466
+ * 将需求文档按端分割,提取每个端的专属内容
2467
+ * @param fullContent 完整的需求文档内容
2468
+ * @param platforms CONSTITUTION.md 定义的端列表
2469
+ * @returns Record<platform, content> 每个端的专属内容
2470
+ */
2471
+ function splitContentByPlatform(fullContent, platforms) {
2472
+ const result = {};
2473
+ const lines = fullContent.split('\n');
2474
+ // 1. 识别端标题的正则模式(支持多种写法)
2475
+ const platformPatterns = platforms.map(p => ({
2476
+ platform: p,
2477
+ // 匹配: "## APP 端需求" / "## H5端需求" / "## Admin 端" / "## miniapp"
2478
+ regex: new RegExp(`^#{1,4}\\s*.*?(?:${p}|${p.toUpperCase()}|${p.charAt(0).toUpperCase() + p.slice(1)}).*?(?:端|需求|$)`, 'i')
2479
+ }));
2480
+ // 2. 扫描文档,找到每个端的起始位置
2481
+ const platformStartLines = {};
2482
+ for (let i = 0; i < lines.length; i++) {
2483
+ const line = lines[i];
2484
+ for (const { platform, regex } of platformPatterns) {
2485
+ if (regex.test(line) && !(platform in platformStartLines)) {
2486
+ platformStartLines[platform] = i;
2487
+ }
2488
+ }
2489
+ }
2490
+ // 3. 提取每个端的内容(从该端标题到下一个端标题之前)
2491
+ for (const platform of platforms) {
2492
+ if (!(platform in platformStartLines))
2493
+ continue;
2494
+ const startLine = platformStartLines[platform];
2495
+ let endLine = lines.length; // 默认到文档末尾
2496
+ // 查找下一个端的起始位置
2497
+ for (const [otherPlatform, otherStart] of Object.entries(platformStartLines)) {
2498
+ if (otherPlatform !== platform && otherStart > startLine) {
2499
+ endLine = Math.min(endLine, otherStart);
2500
+ }
2501
+ }
2502
+ // 提取内容并去除 Markdown 标题标记
2503
+ const content = lines.slice(startLine, endLine).join('\n');
2504
+ result[platform] = content;
2505
+ }
2506
+ return result;
2507
+ }
2508
+ // ============================================================
2509
+ // 【新增】从文件路径或内容推断所属的端
2510
+ // ============================================================
2511
+ /**
2512
+ * 端名语义映射表:将各种写法映射到标准端名
2513
+ * 支持:中文端名、英文缩写、混合写法等
2514
+ */
2515
+ const PLATFORM_ALIAS_MAP = {
2516
+ // H5 移动端
2517
+ 'h5': ['h5', 'h5移动端', 'h5移动', 'mobile', '移动端', '手机浏览器', 'web mobile'],
2518
+ // Admin 后台管理
2519
+ 'admin': ['admin', '后台管理端', '后台', '管理端', 'web', 'pc', '桌面端', '管理后台', 'dashboard'],
2520
+ // App 客户端
2521
+ 'app': ['app', '客户端', 'ios', 'android', 'native', '原生', '移动端app', '手机app'],
2522
+ // 小程序
2523
+ 'miniapp': ['miniapp', '小程序', '微信小程序', '支付宝小程序', 'miniprogram'],
2524
+ // 后端服务
2525
+ 'backend': ['backend', '后端', '服务', 'api', 'server', '服务端', '微服务']
2526
+ };
2527
+ /**
2528
+ * 根据文件路径或内容推断该文档属于哪个端
2529
+ * @param filePath 文件路径
2530
+ * @param content 文件内容
2531
+ * @param platforms CONSTITUTION.md 定义的端列表
2532
+ * @returns 推断出的端名,或 null(无法推断)
2533
+ */
2534
+ function inferPlatformFromPathOrContent(filePath, content, platforms) {
2535
+ // 1. 从文件路径推断(优先级最高)
2536
+ const pathLower = filePath.toLowerCase();
2537
+ // 检查路径中是否包含端名目录,如: 010-requirements/app/REQUIREMENT.md
2538
+ for (const platform of platforms) {
2539
+ const platformPatterns = [
2540
+ new RegExp(`[/\\\\]${platform}[/\\\\]`, 'i'), // /app/ 或 \app\
2541
+ new RegExp(`[/\\\\]${platform}-`, 'i'), // /app-xxx.md
2542
+ new RegExp(`[/\\\\]${platform}_`, 'i'), // /app_xxx.md
2543
+ ];
2544
+ for (const pattern of platformPatterns) {
2545
+ if (pattern.test(pathLower)) {
2546
+ return platform;
2547
+ }
2548
+ }
2549
+ }
2550
+ // 检查文件名本身,如: app-requirement.md
2551
+ const fileName = filePath.split(/[/\\]/).pop()?.toLowerCase() || '';
2552
+ for (const platform of platforms) {
2553
+ if (fileName.startsWith(platform + '-') ||
2554
+ fileName.startsWith(platform + '_') ||
2555
+ fileName.includes('-' + platform + '.') ||
2556
+ fileName.includes('_' + platform + '.')) {
2557
+ return platform;
2558
+ }
2559
+ }
2560
+ // 【新增】2. 语义映射匹配:尝试将内容中的中文端名映射到标准端名
2561
+ const firstLines = content.split('\n').slice(0, 50).join('\n');
2562
+ for (const [standardPlatform, aliases] of Object.entries(PLATFORM_ALIAS_MAP)) {
2563
+ // 只检查这个标准端名是否在 platforms 列表中
2564
+ if (!platforms.includes(standardPlatform))
2565
+ continue;
2566
+ // 检查是否有别名出现在内容中
2567
+ for (const alias of aliases) {
2568
+ const aliasPattern = new RegExp(alias, 'i');
2569
+ if (aliasPattern.test(firstLines)) {
2570
+ logger_1.logger.info(` 🔄 语义映射: "${alias}" → "${standardPlatform}"`);
2571
+ return standardPlatform;
2572
+ }
2573
+ }
2574
+ }
2575
+ // 3. 从文件内容推断(精确匹配标准端名)
2576
+ for (const platform of platforms) {
2577
+ const patterns = [
2578
+ new RegExp(`^#{1,4}\\s*.*?(?:${platform}|${platform.toUpperCase()}|${platform.charAt(0).toUpperCase() + platform.slice(1)}).*?(?:端|需求)`, 'im'),
2579
+ new RegExp(`(?:^|\n)>?.*?(?:${platform}|${platform.toUpperCase()}).*?(?:端|平台|前端|后端)`, 'im'),
2580
+ ];
2581
+ for (const pattern of patterns) {
2582
+ if (pattern.test(firstLines)) {
2583
+ return platform;
2584
+ }
2585
+ }
2586
+ }
2587
+ return null; // 无法推断
2588
+ }
2387
2589
  //# sourceMappingURL=analyze-engine.js.map