@pranavraut033/ats-checker 1.2.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.
@@ -185,27 +185,187 @@ function containsTableLikeStructure(text) {
185
185
  }
186
186
 
187
187
  // src/utils/skills.ts
188
+ var aliasIndexCache = /* @__PURE__ */ new WeakMap();
189
+ function getAliasIndex(aliases) {
190
+ let index = aliasIndexCache.get(aliases);
191
+ if (!index) {
192
+ index = /* @__PURE__ */ new Map();
193
+ for (const [canonical, aliasList] of Object.entries(aliases)) {
194
+ const lower = canonical.toLowerCase();
195
+ index.set(lower, lower);
196
+ for (const alias of aliasList) {
197
+ index.set(alias.toLowerCase(), lower);
198
+ }
199
+ }
200
+ aliasIndexCache.set(aliases, index);
201
+ }
202
+ return index;
203
+ }
188
204
  function normalizeSkill(skill, aliases) {
189
205
  const normalized = skill.trim().toLowerCase();
190
- for (const [canonical, aliasList] of Object.entries(aliases)) {
191
- if (canonical.toLowerCase() === normalized) {
192
- return canonical.toLowerCase();
206
+ return getAliasIndex(aliases).get(normalized) ?? normalized;
207
+ }
208
+ function normalizeSkills(skills, aliases) {
209
+ return unique(skills.map((skill) => normalizeSkill(skill, aliases)));
210
+ }
211
+ function deriveSkillAliases(registry) {
212
+ const aliases = {};
213
+ for (const entry of registry) {
214
+ aliases[entry.canonical] = entry.aliases;
215
+ }
216
+ return aliases;
217
+ }
218
+ function buildCategoryIndex(registry) {
219
+ const index = /* @__PURE__ */ new Map();
220
+ for (const entry of registry) {
221
+ index.set(entry.canonical.toLowerCase(), entry.category);
222
+ }
223
+ return index;
224
+ }
225
+ function mergeKeywordRegistries(base, overrides) {
226
+ const byCanonical = /* @__PURE__ */ new Map();
227
+ for (const entry of base) byCanonical.set(entry.canonical.toLowerCase(), entry);
228
+ for (const entry of overrides) byCanonical.set(entry.canonical.toLowerCase(), entry);
229
+ return [...byCanonical.values()];
230
+ }
231
+
232
+ // src/utils/languages.ts
233
+ var KNOWN_LANGUAGES = [
234
+ "english",
235
+ "spanish",
236
+ "french",
237
+ "german",
238
+ "italian",
239
+ "portuguese",
240
+ "dutch",
241
+ "russian",
242
+ "mandarin",
243
+ "chinese",
244
+ "cantonese",
245
+ "japanese",
246
+ "korean",
247
+ "arabic",
248
+ "hindi",
249
+ "polish",
250
+ "turkish",
251
+ "vietnamese",
252
+ "swedish",
253
+ "norwegian",
254
+ "danish",
255
+ "finnish",
256
+ "greek",
257
+ "hebrew",
258
+ "thai",
259
+ "indonesian",
260
+ "ukrainian",
261
+ "czech"
262
+ ];
263
+ var LANGUAGE_ALIASES = {
264
+ mandarin: "chinese",
265
+ cantonese: "chinese"
266
+ };
267
+ var LEVEL_RANK = {
268
+ a1: 1,
269
+ a2: 2,
270
+ b1: 3,
271
+ b2: 4,
272
+ c1: 5,
273
+ c2: 6,
274
+ basic: 1,
275
+ elementary: 1,
276
+ limited: 2,
277
+ conversational: 3,
278
+ intermediate: 3,
279
+ professional: 4,
280
+ "upper intermediate": 4,
281
+ advanced: 4,
282
+ fluent: 5,
283
+ native: 6,
284
+ "native speaker": 6,
285
+ bilingual: 6,
286
+ // German
287
+ grundkenntnisse: 1,
288
+ gering: 2,
289
+ gut: 3,
290
+ fortgeschritten: 4,
291
+ flie\u00DFend: 5,
292
+ muttersprache: 6,
293
+ muttersprachler: 6,
294
+ // French
295
+ "d\xE9butant": 1,
296
+ "\xE9l\xE9mentaire": 1,
297
+ "limit\xE9": 2,
298
+ "interm\xE9diaire": 3,
299
+ "avanc\xE9": 4,
300
+ courant: 5,
301
+ natif: 6,
302
+ "langue maternelle": 6,
303
+ bilingue: 6
304
+ };
305
+ var LANGUAGE_GROUP = KNOWN_LANGUAGES.join("|");
306
+ var LEVEL_GROUP = Object.keys(LEVEL_RANK).sort((a, b) => b.length - a.length).map((l) => l.replace(/\s+/g, "\\s+")).join("|");
307
+ var BOUNDARY_START = "(?:^|(?<=[^a-z\xE0-\xFF]))";
308
+ var BOUNDARY_END = "(?:$|(?=[^a-z\xE0-\xFF]))";
309
+ var LANGUAGE_LEVEL_RE = new RegExp(
310
+ `\\b(${LANGUAGE_GROUP})\\b(?:\\s*[\\(:\\-]?\\s*(${BOUNDARY_START}(?:${LEVEL_GROUP})${BOUNDARY_END}|[abc][12]))?`,
311
+ "gi"
312
+ );
313
+ var LEVEL_BEFORE_LANGUAGE_RE = new RegExp(
314
+ `${BOUNDARY_START}(${LEVEL_GROUP})${BOUNDARY_END}\\s+(?:in\\s+)?(${LANGUAGE_GROUP})\\b`,
315
+ "gi"
316
+ );
317
+ function canonicalLanguage(name) {
318
+ const lower = name.toLowerCase();
319
+ return LANGUAGE_ALIASES[lower] ?? lower;
320
+ }
321
+ function toParsedLanguage(name, level) {
322
+ const normalizedLevel = level?.toLowerCase().replace(/\s+/g, " ");
323
+ return {
324
+ name: canonicalLanguage(name),
325
+ level: normalizedLevel,
326
+ levelRank: normalizedLevel ? LEVEL_RANK[normalizedLevel] : void 0
327
+ };
328
+ }
329
+ function parseLanguageMentions(text) {
330
+ const found = /* @__PURE__ */ new Map();
331
+ for (const match of text.matchAll(LANGUAGE_LEVEL_RE)) {
332
+ const parsed = toParsedLanguage(match[1], match[2]);
333
+ const existing = found.get(parsed.name);
334
+ if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
335
+ found.set(parsed.name, parsed);
193
336
  }
194
- if (aliasList.some((alias) => alias.toLowerCase() === normalized)) {
195
- return canonical.toLowerCase();
337
+ }
338
+ for (const match of text.matchAll(LEVEL_BEFORE_LANGUAGE_RE)) {
339
+ const parsed = toParsedLanguage(match[2], match[1]);
340
+ const existing = found.get(parsed.name);
341
+ if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
342
+ found.set(parsed.name, parsed);
196
343
  }
197
344
  }
198
- return normalized;
345
+ return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
199
346
  }
200
- function normalizeSkills(skills, aliases) {
201
- return unique(skills.map((skill) => normalizeSkill(skill, aliases)));
347
+ function diffLanguages(resumeLanguages, requiredLanguages) {
348
+ const byName = new Map(resumeLanguages.map((l) => [l.name, l]));
349
+ const matched = [];
350
+ const missing = [];
351
+ for (const required of requiredLanguages) {
352
+ const have = byName.get(required.name);
353
+ const requiredRank = required.levelRank ?? 0;
354
+ const haveRank = have?.levelRank ?? 0;
355
+ if (have && haveRank >= requiredRank) {
356
+ matched.push(required);
357
+ } else {
358
+ missing.push(required);
359
+ }
360
+ }
361
+ return { matched, missing };
202
362
  }
203
363
 
204
364
  // src/core/parser/jd.parser.ts
205
365
  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"],
366
+ [/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc\.?|licence)\b/i, "bachelor"],
367
+ [/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc\.?|diplom)\b/i, "master"],
368
+ [/\b(?:phd|ph\.d\.?|doctorate|doktor|doctorat)\b/i, "phd"],
209
369
  [/\bmba\b/i, "mba"],
210
370
  [/\bassociate(?:'s)?\b/i, "associate"]
211
371
  ];
@@ -228,18 +388,38 @@ function extractPreferredSkills(lines) {
228
388
  return preferred;
229
389
  }
230
390
  function extractRoleKeywords(text) {
231
- const roleMatch = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect)/i);
232
- const titleTokens = roleMatch ? roleMatch[0].split(/\s+/) : [];
233
- return unique(tokenize(titleTokens.join(" ") || text.split(/\n/)[0] || ""));
391
+ const roleMatches = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect|director|consultant|lead|vp)/gi) ?? [];
392
+ const fallback = roleMatches.length === 0 ? [text.split(/\n/)[0] ?? ""] : [];
393
+ return unique(tokenize([...roleMatches, ...fallback].join(" ")));
234
394
  }
235
395
  function extractMinExperience(text) {
236
- const match = text.match(/(\d{1,2})\+?\s+(?:years|yrs)/i);
237
- if (match) {
238
- return Number.parseInt(match[1], 10);
396
+ const match = text.match(/(\d{1,2})\+?\s*(?:years?|yrs\.?|jahre?|ans?|années?)/i);
397
+ if (!match) return void 0;
398
+ const parsed = Number.parseInt(match[1], 10);
399
+ return parsed <= 60 ? parsed : void 0;
400
+ }
401
+ var SURFACE_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/gi;
402
+ function collectKeywordSurfaceForms(rawText, aliases) {
403
+ const matches = rawText.match(SURFACE_TOKEN_RE) ?? [];
404
+ const surfaceForms = {};
405
+ for (const match of matches) {
406
+ const canonical = normalizeSkill(match, aliases);
407
+ if (!(canonical in surfaceForms)) {
408
+ surfaceForms[canonical] = match;
409
+ }
239
410
  }
240
- return void 0;
411
+ return surfaceForms;
412
+ }
413
+ var LANG_SECTION_RE = /^\s*(?:languages?|sprache|langue)s?\s*[:\-–—]?\s*/i;
414
+ var LANG_REQUIREMENT_HINT_RE = /\b(fluent|required|must|need|speak|proficient|native|conversational|intermediate|advanced|professional|[abc][12])\b/i;
415
+ function isLanguageRequired(lang, jobDescription) {
416
+ return splitLines(jobDescription).some((line) => {
417
+ const lower = line.toLowerCase();
418
+ if (!lower.includes(lang.name)) return false;
419
+ return LANG_SECTION_RE.test(line) || LANG_REQUIREMENT_HINT_RE.test(line);
420
+ });
241
421
  }
242
- function extractEducationRequirements(text) {
422
+ function extractDegreeLevels(text) {
243
423
  const found = /* @__PURE__ */ new Set();
244
424
  for (const [pattern, canonical] of DEGREE_VARIANTS) {
245
425
  if (pattern.test(text)) found.add(canonical);
@@ -277,7 +457,13 @@ function parseJobDescription(jobDescription, config) {
277
457
  roleKeywords,
278
458
  keywords,
279
459
  minExperienceYears: extractMinExperience(jobDescription),
280
- educationRequirements: extractEducationRequirements(jobDescription)
460
+ educationRequirements: extractDegreeLevels(jobDescription),
461
+ keywordSurfaceForms: collectKeywordSurfaceForms(jobDescription, config.skillAliases),
462
+ // A language only counts as required if its mention carries a requirement/level cue
463
+ // or sits in a "Languages:" line — plain references ("our Berlin office") don't count.
464
+ requiredLanguages: parseLanguageMentions(jobDescription).filter(
465
+ (lang) => isLanguageRequired(lang, jobDescription)
466
+ )
281
467
  };
282
468
  }
283
469
 
@@ -306,11 +492,37 @@ var MONTHS = {
306
492
  nov: 11,
307
493
  november: 11,
308
494
  dec: 12,
309
- december: 12
495
+ december: 12,
496
+ // German
497
+ januar: 1,
498
+ j\u00E4nner: 1,
499
+ februar: 2,
500
+ m\u00E4rz: 3,
501
+ maerz: 3,
502
+ mai: 5,
503
+ juni: 6,
504
+ juli: 7,
505
+ oktober: 10,
506
+ dezember: 12,
507
+ // French
508
+ janvier: 1,
509
+ f\u00E9vrier: 2,
510
+ fevrier: 2,
511
+ mars: 3,
512
+ avril: 4,
513
+ juin: 6,
514
+ juillet: 7,
515
+ ao\u00FBt: 8,
516
+ aout: 8,
517
+ septembre: 9,
518
+ octobre: 10,
519
+ novembre: 11,
520
+ d\u00E9cembre: 12,
521
+ decembre: 12
310
522
  };
311
523
  function parseDateToken(raw) {
312
524
  const cleaned = raw.trim().toLowerCase();
313
- const monthMatch = cleaned.match(/([a-z]{3,9})\s*(\d{4})/i);
525
+ const monthMatch = cleaned.match(/([a-zà-ÿ]{3,9})\s*(\d{4})/i);
314
526
  if (monthMatch) {
315
527
  const monthName = monthMatch[1].toLowerCase();
316
528
  const year = Number.parseInt(monthMatch[2], 10);
@@ -342,14 +554,14 @@ function monthsBetween(start, end) {
342
554
  function parseDateRange(text, referenceDate) {
343
555
  const normalized = text.trim();
344
556
  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
557
+ /(\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
346
558
  );
347
559
  if (!rangeMatch) {
348
560
  return null;
349
561
  }
350
562
  const startToken = parseDateToken(rangeMatch[1]);
351
563
  const endRaw = rangeMatch[2];
352
- const isPresent = /present|current|now/i.test(endRaw);
564
+ const isPresent = /present|current|now|aktuell|heute|actuellement|présent|actuel/i.test(endRaw);
353
565
  const endToken = isPresent ? void 0 : parseDateToken(endRaw);
354
566
  if (!startToken) {
355
567
  return null;
@@ -403,14 +615,23 @@ function sumExperienceYears(ranges) {
403
615
 
404
616
  // src/core/parser/resume.parser.ts
405
617
  var SECTION_ALIASES = {
406
- summary: ["summary", "profile", "about"],
407
- experience: ["experience", "work experience", "professional experience", "employment"],
408
- skills: ["skills", "technical skills", "technologies"],
409
- education: ["education", "academics", "academic background"],
410
- projects: ["projects", "portfolio"],
411
- certifications: ["certifications", "licenses"]
618
+ summary: ["summary", "profile", "about", "zusammenfassung", "profil", "r\xE9sum\xE9", "\xE0 propos"],
619
+ experience: [
620
+ "experience",
621
+ "work experience",
622
+ "professional experience",
623
+ "employment",
624
+ "erfahrung",
625
+ "berufserfahrung",
626
+ "exp\xE9rience",
627
+ "exp\xE9rience professionnelle"
628
+ ],
629
+ skills: ["skills", "technical skills", "technologies", "f\xE4higkeiten", "kenntnisse", "comp\xE9tences"],
630
+ education: ["education", "academics", "academic background", "ausbildung", "formation", "\xE9tudes"],
631
+ projects: ["projects", "portfolio", "projekte", "projets"],
632
+ certifications: ["certifications", "licenses", "zertifizierungen", "certifications professionnelles"]
412
633
  };
413
- var ACTION_VERBS = [
634
+ var STRONG_VERBS = [
414
635
  "led",
415
636
  "managed",
416
637
  "built",
@@ -431,6 +652,21 @@ var ACTION_VERBS = [
431
652
  "reduced",
432
653
  "increased"
433
654
  ];
655
+ var WEAK_VERBS = ["worked", "helped", "performed", "responsible", "assisted", "participated", "involved"];
656
+ var METRIC_RE = /\d|%|\$|\bk\+|\bm\+/i;
657
+ function classifyAchievement(line) {
658
+ const lower = line.toLowerCase();
659
+ const hasMetric = METRIC_RE.test(line);
660
+ const hasStrongVerb = STRONG_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
661
+ const hasWeakVerb = WEAK_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
662
+ if (hasStrongVerb && hasMetric) {
663
+ return { text: line, strength: "strong", reason: "strong verb + quantified impact" };
664
+ }
665
+ if (hasWeakVerb) {
666
+ return { text: line, strength: "weak", reason: "weak verb" };
667
+ }
668
+ return { text: line, strength: "weak", reason: "no quantified impact" };
669
+ }
434
670
  function detectSection(line) {
435
671
  const normalized = line.trim().toLowerCase();
436
672
  for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
@@ -471,21 +707,27 @@ function extractSections(text) {
471
707
  }
472
708
  function parseSkills(sectionContent, aliases) {
473
709
  if (!sectionContent) return [];
474
- const raw = sectionContent.split(/[,;\n]/).map((skill) => skill.trim()).filter(Boolean);
710
+ const hasBullets = /[•·‣▪○●◦]/.test(sectionContent);
711
+ const normalized = hasBullets ? sectionContent.replace(/\n/g, " ") : sectionContent;
712
+ const raw = normalized.split(/[,;\n]|[•·‣▪○●◦]/).map((skill) => skill.trim().replace(/^[-•·‣▪○●◦\s]+|[-•·‣▪○●◦\s]+$/g, "").trim()).filter(Boolean);
475
713
  return normalizeSkills(raw, aliases);
476
714
  }
477
715
  function parseActionVerbs(text) {
478
716
  const words = tokenize(text);
479
- return ACTION_VERBS.filter((verb) => words.includes(verb));
717
+ return {
718
+ strong: STRONG_VERBS.filter((verb) => words.includes(verb)),
719
+ weak: WEAK_VERBS.filter((verb) => words.includes(verb))
720
+ };
480
721
  }
481
722
  function parseExperience(sectionContent, referenceDate) {
482
723
  if (!sectionContent) {
483
- return { entries: [], rangesInMonths: [], jobTitles: [] };
724
+ return { entries: [], rangesInMonths: [], jobTitles: [], achievements: [] };
484
725
  }
485
726
  const lines = splitLines(sectionContent);
486
727
  const entries = [];
487
728
  const rangesInMonths = [];
488
729
  const jobTitles = [];
730
+ const achievements = [];
489
731
  for (const line of lines) {
490
732
  const range = parseDateRange(line, referenceDate);
491
733
  if (range) {
@@ -500,20 +742,22 @@ function parseExperience(sectionContent, referenceDate) {
500
742
  }
501
743
  continue;
502
744
  }
503
- const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
745
+ const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|VP|Director|Consultant|Architect|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
504
746
  if (titleMatch) {
505
747
  const title = titleMatch[0].trim();
506
748
  jobTitles.push(title.toLowerCase());
507
749
  const entry = { title, description: line };
508
750
  entries.push(entry);
751
+ achievements.push(classifyAchievement(line));
509
752
  continue;
510
753
  }
511
754
  if (entries.length > 0) {
512
755
  const current = entries[entries.length - 1];
513
756
  current.description = [current.description, line].filter(Boolean).join(" ").trim();
514
757
  }
758
+ achievements.push(classifyAchievement(line));
515
759
  }
516
- return { entries, rangesInMonths, jobTitles: unique(jobTitles) };
760
+ return { entries, rangesInMonths, jobTitles: unique(jobTitles), achievements };
517
761
  }
518
762
  function parseEducation(sectionContent) {
519
763
  if (!sectionContent) return [];
@@ -536,7 +780,8 @@ function parseResume(resumeText, config) {
536
780
  const textToScan = sections.summary ?? normalizedText;
537
781
  const yearsMatch = textToScan.match(/(\d{1,2})\+?\s*years?/i);
538
782
  if (yearsMatch) {
539
- totalExperienceYears = Number.parseInt(yearsMatch[1], 10);
783
+ const parsed = Number.parseInt(yearsMatch[1], 10);
784
+ totalExperienceYears = parsed <= 60 ? parsed : 0;
540
785
  }
541
786
  }
542
787
  const requiredSections = ["summary", "experience", "skills", "education"];
@@ -563,11 +808,14 @@ function parseResume(resumeText, config) {
563
808
  sectionContent: sections,
564
809
  skills,
565
810
  jobTitles: experienceData.jobTitles,
566
- actionVerbs,
811
+ actionVerbs: actionVerbs.strong,
812
+ weakVerbs: actionVerbs.weak,
813
+ achievements: experienceData.achievements,
567
814
  educationEntries,
568
815
  experience: experienceData.entries,
569
816
  totalExperienceYears,
570
817
  keywords: collectKeywords(normalizedText),
818
+ languages: parseLanguageMentions(resumeText),
571
819
  warnings
572
820
  };
573
821
  }
@@ -649,6 +897,14 @@ var REQUIRED_SKILL_WEIGHT = 0.7;
649
897
  var OPTIONAL_SKILL_WEIGHT = 0.3;
650
898
  var EXPERIENCE_YEARS_WEIGHT = 0.75;
651
899
  var EXPERIENCE_ROLE_WEIGHT = 0.25;
900
+ var ALL_CATEGORIES = ["technical", "tool", "concept", "soft", "marketing", "domain"];
901
+ function emptyCategoryBuckets() {
902
+ const buckets = {};
903
+ for (const category of ALL_CATEGORIES) {
904
+ buckets[category] = { matched: [], missing: [] };
905
+ }
906
+ return buckets;
907
+ }
652
908
  function scoreSkills(resume, job, config) {
653
909
  const profileRequired = config.profile?.mandatorySkills ?? [];
654
910
  const profileOptional = config.profile?.optionalSkills ?? [];
@@ -687,42 +943,83 @@ function scoreExperience(resume, job, config) {
687
943
  const missingYears = Math.max(requiredYears - resume.totalExperienceYears, 0);
688
944
  return { score, missingYears: Number(missingYears.toFixed(2)) };
689
945
  }
946
+ function keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies) {
947
+ const base = requiredSet.has(keyword) ? 3 : preferredSet.has(keyword) ? 2 : 1;
948
+ const freqBonus = Math.min((jdFrequencies[keyword] ?? 1) - 1, 3) * 0.25;
949
+ return base + freqBonus;
950
+ }
690
951
  function scoreKeywords(resume, job, config) {
691
952
  const jobKeywordSet = new Set(
692
953
  job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
693
954
  );
694
955
  if (jobKeywordSet.size === 0) {
695
- return { score: 100, matchedKeywords: [], missingKeywords: [], overusedKeywords: [] };
956
+ return {
957
+ score: 100,
958
+ matchedKeywords: [],
959
+ missingKeywords: [],
960
+ overusedKeywords: [],
961
+ keywordsByCategory: emptyCategoryBuckets(),
962
+ keywordWeights: []
963
+ };
696
964
  }
697
965
  const resumeTokens = tokenize(resume.normalizedText).map(
698
966
  (t) => normalizeSkill(t, config.skillAliases)
699
967
  );
700
968
  const resumeTokenSet = new Set(resumeTokens);
969
+ const resumeFrequencies = countFrequencies(resumeTokens);
970
+ const requiredSet = new Set(job.requiredSkills);
971
+ const preferredSet = new Set(job.preferredSkills);
972
+ const jdFrequencies = countFrequencies(
973
+ tokenize(job.normalizedText).map((t) => normalizeSkill(t, config.skillAliases))
974
+ );
975
+ const weightOf = (keyword) => keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies);
701
976
  const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
702
977
  const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
703
- const coverage = matchedKeywords.length / jobKeywordSet.size;
704
- const score = clamp(coverage * 100, 0, 100);
705
- const frequencies = countFrequencies(resumeTokens);
978
+ const totalWeight = [...jobKeywordSet].reduce((sum, keyword) => sum + weightOf(keyword), 0);
979
+ const matchedWeight = matchedKeywords.reduce((sum, keyword) => sum + weightOf(keyword), 0);
980
+ const score = clamp(matchedWeight / totalWeight * 100, 0, 100);
706
981
  const totalTokens = resumeTokens.length || 1;
707
982
  const overusedKeywords = matchedKeywords.filter((keyword) => {
708
- const density = (frequencies[keyword] ?? 0) / totalTokens;
983
+ const density = (resumeFrequencies[keyword] ?? 0) / totalTokens;
709
984
  return density > config.keywordDensity.max;
710
985
  });
986
+ const keywordsByCategory = emptyCategoryBuckets();
987
+ for (const keyword of matchedKeywords) {
988
+ keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].matched.push(keyword);
989
+ }
990
+ for (const keyword of missingKeywords) {
991
+ keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].missing.push(keyword);
992
+ }
993
+ for (const bucket of Object.values(keywordsByCategory)) {
994
+ bucket.matched.sort();
995
+ bucket.missing.sort();
996
+ }
997
+ const keywordWeights = [...jobKeywordSet].map((term) => {
998
+ const weight = Number(weightOf(term).toFixed(2));
999
+ return {
1000
+ term,
1001
+ category: config.categoryIndex.get(term) ?? "technical",
1002
+ jdWeight: weight,
1003
+ resumeWeight: resumeFrequencies[term] ?? 0,
1004
+ importance: weight
1005
+ };
1006
+ }).sort((a, b) => a.term.localeCompare(b.term));
711
1007
  return {
712
1008
  score,
713
1009
  matchedKeywords: unique(matchedKeywords).sort(),
714
1010
  missingKeywords: unique(missingKeywords).sort(),
715
- overusedKeywords: unique(overusedKeywords).sort()
1011
+ overusedKeywords: unique(overusedKeywords).sort(),
1012
+ keywordsByCategory,
1013
+ keywordWeights
716
1014
  };
717
1015
  }
718
1016
  function scoreEducation(resume, job) {
719
1017
  if (job.educationRequirements.length === 0) {
720
1018
  return 100;
721
1019
  }
722
- const resumeEducationText = resume.educationEntries.join(" ");
723
- const normalizedEducation = resumeEducationText.toLowerCase();
1020
+ const resumeDegreeLevels = extractDegreeLevels(resume.educationEntries.join(" "));
724
1021
  const matched = job.educationRequirements.filter(
725
- (requirement) => normalizedEducation.includes(requirement.toLowerCase())
1022
+ (requirement) => resumeDegreeLevels.includes(requirement)
726
1023
  );
727
1024
  if (matched.length === 0) {
728
1025
  return 0;
@@ -741,6 +1038,14 @@ function calculateScore(resume, job, config) {
741
1038
  education: educationScore
742
1039
  };
743
1040
  const weightedScore = breakdown.skills * config.weights.skills + breakdown.experience * config.weights.experience + breakdown.keywords * config.weights.keywords + breakdown.education * config.weights.education;
1041
+ const achievementStrength = {
1042
+ strong: resume.achievements.filter((a) => a.strength === "strong").length,
1043
+ weak: resume.achievements.filter((a) => a.strength === "weak").length
1044
+ };
1045
+ const { matched: matchedLanguages, missing: missingLanguages } = diffLanguages(
1046
+ resume.languages,
1047
+ job.requiredLanguages
1048
+ );
744
1049
  return {
745
1050
  score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
746
1051
  breakdown,
@@ -749,6 +1054,11 @@ function calculateScore(resume, job, config) {
749
1054
  matchedKeywords: keywordResult.matchedKeywords,
750
1055
  missingKeywords: keywordResult.missingKeywords,
751
1056
  overusedKeywords: keywordResult.overusedKeywords,
1057
+ keywordsByCategory: keywordResult.keywordsByCategory,
1058
+ keywordWeights: keywordResult.keywordWeights,
1059
+ achievementStrength,
1060
+ matchedLanguages,
1061
+ missingLanguages,
752
1062
  suggestions: [],
753
1063
  warnings: [],
754
1064
  // detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
@@ -762,49 +1072,195 @@ function calculateScore(resume, job, config) {
762
1072
  }
763
1073
 
764
1074
  // src/profiles/index.ts
765
- var defaultSkillAliases = {
1075
+ var defaultKeywordRegistry = [
1076
+ // languages / frameworks
766
1077
  // ponytail: "node" split from javascript — Node.js runtime !== JS language
767
- javascript: ["js"],
768
- node: ["node.js", "nodejs"],
769
- typescript: ["ts"],
770
- react: ["reactjs", "react.js"],
771
- "c++": ["cpp"],
772
- "c#": ["csharp"],
773
- python: ["py"],
774
- sql: ["postgres", "mysql", "sqlite"],
775
- graphql: ["gql"],
776
- aws: ["amazon web services"],
777
- azure: ["microsoft azure"],
778
- gcp: ["google cloud", "google cloud platform"],
779
- docker: ["containers"],
780
- kubernetes: ["k8s"],
781
- html: ["html5"],
782
- css: ["css3"],
783
- // ML / data science
784
- pytorch: ["torch"],
785
- tensorflow: ["tf"],
786
- "scikit-learn": ["sklearn"],
787
- pandas: [],
788
- numpy: [],
789
- fastapi: [],
790
- flask: [],
791
- django: [],
792
- // data / infra
793
- kafka: [],
794
- redis: [],
795
- elasticsearch: ["elastic"],
796
- spark: ["apache spark"],
797
- // common pure-letter tech skills (no symbol chars)
798
- accessibility: ["a11y"],
799
- frontend: ["front-end"],
800
- backend: ["back-end"],
801
- security: ["cybersecurity"],
802
- testing: ["unittest", "pytest"],
803
- microservices: [],
804
- agile: ["scrum"],
805
- blockchain: [],
806
- devops: []
807
- };
1078
+ { canonical: "javascript", aliases: ["js"], category: "technical" },
1079
+ { canonical: "node", aliases: ["node.js", "nodejs"], category: "technical" },
1080
+ { canonical: "typescript", aliases: ["ts"], category: "technical" },
1081
+ { canonical: "react", aliases: ["reactjs", "react.js"], category: "technical" },
1082
+ { canonical: "angular", aliases: ["angularjs"], category: "technical" },
1083
+ { canonical: "vue", aliases: ["vue.js", "vuejs"], category: "technical" },
1084
+ { canonical: "svelte", aliases: [], category: "technical" },
1085
+ { canonical: "next.js", aliases: ["nextjs"], category: "technical" },
1086
+ { canonical: "c++", aliases: ["cpp"], category: "technical" },
1087
+ { canonical: "c#", aliases: ["csharp", ".net"], category: "technical" },
1088
+ { canonical: "java", aliases: [], category: "technical" },
1089
+ { canonical: "python", aliases: ["py"], category: "technical" },
1090
+ { canonical: "go", aliases: ["golang"], category: "technical" },
1091
+ { canonical: "rust", aliases: [], category: "technical" },
1092
+ { canonical: "ruby", aliases: ["ruby on rails", "rails"], category: "technical" },
1093
+ { canonical: "php", aliases: [], category: "technical" },
1094
+ { canonical: "swift", aliases: [], category: "technical" },
1095
+ { canonical: "kotlin", aliases: [], category: "technical" },
1096
+ { canonical: "scala", aliases: [], category: "technical" },
1097
+ { canonical: "html", aliases: ["html5"], category: "technical" },
1098
+ { canonical: "css", aliases: ["css3"], category: "technical" },
1099
+ { canonical: "ios development", aliases: ["ios"], category: "technical" },
1100
+ { canonical: "android development", aliases: ["android"], category: "technical" },
1101
+ { canonical: "react native", aliases: [], category: "technical" },
1102
+ { canonical: "flutter", aliases: [], category: "technical" },
1103
+ { canonical: "machine learning", aliases: ["ml"], category: "technical" },
1104
+ { canonical: "deep learning", aliases: [], category: "technical" },
1105
+ { canonical: "natural language processing", aliases: ["nlp"], category: "technical" },
1106
+ // tools / platforms / infra
1107
+ { canonical: "sql", aliases: ["postgres", "mysql", "sqlite"], category: "tool" },
1108
+ { canonical: "graphql", aliases: ["gql"], category: "tool" },
1109
+ { canonical: "aws", aliases: ["amazon web services"], category: "tool" },
1110
+ { canonical: "azure", aliases: ["microsoft azure"], category: "tool" },
1111
+ { canonical: "gcp", aliases: ["google cloud", "google cloud platform"], category: "tool" },
1112
+ { canonical: "docker", aliases: ["containers"], category: "tool" },
1113
+ { canonical: "kubernetes", aliases: ["k8s"], category: "tool" },
1114
+ { canonical: "terraform", aliases: [], category: "tool" },
1115
+ { canonical: "ansible", aliases: [], category: "tool" },
1116
+ { canonical: "jenkins", aliases: [], category: "tool" },
1117
+ { canonical: "git", aliases: ["github", "gitlab"], category: "tool" },
1118
+ { canonical: "jira", aliases: [], category: "tool" },
1119
+ { canonical: "confluence", aliases: [], category: "tool" },
1120
+ { canonical: "pytorch", aliases: ["torch"], category: "tool" },
1121
+ { canonical: "tensorflow", aliases: ["tf"], category: "tool" },
1122
+ { canonical: "scikit-learn", aliases: ["sklearn"], category: "tool" },
1123
+ { canonical: "pandas", aliases: [], category: "tool" },
1124
+ { canonical: "numpy", aliases: [], category: "tool" },
1125
+ { canonical: "fastapi", aliases: [], category: "tool" },
1126
+ { canonical: "flask", aliases: [], category: "tool" },
1127
+ { canonical: "django", aliases: [], category: "tool" },
1128
+ { canonical: "kafka", aliases: [], category: "tool" },
1129
+ { canonical: "redis", aliases: [], category: "tool" },
1130
+ { canonical: "elasticsearch", aliases: ["elastic"], category: "tool" },
1131
+ { canonical: "spark", aliases: ["apache spark"], category: "tool" },
1132
+ { canonical: "tableau", aliases: [], category: "tool" },
1133
+ { canonical: "power bi", aliases: ["powerbi"], category: "tool" },
1134
+ { canonical: "excel", aliases: ["microsoft excel", "ms excel"], category: "tool" },
1135
+ { canonical: "salesforce", aliases: [], category: "tool" },
1136
+ { canonical: "hubspot", aliases: [], category: "tool" },
1137
+ { canonical: "sap", aliases: [], category: "tool" },
1138
+ { canonical: "quickbooks", aliases: [], category: "tool" },
1139
+ { canonical: "workday", aliases: [], category: "tool" },
1140
+ { canonical: "zendesk", aliases: [], category: "tool" },
1141
+ { canonical: "servicenow", aliases: [], category: "tool" },
1142
+ { canonical: "figma", aliases: [], category: "tool" },
1143
+ { canonical: "photoshop", aliases: ["adobe photoshop"], category: "tool" },
1144
+ { canonical: "illustrator", aliases: ["adobe illustrator"], category: "tool" },
1145
+ { canonical: "autocad", aliases: [], category: "tool" },
1146
+ // engineering concepts
1147
+ { canonical: "accessibility", aliases: ["a11y"], category: "concept" },
1148
+ { canonical: "frontend", aliases: ["front-end"], category: "concept" },
1149
+ { canonical: "backend", aliases: ["back-end"], category: "concept" },
1150
+ { canonical: "security", aliases: ["cybersecurity"], category: "concept" },
1151
+ { canonical: "testing", aliases: ["unittest", "pytest"], category: "concept" },
1152
+ { canonical: "microservices", aliases: [], category: "concept" },
1153
+ { canonical: "agile", aliases: ["scrum"], category: "concept" },
1154
+ { canonical: "kanban", aliases: [], category: "concept" },
1155
+ { canonical: "blockchain", aliases: [], category: "concept" },
1156
+ { canonical: "devops", aliases: [], category: "concept" },
1157
+ { canonical: "ci/cd", aliases: ["continuous integration", "continuous deployment"], category: "concept" },
1158
+ { canonical: "rest api", aliases: ["restful api", "rest apis"], category: "concept" },
1159
+ { canonical: "design patterns", aliases: [], category: "concept" },
1160
+ { canonical: "data structures", aliases: [], category: "concept" },
1161
+ { canonical: "algorithms", aliases: [], category: "concept" },
1162
+ { canonical: "cloud computing", aliases: [], category: "concept" },
1163
+ { canonical: "system design", aliases: [], category: "concept" },
1164
+ { canonical: "tdd", aliases: ["test driven development", "test-driven development"], category: "concept" },
1165
+ { canonical: "ux design", aliases: ["user experience"], category: "concept" },
1166
+ { canonical: "ui design", aliases: ["user interface design"], category: "concept" },
1167
+ { canonical: "project management", aliases: [], category: "concept" },
1168
+ { canonical: "change management", aliases: [], category: "concept" },
1169
+ { canonical: "risk management", aliases: [], category: "concept" },
1170
+ { canonical: "quality assurance", aliases: ["qa"], category: "concept" },
1171
+ // product / data domain
1172
+ { canonical: "roadmap", aliases: [], category: "domain" },
1173
+ { canonical: "stakeholder management", aliases: [], category: "domain" },
1174
+ { canonical: "prioritization", aliases: [], category: "domain" },
1175
+ { canonical: "a/b testing", aliases: ["ab testing"], category: "domain" },
1176
+ { canonical: "analytics", aliases: [], category: "domain" },
1177
+ { canonical: "statistics", aliases: ["stats"], category: "domain" },
1178
+ { canonical: "data visualization", aliases: [], category: "domain" },
1179
+ // finance / accounting domain
1180
+ { canonical: "financial analysis", aliases: [], category: "domain" },
1181
+ { canonical: "budgeting", aliases: [], category: "domain" },
1182
+ { canonical: "forecasting", aliases: [], category: "domain" },
1183
+ { canonical: "bookkeeping", aliases: [], category: "domain" },
1184
+ { canonical: "accounts payable", aliases: ["ap"], category: "domain" },
1185
+ { canonical: "accounts receivable", aliases: ["ar"], category: "domain" },
1186
+ { canonical: "payroll", aliases: [], category: "domain" },
1187
+ { canonical: "auditing", aliases: ["audit"], category: "domain" },
1188
+ { canonical: "tax preparation", aliases: [], category: "domain" },
1189
+ { canonical: "gaap", aliases: [], category: "domain" },
1190
+ // sales / account management domain
1191
+ { canonical: "lead generation", aliases: [], category: "domain" },
1192
+ { canonical: "account management", aliases: [], category: "domain" },
1193
+ { canonical: "crm", aliases: ["customer relationship management"], category: "domain" },
1194
+ { canonical: "sales pipeline", aliases: [], category: "domain" },
1195
+ { canonical: "cold calling", aliases: [], category: "domain" },
1196
+ { canonical: "upselling", aliases: ["cross-selling"], category: "domain" },
1197
+ { canonical: "customer retention", aliases: [], category: "domain" },
1198
+ // human resources domain
1199
+ { canonical: "recruiting", aliases: ["talent acquisition"], category: "domain" },
1200
+ { canonical: "onboarding", aliases: [], category: "domain" },
1201
+ { canonical: "employee relations", aliases: [], category: "domain" },
1202
+ { canonical: "benefits administration", aliases: [], category: "domain" },
1203
+ { canonical: "performance management", aliases: [], category: "domain" },
1204
+ // healthcare domain
1205
+ { canonical: "patient care", aliases: [], category: "domain" },
1206
+ { canonical: "clinical documentation", aliases: [], category: "domain" },
1207
+ { canonical: "hipaa", aliases: [], category: "domain" },
1208
+ { canonical: "electronic health records", aliases: ["ehr", "emr"], category: "domain" },
1209
+ { canonical: "medical billing", aliases: [], category: "domain" },
1210
+ // legal domain
1211
+ { canonical: "contract review", aliases: [], category: "domain" },
1212
+ { canonical: "legal research", aliases: [], category: "domain" },
1213
+ { canonical: "litigation", aliases: [], category: "domain" },
1214
+ { canonical: "regulatory compliance", aliases: ["compliance"], category: "domain" },
1215
+ { canonical: "due diligence", aliases: [], category: "domain" },
1216
+ // education domain
1217
+ { canonical: "curriculum development", aliases: [], category: "domain" },
1218
+ { canonical: "lesson planning", aliases: [], category: "domain" },
1219
+ { canonical: "classroom management", aliases: [], category: "domain" },
1220
+ { canonical: "instructional design", aliases: [], category: "domain" },
1221
+ // operations / supply chain domain
1222
+ { canonical: "supply chain management", aliases: ["supply chain"], category: "domain" },
1223
+ { canonical: "inventory management", aliases: [], category: "domain" },
1224
+ { canonical: "procurement", aliases: [], category: "domain" },
1225
+ { canonical: "vendor management", aliases: [], category: "domain" },
1226
+ { canonical: "logistics", aliases: [], category: "domain" },
1227
+ // customer service domain
1228
+ { canonical: "customer support", aliases: ["customer service"], category: "domain" },
1229
+ { canonical: "technical support", aliases: [], category: "domain" },
1230
+ { canonical: "conflict resolution", aliases: [], category: "domain" },
1231
+ // soft skills
1232
+ { canonical: "communication", aliases: [], category: "soft" },
1233
+ { canonical: "leadership", aliases: [], category: "soft" },
1234
+ { canonical: "teamwork", aliases: ["collaboration"], category: "soft" },
1235
+ { canonical: "problem solving", aliases: ["problem-solving"], category: "soft" },
1236
+ { canonical: "adaptability", aliases: ["flexibility"], category: "soft" },
1237
+ { canonical: "time management", aliases: [], category: "soft" },
1238
+ { canonical: "critical thinking", aliases: [], category: "soft" },
1239
+ { canonical: "creativity", aliases: [], category: "soft" },
1240
+ { canonical: "attention to detail", aliases: [], category: "soft" },
1241
+ { canonical: "decision making", aliases: ["decision-making"], category: "soft" },
1242
+ { canonical: "emotional intelligence", aliases: [], category: "soft" },
1243
+ { canonical: "negotiation", aliases: [], category: "soft" },
1244
+ { canonical: "organization", aliases: ["organizational skills"], category: "soft" },
1245
+ { canonical: "public speaking", aliases: ["presentation skills"], category: "soft" },
1246
+ { canonical: "mentoring", aliases: ["coaching"], category: "soft" },
1247
+ { canonical: "interpersonal skills", aliases: [], category: "soft" },
1248
+ { canonical: "work ethic", aliases: [], category: "soft" },
1249
+ // marketing
1250
+ { canonical: "seo", aliases: ["search engine optimization"], category: "marketing" },
1251
+ { canonical: "branding", aliases: ["brand strategy"], category: "marketing" },
1252
+ { canonical: "campaign management", aliases: [], category: "marketing" },
1253
+ { canonical: "content marketing", aliases: [], category: "marketing" },
1254
+ { canonical: "social media marketing", aliases: ["social media"], category: "marketing" },
1255
+ { canonical: "email marketing", aliases: [], category: "marketing" },
1256
+ { canonical: "digital marketing", aliases: [], category: "marketing" },
1257
+ { canonical: "copywriting", aliases: [], category: "marketing" },
1258
+ { canonical: "market research", aliases: [], category: "marketing" },
1259
+ { canonical: "ppc", aliases: ["pay-per-click", "google ads"], category: "marketing" },
1260
+ { canonical: "conversion rate optimization", aliases: ["cro"], category: "marketing" },
1261
+ { canonical: "public relations", aliases: ["pr"], category: "marketing" }
1262
+ ];
1263
+ var defaultSkillAliases = deriveSkillAliases(defaultKeywordRegistry);
808
1264
  var softwareEngineerProfile = {
809
1265
  name: "software-engineer",
810
1266
  mandatorySkills: ["javascript", "typescript", "react", "node"],
@@ -874,9 +1330,12 @@ function resolveConfig(config = {}) {
874
1330
  keywords: config.weights?.keywords ?? DEFAULT_WEIGHTS.keywords,
875
1331
  education: config.weights?.education ?? DEFAULT_WEIGHTS.education
876
1332
  };
1333
+ const keywordRegistry = mergeKeywordRegistries(defaultKeywordRegistry, config.keywordRegistry ?? []);
877
1334
  const resolved = {
878
1335
  weights: normalizeWeights(weights),
879
- skillAliases: { ...defaultSkillAliases, ...config.skillAliases ?? {} },
1336
+ skillAliases: { ...deriveSkillAliases(keywordRegistry), ...config.skillAliases ?? {} },
1337
+ keywordRegistry,
1338
+ categoryIndex: buildCategoryIndex(keywordRegistry),
880
1339
  profile: config.profile ?? softwareEngineerProfile,
881
1340
  rules: config.rules ?? [],
882
1341
  keywordDensity: config.keywordDensity ?? DEFAULT_KEYWORD_DENSITY,
@@ -896,6 +1355,18 @@ function formatList(values, max = 6) {
896
1355
  const trimmed = uniqueValues.slice(0, max);
897
1356
  return trimmed.join(", ") + (uniqueValues.length > max ? "..." : "");
898
1357
  }
1358
+ function buildAliasReplacementSuggestions(resume, job, config) {
1359
+ const jobKeywordSet = new Set(job.keywords.map((k) => normalizeSkill(k, config.skillAliases)));
1360
+ const replacements = [];
1361
+ for (const token of unique(tokenize(resume.normalizedText))) {
1362
+ const canonical = normalizeSkill(token, config.skillAliases);
1363
+ const jdSurface = job.keywordSurfaceForms[canonical];
1364
+ if (jdSurface && jobKeywordSet.has(canonical) && jdSurface.toLowerCase() !== token.toLowerCase()) {
1365
+ replacements.push(`Replace "${token}" with "${jdSurface}" to match the job description's wording.`);
1366
+ }
1367
+ }
1368
+ return unique(replacements).slice(0, 5);
1369
+ }
899
1370
  var SuggestionEngine = class {
900
1371
  generate(input) {
901
1372
  const suggestions = [];
@@ -910,6 +1381,7 @@ var SuggestionEngine = class {
910
1381
  `Incorporate job-specific keywords: ${formatList(input.score.missingKeywords)}`
911
1382
  );
912
1383
  }
1384
+ suggestions.push(...buildAliasReplacementSuggestions(input.resume, input.job, input.config));
913
1385
  if (input.score.overusedKeywords.length > 0) {
914
1386
  suggestions.push(
915
1387
  `Avoid keyword stuffing for: ${formatList(input.score.overusedKeywords)}`
@@ -930,6 +1402,21 @@ var SuggestionEngine = class {
930
1402
  "Strengthen bullet points with impact verbs (led, built, improved, delivered)."
931
1403
  );
932
1404
  }
1405
+ if (input.resume.weakVerbs.length > 0) {
1406
+ suggestions.push(
1407
+ `Replace weak verbs (${formatList(input.resume.weakVerbs)}) with stronger ones (e.g. led, built, optimized).`
1408
+ );
1409
+ }
1410
+ if (input.score.missingLanguages.length > 0) {
1411
+ const formatted = input.score.missingLanguages.map((l) => l.level ? `${l.name} (${l.level})` : l.name).join(", ");
1412
+ suggestions.push(`Mention your proficiency in: ${formatted}`);
1413
+ }
1414
+ const weakAchievement = input.resume.achievements.find((a) => a.strength === "weak");
1415
+ if (weakAchievement) {
1416
+ suggestions.push(
1417
+ `Strengthen "${weakAchievement.text}" \u2014 add scope/metrics, e.g. "Built and maintained scalable services handling 500k+ requests/day."`
1418
+ );
1419
+ }
933
1420
  if (input.resume.detectedSections.length < 2 && input.resume.raw.trim().length > 300) {
934
1421
  suggestions.push(
935
1422
  "Your resume may use a multi-column layout. Export as a single-column PDF or paste plain text \u2014 most ATS systems and this parser work best with a linear layout."
@@ -1572,7 +2059,8 @@ function analyzeResume(input) {
1572
2059
  resume: parsedResume,
1573
2060
  job: parsedJob,
1574
2061
  score: scoring,
1575
- ruleWarnings: ruleResult.warnings
2062
+ ruleWarnings: ruleResult.warnings,
2063
+ config: resolvedConfig
1576
2064
  });
1577
2065
  let suggestions = suggestionResult.suggestions;
1578
2066
  const llmWarnings = [];
@@ -1592,6 +2080,11 @@ function analyzeResume(input) {
1592
2080
  matchedKeywords: scoring.matchedKeywords,
1593
2081
  missingKeywords: scoring.missingKeywords,
1594
2082
  overusedKeywords: scoring.overusedKeywords,
2083
+ keywordsByCategory: scoring.keywordsByCategory,
2084
+ keywordWeights: scoring.keywordWeights,
2085
+ achievementStrength: scoring.achievementStrength,
2086
+ matchedLanguages: scoring.matchedLanguages,
2087
+ missingLanguages: scoring.missingLanguages,
1595
2088
  experienceGap: scoring.experienceGap,
1596
2089
  detectedSections: parsedResume.detectedSections,
1597
2090
  parsedExperienceYears: parsedResume.totalExperienceYears,
@@ -1634,7 +2127,8 @@ async function analyzeResumeAsync(input) {
1634
2127
  resume: parsedResume,
1635
2128
  job: parsedJob,
1636
2129
  score: scoring,
1637
- ruleWarnings: ruleResult.warnings
2130
+ ruleWarnings: ruleResult.warnings,
2131
+ config: resolvedConfig
1638
2132
  });
1639
2133
  let suggestions = suggestionResult.suggestions;
1640
2134
  const llmWarnings = [];
@@ -1657,6 +2151,11 @@ async function analyzeResumeAsync(input) {
1657
2151
  matchedKeywords: scoring.matchedKeywords,
1658
2152
  missingKeywords: scoring.missingKeywords,
1659
2153
  overusedKeywords: scoring.overusedKeywords,
2154
+ keywordsByCategory: scoring.keywordsByCategory,
2155
+ keywordWeights: scoring.keywordWeights,
2156
+ achievementStrength: scoring.achievementStrength,
2157
+ matchedLanguages: scoring.matchedLanguages,
2158
+ missingLanguages: scoring.missingLanguages,
1660
2159
  experienceGap: scoring.experienceGap,
1661
2160
  detectedSections: parsedResume.detectedSections,
1662
2161
  parsedExperienceYears: parsedResume.totalExperienceYears,
@@ -1712,10 +2211,11 @@ exports.adaptSuggestionEnhancementResponse = adaptSuggestionEnhancementResponse;
1712
2211
  exports.analyzeResume = analyzeResume;
1713
2212
  exports.analyzeResumeAsync = analyzeResumeAsync;
1714
2213
  exports.createPrompt = createPrompt;
2214
+ exports.defaultKeywordRegistry = defaultKeywordRegistry;
1715
2215
  exports.defaultProfiles = defaultProfiles;
1716
2216
  exports.defaultSkillAliases = defaultSkillAliases;
1717
2217
  exports.safeExtractArray = safeExtractArray;
1718
2218
  exports.safeExtractNumber = safeExtractNumber;
1719
2219
  exports.safeExtractString = safeExtractString;
1720
- //# sourceMappingURL=index.js.map
1721
- //# sourceMappingURL=index.js.map
2220
+ //# sourceMappingURL=index.cjs.map
2221
+ //# sourceMappingURL=index.cjs.map