@topmindspace/tms-skills 0.1.0
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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/tms-skills.js +160 -0
- package/package.json +48 -0
- package/top-ppt-html/README.md +254 -0
- package/top-ppt-html/SKILL.md +111 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +168 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +168 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +914 -0
- package/top-ppt-html/assets/pptx-export.js +1944 -0
- package/top-ppt-html/assets/style-gallery.html +589 -0
- package/top-ppt-html/assets/templates/architecture.html +3728 -0
- package/top-ppt-html/assets/templates/engine.css +840 -0
- package/top-ppt-html/assets/templates/presentation.html +3738 -0
- package/top-ppt-html/assets/templates/research.html +4017 -0
- package/top-ppt-html/assets/templates/ui.js +520 -0
- package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
- package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
- package/top-ppt-html/assets/theme-overview-research.png +0 -0
- package/top-ppt-html/assets/theme-overview.png +0 -0
- package/top-ppt-html/evals/prompts.csv +15 -0
- package/top-ppt-html/evals/rubric.schema.json +25 -0
- package/top-ppt-html/evals/run_evals.py +220 -0
- package/top-ppt-html/evals/trace.example.json +16 -0
- package/top-ppt-html/package.json +35 -0
- package/top-ppt-html/references/charts-basic.md +624 -0
- package/top-ppt-html/references/charts-discipline.md +108 -0
- package/top-ppt-html/references/charts-extended.md +482 -0
- package/top-ppt-html/references/charts.md +28 -0
- package/top-ppt-html/references/components-atoms.md +624 -0
- package/top-ppt-html/references/components.md +30 -0
- package/top-ppt-html/references/content-rules.md +490 -0
- package/top-ppt-html/references/design-system-engine.md +235 -0
- package/top-ppt-html/references/design-system.md +478 -0
- package/top-ppt-html/references/failure-modes.md +214 -0
- package/top-ppt-html/references/high-fidelity.md +127 -0
- package/top-ppt-html/references/icons.md +397 -0
- package/top-ppt-html/references/industry-benchmark.md +105 -0
- package/top-ppt-html/references/infographics-stats.md +308 -0
- package/top-ppt-html/references/infographics-structure.md +226 -0
- package/top-ppt-html/references/infographics.md +43 -0
- package/top-ppt-html/references/layout-grammar.md +315 -0
- package/top-ppt-html/references/layouts-architecture.md +108 -0
- package/top-ppt-html/references/layouts-combo.md +600 -0
- package/top-ppt-html/references/layouts-research.md +160 -0
- package/top-ppt-html/references/modes.md +254 -0
- package/top-ppt-html/references/outline-design.md +275 -0
- package/top-ppt-html/references/playbook.md +266 -0
- package/top-ppt-html/references/pptx-export.md +209 -0
- package/top-ppt-html/references/reform-plan.md +252 -0
- package/top-ppt-html/references/styles.md +370 -0
- package/top-ppt-html/references/tech-design.md +138 -0
- package/top-ppt-html/scripts/audit_css.py +109 -0
- package/top-ppt-html/scripts/audit_docs.py +176 -0
- package/top-ppt-html/scripts/audit_skill.py +220 -0
- package/top-ppt-html/scripts/audit_styles.py +351 -0
- package/top-ppt-html/scripts/build_examples.py +2276 -0
- package/top-ppt-html/scripts/build_pptx.js +2380 -0
- package/top-ppt-html/scripts/capture_theme_overview.js +78 -0
- package/top-ppt-html/scripts/checks_html.py +127 -0
- package/top-ppt-html/scripts/cross_verify.py +294 -0
- package/top-ppt-html/scripts/env_probe.py +158 -0
- package/top-ppt-html/scripts/extract_model.py +210 -0
- package/top-ppt-html/scripts/extract_snippet.py +374 -0
- package/top-ppt-html/scripts/gen_channel_a.js +214 -0
- package/top-ppt-html/scripts/layout-constants.json +3377 -0
- package/top-ppt-html/scripts/layout_slots.json +830 -0
- package/top-ppt-html/scripts/lib_layout_regions.js +412 -0
- package/top-ppt-html/scripts/measure_height.py +178 -0
- package/top-ppt-html/scripts/model-schema.json +547 -0
- package/top-ppt-html/scripts/negative_tests.py +307 -0
- package/top-ppt-html/scripts/package_skill.py +291 -0
- package/top-ppt-html/scripts/prepare_images.py +341 -0
- package/top-ppt-html/scripts/probe_image_export.py +188 -0
- package/top-ppt-html/scripts/quality_gate.py +301 -0
- package/top-ppt-html/scripts/regression.py +308 -0
- package/top-ppt-html/scripts/render_compare.py +275 -0
- package/top-ppt-html/scripts/render_from_model.py +698 -0
- package/top-ppt-html/scripts/scaffold_report.py +1054 -0
- package/top-ppt-html/scripts/section-file-map.json +105 -0
- package/top-ppt-html/scripts/sync_runtime.py +662 -0
- package/top-ppt-html/scripts/validate_pptx.py +1510 -0
- package/top-ppt-html/scripts/validate_report.py +1456 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 交付质量门禁(HTML/PPTX + Evals 确定性检查 + rubric 启发式)
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/quality_gate.py report.html
|
|
7
|
+
python scripts/quality_gate.py report.html --pptx report.pptx --model report.model.json
|
|
8
|
+
python scripts/quality_gate.py report.html --trace trace.json --json
|
|
9
|
+
python scripts/quality_gate.py report.html --require-rubric 60
|
|
10
|
+
python scripts/quality_gate.py report.html --deliver # 末尾生成可粘贴的交付说明
|
|
11
|
+
|
|
12
|
+
定位:
|
|
13
|
+
validate_* 回答「结构是否合格」;本脚本再合并:
|
|
14
|
+
① evals/run_evals.py 的确定性/效率检查
|
|
15
|
+
② 无 LLM 的 rubric 启发式(content/layout/chart/infographic/tone 五维)
|
|
16
|
+
③ --deliver:按 SKILL.md「交付说明」七要素(路径/字节数/模式/风格/篇幅/格式/校验+引用)
|
|
17
|
+
生成结构化交付说明——七要素缺一即 gate FAIL,杜绝手拼遗漏
|
|
18
|
+
交付时一键跑完,避免「只过了结构、内容质量靠自觉」。
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
36
|
+
import checks_html # noqa: E402 承载/多样性/组合版式单源
|
|
37
|
+
|
|
38
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
39
|
+
LC = json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
|
|
40
|
+
RUBRIC_PATH = ROOT / 'evals' / 'rubric.schema.json'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run(cmd: list[str]) -> tuple[int, str, float]:
|
|
44
|
+
"""带计时的子进程执行(elapsed 秒用于交付耗时基线,v8.3 起)。"""
|
|
45
|
+
t0 = time.perf_counter()
|
|
46
|
+
r = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
|
|
47
|
+
out = (r.stdout or '') + (r.stderr or '')
|
|
48
|
+
return r.returncode, out, time.perf_counter() - t0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def kb(n: int) -> str:
|
|
52
|
+
return f'{n / 1024:.1f}KB'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_delivery(html: Path, pptx: Path | None, gates: list[dict], rubric: dict) -> dict:
|
|
56
|
+
"""SKILL.md「交付说明」七要素:路径 / 字节数 / 模式 / 风格 / 篇幅 / 格式 / 校验+引用。"""
|
|
57
|
+
txt = html.read_text(encoding='utf-8')
|
|
58
|
+
head = txt[:3000]
|
|
59
|
+
|
|
60
|
+
def attr(name: str) -> str:
|
|
61
|
+
m = re.search(rf'{name}="([^"]+)"', head)
|
|
62
|
+
return m.group(1) if m else ''
|
|
63
|
+
|
|
64
|
+
all_bands = bands(txt)
|
|
65
|
+
content = checks_html.content_bands(txt) # 与 composite_pages 同一口径(防两处维护)
|
|
66
|
+
refs = sorted(set(re.findall(r'id="(ref-\d+)"', txt)), key=lambda s: int(s.split('-')[1]))
|
|
67
|
+
note = {
|
|
68
|
+
'file': str(html),
|
|
69
|
+
'bytes': html.stat().st_size,
|
|
70
|
+
'mode': attr('data-mode'),
|
|
71
|
+
'style': attr('data-style'),
|
|
72
|
+
'theme': attr('data-theme'),
|
|
73
|
+
'pages': len(all_bands),
|
|
74
|
+
'content_pages': len(content),
|
|
75
|
+
'format': 'HTML' + (' + PPTX' if pptx else ''),
|
|
76
|
+
'refs': len(refs),
|
|
77
|
+
'validation': {g['name']: ('通过' if g['ok'] else '未通过') for g in gates},
|
|
78
|
+
'rubric': rubric['score'],
|
|
79
|
+
}
|
|
80
|
+
if pptx and pptx.exists():
|
|
81
|
+
note['pptx'] = {'file': str(pptx), 'bytes': pptx.stat().st_size}
|
|
82
|
+
return note
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def render_delivery(note: dict) -> str:
|
|
86
|
+
lines = [
|
|
87
|
+
f"- 文件:{note['file']}({kb(note['bytes'])})"
|
|
88
|
+
+ (f";{note['pptx']['file']}({kb(note['pptx']['bytes'])})" if note.get('pptx') else ''),
|
|
89
|
+
f"- 模式/风格/主题:{note['mode']} · {note['style']} · {note['theme']}",
|
|
90
|
+
f"- 篇幅:{note['content_pages']} 内容页(全篇 {note['pages']} 屏)",
|
|
91
|
+
f"- 交付格式:{note['format']}",
|
|
92
|
+
'- 校验:' + ' · '.join(f"{k} {v}" for k, v in note['validation'].items())
|
|
93
|
+
+ f" · rubric {note['rubric']}",
|
|
94
|
+
f"- 引用:{note['refs']} 条(参考资料双向对齐)",
|
|
95
|
+
]
|
|
96
|
+
return '\n'.join(lines)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def delivery_complete(note: dict) -> tuple[bool, str]:
|
|
100
|
+
"""七要素齐备性:字段缺失/为空串即不完整(数值 0 是合法值,如无外部引用的 refs=0)。"""
|
|
101
|
+
missing = [k for k in ('file', 'bytes', 'mode', 'style', 'theme', 'pages', 'format', 'refs')
|
|
102
|
+
if note.get(k) in (None, '')]
|
|
103
|
+
return (not missing), ('七要素齐备' if not missing else f'缺 {missing}')
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def bands(txt: str) -> list[str]:
|
|
107
|
+
return re.split(r'(?=<section class="band)', txt)[1:]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def heuristic_rubric(html: Path) -> dict:
|
|
111
|
+
"""五维启发式评分(0–100)。与 evals/rubric.schema.json 字段对齐,便于 LLM 复评时同构。"""
|
|
112
|
+
txt = html.read_text(encoding='utf-8')
|
|
113
|
+
mode_m = re.search(r'data-mode="([^"]+)"', txt)
|
|
114
|
+
mode = mode_m.group(1) if mode_m else 'presentation'
|
|
115
|
+
content_bands = checks_html.content_bands(txt) # 与 composite_pages/delivery 同一口径
|
|
116
|
+
|
|
117
|
+
checks = []
|
|
118
|
+
|
|
119
|
+
def add(cid: str, score: int, ok: bool, notes: str) -> None:
|
|
120
|
+
checks.append({'id': cid, 'score': max(0, min(100, int(score))),
|
|
121
|
+
'pass': bool(ok), 'notes': notes})
|
|
122
|
+
|
|
123
|
+
# ── content ──
|
|
124
|
+
n_sec = len(content_bands)
|
|
125
|
+
n_sowhat = txt.count('class="sowhat')
|
|
126
|
+
n_points = len(re.findall(r'<li\b', txt))
|
|
127
|
+
n_refs = len(set(re.findall(r'id="(ref-\d+)"', txt)))
|
|
128
|
+
n_tbd = len(re.findall(r'class="tbd(?:\s|")', txt))
|
|
129
|
+
c_score = 50
|
|
130
|
+
c_notes = [f'{n_sec} 内容页']
|
|
131
|
+
if n_sec >= 6:
|
|
132
|
+
c_score += 15
|
|
133
|
+
if n_sowhat >= max(1, n_sec // 4):
|
|
134
|
+
c_score += 15
|
|
135
|
+
c_notes.append(f'so-what {n_sowhat}')
|
|
136
|
+
else:
|
|
137
|
+
c_notes.append(f'so-what 偏少({n_sowhat})')
|
|
138
|
+
if n_points >= n_sec * 2:
|
|
139
|
+
c_score += 10
|
|
140
|
+
if n_refs:
|
|
141
|
+
c_score += 10
|
|
142
|
+
c_notes.append(f'引用 {n_refs}')
|
|
143
|
+
else:
|
|
144
|
+
c_notes.append('无外部引用')
|
|
145
|
+
if n_tbd and 'tbd-legend' not in txt and 'flagbar' not in txt:
|
|
146
|
+
c_score -= 10
|
|
147
|
+
c_notes.append('tbd 缺图例')
|
|
148
|
+
add('content', c_score, c_score >= 60, ';'.join(c_notes))
|
|
149
|
+
|
|
150
|
+
# ── layout ──
|
|
151
|
+
v = checks_html.chart_variety(LC)
|
|
152
|
+
multi, n_content = checks_html.composite_pages(txt)
|
|
153
|
+
ratio = (multi / n_content) if n_content else 0
|
|
154
|
+
l_score = 50 + int(ratio * 40)
|
|
155
|
+
if 'g-side' in txt or 'g-211' in txt or 'g-hero' in txt:
|
|
156
|
+
l_score += 10
|
|
157
|
+
add('layout', l_score, l_score >= 60,
|
|
158
|
+
f'组合版式 {multi}/{n_content}({ratio:.0%})')
|
|
159
|
+
|
|
160
|
+
# ── chart ──
|
|
161
|
+
used = checks_html.distinct_chart_types(txt)
|
|
162
|
+
n_chart_pages = len([1 for b in content_bands if re.search(r'data-chart="', b)])
|
|
163
|
+
ch_score = 40
|
|
164
|
+
ch_notes = [f'{len(used)} 型 / {n_chart_pages} 图表页']
|
|
165
|
+
if used:
|
|
166
|
+
floor = checks_html.variety_floor(mode, n_chart_pages, v)
|
|
167
|
+
if len(used) >= floor:
|
|
168
|
+
ch_score += 40
|
|
169
|
+
else:
|
|
170
|
+
ch_score += int(40 * len(used) / max(1, floor))
|
|
171
|
+
ch_notes.append(f'下限 {floor}')
|
|
172
|
+
if n_chart_pages >= 2:
|
|
173
|
+
ch_score += 10
|
|
174
|
+
else:
|
|
175
|
+
ch_notes.append('无图表')
|
|
176
|
+
add('chart', ch_score, ch_score >= 60, ';'.join(ch_notes))
|
|
177
|
+
|
|
178
|
+
# ── infographic ──
|
|
179
|
+
info = set(((LC.get('charts') or {}).get('scaffold') or {}).get('infoTypes') or {})
|
|
180
|
+
has_info = bool(set(used) & info)
|
|
181
|
+
has_struct = has_info or ('class="arch' in txt) or ('class="lane' in txt)
|
|
182
|
+
if mode == 'architecture':
|
|
183
|
+
i_score = 80 if has_struct else 30
|
|
184
|
+
i_notes = '架构模式结构主图:' + ('有' if has_struct else '缺')
|
|
185
|
+
else:
|
|
186
|
+
i_score = 70 if has_info else 55
|
|
187
|
+
i_notes = '信息图页型:' + ('有' if has_info else '未用(非必须)')
|
|
188
|
+
add('infographic', i_score, i_score >= 50, i_notes)
|
|
189
|
+
|
|
190
|
+
# ── tone ──
|
|
191
|
+
words = (LC.get('aiFlavor') or {}).get('words') or []
|
|
192
|
+
plain = re.sub(r'<[^>]+>', ' ', re.sub(r'<script\b[\s\S]*?</script>', ' ', txt))
|
|
193
|
+
hits = [w for w in words if re.search(w, plain)]
|
|
194
|
+
t_score = 100 if not hits else max(0, 100 - 15 * len(hits))
|
|
195
|
+
# 标题过短/空洞 so-what 轻微扣分
|
|
196
|
+
hollow = ((LC.get('contentQuality') or {}).get('sowhat') or {}).get('forbidden') or []
|
|
197
|
+
if any(h in txt for h in hollow):
|
|
198
|
+
t_score -= 15
|
|
199
|
+
add('tone', t_score, t_score >= 60,
|
|
200
|
+
'AI 腔命中 ' + (', '.join(hits[:3]) if hits else '无'))
|
|
201
|
+
|
|
202
|
+
total = round(sum(c['score'] for c in checks) / len(checks))
|
|
203
|
+
overall = all(c['pass'] for c in checks)
|
|
204
|
+
return {
|
|
205
|
+
'overall_pass': overall,
|
|
206
|
+
'score': total,
|
|
207
|
+
'checks': checks,
|
|
208
|
+
'$source': 'heuristic (quality_gate.py) · 可用 LLM 按 rubric.schema.json 复评',
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main() -> int:
|
|
213
|
+
ap = argparse.ArgumentParser(description='TopPPT HTML 交付质量门禁')
|
|
214
|
+
ap.add_argument('html', help='报告 HTML 路径')
|
|
215
|
+
ap.add_argument('--pptx', help='可选:PPTX 路径(跑 validate_pptx --strict)')
|
|
216
|
+
ap.add_argument('--model', help='可选:model.json(PPTX 往返保真)')
|
|
217
|
+
ap.add_argument('--trace', help='可选:过程 trace JSON(并入 evals)')
|
|
218
|
+
ap.add_argument('--require-rubric', type=int, default=60,
|
|
219
|
+
help='启发式 rubric 总分下限(默认 60;传 0 表示只报告不卡死)')
|
|
220
|
+
ap.add_argument('--deliver', action='store_true',
|
|
221
|
+
help='生成 SKILL.md「交付说明」七要素(缺一即 gate FAIL)')
|
|
222
|
+
ap.add_argument('--json', action='store_true')
|
|
223
|
+
args = ap.parse_args()
|
|
224
|
+
|
|
225
|
+
html = Path(args.html)
|
|
226
|
+
if not html.exists():
|
|
227
|
+
print(f'错误:HTML 不存在 {html}')
|
|
228
|
+
return 2
|
|
229
|
+
|
|
230
|
+
result: dict = {'html': str(html), 'gates': []}
|
|
231
|
+
|
|
232
|
+
def gate(name: str, ok: bool, note: str = '', elapsed: float = 0.0) -> None:
|
|
233
|
+
entry = {'name': name, 'ok': bool(ok), 'note': note}
|
|
234
|
+
if elapsed:
|
|
235
|
+
entry['elapsed_s'] = round(elapsed, 2)
|
|
236
|
+
result['gates'].append(entry)
|
|
237
|
+
mark = '✓' if ok else '✗'
|
|
238
|
+
tail = f' · {elapsed:.1f}s' if elapsed else ''
|
|
239
|
+
print(f' {mark} {name}' + (f' — {note}' if note else '') + tail)
|
|
240
|
+
|
|
241
|
+
print(f'交付质量门禁 · {html.name}')
|
|
242
|
+
print('-' * 60)
|
|
243
|
+
|
|
244
|
+
# ① HTML strict
|
|
245
|
+
rc, out, el = run([sys.executable, str(ROOT / 'scripts' / 'validate_report.py'), str(html), '--strict'])
|
|
246
|
+
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)
|
|
248
|
+
|
|
249
|
+
# ② PPTX strict(可选)
|
|
250
|
+
if args.pptx:
|
|
251
|
+
cmd = [sys.executable, str(ROOT / 'scripts' / 'validate_pptx.py'), args.pptx, '--strict']
|
|
252
|
+
if args.model:
|
|
253
|
+
cmd += ['--model=' + args.model]
|
|
254
|
+
rc, out, el = run(cmd)
|
|
255
|
+
tail = [ln for ln in out.splitlines() if ln.startswith('PASS ') or ln.startswith('结论')]
|
|
256
|
+
gate('validate_pptx --strict', rc == 0, tail[-1] if tail else f'exit {rc}', el)
|
|
257
|
+
|
|
258
|
+
# ③ evals 确定性
|
|
259
|
+
cmd = [sys.executable, str(ROOT / 'evals' / 'run_evals.py'), '--score', str(html)]
|
|
260
|
+
if args.trace:
|
|
261
|
+
cmd += ['--trace', args.trace]
|
|
262
|
+
rc, out, el = run(cmd)
|
|
263
|
+
gate('evals --score', rc == 0, '确定性 + 效率检查', el)
|
|
264
|
+
|
|
265
|
+
# ④ rubric 启发式
|
|
266
|
+
rubric = heuristic_rubric(html)
|
|
267
|
+
result['rubric'] = rubric
|
|
268
|
+
print(f' · rubric 启发式总分 {rubric["score"]} · '
|
|
269
|
+
f'{"通过" if rubric["overall_pass"] else "未通过"}')
|
|
270
|
+
for c in rubric['checks']:
|
|
271
|
+
mark = '✓' if c['pass'] else '✗'
|
|
272
|
+
print(f' {mark} [{c["id"]}] {c["score"]:>3} {c["notes"][:70]}')
|
|
273
|
+
if args.require_rubric and args.require_rubric > 0:
|
|
274
|
+
gate(f'rubric ≥ {args.require_rubric}', rubric['score'] >= args.require_rubric,
|
|
275
|
+
f'{rubric["score"]}')
|
|
276
|
+
else:
|
|
277
|
+
print(' · rubric 仅报告(--require-rubric=0)')
|
|
278
|
+
|
|
279
|
+
# ⑤ 交付说明(SKILL.md 七要素 · 缺一即 FAIL)
|
|
280
|
+
if args.deliver:
|
|
281
|
+
note = build_delivery(html, Path(args.pptx) if args.pptx else None,
|
|
282
|
+
result['gates'], rubric)
|
|
283
|
+
complete, why = delivery_complete(note)
|
|
284
|
+
gate('交付说明七要素', complete, why)
|
|
285
|
+
result['delivery'] = note
|
|
286
|
+
print('\n── 交付说明(可直接粘贴)──────────────────────')
|
|
287
|
+
print(render_delivery(note))
|
|
288
|
+
print('──────────────────────────────────────────────')
|
|
289
|
+
|
|
290
|
+
ok = all(g['ok'] for g in result['gates'])
|
|
291
|
+
result['pass'] = ok
|
|
292
|
+
print('-' * 60)
|
|
293
|
+
print('结论:' + ('可交付' if ok else '不可交付(修复后重跑)'))
|
|
294
|
+
|
|
295
|
+
if args.json:
|
|
296
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
297
|
+
return 0 if ok else 1
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == '__main__':
|
|
301
|
+
sys.exit(main())
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 全链路回归(维护工具,与机器环境无关)
|
|
4
|
+
|
|
5
|
+
链路:
|
|
6
|
+
① validate_report.py --strict 每份示例 HTML(三模式预算 + 模型一致性 + 版式断言)
|
|
7
|
+
② extract_model.py → build_pptx.js → validate_pptx.py --strict --model B 通道(交付通道)
|
|
8
|
+
③ gen_channel_a.js(页面预览同引擎产物)→ validate_pptx.py --strict --model --allow-shape-charts
|
|
9
|
+
④ probe_image_export.py(素材图片探针:示例矩阵零图片,真实素材路径/版式/门禁靠它覆盖)
|
|
10
|
+
⑤ 双区组合页探针(split 左区 = 图表):scaffold → extract → B 通道 + A 通道,两通道都过 strict
|
|
11
|
+
⑥ cross_verify.py(python-pptx 第三方裁判,可选:未安装则 SKIP)——覆盖示例矩阵与 ⑤ 探针
|
|
12
|
+
⑦ negative_tests.py(故障注入:往合格产物里埋已知缺陷,断言门禁必须报错)
|
|
13
|
+
⑦-b 任务路由全量遍历(extract_snippet TASK_ROUTES 每条路由可实际抽取,防修复链路静默断裂)
|
|
14
|
+
⑧ 深色主题探针(research / presentation × dark:示例矩阵只有 architecture 是 dark,
|
|
15
|
+
stylesDark / styleDataColorsDark 路径靠它覆盖)
|
|
16
|
+
⑨ measure_height.py(浏览器真值:页高与容器裁切,可选:无 playwright 则 SKIP)
|
|
17
|
+
|
|
18
|
+
用法:
|
|
19
|
+
python scripts/regression.py [--only <示例名关键字>]
|
|
20
|
+
|
|
21
|
+
环境解析(全部自动探测,不绑定任何机器的固定路径):
|
|
22
|
+
Node 可执行文件:TOP_PPT_NODE_EXE > PATH 上的 node > 常见托管目录最新版
|
|
23
|
+
pptxgenjs 所在 node_modules:TOP_PPT_NODE_PATH > 仓库内 node_modules > 逐级父目录 > 常见托管目录
|
|
24
|
+
python-pptx:未安装时跳过交叉裁判(打印 SKIP,不计失败)
|
|
25
|
+
|
|
26
|
+
模板/常量/schema/运行时/示例任何改动后必跑。
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import json
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
38
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
43
|
+
from env_probe import (NODE_HINT, NODE_MODULES_HINT, find_node, # noqa: E402
|
|
44
|
+
find_node_modules, has_python_pptx, node_env)
|
|
45
|
+
|
|
46
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
47
|
+
EX = ROOT / 'assets' / 'examples'
|
|
48
|
+
B_OUT = ROOT / 'dist' / 'regression'
|
|
49
|
+
A_OUT = ROOT / 'dist' / 'regression-a'
|
|
50
|
+
PY = sys.executable
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── 执行 ─────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def run(cmd: list[str], env: dict[str, str] | None = None) -> subprocess.CompletedProcess:
|
|
56
|
+
return subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', env=env)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> int:
|
|
60
|
+
ap = argparse.ArgumentParser(description='TopPPT HTML 全链路回归')
|
|
61
|
+
ap.add_argument('--only', help='只跑名字含该关键字的示例')
|
|
62
|
+
args = ap.parse_args()
|
|
63
|
+
|
|
64
|
+
node = find_node()
|
|
65
|
+
node_path = find_node_modules()
|
|
66
|
+
if not node:
|
|
67
|
+
print('错误: ' + NODE_HINT)
|
|
68
|
+
return 2
|
|
69
|
+
if not node_path:
|
|
70
|
+
print('错误: ' + NODE_MODULES_HINT)
|
|
71
|
+
return 2
|
|
72
|
+
env = node_env(node_path)
|
|
73
|
+
print(f'Node: {node}')
|
|
74
|
+
print(f'NODE_PATH: {node_path}')
|
|
75
|
+
print(f'python-pptx 第三方裁判: {"可用" if has_python_pptx() else "未安装(交叉裁判将跳过)"}')
|
|
76
|
+
|
|
77
|
+
B_OUT.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
fails: list[str] = []
|
|
79
|
+
skipped: list[str] = []
|
|
80
|
+
|
|
81
|
+
examples = sorted(EX.glob('*.html'))
|
|
82
|
+
if args.only:
|
|
83
|
+
examples = [h for h in examples if args.only in h.stem]
|
|
84
|
+
if not examples:
|
|
85
|
+
print('错误: assets/examples/ 下没有可回归的示例。')
|
|
86
|
+
return 2
|
|
87
|
+
|
|
88
|
+
for html in examples:
|
|
89
|
+
stem = html.stem
|
|
90
|
+
model = html.with_suffix('.model.json')
|
|
91
|
+
print(f'=== {stem}')
|
|
92
|
+
r0 = run([PY, str(ROOT / 'scripts' / 'validate_report.py'), str(html), '--strict'])
|
|
93
|
+
print(' [validate_report --strict]', 'OK' if r0.returncode == 0 else 'FAIL')
|
|
94
|
+
if r0.returncode != 0:
|
|
95
|
+
fails.append(stem + ':report')
|
|
96
|
+
print((r0.stdout or '')[-800:])
|
|
97
|
+
continue
|
|
98
|
+
r1 = run([PY, str(ROOT / 'scripts' / 'extract_model.py'), str(html)])
|
|
99
|
+
if r1.returncode != 0:
|
|
100
|
+
fails.append(stem + ':extract')
|
|
101
|
+
print((r1.stdout or '')[-500:])
|
|
102
|
+
continue
|
|
103
|
+
bp = B_OUT / (stem + '.pptx')
|
|
104
|
+
r2 = run([node, str(ROOT / 'scripts' / 'build_pptx.js'), str(bp), '--model=' + str(model)], env=env)
|
|
105
|
+
if r2.returncode != 0 or not bp.exists():
|
|
106
|
+
fails.append(stem + ':build')
|
|
107
|
+
print((r2.stderr or r2.stdout or '')[-800:])
|
|
108
|
+
continue
|
|
109
|
+
r3 = run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(bp), '--strict', '--model=' + str(model)])
|
|
110
|
+
print(' [B 通道 strict]', 'OK (0/0)' if r3.returncode == 0 else f'FAIL exit={r3.returncode}')
|
|
111
|
+
if r3.returncode != 0:
|
|
112
|
+
fails.append(stem + ':pptx-b')
|
|
113
|
+
for line in (r3.stdout or '').splitlines()[-14:]:
|
|
114
|
+
print(' ', line)
|
|
115
|
+
|
|
116
|
+
print('\n=== A 通道(页面预览序列化引擎产物 · gen_channel_a 组装)')
|
|
117
|
+
rA = run([node, str(ROOT / 'scripts' / 'gen_channel_a.js')], env=env)
|
|
118
|
+
if rA.returncode != 0:
|
|
119
|
+
fails.append('A:generate')
|
|
120
|
+
print((rA.stderr or rA.stdout or '')[-800:])
|
|
121
|
+
else:
|
|
122
|
+
ppx_ok = has_python_pptx()
|
|
123
|
+
for ap in sorted(A_OUT.glob('*.pptx')):
|
|
124
|
+
model = EX / (ap.stem + '.model.json')
|
|
125
|
+
if not model.exists():
|
|
126
|
+
# 非示例产物(如 ⑤ 探针的 probe-split.pptx)由探针段单独校验,跳过避免误判
|
|
127
|
+
continue
|
|
128
|
+
# A 通道(预览引擎)图表为形状近似渲染,豁免 MODEL_CHART_COUNT(交付通道 B 不豁免)
|
|
129
|
+
r = run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(ap), '--strict',
|
|
130
|
+
'--model=' + str(model), '--allow-shape-charts'])
|
|
131
|
+
ok = r.returncode == 0
|
|
132
|
+
ppx = 'SKIP'
|
|
133
|
+
if ppx_ok:
|
|
134
|
+
p = run([PY, '-c',
|
|
135
|
+
'import sys;from pptx import Presentation;'
|
|
136
|
+
'print(len(list(Presentation(sys.argv[1]).slides)))', str(ap)])
|
|
137
|
+
ppx = 'OK' if p.returncode == 0 else 'FAIL'
|
|
138
|
+
print(f' {ap.name}: strict={"OK" if ok else "FAIL"} python-pptx={ppx}')
|
|
139
|
+
if not ok:
|
|
140
|
+
fails.append(ap.stem + ':pptx-a')
|
|
141
|
+
for line in (r.stdout or '').splitlines()[-10:]:
|
|
142
|
+
print(' ', line)
|
|
143
|
+
if ppx == 'FAIL':
|
|
144
|
+
fails.append(ap.stem + ':ppx-a')
|
|
145
|
+
if ppx == 'SKIP':
|
|
146
|
+
skipped.append(ap.stem + ':ppx-a')
|
|
147
|
+
|
|
148
|
+
# ④ 素材图片导出探针:示例矩阵刻意零图片,真实素材路径/版式/门禁靠探针覆盖
|
|
149
|
+
r5 = run([PY, str(ROOT / 'scripts' / 'probe_image_export.py')], env=env)
|
|
150
|
+
print(' [probe_image_export]', 'OK' if r5.returncode == 0 else 'FAIL')
|
|
151
|
+
if r5.returncode != 0:
|
|
152
|
+
fails.append('probe-image')
|
|
153
|
+
for line in (r5.stdout or '').splitlines()[-8:]:
|
|
154
|
+
print(' ', line)
|
|
155
|
+
|
|
156
|
+
# ⑤ 双区组合页探针(split 左区 = 图表):示例矩阵只覆盖"左文右图"缺省路径,
|
|
157
|
+
# 左区图表/表格/图片三个变体靠脚手架生成一次走完整交付链路(v8 新增能力);
|
|
158
|
+
# B 通道 + A 通道(同一模型)都跑,产物同名落两目录 → ⑥ 交叉裁判可逐页比对。
|
|
159
|
+
print('\n=== 双区组合页探针(split 左区 = 图表)')
|
|
160
|
+
probe_plan = B_OUT / 'probe-split.plan.json'
|
|
161
|
+
probe_html = B_OUT / 'probe-split.html'
|
|
162
|
+
probe_model = B_OUT / 'probe-split.model.json'
|
|
163
|
+
probe_pptx = B_OUT / 'probe-split.pptx'
|
|
164
|
+
probe_plan.write_text(json.dumps(
|
|
165
|
+
[{'type': 'points', 'eyebrow': '01 · 前置', 'title': '前置要点页(探针占位)'},
|
|
166
|
+
{'type': 'split', 'left_type': 'chart', 'eyebrow': '02 · 组合版式',
|
|
167
|
+
'title': '双区组合页探针:左图右文(左区图表路径)'}], ensure_ascii=False), encoding='utf-8')
|
|
168
|
+
ok6, why6 = False, ''
|
|
169
|
+
r6 = run([PY, str(ROOT / 'scripts' / 'scaffold_report.py'), '--mode', 'presentation',
|
|
170
|
+
'--plan', str(probe_plan), '--out', str(probe_html)])
|
|
171
|
+
if r6.returncode != 0:
|
|
172
|
+
why6 = 'scaffold'
|
|
173
|
+
else:
|
|
174
|
+
r7 = run([PY, str(ROOT / 'scripts' / 'extract_model.py'), str(probe_html), str(probe_model)])
|
|
175
|
+
if r7.returncode != 0:
|
|
176
|
+
why6 = 'extract'
|
|
177
|
+
else:
|
|
178
|
+
r8 = run([node, str(ROOT / 'scripts' / 'build_pptx.js'), str(probe_pptx),
|
|
179
|
+
'--model=' + str(probe_model)], env=env)
|
|
180
|
+
if r8.returncode != 0 or not probe_pptx.exists():
|
|
181
|
+
why6 = 'build'
|
|
182
|
+
else:
|
|
183
|
+
r9 = run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(probe_pptx),
|
|
184
|
+
'--strict', '--model=' + str(probe_model)])
|
|
185
|
+
ok6 = r9.returncode == 0
|
|
186
|
+
if not ok6:
|
|
187
|
+
why6 = 'validate_pptx_b'
|
|
188
|
+
for line in (r9.stdout or '').splitlines()[-10:]:
|
|
189
|
+
print(' ', line)
|
|
190
|
+
else:
|
|
191
|
+
# A 通道同模型(预览序列化引擎):产物同名落 dist/regression-a/
|
|
192
|
+
probe_pptx_a = A_OUT / 'probe-split.pptx'
|
|
193
|
+
if probe_pptx_a.exists():
|
|
194
|
+
probe_pptx_a.unlink()
|
|
195
|
+
r10 = run([node, str(ROOT / 'scripts' / 'gen_channel_a.js'),
|
|
196
|
+
'--model=' + str(probe_model), '--out=' + str(A_OUT)], env=env)
|
|
197
|
+
if r10.returncode != 0 or not probe_pptx_a.exists():
|
|
198
|
+
ok6, why6 = False, 'channel_a'
|
|
199
|
+
print((r10.stderr or r10.stdout or '')[-400:])
|
|
200
|
+
else:
|
|
201
|
+
# 预览引擎图表为形状近似 → 豁免 MODEL_CHART_COUNT(同示例矩阵口径)
|
|
202
|
+
r11 = run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(probe_pptx_a),
|
|
203
|
+
'--strict', '--model=' + str(probe_model),
|
|
204
|
+
'--allow-shape-charts'])
|
|
205
|
+
if r11.returncode != 0:
|
|
206
|
+
ok6, why6 = False, 'validate_pptx_a'
|
|
207
|
+
for line in (r11.stdout or '').splitlines()[-10:]:
|
|
208
|
+
print(' ', line)
|
|
209
|
+
print(' [split 左区图表 B/A 双通道]', 'OK' if ok6 else f'FAIL({why6})')
|
|
210
|
+
if not ok6:
|
|
211
|
+
fails.append('probe-split')
|
|
212
|
+
|
|
213
|
+
# ⑥b 深色主题导出探针:示例矩阵只有 architecture/graphite-dark 是 dark,
|
|
214
|
+
# research / presentation 的 stylesDark + styleDataColorsDark 路径此前零覆盖。
|
|
215
|
+
print('\n=== 深色主题导出探针(research / presentation × dark)')
|
|
216
|
+
dark_fail = []
|
|
217
|
+
for stem in ('2026-09-09-research-mckinsey', '2026-09-09-presentation-business-blue'):
|
|
218
|
+
src_model = EX / f'{stem}.model.json'
|
|
219
|
+
if not src_model.exists():
|
|
220
|
+
continue
|
|
221
|
+
dm = B_OUT / f'{stem}-dark.model.json'
|
|
222
|
+
dp = B_OUT / f'{stem}-dark.pptx'
|
|
223
|
+
data = json.loads(src_model.read_text(encoding='utf-8'))
|
|
224
|
+
data['theme'] = 'dark'
|
|
225
|
+
dm.write_text(json.dumps(data, ensure_ascii=False), encoding='utf-8')
|
|
226
|
+
rb = run([node, str(ROOT / 'scripts' / 'build_pptx.js'), str(dp),
|
|
227
|
+
'--model=' + str(dm)], env=env)
|
|
228
|
+
if rb.returncode != 0 or not dp.exists():
|
|
229
|
+
dark_fail.append(stem + ':build')
|
|
230
|
+
print((rb.stderr or rb.stdout or '')[-300:])
|
|
231
|
+
continue
|
|
232
|
+
rv = run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(dp),
|
|
233
|
+
'--strict', '--model=' + str(dm)])
|
|
234
|
+
print(f' {stem} dark:', 'OK' if rv.returncode == 0 else 'FAIL')
|
|
235
|
+
if rv.returncode != 0:
|
|
236
|
+
dark_fail.append(stem + ':validate')
|
|
237
|
+
for line in (rv.stdout or '').splitlines()[-8:]:
|
|
238
|
+
print(' ', line)
|
|
239
|
+
if dark_fail:
|
|
240
|
+
fails.append('dark-theme')
|
|
241
|
+
|
|
242
|
+
# ⑥ 双通道交叉裁判(python-pptx):放在探针之后,A/B 产物齐全时才逐页比对
|
|
243
|
+
r4 = run([PY, str(ROOT / 'scripts' / 'cross_verify.py')])
|
|
244
|
+
if r4.returncode == 2:
|
|
245
|
+
print(' [cross_verify] SKIP(python-pptx 未安装,可选第三方裁判)')
|
|
246
|
+
skipped.append('cross-verify')
|
|
247
|
+
else:
|
|
248
|
+
print(' [cross_verify]', 'OK' if r4.returncode == 0 else 'FAIL')
|
|
249
|
+
if r4.returncode != 0:
|
|
250
|
+
fails.append('cross-verify')
|
|
251
|
+
print((r4.stdout or '')[-600:])
|
|
252
|
+
|
|
253
|
+
# ⑦ 门禁反向验证(故障注入):正向全绿只能证明“没误报”,这里证明“真能报”
|
|
254
|
+
r12 = run([PY, str(ROOT / 'scripts' / 'negative_tests.py')])
|
|
255
|
+
print(' [negative_tests]', 'OK' if r12.returncode == 0 else 'FAIL')
|
|
256
|
+
if r12.returncode != 0:
|
|
257
|
+
fails.append('negative-tests')
|
|
258
|
+
print((r12.stdout or '')[-600:])
|
|
259
|
+
|
|
260
|
+
# ⑦-b 任务路由全量遍历:--task 与 validate_report FIX_GUIDE 的修复链路不得静默断裂
|
|
261
|
+
# (v8.3 前教训:中文节号不被解析,7/11 修复命令执行即报错且无任何审计覆盖;
|
|
262
|
+
# 实现与 audit_docs ⑥ 共用 ES.verify_routes,防两处维护)
|
|
263
|
+
try:
|
|
264
|
+
import extract_snippet as ES # noqa: PLC0415
|
|
265
|
+
route_bad = ES.verify_routes()
|
|
266
|
+
print(' [task_routes]', 'OK' if not route_bad else f'FAIL {route_bad}')
|
|
267
|
+
if route_bad:
|
|
268
|
+
fails.append('task-routes')
|
|
269
|
+
except ImportError:
|
|
270
|
+
print(' [task_routes] FAIL(extract_snippet 不可导入)')
|
|
271
|
+
fails.append('task-routes')
|
|
272
|
+
|
|
273
|
+
# ⑧ 浏览器真值测量(可选):静态估算看不到「表格被半幅容器横向裁掉一列」这类真实布局缺陷。
|
|
274
|
+
# 当前解释器可能未装 playwright(托管运行时常见);measure_height.py 会自动回落到
|
|
275
|
+
# 本机已装 playwright 的解释器,因此这里只要能跑 measure_height 就不再跳过。
|
|
276
|
+
mh_fail = []
|
|
277
|
+
for html in examples:
|
|
278
|
+
r13 = run([PY, str(ROOT / 'scripts' / 'measure_height.py'), str(html)])
|
|
279
|
+
out = (r13.stdout or '') + (r13.stderr or '')
|
|
280
|
+
if r13.returncode != 0:
|
|
281
|
+
mh_fail.append(html.stem)
|
|
282
|
+
print(out[-500:])
|
|
283
|
+
elif '跳过:' in out and 'playwright' in out.lower():
|
|
284
|
+
# 解释器与回落链都找不到 playwright → 整批跳过(可选依赖)
|
|
285
|
+
print(' [measure_height] SKIP(playwright 未安装,可选真值裁判)')
|
|
286
|
+
skipped.append('measure-height')
|
|
287
|
+
mh_fail = []
|
|
288
|
+
break
|
|
289
|
+
if 'measure-height' not in skipped:
|
|
290
|
+
print(' [measure_height 页高/容器裁切真值]', 'OK' if not mh_fail else f'FAIL {mh_fail}')
|
|
291
|
+
if mh_fail:
|
|
292
|
+
fails.append('measure-height')
|
|
293
|
+
|
|
294
|
+
print('=' * 56)
|
|
295
|
+
if skipped:
|
|
296
|
+
print('跳过(可选依赖缺失):', sorted(set(skipped)))
|
|
297
|
+
if fails:
|
|
298
|
+
print('回归失败:', fails)
|
|
299
|
+
return 1
|
|
300
|
+
print('全链路回归通过:HTML strict + B 通道精导 strict 0/0 + A 通道(预览同引擎产物)strict 0/0'
|
|
301
|
+
+ ' + 双区组合页双通道探针 + 深色主题探针 + 门禁反向验证 + 任务路由全量遍历'
|
|
302
|
+
+ ('' if 'measure-height' in skipped else ' + 浏览器页高/裁切真值')
|
|
303
|
+
+ (' + python-pptx 双裁判 + 双通道交叉一致' if 'cross-verify' not in skipped else ''))
|
|
304
|
+
return 0
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == '__main__':
|
|
308
|
+
sys.exit(main())
|