@pranavraut033/ats-checker 1.3.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -50
- package/dist/chunk-JAFFS7YZ.mjs +1219 -0
- package/dist/chunk-JAFFS7YZ.mjs.map +1 -0
- package/dist/{scoring-BCShrnki.d.mts → config-CVmXtmvk.d.mts} +208 -88
- package/dist/{scoring-BCShrnki.d.ts → config-CVmXtmvk.d.ts} +208 -88
- package/dist/index.cjs +1777 -298
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +840 -133
- package/dist/index.mjs.map +1 -1
- package/dist/lang/de/index.cjs +424 -29
- package/dist/lang/de/index.cjs.map +1 -1
- package/dist/lang/de/index.d.mts +1 -1
- package/dist/lang/de/index.d.ts +1 -1
- package/dist/lang/de/index.mjs +424 -29
- package/dist/lang/de/index.mjs.map +1 -1
- package/dist/lang/en/index.cjs +613 -41
- package/dist/lang/en/index.cjs.map +1 -1
- package/dist/lang/en/index.d.mts +1 -1
- package/dist/lang/en/index.d.ts +1 -1
- package/dist/lang/en/index.mjs +1 -1
- package/dist/lang/en/index.mjs.map +1 -1
- package/dist/pdf/index.cjs.map +1 -1
- package/dist/pdf/index.mjs.map +1 -1
- package/package.json +11 -2
- package/dist/chunk-ZJ5E4H7Z.mjs +0 -446
- package/dist/chunk-ZJ5E4H7Z.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://github.com/Pranavraut033/ats-checker/actions/workflows/ci.yml)
|
|
7
7
|
[](https://github.com/Pranavraut033/ats-checker/actions/workflows/deploy.yml)
|
|
8
8
|
|
|
9
|
-
Zero-dependency TypeScript library that scores a resume against a job description and explains why — skills coverage, keyword overlap, experience match, and education — with no randomness, no LLM, and no external calls.
|
|
9
|
+
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.
|
|
10
10
|
|
|
11
11
|
**[Live Demo →](https://pranavraut033.github.io/ats-checker/)**
|
|
12
12
|
**[Docs →](https://pranavraut033.github.io/ats-checker/docs/)**
|
|
@@ -16,15 +16,21 @@ Zero-dependency TypeScript library that scores a resume against a job descriptio
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
18
|
- **Deterministic** — same input always produces the same score; pin it with `referenceDate` to freeze "Present" date math
|
|
19
|
-
- **Explainable** — breakdown by category (skills / experience / keywords / education) plus matched and missing skill/keyword lists
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
19
|
+
- **Explainable** — breakdown by category (skills / experience / keywords / parseability / education) plus matched and missing skill/keyword lists
|
|
20
|
+
- **Parseability scoring** — a real-ATS-realism dimension that deducts for table/columnar formatting, multi-column layout, special/control characters, non-standard bullets, likely-scanned text, and an unparseable contact email — with the specific deductions itemized in `result.parseabilityReport`
|
|
21
|
+
- **Fuzzy/stem matching by default** — typos and word-form variants ("ReactJS" vs "react", "developing" vs "develop") still match; opt out via `config.matching = { fuzzy: false }` for exact-match-only behavior
|
|
22
|
+
- **Whole-document skill extraction** — skills mentioned in experience bullets and the summary count, not just an explicit Skills section, and each experience entry is dated per-role
|
|
23
|
+
- **Seniority & employment-gap awareness** — infers resume/JD seniority (`junior`–`principal`) and surfaces a capped match signal (`result.seniorityMatch`), and flags employment gaps of 3+ months between roles (`result.employmentGaps`)
|
|
24
|
+
- **Categorized keywords** — every keyword/alias belongs to a category (technical, tool, concept, soft, marketing, domain); results are grouped by category; 407 canonical terms ship by default
|
|
25
|
+
- **Weighted keyword scoring** — JD keywords are weighted by where they appear (required > preferred > body, including header-scoped `Requirements:`/`Preferred:` blocks) and how often, so a missing "required" keyword costs more than a missing body-only one
|
|
22
26
|
- **Alias-aware suggestions** — flags resume terms that should be reworded to match the JD's own wording (e.g. "js" → "JavaScript")
|
|
23
27
|
- **Achievement strength** — classifies resume experience bullets as strong/weak (verb + quantified impact) and suggests rewrites
|
|
24
28
|
- **Multi-language keyword packs** — `/en` and `/de` subpaths ship categorized keyword registries; install more by passing your own `keywordRegistry`
|
|
25
29
|
- **Language proficiency matching** — detects spoken-language requirements in the JD (CEFR `A1`–`C2` or words like "fluent"/"native") and flags resume gaps below the required level
|
|
30
|
+
- **Skill-experience gaps** — parses "N+ years of X" requirements from the JD and flags resume skills whose per-role dated experience (`result.perSkillExperience`) falls short
|
|
31
|
+
- **Contact detection** — parses email/phone/LinkedIn/location and penalizes an unparseable contact email by default (near-knockout, like a real ATS)
|
|
26
32
|
- **Configurable** — adjust weights, add skill aliases or a custom keyword registry, define custom penalty rules
|
|
27
|
-
- **Zero dependencies** — core library has no runtime deps; ships ESM + CJS
|
|
33
|
+
- **Zero dependencies** — core library has no runtime deps (fuzzy/stem matching is hand-rolled); ships ESM + CJS
|
|
28
34
|
- **PDF input** — optional `/pdf` subpath extracts resume text from a PDF buffer (requires `pdfjs-dist` peer dep)
|
|
29
35
|
- **Built-in profiles** — software engineer, data scientist, product manager out of the box
|
|
30
36
|
|
|
@@ -66,11 +72,11 @@ const result = analyzeResume({
|
|
|
66
72
|
config: { referenceDate: "2026-01-01" }, // freeze clock for reproducible scores
|
|
67
73
|
});
|
|
68
74
|
|
|
69
|
-
console.log(result.score);
|
|
70
|
-
console.log(result.matchedSkills);
|
|
71
|
-
console.log(result.missingSkills);
|
|
72
|
-
console.log(result.experienceGap);
|
|
73
|
-
console.log(result.suggestions);
|
|
75
|
+
console.log(result.score); // 39.44
|
|
76
|
+
console.log(result.matchedSkills); // ["javascript", "node", "react", "typescript"]
|
|
77
|
+
console.log(result.missingSkills); // ["accessibility", "frontend", "graphql"]
|
|
78
|
+
console.log(result.experienceGap); // 0 (requirement met)
|
|
79
|
+
console.log(result.suggestions); // ["Highlight these required skills: accessibility, frontend, graphql", ...]
|
|
74
80
|
```
|
|
75
81
|
|
|
76
82
|
---
|
|
@@ -79,33 +85,42 @@ console.log(result.suggestions); // ["Highlight these required skills: acce
|
|
|
79
85
|
|
|
80
86
|
`analyzeResume()` returns an `ATSAnalysisResult`:
|
|
81
87
|
|
|
82
|
-
| Field
|
|
83
|
-
|
|
84
|
-
| `score`
|
|
85
|
-
| `breakdown`
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
88
|
+
| Field | Type | Description |
|
|
89
|
+
| ----------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
90
|
+
| `score` | `number` | Overall ATS score 0–100 after rule penalties |
|
|
91
|
+
| `breakdown` | `ATSBreakdown` | Sub-scores: `skills`, `experience`, `keywords`, `parseability`, `education` |
|
|
92
|
+
| `parseabilityReport` | `ParseabilityReport` | Itemized `{ reason, points }` deductions behind `breakdown.parseability`, plus the underlying formatting signals |
|
|
93
|
+
| `matchedSkills` | `string[]` | Required skills found in the resume |
|
|
94
|
+
| `missingSkills` | `string[]` | Required skills absent from the resume |
|
|
95
|
+
| `matchedKeywords` | `string[]` | JD keywords present in the resume (sorted) |
|
|
96
|
+
| `missingKeywords` | `string[]` | JD keywords absent from the resume (sorted) |
|
|
97
|
+
| `overusedKeywords` | `string[]` | Keywords exceeding density threshold (sorted) |
|
|
98
|
+
| `keywordsByCategory` | `Record<KeywordCategory, {matched, missing}>` | Matched/missing keywords grouped by category |
|
|
99
|
+
| `keywordWeights` | `KeywordWeight[]` | Per-keyword JD importance (`jdWeight`) and resume usage (`resumeWeight`) |
|
|
100
|
+
| `achievementStrength` | `{ strong: number; weak: number }` | Count of resume bullets classified as strong vs weak achievement statements |
|
|
101
|
+
| `matchedLanguages` | `ParsedLanguage[]` | JD-required languages the resume meets or exceeds in proficiency |
|
|
102
|
+
| `missingLanguages` | `ParsedLanguage[]` | JD-required languages absent or below the required proficiency |
|
|
103
|
+
| `seniorityMatch` | `{ resume?, required?, met }` | Resume vs JD inferred seniority (`junior`–`principal`); `met` is `true` whenever either side is unknown |
|
|
104
|
+
| `employmentGaps` | `{ afterRole, months }[]` | Gaps of 3+ months between consecutive dated roles — informational |
|
|
105
|
+
| `perSkillExperience` | `{ skill, years }[]` | Per-skill years of experience derived from per-role dating (which roles actually mention the skill) |
|
|
106
|
+
| `skillExperienceGaps` | `{ skill, requiredYears, resumeYears }[]` | JD skills the resume has but whose per-role dated experience falls short of a JD "N+ years of X" requirement — informational, doesn't affect `score` |
|
|
107
|
+
| `suggestions` | `string[]` | Deterministic improvement recommendations |
|
|
108
|
+
| `warnings` | `string[]` | Parse warnings and section alerts |
|
|
109
|
+
| `experienceGap` | `number` | Years below JD minimum; `0` when met |
|
|
110
|
+
| `detectedSections` | `string[]` | Resume sections the parser found |
|
|
111
|
+
| `parsedExperienceYears` | `number` | Total years from resume date ranges (overlap-deduplicated) |
|
|
112
|
+
| `experienceEntries` | `ParsedExperienceEntry[]` | Parsed job entries: `title`, `company`, `dates` (with `start`/`end`/`durationInMonths`), `skills` (per-role skill mentions) |
|
|
102
113
|
|
|
103
114
|
**Scoring formula:**
|
|
104
|
-
`score = skills×0.
|
|
115
|
+
`score = skills×0.25 + experience×0.20 + keywords×0.25 + parseability×0.20 + education×0.10` → clamped to 0–100 → rule penalties subtracted.
|
|
116
|
+
|
|
117
|
+
The `parseability` sub-score starts at 100 and deducts for formatting/structure signals that trip up real ATS parsers — tables/columns, multi-column layout, special/control characters, non-standard bullets, likely-scanned text, an unparseable contact email, and too few detected sections. This is the single biggest real-world ATS rejection cause the v1 model didn't score at all.
|
|
118
|
+
|
|
119
|
+
Skill and keyword matching use fuzzy/stemmed comparison by default (`config.matching = { fuzzy: true }`), so typos and word-form variants ("ReactJS" vs "react", "developing" vs "develop") still match. Pass `{ fuzzy: false }` to require exact matches only.
|
|
105
120
|
|
|
106
121
|
The `keywords` sub-score is a **weighted** coverage ratio, not a flat count: each JD keyword gets a weight from its location (required > preferred > body text) and frequency, so missing a required keyword drops the score more than missing one mentioned once in the body.
|
|
107
122
|
|
|
108
|
-
> **Caveat — malformed/copy-pasted JD text:** required/preferred detection
|
|
123
|
+
> **Caveat — malformed/copy-pasted JD text:** required/preferred detection combines two heuristics: header-scoped blocks (a line that's just "Requirements:"/"Preferred:"/"Must have:"/"Nice to have:" scopes the bulleted lines that follow it) and line-local trigger phrases (`required`, `must`, `nice to have`, `preferred`) as a fallback. Job postings copy-pasted from a wrapped/columned source can still split a header or trigger phrase across line breaks (e.g. `"Nice to\n\nhave..."`), which drops that block into the unweighted body-keyword bucket instead of required/preferred. Skill keywords themselves (e.g. `react`, `python/fastapi`) are still picked up via the whole-text token scan and unaffected. If a JD looks oddly broken, paste it through a plain-text cleanup pass first, or expect required/preferred weighting to under-count.
|
|
109
124
|
|
|
110
125
|
The `education` sub-score normalizes degree abbreviations on both sides to a canonical level (`bachelor`, `master`, `phd`, `mba`, `associate`) before comparing — so a resume listing "B.S. Computer Science" satisfies a JD requiring "Bachelor's degree".
|
|
111
126
|
|
|
@@ -121,13 +136,24 @@ const result = analyzeResume({
|
|
|
121
136
|
jobDescription: "...",
|
|
122
137
|
config: {
|
|
123
138
|
// Override scoring weights (auto-normalized to sum to 1)
|
|
124
|
-
weights: {
|
|
139
|
+
weights: {
|
|
140
|
+
skills: 0.3,
|
|
141
|
+
experience: 0.2,
|
|
142
|
+
keywords: 0.2,
|
|
143
|
+
parseability: 0.2,
|
|
144
|
+
education: 0.1,
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Fuzzy/stem matching is on by default; disable for exact-match-only (v1 behavior)
|
|
148
|
+
matching: { fuzzy: false },
|
|
125
149
|
|
|
126
150
|
// Additional skill synonyms merged over built-in defaults
|
|
127
151
|
skillAliases: { javascript: ["js", "ecmascript"] },
|
|
128
152
|
|
|
129
153
|
// Categorized keyword/alias entries; merges over the default registry by canonical term
|
|
130
|
-
keywordRegistry: [
|
|
154
|
+
keywordRegistry: [
|
|
155
|
+
{ canonical: "rust", aliases: ["rustlang"], category: "technical" },
|
|
156
|
+
],
|
|
131
157
|
|
|
132
158
|
// Industry profile: sets mandatory/optional skills and minExperience
|
|
133
159
|
profile: {
|
|
@@ -163,17 +189,20 @@ const result = analyzeResume({
|
|
|
163
189
|
|
|
164
190
|
### Defaults
|
|
165
191
|
|
|
166
|
-
| Setting
|
|
167
|
-
|
|
168
|
-
| `weights.skills`
|
|
169
|
-
| `weights.experience`
|
|
170
|
-
| `weights.keywords`
|
|
171
|
-
| `weights.
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `keywordDensity.
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
192
|
+
| Setting | Default |
|
|
193
|
+
| --------------------------------- | ------------------------------------------------------------------------------ |
|
|
194
|
+
| `weights.skills` | `0.25` |
|
|
195
|
+
| `weights.experience` | `0.20` |
|
|
196
|
+
| `weights.keywords` | `0.25` |
|
|
197
|
+
| `weights.parseability` | `0.20` |
|
|
198
|
+
| `weights.education` | `0.10` |
|
|
199
|
+
| `matching.fuzzy` | `true` (stemmed/fuzzy skill & keyword matching) |
|
|
200
|
+
| `keywordDensity.min` | `0.0025` |
|
|
201
|
+
| `keywordDensity.max` | `0.04` |
|
|
202
|
+
| `keywordDensity.overusePenalty` | `5` |
|
|
203
|
+
| `allowPartialMatches` | `true` |
|
|
204
|
+
| `referenceDate` | Current date (use explicit ISO string for determinism) |
|
|
205
|
+
| `sectionPenalties.missingContact` | `12` (no parseable email detected — a real ATS treats this as a near-knockout) |
|
|
177
206
|
|
|
178
207
|
See [Configuration docs](https://pranavraut033.github.io/ats-checker/docs/configuration/) for all options.
|
|
179
208
|
|
|
@@ -181,10 +210,13 @@ See [Configuration docs](https://pranavraut033.github.io/ats-checker/docs/config
|
|
|
181
210
|
|
|
182
211
|
## Keyword Registry, Categories & Aliases
|
|
183
212
|
|
|
184
|
-
Every built-in keyword/skill belongs to a `KeywordRegistry` entry — a canonical term, its aliases, and a category (`technical` | `tool` | `concept` | `soft` | `marketing` | `domain`).
|
|
213
|
+
Every built-in keyword/skill belongs to a `KeywordRegistry` entry — a canonical term, its aliases, and a category (`technical` | `tool` | `concept` | `soft` | `marketing` | `domain`). The default registry ships **407 canonical terms** across cloud/infra, databases, frameworks, ML/AI, testing, security/compliance, soft skills, marketing, and PM/agile vocabulary, with common surface-form aliases pre-loaded so `js` matches `javascript`, `k8s` matches `kubernetes`, `reactjs`/`react.js` match `react`, etc. — and typos/word-form variants match too via fuzzy/stem matching (on by default, see [Configuration](#configuration)).
|
|
185
214
|
|
|
186
215
|
```typescript
|
|
187
|
-
import {
|
|
216
|
+
import {
|
|
217
|
+
defaultKeywordRegistry,
|
|
218
|
+
defaultSkillAliases,
|
|
219
|
+
} from "@pranavraut033/ats-checker";
|
|
188
220
|
// defaultKeywordRegistry: [{ canonical: "javascript", aliases: ["js"], category: "technical" }, ...]
|
|
189
221
|
// defaultSkillAliases: { javascript: ["js"], node: ["node.js", "nodejs"], ... } (derived, back-compat)
|
|
190
222
|
```
|
|
@@ -198,13 +230,17 @@ const result = analyzeResume({
|
|
|
198
230
|
config: {
|
|
199
231
|
keywordRegistry: [
|
|
200
232
|
{ canonical: "rust", aliases: ["rustlang"], category: "technical" },
|
|
201
|
-
{
|
|
233
|
+
{
|
|
234
|
+
canonical: "javascript",
|
|
235
|
+
aliases: ["js", "ecmascript"],
|
|
236
|
+
category: "technical",
|
|
237
|
+
}, // overrides default
|
|
202
238
|
],
|
|
203
239
|
},
|
|
204
240
|
});
|
|
205
241
|
|
|
206
242
|
console.log(result.keywordsByCategory.technical); // { matched: [...], missing: [...] }
|
|
207
|
-
console.log(result.keywordWeights);
|
|
243
|
+
console.log(result.keywordWeights); // [{ term, category, jdWeight, resumeWeight, importance }, ...]
|
|
208
244
|
```
|
|
209
245
|
|
|
210
246
|
You can still pass `config.skillAliases` for a flat override — it merges on top of the registry-derived aliases.
|
|
@@ -304,7 +340,9 @@ const resumeText = await extractTextFromPDF(bytes, {
|
|
|
304
340
|
ocrFallback: async (data) => {
|
|
305
341
|
// bring your own OCR engine, e.g. tesseract.js or a cloud OCR API
|
|
306
342
|
const { recognize } = await import("tesseract.js");
|
|
307
|
-
const {
|
|
343
|
+
const {
|
|
344
|
+
data: { text },
|
|
345
|
+
} = await recognize(data, "eng");
|
|
308
346
|
return text;
|
|
309
347
|
},
|
|
310
348
|
});
|