import type { SkillEntry, ValueScoreBreakdown } from "@capakraken/shared"; export interface ValueScoreInput { skills: SkillEntry[]; lcrCents: number; chargeabilityTarget: number; currentChargeability: number; // actual % (computed from allocations by caller) maxLcrCents: number; // org-wide max (for normalization) } export interface ValueScoreWeights { skillDepth: number; skillBreadth: number; costEfficiency: number; chargeability: number; experience: number; } /** * Computes a context-free value score (price/quality ratio) for a resource. * All dimensions are 0-100; total is a weighted composite clamped to 0-100. */ export function computeValueScore( input: ValueScoreInput, weights: ValueScoreWeights, ): ValueScoreBreakdown { const { skills, lcrCents, chargeabilityTarget, currentChargeability, maxLcrCents } = input; // 1. Skill Depth: avg proficiency normalized to 100 let skillDepth = 0; if (skills.length > 0) { const avgProficiency = skills.reduce((sum, s) => sum + s.proficiency, 0) / skills.length; skillDepth = Math.round((avgProficiency / 5) * 100); } // 2. Skill Breadth: skill count capped at 10 skills → 100 const skillBreadth = Math.min(100, skills.length * 10); // 3. Cost Efficiency: cheapest = 100; if all same LCR → 0 for all let costEfficiency = 0; if (maxLcrCents === 0) { costEfficiency = 100; } else { costEfficiency = Math.round((1 - lcrCents / maxLcrCents) * 100); costEfficiency = Math.max(0, Math.min(100, costEfficiency)); } // 4. Chargeability: closer to target = higher; ±50pp gap = 0 const chargeability = Math.max( 0, 100 - Math.abs(chargeabilityTarget - currentChargeability) * 2, ); // 5. Experience: avg yearsExperience capped at 10yr → 100 let experience = 0; const skillsWithYears = skills.filter((s) => (s.yearsExperience ?? 0) > 0); if (skillsWithYears.length > 0) { const avgYears = skillsWithYears.reduce((sum, s) => sum + (s.yearsExperience ?? 0), 0) / skillsWithYears.length; experience = Math.min(100, Math.round(avgYears * 10)); } const total = Math.max( 0, Math.min( 100, Math.round( skillDepth * weights.skillDepth + skillBreadth * weights.skillBreadth + costEfficiency * weights.costEfficiency + chargeability * weights.chargeability + experience * weights.experience, ), ), ); return { skillDepth, skillBreadth, costEfficiency, chargeability: Math.round(chargeability), experience, total, }; }