@pranavraut033/ats-checker 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -102
- package/dist/index.d.mts +22 -13
- package/dist/index.d.ts +22 -13
- package/dist/index.js +176 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +176 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -1
package/dist/index.mjs
CHANGED
|
@@ -35,13 +35,15 @@ function normalizeWhitespace(text) {
|
|
|
35
35
|
return text.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim();
|
|
36
36
|
}
|
|
37
37
|
function normalizeForComparison(text) {
|
|
38
|
-
return normalizeWhitespace(text).toLowerCase();
|
|
38
|
+
return normalizeWhitespace(text).normalize("NFKC").toLowerCase();
|
|
39
39
|
}
|
|
40
40
|
function splitLines(text) {
|
|
41
41
|
return text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
42
42
|
}
|
|
43
|
+
var TECH_TOKEN_RE = /[a-z0-9][a-z0-9.#+\-/]*[a-z0-9#+]/g;
|
|
43
44
|
function tokenize(text) {
|
|
44
|
-
|
|
45
|
+
const normalized = normalizeForComparison(text);
|
|
46
|
+
return (normalized.match(TECH_TOKEN_RE) ?? []).filter((t) => !STOP_WORDS.has(t));
|
|
45
47
|
}
|
|
46
48
|
function unique(values) {
|
|
47
49
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -215,7 +217,7 @@ function monthsBetween(start, end) {
|
|
|
215
217
|
const endMonth = end.month ?? 12;
|
|
216
218
|
return (end.year - start.year) * 12 + (endMonth - startMonth + 1);
|
|
217
219
|
}
|
|
218
|
-
function parseDateRange(text) {
|
|
220
|
+
function parseDateRange(text, referenceDate) {
|
|
219
221
|
const normalized = text.trim();
|
|
220
222
|
const rangeMatch = normalized.match(/([A-Za-z]{3,9}\s+\d{4}|\d{4})\s*(?:-|to|–|—)\s*(Present|Current|Now|[A-Za-z]{3,9}\s+\d{4}|\d{4})/i);
|
|
221
223
|
if (!rangeMatch) {
|
|
@@ -228,9 +230,10 @@ function parseDateRange(text) {
|
|
|
228
230
|
if (!startToken) {
|
|
229
231
|
return null;
|
|
230
232
|
}
|
|
233
|
+
const ref = referenceDate ?? /* @__PURE__ */ new Date();
|
|
231
234
|
const endTokenResolved = endToken ?? {
|
|
232
|
-
year:
|
|
233
|
-
month:
|
|
235
|
+
year: ref.getFullYear(),
|
|
236
|
+
month: ref.getMonth() + 1
|
|
234
237
|
};
|
|
235
238
|
const durationInMonths = monthsBetween(startToken, endTokenResolved);
|
|
236
239
|
return {
|
|
@@ -275,11 +278,15 @@ var ACTION_VERBS = [
|
|
|
275
278
|
"reduced",
|
|
276
279
|
"increased"
|
|
277
280
|
];
|
|
281
|
+
function escapeRegExp(input) {
|
|
282
|
+
return input.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
|
|
283
|
+
}
|
|
278
284
|
function detectSection(line) {
|
|
279
|
-
const normalized = line.toLowerCase();
|
|
285
|
+
const normalized = line.trim().toLowerCase();
|
|
280
286
|
for (const [section, aliases] of Object.entries(SECTION_ALIASES)) {
|
|
281
287
|
for (const alias of aliases) {
|
|
282
|
-
const
|
|
288
|
+
const safeAlias = escapeRegExp(alias);
|
|
289
|
+
const headerPattern = new RegExp(`^${safeAlias}(\\s*:)?$`, "i");
|
|
283
290
|
if (headerPattern.test(normalized)) {
|
|
284
291
|
return section;
|
|
285
292
|
}
|
|
@@ -321,7 +328,7 @@ function parseActionVerbs(text) {
|
|
|
321
328
|
const words = tokenize(text);
|
|
322
329
|
return ACTION_VERBS.filter((verb) => words.includes(verb));
|
|
323
330
|
}
|
|
324
|
-
function parseExperience(sectionContent) {
|
|
331
|
+
function parseExperience(sectionContent, referenceDate) {
|
|
325
332
|
if (!sectionContent) {
|
|
326
333
|
return { entries: [], rangesInMonths: [], jobTitles: [] };
|
|
327
334
|
}
|
|
@@ -330,7 +337,7 @@ function parseExperience(sectionContent) {
|
|
|
330
337
|
const rangesInMonths = [];
|
|
331
338
|
const jobTitles = [];
|
|
332
339
|
for (const line of lines) {
|
|
333
|
-
const range = parseDateRange(line);
|
|
340
|
+
const range = parseDateRange(line, referenceDate);
|
|
334
341
|
if (range) {
|
|
335
342
|
const previous = entries[entries.length - 1];
|
|
336
343
|
if (previous && !previous.dates) {
|
|
@@ -370,7 +377,7 @@ function parseResume(resumeText, config) {
|
|
|
370
377
|
const { sections, detected } = extractSections(resumeText);
|
|
371
378
|
const skills = parseSkills(sections.skills, config.skillAliases);
|
|
372
379
|
const actionVerbs = parseActionVerbs(normalizedText);
|
|
373
|
-
const experienceData = parseExperience(sections.experience);
|
|
380
|
+
const experienceData = parseExperience(sections.experience, config.referenceDate);
|
|
374
381
|
const educationEntries = parseEducation(sections.education);
|
|
375
382
|
const totalExperienceYears = sumExperienceYears(
|
|
376
383
|
experienceData.entries.map((entry) => entry.dates).filter((range) => Boolean(range))
|
|
@@ -494,8 +501,9 @@ function scoreSkills(resume, job, config) {
|
|
|
494
501
|
0,
|
|
495
502
|
100
|
|
496
503
|
);
|
|
497
|
-
const
|
|
498
|
-
|
|
504
|
+
const matched = [...required].filter((skill) => resumeSkills.has(skill)).sort();
|
|
505
|
+
const missing = [...required].filter((skill) => !resumeSkills.has(skill)).sort();
|
|
506
|
+
return { score, matched, missing };
|
|
499
507
|
}
|
|
500
508
|
function scoreExperience(resume, job, config) {
|
|
501
509
|
const requiredYears = job.minExperienceYears ?? config.profile?.minExperience ?? 0;
|
|
@@ -513,11 +521,15 @@ function scoreExperience(resume, job, config) {
|
|
|
513
521
|
return { score, missingYears: Number(missingYears.toFixed(2)) };
|
|
514
522
|
}
|
|
515
523
|
function scoreKeywords(resume, job, config) {
|
|
516
|
-
const jobKeywordSet = new Set(
|
|
524
|
+
const jobKeywordSet = new Set(
|
|
525
|
+
job.keywords.map((k) => normalizeSkill(k, config.skillAliases))
|
|
526
|
+
);
|
|
517
527
|
if (jobKeywordSet.size === 0) {
|
|
518
528
|
return { score: 100, matchedKeywords: [], missingKeywords: [], overusedKeywords: [] };
|
|
519
529
|
}
|
|
520
|
-
const resumeTokens = tokenize(resume.normalizedText)
|
|
530
|
+
const resumeTokens = tokenize(resume.normalizedText).map(
|
|
531
|
+
(t) => normalizeSkill(t, config.skillAliases)
|
|
532
|
+
);
|
|
521
533
|
const resumeTokenSet = new Set(resumeTokens);
|
|
522
534
|
const matchedKeywords = [...jobKeywordSet].filter((keyword) => resumeTokenSet.has(keyword));
|
|
523
535
|
const missingKeywords = [...jobKeywordSet].filter((keyword) => !resumeTokenSet.has(keyword));
|
|
@@ -531,9 +543,9 @@ function scoreKeywords(resume, job, config) {
|
|
|
531
543
|
});
|
|
532
544
|
return {
|
|
533
545
|
score,
|
|
534
|
-
matchedKeywords: unique(matchedKeywords),
|
|
535
|
-
missingKeywords: unique(missingKeywords),
|
|
536
|
-
overusedKeywords: unique(overusedKeywords)
|
|
546
|
+
matchedKeywords: unique(matchedKeywords).sort(),
|
|
547
|
+
missingKeywords: unique(missingKeywords).sort(),
|
|
548
|
+
overusedKeywords: unique(overusedKeywords).sort()
|
|
537
549
|
};
|
|
538
550
|
}
|
|
539
551
|
function scoreEducation(resume, job) {
|
|
@@ -565,12 +577,17 @@ function calculateScore(resume, job, config) {
|
|
|
565
577
|
return {
|
|
566
578
|
score: clamp(Number(weightedScore.toFixed(2)), 0, 100),
|
|
567
579
|
breakdown,
|
|
580
|
+
matchedSkills: skillsResult.matched,
|
|
581
|
+
missingSkills: skillsResult.missing,
|
|
568
582
|
matchedKeywords: keywordResult.matchedKeywords,
|
|
569
583
|
missingKeywords: keywordResult.missingKeywords,
|
|
570
584
|
overusedKeywords: keywordResult.overusedKeywords,
|
|
571
585
|
suggestions: [],
|
|
572
586
|
warnings: [],
|
|
573
|
-
|
|
587
|
+
// detectedSections / parsedExperienceYears / experienceGap: filled by index.ts
|
|
588
|
+
experienceGap: experienceResult.missingYears,
|
|
589
|
+
detectedSections: [],
|
|
590
|
+
parsedExperienceYears: 0,
|
|
574
591
|
missingExperienceYears: experienceResult.missingYears,
|
|
575
592
|
educationScore
|
|
576
593
|
};
|
|
@@ -578,7 +595,9 @@ function calculateScore(resume, job, config) {
|
|
|
578
595
|
|
|
579
596
|
// src/profiles/index.ts
|
|
580
597
|
var defaultSkillAliases = {
|
|
581
|
-
|
|
598
|
+
// ponytail: "node" split from javascript — Node.js runtime !== JS language
|
|
599
|
+
javascript: ["js"],
|
|
600
|
+
node: ["node.js", "nodejs"],
|
|
582
601
|
typescript: ["ts"],
|
|
583
602
|
react: ["reactjs", "react.js"],
|
|
584
603
|
"c++": ["cpp"],
|
|
@@ -639,7 +658,14 @@ var DEFAULT_SECTION_PENALTIES = {
|
|
|
639
658
|
function normalizeWeights(weights) {
|
|
640
659
|
const total = weights.skills + weights.experience + weights.keywords + weights.education;
|
|
641
660
|
if (total === 0) {
|
|
642
|
-
|
|
661
|
+
const equal = 1 / 4;
|
|
662
|
+
return {
|
|
663
|
+
skills: equal,
|
|
664
|
+
experience: equal,
|
|
665
|
+
keywords: equal,
|
|
666
|
+
education: equal,
|
|
667
|
+
normalizedTotal: 1
|
|
668
|
+
};
|
|
643
669
|
}
|
|
644
670
|
return {
|
|
645
671
|
skills: weights.skills / total,
|
|
@@ -666,7 +692,8 @@ function resolveConfig(config = {}) {
|
|
|
666
692
|
...DEFAULT_SECTION_PENALTIES,
|
|
667
693
|
...config.sectionPenalties ?? {}
|
|
668
694
|
},
|
|
669
|
-
allowPartialMatches: config.allowPartialMatches ?? true
|
|
695
|
+
allowPartialMatches: config.allowPartialMatches ?? true,
|
|
696
|
+
referenceDate: config.referenceDate ? new Date(config.referenceDate) : void 0
|
|
670
697
|
};
|
|
671
698
|
return resolved;
|
|
672
699
|
}
|
|
@@ -778,6 +805,53 @@ var LLMBudgetManager = class {
|
|
|
778
805
|
}
|
|
779
806
|
};
|
|
780
807
|
|
|
808
|
+
// src/llm/validation.ts
|
|
809
|
+
function validateJsonSchema(data, schema) {
|
|
810
|
+
if (schema.type !== "object" || typeof data !== "object" || data === null) return false;
|
|
811
|
+
const obj = data;
|
|
812
|
+
if (schema.required) {
|
|
813
|
+
for (const r of schema.required) {
|
|
814
|
+
if (!(r in obj)) return false;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
818
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
819
|
+
const value = obj[key];
|
|
820
|
+
if (value === void 0) continue;
|
|
821
|
+
if (propSchema == null) continue;
|
|
822
|
+
const expectedType = propSchema.type;
|
|
823
|
+
if (!expectedType) continue;
|
|
824
|
+
switch (expectedType) {
|
|
825
|
+
case "string":
|
|
826
|
+
if (typeof value !== "string") return false;
|
|
827
|
+
break;
|
|
828
|
+
case "number":
|
|
829
|
+
if (typeof value !== "number") return false;
|
|
830
|
+
break;
|
|
831
|
+
case "boolean":
|
|
832
|
+
if (typeof value !== "boolean") return false;
|
|
833
|
+
break;
|
|
834
|
+
case "array":
|
|
835
|
+
if (!Array.isArray(value)) return false;
|
|
836
|
+
const items = propSchema.items;
|
|
837
|
+
if (items && items.type && Array.isArray(value)) {
|
|
838
|
+
const itemType = items.type;
|
|
839
|
+
for (const item of value) {
|
|
840
|
+
if (itemType === "string" && typeof item !== "string") return false;
|
|
841
|
+
if (itemType === "number" && typeof item !== "number") return false;
|
|
842
|
+
if (itemType === "object" && (typeof item !== "object" || item === null)) return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
case "object":
|
|
847
|
+
if (typeof value !== "object" || value === null) return false;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
|
|
781
855
|
// src/llm/llm.manager.ts
|
|
782
856
|
var LLMManager = class {
|
|
783
857
|
constructor(config) {
|
|
@@ -791,6 +865,11 @@ var LLMManager = class {
|
|
|
791
865
|
* Structured call to LLM with timeout and budget protection
|
|
792
866
|
*/
|
|
793
867
|
async callLLM(systemPrompt, userPrompt, schema, options = {}) {
|
|
868
|
+
const onUnhandled = (reason) => {
|
|
869
|
+
this.warnings.push(`Unhandled rejection during LLM call: ${String(reason)}`);
|
|
870
|
+
};
|
|
871
|
+
process.on("unhandledRejection", onUnhandled);
|
|
872
|
+
let clientPromise = void 0;
|
|
794
873
|
try {
|
|
795
874
|
const estimatedTokens = this.estimateTokens(systemPrompt, userPrompt, options.requestedTokens);
|
|
796
875
|
try {
|
|
@@ -798,30 +877,46 @@ var LLMManager = class {
|
|
|
798
877
|
} catch (e) {
|
|
799
878
|
const msg = `LLM budget exhausted: ${e.message}`;
|
|
800
879
|
this.warnings.push(msg);
|
|
880
|
+
globalThis.setTimeout(() => process.removeListener("unhandledRejection", onUnhandled), 100);
|
|
801
881
|
return { success: false, fallback: true, error: msg };
|
|
802
882
|
}
|
|
803
883
|
if (!this.isValidJsonSchema(schema)) {
|
|
804
884
|
const msg = "Invalid JSON schema provided";
|
|
805
885
|
this.warnings.push(msg);
|
|
886
|
+
globalThis.setTimeout(() => process.removeListener("unhandledRejection", onUnhandled), 100);
|
|
806
887
|
return { success: false, fallback: true, error: msg };
|
|
807
888
|
}
|
|
808
889
|
const strictUserPrompt = `${userPrompt}
|
|
809
890
|
|
|
810
891
|
Return ONLY valid JSON matching the schema below.
|
|
811
892
|
No explanations. No markdown. No additional text.`;
|
|
812
|
-
|
|
813
|
-
this.
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
893
|
+
clientPromise = this.client.createCompletion({
|
|
894
|
+
model: options.useThinking ? this.config.models?.thinking || this.config.models?.default || "gpt-4o" : this.config.models?.default || "gpt-4o",
|
|
895
|
+
messages: [
|
|
896
|
+
{ role: "system", content: systemPrompt },
|
|
897
|
+
{ role: "user", content: strictUserPrompt }
|
|
898
|
+
],
|
|
899
|
+
max_tokens: options.requestedTokens || 2e3,
|
|
900
|
+
response_format: schema
|
|
901
|
+
});
|
|
902
|
+
void clientPromise.catch(() => {
|
|
903
|
+
});
|
|
904
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
905
|
+
const id = globalThis.setTimeout(() => reject(new Error(`LLM call timeout after ${this.timeoutMs}ms`)), this.timeoutMs);
|
|
906
|
+
void clientPromise.finally(() => globalThis.clearTimeout(id));
|
|
907
|
+
});
|
|
908
|
+
void timeoutPromise.catch(() => {
|
|
909
|
+
});
|
|
910
|
+
const response = await Promise.race([clientPromise, timeoutPromise]);
|
|
824
911
|
if (!response || !response.content) {
|
|
912
|
+
const graceMs = Math.min(Math.max(this.timeoutMs * 2, 100), 500);
|
|
913
|
+
try {
|
|
914
|
+
await Promise.race([clientPromise.catch(() => {
|
|
915
|
+
}), new Promise((r) => setTimeout(r, graceMs))]);
|
|
916
|
+
} catch (e) {
|
|
917
|
+
} finally {
|
|
918
|
+
process.removeListener("unhandledRejection", onUnhandled);
|
|
919
|
+
}
|
|
825
920
|
return { success: false, fallback: true, error: "Empty response from LLM" };
|
|
826
921
|
}
|
|
827
922
|
let parsedContent;
|
|
@@ -834,15 +929,32 @@ No explanations. No markdown. No additional text.`;
|
|
|
834
929
|
} catch (e) {
|
|
835
930
|
const msg = `Invalid JSON in LLM response: ${e.message}`;
|
|
836
931
|
this.warnings.push(msg);
|
|
932
|
+
const graceMs = Math.min(Math.max(this.timeoutMs * 2, 100), 500);
|
|
933
|
+
try {
|
|
934
|
+
await Promise.race([clientPromise.catch(() => {
|
|
935
|
+
}), new Promise((r) => setTimeout(r, graceMs))]);
|
|
936
|
+
} catch (e2) {
|
|
937
|
+
} finally {
|
|
938
|
+
process.removeListener("unhandledRejection", onUnhandled);
|
|
939
|
+
}
|
|
837
940
|
return { success: false, fallback: true, error: msg };
|
|
838
941
|
}
|
|
839
942
|
if (!this.validateAgainstSchema(parsedContent, schema)) {
|
|
840
943
|
const msg = "LLM response does not match schema";
|
|
841
944
|
this.warnings.push(msg);
|
|
945
|
+
const graceMs = Math.min(Math.max(this.timeoutMs * 2, 100), 500);
|
|
946
|
+
try {
|
|
947
|
+
await Promise.race([clientPromise.catch(() => {
|
|
948
|
+
}), new Promise((r) => setTimeout(r, graceMs))]);
|
|
949
|
+
} catch (e) {
|
|
950
|
+
} finally {
|
|
951
|
+
process.removeListener("unhandledRejection", onUnhandled);
|
|
952
|
+
}
|
|
842
953
|
return { success: false, fallback: true, error: msg };
|
|
843
954
|
}
|
|
844
955
|
const tokensUsed = response.usage?.total_tokens || estimatedTokens;
|
|
845
956
|
this.budgetManager.recordUsage(tokensUsed);
|
|
957
|
+
process.removeListener("unhandledRejection", onUnhandled);
|
|
846
958
|
return {
|
|
847
959
|
success: true,
|
|
848
960
|
fallback: false,
|
|
@@ -852,6 +964,14 @@ No explanations. No markdown. No additional text.`;
|
|
|
852
964
|
} catch (e) {
|
|
853
965
|
const msg = `LLM call failed: ${e.message}`;
|
|
854
966
|
this.warnings.push(msg);
|
|
967
|
+
const graceMs = Math.min(Math.max(this.timeoutMs * 2, 100), 500);
|
|
968
|
+
try {
|
|
969
|
+
await Promise.race([clientPromise.catch(() => {
|
|
970
|
+
}), new Promise((r) => setTimeout(r, graceMs))]);
|
|
971
|
+
} catch (e2) {
|
|
972
|
+
} finally {
|
|
973
|
+
process.removeListener("unhandledRejection", onUnhandled);
|
|
974
|
+
}
|
|
855
975
|
return { success: false, fallback: true, error: msg };
|
|
856
976
|
}
|
|
857
977
|
}
|
|
@@ -874,14 +994,6 @@ No explanations. No markdown. No additional text.`;
|
|
|
874
994
|
return this.config.enable?.[feature] === true;
|
|
875
995
|
}
|
|
876
996
|
// ============ Private helpers ============
|
|
877
|
-
/**
|
|
878
|
-
* Create a timeout promise
|
|
879
|
-
*/
|
|
880
|
-
createTimeout(ms) {
|
|
881
|
-
return new Promise((_, reject) => {
|
|
882
|
-
globalThis.setTimeout(() => reject(new Error(`LLM call timeout after ${ms}ms`)), ms);
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
997
|
/**
|
|
886
998
|
* Estimate tokens for a call (rough approximation)
|
|
887
999
|
* 1 token ≈ 4 characters average
|
|
@@ -904,18 +1016,22 @@ No explanations. No markdown. No additional text.`;
|
|
|
904
1016
|
* Simple schema validation - check required fields exist
|
|
905
1017
|
*/
|
|
906
1018
|
validateAgainstSchema(data, schema) {
|
|
907
|
-
|
|
908
|
-
return
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1019
|
+
try {
|
|
1020
|
+
return validateJsonSchema(data, schema);
|
|
1021
|
+
} catch (e) {
|
|
1022
|
+
if (typeof data !== "object" || data === null) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
const obj = data;
|
|
1026
|
+
if (schema.required) {
|
|
1027
|
+
for (const field of schema.required) {
|
|
1028
|
+
if (!(field in obj)) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
915
1031
|
}
|
|
916
1032
|
}
|
|
1033
|
+
return true;
|
|
917
1034
|
}
|
|
918
|
-
return true;
|
|
919
1035
|
}
|
|
920
1036
|
};
|
|
921
1037
|
|
|
@@ -1274,9 +1390,14 @@ function analyzeResume(input) {
|
|
|
1274
1390
|
return {
|
|
1275
1391
|
score: finalScore,
|
|
1276
1392
|
breakdown: scoring.breakdown,
|
|
1393
|
+
matchedSkills: scoring.matchedSkills,
|
|
1394
|
+
missingSkills: scoring.missingSkills,
|
|
1277
1395
|
matchedKeywords: scoring.matchedKeywords,
|
|
1278
1396
|
missingKeywords: scoring.missingKeywords,
|
|
1279
1397
|
overusedKeywords: scoring.overusedKeywords,
|
|
1398
|
+
experienceGap: scoring.experienceGap,
|
|
1399
|
+
detectedSections: parsedResume.detectedSections,
|
|
1400
|
+
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
1280
1401
|
suggestions,
|
|
1281
1402
|
warnings: [...suggestionResult.warnings, ...llmWarnings]
|
|
1282
1403
|
};
|
|
@@ -1333,9 +1454,14 @@ async function analyzeResumeAsync(input) {
|
|
|
1333
1454
|
return {
|
|
1334
1455
|
score: finalScore,
|
|
1335
1456
|
breakdown: scoring.breakdown,
|
|
1457
|
+
matchedSkills: scoring.matchedSkills,
|
|
1458
|
+
missingSkills: scoring.missingSkills,
|
|
1336
1459
|
matchedKeywords: scoring.matchedKeywords,
|
|
1337
1460
|
missingKeywords: scoring.missingKeywords,
|
|
1338
1461
|
overusedKeywords: scoring.overusedKeywords,
|
|
1462
|
+
experienceGap: scoring.experienceGap,
|
|
1463
|
+
detectedSections: parsedResume.detectedSections,
|
|
1464
|
+
parsedExperienceYears: parsedResume.totalExperienceYears,
|
|
1339
1465
|
suggestions,
|
|
1340
1466
|
warnings: [...suggestionResult.warnings, ...llmWarnings]
|
|
1341
1467
|
};
|