- ✅ Ticket 1.1: Estructura Clean Architecture en backend - ✅ Ticket 1.2: Schemas Zod compartidos - ✅ Ticket 1.3: Refactorización drugs.ts (1362 → 8 archivos modulares) - ✅ Ticket 1.4: Refactorización procedures.ts (3583 → 6 archivos modulares) - ✅ Ticket 1.5: Eliminación de duplicidades (~50 líneas) Cambios principales: - Creada estructura Clean Architecture en backend/src/ - Schemas Zod compartidos en backend/src/shared/schemas/ - Refactorización modular de drugs y procedures - Utilidades genéricas en src/utils/ (filter, validation) - Eliminados scripts obsoletos y documentación antigua - Corregidos errores: QueryClient, import test-error-handling - Build verificado y funcionando correctamente
12 KiB
12 KiB
⚠️ Errores Críticos Médicos - Sistema de Bloqueo
🎯 Objetivo
Definir qué errores médicos son críticos y deben bloquear acciones en la aplicación.
🚨 Errores Críticos que Bloquean Acciones
1. Errores de Dosis
❌ BLOQUEA: Dosis fuera de rango seguro
- Condición: Dosis < mínimo terapéutico O dosis > máximo seguro
- Acción: Bloquear administración, mostrar error crítico
- Ejemplo: Adrenalina 10mg IV (máximo seguro: 1mg)
❌ BLOQUEA: Dosis letal
- Condición: Dosis que puede causar muerte o daño grave
- Acción: Bloquear completamente, alerta de emergencia
- Ejemplo: Adrenalina 1:1000 IV en lugar de 1:10.000
❌ BLOQUEA: Vía de administración incorrecta
- Condición: Vía no indicada para el fármaco
- Acción: Bloquear administración
- Ejemplo: Adrenalina IM en lugar de IV para PCR
⚠️ ADVIERTE (no bloquea): Dosis en límites
- Condición: Dosis en límite superior/inferior del rango
- Acción: Mostrar advertencia, requerir confirmación
- Ejemplo: Dosis máxima recomendada
2. Errores de Protocolos
❌ BLOQUEA: Paso crítico omitido
- Condición: Paso marcado como crítico no ejecutado
- Acción: Bloquear continuación del protocolo
- Ejemplo: No iniciar compresiones en RCP
❌ BLOQUEA: Orden de pasos incorrecto
- Condición: Pasos ejecutados fuera de orden crítico
- Acción: Bloquear continuación, requerir corrección
- Ejemplo: Desfibrilar antes de iniciar compresiones
❌ BLOQUEA: Prerequisito no cumplido
- Condición: Protocolo prerequisito no ejecutado
- Acción: Bloquear inicio del protocolo
- Ejemplo: Intentar SVA sin haber iniciado SVB
⚠️ ADVIERTE (no bloquea): Paso opcional omitido
- Condición: Paso opcional no ejecutado
- Acción: Mostrar advertencia, permitir continuar
- Ejemplo: No usar cánula orofaríngea si no está disponible
3. Errores de Contenido Médico
❌ BLOQUEA: Contenido no validado médicamente
- Condición: Intentar publicar contenido sin validación médica
- Acción: Bloquear publicación
- Ejemplo: Publicar protocolo sin revisión médica
❌ BLOQUEA: Contenido con errores críticos identificados
- Condición: Contenido rechazado por revisor médico con errores críticos
- Acción: Bloquear publicación hasta corrección
- Ejemplo: Dosis incorrectas en vademécum
⚠️ ADVIERTE (no bloquea): Contenido desactualizado
- Condición: Contenido sin actualizar >2 años
- Acción: Mostrar advertencia de desactualización
- Ejemplo: Protocolo basado en guías antiguas
4. Errores de Paciente/Contexto
❌ BLOQUEA: Peso inválido para edad
- Condición: Peso fuera de rango fisiológico para edad
- Acción: Bloquear cálculo de dosis, requerir verificación
- Ejemplo: Niño de 5 años con peso de 100kg
❌ BLOQUEA: Edad incompatible con protocolo
- Condición: Protocolo no aplicable a grupo de edad
- Acción: Bloquear ejecución del protocolo
- Ejemplo: Protocolo pediátrico aplicado a adulto
⚠️ ADVIERTE (no bloquea): Peso fuera de percentiles normales
- Condición: Peso fuera de percentiles 5-95 para edad
- Acción: Mostrar advertencia, permitir continuar con confirmación
- Ejemplo: Peso muy bajo o muy alto pero posible
🏗️ Implementación
Domain Layer
// domain/value-objects/CriticalError.ts
export class CriticalError {
private constructor(
readonly code: string,
readonly message: string,
readonly severity: 'blocking' | 'warning',
readonly category: 'dose' | 'protocol' | 'content' | 'patient'
) {}
static createBlockingDoseError(
code: string,
message: string
): CriticalError {
return new CriticalError(code, message, 'blocking', 'dose');
}
static createBlockingProtocolError(
code: string,
message: string
): CriticalError {
return new CriticalError(code, message, 'blocking', 'protocol');
}
static createWarning(
code: string,
message: string,
category: CriticalError['category']
): CriticalError {
return new CriticalError(code, message, 'warning', category);
}
isBlocking(): boolean {
return this.severity === 'blocking';
}
}
// domain/services/CriticalErrorDetector.ts
export class CriticalErrorDetector {
detectDoseErrors(
dose: number,
doseRange: DoseRange,
drug: Drug,
route: AdministrationRoute
): CriticalError[] {
const errors: CriticalError[] = [];
// Dosis fuera de rango
if (!doseRange.isValid(dose)) {
if (dose < doseRange.min) {
errors.push(CriticalError.createBlockingDoseError(
'DOSE_BELOW_MINIMUM',
`Dosis ${dose}${doseRange.unit} está por debajo del mínimo terapéutico (${doseRange.min}${doseRange.unit})`
));
} else {
errors.push(CriticalError.createBlockingDoseError(
'DOSE_ABOVE_MAXIMUM',
`Dosis ${dose}${doseRange.unit} excede el máximo seguro (${doseRange.max}${doseRange.unit}). RIESGO DE MUERTE.`
));
}
}
// Dosis letal (10x el máximo)
if (dose > doseRange.max * 10) {
errors.push(CriticalError.createBlockingDoseError(
'DOSE_LETHAL',
`Dosis ${dose}${doseRange.unit} es LETAL. Máximo seguro: ${doseRange.max}${doseRange.unit}. BLOQUEAR ADMINISTRACIÓN.`
));
}
// Vía incorrecta
if (!drug.routes.includes(route)) {
errors.push(CriticalError.createBlockingDoseError(
'INVALID_ROUTE',
`Vía ${route} no está indicada para ${drug.genericName}. Vías permitidas: ${drug.routes.join(', ')}`
));
}
// Advertencias
const warningLevel = doseRange.getWarningLevel(dose);
if (warningLevel === 'high') {
errors.push(CriticalError.createWarning(
'DOSE_HIGH_LIMIT',
`Dosis está en el límite superior del rango. Monitorizar efectos adversos.`,
'dose'
));
}
return errors;
}
detectProtocolErrors(
protocol: Protocol,
executedSteps: number[]
): CriticalError[] {
const errors: CriticalError[] = [];
// Pasos críticos omitidos
const criticalSteps = protocol.steps
.filter(s => s.type === 'critical')
.map(s => s.order);
const missingCritical = criticalSteps.filter(
step => !executedSteps.includes(step)
);
if (missingCritical.length > 0) {
errors.push(CriticalError.createBlockingProtocolError(
'CRITICAL_STEPS_MISSING',
`Pasos críticos no ejecutados: ${missingCritical.join(', ')}. No se puede continuar el protocolo.`
));
}
// Orden incorrecto
const sortedExecuted = [...executedSteps].sort((a, b) => a - b);
if (JSON.stringify(executedSteps) !== JSON.stringify(sortedExecuted)) {
errors.push(CriticalError.createBlockingProtocolError(
'STEPS_OUT_OF_ORDER',
`Los pasos se ejecutaron fuera de orden. Orden correcto: ${sortedExecuted.join(' → ')}`
));
}
return errors;
}
detectPatientErrors(
weight: PatientWeight,
age: PatientAge,
protocol: Protocol
): CriticalError[] {
const errors: CriticalError[] = [];
// Peso inválido para edad
const weightValidation = weight.isValidForAge(age);
if (!weightValidation.valid) {
errors.push(CriticalError.createBlockingDoseError(
'INVALID_WEIGHT_FOR_AGE',
weightValidation.warning || 'Peso inválido para la edad del paciente'
));
}
// Protocolo incompatible con edad
if (protocol.ageGroup !== 'todos' && protocol.ageGroup !== age.getAgeGroup()) {
errors.push(CriticalError.createBlockingProtocolError(
'AGE_INCOMPATIBLE',
`Protocolo "${protocol.title}" no es aplicable a grupo de edad ${age.getAgeGroup()}. Protocolo diseñado para: ${protocol.ageGroup}`
));
}
return errors;
}
detectContentErrors(
content: ContentItem,
reviews: MedicalReview[]
): CriticalError[] {
const errors: CriticalError[] = [];
// Contenido no validado
if (content.status !== 'approved' && content.status !== 'published') {
errors.push(CriticalError.createBlockingProtocolError(
'CONTENT_NOT_VALIDATED',
`Contenido "${content.title}" no ha sido validado médicamente. Estado actual: ${content.status}`
));
}
// Contenido rechazado con errores críticos
const rejectedReviews = reviews.filter(r => r.status === 'rejected');
const criticalRejections = rejectedReviews.filter(review =>
review.comments.some(c => c.severity === 'critical')
);
if (criticalRejections.length > 0) {
errors.push(CriticalError.createBlockingProtocolError(
'CONTENT_REJECTED_CRITICAL',
`Contenido rechazado por errores críticos. Debe corregirse antes de publicar.`
));
}
return errors;
}
}
// domain/services/ActionBlocker.ts
export class ActionBlocker {
constructor(private readonly errorDetector: CriticalErrorDetector) {}
async canExecuteAction(
action: 'administer_drug' | 'execute_protocol' | 'publish_content',
context: ActionContext
): Promise<{
allowed: boolean;
blockingErrors: CriticalError[];
warnings: CriticalError[];
}> {
const allErrors: CriticalError[] = [];
switch (action) {
case 'administer_drug':
if (context.dose && context.doseRange && context.drug && context.route) {
const doseErrors = this.errorDetector.detectDoseErrors(
context.dose,
context.doseRange,
context.drug,
context.route
);
allErrors.push(...doseErrors);
}
if (context.weight && context.age) {
const patientErrors = this.errorDetector.detectPatientErrors(
context.weight,
context.age,
context.protocol!
);
allErrors.push(...patientErrors);
}
break;
case 'execute_protocol':
if (context.protocol && context.executedSteps) {
const protocolErrors = this.errorDetector.detectProtocolErrors(
context.protocol,
context.executedSteps
);
allErrors.push(...protocolErrors);
}
break;
case 'publish_content':
if (context.content && context.reviews) {
const contentErrors = this.errorDetector.detectContentErrors(
context.content,
context.reviews
);
allErrors.push(...contentErrors);
}
break;
}
const blockingErrors = allErrors.filter(e => e.isBlocking());
const warnings = allErrors.filter(e => !e.isBlocking());
return {
allowed: blockingErrors.length === 0,
blockingErrors,
warnings
};
}
}
export interface ActionContext {
dose?: number;
doseRange?: DoseRange;
drug?: Drug;
route?: AdministrationRoute;
weight?: PatientWeight;
age?: PatientAge;
protocol?: Protocol;
executedSteps?: number[];
content?: ContentItem;
reviews?: MedicalReview[];
}
✅ Casos de Uso
Caso 1: Bloquear administración de dosis letal
const blocker = new ActionBlocker(errorDetector);
const result = await blocker.canExecuteAction('administer_drug', {
dose: 10, // mg - letal
doseRange: DoseRange.create(0.5, 1, 'mg', 'adulto'),
drug: adrenalina,
route: 'IV',
weight: PatientWeight.fromKg(70),
age: PatientAge.fromYears(35)
});
if (!result.allowed) {
console.error('BLOQUEADO:', result.blockingErrors.map(e => e.message));
// Mostrar alerta crítica al usuario
// Bloquear botón de administración
}
Caso 2: Bloquear protocolo sin pasos críticos
const result = await blocker.canExecuteAction('execute_protocol', {
protocol: rcpProtocol,
executedSteps: [1, 2, 3] // Faltan pasos críticos 4 y 5
});
if (!result.allowed) {
// Bloquear continuación del protocolo
// Mostrar qué pasos críticos faltan
}
📊 Logging de Errores Críticos
Todos los errores críticos deben registrarse:
interface CriticalErrorLog {
errorCode: string;
errorMessage: string;
severity: 'blocking' | 'warning';
category: string;
context: {
userId?: string;
contentId?: string;
drugId?: string;
protocolId?: string;
action: string;
};
timestamp: Date;
blocked: boolean;
}
Fin del documento