From 8d201cb7022b6a32a3ae42a077b11b93e800a31c Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Mon, 10 Aug 2026 08:52:03 +1000 Subject: [PATCH] Audit: assert on the media host, and on data-lectures URL resolvability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--strict` exited 0 on a fold that 404s every read. `pattern` is derived from org and repo alone, so a `data-lectures` URL counted as fully migrated no matter what host served it, what ref it pinned, or whether the file had landed here yet. `lfs_media` was computed per reference at build_audit.py and asserted on nowhere. Two independent assertions, not one: * repoint rule 6 — a `data-lectures` reference on media.githubusercontent.com. That host is the LFS media endpoint and routes per path, so it 404s plain-git files, and everything published here is plain git. * resolvability — ref is not `main`, path is not `lectures/`, or the file is not committed here yet. They cannot be folded together: a media URL parses to exactly the same (ref, path) as the raw URL beside it, so a resolvability check alone exits 0 on the media-host trap. A canonical url_form check is not available as a shortcut either — 25 of the 33 current data-lectures refs use the github.com/{org}/{repo}/raw/{ref}/ form, which is correct outside wasm. The rule-5 comment said the media host was a valid target for LFS files, which this makes false; PLAN's "not covered by CI" paragraph goes stale on the same change. Both updated here. Verified both ways. Today's tree: exit 0, zero warnings, on 33 data-lectures refs. A doctored lecture-wasm with four reads on the media host, one pinning `master` and one keeping the source subdirectory: exit 1 with each class named, and rule 5 fires zero times on it — the host trap is invisible to every other check in the file. Gate 1 of QuantEcon/workspace-lectures#23 step 3. Closes #54. Co-Authored-By: Claude Opus 5 (1M context) --- PLAN.md | 4 ++-- scripts/build_audit.py | 48 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/PLAN.md b/PLAN.md index ac6fc27..d21adeb 100644 --- a/PLAN.md +++ b/PLAN.md @@ -179,9 +179,9 @@ The plain-git decision does not *dissolve* the raw-vs-media trap for the repoint That must return nothing. The scoping is not cosmetic: the form quoted here before was `… repos/`, which could never pass, because it matched this document's own occurrences of the string. Any restatement of this check must exclude the rules that describe it. -This is **not** covered by CI. The strict audit's URL-form check catches only the `github.com/*/raw/` form, and only in `lecture-wasm` ([#48](https://github.com/QuantEcon/data-lectures/pull/48)). A media-host reference to a data-lectures path classifies as `pattern: data-lectures`, so it **passes the audit while 404ing at read time** — verified end to end: a tree with all 12 `.md` reads left on the media host and `migration.yml` flipped to `repointed` exits `--strict` with code 0. `scripts/build_audit.py:196` already computes `lfs_media` per reference and nothing asserts on it; closing that is the first gate on the fold. +This **is** covered by CI now, for the repos the audit scans. A reference classified `pattern: data-lectures` fails the strict audit if it is on `media.githubusercontent.com`, and separately if its `(ref, path)` is anything but `main` + `lectures/`, or if that file is not committed here yet. Those are two independent assertions on purpose: a media URL parses to exactly the same `ref` and `path` as the raw URL beside it, only the host differs, so neither check can stand in for the other. Before this, `scripts/build_audit.py` computed `lfs_media` per reference and asserted on it nowhere — a tree with all 12 `.md` reads left on the media host and `migration.yml` flipped to `repointed` exited `--strict` with code 0, verified end to end. -Note the three `_static/…/inequality/data.ipynb` reads are builder notebooks, which the audit does not scan as data reads at all *and* which the translation sync never carries (it is `.md`-only). They must be changed by hand in all three repos and checked by hand. +**What CI still cannot see is 9 of the fold's 21 reads.** `SCAN_REPOS` is the eight Python-family repos, so `lecture-intro.zh-cn`'s seven reads are outside it; `lectures/_static/**` is excluded by design, so the three `data.ipynb` copies are too. Those are builder notebooks, which the translation sync never carries either (it is `.md`-only), so they must be changed by hand in all three repos and checked by hand. The pre-merge grep in `lecture-python-intro` and `lecture-wasm` ([workspace-lectures#23](https://github.com/QuantEcon/workspace-lectures/issues/23) gate 2) does cover both of those repos' `_static` notebooks, which leaves `lecture-intro.zh-cn` as the only consumer with no automated check of any kind. ## Migration tracks diff --git a/scripts/build_audit.py b/scripts/build_audit.py index c17f985..673a700 100644 --- a/scripts/build_audit.py +++ b/scripts/build_audit.py @@ -459,8 +459,9 @@ def scan(repos_dir: Path): # Repoint rule 5 (PLAN): lecture-wasm executes code cells in the reader's # browser, where the github.com/*/raw/ redirect fails CORS before it is - # followed — wasm reads must use raw.githubusercontent.com, or - # media.githubusercontent.com for LFS files. Only code-cell reads are + # followed — wasm reads must use raw.githubusercontent.com. (The media + # host is CORS-clean too, but it is never a valid target for a + # data-lectures path — see repoint rule 6 below.) Only code-cell reads are # scanned, so {download} and prose links (navigations, CORS-exempt) can # never trip this. for d in datasets: @@ -472,6 +473,49 @@ def scan(repos_dir: Path): f"{r['url_form']} — fails CORS in the browser " f"(repoint rule 5)") + # Repoint rule 6 (PLAN): media.githubusercontent.com is the LFS *media* + # endpoint. It routes per path, not per repo — it serves a file only where + # that path is LFS-tracked in the repo the URL names, and 404s otherwise. + # Everything this repo publishes is plain git, so the media host is never + # a valid way to read a data-lectures path, from any runtime. This has to + # be its own assertion: a media URL parses to exactly the same (ref, path) + # as the raw URL beside it, so the resolvability check below cannot see + # it, and rule 5 above only looks at url_form prefixes in lecture-wasm. + for d in datasets: + for r in d["refs"]: + if r["pattern"] == "data-lectures" and r.get("lfs_media"): + mig_problems.append( + f"{d['file']}: {r['repo']} {r['lecture']} reads it via " + f"media.githubusercontent.com — the LFS media host 404s " + f"plain-git files, and everything published here is plain " + f"git (repoint rule 6)") + + # Does the URL resolve to a file this repo actually publishes? `pattern` + # is derived from org and repo alone (classify_url), so a repoint to a ref + # that does not exist, to a directory this repo does not use, or ahead of + # the bytes landing here, all classify as data-lectures and count as + # migrated. Each 404s for every reader while the audit stays green, so + # assert the two halves `pattern` does not carry: how the URL is spelled, + # and whether the file is in the published tree. + for d in datasets: + for r in d["refs"]: + if r["pattern"] != "data-lectures": + continue + where = f"{r['repo']} {r['lecture']}" + expected = f"lectures/{d['file']}" + if r.get("ref") != "main": + mig_problems.append( + f"{d['file']}: {where} pins ref {r.get('ref')!r} — this " + f"repo serves from main, and a stale ref 404s silently") + elif r.get("path") != expected: + mig_problems.append( + f"{d['file']}: {where} reads {r.get('path')!r}, but the " + f"published tree is flat — expected {expected!r}") + elif not (LECTURES / d["file"]).exists(): + mig_problems.append( + f"{d['file']}: {where} already reads it from data-lectures " + f"but {expected} is not committed here yet") + audit = { "generated": date.today().isoformat(), "repos": {n: {"sha": repos[n]["sha"],