Adding a new exercise type¶
The canonical model is not extended on spec. A new exercise type is added
only when concrete content needs it, and then as one small additive PR. This is
the binding recipe, derived from the real cloze/select multiple-choice work
(#1342) and the EXP-039 schema pipeline.
Before you start, confirm the type is a genuine new type, not a presentation or a convention already covered by the exercise type catalog (text multiple choice, True/False, dropdown/radio/checkbox are not new types). It must be binary SRS-gradable (a single correct/incorrect outcome per element) — that is the line the catalog's "deliberately excluded" list draws.
Steps¶
- EXP entry / justification. Record the need, the binary grading
semantics, and the delimitation from existing types in the relevant
exploration (
docs/explorations/EXP-041-*for exercise-type suitability, or a new EXP). No type without a documented reason. - Pydantic model + enum. Add the value to
ExerciseTypeand the type-specific fields +model_validator(withmodel_config = ConfigDict(extra="forbid")) inplugins/adaptive-learner-plugin-content-loader/adaptive_learner_content_loader/schema.py. The schema (App) is the current source of truth (EXP-039). - Run the generation.
make sync-schemaregeneratesschema/*.jsonand the TS lesson types (frontend/src/storage/types/content/lesson-schema.generated.ts) - the format-reference doc. Never hand-edit a generated artefact; the
make sync-schema-checkdrift gate fails if you do. - Bump the schema version. Raise
CURRENT_SCHEMA_VERSIONinmodels.pyby a minor (additive) step; old content keeps validating (major-version match). - Register the renderer. Add the branch + the type to
SUPPORTED_EXERCISE_TYPESinfrontend/src/components/exercises/shell/ExerciseDispatcher.tsx. The registry must equal the enum — a parity test enforces it, so an unrendered type fails CI (the invariant that prevents dead schema). - Wire grading / SRS. Emit an
ExerciseScoredfrom the renderer viauseControlledExercise; the sharedonComplete→recordStepResultpath inLessonStepView.tsxalready fans each attempt out throughgetStorage().elementErrors.recordBulk— reuse it, do not add a second recording path. - Content-repo validation. Extend the client validator
(
frontend/src/lib/content/validation/content-validator.ts) and, if the type affects the quality minimums, the sharedQUALITY_RULESinscripts/generate_lesson_schema.py(vendored by learn-content-engine, which the content repos mirror pinned to its release). - Authoring docs. Add the type to the
catalog table and a
### <type>reference block with a JSON example (EN + DE). - Tests. Schema accepts a valid example and rejects an invalid one (missing required field / extra key); the renderer renders + grades correct/wrong; the SRS attempt is recorded; add a mobile visual baseline if the control's look is new.
- Follow-up (not this PR). The
learn-content-enginelibrary adopts the extended schema during its migration; note it, do not block on it.
Why this stays small¶
Because the schema is generated (step 3) and the dispatcher parity test forces registry-equals-enum (step 5), a new type is an additive change with a fixed shape: model → generate → renderer → grading → docs → tests. No parallel hand-maintained mirror can drift, and no type can ship without a renderer.