@jadermme/orus-core
Version:
ORUS Core Framework - Universal framework for 6 Pillars assessment, domain-agnostic
269 lines • 8.62 kB
JavaScript
/**
* 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