datapilot-cli
Version:
Enterprise-grade streaming multi-format data analysis with comprehensive statistical insights and intelligent relationship detection - supports CSV, JSON, Excel, TSV, Parquet - memory-efficient, cross-platform
1,019 lines • 48.7 kB
JavaScript
;
/**
* Business Rule Validation Engine
* Implements configurable cross-field validation rules for data quality assessment
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BusinessRuleEngine = void 0;
class BusinessRuleEngine {
data;
headers;
config;
rules = [];
violations = [];
constructor(data, headers, _columnTypes, // Intentionally unused - reserved for future type-specific rules
config = {}) {
this.data = data;
this.headers = headers;
this.config = {
enabledRuleTypes: ['cross_field', 'intra_record'],
maxViolationsToTrack: 1000,
enableBuiltInRules: true,
...config,
};
this.initializeRules();
}
validateData() {
this.violations = [];
// Validate each row against all enabled rules
for (let rowIndex = 0; rowIndex < this.data.length; rowIndex++) {
const row = this.data[rowIndex];
for (const rule of this.rules) {
if (!rule.enabled || this.violations.length >= this.config.maxViolationsToTrack) {
continue;
}
try {
const result = rule.validate(row, this.headers, rowIndex);
if (result && !result.passed) {
this.violations.push(result);
}
}
catch (error) {
console.warn(`Rule ${rule.id} failed on row ${rowIndex}:`, error);
}
}
}
return this.generateValidationReport();
}
initializeRules() {
if (!this.config.enableBuiltInRules) {
return;
}
// Date Chronology Rules
this.addDateChronologyRules();
// Age-Related Rules
this.addAgeValidationRules();
// Financial Calculation Rules
this.addFinancialValidationRules();
// Geographic Consistency Rules
this.addGeographicConsistencyRules();
// Logical Consistency Rules
this.addLogicalConsistencyRules();
// Medical Domain Rules
this.addMedicalValidationRules();
// Add custom rules if provided
if (this.config.customRules) {
this.rules.push(...this.config.customRules.filter((rule) => rule.enabled));
}
// Filter rules by enabled types
this.rules = this.rules.filter((rule) => this.config.enabledRuleTypes.includes(rule.type));
}
addDateChronologyRules() {
// Find date-like columns
const dateColumns = this.findColumnsByPattern(/(date|time|created|updated|start|end|birth|expir)/i);
if (dateColumns.length >= 2) {
// Start/End date validation
const startDateCols = dateColumns.filter((col) => /(start|begin|from)/i.test(col.name));
const endDateCols = dateColumns.filter((col) => /(end|finish|to|until|expir)/i.test(col.name));
for (const startCol of startDateCols) {
for (const endCol of endDateCols) {
this.rules.push({
id: `date_chronology_${startCol.name}_${endCol.name}`,
name: 'Date Chronology Validation',
description: `${startCol.name} should be before ${endCol.name}`,
type: 'cross_field',
severity: 'high',
enabled: true,
validate: (row, _headers, rowIndex) => {
const startValue = row[startCol.index];
const endValue = row[endCol.index];
if (!startValue || !endValue)
return null;
const startDate = this.parseDate(startValue);
const endDate = this.parseDate(endValue);
if (startDate && endDate && startDate > endDate) {
return {
ruleId: `date_chronology_${startCol.name}_${endCol.name}`,
passed: false,
rowIndex,
values: { [startCol.name]: startValue, [endCol.name]: endValue },
issue: `${startCol.name} (${startValue}) is after ${endCol.name} (${endValue})`,
severity: 'high',
};
}
return null;
},
});
}
}
}
}
addAgeValidationRules() {
const ageCol = this.findColumnsByPattern(/^age$/i)[0];
const experienceCol = this.findColumnsByPattern(/(experience|years.*work|tenure)/i)[0];
const birthCol = this.findColumnsByPattern(/(birth|born|dob)/i)[0];
// Age vs Experience validation
if (ageCol && experienceCol) {
this.rules.push({
id: 'age_experience_consistency',
name: 'Age vs Experience Validation',
description: 'Age should be >= (Years of Experience + 16)',
type: 'cross_field',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const age = this.parseNumber(row[ageCol.index]);
const experience = this.parseNumber(row[experienceCol.index]);
if (age !== null && experience !== null) {
const minimumAge = experience + 16; // Assuming work starts at 16
if (age < minimumAge) {
return {
ruleId: 'age_experience_consistency',
passed: false,
rowIndex,
values: { age: row[ageCol.index], experience: row[experienceCol.index] },
issue: `Age (${age}) too young for ${experience} years of experience (minimum: ${minimumAge})`,
severity: 'medium',
};
}
}
return null;
},
});
}
// Age vs Birth Date validation
if (ageCol && birthCol) {
this.rules.push({
id: 'age_birthdate_consistency',
name: 'Age vs Birth Date Validation',
description: 'Age should match calculated age from birth date',
type: 'cross_field',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const age = this.parseNumber(row[ageCol.index]);
const birthDate = this.parseDate(row[birthCol.index] || '');
if (age !== null && birthDate) {
const calculatedAge = new Date().getFullYear() - birthDate.getFullYear();
const ageDifference = Math.abs(age - calculatedAge);
if (ageDifference > 1) {
// Allow 1 year tolerance
return {
ruleId: 'age_birthdate_consistency',
passed: false,
rowIndex,
values: { age: row[ageCol.index], birthDate: row[birthCol.index] },
issue: `Age (${age}) doesn't match birth date (calculated: ${calculatedAge})`,
severity: 'medium',
};
}
}
return null;
},
});
}
}
addFinancialValidationRules() {
// Total = Quantity × Unit Price validation
const totalCols = this.findColumnsByPattern(/(total|amount|sum)/i);
const quantityCols = this.findColumnsByPattern(/(quantity|qty|count|number)/i);
const priceCols = this.findColumnsByPattern(/(price|cost|rate|unit.*price)/i);
for (const totalCol of totalCols) {
for (const qtyCol of quantityCols) {
for (const priceCol of priceCols) {
this.rules.push({
id: `financial_calculation_${totalCol.name}_${qtyCol.name}_${priceCol.name}`,
name: 'Financial Calculation Validation',
description: `${totalCol.name} should equal ${qtyCol.name} × ${priceCol.name}`,
type: 'business_logic',
severity: 'high',
enabled: true,
validate: (row, _headers, rowIndex) => {
const total = this.parseNumber(row[totalCol.index]);
const quantity = this.parseNumber(row[qtyCol.index]);
const price = this.parseNumber(row[priceCol.index]);
if (total !== null && quantity !== null && price !== null) {
const calculatedTotal = quantity * price;
const tolerance = Math.max(0.01, calculatedTotal * 0.001); // 0.1% or $0.01 minimum
if (Math.abs(total - calculatedTotal) > tolerance) {
return {
ruleId: `financial_calculation_${totalCol.name}_${qtyCol.name}_${priceCol.name}`,
passed: false,
rowIndex,
values: {
[totalCol.name]: row[totalCol.index],
[qtyCol.name]: row[qtyCol.index],
[priceCol.name]: row[priceCol.index],
},
issue: `${totalCol.name} (${total}) ≠ ${qtyCol.name} (${quantity}) × ${priceCol.name} (${price}) = ${calculatedTotal}`,
severity: 'high',
};
}
}
return null;
},
});
}
}
}
}
addGeographicConsistencyRules() {
// Find relevant columns (city and country reserved for future geographic validation)
const stateCol = this.findColumnsByPattern(/(state|province|region)/i)[0];
const postalCol = this.findColumnsByPattern(/(postal|zip|postcode)/i)[0];
// Note: This would require external geographic databases for full validation
// For now, we implement basic format consistency checks
if (stateCol && postalCol) {
this.rules.push({
id: 'state_postal_format_consistency',
name: 'State-Postal Code Format Consistency',
description: 'State and postal code formats should be consistent',
type: 'intra_record',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const state = row[stateCol.index]?.toString().trim();
const postal = row[postalCol.index]?.toString().trim();
if (state && postal) {
// US format check (2-letter state, 5 or 9 digit ZIP)
const isUSState = /^[A-Z]{2}$/i.test(state);
const isUSZip = /^\d{5}(-?\d{4})?$/.test(postal);
// Canadian format check (2-letter province, postal format)
const isCAProvince = /^[A-Z]{2}$/i.test(state);
const isCAPostal = /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i.test(postal);
const formatMismatch = (isUSState && !isUSZip && !isCAPostal) || (isCAProvince && !isCAPostal && !isUSZip);
if (formatMismatch) {
return {
ruleId: 'state_postal_format_consistency',
passed: false,
rowIndex,
values: { state, postal },
issue: `State format (${state}) doesn't match postal code format (${postal})`,
severity: 'medium',
};
}
}
return null;
},
});
}
}
addLogicalConsistencyRules() {
// Marital status vs spouse name
const maritalCol = this.findColumnsByPattern(/(marital|marriage)/i)[0];
const spouseCol = this.findColumnsByPattern(/(spouse|partner)/i)[0];
if (maritalCol && spouseCol) {
this.rules.push({
id: 'marital_spouse_consistency',
name: 'Marital Status vs Spouse Consistency',
description: 'Single status should not have spouse name',
type: 'cross_field',
severity: 'low',
enabled: true,
validate: (row, _headers, rowIndex) => {
const marital = row[maritalCol.index]?.toString().toLowerCase().trim();
const spouse = row[spouseCol.index]?.toString().trim();
if (marital && spouse) {
const isSingle = /(single|unmarried|never.*married)/i.test(marital);
const hasSpouseName = spouse.length > 0 && !/(n\/a|na|null|none|empty)/i.test(spouse);
if (isSingle && hasSpouseName) {
return {
ruleId: 'marital_spouse_consistency',
passed: false,
rowIndex,
values: { maritalStatus: marital, spouseName: spouse },
issue: `Marital status is '${marital}' but spouse name is provided: '${spouse}'`,
severity: 'low',
};
}
}
return null;
},
});
}
}
addMedicalValidationRules() {
// Medical vital signs and measurement validation
const bpSysCol = this.findColumnsByPattern(/(blood.*pressure.*sys|systolic|bp.*sys)/i)[0];
const bpDiaCol = this.findColumnsByPattern(/(blood.*pressure.*dia|diastolic|bp.*dia)/i)[0];
const heartRateCol = this.findColumnsByPattern(/(heart.*rate|pulse|hr|bpm)/i)[0];
const temperatureCol = this.findColumnsByPattern(/(temperature|temp|fever)/i)[0];
const heightCol = this.findColumnsByPattern(/(height|stature)/i)[0];
const weightCol = this.findColumnsByPattern(/(weight|mass)/i)[0];
const ageCol = this.findColumnsByPattern(/^age$/i)[0];
// Blood Pressure Systolic vs Diastolic
if (bpSysCol && bpDiaCol) {
this.rules.push({
id: 'blood_pressure_systolic_diastolic',
name: 'Blood Pressure Systolic vs Diastolic',
description: 'Systolic pressure should be greater than diastolic pressure',
type: 'cross_field',
severity: 'high',
enabled: true,
validate: (row, _headers, rowIndex) => {
const systolic = this.parseNumber(row[bpSysCol.index]);
const diastolic = this.parseNumber(row[bpDiaCol.index]);
if (systolic !== null && diastolic !== null) {
if (systolic <= diastolic) {
return {
ruleId: 'blood_pressure_systolic_diastolic',
passed: false,
rowIndex,
values: { systolic: row[bpSysCol.index], diastolic: row[bpDiaCol.index] },
issue: `Systolic BP (${systolic}) should be greater than diastolic BP (${diastolic})`,
severity: 'high',
};
}
// Check for unrealistic values
if (systolic < 60 || systolic > 300 || diastolic < 30 || diastolic > 200) {
return {
ruleId: 'blood_pressure_systolic_diastolic',
passed: false,
rowIndex,
values: { systolic: row[bpSysCol.index], diastolic: row[bpDiaCol.index] },
issue: `Blood pressure values (${systolic}/${diastolic}) outside realistic range (60-300/30-200)`,
severity: 'medium',
};
}
}
return null;
},
});
}
// Heart Rate Validation
if (heartRateCol) {
this.rules.push({
id: 'heart_rate_range',
name: 'Heart Rate Range Validation',
description: 'Heart rate should be within physiologically plausible range',
type: 'business_logic',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const heartRate = this.parseNumber(row[heartRateCol.index]);
if (heartRate !== null) {
// Normal range: 30-220 BPM (covers bradycardia to maximum exercise)
if (heartRate < 30 || heartRate > 220) {
return {
ruleId: 'heart_rate_range',
passed: false,
rowIndex,
values: { heartRate: row[heartRateCol.index] },
issue: `Heart rate (${heartRate} BPM) outside physiological range (30-220 BPM)`,
severity: 'medium',
};
}
}
return null;
},
});
}
// Body Temperature Validation
if (temperatureCol) {
this.rules.push({
id: 'body_temperature_range',
name: 'Body Temperature Range Validation',
description: 'Body temperature should be within survivable range',
type: 'business_logic',
severity: 'high',
enabled: true,
validate: (row, _headers, rowIndex) => {
const temp = this.parseNumber(row[temperatureCol.index]);
if (temp !== null) {
// Detect if Celsius or Fahrenheit based on value range
let tempC = temp;
let unit = '°C';
if (temp > 50) {
// Likely Fahrenheit, convert to Celsius
tempC = ((temp - 32) * 5) / 9;
unit = '°F';
}
// Normal human body temperature range: 32-45°C (89.6-113°F)
if (tempC < 32 || tempC > 45) {
return {
ruleId: 'body_temperature_range',
passed: false,
rowIndex,
values: { temperature: row[temperatureCol.index] },
issue: `Body temperature (${temp}${unit} = ${tempC.toFixed(1)}°C) outside survivable range (32-45°C)`,
severity: 'high',
};
}
}
return null;
},
});
}
// BMI Consistency (Height vs Weight)
if (heightCol && weightCol) {
this.rules.push({
id: 'bmi_plausibility',
name: 'BMI Plausibility Check',
description: 'BMI calculated from height and weight should be realistic',
type: 'cross_field',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const height = this.parseNumber(row[heightCol.index]);
const weight = this.parseNumber(row[weightCol.index]);
if (height !== null && weight !== null && height > 0) {
// Convert height to meters if likely in cm
let heightM = height;
if (height > 3) {
heightM = height / 100; // Convert cm to m
}
const bmi = weight / (heightM * heightM);
// Extreme BMI ranges (survival limits approximately 10-80)
if (bmi < 10 || bmi > 80) {
return {
ruleId: 'bmi_plausibility',
passed: false,
rowIndex,
values: { height: row[heightCol.index], weight: row[weightCol.index] },
issue: `Calculated BMI (${bmi.toFixed(1)}) from height (${height}) and weight (${weight}) is implausible (normal: 10-80)`,
severity: 'medium',
};
}
}
return null;
},
});
}
// Age vs Medical Values Consistency
if (ageCol && bpSysCol) {
this.rules.push({
id: 'age_blood_pressure_consistency',
name: 'Age vs Blood Pressure Consistency',
description: 'Blood pressure should be age-appropriate',
type: 'cross_field',
severity: 'low',
enabled: true,
validate: (row, _headers, rowIndex) => {
const age = this.parseNumber(row[ageCol.index]);
const systolic = this.parseNumber(row[bpSysCol.index]);
if (age !== null && systolic !== null) {
// Very basic age-related BP check
if (age < 18 && systolic > 140) {
return {
ruleId: 'age_blood_pressure_consistency',
passed: false,
rowIndex,
values: { age: row[ageCol.index], systolic: row[bpSysCol.index] },
issue: `High systolic BP (${systolic}) unusual for age ${age} (pediatric hypertension threshold typically <130)`,
severity: 'low',
};
}
}
return null;
},
});
}
// Medical Code Format Validation (ICD-10, etc.)
const diagnosisCol = this.findColumnsByPattern(/(diagnosis|icd|disease|condition)/i)[0];
if (diagnosisCol) {
this.rules.push({
id: 'medical_code_format',
name: 'Medical Code Format Validation',
description: 'Medical codes should follow standard formats',
type: 'business_logic',
severity: 'low',
enabled: true,
validate: (row, _headers, rowIndex) => {
const diagnosis = row[diagnosisCol.index]?.toString().trim();
if (diagnosis && diagnosis.length > 0) {
// Basic ICD-10 format check (letter followed by 2-3 digits, optional decimal and more digits)
const icd10Pattern = /^[A-Z]\d{2}(\.\d{1,4})?$/i;
// Basic ICD-9 format check (3-5 digits, optional decimal)
const icd9Pattern = /^\d{3}(\.\d{1,2})?$/;
if (!icd10Pattern.test(diagnosis) &&
!icd9Pattern.test(diagnosis) &&
!/^[A-Z]/.test(diagnosis)) {
return {
ruleId: 'medical_code_format',
passed: false,
rowIndex,
values: { diagnosis },
issue: `Medical code '${diagnosis}' doesn't match standard ICD-9/ICD-10 format`,
severity: 'low',
};
}
}
return null;
},
});
}
// Additional enhanced validation rules
this.addEducationalValidationRules();
this.addDataIntegrityRules();
this.addStringFormatConsistencyRules();
this.addBusinessDomainRules();
}
/**
* Educational Domain Validation Rules
*/
addEducationalValidationRules() {
const gradeCol = this.findColumnsByPattern(/(grade|gpa|score|mark)/i)[0];
const ageCol = this.findColumnsByPattern(/^age$/i)[0];
const graduationCol = this.findColumnsByPattern(/(graduation|graduate|completed)/i)[0];
const enrollmentCol = this.findColumnsByPattern(/(enrollment|enroll|start)/i)[0];
// GPA Range Validation
if (gradeCol) {
this.rules.push({
id: 'gpa_range_validation',
name: 'GPA Range Validation',
description: 'GPA/Grade should be within valid academic range',
type: 'business_logic',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const grade = this.parseNumber(row[gradeCol.index]);
if (grade !== null) {
// Common GPA scales: 0-4.0, 0-5.0, 0-100 percentage
const isValidGPA = (grade >= 0 && grade <= 4.0) ||
(grade >= 0 && grade <= 5.0) ||
(grade >= 0 && grade <= 100);
if (!isValidGPA || grade < 0) {
return {
ruleId: 'gpa_range_validation',
passed: false,
rowIndex,
values: { [gradeCol.name]: row[gradeCol.index] },
issue: `Grade/GPA (${grade}) outside valid academic range (0-4.0, 0-5.0, or 0-100)`,
severity: 'medium',
};
}
}
return null;
},
});
}
// Age vs Academic Year Validation
if (ageCol && gradeCol) {
this.rules.push({
id: 'age_academic_consistency',
name: 'Age vs Academic Performance Consistency',
description: 'Student age should be appropriate for academic context',
type: 'cross_field',
severity: 'low',
enabled: true,
validate: (row, _headers, rowIndex) => {
const age = this.parseNumber(row[ageCol.index]);
const grade = this.parseNumber(row[gradeCol.index]);
if (age !== null && grade !== null) {
// Very basic age validation for students
if (age < 3 || age > 100) {
return {
ruleId: 'age_academic_consistency',
passed: false,
rowIndex,
values: { age: row[ageCol.index], grade: row[gradeCol.index] },
issue: `Student age (${age}) seems unrealistic for academic context`,
severity: 'low',
};
}
}
return null;
},
});
}
// Graduation vs Enrollment Date Logic
if (graduationCol && enrollmentCol) {
this.rules.push({
id: 'enrollment_graduation_chronology',
name: 'Enrollment vs Graduation Chronology',
description: 'Graduation date should be after enrollment date',
type: 'cross_field',
severity: 'high',
enabled: true,
validate: (row, _headers, rowIndex) => {
const enrollmentDate = this.parseDate(row[enrollmentCol.index] || '');
const graduationDate = this.parseDate(row[graduationCol.index] || '');
if (enrollmentDate && graduationDate) {
if (graduationDate <= enrollmentDate) {
return {
ruleId: 'enrollment_graduation_chronology',
passed: false,
rowIndex,
values: {
enrollment: row[enrollmentCol.index],
graduation: row[graduationCol.index],
},
issue: `Graduation date (${row[graduationCol.index]}) should be after enrollment date (${row[enrollmentCol.index]})`,
severity: 'high',
};
}
// Check for unrealistic graduation timeline (too fast or too slow)
const timeDiffYears = (graduationDate.getTime() - enrollmentDate.getTime()) /
(365.25 * 24 * 60 * 60 * 1000);
if (timeDiffYears < 0.5 || timeDiffYears > 10) {
return {
ruleId: 'enrollment_graduation_chronology',
passed: false,
rowIndex,
values: {
enrollment: row[enrollmentCol.index],
graduation: row[graduationCol.index],
},
issue: `Time between enrollment and graduation (${timeDiffYears.toFixed(1)} years) seems unrealistic`,
severity: 'medium',
};
}
}
return null;
},
});
}
}
/**
* Data Integrity and Consistency Rules
*/
addDataIntegrityRules() {
// Percentage fields validation
const percentageColumns = this.findColumnsByPattern(/(percent|rate|ratio|%)/i);
percentageColumns.forEach((col) => {
this.rules.push({
id: `percentage_range_${col.name}`,
name: 'Percentage Range Validation',
description: `${col.name} should be between 0 and 100`,
type: 'business_logic',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const value = this.parseNumber(row[col.index]);
if (value !== null) {
if (value < 0 || value > 100) {
return {
ruleId: `percentage_range_${col.name}`,
passed: false,
rowIndex,
values: { [col.name]: row[col.index] },
issue: `Percentage value (${value}) in ${col.name} outside valid range (0-100)`,
severity: 'medium',
};
}
}
return null;
},
});
});
// ID field consistency (should not be negative)
const idColumns = this.findColumnsByPattern(/(^id$|_id$|^.*id$)/i);
idColumns.forEach((col) => {
this.rules.push({
id: `id_positive_${col.name}`,
name: 'ID Field Positive Validation',
description: `${col.name} should be positive`,
type: 'business_logic',
severity: 'high',
enabled: true,
validate: (row, _headers, rowIndex) => {
const value = this.parseNumber(row[col.index]);
if (value !== null && value <= 0) {
return {
ruleId: `id_positive_${col.name}`,
passed: false,
rowIndex,
values: { [col.name]: row[col.index] },
issue: `ID field ${col.name} should be positive (found: ${value})`,
severity: 'high',
};
}
return null;
},
});
});
// Count/Quantity field validation
const countColumns = this.findColumnsByPattern(/(count|quantity|number|amount)/i);
countColumns.forEach((col) => {
this.rules.push({
id: `count_non_negative_${col.name}`,
name: 'Count Field Non-Negative Validation',
description: `${col.name} should not be negative`,
type: 'business_logic',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const value = this.parseNumber(row[col.index]);
if (value !== null && value < 0) {
return {
ruleId: `count_non_negative_${col.name}`,
passed: false,
rowIndex,
values: { [col.name]: row[col.index] },
issue: `Count/quantity field ${col.name} should not be negative (found: ${value})`,
severity: 'medium',
};
}
return null;
},
});
});
}
/**
* String Format Consistency Rules
*/
addStringFormatConsistencyRules() {
// Email format validation
const emailColumns = this.findColumnsByPattern(/(email|mail)/i);
emailColumns.forEach((col) => {
this.rules.push({
id: `email_format_${col.name}`,
name: 'Email Format Validation',
description: `${col.name} should contain valid email format`,
type: 'business_logic',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const email = row[col.index]?.toString().trim();
if (email && email.length > 0) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) {
return {
ruleId: `email_format_${col.name}`,
passed: false,
rowIndex,
values: { [col.name]: email },
issue: `Invalid email format: '${email}'`,
severity: 'medium',
};
}
}
return null;
},
});
});
// Phone number format validation
const phoneColumns = this.findColumnsByPattern(/(phone|tel|mobile|contact)/i);
phoneColumns.forEach((col) => {
this.rules.push({
id: `phone_format_${col.name}`,
name: 'Phone Number Format Validation',
description: `${col.name} should contain valid phone format`,
type: 'business_logic',
severity: 'low',
enabled: true,
validate: (row, _headers, rowIndex) => {
const phone = row[col.index]?.toString().trim();
if (phone && phone.length > 0) {
// Basic phone pattern (allows various formats)
const phonePattern = /^[\+]?[\d\s\-\(\)]{7,}$/;
if (!phonePattern.test(phone)) {
return {
ruleId: `phone_format_${col.name}`,
passed: false,
rowIndex,
values: { [col.name]: phone },
issue: `Invalid phone number format: '${phone}'`,
severity: 'low',
};
}
}
return null;
},
});
});
// URL format validation
const urlColumns = this.findColumnsByPattern(/(url|website|link|uri)/i);
urlColumns.forEach((col) => {
this.rules.push({
id: `url_format_${col.name}`,
name: 'URL Format Validation',
description: `${col.name} should contain valid URL format`,
type: 'business_logic',
severity: 'low',
enabled: true,
validate: (row, _headers, rowIndex) => {
const url = row[col.index]?.toString().trim();
if (url && url.length > 0) {
const urlPattern = /^https?:\/\/[^\s]+$/;
if (!urlPattern.test(url)) {
return {
ruleId: `url_format_${col.name}`,
passed: false,
rowIndex,
values: { [col.name]: url },
issue: `Invalid URL format: '${url}' (should start with http:// or https://)`,
severity: 'low',
};
}
}
return null;
},
});
});
}
/**
* Business Domain Specific Rules
*/
addBusinessDomainRules() {
// Product/Inventory validation
const skuCol = this.findColumnsByPattern(/(sku|product.*code|item.*code)/i)[0];
const stockCol = this.findColumnsByPattern(/(stock|inventory|quantity.*hand)/i)[0];
const priceCol = this.findColumnsByPattern(/(price|cost|amount)/i)[0];
if (skuCol && stockCol) {
this.rules.push({
id: 'product_stock_consistency',
name: 'Product Stock Consistency',
description: 'Product with SKU should have valid stock quantity',
type: 'cross_field',
severity: 'medium',
enabled: true,
validate: (row, _headers, rowIndex) => {
const sku = row[skuCol.index]?.toString().trim();
const stock = this.parseNumber(row[stockCol.index]);
if (sku && sku.length > 0 && stock !== null && stock < 0) {
return {
ruleId: 'product_stock_consistency',
passed: false,
rowIndex,
values: { sku, stock: row[stockCol.index] },
issue: `Product ${sku} has negative stock quantity (${stock})`,
severity: 'medium',
};
}
return null;
},
});
}
if (priceCol && stockCol) {
this.rules.push({
id: 'price_stock_business_logic',
name: 'Price Stock Business Logic',
description: 'Products with price should have reasonable stock levels',
type: 'business_logic',
severity: 'low',
enabled: true,
validate: (row, _headers, rowIndex) => {
const price = this.parseNumber(row[priceCol.index]);
const stock = this.parseNumber(row[stockCol.index]);
if (price !== null && stock !== null) {
// Business rule: High-value items (>$1000) with zero stock might need attention
if (price > 1000 && stock === 0) {
return {
ruleId: 'price_stock_business_logic',
passed: false,
rowIndex,
values: { price: row[priceCol.index], stock: row[stockCol.index] },
issue: `High-value item (${price}) has zero stock - potential business impact`,
severity: 'low',
};
}
}
return null;
},
});
}
// Customer relationship validation
const customerIdCol = this.findColumnsByPattern(/(customer.*id|client.*id)/i)[0];
const orderCol = this.findColumnsByPattern(/(order|purchase|transaction)/i)[0];
const statusCol = this.findColumnsByPattern(/(status|state)/i)[0];
if (customerIdCol && orderCol) {
this.rules.push({
id: 'customer_order_relationship',
name: 'Customer Order Relationship',
description: 'Order should have valid customer ID',
type: 'cross_field',
severity: 'high',
enabled: true,
validate: (row, _headers, rowIndex) => {
const customerId = row[customerIdCol.index]?.toString().trim();
const order = row[orderCol.index]?.toString().trim();
if (order && order.length > 0 && (!customerId || customerId.length === 0)) {
return {
ruleId: 'customer_order_relationship',
passed: false,
rowIndex,
values: { customerId, order },
issue: `Order '${order}' exists but customer ID is missing`,
severity: 'high',
};
}
return null;
},
});
}
// Status field consistency
if (statusCol) {
this.rules.push({
id: 'status_field_standardization',
name: 'Status Field Standardization',
description: 'Status values should follow consistent format',
type: 'business_logic',
severity: 'low',
enabled: true,
validate: (row, _headers, rowIndex) => {
const status = row[statusCol.index]?.toString().trim();
if (status && status.length > 0) {
// Check for common status inconsistencies
const hasInconsistentCasing = status !== status.toLowerCase() && status !== status.toUpperCase();
const hasSpecialChars = /[^a-zA-Z0-9\s\-_]/.test(status);
if (hasInconsistentCasing && hasSpecialChars) {
return {
ruleId: 'status_field_standardization',
passed: false,
rowIndex,
values: { status },
issue: `Status '${status}' has inconsistent format (mixed case and special characters)`,
severity: 'low',
};
}
}
return null;
},
});
}
}
findColumnsByPattern(pattern) {
return this.headers
.map((header, index) => ({ name: header, index }))
.filter((col) => pattern.test(col.name));
}
parseDate(value) {
if (!value || typeof value !== 'string')
return null;
const date = new Date(value);
return isNaN(date.getTime()) ? null : date;
}
parseNumber(value) {
if (!value)
return null;
const num = Number(value);
return isNaN(num) ? null : num;
}
generateValidationReport() {
// Group violations by rule ID
const violationsByRule = new Map();
for (const violation of this.violations) {
if (!violationsByRule.has(violation.ruleId)) {
violationsByRule.set(violation.ruleId, []);
}
violationsByRule.get(violation.ruleId).push(violation);
}
const crossFieldValidations = [];
const intraRecordConsistency = [];
for (const [ruleId, violations] of violationsByRule) {
const rule = this.rules.find((r) => r.id === ruleId);
if (!rule)
continue;
const examples = violations.slice(0, 5).map((v) => ({
rowIndex: v.rowIndex,
values: v.values,
issue: v.issue,
}));
if (rule.type === 'cross_field' || rule.type === 'business_logic') {
crossFieldValidations.push({
ruleId,
description: rule.description,
violations: violations.length,
examples,
});
}
else if (rule.type === 'intra_record') {
intraRecordConsistency.push({
ruleDescription: rule.description,
violatingRecords: violations.length,
examples: examples.map((ex) => ({
rowIndex: ex.rowIndex,
inconsistentValues: ex.values,
issue: ex.issue,
})),
});
}
}
const criticalViolations = this.violations.filter((v) => v.severity === 'critical' || v.severity === 'high').length;
return {
crossFieldValidations,
intraRecordConsistency,
totalViolations: this.violations.length,
criticalViolations,
};
}
getViolationSummary() {
const violationsBySeverity = {
critical: 0,
high: 0,
medium: 0,
low: 0,
};
const violationsByType = {
cross_field: 0,
intra_record: 0,
business_logic: 0,
};
for (const violation of this.violations) {
violationsBySeverity[violation.severity]++;
const rule = this.rules.find((r) => r.id === violation.ruleId);
if (rule) {
violationsByType[rule.type]++;
}
}
return {
totalRulesEvaluated: this.rules.length,
totalViolations: this.violations.length,
violationsBySeverity,
violationsByType,
};
}
}
exports.BusinessRuleEngine = BusinessRuleEngine;
//# sourceMappingURL=business-rule-engine.js.map