A curriculum engine that turns a YAML curriculum definition into a deployable SvelteKit learning application.
Project description
learningfoundry
A curriculum engine that turns a YAML curriculum definition into a deployable SvelteKit learning application — with interactive assessments, executable notebooks, and data visualizations — in a single pipeline.
Table of Contents
- Overview
- Installation
- Quick Start
- CLI Reference
- Curriculum YAML Format
- Video blocks
- Lesson titles and markdown headings
- Images and assets
- Content locking
- Configuration File
- Development Setup
Overview
learningfoundry takes a single curriculum.yml file and generates a fully self-contained SvelteKit learning application. The generated app supports:
- Text — Markdown content rendered in the browser
- Video — YouTube embeds
- Quiz — Interactive assessments via quizazz (optional)
- Exercise — Executable notebooks via nbfoundry (stub provided)
- Visualization — D3-based charts via d3foundry (stub provided)
Learner progress is persisted locally in SQLite (via sql.js) — no backend required.
Installation
pip install learningfoundry
With optional quizazz support:
pip install "learningfoundry[quizazz]"
Requirements:
- Python 3.12+
- pnpm (for
previewcommand and generated app development) - Node.js 18+ (for the generated SvelteKit app)
Quick Start
-
Create a curriculum file (see Curriculum YAML Format):
cat > curriculum.yml << 'EOF' version: "1.0.0" curriculum: title: "My Course" description: "A short description." modules: - id: mod-01 title: "Module One" lessons: - id: lesson-01 title: "Getting Started" content_blocks: - type: text ref: content/lesson-01.md - type: video url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" EOF
-
Validate the curriculum:
learningfoundry validate # OK — curriculum is valid.
-
Build and preview locally:
learningfoundry preview # Preview server started at http://localhost:5173
learningfoundry previewis the canonical "see your work" command — it builds the SvelteKit project, installs Node dependencies on first run (and again whenever they change), and starts a Vite dev server. On subsequent runs it skips the install step automatically.learningfoundry buildalone is also available if you want to generate the SvelteKit project without serving it (e.g. to inspect output, deploy a static export viacd dist && pnpm build, or wire into your own toolchain).
CLI Reference
learningfoundry build
Parse → resolve → generate a SvelteKit project.
Usage: learningfoundry build [OPTIONS]
Options:
-c, --config PATH Path to the curriculum YAML file. [default: curriculum.yml]
--log-level LEVEL Logging verbosity. [default: INFO]
Choices: DEBUG, INFO, WARNING, ERROR
-o, --output PATH Output directory for the generated SvelteKit project.
[default: dist]
--base-dir PATH Base directory for content refs.
(default: curriculum file's parent directory)
--help Show this message and exit.
Exit codes:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Curriculum validation error |
| 2 | Content resolution error (missing file, bad URL, etc.) |
| 3 | SvelteKit generation error |
| 4 | Configuration file error |
learningfoundry validate
Validate a curriculum YAML without generating any output.
Usage: learningfoundry validate [OPTIONS]
Options:
-c, --config PATH Path to the curriculum YAML file. [default: curriculum.yml]
--log-level LEVEL Logging verbosity. [default: INFO]
--base-dir PATH Base directory for resolving content refs.
--help Show this message and exit.
Prints OK — curriculum is valid. on success, or a list of errors and exits with code 1.
learningfoundry preview
Build then launch a local Vite dev server.
Usage: learningfoundry preview [OPTIONS]
Options:
-c, --config PATH Path to the curriculum YAML file. [default: curriculum.yml]
--log-level LEVEL Logging verbosity. [default: INFO]
-o, --output PATH Output directory for the generated SvelteKit project.
[default: dist]
--base-dir PATH Base directory for content refs.
--port INTEGER Port for the local dev server. [default: 5173]
--help Show this message and exit.
Runs learningfoundry build, then pnpm install (skipped when every declared dependency is already present in node_modules/), then pnpm run dev in the generated project directory. Requires pnpm on PATH.
This serves the SvelteKit project from source via Vite's dev server; it does not serve the static pnpm build output in dist/build/. For static deploys, use cd dist && pnpm build and host the resulting dist/build/ directory on any static host.
Curriculum YAML Format
version: "1.0.0"
curriculum:
title: "Course Title" # required
description: "Course overview." # optional
modules:
- id: mod-01 # required, kebab-case
title: "Module One" # required
description: "..." # optional
# Optional pre/post assessments (requires quizazz-builder)
pre_assessment:
source: quizazz
ref: assessments/mod-01-pre.yml
post_assessment:
source: quizazz
ref: assessments/mod-01-post.yml
lessons:
- id: lesson-01 # required, kebab-case; unique within module
title: "Lesson One" # required
content_blocks:
# Text block — Markdown file
- type: text
ref: content/mod-01/lesson-01.md
# Video block — `provider` selects the player (default: youtube)
- type: video
url: "https://www.youtube.com/watch?v=XXXXXXXXXXX"
# provider: youtube # optional today; only youtube is implemented
# extensions: {} # optional; player-specific payload (see "Video blocks")
# Quiz block — requires learningfoundry[quizazz]
- type: quiz
source: quizazz
ref: assessments/mod-01-quiz.yml
# Exercise block — requires nbfoundry (stub included)
- type: exercise
source: nbfoundry
ref: exercises/mod-01-exercise.yml
# Visualization block — requires d3foundry (stub included)
- type: visualization
source: d3foundry
ref: visualizations/mod-01-vis.yml
Rules:
- Module and lesson
idvalues must be unique within their scope, and match the pattern[a-z0-9][a-z0-9-]*. - Every curriculum must have at least one module; every module at least one lesson.
- All
refpaths are resolved relative to--base-dir(default: directory containing the curriculum YAML). - Only YouTube URLs are accepted for
videoblocks whenproviderisyoutube(the default):youtube.com/watch?v=oryoutu.be/.
Video blocks
Each video content block carries:
url— Watch URL for the provider (validated for YouTube whenprovider: youtube).provider— Which player to use. Omitted in YAML meansyoutube. New providers (e.g. Vimeo) will add new literal values here together with resolver + frontend support.extensions— Optional mapping of player-specific data. There is no cross-player generic schema: keys and shapes are defined per provider. Examples you might add later for YouTube:chapters(timestamp + title list),transcript_ref(path to WebVTT or plain text),autoplay. The build passesextensionsthrough tocurriculum.jsonunchanged; the Svelte app can grow per-provider components that readcontent.extensions.
Older generated apps only had url in each video block’s content; the template still treats missing provider as youtube.
Lesson titles and markdown headings
Each lesson page renders two title strings, from two different sources:
- The lesson title from
curriculum.yml(thetitle:on a lesson). Used by the sidebar, the breadcrumb, the browser tab, and the page's outer<h1>. - The leading heading in the lesson's markdown file (the
# Headingat the top, if any). Rendered inside the lesson body.
If both strings are identical, the page renders the same title twice and looks broken. The fix is purely an authoring convention — there is no rendering bug to chase.
Convention
- Keep the YAML
title:short and navigation-shaped. Either a number ("3"), a label ("Lesson 3"), or label-plus-abbreviation ("Lesson 3: Cultural Diffusion"). - Make the markdown
# Headingthe descriptive long-form title that complements the YAML title — never echoes it. Imagine reading them together as"<yaml title>: <markdown H1>"; that sentence should flow naturally and contain no repeated words. - If the lesson genuinely has nothing extra to add in a heading, omit the markdown
# Headingentirely and start the lesson with body prose. The page already has the YAML title rendered as its<h1>.
Examples
Good — complementary, reads as one sentence:
# curriculum.yml
- id: lesson-03
title: "Lesson 3"
<!-- content/mod-01/lesson-03.md -->
# The Diffusion of Cultural Artifacts
Most cultural products fail. A reasonable estimate places the fraction…
Renders as:
Lesson 3
The Diffusion of Cultural Artifacts
Most cultural products fail. …
Good — slightly more YAML detail, still no echo:
- id: lesson-03
title: "Lesson 3: Cultural Diffusion"
# Why Most Pop Releases Disappear
Bad — duplicative; both titles render the same string:
- id: lesson-03
title: "The Diffusion of Cultural Artifacts"
# The Diffusion of Cultural Artifacts
Also fine — no markdown heading at all:
- id: lesson-03
title: "Lesson 3: Cultural Diffusion"
Most cultural products fail. A reasonable estimate places the fraction…
Images and assets
Lesson markdown can embed images directly. Place the image file alongside the markdown that uses it and reference it with a relative path:
content/
└── mod-01/
├── lesson-01.md
├── diagram.png
└── figures/
└── architecture.svg
# Lesson One

Here is a smaller inline diagram:
<img src="diagram.png" alt="Diagram" />
How it works:
- Relative URLs (
diagram.png,figures/architecture.svg) are resolved against the markdown file's own directory.learningfoundry buildcopies each unique image intodist/static/content/<sha256[:12]>/<basename>and rewrites the markdown URL to the absolute path/content/<sha256[:12]>/<basename>so it resolves at every nested route in the generated app. - Both the markdown form (
,) and the HTML form (<img src="path">) are recognised. - Absolute URLs (
https://,http://, protocol-relative//..., root-absolute/...) anddata:URIs pass through unchanged — useful for CDN-hosted assets you don't want copied into the build. - Image references inside fenced code blocks (
```or~~~) are left as literal text, so code samples that demonstrate image syntax aren't silently rewritten. - The same image referenced from N lessons is copied exactly once (deduped by content hash).
- A missing image fails the build with the lesson location and the expected on-disk path in the error message.
For production deployment to a CDN, just run cd dist && pnpm build — the static/content/ tree gets bundled into the static export under build/content/, so deploying build/ to any static host (Cloudflare Pages, Netlify, S3+CloudFront, …) serves the images at the same URLs the markdown references.
Content locking
Control access to modules and lessons with a three-level configuration hierarchy (most local wins):
- Per-module
locked— explicittrue/falseoverride; trumps everything. - Curriculum
locking.sequential— when true, module N+1 requires module N complete. - Global config
locking.sequential— project-wide default (see Configuration File below).
curriculum:
locking:
sequential: true # modules must be completed in order
lesson_sequential: false # lessons within a module are free-order
modules:
- id: mod-01
locked: false # always accessible regardless of sequential
lessons:
- id: lesson-01
unlock_module_on_complete: true # completing this unlocks siblings + next module
content_blocks:
- type: quiz
source: quizazz
ref: assessments/quiz.yml
pass_threshold: 0.7 # 70% required to count as passed
unlock_module_on_complete is useful for "gateway" lessons — a single assessment that, once passed, opens the rest of the module and the next one.
Configuration File
An optional config file can set defaults for logging and locking. The CLI always takes precedence.
Default location: ~/.config/learningfoundry/config.yml
logging:
level: INFO # DEBUG | INFO | WARNING | ERROR
output: stdout # stdout | stderr
locking:
sequential: false # default for all curricula on this machine
lesson_sequential: false
Pass a custom config location with -c / --config.
Development Setup
Prerequisites
- Python 3.12+
- pyve (virtual env manager used in this project)
- pnpm 9+ and Node.js 18+
Setup
git clone https://github.com/pointmatic/learningfoundry.git
cd learningfoundry
# Create the Python environment and install the package in editable mode
pyve init
pip install -e .
# Create the test runner environment and install dev dependencies
pyve testenv --init
pyve testenv --install -r requirements-dev.txt
Running Tests
# Fast unit + integration tests (~2 min)
pyve test
# End-to-end SvelteKit smoke tests (requires pnpm, ~15 s extra)
pyve test tests/test_smoke_sveltekit.py -v
Linting and Type Checking
pyve testenv run ruff check .
pyve testenv run mypy src/
Project Structure
learningfoundry/
├── src/learningfoundry/
│ ├── cli.py # Click CLI entry point
│ ├── config.py # Configuration loading
│ ├── exceptions.py # Exception hierarchy
│ ├── generator.py # SvelteKit project generator
│ ├── integrations/ # Quiz / exercise / visualization providers
│ ├── logging_config.py # Logging setup
│ ├── parser.py # YAML parser + version dispatch
│ ├── pipeline.py # run_build / run_validate / run_preview
│ ├── resolver.py # Content reference resolver
│ └── schema_v1.py # Pydantic v1 curriculum schema
├── sveltekit_template/ # SvelteKit app template (copied on build)
├── tests/ # pytest test suite
├── requirements-dev.txt # Dev dependencies
└── pyproject.toml # Build config, ruff, mypy, pytest settings
License
Apache 2.0 — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file learningfoundry-0.60.0.tar.gz.
File metadata
- Download URL: learningfoundry-0.60.0.tar.gz
- Upload date:
- Size: 141.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b09227c66d154defa171d51d94f28fd21fd4c90c2847c0cc6115d5e29c45a316
|
|
| MD5 |
b194bf4f207fcc244df1104506fd7bea
|
|
| BLAKE2b-256 |
2ef9bc143d85f4a8875ce61232df68120fffa5faffc319de4d0f7616cbce810a
|
Provenance
The following attestation bundles were made for learningfoundry-0.60.0.tar.gz:
Publisher:
publish.yml on pointmatic/learningfoundry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
learningfoundry-0.60.0.tar.gz -
Subject digest:
b09227c66d154defa171d51d94f28fd21fd4c90c2847c0cc6115d5e29c45a316 - Sigstore transparency entry: 1427600228
- Sigstore integration time:
-
Permalink:
pointmatic/learningfoundry@dfaf56943bf9ae8e9546115bf392752a6a811049 -
Branch / Tag:
refs/tags/v0.60.0 - Owner: https://github.com/pointmatic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dfaf56943bf9ae8e9546115bf392752a6a811049 -
Trigger Event:
push
-
Statement type:
File details
Details for the file learningfoundry-0.60.0-py3-none-any.whl.
File metadata
- Download URL: learningfoundry-0.60.0-py3-none-any.whl
- Upload date:
- Size: 164.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46fcd67a829b659d3e97f437333b04947ab688373f09d6699f50e4a3daa2cb46
|
|
| MD5 |
0e2243aca99140e2653b6ac55bde373e
|
|
| BLAKE2b-256 |
e9b1f915c9f39300615f319b2775f2db4f07601e8972421fb0f97d1f0c74146d
|
Provenance
The following attestation bundles were made for learningfoundry-0.60.0-py3-none-any.whl:
Publisher:
publish.yml on pointmatic/learningfoundry
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
learningfoundry-0.60.0-py3-none-any.whl -
Subject digest:
46fcd67a829b659d3e97f437333b04947ab688373f09d6699f50e4a3daa2cb46 - Sigstore transparency entry: 1427600362
- Sigstore integration time:
-
Permalink:
pointmatic/learningfoundry@dfaf56943bf9ae8e9546115bf392752a6a811049 -
Branch / Tag:
refs/tags/v0.60.0 - Owner: https://github.com/pointmatic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dfaf56943bf9ae8e9546115bf392752a6a811049 -
Trigger Event:
push
-
Statement type: