ikat 0.2.0

Weave Markdown into camera-ready LaTeX: mermaid-to-TikZ, pgfplots figures, publisher templates
Documentation
# ikat architecture

Version 0.2.0 · 2026-10-07 · 59 cargo + 45 pytest green.

Per-module test counts are deliberately not recorded here — they go
stale every release that adds a test, which is every release.
Regenerate them: `cargo test` (Rust) and
`VIRTUAL_ENV=$PWD/.venv maturin develop && .venv/Scripts/pytest tests/`
(Python, against the built module).

```
mini.md + ikat.toml
      │  fences, tables, citations
      ▼
┌───────────── python/ikat/pipeline.py ──────────────┐
│ Element(kind, body, caption, label, span?)         │
│ resolve_span(): toml default ← fence attr ← call   │
│ figure_env(): figure vs figure* + width            │
└──────┬──────────────────────────────┬──────────────┘
       │ mermaid fence                │ data (plots)
       ▼                              ▼
┌─ Rust src/mermaid.rs ──┐  ┌── Rust src/plot.rs ───┐
│ parse → layered layout │  │ tables → pgfplots code│
│ → tikzpicture          │  │ bar+whiskers, log-y,  │
└────────────────────────┘  │ lines+error bars      │
                            └───────────────────────┘
       └──────────────┬───────────────┘
                      ▼
        complete .tex ──► ikat.compile ──► pdflatex/bibtex ──► PDF
```

## 1. The layer contract

- **Rust owns everything computable.** Parsing, layout, numeric
  formatting, and code generation are deterministic byte-level work —
  exactly what Rust is good at, and exactly what must never differ
  between runs. Python never post-processes Rust output, only embeds it.
- **Python owns documents and tools.** Fence extraction, span
  resolution, float assembly, spec registries, and driving `pdflatex`
  are glue: thin, readable, replaceable. Strings cross the PyO3
  boundary; decisions do not — except through `BuildSpec`, the one
  shared model (see §4).
- **No matplotlib, ever.** Data graphics compile inside the document's
  own TeX run (pgfplots), so figure fonts are document fonts by
  construction. The pipeline cannot produce a font-mismatched figure.
- **No Node.js.** Mermaid renders through our own flowchart-subset
  parser, not mermaid-cli + browser + SVG round-trip.

## 2. End-to-end flow

There are two entry paths, and the distinction matters:

**Fragment path** (`pipeline.weave_fragment`, used by `demo.py`):
markdown + toml → `Element` list (kind, body TeX, caption, label,
span/pos/width). No registries, no numbering, no bibliography —
each fence converts standalone. This is the fast loop for single
figures and the demo.

**Document path** (`document.build_document` / `build_from_paths`,
used by `ikat build`, the showcase `build.py`, and the retired
paper builder): markdown + toml + `BuildSpec` → Rust `convert()`,
which returns a 5-tuple — woven body, document title (the H1),
diagram count, table count, and `FloatNeeds` (which of
`float`/`placeins`/`dblfloatfix`/`graphicx` the body provably
requires). Python then wraps the body: generated preamble, or a
level-1 `preamble_file`, or a level-3 `skeleton`; validates that
the wrapper carries every needed package; appends the bibliography
lines; and hands the complete `.tex` to `compile_pdf`.

The hand-off between the layers is narrow on purpose:

| direction | mechanism |
|---|---|
| Python → Rust | `BuildSpec` JSON (`spec_from_dict` is the single constructor the API, CLI `--spec`, and MCP `spec_json` all share), toml text, markdown text |
| Rust → Python | woven body + title + counts + `FloatNeeds`; error strings (line-numbered errors are M4.1, still open) |
| Python → TeX | complete `.tex`, staged `.bib`/`.bst`, figure PDFs |
| TeX → Python | the PDF path, or a `CompileError` with the log tail |

## 3. Rust core (`src/`, via `ikat._core`)

### 3.1 `mermaid.rs` — flowchart subset → TikZ

Deliberately not mermaid: `graph TD`/`graph LR` with `-->` edges,
`---` links, `|label|` edge labels, `[]` box nodes and `{}`
diamond nodes. Anything else — other directions, other node
shapes, subgraphs, styling statements — is a string error naming
the offending statement, because a diagram that almost renders is
worse than one that refuses.

Layout is deterministic layered (BFS depth from the roots, nodes
ordered within a layer), so the same source always emits the same
coordinates — byte parity is a feature, not an accident. Edges
carry `node[midway,above,fill=white,inner sep=1pt]` labels: the
white knockout keeps diagonal edges from striking through their
own labels. Fence attributes (`span`, `pos`, `width`, `scale`)
are parsed in Rust; the `[scale=]` splice exists in both the Rust
and the Python mirror (a past double-bracket bug is pinned by a
test on each side).

### 3.2 `plot.rs` — data → pgfplots

Two emitters, no plotting library: `barchart` (grouped bars with
min/max whiskers, optional log-y, optional dashed refline) and
`lineplot` (series with symmetric error bars, axis pinned to the
data so no meaningless pre-first-point gap appears). Numbers are
formatted plain-decimal (4 significant decimals max, no trailing
zeros, no scientific notation); tick labels are TeX-escaped
(`$\sim$` for approximations, never `~`).

**Legend placement** (`LegendPos`) is the module's second job.
Seven keywords: `auto` (default), `below`, four inside corners,
`outside-right`. `auto` tries the corners in top-left, top-right,
bottom-right, bottom-left order, testing each candidate box
against the drawn data — polyline plus error-bar segments for
lines (orientation-test segment intersection), whole-slot bar
rects up to the whisker top for bars (log-mapped for log-y, which
is what the eye sees), plus the refline — on visual ranges that
deliberately overestimate pgfplots' padding, so verdicts stay
conservative. Boxes are 30%-by-25% of the ranges: oversized on
purpose, so "free" really means empty. All four occupied, or a
degenerate range (single point), falls back to the under-axis
row, which clears tick labels by construction. Inside corners
cannot touch axis labels — labels live outside the axis box.
Unknown words fail naming the full set.

### 3.3 `table.rs`, `esc.rs`, `doc.rs` — the paper path

- `table.rs`: pipe tables → `tabularx` with per-table column
  specs (the appendix `Xcl` style), caption registry, TABLE
  numbering.
- `esc.rs`: the Unicode→LaTeX map, `$…$` math protection (math
  passes through untouched), code/emphasis spans, IEEE section-
  number stripping. Custom and kept even if the block parser ever
  moves to pulldown-cmark — escaping is orthogonal to splitting.
- `doc.rs`: `convert()`, the document assembler. Walks the line
  stream, dispatches fences/tables/citations/paragraphs, threads
  `FloatNeeds` upward, enforces the citation keyset (a dangling
  key fails the build instead of printing raw). The golden test
  rebuilds the frozen paper `.tex` byte-identical — the paper is
  the test suite.

### 3.4 `config.rs` — `ikat.toml`, validated

`Document` (class, options, columns, margins), `Spans`
(per-kind diagram/plot/table/picture/default), `Template`
(`preamble_file`, `preamble_append`, `abstract_before_maketitle`,
`skeleton`), `Floats` (fractions, counters, `barrier_sections`),
`Pos` (the seven positions with their exact LaTeX specs).
Defaults never move: `[floats]` tuning emits only when
non-default, so a default document weaves byte-identically before
and after the feature existed. Non-default values are validated
at parse time, not at TeX time.

### 3.5 `texenv.rs`, `tectonic.rs`, `md_spike.rs`

- `texenv.rs`: scans any preamble into `(probe file, tlmgr
  package)` needs — the static half of the texliveonfly trick.
  The subprocess half lives in Python (`texenv.py`: one
  `kpsewhich` call to probe, one `tlmgr install` to fix).
- `tectonic.rs`: the optional engine path (cargo feature, off by
  default so wheels stay lean). `tex_for_tectonic` drops the
  `inputenc` line; empty `bib_name` emits no bibliography lines
  because tectonic auto-runs BibTeX and dies on an empty
  `\bibliography{}`. Crate embedding does not link on Windows
  (C deps) — the supported path is the `TECTONIC_EXE`/PATH
  binary cascade, and embedding is a Linux-CI exercise.
- `md_spike.rs`: test-only harness comparing the hand scanner
  against pulldown-cmark events (verbatim info strings, identical
  headings, matching table shapes). The switch itself (M5.4) is
  gated on an empty diff plus a byte-identical golden paper.

## 4. Python layer (`python/ikat/`)

### 4.1 `pipeline.py` — fences, spans, floats

`extract_fences` splits the manuscript; `weave_fragment` maps
each fence to an `Element`. Span resolution is a three-level
cascade — toml kind default, fence attribute, explicit call-site
argument — with `picture` falling back to `diagram`. Position
resolution (`resolve_pos`) mirrors it against `[floats]`.
`figure_env` chooses `figure` vs `figure*` plus width, and
refuses the illegal combination (wide + `here`/`force`) as a
build error with the fix attached. Attr parsing (`_attr`,
`_width_attr`, `_span_attr`) is strict: unknown keys and doubled
tokens fail here, never in a TeX log. A Rust mirror of the
span/pos logic exists for the document path; the two are pinned
to each other by shared test vectors.

### 4.2 `document.py` — BuildSpec assembly

`BuildSpec` is the whole build in one dataclass: `diagrams`
(keyed caption registry), `plots` (keyed captions) + `plot_dir` +
`plot_insert_before` + `plot_attrs` (per-plot span/pos tuning),
`table_captions` + `table_specs` (per-table colspecs),
`title_thanks`/`author`, `bib_name`/`bib_style`/`bib_keys`
(derived from the `.bib` via `--bib`, never frozen),
`graphicspaths`, `preamble_override`/`preamble_append`/`skeleton`.
`spec_from_dict` is the one constructor; `to_json` round-trips
it for `--spec` files. Registries are hand-owned — the spec, not
the markdown, names captions — so renaming a caption cannot
desynchronize the document. `build_document` runs `convert()`,
wraps, validates, and returns `BuildResult` (title, body, full
tex, counts). Template helpers (`list_templates`,
`template_path`, `template_preset`, `list_skeletons`) serve the
CLI, MCP, and headsproof alike.

### 4.3 `compile.py`, `texenv.py`, `cli.py`, `mcp_server.py`

- `compile.py`: `compile_pdf(workdir, main, ensure_packages,
  engine)` drives `pdflatex`+`bibtex` passes (or the tectonic
  cascade), raising `CompileError` with the log tail instead of
  a return code. `ensure_packages=True` gates the build on the
  texenv probe. Deliberately excluded from MCP (workdir-bound,
  minutes-long) — drive it from the CLI.
- `texenv.py`: `check` (report) / `ensure` (install + re-probe)
  around the Rust scan; failure carries the manual texmf-install
  recipe and the MiKTeX Portable fallback.
- `cli.py`: twelve subcommands (`build weave check templates
  template skeletons skeleton flowchart barchart lineplot
  version`), each a thin call into the API. Plot commands take a
  JSON payload (file or stdin) with an optional `--legend`
  override.
- `mcp_server.py`: FastMCP stdio, twelve tools mirroring the
  CLI/API map one-to-one (minus `compile_pdf`). The surface
  test pins the full tool list, so a new tool without MCP
  registration fails loudly.

## 5. Spanning model

Two-column classes (`IEEEtran`, `acmart`, …) distinguish `figure`
(one column) from `figure*` (both). ikat resolves per element:

`ikat.toml [spans]` kind default → ```` ```mermaid {span=…} ````
fence attribute → explicit call-site argument.

Defaults (`diagram = "wide"`, `plot/table = "column"`) encode the
lesson from the Macrame paper: flowcharts need text width, data
graphics hold a column. The showcase paper proves the mix: a wide
pipeline diagram, a column float-resolution diagram, column plots,
and a `table*` token contract — all in one document.

## 6. Templates and skeletons

Level 1 (`preamble_file`) replaces everything before
`\begin{document}`; ikat validates the replacement carries a
`\documentclass` and every package the body provably needs, and
`preamble_append` splices extra lines before
`\begin{document}`. Nine shipped heads cover arXiv, 1/2/3-column
articles, IEEE, ACM, LNCS, Elsevier, APS — each with its
`bib_style` + toml preset, each compile-proven with abstract +
TikZ + table and PDF text verification.

Level 3 (`skeleton`) replaces the whole document. Token contract:
`{{body}}` and `{{bibliography}}` exactly once each
(`{{bibliography}}` waived when `bib_name` is empty, for biblatex
skeletons), `{{abstract}}` exactly once when the manuscript has
one (the whole woven env, honoring
`abstract_before_maketitle`). Unknown and duplicate tokens are
errors; `%` comments are invisible to both counting and
rendering, so skeletons can document themselves.
`skeleton-plain.tex` (article + tokens, `placeins`/`float`
preloaded) is the starting point. Validation scans the whole
skeleton for package needs — the generated tail's assumptions do
not leak into custom documents.

## 7. Strictness catalog

Fail-fast sites, each with a test: unknown mermaid statements,
missing edge targets, empty diagrams; ragged plot series;
dangling citation keys; unknown template/skeleton tokens and
duplicates; template heads missing class or packages; wide floats
with `here`/`force`; unknown float attrs or keys; unknown legend
words; `--bib` keysets that exclude a cited key. The doctrine:
nothing fails in a TeX log that could have failed in Rust or in
the wrapper. Line-numbered errors (statement echo + caret) are
the known gap (M4.1) — today these are context-free strings.

## 8. Testing and CI

Four layers: `cargo test` (unit + golden-paper byte parity +
spike diffs), pytest (end-to-end weaves, surface pinning, float/
skeleton/template suites), compile proofs (`plotproof`,
`floatsproof`, `headsproof`, `skelproof`, `tectproof` — each
compiles real TeX and verifies PDF content, not just exit codes),
and the showcase paper (`examples/ikat-paper/`, a 6-page paper
about ikat built by ikat: the release demo and the composition
proof). `.github/workflows/ci.yml` runs cargo + pytest on
ubuntu/windows/macos and rebuilds the demo + showcase on real
TeX Live, failing on any undefined citation. `release.yml` gates
tags on version equality and publishes PyPI wheels + crates.io
in parallel.

## 9. Roadmap

The plan of record is `docs/ROADMAP.md` (milestones M1–M6 with
acceptance gates). Done: pipeline parity + golden master (M1,
M4.4), floats (M5.1), skeletons (M5.2), CLI/MCP/surface parity
(M4.2, M4.8), legend auto-placement (M2.4), CI + release
workflows (M4.2b). The open frontier: benchmark-JSON presets and
plot regeneration (M2.1, M2.2), figure subsets + layout (M3), the
pulldown-cmark switch (M5.4), line-numbered errors (M4.1), and
shipping itself (M4.3 — names claimed, tokens pending).