Paper Writer
A LaTeX skeleton is a contract between the researcher and the typesetter. Break it and the document does not compile, loudly. Build the skeleton first, then fill it.
Learning Objectives
- Treat a research paper as a structured artifact with a known section graph, not a freeform document.
- Generate a LaTeX skeleton that declares its abstract, sections, figure slots, and bibliography keys before any prose is written.
- Inject figures from experiment outputs (paths and captions) into the skeleton through a deterministic slot mechanism.
- Wire a mocked prose generator that fills each section from a structured outline so the harness is testable without a model.
- Emit a single `paper.tex` plus a `references.bib` plus a manifest that lists every figure referenced and every citation used.
Why a skeleton first
A skeleton is a paper's structure declared as data before any prose exists: sections as named, ordered slots, figures as slots with ids and captions, and bibliography keys declared up front with the entries they point at. Prose is then generated into those slots one at a time.
The alternative is a draft that starts as prose, and that accumulates structural debt. The introduction grows three paragraphs that belong in related work. A figure gets referenced before it is defined. The bibliography ends up with three keys for the same paper. By the time the author notices, rewriting costs more than writing did.
With a skeleton, the harness can check from the declared data alone, with no prose needed, that every cited key has an entry, every figure reference has a slot, and no figure id or bibliography key is duplicated.
Earlier lessons applied the same discipline to plans, tool calls, and traces. The structure is the contract.
The Paper shape
Every field is plain Python data, and the renderer is a pure function from Paper to a LaTeX string. Before rendering, the harness can introspect the paper: count sections, check that every \cite{key} has a matching BibEntry, and check that every figure reference has a matching Figure. Nothing in the harness checks that a figure's file exists on disk. The LaTeX compiler finds that out later.
The render contract
The render contract is the set of properties the renderer guarantees about its output. There are three. Every figure slot in the skeleton emits a \begin{figure} block with a stable label of the form fig:<id>. Every section emits a \section{} with a stable label of the form sec:<id>, so cross-references work. The bibliography emits a \bibliography block (only when the paper declares at least one entry) whose references.bib contains exactly the entries declared on the paper, no more and no fewer.
The renderer emits the labels and the bibliography file itself, so those properties hold by construction. What can break is the paper data feeding it, such as a section citing a key that was never declared. That raises PaperValidationError from _validate before rendering starts, not a warning. The skeleton is the contract, and a render that silently drops a figure breaks it.
Figure injection from experiments
Figure injection turns experiment outputs into Figure records. The earlier lessons in this track produced those outputs as JSON manifests, each carrying a list of artifacts with paths and short captions, and the paper writer reads them.
The injection is deterministic. Figure ids come from the experiment name plus a monotonic counter, and captions come from the manifest. Paths are normalised relative to the paper's output directory, so the LaTeX compiles even when the experiment outputs sit elsewhere on disk.
The mocked prose generator
The lesson does not call a model. A MockProseGenerator is a stand-in that emits prose deterministically. It reads an outline, which is one short string per section, and expands that string into two short paragraphs. The first weaves in the section title and the outline string. The second refers to each id in the section's figure_refs and cites each key in its cites, falling back to a generic sentence when both are empty.
That is enough to test every behaviour of the writer. A real implementation swaps the generator for a model call and the harness around it does not change. This is the value of declaring the prose generator as a callable: the test substitutes a deterministic one, production substitutes a model one, and the rest of the pipeline is identical.
The manifest output
The writer emits three files into the output directory.
The manifest is what a downstream evaluator or critic loop reads. It reads the manifest and does not parse LaTeX. The next lesson, the critic loop, takes this manifest as input and produces a feedback list. That is why the manifest is part of the contract and the LaTeX is not.
Validation gates
The writer runs six checks in _validate before it writes any file.
- The title is non-empty.
- The abstract is non-empty.
- Every figure id is unique within the paper.
- Every bibliography key is unique within the paper.
- Every key in a section's
citesis declared on the paper. - Every id in a section's
figure_refsis declared on the paper.
A failed check raises PaperValidationError with a precise reason, and the harness surfaces that reason as the failure mode. Validation runs inside render_latex, before paper.tex, references.bib, or manifest.json is opened, so a failed check means none of the three files is written. The guarantee is narrower than "nothing happened": write creates the output directory and runs fill_prose (which fills empty section bodies in place) before validating, so a failure can leave an empty directory and a mutated Paper. The three files are also written one after another, with the manifest last, so an I/O error midway could leave paper.tex without a manifest.
I'd write the validation gates before the renderer. Declaring sections, figures, and bibliography keys as data is what lets a failure surface before any file exists, so that is where I'd put my first tests.
I'd keep the prose generator a plain callable from the start. The mock lets every other part of the writer be tested without a model, and swapping in a model call later leaves the harness alone. I'd also treat the manifest, not the LaTeX, as the thing downstream code depends on: the critic loop takes the manifest as input, so it never has to parse LaTeX.
How to read the code
code/main.py defines Paper, Section, Figure, BibEntry, PaperValidationError, MockProseGenerator, PaperWriter, and a render_latex function. The write method takes an output directory and emits paper.tex, references.bib, and manifest.json. The read_experiment_manifest helper converts a list of experiment manifests into Figure records.
code/tests/test_paper_writer.py covers a skeleton render with no sections, a full render with two sections and two figures, the missing-citation gate, the duplicate-figure-id gate, manifest content, and the LaTeX-string contract (every section emits a \section{}, every figure emits a \begin{figure}).
Going further
Two extensions a real implementation will want. The first is multi-format render: the same Paper shape compiles to Markdown for blog posts and HTML for previews, with the renderer becoming a strategy on Paper. The second is citation enrichment: the writer fetches BibTeX entries from a citation key, given a local cache of DOIs. Both add value, and both can be added without touching the skeleton contract.
The skeleton is the bet: sections, figures, and citations declared as data, prose generated into slots, and a manifest emitted alongside the LaTeX. Other improvements compose on top of that.
Code
- main.py (python, 317 lines)
- tests/test_paper_writer.py (python, 189 lines)