@topmindspace/tms-skills 0.1.1 → 0.1.3

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 (64) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +13 -3
  3. package/bin/tms-skills.js +7 -2
  4. package/package.json +1 -1
  5. package/top-ppt-html/README.md +69 -148
  6. package/top-ppt-html/SKILL.md +42 -23
  7. package/top-ppt-html/agents/openai.yaml +5 -0
  8. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  9. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  10. package/top-ppt-html/assets/theme-overview.png +0 -0
  11. package/top-ppt-html/evals/trigger-queries.json +181 -0
  12. package/top-ppt-html/package.json +3 -2
  13. package/top-ppt-html/references/chart-decision-tree.md +42 -0
  14. package/top-ppt-html/references/charts-discipline.md +3 -1
  15. package/top-ppt-html/references/charts.md +24 -15
  16. package/top-ppt-html/references/content-rules.md +54 -34
  17. package/top-ppt-html/references/default-surface.md +62 -0
  18. package/top-ppt-html/references/design-system.md +2 -2
  19. package/top-ppt-html/references/failure-modes.md +14 -1
  20. package/top-ppt-html/references/icons.md +56 -277
  21. package/top-ppt-html/references/illustration-layout.md +39 -0
  22. package/top-ppt-html/references/layout-grammar.md +16 -6
  23. package/top-ppt-html/references/modes.md +16 -11
  24. package/top-ppt-html/references/outline-design.md +24 -0
  25. package/top-ppt-html/references/page-type-matrix.md +35 -0
  26. package/top-ppt-html/references/playbook.md +42 -86
  27. package/top-ppt-html/references/pptx-export.md +20 -2
  28. package/top-ppt-html/references/presentation-craft.md +77 -0
  29. package/top-ppt-html/references/tech-design.md +11 -5
  30. package/top-ppt-html/scripts/audit_docs.py +1 -1
  31. package/top-ppt-html/scripts/audit_skill.py +45 -6
  32. package/top-ppt-html/scripts/build_examples.py +39 -2260
  33. package/top-ppt-html/scripts/build_pptx.js +6 -0
  34. package/top-ppt-html/scripts/check_triggers.py +153 -0
  35. package/top-ppt-html/scripts/checks_html.py +39 -0
  36. package/top-ppt-html/scripts/cross_verify.py +5 -1
  37. package/top-ppt-html/scripts/extract_snippet.py +2 -2
  38. package/top-ppt-html/scripts/layout-constants.json +111 -18
  39. package/top-ppt-html/scripts/lib_layout_regions.js +7 -9
  40. package/top-ppt-html/scripts/negative_tests.py +137 -1
  41. package/top-ppt-html/scripts/package_skill.py +29 -10
  42. package/top-ppt-html/scripts/quality_gate.py +11 -3
  43. package/top-ppt-html/scripts/recommend_layout.py +384 -0
  44. package/top-ppt-html/scripts/regression.py +1 -1
  45. package/top-ppt-html/scripts/smoke_pptx.sh +30 -0
  46. package/top-ppt-html/scripts/sync_runtime.py +22 -25
  47. package/top-ppt-html/scripts/validate_pptx.py +69 -0
  48. package/top-ppt-html/scripts/validate_report.py +336 -10
  49. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +0 -3926
  50. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +0 -168
  51. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +0 -4325
  52. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +0 -321
  53. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +0 -4325
  54. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +0 -321
  55. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +0 -5527
  56. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +0 -914
  57. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +0 -5527
  58. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +0 -914
  59. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +0 -5527
  60. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +0 -914
  61. package/top-ppt-html/references/design-system-engine.md +0 -235
  62. package/top-ppt-html/references/industry-benchmark.md +0 -105
  63. package/top-ppt-html/references/reform-plan.md +0 -252
  64. 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);
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML · description trigger coverage (keyword/heuristic, no LLM).
4
+
5
+ Gates regressions when SKILL.md description drifts away from eval queries.
6
+ See evals/trigger-queries.json → how_to_extend.
7
+
8
+ Usage:
9
+ python3 scripts/check_triggers.py
10
+ python3 scripts/check_triggers.py --json
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ try:
20
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
21
+ except Exception:
22
+ pass
23
+
24
+ ROOT = Path(__file__).resolve().parent.parent
25
+ SKILL = ROOT / 'SKILL.md'
26
+ QUERIES = ROOT / 'evals' / 'trigger-queries.json'
27
+
28
+ # Tokens that, if present in a query AND in description (or Do-NOT zone for negatives),
29
+ # count toward a hit. Multi-char CJK / ASCII keywords preferred over single chars.
30
+ POS_KEYWORDS = [
31
+ '报告', '演示', '汇报', '路演', '调研', '白皮书', '评测', '对标', '经营分析', '复盘',
32
+ 'PPT', 'PPTX', 'pptx', 'slides', 'deck', 'HTML', '网页报告',
33
+ '架构图', '拓扑', '流程图', '泳道', '方案图',
34
+ '排版', '版式', '配色', '图文',
35
+ '快速模式', 'fast', '一键出稿', '直接生成', '少问一句',
36
+ '可视化', '高保真', '可编辑',
37
+ '咨询报告', '分析报告', '研究报告', '项目汇报', '商务',
38
+ ]
39
+
40
+ # Negative-zone cues in description (Do NOT …) — if a miss-query matches these
41
+ # boundary phrases more than positive product cues, treat as correctly excluded.
42
+ NEG_BOUNDARY = [
43
+ '纯代码', '代码工程', '非报告类', '视频', '图片生成',
44
+ 'Word', '源文件', '应用开发', 'coding', 'Do NOT',
45
+ ]
46
+
47
+ # Extra strong negative query cues (if query has these AND lacks report cues → expect miss)
48
+ NEG_QUERY_CUES = [
49
+ 'react', '组件', '单测', 'debug', 'python 报错', '视频', 'mp4', 'midjourney',
50
+ '海报图', '已有的 pptx', '已有 pptx', 'word 文档', '电商官网', 'sql',
51
+ 'nginx', 'refactor', 'typescript', '剪辑', '录像', '字幕', '迁移脚本',
52
+ ]
53
+
54
+
55
+ def load_description() -> str:
56
+ txt = SKILL.read_text(encoding='utf-8')
57
+ m = re.search(r'^description:\s*"(.*)"\s*$', txt, re.M)
58
+ if not m:
59
+ raise SystemExit('SKILL.md missing description frontmatter')
60
+ return m.group(1)
61
+
62
+
63
+ def tokenize_hits(query: str, desc: str) -> list[str]:
64
+ q_low = query.lower()
65
+ d_low = desc.lower()
66
+ hits = []
67
+ for kw in POS_KEYWORDS:
68
+ if kw.lower() in q_low and kw.lower() in d_low:
69
+ hits.append(kw)
70
+ return hits
71
+
72
+
73
+ def looks_negative_query(query: str) -> bool:
74
+ q = query.lower()
75
+ return any(c in q for c in NEG_QUERY_CUES)
76
+
77
+
78
+ def boundary_covered(desc: str) -> bool:
79
+ return sum(1 for b in NEG_BOUNDARY if b.lower() in desc.lower()) >= 2
80
+
81
+
82
+ def evaluate(desc: str, data: dict) -> tuple[list[dict], int, int]:
83
+ rows = []
84
+ fails = 0
85
+ for bucket, default_expect in (('positive', 'hit'), ('negative', 'miss')):
86
+ for item in data.get(bucket, []):
87
+ q = item['query']
88
+ expect = item.get('expect', default_expect)
89
+ hits = tokenize_hits(q, desc)
90
+ if expect == 'hit':
91
+ ok = len(hits) >= 1
92
+ detail = f'keywords={hits}' if ok else 'no shared trigger keyword with description'
93
+ else:
94
+ # miss: either no positive keyword overlap, OR query is clearly
95
+ # out-of-scope and description has Do-NOT boundaries.
96
+ ok = (len(hits) == 0) or (
97
+ looks_negative_query(q) and boundary_covered(desc) and len(hits) <= 1
98
+ )
99
+ # Stricter: if query is a known out-of-scope cue, require miss even
100
+ # when a weak overlapping token exists (e.g. "pptx" in "改已有 pptx").
101
+ if looks_negative_query(q) and boundary_covered(desc):
102
+ ok = True
103
+ detail = (
104
+ f'miss-ok boundary; weak_hits={hits}' if ok
105
+ else f'unexpected trigger overlap={hits}'
106
+ )
107
+ rows.append({
108
+ 'id': item.get('id'),
109
+ 'expect': expect,
110
+ 'ok': ok,
111
+ 'query': q,
112
+ 'detail': detail,
113
+ })
114
+ if not ok:
115
+ fails += 1
116
+ return rows, fails, len(rows)
117
+
118
+
119
+ def main() -> int:
120
+ as_json = '--json' in sys.argv
121
+ if not QUERIES.exists():
122
+ print(f'MISSING {QUERIES}')
123
+ return 1
124
+ data = json.loads(QUERIES.read_text(encoding='utf-8'))
125
+ desc = load_description()
126
+ rows, fails, total = evaluate(desc, data)
127
+ n_pos = sum(1 for r in rows if r['expect'] == 'hit')
128
+ n_neg = sum(1 for r in rows if r['expect'] == 'miss')
129
+ if as_json:
130
+ print(json.dumps({
131
+ 'total': total, 'fails': fails,
132
+ 'positive': n_pos, 'negative': n_neg,
133
+ 'description_chars': len(desc),
134
+ 'rows': rows,
135
+ }, ensure_ascii=False, indent=2))
136
+ else:
137
+ print(f'trigger coverage · {total - fails}/{total} '
138
+ f'(+{n_pos} / −{n_neg}) · description {len(desc)} chars')
139
+ for r in rows:
140
+ mark = 'OK' if r['ok'] else 'FAIL'
141
+ print(f' [{mark}] {r["id"]} expect={r["expect"]} {r["detail"]}')
142
+ if not r['ok']:
143
+ print(f' Q: {r["query"]}')
144
+ if fails:
145
+ print(f'\nFAILED {fails} query(ies). Extend keywords in SKILL description '
146
+ f'or adjust evals/trigger-queries.json (see how_to_extend).')
147
+ else:
148
+ print('\nAll trigger heuristics passed.')
149
+ return 1 if fails else 0
150
+
151
+
152
+ if __name__ == '__main__':
153
+ sys.exit(main())
@@ -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;