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