@topmindspace/tms-skills 0.1.3 → 0.1.4
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 +24 -0
- package/README.md +4 -4
- package/package.json +2 -2
- package/top-ppt-html/README.md +5 -5
- package/top-ppt-html/SKILL.md +11 -11
- package/top-ppt-html/agents/openai.yaml +2 -2
- package/top-ppt-html/assets/pptx-export.js +69 -25
- package/top-ppt-html/assets/style-gallery.html +1 -1
- package/top-ppt-html/assets/templates/architecture.html +69 -25
- package/top-ppt-html/assets/templates/presentation.html +69 -25
- package/top-ppt-html/assets/templates/research.html +69 -25
- package/top-ppt-html/package.json +2 -2
- package/top-ppt-html/references/default-surface.md +1 -1
- package/top-ppt-html/references/modes.md +1 -1
- package/top-ppt-html/references/playbook.md +9 -8
- package/top-ppt-html/references/pptx-export.md +1 -1
- package/top-ppt-html/references/presentation-craft.md +3 -2
- package/top-ppt-html/scripts/audit_skill.py +11 -0
- package/top-ppt-html/scripts/build_pptx.js +24 -10
- package/top-ppt-html/scripts/quality_gate.py +30 -19
- package/top-ppt-html/scripts/test_feedback_gates.py +180 -0
- package/top-ppt-html/scripts/validate_pptx.py +157 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "top-ppt-html",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "TopPPT HTML
|
|
5
|
+
"description": "TopPPT HTML:优雅大气的正式场合演讲/汇报甲板。单文件 HTML + 版式保真可编辑 PPTX;布局骨架 P1–P12;模型单写;strict 0/0 交付。",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
```
|
|
54
54
|
scaffold_report → 只填 REPORT_MODEL → render_from_model --inplace
|
|
55
55
|
→ validate_report --strict # presentation 自动 --layout-qa
|
|
56
|
-
→ quality_gate --deliver
|
|
56
|
+
→ quality_gate --deliver →(用户要 PPTX 时)B 通道 build_pptx → validate_pptx
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
## 长文与密度(Mode A)
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
## 模式 A · 演示汇报 `presentation`(assets/templates/presentation.html)
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
正式场合演讲甲板:优雅、大气、一屏一主张;3 秒抓住结论。工艺 → `presentation-craft.md`;默认面 → `default-surface.md`。**非**咨询密排 dump(那是 Mode B)。
|
|
78
78
|
|
|
79
79
|
要点回顾:
|
|
80
80
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 作战手册 · Playbook(唯一常读入口 · L1)
|
|
2
2
|
|
|
3
3
|
> **每次生成都读这一份,通常就够了。**
|
|
4
|
-
>
|
|
5
|
-
>
|
|
4
|
+
> 决策层:定模式、选页型、选版式组合、选图表、定配色、跑校验——服务**优雅、大气、正式场合**的演讲/汇报甲板(Mode A 默认气质;B/C 按契约,勿把咨询 dump 当 A 默认)。
|
|
5
|
+
> **代码、几何、穷举表不在本文件**——按 `SKILL.md` L2 路由命中再读,**读完即停**;整读大 L2 = **FAIL**。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
Gate 0 参考图 → 六项问询(1 轮)→ 最小大纲(outline-design「轻量最小集」)→ 1 张规划卡 → scaffold_report.py
|
|
47
47
|
→ **只填 REPORT_MODEL** → render_from_model.py --inplace → validate_report.py --strict → 交付
|
|
48
48
|
```
|
|
49
|
-
(Fast Mode 跳过 Gate 0
|
|
49
|
+
(Fast Mode 跳过 Gate 0/六项,仍须最小大纲;默认 **HTML-only**,PPTX 显式 opt-in 走 B 通道;**禁止**手改 HTML 正文与模型双写。)
|
|
50
50
|
|
|
51
51
|
**完整路径**(任一命中)——研究 ≥20 页 / 演示 ≥12 页 / 材料量大且杂 / 含未敲定关键判断 / 用户要看框架。
|
|
52
52
|
```
|
|
@@ -188,10 +188,11 @@ python scripts/validate_pptx.py 2026-09-15-主题.pptx --strict --model=….mode
|
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
**交付前必过**:`validate_report.py --strict` 0/0(**Mode A / presentation 隐含 `--layout-qa`**;research/architecture 不强制);含 PPTX 时 `validate_pptx.py --strict --model=` 0/0。
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
**PPTX 通道**:默认交付只跑 **B 通道**(`build_pptx.js`);**A 通道**(`pptx-export.js` / `gen_channel_a.js`)仅预览与 `cross_verify` / 回归——**勿把 A 产物当交付**。
|
|
192
|
+
**推荐一键门禁**:`python scripts/quality_gate.py 报告.html [--pptx x.pptx --model x.model.json]`(HTML/PPTX/evals **并行**;presentation 附 layout-qa;`--deliver` 出七要素,勿手拼)。
|
|
193
|
+
**A/B 全文对等**:`cross_verify.py` 默认 SKIP,仅 `--full-ab` / `regression.py --full-ab` 启用。
|
|
193
194
|
**PPTX 冒烟**:`bash scripts/smoke_pptx.sh`(extract_model → build_pptx → validate_pptx --strict)。
|
|
194
|
-
**失败自修复**:按输出逐条修 →
|
|
195
|
+
**失败自修复**:按输出逐条修 → 重跑至 0/0;顺序见 `failure-modes.md`。
|
|
195
196
|
|
|
196
197
|
---
|
|
197
198
|
|
|
@@ -200,7 +201,7 @@ python scripts/validate_pptx.py 2026-09-15-主题.pptx --strict --model=….mode
|
|
|
200
201
|
> 常读仍为入口两份;演示路径可加 **L1.5 工艺双件**:`default-surface.md` + `presentation-craft.md`。
|
|
201
202
|
> 默认生成面:`default-surface.md`(12 页型 + 8 图 + V1–V4)。Mode A 工艺:`presentation-craft.md`。插画:`illustration-layout.md`。
|
|
202
203
|
>
|
|
203
|
-
>
|
|
204
|
+
> **整读下列文件 = FAIL / 不合格**(强制 `extract_snippet.py`):`layouts-combo.md` · `components-atoms.md` · `charts-basic.md` · `charts-extended.md` · `content-rules.md` 全文 · `pptx-export.md` · 模式模板 HTML(走 `scaffold_report.py`)。
|
|
204
205
|
> 取码:`python scripts/extract_snippet.py --list` · `--task <名>` · `--chart <类型>` · `--page-type <页型>` · `--file components.md --section 46`(逻辑名自动路由)
|
|
205
206
|
|
|
206
207
|
| 任务 | 只读 |
|
|
@@ -218,5 +219,5 @@ python scripts/validate_pptx.py 2026-09-15-主题.pptx --strict --model=….mode
|
|
|
218
219
|
| 图标 / 语义速查 | `icons.md` · `--task icons` |
|
|
219
220
|
| 起骨架用节奏包 | `scaffold_report.py --list-types` → `--preset consulting` 等 |
|
|
220
221
|
|
|
221
|
-
**纪律**:L0+L1
|
|
222
|
+
**纪律**:L0+L1 默认全部所需(Mode A/Fast 可加 L1.5);命中才开 L2;**读完即执行,不预读**;大文件整读 = **FAIL**,一律 `extract_snippet.py`。页型槽位见 `layout-constants.json` → `layoutSlots`(双通道同源 IR)。
|
|
222
223
|
**维护者**:改常量/schema/引擎后必跑 `sync_runtime.py` → `audit_styles.py` → `audit_docs.py` → `audit_skill.py` → `build_examples.py` → `regression.py`。
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
- **页面预览**:收到/打开报告的人点 header「预览 PPTX」(快捷键 P)即可查看页序列,无需任何环境。预览模态与精导通道使用**同一序列化语义**(`slidesOf → slideXml`)——**版式与文本所见即所得**;带数据的原生图表在交付 PPTX 中为真 chart part(双击可编辑数据),预览侧为形状近似,**图表形态以精导产物为准**。**页面不直接导出文件**:浏览器端跑不了 strict 质检硬门禁,达不到交付质量;预览模态常显**可复制 AI 提示词**,用户想获得 PPTX 时复制给 AI,由智能体走精导通道生成(`?` 帮助弹窗 / 快捷键 H 有同样说明)。
|
|
24
|
-
-
|
|
24
|
+
- **智能体精导(B 通道 · 唯一交付)**:`extract_model.py` → `build_pptx.js` → `validate_pptx.py --strict` 0/0。用户选「HTML+PPTX」或事后要 PPT 时才跑;**不要**用 A 通道(`pptx-export.js` / `gen_channel_a.js`)产物交付——A 仅页面预览与回归双裁判。
|
|
25
25
|
- **双单源**:页面几何/三模式独立比例尺(`typeScale` 基准 + `modeTypeScale` 模式取值)/页型几何/9 风格 token 全在 `scripts/layout-constants.json`;**页型 DSL schema(字段/必填/适用模式)在 `scripts/model-schema.json`**。通道 B 直接 `require`;页面运行时由 `sync_runtime.py` 注入(同时注入三模板的引擎/UI/运行时内联副本)。**改常量只改 JSON,改 schema 只改 schema JSON,然后跑 `python scripts/sync_runtime.py`**。
|
|
26
26
|
|
|
27
27
|
### 会场与字号(venue · P1-4)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **何时读**:Mode A / 正式演示 / 路演 / 演讲 / Fast 演示路径。与 `default-surface.md` 同级常读。
|
|
4
4
|
> **何时不读**:Mode B 密排、Mode C 架构深读(能力保留,不反向污染 A)。
|
|
5
|
-
>
|
|
5
|
+
> **定位**:优雅 · 美观 · 大气;正式场合演讲友好;版式/排版/色彩/内容组织优先;图标/图表克制配文——非 gadget deck、非咨询 dump。
|
|
6
6
|
|
|
7
7
|
## 铁律速查(Hard rules)
|
|
8
8
|
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
validate_report.py --strict # presentation 自动隐含 --layout-qa
|
|
35
|
-
quality_gate.py … --deliver #
|
|
35
|
+
quality_gate.py … --deliver # HTML/PPTX/evals 并行;同口径
|
|
36
|
+
# PPTX 交付仅 B 通道 build_pptx.js(A 通道不交付)
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
检查:V 契约 · 简单全幅 · 骨架连用 · 截断/溢出未拆页 · 半空卡 · 对齐节奏 · 主张标题 WARN · 核图多样性 · WCAG。
|
|
@@ -210,6 +210,17 @@ def main() -> int:
|
|
|
210
210
|
'SKILL.md 未声明交互轮次上限', '已声明(≤3 轮)')
|
|
211
211
|
chk('⑤ 必读文件预算', bool(re.search(r'必读.{0,6}上限|必读文件数|只读当前需要的|不预读', sk)),
|
|
212
212
|
'SKILL.md 未声明必读文件预算/不预读纪律', '已声明(不预读纪律)')
|
|
213
|
+
# ⑤b 反过读硬门禁文案(整读大 L2 = FAIL)+ B 通道交付口径
|
|
214
|
+
ok_snip = bool(re.search(r'extract_snippet', sk)) and bool(
|
|
215
|
+
re.search(r'整读.{0,40}(?:FAIL|不合格)|(?:FAIL|不合格).{0,40}整读', sk))
|
|
216
|
+
chk('⑤ extract_snippet 整读=FAIL', ok_snip,
|
|
217
|
+
'SKILL.md 须同时声明 extract_snippet 与「整读…FAIL/不合格」',
|
|
218
|
+
'已声明 FAIL 口径')
|
|
219
|
+
ok_b = bool(re.search(r'B\s*通道|build_pptx', sk)) and bool(
|
|
220
|
+
re.search(r'A\s*通道|pptx-export|cross_verify', sk))
|
|
221
|
+
chk('⑤ B 通道交付 / A 预览', ok_b,
|
|
222
|
+
'SKILL.md 须区分 B 通道交付与 A 通道预览/cross_verify',
|
|
223
|
+
'已区分')
|
|
213
224
|
|
|
214
225
|
# ⑥ 引用完整性
|
|
215
226
|
missing: list[str] = []
|
|
@@ -174,6 +174,17 @@ function modeFloorPt() {
|
|
|
174
174
|
const f = FS_POLICY.floorPt[m];
|
|
175
175
|
return f != null ? f : 10;
|
|
176
176
|
}
|
|
177
|
+
function snapFont(pt, ladder, floor) {
|
|
178
|
+
/* Snap arbitrary pt to nearest ladder step (≥ floor). fitFont + callers (fz-1) must use this. */
|
|
179
|
+
const lad = (ladder || FONT_LADDER).filter(v => v >= (floor == null ? 0 : floor));
|
|
180
|
+
if (!lad.length) return floor == null ? pt : Math.max(floor, pt);
|
|
181
|
+
let best = lad[0], bd = Math.abs(lad[0] - pt);
|
|
182
|
+
for (let i = 1; i < lad.length; i++) {
|
|
183
|
+
const d = Math.abs(lad[i] - pt);
|
|
184
|
+
if (d < bd) { bd = d; best = lad[i]; }
|
|
185
|
+
}
|
|
186
|
+
return best;
|
|
187
|
+
}
|
|
177
188
|
function fitFont(items, widthIn, availIn, opts) {
|
|
178
189
|
opts = opts || {};
|
|
179
190
|
const floor = opts.floor != null ? opts.floor : modeFloorPt();
|
|
@@ -536,7 +547,7 @@ function imageLayoutShapes(s, layout, img, items, points, isPh, fit, y0, y1) {
|
|
|
536
547
|
const ptsI = points || [];
|
|
537
548
|
const rowHI = fitRowH(ih - 0.1, ptsI.length, IM.noteRowH || 0.62, 0.3);
|
|
538
549
|
const fzI = fitFont(ptsI.map(p => (p[0] || '') + (p[1] || '')), rwI - 0.3, ih, { max: 14, gapFactor: 0.6 });
|
|
539
|
-
const fz = sz(fzI), fzSm = sz(fzI - 1);
|
|
550
|
+
const fz = sz(fzI), fzSm = sz(snapFont(fzI - 1));
|
|
540
551
|
ptsI.forEach((pt, i) => {
|
|
541
552
|
const lyI = iy + i * rowHI;
|
|
542
553
|
s.addShape('rect', { x: rxI, y: lyI + rowHI / 2 - 0.06, w: 0.12, h: 0.12,
|
|
@@ -1490,8 +1501,11 @@ function infoStreamgraph(s, sec, y0, y1) {
|
|
|
1490
1501
|
totals.push(t);
|
|
1491
1502
|
}
|
|
1492
1503
|
const maxTot = Math.max.apply(null, totals.concat([1]));
|
|
1493
|
-
|
|
1494
|
-
const
|
|
1504
|
+
/* Reserve legend INSIDE the plot box — 6 series must not crush the annotation band. */
|
|
1505
|
+
const legH = Math.min(1.2, Math.max(0.28, series.length * 0.22 + 0.08));
|
|
1506
|
+
const plotBottom = P.y1 - legH;
|
|
1507
|
+
const plotH = Math.max(0.6, plotBottom - P.y0);
|
|
1508
|
+
const cyMid = P.y0 + plotH / 2;
|
|
1495
1509
|
const xAt = (i) => MX + (nPts > 1 ? i / (nPts - 1) : 0.5) * CW;
|
|
1496
1510
|
const yAt = (i) => cyMid - (totals[i] / maxTot) * plotH / 2;
|
|
1497
1511
|
const palette = [STYLE.accent, STYLE.faint, STYLE.body, STYLE.line, STYLE.soft, STYLE.surface];
|
|
@@ -1512,14 +1526,14 @@ function infoStreamgraph(s, sec, y0, y1) {
|
|
|
1512
1526
|
}
|
|
1513
1527
|
ribbonRects(s, pts, palette[si % palette.length]);
|
|
1514
1528
|
const lx = MX;
|
|
1515
|
-
s.addShape('rect', { x: lx, y:
|
|
1529
|
+
s.addShape('rect', { x: lx, y: plotBottom + 0.04 + si * 0.2, w: 0.12, h: 0.12,
|
|
1516
1530
|
fill: { color: palette[si % palette.length] }, line: { type: 'none' } });
|
|
1517
|
-
s.addText(String(se.name || ('系列' + (si + 1))), { x: lx + 0.16, y:
|
|
1531
|
+
s.addText(String(se.name || ('系列' + (si + 1))), { x: lx + 0.16, y: plotBottom + 0.02 + si * 0.2, w: 1.5, h: 0.2,
|
|
1518
1532
|
fontFace: STYLE.font, fontSize: sz(10), color: STYLE.body, valign: 'middle' });
|
|
1519
1533
|
base = upper;
|
|
1520
1534
|
});
|
|
1521
1535
|
labels.forEach((lb, i) => {
|
|
1522
|
-
s.addText(String(lb), { x: xAt(i) - 0.4, y:
|
|
1536
|
+
s.addText(String(lb), { x: xAt(i) - 0.4, y: plotBottom - 0.22, w: 0.8, h: 0.2, align: 'center',
|
|
1523
1537
|
fontFace: STYLE.font, fontSize: sz(9.5), color: STYLE.faint });
|
|
1524
1538
|
});
|
|
1525
1539
|
}
|
|
@@ -1745,8 +1759,8 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1745
1759
|
const iy = ireg.y;
|
|
1746
1760
|
const capH = img.caption ? 0.32 : 0;
|
|
1747
1761
|
const ih = Math.max(1.2, ireg.h);
|
|
1748
|
-
imageLayoutShapes(s, layout, img, items, sec.points || [], isPh, fit, iy, iy + ih);
|
|
1749
|
-
if (img.caption) s.addText(img.caption, { x: MX, y:
|
|
1762
|
+
const capYImg = imageLayoutShapes(s, layout, img, items, sec.points || [], isPh, fit, iy, iy + ih);
|
|
1763
|
+
if (img.caption) s.addText(img.caption, { x: MX, y: capYImg + 0.04, w: CW, h: capH,
|
|
1750
1764
|
fontFace: STYLE.font, fontSize: sz(10.5), color: STYLE.faint });
|
|
1751
1765
|
} else if (type === 'donut') {
|
|
1752
1766
|
const dcn = sec.chart || {};
|
|
@@ -2133,7 +2147,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2133
2147
|
const ly0 = bodyY, availP = bodyBottom - ly0;
|
|
2134
2148
|
const rowH = fitRowH(availP, pts.length, SP.rowH, 0.3);
|
|
2135
2149
|
const fzB = fitFont(pts.map(p => (p[0] || '') + (p[1] || '')), w - 0.4, availP, { max: 14, gapFactor: 0.6 });
|
|
2136
|
-
const fz = sz(fzB), fzSm = sz(fzB - 1);
|
|
2150
|
+
const fz = sz(fzB), fzSm = sz(snapFont(fzB - 1));
|
|
2137
2151
|
pts.forEach((pt, i) => {
|
|
2138
2152
|
const ly = ly0 + i * rowH;
|
|
2139
2153
|
s.addShape('rect', { x, y: ly + rowH / 2 - 0.06, w: 0.12, h: 0.12, fill: { color: STYLE.accent }, line: { type: 'none' } });
|
|
@@ -2277,7 +2291,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2277
2291
|
const rowH = fitRowH(avail, pts.length, P.rowH, 0.3);
|
|
2278
2292
|
const fzB = fitFont(pts.map(p => (p[0] || '') + (p[1] || '')), textW - 0.32, avail,
|
|
2279
2293
|
{ max: 15, gapFactor: 0.55 });
|
|
2280
|
-
const fz = sz(fzB), fzSm = sz(fzB - 1);
|
|
2294
|
+
const fz = sz(fzB), fzSm = sz(snapFont(fzB - 1));
|
|
2281
2295
|
pts.forEach((p, i) => {
|
|
2282
2296
|
const y = bodyY + i * rowH;
|
|
2283
2297
|
s.addShape('rect', { x: MX, y: y + rowH / 2 - P.markSize / 2, w: P.markSize, h: P.markSize,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
② 无 LLM 的 rubric 启发式(content/layout/chart/infographic/tone 五维)
|
|
17
17
|
③ --deliver:按 SKILL.md「交付说明」七要素(路径/字节数/模式/风格/篇幅/格式/校验+引用)
|
|
18
18
|
生成结构化交付说明——七要素缺一即 gate FAIL,杜绝手拼遗漏
|
|
19
|
-
|
|
19
|
+
交付时一键跑完;互不依赖的子进程门禁(HTML / PPTX / evals)并行执行以缩短墙钟。
|
|
20
20
|
"""
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
@@ -26,6 +26,7 @@ import re
|
|
|
26
26
|
import subprocess
|
|
27
27
|
import sys
|
|
28
28
|
import time
|
|
29
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
|
|
31
32
|
try:
|
|
@@ -242,33 +243,43 @@ def main() -> int:
|
|
|
242
243
|
print(f'交付质量门禁 · {html.name}')
|
|
243
244
|
print('-' * 60)
|
|
244
245
|
|
|
245
|
-
#
|
|
246
|
+
# ①–③ 互不依赖子进程并行(HTML strict / PPTX strict / evals)
|
|
246
247
|
head_snip = html.read_text(encoding='utf-8')[:2500]
|
|
247
248
|
mode_m = re.search(r'data-mode="([^"]+)"', head_snip)
|
|
248
249
|
mode = mode_m.group(1) if mode_m else 'presentation'
|
|
250
|
+
jobs: list[tuple[str, list[str], str | None]] = []
|
|
251
|
+
# (name, cmd, fixed_note) — fixed_note 非空则覆盖尾部摘要
|
|
249
252
|
vcmd = [sys.executable, str(ROOT / 'scripts' / 'validate_report.py'), str(html), '--strict']
|
|
250
253
|
if mode == 'presentation':
|
|
251
254
|
vcmd.append('--layout-qa')
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
tail = [ln for ln in out.splitlines() if ln.startswith('PASS ') or ln.startswith('结论')]
|
|
255
|
-
gate(gate_name, rc == 0, tail[-1] if tail else f'exit {rc}', el)
|
|
256
|
-
|
|
257
|
-
# ② PPTX strict(可选)
|
|
255
|
+
gate_html = 'validate_report --strict' + (' --layout-qa' if mode == 'presentation' else '')
|
|
256
|
+
jobs.append((gate_html, vcmd, None))
|
|
258
257
|
if args.pptx:
|
|
259
|
-
|
|
258
|
+
pcmd = [sys.executable, str(ROOT / 'scripts' / 'validate_pptx.py'), args.pptx, '--strict']
|
|
260
259
|
if args.model:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
gate('validate_pptx --strict', rc == 0, tail[-1] if tail else f'exit {rc}', el)
|
|
265
|
-
|
|
266
|
-
# ③ evals 确定性
|
|
267
|
-
cmd = [sys.executable, str(ROOT / 'evals' / 'run_evals.py'), '--score', str(html)]
|
|
260
|
+
pcmd += ['--model=' + args.model]
|
|
261
|
+
jobs.append(('validate_pptx --strict', pcmd, None))
|
|
262
|
+
ecmd = [sys.executable, str(ROOT / 'evals' / 'run_evals.py'), '--score', str(html)]
|
|
268
263
|
if args.trace:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
ecmd += ['--trace', args.trace]
|
|
265
|
+
jobs.append(('evals --score', ecmd, '确定性 + 效率检查'))
|
|
266
|
+
|
|
267
|
+
results: dict[str, tuple[int, str, float, str | None]] = {}
|
|
268
|
+
with ThreadPoolExecutor(max_workers=max(1, len(jobs))) as pool:
|
|
269
|
+
futs = {pool.submit(run, cmd): (name, note) for name, cmd, note in jobs}
|
|
270
|
+
for fut in as_completed(futs):
|
|
271
|
+
name, note = futs[fut]
|
|
272
|
+
rc, out, el = fut.result()
|
|
273
|
+
results[name] = (rc, out, el, note)
|
|
274
|
+
|
|
275
|
+
# 按 jobs 声明顺序输出(稳定可读;墙钟已并行)
|
|
276
|
+
for name, _cmd, _note in jobs:
|
|
277
|
+
rc, out, el, note = results[name]
|
|
278
|
+
if note is not None:
|
|
279
|
+
gate(name, rc == 0, note, el)
|
|
280
|
+
else:
|
|
281
|
+
tail = [ln for ln in out.splitlines() if ln.startswith('PASS ') or ln.startswith('结论')]
|
|
282
|
+
gate(name, rc == 0, tail[-1] if tail else f'exit {rc}', el)
|
|
272
283
|
|
|
273
284
|
# ④ rubric 启发式
|
|
274
285
|
rubric = heuristic_rubric(html)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Regression gates for usage-feedback defects (validator + static engine checks)."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from xml.etree import ElementTree as ET
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parent
|
|
12
|
+
sys.path.insert(0, str(ROOT))
|
|
13
|
+
import validate_pptx as V # noqa: E402
|
|
14
|
+
|
|
15
|
+
EMU = 914400
|
|
16
|
+
NS = V.NS
|
|
17
|
+
fails: list[str] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ok(name: str, cond: bool, detail: str = "") -> None:
|
|
21
|
+
status = "PASS" if cond else "FAIL"
|
|
22
|
+
print(f" [{status}] {name}" + (f" — {detail}" if detail and not cond else ""))
|
|
23
|
+
if not cond:
|
|
24
|
+
fails.append(name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_shape_bounds_reads_p_xfrm() -> None:
|
|
28
|
+
xml = (
|
|
29
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
30
|
+
f'<p:graphicFrame xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
31
|
+
"<p:xfrm>"
|
|
32
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(2.5 * EMU)}"/>'
|
|
33
|
+
f'<a:ext cx="{int(8.5 * EMU)}" cy="{int(3.5 * EMU)}"/>'
|
|
34
|
+
"</p:xfrm>"
|
|
35
|
+
"</p:graphicFrame>"
|
|
36
|
+
)
|
|
37
|
+
el = ET.fromstring(xml)
|
|
38
|
+
box = V.shape_bounds(el)
|
|
39
|
+
ok("1 shape_bounds reads p:xfrm", box is not None and box[2] > 0, f"got {box}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_shape_bounds_still_reads_a_xfrm() -> None:
|
|
43
|
+
xml = (
|
|
44
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
45
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
46
|
+
"<p:spPr><a:xfrm>"
|
|
47
|
+
f'<a:off x="{EMU}" y="{EMU}"/>'
|
|
48
|
+
f'<a:ext cx="{2 * EMU}" cy="{EMU}"/>'
|
|
49
|
+
"</a:xfrm></p:spPr></p:sp>"
|
|
50
|
+
)
|
|
51
|
+
el = ET.fromstring(xml)
|
|
52
|
+
box = V.shape_bounds(el)
|
|
53
|
+
ok("1b shape_bounds reads a:xfrm", box is not None and box[0] == EMU)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _text_shape(paras, x=0.6, y=2.5, w=4.0, h=0.6):
|
|
57
|
+
parts = []
|
|
58
|
+
for text, pt in paras:
|
|
59
|
+
parts.append(
|
|
60
|
+
f'<a:p><a:r><a:rPr sz="{int(pt * 100)}"/><a:t>{text}</a:t></a:r></a:p>'
|
|
61
|
+
)
|
|
62
|
+
xml = (
|
|
63
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
64
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
65
|
+
"<p:spPr><a:xfrm>"
|
|
66
|
+
f'<a:off x="{int(x * EMU)}" y="{int(y * EMU)}"/>'
|
|
67
|
+
f'<a:ext cx="{int(w * EMU)}" cy="{int(h * EMU)}"/>'
|
|
68
|
+
"</a:xfrm></p:spPr>"
|
|
69
|
+
f'<p:txBody>{"".join(parts)}</p:txBody></p:sp>'
|
|
70
|
+
)
|
|
71
|
+
return ET.fromstring(xml)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_text_overflow_vertical() -> None:
|
|
75
|
+
paras = [(f"这是第{i}段足够长的中文内容用来累计行高", 12.0) for i in range(8)]
|
|
76
|
+
shape = _text_shape(paras, h=0.55)
|
|
77
|
+
issues = V.text_overflow_vertical_check(shape, 1)
|
|
78
|
+
codes = [i["code"] for i in issues]
|
|
79
|
+
ok(
|
|
80
|
+
"2 TEXT_OVERFLOW_VERTICAL fires on multi-segment cumulative",
|
|
81
|
+
"TEXT_OVERFLOW_VERTICAL" in codes,
|
|
82
|
+
f"codes={codes}",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_annotation_band_overlap() -> None:
|
|
87
|
+
xml = (
|
|
88
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
89
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
90
|
+
"<p:spPr><a:xfrm>"
|
|
91
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(5.5 * EMU)}"/>'
|
|
92
|
+
f'<a:ext cx="{int(4 * EMU)}" cy="{int(1.4 * EMU)}"/>'
|
|
93
|
+
"</a:xfrm></p:spPr></p:sp>"
|
|
94
|
+
)
|
|
95
|
+
el = ET.fromstring(xml)
|
|
96
|
+
issues = V.annotation_band_overlap_check([el], 1, int(7.5 * EMU))
|
|
97
|
+
codes = [i["code"] for i in issues]
|
|
98
|
+
ok(
|
|
99
|
+
"3 ANNOTATION_BAND_OVERLAP fires when content crushes band",
|
|
100
|
+
"ANNOTATION_BAND_OVERLAP" in codes,
|
|
101
|
+
f"codes={codes}",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_font_size_not_snapped() -> None:
|
|
106
|
+
xml = (
|
|
107
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
108
|
+
f'<p:sld xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
109
|
+
"<p:cSld><p:spTree><p:sp><p:txBody>"
|
|
110
|
+
'<a:p><a:r><a:rPr sz="1130"/><a:t>OffLadderSampleTextHere</a:t></a:r></a:p>'
|
|
111
|
+
"</p:txBody></p:sp></p:spTree></p:cSld></p:sld>"
|
|
112
|
+
)
|
|
113
|
+
root = ET.fromstring(xml)
|
|
114
|
+
issues = V.font_size_snap_check(root, 1)
|
|
115
|
+
codes = [i["code"] for i in issues]
|
|
116
|
+
ok(
|
|
117
|
+
"4 FONT_SIZE_NOT_SNAPPED fires on off-ladder size",
|
|
118
|
+
"FONT_SIZE_NOT_SNAPPED" in codes,
|
|
119
|
+
f"codes={codes}",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_engine_static() -> None:
|
|
124
|
+
export = (ROOT.parent / "assets" / "pptx-export.js").read_text(encoding="utf-8")
|
|
125
|
+
build = (ROOT.parent / "scripts" / "build_pptx.js").read_text(encoding="utf-8")
|
|
126
|
+
|
|
127
|
+
m = re.search(r"function vbarShapes\([\s\S]*?\n\}", export)
|
|
128
|
+
body = m.group(0) if m else ""
|
|
129
|
+
ok("B vbarShapes handles ec.series", "ec.series" in body and "series.forEach" in body)
|
|
130
|
+
|
|
131
|
+
ok(
|
|
132
|
+
"C A-channel streamgraph reserves legend inside plot",
|
|
133
|
+
"Reserve legend INSIDE" in export and "plotBottom" in export,
|
|
134
|
+
)
|
|
135
|
+
ok(
|
|
136
|
+
"C B-channel infoStreamgraph reserves legend inside plot",
|
|
137
|
+
"Reserve legend INSIDE" in build and "plotBottom" in build,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
ok(
|
|
141
|
+
"D A-channel caption uses capYImg from imageLayoutShapes",
|
|
142
|
+
"capYImg" in export
|
|
143
|
+
and re.search(r"capYImg\s*=\s*imageLayoutShapes", export) is not None,
|
|
144
|
+
)
|
|
145
|
+
ok(
|
|
146
|
+
"D B-channel caption uses capYImg from imageLayoutShapes",
|
|
147
|
+
"capYImg" in build
|
|
148
|
+
and re.search(r"capYImg\s*=\s*imageLayoutShapes", build) is not None,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
cards = re.search(
|
|
152
|
+
r"else if \(type === 'cards'\) \{[\s\S]*?else if \(type === 'split'\)",
|
|
153
|
+
export,
|
|
154
|
+
)
|
|
155
|
+
cards_body = cards.group(0) if cards else ""
|
|
156
|
+
ok(
|
|
157
|
+
"A A-channel cards uses sz: N object props (not bare sz())",
|
|
158
|
+
"sz: 12" in cards_body
|
|
159
|
+
and not re.search(r"(?<![\w.])sz\s*\(\s*12\s*\)", cards_body),
|
|
160
|
+
)
|
|
161
|
+
ok(
|
|
162
|
+
"A modeSize applied in txSp serialization",
|
|
163
|
+
"function modeSize" in export and "modeSize(" in export,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main() -> int:
|
|
168
|
+
print("feedback-gates regression")
|
|
169
|
+
test_shape_bounds_reads_p_xfrm()
|
|
170
|
+
test_shape_bounds_still_reads_a_xfrm()
|
|
171
|
+
test_text_overflow_vertical()
|
|
172
|
+
test_annotation_band_overlap()
|
|
173
|
+
test_font_size_not_snapped()
|
|
174
|
+
test_engine_static()
|
|
175
|
+
print(f"\n{len(fails)} failed" if fails else "\nAll feedback gates PASS")
|
|
176
|
+
return 1 if fails else 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
sys.exit(main())
|