morphos / frontend /src /analisis.ts
Jose Salazar
Los cortes IRIS también pasan a data/ajustes_clinicos.json
e6acd07
Raw
History Blame Contribute Delete
33.9 kB
// 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<Especie, string[]> = {
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<TablaAjustes>((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<ReferenciasEspecie>((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<Record<string, Hallazgo>>((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 };