Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ card reference); `ref-speak-and-record` is the first reference extension with
no grade function, since a recording has nothing to check it against. See
`docs/extensions.md` for the payload rules and reference lessons.

### `explanation`: post-answer "why" on any exercise (idea 5)

A fifth language-learning exercise idea surfaced a gap the existing "extra
text" fields do not cover: `hint` is shown on demand before/during answering,
`examples` are worked examples shown before answering that must not spoil it,
and neither fits "explain the French word-order rule AFTER the learner
answers". `Exercise` gains an optional `explanation` (Markdown, max 1000
chars, mirrors `hint`'s shape) - not restricted to any exercise type, like
`hint` itself.

`x-schema-version` `1.12` -> `1.13` in both schemas (lesson + content-manifest
move in lockstep, established convention). Additive: content without the new
field validates unchanged.

## [0.22.0] - 2026-08-11

### Element-level stable identity: pairs, blanks, options (engine#91 Phase 2)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ network, storage, or UI code - you supply the bytes and keep fetch +
persistence. The bundled, strict JSON-Schema makes it a self-contained **format
reference**: you can author and validate lessons without the application the
format originated in ([Adaptive Learner](https://github.com/astrapi69/adaptive-learner)).
Tracks the lesson schema, currently **v1.12**.
Tracks the lesson schema, currently **v1.13**.

## Install

Expand Down
2 changes: 1 addition & 1 deletion docs/blog/de/one-source-many-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Drei Dinge bleiben in dem, was Sie schreiben, bewusst getrennt: der **Inhalt** (

Sie müssen dieses JSON nicht von Hand schreiben. Die Referenz-App liefert einen **Lektions-Editor** mit, der dem Wort "einfach" entwachsen ist: der klassische vierstufige Assistent (Metadaten, Karten, Übungen, Speichern & Teilen), ein Buch-Pfad, der eingefügte oder hochgeladene Lehrbuchkapitel in Wissens-Lektionen verwandelt, ein Erweiterungs-Zweig, der selbst benotete Quizze und Diktate ohne JSON autorierbar macht, und ein Bearbeitungsmodus. All das erzeugt Lektionen in demselben kanonischen Schema und kann sogar direkt aus der App einen Pull Request gegen ein Content-Repository öffnen; [Teil 3 der Serie](create-a-lesson-in-the-app.md) geht jeden Pfad durch. Was darüber hinaus **geplant** ist, ist ein reicherer, auf Lehrende zugeschnittener Editor für die schwereren Fälle (größere Sets). Beides ändert das Modell nicht: Die Quelle bleibt der Vertrag, und jeder Editor (einfach oder reich) ist nur ein weiteres Werkzeug, das ihn liest und schreibt.

> **Additiv als Prinzip.** Das Schema ist versioniert (aktuell 1.12) und wächst nur additiv: Neue Felder sind optional, eine letztes Jahr geschriebene Lektion validiert also heute noch. Sie werden nie gezwungen, Inhalte zu migrieren, nur weil sich das Schema bewegt hat.
> **Additiv als Prinzip.** Das Schema ist versioniert (aktuell 1.13) und wächst nur additiv: Neue Felder sind optional, eine letztes Jahr geschriebene Lektion validiert also heute noch. Sie werden nie gezwungen, Inhalte zu migrieren, nur weil sich das Schema bewegt hat.

## Validierung ist Rechtschreibprüfung für Test-Logik

Expand Down
4 changes: 2 additions & 2 deletions docs/blog/de/schema-first-content-engineering.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ tags: [architecture, schema-design, typescript, content-engineering]

*Wie `learn-content-engine` ein stabiles Kern-Schema behält und trotzdem Raum für pädagogische Erfindung lässt: durch eine harte Linie zwischen dem Vertrag, den es besitzt, und den Regeln, die seine Consumer besitzen.*

`learn-content-engine` · Schema aktuell v1.12 · framework-agnostisches TypeScript
`learn-content-engine` · Schema aktuell v1.13 · framework-agnostisches TypeScript

## Das Content-Schema-Dilemma

`learn-content-engine` ist eine framework-agnostische TypeScript-Bibliothek, die Lerninhalte parst und validiert: Sprachkurse zuallererst, wobei ein `domain`-Feld (seit Engine 0.20.0 ein kontrolliertes Vokabular aus bekannten Werten plus other) dieselbe Form auch andere Wissensgebiete tragen lässt (Technik-Kurse, Führerschein-Vorbereitung, Psychologie). Sie verwandelt Rohquellen (Lektions-JSON plus eine `manifest.yaml`) in eine kanonische interne Form, und sie ist die einzige Quelle der Wahrheit für das Lektions-Schema, aktuell Version 1.12.
`learn-content-engine` ist eine framework-agnostische TypeScript-Bibliothek, die Lerninhalte parst und validiert: Sprachkurse zuallererst, wobei ein `domain`-Feld (seit Engine 0.20.0 ein kontrolliertes Vokabular aus bekannten Werten plus other) dieselbe Form auch andere Wissensgebiete tragen lässt (Technik-Kurse, Führerschein-Vorbereitung, Psychologie). Sie verwandelt Rohquellen (Lektions-JSON plus eine `manifest.yaml`) in eine kanonische interne Form, und sie ist die einzige Quelle der Wahrheit für das Lektions-Schema, aktuell Version 1.13.

Der Kern ist bewusst klein. Kein Rendering, keine Persistenz, kein Netzwerk; die einzige Laufzeit-Abhängigkeit ist ein YAML-Parser. Was er bietet, ist reine Validierung und Transformation. Dieser Minimalismus ist der Punkt, und er erzwingt eine harte Frage: *Wie entwickelt man ein Content-Schema weiter, ohne jeden Consumer zu brechen, der davon abhängt?*

Expand Down
2 changes: 1 addition & 1 deletion docs/blog/one-source-many-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Three things stay deliberately separate in what you write: the **content** (the

You don't have to write that JSON by hand. The reference app ships a **lesson creator** that has outgrown the word "simple": the classic four-step wizard (metadata, cards, exercises, save & share), a book path that turns pasted or uploaded textbook chapters into knowledge lessons, an extension branch that makes even graded quizzes and dictation authorable without JSON, and an edit mode. All of it produces lessons in this same canonical schema and can even open a pull request against a content repository directly from the app; part 3 of the series walks through every path. What is **planned** beyond it is a richer, teacher-facing editor for the heavier cases (larger sets). Neither changes the model: the source stays the contract, and every editor (simple or rich) is just one more tool that reads and writes it.

> **Additive by policy.** The schema is versioned (currently 1.12) and only grows additively: new fields are optional, so a lesson written last year still validates today. You are never forced to migrate content just because the schema moved.
> **Additive by policy.** The schema is versioned (currently 1.13) and only grows additively: new fields are optional, so a lesson written last year still validates today. You are never forced to migrate content just because the schema moved.

## Validation is spell-check for test logic

Expand Down
4 changes: 2 additions & 2 deletions docs/blog/schema-first-content-engineering.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ tags: [architecture, schema-design, typescript, content-engineering]

*How `learn-content-engine` keeps a stable core schema and still leaves room for pedagogical invention: by drawing a hard line between the contract it owns and the rules its consumers own.*

`learn-content-engine` · schema currently v1.12 · framework-agnostic TypeScript
`learn-content-engine` · schema currently v1.13 · framework-agnostic TypeScript

## The content-schema dilemma

`learn-content-engine` is a framework-agnostic TypeScript library that parses and validates learning content: language courses foremost, though a `domain` field (a known-values-plus-other vocabulary since engine 0.20.0) lets the same shape carry other knowledge domains (tech courses, driving-test prep, psychology). It turns raw sources (lesson JSON plus a `manifest.yaml`) into a canonical internal shape, and it is the single source of truth for the lesson schema, currently version 1.12.
`learn-content-engine` is a framework-agnostic TypeScript library that parses and validates learning content: language courses foremost, though a `domain` field (a known-values-plus-other vocabulary since engine 0.20.0) lets the same shape carry other knowledge domains (tech courses, driving-test prep, psychology). It turns raw sources (lesson JSON plus a `manifest.yaml`) into a canonical internal shape, and it is the single source of truth for the lesson schema, currently version 1.13.

The core is deliberately small. No rendering, no persistence, no networking; its only runtime dependency is a YAML parser. What it offers is pure validation and transformation. That minimalism is the point, and it forces one hard question: *how do you evolve a content schema without breaking every consumer that depends on it?*

Expand Down
2 changes: 1 addition & 1 deletion docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ inherited by the set's lessons unless a lesson declares its own.

## Schema-version policy (additive)

The lesson schema is versioned (`x-schema-version`, currently `1.12`) and evolves
The lesson schema is versioned (`x-schema-version`, currently `1.13`) and evolves
**additively**: new fields are optional, so **older content stays valid under a
newer schema**. For example, v1.5 added the inline `examples` field; a v1.4
lesson without `examples` validates unchanged under v1.5. v1.6 added the native
Expand Down
19 changes: 18 additions & 1 deletion docs/lesson-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,26 @@ and `prompt`. `type` is one of `matching`, `picture_choice`, `free_text`,
for-type combinations are rejected (see [validation.md](validation.md)).

Common optional exercise fields: `card_ids` (the cards drilled; each must exist),
`distractors`, `hint`, `direction`, `examples`, and `stable_id` (schema v1.9,
`distractors`, `hint`, `direction`, `examples`, `explanation` (schema v1.13,
see [Explanation](#explanation-post-answer)), and `stable_id` (schema v1.9,
see [Stable identity](#stable-identity-stable_id)).

### Explanation (post-answer)

`explanation` (string \| null, max 1000 chars, schema v1.13) is Markdown
explaining WHY the answer/grammar is what it is - a French word-order rule, a
grammatical case, a spelling exception. Timing is what distinguishes it from
the other three "extra text" fields:

| Field | Shown | Purpose |
|---|---|---|
| `examples` | Before answering | Worked examples, must not spoil the answer. |
| `hint` | On demand, before/during answering | A nudge behind a "Need a hint?" button. |
| `explanation` | After answering (correct or not) | The reasoning behind the answer. |

Not restricted to any exercise type; an author uses it wherever the "why" adds
value and omits it everywhere else.

### matching

Match left items to right items. Requires a non-empty `pairs` list of
Expand Down
2 changes: 1 addition & 1 deletion docs/schema-diagrams.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ false ordering sentence survived in this repo for weeks.

<!-- GENERATED:schema-diagrams BEGIN - do not edit by hand, run scripts/generate-schema-diagrams.mjs -->

<!-- schema x-schema-version: 1.12 -->
<!-- schema x-schema-version: 1.13 -->

### 1. Content structure (generated)

Expand Down
2 changes: 1 addition & 1 deletion schema/content-manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -380,5 +380,5 @@
],
"title": "ContentManifest",
"type": "object",
"x-schema-version": "1.12"
"x-schema-version": "1.13"
}
16 changes: 15 additions & 1 deletion schema/lesson.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,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": 1000,
"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). 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.",
Expand Down Expand Up @@ -1244,5 +1258,5 @@
],
"title": "Lesson",
"type": "object",
"x-schema-version": "1.12"
"x-schema-version": "1.13"
}
2 changes: 1 addition & 1 deletion schema/quality-rules.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema-version": "1.12",
"$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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,5 +380,5 @@
],
"title": "ContentManifest",
"type": "object",
"x-schema-version": "1.12"
"x-schema-version": "1.13"
}
16 changes: 15 additions & 1 deletion src/__fixtures__/schema-baseline/lesson.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,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": 1000,
"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). 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.",
Expand Down Expand Up @@ -1244,5 +1258,5 @@
],
"title": "Lesson",
"type": "object",
"x-schema-version": "1.12"
"x-schema-version": "1.13"
}
2 changes: 1 addition & 1 deletion src/schema-baseline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const NEW_ID_BASE = "https://astrapi69.github.io/learn-content-engine/schema";
/** The current, deliberately-set schema version. Bumping it is a conscious
* release decision (new exercise type = minor); update it together with the
* frozen baseline in the same commit. */
const EXPECTED_SCHEMA_VERSION = "1.12";
const EXPECTED_SCHEMA_VERSION = "1.13";

const readText = (relativePath: string): string =>
readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8");
Expand Down
5 changes: 5 additions & 0 deletions src/types/lesson-schema.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ export type Distractors = string[];
* 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.
*/
export type Examples1 = InlineExample[] | null;
/**
* 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). Not restricted to any exercise type. Additive; schema_version 1.13.
*/
export type Explanation = string | null;
/**
* MATCHING: when true, the exercise derives its ``pairs`` from the referenced cards (left = card ``front``, right = card ``back``) instead of listing them explicitly, so a definition lives in one place. Requires non-empty ``card_ids`` and forbids an explicit ``pairs`` list. The engine resolves it to concrete ``pairs`` at parse time. Additive + optional; schema_version stays 1.5.
*/
Expand Down Expand Up @@ -520,6 +524,7 @@ export interface Exercise {
direction?: Direction;
distractors?: Distractors;
examples?: Examples1;
explanation?: Explanation;
/**
* 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.
*/
Expand Down
48 changes: 48 additions & 0 deletions src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,54 @@ describe("schema 1.12 — stable_id on pairs/blanks/options (engine#91 Phase 2)"
});
});

describe("schema 1.13 — explanation on exercises (idea 5: post-answer 'why')", () => {
const lessonWithExplanation = (explanation?: string | null) => ({
id: "l1",
title: "Explanation",
steps: [
{
id: "s1",
type: "exercise",
exercise: {
id: "e1",
type: "free_text",
prompt: "p",
accept: ["a"],
...(explanation !== undefined ? { explanation } : {}),
},
},
],
});

it("accepts an exercise with a Markdown explanation", () => {
const checked = validateLesson(lessonWithExplanation("Il s'agit du subjonctif car..."));
expect(checked.errors).toEqual([]);
expect(checked.valid).toBe(true);
});

it("stays optional: an exercise without explanation validates unchanged (pre-1.13 content)", () => {
expect(validateLesson(lessonWithExplanation()).valid).toBe(true);
});

it("accepts an explicit null (the documented default)", () => {
expect(validateLesson(lessonWithExplanation(null)).valid).toBe(true);
});

it("boundary: rejects an explanation longer than 1000 characters", () => {
expect(validateLesson(lessonWithExplanation("x".repeat(1001))).valid).toBe(false);
});

it("boundary: accepts an explanation at exactly 1000 characters", () => {
expect(validateLesson(lessonWithExplanation("x".repeat(1000))).valid).toBe(true);
});

it("is not restricted to one exercise type: a matching exercise may carry it too", () => {
const lesson = clone(conf("matching"));
exerciseOf(lesson).explanation = "Diese Zuordnung folgt der grammatischen Regel...";
expect(validateLesson(lesson).valid).toBe(true);
});
});

describe("schema 1.9 — attribution and review_status on the set entry (engine#90/#94)", () => {
const manifestWith = (setExtras: Record<string, unknown>) => ({
schema_version: "1.2",
Expand Down
Loading