Modèles de développement SCD pour les soins de santé
Modèles pour la construction de Systèmes d'Aide à la Décision Clinique qui s'intègrent dans les flux de travail du DME. Les modules SCD sont critiques pour la sécurité des patients — tolérance zéro pour les faux négatifs.
Quand l'utiliser
- Mettre en œuvre la vérification des interactions médicamenteuses
- Construire des moteurs de validation des doses
- Mettre en œuvre des systèmes de scores cliniques (NEWS2, qSOFA, APACHE, GCS)
- Concevoir des systèmes d'alerte pour les valeurs cliniques anormales
- Construire une saisie d'ordonnance médicamenteuse avec des contrôles de sécurité
- Intégrer l'interprétation des résultats de laboratoire avec le contexte clinique
Comment ça fonctionne
Le moteur SCD est une bibliothèque de fonctions pures sans effet secondaire. Entrez des données cliniques, obtenez des alertes. Cela le rend entièrement testable.
Trois modules principaux :
checkInteractions(nouveauMedicament, medicamentsActuels, allergies)— Vérifie un nouveau médicament par rapport aux médicaments actuels et aux allergies connues. Retourne unInteractionAlert[]trié par sévérité. Utilise le modèle de donnéesDrugInteractionPair.validateDose(medicament, dose, voie, poids, age, fonctionRenale)— Valide une dose prescrite par rapport aux règles basées sur le poids, l'âge et la fonction rénale. Retourne unDoseValidationResult.calculateNEWS2(signesVItaux)— National Early Warning Score 2 à partir deNEWS2Input. RetourneNEWS2Resultavec le score total, le niveau de risque et les recommandations d'escalade.
UI du DME
↓ (l'utilisateur saisit les données)
Moteur SCD (fonctions pures, pas d'effets secondaires)
├── Vérificateur d'interactions médicamenteuses
├── Validateur de dose
├── Scores cliniques (NEWS2, qSOFA, etc.)
└── Classificateur d'alertes
↓ (retourne les alertes)
UI du DME (affiche les alertes en ligne, bloque si critique)
Vérification des interactions médicamenteuses
interface DrugInteractionPair {
drugA: string; // nom générique
drugB: string; // nom générique
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: `Réactivité croisée avec une allergie documentée : ${allergy}`,
recommendation: 'Ne pas prescrire sans consultation d\'allergologie' });
}
}
return alerts.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
}
Les paires d'interactions doivent être bidirectionnelles : si le Médicament A interagit avec le Médicament B, alors le Médicament B interagit avec le Médicament A.
Validation des doses
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: 'Aucune règle de validation disponible', suggestedRange: null, factors: [] };
const factors: string[] = [];
// SÉCURITÉ : si les règles nécessitent le poids mais que le poids manque, BLOQUER (ne pas laisser passer)
if (rules.weightBased) {
if (!patientWeight || patientWeight <= 0) {
return { valid: false, message: `Poids requis pour ${drug} (médicament en mg/kg)`,
suggestedRange: null, factors: ['weight_missing'] };
}
factors.push('weight');
const maxDose = rules.maxPerKg * patientWeight;
if (dose > maxDose) {
return { valid: false, message: `La dose dépasse le maximum pour ${patientWeight}kg`,
suggestedRange: { min: rules.minPerKg * patientWeight, max: maxDose, unit: rules.unit }, factors };
}
}
// Ajustement en fonction de l'âge (lorsque les règles définissent des tranches d'âge et que l'âge est fourni)
if (rules.ageAdjusted && patientAge !== undefined) {
factors.push('age');
const ageMax = rules.getAgeAdjustedMax(patientAge);
if (dose > ageMax) {
return { valid: false, message: `Dépasse le maximum ajusté à l'âge pour ${patientAge} ans`,
suggestedRange: { min: rules.typicalMin, max: ageMax, unit: rules.unit }, factors };
}
}
// Ajustement rénal (lorsque les règles définissent des tranches de DFGe et que le DFGe est fourni)
if (rules.renalAdjusted && renalFunction !== undefined) {
factors.push('renal');
const renalMax = rules.getRenalAdjustedMax(renalFunction);
if (dose > renalMax) {
return { valid: false, message: `Dépasse le maximum ajusté rénalement pour un DFGe de ${renalFunction}`,
suggestedRange: { min: rules.typicalMin, max: renalMax, unit: rules.unit }, factors };
}
}
// Maximum absolu
if (dose > rules.absoluteMax) {
return { valid: false, message: `Dépasse le maximum absolu ${rules.absoluteMax}${rules.unit}`,
suggestedRange: { min: rules.typicalMin, max: rules.absoluteMax, unit: rules.unit },
factors: [...factors, 'absolute_max'] };
}
return { valid: true, message: 'Dans les limites',
suggestedRange: { min: rules.typicalMin, max: rules.typicalMax, unit: rules.unit }, factors };
}
Score clinique : 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;
}
Les tableaux de scores doivent correspondre exactement aux spécifications du Royal College of Physicians.
Sévérité des alertes et comportement UI
| Sévérité | Comportement UI | Action clinique requise |
|---|---|---|
| Critique | Bloquer l'action. Modale non désactivable. Rouge. | Doit documenter la raison du dépassement pour continuer |
| Majeure | Bannière d'avertissement en ligne. Orange. | Doit accuser réception avant de continuer |
| Mineure | Note d'information en ligne. Jaune. | Information uniquement, aucune action requise |
Les alertes critiques ne doivent JAMAIS être automatiquement ignorées ou implémentées sous forme de notifications toast. Les raisons du dépassement doivent être stockées dans la piste d'audit.
Tester le SCD (Tolérance zéro pour les faux négatifs)
describe('SCD — Sécurité des patients', () => {
INTERACTION_PAIRS.forEach(({ drugA, drugB, severity }) => {
it(`détecte ${drugA} + ${drugB} (${severity})`, () => {
const alerts = checkInteractions(drugA, [drugB], []);
expect(alerts.length).toBeGreaterThan(0);
expect(alerts[0].severity).toBe(severity);
});
it(`détecte ${drugB} + ${drugA} (inverse)`, () => {
const alerts = checkInteractions(drugB, [drugA], []);
expect(alerts.length).toBeGreaterThan(0);
});
});
it('bloque le médicament en mg/kg quand le poids manque', () => {
const result = validateDose('gentamicin', 300, 'iv');
expect(result.valid).toBe(false);
expect(result.factors).toContain('weight_missing');
});
it('gère les données de médicament mal formées avec élégance', () => {
expect(() => checkInteractions('', [], [])).not.toThrow();
});
});
Critère de réussite : 100%. Une seule interaction manquée est un événement de sécurité pour le patient.
Anti-patrons
- Rendre les vérifications SCD facultatives ou contournables sans raison documentée
- Implémenter des vérifications d'interactions sous forme de notifications toast
- Utiliser des types
anypour les données de médicaments ou cliniques - Coder en dur les paires d'interactions au lieu d'utiliser une structure de données maintenable
- Attraper silencieusement les erreurs dans le moteur SCD (doit signaler les échecs bruyamment)
- Ignorer la validation basée sur le poids lorsque le poids n'est pas disponible (doit bloquer, ne pas laisser passer)
Exemples
Exemple 1 : Vérification d'interaction médicamenteuse
const alerts = checkInteractions('warfarin', ['aspirin', 'metformin'], ['penicillin']);
// [{ severity: 'critical', pair: ['warfarin', 'aspirin'],
// message: 'Risque accru de saignement', recommendation: 'Éviter la combinaison' }]
Exemple 2 : Validation de dose
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: 'Dépasse le maximum absolu 4000mg' }
const noWeight = validateDose('gentamicin', 300, 'iv');
// { valid: false, factors: ['weight_missing'] }
Exemple 3 : Score NEWS2
const result = calculateNEWS2({
respiratoryRate: 24, oxygenSaturation: 93, supplementalOxygen: true,
temperature: 38.5, systolicBP: 100, heartRate: 110, consciousness: 'voice'
});
// { total: 13, risk: 'high', escalation: 'Révision clinique urgente. Envisager une USI.' }