@topmindspace/tms-skills 2.0.0 → 2.0.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 (99) hide show
  1. package/CHANGELOG.md +139 -123
  2. package/README.md +126 -103
  3. package/bin/tms-skills.js +160 -160
  4. package/package.json +48 -48
  5. package/top-ppt-html/README.md +254 -253
  6. package/top-ppt-html/SKILL.md +111 -111
  7. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3873 -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-architecture-spectrum.html +3873 -3926
  10. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +167 -167
  11. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4272 -4325
  12. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +320 -320
  13. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4272 -4325
  14. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +320 -320
  15. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4272 -4325
  16. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
  17. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5474 -5527
  18. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +913 -913
  19. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5474 -5527
  20. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +913 -913
  21. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5474 -5527
  22. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
  23. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5474 -5527
  24. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +913 -913
  25. package/top-ppt-html/assets/pptx-export.js +1944 -1944
  26. package/top-ppt-html/assets/style-gallery.html +559 -589
  27. package/top-ppt-html/assets/templates/architecture.html +3675 -3728
  28. package/top-ppt-html/assets/templates/engine.css +787 -840
  29. package/top-ppt-html/assets/templates/presentation.html +3685 -3738
  30. package/top-ppt-html/assets/templates/research.html +3964 -4017
  31. package/top-ppt-html/assets/templates/ui.js +520 -520
  32. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  33. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  34. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  35. package/top-ppt-html/assets/theme-overview.png +0 -0
  36. package/top-ppt-html/evals/prompts.csv +15 -15
  37. package/top-ppt-html/evals/rubric.schema.json +25 -25
  38. package/top-ppt-html/evals/run_evals.py +220 -220
  39. package/top-ppt-html/evals/trace.example.json +16 -16
  40. package/top-ppt-html/package-lock.json +175 -0
  41. package/top-ppt-html/package.json +30 -35
  42. package/top-ppt-html/references/charts-basic.md +624 -624
  43. package/top-ppt-html/references/charts-discipline.md +108 -108
  44. package/top-ppt-html/references/charts-extended.md +482 -482
  45. package/top-ppt-html/references/charts.md +28 -28
  46. package/top-ppt-html/references/components-atoms.md +624 -624
  47. package/top-ppt-html/references/components.md +30 -30
  48. package/top-ppt-html/references/content-rules.md +490 -490
  49. package/top-ppt-html/references/design-system-engine.md +235 -235
  50. package/top-ppt-html/references/design-system.md +471 -478
  51. package/top-ppt-html/references/failure-modes.md +214 -214
  52. package/top-ppt-html/references/high-fidelity.md +127 -127
  53. package/top-ppt-html/references/icons.md +397 -397
  54. package/top-ppt-html/references/industry-benchmark.md +105 -105
  55. package/top-ppt-html/references/infographics-stats.md +308 -308
  56. package/top-ppt-html/references/infographics-structure.md +226 -226
  57. package/top-ppt-html/references/infographics.md +43 -43
  58. package/top-ppt-html/references/layout-grammar.md +315 -315
  59. package/top-ppt-html/references/layouts-architecture.md +108 -108
  60. package/top-ppt-html/references/layouts-combo.md +600 -600
  61. package/top-ppt-html/references/layouts-research.md +160 -160
  62. package/top-ppt-html/references/modes.md +254 -254
  63. package/top-ppt-html/references/outline-design.md +275 -275
  64. package/top-ppt-html/references/playbook.md +266 -266
  65. package/top-ppt-html/references/pptx-export.md +209 -209
  66. package/top-ppt-html/references/reform-plan.md +252 -252
  67. package/top-ppt-html/references/styles.md +336 -370
  68. package/top-ppt-html/references/tech-design.md +138 -138
  69. package/top-ppt-html/scripts/audit_css.py +109 -109
  70. package/top-ppt-html/scripts/audit_docs.py +176 -176
  71. package/top-ppt-html/scripts/audit_skill.py +220 -220
  72. package/top-ppt-html/scripts/audit_styles.py +293 -351
  73. package/top-ppt-html/scripts/build_examples.py +2276 -2276
  74. package/top-ppt-html/scripts/build_pptx.js +2380 -2380
  75. package/top-ppt-html/scripts/capture_theme_overview.js +78 -78
  76. package/top-ppt-html/scripts/checks_html.py +127 -127
  77. package/top-ppt-html/scripts/cross_verify.py +294 -294
  78. package/top-ppt-html/scripts/env_probe.py +158 -158
  79. package/top-ppt-html/scripts/extract_model.py +210 -210
  80. package/top-ppt-html/scripts/extract_snippet.py +374 -374
  81. package/top-ppt-html/scripts/gen_channel_a.js +214 -214
  82. package/top-ppt-html/scripts/layout-constants.json +3309 -3377
  83. package/top-ppt-html/scripts/layout_slots.json +830 -830
  84. package/top-ppt-html/scripts/lib_layout_regions.js +412 -412
  85. package/top-ppt-html/scripts/measure_height.py +178 -178
  86. package/top-ppt-html/scripts/model-schema.json +547 -547
  87. package/top-ppt-html/scripts/negative_tests.py +307 -307
  88. package/top-ppt-html/scripts/package_skill.py +291 -291
  89. package/top-ppt-html/scripts/prepare_images.py +341 -341
  90. package/top-ppt-html/scripts/probe_image_export.py +188 -188
  91. package/top-ppt-html/scripts/quality_gate.py +301 -301
  92. package/top-ppt-html/scripts/regression.py +307 -308
  93. package/top-ppt-html/scripts/render_compare.py +275 -275
  94. package/top-ppt-html/scripts/render_from_model.py +698 -698
  95. package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
  96. package/top-ppt-html/scripts/section-file-map.json +104 -104
  97. package/top-ppt-html/scripts/sync_runtime.py +662 -662
  98. package/top-ppt-html/scripts/validate_pptx.py +1510 -1510
  99. package/top-ppt-html/scripts/validate_report.py +1456 -1456
@@ -1,78 +1,78 @@
1
- #!/usr/bin/env node
2
- /* TopPPT HTML · 主题参考图生成器(维护工具,非交付依赖、非交付硬门禁)
3
- *
4
- * 用法:
5
- * npm install playwright # 或在任意位置安装 playwright 后设置 NODE_PATH
6
- * npx playwright install chromium # 首次需要下载浏览器
7
- * node scripts/capture_theme_overview.js [--channel=msedge|chrome|chromium] [--scale=1.25]
8
- *
9
- * --scale = deviceScaleFactor(默认 1.25)。参考图只用于"让用户看见风格与主题",
10
- * 1.25 在 1480px 视口下已足够清晰,同时把单张 PNG 控制在 ~1MB 内(便于在 IDE 里快速打开)。
11
- *
12
- * 浏览器通道自动探测:优先命令行 --channel=,其次环境变量 TOP_PPT_BROWSER_CHANNEL,
13
- * 最后回落到 Playwright 自带的 chromium(无本机浏览器依赖)。
14
- *
15
- * 产物(assets/,覆盖旧图):
16
- * theme-overview.png —— 默认(演示 Tab)整体参考图
17
- * theme-overview-presentation.png —— 演示模式 Tab
18
- * theme-overview-research.png —— 研究模式 Tab(咨询密排形态)
19
- * theme-overview-architecture.png —— 架构模式 Tab(图为王形态)
20
- *
21
- * 每次截图前断言当前 Tab 文案,防止 Tab 切换未生效导致产出雷同图。 */
22
- const path = require('path');
23
- let chromium;
24
- try {
25
- ({ chromium } = require('playwright'));
26
- } catch (e) {
27
- console.error('未找到 playwright。请先安装:npm install playwright && npx playwright install chromium');
28
- console.error('(本脚本仅用于刷新 assets/theme-overview*.png 参考图,非技能交付依赖。)');
29
- process.exit(2);
30
- }
31
-
32
- const TAB_TEXT = { presentation: '演示汇报', research: '研究报告', architecture: '信息架构图' };
33
- const channelArg = (process.argv.find((a) => a.startsWith('--channel=')) || '').split('=')[1];
34
- const scaleArg = parseFloat((process.argv.find((a) => a.startsWith('--scale=')) || '').split('=')[1]);
35
- const SCALE = Number.isFinite(scaleArg) && scaleArg > 0 ? scaleArg : 1.25;
36
- const CHANNEL = channelArg || process.env.TOP_PPT_BROWSER_CHANNEL
37
- || process.env.TOP_PPT_BROWSER_CHANNEL || '';
38
-
39
- (async () => {
40
- const gallery = 'file:///' + path.resolve(__dirname, '../assets/style-gallery.html').replace(/\\/g, '/');
41
- const outDir = path.resolve(__dirname, '../assets');
42
- /* channel 为空 → 用 Playwright 自带 chromium(不依赖本机已装浏览器) */
43
- const browser = await chromium.launch(CHANNEL ? { channel: CHANNEL } : {});
44
- const page = await browser.newPage({ viewport: { width: 1480, height: 1200 }, deviceScaleFactor: SCALE });
45
- await page.goto(gallery);
46
- await page.waitForSelector('.preset');
47
-
48
- const modes = ['presentation', 'research', 'architecture'];
49
- for (const m of modes) {
50
- const idx = modes.indexOf(m);
51
- await page.evaluate((i) => document.querySelectorAll('.mtab')[i].click(), idx);
52
- await page.waitForTimeout(500);
53
- const on = await page.evaluate(() =>
54
- (document.querySelector('.mtab.on .mtab__t') || {}).textContent || '');
55
- if (on !== TAB_TEXT[m]) {
56
- throw new Error(`Tab 切换断言失败:期望「${TAB_TEXT[m]}」实际「${on}」(防止雷同图回归)`);
57
- }
58
- /* 卡片数必须等于单源风格数(PRESETS 由 sync_runtime.py 从 layout-constants.json 注入),
59
- * 防止「扩了风格但画廊/参考图没跟上」的静默漂移(旧断言只查 n < 9,太弱)。 */
60
- const n = await page.evaluate(() => document.querySelectorAll('.preset').length);
61
- const expected = await page.evaluate(() =>
62
- (typeof PRESETS === 'object' && PRESETS) ? Object.keys(PRESETS).length : 0);
63
- if (expected && n !== expected) {
64
- throw new Error(`画廊卡片数 ${n} ≠ 单源风格数 ${expected}(画廊与 layout-constants.json 漂移)`);
65
- }
66
- if (n < 9) throw new Error(`卡片数异常:${n}`);
67
- await page.screenshot({ path: path.join(outDir, `theme-overview-${m}.png`), fullPage: true });
68
- console.log(`captured: theme-overview-${m}.png (tab=${on}, cards=${n})`);
69
- }
70
-
71
- /* 主图回到演示 Tab(默认态) */
72
- await page.evaluate(() => document.querySelectorAll('.mtab')[0].click());
73
- await page.waitForTimeout(500);
74
- await page.screenshot({ path: path.join(outDir, 'theme-overview.png'), fullPage: true });
75
- console.log('captured: theme-overview.png (default presentation tab)');
76
- await browser.close();
77
- console.log(`done: 4 张主题参考图已刷新(deviceScaleFactor=${SCALE})`);
78
- })().catch((e) => { console.error(e); process.exit(1); });
1
+ #!/usr/bin/env node
2
+ /* TopPPT HTML · 主题参考图生成器(维护工具,非交付依赖、非交付硬门禁)
3
+ *
4
+ * 用法:
5
+ * npm install playwright # 或在任意位置安装 playwright 后设置 NODE_PATH
6
+ * npx playwright install chromium # 首次需要下载浏览器
7
+ * node scripts/capture_theme_overview.js [--channel=msedge|chrome|chromium] [--scale=1.25]
8
+ *
9
+ * --scale = deviceScaleFactor(默认 1.25)。参考图只用于"让用户看见风格与主题",
10
+ * 1.25 在 1480px 视口下已足够清晰,同时把单张 PNG 控制在 ~1MB 内(便于在 IDE 里快速打开)。
11
+ *
12
+ * 浏览器通道自动探测:优先命令行 --channel=,其次环境变量 TOP_PPT_BROWSER_CHANNEL,
13
+ * 最后回落到 Playwright 自带的 chromium(无本机浏览器依赖)。
14
+ *
15
+ * 产物(assets/,覆盖旧图):
16
+ * theme-overview.png —— 默认(演示 Tab)整体参考图
17
+ * theme-overview-presentation.png —— 演示模式 Tab
18
+ * theme-overview-research.png —— 研究模式 Tab(咨询密排形态)
19
+ * theme-overview-architecture.png —— 架构模式 Tab(图为王形态)
20
+ *
21
+ * 每次截图前断言当前 Tab 文案,防止 Tab 切换未生效导致产出雷同图。 */
22
+ const path = require('path');
23
+ let chromium;
24
+ try {
25
+ ({ chromium } = require('playwright'));
26
+ } catch (e) {
27
+ console.error('未找到 playwright。请先安装:npm install playwright && npx playwright install chromium');
28
+ console.error('(本脚本仅用于刷新 assets/theme-overview*.png 参考图,非技能交付依赖。)');
29
+ process.exit(2);
30
+ }
31
+
32
+ const TAB_TEXT = { presentation: '演示汇报', research: '研究报告', architecture: '信息架构图' };
33
+ const channelArg = (process.argv.find((a) => a.startsWith('--channel=')) || '').split('=')[1];
34
+ const scaleArg = parseFloat((process.argv.find((a) => a.startsWith('--scale=')) || '').split('=')[1]);
35
+ const SCALE = Number.isFinite(scaleArg) && scaleArg > 0 ? scaleArg : 1.25;
36
+ const CHANNEL = channelArg || process.env.TOP_PPT_BROWSER_CHANNEL
37
+ || process.env.TOP_PPT_BROWSER_CHANNEL || '';
38
+
39
+ (async () => {
40
+ const gallery = 'file:///' + path.resolve(__dirname, '../assets/style-gallery.html').replace(/\\/g, '/');
41
+ const outDir = path.resolve(__dirname, '../assets');
42
+ /* channel 为空 → 用 Playwright 自带 chromium(不依赖本机已装浏览器) */
43
+ const browser = await chromium.launch(CHANNEL ? { channel: CHANNEL } : {});
44
+ const page = await browser.newPage({ viewport: { width: 1480, height: 1200 }, deviceScaleFactor: SCALE });
45
+ await page.goto(gallery);
46
+ await page.waitForSelector('.preset');
47
+
48
+ const modes = ['presentation', 'research', 'architecture'];
49
+ for (const m of modes) {
50
+ const idx = modes.indexOf(m);
51
+ await page.evaluate((i) => document.querySelectorAll('.mtab')[i].click(), idx);
52
+ await page.waitForTimeout(500);
53
+ const on = await page.evaluate(() =>
54
+ (document.querySelector('.mtab.on .mtab__t') || {}).textContent || '');
55
+ if (on !== TAB_TEXT[m]) {
56
+ throw new Error(`Tab 切换断言失败:期望「${TAB_TEXT[m]}」实际「${on}」(防止雷同图回归)`);
57
+ }
58
+ /* 卡片数必须等于单源风格数(PRESETS 由 sync_runtime.py 从 layout-constants.json 注入),
59
+ * 防止「扩了风格但画廊/参考图没跟上」的静默漂移(旧断言只查 n < 9,太弱)。 */
60
+ const n = await page.evaluate(() => document.querySelectorAll('.preset').length);
61
+ const expected = await page.evaluate(() =>
62
+ (typeof PRESETS === 'object' && PRESETS) ? Object.keys(PRESETS).length : 0);
63
+ if (expected && n !== expected) {
64
+ throw new Error(`画廊卡片数 ${n} ≠ 单源风格数 ${expected}(画廊与 layout-constants.json 漂移)`);
65
+ }
66
+ if (n < 9) throw new Error(`卡片数异常:${n}`);
67
+ await page.screenshot({ path: path.join(outDir, `theme-overview-${m}.png`), fullPage: true });
68
+ console.log(`captured: theme-overview-${m}.png (tab=${on}, cards=${n})`);
69
+ }
70
+
71
+ /* 主图回到演示 Tab(默认态) */
72
+ await page.evaluate(() => document.querySelectorAll('.mtab')[0].click());
73
+ await page.waitForTimeout(500);
74
+ await page.screenshot({ path: path.join(outDir, 'theme-overview.png'), fullPage: true });
75
+ console.log('captured: theme-overview.png (default presentation tab)');
76
+ await browser.close();
77
+ console.log(`done: 4 张主题参考图已刷新(deviceScaleFactor=${SCALE})`);
78
+ })().catch((e) => { console.error(e); process.exit(1); });
@@ -1,127 +1,127 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """TopPPT HTML · HTML 检查公共库(单源:承载正则 / 图表通道 / 多样性下限 / 组合版式)
4
-
5
- 消费方:validate_report.py · quality_gate.py · evals/run_evals.py
6
- 禁止在消费方再手抄 CARRIERS / 多样性公式 / 组合版式判定。
7
- 阈值仍读 layout-constants.json(charts.variety / charts.registry)。
8
- """
9
- from __future__ import annotations
10
-
11
- import json
12
- import re
13
- from pathlib import Path
14
-
15
- ROOT = Path(__file__).resolve().parent.parent
16
- _LC_PATH = ROOT / 'scripts' / 'layout-constants.json'
17
-
18
- # 承载类型正则(组合版式:一页是否含 ≥2 种承载)
19
- CARRIERS = [
20
- r'<svg\b[^>]*data-chart=',
21
- r'class="tbl-wrap',
22
- r'class="card[\s"]',
23
- r'class="metric[\s"]',
24
- r'class="media[\s"]',
25
- r'class="arch[\s"]',
26
- r'class="lane[\s"]',
27
- r'class="matrix[\s"]',
28
- r'class="heat[\s"]',
29
- r'class="bul[\s"]',
30
- r'class="pyr[\s"]',
31
- r'class="steps[\s"]',
32
- r'class="exhibit[\s"]',
33
- r'class="cols-2',
34
- r'class="cols-3',
35
- ]
36
-
37
- SKIP_BAND_IDS = ('agenda', 'cover', 'refs', 'appendix', 'next')
38
-
39
-
40
- def load_lc() -> dict:
41
- return json.loads(_LC_PATH.read_text(encoding='utf-8'))
42
-
43
-
44
- def chart_registry(lc: dict | None = None) -> dict:
45
- lc = lc or load_lc()
46
- return {k: v for k, v in ((lc.get('charts') or {}).get('registry') or {}).items()
47
- if isinstance(v, dict)}
48
-
49
-
50
- def chart_variety(lc: dict | None = None) -> dict:
51
- lc = lc or load_lc()
52
- return (lc.get('charts') or {}).get('variety') or {}
53
-
54
-
55
- def bands(html: str) -> list[str]:
56
- return re.split(r'(?=<section class="band)', html)[1:]
57
-
58
-
59
- def content_bands(html: str, skip_ids: tuple[str, ...] = SKIP_BAND_IDS) -> list[str]:
60
- out = []
61
- for b in bands(html):
62
- head = b[:240]
63
- if any(f'id="{i}"' in head for i in skip_ids):
64
- continue
65
- out.append(b)
66
- return out
67
-
68
-
69
- def chart_types_per_page(html: str) -> list[list[str]]:
70
- return [re.findall(r'data-chart="([^"]+)"', b) for b in bands(html)]
71
-
72
-
73
- def distinct_chart_types(html: str) -> list[str]:
74
- used = [t for ts in chart_types_per_page(html) for t in ts]
75
- return sorted(set(used))
76
-
77
-
78
- def variety_floor(mode: str, n_chart_pages: int, variety: dict | None = None) -> int:
79
- """类型数下限 = min(模式上限, max(1, ⌈图表页数 × ratio⌉))"""
80
- v = variety if variety is not None else chart_variety()
81
- floor = int((v.get('minTypes') or {}).get(mode, 4) or 4)
82
- ratio = float(v.get('minTypesRatio') or 0.6)
83
- return min(floor, max(1, int(-(-n_chart_pages * ratio // 1))))
84
-
85
-
86
- def adjacent_same_type(pages: list[list[str]]) -> list[str]:
87
- """相邻图表页同型告警文案列表。"""
88
- adj, prev, prev_i = [], None, 0
89
- for i, ts in enumerate(pages):
90
- if not ts:
91
- continue
92
- if prev and set(ts) & set(prev):
93
- adj.append(f'第 {prev_i + 1}/{i + 1} 页同型 {sorted(set(ts) & set(prev))}')
94
- prev, prev_i = ts, i
95
- return adj
96
-
97
-
98
- def carrier_count(band: str) -> int:
99
- return sum(1 for pat in CARRIERS if re.search(pat, band))
100
-
101
-
102
- def composite_pages(html: str) -> tuple[int, int]:
103
- """返回 (含 ≥2 种承载的内容页数, 内容页总数)。"""
104
- content = content_bands(html)
105
- multi = sum(1 for b in content if carrier_count(b) >= 2)
106
- return multi, len(content)
107
-
108
-
109
- def composite_required(mode: str, variety: dict | None = None) -> float:
110
- """组合版式比例下限;非 compositeModes 返回 0。"""
111
- v = variety if variety is not None else chart_variety()
112
- if mode not in (v.get('compositeModes') or ['research']):
113
- return 0.0
114
- return float(v.get('compositeMinRatio') or 0)
115
-
116
-
117
- def chart_channel(chart_type: str, reg: dict | None = None) -> str:
118
- reg = reg if reg is not None else chart_registry()
119
- spec = reg.get(str(chart_type or 'bar').lower()) or {}
120
- return str(spec.get('pptx') or 'native')
121
-
122
-
123
- def chart_datatable_mode(chart: dict, reg: dict | None = None) -> str:
124
- reg = reg if reg is not None else chart_registry()
125
- spec = reg.get(str(chart.get('type') or 'bar').lower()) or {}
126
- mode = str(chart.get('dataTable') or spec.get('dataTable') or 'notes').lower()
127
- return 'inline' if mode == 'appendix' else mode
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """TopPPT HTML · HTML 检查公共库(单源:承载正则 / 图表通道 / 多样性下限 / 组合版式)
4
+
5
+ 消费方:validate_report.py · quality_gate.py · evals/run_evals.py
6
+ 禁止在消费方再手抄 CARRIERS / 多样性公式 / 组合版式判定。
7
+ 阈值仍读 layout-constants.json(charts.variety / charts.registry)。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from pathlib import Path
14
+
15
+ ROOT = Path(__file__).resolve().parent.parent
16
+ _LC_PATH = ROOT / 'scripts' / 'layout-constants.json'
17
+
18
+ # 承载类型正则(组合版式:一页是否含 ≥2 种承载)
19
+ CARRIERS = [
20
+ r'<svg\b[^>]*data-chart=',
21
+ r'class="tbl-wrap',
22
+ r'class="card[\s"]',
23
+ r'class="metric[\s"]',
24
+ r'class="media[\s"]',
25
+ r'class="arch[\s"]',
26
+ r'class="lane[\s"]',
27
+ r'class="matrix[\s"]',
28
+ r'class="heat[\s"]',
29
+ r'class="bul[\s"]',
30
+ r'class="pyr[\s"]',
31
+ r'class="steps[\s"]',
32
+ r'class="exhibit[\s"]',
33
+ r'class="cols-2',
34
+ r'class="cols-3',
35
+ ]
36
+
37
+ SKIP_BAND_IDS = ('agenda', 'cover', 'refs', 'appendix', 'next')
38
+
39
+
40
+ def load_lc() -> dict:
41
+ return json.loads(_LC_PATH.read_text(encoding='utf-8'))
42
+
43
+
44
+ def chart_registry(lc: dict | None = None) -> dict:
45
+ lc = lc or load_lc()
46
+ return {k: v for k, v in ((lc.get('charts') or {}).get('registry') or {}).items()
47
+ if isinstance(v, dict)}
48
+
49
+
50
+ def chart_variety(lc: dict | None = None) -> dict:
51
+ lc = lc or load_lc()
52
+ return (lc.get('charts') or {}).get('variety') or {}
53
+
54
+
55
+ def bands(html: str) -> list[str]:
56
+ return re.split(r'(?=<section class="band)', html)[1:]
57
+
58
+
59
+ def content_bands(html: str, skip_ids: tuple[str, ...] = SKIP_BAND_IDS) -> list[str]:
60
+ out = []
61
+ for b in bands(html):
62
+ head = b[:240]
63
+ if any(f'id="{i}"' in head for i in skip_ids):
64
+ continue
65
+ out.append(b)
66
+ return out
67
+
68
+
69
+ def chart_types_per_page(html: str) -> list[list[str]]:
70
+ return [re.findall(r'data-chart="([^"]+)"', b) for b in bands(html)]
71
+
72
+
73
+ def distinct_chart_types(html: str) -> list[str]:
74
+ used = [t for ts in chart_types_per_page(html) for t in ts]
75
+ return sorted(set(used))
76
+
77
+
78
+ def variety_floor(mode: str, n_chart_pages: int, variety: dict | None = None) -> int:
79
+ """类型数下限 = min(模式上限, max(1, ⌈图表页数 × ratio⌉))"""
80
+ v = variety if variety is not None else chart_variety()
81
+ floor = int((v.get('minTypes') or {}).get(mode, 4) or 4)
82
+ ratio = float(v.get('minTypesRatio') or 0.6)
83
+ return min(floor, max(1, int(-(-n_chart_pages * ratio // 1))))
84
+
85
+
86
+ def adjacent_same_type(pages: list[list[str]]) -> list[str]:
87
+ """相邻图表页同型告警文案列表。"""
88
+ adj, prev, prev_i = [], None, 0
89
+ for i, ts in enumerate(pages):
90
+ if not ts:
91
+ continue
92
+ if prev and set(ts) & set(prev):
93
+ adj.append(f'第 {prev_i + 1}/{i + 1} 页同型 {sorted(set(ts) & set(prev))}')
94
+ prev, prev_i = ts, i
95
+ return adj
96
+
97
+
98
+ def carrier_count(band: str) -> int:
99
+ return sum(1 for pat in CARRIERS if re.search(pat, band))
100
+
101
+
102
+ def composite_pages(html: str) -> tuple[int, int]:
103
+ """返回 (含 ≥2 种承载的内容页数, 内容页总数)。"""
104
+ content = content_bands(html)
105
+ multi = sum(1 for b in content if carrier_count(b) >= 2)
106
+ return multi, len(content)
107
+
108
+
109
+ def composite_required(mode: str, variety: dict | None = None) -> float:
110
+ """组合版式比例下限;非 compositeModes 返回 0。"""
111
+ v = variety if variety is not None else chart_variety()
112
+ if mode not in (v.get('compositeModes') or ['research']):
113
+ return 0.0
114
+ return float(v.get('compositeMinRatio') or 0)
115
+
116
+
117
+ def chart_channel(chart_type: str, reg: dict | None = None) -> str:
118
+ reg = reg if reg is not None else chart_registry()
119
+ spec = reg.get(str(chart_type or 'bar').lower()) or {}
120
+ return str(spec.get('pptx') or 'native')
121
+
122
+
123
+ def chart_datatable_mode(chart: dict, reg: dict | None = None) -> str:
124
+ reg = reg if reg is not None else chart_registry()
125
+ spec = reg.get(str(chart.get('type') or 'bar').lower()) or {}
126
+ mode = str(chart.get('dataTable') or spec.get('dataTable') or 'notes').lower()
127
+ return 'inline' if mode == 'appendix' else mode