@pranavraut033/ats-checker 1.2.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -11
- package/dist/chunk-ZJ5E4H7Z.mjs +446 -0
- package/dist/chunk-ZJ5E4H7Z.mjs.map +1 -0
- package/dist/{index.js → index.cjs} +594 -94
- 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 +360 -301
- 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} +13 -4
- package/dist/pdf/index.cjs.map +1 -0
- package/dist/pdf/index.d.mts +15 -2
- package/dist/pdf/index.d.ts +15 -2
- package/dist/pdf/index.mjs +11 -2
- package/dist/pdf/index.mjs.map +1 -1
- 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,209 +1,143 @@
|
|
|
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
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
// German
|
|
59
|
+
grundkenntnisse: 1,
|
|
60
|
+
gering: 2,
|
|
61
|
+
gut: 3,
|
|
62
|
+
fortgeschritten: 4,
|
|
63
|
+
flie\u00DFend: 5,
|
|
64
|
+
muttersprache: 6,
|
|
65
|
+
muttersprachler: 6,
|
|
66
|
+
// French
|
|
67
|
+
"d\xE9butant": 1,
|
|
68
|
+
"\xE9l\xE9mentaire": 1,
|
|
69
|
+
"limit\xE9": 2,
|
|
70
|
+
"interm\xE9diaire": 3,
|
|
71
|
+
"avanc\xE9": 4,
|
|
72
|
+
courant: 5,
|
|
73
|
+
natif: 6,
|
|
74
|
+
"langue maternelle": 6,
|
|
75
|
+
bilingue: 6
|
|
76
|
+
};
|
|
77
|
+
var LANGUAGE_GROUP = KNOWN_LANGUAGES.join("|");
|
|
78
|
+
var LEVEL_GROUP = Object.keys(LEVEL_RANK).sort((a, b) => b.length - a.length).map((l) => l.replace(/\s+/g, "\\s+")).join("|");
|
|
79
|
+
var BOUNDARY_START = "(?:^|(?<=[^a-z\xE0-\xFF]))";
|
|
80
|
+
var BOUNDARY_END = "(?:$|(?=[^a-z\xE0-\xFF]))";
|
|
81
|
+
var LANGUAGE_LEVEL_RE = new RegExp(
|
|
82
|
+
`\\b(${LANGUAGE_GROUP})\\b(?:\\s*[\\(:\\-]?\\s*(${BOUNDARY_START}(?:${LEVEL_GROUP})${BOUNDARY_END}|[abc][12]))?`,
|
|
83
|
+
"gi"
|
|
84
|
+
);
|
|
85
|
+
var LEVEL_BEFORE_LANGUAGE_RE = new RegExp(
|
|
86
|
+
`${BOUNDARY_START}(${LEVEL_GROUP})${BOUNDARY_END}\\s+(?:in\\s+)?(${LANGUAGE_GROUP})\\b`,
|
|
87
|
+
"gi"
|
|
88
|
+
);
|
|
89
|
+
function canonicalLanguage(name) {
|
|
90
|
+
const lower = name.toLowerCase();
|
|
91
|
+
return LANGUAGE_ALIASES[lower] ?? lower;
|
|
145
92
|
}
|
|
146
|
-
function
|
|
147
|
-
|
|
93
|
+
function toParsedLanguage(name, level) {
|
|
94
|
+
const normalizedLevel = level?.toLowerCase().replace(/\s+/g, " ");
|
|
95
|
+
return {
|
|
96
|
+
name: canonicalLanguage(name),
|
|
97
|
+
level: normalizedLevel,
|
|
98
|
+
levelRank: normalizedLevel ? LEVEL_RANK[normalizedLevel] : void 0
|
|
99
|
+
};
|
|
148
100
|
}
|
|
149
|
-
function
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
if (!
|
|
155
|
-
|
|
156
|
-
output.push(value);
|
|
101
|
+
function parseLanguageMentions(text) {
|
|
102
|
+
const found = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const match of text.matchAll(LANGUAGE_LEVEL_RE)) {
|
|
104
|
+
const parsed = toParsedLanguage(match[1], match[2]);
|
|
105
|
+
const existing = found.get(parsed.name);
|
|
106
|
+
if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
|
|
107
|
+
found.set(parsed.name, parsed);
|
|
157
108
|
}
|
|
158
109
|
}
|
|
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;
|
|
110
|
+
for (const match of text.matchAll(LEVEL_BEFORE_LANGUAGE_RE)) {
|
|
111
|
+
const parsed = toParsedLanguage(match[2], match[1]);
|
|
112
|
+
const existing = found.get(parsed.name);
|
|
113
|
+
if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
|
|
114
|
+
found.set(parsed.name, parsed);
|
|
180
115
|
}
|
|
181
116
|
}
|
|
182
|
-
return
|
|
117
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
183
118
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
for (const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
119
|
+
function diffLanguages(resumeLanguages, requiredLanguages) {
|
|
120
|
+
const byName = new Map(resumeLanguages.map((l) => [l.name, l]));
|
|
121
|
+
const matched = [];
|
|
122
|
+
const missing = [];
|
|
123
|
+
for (const required of requiredLanguages) {
|
|
124
|
+
const have = byName.get(required.name);
|
|
125
|
+
const requiredRank = required.levelRank ?? 0;
|
|
126
|
+
const haveRank = have?.levelRank ?? 0;
|
|
127
|
+
if (have && haveRank >= requiredRank) {
|
|
128
|
+
matched.push(required);
|
|
129
|
+
} else {
|
|
130
|
+
missing.push(required);
|
|
194
131
|
}
|
|
195
132
|
}
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
function normalizeSkills(skills, aliases) {
|
|
199
|
-
return unique(skills.map((skill) => normalizeSkill(skill, aliases)));
|
|
133
|
+
return { matched, missing };
|
|
200
134
|
}
|
|
201
135
|
|
|
202
136
|
// src/core/parser/jd.parser.ts
|
|
203
137
|
var DEGREE_VARIANTS = [
|
|
204
|
-
[/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc
|
|
205
|
-
[/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc
|
|
206
|
-
[/\b(?:phd|ph\.d\.?|doctorate)\b/i, "phd"],
|
|
138
|
+
[/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc\.?|licence)\b/i, "bachelor"],
|
|
139
|
+
[/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc\.?|diplom)\b/i, "master"],
|
|
140
|
+
[/\b(?:phd|ph\.d\.?|doctorate|doktor|doctorat)\b/i, "phd"],
|
|
207
141
|
[/\bmba\b/i, "mba"],
|
|
208
142
|
[/\bassociate(?:'s)?\b/i, "associate"]
|
|
209
143
|
];
|
|
@@ -226,18 +160,38 @@ function extractPreferredSkills(lines) {
|
|
|
226
160
|
return preferred;
|
|
227
161
|
}
|
|
228
162
|
function extractRoleKeywords(text) {
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
return unique(tokenize(
|
|
163
|
+
const roleMatches = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect|director|consultant|lead|vp)/gi) ?? [];
|
|
164
|
+
const fallback = roleMatches.length === 0 ? [text.split(/\n/)[0] ?? ""] : [];
|
|
165
|
+
return unique(tokenize([...roleMatches, ...fallback].join(" ")));
|
|
232
166
|
}
|
|
233
167
|
function extractMinExperience(text) {
|
|
234
|
-
const match = text.match(/(\d{1,2})\+?\s
|
|
235
|
-
if (match)
|
|
236
|
-
|
|
168
|
+
const match = text.match(/(\d{1,2})\+?\s*(?:years?|yrs\.?|jahre?|ans?|années?)/i);
|
|
169
|
+
if (!match) return void 0;
|
|
170
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
171
|
+
return parsed <= 60 ? parsed : void 0;
|
|
172
|
+
}
|
|
173
|
+
var SURFACE_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/gi;
|
|
174
|
+
function collectKeywordSurfaceForms(rawText, aliases) {
|
|
175
|
+
const matches = rawText.match(SURFACE_TOKEN_RE) ?? [];
|
|
176
|
+
const surfaceForms = {};
|
|
177
|
+
for (const match of matches) {
|
|
178
|
+
const canonical = normalizeSkill(match, aliases);
|
|
179
|
+
if (!(canonical in surfaceForms)) {
|
|
180
|
+
surfaceForms[canonical] = match;
|
|
181
|
+
}
|
|
237
182
|
}
|
|
238
|
-
return
|
|
183
|
+
return surfaceForms;
|
|
239
184
|
}
|
|
240
|
-
|
|
185
|
+
var LANG_SECTION_RE = /^\s*(?:languages?|sprache|langue)s?\s*[:\-–—]?\s*/i;
|
|
186
|
+
var LANG_REQUIREMENT_HINT_RE = /\b(fluent|required|must|need|speak|proficient|native|conversational|intermediate|advanced|professional|[abc][12])\b/i;
|
|
187
|
+
function isLanguageRequired(lang, jobDescription) {
|
|
188
|
+
return splitLines(jobDescription).some((line) => {
|
|
189
|
+
const lower = line.toLowerCase();
|
|
190
|
+
if (!lower.includes(lang.name)) return false;
|
|
191
|
+
return LANG_SECTION_RE.test(line) || LANG_REQUIREMENT_HINT_RE.test(line);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function extractDegreeLevels(text) {
|
|
241
195
|
const found = /* @__PURE__ */ new Set();
|
|
242
196
|
for (const [pattern, canonical] of DEGREE_VARIANTS) {
|
|
243
197
|
if (pattern.test(text)) found.add(canonical);
|
|
@@ -275,7 +229,13 @@ function parseJobDescription(jobDescription, config) {
|
|
|
275
229
|
roleKeywords,
|
|
276
230
|
keywords,
|
|
277
231
|
minExperienceYears: extractMinExperience(jobDescription),
|
|
278
|
-
educationRequirements:
|
|
232
|
+
educationRequirements: extractDegreeLevels(jobDescription),
|
|
233
|
+
keywordSurfaceForms: collectKeywordSurfaceForms(jobDescription, config.skillAliases),
|
|
234
|
+
// A language only counts as required if its mention carries a requirement/level cue
|
|
235
|
+
// or sits in a "Languages:" line — plain references ("our Berlin office") don't count.
|
|
236
|
+
requiredLanguages: parseLanguageMentions(jobDescription).filter(
|
|
237
|
+
(lang) => isLanguageRequired(lang, jobDescription)
|
|
238
|
+
)
|
|
279
239
|
};
|
|
280
240
|
}
|
|
281
241
|
|
|
@@ -304,11 +264,37 @@ var MONTHS = {
|
|
|
304
264
|
nov: 11,
|
|
305
265
|
november: 11,
|
|
306
266
|
dec: 12,
|
|
307
|
-
december: 12
|
|
267
|
+
december: 12,
|
|
268
|
+
// German
|
|
269
|
+
januar: 1,
|
|
270
|
+
j\u00E4nner: 1,
|
|
271
|
+
februar: 2,
|
|
272
|
+
m\u00E4rz: 3,
|
|
273
|
+
maerz: 3,
|
|
274
|
+
mai: 5,
|
|
275
|
+
juni: 6,
|
|
276
|
+
juli: 7,
|
|
277
|
+
oktober: 10,
|
|
278
|
+
dezember: 12,
|
|
279
|
+
// French
|
|
280
|
+
janvier: 1,
|
|
281
|
+
f\u00E9vrier: 2,
|
|
282
|
+
fevrier: 2,
|
|
283
|
+
mars: 3,
|
|
284
|
+
avril: 4,
|
|
285
|
+
juin: 6,
|
|
286
|
+
juillet: 7,
|
|
287
|
+
ao\u00FBt: 8,
|
|
288
|
+
aout: 8,
|
|
289
|
+
septembre: 9,
|
|
290
|
+
octobre: 10,
|
|
291
|
+
novembre: 11,
|
|
292
|
+
d\u00E9cembre: 12,
|
|
293
|
+
decembre: 12
|
|
308
294
|
};
|
|
309
295
|
function parseDateToken(raw) {
|
|
310
296
|
const cleaned = raw.trim().toLowerCase();
|
|
311
|
-
const monthMatch = cleaned.match(/([a-z]{3,9})\s*(\d{4})/i);
|
|
297
|
+
const monthMatch = cleaned.match(/([a-zà-ÿ]{3,9})\s*(\d{4})/i);
|
|
312
298
|
if (monthMatch) {
|
|
313
299
|
const monthName = monthMatch[1].toLowerCase();
|
|
314
300
|
const year = Number.parseInt(monthMatch[2], 10);
|
|
@@ -340,14 +326,14 @@ function monthsBetween(start, end) {
|
|
|
340
326
|
function parseDateRange(text, referenceDate) {
|
|
341
327
|
const normalized = text.trim();
|
|
342
328
|
const rangeMatch = normalized.match(
|
|
343
|
-
/(\d{1,2}\/\d{4}|[A-Za-z]{3,9}\s+\d{4}|\d{4})\s*(?:-|to
|
|
329
|
+
/(\d{1,2}\/\d{4}|[A-Za-zà-ÿ]{3,9}\s+\d{4}|\d{4})\s*(?:-|to|through|until|bis|jusqu'à|à|–|—)\s*(Present|Current|Now|Aktuell|Heute|Actuellement|Présent|\d{1,2}\/\d{4}|[A-Za-zà-ÿ]{3,9}\s+\d{4}|\d{4})/i
|
|
344
330
|
);
|
|
345
331
|
if (!rangeMatch) {
|
|
346
332
|
return null;
|
|
347
333
|
}
|
|
348
334
|
const startToken = parseDateToken(rangeMatch[1]);
|
|
349
335
|
const endRaw = rangeMatch[2];
|
|
350
|
-
const isPresent = /present|current|now/i.test(endRaw);
|
|
336
|
+
const isPresent = /present|current|now|aktuell|heute|actuellement|présent|actuel/i.test(endRaw);
|
|
351
337
|
const endToken = isPresent ? void 0 : parseDateToken(endRaw);
|
|
352
338
|
if (!startToken) {
|
|
353
339
|
return null;
|
|
@@ -401,14 +387,23 @@ function sumExperienceYears(ranges) {
|
|
|
401
387
|
|
|
402
388
|
// src/core/parser/resume.parser.ts
|
|
403
389
|
var SECTION_ALIASES = {
|
|
404
|
-
summary: ["summary", "profile", "about"],
|
|
405
|
-
experience: [
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
390
|
+
summary: ["summary", "profile", "about", "zusammenfassung", "profil", "r\xE9sum\xE9", "\xE0 propos"],
|
|
391
|
+
experience: [
|
|
392
|
+
"experience",
|
|
393
|
+
"work experience",
|
|
394
|
+
"professional experience",
|
|
395
|
+
"employment",
|
|
396
|
+
"erfahrung",
|
|
397
|
+
"berufserfahrung",
|
|
398
|
+
"exp\xE9rience",
|
|
399
|
+
"exp\xE9rience professionnelle"
|
|
400
|
+
],
|
|
401
|
+
skills: ["skills", "technical skills", "technologies", "f\xE4higkeiten", "kenntnisse", "comp\xE9tences"],
|
|
402
|
+
education: ["education", "academics", "academic background", "ausbildung", "formation", "\xE9tudes"],
|
|
403
|
+
projects: ["projects", "portfolio", "projekte", "projets"],
|
|
404
|
+
certifications: ["certifications", "licenses", "zertifizierungen", "certifications professionnelles"]
|
|
410
405
|
};
|
|
411
|
-
var
|
|
406
|
+
var STRONG_VERBS = [
|
|
412
407
|
"led",
|
|
413
408
|
"managed",
|
|
414
409
|
"built",
|
|
@@ -429,6 +424,21 @@ var ACTION_VERBS = [
|
|
|
429
424
|
"reduced",
|
|
430
425
|
"increased"
|
|
431
426
|
];
|
|
427
|
+
var WEAK_VERBS = ["worked", "helped", "performed", "responsible", "assisted", "participated", "involved"];
|
|
428
|
+
var METRIC_RE = /\d|%|\$|\bk\+|\bm\+/i;
|
|
429
|
+
function classifyAchievement(line) {
|
|
430
|
+
const lower = line.toLowerCase();
|
|
431
|
+
const hasMetric = METRIC_RE.test(line);
|
|
432
|
+
const hasStrongVerb = STRONG_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
|
|
433
|
+
const hasWeakVerb = WEAK_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
|
|
434
|
+
if (hasStrongVerb && hasMetric) {
|
|
435
|
+
return { text: line, strength: "strong", reason: "strong verb + quantified impact" };
|
|
436
|
+
}
|
|
437
|
+
if (hasWeakVerb) {
|
|
438
|
+
return { text: line, strength: "weak", reason: "weak verb" };
|
|
439
|
+
}
|
|
440
|
+
return { text: line, strength: "weak", reason: "no quantified impact" };
|
|
441
|
+
}
|
|
432
442
|
function detectSection(line) {
|
|
433
443
|
const normalized = line.trim().toLowerCase();
|
|
434
444
|
for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
|
|
@@ -469,21 +479,27 @@ function extractSections(text) {
|
|
|
469
479
|
}
|
|
470
480
|
function parseSkills(sectionContent, aliases) {
|
|
471
481
|
if (!sectionContent) return [];
|
|
472
|
-
const
|
|
482
|
+
const hasBullets = /[•·‣▪○●◦]/.test(sectionContent);
|
|
483
|
+
const normalized = hasBullets ? sectionContent.replace(/\n/g, " ") : sectionContent;
|
|
484
|
+
const raw = normalized.split(/[,;\n]|[•·‣▪○●◦]/).map((skill) => skill.trim().replace(/^[-•·‣▪○●◦\s]+|[-•·‣▪○●◦\s]+$/g, "").trim()).filter(Boolean);
|
|
473
485
|
return normalizeSkills(raw, aliases);
|
|
474
486
|
}
|
|
475
487
|
function parseActionVerbs(text) {
|
|
476
488
|
const words = tokenize(text);
|
|
477
|
-
return
|
|
489
|
+
return {
|
|
490
|
+
strong: STRONG_VERBS.filter((verb) => words.includes(verb)),
|
|
491
|
+
weak: WEAK_VERBS.filter((verb) => words.includes(verb))
|
|
492
|
+
};
|
|
478
493
|
}
|
|
479
494
|
function parseExperience(sectionContent, referenceDate) {
|
|
480
495
|
if (!sectionContent) {
|
|
481
|
-
return { entries: [], rangesInMonths: [], jobTitles: [] };
|
|
496
|
+
return { entries: [], rangesInMonths: [], jobTitles: [], achievements: [] };
|
|
482
497
|
}
|
|
483
498
|
const lines = splitLines(sectionContent);
|
|
484
499
|
const entries = [];
|
|
485
500
|
const rangesInMonths = [];
|
|
486
501
|
const jobTitles = [];
|
|
502
|
+
const achievements = [];
|
|
487
503
|
for (const line of lines) {
|
|
488
504
|
const range = parseDateRange(line, referenceDate);
|
|
489
505
|
if (range) {
|
|
@@ -498,20 +514,22 @@ function parseExperience(sectionContent, referenceDate) {
|
|
|
498
514
|
}
|
|
499
515
|
continue;
|
|
500
516
|
}
|
|
501
|
-
const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
|
|
517
|
+
const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|VP|Director|Consultant|Architect|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
|
|
502
518
|
if (titleMatch) {
|
|
503
519
|
const title = titleMatch[0].trim();
|
|
504
520
|
jobTitles.push(title.toLowerCase());
|
|
505
521
|
const entry = { title, description: line };
|
|
506
522
|
entries.push(entry);
|
|
523
|
+
achievements.push(classifyAchievement(line));
|
|
507
524
|
continue;
|
|
508
525
|
}
|
|
509
526
|
if (entries.length > 0) {
|
|
510
527
|
const current = entries[entries.length - 1];
|
|
511
528
|
current.description = [current.description, line].filter(Boolean).join(" ").trim();
|
|
512
529
|
}
|
|
530
|
+
achievements.push(classifyAchievement(line));
|
|
513
531
|
}
|
|
514
|
-
return { entries, rangesInMonths, jobTitles: unique(jobTitles) };
|
|
532
|
+
return { entries, rangesInMonths, jobTitles: unique(jobTitles), achievements };
|
|
515
533
|
}
|
|
516
534
|
function parseEducation(sectionContent) {
|
|
517
535
|
if (!sectionContent) return [];
|
|
@@ -534,7 +552,8 @@ function parseResume(resumeText, config) {
|
|
|
534
552
|
const textToScan = sections.summary ?? normalizedText;
|
|
535
553
|
const yearsMatch = textToScan.match(/(\d{1,2})\+?\s*years?/i);
|
|
536
554
|
if (yearsMatch) {
|
|
537
|
-
|
|
555
|
+
const parsed = Number.parseInt(yearsMatch[1], 10);
|
|
556
|
+
totalExperienceYears = parsed <= 60 ? parsed : 0;
|
|
538
557
|
}
|
|
539
558
|
}
|
|
540
559
|
const requiredSections = ["summary", "experience", "skills", "education"];
|
|
@@ -561,11 +580,14 @@ function parseResume(resumeText, config) {
|
|
|
561
580
|
sectionContent: sections,
|
|
562
581
|
skills,
|
|
563
582
|
jobTitles: experienceData.jobTitles,
|
|
564
|
-
actionVerbs,
|
|
583
|
+
actionVerbs: actionVerbs.strong,
|
|
584
|
+
weakVerbs: actionVerbs.weak,
|
|
585
|
+
achievements: experienceData.achievements,
|
|
565
586
|
educationEntries,
|
|
566
587
|
experience: experienceData.entries,
|
|
567
588
|
totalExperienceYears,
|
|
568
589
|
keywords: collectKeywords(normalizedText),
|
|
590
|
+
languages: parseLanguageMentions(resumeText),
|
|
569
591
|
warnings
|
|
570
592
|
};
|
|
571
593
|
}
|
|
@@ -647,6 +669,14 @@ var REQUIRED_SKILL_WEIGHT = 0.7;
|
|
|
647
669
|
var OPTIONAL_SKILL_WEIGHT = 0.3;
|
|
648
670
|
var EXPERIENCE_YEARS_WEIGHT = 0.75;
|
|
649
671
|
var EXPERIENCE_ROLE_WEIGHT = 0.25;
|
|
672
|
+
var ALL_CATEGORIES = ["technical", "tool", "concept", "soft", "marketing", "domain"];
|
|
673
|
+
function emptyCategoryBuckets() {
|
|
674
|
+
const buckets = {};
|
|
675
|
+
for (const category of ALL_CATEGORIES) {
|
|
676
|
+
buckets[category] = { matched: [], missing: [] };
|
|
677
|
+
}
|
|
678
|
+
return buckets;
|
|
679
|
+
}
|
|
650
680
|
function scoreSkills(resume, job, config) {
|
|
651
681
|
const profileRequired = config.profile?.mandatorySkills ?? [];
|
|
652
682
|
const profileOptional = config.profile?.optionalSkills ?? [];
|
|
@@ -685,42 +715,83 @@ function scoreExperience(resume, job, config) {
|
|
|
685
715
|
const missingYears = Math.max(requiredYears - resume.totalExperienceYears, 0);
|
|
686
716
|
return { score, missingYears: Number(missingYears.toFixed(2)) };
|
|
687
717
|
}
|
|
718
|
+
function keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies) {
|
|
719
|
+
const base = requiredSet.has(keyword) ? 3 : preferredSet.has(keyword) ? 2 : 1;
|
|
720
|
+
const freqBonus = Math.min((jdFrequencies[keyword] ?? 1) - 1, 3) * 0.25;
|
|
721
|
+
return base + freqBonus;
|
|
722
|
+
}
|
|
688
723
|
function scoreKeywords(resume, job, config) {
|
|
689
724
|
const jobKeywordSet = new Set(
|
|
690
725
|
job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
|
|
691
726
|
);
|
|
692
727
|
if (jobKeywordSet.size === 0) {
|
|
693
|
-
return {
|
|
728
|
+
return {
|
|
729
|
+
score: 100,
|
|
730
|
+
matchedKeywords: [],
|
|
731
|
+
missingKeywords: [],
|
|
732
|
+
overusedKeywords: [],
|
|
733
|
+
keywordsByCategory: emptyCategoryBuckets(),
|
|
734
|
+
keywordWeights: []
|
|
735
|
+
};
|
|
694
736
|
}
|
|
695
737
|
const resumeTokens = tokenize(resume.normalizedText).map(
|
|
696
738
|
(t) => normalizeSkill(t, config.skillAliases)
|
|
697
739
|
);
|
|
698
740
|
const resumeTokenSet = new Set(resumeTokens);
|
|
741
|
+
const resumeFrequencies = countFrequencies(resumeTokens);
|
|
742
|
+
const requiredSet = new Set(job.requiredSkills);
|
|
743
|
+
const preferredSet = new Set(job.preferredSkills);
|
|
744
|
+
const jdFrequencies = countFrequencies(
|
|
745
|
+
tokenize(job.normalizedText).map((t) => normalizeSkill(t, config.skillAliases))
|
|
746
|
+
);
|
|
747
|
+
const weightOf = (keyword) => keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies);
|
|
699
748
|
const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
|
|
700
749
|
const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
const
|
|
750
|
+
const totalWeight = [...jobKeywordSet].reduce((sum, keyword) => sum + weightOf(keyword), 0);
|
|
751
|
+
const matchedWeight = matchedKeywords.reduce((sum, keyword) => sum + weightOf(keyword), 0);
|
|
752
|
+
const score = clamp(matchedWeight / totalWeight * 100, 0, 100);
|
|
704
753
|
const totalTokens = resumeTokens.length || 1;
|
|
705
754
|
const overusedKeywords = matchedKeywords.filter((keyword) => {
|
|
706
|
-
const density = (
|
|
755
|
+
const density = (resumeFrequencies[keyword] ?? 0) / totalTokens;
|
|
707
756
|
return density > config.keywordDensity.max;
|
|
708
757
|
});
|
|
758
|
+
const keywordsByCategory = emptyCategoryBuckets();
|
|
759
|
+
for (const keyword of matchedKeywords) {
|
|
760
|
+
keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].matched.push(keyword);
|
|
761
|
+
}
|
|
762
|
+
for (const keyword of missingKeywords) {
|
|
763
|
+
keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].missing.push(keyword);
|
|
764
|
+
}
|
|
765
|
+
for (const bucket of Object.values(keywordsByCategory)) {
|
|
766
|
+
bucket.matched.sort();
|
|
767
|
+
bucket.missing.sort();
|
|
768
|
+
}
|
|
769
|
+
const keywordWeights = [...jobKeywordSet].map((term) => {
|
|
770
|
+
const weight = Number(weightOf(term).toFixed(2));
|
|
771
|
+
return {
|
|
772
|
+
term,
|
|
773
|
+
category: config.categoryIndex.get(term) ?? "technical",
|
|
774
|
+
jdWeight: weight,
|
|
775
|
+
resumeWeight: resumeFrequencies[term] ?? 0,
|
|
776
|
+
importance: weight
|
|
777
|
+
};
|
|
778
|
+
}).sort((a, b) => a.term.localeCompare(b.term));
|
|
709
779
|
return {
|
|
710
780
|
score,
|
|
711
781
|
matchedKeywords: unique(matchedKeywords).sort(),
|
|
712
782
|
missingKeywords: unique(missingKeywords).sort(),
|
|
713
|
-
overusedKeywords: unique(overusedKeywords).sort()
|
|
783
|
+
overusedKeywords: unique(overusedKeywords).sort(),
|
|
784
|
+
keywordsByCategory,
|
|
785
|
+
keywordWeights
|
|
714
786
|
};
|
|
715
787
|
}
|
|
716
788
|
function scoreEducation(resume, job) {
|
|
717
789
|
if (job.educationRequirements.length === 0) {
|
|
718
790
|
return 100;
|
|
719
791
|
}
|
|
720
|
-
const
|
|
721
|
-
const normalizedEducation = resumeEducationText.toLowerCase();
|
|
792
|
+
const resumeDegreeLevels = extractDegreeLevels(resume.educationEntries.join(" "));
|
|
722
793
|
const matched = job.educationRequirements.filter(
|
|
723
|
-
(requirement) =>
|
|
794
|
+
(requirement) => resumeDegreeLevels.includes(requirement)
|
|
724
795
|
);
|
|
725
796
|
if (matched.length === 0) {
|
|
726
797
|
return 0;
|
|
@@ -739,6 +810,14 @@ function calculateScore(resume, job, config) {
|
|
|
739
810
|
education: educationScore
|
|
740
811
|
};
|
|
741
812
|
const weightedScore = breakdown.skills * config.weights.skills + breakdown.experience * config.weights.experience + breakdown.keywords * config.weights.keywords + breakdown.education * config.weights.education;
|
|
813
|
+
const achievementStrength = {
|
|
814
|
+
strong: resume.achievements.filter((a) => a.strength === "strong").length,
|
|
815
|
+
weak: resume.achievements.filter((a) => a.strength === "weak").length
|
|
816
|
+
};
|
|
817
|
+
const { matched: matchedLanguages, missing: missingLanguages } = diffLanguages(
|
|
818
|
+
resume.languages,
|
|
819
|
+
job.requiredLanguages
|
|
820
|
+
);
|
|
742
821
|
return {
|
|
743
822
|
score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
|
|
744
823
|
breakdown,
|
|
@@ -747,6 +826,11 @@ function calculateScore(resume, job, config) {
|
|
|
747
826
|
matchedKeywords: keywordResult.matchedKeywords,
|
|
748
827
|
missingKeywords: keywordResult.missingKeywords,
|
|
749
828
|
overusedKeywords: keywordResult.overusedKeywords,
|
|
829
|
+
keywordsByCategory: keywordResult.keywordsByCategory,
|
|
830
|
+
keywordWeights: keywordResult.keywordWeights,
|
|
831
|
+
achievementStrength,
|
|
832
|
+
matchedLanguages,
|
|
833
|
+
missingLanguages,
|
|
750
834
|
suggestions: [],
|
|
751
835
|
warnings: [],
|
|
752
836
|
// detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
|
|
@@ -759,74 +843,6 @@ function calculateScore(resume, job, config) {
|
|
|
759
843
|
};
|
|
760
844
|
}
|
|
761
845
|
|
|
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
846
|
// src/core/scoring/weights.ts
|
|
831
847
|
var DEFAULT_WEIGHTS = {
|
|
832
848
|
skills: 0.3,
|
|
@@ -872,9 +888,12 @@ function resolveConfig(config = {}) {
|
|
|
872
888
|
keywords: config.weights?.keywords ?? DEFAULT_WEIGHTS.keywords,
|
|
873
889
|
education: config.weights?.education ?? DEFAULT_WEIGHTS.education
|
|
874
890
|
};
|
|
891
|
+
const keywordRegistry = mergeKeywordRegistries(defaultKeywordRegistry, config.keywordRegistry ?? []);
|
|
875
892
|
const resolved = {
|
|
876
893
|
weights: normalizeWeights(weights),
|
|
877
|
-
skillAliases: { ...
|
|
894
|
+
skillAliases: { ...deriveSkillAliases(keywordRegistry), ...config.skillAliases ?? {} },
|
|
895
|
+
keywordRegistry,
|
|
896
|
+
categoryIndex: buildCategoryIndex(keywordRegistry),
|
|
878
897
|
profile: config.profile ?? softwareEngineerProfile,
|
|
879
898
|
rules: config.rules ?? [],
|
|
880
899
|
keywordDensity: config.keywordDensity ?? DEFAULT_KEYWORD_DENSITY,
|
|
@@ -894,6 +913,18 @@ function formatList(values, max = 6) {
|
|
|
894
913
|
const trimmed = uniqueValues.slice(0, max);
|
|
895
914
|
return trimmed.join(", ") + (uniqueValues.length > max ? "..." : "");
|
|
896
915
|
}
|
|
916
|
+
function buildAliasReplacementSuggestions(resume, job, config) {
|
|
917
|
+
const jobKeywordSet = new Set(job.keywords.map((k) => normalizeSkill(k, config.skillAliases)));
|
|
918
|
+
const replacements = [];
|
|
919
|
+
for (const token of unique(tokenize(resume.normalizedText))) {
|
|
920
|
+
const canonical = normalizeSkill(token, config.skillAliases);
|
|
921
|
+
const jdSurface = job.keywordSurfaceForms[canonical];
|
|
922
|
+
if (jdSurface && jobKeywordSet.has(canonical) && jdSurface.toLowerCase() !== token.toLowerCase()) {
|
|
923
|
+
replacements.push(`Replace "${token}" with "${jdSurface}" to match the job description's wording.`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return unique(replacements).slice(0, 5);
|
|
927
|
+
}
|
|
897
928
|
var SuggestionEngine = class {
|
|
898
929
|
generate(input) {
|
|
899
930
|
const suggestions = [];
|
|
@@ -908,6 +939,7 @@ var SuggestionEngine = class {
|
|
|
908
939
|
`Incorporate job-specific keywords: ${formatList(input.score.missingKeywords)}`
|
|
909
940
|
);
|
|
910
941
|
}
|
|
942
|
+
suggestions.push(...buildAliasReplacementSuggestions(input.resume, input.job, input.config));
|
|
911
943
|
if (input.score.overusedKeywords.length > 0) {
|
|
912
944
|
suggestions.push(
|
|
913
945
|
`Avoid keyword stuffing for: ${formatList(input.score.overusedKeywords)}`
|
|
@@ -928,6 +960,21 @@ var SuggestionEngine = class {
|
|
|
928
960
|
"Strengthen bullet points with impact verbs (led, built, improved, delivered)."
|
|
929
961
|
);
|
|
930
962
|
}
|
|
963
|
+
if (input.resume.weakVerbs.length > 0) {
|
|
964
|
+
suggestions.push(
|
|
965
|
+
`Replace weak verbs (${formatList(input.resume.weakVerbs)}) with stronger ones (e.g. led, built, optimized).`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
if (input.score.missingLanguages.length > 0) {
|
|
969
|
+
const formatted = input.score.missingLanguages.map((l) => l.level ? `${l.name} (${l.level})` : l.name).join(", ");
|
|
970
|
+
suggestions.push(`Mention your proficiency in: ${formatted}`);
|
|
971
|
+
}
|
|
972
|
+
const weakAchievement = input.resume.achievements.find((a) => a.strength === "weak");
|
|
973
|
+
if (weakAchievement) {
|
|
974
|
+
suggestions.push(
|
|
975
|
+
`Strengthen "${weakAchievement.text}" \u2014 add scope/metrics, e.g. "Built and maintained scalable services handling 500k+ requests/day."`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
931
978
|
if (input.resume.detectedSections.length < 2 && input.resume.raw.trim().length > 300) {
|
|
932
979
|
suggestions.push(
|
|
933
980
|
"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 +1617,8 @@ function analyzeResume(input) {
|
|
|
1570
1617
|
resume: parsedResume,
|
|
1571
1618
|
job: parsedJob,
|
|
1572
1619
|
score: scoring,
|
|
1573
|
-
ruleWarnings: ruleResult.warnings
|
|
1620
|
+
ruleWarnings: ruleResult.warnings,
|
|
1621
|
+
config: resolvedConfig
|
|
1574
1622
|
});
|
|
1575
1623
|
let suggestions = suggestionResult.suggestions;
|
|
1576
1624
|
const llmWarnings = [];
|
|
@@ -1590,6 +1638,11 @@ function analyzeResume(input) {
|
|
|
1590
1638
|
matchedKeywords: scoring.matchedKeywords,
|
|
1591
1639
|
missingKeywords: scoring.missingKeywords,
|
|
1592
1640
|
overusedKeywords: scoring.overusedKeywords,
|
|
1641
|
+
keywordsByCategory: scoring.keywordsByCategory,
|
|
1642
|
+
keywordWeights: scoring.keywordWeights,
|
|
1643
|
+
achievementStrength: scoring.achievementStrength,
|
|
1644
|
+
matchedLanguages: scoring.matchedLanguages,
|
|
1645
|
+
missingLanguages: scoring.missingLanguages,
|
|
1593
1646
|
experienceGap: scoring.experienceGap,
|
|
1594
1647
|
detectedSections: parsedResume.detectedSections,
|
|
1595
1648
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
@@ -1632,7 +1685,8 @@ async function analyzeResumeAsync(input) {
|
|
|
1632
1685
|
resume: parsedResume,
|
|
1633
1686
|
job: parsedJob,
|
|
1634
1687
|
score: scoring,
|
|
1635
|
-
ruleWarnings: ruleResult.warnings
|
|
1688
|
+
ruleWarnings: ruleResult.warnings,
|
|
1689
|
+
config: resolvedConfig
|
|
1636
1690
|
});
|
|
1637
1691
|
let suggestions = suggestionResult.suggestions;
|
|
1638
1692
|
const llmWarnings = [];
|
|
@@ -1655,6 +1709,11 @@ async function analyzeResumeAsync(input) {
|
|
|
1655
1709
|
matchedKeywords: scoring.matchedKeywords,
|
|
1656
1710
|
missingKeywords: scoring.missingKeywords,
|
|
1657
1711
|
overusedKeywords: scoring.overusedKeywords,
|
|
1712
|
+
keywordsByCategory: scoring.keywordsByCategory,
|
|
1713
|
+
keywordWeights: scoring.keywordWeights,
|
|
1714
|
+
achievementStrength: scoring.achievementStrength,
|
|
1715
|
+
matchedLanguages: scoring.matchedLanguages,
|
|
1716
|
+
missingLanguages: scoring.missingLanguages,
|
|
1658
1717
|
experienceGap: scoring.experienceGap,
|
|
1659
1718
|
detectedSections: parsedResume.detectedSections,
|
|
1660
1719
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
@@ -1699,6 +1758,6 @@ async function enhanceSuggestionsWithLLMAsync(config, suggestions) {
|
|
|
1699
1758
|
}
|
|
1700
1759
|
}
|
|
1701
1760
|
|
|
1702
|
-
export { LLMBudgetManager, LLMManager, LLMPrompts, LLMSchemas, adaptJdClarificationResponse, adaptSectionClassificationResponse, adaptSkillNormalizationResponse, adaptSuggestionEnhancementResponse, analyzeResume, analyzeResumeAsync, createPrompt,
|
|
1761
|
+
export { LLMBudgetManager, LLMManager, LLMPrompts, LLMSchemas, adaptJdClarificationResponse, adaptSectionClassificationResponse, adaptSkillNormalizationResponse, adaptSuggestionEnhancementResponse, analyzeResume, analyzeResumeAsync, createPrompt, safeExtractArray, safeExtractNumber, safeExtractString };
|
|
1703
1762
|
//# sourceMappingURL=index.mjs.map
|
|
1704
1763
|
//# sourceMappingURL=index.mjs.map
|