@pranavraut033/ats-checker 1.1.1 → 1.3.0

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,20 +185,157 @@ 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
+ };
287
+ var LANGUAGE_GROUP = KNOWN_LANGUAGES.join("|");
288
+ var LEVEL_GROUP = Object.keys(LEVEL_RANK).sort((a, b) => b.length - a.length).map((l) => l.replace(/\s+/g, "\\s+")).join("|");
289
+ var LANGUAGE_LEVEL_RE = new RegExp(
290
+ `\\b(${LANGUAGE_GROUP})\\b(?:\\s*[\\(:\\-]?\\s*(${LEVEL_GROUP}|[abc][12]))?`,
291
+ "gi"
292
+ );
293
+ var LEVEL_BEFORE_LANGUAGE_RE = new RegExp(`\\b(${LEVEL_GROUP})\\s+(?:in\\s+)?(${LANGUAGE_GROUP})\\b`, "gi");
294
+ function canonicalLanguage(name) {
295
+ const lower = name.toLowerCase();
296
+ return LANGUAGE_ALIASES[lower] ?? lower;
297
+ }
298
+ function toParsedLanguage(name, level) {
299
+ const normalizedLevel = level?.toLowerCase().replace(/\s+/g, " ");
300
+ return {
301
+ name: canonicalLanguage(name),
302
+ level: normalizedLevel,
303
+ levelRank: normalizedLevel ? LEVEL_RANK[normalizedLevel] : void 0
304
+ };
305
+ }
306
+ function parseLanguageMentions(text) {
307
+ const found = /* @__PURE__ */ new Map();
308
+ for (const match of text.matchAll(LANGUAGE_LEVEL_RE)) {
309
+ const parsed = toParsedLanguage(match[1], match[2]);
310
+ const existing = found.get(parsed.name);
311
+ if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
312
+ found.set(parsed.name, parsed);
193
313
  }
194
- if (aliasList.some((alias) => alias.toLowerCase() === normalized)) {
195
- return canonical.toLowerCase();
314
+ }
315
+ for (const match of text.matchAll(LEVEL_BEFORE_LANGUAGE_RE)) {
316
+ const parsed = toParsedLanguage(match[2], match[1]);
317
+ const existing = found.get(parsed.name);
318
+ if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
319
+ found.set(parsed.name, parsed);
196
320
  }
197
321
  }
198
- return normalized;
322
+ return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
199
323
  }
200
- function normalizeSkills(skills, aliases) {
201
- return unique(skills.map((skill) => normalizeSkill(skill, aliases)));
324
+ function diffLanguages(resumeLanguages, requiredLanguages) {
325
+ const byName = new Map(resumeLanguages.map((l) => [l.name, l]));
326
+ const matched = [];
327
+ const missing = [];
328
+ for (const required of requiredLanguages) {
329
+ const have = byName.get(required.name);
330
+ const requiredRank = required.levelRank ?? 0;
331
+ const haveRank = have?.levelRank ?? 0;
332
+ if (have && haveRank >= requiredRank) {
333
+ matched.push(required);
334
+ } else {
335
+ missing.push(required);
336
+ }
337
+ }
338
+ return { matched, missing };
202
339
  }
203
340
 
204
341
  // src/core/parser/jd.parser.ts
@@ -239,6 +376,18 @@ function extractMinExperience(text) {
239
376
  }
240
377
  return void 0;
241
378
  }
379
+ var SURFACE_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/gi;
380
+ function collectKeywordSurfaceForms(rawText, aliases) {
381
+ const matches = rawText.match(SURFACE_TOKEN_RE) ?? [];
382
+ const surfaceForms = {};
383
+ for (const match of matches) {
384
+ const canonical = normalizeSkill(match, aliases);
385
+ if (!(canonical in surfaceForms)) {
386
+ surfaceForms[canonical] = match;
387
+ }
388
+ }
389
+ return surfaceForms;
390
+ }
242
391
  function extractEducationRequirements(text) {
243
392
  const found = /* @__PURE__ */ new Set();
244
393
  for (const [pattern, canonical] of DEGREE_VARIANTS) {
@@ -277,7 +426,11 @@ function parseJobDescription(jobDescription, config) {
277
426
  roleKeywords,
278
427
  keywords,
279
428
  minExperienceYears: extractMinExperience(jobDescription),
280
- educationRequirements: extractEducationRequirements(jobDescription)
429
+ educationRequirements: extractEducationRequirements(jobDescription),
430
+ keywordSurfaceForms: collectKeywordSurfaceForms(jobDescription, config.skillAliases),
431
+ // ponytail: any language mention in the JD is treated as a requirement — good enough until
432
+ // JDs that merely *reference* a language (not require it) show up as false positives.
433
+ requiredLanguages: parseLanguageMentions(jobDescription)
281
434
  };
282
435
  }
283
436
 
@@ -410,7 +563,7 @@ var SECTION_ALIASES = {
410
563
  projects: ["projects", "portfolio"],
411
564
  certifications: ["certifications", "licenses"]
412
565
  };
413
- var ACTION_VERBS = [
566
+ var STRONG_VERBS = [
414
567
  "led",
415
568
  "managed",
416
569
  "built",
@@ -431,6 +584,21 @@ var ACTION_VERBS = [
431
584
  "reduced",
432
585
  "increased"
433
586
  ];
587
+ var WEAK_VERBS = ["worked", "helped", "performed", "responsible", "assisted", "participated", "involved"];
588
+ var METRIC_RE = /\d|%|\$|\bk\+|\bm\+/i;
589
+ function classifyAchievement(line) {
590
+ const lower = line.toLowerCase();
591
+ const hasMetric = METRIC_RE.test(line);
592
+ const hasStrongVerb = STRONG_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
593
+ const hasWeakVerb = WEAK_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
594
+ if (hasStrongVerb && hasMetric) {
595
+ return { text: line, strength: "strong", reason: "strong verb + quantified impact" };
596
+ }
597
+ if (hasWeakVerb) {
598
+ return { text: line, strength: "weak", reason: "weak verb" };
599
+ }
600
+ return { text: line, strength: "weak", reason: "no quantified impact" };
601
+ }
434
602
  function detectSection(line) {
435
603
  const normalized = line.trim().toLowerCase();
436
604
  for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
@@ -476,16 +644,20 @@ function parseSkills(sectionContent, aliases) {
476
644
  }
477
645
  function parseActionVerbs(text) {
478
646
  const words = tokenize(text);
479
- return ACTION_VERBS.filter((verb) => words.includes(verb));
647
+ return {
648
+ strong: STRONG_VERBS.filter((verb) => words.includes(verb)),
649
+ weak: WEAK_VERBS.filter((verb) => words.includes(verb))
650
+ };
480
651
  }
481
652
  function parseExperience(sectionContent, referenceDate) {
482
653
  if (!sectionContent) {
483
- return { entries: [], rangesInMonths: [], jobTitles: [] };
654
+ return { entries: [], rangesInMonths: [], jobTitles: [], achievements: [] };
484
655
  }
485
656
  const lines = splitLines(sectionContent);
486
657
  const entries = [];
487
658
  const rangesInMonths = [];
488
659
  const jobTitles = [];
660
+ const achievements = [];
489
661
  for (const line of lines) {
490
662
  const range = parseDateRange(line, referenceDate);
491
663
  if (range) {
@@ -506,14 +678,16 @@ function parseExperience(sectionContent, referenceDate) {
506
678
  jobTitles.push(title.toLowerCase());
507
679
  const entry = { title, description: line };
508
680
  entries.push(entry);
681
+ achievements.push(classifyAchievement(line));
509
682
  continue;
510
683
  }
511
684
  if (entries.length > 0) {
512
685
  const current = entries[entries.length - 1];
513
686
  current.description = [current.description, line].filter(Boolean).join(" ").trim();
514
687
  }
688
+ achievements.push(classifyAchievement(line));
515
689
  }
516
- return { entries, rangesInMonths, jobTitles: unique(jobTitles) };
690
+ return { entries, rangesInMonths, jobTitles: unique(jobTitles), achievements };
517
691
  }
518
692
  function parseEducation(sectionContent) {
519
693
  if (!sectionContent) return [];
@@ -541,6 +715,16 @@ function parseResume(resumeText, config) {
541
715
  }
542
716
  const requiredSections = ["summary", "experience", "skills", "education"];
543
717
  const warnings = [];
718
+ const lineCount = splitLines(resumeText).length;
719
+ if (resumeText.trim().length < 100) {
720
+ warnings.push(
721
+ "Almost no text was extracted \u2014 the resume may be a scanned/image PDF. Upload a text-based PDF or paste the text directly."
722
+ );
723
+ } else if (lineCount <= 2) {
724
+ warnings.push(
725
+ "Resume text has no line breaks \u2014 the PDF layout likely didn't export cleanly (common with multi-column designs). Export as a single-column PDF or paste plain text for accurate parsing."
726
+ );
727
+ }
544
728
  for (const section of requiredSections) {
545
729
  if (!detected.includes(section)) {
546
730
  warnings.push(`${section} section not detected`);
@@ -553,11 +737,14 @@ function parseResume(resumeText, config) {
553
737
  sectionContent: sections,
554
738
  skills,
555
739
  jobTitles: experienceData.jobTitles,
556
- actionVerbs,
740
+ actionVerbs: actionVerbs.strong,
741
+ weakVerbs: actionVerbs.weak,
742
+ achievements: experienceData.achievements,
557
743
  educationEntries,
558
744
  experience: experienceData.entries,
559
745
  totalExperienceYears,
560
746
  keywords: collectKeywords(normalizedText),
747
+ languages: parseLanguageMentions(resumeText),
561
748
  warnings
562
749
  };
563
750
  }
@@ -639,6 +826,14 @@ var REQUIRED_SKILL_WEIGHT = 0.7;
639
826
  var OPTIONAL_SKILL_WEIGHT = 0.3;
640
827
  var EXPERIENCE_YEARS_WEIGHT = 0.75;
641
828
  var EXPERIENCE_ROLE_WEIGHT = 0.25;
829
+ var ALL_CATEGORIES = ["technical", "tool", "concept", "soft", "marketing", "domain"];
830
+ function emptyCategoryBuckets() {
831
+ const buckets = {};
832
+ for (const category of ALL_CATEGORIES) {
833
+ buckets[category] = { matched: [], missing: [] };
834
+ }
835
+ return buckets;
836
+ }
642
837
  function scoreSkills(resume, job, config) {
643
838
  const profileRequired = config.profile?.mandatorySkills ?? [];
644
839
  const profileOptional = config.profile?.optionalSkills ?? [];
@@ -677,32 +872,74 @@ function scoreExperience(resume, job, config) {
677
872
  const missingYears = Math.max(requiredYears - resume.totalExperienceYears, 0);
678
873
  return { score, missingYears: Number(missingYears.toFixed(2)) };
679
874
  }
875
+ function keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies) {
876
+ const base = requiredSet.has(keyword) ? 3 : preferredSet.has(keyword) ? 2 : 1;
877
+ const freqBonus = Math.min((jdFrequencies[keyword] ?? 1) - 1, 3) * 0.25;
878
+ return base + freqBonus;
879
+ }
680
880
  function scoreKeywords(resume, job, config) {
681
881
  const jobKeywordSet = new Set(
682
882
  job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
683
883
  );
684
884
  if (jobKeywordSet.size === 0) {
685
- return { score: 100, matchedKeywords: [], missingKeywords: [], overusedKeywords: [] };
885
+ return {
886
+ score: 100,
887
+ matchedKeywords: [],
888
+ missingKeywords: [],
889
+ overusedKeywords: [],
890
+ keywordsByCategory: emptyCategoryBuckets(),
891
+ keywordWeights: []
892
+ };
686
893
  }
687
894
  const resumeTokens = tokenize(resume.normalizedText).map(
688
895
  (t) => normalizeSkill(t, config.skillAliases)
689
896
  );
690
897
  const resumeTokenSet = new Set(resumeTokens);
898
+ const resumeFrequencies = countFrequencies(resumeTokens);
899
+ const requiredSet = new Set(job.requiredSkills);
900
+ const preferredSet = new Set(job.preferredSkills);
901
+ const jdFrequencies = countFrequencies(
902
+ tokenize(job.normalizedText).map((t) => normalizeSkill(t, config.skillAliases))
903
+ );
904
+ const weightOf = (keyword) => keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies);
691
905
  const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
692
906
  const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
693
- const coverage = matchedKeywords.length / jobKeywordSet.size;
694
- const score = clamp(coverage * 100, 0, 100);
695
- const frequencies = countFrequencies(resumeTokens);
907
+ const totalWeight = [...jobKeywordSet].reduce((sum, keyword) => sum + weightOf(keyword), 0);
908
+ const matchedWeight = matchedKeywords.reduce((sum, keyword) => sum + weightOf(keyword), 0);
909
+ const score = clamp(matchedWeight / totalWeight * 100, 0, 100);
696
910
  const totalTokens = resumeTokens.length || 1;
697
911
  const overusedKeywords = matchedKeywords.filter((keyword) => {
698
- const density = (frequencies[keyword] ?? 0) / totalTokens;
912
+ const density = (resumeFrequencies[keyword] ?? 0) / totalTokens;
699
913
  return density > config.keywordDensity.max;
700
914
  });
915
+ const keywordsByCategory = emptyCategoryBuckets();
916
+ for (const keyword of matchedKeywords) {
917
+ keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].matched.push(keyword);
918
+ }
919
+ for (const keyword of missingKeywords) {
920
+ keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].missing.push(keyword);
921
+ }
922
+ for (const bucket of Object.values(keywordsByCategory)) {
923
+ bucket.matched.sort();
924
+ bucket.missing.sort();
925
+ }
926
+ const keywordWeights = [...jobKeywordSet].map((term) => {
927
+ const weight = Number(weightOf(term).toFixed(2));
928
+ return {
929
+ term,
930
+ category: config.categoryIndex.get(term) ?? "technical",
931
+ jdWeight: weight,
932
+ resumeWeight: resumeFrequencies[term] ?? 0,
933
+ importance: weight
934
+ };
935
+ }).sort((a, b) => a.term.localeCompare(b.term));
701
936
  return {
702
937
  score,
703
938
  matchedKeywords: unique(matchedKeywords).sort(),
704
939
  missingKeywords: unique(missingKeywords).sort(),
705
- overusedKeywords: unique(overusedKeywords).sort()
940
+ overusedKeywords: unique(overusedKeywords).sort(),
941
+ keywordsByCategory,
942
+ keywordWeights
706
943
  };
707
944
  }
708
945
  function scoreEducation(resume, job) {
@@ -731,6 +968,14 @@ function calculateScore(resume, job, config) {
731
968
  education: educationScore
732
969
  };
733
970
  const weightedScore = breakdown.skills * config.weights.skills + breakdown.experience * config.weights.experience + breakdown.keywords * config.weights.keywords + breakdown.education * config.weights.education;
971
+ const achievementStrength = {
972
+ strong: resume.achievements.filter((a) => a.strength === "strong").length,
973
+ weak: resume.achievements.filter((a) => a.strength === "weak").length
974
+ };
975
+ const { matched: matchedLanguages, missing: missingLanguages } = diffLanguages(
976
+ resume.languages,
977
+ job.requiredLanguages
978
+ );
734
979
  return {
735
980
  score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
736
981
  breakdown,
@@ -739,6 +984,11 @@ function calculateScore(resume, job, config) {
739
984
  matchedKeywords: keywordResult.matchedKeywords,
740
985
  missingKeywords: keywordResult.missingKeywords,
741
986
  overusedKeywords: keywordResult.overusedKeywords,
987
+ keywordsByCategory: keywordResult.keywordsByCategory,
988
+ keywordWeights: keywordResult.keywordWeights,
989
+ achievementStrength,
990
+ matchedLanguages,
991
+ missingLanguages,
742
992
  suggestions: [],
743
993
  warnings: [],
744
994
  // detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
@@ -752,49 +1002,195 @@ function calculateScore(resume, job, config) {
752
1002
  }
753
1003
 
754
1004
  // src/profiles/index.ts
755
- var defaultSkillAliases = {
1005
+ var defaultKeywordRegistry = [
1006
+ // languages / frameworks
756
1007
  // ponytail: "node" split from javascript — Node.js runtime !== JS language
757
- javascript: ["js"],
758
- node: ["node.js", "nodejs"],
759
- typescript: ["ts"],
760
- react: ["reactjs", "react.js"],
761
- "c++": ["cpp"],
762
- "c#": ["csharp"],
763
- python: ["py"],
764
- sql: ["postgres", "mysql", "sqlite"],
765
- graphql: ["gql"],
766
- aws: ["amazon web services"],
767
- azure: ["microsoft azure"],
768
- gcp: ["google cloud", "google cloud platform"],
769
- docker: ["containers"],
770
- kubernetes: ["k8s"],
771
- html: ["html5"],
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: []
797
- };
1008
+ { canonical: "javascript", aliases: ["js"], category: "technical" },
1009
+ { canonical: "node", aliases: ["node.js", "nodejs"], category: "technical" },
1010
+ { canonical: "typescript", aliases: ["ts"], category: "technical" },
1011
+ { canonical: "react", aliases: ["reactjs", "react.js"], category: "technical" },
1012
+ { canonical: "angular", aliases: ["angularjs"], category: "technical" },
1013
+ { canonical: "vue", aliases: ["vue.js", "vuejs"], category: "technical" },
1014
+ { canonical: "svelte", aliases: [], category: "technical" },
1015
+ { canonical: "next.js", aliases: ["nextjs"], category: "technical" },
1016
+ { canonical: "c++", aliases: ["cpp"], category: "technical" },
1017
+ { canonical: "c#", aliases: ["csharp", ".net"], category: "technical" },
1018
+ { canonical: "java", aliases: [], category: "technical" },
1019
+ { canonical: "python", aliases: ["py"], category: "technical" },
1020
+ { canonical: "go", aliases: ["golang"], category: "technical" },
1021
+ { canonical: "rust", aliases: [], category: "technical" },
1022
+ { canonical: "ruby", aliases: ["ruby on rails", "rails"], category: "technical" },
1023
+ { canonical: "php", aliases: [], category: "technical" },
1024
+ { canonical: "swift", aliases: [], category: "technical" },
1025
+ { canonical: "kotlin", aliases: [], category: "technical" },
1026
+ { canonical: "scala", aliases: [], category: "technical" },
1027
+ { canonical: "html", aliases: ["html5"], category: "technical" },
1028
+ { canonical: "css", aliases: ["css3"], category: "technical" },
1029
+ { canonical: "ios development", aliases: ["ios"], category: "technical" },
1030
+ { canonical: "android development", aliases: ["android"], category: "technical" },
1031
+ { canonical: "react native", aliases: [], category: "technical" },
1032
+ { canonical: "flutter", aliases: [], category: "technical" },
1033
+ { canonical: "machine learning", aliases: ["ml"], category: "technical" },
1034
+ { canonical: "deep learning", aliases: [], category: "technical" },
1035
+ { canonical: "natural language processing", aliases: ["nlp"], category: "technical" },
1036
+ // tools / platforms / infra
1037
+ { canonical: "sql", aliases: ["postgres", "mysql", "sqlite"], category: "tool" },
1038
+ { canonical: "graphql", aliases: ["gql"], category: "tool" },
1039
+ { canonical: "aws", aliases: ["amazon web services"], category: "tool" },
1040
+ { canonical: "azure", aliases: ["microsoft azure"], category: "tool" },
1041
+ { canonical: "gcp", aliases: ["google cloud", "google cloud platform"], category: "tool" },
1042
+ { canonical: "docker", aliases: ["containers"], category: "tool" },
1043
+ { canonical: "kubernetes", aliases: ["k8s"], category: "tool" },
1044
+ { canonical: "terraform", aliases: [], category: "tool" },
1045
+ { canonical: "ansible", aliases: [], category: "tool" },
1046
+ { canonical: "jenkins", aliases: [], category: "tool" },
1047
+ { canonical: "git", aliases: ["github", "gitlab"], category: "tool" },
1048
+ { canonical: "jira", aliases: [], category: "tool" },
1049
+ { canonical: "confluence", aliases: [], category: "tool" },
1050
+ { canonical: "pytorch", aliases: ["torch"], category: "tool" },
1051
+ { canonical: "tensorflow", aliases: ["tf"], category: "tool" },
1052
+ { canonical: "scikit-learn", aliases: ["sklearn"], category: "tool" },
1053
+ { canonical: "pandas", aliases: [], category: "tool" },
1054
+ { canonical: "numpy", aliases: [], category: "tool" },
1055
+ { canonical: "fastapi", aliases: [], category: "tool" },
1056
+ { canonical: "flask", aliases: [], category: "tool" },
1057
+ { canonical: "django", aliases: [], category: "tool" },
1058
+ { canonical: "kafka", aliases: [], category: "tool" },
1059
+ { canonical: "redis", aliases: [], category: "tool" },
1060
+ { canonical: "elasticsearch", aliases: ["elastic"], category: "tool" },
1061
+ { canonical: "spark", aliases: ["apache spark"], category: "tool" },
1062
+ { canonical: "tableau", aliases: [], category: "tool" },
1063
+ { canonical: "power bi", aliases: ["powerbi"], category: "tool" },
1064
+ { canonical: "excel", aliases: ["microsoft excel", "ms excel"], category: "tool" },
1065
+ { canonical: "salesforce", aliases: [], category: "tool" },
1066
+ { canonical: "hubspot", aliases: [], category: "tool" },
1067
+ { canonical: "sap", aliases: [], category: "tool" },
1068
+ { canonical: "quickbooks", aliases: [], category: "tool" },
1069
+ { canonical: "workday", aliases: [], category: "tool" },
1070
+ { canonical: "zendesk", aliases: [], category: "tool" },
1071
+ { canonical: "servicenow", aliases: [], category: "tool" },
1072
+ { canonical: "figma", aliases: [], category: "tool" },
1073
+ { canonical: "photoshop", aliases: ["adobe photoshop"], category: "tool" },
1074
+ { canonical: "illustrator", aliases: ["adobe illustrator"], category: "tool" },
1075
+ { canonical: "autocad", aliases: [], category: "tool" },
1076
+ // engineering concepts
1077
+ { canonical: "accessibility", aliases: ["a11y"], category: "concept" },
1078
+ { canonical: "frontend", aliases: ["front-end"], category: "concept" },
1079
+ { canonical: "backend", aliases: ["back-end"], category: "concept" },
1080
+ { canonical: "security", aliases: ["cybersecurity"], category: "concept" },
1081
+ { canonical: "testing", aliases: ["unittest", "pytest"], category: "concept" },
1082
+ { canonical: "microservices", aliases: [], category: "concept" },
1083
+ { canonical: "agile", aliases: ["scrum"], category: "concept" },
1084
+ { canonical: "kanban", aliases: [], category: "concept" },
1085
+ { canonical: "blockchain", aliases: [], category: "concept" },
1086
+ { canonical: "devops", aliases: [], category: "concept" },
1087
+ { canonical: "ci/cd", aliases: ["continuous integration", "continuous deployment"], category: "concept" },
1088
+ { canonical: "rest api", aliases: ["restful api", "rest apis"], category: "concept" },
1089
+ { canonical: "design patterns", aliases: [], category: "concept" },
1090
+ { canonical: "data structures", aliases: [], category: "concept" },
1091
+ { canonical: "algorithms", aliases: [], category: "concept" },
1092
+ { canonical: "cloud computing", aliases: [], category: "concept" },
1093
+ { canonical: "system design", aliases: [], category: "concept" },
1094
+ { canonical: "tdd", aliases: ["test driven development", "test-driven development"], category: "concept" },
1095
+ { canonical: "ux design", aliases: ["user experience"], category: "concept" },
1096
+ { canonical: "ui design", aliases: ["user interface design"], category: "concept" },
1097
+ { canonical: "project management", aliases: [], category: "concept" },
1098
+ { canonical: "change management", aliases: [], category: "concept" },
1099
+ { canonical: "risk management", aliases: [], category: "concept" },
1100
+ { canonical: "quality assurance", aliases: ["qa"], category: "concept" },
1101
+ // product / data domain
1102
+ { canonical: "roadmap", aliases: [], category: "domain" },
1103
+ { canonical: "stakeholder management", aliases: [], category: "domain" },
1104
+ { canonical: "prioritization", aliases: [], category: "domain" },
1105
+ { canonical: "a/b testing", aliases: ["ab testing"], category: "domain" },
1106
+ { canonical: "analytics", aliases: [], category: "domain" },
1107
+ { canonical: "statistics", aliases: ["stats"], category: "domain" },
1108
+ { canonical: "data visualization", aliases: [], category: "domain" },
1109
+ // finance / accounting domain
1110
+ { canonical: "financial analysis", aliases: [], category: "domain" },
1111
+ { canonical: "budgeting", aliases: [], category: "domain" },
1112
+ { canonical: "forecasting", aliases: [], category: "domain" },
1113
+ { canonical: "bookkeeping", aliases: [], category: "domain" },
1114
+ { canonical: "accounts payable", aliases: ["ap"], category: "domain" },
1115
+ { canonical: "accounts receivable", aliases: ["ar"], category: "domain" },
1116
+ { canonical: "payroll", aliases: [], category: "domain" },
1117
+ { canonical: "auditing", aliases: ["audit"], category: "domain" },
1118
+ { canonical: "tax preparation", aliases: [], category: "domain" },
1119
+ { canonical: "gaap", aliases: [], category: "domain" },
1120
+ // sales / account management domain
1121
+ { canonical: "lead generation", aliases: [], category: "domain" },
1122
+ { canonical: "account management", aliases: [], category: "domain" },
1123
+ { canonical: "crm", aliases: ["customer relationship management"], category: "domain" },
1124
+ { canonical: "sales pipeline", aliases: [], category: "domain" },
1125
+ { canonical: "cold calling", aliases: [], category: "domain" },
1126
+ { canonical: "upselling", aliases: ["cross-selling"], category: "domain" },
1127
+ { canonical: "customer retention", aliases: [], category: "domain" },
1128
+ // human resources domain
1129
+ { canonical: "recruiting", aliases: ["talent acquisition"], category: "domain" },
1130
+ { canonical: "onboarding", aliases: [], category: "domain" },
1131
+ { canonical: "employee relations", aliases: [], category: "domain" },
1132
+ { canonical: "benefits administration", aliases: [], category: "domain" },
1133
+ { canonical: "performance management", aliases: [], category: "domain" },
1134
+ // healthcare domain
1135
+ { canonical: "patient care", aliases: [], category: "domain" },
1136
+ { canonical: "clinical documentation", aliases: [], category: "domain" },
1137
+ { canonical: "hipaa", aliases: [], category: "domain" },
1138
+ { canonical: "electronic health records", aliases: ["ehr", "emr"], category: "domain" },
1139
+ { canonical: "medical billing", aliases: [], category: "domain" },
1140
+ // legal domain
1141
+ { canonical: "contract review", aliases: [], category: "domain" },
1142
+ { canonical: "legal research", aliases: [], category: "domain" },
1143
+ { canonical: "litigation", aliases: [], category: "domain" },
1144
+ { canonical: "regulatory compliance", aliases: ["compliance"], category: "domain" },
1145
+ { canonical: "due diligence", aliases: [], category: "domain" },
1146
+ // education domain
1147
+ { canonical: "curriculum development", aliases: [], category: "domain" },
1148
+ { canonical: "lesson planning", aliases: [], category: "domain" },
1149
+ { canonical: "classroom management", aliases: [], category: "domain" },
1150
+ { canonical: "instructional design", aliases: [], category: "domain" },
1151
+ // operations / supply chain domain
1152
+ { canonical: "supply chain management", aliases: ["supply chain"], category: "domain" },
1153
+ { canonical: "inventory management", aliases: [], category: "domain" },
1154
+ { canonical: "procurement", aliases: [], category: "domain" },
1155
+ { canonical: "vendor management", aliases: [], category: "domain" },
1156
+ { canonical: "logistics", aliases: [], category: "domain" },
1157
+ // customer service domain
1158
+ { canonical: "customer support", aliases: ["customer service"], category: "domain" },
1159
+ { canonical: "technical support", aliases: [], category: "domain" },
1160
+ { canonical: "conflict resolution", aliases: [], category: "domain" },
1161
+ // soft skills
1162
+ { canonical: "communication", aliases: [], category: "soft" },
1163
+ { canonical: "leadership", aliases: [], category: "soft" },
1164
+ { canonical: "teamwork", aliases: ["collaboration"], category: "soft" },
1165
+ { canonical: "problem solving", aliases: ["problem-solving"], category: "soft" },
1166
+ { canonical: "adaptability", aliases: ["flexibility"], category: "soft" },
1167
+ { canonical: "time management", aliases: [], category: "soft" },
1168
+ { canonical: "critical thinking", aliases: [], category: "soft" },
1169
+ { canonical: "creativity", aliases: [], category: "soft" },
1170
+ { canonical: "attention to detail", aliases: [], category: "soft" },
1171
+ { canonical: "decision making", aliases: ["decision-making"], category: "soft" },
1172
+ { canonical: "emotional intelligence", aliases: [], category: "soft" },
1173
+ { canonical: "negotiation", aliases: [], category: "soft" },
1174
+ { canonical: "organization", aliases: ["organizational skills"], category: "soft" },
1175
+ { canonical: "public speaking", aliases: ["presentation skills"], category: "soft" },
1176
+ { canonical: "mentoring", aliases: ["coaching"], category: "soft" },
1177
+ { canonical: "interpersonal skills", aliases: [], category: "soft" },
1178
+ { canonical: "work ethic", aliases: [], category: "soft" },
1179
+ // marketing
1180
+ { canonical: "seo", aliases: ["search engine optimization"], category: "marketing" },
1181
+ { canonical: "branding", aliases: ["brand strategy"], category: "marketing" },
1182
+ { canonical: "campaign management", aliases: [], category: "marketing" },
1183
+ { canonical: "content marketing", aliases: [], category: "marketing" },
1184
+ { canonical: "social media marketing", aliases: ["social media"], category: "marketing" },
1185
+ { canonical: "email marketing", aliases: [], category: "marketing" },
1186
+ { canonical: "digital marketing", aliases: [], category: "marketing" },
1187
+ { canonical: "copywriting", aliases: [], category: "marketing" },
1188
+ { canonical: "market research", aliases: [], category: "marketing" },
1189
+ { canonical: "ppc", aliases: ["pay-per-click", "google ads"], category: "marketing" },
1190
+ { canonical: "conversion rate optimization", aliases: ["cro"], category: "marketing" },
1191
+ { canonical: "public relations", aliases: ["pr"], category: "marketing" }
1192
+ ];
1193
+ var defaultSkillAliases = deriveSkillAliases(defaultKeywordRegistry);
798
1194
  var softwareEngineerProfile = {
799
1195
  name: "software-engineer",
800
1196
  mandatorySkills: ["javascript", "typescript", "react", "node"],
@@ -864,9 +1260,12 @@ function resolveConfig(config = {}) {
864
1260
  keywords: config.weights?.keywords ?? DEFAULT_WEIGHTS.keywords,
865
1261
  education: config.weights?.education ?? DEFAULT_WEIGHTS.education
866
1262
  };
1263
+ const keywordRegistry = mergeKeywordRegistries(defaultKeywordRegistry, config.keywordRegistry ?? []);
867
1264
  const resolved = {
868
1265
  weights: normalizeWeights(weights),
869
- skillAliases: { ...defaultSkillAliases, ...config.skillAliases ?? {} },
1266
+ skillAliases: { ...deriveSkillAliases(keywordRegistry), ...config.skillAliases ?? {} },
1267
+ keywordRegistry,
1268
+ categoryIndex: buildCategoryIndex(keywordRegistry),
870
1269
  profile: config.profile ?? softwareEngineerProfile,
871
1270
  rules: config.rules ?? [],
872
1271
  keywordDensity: config.keywordDensity ?? DEFAULT_KEYWORD_DENSITY,
@@ -886,6 +1285,18 @@ function formatList(values, max = 6) {
886
1285
  const trimmed = uniqueValues.slice(0, max);
887
1286
  return trimmed.join(", ") + (uniqueValues.length > max ? "..." : "");
888
1287
  }
1288
+ function buildAliasReplacementSuggestions(resume, job, config) {
1289
+ const jobKeywordSet = new Set(job.keywords.map((k) => normalizeSkill(k, config.skillAliases)));
1290
+ const replacements = [];
1291
+ for (const token of unique(tokenize(resume.normalizedText))) {
1292
+ const canonical = normalizeSkill(token, config.skillAliases);
1293
+ const jdSurface = job.keywordSurfaceForms[canonical];
1294
+ if (jdSurface && jobKeywordSet.has(canonical) && jdSurface.toLowerCase() !== token.toLowerCase()) {
1295
+ replacements.push(`Replace "${token}" with "${jdSurface}" to match the job description's wording.`);
1296
+ }
1297
+ }
1298
+ return unique(replacements).slice(0, 5);
1299
+ }
889
1300
  var SuggestionEngine = class {
890
1301
  generate(input) {
891
1302
  const suggestions = [];
@@ -900,6 +1311,7 @@ var SuggestionEngine = class {
900
1311
  `Incorporate job-specific keywords: ${formatList(input.score.missingKeywords)}`
901
1312
  );
902
1313
  }
1314
+ suggestions.push(...buildAliasReplacementSuggestions(input.resume, input.job, input.config));
903
1315
  if (input.score.overusedKeywords.length > 0) {
904
1316
  suggestions.push(
905
1317
  `Avoid keyword stuffing for: ${formatList(input.score.overusedKeywords)}`
@@ -920,6 +1332,26 @@ var SuggestionEngine = class {
920
1332
  "Strengthen bullet points with impact verbs (led, built, improved, delivered)."
921
1333
  );
922
1334
  }
1335
+ if (input.resume.weakVerbs.length > 0) {
1336
+ suggestions.push(
1337
+ `Replace weak verbs (${formatList(input.resume.weakVerbs)}) with stronger ones (e.g. led, built, optimized).`
1338
+ );
1339
+ }
1340
+ if (input.score.missingLanguages.length > 0) {
1341
+ const formatted = input.score.missingLanguages.map((l) => l.level ? `${l.name} (${l.level})` : l.name).join(", ");
1342
+ suggestions.push(`Mention your proficiency in: ${formatted}`);
1343
+ }
1344
+ const weakAchievement = input.resume.achievements.find((a) => a.strength === "weak");
1345
+ if (weakAchievement) {
1346
+ suggestions.push(
1347
+ `Strengthen "${weakAchievement.text}" \u2014 add scope/metrics, e.g. "Built and maintained scalable services handling 500k+ requests/day."`
1348
+ );
1349
+ }
1350
+ if (input.resume.detectedSections.length < 2 && input.resume.raw.trim().length > 300) {
1351
+ suggestions.push(
1352
+ "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."
1353
+ );
1354
+ }
923
1355
  return { suggestions, warnings };
924
1356
  }
925
1357
  };
@@ -1557,7 +1989,8 @@ function analyzeResume(input) {
1557
1989
  resume: parsedResume,
1558
1990
  job: parsedJob,
1559
1991
  score: scoring,
1560
- ruleWarnings: ruleResult.warnings
1992
+ ruleWarnings: ruleResult.warnings,
1993
+ config: resolvedConfig
1561
1994
  });
1562
1995
  let suggestions = suggestionResult.suggestions;
1563
1996
  const llmWarnings = [];
@@ -1577,6 +2010,11 @@ function analyzeResume(input) {
1577
2010
  matchedKeywords: scoring.matchedKeywords,
1578
2011
  missingKeywords: scoring.missingKeywords,
1579
2012
  overusedKeywords: scoring.overusedKeywords,
2013
+ keywordsByCategory: scoring.keywordsByCategory,
2014
+ keywordWeights: scoring.keywordWeights,
2015
+ achievementStrength: scoring.achievementStrength,
2016
+ matchedLanguages: scoring.matchedLanguages,
2017
+ missingLanguages: scoring.missingLanguages,
1580
2018
  experienceGap: scoring.experienceGap,
1581
2019
  detectedSections: parsedResume.detectedSections,
1582
2020
  parsedExperienceYears: parsedResume.totalExperienceYears,
@@ -1619,7 +2057,8 @@ async function analyzeResumeAsync(input) {
1619
2057
  resume: parsedResume,
1620
2058
  job: parsedJob,
1621
2059
  score: scoring,
1622
- ruleWarnings: ruleResult.warnings
2060
+ ruleWarnings: ruleResult.warnings,
2061
+ config: resolvedConfig
1623
2062
  });
1624
2063
  let suggestions = suggestionResult.suggestions;
1625
2064
  const llmWarnings = [];
@@ -1642,6 +2081,11 @@ async function analyzeResumeAsync(input) {
1642
2081
  matchedKeywords: scoring.matchedKeywords,
1643
2082
  missingKeywords: scoring.missingKeywords,
1644
2083
  overusedKeywords: scoring.overusedKeywords,
2084
+ keywordsByCategory: scoring.keywordsByCategory,
2085
+ keywordWeights: scoring.keywordWeights,
2086
+ achievementStrength: scoring.achievementStrength,
2087
+ matchedLanguages: scoring.matchedLanguages,
2088
+ missingLanguages: scoring.missingLanguages,
1645
2089
  experienceGap: scoring.experienceGap,
1646
2090
  detectedSections: parsedResume.detectedSections,
1647
2091
  parsedExperienceYears: parsedResume.totalExperienceYears,
@@ -1697,10 +2141,11 @@ exports.adaptSuggestionEnhancementResponse = adaptSuggestionEnhancementResponse;
1697
2141
  exports.analyzeResume = analyzeResume;
1698
2142
  exports.analyzeResumeAsync = analyzeResumeAsync;
1699
2143
  exports.createPrompt = createPrompt;
2144
+ exports.defaultKeywordRegistry = defaultKeywordRegistry;
1700
2145
  exports.defaultProfiles = defaultProfiles;
1701
2146
  exports.defaultSkillAliases = defaultSkillAliases;
1702
2147
  exports.safeExtractArray = safeExtractArray;
1703
2148
  exports.safeExtractNumber = safeExtractNumber;
1704
2149
  exports.safeExtractString = safeExtractString;
1705
- //# sourceMappingURL=index.js.map
1706
- //# sourceMappingURL=index.js.map
2150
+ //# sourceMappingURL=index.cjs.map
2151
+ //# sourceMappingURL=index.cjs.map