# Pptx Builder

Builds PowerPoint decks that keep their fonts and colors on another machine, plus a diagram-mapper that matches any source artifact to the deck's palette.

Status: active

## Install

```bash
claude plugin marketplace add tyroneross/RossLabs-AI-Toolkit
claude plugin install pptx-builder@rosslabs-ai-toolkit
```

## The Problem

Generated PowerPoint decks look fine on the machine that made them, then fall apart when opened elsewhere — fonts substitute, theme colors shift, layouts reflow. The cause is OOXML theme inheritance: a deck that does not fully define its theme inherits the opener's, and the design breaks. Separate problem: drawing diagrams of a codebase, a workflow, or a state machine usually ends in a hand-built image that goes stale the moment the underlying thing changes.

## What I Built

A Claude Code plugin that hosts two skills:

**`pptx-builder`** — PowerPoint deck builder built around the calm-precision-pptx hybrid model. `python-pptx` is the primary structural build engine and the canonical theme owner (typed `structure.json` → tokens → helpers → `.pptx`, following Calm Precision design philosophy and a Gestalt decision framework). The `pptxgenjs` polish layer is wired (v0.6.0+) for slides where pixel-precise composition earns its weight — the cover slide is the first consumer, composed in Node and merged into the python-pptx base by a from-scratch cross-deck slide merge engine at the OOXML/ZIP-package level. Opt in via `--polish-cover`; default OFF — zero regression to the python-pptx primary path.

**`diagram-mapper`** — turns any source artifact (codebase, workflow engine, state machine, API, process doc, org structure, financial model) into an audit-grade diagram with explicit node types, decision branches, and exception paths. Renders to Mermaid (default), SVG, HTML/interactive, or PowerPoint via the same plugin QA stack. When a diagram is embedded into a calm-precision deck, its palette resolves to the deck's tokens so the diagram matches.

## Architecture

Neither skill runs a model of its own. Both are **host-LLM-driven**: the LLM already running the calling session — Claude Code, Codex, or Claude cowork — reads the request, authors the intermediate artifact (`structure.json` for a deck, a node/edge grammar for a diagram), and hands it to this plugin's code. Everything downstream of that handoff — layout selection, rendering, merging, linting, rendering to PNG — is deterministic Python and Node with no LLM or vendor API call in the loop. `structure/schema.json` states this directly: *"the host LLM emits this from content; downstream stages ... read it."* `plugin.json` states the same for both skills: *"no vendor API calls in the build or diagram path."*

```mermaid
flowchart LR
  A["Host LLM (Claude Code / Codex / cowork)<br/>authors structure.json"] --> B["validate_structure.py<br/>jsonschema"]
  B --> C["build_deck.py<br/>dispatch by intent + content.kind"]
  C --> D["auto_layout.select_layout()<br/>detect / classify / match / rank"]
  D --> E["slides_*.py helpers<br/>python-pptx + calm-precision tokens"]
  E --> F{"--polish-cover?"}
  F -->|"default: no"| G[".pptx base deck"]
  F -->|"opt-in"| H["polish/cover.mjs<br/>Node + pptxgenjs"]
  H --> I["merge_pptx.py<br/>OOXML/ZIP splice + slideLayout remap"]
  I --> G
  G --> J["QA gauntlet, 9 gates<br/>typography / lint / contrast / drift / theme-portability"]
  J --> K["render_pptx.py --png<br/>PowerPoint macOS (ground truth)<br/>LibreOffice fallback"]
  K --> L["Shipped .pptx"]
```

**Models: none.** The plugin ships zero model or API calls of its own — every build, merge, and QA script (`build_deck.py`, `merge_pptx.py`, `select_layout()`, `verify_*.py`, the lints) is deterministic Python (`python-pptx`, `lxml`, `jsonschema`) or Node (`pptxgenjs`). The only "model" in the system is the LLM hosting the calling session, and it operates entirely upstream of the plugin's own runtime — it authors `structure.json` or extracts a diagram's node/edge grammar, then the plugin takes over and produces byte-identical output for the same input on every run. This is deliberate: a slide-composition and OOXML-repair pipeline needs to be reproducible and auditable, not creative, so nothing in it depends on model output at build time.

**Tools & infra — which, why:**

| Tool | Why |
|---|---|
| `python-pptx` (>=1.0,<2) | Primary structural build engine and canonical theme owner; composes the typed `structure.json` into OOXML slides via `build/helpers/*` |
| `lxml` (>=4.9,<7) | Direct XML manipulation for the cross-deck slide merge engine (`merge_pptx.py`) and theme-portability repair (`fix_theme_portable.py`) — operations `python-pptx` doesn't expose |
| `jsonschema` (>=4.0,<5) | Validates `structure.json` against `structure/schema.json` before any slide renders, so a malformed request fails at the input boundary, not mid-build |
| `pptxgenjs` (^4.0.1, Node ≥18) | Polish layer for pixel-precise composition; wired for the cover slide only, opt-in via `--polish-cover` |
| Mermaid | Default `diagram-mapper` output — renders natively in Markdown, GitHub/GitLab, Notion, and most docs tooling with no extra render step |
| `uv` | Python dependency/venv management for the skill's scripts (`uv run --project skills/pptx-builder ...`) |
| Microsoft PowerPoint (macOS) | Primary visual-render ground truth, via `render_pptx.py --png` — the renderer the audience actually opens the deck in decides pass/fail |
| LibreOffice | Fallback renderer (`render_qa.sh`) when PowerPoint is unavailable or blocked; checks coverage, doesn't get veto power |
| Databases | None — the plugin is stateless. `structure.json`, the tokens JSON, and the output `.pptx` are the only persistent artifacts, all local files |

`matplotlib`/`Pillow` (chart-as-image) and `pandas`/`openpyxl` (Excel-driven slide data) are declared as reserved optional-dependency groups in `pyproject.toml` but are not wired into the default build path — native, Excel-editable charts are the shipped path instead.

## How it works

1. A user (or an upstream skill such as `tyrone-writing-system/deck-structure`) hands the host LLM a locked argument; the host LLM writes `structure.json` — deck-level `governing_thought`, per-slide `intent` (`cover`, `section`, `big-number`, `bullets`, `two-column`, `compare`, `quote`, `table`, `chart`, `closer`, `custom`), and per-slide `takeaway` — against `skills/pptx-builder/structure/schema.json`.
2. `structure/validate_structure.py` checks the JSON against that schema via `jsonschema` before any rendering starts; a schema violation fails here, not mid-render.
3. `build/build_deck.py` walks the slide list and resolves a handler by `(intent, content.kind)`. An intent that isn't wired raises `NotImplementedError` before any `content[...]` field is read, so an unsupported request fails loud instead of crashing on a missing key.
4. `build/auto_layout/select_layout()` runs first for every slide (detect → classify → match → rank against known templates) and returns a `LayoutDecision`; the dispatched `slides_*.py` helper (bignum, chart, compare, quote, text, framework, scorecard) may consult it but currently falls back to its own in-helper geometry, which already matches the decision's coordinates.
5. Each helper composes the slide with `python-pptx`, pulling colors and fonts from `tokens/calm-precision.tokens.json` (or a project's `.deck-prefs.json` override) — the single source of visual truth that also drives the typography and drift-detection QA gates downstream.
6. If `--polish-cover` (or `polish: true` on the cover slide) is set, `polish/cover.mjs` composes that one slide in Node with `pptxgenjs`, and `build/merge/merge_pptx.py` splices it into the python-pptx base at the OOXML/ZIP-package level, remapping the polish slide's `slideLayout` relationship onto the base deck's theme so the pixel-precise slide doesn't carry a divergent theme. Default is off; the rest of the deck is unaffected either way.
7. The QA gauntlet runs nine gates in order — schema conformance, Inter-only typography, em-dash/italic/fractional-EMU policy, sentence-shape and length lints, WCAG 4.5:1 contrast, token-vs-output drift (`--tokens` canonical, `--skin` legacy for back-compat), OOXML theme-inheritance repair (closes six known cross-machine drift gaps), and a strict portability pass/fail gate. Every gate must exit 0 before the deck is considered shippable.
8. An optional visual-render step drives Microsoft PowerPoint on macOS (`render_pptx.py --png`) as ground truth, with LibreOffice (`render_qa.sh`) as fallback when PowerPoint is unavailable, producing a PNG a human or an audit pass can inspect.
9. `diagram-mapper`, the plugin's sibling skill, follows the same "source first, grammar first, render last" loop for non-deck diagrams and reuses this same QA/render stack whenever the chosen output format is PPTX; when a diagram is embedded into a calm-precision deck, its palette resolves to the deck's tokens so it matches.

## Tech stack

Python (`python-pptx`, `lxml`, `jsonschema`) for the primary build, merge, and QA path, managed with `uv`. Node (`pptxgenjs` ^4.0.1) for the opt-in polish layer. Mermaid as the default `diagram-mapper` render target, with SVG, HTML/interactive, and PowerPoint as progressively heavier fallbacks. No database, queue, or hosted service — the plugin runs entirely inside the calling agent host (Claude Code, Codex, or Claude cowork) against local files.

## Hard Rules (Mechanically Enforced)

Calm Precision base, enforced as lints:

- Action titles only — full sentences, sentence case, no terminal period, ≤95 chars
- One assertion per slide
- Footnote every numeric claim
- Semantic color only — `BRAND_PRIMARY` appears once per slide on the dominant element
- Whitespace is content; outer margins ≥ 0.4"
- Slide background set explicitly on every slide (`#FAFBFC`)

User overrides also mechanically enforced:

- **No em-dashes / en-dashes** — use colons, semicolons, commas, "to"
- **No horizontal row lines in tables** — vertical column separators only
- **No decorative lines** — table header underline is the only allowed horizontal rule
- **No panels for grouping** — whitespace and the 12-column grid carry grouping
- **No bottom takeaway bar** — the action title carries the takeaway
- **Native tables and charts** — Excel-editable, no images of tables
- **Inter only** (FONT_PRIMARY = "Inter"; serif fails)
- **High-contrast light background** mandatory

## The QA Gauntlet

Every gate must exit 0 before a deck is considered shippable:

| Gate | What it catches |
|---|---|
| `validate_structure.py` | `structure.json` schema conformance |
| `verify_typography.py` | Inter only; 3-tier ladder for body / chart-axis / chart-legend |
| `verify_deck.py` | Em-dash / italic / fractional-EMU policy |
| `text_lint.py` | Sentence shape, terminal period, length caps |
| `contrast_lint.py` | WCAG 4.5:1 minimum |
| `drift_check.py` | Tokens-vs-output mismatch (canonical `--tokens` mode reads the python-primary JSON; legacy `--skin` mode reads JS skins for back-compat) |
| `fix_theme_portable.py` | Closes the six OOXML inheritance gaps that cause cross-machine drift |
| `verify_portable.py --strict` | Pass/fail gate run before delivery |
| `render_pptx.py --png` | Microsoft PowerPoint on macOS (primary ground truth); LibreOffice via `render_qa.sh` is the fallback |

## Style-Input Modes

Three modes, no mode-switching ceremony:

1. **No style guidance** — built-in default house skin (Calm Precision tokens).
2. **Copy an existing deck** — ingest a sample `.pptx`, extract tokens via `extract_theme_colors.py`, build new slides that match; `fix_theme_portable.py --theme-map <extracted>` for paste compatibility.
3. **User-provided style** — explicit `.deck-prefs.json` at the repo root or a named skin.

## Repo Layout (folder-per-capability)

```
skills/pptx-builder/
├── design/references/   vendored calm-precision + user overrides + palette library
├── build/               python-pptx PRIMARY build path + cross-deck slide merge
├── polish/               pptxgenjs polish layer (cover slide wired v0.6.0)
├── structure/            schema.json + validator
├── qa/                   typography, lints, contrast, drift, portability, render
├── tokens/                single source of truth
├── samples/               fixtures (cx-premium-kickoff for the slide-1-3 proof)
└── legacy/                demoted v0.2.0 fixed-renderer + v0.3.0 PptxGenJS guided pipeline
```

## Lessons

- **Polish as an opt-in layer, not a replacement.** python-pptx stayed the canonical theme owner and primary build engine; pptxgenjs was wired in as a polish layer scoped to the cover slide only, gated behind `--polish-cover` and OFF by default. The judgment call: pixel-precise composition was worth the added cross-deck slide merge engine for one slide type, not worth risking regression across the whole build path by making it the default.

- **Two render engines, one ranked as ground truth.** `render_pptx.py --png` treats Microsoft PowerPoint on macOS as primary ground truth and LibreOffice (via `render_qa.sh`) as fallback, rather than treating both as equally authoritative. The reasoning: the renderer the audience actually opens the deck in should decide pass/fail; the open-source renderer checks coverage but doesn't get veto power.

- **Legacy paths demoted, not deleted.** `drift_check.py` kept a `--skin` mode that reads JS skins for back-compat alongside the canonical `--tokens` mode, and the earlier v0.2.0 fixed-renderer and v0.3.0 PptxGenJS guided pipeline moved to `legacy/` instead of being removed. After two prior pipeline shifts, the call was to preserve rollback paths and old-skin compatibility rather than force a clean break a third time.

- **No model in the build path, by design.** The plugin's own code — build, merge, and every QA gate — is fully deterministic; the only LLM involved is whichever session is hosting the request, and it stops being involved the moment `structure.json` is handed off. That boundary is what makes the QA gates meaningful: they can assert "this exact output" rather than "output within tolerance of a model's mood."

## Pairs With

- **tyrone-writing-system / deck-structure** — lock the argument before drafting
- **pyramid-principle / pyramid-presentation** — deeper Pyramid storylining
- **design** plugin — `design-critique` and `accessibility-review` passes

## Links

- [HTML project page](https://rosslabs.ai/projects/pptx-builder/)
