Context
The next headline feature for resumelint is JD (job description) matching. The motivating user flow: drop a resume PDF as today, then paste a JD into a textarea, and the UI surfaces what the JD asks for that the resume doesn't claim. This is the single biggest lever we have for repeat usage — a parser-only audit is a one-shot; a JD-match check is something a user will run on every application.
Frame this as a lint check, not as AI tailoring. resumelint sits in the "linter for resume PDFs" lane. "Tailoring" / "matching" framing belongs to other products in the space; resumelint stays diagnostic. The user-visible deliverable here is "your resume mentions X of the skills the JD asks for, here's what's missing" — diagnostic, not prescriptive.
V1 is deterministic — token + skill-phrase overlap, no LLM call. An LLM-assisted "suggest a rewrite that covers this missing skill" is a follow-up, gated behind the WebLLM bullet-rewrite pilot (separate issue).
Goal
After this issue closes:
- The single-page app has a JD input alongside the existing resume dropzone.
- After both are present, the result panel shows a JD-match score, a list of skills/keywords from the JD that the resume covers, and a list of those it doesn't.
- All matching is deterministic, in-browser, byte-stays-on-device — same privacy story as the parser.
- Every claim in the UI ("we found N skills") maps to a function or constant in code — no copy that can't be traced back to logic.
Implementation
Part 1 — JD ingestion
Add a <textarea> in App.tsx that accepts a pasted JD. Strip whitespace, normalize, but otherwise leave the text intact. No file upload for v1; pasted text only.
Part 2 — skill / keyword extraction from the JD
New file: src/lib/jd-match/extract-jd-terms.ts
Approach: deterministic. Two passes:
-
Skill phrase pass. Maintain a curated skill dictionary (src/lib/jd-match/skills.ts) of common tech / role skills with their canonical form and aliases. E.g. {"react": ["react", "reactjs", "react.js"], "kubernetes": ["kubernetes", "k8s"]}. Walk the JD text, find phrase matches (case-insensitive, word-boundary). Output a Set<string> of canonical skill IDs.
-
High-signal noun-phrase pass. Pull noun phrases by simple regex heuristics (capitalized multi-word phrases not in a stop list, single-word acronyms ≥2 letters). This catches things not in the skill dictionary — company names, tool names, etc. Lower-weight than the skill pass.
Both passes ignore boilerplate sections of JDs (legal disclaimers, EEO language, benefits) — match those by anchor phrases and skip those line ranges. Document the anchor list inline in the extraction file.
Part 3 — coverage check against the resume
New file: src/lib/jd-match/coverage.ts
Given the cascade result (CascadeResult from the existing pipeline) and the extracted JD terms:
- Build a flat search corpus from the resume: bullet text, summary, skills section if parsed, project descriptions, education
- For each JD term (skill or noun phrase), check if the corpus mentions it (case-insensitive, word-boundary, alias-aware via the same skills dictionary)
- Output:
{ covered: string[], missing: string[], score: number } where score = covered.length / (covered.length + missing.length) * 100
Apply small weighting: skill-pass matches weigh 1.0, noun-phrase-pass matches weigh 0.5. Note the weighting in the output so it round-trips to UI copy.
Part 4 — UI
New component: src/components/JdMatch.tsx
- Shown only when a JD is present
- Headline number (the score) with the disclaimer "diagnostic, not a verdict"
- Two columns: "Covered (N)" with green checkmarks, "Missing (N)" with neutral dots
- Each missing item is just text — no "add this to your resume" CTA in v1 (that's tailoring territory)
- For each item, on hover show where in the JD it was extracted (a short snippet)
Part 5 — copy
Match resumelint's existing copy discipline:
- Don't say "X% match" — say "your resume mentions N of M skills from this JD"
- Don't frame as "ATS will pass / fail" — say "the JD asks for these; here's what we found"
- First person, acknowledge variance ("we look for skills by phrase match; we don't read context")
- No false precision ("exactly", "precisely"); never frame our parser as what an ATS literally sees
- Affirmative privacy framing ("your text stays in the browser"), not negation
Where to share progress
Drop progress, screenshots, and questions as comments on this issue as you go — that way the team can chip in. Anything blocking you, reach out — that's what I'm here for.
Acceptance criteria
Out of scope
- LLM-assisted rewrite suggestions ("here's how to add Kubernetes to your bullet about Docker") — follow-up issue, gated on the WebLLM pilot
- JD file upload (PDF / DOCX) — paste-only for v1
- JD scoring weights tuned for specific industries — flat weights for v1
- Multi-JD comparison — single JD only for v1
- Saving / sharing JD-match results — pure client-side, no persistence
Notes for the implementer
- Implement the matching deterministically from scratch per the design above — do not port code from any internal/closed codebase.
- The skill dictionary is the slowest-to-tune part of this issue. A good v1 seed is to walk a public skills dataset (check the license before vendoring) and trim to the top 100–200 most-used tech / engineering / product / data skills.
- Consider a
src/lib/jd-match/index.ts barrel export so the rest of the app imports cleanly.
- The framing matters more than the algorithm. If you're unsure whether a piece of copy crosses into "tailoring" territory, ask in the issue comments.
Context
The next headline feature for resumelint is JD (job description) matching. The motivating user flow: drop a resume PDF as today, then paste a JD into a textarea, and the UI surfaces what the JD asks for that the resume doesn't claim. This is the single biggest lever we have for repeat usage — a parser-only audit is a one-shot; a JD-match check is something a user will run on every application.
Frame this as a lint check, not as AI tailoring. resumelint sits in the "linter for resume PDFs" lane. "Tailoring" / "matching" framing belongs to other products in the space; resumelint stays diagnostic. The user-visible deliverable here is "your resume mentions X of the skills the JD asks for, here's what's missing" — diagnostic, not prescriptive.
V1 is deterministic — token + skill-phrase overlap, no LLM call. An LLM-assisted "suggest a rewrite that covers this missing skill" is a follow-up, gated behind the WebLLM bullet-rewrite pilot (separate issue).
Goal
After this issue closes:
Implementation
Part 1 — JD ingestion
Add a
<textarea>inApp.tsxthat accepts a pasted JD. Strip whitespace, normalize, but otherwise leave the text intact. No file upload for v1; pasted text only.Part 2 — skill / keyword extraction from the JD
New file:
src/lib/jd-match/extract-jd-terms.tsApproach: deterministic. Two passes:
Skill phrase pass. Maintain a curated skill dictionary (
src/lib/jd-match/skills.ts) of common tech / role skills with their canonical form and aliases. E.g.{"react": ["react", "reactjs", "react.js"], "kubernetes": ["kubernetes", "k8s"]}. Walk the JD text, find phrase matches (case-insensitive, word-boundary). Output aSet<string>of canonical skill IDs.High-signal noun-phrase pass. Pull noun phrases by simple regex heuristics (capitalized multi-word phrases not in a stop list, single-word acronyms ≥2 letters). This catches things not in the skill dictionary — company names, tool names, etc. Lower-weight than the skill pass.
Both passes ignore boilerplate sections of JDs (legal disclaimers, EEO language, benefits) — match those by anchor phrases and skip those line ranges. Document the anchor list inline in the extraction file.
Part 3 — coverage check against the resume
New file:
src/lib/jd-match/coverage.tsGiven the cascade result (
CascadeResultfrom the existing pipeline) and the extracted JD terms:{ covered: string[], missing: string[], score: number }wherescore = covered.length / (covered.length + missing.length) * 100Apply small weighting: skill-pass matches weigh 1.0, noun-phrase-pass matches weigh 0.5. Note the weighting in the output so it round-trips to UI copy.
Part 4 — UI
New component:
src/components/JdMatch.tsxPart 5 — copy
Match resumelint's existing copy discipline:
Where to share progress
Drop progress, screenshots, and questions as comments on this issue as you go — that way the team can chip in. Anything blocking you, reach out — that's what I'm here for.
Acceptance criteria
extract-jd-terms.ts,coverage.ts, andJdMatch.tsxeach have unit / component testssrc/lib/jd-match/skills.tshas at least 100 entries with aliasesnpm run testgreenOut of scope
Notes for the implementer
src/lib/jd-match/index.tsbarrel export so the rest of the app imports cleanly.