ATS Checker¶
Zero-dependency TypeScript library that scores a resume against a job description and explains why — skills coverage, keyword overlap, experience match, parseability, and education — with no randomness, no LLM, and no external calls.
Quick Start¶
import { analyzeResume } from "@pranavraut033/ats-checker";
const result = analyzeResume({
resumeText: `
Software Engineer with 5 years of experience.
Skills: JavaScript, TypeScript, React, Node.js, SQL
Experience: Senior Engineer at ExampleCorp (Jan 2020 - Present)
Education: B.S. Computer Science
`,
jobDescription: `
Frontend engineer role. Must have React, TypeScript, accessibility best practices.
Preferred: GraphQL. 3+ years required. Bachelor's degree required.
`,
config: { referenceDate: "2026-01-01" }, // freeze clock for reproducible scores
});
console.log(result.score); // e.g. 39.44
console.log(result.matchedSkills); // ["javascript", "node", "react", "typescript"]
console.log(result.missingSkills); // ["accessibility", "frontend", "graphql"]
console.log(result.experienceGap); // 0 (requirement met)
console.log(result.suggestions); // ["Highlight these required skills: ...", ...]
Features¶
- Deterministic — same input always produces the same score; pin it with
referenceDateto freeze "Present" date math - Explainable — breakdown by category (skills / experience / keywords / parseability / education) plus matched and missing skill/keyword lists
- Parseability scoring — deducts for table/columnar formatting, multi-column layout, special characters, non-standard bullets, likely-scanned text, and an unparseable contact email; itemized in
result.parseabilityReport - Fuzzy/stem matching by default — typos and word-form variants ("ReactJS" vs "react") still match; opt out via
config.matching = { fuzzy: false } - Whole-document skill extraction — skills mentioned in experience bullets/summary count, each entry dated per-role
- Seniority & employment-gap awareness —
result.seniorityMatch,result.employmentGaps - Categorized keywords — every keyword/alias belongs to a category (technical, tool, concept, soft, marketing, domain); 407 canonical terms ship by default
- Weighted keyword scoring — JD keywords weighted by location (required > preferred > body, including header-scoped
Requirements:/Preferred:blocks) and frequency - Alias-aware & achievement suggestions — reword suggestions matching the JD's wording, plus strong/weak achievement-bullet feedback
- Multi-language keyword packs —
/enand/desubpaths; bring your own viaconfig.keywordRegistry - Language proficiency matching — JD spoken-language requirements (CEFR
A1–C2or words like "fluent"/"native") matched against resume mentions - Configurable — adjust weights, add skill aliases or a keyword registry, define custom penalty rules
- Deterministic-only core —
analyzeResumeAsync(LLM path) is deprecated;analyzeResumeis the primary API - Zero dependencies — no runtime deps (fuzzy/stem matching is hand-rolled); ships ESM + CJS
- PDF input — optional
/pdfsubpath for extracting text from PDF resumes
Live Demo¶
Output Reference¶
analyzeResume() returns an ATSAnalysisResult:
| Field | Type | Description |
|---|---|---|
score | number | Overall ATS score 0–100 after rule penalties |
breakdown | ATSBreakdown | Sub-scores: skills, experience, keywords, parseability, education |
parseabilityReport | ParseabilityReport | Itemized deductions behind breakdown.parseability |
matchedSkills | string[] | Required skills found in the resume |
missingSkills | string[] | Required skills absent from the resume |
matchedKeywords | string[] | JD keywords present in the resume (sorted) |
missingKeywords | string[] | JD keywords absent from the resume (sorted) |
overusedKeywords | string[] | Keywords exceeding density threshold (sorted) |
keywordsByCategory | Record<KeywordCategory, {matched, missing}> | Matched/missing keywords grouped by category |
keywordWeights | KeywordWeight[] | Per-keyword JD importance (jdWeight) and resume usage (resumeWeight) |
achievementStrength | { strong: number; weak: number } | Count of resume bullets classified strong vs weak |
matchedLanguages | ParsedLanguage[] | JD-required languages the resume meets or exceeds in proficiency |
missingLanguages | ParsedLanguage[] | JD-required languages absent or below the required proficiency |
seniorityMatch | { resume?, required?, met } | Resume vs JD inferred seniority |
employmentGaps | { afterRole, months }[] | Gaps ≥3 months between dated roles |
perSkillExperience | { skill, years }[] | Per-skill years from per-role dating |
suggestions | string[] | Deterministic improvement recommendations |
warnings | string[] | Parse warnings and section alerts |
experienceGap | number | Years below JD minimum; 0 when met |
detectedSections | string[] | Resume sections the parser found |
parsedExperienceYears | number | Total years from resume date ranges |
Scoring formula:
score = skills×0.25 + experience×0.20 + keywords×0.25 + parseability×0.20 + education×0.10 → clamped to 0–100 → rule penalties subtracted. The keywords sub-score is a weighted coverage ratio — see Configuration for how weights are derived.
Documentation¶
- Architecture — pipeline internals and module map
- Configuration — all config options with defaults
- Rules Engine — built-in rules and custom rule API
- LLM Integration (deprecated) —
analyzeResumeAsyncreference
API Reference¶
analyzeResume(input): ATSAnalysisResult¶
Input:
| Field | Type | Required | Description |
|---|---|---|---|
resumeText | string | ✅ | Full text of the resume |
jobDescription | string | ✅ | Job description text |
config | ATSConfig | — | Optional configuration |
extractTextFromPDF(data): Promise<string>¶
Extracts plain text from a PDF buffer. Import from the /pdf subpath; requires pdfjs-dist installed separately.
import { extractTextFromPDF } from "@pranavraut033/ats-checker/pdf";
const resumeText = await extractTextFromPDF(uint8ArrayOrArrayBuffer);
| Parameter | Type | Description |
|---|---|---|
data | Uint8Array \| ArrayBuffer | Raw PDF bytes |
Returns a normalized string ready to pass as resumeText. Text-layer PDFs only.
Multi-column layouts are handled automatically using glyph x/y positions to detect and separate columns before joining lines. Single-column and two-column resumes both parse cleanly.
For PDFs that can't be recovered (scanned/image resumes with no text layer, or near-empty extractions), analyzeResume emits an actionable message in result.warnings:
const result = analyzeResume({ resumeText, jobDescription: "..." });
if (result.warnings.length) {
console.warn(result.warnings);
// e.g. "Almost no text was extracted — the resume may be a scanned/image PDF."
}
If section detection still fails after extraction (fewer than 2 sections found in a long document), a suggestion is also added to result.suggestions advising the user to export as single-column PDF.
Built-in Profiles¶
import {
softwareEngineerProfile,
dataScientistProfile,
productManagerProfile,
defaultSkillAliases,
defaultKeywordRegistry,
} from "@pranavraut033/ats-checker";
Multi-language Keyword Packs¶
import en from "@pranavraut033/ats-checker/en"; // default registry
import de from "@pranavraut033/ats-checker/de"; // seed set, German aliases
const result = analyzeResume({
resumeText,
jobDescription,
config: { keywordRegistry: de },
});
See Configuration → Keyword Registry & Categories.
Language Requirements¶
Spoken-language requirements (not programming languages) are parsed from the JD — CEFR codes (A1–C2) or words like fluent/native/professional — and matched against language mentions in the resume.
const result = analyzeResume({
resumeText: "Languages: German (C1)",
jobDescription: "German (B2) required.",
});
result.matchedLanguages; // [{ name: "german", level: "b2", levelRank: 4 }]
result.missingLanguages; // []
See Configuration → Language Requirements.
Development¶
npm install
npm run build # tsup → ESM + CJS in dist/
npm test # vitest (single pass)
npm run type-check # tsc --noEmit
npm run dev # static demo UI at http://localhost:3005
Made with ❤️ by Pranav Raut