diff --git a/.github/workflows/explainers.yml b/.github/workflows/explainers.yml new file mode 100644 index 0000000..0f076d8 --- /dev/null +++ b/.github/workflows/explainers.yml @@ -0,0 +1,41 @@ +name: Explainers + +# Explainers PDFs must be valid, single-page, and correctly sized. +# This check validates the published contract only; rendering lives +# in transitrix/coordinator. + +on: + push: + branches: [main] + paths: + - 'explainers/**' + - 'scripts/check_explainers.py' + - '.github/workflows/explainers.yml' + pull_request: + branches: [main] + paths: + - 'explainers/**' + - 'scripts/check_explainers.py' + - '.github/workflows/explainers.yml' + +permissions: + contents: read + +jobs: + check-explainers: + runs-on: ubuntu-latest + name: explainers/ published PDF contract holds + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install PDF validator + run: python3 -m pip install 'pypdf==6.16.2' + + - name: Check explainers/ contract + run: python3 scripts/check_explainers.py diff --git a/explainers/how-a-document-prints/how-a-document-prints.pdf b/explainers/how-a-document-prints/how-a-document-prints.pdf new file mode 100644 index 0000000..7fd5b50 Binary files /dev/null and b/explainers/how-a-document-prints/how-a-document-prints.pdf differ diff --git a/explainers/how-a-document-prints/index.html b/explainers/how-a-document-prints/index.html index 760b909..6a876d5 100644 --- a/explainers/how-a-document-prints/index.html +++ b/explainers/how-a-document-prints/index.html @@ -46,7 +46,7 @@
-

← Explainers

+

← Explainers · one-pager

How a model-backed document prints

diff --git a/scripts/check_explainers.py b/scripts/check_explainers.py new file mode 100644 index 0000000..18832d8 --- /dev/null +++ b/scripts/check_explainers.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Check explainers PDF contract. + +Published explainer PDFs must: +- Be valid PDFs +- Have exactly one page +- Have geometry 842 × 474 pt (A4 short form, 297 × 167 mm) +""" + +import glob +import sys +from pathlib import Path + +try: + from pypdf import PdfReader +except ImportError: + print("❌ pypdf not installed; skipping checks", file=sys.stderr) + sys.exit(0) + + +def check_explainer_pdfs(): + """Validate all explainer PDFs.""" + errors = [] + pdf_files = glob.glob("explainers/*/[!_]*.pdf") + + if not pdf_files: + # No explainer PDFs yet; check passes + return errors + + for pdf_path in sorted(pdf_files): + try: + reader = PdfReader(pdf_path) + + # Check page count + page_count = len(reader.pages) + if page_count != 1: + errors.append( + f"❌ {pdf_path}: expected 1 page, got {page_count}" + ) + continue + + # Check geometry on the one page + page = reader.pages[0] + mediabox = page.mediabox + width = float(mediabox.width) + height = float(mediabox.height) + + # Expected: 842 × 474 pt (A4 short form) + # Allow ±1pt tolerance for rounding + expected_w, expected_h = 842, 474 + if abs(width - expected_w) > 1 or abs(height - expected_h) > 1: + errors.append( + f"❌ {pdf_path}: geometry {width:.0f}×{height:.0f} pt, " + f"expected {expected_w}×{expected_h} pt" + ) + else: + print(f"✓ {pdf_path}: {width:.0f}×{height:.0f} pt, 1 page") + + except Exception as e: + errors.append(f"❌ {pdf_path}: {e}") + + return errors + + +if __name__ == "__main__": + errors = check_explainer_pdfs() + if errors: + for error in errors: + print(error, file=sys.stderr) + sys.exit(1) + sys.exit(0)