cd78f72f33
Complete rename of all technical identifiers across the codebase: Package names (11 packages): - @planarchy/* → @capakraken/* in all package.json, tsconfig, imports Import statements: 277 files, 548 occurrences replaced Database & Docker: - PostgreSQL user/db: planarchy → capakraken - Docker volumes: planarchy_pgdata → capakraken_pgdata - Connection strings updated in docker-compose, .env, CI CI/CD: - GitHub Actions workflow: all filter commands updated - Test database credentials updated Infrastructure: - Redis channel: planarchy:sse → capakraken:sse - Logger service name: planarchy-api → capakraken-api - Anonymization seed updated - Start/stop/restart scripts updated Test data: - Seed emails: @planarchy.dev → @capakraken.dev - E2E test credentials: all 11 spec files updated - Email defaults: @planarchy.app → @capakraken.app - localStorage keys: planarchy_* → capakraken_* Documentation: 30+ .md files updated Verification: - pnpm install: workspace resolution works - TypeScript: only pre-existing TS2589 (no new errors) - Engine: 310/310 tests pass - Staffing: 37/37 tests pass Co-Authored-By: claude-flow <ruv@ruv.net>
85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
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,
|
|
};
|
|
}
|