@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/README.md +111 -5
- package/dist/chunk-ZJ5E4H7Z.mjs +446 -0
- package/dist/chunk-ZJ5E4H7Z.mjs.map +1 -0
- package/dist/{index.js → index.cjs} +512 -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 +278 -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.cjs +81 -0
- package/dist/pdf/index.cjs.map +1 -0
- package/dist/pdf/index.d.mts +12 -0
- package/dist/pdf/index.d.ts +12 -0
- package/dist/pdf/index.mjs +79 -0
- package/dist/pdf/index.mjs.map +1 -0
- package/dist/scoring-BCShrnki.d.mts +319 -0
- package/dist/scoring-BCShrnki.d.ts +319 -0
- package/package.json +30 -3
- package/dist/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 [];
|
|
@@ -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 {
|
|
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
|
|
692
|
-
const
|
|
693
|
-
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);
|
|
694
682
|
const totalTokens = resumeTokens.length || 1;
|
|
695
683
|
const overusedKeywords = matchedKeywords.filter((keyword) => {
|
|
696
|
-
const density = (
|
|
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: { ...
|
|
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,
|
|
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
|