AI lesson fixes
For how to use this, see AI lesson fixes.
Some things the lesson checks find can be fixed by a script (see quick fixes). Most can't, so the Check panel offers Fix with AI, which asks the Worker for a fix, checks it, and previews it before anything changes.
Which findings
AI_FIX_CODES in packages/core/src/lessonAiFixes.js lists them. They are the findings about one section whose fix is a change to that section's text, questions or spelling words:
- answers not in the passage, or in it when they shouldn't be (
E_GROUNDING_*,E_ORANGE_PARAPHRASED,E_BACKGROUND_IN_TEXT) - orange questions that give their answers away, aren't a list, have the wrong number of answers, multi-word answers or no blank (
E_ORANGE_ANSWER_IN_PROMPT,E_ORANGE_NOT_A_LIST,W_ORANGE_ANSWER_COUNT,W_ORANGE_MULTIWORD,W_ORANGE_NO_BLANK) - prompts that give another question's answer away (
E_ANSWER_REVEALED_CROSS,W_ANSWER_REVEALED_OPEN) - spelling words that are the wrong length, repeated, hidden in an answer or already vocabulary (
E_SPELLING_LENGTH,E_SPELLING_DUPLICATE,E_SPELLING_COLLISION,W_SPELLING_IN_CAPS) - answers or numbers used twice (
E_ANSWER_WORD_REUSED,E_NUMBER_DUPLICATE) - question wording (
E_RETIRED_STEM,W_WYR_SHAPE,W_OPEN_SPLIT,W_NUMBER_NO_STEPS)
Left out are the codes with a quick fix (W_ORANGE_ORDER and E_ORANGE_PARTIAL_LIST among them), the shape of the lesson or a section (W_SECTION_COUNT, W_SPELLING_COUNT, W_QUESTION_SHAPE, W_NO_QUESTION; adding six sections is not a fix), and E_UNKNOWN_SOURCE. hasAiFix(finding) is what the panel asks.
How a fix is made
- The editor sends the lesson and the finding's key to the Worker with
mode: "fix". - The Worker finds the finding again, shows the model the section (
fixContext), and turns the model's edits into operations (aiEditsToOperations). checkFixdecides whether the fix passes. If it doesn't, the model is told why and tries once more.- The Worker answers with
replace_blockoperations, the explanation and a count of new warnings. - The editor runs
checkFixagain on the lesson as it is now, shows the preview, and applies the fix on Apply.
The editor sends the whole lesson (suggestFix() in aiSuggest.js), since the checks are lesson-wide, and the key of the finding. Image blocks go as their id and type only, since the checks never read an image and an old lesson can hold one inline as a data URL. The Worker runs the checks itself and looks the finding up by its key (findFinding) rather than trusting a description of it, so the prompt carries the checker's own message: prose already written for a model, naming the fix. A finding that is no longer there is refused with a 409, and one that isn't in AI_FIX_CODES with a 400.
Only the request's shape and size are checked before Turnstile. The finding lookup runs the full lesson checks, so it happens behind the verification and the rate limiter, where an unverified request can't make the Worker spend that CPU for free. The lookup's one pass is reused as the "before" side of every checkFix below, so a request validates the unchanged lesson exactly once. The price is that a stale finding costs its requester a token like any other checked request.
The model sees the finding's section block by block, in the same input shape the MCP server takes: text blocks as markup (so formatting and ^[...] footnotes survive), every list as plain strings. Images and VAKT activities are shown but marked as not editable. It is also given the spelling words and answers used in other sections, so a replacement doesn't collide with one, and a short summary of the question types. The rules it is held to: fix this one problem, change as little as possible, and never change a fact, number, date or name.
It answers with FIX_SCHEMA (apps/api/src/lib/lessonFix.js): an explanation and a list of edits, one per block, where an empty field means "keep it". aiEditsToOperations turns each edit into a replace_block operation for @spelling-creator/core/lessonPatch, merging it with the block as it was. An edit that names a block outside the section, or one that may not change, is refused.
checkFix then makes the operations on a copy and runs the checks before and after. A fix passes only if:
- the finding's key is gone,
- no new error appeared (
newFindings, the same filterpatch_lessonuses), and - no text block lost a footnote.
One allowance: a formatting finding's key is the formatted words themselves, so a fix that rewords a passage re-keys a formatting defect that predates it. A formatting finding whose code already fired in the same section is treated as pre-existing rather than new. Formatting only; for every other code a changed key means the fix changed the thing the check is about (a new answer, say), and the fix answers for it.
If the first try fails, the model is shown its edits and what was wrong with them, and asked once more (FIX_ATTEMPTS, 2 in all). If that fails too, the Worker answers 422 and the dialog shows its message.
The editor runs checkFix again on the lesson as it is when the fix arrives, and once more when Apply is pressed (applyAiFix in EditorPage.jsx), since the author may have edited the lesson in the meantime. Operations address blocks by id, so a fix still lands on the right block after other edits, and is only refused when it no longer passes.
applyFixOperations keeps everything the fix didn't touch as the very same objects, keeps the ids of list items whose text didn't change (so the editor's fields stay put), and stores a changed text block as a document, the way the editor does (see Rich text). An applied fix goes through the same path as a quick fix, so the lesson is committed as a version first and the panel's Undo works the same way.
Cost and limits
A fix is one Turnstile check and one rate-limit token, like the other AI helpers, and one or two model calls. Nothing is cached: asking again should give a different fix. A request is refused with a 413 if the lesson is over 300,000 characters as JSON (MAX_FIX_DOC_CHARS).
Where the code is
| File | Does |
|---|---|
packages/core/src/lessonAiFixes.js | AI_FIX_CODES, fixContext, aiEditsToOperations, applyFixOperations, checkFix. Shared. |
apps/api/src/lib/lessonFix.js | The prompt, FIX_SCHEMA, finding the finding again, and the two tries. |
apps/api/src/routes/ai.js | The fix mode: input checks, Turnstile, rate limit. |
packages/core/src/aiSuggest.js | suggestFix(), the browser's call to the Worker. |
apps/web/src/components/editor/AiFixDialog.jsx | The dialog, the preview and the word diff (diff's diffWordsWithSpace). |
apps/web/src/components/editor/LessonChecksSheet.jsx | The Fix with AI button, shown only when the instance has an API and a Turnstile key. |
apps/web/src/pages/EditorPage.jsx | applyAiFix, which checks the fix once more and applies it through the same path as a quick fix. |