UNPKG

@jadermme/orus-core

Version:

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

342 lines 11.4 kB
/** * ORUS Core - Validation Functions * * Pure functions for validating pillar and assessment data structures. * Ensures data integrity and catches inconsistencies early. * * @remarks * All validators: * - Are pure functions (no side effects) * - Return validation results (not throw errors) * - Provide actionable error messages * - Support both runtime and compile-time checking */ import { SCORE_MIN, SCORE_MAX, validateScoreStatus } from '../engine/scoring.js'; /** * Validates a pillar ID * * @param pillarId - Pillar ID to validate * @returns Validation result * * @remarks * - Pure function: deterministic validation * - Checks against valid PillarId enum values * * @example * ```typescript * validatePillarId('PILLAR_1') // => { isValid: true, ... } * validatePillarId('INVALID') // => { isValid: false, ... } * ``` */ export function validatePillarId(pillarId) { const validIds = [ 'PILLAR_1', 'PILLAR_2', 'PILLAR_3', 'PILLAR_4', 'PILLAR_5', 'PILLAR_6' ]; if (!validIds.includes(pillarId)) { return { isValid: false, errors: [ `Invalid pillar ID: "${pillarId}". Must be one of: ${validIds.join(', ')}` ], warnings: [] }; } return { isValid: true, errors: [], warnings: [] }; } /** * Validates a pillar assessment * * @param pillar - Pillar assessment to validate * @param strict - Enable strict mode (additional checks) * @returns Validation result * * @remarks * - Pure function: deterministic validation * - Checks all core fields and their constraints * - Strict mode validates score-status consistency * - Detects stale data (lastUpdated > 90 days ago) * * @example * ```typescript * const result = validatePillarAssessment(pillar, true); * * if (!result.isValid) { * console.error('Validation errors:', result.errors); * } * * if (result.warnings.length > 0) { * console.warn('Warnings:', result.warnings); * } * ``` */ export function validatePillarAssessment(pillar, strict = false) { const errors = []; const warnings = []; // Validate pillarId const pillarIdResult = validatePillarId(pillar.pillarId); if (!pillarIdResult.isValid) { errors.push(...pillarIdResult.errors); } // Validate score if (typeof pillar.score !== 'number') { errors.push(`Score must be a number, got: ${typeof pillar.score}`); } else if (!Number.isFinite(pillar.score)) { errors.push(`Score must be finite, got: ${pillar.score}`); } else if (pillar.score < SCORE_MIN || pillar.score > SCORE_MAX) { errors.push(`Score must be between ${SCORE_MIN} and ${SCORE_MAX}, got: ${pillar.score}`); } // Validate status const validStatuses = ['critical', 'attention', 'healthy']; if (!validStatuses.includes(pillar.status)) { errors.push(`Invalid status: "${pillar.status}". Must be one of: ${validStatuses.join(', ')}`); } // Validate mode const validModes = ['subjective', 'objective', 'hybrid']; if (!validModes.includes(pillar.mode)) { errors.push(`Invalid mode: "${pillar.mode}". Must be one of: ${validModes.join(', ')}`); } // Validate confidence const validConfidences = ['low', 'medium', 'high']; if (!validConfidences.includes(pillar.confidence)) { errors.push(`Invalid confidence: "${pillar.confidence}". Must be one of: ${validConfidences.join(', ')}`); } // Validate dataCompleteness if (typeof pillar.dataCompleteness !== 'number') { errors.push(`dataCompleteness must be a number, got: ${typeof pillar.dataCompleteness}`); } else if (pillar.dataCompleteness < 0 || pillar.dataCompleteness > 1) { errors.push(`dataCompleteness must be between 0 and 1, got: ${pillar.dataCompleteness}`); } // Validate lastUpdated if (!(pillar.lastUpdated instanceof Date)) { errors.push(`lastUpdated must be a Date object`); } else if (isNaN(pillar.lastUpdated.getTime())) { errors.push(`lastUpdated is an invalid Date`); } else { // Check for stale data (warning only) const daysSinceUpdate = (Date.now() - pillar.lastUpdated.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceUpdate > 90) { warnings.push(`Data is stale (${Math.round(daysSinceUpdate)} days old). Consider updating.`); } } // Validate trend (if present) if (pillar.trend !== undefined) { const validTrends = ['improving', 'stable', 'declining']; if (!validTrends.includes(pillar.trend)) { errors.push(`Invalid trend: "${pillar.trend}". Must be one of: ${validTrends.join(', ')}`); } } // Validate insights (if present) if (pillar.insights !== undefined) { if (!Array.isArray(pillar.insights)) { errors.push(`insights must be an array, got: ${typeof pillar.insights}`); } else if (pillar.insights.some((i) => typeof i !== 'string')) { errors.push(`All insights must be strings`); } } // Validate recommendations (if present) if (pillar.recommendations !== undefined) { if (!Array.isArray(pillar.recommendations)) { errors.push(`recommendations must be an array, got: ${typeof pillar.recommendations}`); } else if (pillar.recommendations.some((r) => typeof r !== 'string')) { errors.push(`All recommendations must be strings`); } } // Strict mode: validate score-status consistency if (strict && errors.length === 0) { const isConsistent = validateScoreStatus(pillar.score, pillar.status); if (!isConsistent) { warnings.push(`Score ${pillar.score} may not match status "${pillar.status}". ` + `This could indicate stale data or manual override.`); } } return { isValid: errors.length === 0, errors, warnings }; } /** * Validates a complete six pillars assessment * * @param assessment - Assessment to validate * @param strict - Enable strict mode * @returns Validation result * * @remarks * - Pure function: deterministic validation * - Validates structure, all pillars, and overall consistency * - Checks that all 6 pillars are present * - Validates overall score calculation * * @example * ```typescript * const result = validateSixPillarsAssessment(assessment, true); * * if (!result.isValid) { * console.error('Assessment has errors:', result.errors); * // Do not save or use this assessment * } * ``` */ export function validateSixPillarsAssessment(assessment, strict = false) { const errors = []; const warnings = []; // Validate structure if (!assessment || typeof assessment !== 'object') { return { isValid: false, errors: ['Assessment must be an object'], warnings: [] }; } if (!assessment.pillars || typeof assessment.pillars !== 'object') { return { isValid: false, errors: ['Assessment must have a "pillars" object'], warnings: [] }; } // Validate that all 6 pillars are present const expectedPillars = [ 'PILLAR_1', 'PILLAR_2', 'PILLAR_3', 'PILLAR_4', 'PILLAR_5', 'PILLAR_6' ]; const presentPillars = Object.keys(assessment.pillars); expectedPillars.forEach((pillarId) => { if (!presentPillars.includes(pillarId)) { errors.push(`Missing pillar: ${pillarId}`); } }); // Validate each pillar Object.values(assessment.pillars).forEach((pillar) => { const pillarResult = validatePillarAssessment(pillar, strict); if (!pillarResult.isValid) { errors.push(`Pillar ${pillar.pillarId}: ${pillarResult.errors.join(', ')}`); } if (pillarResult.warnings.length > 0) { warnings.push(`Pillar ${pillar.pillarId}: ${pillarResult.warnings.join(', ')}`); } }); // Validate overallScore if (typeof assessment.overallScore !== 'number') { errors.push(`overallScore must be a number, got: ${typeof assessment.overallScore}`); } else if (!Number.isFinite(assessment.overallScore)) { errors.push(`overallScore must be finite`); } else if (assessment.overallScore < SCORE_MIN || assessment.overallScore > SCORE_MAX) { errors.push(`overallScore must be between ${SCORE_MIN} and ${SCORE_MAX}, got: ${assessment.overallScore}`); } // Strict mode: validate overall score calculation if (strict && errors.length === 0) { const calculatedOverall = Object.values(assessment.pillars).reduce((sum, p) => sum + p.score, 0) / 6; const delta = Math.abs(calculatedOverall - assessment.overallScore); if (delta > 0.1) { warnings.push(`overallScore (${assessment.overallScore}) differs from calculated average (${calculatedOverall.toFixed(2)}). ` + `This may indicate weighted scoring or outdated data.`); } } // Validate lastUpdated if (!(assessment.lastUpdated instanceof Date)) { errors.push(`lastUpdated must be a Date object`); } else if (isNaN(assessment.lastUpdated.getTime())) { errors.push(`lastUpdated is an invalid Date`); } // Validate mode const validModes = ['subjective', 'objective', 'hybrid']; if (!validModes.includes(assessment.mode)) { errors.push(`Invalid mode: "${assessment.mode}". Must be one of: ${validModes.join(', ')}`); } // Validate schemaVersion if (typeof assessment.schemaVersion !== 'number') { errors.push(`schemaVersion must be a number`); } else if (!Number.isInteger(assessment.schemaVersion)) { errors.push(`schemaVersion must be an integer`); } else if (assessment.schemaVersion < 1) { errors.push(`schemaVersion must be >= 1`); } return { isValid: errors.length === 0, errors, warnings }; } /** * Quick validation (non-strict, for runtime checks) * * @param assessment - Assessment to validate * @returns Whether assessment is valid (ignores warnings) * * @remarks * - Pure function: simple boolean check * - Useful for conditional logic and guards * - Does not provide detailed error messages * * @example * ```typescript * if (isValidAssessment(assessment)) { * // Safe to use assessment * saveToDB(assessment); * } * ``` */ export function isValidAssessment(assessment) { const result = validateSixPillarsAssessment(assessment, false); return result.isValid; } /** * Creates a validation error object * * @param field - Field name * @param message - Error message * @param value - Current value * @param expected - Expected value/format * @returns Validation error object * * @remarks * - Pure function: factory for consistent error objects * - Useful for building custom validators * * @example * ```typescript * const error = createValidationError( * 'score', * 'Score out of range', * 15, * '0-10' * ); * ``` */ export function createValidationError(field, message, value, expected) { return { field, message, value, expected }; } //# sourceMappingURL=validators.js.map