Skip to content
QuantEconPublic

About

QuantEcon lectures monorepo — a shared pool of lectures with per-product manifests, mirrored read-only from the six live series (under construction)

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

94 Commits

Folders and files

Repository files navigation

QuantEcon Lectures — monorepo

Status: being prepared — nothing here is published yet. The QuantEcon lectures still live at their individual series sites, linked below: read and cite those. This repository holds a shared pool of lectures mirrored from those series, and the tooling that keeps the pool faithful to them, while the move into this repository is prepared one series at a time.

This repository is the future single source of truth for the QuantEcon lecture series. The goal is to hold every canonical lecture exactly once in a shared pool, and to build each published series or course from a thin configuration that selects and orders pool lectures — never from a copy.

It sits downstream of six live lecture repositories until each one deliberately cuts over. What exists today:

  • The pool: 52 lectures in lectures/, promoted from the series together with the figures and files they use, each with its provenance recorded in sync/ledger.yml.
  • The mirror: every series' lectures, reconstructed on demand from commits pinned in sync/state.yml, which a daily job keeps current.
  • The drift-check: run after every sync, it proves each pool lecture still equals its source, opens a pull request to refresh the pool when a source moves, and opens an issue when another series' copy diverges.
  • The promote tool: it brings lectures into the pool and refuses a lecture name that two series use for different lectures.
  • The merged bibliography: one lectures/_static/quant-econ.bib, generated from the series' own files and the rulings in sync/bib.yml, and refreshed with the pool.

See The plan for what comes next.

Where the lectures actually live (for now)

Until each series is deliberately cut over, the live sites below remain authoritative. Read and cite those, not this repository.

Key Live site Source series
intro https://intro.quantecon.org QuantEcon/lecture-python-intro
programming https://python-programming.quantecon.org QuantEcon/lecture-python-programming
intermediate https://python.quantecon.org QuantEcon/lecture-python.myst
advanced https://python-advanced.quantecon.org QuantEcon/lecture-python-advanced.myst
dp https://quantecon.github.io/lecture-dp/ (planned: https://dynamic-programming.quantecon.org) QuantEcon/lecture-dp
jax https://jax.quantecon.org QuantEcon/lecture-jax

The four posture rules

These rules govern how this repository relates to the six live series until each one cuts over.

  1. Read-only, with stated exceptions. This repository only reads from the live series. Changes in the series themselves are limited to ordinary lecture maintenance, the renames that make every lecture name unique across the published sites (each with a redirect from the old address), and two merges that reconcile lectures two series share. Each is its own reviewed pull request in that series. A series receives the workflow that publishes it from here at its cutover and not before, and no engine or layout change is made in a live series before its own cutover.
  2. The live repos remain the source of truth. Each live series stays authoritative until a deliberate, per-series cutover moves it here. Nothing is migrated implicitly.
  3. After its cutover, a series is edited here. Until then, its lectures are edited in the series repository and reach the pool by sync. At its cutover, the pool becomes its source of truth.
  4. Shadow build, then cut over. Before a series moves, this repository builds it from the pool and compares the result with the series' own published build, with nothing going live. Each cutover is decided once the builds match.

The mirror → pool → products model

The repository is organised around three layers. The first two exist today.

Layer What it is State today
mirror/<key>/ A machine-reconstructed, verbatim copy of each live series' allowlisted paths, pinned to an exact commit. Never hand-edited, never committed. Reconstructed on demand; gitignored.
lectures/ The curated pool — every canonical lecture, held exactly once. 52 lectures and their assets, each recorded in sync/ledger.yml.
products/ One folder per published product, each a thin configuration that selects and orders lectures from the pool. Empty: each series' configuration arrives with the publishing workflow.

Why we commit only SHAs, not the mirror

mirror/<key>/ is a pure function of three inputs: the source repo, a pinned commit SHA, and the allowlist of paths to take from it (lectures/ and environment.yml). Because the mirror is fully determined by those inputs, we commit only the pinned SHAs (in sync/state.yml) and rebuild the mirror on demand. The repository therefore carries no heavy verbatim history, and the mirror can always be reconstructed byte-for-byte.

  • Human intent — which repos feed the mirror — lives in sync/manifest.yml.
  • Machine state — the exact SHA each series is pinned to — lives in sync/state.yml, written by the sync tool.

Directory layout

.
├── _base/            # (later) shared toolchain defaults — placeholder
├── lectures/         # the pool: every canonical lecture once
│   └── _static/      #   their assets: _static/<slug>/ per lecture, _static/_shared/ for shared files,
│                     #   and quant-econ.bib, the merged bibliography (bib-merge-written)
├── products/         # (later) one configuration per published product — empty
├── mirror/           # reconstructed, verbatim, GITIGNORED — never committed
├── docs/tech/        # the technology register (see below)
├── sync/
│   ├── manifest.yml  # human intent: which repos feed the mirror, and each series' class
│   ├── canonical.yml # human intent: the winning series for a same-name collision
│   ├── bib.yml       # human intent: the rulings that settle the bibliographies' conflicts
│   ├── state.yml     # machine state: pinned SHAs (sync-written)
│   ├── ledger.yml    # machine state: provenance of every pool lecture and asset (promote-written)
│   └── toc.yml       # machine state: the watched _toc.yml baselines (drift-check-written)
├── tools/
│   ├── sync          # reconstructs the mirror from pinned SHAs
│   ├── promote       # promotes lectures + their asset closure into the pool; writes the ledger
│   ├── drift-check   # proves the pool still equals its sources; issues, refresh PR
│   ├── bib-merge     # merges the series' bibliographies into the pool's one file; checks it
│   ├── pool-check    # checks the pool keeps its layout conventions
│   └── tests/        # pytest suite for promote, drift-check, bib-merge and pool-check (scratch repos)
└── .github/workflows/
    ├── sync.yml         # daily job that refreshes the pinned SHAs (pin-only)
    ├── drift-check.yml  # runs after sync: drift issues, refresh PR (pool and bibliography), red on a pool edit or a refused refresh
    ├── pool-check.yml   # runs tools/pool-check on pull requests that touch the pool
    └── tests.yml        # runs tools/tests on pull requests

Running the sync

tools/sync reconstructs the mirror and refreshes the pinned SHAs. It is a self-contained script run via uv — its dependencies are declared inline (PEP 723), so there is nothing to install first. Invoke it either directly (./tools/sync …, once the executable bit is set) or through uv (uv run --script tools/sync …), which needs no executable bit.

The tool never commits anything; committing a refreshed sync/state.yml is CI's job.

Command What it does
tools/sync Reconstruct every series into mirror/<key>/ at its already-pinned SHA.
tools/sync dp Reconstruct only the dp series. Any subset of keys works.
tools/sync --update Re-resolve each series' HEAD, re-pin the SHA (with a UTC timestamp) into sync/state.yml when it has moved, then reconstruct.
tools/sync --update --pin-only Re-resolve and re-pin SHAs but skip reconstruction — no downloads. This is what CI runs.
tools/sync --update dp jax Re-pin and reconstruct just dp and jax.
tools/sync -h Full help.

A series' pin (and its timestamp) is only rewritten when the upstream HEAD has actually moved, so a re-pin run over unchanged series leaves sync/state.yml untouched. If a series has no pinned SHA yet, a plain tools/sync reports a clear error and suggests running --update first. Failures are per-series: the tool logs each and carries on, then prints a PASS/FAIL summary and exits non-zero if any series failed.

Promoting lectures

tools/promote copies lectures from the mirror into the pool together with their asset closure — the figures, includes and data files a lecture actually references — and records where each came from in sync/ledger.yml. Asset references are rewritten to the pool's _static/ layout, and every rewrite is recorded, so the pool copy can always be reproduced from its source. Like the other tools it is a self-contained uv script, and it never commits anything or touches the mirror.

Command What it does
tools/promote SLUG … Promote the named lectures, each from the highest-priority series that holds it.
tools/promote --toc SERIES Promote every lecture in a series' _toc.yml. For a consumer series (dp), each lecture comes from its home series.
tools/promote --refresh Re-promote every entry whose canonical series has moved since it was promoted. CI runs it with --exclude $PROMOTE_EXCLUDE.
tools/promote … --exclude SERIES … Leave the named series out of this run.
tools/promote … --dry-run Print the full plan without writing anything.

tools/promote refuses a lecture name held by two canonical-eligible series with different text, naming the lecture and every series involved. It does not pick a winner by run order. A rename or merge upstream fixes it; until then, sync/canonical.yml can name the winning series, and the entry is recorded as interim. A copy that is merely stale (a recorded source still at a version the pool already took, while the canonical copy moved on) is not a collision: it stays a source, and the refresh goes through. A sync/canonical.yml line that names such a stale copy is refused, though, since following it would take the pool back to the lagging text, so delete a line as soon as promote reports that it settles nothing.

Running the drift-check

tools/drift-check proves the pre-cutover invariant: every pool lecture equals its canonical mirror copy modulo the rewrites recorded in sync/ledger.yml, and every other series' copy is where the ledger last saw it. Like tools/sync it is a self-contained uv script. It never writes to the pool or the mirror: the only repository file it writes is sync/toc.yml, under --accept-toc; --json writes its report wherever you point it, and under GitHub Actions it appends step outputs.

Command What it does
tools/drift-check Report against the existing mirror/. Exit 0 clean, 1 findings, 2 a pool file was edited directly.
tools/drift-check --reconstruct Rebuild mirror/ first, for every series the check reads: every canonical-eligible series, and a consumer whose toc is watched.
tools/drift-check --print-issues Show the issues --issues would open, without touching GitHub.
tools/drift-check --issues Open, update and close drift issues to match the findings (CI).
tools/drift-check --accept-toc dp Record a series' _toc.yml at its current pin as the watched baseline.

Four outcomes: refresh (the canonical copy moved upstream; CI runs promote --refresh --exclude $PROMOTE_EXCLUDE and opens a pull request; an entry the refresh refuses is listed with promote's reason, in that pull request when one is opened and always in the red run's refresh step; the usual reason is a same-name copy edited to differ, which sync/canonical.yml or upstream settles; while the entry's canonical copy differs from its ledger record the run stays red every night until it is settled, and a settlement applies the next night), drift (a non-canonical copy diverged from its canonical home, a copy was removed, a watched toc changed or lists a lecture the pool does not hold, or two published sites hold different lectures under one name; each becomes an issue that closes itself when the finding clears), violation (the pool itself was edited; the job goes red), and info (a copy is merely stale, or a divergence converged). drift-check.yml runs it after every sync.

One kind of copy is never watched. A consumer series (class: consumer in sync/manifest.yml, today dp) only republishes other series' lectures, so its copies are recorded as superseded and are expected to go stale. Its _toc.yml is watched, though: the pool's dp selection is promoted from it, so a change there means re-running that promotion with the refresh's exclusions (PROMOTE_EXCLUDE in drift-check.yml; today tools/promote --toc dp --exclude jax), then --accept-toc dp. Accepting alone clears that issue but leaves a newly listed lecture out of the pool, which the check then reports as toc-unpromoted.

Lecture names are unique across the published sites, not only in the pool: a link that leaves a product resolves by name, so two sites that publish different lectures under one name send readers to the wrong one. The check reads every canonical-eligible series' _toc.yml, whether its lectures are in the pool yet or not, and a name two of them list must be one lecture in both, by promote's rule (name-shared). A consumer's toc is not read for this: it only republishes.

Checking the pool's layout

tools/pool-check checks that the pool keeps the layout tools/promote writes, whoever edits it next. lectures/ holds lectures (<slug>.md, no name starting with _, no two names that differ only in case) and _static/. Every folder at the top of _static/ is a lecture's own (_static/<slug>/, beside <slug>.md) or _shared/, and the merged bibliography is the one file there. A file in _static/<slug>/ is used by <slug>.md alone, so an asset two lectures use goes in _shared/. Every asset reference is file-relative (_static/<slug>/fig.png, never /_static/...) and stays inside lectures/. It finds references with promote's own parser, reads lectures/ only, and needs no mirror. Exit 0 clean, 1 a rule broken, 2 an error. pool-check.yml runs it on every pull request that touches the pool.

Running the bibliography merge

tools/bib-merge builds the pool's one bibliography, lectures/_static/quant-econ.bib, from the quant-econ.bib of each series listed in sync/bib.yml. A consumer series such as dp is never a source, and the tool refuses one that is listed: its lectures are copies of other series' lectures, and its bibliography is a copy too. Each series' copy of an entry is normalised to the house style's mechanical rules (Writing a bibliography entry): page ranges with --, no month, a bare DOI, no fields a reader never sees, one field per line. Nothing else in a value changes. Copies that then agree are merged into one entry. Copies that still differ are a conflict, and sync/bib.yml must rule on it: which series' copy to take, any fields to set or drop, why, and a seal recording each copy as it was. If a series later edits a sealed copy, the copy the ruling takes included, the ruling reopens; the change is never folded in silently. The file is generated, never edited by hand: its header records digests of the series' entries, of sync/bib.yml and of its own entries, so the check tells a series that moved (a refresh) from a hand edit or a ruling that was not regenerated (a violation).

Command What it does
tools/bib-merge Regenerate in memory and check. Exit 0 clean, 1 the series' files moved (run --write), 2 a refused key, a failed check, or a committed file that differs from what the same inputs generate.
tools/bib-merge --write Write lectures/_static/quant-econ.bib.
tools/bib-merge --report Print every conflict side by side, with its ruling or why it was refused.
tools/bib-merge --propose Print a sync/bib.yml stub, sealed against the current copies, for every refused key.
tools/bib-merge --reconstruct Rebuild mirror/ for every series first.

Its checks: every key a pool lecture cites resolves; the entries keep the house-style rules the normalising cannot apply (the fields each entry type needs, names joined by and, keys unique when case is ignored); and pybtex, the reader Jupyter Book 1 uses, reads every merged entry as it reads the series copy it came from. It reads the series' files exactly or refuses them: a syntax error, a duplicate key or field, @string, a # concatenation, or a file in which bibtexparser and pybtex find different keys stops the run, and nothing is written that does not read back as the same entries. drift-check.yml runs it after every sync and refreshes the file into the same pull request as the pool; anything it fails on turns that workflow's bibliography job red, which never holds back the pool's refresh.

The technology register

docs/tech/ is a standing record of technology options for improving the lectures: what was looked at, the named files where it would apply, and what was decided and why. An entry is not ready until it names the files it would change; its README sets out the format and the states an entry moves through.

The plan

The move is planned in ten stages: reorganise first on today's toolchain (Jupyter Book 1), then switch the engine once, later.

  1. Set up access and tracking.
  2. Sources in. Bring every lecture of every series into the pool, reconciling differences in the series repositories.
  3. One publishing workflow that builds any product on either engine, rehearsed end to end, including a rollback, on a sandbox series.
  4. Shadow builds. Build each series here from the pool and compare it with its own published build, with nothing going live.
  5. Cutovers, one series at a time. Each series is published through a release in its own repository, so every earlier release is a rollback.
  6. The order of cutovers: dp (a publishing-only move, since it owns no lectures), programming, a go/no-go review, then intro, intermediate, advanced and JAX.
  7. A relay that keeps the translated editions fed from here.
  8. Page history, carried across at each cutover.
  9. mystmd builds alongside, from stage 3, publishing nothing.
  10. One engine switch for all series and editions together, after full testing.

As of October 2026, stage 2 is under way. The promote tool is ready, and the renames that make lecture names unique come next.

Licence

To be confirmed; the mirrored content remains under the licences of its respective source series until cutover.

About

QuantEcon lectures monorepo — a shared pool of lectures with per-product manifests, mirrored read-only from the six live series (under construction)

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages