@pranavraut033/ats-checker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,757 @@
1
+ 'use strict';
2
+
3
+ // src/utils/text.ts
4
+ var STOP_WORDS = /* @__PURE__ */ new Set([
5
+ "the",
6
+ "and",
7
+ "or",
8
+ "a",
9
+ "an",
10
+ "of",
11
+ "for",
12
+ "to",
13
+ "with",
14
+ "in",
15
+ "on",
16
+ "at",
17
+ "by",
18
+ "from",
19
+ "as",
20
+ "is",
21
+ "are",
22
+ "be",
23
+ "this",
24
+ "that",
25
+ "it",
26
+ "was",
27
+ "were",
28
+ "will",
29
+ "can",
30
+ "should",
31
+ "must",
32
+ "have",
33
+ "has",
34
+ "had"
35
+ ]);
36
+ function normalizeWhitespace(text) {
37
+ return text.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim();
38
+ }
39
+ function normalizeForComparison(text) {
40
+ return normalizeWhitespace(text).toLowerCase();
41
+ }
42
+ function splitLines(text) {
43
+ return text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean);
44
+ }
45
+ function tokenize(text) {
46
+ return normalizeForComparison(text).split(/[^a-z0-9+]+/i).map((word) => word.trim()).filter((word) => word.length > 1 && !STOP_WORDS.has(word));
47
+ }
48
+ function unique(values) {
49
+ const seen = /* @__PURE__ */ new Set();
50
+ const output = [];
51
+ for (const value of values) {
52
+ const lower = value.toLowerCase();
53
+ if (!seen.has(lower)) {
54
+ seen.add(lower);
55
+ output.push(value);
56
+ }
57
+ }
58
+ return output;
59
+ }
60
+ function clamp(value, min, max) {
61
+ return Math.min(Math.max(value, min), max);
62
+ }
63
+ function countFrequencies(values) {
64
+ const counts = {};
65
+ for (const value of values) {
66
+ counts[value] = (counts[value] ?? 0) + 1;
67
+ }
68
+ return counts;
69
+ }
70
+ function containsTableLikeStructure(text) {
71
+ const lines = splitLines(text);
72
+ let tableLines = 0;
73
+ for (const line of lines) {
74
+ const hasPipeColumns = line.includes("|") && line.split("|").length >= 3;
75
+ const hasTabColumns = /\t.+\t/.test(line);
76
+ const hasAlignedSpaces = /( {3,})(\S+)( {3,}\S+)/.test(line);
77
+ if (hasPipeColumns || hasTabColumns || hasAlignedSpaces) {
78
+ tableLines += 1;
79
+ }
80
+ }
81
+ return tableLines >= 2;
82
+ }
83
+
84
+ // src/utils/skills.ts
85
+ function normalizeSkill(skill, aliases) {
86
+ const normalized = skill.trim().toLowerCase();
87
+ for (const [canonical, aliasList] of Object.entries(aliases)) {
88
+ if (canonical.toLowerCase() === normalized) {
89
+ return canonical.toLowerCase();
90
+ }
91
+ if (aliasList.some((alias) => alias.toLowerCase() === normalized)) {
92
+ return canonical.toLowerCase();
93
+ }
94
+ }
95
+ return normalized;
96
+ }
97
+ function normalizeSkills(skills, aliases) {
98
+ return unique(skills.map((skill) => normalizeSkill(skill, aliases)));
99
+ }
100
+
101
+ // src/core/parser/jd.parser.ts
102
+ var DEGREE_KEYWORDS = [
103
+ "bachelor",
104
+ "b.s",
105
+ "bs",
106
+ "bsc",
107
+ "master",
108
+ "m.s",
109
+ "ms",
110
+ "msc",
111
+ "phd",
112
+ "doctorate",
113
+ "mba",
114
+ "associate"
115
+ ];
116
+ function extractRequiredSkills(lines) {
117
+ const required = [];
118
+ for (const line of lines) {
119
+ if (/must|require|required|need/i.test(line)) {
120
+ required.push(...line.split(/[,.;•-]/));
121
+ }
122
+ }
123
+ return required.map((value) => value.trim()).filter(Boolean);
124
+ }
125
+ function extractPreferredSkills(lines) {
126
+ const preferred = [];
127
+ for (const line of lines) {
128
+ if (/preferred|nice to have|plus/i.test(line)) {
129
+ preferred.push(...line.split(/[,.;•-]/));
130
+ }
131
+ }
132
+ return preferred.map((value) => value.trim()).filter(Boolean);
133
+ }
134
+ function extractRoleKeywords(text) {
135
+ const roleMatch = text.match(/(engineer|developer|manager|scientist|analyst|designer|architect)/i);
136
+ const titleTokens = roleMatch ? roleMatch[0].split(/\s+/) : [];
137
+ return unique(tokenize(titleTokens.join(" ") || text.split(/\n/)[0] || ""));
138
+ }
139
+ function extractMinExperience(text) {
140
+ const match = text.match(/(\d{1,2})\+?\s+(?:years|yrs)/i);
141
+ if (match) {
142
+ return Number.parseInt(match[1], 10);
143
+ }
144
+ return void 0;
145
+ }
146
+ function extractEducationRequirements(text) {
147
+ const normalized = normalizeForComparison(text);
148
+ return DEGREE_KEYWORDS.filter((degree) => normalized.includes(degree));
149
+ }
150
+ function parseJobDescription(jobDescription, config) {
151
+ const normalizedText = normalizeWhitespace(jobDescription);
152
+ const lines = splitLines(jobDescription);
153
+ const requiredSkillsRaw = extractRequiredSkills(lines);
154
+ const preferredSkillsRaw = extractPreferredSkills(lines);
155
+ const requiredSkills = normalizeSkills(requiredSkillsRaw, config.skillAliases);
156
+ const preferredSkills = normalizeSkills(preferredSkillsRaw, config.skillAliases);
157
+ const keywords = unique([...requiredSkills, ...preferredSkills, ...tokenize(normalizedText)]);
158
+ return {
159
+ raw: jobDescription,
160
+ normalizedText,
161
+ requiredSkills,
162
+ preferredSkills,
163
+ roleKeywords: extractRoleKeywords(jobDescription),
164
+ keywords,
165
+ minExperienceYears: extractMinExperience(jobDescription),
166
+ educationRequirements: extractEducationRequirements(jobDescription)
167
+ };
168
+ }
169
+
170
+ // src/utils/dates.ts
171
+ var MONTHS = {
172
+ jan: 1,
173
+ january: 1,
174
+ feb: 2,
175
+ february: 2,
176
+ mar: 3,
177
+ march: 3,
178
+ apr: 4,
179
+ april: 4,
180
+ may: 5,
181
+ jun: 6,
182
+ june: 6,
183
+ jul: 7,
184
+ july: 7,
185
+ aug: 8,
186
+ august: 8,
187
+ sep: 9,
188
+ sept: 9,
189
+ september: 9,
190
+ oct: 10,
191
+ october: 10,
192
+ nov: 11,
193
+ november: 11,
194
+ dec: 12,
195
+ december: 12
196
+ };
197
+ function parseDateToken(raw) {
198
+ const cleaned = raw.trim().toLowerCase();
199
+ const monthMatch = cleaned.match(/([a-z]{3,9})\s*(\d{4})/i);
200
+ if (monthMatch) {
201
+ const monthName = monthMatch[1].toLowerCase();
202
+ const year = Number.parseInt(monthMatch[2], 10);
203
+ const month = MONTHS[monthName];
204
+ if (!Number.isNaN(year)) {
205
+ return { year, month };
206
+ }
207
+ }
208
+ const yearMatch = cleaned.match(/(20\d{2}|19\d{2})/);
209
+ if (yearMatch) {
210
+ const year = Number.parseInt(yearMatch[1], 10);
211
+ return { year };
212
+ }
213
+ return null;
214
+ }
215
+ function monthsBetween(start, end) {
216
+ const startMonth = start.month ?? 1;
217
+ const endMonth = end.month ?? 12;
218
+ return (end.year - start.year) * 12 + (endMonth - startMonth + 1);
219
+ }
220
+ function parseDateRange(text) {
221
+ const normalized = text.trim();
222
+ const rangeMatch = normalized.match(/([A-Za-z]{3,9}\s+\d{4}|\d{4})\s*(?:-|to|–|—)\s*(Present|Current|Now|[A-Za-z]{3,9}\s+\d{4}|\d{4})/i);
223
+ if (!rangeMatch) {
224
+ return null;
225
+ }
226
+ const startToken = parseDateToken(rangeMatch[1]);
227
+ const endRaw = rangeMatch[2];
228
+ const isPresent = /present|current|now/i.test(endRaw);
229
+ const endToken = isPresent ? void 0 : parseDateToken(endRaw);
230
+ if (!startToken) {
231
+ return null;
232
+ }
233
+ const endTokenResolved = endToken ?? {
234
+ year: (/* @__PURE__ */ new Date()).getFullYear(),
235
+ month: (/* @__PURE__ */ new Date()).getMonth() + 1
236
+ };
237
+ const durationInMonths = monthsBetween(startToken, endTokenResolved);
238
+ return {
239
+ raw: normalized,
240
+ start: rangeMatch[1],
241
+ end: isPresent ? "present" : rangeMatch[2],
242
+ durationInMonths: durationInMonths > 0 ? durationInMonths : void 0
243
+ };
244
+ }
245
+ function sumExperienceYears(ranges) {
246
+ const months = ranges.map((range) => range.durationInMonths ?? 0).reduce((total, value) => total + value, 0);
247
+ return Number((months / 12).toFixed(2));
248
+ }
249
+
250
+ // src/core/parser/resume.parser.ts
251
+ var SECTION_ALIASES = {
252
+ summary: ["summary", "profile", "about"],
253
+ experience: ["experience", "work experience", "professional experience", "employment"],
254
+ skills: ["skills", "technical skills", "technologies"],
255
+ education: ["education", "academics", "academic background"],
256
+ projects: ["projects", "portfolio"],
257
+ certifications: ["certifications", "licenses"]
258
+ };
259
+ var ACTION_VERBS = [
260
+ "led",
261
+ "managed",
262
+ "built",
263
+ "designed",
264
+ "implemented",
265
+ "developed",
266
+ "created",
267
+ "improved",
268
+ "optimized",
269
+ "launched",
270
+ "architected",
271
+ "delivered",
272
+ "shipped",
273
+ "collaborated",
274
+ "automated",
275
+ "mentored",
276
+ "modernized",
277
+ "reduced",
278
+ "increased"
279
+ ];
280
+ function detectSection(line) {
281
+ const normalized = line.toLowerCase();
282
+ for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
283
+ for (const alias of aliases) {
284
+ const headerPattern = new RegExp(`^${alias}(s*:)?$`, "i");
285
+ if (headerPattern.test(normalized)) {
286
+ return section;
287
+ }
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ function extractSections(text) {
293
+ const lines = splitLines(text);
294
+ const sections = {};
295
+ const detected = [];
296
+ let current = null;
297
+ const buffer = [];
298
+ const flush = () => {
299
+ if (current) {
300
+ sections[current] = buffer.join("\n").trim();
301
+ buffer.length = 0;
302
+ }
303
+ };
304
+ for (const line of lines) {
305
+ const maybeSection = detectSection(line);
306
+ if (maybeSection) {
307
+ flush();
308
+ current = maybeSection;
309
+ detected.push(maybeSection);
310
+ continue;
311
+ }
312
+ buffer.push(line);
313
+ }
314
+ flush();
315
+ return { sections, detected: unique(detected) };
316
+ }
317
+ function parseSkills(sectionContent, aliases) {
318
+ if (!sectionContent) return [];
319
+ const raw = sectionContent.split(/[,;\n]/).map((skill) => skill.trim()).filter(Boolean);
320
+ return normalizeSkills(raw, aliases);
321
+ }
322
+ function parseActionVerbs(text) {
323
+ const words = tokenize(text);
324
+ return ACTION_VERBS.filter((verb) => words.includes(verb));
325
+ }
326
+ function parseExperience(sectionContent) {
327
+ if (!sectionContent) {
328
+ return { entries: [], rangesInMonths: [], jobTitles: [] };
329
+ }
330
+ const lines = splitLines(sectionContent);
331
+ const entries = [];
332
+ const rangesInMonths = [];
333
+ const jobTitles = [];
334
+ for (const line of lines) {
335
+ const range = parseDateRange(line);
336
+ if (range) {
337
+ const previous = entries[entries.length - 1];
338
+ if (previous && !previous.dates) {
339
+ previous.dates = range;
340
+ } else {
341
+ entries.push({ dates: range });
342
+ }
343
+ if (range.durationInMonths) {
344
+ rangesInMonths.push(range.durationInMonths);
345
+ }
346
+ continue;
347
+ }
348
+ const titleMatch = line.match(/^(Senior|Lead|Principal|Staff|Software|Full\s*Stack|Frontend|Backend|Engineer|Developer|Manager|Analyst)[^,-]*/i);
349
+ if (titleMatch) {
350
+ const title = titleMatch[0].trim();
351
+ jobTitles.push(title.toLowerCase());
352
+ const entry = { title, description: line };
353
+ entries.push(entry);
354
+ continue;
355
+ }
356
+ if (entries.length > 0) {
357
+ const current = entries[entries.length - 1];
358
+ current.description = [current.description, line].filter(Boolean).join(" ").trim();
359
+ }
360
+ }
361
+ return { entries, rangesInMonths, jobTitles: unique(jobTitles) };
362
+ }
363
+ function parseEducation(sectionContent) {
364
+ if (!sectionContent) return [];
365
+ return splitLines(sectionContent).map((line) => normalizeForComparison(line));
366
+ }
367
+ function collectKeywords(text) {
368
+ return unique(tokenize(text));
369
+ }
370
+ function parseResume(resumeText, config) {
371
+ const normalizedText = normalizeWhitespace(resumeText);
372
+ const { sections, detected } = extractSections(resumeText);
373
+ const skills = parseSkills(sections.skills, config.skillAliases);
374
+ const actionVerbs = parseActionVerbs(normalizedText);
375
+ const experienceData = parseExperience(sections.experience);
376
+ const educationEntries = parseEducation(sections.education);
377
+ const totalExperienceYears = sumExperienceYears(
378
+ experienceData.entries.map((entry) => entry.dates).filter((range) => Boolean(range))
379
+ );
380
+ const requiredSections = ["summary", "experience", "skills", "education"];
381
+ const warnings = [];
382
+ for (const section of requiredSections) {
383
+ if (!detected.includes(section)) {
384
+ warnings.push(`${section} section not detected`);
385
+ }
386
+ }
387
+ return {
388
+ raw: resumeText,
389
+ normalizedText,
390
+ detectedSections: detected,
391
+ sectionContent: sections,
392
+ skills,
393
+ jobTitles: experienceData.jobTitles,
394
+ actionVerbs,
395
+ educationEntries,
396
+ experience: experienceData.entries,
397
+ totalExperienceYears,
398
+ keywords: collectKeywords(normalizedText),
399
+ warnings
400
+ };
401
+ }
402
+
403
+ // src/core/rules/rule.engine.ts
404
+ var RuleEngine = class {
405
+ constructor(config) {
406
+ this.config = config;
407
+ }
408
+ applySectionPenalties(resume) {
409
+ const requiredSections = ["summary", "experience", "skills", "education"];
410
+ let totalPenalty = 0;
411
+ const warnings = [];
412
+ const penaltyBySection = {
413
+ summary: this.config.sectionPenalties.missingSummary,
414
+ experience: this.config.sectionPenalties.missingExperience,
415
+ skills: this.config.sectionPenalties.missingSkills,
416
+ education: this.config.sectionPenalties.missingEducation,
417
+ projects: 0,
418
+ certifications: 0
419
+ };
420
+ for (const section of requiredSections) {
421
+ if (!resume.detectedSections.includes(section)) {
422
+ const penalty = penaltyBySection[section] ?? 0;
423
+ totalPenalty += penalty;
424
+ warnings.push(`${section} section missing (penalty ${penalty})`);
425
+ }
426
+ }
427
+ return { totalPenalty, warnings };
428
+ }
429
+ applyDefaultRules(input) {
430
+ let totalPenalty = 0;
431
+ const warnings = [];
432
+ const sectionResult = this.applySectionPenalties(input.resume);
433
+ totalPenalty += sectionResult.totalPenalty;
434
+ warnings.push(...sectionResult.warnings);
435
+ if (containsTableLikeStructure(input.resume.raw)) {
436
+ totalPenalty += 8;
437
+ warnings.push("Detected table-like or columnar formatting (penalty 8)");
438
+ }
439
+ if (input.overusedKeywords && input.overusedKeywords.length > 0) {
440
+ const penalty = input.overusedKeywords.length * this.config.keywordDensity.overusePenalty;
441
+ totalPenalty += penalty;
442
+ warnings.push(`Keyword stuffing detected for: ${input.overusedKeywords.join(", ")} (penalty ${penalty})`);
443
+ }
444
+ if (input.resume.detectedSections.length < 3) {
445
+ totalPenalty += 5;
446
+ warnings.push("Few recognizable sections found (penalty 5)");
447
+ }
448
+ return { totalPenalty, warnings };
449
+ }
450
+ evaluate(input) {
451
+ const defaultResult = this.applyDefaultRules(input);
452
+ let totalPenalty = defaultResult.totalPenalty;
453
+ const warnings = [...defaultResult.warnings];
454
+ for (const rule of this.config.rules) {
455
+ const context = {
456
+ resume: input.resume,
457
+ job: input.job,
458
+ weights: this.config.weights,
459
+ keywordDensity: this.config.keywordDensity,
460
+ breakdown: input.breakdown,
461
+ matchedKeywords: input.matchedKeywords,
462
+ overusedKeywords: input.overusedKeywords
463
+ };
464
+ if (rule.condition(context)) {
465
+ totalPenalty += rule.penalty;
466
+ if (rule.warning) {
467
+ warnings.push(rule.warning);
468
+ }
469
+ }
470
+ }
471
+ return { totalPenalty, warnings };
472
+ }
473
+ };
474
+
475
+ // src/core/scoring/scorer.ts
476
+ var REQUIRED_SKILL_WEIGHT = 0.7;
477
+ var OPTIONAL_SKILL_WEIGHT = 0.3;
478
+ var EXPERIENCE_YEARS_WEIGHT = 0.75;
479
+ var EXPERIENCE_ROLE_WEIGHT = 0.25;
480
+ function scoreSkills(resume, job, config) {
481
+ const profileRequired = config.profile?.mandatorySkills ?? [];
482
+ const profileOptional = config.profile?.optionalSkills ?? [];
483
+ const required = new Set(
484
+ normalizeSkills([...job.requiredSkills, ...profileRequired], config.skillAliases)
485
+ );
486
+ const optional = new Set(
487
+ normalizeSkills([...job.preferredSkills, ...profileOptional], config.skillAliases)
488
+ );
489
+ const resumeSkills = new Set(normalizeSkills(resume.skills, config.skillAliases));
490
+ const matchedRequired = [...required].filter((skill) => resumeSkills.has(skill));
491
+ const matchedOptional = [...optional].filter((skill) => resumeSkills.has(skill));
492
+ const requiredCoverage = required.size === 0 ? 1 : matchedRequired.length / required.size;
493
+ const optionalCoverage = optional.size === 0 ? 1 : matchedOptional.length / optional.size;
494
+ const score = clamp(
495
+ (requiredCoverage * REQUIRED_SKILL_WEIGHT + optionalCoverage * OPTIONAL_SKILL_WEIGHT) * 100,
496
+ 0,
497
+ 100
498
+ );
499
+ const missing = [...required].filter((skill) => !resumeSkills.has(skill));
500
+ return { score, missing };
501
+ }
502
+ function scoreExperience(resume, job, config) {
503
+ const requiredYears = job.minExperienceYears ?? config.profile?.minExperience ?? 0;
504
+ if (!requiredYears) {
505
+ return { score: 100, missingYears: 0 };
506
+ }
507
+ const yearCoverage = clamp(resume.totalExperienceYears / requiredYears, 0, 2);
508
+ const yearsComponent = clamp(yearCoverage, 0, 1) * EXPERIENCE_YEARS_WEIGHT;
509
+ const jobRoleSet = new Set(job.roleKeywords.map((value) => value.toLowerCase()));
510
+ const titleMatches = resume.jobTitles.filter((title) => jobRoleSet.has(title.toLowerCase()));
511
+ const titleCoverage = jobRoleSet.size === 0 ? 1 : titleMatches.length / jobRoleSet.size;
512
+ const roleComponent = clamp(titleCoverage, 0, 1) * EXPERIENCE_ROLE_WEIGHT;
513
+ const score = clamp((yearsComponent + roleComponent) * 100, 0, 100);
514
+ const missingYears = Math.max(requiredYears - resume.totalExperienceYears, 0);
515
+ return { score, missingYears: Number(missingYears.toFixed(2)) };
516
+ }
517
+ function scoreKeywords(resume, job, config) {
518
+ const jobKeywordSet = new Set(job.keywords.map((value) => value.toLowerCase()));
519
+ if (jobKeywordSet.size === 0) {
520
+ return { score: 100, matchedKeywords: [], missingKeywords: [], overusedKeywords: [] };
521
+ }
522
+ const resumeTokens = tokenize(resume.normalizedText);
523
+ const resumeTokenSet = new Set(resumeTokens);
524
+ const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
525
+ const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
526
+ const coverage = matchedKeywords.length / jobKeywordSet.size;
527
+ const score = clamp(coverage * 100, 0, 100);
528
+ const frequencies = countFrequencies(resumeTokens);
529
+ const totalTokens = resumeTokens.length || 1;
530
+ const overusedKeywords = matchedKeywords.filter((keyword) => {
531
+ const density = (frequencies[keyword] ?? 0) / totalTokens;
532
+ return density > config.keywordDensity.max;
533
+ });
534
+ return {
535
+ score,
536
+ matchedKeywords: unique(matchedKeywords),
537
+ missingKeywords: unique(missingKeywords),
538
+ overusedKeywords: unique(overusedKeywords)
539
+ };
540
+ }
541
+ function scoreEducation(resume, job) {
542
+ if (job.educationRequirements.length === 0) {
543
+ return 100;
544
+ }
545
+ const resumeEducationText = resume.educationEntries.join(" ");
546
+ const normalizedEducation = resumeEducationText.toLowerCase();
547
+ const matched = job.educationRequirements.filter(
548
+ (requirement) => normalizedEducation.includes(requirement.toLowerCase())
549
+ );
550
+ if (matched.length === 0) {
551
+ return 0;
552
+ }
553
+ return clamp(matched.length / job.educationRequirements.length * 100, 0, 100);
554
+ }
555
+ function calculateScore(resume, job, config) {
556
+ const skillsResult = scoreSkills(resume, job, config);
557
+ const experienceResult = scoreExperience(resume, job, config);
558
+ const keywordResult = scoreKeywords(resume, job, config);
559
+ const educationScore = scoreEducation(resume, job);
560
+ const breakdown = {
561
+ skills: skillsResult.score,
562
+ experience: experienceResult.score,
563
+ keywords: keywordResult.score,
564
+ education: educationScore
565
+ };
566
+ const weightedScore = breakdown.skills * config.weights.skills + breakdown.experience * config.weights.experience + breakdown.keywords * config.weights.keywords + breakdown.education * config.weights.education;
567
+ return {
568
+ score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
569
+ breakdown,
570
+ matchedKeywords: keywordResult.matchedKeywords,
571
+ missingKeywords: keywordResult.missingKeywords,
572
+ overusedKeywords: keywordResult.overusedKeywords,
573
+ suggestions: [],
574
+ warnings: [],
575
+ missingSkills: skillsResult.missing,
576
+ missingExperienceYears: experienceResult.missingYears,
577
+ educationScore
578
+ };
579
+ }
580
+
581
+ // src/profiles/index.ts
582
+ var defaultSkillAliases = {
583
+ javascript: ["js", "node", "node.js", "nodejs"],
584
+ typescript: ["ts"],
585
+ react: ["reactjs", "react.js"],
586
+ "c++": ["cpp"],
587
+ "c#": ["csharp"],
588
+ python: ["py"],
589
+ sql: ["postgres", "mysql", "sqlite"],
590
+ graphql: ["gql"],
591
+ aws: ["amazon web services"],
592
+ azure: ["microsoft azure"],
593
+ gcp: ["google cloud", "google cloud platform"],
594
+ docker: ["containers"],
595
+ kubernetes: ["k8s"],
596
+ html: ["html5"],
597
+ css: ["css3"]
598
+ };
599
+ var softwareEngineerProfile = {
600
+ name: "software-engineer",
601
+ mandatorySkills: ["javascript", "typescript", "react", "node"],
602
+ optionalSkills: ["graphql", "sql", "docker"],
603
+ minExperience: 3
604
+ };
605
+ var dataScientistProfile = {
606
+ name: "data-scientist",
607
+ mandatorySkills: ["python", "sql", "statistics"],
608
+ optionalSkills: ["pandas", "numpy", "pytorch", "tensorflow"],
609
+ minExperience: 2
610
+ };
611
+ var productManagerProfile = {
612
+ name: "product-manager",
613
+ mandatorySkills: ["roadmap", "stakeholder management", "prioritization"],
614
+ optionalSkills: ["a/b testing", "analytics", "sql"],
615
+ minExperience: 3
616
+ };
617
+ var defaultProfiles = [
618
+ softwareEngineerProfile,
619
+ dataScientistProfile,
620
+ productManagerProfile
621
+ ];
622
+
623
+ // src/core/scoring/weights.ts
624
+ var DEFAULT_WEIGHTS = {
625
+ skills: 0.3,
626
+ experience: 0.3,
627
+ keywords: 0.25,
628
+ education: 0.15
629
+ };
630
+ var DEFAULT_KEYWORD_DENSITY = {
631
+ min: 25e-4,
632
+ max: 0.04,
633
+ overusePenalty: 5
634
+ };
635
+ var DEFAULT_SECTION_PENALTIES = {
636
+ missingSummary: 4,
637
+ missingExperience: 10,
638
+ missingSkills: 8,
639
+ missingEducation: 6
640
+ };
641
+ function normalizeWeights(weights) {
642
+ const total = weights.skills + weights.experience + weights.keywords + weights.education;
643
+ if (total === 0) {
644
+ return { ...weights, normalizedTotal: 1 };
645
+ }
646
+ return {
647
+ skills: weights.skills / total,
648
+ experience: weights.experience / total,
649
+ keywords: weights.keywords / total,
650
+ education: weights.education / total,
651
+ normalizedTotal: 1
652
+ };
653
+ }
654
+ function resolveConfig(config = {}) {
655
+ const weights = {
656
+ skills: config.weights?.skills ?? DEFAULT_WEIGHTS.skills,
657
+ experience: config.weights?.experience ?? DEFAULT_WEIGHTS.experience,
658
+ keywords: config.weights?.keywords ?? DEFAULT_WEIGHTS.keywords,
659
+ education: config.weights?.education ?? DEFAULT_WEIGHTS.education
660
+ };
661
+ const resolved = {
662
+ weights: normalizeWeights(weights),
663
+ skillAliases: { ...defaultSkillAliases, ...config.skillAliases ?? {} },
664
+ profile: config.profile ?? softwareEngineerProfile,
665
+ rules: config.rules ?? [],
666
+ keywordDensity: config.keywordDensity ?? DEFAULT_KEYWORD_DENSITY,
667
+ sectionPenalties: {
668
+ ...DEFAULT_SECTION_PENALTIES,
669
+ ...config.sectionPenalties ?? {}
670
+ },
671
+ allowPartialMatches: config.allowPartialMatches ?? true
672
+ };
673
+ return resolved;
674
+ }
675
+
676
+ // src/core/suggestions/suggestion.engine.ts
677
+ function formatList(values, max = 6) {
678
+ const uniqueValues = Array.from(new Set(values));
679
+ const trimmed = uniqueValues.slice(0, max);
680
+ return trimmed.join(", ") + (uniqueValues.length > max ? "..." : "");
681
+ }
682
+ var SuggestionEngine = class {
683
+ generate(input) {
684
+ const suggestions = [];
685
+ const warnings = [...input.ruleWarnings, ...input.resume.warnings];
686
+ if (input.score.missingSkills.length > 0) {
687
+ suggestions.push(
688
+ `Highlight these required skills: ${formatList(input.score.missingSkills)}`
689
+ );
690
+ }
691
+ if (input.score.missingKeywords.length > 0) {
692
+ suggestions.push(
693
+ `Incorporate job-specific keywords: ${formatList(input.score.missingKeywords)}`
694
+ );
695
+ }
696
+ if (input.score.overusedKeywords.length > 0) {
697
+ suggestions.push(
698
+ `Avoid keyword stuffing for: ${formatList(input.score.overusedKeywords)}`
699
+ );
700
+ }
701
+ if (input.score.missingExperienceYears > 0) {
702
+ suggestions.push(
703
+ `Clarify at least ${input.job.minExperienceYears ?? input.score.missingExperienceYears} years of relevant experience with quantified achievements.`
704
+ );
705
+ }
706
+ if (input.job.educationRequirements.length > 0 && input.score.educationScore === 0) {
707
+ suggestions.push(
708
+ `State your education credentials matching: ${formatList(input.job.educationRequirements)}`
709
+ );
710
+ }
711
+ if (input.resume.actionVerbs.length < 3) {
712
+ suggestions.push(
713
+ "Strengthen bullet points with impact verbs (led, built, improved, delivered)."
714
+ );
715
+ }
716
+ return { suggestions, warnings };
717
+ }
718
+ };
719
+
720
+ // src/index.ts
721
+ function analyzeResume(input) {
722
+ const resolvedConfig = resolveConfig(input.config ?? {});
723
+ const parsedResume = parseResume(input.resumeText, resolvedConfig);
724
+ const parsedJob = parseJobDescription(input.jobDescription, resolvedConfig);
725
+ const scoring = calculateScore(parsedResume, parsedJob, resolvedConfig);
726
+ const ruleEngine = new RuleEngine(resolvedConfig);
727
+ const ruleResult = ruleEngine.evaluate({
728
+ resume: parsedResume,
729
+ job: parsedJob,
730
+ breakdown: scoring.breakdown,
731
+ matchedKeywords: scoring.matchedKeywords,
732
+ overusedKeywords: scoring.overusedKeywords
733
+ });
734
+ const suggestionEngine = new SuggestionEngine();
735
+ const suggestionResult = suggestionEngine.generate({
736
+ resume: parsedResume,
737
+ job: parsedJob,
738
+ score: scoring,
739
+ ruleWarnings: ruleResult.warnings
740
+ });
741
+ const finalScore = clamp(scoring.score - ruleResult.totalPenalty, 0, 100);
742
+ return {
743
+ score: finalScore,
744
+ breakdown: scoring.breakdown,
745
+ matchedKeywords: scoring.matchedKeywords,
746
+ missingKeywords: scoring.missingKeywords,
747
+ overusedKeywords: scoring.overusedKeywords,
748
+ suggestions: suggestionResult.suggestions,
749
+ warnings: suggestionResult.warnings
750
+ };
751
+ }
752
+
753
+ exports.analyzeResume = analyzeResume;
754
+ exports.defaultProfiles = defaultProfiles;
755
+ exports.defaultSkillAliases = defaultSkillAliases;
756
+ //# sourceMappingURL=index.js.map
757
+ //# sourceMappingURL=index.js.map