/** * chooseNarrative.ts * Generates 3 candidate positioning headlines and selects the strongest one. * The positioning angle drives the entire carousel's tone and framing. */ import type { CandidateProfile, FitAnalysis, HighlightCandidate, PositioningAngle, PositioningMode, TargetRoleProfile, } from '../schema/carouselSchema'; // --------------------------------------------------------------------------- // Headline template generators by mode // --------------------------------------------------------------------------- function buildRecruiterHeadlines( candidate: CandidateProfile, role: TargetRoleProfile, fit: FitAnalysis ): string[] { const title = role.title || 'Product Analyst'; const mostRecentRole = candidate.roles[0]?.title || 'Analyst'; const topAlignment = fit.strongestAlignments[0]?.area || ''; const secondAlignment = fit.strongestAlignments[1]?.area || ''; const domain = fit.strongestAlignments[0] ? role.domainClues[0] || '' : ''; const headlines = [ // Clarity + fit signal headline `${mostRecentRole} with proven track record in ${topAlignment}${secondAlignment ? ` and ${secondAlignment}` : ''} — ready to drive impact as ${title}`, // Impact-led headline — no raw metric injected, keep it clean `${title}-ready: ${topAlignment ? `${topAlignment} expertise` : 'cross-functional leader'} with measurable business outcomes`, // Domain-specific fit headline `${mostRecentRole} bringing ${domain ? `${domain} domain depth` : 'deep technical expertise'} and strong execution to ${title} role`, ]; return headlines.filter((h) => h.length > 10 && h.length < 160); } function buildHiringManagerHeadlines( candidate: CandidateProfile, role: TargetRoleProfile, fit: FitAnalysis ): string[] { const mostRecentRole = candidate.roles[0]?.title || 'Engineer'; const topTool = candidate.tools.slice(0, 2).join(' + ') || 'modern stack'; const topProblem = role.themes[0] || 'complex systems'; const ownershipVerbs = candidate.ownershipIndicators.slice(0, 2).join(' and '); const headlines = [ // Technical credibility headline `${mostRecentRole} who ${ownershipVerbs || 'builds and ships'} — experienced with ${topTool} at scale`, // Problem-solver framing `${topProblem ? `${capitalize(topProblem)}-focused` : 'Systems-minded'} ${mostRecentRole.toLowerCase()} — ${fit.bestFitStory.split(':')[1]?.trim() || 'strong execution track record'}`, // Technical stack + outcomes `${mostRecentRole} who solves ${topProblem || 'hard technical problems'} — ${topTool ? `${topTool} practitioner` : 'strong generalist'}`, ]; return headlines.filter((h) => h.length > 10 && h.length < 160); } function buildPersonalBrandHeadlines( candidate: CandidateProfile, role: TargetRoleProfile, fit: FitAnalysis ): string[] { const mostRecentRole = candidate.roles[0]?.title || 'Professional'; const uniqueSignal = candidate.metrics[0] || fit.bestAchievements[0]?.split(' ').slice(0, 5).join(' ') || ''; const topAlignment = fit.strongestAlignments[0]?.area || ''; const headlines = [ // Distinctive identity statement `The ${topAlignment ? `${topAlignment}-first` : 'results-first'} ${mostRecentRole.toLowerCase()} your company needs right now`, // Bold claim `Not just a ${mostRecentRole.toLowerCase()} — a ${role.themes[0] || 'high-impact'} operator who ships and scales`, // Memorable framing `${uniqueSignal ? `${uniqueSignal}` : 'Measurable results'} — that's what a great ${mostRecentRole.toLowerCase()} looks like`, ]; return headlines.filter((h) => h.length > 10 && h.length < 160); } // --------------------------------------------------------------------------- // Headline scoring // --------------------------------------------------------------------------- interface ScoredHeadline { headline: string; fitScore: number; clarityScore: number; distinctivenessScore: number; credibilityScore: number; compositeScore: number; } function scoreHeadline( headline: string, candidate: CandidateProfile, role: TargetRoleProfile, fit: FitAnalysis, mode: PositioningMode ): ScoredHeadline { const lower = headline.toLowerCase(); let fitScore = 5; let clarityScore = 5; let distinctivenessScore = 5; let credibilityScore = 5; // Fit score — does the headline mention role-relevant terms? const roleTerms = [...role.requiredSkills, ...role.themes, ...role.domainClues]; const roleTermMatches = roleTerms.filter((t) => lower.includes(t.toLowerCase())).length; fitScore = Math.min(5 + roleTermMatches * 2, 10); // Clarity score — penalize very long or vague headlines const wordCount = headline.split(/\s+/).length; if (wordCount <= 12) clarityScore = 9; else if (wordCount <= 18) clarityScore = 7; else if (wordCount <= 25) clarityScore = 5; else clarityScore = 3; // Penalize vague/generic phrases if (/results-oriented|passionate|self-starter|dynamic|innovative/i.test(headline)) { clarityScore -= 2; } // Distinctiveness — does it avoid pure job title repetition? if (lower.includes(role.title.toLowerCase()) && wordCount < 8) { distinctivenessScore = 3; // Just repeating the title } else if (/\b(who|that|with proven|ready to|bringing|not just)\b/i.test(headline)) { distinctivenessScore = 8; // Has a distinctive framing device } // Credibility — can we trace the claim to the resume? const candidateTerms = [ ...candidate.tools, ...candidate.ownershipIndicators, ...candidate.domains, ]; const credibleTerms = candidateTerms.filter((t) => lower.includes(t.toLowerCase())).length; credibilityScore = Math.min(4 + credibleTerms * 2, 10); // Mode-specific weights let composite: number; if (mode === 'recruiter') { composite = fitScore * 0.35 + clarityScore * 0.35 + distinctivenessScore * 0.15 + credibilityScore * 0.15; } else if (mode === 'hiring-manager') { composite = fitScore * 0.3 + clarityScore * 0.2 + distinctivenessScore * 0.2 + credibilityScore * 0.3; } else { // personal-brand composite = fitScore * 0.2 + clarityScore * 0.25 + distinctivenessScore * 0.35 + credibilityScore * 0.2; } return { headline, fitScore: Math.max(0, Math.min(10, Math.round(fitScore * 10) / 10)), clarityScore: Math.max(0, Math.min(10, Math.round(clarityScore * 10) / 10)), distinctivenessScore: Math.max(0, Math.min(10, Math.round(distinctivenessScore * 10) / 10)), credibilityScore: Math.max(0, Math.min(10, Math.round(credibilityScore * 10) / 10)), compositeScore: Math.max(0, Math.min(10, Math.round(composite * 10) / 10)), }; } // --------------------------------------------------------------------------- // Subheading generator // --------------------------------------------------------------------------- function generateSubheading( chosen: ScoredHeadline, candidate: CandidateProfile, role: TargetRoleProfile, fit: FitAnalysis, mode: PositioningMode ): string { const topAchievement = fit.bestAchievements[0] || ''; const topTool = candidate.tools[0] || ''; const topAlignment = fit.strongestAlignments[0]?.area || ''; if (mode === 'recruiter') { return `Open to ${role.title} opportunities — ${topAlignment ? `${topAlignment} specialist` : 'results-driven professional'} with${topAchievement ? ` a track record of measurable impact` : ` deep domain expertise`}.`; } else if (mode === 'hiring-manager') { return `${topTool ? `${capitalize(topTool)} practitioner` : 'Technical practitioner'} focused on building systems that scale — ${fit.strongestAlignments.length} strong alignment areas with ${role.title}.`; } else { return `Sharing the systems, strategies, and lessons that built ${fit.bestAchievements.length > 0 ? 'real results' : 'a real career'} — and why it matters for ${role.title}.`; } } // --------------------------------------------------------------------------- // Main chooser // --------------------------------------------------------------------------- /** * Generate 3 positioning headlines and return the best-scored PositioningAngle. */ export function choosePositioningAngle( candidate: CandidateProfile, role: TargetRoleProfile, fit: FitAnalysis, mode: PositioningMode ): PositioningAngle { // Generate mode-appropriate headlines let headlines: string[]; if (mode === 'recruiter') { headlines = buildRecruiterHeadlines(candidate, role, fit); } else if (mode === 'hiring-manager') { headlines = buildHiringManagerHeadlines(candidate, role, fit); } else { headlines = buildPersonalBrandHeadlines(candidate, role, fit); } // Fallback if generator returned fewer than 3 while (headlines.length < 3) { headlines.push(`${candidate.roles[0]?.title || 'Professional'} targeting ${role.title} — strong fit across ${fit.strongestAlignments.length} key areas`); } // Score all candidates const scored = headlines.map((h) => scoreHeadline(h, candidate, role, fit, mode)); scored.sort((a, b) => b.compositeScore - a.compositeScore); const best = scored[0]; const subheading = generateSubheading(best, candidate, role, fit, mode); const rationale = `Selected over ${scored.length - 1} alternatives. Fit: ${best.fitScore}/10, Clarity: ${best.clarityScore}/10, Distinctiveness: ${best.distinctivenessScore}/10, Credibility: ${best.credibilityScore}/10.`; return { headline: best.headline, subheading, rationale, mode, fitScore: best.fitScore, clarityScore: best.clarityScore, distinctivenessScore: best.distinctivenessScore, credibilityScore: best.credibilityScore, compositeScore: best.compositeScore, }; } // --------------------------------------------------------------------------- // Helper // --------------------------------------------------------------------------- function capitalize(str: string): string { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); }