Configuration¶
The analyzeResume function accepts an optional config object to customize scoring behavior, skill matching, and validation rules.
Weights¶
Control the relative importance of each scoring component. Values are normalized to sum to 1.0.
config: {
weights: {
skills: 0.3, // 30% weight for skill matches
experience: 0.2, // 20% for experience
keywords: 0.2, // 20% for keyword matches
parseability: 0.2, // 20% for formatting/structure realism
education: 0.1 // 10% for education
}
}
Default weights: skills 0.25, experience 0.20, keywords 0.25, parseability 0.20, education 0.10
Matching (fuzzy/stem)¶
Skill and keyword comparison falls back to stemmed/bounded-Levenshtein matching when an exact alias lookup misses — so typos and word-form variants ("ReactJS" vs "react", "developing" vs "develop") still count as a match. On by default.
config: {
matching: {
fuzzy: true, // default: true — set false to require exact matches only (v1 behavior)
threshold: 2 // optional: max edit distance passed to the bounded Levenshtein check
}
}
Parseability¶
A dedicated 0-100 sub-score (weighted weights.parseability, default 0.20) models the single biggest real-world ATS rejection cause: formatting the parser can't cleanly extract. It isn't directly configurable — it's derived from the parser's FormattingSignals — but you can see exactly what it deducted via result.parseabilityReport:
result.parseabilityReport;
// {
// hasTables: true, hasMultiColumn: false, hasSpecialChars: false,
// nonStandardBullets: false, likelyScanned: false, contactParseable: true,
// detectedSectionCount: 4,
// deductions: [
// { reason: "Table or columnar formatting detected — ...", points: 20 }
// ]
// }
Skill Aliases¶
Map skill synonyms to canonical names for better matching.
config: {
skillAliases: {
"javascript": ["js", "ecmascript", "es6"],
"react": ["reactjs", "react.js"],
"node": ["nodejs", "node.js"]
}
}
When "js" appears in a resume, it's treated as "javascript" for scoring.
Keyword Registry & Categories¶
The built-in defaultKeywordRegistry is a list of { canonical, aliases, category } entries — skillAliases is derived from it for backward compatibility. Each entry's category is one of: technical, tool, concept, soft, marketing, domain.
config: {
keywordRegistry: [
{ canonical: "rust", aliases: ["rustlang"], category: "technical" },
{
canonical: "javascript",
aliases: ["js", "ecmascript"],
category: "technical",
}, // overrides default entry
];
}
Entries merge over defaultKeywordRegistry by canonical term — your entries win on conflict, everything else from the default registry is kept. Categories drive result.keywordsByCategory, which groups matched/missing keywords for display.
Keyword Weighting¶
Within scoreKeywords, each JD keyword gets a weight based on:
- Location: required (
3) > preferred (2) > body-only (1) - Frequency: a small bonus when the JD repeats the term
The keywords sub-score is sum(weight of matched) / sum(weight of all) × 100 — missing a required keyword costs more than missing a body-only one. Per-keyword weights are exposed in result.keywordWeights (jdWeight/importance, and resumeWeight — how often it appears in the resume).
Multi-language Packs¶
Categorized registries for other languages ship as subpath exports (canonical terms stay English so scoring/profiles are unaffected; the pack supplies localized aliases):
See src/lang/ for available packs (en, de).
Language Requirements¶
Spoken-language requirements (English, German, Spanish, etc. — distinct from the keyword registry, which is for tech/domain terms) are parsed automatically from free text — no config needed. Both the JD parser and resume parser scan for:
- CEFR codes:
A1,A2,B1,B2,C1,C2 - Descriptive levels:
basic/elementary,conversational/intermediate,professional/advanced,fluent,native/bilingual
Any language mentioned in the JD is treated as required. A resume language counts as a match only if its level rank is equal to or higher than the JD's:
const result = analyzeResume({
resumeText: "Languages: German (C1), fluent Spanish",
jobDescription: "German (B2) required. Native English speaker preferred.",
});
result.matchedLanguages; // [{ name: "german", level: "b2", levelRank: 4 }]
result.missingLanguages; // [{ name: "english", level: "native", levelRank: 6 }]
This does not feed into breakdown or score — it's informational, surfaced via result.matchedLanguages/result.missingLanguages and a suggestion when a required language is missing or under-leveled. See src/utils/languages.ts for the supported language/level list.
Industry Profiles¶
Define required skills and minimum experience for specific roles.
config: {
profile: {
name: "Frontend Developer",
mandatorySkills: ["javascript", "html", "css"],
optionalSkills: ["react", "vue", "angular"],
minExperience: 2 // minimum years
}
}
Mandatory skills not found reduce the score; optional skills boost it when present.
Keyword Density¶
Configure detection of keyword stuffing or underuse.
config: {
keywordDensity: {
min: 0.0025, // Minimum density threshold (default)
max: 0.04, // Maximum before penalty (default)
overusePenalty: 5 // Points deducted for stuffing (default)
}
}
Density is calculated as (keyword occurrences) / (total words).
Section Penalties¶
Penalize missing resume sections.
config: {
sectionPenalties: {
missingSummary: 5,
missingExperience: 10,
missingSkills: 5,
missingEducation: 5,
missingContact: 12 // default: a real ATS treats no parseable email as a near-knockout
}
}
Note: an unparseable contact email is also one of the deductions inside breakdown.parseability (see Parseability) — missingContact is the separate rule-engine penalty on top of that, not a duplicate of it.
Custom Rules¶
Add your own validation logic with penalties.
config: {
rules: [
{
id: "no-tables",
description: "Resumes with tables are hard for ATS to parse",
penalty: 10,
warning: "Remove tables from your resume",
condition: (context) => context.resume.hasTables,
},
];
}
Rules receive a RuleContext with parsed resume/job data, current breakdown, and matched keywords.
Reference Date¶
Freeze the "Present"/"Now"/"Current" end date used in experience date ranges. Without this, experience years are calculated relative to new Date() — meaning the same resume produces a slightly different score each month. Set it to an ISO date string for fully reproducible scoring.
Useful for: testing, CI pipelines, caching scores, or any context where you need identical output for identical input.
Partial Matches¶
Allow partial keyword matches (e.g., "Java" matches "JavaScript").
Defaults & Resolution¶
All user input is merged with sane defaults using resolveConfig() and weights are normalized to sum to 1.0.
Default values:
- Weights: skills 0.25, experience 0.20, keywords 0.25, parseability 0.20, education 0.10
- Matching:
fuzzy: true(stemmed/bounded-Levenshtein fallback for skills & keywords) - Keyword Density: min 0.0025, max 0.04, overusePenalty 5
- Section Penalties: missingSummary 4, missingExperience 10, missingSkills 8, missingEducation 6, missingContact 12
- Partial Matches:
allowPartialMatches: true - Skill Aliases: merged from built-in
defaultSkillAliases+ your overrides - Keyword Registry: merged from
defaultKeywordRegistry(407 canonical terms) + yourkeywordRegistryentries (by canonical term), thenskillAliaseslayered on top - Language Requirements: parsed automatically from JD/resume text, no config — see Language Requirements
- Profile:
softwareEngineerProfileunless overridden
See implementation in src/core/scoring/weights.ts.
For rule customization, refer to Rules Engine.
Complete Example¶
import { analyzeResume } from "@pranavraut033/ats-checker";
const result = analyzeResume({
resumeText: "...",
jobDescription: "...",
config: {
weights: {
skills: 0.4,
experience: 0.2,
keywords: 0.15,
parseability: 0.15,
education: 0.1,
},
skillAliases: { typescript: ["ts"] },
profile: {
mandatorySkills: ["javascript", "react"],
minExperience: 3,
},
rules: [
{
id: "phone-number",
penalty: 2,
condition: (context) => !context.resume.contact?.phone,
},
],
},
});