{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://schemas.fablepool.org/v1/attempt-answer.schema.json", "title": "FablePool AnswerSpec, Submission, Attempt, AttemptResult", "description": "Grading configuration (AnswerSpec, embedded in ProblemVersion), learner submission payloads, attempt envelopes, and grading results. Fields annotated with x-fablepool-secret: true are stripped from the delivery view served to learners.", "$defs": { "AnswerSpec": { "oneOf": [ { "$ref": "#/$defs/MultipleChoiceSpec" }, { "$ref": "#/$defs/NumericSpec" }, { "$ref": "#/$defs/ExpressionSpec" }, { "$ref": "#/$defs/FreeTextSpec" }, { "$ref": "#/$defs/CodeSpec" }, { "$ref": "#/$defs/OrderingSpec" }, { "$ref": "#/$defs/MatchingSpec" }, { "$ref": "#/$defs/WidgetSpec" } ] }, "MultipleChoiceSpec": { "type": "object", "required": ["type", "choices"], "additionalProperties": false, "properties": { "type": { "const": "multiple_choice" }, "multipleSelect": { "type": "boolean", "default": false }, "shuffleChoices": { "type": "boolean", "default": true }, "partialCredit": { "description": "Only meaningful with multipleSelect: score = (correct selected − incorrect selected) / total correct, clamped to [0,1].", "type": "boolean", "default": false }, "choices": { "type": "array", "minItems": 2, "maxItems": 10, "items": { "type": "object", "required": ["id", "content", "correct"], "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" }, "content": { "$ref": "problem.schema.json#/$defs/InlineContent" }, "correct": { "type": "boolean", "x-fablepool-secret": true }, "feedback": { "description": "Choice-specific explanation shown after grading.", "$ref": "problem.schema.json#/$defs/InlineContent", "x-fablepool-secret": true } } } } } }, "NumericSpec": { "type": "object", "required": ["type", "answer"], "additionalProperties": false, "properties": { "type": { "const": "numeric" }, "answer": { "x-fablepool-secret": true, "oneOf": [ { "type": "object", "required": ["value"], "additionalProperties": false, "properties": { "value": { "type": "number" }, "tolerance": { "type": "object", "required": ["mode", "amount"], "additionalProperties": false, "properties": { "mode": { "type": "string", "enum": ["absolute", "relative"] }, "amount": { "type": "number", "exclusiveMinimum": 0 } } } } }, { "type": "object", "required": ["min", "max"], "additionalProperties": false, "properties": { "min": { "type": "number" }, "max": { "type": "number" } } } ] }, "integerOnly": { "type": "boolean", "default": false }, "unit": { "type": "object", "required": ["expected"], "additionalProperties": false, "properties": { "expected": { "type": "string", "maxLength": 30 }, "required": { "type": "boolean", "default": false }, "accepted": { "description": "Alternate spellings/equivalents accepted as the expected unit, e.g. ['m/s', 'meters per second'].", "type": "array", "maxItems": 10, "items": { "type": "string", "maxLength": 30 } } } }, "inputHint": { "description": "Placeholder/format guidance shown in the answer box, e.g. 'Round to 2 decimal places'.", "type": "string", "maxLength": 200 } } }, "ExpressionSpec": { "type": "object", "required": ["type", "canonical", "equivalence"], "additionalProperties": false, "properties": { "type": { "const": "expression" }, "canonical": { "description": "Reference expression in LaTeX, parsed server-side via SymPy.", "type": "string", "minLength": 1, "maxLength": 2000, "x-fablepool-secret": true }, "equivalence": { "description": "symbolic: SymPy simplify(a−b)==0; numeric_sampling: evaluate both at random points within variable domains; literal: normalized string comparison (for 'express in the form …' tasks).", "type": "string", "enum": ["symbolic", "numeric_sampling", "literal"] }, "variables": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["name"], "additionalProperties": false, "properties": { "name": { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,15}$" }, "domain": { "type": "object", "additionalProperties": false, "properties": { "min": { "type": "number", "default": -10 }, "max": { "type": "number", "default": 10 }, "exclude": { "type": "array", "maxItems": 20, "items": { "type": "number" } }, "integer": { "type": "boolean", "default": false } } } } } }, "samplePoints": { "type": "integer", "minimum": 5, "maximum": 100, "default": 20 }, "tolerance": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.001, "default": 1e-6 }, "allowedFunctions": { "description": "Whitelist of function names the learner's expression may use; absent means the grader's default safe set.", "type": "array", "maxItems": 40, "items": { "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,20}$" } }, "forbidConstantAnswer": { "description": "Reject answers that are constant when variables are expected (anti-guessing).", "type": "boolean", "default": true } } }, "FreeTextSpec": { "description": "Proof-style or explanation tasks. Never auto-graded for correctness; graded by self-assessment against the model solution or by peer/reviewer rubric.", "type": "object", "required": ["type", "gradingMode"], "additionalProperties": false, "properties": { "type": { "const": "free_text" }, "gradingMode": { "type": "string", "enum": ["self_assess", "peer_review", "reviewer"] }, "minWords": { "type": "integer", "minimum": 1, "maximum": 5000 }, "maxWords": { "type": "integer", "minimum": 10, "maximum": 10000 }, "allowMath": { "type": "boolean", "default": true }, "rubric": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["criterion", "points"], "additionalProperties": false, "properties": { "criterion": { "type": "string", "maxLength": 300 }, "points": { "type": "integer", "minimum": 1, "maximum": 20 }, "description": { "type": "string", "maxLength": 1000 } } } }, "selfAssessChecklist": { "description": "Shown alongside the model solution in self_assess mode: 'Did your proof cover the base case?' etc.", "type": "array", "maxItems": 10, "items": { "type": "string", "maxLength": 300 } } } }, "CodeSpec": { "type": "object", "required": ["type", "languages", "testSuites", "limits"], "additionalProperties": false, "properties": { "type": { "const": "code" }, "languages": { "type": "array", "minItems": 1, "maxItems": 6, "uniqueItems": true, "items": { "type": "string", "enum": ["python", "javascript", "c", "cpp", "java", "rust", "go"] } }, "starterCode": { "description": "Map of language → starter source shown in the editor.", "type": "object", "additionalProperties": false, "patternProperties": { "^(python|javascript|c|cpp|java|rust|go)$": { "type": "string", "maxLength": 20000 } } }, "harness": { "description": "How submitted code is invoked: 'stdin_stdout' runs the program with stdin and compares stdout; 'function' calls a named entry function with JSON-decoded args.", "type": "object", "required": ["mode"], "additionalProperties": false, "properties": { "mode": { "type": "string", "enum": ["stdin_stdout", "function"] }, "functionName": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]{0,63}$" } } }, "testSuites": { "type": "array", "minItems": 1, "maxItems": 10, "items": { "type": "object", "required": ["name", "visibility", "cases"], "additionalProperties": false, "properties": { "name": { "type": "string", "maxLength": 100 }, "visibility": { "description": "public cases are shown to learners; hidden cases are secret and stripped from the delivery view.", "type": "string", "enum": ["public", "hidden"] }, "cases": { "type": "array", "minItems": 1, "maxItems": 50, "x-fablepool-secret": "when-hidden", "items": { "type": "object", "required": ["input", "expected"], "additionalProperties": false, "properties": { "name": { "type": "string", "maxLength": 100 }, "input": { "description": "stdin text (stdin_stdout mode) or JSON array of arguments (function mode).", "type": "string", "maxLength": 65536 }, "expected": { "type": "string", "maxLength": 65536 }, "matcher": { "type": "string", "enum": ["exact", "trimmed_lines", "tokens", "json", "float_tolerance"], "default": "trimmed_lines" }, "floatTolerance": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.01 }, "points": { "type": "integer", "minimum": 1, "maximum": 100, "default": 1 }, "timeoutMs": { "type": "integer", "minimum": 100, "maximum": 30000 } } } } } } }, "limits": { "type": "object", "additionalProperties": false, "properties": { "timeMsPerCase": { "type": "integer", "minimum": 100, "maximum": 30000, "default": 2000 }, "memoryMb": { "type": "integer", "minimum": 16, "maximum": 512, "default": 128 }, "outputKb": { "type": "integer", "minimum": 1, "maximum": 1024, "default": 64 }, "sourceKb": { "type": "integer", "minimum": 1, "maximum": 256, "default": 64 } } } } }, "OrderingSpec": { "type": "object", "required": ["type", "items", "correctOrder"], "additionalProperties": false, "properties": { "type": { "const": "ordering" }, "prompt": { "type": "string", "maxLength": 300 }, "items": { "type": "array", "minItems": 2, "maxItems": 12, "items": { "type": "object", "required": ["id", "content"], "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" }, "content": { "$ref": "problem.schema.json#/$defs/InlineContent" } } } }, "correctOrder": { "type": "array", "minItems": 2, "maxItems": 12, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" }, "x-fablepool-secret": true }, "partialCredit": { "description": "none: all-or-nothing; adjacent_pairs: fraction of correctly ordered adjacent pairs; longest_subsequence: |LIS| / n.", "type": "string", "enum": ["none", "adjacent_pairs", "longest_subsequence"], "default": "none" } } }, "MatchingSpec": { "type": "object", "required": ["type", "left", "right", "correctPairs"], "additionalProperties": false, "properties": { "type": { "const": "matching" }, "prompt": { "type": "string", "maxLength": 300 }, "left": { "type": "array", "minItems": 2, "maxItems": 10, "items": { "$ref": "#/$defs/MatchItem" } }, "right": { "description": "May contain more entries than 'left' to act as distractors.", "type": "array", "minItems": 2, "maxItems": 14, "items": { "$ref": "#/$defs/MatchItem" } }, "correctPairs": { "type": "array", "minItems": 2, "maxItems": 14, "x-fablepool-secret": true, "items": { "type": "object", "required": ["left", "right"], "additionalProperties": false, "properties": { "left": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" }, "right": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" } } } }, "allowManyToOne": { "description": "If true, multiple left items may map to the same right item.", "type": "boolean", "default": false }, "partialCredit": { "type": "boolean", "default": true } } }, "MatchItem": { "type": "object", "required": ["id", "content"], "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" }, "content": { "$ref": "problem.schema.json#/$defs/InlineContent" } } }, "WidgetSpec": { "description": "Gradable widget task. The widget reports an opaque state; grading always happens server-side via a validator declared here — widget-reported verdicts are never trusted (see docs/architecture/07-widget-sandboxing.md).", "type": "object", "required": ["type", "widgetName", "widgetVersion", "validator"], "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": { "type": "object" }, "validator": { "x-fablepool-secret": true, "oneOf": [ { "description": "Deep-equality (after key sorting) of submitted widget state against an expected state object.", "type": "object", "required": ["kind", "expectedState"], "additionalProperties": false, "properties": { "kind": { "const": "state_equals" }, "expectedState": { "type": "object" } } }, { "description": "Submitted state must validate against this embedded JSON Schema (e.g. constraints on coordinates, set membership).", "type": "object", "required": ["kind", "stateSchema"], "additionalProperties": false, "properties": { "kind": { "const": "state_schema" }, "stateSchema": { "type": "object" } } }, { "description": "Named server-side validator function from the trusted core (registered in the grading service), with config.", "type": "object", "required": ["kind", "name"], "additionalProperties": false, "properties": { "kind": { "const": "builtin" }, "name": { "type": "string", "pattern": "^[a-z0-9_]{1,64}$" }, "config": { "type": "object" } } } ] } } }, "Submission": { "oneOf": [ { "type": "object", "required": ["type", "selectedChoiceIds"], "additionalProperties": false, "properties": { "type": { "const": "multiple_choice" }, "selectedChoiceIds": { "type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" } } } }, { "type": "object", "required": ["type", "value"], "additionalProperties": false, "properties": { "type": { "const": "numeric" }, "value": { "type": "number" }, "unit": { "type": "string", "maxLength": 30 } } }, { "type": "object", "required": ["type", "latex"], "additionalProperties": false, "properties": { "type": { "const": "expression" }, "latex": { "type": "string", "minLength": 1, "maxLength": 2000 } } }, { "type": "object", "required": ["type", "content"], "additionalProperties": false, "properties": { "type": { "const": "free_text" }, "content": { "$ref": "problem.schema.json#/$defs/RichContent" }, "selfAssessment": { "description": "Learner's checklist results in self_assess mode (index → checked).", "type": "array", "maxItems": 10, "items": { "type": "boolean" } } } }, { "type": "object", "required": ["type", "language", "source"], "additionalProperties": false, "properties": { "type": { "const": "code" }, "language": { "type": "string", "enum": ["python", "javascript", "c", "cpp", "java", "rust", "go"] }, "source": { "type": "string", "minLength": 1, "maxLength": 262144 } } }, { "type": "object", "required": ["type", "order"], "additionalProperties": false, "properties": { "type": { "const": "ordering" }, "order": { "type": "array", "minItems": 2, "maxItems": 12, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" } } } }, { "type": "object", "required": ["type", "pairs"], "additionalProperties": false, "properties": { "type": { "const": "matching" }, "pairs": { "type": "array", "minItems": 1, "maxItems": 14, "items": { "type": "object", "required": ["left", "right"], "additionalProperties": false, "properties": { "left": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" }, "right": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,32}$" } } } } } }, { "type": "object", "required": ["type", "state"], "additionalProperties": false, "properties": { "type": { "const": "widget" }, "state": { "description": "Opaque widget state captured via the postMessage protocol; size-capped server-side at 32 KiB serialized.", "type": "object" } } } ] }, "Attempt": { "description": "Envelope persisted as a ProblemAttempt row and accepted by POST /api/v1/problems/{id}/attempts.", "type": "object", "required": ["problemId", "problemVersionId", "submission"], "additionalProperties": false, "properties": { "attemptId": { "type": "string", "format": "uuid" }, "problemId": { "type": "string", "format": "uuid" }, "problemVersionId": { "type": "string", "format": "uuid" }, "userId": { "type": "string", "format": "uuid" }, "submittedAt": { "type": "string", "format": "date-time" }, "timeSpentSeconds": { "type": "integer", "minimum": 0, "maximum": 86400 }, "hintsUsed": { "type": "array", "maxItems": 8, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,64}$" } }, "clientContext": { "description": "Coarse, privacy-respecting context for analytics; no fingerprinting fields permitted.", "type": "object", "additionalProperties": false, "properties": { "locale": { "type": "string", "maxLength": 20 }, "platform": { "type": "string", "enum": ["web", "pwa", "api"] }, "offlineQueued": { "type": "boolean" } } }, "submission": { "$ref": "#/$defs/Submission" } } }, "AttemptResult": { "type": "object", "required": ["attemptId", "verdict", "gradedBy"], "additionalProperties": false, "properties": { "attemptId": { "type": "string", "format": "uuid" }, "verdict": { "type": "string", "enum": ["correct", "partial", "incorrect", "pending", "error"] }, "score": { "type": "number", "minimum": 0, "maximum": 1 }, "scorePenaltyApplied": { "type": "number", "minimum": 0, "maximum": 0.8 }, "gradedBy": { "type": "string", "enum": ["auto", "self", "peer", "reviewer"] }, "gradedAt": { "type": "string", "format": "date-time" }, "feedback": { "type": "array", "maxItems": 20, "items": { "type": "object", "required": ["message"], "additionalProperties": false, "properties": { "message": { "type": "string", "maxLength": 2000 }, "severity": { "type": "string", "enum": ["info", "hint", "error"], "default": "info" } } } }, "codeResults": { "description": "Per-case outcomes for code submissions. Hidden cases report verdicts but never inputs/expected outputs.", "type": "array", "maxItems": 500, "items": { "type": "object", "required": ["suite", "caseIndex", "verdict"], "additionalProperties": false, "properties": { "suite": { "type": "string", "maxLength": 100 }, "caseIndex": { "type": "integer", "minimum": 0 }, "caseName": { "type": "string", "maxLength": 100 }, "verdict": { "type": "string", "enum": ["passed", "failed", "timeout", "memory_exceeded", "output_limit", "runtime_error", "compile_error", "sandbox_error"] }, "timeMs": { "type": "integer", "minimum": 0 }, "memoryKb": { "type": "integer", "minimum": 0 }, "stderrExcerpt": { "type": "string", "maxLength": 4096 }, "diffExcerpt": { "description": "Only populated for public test cases.", "type": "string", "maxLength": 4096 } } } } } } } }