MaternAlert / src /ocr /validators /rules.ts
Auspicious14's picture
feat(ocr, risk): add patient OCR and risk assessment
27e706d
Raw
History Blame Contribute Delete
9.48 kB
import { ValidationWarning } from "./types";
import { ExtractedPatientData } from "../interfaces/extracted-patient.interface";
import {
normalizePhoneNumber,
normalizeName,
normalizeDate,
normalizeNumeric,
calculateBMI,
} from "../utils/smart-corrections";
const BLOOD_GROUPS = [
"A_POSITIVE",
"A_NEGATIVE",
"B_POSITIVE",
"B_NEGATIVE",
"AB_POSITIVE",
"AB_NEGATIVE",
"O_POSITIVE",
"O_NEGATIVE",
"A+",
"A-",
"B+",
"B-",
"AB+",
"AB-",
"O+",
"O-",
];
const GENOTYPES = ["AA", "AS", "AC", "SS", "SC", "CC"];
const NIGERIAN_PHONE_REGEX = /^(\+234|0)?[789]\d{9}$/;
export const validateGestationalAge = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.gestationalAge.value === null) return null;
const ga = data.gestationalAge.value;
if (ga < 4 || ga > 42) {
return {
field: "gestationalAge",
message: `Gestational age (${ga} weeks) outside expected range (4-42 weeks).`,
severity: "error",
};
}
return null;
};
export const validateAge = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.age.value === null) return null;
const age = data.age.value;
if (age < 10 || age > 70) {
return {
field: "age",
message:
age < 10 ? "Age appears too young for pregnancy." : "Age appears very high for pregnancy.",
severity: "warning",
};
}
return null;
};
export const validateGravidityParity = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.gravidity.value === null || data.parity.value === null) return null;
if (data.gravidity.value < data.parity.value) {
return {
field: "gravidity",
message: "Gravidity cannot be less than parity.",
severity: "error",
};
}
if (data.age.value !== null && data.age.value <= 15 && data.gravidity.value >= 8) {
return {
field: "gravidity",
message: "Gravidity appears unusually high for this age.",
severity: "warning",
};
}
return null;
};
export const validateLivingChildren = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.livingChildren.value === null || data.parity.value === null) return null;
if (data.livingChildren.value > data.parity.value) {
return {
field: "livingChildren",
message: "Living children cannot exceed parity.",
severity: "error",
};
}
return null;
};
export const validatePreviousCaesareans = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.previousCaesareans.value === null || data.parity.value === null) return null;
if (data.previousCaesareans.value > 0 && data.parity.value === 0) {
return {
field: "previousCaesareans",
message: "Previous caesareans cannot be greater than zero if parity is zero.",
severity: "error",
};
}
return null;
};
export const validateBloodGroup = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.bloodGroup.value === null) return null;
const bg = data.bloodGroup.value.toUpperCase().trim();
if (!BLOOD_GROUPS.includes(bg)) {
return {
field: "bloodGroup",
message: `Blood group "${bg}" is not recognized.`,
severity: "warning",
};
}
return null;
};
export const validateGenotype = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.genotype.value === null) return null;
const gt = data.genotype.value.toUpperCase().trim();
if (!GENOTYPES.includes(gt)) {
return {
field: "genotype",
message: `Genotype "${gt}" is not recognized.`,
severity: "warning",
};
}
return null;
};
export const validateLastMenstrualPeriod = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.lastMenstrualPeriod.value === null) return null;
try {
const lmp = new Date(data.lastMenstrualPeriod.value);
const today = new Date();
if (lmp > today) {
return {
field: "lastMenstrualPeriod",
message: "Last menstrual period cannot be in the future.",
severity: "error",
};
}
} catch (e) {
return {
field: "lastMenstrualPeriod",
message: "Unable to parse last menstrual period date.",
severity: "warning",
};
}
return null;
};
export const validateEstimatedDeliveryDate = (
data: ExtractedPatientData
): ValidationWarning | null => {
if (data.estimatedDeliveryDate.value === null) return null;
try {
const edd = new Date(data.estimatedDeliveryDate.value);
const today = new Date();
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
if (edd < sixMonthsAgo) {
return {
field: "estimatedDeliveryDate",
message: "Estimated delivery date appears to be several months in the past.",
severity: "warning",
};
}
} catch (e) {
return {
field: "estimatedDeliveryDate",
message: "Unable to parse estimated delivery date.",
severity: "warning",
};
}
return null;
};
export const validateLmpAndGestationalAge = (
data: ExtractedPatientData
): ValidationWarning | null => {
if (data.lastMenstrualPeriod.value === null || data.gestationalAge.value === null) return null;
try {
const lmp = new Date(data.lastMenstrualPeriod.value);
const today = new Date();
const diffMs = today.getTime() - lmp.getTime();
const diffWeeks = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 7));
if (Math.abs(diffWeeks - data.gestationalAge.value) > 4) {
return {
field: "gestationalAge",
message: "Gestational age and last menstrual period do not match.",
severity: "warning",
};
}
} catch {
return null;
}
return null;
};
export const validatePhoneNumber = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.phone.value === null) return null;
const original = data.phone.value;
const normalized = normalizePhoneNumber(original);
if (!NIGERIAN_PHONE_REGEX.test(normalized)) {
return {
field: "phone",
message: "Phone number does not match expected Nigerian format.",
severity: "warning",
suggestion: normalized !== original ? normalized : undefined,
};
}
if (original !== normalized) {
return {
field: "phone",
message: "Possible OCR error detected in phone number.",
severity: "info",
suggestion: normalized,
};
}
return null;
};
export const validateBloodPressure = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.bloodPressure.value === null) return null;
const bp = data.bloodPressure.value;
const match = bp.match(/(\d+)\s*\/\s*(\d+)/);
if (!match) {
return {
field: "bloodPressure",
message: "Unable to parse blood pressure.",
severity: "warning",
};
}
const systolic = parseInt(match[1], 10);
const diastolic = parseInt(match[2], 10);
if (systolic < 70 || systolic > 200 || diastolic < 40 || diastolic > 130) {
return {
field: "bloodPressure",
message: `Blood pressure (${systolic}/${diastolic}) appears outside expected range.`,
severity: "error",
};
}
return null;
};
export const validateWeight = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.weight.value === null) return null;
const weight = data.weight.value;
if (weight < 30 || weight > 200) {
return {
field: "weight",
message: `Weight (${weight} kg) appears outside expected range.`,
severity: "warning",
};
}
return null;
};
export const validateHeight = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.height.value === null) return null;
const height = data.height.value;
if (height < 120 || height > 220) {
return {
field: "height",
message: `Height (${height} cm) appears outside expected range.`,
severity: "warning",
};
}
return null;
};
export const validateBMI = (data: ExtractedPatientData): ValidationWarning | null => {
if (data.weight.value === null || data.height.value === null) return null;
const calculatedBMI = calculateBMI(data.weight.value, data.height.value);
if (data.bmi.value !== null && Math.abs(data.bmi.value - calculatedBMI) > 2) {
return {
field: "bmi",
message: `BMI (${data.bmi.value}) differs from calculated BMI (${calculatedBMI}).`,
severity: "warning",
suggestion: String(calculatedBMI),
};
}
return null;
};
export const validateConfidence = (data: ExtractedPatientData): ValidationWarning[] => {
const warnings: ValidationWarning[] = [];
const fields = Object.keys(data) as Array<keyof ExtractedPatientData>;
fields.forEach((field) => {
const fieldData = data[field];
if (
typeof fieldData === "object" &&
fieldData !== null &&
"confidence" in fieldData &&
fieldData.confidence < 0.5
) {
warnings.push({
field: String(field),
message: `Low OCR confidence (${Math.round(fieldData.confidence * 100)}%).`,
severity: "warning",
});
}
});
return warnings;
};
export const ALL_RULES = [
validateGestationalAge,
validateAge,
validateGravidityParity,
validateLivingChildren,
validatePreviousCaesareans,
validateBloodGroup,
validateGenotype,
validateLastMenstrualPeriod,
validateEstimatedDeliveryDate,
validateLmpAndGestationalAge,
validatePhoneNumber,
validateBloodPressure,
validateWeight,
validateHeight,
validateBMI,
];