- labs-validate-dev.yml: the Pyodide/WASM export smoke test imported the
pre-taxonomy-refactor path `from mlsysim.core.engine import Engine`. The
refactor moved Engine to mlsysim/engine/engine.py (exported at the package
top level). Use the canonical public import `from mlsysim import Engine`.
Fixes the "ModuleNotFoundError: No module named 'mlsysim.core.engine'" failure.
- LIVE_INTERVIEWER_PLAN.md: "pre-selects" -> "preselects" (codespell dictionary).
- Updated all GitHub Actions workflows (build-pdfs, preview-dev, publish-live, update-pdfs) to loop over `tutorial_module1` through `tutorial_module4` instead of the legacy `part1`/`part2`.
- Updated Quarto documentation (`slides.qmd`, `_quarto-html.yml`) to correctly surface the 4 new module PDFs on the website.
- Updated the tutorial Makefile to compile all 4 modules.
- Updated `.gitignore` and instructor quickstart documentation to reflect the new slide outputs.
projects.json was fixed in ae33e46984 to point at design-grammar
(renamed from periodic-table), but Book Validate's path filter only
watches book/** and book-validate-dev.yml, so the fix didn't auto-run
through a fresh Book Validate. Adding a comment-only line to the
workflow file to trigger the path filter once.
No behavioral change.
a57c6d7eb9 renamed periodic-table → design-grammar but missed the
contributor-generator SSOT. The Update Contributors job inside
Book Validate was failing because projects.py update-files emitted
'periodic-table/.all-contributorsrc' and 'periodic-table/README.md'
which no longer exist, causing 'git add -A' to fail (silenced by
|| true) before any contributor files got staged. Subsequent
'git commit' had nothing to commit and exited 1 under set -e.
After this fix update-files emits design-grammar paths that exist.
Both workflows grouped only on github.ref, so a manual workflow_dispatch
(or, for tinytorch, a workflow_call from publish-live) on the same SHA
as a push cancelled the push run via cancel-in-progress, leaving the
README badge red on a healthy SHA.
Switch both to the head_ref||run_id pattern already used by kits, mlsysim,
site, slides, and instructors: PRs still cancel on amend (head_ref shared),
push/dispatch/call get unique groups via run_id.
Aligns book-publish-live.yml with the rest of the publish workflows
(instructors, kits, labs, mlsysim, slides, staffml, tinytorch), which
all use:
site_only:
type: boolean
default: false
The book workflow was the lone outlier carrying:
skip_version:
type: choice
options: [no, yes]
default: 'no'
Behavior is unchanged: site_only=true skips the DOI rewrite, tag
creation, release-notes generation, and GitHub Release creation while
still running the full HTML/PDF/EPUB build and gh-pages deploy.
Changes:
- workflow_dispatch.inputs.skip_version → site_only (choice → boolean)
- workflow_call.inputs.skip_version → site_only (string → boolean)
- 5x `inputs.skip_version != 'yes'` → `inputs.site_only != 'true'`
- Inline comment update describing the skip-cascade guard
Per the project's release convention, workflow file changes must be
on main before any dispatch that uses the new input name. Merge dev
into main and push both branches before the next workflow_dispatch
that wants to set site_only=true.
mlsysbook.ai sits behind Cloudflare in front of GitHub Pages. After any
*-publish-live.yml workflow ships to gh-pages, the edge can serve stale
HTML (and stale 404s for new assets) until Cloudflare's TTL rolls over.
This was the underlying cause of today's staffml og-image issue: the
PNG shipped fine, but the edge kept returning the pre-deploy 404.
This workflow purges the mlsysbook.ai zone after any of the 9
*-publish-live.yml workflows completes successfully. One workflow file,
zero touches to the existing 9 deploy workflows.
Design notes:
- workflow_run trigger, not push:gh-pages — GitHub Actions deliberately
suppresses workflow triggers from commits made with the default
GITHUB_TOKEN (anti-recursion guard), and all 9 deploys use it. So
push:gh-pages would never fire. workflow_run chains off each
upstream workflow's completion event, which sidesteps that.
- purge_everything (zone-wide), not purge_by_url — Cloudflare's
purge-by-hostname mode is Enterprise-only. The Pro/Free tiers offer
"purge everything" or "purge up to 30 URLs". Maintaining a hand-
curated 9-product URL list is more error-prone than the cost of
re-warming the zone; content is mostly static and re-caches on
first request.
- vars.CLOUDFLARE_ZONE_ID, not a new secret — Zone ids aren't
sensitive (they're returned in plain text by any Cloudflare API call
on the zone). Stored as a repo variable, configured once.
- Reuses secrets.CLOUDFLARE_API_TOKEN already configured for
staffml-publish-live.yml's D1/Worker shipments. The token needs
Zone.Cache Purge permission on the mlsysbook.ai zone — verify
scope when the workflow first runs.
- Concurrency group "cloudflare-purge" with cancel-in-progress: false
serializes purges so a burst of back-to-back deploys collapses into
a trailing purge instead of racing the API.
To activate after merge:
1. Merge to dev, then dev → main (workflow_run only watches the
default branch — won't fire until on main).
2. Set vars.CLOUDFLARE_ZONE_ID under Settings → Secrets and
variables → Actions → Variables.
3. Confirm the existing CLOUDFLARE_API_TOKEN has Zone.Cache Purge
scope (it should, since the D1/Worker steps need it).
4. Next deploy will trigger an automatic purge; verify in the
Actions tab.
Live deploy mounts _build/ under /tinytorch/, so the meta tag's
hardcoded /tinytorch/release-manifest.json resolves correctly.
Visual-smoke serves _build/ at the root, so it needs the file at
_build/tinytorch/release-manifest.json to match the same path.
Mirror the manifest there after emit so smoke's HTTP server finds it.
The release-pill JS fetches /tinytorch/release-manifest.json at runtime.
Live publish-live emits it; preview-dev didn't, so every preview build
404'd on the manifest and visual-smoke caught it as a console error on
all 16 page/viewport combinations.
Stamp a dev manifest (release-id = 0.0.0-dev+<sha7>) right after
quarto render so the pill renders cleanly and smoke goes green.
The gh-pages deploy step was issuing a blanket cleanup of "stale" root files
that in fact belong to the landing page deployed by site-publish-live.yml:
rm -rf contents/ assets/ site_libs/ tools/
rm -f _redirects netlify.toml search.json sitemap.xml site.webmanifest 404.html
The mlsysbook.ai/ landing page references /assets/, /site_libs/, /404.html,
/search.json, /sitemap.xml, /site.webmanifest at root. Wiping them on every
vol1/vol2 deploy left the landing rendering as unstyled HTML (every CSS/JS
404'd) until the next site-publish-live run repopulated root.
This was the cause of the 2026-05-13 mlsysbook.ai outage: commit fdea8fb017
on gh-pages ('Deploy release vol1-v0.6.1+vol2-v0.1.1 to /vol1/ + /vol2/')
deleted 59 root files and the landing page was broken from then until now.
Fix: restrict the cleanup to paths that are genuinely pre-subdir-migration
artifacts (contents/, tools/, _redirects, netlify.toml). Everything else at
root is owned by site-publish-live.yml and must be preserved. Comment block
above the cleanup names the protected set explicitly so this doesn't
regress.
The first skip_version=yes run (25830710777) reported success at the workflow level but actually skipped all the build and deploy jobs. Trace of the cascade:
update-version SKIPPED (skip_version=yes)
merge-to-main SUCCESS (always() guard)
trigger-production-build SKIPPED ← unintended
call-production-build SKIPPED ← unintended
download-and-deploy-artifacts SKIPPED ← unintended (no rebuild!)
Root cause: GitHub Actions' implicit skip propagation. When merge-to-main runs under always() with a skipped predecessor, its 'success' result does not act like a normal success for downstream jobs — they still inherit the skip from update-version transitively unless they also use always().
Add explicit always() + result-checks to the three build/deploy jobs:
- trigger-production-build: runs when merge-to-main is success
- call-production-build: runs when merge-to-main AND trigger-production-build are success
- download-and-deploy-artifacts: runs when validate-inputs AND call-production-build are success
After this fix, skip_version=yes does what the input description says: skips the version bump and the tag/release machinery, but builds HTML+PDF+EPUB and deploys to gh-pages exactly like a normal publish.
When a tweak ships within a day or two of a numbered release, bumping the version again is the wrong signal — the change isn't a new minor or patch, it's content errata on the just-published version. Currently the only way to redeploy is to let the workflow bump the version, which creates a misleading v0.6.1 right after v0.6.0.
Adds a new workflow_dispatch input skip_version (default 'no'). When set to 'yes':
- No-downgrade guard step in validate-inputs is skipped.
- update-version, create-tag, generate-release-notes, and create-release jobs are all skipped.
- merge-to-main, build, and deploy-artifacts run normally so HTML/PDF/EPUB are regenerated from the current dev tip and pushed to gh-pages at the existing version path (/vol1/, /vol2/).
- summary tolerates the skipped create-release via always() + result-check.
Mirrors the spirit of tinytorch-publish-live's site_only flag, but with full-build semantics (not website-only) since the book's PDF + EPUB also reflect content changes.
Mirror-added to the workflow_call input set so reusable callers can opt in.
The UPDATE_VERSION job bumps settings.ini, pyproject.toml, install.sh, and the README badge, but did not touch the chip literal in tinytorch/quarto/assets/scripts/sidebar-subtitle.html. The v0.1.10 → v0.1.11 release on 2026-05-11 surfaced the gap: the chip remained at v0.1.10 while every other surface advanced to v0.1.11.
Adds a dedicated step that:
- Uses the TINYTORCH_VERSION_CHIP anchor comment already on the chip line as a sed address so we touch exactly one line.
- Substitutes the v\d+\.\d+\.\d+ token in place.
- Adds sidebar-subtitle.html to the existing 'Commit version update' git add list so the change ships in the same auto-bump commit alongside settings.ini and friends.
This keeps tinytorch/settings.ini the single source of truth: every other version surface (installer, README badge, navbar chip) is mechanically derived from it at publish time.
The companion build-slides job was bumped to 40 min on 2026-05-12 with a
comment explaining apt-install of the texlive stack reliably overruns
shorter timeouts on ubuntu-latest. The symmetric fix on build-paper was
missed.
Result: mlsysim-publish runs have been Russian roulette since
2026-05-12, succeeding when apt finishes under 10 min and cancelling at
the boundary when it doesn't. 2026-05-13 run 25815971246 cancelled here
at exactly 9 min 57 sec into apt — diagnosis confirmed.
30 min matches the apt headroom logic used on build-slides (paper apt is
smaller than slides apt, hence 30 not 40).
Follow-up to 65c27263d8 (the big xelatex→pdflatex move). That commit
migrated slides-build-pdfs.yml but missed slides-publish-live.yml,
which has its own INLINE build step (independent of slides-build-pdfs)
that compiles every chapter deck and packages the PDFs as the
GitHub Release artifact.
Switch the inline build's xelatex → pdflatex. Drop texlive-xetex
(engine no longer needed) and texlive-fonts-extra (was for tex-gyre,
not needed with helvet/courier). Drop apt-get update (defensive; the
runner image's apt cache is current within ~1 week).
Switches 35 chapter decks (slides/vol1 + slides/vol2) and 2 mlsysim
tutorial decks (mlsysim/tutorial/slides/tutorial_part{1,2}.tex) from
xelatex+fontspec+texgyreheros to pdflatex+helvet+courier.
Why
---
Yesterday's slides-publish-live shipped PDFs with completely blank text
(see https://github.com/harvard-edge/cs249r_book/releases/download/
slides-latest/vol1_00_course_overview.pdf — Liberation Sans was
embedded but glyphs were missing because xelatex on Ubuntu CI could
not find texgyreheros.otf and silently fell back). The mlsysim
tutorial decks fared no better, requiring four rounds of fixes
(Helvetica Neue → texgyreheros → tex-gyre apt + mktexlsr → Latin
Modern) before they would even compile.
The root cause is engine choice: fontspec on xelatex makes runtime
OS-font discovery the build's critical path. If the host TeX install
or fontconfig doesn't have the requested font, fontspec either errors
hard (mlsysim's experience) or silently substitutes (slides' silent
broken-PDF failure mode). Either way the build is fragile.
paper.tex has been using pdflatex+helvet+mathpazo+courier for months
without a single font issue. Bringing slides + mlsysim tutorial onto
the same engine + font convention eliminates the entire failure class.
Changes
-------
* 35 chapter decks: replace
\usepackage{fontspec}
\setsansfont{texgyreheros}[Extension=.otf, ...]
\setmonofont{texgyrecursor}[Extension=.otf, ...]
with
\usepackage[T1]{fontenc}
\usepackage[scaled=0.9]{helvet}
\usepackage{courier}
\renewcommand{\familydefault}{\sfdefault}
Same Helvetica-class look (NimbusSans = URW's Helvetica clone in
helvet), shipped via texlive-fonts-recommended Depends.
* 2 mlsysim tutorial decks: same swap (was Latin Modern from the prior
retry).
* slides/Makefile: LATEX := xelatex → pdflatex.
* mlsysim/tutorial/Makefile: same.
* slides-build-pdfs.yml: drop texlive-xetex (engine), drop
texlive-fonts-extra (was for tex-gyre). Result: smaller apt-install,
no fontspec silent-fallback class of bugs.
* mlsysim-build-pdfs.yml: same engine + apt cleanup; drop JetBrains
Mono manual fc-cache step (we no longer reference JetBrains Mono).
Bar copy
--------
* mlsysim/docs/config/announcement.yml: append Lecture Slides to the
"Alongside the book" row — the mlsysim tutorial deliverable IS slides,
so cross-linking is natural.
* site/config/announcement.yml: drop the "🎓 Teach with it: Lecture
Slides · Instructor Hub" line entirely from the landing bar. Landing
is for learners; teachers find Instructor Hub via the navbar Teach
menu, and Slides via Instructor Hub.
Verification
------------
Local Mac builds with pdflatex:
- vol1/01_introduction: 52 pages, NimbusSans-Regu/Bold embedded
- vol2/05_distributed_training: 48 pages, NimbusSans embedded
- mlsysim tutorial_part1: 90 pages, NimbusSans embedded
- mlsysim tutorial_part2: 47 pages, NimbusSans embedded
All Type 1, sub-set, Unicode tag yes — renders cleanly.
The book-publish-live workflow had a fallback block that wrote a
meta-refresh stub to gh-pages root index.html whenever it ran. On any
non-"all" deploy the stub was written if index.html was missing; on
"all" deploys the stub was written unconditionally when the (wrong-
path) LANDING_BUILD wasn't found.
In practice this clobbered the real landing page deployed by
site-publish-live.yml — including just now, where mlsysbook.ai/ was
serving a redirect to /vol1/ instead of the rendered site/ landing.
The root index.html is now solely owned by site-publish-live.yml.
The 'Emit release manifest' step in mlsysim-publish-live was hashing
`mlsysim/python` as the python package source path, but the package
actually lives at `mlsysim/mlsysim/` (confirmed via
mlsysim/pyproject.toml's `packages = ["mlsysim"]` and the
__init__.py present at mlsysim/mlsysim/__init__.py).
The 2026-05-12 publish run reached the release-manifest emit step
(after the lean apt and Latin Modern fixes got the build through)
and failed with:
FileNotFoundError: input path does not exist: mlsysim/python
Fix the path to mlsysim/mlsysim. Same hash mechanism, same fingerprint
inputs — just pointing at where the package actually is.
Reverts the paper-side changes from 2eb2b8f28 (timeout 10→30) and
8cf6c227a (apt strip: drop texlive-science + apt-get update). The
paper PDF build was never actually broken — it had been working fine
for months under the original 10-min timeout with the full apt list.
What actually happened on 2026-05-12: the slides side had a real bug
(Helvetica Neue / texgyreheros lookup on Ubuntu) which I went deep
fixing. Several iterations of dispatches loaded the runner pool
heavily; one paper attempt got slow enough during apt-install to
brush against the 10-min timeout. I read that as a paper bug and
bumped its timeout, then stripped its apt list 'for good measure'.
Both were over-engineering — the paper compiles fine at original
config under normal runner conditions. Reverts:
- timeout-minutes: 30 → 10 (back to original)
- apt list: add back texlive-science + apt-get update -qq
- install style: back to -y -qq with > /dev/null (matching original)
The slides-side fixes (Latin Modern font swap + slim apt list)
stay — those addressed a real Ubuntu packaging quirk.
paper.tex is plain pdflatex with mathpazo (Palatino) + helvet
(Helvetica) + courier — all standard PostScript fonts in
texlive-fonts-recommended. The actual graphics/citation packages it
uses (tikz, pgfplots, subcaption, natbib, hyperref, cleveref,
microtype, fancyhdr, titlesec, etc.) all live in texlive-latex-extra
or texlive-latex-recommended.
A grep for the texlive-science-specific packages (physics, siunitx,
chemnum, chemfig, circuitikz, mhchem, braket, esint, isotope, etc.)
shows ZERO matches in paper.tex. texlive-science was being installed
for ~50-100 MB of dead packages.
Drop it. Also drop apt-get update (defensive against a stale package
cache, but the GHA runner image's apt cache is current within ~1 week
and these texlive packages are stable). Add texlive-latex-recommended
explicitly (was being pulled transitively, but explicit is better).
Expected effect: paper apt-install falls from ~25 min (the recent
2026-05-12 timeout-hitter) to ~3-5 min on a fresh runner. The actual
pdflatex × 3 + bibtex compile is ~30s.
After the Latin Modern font swap (commit 6678b7abc), the slides job no
longer needs:
texlive-fonts-extra (629 MB) — was the home of texgyreheros/tex-gyre
tex-gyre (~5 MB) — Suggests we explicitly pinned for texgyreheros
fonts-liberation — Liberation Sans fallback we never used
mktexlsr post-step — was the workaround for the missing fonts
apt-get update — defensive; runner image's cache is current
Latin Modern lives in `lmodern` (already installed) and `texlive-base`
Depends, so xelatex finds it without any of the above. Drop them all.
Expected effect: apt-install on build-slides falls from ~10 min to
~2-3 min on a fresh runner. Job timeout-minutes left at 40 as headroom
during the first few runs; can drop to 20 once verified.
This is the small/medium-impact half of the longer-term plan (full
tectonic migration would be the bigger one, deferred).
The Latin Modern font swap fixed the slides side of mlsysim-publish-live,
but the parallel build-paper job is now the bottleneck — its 10-min
timeout was hit mid 'Install LaTeX dependencies' on the 2026-05-12 publish
retry, cancelling the whole pipeline (Build & Deploy skipped because
build-paper cancelled).
Same root cause as the earlier slides-side timeout bump (20→40): the
texlive-latex-extra + fonts-recommended + science + poppler-utils
combo can consume the full 10 min in apt-install alone on a fresh
runner. Bump to 30 — gives apt the room it needs without inviting
runaway jobs (real pdflatex build is ~2 min).
The 2026-05-12 retries of mlsysim-build-pdfs were cancelled at the 20-min
mark, all of them inside the 'Install TeX Live (xelatex)' step before
mktexlsr or the actual build had a chance to run.
texlive-fonts-extra alone weighs 629 MB; combined with
texlive-{xetex,fonts-recommended,latex-extra,latex-recommended,base},
tex-gyre, fonts-liberation, and the post-install dpkg triggers, the
apt-install can comfortably consume 15 min on its own. Add another
~5 min for tlmgr metropolis install, JetBrains Mono fetch, inkscape
SVG→PDF on a dozen figures, and two xelatex runs per deck (twice for
cross-refs), and 20 min becomes a hard ceiling rather than a runaway
guard.
Bump to 40 min. Bounds runaway jobs but gives apt + xelatex actual
working room. Sustainable long-term fix is to factor the TeX install
into a reusable composite action so the cost amortizes via runner
cache (see follow-up).
Even with tex-gyre explicitly added to the apt install list, xelatex
still errored with 'The font texgyreheros-regular cannot be found'.
The package was installing the .otf files but kpsewhich's ls-R cache
wasn't being refreshed under --no-install-recommends — the dpkg
trigger that runs mktexlsr post-install ships as part of
texlive-binaries' Recommends chain, and --no-install-recommends
filters out the trigger context.
Run mktexlsr explicitly after the apt install. Adds ~2s to the build
and turns the failed 'cannot be found' lookup into a successful
file resolution.
The publish workflow had a step that wholesale-rewrote
tinytorch/quarto/config/announcement.yml on every release with a
release-only banner ("🎉 vX.Y.Z released — ..."), silently dropping
the 4-line ecosystem cross-link template the file holds. The
regression hit v0.1.10 (commit 0009f55a9, 2026-04-22) and again
v0.1.11 (2026-05-11), each time requiring a hand-restore.
Remove the step. The release version is already surfaced on the
deployed site via:
- footer release pill (rendered at runtime from release-manifest.json)
- navbar version chip (templated from tinytorch/settings.ini)
so a third release-version surface in the announcement bar isn't
needed. If a future release warrants a top-of-bar mention, the
right pattern is to *prepend* a transient `release_note` line to the
existing content (ruamel.yaml round-trip on the same file) rather
than rewriting the whole document. Inline comment in the workflow
documents this choice so the next maintainer doesn't reintroduce
the regression.
Also drops tinytorch/quarto/config/announcement.yml from the version-
update commit's git-add list since this file is no longer modified by
the workflow.
The tutorial decks use the canonical lecture-deck font pattern
(\setsansfont{texgyreheros}[Extension=.otf, UprightFont=*-regular,
...]) which routes through kpsewhich on the texmf tree. On Ubuntu the
matching .otf files ship in the standalone `tex-gyre` apt package,
which is only a *Suggests* of texlive-fonts-extra — never a Depends.
The mlsysim-build-pdfs workflow uses --no-install-recommends, which
filters out both Recommends and Suggests, so tex-gyre was never
installed. xelatex then failed with:
! Package fontspec Error: The font "texgyreheros-regular"
cannot be found.
Pin tex-gyre into the apt install list explicitly. Leaves the .tex
preambles on the canonical lecture-deck pattern (which works on macOS
TeX Live for the same kpsewhich reason). Mac local build verified
producing 47-page PDFs with TeXGyreHeros + TeXGyreCursor embedded.
The Cloudflare worker deploy step (Install worker deps + deploy) runs
`wrangler` to publish the vault worker. wrangler@4.87 dropped support
for Node 20 and refuses to start with:
Wrangler requires at least Node.js v22.0.0.
You are using v20.20.2.
This failed the worker step on every staffml publish since the wrangler
bump, leaving the live worker on stale code even when the Next.js site,
D1 ship, and /interviews/ redirect all deployed successfully.
Hardcode NODE_VERSION='22' in the staffml workflow env block. The shared
vars.NODE_VERSION repo variable (=20) is intentionally left alone so
other workflows (tinytorch builds, labs/socratiq validate, etc.) stay
on the older LTS until each is individually verified on 22.
Each subsite now delegates to its own *-publish-live.yml via uses: instead
of duplicating the build/deploy steps. Five previously-inline jobs
(kits/tinytorch/mlsysim/labs/staffml) and the gh-CLI-dispatched book job
all become workflow_call invocations. Slides/site/instructors stay as
workflow_call (instructors gained the trigger in the prior commit).
Net effect:
- staffml deploy now includes vault-cli build, type check, tests, D1 ship,
Cloudflare worker deploy, and /interviews/ redirect (previously skipped)
- kits/tinytorch/mlsysim/labs all get release manifest + tag + GitHub
Release on a versioned dispatch
- tinytorch dev→main sync + version bump now runs (previously skipped)
Also:
- Drop the workflow-level concurrency: gh-pages-deploy block. With it,
parent held the lock for the entire run while every child's deploy job
declared the same group on its own job — deadlock. Child-level locks
are sufficient and correctly serialize across subsites.
- Drop actions: write from permissions (no longer needed; we no longer
dispatch via gh CLI).
- Add shared site_only (default true) and description inputs so a single
"publish all" run propagates the same release knobs to every subsite.
- Default deploy_book to false — vol1/vol2 are in active development and
the book release is heavier; opt in explicitly.
File shrinks from 393 → 256 lines.
Adds workflow_call: alongside the existing workflow_dispatch: trigger on
book/instructors/kits/labs/mlsysim/staffml/tinytorch publish-live so the
publish-all-live orchestrator can invoke them via uses: rather than
duplicating their build/deploy logic inline.
workflow_call doesn't support type: choice, so release_type / testing_mode
/ deploy_target / ai_generated_notes drop to type: string when called.
Defaults match the dispatch defaults.
book and tinytorch additionally have github.event.inputs.X refactored to
inputs.X (106 + 21 sites) so the same expressions work under both
triggers — github.event.inputs is empty when invoked via workflow_call.
site and slides already had workflow_call and are untouched.
- Workflow: default contents:read for jobs without explicit permissions
- audit_math_rendering: match </script> and </style> with optional whitespace
- SocratiQ: tooltip via DOM APIs; iterative HTML stripping; quote normalization
for JSON repair; repeat-until-stable tag cleanup in SR/copy flows
- Rebuild bundle.js for Quarto embed path
Brings in the dev-side prose / bib / math fixes that landed since the
yaml-audit branch was cut, and resolves three small conflicts:
* interviews/vault-cli/scripts/archive/split_corpus.py
origin/dev deleted it (archive cleanup); we honor the deletion.
* interviews/vault-cli/scripts/validate_drafts.py
origin/dev removed a leftover no-op statement; took theirs.
* interviews/vault-cli/scripts/summarize_proposed_chains.py
origin/dev renamed loop var lvl→level; took theirs.
The two protected qmds (data_selection.qmd, model_compression.qmd)
are temp-stashed before the merge to honor the 'do not touch' rule;
restored after the merge commit lands.
After this commit, yaml-audit contains every commit on origin/dev as
an ancestor, so dev can fast-forward to yaml-audit's tip when the
maintainer is ready to merge.
The path filter included `book/**` plus the two workflow YAMLs, then
`!tinytorch/**` as an exclude. The exclude was always a no-op:
tinytorch/ lives at the repo root (/tinytorch/), not under /book/, so
the `book/**` glob never matched anything in tinytorch in the first
place. GitHub's `paths`-with-`!` syntax is also strict about ordering —
an exclude only matters if a prior include would have matched, which
isn't the case here.
Removing the dead line tightens the filter to its actual semantics
(any change under book/ or to validate-dev.yml/preview-dev.yml triggers
the workflow) and prevents future-confusion about whether tinytorch
edits are gated by this workflow (they are, but via tinytorch-validate-dev,
not this one).
The push paths only listed content paths (interviews/staffml/**,
vault questions/chains/schema). When a CI fix landed in any of the
three staffml-* workflow files themselves, the preview-dev workflow
didn't auto-trigger on the push that fixed it — leaving the README
badge stuck on the previous (red) push run until someone happened
to push an unrelated change to interviews/staffml/.
Surfaced this hour: the concurrency-group fix in 2a61ece3f corrected
the actual workflow_call cancellation bug, but the badge stayed red
because that fix only touched .github/workflows/staffml-validate-*.yml.
Add the three workflow file paths to the push trigger so a CI-only
fix re-runs the preview pipeline and updates the badge directly.
Both reusable workflows used `group: ${{ github.workflow }}-...`, but
when GitHub runs a workflow via `workflow_call`, github.workflow resolves
to the CALLER'S workflow name. So when staffml-preview-dev calls both
staffml-validate-dev and staffml-validate-vault via `uses:` from the
same parent run, the two reusable workflows collapsed into the same
concurrency group (parent-name + parent-run-id). With
`cancel-in-progress: true`, whichever queued first got cancelled by the
later one.
Concretely, on every push run since 6ddb82a71b (2026-05-02):
- Validate (Vault) jobs queue at parent+~3s with no runner assigned
- Validate (Dev) jobs queue at parent+~5s
- Vault jobs cancel ~1s later (cancel-in-progress fires when the
second occupant of the shared group enters)
Net effect: vault validation never ran but the StaffML preview-dev run
overall reported 'cancelled', flipping the README badge red despite
build + Validate (Dev) all green. 9 push runs in a row affected.
Fix: replace ${{ github.workflow }} with a literal workflow-identifying
string in each group key so the two reusable workflows live in disjoint
groups regardless of caller. The fallback to head_ref/run_id is kept,
so PR cancel-on-amend and standalone-vs-uses uniqueness still work.
Tested by dispatching staffml-validate-vault standalone before this
commit (run 25351824595): both jobs ran cleanly to success, confirming
the failure was purely the concurrency interaction between the two
reusable workflows in the same parent, not anything in the validation
logic itself.
Cold container build is ~60–90 min on a GHA runner. When an external
URL the build needs is dead (Inkscape PPA outage, CRAN mirror flap,
historic 2025 tlnet repo, GitHub releases for the Quarto .deb), the
failure currently surfaces 30+ min in — half a runner-hour wasted per
attempt. Preflight catches these in <30 s before the docker build job
starts.
Two pools, deliberately different gates:
- Required URLs (Inkscape PPA, CRAN pubkey + InRelease, Quarto .deb,
Utah historic 2025 tlnet tlpdb): every one must return 200. These
have no in-script fallback — a dead one will fail the build no
matter how many retries the Dockerfile attempts.
- TL install-tl mirror pool (mirror.ctan + 4 university mirrors):
install-texlive-base.sh already iterates and falls through on
failure, so the gate requires ≥3 of 5 alive — strict enough to
catch a wide outage, loose enough not to fail on one flaky mirror.
Probes run via xargs -P 8 in parallel; whole job is ~10 s wall-clock.
build job declares needs: preflight, so a preflight failure leaves the
expensive build job in skipped state instead of consuming runner time.
Auth-gated endpoints (ghcr.io, mirror.gcr.io) are intentionally not
probed — they return 401 unauth and are already validated by the
existing 'Check registry access' step inside the build job.
Phase 8 deliverable: runs audit_corpus_batched.py against the full
corpus on the 1st of each month at 14:00 UTC, plus workflow_dispatch
for ad-hoc operator-triggered runs.
Job:
- Set up Python 3.12, install vault-cli
- vault check --strict + pytest (sanity)
- Run audit_corpus_batched.py --all --workers 4 --max-calls <input>
(cron uses 250; manual lets operator override)
- Run summarize_audit.py to emit AUDIT_FINDINGS.md
- Upload _pipeline/runs/cron-<UTC>/ as a 90-day-retention artifact
Activation requirements (documented in YAML comments):
1. Repository secret GEMINI_API_KEY must be provisioned
2. Runner image must have gemini CLI installed (or script adapted
to Python SDK)
Until those land, the workflow YAML is in place but workflow_dispatch
fails clearly on the gemini-CLI-missing check. Activation is a
one-line change once the auth path is decided.
TODO (deferred to a follow-up): regression-comparison vs. last
month's artifact + gh issue create on threshold breach. Skipped on
the first run (no baseline). The skeleton is documented inline.
CORPUS_HARDENING_PLAN.md Phase 8.
The one-shot sync-area-labels.mjs script already migrated existing issues
from area: interview to area: staffml and added area: mlsysim. This commit
aligns the *recurring* surfaces with that migration:
- auto-label.yml: replace path-prefix rules with the same 11-rule table
used by sync-area-labels.mjs (interviews/ → staffml, mlsysim/ → mlsysim,
add tools/ + site/, drop redundant book/tools/).
- ISSUE_TEMPLATE/interview_question.yml: default new issues to area: staffml
(was area: interview).
- release.yml: add 📈 MLSysim and 🎓 StaffML changelog sections so the
area labels surface properly in generated release notes.
Sync the yaml-audit branch with the latest dev work since the previous
sync (5c5af75ed). Brings in 73 commits including:
- CI security fixes: postcss XSS bump, uuid bounds bump, codeql
paths-ignore for vendored bundles, read-only token on
staffml-validate-vault workflow
- kits/ dark mode polish: code-block readability, dropdown contrast
- vault-cli/: pre-commit ruff hook + 20 ruff fixes, all-contributors
auto-credit workflow change to pull_request_target
- dev's earlier merge of yaml-audit (836d481b5) carrying the
pre-trailer-strip Phase 1/2/3 history; this merge harmonises that
with the current trailer-clean yaml-audit tip
- misc bug fixes (tinytorch perceptron seed, infra workflows,
socratiq vite dev injector)
Conflicts resolved (if any) preserve the yaml-audit-side authoritative
state for vault/* files (we own those) and the dev-side authoritative
state for .github/workflows/* and other shared infrastructure.
# Conflicts:
# .github/workflows/all-contributors-auto-credit.yml
# .github/workflows/staffml-preview-dev.yml
# interviews/staffml/src/data/corpus-summary.json
# interviews/staffml/src/data/vault-manifest.json
# interviews/staffml/tests/chain-and-vault-smoke.mjs
# interviews/vault-cli/README.md
# interviews/vault-cli/docs/CHAIN_ROADMAP.md
# interviews/vault-cli/scripts/build_chains_with_gemini.py
# interviews/vault-cli/scripts/generate_question_for_gap.py
# interviews/vault-cli/scripts/merge_chain_passes.py
# interviews/vault-cli/scripts/validate_drafts.py
# interviews/vault-cli/src/vault_cli/legacy_export.py
# interviews/vault-cli/tests/test_chain_validation.py
# interviews/vault/.gitignore
# interviews/vault/ARCHITECTURE.md
# interviews/vault/chains.json
# interviews/vault/id-registry.yaml
# interviews/vault/questions/edge/optimization/edge-2536.yaml
# interviews/vault/questions/mobile/deployment/mobile-2147.yaml
# tinytorch/src/03_layers/03_layers.py