// Engine 6 — KROW Score Engine (formula-based, auto-recalculated) // Reliability = Attendance×.30 + Performance×.25 + Education×.10 + ClientReviews×.15 + SupervisorReviews×.10 + Growth×.05 + Experience×.05 // KROW Score = Reliability×.40 + AI Interview×.25 + Education×.15 + Growth×.10 + Experience×.10 const clamp = (n) => Math.max(0, Math.min(100, n)); export function computeKrowScore(profile) { const attendance = clamp(profile.attendance_score ?? 100); const performance = clamp(profile.performance_score ?? 0); const education = clamp( (profile.completed_courses?.length || 0) * 12 + (profile.earned_badges?.length || 0) * 8 ); const clientReviews = profile.client_rating ? clamp((profile.client_rating / 5) * 100) : 0; const supervisorReviews = profile.supervisor_rating ? clamp((profile.supervisor_rating / 5) * 100) : 0; const growth = clamp((profile.xp || 0) / 10); const experience = clamp( (profile.experience_years || 0) * 10 + (profile.experience?.length || 0) * 5 ); const reliability = Math.round( attendance * 0.30 + performance * 0.25 + education * 0.10 + clientReviews * 0.15 + supervisorReviews * 0.10 + growth * 0.05 + experience * 0.05 ); const krow_score = Math.round( reliability * 0.40 + clamp(profile.ai_interview_score || 0) * 0.25 + education * 0.15 + growth * 0.10 + experience * 0.10 ); return { krow_score, reliability, breakdown: { attendance, performance, education, clientReviews, supervisorReviews, growth, experience }, }; } const CORE_FIELDS = ['full_name', 'email', 'phone', 'address', 'current_position', 'desired_position', 'career_goals', 'transportation', 'resume_url']; export function computeProfileCompletion(profile) { let filled = 0; for (const f of CORE_FIELDS) if (profile[f]) filled++; if (profile.languages?.length) filled++; if (profile.availability?.length) filled++; if (profile.skills?.length) filled++; if (profile.experience?.length) filled++; if (profile.completed_courses?.length) filled++; if (profile.earned_badges?.length) filled++; if (profile.ai_interview_score > 0) filled++; const total = CORE_FIELDS.length + 5; return Math.round((filled / total) * 100); } /** Returns only the recomputed fields — merge into an update patch */ export function recalcProfilePatch(profile) { const { krow_score, reliability, breakdown } = computeKrowScore(profile); return { krow_score, reliability_score: reliability, profile_completion: computeProfileCompletion(profile), score_breakdown: breakdown, }; } // Engine 11 (light) — recommendation helpers export function recommendNextCourse(profile, learningPaths = [], courses = []) { const p = profile || {}; const completedIds = new Set((p.completed_courses || []).map(c => c.course_id)); const desired = (p.desired_position || '').toLowerCase(); const path = (desired && learningPaths.find(p => p.name?.toLowerCase().includes(desired) || p.target_role?.toLowerCase().includes(desired) )) || learningPaths[0]; if (path?.steps?.length) { const nextStep = path.steps.find(s => s.course_id && !completedIds.has(s.course_id)); const course = courses.find(c => c.id === nextStep?.course_id); if (course) return { course, path }; } const uncompleted = courses.filter(c => c.status === 'active' && !completedIds.has(c.id)); uncompleted.sort((a, b) => (b.xp || 0) - (a.xp || 0)); return uncompleted[0] ? { course: uncompleted[0], path: null } : { course: null, path }; } export function computeJobMatch(profile, job) { const p = profile || {}; let score = 0; const desired = (p.desired_position || '').toLowerCase(); if (desired && (job.title?.toLowerCase().includes(desired) || job.role_category?.toLowerCase().includes(desired))) score += 35; const profileSkills = new Set((p.skills || []).map(s => s.toLowerCase())); const profileCerts = new Set((p.earned_badges || []).map(b => b.name?.toLowerCase()).filter(Boolean)); const reqSkills = (job.qualifications || []).map(q => q.toLowerCase()); const reqCerts = (job.certifications_required || []).map(c => c.toLowerCase()); let skillHits = 0; for (const s of reqSkills) { for (const ps of profileSkills) { if (s.includes(ps) || ps.includes(s)) { skillHits++; break; } } } if (reqSkills.length) score += Math.min(35, (skillHits / reqSkills.length) * 35); let certHits = 0; for (const c of reqCerts) { for (const pc of profileCerts) { if (c.includes(pc) || pc.includes(c)) { certHits++; break; } } } if (reqCerts.length) score += Math.min(20, (certHits / reqCerts.length) * 20); if ((p.experience_years || 0) >= (job.min_experience_years || 0)) score += 10; return Math.min(100, Math.round(score)); } export function recommendJobs(profile, jobPostings = [], limit = 3) { return jobPostings .filter(j => j.status === 'active') .map(j => ({ job: j, match: computeJobMatch(profile, j) })) .filter(r => r.match > 0) .sort((a, b) => b.match - a.match) .slice(0, limit); }