Skip to content

JD matching (v1, deterministic) — paste-a-JD textarea + skill / keyword coverage check #2

Description

@s-annam

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:

  1. 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.

  2. 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

  • User can paste a JD into a textarea on the same page as the resume drop
  • After both are present, a JD-match panel shows: score, covered list, missing list
  • All logic deterministic and in-browser — no network call for matching
  • extract-jd-terms.ts, coverage.ts, and JdMatch.tsx each have unit / component tests
  • The skills dictionary in src/lib/jd-match/skills.ts has at least 100 entries with aliases
  • Boilerplate JD sections (EEO, benefits) excluded from term extraction — covered by tests
  • Copy reviewed against resumelint's copy-discipline conventions (see CONTRIBUTING.md)
  • Existing parser tests still pass; npm run test green
  • CI green on the PR

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestfeatureNew functionality

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions