Knowledge Base Course Generator

This course generator is the most generic of them and can be adopted to create courses for any subject or topic. The idea is to reduce the need to write manifests for the lessons and exercises by giving special meaning to the names of their files and directories. The documentation of the code contains the specification of the file structure that is used to generate the manifests, so it's reproduced below. Note that Vec is a list of values, Ustr is a unique string type that is treated just like a string, a BTreeMap is used to store the metadata as keys and their associated list of values, and Option is used to represent files that are not required.

Here's the specification for an exercise:

#![allow(unused)]
fn main() {
/// Represents a knowledge base exercise.
///
/// Inside a knowledge base lesson directory, Trane will look for matching pairs of files with names
/// `<SHORT_EXERCISE_ID>.front.md` and `<SHORT_EXERCISE_ID>.back.md`. The short ID is used to
/// generate the final exercise ID, by combining it with the lesson ID. For example, files
/// `e.front.md` and `e.back.md` in a course with ID `a::b::c` inside a lesson directory named
/// `d.lesson` will generate and exercise with ID `a::b::c::d::e`.
///
/// Each the optional fields mirror one of the fields in the
/// [ExerciseManifest](crate::data::ExerciseManifest) and their values can be set by writing a JSON
/// file inside the lesson directory with the name `<SHORT_EXERCISE_ID>.<PROPERTY_NAME>.json`. This
/// file should contain a JSON serialization of the desired value. For example, to set the
/// exercise's name for an exercise with a short ID value of `ex1`, one would write a file named
/// `ex1.name.json` containing a JSON string with the desired name.
///
/// Trane will ignore any markdown files that do not match the exercise name pattern or that do not
/// have a matching pair of front and back files.
pub struct KnowledgeBaseExercise {
    /// The short ID of the lesson, which is used to easily identify the exercise and to generate
    /// the final exercise ID.
    pub short_id: String,

    /// The short ID of the lesson to which this exercise belongs.
    pub short_lesson_id: Ustr,

    /// The ID of the course to which this lesson belongs.
    pub course_id: Ustr,

    /// The path to the file containing the front of the flashcard.
    pub front_file: String,

    /// The path to the file containing the back of the flashcard. This path is optional, because a
    /// flashcard is not required to provide an answer.
    pub back_file: Option<String>,

    /// The name of the exercise to be presented to the user.
    pub name: Option<String>,

    /// An optional description of the exercise.
    pub description: Option<String>,

    /// The type of knowledge the exercise tests. Currently, Trane does not make any distinction
    /// between the types of exercises, but that will likely change in the future. The option to set
    /// the type is provided, but most users should not need to use it.
    pub exercise_type: Option<ExerciseType>,
}
}

And here's the specification for a lesson:

#![allow(unused)]
fn main() {
/// Represents a knowledge base lesson.
///
/// In a knowledge base course, lessons are generated by searching for all directories with a name
/// in the format `<SHORT_LESSON_ID>.lesson`. In this case, the short ID is not the entire lesson ID
/// one would use in the lesson manifest, but rather a short identifier that is combined with the
/// course ID to generate the final lesson ID. For example, a course with ID `a::b::c` which
/// contains a directory of name `d.lesson` will generate the manifest for a lesson with ID
/// `a::b::c::d`.
///
/// All the optional fields mirror one of the fields in the
/// [LessonManifest](crate::data::LessonManifest) and their values can be set by writing a JSON file
/// inside the lesson directory with the name `lesson.<PROPERTY_NAME>.json`. This file should
/// contain a JSON serialization of the desired value. For example, to set the lesson's dependencies
/// one would write a file named `lesson.dependencies.json` containing a JSON array of strings, each
/// of them the ID of a dependency.
///
/// The material and instructions of the lesson do not follow this convention. Instead, the files
/// `lesson.instructoins.md` and `lesson.material.md` contain the instructions and material of the
/// lesson.
///
/// None of the `<SHORT_LESSON_ID>.lesson` directories should contain a `lesson_manifest.json` file,
/// as that file would indicate to Trane that this is a regular lesson and not a generated lesson.
pub struct KnowledgeBaseLesson {
    /// The short ID of the lesson, which is used to easily identify the lesson and to generate the
    /// final lesson ID.
    pub short_id: Ustr,

    /// The ID of the course to which this lesson belongs.
    pub course_id: Ustr,

    /// The IDs of all dependencies of this lesson. The values can be full lesson IDs or the short
    /// ID of one of the other lessons in the course. If Trane finds a dependency with a short ID,
    /// it will automatically generate the full lesson ID. Not setting this value will indicate that
    /// the lesson has no dependencies.
    pub dependencies: Vec<Ustr>,

    /// The IDs of all courses or lessons that this lesson supersedes. Like the dependencies, the
    /// values can be full lesson IDs or the short ID of one of the other lessons in the course. The
    /// same resolution rules apply.
    pub superseded: Vec<Ustr>,

    /// The name of the lesson to be presented to the user.
    pub name: Option<String>,

    /// An optional description of the lesson.
    pub description: Option<String>,

    //// A mapping of String keys to a list of String values used to store arbitrary metadata about
    ///the lesson. This value is set to a `BTreeMap` to ensure that the keys are sorted in a
    ///consistent order when serialized. This is an implementation detail and does not affect how
    ///the value should be written to a file. A JSON map of strings to list of strings works.
    pub metadata: Option<BTreeMap<String, Vec<String>>>,

    /// Indicates whether the `lesson.instructions.md` file is present in the lesson directory.
    pub has_instructions: bool,

    /// Indicates whether the `lesson.material.md` file is present in the lesson directory.
    pub has_material: bool,
}
}

If we translate this specification into an example course with two lessons of two exercises each, the file structure is as follows:

course_root/
├── course_manifest.json
├── lesson_1.lesson/
|   ├── lesson.name.json
|   ├── lesson.description.json
|   ├── lesson.dependencies.json
|   ├── lesson.metadata.json
|   ├── lesson.instructions.md
|   ├── lesson.material.md
│   ├── exercise_1.front.md
│   ├── exercise_1.back.md
│   ├── exercise_1.name.json
│   ├── exercise_1.description.json
│   ├── exercise_1.type.json
│   ├── exercise_2.front.md
│   └── exercise_2.back.md
│   ├── exercise_2.name.json
│   ├── exercise_2.description.json
│   ├── exercise_2.type.json
└── lesson_2.lesson/
    ├── exercise_3.front.md
    ├── exercise_3.back.md
    ├── exercise_4.front.md

The first lesson includes all the files that can be written to specify the properties of a lesson, the front, and back of exercises, and the properties of each exercise. The second lesson contains the minimum number of files required to specify the lesson and exercises. Not even the dependencies of a lesson are required. If they are missing, the lesson will be assumed to have no dependencies. The rest of the properties are given a sensible default. Also note that an exercise is not required to have a back file, such as exercise_4 in the example above. This can happen, for example, if the exercise is open-ended or refers to an external resource.

Most of the time, a course author will only need to specify the front and back of the exercises and the dependencies for each lesson. The flashcards already needed to be written anyway, and the dependencies are much easier to write than the entire manifest. Following this strategy, most of the extra effort to writing the course materials themselves that was previously required to complete the course is eliminated.

Example Configuration

Example of a course manifest for a knowledge base course:

{
  "id": "trane::example::knowledge_base",
  "name": "Example Knowledge Base Course",
  "dependencies": [],
  "description": "An example knowledge base course.",
  "authors": [
    "The Trane Project"
  ],
  "metadata": null,
  "course_material": null,
  "course_instructions": null,
  "generator_config": {
    "KnowledgeBase": {}
  }
}

Example Courses

Below there are a few examples of official Trane courses that are using this course generator to give you a better idea of how it is used:

  • Tenor saxophone long tones: A course to teach you how to produce long tones on the tenor saxophone, starting at the first octave and gradually working your way up.
  • Art of Problem Solving 1: The "Art of Problem Solving" book series is intended to begin preparing students for competing in math Olympiads. This course translates the first volume of that series into a Trane course.

Simple Course Builder

For simple courses that mostly have flashcards with a front and back, it's possible to declare the entire course in a single file with the help of the SimpleKnowledgeBaseCourse object. Simply write a JSON file that is the serialization of that type and call:

trane-simple-build <PATH_TO_FILE> <OUTPUT_DIR>

The trane-simple-build utility is found in the trane-cli repository. You can install it by cloning that repository and running the following command at the root of the repository:

cargo install --path .

Note that this assumes that you have installed the Rust toolchain. If you haven't, you can install it by following the instructions in the Rust website.

Below is the specification of the SimpleKnowledgeBaseCourse type:

#![allow(unused)]
fn main() {
/// Represents a simple knowledge base course which only specifies the course manifest and a list of
/// simple lessons. It is meant to help course authors write simple knowledge base courses by
/// writing a simple configuration to a single JSON file.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimpleKnowledgeBaseCourse {
    /// The manifest for this course.
    pub manifest: CourseManifest,

    /// The simple lessons in this course.
    #[serde(default)]
    pub lessons: Vec<SimpleKnowledgeBaseLesson>,
}
}

and the specification of the SimpleKnowledgeBaseLesson type referenced by the course type:

#![allow(unused)]
fn main() {
/// Represents a simple knowledge base lesson which only specifies the short ID of the lesson, the
/// dependencies of the lesson, and a list of simple exercises. The instructions, material, and
/// metadata can be optionally specified as well. In a lot of cases, this is enough to deliver the
/// full functionality of Trane. It is meant to help course authors write simple knowledge base
/// courses by writing a simple configuration to a single JSON file.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimpleKnowledgeBaseLesson {
    /// The short ID of the lesson.
    pub short_id: Ustr,

    /// The dependencies of the lesson.
    #[serde(default)]
    pub dependencies: Vec<Ustr>,

    /// The courses or lessons that this lesson supersedes.
    #[serde(default)]
    pub superseded: Vec<Ustr>,

    /// The simple exercises in the lesson.
    #[serde(default)]
    pub exercises: Vec<SimpleKnowledgeBaseExercise>,

    /// The optional metadata for the lesson.
    #[serde(default)]
    pub metadata: Option<BTreeMap<String, Vec<String>>>,

    /// A list of additional files to write in the lesson directory.
    #[serde(default)]
    pub additional_files: Vec<AssetBuilder>,
}
}

Examples

The specification above might not be very clear, specially if you don't know Rust, so here are a few examples of existing courses that use this builder:

  • trane-n-back: A course that trains your working memory by playing the n-back game.
  • Tenor saxophone long tones: This course was mentioned above and while it's a valid knowledge base course, it's generated from this file.

Usually when generating those courses, the following commands are used:

cd <DIRECTORY_OF_COURSE_CONFIG_FILE>
trane-simple-build course_config.json .

Running the command generates all the lesson directories, lesson metadata files, and exercise files. The changes are then committed to the repository.