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.
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +20 -37
- package/dist/commands/analyze.js.map +1 -1
- package/dist/core/analyze-engine.d.ts.map +1 -1
- package/dist/core/analyze-engine.js +144 -19
- package/dist/core/analyze-engine.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1699
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2801
|
+
// 2. 语义映射匹配:硬编码 PLATFORM_ALIAS_MAP + CONSTITUTION.md 动态别名
|
|
2688
2802
|
const firstLines = content.split('\n').slice(0, 50).join('\n');
|
|
2689
|
-
|
|
2690
|
-
|
|
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
|
-
|
|
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
|
}
|