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