TAI BUI
Back to curriculum
CapstonePython

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.

TIME: ~90 minutesPREREQUISITES: Phase 19 lessons 50-53

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.

Check: Why a skeleton first0/3
1. What is a skeleton in the paper writer?Define
2. Why is it cheaper to start from a skeleton than from a prose draft?Explain
3. Why can the harness check cites and figure references before any prose exists?Explain

The Paper shape

Mermaid diagram

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.

Check: The Paper shape0/2
1. In the paper writer, what is the renderer?Define
2. write() succeeds, but pdflatex later stops because figs/loss.pdf is missing. Which assumption did the author get wrong?Analyze

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.

Check: The render contract0/4
1. What is the render contract?Define
2. Which label formats does the renderer give every figure and every section?Define
3. The renderer already emits the labels and the bibliography file itself. Why does the writer still need PaperValidationError?Explain
4. What does `references.bib` contain after a successful write?Explain

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.

Mermaid diagram

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.

Check: Figure injection from experiments0/5
1. What is figure injection?Define
2. Why are figure paths normalised relative to the paper's output directory?Explain
3. Why are figure ids built from the experiment name plus a monotonic counter?Explain
4. read_experiment_manifest receives two manifests. Manifest exp-a has artifacts x.pdf and y.pdf, and manifest exp-b has artifact z.pdf, all with captions and non-empty paths. Which figure ids does it return, in order?Apply
5. A manifest named x has two artifacts. The first has an empty path, the second has path /abs/q.pdf. What does read_experiment_manifest return?Apply

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.

Check: The mocked prose generator0/4
1. What does MockProseGenerator do?Define
2. Why is the prose generator a callable injected into PaperWriter rather than hard-wired into it?Explain
3. A Section has id related, title Related Work, no cites and no figure_refs, and the generator has no outline for it. What does MockProseGenerator return?Apply
4. A Section already has body 'mine'. PaperWriter.fill_prose runs on the paper, and the generator has an outline for that section. What is the body afterwards?Apply

The manifest output

The writer emits three files into the output directory.

Mermaid diagram

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.

Check: The manifest output0/5
1. What three files does PaperWriter.write emit on success?Define
2. What is manifest.json for?Define
3. Why is the manifest part of the writer's contract instead of just the LaTeX file?Explain
4. A critic stage extracts the list of cited keys with a regex over paper.tex. It breaks when the renderer changes how it prints citations. What design rule was ignored?Analyze
5. An evaluator must flag sections whose body is suspiciously short. The writer's manifest has a body_chars number for each section. Which design uses the contract correctly?Design

Validation gates

The writer runs six checks in _validate before it writes any file.

  1. The title is non-empty.
  2. The abstract is non-empty.
  3. Every figure id is unique within the paper.
  4. Every bibliography key is unique within the paper.
  5. Every key in a section's cites is declared on the paper.
  6. Every id in a section's figure_refs is 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.

Check: Validation gates0/5
1. Which failure makes `_validate` raise `PaperValidationError` before any file is written?Explain
2. Two BibEntry records in a paper share the key `smith2020`. What happens on `write`?Apply
3. A section's `cites` lists `lee2022`, but `paper.bibliography` has no such key. You call `writer.write(paper, out_dir)`. What is true afterwards?Analyze
4. write() raised PaperValidationError for an unknown citation. The author fixes the bibliography, edits the outline for the intro section, and calls write() again on the same Paper object. The intro prose still shows the old outline. Why?Analyze
5. Design A validates inside render_latex, before any output file is opened. Design B writes paper.tex first and validates afterwards. Which failure does B risk that A avoids?Analyze
Author's Note

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}).

Check: How to read the code0/2
1. A test builds Paper(title='A & B', authors=['X'], abstract='x') with no sections and renders it. What happens to the title?Apply
2. render_latex gets Paper(title='T', authors=['A'], abstract='abs') with no sections, figures or bibliography. What does the output look like?Apply

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.

Warm-up0/3
1. You are testing code that normally calls a language model. How do you keep the tests deterministic?Warm-up
2. An experiment run saves its results so a later stage can read them. Which format makes that handoff easiest to read reliably by program?Warm-up
3. A script must write three output files, and its input may be invalid. Which order of work is safest?Warm-up
Check: Going further0/1
1. You must also publish the same paper as a Markdown blog post without weakening the skeleton contract. Which design fits?Design

Code