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