@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.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,19 +34,117 @@ 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();
36
132
  }
37
133
  function normalizeForComparison(text) {
38
- return normalizeWhitespace(text).toLowerCase();
134
+ return normalizeWhitespace(text).normalize("NFKC").toLowerCase();
39
135
  }
40
136
  function splitLines(text) {
41
137
  return text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean);
42
138
  }
139
+ var TECH_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/g;
43
140
  function tokenize(text) {
44
- return normalizeForComparison(text).split(/[^a-z0-9+]+/i).map((word) => word.trim()).filter((word) => word.length > 1 && !STOP_WORDS.has(word));
141
+ const normalized = normalizeForComparison(text);
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, "\\$&");
45
148
  }
46
149
  function unique(values) {
47
150
  const seen = /* @__PURE__ */ new Set();
@@ -97,37 +200,30 @@ function normalizeSkills(skills, aliases) {
97
200
  }
98
201
 
99
202
  // src/core/parser/jd.parser.ts
100
- var DEGREE_KEYWORDS = [
101
- "bachelor",
102
- "b.s",
103
- "bs",
104
- "bsc",
105
- "master",
106
- "m.s",
107
- "ms",
108
- "msc",
109
- "phd",
110
- "doctorate",
111
- "mba",
112
- "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"]
113
209
  ];
114
210
  function extractRequiredSkills(lines) {
115
211
  const required = [];
116
212
  for (const line of lines) {
117
213
  if (/must|require|required|need/i.test(line)) {
118
- required.push(...line.split(/[,.;•-]/));
214
+ required.push(...tokenize(line));
119
215
  }
120
216
  }
121
- return required.map((value) => value.trim()).filter(Boolean);
217
+ return required;
122
218
  }
123
219
  function extractPreferredSkills(lines) {
124
220
  const preferred = [];
125
221
  for (const line of lines) {
126
222
  if (/preferred|nice to have|plus/i.test(line)) {
127
- preferred.push(...line.split(/[,.;•-]/));
223
+ preferred.push(...tokenize(line));
128
224
  }
129
225
  }
130
- return preferred.map((value) => value.trim()).filter(Boolean);
226
+ return preferred;
131
227
  }
132
228
  function extractRoleKeywords(text) {
133
229
  const roleMatch = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect)/i);
@@ -142,23 +238,41 @@ function extractMinExperience(text) {
142
238
  return void 0;
143
239
  }
144
240
  function extractEducationRequirements(text) {
145
- const normalized = normalizeForComparison(text);
146
- 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];
147
246
  }
148
247
  function parseJobDescription(jobDescription, config) {
149
248
  const normalizedText = normalizeWhitespace(jobDescription);
150
249
  const lines = splitLines(jobDescription);
151
- const requiredSkillsRaw = extractRequiredSkills(lines);
152
- 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);
153
265
  const requiredSkills = normalizeSkills(requiredSkillsRaw, config.skillAliases);
154
266
  const preferredSkills = normalizeSkills(preferredSkillsRaw, config.skillAliases);
155
- 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]);
156
270
  return {
157
271
  raw: jobDescription,
158
272
  normalizedText,
159
273
  requiredSkills,
160
274
  preferredSkills,
161
- roleKeywords: extractRoleKeywords(jobDescription),
275
+ roleKeywords,
162
276
  keywords,
163
277
  minExperienceYears: extractMinExperience(jobDescription),
164
278
  educationRequirements: extractEducationRequirements(jobDescription)
@@ -203,6 +317,14 @@ function parseDateToken(raw) {
203
317
  return { year, month };
204
318
  }
205
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
+ }
206
328
  const yearMatch = cleaned.match(/(20\d{2}|19\d{2})/);
207
329
  if (yearMatch) {
208
330
  const year = Number.parseInt(yearMatch[1], 10);
@@ -215,9 +337,11 @@ function monthsBetween(start, end) {
215
337
  const endMonth = end.month ?? 12;
216
338
  return (end.year - start.year) * 12 + (endMonth - startMonth + 1);
217
339
  }
218
- function parseDateRange(text) {
340
+ function parseDateRange(text, referenceDate) {
219
341
  const normalized = text.trim();
220
- 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
+ );
221
345
  if (!rangeMatch) {
222
346
  return null;
223
347
  }
@@ -228,20 +352,50 @@ function parseDateRange(text) {
228
352
  if (!startToken) {
229
353
  return null;
230
354
  }
355
+ const ref = referenceDate ?? /* @__PURE__ */ new Date();
231
356
  const endTokenResolved = endToken ?? {
232
- year: (/* @__PURE__ */ new Date()).getFullYear(),
233
- month: (/* @__PURE__ */ new Date()).getMonth() + 1
357
+ year: ref.getFullYear(),
358
+ month: ref.getMonth() + 1
234
359
  };
235
360
  const durationInMonths = monthsBetween(startToken, endTokenResolved);
236
361
  return {
237
362
  raw: normalized,
238
363
  start: rangeMatch[1],
239
364
  end: isPresent ? "present" : rangeMatch[2],
240
- 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
241
370
  };
242
371
  }
243
372
  function sumExperienceYears(ranges) {
244
- 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);
245
399
  return Number((months / 12).toFixed(2));
246
400
  }
247
401
 
@@ -275,9 +429,6 @@ var ACTION_VERBS = [
275
429
  "reduced",
276
430
  "increased"
277
431
  ];
278
- function escapeRegExp(input) {
279
- return input.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
280
- }
281
432
  function detectSection(line) {
282
433
  const normalized = line.trim().toLowerCase();
283
434
  for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
@@ -325,7 +476,7 @@ function parseActionVerbs(text) {
325
476
  const words = tokenize(text);
326
477
  return ACTION_VERBS.filter((verb) => words.includes(verb));
327
478
  }
328
- function parseExperience(sectionContent) {
479
+ function parseExperience(sectionContent, referenceDate) {
329
480
  if (!sectionContent) {
330
481
  return { entries: [], rangesInMonths: [], jobTitles: [] };
331
482
  }
@@ -334,7 +485,7 @@ function parseExperience(sectionContent) {
334
485
  const rangesInMonths = [];
335
486
  const jobTitles = [];
336
487
  for (const line of lines) {
337
- const range = parseDateRange(line);
488
+ const range = parseDateRange(line, referenceDate);
338
489
  if (range) {
339
490
  const previous = entries[entries.length - 1];
340
491
  if (previous && !previous.dates) {
@@ -374,11 +525,18 @@ function parseResume(resumeText, config) {
374
525
  const { sections, detected } = extractSections(resumeText);
375
526
  const skills = parseSkills(sections.skills, config.skillAliases);
376
527
  const actionVerbs = parseActionVerbs(normalizedText);
377
- const experienceData = parseExperience(sections.experience);
528
+ const experienceData = parseExperience(sections.experience, config.referenceDate);
378
529
  const educationEntries = parseEducation(sections.education);
379
- const totalExperienceYears = sumExperienceYears(
530
+ let totalExperienceYears = sumExperienceYears(
380
531
  experienceData.entries.map((entry) => entry.dates).filter((range) => Boolean(range))
381
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
+ }
382
540
  const requiredSections = ["summary", "experience", "skills", "education"];
383
541
  const warnings = [];
384
542
  for (const section of requiredSections) {
@@ -498,8 +656,9 @@ function scoreSkills(resume, job, config) {
498
656
  0,
499
657
  100
500
658
  );
501
- const missing = [...required].filter((skill) => !resumeSkills.has(skill));
502
- return { score, missing };
659
+ const matched = [...required].filter((skill) => resumeSkills.has(skill)).sort();
660
+ const missing = [...required].filter((skill) => !resumeSkills.has(skill)).sort();
661
+ return { score, matched, missing };
503
662
  }
504
663
  function scoreExperience(resume, job, config) {
505
664
  const requiredYears = job.minExperienceYears ?? config.profile?.minExperience ?? 0;
@@ -517,11 +676,15 @@ function scoreExperience(resume, job, config) {
517
676
  return { score, missingYears: Number(missingYears.toFixed(2)) };
518
677
  }
519
678
  function scoreKeywords(resume, job, config) {
520
- const jobKeywordSet = new Set(job.keywords.map((value) => value.toLowerCase()));
679
+ const jobKeywordSet = new Set(
680
+ job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
681
+ );
521
682
  if (jobKeywordSet.size === 0) {
522
683
  return { score: 100, matchedKeywords: [], missingKeywords: [], overusedKeywords: [] };
523
684
  }
524
- const resumeTokens = tokenize(resume.normalizedText);
685
+ const resumeTokens = tokenize(resume.normalizedText).map(
686
+ (t) => normalizeSkill(t, config.skillAliases)
687
+ );
525
688
  const resumeTokenSet = new Set(resumeTokens);
526
689
  const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
527
690
  const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
@@ -535,9 +698,9 @@ function scoreKeywords(resume, job, config) {
535
698
  });
536
699
  return {
537
700
  score,
538
- matchedKeywords: unique(matchedKeywords),
539
- missingKeywords: unique(missingKeywords),
540
- overusedKeywords: unique(overusedKeywords)
701
+ matchedKeywords: unique(matchedKeywords).sort(),
702
+ missingKeywords: unique(missingKeywords).sort(),
703
+ overusedKeywords: unique(overusedKeywords).sort()
541
704
  };
542
705
  }
543
706
  function scoreEducation(resume, job) {
@@ -569,12 +732,18 @@ function calculateScore(resume, job, config) {
569
732
  return {
570
733
  score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
571
734
  breakdown,
735
+ matchedSkills: skillsResult.matched,
736
+ missingSkills: skillsResult.missing,
572
737
  matchedKeywords: keywordResult.matchedKeywords,
573
738
  missingKeywords: keywordResult.missingKeywords,
574
739
  overusedKeywords: keywordResult.overusedKeywords,
575
740
  suggestions: [],
576
741
  warnings: [],
577
- missingSkills: skillsResult.missing,
742
+ // detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
743
+ experienceGap: experienceResult.missingYears,
744
+ detectedSections: [],
745
+ parsedExperienceYears: 0,
746
+ experienceEntries: [],
578
747
  missingExperienceYears: experienceResult.missingYears,
579
748
  educationScore
580
749
  };
@@ -582,7 +751,9 @@ function calculateScore(resume, job, config) {
582
751
 
583
752
  // src/profiles/index.ts
584
753
  var defaultSkillAliases = {
585
- javascript: ["js", "node", "node.js", "nodejs"],
754
+ // ponytail: "node" split from javascript — Node.js runtime !== JS language
755
+ javascript: ["js"],
756
+ node: ["node.js", "nodejs"],
586
757
  typescript: ["ts"],
587
758
  react: ["reactjs", "react.js"],
588
759
  "c++": ["cpp"],
@@ -596,7 +767,31 @@ var defaultSkillAliases = {
596
767
  docker: ["containers"],
597
768
  kubernetes: ["k8s"],
598
769
  html: ["html5"],
599
- 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: []
600
795
  };
601
796
  var softwareEngineerProfile = {
602
797
  name: "software-engineer",
@@ -677,7 +872,8 @@ function resolveConfig(config = {}) {
677
872
  ...DEFAULT_SECTION_PENALTIES,
678
873
  ...config.sectionPenalties ?? {}
679
874
  },
680
- allowPartialMatches: config.allowPartialMatches ?? true
875
+ allowPartialMatches: config.allowPartialMatches ?? true,
876
+ referenceDate: config.referenceDate ? new Date(config.referenceDate) : void 0
681
877
  };
682
878
  return resolved;
683
879
  }
@@ -1374,9 +1570,15 @@ function analyzeResume(input) {
1374
1570
  return {
1375
1571
  score: finalScore,
1376
1572
  breakdown: scoring.breakdown,
1573
+ matchedSkills: scoring.matchedSkills,
1574
+ missingSkills: scoring.missingSkills,
1377
1575
  matchedKeywords: scoring.matchedKeywords,
1378
1576
  missingKeywords: scoring.missingKeywords,
1379
1577
  overusedKeywords: scoring.overusedKeywords,
1578
+ experienceGap: scoring.experienceGap,
1579
+ detectedSections: parsedResume.detectedSections,
1580
+ parsedExperienceYears: parsedResume.totalExperienceYears,
1581
+ experienceEntries: parsedResume.experience,
1380
1582
  suggestions,
1381
1583
  warnings: [...suggestionResult.warnings, ...llmWarnings]
1382
1584
  };
@@ -1433,9 +1635,15 @@ async function analyzeResumeAsync(input) {
1433
1635
  return {
1434
1636
  score: finalScore,
1435
1637
  breakdown: scoring.breakdown,
1638
+ matchedSkills: scoring.matchedSkills,
1639
+ missingSkills: scoring.missingSkills,
1436
1640
  matchedKeywords: scoring.matchedKeywords,
1437
1641
  missingKeywords: scoring.missingKeywords,
1438
1642
  overusedKeywords: scoring.overusedKeywords,
1643
+ experienceGap: scoring.experienceGap,
1644
+ detectedSections: parsedResume.detectedSections,
1645
+ parsedExperienceYears: parsedResume.totalExperienceYears,
1646
+ experienceEntries: parsedResume.experience,
1439
1647
  suggestions,
1440
1648
  warnings: [...suggestionResult.warnings, ...llmWarnings]
1441
1649
  };