@juicedresume/mcp 0.3.0 → 0.3.2
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.js +266 -102
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -215,6 +215,23 @@ var TEMPLATE_IDS = [
|
|
|
215
215
|
"oneline",
|
|
216
216
|
"rolodex"
|
|
217
217
|
];
|
|
218
|
+
var MULTI_COLUMN_TEMPLATES = [
|
|
219
|
+
"aurora",
|
|
220
|
+
"coral",
|
|
221
|
+
"prism",
|
|
222
|
+
"deedy",
|
|
223
|
+
"nova",
|
|
224
|
+
"axis",
|
|
225
|
+
"kintsugi",
|
|
226
|
+
"portrait",
|
|
227
|
+
"cameo",
|
|
228
|
+
"bauhaus",
|
|
229
|
+
"europass",
|
|
230
|
+
"rolodex"
|
|
231
|
+
];
|
|
232
|
+
function isMultiColumnTemplate(template) {
|
|
233
|
+
return !!template && MULTI_COLUMN_TEMPLATES.includes(template);
|
|
234
|
+
}
|
|
218
235
|
var Styling = z.object({
|
|
219
236
|
template: z.enum(TEMPLATE_IDS).default("aurora"),
|
|
220
237
|
colorMode: z.enum(["light", "dark"]).default("light"),
|
|
@@ -957,8 +974,67 @@ var SKILLS = [
|
|
|
957
974
|
];
|
|
958
975
|
var SKILL_INDEX = /* @__PURE__ */ new Map();
|
|
959
976
|
for (const s of SKILLS) SKILL_INDEX.set(s.name.toLowerCase(), s);
|
|
977
|
+
var SKILL_ALIASES = {
|
|
978
|
+
js: "JavaScript",
|
|
979
|
+
ecmascript: "JavaScript",
|
|
980
|
+
ts: "TypeScript",
|
|
981
|
+
"react.js": "React",
|
|
982
|
+
reactjs: "React",
|
|
983
|
+
"vue.js": "Vue",
|
|
984
|
+
vuejs: "Vue",
|
|
985
|
+
"next.js": "Next.js",
|
|
986
|
+
next: "Next.js",
|
|
987
|
+
nextjs: "Next.js",
|
|
988
|
+
"node.js": "Node.js",
|
|
989
|
+
node: "Node.js",
|
|
990
|
+
nodejs: "Node.js",
|
|
991
|
+
k8s: "Kubernetes",
|
|
992
|
+
postgres: "PostgreSQL",
|
|
993
|
+
psql: "PostgreSQL",
|
|
994
|
+
golang: "Go",
|
|
995
|
+
py: "Python",
|
|
996
|
+
"c sharp": "C#",
|
|
997
|
+
csharp: "C#",
|
|
998
|
+
"c plus plus": "C++",
|
|
999
|
+
cpp: "C++",
|
|
1000
|
+
tf: "Terraform",
|
|
1001
|
+
gha: "GitHub Actions",
|
|
1002
|
+
"rest api": "REST APIs",
|
|
1003
|
+
rest: "REST APIs",
|
|
1004
|
+
restful: "REST APIs",
|
|
1005
|
+
"objective c": "Objective-C",
|
|
1006
|
+
"spring-boot": "Spring Boot",
|
|
1007
|
+
"tailwind": "Tailwind CSS",
|
|
1008
|
+
tailwindcss: "Tailwind CSS",
|
|
1009
|
+
gcp: "GCP",
|
|
1010
|
+
"google cloud": "GCP",
|
|
1011
|
+
"google cloud platform": "GCP",
|
|
1012
|
+
aws: "AWS",
|
|
1013
|
+
"amazon web services": "AWS",
|
|
1014
|
+
sklearn: "scikit-learn",
|
|
1015
|
+
"scikit learn": "scikit-learn",
|
|
1016
|
+
hf: "Hugging Face",
|
|
1017
|
+
huggingface: "Hugging Face",
|
|
1018
|
+
pytorch: "PyTorch",
|
|
1019
|
+
tensorflow: "TensorFlow",
|
|
1020
|
+
"power-bi": "Power BI",
|
|
1021
|
+
powerbi: "Power BI"
|
|
1022
|
+
};
|
|
1023
|
+
function normalizeSkill(s) {
|
|
1024
|
+
const key = s.toLowerCase().trim();
|
|
1025
|
+
if (SKILL_INDEX.has(key)) return SKILL_INDEX.get(key).name;
|
|
1026
|
+
const stripped = key.replace(/\.js$/, "");
|
|
1027
|
+
if (SKILL_ALIASES[key]) return SKILL_ALIASES[key];
|
|
1028
|
+
if (SKILL_ALIASES[stripped]) return SKILL_ALIASES[stripped];
|
|
1029
|
+
if (SKILL_INDEX.has(stripped)) return SKILL_INDEX.get(stripped).name;
|
|
1030
|
+
return s.trim();
|
|
1031
|
+
}
|
|
960
1032
|
function lookupSkill(s) {
|
|
961
|
-
|
|
1033
|
+
const key = s.toLowerCase().trim();
|
|
1034
|
+
const direct = SKILL_INDEX.get(key);
|
|
1035
|
+
if (direct) return direct;
|
|
1036
|
+
const canonical = normalizeSkill(s).toLowerCase();
|
|
1037
|
+
return SKILL_INDEX.get(canonical);
|
|
962
1038
|
}
|
|
963
1039
|
function searchSkills(query, limit = 12) {
|
|
964
1040
|
const q = query.toLowerCase().trim();
|
|
@@ -1327,6 +1403,9 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
1327
1403
|
"day",
|
|
1328
1404
|
"days"
|
|
1329
1405
|
]);
|
|
1406
|
+
function escapeRe(s) {
|
|
1407
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1408
|
+
}
|
|
1330
1409
|
function tokens(s) {
|
|
1331
1410
|
return s.toLowerCase().replace(/[^a-z0-9+./# -]/g, " ").split(/\s+/).filter((w) => w && w.length > 1 && !STOPWORDS.has(w));
|
|
1332
1411
|
}
|
|
@@ -1351,7 +1430,20 @@ function resumeText(resume) {
|
|
|
1351
1430
|
}
|
|
1352
1431
|
function tailorToJob(resume, jdText) {
|
|
1353
1432
|
const jdToks = tokens(jdText);
|
|
1354
|
-
const
|
|
1433
|
+
const resumeFullText = resumeText(resume).toLowerCase();
|
|
1434
|
+
const resumeToks = new Set(tokens(resumeFullText));
|
|
1435
|
+
const resumeNorm = new Set([...resumeToks].map((t) => normalizeSkill(t).toLowerCase()));
|
|
1436
|
+
function resumeHas(keyword) {
|
|
1437
|
+
const k = keyword.toLowerCase();
|
|
1438
|
+
if (k.includes(" ")) {
|
|
1439
|
+
return new RegExp(`\\b${escapeRe(k)}\\b`).test(resumeFullText);
|
|
1440
|
+
}
|
|
1441
|
+
if (resumeToks.has(k)) return true;
|
|
1442
|
+
const canon = normalizeSkill(keyword).toLowerCase();
|
|
1443
|
+
if (resumeNorm.has(canon)) return true;
|
|
1444
|
+
if (canon.includes(" ") && new RegExp(`\\b${escapeRe(canon)}\\b`).test(resumeFullText)) return true;
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1355
1447
|
const jdGrams = /* @__PURE__ */ new Set([...jdToks, ...bigrams(jdToks)]);
|
|
1356
1448
|
const skillNames = new Map(SKILLS.map((s) => [s.name.toLowerCase(), s]));
|
|
1357
1449
|
const freq = /* @__PURE__ */ new Map();
|
|
@@ -1367,17 +1459,22 @@ function tailorToJob(resume, jdText) {
|
|
|
1367
1459
|
for (const [k, c] of sortedNonSkill) jdSkills.set(k, { importance: c });
|
|
1368
1460
|
const matched = [];
|
|
1369
1461
|
const missing = [];
|
|
1462
|
+
const kindMult = (kind) => kind === "hard" ? 2 : kind === "soft" ? 1.3 : 1;
|
|
1370
1463
|
for (const [k, meta] of jdSkills) {
|
|
1371
|
-
const
|
|
1372
|
-
if (
|
|
1373
|
-
matched.push({ keyword: k, kind: meta.kind, count:
|
|
1464
|
+
const w = meta.importance * kindMult(meta.kind);
|
|
1465
|
+
if (resumeHas(k)) {
|
|
1466
|
+
matched.push({ keyword: k, kind: meta.kind, count: w });
|
|
1374
1467
|
} else {
|
|
1375
|
-
missing.push({ keyword: k, kind: meta.kind, importance:
|
|
1468
|
+
missing.push({ keyword: k, kind: meta.kind, importance: w });
|
|
1376
1469
|
}
|
|
1377
1470
|
}
|
|
1378
1471
|
const matchedWeight = matched.reduce((a, m) => a + m.count, 0);
|
|
1379
1472
|
const totalWeight = matchedWeight + missing.reduce((a, m) => a + m.importance, 0);
|
|
1380
|
-
const score = totalWeight === 0 ? 0 : Math.round(matchedWeight / totalWeight * 100);
|
|
1473
|
+
const score = totalWeight === 0 ? 0 : Math.min(95, Math.round(matchedWeight / totalWeight * 100));
|
|
1474
|
+
const titleToks = tokens(resume.personal.title || "");
|
|
1475
|
+
const jdTokSet = new Set(jdToks);
|
|
1476
|
+
const titleHits = titleToks.filter((t) => jdTokSet.has(t)).length;
|
|
1477
|
+
const titleMatch = titleToks.length === 0 ? 0 : Math.round(titleHits / titleToks.length * 100);
|
|
1381
1478
|
missing.sort((a, b) => b.importance - a.importance);
|
|
1382
1479
|
matched.sort((a, b) => b.count - a.count);
|
|
1383
1480
|
return {
|
|
@@ -1385,7 +1482,8 @@ function tailorToJob(resume, jdText) {
|
|
|
1385
1482
|
matched,
|
|
1386
1483
|
missing,
|
|
1387
1484
|
jdSkills: jdSkills.size,
|
|
1388
|
-
resumeSkills: resumeToks.size
|
|
1485
|
+
resumeSkills: resumeToks.size,
|
|
1486
|
+
titleMatch
|
|
1389
1487
|
};
|
|
1390
1488
|
}
|
|
1391
1489
|
|
|
@@ -1393,7 +1491,7 @@ function tailorToJob(resume, jdText) {
|
|
|
1393
1491
|
function stripHtml(s) {
|
|
1394
1492
|
return (s || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
1395
1493
|
}
|
|
1396
|
-
function passSoft(id, label, score, message, suggestion) {
|
|
1494
|
+
function passSoft(id, label, score, message, suggestion, weight) {
|
|
1397
1495
|
const s = Math.max(0, Math.min(1, score));
|
|
1398
1496
|
return {
|
|
1399
1497
|
id,
|
|
@@ -1402,12 +1500,10 @@ function passSoft(id, label, score, message, suggestion) {
|
|
|
1402
1500
|
status: s >= 0.85 ? "pass" : s >= 0.55 ? "warn" : "fail",
|
|
1403
1501
|
message,
|
|
1404
1502
|
suggestion: suggestion ?? null,
|
|
1405
|
-
severity: "soft"
|
|
1503
|
+
severity: "soft",
|
|
1504
|
+
weight: weight ?? 1
|
|
1406
1505
|
};
|
|
1407
1506
|
}
|
|
1408
|
-
function hardCheck(id, label, score, message, suggestion) {
|
|
1409
|
-
return { ...passSoft(id, label, score, message, suggestion), severity: "hard" };
|
|
1410
|
-
}
|
|
1411
1507
|
function firstBulletStrongest(resume) {
|
|
1412
1508
|
const exp = resume.sections.find((s) => s.type === "experience");
|
|
1413
1509
|
if (!exp?.items?.length) return null;
|
|
@@ -1430,7 +1526,8 @@ function firstBulletStrongest(resume) {
|
|
|
1430
1526
|
"Lead bullet of recent role is strongest",
|
|
1431
1527
|
score,
|
|
1432
1528
|
topQuantified && topLength >= avgOtherLength ? "Top bullet of your most-recent role is quantified and substantive \u2014 recruiter eye lands here first." : "Your strongest bullet should be FIRST in your most-recent role (recruiters skim top-down).",
|
|
1433
|
-
"Reorder bullets in your most-recent experience so the most quantified / highest-impact bullet is first."
|
|
1529
|
+
"Reorder bullets in your most-recent experience so the most quantified / highest-impact bullet is first.",
|
|
1530
|
+
0.5
|
|
1434
1531
|
)
|
|
1435
1532
|
};
|
|
1436
1533
|
}
|
|
@@ -1498,20 +1595,6 @@ function dobAgePenalty(resume) {
|
|
|
1498
1595
|
)
|
|
1499
1596
|
};
|
|
1500
1597
|
}
|
|
1501
|
-
function singleColumnHard(resume) {
|
|
1502
|
-
const layout = resume.styling?.layout;
|
|
1503
|
-
const twoCol = resume.sections.some((s) => s.columns === 2) || layout === "two-column" || layout === "sidebar-left" || layout === "sidebar-right";
|
|
1504
|
-
return {
|
|
1505
|
-
dim: "ats",
|
|
1506
|
-
check: hardCheck(
|
|
1507
|
-
"AT12.single-column-hard",
|
|
1508
|
-
"Single-column layout (ATS-critical)",
|
|
1509
|
-
twoCol ? 0 : 1,
|
|
1510
|
-
twoCol ? "Two-column / sidebar layout detected \u2014 #1 cause of ATS parse failure. Workday concatenates columns into gibberish." : "Single-column layout \u2014 every ATS parses this cleanly.",
|
|
1511
|
-
"Switch to a single-column template (Jake, Harvard, Classic). Multi-column is the top score-destroyer in ATS systems."
|
|
1512
|
-
)
|
|
1513
|
-
};
|
|
1514
|
-
}
|
|
1515
1598
|
function threeLocationsRule(resume) {
|
|
1516
1599
|
const summary = resume.sections.find((s) => s.type === "summary");
|
|
1517
1600
|
const skills = resume.sections.find((s) => s.type === "skills");
|
|
@@ -1540,42 +1623,13 @@ function threeLocationsRule(resume) {
|
|
|
1540
1623
|
)
|
|
1541
1624
|
};
|
|
1542
1625
|
}
|
|
1543
|
-
function bulletLengthSweetSpot(resume) {
|
|
1544
|
-
const exp = resume.sections.find((s) => s.type === "experience");
|
|
1545
|
-
if (!exp?.items?.length) return null;
|
|
1546
|
-
const bullets = [];
|
|
1547
|
-
for (const it of exp.items) {
|
|
1548
|
-
const html = it.description || "";
|
|
1549
|
-
const lis = html.match(/<li[^>]*>([\s\S]*?)<\/li>/gi) || [];
|
|
1550
|
-
for (const li of lis) bullets.push(stripHtml(li));
|
|
1551
|
-
}
|
|
1552
|
-
if (bullets.length === 0) return null;
|
|
1553
|
-
let inRange = 0;
|
|
1554
|
-
for (const b of bullets) {
|
|
1555
|
-
const w = b.split(/\s+/).filter(Boolean).length;
|
|
1556
|
-
if (w >= 12 && w <= 25) inRange++;
|
|
1557
|
-
}
|
|
1558
|
-
const ratio = inRange / bullets.length;
|
|
1559
|
-
return {
|
|
1560
|
-
dim: "brevity",
|
|
1561
|
-
check: passSoft(
|
|
1562
|
-
"B5.sweet-spot",
|
|
1563
|
-
"Bullets in 12\u201325 word range",
|
|
1564
|
-
ratio,
|
|
1565
|
-
`${Math.round(ratio * 100)}% of your bullets fall in the 12\u201325 word sweet spot.`,
|
|
1566
|
-
"Aim for bullets between 12 and 25 words \u2014 short enough for the 6-second recruiter scan, long enough to convey impact + metric."
|
|
1567
|
-
)
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
1626
|
function additionalChecks(resume) {
|
|
1571
1627
|
return [
|
|
1572
1628
|
firstBulletStrongest(resume),
|
|
1573
1629
|
dateFormatConsistency(resume),
|
|
1574
1630
|
targetTitle(resume),
|
|
1575
1631
|
dobAgePenalty(resume),
|
|
1576
|
-
|
|
1577
|
-
threeLocationsRule(resume),
|
|
1578
|
-
bulletLengthSweetSpot(resume)
|
|
1632
|
+
threeLocationsRule(resume)
|
|
1579
1633
|
].filter((x) => x !== null);
|
|
1580
1634
|
}
|
|
1581
1635
|
|
|
@@ -2103,6 +2157,11 @@ function impactChecks(resume, b) {
|
|
|
2103
2157
|
const scaleSignals = E.filter((x) => SCALE_RE.test(x.text));
|
|
2104
2158
|
const timeBoundRe = /\b(within|in|over)\s+\d+\s*(weeks?|months?|quarters?|years?|days?)\b|\bq[1-4]\s?'?\d{2,4}\b/i;
|
|
2105
2159
|
const timeBound = E.filter((x) => timeBoundRe.test(x.text));
|
|
2160
|
+
const quantifiedEarly = quantified.filter((x) => {
|
|
2161
|
+
const head = x.text.split(/\s+/).slice(0, 8).join(" ");
|
|
2162
|
+
return QUANT_RE.test(head);
|
|
2163
|
+
});
|
|
2164
|
+
const earlyRatio = quantified.length ? quantifiedEarly.length / quantified.length : 0;
|
|
2106
2165
|
return [
|
|
2107
2166
|
mk(
|
|
2108
2167
|
"A1.quantification",
|
|
@@ -2110,15 +2169,20 @@ function impactChecks(resume, b) {
|
|
|
2110
2169
|
qRatio >= 0.6 ? 1 : qRatio >= 0.4 ? 0.7 : qRatio / 0.4,
|
|
2111
2170
|
`${quantified.length} of ${total} experience bullets include numbers, %, $, or counts (${Math.round(qRatio * 100)}%).`,
|
|
2112
2171
|
nonQ.slice(0, 6).map((x) => x.id),
|
|
2113
|
-
qRatio < 0.6 ? "Add concrete metrics: % change, $ amount, users, time saved." : void 0
|
|
2172
|
+
qRatio < 0.6 ? "Add concrete metrics: % change, $ amount, users, time saved." : void 0,
|
|
2173
|
+
void 0,
|
|
2174
|
+
3
|
|
2114
2175
|
),
|
|
2115
2176
|
mk(
|
|
2116
2177
|
"A2.metric-diversity",
|
|
2117
2178
|
"Variety of metric types",
|
|
2118
|
-
|
|
2179
|
+
// Bonus signal: absence is neutral (0.8), presence lifts toward 1.
|
|
2180
|
+
metricTypes.size === 0 ? 0.8 : Math.min(1, 0.8 + metricTypes.size * 0.1),
|
|
2119
2181
|
metricTypes.size === 0 ? "No metric types detected (%, $, time, counts, ratios)." : `Uses ${metricTypes.size} metric type(s): ${[...metricTypes].join(", ")}.`,
|
|
2120
2182
|
[],
|
|
2121
|
-
metricTypes.size < 3 ? "Mix metric flavors: a % gain, a $ saving, and a scale number tells a richer story than three %s." : void 0
|
|
2183
|
+
metricTypes.size < 3 ? "Mix metric flavors: a % gain, a $ saving, and a scale number tells a richer story than three %s." : void 0,
|
|
2184
|
+
void 0,
|
|
2185
|
+
0.5
|
|
2122
2186
|
),
|
|
2123
2187
|
mk(
|
|
2124
2188
|
"A3.xyz-bullets",
|
|
@@ -2126,7 +2190,9 @@ function impactChecks(resume, b) {
|
|
|
2126
2190
|
Math.min(1, xyz.length / Math.max(1, total * 0.4)),
|
|
2127
2191
|
`${xyz.length} of ${total} bullets read as "Accomplished X (measured by Y) by doing Z."`,
|
|
2128
2192
|
[],
|
|
2129
|
-
xyz.length / total < 0.4 ? "Reframe bullets as Action + Measurable Outcome + Method ('Cut LLM cost 35% by redesigning the prompt+cache layer')." : void 0
|
|
2193
|
+
xyz.length / total < 0.4 ? "Reframe bullets as Action + Measurable Outcome + Method ('Cut LLM cost 35% by redesigning the prompt+cache layer')." : void 0,
|
|
2194
|
+
void 0,
|
|
2195
|
+
2
|
|
2130
2196
|
),
|
|
2131
2197
|
mk(
|
|
2132
2198
|
"A4.responsibility",
|
|
@@ -2142,7 +2208,9 @@ function impactChecks(resume, b) {
|
|
|
2142
2208
|
strongStart.length / total,
|
|
2143
2209
|
`${strongStart.length} of ${total} bullets open with a strong verb (Led, Built, Drove, \u2026).`,
|
|
2144
2210
|
E.filter((x) => !STRONG_VERBS.has(norm(firstWord(x.text)))).slice(0, 5).map((x) => x.id),
|
|
2145
|
-
strongStart.length / total < 0.85 ? "Replace soft openers with strong past-tense verbs." : void 0
|
|
2211
|
+
strongStart.length / total < 0.85 ? "Replace soft openers with strong past-tense verbs." : void 0,
|
|
2212
|
+
void 0,
|
|
2213
|
+
2
|
|
2146
2214
|
),
|
|
2147
2215
|
mk(
|
|
2148
2216
|
"A6.weak-verbs",
|
|
@@ -2163,18 +2231,34 @@ function impactChecks(resume, b) {
|
|
|
2163
2231
|
mk(
|
|
2164
2232
|
"A8.scale-signals",
|
|
2165
2233
|
"Mentions team size, budget, or org scale",
|
|
2166
|
-
|
|
2167
|
-
scaleSignals.length === 0 ?
|
|
2234
|
+
// Bonus signal: absence is neutral, presence lifts the score.
|
|
2235
|
+
scaleSignals.length === 0 ? 0.8 : Math.min(1, 0.8 + scaleSignals.length * 0.1),
|
|
2236
|
+
scaleSignals.length === 0 ? "No team/budget/scale signals detected. A nice-to-have, not a deduction." : `${scaleSignals.length} bullet(s) reference team size, budget, or organisation scale.`,
|
|
2168
2237
|
[],
|
|
2169
|
-
scaleSignals.length === 0 ? "Add scale: 'Led a team of 6', 'Owned a $2M budget', '60+ engineers depend on this service.'" : void 0
|
|
2238
|
+
scaleSignals.length === 0 ? "Optional boost. Add scale: 'Led a team of 6', 'Owned a $2M budget', '60+ engineers depend on this service.'" : void 0,
|
|
2239
|
+
void 0,
|
|
2240
|
+
0.5
|
|
2170
2241
|
),
|
|
2171
2242
|
mk(
|
|
2172
2243
|
"A9.time-bound",
|
|
2173
2244
|
"Time-bound impact statements",
|
|
2174
|
-
|
|
2175
|
-
timeBound.length === 0 ?
|
|
2245
|
+
// Bonus signal: absence is neutral, presence lifts the score.
|
|
2246
|
+
timeBound.length === 0 ? 0.8 : Math.min(1, 0.8 + timeBound.length * 0.1),
|
|
2247
|
+
timeBound.length === 0 ? "No time-bound results yet. A nice-to-have, not a deduction." : `${timeBound.length} bullet(s) include a timeframe.`,
|
|
2248
|
+
[],
|
|
2249
|
+
timeBound.length === 0 ? "Optional boost. Anchor results in time: 'within 6 months', 'in 12 weeks', 'over Q3 2025'." : void 0,
|
|
2250
|
+
void 0,
|
|
2251
|
+
0.5
|
|
2252
|
+
),
|
|
2253
|
+
mk(
|
|
2254
|
+
"A10.metric-position",
|
|
2255
|
+
"Metrics appear early in the bullet",
|
|
2256
|
+
quantified.length === 0 ? 0.8 : earlyRatio,
|
|
2257
|
+
quantified.length === 0 ? "No quantified bullets yet to position." : `${quantifiedEarly.length} of ${quantified.length} quantified bullets lead with the number.`,
|
|
2176
2258
|
[],
|
|
2177
|
-
|
|
2259
|
+
quantified.length && earlyRatio < 0.6 ? "Move the number toward the front: 'Cut costs 35% by\u2026' scans faster than a metric buried at the end." : void 0,
|
|
2260
|
+
void 0,
|
|
2261
|
+
1
|
|
2178
2262
|
)
|
|
2179
2263
|
];
|
|
2180
2264
|
}
|
|
@@ -2206,7 +2290,7 @@ function brevityChecks(resume, b) {
|
|
|
2206
2290
|
return r.bullets > 5;
|
|
2207
2291
|
});
|
|
2208
2292
|
const pages = Math.max(1, Math.ceil(totalWords / 450));
|
|
2209
|
-
const targetPages = experienceYears(resume)
|
|
2293
|
+
const targetPages = experienceYears(resume) < 5 ? 1 : 2;
|
|
2210
2294
|
const lengthOK = pages <= targetPages;
|
|
2211
2295
|
return [
|
|
2212
2296
|
mk(
|
|
@@ -2239,7 +2323,7 @@ function brevityChecks(resume, b) {
|
|
|
2239
2323
|
mk(
|
|
2240
2324
|
"B4.page-length",
|
|
2241
2325
|
`Resume length fits ${targetPages} page${targetPages > 1 ? "s" : ""}`,
|
|
2242
|
-
lengthOK ? 1 : Math.max(0, 1 - (pages - targetPages) * 0.
|
|
2326
|
+
lengthOK ? 1 : Math.max(0, 1 - (pages - targetPages) * 0.35),
|
|
2243
2327
|
`Estimated ${pages} page${pages > 1 ? "s" : ""} (${totalWords} words total).`,
|
|
2244
2328
|
[],
|
|
2245
2329
|
!lengthOK ? `Cut ~${(pages - targetPages) * 450} words to fit ${targetPages} page${targetPages > 1 ? "s" : ""}.` : void 0
|
|
@@ -2337,7 +2421,9 @@ function styleChecks(resume, b) {
|
|
|
2337
2421
|
passive.length / total <= 0.15 ? 1 : 1 - (passive.length / total - 0.15) * 2,
|
|
2338
2422
|
`${passive.length} of ${total} bullets read as passive voice (${Math.round(passive.length / total * 100)}%).`,
|
|
2339
2423
|
passive.slice(0, 5).map((x) => x.id),
|
|
2340
|
-
passive.length / total > 0.15 ? "Flip subject/object: 'The project was led by me' \u2192 'Led the project.'" : void 0
|
|
2424
|
+
passive.length / total > 0.15 ? "Flip subject/object: 'The project was led by me' \u2192 'Led the project.'" : void 0,
|
|
2425
|
+
void 0,
|
|
2426
|
+
1.5
|
|
2341
2427
|
),
|
|
2342
2428
|
mk(
|
|
2343
2429
|
"C3.tense-consistency",
|
|
@@ -2377,7 +2463,9 @@ function styleChecks(resume, b) {
|
|
|
2377
2463
|
periodConsistent ? 1 : 0.5,
|
|
2378
2464
|
periodConsistent ? "All bullets agree on whether they end with a period." : `Mixed: ${endsWithPeriod} end with a period, ${endsWithout} don't.`,
|
|
2379
2465
|
[],
|
|
2380
|
-
!periodConsistent ? "Pick one rule for the whole resume." : void 0
|
|
2466
|
+
!periodConsistent ? "Pick one rule for the whole resume." : void 0,
|
|
2467
|
+
void 0,
|
|
2468
|
+
0.4
|
|
2381
2469
|
),
|
|
2382
2470
|
mk(
|
|
2383
2471
|
"C8.punctuation-chars",
|
|
@@ -2385,7 +2473,9 @@ function styleChecks(resume, b) {
|
|
|
2385
2473
|
punctConsistent ? 1 : 0.5,
|
|
2386
2474
|
punctConsistent ? "Punctuation glyphs are consistent." : "Mixes smart quotes/em-dashes with ASCII equivalents.",
|
|
2387
2475
|
[],
|
|
2388
|
-
!punctConsistent ? 'Pick either smart quotes (\u201C\u201D) or straight quotes (") consistently.' : void 0
|
|
2476
|
+
!punctConsistent ? 'Pick either smart quotes (\u201C\u201D) or straight quotes (") consistently.' : void 0,
|
|
2477
|
+
void 0,
|
|
2478
|
+
0.4
|
|
2389
2479
|
),
|
|
2390
2480
|
mk(
|
|
2391
2481
|
"C9.capitalization",
|
|
@@ -2393,7 +2483,9 @@ function styleChecks(resume, b) {
|
|
|
2393
2483
|
capConsistent ? 1 : 0.5,
|
|
2394
2484
|
capConsistent ? "Every bullet starts with the same case." : `${startsCap} of ${total} bullets start with a capital letter.`,
|
|
2395
2485
|
[],
|
|
2396
|
-
!capConsistent ? "Capitalize the first letter of every bullet." : void 0
|
|
2486
|
+
!capConsistent ? "Capitalize the first letter of every bullet." : void 0,
|
|
2487
|
+
void 0,
|
|
2488
|
+
0.4
|
|
2397
2489
|
),
|
|
2398
2490
|
mk(
|
|
2399
2491
|
"C10.acronym-density",
|
|
@@ -2448,6 +2540,20 @@ function structureChecks(resume, b) {
|
|
|
2448
2540
|
}
|
|
2449
2541
|
const expRev = expSec ? rev(expSec.items || []) : true;
|
|
2450
2542
|
const eduRev = eduSec ? rev(eduSec.items || []) : true;
|
|
2543
|
+
const nowMonths = (/* @__PURE__ */ new Date()).getFullYear() * 12 + ((/* @__PURE__ */ new Date()).getMonth() + 1);
|
|
2544
|
+
let maxGapMonths = 0;
|
|
2545
|
+
if (expSec) {
|
|
2546
|
+
const ranges = (expSec.items || []).map((it) => ({
|
|
2547
|
+
start: parseMMYYYY(it.startDate || ""),
|
|
2548
|
+
end: it.current ? nowMonths : parseMMYYYY(it.endDate || it.startDate || "")
|
|
2549
|
+
})).filter((r) => r.start && r.end);
|
|
2550
|
+
ranges.sort((a, b2) => b2.start - a.start);
|
|
2551
|
+
for (let i = 0; i < ranges.length - 1; i++) {
|
|
2552
|
+
const gap = ranges[i].start - ranges[i + 1].end;
|
|
2553
|
+
if (gap > maxGapMonths) maxGapMonths = gap;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
const gapOK = maxGapMonths <= 6;
|
|
2451
2557
|
function isCanonical(type, title) {
|
|
2452
2558
|
const t = title.toLowerCase().trim();
|
|
2453
2559
|
const list = ATS_SECTION_LABELS[type] || [];
|
|
@@ -2517,6 +2623,16 @@ function structureChecks(resume, b) {
|
|
|
2517
2623
|
expRev && eduRev ? "Both sections are reverse-chronological." : `${!expRev ? "Experience" : ""}${!expRev && !eduRev ? " & " : ""}${!eduRev ? "Education" : ""} out of order.`,
|
|
2518
2624
|
[],
|
|
2519
2625
|
!expRev || !eduRev ? "List most recent first." : void 0
|
|
2626
|
+
),
|
|
2627
|
+
mk(
|
|
2628
|
+
"D9.employment-gaps",
|
|
2629
|
+
"No large unexplained employment gaps",
|
|
2630
|
+
gapOK ? 1 : 0.7,
|
|
2631
|
+
gapOK ? "No employment gap over 6 months between roles." : `A gap of about ${maxGapMonths} months sits between two roles. Some employers screen for gaps over 6 months.`,
|
|
2632
|
+
[],
|
|
2633
|
+
gapOK ? void 0 : "A gap over 6 months is fine, but consider a one-line note (contract work, study, caregiving) or a brief bridging entry so it does not read as unexplained.",
|
|
2634
|
+
void 0,
|
|
2635
|
+
0.5
|
|
2520
2636
|
)
|
|
2521
2637
|
];
|
|
2522
2638
|
}
|
|
@@ -2524,7 +2640,7 @@ function atsChecks(resume, b) {
|
|
|
2524
2640
|
const s = resume.styling;
|
|
2525
2641
|
const p = resume.personal;
|
|
2526
2642
|
const allText = b.bullets.map((x) => x.text).join(" ");
|
|
2527
|
-
const isSingleCol = s.
|
|
2643
|
+
const isSingleCol = !isMultiColumnTemplate(s.template);
|
|
2528
2644
|
const isSafeFont = SAFE_FONTS.has(s.fontHeading) && SAFE_FONTS.has(s.fontBody);
|
|
2529
2645
|
const fontSizeOK = s.fontSize >= 9.5 && s.fontSize <= 12;
|
|
2530
2646
|
const safeBullet = SAFE_BULLET_GLYPHS.has(s.bulletGlyph === "disc" ? "\u2022" : s.bulletGlyph === "dash" ? "-" : "");
|
|
@@ -2532,6 +2648,9 @@ function atsChecks(resume, b) {
|
|
|
2532
2648
|
const phoneOK = !p.phone || /(\+?\d[\d\s().-]{6,})/.test(p.phone);
|
|
2533
2649
|
const hasLinkedIn = !!(p.linkedin && /linkedin\.com\/in\//i.test(p.linkedin));
|
|
2534
2650
|
const locationOK = !!p.location && /,/.test(p.location);
|
|
2651
|
+
const nameOK = !!(p.fullName && p.fullName.trim().length >= 2);
|
|
2652
|
+
const hasPhone = !!(p.phone && /(\+?\d[\d\s().-]{6,})/.test(p.phone));
|
|
2653
|
+
const contactParseable = nameOK && (emailOK || hasPhone);
|
|
2535
2654
|
const allDates = [];
|
|
2536
2655
|
for (const sec of resume.sections) for (const it of sec.items || []) {
|
|
2537
2656
|
if (it.startDate) allDates.push(it.startDate);
|
|
@@ -2559,19 +2678,29 @@ function atsChecks(resume, b) {
|
|
|
2559
2678
|
return [
|
|
2560
2679
|
mk(
|
|
2561
2680
|
"E1.single-column",
|
|
2562
|
-
"Single-column layout (ATS-
|
|
2563
|
-
|
|
2564
|
-
|
|
2681
|
+
"Single-column layout (ATS-friendly)",
|
|
2682
|
+
// Proportional soft penalty, not an auto-fail. Modern parsers handle
|
|
2683
|
+
// columns (Textkernel reports ~90% correct column extraction vs ~97%
|
|
2684
|
+
// single-column), but ~1 in 10 still scrambles reading order and columns
|
|
2685
|
+
// cost human skimmability. A single, evidence-scaled ding — the overall
|
|
2686
|
+
// score also takes a small proportional multi-column haircut (see below).
|
|
2687
|
+
isSingleCol ? 1 : 0.55,
|
|
2688
|
+
isSingleCol ? "Single-column layout \u2014 every ATS parses this cleanly." : "Multi-column template. Modern parsers usually handle columns, but a minority still scramble reading order, and columns are harder for a recruiter to skim. Single-column is the zero-risk choice.",
|
|
2565
2689
|
[],
|
|
2566
|
-
!isSingleCol ? "
|
|
2567
|
-
|
|
2690
|
+
!isSingleCol ? "For maximum ATS safety, switch to a single-column template (Jake, Harvard, Classic) in the Templates tab. Not required, but it removes all parse risk." : void 0,
|
|
2691
|
+
"soft"
|
|
2568
2692
|
),
|
|
2569
2693
|
mk(
|
|
2570
|
-
"E2.contact-
|
|
2571
|
-
"
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2694
|
+
"E2.contact-parseable",
|
|
2695
|
+
"Name + contact details are parseable",
|
|
2696
|
+
// Hard dependency: parsers build the candidate record from a name plus at
|
|
2697
|
+
// least one of email/phone (Daxtra: without one of these the parse fails).
|
|
2698
|
+
contactParseable ? 1 : 0,
|
|
2699
|
+
contactParseable ? "Full name and at least one contact channel (email or phone) are present." : !nameOK ? "No full name detected. Parsers need a name plus an email or phone to create a candidate record." : "No email or phone detected. Most parsers reject a record that has neither.",
|
|
2700
|
+
[],
|
|
2701
|
+
!contactParseable ? "Add your full name and at least one of email or phone. Without these, many ATS drop the whole resume." : void 0,
|
|
2702
|
+
contactParseable ? "soft" : "hard",
|
|
2703
|
+
2
|
|
2575
2704
|
),
|
|
2576
2705
|
mk(
|
|
2577
2706
|
"E3.email-valid",
|
|
@@ -2604,7 +2733,9 @@ function atsChecks(resume, b) {
|
|
|
2604
2733
|
hasLinkedIn ? 1 : 0,
|
|
2605
2734
|
hasLinkedIn ? "LinkedIn URL detected." : "LinkedIn URL missing or non-canonical.",
|
|
2606
2735
|
[],
|
|
2607
|
-
!hasLinkedIn ? "Add a linkedin.com/in/your-handle URL." : void 0
|
|
2736
|
+
!hasLinkedIn ? "Add a linkedin.com/in/your-handle URL. Resumes with one see materially higher recruiter response." : void 0,
|
|
2737
|
+
void 0,
|
|
2738
|
+
1.5
|
|
2608
2739
|
),
|
|
2609
2740
|
mk(
|
|
2610
2741
|
"E7.date-format",
|
|
@@ -2628,7 +2759,9 @@ function atsChecks(resume, b) {
|
|
|
2628
2759
|
safeBullet ? 1 : 0.6,
|
|
2629
2760
|
safeBullet ? "Bullet glyph is ATS-safe." : `Bullet glyph '${s.bulletGlyph}' may not survive every ATS parser.`,
|
|
2630
2761
|
[],
|
|
2631
|
-
!safeBullet ? "Switch bullet glyph to disc, dash, or square (Customize \u2192 Layout \u2192 bullet glyph)." : void 0
|
|
2762
|
+
!safeBullet ? "Switch bullet glyph to disc, dash, or square (Customize \u2192 Layout \u2192 bullet glyph)." : void 0,
|
|
2763
|
+
void 0,
|
|
2764
|
+
0.5
|
|
2632
2765
|
),
|
|
2633
2766
|
mk(
|
|
2634
2767
|
"E10.font-safe",
|
|
@@ -2636,7 +2769,9 @@ function atsChecks(resume, b) {
|
|
|
2636
2769
|
isSafeFont ? 1 : 0.5,
|
|
2637
2770
|
isSafeFont ? "Fonts are ATS-safe." : `Heading '${s.fontHeading}' or body '${s.fontBody}' may not be embedded by some PDF renderers.`,
|
|
2638
2771
|
[],
|
|
2639
|
-
!isSafeFont ? "Use Arial, Calibri, Helvetica, Garamond, or one of the safe defaults." : void 0
|
|
2772
|
+
!isSafeFont ? "Use Arial, Calibri, Helvetica, Garamond, or one of the safe defaults." : void 0,
|
|
2773
|
+
void 0,
|
|
2774
|
+
0.5
|
|
2640
2775
|
),
|
|
2641
2776
|
mk(
|
|
2642
2777
|
"E11.font-size",
|
|
@@ -2644,7 +2779,9 @@ function atsChecks(resume, b) {
|
|
|
2644
2779
|
fontSizeOK ? 1 : 0.5,
|
|
2645
2780
|
fontSizeOK ? `Font size ${s.fontSize}pt is in range.` : `Font size ${s.fontSize}pt is outside the 10\u201312pt range.`,
|
|
2646
2781
|
[],
|
|
2647
|
-
!fontSizeOK ? "Set body size to 10\u201312pt." : void 0
|
|
2782
|
+
!fontSizeOK ? "Set body size to 10\u201312pt." : void 0,
|
|
2783
|
+
void 0,
|
|
2784
|
+
0.5
|
|
2648
2785
|
),
|
|
2649
2786
|
mk(
|
|
2650
2787
|
"E12.no-killer-chars",
|
|
@@ -2669,7 +2806,7 @@ function skillsChecks(resume, b) {
|
|
|
2669
2806
|
if (t.length < 3) continue;
|
|
2670
2807
|
counts.set(t, (counts.get(t) || 0) + 1);
|
|
2671
2808
|
}
|
|
2672
|
-
const stuffed = [...counts.entries()].filter(([k, v]) => v >
|
|
2809
|
+
const stuffed = [...counts.entries()].filter(([k, v]) => v > 8 && /^[a-z]+$/.test(k) && !["with", "from", "into", "over", "under", "across", "by", "to", "for", "the", "and", "of", "on", "in", "a", "an", "that", "this", "was", "were", "are", "have", "has"].includes(k));
|
|
2673
2810
|
const allText = b.bullets.map((x) => x.text).join(" ");
|
|
2674
2811
|
const acronyms = [...new Set(Array.from(allText.matchAll(/\b([A-Z]{2,5})\b/g)).map((m) => m[1]))].slice(0, 5);
|
|
2675
2812
|
const expanded = acronyms.filter((a) => new RegExp(`\\b${a}\\s*\\(`).test(allText));
|
|
@@ -2712,7 +2849,9 @@ function skillsChecks(resume, b) {
|
|
|
2712
2849
|
acronyms.length === 0 ? 1 : expanded.length / acronyms.length,
|
|
2713
2850
|
acronyms.length === 0 ? "No acronyms detected." : `${expanded.length} of ${acronyms.length} top acronyms have an inline expansion.`,
|
|
2714
2851
|
[],
|
|
2715
|
-
acronyms.length > 0 && expanded.length < acronyms.length ? `Expand at least once: e.g. '${acronyms[0]} (\u2026)'.` : void 0
|
|
2852
|
+
acronyms.length > 0 && expanded.length < acronyms.length ? `Expand at least once: e.g. '${acronyms[0]} (\u2026)'. Recruiter search is exact-match, so 'ML (Machine Learning)' catches both queries.` : void 0,
|
|
2853
|
+
void 0,
|
|
2854
|
+
0.5
|
|
2716
2855
|
)
|
|
2717
2856
|
];
|
|
2718
2857
|
}
|
|
@@ -2727,6 +2866,10 @@ function polishChecks(resume, b) {
|
|
|
2727
2866
|
const linkedinGeneric = p.linkedin && /\/in\/[a-z0-9]{8,}-[a-z0-9]{4,}/i.test(p.linkedin) && !/[a-zA-Z]/.test(p.linkedin.split("/in/")[1]?.split(/[-?#]/)[0] || "");
|
|
2728
2867
|
const hasCustomWebsite = !!p.website && !/(linkedin\.com|github\.com|facebook\.com|twitter\.com|x\.com)/i.test(p.website);
|
|
2729
2868
|
const hasHeadline = !!p.title;
|
|
2869
|
+
const rawHtml = resume.sections.flatMap((sec) => (sec.items || []).flatMap((it) => [it.description, it.body].filter(Boolean))).join(" ");
|
|
2870
|
+
const HIDDEN_STYLE_RE = /color\s*:\s*(#fff(fff)?|#ffffff|white|rgba?\(\s*255\s*,\s*255\s*,\s*255)|font-size\s*:\s*0|opacity\s*:\s*0|visibility\s*:\s*hidden|display\s*:\s*none/i;
|
|
2871
|
+
const PROMPT_INJECT_RE = /\b(ignore (all )?(the )?previous|as an ai|you are an ai|rate (this|the|me|my) (resume|candidate|application) (highly|as|a perfect|10)|you must (rate|score|recommend)|system prompt|disregard (the )?instructions|hiring manager must)\b/i;
|
|
2872
|
+
const hasHiddenText = HIDDEN_STYLE_RE.test(rawHtml) || PROMPT_INJECT_RE.test(stripHtml2(rawHtml).toLowerCase());
|
|
2730
2873
|
return [
|
|
2731
2874
|
mk(
|
|
2732
2875
|
"G1.no-placeholders",
|
|
@@ -2771,14 +2914,26 @@ function polishChecks(resume, b) {
|
|
|
2771
2914
|
mk(
|
|
2772
2915
|
"G6.custom-portfolio",
|
|
2773
2916
|
"Custom portfolio or personal site (nice-to-have)",
|
|
2774
|
-
hasCustomWebsite ? 1 : 0.
|
|
2775
|
-
hasCustomWebsite ? "Custom site present." : "No personal site listed.",
|
|
2917
|
+
hasCustomWebsite ? 1 : 0.9,
|
|
2918
|
+
hasCustomWebsite ? "Custom site present." : "No personal site listed. Optional, not a deduction.",
|
|
2919
|
+
[],
|
|
2920
|
+
hasCustomWebsite ? void 0 : "A custom domain (yourname.dev) lifts a senior resume \u2014 optional.",
|
|
2921
|
+
void 0,
|
|
2922
|
+
0.5
|
|
2923
|
+
),
|
|
2924
|
+
mk(
|
|
2925
|
+
"G8.hidden-text",
|
|
2926
|
+
"No hidden text or keyword tricks",
|
|
2927
|
+
hasHiddenText ? 0 : 1,
|
|
2928
|
+
hasHiddenText ? "Hidden or invisible text (white-on-white, zero-size) or an AI-instruction string was detected. ATS convert your file to plain text, so these become visible and read as gaming the system." : "No hidden text or prompt-injection tricks detected.",
|
|
2776
2929
|
[],
|
|
2777
|
-
|
|
2930
|
+
hasHiddenText ? "Remove any white/invisible text, zero-size fonts, or 'rate me highly'-style instructions. They backfire once the ATS strips formatting." : void 0,
|
|
2931
|
+
void 0,
|
|
2932
|
+
2
|
|
2778
2933
|
)
|
|
2779
2934
|
];
|
|
2780
2935
|
}
|
|
2781
|
-
function mk(id, label, score, message, evidence, suggestion, severity) {
|
|
2936
|
+
function mk(id, label, score, message, evidence, suggestion, severity, weight) {
|
|
2782
2937
|
const s = Math.max(0, Math.min(1, score));
|
|
2783
2938
|
return {
|
|
2784
2939
|
id,
|
|
@@ -2788,7 +2943,8 @@ function mk(id, label, score, message, evidence, suggestion, severity) {
|
|
|
2788
2943
|
message,
|
|
2789
2944
|
evidence: evidence ?? [],
|
|
2790
2945
|
suggestion: suggestion ?? null,
|
|
2791
|
-
severity: severity || "soft"
|
|
2946
|
+
severity: severity || "soft",
|
|
2947
|
+
weight: weight ?? 1
|
|
2792
2948
|
};
|
|
2793
2949
|
}
|
|
2794
2950
|
function parseMMYYYY(d) {
|
|
@@ -2818,9 +2974,14 @@ function experienceYears(resume) {
|
|
|
2818
2974
|
}
|
|
2819
2975
|
function aggregate(checks, weight) {
|
|
2820
2976
|
if (!checks.length) return { score: 0, weighted: 0 };
|
|
2821
|
-
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2977
|
+
let wsum = 0, wtot = 0;
|
|
2978
|
+
for (const c of checks) {
|
|
2979
|
+
const w = c.weight ?? 1;
|
|
2980
|
+
wsum += c.score * w;
|
|
2981
|
+
wtot += w;
|
|
2982
|
+
}
|
|
2983
|
+
const mean = wtot ? wsum / wtot : 0;
|
|
2984
|
+
return { score: Math.round(mean * 100), weighted: mean * weight };
|
|
2824
2985
|
}
|
|
2825
2986
|
var WEIGHTS = {
|
|
2826
2987
|
impact: 22,
|
|
@@ -2870,12 +3031,15 @@ function scoreResume(resume) {
|
|
|
2870
3031
|
};
|
|
2871
3032
|
});
|
|
2872
3033
|
let overall = Math.round(weightedSum / totalWeight * 100);
|
|
3034
|
+
if (isMultiColumnTemplate(resume.styling.template)) {
|
|
3035
|
+
overall = Math.round(overall * 0.93);
|
|
3036
|
+
}
|
|
2873
3037
|
const hardFail = dimensions.some((d) => d.checks.some((c) => c.severity === "hard" && c.status === "fail"));
|
|
2874
3038
|
if (hardFail) overall = Math.min(overall, 60);
|
|
2875
3039
|
const sortedDims = [...dimensions].sort((a, b2) => a.score - b2.score);
|
|
2876
3040
|
const worst = sortedDims[0];
|
|
2877
3041
|
const best = sortedDims[sortedDims.length - 1];
|
|
2878
|
-
const allFails = dimensions.flatMap((d) => d.checks.map((c) => ({ ...c, dim: d.dimension, dimLabel: d.label, weight: d.weight }))).filter((c) => c.suggestion && c.status !== "pass").sort((a, b2) =>
|
|
3042
|
+
const allFails = dimensions.flatMap((d) => d.checks.map((c) => ({ ...c, dim: d.dimension, dimLabel: d.label, weight: d.weight }))).filter((c) => c.suggestion && c.status !== "pass").sort((a, b2) => (1 - b2.score) * b2.weight - (1 - a.score) * a.weight);
|
|
2879
3043
|
const priorities = allFails.slice(0, 5);
|
|
2880
3044
|
return {
|
|
2881
3045
|
overall,
|
|
@@ -3325,7 +3489,7 @@ function runOnce(cmd, args, cwd) {
|
|
|
3325
3489
|
import { promises as fs3 } from "fs";
|
|
3326
3490
|
import path3 from "path";
|
|
3327
3491
|
var server = new Server(
|
|
3328
|
-
{ name: "juiced-resume", version: "0.3.
|
|
3492
|
+
{ name: "juiced-resume", version: "0.3.1" },
|
|
3329
3493
|
{ capabilities: { tools: {} } }
|
|
3330
3494
|
);
|
|
3331
3495
|
var idArg = z2.object({ id: z2.string() });
|
package/package.json
CHANGED