LinkedIn (and most og:image consumers) silently reject SVG og:images and
fall back to scraping the first prominent <img> on the page, which was
the Harvard SEAS shield in the EcosystemBar. Result: every staffml
share on LinkedIn showed the Harvard logo instead of the StaffML brand.
Changes:
- Rasterize public/og-image.svg to public/og-image.png at 1200x630
- Point openGraph.images and twitter.images at the new PNG
- Add explicit type: "image/png" on the openGraph image entry
- Change metadataBase from https://staffml.ai (shortname) to
https://mlsysbook.ai (canonical), so og URLs resolve absolutely to
the asset's real serving location
- Use process.env.NEXT_PUBLIC_BASE_PATH for the image URL, matching
the existing pattern at layout.tsx:87 for theme-bootstrap.js. In
production (NEXT_PUBLIC_BASE_PATH=/staffml) the og image resolves
to https://mlsysbook.ai/staffml/og-image.png
The SVG file is left in place for now (harmless; can be removed later).
After deploy, run the URL through linkedin.com/post-inspector to bust
the ~7-day OG cache.
The Quarto navbar's theme toggle now collapses into the hamburger menu at narrow widths alongside Subscribe / Star / Support / GitHub (via shared/config/site-head.html JS, prior commit). StaffML's EcosystemBar component had its own theme-toggle button in the desktop right-cluster, but the mobile-menu panel never included it — so on narrow viewports the theme toggle was hidden entirely behind the hamburger with no way to reach it.
Adds a Theme button at the end of the mobile-menu's right-actions row, after GitHub, matching the order users see when the navbar is expanded. Uses the same toggleTheme() callback the desktop button binds to, and closes the mobile menu on click for consistency with the other items in that panel. Sun / moon icon mirrors the desktop button.
flex-wrap on the parent row so five items (Subscribe / Star / Support / GitHub / Theme) wrap cleanly on narrow phones instead of overflowing horizontally.
The shared/config/navbar-common.yml right-side group was reordered to put the newsletter Subscribe button first (commit 9e6f4bc0e4). StaffML's EcosystemBar is a separate Next.js component that hand-mirrors the same items, so it needs the same reorder to stay pixel-matched across the curriculum.
Updates both:
- Desktop horizontal layout (xl+) at the right side of the bar
- Mobile collapsed layout (below xl) inside the hamburger panel
Mobile responsiveness is preserved: the items use existing flex layout, no positional CSS or nth-child selectors that depend on the old order. The only change is the order of the four <a> elements.
The previous fix made `vault build` always emit vault-manifest.json so
CI publishes stop shipping releaseId="dev". That worked for releaseId
but broke the staffml smoke test:
assert manifest["questionCount"] == len(corpus-summary)
The bundled corpus-summary.json is hand-maintained and lags behind the
live vault YAMLs (9521 vs 9525 right now). Counting `loaded` made the
manifest claim 9525 published questions while the Next.js bundle ships
9521 — they describe different artifacts and the assertion broke.
Read published_count from corpus-summary.json when it exists, falling
back to `loaded` for fresh checkouts that haven't generated the bundle
yet. The manifest now describes what the site actually ships.
chainCount stays computed from `loaded`: corpus-summary.json does not
carry chain memberships (chains live in vault.db, served by the Worker),
and the historical contract for chainCount has been "vault state at
build time" — silently dropping it to 0 would mislead consumers.
Verified locally: vault build --release-id "0.99.0-test" now writes
questionCount=9521 (matches bundled corpus) and chainCount=843 (from
loaded), and the smoke-test invariant
`manifest.questionCount == len(corpus-summary)` holds.
The manifest emission was previously inside an `if local_json:` block,
meaning CI publishes — which call `vault build --release-id X.Y.Z`
without `--local-json` — would never refresh the manifest. The committed
file (with releaseId="dev" from a developer's local default-flag build)
would survive the CI pipeline and ship to gh-pages, leaving the staffml
footer pill stuck on "dev" indefinitely.
Move the manifest emission out of the local-json gate. It now runs on
every build, stamping the manifest with whatever release_id the CLI was
invoked with. The corpus.json + visual-asset mirroring stay correctly
gated behind --local-json (those are local-dev-only artifacts).
Derive published_count directly from `loaded` instead of from
`local_result["count"]` so it's available outside the local-json branch.
Verified locally: `vault build --release-id "0.99.0-test"` now writes
`releaseId: "0.99.0-test"` with a fresh buildDate. Without this fix the
file retained its committed `releaseId: "dev"` regardless of the flag.
Chains referenced cloud-2241, cloud-2304, cloud-2559, cloud-4550 while
those YAMLs were still status:draft, which broke apply_proposed_chains
--dry-run and StaffML vault CI. Mark them published so the registry and
corpus stay consistent.
Next.js auto-generates interviews/staffml/next-env.d.ts on every
`next dev` and `next build`. Under Next 16 + Turbopack the two modes
write different import lines:
next dev → import "./.next/dev/types/routes.d.ts";
next build → import "./.next/types/routes.d.ts";
So every commit made after running the other mode flipped the line.
git log -p on this file shows it ping-ponging across commits as a
constant noise generator, with all 7 active worktrees regularly
showing it as modified for no real reason.
Next.js documentation says to commit next-env.d.ts, but that guidance
predates the dev/prod path split. A lot of Next.js 16 projects have
moved to gitignoring it for exactly this reason; the file regenerates
on first `next dev` or `next build` in a fresh checkout, so the only
cost is one extra command before TypeScript can typecheck.
Changes:
- Add `next-env.d.ts` to interviews/staffml/.gitignore with a comment
explaining why (so a future maintainer doesn't re-track it).
- `git rm --cached` the file. The local working-tree copy is preserved
in every existing worktree; status just stops flagging it.
The "going-forward" LFS rules in .gitattributes were creating
phantom-modification noise on every PDF and MP3 that was committed
before the rules existed. The LFS clean filter would compute a
133-byte pointer in memory that never matched the raw blob in HEAD,
so 26 PDFs and 2 MP3s permanently showed as `M` in `git status`.
Pre-commit's stash mechanism then crashed on those phantom-dirty
binaries (today's 00_tinytorch.pdf hook failure being the latest
symptom).
The set of files actually backed by LFS was small: 11 paper figures
totalling ~540 KB, all derivable from SVG sources committed alongside
them. The bandwidth/storage benefit of LFS for that scope is
negligible, and the operational cost (LFS hooks, smudge/clean filter
surprises, collaborator setup) is real.
Changes:
- Drop all 9 *.{pdf,epub,mp3,wav,m4a,mp4,mov,webm,wasm} LFS rules
from .gitattributes; keep the text-handling normalization.
- Re-stage the 11 LFS-tracked paper figures via `git add --renormalize`,
converting them from 133-byte pointers to raw blobs in HEAD.
Past commits in history retain LFS pointers for those 11 figures.
Anyone needing to check out a pre-this-commit revision and rehydrate
the binaries still needs git-lfs installed; new clones at this tip
or later don't.
Future direction: the paper figures are build artifacts derivable from
their SVG sources. A follow-up should add them to .gitignore and have
the paper-build workflow regenerate them on demand.
The /about page prerender was failing in `next build` (Turbopack):
SyntaxError: missing ) after argument list
Root cause: corpus.ts had a user-facing error message with escaped
backticks inside a regular double-quoted string:
"Run \`vault build --local-json\` from the repo root..."
When Turbopack's minifier inlined this into a template literal, it
emitted a literal backslash followed by an unescaped backtick, which
prematurely closed the template literal and corrupted the chunked
SSR file. The /about page imports getQuestions() from this module,
so the broken chunk took down the build at static-export time.
Replace the escaped backticks with single quotes in the message;
build now succeeds. Runtime semantics unchanged.
Three 'if cond: stmt' single-line forms in the release-stats loop tripped
ruff E701. Re-formatted to ruff-clean multi-line conditionals; behavior
unchanged.
Per-file audit caught 14 cite keys whose surname prefix or year did not
match the entry's actual paper, plus 4 DOI duplicates and 3 corrupted
orphan entries. Renames preserve the cited paper; only the key changes.
Renames (key -> first-author-surname-year-shortform):
- vol2: agarwal2022 -> ouyang2022instructgpt; alistarh2024 ->
ashkboos2024quarot; belkada2022 -> dettmers2022llmint8; borgeaud2022 ->
hoffmann2022chinchilla; bosma2022 -> wei2022cot; ermon2023 ->
rafailov2023dpo; koyejo2023 -> schaeffer2023mirage; nofal2023 ->
beyer2016sre (year/publisher also corrected to O'Reilly 2016).
- vol1: mccarthy2006 -> mccarthy1955dartmouth; krizhevsky2017 ->
krizhevsky2012imagenet; zhang2021 -> zhang2017rethinking; ford2012 ->
savage2009flaw; wonyoung_kim2008 -> kim2008dvfs; estrada2026 ->
dehghani2022datamesh; michelucci2018 -> glorot2010xavier (entry was
Michelucci textbook chapter, prose wanted Glorot/Bengio AISTATS 2010);
chapelle2009 -> chapelle2006semisupervised (entry was 1-page IEEE
review, prose wanted the actual MIT Press book).
- interviews: key555befcd -> gierl2013automatic; chiang2023 ->
zheng2023judging; boylan1989 -> tay2024interview (Grind 75 web
resource); stenbeck1992 -> hambleton1991 (entry was 1992 review of the
1991 IRT book, content was the book).
DOI dedup:
- vol1 palmer1980 + palmer1980intel8087 -> palmer1980intel8087 (same
paper, redirected cite, deleted dupe).
- vol2 masanet2020 + masanet2020energy -> masanet2020energy (same paper,
redirected cite, deleted dupe).
- vol1 abadi2016tensorflow had wrong DOI pointing to the 2018 EuroSys
Dynamic Control Flow paper; rebuilt as the OSDI 2016 TensorFlow paper
it claims to be. Mirrored same correction into vol2's duplicate entry.
Orphan deletions (zero cite sites, corrupted metadata):
- vol1 acun2023; vol1 aggarwal2018; interviews gallifant2024 (the clean
GPT-4 entry already exists at openai2023gpt4).
- vol1 yu2018 (legitimate paper but unused).
- vol2 mckinsey2018ai and triton.jit (orphans flagged for missing year;
triton.jit was a false positive from a Python decorator inside a code
block, not a citation).
Field repairs:
- aws2020s3: added year=2020, fixed corrupted author "A. W. Services"
to {Amazon Web Services}, added howpublished + url.
51 cite-site updates across 25 files in vol1/vol2/interviews/mlsysim.
All book-prose.md §5 cite-mechanics audit greps return zero hits.
bib_lint reports 0 errors across all three modified bibs.
Remove ten files from the public repo that should never have been
tracked. Verified no code references any of them before deleting.
AI-prompt files (private to author tooling, do not belong in the public
repo):
- interviews/vault-cli/docs/GEMINI_SELF_AUDIT_PROMPT.md
- interviews/vault/_pipeline/runs/gemini-self-audit/prompts/{cloud,
edge,global,mobile,tinyml}_audit_prompt.md (5 per-track prompts;
interviews/vault/.gitignore already excludes /_pipeline/, but these
five were force-added in f6c41d7689 before the rule was set)
Dev-scratch artifacts (clearly leftover dev iteration; filenames literally
say 'final' four different ways):
- interviews/vault-cli/check_results_absolute_final.json
- interviews/vault-cli/check_results_after_repair.json
- interviews/vault-cli/check_results_final.json
- interviews/vault-cli/check_results_total_final.json
No production code, tests, docs, or CI references any of these paths.
The audit-pipeline scripts that *would* write into _pipeline/ already
respect the existing gitignore rule for that directory tree.
`make paper` regenerates these files from the live corpus on each build,
so committing them here just lets a fresh checkout produce a paper.pdf
without first running the full data-pipeline. Drift caught:
- corpus_stats.json was a 9,757 snapshot from an interim state; refreshed
to the current 9,521 published + 843 chains + 87 topics
- 11 figure PDFs (heatmaps, distributions, pipeline schematics, etc.)
re-rendered from corpus_stats.json
paper.pdf builds clean (35 pages, 779 KB, 0 errors). Verified that the
new macros render: 9,521 questions and 87 topics in the abstract, 92.4%
validated in §Schema Validation, and the refreshed mobile-track prose
with the A17 Pro / Snapdragon 8 Gen 3 NPU figures in §Mobile.
The mobile-track illustrative numbers were anchored to roughly 2022 figures:
'15 TOPS at 5 W' for the NPU and a 4,500 mAh battery. Update to the
current-generation envelope (Apple A17 Pro Neural Engine and Qualcomm
Snapdragon 8 Gen 3 Hexagon both reach 30-40 TOPS at 4-5 W; flagship
batteries cluster at $\\sim$5,000 mAh) so the prose stays defensible
through the 1.0.x release window.
Also tighten the battery-life claim. The original 'drain the battery
in under 2 hours' figure assumed total system draw, not the bare 5 W
NPU number. Make that explicit by saying the NPU plus CPU, camera
pipeline, and memory subsystem draws closer to 10 W of system power,
which is what produces the sub-2-hour estimate.
Pure prose change in track description; no macro or schema impact.
The paper's auto-generated macros.tex was last regenerated when the v1.0.0
snapshot held 9,446 published questions; the post-tag audit work has since
brought the published count to 9,521 (cloud +49, edge +14, mobile +2,
tinyml +6, global +4) and consolidated topics from 89 to 87. Re-run
`vault export-paper 1.0.0` so paper and site agree by construction.
While here, fix a bug in the export-paper command itself: \numvalidated
was hardcoded to 100.0\% regardless of the actual flag distribution. The
flag isn't compiled into vault.db, so we read it back from the source
YAMLs and emit the real percentage. Current state is 92.4\% (8,794 of
9,521 published questions carry validated=true). The drift came from
new questions added without the flag set; the conservative fallback if
the YAML scan fails preserves the legacy 100.0\% so the build never
breaks.
The macros change is the meaningful diff. release.json for 1.0.0 is
left untouched to preserve the historical release metadata; vault.db is
gitignored anyway so contributors rebuild it locally via `vault build`
before paper renders.
The pre-push codespell hook flags 'retuned' as a likely typo for
'returned'. The actual intent is the verb 're-tune' (tune again);
hyphenating it sidesteps the false positive while keeping the
meaning. Same pattern as edge-2167.yaml (fixed in wave-4).
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 /contribute page's topic datalist mapped allTopics with key={t.id},
but topic ids appear in multiple competency areas (54 topics shared
across 2-11 areas, e.g. 'mlops-lifecycle' spans 11 areas). Each
duplicate triggered the React 'two children with the same key' warning
— 326 of them per page load.
Fix: namespace the key by area, key={`${t.area}::${t.id}`}. The
'value' attribute stays as t.id since that's what the user picks.
Verified by walkthrough script: /contribute now renders with zero
console errors, like the other 18 routes.
Three small renderer fixes that came out of inspecting how the
audit-corrected YAML content lands on /practice/?q=...:
1. Strip the redundant 'Conclusion & Interpretation:' / 'Result:'
prefixes from result steps. The green callout already signals
'this is the conclusion'; leaving the labels in produces noise
like 'Conclusion & Interpretation: Result: Memory-Bound. ...'.
Handles bold, unbold, and bold-wrapping-the-whole-phrase forms.
2. Teach the number-and-unit highlighter about scientific notation
(Ne12, 1.2×10^14) so phrases like '120e12 FLOPs' render as a
single number+unit chunk instead of '120' (bold) + 'e12' (plain)
+ 'FLOPs' (gray). Also broaden the unit vocabulary to include
Hz/MHz/GHz, W/mW/μW/mJ/μJ/J, MACs, cycles, frames, samples, and
common compound rates (FLOPs/byte, FLOP/cycle, etc.).
3. Distinguish a *section header* line ('**Conclusion & Interpretation:**'
alone on its line) from a *result* line. Previously the parser
marked the header as isResult=true, which then rendered an empty
green callout because cleanStepText stripped the header to ''.
Filter empty steps after cleaning as a belt-and-braces.
Verified across 10 sample questions covering different tracks
(cloud/edge/mobile/tinyml) and napkin-math shapes (sci notation,
multi-section structured, quantization-with-code, compute-bound,
memory-bound, I/O-bound). No regressions; the result blocks now
read directly with the verdict, not the section label.
Add interviews/staffml/README.md covering the local development
workflow that the prior commit's predev hook relies on:
- TL;DR install + run-dev steps
- explanation of the production-worker vs local-static data flow
- what the predev hook does (sync-periodic-table + vault build --local)
- env vars (NEXT_PUBLIC_VAULT_FALLBACK, NEXT_PUBLIC_VAULT_API,
STAFFML_SKIP_LOCAL_CORPUS) and their effects
- troubleshooting the three failure modes that bit us during the YAML
audit work (could-not-load, stale content, infinite loading)
Update interviews/vault-cli/README.md to surface `vault build --local`
in the Local-dev section with a pointer to the StaffML README.
The intent: a contributor who edits a YAML and doesn't see the change
in the dev server should now find the answer in the README before
they're forced to read the loader source.
Before this change, the StaffML Next.js dev server fetched scenario and
details (including napkin_math) from the production Cloudflare Worker
even when contributors had local YAML edits — so changes weren't visible
without shipping. The opt-in static-fallback path existed but was wired
incorrectly: getStaticFullDetail used a Function-constructor dynamic
import of ../data/corpus.json, which Turbopack rewrote to a non-existent
/_next/static/data/corpus.json URL and 404'd at runtime.
Fix in three parts:
1. Loader (interviews/staffml/src/lib/corpus.ts): replace the broken
dynamic import with fetch('/data/corpus.json'). On failure, throw a
clear error pointing at `vault build --local`.
2. Build (interviews/vault-cli/src/vault_cli/commands/build.py): mirror
the generated corpus.json into interviews/staffml/public/data/ so
Next serves it as a static asset. Add --local as a clearer alias for
--local-json and update the help text to spell out the dev workflow.
3. Wiring (interviews/staffml/package.json + scripts/build-local-corpus.mjs):
predev now runs `vault build --local` automatically, with a soft-fail
path if the vault CLI isn't installed (so first-time contributors
still get a working dev server, just with the worker fallback). The
committed .env.development sets NEXT_PUBLIC_VAULT_FALLBACK=static so
the static path is the default in dev. Both copies of corpus.json are
gitignored as build artifacts (the YAMLs are the source of truth).
Adds the deterministic and semantic audit tooling used to drive the
release-readiness pass on the YAML question corpus:
- audit_yaml_corpus.py — read-only schema + authoring-convention audit
- format_yaml_questions.py — canonical formatter (idempotent)
- fix_yaml_hygiene.py — bulk hygiene fixups
- prepare_semantic_review_queue.py — emit JSONL queues per track for LLM review
- semantic_audit_questions.py — parallel LLM audit runner (gpt-5.4-mini)
- run_semantic_audit_tracks.py — per-track orchestrator wrapping the runner
- build_semantic_fix_queue.py — collect findings into a prioritized fix queue
- compare_semantic_passes.py — diff two semantic-audit passes for stability
- summarize_semantic_audit.py — markdown summary from findings JSONL
Also adds interviews/vault/audit/README.md describing the workflow.
Audit output artifacts (semantic-review-queue/, semantic-review-results/,
fresh-yaml-audit/) are produced by these scripts on demand and remain
untracked.