# Fullbleed font-pipeline regression report

**Status:** fixed in Fullbleed 2.3.1 and verified on 2026-08-17 against preserved 2.3.0 artifacts.

This report records two silent font-path failures found while building the showcase, the narrow controls used to isolate them, and the 2.3.1 verification result. It describes observed behavior; it does not claim a source-level root cause.

## Result at a glance

| Regression | 2.3.0 | 2.3.1 | Verification |
|---|---|---|---|
| Synthetic 700/800 weight preview | Fullbleed's finalized-PDF PNG preview painted unrelated, missing, or displaced glyphs | All requested weights are readable and preserve the source characters | visual before/after review; preview hash changed |
| Compiled fixed VDP with Lato slots | slots were discovered, but four Lato-backed values remained as literal `{{placeholders}}` | all custom-font values bind; no placeholder remains | extracted-text assertions plus visual review |
| Glyph/profile diagnostics | zero glyph findings and no PDF/A seed blockers despite the bad preview | still clean | useful structural evidence, but not a visual oracle |

The showcase's global `font-weight: 400 !important` and Courier binding-slot workarounds have been removed.

## Reproduction environment

| Item | Value |
|---|---|
| Baseline runtime | Fullbleed 2.3.0 |
| Verified runtime | Fullbleed 2.3.1 |
| Python | 3.11.8 |
| OS | Windows 10 build 26200 (`Windows-10-10.0.26200-SP0`) |
| Font | `assets/vendor/fonts/Lato-Regular.ttf` |
| Font SHA-256 | `d636e4683231f931eda222d588e944d082bfd3bdba02f928bee461c0f185b251` |
| PDF/A output intent | Windows `sRGB Color Space Profile.icm` |

From the repository root:

```powershell
.\.venv\Scripts\python.exe .\diagnostics\font-issues\reproduce.py
```

The current machine-readable result is [`diagnostics/font-issues/output/evidence.json`](diagnostics/font-issues/output/evidence.json). The original PDFs, previews, and evidence are frozen under [`diagnostics/font-issues/baseline-2.3.0`](diagnostics/font-issues/baseline-2.3.0). The synthetic fixture deliberately retains its “2.3.0” eyebrow so its source and generated PDF remain byte-for-byte comparable.

## Regression 1: synthetic-weight text was corrupted in finalized-PDF previews

### Fixture

The diagnostic registers one physical Lato Regular face and aliases it for the four Helvetica fallbacks needed by the PDF/A resolver. CSS then requests weights 400, 650, 700, and 800. A control forces the same document to weight 400.

### 2.3.0 observation

- Weight 400 was readable.
- Weight 650 was readable in this fixture.
- The weight-700 heading and the 700/800 specimen lines painted as unrelated, missing, or displaced characters.
- The forced-400 control was clean.
- `render_pdf_with_glyph_report` returned zero findings for both variants.
- `inspect_pdf` reported `pdfa2b`, metadata, an output intent, one embedded font, no warnings, and no seed blockers.

![2.3.0 synthetic-weight preview with corrupt 700/800 painting](diagnostics/font-issues/baseline-2.3.0/synthetic/synthetic_page1.png)

### 2.3.1 verification

The same fixture now paints the heading and every 400/650/700/800 line correctly.

![2.3.1 synthetic-weight preview with readable requested weights](diagnostics/font-issues/output/synthetic/synthetic_page1.png)

The strongest localization signal is that `synthetic-weight-synthetic.pdf` has the same SHA-256 under both runtimes, while the PNG produced by `render_finalized_pdf_image_pages_to_dir` changed:

| Artifact | 2.3.0 SHA-256 | 2.3.1 SHA-256 |
|---|---|---|
| synthetic PDF | `b70663f93ab7edb80bbb7be6532e1b24a058902f83f52676755fd1cefbf6d323` | same |
| synthetic preview PNG | `d674ccf45d059c7c6182553d5dda21930d08882cf2d4124819a889a7205a406c` | `29989ef98fd670a112b253d9ccea12563eddfb56c83e68782383814e1632fdfb` |
| forced-400 PDF | `49824b3c96d62f1c6c32422e997d3e3ae93607aeee95fa94d2c2144a4093d1b4` | same |
| forced-400 preview PNG | `d27d827352fbe2c20d813e693ada6c51e84afa814064af1c4707acadb1f95892` | same |

That evidence localizes the observed regression to Fullbleed's finalized-PDF preview/raster path rather than to changed PDF bytes. It does not, by itself, certify how every external PDF viewer rendered the file.

## Regression 2: fixed VDP discovered custom-font slots but left them unbound

### Fixture

The pass template contains five paint-only slots:

```text
attendee, role, track, seat, pass_id
```

The original comparison put four slots in Lato and retained Courier for `pass_id` as an internal control. A second variant used Courier for every slot.

### 2.3.0 observation

- `compiled.stats()` listed all five names in `binding_slots` and `reflow_binding_slots`.
- `render_pdf_bindings_to_file` returned successfully.
- The four Lato-backed values visibly and textually remained `{{attendee}}`, `{{role}}`, `{{track}}`, and `{{seat}}`.
- The Courier-backed `pass_id` substituted in that same PDF.
- The all-Courier control substituted every value.

![2.3.0 Lato-backed slots left as literal placeholders](diagnostics/font-issues/baseline-2.3.0/vdp-custom-font/custom-font_page1.png)

### 2.3.1 verification

The original four-Lato/one-Courier probe substituted every value on its first 2.3.1 rerun. The migrated fixture goes further: all five slots now use Lato Regular, all five remain present in `binding_slots`, and extracted text contains every expected value with no `{{` token:

```text
ADA RIVERA
RESEARCHER
SIGNAL
A12
NS-000001
```

![2.3.1 all-Lato fixed VDP with every value bound](diagnostics/font-issues/output/vdp-custom-font/custom-font_page1.png)

The diagnostic exits nonzero if the custom binding slots are incomplete, a placeholder survives, or an expected value is absent.

## Why the issue was easy to miss

Both 2.3.0 failures returned successfully. In the synthetic-weight case, glyph coverage and profile seed inspection were green even though Fullbleed's own page preview was visibly wrong. In the VDP case, compilation advertised the slots even though the fixed renderer left their template text untouched. The practical lesson is that structural diagnostics need a rendered-page regression gate, and binding tests should also assert on extracted output text.

## Regression assertions retained

1. Preserve the original 2.3.0 PDFs and PNGs as immutable comparison artifacts.
2. Render the synthetic-weight fixture and visually verify the heading plus 400/650/700/800 regions.
3. Record both PDF and preview hashes; an unchanged PDF with a changed preview is meaningful here.
4. Compile the all-Lato fixed VDP fixture and require all five slot names.
5. Render one record, reject any remaining `{{...}}` token, and require all five expected strings in extracted text.
6. Keep the Courier variant as a control.

## Claim boundary

The PDF/A statements are Fullbleed seed-inspection results, not independent conformance certification. Visual conclusions come from Fullbleed-generated PNG previews reviewed at full resolution. The report intentionally separates observed evidence from implementation hypotheses.
