@topmindspace/tms-skills 0.1.0 → 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 (102) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +9 -3
  3. package/bin/tms-skills.js +200 -160
  4. package/package.json +12 -9
  5. package/top-ppt-html/README.md +162 -254
  6. package/top-ppt-html/SKILL.md +126 -111
  7. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -3926
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +167 -167
  9. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -4325
  10. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
  11. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -5527
  12. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
  13. package/top-ppt-html/assets/pptx-export.js +1944 -1944
  14. package/top-ppt-html/assets/style-gallery.html +589 -589
  15. package/top-ppt-html/assets/templates/architecture.html +3728 -3728
  16. package/top-ppt-html/assets/templates/engine.css +840 -840
  17. package/top-ppt-html/assets/templates/presentation.html +3738 -3738
  18. package/top-ppt-html/assets/templates/research.html +4017 -4017
  19. package/top-ppt-html/assets/templates/ui.js +520 -520
  20. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  21. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  22. package/top-ppt-html/assets/theme-overview.png +0 -0
  23. package/top-ppt-html/evals/prompts.csv +15 -15
  24. package/top-ppt-html/evals/rubric.schema.json +25 -25
  25. package/top-ppt-html/evals/run_evals.py +220 -220
  26. package/top-ppt-html/evals/trace.example.json +16 -16
  27. package/top-ppt-html/package-lock.json +186 -0
  28. package/top-ppt-html/package.json +11 -11
  29. package/top-ppt-html/references/charts-basic.md +624 -624
  30. package/top-ppt-html/references/charts-discipline.md +110 -108
  31. package/top-ppt-html/references/charts-extended.md +482 -482
  32. package/top-ppt-html/references/charts.md +37 -28
  33. package/top-ppt-html/references/components-atoms.md +624 -624
  34. package/top-ppt-html/references/components.md +30 -30
  35. package/top-ppt-html/references/content-rules.md +510 -490
  36. package/top-ppt-html/references/default-surface.md +61 -0
  37. package/top-ppt-html/references/design-system.md +478 -478
  38. package/top-ppt-html/references/failure-modes.md +227 -214
  39. package/top-ppt-html/references/high-fidelity.md +127 -127
  40. package/top-ppt-html/references/icons.md +176 -397
  41. package/top-ppt-html/references/infographics-stats.md +308 -308
  42. package/top-ppt-html/references/infographics-structure.md +226 -226
  43. package/top-ppt-html/references/infographics.md +43 -43
  44. package/top-ppt-html/references/layout-grammar.md +325 -315
  45. package/top-ppt-html/references/layouts-architecture.md +108 -108
  46. package/top-ppt-html/references/layouts-combo.md +600 -600
  47. package/top-ppt-html/references/layouts-research.md +160 -160
  48. package/top-ppt-html/references/modes.md +259 -254
  49. package/top-ppt-html/references/outline-design.md +275 -275
  50. package/top-ppt-html/references/playbook.md +284 -266
  51. package/top-ppt-html/references/pptx-export.md +227 -209
  52. package/top-ppt-html/references/presentation-craft.md +69 -0
  53. package/top-ppt-html/references/styles.md +370 -370
  54. package/top-ppt-html/references/tech-design.md +138 -138
  55. package/top-ppt-html/scripts/audit_css.py +109 -109
  56. package/top-ppt-html/scripts/audit_docs.py +176 -176
  57. package/top-ppt-html/scripts/audit_skill.py +231 -220
  58. package/top-ppt-html/scripts/audit_styles.py +351 -351
  59. package/top-ppt-html/scripts/build_examples.py +55 -2276
  60. package/top-ppt-html/scripts/build_pptx.js +2386 -2380
  61. package/top-ppt-html/scripts/capture_theme_overview.js +79 -78
  62. package/top-ppt-html/scripts/checks_html.py +166 -127
  63. package/top-ppt-html/scripts/cross_verify.py +298 -294
  64. package/top-ppt-html/scripts/env_probe.py +158 -158
  65. package/top-ppt-html/scripts/extract_model.py +210 -210
  66. package/top-ppt-html/scripts/extract_snippet.py +374 -374
  67. package/top-ppt-html/scripts/gen_channel_a.js +214 -214
  68. package/top-ppt-html/scripts/layout-constants.json +111 -18
  69. package/top-ppt-html/scripts/lib_layout_regions.js +410 -412
  70. package/top-ppt-html/scripts/measure_height.py +178 -178
  71. package/top-ppt-html/scripts/negative_tests.py +433 -307
  72. package/top-ppt-html/scripts/package_skill.py +294 -291
  73. package/top-ppt-html/scripts/prepare_images.py +341 -341
  74. package/top-ppt-html/scripts/probe_image_export.py +187 -188
  75. package/top-ppt-html/scripts/quality_gate.py +309 -301
  76. package/top-ppt-html/scripts/recommend_layout.py +384 -0
  77. package/top-ppt-html/scripts/regression.py +308 -308
  78. package/top-ppt-html/scripts/render_compare.py +275 -275
  79. package/top-ppt-html/scripts/render_from_model.py +698 -698
  80. package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
  81. package/top-ppt-html/scripts/section-file-map.json +104 -104
  82. package/top-ppt-html/scripts/smoke_pptx.sh +30 -0
  83. package/top-ppt-html/scripts/sync_runtime.py +659 -662
  84. package/top-ppt-html/scripts/validate_pptx.py +1579 -1510
  85. package/top-ppt-html/scripts/validate_report.py +1755 -1456
  86. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +0 -3926
  87. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +0 -168
  88. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +0 -4325
  89. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +0 -321
  90. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +0 -4325
  91. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +0 -321
  92. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +0 -5527
  93. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +0 -914
  94. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +0 -5527
  95. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +0 -914
  96. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +0 -5527
  97. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +0 -914
  98. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  99. package/top-ppt-html/references/design-system-engine.md +0 -235
  100. package/top-ppt-html/references/industry-benchmark.md +0 -105
  101. package/top-ppt-html/references/reform-plan.md +0 -252
  102. package/top-ppt-html/scripts/layout_slots.json +0 -830
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML · 布局选型建议(Batch 2)
4
+
5
+ 用法:
6
+ python scripts/recommend_layout.py --mode B --intent "经营分析 占比极偏" --pages 10
7
+ python scripts/recommend_layout.py --mode A --intent "路演" --intent "趋势" --json
8
+ python scripts/recommend_layout.py --mode C --from-model path/to.model.json
9
+ echo '{"mode":"research","intents":["架构"]}' | python scripts/recommend_layout.py --stdin
10
+
11
+ 输出 JSON 页序列:{pageType, skel, chart?, rationale, v?}
12
+ 启发式:playbook V1–V4 / sizeByComplexity / 极偏禁 donut / 高容量多页序列 / layoutSystem.defaultCharts(核图 8);advanced 仅意图命中;Mode A 主力骨架 P1–P4+P6/P10,次级 P7–P9/P11–P12 不默认。
13
+ 不列入 package REQUIRED(可选工具;Batch 3 再定是否纳入)。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ try:
23
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
24
+ except Exception:
25
+ pass
26
+
27
+ ROOT = Path(__file__).resolve().parent
28
+ LC = json.loads((ROOT / 'layout-constants.json').read_text(encoding='utf-8'))
29
+ LS = LC.get('layoutSystem') or {}
30
+ PAGE_TO_PRESET = dict(LS.get('pageToPreset') or {})
31
+ DEFAULT_CHARTS = list(LS.get('defaultCharts') or
32
+ ['bar', 'hbar', 'line', 'donut', 'progress', 'area', 'stack', 'dualline'])
33
+ ADVANCED_CHARTS = set(LS.get('advancedCharts') or [])
34
+ _MODE_SKELS = LS.get('modeSkels') or {}
35
+ PRIMARY_SKELS = {
36
+ 'presentation': list((_MODE_SKELS.get('presentation') or {}).get('primary') or
37
+ ['P1', 'P2', 'P3', 'P4', 'P6', 'P10']),
38
+ 'research': list((_MODE_SKELS.get('research') or {}).get('primary') or
39
+ ['P5', 'P7', 'P8', 'P4', 'P6']),
40
+ 'architecture': list((_MODE_SKELS.get('architecture') or {}).get('primary') or
41
+ ['P9', 'P10', 'P11']),
42
+ }
43
+ SECONDARY_SKELS = {
44
+ 'presentation': list((_MODE_SKELS.get('presentation') or {}).get('secondary') or
45
+ ['P7', 'P8', 'P9', 'P11', 'P12']),
46
+ 'research': list((_MODE_SKELS.get('research') or {}).get('secondary') or []),
47
+ 'architecture': list((_MODE_SKELS.get('architecture') or {}).get('secondary') or []),
48
+ }
49
+
50
+ SIZE_BANDS = list(((LC.get('charts') or {}).get('sizeByComplexity') or {}).get('bands') or [])
51
+ V_MAP = dict(LS.get('vMap') or {'V1': 'P1', 'V2': 'P2', 'V3': 'P3', 'V4': 'P4'})
52
+
53
+ MODE_ALIAS = {
54
+ 'a': 'presentation', 'presentation': 'presentation', '演示': 'presentation',
55
+ 'b': 'research', 'research': 'research', '研究': 'research',
56
+ 'c': 'architecture', 'architecture': 'architecture', '架构': 'architecture',
57
+ }
58
+
59
+ # 模式默认节奏(页型序列);cover/closing 由调用方决定是否保留
60
+ DEFAULT_SEQ = {
61
+ # Mode A:偏 V1–V4 / claim 页(kpi/points/quote);降低 donut 默认权重(构成意图命中再用)
62
+ 'presentation': [
63
+ 'cover', 'agenda', 'kpi', 'points', 'bar', 'points', 'comparison', 'quote', 'closing',
64
+ ],
65
+ 'research': [
66
+ 'cover', 'agenda', 'metrics', 'exhibit', 'twocol', 'halftable',
67
+ 'exhibit', 'threecol', 'matrix', 'flags', 'closing',
68
+ ],
69
+ 'architecture': [
70
+ 'cover', 'diagram', 'lane', 'diagram', 'points', 'closing',
71
+ ],
72
+ }
73
+
74
+ INTENT_RULES: list[tuple[tuple[str, ...], dict]] = [
75
+ (('极偏', '占比失衡', '长尾', '0.5%'), {
76
+ 'pageType': 'kpi', 'chart': None, 'v': 'V3',
77
+ 'rationale': '极偏占比禁 donut/pie → V3 大数+佐证',
78
+ }),
79
+ (('占比', '构成', '份额', '结构'), {
80
+ 'pageType': 'points', 'chart': 'stack', 'v': 'V1',
81
+ 'rationale': '构成类优先 stack/hbar+V1;donut 仅非极偏且需环形直觉时用(降默认权重)',
82
+ }),
83
+ (('趋势', '爬坡', '时间序列', '同比', '环比'), {
84
+ 'pageType': 'bar', 'chart': 'line', 'v': 'V2',
85
+ 'rationale': '时间趋势 → line;演示用 V2 上图下带',
86
+ }),
87
+ (('排名', '对比', '谁高谁低', '对标'), {
88
+ 'pageType': 'bar', 'chart': 'hbar', 'v': 'V1',
89
+ 'rationale': '排名/对比 → hbar',
90
+ }),
91
+ (('架构', '拓扑', '分层', '泳道', '流程'), {
92
+ 'pageType': 'diagram', 'chart': 'network', 'v': None,
93
+ 'rationale': '结构图为王;skel P10/P11',
94
+ }),
95
+ (('路演', '融资', 'pitch', '发布', '汇报', '演讲', 'demo', '演示'), {
96
+ 'pageType': 'points', 'chart': None, 'v': 'V1',
97
+ 'rationale': '演示节奏偏 V1–V4 + claim;简单图禁全幅;motion=none',
98
+ }),
99
+ (('经营', '复盘', '分析', '调研'), {
100
+ 'pageType': 'exhibit', 'chart': 'bar', 'v': None,
101
+ 'rationale': '研究证据页 exhibit + bar',
102
+ }),
103
+ (('瀑布', 'waterfall', '增减拆解', '桥图'), {
104
+ 'pageType': 'bar', 'chart': 'waterfall', 'v': 'V1',
105
+ 'rationale': 'advanced:waterfall 意图命中(非默认池)',
106
+ }),
107
+ (('漏斗', 'funnel', '转化率'), {
108
+ 'pageType': 'bar', 'chart': 'funnel', 'v': 'V1',
109
+ 'rationale': 'advanced:funnel 意图命中',
110
+ }),
111
+ (('甘特', 'gantt', '排期', '里程碑计划'), {
112
+ 'pageType': 'bar', 'chart': 'gantt', 'v': None,
113
+ 'rationale': 'advanced:gantt 意图命中',
114
+ }),
115
+ (('桑基', 'sankey', '流向', '分流'), {
116
+ 'pageType': 'diagram', 'chart': 'sankey', 'v': None,
117
+ 'rationale': 'advanced/信息图:流向意图',
118
+ }),
119
+ (('长文', '密集', '材料多', '证据多', '详细', 'deep dive', '白皮书', '高容量'), {
120
+ 'pageType': 'points', 'chart': None, 'v': 'V1',
121
+ 'rationale': '高容量→多页序列(勿塞一页)',
122
+ 'sequence': 'high_volume',
123
+ }),
124
+ ]
125
+
126
+ # 高容量内容:推荐多页分解,而非一页塞爆
127
+ HIGH_VOLUME_SEQ = {
128
+ 'presentation': [
129
+ 'cover', 'agenda',
130
+ 'kpi', # claim
131
+ 'points', # evidence cards
132
+ 'cards', # secondary evidence
133
+ 'bar', # chart evidence
134
+ 'points', # detail list
135
+ 'table', # detail table if needed
136
+ 'quote', # breath
137
+ 'closing',
138
+ ],
139
+ 'research': [
140
+ 'cover', 'agenda', 'metrics', 'exhibit', 'twocol',
141
+ 'halftable', 'exhibit', 'threecol', 'table', 'flags', 'closing',
142
+ ],
143
+ 'architecture': [
144
+ 'cover', 'diagram', 'lane', 'diagram', 'points', 'points', 'closing',
145
+ ],
146
+ }
147
+
148
+
149
+ def _norm_mode(m: str) -> str:
150
+ return MODE_ALIAS.get((m or 'b').lower().strip(), 'research')
151
+
152
+
153
+ def _skel(page_type: str, v: str | None = None, mode: str | None = None,
154
+ allow_secondary: bool = False) -> str:
155
+ if page_type in ('cover', 'closing'):
156
+ return 'P1'
157
+ if page_type in ('agenda',):
158
+ return 'P2'
159
+ if v and v in V_MAP:
160
+ return V_MAP[v]
161
+ sk = PAGE_TO_PRESET.get(page_type, 'P4')
162
+ # Mode A: remap secondary skels to primary unless intent explicitly allows
163
+ if mode == 'presentation' and not allow_secondary and sk in set(SECONDARY_SKELS.get('presentation') or []):
164
+ remap = {
165
+ 'P7': 'P4', 'P8': 'P1', 'P9': 'P10', 'P11': 'P6', 'P12': 'P6',
166
+ }
167
+ sk = remap.get(sk, 'P4')
168
+ return sk
169
+
170
+
171
+ def _chart_ok(chart: str | None, n_cats: int, n_pts: int) -> str | None:
172
+ """Apply sizeByComplexity forbid list; return possibly replaced chart."""
173
+ if not chart:
174
+ return None
175
+ for band in SIZE_BANDS:
176
+ if n_cats <= int(band.get('maxCats') or 99) and n_pts <= int(band.get('maxPts') or 99):
177
+ forbid = set(band.get('forbid') or [])
178
+ if chart in forbid:
179
+ prefer = list(band.get('prefer') or ['kpi'])
180
+ return None if prefer[0] in ('kpi', 'progress', 'vsbar') else prefer[0]
181
+ return chart
182
+ return chart if chart in DEFAULT_CHARTS else DEFAULT_CHARTS[0]
183
+
184
+
185
+ def _complexity(sec: dict) -> tuple[int, int]:
186
+ ch = sec.get('chart') if isinstance(sec.get('chart'), dict) else {}
187
+ vals = ch.get('values') or ch.get('data') or []
188
+ labels = ch.get('labels') or []
189
+ pts = sec.get('points') or sec.get('items') or []
190
+ n_pts = len(vals) if vals else len(pts)
191
+ n_cats = len(labels) if labels else (len(vals) if vals else (1 if pts else 0))
192
+ return max(n_cats, 0), max(n_pts, 0)
193
+
194
+
195
+ def _skew(sec: dict) -> bool:
196
+ ch = sec.get('chart') if isinstance(sec.get('chart'), dict) else {}
197
+ vals = [float(v) for v in (ch.get('values') or []) if isinstance(v, (int, float))]
198
+ vals = [v for v in vals if v >= 0]
199
+ if len(vals) < 2:
200
+ return False
201
+ total = sum(vals) or 1.0
202
+ pcts = [v / total * 100 for v in vals]
203
+ mn, mx = min(pcts), max(pcts)
204
+ return mn < 5.0 or (mn > 0 and mx / mn > 20)
205
+
206
+
207
+ def _match_intent(text: str) -> dict | None:
208
+ for keys, hint in INTENT_RULES:
209
+ if any(k in text for k in keys):
210
+ return dict(hint)
211
+ return None
212
+
213
+
214
+ def recommend(
215
+ mode: str,
216
+ intents: list[str] | None = None,
217
+ pages: int | None = None,
218
+ model: dict | None = None,
219
+ ) -> list[dict]:
220
+ mode = _norm_mode(mode)
221
+ intents = intents or []
222
+ blob = ' '.join(intents)
223
+
224
+ if model and isinstance(model.get('sections'), list) and model['sections']:
225
+ out = []
226
+ for i, sec in enumerate(model['sections'], 1):
227
+ if not isinstance(sec, dict):
228
+ continue
229
+ pt = str(sec.get('type') or 'points')
230
+ n_cats, n_pts = _complexity(sec)
231
+ ch = None
232
+ if isinstance(sec.get('chart'), dict):
233
+ ch = sec['chart'].get('type')
234
+ elif pt in DEFAULT_CHARTS:
235
+ ch = pt if pt != 'bar' else 'bar'
236
+ rationale = 'from-model'
237
+ v = None
238
+ if _skew(sec) and (ch in ('donut', 'pie', 'multidonut') or pt == 'donut'):
239
+ pt, ch, v = 'kpi', None, 'V3'
240
+ rationale = 'model 极偏占比 → V3 KPI(禁 donut)'
241
+ else:
242
+ ch = _chart_ok(ch, n_cats or 3, n_pts or 3)
243
+ if mode == 'presentation':
244
+ if n_cats <= 2 and n_pts <= 3:
245
+ v = 'V3'
246
+ rationale = '演示·简单数据 → V3 大数,禁全幅简单图'
247
+ elif n_pts >= 8:
248
+ v = 'V2'
249
+ rationale = '演示·复杂序列 → V2 上图下带'
250
+ else:
251
+ v = 'V1'
252
+ rationale = '演示·默认 V1 主视觉+右注解'
253
+ if blob:
254
+ hit = _match_intent(blob)
255
+ if hit and i == min(3, len(model['sections'])):
256
+ pt = hit.get('pageType', pt)
257
+ ch = hit.get('chart', ch)
258
+ v = hit.get('v', v)
259
+ rationale = hit.get('rationale', rationale)
260
+ allow_sec = bool(ch and ch in ADVANCED_CHARTS) or pt in ('diagram', 'lane', 'matrix', 'heatmap')
261
+ skel = sec.get('layoutPreset') or _skel(pt, v, mode, allow_secondary=allow_sec)
262
+ row = {'page': i, 'pageType': pt, 'skel': skel, 'chart': ch,
263
+ 'rationale': rationale}
264
+ if v:
265
+ row['v'] = v
266
+ out.append(row)
267
+ return out
268
+
269
+ intent_hit = _match_intent(blob) if blob else None
270
+ high_vol = bool(intent_hit and intent_hit.get('sequence') == 'high_volume')
271
+ # 页数偏多或意图含高容量 → 多页序列(议程→主张→证据卡→明细),禁一页塞爆
272
+ if high_vol or (pages and pages >= 10) or any(
273
+ k in blob for k in ('长文', '密集', '材料多', '证据多', '详细', '白皮书')):
274
+ seq = list(HIGH_VOLUME_SEQ.get(mode, HIGH_VOLUME_SEQ['presentation']))
275
+ high_vol = True
276
+ else:
277
+ seq = list(DEFAULT_SEQ.get(mode, DEFAULT_SEQ['research']))
278
+ if pages and pages > 0:
279
+ mid = seq[1:-1] if len(seq) > 2 else seq
280
+ if pages <= 2:
281
+ seq = seq[:1] + seq[-1:]
282
+ else:
283
+ need = pages - 2
284
+ body = (mid * ((need // max(len(mid), 1)) + 1))[:need]
285
+ seq = [seq[0]] + body + [seq[-1]]
286
+
287
+ out = []
288
+ for i, pt in enumerate(seq, 1):
289
+ # 默认只从核图 8 轮换;advanced 不进默认节奏
290
+ ch = pt if pt in DEFAULT_CHARTS else (DEFAULT_CHARTS[(i - 1) % len(DEFAULT_CHARTS)]
291
+ if pt in ('bar', 'exhibit', 'donut') else None)
292
+ if ch in ADVANCED_CHARTS:
293
+ ch = DEFAULT_CHARTS[(i - 1) % len(DEFAULT_CHARTS)]
294
+ if pt == 'bar':
295
+ ch = 'bar'
296
+ if pt == 'donut':
297
+ ch = 'donut'
298
+ if pt == 'exhibit':
299
+ ch = 'bar'
300
+ if pt == 'comparison':
301
+ ch = 'hbar' # 对比默认 hbar,避免 donut 占默认序列
302
+ # Mode A:内容页默认 V1;kpi→V3;复杂序列由 intent/from-model 升 V2
303
+ if mode == 'presentation' and pt not in ('cover', 'agenda', 'closing', 'quote'):
304
+ v = 'V3' if pt == 'kpi' else 'V1'
305
+ else:
306
+ v = None
307
+ rationale = f'default {mode} rhythm'
308
+ allow_sec = bool(ch and ch in ADVANCED_CHARTS) or pt in ('diagram', 'lane', 'matrix', 'heatmap')
309
+ skel = _skel(pt, v, mode, allow_secondary=allow_sec)
310
+ row = {'page': i, 'pageType': pt, 'skel': skel, 'chart': ch, 'rationale': rationale}
311
+ if v:
312
+ row['v'] = v
313
+ out.append(row)
314
+
315
+ if intent_hit and out and intent_hit.get('sequence') != 'high_volume':
316
+ idx = 2 if len(out) > 2 else 0
317
+ if out[idx]['pageType'] in ('cover', 'agenda'):
318
+ idx = min(idx + 1, len(out) - 1)
319
+ out[idx]['pageType'] = intent_hit.get('pageType', out[idx]['pageType'])
320
+ out[idx]['chart'] = intent_hit.get('chart')
321
+ if intent_hit.get('v'):
322
+ out[idx]['v'] = intent_hit['v']
323
+ ch_i = out[idx].get('chart')
324
+ allow_sec = bool(ch_i and ch_i in ADVANCED_CHARTS) or out[idx]['pageType'] in ('diagram', 'lane', 'matrix', 'heatmap')
325
+ out[idx]['skel'] = _skel(out[idx]['pageType'], out[idx].get('v'), mode, allow_secondary=allow_sec)
326
+ out[idx]['rationale'] = intent_hit.get('rationale', '')
327
+ if high_vol:
328
+ for r in out:
329
+ if r['pageType'] in ('cover', 'closing', 'agenda'):
330
+ continue
331
+ r['rationale'] = (r.get('rationale') or '') + (
332
+ ' · 高容量多页分解(禁一页塞爆/截断)' if '高容量' not in (r.get('rationale') or '') else '')
333
+ return out
334
+
335
+
336
+ def main() -> int:
337
+ ap = argparse.ArgumentParser(description='Recommend pageType/skel/chart sequence')
338
+ ap.add_argument('--mode', default=None, help='A|B|C or presentation|research|architecture')
339
+ ap.add_argument('--intent', action='append', default=[], help='repeatable intent phrases')
340
+ ap.add_argument('--pages', type=int, default=None)
341
+ ap.add_argument('--from-model', default=None, help='path to REPORT_MODEL JSON')
342
+ ap.add_argument('--stdin', action='store_true', help='read JSON {mode,intents,pages,model} from stdin')
343
+ ap.add_argument('--json', action='store_true', help='machine-readable (default when --stdin/--from-model)')
344
+ args = ap.parse_args()
345
+
346
+ model = None
347
+ intents = list(args.intent or [])
348
+ mode = args.mode
349
+ pages = args.pages
350
+
351
+ if args.stdin:
352
+ raw = sys.stdin.read()
353
+ data = json.loads(raw) if raw.strip() else {}
354
+ mode = mode or data.get('mode')
355
+ intents = intents or list(data.get('intents') or [])
356
+ pages = pages if pages is not None else data.get('pages')
357
+ model = data.get('model') or data if 'sections' in data else None
358
+
359
+ if args.from_model:
360
+ model = json.loads(Path(args.from_model).read_text(encoding='utf-8'))
361
+ mode = mode or model.get('mode')
362
+
363
+ if not mode:
364
+ ap.error('--mode is required (or provide via --from-model/--stdin)')
365
+
366
+ rows = recommend(mode, intents, pages, model)
367
+ as_json = args.json or args.stdin or bool(args.from_model)
368
+ payload = {'mode': _norm_mode(mode), 'pages': rows}
369
+ if as_json:
370
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
371
+ else:
372
+ print(f"# recommend_layout · mode={payload['mode']}"
373
+ + (f" · intents={intents!r}" if intents else '')
374
+ + (f" · pages={pages}" if pages else ''))
375
+ print(f"{'#':>3} {'pageType':<14} {'skel':<6} {'chart':<12} {'v':<4} rationale")
376
+ for r in rows:
377
+ print(f"{r['page']:>3} {r['pageType']:<14} {r['skel']:<6} "
378
+ f"{str(r.get('chart') or '—'):<12} {str(r.get('v') or '—'):<4} "
379
+ f"{r.get('rationale') or ''}")
380
+ return 0
381
+
382
+
383
+ if __name__ == '__main__':
384
+ sys.exit(main())