codigo0/docs/ERRORES_CRITICOS_MEDICOS.md
planetazuzu 5d7a6500fe refactor: Fase 1 - Clean Architecture, refactorización modular y eliminación de duplicidades
-  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
2026-01-25 21:09:47 +01:00

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