@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/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((t) => !STOP_WORDS.has(t));
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 DEGREE_KEYWORDS = [
103
- "bachelor",
104
- "b.s",
105
- "bs",
106
- "bsc",
107
- "master",
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.split(/[,.;•-]/));
214
+ required.push(...tokenize(line));
121
215
  }
122
216
  }
123
- return required.map((value) => value.trim()).filter(Boolean);
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.split(/[,.;•-]/));
223
+ preferred.push(...tokenize(line));
130
224
  }
131
225
  }
132
- return preferred.map((value) => value.trim()).filter(Boolean);
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 normalized = normalizeForComparison(text);
148
- return DEGREE_KEYWORDS.filter((degree) => normalized.includes(degree));
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 requiredSkillsRaw = extractRequiredSkills(lines);
154
- const preferredSkillsRaw = extractPreferredSkills(lines);
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 keywords = unique([...requiredSkills, ...preferredSkills, ...tokenize(normalizedText)]);
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: extractRoleKeywords(jobDescription),
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(/([A-Za-z]{3,9}\s+\d{4}|\d{4})\s*(?:-|to|–|—)\s*(Present|Current|Now|[A-Za-z]{3,9}\s+\d{4}|\d{4})/i);
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 months = ranges.map((range) => range.durationInMonths ?? 0).reduce((total, value) => total + value, 0);
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
- const totalExperienceYears = sumExperienceYears(
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
  };