Case study · AI engineering and product design
Shortlist: show your real experience, clearly
Shortlist began as a resume chatbot with an “ATS score”. Version 2 is a career tool built on one rule: it never helps you claim something your resume can't back up. It checks how your resume reads, matches it to a job requirement by requirement with quotes as evidence, and suggests rewrites that keep your facts.
- Role
- Product, design and engineering
- Stack
- Python, FastAPI, Gemini, Next.js
- Core
- Parser, checks, hybrid RAG, guardrails
- Status
- Working prototype; parsing and matching evals run
01
The problem and who it's for
Job seekers, especially early-career candidates and people changing fields, tailor a resume for every application without knowing what a recruiter or tracking system sees. Most AI resume tools make this worse: they produce confident scores no one can verify and rewrite bullets with metrics the candidate never achieved, which falls apart in the interview.
Hypotheses, not findings. These pains come from desk research and conversations, not structured interviews. Section 10 describes the study that would test them.
02
Vision and solution
Make real experience easy to see. Shortlist explains instead of scoring, cites instead of asserting, and flags anything a rewrite adds.
- Review: readability checks (columns, tables and photos, headings, contact details, bullet openers, numbers, unnecessary personal details).
- Profile: skills split into “shown in your work” and “only listed”, strongest achievements, and where the resume undersells you.
- Match: each requirement in a job ad, met / partly / not shown, with quotes from your resume and what to make visible.
- Tailor: rewrites aimed at the job, a word-level diff, and a clean single-column Word export.
- Ask: interview prep from your own resume, with the lines each answer relies on.
03
What v1 did, and what changed
| v1 | v2 | |
|---|---|---|
| Parsing | Section rules tuned to one resume: the author's own job titles and a “GitHub Link 2” string | Heading synonyms, wrapped bullets, roles from date ranges, two-column PDFs read column by column, DOCX |
| ATS score | 0–100, including 10 points every resume got for “action verbs” that were never checked | 13 explainable checks with the exact lines involved and a fix; no single score |
| Job match | 37 hard-coded keywords matched as substrings (“ai” matched inside “maintain”) | Requirements extracted from the ad, judged one by one with resume quotes; scored in code |
| Rewrites | A generic review paragraph | Three versions per bullet, checked for numbers and skills the original never mentioned |
| State | One global resume and chat history shared by every visitor | Private workspaces; several resumes and jobs; tailored versions per job |
| Reliability | 5 imported packages missing from requirements; README described a different model | Clean install, 42 backend tests, CI, Docker; runs without a key |
| Interface | Streamlit tabs | A resume viewer with evidence highlighted beside the analysis, on desktop and phone |
Every v1 issue above was confirmed by installing and running the original code.
04
Architecture and technical decisions
On upload (no AI)
On match (one AI call)
Deterministic first, model second
Parsing, layout inspection, readability checks and the keyword view need no model: they're instant, free, testable and still work with AI switched off. The model is used where judgement is needed (is this experience equivalent?), on clean, located text the parser produced.
The score is computed in code, never by the model
Coverage weights required items double and gives half credit for partial evidence. If the model says “met” but its quote can't be found in the resume, the requirement is downgraded to partial and the UI says why.
Guardrails on rewrites
The prompt forbids new facts, and the server checks anyway: any number not in the original, or any skill that appears nowhere on the resume, is flagged next to the suggestion. Placeholders like [X]% are encouraged when a result is missing.
Retrieval per requirement
Each requirement retrieves its three most relevant resume passages (local bge-small embeddings + BM25, fused with RRF). The union, usually most of a short resume, goes to one model call that judges every requirement, which keeps cost to one call per match.
Shared core with Margin
Ingestion, hybrid retrieval, quote verification, workspace security and the LLM client are the same tested modules as my document tool Margin, adapted rather than rewritten.
05
User journey and interface decisions
- Upload a resume (or a sample). Progress shows real bytes sent, then the server reading it; nothing is shown as done before it is.
- Check what was found: sections with a type you can change, the dated roles behind “years of experience”, contact details and skills. Corrections re-run the checks.
- Work in three panels: the resume, the analysis (overview, checks, and feedback by section) and an assistant for questions and rewrites.
- Paste a job ad and open the match: coverage, then every requirement side by side with what the resume shows, and how each finding was reached.
- Edit in the editor: original beside your version, rule-based clarity notes as you type, AI suggestions with fact checks, undo and redo, and a Word export.
- Ask the assistant about your fit for a role. Answers cite the resume and the job ad, and say so when there isn't enough evidence. Keep what's useful under Saved.
- No vanity score. The only number is coverage, labelled as what the resume shows against this ad, “not a prediction of an interview or of any ATS ranking”.
- Problems first. Required items and gaps lead; passes are folded away.
- Warnings beside the suggestion, not in a footnote: “Adds 45, a number your original doesn't state”.
- Its own identity, not Margin's. The first version reused Margin's editorial look (serif type, warm paper) with a green accent, and side by side the two read as one template. Margin serves careful readers; Shortlist serves job seekers who are often anxious and short of time. So Shortlist moved to a direct grotesque typeface, a cool neutral paper, electric cobalt for actions and lime reserved for evidence, and a match report that leads with the requirement checklist and keeps the resume beside it as evidence. It then grew a marketing site and a workspace with a sidebar, whose overview recommends one next step worked out from what's actually in your workspace. The engineering core is still shared with Margin; the look isn't.
- Honest about the AI. Facts carry a citation; comparisons are labelled “Judgement”; uncited advice is labelled “General advice” rather than passed off as fact; every answer lists the passages the model was given.
- Your data, your call. No account: a private browser workspace with a JSON export and a “delete everything” that removes files from the server too.
- Keyboard-operable everywhere, skip link, ARIA tabs and meters, light and dark (follow the system or choose), and panels that collapse to a switch on phones. The tokens and components are documented on a live design-system page built from the real components.
06
Evaluation
Six synthetic resumes in deliberately different formats (including a two-column PDF and a Word file) and four job ads, with hand-labelled gold data. v1 is the original code from the main branch, run unchanged on the same files. No API key needed; both evals run in CI.
| Metric | v1 | v2 |
|---|---|---|
| Files parsed | 5/6 (no DOCX) | 6/6 |
| Section F1 | 0.732 | 1.0 |
| Name found | 0.5 | 1.0 |
| Contact details found | 0.533 | 1.0 |
| Roles extracted correctly | not extracted | 6/6 |
| Metric | v1 keywords | v2 keywords | v2 AI |
|---|---|---|---|
| Requirements it can assess | 0.5 | 0.838 | 1.0 |
| Accuracy (met / partial / missing) | 0.216 | 0.622 | 0.905 |
| Evidence detection F1 | 0.174 | 0.849 | 0.965 |
| Over-claim rate | 0.0 | 0.0 | 0.0 |
Read with care: v2 was developed while looking at these files and the labels were written by the same person, so these numbers show the change from v1 on known cases, not accuracy on unseen resumes. Keyword methods can't judge requirements like “a track record of improving latency”, which is why the match itself uses retrieval and the model. The AI column comes from evals/run_matching.py --llm, run on 2026-10-21 with gemini-3.5-flash-lite: one call per resume–job pair, quotes verified, and an unverifiable “met” downgraded, exactly as in the product. It never marked a missing requirement as met.
07
Challenges, iterations and trade-offs
- Two-column resumes. Sorted extraction merged the columns line by line and lost every section. Shortlist now finds the gutter and reads each column in turn, and still warns that some tracking systems won't.
- Names in capitals looked like section headings; an ALL-CAPS line before the first real section is now treated as the header.
- Wrapped bullets and URLs (“github.com/” at the end of a line) are rejoined before matching.
- “React.js” counted as JavaScript. A skill can no longer start right after a dot. Each fix came with a test.
- Trade-off: anonymous workspaces mean no sign-up but no recovery. Fine for a portfolio demo; a real product needs accounts.
08
Testing and verified results
- 42 backend tests (pytest), with a fake model that can also misbehave on purpose: invented evidence gets downgraded, inflated rewrites get flagged.
- Gold-label parsing tests for each of the six resume formats.
- 13 frontend unit tests (Vitest), typecheck and lint.
- A scripted browser walkthrough of every screen at 1440 px and 390 px in light and dark: no console errors, no horizontal scroll.
09
Demo
Start from the dashboard with a sample resume and job. The live demo runs gemini-3.5-flash-lite on free hosting, so the first visit after a quiet spell takes up to a minute to wake the server.
10
What's next, and what I learned
- Usability study (planned, not done): 6 job seekers each tailor their own resume for a real ad, think-aloud, 30 minutes. Measure whether they understand why a requirement is “partly” met, how often they accept, edit or reject rewrites, and whether flagged additions change what they keep.
- A held-out set of real, consented resumes labelled by someone else; run the AI match eval and compare models.
- Cover-letter drafting from the same evidence, and accounts with version history.
- Lesson: the most valuable AI behaviour here is restraint. Showing a candidate what their resume already proves, and refusing to invent the rest, is more useful than any score.