Skip to content

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

v1v2
ParsingSection rules tuned to one resume: the author's own job titles and a “GitHub Link 2” stringHeading synonyms, wrapped bullets, roles from date ranges, two-column PDFs read column by column, DOCX
ATS score0–100, including 10 points every resume got for “action verbs” that were never checked13 explainable checks with the exact lines involved and a fix; no single score
Job match37 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
RewritesA generic review paragraphThree versions per bullet, checked for numbers and skills the original never mentioned
StateOne global resume and chat history shared by every visitorPrivate workspaces; several resumes and jobs; tailored versions per job
Reliability5 imported packages missing from requirements; README described a different modelClean install, 42 backend tests, CI, Docker; runs without a key
InterfaceStreamlit tabsA 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)

PDF / DOCX · type from bytes · 5 MB, 8 pages
Column-aware text · layout facts (columns, tables, images)
Sections · bullets · roles and dates · contact
13 readability checks · section chunks + local embeddings

On match (one AI call)

Job ad → cited requirements (once per job)
Per requirement: top-3 resume passages (dense + BM25, RRF)
Gemini judges each requirement with quotes
Quotes verified · unverifiable “met” downgraded · score in code
FastAPI backend (Docker), Next.js frontend, SQLite, workspace tokens, per-workspace hourly AI limit, results cached.

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

  1. Upload a resume (or a sample). Progress shows real bytes sent, then the server reading it; nothing is shown as done before it is.
  2. 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.
  3. Work in three panels: the resume, the analysis (overview, checks, and feedback by section) and an assistant for questions and rewrites.
  4. 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.
  5. 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.
  6. 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.

Parsing (6 resumes)
Metricv1v2
Files parsed5/6 (no DOCX)6/6
Section F10.7321.0
Name found0.51.0
Contact details found0.5331.0
Roles extracted correctlynot extracted6/6
Requirement matching (74 requirements, 8 resume–job pairs)
Metricv1 keywordsv2 keywordsv2 AI
Requirements it can assess0.50.8381.0
Accuracy (met / partial / missing)0.2160.6220.905
Evidence detection F10.1740.8490.965
Over-claim rate0.00.00.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.