---
name: ed-packet
description: Generate and print a publication-quality educational mini-workbook (packet) on a specific topic for one of the kids. Auto-applies when bio-zack says "generate/print an ed packet on X", "make a workbook/worksheet packet about X", "penelope needs practice with X — make her a packet", or similar. Produces a themed, pedagogically-sequenced, adversarially-reviewed B&W packet, prints it duplex, and prints the certificate and answer key as separate sheets.
---

# ed-packet

turn "generate and print an ed packet on <topic>" into a printed, workbook-chapter-quality
packet: accurate programmatic figures, real pedagogy, professional B&W layout, adversarial
review, paper in the tray. honed across SIX packets (2026-07-12): analog clocks, money,
fractions, maps, solar-system reader, elapsed time — all under `data/education/packets/`.
**import `tools/packetlib.py` — never copy-paste a build script.**

## audience defaults

- **penelope** (default learner): b. march 2019 — compute age from today. STRONG reader
  (chapter books ~grade 3-4; War of the Worlds at 7), Singapore Math track. real sentences
  with personality; never babyish. check `memory/entities/people/penelope.md` first.
- **cora**: not reading independently yet (mid-2026) — needs read-aloud design.
- **the shared universe** (kids love continuity — reuse it): Time Bureau Press "Field
  Manual No. N" series numbering; villain **Shortchange Sam** (cheats with coins, fakes
  order tickets, counts his starting square); the town of **Maplewood** (mapgen TOWN);
  the friendly Martian tripod + skater-girl spot art (reuse from prior packets' images/);
  every packet ends "CASE CLOSED" + a named certificate ("Master of the Mint").
- cross-reference sibling packets when the MATH transfers: count-up change-making =
  two-hop elapsed time; clock quarters = coin quarters = fraction quarters. say so on
  the page — transfer is the point.

## the workflow (8 phases, in order)

### 1. pedagogy spec (write it BEFORE building — every item below was a real panel finding)

- **curriculum-node check FIRST — pull the node's S/G/C/N rungs + her level and let them
  drive the sequence.** if the packet topic maps to a node in the curriculum graph
  (`data/education/curriculum/`), that node may carry `representations`: up to four
  alternative teaching representations of the SAME skill — **C** (Concrete-manipulative:
  cubes/coins/fingers), **G** (Graphical: ten-frame/bar-model/number-line), **S** (Symbolic:
  the number sentence), **N** (Narrative/real-world: the story the skill lives in). Each rung
  is `{move, check, down?}` — a teaching move, a check question, and an optional entry prereq.
  This is the Concrete-Pictorial-Abstract + Narrative spine Penelope's Singapore/Dimensions
  curriculum already runs on (schema.md "Representations (S/G/C/N)"; OS-DESIGN.md "From Rick's
  LATTICE"). When they exist, they are the packet's backbone:
  - **fetch** — one capability call returns the node incl. `representations` AND the kid's
    current mastery level (both drive the sequence):
    ```bash
    ./run capabilities call vita.education.node --input '{"id": "<node-id>", "kid": "penelope"}'
    ```
    → envelope `data`: `representations` (ordered S,G,C,N, present rungs only; each `{move,
    check, down?}`; `{}` == none authored yet), `level` (L0–L4), `status`, `gates`, `title`.
    Don't know the exact node id? list her frontier — `PYTHONPATH=scripts uv run python -m
    education.curriculum_lib frontier penelope` (run from vita) — and match the topic to a
    node id, or read the exported `graph.json` / `load_graph().node(id).representations`
    (`from education.curriculum_lib import load_graph`, PYTHONPATH=scripts — also carries the
    rungs). If the topic has NO node, or the node carries no rungs (`{}`), skip this and use
    the mastery-ladder discipline below as before (it's how the first six packets were built).
  - **sequence the packet C → G → S → N.** Lead with the **C** rung's teaching move (concrete/
    manipulative), then **G** (pictorial), then **S** (symbolic) — matching how the skill is
    taught and how she consolidates (concrete → pictorial → symbolic). Use the **N** rung for
    the word-problem / real-world application pages. A rung's `down` is its entry prereq —
    honor it (teach/recap the down-skill before that rung, same "nothing used before taught"
    rule).
  - **seed items from the rung `check` questions.** Each `check` is an assessment item at that
    representation level: the C `check` becomes a manipulative-style item (draw/count objects),
    the G `check` a picture/diagram item, the S `check` a bare-symbolic item, the N `check` a
    word problem. (Same rules as always apply — never print the check verbatim as the worked
    example she copies; use it as the SHAPE and generate fresh instances; the emphasized rung
    still gets its hard case.)
  - **weight the representations by her LEVEL on that node** (from the fetched `level` / the
    frontier) — the packet's rung mix is a function of BOTH the node's rungs AND where she is:
    - **just entering (L0→L1)** → lead heavily **Concrete**; C+G carry most pages, S is a brief
      landing, N one applied page. She needs to build the idea with objects first.
    - **consolidating (L2→L3)** → weight **G → S**; concrete is a quick warm-up, the bulk is
      pictorial-into-symbolic (the transfer she's actually working), N for transfer-out.
    - **fluency push (→L4)** → **Symbolic / timed**; bare-symbolic reps at volume, minimal
      C/G scaffold, N only to keep it from being rote. (A fluency node has `tier: L4` +
      `fluency_aim`; match the aim's metric — the timed page is the point.)
    the "hardest rung gets the most items" rule (below) still governs WITHIN the weighted band.
- **mastery ladder**: smallest teachable steps; nothing used before taught. the #1
  recurring violation is a SKILL GAP between adjacent pages (page 9 needed a 17¢
  count-up when page 8 only taught counting by ones; 3:05 needed a leading zero nobody
  taught). walk the ladder page-by-page asking "was every technique this page needs
  taught earlier?"
- **allocate practice by DIFFICULTY, not page symmetry.** the panel found the inverse on
  3 of 5 packets: easiest concept over-drilled, hardest concept (quarter-to, comparing
  fractions, route-writing) got 2 reps. hardest rung gets the most items.
- **production ≥ recognition.** every packet needs pages where she CONSTRUCTS (draw the
  hands, write the route, shade the fraction, write the fraction symbol) — and check the
  production covers each sub-skill (WEST was never produced; ½ was never written).
- **misconception inventory** → one honest trap each. traps must be correctly-rendered
  instances of the wrong idea. include a COUNTER-trap so the trap rule doesn't become
  the new rule (6 pennies DO beat a nickel).
- **notation audit**: every symbol she must write, taught before first use.
- **matching exercises get exactly one decoy** ("one ticket matches no treat!"); "circle
  every one that..." needs at least one negative case or it discriminates nothing.
- **never reuse the worked example as practice item (a)** — the child copies the printed
  answer; every practice item is a fresh instance. and FADE the scaffold: the last item
  in a scaffolded set drops the helper boxes ("no hop boxes this time, Agent").
- **production items must exercise the emphasized skill's hard case** — if the
  instruction says "mind the hour hand's creep," no item may land on :00 where creep is
  invisible; if a time must be READ off a face, pick times whose hands are ≥30° apart.
- **key hint prose derives from data too**: a hand-position or method hint written as a
  casual ternary WILL be wrong at the edge (the "just past the 4" at 4:00 bug) — branch
  every case or generate the hint from the same numbers as the figure.
- **no self-answering pages**: don't print the answer in a caption/learn-box on the same
  page as the question (the "Sun (a star!)" bug); reference-numbers in a LEARN box make
  the exercise below copy-work — move the fact into the exercise as retrieval.
- **narrative promised = narrative delivered**: name the villain, thread ≥3 beats through
  the middle pages, close the case before the certificate. a cover that promises a
  "case" that never happens reads as a lie to a chapter-book kid.
- **claims must be literally true**: "drawn true to size" → use TRUE_SCALE so a real coin
  lies on the page exactly; a certificate says "weather" only if weather was taught.
- **readers (non-math)**: passages FK grade ~3.5-4.5 with ramping difficulty; question
  mix = literal + evidence-hunt ("underline the sentence") + genuine inference + main
  idea + prediction; vocabulary page tests ONLY words the passages actually used; give
  open-writing prompts a printed sentence-starter.
- **length is content-determined**: cover + one page per rung + cert + key. 9-11 pages
  typical. no filler.

### 2. figure generators — precision is programmatic, ALWAYS

- any figure whose geometry/quantity IS the lesson comes from a generator with a
  `verify_*`/assert wired into the BUILD (not just `__main__` — marsgen lesson: put
  `_selfcheck()` at module import so a build can't ship unverified).
- gen-AI never draws pedagogical content.
- **available in `tools/`**: `clockgen` (analog clocks: proportional hour hand, casing
  halo for near-hands, broken-clock mode, minute rings, wedges), `coingen` (US coins:
  TRUE_SCALE print size, reeded/smooth edges, blank faces, subset-sum solvability
  verify), `fracgen` (pies/bars/dot-sets: exact partitions, honest-unequal + distractor
  shading modes, stacked frac_text SVG), `mapgen` (letter/number grid towns: coordinate
  engine, route verify incl. off-map, glyphs, compass, single-stroke grid), `marsgen`
  (verified planet fact table + diagrams).
- generator perceptual rules learned the hard way: keep rotations off figures where tilt
  reads as inequality (a tilted ⅙ pie "looks unequal" — two reviewers flagged pixel-
  perfect geometry); SVG text sitting on lines needs a white knockout rect; grids drawn
  as per-cell rects double-stroke into fake-heavy lines — draw single lines; when two
  hands/elements can near-overlap, add a white casing halo.

### 3. decorative art — gpt-image-2, B&W line-art house style

2-4 images; run in background bash while you build (~2min each). REUSE the existing
library first (`analog-clocks-2026-07-12/images/`: tripod, skater; other packets:
piggy, shop, cat-pizza, chocolate, pirate-cat, rover). style prompt that works:

> "…illustration for a children's workbook, black ink line art on pure white background,
> clean bold outlines, no shading, no gray tones, no color. [subject]. Simple bold clean
> outlines, minimal detail, readable at small print size. No text, no words, no watermark."

if art must contain topic content, pick the easiest instance, spell it out explicitly
("shows exactly 3 o'clock: short hour hand on the 3, long minute hand straight up"),
and VERIFY the result visually. **wait for art to land before the final render** — an
early render bakes in broken-image icons.

### 4. single-source-of-truth manifest

every exercise is data; the figure, the prompt, and the answer-key entry derive from the
same datum (helpers like `minutes_between()` for derived answers — asserted). the key
literally cannot disagree with the artwork. accumulate answers via `Packet.ans()`.
**key conventions** (panel-derived): label entries "Ex 1a/1b…" matching on-page letters;
key section headers "Part N (page M) · Title"; underline-task answers quoted VERBATIM
from the passage (+ "accept either" notes when two sentences qualify); when a gag/trap
can catch a correct-reasoning kid, tell the grading parent what the wrong-looking
answer means.

### 5. layout — packetlib + chromium pdf

```python
import sys; sys.path.insert(0, "/home/zack/work/vita/.claude/skills/ed-packet/tools")
from packetlib import Packet, header, teach, exnum, dslot, wline, item_row
p = Packet("Title", "/abs/path/to/packet-dir", extra_css=...)
p.raw_page(cover_html, cls="page cover"); p.page(...); ...  # cert last, key auto-appends
p.build(); p.render(); p.split(); p.raster()
```

- fonts already installed: Baloo 2 (headings/numerals), Andika (body), Patrick Hand
  (hand-written accents — but NOT for numeral sequences the child must copy; use Baloo
  via `.numseq`).
- pencil ergonomics (all in packetlib defaults — don't shrink them): dbox 0.68×0.58in;
  boxes that hold 3 digits ≥0.58in wide; write-lines sized to the EXPECTED ANSWER
  (place names ≥2in); draw-target figures ≥1.6in; drawzones ≥1.5in.
- **all write-in strokes pure #111** (dashed distinguishes from solid); no gray tints
  under text; grays ≥#444 only for decorative "faded" elements.
- **gestalt = meaning**: items that belong together sit closer than items that don't;
  box each comparison pair; matching = items above, answers below, anchor dots, clear
  line space, lines must never cross other content; label multi-item exercises a/b/c.
- every affordance explicit: YES/NO gets "(circle one)"; two-blank route orders get
  (direction)(how many) sublabels; duplex-referencing exercises get a recap stub so she
  doesn't flip pages mid-task.
- flex rows: COUNT the widths (usable width ~7.3in / ~700px). 3 clock-pairs, 12 seq
  boxes, 6 coins+boxes all overflowed once. one-item-per-row with inline answer slots
  is the safe default for wide items; `.page` has overflow:hidden so overflow FAILS
  SILENTLY — the only detector is re-rastering and reading the page.
- after ANY content edit, re-render and re-Read every touched page (footer collision is
  the #1 regression; passages growing during fix rounds re-broke 3 pages).

### 6. QA — own pass, then adversarial panel, then ADJUDICATE

1. `p.raster()` → Read EVERY page yourself first (broken art, overflow, wrapped rows,
   hand positions).
2. re-raster at `-r 100` into `review/`, then spawn **3 parallel background agents**
   (general-purpose, `run_in_background: true`): pedagogy (methodology lens, sequence/
   volume/misconceptions/engagement), accuracy (two-pass: blind-read every figure off
   the images, then cross-check manifest + key + every teaching-text claim), production
   (copy ambiguity, pencil ergonomics, print survivability, duplex pairing, key
   consistency). tell them the cert+key print as separate sheets and what the exercise
   labeling scheme is. keep building the NEXT packet while panels run.
3. **adjudicate — don't auto-apply.** reviewers were measurably wrong once (claimed a
   pixel-exact ⅙ pie had unequal wedges) and right about the perception anyway. classify
   each finding: mathematically wrong (fix the data) vs perceptually misleading (fix the
   rendering) vs reviewer error (fix nothing, maybe add a comment). verify surprising
   claims against the source before editing.
4. expect the panel to find real BLOCKERs even when you're confident — across 6 packets
   it caught: untaught notation, gray-dash print dropout, boxes too small for "100",
   false physical claims, key-on-certificate-back, vocab never taught, count-up skill
   gap, gestalt mis-pairing. every panel paid for itself.
5. fix, re-render, re-Read. repeat until dry.

### 7. output split + print

`p.split()` produces THREE pdfs: `packet-main.pdf` (cover+content, duplex),
`certificate.pdf` (own sheet — a keepsake never gets pencil-embossed exercises on its
back), `answer-key.pdf` (own sheet — never visible to the kid).

```
lp -d Brother_MFC_L3770CDW_series -o media=letter -o sides=two-sided-long-edge -o print-color-mode=monochrome -t "<topic>-packet" packet-main.pdf
lp -d Brother_MFC_L3770CDW_series -o media=letter -o sides=one-sided -o print-color-mode=monochrome -t "<topic>-certificate" certificate.pdf
lp -d Brother_MFC_L3770CDW_series -o media=letter -o sides=one-sided -o print-color-mode=monochrome -t "<topic>-answer-key" answer-key.pdf
```

verify with `lpstat -W not-completed -o` until drained.

### 8. persist

- packet dir: `data/education/packets/<topic>-<YYYY-MM-DD>/` (generators, build script,
  index.html, 3 pdfs, images/, pages/, review/).
- new reusable generators → copy into this skill's `tools/`.
- log to `data/scratch/daily.md`; note the topic in the kid's entity file. when bio-zack
  reports how she did, FILE IT — it calibrates the next packet's difficulty, and log a
  homeostasis prediction on completion/mastery when handing a packet over.

### 9. author the gumball review pack (spaced-repetition coupling, added 2026-07-13)

every packet ships with a review pack so gumball can reinforce it after the kid
finishes the physical packet (SM-2 schedule: dense for days, expanding intervals,
long-tail refreshers past mastery).

- write `~/work/gumball/backend/data/review_packs/<topic>.json` (slug = packet dir
  name minus date). schema + generator catalog: `~/work/gumball/plans/REVIEW_SYSTEM_DESIGN.md`.
- 4-7 concepts, rung order = the packet's own teaching sequence, content strictly
  limited to what the packet TAUGHT (no untaught notation — same rule as the packet
  itself). prefer generative types (clock_read, fraction_identify, money_count, …)
  over fact_mc; author fact banks from packet content for recall topics.
- a concept may reference a generator that doesn't exist yet — it parks as
  unavailable until the generator ships in gumball. if the topic needs a NEW
  generator, add it to `backend/app/services/review_generators/` (registry pattern,
  deterministic from the passed rng, 100-seed validity test).
- sync: `curl -X POST http://192.168.254.140:8000/admin/review/sync -H 'X-Profile-Id: 1'`
  (or restart gumball — packs also sync at startup).
- do NOT activate the pack — activation = "kid finished the packet", done by
  bio-zack on the `/admin/packets` page, or by me via
  `POST /admin/review/packs/{id}/activate {"profile_id": N}` when he says
  "penelope finished the <topic> packet" (profile ids: penelope=3, cora=2).

## mechanical pitfalls (cost a revision each — check before rendering)

- shell cwd persists between bash calls: `cd` to the packet dir EXPLICITLY in every
  build command, or you rebuild the wrong packet (happened twice).
- background art finishing after the first render → broken-image icons in the pdf.
- `pdftoppm` pads page numbers by total count: 9 pages → `pg-1.png`, 10+ → `pg-01.png`.
- grid CSS: a "pair" of spans = 2 grid cells — wrap pairs in a container or the column
  math is wrong.
- `uv run` needs `--with pypdf` for `p.split()`.
- review/ renders must be re-generated AFTER the last edit or panel agents flag already-
  fixed issues (tell them the render date if there's any gap).
- when a passage/exercise grows during a fix round, the page it lives on AND the pages
  after it need re-reading — content pushes downward silently.
