@pranavraut033/ats-checker 1.3.0 → 1.3.2

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
@@ -54,15 +54,38 @@ var LEVEL_RANK = {
54
54
  fluent: 5,
55
55
  native: 6,
56
56
  "native speaker": 6,
57
- bilingual: 6
57
+ bilingual: 6,
58
+ // German
59
+ grundkenntnisse: 1,
60
+ gering: 2,
61
+ gut: 3,
62
+ fortgeschritten: 4,
63
+ flie\u00DFend: 5,
64
+ muttersprache: 6,
65
+ muttersprachler: 6,
66
+ // French
67
+ "d\xE9butant": 1,
68
+ "\xE9l\xE9mentaire": 1,
69
+ "limit\xE9": 2,
70
+ "interm\xE9diaire": 3,
71
+ "avanc\xE9": 4,
72
+ courant: 5,
73
+ natif: 6,
74
+ "langue maternelle": 6,
75
+ bilingue: 6
58
76
  };
59
77
  var LANGUAGE_GROUP = KNOWN_LANGUAGES.join("|");
60
78
  var LEVEL_GROUP = Object.keys(LEVEL_RANK).sort((a, b) => b.length - a.length).map((l) => l.replace(/\s+/g, "\\s+")).join("|");
79
+ var BOUNDARY_START = "(?:^|(?<=[^a-z\xE0-\xFF]))";
80
+ var BOUNDARY_END = "(?:$|(?=[^a-z\xE0-\xFF]))";
61
81
  var LANGUAGE_LEVEL_RE = new RegExp(
62
- `\\b(${LANGUAGE_GROUP})\\b(?:\\s*[\\(:\\-]?\\s*(${LEVEL_GROUP}|[abc][12]))?`,
82
+ `\\b(${LANGUAGE_GROUP})\\b(?:\\s*[\\(:\\-]?\\s*(${BOUNDARY_START}(?:${LEVEL_GROUP})${BOUNDARY_END}|[abc][12]))?`,
83
+ "gi"
84
+ );
85
+ var LEVEL_BEFORE_LANGUAGE_RE = new RegExp(
86
+ `${BOUNDARY_START}(${LEVEL_GROUP})${BOUNDARY_END}\\s+(?:in\\s+)?(${LANGUAGE_GROUP})\\b`,
63
87
  "gi"
64
88
  );
65
- var LEVEL_BEFORE_LANGUAGE_RE = new RegExp(`\\b(${LEVEL_GROUP})\\s+(?:in\\s+)?(${LANGUAGE_GROUP})\\b`, "gi");
66
89
  function canonicalLanguage(name) {
67
90
  const lower = name.toLowerCase();
68
91
  return LANGUAGE_ALIASES[lower] ?? lower;
@@ -112,9 +135,9 @@ function diffLanguages(resumeLanguages, requiredLanguages) {
112
135
 
113
136
  // src/core/parser/jd.parser.ts
114
137
  var DEGREE_VARIANTS = [
115
- [/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc\.?)\b/i, "bachelor"],
116
- [/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc\.?)\b/i, "master"],
117
- [/\b(?:phd|ph\.d\.?|doctorate)\b/i, "phd"],
138
+ [/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc\.?|licence)\b/i, "bachelor"],
139
+ [/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc\.?|diplom)\b/i, "master"],
140
+ [/\b(?:phd|ph\.d\.?|doctorate|doktor|doctorat)\b/i, "phd"],
118
141
  [/\bmba\b/i, "mba"],
119
142
  [/\bassociate(?:'s)?\b/i, "associate"]
120
143
  ];
@@ -137,16 +160,15 @@ function extractPreferredSkills(lines) {
137
160
  return preferred;
138
161
  }
139
162
  function extractRoleKeywords(text) {
140
- const roleMatch = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect)/i);
141
- const titleTokens = roleMatch ? roleMatch[0].split(/\s+/) : [];
142
- return unique(tokenize(titleTokens.join(" ") || text.split(/\n/)[0] || ""));
163
+ const roleMatches = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect|director|consultant|lead|vp)/gi) ?? [];
164
+ const fallback = roleMatches.length === 0 ? [text.split(/\n/)[0] ?? ""] : [];
165
+ return unique(tokenize([...roleMatches, ...fallback].join(" ")));
143
166
  }
144
167
  function extractMinExperience(text) {
145
- const match = text.match(/(\d{1,2})\+?\s+(?:years|yrs)/i);
146
- if (match) {
147
- return Number.parseInt(match[1], 10);
148
- }
149
- return void 0;
168
+ const match = text.match(/(\d{1,2})\+?\s*(?:years?|yrs\.?|jahre?|ans?|années?)/i);
169
+ if (!match) return void 0;
170
+ const parsed = Number.parseInt(match[1], 10);
171
+ return parsed <= 60 ? parsed : void 0;
150
172
  }
151
173
  var SURFACE_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/gi;
152
174
  function collectKeywordSurfaceForms(rawText, aliases) {
@@ -160,7 +182,16 @@ function collectKeywordSurfaceForms(rawText, aliases) {
160
182
  }
161
183
  return surfaceForms;
162
184
  }
163
- function extractEducationRequirements(text) {
185
+ var LANG_SECTION_RE = /^\s*(?:languages?|sprache|langue)s?\s*[:\-–—]?\s*/i;
186
+ var LANG_REQUIREMENT_HINT_RE = /\b(fluent|required|must|need|speak|proficient|native|conversational|intermediate|advanced|professional|[abc][12])\b/i;
187
+ function isLanguageRequired(lang, jobDescription) {
188
+ return splitLines(jobDescription).some((line) => {
189
+ const lower = line.toLowerCase();
190
+ if (!lower.includes(lang.name)) return false;
191
+ return LANG_SECTION_RE.test(line) || LANG_REQUIREMENT_HINT_RE.test(line);
192
+ });
193
+ }
194
+ function extractDegreeLevels(text) {
164
195
  const found = /* @__PURE__ */ new Set();
165
196
  for (const [pattern, canonical] of DEGREE_VARIANTS) {
166
197
  if (pattern.test(text)) found.add(canonical);
@@ -198,11 +229,13 @@ function parseJobDescription(jobDescription, config) {
198
229
  roleKeywords,
199
230
  keywords,
200
231
  minExperienceYears: extractMinExperience(jobDescription),
201
- educationRequirements: extractEducationRequirements(jobDescription),
232
+ educationRequirements: extractDegreeLevels(jobDescription),
202
233
  keywordSurfaceForms: collectKeywordSurfaceForms(jobDescription, config.skillAliases),
203
- // ponytail: any language mention in the JD is treated as a requirement — good enough until
204
- // JDs that merely *reference* a language (not require it) show up as false positives.
205
- requiredLanguages: parseLanguageMentions(jobDescription)
234
+ // A language only counts as required if its mention carries a requirement/level cue
235
+ // or sits in a "Languages:" line plain references ("our Berlin office") don't count.
236
+ requiredLanguages: parseLanguageMentions(jobDescription).filter(
237
+ (lang) => isLanguageRequired(lang, jobDescription)
238
+ )
206
239
  };
207
240
  }
208
241
 
@@ -231,11 +264,37 @@ var MONTHS = {
231
264
  nov: 11,
232
265
  november: 11,
233
266
  dec: 12,
234
- december: 12
267
+ december: 12,
268
+ // German
269
+ januar: 1,
270
+ j\u00E4nner: 1,
271
+ februar: 2,
272
+ m\u00E4rz: 3,
273
+ maerz: 3,
274
+ mai: 5,
275
+ juni: 6,
276
+ juli: 7,
277
+ oktober: 10,
278
+ dezember: 12,
279
+ // French
280
+ janvier: 1,
281
+ f\u00E9vrier: 2,
282
+ fevrier: 2,
283
+ mars: 3,
284
+ avril: 4,
285
+ juin: 6,
286
+ juillet: 7,
287
+ ao\u00FBt: 8,
288
+ aout: 8,
289
+ septembre: 9,
290
+ octobre: 10,
291
+ novembre: 11,
292
+ d\u00E9cembre: 12,
293
+ decembre: 12
235
294
  };
236
295
  function parseDateToken(raw) {
237
296
  const cleaned = raw.trim().toLowerCase();
238
- const monthMatch = cleaned.match(/([a-z]{3,9})\s*(\d{4})/i);
297
+ const monthMatch = cleaned.match(/([a-zà-ÿ]{3,9})\s*(\d{4})/i);
239
298
  if (monthMatch) {
240
299
  const monthName = monthMatch[1].toLowerCase();
241
300
  const year = Number.parseInt(monthMatch[2], 10);
@@ -267,14 +326,14 @@ function monthsBetween(start, end) {
267
326
  function parseDateRange(text, referenceDate) {
268
327
  const normalized = text.trim();
269
328
  const rangeMatch = normalized.match(
270
- /(\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
329
+ /(\d{1,2}\/\d{4}|[A-Za-zà-ÿ]{3,9}\s+\d{4}|\d{4})\s*(?:-|to|through|until|bis|jusqu'à|à|–|—)\s*(Present|Current|Now|Aktuell|Heute|Actuellement|Présent|\d{1,2}\/\d{4}|[A-Za-zà-ÿ]{3,9}\s+\d{4}|\d{4})/i
271
330
  );
272
331
  if (!rangeMatch) {
273
332
  return null;
274
333
  }
275
334
  const startToken = parseDateToken(rangeMatch[1]);
276
335
  const endRaw = rangeMatch[2];
277
- const isPresent = /present|current|now/i.test(endRaw);
336
+ const isPresent = /present|current|now|aktuell|heute|actuellement|présent|actuel/i.test(endRaw);
278
337
  const endToken = isPresent ? void 0 : parseDateToken(endRaw);
279
338
  if (!startToken) {
280
339
  return null;
@@ -328,12 +387,21 @@ function sumExperienceYears(ranges) {
328
387
 
329
388
  // src/core/parser/resume.parser.ts
330
389
  var SECTION_ALIASES = {
331
- summary: ["summary", "profile", "about"],
332
- experience: ["experience", "work experience", "professional experience", "employment"],
333
- skills: ["skills", "technical skills", "technologies"],
334
- education: ["education", "academics", "academic background"],
335
- projects: ["projects", "portfolio"],
336
- certifications: ["certifications", "licenses"]
390
+ summary: ["summary", "profile", "about", "zusammenfassung", "profil", "r\xE9sum\xE9", "\xE0 propos"],
391
+ experience: [
392
+ "experience",
393
+ "work experience",
394
+ "professional experience",
395
+ "employment",
396
+ "erfahrung",
397
+ "berufserfahrung",
398
+ "exp\xE9rience",
399
+ "exp\xE9rience professionnelle"
400
+ ],
401
+ skills: ["skills", "technical skills", "technologies", "f\xE4higkeiten", "kenntnisse", "comp\xE9tences"],
402
+ education: ["education", "academics", "academic background", "ausbildung", "formation", "\xE9tudes"],
403
+ projects: ["projects", "portfolio", "projekte", "projets"],
404
+ certifications: ["certifications", "licenses", "zertifizierungen", "certifications professionnelles"]
337
405
  };
338
406
  var STRONG_VERBS = [
339
407
  "led",
@@ -411,7 +479,9 @@ function extractSections(text) {
411
479
  }
412
480
  function parseSkills(sectionContent, aliases) {
413
481
  if (!sectionContent) return [];
414
- const raw = sectionContent.split(/[,;\n]/).map((skill) => skill.trim()).filter(Boolean);
482
+ const hasBullets = /[•·‣▪○●◦]/.test(sectionContent);
483
+ const normalized = hasBullets ? sectionContent.replace(/\n/g, " ") : sectionContent;
484
+ const raw = normalized.split(/[,;\n]|[•·‣▪○●◦]/).map((skill) => skill.trim().replace(/^[-•·‣▪○●◦\s]+|[-•·‣▪○●◦\s]+$/g, "").trim()).filter(Boolean);
415
485
  return normalizeSkills(raw, aliases);
416
486
  }
417
487
  function parseActionVerbs(text) {
@@ -444,7 +514,7 @@ function parseExperience(sectionContent, referenceDate) {
444
514
  }
445
515
  continue;
446
516
  }
447
- const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
517
+ const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|VP|Director|Consultant|Architect|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
448
518
  if (titleMatch) {
449
519
  const title = titleMatch[0].trim();
450
520
  jobTitles.push(title.toLowerCase());
@@ -482,7 +552,8 @@ function parseResume(resumeText, config) {
482
552
  const textToScan = sections.summary ?? normalizedText;
483
553
  const yearsMatch = textToScan.match(/(\d{1,2})\+?\s*years?/i);
484
554
  if (yearsMatch) {
485
- totalExperienceYears = Number.parseInt(yearsMatch[1], 10);
555
+ const parsed = Number.parseInt(yearsMatch[1], 10);
556
+ totalExperienceYears = parsed <= 60 ? parsed : 0;
486
557
  }
487
558
  }
488
559
  const requiredSections = ["summary", "experience", "skills", "education"];
@@ -718,10 +789,9 @@ function scoreEducation(resume, job) {
718
789
  if (job.educationRequirements.length === 0) {
719
790
  return 100;
720
791
  }
721
- const resumeEducationText = resume.educationEntries.join(" ");
722
- const normalizedEducation = resumeEducationText.toLowerCase();
792
+ const resumeDegreeLevels = extractDegreeLevels(resume.educationEntries.join(" "));
723
793
  const matched = job.educationRequirements.filter(
724
- (requirement) => normalizedEducation.includes(requirement.toLowerCase())
794
+ (requirement) => resumeDegreeLevels.includes(requirement)
725
795
  );
726
796
  if (matched.length === 0) {
727
797
  return 0;