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

3.1 KiB

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. typstghcr.io/typst/typst:0.15.1 (Rust binary, upstream image).
  2. pandoc + LaTeXpandoc/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

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.