// Motor de detección de patrones clínicos. // Puerto TypeScript fiel de js/analisis.js — la lógica no cambia; sólo se añaden tipos. // Compara valores contra rangos de referencia ajustados por edad, raza y sexo, // clasifica gravedad y detecta patrones clínicos. import type { AjustesClinicos, Alteraciones, TablaAjustes, Especie, Gravedad, Hallazgo, Paciente, Patron, RangoReferencia, Referencias, ReferenciasEspecie, ResultadoAnalisis, ValoresFormulario, } from './tipos.js'; // Gravedad // La desviación se mide en múltiplos del ancho del rango de referencia. // Ej: rango WBC 6-17 (ancho = 11). WBC = 28 → desviación = 11/11 = 1.0 → moderado. // Cortes clinicos explicitos para el lado BAJO, en la unidad del analito. La regla generica de // anchos de rango no sabe expresar la gravedad de estos dos: con rango 24-45, un gato // necesitaria un hematocrito NEGATIVO para llegar a 'grave', y con 200-500 unas plaquetas de // -250. Es decir, ni una anemia felina ni una trombocitopenia disparaban nunca el suelo de // derivacion. Anclajes de la literatura (auditoria 2026-07-31): // - PCV <= 20% es umbral de transfusion en perro y gato, y <= 12% (Hgb < 4 g/dL) es // potencialmente mortal — Thrall 3.a ed., p. 240. // - PCV 13 en gato se describe como "markedly anemic" y 24 como "mildly anemic" — p. 800-801. // - El riesgo hemorragico por trombocitopenia aparece por debajo de 30x10^3/uL — // Fundamentals 3.a ed., p. 310. // Los cortes interiores del gato (14, 20) siguen la gradacion de uso comun; no estan en estas // dos obras, que no traen tabla de gradacion. const clasificarGravedad = ( valor: number, ref: RangoReferencia, ajustes: AjustesClinicos, clave?: string, especie?: Especie, ): Gravedad => { const cortesBajo = clave && especie ? ajustes.cortes_gravedad_bajo[clave]?.[especie] : undefined; if (cortesBajo && valor < ref.inferior) { // `moderado_hasta: null` = ese analito nunca llega a 'grave' por este lado. if (cortesBajo.moderado_hasta !== null && valor < cortesBajo.moderado_hasta) return 'grave'; if (valor < cortesBajo.leve_hasta) return 'moderado'; return 'leve'; } const cortesAlto = clave && especie ? ajustes.cortes_gravedad_alto[clave]?.[especie] : undefined; if (cortesAlto && valor > ref.superior) { if (cortesAlto.moderado_hasta !== null && valor > cortesAlto.moderado_hasta) return 'grave'; if (valor > cortesAlto.leve_hasta) return 'moderado'; return 'leve'; } // Mide cuantos anchos de rango de referencia se desvia el valor const rango = ref.superior - ref.inferior; const desviacion = valor > ref.superior ? (valor - ref.superior) / rango : (ref.inferior - valor) / rango; if (desviacion <= ajustes.umbrales_gravedad.leve) return 'leve'; if (desviacion <= ajustes.umbrales_gravedad.moderado) return 'moderado'; return 'grave'; }; // Edad // Orden de las categorías: la primera cuyo límite no se supera es la que aplica; si se superan // todas, la última. Los límites viven en data/ajustes_clinicos.json, junto a los factores que // seleccionan, para que cambiar "senior a partir de los 7 años" sea una edición de datos. const ORDEN_EDAD: Record = { canino: ['cachorro', 'adulto', 'senior', 'geriatrico'], felino: ['cachorro', 'adulto', 'senior'], }; const categorizarEdad = (edadMeses: number | null, especie: Especie, ajustes: AjustesClinicos): string => { if (edadMeses === null) return 'adulto'; const limites = ajustes.limites_edad_meses[especie] ?? {}; const orden = ORDEN_EDAD[especie]; for (const categoria of orden.slice(0, -1)) { if (edadMeses < (limites[categoria] ?? Infinity)) return categoria; } return orden[orden.length - 1]; }; // Ajustes por sexo // Retirado el ajuste que subia un 15% el techo de creatinina en el gato MACHO: no aparece en // ninguna de las dos obras del corpus. Lo que si esta documentado es la masa muscular (los // galgos tienen creatinina mas alta, Thrall 3.a ed., p. 363) y la raza Birman (Fundamentals // 3.a ed., p. 585), y ambos se tratan ahora como ajuste de RAZA. Ademas, la secrecion tubular // de creatinina es cosa del perro macho —"cats and ponies do not secrete or reabsorb Ct in // their kidneys" (Thrall p. 363)— y se describe como clinicamente intrascendente. Tolerar un // 15% mas de creatinina justo en la poblacion con mas ERC era infradeteccion sin respaldo. // // Lo unico con respaldo por sexo es menor: en perro las plaquetas son hasta un 10% mayores en // hembras y en enteros (Fundamentals p. 310). No se implementa: por debajo de la resolucion // clinica del motor. // Combina TODOS los grupos que casan, no solo el primero: un shiba inu pertenece a la vez al // grupo de microcitosis fisiologica y al de plaquetas bajas, y quedarse con el primero perdia // el segundo en silencio. const obtenerAjustesRaza = (raza: string | null, especie: Especie, ajustes: AjustesClinicos): TablaAjustes => { const razaNorm = raza?.toLowerCase().trim() ?? ''; const grupos = ajustes.ajustes_raza[especie] ?? []; return grupos .filter((g) => g.razas.some((r) => razaNorm.includes(r))) .reduce((acc, grupo) => { for (const [clave, factor] of Object.entries(grupo.ajustes)) { acc[clave] = { inferior: (acc[clave]?.inferior ?? 1) * (factor.inferior ?? 1), superior: (acc[clave]?.superior ?? 1) * (factor.superior ?? 1), }; } return acc; }, {}); }; // Ajuste de referencias const ajustarReferencias = ( refsEspecie: ReferenciasEspecie, paciente: Paciente, ajustes: AjustesClinicos, ): ReferenciasEspecie => { const especie = paciente.especie as Especie; const catEdad = categorizarEdad(paciente.edadMeses, especie, ajustes); const ajEdad = ajustes.ajustes_edad[especie]?.[catEdad] ?? {}; const ajRaza = obtenerAjustesRaza(paciente.raza, especie, ajustes); const ajSexo = (paciente.sexo ? ajustes.ajustes_sexo[especie]?.[paciente.sexo] : undefined) ?? {}; // Multiplica los limites inferiores y superiores por los factores de edad, raza y sexo return Object.entries(refsEspecie).reduce((acc, [clave, ref]) => { const factorEdad = ajEdad[clave] ?? {}; const factorRaza = ajRaza[clave] ?? {}; const factorSexo = ajSexo[clave] ?? {}; acc[clave] = { ...ref, inferior: ref.inferior * (factorEdad.inferior ?? 1) * (factorRaza.inferior ?? 1) * (factorSexo.inferior ?? 1), superior: ref.superior * (factorEdad.superior ?? 1) * (factorRaza.superior ?? 1) * (factorSexo.superior ?? 1), }; return acc; }, {}); }; // Estadiaje IRIS de la enfermedad renal crónica. // Los cortes (creatinina, SDMA y UP/C) viven en data/ajustes_clinicos.json → `iris`, junto a su // procedencia bibliográfica y la limitación deliberada del estadiaje sobre una analítica suelta. type EstadioIris = { estadio: number; subestadio: string; nota: string }; const estadificarIris = ( creat: number | null, sdma: number | null, upc: number | null, especie: Especie, ajustes: AjustesClinicos, ): EstadioIris | null => { if (creat === null && sdma === null) return null; const cortesCreat = ajustes.iris.cortes_creatinina[especie] ?? []; const cortesSdma = ajustes.iris.cortes_sdma[especie] ?? []; const porCreatinina = creat === null ? 1 : cortesCreat.findIndex((techo) => creat <= techo) + 1 || 4; // Escalada por SDMA: la guía sube un estadio cuando la SDMA persistente supera el umbral // del estadio asignado por creatinina (>18 en el 1, >35/25 en el 2, >54/38 en el 3). const umbral = cortesSdma[porCreatinina - 1]; const escalado = sdma !== null && umbral !== undefined && sdma > umbral ? porCreatinina + 1 : porCreatinina; if (escalado < 2) return null; // estadio 1: sin azotemia ni SDMA alta, nada que informar const [noProteinurico, proteinurico] = ajustes.iris.cortes_upc[especie] ?? [0, 0]; const subestadio = upc === null ? '' : upc > proteinurico ? 'proteinúrico' : upc >= noProteinurico ? 'proteinúrico limítrofe' : 'no proteinúrico'; const nota = escalado > porCreatinina ? `Estadio elevado de ${porCreatinina} a ${escalado} por SDMA discrepante (${sdma} µg/dL).` : ''; return { estadio: escalado, subestadio, nota }; }; // Detección de patrones clínicos const detectarPatrones = ( hallazgos: Hallazgo[], especie: Especie, alt: Alteraciones, ajustes: AjustesClinicos, ): Patron[] => { const mapa = hallazgos.reduce>((acc, h) => { acc[h.clave] = h; return acc; }, {}); const esAlto = (clave: string): boolean => mapa[clave]?.direccion === 'alto'; const esBajo = (clave: string): boolean => mapa[clave]?.direccion === 'bajo'; const presente = (clave: string): boolean => clave in mapa; const valor = (clave: string): number | null => mapa[clave]?.valor ?? null; const gravedadDe = (...claves: string[]): Gravedad => { const clave = claves.find((c) => mapa[c]); return (clave ? mapa[clave]?.gravedad : undefined) ?? 'leve'; }; const patrones: Patron[] = []; const agregar = (patron: Patron): number => patrones.push(patron); // Serie roja if (esBajo('hct') || esBajo('hgb') || esBajo('rbc')) { // Clasifica el tipo de anemia segun el VCM para sugerir la etiologia mas probable const tipoPorVcm = !presente('vcm') ? '' : esBajo('vcm') ? 'microcítica' : esAlto('vcm') ? 'macrocítica' : 'normocítica'; const claveEtiologia = esBajo('vcm') ? 'ferropenia' : esAlto('vcm') ? 'macrocitica' : tipoPorVcm === 'normocítica' ? 'normocitica' : null; const etiologia = claveEtiologia ? alt.anemia.etiologias?.[claveEtiologia] ?? '' : ''; agregar({ nombre: `${alt.anemia.nombre}${tipoPorVcm ? ` ${tipoPorVcm}` : ''}`, descripcion: [alt.anemia.prefijo, etiologia].filter(Boolean).join(' '), gravedad: gravedadDe('hct', 'hgb', 'rbc'), parametros: ['hct', 'hgb', 'rbc', 'vcm'].filter(presente), }); } if (esAlto('hct') || esAlto('rbc')) agregar({ nombre: alt.eritrocitosis.nombre, descripcion: alt.eritrocitosis.descripcion, gravedad: gravedadDe('hct', 'rbc'), parametros: ['hct', 'rbc', 'hgb'].filter(presente), }); // Serie blanca if (esAlto('wbc')) { // Diferencia leucocitosis neutrofilica de linfocitica; si no hay diferencial, informa generico const neutrofilia = esAlto('neutro'); const linfocitosis = esAlto('linfo'); if (neutrofilia) agregar({ nombre: alt.leucocitosis_neutrofilica.nombre, descripcion: alt.leucocitosis_neutrofilica.descripcion, gravedad: gravedadDe('wbc', 'neutro'), parametros: ['wbc', 'neutro'].filter(presente), }); if (linfocitosis) agregar({ nombre: alt.leucocitosis_linfocitica.nombre, descripcion: alt.leucocitosis_linfocitica.descripcion, gravedad: gravedadDe('wbc', 'linfo'), parametros: ['wbc', 'linfo'].filter(presente), }); if (!neutrofilia && !linfocitosis) agregar({ nombre: alt.leucocitosis.nombre, descripcion: alt.leucocitosis.descripcion, gravedad: gravedadDe('wbc'), parametros: ['wbc'], }); } if (esBajo('wbc')) agregar({ nombre: alt.leucopenia.nombre, descripcion: alt.leucopenia.descripcion, gravedad: gravedadDe('wbc'), parametros: ['wbc'], }); if (esBajo('neutro')) agregar({ nombre: alt.neutropenia.nombre, descripcion: alt.neutropenia.descripcion, gravedad: gravedadDe('neutro'), parametros: ['neutro'], }); if (esBajo('linfo')) agregar({ nombre: alt.linfopenia.nombre, descripcion: alt.linfopenia.descripcion, gravedad: gravedadDe('linfo'), parametros: ['linfo'], }); if (esAlto('eosino')) agregar({ nombre: alt.eosinofilia.nombre, descripcion: alt.eosinofilia.descripcion, gravedad: gravedadDe('eosino'), parametros: ['eosino'], }); // Plaquetas if (esBajo('plt')) agregar({ nombre: alt.trombocitopenia.nombre, descripcion: alt.trombocitopenia.descripcion, gravedad: gravedadDe('plt'), parametros: ['plt'], }); if (esAlto('plt')) agregar({ nombre: alt.trombocitosis.nombre, descripcion: alt.trombocitosis.descripcion, gravedad: gravedadDe('plt'), parametros: ['plt'], }); // Hígado if (esAlto('alt') && esAlto('ast')) agregar({ nombre: alt.dano_hepatocelular.nombre, descripcion: alt.dano_hepatocelular.descripcion, gravedad: gravedadDe('alt', 'ast'), parametros: ['alt', 'ast'].filter(presente), }); else if (esAlto('alt')) agregar({ nombre: alt.alt_aislada.nombre, descripcion: alt.alt_aislada.descripcion, gravedad: gravedadDe('alt'), parametros: ['alt'], }); if (esAlto('fal')) agregar({ nombre: alt.patron_colestasico.nombre, descripcion: alt.patron_colestasico.descripcion[especie] ?? alt.patron_colestasico.descripcion.canino, gravedad: gravedadDe('fal'), parametros: ['fal'], }); if (esAlto('bili')) agregar({ nombre: alt.hiperbilirrubinemia.nombre, descripcion: alt.hiperbilirrubinemia.descripcion, gravedad: gravedadDe('bili'), parametros: ['bili'], }); // Riñón if (esAlto('bun') && esAlto('creat')) agregar({ nombre: alt.azotemia.nombre, descripcion: alt.azotemia.descripcion, gravedad: gravedadDe('creat', 'bun'), parametros: ['bun', 'creat'].filter(presente), }); else if (esAlto('bun')) agregar({ nombre: alt.hiperuremia_bun.nombre, descripcion: alt.hiperuremia_bun.descripcion, gravedad: gravedadDe('bun'), parametros: ['bun'], }); else if (esAlto('creat')) agregar({ nombre: alt.creatinina_aislada.nombre, descripcion: alt.creatinina_aislada.descripcion, gravedad: gravedadDe('creat'), parametros: ['creat'], }); // Estadiaje IRIS. Se emite como patrón aparte de la azotemia porque responde a otra // pregunta: la azotemia dice QUÉ está alterado, el estadio dice CUÁNTO y guía el manejo. const estadio = estadificarIris(valor('creat'), valor('sdma'), valor('upc'), especie, ajustes); if (estadio) agregar({ nombre: `${alt.erc_iris.nombre} — estadio ${estadio.estadio}${estadio.subestadio ? `, ${estadio.subestadio}` : ''}`, descripcion: `${alt.erc_iris.descripcion} ${estadio.nota}`, gravedad: estadio.estadio >= 4 ? 'grave' : estadio.estadio === 3 ? 'moderado' : 'leve', parametros: ['creat', 'sdma', 'upc'].filter(presente), }); if (esBajo('bun')) agregar({ nombre: alt.bun_disminuido.nombre, descripcion: alt.bun_disminuido.descripcion, gravedad: gravedadDe('bun'), parametros: ['bun'], }); // Glucosa if (esAlto('gluc')) agregar({ nombre: alt.hiperglucemia.nombre, descripcion: alt.hiperglucemia.descripcion[especie] ?? alt.hiperglucemia.descripcion.canino, gravedad: gravedadDe('gluc'), parametros: ['gluc'], }); if (esBajo('gluc')) agregar({ nombre: alt.hipoglucemia.nombre, descripcion: alt.hipoglucemia.descripcion, gravedad: gravedadDe('gluc'), parametros: ['gluc'], }); // Proteínas if (esAlto('prot')) agregar({ nombre: alt.hiperproteinemia.nombre, descripcion: alt.hiperproteinemia.descripcion, gravedad: gravedadDe('prot'), parametros: ['prot'], }); if (esBajo('alb')) { const hipoproteinemia = esBajo('prot'); const claveAlteracion = hipoproteinemia ? 'hipoproteinemia_hipoalbuminemia' : 'hipoalbuminemia'; agregar({ nombre: alt[claveAlteracion].nombre, descripcion: alt[claveAlteracion].descripcion, gravedad: gravedadDe('alb'), parametros: ['alb', ...(hipoproteinemia ? ['prot'] : [])].filter(presente), }); } // Electrolitos const valSodio = valor('sodio'); const valPotasio = valor('potasio'); // Ratio Na/K < 27 es sugestivo de hipoadrenocorticismo; la gravedad aumenta a menor ratio if (valSodio !== null && valPotasio !== null && valPotasio > 0) { const ratioNaK = valSodio / valPotasio; if (ratioNaK < 27) agregar({ nombre: alt.ratio_nak.nombre, descripcion: alt.ratio_nak.descripcion.replace('{ratio}', ratioNaK.toFixed(1)), gravedad: ratioNaK < 20 ? 'grave' : ratioNaK < 24 ? 'moderado' : 'leve', parametros: ['sodio', 'potasio'].filter(presente), }); } if (esAlto('sodio')) agregar({ nombre: alt.hipernatremia.nombre, descripcion: alt.hipernatremia.descripcion, gravedad: gravedadDe('sodio'), parametros: ['sodio'], }); if (esBajo('sodio')) agregar({ nombre: alt.hiponatremia.nombre, descripcion: alt.hiponatremia.descripcion, gravedad: gravedadDe('sodio'), parametros: ['sodio'], }); // El fosforo es EL discriminador de la hipercalcemia, y sin el se devuelve una lista de // diferenciales que no discrimina nada. Thrall 3.a ed., p. 586 y p. 997: con calcio alto y // fosforo normal o bajo solo quedan dos diagnosticos probables —hipercalcemia maligna // (linfoma el tumor mas frecuente, luego adenocarcinoma de sacos anales) e hiperparatiroidismo // primario—, mientras que hipoadrenocorticismo, fallo renal, toxicidad por vitamina D y // enfermedad granulomatosa cursan habitualmente con el fosforo ALTO. if (esAlto('calc')) { // Solo se matiza cuando el fosforo aparece como hallazgo: un fosforo dentro de rango y uno // no medido son indistinguibles aqui (a detectarPatrones solo llegan los valores alterados). const matizPorFosforo = esBajo('fosf') ? ' Con el fósforo bajo, los dos diferenciales probables son hipercalcemia maligna ' + '(linfoma, adenocarcinoma de sacos anales) e hiperparatiroidismo primario.' : esAlto('fosf') ? ' Con el fósforo alto, priorizar fallo renal, hipoadrenocorticismo, toxicidad por ' + 'vitamina D y enfermedad granulomatosa.' : ''; agregar({ nombre: alt.hipercalcemia.nombre, descripcion: alt.hipercalcemia.descripcion + matizPorFosforo, gravedad: gravedadDe('calc'), parametros: ['calc', ...(presente('fosf') ? ['fosf'] : [])], }); } if (esBajo('calc')) agregar({ nombre: alt.hipocalcemia.nombre, descripcion: alt.hipocalcemia.descripcion, gravedad: gravedadDe('calc'), parametros: ['calc'], }); if (esBajo('potasio')) agregar({ nombre: alt.hipopotasemia.nombre, descripcion: alt.hipopotasemia.descripcion, gravedad: gravedadDe('potasio'), parametros: ['potasio'], }); if (esAlto('potasio')) agregar({ nombre: alt.hiperpotasemia.nombre, descripcion: alt.hiperpotasemia.descripcion, gravedad: gravedadDe('potasio'), parametros: ['potasio'], }); if (esAlto('fosf')) agregar({ nombre: alt.hiperfosforemia.nombre, descripcion: alt.hiperfosforemia.descripcion, gravedad: gravedadDe('fosf'), parametros: ['fosf'], }); // Urianálisis // Corte de hiposteinuria en 1,007, no en 1,008: la isosteinuria es 1,007-1,013 en perro y // gato (Thrall 3.a ed., Tabla 24.8; Fundamentals 3.a ed., p. 565, que la describe como // "similar to the often-used 1.008-1.012"). Con 1,008 un USG de 1,0075 se etiquetaba como // hiposteinuria cuando la literatura lo considera isosteinurico. const valUsg = valor('usg'); if (valUsg !== null && valUsg < 1.007) agregar({ nombre: alt.hiposthenuria.nombre, descripcion: alt.hiposthenuria.descripcion, gravedad: valUsg < 1.005 ? 'grave' : 'moderado', parametros: ['usg'], }); else if (valUsg !== null && valUsg < 1.013) agregar({ nombre: alt.isosthenuria.nombre, descripcion: alt.isosthenuria.descripcion, gravedad: 'leve', parametros: ['usg'], }); // Tiroides if (especie === 'canino' && esBajo('t4_total')) agregar({ nombre: alt.hipotiroidismo.nombre, descripcion: alt.hipotiroidismo.descripcion.canino, gravedad: gravedadDe('t4_total'), parametros: ['t4_total'].filter(presente), }); if (esAlto('t4_total')) agregar({ nombre: alt.hipertiroidismo.nombre, descripcion: alt.hipertiroidismo.descripcion[especie] ?? alt.hipertiroidismo.descripcion.felino, gravedad: gravedadDe('t4_total'), parametros: ['t4_total'].filter(presente), }); // Suprarrenal / Cortisol if (esAlto('cortisol_acth')) agregar({ nombre: alt.hiperadrenocorticismo.nombre, descripcion: alt.hiperadrenocorticismo.descripcion[especie] ?? alt.hiperadrenocorticismo.descripcion.canino, gravedad: gravedadDe('cortisol_acth'), parametros: ['cortisol_acth', ...(presente('cortisol_bas') ? ['cortisol_bas'] : [])], }); if (esBajo('cortisol_acth')) agregar({ nombre: alt.hipoadrenocorticismo_cortisol.nombre, descripcion: alt.hipoadrenocorticismo_cortisol.descripcion, gravedad: gravedadDe('cortisol_acth'), parametros: ['cortisol_acth', ...(presente('cortisol_bas') ? ['cortisol_bas'] : [])], }); if (esBajo('cortisol_bas') && !presente('cortisol_acth')) agregar({ nombre: alt.cortisol_basal_bajo.nombre, descripcion: alt.cortisol_basal_bajo.descripcion, gravedad: 'moderado', parametros: ['cortisol_bas'], }); // Insulina if (esBajo('insulina') && esAlto('gluc')) agregar({ nombre: alt.deficit_insulina.nombre, descripcion: alt.deficit_insulina.descripcion, gravedad: 'moderado', parametros: ['insulina', 'gluc'].filter(presente), }); // Páncreas exocrino (PLI) if (esAlto('pli')) agregar({ nombre: alt.pancreatitis.nombre, descripcion: alt.pancreatitis.descripcion[especie] ?? alt.pancreatitis.descripcion.canino, gravedad: gravedadDe('pli'), parametros: ['pli', ...(presente('lipasa') ? ['lipasa'] : []), ...(presente('amylasa') ? ['amylasa'] : [])].filter(presente), }); if (esAlto('amylasa') && !presente('pli')) agregar({ nombre: alt.hiperamylasemia.nombre, descripcion: alt.hiperamylasemia.descripcion, gravedad: gravedadDe('amylasa'), parametros: ['amylasa'], }); // Tiroides — TSH if (esAlto('tsh')) agregar({ nombre: alt.tsh_elevado.nombre, descripcion: alt.tsh_elevado.descripcion[especie] ?? alt.tsh_elevado.descripcion.canino, gravedad: gravedadDe('tsh'), parametros: ['tsh', ...(presente('t4_total') ? ['t4_total'] : []), ...(presente('t4_libre') ? ['t4_libre'] : [])].filter(presente), }); if (esBajo('tsh')) agregar({ nombre: alt.tsh_suprimido.nombre, descripcion: alt.tsh_suprimido.descripcion[especie] ?? alt.tsh_suprimido.descripcion.canino, gravedad: gravedadDe('tsh'), parametros: ['tsh', ...(presente('t4_total') ? ['t4_total'] : [])].filter(presente), }); if (esBajo('t4_libre') && !presente('tsh')) agregar({ nombre: alt.t4_libre_baja.nombre, descripcion: alt.t4_libre_baja.descripcion[especie] ?? alt.t4_libre_baja.descripcion.canino, gravedad: gravedadDe('t4_libre'), parametros: ['t4_libre', ...(presente('t4_total') ? ['t4_total'] : [])].filter(presente), }); // Biomarcadores cardíacos if (esAlto('ctni')) agregar({ nombre: alt.dano_miocardico.nombre, descripcion: alt.dano_miocardico.descripcion, gravedad: gravedadDe('ctni'), parametros: ['ctni', ...(presente('nt_probnp') ? ['nt_probnp'] : [])].filter(presente), }); if (esAlto('nt_probnp')) agregar({ nombre: alt.cardiopatia_bnp.nombre, descripcion: alt.cardiopatia_bnp.descripcion[especie] ?? alt.cardiopatia_bnp.descripcion.canino, gravedad: gravedadDe('nt_probnp'), parametros: ['nt_probnp', ...(presente('ctni') ? ['ctni'] : [])].filter(presente), }); // Proteínas de fase aguda if (esAlto('crp') || esAlto('saa')) agregar({ nombre: alt.inflamacion_aguda.nombre, descripcion: alt.inflamacion_aguda.descripcion[especie] ?? alt.inflamacion_aguda.descripcion.canino, gravedad: gravedadDe('crp', 'saa'), parametros: ['crp', 'saa'].filter(presente), }); // Progesterona if (esAlto('progesterona')) agregar({ nombre: alt.progesterona_elevada.nombre, descripcion: alt.progesterona_elevada.descripcion[especie] ?? alt.progesterona_elevada.descripcion.canino, gravedad: gravedadDe('progesterona'), parametros: ['progesterona'], }); // Magnesio if (esBajo('magnesio')) agregar({ nombre: alt.hipomagnesemia.nombre, descripcion: alt.hipomagnesemia.descripcion, gravedad: gravedadDe('magnesio'), parametros: ['magnesio'], }); if (esAlto('magnesio')) agregar({ nombre: alt.hipermagnesemia.nombre, descripcion: alt.hipermagnesemia.descripcion, gravedad: gravedadDe('magnesio'), parametros: ['magnesio'], }); // Hierro if (esBajo('hierro')) agregar({ nombre: alt.ferropenia_hierro.nombre, descripcion: alt.ferropenia_hierro.descripcion, gravedad: gravedadDe('hierro'), parametros: ['hierro'], }); // Ácido úrico if (esAlto('ac_urico')) agregar({ nombre: alt.ac_urico_elevado.nombre, descripcion: alt.ac_urico_elevado.descripcion, gravedad: gravedadDe('ac_urico'), parametros: ['ac_urico'], }); // LDH if (esAlto('ldh')) agregar({ nombre: alt.ldh_elevada.nombre, descripcion: alt.ldh_elevada.descripcion, gravedad: gravedadDe('ldh'), parametros: ['ldh'], }); // Monitorización de fármacos (TDM) if (esBajo('fenobarbital')) agregar({ nombre: alt.fenobarbital_subterapeutico.nombre, descripcion: alt.fenobarbital_subterapeutico.descripcion, gravedad: gravedadDe('fenobarbital'), parametros: ['fenobarbital'], }); if (esAlto('fenobarbital')) agregar({ nombre: alt.fenobarbital_toxico.nombre, descripcion: alt.fenobarbital_toxico.descripcion, gravedad: gravedadDe('fenobarbital'), parametros: ['fenobarbital'], }); if (esBajo('ciclosporina')) agregar({ nombre: alt.ciclosporina_subterapeutica.nombre, descripcion: alt.ciclosporina_subterapeutica.descripcion, gravedad: gravedadDe('ciclosporina'), parametros: ['ciclosporina'], }); if (esAlto('ciclosporina')) agregar({ nombre: alt.ciclosporina_toxica.nombre, descripcion: alt.ciclosporina_toxica.descripcion, gravedad: gravedadDe('ciclosporina'), parametros: ['ciclosporina'], }); // Coagulación if (esAlto('pt') && !esAlto('aptt')) agregar({ nombre: alt.coagulopatia_extrinseca.nombre, descripcion: alt.coagulopatia_extrinseca.descripcion, gravedad: gravedadDe('pt'), parametros: ['pt'], }); if (esAlto('aptt') && !esAlto('pt')) agregar({ nombre: alt.coagulopatia_intrinseca.nombre, descripcion: alt.coagulopatia_intrinseca.descripcion, gravedad: gravedadDe('aptt'), parametros: ['aptt'], }); if (esAlto('pt') && esAlto('aptt')) agregar({ nombre: alt.coagulopatia_mixta.nombre, descripcion: alt.coagulopatia_mixta.descripcion, gravedad: gravedadDe('pt', 'aptt', 'act'), parametros: ['pt', 'aptt', ...(presente('act') ? ['act'] : [])].filter(presente), }); if ((esAlto('ddimeros') || esAlto('fdp')) && esBajo('fibrinogeno')) agregar({ nombre: alt.cid.nombre, descripcion: alt.cid.descripcion, gravedad: 'grave', parametros: ['ddimeros', 'fdp', 'fibrinogeno', 'plt'].filter(presente), }); if (esAlto('fibrinogeno') && !esAlto('ddimeros') && !esAlto('fdp')) agregar({ nombre: alt.hiperfibrinogenemia.nombre, descripcion: alt.hiperfibrinogenemia.descripcion, gravedad: gravedadDe('fibrinogeno'), parametros: ['fibrinogeno'], }); if (esBajo('fibrinogeno') && !esAlto('ddimeros') && !esAlto('fdp')) agregar({ nombre: alt.hipofibrinogenemia.nombre, descripcion: alt.hipofibrinogenemia.descripcion, gravedad: gravedadDe('fibrinogeno'), parametros: ['fibrinogeno'], }); if (esBajo('vwf')) agregar({ nombre: alt.deficit_vwf.nombre, descripcion: alt.deficit_vwf.descripcion, gravedad: gravedadDe('vwf'), parametros: ['vwf', ...(presente('aptt') ? ['aptt'] : [])].filter(presente), }); if (esBajo('antitrombina')) agregar({ nombre: alt.antitrombina_baja.nombre, descripcion: alt.antitrombina_baja.descripcion, gravedad: gravedadDe('antitrombina'), parametros: ['antitrombina'], }); // Urianálisis — sedimento / UPC if (esAlto('rbc_uri')) agregar({ nombre: alt.hematuria_uri.nombre, descripcion: alt.hematuria_uri.descripcion, gravedad: gravedadDe('rbc_uri'), parametros: ['rbc_uri'], }); if (esAlto('wbc_uri')) agregar({ nombre: alt.piuria.nombre, descripcion: alt.piuria.descripcion, gravedad: gravedadDe('wbc_uri'), parametros: ['wbc_uri'], }); if (esAlto('upc')) agregar({ nombre: alt.proteinuria_upc.nombre, descripcion: alt.proteinuria_upc.descripcion, gravedad: gravedadDe('upc'), parametros: ['upc'], }); // Gasometría — ácido-base if (presente('ph_sangre')) { const phBajo = esBajo('ph_sangre'); const phAlto = esAlto('ph_sangre'); const hipercarbia = esAlto('pco2'); const hipocarbia = esBajo('pco2'); const componenteAcidMet = esBajo('hco3') || esBajo('exceso_base'); const componenteAlcalMet = esAlto('hco3') || esAlto('exceso_base'); if (phBajo) { if (hipercarbia && componenteAcidMet) { agregar({ nombre: alt.acidosis_respiratoria.nombre + ' + ' + alt.acidosis_metabolica.nombre, descripcion: alt.acidosis_metabolica.descripcion, gravedad: 'grave', parametros: ['ph_sangre', 'pco2', 'hco3', 'exceso_base'].filter(presente), }); } else if (hipercarbia) { agregar({ nombre: alt.acidosis_respiratoria.nombre, descripcion: alt.acidosis_respiratoria.descripcion, gravedad: gravedadDe('ph_sangre', 'pco2'), parametros: ['ph_sangre', 'pco2'].filter(presente), }); } else if (componenteAcidMet) { agregar({ nombre: alt.acidosis_metabolica.nombre, descripcion: alt.acidosis_metabolica.descripcion, gravedad: gravedadDe('ph_sangre', 'hco3', 'exceso_base'), parametros: ['ph_sangre', 'hco3', 'exceso_base', 'anion_gap'].filter(presente), }); } } if (phAlto) { if (hipocarbia && componenteAlcalMet) { agregar({ nombre: alt.alcalosis_respiratoria.nombre + ' + ' + alt.alcalosis_metabolica.nombre, descripcion: alt.alcalosis_metabolica.descripcion, gravedad: 'grave', parametros: ['ph_sangre', 'pco2', 'hco3', 'exceso_base'].filter(presente), }); } else if (hipocarbia) { agregar({ nombre: alt.alcalosis_respiratoria.nombre, descripcion: alt.alcalosis_respiratoria.descripcion, gravedad: gravedadDe('ph_sangre', 'pco2'), parametros: ['ph_sangre', 'pco2'].filter(presente), }); } else if (componenteAlcalMet) { agregar({ nombre: alt.alcalosis_metabolica.nombre, descripcion: alt.alcalosis_metabolica.descripcion, gravedad: gravedadDe('ph_sangre', 'hco3', 'exceso_base'), parametros: ['ph_sangre', 'hco3', 'exceso_base'].filter(presente), }); } } } if (esBajo('po2')) agregar({ nombre: alt.hipoxemia.nombre, descripcion: alt.hipoxemia.descripcion, gravedad: gravedadDe('po2', 'so2'), parametros: ['po2', ...(presente('so2') ? ['so2'] : [])].filter(presente), }); if (esAlto('lactato')) agregar({ nombre: alt.hiperlactatemia.nombre, descripcion: alt.hiperlactatemia.descripcion, gravedad: gravedadDe('lactato'), parametros: ['lactato'], }); if (esBajo('ca_ion')) agregar({ nombre: alt.ca_ionizado_bajo.nombre, descripcion: alt.ca_ionizado_bajo.descripcion, gravedad: gravedadDe('ca_ion'), parametros: ['ca_ion'], }); if (esAlto('ca_ion')) agregar({ nombre: alt.ca_ionizado_alto.nombre, descripcion: alt.ca_ionizado_alto.descripcion, gravedad: gravedadDe('ca_ion'), parametros: ['ca_ion'], }); if (esAlto('anion_gap')) agregar({ nombre: alt.anion_gap_elevado.nombre, descripcion: alt.anion_gap_elevado.descripcion, gravedad: gravedadDe('anion_gap'), parametros: ['anion_gap', ...(presente('lactato') ? ['lactato'] : [])].filter(presente), }); return patrones; }; // Exportación principal export const analizarResultados = ( valoresInput: ValoresFormulario, paciente: Paciente, referencias: Referencias, alteraciones: Alteraciones, ajustes: AjustesClinicos, ): ResultadoAnalisis => { const especie = paciente.especie; const refsEspecie = especie ? referencias[especie] : undefined; if (!refsEspecie || !especie) return { hallazgos: [], patrones: [] }; // Ajusta los rangos segun edad, raza y sexo antes de comparar const refsAjustadas = ajustarReferencias(refsEspecie, paciente, ajustes); const hallazgos: Hallazgo[] = []; for (const [clave, ref] of Object.entries(refsAjustadas)) { const crudo = valoresInput[clave]; if (crudo === null || crudo === undefined || crudo === '') continue; const valorNum = typeof crudo === 'number' ? crudo : parseFloat(crudo); if (isNaN(valorNum)) continue; if (valorNum > ref.superior) { hallazgos.push({ clave, nombre: ref.nombre, valor: valorNum, unidad: ref.unidad, direccion: 'alto', gravedad: clasificarGravedad(valorNum, ref, ajustes, clave, especie), }); } else if (valorNum < ref.inferior) { hallazgos.push({ clave, nombre: ref.nombre, valor: valorNum, unidad: ref.unidad, direccion: 'bajo', gravedad: clasificarGravedad(valorNum, ref, ajustes, clave, especie), }); } } return { hallazgos, patrones: detectarPatrones(hallazgos, especie, alteraciones, ajustes) }; }; // Exportado para pruebas unitarias del cálculo de gravedad de forma aislada. export const _internos = { clasificarGravedad, categorizarEdad, ajustarReferencias };