@pranavraut033/ats-checker 1.0.5 → 1.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/README.md +144 -108
- package/dist/index.d.mts +22 -9
- package/dist/index.d.ts +22 -9
- package/dist/index.js +44 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +44 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
# ats-checker
|
|
1
|
+
# @pranavraut033/ats-checker
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@pranavraut033/ats-checker)
|
|
4
4
|
[](https://www.npmjs.com/package/@pranavraut033/ats-checker)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://github.com/Pranavraut033/ats-checker/actions/workflows/deploy.yml)
|
|
7
6
|
[](https://github.com/Pranavraut033/ats-checker/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/Pranavraut033/ats-checker/actions/workflows/deploy.yml)
|
|
8
|
+
|
|
9
|
+
Zero-dependency TypeScript library that scores a resume against a job description and explains why — skills coverage, keyword overlap, experience match, and education — with no randomness, no LLM, and no external calls.
|
|
10
|
+
|
|
11
|
+
**[Live Demo →](https://pranavraut033.github.io/ats-checker/)**
|
|
12
|
+
**[Docs →](https://pranavraut033.github.io/ats-checker/docs/)**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Features
|
|
8
17
|
|
|
9
|
-
|
|
18
|
+
- **Deterministic** — same input always produces the same score; pin it with `referenceDate` to freeze "Present" date math
|
|
19
|
+
- **Explainable** — breakdown by category (skills / experience / keywords / education) plus matched and missing skill/keyword lists
|
|
20
|
+
- **Configurable** — adjust weights, add skill aliases, define custom penalty rules
|
|
21
|
+
- **Zero dependencies** — core library has no runtime deps; ships ESM + CJS
|
|
22
|
+
- **Built-in profiles** — software engineer, data scientist, product manager out of the box
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Node.js ≥ 18
|
|
29
|
+
|
|
30
|
+
---
|
|
10
31
|
|
|
11
32
|
## Installation
|
|
12
33
|
|
|
@@ -14,173 +35,188 @@ A zero-dependency TypeScript library for evaluating resume compatibility with Ap
|
|
|
14
35
|
npm install @pranavraut033/ats-checker
|
|
15
36
|
```
|
|
16
37
|
|
|
38
|
+
---
|
|
39
|
+
|
|
17
40
|
## Usage
|
|
18
41
|
|
|
19
42
|
```typescript
|
|
20
43
|
import { analyzeResume } from "@pranavraut033/ats-checker";
|
|
21
44
|
|
|
22
45
|
const result = analyzeResume({
|
|
23
|
-
resumeText: `
|
|
24
|
-
Software Engineer with 5 years of experience
|
|
25
|
-
|
|
46
|
+
resumeText: `
|
|
47
|
+
Software Engineer with 5 years of experience.
|
|
48
|
+
Skills: JavaScript, TypeScript, React, Node.js, SQL
|
|
49
|
+
Experience: Senior Engineer at ExampleCorp (Jan 2020 - Present)
|
|
50
|
+
Education: B.S. Computer Science
|
|
51
|
+
`,
|
|
52
|
+
jobDescription: `
|
|
53
|
+
Frontend engineer role. Must have React, TypeScript, accessibility best practices.
|
|
54
|
+
Preferred: GraphQL. 3+ years required. Bachelor's degree required.
|
|
55
|
+
`,
|
|
56
|
+
config: { referenceDate: "2026-01-01" }, // freeze clock for reproducible scores
|
|
26
57
|
});
|
|
27
58
|
|
|
28
|
-
console.log(result.score);
|
|
29
|
-
console.log(result.
|
|
30
|
-
console.log(result.
|
|
59
|
+
console.log(result.score); // e.g. 72.45
|
|
60
|
+
console.log(result.matchedSkills); // ["javascript", "node", "react", "typescript"]
|
|
61
|
+
console.log(result.missingSkills); // ["accessibility best practices", "graphql"]
|
|
62
|
+
console.log(result.experienceGap); // 0 (requirement met)
|
|
63
|
+
console.log(result.suggestions); // ["Add GraphQL to your skills section", ...]
|
|
31
64
|
```
|
|
32
65
|
|
|
33
|
-
|
|
66
|
+
---
|
|
34
67
|
|
|
35
|
-
|
|
68
|
+
## Output
|
|
36
69
|
|
|
70
|
+
`analyzeResume()` returns an `ATSAnalysisResult`:
|
|
37
71
|
|
|
72
|
+
| Field | Type | Description |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `score` | `number` | Overall ATS score 0–100 after rule penalties |
|
|
75
|
+
| `breakdown` | `ATSBreakdown` | Sub-scores: `skills`, `experience`, `keywords`, `education` |
|
|
76
|
+
| `matchedSkills` | `string[]` | Required skills found in the resume |
|
|
77
|
+
| `missingSkills` | `string[]` | Required skills absent from the resume |
|
|
78
|
+
| `matchedKeywords` | `string[]` | JD keywords present in the resume (sorted) |
|
|
79
|
+
| `missingKeywords` | `string[]` | JD keywords absent from the resume (sorted) |
|
|
80
|
+
| `overusedKeywords` | `string[]` | Keywords exceeding density threshold (sorted) |
|
|
81
|
+
| `suggestions` | `string[]` | Deterministic improvement recommendations |
|
|
82
|
+
| `warnings` | `string[]` | Parse warnings and section alerts |
|
|
83
|
+
| `experienceGap` | `number` | Years below JD minimum; `0` when met |
|
|
84
|
+
| `detectedSections` | `string[]` | Resume sections the parser found |
|
|
85
|
+
| `parsedExperienceYears` | `number` | Total years from resume date ranges |
|
|
38
86
|
|
|
39
|
-
|
|
87
|
+
**Scoring formula:**
|
|
88
|
+
`score = skills×0.30 + experience×0.30 + keywords×0.25 + education×0.15` → clamped to 0–100 → rule penalties subtracted.
|
|
40
89
|
|
|
41
|
-
|
|
42
|
-
import { analyzeResumeAsync } from "@pranavraut033/ats-checker";
|
|
43
|
-
|
|
44
|
-
const myLLMClient = /* implement LLMClient (OpenAI/Anthropic/local) */;
|
|
45
|
-
|
|
46
|
-
const result = await analyzeResumeAsync({
|
|
47
|
-
resumeText: "...",
|
|
48
|
-
jobDescription: "...",
|
|
49
|
-
llm: {
|
|
50
|
-
client: myLLMClient,
|
|
51
|
-
models: { default: "gpt-4o-mini" },
|
|
52
|
-
limits: { maxCalls: 3, maxTokensPerCall: 1000, maxTotalTokens: 5000 },
|
|
53
|
-
enable: { suggestions: true }
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
console.log(result.score); // unchanged by LLM
|
|
58
|
-
console.log(result.suggestions); // enhanced wording/context
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Note: Passing `llm` to `analyzeResume` (sync) will add a warning and skip enhancement. Prefer `analyzeResumeAsync` for LLM features.
|
|
90
|
+
---
|
|
62
91
|
|
|
63
92
|
## Configuration
|
|
64
93
|
|
|
65
|
-
|
|
94
|
+
All options are optional. Pass any subset; `resolveConfig()` fills in defaults.
|
|
66
95
|
|
|
67
96
|
```typescript
|
|
68
97
|
const result = analyzeResume({
|
|
69
98
|
resumeText: "...",
|
|
70
99
|
jobDescription: "...",
|
|
71
100
|
config: {
|
|
101
|
+
// Override scoring weights (auto-normalized to sum to 1)
|
|
72
102
|
weights: { skills: 0.4, experience: 0.3, keywords: 0.2, education: 0.1 },
|
|
73
|
-
|
|
103
|
+
|
|
104
|
+
// Additional skill synonyms merged over built-in defaults
|
|
105
|
+
skillAliases: { javascript: ["js", "ecmascript"] },
|
|
106
|
+
|
|
107
|
+
// Industry profile: sets mandatory/optional skills and minExperience
|
|
108
|
+
profile: {
|
|
109
|
+
mandatorySkills: ["javascript", "react"],
|
|
110
|
+
optionalSkills: ["graphql", "docker"],
|
|
111
|
+
minExperience: 3,
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Freeze "Present" end dates for reproducible experience scoring
|
|
115
|
+
referenceDate: "2026-01-01",
|
|
116
|
+
|
|
117
|
+
// Keyword density thresholds
|
|
118
|
+
keywordDensity: { min: 0.0025, max: 0.04, overusePenalty: 5 },
|
|
119
|
+
|
|
120
|
+
// Custom penalty rules
|
|
74
121
|
rules: [
|
|
75
122
|
{
|
|
76
|
-
id: "
|
|
77
|
-
penalty:
|
|
78
|
-
warning: "
|
|
79
|
-
condition: (ctx) =>
|
|
123
|
+
id: "no-tables",
|
|
124
|
+
penalty: 10,
|
|
125
|
+
warning: "Remove tables — ATS parsers often mangle them",
|
|
126
|
+
condition: (ctx) => ctx.resume.detectedSections.length < 2,
|
|
80
127
|
},
|
|
81
128
|
{
|
|
82
|
-
id: "
|
|
83
|
-
penalty:
|
|
84
|
-
warning: "
|
|
85
|
-
condition: (ctx) =>
|
|
86
|
-
}
|
|
87
|
-
]
|
|
88
|
-
}
|
|
129
|
+
id: "experience-gap",
|
|
130
|
+
penalty: 5,
|
|
131
|
+
warning: "Resume has less than 3 years experience",
|
|
132
|
+
condition: (ctx) => ctx.resume.totalExperienceYears < 3,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
89
136
|
});
|
|
90
137
|
```
|
|
91
138
|
|
|
92
|
-
|
|
139
|
+
### Defaults
|
|
140
|
+
|
|
141
|
+
| Setting | Default |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `weights.skills` | `0.30` |
|
|
144
|
+
| `weights.experience` | `0.30` |
|
|
145
|
+
| `weights.keywords` | `0.25` |
|
|
146
|
+
| `weights.education` | `0.15` |
|
|
147
|
+
| `keywordDensity.min` | `0.0025` |
|
|
148
|
+
| `keywordDensity.max` | `0.04` |
|
|
149
|
+
| `keywordDensity.overusePenalty` | `5` |
|
|
150
|
+
| `allowPartialMatches` | `true` |
|
|
151
|
+
| `referenceDate` | Current date (use explicit ISO string for determinism) |
|
|
152
|
+
|
|
153
|
+
See [Configuration docs](https://pranavraut033.github.io/ats-checker/docs/configuration/) for all options.
|
|
93
154
|
|
|
94
|
-
|
|
155
|
+
---
|
|
95
156
|
|
|
96
|
-
-
|
|
97
|
-
- Keyword density: min 0.0025, max 0.04, overusePenalty 5
|
|
98
|
-
- Section penalties: summary 4, experience 10, skills 8, education 6
|
|
99
|
-
- Partial matches: `allowPartialMatches: true`
|
|
157
|
+
## Built-in Skill Aliases
|
|
100
158
|
|
|
101
|
-
|
|
159
|
+
Common tech synonyms are pre-loaded so `js` matches `javascript`, `k8s` matches `kubernetes`, etc. Extend or override via `config.skillAliases`.
|
|
102
160
|
|
|
103
|
-
|
|
161
|
+
```typescript
|
|
162
|
+
import { defaultSkillAliases } from "@pranavraut033/ats-checker";
|
|
163
|
+
// { javascript: ["js"], node: ["node.js", "nodejs"], typescript: ["ts"], ... }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
104
167
|
|
|
105
|
-
|
|
168
|
+
## Built-in Profiles
|
|
106
169
|
|
|
107
170
|
```typescript
|
|
171
|
+
import {
|
|
172
|
+
softwareEngineerProfile,
|
|
173
|
+
dataScientistProfile,
|
|
174
|
+
productManagerProfile,
|
|
175
|
+
} from "@pranavraut033/ats-checker";
|
|
176
|
+
|
|
108
177
|
const result = analyzeResume({
|
|
109
178
|
resumeText: "...",
|
|
110
179
|
jobDescription: "...",
|
|
111
|
-
config: {
|
|
112
|
-
rules: [
|
|
113
|
-
{
|
|
114
|
-
id: "min-years",
|
|
115
|
-
penalty: 5,
|
|
116
|
-
warning: "Less than 3 years experience",
|
|
117
|
-
condition: (ctx) => (ctx.resume.experienceYears ?? 0) < 3
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: "require-contact",
|
|
121
|
-
penalty: 2,
|
|
122
|
-
warning: "Add phone/email to contact info",
|
|
123
|
-
condition: (ctx) => !ctx.resume.contactInfo?.phone || !ctx.resume.contactInfo?.email
|
|
124
|
-
}
|
|
125
|
-
]
|
|
126
|
-
}
|
|
180
|
+
config: { profile: softwareEngineerProfile },
|
|
127
181
|
});
|
|
128
182
|
```
|
|
129
|
-
See [Rules Engine](docs/rules.md) for default rules and context fields.
|
|
130
|
-
|
|
131
|
-
## Features
|
|
132
183
|
|
|
133
|
-
|
|
134
|
-
- Detects common ATS issues like missing sections or keyword overuse
|
|
135
|
-
- Customizable scoring weights and validation rules
|
|
136
|
-
- Optional LLM integration for enhanced suggestions
|
|
137
|
-
- Includes a web interface for testing (`npm run dev`)
|
|
138
|
-
- [Live Demo](https://pranavraut033.github.io/ats-checker/)
|
|
184
|
+
---
|
|
139
185
|
|
|
140
|
-
##
|
|
186
|
+
## LLM Integration (deprecated)
|
|
141
187
|
|
|
142
|
-
|
|
188
|
+
`analyzeResumeAsync` accepts an optional `llm` config that rewrites suggestion text via a caller-supplied LLM client. **This path is deprecated** — scores and breakdowns are never touched by LLM. Prefer calling `analyzeResume` and running your own LLM pass on `result.suggestions` if you want AI-enhanced wording.
|
|
143
189
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
**Input:**
|
|
147
|
-
- `resumeText: string` - The full text of the resume
|
|
148
|
-
- `jobDescription: string` - The job description text
|
|
149
|
-
- `config?: ATSConfig` - Optional configuration overrides
|
|
150
|
-
|
|
151
|
-
**Output:**
|
|
152
|
-
- `score: number` - Overall ATS score (0-100)
|
|
153
|
-
- `breakdown: ATSBreakdown` - Component scores
|
|
154
|
-
- `matchedKeywords: string[]` - Keywords found in both
|
|
155
|
-
- `missingKeywords: string[]` - Important keywords not in resume
|
|
156
|
-
- `suggestions: string[]` - Improvement recommendations
|
|
157
|
-
- `warnings: string[]` - Issues detected
|
|
190
|
+
---
|
|
158
191
|
|
|
159
192
|
## Development
|
|
160
193
|
|
|
161
194
|
```bash
|
|
162
195
|
npm install
|
|
163
|
-
npm run build
|
|
164
|
-
npm test
|
|
165
|
-
npm run
|
|
196
|
+
npm run build # tsup → ESM + CJS in dist/
|
|
197
|
+
npm test # vitest (single pass)
|
|
198
|
+
npm run type-check # tsc --noEmit
|
|
199
|
+
npm run dev # static demo UI at http://localhost:3005
|
|
166
200
|
```
|
|
167
201
|
|
|
202
|
+
---
|
|
203
|
+
|
|
168
204
|
## Documentation
|
|
169
205
|
|
|
170
|
-
|
|
171
|
-
- https://Pranavraut033.github.io/ats-checker/docs/
|
|
206
|
+
Full docs at **[pranavraut033.github.io/ats-checker/docs/](https://pranavraut033.github.io/ats-checker/docs/)**
|
|
172
207
|
|
|
173
|
-
**Local Docs** (in repository):
|
|
174
|
-
- [Configuration Guide](docs/configuration.md)
|
|
175
|
-
- [LLM Integration](docs/llm-integration.md)
|
|
176
|
-
- [Web Interface](docs/ui.md)
|
|
177
208
|
- [Architecture](docs/architecture.md)
|
|
209
|
+
- [Configuration](docs/configuration.md)
|
|
210
|
+
- [Rules Engine](docs/rules.md)
|
|
211
|
+
|
|
212
|
+
---
|
|
178
213
|
|
|
179
214
|
## Contributing
|
|
180
215
|
|
|
181
|
-
|
|
216
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). PRs welcome.
|
|
182
217
|
|
|
183
|
-
|
|
218
|
+
---
|
|
184
219
|
|
|
185
|
-
|
|
220
|
+
## License
|
|
186
221
|
|
|
222
|
+
MIT © [Pranav Raut](https://github.com/Pranavraut033)
|
package/dist/index.d.mts
CHANGED
|
@@ -79,6 +79,13 @@ interface ATSConfig {
|
|
|
79
79
|
keywordDensity?: KeywordDensityConfig;
|
|
80
80
|
sectionPenalties?: SectionPenaltyConfig;
|
|
81
81
|
allowPartialMatches?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* ISO date string (e.g. "2024-06-01") used as the "today" reference when
|
|
84
|
+
* computing duration for open-ended date ranges ("Present"/"Current"/"Now").
|
|
85
|
+
* Omit to use the actual current date (live/production behaviour).
|
|
86
|
+
* Set to a fixed value in tests or batch processing to guarantee determinism.
|
|
87
|
+
*/
|
|
88
|
+
referenceDate?: string;
|
|
82
89
|
}
|
|
83
90
|
interface NormalizedWeights extends ATSWeights {
|
|
84
91
|
/** Weights normalized so they sum to 1. */
|
|
@@ -92,6 +99,8 @@ interface ResolvedATSConfig {
|
|
|
92
99
|
keywordDensity: KeywordDensityConfig;
|
|
93
100
|
sectionPenalties: Required<SectionPenaltyConfig>;
|
|
94
101
|
allowPartialMatches: boolean;
|
|
102
|
+
/** Resolved reference date for "Present" duration calculations. */
|
|
103
|
+
referenceDate?: Date;
|
|
95
104
|
}
|
|
96
105
|
interface RuleContext {
|
|
97
106
|
resume: ParsedResume;
|
|
@@ -224,11 +233,21 @@ interface AnalyzeResumeInput {
|
|
|
224
233
|
interface ATSAnalysisResult {
|
|
225
234
|
score: number;
|
|
226
235
|
breakdown: ATSBreakdown;
|
|
236
|
+
/** Skills found in the resume that satisfy JD + profile requirements. */
|
|
237
|
+
matchedSkills: string[];
|
|
238
|
+
/** Required skills absent from the resume. */
|
|
239
|
+
missingSkills: string[];
|
|
227
240
|
matchedKeywords: string[];
|
|
228
241
|
missingKeywords: string[];
|
|
229
242
|
overusedKeywords: string[];
|
|
230
243
|
suggestions: string[];
|
|
231
244
|
warnings: string[];
|
|
245
|
+
/** Years below the JD's minimum experience requirement; 0 when the requirement is met. */
|
|
246
|
+
experienceGap: number;
|
|
247
|
+
/** Resume sections the parser successfully detected (e.g. "summary", "skills"). */
|
|
248
|
+
detectedSections: string[];
|
|
249
|
+
/** Total years of experience parsed from the resume's date ranges. */
|
|
250
|
+
parsedExperienceYears: number;
|
|
232
251
|
}
|
|
233
252
|
|
|
234
253
|
declare const defaultSkillAliases: SkillAliases;
|
|
@@ -471,18 +490,12 @@ declare function safeExtractNumber(obj: unknown, key: string): number | undefine
|
|
|
471
490
|
*/
|
|
472
491
|
declare function analyzeResume(input: AnalyzeResumeInput): ATSAnalysisResult;
|
|
473
492
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
493
|
+
* @deprecated The LLM layer only rewrites suggestion text and adds non-determinism.
|
|
494
|
+
* Prefer `analyzeResume` (sync, deterministic) and call your own LLM on the result if needed.
|
|
495
|
+
* This function will be removed in a future major version.
|
|
476
496
|
*
|
|
477
497
|
* @param input Resume, job description, and optional LLM config
|
|
478
498
|
* @returns Promise<ATSAnalysisResult>
|
|
479
|
-
*
|
|
480
|
-
* @example
|
|
481
|
-
* const result = await analyzeResumeAsync({
|
|
482
|
-
* resumeText,
|
|
483
|
-
* jobDescription,
|
|
484
|
-
* llm: { client, limits: {...}, enable: { suggestions: true } }
|
|
485
|
-
* });
|
|
486
499
|
*/
|
|
487
500
|
declare function analyzeResumeAsync(input: AnalyzeResumeInput): Promise<ATSAnalysisResult>;
|
|
488
501
|
|
package/dist/index.d.ts
CHANGED
|
@@ -79,6 +79,13 @@ interface ATSConfig {
|
|
|
79
79
|
keywordDensity?: KeywordDensityConfig;
|
|
80
80
|
sectionPenalties?: SectionPenaltyConfig;
|
|
81
81
|
allowPartialMatches?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* ISO date string (e.g. "2024-06-01") used as the "today" reference when
|
|
84
|
+
* computing duration for open-ended date ranges ("Present"/"Current"/"Now").
|
|
85
|
+
* Omit to use the actual current date (live/production behaviour).
|
|
86
|
+
* Set to a fixed value in tests or batch processing to guarantee determinism.
|
|
87
|
+
*/
|
|
88
|
+
referenceDate?: string;
|
|
82
89
|
}
|
|
83
90
|
interface NormalizedWeights extends ATSWeights {
|
|
84
91
|
/** Weights normalized so they sum to 1. */
|
|
@@ -92,6 +99,8 @@ interface ResolvedATSConfig {
|
|
|
92
99
|
keywordDensity: KeywordDensityConfig;
|
|
93
100
|
sectionPenalties: Required<SectionPenaltyConfig>;
|
|
94
101
|
allowPartialMatches: boolean;
|
|
102
|
+
/** Resolved reference date for "Present" duration calculations. */
|
|
103
|
+
referenceDate?: Date;
|
|
95
104
|
}
|
|
96
105
|
interface RuleContext {
|
|
97
106
|
resume: ParsedResume;
|
|
@@ -224,11 +233,21 @@ interface AnalyzeResumeInput {
|
|
|
224
233
|
interface ATSAnalysisResult {
|
|
225
234
|
score: number;
|
|
226
235
|
breakdown: ATSBreakdown;
|
|
236
|
+
/** Skills found in the resume that satisfy JD + profile requirements. */
|
|
237
|
+
matchedSkills: string[];
|
|
238
|
+
/** Required skills absent from the resume. */
|
|
239
|
+
missingSkills: string[];
|
|
227
240
|
matchedKeywords: string[];
|
|
228
241
|
missingKeywords: string[];
|
|
229
242
|
overusedKeywords: string[];
|
|
230
243
|
suggestions: string[];
|
|
231
244
|
warnings: string[];
|
|
245
|
+
/** Years below the JD's minimum experience requirement; 0 when the requirement is met. */
|
|
246
|
+
experienceGap: number;
|
|
247
|
+
/** Resume sections the parser successfully detected (e.g. "summary", "skills"). */
|
|
248
|
+
detectedSections: string[];
|
|
249
|
+
/** Total years of experience parsed from the resume's date ranges. */
|
|
250
|
+
parsedExperienceYears: number;
|
|
232
251
|
}
|
|
233
252
|
|
|
234
253
|
declare const defaultSkillAliases: SkillAliases;
|
|
@@ -471,18 +490,12 @@ declare function safeExtractNumber(obj: unknown, key: string): number | undefine
|
|
|
471
490
|
*/
|
|
472
491
|
declare function analyzeResume(input: AnalyzeResumeInput): ATSAnalysisResult;
|
|
473
492
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
493
|
+
* @deprecated The LLM layer only rewrites suggestion text and adds non-determinism.
|
|
494
|
+
* Prefer `analyzeResume` (sync, deterministic) and call your own LLM on the result if needed.
|
|
495
|
+
* This function will be removed in a future major version.
|
|
476
496
|
*
|
|
477
497
|
* @param input Resume, job description, and optional LLM config
|
|
478
498
|
* @returns Promise<ATSAnalysisResult>
|
|
479
|
-
*
|
|
480
|
-
* @example
|
|
481
|
-
* const result = await analyzeResumeAsync({
|
|
482
|
-
* resumeText,
|
|
483
|
-
* jobDescription,
|
|
484
|
-
* llm: { client, limits: {...}, enable: { suggestions: true } }
|
|
485
|
-
* });
|
|
486
499
|
*/
|
|
487
500
|
declare function analyzeResumeAsync(input: AnalyzeResumeInput): Promise<ATSAnalysisResult>;
|
|
488
501
|
|
package/dist/index.js
CHANGED
|
@@ -37,13 +37,15 @@ function normalizeWhitespace(text) {
|
|
|
37
37
|
return text.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim();
|
|
38
38
|
}
|
|
39
39
|
function normalizeForComparison(text) {
|
|
40
|
-
return normalizeWhitespace(text).toLowerCase();
|
|
40
|
+
return normalizeWhitespace(text).normalize("NFKC").toLowerCase();
|
|
41
41
|
}
|
|
42
42
|
function splitLines(text) {
|
|
43
43
|
return text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
44
44
|
}
|
|
45
|
+
var TECH_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/g;
|
|
45
46
|
function tokenize(text) {
|
|
46
|
-
|
|
47
|
+
const normalized = normalizeForComparison(text);
|
|
48
|
+
return (normalized.match(TECH_TOKEN_RE) ?? []).filter((t) => !STOP_WORDS.has(t));
|
|
47
49
|
}
|
|
48
50
|
function unique(values) {
|
|
49
51
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -217,7 +219,7 @@ function monthsBetween(start, end) {
|
|
|
217
219
|
const endMonth = end.month ?? 12;
|
|
218
220
|
return (end.year - start.year) * 12 + (endMonth - startMonth + 1);
|
|
219
221
|
}
|
|
220
|
-
function parseDateRange(text) {
|
|
222
|
+
function parseDateRange(text, referenceDate) {
|
|
221
223
|
const normalized = text.trim();
|
|
222
224
|
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
225
|
if (!rangeMatch) {
|
|
@@ -230,9 +232,10 @@ function parseDateRange(text) {
|
|
|
230
232
|
if (!startToken) {
|
|
231
233
|
return null;
|
|
232
234
|
}
|
|
235
|
+
const ref = referenceDate ?? /* @__PURE__ */ new Date();
|
|
233
236
|
const endTokenResolved = endToken ?? {
|
|
234
|
-
year:
|
|
235
|
-
month:
|
|
237
|
+
year: ref.getFullYear(),
|
|
238
|
+
month: ref.getMonth() + 1
|
|
236
239
|
};
|
|
237
240
|
const durationInMonths = monthsBetween(startToken, endTokenResolved);
|
|
238
241
|
return {
|
|
@@ -327,7 +330,7 @@ function parseActionVerbs(text) {
|
|
|
327
330
|
const words = tokenize(text);
|
|
328
331
|
return ACTION_VERBS.filter((verb) => words.includes(verb));
|
|
329
332
|
}
|
|
330
|
-
function parseExperience(sectionContent) {
|
|
333
|
+
function parseExperience(sectionContent, referenceDate) {
|
|
331
334
|
if (!sectionContent) {
|
|
332
335
|
return { entries: [], rangesInMonths: [], jobTitles: [] };
|
|
333
336
|
}
|
|
@@ -336,7 +339,7 @@ function parseExperience(sectionContent) {
|
|
|
336
339
|
const rangesInMonths = [];
|
|
337
340
|
const jobTitles = [];
|
|
338
341
|
for (const line of lines) {
|
|
339
|
-
const range = parseDateRange(line);
|
|
342
|
+
const range = parseDateRange(line, referenceDate);
|
|
340
343
|
if (range) {
|
|
341
344
|
const previous = entries[entries.length - 1];
|
|
342
345
|
if (previous && !previous.dates) {
|
|
@@ -376,7 +379,7 @@ function parseResume(resumeText, config) {
|
|
|
376
379
|
const { sections, detected } = extractSections(resumeText);
|
|
377
380
|
const skills = parseSkills(sections.skills, config.skillAliases);
|
|
378
381
|
const actionVerbs = parseActionVerbs(normalizedText);
|
|
379
|
-
const experienceData = parseExperience(sections.experience);
|
|
382
|
+
const experienceData = parseExperience(sections.experience, config.referenceDate);
|
|
380
383
|
const educationEntries = parseEducation(sections.education);
|
|
381
384
|
const totalExperienceYears = sumExperienceYears(
|
|
382
385
|
experienceData.entries.map((entry) => entry.dates).filter((range) => Boolean(range))
|
|
@@ -500,8 +503,9 @@ function scoreSkills(resume, job, config) {
|
|
|
500
503
|
0,
|
|
501
504
|
100
|
|
502
505
|
);
|
|
503
|
-
const
|
|
504
|
-
|
|
506
|
+
const matched = [...required].filter((skill) => resumeSkills.has(skill)).sort();
|
|
507
|
+
const missing = [...required].filter((skill) => !resumeSkills.has(skill)).sort();
|
|
508
|
+
return { score, matched, missing };
|
|
505
509
|
}
|
|
506
510
|
function scoreExperience(resume, job, config) {
|
|
507
511
|
const requiredYears = job.minExperienceYears ?? config.profile?.minExperience ?? 0;
|
|
@@ -519,11 +523,15 @@ function scoreExperience(resume, job, config) {
|
|
|
519
523
|
return { score, missingYears: Number(missingYears.toFixed(2)) };
|
|
520
524
|
}
|
|
521
525
|
function scoreKeywords(resume, job, config) {
|
|
522
|
-
const jobKeywordSet = new Set(
|
|
526
|
+
const jobKeywordSet = new Set(
|
|
527
|
+
job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
|
|
528
|
+
);
|
|
523
529
|
if (jobKeywordSet.size === 0) {
|
|
524
530
|
return { score: 100, matchedKeywords: [], missingKeywords: [], overusedKeywords: [] };
|
|
525
531
|
}
|
|
526
|
-
const resumeTokens = tokenize(resume.normalizedText)
|
|
532
|
+
const resumeTokens = tokenize(resume.normalizedText).map(
|
|
533
|
+
(t) => normalizeSkill(t, config.skillAliases)
|
|
534
|
+
);
|
|
527
535
|
const resumeTokenSet = new Set(resumeTokens);
|
|
528
536
|
const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
|
|
529
537
|
const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
|
|
@@ -537,9 +545,9 @@ function scoreKeywords(resume, job, config) {
|
|
|
537
545
|
});
|
|
538
546
|
return {
|
|
539
547
|
score,
|
|
540
|
-
matchedKeywords: unique(matchedKeywords),
|
|
541
|
-
missingKeywords: unique(missingKeywords),
|
|
542
|
-
overusedKeywords: unique(overusedKeywords)
|
|
548
|
+
matchedKeywords: unique(matchedKeywords).sort(),
|
|
549
|
+
missingKeywords: unique(missingKeywords).sort(),
|
|
550
|
+
overusedKeywords: unique(overusedKeywords).sort()
|
|
543
551
|
};
|
|
544
552
|
}
|
|
545
553
|
function scoreEducation(resume, job) {
|
|
@@ -571,12 +579,17 @@ function calculateScore(resume, job, config) {
|
|
|
571
579
|
return {
|
|
572
580
|
score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
|
|
573
581
|
breakdown,
|
|
582
|
+
matchedSkills: skillsResult.matched,
|
|
583
|
+
missingSkills: skillsResult.missing,
|
|
574
584
|
matchedKeywords: keywordResult.matchedKeywords,
|
|
575
585
|
missingKeywords: keywordResult.missingKeywords,
|
|
576
586
|
overusedKeywords: keywordResult.overusedKeywords,
|
|
577
587
|
suggestions: [],
|
|
578
588
|
warnings: [],
|
|
579
|
-
|
|
589
|
+
// detectedSections / parsedExperienceYears / experienceGap: filled by index.ts
|
|
590
|
+
experienceGap: experienceResult.missingYears,
|
|
591
|
+
detectedSections: [],
|
|
592
|
+
parsedExperienceYears: 0,
|
|
580
593
|
missingExperienceYears: experienceResult.missingYears,
|
|
581
594
|
educationScore
|
|
582
595
|
};
|
|
@@ -584,7 +597,9 @@ function calculateScore(resume, job, config) {
|
|
|
584
597
|
|
|
585
598
|
// src/profiles/index.ts
|
|
586
599
|
var defaultSkillAliases = {
|
|
587
|
-
|
|
600
|
+
// ponytail: "node" split from javascript — Node.js runtime !== JS language
|
|
601
|
+
javascript: ["js"],
|
|
602
|
+
node: ["node.js", "nodejs"],
|
|
588
603
|
typescript: ["ts"],
|
|
589
604
|
react: ["reactjs", "react.js"],
|
|
590
605
|
"c++": ["cpp"],
|
|
@@ -679,7 +694,8 @@ function resolveConfig(config = {}) {
|
|
|
679
694
|
...DEFAULT_SECTION_PENALTIES,
|
|
680
695
|
...config.sectionPenalties ?? {}
|
|
681
696
|
},
|
|
682
|
-
allowPartialMatches: config.allowPartialMatches ?? true
|
|
697
|
+
allowPartialMatches: config.allowPartialMatches ?? true,
|
|
698
|
+
referenceDate: config.referenceDate ? new Date(config.referenceDate) : void 0
|
|
683
699
|
};
|
|
684
700
|
return resolved;
|
|
685
701
|
}
|
|
@@ -1376,9 +1392,14 @@ function analyzeResume(input) {
|
|
|
1376
1392
|
return {
|
|
1377
1393
|
score: finalScore,
|
|
1378
1394
|
breakdown: scoring.breakdown,
|
|
1395
|
+
matchedSkills: scoring.matchedSkills,
|
|
1396
|
+
missingSkills: scoring.missingSkills,
|
|
1379
1397
|
matchedKeywords: scoring.matchedKeywords,
|
|
1380
1398
|
missingKeywords: scoring.missingKeywords,
|
|
1381
1399
|
overusedKeywords: scoring.overusedKeywords,
|
|
1400
|
+
experienceGap: scoring.experienceGap,
|
|
1401
|
+
detectedSections: parsedResume.detectedSections,
|
|
1402
|
+
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
1382
1403
|
suggestions,
|
|
1383
1404
|
warnings: [...suggestionResult.warnings, ...llmWarnings]
|
|
1384
1405
|
};
|
|
@@ -1435,9 +1456,14 @@ async function analyzeResumeAsync(input) {
|
|
|
1435
1456
|
return {
|
|
1436
1457
|
score: finalScore,
|
|
1437
1458
|
breakdown: scoring.breakdown,
|
|
1459
|
+
matchedSkills: scoring.matchedSkills,
|
|
1460
|
+
missingSkills: scoring.missingSkills,
|
|
1438
1461
|
matchedKeywords: scoring.matchedKeywords,
|
|
1439
1462
|
missingKeywords: scoring.missingKeywords,
|
|
1440
1463
|
overusedKeywords: scoring.overusedKeywords,
|
|
1464
|
+
experienceGap: scoring.experienceGap,
|
|
1465
|
+
detectedSections: parsedResume.detectedSections,
|
|
1466
|
+
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
1441
1467
|
suggestions,
|
|
1442
1468
|
warnings: [...suggestionResult.warnings, ...llmWarnings]
|
|
1443
1469
|
};
|