resuml 1.11.0 → 1.12.1
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/dist/index.cjs +454 -11
- package/dist/index.cjs.map +1 -1
- package/dist/mcp/server.cjs +454 -11
- package/dist/mcp/server.cjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -481,6 +481,7 @@ var init_en = __esm({
|
|
|
481
481
|
],
|
|
482
482
|
pronouns: ["i", "me", "my", "mine", "myself", "we", "our", "ours"],
|
|
483
483
|
stopWords: [
|
|
484
|
+
// Articles & determiners
|
|
484
485
|
"a",
|
|
485
486
|
"an",
|
|
486
487
|
"the",
|
|
@@ -551,7 +552,281 @@ var init_en = __esm({
|
|
|
551
552
|
"such",
|
|
552
553
|
"than",
|
|
553
554
|
"too",
|
|
554
|
-
"very"
|
|
555
|
+
"very",
|
|
556
|
+
// Pronouns & possessives (also checked by pronoun check, but filter from JD keywords)
|
|
557
|
+
"you",
|
|
558
|
+
"your",
|
|
559
|
+
"yours",
|
|
560
|
+
"yourself",
|
|
561
|
+
"we",
|
|
562
|
+
"our",
|
|
563
|
+
"ours",
|
|
564
|
+
"ourselves",
|
|
565
|
+
"they",
|
|
566
|
+
"them",
|
|
567
|
+
"their",
|
|
568
|
+
"theirs",
|
|
569
|
+
"he",
|
|
570
|
+
"she",
|
|
571
|
+
"his",
|
|
572
|
+
"her",
|
|
573
|
+
"hers",
|
|
574
|
+
"who",
|
|
575
|
+
"whom",
|
|
576
|
+
"whose",
|
|
577
|
+
"which",
|
|
578
|
+
"what",
|
|
579
|
+
"where",
|
|
580
|
+
"when",
|
|
581
|
+
"how",
|
|
582
|
+
"why",
|
|
583
|
+
// Common JD filler words (not meaningful for skill matching)
|
|
584
|
+
"able",
|
|
585
|
+
"also",
|
|
586
|
+
"across",
|
|
587
|
+
"already",
|
|
588
|
+
"always",
|
|
589
|
+
"among",
|
|
590
|
+
"any",
|
|
591
|
+
"apply",
|
|
592
|
+
"become",
|
|
593
|
+
"believe",
|
|
594
|
+
"best",
|
|
595
|
+
"bring",
|
|
596
|
+
"change",
|
|
597
|
+
"come",
|
|
598
|
+
"committed",
|
|
599
|
+
"company",
|
|
600
|
+
"comfortable",
|
|
601
|
+
"critical",
|
|
602
|
+
"current",
|
|
603
|
+
"day",
|
|
604
|
+
"desired",
|
|
605
|
+
"either",
|
|
606
|
+
"end",
|
|
607
|
+
"ensure",
|
|
608
|
+
"environment",
|
|
609
|
+
"equal",
|
|
610
|
+
"even",
|
|
611
|
+
"excellent",
|
|
612
|
+
"exciting",
|
|
613
|
+
"exceptional",
|
|
614
|
+
"expected",
|
|
615
|
+
"experience",
|
|
616
|
+
"fast",
|
|
617
|
+
"field",
|
|
618
|
+
"find",
|
|
619
|
+
"first",
|
|
620
|
+
"focused",
|
|
621
|
+
"follow",
|
|
622
|
+
"get",
|
|
623
|
+
"give",
|
|
624
|
+
"go",
|
|
625
|
+
"going",
|
|
626
|
+
"good",
|
|
627
|
+
"great",
|
|
628
|
+
"group",
|
|
629
|
+
"grow",
|
|
630
|
+
"growing",
|
|
631
|
+
"growth",
|
|
632
|
+
"help",
|
|
633
|
+
"here",
|
|
634
|
+
"high",
|
|
635
|
+
"highly",
|
|
636
|
+
"ideal",
|
|
637
|
+
"impact",
|
|
638
|
+
"important",
|
|
639
|
+
"include",
|
|
640
|
+
"includes",
|
|
641
|
+
"including",
|
|
642
|
+
"industry",
|
|
643
|
+
"interested",
|
|
644
|
+
"job",
|
|
645
|
+
"join",
|
|
646
|
+
"just",
|
|
647
|
+
"keep",
|
|
648
|
+
"key",
|
|
649
|
+
"know",
|
|
650
|
+
"large",
|
|
651
|
+
"latest",
|
|
652
|
+
"lead",
|
|
653
|
+
"level",
|
|
654
|
+
"like",
|
|
655
|
+
"location",
|
|
656
|
+
"long",
|
|
657
|
+
"look",
|
|
658
|
+
"looking",
|
|
659
|
+
"love",
|
|
660
|
+
"make",
|
|
661
|
+
"many",
|
|
662
|
+
"much",
|
|
663
|
+
"must",
|
|
664
|
+
"need",
|
|
665
|
+
"new",
|
|
666
|
+
"next",
|
|
667
|
+
"offer",
|
|
668
|
+
"one",
|
|
669
|
+
"only",
|
|
670
|
+
"open",
|
|
671
|
+
"opportunity",
|
|
672
|
+
"order",
|
|
673
|
+
"others",
|
|
674
|
+
"own",
|
|
675
|
+
"pace",
|
|
676
|
+
"part",
|
|
677
|
+
"partner",
|
|
678
|
+
"passionate",
|
|
679
|
+
"people",
|
|
680
|
+
"per",
|
|
681
|
+
"play",
|
|
682
|
+
"plus",
|
|
683
|
+
"position",
|
|
684
|
+
"preferred",
|
|
685
|
+
"provide",
|
|
686
|
+
"put",
|
|
687
|
+
"qualifications",
|
|
688
|
+
"quickly",
|
|
689
|
+
"range",
|
|
690
|
+
"related",
|
|
691
|
+
"required",
|
|
692
|
+
"requirements",
|
|
693
|
+
"requirement",
|
|
694
|
+
"responsible",
|
|
695
|
+
"responsibilities",
|
|
696
|
+
"responsibility",
|
|
697
|
+
"result",
|
|
698
|
+
"right",
|
|
699
|
+
"role",
|
|
700
|
+
"run",
|
|
701
|
+
"same",
|
|
702
|
+
"see",
|
|
703
|
+
"seek",
|
|
704
|
+
"seeking",
|
|
705
|
+
"set",
|
|
706
|
+
"several",
|
|
707
|
+
"since",
|
|
708
|
+
"skills",
|
|
709
|
+
"someone",
|
|
710
|
+
"start",
|
|
711
|
+
"state",
|
|
712
|
+
"still",
|
|
713
|
+
"strong",
|
|
714
|
+
"success",
|
|
715
|
+
"successful",
|
|
716
|
+
"support",
|
|
717
|
+
"sure",
|
|
718
|
+
"take",
|
|
719
|
+
"team",
|
|
720
|
+
"then",
|
|
721
|
+
"there",
|
|
722
|
+
"thing",
|
|
723
|
+
"think",
|
|
724
|
+
"through",
|
|
725
|
+
"time",
|
|
726
|
+
"together",
|
|
727
|
+
"top",
|
|
728
|
+
"truly",
|
|
729
|
+
"try",
|
|
730
|
+
"two",
|
|
731
|
+
"type",
|
|
732
|
+
"use",
|
|
733
|
+
"used",
|
|
734
|
+
"using",
|
|
735
|
+
"value",
|
|
736
|
+
"want",
|
|
737
|
+
"way",
|
|
738
|
+
"well",
|
|
739
|
+
"while",
|
|
740
|
+
"within",
|
|
741
|
+
"without",
|
|
742
|
+
"work",
|
|
743
|
+
"working",
|
|
744
|
+
"world",
|
|
745
|
+
"would",
|
|
746
|
+
"year",
|
|
747
|
+
"years",
|
|
748
|
+
// Section headers & structural words (not technical skills)
|
|
749
|
+
"description",
|
|
750
|
+
"overview",
|
|
751
|
+
"summary",
|
|
752
|
+
"duties",
|
|
753
|
+
"bachelor",
|
|
754
|
+
"bachelors",
|
|
755
|
+
"master",
|
|
756
|
+
"masters",
|
|
757
|
+
"degree",
|
|
758
|
+
"phd",
|
|
759
|
+
"minimum",
|
|
760
|
+
"preferred",
|
|
761
|
+
"implement",
|
|
762
|
+
"process",
|
|
763
|
+
"robust",
|
|
764
|
+
"consistent",
|
|
765
|
+
"operations",
|
|
766
|
+
// URL/email/domain fragments
|
|
767
|
+
"http",
|
|
768
|
+
"https",
|
|
769
|
+
"www",
|
|
770
|
+
"com",
|
|
771
|
+
"org",
|
|
772
|
+
"net",
|
|
773
|
+
"mailto",
|
|
774
|
+
// Resume/YAML schema field names (in case raw YAML is pasted)
|
|
775
|
+
"name",
|
|
776
|
+
"keywords",
|
|
777
|
+
"highlights",
|
|
778
|
+
"startdate",
|
|
779
|
+
"enddate",
|
|
780
|
+
"website",
|
|
781
|
+
"profiles",
|
|
782
|
+
"basics",
|
|
783
|
+
"position",
|
|
784
|
+
"institution",
|
|
785
|
+
"studytype",
|
|
786
|
+
"fluency",
|
|
787
|
+
"issuer",
|
|
788
|
+
"network",
|
|
789
|
+
"username",
|
|
790
|
+
"countrycode",
|
|
791
|
+
"region",
|
|
792
|
+
// Generic nouns that aren't skills
|
|
793
|
+
"product",
|
|
794
|
+
"company",
|
|
795
|
+
"service",
|
|
796
|
+
"services",
|
|
797
|
+
"platform",
|
|
798
|
+
"solutions",
|
|
799
|
+
"ability",
|
|
800
|
+
"opportunity",
|
|
801
|
+
"candidate",
|
|
802
|
+
"applicant",
|
|
803
|
+
"position",
|
|
804
|
+
"salary",
|
|
805
|
+
"compensation",
|
|
806
|
+
"benefits",
|
|
807
|
+
"perks",
|
|
808
|
+
"bonus",
|
|
809
|
+
"development",
|
|
810
|
+
"management",
|
|
811
|
+
"knowledge",
|
|
812
|
+
"modern",
|
|
813
|
+
"advanced",
|
|
814
|
+
"practices",
|
|
815
|
+
"nice",
|
|
816
|
+
"technologies",
|
|
817
|
+
"technology",
|
|
818
|
+
"frameworks",
|
|
819
|
+
"framework",
|
|
820
|
+
"tools",
|
|
821
|
+
"data",
|
|
822
|
+
"based",
|
|
823
|
+
"contribute",
|
|
824
|
+
"contributions",
|
|
825
|
+
"migration",
|
|
826
|
+
"leading",
|
|
827
|
+
"source",
|
|
828
|
+
"visit",
|
|
829
|
+
"join"
|
|
555
830
|
]
|
|
556
831
|
};
|
|
557
832
|
en_default = en;
|
|
@@ -1122,8 +1397,20 @@ var init_genericChecks = __esm({
|
|
|
1122
1397
|
});
|
|
1123
1398
|
|
|
1124
1399
|
// src/ats/jdMatcher.ts
|
|
1400
|
+
function stripNoise(text) {
|
|
1401
|
+
return text.replace(/https?:\/\/[^\s]+/gi, " ").replace(/www\.[^\s]+/gi, " ").replace(/[\w.+-]+@[\w.-]+\.[a-z]{2,}/gi, " ").replace(/(?:^|\s)\/[\w/.-]+/g, " ").replace(/\b[a-z]+[A-Z][a-zA-Z]*\b/g, (match2) => {
|
|
1402
|
+
return match2.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1125
1405
|
function tokenize(text, stopWords) {
|
|
1126
|
-
return text.toLowerCase().replace(/[^a-zA-Z0-9äöüßÄÖÜàáâãéèêëíìîïóòôõúùûüñç\s
|
|
1406
|
+
return text.toLowerCase().replace(/[^a-zA-Z0-9äöüßÄÖÜàáâãéèêëíìîïóòôõúùûüñç\s/+-]/g, " ").split(/\s+/).filter((word) => {
|
|
1407
|
+
if (word.length <= 2) return false;
|
|
1408
|
+
if (stopWords.has(word)) return false;
|
|
1409
|
+
if (word.startsWith("//") || word.startsWith("http")) return false;
|
|
1410
|
+
if (/^\d+$/.test(word)) return false;
|
|
1411
|
+
if (/^[/+-]+$/.test(word)) return false;
|
|
1412
|
+
return true;
|
|
1413
|
+
});
|
|
1127
1414
|
}
|
|
1128
1415
|
function simpleStem(word, language) {
|
|
1129
1416
|
if (language === "de") {
|
|
@@ -1166,20 +1453,163 @@ function buildTfMap(tokens) {
|
|
|
1166
1453
|
}
|
|
1167
1454
|
return tf;
|
|
1168
1455
|
}
|
|
1456
|
+
function splitJdSections(text) {
|
|
1457
|
+
const lines = text.split("\n");
|
|
1458
|
+
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)/i;
|
|
1459
|
+
let inReqSection = false;
|
|
1460
|
+
const reqLines = [];
|
|
1461
|
+
const otherLines = [];
|
|
1462
|
+
for (const line of lines) {
|
|
1463
|
+
const trimmed = line.trim();
|
|
1464
|
+
if (reqPatterns.test(trimmed.replace(/[:#*-]/g, "").trim())) {
|
|
1465
|
+
inReqSection = true;
|
|
1466
|
+
} else if (/^(about|summary|who we are|our (company|team|mission)|description|overview|benefits|perks|compensation|salary)/i.test(trimmed.replace(/[:#*-]/g, "").trim())) {
|
|
1467
|
+
inReqSection = false;
|
|
1468
|
+
}
|
|
1469
|
+
if (inReqSection) {
|
|
1470
|
+
reqLines.push(line);
|
|
1471
|
+
} else {
|
|
1472
|
+
otherLines.push(line);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
requirementText: reqLines.join("\n"),
|
|
1477
|
+
otherText: otherLines.join("\n")
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function extractCompoundTerms(text) {
|
|
1481
|
+
const patterns = [
|
|
1482
|
+
/\b(machine\s+learning)\b/gi,
|
|
1483
|
+
/\b(deep\s+learning)\b/gi,
|
|
1484
|
+
/\b(computer\s+vision)\b/gi,
|
|
1485
|
+
/\b(natural\s+language\s+processing)\b/gi,
|
|
1486
|
+
/\b(data\s+pipelines?)\b/gi,
|
|
1487
|
+
/\b(data\s+models?)\b/gi,
|
|
1488
|
+
/\b(data\s+engineering)\b/gi,
|
|
1489
|
+
/\b(data\s+structures?)\b/gi,
|
|
1490
|
+
/\b(data\s+quality)\b/gi,
|
|
1491
|
+
/\b(data\s+flows?)\b/gi,
|
|
1492
|
+
/\b(data\s+orchestration)\b/gi,
|
|
1493
|
+
/\b(data\s+warehou\w+)\b/gi,
|
|
1494
|
+
/\b(synthetic\s+data)\b/gi,
|
|
1495
|
+
/\b(ci\s*\/?\s*cd)\b/gi,
|
|
1496
|
+
/\b(rest\s+api)\b/gi,
|
|
1497
|
+
/\b(open\s+source)\b/gi,
|
|
1498
|
+
/\b(human[\s-]+in[\s-]+the[\s-]+loop)\b/gi,
|
|
1499
|
+
/\b(self[\s-]+service)\b/gi,
|
|
1500
|
+
/\b(agentic\s+workflows?)\b/gi,
|
|
1501
|
+
/\b(distributed\s+systems?)\b/gi,
|
|
1502
|
+
/\b(cloud\s+infrastructure)\b/gi,
|
|
1503
|
+
/\b(micro\s*services?)\b/gi,
|
|
1504
|
+
/\b(full[\s-]+stack)\b/gi,
|
|
1505
|
+
/\b(front[\s-]*end)\b/gi,
|
|
1506
|
+
/\b(back[\s-]*end)\b/gi,
|
|
1507
|
+
/\b(sql\s*\/?\s*nosql)\b/gi
|
|
1508
|
+
];
|
|
1509
|
+
const found = [];
|
|
1510
|
+
for (const pattern of patterns) {
|
|
1511
|
+
const matches = text.matchAll(pattern);
|
|
1512
|
+
for (const m of matches) {
|
|
1513
|
+
const term = m[1]?.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1514
|
+
if (term && !found.includes(term)) {
|
|
1515
|
+
found.push(term);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return found;
|
|
1520
|
+
}
|
|
1521
|
+
function extractBrandNames(text) {
|
|
1522
|
+
const brands = /* @__PURE__ */ new Set();
|
|
1523
|
+
const brandPatterns = [
|
|
1524
|
+
/\bat\s+([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*)/g,
|
|
1525
|
+
/(?:^|\.\s+)([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*)\s+(?:is|are|was|has|Inc|Corp|Ltd|GmbH)/g,
|
|
1526
|
+
/\b(?:join(?:ing)?|about)\s+([A-Z][a-zA-Z]+)/g
|
|
1527
|
+
];
|
|
1528
|
+
for (const pattern of brandPatterns) {
|
|
1529
|
+
const matches = text.matchAll(pattern);
|
|
1530
|
+
for (const m of matches) {
|
|
1531
|
+
const name = m[1]?.toLowerCase();
|
|
1532
|
+
if (name) {
|
|
1533
|
+
for (const word of name.split(/\s+/)) {
|
|
1534
|
+
if (word.length > 2) brands.add(word);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const knownBrands = [
|
|
1540
|
+
"apple",
|
|
1541
|
+
"google",
|
|
1542
|
+
"meta",
|
|
1543
|
+
"facebook",
|
|
1544
|
+
"amazon",
|
|
1545
|
+
"microsoft",
|
|
1546
|
+
"netflix",
|
|
1547
|
+
"uber",
|
|
1548
|
+
"airbnb",
|
|
1549
|
+
"twitter",
|
|
1550
|
+
"linkedin",
|
|
1551
|
+
"spotify",
|
|
1552
|
+
"stripe",
|
|
1553
|
+
"shopify",
|
|
1554
|
+
"iphone",
|
|
1555
|
+
"ipad",
|
|
1556
|
+
"mac",
|
|
1557
|
+
"macbook",
|
|
1558
|
+
"airpods",
|
|
1559
|
+
"android",
|
|
1560
|
+
"windows",
|
|
1561
|
+
"alexa",
|
|
1562
|
+
"siri",
|
|
1563
|
+
"cortana",
|
|
1564
|
+
"gmail",
|
|
1565
|
+
"chrome",
|
|
1566
|
+
"safari",
|
|
1567
|
+
"firefox"
|
|
1568
|
+
];
|
|
1569
|
+
for (const b of knownBrands) brands.add(b);
|
|
1570
|
+
return brands;
|
|
1571
|
+
}
|
|
1169
1572
|
function extractKeywords(text, language, maxKeywords = 30) {
|
|
1170
1573
|
const langData = getLanguageData(language);
|
|
1171
1574
|
const stopWords = new Set(langData.stopWords);
|
|
1172
|
-
const
|
|
1173
|
-
const
|
|
1575
|
+
const cleanText = stripNoise(text);
|
|
1576
|
+
const compoundTerms = extractCompoundTerms(cleanText);
|
|
1577
|
+
const brandNames = extractBrandNames(text);
|
|
1578
|
+
const { requirementText } = splitJdSections(cleanText);
|
|
1579
|
+
const hasRequirementSections = requirementText.trim().length > 0;
|
|
1580
|
+
let allTokens;
|
|
1581
|
+
if (hasRequirementSections) {
|
|
1582
|
+
allTokens = tokenize(requirementText, stopWords).filter((t) => !brandNames.has(t));
|
|
1583
|
+
} else {
|
|
1584
|
+
allTokens = tokenize(cleanText, stopWords).filter((t) => !brandNames.has(t));
|
|
1585
|
+
}
|
|
1586
|
+
const stemmed = allTokens.map((t) => simpleStem(t, language));
|
|
1174
1587
|
const tf = buildTfMap(stemmed);
|
|
1175
1588
|
const stemToOriginal = /* @__PURE__ */ new Map();
|
|
1176
|
-
for (let i = 0; i <
|
|
1589
|
+
for (let i = 0; i < allTokens.length; i++) {
|
|
1177
1590
|
const stem = stemmed[i] ?? "";
|
|
1178
1591
|
if (!stemToOriginal.has(stem)) {
|
|
1179
|
-
stemToOriginal.set(stem,
|
|
1592
|
+
stemToOriginal.set(stem, allTokens[i] ?? "");
|
|
1180
1593
|
}
|
|
1181
1594
|
}
|
|
1182
|
-
|
|
1595
|
+
const compoundsFlat = compoundTerms.join(" ").split(/\s+/);
|
|
1596
|
+
const compoundWordSet = new Set(compoundsFlat);
|
|
1597
|
+
const singleKeywords = [...tf.entries()].filter(([stem]) => stem.length > 2).filter(([stem]) => {
|
|
1598
|
+
const original = stemToOriginal.get(stem) || stem;
|
|
1599
|
+
if (compoundWordSet.has(original) && !allTokens.includes(original)) {
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
return true;
|
|
1603
|
+
}).sort((a, b) => b[1] - a[1]).slice(0, maxKeywords - compoundTerms.length).map(([stem]) => stemToOriginal.get(stem) || stem);
|
|
1604
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1605
|
+
const keywords = [];
|
|
1606
|
+
for (const term of [...compoundTerms, ...singleKeywords]) {
|
|
1607
|
+
if (!seen.has(term)) {
|
|
1608
|
+
seen.add(term);
|
|
1609
|
+
keywords.push(term);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return keywords.slice(0, maxKeywords);
|
|
1183
1613
|
}
|
|
1184
1614
|
function matchJobDescription(resume, jobDescription, language = "en") {
|
|
1185
1615
|
const langData = getLanguageData(language);
|
|
@@ -1192,11 +1622,24 @@ function matchJobDescription(resume, jobDescription, language = "en") {
|
|
|
1192
1622
|
const matched = [];
|
|
1193
1623
|
const missing = [];
|
|
1194
1624
|
for (const keyword of jdKeywords) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1625
|
+
if (keyword.includes(" ")) {
|
|
1626
|
+
const parts = keyword.split(/\s+/);
|
|
1627
|
+
const allPartsMatch = parts.every((part) => {
|
|
1628
|
+
const stem = simpleStem(part, language);
|
|
1629
|
+
return resumeStems.has(stem) || resumeTokenSet.has(part.toLowerCase());
|
|
1630
|
+
});
|
|
1631
|
+
if (allPartsMatch) {
|
|
1632
|
+
matched.push(keyword);
|
|
1633
|
+
} else {
|
|
1634
|
+
missing.push(keyword);
|
|
1635
|
+
}
|
|
1198
1636
|
} else {
|
|
1199
|
-
|
|
1637
|
+
const stem = simpleStem(keyword, language);
|
|
1638
|
+
if (resumeStems.has(stem) || resumeTokenSet.has(keyword.toLowerCase())) {
|
|
1639
|
+
matched.push(keyword);
|
|
1640
|
+
} else {
|
|
1641
|
+
missing.push(keyword);
|
|
1642
|
+
}
|
|
1200
1643
|
}
|
|
1201
1644
|
}
|
|
1202
1645
|
const matchPercentage = jdKeywords.length > 0 ? Math.round(matched.length / jdKeywords.length * 100) : 0;
|