@topmindspace/tms-skills 0.1.13 → 0.1.15

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 +34 -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
@@ -77,8 +77,7 @@ def sowhat(sec: dict) -> str:
77
77
  t = sec.get('soWhat')
78
78
  if not t:
79
79
  return ''
80
- return (f' <div class="sowhat rv"><span class="sowhat__k">So what</span>'
81
- f'<span class="sowhat__v">{cite(t)}</span></div>\n')
80
+ return (f' <div class="sowhat rv"><span class="sowhat__v">{cite(t)}</span></div>\n')
82
81
 
83
82
 
84
83
  def footnote(sec: dict) -> str:
@@ -326,8 +325,7 @@ def r_comparison(i, sec):
326
325
  + panel(sec.get('left')) + '\n' + panel(sec.get('right'), True) + '\n'
327
326
  + ' </div>\n')
328
327
  if sec.get('verdict'):
329
- body += (f' <div class="sowhat rv"><span class="sowhat__k">结论</span>'
330
- f'<span class="sowhat__v">{cite(sec["verdict"])}</span></div>\n')
328
+ body += (f' <div class="sowhat rv"><span class="sowhat__v">{cite(sec["verdict"])}</span></div>\n')
331
329
  sec = dict(sec)
332
330
  sec.pop('verdict', None)
333
331
  return wrap(i, 'comparison', body, sec)
@@ -282,8 +282,7 @@ def pg_chart(i, sec):
282
282
  </div>
283
283
  {SIDE_NOTES}
284
284
  </div>
285
- <div class="sowhat rv"><span class="sowhat__k">So what</span>
286
- <span class="sowhat__v">一行含义(≤60 字)。</span></div>
285
+ <div class="sowhat rv"><span class="sowhat__v">一行含义(≤60 字)。</span></div>
287
286
  </div>
288
287
  </section>'''
289
288
  return html, {'type': 'bar', 'chart': {'type': ct, 'labels': ['A', 'B', 'C'], 'values': [42, 61, 35],
@@ -303,8 +302,7 @@ def pg_donut(i, sec):
303
302
  </div>
304
303
  {SIDE_NOTES}
305
304
  </div>
306
- <div class="sowhat rv"><span class="sowhat__k">So what</span>
307
- <span class="sowhat__v">一行含义(≤60 字)。</span></div>
305
+ <div class="sowhat rv"><span class="sowhat__v">一行含义(≤60 字)。</span></div>
308
306
  </div>
309
307
  </section>'''
310
308
  return html, {'type': 'donut', 'chart': {'labels': ['A', 'B', 'C'], 'values': [45, 35, 20],
@@ -327,7 +325,6 @@ def pg_exhibit(i, sec):
327
325
  {SIDE_NOTES}
328
326
  </div>
329
327
  <div class="sowhat rv">
330
- <span class="sowhat__k">So what</span>
331
328
  <span class="sowhat__v">一行含义或建议(≤60 字)。</span>
332
329
  </div>
333
330
  </div>
@@ -556,8 +553,7 @@ def pg_comparison(i, sec):
556
553
  <h3 class="t-h3" style="color:var(--accent)">目标</h3>
557
554
  <ul class="ul"><li><strong>要点。</strong>一句话。</li><li><strong>要点。</strong>一句话。</li></ul></div>
558
555
  </div>
559
- <div class="sowhat rv"><span class="sowhat__k">结论</span>
560
- <span class="sowhat__v">一行结论(≤60 字)。</span></div>
556
+ <div class="sowhat rv"><span class="sowhat__v">一行结论(≤60 字)。</span></div>
561
557
  </div>
562
558
  </section>'''
563
559
  return html, {'type': 'comparison',
@@ -721,8 +717,7 @@ def pg_info(i, sec):
721
717
  <text x="{w // 2}" y="{h // 2}" text-anchor="middle" class="f-txt3" font-size="13">{t} 信息图占位</text>
722
718
  </svg>
723
719
  </div>
724
- {src} <div class="sowhat rv"><span class="sowhat__k">So what</span>
725
- <span class="sowhat__v">一行含义(≤60 字)。</span></div>
720
+ {src} <div class="sowhat rv"><span class="sowhat__v">一行含义(≤60 字)。</span></div>
726
721
  </div>
727
722
  </section>'''
728
723
  model = {'type': t}
@@ -760,7 +755,6 @@ BUILDERS = {
760
755
 
761
756
 
762
757
  SOWHAT_BLOCK = ''' <div class="sowhat rv">
763
- <span class="sowhat__k">So what</span>
764
758
  <span class="sowhat__v">这一页的含义或下一步建议(≤60 字)。</span>
765
759
  </div>
766
760
  '''
@@ -225,7 +225,7 @@ def test_annotation_band_reports_all_invaders() -> None:
225
225
  els = [
226
226
  sp(5.0, 1.2, 4.0, "PlotSeriesA"),
227
227
  sp(5.2, 1.1, 4.0, "PlotSeriesB"),
228
- sp(6.05, 0.55, 12.0, "SO WHAT locked path"),
228
+ sp(6.05, 0.55, 12.0, "Platform locked path takeaway"),
229
229
  ]
230
230
  issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
231
231
  codes = [i["code"] for i in issues]
@@ -252,7 +252,7 @@ def test_annotation_band_so_what_band_top() -> None:
252
252
  els = [
253
253
  # Body ends at 6.30 — inside (soWhatY=6.05, contentBottomWithNote=6.40]
254
254
  sp(5.0, 1.3, 4.0, "LegendCrushMid"),
255
- sp(6.05, 0.55, 12.0, "SO WHAT platform first"),
255
+ sp(6.05, 0.55, 12.0, "Platform first takeaway text"),
256
256
  ]
257
257
  issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
258
258
  codes = [i["code"] for i in issues]
@@ -439,7 +439,7 @@ def annotation_band_overlap_check(
439
439
  def _is_annotation_self(y: int, cy: int, cx: int, text: str) -> bool:
440
440
  """so-what / footnote / source row living in the annotation zone."""
441
441
  if _at_so_what_slot(y) and cy <= int(0.85 * 914400):
442
- # Fill rect, accent strip, or SO WHAT text — all live in the slot
442
+ # Fill rect, accent strip, or 结论条正文 — all live in the slot
443
443
  if ann_pat.search(text) or _is_bar_like(cy, cx) or cx <= int(0.15 * 914400):
444
444
  return True
445
445
  if _is_note_like(y, cy, text):
@@ -464,7 +464,7 @@ def annotation_band_overlap_check(
464
464
  note_like = _is_note_like(y, cy, txt)
465
465
  labeled = bool(ann_pat.search(txt))
466
466
  bar = _is_bar_like(cy, cx)
467
- # Fill rect sits on soWhatY (±0.02); labeled SO WHAT text may sit slightly below
467
+ # Fill rect sits on soWhatY (±0.02); 结论条正文 may sit slightly below
468
468
  on_fill = abs(y - so_what_y) <= int(0.02 * 914400) and bar
469
469
  labeled_slot = labeled and _at_so_what_slot(y) and cy <= int(0.85 * 914400)
470
470
  if note_like:
@@ -503,7 +503,7 @@ def annotation_band_overlap_check(
503
503
  "ANNOTATION_BAND_OVERLAP",
504
504
  f"主内容侵入注释带(元素底边 {bottom/914400:.2f}in 越过注释带顶 "
505
505
  f"{band_top_in:.2f}in,重叠 {overlap/914400:.2f}in)——"
506
- "图例/系列请收入主图区或压缩系列数,禁止压进 so-what/来源行。",
506
+ "图例/系列请收入主图区或压缩系列数,禁止压进结论条/来源行。",
507
507
  slide=slide_no,
508
508
  ))
509
509
  return out
@@ -619,9 +619,10 @@ def _infer_container_pad(shape: ET.Element, pad_map: dict[str, float], min_pad:
619
619
  return float(pad_map[kind]), kind
620
620
  if kind:
621
621
  return min_pad, kind
622
- # 启发式兜底:仅对明确的 so-what / 待核实文本用对应 pad(其余 minPad)
622
+ # 启发式兜底:仅对明确的结论条 / 待核实文本用对应 pad(其余 minPad)
623
623
  text = text_content(shape)
624
- if "SO WHAT" in text:
624
+ # Legacy "SO WHAT" label OR tr:soWhat already handled above; keep SO WHAT for old files
625
+ if "SO WHAT" in text or "结论条" in text:
625
626
  return float(pad_map.get("soWhat") or min_pad), "soWhat"
626
627
  if "待核实" in text:
627
628
  return float(pad_map.get("band") or min_pad), "band"
@@ -995,12 +996,27 @@ def inspect_slide(
995
996
  issue("LOW_TEXT_DENSITY", "多个文本框但总字符 <25;信息密度不足。", slide=slide_number)
996
997
  )
997
998
 
998
- if re.search(r"EXHIBIT\s*\d", combined_text, re.IGNORECASE) and "SO WHAT" not in combined_text:
999
- warnings.append(
1000
- issue("EXHIBIT_PAGE_MISSING_SOWHAT",
1001
- "页面带 Exhibit 编号但没有 'SO WHAT' 结论条(research R2 版式)。",
1002
- slide=slide_number)
1003
- )
999
+ if re.search(r"EXHIBIT\s*\d", combined_text, re.IGNORECASE):
1000
+ has_conclusion_bar = False
1001
+ for el in all_elements:
1002
+ oname = _shape_object_name(el)
1003
+ if "soWhat" in (oname or ""):
1004
+ has_conclusion_bar = True
1005
+ break
1006
+ box = shape_bounds(el)
1007
+ if box is None:
1008
+ continue
1009
+ _x, y, cx, cy = box
1010
+ # Wide fill at soWhatY ≈ 6.05in (MD3 结论条衬底)
1011
+ if abs(y / 914400 - 6.05) <= 0.08 and cx / 914400 >= 6.0 and cy / 914400 <= 0.85:
1012
+ has_conclusion_bar = True
1013
+ break
1014
+ if not has_conclusion_bar and "SO WHAT" not in combined_text:
1015
+ warnings.append(
1016
+ issue("EXHIBIT_PAGE_MISSING_SOWHAT",
1017
+ "页面带 Exhibit 编号但没有结论条(research R2 版式 · MD3 衬条)。",
1018
+ slide=slide_number)
1019
+ )
1004
1020
 
1005
1021
  # v9:HTML 标签源码泄漏进 PPTX 文本(模型字段未净化时原样露出)
1006
1022
  tag_leaks = re.findall(
@@ -23,7 +23,7 @@ research / architecture 仍须显式传 --layout-qa(不强制)。
23
23
  强调带约束(band--deep 反相页 ≤ 上限且不作末页) / 待核实标注(.tbd ↔ .tbd-legend/.flagbar) /
24
24
  素材图片与配图占位(零外链 + alt + 内联体积 + 版式/比例锁定类对应 + 占位可见标签 + 模型对应) /
25
25
  PPTX 预览配套(预览按钮 + 模型 + 运行时,页面无导出按钮) /
26
- 内容级质量(版式节奏连用上限 / so-what 实质与空洞套话 / research 标题含数字或判断词)
26
+ 内容级质量(版式节奏连用上限 / 结论条实质与空洞套话 / research 标题含数字或判断词)
27
27
  """
28
28
  import sys
29
29
  import re
@@ -97,7 +97,7 @@ IMAGE_MAX_PER_PAGE = int(IMAGE_SPEC.get('maxPerPage') or 6)
97
97
  IMAGE_MAX_INLINE = int(IMAGE_SPEC.get('maxInlineBytes') or 1572864)
98
98
  IMAGE_MAX_TOTAL = int(IMAGE_SPEC.get('maxTotalInlineBytes') or 8388608)
99
99
 
100
- AGENDA_SINGLE_MAX = 8 # 超过则建议 agenda--2col
100
+ AGENDA_SINGLE_MAX = int((((LC.get("contentQuality") or {}).get("agenda") or {}).get("singleMax")) or 8) # 超过则须 agenda--2col
101
101
  # 页高估算参数(单源 layout-constants.pageHeightEstimate)
102
102
  _HE = LC.get('pageHeightEstimate') or {}
103
103
  SCREEN_BUDGET_PX = int(_HE.get('screenBudgetPx') or 1000)
@@ -356,7 +356,7 @@ def _check_bands(txt, chk, mode="presentation"):
356
356
 
357
357
 
358
358
  def _check_icons(txt, chk):
359
- """图标使用:长报告不应全文无图标(要点卡/关键列表至少一处)。"""
359
+ """图标使用:长报告不应全文无图标;含 ≥2 张卡的内容页应有 .card__ico(WARN)。"""
360
360
  if len(_bands(txt)) < 6:
361
361
  return
362
362
  has_icon = ('ul--ico' in txt or 'metric__ico' in txt or
@@ -364,6 +364,24 @@ def _check_icons(txt, chk):
364
364
  re.search(r'card__ico">\s*<svg', txt))
365
365
  chk("长报告(≥6页)要点卡/关键列表有图标", bool(has_icon),
366
366
  "全文未用图标(见 icons.md 使用准则)", level="WARN")
367
+ # Soft gate: points/cards pages with ≥2 .card and zero .card__ico
368
+ bare_card_pages = 0
369
+ for m in re.finditer(r'<section\b([^>]*)>([\s\S]*?)</section>', txt):
370
+ attrs, body = m.group(1), m.group(2)
371
+ pt = re.search(r'data-page-type="([^"]+)"', attrs)
372
+ page_type = (pt.group(1) if pt else '') or ''
373
+ if page_type not in ('points', 'cards', 'split', 'comparison'):
374
+ # Also catch sections that clearly use card grids
375
+ if body.count('class="card') + body.count("class='card") < 2:
376
+ continue
377
+ n_cards = len(re.findall(r'class="[^"]*\bcard\b', body))
378
+ if n_cards < 2:
379
+ continue
380
+ if 'card__ico' not in body:
381
+ bare_card_pages += 1
382
+ chk("要点/卡片页(≥2 卡)含 .card__ico", bare_card_pages == 0,
383
+ f"{bare_card_pages} 页有多卡但无图标(见 icons.md 四正当位置)" if bare_card_pages else "",
384
+ level="WARN")
367
385
 
368
386
 
369
387
  def _check_table_rows(txt, chk, mode="presentation"):
@@ -763,7 +781,7 @@ def _check_content_quality(txt, chk, mode, model):
763
781
  if any(f in body for f in forbidden):
764
782
  hollow.append(f'{src}:{body[:24]}')
765
783
  if texts:
766
- chk(f"so-what 实质(长度 ≥{min_chars} 字且无空洞套话)", not short and not hollow,
784
+ chk(f"结论条实质(长度 ≥{min_chars} 字且无空洞套话)", not short and not hollow,
767
785
  "; ".join((short + hollow)[:4]) if (short or hollow) else "", level="WARN")
768
786
 
769
787
  # ③ research 行动标题须含数字或判断词
@@ -1651,8 +1669,12 @@ def main():
1651
1669
  n_items = txt.count('class="agenda__a"')
1652
1670
  chk("Agenda 条目可点击跳转 (≥4)", n_items >= 4, f"{n_items} 条")
1653
1671
  if n_items > AGENDA_SINGLE_MAX:
1672
+ # 只认大纲元素 class(禁被 engine.css 的 .agenda--2col 选择器误伤)
1673
+ has_2col = bool(re.search(
1674
+ r'<ol\b[^>]*class="[^"]*\bagenda--2col\b', txt)) or bool(re.search(
1675
+ r'<ol\b[^>]*class="[^"]*\bagenda\b[^"]*\bagenda--2col\b', txt))
1654
1676
  chk(f"Agenda >{AGENDA_SINGLE_MAX} 条时已用 agenda--2col 双列",
1655
- 'agenda--2col' in txt, f"{n_items} 条未双列", level="WARN")
1677
+ has_2col, f"{n_items} 条未双列(大纲很多→两排/两列,禁单列撑爆一屏)", level="WARN")
1656
1678
 
1657
1679
  # ── 主标题粗体 ──
1658
1680
  chk("主标题粗体 --fw-title/--fw-display",