@topmindspace/tms-skills 0.1.13 → 0.1.14

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 (44) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.en.md +3 -3
  3. package/README.md +3 -3
  4. package/package.json +1 -1
  5. package/top-ppt-html/README.en.md +2 -2
  6. package/top-ppt-html/README.md +1 -1
  7. package/top-ppt-html/SKILL.md +3 -3
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +59 -20
  9. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +64 -26
  10. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +64 -42
  11. package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +85 -46
  12. package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.model.json +1 -1
  13. package/top-ppt-html/assets/pptx-export.js +40 -17
  14. package/top-ppt-html/assets/showcase/showcase-charts.png +0 -0
  15. package/top-ppt-html/assets/showcase/showcase-cover.png +0 -0
  16. package/top-ppt-html/assets/showcase/showcase-gates.png +0 -0
  17. package/top-ppt-html/assets/showcase/showcase-modes.png +0 -0
  18. package/top-ppt-html/assets/showcase/showcase-positioning.png +0 -0
  19. package/top-ppt-html/assets/showcase/showcase-split.png +0 -0
  20. package/top-ppt-html/assets/showcase/showcase-toolbar.png +0 -0
  21. package/top-ppt-html/assets/style-gallery.html +3 -3
  22. package/top-ppt-html/assets/templates/architecture.html +59 -20
  23. package/top-ppt-html/assets/templates/engine.css +17 -1
  24. package/top-ppt-html/assets/templates/presentation.html +59 -20
  25. package/top-ppt-html/assets/templates/research.html +60 -27
  26. package/top-ppt-html/assets/templates/ui.js +1 -1
  27. package/top-ppt-html/package-lock.json +2 -2
  28. package/top-ppt-html/package.json +1 -1
  29. package/top-ppt-html/references/components-atoms.md +14 -1
  30. package/top-ppt-html/references/content-rules.md +1 -1
  31. package/top-ppt-html/references/design-system.md +1 -1
  32. package/top-ppt-html/references/icons.md +19 -0
  33. package/top-ppt-html/references/layout-grammar.md +17 -0
  34. package/top-ppt-html/references/layouts-combo.md +2 -4
  35. package/top-ppt-html/references/modes.md +1 -1
  36. package/top-ppt-html/references/pptx-export.md +1 -1
  37. package/top-ppt-html/scripts/build_pptx.js +50 -22
  38. package/top-ppt-html/scripts/layout-constants.json +18 -6
  39. package/top-ppt-html/scripts/quality_gate.py +2 -2
  40. package/top-ppt-html/scripts/render_from_model.py +2 -4
  41. package/top-ppt-html/scripts/scaffold_report.py +4 -10
  42. package/top-ppt-html/scripts/test_feedback_gates.py +2 -2
  43. package/top-ppt-html/scripts/validate_pptx.py +27 -11
  44. package/top-ppt-html/scripts/validate_report.py +27 -5
@@ -264,7 +264,8 @@ section.band{scroll-snap-align:start}
264
264
  svh 兼容移动端浏览器工具栏;旧浏览器落到 100vh。 */
265
265
  .band{padding-block:var(--band-pad);min-height:var(--band-min);
266
266
  display:flex;flex-direction:column;justify-content:center}
267
- .band>.wrap{width:100%}
267
+ .band>.wrap{width:100%;min-height:0}
268
+ .band>.wrap>.grid,.band>.wrap>.stack,.band>.wrap>.fig,.band>.wrap>.cols-2,.band>.wrap>.cols-3,.band>.wrap>.exhibit{min-height:0}
268
269
  .band--top{justify-content:flex-start} /* 内容约六七成屏:贴顶 + 底部自然留白 */
269
270
  .band--fit{justify-content:center} /* 内容接近一屏:垂直居中(默认行为,保留以兼容旧报告) */
270
271
  .band--flow{min-height:0;justify-content:flex-start} /* 长内容页(附录/全量参考)显式退出固定页高 */
@@ -505,6 +506,21 @@ tbody tr:hover{background:var(--surface-1)}
505
506
  td strong{color:var(--text);font-weight:600}
506
507
  td.k{color:var(--text);font-weight:500;white-space:nowrap}
507
508
  td.num{font-variant-numeric:tabular-nums}
509
+ /* ── 结论条(MD3 tonal strip · 本页重点收口;默认不显示「So what」标签) ──
510
+ 与上方主内容呼吸间距 ≥ --sp-6;flex:none 防止被主内容挤压;一屏页不得压进注释带。 */
511
+ .band>.wrap:has(>.sowhat),.band>.wrap:has(>.note){display:flex;flex-direction:column}
512
+ .sowhat{display:flex;align-items:flex-start;gap:var(--sp-3);flex:none;
513
+ margin-top:clamp(28px,3.6vh,48px); /* ≥ --sp-6 量级,修「贴底」 */
514
+ padding:var(--sp-4) var(--sp-5);background:var(--accent-soft);border:1px solid var(--accent-soft-2);
515
+ border-left:4px solid var(--accent);border-radius:var(--r-md);max-width:100%}
516
+ .sowhat__k{display:none}
517
+ .sowhat--labeled .sowhat__k{display:inline;font-size:var(--fs-xs);font-weight:700;letter-spacing:.1em;
518
+ text-transform:uppercase;color:var(--accent-text);white-space:nowrap;flex:none;padding-top:.15em}
519
+ .sowhat__v,.sowhat>:not(.sowhat__k){font-size:var(--fs-h3);color:var(--text);font-weight:var(--fw-title);
520
+ line-height:1.55;flex:1;min-width:0}
521
+ .sowhat__v p,.sowhat__stack{margin:0}
522
+ .sowhat__stack{display:flex;flex-direction:column;gap:var(--sp-2)}
523
+
508
524
  .note{display:flex;gap:var(--sp-4);padding:clamp(20px,2vw,28px);border-radius:var(--r-lg);
509
525
  background:var(--accent-soft);border-left:3px solid var(--accent)}
510
526
  .note__ico{width:26px;height:26px;flex:none;border-radius:50%;background:var(--accent);
@@ -910,13 +926,7 @@ color:var(--accent-text);white-space:nowrap}
910
926
  .exhibit__src{margin-top:var(--sp-3);padding-top:var(--sp-2);border-top:1px solid var(--border-soft);
911
927
  font-size:var(--fs-xs);color:var(--text-3);line-height:1.5}
912
928
 
913
- /* ── research 组件:so-what 结论条 ── */
914
- .sowhat{display:flex;align-items:baseline;gap:var(--sp-3);margin-top:var(--sp-5);
915
- padding:var(--sp-3) var(--sp-4);border-left:3px solid var(--accent);background:var(--accent-soft);
916
- border-radius:0 var(--r-md) var(--r-md) 0}
917
- .sowhat__k{font-size:var(--fs-xs);font-weight:700;letter-spacing:.1em;text-transform:uppercase;
918
- color:var(--accent-text);white-space:nowrap;flex:none}
919
- .sowhat__v{font-size:var(--fs-body);color:var(--text);font-weight:500;line-height:1.55}
929
+ /* ── 结论条 .sowhat 已迁入 engine.css(MD3 衬条 · 无 So what 标签)── */
920
930
 
921
931
  /* ── research 组件:半表半图分栏(R7) ── */
922
932
  .g-half{grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);align-items:start}
@@ -1756,7 +1766,7 @@ if(e.isIntersecting){e.target.classList.add('in');o.unobserve(e.target)}})},
1756
1766
  var PROMPT=[
1757
1767
  '请为这份 HTML 报告生成可编辑 PPTX 并交付:',
1758
1768
  '1) 阅读 references/pptx-export.md 与 references/modes.md(三模式独立规则);',
1759
- '2) 核对/补全 window.REPORT_MODEL 为与正文严格一致的内容(含 mode/style/theme/title/agenda/sections/closing;theme 为 light 或 dark、与页面 data-theme 一致——PPTX 按此导出亮色版或深色版;29 种页型字段与 30 类 chart.type 见 scripts/model-schema.json;research 关键图表页用 type:"exhibit" 并填 exhibitNo/soWhat/footnote);',
1769
+ '2) 核对/补全 window.REPORT_MODEL 为与正文严格一致的内容(含 mode/style/theme/title/agenda/sections/closing;theme 为 light 或 dark、与页面 data-theme 一致——PPTX 按此导出亮色版或深色版;29 种页型字段与 30 类 chart.type 见 scripts/model-schema.json;research 关键图表页用 type:"exhibit" 并填 exhibitNo/soWhat(结论条)/footnote);',
1760
1770
  '3) 运行 python scripts/validate_report.py <报告.html> 直到全 PASS(含模型一致性检查);',
1761
1771
  '4) 智能体精导并质检(0/0 才交付):python scripts/extract_model.py <报告.html> → NODE_PATH=<pptxgenjs 所在 node_modules> node scripts/build_pptx.js <报告.pptx> --model=<报告.model.json>(如需覆盖模型主题可加 --theme=light|dark) → python scripts/validate_pptx.py <报告.pptx> --strict --model=<报告.model.json>。',
1762
1772
  '报告文件:<请填写报告.html 的路径>'
@@ -2067,7 +2077,7 @@ window.REPORT_MODEL = {
2067
2077
 
2068
2078
  /* __TOPPPT_RUNTIME_START__ */
2069
2079
  /* ══ PPTX 导出运行时(assets/pptx-export.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */
2070
- /* __TOPPPT_RUNTIME_SHA__:77ca318e7a510e01 */
2080
+ /* __TOPPPT_RUNTIME_SHA__:f4dc2ab366ea79c4 */
2071
2081
  /* ═══════════════════════════════════════════════════════════════════════════
2072
2082
  * TopPPT HTML· 浏览器端 PPTX 预览运行时
2073
2083
  * 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
@@ -2148,7 +2158,7 @@ var PRESETS_DARK = {
2148
2158
  var STYLE_DATA_COLORS = {"business-blue":["1a73e8","3526bb","21a199","b77148","99983d"],"apple-mono":["1d1d1f","0071e3","0f7b6c","c2410c","7c3aed"],"mckinsey":["003a70","2826bb","21a18e","b76848","99903d"],"brand-red":["d0021b","bb6326","d634ae","40a182","438aa8"],"warm-sand":["96681f","839324","cf3b3e","4d88b2","4a47a4"],"deep-teal":["0f6b5c","2683bb","22aa3e","b74870","a85c43"],"graphite-dark":["0369a1","2637bb","1f9977","b75948","a18a40"],"indigo-violet":["5b5bd6","7432ae","4197c8","a08f4b","7ca04b"],"spectrum":["4563ef","5b26bb","269ebb","af8746","83993d"]};
2149
2159
  var STYLE_DATA_COLORS_DARK = {"business-blue":["8ab4f8","7966e0","88e7e5","d4ad91","cbce83"],"apple-mono":["f5f5f7","2997ff","2dd4bf","ff9f0a","bf5af2"],"mckinsey":["7fb2e5","6f6bdb","8ce3d8","d1a694","cbc586"],"brand-red":["ff5a66","e09e66","e788cb","91d4c2","83b4ce"],"warm-sand":["d9b36a","c2d571","df9391","97b5ce","8e89c7"],"deep-teal":["4fc3b0","7aaecd","97d8a4","c99cac","c29c8f"],"graphite-dark":["4da3ff","6c66e0","88e7dc","d4a691","cec983"],"indigo-violet":["a5a4f0","a967e0","89c3e7","d3c792","aece83"],"spectrum":["8ba0f8","9066e0","88d6e7","d4ba91","bdce83"]};
2150
2160
  var GRID = {"columns":12,"gutter":0.16,"colW":0.8644,"x":[0.6,1.6244,2.6488,3.6732,4.6976,5.722,6.7464,7.7708,8.7952,9.8196,10.844,11.8684,12.733],"y":[2.35,3.27,4.19,5.11,6.03,6.9],"safe":{"top":0.4,"bottom":0.4,"left":0.6,"right":0.6}};
2151
- var TYPO = {"levels":[{"id":"C0","name":"封面/章节幕标题","role":"coverTitle","usage":"封面主标题、章节幕标题"},{"id":"T1","name":"页码/章节徽章","role":"caption","usage":"页码徽章、章节编号"},{"id":"T2","name":"页面主标题/结论标题","role":"h1","usage":"每页顶部结论句(research 行动标题)"},{"id":"T3","name":"副标题/语境说明","role":"lead","usage":"主标题下方导语"},{"id":"T4","name":"模块标题/图表标题","role":"h2","usage":"卡片标题、图表标题、面板标题"},{"id":"T5","name":"证据编号/轻量标签","role":"micro","usage":"Exhibit 编号、状态标签"},{"id":"T6","name":"证据块标题/小节标题","role":"h2","usage":"分栏小标题、证据块标题"},{"id":"T7","name":"正文解释段落","role":"body","usage":"证据解释、管理解读正文、表格正文"},{"id":"T8","name":"结论条文字","role":"body","usage":"so-what 结论条、核心结论框"},{"id":"T9","name":"SO WHAT 标签","role":"caption","usage":"so-what 标签行"},{"id":"T10","name":"SO WHAT 正文/业务含义","role":"body","usage":"so-what 正文、行动含义、表格行动项"},{"id":"T11","name":"图表轴/图例/刻度/微标签","role":"micro","usage":"坐标轴、图例、刻度、单位(禁用于表格正文与完整短句)"},{"id":"T12","name":"图表数据标签","role":"caption","usage":"折线点值、柱形标签、百分比标注"},{"id":"T13","name":"关键 KPI 大数字","role":"h1","usage":"KPI 大数字、hero 数值"},{"id":"T14","name":"注释/口径/来源/页脚","role":"micro","usage":"footnote、来源行、页脚、小页码"}],"tableSemanticRule":{"body":["T7","T10"],"microAllowed":["T11"],"microForbiddenFor":["表格正文","行动项","风险项","解释句","建议句","长项目符号","完整短句"]}};
2161
+ var TYPO = {"levels":[{"id":"C0","name":"封面/章节幕标题","role":"coverTitle","usage":"封面主标题、章节幕标题"},{"id":"T1","name":"页码/章节徽章","role":"caption","usage":"页码徽章、章节编号"},{"id":"T2","name":"页面主标题/结论标题","role":"h1","usage":"每页顶部结论句(research 行动标题)"},{"id":"T3","name":"副标题/语境说明","role":"lead","usage":"主标题下方导语"},{"id":"T4","name":"模块标题/图表标题","role":"h2","usage":"卡片标题、图表标题、面板标题"},{"id":"T5","name":"证据编号/轻量标签","role":"micro","usage":"Exhibit 编号、状态标签"},{"id":"T6","name":"证据块标题/小节标题","role":"h2","usage":"分栏小标题、证据块标题"},{"id":"T7","name":"正文解释段落","role":"body","usage":"证据解释、管理解读正文、表格正文"},{"id":"T8","name":"结论条文字","role":"body","usage":"结论条、核心结论框"},{"id":"T9","name":"结论条标签(可选·默认隐藏)","role":"caption","usage":"结论条可选标签(默认不显示)"},{"id":"T10","name":"结论条正文/业务含义","role":"body","usage":"结论条正文、行动含义、表格行动项"},{"id":"T11","name":"图表轴/图例/刻度/微标签","role":"micro","usage":"坐标轴、图例、刻度、单位(禁用于表格正文与完整短句)"},{"id":"T12","name":"图表数据标签","role":"caption","usage":"折线点值、柱形标签、百分比标注"},{"id":"T13","name":"关键 KPI 大数字","role":"h1","usage":"KPI 大数字、hero 数值"},{"id":"T14","name":"注释/口径/来源/页脚","role":"micro","usage":"footnote、来源行、页脚、小页码"}],"tableSemanticRule":{"body":["T7","T10"],"microAllowed":["T11"],"microForbiddenFor":["表格正文","行动项","风险项","解释句","建议句","长项目符号","完整短句"]}};
2152
2162
  var CONT = {"pad":{"card":0.18,"panel":0.2,"cell":0.08,"band":0.16,"soWhat":0.16,"kpi":0.18,"chart":0.1,"head":0.05,"footer":0.05,"node":0.06},"minPad":0.06,"textMetrics":{"lineFactor":1.45,"emAsciiRatio":0.52},"overflowRule":"溢出处置优先级(禁静默截断):① 重构承载(列表/卡片/表/图/组合版式)→ ② 拆页/分章(议程→主张→证据卡→明细;01a/01b;信息须完整)→ ③ 换布局形态(V1–V4/多列/卡栅格)→ ④ 有限 fontShrink(阶梯内、模式 floor 以上、最多 4 档)→ ⑤ 禁止:静默截断、砍 so-what/证据/口径、为审美稀疏删决策必需信息。单页字数预算是预警不是删字许可证。","fontShrink":{"ladder":[15,14,13.5,13,12.5,12,11.5,11,10.5,10,9.5,9,8.5],"maxShrinkSteps":4,"floorPt":{"presentation":10,"research":9,"architecture":10},"preferContentFirst":true,"warnBelow":9.5},"continuousTextRule":"语义连续的一句话 / so-what 主句 / 结论句 / 表格单元格正文不得拆成多个独立文本框(避免异常空格、断句、基线漂移);局部加粗高亮必须用同一文本框内富文本。"};
2153
2163
  var REG = {"bar":{"html":"svg-bars","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":160}},"hbar":{"html":"svg-hbars","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":100}},"stack":{"html":"svg-stack","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":160},"nativeOpt":{"barGrouping":"stacked"}},"stackline":{"html":"svg-stackline","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":40},"nativeOpt":{"barGrouping":"percentStacked"}},"line":{"html":"svg-line","pptx":"native","nativeType":"line","dataTable":"notes","min":{"vbHeightMin":160}},"dualline":{"html":"svg-dualline","pptx":"native","nativeType":"line","dataTable":"notes","min":{"vbHeightMin":160}},"area":{"html":"svg-area","pptx":"native","nativeType":"area","dataTable":"notes","min":{"vbHeightMin":160}},"donut":{"html":"svg-donut","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":140}},"multidonut":{"html":"svg-mdonut","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":140}},"pie":{"html":"svg-pie","pptx":"native","nativeType":"pie","dataTable":"notes","min":{"pxWidthMin":140}},"radar":{"html":"svg-radar","pptx":"native","nativeType":"radar","dataTable":"notes","min":{"pxWidthMin":220}},"scatter":{"html":"svg-scatter","pptx":"native","nativeType":"scatter","dataTable":"notes","min":{"vbHeightMin":180}},"bubble":{"html":"svg-bubble","pptx":"native","nativeType":"bubble","dataTable":"notes","min":{"vbHeightMin":180}},"waterfall":{"html":"svg-wf","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":170},"nativeTrick":"stacked+hiddenBase+connector"},"gauge":{"html":"svg-gauge","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":180},"nativeTrick":"doughnut+firstSliceAng+hiddenRemainder"},"pareto":{"html":"svg-pareto","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":180},"nativeTrick":"multiType+bar+line"},"gantt":{"html":"svg-gantt","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":130},"path":{"forbidPresetShapes":true}},"vsbar":{"html":"svg-vsbar","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":100},"path":{"forbidPresetShapes":true}},"progress":{"html":"svg-progress","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":100},"path":{"forbidPresetShapes":true}},"sparkline":{"html":"svg-spark","pptx":"shape","dataTable":"off","min":{"vbHeightMin":20},"path":{"forbidPresetShapes":true}},"funnel":{"html":"svg-funnel","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"minSamplePoints":8,"forbidPresetShapes":true}},"treemap":{"html":"svg-treemap","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":170},"path":{"forbidPresetShapes":true}},"marimekko":{"html":"svg-marimekko","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"boxplot":{"html":"svg-boxplot","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}},"network":{"html":"svg-network","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":200},"path":{"forbidPresetShapes":true}},"sankey":{"html":"svg-sankey","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":200},"path":{"minSamplePoints":16,"trackBothEdges":true,"forbidPresetShapes":true}},"streamgraph":{"html":"svg-stream","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":160},"path":{"minSamplePoints":24,"trackBothEdges":true,"forbidPresetShapes":true}},"slope":{"html":"svg-slope","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}},"dumbbell":{"html":"svg-dumbbell","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"lollipop":{"html":"svg-lollipop","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":140},"path":{"forbidPresetShapes":true}},"dotplot":{"html":"svg-dotplot","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":120},"path":{"forbidPresetShapes":true}},"bulletchart":{"html":"svg-bullet","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":140},"path":{"forbidPresetShapes":true}},"waffle":{"html":"svg-waffle","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"radialbar":{"html":"svg-radialbar","pptx":"shape","dataTable":"notes","min":{"pxWidthMin":200},"path":{"minSamplePoints":16,"forbidPresetShapes":true}},"rose":{"html":"svg-rose","pptx":"shape","dataTable":"notes","min":{"pxWidthMin":180},"path":{"minSamplePoints":16,"forbidPresetShapes":true}},"candlestick":{"html":"svg-candle","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}}};
2154
2164
  var DEEP = {"triggers":["高保真","1:1","一比一","精确还原","按图还原","正式交付","不能偏移","像素级","严格对照","逐页验收"],"complexCharts":["sankey","streamgraph","treemap","marimekko","boxplot","network","rose","radialbar","candlestick","funnel"],"checks":{"anchors":true,"containerOverflow":true,"renderCompare":true,"manifest":true},"renderCompare":{"engine":"soffice","fallback":"powerpoint-com","skipIfMissing":true,"dpi":96}};
@@ -2399,7 +2409,7 @@ function fitRowH(avail, n, pref, floor) {
2399
2409
  function chartBottom(hasSoWhat, hasFootnote) {
2400
2410
  /* D8: tightest of soWhat/footnote/contentBottomWithNote — no short-circuit */
2401
2411
  var bot = CONTENT_BOTTOM;
2402
- if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
2412
+ if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.20);
2403
2413
  if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
2404
2414
  if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
2405
2415
  return bot;
@@ -2483,24 +2493,31 @@ function donutWedges(labels, values) {
2483
2493
  });
2484
2494
  return segs;
2485
2495
  }
2486
- function donutChart(sh, ec, S, dcols) {
2496
+ function donutChart(sh, ec, S, dcols, bottomY) {
2487
2497
  var d = PT.donut, labels = ec.labels || [], values = ec.values || [];
2488
2498
  var segs = donutWedges(labels, values);
2489
- var bx = d.centerX - d.r, by = d.centerY - d.r, bw = d.r * 2;
2499
+ var r = d.r;
2500
+ if (bottomY != null && d.centerY + r > bottomY) {
2501
+ r = Math.max(0.9, bottomY - d.centerY - 0.04);
2502
+ }
2503
+ var holeR = d.holeR * (r / d.r);
2504
+ var bx = d.centerX - r, by = d.centerY - r, bw = r * 2;
2490
2505
  segs.forEach(function (sg) {
2491
2506
  sh.push(shape('pie', bx, by, bw, bw, dcols[sg[2] % dcols.length], { av: pieAv(sg[0], sg[1]) }));
2492
2507
  });
2493
- var hb = d.holeR * 2;
2494
- sh.push(shape('ellipse', d.centerX - d.holeR, d.centerY - d.holeR, hb, hb, S.bg));
2508
+ var hb = holeR * 2;
2509
+ sh.push(shape('ellipse', d.centerX - holeR, d.centerY - holeR, hb, hb, S.bg));
2495
2510
  var total = 0; values.forEach(function (v) { total += Math.max(0, v); });
2496
2511
  var totalStr = (Math.round(total * 10) / 10) + (ec.unit || '');
2497
- sh.push(txSp(d.centerX - d.holeR, d.centerY - 0.42, hb, 0.5,
2512
+ sh.push(txSp(d.centerX - holeR, d.centerY - 0.42, hb, 0.5,
2498
2513
  [[{ t: totalStr, sz: 20, b: 1, col: S.ink, font: S.fontDisplay }]], { align: 'ctr', anchor: 'ctr' }));
2499
- sh.push(txSp(d.centerX - d.holeR, d.centerY + 0.08, hb, 0.35,
2514
+ sh.push(txSp(d.centerX - holeR, d.centerY + 0.08, hb, 0.35,
2500
2515
  [[{ t: ec.centerLabel || '合计', sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
2501
2516
  var n = Math.max(1, labels.length);
2502
- var rowH = Math.min(d.legendRowH, 3.4 / n);
2517
+ var maxLegH = (bottomY != null) ? Math.max(1.2, bottomY - (d.centerY - 1.7)) : 3.4;
2518
+ var rowH = Math.min(d.legendRowH, maxLegH / n, 3.4 / n);
2503
2519
  var ly0 = d.centerY - (n * rowH) / 2 + 0.1;
2520
+ if (bottomY != null) ly0 = Math.min(ly0, bottomY - n * rowH - 0.04);
2504
2521
  var sum2 = total || 1;
2505
2522
  labels.forEach(function (lb, i) {
2506
2523
  var v = values[i] || 0;
@@ -2557,14 +2574,13 @@ function table(x, y, w, colW, rows, S, opts) {
2557
2574
  function soWhatBar(sh, text, S) {
2558
2575
  var R = (typeof regionOf === 'function') ? regionOf('exhibit', 'annotation')
2559
2576
  : { x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
2560
- /* 与 B 通道同算法:长结论条有限缩字号,优先保容器不溢出 */
2561
- var bodyW = Math.max(2.5, R.w - 0.44 - 1.35);
2562
- var fzB = fitFont([String(text || '')], bodyW, R.h - 0.14, { max: 12.5, gapFactor: 0.2, maxShrinkSteps: 3 });
2577
+ /* MD3 结论条:衬底 + 左 accent 轨 + 正文(不绘制「SO WHAT」标签字样) */
2578
+ var bodyW = Math.max(2.5, R.w - 0.44);
2579
+ var fzB = fitFont([String(text || '')], bodyW, R.h - 0.14, { max: 14, gapFactor: 0.2, maxShrinkSteps: 3 });
2563
2580
  sh.push(shape('rect', R.x, R.y, R.w, R.h, S.soft));
2564
2581
  sh.push(shape('rect', R.x, R.y, 0.06, R.h, S.accent));
2565
2582
  sh.push(txSp(R.x + 0.22, R.y + 0.06, R.w - 0.44, R.h - 0.12,
2566
- [[{ t: 'SO WHAT ', sz: 11, b: 1, col: S.accent, font: S.font, spc: 150 },
2567
- { t: text, sz: fzB, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
2583
+ [[{ t: String(text || ''), sz: fzB, b: 1, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
2568
2584
  }
2569
2585
  function footnoteLine(sh, text, S) {
2570
2586
  var R = (typeof regionOf === 'function') ? regionOf('exhibit', 'footnote')
@@ -3424,7 +3440,7 @@ function slidesOf(model, S) {
3424
3440
  if (img.caption) sh.push(txSp(MX, capYImg + 0.04, CW, capH2, [[{ t: img.caption, sz: 10.5, col: S.faint, font: S.font }]]));
3425
3441
  } else if (type === 'donut') {
3426
3442
  var dcn = sec.chart || {};
3427
- if (dcn.labels && dcn.values) donutChart(sh, dcn, S, dcn.colors || donutColors(model.style, S, model.theme));
3443
+ if (dcn.labels && dcn.values) donutChart(sh, dcn, S, dcn.colors || donutColors(model.style, S, model.theme), bodyBottom);
3428
3444
  if (sec.note) sh.push(txSp(MX, PT.note.y, CW, PT.note.h, [[{ t: sec.note, sz: 11, col: S.faint, font: S.font }]]));
3429
3445
  } else if (type === 'heatmap') {
3430
3446
  var H = PT.heatmap;
@@ -3577,10 +3593,27 @@ function slidesOf(model, S) {
3577
3593
  var bRR = regOf('bar', isHBar ? 'hbar' : 'primary', { bottom: botY }) ||
3578
3594
  { x: isHBar ? MX : MX + PT.bar.chartX, y: isHBar ? PT.bar.hbarY0 : PT.bar.chartY,
3579
3595
  w: isHBar ? CW : CW - PT.bar.chartW, h: 3 };
3596
+ var ptsB = (sec.points || []).filter(Boolean);
3597
+ var bx = bRR.x, bw = bRR.w;
3598
+ if (ptsB.length && !isHBar) {
3599
+ var sideW = Math.min(3.6, Math.max(2.6, bRR.w * 0.30));
3600
+ bw = Math.max(3.2, bRR.w - sideW - 0.28);
3601
+ var sx = bRR.x + bw + 0.28;
3602
+ var rowHb = Math.min(0.72, (bRR.h - 0.1) / Math.max(1, ptsB.length));
3603
+ ptsB.slice(0, 6).forEach(function (p, i) {
3604
+ var k = Array.isArray(p) ? String(p[0] || '') : String((p && p.t) || '');
3605
+ var v = Array.isArray(p) ? String(p[1] || '') : String((p && p.d) || '');
3606
+ var y = bRR.y + i * rowHb;
3607
+ sh.push(shape('rect', sx, y + 0.12, 0.08, 0.08, S.accent));
3608
+ sh.push(txSp(sx + 0.2, y, sideW - 0.25, rowHb,
3609
+ [[{ t: k + (v ? ' ' : ''), sz: 12, b: 1, col: S.ink, font: S.font },
3610
+ { t: v, sz: 11, col: S.body, font: S.font }]], { anchor: 'ctr' }));
3611
+ });
3612
+ }
3580
3613
  if (isHBar) {
3581
- chartBlockShapes(sh, ch, S, bRR.x, bRR.y, bRR.w, bRR.h, dcols2);
3614
+ chartBlockShapes(sh, ch, S, bx, bRR.y, bw, bRR.h, dcols2);
3582
3615
  } else {
3583
- chartBlockShapes(sh, ch, S, bRR.x, bRR.y, bRR.w, Math.max(1.5, bRR.h), dcols2);
3616
+ chartBlockShapes(sh, ch, S, bx, bRR.y, bw, Math.max(1.5, bRR.h), dcols2);
3584
3617
  }
3585
3618
  if (sec.note) sh.push(txSp(MX, PT.note.y, CW, PT.note.h, [[{ t: sec.note, sz: 11, col: S.faint, font: S.font }]]));
3586
3619
  } else if (type === 'twocol' || type === 'threecol') {
@@ -262,7 +262,7 @@ if(e.isIntersecting){e.target.classList.add('in');o.unobserve(e.target)}})},
262
262
  var PROMPT=[
263
263
  '请为这份 HTML 报告生成可编辑 PPTX 并交付:',
264
264
  '1) 阅读 references/pptx-export.md 与 references/modes.md(三模式独立规则);',
265
- '2) 核对/补全 window.REPORT_MODEL 为与正文严格一致的内容(含 mode/style/theme/title/agenda/sections/closing;theme 为 light 或 dark、与页面 data-theme 一致——PPTX 按此导出亮色版或深色版;29 种页型字段与 30 类 chart.type 见 scripts/model-schema.json;research 关键图表页用 type:"exhibit" 并填 exhibitNo/soWhat/footnote);',
265
+ '2) 核对/补全 window.REPORT_MODEL 为与正文严格一致的内容(含 mode/style/theme/title/agenda/sections/closing;theme 为 light 或 dark、与页面 data-theme 一致——PPTX 按此导出亮色版或深色版;29 种页型字段与 30 类 chart.type 见 scripts/model-schema.json;research 关键图表页用 type:"exhibit" 并填 exhibitNo/soWhat(结论条)/footnote);',
266
266
  '3) 运行 python scripts/validate_report.py <报告.html> 直到全 PASS(含模型一致性检查);',
267
267
  '4) 智能体精导并质检(0/0 才交付):python scripts/extract_model.py <报告.html> → NODE_PATH=<pptxgenjs 所在 node_modules> node scripts/build_pptx.js <报告.pptx> --model=<报告.model.json>(如需覆盖模型主题可加 --theme=light|dark) → python scripts/validate_pptx.py <报告.pptx> --strict --model=<报告.model.json>。',
268
268
  '报告文件:<请填写报告.html 的路径>'
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "top-ppt-html",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "top-ppt-html",
9
- "version": "0.1.13",
9
+ "version": "0.1.14",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "pptxgenjs": "^4.0.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "top-ppt-html",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "private": true,
5
5
  "description": "TopPPT HTML:正式商务演示双交付。单文件 HTML + 版式保真可编辑 PPTX;MD3 密度克制;布局骨架 P1–P12;模型单写;strict 0/0 交付。",
6
6
  "license": "MIT",
@@ -540,7 +540,8 @@ python scripts/prepare_images.py ./photos --layout grid --mode path # 大图
540
540
  }
541
541
  ```
542
542
 
543
- > 条目仍放不下:压缩 `.agenda__a` 的 `padding`(如 `clamp(12px,1.6vh,18px)`);>16 条拆「上篇/下篇」两个区,或只列一级章节。
543
+ > **阈值(layout-constants `contentQuality.agenda`)**:≤8 单列;**>8 须 `.agenda--2col`(两列/两排)**;>16 拆「上篇/下篇」或只列一级章节。放不下先换列,禁单列撑爆一屏。
544
+ > 条目仍偏高:压缩 `.agenda__a` 的 `padding`(如 `clamp(12px,1.6vh,18px)`)。
544
545
 
545
546
  ---
546
547
 
@@ -622,3 +623,15 @@ python scripts/prepare_images.py ./photos --layout grid --mode path # 大图
622
623
 
623
624
  ---
624
625
 
626
+ ## 结论条 `.sowhat`(本页重点收口)
627
+
628
+ > **语义**:本页关键 takeaway,不是脚注。
629
+ > **版式**:MD3 tonal 衬条——`accent-soft` 底 + 左 4px accent 轨 + 略大于正文的 `--fw-title` 字重。
630
+ > **标签**:**默认不显示**「So what / SO WHAT / 结论」字样;只写句子(或 `.sowhat__stack` 短多行)。罕见需标签时加 `.sowhat--labeled` + `.sowhat__k`。
631
+ > **间距**:与上方主内容 `margin-top: var(--sp-5)`(≥24px),禁止贴底。
632
+ > **PPTX**:`soWhatBar()` 同源衬底+左轨+正文,**不绘制 SO WHAT 字符串**。
633
+
634
+ ```html
635
+ <div class="sowhat rv"><span class="sowhat__v">一行含义或建议(≤60 字)。</span></div>
636
+ ```
637
+
@@ -474,7 +474,7 @@ DGIG: 数据 = 能自我增强的生产资料
474
474
 
475
475
  1. **发现(Finding)**:直接陈述数据与事实,≤200 字/段;数字前置("入湖表 68% 从未被查询")。
476
476
  2. **证据(Evidence)**:Exhibit 图表框 / 密表 / 指标带;外部数据必带 `[n]`;每个 Exhibit 必有来源行。
477
- 3. **含义(So what)**:`.sowhat` 一行(≤60 字)——"这意味着…/因此建议…";写不出 so-what 说明这页论证不完整。
477
+ 3. **结论条**:`.sowhat` 一行收口(≤60 字)——"这意味着…/因此建议…";**不显示「So what」标签**,靠 MD3 衬条(左 accent 轨 + soft 底)强调;写不出结论说明这页论证不完整。
478
478
 
479
479
  ### 8-2、行动标题写法
480
480
 
@@ -207,7 +207,7 @@ PPTX 无"页高"概念(固定 13.333×7.5in),对应纪律是**内容不越
207
207
  | `T5` | 证据编号 / 轻量标签 | `micro` | Exhibit 编号、状态标签 |
208
208
  | `T6` | 证据块标题 / 小节标题 | `h2` | 分栏小标题 |
209
209
  | `T7` | 正文解释段落 | `body` | 证据解释、管理解读正文 |
210
- | `T8` | 结论条文字 | `body` | so-what 结论条 |
210
+ | `T8` | 结论条文字 | `body` / 略大于 body | MD3 结论条正文(无 So what 标签) |
211
211
  | `T9` | SO WHAT 标签 | `caption` | so-what 标签行 |
212
212
  | `T10` | SO WHAT 正文 / 业务含义 | `body` | so-what 正文、行动含义 |
213
213
  | `T11` | 图表轴 / 图例 / 刻度 | `micro` | 坐标轴、图例、刻度、单位(**禁用于表格正文**) |
@@ -10,8 +10,27 @@
10
10
 
11
11
  ## 使用准则(先读再用)
12
12
 
13
+ ## 行业共识原则(商务演示 · 非装饰)
14
+
15
+ 图标在正式商务演示文稿中是 **wayfinding(路标)**,不是插画墙。对齐常见咨询/产品 Deck 实践:
16
+
17
+ 1. **语义先行**:每个图标必须能回答「这是哪一类信息」;换掉标题后图标仍应成立,否则删。
18
+ 2. **同一家族**:全篇统一描边风(stroke ≈1.8、圆角线帽、`currentColor`);禁混用填充风/emoji/三方彩色标。
19
+ 3. **有限密度**:演示 3–8 / 研究 2–4 / 架构主图内 0;超过即噪音。
20
+ 4. **位置克制**:只走四正当位置(卡头 / 关键列表 / 指标角标 / 提示条);**Agenda 大编号、表格单元格、图表内部、正文行内**一律不用。
21
+ 5. **一致性 > 个性**:同类信息复用同一语义图标(如「风险」全篇都用警告),不要每页换隐喻。
22
+ 6. **可导出意识**:PPTX 通道用 accent 小方块路标代替 SVG;不把图标 ID 写进模型字段。
23
+
24
+ 违反时:先减数量,再统一家族,最后才考虑换布局——不要靠堆图标补「空旷」。
25
+
26
+
27
+
13
28
  **图标是信息路标,不是装饰。**
14
29
 
30
+ ### Mode A 内容页落地(必做,非可选点缀)
31
+
32
+ 含 **cards / points / key lists / note strips** 的内容页须在下表正当位置取用图标;showcase / scaffold / 引擎实践不得再交「零图标展板」。密度:演示 3–8 / 研究 2–4。门禁:`validate_report` 对「≥2 卡且无 `.card__ico`」发 **WARN**。
33
+
15
34
  ### 哪里用(四个正当位置)
16
35
 
17
36
  | 位置 | 做法 | 效果 |
@@ -323,3 +323,20 @@ fill = Body 区被内容覆盖的估算面积 / Body 区面积
323
323
  - [ ] `LAYOUT_*` 全 PASS
324
324
 
325
325
  > 门禁与失败修复见 `layout-constants.qualityGates` 与 `failure-modes.md`。间距与色 token 见 `design-system.md`。
326
+
327
+ ## 大纲自适应(Agenda)
328
+
329
+ | 条目数 | 版式 | 类名 |
330
+ |--------|------|------|
331
+ | ≤8 | 单列 | `.agenda` |
332
+ | 9–16 | **两列 / 两排** | `.agenda.agenda--2col` |
333
+ | >16 | 拆上篇/下篇或只列一级章节 | — |
334
+
335
+ 阈值单源:`layout-constants.json` → `contentQuality.agenda.singleMax`(默认 8)。校验:`validate_report` WARN(只认 `<ol class="…agenda--2col">`,不被 CSS 选择器误伤)。
336
+
337
+ ## 一页一屏(高度契约)
338
+
339
+ - `section.band` 默认 `min-height: 100vh/svh`;主内容**不得**溢出画布底或压进结论条 / 注释带。
340
+ - 放不下按 `containers.overflowRule`:重构承载 → 拆页/分章 → 换布局形态 → 有限 fontShrink;**禁静默截断**。
341
+ - `band--flow` 仅长结构/附录;结论条 `margin-top: clamp(28px,3.6vh,48px)` + PPTX `chartBottom` 相对 `soWhatY` 让位。
342
+
@@ -105,12 +105,11 @@
105
105
  </div>
106
106
  </div>
107
107
  <div class="sowhat rv">
108
- <span class="sowhat__k">结论</span>
109
108
  <span class="sowhat__v">平台化不是可选项,而是规模化穿越分水岭的唯一共同路径。</span>
110
109
  </div>
111
110
  ```
112
111
 
113
- > 每栏要点 ≤4 条、同构对仗(同一维度正面 vs 反面);结论条演示模式用 `.sowhat__k` 写"结论",research 沿用"So what"。
112
+ > 每栏要点 ≤4 条、同构对仗(同一维度正面 vs 反面);结论条默认**不写标签字样**(无「So what」/「结论」),靠 MD3 衬条强调;罕见需标签时用 `.sowhat--labeled` + (已弃用默认标签)。
114
113
  > **PPTX 映射**:`comparison` 页型——`left/right = {title, points:[[k,v]…]}`,左 surface / 右 soft 面板 + `verdict`(结论条,accent 实底);research 亦可填 `soWhat`(soft 底结论条,与 R 系页型通用件同源)。
115
114
 
116
115
  ---
@@ -445,8 +444,7 @@
445
444
  </ul>
446
445
  </div>
447
446
  </div>
448
- <div class="sowhat rv"><span class="sowhat__k">So what</span>
449
- <span class="sowhat__v">一行含义(≤60 字)。</span></div>
447
+ <div class="sowhat rv"><span class="sowhat__v">一行含义(≤60 字)。</span></div>
450
448
  ```
451
449
 
452
450
  ### 46c-3 代码:双区自由组合(左图 + 右表 / 左文 + 右图)
@@ -114,7 +114,7 @@
114
114
  [eyebrow] 02 · 现状诊断(章节编号+章节名)——必备
115
115
  [行动标题] 结论句 ≥12 字("六成企业试点卡在治理与集成——规模化分水岭已经出现")——必备
116
116
  [论证区] 锁定版式 R1–R8 之一——必备
117
- [so-what 条] .sowhat 一行含义——仅关键论证页(Exhibit 页/结论页)加
117
+ [结论条] .sowhat 一行收口(无 So what 标签 · MD3 衬条)——仅关键论证页加
118
118
  [来源行] .exhibit__src 随 Exhibit 组件自带;独立 .footnote 仅当页内有关键外部数据且无 Exhibit 时才加
119
119
  ```
120
120
 
@@ -108,7 +108,7 @@ HTML 侧仍用 MD3 clamp;双端都以「最后一排可读」为准(`present
108
108
 
109
109
  > 几何常量见 `layout-constants.json` `pageTypes.{sankey,treemap,boxplot,network,marimekko,streamgraph}`(含节点/流带上限、采样点下限、双边界追踪约束);版式与生成规则详见 `references/infographics.md`。
110
110
 
111
- **通用可选字段(全部页型)**:`soWhat`(so-what 结论条,accent 左边线 + soft 底)、`footnote`(页脚来源行)、`flags`(**待核实标注清单** `["…"]` → accent 强调色清单条,提示用户二次确认,并写入演讲者备注)、`image`(素材图片 / 配图占位,见 `image` 页型)、`exhibitNo`(非 exhibit 页型也可带 Exhibit 徽标)。research 模式 `soWhat/footnote` **只在关键论证页填写**(克制条款);comparison 页型的 `verdict` 为 accent 实底结论条。
111
+ **通用可选字段(全部页型)**:`soWhat`(结论条,MD3 accent 左轨 + soft 底;**不绘制「SO WHAT」标签**)、`footnote`(页脚来源行)、`flags`(**待核实标注清单** `["…"]` → accent 强调色清单条,提示用户二次确认,并写入演讲者备注)、`image`(素材图片 / 配图占位,见 `image` 页型)、`exhibitNo`(非 exhibit 页型也可带 Exhibit 徽标)。research 模式 `soWhat/footnote` **只在关键论证页填写**(克制条款;结论条无 So what 字样);comparison 页型的 `verdict` 为 accent 实底结论条。
112
112
 
113
113
  > **素材图片(单源 `layout-constants.json` 的 `imageSpec`)**:`image` 三选一必填——`src`(用户图,data: 内联或相对路径)|`items:[{src,alt?,caption?,placeholder?}]`(多图版式)|`placeholder:true`(**配图占位**,无素材时锁版式用)。
114
114
  > - **六版式**(`layout`):`full` 版心全宽 3:1|`half` 左图右注 4:3|`bleed` 通栏出血 21:9|`grid` 多图网格 4:3(2/3/4/6 张)|`compare` 双图 A/B 4:3|`wall` Logo 墙 1:1。比例写在 `imageSpec.ratioDefault`,**HTML 用同比例锁定类、PPTX 用同一比例算高度并垂直居中**——两通道版式一致。
@@ -364,16 +364,16 @@ function isRec(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
364
364
  function soWhatBar(s, text) {
365
365
  const R = REGIONS.regionOf('exhibit', 'annotation') ||
366
366
  { x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
367
- /* 长结论条在 bar 内有限缩字号(fontShrink),优先保内容完整不越出容器 */
368
- const labelW = 1.35;
369
- const bodyW = Math.max(2.5, R.w - 0.44 - labelW);
370
- const fzB = fitFont([String(text || '')], bodyW, R.h - 0.14, { max: 12.5, gapFactor: 0.2, maxShrinkSteps: 3 });
367
+ /* MD3 结论条:衬底 + 左 accent 轨 + 正文(不绘制「SO WHAT」标签字样) */
368
+ const bodyW = Math.max(2.5, R.w - 0.44);
369
+ const fzB = fitFont([String(text || '')], bodyW, R.h - 0.14, { max: 14, gapFactor: 0.2, maxShrinkSteps: 3 });
371
370
  s.addShape('rect', { x: R.x, y: R.y, w: R.w, h: R.h, fill: { color: STYLE.soft }, line: { type: 'none' }, objectName: trName('soWhat') });
372
371
  s.addShape('rect', { x: R.x, y: R.y, w: 0.06, h: R.h, fill: { color: STYLE.accent }, line: { type: 'none' } });
373
- s.addText([
374
- { text: 'SO WHAT ', options: { fontSize: sz(11), bold: true, color: STYLE.accent, charSpacing: 1.5 } },
375
- { text: text, options: { fontSize: sz(fzB), color: STYLE.ink } },
376
- ], { x: R.x + 0.22, y: R.y + 0.06, w: R.w - 0.44, h: R.h - 0.12, fontFace: STYLE.font, valign: 'middle', objectName: trName('soWhat') });
372
+ s.addText(String(text || ''), {
373
+ x: R.x + 0.22, y: R.y + 0.06, w: R.w - 0.44, h: R.h - 0.12,
374
+ fontFace: STYLE.font, fontSize: sz(fzB), bold: true, color: STYLE.ink, valign: 'middle',
375
+ objectName: trName('soWhat'),
376
+ });
377
377
  }
378
378
  function footnoteLine(s, text) {
379
379
  const R = REGIONS.regionOf('exhibit', 'footnote') ||
@@ -576,7 +576,7 @@ function chartBottom(hasSoWhat, hasFootnote) {
576
576
  /* 取最紧下界:so-what / footnote / contentBottomWithNote 并存时不得短路(D8)。
577
577
  * 与 ~1630 flagY 的 Math.min 让位同口径;withNote 时对齐 regionOf 的 contentBottomWithNote。 */
578
578
  let bot = CONTENT_BOTTOM;
579
- if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
579
+ if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.20);
580
580
  if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
581
581
  if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
582
582
  return bot;
@@ -827,15 +827,22 @@ function paretoChart(s, c, pos) {
827
827
  ], Object.assign({}, barOpts, pos));
828
828
  } catch (e) { chartTry(s, pptx.ChartType.bar, chartSeries(c), Object.assign({}, barOpts, pos), c); }
829
829
  }
830
- function nativeDonutChart(s, dcn, dcols) {
830
+ function nativeDonutChart(s, dcn, dcols, bottomY) {
831
831
  const d = PT.donut;
832
+ /* 有结论条/脚注时压半径,避免环图底边压进 soWhatY(D11) */
833
+ let r = d.r;
834
+ const cy = d.centerY;
835
+ if (bottomY != null && cy + r > bottomY) {
836
+ r = Math.max(0.9, bottomY - cy - 0.04);
837
+ }
832
838
  s.addChart(pptx.ChartType.doughnut, chartSeries(dcn), {
833
- x: d.centerX - d.r, y: d.centerY - d.r, w: d.r * 2, h: d.r * 2,
834
- holeSize: Math.round(d.holeR / d.r * 100),
839
+ x: d.centerX - r, y: cy - r, w: r * 2, h: r * 2,
840
+ holeSize: Math.round((d.holeR / d.r) * 100),
835
841
  chartColors: dcols,
836
842
  showLegend: false, showValue: false, showPercent: false, showTitle: false,
837
843
  dataBorder: { pt: 1.5, color: STYLE.bg },
838
844
  });
845
+ return r;
839
846
  }
840
847
  /* ═══ 形状通道图表(非原生类型的高保真形状还原)═══
841
848
  * 与 A 通道(assets/pptx-export.js 的 chartShapes)语义一致:类别标签与数值都落为文本,
@@ -1774,17 +1781,19 @@ CONTENT.sections.forEach((sec) => {
1774
1781
  if (dcn.labels && dcn.values) {
1775
1782
  const d = PT.donut;
1776
1783
  const dcols = dcn.colors || donutColors(styleArg);
1777
- nativeDonutChart(s, dcn, dcols);
1778
- const hb = d.holeR * 2;
1784
+ const usedR = nativeDonutChart(s, dcn, dcols, bodyBottom) || d.r;
1785
+ const holeR = d.holeR * (usedR / d.r);
1786
+ const hb = holeR * 2;
1779
1787
  let total = 0; dcn.values.forEach(v => { total += Math.max(0, v); });
1780
1788
  const totalStr = (Math.round(total * 10) / 10) + (dcn.unit || '');
1781
- s.addText(totalStr, { x: d.centerX - d.holeR, y: d.centerY - 0.42, w: hb, h: 0.5, align: 'center',
1789
+ s.addText(totalStr, { x: d.centerX - holeR, y: d.centerY - 0.42, w: hb, h: 0.5, align: 'center',
1782
1790
  fontFace: STYLE.fontDisplay, fontSize: sz(20), bold: true, color: STYLE.ink, valign: 'middle' });
1783
- s.addText(dcn.centerLabel || '合计', { x: d.centerX - d.holeR, y: d.centerY + 0.08, w: hb, h: 0.35, align: 'center',
1791
+ s.addText(dcn.centerLabel || '合计', { x: d.centerX - holeR, y: d.centerY + 0.08, w: hb, h: 0.35, align: 'center',
1784
1792
  fontFace: STYLE.font, fontSize: sz(9.5), color: STYLE.faint, valign: 'middle' });
1785
1793
  const n = Math.max(1, dcn.labels.length);
1786
- const rowH = Math.min(d.legendRowH, 3.4 / n);
1787
- const ly0 = d.centerY - (n * rowH) / 2 + 0.1;
1794
+ const maxLegH = Math.max(1.2, bodyBottom - (d.centerY - 1.7));
1795
+ const rowH = Math.min(d.legendRowH, maxLegH / n, 3.4 / n);
1796
+ const ly0 = Math.min(d.centerY - (n * rowH) / 2 + 0.1, bodyBottom - n * rowH - 0.04);
1788
1797
  const sum2 = total || 1;
1789
1798
  dcn.labels.forEach((lb, i) => {
1790
1799
  const v = dcn.values[i] || 0;
@@ -1988,11 +1997,30 @@ CONTENT.sections.forEach((sec) => {
1988
1997
  const isH = c.type === 'hbar';
1989
1998
  const reg = REGIONS.regionOf('bar', isH ? 'hbar' : 'primary', { bottom: bot }) ||
1990
1999
  { x: MX, y: isH ? PT.bar.hbarY0 : PT.bar.chartY, w: CW, h: 3 };
2000
+ const pts = (sec.points || []).filter(Boolean);
2001
+ let cx = reg.x, cw = reg.w;
2002
+ /* 有侧栏要点时让出右栏,保证 HTML g-side / 怎么读这张图 与 PPTX 同源可核对 */
2003
+ if (pts.length && !isH) {
2004
+ const sideW = Math.min(3.6, Math.max(2.6, reg.w * 0.30));
2005
+ cw = Math.max(3.2, reg.w - sideW - 0.28);
2006
+ const sx = reg.x + cw + 0.28;
2007
+ const rowH = Math.min(0.72, (reg.h - 0.1) / Math.max(1, pts.length));
2008
+ pts.slice(0, 6).forEach((p, i) => {
2009
+ const k = Array.isArray(p) ? String(p[0] || '') : String((p && p.t) || '');
2010
+ const v = Array.isArray(p) ? String(p[1] || '') : String((p && p.d) || '');
2011
+ const y = reg.y + i * rowH;
2012
+ s.addShape('rect', { x: sx, y: y + 0.12, w: 0.08, h: 0.08,
2013
+ fill: { color: STYLE.accent }, line: { type: 'none' } });
2014
+ s.addText([
2015
+ { text: k + (v ? ' ' : ''), options: { fontSize: sz(12), bold: true, color: STYLE.ink } },
2016
+ { text: v, options: { fontSize: sz(11), color: STYLE.body } },
2017
+ ], { x: sx + 0.2, y, w: sideW - 0.25, h: rowH, fontFace: STYLE.font, valign: 'middle' });
2018
+ });
2019
+ }
1991
2020
  if (isH) {
1992
- chartBlock(s, c, reg.x, reg.y, reg.w, reg.h, dcols);
2021
+ chartBlock(s, c, cx, reg.y, cw, reg.h, dcols);
1993
2022
  } else {
1994
- /* 非 hbar 保持两侧留白(chartX/chartW),region 已扣减 */
1995
- chartBlock(s, c, reg.x, reg.y, reg.w, Math.max(1.5, reg.h), dcols);
2023
+ chartBlock(s, c, cx, reg.y, cw, Math.max(1.5, reg.h), dcols);
1996
2024
  }
1997
2025
  if (sec.note) s.addText(sec.note, { x: MX, y: PT.note.y, w: CW, h: PT.note.h, fontFace: STYLE.font,
1998
2026
  fontSize: sz(11), color: STYLE.faint });
@@ -2344,7 +2372,7 @@ CONTENT.sections.forEach((sec) => {
2344
2372
  if (sec.steps) _notes.push('步骤:' + sec.steps.map(st => (isRec(st) ? st.t : st[0]) || '').join(' → '));
2345
2373
  if (sec.items) _notes.push('达成对比:' + sec.items.map(it => it[0] + ' ' + it[1] + (it[2] != null ? '/' + it[2] : '')).join(';'));
2346
2374
  if (sec.levels) _notes.push('层级:' + sec.levels.map(l => (isRec(l) ? l.t : l[0]) || '').join(' → '));
2347
- if (sec.soWhat) _notes.push('So what:' + sec.soWhat);
2375
+ if (sec.soWhat) _notes.push('结论:' + sec.soWhat);
2348
2376
  if (sec.layoutPreset) _notes.push('布局骨架:' + sec.layoutPreset);
2349
2377
  if (flagItems.length) _notes.push('待核实(需二次确认):' + flagItems.join(';'));
2350
2378
  if (sec.note) _notes.push('注:' + sec.note);
@@ -141,19 +141,19 @@
141
141
  "id": "T8",
142
142
  "name": "结论条文字",
143
143
  "role": "body",
144
- "usage": "so-what 结论条、核心结论框"
144
+ "usage": "结论条、核心结论框"
145
145
  },
146
146
  {
147
147
  "id": "T9",
148
- "name": "SO WHAT 标签",
148
+ "name": "结论条标签(可选·默认隐藏)",
149
149
  "role": "caption",
150
- "usage": "so-what 标签行"
150
+ "usage": "结论条可选标签(默认不显示)"
151
151
  },
152
152
  {
153
153
  "id": "T10",
154
- "name": "SO WHAT 正文/业务含义",
154
+ "name": "结论条正文/业务含义",
155
155
  "role": "body",
156
- "usage": "so-what 正文、行动含义、表格行动项"
156
+ "usage": "结论条正文、行动含义、表格行动项"
157
157
  },
158
158
  {
159
159
  "id": "T11",
@@ -1297,13 +1297,25 @@
1297
1297
  "进一步研究",
1298
1298
  "需要进一步",
1299
1299
  "保持关注"
1300
- ]
1300
+ ],
1301
+ "$comment": " 结论条无 So what 标签;HTML margin 与 PPTX chartBottom/soWhatY 共同保证呼吸间距。"
1301
1302
  },
1302
1303
  "researchTitle": {
1303
1304
  "$comment": "判断词负向前瞻(应用/需求/一览/将来…)不可省:去掉后『智能体平台的应用架构说明』这类主题词式长标题会因命中单字『应』而被误判为结论句,门禁形同虚设。",
1304
1305
  "minChars": 12,
1305
1306
  "requireDigitOrJudgment": true,
1306
1307
  "judgmentPattern": "\\d|[二两三四五六七八九十半]|一(?!览|体|致|直|些|般|系列|同|起|方面)|是(?!否)|应(?!用|急|对|届)|需(?!求)|将(?!来|近)|增|降|优先|瓶颈|关键|高于|低于|快于|慢于|第一|唯一|从.{1,12}到|翻倍|过半|不足|领先|落后|卡在|分化|出现|成为|仍|并非|必须|才能|才是|最大|最浅|最深|马当先|明显|尚在|已(?!知)|提升|贡献|占比|领跑|先行|滞后|分野|显著|差异|穿越|兑现|放量|[::—–]"
1308
+ },
1309
+ "agenda": {
1310
+ "$comment": "大纲条目数阈值:≤singleMax 单列;>singleMax 须 .agenda--2col(两列/两排);>splitMax 拆上篇/下篇或只列一级章节。scaffold / validate_report 消费。",
1311
+ "singleMax": 8,
1312
+ "splitMax": 16,
1313
+ "twoColClass": "agenda--2col"
1314
+ },
1315
+ "pageHeight": {
1316
+ "$comment": "一页一屏:section.band 默认 min-height=100vh(svh);主内容不得压进结论条/注释带。放不下按 overflowRule:重构承载→拆页→换形态→有限 fontShrink;禁静默截断。band--flow 仅长结构/附录。",
1317
+ "fitContract": "16:9 / band--fit",
1318
+ "forbidCrushAnnotation": true
1307
1319
  }
1308
1320
  },
1309
1321
  "containers": {
@@ -134,9 +134,9 @@ def heuristic_rubric(html: Path) -> dict:
134
134
  c_score += 15
135
135
  if n_sowhat >= max(1, n_sec // 4):
136
136
  c_score += 15
137
- c_notes.append(f'so-what {n_sowhat}')
137
+ c_notes.append(f'结论条 {n_sowhat}')
138
138
  else:
139
- c_notes.append(f'so-what 偏少({n_sowhat})')
139
+ c_notes.append(f'结论条偏少({n_sowhat})')
140
140
  if n_points >= n_sec * 2:
141
141
  c_score += 10
142
142
  if n_refs: