speccore 8.3.264 → 8.3.268

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.
Files changed (33) hide show
  1. package/dist/commands/analyze.d.ts.map +1 -1
  2. package/dist/commands/analyze.js +209 -11
  3. package/dist/commands/analyze.js.map +1 -1
  4. package/dist/commands/doc2spec.d.ts.map +1 -1
  5. package/dist/commands/doc2spec.js +38 -5
  6. package/dist/commands/doc2spec.js.map +1 -1
  7. package/dist/commands/execute.js +14 -4
  8. package/dist/commands/execute.js.map +1 -1
  9. package/dist/commands/iteration/taskify.js +182 -21
  10. package/dist/commands/iteration/taskify.js.map +1 -1
  11. package/dist/core/agent-adapter.d.ts +8 -0
  12. package/dist/core/agent-adapter.d.ts.map +1 -1
  13. package/dist/core/agent-adapter.js +49 -5
  14. package/dist/core/agent-adapter.js.map +1 -1
  15. package/dist/core/ask-host-ai.d.ts.map +1 -1
  16. package/dist/core/ask-host-ai.js +4 -2
  17. package/dist/core/ask-host-ai.js.map +1 -1
  18. package/dist/core/prompt-builder.d.ts.map +1 -1
  19. package/dist/core/prompt-builder.js +13 -5
  20. package/dist/core/prompt-builder.js.map +1 -1
  21. package/dist/core/requirement-clarifier.d.ts.map +1 -1
  22. package/dist/core/requirement-clarifier.js +4 -0
  23. package/dist/core/requirement-clarifier.js.map +1 -1
  24. package/dist/core/spec-paths.d.ts.map +1 -1
  25. package/dist/core/spec-paths.js +5 -0
  26. package/dist/core/spec-paths.js.map +1 -1
  27. package/dist/core/spec-skeleton.d.ts.map +1 -1
  28. package/dist/core/spec-skeleton.js +15 -0
  29. package/dist/core/spec-skeleton.js.map +1 -1
  30. package/dist/core/unit-context-assembler.d.ts.map +1 -1
  31. package/dist/core/unit-context-assembler.js +107 -4
  32. package/dist/core/unit-context-assembler.js.map +1 -1
  33. package/package.json +1 -1
@@ -1701,27 +1701,49 @@ async function createTaskFromSection(iterationDir, taskId, section, allPlatforms
1701
1701
  taskPlatforms = [section.platform];
1702
1702
  }
1703
1703
  else {
1704
- // v8.3.125+: 使用三层匹配推断平台(替代脆弱的字符串包含匹配)
1704
+ // v8.3.268: 端覆盖裁剪——_matrix 显式声明优先;为空时从 020-specs 端级文档反推
1705
+ // (仅当端级存在非骨架、≥200 字符的 TECH/TEST/UI_SPEC 才算涉及端);
1706
+ // 仍无结果才回退 Spec 匹配 + 六层推断。修复 _matrix platforms 全空时
1707
+ // 每 Task 平铺全部端、幽灵端子任务无中生有(17 Task × 4 端 ≈ 半数无真实规格)。
1705
1708
  taskPlatforms = [];
1706
1709
  const sectionFeatureName = section.name.replace(/^\d+(\.\d+)*\s*/, '').trim();
1707
- for (const platform of allPlatforms) {
1708
- const bestMatch = findBestSpecMatch(specContents, sectionFeatureName, platform, 'TECH.md');
1709
- if (bestMatch && bestMatch.score >= 20) {
1710
- taskPlatforms.push(platform);
1710
+ const matrixPlatforms = await readMatrixPlatforms(iterationDir, sectionFeatureName);
1711
+ if (matrixPlatforms.length > 0) {
1712
+ taskPlatforms = matrixPlatforms;
1713
+ }
1714
+ else {
1715
+ for (const platform of allPlatforms) {
1716
+ const hasRealSpec = Object.entries(specContents).some(([key, content]) => {
1717
+ const parts = key.split('/');
1718
+ return parts.length >= 3 && parts[0].includes(sectionFeatureName) && parts[1] === platform &&
1719
+ /(TECH|TEST|UI_SPEC)\.md$/.test(parts[2]) &&
1720
+ !content.includes('SPEC-SKELETON') && content.trim().length >= 200;
1721
+ });
1722
+ if (hasRealSpec)
1723
+ taskPlatforms.push(platform);
1711
1724
  }
1712
1725
  }
1713
- // v8.3.185+: Spec 匹配失败时,用六层端推断机制(显式声明→_matrix→目录→关键词→AI)
1714
1726
  if (taskPlatforms.length === 0) {
1715
- const { inferFeaturePlatforms } = await Promise.resolve().then(() => __importStar(require('../../core/platform-inference')));
1716
- const featureDir = (0, path_1.join)(iterationDir, '010-requirements', 'features', sectionFeatureName);
1717
- const inference = await inferFeaturePlatforms({
1718
- featureName: sectionFeatureName,
1719
- featureDir,
1720
- allPlatforms,
1721
- });
1722
- taskPlatforms = inference.platforms;
1723
- if (inference.needsConfirm || inference.source === 'ai-required') {
1724
- logger_1.logger.warn(` ⚠️ 功能「${sectionFeatureName}」端推断置信度 ${inference.confidence}(${inference.source}),建议人工确认 _matrix.md`);
1727
+ // v8.3.125+: 使用三层匹配推断平台(替代脆弱的字符串包含匹配)
1728
+ for (const platform of allPlatforms) {
1729
+ const bestMatch = findBestSpecMatch(specContents, sectionFeatureName, platform, 'TECH.md');
1730
+ if (bestMatch && bestMatch.score >= 20) {
1731
+ taskPlatforms.push(platform);
1732
+ }
1733
+ }
1734
+ // v8.3.185+: Spec 匹配失败时,用六层端推断机制(显式声明→_matrix→目录→关键词→AI)
1735
+ if (taskPlatforms.length === 0) {
1736
+ const { inferFeaturePlatforms } = await Promise.resolve().then(() => __importStar(require('../../core/platform-inference')));
1737
+ const featureDir = (0, path_1.join)(iterationDir, '010-requirements', 'features', sectionFeatureName);
1738
+ const inference = await inferFeaturePlatforms({
1739
+ featureName: sectionFeatureName,
1740
+ featureDir,
1741
+ allPlatforms,
1742
+ });
1743
+ taskPlatforms = inference.platforms;
1744
+ if (inference.needsConfirm || inference.source === 'ai-required') {
1745
+ logger_1.logger.warn(` ⚠️ 功能「${sectionFeatureName}」端推断置信度 ${inference.confidence}(${inference.source}),建议人工确认 _matrix.md`);
1746
+ }
1725
1747
  }
1726
1748
  }
1727
1749
  }
@@ -1838,6 +1860,46 @@ ${taskPlatforms.map((p) => `| ${subtaskIdMap.get(p)} | ${p} | ${owner} | 🔲
1838
1860
  await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml'), contractYaml);
1839
1861
  }
1840
1862
  }
1863
+ // v8.3.268: 合并端级契约——全局契约可能是空壳(apis: [] 或无接口),
1864
+ // 真契约在各功能单元的 020-specs/{feature}/{端}/API_CONTRACT.yaml。
1865
+ // 修复 execute 注入 _shared/API_CONTRACT.yaml 时拿到空契约、AI 无中生有发明接口的断链。
1866
+ const featName = section.name.replace(/^\d+(\.\d+)*\s*/, '').trim();
1867
+ const specsRoot = (0, path_1.join)(iterationDir, '020-specs');
1868
+ const platContractParts = [];
1869
+ if (await (0, fs_extra_1.pathExists)(specsRoot)) {
1870
+ const featEntries = await (0, fs_extra_1.readdir)(specsRoot, { withFileTypes: true });
1871
+ for (const fe of featEntries) {
1872
+ if (!fe.isDirectory() || !fe.name.includes(featName))
1873
+ continue;
1874
+ const platEntries = await (0, fs_extra_1.readdir)((0, path_1.join)(specsRoot, fe.name), { withFileTypes: true });
1875
+ for (const pe of platEntries) {
1876
+ if (!pe.isDirectory())
1877
+ continue;
1878
+ const contractPath = (0, path_1.join)(specsRoot, fe.name, pe.name, 'API_CONTRACT.yaml');
1879
+ if (!(await (0, fs_extra_1.pathExists)(contractPath)))
1880
+ continue;
1881
+ const contractContent = await (0, fs_extra_1.readFile)(contractPath, 'utf-8');
1882
+ // 空壳判定:无接口条目(apis: [] / paths 缺 / 仅说明文字)
1883
+ const hasApis = /apis:\s*[\n\r]\s*-\s/m.test(contractContent) || /^paths:\s*$/m.test(contractContent);
1884
+ if (!hasApis)
1885
+ continue;
1886
+ platContractParts.push(`## ${pe.name} 端契约(${fe.name})\n\n${contractContent.trim()}`);
1887
+ }
1888
+ }
1889
+ }
1890
+ if (platContractParts.length > 0) {
1891
+ const sharedContractPath = (0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml');
1892
+ let baseContract = await (0, fs_extra_1.pathExists)(sharedContractPath)
1893
+ ? await (0, fs_extra_1.readFile)(sharedContractPath, 'utf-8')
1894
+ : '';
1895
+ // 全局索引壳保留,但去掉空 apis 标记,避免误导(真契约已合并于下方)
1896
+ baseContract = baseContract.replace(/\n?apis:\s*\[\]\s*$/, '').trimEnd();
1897
+ const merged = (baseContract
1898
+ ? `${baseContract}\n\n---\n\n`
1899
+ : '# 任务级接口契约(合并自端级契约)\n\n') + platContractParts.join('\n\n---\n\n') + '\n';
1900
+ await (0, fs_extra_1.writeFile)(sharedContractPath, merged);
1901
+ logger_1.logger.debug(` 📄 ${taskDir.split('/').pop()}: _shared 契约合并 ${platContractParts.length} 份端级契约`);
1902
+ }
1841
1903
  // ── 4. 核心规格写入 00-specs/(REQ/TECH/SCHEMA/CHANGELOG) ──
1842
1904
  const acItems = generateAcceptanceCriteria(section);
1843
1905
  const aiReqContent = section._reqContent;
@@ -2982,6 +3044,38 @@ function generateMonitorTemplate(section, material) {
2982
3044
  // 从 analyze 产出中提取任务相关内容
2983
3045
  // ═══════════════════════════════════════════════
2984
3046
  /** 加载迭代级 020-specs/ 文档内容(包括 global/ 全局文档 + 根目录回退 + 各端子目录文档) */
3047
+ /**
3048
+ * v8.3.268: 读取功能单元的端覆盖声明(010-requirements/features/{feature}/_matrix.md 的 platforms 字段)。
3049
+ * 修复 _matrix platforms 全空/缺失时 taskify 每 Task 平铺全部端、幽灵端子任务无中生有的问题。
3050
+ * 解析格式:platforms: 后跟随 ` - 端名` 行;` []`(空标记)或不存在时返回空数组,交由反推逻辑处理。
3051
+ */
3052
+ async function readMatrixPlatforms(iterationDir, featureName) {
3053
+ try {
3054
+ const featuresDir = (0, path_1.join)(iterationDir, '010-requirements', 'features');
3055
+ if (!(await (0, fs_extra_1.pathExists)(featuresDir)))
3056
+ return [];
3057
+ const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
3058
+ for (const e of entries) {
3059
+ if (!e.isDirectory() || !e.name.includes(featureName))
3060
+ continue;
3061
+ const matrixPath = (0, path_1.join)(featuresDir, e.name, '_matrix.md');
3062
+ if (!(await (0, fs_extra_1.pathExists)(matrixPath)))
3063
+ continue;
3064
+ const raw = await (0, fs_extra_1.readFile)(matrixPath, 'utf-8');
3065
+ const m = raw.match(/platforms:\s*\n((?:\s*[-\[]?[\s\S]*?))(?=\n(?:dependencies|apis|interfaces|$))/);
3066
+ const block = m ? m[1] : '';
3067
+ if (!block || /^\s*\[\s*\]\s*$/.test(block.trim()))
3068
+ continue;
3069
+ const plats = block.split('\n')
3070
+ .map(l => l.replace(/^\s*-\s*/, '').trim())
3071
+ .filter(p => p && !p.startsWith('#') && !p.includes(':'));
3072
+ if (plats.length > 0)
3073
+ return plats;
3074
+ }
3075
+ }
3076
+ catch { /* 读取失败不阻断,回退端级规格反推逻辑 */ }
3077
+ return [];
3078
+ }
2985
3079
  async function loadSpecContents(iterationDir) {
2986
3080
  const specs = {};
2987
3081
  const specDir = (0, path_1.join)(iterationDir, '020-specs');
@@ -3100,6 +3194,13 @@ function findBestSpecMatch(specContents, taskName, platform, docType) {
3100
3194
  if (candidates.length === 0)
3101
3195
  return null;
3102
3196
  candidates.sort((a, b) => b.score - a.score);
3197
+ // v8.3.265+: 并列最高分告警——双向 includes 在通用词任务名(如「管理」)下可能命中多个模块,静默取第一个会错配文档
3198
+ const topScore = candidates[0].score;
3199
+ const ties = candidates.filter(c => c.score === topScore);
3200
+ if (ties.length > 1) {
3201
+ logger_1.logger.warn(` ⚠️ Spec 文档匹配并列 ${ties.length} 个候选(任务「${taskName}」,得分 ${topScore}),已取第一个: ${ties[0].key}`);
3202
+ logger_1.logger.warn(` 并列候选: ${ties.map(t => t.key).join(' | ')}`);
3203
+ }
3103
3204
  return candidates[0];
3104
3205
  }
3105
3206
  /** 从完整文档中提取与任务名相关的段落 */
@@ -3234,10 +3335,42 @@ function extractTaskDevGuideContent(specContents, section, taskPlatforms) {
3234
3335
  }
3235
3336
  return results.join('\n\n');
3236
3337
  }
3338
+ // v8.3.268: 按文档类型从 TECH 提取中分化内容——按 ### 标题切段,
3339
+ // 只保留与文档类型相关的段落。修复「一段多用」:ROUTES/STATE/STYLE_GUIDE/
3340
+ // COMPONENT_TREE 四份文档正文完全相同(仅标题不同)的问题。
3341
+ function extractSectionByKeywords(material, keywords) {
3342
+ const sections = material.split(/^#{2,4}\s+/m);
3343
+ const matched = [];
3344
+ for (const sec of sections) {
3345
+ const headLine = sec.split('\n')[0] || '';
3346
+ if (keywords.some(kw => headLine.includes(kw))) {
3347
+ matched.push(sec.trim());
3348
+ }
3349
+ }
3350
+ // 无 ### 标题结构的兜底:整段中关键词密度最高的前 N 行
3351
+ if (matched.length === 0 && material.includes('\n|')) {
3352
+ const lines = material.split('\n');
3353
+ const tableStart = lines.findIndex(l => keywords.some(kw => l.includes(kw)));
3354
+ if (tableStart >= 0) {
3355
+ const block = [];
3356
+ for (let i = tableStart; i < lines.length; i++) {
3357
+ if (i > tableStart && /^#{2,4}\s/.test(lines[i]))
3358
+ break;
3359
+ block.push(lines[i]);
3360
+ }
3361
+ return block.join('\n').trim();
3362
+ }
3363
+ }
3364
+ return matched.join('\n\n');
3365
+ }
3237
3366
  // 前端专属:组件树
3238
3367
  function generateComponentTree(section, platform, material) {
3239
3368
  if (material && material.trim().length > 200) {
3240
- return `# ${section.name} — 组件树 (${platform})\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
3369
+ // v8.3.268: 仅提取组件拆分/组件清单相关段落
3370
+ const extracted = extractSectionByKeywords(material, ['组件拆分', '组件清单', '组件树', '共享组件']);
3371
+ if (extracted) {
3372
+ return `# ${section.name} — 组件树 (${platform})\n\n> 来源: analyze → TECH.md(组件拆分段提取)\n\n${extracted}\n`;
3373
+ }
3241
3374
  }
3242
3375
  return `# ${section.name} — 组件树 (${platform})
3243
3376
 
@@ -3260,7 +3393,11 @@ function generateComponentTree(section, platform, material) {
3260
3393
  // 前端专属:路由
3261
3394
  function generateRoutesDoc(section, platform, material) {
3262
3395
  if (material && material.trim().length > 200) {
3263
- return `# ${section.name} — 路由设计 (${platform})\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
3396
+ // v8.3.268: 仅提取页面清单与路由相关段落
3397
+ const extracted = extractSectionByKeywords(material, ['页面清单', '路由', '导航']);
3398
+ if (extracted) {
3399
+ return `# ${section.name} — 路由设计 (${platform})\n\n> 来源: analyze → TECH.md(页面/路由段提取)\n\n${extracted}\n`;
3400
+ }
3264
3401
  }
3265
3402
  return `# ${section.name} — 路由设计 (${platform})
3266
3403
 
@@ -3278,7 +3415,11 @@ function generateRoutesDoc(section, platform, material) {
3278
3415
  // 前端专属:状态管理
3279
3416
  function generateStateDoc(section, platform, material) {
3280
3417
  if (material && material.trim().length > 200) {
3281
- return `# ${section.name} — 状态管理 (${platform})\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
3418
+ // v8.3.268: 仅提取状态管理/Store/数据流相关段落
3419
+ const extracted = extractSectionByKeywords(material, ['状态管理', 'Store', '数据流', '状态枚举']);
3420
+ if (extracted) {
3421
+ return `# ${section.name} — 状态管理 (${platform})\n\n> 来源: analyze → TECH.md(状态管理段提取)\n\n${extracted}\n`;
3422
+ }
3282
3423
  }
3283
3424
  return `# ${section.name} — 状态管理 (${platform})
3284
3425
 
@@ -3301,7 +3442,11 @@ function generateStateDoc(section, platform, material) {
3301
3442
  // 前端专属:样式规范
3302
3443
  function generateStyleGuide(section, platform, material) {
3303
3444
  if (material && material.trim().length > 200) {
3304
- return `# ${section.name} — 样式规范 (${platform})\n\n> 来源: analyze → TECH.md(自动提取)\n\n${material.trim()}\n`;
3445
+ // v8.3.268: 仅提取样式/主题/设计 Token 相关段落
3446
+ const extracted = extractSectionByKeywords(material, ['样式', '主题', '设计 Token', '视觉', '响应式']);
3447
+ if (extracted) {
3448
+ return `# ${section.name} — 样式规范 (${platform})\n\n> 来源: analyze → TECH.md(样式/主题段提取)\n\n${extracted}\n`;
3449
+ }
3305
3450
  }
3306
3451
  const isH5 = platform.includes('h5') || platform.includes('mobile');
3307
3452
  const isMiniapp = platform.includes('miniapp') || platform.includes('小程序');
@@ -4136,7 +4281,11 @@ function parseFunctionMapTable(content, allPlatforms) {
4136
4281
  continue;
4137
4282
  if (cells.some(c => c.includes('功能单元')) || cells.some(c => c.includes('涉及端')))
4138
4283
  continue;
4139
- const unitName = cells[1];
4284
+ // v8.3.268: 功能单元列取 cells[0](表头第一列)。此前取 cells[1] 依赖旧数据
4285
+ // 「| 目录名 | 名称 | 端列表 | …」的列错位(cells[1]=名称),一旦表格对齐表头
4286
+ // (cells[1]=涉及端)任务名就变成端列表。「dir(名称)」格式剥尾部括号注释。
4287
+ const unitNameRaw = cells[0] || cells[1];
4288
+ const unitName = unitNameRaw.replace(/[((][^))]*[))]$/, '').trim();
4140
4289
  if (!unitName || unitName === '#' || unitName === '功能单元')
4141
4290
  continue;
4142
4291
  let platforms = [];
@@ -4146,6 +4295,18 @@ function parseFunctionMapTable(content, allPlatforms) {
4146
4295
  platforms = raw.split(/[,,]/).map(p => p.trim()).filter(p => p && allPlatforms.includes(p));
4147
4296
  }
4148
4297
  }
4298
+ // v8.3.268: 端列自适应——AI 落盘 FUNCTION_MAP 表格可能列错位(涉及端表头下是
4299
+ // 名称、端列表落在其他列),按表头解析落空时扫描整行,找「逗号分隔且 ≥2 个
4300
+ // 值全部属于标准端名」的单元格作为端列,避免静默回退全端平铺。
4301
+ if (platforms.length === 0) {
4302
+ for (const cell of cells) {
4303
+ const parts = cell.split(/[,,]/).map(p => p.trim()).filter(Boolean);
4304
+ if (parts.length >= 2 && parts.every(p => allPlatforms.includes(p))) {
4305
+ platforms = parts;
4306
+ break;
4307
+ }
4308
+ }
4309
+ }
4149
4310
  if (platforms.length === 0)
4150
4311
  platforms = [...allPlatforms];
4151
4312
  let sharedCapability = '无';