chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import type { SkillEntry, ValueScoreBreakdown } from "@planarchy/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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user