From f2d84bdfe3c522bbd50996d97ef68336ca799305 Mon Sep 17 00:00:00 2001 From: Asterios Raptis Date: Tue, 1 Sep 2026 15:12:39 +0200 Subject: [PATCH] chore(engine): pin 0.23.0, refresh the schema mirror, sync adopted extensions learn-content-engine 0.23.0 adds Exercise.explanation (a post-answer "why" field, maxLength 2000) and three new reference extensions, ext:ref-audio-choice, ext:ref-audio-tiles, and ext:ref-speak-and-record. The vendored schema mirror (schema/lesson.schema.json, schema/content-manifest.schema.json, schema/quality-rules.json) is refreshed against the npm tarball for this version via check_schema_drift.py --update; scripts/lce_schema.py came back byte-identical and needed no change. scripts/validate_with_engine.mjs's ADOPTED_EXTENSIONS listed only 4 of the 9 ext: types the app actually supports (ext:al-categorization, ext:al-error-correction, ext:al-reading-comprehension, ext:al-graded-quiz), missing ext:al-dictation, ext:al-image-description, ext:al-speak-and-record, ext:al-audio-choice, and ext:al-audio-tiles. The same gap was just found and fixed in alc-dog-training (PR #21) - it is a genuine, independent bug, not something introduced by this repin. The list now mirrors adaptive-learner's SUPPORTED_EXTENSIONS verbatim. Proof, via this repo's own runner at the new pin: - check_schema_drift.py: mirror in sync with 0.23.0 - validate_with_engine.mjs --self-test: all classes pass - engine-validate: 28 lessons + 4 manifests, 0 errors - lint-warnings: 0 errors, 23 pre-existing W-CARD-UNUSED/W-TILES-DUP warnings (unrelated to this repin) - audit_content.py: no quality issues found - check-stable-ids: 457 == 457, 28 == 28 lessons, 0 retired (unchanged) - check-stable-id-coverage: 3 of 3 sets fully minted, baseline matches Co-Authored-By: Claude Sonnet 5 --- schema/content-manifest.schema.json | 2 +- schema/engine-version.txt | 2 +- schema/lesson.schema.json | 55 ++++++++++++++++++++++++++++- schema/quality-rules.json | 2 +- scripts/validate_with_engine.mjs | 5 +++ 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/schema/content-manifest.schema.json b/schema/content-manifest.schema.json index bbddda5..60f813c 100644 --- a/schema/content-manifest.schema.json +++ b/schema/content-manifest.schema.json @@ -380,5 +380,5 @@ ], "title": "ContentManifest", "type": "object", - "x-schema-version": "1.11" + "x-schema-version": "1.13" } diff --git a/schema/engine-version.txt b/schema/engine-version.txt index 5a03fb7..ca222b7 100644 --- a/schema/engine-version.txt +++ b/schema/engine-version.txt @@ -1 +1 @@ -0.20.0 +0.23.0 diff --git a/schema/lesson.schema.json b/schema/lesson.schema.json index c97637b..861b102 100644 --- a/schema/lesson.schema.json +++ b/schema/lesson.schema.json @@ -266,6 +266,19 @@ "default": null, "description": "Optional placeholder text shown inside the input (``type`` mode) before the user starts typing.", "title": "Placeholder" + }, + "stable_id": { + "anyOf": [ + { + "$ref": "#/$defs/SlugId" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Stable Id", + "description": "engine#91 - schema 1.12 (additive). Element-level counterpart to the exercise/card stable_id (engine#90): identifies THIS blank for progress/SRS joins below the exercise level, so an answer-text correction (moving `accept[0]`) does not orphan its learner row. Once published it NEVER changes. Shares the SAME per-set stable_id namespace as card/exercise ids (checked the same way: collectStableIds set-wide, the schema's E-STABLE-ID-DUP rule per-document). Opaque mint-once value, NOT derived from content. Optional: content without it validates unchanged. Uses the strict $defs/SlugId shape (hyphens only) - unlike the card/exercise field, this is a brand-new field with no legacy underscore-bearing ids to grandfather." } }, "required": [ @@ -396,6 +409,20 @@ "description": "Optional inline worked examples shown BEFORE the answer controls, to help the learner understand the task (schema v1.5, additive). Each is plain text or a syntax-highlighted code snippet (see ``InlineExample.language``). Author responsibility not to spoil the answer. Independent of the per-type fields; absent on exercises that need no example.", "title": "Examples" }, + "explanation": { + "anyOf": [ + { + "maxLength": 2000, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional Markdown explanation of WHY the answer/grammar is what it is (e.g. word order, a grammatical case), shown AFTER the learner answers - regardless of correct or incorrect - unlike ``hint`` (on demand, before/during answering) and ``examples`` (worked examples shown before answering, must not spoil it). 2000 chars, twice ``hint``'s 1000: Markdown structure (a short list, bold terms) eats characters faster than a single-line nudge, and a real \"why\" often runs longer than a hint. Not restricted to any exercise type. Additive; schema_version 1.13.", + "title": "Explanation" + }, "ext_payload": { "additionalProperties": true, "description": "Opaque per-exercise payload for an ``ext:`` extension type. The core engine does not interpret it; the registered extension validator does. Absent on core exercises.", @@ -899,6 +926,19 @@ "title": "Correct", "type": "boolean" }, + "stable_id": { + "anyOf": [ + { + "$ref": "#/$defs/SlugId" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Stable Id", + "description": "engine#91 - schema 1.12 (additive). Element-level counterpart to the exercise/card stable_id (engine#90): identifies THIS option for progress/SRS joins below the exercise level. Once published it NEVER changes. Shares the SAME per-set stable_id namespace as card/exercise ids (checked the same way: collectStableIds set-wide, the schema's E-STABLE-ID-DUP rule per-document). Opaque mint-once value, NOT derived from content. Optional: content without it validates unchanged. Uses the strict $defs/SlugId shape (hyphens only) - unlike the card/exercise field, this is a brand-new field with no legacy underscore-bearing ids to grandfather." + }, "text": { "description": "The option text shown to the learner. Unique within the exercise - the text IS the option, so a duplicate would be ambiguous.", "maxLength": 500, @@ -930,6 +970,19 @@ "minLength": 1, "title": "Right", "type": "string" + }, + "stable_id": { + "anyOf": [ + { + "$ref": "#/$defs/SlugId" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Stable Id", + "description": "engine#91 - schema 1.12 (additive). Element-level counterpart to the exercise/card stable_id (engine#90): identifies THIS pair for progress/SRS joins below the exercise level, so an answer-text correction (moving `left`/`right`) does not orphan its learner row. Once published it NEVER changes. Shares the SAME per-set stable_id namespace as card/exercise ids (checked the same way: collectStableIds set-wide, the schema's E-STABLE-ID-DUP rule per-document). Opaque mint-once value, NOT derived from content. Optional: content without it validates unchanged. Uses the strict $defs/SlugId shape (hyphens only) - unlike the card/exercise field, this is a brand-new field with no legacy underscore-bearing ids to grandfather." } }, "required": [ @@ -1205,5 +1258,5 @@ ], "title": "Lesson", "type": "object", - "x-schema-version": "1.11" + "x-schema-version": "1.13" } diff --git a/schema/quality-rules.json b/schema/quality-rules.json index 849974f..bb6f88f 100644 --- a/schema/quality-rules.json +++ b/schema/quality-rules.json @@ -1,5 +1,5 @@ { - "$schema-version": "1.11", + "$schema-version": "1.13", "_comment": "GENERATED from scripts/generate_lesson_schema.py (EXP-039). Do not edit. Shared quality minimums for the content quality gate (app + content repo).", "rules": { "minExerciseTypes": 2, diff --git a/scripts/validate_with_engine.mjs b/scripts/validate_with_engine.mjs index 5e86953..156b6a5 100644 --- a/scripts/validate_with_engine.mjs +++ b/scripts/validate_with_engine.mjs @@ -47,6 +47,11 @@ const ADOPTED_EXTENSIONS = [ "ext:al-error-correction", "ext:al-reading-comprehension", "ext:al-graded-quiz", + "ext:al-dictation", + "ext:al-image-description", + "ext:al-speak-and-record", + "ext:al-audio-choice", + "ext:al-audio-tiles", ].map((type) => ({ type, major: 1, validate: () => [] })); const withExtensions = { extensions: ADOPTED_EXTENSIONS };