Register of datasets whose committed copy differs from what upstream publishes today, so the migration can proceed without stopping to reconcile each one, and the deltas get reviewed together once it completes.
The policy this implements
A migration moves bytes; it does not update them. The copy that lands in this repo is the copy the lectures already consume, validated byte-identical in the repoint PR. That is what makes a repoint safe to merge: it provably cannot change a single figure.
Adopting a newer upstream vintage is a different change with a different risk profile — it does change lecture output, needs figures re-reviewed, and is an author-facing decision rather than an infrastructure one. Conflating the two would turn every repoint into a content review and stall the programme.
So when a migration discovers a delta against upstream:
- Migrate what the lectures use, unchanged, with the byte-compare gate as normal.
- Record the delta here, with enough detail that resolving it later needs no re-investigation.
- Review the register once the migration completes and decide each case on its merits: adopt the new vintage, keep the frozen copy deliberately, or reconcile a local edit.
Recording the delta in the dataset's manifest (integrity.upstream) is what makes this honest rather than a deferral — the gap is visible in the generated catalog from the day it is found, not filed away in an issue nobody reads.
Two kinds of delta
They look similar and need opposite responses, so the register distinguishes them.
Upstream moved. A newer vintage exists; our copy is an older one. Resolving means deciding whether to adopt it — and per AGENTS.md, a new vintage gets a new filename rather than replacing the old one, so consumers opt in and existing figures stay valid.
Our copy diverges. Upstream is unchanged but the committed file was modified, or was constructed by a process we cannot reproduce. Resolving means reconciling the edit — usually by recording it precisely enough to reapply, or by rebuilding from upstream and accepting the output change.
Register
mpd2020.xlsx — our copy diverges (three header labels)
Found while writing its manifest in #38.
Cell on Regional data |
Ours |
Upstream |
| row0 col1 |
gdppc_2011 |
GDP pc 2011 prices |
| row0 col9 |
pop |
Population |
| row0 col18 |
gdppc_2011 |
(empty) |
Every data value matches, including all 21,683 rows of Full data. Only these three labels differ.
The edits are ours, established rather than assumed. The Internet Archive holds the upstream file with one unchanging content digest (4OWZWHTE5HXBBF4XLCCQTGDXY4HXOGOK) across every snapshot from 2021-01-10 to 2026-01-02, and a copy fetched 2026-08-06 matches it exactly — so upstream was byte-stable two years before these bytes were committed to lecture-python-intro on 2023-03-23.
Why this cannot simply be "fixed". long_run_growth reads that sheet with header=(0,1,2). The renames are load-bearing: replacing the file with a pristine upstream copy would silently change what the lecture plots, with no error raised. Any resolution has to move the lecture and the file together.
Options when reviewed: keep the edited copy and document it as the intended state (cheapest, and what the manifest records today); or restore the upstream file and adapt the lecture's column handling, which makes the file genuinely verbatim and removes a permanent local patch. There is no urgency either way — the current state is correct and documented.
life-expectancy-vs-gdp-per-capita.csv — upstream moved (a new vintage, and it is not a drop-in)
Found while writing its manifest in #74. Measured against the live grapher endpoint on 2026-08-12: HTTP 200, 1,414,428 B against our 2,059,709 B.
Four of eight columns are renamed. Only Entity, Code, Year and GDP per capita are stable.
| Ours |
Today's export |
Life expectancy at birth (historical) |
Life expectancy at birth |
Population (historical estimates) |
Population |
Continent |
World region according to OWID |
417485-annotations — position 6 |
GDP per capita (Annotations) — position 8 |
The annotations column is renamed and moved, not dropped. That correction matters, because "drops 417485-annotations" is how this delta had been described in the work plan, and it points at the wrong failure. Coverage changes too: 62,156 → 29,912 rows, Year -10000..2021 → 1..2023, entities 317 → 276. Today's metadata cites Maddison Project Database 2023, HMD 2025 and UN WPP 2024, all of which postdate this export.
Why a refresh is a breaking change, in two different ways. The loud one: all four consuming lectures hardcode the old label and pass it as usecols, so today's export raises ValueError: Usecols do not match columns, columns expected but not found: ['Life expectancy at birth (historical)']. Note it names only the life-expectancy column — the annotations column is not in usecols, so the annotations change is not what breaks the read.
The quiet one is the reason this belongs in a register rather than a bug tracker. After relabelling usecols, dropna() yields 13,239 rows against our 12,445, so the sentence "dropped the number of rows in our DataFrame from 62156 to 12445" — hardcoded in the prose of all four repos — becomes wrong. And year == 2018 returns 166 rows in both vintages, so the final scatter and OLS fit would look superficially identical while every life-expectancy value shifted. A refresh that fixed only the ValueError would ship a silently different figure.
upstream-moved, established rather than assumed. The committed file's column names are OWID's own older schema — the (historical) suffixes and the numeric 417485-annotations variable id are not names anyone here would invent — and its Year ceiling of 2021 is consistent with an export taken before the source releases the current metadata cites. Our 62,156 rows are a superset of today's 29,912 in coverage, which rules out a local truncation. No QuantEcon edit is implicated.
Options when reviewed. Per AGENTS.md "Corrections vs vintages", adopting the new vintage means a new filename so consumers opt in — never a replacement of this file. That would also mean editing usecols and the two hardcoded row counts in four repos, and re-reviewing five figures. Keeping the frozen copy is the status quo and is what the manifest records today. There is no urgency: the current state is correct, documented, and the lecture embeds the live grapher as an iframe beside the frozen data anyway, so a reader already sees the current OWID vintage next to the figure.
Nothing else, yet
The other twenty-four manifested datasets have no known upstream delta. Twelve carry integrity.upstream.status: unverifiable, which is a different condition: not "we know it differs" but "we cannot check". Those are a reproducibility gap tracked through builder_status and PLAN Phase 9, not a drift item — they only enter this register if a check is later performed and finds a difference.
How entries get added
Any migration or verification that finds a delta adds a section here, and sets the dataset's integrity.upstream fields in the same PR so the catalog reflects it. The two must not diverge — a register entry with no manifest note is invisible to anyone reading the dataset.
Automating the detection side is proposed separately; see the upstream-freshness dashboard issue. This issue is the human register and stays useful regardless of whether that lands.
Part of #8. Related: #38 (the manifests that surfaced the first entry), #35 (licensing, the same record-and-track shape applied to a different question), and PLAN Phase 7.
Register of datasets whose committed copy differs from what upstream publishes today, so the migration can proceed without stopping to reconcile each one, and the deltas get reviewed together once it completes.
The policy this implements
A migration moves bytes; it does not update them. The copy that lands in this repo is the copy the lectures already consume, validated byte-identical in the repoint PR. That is what makes a repoint safe to merge: it provably cannot change a single figure.
Adopting a newer upstream vintage is a different change with a different risk profile — it does change lecture output, needs figures re-reviewed, and is an author-facing decision rather than an infrastructure one. Conflating the two would turn every repoint into a content review and stall the programme.
So when a migration discovers a delta against upstream:
Recording the delta in the dataset's manifest (
integrity.upstream) is what makes this honest rather than a deferral — the gap is visible in the generated catalog from the day it is found, not filed away in an issue nobody reads.Two kinds of delta
They look similar and need opposite responses, so the register distinguishes them.
Upstream moved. A newer vintage exists; our copy is an older one. Resolving means deciding whether to adopt it — and per
AGENTS.md, a new vintage gets a new filename rather than replacing the old one, so consumers opt in and existing figures stay valid.Our copy diverges. Upstream is unchanged but the committed file was modified, or was constructed by a process we cannot reproduce. Resolving means reconciling the edit — usually by recording it precisely enough to reapply, or by rebuilding from upstream and accepting the output change.
Register
mpd2020.xlsx— our copy diverges (three header labels)Found while writing its manifest in #38.
Regional datagdppc_2011GDP pc 2011 pricespopPopulationgdppc_2011Every data value matches, including all 21,683 rows of
Full data. Only these three labels differ.The edits are ours, established rather than assumed. The Internet Archive holds the upstream file with one unchanging content digest (
4OWZWHTE5HXBBF4XLCCQTGDXY4HXOGOK) across every snapshot from 2021-01-10 to 2026-01-02, and a copy fetched 2026-08-06 matches it exactly — so upstream was byte-stable two years before these bytes were committed tolecture-python-introon 2023-03-23.Why this cannot simply be "fixed".
long_run_growthreads that sheet withheader=(0,1,2). The renames are load-bearing: replacing the file with a pristine upstream copy would silently change what the lecture plots, with no error raised. Any resolution has to move the lecture and the file together.Options when reviewed: keep the edited copy and document it as the intended state (cheapest, and what the manifest records today); or restore the upstream file and adapt the lecture's column handling, which makes the file genuinely
verbatimand removes a permanent local patch. There is no urgency either way — the current state is correct and documented.life-expectancy-vs-gdp-per-capita.csv— upstream moved (a new vintage, and it is not a drop-in)Found while writing its manifest in #74. Measured against the live grapher endpoint on 2026-08-12:
HTTP 200, 1,414,428 B against our 2,059,709 B.Four of eight columns are renamed. Only
Entity,Code,YearandGDP per capitaare stable.Life expectancy at birth (historical)Life expectancy at birthPopulation (historical estimates)PopulationContinentWorld region according to OWID417485-annotations— position 6GDP per capita (Annotations)— position 8The annotations column is renamed and moved, not dropped. That correction matters, because "drops
417485-annotations" is how this delta had been described in the work plan, and it points at the wrong failure. Coverage changes too: 62,156 → 29,912 rows,Year-10000..2021 → 1..2023, entities 317 → 276. Today's metadata cites Maddison Project Database 2023, HMD 2025 and UN WPP 2024, all of which postdate this export.Why a refresh is a breaking change, in two different ways. The loud one: all four consuming lectures hardcode the old label and pass it as
usecols, so today's export raisesValueError: Usecols do not match columns, columns expected but not found: ['Life expectancy at birth (historical)']. Note it names only the life-expectancy column — the annotations column is not inusecols, so the annotations change is not what breaks the read.The quiet one is the reason this belongs in a register rather than a bug tracker. After relabelling
usecols,dropna()yields 13,239 rows against our 12,445, so the sentence "dropped the number of rows in our DataFrame from 62156 to 12445" — hardcoded in the prose of all four repos — becomes wrong. Andyear == 2018returns 166 rows in both vintages, so the final scatter and OLS fit would look superficially identical while every life-expectancy value shifted. A refresh that fixed only theValueErrorwould ship a silently different figure.upstream-moved, established rather than assumed. The committed file's column names are OWID's own older schema — the(historical)suffixes and the numeric417485-annotationsvariable id are not names anyone here would invent — and itsYearceiling of 2021 is consistent with an export taken before the source releases the current metadata cites. Our 62,156 rows are a superset of today's 29,912 in coverage, which rules out a local truncation. No QuantEcon edit is implicated.Options when reviewed. Per
AGENTS.md"Corrections vs vintages", adopting the new vintage means a new filename so consumers opt in — never a replacement of this file. That would also mean editingusecolsand the two hardcoded row counts in four repos, and re-reviewing five figures. Keeping the frozen copy is the status quo and is what the manifest records today. There is no urgency: the current state is correct, documented, and the lecture embeds the live grapher as an iframe beside the frozen data anyway, so a reader already sees the current OWID vintage next to the figure.Nothing else, yet
The other twenty-four manifested datasets have no known upstream delta. Twelve carry
integrity.upstream.status: unverifiable, which is a different condition: not "we know it differs" but "we cannot check". Those are a reproducibility gap tracked throughbuilder_statusand PLAN Phase 9, not a drift item — they only enter this register if a check is later performed and finds a difference.How entries get added
Any migration or verification that finds a delta adds a section here, and sets the dataset's
integrity.upstreamfields in the same PR so the catalog reflects it. The two must not diverge — a register entry with no manifest note is invisible to anyone reading the dataset.Automating the detection side is proposed separately; see the upstream-freshness dashboard issue. This issue is the human register and stays useful regardless of whether that lands.
Part of #8. Related: #38 (the manifests that surfaced the first entry), #35 (licensing, the same record-and-track shape applied to a different question), and PLAN Phase 7.