@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
|
@@ -185,27 +185,187 @@ function containsTableLikeStructure(text) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// src/utils/skills.ts
|
|
188
|
+
var aliasIndexCache = /* @__PURE__ */ new WeakMap();
|
|
189
|
+
function getAliasIndex(aliases) {
|
|
190
|
+
let index = aliasIndexCache.get(aliases);
|
|
191
|
+
if (!index) {
|
|
192
|
+
index = /* @__PURE__ */ new Map();
|
|
193
|
+
for (const [canonical, aliasList] of Object.entries(aliases)) {
|
|
194
|
+
const lower = canonical.toLowerCase();
|
|
195
|
+
index.set(lower, lower);
|
|
196
|
+
for (const alias of aliasList) {
|
|
197
|
+
index.set(alias.toLowerCase(), lower);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
aliasIndexCache.set(aliases, index);
|
|
201
|
+
}
|
|
202
|
+
return index;
|
|
203
|
+
}
|
|
188
204
|
function normalizeSkill(skill, aliases) {
|
|
189
205
|
const normalized = skill.trim().toLowerCase();
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
return getAliasIndex(aliases).get(normalized) ?? normalized;
|
|
207
|
+
}
|
|
208
|
+
function normalizeSkills(skills, aliases) {
|
|
209
|
+
return unique(skills.map((skill) => normalizeSkill(skill, aliases)));
|
|
210
|
+
}
|
|
211
|
+
function deriveSkillAliases(registry) {
|
|
212
|
+
const aliases = {};
|
|
213
|
+
for (const entry of registry) {
|
|
214
|
+
aliases[entry.canonical] = entry.aliases;
|
|
215
|
+
}
|
|
216
|
+
return aliases;
|
|
217
|
+
}
|
|
218
|
+
function buildCategoryIndex(registry) {
|
|
219
|
+
const index = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const entry of registry) {
|
|
221
|
+
index.set(entry.canonical.toLowerCase(), entry.category);
|
|
222
|
+
}
|
|
223
|
+
return index;
|
|
224
|
+
}
|
|
225
|
+
function mergeKeywordRegistries(base, overrides) {
|
|
226
|
+
const byCanonical = /* @__PURE__ */ new Map();
|
|
227
|
+
for (const entry of base) byCanonical.set(entry.canonical.toLowerCase(), entry);
|
|
228
|
+
for (const entry of overrides) byCanonical.set(entry.canonical.toLowerCase(), entry);
|
|
229
|
+
return [...byCanonical.values()];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/utils/languages.ts
|
|
233
|
+
var KNOWN_LANGUAGES = [
|
|
234
|
+
"english",
|
|
235
|
+
"spanish",
|
|
236
|
+
"french",
|
|
237
|
+
"german",
|
|
238
|
+
"italian",
|
|
239
|
+
"portuguese",
|
|
240
|
+
"dutch",
|
|
241
|
+
"russian",
|
|
242
|
+
"mandarin",
|
|
243
|
+
"chinese",
|
|
244
|
+
"cantonese",
|
|
245
|
+
"japanese",
|
|
246
|
+
"korean",
|
|
247
|
+
"arabic",
|
|
248
|
+
"hindi",
|
|
249
|
+
"polish",
|
|
250
|
+
"turkish",
|
|
251
|
+
"vietnamese",
|
|
252
|
+
"swedish",
|
|
253
|
+
"norwegian",
|
|
254
|
+
"danish",
|
|
255
|
+
"finnish",
|
|
256
|
+
"greek",
|
|
257
|
+
"hebrew",
|
|
258
|
+
"thai",
|
|
259
|
+
"indonesian",
|
|
260
|
+
"ukrainian",
|
|
261
|
+
"czech"
|
|
262
|
+
];
|
|
263
|
+
var LANGUAGE_ALIASES = {
|
|
264
|
+
mandarin: "chinese",
|
|
265
|
+
cantonese: "chinese"
|
|
266
|
+
};
|
|
267
|
+
var LEVEL_RANK = {
|
|
268
|
+
a1: 1,
|
|
269
|
+
a2: 2,
|
|
270
|
+
b1: 3,
|
|
271
|
+
b2: 4,
|
|
272
|
+
c1: 5,
|
|
273
|
+
c2: 6,
|
|
274
|
+
basic: 1,
|
|
275
|
+
elementary: 1,
|
|
276
|
+
limited: 2,
|
|
277
|
+
conversational: 3,
|
|
278
|
+
intermediate: 3,
|
|
279
|
+
professional: 4,
|
|
280
|
+
"upper intermediate": 4,
|
|
281
|
+
advanced: 4,
|
|
282
|
+
fluent: 5,
|
|
283
|
+
native: 6,
|
|
284
|
+
"native speaker": 6,
|
|
285
|
+
bilingual: 6,
|
|
286
|
+
// German
|
|
287
|
+
grundkenntnisse: 1,
|
|
288
|
+
gering: 2,
|
|
289
|
+
gut: 3,
|
|
290
|
+
fortgeschritten: 4,
|
|
291
|
+
flie\u00DFend: 5,
|
|
292
|
+
muttersprache: 6,
|
|
293
|
+
muttersprachler: 6,
|
|
294
|
+
// French
|
|
295
|
+
"d\xE9butant": 1,
|
|
296
|
+
"\xE9l\xE9mentaire": 1,
|
|
297
|
+
"limit\xE9": 2,
|
|
298
|
+
"interm\xE9diaire": 3,
|
|
299
|
+
"avanc\xE9": 4,
|
|
300
|
+
courant: 5,
|
|
301
|
+
natif: 6,
|
|
302
|
+
"langue maternelle": 6,
|
|
303
|
+
bilingue: 6
|
|
304
|
+
};
|
|
305
|
+
var LANGUAGE_GROUP = KNOWN_LANGUAGES.join("|");
|
|
306
|
+
var LEVEL_GROUP = Object.keys(LEVEL_RANK).sort((a, b) => b.length - a.length).map((l) => l.replace(/\s+/g, "\\s+")).join("|");
|
|
307
|
+
var BOUNDARY_START = "(?:^|(?<=[^a-z\xE0-\xFF]))";
|
|
308
|
+
var BOUNDARY_END = "(?:$|(?=[^a-z\xE0-\xFF]))";
|
|
309
|
+
var LANGUAGE_LEVEL_RE = new RegExp(
|
|
310
|
+
`\\b(${LANGUAGE_GROUP})\\b(?:\\s*[\\(:\\-]?\\s*(${BOUNDARY_START}(?:${LEVEL_GROUP})${BOUNDARY_END}|[abc][12]))?`,
|
|
311
|
+
"gi"
|
|
312
|
+
);
|
|
313
|
+
var LEVEL_BEFORE_LANGUAGE_RE = new RegExp(
|
|
314
|
+
`${BOUNDARY_START}(${LEVEL_GROUP})${BOUNDARY_END}\\s+(?:in\\s+)?(${LANGUAGE_GROUP})\\b`,
|
|
315
|
+
"gi"
|
|
316
|
+
);
|
|
317
|
+
function canonicalLanguage(name) {
|
|
318
|
+
const lower = name.toLowerCase();
|
|
319
|
+
return LANGUAGE_ALIASES[lower] ?? lower;
|
|
320
|
+
}
|
|
321
|
+
function toParsedLanguage(name, level) {
|
|
322
|
+
const normalizedLevel = level?.toLowerCase().replace(/\s+/g, " ");
|
|
323
|
+
return {
|
|
324
|
+
name: canonicalLanguage(name),
|
|
325
|
+
level: normalizedLevel,
|
|
326
|
+
levelRank: normalizedLevel ? LEVEL_RANK[normalizedLevel] : void 0
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function parseLanguageMentions(text) {
|
|
330
|
+
const found = /* @__PURE__ */ new Map();
|
|
331
|
+
for (const match of text.matchAll(LANGUAGE_LEVEL_RE)) {
|
|
332
|
+
const parsed = toParsedLanguage(match[1], match[2]);
|
|
333
|
+
const existing = found.get(parsed.name);
|
|
334
|
+
if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
|
|
335
|
+
found.set(parsed.name, parsed);
|
|
193
336
|
}
|
|
194
|
-
|
|
195
|
-
|
|
337
|
+
}
|
|
338
|
+
for (const match of text.matchAll(LEVEL_BEFORE_LANGUAGE_RE)) {
|
|
339
|
+
const parsed = toParsedLanguage(match[2], match[1]);
|
|
340
|
+
const existing = found.get(parsed.name);
|
|
341
|
+
if (!existing || (parsed.levelRank ?? 0) > (existing.levelRank ?? 0)) {
|
|
342
|
+
found.set(parsed.name, parsed);
|
|
196
343
|
}
|
|
197
344
|
}
|
|
198
|
-
return
|
|
345
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
199
346
|
}
|
|
200
|
-
function
|
|
201
|
-
|
|
347
|
+
function diffLanguages(resumeLanguages, requiredLanguages) {
|
|
348
|
+
const byName = new Map(resumeLanguages.map((l) => [l.name, l]));
|
|
349
|
+
const matched = [];
|
|
350
|
+
const missing = [];
|
|
351
|
+
for (const required of requiredLanguages) {
|
|
352
|
+
const have = byName.get(required.name);
|
|
353
|
+
const requiredRank = required.levelRank ?? 0;
|
|
354
|
+
const haveRank = have?.levelRank ?? 0;
|
|
355
|
+
if (have && haveRank >= requiredRank) {
|
|
356
|
+
matched.push(required);
|
|
357
|
+
} else {
|
|
358
|
+
missing.push(required);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { matched, missing };
|
|
202
362
|
}
|
|
203
363
|
|
|
204
364
|
// src/core/parser/jd.parser.ts
|
|
205
365
|
var DEGREE_VARIANTS = [
|
|
206
|
-
[/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc
|
|
207
|
-
[/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc
|
|
208
|
-
[/\b(?:phd|ph\.d\.?|doctorate)\b/i, "phd"],
|
|
366
|
+
[/\b(?:bachelor(?:'s)?|b\.s\.?|bs\.?|bsc\.?|licence)\b/i, "bachelor"],
|
|
367
|
+
[/\b(?:master(?:'s)?|m\.s\.?|ms\.?|msc\.?|diplom)\b/i, "master"],
|
|
368
|
+
[/\b(?:phd|ph\.d\.?|doctorate|doktor|doctorat)\b/i, "phd"],
|
|
209
369
|
[/\bmba\b/i, "mba"],
|
|
210
370
|
[/\bassociate(?:'s)?\b/i, "associate"]
|
|
211
371
|
];
|
|
@@ -228,18 +388,38 @@ function extractPreferredSkills(lines) {
|
|
|
228
388
|
return preferred;
|
|
229
389
|
}
|
|
230
390
|
function extractRoleKeywords(text) {
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
return unique(tokenize(
|
|
391
|
+
const roleMatches = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect|director|consultant|lead|vp)/gi) ?? [];
|
|
392
|
+
const fallback = roleMatches.length === 0 ? [text.split(/\n/)[0] ?? ""] : [];
|
|
393
|
+
return unique(tokenize([...roleMatches, ...fallback].join(" ")));
|
|
234
394
|
}
|
|
235
395
|
function extractMinExperience(text) {
|
|
236
|
-
const match = text.match(/(\d{1,2})\+?\s
|
|
237
|
-
if (match)
|
|
238
|
-
|
|
396
|
+
const match = text.match(/(\d{1,2})\+?\s*(?:years?|yrs\.?|jahre?|ans?|années?)/i);
|
|
397
|
+
if (!match) return void 0;
|
|
398
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
399
|
+
return parsed <= 60 ? parsed : void 0;
|
|
400
|
+
}
|
|
401
|
+
var SURFACE_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/gi;
|
|
402
|
+
function collectKeywordSurfaceForms(rawText, aliases) {
|
|
403
|
+
const matches = rawText.match(SURFACE_TOKEN_RE) ?? [];
|
|
404
|
+
const surfaceForms = {};
|
|
405
|
+
for (const match of matches) {
|
|
406
|
+
const canonical = normalizeSkill(match, aliases);
|
|
407
|
+
if (!(canonical in surfaceForms)) {
|
|
408
|
+
surfaceForms[canonical] = match;
|
|
409
|
+
}
|
|
239
410
|
}
|
|
240
|
-
return
|
|
411
|
+
return surfaceForms;
|
|
412
|
+
}
|
|
413
|
+
var LANG_SECTION_RE = /^\s*(?:languages?|sprache|langue)s?\s*[:\-–—]?\s*/i;
|
|
414
|
+
var LANG_REQUIREMENT_HINT_RE = /\b(fluent|required|must|need|speak|proficient|native|conversational|intermediate|advanced|professional|[abc][12])\b/i;
|
|
415
|
+
function isLanguageRequired(lang, jobDescription) {
|
|
416
|
+
return splitLines(jobDescription).some((line) => {
|
|
417
|
+
const lower = line.toLowerCase();
|
|
418
|
+
if (!lower.includes(lang.name)) return false;
|
|
419
|
+
return LANG_SECTION_RE.test(line) || LANG_REQUIREMENT_HINT_RE.test(line);
|
|
420
|
+
});
|
|
241
421
|
}
|
|
242
|
-
function
|
|
422
|
+
function extractDegreeLevels(text) {
|
|
243
423
|
const found = /* @__PURE__ */ new Set();
|
|
244
424
|
for (const [pattern, canonical] of DEGREE_VARIANTS) {
|
|
245
425
|
if (pattern.test(text)) found.add(canonical);
|
|
@@ -277,7 +457,13 @@ function parseJobDescription(jobDescription, config) {
|
|
|
277
457
|
roleKeywords,
|
|
278
458
|
keywords,
|
|
279
459
|
minExperienceYears: extractMinExperience(jobDescription),
|
|
280
|
-
educationRequirements:
|
|
460
|
+
educationRequirements: extractDegreeLevels(jobDescription),
|
|
461
|
+
keywordSurfaceForms: collectKeywordSurfaceForms(jobDescription, config.skillAliases),
|
|
462
|
+
// A language only counts as required if its mention carries a requirement/level cue
|
|
463
|
+
// or sits in a "Languages:" line — plain references ("our Berlin office") don't count.
|
|
464
|
+
requiredLanguages: parseLanguageMentions(jobDescription).filter(
|
|
465
|
+
(lang) => isLanguageRequired(lang, jobDescription)
|
|
466
|
+
)
|
|
281
467
|
};
|
|
282
468
|
}
|
|
283
469
|
|
|
@@ -306,11 +492,37 @@ var MONTHS = {
|
|
|
306
492
|
nov: 11,
|
|
307
493
|
november: 11,
|
|
308
494
|
dec: 12,
|
|
309
|
-
december: 12
|
|
495
|
+
december: 12,
|
|
496
|
+
// German
|
|
497
|
+
januar: 1,
|
|
498
|
+
j\u00E4nner: 1,
|
|
499
|
+
februar: 2,
|
|
500
|
+
m\u00E4rz: 3,
|
|
501
|
+
maerz: 3,
|
|
502
|
+
mai: 5,
|
|
503
|
+
juni: 6,
|
|
504
|
+
juli: 7,
|
|
505
|
+
oktober: 10,
|
|
506
|
+
dezember: 12,
|
|
507
|
+
// French
|
|
508
|
+
janvier: 1,
|
|
509
|
+
f\u00E9vrier: 2,
|
|
510
|
+
fevrier: 2,
|
|
511
|
+
mars: 3,
|
|
512
|
+
avril: 4,
|
|
513
|
+
juin: 6,
|
|
514
|
+
juillet: 7,
|
|
515
|
+
ao\u00FBt: 8,
|
|
516
|
+
aout: 8,
|
|
517
|
+
septembre: 9,
|
|
518
|
+
octobre: 10,
|
|
519
|
+
novembre: 11,
|
|
520
|
+
d\u00E9cembre: 12,
|
|
521
|
+
decembre: 12
|
|
310
522
|
};
|
|
311
523
|
function parseDateToken(raw) {
|
|
312
524
|
const cleaned = raw.trim().toLowerCase();
|
|
313
|
-
const monthMatch = cleaned.match(/([a-z]{3,9})\s*(\d{4})/i);
|
|
525
|
+
const monthMatch = cleaned.match(/([a-zà-ÿ]{3,9})\s*(\d{4})/i);
|
|
314
526
|
if (monthMatch) {
|
|
315
527
|
const monthName = monthMatch[1].toLowerCase();
|
|
316
528
|
const year = Number.parseInt(monthMatch[2], 10);
|
|
@@ -342,14 +554,14 @@ function monthsBetween(start, end) {
|
|
|
342
554
|
function parseDateRange(text, referenceDate) {
|
|
343
555
|
const normalized = text.trim();
|
|
344
556
|
const rangeMatch = normalized.match(
|
|
345
|
-
/(\d{1,2}\/\d{4}|[A-Za-z]{3,9}\s+\d{4}|\d{4})\s*(?:-|to
|
|
557
|
+
/(\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
|
|
346
558
|
);
|
|
347
559
|
if (!rangeMatch) {
|
|
348
560
|
return null;
|
|
349
561
|
}
|
|
350
562
|
const startToken = parseDateToken(rangeMatch[1]);
|
|
351
563
|
const endRaw = rangeMatch[2];
|
|
352
|
-
const isPresent = /present|current|now/i.test(endRaw);
|
|
564
|
+
const isPresent = /present|current|now|aktuell|heute|actuellement|présent|actuel/i.test(endRaw);
|
|
353
565
|
const endToken = isPresent ? void 0 : parseDateToken(endRaw);
|
|
354
566
|
if (!startToken) {
|
|
355
567
|
return null;
|
|
@@ -403,14 +615,23 @@ function sumExperienceYears(ranges) {
|
|
|
403
615
|
|
|
404
616
|
// src/core/parser/resume.parser.ts
|
|
405
617
|
var SECTION_ALIASES = {
|
|
406
|
-
summary: ["summary", "profile", "about"],
|
|
407
|
-
experience: [
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
618
|
+
summary: ["summary", "profile", "about", "zusammenfassung", "profil", "r\xE9sum\xE9", "\xE0 propos"],
|
|
619
|
+
experience: [
|
|
620
|
+
"experience",
|
|
621
|
+
"work experience",
|
|
622
|
+
"professional experience",
|
|
623
|
+
"employment",
|
|
624
|
+
"erfahrung",
|
|
625
|
+
"berufserfahrung",
|
|
626
|
+
"exp\xE9rience",
|
|
627
|
+
"exp\xE9rience professionnelle"
|
|
628
|
+
],
|
|
629
|
+
skills: ["skills", "technical skills", "technologies", "f\xE4higkeiten", "kenntnisse", "comp\xE9tences"],
|
|
630
|
+
education: ["education", "academics", "academic background", "ausbildung", "formation", "\xE9tudes"],
|
|
631
|
+
projects: ["projects", "portfolio", "projekte", "projets"],
|
|
632
|
+
certifications: ["certifications", "licenses", "zertifizierungen", "certifications professionnelles"]
|
|
412
633
|
};
|
|
413
|
-
var
|
|
634
|
+
var STRONG_VERBS = [
|
|
414
635
|
"led",
|
|
415
636
|
"managed",
|
|
416
637
|
"built",
|
|
@@ -431,6 +652,21 @@ var ACTION_VERBS = [
|
|
|
431
652
|
"reduced",
|
|
432
653
|
"increased"
|
|
433
654
|
];
|
|
655
|
+
var WEAK_VERBS = ["worked", "helped", "performed", "responsible", "assisted", "participated", "involved"];
|
|
656
|
+
var METRIC_RE = /\d|%|\$|\bk\+|\bm\+/i;
|
|
657
|
+
function classifyAchievement(line) {
|
|
658
|
+
const lower = line.toLowerCase();
|
|
659
|
+
const hasMetric = METRIC_RE.test(line);
|
|
660
|
+
const hasStrongVerb = STRONG_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
|
|
661
|
+
const hasWeakVerb = WEAK_VERBS.some((verb) => new RegExp(`\\b${verb}\\b`).test(lower));
|
|
662
|
+
if (hasStrongVerb && hasMetric) {
|
|
663
|
+
return { text: line, strength: "strong", reason: "strong verb + quantified impact" };
|
|
664
|
+
}
|
|
665
|
+
if (hasWeakVerb) {
|
|
666
|
+
return { text: line, strength: "weak", reason: "weak verb" };
|
|
667
|
+
}
|
|
668
|
+
return { text: line, strength: "weak", reason: "no quantified impact" };
|
|
669
|
+
}
|
|
434
670
|
function detectSection(line) {
|
|
435
671
|
const normalized = line.trim().toLowerCase();
|
|
436
672
|
for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
|
|
@@ -471,21 +707,27 @@ function extractSections(text) {
|
|
|
471
707
|
}
|
|
472
708
|
function parseSkills(sectionContent, aliases) {
|
|
473
709
|
if (!sectionContent) return [];
|
|
474
|
-
const
|
|
710
|
+
const hasBullets = /[•·‣▪○●◦]/.test(sectionContent);
|
|
711
|
+
const normalized = hasBullets ? sectionContent.replace(/\n/g, " ") : sectionContent;
|
|
712
|
+
const raw = normalized.split(/[,;\n]|[•·‣▪○●◦]/).map((skill) => skill.trim().replace(/^[-•·‣▪○●◦\s]+|[-•·‣▪○●◦\s]+$/g, "").trim()).filter(Boolean);
|
|
475
713
|
return normalizeSkills(raw, aliases);
|
|
476
714
|
}
|
|
477
715
|
function parseActionVerbs(text) {
|
|
478
716
|
const words = tokenize(text);
|
|
479
|
-
return
|
|
717
|
+
return {
|
|
718
|
+
strong: STRONG_VERBS.filter((verb) => words.includes(verb)),
|
|
719
|
+
weak: WEAK_VERBS.filter((verb) => words.includes(verb))
|
|
720
|
+
};
|
|
480
721
|
}
|
|
481
722
|
function parseExperience(sectionContent, referenceDate) {
|
|
482
723
|
if (!sectionContent) {
|
|
483
|
-
return { entries: [], rangesInMonths: [], jobTitles: [] };
|
|
724
|
+
return { entries: [], rangesInMonths: [], jobTitles: [], achievements: [] };
|
|
484
725
|
}
|
|
485
726
|
const lines = splitLines(sectionContent);
|
|
486
727
|
const entries = [];
|
|
487
728
|
const rangesInMonths = [];
|
|
488
729
|
const jobTitles = [];
|
|
730
|
+
const achievements = [];
|
|
489
731
|
for (const line of lines) {
|
|
490
732
|
const range = parseDateRange(line, referenceDate);
|
|
491
733
|
if (range) {
|
|
@@ -500,20 +742,22 @@ function parseExperience(sectionContent, referenceDate) {
|
|
|
500
742
|
}
|
|
501
743
|
continue;
|
|
502
744
|
}
|
|
503
|
-
const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
|
|
745
|
+
const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|VP|Director|Consultant|Architect|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
|
|
504
746
|
if (titleMatch) {
|
|
505
747
|
const title = titleMatch[0].trim();
|
|
506
748
|
jobTitles.push(title.toLowerCase());
|
|
507
749
|
const entry = { title, description: line };
|
|
508
750
|
entries.push(entry);
|
|
751
|
+
achievements.push(classifyAchievement(line));
|
|
509
752
|
continue;
|
|
510
753
|
}
|
|
511
754
|
if (entries.length > 0) {
|
|
512
755
|
const current = entries[entries.length - 1];
|
|
513
756
|
current.description = [current.description, line].filter(Boolean).join(" ").trim();
|
|
514
757
|
}
|
|
758
|
+
achievements.push(classifyAchievement(line));
|
|
515
759
|
}
|
|
516
|
-
return { entries, rangesInMonths, jobTitles: unique(jobTitles) };
|
|
760
|
+
return { entries, rangesInMonths, jobTitles: unique(jobTitles), achievements };
|
|
517
761
|
}
|
|
518
762
|
function parseEducation(sectionContent) {
|
|
519
763
|
if (!sectionContent) return [];
|
|
@@ -536,7 +780,8 @@ function parseResume(resumeText, config) {
|
|
|
536
780
|
const textToScan = sections.summary ?? normalizedText;
|
|
537
781
|
const yearsMatch = textToScan.match(/(\d{1,2})\+?\s*years?/i);
|
|
538
782
|
if (yearsMatch) {
|
|
539
|
-
|
|
783
|
+
const parsed = Number.parseInt(yearsMatch[1], 10);
|
|
784
|
+
totalExperienceYears = parsed <= 60 ? parsed : 0;
|
|
540
785
|
}
|
|
541
786
|
}
|
|
542
787
|
const requiredSections = ["summary", "experience", "skills", "education"];
|
|
@@ -563,11 +808,14 @@ function parseResume(resumeText, config) {
|
|
|
563
808
|
sectionContent: sections,
|
|
564
809
|
skills,
|
|
565
810
|
jobTitles: experienceData.jobTitles,
|
|
566
|
-
actionVerbs,
|
|
811
|
+
actionVerbs: actionVerbs.strong,
|
|
812
|
+
weakVerbs: actionVerbs.weak,
|
|
813
|
+
achievements: experienceData.achievements,
|
|
567
814
|
educationEntries,
|
|
568
815
|
experience: experienceData.entries,
|
|
569
816
|
totalExperienceYears,
|
|
570
817
|
keywords: collectKeywords(normalizedText),
|
|
818
|
+
languages: parseLanguageMentions(resumeText),
|
|
571
819
|
warnings
|
|
572
820
|
};
|
|
573
821
|
}
|
|
@@ -649,6 +897,14 @@ var REQUIRED_SKILL_WEIGHT = 0.7;
|
|
|
649
897
|
var OPTIONAL_SKILL_WEIGHT = 0.3;
|
|
650
898
|
var EXPERIENCE_YEARS_WEIGHT = 0.75;
|
|
651
899
|
var EXPERIENCE_ROLE_WEIGHT = 0.25;
|
|
900
|
+
var ALL_CATEGORIES = ["technical", "tool", "concept", "soft", "marketing", "domain"];
|
|
901
|
+
function emptyCategoryBuckets() {
|
|
902
|
+
const buckets = {};
|
|
903
|
+
for (const category of ALL_CATEGORIES) {
|
|
904
|
+
buckets[category] = { matched: [], missing: [] };
|
|
905
|
+
}
|
|
906
|
+
return buckets;
|
|
907
|
+
}
|
|
652
908
|
function scoreSkills(resume, job, config) {
|
|
653
909
|
const profileRequired = config.profile?.mandatorySkills ?? [];
|
|
654
910
|
const profileOptional = config.profile?.optionalSkills ?? [];
|
|
@@ -687,42 +943,83 @@ function scoreExperience(resume, job, config) {
|
|
|
687
943
|
const missingYears = Math.max(requiredYears - resume.totalExperienceYears, 0);
|
|
688
944
|
return { score, missingYears: Number(missingYears.toFixed(2)) };
|
|
689
945
|
}
|
|
946
|
+
function keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies) {
|
|
947
|
+
const base = requiredSet.has(keyword) ? 3 : preferredSet.has(keyword) ? 2 : 1;
|
|
948
|
+
const freqBonus = Math.min((jdFrequencies[keyword] ?? 1) - 1, 3) * 0.25;
|
|
949
|
+
return base + freqBonus;
|
|
950
|
+
}
|
|
690
951
|
function scoreKeywords(resume, job, config) {
|
|
691
952
|
const jobKeywordSet = new Set(
|
|
692
953
|
job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
|
|
693
954
|
);
|
|
694
955
|
if (jobKeywordSet.size === 0) {
|
|
695
|
-
return {
|
|
956
|
+
return {
|
|
957
|
+
score: 100,
|
|
958
|
+
matchedKeywords: [],
|
|
959
|
+
missingKeywords: [],
|
|
960
|
+
overusedKeywords: [],
|
|
961
|
+
keywordsByCategory: emptyCategoryBuckets(),
|
|
962
|
+
keywordWeights: []
|
|
963
|
+
};
|
|
696
964
|
}
|
|
697
965
|
const resumeTokens = tokenize(resume.normalizedText).map(
|
|
698
966
|
(t) => normalizeSkill(t, config.skillAliases)
|
|
699
967
|
);
|
|
700
968
|
const resumeTokenSet = new Set(resumeTokens);
|
|
969
|
+
const resumeFrequencies = countFrequencies(resumeTokens);
|
|
970
|
+
const requiredSet = new Set(job.requiredSkills);
|
|
971
|
+
const preferredSet = new Set(job.preferredSkills);
|
|
972
|
+
const jdFrequencies = countFrequencies(
|
|
973
|
+
tokenize(job.normalizedText).map((t) => normalizeSkill(t, config.skillAliases))
|
|
974
|
+
);
|
|
975
|
+
const weightOf = (keyword) => keywordWeightOf(keyword, requiredSet, preferredSet, jdFrequencies);
|
|
701
976
|
const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
|
|
702
977
|
const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
|
|
703
|
-
const
|
|
704
|
-
const
|
|
705
|
-
const
|
|
978
|
+
const totalWeight = [...jobKeywordSet].reduce((sum, keyword) => sum + weightOf(keyword), 0);
|
|
979
|
+
const matchedWeight = matchedKeywords.reduce((sum, keyword) => sum + weightOf(keyword), 0);
|
|
980
|
+
const score = clamp(matchedWeight / totalWeight * 100, 0, 100);
|
|
706
981
|
const totalTokens = resumeTokens.length || 1;
|
|
707
982
|
const overusedKeywords = matchedKeywords.filter((keyword) => {
|
|
708
|
-
const density = (
|
|
983
|
+
const density = (resumeFrequencies[keyword] ?? 0) / totalTokens;
|
|
709
984
|
return density > config.keywordDensity.max;
|
|
710
985
|
});
|
|
986
|
+
const keywordsByCategory = emptyCategoryBuckets();
|
|
987
|
+
for (const keyword of matchedKeywords) {
|
|
988
|
+
keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].matched.push(keyword);
|
|
989
|
+
}
|
|
990
|
+
for (const keyword of missingKeywords) {
|
|
991
|
+
keywordsByCategory[config.categoryIndex.get(keyword) ?? "technical"].missing.push(keyword);
|
|
992
|
+
}
|
|
993
|
+
for (const bucket of Object.values(keywordsByCategory)) {
|
|
994
|
+
bucket.matched.sort();
|
|
995
|
+
bucket.missing.sort();
|
|
996
|
+
}
|
|
997
|
+
const keywordWeights = [...jobKeywordSet].map((term) => {
|
|
998
|
+
const weight = Number(weightOf(term).toFixed(2));
|
|
999
|
+
return {
|
|
1000
|
+
term,
|
|
1001
|
+
category: config.categoryIndex.get(term) ?? "technical",
|
|
1002
|
+
jdWeight: weight,
|
|
1003
|
+
resumeWeight: resumeFrequencies[term] ?? 0,
|
|
1004
|
+
importance: weight
|
|
1005
|
+
};
|
|
1006
|
+
}).sort((a, b) => a.term.localeCompare(b.term));
|
|
711
1007
|
return {
|
|
712
1008
|
score,
|
|
713
1009
|
matchedKeywords: unique(matchedKeywords).sort(),
|
|
714
1010
|
missingKeywords: unique(missingKeywords).sort(),
|
|
715
|
-
overusedKeywords: unique(overusedKeywords).sort()
|
|
1011
|
+
overusedKeywords: unique(overusedKeywords).sort(),
|
|
1012
|
+
keywordsByCategory,
|
|
1013
|
+
keywordWeights
|
|
716
1014
|
};
|
|
717
1015
|
}
|
|
718
1016
|
function scoreEducation(resume, job) {
|
|
719
1017
|
if (job.educationRequirements.length === 0) {
|
|
720
1018
|
return 100;
|
|
721
1019
|
}
|
|
722
|
-
const
|
|
723
|
-
const normalizedEducation = resumeEducationText.toLowerCase();
|
|
1020
|
+
const resumeDegreeLevels = extractDegreeLevels(resume.educationEntries.join(" "));
|
|
724
1021
|
const matched = job.educationRequirements.filter(
|
|
725
|
-
(requirement) =>
|
|
1022
|
+
(requirement) => resumeDegreeLevels.includes(requirement)
|
|
726
1023
|
);
|
|
727
1024
|
if (matched.length === 0) {
|
|
728
1025
|
return 0;
|
|
@@ -741,6 +1038,14 @@ function calculateScore(resume, job, config) {
|
|
|
741
1038
|
education: educationScore
|
|
742
1039
|
};
|
|
743
1040
|
const weightedScore = breakdown.skills * config.weights.skills + breakdown.experience * config.weights.experience + breakdown.keywords * config.weights.keywords + breakdown.education * config.weights.education;
|
|
1041
|
+
const achievementStrength = {
|
|
1042
|
+
strong: resume.achievements.filter((a) => a.strength === "strong").length,
|
|
1043
|
+
weak: resume.achievements.filter((a) => a.strength === "weak").length
|
|
1044
|
+
};
|
|
1045
|
+
const { matched: matchedLanguages, missing: missingLanguages } = diffLanguages(
|
|
1046
|
+
resume.languages,
|
|
1047
|
+
job.requiredLanguages
|
|
1048
|
+
);
|
|
744
1049
|
return {
|
|
745
1050
|
score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
|
|
746
1051
|
breakdown,
|
|
@@ -749,6 +1054,11 @@ function calculateScore(resume, job, config) {
|
|
|
749
1054
|
matchedKeywords: keywordResult.matchedKeywords,
|
|
750
1055
|
missingKeywords: keywordResult.missingKeywords,
|
|
751
1056
|
overusedKeywords: keywordResult.overusedKeywords,
|
|
1057
|
+
keywordsByCategory: keywordResult.keywordsByCategory,
|
|
1058
|
+
keywordWeights: keywordResult.keywordWeights,
|
|
1059
|
+
achievementStrength,
|
|
1060
|
+
matchedLanguages,
|
|
1061
|
+
missingLanguages,
|
|
752
1062
|
suggestions: [],
|
|
753
1063
|
warnings: [],
|
|
754
1064
|
// detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
|
|
@@ -762,49 +1072,195 @@ function calculateScore(resume, job, config) {
|
|
|
762
1072
|
}
|
|
763
1073
|
|
|
764
1074
|
// src/profiles/index.ts
|
|
765
|
-
var
|
|
1075
|
+
var defaultKeywordRegistry = [
|
|
1076
|
+
// languages / frameworks
|
|
766
1077
|
// ponytail: "node" split from javascript — Node.js runtime !== JS language
|
|
767
|
-
javascript: ["js"],
|
|
768
|
-
node: ["node.js", "nodejs"],
|
|
769
|
-
typescript: ["ts"],
|
|
770
|
-
react: ["reactjs", "react.js"],
|
|
771
|
-
"
|
|
772
|
-
"
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
"
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
1078
|
+
{ canonical: "javascript", aliases: ["js"], category: "technical" },
|
|
1079
|
+
{ canonical: "node", aliases: ["node.js", "nodejs"], category: "technical" },
|
|
1080
|
+
{ canonical: "typescript", aliases: ["ts"], category: "technical" },
|
|
1081
|
+
{ canonical: "react", aliases: ["reactjs", "react.js"], category: "technical" },
|
|
1082
|
+
{ canonical: "angular", aliases: ["angularjs"], category: "technical" },
|
|
1083
|
+
{ canonical: "vue", aliases: ["vue.js", "vuejs"], category: "technical" },
|
|
1084
|
+
{ canonical: "svelte", aliases: [], category: "technical" },
|
|
1085
|
+
{ canonical: "next.js", aliases: ["nextjs"], category: "technical" },
|
|
1086
|
+
{ canonical: "c++", aliases: ["cpp"], category: "technical" },
|
|
1087
|
+
{ canonical: "c#", aliases: ["csharp", ".net"], category: "technical" },
|
|
1088
|
+
{ canonical: "java", aliases: [], category: "technical" },
|
|
1089
|
+
{ canonical: "python", aliases: ["py"], category: "technical" },
|
|
1090
|
+
{ canonical: "go", aliases: ["golang"], category: "technical" },
|
|
1091
|
+
{ canonical: "rust", aliases: [], category: "technical" },
|
|
1092
|
+
{ canonical: "ruby", aliases: ["ruby on rails", "rails"], category: "technical" },
|
|
1093
|
+
{ canonical: "php", aliases: [], category: "technical" },
|
|
1094
|
+
{ canonical: "swift", aliases: [], category: "technical" },
|
|
1095
|
+
{ canonical: "kotlin", aliases: [], category: "technical" },
|
|
1096
|
+
{ canonical: "scala", aliases: [], category: "technical" },
|
|
1097
|
+
{ canonical: "html", aliases: ["html5"], category: "technical" },
|
|
1098
|
+
{ canonical: "css", aliases: ["css3"], category: "technical" },
|
|
1099
|
+
{ canonical: "ios development", aliases: ["ios"], category: "technical" },
|
|
1100
|
+
{ canonical: "android development", aliases: ["android"], category: "technical" },
|
|
1101
|
+
{ canonical: "react native", aliases: [], category: "technical" },
|
|
1102
|
+
{ canonical: "flutter", aliases: [], category: "technical" },
|
|
1103
|
+
{ canonical: "machine learning", aliases: ["ml"], category: "technical" },
|
|
1104
|
+
{ canonical: "deep learning", aliases: [], category: "technical" },
|
|
1105
|
+
{ canonical: "natural language processing", aliases: ["nlp"], category: "technical" },
|
|
1106
|
+
// tools / platforms / infra
|
|
1107
|
+
{ canonical: "sql", aliases: ["postgres", "mysql", "sqlite"], category: "tool" },
|
|
1108
|
+
{ canonical: "graphql", aliases: ["gql"], category: "tool" },
|
|
1109
|
+
{ canonical: "aws", aliases: ["amazon web services"], category: "tool" },
|
|
1110
|
+
{ canonical: "azure", aliases: ["microsoft azure"], category: "tool" },
|
|
1111
|
+
{ canonical: "gcp", aliases: ["google cloud", "google cloud platform"], category: "tool" },
|
|
1112
|
+
{ canonical: "docker", aliases: ["containers"], category: "tool" },
|
|
1113
|
+
{ canonical: "kubernetes", aliases: ["k8s"], category: "tool" },
|
|
1114
|
+
{ canonical: "terraform", aliases: [], category: "tool" },
|
|
1115
|
+
{ canonical: "ansible", aliases: [], category: "tool" },
|
|
1116
|
+
{ canonical: "jenkins", aliases: [], category: "tool" },
|
|
1117
|
+
{ canonical: "git", aliases: ["github", "gitlab"], category: "tool" },
|
|
1118
|
+
{ canonical: "jira", aliases: [], category: "tool" },
|
|
1119
|
+
{ canonical: "confluence", aliases: [], category: "tool" },
|
|
1120
|
+
{ canonical: "pytorch", aliases: ["torch"], category: "tool" },
|
|
1121
|
+
{ canonical: "tensorflow", aliases: ["tf"], category: "tool" },
|
|
1122
|
+
{ canonical: "scikit-learn", aliases: ["sklearn"], category: "tool" },
|
|
1123
|
+
{ canonical: "pandas", aliases: [], category: "tool" },
|
|
1124
|
+
{ canonical: "numpy", aliases: [], category: "tool" },
|
|
1125
|
+
{ canonical: "fastapi", aliases: [], category: "tool" },
|
|
1126
|
+
{ canonical: "flask", aliases: [], category: "tool" },
|
|
1127
|
+
{ canonical: "django", aliases: [], category: "tool" },
|
|
1128
|
+
{ canonical: "kafka", aliases: [], category: "tool" },
|
|
1129
|
+
{ canonical: "redis", aliases: [], category: "tool" },
|
|
1130
|
+
{ canonical: "elasticsearch", aliases: ["elastic"], category: "tool" },
|
|
1131
|
+
{ canonical: "spark", aliases: ["apache spark"], category: "tool" },
|
|
1132
|
+
{ canonical: "tableau", aliases: [], category: "tool" },
|
|
1133
|
+
{ canonical: "power bi", aliases: ["powerbi"], category: "tool" },
|
|
1134
|
+
{ canonical: "excel", aliases: ["microsoft excel", "ms excel"], category: "tool" },
|
|
1135
|
+
{ canonical: "salesforce", aliases: [], category: "tool" },
|
|
1136
|
+
{ canonical: "hubspot", aliases: [], category: "tool" },
|
|
1137
|
+
{ canonical: "sap", aliases: [], category: "tool" },
|
|
1138
|
+
{ canonical: "quickbooks", aliases: [], category: "tool" },
|
|
1139
|
+
{ canonical: "workday", aliases: [], category: "tool" },
|
|
1140
|
+
{ canonical: "zendesk", aliases: [], category: "tool" },
|
|
1141
|
+
{ canonical: "servicenow", aliases: [], category: "tool" },
|
|
1142
|
+
{ canonical: "figma", aliases: [], category: "tool" },
|
|
1143
|
+
{ canonical: "photoshop", aliases: ["adobe photoshop"], category: "tool" },
|
|
1144
|
+
{ canonical: "illustrator", aliases: ["adobe illustrator"], category: "tool" },
|
|
1145
|
+
{ canonical: "autocad", aliases: [], category: "tool" },
|
|
1146
|
+
// engineering concepts
|
|
1147
|
+
{ canonical: "accessibility", aliases: ["a11y"], category: "concept" },
|
|
1148
|
+
{ canonical: "frontend", aliases: ["front-end"], category: "concept" },
|
|
1149
|
+
{ canonical: "backend", aliases: ["back-end"], category: "concept" },
|
|
1150
|
+
{ canonical: "security", aliases: ["cybersecurity"], category: "concept" },
|
|
1151
|
+
{ canonical: "testing", aliases: ["unittest", "pytest"], category: "concept" },
|
|
1152
|
+
{ canonical: "microservices", aliases: [], category: "concept" },
|
|
1153
|
+
{ canonical: "agile", aliases: ["scrum"], category: "concept" },
|
|
1154
|
+
{ canonical: "kanban", aliases: [], category: "concept" },
|
|
1155
|
+
{ canonical: "blockchain", aliases: [], category: "concept" },
|
|
1156
|
+
{ canonical: "devops", aliases: [], category: "concept" },
|
|
1157
|
+
{ canonical: "ci/cd", aliases: ["continuous integration", "continuous deployment"], category: "concept" },
|
|
1158
|
+
{ canonical: "rest api", aliases: ["restful api", "rest apis"], category: "concept" },
|
|
1159
|
+
{ canonical: "design patterns", aliases: [], category: "concept" },
|
|
1160
|
+
{ canonical: "data structures", aliases: [], category: "concept" },
|
|
1161
|
+
{ canonical: "algorithms", aliases: [], category: "concept" },
|
|
1162
|
+
{ canonical: "cloud computing", aliases: [], category: "concept" },
|
|
1163
|
+
{ canonical: "system design", aliases: [], category: "concept" },
|
|
1164
|
+
{ canonical: "tdd", aliases: ["test driven development", "test-driven development"], category: "concept" },
|
|
1165
|
+
{ canonical: "ux design", aliases: ["user experience"], category: "concept" },
|
|
1166
|
+
{ canonical: "ui design", aliases: ["user interface design"], category: "concept" },
|
|
1167
|
+
{ canonical: "project management", aliases: [], category: "concept" },
|
|
1168
|
+
{ canonical: "change management", aliases: [], category: "concept" },
|
|
1169
|
+
{ canonical: "risk management", aliases: [], category: "concept" },
|
|
1170
|
+
{ canonical: "quality assurance", aliases: ["qa"], category: "concept" },
|
|
1171
|
+
// product / data domain
|
|
1172
|
+
{ canonical: "roadmap", aliases: [], category: "domain" },
|
|
1173
|
+
{ canonical: "stakeholder management", aliases: [], category: "domain" },
|
|
1174
|
+
{ canonical: "prioritization", aliases: [], category: "domain" },
|
|
1175
|
+
{ canonical: "a/b testing", aliases: ["ab testing"], category: "domain" },
|
|
1176
|
+
{ canonical: "analytics", aliases: [], category: "domain" },
|
|
1177
|
+
{ canonical: "statistics", aliases: ["stats"], category: "domain" },
|
|
1178
|
+
{ canonical: "data visualization", aliases: [], category: "domain" },
|
|
1179
|
+
// finance / accounting domain
|
|
1180
|
+
{ canonical: "financial analysis", aliases: [], category: "domain" },
|
|
1181
|
+
{ canonical: "budgeting", aliases: [], category: "domain" },
|
|
1182
|
+
{ canonical: "forecasting", aliases: [], category: "domain" },
|
|
1183
|
+
{ canonical: "bookkeeping", aliases: [], category: "domain" },
|
|
1184
|
+
{ canonical: "accounts payable", aliases: ["ap"], category: "domain" },
|
|
1185
|
+
{ canonical: "accounts receivable", aliases: ["ar"], category: "domain" },
|
|
1186
|
+
{ canonical: "payroll", aliases: [], category: "domain" },
|
|
1187
|
+
{ canonical: "auditing", aliases: ["audit"], category: "domain" },
|
|
1188
|
+
{ canonical: "tax preparation", aliases: [], category: "domain" },
|
|
1189
|
+
{ canonical: "gaap", aliases: [], category: "domain" },
|
|
1190
|
+
// sales / account management domain
|
|
1191
|
+
{ canonical: "lead generation", aliases: [], category: "domain" },
|
|
1192
|
+
{ canonical: "account management", aliases: [], category: "domain" },
|
|
1193
|
+
{ canonical: "crm", aliases: ["customer relationship management"], category: "domain" },
|
|
1194
|
+
{ canonical: "sales pipeline", aliases: [], category: "domain" },
|
|
1195
|
+
{ canonical: "cold calling", aliases: [], category: "domain" },
|
|
1196
|
+
{ canonical: "upselling", aliases: ["cross-selling"], category: "domain" },
|
|
1197
|
+
{ canonical: "customer retention", aliases: [], category: "domain" },
|
|
1198
|
+
// human resources domain
|
|
1199
|
+
{ canonical: "recruiting", aliases: ["talent acquisition"], category: "domain" },
|
|
1200
|
+
{ canonical: "onboarding", aliases: [], category: "domain" },
|
|
1201
|
+
{ canonical: "employee relations", aliases: [], category: "domain" },
|
|
1202
|
+
{ canonical: "benefits administration", aliases: [], category: "domain" },
|
|
1203
|
+
{ canonical: "performance management", aliases: [], category: "domain" },
|
|
1204
|
+
// healthcare domain
|
|
1205
|
+
{ canonical: "patient care", aliases: [], category: "domain" },
|
|
1206
|
+
{ canonical: "clinical documentation", aliases: [], category: "domain" },
|
|
1207
|
+
{ canonical: "hipaa", aliases: [], category: "domain" },
|
|
1208
|
+
{ canonical: "electronic health records", aliases: ["ehr", "emr"], category: "domain" },
|
|
1209
|
+
{ canonical: "medical billing", aliases: [], category: "domain" },
|
|
1210
|
+
// legal domain
|
|
1211
|
+
{ canonical: "contract review", aliases: [], category: "domain" },
|
|
1212
|
+
{ canonical: "legal research", aliases: [], category: "domain" },
|
|
1213
|
+
{ canonical: "litigation", aliases: [], category: "domain" },
|
|
1214
|
+
{ canonical: "regulatory compliance", aliases: ["compliance"], category: "domain" },
|
|
1215
|
+
{ canonical: "due diligence", aliases: [], category: "domain" },
|
|
1216
|
+
// education domain
|
|
1217
|
+
{ canonical: "curriculum development", aliases: [], category: "domain" },
|
|
1218
|
+
{ canonical: "lesson planning", aliases: [], category: "domain" },
|
|
1219
|
+
{ canonical: "classroom management", aliases: [], category: "domain" },
|
|
1220
|
+
{ canonical: "instructional design", aliases: [], category: "domain" },
|
|
1221
|
+
// operations / supply chain domain
|
|
1222
|
+
{ canonical: "supply chain management", aliases: ["supply chain"], category: "domain" },
|
|
1223
|
+
{ canonical: "inventory management", aliases: [], category: "domain" },
|
|
1224
|
+
{ canonical: "procurement", aliases: [], category: "domain" },
|
|
1225
|
+
{ canonical: "vendor management", aliases: [], category: "domain" },
|
|
1226
|
+
{ canonical: "logistics", aliases: [], category: "domain" },
|
|
1227
|
+
// customer service domain
|
|
1228
|
+
{ canonical: "customer support", aliases: ["customer service"], category: "domain" },
|
|
1229
|
+
{ canonical: "technical support", aliases: [], category: "domain" },
|
|
1230
|
+
{ canonical: "conflict resolution", aliases: [], category: "domain" },
|
|
1231
|
+
// soft skills
|
|
1232
|
+
{ canonical: "communication", aliases: [], category: "soft" },
|
|
1233
|
+
{ canonical: "leadership", aliases: [], category: "soft" },
|
|
1234
|
+
{ canonical: "teamwork", aliases: ["collaboration"], category: "soft" },
|
|
1235
|
+
{ canonical: "problem solving", aliases: ["problem-solving"], category: "soft" },
|
|
1236
|
+
{ canonical: "adaptability", aliases: ["flexibility"], category: "soft" },
|
|
1237
|
+
{ canonical: "time management", aliases: [], category: "soft" },
|
|
1238
|
+
{ canonical: "critical thinking", aliases: [], category: "soft" },
|
|
1239
|
+
{ canonical: "creativity", aliases: [], category: "soft" },
|
|
1240
|
+
{ canonical: "attention to detail", aliases: [], category: "soft" },
|
|
1241
|
+
{ canonical: "decision making", aliases: ["decision-making"], category: "soft" },
|
|
1242
|
+
{ canonical: "emotional intelligence", aliases: [], category: "soft" },
|
|
1243
|
+
{ canonical: "negotiation", aliases: [], category: "soft" },
|
|
1244
|
+
{ canonical: "organization", aliases: ["organizational skills"], category: "soft" },
|
|
1245
|
+
{ canonical: "public speaking", aliases: ["presentation skills"], category: "soft" },
|
|
1246
|
+
{ canonical: "mentoring", aliases: ["coaching"], category: "soft" },
|
|
1247
|
+
{ canonical: "interpersonal skills", aliases: [], category: "soft" },
|
|
1248
|
+
{ canonical: "work ethic", aliases: [], category: "soft" },
|
|
1249
|
+
// marketing
|
|
1250
|
+
{ canonical: "seo", aliases: ["search engine optimization"], category: "marketing" },
|
|
1251
|
+
{ canonical: "branding", aliases: ["brand strategy"], category: "marketing" },
|
|
1252
|
+
{ canonical: "campaign management", aliases: [], category: "marketing" },
|
|
1253
|
+
{ canonical: "content marketing", aliases: [], category: "marketing" },
|
|
1254
|
+
{ canonical: "social media marketing", aliases: ["social media"], category: "marketing" },
|
|
1255
|
+
{ canonical: "email marketing", aliases: [], category: "marketing" },
|
|
1256
|
+
{ canonical: "digital marketing", aliases: [], category: "marketing" },
|
|
1257
|
+
{ canonical: "copywriting", aliases: [], category: "marketing" },
|
|
1258
|
+
{ canonical: "market research", aliases: [], category: "marketing" },
|
|
1259
|
+
{ canonical: "ppc", aliases: ["pay-per-click", "google ads"], category: "marketing" },
|
|
1260
|
+
{ canonical: "conversion rate optimization", aliases: ["cro"], category: "marketing" },
|
|
1261
|
+
{ canonical: "public relations", aliases: ["pr"], category: "marketing" }
|
|
1262
|
+
];
|
|
1263
|
+
var defaultSkillAliases = deriveSkillAliases(defaultKeywordRegistry);
|
|
808
1264
|
var softwareEngineerProfile = {
|
|
809
1265
|
name: "software-engineer",
|
|
810
1266
|
mandatorySkills: ["javascript", "typescript", "react", "node"],
|
|
@@ -874,9 +1330,12 @@ function resolveConfig(config = {}) {
|
|
|
874
1330
|
keywords: config.weights?.keywords ?? DEFAULT_WEIGHTS.keywords,
|
|
875
1331
|
education: config.weights?.education ?? DEFAULT_WEIGHTS.education
|
|
876
1332
|
};
|
|
1333
|
+
const keywordRegistry = mergeKeywordRegistries(defaultKeywordRegistry, config.keywordRegistry ?? []);
|
|
877
1334
|
const resolved = {
|
|
878
1335
|
weights: normalizeWeights(weights),
|
|
879
|
-
skillAliases: { ...
|
|
1336
|
+
skillAliases: { ...deriveSkillAliases(keywordRegistry), ...config.skillAliases ?? {} },
|
|
1337
|
+
keywordRegistry,
|
|
1338
|
+
categoryIndex: buildCategoryIndex(keywordRegistry),
|
|
880
1339
|
profile: config.profile ?? softwareEngineerProfile,
|
|
881
1340
|
rules: config.rules ?? [],
|
|
882
1341
|
keywordDensity: config.keywordDensity ?? DEFAULT_KEYWORD_DENSITY,
|
|
@@ -896,6 +1355,18 @@ function formatList(values, max = 6) {
|
|
|
896
1355
|
const trimmed = uniqueValues.slice(0, max);
|
|
897
1356
|
return trimmed.join(", ") + (uniqueValues.length > max ? "..." : "");
|
|
898
1357
|
}
|
|
1358
|
+
function buildAliasReplacementSuggestions(resume, job, config) {
|
|
1359
|
+
const jobKeywordSet = new Set(job.keywords.map((k) => normalizeSkill(k, config.skillAliases)));
|
|
1360
|
+
const replacements = [];
|
|
1361
|
+
for (const token of unique(tokenize(resume.normalizedText))) {
|
|
1362
|
+
const canonical = normalizeSkill(token, config.skillAliases);
|
|
1363
|
+
const jdSurface = job.keywordSurfaceForms[canonical];
|
|
1364
|
+
if (jdSurface && jobKeywordSet.has(canonical) && jdSurface.toLowerCase() !== token.toLowerCase()) {
|
|
1365
|
+
replacements.push(`Replace "${token}" with "${jdSurface}" to match the job description's wording.`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return unique(replacements).slice(0, 5);
|
|
1369
|
+
}
|
|
899
1370
|
var SuggestionEngine = class {
|
|
900
1371
|
generate(input) {
|
|
901
1372
|
const suggestions = [];
|
|
@@ -910,6 +1381,7 @@ var SuggestionEngine = class {
|
|
|
910
1381
|
`Incorporate job-specific keywords: ${formatList(input.score.missingKeywords)}`
|
|
911
1382
|
);
|
|
912
1383
|
}
|
|
1384
|
+
suggestions.push(...buildAliasReplacementSuggestions(input.resume, input.job, input.config));
|
|
913
1385
|
if (input.score.overusedKeywords.length > 0) {
|
|
914
1386
|
suggestions.push(
|
|
915
1387
|
`Avoid keyword stuffing for: ${formatList(input.score.overusedKeywords)}`
|
|
@@ -930,6 +1402,21 @@ var SuggestionEngine = class {
|
|
|
930
1402
|
"Strengthen bullet points with impact verbs (led, built, improved, delivered)."
|
|
931
1403
|
);
|
|
932
1404
|
}
|
|
1405
|
+
if (input.resume.weakVerbs.length > 0) {
|
|
1406
|
+
suggestions.push(
|
|
1407
|
+
`Replace weak verbs (${formatList(input.resume.weakVerbs)}) with stronger ones (e.g. led, built, optimized).`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
if (input.score.missingLanguages.length > 0) {
|
|
1411
|
+
const formatted = input.score.missingLanguages.map((l) => l.level ? `${l.name} (${l.level})` : l.name).join(", ");
|
|
1412
|
+
suggestions.push(`Mention your proficiency in: ${formatted}`);
|
|
1413
|
+
}
|
|
1414
|
+
const weakAchievement = input.resume.achievements.find((a) => a.strength === "weak");
|
|
1415
|
+
if (weakAchievement) {
|
|
1416
|
+
suggestions.push(
|
|
1417
|
+
`Strengthen "${weakAchievement.text}" \u2014 add scope/metrics, e.g. "Built and maintained scalable services handling 500k+ requests/day."`
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
933
1420
|
if (input.resume.detectedSections.length < 2 && input.resume.raw.trim().length > 300) {
|
|
934
1421
|
suggestions.push(
|
|
935
1422
|
"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."
|
|
@@ -1572,7 +2059,8 @@ function analyzeResume(input) {
|
|
|
1572
2059
|
resume: parsedResume,
|
|
1573
2060
|
job: parsedJob,
|
|
1574
2061
|
score: scoring,
|
|
1575
|
-
ruleWarnings: ruleResult.warnings
|
|
2062
|
+
ruleWarnings: ruleResult.warnings,
|
|
2063
|
+
config: resolvedConfig
|
|
1576
2064
|
});
|
|
1577
2065
|
let suggestions = suggestionResult.suggestions;
|
|
1578
2066
|
const llmWarnings = [];
|
|
@@ -1592,6 +2080,11 @@ function analyzeResume(input) {
|
|
|
1592
2080
|
matchedKeywords: scoring.matchedKeywords,
|
|
1593
2081
|
missingKeywords: scoring.missingKeywords,
|
|
1594
2082
|
overusedKeywords: scoring.overusedKeywords,
|
|
2083
|
+
keywordsByCategory: scoring.keywordsByCategory,
|
|
2084
|
+
keywordWeights: scoring.keywordWeights,
|
|
2085
|
+
achievementStrength: scoring.achievementStrength,
|
|
2086
|
+
matchedLanguages: scoring.matchedLanguages,
|
|
2087
|
+
missingLanguages: scoring.missingLanguages,
|
|
1595
2088
|
experienceGap: scoring.experienceGap,
|
|
1596
2089
|
detectedSections: parsedResume.detectedSections,
|
|
1597
2090
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
@@ -1634,7 +2127,8 @@ async function analyzeResumeAsync(input) {
|
|
|
1634
2127
|
resume: parsedResume,
|
|
1635
2128
|
job: parsedJob,
|
|
1636
2129
|
score: scoring,
|
|
1637
|
-
ruleWarnings: ruleResult.warnings
|
|
2130
|
+
ruleWarnings: ruleResult.warnings,
|
|
2131
|
+
config: resolvedConfig
|
|
1638
2132
|
});
|
|
1639
2133
|
let suggestions = suggestionResult.suggestions;
|
|
1640
2134
|
const llmWarnings = [];
|
|
@@ -1657,6 +2151,11 @@ async function analyzeResumeAsync(input) {
|
|
|
1657
2151
|
matchedKeywords: scoring.matchedKeywords,
|
|
1658
2152
|
missingKeywords: scoring.missingKeywords,
|
|
1659
2153
|
overusedKeywords: scoring.overusedKeywords,
|
|
2154
|
+
keywordsByCategory: scoring.keywordsByCategory,
|
|
2155
|
+
keywordWeights: scoring.keywordWeights,
|
|
2156
|
+
achievementStrength: scoring.achievementStrength,
|
|
2157
|
+
matchedLanguages: scoring.matchedLanguages,
|
|
2158
|
+
missingLanguages: scoring.missingLanguages,
|
|
1660
2159
|
experienceGap: scoring.experienceGap,
|
|
1661
2160
|
detectedSections: parsedResume.detectedSections,
|
|
1662
2161
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
@@ -1712,10 +2211,11 @@ exports.adaptSuggestionEnhancementResponse = adaptSuggestionEnhancementResponse;
|
|
|
1712
2211
|
exports.analyzeResume = analyzeResume;
|
|
1713
2212
|
exports.analyzeResumeAsync = analyzeResumeAsync;
|
|
1714
2213
|
exports.createPrompt = createPrompt;
|
|
2214
|
+
exports.defaultKeywordRegistry = defaultKeywordRegistry;
|
|
1715
2215
|
exports.defaultProfiles = defaultProfiles;
|
|
1716
2216
|
exports.defaultSkillAliases = defaultSkillAliases;
|
|
1717
2217
|
exports.safeExtractArray = safeExtractArray;
|
|
1718
2218
|
exports.safeExtractNumber = safeExtractNumber;
|
|
1719
2219
|
exports.safeExtractString = safeExtractString;
|
|
1720
|
-
//# sourceMappingURL=index.
|
|
1721
|
-
//# sourceMappingURL=index.
|
|
2220
|
+
//# sourceMappingURL=index.cjs.map
|
|
2221
|
+
//# sourceMappingURL=index.cjs.map
|