# Question-bank YAML schema (v1) The file contains one UTF-8 YAML document whose top level is a mapping. Bank and question field names must be strings. All top-level fields below are required except `default_shuffle_choices`. Readers new to YAML should begin with the [complete authoring and build walkthrough](getting-started.md). | Field | Meaning | |---|---| | `schema_version` | Integer `1`; strings, floating-point values, and booleans are rejected | | `course` | Nonempty course identifier text, for example `COURSE123` | | `bank_id` | Stable identifier matching `[A-Za-z][A-Za-z0-9_.-]*` | | `title` | Nonempty Canvas assessment display title | | `default_shuffle_choices` | Optional boolean controlling QTI choice shuffling for questions without an override; default `false` | | `questions` | Nonempty ordered list of question mappings | Each question is a mapping with the fields below. All fields are required except `format` (default `plain`), `assessments` (default unrestricted), and `shuffle_choices` (default inherited from the bank). | Field | Valid values | |---|---| | `id` | Unique stable identifier matching `[A-Za-z][A-Za-z0-9_.-]*`, such as `M1C01` | | `title` | Nonempty short instructor title | | `area` | Nonempty broad coverage area for rotation | | `topic` | Nonempty specific topic | | `difficulty` | `Basic`, `Moderate`, or `Challenging` | | `assessments` | Optional list of unique, nonempty assessment names; omitted or `[]` means eligible for any assessment | | `minutes` | Finite positive number of estimated minutes; booleans and quoted numbers are rejected | | `points` | Finite positive number of grading points; booleans and quoted numbers are rejected | | `format` | `plain` (default) or `html` | | `shuffle_choices` | Optional boolean overriding `default_shuffle_choices` for this question | | `stem` | Nonempty complete question text (Unicode text or authored HTML) | | `choices` | Ordered mapping of at least two choice keys to nonempty, distinct text options; `A`, `B`, `C`, `D` are conventional labels | | `answer` | String matching exactly one choice key | | `explanation` | Nonempty instructor explanation and Canvas feedback | ## Validation rules Identifiers and choice keys must start with an ASCII letter, followed only by ASCII letters, digits, underscores, hyphens, or periods. Choice keys must be strings; quote labels such as `yes` or `no` that YAML may otherwise interpret as booleans. Difficulty values such as `Basic` are case-sensitive. Assessment names are user-defined and matched exactly, including case and whitespace; quote numeric names such as `"1"`. Use `assessments: ["Homework 1", "Quiz 2"]` to restrict eligibility. Text fields cannot be empty or contain only whitespace. Question IDs must be unique within the bank. Question stems must also be unique after converting to lowercase and collapsing whitespace. Choice text must be unique within each question after removing leading and trailing whitespace; this comparison is case-sensitive. These comparisons detect duplicates without changing the stored text. Points and minutes accept integers and decimal numbers greater than zero. Zero, negative values, booleans, NaN (`.nan`), and infinity (`.inf` or `-.inf`) are invalid. Shuffle settings must be YAML booleans written as `true` or `false`, without quotation marks. During QTI export, `shuffle_choices` takes precedence over `default_shuffle_choices`; if both are omitted, choices are not shuffled. The exporter writes the effective setting as `shuffle="Yes"` or `shuffle="No"` on `render_choice`. Duplicate YAML mapping keys are rejected at every level, including optional metadata, instead of silently keeping the last value. YAML merge keys (`<<`) are unsupported; write fields explicitly so overrides cannot hide duplicate definitions. Loading accepts only safe YAML data and rejects custom object constructors and multiple documents in one file. Recursive aliases are rejected; nonrecursive aliases retain their values as independent copies. Optional metadata may contain dates, binary values, sets, and nested mappings with scalar keys. Additional optional YAML metadata can be kept for authoring (for example `outcome`, `tags`, or `last_reviewed`). Loading and saving preserve these values, but schema validation ignores their meaning and QTI export does not include them. The recognized optional fields `assessments`, `format`, `default_shuffle_choices`, and `shuffle_choices` control documented toolkit behavior. Validation does not insert defaults or modify the bank and does not verify HTML rendering or Canvas compatibility. After installing the tool, check a bank with: ```bash coursebanks validate examples/sample_course/questions.yaml ``` A valid bank produces a `Valid:` summary and exit code 0. Invalid content produces an `ERROR:` diagnostic and exit code 2. Question diagnostics identify the question position, supplied ID, and invalid field; YAML syntax and duplicate-key errors include file and line information. ## Sample item ```yaml - id: M1C01 title: Continuum length scale area: Continuum / fluids and stress topic: Continuum assumptions difficulty: Basic assessments: [] minutes: 1.5 points: 2 format: plain stem: If the characteristic length increases tenfold at fixed mean free path, how does Kn change? choices: A: It decreases tenfold. B: It increases tenfold. C: It does not change. D: It becomes the Reynolds number. answer: A explanation: Kn is the ratio of mean free path to characteristic length. ``` For a complete bank containing the top-level fields, start from the [sample bank](https://codeberg.org/ddmarshall/QuestionBankToolkit/src/branch/main/examples/sample_course/questions.yaml). **Avoid custom LaTeX macros in question stems.** For Canvas, start with clear Unicode mathematics. Test any HTML math formatting in Canvas before release. No general LaTeX-to-Canvas equation converter is included. ## Migrating existing banks Schema version remains `1` during active development. Loading a validated bank converts legacy `exam: Midterm` to `assessments: [Midterm]`, `exam: Final` to `assessments: [Final]`, and `exam: Either` to `assessments: []`. Other nonempty legacy names become single-element lists. Do not supply both fields on the same question. The special meaning of `Either` applies only to the legacy field; `assessments: [Either]` refers to an assessment literally named Either. Validation and loading leave the source file unchanged. QTI export stores eligibility as a JSON array in the `assessments` metadata field; import restores it. QTI import reads the optional `render_choice` shuffle attribute, treats an omitted attribute as `No`, and stores the effective result in each question's `shuffle_choices` field. Import also accepts legacy `suggested_exam` metadata, using unrestricted eligibility if neither metadata field exists. An optional import metadata CSV keyed by `ID` can override eligibility using an `Assessments` column containing a JSON array or the legacy `Suggested exam` column. Precedence is CSV `Assessments`, CSV `Suggested exam`, QTI `assessments`, then QTI `suggested_exam`; empty CSV cells supply no override. Use `[]` to explicitly override restrictions with unrestricted eligibility. The generated question catalog also represents `assessments` as a JSON array. See [the QTI import guide](importing-qti.md) for the complete preservation, normalization, defaulting, and CSV precedence contract.