name: pptx-structural-asserts description: "Programmatic python-pptx structural assertions for deck QA. Runs fifteen checks: text-frame overflow, contrast violations (luminance ratio), alignment-grid drift, repeated-layout-hash, title-underline spam, dark/light run length, missing speaker notes, duplicate titles, body-word density, font render-presence (portability), safe-area breathing-room, plus three archetype-spec checks (waterfall zero-baseline algebra, decision_options column-width sum, risk_heatmap WCAG-AA contrast). Single entry point: check_pptx.py emits a merged JSON report consumed by @deck-critic. Keywords: structural QA, contrast, overflow, alignment grid, layout hash, archetype asserts, font portability, safe area, python-pptx assertions."
PPTX Structural Asserts
Deterministic, fast, no-render-required structural QA. Loads the deck
via python-pptx and runs fifteen checks against shape geometry, text
frames, runs, notes, and (when a --spec is supplied) deck-spec
archetype invariants. Output is a single JSON report consumed by
@deck-critic.
Pipeline / How to Invoke
check_pptx.py is the only entry point the production QA
pipeline (@deck-critic Step 2) calls. When invoked with --spec
it imports check_archetypes.run() from this same skill and merges
the archetype findings into the same report under
archetype_violations. Archetype fail statuses surface as
blocking findings (tagged archetype.<check-id>); warn statuses
surface as warning findings. Calling check_archetypes.py directly
is still supported for local debugging but is NOT part of the
production pipeline (no separate report file).
When to Use This Skill
Load this skill when you are @deck-critic and need:
- A deterministic verdict on the deck's structure (independent of any render engine being installed)
- Pass/fail signals for: aspect-ratio, overflow, contrast, alignment, layout repetition, underline spam, dark/light rhythm, speaker notes, duplicate titles
Files in This Skill
| File | Purpose |
|---|---|
| SKILL.md | This file |
| scripts/check_pptx.py | The check runner — loads pptx + deck-spec, emits JSON report |
| scripts/check_archetypes.py | Cheap spec-level invariants for the 7 archetype recipes added in session 2026-05-04-c8d3b2a1 (waterfall zero-baseline algebra, decision_options column-width sum, risk_heatmap WCAG-AA contrast). Operates on deck-spec.json only — no .pptx parsing required. Imported and run by check_pptx.py when --spec is supplied (wired in fix iteration 1 of the same session). |
Quick Use
python .github/skills/pptx-structural-asserts/scripts/check_pptx.py \
--pptx <session>/agents/deck-builder/output.pptx \
--spec <session>/agents/deck-builder/deck-spec.json \
--out <session>/agents/deck-critic/structural-report.json
Exit codes: 0 on script success (regardless of pass/fail of checks);
1 on script error (file not found, malformed pptx, etc.).
Checks (Severities)
| Check | Severity | Detection |
|---|---|---|
aspect_ratio_pass |
blocking | prs.slide_width / prs.slide_height ≈ 16:9 (±0.5%) |
overflow_violations |
blocking | Body text-frame text-length × est-line-height > frame height |
contrast_violations |
blocking | WCAG luminance ratio of run color vs slide bg <4.5:1 |
alignment_violations |
warn | Shape left/top not on 0.05" snap grid |
repeated_layout_hash |
blocking | Hash of (sorted shape positions, text-frame counts) repeats on consecutive slides |
title_underline_count |
blocking if >2 | Count of slides with a thin (≤0.05" tall) rectangle directly below title |
max_same_bg_run |
blocking if ≥3 | Longest run of consecutive slides with same fill color |
speaker_notes_missing |
blocking | Slides with empty notes_text_frame.text |
duplicate_titles |
blocking | Slides whose first text-frame matches another's |
body_word_max_violations |
warn | Slides with body text >30 words total (F11 — Reynolds) |
font_not_render_present |
warn | Run fonts not in the design system's render_safe allowlist or declared *_fallback (C1 — font portability). Requires --spec with design_system_tokens.fonts. |
safe_area_violations |
warn | Content text shapes intruding into the slide's grid.safe_area_inches margin ("breathing room"; full-bleed/background shapes exempt) (C4). Requires --spec. |
archetype.waterfall.zero_baseline |
blocking | Spec-level: waterfall start + Σdeltas == end (±0.5%). Requires --spec. |
archetype.decision_options.columns_sum_to_slide_width |
warn | Spec-level: derived option-column widths sum to slide-content width within 0.05". Requires --spec. |
archetype.risk_heatmap.contrast_aa |
blocking | Spec-level: WHITE labels on green/amber/red heatmap cells clear WCAG AA normal-text 4.5:1. Requires --spec. |
Report Schema
structural-report.json produced by check_pptx.py:
{
"session_id": null,
"pptx_path": "...",
"spec_path": "...",
"slide_count": 12,
"aspect_ratio": 1.7777,
"aspect_ratio_pass": true,
"overflow_violations": [{"slide": 7, "estimated_overflow_em": 1.4}],
"contrast_violations": [{"slide": 4, "fg": "#A0A4B0", "bg": "#F4F5F7", "ratio": 2.1}],
"alignment_violations": [{"slide": 3, "shape": "Body", "off_snap_axis": "left", "off_by_emu": 4571}],
"repeated_layout_hash": [{"slides": [4, 5], "hash": "h:abc123"}],
"title_underline_count": 1,
"underline_slides": [2],
"max_same_bg_run": 2,
"background_sequence": ["dark", "light", "light", "dark", "light", "..."],
"speaker_notes_missing": [],
"duplicate_titles": [],
"body_word_max_violations": [],
"fonts_used": ["Calibri", "Calibri Light"],
"font_not_render_present": [],
"render_safe_fonts_known": ["Carlito", "DejaVu Sans", "..."],
"safe_area_violations": [{"slide": 6, "shape": "Body", "edges": ["left"], "safe_area_emu": 457200}],
"archetype_violations": [
{"id": "waterfall.zero_baseline", "slide_index": 5,
"status": "fail",
"message": "waterfall algebra broken: start(100) + Σdeltas(+8) = 108, but end=120 (tol=0.6)..."}
],
"archetype_runner_available": true,
"blocking_findings": ["archetype.waterfall.zero_baseline"],
"warning_findings": []
}
Implementation Notes
- The script imports
python-pptx. If unavailable, it auto-installs viapip install python-pptx(matching the deck-builder's behavior) before failing. - Color comparisons traverse run → font → color → rgb. When color is
inherited from the master/layout, the script falls back to the slide
background and assumes default theme (note: this is a known false-
negative; recorded in
report.notes). - Alignment snap is 0.05" = 45720 EMU (914400 EMU per inch × 0.05).
- Layout-hash combines: shape count, sorted (left, top, width, height) tuples rounded to 0.1", and text-frame count per slide.
- Background detection: read slide background fill if solid; else derive from the largest filled rectangle covering ≥90% of slide area (heuristic for blank-layout decks where background is implemented via an underlying full-bleed shape).
Why Both Visual + Structural?
Visual QA catches "feels wrong" issues (focal point, density, decorative shapes). Structural QA catches "is wrong" issues (overflow, contrast, missing notes). Either alone is insufficient:
- Visual-only fails when render engine is missing.
- Structural-only misses purely visual antipatterns (5, 6).
@deck-critic runs both passes and combines findings.
Rendering Subsystem Rebuild (2026-05-04)
scripts/check_pptx.py was rewritten in session 2026-05-04-7d3f9a2b. New / changed report fields:
- overflow_violations (F1) — now uses real Pillow text
metrics per shape, with no slack tolerance, honouring
margin_left/right/top/bottom,space_before/after, and paragraphlevelindentation. The font is resolved via the canonicalrender-visual/assets/font_locator.find_dejavu_sans()helper (C3). - contrast_violations + contrast_unresolved (F4) —
contrast pairs are now theme-resolved (background fill walks
shape ↔ slide-background ↔ theme; text resolves through
a:solidFillthen themeschemeClr). Pairs that cannot resolve to a concrete RGB land incontrast_unresolved(separate fromcontrast_violations). The deck-critic treatscontrast_unresolved >= 5as a BLOCKING revise. - g_label_threshold (F7) — saturation-aware. Hard-coded
thresholds:
sat > 0.45→ accent regardless of luminance;lum < 0.18→ dark;lum > 0.72→ light; else accent (C4). The previous luminance-only heuristic mis-classified saturated brand backgrounds aslight/dark. - dark_light_run (F8) — thresholds:
>= 3warn;>= 7BLOCKING;>= 5 with no accent breakBLOCKING. - ody_word_max (F11) — canonical limit lowered from
70 to 30 words per body. Flagged in
body_density_violations. - **ont_fallback** — when the canonical DejaVu Sans bundle is unavailable and PIL bitmap-default is used as a last resort, this field surfaces so the critic can warn that overflow metrics are approximate.