@pranavraut033/ats-checker 1.3.3 → 2.0.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.
@@ -1,3 +1,27 @@
1
+ interface EmploymentGap {
2
+ /** Title (or company/fallback label) of the role worked immediately before the gap. */
3
+ afterRole: string;
4
+ months: number;
5
+ }
6
+
7
+ /** ATS-relevant parseability signals extracted from a resume's raw text. */
8
+ interface FormattingSignals {
9
+ /** Pipe/tab/aligned-space table structure (reuses containsTableLikeStructure). */
10
+ hasTables: boolean;
11
+ /** Very wide, inconsistent whitespace gaps suggesting a PDF-extracted multi-column layout. */
12
+ hasMultiColumn: boolean;
13
+ /** Control chars / private-use-area glyphs (icon fonts) / replacement chars from a bad extraction. */
14
+ hasSpecialChars: boolean;
15
+ /** Bullets other than the common -, *, •, o, numbered/lettered list markers. */
16
+ nonStandardBullets: boolean;
17
+ /** Text density/shape consistent with a scanned image (no embedded text layer) rather than real text. */
18
+ likelyScanned: boolean;
19
+ /** A plausible email address is present in the raw text (contact info is extractable). */
20
+ contactParseable: boolean;
21
+ }
22
+
23
+ type Seniority = "junior" | "mid" | "senior" | "lead" | "principal";
24
+
1
25
  type ResumeSection = "summary" | "experience" | "skills" | "education" | "projects" | "certifications";
2
26
  interface ParsedDateRange {
3
27
  raw?: string;
@@ -16,6 +40,8 @@ interface ParsedExperienceEntry {
16
40
  location?: string;
17
41
  dates?: ParsedDateRange;
18
42
  description?: string;
43
+ /** Skills mentioned within this role's bullets, resolved via the alias/fuzzy registry lookup. */
44
+ skills: string[];
19
45
  }
20
46
  interface ParsedAchievement {
21
47
  text: string;
@@ -47,8 +73,26 @@ interface ParsedResume {
47
73
  totalExperienceYears: number;
48
74
  keywords: string[];
49
75
  languages: ParsedLanguage[];
76
+ /** Contact details extracted via regex; absent fields mean detection failed, not that the resume lacks them. */
77
+ contact?: {
78
+ email?: string;
79
+ phone?: string;
80
+ linkedin?: string;
81
+ location?: string;
82
+ };
83
+ /** Inferred overall seniority (highest-ranked signal across job titles), or undefined if unknown. */
84
+ seniority?: Seniority;
85
+ /** Gaps of minGapMonths+ between consecutive (chronologically merged) roles. */
86
+ employmentGaps: EmploymentGap[];
87
+ /** ATS-relevant parseability/formatting signals extracted from the raw text. */
88
+ formatting: FormattingSignals;
50
89
  warnings: string[];
51
90
  }
91
+ /** A JD's "N+ years of <skill>" requirement, e.g. { skill: "figma", years: 5 }. */
92
+ interface SkillExperienceRequirement {
93
+ skill: string;
94
+ years: number;
95
+ }
52
96
  interface ParsedJobDescription {
53
97
  raw: string;
54
98
  normalizedText: string;
@@ -57,97 +101,14 @@ interface ParsedJobDescription {
57
101
  roleKeywords: string[];
58
102
  keywords: string[];
59
103
  minExperienceYears?: number;
104
+ /** Per-skill year requirements parsed from the JD, e.g. "5+ years of Figma". */
105
+ skillExperienceRequirements: SkillExperienceRequirement[];
60
106
  educationRequirements: string[];
61
107
  /** canonical keyword -> the surface form (original casing/spelling) the JD used. */
62
108
  keywordSurfaceForms: Record<string, string>;
63
109
  requiredLanguages: ParsedLanguage[];
64
- }
65
-
66
- interface ATSWeights {
67
- skills: number;
68
- experience: number;
69
- keywords: number;
70
- education: number;
71
- }
72
- type SkillAliases = Record<string, string[]>;
73
- type KeywordCategory = "technical" | "tool" | "concept" | "soft" | "marketing" | "domain";
74
- interface KeywordEntry {
75
- canonical: string;
76
- aliases: string[];
77
- category: KeywordCategory;
78
- }
79
- type KeywordRegistry = KeywordEntry[];
80
- interface ATSProfile {
81
- name: string;
82
- mandatorySkills: string[];
83
- optionalSkills: string[];
84
- minExperience?: number;
85
- }
86
- interface KeywordDensityConfig {
87
- /** Minimum density before a keyword is considered underused (informational only). */
88
- min: number;
89
- /** Maximum density before a keyword is considered stuffed. */
90
- max: number;
91
- /** Penalty applied when density exceeds max. */
92
- overusePenalty: number;
93
- }
94
- interface SectionPenaltyConfig {
95
- missingSummary?: number;
96
- missingExperience?: number;
97
- missingSkills?: number;
98
- missingEducation?: number;
99
- }
100
- interface ATSRule {
101
- id: string;
102
- description?: string;
103
- penalty: number;
104
- warning?: string;
105
- condition: (context: RuleContext) => boolean;
106
- }
107
- interface ATSConfig {
108
- weights?: Partial<ATSWeights>;
109
- skillAliases?: SkillAliases;
110
- /** Categorized keyword/alias entries (technical, tool, concept, soft, marketing, domain). Merges over the default registry by canonical term. */
111
- keywordRegistry?: KeywordRegistry;
112
- profile?: ATSProfile;
113
- rules?: ATSRule[];
114
- keywordDensity?: KeywordDensityConfig;
115
- sectionPenalties?: SectionPenaltyConfig;
116
- allowPartialMatches?: boolean;
117
- /**
118
- * ISO date string (e.g. "2024-06-01") used as the "today" reference when
119
- * computing duration for open-ended date ranges ("Present"/"Current"/"Now").
120
- * Omit to use the actual current date (live/production behaviour).
121
- * Set to a fixed value in tests or batch processing to guarantee determinism.
122
- */
123
- referenceDate?: string;
124
- }
125
- interface NormalizedWeights extends ATSWeights {
126
- /** Weights normalized so they sum to 1. */
127
- normalizedTotal: number;
128
- }
129
- interface ResolvedATSConfig {
130
- weights: NormalizedWeights;
131
- skillAliases: SkillAliases;
132
- keywordRegistry: KeywordRegistry;
133
- /** canonical term -> category, derived once from keywordRegistry. */
134
- categoryIndex: Map<string, KeywordCategory>;
135
- profile?: ATSProfile;
136
- rules: ATSRule[];
137
- keywordDensity: KeywordDensityConfig;
138
- sectionPenalties: Required<SectionPenaltyConfig>;
139
- allowPartialMatches: boolean;
140
- /** Resolved reference date for "Present" duration calculations. */
141
- referenceDate?: Date;
142
- }
143
- interface RuleContext {
144
- resume: ParsedResume;
145
- job: ParsedJobDescription;
146
- weights: NormalizedWeights;
147
- keywordDensity: KeywordDensityConfig;
148
- breakdown?: ATSBreakdown;
149
- matchedKeywords?: string[];
150
- overusedKeywords?: string[];
110
+ /** Inferred seniority cue from the JD's role/title text, or undefined if unknown. */
111
+ seniority?: Seniority;
151
112
  }
152
113
 
153
114
  /**
@@ -260,8 +221,32 @@ interface ATSBreakdown {
260
221
  skills: number;
261
222
  experience: number;
262
223
  keywords: number;
224
+ /** Real-ATS parseability score (0-100) — formatting, contact extractability, section coverage. */
225
+ parseability: number;
263
226
  education: number;
264
227
  }
228
+ /** One point deduction applied while computing the parseability score. */
229
+ interface ParseabilityDeduction {
230
+ reason: string;
231
+ points: number;
232
+ }
233
+ /** Detail behind `breakdown.parseability` — the formatting signals plus what was deducted and why. */
234
+ interface ParseabilityReport extends FormattingSignals {
235
+ /** Number of standard resume sections the parser detected. */
236
+ detectedSectionCount: number;
237
+ /** Ordered list of deductions applied to get from 100 down to `breakdown.parseability`. */
238
+ deductions: ParseabilityDeduction[];
239
+ }
240
+ /** Comparison between the resume's inferred seniority and the JD's required seniority. */
241
+ interface SeniorityMatch {
242
+ resume?: Seniority;
243
+ required?: Seniority;
244
+ /**
245
+ * True when the resume's seniority rank is >= the JD's required rank, or when either side
246
+ * is unknown (we never penalize on missing signal — only on a confirmed mismatch).
247
+ */
248
+ met: boolean;
249
+ }
265
250
  interface AnalyzeResumeInput {
266
251
  resumeText: string;
267
252
  jobDescription: string;
@@ -314,6 +299,141 @@ interface ATSAnalysisResult {
314
299
  parsedExperienceYears: number;
315
300
  /** Parsed experience entries from the resume, with titles and date ranges. */
316
301
  experienceEntries: ParsedExperienceEntry[];
302
+ /**
303
+ * JD skills the resume has but whose overall experience falls short of the JD's per-skill
304
+ * year requirement (e.g. JD wants "5+ years Figma", resume has Figma but only 3 years total).
305
+ * Informational only — does not feed `score`/`breakdown`, same as language proficiency.
306
+ */
307
+ skillExperienceGaps: {
308
+ skill: string;
309
+ requiredYears: number;
310
+ resumeYears: number;
311
+ }[];
312
+ /** Detail behind `breakdown.parseability` — which formatting signals were detected and penalized. */
313
+ parseabilityReport: ParseabilityReport;
314
+ /** Gaps of 3+ months between consecutive (chronologically merged) roles. Informational only. */
315
+ employmentGaps: EmploymentGap[];
316
+ /** Resume vs JD-required seniority comparison. Informational only outside its modest, capped
317
+ * contribution to `breakdown.experience` (see scorer.ts). */
318
+ seniorityMatch: SeniorityMatch;
319
+ /**
320
+ * Per-skill years of experience derived from per-role dating (sum of durations of roles whose
321
+ * bullets mention that skill), replacing the old "assume the skill spans the whole resume
322
+ * tenure" approximation. Informational only — does not feed `score`/`breakdown`.
323
+ */
324
+ perSkillExperience: {
325
+ skill: string;
326
+ years: number;
327
+ }[];
328
+ }
329
+
330
+ interface ATSWeights {
331
+ skills: number;
332
+ experience: number;
333
+ keywords: number;
334
+ /** How much the resume's real-ATS parseability (formatting/contact/sections) counts toward score. */
335
+ parseability: number;
336
+ education: number;
337
+ }
338
+ type SkillAliases = Record<string, string[]>;
339
+ type KeywordCategory = "technical" | "tool" | "concept" | "soft" | "marketing" | "domain";
340
+ interface KeywordEntry {
341
+ canonical: string;
342
+ aliases: string[];
343
+ category: KeywordCategory;
344
+ }
345
+ type KeywordRegistry = KeywordEntry[];
346
+ interface ATSProfile {
347
+ name: string;
348
+ mandatorySkills: string[];
349
+ optionalSkills: string[];
350
+ minExperience?: number;
351
+ }
352
+ interface KeywordDensityConfig {
353
+ /** Minimum density before a keyword is considered underused (informational only). */
354
+ min: number;
355
+ /** Maximum density before a keyword is considered stuffed. */
356
+ max: number;
357
+ /** Penalty applied when density exceeds max. */
358
+ overusePenalty: number;
359
+ }
360
+ interface SectionPenaltyConfig {
361
+ missingSummary?: number;
362
+ missingExperience?: number;
363
+ missingSkills?: number;
364
+ missingEducation?: number;
365
+ /**
366
+ * Penalty when no parseable contact email is detected. Defaults to 12 — a real ATS treats
367
+ * unparseable contact info as a near-knockout (recruiters/automated outreach can't reach the
368
+ * candidate at all), so this is deliberately more than a token warning-only value.
369
+ */
370
+ missingContact?: number;
371
+ }
372
+ interface MatchingConfig {
373
+ /**
374
+ * Fall back to stemmed/fuzzy matching (typos, word-form variants like "developing" vs
375
+ * "develop", "ReactJS" vs "react") when an exact alias lookup misses. Default: true.
376
+ * Set to false to reproduce pre-v2 exact-match-only behavior.
377
+ */
378
+ fuzzy?: boolean;
379
+ /** Passed through to fuzzyEqual's bounded Levenshtein distance when fuzzy matching is enabled. */
380
+ threshold?: number;
381
+ }
382
+ interface ATSRule {
383
+ id: string;
384
+ description?: string;
385
+ penalty: number;
386
+ warning?: string;
387
+ condition: (context: RuleContext) => boolean;
388
+ }
389
+ interface ATSConfig {
390
+ weights?: Partial<ATSWeights>;
391
+ skillAliases?: SkillAliases;
392
+ /** Categorized keyword/alias entries (technical, tool, concept, soft, marketing, domain). Merges over the default registry by canonical term. */
393
+ keywordRegistry?: KeywordRegistry;
394
+ profile?: ATSProfile;
395
+ rules?: ATSRule[];
396
+ keywordDensity?: KeywordDensityConfig;
397
+ sectionPenalties?: SectionPenaltyConfig;
398
+ allowPartialMatches?: boolean;
399
+ /** Stemmed/fuzzy matching behavior for skills/keywords. Defaults to `{ fuzzy: true }`. */
400
+ matching?: MatchingConfig;
401
+ /**
402
+ * ISO date string (e.g. "2024-06-01") used as the "today" reference when
403
+ * computing duration for open-ended date ranges ("Present"/"Current"/"Now").
404
+ * Omit to use the actual current date (live/production behaviour).
405
+ * Set to a fixed value in tests or batch processing to guarantee determinism.
406
+ */
407
+ referenceDate?: string;
408
+ }
409
+ interface NormalizedWeights extends ATSWeights {
410
+ /** Weights normalized so they sum to 1. */
411
+ normalizedTotal: number;
412
+ }
413
+ interface ResolvedATSConfig {
414
+ weights: NormalizedWeights;
415
+ skillAliases: SkillAliases;
416
+ keywordRegistry: KeywordRegistry;
417
+ /** canonical term -> category, derived once from keywordRegistry. */
418
+ categoryIndex: Map<string, KeywordCategory>;
419
+ profile?: ATSProfile;
420
+ rules: ATSRule[];
421
+ keywordDensity: KeywordDensityConfig;
422
+ sectionPenalties: Required<SectionPenaltyConfig>;
423
+ allowPartialMatches: boolean;
424
+ /** Resolved matching behavior; `fuzzy` is always defined (defaults to true). */
425
+ matching: Required<Pick<MatchingConfig, "fuzzy">> & Pick<MatchingConfig, "threshold">;
426
+ /** Resolved reference date for "Present" duration calculations. */
427
+ referenceDate?: Date;
428
+ }
429
+ interface RuleContext {
430
+ resume: ParsedResume;
431
+ job: ParsedJobDescription;
432
+ weights: NormalizedWeights;
433
+ keywordDensity: KeywordDensityConfig;
434
+ breakdown?: ATSBreakdown;
435
+ matchedKeywords?: string[];
436
+ overusedKeywords?: string[];
317
437
  }
318
438
 
319
- export type { ATSProfile as A, JSONSchema as J, KeywordRegistry as K, LLMConfig as L, NormalizedWeights as N, ParsedDateRange as P, ResolvedATSConfig as R, SkillAliases as S, LLMResult as a, LLMBudget as b, AnalyzeResumeInput as c, ATSAnalysisResult as d, ATSWeights as e, KeywordCategory as f, KeywordEntry as g, KeywordDensityConfig as h, SectionPenaltyConfig as i, ATSRule as j, ATSConfig as k, RuleContext as l, ResumeSection as m, ParsedExperienceEntry as n, ParsedAchievement as o, ParsedLanguage as p, ParsedResume as q, ParsedJobDescription as r, ATSBreakdown as s, KeywordWeight as t, LLMClient as u, LLMFeatures as v, AnalyzeResumeInputV2 as w, LLMUsageStats as x };
439
+ export type { ATSProfile as A, AnalyzeResumeInputV2 as B, LLMUsageStats as C, JSONSchema as J, KeywordRegistry as K, LLMConfig as L, MatchingConfig as M, NormalizedWeights as N, ParsedDateRange as P, ResolvedATSConfig as R, SkillAliases as S, LLMResult as a, LLMBudget as b, AnalyzeResumeInput as c, ATSAnalysisResult as d, ATSWeights as e, KeywordCategory as f, KeywordEntry as g, KeywordDensityConfig as h, SectionPenaltyConfig as i, ATSRule as j, ATSConfig as k, RuleContext as l, ResumeSection as m, ParsedExperienceEntry as n, ParsedAchievement as o, ParsedLanguage as p, ParsedResume as q, SkillExperienceRequirement as r, ParsedJobDescription as s, ATSBreakdown as t, ParseabilityDeduction as u, ParseabilityReport as v, SeniorityMatch as w, KeywordWeight as x, LLMClient as y, LLMFeatures as z };