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
66 lines
3.1 KiB
Markdown
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.
|