@topmindspace/tms-skills 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/package.json +1 -1
  3. package/top-ppt-html/README.md +38 -130
  4. package/top-ppt-html/SKILL.md +34 -19
  5. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  6. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  7. package/top-ppt-html/assets/theme-overview.png +0 -0
  8. package/top-ppt-html/package.json +1 -1
  9. package/top-ppt-html/references/charts-discipline.md +3 -1
  10. package/top-ppt-html/references/charts.md +24 -15
  11. package/top-ppt-html/references/content-rules.md +54 -34
  12. package/top-ppt-html/references/default-surface.md +61 -0
  13. package/top-ppt-html/references/design-system.md +2 -2
  14. package/top-ppt-html/references/failure-modes.md +14 -1
  15. package/top-ppt-html/references/icons.md +56 -277
  16. package/top-ppt-html/references/layout-grammar.md +16 -6
  17. package/top-ppt-html/references/modes.md +16 -11
  18. package/top-ppt-html/references/playbook.md +31 -13
  19. package/top-ppt-html/references/pptx-export.md +20 -2
  20. package/top-ppt-html/references/presentation-craft.md +69 -0
  21. package/top-ppt-html/references/tech-design.md +5 -5
  22. package/top-ppt-html/scripts/audit_skill.py +13 -2
  23. package/top-ppt-html/scripts/build_examples.py +39 -2260
  24. package/top-ppt-html/scripts/build_pptx.js +6 -0
  25. package/top-ppt-html/scripts/checks_html.py +39 -0
  26. package/top-ppt-html/scripts/cross_verify.py +5 -1
  27. package/top-ppt-html/scripts/extract_snippet.py +2 -2
  28. package/top-ppt-html/scripts/layout-constants.json +111 -18
  29. package/top-ppt-html/scripts/lib_layout_regions.js +7 -9
  30. package/top-ppt-html/scripts/negative_tests.py +127 -1
  31. package/top-ppt-html/scripts/package_skill.py +13 -10
  32. package/top-ppt-html/scripts/quality_gate.py +11 -3
  33. package/top-ppt-html/scripts/recommend_layout.py +384 -0
  34. package/top-ppt-html/scripts/regression.py +1 -1
  35. package/top-ppt-html/scripts/smoke_pptx.sh +30 -0
  36. package/top-ppt-html/scripts/sync_runtime.py +22 -25
  37. package/top-ppt-html/scripts/validate_pptx.py +69 -0
  38. package/top-ppt-html/scripts/validate_report.py +309 -10
  39. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +0 -3926
  40. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +0 -168
  41. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +0 -4325
  42. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +0 -321
  43. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +0 -4325
  44. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +0 -321
  45. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +0 -5527
  46. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +0 -914
  47. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +0 -5527
  48. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +0 -914
  49. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +0 -5527
  50. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +0 -914
  51. package/top-ppt-html/references/design-system-engine.md +0 -235
  52. package/top-ppt-html/references/industry-benchmark.md +0 -105
  53. package/top-ppt-html/references/reform-plan.md +0 -252
  54. package/top-ppt-html/scripts/layout_slots.json +0 -830
@@ -1588,6 +1588,11 @@ function addTable(s, tbl, x, y, w, availH, opts) {
1588
1588
  /* 章节页 · 按页型分发 */
1589
1589
  CONTENT.sections.forEach((sec) => {
1590
1590
  const type = sec.type || 'points';
1591
+ // Batch 2: honor layoutPreset (scaffold/recommend_layout → PPTX IR alignment)
1592
+ if (!sec.layoutPreset) {
1593
+ const _p2p = (LC.layoutSystem && LC.layoutSystem.pageToPreset) || {};
1594
+ if (_p2p[type]) sec.layoutPreset = _p2p[type];
1595
+ }
1591
1596
  const s = base();
1592
1597
  if (type !== 'quote') head(s, sec.eyebrow, sec.title, sec.lead);
1593
1598
 
@@ -2319,6 +2324,7 @@ CONTENT.sections.forEach((sec) => {
2319
2324
  if (sec.items) _notes.push('达成对比:' + sec.items.map(it => it[0] + ' ' + it[1] + (it[2] != null ? '/' + it[2] : '')).join(';'));
2320
2325
  if (sec.levels) _notes.push('层级:' + sec.levels.map(l => (isRec(l) ? l.t : l[0]) || '').join(' → '));
2321
2326
  if (sec.soWhat) _notes.push('So what:' + sec.soWhat);
2327
+ if (sec.layoutPreset) _notes.push('布局骨架:' + sec.layoutPreset);
2322
2328
  if (flagItems.length) _notes.push('待核实(需二次确认):' + flagItems.join(';'));
2323
2329
  if (sec.note) _notes.push('注:' + sec.note);
2324
2330
  if (sec.footnote) _notes.push('来源:' + sec.footnote);
@@ -83,6 +83,45 @@ def variety_floor(mode: str, n_chart_pages: int, variety: dict | None = None) ->
83
83
  return min(floor, max(1, int(-(-n_chart_pages * ratio // 1))))
84
84
 
85
85
 
86
+
87
+ def core_charts(lc: dict | None = None) -> list[str]:
88
+ lc = lc or load_lc()
89
+ ls = lc.get('layoutSystem') or {}
90
+ return list(ls.get('defaultCharts') or
91
+ ['bar', 'hbar', 'line', 'donut', 'progress', 'area', 'stack', 'dualline'])
92
+
93
+
94
+ def advanced_charts(lc: dict | None = None) -> set[str]:
95
+ lc = lc or load_lc()
96
+ ls = lc.get('layoutSystem') or {}
97
+ return set(ls.get('advancedCharts') or [])
98
+
99
+
100
+ def variety_core_preference(used: list[str], variety: dict | None = None,
101
+ lc: dict | None = None) -> str | None:
102
+ """preferCoreFirst:advanced 已用但核图多样性不足 → WARN 文案。
103
+
104
+ advanced 仍计入 minTypes(不罚);仅提示勿用冷门图凑下限。
105
+ """
106
+ v = variety if variety is not None else chart_variety(lc)
107
+ if not v.get('preferCoreFirst'):
108
+ return None
109
+ core = set(core_charts(lc))
110
+ adv = advanced_charts(lc)
111
+ distinct = set(used)
112
+ core_used = distinct & core
113
+ adv_used = distinct & adv if adv else (distinct - core)
114
+ if not adv_used:
115
+ return None
116
+ floor_hint = int(v.get('corePreferMin') or 3)
117
+ if len(core_used) >= min(floor_hint, len(core)):
118
+ return None
119
+ return (
120
+ f"核图仅 {sorted(core_used) or '∅'}(建议先 ≥{floor_hint} 种核图拉开多样性);"
121
+ f"已用 advanced {sorted(adv_used)}——允许计入 minTypes,但勿为凑下限而选用;"
122
+ f"仅内容意图命中时用 advanced"
123
+ )
124
+
86
125
  def adjacent_same_type(pages: list[list[str]]) -> list[str]:
87
126
  """相邻图表页同型告警文案列表。"""
88
127
  adj, prev, prev_i = [], None, 0
@@ -8,7 +8,7 @@
8
8
  ③ 文本溢出启发式:文本估算宽 vs shape 宽 × 可容行数
9
9
  ④ B 通道原生图表数值 ↔ 模型数值(类别标签一致不代表数值一致;数值错乱只能这里拓)
10
10
  ⑤ 字号比例尺同源:B 的字号必须是 A 字号集的子集(防 modeSize()/sz() 后处理漂移)
11
- 用法:先 node scripts/gen_channel_a.js 生成 A 通道产物,再 python scripts/cross_verify.py
11
+ 默认 SKIP;加 --full-ab 才执行。用法:先 node scripts/gen_channel_a.js 生成 A 通道产物,再 python scripts/cross_verify.py
12
12
  改 pptx-export.js 序列化骨架后必跑(见 references/pptx-export.md 双裁判教训)。
13
13
  """
14
14
  import json
@@ -291,4 +291,8 @@ def main() -> int:
291
291
 
292
292
 
293
293
  if __name__ == '__main__':
294
+ # Batch 2: A/B 全文对等默认关闭;显式 --full-ab 才跑(Fast/标准交付不依赖)
295
+ if '--full-ab' not in sys.argv:
296
+ print('cross_verify: SKIP(默认关闭;需要 A/B 全文对等时加 --full-ab)')
297
+ sys.exit(0)
294
298
  sys.exit(main())
@@ -118,9 +118,9 @@ TASK_ROUTES = {
118
118
  ],
119
119
  },
120
120
  'icons': {
121
- 'desc': '图标语义速查与使用准则',
121
+ 'desc': '图标语义选型 / 禁区 / 尺寸 / 高频 SVG(完整枚举见归档,不预读)',
122
122
  'reads': [
123
- ('icons.md', ['速查', '使用准则']),
123
+ ('icons.md', ['使用准则', '速查', '高频取码']),
124
124
  ],
125
125
  },
126
126
  }
@@ -54,7 +54,7 @@
54
54
  "micro": 10
55
55
  },
56
56
  "modeTypeScale": {
57
- "$comment": "三模式独立排版比例尺(pt)。research=咨询密排(正文 10.5pt、标题 22pt);architecture 与 presentation 同基。序列化器按「调用点基准 pt → 最接近 typeScale 角色」映射到当前模式取值。",
57
+ "$comment": "三模式独立排版比例尺(pt)。默认按中型会议室投影。小会议室可保持原值;礼堂/大报告厅宜更大标题档、更少每页单元(见 pptx-export 会场节)。research=咨询密排(正文 10.5pt、标题 22pt);architecture 与 presentation 同基。",
58
58
  "presentation": {
59
59
  "coverTitle": 44,
60
60
  "coverSub": 18,
@@ -820,7 +820,7 @@
820
820
  }
821
821
  },
822
822
  "variety": {
823
- "$comment": "图表多样性预算(validate_report.py 与 evals/run_evals.py 消费)——治「图表定义 36 种、实际只用 bar/line/donut」。类型数下限 = min(minTypes[模式], max(1, ⌈图表页数 × minTypesRatio⌉));noRepeatAdjacent = 相邻图表页不得同 data-chart;compositeMinRatio/compositeModes = 组合版式(内容页含 ≥2 种承载类型)比例下限与适用模式(治「单件页当默认」,只对 research 生效:A 演示按契约每屏一主张,C 架构恒为一页一图)。判据与选型见 references/playbook.md §四/§五。",
823
+ "$comment": "图表多样性预算(validate_report.py 与 evals/run_evals.py 消费)——治「图表定义 36 种、实际只用 bar/line/donut」。类型数下限 = min(minTypes[模式], max(1, ⌈图表页数 × minTypesRatio⌉));noRepeatAdjacent = 相邻图表页不得同 data-chart;compositeMinRatio/compositeModes 只对 research 生效(A 演示一屏一主张,C 架构一页一图)。preferCoreFirst:多样性宜先拉开 layoutSystem.defaultCharts(核图 8);advanced 图种在使用时同样计入 minTypes(不因用 advanced 受罚),但仅当内容意图命中时选用,禁为凑下限硬上冷门图。禁反模式:简单图全幅、极偏 donut/pie(见 layout-qa / sizeByComplexity)。判据与选型见 references/playbook.md §五。",
824
824
  "minTypes": {
825
825
  "presentation": 4,
826
826
  "research": 6,
@@ -831,7 +831,9 @@
831
831
  "compositeMinRatio": 0.3,
832
832
  "compositeModes": [
833
833
  "research"
834
- ]
834
+ ],
835
+ "preferCoreFirst": true,
836
+ "corePreferMin": 3
835
837
  },
836
838
  "registry": {
837
839
  "$comment": "图表登记四元组:pptx=native 走 addChart(真 chart part + 内嵌 Excel,双击可编辑数据);pptx=shape 走 OOXML 原生形状/SVG path 高保真还原,且**必须**按 dataTable 策略附数据表保证数据可追溯;nativeTrick = 用原生图表技巧实现的类型;path = 形状还原的几何约束(采样点下限 / 双边界追踪 / 禁预设形状)。build_pptx.js 与 pptx-export.js 双引擎按本表分派,validate_pptx.py 按本表断言。",
@@ -1218,15 +1220,16 @@
1218
1220
  }
1219
1221
  },
1220
1222
  "checkBudgets": {
1221
- "$comment": "单页预算(validate_report.py 消费):char 单页纯文字上限 / unit 并列单元上限 / empty 过空页阈值 / fit 满屏页文字上限 / wrap 版心宽度断言 / trow 表格行数上限。bodyPx 供页高溢出静态估算(HTML 正文基准 px,与 modes 契约一致)。",
1223
+ "$comment": "单页预算(validate_report 消费):char=文字量预警(WARN,非「字多就删」);unit=并列单元预警;empty=过空页 FAIL;fit=满屏页文字;wrap=版心;trow=表行。Mode A 密卡/列表页允许偏密;超 char 先走 overflowRule,不对字数单独 FAIL。",
1222
1224
  "presentation": {
1223
- "char": 1500,
1225
+ "char": 1800,
1224
1226
  "unit": 8,
1225
1227
  "empty": 80,
1226
1228
  "fit": 1000,
1227
1229
  "wrap": 1400,
1228
1230
  "trow": 8,
1229
- "bodyPx": 17
1231
+ "bodyPx": 17,
1232
+ "$comment": "预警阈值;结构合理的密页可接近此值;溢出无拆页→ layout-qa FAIL"
1230
1233
  },
1231
1234
  "research": {
1232
1235
  "char": 3200,
@@ -1323,9 +1326,9 @@
1323
1326
  "lineFactor": 1.45,
1324
1327
  "emAsciiRatio": 0.52
1325
1328
  },
1326
- "overflowRule": "溢出处置优先级:① 优化内容(列表化/精炼/合并/拆页/换组合版式)→ ② 调整容器尺寸与行宽 → ③ 有限度缩小字号(见 fontShrink)→ ④ 仍装不下则拆页。禁止无限缩字号硬塞,也禁止把「精简」当第一手段砍掉决策必需信息。",
1329
+ "overflowRule": "溢出处置优先级(禁静默截断):① 重构承载(列表/卡片/表/图/组合版式)→ ② 拆页/分章(议程→主张→证据卡→明细;01a/01b;信息须完整)→ ③ 换布局形态(V1–V4/多列/卡栅格)→ ④ 有限 fontShrink(阶梯内、模式 floor 以上、最多 4 档)→ ⑤ 禁止:静默截断、砍 so-what/证据/口径、为审美稀疏删决策必需信息。单页字数预算是预警不是删字许可证。",
1327
1330
  "fontShrink": {
1328
- "$comment": "有限缩字号策略(v8.2 放松)。内容确实偏多、优化格式与措辞后仍装不下时,允许在阶梯内下探;绝对下限按模式。优先顺序永远是:列表化/精炼 → 换组合/拆页 → 最后才缩字号。",
1331
+ "$comment": "有限缩字号(最后手段)。优先:重构承载 → 拆页/分章 → 换布局 → 最后才缩字号。最多 4 档;绝对下限按模式 floor。禁止无限缩字号,禁止用缩字号代替拆页。",
1329
1332
  "ladder": [
1330
1333
  15,
1331
1334
  14,
@@ -1732,8 +1735,8 @@
1732
1735
  ],
1733
1736
  "fillTarget": {
1734
1737
  "presentation": [
1735
- 62,
1736
- 78
1738
+ 58,
1739
+ 75
1737
1740
  ],
1738
1741
  "research": [
1739
1742
  70,
@@ -1774,15 +1777,18 @@
1774
1777
  ]
1775
1778
  },
1776
1779
  "list": {
1777
- "maxItemLines": 2,
1778
- "maxItemChars": 45,
1780
+ "maxItemLines": 3,
1781
+ "maxItemChars": 72,
1779
1782
  "itemGap": "sp-3",
1780
- "maxPerCard": 4
1783
+ "maxPerCard": 5,
1784
+ "$comment": "结构引导(非硬砍刀):条目宜可读完整;超长→加行/拆项/拆页,禁省略号截断充数。maxItemChars/maxItemLines 为投影可读建议,校验不因「字多」单独 FAIL。",
1785
+ "preferItemChars": 45
1781
1786
  },
1782
1787
  "card": {
1783
1788
  "padToken": "sp-5",
1784
1789
  "equalHeight": true,
1785
- "maxBullets": 4
1790
+ "maxBullets": 5,
1791
+ "$comment": "maxBullets 为建议;更多→拆卡/跟进页,勿截断条目。"
1786
1792
  },
1787
1793
  "metric": {
1788
1794
  "perRow": [
@@ -1837,11 +1843,73 @@
1837
1843
  "a-end",
1838
1844
  "align-items"
1839
1845
  ],
1840
- "requireOnMixed": true
1846
+ "requireOnMixed": true,
1847
+ "$comment": "混排须显式对齐类;layout-qa 另检列节奏(多列网格共享顶边/左缘意图)。",
1848
+ "requireColumnRhythm": true
1849
+ },
1850
+ "fillTargetComment": "填充率目标带。Mode A 大气留白:cover/section/quote/closing 等休止页为 intentional whitespace,不按 underfill/FILL_LOW 惩罚(见 qualityGates.underfill.exemptIds + validate LAYOUT_FILL 轻量页豁免)。内容页半空仍属 underfill。硬规则守舞台;自适应留给内容。",
1851
+ "modeSkels": {
1852
+ "$comment": "Mode A 主力骨架 = P1–P4 + P6/P10;P7–P9、P11–P12 为 A 次级(research/architecture 场景下可升主力)。P5 三卡为 A 常用扩展(非默认轮换)。次级仍合法、文档与画廊可引用,禁止删除。",
1853
+ "presentation": {
1854
+ "primary": [
1855
+ "P1",
1856
+ "P2",
1857
+ "P3",
1858
+ "P4",
1859
+ "P6",
1860
+ "P10"
1861
+ ],
1862
+ "secondary": [
1863
+ "P7",
1864
+ "P8",
1865
+ "P9",
1866
+ "P11",
1867
+ "P12"
1868
+ ],
1869
+ "extended": [
1870
+ "P5"
1871
+ ]
1872
+ },
1873
+ "research": {
1874
+ "primary": [
1875
+ "P5",
1876
+ "P7",
1877
+ "P8",
1878
+ "P4",
1879
+ "P6"
1880
+ ],
1881
+ "secondary": [
1882
+ "P1",
1883
+ "P2",
1884
+ "P3",
1885
+ "P9",
1886
+ "P10",
1887
+ "P11",
1888
+ "P12"
1889
+ ]
1890
+ },
1891
+ "architecture": {
1892
+ "primary": [
1893
+ "P9",
1894
+ "P10",
1895
+ "P11"
1896
+ ],
1897
+ "secondary": [
1898
+ "P1",
1899
+ "P2",
1900
+ "P3",
1901
+ "P4",
1902
+ "P5",
1903
+ "P6",
1904
+ "P7",
1905
+ "P8",
1906
+ "P12"
1907
+ ]
1908
+ }
1841
1909
  }
1842
1910
  },
1843
1911
  "qualityGates": {
1844
- "$comment": "v9 硬门禁阈值(validate_report / validate_pptx / quality_gate 消费)。tagLeakPatterns = 可见文本中禁止出现的 HTML 源码片段;titleOnly = 去页头后正文字符上限(低于即 TITLE_ONLY_PAGE);underfillCarriers = 承载组件数下限;roundtripMin = 模型代表性文本落页覆盖率下限(原 0.50 过松,放行截断);chartSkew = 极偏占比禁 donut/pie;chartOversize = 简单图占内容区面积上限。",
1912
+ "$comment": "v9+ 硬门禁。tagLeakPatterns / titleOnly / underfill / roundtripMin / chartSkew / chartOversize 同前。antiTruncation:截断迹象与溢出未拆页;chromeDriftIn:跨页 chrome y 容差(英寸)。",
1845
1913
  "tagLeakPatterns": [
1846
1914
  "<a ",
1847
1915
  "</a>",
@@ -1866,8 +1934,11 @@
1866
1934
  "refs",
1867
1935
  "appendix",
1868
1936
  "next",
1869
- "closing"
1870
- ]
1937
+ "closing",
1938
+ "section",
1939
+ "chapter"
1940
+ ],
1941
+ "$comment": "intentional whitespace 豁免:封面/章节幕/金句/收尾/参考资料等休止页不要求承载数下限;其余内容页半空 = underfill FAIL。"
1871
1942
  },
1872
1943
  "roundtripMin": 0.8,
1873
1944
  "chartSkew": {
@@ -1885,6 +1956,28 @@
1885
1956
  "simpleMaxCats": 2,
1886
1957
  "simpleMaxPts": 3,
1887
1958
  "maxContentAreaPct": 55
1959
+ },
1960
+ "antiTruncation": {
1961
+ "$comment": "截断/内容损失启发式。ellipsisInContent=正文/列表中的截断省略号;overflowNoSplit 与 layout-qa 同源。",
1962
+ "ellipsisPatterns": [
1963
+ "…",
1964
+ "...",
1965
+ "……",
1966
+ "…。",
1967
+ "...。"
1968
+ ],
1969
+ "forbidEllipsisTruncate": true,
1970
+ "overflowNoSplitFail": true
1971
+ },
1972
+ "chromeDriftIn": {
1973
+ "$comment": "PPTX 内容页页脚/页码 y 相对中位数漂移超过此值(英寸)→ WARN CHROME_DRIFT。封面/收尾豁免。默认 0.2in,抓明显漂移、不挡正常页型微差。",
1974
+ "maxAbsDeltaIn": 0.2
1975
+ },
1976
+ "halfEmpty": {
1977
+ "$comment": "半空卡:同页多卡时,正文过短的卡占比过高 → layout-qa WARN/FAIL。",
1978
+ "minCards": 3,
1979
+ "shortPlainChars": 12,
1980
+ "maxShortRatio": 0.5
1888
1981
  }
1889
1982
  },
1890
1983
  "imageAdmission": {
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * TopPPT HTML · 页型布局区域 IR(双通道几何收敛起点)
3
3
  *
4
- * 从 layout-constants.json + layout_slots.json 派生「语义槽位 → 英寸矩形」。
5
- * B 通道(build_pptx.js)直接 require;A 通道消费同一 LC 常量(数字同源),
6
- * 本模块是把「槽位语义」从散落坐标收拢成可测接口的第一步。
4
+ * 从 layout-constants.json(含 layoutSlots)派生「语义槽位 → 英寸矩形」。
5
+ * B 通道(build_pptx.js)直接 require;A 通道消费同一 LC 常量(数字同源)。
7
6
  *
8
- * 纪律:改几何只改 layout-constants.json;改槽位只改 layout_slots.json。
9
- * 新增页型先扩 layout_slots.json,再在此补 regionOf 分支,最后写双引擎。
7
+ * 纪律:改几何与槽位只改 layout-constants.json(layoutSlots 已并入)。
8
+ * 新增页型先扩 LC.layoutSlots,再在此补 regionOf 分支,最后写双引擎。
10
9
  */
11
10
  'use strict';
12
11
 
@@ -15,10 +14,9 @@ const path = require('path');
15
14
 
16
15
  const ROOT = path.join(__dirname, '..');
17
16
  const LC = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout-constants.json'), 'utf8'));
18
- // 布局 IR:优先 layout-constants.layoutSlots(v9 单源合并);否则回落 layout_slots.json
19
- let SLOTS = LC.layoutSlots;
17
+ const SLOTS = LC.layoutSlots;
20
18
  if (!SLOTS || !SLOTS.pageTypes) {
21
- SLOTS = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout_slots.json'), 'utf8'));
19
+ throw new Error('layout-constants.json 缺 layoutSlots.pageTypes(Batch 3 单源)');
22
20
  }
23
21
 
24
22
  const INFO_TYPES = {
@@ -387,7 +385,7 @@ function regionOf(pageType, slotId, opts) {
387
385
  return null;
388
386
  }
389
387
 
390
- /** 槽位是否在 layout_slots.json 中声明为 required */
388
+ /** 槽位是否在 LC.layoutSlots 中声明为 required */
391
389
  function slotRequired(pageType, slotId) {
392
390
  const def = (SLOTS.pageTypes || {})[pageType];
393
391
  if (!def) return false;
@@ -20,6 +20,9 @@
20
20
  N12 模型 theme 与 data-theme 矛盾
21
21
  N13 模型 mode 与 data-mode 矛盾
22
22
  N14 内部锚点断裂(链接目标 id 不存在)
23
+ L4 截断迹象(列表项省略号砍义)
24
+ L5 溢出未拆页
25
+ L6 混排缺对齐(ALIGN_RHYTHM)
23
26
 
24
27
  用法: python scripts/negative_tests.py
25
28
  """
@@ -62,6 +65,25 @@ def expect_fail(name: str, path: Path, keyword: str) -> bool:
62
65
  return ok
63
66
 
64
67
 
68
+
69
+ def expect_layout_qa_fail(name: str, path: Path, keyword: str) -> bool:
70
+ """Run validate_report --strict --layout-qa and require FAIL containing keyword."""
71
+ r = subprocess.run(
72
+ [PY, str(ROOT / 'scripts' / 'validate_report.py'), str(path),
73
+ '--strict', '--layout-qa'],
74
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
75
+ out = (r.stdout or '') + (r.stderr or '')
76
+ hit = [ln for ln in out.splitlines()
77
+ if keyword in ln and ('[FAIL]' in ln or 'FAIL' in ln)]
78
+ ok = r.returncode != 0 and bool(hit)
79
+ print(f' [{"OK" if ok else "FAIL"}] {name}')
80
+ if hit:
81
+ print(f' 抓到: {hit[0].strip()[:120]}')
82
+ elif not ok:
83
+ print(f' layout-qa 未触发(期望含「{keyword}」)')
84
+ return ok
85
+
86
+
65
87
  def main() -> int:
66
88
  base = ROOT / 'assets' / 'examples' / '2026-09-09-research-mckinsey.html'
67
89
  if not base.exists():
@@ -163,7 +185,111 @@ def main() -> int:
163
185
  p.write_text(content, encoding='utf-8')
164
186
  ok = expect_fail(name, p, keyword) and ok
165
187
 
166
- ok = _pptx_notes_case() and ok
188
+
189
+ # ── Batch 2 · layout-qa 专用负例 ─────────────────────────────────────
190
+ # L1 管线产物缺 data-skel:先造带 skel 的页,再撕掉其中一页的标记
191
+ import tempfile
192
+ scaf = OUT / 'L1-scaffold.html'
193
+ subprocess.run(
194
+ [PY, str(ROOT / 'scripts' / 'scaffold_report.py'),
195
+ '--mode', 'presentation', '--style', 'business-blue', '--theme', 'light',
196
+ '--title', 'LayoutQA Neg', '--sections', '5', '--out', str(scaf)],
197
+ capture_output=True, text=True)
198
+ if scaf.exists() and 'data-skel="' in scaf.read_text(encoding='utf-8'):
199
+ t_l1 = scaf.read_text(encoding='utf-8')
200
+ # 撕掉第一个内容页的 data-skel
201
+ t_l1b = re.sub(r'(id="s1") data-skel="P\d+"', r'\1', t_l1, count=1)
202
+ p_l1 = OUT / 'L1-missing-skel.html'
203
+ p_l1.write_text(t_l1b, encoding='utf-8')
204
+ ok = expect_layout_qa_fail('L1 缺 data-skel', p_l1, 'LAYOUT_QA_MISSING_SKEL') and ok
205
+ # L2 连续 3 页同一 skel
206
+ t_l2 = t_l1
207
+ # force s1,s2,s3 all P4
208
+ t_l2 = re.sub(r'(id="s1"[^>]*data-skel=")P\d+"', r'\1P4"', t_l2, count=1)
209
+ t_l2 = re.sub(r'(id="s2"[^>]*data-skel=")P\d+"', r'\1P4"', t_l2, count=1)
210
+ t_l2 = re.sub(r'(id="s3"[^>]*data-skel=")P\d+"', r'\1P4"', t_l2, count=1)
211
+ # also rewrite if attribute order is data-skel before id
212
+ t_l2 = re.sub(r'(data-skel=")P\d+(" data-page-type="[^"]+" id="s1")', r'\1P4\2', t_l2)
213
+ t_l2 = re.sub(r'(data-skel=")P\d+("([^>]*) id="s1")', r'\1P4\2', t_l2)
214
+ # simpler: replace first three data-skel values
215
+ def _force_p4(src, n=3):
216
+ out, c = [], 0
217
+ for part in re.split(r'(data-skel="P\d+")', src):
218
+ if part.startswith('data-skel="') and c < n:
219
+ out.append('data-skel="P4"'); c += 1
220
+ else:
221
+ out.append(part)
222
+ return ''.join(out)
223
+ t_l2 = _force_p4(t_l1, 3)
224
+ p_l2 = OUT / 'L2-skel-streak.html'
225
+ p_l2.write_text(t_l2, encoding='utf-8')
226
+ ok = expect_layout_qa_fail('L2 连续同 skel', p_l2, 'LAYOUT_QA_SKEL_STREAK') and ok
227
+ # L3 极偏 donut in REPORT_MODEL
228
+ t_l3 = t_l1
229
+ if 'window.REPORT_MODEL' in t_l3:
230
+ # inject a skewed donut section into model JSON
231
+ inj = (
232
+ '{"type":"donut","title":"极偏环",'
233
+ '"chart":{"type":"donut","labels":["A","B"],"values":[0.5,99.5]}},'
234
+ )
235
+ t_l3 = re.sub(r'("sections"\s*:\s*\[)', r'\1' + inj, t_l3, count=1)
236
+ p_l3 = OUT / 'L3-skew-donut.html'
237
+ p_l3.write_text(t_l3, encoding='utf-8')
238
+ ok = expect_layout_qa_fail('L3 极偏 donut', p_l3, 'LAYOUT_QA_SKEW_DONUT') and ok
239
+
240
+ # L4 截断迹象:列表项以省略号砍义
241
+ t_l4 = t_l1
242
+ inj4 = '<div class="card"><ul><li class="card__li">这项证据其实很长但被故意截断了…</li></ul></div>'
243
+ t_l4 = re.sub(r'(id="s1"[^>]*>)', lambda m: m.group(1) + inj4, t_l4, count=1)
244
+ p_l4 = OUT / 'L4-truncation.html'
245
+ p_l4.write_text(t_l4, encoding='utf-8')
246
+ ok = expect_layout_qa_fail('L4 截断迹象', p_l4, 'LAYOUT_QA_TRUNCATION') and ok
247
+
248
+ # L5 溢出未拆页:塞入超长正文使页高估算爆掉且无续页信号
249
+ t_l5 = t_l1
250
+ wall = '论证要点' + ('详细证据与口径说明,必须完整保留不得删减。' * 40)
251
+ t_l5 = re.sub(
252
+ r'(id="s1"[^>]*>)',
253
+ lambda m: m.group(1) + '<div class="card"><p>' + wall + '</p></div>'
254
+ + '<div class="card"><p>' + wall + '</p></div>'
255
+ + '<div class="card"><p>' + wall + '</p></div>',
256
+ t_l5, count=1)
257
+ p_l5 = OUT / 'L5-overflow-nosplit.html'
258
+ p_l5.write_text(t_l5, encoding='utf-8')
259
+ ok = expect_layout_qa_fail('L5 溢出未拆页', p_l5, 'LAYOUT_QA_OVERFLOW_NO_SPLIT') and ok
260
+
261
+ # L6 混排缺对齐:图+卡同页但无对齐类
262
+ t_l6 = t_l1
263
+ inj6 = (
264
+ '<div class="grid g-2">'
265
+ '<div class="fig"><svg class="chart" data-chart="bar" style="height:200px"></svg></div>'
266
+ '<div class="card"><ul><li>证据甲</li><li>证据乙</li></ul></div>'
267
+ '</div>'
268
+ )
269
+ t_l6 = re.sub(r'(id="s1"[^>]*>)', lambda m: m.group(1) + inj6, t_l6, count=1)
270
+ # 撕掉 s1 段内对齐类,确保 ALIGN_RHYTHM 能抓住
271
+ def _strip_align_s1(src: str) -> str:
272
+ m = re.search(r'(<section[^>]*id="s1"[^>]*>)([\s\S]*?)(</section>)', src)
273
+ if not m:
274
+ # attribute order may be data-skel before id
275
+ m = re.search(r'(<section[^>]*id="s1"[^>]*>)([\s\S]*?)(</section>)', src)
276
+ if not m:
277
+ return src
278
+ body = m.group(2)
279
+ body = re.sub(r'\ba-start\b', '', body)
280
+ body = re.sub(r'\ba-c\b', '', body)
281
+ body = re.sub(r'\ba-end\b', '', body)
282
+ body = body.replace('align-items:start', '').replace('align-items: center', '')
283
+ return src[:m.start()] + m.group(1) + body + m.group(3) + src[m.end():]
284
+ t_l6 = _strip_align_s1(t_l6)
285
+ p_l6 = OUT / 'L6-align-rhythm.html'
286
+ p_l6.write_text(t_l6, encoding='utf-8')
287
+ ok = expect_layout_qa_fail('L6 混排缺对齐', p_l6, 'LAYOUT_QA_ALIGN_RHYTHM') and ok
288
+
289
+ else:
290
+ print(' [SKIP] layout-qa 负例(scaffold 未产出 data-skel)')
291
+
292
+ ok = _pptx_notes_case() and ok
167
293
  ok = _chart_data_case() and ok
168
294
  ok = _title_pattern_case() and ok
169
295
  ok = _font_scale_case() and ok
@@ -17,14 +17,14 @@
17
17
  README.md 人类视角的简介 / 安装 / 开发 / 打包
18
18
  package.json Node 依赖声明(pptxgenjs)与常用命令
19
19
  assets/templates/ 三份模式模板 + engine.css / ui.js(公共片段注入源)
20
- assets/examples/ 9 份示例 HTML + 9 份 REPORT_MODEL(复制起点 · 覆盖全部 9 风格)
20
+ assets/examples/ 3 份黄金样张 HTML + model(每模式 1;风格见 style-gallery)
21
21
  assets/pptx-export.js PPTX 预览运行时(注入源)
22
22
  assets/style-gallery.html 风格 × 模式 × 亮暗主题交互画廊
23
23
  assets/theme-overview*.png 3 张主题参考图(整体 + research + architecture;演示态与整体图相同)
24
- references/*.md 15 篇规范(playbook.md = 唯一常读入口 L1;其余 L2 按需加载)
24
+ references/*.md 规范全文(Batch3 起 ≥20 篇;playbook.md = L1;其余 L2 按需)
25
25
  scripts/ 生成 / 校验 / 回归 / 维护工具(py + js + json 单源)
26
26
  scripts/extract_snippet.py L2 节级片段抽取(减少整读大规范)
27
- scripts/layout_slots.json 页型布局 IR(高频页型语义槽位)
27
+ # layoutSlots 已并入 layout-constants.json(Batch 3)
28
28
 
29
29
  ────────────────────────────────────────────────────────────────────────
30
30
  不进包(构建产物 / 本地状态 / 缓存,与 .gitignore 口径一致)
@@ -42,7 +42,7 @@
42
42
  ────────────────────────────────────────────────────────────────────────
43
43
  · SKILL.md frontmatter 含 name / description;name 与分发包目录名一致;
44
44
  description 为单行双引号字符串且 ≤ 1024 字符(平台截断阈值)
45
- · references(含 layout-grammar)、3 份模式模板 + engine.css / ui.js、9 组示例、3 张参考图齐全
45
+ · references(含 layout-grammar)、3 份模式模板 + engine.css / ui.js、3 组黄金样张、3 张参考图齐全
46
46
  · 关键脚本齐全(单源注入 / 审计 / 示例重建 / 回归 / 双校验器 / 精导 / 打包)
47
47
  """
48
48
  import fnmatch
@@ -118,27 +118,30 @@ REQUIRED = [
118
118
  'assets/templates/ui.js',
119
119
  'references/playbook.md',
120
120
  'references/layout-grammar.md',
121
+ 'references/default-surface.md',
122
+ 'references/presentation-craft.md',
121
123
  'references/modes.md', 'references/outline-design.md', 'references/design-system.md',
122
124
  'references/styles.md', 'references/content-rules.md', 'references/components.md',
123
125
  'references/charts.md',
124
126
  'references/infographics.md', 'references/icons.md', 'references/pptx-export.md',
125
127
  'references/high-fidelity.md', 'references/failure-modes.md',
126
- 'references/tech-design.md', 'references/industry-benchmark.md',
128
+ 'references/tech-design.md',
127
129
  'scripts/env_probe.py', 'scripts/audit_docs.py', 'scripts/audit_skill.py',
128
130
  'scripts/scaffold_report.py',
131
+ 'scripts/render_from_model.py',
132
+ 'scripts/recommend_layout.py',
129
133
  'scripts/sync_runtime.py', 'scripts/audit_styles.py', 'scripts/build_examples.py',
130
134
  'scripts/regression.py', 'scripts/validate_report.py', 'scripts/validate_pptx.py',
131
135
  'scripts/extract_model.py', 'scripts/build_pptx.js', 'scripts/gen_channel_a.js',
132
136
  'scripts/cross_verify.py', 'scripts/prepare_images.py', 'scripts/probe_image_export.py',
133
137
  'scripts/render_compare.py', 'scripts/capture_theme_overview.js', 'scripts/package_skill.py',
134
- 'scripts/layout-constants.json', 'scripts/model-schema.json', 'scripts/layout_slots.json',
138
+ 'scripts/layout-constants.json', 'scripts/model-schema.json',
135
139
  'scripts/lib_layout_regions.js', 'scripts/extract_snippet.py', 'scripts/quality_gate.py',
136
140
  'scripts/checks_html.py', 'scripts/section-file-map.json',
137
141
  'scripts/measure_height.py', 'scripts/audit_css.py', 'scripts/negative_tests.py',
138
142
  'references/components-atoms.md', 'references/layouts-research.md',
139
143
  'references/layouts-architecture.md', 'references/layouts-combo.md',
140
144
  'references/charts-basic.md', 'references/charts-extended.md', 'references/charts-discipline.md',
141
- 'references/design-system-engine.md',
142
145
  'references/infographics-stats.md', 'references/infographics-structure.md',
143
146
  'evals/prompts.csv', 'evals/run_evals.py', 'evals/rubric.schema.json',
144
147
  'evals/trace.example.json',
@@ -146,9 +149,9 @@ REQUIRED = [
146
149
 
147
150
  # 必须达到最小数量的集合(名称含日期,故按数量校验)
148
151
  MIN_COUNTS = {
149
- 'assets/examples/*.html': 9,
150
- 'assets/examples/*.model.json': 9,
151
- 'references/*.md': 15,
152
+ 'assets/examples/*.html': 3, # Batch3 每模式 1 份黄金样张
153
+ 'assets/examples/*.model.json': 3,
154
+ 'references/*.md': 22, # craft 轮后含 default-surface + presentation-craft;≥22 防误删
152
155
  }
153
156
 
154
157
 
@@ -4,6 +4,7 @@
4
4
 
5
5
  用法:
6
6
  python scripts/quality_gate.py report.html
7
+ # Mode A / presentation 自动对 validate_report 附加 --layout-qa
7
8
  python scripts/quality_gate.py report.html --pptx report.pptx --model report.model.json
8
9
  python scripts/quality_gate.py report.html --trace trace.json --json
9
10
  python scripts/quality_gate.py report.html --require-rubric 60
@@ -241,10 +242,17 @@ def main() -> int:
241
242
  print(f'交付质量门禁 · {html.name}')
242
243
  print('-' * 60)
243
244
 
244
- # ① HTML strict
245
- rc, out, el = run([sys.executable, str(ROOT / 'scripts' / 'validate_report.py'), str(html), '--strict'])
245
+ # ① HTML strict(Mode A / presentation 自动附带 --layout-qa)
246
+ head_snip = html.read_text(encoding='utf-8')[:2500]
247
+ mode_m = re.search(r'data-mode="([^"]+)"', head_snip)
248
+ mode = mode_m.group(1) if mode_m else 'presentation'
249
+ vcmd = [sys.executable, str(ROOT / 'scripts' / 'validate_report.py'), str(html), '--strict']
250
+ if mode == 'presentation':
251
+ vcmd.append('--layout-qa')
252
+ rc, out, el = run(vcmd)
253
+ gate_name = 'validate_report --strict' + (' --layout-qa' if mode == 'presentation' else '')
246
254
  tail = [ln for ln in out.splitlines() if ln.startswith('PASS ') or ln.startswith('结论')]
247
- gate('validate_report --strict', rc == 0, tail[-1] if tail else f'exit {rc}', el)
255
+ gate(gate_name, rc == 0, tail[-1] if tail else f'exit {rc}', el)
248
256
 
249
257
  # ② PPTX strict(可选)
250
258
  if args.pptx: