name: resume-mgmt description: >- Edit, rebuild, tailor, or publish James's resume — the HTML source in the portfolio repo, the generated PDF, and the structured resume.json the site and chat agent read. Also covers reverse-engineering a design back out of a browser-printed PDF when the source is lost. Use for "update my resume", "change this bullet", "tailor my resume for X", "rebuild the resume PDF", "put the new resume on the site", or any resume wording/design change.
resume-mgmt
Where the resume lives
| Artifact | Path (in ~/dev/portfolio) | Role |
|---|---|---|
| HTML | public/resume/jcvolpe-resume-2026.html | Source of truth for the printed resume |
public/resume/jcvolpe-resume-2026.pdf | Generated by pnpm resume:pdf. Never hand-edit | |
| JSON | generated/resume.json | Structured data the About page and the eve chat agent read |
| Profile | generated/profile.json | Current role/location + resumeFilename, the PDF the About page serves |
| Vault MD | vault/**/*.md | Built from the JSON by pnpm vault:build; feeds chat semantic search |
The HTML and the JSON are independent copies of the same facts. Changing one does not touch the other. A wording change the user asks for is usually a change to both — check whether the JSON still agrees before declaring done.
Workflow for a wording or content change
- Edit
public/resume/jcvolpe-resume-2026.html. pnpm resume:pdf— Playwright renders it through the installed Chrome and asserts the result is still one page. A resume that spills to page 2 is a failure, not a new page.- Mirror the same change into
generated/resume.jsonif the fact is one the site/chat states (title, dates, bullets, skills). - If the chat agent must know it:
pnpm vault:build, thenpnpm exec tsx --env-file=.env.production scripts/build-vault-vector-store.ts(thepnpm vault:vector-store:buildscript points at.env.local, which has no OpenAI key; the prod store id and key live in.env.production). It replaces only the vault files and leaves theproject-docentries alone. The vault markdown the deployed chat greps is bundled into the Lambda, soread_file/grepkeep answering with the old text until the next deploy — onlyfile_searchupdates immediately. - Read the rendered page back (
pdftoppm -r 130 -png <pdf> out, then view the PNG) before reporting. Line wrap and page fit only exist after rendering.
DynamoDB (PortfolioStack-AdminData, PK=PROFILE) outranks both JSON files
once an admin save has happened — until then reads fall back to the committed
seed. Check before assuming which layer is live:
aws dynamodb get-item --region us-east-1 --table-name PortfolioStack-AdminData \
--key '{"PK":{"S":"PROFILE"},"SK":{"S":"MAIN"}}'
Publishing to the site
src/app/about/page.tsx serves the admin-uploaded profile.resumePdfKey when
set, otherwise public/resume/<profile.resumeFilename>. Adding a new PDF to
public/resume/ publishes nothing on its own — one of those two fields has to
point at it (admin upload at /admin, or the profile field).
Rebuilding a design from a printed PDF
When the only copy of a resume is a PDF that some tool printed, the design is recoverable — and this is faster than eyeballing it. Precise numbers first, then one or two calibration passes.
pdftotext -layout in.pdf - # copy, in reading order and columns
pdffonts in.pdf # the actual typefaces
pdftoppm -r 130 -png in.pdf page # full-page look
pdftoppm -r 300 -png -x X -y Y -W W -H H in.pdf crop # zoom a detail
Exact colors and font sizes live in the content streams. qpdf, mutool, and
PIL may or may not be installed; this stdlib snippet always works:
import re, zlib
from collections import Counter
data = open('in.pdf','rb').read()
out = []
for m in re.finditer(rb'stream\r?
', data):
s = m.end(); e = data.find(b'endstream', s)
try: out.append(zlib.decompress(data[s:e]))
except Exception: pass
blob = b'
'.join(out)
hexes = ['#%02x%02x%02x' % tuple(round(float(v) * 255) for v in c)
for c in re.findall(rb'([0-9.]+) ([0-9.]+) ([0-9.]+) rg', blob)]
print(Counter(hexes).most_common())
print(Counter(re.findall(rb'/(F\d+) ([0-9.]+) Tf', blob)).most_common())
The one that costs an hour: in a PDF printed by Chrome, the page content is
drawn under a 0.75 scale, so the Tf sizes are the CSS px values, not
points. Write 37px, not 37pt. Taking them as points renders everything
1.33x too big and pushes a one-page resume onto two.
Calibrate by comparing word widths rather than by eye — pdftotext -bbox on
both the original and your rebuild, then diff the xMax - xMin of the same
word. A ratio near 1.0 means the size is right; a ratio that differs per element
means that element's size is wrong, not the global scale. Expect two or three
render → compare → tune passes.
Gotchas
chromium.launch({ channel: 'chrome' })uses the Chrome already installed on the machine. Plainchromium.launch()wants Playwright's own browser download, which is often absent even in a repo with Playwright tests.page.pdf({ preferCSSPageSize: true })is what honors@page { size: A4 }; without it the output silently becomes Letter.- Scripts in the portfolio repo transpile to CJS — no top-level
await. Wrap the body inasync function main()and call it, like the sibling scripts. - The portfolio repo forbids code comments; keep resume context in the repo's
AGENTS.mdor Linear, not in the HTML.
