@pranavraut033/ats-checker 1.2.0 → 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/README.md +75 -5
- package/dist/chunk-ZJ5E4H7Z.mjs +446 -0
- package/dist/chunk-ZJ5E4H7Z.mjs.map +1 -0
- package/dist/{index.js → index.cjs} +497 -67
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +4 -259
- package/dist/index.d.ts +4 -259
- package/dist/index.mjs +263 -274
- package/dist/index.mjs.map +1 -1
- package/dist/lang/de/index.cjs +70 -0
- package/dist/lang/de/index.cjs.map +1 -0
- package/dist/lang/de/index.d.mts +16 -0
- package/dist/lang/de/index.d.ts +16 -0
- package/dist/lang/de/index.mjs +65 -0
- package/dist/lang/de/index.mjs.map +1 -0
- package/dist/lang/en/index.cjs +212 -0
- package/dist/lang/en/index.cjs.map +1 -0
- package/dist/lang/en/index.d.mts +5 -0
- package/dist/lang/en/index.d.ts +5 -0
- package/dist/lang/en/index.mjs +9 -0
- package/dist/lang/en/index.mjs.map +1 -0
- package/dist/pdf/{index.js → index.cjs} +2 -2
- package/dist/pdf/index.cjs.map +1 -0
- package/dist/scoring-BCShrnki.d.mts +319 -0
- package/dist/scoring-BCShrnki.d.ts +319 -0
- package/package.json +13 -1
- package/dist/index.js.map +0 -1
- package/dist/pdf/index.js.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,202 +1,113 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
147
|
-
|
|
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
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
if (!
|
|
155
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
94
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
183
95
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
for (const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
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
|
|
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
|
|
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
|
|
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 [];
|
|
@@ -561,11 +509,14 @@ function parseResume(resumeText, config) {
|
|
|
561
509
|
sectionContent: sections,
|
|
562
510
|
skills,
|
|
563
511
|
jobTitles: experienceData.jobTitles,
|
|
564
|
-
actionVerbs,
|
|
512
|
+
actionVerbs: actionVerbs.strong,
|
|
513
|
+
weakVerbs: actionVerbs.weak,
|
|
514
|
+
achievements: experienceData.achievements,
|
|
565
515
|
educationEntries,
|
|
566
516
|
experience: experienceData.entries,
|
|
567
517
|
totalExperienceYears,
|
|
568
518
|
keywords: collectKeywords(normalizedText),
|
|
519
|
+
languages: parseLanguageMentions(resumeText),
|
|
569
520
|
warnings
|
|
570
521
|
};
|
|
571
522
|
}
|
|
@@ -647,6 +598,14 @@ var REQUIRED_SKILL_WEIGHT = 0.7;
|
|
|
647
598
|
var OPTIONAL_SKILL_WEIGHT = 0.3;
|
|
648
599
|
var EXPERIENCE_YEARS_WEIGHT = 0.75;
|
|
649
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
|
+
}
|
|
650
609
|
function scoreSkills(resume, job, config) {
|
|
651
610
|
const profileRequired = config.profile?.mandatorySkills ?? [];
|
|
652
611
|
const profileOptional = config.profile?.optionalSkills ?? [];
|
|
@@ -685,32 +644,74 @@ function scoreExperience(resume, job, config) {
|
|
|
685
644
|
const missingYears = Math.max(requiredYears - resume.totalExperienceYears, 0);
|
|
686
645
|
return { score, missingYears: Number(missingYears.toFixed(2)) };
|
|
687
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
|
+
}
|
|
688
652
|
function scoreKeywords(resume, job, config) {
|
|
689
653
|
const jobKeywordSet = new Set(
|
|
690
654
|
job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
|
|
691
655
|
);
|
|
692
656
|
if (jobKeywordSet.size === 0) {
|
|
693
|
-
return {
|
|
657
|
+
return {
|
|
658
|
+
score: 100,
|
|
659
|
+
matchedKeywords: [],
|
|
660
|
+
missingKeywords: [],
|
|
661
|
+
overusedKeywords: [],
|
|
662
|
+
keywordsByCategory: emptyCategoryBuckets(),
|
|
663
|
+
keywordWeights: []
|
|
664
|
+
};
|
|
694
665
|
}
|
|
695
666
|
const resumeTokens = tokenize(resume.normalizedText).map(
|
|
696
667
|
(t) => normalizeSkill(t, config.skillAliases)
|
|
697
668
|
);
|
|
698
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);
|
|
699
677
|
const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
|
|
700
678
|
const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
const
|
|
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);
|
|
704
682
|
const totalTokens = resumeTokens.length || 1;
|
|
705
683
|
const overusedKeywords = matchedKeywords.filter((keyword) => {
|
|
706
|
-
const density = (
|
|
684
|
+
const density = (resumeFrequencies[keyword] ?? 0) / totalTokens;
|
|
707
685
|
return density > config.keywordDensity.max;
|
|
708
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));
|
|
709
708
|
return {
|
|
710
709
|
score,
|
|
711
710
|
matchedKeywords: unique(matchedKeywords).sort(),
|
|
712
711
|
missingKeywords: unique(missingKeywords).sort(),
|
|
713
|
-
overusedKeywords: unique(overusedKeywords).sort()
|
|
712
|
+
overusedKeywords: unique(overusedKeywords).sort(),
|
|
713
|
+
keywordsByCategory,
|
|
714
|
+
keywordWeights
|
|
714
715
|
};
|
|
715
716
|
}
|
|
716
717
|
function scoreEducation(resume, job) {
|
|
@@ -739,6 +740,14 @@ function calculateScore(resume, job, config) {
|
|
|
739
740
|
education: educationScore
|
|
740
741
|
};
|
|
741
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
|
+
);
|
|
742
751
|
return {
|
|
743
752
|
score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
|
|
744
753
|
breakdown,
|
|
@@ -747,6 +756,11 @@ function calculateScore(resume, job, config) {
|
|
|
747
756
|
matchedKeywords: keywordResult.matchedKeywords,
|
|
748
757
|
missingKeywords: keywordResult.missingKeywords,
|
|
749
758
|
overusedKeywords: keywordResult.overusedKeywords,
|
|
759
|
+
keywordsByCategory: keywordResult.keywordsByCategory,
|
|
760
|
+
keywordWeights: keywordResult.keywordWeights,
|
|
761
|
+
achievementStrength,
|
|
762
|
+
matchedLanguages,
|
|
763
|
+
missingLanguages,
|
|
750
764
|
suggestions: [],
|
|
751
765
|
warnings: [],
|
|
752
766
|
// detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
|
|
@@ -759,74 +773,6 @@ function calculateScore(resume, job, config) {
|
|
|
759
773
|
};
|
|
760
774
|
}
|
|
761
775
|
|
|
762
|
-
// src/profiles/index.ts
|
|
763
|
-
var defaultSkillAliases = {
|
|
764
|
-
// ponytail: "node" split from javascript — Node.js runtime !== JS language
|
|
765
|
-
javascript: ["js"],
|
|
766
|
-
node: ["node.js", "nodejs"],
|
|
767
|
-
typescript: ["ts"],
|
|
768
|
-
react: ["reactjs", "react.js"],
|
|
769
|
-
"c++": ["cpp"],
|
|
770
|
-
"c#": ["csharp"],
|
|
771
|
-
python: ["py"],
|
|
772
|
-
sql: ["postgres", "mysql", "sqlite"],
|
|
773
|
-
graphql: ["gql"],
|
|
774
|
-
aws: ["amazon web services"],
|
|
775
|
-
azure: ["microsoft azure"],
|
|
776
|
-
gcp: ["google cloud", "google cloud platform"],
|
|
777
|
-
docker: ["containers"],
|
|
778
|
-
kubernetes: ["k8s"],
|
|
779
|
-
html: ["html5"],
|
|
780
|
-
css: ["css3"],
|
|
781
|
-
// ML / data science
|
|
782
|
-
pytorch: ["torch"],
|
|
783
|
-
tensorflow: ["tf"],
|
|
784
|
-
"scikit-learn": ["sklearn"],
|
|
785
|
-
pandas: [],
|
|
786
|
-
numpy: [],
|
|
787
|
-
fastapi: [],
|
|
788
|
-
flask: [],
|
|
789
|
-
django: [],
|
|
790
|
-
// data / infra
|
|
791
|
-
kafka: [],
|
|
792
|
-
redis: [],
|
|
793
|
-
elasticsearch: ["elastic"],
|
|
794
|
-
spark: ["apache spark"],
|
|
795
|
-
// common pure-letter tech skills (no symbol chars)
|
|
796
|
-
accessibility: ["a11y"],
|
|
797
|
-
frontend: ["front-end"],
|
|
798
|
-
backend: ["back-end"],
|
|
799
|
-
security: ["cybersecurity"],
|
|
800
|
-
testing: ["unittest", "pytest"],
|
|
801
|
-
microservices: [],
|
|
802
|
-
agile: ["scrum"],
|
|
803
|
-
blockchain: [],
|
|
804
|
-
devops: []
|
|
805
|
-
};
|
|
806
|
-
var softwareEngineerProfile = {
|
|
807
|
-
name: "software-engineer",
|
|
808
|
-
mandatorySkills: ["javascript", "typescript", "react", "node"],
|
|
809
|
-
optionalSkills: ["graphql", "sql", "docker"],
|
|
810
|
-
minExperience: 3
|
|
811
|
-
};
|
|
812
|
-
var dataScientistProfile = {
|
|
813
|
-
name: "data-scientist",
|
|
814
|
-
mandatorySkills: ["python", "sql", "statistics"],
|
|
815
|
-
optionalSkills: ["pandas", "numpy", "pytorch", "tensorflow"],
|
|
816
|
-
minExperience: 2
|
|
817
|
-
};
|
|
818
|
-
var productManagerProfile = {
|
|
819
|
-
name: "product-manager",
|
|
820
|
-
mandatorySkills: ["roadmap", "stakeholder management", "prioritization"],
|
|
821
|
-
optionalSkills: ["a/b testing", "analytics", "sql"],
|
|
822
|
-
minExperience: 3
|
|
823
|
-
};
|
|
824
|
-
var defaultProfiles = [
|
|
825
|
-
softwareEngineerProfile,
|
|
826
|
-
dataScientistProfile,
|
|
827
|
-
productManagerProfile
|
|
828
|
-
];
|
|
829
|
-
|
|
830
776
|
// src/core/scoring/weights.ts
|
|
831
777
|
var DEFAULT_WEIGHTS = {
|
|
832
778
|
skills: 0.3,
|
|
@@ -872,9 +818,12 @@ function resolveConfig(config = {}) {
|
|
|
872
818
|
keywords: config.weights?.keywords ?? DEFAULT_WEIGHTS.keywords,
|
|
873
819
|
education: config.weights?.education ?? DEFAULT_WEIGHTS.education
|
|
874
820
|
};
|
|
821
|
+
const keywordRegistry = mergeKeywordRegistries(defaultKeywordRegistry, config.keywordRegistry ?? []);
|
|
875
822
|
const resolved = {
|
|
876
823
|
weights: normalizeWeights(weights),
|
|
877
|
-
skillAliases: { ...
|
|
824
|
+
skillAliases: { ...deriveSkillAliases(keywordRegistry), ...config.skillAliases ?? {} },
|
|
825
|
+
keywordRegistry,
|
|
826
|
+
categoryIndex: buildCategoryIndex(keywordRegistry),
|
|
878
827
|
profile: config.profile ?? softwareEngineerProfile,
|
|
879
828
|
rules: config.rules ?? [],
|
|
880
829
|
keywordDensity: config.keywordDensity ?? DEFAULT_KEYWORD_DENSITY,
|
|
@@ -894,6 +843,18 @@ function formatList(values, max = 6) {
|
|
|
894
843
|
const trimmed = uniqueValues.slice(0, max);
|
|
895
844
|
return trimmed.join(", ") + (uniqueValues.length > max ? "..." : "");
|
|
896
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
|
+
}
|
|
897
858
|
var SuggestionEngine = class {
|
|
898
859
|
generate(input) {
|
|
899
860
|
const suggestions = [];
|
|
@@ -908,6 +869,7 @@ var SuggestionEngine = class {
|
|
|
908
869
|
`Incorporate job-specific keywords: ${formatList(input.score.missingKeywords)}`
|
|
909
870
|
);
|
|
910
871
|
}
|
|
872
|
+
suggestions.push(...buildAliasReplacementSuggestions(input.resume, input.job, input.config));
|
|
911
873
|
if (input.score.overusedKeywords.length > 0) {
|
|
912
874
|
suggestions.push(
|
|
913
875
|
`Avoid keyword stuffing for: ${formatList(input.score.overusedKeywords)}`
|
|
@@ -928,6 +890,21 @@ var SuggestionEngine = class {
|
|
|
928
890
|
"Strengthen bullet points with impact verbs (led, built, improved, delivered)."
|
|
929
891
|
);
|
|
930
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
|
+
}
|
|
931
908
|
if (input.resume.detectedSections.length < 2 && input.resume.raw.trim().length > 300) {
|
|
932
909
|
suggestions.push(
|
|
933
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."
|
|
@@ -1570,7 +1547,8 @@ function analyzeResume(input) {
|
|
|
1570
1547
|
resume: parsedResume,
|
|
1571
1548
|
job: parsedJob,
|
|
1572
1549
|
score: scoring,
|
|
1573
|
-
ruleWarnings: ruleResult.warnings
|
|
1550
|
+
ruleWarnings: ruleResult.warnings,
|
|
1551
|
+
config: resolvedConfig
|
|
1574
1552
|
});
|
|
1575
1553
|
let suggestions = suggestionResult.suggestions;
|
|
1576
1554
|
const llmWarnings = [];
|
|
@@ -1590,6 +1568,11 @@ function analyzeResume(input) {
|
|
|
1590
1568
|
matchedKeywords: scoring.matchedKeywords,
|
|
1591
1569
|
missingKeywords: scoring.missingKeywords,
|
|
1592
1570
|
overusedKeywords: scoring.overusedKeywords,
|
|
1571
|
+
keywordsByCategory: scoring.keywordsByCategory,
|
|
1572
|
+
keywordWeights: scoring.keywordWeights,
|
|
1573
|
+
achievementStrength: scoring.achievementStrength,
|
|
1574
|
+
matchedLanguages: scoring.matchedLanguages,
|
|
1575
|
+
missingLanguages: scoring.missingLanguages,
|
|
1593
1576
|
experienceGap: scoring.experienceGap,
|
|
1594
1577
|
detectedSections: parsedResume.detectedSections,
|
|
1595
1578
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
@@ -1632,7 +1615,8 @@ async function analyzeResumeAsync(input) {
|
|
|
1632
1615
|
resume: parsedResume,
|
|
1633
1616
|
job: parsedJob,
|
|
1634
1617
|
score: scoring,
|
|
1635
|
-
ruleWarnings: ruleResult.warnings
|
|
1618
|
+
ruleWarnings: ruleResult.warnings,
|
|
1619
|
+
config: resolvedConfig
|
|
1636
1620
|
});
|
|
1637
1621
|
let suggestions = suggestionResult.suggestions;
|
|
1638
1622
|
const llmWarnings = [];
|
|
@@ -1655,6 +1639,11 @@ async function analyzeResumeAsync(input) {
|
|
|
1655
1639
|
matchedKeywords: scoring.matchedKeywords,
|
|
1656
1640
|
missingKeywords: scoring.missingKeywords,
|
|
1657
1641
|
overusedKeywords: scoring.overusedKeywords,
|
|
1642
|
+
keywordsByCategory: scoring.keywordsByCategory,
|
|
1643
|
+
keywordWeights: scoring.keywordWeights,
|
|
1644
|
+
achievementStrength: scoring.achievementStrength,
|
|
1645
|
+
matchedLanguages: scoring.matchedLanguages,
|
|
1646
|
+
missingLanguages: scoring.missingLanguages,
|
|
1658
1647
|
experienceGap: scoring.experienceGap,
|
|
1659
1648
|
detectedSections: parsedResume.detectedSections,
|
|
1660
1649
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
@@ -1699,6 +1688,6 @@ async function enhanceSuggestionsWithLLMAsync(config, suggestions) {
|
|
|
1699
1688
|
}
|
|
1700
1689
|
}
|
|
1701
1690
|
|
|
1702
|
-
export { LLMBudgetManager, LLMManager, LLMPrompts, LLMSchemas, adaptJdClarificationResponse, adaptSectionClassificationResponse, adaptSkillNormalizationResponse, adaptSuggestionEnhancementResponse, analyzeResume, analyzeResumeAsync, createPrompt,
|
|
1691
|
+
export { LLMBudgetManager, LLMManager, LLMPrompts, LLMSchemas, adaptJdClarificationResponse, adaptSectionClassificationResponse, adaptSkillNormalizationResponse, adaptSuggestionEnhancementResponse, analyzeResume, analyzeResumeAsync, createPrompt, safeExtractArray, safeExtractNumber, safeExtractString };
|
|
1703
1692
|
//# sourceMappingURL=index.mjs.map
|
|
1704
1693
|
//# sourceMappingURL=index.mjs.map
|