{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.fablepool.org/v1/course.schema.json", "title": "FablePool CourseVersion", "description": "Canonical storage format for one immutable version of a course: ordered modules containing lessons, problem sets, quizzes, and projects. Problems are referenced by id, never embedded, so they version and fork independently.", "type": "object", "required": ["schemaVersion", "kind", "title", "locale", "license", "modules"], "additionalProperties": false, "properties": { "schemaVersion": { "const": "1" }, "kind": { "const": "course" }, "courseId": { "type": "string", "format": "uuid" }, "versionLabel": { "type": "string", "maxLength": 32 }, "slug": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "minLength": 3, "maxLength": 80 }, "title": { "type": "string", "minLength": 3, "maxLength": 200 }, "summary": { "type": "string", "maxLength": 500 }, "description": { "$ref": "problem.schema.json#/$defs/RichContent" }, "locale": { "type": "string", "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$" }, "translationOf": { "type": "string", "format": "uuid" }, "license": { "type": "string", "enum": ["CC-BY-SA-4.0", "CC-BY-4.0", "CC0-1.0"] }, "attribution": { "$ref": "problem.schema.json#/$defs/Attribution" }, "tags": { "type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 50 } }, "topics": { "type": "array", "maxItems": 5, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 50 } }, "difficulty": { "type": "object", "required": ["level"], "additionalProperties": false, "properties": { "level": { "type": "integer", "minimum": 1, "maximum": 5 }, "label": { "type": "string", "enum": ["intro", "easy", "medium", "hard", "expert"] } } }, "estimatedHours": { "type": "number", "exclusiveMinimum": 0, "maximum": 200 }, "objectives": { "description": "Learner-facing outcomes: 'After this course you can …'.", "type": "array", "minItems": 1, "maxItems": 12, "items": { "type": "string", "maxLength": 300 } }, "prerequisites": { "type": "array", "maxItems": 10, "items": { "$ref": "problem.schema.json#/$defs/Prerequisite" } }, "coverAssetId": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{8,64}$" }, "assets": { "type": "array", "maxItems": 60, "items": { "$ref": "problem.schema.json#/$defs/AssetRef" } }, "modules": { "type": "array", "minItems": 1, "maxItems": 30, "items": { "$ref": "#/$defs/Module" } }, "completionPolicy": { "type": "object", "additionalProperties": false, "properties": { "requiredLessonFraction": { "description": "Fraction of required lessons that must be completed for course completion.", "type": "number", "minimum": 0.5, "maximum": 1, "default": 1 }, "requireFinalProject": { "type": "boolean", "default": false }, "minQuizAverage": { "type": "number", "minimum": 0, "maximum": 1 } } }, "changelog": { "type": "string", "maxLength": 2000 }, "x-extensions": { "type": "object", "propertyNames": { "pattern": "^[a-z0-9-]+(\\.[a-z0-9-]+)+$" } } }, "$defs": { "Module": { "type": "object", "required": ["id", "title", "lessons"], "additionalProperties": false, "properties": { "id": { "description": "Stable within the course family across versions, so Progress rows survive edits.", "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,64}$" }, "title": { "type": "string", "minLength": 1, "maxLength": 200 }, "summary": { "type": "string", "maxLength": 500 }, "unlockPolicy": { "description": "sequential: requires the previous module's required lessons; open: always available.", "type": "string", "enum": ["sequential", "open"], "default": "sequential" }, "lessons": { "type": "array", "minItems": 1, "maxItems": 40, "items": { "$ref": "#/$defs/Lesson" } } } }, "Lesson": { "type": "object", "required": ["id", "title", "lessonType", "items"], "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,64}$" }, "title": { "type": "string", "minLength": 1, "maxLength": 200 }, "lessonType": { "type": "string", "enum": ["lesson", "problem_set", "quiz", "project"] }, "summary": { "type": "string", "maxLength": 500 }, "required": { "description": "Counts toward course completion when true.", "type": "boolean", "default": true }, "estimatedMinutes": { "type": "integer", "minimum": 1, "maximum": 480 }, "items": { "type": "array", "minItems": 1, "maxItems": 60, "items": { "$ref": "#/$defs/LessonItem" } }, "quizConfig": { "description": "Required when lessonType is 'quiz'; ignored otherwise.", "$ref": "#/$defs/QuizConfig" }, "projectConfig": { "description": "Required when lessonType is 'project'; ignored otherwise.", "$ref": "#/$defs/ProjectConfig" } }, "allOf": [ { "if": { "properties": { "lessonType": { "const": "quiz" } }, "required": ["lessonType"] }, "then": { "required": ["quizConfig"] } }, { "if": { "properties": { "lessonType": { "const": "project" } }, "required": ["lessonType"] }, "then": { "required": ["projectConfig"] } } ] }, "LessonItem": { "oneOf": [ { "description": "Expository content section rendered inline.", "type": "object", "required": ["itemType", "content"], "additionalProperties": false, "properties": { "itemType": { "const": "content" }, "id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,64}$" }, "content": { "$ref": "problem.schema.json#/$defs/RichContent" } } }, { "description": "Reference to a problem. 'published-latest' tracks the problem's current published version; pinning a versionId freezes it (required inside quizzes for fairness).", "type": "object", "required": ["itemType", "problemId", "versionPolicy"], "additionalProperties": false, "properties": { "itemType": { "const": "problem" }, "id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,64}$" }, "problemId": { "type": "string", "format": "uuid" }, "versionPolicy": { "oneOf": [ { "type": "object", "required": ["mode"], "additionalProperties": false, "properties": { "mode": { "const": "published-latest" } } }, { "type": "object", "required": ["mode", "versionId"], "additionalProperties": false, "properties": { "mode": { "const": "pinned" }, "versionId": { "type": "string", "format": "uuid" } } } ] }, "weight": { "description": "Relative score weight inside quizzes/problem sets.", "type": "number", "exclusiveMinimum": 0, "maximum": 10, "default": 1 }, "optional": { "type": "boolean", "default": false } } } ] }, "QuizConfig": { "type": "object", "required": ["passingScore"], "additionalProperties": false, "properties": { "passingScore": { "type": "number", "minimum": 0, "maximum": 1 }, "attemptsAllowed": { "description": "0 means unlimited.", "type": "integer", "minimum": 0, "maximum": 10, "default": 0 }, "shuffleProblems": { "type": "boolean", "default": false }, "timeLimitMinutes": { "type": "integer", "minimum": 1, "maximum": 480 }, "showSolutionsAfter": { "type": "string", "enum": ["each_attempt", "pass", "exhausted_attempts", "never"], "default": "pass" } } }, "ProjectConfig": { "type": "object", "required": ["brief", "submissionType"], "additionalProperties": false, "properties": { "brief": { "$ref": "problem.schema.json#/$defs/RichContent" }, "submissionType": { "type": "string", "enum": ["free_text", "code", "url"] }, "rubric": { "type": "array", "minItems": 1, "maxItems": 12, "items": { "type": "object", "required": ["criterion", "points"], "additionalProperties": false, "properties": { "criterion": { "type": "string", "maxLength": 300 }, "points": { "type": "integer", "minimum": 1, "maximum": 50 }, "description": { "type": "string", "maxLength": 1000 } } } }, "gradingMode": { "type": "string", "enum": ["self_assess", "peer_review", "reviewer"], "default": "self_assess" }, "peerReviewsRequired": { "type": "integer", "minimum": 1, "maximum": 5 } } } } }