speccore 8.3.333 → 8.3.340

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 (120) hide show
  1. package/.agents/skills/spec-ask/SKILL.md +4 -0
  2. package/.agents/skills/spec-unitize/SKILL.md +9 -1
  3. package/.agents/skills/speccore-router/SKILL.md +12 -9
  4. package/dist/cli.js +63 -19
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/ask.d.ts.map +1 -1
  7. package/dist/commands/ask.js +6 -0
  8. package/dist/commands/ask.js.map +1 -1
  9. package/dist/commands/doc2spec.d.ts +44 -1
  10. package/dist/commands/doc2spec.d.ts.map +1 -1
  11. package/dist/commands/doc2spec.js +257 -5
  12. package/dist/commands/doc2spec.js.map +1 -1
  13. package/dist/commands/execute.js +116 -64
  14. package/dist/commands/execute.js.map +1 -1
  15. package/dist/commands/help.js +1 -0
  16. package/dist/commands/help.js.map +1 -1
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +71 -40
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/iteration/create.d.ts.map +1 -1
  21. package/dist/commands/iteration/create.js +72 -10
  22. package/dist/commands/iteration/create.js.map +1 -1
  23. package/dist/commands/iteration/taskify.d.ts.map +1 -1
  24. package/dist/commands/iteration/taskify.js +133 -35
  25. package/dist/commands/iteration/taskify.js.map +1 -1
  26. package/dist/commands/pr.d.ts.map +1 -1
  27. package/dist/commands/pr.js +26 -2
  28. package/dist/commands/pr.js.map +1 -1
  29. package/dist/commands/prompts.d.ts +6 -0
  30. package/dist/commands/prompts.d.ts.map +1 -1
  31. package/dist/commands/prompts.js +19 -10
  32. package/dist/commands/prompts.js.map +1 -1
  33. package/dist/commands/status-panel.d.ts.map +1 -1
  34. package/dist/commands/status-panel.js +23 -34
  35. package/dist/commands/status-panel.js.map +1 -1
  36. package/dist/commands/unitize.d.ts +3 -0
  37. package/dist/commands/unitize.d.ts.map +1 -1
  38. package/dist/commands/unitize.js +32 -9
  39. package/dist/commands/unitize.js.map +1 -1
  40. package/dist/commands/welcome.d.ts.map +1 -1
  41. package/dist/commands/welcome.js +41 -0
  42. package/dist/commands/welcome.js.map +1 -1
  43. package/dist/core/ask-context.d.ts.map +1 -1
  44. package/dist/core/ask-context.js +14 -3
  45. package/dist/core/ask-context.js.map +1 -1
  46. package/dist/core/ask-engine.d.ts.map +1 -1
  47. package/dist/core/ask-engine.js +39 -7
  48. package/dist/core/ask-engine.js.map +1 -1
  49. package/dist/core/environment-config.d.ts +11 -0
  50. package/dist/core/environment-config.d.ts.map +1 -1
  51. package/dist/core/environment-config.js +22 -0
  52. package/dist/core/environment-config.js.map +1 -1
  53. package/dist/core/git-integration.d.ts +1 -9
  54. package/dist/core/git-integration.d.ts.map +1 -1
  55. package/dist/core/git-integration.js +126 -12
  56. package/dist/core/git-integration.js.map +1 -1
  57. package/dist/core/global-knowledge.d.ts.map +1 -1
  58. package/dist/core/global-knowledge.js +24 -10
  59. package/dist/core/global-knowledge.js.map +1 -1
  60. package/dist/core/handoff-generator.d.ts.map +1 -1
  61. package/dist/core/handoff-generator.js +3 -1
  62. package/dist/core/handoff-generator.js.map +1 -1
  63. package/dist/core/intent-recognition.d.ts.map +1 -1
  64. package/dist/core/intent-recognition.js +63 -5
  65. package/dist/core/intent-recognition.js.map +1 -1
  66. package/dist/core/knowledge-graph.d.ts.map +1 -1
  67. package/dist/core/knowledge-graph.js +6 -4
  68. package/dist/core/knowledge-graph.js.map +1 -1
  69. package/dist/core/name-validator.d.ts.map +1 -1
  70. package/dist/core/name-validator.js +41 -20
  71. package/dist/core/name-validator.js.map +1 -1
  72. package/dist/core/pinyin-initials.d.ts +10 -0
  73. package/dist/core/pinyin-initials.d.ts.map +1 -0
  74. package/dist/core/pinyin-initials.js +44 -0
  75. package/dist/core/pinyin-initials.js.map +1 -0
  76. package/dist/core/reindex-engine.js +4 -4
  77. package/dist/core/reindex-engine.js.map +1 -1
  78. package/dist/core/skeleton-residue.d.ts +13 -0
  79. package/dist/core/skeleton-residue.d.ts.map +1 -1
  80. package/dist/core/skeleton-residue.js +83 -14
  81. package/dist/core/skeleton-residue.js.map +1 -1
  82. package/dist/core/spec-merger.d.ts.map +1 -1
  83. package/dist/core/spec-merger.js +3 -2
  84. package/dist/core/spec-merger.js.map +1 -1
  85. package/dist/core/state.d.ts +10 -0
  86. package/dist/core/state.d.ts.map +1 -1
  87. package/dist/core/state.js +20 -1
  88. package/dist/core/state.js.map +1 -1
  89. package/dist/core/task-paths.d.ts +12 -0
  90. package/dist/core/task-paths.d.ts.map +1 -1
  91. package/dist/core/task-paths.js +33 -1
  92. package/dist/core/task-paths.js.map +1 -1
  93. package/dist/core/unified-config.d.ts +7 -1
  94. package/dist/core/unified-config.d.ts.map +1 -1
  95. package/dist/core/unified-config.js +41 -2
  96. package/dist/core/unified-config.js.map +1 -1
  97. package/dist/core/unit-staffing.d.ts +21 -4
  98. package/dist/core/unit-staffing.d.ts.map +1 -1
  99. package/dist/core/unit-staffing.js +59 -12
  100. package/dist/core/unit-staffing.js.map +1 -1
  101. package/dist/core/validator.d.ts.map +1 -1
  102. package/dist/core/validator.js +45 -5
  103. package/dist/core/validator.js.map +1 -1
  104. package/dist/core/verify-report-aggregator.d.ts.map +1 -1
  105. package/dist/core/verify-report-aggregator.js +19 -2
  106. package/dist/core/verify-report-aggregator.js.map +1 -1
  107. package/dist/utils/task-utils.d.ts +4 -1
  108. package/dist/utils/task-utils.d.ts.map +1 -1
  109. package/dist/utils/task-utils.js +30 -4
  110. package/dist/utils/task-utils.js.map +1 -1
  111. package/package.json +4 -3
  112. package/templates/deploy-examples.yaml +33 -5
  113. package/templates/html/speccore-ask-explain.html +1 -1
  114. package/templates/html/speccore-ask-guide.html +1 -1
  115. package/templates/html/speccore-ask-match.html +1 -1
  116. package/templates/html/speccore-ask-pipeline.html +1 -1
  117. package/templates/html/speccore-ask-result.html +1 -1
  118. package/templates/html/speccore-help.html +2 -2
  119. package/templates/speccore-yml-example.yml +170 -50
  120. package/templates/verify-spec-example.yaml +66 -1
@@ -1 +1 @@
1
- {"version":3,"file":"doc2spec.d.ts","sourceRoot":"","sources":["../../src/commands/doc2spec.ts"],"names":[],"mappings":"AA4EA;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAerF;AAyCD,eAGA,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAS7C,CAAC;AAYF,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwD9E;AA2nBD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,MAAM,EACpB,iBAAiB,CAAC,EAAE,MAAM,EAC1B,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CA+Yf"}
1
+ {"version":3,"file":"doc2spec.d.ts","sourceRoot":"","sources":["../../src/commands/doc2spec.ts"],"names":[],"mappings":"AA4EA;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAerF;AAyCD,eAGA,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAS7C,CAAC;AAYF,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwD9E;AAyUD,MAAM,WAAW,WAAW;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AAEzD,sCAAsC;AACtC,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzD;AAkBD,oDAAoD;AACpD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAenG;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,WAAW,EAAE,EACtB,QAAQ,EAAE,MAAM,GACf;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAqCzD;AA2XD,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,EAAE,SAAS,GAAG,aAAa,CAAC;CACrC;AAgDD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,MAAM,EACpB,iBAAiB,CAAC,EAAE,MAAM,EAC1B,WAAW,CAAC,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAgaf;AAsfD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,EAAE,EACpB,MAAM,EAAE,MAAM,EAAE,GACf;IAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAqBjE"}
@@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PANDOC_FORMAT_MAP = void 0;
4
4
  exports.fixImageRefs = fixImageRefs;
5
5
  exports.doc2specCommand = doc2specCommand;
6
+ exports.buildTitleOutline = buildTitleOutline;
7
+ exports.parseTitleRenames = parseTitleRenames;
8
+ exports.applyTitleRenames = applyTitleRenames;
6
9
  exports.splitDocumentToUnits = splitDocumentToUnits;
10
+ exports.filterUnitsByTitles = filterUnitsByTitles;
7
11
  /**
8
12
  * doc2spec — 文档导入 → SpecCore Markdown
9
13
  *
@@ -400,14 +404,26 @@ async function processSingle(options) {
400
404
  // 从 MD 文件位置解析是错的——统一转为相对 MD 文件的正斜杠路径
401
405
  const mdDir = (0, path_1.dirname)(outputPath);
402
406
  content = (0, markdown_image_1.imgTagsToMarkdown)(fixImageRefs(content, mdDir, (0, path_1.join)(iterDir, '010-requirements', 'assets', 'extracted')));
403
- // 4. 接口表格检测 + 提示
407
+ // 4. 图片引用注释(告知 Task 如何引用这些图)
408
+ // v8.3.75+: 修正 Task 引用路径(从 030-tasks/Task-NNN/00-specs/ 出发需 ../../../)
409
+ content = content.replace(/^#/, `# ${platform || '通用'}需求\n\n<!-- \n 原型图片路径: 010-requirements/assets/extracted/\n converted/ 引用方式: ![原型](../assets/extracted/xxx.png)\n Task 引用方式: ![原型](../../../010-requirements/assets/extracted/xxx.png)\n 所有 Task 共享此目录,无需重复存放。\n-->\n\n#`);
410
+ // 5. AI 标题规范化(v8.3.335+ / v8.3.337 默认关闭):产品原文档标题常不规范,
411
+ // 用户明确要求(--normalize 或「按标准文档转换」)时按 SpecCore 标准标题体系
412
+ // AI 语义重写标题(正文一字不动,H1 文档标题也由 AI 给出)。
413
+ // 调整前版本快照到 backup/。--no-ai / --task 场景跳过;AI 不可用时静默降级不阻断
414
+ let normalizeReport = null;
415
+ if (options.normalize === true && options.ai !== false && !taskId) {
416
+ const result = await normalizeTitlesWithAI(content, platform || sourceBase, outputPath);
417
+ if (result) {
418
+ content = result.content;
419
+ normalizeReport = { applied: result.applied, skipped: result.skipped.length, backupPath: result.backupPath };
420
+ }
421
+ }
422
+ // 6. 接口表格检测 + 提示
404
423
  const hasInterfaceTable = /\|\s*方法\s*\|/i.test(content) || /\|\s*METHOD\s*\|/i.test(content);
405
424
  if (!hasInterfaceTable) {
406
425
  content += `\n---\n> ⚠️ Word 自动转换,请在下方补充接口定义表格后运行 speccore execute:\n\n| 方法 | 路径 | 说明 |\n| :--- | :--- | :--- |\n| | | |\n`;
407
426
  }
408
- // 5. 图片引用注释(告知 Task 如何引用这些图)
409
- // v8.3.75+: 修正 Task 引用路径(从 030-tasks/Task-NNN/00-specs/ 出发需 ../../../)
410
- content = content.replace(/^#/, `# ${platform || '通用'}需求\n\n<!-- \n 原型图片路径: 010-requirements/assets/extracted/\n converted/ 引用方式: ![原型](../assets/extracted/xxx.png)\n Task 引用方式: ![原型](../../../010-requirements/assets/extracted/xxx.png)\n 所有 Task 共享此目录,无需重复存放。\n-->\n\n#`);
411
427
  await (0, fs_extra_1.writeFile)(outputPath, content);
412
428
  // ── 更新 INDEX.md ──
413
429
  const indexPath = (0, path_1.join)(iterDir, '010-requirements', 'INDEX.md');
@@ -452,6 +468,19 @@ async function processSingle(options) {
452
468
  const fileStat = await (0, fs_extra_1.stat)(outputPath);
453
469
  logger_1.logger.success(`输出: ${outputPath} (${fileStat.size} bytes)`);
454
470
  logger_1.logger.info(`图片: ${imageCount} 张 → ${imageDir}/`);
471
+ // 标题规范化结果汇报(AI 重写统计 + 调整前快照位置)
472
+ if (normalizeReport) {
473
+ logger_1.logger.info(`📝 标题规范化: ${normalizeReport.applied} 处重写`);
474
+ if (normalizeReport.skipped > 0) {
475
+ logger_1.logger.info(` ⚠️ ${normalizeReport.skipped} 处 AI 映射未匹配(章节原样保留)`);
476
+ }
477
+ if (normalizeReport.backupPath) {
478
+ logger_1.logger.info(` 💾 调整前快照: ${normalizeReport.backupPath}`);
479
+ }
480
+ }
481
+ else if (options.normalize === true && options.ai !== false && !taskId) {
482
+ logger_1.logger.info('💡 标题规范化跳过(未配置 AI provider)——可配置 SPECCORE_LLM_KEY 后重试');
483
+ }
455
484
  logger_1.logger.info(`接口表: ${hasInterfaceTable ? '✅' : '⚠️ 缺失(已追加提示)'}`);
456
485
  logger_1.logger.info(`🔍 质量: ${grade} ${report.score}/100 (${report.errors}错误 ${report.warnings}警告)`);
457
486
  logger_1.logger.info(` 详见: ${reportPath}`);
@@ -486,6 +515,182 @@ async function processSingle(options) {
486
515
  throw error;
487
516
  }
488
517
  }
518
+ // ================================================================
519
+ // AI 标题规范化(v8.3.335+):转换后按 SpecCore 标准标题体系重写标题
520
+ //
521
+ // 设计原则:
522
+ // 1. AI 只产「标题映射」,CLI 确定性回写——正文一字不动、行数不变,天然防丢章节
523
+ // 2. 应用前把「规范化前版本」快照到 backup/,随时可回溯(用户要求「以防要用」)
524
+ // 3. AI 不可用 / 映射质量差 → 静默跳过,绝不阻断转换主链路
525
+ // ================================================================
526
+ /** SpecCore 标准需求文档标题体系(AI 标题规范化的参照规范) */
527
+ const STANDARD_DOC_TITLES = {
528
+ docTitleSuffix: '需求', // H1 格式:{文档名}需求
529
+ globalSections: [
530
+ // 全局章节标准名(项目级通用信息,不构成功能单元)
531
+ '文档概述', '产品定位', '项目背景', '目标用户', '用户画像',
532
+ '名词约定', '术语表', '功能架构', '非功能需求', '验收标准',
533
+ '上线计划', '风险与缓解', '关联文档',
534
+ ],
535
+ featurePrefix: '功能模块', // 功能章节格式:功能模块{中文序号}:{功能名}
536
+ featureSubsections: [
537
+ // 功能模块下常用子节标准名
538
+ '用户场景', '页面布局', '操作步骤', '业务规则', '异常场景处理', '验收标准',
539
+ ],
540
+ };
541
+ /** 提取文档标题大纲(含原层级),供 AI 参照重写(导出供测试) */
542
+ function buildTitleOutline(content) {
543
+ return content.split('\n').filter((l) => /^#{1,6}\s+/.test(l)).join('\n');
544
+ }
545
+ /** 提取各章节开头内容(标题 + 首 3 行正文),给 AI 提供语义依据 */
546
+ function buildSectionHeads(content) {
547
+ const lines = content.split('\n');
548
+ const heads = [];
549
+ for (let i = 0; i < lines.length; i++) {
550
+ if (!/^#{1,6}\s+/.test(lines[i]))
551
+ continue;
552
+ const body = [];
553
+ for (let j = i + 1; j < lines.length && body.length < 3 && !/^#{1,6}\s+/.test(lines[j]); j++) {
554
+ const t = lines[j].trim();
555
+ if (t)
556
+ body.push(t.slice(0, 80));
557
+ }
558
+ heads.push(`${lines[i]}\n${body.join('\n')}`);
559
+ }
560
+ return heads.join('\n\n');
561
+ }
562
+ /** 解析 AI 返回的标题映射 JSON(容忍 markdown 代码围栏等噪声,导出供测试) */
563
+ function parseTitleRenames(text) {
564
+ try {
565
+ const cleaned = text.replace(/```(?:json)?\s*/gi, '').replace(/```/g, '').trim();
566
+ const start = cleaned.indexOf('{');
567
+ const end = cleaned.lastIndexOf('}');
568
+ if (start < 0 || end <= start)
569
+ return null;
570
+ const parsed = JSON.parse(cleaned.slice(start, end + 1));
571
+ const renames = (Array.isArray(parsed.renames) ? parsed.renames : [])
572
+ .filter((r) => r && typeof r.from === 'string' && typeof r.to === 'string' && r.from.trim() && r.to.trim())
573
+ .map((r) => ({ from: r.from.trim(), to: r.to.trim() }));
574
+ const docTitle = typeof parsed.docTitle === 'string' && parsed.docTitle.trim() ? parsed.docTitle.trim() : '';
575
+ return renames.length > 0 ? { renames, docTitle } : null;
576
+ }
577
+ catch {
578
+ return null;
579
+ }
580
+ }
581
+ /**
582
+ * CLI 确定性回写标题映射:只替换标题行文字,正文一字不动。
583
+ * from 匹配时两侧都剥离章节编号(容忍 AI 带不带编号),同名标题按出现顺序一一对应。
584
+ * 未匹配到的 from 记入 skipped(章节原样保留),绝不静默丢章节。
585
+ */
586
+ function applyTitleRenames(content, renames, docTitle) {
587
+ const lines = content.split('\n');
588
+ const used = new Set();
589
+ const skipped = [];
590
+ let applied = 0;
591
+ let h1Done = false;
592
+ for (let i = 0; i < lines.length; i++) {
593
+ const m = lines[i].match(/^(#{1,6})\s+(.+)$/);
594
+ if (!m)
595
+ continue;
596
+ const level = m[1].length;
597
+ const rawText = m[2].trim();
598
+ // H1:整行替换为 docTitle(文档唯一标题;AI 未给出时保持原样)
599
+ if (level === 1) {
600
+ if (!h1Done && docTitle) {
601
+ lines[i] = `# ${docTitle}`;
602
+ h1Done = true;
603
+ applied++;
604
+ }
605
+ continue;
606
+ }
607
+ // 其余标题:strip 编号前缀后与 from 精确匹配
608
+ const normalized = stripSectionNo(rawText).trim();
609
+ const idx = renames.findIndex((r, j) => !used.has(j) && stripSectionNo(r.from).trim() === normalized);
610
+ if (idx < 0)
611
+ continue;
612
+ used.add(idx);
613
+ // to 需为完整标题行(含 # 前缀,层级由 AI 决定);未带前缀时沿用原层级兜底
614
+ lines[i] = /^#{1,6}\s+/.test(renames[idx].to) ? renames[idx].to : `${'#'.repeat(level)} ${renames[idx].to}`;
615
+ applied++;
616
+ }
617
+ for (let j = 0; j < renames.length; j++) {
618
+ if (!used.has(j))
619
+ skipped.push(renames[j].from);
620
+ }
621
+ return { content: lines.join('\n'), applied, skipped };
622
+ }
623
+ /** 标题规范化前,把「规范化前版本」快照到 010-requirements/backup/ */
624
+ async function backupPreNormalizeContent(content, outputPath) {
625
+ try {
626
+ const backupDir = (0, path_1.join)((0, path_1.dirname)((0, path_1.dirname)(outputPath)), 'backup');
627
+ await (0, fs_extra_1.ensureDir)(backupDir);
628
+ const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
629
+ const backupPath = (0, path_1.join)(backupDir, `${(0, path_1.basename)(outputPath, '.md')}-${ts}.md`);
630
+ await (0, fs_extra_1.writeFile)(backupPath, content);
631
+ return backupPath;
632
+ }
633
+ catch {
634
+ return null;
635
+ }
636
+ }
637
+ /**
638
+ * AI 标题规范化主编排:调用 LLM 产标题映射 → 回写 → 快照。
639
+ * 返回 null 表示跳过(AI 不可用/无有效映射),调用方原样保留文档。
640
+ */
641
+ async function normalizeTitlesWithAI(content, docLabel, outputPath) {
642
+ try {
643
+ const outline = buildTitleOutline(content);
644
+ // 标题太少(<3 个)时文档无结构可调,直接跳过
645
+ if (outline.split('\n').filter(Boolean).length < 3)
646
+ return null;
647
+ const systemPrompt = [
648
+ '你是资深产品需求文档编辑。任务:把 PRD 文档的章节标题按 SpecCore 标准标题体系重写,正文一字不动。',
649
+ '',
650
+ 'SpecCore 标准标题体系:',
651
+ `1. 文档唯一 H1 标题:{文档名}${STANDARD_DOC_TITLES.docTitleSuffix}(如「${docLabel}需求」)`,
652
+ `2. 全局章节(项目级通用信息,不算功能单元)归 H2,标准名:${STANDARD_DOC_TITLES.globalSections.join('、')}`,
653
+ `3. 功能模块归 H2,标题格式「${STANDARD_DOC_TITLES.featurePrefix}{中文序号}:{功能名}」(序号从「一」连续递增)`,
654
+ ` 功能模块下子节归 H3,常用标准名:${STANDARD_DOC_TITLES.featureSubsections.join('、')}`,
655
+ '4. 层级理顺:H1 只留文档标题一个;层级不跳级(### 后不能直接 #####)',
656
+ '',
657
+ '重写规则:',
658
+ '- 只改标题行文字与层级,不改章节顺序,不增删章节',
659
+ '- 原标题语义映射到最接近的标准名(「背景介绍」→「项目背景」;「会议管理」→「功能模块一:会议室管理」)',
660
+ '- 编号连续:全局章节与功能模块共用同一组中文序号(一、二、三…),子节编号 x.1、x.2 连续',
661
+ '- 无法归类的章节保留原意、统一格式,不要强行套标准名',
662
+ '',
663
+ '严格只返回 JSON,不要输出任何 JSON 以外的文字:',
664
+ '{"renames":[{"from":"原标题文字","to":"## 新标题行"}],"docTitle":"新文档标题"}',
665
+ '- from:文档中该标题去掉「#」与编号前缀后的精确文字',
666
+ '- to:完整新标题行(必须含 # 前缀,层级由你决定)',
667
+ '- 覆盖除 H1 外的所有标题;H1 的新内容放 docTitle',
668
+ ].join('\n');
669
+ // 大文档只传前 20000 字符的章节抽样(大纲仍覆盖全文,语义依据够用)
670
+ const truncated = content.length > 20000;
671
+ const sample = truncated ? content.slice(0, 20000) : content;
672
+ const userPrompt = `文档标题大纲(含原层级):\n\n${outline}\n\n` +
673
+ `各章节开头内容(语义依据${truncated ? ',文档较长仅抽样' : ''}):\n\n${buildSectionHeads(sample)}`;
674
+ const providers = await (0, ask_llm_1.resolveAiProviders)('doc2spec');
675
+ const response = await (0, ask_llm_1.callLlm)(systemPrompt, userPrompt, providers, 3000);
676
+ if (!response)
677
+ return null;
678
+ const parsed = parseTitleRenames(response);
679
+ if (!parsed)
680
+ return null;
681
+ // 防丢章节:AI 映射命中过少说明输出质量差,放弃本次规范化(宁可不动也别改错)
682
+ const before = content;
683
+ const applied = applyTitleRenames(content, parsed.renames, parsed.docTitle);
684
+ if (applied.applied < Math.min(3, Math.ceil(parsed.renames.length / 2)))
685
+ return null;
686
+ // 应用前快照「规范化前版本」(用户要求备份以防要用)
687
+ const backupPath = await backupPreNormalizeContent(before, outputPath);
688
+ return { content: applied.content, applied: applied.applied, skipped: applied.skipped, backupPath };
689
+ }
690
+ catch {
691
+ return null;
692
+ }
693
+ }
489
694
  /**
490
695
  * --classify 模式:扫描 sources/ 下所有文件,AI 智能分类后输出到 staging/
491
696
  *
@@ -806,7 +1011,7 @@ async function readUnitTitle(unitDir) {
806
1011
  * 将转换后的文档拆分为功能单元,写入 features/
807
1012
  * v8.3.191+: 导出供 run 流水线「切分功能单元」步骤复用(对已转换文档切分,无需 --file)
808
1013
  */
809
- async function splitDocumentToUnits(iterDir, outputPath, platformLabel, sourceFileName, translateEn, translateResponse, onDuplicate) {
1014
+ async function splitDocumentToUnits(iterDir, outputPath, platformLabel, sourceFileName, translateEn, translateResponse, onDuplicate, titles) {
810
1015
  const content = await (0, fs_extra_1.readFile)(outputPath, 'utf-8');
811
1016
  // v8.3.263: 收集同目录其它文档名(端级文档)——总纲文档(如 overview-requirements.md)
812
1017
  // 的端级汇总章节与端文档切出的单元语义重复(文本层面不相似,Jaccard 拦不住),
@@ -924,6 +1129,22 @@ async function splitDocumentToUnits(iterDir, outputPath, platformLabel, sourceFi
924
1129
  }
925
1130
  }
926
1131
  catch { }
1132
+ // v8.3.334+: 标题过滤——用户指定只切某些标题的章节时,其余功能单元不参与
1133
+ // 英译/写盘(不占序号);globalSections 不受影响,照常归 overview
1134
+ if (titles && titles.length > 0) {
1135
+ const { kept, dropped, unmatched } = filterUnitsByTitles(units, titles);
1136
+ units = kept;
1137
+ if (dropped.length > 0) {
1138
+ logger_1.logger.info(` 🎯 标题过滤: 指定 ${titles.length} 个标题,保留 ${kept.length}/${kept.length + dropped.length} 个单元`);
1139
+ logger_1.logger.info(` ⏭️ 跳过 ${dropped.length} 个非指定单元: ${dropped.join('、')}`);
1140
+ }
1141
+ if (unmatched.length > 0) {
1142
+ logger_1.logger.warn(` ⚠️ 以下标题未在文档中命中(检查名字/编号): ${unmatched.join('、')}`);
1143
+ }
1144
+ if (units.length === 0) {
1145
+ logger_1.logger.warn(' ⏹️ 标题过滤后无剩余功能单元,仅处理全局章节(如有)');
1146
+ }
1147
+ }
927
1148
  let written = 0;
928
1149
  let skipped = 0;
929
1150
  // v8.3.272+: 标题英译(循环外批量:LLM 翻译 + 缓存,失败回落拼音)
@@ -1720,6 +1941,37 @@ function stripSectionPrefix(t) {
1720
1941
  .replace(/^[((][一二三四五六七八九十百\d]+[))][、..\s]*/, '')
1721
1942
  .trim();
1722
1943
  }
1944
+ /**
1945
+ * v8.3.334+: 标题过滤——用户指定只切某些标题的章节时,过滤功能单元列表。
1946
+ *
1947
+ * 匹配语义:归一化(去章节编号前缀/去空白/小写)后双向包含——
1948
+ * 「预订管理」命中「3.1 预订管理」「预订管理流程」;「预订管理」与「会议预订管理」也命中。
1949
+ * 返回 kept(保留)/ dropped(被过滤的单元标题)/ unmatched(用户指定但未命中的标题名)。
1950
+ */
1951
+ function filterUnitsByTitles(units, titles) {
1952
+ const norm = (t) => stripSectionPrefix(t).toLowerCase().replace(/[\s ·•・]+/g, '');
1953
+ const wanted = titles.map((t) => norm(t)).filter(Boolean);
1954
+ const kept = [];
1955
+ const dropped = [];
1956
+ for (const u of units) {
1957
+ const nt = norm(u.title);
1958
+ const hit = nt.length > 0 && wanted.some((w) => w.includes(nt) || nt.includes(w));
1959
+ if (hit)
1960
+ kept.push(u);
1961
+ else
1962
+ dropped.push(u.title);
1963
+ }
1964
+ const unmatched = titles.filter((t) => {
1965
+ const w = norm(t);
1966
+ if (!w)
1967
+ return false;
1968
+ return !units.some((u) => {
1969
+ const nt = norm(u.title);
1970
+ return nt.length > 0 && (w.includes(nt) || nt.includes(w));
1971
+ });
1972
+ });
1973
+ return { kept, dropped, unmatched };
1974
+ }
1723
1975
  function sanitizeUnitName(name) {
1724
1976
  return name
1725
1977
  .replace(/[^a-zA-Z0-9\u4e00-\u9fa5\-_]/g, '')