Files
mopac-pdf/docs/ENGINE-DECISION.md
T
mrcharles 10e930e46d mopac-pdf v0: markdown to beautiful PDFs via digest-pinned typst
Go CLI that turns the markdown currency of this stack (Redmine notes,
Discourse posts, briefing output) into typeset PDFs: two embedded typst
templates (report with title page/TOC/headers, dense brief), front-matter
(title/subtitle/author/date/classification/template), GFM tables, and
bar charts rendered in pure Go (go-chart) from fenced chart data blocks.
Engine is a prebuilt typst 0.15.1 container pinned by digest and run
--network none; PDF bytes to stdout or -o. Exit codes 0/1/2.

All dev in docker (dev.sh/Makefile); unit tests + golden typst fixtures
plus a host-side smoke against the real engine container.

Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-29 05:44:24 -05:00

66 lines
3.1 KiB
Markdown

# Engine decision: typst (docker, digest-pinned) over pandoc+LaTeX
Date: 2026-08-29 — Redmine 499 (mopac-pdf v0). Constraint set: our code is
Go/PHP/C/Java only; prebuilt third-party binaries are allowed as
digest-pinned docker tooling (supply chain), never vendored source. All dev
in docker.
## Candidates
1. **typst**`ghcr.io/typst/typst:0.15.1` (Rust binary, upstream image).
2. **pandoc + LaTeX**`pandoc/latex:3.5` (Haskell pandoc + TeX Live,
already cached on the host).
Both are legally clean as container tooling. Both produce real PDFs from
structured input. Measured on ultix-streaming, 2026-08-29, same content
(TOC + table + embedded chart figure):
| | typst 0.15.1 | pandoc 3.5 + xelatex |
|---|---|---|
| cold-ish compile (TOC + table + figure) | **0.94 s** | 7.66 s |
| image size | ~200 MB | 758 MB |
| output | 55 KB, modern layout | 33 KB, classic LaTeX look |
| stdin -> stdout PDF | native (`compile - -`) | no (needs file io for pdf-engine) |
| programmatic layout (templates) | typst scripting language: functions, show rules, `#show: tmpl.with(...)` | LaTeX templates: `\newcommand`, class files, preamble surgery |
| charts/figures/tables | first-class (`#figure`, `#table`, image embedding) | fine via LaTeX packages, verbose |
| error messages | precise, spans, exit codes | TeX log archaeology |
| engine invocation | one process, one shot | pandoc -> (xela|pdf)latex, multi-pass for TOC |
## Decision
**typst.** It wins on every axis that matters for this pipeline:
- **Speed**: ~8x faster compiles matter when the COS briefing world fans
out reports and the harness treats PDF generation as a tool call.
- **Template ergonomics**: templates are small typed functions composed
via `#show: report.with(title: ...)`, which is exactly the front-matter
contract mopac-pdf needs. No LaTeX preamble/class gymnastics, no
multi-pass TOC juggling.
- **Modern output**: real typographic defaults (Libertinus), clean
tables/figures/TOC out of the box — "beautifully formatted" is the spec.
- **Streaming contract**: `typst compile --root /work doc.typ -` pipes PDF
bytes to stdout — the engine layer stays bytes-in/bytes-out, no shared
output files, no root-owned artifacts in repos.
pandoc+LaTeX stays a fine document *conversion* tool (its markdown parser
is broader) and remains available in the tool cache for one-off
conversions; it is not the rendering engine for this pipeline.
## Pinning & supply chain
The engine image is pinned by digest in `internal/engine`:
```
ghcr.io/typst/typst@sha256:032e292249bcd378480cc7c142cfa324b63ef8aadeb88d7e7230320c4c9c422f
```
(verified = `ghcr.io/typst/typst:0.15.1`; the version tag and the digest
were cross-checked via `docker inspect`). The container runs
`--network none`; the compile root is a private temp dir mounted at
`/work`; typst never sees the network or the repo. Engine migrations are a
digest bump + `MOPAC_PDF_ENGINE` override for canary testing.
Our code treats the engine as a pure function: `compile(root, doc.typ) ->
PDF bytes`, exercised through a stub in unit tests and the real container
in smoke.