{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.fablepool.org/v1/problem-content.json", "title": "FablePool Problem Content", "description": "The structured content payload stored in ProblemVersion.content. Covers the statement, the answer specification for all supported answer types, hints and the canonical solution. Metadata (title, tags, difficulty, prerequisites, license) lives in relational columns, not here.", "type": "object", "required": ["schema", "statement", "answer", "solution"], "additionalProperties": false, "properties": { "schema": { "const": "fablepool.problem/1" }, "statement": { "$ref": "https://schemas.fablepool.org/v1/content-document.json" }, "answer": { "$ref": "#/$defs/answer" }, "hints": { "type": "array", "maxItems": 10, "items": { "$ref": "#/$defs/hint" }, "default": [] }, "solution": { "$ref": "#/$defs/richText" }, "wrong_answer_feedback": { "type": "array", "maxItems": 20, "description": "Targeted feedback for common wrong answers (misconception handling).", "items": { "type": "object", "required": ["match", "feedback"], "additionalProperties": false, "properties": { "match": { "type": "string", "maxLength": 500, "description": "Exact wrong answer (numeric/expression/choice id) to match." }, "feedback": { "$ref": "#/$defs/richText" } } } } }, "$defs": { "richText": { "oneOf": [ { "type": "string", "minLength": 1, "maxLength": 10000 }, { "$ref": "https://schemas.fablepool.org/v1/content-document.json" } ] }, "slug": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$" }, "hint": { "type": "object", "required": ["content"], "additionalProperties": false, "properties": { "content": { "$ref": "#/$defs/richText" }, "penalty": { "type": "number", "minimum": 0, "maximum": 1, "default": 0, "description": "Score fraction deducted when this hint is revealed." } } }, "answer": { "oneOf": [ { "$ref": "#/$defs/multipleChoice" }, { "$ref": "#/$defs/numeric" }, { "$ref": "#/$defs/expression" }, { "$ref": "#/$defs/freeText" }, { "$ref": "#/$defs/code" }, { "$ref": "#/$defs/ordering" }, { "$ref": "#/$defs/matching" }, { "$ref": "#/$defs/widget" } ] }, "multipleChoice": { "type": "object", "required": ["type", "choices"], "additionalProperties": false, "properties": { "type": { "const": "multiple_choice" }, "multiple_select": { "type": "boolean", "default": false }, "shuffle": { "type": "boolean", "default": true }, "choices": { "type": "array", "minItems": 2, "maxItems": 12, "contains": { "type": "object", "required": ["correct"], "properties": { "correct": { "const": true } } }, "items": { "type": "object", "required": ["id", "content", "correct"], "additionalProperties": false, "properties": { "id": { "$ref": "#/$defs/slug" }, "content": { "$ref": "#/$defs/richText" }, "correct": { "type": "boolean" }, "feedback": { "$ref": "#/$defs/richText" } } } } } }, "numeric": { "type": "object", "required": ["type", "answer"], "additionalProperties": false, "properties": { "type": { "const": "numeric" }, "answer": { "type": "number" }, "tolerance": { "oneOf": [ { "type": "object", "required": ["mode", "value"], "additionalProperties": false, "properties": { "mode": { "const": "absolute" }, "value": { "type": "number", "minimum": 0 } } }, { "type": "object", "required": ["mode", "value"], "additionalProperties": false, "properties": { "mode": { "const": "relative" }, "value": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 } } }, { "type": "object", "required": ["mode", "min", "max"], "additionalProperties": false, "properties": { "mode": { "const": "range" }, "min": { "type": "number" }, "max": { "type": "number" } } } ] }, "units": { "type": "string", "maxLength": 32, "description": "Expected unit string, e.g. 'm/s^2'; shown next to the input." }, "precision": { "type": "integer", "minimum": 0, "maximum": 15, "description": "Significant decimal places expected from the learner." } } }, "expression": { "type": "object", "required": ["type", "answer"], "additionalProperties": false, "properties": { "type": { "const": "expression" }, "answer": { "type": "string", "minLength": 1, "maxLength": 2000, "description": "Canonical answer in LaTeX or SymPy-compatible syntax." }, "equivalence": { "enum": ["symbolic", "numeric_sampling"], "default": "symbolic" }, "variables": { "type": "array", "maxItems": 16, "items": { "type": "object", "required": ["name"], "additionalProperties": false, "properties": { "name": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,15}$" }, "domain": { "type": "object", "required": ["min", "max"], "additionalProperties": false, "properties": { "min": { "type": "number" }, "max": { "type": "number" } } }, "exclude": { "type": "array", "maxItems": 32, "items": { "type": "number" } } } } }, "trials": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20, "description": "Number of random evaluations for numeric_sampling equivalence." }, "allowed_functions": { "type": "array", "maxItems": 64, "items": { "type": "string", "maxLength": 32 } } } }, "freeText": { "type": "object", "required": ["type", "rubric"], "additionalProperties": false, "description": "Proof-style or open-ended response, graded by rubric (peer, self, or reviewer grading).", "properties": { "type": { "const": "free_text" }, "grading": { "enum": ["peer", "self", "reviewer"], "default": "self" }, "rubric": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "type": "object", "required": ["criterion", "points"], "additionalProperties": false, "properties": { "criterion": { "type": "string", "minLength": 1, "maxLength": 500 }, "points": { "type": "number", "minimum": 0, "maximum": 100 }, "description": { "type": "string", "maxLength": 2000 } } } }, "min_words": { "type": "integer", "minimum": 0, "maximum": 10000 }, "max_words": { "type": "integer", "minimum": 1, "maximum": 50000 }, "exemplar": { "$ref": "#/$defs/richText" } } }, "code": { "type": "object", "required": ["type", "language", "test_cases"], "additionalProperties": false, "description": "Code challenge executed in an isolated sandbox service, never on the application server.", "properties": { "type": { "const": "code" }, "language": { "enum": ["python", "javascript", "typescript", "c", "cpp", "java", "rust", "go", "haskell"] }, "starter_code": { "type": "string", "maxLength": 20000, "default": "" }, "solution_code": { "type": "string", "maxLength": 50000 }, "entry_point": { "type": "string", "maxLength": 128, "description": "Function or class the harness calls, e.g. 'solve'." }, "harness": { "type": "string", "maxLength": 50000, "description": "Optional grader harness run alongside the submission inside the sandbox." }, "test_cases": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "required": ["input", "expected_output"], "additionalProperties": false, "properties": { "name": { "type": "string", "maxLength": 100 }, "input": { "type": "string", "maxLength": 20000 }, "expected_output": { "type": "string", "maxLength": 20000 }, "hidden": { "type": "boolean", "default": false }, "points": { "type": "number", "minimum": 0, "default": 1 } } } }, "limits": { "type": "object", "additionalProperties": false, "properties": { "time_limit_ms": { "type": "integer", "minimum": 100, "maximum": 30000, "default": 2000 }, "memory_limit_mb": { "type": "integer", "minimum": 16, "maximum": 1024, "default": 256 } } } } }, "ordering": { "type": "object", "required": ["type", "items", "correct_order"], "additionalProperties": false, "properties": { "type": { "const": "ordering" }, "items": { "type": "array", "minItems": 2, "maxItems": 20, "items": { "type": "object", "required": ["id", "content"], "additionalProperties": false, "properties": { "id": { "$ref": "#/$defs/slug" }, "content": { "$ref": "#/$defs/richText" } } } }, "correct_order": { "type": "array", "minItems": 2, "maxItems": 20, "uniqueItems": true, "items": { "$ref": "#/$defs/slug" } }, "partial_credit": { "type": "boolean", "default": false } } }, "matching": { "type": "object", "required": ["type", "left", "right", "pairs"], "additionalProperties": false, "properties": { "type": { "const": "matching" }, "left": { "type": "array", "minItems": 2, "maxItems": 20, "items": { "type": "object", "required": ["id", "content"], "additionalProperties": false, "properties": { "id": { "$ref": "#/$defs/slug" }, "content": { "$ref": "#/$defs/richText" } } } }, "right": { "type": "array", "minItems": 2, "maxItems": 20, "items": { "type": "object", "required": ["id", "content"], "additionalProperties": false, "properties": { "id": { "$ref": "#/$defs/slug" }, "content": { "$ref": "#/$defs/richText" } } } }, "pairs": { "type": "array", "minItems": 1, "maxItems": 40, "items": { "type": "object", "required": ["left", "right"], "additionalProperties": false, "properties": { "left": { "$ref": "#/$defs/slug" }, "right": { "$ref": "#/$defs/slug" } } } }, "shuffle": { "type": "boolean", "default": true }, "partial_credit": { "type": "boolean", "default": true } } }, "widget": { "type": "object", "required": ["type", "widget_id", "grading"], "additionalProperties": false, "description": "Interactive widget-based task. The widget reports a result over the sandbox postMessage API; server_replay grading re-validates the reported result server-side.", "properties": { "type": { "const": "widget" }, "widget_id": { "$ref": "#/$defs/slug" }, "widget_version": { "type": "string", "pattern": "^\\d+(\\.\\d+){0,2}$" }, "params": { "type": "object" }, "grading": { "type": "object", "required": ["mode"], "additionalProperties": false, "properties": { "mode": { "enum": ["widget_reported", "server_replay"] }, "max_score": { "type": "number", "minimum": 0, "default": 1 } } } } } } }