Skip to content

Commit ca66447

Browse files
mmckyclaude
andauthored
Tracker: full per-series dataset manifest + reader-facing milestones (#22)
* Tracker: full per-series dataset manifest + reader-facing milestones Two changes to the migration page, from review of the live dashboard: - "All datasets by series": every static file the lectures read, grouped by the series that owns the bytes, each with a plain-language migration status (migrated / copied-not-switched / queued / not scheduled) and a per-series progress meter — the how-far-along view the per-dataset steppers alone didn't give. - The page is now plan-agnostic for readers who are not doing the migration: wave codes (P1…P4) appear only in the new Milestones section, which presents the programme itself — completed waves (derived from the tracker records), upcoming waves (with reader-facing titles now carried in migration.yml), the broad sweep, and the final data.quantecon.org switch. Lifecycle jargon (repointed etc.) is confined to the explained stepper detail; statuses elsewhere speak plainly, and the overview page copy follows suit. Part of #20. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Copilot review: landed-record crash + unscheduled miscount - milestones(): (repoints or [{}]) guards a record whose repoints list is empty or null — previously an IndexError as soon as any wave dataset was merely pending/landed - render_migration(): compute the "not yet scheduled" tile directly from dataset_status() over the scanned datasets instead of n_total - migrated - queued, which counted landed datasets as unscheduled Regression-tested with a synthetic landed record (repoints: []): the milestone renders and the tile drops 26 → 25 as it should. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 8a04862 commit ca66447

2 files changed

Lines changed: 191 additions & 47 deletions

File tree

migration.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,11 @@ datasets:
8282

8383
# Planned waves that have not landed anything here yet. `datasets` names the
8484
# files as the audit sees them today, so the dashboard can join the two views.
85+
# `title` is the reader-facing milestone name (the dashboard is read by people
86+
# who are not doing the migration); `scope` carries the insider detail.
8587
pending:
8688
- pilot: P3
89+
title: Large data files, and the external data repository
8790
scope: the LFS case — heavy_tails Forbes/cities set + SCF extracts (fold in high_dim_data)
8891
datasets:
8992
- forbes-global2000.csv
@@ -96,6 +99,7 @@ pending:
9699
- QuantEcon/data-lectures#2
97100
- QuantEcon/meta#337
98101
- pilot: P4
102+
title: Maintained snapshots for lectures that fetch live API data
99103
scope: the dynamic-snapshot case — a maintained UNRATE twin for the incidental FRED consumers
100104
datasets: [] # produces a NEW file here; no existing static file moves
101105
tracking:

scripts/render_audit.py

Lines changed: 187 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@
205205
.meter .seg + .seg { margin-left: 2px; }
206206
.meter .seg.a { background: var(--meter-a); }
207207
.meter .seg.b { background: var(--meter-b); }
208+
.meter.mini { height: 8px; max-width: 340px; margin: 6px 0 2px; border-radius: 4px; }
208209
/* bar list — one measure, single hue, labeled rows */
209210
.barlist { display: grid; grid-template-columns: max-content 1fr max-content; gap: 6px 12px; align-items: center; margin: 14px 0; }
210211
.barlist .lbl { font-size: 13px; }
@@ -360,13 +361,13 @@ def migration_meter(audit: dict) -> str:
360361
<h3>Migration progress</h3>
361362
<div class="meter" role="img" aria-label="{n_repointed} of {n_total} static datasets repointed to data-lectures">{segs}</div>
362363
<div class="legend">
363-
<span class="key"><span class="swatch" style="background:var(--meter-a)"></span>repointedconsumers read data-lectures ({n_repointed})</span>
364-
<span class="key"><span class="swatch" style="background:var(--meter-b)"></span>landed here, consumers not yet repointed ({n_landed})</span>
365-
<span class="key"><span class="swatch" style="background:var(--meter-track); border:1px solid var(--line)"></span>not migrated ({n_total - n_repointed - n_landed}, of which {len(queued)} queued in P3)</span>
364+
<span class="key"><span class="swatch" style="background:var(--meter-a)"></span>migratedevery consuming lecture reads the central copy ({n_repointed})</span>
365+
<span class="key"><span class="swatch" style="background:var(--meter-b)"></span>copied here, lectures not yet switched ({n_landed})</span>
366+
<span class="key"><span class="swatch" style="background:var(--meter-track); border:1px solid var(--line)"></span>not migrated ({n_total - n_repointed - n_landed}, of which {len(queued)} queued for the next waves)</span>
366367
</div>
367-
<p class="note">Denominator: the {n_total} distinct static files lectures read today. The interim→final
368-
URL cutover ({issue_link("QuantEcon/data-lectures#15")}) advances datasets from <em>repointed</em> to
369-
<em>final</em> once <code>data.quantecon.org</code> is live; no dataset is final yet.</p>
368+
<p class="note">Denominator: the {n_total} distinct static files lectures read today. The full
369+
per-series manifest and the milestone plan are on the
370+
<a href="migration.html">migration tracker</a>.</p>
370371
"""
371372

372373

@@ -400,11 +401,12 @@ def what_changed() -> str:
400401
<h2>What moved since the {PREV_SNAPSHOT} snapshot</h2>
401402
<p class="lede">The audit was first taken by hand on {PREV_SNAPSHOT}. Regenerating from live
402403
repo state two days later, the picture has already changed — which is why this dashboard is generated.</p>
403-
<div class="finding ok"><b>4 datasets migrated and repointed.</b>
404-
<p>P1 moved <code>lingcod_msy_recovery.csv</code> (was a Colab-breaking local path);
405-
P2 moved the <code>pandas_panel</code> trio <code>realwage.csv</code> / <code>countries.csv</code> /
406-
<code>employ.csv</code> (5 of their 6 reads pointed at the retired legacy repo). All consumers
407-
now read data-lectures. Details on the <a href="migration.html">migration tracker</a>.</p></div>
404+
<div class="finding ok"><b>4 datasets migrated.</b>
405+
<p>The first two migration waves moved <code>lingcod_msy_recovery.csv</code> (was a
406+
Colab-breaking local path) and the <code>pandas_panel</code> trio <code>realwage.csv</code> /
407+
<code>countries.csv</code> / <code>employ.csv</code> (5 of their 6 reads pointed at the retired
408+
legacy repo). Every consuming lecture now reads the central copy. Details on the
409+
<a href="migration.html">migration tracker</a>.</p></div>
408410
<div class="finding ok"><b>Legacy-repo URLs: 8 → 0.</b>
409411
<p>The pre-MyST <code>QuantEcon/lecture-python</code> repo was renamed to
410412
<code>lecture-python.rst</code> and archived; the P2 repoints plus the <code>ols</code>
@@ -471,6 +473,88 @@ def render_index(audit: dict) -> str:
471473
# Page: migration tracker
472474
# ---------------------------------------------------------------------------
473475

476+
# Reader-facing status labels — the dashboard is read by people who are not
477+
# doing the migration, so the manifest speaks plainly; the lifecycle terms
478+
# (pending/landed/repointed/final) appear only in the explained stepper detail.
479+
STATUS_META = {
480+
# status → (pill css, label)
481+
"final": ("p-local", "✓ migrated"),
482+
"repointed": ("p-local", "✓ migrated"),
483+
"landed": ("p-own", "● copied, not yet switched"),
484+
"queued": ("p-legacy", "queued"),
485+
"unscheduled": ("p-embed", "not scheduled"),
486+
}
487+
488+
489+
def dataset_status(fname: str, migration: dict) -> tuple[str, str]:
490+
"""→ (status, pilot) for any static dataset the audit sees."""
491+
rec = (migration.get("datasets") or {}).get(fname)
492+
if rec:
493+
return rec.get("status", "pending"), rec.get("pilot", "")
494+
for wave in migration.get("pending") or []:
495+
if fname in (wave.get("datasets") or []):
496+
return "queued", wave.get("pilot", "")
497+
return "unscheduled", ""
498+
499+
500+
def series_manifest(audit: dict) -> str:
501+
"""Every static dataset, grouped by the lecture series that owns the bytes
502+
today, with its migration status — the how-far-along view."""
503+
migration = audit["migration"] or {}
504+
groups: dict[str, list] = {}
505+
for d in audit["datasets"]:
506+
groups.setdefault(repo_of_record(d), []).append(d)
507+
order = ["lecture-python-intro", "lecture-python-programming",
508+
"lecture-python.myst", "lecture-python-advanced.myst",
509+
"data-lectures"]
510+
rows = ""
511+
for repo in [r for r in order if r in groups] + sorted(set(groups) - set(order)):
512+
ds = sorted(groups[repo], key=lambda x: x["file"])
513+
statuses = [dataset_status(d["file"], migration)[0] for d in ds]
514+
n = len(ds)
515+
n_done = sum(1 for s in statuses if s in ("repointed", "final"))
516+
n_landed = statuses.count("landed")
517+
n_queued = statuses.count("queued")
518+
counts = [f"{n_done} migrated" if n_done else "",
519+
f"{n_landed} copied, not switched" if n_landed else "",
520+
f"{n_queued} queued" if n_queued else "",
521+
f"{n - n_done - n_landed - n_queued} not scheduled"
522+
if n - n_done - n_landed - n_queued else ""]
523+
pct = lambda k: max(0.8, 100 * k / n) if k else 0
524+
segs = ""
525+
if n_done:
526+
segs += f'<div class="seg a" style="width:{pct(n_done):.1f}%"></div>'
527+
if n_landed:
528+
segs += f'<div class="seg b" style="width:{pct(n_landed):.1f}%"></div>'
529+
rows += (f'<tr class="group"><td colspan="4">{esc(repo)} — '
530+
f'{n} file{"s" if n != 1 else ""} · '
531+
f'{esc(" · ".join(c for c in counts if c))}'
532+
f'<div class="meter mini" role="img" aria-label="{n_done} of {n} repointed">{segs}</div>'
533+
f'</td></tr>')
534+
for d, status_pilot in zip(ds, (dataset_status(d["file"], migration) for d in ds)):
535+
status, pilot = status_pilot
536+
css, label = STATUS_META.get(status, ("p-embed", status))
537+
if pilot and status == "queued":
538+
label += f" · wave {pilot}"
539+
pills = " ".join(pattern_pill(p) for p in d["patterns"])
540+
rows += (f'<tr><td class="mono">{esc(d["file"])}</td>'
541+
f'<td class="note">{esc(d["description"])}</td>'
542+
f'<td>{pills}</td>'
543+
f'<td><span class="pill {css}">{esc(label)}</span></td></tr>')
544+
return f"""
545+
<h2>All datasets by series</h2>
546+
<p class="lede">Every static file the lectures read today, grouped by the series that owns the
547+
bytes, with its migration status — the how-far-along view. <em>Not scheduled</em> means no
548+
milestone has claimed the file yet; the broad sweep (see the milestones below) eventually
549+
covers them all. Data written by the lectures themselves and live-API reads are not migration
550+
targets and are tracked on the <a href="audit.html">audit page</a>.</p>
551+
<div class="tablewrap"><table>
552+
<tr><th>File</th><th>Contents</th><th>Hosting today</th><th>Status</th></tr>
553+
{rows}
554+
</table></div>
555+
"""
556+
557+
474558
def stepper(fname: str, rec: dict, verified: bool) -> str:
475559
status = rec.get("status", "pending")
476560
if status not in STATUS_STEPS:
@@ -499,6 +583,73 @@ def stepper(fname: str, rec: dict, verified: bool) -> str:
499583
return f'<div class="steps">{steps}</div><p class="note" style="margin-bottom:0">{check}</p>'
500584

501585

586+
def milestones(audit: dict) -> str:
587+
"""The migration programme as reader-facing milestones — completed waves
588+
(derived from migration.yml records), upcoming waves, the broad sweep, and
589+
the final-URL switch. This section is what lets the rest of the dashboard
590+
stay plan-agnostic: wave codes like P3 mean something only because they
591+
are presented here."""
592+
mig = audit["migration"] or {}
593+
recs = mig.get("datasets") or {}
594+
manifests = audit["manifests"]
595+
596+
by_pilot: dict[str, list] = {}
597+
for fname, rec in recs.items():
598+
by_pilot.setdefault(rec.get("pilot", "?"), []).append((fname, rec))
599+
items = ""
600+
for pilot in sorted(by_pilot):
601+
entries = by_pilot[pilot]
602+
files = sorted(f for f, _ in entries)
603+
done = all(r.get("status") in ("repointed", "final") for _, r in entries)
604+
dates = [str((r.get("repoints") or [{}])[-1].get("date", "")) for _, r in entries]
605+
series = {c["repo"] for f, _ in entries
606+
for c in (manifests.get(f, {}).get("consumers") or [])}
607+
n_series = len(series)
608+
mark, cls = ("✓", "ok") if done else ("○", "")
609+
items += f"""
610+
<div class="finding {cls}">
611+
<b>{mark} Wave {esc(pilot)}{len(files)} dataset{"s" if len(files) != 1 else ""},
612+
consumed by {n_series} lecture series{" — completed " + esc(max(dates)) if done and max(dates) else ""}</b>
613+
<p>{" · ".join(f"<code>{esc(f)}</code>" for f in files)}</p>
614+
</div>
615+
"""
616+
for w in mig.get("pending") or []:
617+
files = w.get("datasets") or []
618+
flist = (" · ".join(f"<code>{esc(f)}</code>" for f in sorted(files))
619+
if files else "produces new snapshot files here; no existing file moves")
620+
tracking = " · ".join(issue_link(t) for t in w.get("tracking") or [])
621+
items += f"""
622+
<div class="finding">
623+
<b>○ Wave {esc(w.get("pilot", ""))}{esc(w.get("title", w.get("scope", "")))}</b>
624+
<p>{flist}</p>
625+
<p>Tracking: {tracking}</p>
626+
</div>
627+
"""
628+
n_unsched = sum(1 for d in audit["datasets"]
629+
if dataset_status(d["file"], mig)[0] == "unscheduled")
630+
items += f"""
631+
<div class="finding">
632+
<b>○ Broad sweep — every remaining static file</b>
633+
<p>The {n_unsched} not-yet-scheduled files move once the waves above have proven the
634+
process for each kind of data. Mechanical from there: one data PR here, one switch PR
635+
per consuming lecture repo.</p>
636+
</div>
637+
<div class="finding">
638+
<b>○ Final URLs — serve everything from <code>data.quantecon.org</code></b>
639+
<p>Migrated lectures currently read this repository's GitHub URL. Once the custom domain
640+
is live, every migrated dataset switches to its permanent
641+
<code>data.quantecon.org/lectures/…</code> address in a single sweep
642+
({issue_link("QuantEcon/data-lectures#15")}). No dataset has made this step yet.</p>
643+
</div>
644+
"""
645+
return f"""
646+
<h2>Milestones</h2>
647+
<p class="lede">The migration proceeds in small waves, each proving the process for a harder
648+
kind of data before the broad sweep moves the rest.</p>
649+
{items}
650+
"""
651+
652+
502653
def render_migration(audit: dict) -> str:
503654
mig = audit["migration"] or {}
504655
recs = mig.get("datasets") or {}
@@ -518,67 +669,56 @@ def render_migration(audit: dict) -> str:
518669
<div class="pipeline">
519670
<div class="head">
520671
<span class="name mono">{esc(fname)}</span>
521-
<span class="pill p-dl">{esc(rec.get("pilot", ""))}</span>
672+
<span class="pill p-dl">wave {esc(rec.get("pilot", ""))}</span>
522673
{pattern_pill(prior)} <span class="note">→</span> {pattern_pill("data-lectures")}
523674
<span class="note" style="margin-left:auto"><a href="{RAW}/{esc(fname)}">file</a> ·
524675
<a href="{DL}/blob/main/lectures/{esc(fname)}.yml">manifest</a></span>
525676
</div>
526677
<p class="note" style="margin:0 0 10px">{esc(m.get("title", ""))} — consumed by {consumers or "—"}</p>
527678
{stepper(fname, rec, verified)}
528679
</div>
529-
"""
530-
531-
waves = ""
532-
for w in mig.get("pending") or []:
533-
files = "".join(f'<li><code>{esc(f)}</code> — {esc((by_name.get(f) or {}).get("description", ""))}</li>'
534-
for f in w.get("datasets") or []) or \
535-
"<li class='note'>produces a new file here; no existing static file moves</li>"
536-
tracking = " · ".join(issue_link(t) for t in w.get("tracking") or [])
537-
waves += f"""
538-
<div class="finding">
539-
<b>{esc(w.get("pilot", ""))}{esc(w.get("scope", ""))}</b>
540-
<ul>{files}</ul>
541-
<p>Tracking: {tracking}</p>
542-
</div>
543680
"""
544681

545682
problems = audit["problems"]["migration_inconsistencies"]
546683
consistency = ('<div class="finding ok"><b>Consistency check: clean.</b>'
547-
'<p>migration.yml, the manifests and today\'s scan of every consumer repo agree.</p></div>'
684+
'<p>The tracker, the dataset manifests and today\'s scan of every lecture repo agree.</p></div>'
548685
if not problems else
549686
'<div class="finding crit"><b>Consistency check: FAILING.</b><ul>'
550687
+ "".join(f"<li>{esc(p)}</li>" for p in problems) + "</ul></div>")
551688

552689
n_re = sum(1 for r in recs.values() if r.get("status") in ("repointed", "final"))
553690
queued = {f for w in (mig.get("pending") or []) for f in w.get("datasets") or []}
691+
n_unsched = sum(1 for d in audit["datasets"]
692+
if dataset_status(d["file"], mig)[0] == "unscheduled")
554693
body = f"""
555694
<header class="doc">
556-
<p class="eyebrow">Migration tracker · source of truth: <code>migration.yml</code> + <code>lectures/*.yml</code></p>
557-
<h1>Datasets moving into data-lectures</h1>
558-
<p class="meta">Each dataset's position in the lifecycle
559-
<em>pending → landed → repointed → final</em>, with the PRs that moved it. The build
560-
verifies every status against a fresh scan of the consumer repos, so this page cannot
695+
<p class="eyebrow">Migration tracker · generated and verified on every build</p>
696+
<h1>Moving lecture data into one central repository</h1>
697+
<p class="meta">The QuantEcon lectures read data from many places — their own repos, local
698+
files, retired repos, live APIs. This tracker shows the move to a single canonical home
699+
(<a href="{DL}">data-lectures</a>): what has moved, what's next, and how far along it is.
700+
Every status is verified against a fresh scan of the lecture repos, so this page cannot
561701
claim a migration that didn't happen.</p>
562702
</header>
563703
<div class="stats">
564-
<div class="stat ok"><b>{n_re}</b><span>repointed — consumers read data-lectures today</span></div>
565-
<div class="stat"><b>{len(queued)}</b><span>queued in pending waves (P3)</span></div>
566-
<div class="stat"><b>0</b><span>final — on data.quantecon.org (gated on DNS, {issue_link("QuantEcon/data-lectures#15")})</span></div>
704+
<div class="stat ok"><b>{n_re}</b><span>datasets migrated — every consuming lecture reads the central copy</span></div>
705+
<div class="stat"><b>{len(queued)}</b><span>queued for the next waves</span></div>
706+
<div class="stat"><b>{n_unsched}</b><span>not yet scheduled (the broad sweep)</span></div>
567707
</div>
708+
<h3>How a dataset moves</h3>
709+
<p><b>pending</b> — identified for migration; nothing has moved yet.
710+
<b>landed</b> — the file and its metadata are merged into the central repo; lectures unchanged.
711+
<b>repointed</b> — every consuming lecture now reads the central copy (shown as
712+
<em>✓ migrated</em> in the table below).
713+
<b>final</b> — the lecture reads the permanent <code>data.quantecon.org</code> address
714+
(the last milestone below).</p>
568715
{consistency}
569-
<h2>Migrated datasets</h2>
716+
{series_manifest(audit)}
717+
{milestones(audit)}
718+
<h2>Migration record — dataset by dataset</h2>
719+
<p class="lede">The full provenance for each migrated dataset: which pull requests landed the
720+
data and switched each consuming lecture, and where it sits in the lifecycle.</p>
570721
{pipelines}
571-
<h2>Pending waves</h2>
572-
<p class="lede">Scoped but not landed. Files listed here still appear under their current
573-
hosting pattern in the <a href="audit.html">audit</a>.</p>
574-
{waves}
575-
<h2>How a dataset moves</h2>
576-
<p><b>pending</b> — identified for migration; nothing in data-lectures yet.
577-
<b>landed</b> — file + manifest merged here; consumers unchanged.
578-
<b>repointed</b> — every consumer reads this repo's interim raw URL
579-
(<code>github.com/QuantEcon/data-lectures/raw/main/lectures/…</code>).
580-
<b>final</b> — every consumer reads <code>https://data.quantecon.org/lectures/…</code>;
581-
the cutover is one sweep tracked in {issue_link("QuantEcon/data-lectures#15")}, gated on Phase 4 DNS.</p>
582722
"""
583723
return page("Migration tracker — data-lectures", "migration", body, audit)
584724

0 commit comments

Comments
 (0)