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 };