Technical documentation

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

Download for your agent (.md)

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.

Canvas coursemodules, pages, files,assignments, discussions Exporta zip, plus course.json;3 requests at a time Enginetext of every file, namesout, visual pages picked Agent: /build-subject8 steps in a work folder,every number recomputed Plan foldersrc/subjects/<id>/: days,questions, mock, formulas Simeon.htmlbuild_app.py and tests;progress in the browser no AI: plain code, on the student's computer AI: the student's own Claude Code or Codex
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:

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:

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.

until none fail until green 1 2 3 4 5 6 7 8 ReadSurveyDesignQuestions CheckWriteBuild, testReport a .md per file_visuals/source.json survey.mdpapers.json design.md questions/one per paper checks.pychecks.jsontraces subject folder history.jsoncheck.mdtraces.json reports.md PROGRESS.md, rewritten after every step: a stopped build carries on from here
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:

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:

teaching filesone card eachfrom the cardsstep 3 Lecture 1.pdf Lecture 2.pptx Seminar 1.pdf … every file cards/Lecture 1.json cards/Lecture 2.json cards/Seminar 1.json … concepts.jsonwhere it's taught,what it builds on,marks in past papers The plantime followsmarks, orderfollows builds_on
Lecture by lecture: each file is read once, on its own, so nothing is lost to a long build running out of room.

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:

Sonnet, a courseOpus, the same courseOpus, a readings courseA job with six helpers 010M20M30M tokens sent in one build Sonnet: 5–7M tokens Sonnet: up to 7M tokens 5–7M Opus: 15.8M tokens15.8M Opus, readings course: 26.7M tokens26.7M The job itself: 3.5M tokens Its six helpers: 5.7M tokens 3.5M + 5.7M for 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:

Command output: about 50% What it wrote: about 25% Pictures: about 15% Everything else: about 10% command output, 50%what it wrote, 25%pictures, 15%rest, 10%
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