CV/publications data pipeline

CV/publications data pipeline

This repo’s website (Jekyll, minimal-mistakes theme, deployed via GitHub Pages’ native build) is the canonical public surface for Wenbin’s CV, publications, and bio — but the thing that actually gets edited is the structured data under data/. Every rendered surface (the Jekyll CV page, the publications collection, the homepage bio/news, the PDF CV export, LinkedIn paste blocks, a department packet) is generated from that data by scripts/render_*.py. Never hand-edit a generated file — edit data/* and run /publish.

_pages/cv.md is the single markdown source for the CV — there is no separate Pandoc-source copy anymore. scripts/render_cv.py writes it once, then pipes that same content (with its YAML frontmatter, generated-by marker, and “Download PDF” link stripped) directly into Pandoc/xelatex to produce cv/cv-wenbin-yang.pdf. One file to edit, one PDF to download — no cv/cv.md, cv/cv.html, or .docx anymore.

Generated paths — never hand-edit these

  • _pages/cv.md — the single CV source (website + PDF, see above)
  • _pages/about.md
  • _publications/*.md
  • cv/cv-wenbin-yang.pdf
  • out/**

The text-based ones (_pages/cv.md, _pages/about.md, _publications/*.md) carry a <!-- generated by scripts/render_*.py from data/... — do not edit by hand --> marker at the top. scripts/check_drift.py warns if one of those looks hand-edited out of band (the PDF has no reliable way to carry a text marker, and is fully rebuilt on every /publish anyway).

The /update-profile command

The single capture entry point. Say something like:

gave an invited talk at ICORD in Vancouver, March 12, on closed-loop BP control, ~40 people

Omar Hassan’s project wrapped, he’s starting a PhD at UBC this fall

It classifies the sentence against the record types below (splitting into multiple entries/files if it describes more than one thing), appends to the right file(s), and asks a follow-up question only for whichever required field is actually missing — never re-asks for something it can already infer. If the type itself is genuinely ambiguous (e.g. “received the XYZ Award” could be awards.yml or funding.yml depending on whether it carries a stipend), ask a quick clarifying question instead of guessing.

After logging, report a one-line summary of what was saved and where, and remind the user that /publish is the separate step that actually regenerates and publishes the site — capture and publish are deliberately decoupled so multiple updates can be batched before one regeneration.

Classification cues

Says something like…Goes toRequired field(s)
“gave a talk / presented / poster at a conference”data/talks.ymlselectivity (invited / contributed-oral / poster / submitted)
“won / received an award, medal, recognition” (no dollar amount)data/awards.ymlbasis (how you were selected/judged)
“received a grant / fellowship / funding” (has a dollar amount or funding agency)data/funding.ymlagency, amount
(both of the above) — is this a big, career-level fixture or a smaller/one-off recognition?set tier: major or tier: minor on the entryask if not obvious — defaults to minor, which ages off the homepage after 6 months
“reviewed for / served on a committee / organized / led”data/service.yml
“student / trainee / mentee” update — joined, wrapped up, moved ondata/mentorship.ymloutcome (where they went next, or "ongoing")
“taught / TA’d / ran a workshop”data/teaching.yml
“public talk / science communication / outreach event”data/outreach.yml
“paper got accepted / published / submitted / status changed”update the matching entry’s status in data/publications.bib (match by title — ask if no confident match)

Data schema (data/)

Dates: ISO-8601 where a specific day matters (YYYY-MM-DD); a bare year or YYYY-MM string is fine for coarser entries — match the granularity already used in the file you’re editing.

bio.yml

Slow-changing scaffold (identity, contact, education, appointments, languages, references, professional development) — edited by hand a few times a decade, not through /update-profile.

  • bios.homepage — conversational, first-person, public-facing. Feeds the homepage opening paragraph. Distinct register from the CV bios below.
  • bios.short / bios.medium / bios.long — CV-register prose, for LinkedIn/dept-packet/CCV use.
  • research_interests[] — one canonical list of short phrases. Rendered as homepage bullets and joined into the CV’s research-interests paragraph — don’t maintain two separately-worded versions.
  • education[], appointments[] (each with start/endend: null means present), languages[], references[], professional_development[].
  • No news[]/awards[] list here — the homepage’s “Selected Awards” section is auto-derived from funding.yml + awards.yml (see those sections below), not hand-maintained. Don’t add a manual list for it.

publications.bib

BibTeX, one entry per publication. Custom fields beyond standard BibTeX:

  • permalink — the live Jekyll URL for this publication’s page. Never changes once set, even on rename/retitle — it may already be indexed or shared.
  • status — one of published / accepted / in-revision / submitted / in-prep.
  • selectedtrue/false, controls inclusion in the homepage’s “Selected Publications”.
  • excerpt, citation — short blurb and full pre-formatted citation string, used verbatim by the Jekyll publications collection.

Two separate visibility thresholds, both driven by status — don’t conflate them:

  • CV list (_pages/cv.md, via render_cv.py’s cv_visible_publications()): everything submitted and beyond (submitted, in-revision, accepted, published), annotated with its status where not yet published. in-prep work is deliberately excluded from the public CV/PDF — keep tracking it in publications.bib for your own reference (so nothing gets forgotten once it’s actually submitted), just don’t add it until its status moves off in-prep.
  • Individual public page (_publications/*.md, via render_pubs.py): only published/accepted get their own page — an “in-revision” or “submitted” manuscript has nothing worth a public page yet, so render_pubs.py skips page generation for those statuses (they still show up in the CV list above, just without a live /publication/... URL).

funding.yml

Grants & fellowships (financial). Fields: award, agency, amount, role, start, end, tier (major/minor — see below), optional date (YYYY-MM).

start/end describe the funding period (shown on the CV’s Grants & Fellowships bullets) — date is a separate, “hidden” field for exactly when it was awarded, which doesn’t always coincide with the period start (e.g. told in March that a fellowship starting later that year was granted). date is never shown on the CV; it exists purely so the homepage’s Selected Awards section (see below) can show/sort by the real month instead of falling back to a bare year. Same convention as awards.yml’s date field, below.

awards.yml

Non-financial recognitions (best-presentation awards, competition medals). Required: basis — how you were selected/judged. If it has a dollar amount, it’s funding.yml instead. Also has tier (major/minor) and optional date (YYYY-MM, same “hidden, homepage-only precision” role as funding.yml’s — falls back to the bare year field if not set).

tier drives the homepage’s “Selected Awards” section (render_about.py, merging both files): major entries always show; minor entries show only if dated within the last 6 months (RECENT_MONTHS in render_about.py) — a big current fellowship stays visible indefinitely, a smaller recognition (a publication award, a conference best-presentation award) surfaces for a while after it happens and then quietly ages off. Missing tier defaults to minor, so when logging a new funding/award entry via /update-profile, set it explicitly for anything that should be a permanent “major” fixture — otherwise it’ll only show for 6 months and then silently disappear from the homepage.

talks.yml

Conference presentations. Required: selectivity — one of invited / contributed-oral / poster / submitted. Optional: outcome (e.g. an award the talk led to), title (null is fine for untitled poster entries).

teaching.yml

Courses/workshops taught. Fields: role, course, institution, period, description, optional audience_size.

mentorship.yml

Trainees supervised. Required: outcome — where they went next, or "ongoing" for active trainees. Never save an entry without this field; it’s the one fact about a trainee that’s impossible to reconstruct later.

service.yml

Reviewing, committees, leadership/organizing roles. Fields: role, organization, period, description.

outreach.yml

Public engagement / science communication. Same shape as service.yml; empty until the first entry.

projects.yml

Research project narratives. Fields: title, status (active/complete), description, optional note, related_publication. Not currently rendered on the CV (removed from templates/cv_body.md.j2 at the user’s request) — the data stays here in case it’s useful for something else later (e.g. a grant application); nothing reads it right now.

affiliations.yml

Professional society memberships (e.g. SfN, CAN). Fields: name, abbreviation, period (free text like "2017–2018", or null if unknown/ongoing).

CV section structure (templates/cv_body.md.j2)

The CV has a fixed, flat section list, in this order — no subsections (H3s) under any of them, per the user’s explicit request:

Positions, Education, Publications, Grants & Fellowships, Awards, Conference Presentations, Teaching, Student Supervision & Mentorship, Professional Service, Professional Affiliations, Others.

“Positions” = bio.yml’s appointments[] (jobs held) — kept distinct from “Education” (degrees). Conference Presentations is one flat reverse-chronological list with selectivity shown inline per entry (e.g. “(Invited)”, “(Contributed Oral; …)”) rather than grouped under Invited/Contributed/Poster subheadings.

“Others” is a deliberate catch-all: bio.yml’s professional_development[], languages[], and references[], plus outreach.yml entries if any exist — none of these fit one of the named sections above, so rather than guess where they belong they’re dumped here together, unlabeled, for the user to manually re-sort later. If you add a new data file/category, default new content to “Others” too unless told which named section it belongs in.

The /publish command

Runs the render pipeline end-to-end (scripts/publish.py): regenerates every generated path above from current data/ (including the PDF, built directly by render_cv.py), then git add + commit + push. A distinct, deliberate action from /update-profile — capture now, publish on your own cadence.

Local toolchain

Python 3, Pandoc, and MiKTeX (for xelatex) are required and are installed via winget on this machine, not on the default PATH inherited by every new shell — render_cv.py calls Pandoc/xelatex by their full install paths directly (C:\Users\bysin\AppData\Local\Pandoc\pandoc.exe, MiKTeX’s miktex\bin\x64 prepended to PATH for xelatex) rather than relying on PATH already having them. Ruby/Jekyll are only needed for optional local jekyll serve preview — GitHub Pages builds Jekyll on its own servers regardless.