Patrones de Desarrollo de CDSS en Salud
Patrones para construir Sistemas de Apoyo a la Decisión Clínica (CDSS) que se integren en los flujos de trabajo de la HCE. Los módulos CDSS son críticos para la seguridad del paciente — tolerancia cero a falsos negativos.
Cuándo Usar
- Implementar verificación de interacciones farmacológicas
- Construir motores de validación de dosis
- Implementar sistemas de puntuación clínica (NEWS2, qSOFA, APACHE, GCS)
- Diseñar sistemas de alertas para valores clínicos anormales
- Construir entrada de órdenes de medicación con controles de seguridad
- Integrar interpretación de resultados de laboratorio con contexto clínico
Cómo Funciona
El motor CDSS es una biblioteca de funciones puras sin efectos secundarios. Ingrese datos clínicos, obtenga alertas. Esto lo hace completamente testeable.
Tres módulos principales:
checkInteractions(nuevoFarmaco, medicacionActual, alergias)— Verifica un nuevo fármaco contra la medicación actual y las alergias conocidas. DevuelveInteractionAlert[]ordenados por severidad. Utiliza el modelo de datosDrugInteractionPair.validateDose(farmaco, dosis, via, peso, edad, funcionRenal)— Valida una dosis prescrita según reglas basadas en peso, edad y función renal. DevuelveDoseValidationResult.calculateNEWS2(signosVitales)— National Early Warning Score 2 desdeNEWS2Input. DevuelveNEWS2Resultcon puntuación total, nivel de riesgo y guía de escalamiento.
Interfaz HCE
↓ (el usuario ingresa datos)
Motor CDSS (funciones puras, sin efectos secundarios)
├── Verificador de Interacciones Farmacológicas
├── Validador de Dosis
├── Puntuación Clínica (NEWS2, qSOFA, etc.)
└── Clasificador de Alertas
↓ (devuelve alertas)
Interfaz HCE (muestra alertas en línea, bloquea si es crítico)
Verificación de Interacciones Farmacológicas
interface DrugInteractionPair {
drugA: string; // nombre genérico
drugB: string; // nombre genérico
severity: 'critical' | 'major' | 'minor';
mechanism: string;
clinicalEffect: string;
recommendation: string;
}
function checkInteractions(
newDrug: string,
currentMedications: string[],
allergyList: string[]
): InteractionAlert[] {
if (!newDrug) return [];
const alerts: InteractionAlert[] = [];
for (const current of currentMedications) {
const interaction = findInteraction(newDrug, current);
if (interaction) {
alerts.push({ severity: interaction.severity, pair: [newDrug, current],
message: interaction.clinicalEffect, recommendation: interaction.recommendation });
}
}
for (const allergy of allergyList) {
if (isCrossReactive(newDrug, allergy)) {
alerts.push({ severity: 'critical', pair: [newDrug, allergy],
message: `Reactividad cruzada con alergia documentada: ${allergy}`,
recommendation: 'No prescribir sin consulta de alergología' });
}
}
return alerts.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
}
Los pares de interacción deben ser bidireccionales: si el fármaco A interactúa con el fármaco B, entonces el fármaco B interactúa con el fármaco A.
Validación de Dosis
interface DoseValidationResult {
valid: boolean;
message: string;
suggestedRange: { min: number; max: number; unit: string } | null;
factors: string[];
}
function validateDose(
drug: string,
dose: number,
route: 'oral' | 'iv' | 'im' | 'sc' | 'topical',
patientWeight?: number,
patientAge?: number,
renalFunction?: number
): DoseValidationResult {
const rules = getDoseRules(drug, route);
if (!rules) return { valid: true, message: 'No hay reglas de validación disponibles', suggestedRange: null, factors: [] };
const factors: string[] = [];
// SEGURIDAD: si las reglas requieren peso pero el peso falta, BLOQUEAR (no permitir)
if (rules.weightBased) {
if (!patientWeight || patientWeight <= 0) {
return { valid: false, message: `Peso requerido para ${drug} (fármaco mg/kg)`,
suggestedRange: null, factors: ['weight_missing'] };
}
factors.push('weight');
const maxDose = rules.maxPerKg * patientWeight;
if (dose > maxDose) {
return { valid: false, message: `Dosis excede el máximo para ${patientWeight}kg`,
suggestedRange: { min: rules.minPerKg * patientWeight, max: maxDose, unit: rules.unit }, factors };
}
}
// Ajuste por edad (cuando las reglas definen rangos de edad y se proporciona edad)
if (rules.ageAdjusted && patientAge !== undefined) {
factors.push('age');
const ageMax = rules.getAgeAdjustedMax(patientAge);
if (dose > ageMax) {
return { valid: false, message: `Excede el máximo ajustado por edad para ${patientAge}años`,
suggestedRange: { min: rules.typicalMin, max: ageMax, unit: rules.unit }, factors };
}
}
// Ajuste renal (cuando las reglas definen rangos de eGFR y se proporciona eGFR)
if (rules.renalAdjusted && renalFunction !== undefined) {
factors.push('renal');
const renalMax = rules.getRenalAdjustedMax(renalFunction);
if (dose > renalMax) {
return { valid: false, message: `Excede el máximo ajustado renal para eGFR ${renalFunction}`,
suggestedRange: { min: rules.typicalMin, max: renalMax, unit: rules.unit }, factors };
}
}
// Máximo absoluto
if (dose > rules.absoluteMax) {
return { valid: false, message: `Excede el máximo absoluto ${rules.absoluteMax}${rules.unit}`,
suggestedRange: { min: rules.typicalMin, max: rules.absoluteMax, unit: rules.unit },
factors: [...factors, 'absolute_max'] };
}
return { valid: true, message: 'Dentro del rango',
suggestedRange: { min: rules.typicalMin, max: rules.typicalMax, unit: rules.unit }, factors };
}
Puntuación Clínica: NEWS2
interface NEWS2Input {
respiratoryRate: number; oxygenSaturation: number; supplementalOxygen: boolean;
temperature: number; systolicBP: number; heartRate: number;
consciousness: 'alert' | 'voice' | 'pain' | 'unresponsive';
}
interface NEWS2Result {
total: number; // 0-20
risk: 'low' | 'low-medium' | 'medium' | 'high';
components: Record<string, number>;
escalation: string;
}
Las tablas de puntuación deben coincidir exactamente con la especificación del Royal College of Physicians.
Severidad de Alerta y Comportamiento de UI
| Severidad | Comportamiento de UI | Acción Clínica Requerida |
|---|---|---|
| Crítico | Bloquear acción. Modal no descartable. Rojo. | Debe documentar motivo de anulación para continuar |
| Mayor | Banner de advertencia en línea. Naranja. | Debe confirmar antes de proceder |
| Menor | Nota informativa en línea. Amarillo. | Solo concientización, no se requiere acción |
Las alertas críticas NUNCA deben auto-descartarse ni implementarse como notificaciones toast. Los motivos de anulación deben almacenarse en la traza de auditoría.
Pruebas de CDSS (Tolerancia Cero a Falsos Negativos)
describe('CDSS — Seguridad del Paciente', () => {
INTERACTION_PAIRS.forEach(({ drugA, drugB, severity }) => {
it(`detecta ${drugA} + ${drugB} (${severity})`, () => {
const alerts = checkInteractions(drugA, [drugB], []);
expect(alerts.length).toBeGreaterThan(0);
expect(alerts[0].severity).toBe(severity);
});
it(`detecta ${drugB} + ${drugA} (inverso)`, () => {
const alerts = checkInteractions(drugB, [drugA], []);
expect(alerts.length).toBeGreaterThan(0);
});
});
it('bloquea fármaco mg/kg cuando falta el peso', () => {
const result = validateDose('gentamicin', 300, 'iv');
expect(result.valid).toBe(false);
expect(result.factors).toContain('weight_missing');
});
it('maneja datos de fármaco mal formados con gracia', () => {
expect(() => checkInteractions('', [], [])).not.toThrow();
});
});
Criterio de aprobación: 100%. Una sola interacción no detectada es un evento de seguridad del paciente.
Antipatrones
- Hacer las comprobaciones CDSS opcionales o saltables sin motivo documentado
- Implementar comprobaciones de interacción como notificaciones toast
- Usar tipos
anypara datos de fármacos o clínicos - Codificar pares de interacción en lugar de usar una estructura de datos mantenible
- Capturar errores silenciosamente en el motor CDSS (debe mostrar fallos de forma evidente)
- Omitir la validación basada en peso cuando el peso no está disponible (debe bloquear, no permitir)
Ejemplos
Ejemplo 1: Verificación de Interacción Farmacológica
const alerts = checkInteractions('warfarin', ['aspirin', 'metformin'], ['penicillin']);
// [{ severity: 'critical', pair: ['warfarin', 'aspirin'],
// message: 'Aumento del riesgo de sangrado', recommendation: 'Evitar la combinación' }]
Ejemplo 2: Validación de Dosis
const ok = validateDose('paracetamol', 1000, 'oral', 70, 45);
// { valid: true, suggestedRange: { min: 500, max: 4000, unit: 'mg' } }
const bad = validateDose('paracetamol', 5000, 'oral', 70, 45);
// { valid: false, message: 'Excede el máximo absoluto 4000mg' }
const noWeight = validateDose('gentamicin', 300, 'iv');
// { valid: false, factors: ['weight_missing'] }
Ejemplo 3: Puntuación NEWS2
const result = calculateNEWS2({
respiratoryRate: 24, oxygenSaturation: 93, supplementalOxygen: true,
temperature: 38.5, systolicBP: 100, heartRate: 110, consciousness: 'voice'
});
// { total: 13, risk: 'high', escalation: 'Revisión clínica urgente. Considerar UCI.' }