@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.
package/dist/index.mjs CHANGED
@@ -1,202 +1,113 @@
1
- // src/utils/text.ts
2
- var STOP_WORDS = /* @__PURE__ */ new Set([
3
- // articles / prepositions / conjunctions
4
- "the",
5
- "and",
6
- "or",
7
- "a",
8
- "an",
9
- "of",
10
- "for",
11
- "to",
12
- "with",
13
- "in",
14
- "on",
15
- "at",
16
- "by",
17
- "from",
18
- "as",
19
- "into",
20
- "onto",
21
- "upon",
22
- "via",
23
- "per",
24
- "plus",
25
- // verbs / modals
26
- "is",
27
- "are",
28
- "be",
29
- "was",
30
- "were",
31
- "will",
32
- "can",
33
- "should",
34
- "must",
35
- "have",
36
- "has",
37
- "had",
38
- "do",
39
- "does",
40
- "did",
41
- "get",
42
- "give",
43
- "go",
44
- "use",
45
- "see",
46
- "help",
47
- "work",
48
- "build",
49
- "show",
50
- "need",
51
- "want",
52
- "make",
53
- "let",
54
- // pronouns / determiners
55
- "it",
56
- "its",
57
- "this",
58
- "that",
59
- "these",
60
- "those",
61
- "we",
62
- "our",
63
- "you",
64
- "your",
65
- "they",
66
- "their",
67
- "us",
68
- "who",
69
- "what",
70
- "which",
71
- "how",
72
- // common English fillers that leak into JDs
73
- "no",
74
- "not",
75
- "all",
76
- "any",
77
- "also",
78
- "more",
79
- "well",
80
- "very",
81
- "highly",
82
- "across",
83
- "over",
84
- "under",
85
- "within",
86
- "about",
87
- "out",
88
- "up",
89
- "down",
90
- "new",
91
- "if",
92
- "so",
93
- "such",
94
- "both",
95
- "each",
96
- "one",
97
- "many",
98
- "only",
99
- // JD/HR boilerplate — never skills
100
- "years",
101
- "year",
102
- "experience",
103
- "required",
104
- "requirement",
105
- "requirements",
106
- "preferred",
107
- "role",
108
- "degree",
109
- "practices",
110
- "best",
111
- "skills",
112
- "team",
113
- "field",
114
- "related",
115
- "relevant",
116
- "desired",
117
- "strong",
118
- "solid",
119
- "good",
120
- "first",
121
- "based",
122
- "day",
123
- "week",
124
- "month",
125
- "time",
126
- "fast",
127
- "open",
128
- "dynamic"
129
- ]);
130
- function normalizeWhitespace(text) {
131
- return text.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim();
132
- }
133
- function normalizeForComparison(text) {
134
- return normalizeWhitespace(text).normalize("NFKC").toLowerCase();
135
- }
136
- function splitLines(text) {
137
- return text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean);
138
- }
139
- var TECH_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/g;
140
- function tokenize(text) {
141
- const normalized = normalizeForComparison(text);
142
- return (normalized.match(TECH_TOKEN_RE) ?? []).filter(
143
- (t) => /[a-z]/.test(t) && !STOP_WORDS.has(t)
144
- );
1
+ import { clamp, mergeKeywordRegistries, defaultKeywordRegistry, softwareEngineerProfile, buildCategoryIndex, deriveSkillAliases, normalizeWhitespace, splitLines, normalizeSkills, tokenize, unique, containsTableLikeStructure, normalizeForComparison, STOP_WORDS, normalizeSkill, countFrequencies, escapeRegExp } from './chunk-ZJ5E4H7Z.mjs';
2
+ export { defaultKeywordRegistry, defaultProfiles, defaultSkillAliases } from './chunk-ZJ5E4H7Z.mjs';
3
+
4
+ // src/utils/languages.ts
5
+ var KNOWN_LANGUAGES = [
6
+ "english",
7
+ "spanish",
8
+ "french",
9
+ "german",
10
+ "italian",
11
+ "portuguese",
12
+ "dutch",
13
+ "russian",
14
+ "mandarin",
15
+ "chinese",
16
+ "cantonese",
17
+ "japanese",
18
+ "korean",
19
+ "arabic",
20
+ "hindi",
21
+ "polish",
22
+ "turkish",
23
+ "vietnamese",
24
+ "swedish",
25
+ "norwegian",
26
+ "danish",
27
+ "finnish",
28
+ "greek",
29
+ "hebrew",
30
+ "thai",
31
+ "indonesian",
32
+ "ukrainian",
33
+ "czech"
34
+ ];
35
+ var LANGUAGE_ALIASES = {
36
+ mandarin: "chinese",
37
+ cantonese: "chinese"
38
+ };
39
+ var LEVEL_RANK = {
40
+ a1: 1,
41
+ a2: 2,
42
+ b1: 3,
43
+ b2: 4,
44
+ c1: 5,
45
+ c2: 6,
46
+ basic: 1,
47
+ elementary: 1,
48
+ limited: 2,
49
+ conversational: 3,
50
+ intermediate: 3,
51
+ professional: 4,
52
+ "upper intermediate": 4,
53
+ advanced: 4,
54
+ fluent: 5,
55
+ native: 6,
56
+ "native speaker": 6,
57
+ bilingual: 6
58
+ };
59
+ var LANGUAGE_GROUP = KNOWN_LANGUAGES.join("|");
60
+ var LEVEL_GROUP = Object.keys(LEVEL_RANK).sort((a, b) => b.length - a.length).map((l) => l.replace(/\s+/g, "\\s+")).join("|");
61
+ var LANGUAGE_LEVEL_RE = new RegExp(
62
+ `\\b(${LANGUAGE_GROUP})\\b(?:\\s*[\\(:\\-]?\\s*(${LEVEL_GROUP}|[abc][12]))?`,
63
+ "gi"
64
+ );
65
+ var LEVEL_BEFORE_LANGUAGE_RE = new RegExp(`\\b(${LEVEL_GROUP})\\s+(?:in\\s+)?(${LANGUAGE_GROUP})\\b`, "gi");
66
+ function canonicalLanguage(name) {
67
+ const lower = name.toLowerCase();
68
+ return LANGUAGE_ALIASES[lower] ?? lower;
145
69
  }
146
- function escapeRegExp(input) {
147
- return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
70
+ function toParsedLanguage(name, level) {
71
+ const normalizedLevel = level?.toLowerCase().replace(/\s+/g, " ");
72
+ return {
73
+ name: canonicalLanguage(name),
74
+ level: normalizedLevel,
75
+ levelRank: normalizedLevel ? LEVEL_RANK[normalizedLevel] : void 0
76
+ };
148
77
  }
149
- function unique(values) {
150
- const seen = /* @__PURE__ */ new Set();
151
- const output = [];
152
- for (const value of values) {
153
- const lower = value.toLowerCase();
154
- if (!seen.has(lower)) {
155
- seen.add(lower);
156
- output.push(value);
78
+ function parseLanguageMentions(text) {
79
+ const found = /* @__PURE__ */ new Map();
80
+ for (const match of text.matchAll(LANGUAGE_LEVEL_RE)) {
81
+ const parsed = toParsedLanguage(match[1], match[2]);
82
+ const existing = found.get(parsed.name);
83
+ if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
84
+ found.set(parsed.name, parsed);
157
85
  }
158
86
  }
159
- return output;
160
- }
161
- function clamp(value, min, max) {
162
- return Math.min(Math.max(value, min), max);
163
- }
164
- function countFrequencies(values) {
165
- const counts = {};
166
- for (const value of values) {
167
- counts[value] = (counts[value] ?? 0) + 1;
168
- }
169
- return counts;
170
- }
171
- function containsTableLikeStructure(text) {
172
- const lines = splitLines(text);
173
- let tableLines = 0;
174
- for (const line of lines) {
175
- const hasPipeColumns = line.includes("|") && line.split("|").length >= 3;
176
- const hasTabColumns = /\t.+\t/.test(line);
177
- const hasAlignedSpaces = /( {3,})(\S+)( {3,}\S+)/.test(line);
178
- if (hasPipeColumns || hasTabColumns || hasAlignedSpaces) {
179
- tableLines += 1;
87
+ for (const match of text.matchAll(LEVEL_BEFORE_LANGUAGE_RE)) {
88
+ const parsed = toParsedLanguage(match[2], match[1]);
89
+ const existing = found.get(parsed.name);
90
+ if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
91
+ found.set(parsed.name, parsed);
180
92
  }
181
93
  }
182
- return tableLines >= 2;
94
+ return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
183
95
  }
184
-
185
- // src/utils/skills.ts
186
- function normalizeSkill(skill, aliases) {
187
- const normalized = skill.trim().toLowerCase();
188
- for (const [canonical, aliasList] of Object.entries(aliases)) {
189
- if (canonical.toLowerCase() === normalized) {
190
- return canonical.toLowerCase();
191
- }
192
- if (aliasList.some((alias) => alias.toLowerCase() === normalized)) {
193
- return canonical.toLowerCase();
96
+ function diffLanguages(resumeLanguages, requiredLanguages) {
97
+ const byName = new Map(resumeLanguages.map((l) => [l.name, l]));
98
+ const matched = [];
99
+ const missing = [];
100
+ for (const required of requiredLanguages) {
101
+ const have = byName.get(required.name);
102
+ const requiredRank = required.levelRank ?? 0;
103
+ const haveRank = have?.levelRank ?? 0;
104
+ if (have && haveRank >= requiredRank) {
105
+ matched.push(required);
106
+ } else {
107
+ missing.push(required);
194
108
  }
195
109
  }
196
- return normalized;
197
- }
198
- function normalizeSkills(skills, aliases) {
199
- return unique(skills.map((skill) => normalizeSkill(skill, aliases)));
110
+ return { matched, missing };
200
111
  }
201
112
 
202
113
  // src/core/parser/jd.parser.ts
@@ -237,6 +148,18 @@ function extractMinExperience(text) {
237
148
  }
238
149
  return void 0;
239
150
  }
151
+ var SURFACE_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/gi;
152
+ function collectKeywordSurfaceForms(rawText, aliases) {
153
+ const matches = rawText.match(SURFACE_TOKEN_RE) ?? [];
154
+ const surfaceForms = {};
155
+ for (const match of matches) {
156
+ const canonical = normalizeSkill(match, aliases);
157
+ if (!(canonical in surfaceForms)) {
158
+ surfaceForms[canonical] = match;
159
+ }
160
+ }
161
+ return surfaceForms;
162
+ }
240
163
  function extractEducationRequirements(text) {
241
164
  const found = /* @__PURE__ */ new Set();
242
165
  for (const [pattern, canonical] of DEGREE_VARIANTS) {
@@ -275,7 +198,11 @@ function parseJobDescription(jobDescription, config) {
275
198
  roleKeywords,
276
199
  keywords,
277
200
  minExperienceYears: extractMinExperience(jobDescription),
278
- educationRequirements: extractEducationRequirements(jobDescription)
201
+ educationRequirements: extractEducationRequirements(jobDescription),
202
+ 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)
279
206
  };
280
207
  }
281
208
 
@@ -408,7 +335,7 @@ var SECTION_ALIASES = {
408
335
  projects: ["projects", "portfolio"],
409
336
  certifications: ["certifications", "licenses"]
410
337
  };
411
- var ACTION_VERBS = [
338
+ var STRONG_VERBS = [
412
339
  "led",
413
340
  "managed",
414
341
  "built",
@@ -429,6 +356,21 @@ var ACTION_VERBS = [
429
356
  "reduced",
430
357
  "increased"
431
358
  ];
359
+ var WEAK_VERBS = ["worked", "helped", "performed", "responsible", "assisted", "participated", "involved"];
360
+ var METRIC_RE = /\d|%|\$|\bk\+|\bm\+/i;
361
+ function classifyAchievement(line) {
362
+ const lower = line.toLowerCase();
363
+ const hasMetric = METRIC_RE.test(line);
364
+ const hasStrongVerb = STRONG_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
365
+ const hasWeakVerb = WEAK_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
366
+ if (hasStrongVerb && hasMetric) {
367
+ return { text: line, strength: "strong", reason: "strong verb + quantified impact" };
368
+ }
369
+ if (hasWeakVerb) {
370
+ return { text: line, strength: "weak", reason: "weak verb" };
371
+ }
372
+ return { text: line, strength: "weak", reason: "no quantified impact" };
373
+ }
432
374
  function detectSection(line) {
433
375
  const normalized = line.trim().toLowerCase();
434
376
  for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
@@ -474,16 +416,20 @@ function parseSkills(sectionContent, aliases) {
474
416
  }
475
417
  function parseActionVerbs(text) {
476
418
  const words = tokenize(text);
477
- return ACTION_VERBS.filter((verb) => words.includes(verb));
419
+ return {
420
+ strong: STRONG_VERBS.filter((verb) => words.includes(verb)),
421
+ weak: WEAK_VERBS.filter((verb) => words.includes(verb))
422
+ };
478
423
  }
479
424
  function parseExperience(sectionContent, referenceDate) {
480
425
  if (!sectionContent) {
481
- return { entries: [], rangesInMonths: [], jobTitles: [] };
426
+ return { entries: [], rangesInMonths: [], jobTitles: [], achievements: [] };
482
427
  }
483
428
  const lines = splitLines(sectionContent);
484
429
  const entries = [];
485
430
  const rangesInMonths = [];
486
431
  const jobTitles = [];
432
+ const achievements = [];
487
433
  for (const line of lines) {
488
434
  const range = parseDateRange(line, referenceDate);
489
435
  if (range) {
@@ -504,14 +450,16 @@ function parseExperience(sectionContent, referenceDate) {
504
450
  jobTitles.push(title.toLowerCase());
505
451
  const entry = { title, description: line };
506
452
  entries.push(entry);
453
+ achievements.push(classifyAchievement(line));
507
454
  continue;
508
455
  }
509
456
  if (entries.length > 0) {
510
457
  const current = entries[entries.length - 1];
511
458
  current.description = [current.description, line].filter(Boolean).join(" ").trim();
512
459
  }
460
+ achievements.push(classifyAchievement(line));
513
461
  }
514
- return { entries, rangesInMonths, jobTitles: unique(jobTitles) };
462
+ return { entries, rangesInMonths, jobTitles: unique(jobTitles), achievements };
515
463
  }
516
464
  function parseEducation(sectionContent) {
517
465
  if (!sectionContent) return [];
@@ -539,6 +487,16 @@ function parseResume(resumeText, config) {
539
487
  }
540
488
  const requiredSections = ["summary", "experience", "skills", "education"];
541
489
  const warnings = [];
490
+ const lineCount = splitLines(resumeText).length;
491
+ if (resumeText.trim().length < 100) {
492
+ warnings.push(
493
+ "Almost no text was extracted \u2014 the resume may be a scanned/image PDF. Upload a text-based PDF or paste the text directly."
494
+ );
495
+ } else if (lineCount <= 2) {
496
+ warnings.push(
497
+ "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."
498
+ );
499
+ }
542
500
  for (const section of requiredSections) {
543
501
  if (!detected.includes(section)) {
544
502
  warnings.push(`${section} section not detected`);
@@ -551,11 +509,14 @@ function parseResume(resumeText, config) {
551
509
  sectionContent: sections,
552
510
  skills,
553
511
  jobTitles: experienceData.jobTitles,
554
- actionVerbs,
512
+ actionVerbs: actionVerbs.strong,
513
+ weakVerbs: actionVerbs.weak,
514
+ achievements: experienceData.achievements,
555
515
  educationEntries,
556
516
  experience: experienceData.entries,
557
517
  totalExperienceYears,
558
518
  keywords: collectKeywords(normalizedText),
519
+ languages: parseLanguageMentions(resumeText),
559
520
  warnings
560
521
  };
561
522
  }
@@ -637,6 +598,14 @@ var REQUIRED_SKILL_WEIGHT = 0.7;
637
598
  var OPTIONAL_SKILL_WEIGHT = 0.3;
638
599
  var EXPERIENCE_YEARS_WEIGHT = 0.75;
639
600
  var EXPERIENCE_ROLE_WEIGHT = 0.25;
601
+ var ALL_CATEGORIES = ["technical", "tool", "concept", "soft", "marketing", "domain"];
602
+ function emptyCategoryBuckets() {
603
+ const buckets = {};
604
+ for (const category of ALL_CATEGORIES) {
605
+ buckets[category] = { matched: [], missing: [] };
606
+ }
607
+ return buckets;
608
+ }
640
609
  function scoreSkills(resume, job, config) {
641
610
  const profileRequired = config.profile?.mandatorySkills ?? [];
642
611
  const profileOptional = config.profile?.optionalSkills ?? [];
@@ -675,32 +644,74 @@ function scoreExperience(resume, job, config) {
675
644
  const missingYears = Math.max(requiredYears - resume.totalExperienceYears, 0);
676
645
  return { score, missingYears: Number(missingYears.toFixed(2)) };
677
646
  }
647
+ function keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies) {
648
+ const base = requiredSet.has(keyword) ? 3 : preferredSet.has(keyword) ? 2 : 1;
649
+ const freqBonus = Math.min((jdFrequencies[keyword] ?? 1) - 1, 3) * 0.25;
650
+ return base + freqBonus;
651
+ }
678
652
  function scoreKeywords(resume, job, config) {
679
653
  const jobKeywordSet = new Set(
680
654
  job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
681
655
  );
682
656
  if (jobKeywordSet.size === 0) {
683
- return { score: 100, matchedKeywords: [], missingKeywords: [], overusedKeywords: [] };
657
+ return {
658
+ score: 100,
659
+ matchedKeywords: [],
660
+ missingKeywords: [],
661
+ overusedKeywords: [],
662
+ keywordsByCategory: emptyCategoryBuckets(),
663
+ keywordWeights: []
664
+ };
684
665
  }
685
666
  const resumeTokens = tokenize(resume.normalizedText).map(
686
667
  (t) => normalizeSkill(t, config.skillAliases)
687
668
  );
688
669
  const resumeTokenSet = new Set(resumeTokens);
670
+ const resumeFrequencies = countFrequencies(resumeTokens);
671
+ const requiredSet = new Set(job.requiredSkills);
672
+ const preferredSet = new Set(job.preferredSkills);
673
+ const jdFrequencies = countFrequencies(
674
+ tokenize(job.normalizedText).map((t) => normalizeSkill(t, config.skillAliases))
675
+ );
676
+ const weightOf = (keyword) => keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies);
689
677
  const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
690
678
  const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
691
- const coverage = matchedKeywords.length / jobKeywordSet.size;
692
- const score = clamp(coverage * 100, 0, 100);
693
- const frequencies = countFrequencies(resumeTokens);
679
+ const totalWeight = [...jobKeywordSet].reduce((sum, keyword) => sum + weightOf(keyword), 0);
680
+ const matchedWeight = matchedKeywords.reduce((sum, keyword) => sum + weightOf(keyword), 0);
681
+ const score = clamp(matchedWeight / totalWeight * 100, 0, 100);
694
682
  const totalTokens = resumeTokens.length || 1;
695
683
  const overusedKeywords = matchedKeywords.filter((keyword) => {
696
- const density = (frequencies[keyword] ?? 0) / totalTokens;
684
+ const density = (resumeFrequencies[keyword] ?? 0) / totalTokens;
697
685
  return density > config.keywordDensity.max;
698
686
  });
687
+ const keywordsByCategory = emptyCategoryBuckets();
688
+ for (const keyword of matchedKeywords) {
689
+ keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].matched.push(keyword);
690
+ }
691
+ for (const keyword of missingKeywords) {
692
+ keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].missing.push(keyword);
693
+ }
694
+ for (const bucket of Object.values(keywordsByCategory)) {
695
+ bucket.matched.sort();
696
+ bucket.missing.sort();
697
+ }
698
+ const keywordWeights = [...jobKeywordSet].map((term) => {
699
+ const weight = Number(weightOf(term).toFixed(2));
700
+ return {
701
+ term,
702
+ category: config.categoryIndex.get(term) ?? "technical",
703
+ jdWeight: weight,
704
+ resumeWeight: resumeFrequencies[term] ?? 0,
705
+ importance: weight
706
+ };
707
+ }).sort((a, b) => a.term.localeCompare(b.term));
699
708
  return {
700
709
  score,
701
710
  matchedKeywords: unique(matchedKeywords).sort(),
702
711
  missingKeywords: unique(missingKeywords).sort(),
703
- overusedKeywords: unique(overusedKeywords).sort()
712
+ overusedKeywords: unique(overusedKeywords).sort(),
713
+ keywordsByCategory,
714
+ keywordWeights
704
715
  };
705
716
  }
706
717
  function scoreEducation(resume, job) {
@@ -729,6 +740,14 @@ function calculateScore(resume, job, config) {
729
740
  education: educationScore
730
741
  };
731
742
  const weightedScore = breakdown.skills * config.weights.skills + breakdown.experience * config.weights.experience + breakdown.keywords * config.weights.keywords + breakdown.education * config.weights.education;
743
+ const achievementStrength = {
744
+ strong: resume.achievements.filter((a) => a.strength === "strong").length,
745
+ weak: resume.achievements.filter((a) => a.strength === "weak").length
746
+ };
747
+ const { matched: matchedLanguages, missing: missingLanguages } = diffLanguages(
748
+ resume.languages,
749
+ job.requiredLanguages
750
+ );
732
751
  return {
733
752
  score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
734
753
  breakdown,
@@ -737,6 +756,11 @@ function calculateScore(resume, job, config) {
737
756
  matchedKeywords: keywordResult.matchedKeywords,
738
757
  missingKeywords: keywordResult.missingKeywords,
739
758
  overusedKeywords: keywordResult.overusedKeywords,
759
+ keywordsByCategory: keywordResult.keywordsByCategory,
760
+ keywordWeights: keywordResult.keywordWeights,
761
+ achievementStrength,
762
+ matchedLanguages,
763
+ missingLanguages,
740
764
  suggestions: [],
741
765
  warnings: [],
742
766
  // detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
@@ -749,74 +773,6 @@ function calculateScore(resume, job, config) {
749
773
  };
750
774
  }
751
775
 
752
- // src/profiles/index.ts
753
- var defaultSkillAliases = {
754
- // ponytail: "node" split from javascript — Node.js runtime !== JS language
755
- javascript: ["js"],
756
- node: ["node.js", "nodejs"],
757
- typescript: ["ts"],
758
- react: ["reactjs", "react.js"],
759
- "c++": ["cpp"],
760
- "c#": ["csharp"],
761
- python: ["py"],
762
- sql: ["postgres", "mysql", "sqlite"],
763
- graphql: ["gql"],
764
- aws: ["amazon web services"],
765
- azure: ["microsoft azure"],
766
- gcp: ["google cloud", "google cloud platform"],
767
- docker: ["containers"],
768
- kubernetes: ["k8s"],
769
- html: ["html5"],
770
- css: ["css3"],
771
- // ML / data science
772
- pytorch: ["torch"],
773
- tensorflow: ["tf"],
774
- "scikit-learn": ["sklearn"],
775
- pandas: [],
776
- numpy: [],
777
- fastapi: [],
778
- flask: [],
779
- django: [],
780
- // data / infra
781
- kafka: [],
782
- redis: [],
783
- elasticsearch: ["elastic"],
784
- spark: ["apache spark"],
785
- // common pure-letter tech skills (no symbol chars)
786
- accessibility: ["a11y"],
787
- frontend: ["front-end"],
788
- backend: ["back-end"],
789
- security: ["cybersecurity"],
790
- testing: ["unittest", "pytest"],
791
- microservices: [],
792
- agile: ["scrum"],
793
- blockchain: [],
794
- devops: []
795
- };
796
- var softwareEngineerProfile = {
797
- name: "software-engineer",
798
- mandatorySkills: ["javascript", "typescript", "react", "node"],
799
- optionalSkills: ["graphql", "sql", "docker"],
800
- minExperience: 3
801
- };
802
- var dataScientistProfile = {
803
- name: "data-scientist",
804
- mandatorySkills: ["python", "sql", "statistics"],
805
- optionalSkills: ["pandas", "numpy", "pytorch", "tensorflow"],
806
- minExperience: 2
807
- };
808
- var productManagerProfile = {
809
- name: "product-manager",
810
- mandatorySkills: ["roadmap", "stakeholder management", "prioritization"],
811
- optionalSkills: ["a/b testing", "analytics", "sql"],
812
- minExperience: 3
813
- };
814
- var defaultProfiles = [
815
- softwareEngineerProfile,
816
- dataScientistProfile,
817
- productManagerProfile
818
- ];
819
-
820
776
  // src/core/scoring/weights.ts
821
777
  var DEFAULT_WEIGHTS = {
822
778
  skills: 0.3,
@@ -862,9 +818,12 @@ function resolveConfig(config = {}) {
862
818
  keywords: config.weights?.keywords ?? DEFAULT_WEIGHTS.keywords,
863
819
  education: config.weights?.education ?? DEFAULT_WEIGHTS.education
864
820
  };
821
+ const keywordRegistry = mergeKeywordRegistries(defaultKeywordRegistry, config.keywordRegistry ?? []);
865
822
  const resolved = {
866
823
  weights: normalizeWeights(weights),
867
- skillAliases: { ...defaultSkillAliases, ...config.skillAliases ?? {} },
824
+ skillAliases: { ...deriveSkillAliases(keywordRegistry), ...config.skillAliases ?? {} },
825
+ keywordRegistry,
826
+ categoryIndex: buildCategoryIndex(keywordRegistry),
868
827
  profile: config.profile ?? softwareEngineerProfile,
869
828
  rules: config.rules ?? [],
870
829
  keywordDensity: config.keywordDensity ?? DEFAULT_KEYWORD_DENSITY,
@@ -884,6 +843,18 @@ function formatList(values, max = 6) {
884
843
  const trimmed = uniqueValues.slice(0, max);
885
844
  return trimmed.join(", ") + (uniqueValues.length > max ? "..." : "");
886
845
  }
846
+ function buildAliasReplacementSuggestions(resume, job, config) {
847
+ const jobKeywordSet = new Set(job.keywords.map((k) => normalizeSkill(k, config.skillAliases)));
848
+ const replacements = [];
849
+ for (const token of unique(tokenize(resume.normalizedText))) {
850
+ const canonical = normalizeSkill(token, config.skillAliases);
851
+ const jdSurface = job.keywordSurfaceForms[canonical];
852
+ if (jdSurface && jobKeywordSet.has(canonical) && jdSurface.toLowerCase() !== token.toLowerCase()) {
853
+ replacements.push(`Replace "${token}" with "${jdSurface}" to match the job description's wording.`);
854
+ }
855
+ }
856
+ return unique(replacements).slice(0, 5);
857
+ }
887
858
  var SuggestionEngine = class {
888
859
  generate(input) {
889
860
  const suggestions = [];
@@ -898,6 +869,7 @@ var SuggestionEngine = class {
898
869
  `Incorporate job-specific keywords: ${formatList(input.score.missingKeywords)}`
899
870
  );
900
871
  }
872
+ suggestions.push(...buildAliasReplacementSuggestions(input.resume, input.job, input.config));
901
873
  if (input.score.overusedKeywords.length > 0) {
902
874
  suggestions.push(
903
875
  `Avoid keyword stuffing for: ${formatList(input.score.overusedKeywords)}`
@@ -918,6 +890,26 @@ var SuggestionEngine = class {
918
890
  "Strengthen bullet points with impact verbs (led, built, improved, delivered)."
919
891
  );
920
892
  }
893
+ if (input.resume.weakVerbs.length > 0) {
894
+ suggestions.push(
895
+ `Replace weak verbs (${formatList(input.resume.weakVerbs)}) with stronger ones (e.g. led, built, optimized).`
896
+ );
897
+ }
898
+ if (input.score.missingLanguages.length > 0) {
899
+ const formatted = input.score.missingLanguages.map((l) => l.level ? `${l.name} (${l.level})` : l.name).join(", ");
900
+ suggestions.push(`Mention your proficiency in: ${formatted}`);
901
+ }
902
+ const weakAchievement = input.resume.achievements.find((a) => a.strength === "weak");
903
+ if (weakAchievement) {
904
+ suggestions.push(
905
+ `Strengthen "${weakAchievement.text}" \u2014 add scope/metrics, e.g. "Built and maintained scalable services handling 500k+ requests/day."`
906
+ );
907
+ }
908
+ if (input.resume.detectedSections.length < 2 && input.resume.raw.trim().length > 300) {
909
+ suggestions.push(
910
+ "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."
911
+ );
912
+ }
921
913
  return { suggestions, warnings };
922
914
  }
923
915
  };
@@ -1555,7 +1547,8 @@ function analyzeResume(input) {
1555
1547
  resume: parsedResume,
1556
1548
  job: parsedJob,
1557
1549
  score: scoring,
1558
- ruleWarnings: ruleResult.warnings
1550
+ ruleWarnings: ruleResult.warnings,
1551
+ config: resolvedConfig
1559
1552
  });
1560
1553
  let suggestions = suggestionResult.suggestions;
1561
1554
  const llmWarnings = [];
@@ -1575,6 +1568,11 @@ function analyzeResume(input) {
1575
1568
  matchedKeywords: scoring.matchedKeywords,
1576
1569
  missingKeywords: scoring.missingKeywords,
1577
1570
  overusedKeywords: scoring.overusedKeywords,
1571
+ keywordsByCategory: scoring.keywordsByCategory,
1572
+ keywordWeights: scoring.keywordWeights,
1573
+ achievementStrength: scoring.achievementStrength,
1574
+ matchedLanguages: scoring.matchedLanguages,
1575
+ missingLanguages: scoring.missingLanguages,
1578
1576
  experienceGap: scoring.experienceGap,
1579
1577
  detectedSections: parsedResume.detectedSections,
1580
1578
  parsedExperienceYears: parsedResume.totalExperienceYears,
@@ -1617,7 +1615,8 @@ async function analyzeResumeAsync(input) {
1617
1615
  resume: parsedResume,
1618
1616
  job: parsedJob,
1619
1617
  score: scoring,
1620
- ruleWarnings: ruleResult.warnings
1618
+ ruleWarnings: ruleResult.warnings,
1619
+ config: resolvedConfig
1621
1620
  });
1622
1621
  let suggestions = suggestionResult.suggestions;
1623
1622
  const llmWarnings = [];
@@ -1640,6 +1639,11 @@ async function analyzeResumeAsync(input) {
1640
1639
  matchedKeywords: scoring.matchedKeywords,
1641
1640
  missingKeywords: scoring.missingKeywords,
1642
1641
  overusedKeywords: scoring.overusedKeywords,
1642
+ keywordsByCategory: scoring.keywordsByCategory,
1643
+ keywordWeights: scoring.keywordWeights,
1644
+ achievementStrength: scoring.achievementStrength,
1645
+ matchedLanguages: scoring.matchedLanguages,
1646
+ missingLanguages: scoring.missingLanguages,
1643
1647
  experienceGap: scoring.experienceGap,
1644
1648
  detectedSections: parsedResume.detectedSections,
1645
1649
  parsedExperienceYears: parsedResume.totalExperienceYears,
@@ -1684,6 +1688,6 @@ async function enhanceSuggestionsWithLLMAsync(config, suggestions) {
1684
1688
  }
1685
1689
  }
1686
1690
 
1687
- export { LLMBudgetManager, LLMManager, LLMPrompts, LLMSchemas, adaptJdClarificationResponse, adaptSectionClassificationResponse, adaptSkillNormalizationResponse, adaptSuggestionEnhancementResponse, analyzeResume, analyzeResumeAsync, createPrompt, defaultProfiles, defaultSkillAliases, safeExtractArray, safeExtractNumber, safeExtractString };
1691
+ export { LLMBudgetManager, LLMManager, LLMPrompts, LLMSchemas, adaptJdClarificationResponse, adaptSectionClassificationResponse, adaptSkillNormalizationResponse, adaptSuggestionEnhancementResponse, analyzeResume, analyzeResumeAsync, createPrompt, safeExtractArray, safeExtractNumber, safeExtractString };
1688
1692
  //# sourceMappingURL=index.mjs.map
1689
1693
  //# sourceMappingURL=index.mjs.map