Files
coursebank/docs/guide/setup.md
T
alexm abc0bdf621
Sync README to GitHub / sync (push) Successful in 12s
CI / check (push) Successful in 8m31s
Deploy docs / deploy (push) Successful in 6m44s
Nightly / nightly (push) Successful in 10m33s
Dev (#1)
Reviewed-on: #1
2026-08-07 15:48:11 -04:00

6.5 KiB

Setting up a course

A course is a directory in a git repository. Nothing lives in a database, and the tool holds no state of its own, so a course you set up in 2026 opens in 2031 with whatever version of coursebank you have then.

Make the directory

$ coursebank init --code "BIOSC 1540" --title "Computational Biology" --term 2026s
wrote course.yaml
wrote 4 JSON Schema files

That gives you:

Path Holds Written by
course.yaml identity, grading policy, objectives, lectures you
banks/ items, with design intent and pooled statistics you, then calibrate
assessments/ what was given, to whom, when assemble, then you
data/ one row per student per item ingest
reports/ generated Markdown and HTML report
build/ exports: QTI packages, .typ files, PDFs export
schema/ JSON Schemas for editor validation init, schema

build/ and reports/ are in the generated .gitignore. The other four are the repository's content and belong in review.

Add --with-examples if you want a filled-in bank to read rather than an empty directory to stare at.

Point your editor at the schemas

The schemas are the difference between authoring items and looking up field names. With them wired in, your editor completes cognitive_process from the eleven legal values and underlines a typo in learning_objectives as you type.

Put the modeline at the top of each file:

# yaml-language-server: $schema=../schema/bank.schema.json

coursebank schema reprints the paths and the exact line to paste. Rerun it after upgrading, since the schemas ship with the binary.

Fill in course.yaml

Four registries live here, and everything else references them by id.

Policy

Conventions stated once instead of per assessment:

policy:
  points_per_item: 1.5
  options_per_item: 5
  bonus_levels: [5]
  allow_partial_credit: true
  partial_credit_floor_level: 3
  mastery_threshold: 0.75
  min_items_for_mastery: 3

partial_credit_floor_level: 3 is the one to think about. Below Apply, a defensible wrong answer usually means the item is unclear rather than that the student partly understood something. Setting a floor makes that a rule you decided once, so it stops being an argument you have every term with a student at your desk.

Units and lectures

Units are the coarse grouping. Lectures carry a date and belong to a unit:

units:
  - id: u1
    title: Sequence analysis
    description: Alignment, scoring models, and database search.

lectures:
  l09:
    title: Pairwise alignment
    date: 2026-02-10
    unit: u1
    readings:
      - "Durbin et al., ch. 2"

The dates are what let a student report say which lecture to review, and what lets assemble --lectures l09,l10 draw only from material you have taught.

Learning objectives

The load-bearing registry. An objective's wording lives in exactly one place, so rewording it updates every report that quotes it:

learning_objectives:
  lo-align-algorithm:
    text: Trace the dynamic programming recurrence for a global or local alignment and explain what each term contributes.
    unit: u1
    lectures: [l09]
    level_ceiling: 4
    tags: [algorithms]

  lo-align-scoring:
    text: Predict how changing a substitution matrix or gap penalty changes the resulting alignment.
    unit: u1
    lectures: [l10]
    prerequisites: [lo-align-algorithm]
    level_ceiling: 4

Write the text in the second person and start with a verb, because reports quote it verbatim to students.

Three fields do work later that is easy to miss now. prerequisites is walked backwards by student reports to suggest where to start reviewing, so a student who missed the scoring objective gets pointed at the algorithm first. level_ceiling is the highest level you intend to assess the objective at; placing an item above it is a warning, which means either the item overreaches or the ceiling needs raising, and both are useful to be asked about. assessed: false marks an objective you teach but measure some other way, such as by project rubric, which stops coverage reporting from flagging it as a gap on every run.

Objective ids are join keys. Renaming one orphans every item and every stored response that referenced it, so pick names you can live with.

Stimuli

A shared passage, table, or figure that several items ask about:

stimuli:
  s-dock-poses:
    body: |
      A docking run produces five poses of the same ligand. Scores are in
      kcal/mol; RMSD is measured against the crystallographic pose.

      ```
        Pose   Score   RMSD (Å)   Cluster size
          1    -9.8       6.2            3
          2    -9.4       1.1           28
      ```
    caption: Docking output for a single ligand against one receptor.

Items reference it with stimulus: s-dock-poses. Declaring it here rather than pasting it into four items means a correction to the table happens once.

Check it

$ coursebank validate
course.yaml: ok
banks: 0 files, 0 items
assessments: 0 records

validate enforces what must be true: every reference resolves, ids are unique, keys are present, credit is in range. It reports everything wrong in one pass instead of one problem per run, because fixing one typo per invocation is not a workflow.

lint is separate and advises on what is usually a mistake: an option that gives away the answer by being longer than the others, a stem with no task in it, a level that disagrees with the cognitive process. Every rule has a code you can silence. The split matters because a linter that blocks a commit for a style opinion gets disabled, and then you lose the validator with it.

Exit codes are meaningful. 0 means success, 1 means the command failed, and 2 means validation or linting found something.

Where to go next

authoring writes the first bank. first_exam takes an exam from blueprint to student report.

Reading a course from Rust

The CLI is one caller. Catalog::load reads a whole course directory and indexes every item by global id:

use std::path::Path;

use coursebank::Catalog;

# fn main() -> coursebank::Result<()> {
let catalog = Catalog::load(Path::new("path/to/course"))?;

println!("{} items across {} banks", catalog.entries.len(), catalog.banks.len());

// Global ids are `bank::item`. Bare ids resolve when unambiguous.
let entry = catalog.require("sequence-analysis::q-align-recall-001")?;
println!("{}", entry.item.display_title());
# Ok(())
# }