/* ats.jsx — heuristic ATS / quality scorer. Returns score + category bars + checklist. */ const ACTION_VERBS = ["led","built","designed","launched","shipped","drove","grew","increased","reduced","cut","owned","created","developed","managed","improved","delivered","partnered","introduced","scaled","ran","raised","streamlined","architected","mentored","spearheaded"]; function hasNumber(s) { return /\d/.test(s || ""); } function computeATS(data) { const b = data.basics || {}; const exp = data.experience || []; const edu = data.education || []; const skills = data.skills || []; const allBullets = exp.flatMap(e => (e.bullets || []).filter(Boolean)); const skillCount = skills.reduce((n, g) => n + (g.items || []).length, 0); // --- category scores (0..1) --- const contactFields = ["name","email","phone","location"].filter(k => (b[k] || "").trim()); const contact = contactFields.length / 4; const summaryWords = (data.summary || "").trim().split(/\s+/).filter(Boolean).length; const summaryScore = data.summary ? Math.min(1, summaryWords / 40) : 0; const expScore = exp.length === 0 ? 0 : Math.min(1, (Math.min(exp.length, 3) / 3) * 0.5 + (Math.min(allBullets.length, 6) / 6) * 0.5); const quantified = allBullets.filter(hasNumber).length; const quantScore = allBullets.length ? Math.min(1, quantified / Math.max(3, allBullets.length * 0.5)) : 0; const verbBullets = allBullets.filter(x => ACTION_VERBS.includes((x.trim().split(/\s+/)[0] || "").toLowerCase())).length; const verbScore = allBullets.length ? verbBullets / allBullets.length : 0; const skillScore = Math.min(1, skillCount / 10); const eduScore = edu.length ? 1 : 0; const categories = [ { key: "Contact info", score: contact, weight: 12 }, { key: "Professional summary", score: summaryScore, weight: 14 }, { key: "Work experience", score: expScore, weight: 24 }, { key: "Quantified impact", score: quantScore, weight: 20 }, { key: "Action verbs", score: verbScore, weight: 12 }, { key: "Skills & keywords", score: skillScore, weight: 12 }, { key: "Education", score: eduScore, weight: 6 }, ]; const score = Math.round(categories.reduce((s, c) => s + c.score * c.weight, 0)); // --- checklist --- const checks = [ { state: contact === 1 ? "ok" : "wn", title: contact === 1 ? "Complete contact details" : "Add missing contact details", desc: contact === 1 ? "Name, email, phone and location are all present." : "Recruiters and parsers expect name, email, phone, and location." }, { state: b.email && /@/.test(b.email) ? "ok" : "no", title: "Parseable email address", desc: "A standard email format helps the ATS extract your contact card." }, { state: data.summary ? (summaryWords >= 30 ? "ok" : "wn") : "no", title: summaryWords >= 30 ? "Strong summary length" : "Tighten or add a summary", desc: "Aim for 30–60 words framing your value and seniority." }, { state: quantified >= 3 ? "ok" : "wn", title: quantified >= 3 ? `${quantified} quantified achievements` : "Quantify more achievements", desc: "Bullets with metrics (%, $, ×, counts) read as higher-impact to humans and ATS keyword scans." }, { state: verbScore >= 0.7 ? "ok" : "wn", title: verbScore >= 0.7 ? "Bullets lead with action verbs" : "Start bullets with action verbs", desc: "Lead with verbs like Led, Built, Shipped — not “Responsible for”." }, { state: skillCount >= 8 ? "ok" : "wn", title: skillCount >= 8 ? `${skillCount} skills / keywords listed` : "Add more relevant keywords", desc: "Mirror the language of the job description in your Skills section." }, { state: "ok", title: "ATS-safe single-column layout", desc: "No tables, text boxes, or multi-column blocks — parsers read top to bottom." }, { state: "ok", title: "Standard, embeddable fonts", desc: "Uses common typefaces so text extracts cleanly on any system." }, ]; return { score, categories, checks }; } window.computeATS = computeATS;