@pranavraut033/ats-checker 1.1.0 → 1.1.1
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 +2 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +219 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +219 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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,9 +527,16 @@ 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 = [];
|
|
387
542
|
for (const section of requiredSections) {
|
|
@@ -584,10 +739,11 @@ function calculateScore(resume, job, config) {
|
|
|
584
739
|
overusedKeywords: keywordResult.overusedKeywords,
|
|
585
740
|
suggestions: [],
|
|
586
741
|
warnings: [],
|
|
587
|
-
// detectedSections / parsedExperienceYears / experienceGap: filled by index.ts
|
|
742
|
+
// detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
|
|
588
743
|
experienceGap: experienceResult.missingYears,
|
|
589
744
|
detectedSections: [],
|
|
590
745
|
parsedExperienceYears: 0,
|
|
746
|
+
experienceEntries: [],
|
|
591
747
|
missingExperienceYears: experienceResult.missingYears,
|
|
592
748
|
educationScore
|
|
593
749
|
};
|
|
@@ -611,7 +767,31 @@ var defaultSkillAliases = {
|
|
|
611
767
|
docker: ["containers"],
|
|
612
768
|
kubernetes: ["k8s"],
|
|
613
769
|
html: ["html5"],
|
|
614
|
-
css: ["css3"]
|
|
770
|
+
css: ["css3"],
|
|
771
|
+
// ML / data science
|
|
772
|
+
pytorch: ["torch"],
|
|
773
|
+
tensorflow: ["tf"],
|
|
774
|
+
"scikit-learn": ["sklearn"],
|
|
775
|
+
pandas: [],
|
|
776
|
+
numpy: [],
|
|
777
|
+
fastapi: [],
|
|
778
|
+
flask: [],
|
|
779
|
+
django: [],
|
|
780
|
+
// data / infra
|
|
781
|
+
kafka: [],
|
|
782
|
+
redis: [],
|
|
783
|
+
elasticsearch: ["elastic"],
|
|
784
|
+
spark: ["apache spark"],
|
|
785
|
+
// common pure-letter tech skills (no symbol chars)
|
|
786
|
+
accessibility: ["a11y"],
|
|
787
|
+
frontend: ["front-end"],
|
|
788
|
+
backend: ["back-end"],
|
|
789
|
+
security: ["cybersecurity"],
|
|
790
|
+
testing: ["unittest", "pytest"],
|
|
791
|
+
microservices: [],
|
|
792
|
+
agile: ["scrum"],
|
|
793
|
+
blockchain: [],
|
|
794
|
+
devops: []
|
|
615
795
|
};
|
|
616
796
|
var softwareEngineerProfile = {
|
|
617
797
|
name: "software-engineer",
|
|
@@ -1398,6 +1578,7 @@ function analyzeResume(input) {
|
|
|
1398
1578
|
experienceGap: scoring.experienceGap,
|
|
1399
1579
|
detectedSections: parsedResume.detectedSections,
|
|
1400
1580
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
1581
|
+
experienceEntries: parsedResume.experience,
|
|
1401
1582
|
suggestions,
|
|
1402
1583
|
warnings: [...suggestionResult.warnings, ...llmWarnings]
|
|
1403
1584
|
};
|
|
@@ -1462,6 +1643,7 @@ async function analyzeResumeAsync(input) {
|
|
|
1462
1643
|
experienceGap: scoring.experienceGap,
|
|
1463
1644
|
detectedSections: parsedResume.detectedSections,
|
|
1464
1645
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
1646
|
+
experienceEntries: parsedResume.experience,
|
|
1465
1647
|
suggestions,
|
|
1466
1648
|
warnings: [...suggestionResult.warnings, ...llmWarnings]
|
|
1467
1649
|
};
|