UNPKG

@jadermme/orus-core

Version:

ORUS Core Framework - Universal framework for 6 Pillars assessment, domain-agnostic

269 lines 8.62 kB
/** * ORUS Core - Scoring Engine * * Pure functions for score normalization, inference, and validation. * 100% domain-agnostic, deterministic, and side-effect free. * * @remarks * All functions follow these principles: * - Pure functions (same input = same output) * - No side effects * - Immutable inputs * - Clear error handling * - Extensive JSDoc documentation */ import { DEFAULT_STATUS_THRESHOLDS } from '../config/defaults.js'; /** * Score boundaries (universal constant) * * @remarks * Scores are ALWAYS in the range [0, 10] * - 0 = worst possible state * - 10 = best possible state * - Decimals allowed (e.g., 5.5, 7.8) */ export const SCORE_MIN = 0; export const SCORE_MAX = 10; /** * Clamps a score to the valid range [0, 10] * * @param score - Raw score to clamp * @returns Clamped score between 0 and 10 * * @remarks * - Pure function: no side effects * - Handles edge cases: NaN, Infinity, negative numbers * - Always returns a valid number in range * * @example * ```typescript * clampScore(15) // => 10 * clampScore(-5) // => 0 * clampScore(7.5) // => 7.5 * clampScore(NaN) // => 0 * clampScore(Infinity) // => 10 * ``` */ export function clampScore(score) { // Handle NaN and invalid numbers if (!Number.isFinite(score)) { return score > 0 ? SCORE_MAX : SCORE_MIN; } // Clamp to range if (score < SCORE_MIN) return SCORE_MIN; if (score > SCORE_MAX) return SCORE_MAX; return score; } /** * Infers status from a numeric score * * @param score - Numeric score (0-10) * @param thresholds - Status thresholds (optional, defaults to ORUS defaults) * @returns Inferred status: "critical", "attention", or "healthy" * * @remarks * - Pure function: deterministic based on thresholds * - Automatically clamps scores to valid range * - Uses inclusive boundaries (>= lower, <= upper) * - Custom thresholds allow vertical-specific classification * * Default thresholds: * - critical: [0, 3.9] * - attention: [4.0, 6.9] * - healthy: [7.0, 10] * * @example * ```typescript * inferStatusFromScore(2.5) // => "critical" * inferStatusFromScore(5.0) // => "attention" * inferStatusFromScore(8.5) // => "healthy" * * // Custom thresholds * const customThresholds = { * critical: [0, 5] as [number, number], * attention: [5.1, 7.5] as [number, number], * healthy: [7.6, 10] as [number, number] * }; * inferStatusFromScore(6.0, customThresholds) // => "attention" * ``` */ export function inferStatusFromScore(score, thresholds = DEFAULT_STATUS_THRESHOLDS) { const clampedScore = clampScore(score); if (clampedScore <= thresholds.critical[1]) { return 'critical'; } if (clampedScore <= thresholds.attention[1]) { return 'attention'; } return 'healthy'; } /** * Normalizes a status to a representative numeric score * * @param status - Status to normalize * @param thresholds - Status thresholds (optional, defaults to ORUS defaults) * @returns Midpoint score for the given status * * @remarks * - Pure function: always returns the same score for the same status * - Returns the midpoint of the status range * - Useful for initializing scores from subjective assessments * - Round to 1 decimal place for readability * * Default outputs: * - "critical" => 2.0 (midpoint of 0-3.9) * - "attention" => 5.5 (midpoint of 4.0-6.9) * - "healthy" => 8.5 (midpoint of 7.0-10) * * @example * ```typescript * normalizeScoreFromStatus("critical") // => 2.0 * normalizeScoreFromStatus("attention") // => 5.5 * normalizeScoreFromStatus("healthy") // => 8.5 * * // Custom thresholds * const customThresholds = { * critical: [0, 4] as [number, number], * attention: [4.1, 7] as [number, number], * healthy: [7.1, 10] as [number, number] * }; * normalizeScoreFromStatus("critical", customThresholds) // => 2.0 * ``` */ export function normalizeScoreFromStatus(status, thresholds = DEFAULT_STATUS_THRESHOLDS) { const [lower, upper] = thresholds[status]; const midpoint = (lower + upper) / 2; // Round to 1 decimal place for readability return Math.round(midpoint * 10) / 10; } /** * Validates if a score matches a status * * @param score - Numeric score (0-10) * @param status - Status to validate against * @param thresholds - Status thresholds (optional, defaults to ORUS defaults) * @returns true if score is within the status range, false otherwise * * @remarks * - Pure function: deterministic validation * - Automatically clamps scores * - Useful for data consistency checks * - Can detect stale or inconsistent data * * @example * ```typescript * validateScoreStatus(2.5, "critical") // => true * validateScoreStatus(2.5, "healthy") // => false * validateScoreStatus(8.0, "healthy") // => true * validateScoreStatus(5.5, "attention") // => true * ``` */ export function validateScoreStatus(score, status, thresholds = DEFAULT_STATUS_THRESHOLDS) { const inferredStatus = inferStatusFromScore(score, thresholds); return inferredStatus === status; } /** * Calculates a confidence-adjusted score * * @param score - Base score (0-10) * @param confidence - Confidence level: "low" | "medium" | "high" * @returns Object with adjusted score and confidence factor * * @remarks * - Pure function: deterministic adjustment * - Low confidence: regression to mean (5.0) by 30% * - Medium confidence: regression to mean by 15% * - High confidence: no adjustment (100% trust) * - Useful for hybrid mode calculations * - Represents uncertainty in subjective assessments * * Confidence factors: * - low: 0.7 (70% weight on score, 30% on mean) * - medium: 0.85 (85% weight on score, 15% on mean) * - high: 1.0 (100% trust in score) * * @example * ```typescript * adjustScoreByConfidence(8.0, "low") * // => { adjustedScore: 7.1, confidenceFactor: 0.7 } * // Calculation: 8.0 * 0.7 + 5.0 * 0.3 = 7.1 * * adjustScoreByConfidence(8.0, "medium") * // => { adjustedScore: 7.6, confidenceFactor: 0.85 } * * adjustScoreByConfidence(8.0, "high") * // => { adjustedScore: 8.0, confidenceFactor: 1.0 } * ``` */ export function adjustScoreByConfidence(score, confidence) { const clampedScore = clampScore(score); // Confidence factors (how much to trust the score) const confidenceFactors = { low: 0.7, medium: 0.85, high: 1.0 }; const factor = confidenceFactors[confidence]; const mean = 5.0; // Neutral midpoint // Regression toward mean based on confidence const adjustedScore = clampedScore * factor + mean * (1 - factor); return { adjustedScore: Math.round(adjustedScore * 10) / 10, confidenceFactor: factor }; } /** * Calculates score delta (change) between two scores * * @param currentScore - Current score (0-10) * @param previousScore - Previous score (0-10) * @returns Delta object with absolute and percentage change * * @remarks * - Pure function: deterministic calculation * - Positive delta = improvement * - Negative delta = deterioration * - Percentage relative to max possible change from previous score * - Handles edge cases (division by zero, etc.) * * @example * ```typescript * calculateScoreDelta(8.0, 5.0) * // => { absoluteDelta: 3.0, percentageDelta: 60, isImproving: true } * * calculateScoreDelta(4.0, 7.0) * // => { absoluteDelta: -3.0, percentageDelta: -42.86, isImproving: false } * * calculateScoreDelta(5.0, 5.0) * // => { absoluteDelta: 0, percentageDelta: 0, isImproving: false } * ``` */ export function calculateScoreDelta(currentScore, previousScore) { const current = clampScore(currentScore); const previous = clampScore(previousScore); const absoluteDelta = current - previous; // Calculate percentage relative to the maximum possible change // If improving: max change is (10 - previous) // If declining: max change is previous (going to 0) let percentageDelta = 0; if (absoluteDelta > 0) { const maxPossibleGain = SCORE_MAX - previous; percentageDelta = maxPossibleGain > 0 ? (absoluteDelta / maxPossibleGain) * 100 : 0; } else if (absoluteDelta < 0) { const maxPossibleLoss = previous - SCORE_MIN; percentageDelta = maxPossibleLoss > 0 ? (absoluteDelta / maxPossibleLoss) * 100 : 0; } return { absoluteDelta: Math.round(absoluteDelta * 10) / 10, percentageDelta: Math.round(percentageDelta * 10) / 10, isImproving: absoluteDelta > 0 }; } //# sourceMappingURL=scoring.js.map