Lesson validation
Every tool that writes a lesson — create_lesson, create_lesson_file, update_lesson
and patch_lesson — checks it against the authoring standard first. Errors reject the
write; warnings ride along with a successful one.
The point of validating rather than only documenting is that the standard then holds even
when the model never read it. Server instructions are optional in the MCP spec and some
clients drop them (claude.ai's connector UI is the notable one), and a tool description is
advice the model may or may not follow. Validation does not depend on either.
The split between the two halves of the standard lives in two files:
Keep them in step: a rule stated in one that the other also covers should describe the same thing.
Errors — the write is rejected
A rejection names the section, the offending value and the fix, because the model reads it
and resubmits — "validation failed" buys a guess, a specific message buys a correction in
one round trip. Up to 25 are listed at a time.
Warnings — saved, and reported back
Returned as a warnings array on the successful result:
These are warnings and not errors because a legitimate lesson can trip each one: a user who
asks for four sections gets W_SECTION_COUNT and should not be blocked by it.
skipValidation
Every writing tool takes skipValidation: true, which turns the errors off (and with
them the warnings — nothing is checked). It exists for the user who deliberately wants a
lesson the standard forbids, not as a way around a defect that should be fixed.
Patching an existing lesson
patch_lesson validates the lesson before and after the edit and holds the caller
only to the difference. Without that, a one-line tweak to a lesson written in the web
editor — or written before these rules existed — would be blocked by defects the patch
never touched and the assistant may have no mandate to change. The filter applies to
warnings as well as errors, so a patch reports only what its own edit introduced.
Findings are matched on the defect's identity rather than its message, which has to hold two properties at once:
- Section numbers can't be part of it. Moving or inserting a section would otherwise
make every later finding look new. The identity uses the section's and block's ids,
which survive
move_section,move_blockandreplace_block. - Block identity has to be part of it. Code plus offending value alone is not enough: a patch can add a genuinely new question carrying the same defect on the same word, and it would be written off as pre-existing. Including the block's id separates them.
For a collision, which names two parties (two spelling words, two questions), the pair is sorted before it becomes a key — otherwise reordering the sections swaps which end the walk reaches first and rewrites the identity of a defect nobody touched.
update_lesson replaces the whole document, so it gets no such exemption: whatever the
result contains, the caller sent. That is a reason to prefer patch_lesson for small
edits.
Comparison rules
Text is normalised before any comparison — uppercased, punctuation dropped, whitespace collapsed. Two details matter and both caused false failures before they were handled:
- Thousands separators. The passage says
3,776and the answer field holds3776. Both normalise to3776. - Decimal points.
112.5has to survive the punctuation strip as one token, while the full stop inMAGMA.must not.
Grounding uses whole-word matching, so ASH is not found inside WASHED. The spelling
collision check deliberately uses raw substring matching instead, because PRISON
really is inside PRISONER'S.
Passages are flattened out of rich text first, so a lesson round-tripped through the web editor (which stores HTML) is compared on its words rather than its markup.