speccore 6.40.1 → 6.40.2

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.
@@ -1172,7 +1172,7 @@ async function writePerPlatform(iterDir, report, filename) {
1172
1172
  .map(e => e.name);
1173
1173
  if (features.length === 0)
1174
1174
  return;
1175
- // 平台列表从 CONSTITUTION 获取,默认四端
1175
+ // 平台列表从 CONSTITUTION 获取(不再硬编码默认值)
1176
1176
  const platforms = await detectPlatformsFromConstitution();
1177
1177
  for (const platform of platforms) {
1178
1178
  const platformDir = (0, path_1.join)(specsBase, platform);
@@ -1626,14 +1626,65 @@ async function supplementAnalysis(input) {
1626
1626
  }
1627
1627
  /** 已检测到的后端平台列表(从 CONSTITUTION.md 解析) */
1628
1628
  let _detectedBackendPlatforms = [];
1629
- /** 从 CONSTITUTION.md 提取平台列表(支持中文端名 + 工程名映射) */
1629
+ /** 从 CONSTITUTION.md 动态提取的端名映射(技术栈标题解析) */
1630
+ let _dynamicPlatformAliases = {};
1631
+ /**
1632
+ * 从 CONSTITUTION.md 技术栈章节标题提取端名信息。
1633
+ * 匹配模式: ### 中文端名 (English Name)
1634
+ * 例如: ### 后台管理端 (Admin Dashboard) → { chinese: '后台管理端', english: 'Admin Dashboard' }
1635
+ */
1636
+ function parseTechStackHeaders(content) {
1637
+ const results = [];
1638
+ const lines = content.split('\n');
1639
+ for (const line of lines) {
1640
+ // 匹配: ### 后台管理端 (Admin Dashboard) 或 ### H5 移动端 (Mobile H5)
1641
+ const match = line.match(/^###\s+(.+?)\s*\(([^)]+)\)/);
1642
+ if (match) {
1643
+ results.push({
1644
+ chinese: match[1].trim(),
1645
+ english: match[2].trim(),
1646
+ fullTitle: line.trim(),
1647
+ });
1648
+ }
1649
+ }
1650
+ return results;
1651
+ }
1652
+ /**
1653
+ * 将技术栈标题解析结果合并到动态别名映射中。
1654
+ * 这样 inferPlatformFromPathOrContent 也能使用 CONSTITUTION.md 定义的端名。
1655
+ */
1656
+ function buildDynamicAliasesFromTechStack(techStackEntries) {
1657
+ const aliases = {};
1658
+ for (const entry of techStackEntries) {
1659
+ const normalized = normalizeToStandardPlatform(entry.chinese) || normalizeToStandardPlatform(entry.english);
1660
+ if (!normalized)
1661
+ continue;
1662
+ if (!aliases[normalized]) {
1663
+ aliases[normalized] = [];
1664
+ }
1665
+ // 添加中文名、英文名、全标题作为别名
1666
+ const newAliases = [entry.chinese, entry.english, entry.fullTitle];
1667
+ for (const a of newAliases) {
1668
+ const lower = a.toLowerCase();
1669
+ if (!aliases[normalized].includes(lower)) {
1670
+ aliases[normalized].push(lower);
1671
+ }
1672
+ }
1673
+ }
1674
+ return aliases;
1675
+ }
1676
+ /** 从 CONSTITUTION.md 提取平台列表(两层确定性匹配)
1677
+ * Layer 1: 表格「对应需求端」列(用户显式声明)
1678
+ * Layer 2: 技术栈章节标题 ### 中文端名 (English Name)
1679
+ * ⚠️ 不再提供硬编码默认值 — 端列表应由 AI 根据项目实际情况判断
1680
+ */
1630
1681
  async function detectPlatformsFromConstitution() {
1631
1682
  try {
1632
1683
  const constitutionPath = (0, path_1.join)(process.cwd(), '.speccore', 'CONSTITUTION.md');
1633
1684
  if (require('fs').existsSync(constitutionPath)) {
1634
1685
  const content = require('fs').readFileSync(constitutionPath, 'utf-8');
1635
1686
  const lines = content.split('\n');
1636
- // 1. 先尝试从表头定位「对应需求端」和「工程名」列的索引
1687
+ // ── Layer 1: 表格「对应需求端」列 ──
1637
1688
  let headerRowIndex = -1;
1638
1689
  let headerCells = [];
1639
1690
  for (let i = 0; i < lines.length; i++) {
@@ -1649,22 +1700,29 @@ async function detectPlatformsFromConstitution() {
1649
1700
  const platforms = [];
1650
1701
  const backendPlatforms = [];
1651
1702
  const seen = new Set();
1652
- // 解析数据行(跳过头部和分隔线)
1653
1703
  for (let i = headerRowIndex + 2; i < lines.length; i++) {
1654
1704
  const line = lines[i].trim();
1655
- if (!line.startsWith('|') || line.match(/^\|[\s:-]+/))
1705
+ // 分隔行跳过(如 | :--- | :--- |)
1706
+ if (line.match(/^\|[\s:-]+/))
1707
+ continue;
1708
+ // 空行跳过(表格内可能有空行)
1709
+ if (line === '')
1656
1710
  continue;
1711
+ // 非表格行 → 当前表格结束,终止读取
1712
+ if (!line.startsWith('|'))
1713
+ break;
1657
1714
  const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
1658
1715
  const projectName = cells[0] || '';
1659
1716
  const platformChinese = cells[5] || cells[cells.length - 1] || '';
1717
+ // 跳过空值和占位符(如「待填写」)
1660
1718
  if (!platformChinese || !projectName)
1661
1719
  continue;
1662
- // 归一化:中文端名 → 标准端名
1720
+ if (/待填写|待补充|TODO|TBD|N\/A/i.test(platformChinese))
1721
+ continue;
1663
1722
  const normalized = normalizeToStandardPlatform(platformChinese);
1664
1723
  if (normalized && !seen.has(normalized)) {
1665
1724
  seen.add(normalized);
1666
1725
  platforms.push(normalized);
1667
- // 判断前后端:从工程名或中文端名推断
1668
1726
  const isBackend = /service|server|api|backend|后台|服务|后端/i.test(projectName) ||
1669
1727
  /后台|服务|后端/.test(platformChinese);
1670
1728
  if (isBackend) {
@@ -1674,18 +1732,52 @@ async function detectPlatformsFromConstitution() {
1674
1732
  }
1675
1733
  if (platforms.length > 0) {
1676
1734
  _detectedBackendPlatforms = backendPlatforms;
1735
+ // Layer 1 成功:也解析技术栈标题来构建动态别名
1736
+ const techStackEntries = parseTechStackHeaders(content);
1737
+ if (techStackEntries.length > 0) {
1738
+ _dynamicPlatformAliases = buildDynamicAliasesFromTechStack(techStackEntries);
1739
+ }
1740
+ return platforms;
1741
+ }
1742
+ }
1743
+ // ── Layer 2: 技术栈章节标题 ### 中文端名 (English Name) ──
1744
+ const techStackEntries = parseTechStackHeaders(content);
1745
+ if (techStackEntries.length > 0) {
1746
+ const platforms = [];
1747
+ const backendPlatforms = [];
1748
+ const seen = new Set();
1749
+ for (const entry of techStackEntries) {
1750
+ const normalized = normalizeToStandardPlatform(entry.chinese) || normalizeToStandardPlatform(entry.english);
1751
+ if (!normalized || seen.has(normalized))
1752
+ continue;
1753
+ seen.add(normalized);
1754
+ platforms.push(normalized);
1755
+ // 判断前后端
1756
+ const isBackend = /service|server|api|backend|后台|服务|后端/i.test(entry.chinese + ' ' + entry.english);
1757
+ if (isBackend) {
1758
+ backendPlatforms.push(normalized);
1759
+ }
1760
+ }
1761
+ if (platforms.length > 0) {
1762
+ _detectedBackendPlatforms = backendPlatforms;
1763
+ _dynamicPlatformAliases = buildDynamicAliasesFromTechStack(techStackEntries);
1764
+ logger_1.logger.info(` 📋 从技术栈标题检测到 ${platforms.length} 个端: ${platforms.join(', ')}`);
1677
1765
  return platforms;
1678
1766
  }
1679
1767
  }
1680
- // 2. 回退:简单正则匹配(兼容旧格式)
1768
+ // Layer 2.5: 简单正则匹配(兼容旧格式)
1681
1769
  const match = content.match(/对应需求端[||]\s*([a-z,\s]+)/i);
1682
1770
  if (match) {
1683
- return match[1].split(/[,,]/).map((s) => s.trim()).filter(Boolean);
1771
+ const result = match[1].split(/[,,]/).map((s) => s.trim()).filter(Boolean);
1772
+ if (result.length > 0)
1773
+ return result;
1684
1774
  }
1685
1775
  }
1686
1776
  }
1687
1777
  catch { }
1688
- return ['app', 'h5', 'miniapp', 'admin']; // 默认四端
1778
+ // ⚠️ 不再硬编码默认端列表 端列表应由 AI 根据 CONSTITUTION.md + 需求文档判断
1779
+ // 如果 Layer 1 和 Layer 2 都无法检测到端,返回空数组,由 AI 在 prompt 中自行发现
1780
+ return [];
1689
1781
  }
1690
1782
  /**
1691
1783
  * 将中文端名/工程名归一化为标准端名
@@ -1693,11 +1785,28 @@ async function detectPlatformsFromConstitution() {
1693
1785
  */
1694
1786
  function normalizeToStandardPlatform(name) {
1695
1787
  const nameLower = name.toLowerCase().trim();
1788
+ // 【v6.40.2 修复】两阶段最长匹配策略:
1789
+ // Phase 1: 精确匹配(name === alias),最长优先
1790
+ // Phase 2: 包含匹配(name.includes(alias) 或 alias.includes(name)),最长优先
1791
+ // 避免「后台服务端」被短别名「后台」先匹配到 admin
1792
+ // 避免「移动端」被更长别名「移动端app」误匹配到 app
1793
+ const allPairs = [];
1696
1794
  for (const [standardName, aliases] of Object.entries(PLATFORM_ALIAS_MAP)) {
1697
1795
  for (const alias of aliases) {
1698
- if (nameLower === alias || nameLower.includes(alias) || alias.includes(nameLower)) {
1699
- return standardName;
1700
- }
1796
+ allPairs.push({ alias: alias.toLowerCase(), platform: standardName });
1797
+ }
1798
+ }
1799
+ allPairs.sort((a, b) => b.alias.length - a.alias.length);
1800
+ // Phase 1: 精确匹配
1801
+ for (const pair of allPairs) {
1802
+ if (nameLower === pair.alias) {
1803
+ return pair.platform;
1804
+ }
1805
+ }
1806
+ // Phase 2: 包含匹配
1807
+ for (const pair of allPairs) {
1808
+ if (nameLower.includes(pair.alias) || pair.alias.includes(nameLower)) {
1809
+ return pair.platform;
1701
1810
  }
1702
1811
  }
1703
1812
  // 无法映射时,返回清理后的原始值
@@ -1749,7 +1858,12 @@ async function generateSpecsFromRequirements(reqPaths, iteration, specDir) {
1749
1858
  if (unclassifiedFiles.length > 0) {
1750
1859
  logger_1.logger.info('');
1751
1860
  logger_1.logger.info(` ❓ 发现 ${unclassifiedFiles.length} 个文档无法自动识别所属端`);
1752
- logger_1.logger.info(` 💡 可用端列表: ${platforms.join(', ')}`);
1861
+ if (platforms.length > 0) {
1862
+ logger_1.logger.info(` 💡 可用端列表: ${platforms.join(', ')}`);
1863
+ }
1864
+ else {
1865
+ logger_1.logger.info(` 💡 CONSTITUTION.md 未检测到端列表,将由 AI 根据需求文档内容判断`);
1866
+ }
1753
1867
  logger_1.logger.info('');
1754
1868
  for (const file of unclassifiedFiles) {
1755
1869
  const fileName = file.path.split(/[/\\]/).pop() || file.path;
@@ -2684,17 +2798,28 @@ function inferPlatformFromPathOrContent(filePath, content, platforms) {
2684
2798
  return platform;
2685
2799
  }
2686
2800
  }
2687
- // 【新增】2. 语义映射匹配:尝试将内容中的中文端名映射到标准端名
2801
+ // 2. 语义映射匹配:硬编码 PLATFORM_ALIAS_MAP + CONSTITUTION.md 动态别名
2688
2802
  const firstLines = content.split('\n').slice(0, 50).join('\n');
2689
- for (const [standardPlatform, aliases] of Object.entries(PLATFORM_ALIAS_MAP)) {
2690
- // 只检查这个标准端名是否在 platforms 列表中
2803
+ // 合并硬编码映射和 CONSTITUTION.md 动态提取的别名
2804
+ const mergedAliases = { ...PLATFORM_ALIAS_MAP };
2805
+ for (const [platform, aliases] of Object.entries(_dynamicPlatformAliases)) {
2806
+ if (!mergedAliases[platform]) {
2807
+ mergedAliases[platform] = [];
2808
+ }
2809
+ for (const a of aliases) {
2810
+ if (!mergedAliases[platform].includes(a)) {
2811
+ mergedAliases[platform].push(a);
2812
+ }
2813
+ }
2814
+ }
2815
+ for (const [standardPlatform, aliases] of Object.entries(mergedAliases)) {
2691
2816
  if (!platforms.includes(standardPlatform))
2692
2817
  continue;
2693
- // 检查是否有别名出现在内容中
2694
2818
  for (const alias of aliases) {
2695
2819
  const aliasPattern = new RegExp(alias, 'i');
2696
2820
  if (aliasPattern.test(firstLines)) {
2697
- logger_1.logger.info(` 🔄 语义映射: "${alias}" "${standardPlatform}"`);
2821
+ const source = PLATFORM_ALIAS_MAP[standardPlatform]?.includes(alias) ? '静态映射' : 'CONSTITUTION动态';
2822
+ logger_1.logger.info(` 🔄 语义映射(${source}): "${alias}" → "${standardPlatform}"`);
2698
2823
  return standardPlatform;
2699
2824
  }
2700
2825
  }