# How the PDF Compilation Loop Verifies Page Counts and Prevents Orphaned Entries

> Learn how the PDF compilation loop verifies page counts and prevents orphaned entries using Python scripts and LaTeX macros in this technical article. Ensure document integrity.

- Repository: [Mads Lorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search)
- Tags: internals
- Published: 2026-07-14

---

**The repository implements a repeat-until-valid workflow where [`tools/verify_pdf.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/verify_pdf.py) enforces exact page counts using `pdfinfo` while LaTeX macros like `\needspace` prevent section titles from separating from their content.**

The MadsLorentzen/ai-job-search repository automates professional document generation through a robust **PDF compilation loop** that guarantees strict layout constraints. This system combines Python-based verification with LaTeX spacing controls to ensure every CV and cover letter meets exact page requirements while eliminating orphaned headings.

## The Repeat-Until-Valid Compilation Workflow

The architecture follows a **compile-verify-adjust** cycle that repeats until all validation criteria pass. The process begins with LaTeX compilation engines and concludes with automated verification scripts.

### LaTeX Compilation Engines

The CV compiles with **`lualatex`** while the cover letter uses **`xelatex`**, both running in non-stop mode to enable automated iteration. As noted in the repository documentation at line 221 of [`README.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/README.md), these engines handle the modern font requirements and Unicode characters common in international CVs.

### The Verification Layer

After each compilation, the script **[`tools/verify_pdf.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/verify_pdf.py)** performs three critical checks to validate the generated PDF. This Python module serves as the gatekeeper for document quality, ensuring that layout adjustments continue until the output meets specifications.

## How Page Count Verification Works

The verification script extracts metadata and content from the compiled PDF to enforce strict pagination requirements.

### Extracting Page Counts with pdfinfo

The script calls `pdfinfo` to retrieve the actual page count and compares it against the `expected_pages` parameter. In [`tools/verify_pdf.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/verify_pdf.py), lines 49-55 implement this check:

```python
if expected_pages is not None:
    actual_pages = parse_page_count(run_tool(["pdfinfo", str(pdf_path)]))
    if actual_pages != expected_pages:
        raise VerificationError(
            f"expected {expected_pages} page(s), found {actual_pages}"
        )

```

This guarantees that CVs are **exactly 2 pages** and cover letters are **exactly 1 page**, matching the requirements supplied by the CI job or Claude assistant.

### Text Layer Validation

Beyond page counts, the script validates content presence using `pdftotext` to ensure the PDF contains extractable text. Lines 59-62 check that the document meets a minimum character threshold, preventing compilation errors that might produce blank or corrupted pages. Optional required string validation (lines 64-66) further ensures critical content sections exist.

## Preventing Orphaned Entries with LaTeX Macros

While Python handles pagination verification, the LaTeX templates prevent visual orphans through spacing macros documented in **[`.claude/skills/job-application-assistant/05-cv-templates.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-application-assistant/05-cv-templates.md)** (lines 70-80).

### The needspace Macro

The `\needspace{5\baselineskip}` command appears **before every `\cventry`** in the LaTeX source. This macro forces LaTeX to check for sufficient remaining vertical space before starting a new entry. If insufficient space exists, LaTeX automatically inserts a page break, preventing section headings from appearing at the bottom of a page while their bullet lists flow to the next.

### Dynamic Page Adjustment with enlargethispage

When content nearly fits but spills slightly onto an additional page, the `\enlargethispage{2-3\baselineskip}` command pulls extra lines onto the current page. This fine-tuning capability allows the compilation loop to compress sections slightly rather than consuming an entire new page for one or two lines of content.

## Implementation in CI and Automation

The verification loop integrates directly into the continuous integration pipeline defined in **[`.github/workflows/ci.yml`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.github/workflows/ci.yml)**.

### CI Pipeline Integration

The workflow compiles the example CV using `lualatex`, then immediately invokes the verification script to enforce the two-page requirement. If `verify_pdf` raises a `VerificationError`, the Claude assistant or CI system adjusts the LaTeX source—adding `\needspace` or tuning `\enlargethispage` values—before recompiling. This cycle continues until the PDF satisfies both page count and content requirements.

You can run the verifier manually to check existing PDFs:

```bash

# Verify that a CV is exactly 2 pages and contains at least 100 characters

python -m tools.verify_pdf cv/main_example.pdf --pages 2 --min-chars 100

```

### Python Implementation Example

A typical automation script implements the loop as follows:

```python
import subprocess
from pathlib import Path
from tools.verify_pdf import verify_pdf, VerificationError

pdf_path = Path("cv/main_example.pdf")
while True:
    # Compile with lualatex (non-stop mode)

    subprocess.run(
        ["lualatex", "-interaction=nonstopmode", "-halt-on-error", "cv/main_example.tex"],
        check=True,
    )
    try:
        verify_pdf(pdf_path, expected_pages=2, min_chars=100)
        print("✅ PDF is valid")
        break
    except VerificationError as e:
        print(f"⚠️ Verification failed: {e}")
        # Assistant would edit the .tex (e.g., insert \needspace) before next iteration

```

## Summary

- **[`tools/verify_pdf.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/verify_pdf.py)** enforces exact page counts using `pdfinfo` and validates content presence via `pdftotext`
- The **repeat-until-valid workflow** automatically recompiles documents when verification fails, adjusting LaTeX spacing macros between iterations
- **`\needspace{5\baselineskip}`** prevents orphaned headings by ensuring sufficient space exists before starting new entries
- **`\enlargethispage`** allows minor page compression to avoid single-line overflows onto new pages
- The system guarantees CVs are **exactly 2 pages** and cover letters are **exactly 1 page** through automated CI integration

## Frequently Asked Questions

### What happens if the PDF page count doesn't match the expected value?

The `verify_pdf` function raises a `VerificationError` immediately when `actual_pages` differs from `expected_pages`. This exception triggers the compilation loop to adjust the LaTeX source—typically by modifying spacing macros—and recompile until the page count matches the requirement exactly.

### How does `\needspace` prevent orphaned entries in LaTeX?

The `\needspace{5\baselineskip}` command checks that at least five lines of vertical space remain on the current page before rendering a `\cventry` section. If insufficient space exists, LaTeX inserts a page break first, ensuring that section titles never appear isolated at the bottom of a page while their content flows to the next.

### Can I run the verification script manually outside of the CI pipeline?

Yes, the [`tools/verify_pdf.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/verify_pdf.py) module supports command-line execution. You can verify any PDF manually by specifying the expected page count and minimum character requirements: `python -m tools.verify_pdf path/to/file.pdf --pages 2 --min-chars 100`.

### Why does the repository use different LaTeX engines for CVs versus cover letters?

The CV uses `lualatex` to handle complex Unicode characters and modern font features common in international applications, while the cover letter employs `xelatex` for its robust font handling capabilities. Both engines support the spacing macros required for orphan prevention and integrate seamlessly with the Python verification layer.