430 lines
12 KiB
Markdown
430 lines
12 KiB
Markdown
|
|
# ⚠️ 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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
|
||
|
|
```typescript
|
||
|
|
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
|
||
|
|
```typescript
|
||
|
|
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:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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**
|