Blog
typescriptresumeats

A résumé engine with no model in the loop

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:

  1. For each job, keep the bullets tagged with the role, sort by weight descending, and take the top four.
  2. Drop any job left with no matching bullets.
  3. Filter and weight-sort the skills the same way.
  4. 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.