The résumé page offers six versions of my CV — front-end, full-stack, ML, security, DevOps, PM. None of them are written by hand, and none of them are written by a model. They fall out of a single dataset.
The data model
Every bullet and every skill is tagged with the roles it belongs to and given a weight from 1 to 10:
type Role = "frontend" | "fullstack" | "ml" | "security" | "devops" | "pm";
type Bullet = { text: string; roles: Role[]; weight: number };That is the entire source of truth. There is no per-role copy to keep in sync.
The selection function
selectResume(resume, role) is pure and deterministic:
- For each job, keep the bullets tagged with the role, sort by weight descending, and take the top four.
- Drop any job left with no matching bullets.
- Filter and weight-sort the skills the same way.
- Estimate the rendered height. If it runs over one page, drop the globally lowest-weight bullet and check again.
while (estimate(selected) > ONE_PAGE && dropLowestWeightBullet(selected)) {
// keep trimming
}Because it is a plain function, it gets unit tests: for all six roles, assert no bullet leaks in from an untagged role, the four-bullet cap holds, ordering is by weight, and the result fits a page.
The PDF has to survive a parser
A résumé that an applicant-tracking system cannot read is worse than no résumé.
The PDF is built with @react-pdf/renderer using the built-in Helvetica — no
custom fonts — and two things that turned out to matter:
No letter-spacing
letterSpacing renders as real spaces between glyphs, so pdftotext reads the
heading Experience back as E x p e r i e n c e. It is gone from every style.
ASCII only
Curly quotes, en dashes and bullet characters come back as replacement
characters in some extractors. A small pdfSafe() folds them to ASCII before
they reach the document. Bullets are hyphens; skills are comma-separated.
The verification is the literal command from the spec: pdftotext output.pdf -,
run for all six roles, checked for a clean text layer.