@pranavraut033/ats-checker 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/{chunk-ZJ5E4H7Z.mjs → chunk-IY7OVF6Z.mjs} +36 -3
- package/dist/chunk-IY7OVF6Z.mjs.map +1 -0
- package/dist/index.cjs +137 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +106 -14
- package/dist/index.mjs.map +1 -1
- package/dist/lang/de/index.d.mts +1 -1
- package/dist/lang/de/index.d.ts +1 -1
- package/dist/lang/en/index.d.mts +1 -1
- package/dist/lang/en/index.d.ts +1 -1
- package/dist/lang/en/index.mjs +1 -1
- package/dist/{scoring-BCShrnki.d.mts → scoring-pZZR3soa.d.mts} +25 -1
- package/dist/{scoring-BCShrnki.d.ts → scoring-pZZR3soa.d.ts} +25 -1
- package/package.json +1 -1
- package/dist/chunk-ZJ5E4H7Z.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as ATSProfile, S as SkillAliases, K as KeywordRegistry, L as LLMConfig, J as JSONSchema, a as LLMResult, b as LLMBudget, c as AnalyzeResumeInput, d as ATSAnalysisResult } from './scoring-
|
|
2
|
-
export {
|
|
1
|
+
import { A as ATSProfile, S as SkillAliases, K as KeywordRegistry, L as LLMConfig, J as JSONSchema, a as LLMResult, b as LLMBudget, c as AnalyzeResumeInput, d as ATSAnalysisResult } from './scoring-pZZR3soa.mjs';
|
|
2
|
+
export { t as ATSBreakdown, k as ATSConfig, j as ATSRule, e as ATSWeights, x as AnalyzeResumeInputV2, f as KeywordCategory, h as KeywordDensityConfig, g as KeywordEntry, u as KeywordWeight, v as LLMClient, w as LLMFeatures, y as LLMUsageStats, N as NormalizedWeights, o as ParsedAchievement, P as ParsedDateRange, n as ParsedExperienceEntry, s as ParsedJobDescription, p as ParsedLanguage, q as ParsedResume, R as ResolvedATSConfig, m as ResumeSection, l as RuleContext, i as SectionPenaltyConfig, r as SkillExperienceRequirement } from './scoring-pZZR3soa.mjs';
|
|
3
3
|
|
|
4
4
|
declare const defaultKeywordRegistry: KeywordRegistry;
|
|
5
5
|
declare const defaultSkillAliases: SkillAliases;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as ATSProfile, S as SkillAliases, K as KeywordRegistry, L as LLMConfig, J as JSONSchema, a as LLMResult, b as LLMBudget, c as AnalyzeResumeInput, d as ATSAnalysisResult } from './scoring-
|
|
2
|
-
export {
|
|
1
|
+
import { A as ATSProfile, S as SkillAliases, K as KeywordRegistry, L as LLMConfig, J as JSONSchema, a as LLMResult, b as LLMBudget, c as AnalyzeResumeInput, d as ATSAnalysisResult } from './scoring-pZZR3soa.js';
|
|
2
|
+
export { t as ATSBreakdown, k as ATSConfig, j as ATSRule, e as ATSWeights, x as AnalyzeResumeInputV2, f as KeywordCategory, h as KeywordDensityConfig, g as KeywordEntry, u as KeywordWeight, v as LLMClient, w as LLMFeatures, y as LLMUsageStats, N as NormalizedWeights, o as ParsedAchievement, P as ParsedDateRange, n as ParsedExperienceEntry, s as ParsedJobDescription, p as ParsedLanguage, q as ParsedResume, R as ResolvedATSConfig, m as ResumeSection, l as RuleContext, i as SectionPenaltyConfig, r as SkillExperienceRequirement } from './scoring-pZZR3soa.js';
|
|
3
3
|
|
|
4
4
|
declare const defaultKeywordRegistry: KeywordRegistry;
|
|
5
5
|
declare const defaultSkillAliases: SkillAliases;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { clamp, mergeKeywordRegistries, defaultKeywordRegistry, softwareEngineerProfile, buildCategoryIndex, deriveSkillAliases, normalizeWhitespace, splitLines, normalizeSkills, tokenize,
|
|
2
|
-
export { defaultKeywordRegistry, defaultProfiles, defaultSkillAliases } from './chunk-
|
|
1
|
+
import { ROLE_NOUNS, unique, clamp, mergeKeywordRegistries, defaultKeywordRegistry, softwareEngineerProfile, buildCategoryIndex, deriveSkillAliases, normalizeWhitespace, splitLines, normalizeSkills, tokenize, containsTableLikeStructure, normalizeForComparison, STOP_WORDS, normalizeSkill, countFrequencies, escapeRegExp } from './chunk-IY7OVF6Z.mjs';
|
|
2
|
+
export { defaultKeywordRegistry, defaultProfiles, defaultSkillAliases } from './chunk-IY7OVF6Z.mjs';
|
|
3
3
|
|
|
4
4
|
// src/utils/languages.ts
|
|
5
5
|
var KNOWN_LANGUAGES = [
|
|
@@ -159,8 +159,9 @@ function extractPreferredSkills(lines) {
|
|
|
159
159
|
}
|
|
160
160
|
return preferred;
|
|
161
161
|
}
|
|
162
|
+
var ROLE_NOUN_RE = new RegExp(`(${ROLE_NOUNS.join("|")})`, "gi");
|
|
162
163
|
function extractRoleKeywords(text) {
|
|
163
|
-
const roleMatches = text.match(
|
|
164
|
+
const roleMatches = text.match(ROLE_NOUN_RE) ?? [];
|
|
164
165
|
const fallback = roleMatches.length === 0 ? [text.split(/\n/)[0] ?? ""] : [];
|
|
165
166
|
return unique(tokenize([...roleMatches, ...fallback].join(" ")));
|
|
166
167
|
}
|
|
@@ -191,6 +192,32 @@ function isLanguageRequired(lang, jobDescription) {
|
|
|
191
192
|
return LANG_SECTION_RE.test(line) || LANG_REQUIREMENT_HINT_RE.test(line);
|
|
192
193
|
});
|
|
193
194
|
}
|
|
195
|
+
var YEARS_BEFORE_SKILL_RE = /(\d{1,2})\+?\s*years?\s*(?:of|in|with)?\s*((?:[a-z0-9][a-z0-9.#+\-/]*\s*){1,4})/gi;
|
|
196
|
+
var SKILL_BEFORE_YEARS_RE = /((?:[a-z0-9][a-z0-9.#+\-/]*[\s,]*){1,4})\(?(\d{1,2})\+?\s*years?\)?/gi;
|
|
197
|
+
function extractSkillExperienceRequirements(lines, skillVocab, aliases) {
|
|
198
|
+
const requirements = /* @__PURE__ */ new Map();
|
|
199
|
+
const record = (rawWord, years) => {
|
|
200
|
+
if (!Number.isFinite(years) || years <= 0 || years > 60) return;
|
|
201
|
+
const canonical = normalizeSkill(rawWord, aliases);
|
|
202
|
+
if (!skillVocab.has(rawWord.toLowerCase()) && !skillVocab.has(canonical)) return;
|
|
203
|
+
const existing = requirements.get(canonical);
|
|
204
|
+
if (existing === void 0 || years > existing) {
|
|
205
|
+
requirements.set(canonical, years);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
for (const re of [YEARS_BEFORE_SKILL_RE, SKILL_BEFORE_YEARS_RE]) {
|
|
210
|
+
re.lastIndex = 0;
|
|
211
|
+
let match;
|
|
212
|
+
while (match = re.exec(line)) {
|
|
213
|
+
const years = Number.parseInt(re === YEARS_BEFORE_SKILL_RE ? match[1] : match[2], 10);
|
|
214
|
+
const candidateWords = tokenize(re === YEARS_BEFORE_SKILL_RE ? match[2] : match[1]);
|
|
215
|
+
for (const word of candidateWords) record(word, years);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return [...requirements.entries()].map(([skill, years]) => ({ skill, years })).sort((a, b) => a.skill.localeCompare(b.skill));
|
|
220
|
+
}
|
|
194
221
|
function extractDegreeLevels(text) {
|
|
195
222
|
const found = /* @__PURE__ */ new Set();
|
|
196
223
|
for (const [pattern, canonical] of DEGREE_VARIANTS) {
|
|
@@ -221,6 +248,11 @@ function parseJobDescription(jobDescription, config) {
|
|
|
221
248
|
const bodyTokens = tokenize(normalizedText).filter(isSkillLike);
|
|
222
249
|
const roleKeywords = extractRoleKeywords(jobDescription);
|
|
223
250
|
const keywords = unique([...requiredSkills, ...preferredSkills, ...roleKeywords, ...bodyTokens]);
|
|
251
|
+
const skillExperienceRequirements = extractSkillExperienceRequirements(
|
|
252
|
+
lines,
|
|
253
|
+
skillVocab,
|
|
254
|
+
config.skillAliases
|
|
255
|
+
);
|
|
224
256
|
return {
|
|
225
257
|
raw: jobDescription,
|
|
226
258
|
normalizedText,
|
|
@@ -229,6 +261,7 @@ function parseJobDescription(jobDescription, config) {
|
|
|
229
261
|
roleKeywords,
|
|
230
262
|
keywords,
|
|
231
263
|
minExperienceYears: extractMinExperience(jobDescription),
|
|
264
|
+
skillExperienceRequirements,
|
|
232
265
|
educationRequirements: extractDegreeLevels(jobDescription),
|
|
233
266
|
keywordSurfaceForms: collectKeywordSurfaceForms(jobDescription, config.skillAliases),
|
|
234
267
|
// A language only counts as required if its mention carries a requirement/level cue
|
|
@@ -425,6 +458,10 @@ var STRONG_VERBS = [
|
|
|
425
458
|
"increased"
|
|
426
459
|
];
|
|
427
460
|
var WEAK_VERBS = ["worked", "helped", "performed", "responsible", "assisted", "participated", "involved"];
|
|
461
|
+
var TITLE_SENIORITY_PREFIXES = ["senior", "lead", "principal", "staff", "vp", "director"];
|
|
462
|
+
var TITLE_MODIFIER_WORDS = ["software", "full\\s*stack", "frontend", "backend"];
|
|
463
|
+
var TITLE_PREFIX_WORDS = unique([...TITLE_SENIORITY_PREFIXES, ...TITLE_MODIFIER_WORDS, ...ROLE_NOUNS]);
|
|
464
|
+
var TITLE_RE = new RegExp(`^(${TITLE_PREFIX_WORDS.join("|")})[^,(-]*`, "i");
|
|
428
465
|
var METRIC_RE = /\d|%|\$|\bk\+|\bm\+/i;
|
|
429
466
|
function classifyAchievement(line) {
|
|
430
467
|
const lower = line.toLowerCase();
|
|
@@ -502,21 +539,26 @@ function parseExperience(sectionContent, referenceDate) {
|
|
|
502
539
|
const achievements = [];
|
|
503
540
|
for (const line of lines) {
|
|
504
541
|
const range = parseDateRange(line, referenceDate);
|
|
542
|
+
const titleMatch = line.match(TITLE_RE);
|
|
543
|
+
const title = titleMatch ? titleMatch[0].trim() : void 0;
|
|
505
544
|
if (range) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
545
|
+
if (title) {
|
|
546
|
+
jobTitles.push(title.toLowerCase());
|
|
547
|
+
entries.push({ title, dates: range, description: line });
|
|
509
548
|
} else {
|
|
510
|
-
entries.
|
|
549
|
+
const previous = entries[entries.length - 1];
|
|
550
|
+
if (previous && !previous.dates) {
|
|
551
|
+
previous.dates = range;
|
|
552
|
+
} else {
|
|
553
|
+
entries.push({ dates: range });
|
|
554
|
+
}
|
|
511
555
|
}
|
|
512
556
|
if (range.durationInMonths) {
|
|
513
557
|
rangesInMonths.push(range.durationInMonths);
|
|
514
558
|
}
|
|
515
559
|
continue;
|
|
516
560
|
}
|
|
517
|
-
|
|
518
|
-
if (titleMatch) {
|
|
519
|
-
const title = titleMatch[0].trim();
|
|
561
|
+
if (title) {
|
|
520
562
|
jobTitles.push(title.toLowerCase());
|
|
521
563
|
const entry = { title, description: line };
|
|
522
564
|
entries.push(entry);
|
|
@@ -538,6 +580,14 @@ function parseEducation(sectionContent) {
|
|
|
538
580
|
function collectKeywords(text) {
|
|
539
581
|
return unique(tokenize(text));
|
|
540
582
|
}
|
|
583
|
+
var EMAIL_RE = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i;
|
|
584
|
+
var PHONE_RE = /\+?\(?\d{1,4}\)?(?:[\s.-]?\d{2,4}){2,4}/;
|
|
585
|
+
function extractContact(text) {
|
|
586
|
+
const email = text.match(EMAIL_RE)?.[0];
|
|
587
|
+
const phone = text.match(PHONE_RE)?.[0]?.trim();
|
|
588
|
+
if (!email && !phone) return void 0;
|
|
589
|
+
return { email, phone };
|
|
590
|
+
}
|
|
541
591
|
function parseResume(resumeText, config) {
|
|
542
592
|
const normalizedText = normalizeWhitespace(resumeText);
|
|
543
593
|
const { sections, detected } = extractSections(resumeText);
|
|
@@ -573,6 +623,12 @@ function parseResume(resumeText, config) {
|
|
|
573
623
|
warnings.push(`${section} section not detected`);
|
|
574
624
|
}
|
|
575
625
|
}
|
|
626
|
+
const contact = extractContact(resumeText);
|
|
627
|
+
if (!contact?.email) {
|
|
628
|
+
warnings.push(
|
|
629
|
+
"No email address detected \u2014 most ATS require a parseable contact email"
|
|
630
|
+
);
|
|
631
|
+
}
|
|
576
632
|
return {
|
|
577
633
|
raw: resumeText,
|
|
578
634
|
normalizedText,
|
|
@@ -588,6 +644,7 @@ function parseResume(resumeText, config) {
|
|
|
588
644
|
totalExperienceYears,
|
|
589
645
|
keywords: collectKeywords(normalizedText),
|
|
590
646
|
languages: parseLanguageMentions(resumeText),
|
|
647
|
+
contact,
|
|
591
648
|
warnings
|
|
592
649
|
};
|
|
593
650
|
}
|
|
@@ -637,6 +694,11 @@ var RuleEngine = class {
|
|
|
637
694
|
totalPenalty += 5;
|
|
638
695
|
warnings.push("Few recognizable sections found (penalty 5)");
|
|
639
696
|
}
|
|
697
|
+
if (!input.resume.contact?.email) {
|
|
698
|
+
const penalty = this.config.sectionPenalties.missingContact;
|
|
699
|
+
totalPenalty += penalty;
|
|
700
|
+
warnings.push(`Missing contact email (penalty ${penalty})`);
|
|
701
|
+
}
|
|
640
702
|
return { totalPenalty, warnings };
|
|
641
703
|
}
|
|
642
704
|
evaluate(input) {
|
|
@@ -708,8 +770,9 @@ function scoreExperience(resume, job, config) {
|
|
|
708
770
|
const yearCoverage = clamp(resume.totalExperienceYears / requiredYears, 0, 2);
|
|
709
771
|
const yearsComponent = clamp(yearCoverage, 0, 1) * EXPERIENCE_YEARS_WEIGHT;
|
|
710
772
|
const jobRoleSet = new Set(job.roleKeywords.map((value) => value.toLowerCase()));
|
|
711
|
-
const
|
|
712
|
-
const
|
|
773
|
+
const resumeTitleTokens = new Set(resume.jobTitles.flatMap((t) => tokenize(t)));
|
|
774
|
+
const matchedRoles = job.roleKeywords.filter((rk) => resumeTitleTokens.has(rk.toLowerCase()));
|
|
775
|
+
const titleCoverage = jobRoleSet.size === 0 ? 1 : matchedRoles.length / jobRoleSet.size;
|
|
713
776
|
const roleComponent = clamp(titleCoverage, 0, 1) * EXPERIENCE_ROLE_WEIGHT;
|
|
714
777
|
const score = clamp((yearsComponent + roleComponent) * 100, 0, 100);
|
|
715
778
|
const missingYears = Math.max(requiredYears - resume.totalExperienceYears, 0);
|
|
@@ -785,6 +848,17 @@ function scoreKeywords(resume, job, config) {
|
|
|
785
848
|
keywordWeights
|
|
786
849
|
};
|
|
787
850
|
}
|
|
851
|
+
function computeSkillExperienceGaps(resume, job, config) {
|
|
852
|
+
if (job.skillExperienceRequirements.length === 0) return [];
|
|
853
|
+
const resumeSkills = new Set(normalizeSkills(resume.skills, config.skillAliases));
|
|
854
|
+
const gaps = [];
|
|
855
|
+
for (const { skill, years } of job.skillExperienceRequirements) {
|
|
856
|
+
if (resumeSkills.has(skill) && resume.totalExperienceYears < years) {
|
|
857
|
+
gaps.push({ skill, requiredYears: years, resumeYears: resume.totalExperienceYears });
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return gaps.sort((a, b) => a.skill.localeCompare(b.skill));
|
|
861
|
+
}
|
|
788
862
|
function scoreEducation(resume, job) {
|
|
789
863
|
if (job.educationRequirements.length === 0) {
|
|
790
864
|
return 100;
|
|
@@ -818,6 +892,7 @@ function calculateScore(resume, job, config) {
|
|
|
818
892
|
resume.languages,
|
|
819
893
|
job.requiredLanguages
|
|
820
894
|
);
|
|
895
|
+
const skillExperienceGaps = computeSkillExperienceGaps(resume, job, config);
|
|
821
896
|
return {
|
|
822
897
|
score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
|
|
823
898
|
breakdown,
|
|
@@ -831,6 +906,7 @@ function calculateScore(resume, job, config) {
|
|
|
831
906
|
achievementStrength,
|
|
832
907
|
matchedLanguages,
|
|
833
908
|
missingLanguages,
|
|
909
|
+
skillExperienceGaps,
|
|
834
910
|
suggestions: [],
|
|
835
911
|
warnings: [],
|
|
836
912
|
// detectedSections / parsedExperienceYears / experienceGap / experienceEntries: filled by index.ts
|
|
@@ -859,7 +935,10 @@ var DEFAULT_SECTION_PENALTIES = {
|
|
|
859
935
|
missingSummary: 4,
|
|
860
936
|
missingExperience: 10,
|
|
861
937
|
missingSkills: 8,
|
|
862
|
-
missingEducation: 6
|
|
938
|
+
missingEducation: 6,
|
|
939
|
+
// Warning-only by default — see determinism note in the contact/parseability plan step;
|
|
940
|
+
// callers who want it to actually move the score can override via config.sectionPenalties.
|
|
941
|
+
missingContact: 0
|
|
863
942
|
};
|
|
864
943
|
function normalizeWeights(weights) {
|
|
865
944
|
const total = weights.skills + weights.experience + weights.keywords + weights.education;
|
|
@@ -980,6 +1059,16 @@ var SuggestionEngine = class {
|
|
|
980
1059
|
"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."
|
|
981
1060
|
);
|
|
982
1061
|
}
|
|
1062
|
+
if (!input.resume.contact?.email) {
|
|
1063
|
+
suggestions.push(
|
|
1064
|
+
"Add a clearly formatted email address near the top of your resume so ATS and recruiters can contact you."
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
for (const gap of input.score.skillExperienceGaps) {
|
|
1068
|
+
suggestions.push(
|
|
1069
|
+
`The role asks for ${gap.requiredYears}+ years of ${gap.skill}; make that duration explicit in your experience section.`
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
983
1072
|
return { suggestions, warnings };
|
|
984
1073
|
}
|
|
985
1074
|
};
|
|
@@ -1145,7 +1234,8 @@ No explanations. No markdown. No additional text.`;
|
|
|
1145
1234
|
});
|
|
1146
1235
|
const timeoutPromise = new Promise((_, reject) => {
|
|
1147
1236
|
const id = globalThis.setTimeout(() => reject(new Error(`LLM call timeout after ${this.timeoutMs}ms`)), this.timeoutMs);
|
|
1148
|
-
void clientPromise.finally(() => globalThis.clearTimeout(id))
|
|
1237
|
+
void clientPromise.finally(() => globalThis.clearTimeout(id)).catch(() => {
|
|
1238
|
+
});
|
|
1149
1239
|
});
|
|
1150
1240
|
void timeoutPromise.catch(() => {
|
|
1151
1241
|
});
|
|
@@ -1643,6 +1733,7 @@ function analyzeResume(input) {
|
|
|
1643
1733
|
achievementStrength: scoring.achievementStrength,
|
|
1644
1734
|
matchedLanguages: scoring.matchedLanguages,
|
|
1645
1735
|
missingLanguages: scoring.missingLanguages,
|
|
1736
|
+
skillExperienceGaps: scoring.skillExperienceGaps,
|
|
1646
1737
|
experienceGap: scoring.experienceGap,
|
|
1647
1738
|
detectedSections: parsedResume.detectedSections,
|
|
1648
1739
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
@@ -1714,6 +1805,7 @@ async function analyzeResumeAsync(input) {
|
|
|
1714
1805
|
achievementStrength: scoring.achievementStrength,
|
|
1715
1806
|
matchedLanguages: scoring.matchedLanguages,
|
|
1716
1807
|
missingLanguages: scoring.missingLanguages,
|
|
1808
|
+
skillExperienceGaps: scoring.skillExperienceGaps,
|
|
1717
1809
|
experienceGap: scoring.experienceGap,
|
|
1718
1810
|
detectedSections: parsedResume.detectedSections,
|
|
1719
1811
|
parsedExperienceYears: parsedResume.totalExperienceYears,
|