Padrões de Desenvolvimento CDSS para Saúde
Padrões para construir Sistemas de Apoio à Decisão Clínica (CDSS) que se integram a fluxos de trabalho de Prontuário Eletrônico (EMR). Os módulos CDSS são críticos para a segurança do paciente — tolerância zero a falsos negativos.
Quando Usar
- Implementando verificação de interações medicamentosas
- Construindo motores de validação de dosagem
- Implementando sistemas de pontuação clínica (NEWS2, qSOFA, APACHE, GCS)
- Projetando sistemas de alerta para valores clínicos anormais
- Construindo entrada de pedidos de medicação com verificações de segurança
- Integrando interpretação de resultados laboratoriais com contexto clínico
Como Funciona
O motor CDSS é uma biblioteca de funções puras sem efeitos colaterais. Insira dados clínicos, produza alertas. Isso o torna totalmente testável.
Três módulos principais:
checkInteractions(newDrug, currentMeds, allergies)— Verifica um novo medicamento contra medicamentos atuais e alergias conhecidas. RetornaInteractionAlert[]ordenado por severidade. Usa o modelo de dadosDrugInteractionPair.validateDose(drug, dose, route, weight, age, renalFunction)— Valida uma dose prescrita com base em regras ajustadas por peso, idade e função renal. RetornaDoseValidationResult.calculateNEWS2(vitals)— National Early Warning Score 2, a partir deNEWS2Input. RetornaNEWS2Resultcom pontuação total, nível de risco e orientação de escalonamento.
UI do EMR
↓ (usuário insere dados)
Motor CDSS (funções puras, sem efeitos colaterais)
├── Verificador de Interações Medicamentosas
├── Validador de Dosagem
├── Pontuação Clínica (NEWS2, qSOFA, etc.)
└── Classificador de Alertas
↓ (retorna alertas)
UI do EMR (exibe alertas inline, bloqueia se crítico)
Verificação de Interações Medicamentosas
interface DrugInteractionPair {
drugA: string; // nome genérico
drugB: string; // nome 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: `Reatividade cruzada com alergia documentada: ${allergy}`,
recommendation: 'Não prescrever sem consulta alergológica' });
}
}
return alerts.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
}
Os pares de interação devem ser bidirecionais: se o Medicamento A interage com o Medicamento B, então o Medicamento B interage com o Medicamento A.
Validação de Dosagem
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: 'Nenhuma regra de validação disponível', suggestedRange: null, factors: [] };
const factors: string[] = [];
// SEGURANÇA: se as regras exigem peso mas o peso está faltando, BLOQUEAR (não passar)
if (rules.weightBased) {
if (!patientWeight || patientWeight <= 0) {
return { valid: false, message: `Peso necessário para ${drug} (medicamento mg/kg)`,
suggestedRange: null, factors: ['weight_missing'] };
}
factors.push('weight');
const maxDose = rules.maxPerKg * patientWeight;
if (dose > maxDose) {
return { valid: false, message: `Dose excede o máximo para ${patientWeight}kg`,
suggestedRange: { min: rules.minPerKg * patientWeight, max: maxDose, unit: rules.unit }, factors };
}
}
// Ajuste por idade (quando as regras definem faixas etárias e a idade é fornecida)
if (rules.ageAdjusted && patientAge !== undefined) {
factors.push('age');
const ageMax = rules.getAgeAdjustedMax(patientAge);
if (dose > ageMax) {
return { valid: false, message: `Excede o máximo ajustado por idade para ${patientAge}anos`,
suggestedRange: { min: rules.typicalMin, max: ageMax, unit: rules.unit }, factors };
}
}
// Ajuste renal (quando as regras definem faixas de eGFR e eGFR é fornecida)
if (rules.renalAdjusted && renalFunction !== undefined) {
factors.push('renal');
const renalMax = rules.getRenalAdjustedMax(renalFunction);
if (dose > renalMax) {
return { valid: false, message: `Excede o 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 o 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 da faixa',
suggestedRange: { min: rules.typicalMin, max: rules.typicalMax, unit: rules.unit }, factors };
}
Pontuação 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;
}
As tabelas de pontuação devem corresponder exatamente à especificação do Royal College of Physicians.
Severidade do Alerta e Comportamento da UI
| Severidade | Comportamento da UI | Ação Necessária do Clínico |
|---|---|---|
| Crítico | Bloquear ação. Modal não dispensável. Vermelho. | Deve documentar motivo da anulação para prosseguir |
| Grave | Banner de aviso inline. Laranja. | Deve reconhecer antes de prosseguir |
| Menor | Nota informativa inline. Amarelo. | Apenas ciência, nenhuma ação necessária |
Alertas críticos NUNCA devem ser automaticamente dispensados ou implementados como notificações toast. Os motivos de anulação devem ser armazenados na trilha de auditoria.
Testando CDSS (Tolerância Zero a Falsos Negativos)
describe('CDSS — Segurança do 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('bloqueia medicamento mg/kg quando o peso está faltando', () => {
const result = validateDose('gentamicin', 300, 'iv');
expect(result.valid).toBe(false);
expect(result.factors).toContain('weight_missing');
});
it('lida graciosamente com dados de medicamento malformados', () => {
expect(() => checkInteractions('', [], [])).not.toThrow();
});
});
Critério de aprovação: 100%. Uma única interação perdida é um evento de segurança do paciente.
Anti-Padrões
- Tornar as verificações CDSS opcionais ou ignoráveis sem motivo documentado
- Implementar verificações de interação como notificações toast
- Usar tipos
anypara dados de medicamentos ou clínicos - Codificar pares de interação em vez de usar uma estrutura de dados sustentável
- Silenciar erros no motor CDSS (deve falhar ruidosamente)
- Ignorar a validação baseada em peso quando o peso não está disponível (deve bloquear, não passar)
Exemplos
Exemplo 1: Verificação de Interação Medicamentosa
const alerts = checkInteractions('warfarin', ['aspirin', 'metformin'], ['penicillin']);
// [{ severity: 'critical', pair: ['warfarin', 'aspirin'],
// message: 'Risco aumentado de sangramento', recommendation: 'Evitar combinação' }]
Exemplo 2: Validação de Dosagem
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 o máximo absoluto 4000mg' }
const noWeight = validateDose('gentamicin', 300, 'iv');
// { valid: false, factors: ['weight_missing'] }
Exemplo 3: Pontuação NEWS2
const result = calculateNEWS2({
respiratoryRate: 24, oxygenSaturation: 93, supplementalOxygen: true,
temperature: 38.5, systolicBP: 100, heartRate: 110, consciousness: 'voice'
});
// { total: 13, risk: 'high', escalation: 'Revisão clínica urgente. Considere UTI.' }