Skip to main content
Archforge

The preflight linter for AI-generated PowerPoint.

Catches silent font fallback, unreadable sizes, colliding frames, off-canvas text, and AI-tell punctuation in built .pptx files, before a human ever sees a render. Font-fallback and letter-tracking detection (E1/E4) is CJK-specific by design -- deepest for Hangul -- and does not fire on Latin-only decks, and the small-text rule W8 is gated the same way; every other gate is script-independent.

Try it in your browser -- no install, the deck never leaves the page: love-ash.github.io/archforge

pypi python license ci

Quickstart · What it catches · CI · Calibration record · Corpus results · Discussions · 한국어 README

AI agents / LLMs: read llms.txt, or pip install archforge then archforge skill --install to teach your agent the build-lint-fix loop.

demo

PowerPoint opens both of these decks without a single warning. One of them is broken:

before / after

Code review cannot see any of it, because the defects live in font slots, autofit scales, and coordinates that only materialize at render time. Archforge reads the .pptx itself (XML, font-resolution chain, geometry, image alpha), so it needs no PowerPoint installation and runs anywhere your agent or CI runs.

30 seconds

pip install archforge
archforge demo        # builds broken.pptx + fixed.pptx and lints both, in front of you

Then point it at your own deck:

archforge deck.pptx                 # objective defects only (core profile, the default)
archforge deck.pptx --profile full  # + AI-tell / style rules: machine-made decks want this
archforge deck.pptx --json          # machine-readable JSON (agents / CI)
archforge scan decks/ --profile full   # many files, directories, or globs in one run

The decks in examples/ demonstrate the flagship defects and the profile split, each with expected outputs.

Why

The worst pptx defects are silent. No error is raised when:

  • text lands on a font that lacks its glyphs and silently falls back to an OS default (the classic case: CJK text on a Latin-only font)
  • positive letter-spacing quietly wrecks CJK character spacing
  • autofit shrinks text below readable size
  • text frames collide, or glyphs run off the canvas

These are exactly the defects machine-generated decks produce, and exactly the ones an LLM cannot see in its own output. Archforge is the gate between "the build succeeded" and "a human would sign off on the render." It is deliberately independent of the authoring side: whether the deck came from python-pptx, PptxGenJS, OfficeCLI, or PowerPoint itself, the same file goes in and the same exit code comes out, and the public corpus keeps fixtures from all four writers to enforce that.

Usage

archforge deck.pptx --profile full --fail-incomplete --json   # the agent/CI command
archforge scan decks/ --profile full         # many files, dirs, or globs at once
archforge fix deck.pptx -o fixed.pptx        # auto-fix E1/E2/E4 (new in 0.8.1)
archforge deck.pptx --html report.html       # annotated visual report (new in 0.8.1)
archforge deck.pptx --sarif o.sarif          # SARIF / --junit o.xml for CI systems
archforge rules                              # rule list; `archforge explain W15` for one

Every flag (thresholds, baseline, severity overrides, schema 2.0, timeout), the config file, and the JSON contract: docs/USAGE.md. Recipes: Claude Code · Codex/agents · PptxGenJS · OfficeCLI · GitHub Actions.

CI

GitHub Action (composite). Pinning the action tag pins the linter: by default it installs the exact source checked out at that ref, not whatever PyPI's latest is. Deck-folder config files are ignored (--no-config) and incomplete checks fail (fail-incomplete: true) unless you opt out, so a PR cannot weaken the gate by committing a config next to its deck. files takes one path, directory, or glob per line; globs are expanded by archforge scan itself, so paths with spaces and ** both behave.

jobs:
  deck-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Love-Ash/archforge@v0.11.0
        with:
          files: |
            decks/
          profile: full
          sarif: archforge.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: archforge.sarif

pre-commit:

repos:
  - repo: https://github.com/Love-Ash/archforge
    rev: v0.11.0
    hooks:
      - id: archforge
        # args: [--profile, full]

What it catches

ERRORs block shipping (exit 1):

Code Meaning
E1 The font that will actually render Hangul text is Latin-only: silent Malgun fallback. Resolution follows a measured PowerPoint model (see below)
E2 Dash-family characters used as sentence punctuation (the top AI-generated-deck tell). Numeric ranges (2020 to 2024 with an en dash, Q1 to Q3, 5% to 10%) and minus signs pass by default; --strict blocks everything
E3 Effective size below 5pt after autofit and the full placeholder inheritance chain: unreadable
E4 Positive tracking on consecutive Hangul/Hanja: letter-spacing damage (kana-containing runs are exempt; tracked kana is legitimate Japanese practice)

WARNs are advisory:

Code Meaning
W1 Body-class frame below 9pt
W5 No font size anywhere in the inheritance chain
W6 Same layout skeleton on 4+ pages (tunable; template systems: tune or skip)
W7 Low text-over-image contrast (needs --render)
W8 Small CJK in narrow frames, no wider than 4in (device mockups, cards)
Between the gates: text at 5.0-9.0pt in a frame that is neither body-class (W1) nor narrow (W8) is deliberately not judged. Measured across 29 real decks (2026-08-10): every one of the 1,231 runs in that band was page furniture -- copyright lines, template watermarks, page numbers, running footers -- and zero were content meant to be read. A gate there would trade hundreds of false positives for no measured catch. The measurement is re-runnable; an outside report of a real miss in this band is exactly the fixture #8 asks for.
W9 Accent vertical bars repeated as list markers
W10 Hand-drawn diagram cloned across pages
W11 AI-tell copy: buzzwords, stock openings
W12 Footer baseline drift
W13 Native PowerPoint shadow/glow/3D effects
W14 Titles are nominal phrases, not claims (Korean heuristic; numeric titles count as claims)
W15 Estimated text-on-text overlap
W16 Text glyphs or picture ink off-canvas
W17 Text straddling an image ink edge
W18 Some spans could not be checked (malformed input): results incomplete. Fails under --strict
W19 Text color nearly identical to its own shape's solid fill (under 2.0:1 contrast): ghost placeholder text and near-invisible labels. XML colors only, no render needed; runs in the full profile while the threshold soaks
W20 Text buried on what is drawn behind it, at under 2.0:1 contrast: a caption laid across a chart's bars, a footnote dropped onto a colored panel, ghost text sitting directly on the slide background, or a caption buried inside an svgBlip vector chart. Coverage is summed across every shape under the run; undecodable fills and backgrounds abstain; XML colors and geometry, no render; full profile while the coverage floor soaks
W21 A color painted a handful of times right beside a near-identical one used throughout the deck: what a mistyped hex looks like from outside. Judged deck-wide rather than per page, on redmean distance at or under 16 with the dominant color painted at least 6x more often. Deliberate ramps are excluded by requiring that neither color carry more than two near neighbours, which is what separates a gradient from a slip. Measured on 148 decks, where 95% report nothing and the worst reports two; full profile while the thresholds soak
W22 Text crossed by a hairline rule: a thin divider passing through the glyph box and covering at least half the run, what an unintended strikethrough looks like. Underlines and section dividers sit outside the glyph box and pass by geometry; full profile while the thresholds soak

Profiles separate objective defects from style policy, and since 0.4.0 the default is core: only the mechanical gates (E1/E3/E4, W1/W5/W7/W8, W15-W18) run unless you opt in. full adds the AI-tell and convention rules (E2 dashes, W6 repetition, W9-W14) and the render rules still soaking their thresholds (W19-W22), and is the right mode for agent build-loops linting machine-generated decks; editorial drops W6/W14 and the soaking W19-W22 for editorial and portfolio decks. Excluded rules are not merely hidden, they are not executed, and every choice is recorded in the JSON summary, so nothing is silently bypassed.

How it works

The E1 font-resolution model is measured, not guessed from the OOXML spec: probe decks rendered through PowerPoint COM pinned the actual priority (run a:ea > paragraph defRPr > lstStyle chain > theme ea > a:latin on an empty theme slot > OS fallback). Effective sizes walk the same chain; geometry approximates real glyph and image-ink areas with insets, group transforms, and merged cells; incompleteness is a first-class output (W18 / summary.incomplete), so summary.pass under --fail-incomplete is the honest gate. Font-coverage knowledge is Hangul-deep and CJK-aware; other scripts are never falsely flagged; the target renderer is PowerPoint for Windows.

Full model, calibration method, renderer-coverage matrix, and scope: docs/HOW_IT_WORKS.md and docs/CALIBRATION.md. Roadmap to 1.0: docs/ROADMAP.md. What is stable and what may move: docs/DEPRECATION.md.

Agent integration

Designed for LLM-agent build-lint-fix loops:

build deck.pptx
loop:
    result = archforge deck.pptx --profile full --fail-incomplete --json   # machine-made decks
    if result.summary.pass: break   # pass reflects the active policy (summary.policy)
    fix listed defects (location payloads point at the exact shape/run), rebuild
review WARNs against renders

The Agent Skills pack (standard SKILL.md + YAML frontmatter) teaches this loop and per-code fixes to any supporting agent (Claude Code, Codex, ...). It ships inside the wheel: archforge skill --install. If you cloned the repo, skills/archforge-pptx-lint/ is the same file.

A passing lint is not a finished deck: the linter owns the mechanical defect class; composition and narrative still need eyes on renders.

Community and contributing

Name

archforge = arch (structure) + forge. A forge where a deck's structure and typography get hammered straight before shipping.

Author

Built and calibrated by Minjae Kwon (Ash) (@Love-Ash · LinkedIn). If archforge caught something before your audience did, a star helps the next person find it. I write up the measurement work behind the gates (how PowerPoint actually resolves fonts, and what AI-built decks silently break); say hi on LinkedIn.

License

MIT © Minjae Kwon (Ash)

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

archforge-0.11.0.tar.gz (845.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

archforge-0.11.0-py3-none-any.whl (138.3 kB view details)

Uploaded Python 3

File details

Details for the file archforge-0.11.0.tar.gz.

File metadata

  • Download URL: archforge-0.11.0.tar.gz
  • Upload date:
  • Size: 845.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for archforge-0.11.0.tar.gz
Algorithm Hash digest
SHA256 6da0d3ff5bb126c6be89f667d1c971c3eae085a0be24769c30c01c5ba5381d51
MD5 318062dadd39da821742f734b34d151f
BLAKE2b-256 8410647db7055b07a10622c28e8891c5adc61757d70ce5951e2d99f49ab266af

See more details on using hashes here.

Provenance

The following attestation bundles were made for archforge-0.11.0.tar.gz:

Publisher: publish.yml on Love-Ash/archforge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file archforge-0.11.0-py3-none-any.whl.

File metadata

  • Download URL: archforge-0.11.0-py3-none-any.whl
  • Upload date:
  • Size: 138.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for archforge-0.11.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fd3ef00fffe3a6f7c58e53a330aa5f32f0c70cf3de2094f0d8955e019ff445f1
MD5 eaf2b5b29c98731e5282f389bc4b6540
BLAKE2b-256 611acb1a5539db2088c678ffcd3d1553b4ad311f1f65092fc4bdc577c87542b4

See more details on using hashes here.

Provenance

The following attestation bundles were made for archforge-0.11.0-py3-none-any.whl:

Publisher: publish.yml on Love-Ash/archforge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.11.0 This release

2 files

0.10.0

2 files

0.9.3

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.1

2 files

0.8.0

2 files

0.7.1

2 files

0.7.0

2 files

0.6.2

2 files

0.6.1

1 file

0.6.0

1 file

0.5.0

1 file

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page