codigo0/docs/SISTEMA_VALIDACION_PROTOCOLOS.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

16 KiB

📋 Sistema de Validación de Protocolos

🎯 Objetivo

Crear un sistema para validar la secuencia de pasos de protocolos y las dependencias entre protocolos.


📋 Reglas de Negocio

1. Validación de Secuencia de Pasos

Reglas de Secuencia

  • Los pasos deben estar numerados secuencialmente (1, 2, 3...)
  • No puede haber pasos faltantes en la secuencia
  • Los pasos deben tener un orden lógico (no se puede hacer paso 3 antes del paso 1)
  • Pasos críticos deben estar marcados y no pueden omitirse
  • Pasos opcionales deben estar claramente marcados

Tipos de Pasos

  • Obligatorio: Debe ejecutarse siempre
  • Condicional: Se ejecuta si se cumple condición
  • Opcional: Puede omitirse según contexto
  • Crítico: Bloquea el protocolo si se omite

2. Dependencias entre Protocolos

Tipos de Dependencias

  • Prerequisito: Protocolo A debe completarse antes de Protocolo B
  • Siguiente: Protocolo A normalmente lleva a Protocolo B
  • Alternativo: Protocolo A o Protocolo B (mutuamente excluyentes)
  • Complementario: Protocolo A se usa junto con Protocolo B

🏗️ Arquitectura

Domain Layer

// domain/entities/Protocol.ts
export interface Protocol {
  readonly id: string;
  readonly title: string;
  readonly shortTitle: string;
  readonly category: Category;
  readonly priority: Priority;
  readonly ageGroup: AgeGroup;
  readonly steps: readonly ProtocolStep[];
  readonly prerequisites: readonly string[]; // IDs de protocolos prerequisito
  readonly nextProtocols: readonly string[]; // IDs de protocolos siguientes
  readonly alternatives: readonly string[]; // IDs de protocolos alternativos
  readonly complements: readonly string[]; // IDs de protocolos complementarios
  readonly criticalSteps: readonly number[]; // Índices de pasos críticos
  readonly status: ContentStatus;
}

export interface ProtocolStep {
  readonly order: number; // Orden en la secuencia (1, 2, 3...)
  readonly type: 'obligatory' | 'conditional' | 'optional' | 'critical';
  readonly content: string;
  readonly condition?: string; // Condición para pasos condicionales
  readonly equipment?: readonly string[];
  readonly drugs?: readonly string[];
  readonly warnings?: readonly string[];
  readonly estimatedTime?: number; // Segundos estimados
}

// domain/value-objects/ProtocolSequence.ts
export class ProtocolSequence {
  private constructor(private readonly steps: readonly ProtocolStep[]) {}
  
  static create(steps: ProtocolStep[]): ProtocolSequence {
    // Validar secuencia
    const sortedSteps = [...steps].sort((a, b) => a.order - b.order);
    
    // Verificar que no hay huecos
    for (let i = 0; i < sortedSteps.length; i++) {
      if (sortedSteps[i].order !== i + 1) {
        throw new Error(`Paso faltante en secuencia: esperado ${i + 1}, encontrado ${sortedSteps[i].order}`);
      }
    }
    
    // Verificar que no hay duplicados
    const orders = sortedSteps.map(s => s.order);
    const uniqueOrders = new Set(orders);
    if (orders.length !== uniqueOrders.size) {
      throw new Error('Hay pasos con el mismo número de orden');
    }
    
    return new ProtocolSequence(sortedSteps);
  }
  
  validateExecution(executedSteps: number[]): ProtocolValidationResult {
    const errors: string[] = [];
    const warnings: string[] = [];
    
    // Verificar que todos los pasos críticos están ejecutados
    const criticalSteps = this.steps
      .filter(s => s.type === 'critical')
      .map(s => s.order);
    
    const missingCritical = criticalSteps.filter(step => !executedSteps.includes(step));
    if (missingCritical.length > 0) {
      errors.push(
        `Pasos críticos no ejecutados: ${missingCritical.join(', ')}`
      );
    }
    
    // Verificar que todos los pasos obligatorios están ejecutados
    const obligatorySteps = this.steps
      .filter(s => s.type === 'obligatory')
      .map(s => s.order);
    
    const missingObligatory = obligatorySteps.filter(step => !executedSteps.includes(step));
    if (missingObligatory.length > 0) {
      errors.push(
        `Pasos obligatorios no ejecutados: ${missingObligatory.join(', ')}`
      );
    }
    
    // Verificar orden de ejecución
    const sortedExecuted = [...executedSteps].sort((a, b) => a - b);
    if (JSON.stringify(executedSteps) !== JSON.stringify(sortedExecuted)) {
      warnings.push('Los pasos no se ejecutaron en orden secuencial');
    }
    
    // Verificar pasos condicionales
    for (const step of this.steps) {
      if (step.type === 'conditional' && executedSteps.includes(step.order)) {
        if (!step.condition) {
          warnings.push(`Paso ${step.order} es condicional pero no tiene condición definida`);
        }
      }
    }
    
    return {
      valid: errors.length === 0,
      errors,
      warnings,
      executedSteps,
      missingSteps: this.steps
        .map(s => s.order)
        .filter(order => !executedSteps.includes(order))
    };
  }
  
  getNextStep(currentStep?: number): ProtocolStep | null {
    if (currentStep === undefined) {
      return this.steps[0] || null;
    }
    
    const currentIndex = this.steps.findIndex(s => s.order === currentStep);
    if (currentIndex === -1) return null;
    
    return this.steps[currentIndex + 1] || null;
  }
  
  getStep(order: number): ProtocolStep | null {
    return this.steps.find(s => s.order === order) || null;
  }
}

export interface ProtocolValidationResult {
  valid: boolean;
  errors: string[];
  warnings: string[];
  executedSteps: number[];
  missingSteps: number[];
}

// domain/value-objects/ProtocolDependency.ts
export class ProtocolDependency {
  private constructor(
    readonly protocolId: string,
    readonly dependencyType: 'prerequisite' | 'next' | 'alternative' | 'complement',
    readonly targetProtocolId: string,
    readonly required: boolean
  ) {}
  
  static createPrerequisite(
    protocolId: string,
    prerequisiteId: string,
    required: boolean = true
  ): ProtocolDependency {
    return new ProtocolDependency(
      protocolId,
      'prerequisite',
      prerequisiteId,
      required
    );
  }
  
  static createNext(
    protocolId: string,
    nextProtocolId: string
  ): ProtocolDependency {
    return new ProtocolDependency(
      protocolId,
      'next',
      nextProtocolId,
      false
    );
  }
  
  static createAlternative(
    protocolId: string,
    alternativeId: string
  ): ProtocolDependency {
    return new ProtocolDependency(
      protocolId,
      'alternative',
      alternativeId,
      false
    );
  }
  
  static createComplement(
    protocolId: string,
    complementId: string
  ): ProtocolDependency {
    return new ProtocolDependency(
      protocolId,
      'complement',
      complementId,
      false
    );
  }
  
  validate(
    protocolExecuted: boolean,
    targetProtocolExecuted: boolean
  ): { valid: boolean; error?: string } {
    switch (this.dependencyType) {
      case 'prerequisite':
        if (this.required && protocolExecuted && !targetProtocolExecuted) {
          return {
            valid: false,
            error: `Protocolo ${this.targetProtocolId} debe ejecutarse antes de ${this.protocolId}`
          };
        }
        break;
        
      case 'alternative':
        if (protocolExecuted && targetProtocolExecuted) {
          return {
            valid: false,
            error: `Protocolos ${this.protocolId} y ${this.targetProtocolId} son mutuamente excluyentes`
          };
        }
        break;
    }
    
    return { valid: true };
  }
}

Application Layer

// application/services/ProtocolValidationService.ts
export class ProtocolValidationService {
  constructor(
    private readonly protocolRepository: IProtocolRepository,
    private readonly dependencyRepository: IDependencyRepository
  ) {}
  
  async validateProtocolSequence(
    protocolId: string,
    executedSteps: number[]
  ): Promise<ProtocolValidationResult> {
    const protocol = await this.protocolRepository.findById(protocolId);
    if (!protocol) {
      throw new Error(`Protocolo ${protocolId} no encontrado`);
    }
    
    const sequence = ProtocolSequence.create(protocol.steps);
    return sequence.validateExecution(executedSteps);
  }
  
  async validateProtocolDependencies(
    protocolId: string,
    executedProtocols: string[]
  ): Promise<DependencyValidationResult> {
    const protocol = await this.protocolRepository.findById(protocolId);
    if (!protocol) {
      throw new Error(`Protocolo ${protocolId} no encontrado`);
    }
    
    const dependencies = await this.dependencyRepository.findByProtocol(protocolId);
    const errors: string[] = [];
    const warnings: string[] = [];
    
    // Validar prerequisitos
    for (const prereqId of protocol.prerequisites) {
      if (!executedProtocols.includes(prereqId)) {
        const prereq = await this.protocolRepository.findById(prereqId);
        errors.push(
          `Protocolo prerequisito "${prereq?.title || prereqId}" no ha sido ejecutado`
        );
      }
    }
    
    // Validar alternativos
    for (const altId of protocol.alternatives) {
      if (executedProtocols.includes(altId)) {
        const alt = await this.protocolRepository.findById(altId);
        warnings.push(
          `Protocolo alternativo "${alt?.title || altId}" ya fue ejecutado. ` +
          `Estos protocolos son mutuamente excluyentes.`
        );
      }
    }
    
    // Sugerir protocolos siguientes
    const suggestedNext: string[] = [];
    for (const nextId of protocol.nextProtocols) {
      if (!executedProtocols.includes(nextId)) {
        suggestedNext.push(nextId);
      }
    }
    
    return {
      valid: errors.length === 0,
      errors,
      warnings,
      suggestedNext
    };
  }
  
  async getProtocolExecutionPath(
    startProtocolId: string,
    context: ProtocolContext
  ): Promise<ProtocolExecutionPath> {
    const path: ProtocolExecutionPath = {
      protocols: [],
      totalEstimatedTime: 0,
      warnings: []
    };
    
    const visited = new Set<string>();
    const queue: string[] = [startProtocolId];
    
    while (queue.length > 0) {
      const protocolId = queue.shift()!;
      if (visited.has(protocolId)) continue;
      
      visited.add(protocolId);
      const protocol = await this.protocolRepository.findById(protocolId);
      if (!protocol) continue;
      
      // Validar prerequisitos
      const depValidation = await this.validateProtocolDependencies(
        protocolId,
        path.protocols.map(p => p.id)
      );
      
      if (!depValidation.valid) {
        path.warnings.push(...depValidation.errors);
        continue; // Saltar si faltan prerequisitos
      }
      
      // Calcular tiempo estimado
      const estimatedTime = protocol.steps.reduce(
        (sum, step) => sum + (step.estimatedTime || 30),
        0
      );
      
      path.protocols.push({
        id: protocol.id,
        title: protocol.title,
        estimatedTime
      });
      
      path.totalEstimatedTime += estimatedTime;
      
      // Agregar protocolos siguientes a la cola
      for (const nextId of protocol.nextProtocols) {
        if (!visited.has(nextId) && !queue.includes(nextId)) {
          queue.push(nextId);
        }
      }
    }
    
    return path;
  }
}

export interface DependencyValidationResult {
  valid: boolean;
  errors: string[];
  warnings: string[];
  suggestedNext: string[];
}

export interface ProtocolExecutionPath {
  protocols: Array<{
    id: string;
    title: string;
    estimatedTime: number;
  }>;
  totalEstimatedTime: number;
  warnings: string[];
}

export interface ProtocolContext {
  ageGroup: AgeGroup;
  priority: Priority;
  availableEquipment?: string[];
  availableDrugs?: string[];
}

🗄️ Esquema de Base de Datos

-- Tabla de pasos de protocolo
CREATE TABLE tes_content.protocol_steps (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  protocol_id VARCHAR(100) NOT NULL REFERENCES tes_content.content_items(id),
  step_order INTEGER NOT NULL,
  step_type VARCHAR(20) NOT NULL DEFAULT 'obligatory',
  content TEXT NOT NULL,
  condition TEXT,
  equipment TEXT[],
  drugs TEXT[],
  warnings TEXT[],
  estimated_time_seconds INTEGER,
  
  CONSTRAINT chk_step_type CHECK (step_type IN ('obligatory', 'conditional', 'optional', 'critical')),
  CONSTRAINT unique_protocol_step_order UNIQUE (protocol_id, step_order)
);

CREATE INDEX idx_protocol_steps_protocol_id ON tes_content.protocol_steps(protocol_id);
CREATE INDEX idx_protocol_steps_order ON tes_content.protocol_steps(protocol_id, step_order);

-- Tabla de dependencias entre protocolos
CREATE TABLE tes_content.protocol_dependencies (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  protocol_id VARCHAR(100) NOT NULL REFERENCES tes_content.content_items(id),
  dependency_type VARCHAR(20) NOT NULL,
  target_protocol_id VARCHAR(100) NOT NULL REFERENCES tes_content.content_items(id),
  required BOOLEAN NOT NULL DEFAULT false,
  notes TEXT,
  
  CONSTRAINT chk_dependency_type CHECK (
    dependency_type IN ('prerequisite', 'next', 'alternative', 'complement')
  ),
  CONSTRAINT chk_no_self_dependency CHECK (protocol_id != target_protocol_id)
);

CREATE INDEX idx_protocol_dependencies_protocol ON tes_content.protocol_dependencies(protocol_id);
CREATE INDEX idx_protocol_dependencies_target ON tes_content.protocol_dependencies(target_protocol_id);
CREATE INDEX idx_protocol_dependencies_type ON tes_content.protocol_dependencies(dependency_type);

Casos de Uso

Caso 1: Validar ejecución de protocolo

const validation = await protocolValidationService.validateProtocolSequence(
  'rcp-adulto-svb',
  [1, 2, 3, 4, 5] // Pasos ejecutados
);

if (!validation.valid) {
  console.error('Errores:', validation.errors);
  // Bloquear continuación del protocolo
}

Caso 2: Validar dependencias antes de iniciar protocolo

const depValidation = await protocolValidationService.validateProtocolDependencies(
  'rcp-adulto-sva',
  ['rcp-adulto-svb'] // Protocolos ya ejecutados
);

if (!depValidation.valid) {
  throw new Error(`No se puede ejecutar: ${depValidation.errors.join(', ')}`);
}

Caso 3: Obtener ruta de ejecución sugerida

const path = await protocolValidationService.getProtocolExecutionPath(
  'rcp-adulto-svb',
  {
    ageGroup: 'adulto',
    priority: 'critico'
  }
);

console.log(`Ruta sugerida: ${path.protocols.map(p => p.title).join(' → ')}`);
console.log(`Tiempo estimado: ${path.totalEstimatedTime} segundos`);

🧪 Tests

describe('ProtocolValidationService', () => {
  describe('validateProtocolSequence', () => {
    it('should reject if critical steps are missing', async () => {
      const result = await service.validateProtocolSequence(
        'rcp-adulto-svb',
        [1, 2, 3] // Faltan pasos críticos 4 y 5
      );
      
      expect(result.valid).toBe(false);
      expect(result.errors).toContain(expect.stringContaining('críticos'));
    });
    
    it('should warn if steps are out of order', async () => {
      const result = await service.validateProtocolSequence(
        'rcp-adulto-svb',
        [1, 3, 2, 4, 5] // Orden incorrecto
      );
      
      expect(result.warnings).toContain(expect.stringContaining('orden'));
    });
  });
  
  describe('validateProtocolDependencies', () => {
    it('should reject if prerequisites are missing', async () => {
      const result = await service.validateProtocolDependencies(
        'rcp-adulto-sva',
        [] // No hay protocolos ejecutados
      );
      
      expect(result.valid).toBe(false);
      expect(result.errors).toContain(expect.stringContaining('prerequisito'));
    });
  });
});

Fin del documento