@pranavraut033/ats-checker 1.0.5 → 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.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/utils/text.ts
4
4
  var STOP_WORDS = /* @__PURE__ */ new Set([
5
+ // articles / prepositions / conjunctions
5
6
  "the",
6
7
  "and",
7
8
  "or",
@@ -17,12 +18,16 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
17
18
  "by",
18
19
  "from",
19
20
  "as",
21
+ "into",
22
+ "onto",
23
+ "upon",
24
+ "via",
25
+ "per",
26
+ "plus",
27
+ // verbs / modals
20
28
  "is",
21
29
  "are",
22
30
  "be",
23
- "this",
24
- "that",
25
- "it",
26
31
  "was",
27
32
  "were",
28
33
  "will",
@@ -31,19 +36,117 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
31
36
  "must",
32
37
  "have",
33
38
  "has",
34
- "had"
39
+ "had",
40
+ "do",
41
+ "does",
42
+ "did",
43
+ "get",
44
+ "give",
45
+ "go",
46
+ "use",
47
+ "see",
48
+ "help",
49
+ "work",
50
+ "build",
51
+ "show",
52
+ "need",
53
+ "want",
54
+ "make",
55
+ "let",
56
+ // pronouns / determiners
57
+ "it",
58
+ "its",
59
+ "this",
60
+ "that",
61
+ "these",
62
+ "those",
63
+ "we",
64
+ "our",
65
+ "you",
66
+ "your",
67
+ "they",
68
+ "their",
69
+ "us",
70
+ "who",
71
+ "what",
72
+ "which",
73
+ "how",
74
+ // common English fillers that leak into JDs
75
+ "no",
76
+ "not",
77
+ "all",
78
+ "any",
79
+ "also",
80
+ "more",
81
+ "well",
82
+ "very",
83
+ "highly",
84
+ "across",
85
+ "over",
86
+ "under",
87
+ "within",
88
+ "about",
89
+ "out",
90
+ "up",
91
+ "down",
92
+ "new",
93
+ "if",
94
+ "so",
95
+ "such",
96
+ "both",
97
+ "each",
98
+ "one",
99
+ "many",
100
+ "only",
101
+ // JD/HR boilerplate — never skills
102
+ "years",
103
+ "year",
104
+ "experience",
105
+ "required",
106
+ "requirement",
107
+ "requirements",
108
+ "preferred",
109
+ "role",
110
+ "degree",
111
+ "practices",
112
+ "best",
113
+ "skills",
114
+ "team",
115
+ "field",
116
+ "related",
117
+ "relevant",
118
+ "desired",
119
+ "strong",
120
+ "solid",
121
+ "good",
122
+ "first",
123
+ "based",
124
+ "day",
125
+ "week",
126
+ "month",
127
+ "time",
128
+ "fast",
129
+ "open",
130
+ "dynamic"
35
131
  ]);
36
132
  function normalizeWhitespace(text) {
37
133
  return text.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim();
38
134
  }
39
135
  function normalizeForComparison(text) {
40
- return normalizeWhitespace(text).toLowerCase();
136
+ return normalizeWhitespace(text).normalize("NFKC").toLowerCase();
41
137
  }
42
138
  function splitLines(text) {
43
139
  return text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean);
44
140
  }
141
+ var TECH_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/g;
45
142
  function tokenize(text) {
46
- return normalizeForComparison(text).split(/[^a-z0-9+]+/i).map((word) => word.trim()).filter((word) => word.length > 1 && !STOP_WORDS.has(word));
143
+ const normalized = normalizeForComparison(text);
144
+ return (normalized.match(TECH_TOKEN_RE) ?? []).filter(
145
+ (t) => /[a-z]/.test(t) && !STOP_WORDS.has(t)
146
+ );
147
+ }
148
+ function escapeRegExp(input) {
149
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
150
  }
48
151
  function unique(values) {
49
152
  const seen = /* @__PURE__ */ new Set();
@@ -99,37 +202,30 @@ function normalizeSkills(skills, aliases) {
99
202
  }
100
203
 
101
204
  // 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"
205
+ var DEGREE_VARIANTS = [
206
+ [/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc\.?)\b/i, "bachelor"],
207
+ [/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc\.?)\b/i, "master"],
208
+ [/\b(?:phd|ph\.d\.?|doctorate)\b/i, "phd"],
209
+ [/\bmba\b/i, "mba"],
210
+ [/\bassociate(?:'s)?\b/i, "associate"]
115
211
  ];
116
212
  function extractRequiredSkills(lines) {
117
213
  const required = [];
118
214
  for (const line of lines) {
119
215
  if (/must|require|required|need/i.test(line)) {
120
- required.push(...line.split(/[,.;•-]/));
216
+ required.push(...tokenize(line));
121
217
  }
122
218
  }
123
- return required.map((value) => value.trim()).filter(Boolean);
219
+ return required;
124
220
  }
125
221
  function extractPreferredSkills(lines) {
126
222
  const preferred = [];
127
223
  for (const line of lines) {
128
224
  if (/preferred|nice to have|plus/i.test(line)) {
129
- preferred.push(...line.split(/[,.;•-]/));
225
+ preferred.push(...tokenize(line));
130
226
  }
131
227
  }
132
- return preferred.map((value) => value.trim()).filter(Boolean);
228
+ return preferred;
133
229
  }
134
230
  function extractRoleKeywords(text) {
135
231
  const roleMatch = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect)/i);
@@ -144,23 +240,41 @@ function extractMinExperience(text) {
144
240
  return void 0;
145
241
  }
146
242
  function extractEducationRequirements(text) {
147
- const normalized = normalizeForComparison(text);
148
- return DEGREE_KEYWORDS.filter((degree) => normalized.includes(degree));
243
+ const found = /* @__PURE__ */ new Set();
244
+ for (const [pattern, canonical] of DEGREE_VARIANTS) {
245
+ if (pattern.test(text)) found.add(canonical);
246
+ }
247
+ return [...found];
149
248
  }
150
249
  function parseJobDescription(jobDescription, config) {
151
250
  const normalizedText = normalizeWhitespace(jobDescription);
152
251
  const lines = splitLines(jobDescription);
153
- const requiredSkillsRaw = extractRequiredSkills(lines);
154
- const preferredSkillsRaw = extractPreferredSkills(lines);
252
+ const skillVocab = /* @__PURE__ */ new Set();
253
+ for (const [canonical, aliases] of Object.entries(config.skillAliases)) {
254
+ skillVocab.add(canonical.toLowerCase());
255
+ for (const alias of aliases) skillVocab.add(alias.toLowerCase());
256
+ }
257
+ for (const s of config.profile?.mandatorySkills ?? []) skillVocab.add(s.toLowerCase());
258
+ for (const s of config.profile?.optionalSkills ?? []) skillVocab.add(s.toLowerCase());
259
+ const isSkillLike = (t) => {
260
+ if (skillVocab.has(t)) return true;
261
+ if (/[.#+]/.test(t) && /[a-z]/.test(t)) return true;
262
+ if (t.includes("/")) return t.split("/").some((p) => p.length >= 2 && !STOP_WORDS.has(p));
263
+ return false;
264
+ };
265
+ const requiredSkillsRaw = extractRequiredSkills(lines).filter(isSkillLike);
266
+ const preferredSkillsRaw = extractPreferredSkills(lines).filter(isSkillLike);
155
267
  const requiredSkills = normalizeSkills(requiredSkillsRaw, config.skillAliases);
156
268
  const preferredSkills = normalizeSkills(preferredSkillsRaw, config.skillAliases);
157
- const keywords = unique([...requiredSkills, ...preferredSkills, ...tokenize(normalizedText)]);
269
+ const bodyTokens = tokenize(normalizedText).filter(isSkillLike);
270
+ const roleKeywords = extractRoleKeywords(jobDescription);
271
+ const keywords = unique([...requiredSkills, ...preferredSkills, ...roleKeywords, ...bodyTokens]);
158
272
  return {
159
273
  raw: jobDescription,
160
274
  normalizedText,
161
275
  requiredSkills,
162
276
  preferredSkills,
163
- roleKeywords: extractRoleKeywords(jobDescription),
277
+ roleKeywords,
164
278
  keywords,
165
279
  minExperienceYears: extractMinExperience(jobDescription),
166
280
  educationRequirements: extractEducationRequirements(jobDescription)
@@ -205,6 +319,14 @@ function parseDateToken(raw) {
205
319
  return { year, month };
206
320
  }
207
321
  }
322
+ const slashMatch = cleaned.match(/^(\d{1,2})\/(\d{4})$/);
323
+ if (slashMatch) {
324
+ const month = Number.parseInt(slashMatch[1], 10);
325
+ const year = Number.parseInt(slashMatch[2], 10);
326
+ if (month >= 1 && month <= 12 && !Number.isNaN(year)) {
327
+ return { year, month };
328
+ }
329
+ }
208
330
  const yearMatch = cleaned.match(/(20\d{2}|19\d{2})/);
209
331
  if (yearMatch) {
210
332
  const year = Number.parseInt(yearMatch[1], 10);
@@ -217,9 +339,11 @@ function monthsBetween(start, end) {
217
339
  const endMonth = end.month ?? 12;
218
340
  return (end.year - start.year) * 12 + (endMonth - startMonth + 1);
219
341
  }
220
- function parseDateRange(text) {
342
+ function parseDateRange(text, referenceDate) {
221
343
  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);
344
+ const rangeMatch = normalized.match(
345
+ /(\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
346
+ );
223
347
  if (!rangeMatch) {
224
348
  return null;
225
349
  }
@@ -230,20 +354,50 @@ function parseDateRange(text) {
230
354
  if (!startToken) {
231
355
  return null;
232
356
  }
357
+ const ref = referenceDate ?? /* @__PURE__ */ new Date();
233
358
  const endTokenResolved = endToken ?? {
234
- year: (/* @__PURE__ */ new Date()).getFullYear(),
235
- month: (/* @__PURE__ */ new Date()).getMonth() + 1
359
+ year: ref.getFullYear(),
360
+ month: ref.getMonth() + 1
236
361
  };
237
362
  const durationInMonths = monthsBetween(startToken, endTokenResolved);
238
363
  return {
239
364
  raw: normalized,
240
365
  start: rangeMatch[1],
241
366
  end: isPresent ? "present" : rangeMatch[2],
242
- durationInMonths: durationInMonths > 0 ? durationInMonths : void 0
367
+ durationInMonths: durationInMonths > 0 ? durationInMonths : void 0,
368
+ startYear: startToken.year,
369
+ startMonth: startToken.month,
370
+ endYear: endTokenResolved.year,
371
+ endMonth: endTokenResolved.month
243
372
  };
244
373
  }
245
374
  function sumExperienceYears(ranges) {
246
- const months = ranges.map((range) => range.durationInMonths ?? 0).reduce((total, value) => total + value, 0);
375
+ const withBounds = ranges.filter(
376
+ (r) => r.startYear !== void 0 && r.endYear !== void 0
377
+ );
378
+ if (withBounds.length === ranges.length && ranges.length > 0) {
379
+ const toIndex = (year, month) => year * 12 + month;
380
+ const intervals = withBounds.map((r) => ({
381
+ s: toIndex(r.startYear, r.startMonth ?? 1),
382
+ e: toIndex(r.endYear, r.endMonth ?? 12)
383
+ })).sort((a, b) => a.s - b.s);
384
+ let totalMonths = 0;
385
+ let curStart = intervals[0].s;
386
+ let curEnd = intervals[0].e;
387
+ for (let i = 1; i < intervals.length; i++) {
388
+ const { s, e } = intervals[i];
389
+ if (s <= curEnd) {
390
+ curEnd = Math.max(curEnd, e);
391
+ } else {
392
+ totalMonths += curEnd - curStart + 1;
393
+ curStart = s;
394
+ curEnd = e;
395
+ }
396
+ }
397
+ totalMonths += curEnd - curStart + 1;
398
+ return Number((totalMonths / 12).toFixed(2));
399
+ }
400
+ const months = ranges.reduce((total, r) => total + (r.durationInMonths ?? 0), 0);
247
401
  return Number((months / 12).toFixed(2));
248
402
  }
249
403
 
@@ -277,9 +431,6 @@ var ACTION_VERBS = [
277
431
  "reduced",
278
432
  "increased"
279
433
  ];
280
- function escapeRegExp(input) {
281
- return input.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
282
- }
283
434
  function detectSection(line) {
284
435
  const normalized = line.trim().toLowerCase();
285
436
  for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
@@ -327,7 +478,7 @@ function parseActionVerbs(text) {
327
478
  const words = tokenize(text);
328
479
  return ACTION_VERBS.filter((verb) => words.includes(verb));
329
480
  }
330
- function parseExperience(sectionContent) {
481
+ function parseExperience(sectionContent, referenceDate) {
331
482
  if (!sectionContent) {
332
483
  return { entries: [], rangesInMonths: [], jobTitles: [] };
333
484
  }
@@ -336,7 +487,7 @@ function parseExperience(sectionContent) {
336
487
  const rangesInMonths = [];
337
488
  const jobTitles = [];
338
489
  for (const line of lines) {
339
- const range = parseDateRange(line);
490
+ const range = parseDateRange(line, referenceDate);
340
491
  if (range) {
341
492
  const previous = entries[entries.length - 1];
342
493
  if (previous && !previous.dates) {
@@ -376,11 +527,18 @@ function parseResume(resumeText, config) {
376
527
  const { sections, detected } = extractSections(resumeText);
377
528
  const skills = parseSkills(sections.skills, config.skillAliases);
378
529
  const actionVerbs = parseActionVerbs(normalizedText);
379
- const experienceData = parseExperience(sections.experience);
530
+ const experienceData = parseExperience(sections.experience, config.referenceDate);
380
531
  const educationEntries = parseEducation(sections.education);
381
- const totalExperienceYears = sumExperienceYears(
532
+ let totalExperienceYears = sumExperienceYears(
382
533
  experienceData.entries.map((entry) => entry.dates).filter((range) => Boolean(range))
383
534
  );
535
+ if (totalExperienceYears === 0) {
536
+ const textToScan = sections.summary ?? normalizedText;
537
+ const yearsMatch = textToScan.match(/(\d{1,2})\+?\s*years?/i);
538
+ if (yearsMatch) {
539
+ totalExperienceYears = Number.parseInt(yearsMatch[1], 10);
540
+ }
541
+ }
384
542
  const requiredSections = ["summary", "experience", "skills", "education"];
385
543
  const warnings = [];
386
544
  for (const section of requiredSections) {
@@ -500,8 +658,9 @@ function scoreSkills(resume, job, config) {
500
658
  0,
501
659
  100
502
660
  );
503
- const missing = [...required].filter((skill) => !resumeSkills.has(skill));
504
- return { score, missing };
661
+ const matched = [...required].filter((skill) => resumeSkills.has(skill)).sort();
662
+ const missing = [...required].filter((skill) => !resumeSkills.has(skill)).sort();
663
+ return { score, matched, missing };
505
664
  }
506
665
  function scoreExperience(resume, job, config) {
507
666
  const requiredYears = job.minExperienceYears ?? config.profile?.minExperience ?? 0;
@@ -519,11 +678,15 @@ function scoreExperience(resume, job, config) {
519
678
  return { score, missingYears: Number(missingYears.toFixed(2)) };
520
679
  }
521
680
  function scoreKeywords(resume, job, config) {
522
- const jobKeywordSet = new Set(job.keywords.map((value) => value.toLowerCase()));
681
+ const jobKeywordSet = new Set(
682
+ job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
683
+ );
523
684
  if (jobKeywordSet.size === 0) {
524
685
  return { score: 100, matchedKeywords: [], missingKeywords: [], overusedKeywords: [] };
525
686
  }
526
- const resumeTokens = tokenize(resume.normalizedText);
687
+ const resumeTokens = tokenize(resume.normalizedText).map(
688
+ (t) => normalizeSkill(t, config.skillAliases)
689
+ );
527
690
  const resumeTokenSet = new Set(resumeTokens);
528
691
  const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
529
692
  const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
@@ -537,9 +700,9 @@ function scoreKeywords(resume, job, config) {
537
700
  });
538
701
  return {
539
702
  score,
540
- matchedKeywords: unique(matchedKeywords),
541
- missingKeywords: unique(missingKeywords),
542
- overusedKeywords: unique(overusedKeywords)
703
+ matchedKeywords: unique(matchedKeywords).sort(),
704
+ missingKeywords: unique(missingKeywords).sort(),
705
+ overusedKeywords: unique(overusedKeywords).sort()
543
706
  };
544
707
  }
545
708
  function scoreEducation(resume, job) {
@@ -571,12 +734,18 @@ function calculateScore(resume, job, config) {
571
734
  return {
572
735
  score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
573
736
  breakdown,
737
+ matchedSkills: skillsResult.matched,
738
+ missingSkills: skillsResult.missing,
574
739
  matchedKeywords: keywordResult.matchedKeywords,
575
740
  missingKeywords: keywordResult.missingKeywords,
576
741
  overusedKeywords: keywordResult.overusedKeywords,
577
742
  suggestions: [],
578
743
  warnings: [],
579
- missingSkills: skillsResult.missing,
744
+ // detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
745
+ experienceGap: experienceResult.missingYears,
746
+ detectedSections: [],
747
+ parsedExperienceYears: 0,
748
+ experienceEntries: [],
580
749
  missingExperienceYears: experienceResult.missingYears,
581
750
  educationScore
582
751
  };
@@ -584,7 +753,9 @@ function calculateScore(resume, job, config) {
584
753
 
585
754
  // src/profiles/index.ts
586
755
  var defaultSkillAliases = {
587
- javascript: ["js", "node", "node.js", "nodejs"],
756
+ // ponytail: "node" split from javascript — Node.js runtime !== JS language
757
+ javascript: ["js"],
758
+ node: ["node.js", "nodejs"],
588
759
  typescript: ["ts"],
589
760
  react: ["reactjs", "react.js"],
590
761
  "c++": ["cpp"],
@@ -598,7 +769,31 @@ var defaultSkillAliases = {
598
769
  docker: ["containers"],
599
770
  kubernetes: ["k8s"],
600
771
  html: ["html5"],
601
- css: ["css3"]
772
+ css: ["css3"],
773
+ // ML / data science
774
+ pytorch: ["torch"],
775
+ tensorflow: ["tf"],
776
+ "scikit-learn": ["sklearn"],
777
+ pandas: [],
778
+ numpy: [],
779
+ fastapi: [],
780
+ flask: [],
781
+ django: [],
782
+ // data / infra
783
+ kafka: [],
784
+ redis: [],
785
+ elasticsearch: ["elastic"],
786
+ spark: ["apache spark"],
787
+ // common pure-letter tech skills (no symbol chars)
788
+ accessibility: ["a11y"],
789
+ frontend: ["front-end"],
790
+ backend: ["back-end"],
791
+ security: ["cybersecurity"],
792
+ testing: ["unittest", "pytest"],
793
+ microservices: [],
794
+ agile: ["scrum"],
795
+ blockchain: [],
796
+ devops: []
602
797
  };
603
798
  var softwareEngineerProfile = {
604
799
  name: "software-engineer",
@@ -679,7 +874,8 @@ function resolveConfig(config = {}) {
679
874
  ...DEFAULT_SECTION_PENALTIES,
680
875
  ...config.sectionPenalties ?? {}
681
876
  },
682
- allowPartialMatches: config.allowPartialMatches ?? true
877
+ allowPartialMatches: config.allowPartialMatches ?? true,
878
+ referenceDate: config.referenceDate ? new Date(config.referenceDate) : void 0
683
879
  };
684
880
  return resolved;
685
881
  }
@@ -1376,9 +1572,15 @@ function analyzeResume(input) {
1376
1572
  return {
1377
1573
  score: finalScore,
1378
1574
  breakdown: scoring.breakdown,
1575
+ matchedSkills: scoring.matchedSkills,
1576
+ missingSkills: scoring.missingSkills,
1379
1577
  matchedKeywords: scoring.matchedKeywords,
1380
1578
  missingKeywords: scoring.missingKeywords,
1381
1579
  overusedKeywords: scoring.overusedKeywords,
1580
+ experienceGap: scoring.experienceGap,
1581
+ detectedSections: parsedResume.detectedSections,
1582
+ parsedExperienceYears: parsedResume.totalExperienceYears,
1583
+ experienceEntries: parsedResume.experience,
1382
1584
  suggestions,
1383
1585
  warnings: [...suggestionResult.warnings, ...llmWarnings]
1384
1586
  };
@@ -1435,9 +1637,15 @@ async function analyzeResumeAsync(input) {
1435
1637
  return {
1436
1638
  score: finalScore,
1437
1639
  breakdown: scoring.breakdown,
1640
+ matchedSkills: scoring.matchedSkills,
1641
+ missingSkills: scoring.missingSkills,
1438
1642
  matchedKeywords: scoring.matchedKeywords,
1439
1643
  missingKeywords: scoring.missingKeywords,
1440
1644
  overusedKeywords: scoring.overusedKeywords,
1645
+ experienceGap: scoring.experienceGap,
1646
+ detectedSections: parsedResume.detectedSections,
1647
+ parsedExperienceYears: parsedResume.totalExperienceYears,
1648
+ experienceEntries: parsedResume.experience,
1441
1649
  suggestions,
1442
1650
  warnings: [...suggestionResult.warnings, ...llmWarnings]
1443
1651
  };