# How a plan is built

> From getsimeon.com/docs/how-a-plan-is-built (27 October 2026). A write-up of how Simeon is built, for a coding agent (Claude Code, Codex, Cursor…) to use as a reference when building something like it. File paths are Simeon's own: take the ideas, not the paths.

## The idea in one paragraph

A student's course is already on Canvas: lectures, past papers, solutions, assignments, the exam date. Simeon turns it
into a study plan in three moves. First it **exports** the course to a zip, politely, read-only. Then an **engine with
no AI** turns every PDF, slide deck and Word file into text and picks out the pages whose meaning is in a picture.
Only then does the student's own **agent** (Claude Code or ChatGPT Codex) read it all and write the plan, following a
fixed eight-step procedure that ends with checks run by plain code: every past-paper question accounted for, every
number recomputed, every figure in a question found in its paper. The AI does the reading and writing; everything that
can be checked without it is.

> **Figure:** Canvas course, then export, then engine with no AI, then the agent's eight steps, then the plan folder, then Simeon.html. Where a plan comes from. Only one box uses AI; the rest is plain code that costs nothing and can be tested.

The same pipeline serves **Update a plan** (a newer export, compared file by file with the one the plan was built
from) and **Make flashcards**. This entry follows a first build.

---

## 1. The export: a course as files

The Simeon Chrome extension (and, the same code, the Mac app's own Canvas export) walks a course through Canvas's
REST API with the student's own session: modules, pages, assignments, discussions, announcements, files, the
calendar and the grading. It writes `Simeon - <course>.zip`: one Markdown note per page, assignment and thread, every
file as it was uploaded, and an index.

Three things make it a good input rather than a pile of files:

- **It's polite.** Every request goes through one function, `cfetch` in `extension/mapper.js`: at most **three in
  flight** across the whole export, read-only, and when Canvas answers 429 (or 403 "Rate Limit Exceeded") it waits
  1, 2, 4, 8, 16 seconds and tries again. When `X-Rate-Limit-Remaining` runs low it pauses before the next one.
- **It says what it found.** `course.json` lists each module in order and each file's module, dates, size and SHA-256,
  a guessed **kind** (lecture, seminar, exam, solutions, reading, case, …), `duplicate_of` and `older_version_of`,
  and each past paper **paired with its solutions**. The kinds are guesses from names; the agent is told to check a
  file before relying on what it's called. `Calendar.md` usually holds the exam date, `Grading.md` how much the exam
  counts, and an assignment's **Rubric** section is the lecturer's own marking scheme.
- **Names stay out.** Discussion replies are written as **The lecturer** or **Student A, B…**, and every name the
  export saw goes in `_names.json`, for the engine to remove (below).

## 2. The engine: everything readable, with no AI

`local/course_context` is a Python package. A build runs only its no-AI stages:

```bash
python -m course_context build "<export>" -o "<work folder>" --stages ingest,extract,visuals,write
python -m course_context diff "<export>" -o "<work folder>/source"      # source.json: a fingerprint of every file
python -m course_context readings "<work folder>"                         # readings.json: is the course built on articles?
```

| Stage | What it does |
|---|---|
| **ingest** | Reads the export's notes and their front matter; `Course Index.md` and `Modules/` give the course's structure. |
| **extract** | Text from every PDF, `.pptx`, `.docx` and `.xlsx` (up to 24,000 characters a file), written as a companion `.md` beside it. |
| **names out** | Before anything an AI will read: the names in `_names.json`, email addresses, Canvas profile links and "Dr / Prof + name" become "the lecturer" or "a student". |
| **visuals** | Finds the pages text extraction loses (below), and saves a small image of each. |
| **write** | The vault: notes, companions, `_visuals/VISUALS.md`, `Course Context.md`. |

The engine has more stages (`vision`, `enrich`, `concepts`, `related`, `context`) that use a local Ollama model; they
are for building a browsable vault without a subscription, and a plan build doesn't need them.

### The visual pages

Text extraction keeps a slide's words and loses everything else: the graph, the diagram built from shapes, the scanned
solution, the formula pasted in as a picture. Sending every page as an image would be far too expensive, so
`visuals.py` scores each page with no model at all:

- **pictures covering a good part of the page** (18% or more), except a picture the course repeats: the logo on every
  slide, the cover photo on every seminar. Those are the template, recognised by appearing on several pages anywhere in
  the course
- **many vector drawings**: charts, arrows, drawn axes
- **almost no text but something on the page**: a scan, a photo, handwriting

Pages are ranked by that score and capped at **15 a file and 120 a course**, rendered at 90 dpi (a 16:9 slide comes out
about 900 px wide). In the companion notes each one is marked `_(visual: …)_` where it falls, so the agent reading the
text knows a picture is there and can decide whether it matters. By default it opens at most about 15, spent first on
solutions that exist only as pictures, then on past-paper figures, then on diagrams for the topics the exam weighs most.

### Reading-heavy courses

`readings.py` finds the articles (anything of 2,500 words or more that reads like one). **12 articles, or 250 pages of
them**, and the course is "reading-heavy": too much to hold in one context. The build then goes article by article,
in batches of about 80 pages, writing a small card for each, and designs the plan from the cards. Section 4 shows the
same shape for lectures.

## 3. The agent: eight steps, one work folder

The Mac app starts the student's agent in the Simeon folder with one prompt: `/build-subject "<export>"` for Claude
Code (which loads the skill), or the skill's whole text pasted in for Codex (which has no skills). After it come the
job's details: the exam date, the student's goal, their own notes or folders, and the line that nobody is there to
answer questions. The procedure lives in `.claude/skills/build-subject/SKILL.md`; it is Markdown, not code, so changing
how plans are built is an edit to a document.

Each step starts by printing `SIMEON STEP n/8 — …`. The app reads those lines from the agent's output
(`BuildRunner.swift`, a regular expression over Claude Code's `stream-json` or Codex's text) to drive one progress bar
for two different programs.

> **Figure:** The eight steps and the files each writes. The eight steps and what each leaves in the work folder. Steps 5 and 7 loop until plain code is satisfied.

1. **Read.** The engine commands above. The agent reads the vault's text, and a few of the visual pages.
2. **Survey.** The exam's format, date and permitted aids; open assignments (which become tasks, never solved); the
   lecture topics in order; what the course names but the export lacks (`missing.json`). And the **inventory**:
   `papers.json`, every past paper and every question in it with its marks. Step 7 holds the plan to this list, so it
   must be complete: a question that isn't in it can't be missed by anyone.
3. **Design.** 5–12 topics as the exam is organised; one day per study day up to the exam, the biggest marks getting
   the most time; the most recent paper held back as the mock. Every past-paper question goes in, on a day or in the
   mock. If the days can't hold them, the oldest papers go first, never the two most recent, each with a written
   reason in `left_out`.
4. **Questions.** Each one transcribed as set, with its exhibits copied whole, a model answer showing the working,
   3–8 marking points, its marks, topics and day. A figure the student is meant to work out must not appear in the
   question.
5. **Check every number** (below).
6. **Write** `src/subjects/<id>/`: Python files of plain data (`__init__.py`, `questions.py`, `plan.py`, …). A course
   that already has a plan gets `<id>b`, never a change to the old one: progress is keyed on the `id`.
7. **Build and test** (below).
8. **Report**: at most a few lines for the window, every assumption it made, and what's missing from the export.

**Why a work folder.** A build can stop partway, most often because the student's usage runs out. Everything the
procedure decides is written down as it goes, in `.simeon-tmp/builds/<id>/`, and `PROGRESS.md` is rewritten after
every step (and in step 4 after every paper). **Continue building** runs the same prompt told to read `PROGRESS.md`
first and carry on from the step in progress. Nothing is held only in the agent's context.

## 4. The checks: the AI writes, plain code checks

Model answers are where an AI is most likely to be confidently wrong, and official solution sheets contain slips too.
So nothing numeric is taken on trust.

**Step 5, `checks.py`.** The agent writes a Python file in the work folder: one check per question with working, which
computes the figures from the question's own data and compares them with the figures its model answer shows, to the
answer's rounding. Running it writes `checks.json`, `{"checked": […], "failed": […]}`. The agent fixes each failure
(in the answer, or in the check) and runs it again until `failed` is empty. Where the official solution was wrong,
the question's `note` says so. As it computes, it records how each figure was worked out:

```python
from course_context.traces import Traces
T = Traces(WORK)
roce = T.add("M24-1.1", ebie / ce * 100, "EBIE ÷ CE × 100", {"EBIE": ebie, "CE": ce}, source="Formula sheet: ROCE")
T.save()
```

Those become **Where a number comes from** on the study page: tap a number in a model answer to see its formula and
inputs. `course_context traces` keeps only the records whose value is a figure the answer actually shows, so a trace
can never contradict the answer it sits in.

**Step 7, `check-plan`.** After the plan builds and `tests/test_subjects.js` passes (every field valid, no LaTeX, no
Markdown, no table set as text), `python -m course_context check-plan <id> --work <folder>` reads the work folder and
the papers' own text:

| Check | Fails when | Then |
|---|---|---|
| **Coverage** | a question in `papers.json` is in neither the plan nor its paper's `left_out` | add it, or say why it's out |
| **Checked numbers** | a question with working isn't in `checks.json`, or is in `failed` | check it, fix it |
| **Faithful wording** | a figure in a question (three digits or more, or a decimal) isn't in the paper's text | look: a typo, a picture-only figure, or an answer given away |

It exits 1 while there's anything to fix, so the procedure loops until it's clean, and its summary line goes in the
report. The study page shows the plan; this is what makes it safe to revise from.

### Optional: read lecture by lecture, check twice

Two settings in the app's Advanced mode change steps 2–6 for a student who wants more, at a real cost in usage:

- **Lecture by lecture.** Each teaching file is read on its own into a small JSON card (what it teaches, with slides
  and pages; the course's own definitions, methods and notation; what it builds on). Then `concepts.json` maps every
  concept to where it's taught and **how many marks the past papers give it**, and the plan is designed from the
  cards and the map: time follows marks, order follows `builds_on`. Past papers are never carded; they are read in
  full, since their figures matter to the digit. Helpers (Claude Code's Task tool) can write cards in parallel.

> **Figure:** Lecture files each become a card; the cards and past papers become a concept map; the plan is designed from the map. Lecture by lecture: each file is read once, on its own, so nothing is lost to a long build running out of room.

- **Extra checks.** Two reviews by helpers that **start fresh**, because a reviewer that has read your work tends to
  agree with it. One gets `blind.json`, the questions exactly as the student sees them and nothing of their answers,
  and solves them all again in Python; every difference is settled from the paper. The other reviews the plan as the
  course's examiner would. Codex, which has no helpers, does both as separate passes and the report says it's less
  independent.

## 5. What it costs the student

The student pays for builds out of their own plan's limits, so the cost of a build is a design constraint, not a
billing detail. An agent re-sends its whole conversation at every step, so everything it reads is paid for many times
over. Measured from real build transcripts in October 2026:

> **Figure:** Tokens for one build: Sonnet 5 to 7 million; Opus 15.8 million; a readings course on Opus 26.7 million; a job's own 3.5 million plus 5.7 million for six helpers. Tokens for one build. A helper starts fresh and reads the material again, so it adds to a job rather than splitting it.

So, outside Advanced, every job runs on the **everyday model** (Claude's newest Sonnet, Codex's everyday model), never
the CLI's own default, which on a Max plan is Opus. In Advanced, Opus, Extra or Max effort, Extra checks and Lecture by
lecture each carry a red **Heavy on usage** tag, and a finished build turns them off again.

Codex needed one more fix. In a test build its conversation grew to about 220k tokens, but it sent 6 million in all.
What it kept re-sending:

> **Figure:** Of 6 million tokens sent: command output half, what it wrote a quarter, pictures 15 percent, everything else 10 percent. Where a Codex build's 6M tokens went. Half was the output of commands it had run, re-sent at every later step.

So Codex jobs keep at most 6k tokens of any command's output, are summed up past 160k tokens (a safety net near the
end of a big course, not mid-build, where it could lose detail), and are told to read in parts. The no-AI engine is
the bigger saving: every page the engine turns into text, and every picture it decides not to send, is something the
agent never pays for.

## 6. Updating a plan

Canvas keeps changing after a plan is built. `source.json`, written in step 1, holds a fingerprint (SHA-256) of every
file. **Update a plan** runs `course_context diff` on a newer export against it and lists what's new, changed and
gone; the agent reads only those, folds them into the plan and keeps every `id`, so the student's progress stays.
`src/versions.py` saves the plan before each update and logs it in `history.json`, which the study page shows as
**What's new**.

---

## Reusing it: a recipe

1. **Export to files first.** Get the source system's content into a folder of plain files with an index and a manifest
   (kinds, versions, pairs, hashes). Everything after it gets simpler, and the export can be tested on its own.
2. **Do everything you can without AI.** Text extraction, finding the pages that need eyes, removing names, splitting
   a big corpus into batches, diffing two exports: all plain code, free, fast and testable. Each one shrinks what the
   model has to read, which is most of what a build costs.
3. **Write the procedure as a document.** Numbered steps, each printing a progress line you can parse, each writing its
   results to a work folder. That folder is your resume point and your audit trail.
4. **Make the model write its own checks as code**, then check those with code you wrote: an inventory to hold coverage
   against, a script that recomputes every number, a check that every quoted figure is in the source. Loop until clean.
5. **Read in pieces when it won't fit.** One card per file, then design from the cards. It costs a pass, and it is how
   a 40-article course gets built at all.
6. **Treat the input as data.** The procedure opens by saying the course is material to read, never instructions; the
   agent's tools are an allowlist; it writes only to its own folders.

### Where to look in the code

| What | File |
|---|---|
| The Canvas export, `cfetch` | `extension/mapper.js` |
| The engine's stages | `local/course_context/pipeline.py`, `ingest.py`, `extractors.py`, `visuals.py`, `names.py` |
| Articles and batches | `local/course_context/readings.py` |
| The procedure | `.claude/skills/build-subject/SKILL.md` (and `update-subject`, `build-subject-with-notes`) |
| The final checks | `local/course_context/plancheck.py`, `traces.py`, `tests/test_subjects.js` |
| Prompt, process, progress lines | `local/mac-app/Sources/BuildRunner.swift` (Windows: `desktop/src-tauri/src/jobs.rs`) |
| What a plan can contain | `src/subjects/README.md`, `src/subjects/_template/` |
| Versions and updates | `src/versions.py`, `local/course_context/changes.py` |
