3.9 KiB
REPORT-20260829-1300-pdf — mopac-pdf v0 (Redmine 499)
Status: DONE. ukrrs/mopac-pdf created (Gitea API/tea), seeded AGPLv3,
implemented, tested, smoked, pushed to main
(https://git.knownelement.com/ukrrs/mopac-pdf). Clone at
~/projects/meta/MOPAC/pdf.
Engine decision: typst (digest-pinned), pandoc+LaTeX rejected
Full rationale + measurements committed as docs/ENGINE-DECISION.md in
the repo. Summary, measured 2026-08-29 on identical content (TOC + table +
chart figure):
| typst 0.15.1 | pandoc 3.5 + xelatex | |
|---|---|---|
| compile | 0.94 s | 7.66 s (~8x slower) |
| image | ~200 MB | 758 MB |
| templates | typed functions, #show: report.with(...) |
LaTeX preamble/class surgery, multi-pass TOC |
| output | modern typography out of the box | classic LaTeX look |
typst also natively streams compile doc.typ - to stdout, which keeps our
engine layer pure bytes-in/bytes-out (no root-owned files, no temp
collisions). Supply chain per Charles' clarification: prebuilt Rust binary
in a digest-pinned image is tooling; OUR code is Go only.
Pin: ghcr.io/typst/typst@sha256:032e292...9c4c422f (= 0.15.1,
cross-checked), run --network none with a private /work root.
What shipped (v0)
- CLI
mopac-pdf: markdown + front-matter (title/subtitle/author/date/ classification/template) -> PDF to stdout or-o;-t/-Ttemplate pick/override (templates embedded in the binary AND loadable from a dir);-pageshelper; exit codes 0 ok / 1 usage-input / 2 engine. - Templates:
report(title page, TOC, numbered headings, running header, page X/Y + classification footer) andbrief(dense 1-3 pager, compact title block, classification badge, small tables). - Markdown subset the whole stack actually emits: headings, paragraphs, inline bold/italic/code/links, ul/ol, blockquotes, hr, GFM tables with alignment, fenced code, block images.
- Charts: fenced ```chart data blocks -> bar charts rendered PURE GO (vendored go-chart, MIT) -> PNG figures. One type proven end to end, per v0 scope.
- Dev in docker only:
dev.sh/makeroute through the family builder digest; deps vendored (hermetic); smoke drives the REAL typst container from the host (11 checks: both templates, stdin/stdout, page counts, exit codes, custom template dir — all green). - Tests: 7 packages ok — front-matter table tests, parser tests, golden .md -> .typ fixtures (drift-catching), chart PNG decode, tiny /Pages /Count parser tests, engine argv contract via stub docker, CLI flag/exit-code matrix.
Sample outputs (local, regenerable via ./dev.sh smoke)
- ~/projects/meta/MOPAC/pdf/out/sample-report.pdf — 4 pages (title, TOC, body + table + chart)
- ~/projects/meta/MOPAC/pdf/out/sample-brief.pdf — 1 page (dense exec brief)
- ~/projects/meta/MOPAC/pdf/out/stdin-report.pdf — stdin->stdout path
- fixtures live in-repo: testdata/sample-{report,brief}.md (+ .typ.golden)
How the briefing pipeline will call it
The COS briefing/harness side already speaks markdown; the contract here
is a single exec: mopac-pdf -o <path>.pdf <brief.md> (or pipe bytes:
mopac-pdf < brief.md > brief.pdf). Front-matter carries the identity
(title/date/classification/template=brief for the 0630 morning brief,
report for long-form), so callers never touch typst. Failure semantics
are harness-shaped: 0/1/2, engine stderr surfaced verbatim, no partial
output files on failure. Engine image + docker binary overridable by env
for canary/migration. No daemon, no state — a pure function, safe to call
from any loop turn.
Next (proposals, not started)
- More chart types (line, donut) — renderer is a one-function extension point; needs a data-block convention for series.
- More templates: letterhead, invoice, slide-deck-ish landscape.
- Nested lists >1 level; footnotes; typst-side escape hardening fuzz.
- Optional direct embedding of briefing charts without PNG round-trip (emit typst vector drawing for crisp print) once typst scripting surface stabilizes.