Files
lnet_tutor/docs/exam-format.md
2025-10-22 20:14:31 +08:00

184 lines
6.3 KiB
Markdown

# Exam Format Specification (Minimal)
## Purpose
A simple, portable exam format. The system reads an exam JSON, renders it online, collects answers, and returns a JSON that bundles the original exam plus the answers (and optionally a basic result summary).
## Top-level Exam Structure
- **examId**: string (unique)
- **subject**: string
- **title**: string
- **difficulty**: one of `beginner | intermediate | advanced`
- **durationMinutes**: integer ≥ 1
- **sections**: array of sections, each containing questions
- **metadata**: optional object (version, createdAt, etc.)
## Question Types (required support)
- Single Choice (one correct answer)
- Multiple Choices (multiple correct answers)
- True/False
- Essay
- Simple Coding
- Coding Exercise
## "I Don't Know" Option
For `single_choice`, `multiple_choices`, and `true_false` questions, an "I don't know" option is automatically available. This allows honest assessment without guessing.
## Section Structure
- **id**: string
- **title**: string
- **questions**: array of questions (of any supported type)
## Common Question Fields
- **id**: string
- **type**: `single_choice | multiple_choices | true_false | essay | code_simple | code_exercise`
- **prompt**: string (supports simple Markdown)
- **points**: integer ≥ 0
- **allowIDK**: boolean (optional, default true for single_choice, multiple_choices, true_false)
## Type-specific Fields
- **single_choice** (one correct answer)
- `choices`: array of `{ key: string, text: string }`
- `answer`: string (the correct `key`)
- `allowIDK`: boolean (default true) - adds "I don't know" option
- **multiple_choices** (multiple correct answers)
- `choices`: array of `{ key: string, text: string }`
- `answer`: array of strings (all correct `key`s, e.g., ["A", "C"])
- `allowIDK`: boolean (default true) - adds "I don't know" option
- `partialCredit`: boolean (default true) - award partial points for some correct
- **true_false**
- `answer`: boolean
- `allowIDK`: boolean (default true) - adds "I don't know" option (scores as wrong)
- **essay**
- `rubric`: `{ criteria: [{ name: string, weight: number }], maxPoints: integer }`
- Notes: `answer` omitted; scored manually or later
- **code_simple**
- `language`: `python | typescript | javascript`
- `tests`: array of `{ input: string, expected: string, visibility?: public|hidden }`
- **code_exercise**
- `language`: `python | typescript | javascript`
- `tests`: array of `{ input: string, expected: string, visibility?: public|hidden, weight?: number }`
- `rubric`: `{ criteria: [{ name: string, weight: number }], maxPoints: integer }`
- `constraints?`: string (optional)
- `starterCode?`: string (optional)
## Minimal Valid Exam JSON (example)
```json
{
"examId": "sample-exam-v1",
"subject": "python",
"title": "Sample Exam",
"difficulty": "beginner",
"durationMinutes": 60,
"sections": [
{
"id": "sec-1",
"title": "Single Choice",
"questions": [
{
"id": "q1",
"type": "single_choice",
"prompt": "Which is a valid list literal?",
"choices": [
{ "key": "A", "text": "(1, 2, 3)" },
{ "key": "B", "text": "{1, 2, 3}" },
{ "key": "C", "text": "[1, 2, 3]" }
],
"answer": "C",
"points": 2
}
]
},
{
"id": "sec-2",
"title": "True / False",
"questions": [
{ "id": "q2", "type": "true_false", "prompt": "Tuples are immutable.", "answer": true, "points": 2 }
]
},
{
"id": "sec-3",
"title": "Essay",
"questions": [
{
"id": "q3",
"type": "essay",
"prompt": "Explain decorators and a common use case.",
"rubric": { "criteria": [{ "name": "Correctness", "weight": 0.6 }, { "name": "Clarity", "weight": 0.4 }], "maxPoints": 8 },
"points": 8
}
]
},
{
"id": "sec-4",
"title": "Simple Coding",
"questions": [
{
"id": "q4",
"type": "code_simple",
"language": "python",
"prompt": "Implement squares(n) returning list of squares 0..n.",
"tests": [ { "input": "squares(3)", "expected": "[0, 1, 4, 9]", "visibility": "hidden" } ],
"points": 10
}
]
},
{
"id": "sec-5",
"title": "Coding Exercise",
"questions": [
{
"id": "q5",
"type": "code_exercise",
"language": "python",
"prompt": "Implement paginate(items, page, per_page). Return items, page, per_page, total, total_pages.",
"constraints": "O(n) acceptable; validate inputs (page>=1, per_page>=1).",
"tests": [
{ "input": "paginate([1,2,3,4,5], 2, 2)", "expected": "{items:[3,4],page:2,per_page:2,total:5,total_pages:3}", "visibility": "hidden", "weight": 2 },
{ "input": "paginate([], 1, 10)", "expected": "{items:[],page:1,per_page:10,total:0,total_pages:0}", "visibility": "hidden" }
],
"rubric": { "criteria": [{ "name": "Correctness", "weight": 0.6 }, { "name": "Structure", "weight": 0.2 }, { "name": "EdgeCases", "weight": 0.2 }], "maxPoints": 20 },
"points": 20
}
]
}
]
}
```
## Output Shape (what the system returns)
- Echoes the input exam JSON as `exam`
- Captured answers as `attempt`
```json
{
"exam": { "...": "(same as input)" },
"attempt": {
"attemptId": "attempt-001",
"startedAt": "2025-10-20T10:00:00Z",
"submittedAt": "2025-10-20T10:45:00Z",
"answers": [
{ "questionId": "q1", "response": "C", "timeSec": 25 },
{ "questionId": "q2", "response": true, "timeSec": 10 },
{ "questionId": "q3", "response": "Decorators wrap functions to add behavior...", "timeSec": 180 },
{ "questionId": "q4", "response": { "code": "def squares(n): ..." }, "timeSec": 420 },
{ "questionId": "q5", "response": { "code": "def paginate(items, page, per_page): ..." }, "timeSec": 900 }
]
}
}
```
## Validation Checklist
- Required top-level fields present
- Each section and question has a unique `id`
- `points` ≥ 0
- Type-specific fields present according to question type
## Versioning
- Use semantic versioning in `metadata.version` for exam files
- New optional fields are allowed without breaking existing behavior