@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.
- package/CHANGELOG.md +139 -123
- package/README.md +126 -103
- package/bin/tms-skills.js +160 -160
- package/package.json +48 -48
- package/top-ppt-html/README.md +254 -253
- package/top-ppt-html/SKILL.md +111 -111
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3873 -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 +3873 -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 +4272 -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 +4272 -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 +4272 -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 +5474 -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 +5474 -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 +5474 -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 +5474 -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 +559 -589
- package/top-ppt-html/assets/templates/architecture.html +3675 -3728
- package/top-ppt-html/assets/templates/engine.css +787 -840
- package/top-ppt-html/assets/templates/presentation.html +3685 -3738
- package/top-ppt-html/assets/templates/research.html +3964 -4017
- package/top-ppt-html/assets/templates/ui.js +520 -520
- 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 -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 +175 -0
- package/top-ppt-html/package.json +30 -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 +471 -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 +336 -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 +293 -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 +3309 -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 +307 -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,301 +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())
|
|
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())
|