Skip to content

Repository files navigation

questforge

Build License: MIT C++20 Platform: Linux | Windows

A C++20 command-line tool that assembles randomized exams from a YAML question catalog and renders them as PDFs via Typst.

Table of Contents

What it does

Given a catalog of questions (with metadata like topic, difficulty, and points), questforge selects a random subset matching your criteria and produces a print-ready PDF. Multiple test variants with different question selections can be generated from the same catalog using explicit seeds for reproducibility.

Example of a generated test with a school header

Features

  • Randomized selection — pick a configurable number of easy, medium, and hard questions, optionally restricted to specific topics.
  • Reproducible results — an explicit seed (--seed) makes every run repeatable; important for debugging and for handing out multiple variants of the same exam.
  • Provenance tracking — every run logs and embeds the effective seed and a 64-bit FNV-1a fingerprint of the catalog file (file bytes) in the PDF footer, so you can always trace which exact catalog version produced a test.
  • Multiple variants — combine different seeds and filters to generate distinct versions of one test from a single catalog.
  • Print-ready PDFs — clean, school-friendly layout rendered by Typst; math is written in Typst syntax (not LaTeX).
  • School header — optional logo and school name lines from a small YAML config (see below).
  • Solution sheets--solutions renders a matching solution sheet from the same selection in the same run, so questions and solutions can never drift apart.

How it works

questforge is a small pipeline of independent layers, each testable on its own:

CLI (generate command)
   │
   ▼
QuestionRepository — loads and validates the question catalog (YAML)
   │
   ▼
TestGenerator — filters by difficulty/topic and randomly selects questions (seeded)
   │
   ▼
TypstRenderer — fills the Typst template (inja) and invokes `typst compile`

The random selection uses the C++ standard library (<random>) seeded with an explicit per-run value: with the same seed and the same catalog, the same test is generated every time.

Provenance: Each run computes a 64-bit FNV-1a fingerprint over the raw catalog file bytes (not a semantic hash). The fingerprint and the effective seed are logged to stdout and embedded in the PDF footer. This lets you verify later exactly which catalog version produced a given test.

Getting started

  1. Clone the repository:
git clone https://codeberg.org/Gwynspring/questforge.git
  1. Install the prerequisites.

Prerequisites: CMake ≥ 3.25, Ninja, a C++20 compiler, and internet access for the first cmake --preset run. That's it — yaml-cpp, inja, CLI11, spdlog, and GoogleTest are fetched and built from source automatically via CMake's FetchContent, pinned to fixed versions in CMakeLists.txt. There's nothing to install via a package manager for them.

Fedora

sudo dnf install -y cmake ninja-build gcc-c++

Debian / Ubuntu

sudo apt install -y cmake ninja-build g++

Arch Linux

sudo pacman -S --needed cmake ninja gcc

All distros

Typst is an external binary that is not bundled. Install it following the official installation guide.

Build

To build the project run the following commands in the project root:

cmake --preset default
cmake --build build

# Run tests
ctest --test-dir build --output-on-failure

Usage

To generate a test, use the following command:

./build/questforge generate --catalog data/catalog/algebra.yaml --easy 1 --medium 1 --hard 1 --out /tmp/test.pdf

Available options for generate:

Option Required Description
-c, --catalog yes Path to the YAML question catalog.
-o, --out yes Path of the generated PDF (must end in .pdf).
--easy no Number of easy questions to select (default: 0).
--medium no Number of medium questions to select (default: 0).
--hard no Number of hard questions to select (default: 0).
--topics no Comma-separated list of topics to filter by (e.g. algebra,geometry).
-s, --seed no Explicit random seed for reproducible selection.
--template no Path to the Typst template (default: templates/test.typ.jinja).
--config no Path to a school header config (logo + school name, see below).
--date no auto fills in today's date (UTC); any other value is printed as-is; omit to keep a handwritten date line.
--solutions no Additionally renders a solution sheet PDF to this path; requires every selected question to have a solution.

Example with a school header and auto-generated date:

./build/questforge generate --catalog data/catalog/algebra.yaml --easy 1 \
  --config data/school/bulme.yaml --date auto --out /tmp/test.pdf

Every run prints the effective seed and catalog fingerprint to stdout:

[2026-08-25 21:50:39.968] [info] Seed: 550616576, Catalog fingerprint: 687fecef5327a08c

These values are also embedded in the PDF footer for later traceability.

Multiple variants

To create several distinct variants of the same test, run the command once per variant with a different seed — e.g. one variant per class group:

for seed in 1 2 3; do
  ./build/questforge generate --catalog data/catalog/algebra.yaml \
    --easy 2 --medium 2 --hard 1 --seed "$seed" --out "/tmp/test_variant_$seed.pdf"
done

Question catalog format

questions:
  - id: alg-001
    topic: algebra
    difficulty: easy      # easy | medium | hard
    points: 2
    text: "Solve: $2x + 3 = 7$"
    image: null           # optional path relative to catalog
    solution: "$2x = 7 - 3$ → $x = 2$"   # optional, needed for --solutions
    tags: [equations, linear]

Fields per question:

Field Required Description
id yes Unique identifier within the catalog.
topic yes Topic used for --topics filtering.
difficulty yes One of easy, medium, hard.
points yes Points awarded for the question (must be > 0).
text yes Question text; math in Typst syntax.
image no Path to an image, relative to the catalog (default: null).
solution no Solution text (Typst syntax). Required on every selected question when using --solutions.
tags yes List of tags (can be empty, e.g. []).

Math is written in Typst syntax (not LaTeX). If you are not familiar with it, see the official Typst tutorial.

School header config

With --config, the generated PDF gets a school header: an optional logo on the left and the school name lines centered next to it (see data/school/bulme.yaml for an example).

school:
  - Höhere Technische Bundes-Lehranstalt
  - Graz-Gösting (BULME)
  - Abteilung für Elektrotechnik
logo: bulme.png
Field Required Description
school yes List of text lines, rendered centered in the header.
logo no Path to the logo image, resolved relative to the config file.

Without --config, no header is rendered. The date is controlled independently via --date: auto prints today's date (UTC), any other value is printed verbatim, and omitting the flag keeps a blank line for a handwritten date.

Project status

The core pipeline is functional: catalog loading, seeded random selection, school headers, solution sheets, and provenance tracking (seed + catalog fingerprint in log output and PDF footer) all work end-to-end.

The project is in active development. What started as a personal learning project is evolving into a tool meant for real use by teachers. Current focus is on hardening the existing features before adding the next phase (Moodle GIFT import, batch variant generation, validation command).

Contributing

This is a personal project I'm using to learn C++. It's not accepting contributions or pull requests at this time.

AI usage

This project is developed with AI assistance (opencode) used as a learning aid, not as an author of core logic. See AI_USAGE.md for details.

License

MIT — see LICENSE.

Third-party notices

questforge links against yaml-cpp, inja (which vendors nlohmann/json), CLI11, and spdlog (all MIT/BSD-3-Clause) and shells out to the separately-installed Typst CLI. License texts for everything bundled into the compiled binary are in THIRD_PARTY_NOTICES.md.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages