How a plan is built
From a Canvas course to a checked study plan: the export, the engine with no AI, the agent's eight steps, the checks run by plain code, and what a build costs. · 27 October 2026
The whole entry as Markdown, to give to Claude Code, Codex or Cursor: paste it in, or put the file in your project.
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.
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,
cfetchinextension/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. WhenX-Rate-Limit-Remainingruns low it pauses before the next one. - It says what it found.
course.jsonlists each module in order and each file's module, dates, size and SHA-256, a guessed kind (lecture, seminar, exam, solutions, reading, case, …),duplicate_ofandolder_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.mdusually holds the exam date,Grading.mdhow 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:
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.
- Read. The engine commands above. The agent reads the vault's text, and a few of the visual pages.
- 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. - 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. - 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.
- Check every number (below).
- 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 theid. - Build and test (below).
- 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:
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.jsonmaps 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 followsbuilds_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.
- 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:
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:
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 |