{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.fablepool.org/v1/problem.schema.json", "title": "FablePool ProblemVersion", "description": "Canonical storage format for one immutable version of a community problem. Includes the shared rich-content model used by all FablePool schemas.", "type": "object", "required": ["schemaVersion", "kind", "title", "locale", "license", "difficulty", "tags", "statement", "answerSpec"], "additionalProperties": false, "properties": { "schemaVersion": { "const": "1" }, "kind": { "const": "problem" }, "problemId": { "description": "Stable identity of the problem family (UUID). Absent in not-yet-saved drafts and in import bundles before id assignment.", "type": "string", "format": "uuid" }, "versionLabel": { "description": "Human-readable version label assigned by the platform, e.g. 'v3'. Informational only; authoritative version identity lives in the database.", "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": { "description": "Plain-text teaser shown in lists and search results. No markup.", "type": "string", "maxLength": 500 }, "locale": { "description": "BCP 47 language tag of this version's content.", "type": "string", "pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$" }, "translationOf": { "description": "If this version is a translation, the problemId of the source-locale problem.", "type": "string", "format": "uuid" }, "license": { "description": "Content license. The platform only accepts CC BY-SA 4.0 or more permissive CC licenses compatible with it.", "type": "string", "enum": ["CC-BY-SA-4.0", "CC-BY-4.0", "CC0-1.0"] }, "attribution": { "$ref": "#/$defs/Attribution" }, "tags": { "type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 50 } }, "topics": { "description": "Slugs of curated taxonomy topics (Topic entities), e.g. 'number-theory'.", "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"] } } }, "estimatedMinutes": { "type": "integer", "minimum": 1, "maximum": 240 }, "prerequisites": { "type": "array", "maxItems": 10, "items": { "$ref": "#/$defs/Prerequisite" } }, "statement": { "$ref": "#/$defs/RichContent" }, "answerSpec": { "$ref": "attempt-answer.schema.json#/$defs/AnswerSpec" }, "hints": { "type": "array", "maxItems": 8, "items": { "$ref": "hint-solution.schema.json#/$defs/Hint" } }, "solutions": { "type": "array", "maxItems": 5, "items": { "$ref": "hint-solution.schema.json#/$defs/Solution" } }, "assets": { "description": "Manifest of MediaAssets referenced by content blocks. Every image/audio block must reference an assetId listed here.", "type": "array", "maxItems": 30, "items": { "$ref": "#/$defs/AssetRef" } }, "changelog": { "description": "Author-written summary of what changed relative to the parent version.", "type": "string", "maxLength": 2000 }, "x-extensions": { "description": "Namespaced experimental extensions. Keys must be reverse-DNS style. Renderers must ignore unknown extensions.", "type": "object", "propertyNames": { "pattern": "^[a-z0-9-]+(\\.[a-z0-9-]+)+$" } } }, "$defs": { "Prerequisite": { "type": "object", "required": ["refType", "ref"], "additionalProperties": false, "properties": { "refType": { "type": "string", "enum": ["problem", "course", "topic"] }, "ref": { "description": "UUID for problem/course, topic slug for topic.", "type": "string", "maxLength": 80 }, "note": { "type": "string", "maxLength": 200 } } }, "Attribution": { "type": "object", "additionalProperties": false, "properties": { "authors": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "type": "object", "required": ["name"], "additionalProperties": false, "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 120 }, "userId": { "type": "string", "format": "uuid" }, "url": { "type": "string", "format": "uri", "pattern": "^https://" }, "role": { "type": "string", "enum": ["author", "translator", "illustrator", "editor"] } } } }, "forkedFrom": { "type": "object", "required": ["problemId", "versionId"], "additionalProperties": false, "properties": { "problemId": { "type": "string", "format": "uuid" }, "versionId": { "type": "string", "format": "uuid" }, "title": { "type": "string", "maxLength": 200 }, "url": { "type": "string", "format": "uri", "pattern": "^https://" } } }, "externalSources": { "description": "Citations for adapted material. Required by policy when content is adapted from a compatible external OER.", "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["title", "license"], "additionalProperties": false, "properties": { "title": { "type": "string", "maxLength": 300 }, "author": { "type": "string", "maxLength": 200 }, "url": { "type": "string", "format": "uri", "pattern": "^https://" }, "license": { "type": "string", "maxLength": 50 } } } } } }, "AssetRef": { "type": "object", "required": ["assetId", "mediaType", "sha256"], "additionalProperties": false, "properties": { "assetId": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{8,64}$" }, "mediaType": { "type": "string", "enum": ["image/png", "image/jpeg", "image/webp", "image/svg+xml", "image/gif", "audio/mpeg", "audio/ogg"] }, "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "byteSize": { "type": "integer", "minimum": 1, "maximum": 10485760 }, "originalFilename": { "type": "string", "maxLength": 255 } } }, "RichContent": { "description": "Ordered list of block nodes. This is the shared content model for statements, hints, solutions, lessons, and discussion-free contexts.", "type": "array", "maxItems": 200, "items": { "$ref": "#/$defs/Block" } }, "Block": { "oneOf": [ { "$ref": "#/$defs/ParagraphBlock" }, { "$ref": "#/$defs/HeadingBlock" }, { "$ref": "#/$defs/MathBlock" }, { "$ref": "#/$defs/CodeBlock" }, { "$ref": "#/$defs/ImageBlock" }, { "$ref": "#/$defs/ListBlock" }, { "$ref": "#/$defs/TableBlock" }, { "$ref": "#/$defs/CalloutBlock" }, { "$ref": "#/$defs/QuoteBlock" }, { "$ref": "#/$defs/WidgetBlock" }, { "$ref": "#/$defs/DividerBlock" } ] }, "ParagraphBlock": { "type": "object", "required": ["type", "content"], "additionalProperties": false, "properties": { "type": { "const": "paragraph" }, "content": { "$ref": "#/$defs/InlineContent" } } }, "HeadingBlock": { "type": "object", "required": ["type", "level", "content"], "additionalProperties": false, "properties": { "type": { "const": "heading" }, "level": { "description": "2–4 only. Level 1 is reserved for the document title rendered by the platform chrome.", "type": "integer", "minimum": 2, "maximum": 4 }, "content": { "$ref": "#/$defs/InlineContent" } } }, "MathBlock": { "type": "object", "required": ["type", "latex", "alt"], "additionalProperties": false, "properties": { "type": { "const": "math" }, "latex": { "type": "string", "minLength": 1, "maxLength": 5000 }, "alt": { "description": "Spoken-form description for screen readers, e.g. 'the integral from 0 to 1 of x squared dx'. Required for display math.", "type": "string", "minLength": 1, "maxLength": 1000 }, "numbered": { "type": "boolean", "default": false } } }, "CodeBlock": { "type": "object", "required": ["type", "language", "code"], "additionalProperties": false, "properties": { "type": { "const": "code" }, "language": { "type": "string", "enum": ["python", "javascript", "typescript", "c", "cpp", "java", "rust", "go", "haskell", "sql", "pseudocode", "text"] }, "code": { "type": "string", "maxLength": 20000 }, "caption": { "type": "string", "maxLength": 300 }, "highlightLines": { "type": "array", "maxItems": 50, "items": { "type": "integer", "minimum": 1 } } } }, "ImageBlock": { "type": "object", "required": ["type", "assetId", "alt"], "additionalProperties": false, "properties": { "type": { "const": "image" }, "assetId": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{8,64}$" }, "alt": { "description": "Required alternative text. Use empty string only for purely decorative images.", "type": "string", "maxLength": 1000 }, "caption": { "$ref": "#/$defs/InlineContent" }, "width": { "type": "integer", "minimum": 1, "maximum": 4096 }, "height": { "type": "integer", "minimum": 1, "maximum": 4096 } } }, "ListBlock": { "type": "object", "required": ["type", "ordered", "items"], "additionalProperties": false, "properties": { "type": { "const": "list" }, "ordered": { "type": "boolean" }, "items": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "required": ["content"], "additionalProperties": false, "properties": { "content": { "$ref": "#/$defs/RichContent" } } } } } }, "TableBlock": { "type": "object", "required": ["type", "rows"], "additionalProperties": false, "properties": { "type": { "const": "table" }, "caption": { "type": "string", "maxLength": 300 }, "header": { "type": "array", "maxItems": 12, "items": { "$ref": "#/$defs/InlineContent" } }, "rows": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "array", "minItems": 1, "maxItems": 12, "items": { "$ref": "#/$defs/InlineContent" } } } } }, "CalloutBlock": { "type": "object", "required": ["type", "variant", "content"], "additionalProperties": false, "properties": { "type": { "const": "callout" }, "variant": { "type": "string", "enum": ["note", "tip", "warning", "definition", "theorem", "example"] }, "title": { "type": "string", "maxLength": 200 }, "content": { "$ref": "#/$defs/RichContent" } } }, "QuoteBlock": { "type": "object", "required": ["type", "content"], "additionalProperties": false, "properties": { "type": { "const": "quote" }, "content": { "$ref": "#/$defs/RichContent" }, "attribution": { "type": "string", "maxLength": 200 } } }, "WidgetBlock": { "description": "Embeds a sandboxed widget. The textFallback is mandatory so content degrades on low-bandwidth/no-JS clients and for assistive technology when the widget declares an 'alternative-task' strategy.", "type": "object", "required": ["type", "widgetName", "widgetVersion", "textFallback"], "additionalProperties": false, "properties": { "type": { "const": "widget" }, "widgetName": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 80 }, "widgetVersion": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" }, "params": { "description": "Instance parameters; validated against the widget manifest's paramsSchema at save time.", "type": "object" }, "heightHint": { "type": "integer", "minimum": 100, "maximum": 1200 }, "textFallback": { "$ref": "#/$defs/RichContent" } } }, "DividerBlock": { "type": "object", "required": ["type"], "additionalProperties": false, "properties": { "type": { "const": "divider" } } }, "InlineContent": { "type": "array", "maxItems": 500, "items": { "$ref": "#/$defs/Inline" } }, "Inline": { "oneOf": [ { "$ref": "#/$defs/TextSpan" }, { "$ref": "#/$defs/InlineMath" }, { "$ref": "#/$defs/Link" } ] }, "TextSpan": { "type": "object", "required": ["type", "text"], "additionalProperties": false, "properties": { "type": { "const": "text" }, "text": { "type": "string", "maxLength": 5000 }, "marks": { "type": "array", "maxItems": 5, "uniqueItems": true, "items": { "type": "string", "enum": ["bold", "italic", "code", "strike", "sub", "sup"] } } } }, "InlineMath": { "type": "object", "required": ["type", "latex"], "additionalProperties": false, "properties": { "type": { "const": "math" }, "latex": { "type": "string", "minLength": 1, "maxLength": 1000 }, "alt": { "type": "string", "maxLength": 500 } } }, "Link": { "type": "object", "required": ["type", "href", "content"], "additionalProperties": false, "properties": { "type": { "const": "link" }, "href": { "description": "https URLs or internal app paths only. javascript:/data: URIs are rejected by schema and again by the sanitizer.", "type": "string", "maxLength": 2000, "pattern": "^(https://|/)" }, "content": { "type": "array", "minItems": 1, "maxItems": 50, "items": { "$ref": "#/$defs/TextSpan" } } } } } }