# Math Worksheets

> Generate professional math practice worksheets and full answer keys as PDFs. Compiles LaTeX to PDF using tectonic (free, no account needed). Supports any math topic from elementary through AP Calculus BC (Pre-Algebra, Algebra 1/2, Geometry, Pre-Calc, Calculus AB/BC — limits, derivatives, integrals, series). Handles coordinate plane grids, geometric figures, tables, and multi-part problems. Use when a user asks for a math worksheet, practice problems, homework help sheet, or answer key for any K-12 math topic including calculus.

- Skill: `stellawuellner/math-worksheets` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add stellawuellner/math-worksheets`
- Raw SKILL.md: https://api.skillmd.com/api/skills/stellawuellner/math-worksheets/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: stellawuellner (https://skillmd.com/u/stellawuellner)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/stellawuellner/math-worksheets

---


# Math Worksheet Generator

Generate a student worksheet PDF + full step-by-step answer key PDF for any math topic from elementary through AP Calculus BC. Compiles LaTeX with `tectonic` (no TeX installation required — it auto-downloads packages).

This skill is **agent-agnostic**: it works in any agent harness that can read files and run shell commands (Claude Code, Gemini, Codex, OpenClaw, etc.). It uses no harness-specific primitives; every step is plain file I/O and shell commands, and the platform-specific delivery step is optional with a portable fallback.

Throughout this document, `$SKILL_DIR` means the directory containing this SKILL.md. Resolve it once at the start (e.g. `SKILL_DIR=/path/to/math-worksheets`) — do not rely on `$0`, which is only meaningful inside a script. In Claude Code the skill's directory is already available as `${CLAUDE_SKILL_DIR}`, so `SKILL_DIR="${CLAUDE_SKILL_DIR}"` works; other agents resolve the path however they locate the skill.

## A note on accuracy

Generate the problems with whatever model your agent is already running. Correctness does not depend on the model getting the math right the first time: the SymPy verification gate (steps 4–5) independently checks every answer and blocks the build if any check fails, so a wrong answer never reaches the PDF. Stronger reasoning models simply need fewer retries to pass the gate. The skill does not detect, switch, or recommend models.

## Limitations

What the gates guarantee is bounded, and the boundaries are deliberate:

- **Manual answers are not machine-verified.** Proofs, constructions, sketches, and "explain your reasoning" responses are declared `manual` in the verification file and printed with a ♠ marker in the answer key; only a human can grade them. A sheet with zero machine-checkable answers refuses to build unless explicitly acknowledged (step 4).
- **Verification proves the math and its transcription, not pedagogical fitness.** A green gate chain means every printed answer was recomputed and matched — not that the problems teach the requested topic well or ramp sensibly. Those judgments stay with the human reviewing the output.
- **`approx` and trap checks confirm the arithmetic of the formula you wrote, not that the formula fits the story.** Keep expressions faithful transcriptions of the problem's givens; see the trust-boundary notes in step 4.
- **The verifier's guarantee is version-specific.** Its accuracy was measured against ground-truth corpora on a pinned SymPy release; every run stamps the version it used and flags drift from the measured baseline.

## Prerequisites

**tectonic** (LaTeX compiler — auto-downloads packages on demand). Install with whichever is available:

```bash
brew install tectonic       # macOS or Linux with Homebrew
cargo install tectonic      # any platform with Rust
sudo apt install tectonic   # Debian/Ubuntu (recent releases)
# or download a release binary: https://github.com/tectonic-typesetting/tectonic/releases
```

**python3 + sympy** for answer verification: `pip3 install sympy`. The scripts pick the first python3 that can actually `import sympy` (machines often carry several pythons); pin one with `MWS_PYTHON_CANDIDATES=/path/to/python3` if needed.

**No tectonic?** `scripts/compile.sh` falls back to `pdflatex` automatically. Unlike tectonic, pdflatex does not download packages — install the full set up front: `texlive-latex-base texlive-pictures texlive-latex-recommended texlive-latex-extra` (the last one supplies `enumitem`, `mdframed`, and friends used by the templates).

Output directory (create if needed): `~/Documents/Worksheets/`. In headless or sandboxed environments where the user won't browse `~/Documents`, use `./worksheets/` inside the workspace instead and report the paths.

## Workflow

### 1. Gather requirements

Ask (or infer from context):
- **Student**: name, grade, course (e.g. "8th grade, Pre-Algebra")
- **Topic**: e.g. "factoring trinomials", "solving two-step equations"
- **Problem count**: default 10 if not specified
- **Page use**: the page budget is COMPUTED from the problem set (`scripts/page_budget.py`), not fixed. Fifty graphing problems that each need a coordinate plane legitimately run past 20 pages and are allowed to; a flat cap could only be met by shrinking the work space, which is the one thing that must never give. `build.sh` prints the ideal page count and the double-sided sheet count before compiling, so paper cost is visible. If a set is bigger than intended, reduce the PROBLEM COUNT, never the work space. Hard ceiling: 100 problems — past that, split into several worksheets
- **Paper size**: US Letter by default. For A4 or Legal, add the paper as a documentclass option (`\documentclass[12pt,a4paper]{article}`) with metric geometry margins, and pass `--paper a4` to `page_budget.py`. See `references/latex-templates.md` → "Paper size"
- **Accessibility**: for large-print or dyslexia-friendly output use `\documentclass[17pt]{extarticle}` (or 14pt) plus `\accessiblemode{large|dyslexia|both}`. The page budget adapts automatically; larger type simply means more pages. See `references/latex-templates.md` → "Accessibility"
- **Locale**: `\mwslocale{eu}` prints decimal commas and `\times`. The verify JSON stays canonical (period decimals) — only the printed form is localised, via `\dec{}` and `\mtimes`. Pair this with A4 paper for European users
- **Format preference**: timed quiz, homework practice, mixed difficulty, or topic drill.
  For a **timed quiz**, point values are the default: place `\probpts{N}` at the end of
  each stem and `Score: \underline{\hspace{1.5cm}} / \totalpoints\ pts` in the title
  block (rendered in step 4c). Difficulty stars (`\probmeta{N}`) are opt-in for
  practice/mixed-difficulty sheets on request; skip markers for early-elementary
  drills unless asked.

**Photo input shortcut**: If the user provides a photo of homework or a textbook page, read the image with whatever vision capability your agent provides (or by reading the image file directly) to extract problem types, format, and difficulty, then mirror that style exactly.

### 2. Design problems

Design problems appropriate to the student's level. Increase difficulty gradually across the set. Every problem must be mathematically correct — verify your own solutions.

See `references/problem-library.md` for topic-specific problem type menus.

**The grade level never prints on the worksheet or the study guide.** `\wstitleblock` takes a course argument and does not typeset it: a child working a grade below or above reads the label before the mathematics, and it tells the student nothing they need. The level is the adult's information, so it prints on the ANSWER KEY — in the title block and again in a generated **Curriculum** section beside the standards codes and the difficulty range, which is where a parent or teacher actually looks. Keep passing the course argument: it is the one place the level is declared, and the answer key's curriculum section reads it from there.

**Tag every problem** with `"standard"` (a code from `references/standards-map.md` — never invent codes, and **grep that file before concluding it lacks a row**: 11 reviewed cases asserted a missing row that was there, two of them naming the exact code they called absent, and then tagged a high-school sheet with a grade-8 standard), `"difficulty"` (1–5 per that file's ladders), `"bloom"` (recall/apply/analyze/justify, same file), and `"skill"` (a short stable name for the skill the problem exercises, e.g. `"right-triangle-trig"` — reuse the same name across problems of the same Part). Skill tags are all-or-nothing and GATING: the study guide must contain an entry tagged with each distinct worksheet skill, enforced by `build.sh`'s `coverage-ss` gate; a partially tagged sheet fails it. Ramp difficulty: start at 1–2, majority 2–3, end with one or two 4–5 challenges. Verification reports standards coverage, the bloom mix, the skill mix (the set the study guide must cover), and flags ramp drops, so a parent can see exactly which standards the sheet exercises and at what cognitive level.

**Facets (skill coverage):** a facet is a distinct method the sheet tests (e.g. `side-from-angle` vs `angle-from-sides` vs `pythagorean`) — standards codes are usually one constant per sheet and cannot show this. For sheets of 10+ problems, declare top-level `"facets": [...]` in the verify JSON and tag **every** problem with a `"facet"` from that list; consult `references/problem-library.md` → "Facet checklists" (topics without a table use your own lowercase-kebab names). The gate is strict: an unlisted facet, a planned facet with zero problems, or an untagged problem is a build failure. Tag TRUTH is your responsibility — the gate verifies plan/tag consistency, not that a tag describes its problem. Tag the `verify_ss_` entries' `"facet"` too: the facet-coverage gate (step 5) requires every worksheet facet to have a study-guide worked example.

**Interleave after the warm-up:** a blocked warm-up over the first third of the sheet is fine; after that, keep same-facet runs to 3 or fewer so the student must *choose* the method — unless the sheet is genuinely a drill, declared with top-level `"format": "drill"`. `verify.py` flags longer runs (exit 2, manual review) with a concrete swap suggestion. See `references/problem-library.md` → "How to interleave".

**Misconception traps:** consult the misconception table (`references/problem-library.md` → "Misconception traps & error analysis") when choosing givens — pick numbers so the canonical wrong method lands visibly off the correct answer — and declare `"traps"` on the 2–3 problems you designed that way. Traps are optional, but declared ones are machine-checked (step 4) AND printed in the answer key as a "Common wrong answers" block (`scripts/render_quick_answers.py` emits one `\commonerror` line per trap, so the grader sees "If they got 7.37: used cos instead of tan"). Declaring traps therefore pays twice: it hardens the problem design and it turns grading into teaching.

**Tiered worksheets (differentiation, on request):** when asked for tiers (support/on-level/challenge), build ONE set of problem skeletons, then re-parameterize per tier — same structure and standards, different givens and difficulty band (support: 1–2 with a hints box and a worked first step; core: 2–3; challenge: 3–5). Each tier gets its OWN verify JSON (the gate re-checks every tier) and file prefix: `wsS_`/`wsC_`/`wsX_` + matching keys. Because construction is JSON-first, a tier is a data change, not a rewrite.

**Tier naming for the build driver:** give each tier its own *stem* — `verify_factoring_S_2026-07-27.json` with `wsS_factoring_S_2026-07-27.tex` (or plain `ws_factoring_S_...`; both are discovered). What matters is that two tiers never share a stem in one directory: `scripts/build.sh` derives the document names from the verify JSON's stem, and two candidates for one role is a hard discovery error, not a guess.

### 3. Write LaTeX source

Write **three** `.tex` files to `/tmp/`:
- `ws_TOPIC_DATE.tex` — student worksheet (blank work areas)
- `ak_TOPIC_DATE.tex` — answer key (full step-by-step solutions)
- `ss_TOPIC_DATE.tex` — skills summary / study guide (cheat sheet)

The **skills summary** is a reference card (2 pages by default — see "Length" below) the student can use while working through the worksheet or when studying. Per skill section: **formulabox → examplebox → tryitbox**. Its structure is not house taste; each element carries a documented learning effect, and the macros exist so using the effect costs one line:

- The worked example IS the worked-example effect: novices learn more from studying a worked solution than from solving, and the `\step` structure with a strategy first line is what makes the reasoning visible rather than just the arithmetic.
- The **try-it** is retrieval practice — the single best-supported study behaviour there is. It re-parameterizes the example (same skeleton, new givens) and prints only the stem plus the upside-down answer, so checking requires attempting first.
- **`\why{...}`** (optional, at most one per example) prints a one-line self-explanation aside after a step — *the reason the move is legal*, not a restatement of it. A printed model of self-explanation teaches the habit the research prompts train. Keep it conceptual and number-free; the prose checker reads any numbers in it against the entry.
- **`\fadestep{...}`** (optional, inside a try-it) turns it into a completion problem: the setup shown ("Started for you: …"), the finish left to the student. Backwards-fading measurably beats the straight example-to-problem jump **on multi-step skills** — use it when the example needed 3+ computational steps, skip it on one-move skills where the bare try-it is the right next step. End the box with the check answer right-aligned: `\hfill\rotatebox{180}{\footnotesize check: $\ans{...}$}`.
- **Dual coding:** when a skill is spatial (trig ratios, transformations, area models, number lines), put a small **value-free** diagram in its formulabox next to the rule, built from the shipped figure macros or house styles — a rule the student can *see* is retained better than the same rule in symbols alone. Value-free, so the figure-scope rules never apply; budget for it via "pages" below if two diagrams push past the default.
- Boxes carry printed title tabs (RULE / EXAMPLE / TRY IT / WATCH OUT) so a student scanning for the formula finds it without reading — do not add your own headings inside boxes.

It contains:
**Length.** The default budget is **2 pages**, and the discipline of the small page is most of what makes a reference card one. Declare a top-level `"pages"` (integer 1–6) in the verify_ss JSON only when (a) diagrams the skills genuinely need — a unit circle, a transformation grid, labelled solids — do not fit the default, or (b) the user asked for a longer guide. `verify.py` validates the field; `build.sh` reads it for the compile-ss page cap. The five-full-sections ceiling below scales with it: about 2–3 full sections per page.

**Study guide without a worksheet.** When the user wants ONLY a study guide ("make Leo a reference sheet for the unit circle"), write just `ss_TOPIC_DATE.tex` + `verify_ss_TOPIC_DATE.json` and build with `bash scripts/build.sh /tmp/verify_ss_TOPIC_DATE.json --study-guide-only`. Every guide-integrity gate still runs (verify, template, compile with page cap, answer binding, structure, prose, overprint); only the sheet-relative gates (skill coverage, facets) skip, because there is no sheet. The guide's worked examples are still fully machine-verified.

- One section per distinct skill tested, matching the worksheet's `"skill"` names. **The slug belongs in the JSON, not in the heading** — `\skillheading` is gated at 57 characters (the budget includes the `"Skill N --- "` prefix, which eats about 12), and the coverage gate reads the JSON tag, never the printed title. Six eval-run agents overflowed this heading by echoing the slug in parentheses to make the tagging visible to a reader; it does not need to be visible there. Sections are — `build.sh`'s `coverage-ss` gate fails the build if any worksheet skill lacks a tagged study-guide entry. **Five full sections is the hard ceiling** for the 2-page cap (a full section costs ~234pt against a 1382pt two-page budget); aim for 3–4. When over budget, drop the watch-out box first, then merge two thin skills, then shorten worked examples — never drop the try-it. Add no `\vspace` between boxes; they carry their own spacing. See `references/latex-templates.md` → "Page budget"
- A **formula/rule box** (blue) per skill — the key facts and formulas
- A **mini worked example** (green) per skill — a `\step` strategy line naming why this tool applies, then the computation — fewer steps than worksheet problems, never a bare answer chain (see the exemplars in `references/latex-templates.md`)
- A **try-it** (violet) per worked example — a re-parameterization of that section's example (same skeleton, new givens), printing ONLY the stem plus the verified answer upside down INSIDE the box via `\rotatebox{180}{\footnotesize check: $\ans{...}$}` — no worked steps; solving it is the student's job. Formula-only sections (no example) are legal and need no try-it
- An optional **watch-out box** (orange) — common mistakes worth flagging
- Optional **key vocabulary** section at the bottom

See `references/latex-templates.md` → "Skills Summary / Study Guide Template" for the full shell and box macros.

**Verify the study guide too.** Its worked mini-examples are math the student learns from first, so they must not be exempt from the gate (audit 3c). Write a second verification file `/tmp/verify_ss_TOPIC_DATE.json` — one entry per worked example's computation AND one per try-it. Entries MUST be listed in document order (example, try-it, example, try-it, …) because binding is positional; tag each try-it entry `"role": "tryit"` and each entry with the `"skill"` it demonstrates. The build driver (step 5) verifies it and binds its printed answers automatically; a missing `ss_` document or `verify_ss_` JSON is a **build failure**, not a skip. Formula boxes (no computed answer) need no entry; every *worked example or try-it with a printed result* does. `check_answer_key.py` segments ss documents **by box** (`examplebox`/`tryitbox`) — keep one worked example or try-it per box, and one **id** per box — so box count = `problem_count` = the number of distinct ids. A box whose example needs more than one check (a distance AND the perimeter built from it) may carry several entries under that one id; what must not happen is two boxes sharing an id, or one box holding two, and print each result with `\ans{...}` (defined in the study-guide shell); a result set in bare `\boldsymbol` is invisible to the gate. A try-it may be type `manual` only when its paired example is also `manual` (exit-2 visibility, never silent). The structure gate (`tests/check_study_guide.py`) additionally requires ≥2 `\step` lines and a boxed final answer per examplebox — the first step is the strategy sentence, capped at ONE sentence (page 1 fills fast and the guide is hard-capped at 2 pages — enforced mechanically: the `compile-ss` gate reads the page count from the engine's log and fails a 3-page guide).

**Subtitle from facets:** when `"facets"` is declared, write a top-level `"subtitle"` in the verify JSON composed from that facet list *first*, then copy it verbatim into the worksheet's title block. The facet-coverage gate (step 5) binds the two, so the title can never promise a skill the sheet doesn't plan.

See `references/latex-templates.md` for document templates, coordinate planes, tables, geometric figures, and answer key patterns.

**Use the figure HOUSE STYLES before raw TikZ** — `references/latex-templates.md` → "Figure house style". The preamble ships graph styles (`wsgrid`, `wsgridwide`, `wsgridq1`, `wstrig`, `wsfuntall` + `wscurve`/`wsasym`/`wsopen`/`wsclosed`), chart styles (`wsbar`, `wshist`, `wsboxplot`, `wsboxplot pair`, `\wsdotplot`, `wsstemleaf`, `\wspictorow`) and `figure-macros.tex` ships geometry marks (`\congtick`, `\parallelmark`), a transformation grid (`\gridtrifig`), fixed-size solids (`\cylfig` … `\cylnetfig`) and the K-4 model set (`\tenframefig`, `\arrayfig`, `\basetenfig`, `\clockfig`, `\fraclinefig`, `\tapefig`, `\coinrowfig`, …). Four rendered-page reviews found the raw-TikZ route producing grids students cannot plot on, self-overprinting solids, color-only semantics that die in photocopy, and hand-rolled K-4 models with broken proportions — the styles encode the fixes once. Two standing rules: figures are SPACE-GENEROUS (budget `workspace_cm` for the style's natural size rather than scaling it down), and every "sketch/plot" problem's answer key prints the model graph on the same grid style the worksheet used.

**Draw diagrams in TikZ, not as imported images.** TikZ uses the document's own fonts and line weights, so a figure's labels match the surrounding mathematics exactly — an imported bitmap or an externally-drawn PDF does not, and cannot be regenerated when the numbers change. The shipped preamble loads TikZ, pgfplots and the libraries below for every document, so nothing needs requesting:

| Library | Use it for | Template |
|---|---|---|
| `arrows.meta` | number lines, vectors, mapping diagrams — `-{Stealth}` reads as an arrow at small sizes where a bare `->` reads as a tick | "Number line" |
| `patterns` | shaded fraction / percent / probability area models; hatching survives photocopying where a light grey fill does not | "Shaded area model" |
| `decorations.pathreplacing` | braces on bar models and part-whole diagrams | "Brace annotating a part" |
| `pgfplots: statistics` | box plots | "Box plot" |
| `pgfplots: fillbetween` | area between curves | "Area between two curves" |
| `pgfplots: groupplots` | two displays with shared limits | "Two displays for a compare item" |
| `adjustbox` | scaling a figure that is genuinely too wide | "Fitting an oversized figure" |
| `positioning`, `intersections`, `shapes.geometric` | relative node placement, named-path constructions, sorting-diagram nodes | — |

**The binding rule applies to every one of them: a figure's numbers come from the same JSON the checker reads.** The sharpest trap is the box plot. Hand pgfplots raw data and it computes quartiles by an interpolating convention, while `verify.py`'s `stats` type uses school median-of-halves — on `[4, 6, 7, 9, 11, 12, 18]` verify says median 9, q1 6, q3 12 and pgfplots draws median 8 with a box from 5 to 11.5. The sheet would print a figure contradicting its own answer key, and no gate reads inside a plot. Declare the five-number summary as `stats` entries and pass those values with `boxplot prepared`.

`forest`, `tikz-cd`, `circuitikz`, `chemfig` and `quantikz` are deliberately NOT loaded — see the reasons in `templates/worksheet-preamble.tex`. Factor trees and probability trees, the one genuinely mathematical case, draw fine with TikZ's built-in `child` syntax at no extra load cost.

**Answer-key shell:** the `ak_` document starts exactly like the worksheet
(`\input{worksheet-preamble}`), then `\akheader{TOPIC}` + `\aktitleblock{...}`, and ONE
`\input{qa_TOPIC_DATE}` line directly under the title block — the build driver's
quick-answers gate regenerates that bank from the verify JSON every build and **fails a
key that hand-rolls its preamble or never `\input`s the bank** (both messages teach the
fix). `\akheader` switches `\ans{...}` to a compact same-line box, so end each worked
solution's last step with `\ans{x = 5.00}`; keep the display `\[ \boxed{...} \]` form
for long or multi-value answers. See `references/latex-templates.md` → "Answer Key
Patterns".

**Preamble — \input the shipped template, never retype it.** Start every document with:
```latex
\documentclass[12pt]{article}
\usepackage[margin=1in, top=0.75in, bottom=0.75in]{geometry}  % ss_: margin=0.85in, top/bottom 0.7in
\input{worksheet-preamble}
\input{figure-macros}    % when using the shipped figure macros (\rtfig, \trifig, \refrt)
```
`$SKILL_DIR/templates/worksheet-preamble.tex` carries the packages, `\problem`, `\fittedtitle`, the `\wsheader`/`\akheader`/`\ssheader` + title-block macros, and the study-guide box environments; `templates/figure-macros.tex` carries the figure macros. `compile.sh` (and therefore `build.sh`) stages both files beside your `.tex` automatically, so `/tmp` compiles just work — but only when you compile through the scripts, never by invoking the engine directly. Only `geometry` stays in the document (margins differ between worksheets and study guides). `build.sh` enforces this mechanically — the `template-ws`/`template-ak`/`template-ss` gates (`tests/check_template_use.py`) fail any document that hand-rolls its shell, `\input`s a non-shipped preamble file, or redefines a shipped macro/environment/color.

**Work space defaults**: `\problem[5cm]{...}` per problem; `8cm` for multi-step; `10cm+` for graphs.
The workspace must live **inside the problem's unbreakable block** (the `\problem` macro's
minipage, or a per-item minipage in lists — see the templates): `\vspace` glue outside it
is silently discarded when it falls at a page or column break, leaving a bottom-of-page
problem with zero room to work. These are a floor, not a suggestion: `tests/check_layout.py`
fails a worksheet whose problems get under 2.5cm and flags workspace `\vspace` left outside
a minipage. A sheet with correct answers and nowhere to write them is a sheet the student
cannot use.

**Page overrun: count problems per page BEFORE choosing a remedy.** `page_budget.py`
prints two NOTEs whose fixes point in opposite directions, and applying the wrong one
makes the sheet worse. `pdftotext` the failing PDF and look at what actually landed on
a page:
- **2 or more problems per page** — the blocks pack fine and the budget is simply
  under-measuring them (its stem charge is a flat 0.6cm). **Declare `workspace_cm`**
  covering the block's real height. That is the honest move: the declaration is a
  measurement, and it raises the ceiling without adding content.
- **1 problem per page** — the block is taller than half a page, so nothing can share
  with it and the rest of every page is stranded. **Shorten the block** — a smaller
  figure, or the work space *beside* the figure instead of under it. A larger
  `workspace_cm` here raises the ceiling without recovering one centimetre of the
  stranded space, and it is no longer a measurement of anything.

A side-by-side `minipage[t]` figure/question layout is the usual way to recover that
stranded whitespace, and it compresses no work space — but put `\vspace{0pt}` first in
BOTH columns, or a `tikzpicture`'s bottom-sitting baseline makes the columns stack and
doubles the block instead (the tell: shrinking the figure changes the page count by
exactly zero).

**Answer location (every problem gets one):** `\problem` sheets emit the right-aligned
answer blank automatically whenever the workspace argument is positive — write nothing
extra. On enumerate/`\item` sheets, end every item with `\ansline` (or an inline
`\ansblank` for drill formats: `$7 + 5 =$~\ansblank`). Mark `\noansline` only where the
worked product IS the answer — graph sketches, proofs, constructions (typically the
`manual`-type set). `tests/check_layout.py` enforces one answer-location macro per item;
answer keys and study guides are unaffected.

**Units are verified data, not decoration:** any problem whose final answer carries a
measurement unit declares `"answer_unit"` in its JSON entry (`"ft"`, `"m"`, `"cm^2"`,
`"square units"` — distinct from the deg/rad angle-mode field `"unit"`) and ends with
`\answerline{<unit>}` on the sheet (write exponents math-wrapped: `\answerline{cm$^2$}`;
`\answerline` replaces `\ansline` there and suppresses `\problem`'s automatic blank).
The key must print the same unit inside `\ans{}`/`\boxed{}` (`\text{ft}`).
Both directions are gated: `tests/check_answer_line.py` fails a declared unit with no
matching `\answerline` and an `\answerline` unit the JSON never declared;
`tests/check_answer_key.py` fails a key whose box omits the declared unit or prints a
unit the JSON never declared — a metres problem answered in feet no longer passes any
gate. Prefix currency symbols are out of scope: write "in dollars" in the stem.

**Figure scope**: a figure carrying numbers belongs to one problem, but on the page it
merely sits *near* several. If problem 6 shows a triangle labelled `a=6, b=8` and
problems 7-8 show none, a student reading 7 will apply the nearest figure to it — the
figure is correct and the worksheet is still wrong. So within one problem list, **no
problem may be left with no figure at all while another problem's figure carries
values**. A problem holding its own *value-free* figure — a blank coordinate grid to
plot on, an unlabelled diagram to mark up — is fine, and mixes freely with graphed
problems beside it: the picture inside its block is visibly the one it means, and a
grid's axis numbers are its own scale, not another problem's data.
Shared labelling conventions go in a single value-free reference figure placed with the
directions, captioned so it cannot be mistaken for a problem's givens, e.g. "How every
triangle here is labelled. No values shown: use the numbers given in each problem."
Use the shipped `\refrt` macro for it. Its convention is the renderer's: sides `a/b/c`
opposite vertices `A/B/C`, right angle at `C`, hypotenuse `c` — the reference figure and
the renderer-built `\probfig` figures share one page, so
`tests/test_figure_convention.py` fails the build if the macro and
`render_figures.py` ever mark the right angle at different vertices.
`tests/check_layout.py` enforces the scope rule.

**A data display several problems read from goes ABOVE the first `\problem`.**
The scope rule is all-or-nothing per problem list, and a data display IS its
values, so the rule's usual remedy — make the figure value-free — cannot apply
to it. A bar chart set inside problem 3 of a mixed graph/table/algebra sheet is
a valued figure beside figureless problems, and `check_layout` fails the sheet;
an author who does not know the pattern deletes the chart or fights the gate.
Problem regions run from one `\problem` to the next, so a display placed before
the first one belongs to no region and is scoped to none of them. Caption it
with the problems that use it — "Problems 4-7 refer to this chart" — so a
student is never left guessing which givens are theirs. Five eval batches
arrived at this independently; it is the standard answer for mixed-representation
sheets.

**Know what that costs, because nothing else will tell you.** A display above
problem 1 is outside every problem region, so `figure_label_numbers` never reads
its printed values against the JSON: a drifted axis tick or a mis-plotted point
on a SHARED graph is invisible to the entire gate chain, on the one figure the
most problems depend on. It is also invisible to the page budget — charge its
height to the problems that consume it via `workspace_cm`. So: source the
plotted values from the same `data` array the `read_data` checks use (never
retype them), and read the rendered page yourself before shipping. Hoisting a
figure out of a problem to quiet the scope check, when only that problem uses
it, trades a gate that works for one that cannot run — keep a single problem's
figure inside its own block.

**Triangle figures MUST come from the renderer, never hand-drawn TikZ with values.**
Step 4b generates `\probfig{N}` macros from the verify JSON — every `triangle` problem
automatically, plus `approx` and `eval` problems that declare a `"figure"` object. A
write-the-ratio problem ("write $\tan A$ as a fraction") is verified as `eval` and is
renderer-figured like any other right triangle — the renderer covers it, so the
hand-macro escape hatch never applies to it. Reference the macros
in the worksheet instead of writing TikZ: hand-computed figure coordinates are exactly
the retyping drift the JSON-first pipeline exists to prevent (the reference SSA swing
figure itself shipped with wrong hand-computed constants until the renderer replaced
them). Leave a `\probfig{N}` placeholder while writing the `.tex`; the macro exists
once step 4b's figs file is `\input`. Hand-built TikZ per `references/latex-templates.md`
remains the path only for figure kinds the renderer doesn't cover — circles, sectors,
solids, transversals, coordinate grids. `\includegraphics` in worksheet problems is
rejected as unverifiable (`check_prose_consistency.py` exits 2 — no checker can bind an
external image's values to the JSON): figures come from `scripts/render_figures.py` or
the TikZ templates, never from image files.

### 4. Write and run the verification file

Before compiling, write `/tmp/verify_TOPIC_DATE.json` — a structured data file describing each problem and its expected answer. The bundled `scripts/verify.py` evaluates this using SymPy. No generated code is ever executed. The build driver (step 5) runs it first and fail-fast; to iterate on verification alone, see "Debugging individual gates" below.

**Field reference that cannot go stale:** `python3 "$SKILL_DIR/scripts/verify.py" --schema` prints every type's required/optional fields, the allowed functions/constants/variables, and one working example per type, generated live from the enforced schema (`--schema json` for machine-readable output).

**JSON format** — always set `problem_count` to the number of problems on the worksheet. Two gates enforce coverage, and they are not the same gate:

- `verify.py` hard-fails unless every problem id 1..N has at least one check. This counts **problems**.
- `tests/check_answer_slots.py` hard-fails unless every id has **one entry per response the problem asks for** — every printed `\ansline`/`\ansblank`/`\answerline`, and every lettered `(a)`/`(b)`/`(c)` sub-part in the stem. This counts **answers**.

**The answer key reads: header, Quick Answers, worked solutions — then a final page of its own.** The generated bank emits the verification and curriculum summaries (and the common-wrong-answers table) via `\AtEndDocument`, so they claim the LAST page automatically; the `ak_` author writes nothing extra and the one-`\input` contract is unchanged. Answer keys also breathe by design: `\akheader` switches on rubber inter-problem space, `\raggedbottom`, and a small stretchable `\parskip` — the key's reader is a grader scanning solutions, and a page set solid reads as a wall. The worksheet keeps its measured fixed layout (work space there is a promise to the student).

**The answer key says what it can and cannot vouch for.** Its Quick Answers bank has three states, and a "What is verified" note names the problems in each: a plain value is machine-checked; `$\spadesuit$` (a spade — chosen because no maths in these documents uses the glyph, where the old `---` sat one glyph from a minus sign) marks an answer only the instructor can judge, with the worked solution stating what a correct response must contain; and **`[unchecked]`** marks a problem printing more responses than the verification covers, where at least one printed answer carries no guarantee. The note is silent on a fully verified sheet — a legend explaining marks that never appear is the noise that makes real warnings invisible. Aim for a key with no `[unchecked]` marks; when one is unavoidable, the instructor at least knows where to look instead of trusting a complete-looking column.

When a problem carries more than one entry, give each a `"slot"` — the name of the response it covers (`"AC"`, `"the ones digit"`, `"(b)"`). No check reads it; the Quick Answers bank prints it, so a grader can tell which value answers which question. Without it the bank joins values in array order, and a sheet asking for "AC and BD" printed `2. 2.83, 8.49` with BD first.

**A slot label is a promise about FORM, and `tests/check_answer_slots.py` holds you to the narrow cases it can read.** The run-2 judge rejected a sheet whose bank row read `the equation = 6` (the check verified the slope; the student writes `y = 6x`) and another whose `word form` / `colon form` / `fraction form` slots all carried the identical value `3/5`. Three hard-fail rules, each measured 100%-precise on the 2953-slotted-entry corpus: a slot whose head noun is *equation* must have `=` in its value — key the equation itself with `equiv` (`expr "y - 6*x"`, `expected "y = 6*x"`), or rename the slot to what the check verifies ("of the equation", "equation value at 4" and similar part-namings are exempt); a *colon form* or *word form* slot names a transcription the expression grammar cannot hold (`3:5`, words) — declare that response `manual` with the printed form in the desc, and let the fraction-form entry carry the machine check.

The second gate exists because the first was mistaken for it. "At least one check per problem" was read as "a partially-verified answer key can never slip through", and those are different promises: a problem printing three blanks satisfies the per-id rule with one entry. In a 300-case review, **172 shipped a printed answer that nothing verified** — often the very skill the problem was tagged with. If a problem asks the student to write the number word *and* the ones digit, that is two entries. If a blank is working space rather than an answer, print it with `\scratchblank` and it is not counted. Use it for a **scaffolding blank that transcribes a given** — "$y$ of $C$ ___ $-$ $y$ of $B$ ___" where both coordinates are printed in the stem — and for **a value the problem already asked for elsewhere**. A check on either is vacuous, and dressing one up as a verified answer is the same overclaim the gate exists to stop:
```json
{
  "topic": "derivatives and trig",
  "problem_count": 17,
  "problems": [
    {"id": 1, "type": "solve",  "expr": "x**2 - 5*x + 6",      "expected": [2, 3]},
    {"id": 2, "type": "factor", "expr": "x**2 - 7*x + 12",     "expected": "(x-3)*(x-4)"},
    {"id": 3, "type": "eval",   "expr": "(x-1)*(x+2)", "at": {"x": 0}, "expected": -2},
    {"id": 4, "type": "zeros",  "expr": "x*(x-3)**2",           "expected": [0, 3]},
    {"id": 5, "type": "expand", "expr": "(x+2)**2",             "expected": "x**2 + 4*x + 4"},
    {"id": 6, "type": "diff",   "expr": "x**3 - 4*x",           "expected": "3*x**2 - 4"},
    {"id": 7, "type": "integrate", "expr": "6*x**2",            "expected": "2*x**3"},
    {"id": 8, "type": "limit",  "expr": "sin(x)/x", "to": 0,    "expected": 1},
    {"id": 9, "type": "equiv",  "expr": "sin(2*x)",             "expected": "2*sin(x)*cos(x)"},
    {"id": 10, "type": "solve_interval", "expr": "2*sin(t) - 1", "var": "t",
               "interval": [0, 360], "unit": "deg", "expected": [30, 150]},
    {"id": 11, "type": "approx", "expr": "9*tan(35*pi/180)", "expected": 6.30, "tol": 0.01},
    {"id": 12, "type": "distance", "points": [[1, 2], [4, 6]], "expected": 5},
    {"id": 13, "type": "midpoint", "points": [[2, -3], [8, 7]], "expected": [5, 2]},
    {"id": 14, "type": "slope", "points": [[2, 1], [2, 9]], "expected": "undefined"},
    {"id": 15, "type": "polygon_area", "points": [[0, 0], [5, 0], [6, 4], [1, 3]], "expected": 17},
    {"id": 16, "type": "triangle", "given": {"a": 7, "b": 11, "C": 34},
               "solve_for": "c", "expected": 6.51},
    {"id": 17, "type": "manual", "desc": "Graph sketch — verify visually"}
  ]
}
```

**Type reference:**

| Type | Verifiable? | What it checks |
|---|---|---|
| `solve` | ✅ | Roots of expr=0 match expected list (optional `var`, default `x`). An equation with no solution is keyed `"no solution"` and an identity `"all real numbers"` — both are decided from the expression, not from an empty root list, so keying an identity `[]` fails. Free symbols beyond `var` make it a literal equation: it solves for `var` in terms of the others |
| `zeros` | ✅ | Zeros of expr match expected list (duplicates collapse) |
| `factor` | ✅ | Factored form is equivalent to expr |
| `expand` | ✅ | Expanded form is equivalent to expr |
| `eval` | ✅ | expr evaluated at given values matches expected |
| `diff` | ✅ | Derivative of expr matches expected (optional `order`) |
| `integrate` | ✅ | Expected antiderivative differentiates back to expr — omit the `+C` |
| `limit` | ✅ | Limit of expr as var → `to`; optional `dir`: `"+"`, `"-"`, `"+-"` (default) |
| `equiv` | ✅ | expr and expected are the same function (trig identities, simplification). **The only type that accepts an equation:** where the student's answer *is* an equation, write it as one — `"expected": "(x-3)**2 + (y+5)**2 = 25"` — and the check compares `lhs - rhs` exactly as before. Do this whenever the ask is "write it in ⟨form⟩" for a locus (centre-radius, standard form of a conic); leave it 

…(truncated)
