resuml 3.0.0 → 3.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/bin/resuml +3 -2
- package/dist/ats/index.d.ts +83 -0
- package/dist/ats/index.js +8 -0
- package/dist/ats/index.js.map +1 -0
- package/dist/chunk-G4AN2EMI.js +461 -0
- package/dist/chunk-G4AN2EMI.js.map +1 -0
- package/dist/chunk-M6JY5UDJ.js +778 -0
- package/dist/chunk-M6JY5UDJ.js.map +1 -0
- package/dist/chunk-N55EPZ2N.js +1836 -0
- package/dist/chunk-N55EPZ2N.js.map +1 -0
- package/dist/{chunk-R4MD5YMV.js → chunk-QR77BRMN.js} +154 -2434
- package/dist/chunk-QR77BRMN.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +24 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +4 -424
- package/dist/index.js +10 -775
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +6 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/skills/index.d.ts +67 -0
- package/dist/skills/index.js +13 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/types/index.d.ts +343 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +28 -3
- package/dist/chunk-R4MD5YMV.js.map +0 -1
- package/src/types/resume.ts +0 -344
- package/src/types/schema.d.ts +0 -6
|
@@ -4,1442 +4,169 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
// src/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
throw new Error("No YAML content provided for processing.");
|
|
14
|
-
}
|
|
15
|
-
const dataObjects = yamlContents.map((content) => {
|
|
16
|
-
try {
|
|
17
|
-
return parse(content);
|
|
18
|
-
} catch (error) {
|
|
19
|
-
console.warn("Failed to parse YAML content:", error);
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}).filter((data) => typeof data === "object" && data !== null);
|
|
23
|
-
if (dataObjects.length === 0) {
|
|
24
|
-
throw new Error("No valid YAML content found after parsing.");
|
|
25
|
-
}
|
|
26
|
-
const customizer = (objValue, srcValue) => {
|
|
27
|
-
if (Array.isArray(objValue)) {
|
|
28
|
-
return objValue.concat(srcValue);
|
|
29
|
-
}
|
|
30
|
-
return void 0;
|
|
31
|
-
};
|
|
32
|
-
const mergedData = dataObjects.reduce((acc, data) => merge(acc, data, customizer), {});
|
|
33
|
-
return new Promise((resolve, reject) => {
|
|
34
|
-
validate(mergedData, (errors, isValid) => {
|
|
35
|
-
if (!isValid) {
|
|
36
|
-
reject(
|
|
37
|
-
new Error(`Resume data failed schema validation: ${JSON.stringify(errors, null, 2)}`)
|
|
38
|
-
);
|
|
39
|
-
} else {
|
|
40
|
-
resolve(mergedData);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
});
|
|
7
|
+
// src/ats/skills/matcher.ts
|
|
8
|
+
function isTokenChar(ch) {
|
|
9
|
+
if (ch >= "a" && ch <= "z") return true;
|
|
10
|
+
if (ch >= "A" && ch <= "Z") return true;
|
|
11
|
+
if (ch >= "0" && ch <= "9") return true;
|
|
12
|
+
return ch === "." || ch === "/" || ch === "-" || ch === "+" || ch === "#" || ch === "_";
|
|
44
13
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
import fs from "fs";
|
|
48
|
-
import path from "path";
|
|
49
|
-
import yaml from "yaml";
|
|
50
|
-
import { z } from "zod";
|
|
51
|
-
import merge2 from "lodash.merge";
|
|
52
|
-
var weightEnum = z.enum(["high", "medium", "low"]);
|
|
53
|
-
var atsConfigSchema = z.object({
|
|
54
|
-
weights: z.object({
|
|
55
|
-
tiers: z.object({
|
|
56
|
-
parsing: z.number().int().min(0).max(100),
|
|
57
|
-
match: z.number().int().min(0).max(100),
|
|
58
|
-
recruiter: z.number().int().min(0).max(100)
|
|
59
|
-
}).partial().optional(),
|
|
60
|
-
checks: z.record(z.string(), weightEnum).optional()
|
|
61
|
-
}).partial().optional(),
|
|
62
|
-
thresholds: z.object({
|
|
63
|
-
rating: z.object({
|
|
64
|
-
excellent: z.number(),
|
|
65
|
-
good: z.number(),
|
|
66
|
-
needsWork: z.number()
|
|
67
|
-
}).partial().optional(),
|
|
68
|
-
grade: z.object({ A: z.number(), B: z.number(), C: z.number(), D: z.number() }).partial().optional(),
|
|
69
|
-
seniorYoeCutoff: z.number().int().min(0).optional(),
|
|
70
|
-
wordCount: z.object({ min: z.number(), max: z.number(), seniorMax: z.number() }).partial().optional(),
|
|
71
|
-
bulletsPerRole: z.object({ min: z.number(), max: z.number(), seniorMax: z.number() }).partial().optional()
|
|
72
|
-
}).partial().optional(),
|
|
73
|
-
disable: z.array(z.string()).optional(),
|
|
74
|
-
locale: z.string().optional()
|
|
75
|
-
});
|
|
76
|
-
var fileSchema = z.object({ ats: atsConfigSchema.optional() });
|
|
77
|
-
var defaultConfig = {
|
|
78
|
-
weights: {
|
|
79
|
-
tiers: { parsing: 30, match: 50, recruiter: 20 },
|
|
80
|
-
checks: {}
|
|
81
|
-
},
|
|
82
|
-
thresholds: {
|
|
83
|
-
rating: { excellent: 90, good: 75, needsWork: 60 },
|
|
84
|
-
grade: { A: 90, B: 80, C: 70, D: 60 },
|
|
85
|
-
seniorYoeCutoff: 10,
|
|
86
|
-
wordCount: { min: 400, max: 800, seniorMax: 1600 },
|
|
87
|
-
bulletsPerRole: { min: 3, max: 6, seniorMax: 10 }
|
|
88
|
-
},
|
|
89
|
-
disable: [],
|
|
90
|
-
locale: "en"
|
|
91
|
-
};
|
|
92
|
-
function loadConfig(opts = {}) {
|
|
93
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
94
|
-
const file = opts.configPath ?? path.join(cwd, "resuml.config.yaml");
|
|
95
|
-
if (!fs.existsSync(file)) return defaultConfig;
|
|
96
|
-
const raw = fs.readFileSync(file, "utf8");
|
|
97
|
-
const parsed = yaml.parse(raw) ?? {};
|
|
98
|
-
const result = fileSchema.safeParse(parsed);
|
|
99
|
-
if (!result.success) {
|
|
100
|
-
const issue = result.error.issues[0];
|
|
101
|
-
const where = issue?.path.join(".") ?? "<root>";
|
|
102
|
-
throw new Error(`Invalid resuml.config.yaml at "${where}": ${issue?.message}`);
|
|
103
|
-
}
|
|
104
|
-
return merge2({}, defaultConfig, result.data.ats ?? {});
|
|
14
|
+
function isLetter(ch) {
|
|
15
|
+
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z";
|
|
105
16
|
}
|
|
106
|
-
function
|
|
107
|
-
return
|
|
17
|
+
function isDigit(ch) {
|
|
18
|
+
return ch >= "0" && ch <= "9";
|
|
108
19
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (Array.isArray(v)) return v.length === 0;
|
|
117
|
-
return v === void 0;
|
|
118
|
-
});
|
|
119
|
-
const passed = missing.length === 0;
|
|
120
|
-
return {
|
|
121
|
-
id: "conventional-sections",
|
|
122
|
-
tier: "parsing",
|
|
123
|
-
weight: "high",
|
|
124
|
-
status: passed ? "pass" : "fail",
|
|
125
|
-
score: Math.round((required.length - missing.length) / required.length * 100),
|
|
126
|
-
message: passed ? "All conventional sections present." : `Missing required sections: ${missing.join(", ")}.`,
|
|
127
|
-
hints: passed ? [] : [`Add ${missing.join(", ")} to your YAML.`]
|
|
128
|
-
};
|
|
129
|
-
};
|
|
130
|
-
var dateFormatConsistency = (resume) => {
|
|
131
|
-
const all = [];
|
|
132
|
-
for (const w of resume.work || []) {
|
|
133
|
-
if (w.startDate) all.push({ date: w.startDate, where: `work "${w.name || ""}".startDate` });
|
|
134
|
-
if (w.endDate) all.push({ date: w.endDate, where: `work "${w.name || ""}".endDate` });
|
|
135
|
-
}
|
|
136
|
-
for (const e of resume.education || []) {
|
|
137
|
-
if (e.startDate)
|
|
138
|
-
all.push({ date: e.startDate, where: `education "${e.institution || ""}".startDate` });
|
|
139
|
-
if (e.endDate)
|
|
140
|
-
all.push({ date: e.endDate, where: `education "${e.institution || ""}".endDate` });
|
|
141
|
-
}
|
|
142
|
-
if (all.length === 0) {
|
|
143
|
-
return {
|
|
144
|
-
id: "date-format-consistency",
|
|
145
|
-
tier: "parsing",
|
|
146
|
-
weight: "medium",
|
|
147
|
-
status: "skipped",
|
|
148
|
-
score: 0,
|
|
149
|
-
message: "No dates to check.",
|
|
150
|
-
hints: []
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
const bad = all.filter((d) => !ISO_DATE.test(d.date));
|
|
154
|
-
const passed = bad.length === 0;
|
|
155
|
-
return {
|
|
156
|
-
id: "date-format-consistency",
|
|
157
|
-
tier: "parsing",
|
|
158
|
-
weight: "medium",
|
|
159
|
-
status: passed ? "pass" : bad.length <= 1 ? "warn" : "fail",
|
|
160
|
-
score: Math.round((all.length - bad.length) / all.length * 100),
|
|
161
|
-
message: passed ? "All dates use ISO-8601 format." : `Non-ISO dates: ${bad.slice(0, 3).map((b) => `${b.where}=${b.date}`).join("; ")}.`,
|
|
162
|
-
hints: passed ? [] : ["Use YYYY-MM or YYYY-MM-DD for every date field."]
|
|
163
|
-
};
|
|
164
|
-
};
|
|
165
|
-
var contactInBody = (resume) => {
|
|
166
|
-
const b = resume.basics;
|
|
167
|
-
const checks = [
|
|
168
|
-
{ ok: !!b?.name, field: "name" },
|
|
169
|
-
{ ok: !!b?.email, field: "email" },
|
|
170
|
-
{ ok: !!b?.phone, field: "phone" },
|
|
171
|
-
{ ok: !!b?.location?.city, field: "location.city" }
|
|
172
|
-
];
|
|
173
|
-
const missing = checks.filter((c) => !c.ok).map((c) => c.field);
|
|
174
|
-
const passed = missing.length === 0;
|
|
175
|
-
return {
|
|
176
|
-
id: "contact-in-body",
|
|
177
|
-
tier: "parsing",
|
|
178
|
-
weight: "high",
|
|
179
|
-
status: passed ? "pass" : "fail",
|
|
180
|
-
score: Math.round((checks.length - missing.length) / checks.length * 100),
|
|
181
|
-
message: passed ? "Contact information present in basics." : `Missing contact fields: ${missing.join(", ")}.`,
|
|
182
|
-
hints: passed ? [] : [`Add ${missing.join(", ")} to basics.`]
|
|
183
|
-
};
|
|
184
|
-
};
|
|
185
|
-
var reverseChronOrder = (resume) => {
|
|
186
|
-
const work = resume.work || [];
|
|
187
|
-
if (work.length < 2) {
|
|
188
|
-
return {
|
|
189
|
-
id: "reverse-chron-order",
|
|
190
|
-
tier: "parsing",
|
|
191
|
-
weight: "medium",
|
|
192
|
-
status: "skipped",
|
|
193
|
-
score: 100,
|
|
194
|
-
message: "Single or no work entry.",
|
|
195
|
-
hints: []
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
let outOfOrder = 0;
|
|
199
|
-
for (let i = 0; i < work.length - 1; i++) {
|
|
200
|
-
const curr = work[i];
|
|
201
|
-
const next = work[i + 1];
|
|
202
|
-
const a = curr?.startDate || "";
|
|
203
|
-
const b = next?.startDate || "";
|
|
204
|
-
if (a && b && a < b) outOfOrder++;
|
|
20
|
+
function trimTokenBoundary(tok) {
|
|
21
|
+
let start = 0;
|
|
22
|
+
let end = tok.length;
|
|
23
|
+
while (start < end) {
|
|
24
|
+
const ch = tok.charAt(start);
|
|
25
|
+
if (isLetter(ch) || isDigit(ch) || ch === "+" || ch === "#") break;
|
|
26
|
+
start++;
|
|
205
27
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
weight: "medium",
|
|
211
|
-
status: passed ? "pass" : "fail",
|
|
212
|
-
score: passed ? 100 : Math.max(0, 100 - outOfOrder * 50),
|
|
213
|
-
message: passed ? "Work entries in reverse-chronological order." : `${outOfOrder} pair(s) out of reverse-chronological order.`,
|
|
214
|
-
hints: passed ? [] : ["Reorder work[] so the most recent role is first."]
|
|
215
|
-
};
|
|
216
|
-
};
|
|
217
|
-
var educationComplete = (resume) => {
|
|
218
|
-
const edu = resume.education || [];
|
|
219
|
-
if (edu.length === 0) {
|
|
220
|
-
return {
|
|
221
|
-
id: "education-complete",
|
|
222
|
-
tier: "parsing",
|
|
223
|
-
weight: "low",
|
|
224
|
-
status: "fail",
|
|
225
|
-
score: 0,
|
|
226
|
-
message: "No education entries.",
|
|
227
|
-
hints: ["Add at least one education entry with institution, area, and studyType."]
|
|
228
|
-
};
|
|
28
|
+
while (end > start) {
|
|
29
|
+
const ch = tok.charAt(end - 1);
|
|
30
|
+
if (isLetter(ch) || isDigit(ch) || ch === "+" || ch === "#") break;
|
|
31
|
+
end--;
|
|
229
32
|
}
|
|
230
|
-
|
|
231
|
-
const passed = incomplete.length === 0;
|
|
232
|
-
return {
|
|
233
|
-
id: "education-complete",
|
|
234
|
-
tier: "parsing",
|
|
235
|
-
weight: "low",
|
|
236
|
-
status: passed ? "pass" : "fail",
|
|
237
|
-
score: Math.round((edu.length - incomplete.length) / edu.length * 100),
|
|
238
|
-
message: passed ? "All education entries complete." : `${incomplete.length} education entry(ies) missing institution/area/studyType.`,
|
|
239
|
-
hints: passed ? [] : ["Fill in institution, area and studyType for every education entry."]
|
|
240
|
-
};
|
|
241
|
-
};
|
|
242
|
-
var allParsingChecks = [
|
|
243
|
-
conventionalSections,
|
|
244
|
-
dateFormatConsistency,
|
|
245
|
-
contactInBody,
|
|
246
|
-
reverseChronOrder,
|
|
247
|
-
educationComplete
|
|
248
|
-
];
|
|
249
|
-
|
|
250
|
-
// src/ats/i18n/en.ts
|
|
251
|
-
var en = {
|
|
252
|
-
actionVerbs: [
|
|
253
|
-
// Leadership & Management
|
|
254
|
-
"achieved",
|
|
255
|
-
"administered",
|
|
256
|
-
"advanced",
|
|
257
|
-
"allocated",
|
|
258
|
-
"approved",
|
|
259
|
-
"assigned",
|
|
260
|
-
"authorized",
|
|
261
|
-
"chaired",
|
|
262
|
-
"consolidated",
|
|
263
|
-
"coordinated",
|
|
264
|
-
"delegated",
|
|
265
|
-
"directed",
|
|
266
|
-
"established",
|
|
267
|
-
"executed",
|
|
268
|
-
"headed",
|
|
269
|
-
"hired",
|
|
270
|
-
"hosted",
|
|
271
|
-
"led",
|
|
272
|
-
"managed",
|
|
273
|
-
"mentored",
|
|
274
|
-
"motivated",
|
|
275
|
-
"orchestrated",
|
|
276
|
-
"organized",
|
|
277
|
-
"oversaw",
|
|
278
|
-
"planned",
|
|
279
|
-
"presided",
|
|
280
|
-
"prioritized",
|
|
281
|
-
"produced",
|
|
282
|
-
"recruited",
|
|
283
|
-
"spearheaded",
|
|
284
|
-
"supervised",
|
|
285
|
-
// Technical & Engineering
|
|
286
|
-
"architected",
|
|
287
|
-
"automated",
|
|
288
|
-
"built",
|
|
289
|
-
"coded",
|
|
290
|
-
"configured",
|
|
291
|
-
"debugged",
|
|
292
|
-
"deployed",
|
|
293
|
-
"designed",
|
|
294
|
-
"developed",
|
|
295
|
-
"devised",
|
|
296
|
-
"engineered",
|
|
297
|
-
"implemented",
|
|
298
|
-
"installed",
|
|
299
|
-
"integrated",
|
|
300
|
-
"launched",
|
|
301
|
-
"maintained",
|
|
302
|
-
"migrated",
|
|
303
|
-
"modernized",
|
|
304
|
-
"optimized",
|
|
305
|
-
"overhauled",
|
|
306
|
-
"programmed",
|
|
307
|
-
"prototyped",
|
|
308
|
-
"refactored",
|
|
309
|
-
"reengineered",
|
|
310
|
-
"resolved",
|
|
311
|
-
"restructured",
|
|
312
|
-
"revamped",
|
|
313
|
-
"scaled",
|
|
314
|
-
"shipped",
|
|
315
|
-
"standardized",
|
|
316
|
-
"streamlined",
|
|
317
|
-
"tested",
|
|
318
|
-
"troubleshot",
|
|
319
|
-
"upgraded",
|
|
320
|
-
// Achievement & Impact
|
|
321
|
-
"accelerated",
|
|
322
|
-
"accomplished",
|
|
323
|
-
"boosted",
|
|
324
|
-
"completed",
|
|
325
|
-
"contributed",
|
|
326
|
-
"converted",
|
|
327
|
-
"decreased",
|
|
328
|
-
"delivered",
|
|
329
|
-
"doubled",
|
|
330
|
-
"earned",
|
|
331
|
-
"eliminated",
|
|
332
|
-
"exceeded",
|
|
333
|
-
"expanded",
|
|
334
|
-
"expedited",
|
|
335
|
-
"generated",
|
|
336
|
-
"grew",
|
|
337
|
-
"improved",
|
|
338
|
-
"increased",
|
|
339
|
-
"maximized",
|
|
340
|
-
"minimized",
|
|
341
|
-
"outperformed",
|
|
342
|
-
"pioneered",
|
|
343
|
-
"recovered",
|
|
344
|
-
"reduced",
|
|
345
|
-
"saved",
|
|
346
|
-
"simplified",
|
|
347
|
-
"solved",
|
|
348
|
-
"surpassed",
|
|
349
|
-
"transformed",
|
|
350
|
-
"tripled",
|
|
351
|
-
// Communication & Collaboration
|
|
352
|
-
"advised",
|
|
353
|
-
"advocated",
|
|
354
|
-
"briefed",
|
|
355
|
-
"collaborated",
|
|
356
|
-
"communicated",
|
|
357
|
-
"consulted",
|
|
358
|
-
"convinced",
|
|
359
|
-
"counseled",
|
|
360
|
-
"defined",
|
|
361
|
-
"demonstrated",
|
|
362
|
-
"documented",
|
|
363
|
-
"educated",
|
|
364
|
-
"facilitated",
|
|
365
|
-
"guided",
|
|
366
|
-
"influenced",
|
|
367
|
-
"informed",
|
|
368
|
-
"instructed",
|
|
369
|
-
"liaised",
|
|
370
|
-
"negotiated",
|
|
371
|
-
"partnered",
|
|
372
|
-
"persuaded",
|
|
373
|
-
"presented",
|
|
374
|
-
"promoted",
|
|
375
|
-
"proposed",
|
|
376
|
-
"published",
|
|
377
|
-
"recommended",
|
|
378
|
-
"represented",
|
|
379
|
-
"trained",
|
|
380
|
-
// Analysis & Research
|
|
381
|
-
"analyzed",
|
|
382
|
-
"assessed",
|
|
383
|
-
"audited",
|
|
384
|
-
"benchmarked",
|
|
385
|
-
"calculated",
|
|
386
|
-
"compared",
|
|
387
|
-
"compiled",
|
|
388
|
-
"conducted",
|
|
389
|
-
"discovered",
|
|
390
|
-
"evaluated",
|
|
391
|
-
"examined",
|
|
392
|
-
"explored",
|
|
393
|
-
"forecasted",
|
|
394
|
-
"identified",
|
|
395
|
-
"inspected",
|
|
396
|
-
"interpreted",
|
|
397
|
-
"investigated",
|
|
398
|
-
"mapped",
|
|
399
|
-
"measured",
|
|
400
|
-
"modeled",
|
|
401
|
-
"monitored",
|
|
402
|
-
"quantified",
|
|
403
|
-
"researched",
|
|
404
|
-
"reviewed",
|
|
405
|
-
"surveyed",
|
|
406
|
-
"synthesized",
|
|
407
|
-
"tracked",
|
|
408
|
-
"validated",
|
|
409
|
-
"verified",
|
|
410
|
-
// Creation & Innovation
|
|
411
|
-
"conceptualized",
|
|
412
|
-
"crafted",
|
|
413
|
-
"created",
|
|
414
|
-
"customized",
|
|
415
|
-
"formulated",
|
|
416
|
-
"founded",
|
|
417
|
-
"initiated",
|
|
418
|
-
"innovated",
|
|
419
|
-
"introduced",
|
|
420
|
-
"invented",
|
|
421
|
-
"originated",
|
|
422
|
-
"shaped"
|
|
423
|
-
],
|
|
424
|
-
pronouns: ["i", "me", "my", "mine", "myself", "we", "our", "ours"],
|
|
425
|
-
stopWords: [
|
|
426
|
-
// Articles & determiners
|
|
427
|
-
"a",
|
|
428
|
-
"an",
|
|
429
|
-
"the",
|
|
430
|
-
"and",
|
|
431
|
-
"or",
|
|
432
|
-
"but",
|
|
433
|
-
"in",
|
|
434
|
-
"on",
|
|
435
|
-
"at",
|
|
436
|
-
"to",
|
|
437
|
-
"for",
|
|
438
|
-
"of",
|
|
439
|
-
"with",
|
|
440
|
-
"by",
|
|
441
|
-
"from",
|
|
442
|
-
"is",
|
|
443
|
-
"was",
|
|
444
|
-
"are",
|
|
445
|
-
"were",
|
|
446
|
-
"be",
|
|
447
|
-
"been",
|
|
448
|
-
"being",
|
|
449
|
-
"have",
|
|
450
|
-
"has",
|
|
451
|
-
"had",
|
|
452
|
-
"do",
|
|
453
|
-
"does",
|
|
454
|
-
"did",
|
|
455
|
-
"will",
|
|
456
|
-
"would",
|
|
457
|
-
"could",
|
|
458
|
-
"should",
|
|
459
|
-
"may",
|
|
460
|
-
"might",
|
|
461
|
-
"shall",
|
|
462
|
-
"can",
|
|
463
|
-
"this",
|
|
464
|
-
"that",
|
|
465
|
-
"these",
|
|
466
|
-
"those",
|
|
467
|
-
"it",
|
|
468
|
-
"its",
|
|
469
|
-
"as",
|
|
470
|
-
"if",
|
|
471
|
-
"not",
|
|
472
|
-
"no",
|
|
473
|
-
"so",
|
|
474
|
-
"up",
|
|
475
|
-
"out",
|
|
476
|
-
"about",
|
|
477
|
-
"into",
|
|
478
|
-
"over",
|
|
479
|
-
"after",
|
|
480
|
-
"before",
|
|
481
|
-
"between",
|
|
482
|
-
"under",
|
|
483
|
-
"above",
|
|
484
|
-
"below",
|
|
485
|
-
"all",
|
|
486
|
-
"each",
|
|
487
|
-
"every",
|
|
488
|
-
"both",
|
|
489
|
-
"few",
|
|
490
|
-
"more",
|
|
491
|
-
"most",
|
|
492
|
-
"other",
|
|
493
|
-
"some",
|
|
494
|
-
"such",
|
|
495
|
-
"than",
|
|
496
|
-
"too",
|
|
497
|
-
"very",
|
|
498
|
-
// Pronouns & possessives (also checked by pronoun check, but filter from JD keywords)
|
|
499
|
-
"you",
|
|
500
|
-
"your",
|
|
501
|
-
"yours",
|
|
502
|
-
"yourself",
|
|
503
|
-
"we",
|
|
504
|
-
"our",
|
|
505
|
-
"ours",
|
|
506
|
-
"ourselves",
|
|
507
|
-
"they",
|
|
508
|
-
"them",
|
|
509
|
-
"their",
|
|
510
|
-
"theirs",
|
|
511
|
-
"he",
|
|
512
|
-
"she",
|
|
513
|
-
"his",
|
|
514
|
-
"her",
|
|
515
|
-
"hers",
|
|
516
|
-
"who",
|
|
517
|
-
"whom",
|
|
518
|
-
"whose",
|
|
519
|
-
"which",
|
|
520
|
-
"what",
|
|
521
|
-
"where",
|
|
522
|
-
"when",
|
|
523
|
-
"how",
|
|
524
|
-
"why",
|
|
525
|
-
// Common JD filler words (not meaningful for skill matching)
|
|
526
|
-
"able",
|
|
527
|
-
"also",
|
|
528
|
-
"across",
|
|
529
|
-
"already",
|
|
530
|
-
"always",
|
|
531
|
-
"among",
|
|
532
|
-
"any",
|
|
533
|
-
"apply",
|
|
534
|
-
"become",
|
|
535
|
-
"believe",
|
|
536
|
-
"best",
|
|
537
|
-
"bring",
|
|
538
|
-
"change",
|
|
539
|
-
"come",
|
|
540
|
-
"committed",
|
|
541
|
-
"company",
|
|
542
|
-
"comfortable",
|
|
543
|
-
"critical",
|
|
544
|
-
"current",
|
|
545
|
-
"day",
|
|
546
|
-
"desired",
|
|
547
|
-
"either",
|
|
548
|
-
"end",
|
|
549
|
-
"ensure",
|
|
550
|
-
"environment",
|
|
551
|
-
"equal",
|
|
552
|
-
"even",
|
|
553
|
-
"excellent",
|
|
554
|
-
"exciting",
|
|
555
|
-
"exceptional",
|
|
556
|
-
"expected",
|
|
557
|
-
"experience",
|
|
558
|
-
"fast",
|
|
559
|
-
"field",
|
|
560
|
-
"find",
|
|
561
|
-
"first",
|
|
562
|
-
"focused",
|
|
563
|
-
"follow",
|
|
564
|
-
"get",
|
|
565
|
-
"give",
|
|
566
|
-
"go",
|
|
567
|
-
"going",
|
|
568
|
-
"good",
|
|
569
|
-
"great",
|
|
570
|
-
"group",
|
|
571
|
-
"grow",
|
|
572
|
-
"growing",
|
|
573
|
-
"growth",
|
|
574
|
-
"help",
|
|
575
|
-
"here",
|
|
576
|
-
"high",
|
|
577
|
-
"highly",
|
|
578
|
-
"ideal",
|
|
579
|
-
"impact",
|
|
580
|
-
"important",
|
|
581
|
-
"include",
|
|
582
|
-
"includes",
|
|
583
|
-
"including",
|
|
584
|
-
"industry",
|
|
585
|
-
"interested",
|
|
586
|
-
"job",
|
|
587
|
-
"join",
|
|
588
|
-
"just",
|
|
589
|
-
"keep",
|
|
590
|
-
"key",
|
|
591
|
-
"know",
|
|
592
|
-
"large",
|
|
593
|
-
"latest",
|
|
594
|
-
"lead",
|
|
595
|
-
"level",
|
|
596
|
-
"like",
|
|
597
|
-
"location",
|
|
598
|
-
"long",
|
|
599
|
-
"look",
|
|
600
|
-
"looking",
|
|
601
|
-
"love",
|
|
602
|
-
"make",
|
|
603
|
-
"many",
|
|
604
|
-
"much",
|
|
605
|
-
"must",
|
|
606
|
-
"need",
|
|
607
|
-
"new",
|
|
608
|
-
"next",
|
|
609
|
-
"offer",
|
|
610
|
-
"one",
|
|
611
|
-
"only",
|
|
612
|
-
"open",
|
|
613
|
-
"opportunity",
|
|
614
|
-
"order",
|
|
615
|
-
"others",
|
|
616
|
-
"own",
|
|
617
|
-
"pace",
|
|
618
|
-
"part",
|
|
619
|
-
"partner",
|
|
620
|
-
"passionate",
|
|
621
|
-
"people",
|
|
622
|
-
"per",
|
|
623
|
-
"play",
|
|
624
|
-
"plus",
|
|
625
|
-
"position",
|
|
626
|
-
"preferred",
|
|
627
|
-
"provide",
|
|
628
|
-
"put",
|
|
629
|
-
"qualifications",
|
|
630
|
-
"quickly",
|
|
631
|
-
"range",
|
|
632
|
-
"related",
|
|
633
|
-
"required",
|
|
634
|
-
"requirements",
|
|
635
|
-
"requirement",
|
|
636
|
-
"responsible",
|
|
637
|
-
"responsibilities",
|
|
638
|
-
"responsibility",
|
|
639
|
-
"result",
|
|
640
|
-
"right",
|
|
641
|
-
"role",
|
|
642
|
-
"run",
|
|
643
|
-
"same",
|
|
644
|
-
"see",
|
|
645
|
-
"seek",
|
|
646
|
-
"seeking",
|
|
647
|
-
"set",
|
|
648
|
-
"several",
|
|
649
|
-
"since",
|
|
650
|
-
"skills",
|
|
651
|
-
"someone",
|
|
652
|
-
"start",
|
|
653
|
-
"state",
|
|
654
|
-
"still",
|
|
655
|
-
"strong",
|
|
656
|
-
"success",
|
|
657
|
-
"successful",
|
|
658
|
-
"support",
|
|
659
|
-
"sure",
|
|
660
|
-
"take",
|
|
661
|
-
"team",
|
|
662
|
-
"then",
|
|
663
|
-
"there",
|
|
664
|
-
"thing",
|
|
665
|
-
"think",
|
|
666
|
-
"through",
|
|
667
|
-
"time",
|
|
668
|
-
"together",
|
|
669
|
-
"top",
|
|
670
|
-
"truly",
|
|
671
|
-
"try",
|
|
672
|
-
"two",
|
|
673
|
-
"type",
|
|
674
|
-
"use",
|
|
675
|
-
"used",
|
|
676
|
-
"using",
|
|
677
|
-
"value",
|
|
678
|
-
"want",
|
|
679
|
-
"way",
|
|
680
|
-
"well",
|
|
681
|
-
"while",
|
|
682
|
-
"within",
|
|
683
|
-
"without",
|
|
684
|
-
"work",
|
|
685
|
-
"working",
|
|
686
|
-
"world",
|
|
687
|
-
"would",
|
|
688
|
-
"year",
|
|
689
|
-
"years",
|
|
690
|
-
// Section headers & structural words (not technical skills)
|
|
691
|
-
"description",
|
|
692
|
-
"overview",
|
|
693
|
-
"summary",
|
|
694
|
-
"duties",
|
|
695
|
-
"bachelor",
|
|
696
|
-
"bachelors",
|
|
697
|
-
"master",
|
|
698
|
-
"masters",
|
|
699
|
-
"degree",
|
|
700
|
-
"phd",
|
|
701
|
-
"minimum",
|
|
702
|
-
"preferred",
|
|
703
|
-
"implement",
|
|
704
|
-
"process",
|
|
705
|
-
"robust",
|
|
706
|
-
"consistent",
|
|
707
|
-
"operations",
|
|
708
|
-
// URL/email/domain fragments
|
|
709
|
-
"http",
|
|
710
|
-
"https",
|
|
711
|
-
"www",
|
|
712
|
-
"com",
|
|
713
|
-
"org",
|
|
714
|
-
"net",
|
|
715
|
-
"mailto",
|
|
716
|
-
// Resume/YAML schema field names (in case raw YAML is pasted)
|
|
717
|
-
"name",
|
|
718
|
-
"keywords",
|
|
719
|
-
"highlights",
|
|
720
|
-
"startdate",
|
|
721
|
-
"enddate",
|
|
722
|
-
"website",
|
|
723
|
-
"profiles",
|
|
724
|
-
"basics",
|
|
725
|
-
"position",
|
|
726
|
-
"institution",
|
|
727
|
-
"studytype",
|
|
728
|
-
"fluency",
|
|
729
|
-
"issuer",
|
|
730
|
-
"network",
|
|
731
|
-
"username",
|
|
732
|
-
"countrycode",
|
|
733
|
-
"region",
|
|
734
|
-
// Generic nouns that aren't skills
|
|
735
|
-
"product",
|
|
736
|
-
"company",
|
|
737
|
-
"service",
|
|
738
|
-
"services",
|
|
739
|
-
"platform",
|
|
740
|
-
"solutions",
|
|
741
|
-
"ability",
|
|
742
|
-
"opportunity",
|
|
743
|
-
"candidate",
|
|
744
|
-
"applicant",
|
|
745
|
-
"position",
|
|
746
|
-
"salary",
|
|
747
|
-
"compensation",
|
|
748
|
-
"benefits",
|
|
749
|
-
"perks",
|
|
750
|
-
"bonus",
|
|
751
|
-
"development",
|
|
752
|
-
"management",
|
|
753
|
-
"knowledge",
|
|
754
|
-
"modern",
|
|
755
|
-
"advanced",
|
|
756
|
-
"practices",
|
|
757
|
-
"nice",
|
|
758
|
-
"technologies",
|
|
759
|
-
"technology",
|
|
760
|
-
"frameworks",
|
|
761
|
-
"framework",
|
|
762
|
-
"tools",
|
|
763
|
-
"data",
|
|
764
|
-
"based",
|
|
765
|
-
"contribute",
|
|
766
|
-
"contributions",
|
|
767
|
-
"migration",
|
|
768
|
-
"leading",
|
|
769
|
-
"source",
|
|
770
|
-
"visit",
|
|
771
|
-
// Common verbs & verb forms (not technical skills, supplement action verbs list)
|
|
772
|
-
"collaborate",
|
|
773
|
-
"collaborating",
|
|
774
|
-
"collaboratively",
|
|
775
|
-
"communicate",
|
|
776
|
-
"communicating",
|
|
777
|
-
"contributing",
|
|
778
|
-
"coordinate",
|
|
779
|
-
"coordinating",
|
|
780
|
-
"demonstrate",
|
|
781
|
-
"demonstrating",
|
|
782
|
-
"design",
|
|
783
|
-
"designing",
|
|
784
|
-
"designed",
|
|
785
|
-
"develop",
|
|
786
|
-
"developing",
|
|
787
|
-
"developed",
|
|
788
|
-
"drive",
|
|
789
|
-
"driving",
|
|
790
|
-
"driven",
|
|
791
|
-
"enable",
|
|
792
|
-
"enabling",
|
|
793
|
-
"evaluate",
|
|
794
|
-
"evaluating",
|
|
795
|
-
"execute",
|
|
796
|
-
"executing",
|
|
797
|
-
"facilitate",
|
|
798
|
-
"facilitating",
|
|
799
|
-
"identify",
|
|
800
|
-
"identifying",
|
|
801
|
-
"influence",
|
|
802
|
-
"influencing",
|
|
803
|
-
"interact",
|
|
804
|
-
"interacting",
|
|
805
|
-
"lead",
|
|
806
|
-
"leverage",
|
|
807
|
-
"leveraging",
|
|
808
|
-
"manage",
|
|
809
|
-
"managing",
|
|
810
|
-
"mentor",
|
|
811
|
-
"mentoring",
|
|
812
|
-
"operate",
|
|
813
|
-
"operating",
|
|
814
|
-
"optimize",
|
|
815
|
-
"optimizing",
|
|
816
|
-
"participate",
|
|
817
|
-
"participating",
|
|
818
|
-
"report",
|
|
819
|
-
"reporting",
|
|
820
|
-
"solve",
|
|
821
|
-
"solving",
|
|
822
|
-
"understand",
|
|
823
|
-
"understanding",
|
|
824
|
-
// Common adjectives & descriptors (not technical skills)
|
|
825
|
-
"fluent",
|
|
826
|
-
"proficient",
|
|
827
|
-
"deep",
|
|
828
|
-
"solid",
|
|
829
|
-
"proven",
|
|
830
|
-
"hands-on",
|
|
831
|
-
"detail-oriented",
|
|
832
|
-
"results-driven",
|
|
833
|
-
"self-motivated",
|
|
834
|
-
"proactive",
|
|
835
|
-
"creative",
|
|
836
|
-
"innovative",
|
|
837
|
-
"dynamic",
|
|
838
|
-
"strategic",
|
|
839
|
-
"analytical",
|
|
840
|
-
"collaborative",
|
|
841
|
-
"effective",
|
|
842
|
-
"efficient",
|
|
843
|
-
"reliable",
|
|
844
|
-
"flexible",
|
|
845
|
-
"adaptable",
|
|
846
|
-
"motivated",
|
|
847
|
-
"dedicated",
|
|
848
|
-
"capable",
|
|
849
|
-
"qualified",
|
|
850
|
-
"diverse",
|
|
851
|
-
"inclusive",
|
|
852
|
-
"global",
|
|
853
|
-
"local",
|
|
854
|
-
"remote",
|
|
855
|
-
"hybrid",
|
|
856
|
-
"onsite",
|
|
857
|
-
"full-time",
|
|
858
|
-
"part-time",
|
|
859
|
-
"contract",
|
|
860
|
-
"permanent",
|
|
861
|
-
// Role titles & department names (not skills themselves)
|
|
862
|
-
"designer",
|
|
863
|
-
"designers",
|
|
864
|
-
"developer",
|
|
865
|
-
"developers",
|
|
866
|
-
"engineer",
|
|
867
|
-
"engineers",
|
|
868
|
-
"manager",
|
|
869
|
-
"managers",
|
|
870
|
-
"director",
|
|
871
|
-
"analyst",
|
|
872
|
-
"analysts",
|
|
873
|
-
"architect",
|
|
874
|
-
"architects",
|
|
875
|
-
"consultant",
|
|
876
|
-
"consultants",
|
|
877
|
-
"specialist",
|
|
878
|
-
"specialists",
|
|
879
|
-
"coordinator",
|
|
880
|
-
"lead",
|
|
881
|
-
"principal",
|
|
882
|
-
"staff",
|
|
883
|
-
"junior",
|
|
884
|
-
"mid",
|
|
885
|
-
"department",
|
|
886
|
-
"organization",
|
|
887
|
-
"division",
|
|
888
|
-
"stakeholder",
|
|
889
|
-
"stakeholders",
|
|
890
|
-
"client",
|
|
891
|
-
"clients",
|
|
892
|
-
"customer",
|
|
893
|
-
"customers",
|
|
894
|
-
// Date & time words
|
|
895
|
-
"date",
|
|
896
|
-
"dates",
|
|
897
|
-
"month",
|
|
898
|
-
"months",
|
|
899
|
-
"week",
|
|
900
|
-
"weeks",
|
|
901
|
-
"daily",
|
|
902
|
-
"weekly",
|
|
903
|
-
"monthly",
|
|
904
|
-
"quarterly",
|
|
905
|
-
"annual",
|
|
906
|
-
"annually",
|
|
907
|
-
// More generic words that aren't skills
|
|
908
|
-
"code",
|
|
909
|
-
"coding",
|
|
910
|
-
"url",
|
|
911
|
-
"contact",
|
|
912
|
-
"information",
|
|
913
|
-
"apply",
|
|
914
|
-
"application",
|
|
915
|
-
"review",
|
|
916
|
-
"reviews",
|
|
917
|
-
"quality",
|
|
918
|
-
"scale",
|
|
919
|
-
"scalable",
|
|
920
|
-
"system",
|
|
921
|
-
"systems",
|
|
922
|
-
"solution",
|
|
923
|
-
"feature",
|
|
924
|
-
"features",
|
|
925
|
-
"project",
|
|
926
|
-
"projects",
|
|
927
|
-
"build",
|
|
928
|
-
"building",
|
|
929
|
-
"deliver",
|
|
930
|
-
"delivery",
|
|
931
|
-
"cross-functional"
|
|
932
|
-
]
|
|
933
|
-
};
|
|
934
|
-
var en_default = en;
|
|
935
|
-
|
|
936
|
-
// src/ats/i18n/de.ts
|
|
937
|
-
var de = {
|
|
938
|
-
actionVerbs: [
|
|
939
|
-
// Führung & Management
|
|
940
|
-
"geleitet",
|
|
941
|
-
"gef\xFChrt",
|
|
942
|
-
"koordiniert",
|
|
943
|
-
"organisiert",
|
|
944
|
-
"verwaltet",
|
|
945
|
-
"delegiert",
|
|
946
|
-
"beaufsichtigt",
|
|
947
|
-
"betreut",
|
|
948
|
-
"eingestellt",
|
|
949
|
-
"motiviert",
|
|
950
|
-
"verantwortet",
|
|
951
|
-
"gesteuert",
|
|
952
|
-
"\xFCberwacht",
|
|
953
|
-
"priorisiert",
|
|
954
|
-
"geplant",
|
|
955
|
-
// Technik & Entwicklung
|
|
956
|
-
"entwickelt",
|
|
957
|
-
"implementiert",
|
|
958
|
-
"programmiert",
|
|
959
|
-
"konfiguriert",
|
|
960
|
-
"automatisiert",
|
|
961
|
-
"deployt",
|
|
962
|
-
"gebaut",
|
|
963
|
-
"entworfen",
|
|
964
|
-
"integriert",
|
|
965
|
-
"migriert",
|
|
966
|
-
"modernisiert",
|
|
967
|
-
"optimiert",
|
|
968
|
-
"refaktoriert",
|
|
969
|
-
"skaliert",
|
|
970
|
-
"standardisiert",
|
|
971
|
-
"getestet",
|
|
972
|
-
"aufgebaut",
|
|
973
|
-
"eingef\xFChrt",
|
|
974
|
-
"bereitgestellt",
|
|
975
|
-
"umgesetzt",
|
|
976
|
-
// Leistung & Ergebnisse
|
|
977
|
-
"verbessert",
|
|
978
|
-
"gesteigert",
|
|
979
|
-
"reduziert",
|
|
980
|
-
"beschleunigt",
|
|
981
|
-
"erreicht",
|
|
982
|
-
"\xFCbertroffen",
|
|
983
|
-
"erweitert",
|
|
984
|
-
"vereinfacht",
|
|
985
|
-
"gel\xF6st",
|
|
986
|
-
"transformiert",
|
|
987
|
-
"erh\xF6ht",
|
|
988
|
-
"verdoppelt",
|
|
989
|
-
"verdreifacht",
|
|
990
|
-
"generiert",
|
|
991
|
-
"gespart",
|
|
992
|
-
"maximiert",
|
|
993
|
-
"minimiert",
|
|
994
|
-
"eliminiert",
|
|
995
|
-
"geliefert",
|
|
996
|
-
"abgeschlossen",
|
|
997
|
-
// Kommunikation & Zusammenarbeit
|
|
998
|
-
"beraten",
|
|
999
|
-
"pr\xE4sentiert",
|
|
1000
|
-
"dokumentiert",
|
|
1001
|
-
"geschult",
|
|
1002
|
-
"trainiert",
|
|
1003
|
-
"vermittelt",
|
|
1004
|
-
"kommuniziert",
|
|
1005
|
-
"verhandelt",
|
|
1006
|
-
"zusammengearbeitet",
|
|
1007
|
-
"unterst\xFCtzt",
|
|
1008
|
-
"gef\xF6rdert",
|
|
1009
|
-
"empfohlen",
|
|
1010
|
-
"vorgestellt",
|
|
1011
|
-
"publiziert",
|
|
1012
|
-
// Analyse & Forschung
|
|
1013
|
-
"analysiert",
|
|
1014
|
-
"bewertet",
|
|
1015
|
-
"evaluiert",
|
|
1016
|
-
"untersucht",
|
|
1017
|
-
"erforscht",
|
|
1018
|
-
"identifiziert",
|
|
1019
|
-
"gemessen",
|
|
1020
|
-
"\xFCberwacht",
|
|
1021
|
-
"validiert",
|
|
1022
|
-
"verifiziert",
|
|
1023
|
-
"gepr\xFCft",
|
|
1024
|
-
"verglichen",
|
|
1025
|
-
"recherchiert",
|
|
1026
|
-
"quantifiziert",
|
|
1027
|
-
// Kreation & Innovation
|
|
1028
|
-
"konzipiert",
|
|
1029
|
-
"erstellt",
|
|
1030
|
-
"gestaltet",
|
|
1031
|
-
"initiiert",
|
|
1032
|
-
"innoviert",
|
|
1033
|
-
"eingef\xFChrt",
|
|
1034
|
-
"gegr\xFCndet",
|
|
1035
|
-
"formuliert"
|
|
1036
|
-
],
|
|
1037
|
-
pronouns: [
|
|
1038
|
-
"ich",
|
|
1039
|
-
"mich",
|
|
1040
|
-
"mir",
|
|
1041
|
-
"mein",
|
|
1042
|
-
"meine",
|
|
1043
|
-
"meinem",
|
|
1044
|
-
"meiner",
|
|
1045
|
-
"meines",
|
|
1046
|
-
"wir",
|
|
1047
|
-
"unser",
|
|
1048
|
-
"unsere"
|
|
1049
|
-
],
|
|
1050
|
-
stopWords: [
|
|
1051
|
-
"ein",
|
|
1052
|
-
"eine",
|
|
1053
|
-
"einer",
|
|
1054
|
-
"eines",
|
|
1055
|
-
"einem",
|
|
1056
|
-
"der",
|
|
1057
|
-
"die",
|
|
1058
|
-
"das",
|
|
1059
|
-
"den",
|
|
1060
|
-
"dem",
|
|
1061
|
-
"des",
|
|
1062
|
-
"und",
|
|
1063
|
-
"oder",
|
|
1064
|
-
"aber",
|
|
1065
|
-
"in",
|
|
1066
|
-
"an",
|
|
1067
|
-
"auf",
|
|
1068
|
-
"zu",
|
|
1069
|
-
"f\xFCr",
|
|
1070
|
-
"von",
|
|
1071
|
-
"mit",
|
|
1072
|
-
"bei",
|
|
1073
|
-
"aus",
|
|
1074
|
-
"ist",
|
|
1075
|
-
"war",
|
|
1076
|
-
"sind",
|
|
1077
|
-
"waren",
|
|
1078
|
-
"wird",
|
|
1079
|
-
"wurde",
|
|
1080
|
-
"werden",
|
|
1081
|
-
"hat",
|
|
1082
|
-
"hatte",
|
|
1083
|
-
"haben",
|
|
1084
|
-
"hatten",
|
|
1085
|
-
"sein",
|
|
1086
|
-
"kann",
|
|
1087
|
-
"k\xF6nnte",
|
|
1088
|
-
"soll",
|
|
1089
|
-
"sollte",
|
|
1090
|
-
"muss",
|
|
1091
|
-
"musste",
|
|
1092
|
-
"darf",
|
|
1093
|
-
"diese",
|
|
1094
|
-
"dieser",
|
|
1095
|
-
"dieses",
|
|
1096
|
-
"diesem",
|
|
1097
|
-
"diesen",
|
|
1098
|
-
"als",
|
|
1099
|
-
"wenn",
|
|
1100
|
-
"nicht",
|
|
1101
|
-
"kein",
|
|
1102
|
-
"keine",
|
|
1103
|
-
"so",
|
|
1104
|
-
"auch",
|
|
1105
|
-
"noch",
|
|
1106
|
-
"schon",
|
|
1107
|
-
"nach",
|
|
1108
|
-
"vor",
|
|
1109
|
-
"\xFCber",
|
|
1110
|
-
"unter",
|
|
1111
|
-
"zwischen",
|
|
1112
|
-
"durch",
|
|
1113
|
-
"ohne",
|
|
1114
|
-
"um",
|
|
1115
|
-
"bis",
|
|
1116
|
-
"alle",
|
|
1117
|
-
"jede",
|
|
1118
|
-
"jeder",
|
|
1119
|
-
"jedes",
|
|
1120
|
-
"mehr",
|
|
1121
|
-
"viel",
|
|
1122
|
-
"sehr"
|
|
1123
|
-
]
|
|
1124
|
-
};
|
|
1125
|
-
var de_default = de;
|
|
1126
|
-
|
|
1127
|
-
// src/ats/i18n/index.ts
|
|
1128
|
-
var languages = { en: en_default, de: de_default };
|
|
1129
|
-
function getLanguageData(language) {
|
|
1130
|
-
return languages[language] ?? languages["en"] ?? en_default;
|
|
33
|
+
return start === 0 && end === tok.length ? tok : tok.slice(start, end);
|
|
1131
34
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
35
|
+
function tokenize(text) {
|
|
36
|
+
const tokens = [];
|
|
37
|
+
const len = text.length;
|
|
38
|
+
let i = 0;
|
|
39
|
+
while (i < len) {
|
|
40
|
+
while (i < len && !isTokenChar(text.charAt(i))) i++;
|
|
41
|
+
const start = i;
|
|
42
|
+
while (i < len && isTokenChar(text.charAt(i))) i++;
|
|
43
|
+
if (i === start) continue;
|
|
44
|
+
const raw = trimTokenBoundary(text.slice(start, i));
|
|
45
|
+
if (!raw) continue;
|
|
46
|
+
let hasCore = false;
|
|
47
|
+
for (let k = 0; k < raw.length; k++) {
|
|
48
|
+
const ch = raw.charAt(k);
|
|
49
|
+
if (isLetter(ch) || isDigit(ch) || ch === "+" || ch === "#") {
|
|
50
|
+
hasCore = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!hasCore) continue;
|
|
55
|
+
let letterCount = 0;
|
|
56
|
+
let upperCount = 0;
|
|
57
|
+
for (let k = 0; k < raw.length; k++) {
|
|
58
|
+
const ch = raw.charAt(k);
|
|
59
|
+
if (isLetter(ch)) {
|
|
60
|
+
letterCount++;
|
|
61
|
+
if (ch >= "A" && ch <= "Z") upperCount++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const isAllUpper = letterCount >= 2 && upperCount === letterCount;
|
|
65
|
+
tokens.push({ raw, norm: raw.toLowerCase(), isAllUpper });
|
|
1147
66
|
}
|
|
1148
|
-
|
|
1149
|
-
return ms / (1e3 * 60 * 60 * 24 * 365.25);
|
|
67
|
+
return tokens;
|
|
1150
68
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
var wordCount = (s) => s.trim().split(/\s+/).filter(Boolean).length;
|
|
1154
|
-
var firstWord = (s) => s.trim().split(/\s+/)[0]?.toLowerCase().replace(/[^a-zA-ZäöüßÄÖÜàáâãéèêëíìîïóòôõúùûñç]/g, "") || "";
|
|
1155
|
-
function isSenior(resume, cfg) {
|
|
1156
|
-
return computeYoeYears(resume.work) >= cfg.thresholds.seniorYoeCutoff;
|
|
69
|
+
function phraseToTokens(phrase) {
|
|
70
|
+
return phrase.toLowerCase().split(/\s+/).map((t) => trimTokenBoundary(t)).filter(Boolean);
|
|
1157
71
|
}
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
message: `Summary ${w < 20 ? "short" : "long"} (${w} words). Aim for 20-50.`,
|
|
1191
|
-
hints: [w < 20 ? "Expand to 20-50 words." : "Trim to the most impactful 20-50 words."]
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
return {
|
|
1195
|
-
id: "summary-length",
|
|
1196
|
-
tier: "recruiter",
|
|
1197
|
-
weight: "medium",
|
|
1198
|
-
status: "fail",
|
|
1199
|
-
score: Math.max(0, 100 - Math.abs(w - 35) * 2),
|
|
1200
|
-
message: `Summary length ${w} words is far from target.`,
|
|
1201
|
-
hints: ["Rewrite the summary in 2-4 sentences (20-50 words)."]
|
|
1202
|
-
};
|
|
1203
|
-
};
|
|
1204
|
-
var actionVerbStart = (resume, language) => {
|
|
1205
|
-
const verbs = new Set(getLanguageData(language).actionVerbs);
|
|
1206
|
-
const all = [];
|
|
1207
|
-
(resume.work || []).forEach((w, i) => {
|
|
1208
|
-
(w.highlights || []).forEach(
|
|
1209
|
-
(h, j) => all.push({ text: h, path: `work[${i}].highlights[${j}]` })
|
|
1210
|
-
);
|
|
1211
|
-
});
|
|
1212
|
-
(resume.projects || []).forEach((p, i) => {
|
|
1213
|
-
(p.highlights || []).forEach(
|
|
1214
|
-
(h, j) => all.push({ text: h, path: `projects[${i}].highlights[${j}]` })
|
|
1215
|
-
);
|
|
1216
|
-
});
|
|
1217
|
-
if (all.length === 0) {
|
|
1218
|
-
return {
|
|
1219
|
-
id: "action-verb-start",
|
|
1220
|
-
tier: "recruiter",
|
|
1221
|
-
weight: "medium",
|
|
1222
|
-
status: "skipped",
|
|
1223
|
-
score: 0,
|
|
1224
|
-
message: "No highlights.",
|
|
1225
|
-
hints: []
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
const without = all.filter((h) => !verbs.has(firstWord(h.text)));
|
|
1229
|
-
const passed = without.length === 0;
|
|
1230
|
-
return {
|
|
1231
|
-
id: "action-verb-start",
|
|
1232
|
-
tier: "recruiter",
|
|
1233
|
-
weight: "medium",
|
|
1234
|
-
status: passed ? "pass" : without.length / all.length > 0.3 ? "fail" : "warn",
|
|
1235
|
-
score: Math.round((all.length - without.length) / all.length * 100),
|
|
1236
|
-
message: passed ? "All highlights start with action verbs." : `${without.length} of ${all.length} highlights miss an action verb.`,
|
|
1237
|
-
hints: passed ? [] : without.slice(0, 3).map((h) => `${h.path}: start with an action verb instead of "${firstWord(h.text)}".`)
|
|
1238
|
-
};
|
|
1239
|
-
};
|
|
1240
|
-
var QUANT_RE = /\d+%?|\$[\d,]+|[\d,]+\+?\s*(users|clients|customers|people|team|members|projects|applications|servers|services|endpoints|requests|transactions)/i;
|
|
1241
|
-
var quantificationDensity = (resume) => {
|
|
1242
|
-
const all = [];
|
|
1243
|
-
(resume.work || []).forEach((w, i) => {
|
|
1244
|
-
(w.highlights || []).forEach(
|
|
1245
|
-
(h, j) => all.push({ text: h, path: `work[${i}].highlights[${j}]` })
|
|
1246
|
-
);
|
|
1247
|
-
});
|
|
1248
|
-
(resume.projects || []).forEach((p, i) => {
|
|
1249
|
-
(p.highlights || []).forEach(
|
|
1250
|
-
(h, j) => all.push({ text: h, path: `projects[${i}].highlights[${j}]` })
|
|
1251
|
-
);
|
|
1252
|
-
});
|
|
1253
|
-
if (all.length === 0) {
|
|
1254
|
-
return {
|
|
1255
|
-
id: "quantification-density",
|
|
1256
|
-
tier: "recruiter",
|
|
1257
|
-
weight: "high",
|
|
1258
|
-
status: "skipped",
|
|
1259
|
-
score: 0,
|
|
1260
|
-
message: "No highlights.",
|
|
1261
|
-
hints: []
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
const quantified = all.filter((h) => QUANT_RE.test(h.text));
|
|
1265
|
-
const ratio = quantified.length / all.length;
|
|
1266
|
-
const status = ratio >= 0.5 ? "pass" : ratio >= 0.3 ? "warn" : "fail";
|
|
1267
|
-
return {
|
|
1268
|
-
id: "quantification-density",
|
|
1269
|
-
tier: "recruiter",
|
|
1270
|
-
weight: "high",
|
|
1271
|
-
status,
|
|
1272
|
-
score: Math.min(100, Math.round(ratio * 200)),
|
|
1273
|
-
message: `${quantified.length}/${all.length} highlights quantified (${Math.round(ratio * 100)}%).`,
|
|
1274
|
-
hints: status === "pass" ? [] : all.filter((h) => !QUANT_RE.test(h.text)).slice(0, 3).map((h) => `${h.path}: add a number/metric.`)
|
|
1275
|
-
};
|
|
1276
|
-
};
|
|
1277
|
-
var pronounLeakage = (resume, language) => {
|
|
1278
|
-
const set = new Set(getLanguageData(language).pronouns);
|
|
1279
|
-
const blocks = [];
|
|
1280
|
-
if (resume.basics?.summary) blocks.push({ text: resume.basics.summary, path: "basics.summary" });
|
|
1281
|
-
(resume.work || []).forEach((w, i) => {
|
|
1282
|
-
if (w.summary) blocks.push({ text: w.summary, path: `work[${i}].summary` });
|
|
1283
|
-
(w.highlights || []).forEach(
|
|
1284
|
-
(h, j) => blocks.push({ text: h, path: `work[${i}].highlights[${j}]` })
|
|
1285
|
-
);
|
|
1286
|
-
});
|
|
1287
|
-
const hits = [];
|
|
1288
|
-
for (const b of blocks) {
|
|
1289
|
-
for (const w of b.text.toLowerCase().split(/\s+/)) {
|
|
1290
|
-
const clean = w.replace(/[^a-zA-ZäöüßÄÖÜ]/g, "");
|
|
1291
|
-
if (set.has(clean)) hits.push({ pronoun: clean, path: b.path });
|
|
72
|
+
function looksLikeAcronym(phrase) {
|
|
73
|
+
const trimmed = phrase.trim();
|
|
74
|
+
if (/\s/.test(trimmed)) return false;
|
|
75
|
+
if (trimmed.length < 2 || trimmed.length > 6) return false;
|
|
76
|
+
const letters = trimmed.replace(/[^a-zA-Z]/g, "");
|
|
77
|
+
return letters.length >= 2 && letters === letters.toUpperCase();
|
|
78
|
+
}
|
|
79
|
+
var SkillIndex = class {
|
|
80
|
+
skills;
|
|
81
|
+
byFirstToken = /* @__PURE__ */ new Map();
|
|
82
|
+
maxPhraseLen;
|
|
83
|
+
constructor(skills) {
|
|
84
|
+
this.skills = skills;
|
|
85
|
+
let maxLen = 1;
|
|
86
|
+
skills.forEach((skill, i) => {
|
|
87
|
+
for (const phrase of [skill.canonical, ...skill.aliases]) {
|
|
88
|
+
const tokens = phraseToTokens(phrase);
|
|
89
|
+
const first = tokens[0];
|
|
90
|
+
if (!first) continue;
|
|
91
|
+
maxLen = Math.max(maxLen, tokens.length);
|
|
92
|
+
const list = this.byFirstToken.get(first);
|
|
93
|
+
const entry = {
|
|
94
|
+
skillIdx: i,
|
|
95
|
+
phraseTokens: tokens,
|
|
96
|
+
requiresCaseMatch: looksLikeAcronym(phrase)
|
|
97
|
+
};
|
|
98
|
+
if (list) list.push(entry);
|
|
99
|
+
else this.byFirstToken.set(first, [entry]);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
for (const list of this.byFirstToken.values()) {
|
|
103
|
+
list.sort((a, b) => b.phraseTokens.length - a.phraseTokens.length);
|
|
1292
104
|
}
|
|
105
|
+
this.maxPhraseLen = maxLen;
|
|
1293
106
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Scan text for skills. Longest-match wins at each position, e.g. "Next.js"
|
|
109
|
+
* doesn't also fire "Next", and "Google Cloud Platform" doesn't also fire
|
|
110
|
+
* "Google".
|
|
111
|
+
*
|
|
112
|
+
* Returns a list of unique skills with occurrence counts.
|
|
113
|
+
*/
|
|
114
|
+
scan(text) {
|
|
115
|
+
const tokens = tokenize(text);
|
|
116
|
+
const hits = /* @__PURE__ */ new Map();
|
|
117
|
+
let i = 0;
|
|
118
|
+
while (i < tokens.length) {
|
|
119
|
+
const head = tokens[i];
|
|
120
|
+
if (!head) break;
|
|
121
|
+
const bucket = this.byFirstToken.get(head.norm);
|
|
122
|
+
if (!bucket) {
|
|
123
|
+
i++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
let matched = false;
|
|
127
|
+
for (const cand of bucket) {
|
|
128
|
+
const len = cand.phraseTokens.length;
|
|
129
|
+
if (i + len > tokens.length) continue;
|
|
130
|
+
let ok = true;
|
|
131
|
+
for (let k = 0; k < len; k++) {
|
|
132
|
+
const tok = tokens[i + k];
|
|
133
|
+
if (!tok || tok.norm !== cand.phraseTokens[k]) {
|
|
134
|
+
ok = false;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!ok) continue;
|
|
139
|
+
if (cand.requiresCaseMatch && !head.isAllUpper) continue;
|
|
140
|
+
hits.set(cand.skillIdx, (hits.get(cand.skillIdx) ?? 0) + 1);
|
|
141
|
+
i += len;
|
|
142
|
+
matched = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (!matched) i++;
|
|
146
|
+
}
|
|
147
|
+
const result = [];
|
|
148
|
+
for (const [skillIdx, count] of hits) {
|
|
149
|
+
const skill = this.skills[skillIdx];
|
|
150
|
+
if (!skill) continue;
|
|
151
|
+
result.push({ skill, occurrences: count });
|
|
152
|
+
}
|
|
153
|
+
result.sort((a, b) => {
|
|
154
|
+
if (a.skill.hot !== b.skill.hot) return a.skill.hot ? -1 : 1;
|
|
155
|
+
if (a.occurrences !== b.occurrences) return b.occurrences - a.occurrences;
|
|
156
|
+
return a.skill.canonical.localeCompare(b.skill.canonical);
|
|
157
|
+
});
|
|
158
|
+
return result;
|
|
1346
159
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
parts.push(...p.highlights || []);
|
|
160
|
+
get size() {
|
|
161
|
+
return this.skills.length;
|
|
1350
162
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const status = passed ? "pass" : total < min * 0.7 || total > max * 1.5 ? "fail" : "warn";
|
|
1354
|
-
return {
|
|
1355
|
-
id: "word-count-total",
|
|
1356
|
-
tier: "recruiter",
|
|
1357
|
-
weight: "low",
|
|
1358
|
-
status,
|
|
1359
|
-
score: passed ? 100 : Math.max(0, 100 - Math.round(Math.abs(total - (min + max) / 2) / 10)),
|
|
1360
|
-
message: `Resume body: ${total} words (target ${min}-${max}${senior ? ", senior" : ""}).`,
|
|
1361
|
-
hints: passed ? [] : [total < min ? "Add more depth to highlights." : "Trim less impactful highlights."]
|
|
1362
|
-
};
|
|
1363
|
-
};
|
|
1364
|
-
var highlightLength = (resume) => {
|
|
1365
|
-
const all = [];
|
|
1366
|
-
(resume.work || []).forEach((w, i) => {
|
|
1367
|
-
(w.highlights || []).forEach(
|
|
1368
|
-
(h, j) => all.push({ text: h, path: `work[${i}].highlights[${j}]` })
|
|
1369
|
-
);
|
|
1370
|
-
});
|
|
1371
|
-
if (all.length === 0) {
|
|
1372
|
-
return {
|
|
1373
|
-
id: "highlight-length",
|
|
1374
|
-
tier: "recruiter",
|
|
1375
|
-
weight: "low",
|
|
1376
|
-
status: "skipped",
|
|
1377
|
-
score: 0,
|
|
1378
|
-
message: "No highlights.",
|
|
1379
|
-
hints: []
|
|
1380
|
-
};
|
|
163
|
+
get maxPhraseTokens() {
|
|
164
|
+
return this.maxPhraseLen;
|
|
1381
165
|
}
|
|
1382
|
-
const long = all.filter((h) => wordCount(h.text) > 30);
|
|
1383
|
-
const passed = long.length === 0;
|
|
1384
|
-
return {
|
|
1385
|
-
id: "highlight-length",
|
|
1386
|
-
tier: "recruiter",
|
|
1387
|
-
weight: "low",
|
|
1388
|
-
status: passed ? "pass" : long.length / all.length > 0.3 ? "fail" : "warn",
|
|
1389
|
-
score: Math.round((all.length - long.length) / all.length * 100),
|
|
1390
|
-
message: passed ? "All highlights at most 30 words." : `${long.length} highlight(s) over 30 words.`,
|
|
1391
|
-
hints: passed ? [] : long.slice(0, 3).map((h) => `${h.path}: trim to 30 words or fewer.`)
|
|
1392
|
-
};
|
|
1393
166
|
};
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1396
|
-
try {
|
|
1397
|
-
const host = new URL(url).hostname.toLowerCase();
|
|
1398
|
-
return host === "linkedin.com" || host.endsWith(".linkedin.com");
|
|
1399
|
-
} catch {
|
|
1400
|
-
return false;
|
|
1401
|
-
}
|
|
167
|
+
function buildSkillIndex(skills) {
|
|
168
|
+
return new SkillIndex(skills);
|
|
1402
169
|
}
|
|
1403
|
-
var hasLinkedin = (resume) => {
|
|
1404
|
-
const profiles = resume.basics?.profiles || [];
|
|
1405
|
-
const found = profiles.some(
|
|
1406
|
-
(p) => p.network?.toLowerCase() === "linkedin" || isLinkedInUrl(p.url)
|
|
1407
|
-
);
|
|
1408
|
-
return {
|
|
1409
|
-
id: "has-linkedin",
|
|
1410
|
-
tier: "recruiter",
|
|
1411
|
-
weight: "low",
|
|
1412
|
-
status: found ? "pass" : "warn",
|
|
1413
|
-
score: found ? 100 : 0,
|
|
1414
|
-
message: found ? "LinkedIn profile present." : "No LinkedIn profile.",
|
|
1415
|
-
hints: found ? [] : ["Add a LinkedIn profile to basics.profiles."]
|
|
1416
|
-
};
|
|
1417
|
-
};
|
|
1418
|
-
var skillsPopulated = (resume) => {
|
|
1419
|
-
const skills = resume.skills || [];
|
|
1420
|
-
const withKeywords = skills.filter((s) => (s.keywords?.length ?? 0) > 0);
|
|
1421
|
-
const passed = withKeywords.length >= 3;
|
|
1422
|
-
return {
|
|
1423
|
-
id: "skills-populated",
|
|
1424
|
-
tier: "recruiter",
|
|
1425
|
-
weight: "medium",
|
|
1426
|
-
status: passed ? "pass" : withKeywords.length < 2 ? "fail" : "warn",
|
|
1427
|
-
score: Math.min(100, Math.round(withKeywords.length / 3 * 100)),
|
|
1428
|
-
message: passed ? `${withKeywords.length} skill categories with keywords.` : `Only ${withKeywords.length} skill categories with keywords (need 3+).`,
|
|
1429
|
-
hints: passed ? [] : ["Add at least 3 skill categories with keywords."]
|
|
1430
|
-
};
|
|
1431
|
-
};
|
|
1432
|
-
var allRecruiterChecks = [
|
|
1433
|
-
summaryLength,
|
|
1434
|
-
actionVerbStart,
|
|
1435
|
-
quantificationDensity,
|
|
1436
|
-
pronounLeakage,
|
|
1437
|
-
bulletsPerRole,
|
|
1438
|
-
wordCountTotal,
|
|
1439
|
-
highlightLength,
|
|
1440
|
-
hasLinkedin,
|
|
1441
|
-
skillsPopulated
|
|
1442
|
-
];
|
|
1443
170
|
|
|
1444
171
|
// data/skills/skills.json
|
|
1445
172
|
var skills_default = {
|
|
@@ -16407,1028 +15134,21 @@ var skills_default = {
|
|
|
16407
15134
|
]
|
|
16408
15135
|
};
|
|
16409
15136
|
|
|
16410
|
-
// src/ats/skills/matcher.ts
|
|
16411
|
-
function isTokenChar(ch) {
|
|
16412
|
-
if (ch >= "a" && ch <= "z") return true;
|
|
16413
|
-
if (ch >= "A" && ch <= "Z") return true;
|
|
16414
|
-
if (ch >= "0" && ch <= "9") return true;
|
|
16415
|
-
return ch === "." || ch === "/" || ch === "-" || ch === "+" || ch === "#" || ch === "_";
|
|
16416
|
-
}
|
|
16417
|
-
function isLetter(ch) {
|
|
16418
|
-
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z";
|
|
16419
|
-
}
|
|
16420
|
-
function isDigit(ch) {
|
|
16421
|
-
return ch >= "0" && ch <= "9";
|
|
16422
|
-
}
|
|
16423
|
-
function trimTokenBoundary(tok) {
|
|
16424
|
-
let start = 0;
|
|
16425
|
-
let end = tok.length;
|
|
16426
|
-
while (start < end) {
|
|
16427
|
-
const ch = tok.charAt(start);
|
|
16428
|
-
if (isLetter(ch) || isDigit(ch) || ch === "+" || ch === "#") break;
|
|
16429
|
-
start++;
|
|
16430
|
-
}
|
|
16431
|
-
while (end > start) {
|
|
16432
|
-
const ch = tok.charAt(end - 1);
|
|
16433
|
-
if (isLetter(ch) || isDigit(ch) || ch === "+" || ch === "#") break;
|
|
16434
|
-
end--;
|
|
16435
|
-
}
|
|
16436
|
-
return start === 0 && end === tok.length ? tok : tok.slice(start, end);
|
|
16437
|
-
}
|
|
16438
|
-
function tokenize(text) {
|
|
16439
|
-
const tokens = [];
|
|
16440
|
-
const len = text.length;
|
|
16441
|
-
let i = 0;
|
|
16442
|
-
while (i < len) {
|
|
16443
|
-
while (i < len && !isTokenChar(text.charAt(i))) i++;
|
|
16444
|
-
const start = i;
|
|
16445
|
-
while (i < len && isTokenChar(text.charAt(i))) i++;
|
|
16446
|
-
if (i === start) continue;
|
|
16447
|
-
const raw = trimTokenBoundary(text.slice(start, i));
|
|
16448
|
-
if (!raw) continue;
|
|
16449
|
-
let hasCore = false;
|
|
16450
|
-
for (let k = 0; k < raw.length; k++) {
|
|
16451
|
-
const ch = raw.charAt(k);
|
|
16452
|
-
if (isLetter(ch) || isDigit(ch) || ch === "+" || ch === "#") {
|
|
16453
|
-
hasCore = true;
|
|
16454
|
-
break;
|
|
16455
|
-
}
|
|
16456
|
-
}
|
|
16457
|
-
if (!hasCore) continue;
|
|
16458
|
-
let letterCount = 0;
|
|
16459
|
-
let upperCount = 0;
|
|
16460
|
-
for (let k = 0; k < raw.length; k++) {
|
|
16461
|
-
const ch = raw.charAt(k);
|
|
16462
|
-
if (isLetter(ch)) {
|
|
16463
|
-
letterCount++;
|
|
16464
|
-
if (ch >= "A" && ch <= "Z") upperCount++;
|
|
16465
|
-
}
|
|
16466
|
-
}
|
|
16467
|
-
const isAllUpper = letterCount >= 2 && upperCount === letterCount;
|
|
16468
|
-
tokens.push({ raw, norm: raw.toLowerCase(), isAllUpper });
|
|
16469
|
-
}
|
|
16470
|
-
return tokens;
|
|
16471
|
-
}
|
|
16472
|
-
function phraseToTokens(phrase) {
|
|
16473
|
-
return phrase.toLowerCase().split(/\s+/).map((t) => trimTokenBoundary(t)).filter(Boolean);
|
|
16474
|
-
}
|
|
16475
|
-
function looksLikeAcronym(phrase) {
|
|
16476
|
-
const trimmed = phrase.trim();
|
|
16477
|
-
if (/\s/.test(trimmed)) return false;
|
|
16478
|
-
if (trimmed.length < 2 || trimmed.length > 6) return false;
|
|
16479
|
-
const letters = trimmed.replace(/[^a-zA-Z]/g, "");
|
|
16480
|
-
return letters.length >= 2 && letters === letters.toUpperCase();
|
|
16481
|
-
}
|
|
16482
|
-
var SkillIndex = class {
|
|
16483
|
-
skills;
|
|
16484
|
-
byFirstToken = /* @__PURE__ */ new Map();
|
|
16485
|
-
maxPhraseLen;
|
|
16486
|
-
constructor(skills) {
|
|
16487
|
-
this.skills = skills;
|
|
16488
|
-
let maxLen = 1;
|
|
16489
|
-
skills.forEach((skill, i) => {
|
|
16490
|
-
for (const phrase of [skill.canonical, ...skill.aliases]) {
|
|
16491
|
-
const tokens = phraseToTokens(phrase);
|
|
16492
|
-
const first = tokens[0];
|
|
16493
|
-
if (!first) continue;
|
|
16494
|
-
maxLen = Math.max(maxLen, tokens.length);
|
|
16495
|
-
const list = this.byFirstToken.get(first);
|
|
16496
|
-
const entry = {
|
|
16497
|
-
skillIdx: i,
|
|
16498
|
-
phraseTokens: tokens,
|
|
16499
|
-
requiresCaseMatch: looksLikeAcronym(phrase)
|
|
16500
|
-
};
|
|
16501
|
-
if (list) list.push(entry);
|
|
16502
|
-
else this.byFirstToken.set(first, [entry]);
|
|
16503
|
-
}
|
|
16504
|
-
});
|
|
16505
|
-
for (const list of this.byFirstToken.values()) {
|
|
16506
|
-
list.sort((a, b) => b.phraseTokens.length - a.phraseTokens.length);
|
|
16507
|
-
}
|
|
16508
|
-
this.maxPhraseLen = maxLen;
|
|
16509
|
-
}
|
|
16510
|
-
/**
|
|
16511
|
-
* Scan text for skills. Longest-match wins at each position, e.g. "Next.js"
|
|
16512
|
-
* doesn't also fire "Next", and "Google Cloud Platform" doesn't also fire
|
|
16513
|
-
* "Google".
|
|
16514
|
-
*
|
|
16515
|
-
* Returns a list of unique skills with occurrence counts.
|
|
16516
|
-
*/
|
|
16517
|
-
scan(text) {
|
|
16518
|
-
const tokens = tokenize(text);
|
|
16519
|
-
const hits = /* @__PURE__ */ new Map();
|
|
16520
|
-
let i = 0;
|
|
16521
|
-
while (i < tokens.length) {
|
|
16522
|
-
const head = tokens[i];
|
|
16523
|
-
if (!head) break;
|
|
16524
|
-
const bucket = this.byFirstToken.get(head.norm);
|
|
16525
|
-
if (!bucket) {
|
|
16526
|
-
i++;
|
|
16527
|
-
continue;
|
|
16528
|
-
}
|
|
16529
|
-
let matched = false;
|
|
16530
|
-
for (const cand of bucket) {
|
|
16531
|
-
const len = cand.phraseTokens.length;
|
|
16532
|
-
if (i + len > tokens.length) continue;
|
|
16533
|
-
let ok = true;
|
|
16534
|
-
for (let k = 0; k < len; k++) {
|
|
16535
|
-
const tok = tokens[i + k];
|
|
16536
|
-
if (!tok || tok.norm !== cand.phraseTokens[k]) {
|
|
16537
|
-
ok = false;
|
|
16538
|
-
break;
|
|
16539
|
-
}
|
|
16540
|
-
}
|
|
16541
|
-
if (!ok) continue;
|
|
16542
|
-
if (cand.requiresCaseMatch && !head.isAllUpper) continue;
|
|
16543
|
-
hits.set(cand.skillIdx, (hits.get(cand.skillIdx) ?? 0) + 1);
|
|
16544
|
-
i += len;
|
|
16545
|
-
matched = true;
|
|
16546
|
-
break;
|
|
16547
|
-
}
|
|
16548
|
-
if (!matched) i++;
|
|
16549
|
-
}
|
|
16550
|
-
const result = [];
|
|
16551
|
-
for (const [skillIdx, count] of hits) {
|
|
16552
|
-
const skill = this.skills[skillIdx];
|
|
16553
|
-
if (!skill) continue;
|
|
16554
|
-
result.push({ skill, occurrences: count });
|
|
16555
|
-
}
|
|
16556
|
-
result.sort((a, b) => {
|
|
16557
|
-
if (a.skill.hot !== b.skill.hot) return a.skill.hot ? -1 : 1;
|
|
16558
|
-
if (a.occurrences !== b.occurrences) return b.occurrences - a.occurrences;
|
|
16559
|
-
return a.skill.canonical.localeCompare(b.skill.canonical);
|
|
16560
|
-
});
|
|
16561
|
-
return result;
|
|
16562
|
-
}
|
|
16563
|
-
get size() {
|
|
16564
|
-
return this.skills.length;
|
|
16565
|
-
}
|
|
16566
|
-
get maxPhraseTokens() {
|
|
16567
|
-
return this.maxPhraseLen;
|
|
16568
|
-
}
|
|
16569
|
-
};
|
|
16570
|
-
function buildSkillIndex(skills) {
|
|
16571
|
-
return new SkillIndex(skills);
|
|
16572
|
-
}
|
|
16573
|
-
|
|
16574
15137
|
// src/ats/skills/index.ts
|
|
16575
15138
|
var cached = null;
|
|
16576
15139
|
function getSkillIndex() {
|
|
16577
15140
|
if (!cached) cached = buildSkillIndex(skills_default.skills);
|
|
16578
15141
|
return cached;
|
|
16579
15142
|
}
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
function extractResumeText(resume) {
|
|
16583
|
-
const parts = [];
|
|
16584
|
-
if (resume.basics?.summary) parts.push(resume.basics.summary);
|
|
16585
|
-
if (resume.basics?.label) parts.push(resume.basics.label);
|
|
16586
|
-
for (const w of resume.work || []) {
|
|
16587
|
-
if (w.position) parts.push(w.position);
|
|
16588
|
-
if (w.summary) parts.push(w.summary);
|
|
16589
|
-
parts.push(...w.highlights || []);
|
|
16590
|
-
}
|
|
16591
|
-
for (const s of resume.skills || []) {
|
|
16592
|
-
if (s.name) parts.push(s.name);
|
|
16593
|
-
parts.push(...s.keywords || []);
|
|
16594
|
-
}
|
|
16595
|
-
for (const p of resume.projects || []) {
|
|
16596
|
-
if (p.name) parts.push(p.name);
|
|
16597
|
-
if (p.description) parts.push(p.description);
|
|
16598
|
-
parts.push(...p.highlights || []);
|
|
16599
|
-
}
|
|
16600
|
-
for (const e of resume.education || []) {
|
|
16601
|
-
if (e.area) parts.push(e.area);
|
|
16602
|
-
if (e.studyType) parts.push(e.studyType);
|
|
16603
|
-
parts.push(...e.courses || []);
|
|
16604
|
-
}
|
|
16605
|
-
for (const c of resume.certificates || []) {
|
|
16606
|
-
if (c.name) parts.push(c.name);
|
|
16607
|
-
}
|
|
16608
|
-
return parts.join(" ");
|
|
16609
|
-
}
|
|
16610
|
-
function splitJdSections(text) {
|
|
16611
|
-
const lines = text.split("\n");
|
|
16612
|
-
const reqPatterns = /^(required|requirements?|minimum|preferred|qualifications?|must[\s-]have|nice[\s-]to[\s-]have|what you.?ll|what we.?re looking|skills|technical|you.?ll need|responsibilities|you will)/i;
|
|
16613
|
-
const nonReqPatterns = /^(about|summary|who we are|our (company|team|mission)|description|overview|benefits|perks|compensation|salary)/i;
|
|
16614
|
-
let inReqSection = false;
|
|
16615
|
-
const reqLines = [];
|
|
16616
|
-
for (const line of lines) {
|
|
16617
|
-
const header = line.trim().replace(/[:#*-]/g, "").trim();
|
|
16618
|
-
if (reqPatterns.test(header)) inReqSection = true;
|
|
16619
|
-
else if (nonReqPatterns.test(header)) inReqSection = false;
|
|
16620
|
-
if (inReqSection) reqLines.push(line);
|
|
16621
|
-
}
|
|
16622
|
-
return {
|
|
16623
|
-
requirementText: reqLines.join("\n"),
|
|
16624
|
-
fullText: text
|
|
16625
|
-
};
|
|
16626
|
-
}
|
|
16627
|
-
function rankSkills(fullMatches, requirementMatches) {
|
|
16628
|
-
const reqIds = new Set(requirementMatches.map((m) => m.skill.id));
|
|
16629
|
-
return fullMatches.map((m) => {
|
|
16630
|
-
const inRequirementSection = reqIds.has(m.skill.id);
|
|
16631
|
-
const score = m.occurrences * 1 + (inRequirementSection ? 3 : 0) + (m.skill.hot ? 1.5 : 0);
|
|
16632
|
-
return {
|
|
16633
|
-
canonical: m.skill.canonical,
|
|
16634
|
-
occurrences: m.occurrences,
|
|
16635
|
-
inRequirementSection,
|
|
16636
|
-
hot: m.skill.hot,
|
|
16637
|
-
score
|
|
16638
|
-
};
|
|
16639
|
-
}).sort((a, b) => {
|
|
16640
|
-
if (a.inRequirementSection !== b.inRequirementSection) return a.inRequirementSection ? -1 : 1;
|
|
16641
|
-
if (a.score !== b.score) return b.score - a.score;
|
|
16642
|
-
return a.canonical.localeCompare(b.canonical);
|
|
16643
|
-
});
|
|
16644
|
-
}
|
|
16645
|
-
function matchJobDescription(resume, jobDescription, _language = "en") {
|
|
16646
|
-
if (!jobDescription.trim()) {
|
|
16647
|
-
return { matched: [], missing: [], extra: [], matchPercentage: 0 };
|
|
16648
|
-
}
|
|
16649
|
-
const skillIndex = getSkillIndex();
|
|
16650
|
-
const fullMatches = skillIndex.scan(jobDescription);
|
|
16651
|
-
const { requirementText } = splitJdSections(jobDescription);
|
|
16652
|
-
const requirementMatches = requirementText.trim() ? skillIndex.scan(requirementText) : fullMatches;
|
|
16653
|
-
const ranked = rankSkills(fullMatches, requirementMatches).slice(0, 25);
|
|
16654
|
-
const resumeText = extractResumeText(resume);
|
|
16655
|
-
const resumeMatches = skillIndex.scan(resumeText);
|
|
16656
|
-
const resumeSkillIds = new Set(resumeMatches.map((m) => m.skill.id));
|
|
16657
|
-
const matched = [];
|
|
16658
|
-
const missing = [];
|
|
16659
|
-
for (const r of ranked) {
|
|
16660
|
-
const match = fullMatches.find((m) => m.skill.canonical === r.canonical);
|
|
16661
|
-
if (!match) continue;
|
|
16662
|
-
if (resumeSkillIds.has(match.skill.id)) matched.push(r.canonical);
|
|
16663
|
-
else missing.push(r.canonical);
|
|
16664
|
-
}
|
|
16665
|
-
const matchPercentage = ranked.length > 0 ? Math.round(matched.length / ranked.length * 100) : 0;
|
|
16666
|
-
const jdSkillIds = new Set(fullMatches.map((m) => m.skill.id));
|
|
16667
|
-
const extra = [];
|
|
16668
|
-
for (const m of resumeMatches) {
|
|
16669
|
-
if (!jdSkillIds.has(m.skill.id)) extra.push(m.skill.canonical);
|
|
16670
|
-
}
|
|
16671
|
-
extra.splice(25);
|
|
16672
|
-
return { matched, missing, extra, matchPercentage };
|
|
16673
|
-
}
|
|
16674
|
-
|
|
16675
|
-
// src/ats/checks/match.ts
|
|
16676
|
-
var SENIORITY = /\b(junior|senior|lead|staff|principal|head of|vp|chief)\b/gi;
|
|
16677
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
16678
|
-
"a",
|
|
16679
|
-
"an",
|
|
16680
|
-
"the",
|
|
16681
|
-
"of",
|
|
16682
|
-
"at",
|
|
16683
|
-
"for",
|
|
16684
|
-
"in",
|
|
16685
|
-
"on",
|
|
16686
|
-
"to",
|
|
16687
|
-
"with",
|
|
16688
|
-
"and",
|
|
16689
|
-
"or"
|
|
16690
|
-
]);
|
|
16691
|
-
function tokenize2(s) {
|
|
16692
|
-
return s.toLowerCase().replace(SENIORITY, "").replace(/[^a-z0-9 ]/g, " ").split(/\s+/).filter((w) => w && !STOPWORDS.has(w));
|
|
16693
|
-
}
|
|
16694
|
-
function jaccard(a, b) {
|
|
16695
|
-
const A = new Set(a), B = new Set(b);
|
|
16696
|
-
let inter = 0;
|
|
16697
|
-
for (const x of A) if (B.has(x)) inter++;
|
|
16698
|
-
const union = A.size + B.size - inter;
|
|
16699
|
-
return union === 0 ? 0 : inter / union;
|
|
16700
|
-
}
|
|
16701
|
-
function trimTitle(t) {
|
|
16702
|
-
return t.trim().replace(/\s*[,\-–—]\s.*$/, "").trim();
|
|
16703
|
-
}
|
|
16704
|
-
function extractJdTitle(jd) {
|
|
16705
|
-
const lines = jd.split("\n").slice(0, 8);
|
|
16706
|
-
for (const l of lines) {
|
|
16707
|
-
const m = l.match(/(?:role|position|title)[\s:-]+(.+)/i) || l.match(/looking for (?:an? )?(.+?)(?:\s+with|\s+to|$)/i);
|
|
16708
|
-
if (m?.[1]) return trimTitle(m[1]);
|
|
16709
|
-
}
|
|
16710
|
-
const fallback = lines.find(
|
|
16711
|
-
(l) => /\b(engineer|developer|manager|designer|analyst|scientist|architect|lead)\b/i.test(l)
|
|
16712
|
-
);
|
|
16713
|
-
return fallback ? trimTitle(fallback) : void 0;
|
|
16714
|
-
}
|
|
16715
|
-
var titleAlignment = (resume, _l, { jobDescription }) => {
|
|
16716
|
-
if (!jobDescription) {
|
|
16717
|
-
return {
|
|
16718
|
-
id: "title-alignment",
|
|
16719
|
-
tier: "match",
|
|
16720
|
-
weight: "high",
|
|
16721
|
-
status: "skipped",
|
|
16722
|
-
score: 0,
|
|
16723
|
-
message: "No JD.",
|
|
16724
|
-
hints: []
|
|
16725
|
-
};
|
|
16726
|
-
}
|
|
16727
|
-
const resumeTitle = resume.work?.[0]?.position || resume.basics?.label;
|
|
16728
|
-
const jdTitle = extractJdTitle(jobDescription);
|
|
16729
|
-
if (!resumeTitle || !jdTitle) {
|
|
16730
|
-
return {
|
|
16731
|
-
id: "title-alignment",
|
|
16732
|
-
tier: "match",
|
|
16733
|
-
weight: "high",
|
|
16734
|
-
status: "warn",
|
|
16735
|
-
score: 50,
|
|
16736
|
-
message: "Could not extract title from JD or resume.",
|
|
16737
|
-
hints: ["Set basics.label to your target title."]
|
|
16738
|
-
};
|
|
16739
|
-
}
|
|
16740
|
-
const j = jaccard(tokenize2(resumeTitle), tokenize2(jdTitle));
|
|
16741
|
-
const status = j >= 0.6 ? "pass" : j >= 0.3 ? "warn" : "fail";
|
|
16742
|
-
return {
|
|
16743
|
-
id: "title-alignment",
|
|
16744
|
-
tier: "match",
|
|
16745
|
-
weight: "high",
|
|
16746
|
-
status,
|
|
16747
|
-
score: Math.round(j * 100),
|
|
16748
|
-
message: `Title overlap ${Math.round(j * 100)}% (resume "${resumeTitle}" vs JD "${jdTitle}").`,
|
|
16749
|
-
hints: status === "pass" ? [] : [`Consider aligning basics.label closer to "${jdTitle}".`]
|
|
16750
|
-
};
|
|
16751
|
-
};
|
|
16752
|
-
var EDU_RE = {
|
|
16753
|
-
3: /\b(phd|ph\.?d|doctorate|doctoral)\b/i,
|
|
16754
|
-
2: /\b(master|m\.?s|m\.?a|mba|graduate degree)\b/i,
|
|
16755
|
-
1: /\b(bachelor|b\.?s|b\.?a|undergraduate)\b/i
|
|
16756
|
-
};
|
|
16757
|
-
function eduLevel(text) {
|
|
16758
|
-
if (!text) return 0;
|
|
16759
|
-
if (EDU_RE[3].test(text)) return 3;
|
|
16760
|
-
if (EDU_RE[2].test(text)) return 2;
|
|
16761
|
-
if (EDU_RE[1].test(text)) return 1;
|
|
16762
|
-
return 0;
|
|
16763
|
-
}
|
|
16764
|
-
var educationLevel = (resume, _l, { jobDescription }) => {
|
|
16765
|
-
if (!jobDescription) {
|
|
16766
|
-
return {
|
|
16767
|
-
id: "education-level",
|
|
16768
|
-
tier: "match",
|
|
16769
|
-
weight: "medium",
|
|
16770
|
-
status: "skipped",
|
|
16771
|
-
score: 0,
|
|
16772
|
-
message: "No JD.",
|
|
16773
|
-
hints: []
|
|
16774
|
-
};
|
|
16775
|
-
}
|
|
16776
|
-
const required = eduLevel(jobDescription);
|
|
16777
|
-
if (required === 0) {
|
|
16778
|
-
return {
|
|
16779
|
-
id: "education-level",
|
|
16780
|
-
tier: "match",
|
|
16781
|
-
weight: "medium",
|
|
16782
|
-
status: "skipped",
|
|
16783
|
-
score: 0,
|
|
16784
|
-
message: "JD does not specify education level.",
|
|
16785
|
-
hints: []
|
|
16786
|
-
};
|
|
16787
|
-
}
|
|
16788
|
-
const have = Math.max(0, ...(resume.education || []).map((e) => eduLevel(e.studyType || "")));
|
|
16789
|
-
const passed = have >= required;
|
|
16790
|
-
return {
|
|
16791
|
-
id: "education-level",
|
|
16792
|
-
tier: "match",
|
|
16793
|
-
weight: "medium",
|
|
16794
|
-
status: passed ? "pass" : "fail",
|
|
16795
|
-
score: passed ? 100 : Math.round(have / required * 100),
|
|
16796
|
-
message: `Resume level ${have}, JD required ${required}.`,
|
|
16797
|
-
hints: passed ? [] : ["JD requires a higher degree level than the resume reports."]
|
|
16798
|
-
};
|
|
16799
|
-
};
|
|
16800
|
-
var YOE_RE = /(\d+)\s*(?:\+|[-–—]\s*\d+|to\s*\d+)?\s*years?/i;
|
|
16801
|
-
var yoeMatch = (resume, _l, { jobDescription }) => {
|
|
16802
|
-
if (!jobDescription) {
|
|
16803
|
-
return {
|
|
16804
|
-
id: "yoe-match",
|
|
16805
|
-
tier: "match",
|
|
16806
|
-
weight: "high",
|
|
16807
|
-
status: "skipped",
|
|
16808
|
-
score: 0,
|
|
16809
|
-
message: "No JD.",
|
|
16810
|
-
hints: []
|
|
16811
|
-
};
|
|
16812
|
-
}
|
|
16813
|
-
const m = jobDescription.match(YOE_RE);
|
|
16814
|
-
if (!m) {
|
|
16815
|
-
return {
|
|
16816
|
-
id: "yoe-match",
|
|
16817
|
-
tier: "match",
|
|
16818
|
-
weight: "high",
|
|
16819
|
-
status: "skipped",
|
|
16820
|
-
score: 0,
|
|
16821
|
-
message: "JD does not specify years requirement.",
|
|
16822
|
-
hints: []
|
|
16823
|
-
};
|
|
16824
|
-
}
|
|
16825
|
-
const required = parseInt(m[1] ?? "0", 10);
|
|
16826
|
-
const have = Math.floor(computeYoeYears(resume.work || []));
|
|
16827
|
-
const status = have >= required ? "pass" : have >= required - 1 ? "warn" : "fail";
|
|
16828
|
-
return {
|
|
16829
|
-
id: "yoe-match",
|
|
16830
|
-
tier: "match",
|
|
16831
|
-
weight: "high",
|
|
16832
|
-
status,
|
|
16833
|
-
score: Math.min(100, Math.round(have / required * 100)),
|
|
16834
|
-
message: `${have} YOE detected vs ${required} required.`,
|
|
16835
|
-
hints: status === "pass" ? [] : ["Highlight relevant earlier roles or projects to fill the gap."]
|
|
16836
|
-
};
|
|
16837
|
-
};
|
|
16838
|
-
var hardSkillOverlap = (resume, language, { jobDescription }) => {
|
|
16839
|
-
if (!jobDescription) {
|
|
16840
|
-
return {
|
|
16841
|
-
id: "hard-skill-overlap",
|
|
16842
|
-
tier: "match",
|
|
16843
|
-
weight: "high",
|
|
16844
|
-
status: "skipped",
|
|
16845
|
-
score: 0,
|
|
16846
|
-
message: "No JD.",
|
|
16847
|
-
hints: []
|
|
16848
|
-
};
|
|
16849
|
-
}
|
|
16850
|
-
const km = matchJobDescription(resume, jobDescription, language);
|
|
16851
|
-
const pct = km.matchPercentage;
|
|
16852
|
-
const status = pct >= 70 ? "pass" : pct >= 50 ? "warn" : "fail";
|
|
16853
|
-
return {
|
|
16854
|
-
id: "hard-skill-overlap",
|
|
16855
|
-
tier: "match",
|
|
16856
|
-
weight: "high",
|
|
16857
|
-
status,
|
|
16858
|
-
score: pct,
|
|
16859
|
-
message: `${km.matched.length}/${km.matched.length + km.missing.length} hard skills matched (${pct}%).`,
|
|
16860
|
-
hints: status === "pass" ? [] : km.missing.slice(0, 5).map((s) => `Add evidence of "${s}" to skills/highlights.`)
|
|
16861
|
-
};
|
|
16862
|
-
};
|
|
16863
|
-
var allMatchChecks = [hardSkillOverlap, titleAlignment, educationLevel, yoeMatch];
|
|
16864
|
-
var KNOCKOUTS = [
|
|
16865
|
-
{
|
|
16866
|
-
signal: "work-auth",
|
|
16867
|
-
jdPattern: /(work\s*auth|authorization to work|right to work|us citizen|green card|h-?1b|visa sponsorship)/i,
|
|
16868
|
-
resumeMatch: (r) => /(work auth|authorized|citizen|green card|visa)/i.test(r.basics?.summary || ""),
|
|
16869
|
-
recommendation: "Confirm authorization status in the application form."
|
|
16870
|
-
},
|
|
16871
|
-
{
|
|
16872
|
-
signal: "location",
|
|
16873
|
-
jdPattern: /(must be located|on-?site|relocate|based in)\s+([a-zA-Z ,]+)/i,
|
|
16874
|
-
resumeMatch: (r, m) => {
|
|
16875
|
-
const jdLoc = (m[2] ?? "").toLowerCase().trim();
|
|
16876
|
-
const resumeCity = (r.basics?.location?.city ?? "").toLowerCase().trim();
|
|
16877
|
-
if (!jdLoc || !resumeCity) return false;
|
|
16878
|
-
return jdLoc.includes(resumeCity) || resumeCity.includes(jdLoc.split(/[, ]+/)[0] ?? "");
|
|
16879
|
-
},
|
|
16880
|
-
recommendation: "Verify location requirement against your basics.location.city."
|
|
16881
|
-
},
|
|
16882
|
-
{
|
|
16883
|
-
signal: "clearance",
|
|
16884
|
-
jdPattern: /(ts\/sci|secret clearance|security clearance|active clearance)/i,
|
|
16885
|
-
resumeMatch: (r) => /clearance/i.test(JSON.stringify(r)),
|
|
16886
|
-
recommendation: "Confirm clearance level on the application form."
|
|
16887
|
-
},
|
|
16888
|
-
{
|
|
16889
|
-
signal: "certification",
|
|
16890
|
-
jdPattern: /(cissp|aws certified|pmp|ccna|cpa|required certification)/i,
|
|
16891
|
-
resumeMatch: (r) => (r.certificates?.length ?? 0) > 0,
|
|
16892
|
-
recommendation: "List required certificates in the certificates section if held."
|
|
16893
|
-
}
|
|
16894
|
-
];
|
|
16895
|
-
function extractKnockouts(resume, jobDescription) {
|
|
16896
|
-
const out = [];
|
|
16897
|
-
for (const k of KNOCKOUTS) {
|
|
16898
|
-
const m = jobDescription.match(k.jdPattern);
|
|
16899
|
-
if (!m) continue;
|
|
16900
|
-
if (k.resumeMatch(resume, m)) continue;
|
|
16901
|
-
out.push({
|
|
16902
|
-
signal: k.signal,
|
|
16903
|
-
evidence: `JD: "${m[0]}"; resume silent.`,
|
|
16904
|
-
recommendation: k.recommendation
|
|
16905
|
-
});
|
|
16906
|
-
}
|
|
16907
|
-
return out;
|
|
16908
|
-
}
|
|
16909
|
-
|
|
16910
|
-
// src/ats/scoring.ts
|
|
16911
|
-
var weightMultiplier = {
|
|
16912
|
-
high: 3,
|
|
16913
|
-
medium: 2,
|
|
16914
|
-
low: 1
|
|
16915
|
-
};
|
|
16916
|
-
function gradeFromScore(score, t) {
|
|
16917
|
-
if (score >= t.A) return "A";
|
|
16918
|
-
if (score >= t.B) return "B";
|
|
16919
|
-
if (score >= t.C) return "C";
|
|
16920
|
-
if (score >= t.D) return "D";
|
|
16921
|
-
return "F";
|
|
16922
|
-
}
|
|
16923
|
-
function computeTierScore(checks) {
|
|
16924
|
-
const active = checks.filter((c) => c.status !== "skipped");
|
|
16925
|
-
if (active.length === 0) return 0;
|
|
16926
|
-
let weighted = 0;
|
|
16927
|
-
let total = 0;
|
|
16928
|
-
for (const c of active) {
|
|
16929
|
-
const m = weightMultiplier[c.weight];
|
|
16930
|
-
weighted += c.score * m;
|
|
16931
|
-
total += 100 * m;
|
|
16932
|
-
}
|
|
16933
|
-
return total > 0 ? Math.round(weighted / total * 100) : 0;
|
|
16934
|
-
}
|
|
16935
|
-
function computeTotalScore(tiers, weights) {
|
|
16936
|
-
if (tiers.match === void 0) {
|
|
16937
|
-
const sum2 = weights.parsing + weights.recruiter;
|
|
16938
|
-
const wp = weights.parsing / sum2;
|
|
16939
|
-
const wr = weights.recruiter / sum2;
|
|
16940
|
-
return Math.round(
|
|
16941
|
-
tiers.parsing * (sum2 === 50 ? 0.4 : wp) + tiers.recruiter * (sum2 === 50 ? 0.6 : wr)
|
|
16942
|
-
);
|
|
16943
|
-
}
|
|
16944
|
-
const sum = weights.parsing + weights.match + weights.recruiter;
|
|
16945
|
-
return Math.round(
|
|
16946
|
-
(tiers.parsing * weights.parsing + tiers.match * weights.match + tiers.recruiter * weights.recruiter) / sum
|
|
16947
|
-
);
|
|
16948
|
-
}
|
|
16949
|
-
function scoreToRating(score, t) {
|
|
16950
|
-
if (score >= t.excellent) return "excellent";
|
|
16951
|
-
if (score >= t.good) return "good";
|
|
16952
|
-
if (score >= t.needsWork) return "needs-work";
|
|
16953
|
-
return "poor";
|
|
16954
|
-
}
|
|
16955
|
-
function generateSummary(score, rating, hasJd, knockouts) {
|
|
16956
|
-
const ratingLabel = {
|
|
16957
|
-
excellent: "Excellent",
|
|
16958
|
-
good: "Good",
|
|
16959
|
-
"needs-work": "Needs Work",
|
|
16960
|
-
poor: "Poor"
|
|
16961
|
-
}[rating];
|
|
16962
|
-
const knockoutNote = knockouts > 0 ? ` ${knockouts} knockout signal${knockouts === 1 ? "" : "s"} flagged.` : "";
|
|
16963
|
-
return `ATS ${score}/100 (${ratingLabel}).${hasJd ? " Includes JD match." : ""}${knockoutNote}`;
|
|
16964
|
-
}
|
|
16965
|
-
|
|
16966
|
-
// src/ats/index.ts
|
|
16967
|
-
function applyConfig(checks, cfg) {
|
|
16968
|
-
return checks.filter((c) => !cfg.disable.includes(c.id)).map((c) => ({ ...c, weight: effectiveWeight(c.id, c.weight, cfg) }));
|
|
16969
|
-
}
|
|
16970
|
-
function buildTier(_tier, checks, cfg) {
|
|
16971
|
-
const filtered = applyConfig(checks, cfg);
|
|
16972
|
-
const score = computeTierScore(filtered);
|
|
16973
|
-
return {
|
|
16974
|
-
score,
|
|
16975
|
-
grade: gradeFromScore(score, cfg.thresholds.grade),
|
|
16976
|
-
checks: filtered
|
|
16977
|
-
};
|
|
16978
|
-
}
|
|
16979
|
-
function analyzeAts(resume, options = {}) {
|
|
16980
|
-
const cfg = options.config ?? defaultConfig;
|
|
16981
|
-
const language = options.language ?? cfg.locale;
|
|
16982
|
-
const parsingChecks = allParsingChecks.map((fn) => fn(resume, language));
|
|
16983
|
-
const recruiterChecks = allRecruiterChecks.map((fn) => fn(resume, language, cfg));
|
|
16984
|
-
const parsing = buildTier("parsing", parsingChecks, cfg);
|
|
16985
|
-
const recruiter = buildTier("recruiter", recruiterChecks, cfg);
|
|
16986
|
-
let match;
|
|
16987
|
-
let knockouts = [];
|
|
16988
|
-
if (options.jobDescription) {
|
|
16989
|
-
const matchChecks = allMatchChecks.map(
|
|
16990
|
-
(fn) => fn(resume, language, { jobDescription: options.jobDescription })
|
|
16991
|
-
);
|
|
16992
|
-
match = buildTier("match", matchChecks, cfg);
|
|
16993
|
-
knockouts = extractKnockouts(resume, options.jobDescription);
|
|
16994
|
-
}
|
|
16995
|
-
const totalScore = computeTotalScore(
|
|
16996
|
-
{ parsing: parsing.score, match: match?.score, recruiter: recruiter.score },
|
|
16997
|
-
cfg.weights.tiers
|
|
16998
|
-
);
|
|
16999
|
-
const rating = scoreToRating(totalScore, cfg.thresholds.rating);
|
|
17000
|
-
const summary = generateSummary(totalScore, rating, !!options.jobDescription, knockouts.length);
|
|
17001
|
-
return {
|
|
17002
|
-
score: totalScore,
|
|
17003
|
-
rating,
|
|
17004
|
-
tiers: match ? { parsing, match, recruiter } : { parsing, recruiter },
|
|
17005
|
-
knockouts,
|
|
17006
|
-
summary
|
|
17007
|
-
};
|
|
17008
|
-
}
|
|
17009
|
-
|
|
17010
|
-
// src/utils/themeLoader.ts
|
|
17011
|
-
import { execFileSync } from "child_process";
|
|
17012
|
-
import { createRequire } from "module";
|
|
17013
|
-
var require2 = createRequire(import.meta.url);
|
|
17014
|
-
function installTheme(packageName) {
|
|
17015
|
-
try {
|
|
17016
|
-
execFileSync("npm", ["install", packageName], {
|
|
17017
|
-
stdio: ["inherit", "pipe", "pipe"],
|
|
17018
|
-
encoding: "utf8"
|
|
17019
|
-
});
|
|
17020
|
-
} catch (error) {
|
|
17021
|
-
throw new Error(`Failed to install ${packageName}: ${error.message}`);
|
|
17022
|
-
}
|
|
17023
|
-
}
|
|
17024
|
-
function loadTheme(themeName, options) {
|
|
17025
|
-
let jsonResumeThemeName;
|
|
17026
|
-
let nativeThemeName;
|
|
17027
|
-
const autoInstall = options?.autoInstall !== false;
|
|
17028
|
-
try {
|
|
17029
|
-
jsonResumeThemeName = themeName.startsWith("jsonresume-theme-") ? themeName : `jsonresume-theme-${themeName}`;
|
|
17030
|
-
try {
|
|
17031
|
-
return require2(jsonResumeThemeName);
|
|
17032
|
-
} catch (_jsonResumeError) {
|
|
17033
|
-
nativeThemeName = `@resuml/theme-${themeName}`;
|
|
17034
|
-
try {
|
|
17035
|
-
return require2(nativeThemeName);
|
|
17036
|
-
} catch (_nativeError) {
|
|
17037
|
-
if (!autoInstall) {
|
|
17038
|
-
throw new Error(
|
|
17039
|
-
`Theme package ${jsonResumeThemeName} or ${nativeThemeName} not found in node_modules.
|
|
17040
|
-
Please install the theme package manually.`
|
|
17041
|
-
);
|
|
17042
|
-
}
|
|
17043
|
-
console.log(`\u{1F4E6} Theme ${jsonResumeThemeName} not found. Installing...`);
|
|
17044
|
-
try {
|
|
17045
|
-
installTheme(jsonResumeThemeName);
|
|
17046
|
-
console.log(`\u2705 Successfully installed ${jsonResumeThemeName}`);
|
|
17047
|
-
return require2(jsonResumeThemeName);
|
|
17048
|
-
} catch (installError) {
|
|
17049
|
-
throw new Error(
|
|
17050
|
-
`Failed to auto-install theme ${jsonResumeThemeName}: ${installError.message}`
|
|
17051
|
-
);
|
|
17052
|
-
}
|
|
17053
|
-
}
|
|
17054
|
-
}
|
|
17055
|
-
} catch (error) {
|
|
17056
|
-
if (error instanceof Error && error.message.includes("Failed to auto-install")) {
|
|
17057
|
-
throw error;
|
|
17058
|
-
}
|
|
17059
|
-
throw new Error(`Theme package ${themeName} not found`);
|
|
17060
|
-
}
|
|
17061
|
-
}
|
|
17062
|
-
|
|
17063
|
-
// src/utils/resumeTemplate.ts
|
|
17064
|
-
function generateResumeYaml(name, email, label) {
|
|
17065
|
-
return `# =============================================================================
|
|
17066
|
-
# Resume - Generated by resuml
|
|
17067
|
-
# Documentation: https://github.com/phoinixi/resuml
|
|
17068
|
-
# Schema: https://jsonresume.org/schema/
|
|
17069
|
-
# =============================================================================
|
|
17070
|
-
|
|
17071
|
-
# --- Basic Information ---
|
|
17072
|
-
basics:
|
|
17073
|
-
name: '${name}'
|
|
17074
|
-
label: '${label}'
|
|
17075
|
-
# image: 'https://example.com/photo.jpg'
|
|
17076
|
-
email: '${email}'
|
|
17077
|
-
# phone: '+1-555-123-4567'
|
|
17078
|
-
# url: 'https://yourwebsite.com'
|
|
17079
|
-
summary: >-
|
|
17080
|
-
Write a short professional summary here.
|
|
17081
|
-
This supports multi-line strings in YAML.
|
|
17082
|
-
location:
|
|
17083
|
-
# address: '123 Main Street'
|
|
17084
|
-
# postalCode: '12345'
|
|
17085
|
-
city: 'Your City'
|
|
17086
|
-
countryCode: 'US'
|
|
17087
|
-
# region: 'Your State'
|
|
17088
|
-
profiles:
|
|
17089
|
-
- network: 'LinkedIn'
|
|
17090
|
-
username: 'your-username'
|
|
17091
|
-
url: 'https://linkedin.com/in/your-username'
|
|
17092
|
-
- network: 'GitHub'
|
|
17093
|
-
username: 'your-username'
|
|
17094
|
-
url: 'https://github.com/your-username'
|
|
17095
|
-
|
|
17096
|
-
# --- Work Experience ---
|
|
17097
|
-
work:
|
|
17098
|
-
- name: 'Company Name'
|
|
17099
|
-
position: 'Job Title'
|
|
17100
|
-
url: 'https://company.com'
|
|
17101
|
-
startDate: '2020-01-01'
|
|
17102
|
-
# endDate: '2023-12-31' # Omit for current position
|
|
17103
|
-
summary: 'Brief description of your role and responsibilities.'
|
|
17104
|
-
highlights:
|
|
17105
|
-
- 'Key achievement or responsibility'
|
|
17106
|
-
- 'Another notable accomplishment'
|
|
17107
|
-
|
|
17108
|
-
# --- Education ---
|
|
17109
|
-
education:
|
|
17110
|
-
- institution: 'University Name'
|
|
17111
|
-
url: 'https://university.edu'
|
|
17112
|
-
area: 'Field of Study'
|
|
17113
|
-
studyType: 'Bachelor of Science'
|
|
17114
|
-
startDate: '2014-09-01'
|
|
17115
|
-
endDate: '2018-06-01'
|
|
17116
|
-
# score: '3.8'
|
|
17117
|
-
courses:
|
|
17118
|
-
- 'Relevant Course 1'
|
|
17119
|
-
- 'Relevant Course 2'
|
|
17120
|
-
|
|
17121
|
-
# --- Skills ---
|
|
17122
|
-
skills:
|
|
17123
|
-
- name: 'Programming Languages'
|
|
17124
|
-
level: 'Expert'
|
|
17125
|
-
keywords:
|
|
17126
|
-
- 'JavaScript'
|
|
17127
|
-
- 'TypeScript'
|
|
17128
|
-
- 'Python'
|
|
17129
|
-
- name: 'Frameworks'
|
|
17130
|
-
level: 'Advanced'
|
|
17131
|
-
keywords:
|
|
17132
|
-
- 'React'
|
|
17133
|
-
- 'Node.js'
|
|
17134
|
-
|
|
17135
|
-
# --- Projects ---
|
|
17136
|
-
# projects:
|
|
17137
|
-
# - name: 'Project Name'
|
|
17138
|
-
# description: 'Brief project description'
|
|
17139
|
-
# highlights:
|
|
17140
|
-
# - 'Key feature or result'
|
|
17141
|
-
# keywords:
|
|
17142
|
-
# - 'Technology Used'
|
|
17143
|
-
# startDate: '2023-01-01'
|
|
17144
|
-
# endDate: '2023-06-30'
|
|
17145
|
-
# url: 'https://github.com/you/project'
|
|
17146
|
-
# roles:
|
|
17147
|
-
# - 'Developer'
|
|
17148
|
-
# type: 'application'
|
|
17149
|
-
|
|
17150
|
-
# --- Languages ---
|
|
17151
|
-
# languages:
|
|
17152
|
-
# - language: 'English'
|
|
17153
|
-
# fluency: 'Native speaker'
|
|
17154
|
-
# - language: 'Spanish'
|
|
17155
|
-
# fluency: 'Professional working proficiency'
|
|
17156
|
-
|
|
17157
|
-
# --- Interests ---
|
|
17158
|
-
# interests:
|
|
17159
|
-
# - name: 'Open Source'
|
|
17160
|
-
# keywords:
|
|
17161
|
-
# - 'Contributing'
|
|
17162
|
-
# - 'Community'
|
|
17163
|
-
|
|
17164
|
-
# --- References ---
|
|
17165
|
-
# references:
|
|
17166
|
-
# - name: 'Jane Smith'
|
|
17167
|
-
# reference: 'It was a pleasure working with...'
|
|
17168
|
-
|
|
17169
|
-
# --- Awards ---
|
|
17170
|
-
# awards:
|
|
17171
|
-
# - title: 'Award Name'
|
|
17172
|
-
# date: '2023-01-01'
|
|
17173
|
-
# awarder: 'Organization'
|
|
17174
|
-
# summary: 'Description of the award'
|
|
17175
|
-
|
|
17176
|
-
# --- Certificates ---
|
|
17177
|
-
# certificates:
|
|
17178
|
-
# - name: 'Certificate Name'
|
|
17179
|
-
# date: '2023-01-01'
|
|
17180
|
-
# issuer: 'Issuing Organization'
|
|
17181
|
-
# url: 'https://example.com/cert'
|
|
17182
|
-
|
|
17183
|
-
# --- Publications ---
|
|
17184
|
-
# publications:
|
|
17185
|
-
# - name: 'Publication Title'
|
|
17186
|
-
# publisher: 'Publisher'
|
|
17187
|
-
# releaseDate: '2023-01-01'
|
|
17188
|
-
# url: 'https://example.com/publication'
|
|
17189
|
-
# summary: 'Brief description'
|
|
17190
|
-
|
|
17191
|
-
# --- Volunteer ---
|
|
17192
|
-
# volunteers:
|
|
17193
|
-
# - organization: 'Organization Name'
|
|
17194
|
-
# position: 'Volunteer Role'
|
|
17195
|
-
# url: 'https://organization.com'
|
|
17196
|
-
# startDate: '2022-01-01'
|
|
17197
|
-
# summary: 'Description of volunteer work'
|
|
17198
|
-
# highlights:
|
|
17199
|
-
# - 'Notable contribution'
|
|
17200
|
-
`;
|
|
17201
|
-
}
|
|
17202
|
-
|
|
17203
|
-
// src/utils/themeInfo.ts
|
|
17204
|
-
import { createRequire as createRequire2 } from "module";
|
|
17205
|
-
var KNOWN_THEMES = [
|
|
17206
|
-
{
|
|
17207
|
-
name: "stackoverflow",
|
|
17208
|
-
pkg: "jsonresume-theme-stackoverflow",
|
|
17209
|
-
description: "Stack Overflow inspired theme"
|
|
17210
|
-
},
|
|
17211
|
-
{ name: "elegant", pkg: "jsonresume-theme-elegant", description: "Elegant and professional" },
|
|
17212
|
-
{ name: "react", pkg: "jsonresume-theme-react", description: "Built with React components" },
|
|
17213
|
-
{ name: "even", pkg: "jsonresume-theme-even", description: "Clean and minimal" },
|
|
17214
|
-
{ name: "kendall", pkg: "jsonresume-theme-kendall", description: "Simple and clean layout" },
|
|
17215
|
-
{ name: "macchiato", pkg: "jsonresume-theme-macchiato", description: "Beautiful and modern" },
|
|
17216
|
-
{ name: "flat", pkg: "jsonresume-theme-flat", description: "Flat design theme" },
|
|
17217
|
-
{ name: "class", pkg: "jsonresume-theme-class", description: "Classic professional look" },
|
|
17218
|
-
{ name: "short", pkg: "jsonresume-theme-short", description: "Compact single-page resume" },
|
|
17219
|
-
{ name: "spartan", pkg: "jsonresume-theme-spartan", description: "Minimalist Spartan design" },
|
|
17220
|
-
{ name: "paper", pkg: "jsonresume-theme-paper", description: "Paper-like clean design" },
|
|
17221
|
-
{ name: "onepage", pkg: "jsonresume-theme-onepage", description: "One page resume layout" }
|
|
17222
|
-
];
|
|
17223
|
-
function isThemeInstalled(pkg) {
|
|
17224
|
-
try {
|
|
17225
|
-
const req = createRequire2(process.cwd() + "/");
|
|
17226
|
-
req.resolve(pkg);
|
|
17227
|
-
return true;
|
|
17228
|
-
} catch {
|
|
17229
|
-
return false;
|
|
17230
|
-
}
|
|
17231
|
-
}
|
|
17232
|
-
function getInstalledVersion(pkg) {
|
|
17233
|
-
try {
|
|
17234
|
-
const req = createRequire2(process.cwd() + "/");
|
|
17235
|
-
const pkgJson = req(`${pkg}/package.json`);
|
|
17236
|
-
return pkgJson.version ?? null;
|
|
17237
|
-
} catch {
|
|
17238
|
-
return null;
|
|
17239
|
-
}
|
|
17240
|
-
}
|
|
17241
|
-
|
|
17242
|
-
// src/ats/rubric.ts
|
|
17243
|
-
var rubric = [
|
|
17244
|
-
// Parsing tier - Greenhouse-doc-grounded
|
|
17245
|
-
{
|
|
17246
|
-
id: "conventional-sections",
|
|
17247
|
-
tier: "parsing",
|
|
17248
|
-
weight: "high",
|
|
17249
|
-
evidenceLevel: "evidence",
|
|
17250
|
-
description: "Required JSON Resume sections (basics, work, education) are present.",
|
|
17251
|
-
source: "https://support.greenhouse.io/hc/en-us/articles/200989175-Unsuccessful-resume-parse"
|
|
17252
|
-
},
|
|
17253
|
-
{
|
|
17254
|
-
id: "date-format-consistency",
|
|
17255
|
-
tier: "parsing",
|
|
17256
|
-
weight: "medium",
|
|
17257
|
-
evidenceLevel: "evidence",
|
|
17258
|
-
description: "Dates use ISO-8601 (YYYY-MM-DD or YYYY-MM); no mixed formats; no >6mo gaps.",
|
|
17259
|
-
source: "https://hireflow.net/blog/taleo-resume-parsing-problems-explained"
|
|
17260
|
-
},
|
|
17261
|
-
{
|
|
17262
|
-
id: "contact-in-body",
|
|
17263
|
-
tier: "parsing",
|
|
17264
|
-
weight: "high",
|
|
17265
|
-
evidenceLevel: "evidence",
|
|
17266
|
-
description: "Email and phone present in basics block (parseable by Textkernel/Sovren).",
|
|
17267
|
-
source: "https://developer.textkernel.com/tx-platform/v10/faq/"
|
|
17268
|
-
},
|
|
17269
|
-
{
|
|
17270
|
-
id: "reverse-chron-order",
|
|
17271
|
-
tier: "parsing",
|
|
17272
|
-
weight: "medium",
|
|
17273
|
-
evidenceLevel: "evidence",
|
|
17274
|
-
description: "work[] and education[] sorted descending by startDate."
|
|
17275
|
-
},
|
|
17276
|
-
{
|
|
17277
|
-
id: "education-complete",
|
|
17278
|
-
tier: "parsing",
|
|
17279
|
-
weight: "low",
|
|
17280
|
-
evidenceLevel: "evidence",
|
|
17281
|
-
description: "Each education entry has institution, area, and studyType."
|
|
17282
|
-
},
|
|
17283
|
-
{
|
|
17284
|
-
id: "pdf-text-extractable",
|
|
17285
|
-
tier: "parsing",
|
|
17286
|
-
weight: "high",
|
|
17287
|
-
evidenceLevel: "evidence",
|
|
17288
|
-
description: "Rendered PDF body text is selectable (>=70% of resume word count extractable).",
|
|
17289
|
-
source: "https://www.ashbyhq.com/product-updates/ai-assisted-application-review"
|
|
17290
|
-
},
|
|
17291
|
-
{
|
|
17292
|
-
id: "pdf-size-under-2.5mb",
|
|
17293
|
-
tier: "parsing",
|
|
17294
|
-
weight: "medium",
|
|
17295
|
-
evidenceLevel: "evidence",
|
|
17296
|
-
description: "Rendered PDF is under 2.5 MB (Greenhouse documented limit).",
|
|
17297
|
-
source: "https://support.greenhouse.io/hc/en-us/articles/200989175-Unsuccessful-resume-parse"
|
|
17298
|
-
},
|
|
17299
|
-
// Match tier - JD-aware
|
|
17300
|
-
{
|
|
17301
|
-
id: "hard-skill-overlap",
|
|
17302
|
-
tier: "match",
|
|
17303
|
-
weight: "high",
|
|
17304
|
-
evidenceLevel: "evidence",
|
|
17305
|
-
description: "Hard skill overlap with JD using bundled O*NET-trie skill index. ESCO migration tracked separately."
|
|
17306
|
-
},
|
|
17307
|
-
{
|
|
17308
|
-
id: "title-alignment",
|
|
17309
|
-
tier: "match",
|
|
17310
|
-
weight: "high",
|
|
17311
|
-
evidenceLevel: "evidence",
|
|
17312
|
-
description: "Most-recent work[].position aligns with JD title via token Jaccard after stripping seniority modifiers."
|
|
17313
|
-
},
|
|
17314
|
-
{
|
|
17315
|
-
id: "education-level",
|
|
17316
|
-
tier: "match",
|
|
17317
|
-
weight: "medium",
|
|
17318
|
-
evidenceLevel: "evidence",
|
|
17319
|
-
description: "Max studyType meets/exceeds JD education requirement (Bachelor/Master/PhD detection)."
|
|
17320
|
-
},
|
|
17321
|
-
{
|
|
17322
|
-
id: "yoe-match",
|
|
17323
|
-
tier: "match",
|
|
17324
|
-
weight: "high",
|
|
17325
|
-
evidenceLevel: "evidence",
|
|
17326
|
-
description: "Total years of experience (overlap-merged from work[]) meets JD years requirement."
|
|
17327
|
-
},
|
|
17328
|
-
// Recruiter tier - Rezi/Teal/Enhancv-grounded numbers
|
|
17329
|
-
{
|
|
17330
|
-
id: "summary-length",
|
|
17331
|
-
tier: "recruiter",
|
|
17332
|
-
weight: "medium",
|
|
17333
|
-
evidenceLevel: "convention",
|
|
17334
|
-
description: "Professional summary 20-50 words / 2-4 sentences.",
|
|
17335
|
-
source: "https://www.rezi.ai/rezi-docs/the-rezi-score-explained"
|
|
17336
|
-
},
|
|
17337
|
-
{
|
|
17338
|
-
id: "action-verb-start",
|
|
17339
|
-
tier: "recruiter",
|
|
17340
|
-
weight: "medium",
|
|
17341
|
-
evidenceLevel: "convention",
|
|
17342
|
-
description: "Highlights start with an action verb."
|
|
17343
|
-
},
|
|
17344
|
-
{
|
|
17345
|
-
id: "quantification-density",
|
|
17346
|
-
tier: "recruiter",
|
|
17347
|
-
weight: "high",
|
|
17348
|
-
evidenceLevel: "convention",
|
|
17349
|
-
description: "At least 50% of highlights include numbers/metrics."
|
|
17350
|
-
},
|
|
17351
|
-
{
|
|
17352
|
-
id: "pronoun-leakage",
|
|
17353
|
-
tier: "recruiter",
|
|
17354
|
-
weight: "low",
|
|
17355
|
-
evidenceLevel: "convention",
|
|
17356
|
-
description: "Convention, not ATS: ATS does not filter on pronouns; this is recruiter style.",
|
|
17357
|
-
source: "https://resume.io/blog/first-person-resume"
|
|
17358
|
-
},
|
|
17359
|
-
{
|
|
17360
|
-
id: "bullets-per-role",
|
|
17361
|
-
tier: "recruiter",
|
|
17362
|
-
weight: "medium",
|
|
17363
|
-
evidenceLevel: "convention",
|
|
17364
|
-
description: "Each work entry has 3-6 highlights (10 for senior).",
|
|
17365
|
-
source: "https://www.rezi.ai/rezi-docs/the-rezi-score-explained"
|
|
17366
|
-
},
|
|
17367
|
-
{
|
|
17368
|
-
id: "word-count-total",
|
|
17369
|
-
tier: "recruiter",
|
|
17370
|
-
weight: "low",
|
|
17371
|
-
evidenceLevel: "convention",
|
|
17372
|
-
description: "Total resume body 400-800 words (1600 senior).",
|
|
17373
|
-
source: "https://www.rezi.ai/rezi-docs/the-rezi-score-explained"
|
|
17374
|
-
},
|
|
17375
|
-
{
|
|
17376
|
-
id: "highlight-length",
|
|
17377
|
-
tier: "recruiter",
|
|
17378
|
-
weight: "low",
|
|
17379
|
-
evidenceLevel: "convention",
|
|
17380
|
-
description: "Each highlight fits within two visual lines (~30 words)."
|
|
17381
|
-
},
|
|
17382
|
-
{
|
|
17383
|
-
id: "has-linkedin",
|
|
17384
|
-
tier: "recruiter",
|
|
17385
|
-
weight: "low",
|
|
17386
|
-
evidenceLevel: "convention",
|
|
17387
|
-
description: "LinkedIn profile present in basics.profiles."
|
|
17388
|
-
},
|
|
17389
|
-
{
|
|
17390
|
-
id: "skills-populated",
|
|
17391
|
-
tier: "recruiter",
|
|
17392
|
-
weight: "medium",
|
|
17393
|
-
evidenceLevel: "convention",
|
|
17394
|
-
description: "At least 3 skill categories with keywords."
|
|
17395
|
-
}
|
|
17396
|
-
];
|
|
17397
|
-
var byId = new Map(rubric.map((r) => [r.id, r]));
|
|
17398
|
-
function getRubricEntry(id, opts = {}) {
|
|
17399
|
-
const entry = byId.get(id);
|
|
17400
|
-
if (!entry && opts.strict) throw new Error(`Unknown rubric id: ${id}`);
|
|
17401
|
-
return entry;
|
|
17402
|
-
}
|
|
17403
|
-
function rubricByTier(tier) {
|
|
17404
|
-
return rubric.filter((r) => r.tier === tier);
|
|
17405
|
-
}
|
|
17406
|
-
function listRubricMarkdown() {
|
|
17407
|
-
const tiers = ["parsing", "match", "recruiter"];
|
|
17408
|
-
const sections = tiers.map((t) => {
|
|
17409
|
-
const entries = rubricByTier(t);
|
|
17410
|
-
const lines = entries.map(
|
|
17411
|
-
(e) => `- **${e.id}** (${e.weight}, ${e.evidenceLevel}): ${e.description}${e.source ? `
|
|
17412
|
-
Source: ${e.source}` : ""}`
|
|
17413
|
-
).join("\n");
|
|
17414
|
-
return `## Tier: ${t}
|
|
17415
|
-
|
|
17416
|
-
${lines}`;
|
|
17417
|
-
});
|
|
17418
|
-
return ["# resuml ATS Rubric", "", ...sections].join("\n\n");
|
|
15143
|
+
function _resetSkillIndexCache() {
|
|
15144
|
+
cached = null;
|
|
17419
15145
|
}
|
|
17420
15146
|
|
|
17421
15147
|
export {
|
|
17422
15148
|
__export,
|
|
17423
|
-
|
|
17424
|
-
|
|
17425
|
-
|
|
17426
|
-
|
|
17427
|
-
generateResumeYaml,
|
|
17428
|
-
KNOWN_THEMES,
|
|
17429
|
-
isThemeInstalled,
|
|
17430
|
-
getInstalledVersion,
|
|
17431
|
-
getRubricEntry,
|
|
17432
|
-
listRubricMarkdown
|
|
15149
|
+
tokenize,
|
|
15150
|
+
SkillIndex,
|
|
15151
|
+
getSkillIndex,
|
|
15152
|
+
_resetSkillIndexCache
|
|
17433
15153
|
};
|
|
17434
|
-
//# sourceMappingURL=chunk-
|
|
15154
|
+
//# sourceMappingURL=chunk-QR77BRMN.js.map
|