@pranavraut033/ats-checker 1.1.0 → 1.2.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 +38 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +234 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +234 -37
- package/dist/index.mjs.map +1 -1
- package/dist/pdf/index.d.mts +12 -0
- package/dist/pdf/index.d.ts +12 -0
- package/dist/pdf/index.js +81 -0
- package/dist/pdf/index.js.map +1 -0
- package/dist/pdf/index.mjs +79 -0
- package/dist/pdf/index.mjs.map +1 -0
- package/package.json +19 -4
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/utils/text.ts
|
|
2
2
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
3
|
+
// articles / prepositions / conjunctions
|
|
3
4
|
"the",
|
|
4
5
|
"and",
|
|
5
6
|
"or",
|
|
@@ -15,12 +16,16 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
15
16
|
"by",
|
|
16
17
|
"from",
|
|
17
18
|
"as",
|
|
19
|
+
"into",
|
|
20
|
+
"onto",
|
|
21
|
+
"upon",
|
|
22
|
+
"via",
|
|
23
|
+
"per",
|
|
24
|
+
"plus",
|
|
25
|
+
// verbs / modals
|
|
18
26
|
"is",
|
|
19
27
|
"are",
|
|
20
28
|
"be",
|
|
21
|
-
"this",
|
|
22
|
-
"that",
|
|
23
|
-
"it",
|
|
24
29
|
"was",
|
|
25
30
|
"were",
|
|
26
31
|
"will",
|
|
@@ -29,7 +34,98 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
29
34
|
"must",
|
|
30
35
|
"have",
|
|
31
36
|
"has",
|
|
32
|
-
"had"
|
|
37
|
+
"had",
|
|
38
|
+
"do",
|
|
39
|
+
"does",
|
|
40
|
+
"did",
|
|
41
|
+
"get",
|
|
42
|
+
"give",
|
|
43
|
+
"go",
|
|
44
|
+
"use",
|
|
45
|
+
"see",
|
|
46
|
+
"help",
|
|
47
|
+
"work",
|
|
48
|
+
"build",
|
|
49
|
+
"show",
|
|
50
|
+
"need",
|
|
51
|
+
"want",
|
|
52
|
+
"make",
|
|
53
|
+
"let",
|
|
54
|
+
// pronouns / determiners
|
|
55
|
+
"it",
|
|
56
|
+
"its",
|
|
57
|
+
"this",
|
|
58
|
+
"that",
|
|
59
|
+
"these",
|
|
60
|
+
"those",
|
|
61
|
+
"we",
|
|
62
|
+
"our",
|
|
63
|
+
"you",
|
|
64
|
+
"your",
|
|
65
|
+
"they",
|
|
66
|
+
"their",
|
|
67
|
+
"us",
|
|
68
|
+
"who",
|
|
69
|
+
"what",
|
|
70
|
+
"which",
|
|
71
|
+
"how",
|
|
72
|
+
// common English fillers that leak into JDs
|
|
73
|
+
"no",
|
|
74
|
+
"not",
|
|
75
|
+
"all",
|
|
76
|
+
"any",
|
|
77
|
+
"also",
|
|
78
|
+
"more",
|
|
79
|
+
"well",
|
|
80
|
+
"very",
|
|
81
|
+
"highly",
|
|
82
|
+
"across",
|
|
83
|
+
"over",
|
|
84
|
+
"under",
|
|
85
|
+
"within",
|
|
86
|
+
"about",
|
|
87
|
+
"out",
|
|
88
|
+
"up",
|
|
89
|
+
"down",
|
|
90
|
+
"new",
|
|
91
|
+
"if",
|
|
92
|
+
"so",
|
|
93
|
+
"such",
|
|
94
|
+
"both",
|
|
95
|
+
"each",
|
|
96
|
+
"one",
|
|
97
|
+
"many",
|
|
98
|
+
"only",
|
|
99
|
+
// JD/HR boilerplate — never skills
|
|
100
|
+
"years",
|
|
101
|
+
"year",
|
|
102
|
+
"experience",
|
|
103
|
+
"required",
|
|
104
|
+
"requirement",
|
|
105
|
+
"requirements",
|
|
106
|
+
"preferred",
|
|
107
|
+
"role",
|
|
108
|
+
"degree",
|
|
109
|
+
"practices",
|
|
110
|
+
"best",
|
|
111
|
+
"skills",
|
|
112
|
+
"team",
|
|
113
|
+
"field",
|
|
114
|
+
"related",
|
|
115
|
+
"relevant",
|
|
116
|
+
"desired",
|
|
117
|
+
"strong",
|
|
118
|
+
"solid",
|
|
119
|
+
"good",
|
|
120
|
+
"first",
|
|
121
|
+
"based",
|
|
122
|
+
"day",
|
|
123
|
+
"week",
|
|
124
|
+
"month",
|
|
125
|
+
"time",
|
|
126
|
+
"fast",
|
|
127
|
+
"open",
|
|
128
|
+
"dynamic"
|
|
33
129
|
]);
|
|
34
130
|
function normalizeWhitespace(text) {
|
|
35
131
|
return text.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim();
|
|
@@ -43,7 +139,12 @@ function splitLines(text) {
|
|
|
43
139
|
var TECH_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/g;
|
|
44
140
|
function tokenize(text) {
|
|
45
141
|
const normalized = normalizeForComparison(text);
|
|
46
|
-
return (normalized.match(TECH_TOKEN_RE) ?? []).filter(
|
|
142
|
+
return (normalized.match(TECH_TOKEN_RE) ?? []).filter(
|
|
143
|
+
(t) => /[a-z]/.test(t) && !STOP_WORDS.has(t)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
function escapeRegExp(input) {
|
|
147
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
148
|
}
|
|
48
149
|
function unique(values) {
|
|
49
150
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -99,37 +200,30 @@ function normalizeSkills(skills, aliases) {
|
|
|
99
200
|
}
|
|
100
201
|
|
|
101
202
|
// src/core/parser/jd.parser.ts
|
|
102
|
-
var
|
|
103
|
-
"bachelor",
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"m.s",
|
|
109
|
-
"ms",
|
|
110
|
-
"msc",
|
|
111
|
-
"phd",
|
|
112
|
-
"doctorate",
|
|
113
|
-
"mba",
|
|
114
|
-
"associate"
|
|
203
|
+
var DEGREE_VARIANTS = [
|
|
204
|
+
[/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc\.?)\b/i, "bachelor"],
|
|
205
|
+
[/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc\.?)\b/i, "master"],
|
|
206
|
+
[/\b(?:phd|ph\.d\.?|doctorate)\b/i, "phd"],
|
|
207
|
+
[/\bmba\b/i, "mba"],
|
|
208
|
+
[/\bassociate(?:'s)?\b/i, "associate"]
|
|
115
209
|
];
|
|
116
210
|
function extractRequiredSkills(lines) {
|
|
117
211
|
const required = [];
|
|
118
212
|
for (const line of lines) {
|
|
119
213
|
if (/must|require|required|need/i.test(line)) {
|
|
120
|
-
required.push(...line
|
|
214
|
+
required.push(...tokenize(line));
|
|
121
215
|
}
|
|
122
216
|
}
|
|
123
|
-
return required
|
|
217
|
+
return required;
|
|
124
218
|
}
|
|
125
219
|
function extractPreferredSkills(lines) {
|
|
126
220
|
const preferred = [];
|
|
127
221
|
for (const line of lines) {
|
|
128
222
|
if (/preferred|nice to have|plus/i.test(line)) {
|
|
129
|
-
preferred.push(...line
|
|
223
|
+
preferred.push(...tokenize(line));
|
|
130
224
|
}
|
|
131
225
|
}
|
|
132
|
-
return preferred
|
|
226
|
+
return preferred;
|
|
133
227
|
}
|
|
134
228
|
function extractRoleKeywords(text) {
|
|
135
229
|
const roleMatch = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect)/i);
|
|
@@ -144,23 +238,41 @@ function extractMinExperience(text) {
|
|
|
144
238
|
return void 0;
|
|
145
239
|
}
|
|
146
240
|
function extractEducationRequirements(text) {
|
|
147
|
-
const
|
|
148
|
-
|
|
241
|
+
const found = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const [pattern, canonical] of DEGREE_VARIANTS) {
|
|
243
|
+
if (pattern.test(text)) found.add(canonical);
|
|
244
|
+
}
|
|
245
|
+
return [...found];
|
|
149
246
|
}
|
|
150
247
|
function parseJobDescription(jobDescription, config) {
|
|
151
248
|
const normalizedText = normalizeWhitespace(jobDescription);
|
|
152
249
|
const lines = splitLines(jobDescription);
|
|
153
|
-
const
|
|
154
|
-
const
|
|
250
|
+
const skillVocab = /* @__PURE__ */ new Set();
|
|
251
|
+
for (const [canonical, aliases] of Object.entries(config.skillAliases)) {
|
|
252
|
+
skillVocab.add(canonical.toLowerCase());
|
|
253
|
+
for (const alias of aliases) skillVocab.add(alias.toLowerCase());
|
|
254
|
+
}
|
|
255
|
+
for (const s of config.profile?.mandatorySkills ?? []) skillVocab.add(s.toLowerCase());
|
|
256
|
+
for (const s of config.profile?.optionalSkills ?? []) skillVocab.add(s.toLowerCase());
|
|
257
|
+
const isSkillLike = (t) => {
|
|
258
|
+
if (skillVocab.has(t)) return true;
|
|
259
|
+
if (/[.#+]/.test(t) && /[a-z]/.test(t)) return true;
|
|
260
|
+
if (t.includes("/")) return t.split("/").some((p) => p.length >= 2 && !STOP_WORDS.has(p));
|
|
261
|
+
return false;
|
|
262
|
+
};
|
|
263
|
+
const requiredSkillsRaw = extractRequiredSkills(lines).filter(isSkillLike);
|
|
264
|
+
const preferredSkillsRaw = extractPreferredSkills(lines).filter(isSkillLike);
|
|
155
265
|
const requiredSkills = normalizeSkills(requiredSkillsRaw, config.skillAliases);
|
|
156
266
|
const preferredSkills = normalizeSkills(preferredSkillsRaw, config.skillAliases);
|
|
157
|
-
const
|
|
267
|
+
const bodyTokens = tokenize(normalizedText).filter(isSkillLike);
|
|
268
|
+
const roleKeywords = extractRoleKeywords(jobDescription);
|
|
269
|
+
const keywords = unique([...requiredSkills, ...preferredSkills, ...roleKeywords, ...bodyTokens]);
|
|
158
270
|
return {
|
|
159
271
|
raw: jobDescription,
|
|
160
272
|
normalizedText,
|
|
161
273
|
requiredSkills,
|
|
162
274
|
preferredSkills,
|
|
163
|
-
roleKeywords
|
|
275
|
+
roleKeywords,
|
|
164
276
|
keywords,
|
|
165
277
|
minExperienceYears: extractMinExperience(jobDescription),
|
|
166
278
|
educationRequirements: extractEducationRequirements(jobDescription)
|
|
@@ -205,6 +317,14 @@ function parseDateToken(raw) {
|
|
|
205
317
|
return { year, month };
|
|
206
318
|
}
|
|
207
319
|
}
|
|
320
|
+
const slashMatch = cleaned.match(/^(\d{1,2})\/(\d{4})$/);
|
|
321
|
+
if (slashMatch) {
|
|
322
|
+
const month = Number.parseInt(slashMatch[1], 10);
|
|
323
|
+
const year = Number.parseInt(slashMatch[2], 10);
|
|
324
|
+
if (month >= 1 && month <= 12 && !Number.isNaN(year)) {
|
|
325
|
+
return { year, month };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
208
328
|
const yearMatch = cleaned.match(/(20\d{2}|19\d{2})/);
|
|
209
329
|
if (yearMatch) {
|
|
210
330
|
const year = Number.parseInt(yearMatch[1], 10);
|
|
@@ -219,7 +339,9 @@ function monthsBetween(start, end) {
|
|
|
219
339
|
}
|
|
220
340
|
function parseDateRange(text, referenceDate) {
|
|
221
341
|
const normalized = text.trim();
|
|
222
|
-
const rangeMatch = normalized.match(
|
|
342
|
+
const rangeMatch = normalized.match(
|
|
343
|
+
/(\d{1,2}\/\d{4}|[A-Za-z]{3,9}\s+\d{4}|\d{4})\s*(?:-|to|–|—)\s*(Present|Current|Now|\d{1,2}\/\d{4}|[A-Za-z]{3,9}\s+\d{4}|\d{4})/i
|
|
344
|
+
);
|
|
223
345
|
if (!rangeMatch) {
|
|
224
346
|
return null;
|
|
225
347
|
}
|
|
@@ -240,11 +362,40 @@ function parseDateRange(text, referenceDate) {
|
|
|
240
362
|
raw: normalized,
|
|
241
363
|
start: rangeMatch[1],
|
|
242
364
|
end: isPresent ? "present" : rangeMatch[2],
|
|
243
|
-
durationInMonths: durationInMonths > 0 ? durationInMonths : void 0
|
|
365
|
+
durationInMonths: durationInMonths > 0 ? durationInMonths : void 0,
|
|
366
|
+
startYear: startToken.year,
|
|
367
|
+
startMonth: startToken.month,
|
|
368
|
+
endYear: endTokenResolved.year,
|
|
369
|
+
endMonth: endTokenResolved.month
|
|
244
370
|
};
|
|
245
371
|
}
|
|
246
372
|
function sumExperienceYears(ranges) {
|
|
247
|
-
const
|
|
373
|
+
const withBounds = ranges.filter(
|
|
374
|
+
(r) => r.startYear !== void 0 && r.endYear !== void 0
|
|
375
|
+
);
|
|
376
|
+
if (withBounds.length === ranges.length && ranges.length > 0) {
|
|
377
|
+
const toIndex = (year, month) => year * 12 + month;
|
|
378
|
+
const intervals = withBounds.map((r) => ({
|
|
379
|
+
s: toIndex(r.startYear, r.startMonth ?? 1),
|
|
380
|
+
e: toIndex(r.endYear, r.endMonth ?? 12)
|
|
381
|
+
})).sort((a, b) => a.s - b.s);
|
|
382
|
+
let totalMonths = 0;
|
|
383
|
+
let curStart = intervals[0].s;
|
|
384
|
+
let curEnd = intervals[0].e;
|
|
385
|
+
for (let i = 1; i < intervals.length; i++) {
|
|
386
|
+
const { s, e } = intervals[i];
|
|
387
|
+
if (s <= curEnd) {
|
|
388
|
+
curEnd = Math.max(curEnd, e);
|
|
389
|
+
} else {
|
|
390
|
+
totalMonths += curEnd - curStart + 1;
|
|
391
|
+
curStart = s;
|
|
392
|
+
curEnd = e;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
totalMonths += curEnd - curStart + 1;
|
|
396
|
+
return Number((totalMonths / 12).toFixed(2));
|
|
397
|
+
}
|
|
398
|
+
const months = ranges.reduce((total, r) => total + (r.durationInMonths ?? 0), 0);
|
|
248
399
|
return Number((months / 12).toFixed(2));
|
|
249
400
|
}
|
|
250
401
|
|
|
@@ -278,9 +429,6 @@ var ACTION_VERBS = [
|
|
|
278
429
|
"reduced",
|
|
279
430
|
"increased"
|
|
280
431
|
];
|
|
281
|
-
function escapeRegExp(input) {
|
|
282
|
-
return input.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
|
|
283
|
-
}
|
|
284
432
|
function detectSection(line) {
|
|
285
433
|
const normalized = line.trim().toLowerCase();
|
|
286
434
|
for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
|
|
@@ -379,11 +527,28 @@ function parseResume(resumeText, config) {
|
|
|
379
527
|
const actionVerbs = parseActionVerbs(normalizedText);
|
|
380
528
|
const experienceData = parseExperience(sections.experience, config.referenceDate);
|
|
381
529
|
const educationEntries = parseEducation(sections.education);
|
|
382
|
-
|
|
530
|
+
let totalExperienceYears = sumExperienceYears(
|
|
383
531
|
experienceData.entries.map((entry) => entry.dates).filter((range) => Boolean(range))
|
|
384
532
|
);
|
|
533
|
+
if (totalExperienceYears === 0) {
|
|
534
|
+
const textToScan = sections.summary ?? normalizedText;
|
|
535
|
+
const yearsMatch = textToScan.match(/(\d{1,2})\+?\s*years?/i);
|
|
536
|
+
if (yearsMatch) {
|
|
537
|
+
totalExperienceYears = Number.parseInt(yearsMatch[1], 10);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
385
540
|
const requiredSections = ["summary", "experience", "skills", "education"];
|
|
386
541
|
const warnings = [];
|
|
542
|
+
const lineCount = splitLines(resumeText).length;
|
|
543
|
+
if (resumeText.trim().length < 100) {
|
|
544
|
+
warnings.push(
|
|
545
|
+
"Almost no text was extracted \u2014 the resume may be a scanned/image PDF. Upload a text-based PDF or paste the text directly."
|
|
546
|
+
);
|
|
547
|
+
} else if (lineCount <= 2) {
|
|
548
|
+
warnings.push(
|
|
549
|
+
"Resume text has no line breaks \u2014 the PDF layout likely didn't export cleanly (common with multi-column designs). Export as a single-column PDF or paste plain text for accurate parsing."
|
|
550
|
+
);
|
|
551
|
+
}
|
|
387
552
|
for (const section of requiredSections) {
|
|
388
553
|
if (!detected.includes(section)) {
|
|
389
554
|
warnings.push(`${section} section not detected`);
|
|
@@ -584,10 +749,11 @@ function calculateScore(resume, job, config) {
|
|
|
584
749
|
overusedKeywords: keywordResult.overusedKeywords,
|
|
585
750
|
suggestions: [],
|
|
586
751
|
warnings: [],
|
|
587
|
-
// detectedSections / parsedExperienceYears / experienceGap: filled by index.ts
|
|
752
|
+
// detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
|
|
588
753
|
experienceGap: experienceResult.missingYears,
|
|
589
754
|
detectedSections: [],
|
|
590
755
|
parsedExperienceYears: 0,
|
|
756
|
+
experienceEntries: [],
|
|
591
757
|
missingExperienceYears: experienceResult.missingYears,
|
|
592
758
|
educationScore
|
|
593
759
|
};
|
|
@@ -611,7 +777,31 @@ var defaultSkillAliases = {
|
|
|
611
777
|
docker: ["containers"],
|
|
612
778
|
kubernetes: ["k8s"],
|
|
613
779
|
html: ["html5"],
|
|
614
|
-
css: ["css3"]
|
|
780
|
+
css: ["css3"],
|
|
781
|
+
// ML / data science
|
|
782
|
+
pytorch: ["torch"],
|
|
783
|
+
tensorflow: ["tf"],
|
|
784
|
+
"scikit-learn": ["sklearn"],
|
|
785
|
+
pandas: [],
|
|
786
|
+
numpy: [],
|
|
787
|
+
fastapi: [],
|
|
788
|
+
flask: [],
|
|
789
|
+
django: [],
|
|
790
|
+
// data / infra
|
|
791
|
+
kafka: [],
|
|
792
|
+
redis: [],
|
|
793
|
+
elasticsearch: ["elastic"],
|
|
794
|
+
spark: ["apache spark"],
|
|
795
|
+
// common pure-letter tech skills (no symbol chars)
|
|
796
|
+
accessibility: ["a11y"],
|
|
797
|
+
frontend: ["front-end"],
|
|
798
|
+
backend: ["back-end"],
|
|
799
|
+
security: ["cybersecurity"],
|
|
800
|
+
testing: ["unittest", "pytest"],
|
|
801
|
+
microservices: [],
|
|
802
|
+
agile: ["scrum"],
|
|
803
|
+
blockchain: [],
|
|
804
|
+
devops: []
|
|
615
805
|
};
|
|
616
806
|
var softwareEngineerProfile = {
|
|
617
807
|
name: "software-engineer",
|
|
@@ -738,6 +928,11 @@ var SuggestionEngine = class {
|
|
|
738
928
|
"Strengthen bullet points with impact verbs (led, built, improved, delivered)."
|
|
739
929
|
);
|
|
740
930
|
}
|
|
931
|
+
if (input.resume.detectedSections.length < 2 && input.resume.raw.trim().length > 300) {
|
|
932
|
+
suggestions.push(
|
|
933
|
+
"Your resume may use a multi-column layout. Export as a single-column PDF or paste plain text \u2014 most ATS systems and this parser work best with a linear layout."
|
|
934
|
+
);
|
|
935
|
+
}
|
|
741
936
|
return { suggestions, warnings };
|
|
742
937
|
}
|
|
743
938
|
};
|
|
@@ -1398,6 +1593,7 @@ function analyzeResume(input) {
|
|
|
1398
1593
|
experienceGap: scoring.experienceGap,
|
|
1399
1594
|
detectedSections: parsedResume.detectedSections,
|
|
1400
1595
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
1596
|
+
experienceEntries: parsedResume.experience,
|
|
1401
1597
|
suggestions,
|
|
1402
1598
|
warnings: [...suggestionResult.warnings, ...llmWarnings]
|
|
1403
1599
|
};
|
|
@@ -1462,6 +1658,7 @@ async function analyzeResumeAsync(input) {
|
|
|
1462
1658
|
experienceGap: scoring.experienceGap,
|
|
1463
1659
|
detectedSections: parsedResume.detectedSections,
|
|
1464
1660
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
1661
|
+
experienceEntries: parsedResume.experience,
|
|
1465
1662
|
suggestions,
|
|
1466
1663
|
warnings: [...suggestionResult.warnings, ...llmWarnings]
|
|
1467
1664
|
};
|