Files
mopac-pmo/archive/REPORT-20260829-1300-pdf.md

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/-T template pick/override (templates embedded in the binary AND loadable from a dir); -pages helper; exit codes 0 ok / 1 usage-input / 2 engine.
  • Templates: report (title page, TOC, numbered headings, running header, page X/Y + classification footer) and brief (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/make route 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.