Skip to content

Lesson checks ​

For how to use this, see Lesson checks.

The editor checks a lesson against the lesson standard as it is written. The checks are the same code the MCP server runs on every write: @spelling-creator/core/lessonChecks. The rules that need judgment live in apps/mcp/src/standards.md; the checks are the mechanically checkable half of it.

The standard's numbers are defaults, not universal rules. The two "semi-open" orange types, and their order, come from the S2C guidebook (see the comments in packages/core/src/questions.js and at ORANGE_TIGHT in lessonChecks.js); the short answer rules come from letterboard spelling, where every letter is pointed to; the lesson shape (SECTION_COUNT, SPELLING_WORDS_PER_SECTION, QUESTION_ORDER) is the app's own default. Other Spelling practices do things differently, which is part of why most shape rules are warnings.

In the editor ​

  • Check in the editor's top bar (and Check lesson in the phone overflow menu) opens the panel at /editor/check, with a red badge counting problems.
  • SectionOutline.jsx shows a problem count beside each section that has one.
  • Choosing a finding closes the sheet, then goToFinding in EditorPage.jsx leaves Preview, expands the section, scrolls the block into view and focuses a field (see Two descriptions of every finding for how it picks which one).
  • Below the findings sits the Facts section (FactCheckSection.jsx), which runs only on request. See Fact checking.

Nothing blocks. A lesson with problems saves, exports, publishes and prints exactly as before.

Problems and suggestions ​

The checks report two levels, and the editor keeps the MCP server's split between them:

LevelFrom coreIn the editor
ProblemerrorsAlways listed, counted in the bar and the outline.
SuggestionwarningsFolded away behind Show N suggestions, and never counted anywhere but there.

The line between them is whether a legitimate lesson could ever trip the check. An MCP write is rejected on an error and carries warnings along with a successful one; the editor blocks on neither.

The full list of codes and what trips each one is in Lesson validation. The editor's wording for each is in apps/web/src/locales/en/checks.json under codes.

Quick fixes ​

A finding whose fix needs no judgment gets a button under it in the panel. The fix is made straight away, the panel stays open, and the finding drops off the list once the checks rerun. A click on a finding that has already gone stale (the panel lags an edit by a beat, or a collaborator fixed it first) says so in a toast (fix.gone) rather than doing nothing.

CodeButton (quickFix in checks.json)What it does
W_FORMAT_BOLDRemove the boldTakes bold off every text block in the section. Italics and underlining stay.
W_FORMAT_UNDERLINERemove the underliningThe same for underlining.
W_FORMAT_CAPSRemove the formattingUnformats the ALL-CAPS spans only.
E_FORMAT_HEAVYRemove all formatting in this sectionEvery mark in the section's text, italics included.
E_FORMAT_LONG_EMPHASIS, E_FORMAT_LONG_ITALICMake it plainUnformats the one long span the finding quotes.
W_VAKT_NOT_LASTMove it to the endMoves the section's VAKT activities to its end, keeping their order.
W_ORANGE_ORDERSwap themPuts the tight orange question in the first orange slot.
E_ORANGE_PARTIAL_LISTAccept "silt" tooAdds the item the passage's list goes on to as an accepted answer. A two-word item gives its last word: "fine silt" adds "silt".

Before a fix is made, applyLessonFix in EditorPage.jsx commits the lesson as it stood, so the fix is a version of its own in Version history and Undo this change there can take it back at any time. The panel then shows Fixed. with a quicker Undo. It stays only while nothing else has changed: once the lesson has been edited again, putting the old lesson back would throw that edit away too, so the line goes and Version history is the way back. The Undo is in the panel rather than in a toast because the panel is modal, and while it is open nothing outside it can be clicked.

Most findings need judgment instead: whether to change an answer or the passage it should be in, say. Those get Fix with AI, which shows the fix before making it. See AI lesson fixes.

The quick fixes live in packages/core/src/lessonFixes.js. Each takes the document and the finding and returns the fixed document, or null when there is nothing left to do. Only the section and blocks a fix touches are new objects, and text blocks are written with withTextBlockDocument, the way the editor writes them. QUICK_FIX_CODES lists the codes and hasQuickFix(finding) is what the panel asks.

How it fits together ​

FileDoes
packages/core/src/lessonChecks.jsThe checks. validateLesson(doc) returns { errors, warnings }.
apps/web/src/lib/lessonChecks.jsuseLessonChecks(doc), the per-section tallies, and describeFinding, which words a finding for a person.
packages/core/src/lessonFixes.jsThe quick fixes. applyQuickFix(doc, finding) returns the fixed document or null.
apps/web/src/locales/en/checks.jsonThe editor's wording for every code, under codes, and the fix buttons, under quickFix.
apps/web/src/components/editor/LessonChecksSheet.jsxThe panel.
apps/web/src/components/editor/checkGroups.jsGrouping findings by section and naming the groups, shared with the panel's Facts section.
apps/web/src/components/editor/SectionOutline.jsxThe per-section counts.

validateLesson takes the canonical document, which is the shape the editor already holds in state, so the editor passes it doc with no conversion.

The checks are cheap (about a millisecond on a full six-section lesson). What isn't cheap is the page that calls the hook, which is the whole editor, so the hook is built to avoid rendering it again. It reruns the checks once editing has paused for 300ms (SETTLE_MS), and replaces its result only when a finding actually changed. Most edits (typing inside a passage, say) change none, so they cost no render beyond their own. A test in lessonChecks.test.js holds it to that.

It is also wrapped so that a bug in one check logs an error and shows nothing, rather than taking the editor down. An empty lesson is not checked at all; its only finding would be "0 sections", greeting everyone who opens the editor.

Two descriptions of every finding ​

Each finding carries message, prose written for a model: it names fields in backticks and ends by telling the model to resubmit or pass skipValidation. None of that makes sense to a person, and it isn't translated. So findings also carry:

  • params: the same facts as data (the answer, the word, the question number, the other question in a collision).
  • sectionId and blockId: where to go. A question's finding points at the question, a spelling word's at its spelling block, a formatting finding at the text block holding the first offending span. Findings about a section's shape point at its first relevant question.
  • itemId: within that block, the one spelling word or orange answer the finding is about. The editor focuses the field whose data-collab-field ends in that id, so a finding about "ash" lands on "ash", not on the first word in the list, even when the same word appears twice. Without one, it focuses the block's first field.

Values quoted from the passage keep the passage's own casing. The checks compare uppercased text, but a word shown back as SILT would read as vocabulary, which is what ALL CAPS means in a lesson.

The editor ignores message and renders t("codes.<code>", params) from the checks namespace. The MCP server sends only code, section and message to the model (toWireWarnings in apps/mcp/src/tools.js), so the extra fields never reach it.

A code with more than one wording uses an i18next context, chosen by the CONTEXTS table in lib/lessonChecks.js: E_SPELLING_DUPLICATE_same for a word listed twice in one section, E_ANSWER_WORD_REUSED_inside for an answer found inside a longer one, E_FORMAT_LONG_EMPHASIS_bold for bold rather than underlining, E_FORMAT_HEAVY_share when a section broke the share limit rather than the span count (core says which, in params.tooMany), W_ORANGE_ANSWER_COUNT_open for a suggested answers question with no answers, and W_WYR_SHAPE_many for a prompt that chains several "or"s. A collision with a question in the same section names it as "question 2" rather than "section 1, question 2".

Keeping the wording in step ​

apps/web/src/lib/lessonChecks.test.js reads every E_/W_ code out of the core source and fails if checks.json is missing the base wording for one (the one a finding falls back to when no context applies), is missing a wording for any context in CONTEXTS, or still has wording for a code core no longer reports. It also fails if quickFix doesn't label exactly the codes in QUICK_FIX_CODES.

It checks that the wording exists, not that a finding's params fill it in. That is only exercised for the codes its fixture lesson trips, so a new check needs its params, a line in checks.json, and a case in that fixture.

Copyright © 2026 Spelling Creator.