@topmindspace/tms-skills 2.0.1 → 2.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 +145 -134
- package/README.md +1 -1
- package/bin/tms-skills.js +160 -160
- package/package.json +48 -48
- package/top-ppt-html/README.md +254 -254
- package/top-ppt-html/SKILL.md +111 -111
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +167 -167
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +167 -167
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +913 -913
- package/top-ppt-html/assets/pptx-export.js +1944 -1944
- package/top-ppt-html/assets/style-gallery.html +589 -589
- package/top-ppt-html/assets/templates/architecture.html +3728 -3728
- package/top-ppt-html/assets/templates/engine.css +840 -840
- package/top-ppt-html/assets/templates/presentation.html +3738 -3738
- package/top-ppt-html/assets/templates/research.html +4017 -4017
- package/top-ppt-html/assets/templates/ui.js +520 -520
- package/top-ppt-html/evals/prompts.csv +15 -15
- package/top-ppt-html/evals/rubric.schema.json +25 -25
- package/top-ppt-html/evals/run_evals.py +220 -220
- package/top-ppt-html/evals/trace.example.json +16 -16
- package/top-ppt-html/package-lock.json +207 -0
- package/top-ppt-html/package.json +35 -35
- package/top-ppt-html/references/charts-basic.md +624 -624
- package/top-ppt-html/references/charts-discipline.md +108 -108
- package/top-ppt-html/references/charts-extended.md +482 -482
- package/top-ppt-html/references/charts.md +28 -28
- package/top-ppt-html/references/components-atoms.md +624 -624
- package/top-ppt-html/references/components.md +30 -30
- package/top-ppt-html/references/content-rules.md +490 -490
- package/top-ppt-html/references/design-system-engine.md +235 -235
- package/top-ppt-html/references/design-system.md +478 -478
- package/top-ppt-html/references/failure-modes.md +214 -214
- package/top-ppt-html/references/high-fidelity.md +127 -127
- package/top-ppt-html/references/icons.md +397 -397
- package/top-ppt-html/references/industry-benchmark.md +105 -105
- package/top-ppt-html/references/infographics-stats.md +308 -308
- package/top-ppt-html/references/infographics-structure.md +226 -226
- package/top-ppt-html/references/infographics.md +43 -43
- package/top-ppt-html/references/layout-grammar.md +315 -315
- package/top-ppt-html/references/layouts-architecture.md +108 -108
- package/top-ppt-html/references/layouts-combo.md +600 -600
- package/top-ppt-html/references/layouts-research.md +160 -160
- package/top-ppt-html/references/modes.md +254 -254
- package/top-ppt-html/references/outline-design.md +275 -275
- package/top-ppt-html/references/playbook.md +266 -266
- package/top-ppt-html/references/pptx-export.md +209 -209
- package/top-ppt-html/references/reform-plan.md +252 -252
- package/top-ppt-html/references/styles.md +370 -370
- package/top-ppt-html/references/tech-design.md +138 -138
- package/top-ppt-html/scripts/audit_css.py +109 -109
- package/top-ppt-html/scripts/audit_docs.py +176 -176
- package/top-ppt-html/scripts/audit_skill.py +220 -220
- package/top-ppt-html/scripts/audit_styles.py +351 -351
- package/top-ppt-html/scripts/build_examples.py +2276 -2276
- package/top-ppt-html/scripts/build_pptx.js +2380 -2380
- package/top-ppt-html/scripts/capture_theme_overview.js +78 -78
- package/top-ppt-html/scripts/checks_html.py +127 -127
- package/top-ppt-html/scripts/cross_verify.py +294 -294
- package/top-ppt-html/scripts/env_probe.py +158 -158
- package/top-ppt-html/scripts/extract_model.py +210 -210
- package/top-ppt-html/scripts/extract_snippet.py +374 -374
- package/top-ppt-html/scripts/gen_channel_a.js +214 -214
- package/top-ppt-html/scripts/layout-constants.json +3377 -3377
- package/top-ppt-html/scripts/layout_slots.json +830 -830
- package/top-ppt-html/scripts/lib_layout_regions.js +412 -412
- package/top-ppt-html/scripts/measure_height.py +178 -178
- package/top-ppt-html/scripts/model-schema.json +547 -547
- package/top-ppt-html/scripts/negative_tests.py +307 -307
- package/top-ppt-html/scripts/package_skill.py +291 -291
- package/top-ppt-html/scripts/prepare_images.py +341 -341
- package/top-ppt-html/scripts/probe_image_export.py +188 -188
- package/top-ppt-html/scripts/quality_gate.py +301 -301
- package/top-ppt-html/scripts/regression.py +308 -308
- package/top-ppt-html/scripts/render_compare.py +275 -275
- package/top-ppt-html/scripts/render_from_model.py +698 -698
- package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
- package/top-ppt-html/scripts/section-file-map.json +104 -104
- package/top-ppt-html/scripts/sync_runtime.py +662 -662
- package/top-ppt-html/scripts/validate_pptx.py +1510 -1510
- package/top-ppt-html/scripts/validate_report.py +1456 -1456
|
@@ -1,307 +1,307 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""TopPPT HTML · 门禁反向验证(故障注入)
|
|
4
|
-
|
|
5
|
-
校验器最容易退化成「只会说 OK 的摆设」。本脚本往合格产物里**注入已知缺陷**,
|
|
6
|
-
断言对应门禁必须报错——门禁抓不到即视为失败。
|
|
7
|
-
|
|
8
|
-
覆盖:
|
|
9
|
-
N1 Exhibit 框漏编号(旧实现只校验已编号者的连续性 → 静默通过)
|
|
10
|
-
N2 参考资料两位数编号错配(旧正则 `ref-\\d` 两侧同时落空 → 静默通过)
|
|
11
|
-
N3 参考资料编号跳号
|
|
12
|
-
N4 PPTX 演讲者备注被剔离(dataTable=notes 声称数据入备注,不核对就只是一句承诺)
|
|
13
|
-
N5 模型图表数值篡改(cross_verify 数值核对)
|
|
14
|
-
N6 行动标题判断词鉴别力(正则本身)
|
|
15
|
-
N7 PPTX 字号越出比例尺
|
|
16
|
-
N8 data-chart 未登记类型(登记表白名单)
|
|
17
|
-
N9 图表多样性塌陷(全篇压成同型 → 不同类型数 < 模式下限)
|
|
18
|
-
N10 图片外链(零外链铁律)
|
|
19
|
-
N11 待核实有标色无图例(.tbd 必须配 .tbd-legend/.flagbar)
|
|
20
|
-
N12 模型 theme 与 data-theme 矛盾
|
|
21
|
-
N13 模型 mode 与 data-mode 矛盾
|
|
22
|
-
N14 内部锚点断裂(链接目标 id 不存在)
|
|
23
|
-
|
|
24
|
-
用法: python scripts/negative_tests.py
|
|
25
|
-
"""
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
import json
|
|
29
|
-
import re
|
|
30
|
-
import subprocess
|
|
31
|
-
import sys
|
|
32
|
-
import zipfile
|
|
33
|
-
from pathlib import Path
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
37
|
-
except Exception:
|
|
38
|
-
pass
|
|
39
|
-
|
|
40
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
41
|
-
OUT = ROOT / 'dist' / 'negtest'
|
|
42
|
-
PY = sys.executable
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def run_validate(path: Path) -> tuple[int, str]:
|
|
46
|
-
r = subprocess.run([PY, str(ROOT / 'scripts' / 'validate_report.py'), str(path), '--strict'],
|
|
47
|
-
capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
48
|
-
return r.returncode, (r.stdout or '') + (r.stderr or '')
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def expect_fail(name: str, path: Path, keyword: str) -> bool:
|
|
52
|
-
code, out = run_validate(path)
|
|
53
|
-
# --strict 下 WARN 同样计失败(退出码 1),故 [WARN] 行也算「门禁抓到」
|
|
54
|
-
hit = [ln for ln in out.splitlines()
|
|
55
|
-
if keyword in ln and ('[FAIL]' in ln or '[WARN]' in ln)]
|
|
56
|
-
ok = code != 0 and bool(hit)
|
|
57
|
-
print(f' [{"OK" if ok else "FAIL"}] {name}')
|
|
58
|
-
if hit:
|
|
59
|
-
print(f' 抓到: {hit[0].strip()[:120]}')
|
|
60
|
-
elif not ok:
|
|
61
|
-
print(f' 门禁未触发(期望 FAIL 含「{keyword}」)')
|
|
62
|
-
return ok
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def main() -> int:
|
|
66
|
-
base = ROOT / 'assets' / 'examples' / '2026-09-09-research-mckinsey.html'
|
|
67
|
-
if not base.exists():
|
|
68
|
-
print(f'SKIP:基准示例缺失 {base}')
|
|
69
|
-
return 0
|
|
70
|
-
txt = base.read_text(encoding='utf-8')
|
|
71
|
-
OUT.mkdir(parents=True, exist_ok=True)
|
|
72
|
-
|
|
73
|
-
code, out = run_validate(base)
|
|
74
|
-
if code != 0:
|
|
75
|
-
print('[FAIL] 基准示例本身未通过 strict,反向验证失去意义')
|
|
76
|
-
return 1
|
|
77
|
-
print('基准示例 strict 通过,开始故障注入:')
|
|
78
|
-
|
|
79
|
-
cases: list[tuple[str, str, str]] = []
|
|
80
|
-
|
|
81
|
-
# N1 抹掉一个 Exhibit 编号(框仍在)
|
|
82
|
-
cases.append(('N1 Exhibit 框漏编号',
|
|
83
|
-
txt.replace('class="exhibit__no"', 'class="exhibit__no--x"', 1),
|
|
84
|
-
'Exhibit 框均已编号'))
|
|
85
|
-
|
|
86
|
-
# N2 参考资料改成两位数编号,与正文引用错配
|
|
87
|
-
cases.append(('N2 引用编号两位数错配',
|
|
88
|
-
re.sub(r'id="ref-1"', 'id="ref-10"', txt, count=1),
|
|
89
|
-
'双向对齐'))
|
|
90
|
-
|
|
91
|
-
# N3 编号跳号(两侧同时改,集合仍对齐,但 1..N 不连续)
|
|
92
|
-
t3 = txt.replace('ref-2"', 'ref-9"')
|
|
93
|
-
cases.append(('N3 参考资料编号跳号', t3, '编号连续'))
|
|
94
|
-
|
|
95
|
-
# N8 data-chart 用了登记表外的类型
|
|
96
|
-
cases.append(('N8 图表类型未登记',
|
|
97
|
-
txt.replace('data-chart="hbar"', 'data-chart="hbarx"', 1),
|
|
98
|
-
'登记表内'))
|
|
99
|
-
|
|
100
|
-
# N9 图表多样性塌陷(全部压成 bar → 不同类型数 1 < research 下限 6)
|
|
101
|
-
t9 = txt
|
|
102
|
-
for ct in ('hbar', 'donut', 'waterfall', 'sankey', 'treemap', 'boxplot',
|
|
103
|
-
'network', 'marimekko', 'streamgraph', 'gantt', 'rose', 'candlestick'):
|
|
104
|
-
t9 = t9.replace(f'data-chart="{ct}"', 'data-chart="bar"')
|
|
105
|
-
cases.append(('N9 图表多样性塌陷', t9, '图表多样性'))
|
|
106
|
-
|
|
107
|
-
# N10 外链图片(零外链铁律)
|
|
108
|
-
cases.append(('N10 图片外链',
|
|
109
|
-
txt.replace('</body>',
|
|
110
|
-
'<img src="https://example.com/x.png" alt="外链注入"></body>', 1),
|
|
111
|
-
'图片源无外链'))
|
|
112
|
-
|
|
113
|
-
# N11 待核实有标色无图例(改名不得包含原串——校验是子串判断)
|
|
114
|
-
t11 = txt.replace('tbd-legend', 'tbd-leg').replace('flagbar', 'flag-bar')
|
|
115
|
-
cases.append(('N11 待核实无图例', t11, '待核实标注'))
|
|
116
|
-
|
|
117
|
-
# N12 模型 theme 与页面 data-theme 矛盾
|
|
118
|
-
cases.append(('N12 模型主题不一致',
|
|
119
|
-
txt.replace('"theme": "light"', '"theme": "dark"', 1),
|
|
120
|
-
'data-theme 一致'))
|
|
121
|
-
|
|
122
|
-
# N13 模型 mode 与页面 data-mode 矛盾
|
|
123
|
-
cases.append(('N13 模型模式不一致',
|
|
124
|
-
txt.replace('"mode": "research"', '"mode": "presentation"', 1),
|
|
125
|
-
'data-mode 一致'))
|
|
126
|
-
|
|
127
|
-
# N14 内部锚点断裂(链接目标 id 不存在)
|
|
128
|
-
cases.append(('N14 锚点断裂',
|
|
129
|
-
txt.replace('href="#s1"', 'href="#s404"'),
|
|
130
|
-
'锚点闭环'))
|
|
131
|
-
|
|
132
|
-
# N15 HTML 标签泄漏进可见文本(截图级:转义标签当字面量显示)
|
|
133
|
-
cases.append(('N15 标签泄漏',
|
|
134
|
-
txt.replace('</p>',
|
|
135
|
-
' <a class="cite" href="#ref-1">[1]</a>。</p>', 1),
|
|
136
|
-
'HTML_TAG_IN_TEXT'))
|
|
137
|
-
|
|
138
|
-
# N16 极偏 donut(0.5% vs 99.5% 应改 KPI,禁环图)
|
|
139
|
-
t16 = txt
|
|
140
|
-
if '"type": "donut"' in t16 or '"type":"donut"' in t16:
|
|
141
|
-
t16 = re.sub(
|
|
142
|
-
r'("type"\s*:\s*"donut"[\s\S]{0,200}?"values"\s*:\s*\[)[^\]]*(\])',
|
|
143
|
-
r'\g<1>0.5, 99.5\g<2>', t16, count=1)
|
|
144
|
-
cases.append(('N16 极偏 donut', t16, 'CHART_SKEW'))
|
|
145
|
-
|
|
146
|
-
# N17 模型字段夹带 HTML 标签(extract 净化 + validate 扫描 REPORT_MODEL)
|
|
147
|
-
t17 = re.sub(
|
|
148
|
-
r'("soWhat"\s*:\s*")',
|
|
149
|
-
r'\g<1><a class=\\"cite\\" href=\\"#ref-1\\">[1]</a> ',
|
|
150
|
-
txt, count=1)
|
|
151
|
-
if t17 == txt:
|
|
152
|
-
t17 = txt.replace(
|
|
153
|
-
'"footnote":',
|
|
154
|
-
'"footnote": "<strong>x</strong> ', 1)
|
|
155
|
-
cases.append(('N17 模型夹带标签', t17, 'HTML_TAG_IN_TEXT'))
|
|
156
|
-
|
|
157
|
-
ok = True
|
|
158
|
-
for name, content, keyword in cases:
|
|
159
|
-
if content == txt:
|
|
160
|
-
print(f' [SKIP] {name}(基准示例无对应结构)')
|
|
161
|
-
continue
|
|
162
|
-
p = OUT / (name.split()[0] + '.html')
|
|
163
|
-
p.write_text(content, encoding='utf-8')
|
|
164
|
-
ok = expect_fail(name, p, keyword) and ok
|
|
165
|
-
|
|
166
|
-
ok = _pptx_notes_case() and ok
|
|
167
|
-
ok = _chart_data_case() and ok
|
|
168
|
-
ok = _title_pattern_case() and ok
|
|
169
|
-
ok = _font_scale_case() and ok
|
|
170
|
-
|
|
171
|
-
print('反向验证通过:注入的缺陷都被门禁抓住。' if ok
|
|
172
|
-
else '反向验证失败:存在抓不到的缺陷,门禁有假阴性。')
|
|
173
|
-
return 0 if ok else 1
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _font_scale_case() -> bool:
|
|
177
|
-
"""N7:往 PPTX 里塞一个比例尺外的字号 → FONT_SIZE_OFF_SCALE 必须报。"""
|
|
178
|
-
name = '2026-09-09-research-mckinsey'
|
|
179
|
-
src = ROOT / 'dist' / 'regression' / f'{name}.pptx'
|
|
180
|
-
model = ROOT / 'assets' / 'examples' / f'{name}.model.json'
|
|
181
|
-
if not src.exists() or not model.exists():
|
|
182
|
-
print(' [SKIP] N7 字号越出比例尺(需先跑 regression)')
|
|
183
|
-
return True
|
|
184
|
-
out = OUT / 'N7-off-scale.pptx'
|
|
185
|
-
patched = False
|
|
186
|
-
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zout:
|
|
187
|
-
for item in zin.infolist():
|
|
188
|
-
data = zin.read(item.filename)
|
|
189
|
-
if not patched and item.filename == 'ppt/slides/slide4.xml':
|
|
190
|
-
text = data.decode('utf-8')
|
|
191
|
-
new = re.sub(r'sz="\d+"', 'sz="1234"', text, count=1) # 12.34pt:任何档位都没有
|
|
192
|
-
if new != text:
|
|
193
|
-
data = new.encode('utf-8')
|
|
194
|
-
patched = True
|
|
195
|
-
zout.writestr(item, data)
|
|
196
|
-
if not patched:
|
|
197
|
-
print(' [SKIP] N7 字号越出比例尺(未找到可改写的 sz 属性)')
|
|
198
|
-
return True
|
|
199
|
-
report = OUT / 'N7.json'
|
|
200
|
-
subprocess.run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(out),
|
|
201
|
-
'--strict', '--model=' + str(model), '--json-out', str(report)],
|
|
202
|
-
capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
203
|
-
try:
|
|
204
|
-
data = json.loads(report.read_text(encoding='utf-8'))
|
|
205
|
-
except (OSError, json.JSONDecodeError):
|
|
206
|
-
print(' [FAIL] N7 字号越出比例尺:校验报告不可解析')
|
|
207
|
-
return False
|
|
208
|
-
codes = {i.get('code') for i in (data.get('errors') or []) + (data.get('warnings') or [])}
|
|
209
|
-
ok = 'FONT_SIZE_OFF_SCALE' in codes
|
|
210
|
-
print(f' [{"OK" if ok else "FAIL"}] N7 字号越出比例尺')
|
|
211
|
-
if not ok:
|
|
212
|
-
print(f' 门禁未触发(期望 FONT_SIZE_OFF_SCALE,实际 {sorted(codes)[:6]})')
|
|
213
|
-
return ok
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def _title_pattern_case() -> bool:
|
|
217
|
-
"""N6:主题词式长标题必须被判为「无判断信号」,结论句标题必须放行。"""
|
|
218
|
-
lc = json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
|
|
219
|
-
pat = ((lc.get('contentQuality') or {}).get('researchTitle') or {}).get('judgmentPattern')
|
|
220
|
-
if not pat:
|
|
221
|
-
print(' [SKIP] N6 行动标题判断词(未配置 judgmentPattern)')
|
|
222
|
-
return True
|
|
223
|
-
topic = ['智能体平台的应用架构与技术选型说明', '关于数据治理需求的整体情况介绍',
|
|
224
|
-
'企业知识库建设的相关背景与范围', '平台能力地图与模块组成一览表',
|
|
225
|
-
'项目实施过程中的应急预案汇总', '面向未来的技术路线图规划说明']
|
|
226
|
-
claim = ['投入产出比在第 3 季度首次转正', '三成团队已把智能体纳入日常流程',
|
|
227
|
-
'数据质量是当前最大的落地瓶颈', '推理成本一年内下降 62%',
|
|
228
|
-
'从试点到规模化:关键卡在治理', '自建方案的总成本高于采购方案']
|
|
229
|
-
leaked = [t for t in topic if re.search(pat, t)]
|
|
230
|
-
hurt = [t for t in claim if not re.search(pat, t)]
|
|
231
|
-
ok = not leaked and not hurt
|
|
232
|
-
print(f' [{"OK" if ok else "FAIL"}] N6 行动标题判断词鉴别力'
|
|
233
|
-
f'(漏放主题词 {len(leaked)}/{len(topic)} · 误伤结论句 {len(hurt)}/{len(claim)})')
|
|
234
|
-
for t in (leaked + hurt)[:3]:
|
|
235
|
-
print(f' {t}')
|
|
236
|
-
return ok
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def _chart_data_case() -> bool:
|
|
240
|
-
"""N5:篡改模型里的图表数值 → cross_verify 的数值核对必须报不符。"""
|
|
241
|
-
name = '2026-09-09-research-mckinsey'
|
|
242
|
-
pptx = ROOT / 'dist' / 'regression' / f'{name}.pptx'
|
|
243
|
-
mp = ROOT / 'assets' / 'examples' / f'{name}.model.json'
|
|
244
|
-
if not pptx.exists() or not mp.exists():
|
|
245
|
-
print(' [SKIP] N5 图表数值篡改(需先跑 regression)')
|
|
246
|
-
return True
|
|
247
|
-
sys.path.insert(0, str(ROOT / 'scripts'))
|
|
248
|
-
try:
|
|
249
|
-
from pptx import Presentation # noqa: PLC0415
|
|
250
|
-
import cross_verify as CV # noqa: PLC0415
|
|
251
|
-
except ImportError:
|
|
252
|
-
print(' [SKIP] N5 图表数值篡改(python-pptx 未安装)')
|
|
253
|
-
return True
|
|
254
|
-
model = json.loads(mp.read_text(encoding='utf-8'))
|
|
255
|
-
touched = False
|
|
256
|
-
for sec in (model.get('sections') or []):
|
|
257
|
-
c = CV._model_chart(sec) if isinstance(sec, dict) else None
|
|
258
|
-
if c and isinstance(c.get('values'), list) and c['values']:
|
|
259
|
-
c['values'] = [(v + 777) if isinstance(v, (int, float)) else v for v in c['values']]
|
|
260
|
-
touched = True
|
|
261
|
-
break
|
|
262
|
-
if not touched:
|
|
263
|
-
print(' [SKIP] N5 图表数值篡改(基准模型无可改图表)')
|
|
264
|
-
return True
|
|
265
|
-
issues = CV.chart_data_verify(Presentation(str(pptx)), model, name)
|
|
266
|
-
ok = bool(issues)
|
|
267
|
-
print(f' [{"OK" if ok else "FAIL"}] N5 模型图表数值被篡改')
|
|
268
|
-
if ok:
|
|
269
|
-
print(f' 抓到: {issues[0][:120]}')
|
|
270
|
-
else:
|
|
271
|
-
print(' 门禁未触发(数值核对形同虚设)')
|
|
272
|
-
return ok
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def _pptx_notes_case() -> bool:
|
|
276
|
-
"""N4:剥离演讲者备注 → dataTable=notes 的图表数据不可追溯,必须被 strict 抓住。"""
|
|
277
|
-
name = '2026-09-09-research-mckinsey'
|
|
278
|
-
src = ROOT / 'dist' / 'regression' / f'{name}.pptx'
|
|
279
|
-
model = ROOT / 'assets' / 'examples' / f'{name}.model.json'
|
|
280
|
-
if not src.exists() or not model.exists():
|
|
281
|
-
print(' [SKIP] N4 备注剥离(需先跑 regression 生成 PPTX)')
|
|
282
|
-
return True
|
|
283
|
-
stripped = OUT / 'N4-no-notes.pptx'
|
|
284
|
-
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(stripped, 'w', zipfile.ZIP_DEFLATED) as zout:
|
|
285
|
-
for item in zin.infolist():
|
|
286
|
-
if item.filename.startswith('ppt/notesSlides/'):
|
|
287
|
-
continue
|
|
288
|
-
zout.writestr(item, zin.read(item.filename))
|
|
289
|
-
report = OUT / 'N4.json'
|
|
290
|
-
subprocess.run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(stripped),
|
|
291
|
-
'--strict', '--model=' + str(model), '--json-out', str(report)],
|
|
292
|
-
capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
293
|
-
try:
|
|
294
|
-
data = json.loads(report.read_text(encoding='utf-8'))
|
|
295
|
-
except (OSError, json.JSONDecodeError):
|
|
296
|
-
print(' [FAIL] N4 备注剥离:校验报告不可解析')
|
|
297
|
-
return False
|
|
298
|
-
codes = {i.get('code') for i in (data.get('errors') or []) + (data.get('warnings') or [])}
|
|
299
|
-
ok = 'MODEL_CHART_NOTES_MISSING' in codes
|
|
300
|
-
print(f' [{"OK" if ok else "FAIL"}] N4 演讲者备注被剥离')
|
|
301
|
-
if not ok:
|
|
302
|
-
print(f' 门禁未触发(期望 MODEL_CHART_NOTES_MISSING,实际 {sorted(codes)[:6]})')
|
|
303
|
-
return ok
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if __name__ == '__main__':
|
|
307
|
-
sys.exit(main())
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 门禁反向验证(故障注入)
|
|
4
|
+
|
|
5
|
+
校验器最容易退化成「只会说 OK 的摆设」。本脚本往合格产物里**注入已知缺陷**,
|
|
6
|
+
断言对应门禁必须报错——门禁抓不到即视为失败。
|
|
7
|
+
|
|
8
|
+
覆盖:
|
|
9
|
+
N1 Exhibit 框漏编号(旧实现只校验已编号者的连续性 → 静默通过)
|
|
10
|
+
N2 参考资料两位数编号错配(旧正则 `ref-\\d` 两侧同时落空 → 静默通过)
|
|
11
|
+
N3 参考资料编号跳号
|
|
12
|
+
N4 PPTX 演讲者备注被剔离(dataTable=notes 声称数据入备注,不核对就只是一句承诺)
|
|
13
|
+
N5 模型图表数值篡改(cross_verify 数值核对)
|
|
14
|
+
N6 行动标题判断词鉴别力(正则本身)
|
|
15
|
+
N7 PPTX 字号越出比例尺
|
|
16
|
+
N8 data-chart 未登记类型(登记表白名单)
|
|
17
|
+
N9 图表多样性塌陷(全篇压成同型 → 不同类型数 < 模式下限)
|
|
18
|
+
N10 图片外链(零外链铁律)
|
|
19
|
+
N11 待核实有标色无图例(.tbd 必须配 .tbd-legend/.flagbar)
|
|
20
|
+
N12 模型 theme 与 data-theme 矛盾
|
|
21
|
+
N13 模型 mode 与 data-mode 矛盾
|
|
22
|
+
N14 内部锚点断裂(链接目标 id 不存在)
|
|
23
|
+
|
|
24
|
+
用法: python scripts/negative_tests.py
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
import zipfile
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
41
|
+
OUT = ROOT / 'dist' / 'negtest'
|
|
42
|
+
PY = sys.executable
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_validate(path: Path) -> tuple[int, str]:
|
|
46
|
+
r = subprocess.run([PY, str(ROOT / 'scripts' / 'validate_report.py'), str(path), '--strict'],
|
|
47
|
+
capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
48
|
+
return r.returncode, (r.stdout or '') + (r.stderr or '')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def expect_fail(name: str, path: Path, keyword: str) -> bool:
|
|
52
|
+
code, out = run_validate(path)
|
|
53
|
+
# --strict 下 WARN 同样计失败(退出码 1),故 [WARN] 行也算「门禁抓到」
|
|
54
|
+
hit = [ln for ln in out.splitlines()
|
|
55
|
+
if keyword in ln and ('[FAIL]' in ln or '[WARN]' in ln)]
|
|
56
|
+
ok = code != 0 and bool(hit)
|
|
57
|
+
print(f' [{"OK" if ok else "FAIL"}] {name}')
|
|
58
|
+
if hit:
|
|
59
|
+
print(f' 抓到: {hit[0].strip()[:120]}')
|
|
60
|
+
elif not ok:
|
|
61
|
+
print(f' 门禁未触发(期望 FAIL 含「{keyword}」)')
|
|
62
|
+
return ok
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> int:
|
|
66
|
+
base = ROOT / 'assets' / 'examples' / '2026-09-09-research-mckinsey.html'
|
|
67
|
+
if not base.exists():
|
|
68
|
+
print(f'SKIP:基准示例缺失 {base}')
|
|
69
|
+
return 0
|
|
70
|
+
txt = base.read_text(encoding='utf-8')
|
|
71
|
+
OUT.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
code, out = run_validate(base)
|
|
74
|
+
if code != 0:
|
|
75
|
+
print('[FAIL] 基准示例本身未通过 strict,反向验证失去意义')
|
|
76
|
+
return 1
|
|
77
|
+
print('基准示例 strict 通过,开始故障注入:')
|
|
78
|
+
|
|
79
|
+
cases: list[tuple[str, str, str]] = []
|
|
80
|
+
|
|
81
|
+
# N1 抹掉一个 Exhibit 编号(框仍在)
|
|
82
|
+
cases.append(('N1 Exhibit 框漏编号',
|
|
83
|
+
txt.replace('class="exhibit__no"', 'class="exhibit__no--x"', 1),
|
|
84
|
+
'Exhibit 框均已编号'))
|
|
85
|
+
|
|
86
|
+
# N2 参考资料改成两位数编号,与正文引用错配
|
|
87
|
+
cases.append(('N2 引用编号两位数错配',
|
|
88
|
+
re.sub(r'id="ref-1"', 'id="ref-10"', txt, count=1),
|
|
89
|
+
'双向对齐'))
|
|
90
|
+
|
|
91
|
+
# N3 编号跳号(两侧同时改,集合仍对齐,但 1..N 不连续)
|
|
92
|
+
t3 = txt.replace('ref-2"', 'ref-9"')
|
|
93
|
+
cases.append(('N3 参考资料编号跳号', t3, '编号连续'))
|
|
94
|
+
|
|
95
|
+
# N8 data-chart 用了登记表外的类型
|
|
96
|
+
cases.append(('N8 图表类型未登记',
|
|
97
|
+
txt.replace('data-chart="hbar"', 'data-chart="hbarx"', 1),
|
|
98
|
+
'登记表内'))
|
|
99
|
+
|
|
100
|
+
# N9 图表多样性塌陷(全部压成 bar → 不同类型数 1 < research 下限 6)
|
|
101
|
+
t9 = txt
|
|
102
|
+
for ct in ('hbar', 'donut', 'waterfall', 'sankey', 'treemap', 'boxplot',
|
|
103
|
+
'network', 'marimekko', 'streamgraph', 'gantt', 'rose', 'candlestick'):
|
|
104
|
+
t9 = t9.replace(f'data-chart="{ct}"', 'data-chart="bar"')
|
|
105
|
+
cases.append(('N9 图表多样性塌陷', t9, '图表多样性'))
|
|
106
|
+
|
|
107
|
+
# N10 外链图片(零外链铁律)
|
|
108
|
+
cases.append(('N10 图片外链',
|
|
109
|
+
txt.replace('</body>',
|
|
110
|
+
'<img src="https://example.com/x.png" alt="外链注入"></body>', 1),
|
|
111
|
+
'图片源无外链'))
|
|
112
|
+
|
|
113
|
+
# N11 待核实有标色无图例(改名不得包含原串——校验是子串判断)
|
|
114
|
+
t11 = txt.replace('tbd-legend', 'tbd-leg').replace('flagbar', 'flag-bar')
|
|
115
|
+
cases.append(('N11 待核实无图例', t11, '待核实标注'))
|
|
116
|
+
|
|
117
|
+
# N12 模型 theme 与页面 data-theme 矛盾
|
|
118
|
+
cases.append(('N12 模型主题不一致',
|
|
119
|
+
txt.replace('"theme": "light"', '"theme": "dark"', 1),
|
|
120
|
+
'data-theme 一致'))
|
|
121
|
+
|
|
122
|
+
# N13 模型 mode 与页面 data-mode 矛盾
|
|
123
|
+
cases.append(('N13 模型模式不一致',
|
|
124
|
+
txt.replace('"mode": "research"', '"mode": "presentation"', 1),
|
|
125
|
+
'data-mode 一致'))
|
|
126
|
+
|
|
127
|
+
# N14 内部锚点断裂(链接目标 id 不存在)
|
|
128
|
+
cases.append(('N14 锚点断裂',
|
|
129
|
+
txt.replace('href="#s1"', 'href="#s404"'),
|
|
130
|
+
'锚点闭环'))
|
|
131
|
+
|
|
132
|
+
# N15 HTML 标签泄漏进可见文本(截图级:转义标签当字面量显示)
|
|
133
|
+
cases.append(('N15 标签泄漏',
|
|
134
|
+
txt.replace('</p>',
|
|
135
|
+
' <a class="cite" href="#ref-1">[1]</a>。</p>', 1),
|
|
136
|
+
'HTML_TAG_IN_TEXT'))
|
|
137
|
+
|
|
138
|
+
# N16 极偏 donut(0.5% vs 99.5% 应改 KPI,禁环图)
|
|
139
|
+
t16 = txt
|
|
140
|
+
if '"type": "donut"' in t16 or '"type":"donut"' in t16:
|
|
141
|
+
t16 = re.sub(
|
|
142
|
+
r'("type"\s*:\s*"donut"[\s\S]{0,200}?"values"\s*:\s*\[)[^\]]*(\])',
|
|
143
|
+
r'\g<1>0.5, 99.5\g<2>', t16, count=1)
|
|
144
|
+
cases.append(('N16 极偏 donut', t16, 'CHART_SKEW'))
|
|
145
|
+
|
|
146
|
+
# N17 模型字段夹带 HTML 标签(extract 净化 + validate 扫描 REPORT_MODEL)
|
|
147
|
+
t17 = re.sub(
|
|
148
|
+
r'("soWhat"\s*:\s*")',
|
|
149
|
+
r'\g<1><a class=\\"cite\\" href=\\"#ref-1\\">[1]</a> ',
|
|
150
|
+
txt, count=1)
|
|
151
|
+
if t17 == txt:
|
|
152
|
+
t17 = txt.replace(
|
|
153
|
+
'"footnote":',
|
|
154
|
+
'"footnote": "<strong>x</strong> ', 1)
|
|
155
|
+
cases.append(('N17 模型夹带标签', t17, 'HTML_TAG_IN_TEXT'))
|
|
156
|
+
|
|
157
|
+
ok = True
|
|
158
|
+
for name, content, keyword in cases:
|
|
159
|
+
if content == txt:
|
|
160
|
+
print(f' [SKIP] {name}(基准示例无对应结构)')
|
|
161
|
+
continue
|
|
162
|
+
p = OUT / (name.split()[0] + '.html')
|
|
163
|
+
p.write_text(content, encoding='utf-8')
|
|
164
|
+
ok = expect_fail(name, p, keyword) and ok
|
|
165
|
+
|
|
166
|
+
ok = _pptx_notes_case() and ok
|
|
167
|
+
ok = _chart_data_case() and ok
|
|
168
|
+
ok = _title_pattern_case() and ok
|
|
169
|
+
ok = _font_scale_case() and ok
|
|
170
|
+
|
|
171
|
+
print('反向验证通过:注入的缺陷都被门禁抓住。' if ok
|
|
172
|
+
else '反向验证失败:存在抓不到的缺陷,门禁有假阴性。')
|
|
173
|
+
return 0 if ok else 1
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _font_scale_case() -> bool:
|
|
177
|
+
"""N7:往 PPTX 里塞一个比例尺外的字号 → FONT_SIZE_OFF_SCALE 必须报。"""
|
|
178
|
+
name = '2026-09-09-research-mckinsey'
|
|
179
|
+
src = ROOT / 'dist' / 'regression' / f'{name}.pptx'
|
|
180
|
+
model = ROOT / 'assets' / 'examples' / f'{name}.model.json'
|
|
181
|
+
if not src.exists() or not model.exists():
|
|
182
|
+
print(' [SKIP] N7 字号越出比例尺(需先跑 regression)')
|
|
183
|
+
return True
|
|
184
|
+
out = OUT / 'N7-off-scale.pptx'
|
|
185
|
+
patched = False
|
|
186
|
+
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zout:
|
|
187
|
+
for item in zin.infolist():
|
|
188
|
+
data = zin.read(item.filename)
|
|
189
|
+
if not patched and item.filename == 'ppt/slides/slide4.xml':
|
|
190
|
+
text = data.decode('utf-8')
|
|
191
|
+
new = re.sub(r'sz="\d+"', 'sz="1234"', text, count=1) # 12.34pt:任何档位都没有
|
|
192
|
+
if new != text:
|
|
193
|
+
data = new.encode('utf-8')
|
|
194
|
+
patched = True
|
|
195
|
+
zout.writestr(item, data)
|
|
196
|
+
if not patched:
|
|
197
|
+
print(' [SKIP] N7 字号越出比例尺(未找到可改写的 sz 属性)')
|
|
198
|
+
return True
|
|
199
|
+
report = OUT / 'N7.json'
|
|
200
|
+
subprocess.run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(out),
|
|
201
|
+
'--strict', '--model=' + str(model), '--json-out', str(report)],
|
|
202
|
+
capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
203
|
+
try:
|
|
204
|
+
data = json.loads(report.read_text(encoding='utf-8'))
|
|
205
|
+
except (OSError, json.JSONDecodeError):
|
|
206
|
+
print(' [FAIL] N7 字号越出比例尺:校验报告不可解析')
|
|
207
|
+
return False
|
|
208
|
+
codes = {i.get('code') for i in (data.get('errors') or []) + (data.get('warnings') or [])}
|
|
209
|
+
ok = 'FONT_SIZE_OFF_SCALE' in codes
|
|
210
|
+
print(f' [{"OK" if ok else "FAIL"}] N7 字号越出比例尺')
|
|
211
|
+
if not ok:
|
|
212
|
+
print(f' 门禁未触发(期望 FONT_SIZE_OFF_SCALE,实际 {sorted(codes)[:6]})')
|
|
213
|
+
return ok
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _title_pattern_case() -> bool:
|
|
217
|
+
"""N6:主题词式长标题必须被判为「无判断信号」,结论句标题必须放行。"""
|
|
218
|
+
lc = json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
|
|
219
|
+
pat = ((lc.get('contentQuality') or {}).get('researchTitle') or {}).get('judgmentPattern')
|
|
220
|
+
if not pat:
|
|
221
|
+
print(' [SKIP] N6 行动标题判断词(未配置 judgmentPattern)')
|
|
222
|
+
return True
|
|
223
|
+
topic = ['智能体平台的应用架构与技术选型说明', '关于数据治理需求的整体情况介绍',
|
|
224
|
+
'企业知识库建设的相关背景与范围', '平台能力地图与模块组成一览表',
|
|
225
|
+
'项目实施过程中的应急预案汇总', '面向未来的技术路线图规划说明']
|
|
226
|
+
claim = ['投入产出比在第 3 季度首次转正', '三成团队已把智能体纳入日常流程',
|
|
227
|
+
'数据质量是当前最大的落地瓶颈', '推理成本一年内下降 62%',
|
|
228
|
+
'从试点到规模化:关键卡在治理', '自建方案的总成本高于采购方案']
|
|
229
|
+
leaked = [t for t in topic if re.search(pat, t)]
|
|
230
|
+
hurt = [t for t in claim if not re.search(pat, t)]
|
|
231
|
+
ok = not leaked and not hurt
|
|
232
|
+
print(f' [{"OK" if ok else "FAIL"}] N6 行动标题判断词鉴别力'
|
|
233
|
+
f'(漏放主题词 {len(leaked)}/{len(topic)} · 误伤结论句 {len(hurt)}/{len(claim)})')
|
|
234
|
+
for t in (leaked + hurt)[:3]:
|
|
235
|
+
print(f' {t}')
|
|
236
|
+
return ok
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _chart_data_case() -> bool:
|
|
240
|
+
"""N5:篡改模型里的图表数值 → cross_verify 的数值核对必须报不符。"""
|
|
241
|
+
name = '2026-09-09-research-mckinsey'
|
|
242
|
+
pptx = ROOT / 'dist' / 'regression' / f'{name}.pptx'
|
|
243
|
+
mp = ROOT / 'assets' / 'examples' / f'{name}.model.json'
|
|
244
|
+
if not pptx.exists() or not mp.exists():
|
|
245
|
+
print(' [SKIP] N5 图表数值篡改(需先跑 regression)')
|
|
246
|
+
return True
|
|
247
|
+
sys.path.insert(0, str(ROOT / 'scripts'))
|
|
248
|
+
try:
|
|
249
|
+
from pptx import Presentation # noqa: PLC0415
|
|
250
|
+
import cross_verify as CV # noqa: PLC0415
|
|
251
|
+
except ImportError:
|
|
252
|
+
print(' [SKIP] N5 图表数值篡改(python-pptx 未安装)')
|
|
253
|
+
return True
|
|
254
|
+
model = json.loads(mp.read_text(encoding='utf-8'))
|
|
255
|
+
touched = False
|
|
256
|
+
for sec in (model.get('sections') or []):
|
|
257
|
+
c = CV._model_chart(sec) if isinstance(sec, dict) else None
|
|
258
|
+
if c and isinstance(c.get('values'), list) and c['values']:
|
|
259
|
+
c['values'] = [(v + 777) if isinstance(v, (int, float)) else v for v in c['values']]
|
|
260
|
+
touched = True
|
|
261
|
+
break
|
|
262
|
+
if not touched:
|
|
263
|
+
print(' [SKIP] N5 图表数值篡改(基准模型无可改图表)')
|
|
264
|
+
return True
|
|
265
|
+
issues = CV.chart_data_verify(Presentation(str(pptx)), model, name)
|
|
266
|
+
ok = bool(issues)
|
|
267
|
+
print(f' [{"OK" if ok else "FAIL"}] N5 模型图表数值被篡改')
|
|
268
|
+
if ok:
|
|
269
|
+
print(f' 抓到: {issues[0][:120]}')
|
|
270
|
+
else:
|
|
271
|
+
print(' 门禁未触发(数值核对形同虚设)')
|
|
272
|
+
return ok
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _pptx_notes_case() -> bool:
|
|
276
|
+
"""N4:剥离演讲者备注 → dataTable=notes 的图表数据不可追溯,必须被 strict 抓住。"""
|
|
277
|
+
name = '2026-09-09-research-mckinsey'
|
|
278
|
+
src = ROOT / 'dist' / 'regression' / f'{name}.pptx'
|
|
279
|
+
model = ROOT / 'assets' / 'examples' / f'{name}.model.json'
|
|
280
|
+
if not src.exists() or not model.exists():
|
|
281
|
+
print(' [SKIP] N4 备注剥离(需先跑 regression 生成 PPTX)')
|
|
282
|
+
return True
|
|
283
|
+
stripped = OUT / 'N4-no-notes.pptx'
|
|
284
|
+
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(stripped, 'w', zipfile.ZIP_DEFLATED) as zout:
|
|
285
|
+
for item in zin.infolist():
|
|
286
|
+
if item.filename.startswith('ppt/notesSlides/'):
|
|
287
|
+
continue
|
|
288
|
+
zout.writestr(item, zin.read(item.filename))
|
|
289
|
+
report = OUT / 'N4.json'
|
|
290
|
+
subprocess.run([PY, str(ROOT / 'scripts' / 'validate_pptx.py'), str(stripped),
|
|
291
|
+
'--strict', '--model=' + str(model), '--json-out', str(report)],
|
|
292
|
+
capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
293
|
+
try:
|
|
294
|
+
data = json.loads(report.read_text(encoding='utf-8'))
|
|
295
|
+
except (OSError, json.JSONDecodeError):
|
|
296
|
+
print(' [FAIL] N4 备注剥离:校验报告不可解析')
|
|
297
|
+
return False
|
|
298
|
+
codes = {i.get('code') for i in (data.get('errors') or []) + (data.get('warnings') or [])}
|
|
299
|
+
ok = 'MODEL_CHART_NOTES_MISSING' in codes
|
|
300
|
+
print(f' [{"OK" if ok else "FAIL"}] N4 演讲者备注被剥离')
|
|
301
|
+
if not ok:
|
|
302
|
+
print(f' 门禁未触发(期望 MODEL_CHART_NOTES_MISSING,实际 {sorted(codes)[:6]})')
|
|
303
|
+
return ok
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == '__main__':
|
|
307
|
+
sys.exit(main())
|