UNPKG

@sailboat-computer/validation

Version:

Validation framework for sailboat computer v3

211 lines 7.49 kB
"use strict"; /** * Base validation rule implementation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RateOfChangeRule = exports.RangeValidationRule = exports.BaseValidationRule = void 0; const types_1 = require("../types"); /** * Abstract base class for validation rules */ class BaseValidationRule { constructor(name, description, category, severity) { this.name = name; this.description = description; this.category = category; this.severity = severity; } /** * Get rule configuration (can be overridden by subclasses) */ getConfiguration() { return { name: this.name, description: this.description, category: this.category, severity: this.severity }; } /** * Helper method to create a validation result */ createResult(passed, message, details) { const result = { rule: this.name, passed, severity: this.severity }; if (message !== undefined) { result.message = message; } if (details !== undefined) { result.details = details; } return result; } /** * Helper method to check if a numeric value is within range */ isInRange(value, min, max, inclusive = true) { if (inclusive) { return value >= min && value <= max; } else { return value > min && value < max; } } /** * Helper method to check rate of change */ checkRateOfChange(currentValue, previousValue, timeDeltaSeconds, maxRatePerSecond) { if (timeDeltaSeconds <= 0) return true; const rateOfChange = Math.abs(currentValue - previousValue) / timeDeltaSeconds; return rateOfChange <= maxRatePerSecond; } /** * Helper method to get previous sensor data from context */ getPreviousData(data, context, maxAgeSeconds = 60) { const cutoffTime = data.timestamp.getTime() - (maxAgeSeconds * 1000); return context.recentData .filter(d => d.sensorId === data.sensorId && d.timestamp.getTime() >= cutoffTime && d.timestamp.getTime() < data.timestamp.getTime()) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())[0]; } /** * Helper method to extract numeric value from sensor data */ extractNumericValue(data) { if (typeof data.value === 'number') { return data.value; } if (typeof data.value === 'object' && data.value !== null) { // Try common numeric field names const obj = data.value; for (const field of ['value', 'reading', 'measurement', 'data']) { if (typeof obj[field] === 'number') { return obj[field]; } } } return null; } /** * Helper method to check if sensor type matches expected types */ matchesSensorType(data, expectedTypes) { return expectedTypes.some(type => data.sensorType.toLowerCase().includes(type.toLowerCase())); } /** * Helper method to get operational context priority */ getContextPriority(context) { switch (context.operationalContext.toLowerCase()) { case 'emergency': case 'safety': return 5; case 'sailing': case 'motoring': return 4; case 'anchored': return 3; case 'maintenance': return 2; case 'docked': default: return 1; } } } exports.BaseValidationRule = BaseValidationRule; /** * Range validation rule for numeric sensor values */ class RangeValidationRule extends BaseValidationRule { constructor(name, description, minValue, maxValue, sensorTypes, severity = types_1.AlertSeverity.ALARM) { super(name, description, 'physical', severity); this.minValue = minValue; this.maxValue = maxValue; this.sensorTypes = sensorTypes; } validate(data, context) { const value = this.extractNumericValue(data); if (value === null) { return this.createResult(false, `Unable to extract numeric value from ${data.sensorType} sensor`, { rawValue: data.value }); } const inRange = this.isInRange(value, this.minValue, this.maxValue); return this.createResult(inRange, inRange ? undefined : `Value ${value} ${data.unit} is outside valid range [${this.minValue}, ${this.maxValue}] ${data.unit}`, { value, minValue: this.minValue, maxValue: this.maxValue, unit: data.unit }); } isApplicable(data) { return this.matchesSensorType(data, this.sensorTypes); } getConfiguration() { return { ...super.getConfiguration(), minValue: this.minValue, maxValue: this.maxValue, sensorTypes: this.sensorTypes }; } } exports.RangeValidationRule = RangeValidationRule; /** * Rate of change validation rule */ class RateOfChangeRule extends BaseValidationRule { constructor(name, description, maxRatePerSecond, sensorTypes, severity = types_1.AlertSeverity.WARNING) { super(name, description, 'physical', severity); this.maxRatePerSecond = maxRatePerSecond; this.sensorTypes = sensorTypes; } validate(data, context) { const currentValue = this.extractNumericValue(data); if (currentValue === null) { return this.createResult(false, `Unable to extract numeric value from ${data.sensorType} sensor`, { rawValue: data.value }); } const previousData = this.getPreviousData(data, context); if (!previousData) { // No previous data to compare - pass validation return this.createResult(true, 'No previous data available for rate of change validation', { currentValue }); } const previousValue = this.extractNumericValue(previousData); if (previousValue === null) { return this.createResult(true, 'Previous data value not numeric - skipping rate validation', { currentValue }); } const timeDelta = (data.timestamp.getTime() - previousData.timestamp.getTime()) / 1000; const rateValid = this.checkRateOfChange(currentValue, previousValue, timeDelta, this.maxRatePerSecond); const actualRate = timeDelta > 0 ? Math.abs(currentValue - previousValue) / timeDelta : 0; return this.createResult(rateValid, rateValid ? undefined : `Rate of change ${actualRate.toFixed(3)} ${data.unit}/s exceeds maximum ${this.maxRatePerSecond} ${data.unit}/s`, { currentValue, previousValue, timeDelta, actualRate, maxRate: this.maxRatePerSecond, unit: data.unit }); } isApplicable(data) { return this.matchesSensorType(data, this.sensorTypes); } getConfiguration() { return { ...super.getConfiguration(), maxRatePerSecond: this.maxRatePerSecond, sensorTypes: this.sensorTypes }; } } exports.RateOfChangeRule = RateOfChangeRule; //# sourceMappingURL=base-rule.js.map