MEMORIA TÉCNICA

Registro de la Propiedad Intelectual de la Comunidad de Madrid

Obra: WHeat-Jobs | Fenotipo

Copyright © 2026 Ricardo M. Trigo Calonge
versión 1.0
Inscrita en el Registro Territorial de la Propiedad Intelectual de Madrid · Asiento 16/2026/8798 · Fecha de efecto 07/09/2026

1. Identificación de la obra y declaración de autoría

Autor de la obra software
Ricardo Manuel Trigo Calonge
Doctor en Derecho · Licenciado con Grado en Ciencias Químicas · Máster en Fisioterapia, Fisiología y Psicología del Deporte
Madrid, España · 2026

La presente memoria técnica describe la estructura, arquitectura, organización funcional, persistencia, lógica algorítmica y subsistemas principales de la obra software WHeat-Jobs | Fenotipo, a efectos de identificación técnica de la obra y de exposición ordenada de sus elementos originales de diseño e implementación.

Se hace constar que el Dr. Ricardo Manuel Trigo Calonge es autor del diseño funcional, de la arquitectura de software, de la organización lógica del sistema, del modelo algorítmico, del diseño de persistencia y del código fuente incorporado a la presente obra. La eventual utilización de herramientas de asistencia a la programación no altera la autoría sobre la concepción, selección, validación e integración final del resultado.

2. Ficha técnica y entorno de ejecución

Naturaleza de la obra: Aplicación web de gestión biométrica, nutricional y clínica, con motor de evaluación funcional heurística auto-referenciada y asistencia mediante inteligencia artificial
Arquitectura de software: Patrón Modelo-Vista-Controlador (MVC) con separación entre presentación, servicios, persistencia técnica, cálculo y evaluación heurística
Lenguaje y framework: C# v13.0 | .NET 10.0 | ASP.NET Core MVC | Visual Studio 2026
Gestión de datos: SQL Server y Entity Framework Core (Code-First)
Autenticación y acceso: Microsoft AspNetCore Identity con tipado fuerte de modelos y validación estructurada
Motores de cálculo: Motor hardcodeado, motor declarativo y modo comparativo dual con resolución en tiempo de ejecución
Unidad lógica de entrada: HealthDashDataBundle como Colección de Datos Funcionales de perfil, biometría diaria y analítica clínica
Entorno de despliegue: Servidor web con soporte .NET Runtime y almacenamiento interno de ficheros funcionales de trabajo
Canal de importación masiva: Subida web de ficheros comprimidos mediante ZIP fragmentado por bloques (chunked upload), con recomposición secuencial en servidor, extracción controlada y persistencia interna del export.xml
Importación analítica clínica: Lectura automática de informes de laboratorio en PDF mediante IA nativa (AnthropicClient con documento base64 directo, sin OCR previo): extracción de hasta 60 biomarcadores con conversión automática de unidades por laboratorio, revisión interactiva con semáforo de rangos de referencia y persistencia en AnaliticaClinica

Esquema de la arquitectura lógica del sistema WHeat-Jobs | Fenotipo

Diagrama de Arquitectura

Diagrama 1

3. Descripción funcional y algorítmica

3.1. Descripción funcional general

WHeat-Jobs | Fenotipo es una plataforma web orientada a la integración, estructuración y explotación funcional de datos biométricos diarios, analíticas clínicas y variables personales de contexto, con la finalidad de generar indicadores compuestos, seguir su evolución temporal y asistir en la interpretación funcional del estado del usuario.

El sistema integra tres fuentes de datos primarias: PerfilPersonal, BiodataDiaria y AnaliticaClinica. A partir de ellas construye una unidad lógica de entrada denominada Colección de Datos Funcionales o HealthDashDataBundle, que constituye la base inmediata del cálculo técnico.

La aplicación no se limita al almacenamiento o visualización de información, sino que incorpora una arquitectura propia de formación de contexto fisiológico, cálculo técnico, persistencia histórica, reconstrucción temporal y evaluación heurística orientada a objetivo.

Junto a la gestión manual y estructurada de datos fisiológicos y clínicos, el sistema incorpora un subsistema de importación masiva destinado a la ingestión de exportaciones externas de datos de salud generadas por plataformas de terceros y dispositivos personales. En su estado actual, dicho subsistema admite la subida de ficheros comprimidos ZIP que contienen un export.xml de gran tamaño, evitando la carga directa monolítica del XML mediante una estrategia de subida fragmentada por bloques, recomposición en servidor, extracción controlada y persistencia interna.

Esta arquitectura desacopla la fase de transporte del fichero desde el equipo del usuario y la fase de lectura funcional del XML ya persistido. En consecuencia, el sistema no opera sobre rutas locales del cliente, sino sobre una copia interna validada y almacenada en servidor, reutilizable para lecturas posteriores, generación de archivos reducidos por semana y trazabilidad técnica.

La formación de la Colección de Datos Funcionales sigue reglas temporales determinadas. La analítica clínica utilizada es la última analítica disponible con fecha igual o anterior a la fecha de cálculo. La biometría diaria, por su parte, se selecciona dentro de una ventana temporal configurable de días naturales, aplicada de forma homogénea al cálculo actual y al cálculo histórico reconstruido.

En la configuración operativa vigente, la ventana biométrica queda expresada, con carácter general, como VentanaBiométrica = [ FechaCalculo − 6 días , FechaCalculo ], siendo ambas fechas inclusivas. En consecuencia, el número máximo teórico de registros biométricos integrados por cálculo es de 7 registros diarios, salvo ausencia material de datos en alguna de las fechas comprendidas.

El núcleo funcional actual se articula mediante tres índices: Score SNC, Score Metabólico y Score Bioenergético. Dichos scores se obtienen a partir del HealthDashDataBundle, incorporando reglas de interpretación, umbrales fisiológicos y, cuando procede, un factor corrector de fiabilidad analítica en función de la antigüedad de la analítica utilizada.

Junto a dichos scores compuestos, la aplicación incorpora además una línea de análisis paralela centrada en la evolución temporal de variables directas de BiodataDiaria y un subsistema específico de análisis energético-metabólico diario basado en EnergyDailyBalance. Estas capas no sustituyen al scoring, sino que lo complementan al permitir examinar tendencias recientes, balance energético, suficiencia proteica y disponibilidad energética funcional desde una perspectiva diaria y longitudinal.

El cálculo técnico se articula mediante una fachada de motor de score capaz de resolver dinámicamente distintas implementaciones. El sistema soporta un motor hardcodeado de referencia operativa, un motor declarativo alineado funcional y matemáticamente con el motor hardcodeado, y un modo de comparación dual destinado al contraste técnico entre ambos.

A diferencia de una implementación declarativa meramente efímera o dependiente únicamente del estado dinámico del sistema, la arquitectura actual incorpora persistencia histórica específica del motor declarativo. En cada ejecución declarativa se registra no solo el resultado numérico obtenido, sino también el estado declarativo efectivo aplicado en ese momento, incluyendo serialización estructurada, metadatos de versión y huella criptográfica de integridad.

La persistencia del estado declarativo efectivo permite reconstruir con precisión el modelo utilizado en cualquier cálculo histórico, facilitando auditoría técnica, comparación longitudinal entre configuraciones y verificación de consistencia lógica entre versiones.

3.2. Fundamentación científico-técnica del motor hardcoded

El motor hardcoded de WHeat-Jobs | Fenotipo no se configura como un sistema de diagnóstico médico autónomo, sino como un modelo funcional de estratificación fisiológica. Su estructura combina, de una parte, umbrales apoyados en referencias clínicas o biomédicas consolidadas y, de otra, tramos heurísticos internos orientados a monitorización funcional, detección de desviaciones y generación prudencial de recomendaciones.

En el dominio metabólico, la base científica es comparativamente más estable. La glucosa basal y la hemoglobina glicosilada (HbA1c) se apoyan en estándares contemporáneos de clasificación de normoglucemia, prediabetes y diabetes, que el sistema reutiliza en forma simplificada para producir scoring funcional. De igual modo, triglicéridos y colesterol HDL se incorporan como variables de contexto cardiometabólico.

La variable pasos / NEAT no constituye un criterio diagnóstico clínico formal, pero sí un marcador funcional razonable del volumen de movimiento habitual. La evidencia y las recomendaciones internacionales de actividad física respaldan la relevancia del movimiento cotidiano y del descenso del sedentarismo como ejes de salud general.

En el dominio SNC, el modelo utiliza HRV, sueño, frecuencia cardiaca en reposo, tensión arterial y magnesio sérico. La literatura apoya que la HRV disminuye con la edad y que una mayor variabilidad suele asociarse a mejor flexibilidad autonómica y mejor capacidad de recuperación; no obstante, los puntos de corte concretos empleados por el motor no equivalen a umbrales diagnósticos universales.

En materia de sueño, el motor combina el tiempo total de sueño con el sueño profundo y, cuando existe dato disponible, con otras variables de arquitectura del sueño. Las guías de referencia recomiendan entre 7 y 9 horas en adultos de 18 a 64 años, y entre 7 y 8 horas en mayores de 65. A partir de esa edad la relación entre duración del sueño y resultados en salud muestra una curva en U: tanto el déficit como el exceso se asocian a mayor mortalidad, y la arquitectura del sueño cambia fisiológicamente con la edad (menor proporción de sueño profundo, mayor fragmentación). Los umbrales del motor reflejan estas matizaciones, sin pretender equivalencia con criterios diagnósticos.

La frecuencia cardiaca en reposo se incorpora porque una frecuencia más elevada se ha asociado, a nivel poblacional, con mayor mortalidad total y cardiovascular. En cuanto a la presión arterial, el sistema utiliza una traducción prudente de la literatura y de las guías contemporáneas.

El bloque de magnesio sérico posee respaldo biomédico, aunque con menor uniformidad absoluta en torno al concepto de nivel óptimo. La literatura reciente sobre estandarización de rangos de referencia propone prestar especial atención a niveles séricos en torno a 0,85 mmol/L.

En el dominio bioenergético, hemoglobina, TSH y ferritina constituyen el núcleo base. La hemoglobina se emplea como marcador indirecto de capacidad de transporte de oxígeno; la TSH, como proxy de regulación tiroidea; y la ferritina, como indicador de reservas de hierro.

La modulación bioenergética por sueño profundo, HRV y frecuencia cardiaca en reposo responde a una hipótesis fisiológica plausible: el estado de recuperación autonómica y del sueño condiciona la expresión funcional del sistema bioenergético.

Conclusión metodológica: el motor hardcoded de WHeat-Jobs | Fenotipo es un sistema mixto: combina tramos clínicamente anclados con tramos funcionales heurísticos. La convergencia posterior del motor declarativo acredita la reproducción fiel de esa lógica, pero no transforma automáticamente todos los umbrales en estándares clínicos oficiales.
3.3. Evaluación heurística orientada a objetivo
Precisión terminológica del sistema: el HealthDashDataBundle constituye la Colección de Datos Funcionales de entrada; los resultados numéricos de cada motor constituyen los scores técnicos; y el snapshot funcional representa la síntesis resultante de la consideración conjunta de los motores, su comparación técnica, la interpretación heurística, las recomendaciones y el prediagnóstico funcional.

Sobre el resultado técnico de cálculo, el sistema aplica una segunda capa de evaluación heurística destinada a determinar la dirección del estado, la magnitud del cambio, la alineación con el objetivo y el comportamiento de la pauta.

Esta lógica permite emitir decisiones operativas sobre la pauta, distinguiendo entre mantenimiento, ajuste, intensificación o sustitución, sin confundir la mera convergencia con el cumplimiento efectivo del objetivo.

El sistema dispone de persistencia técnica del cálculo y de persistencia heurística del resultado operativo actual. La primera se registra en HealthScoreHistory; la segunda, en HealthEvolutionSnapshot.

El cálculo histórico puede visualizarse como score técnico persistido o como reconstrucción histórica recalculada sobre datos fuente, manteniéndose el score técnico persistido como referencia principal de trazabilidad.

3.4. Especificación práctica del algoritmo
Especificación práctica del algoritmo

El algoritmo opera en ciclos de captura de contexto, importación, agregación, cálculo, evaluación heurística y persistencia.

FASE 0
Captura externa e importación masiva
Selección por el usuario de un fichero export.zip, subida web fragmentada por bloques, recomposición secuencial del ZIP en servidor, extracción controlada del export.xml y persistencia interna del XML resultante como fuente estable de lectura.
FASE 0 BIS
Reducción semanal, agregación diaria y normalización Biodata
A partir del export.xml ya persistido, el sistema genera un XML reducido por semana natural, reconstruye por fecha la información fisiológica relevante mediante lectura estructural en streaming y transforma el resultado en registros normalizados compatibles con BiodataDiaria.
FASE A
Contexto
Obtención de PerfilPersonal, selección de la ventana temporal configurada de BiodataDiaria y determinación de la AnaliticaClinica más próxima anterior o igual a la fecha de cálculo.
FASE B
Cálculo técnico
Cálculo de Score SNC, Score Metabólico y Score Bioenergético mediante motor técnico seleccionable.
FASE C
Recomendación inmediata
Generación de recomendaciones basadas en el estado funcional actual y en el cruce técnico entre scores y biomarcadores.
FASE D
Evaluación evolutiva y tendencias
Contraste entre histórico, pauta activa y objetivo para determinar dirección, alineación y decisión operativa, incorporando análisis longitudinal de variables directas de BiodataDiaria.
FASE E
Persistencia y trazabilidad
Persistencia del cálculo técnico en HealthScoreHistory, del resultado heurístico actual en HealthEvolutionSnapshot y del balance energético diario en EnergyDailyBalance.
ECUACIÓN CONCEPTUAL DE DESVIACIÓN FUNCIONAL
ΔEstado = |Objetivo - Real|
Expresión conceptual del diferencial entre el estado objetivo y el estado observado. Su traducción operativa se realiza mediante scores, histórico técnico y evaluación heurística orientada a objetivo.

En suma, el sistema analiza el estado actual del usuario, la dirección de su evolución y el comportamiento de la pauta, a fin de determinar si la intervención aplicada aproxima efectiva y progresivamente al objetivo funcional previsto.

3.5. Arquitectura de variables del algoritmo
Arquitectura de variables del algoritmo

El sistema distingue entre variables de latencia aguda y crónica. Aunque la base de datos admite un conjunto amplio de parámetros biométricos, clínicos y de perfil, el motor heurístico opera en su estado actual sobre un núcleo progresivamente auditado de variables esenciales, susceptible de ampliación o recalibración.

LATENCIA AGUDA Vectores dinámicos
  • TemperaturaCorporal: registro térmico basal.
  • HRV: equilibrio del sistema nervioso autónomo.
  • FrecuenciaCardiacaReposo: mínimo absoluto de pulso durante la ventana de sueño real (no la media del día, ver 8.21).
  • SueñoProfundoMinutos: reparación física.
  • SueñoRemMinutos: recuperación neurocognitiva.
  • Peso: masa corporal total.
  • PasosDiarios: movimiento NEAT.
  • LitrosAgua: hidratación registrada.
  • CaloriasConsumidas: ingesta energética diaria.
LATENCIA CRÓNICA Vectores estructurales
  • GlucosaAyunas: azúcar basal.
  • HemoglobinaGlicosilada: media glucémica de largo plazo.
  • Trigliceridos: grasas circulantes.
  • ColesterolHDL: fracción protectora.
  • ColesterolLDL: fracción aterogénica.
  • ProteinaCReactiva: inflamación de bajo grado.
  • Ferritina: almacén de hierro.
  • TSH: regulación tiroidea.
  • VitaminaD3: soporte inmunometabólico.
  • GGT / ALT: marcadores hepáticos.
  • Creatinina / FiltradoGlomerular: eficiencia renal.
  • CPK: daño muscular.
  • MagnesioSerico: regulador neuromuscular y metabólico.

4. Arquitectura de datos y lógica de negocio

El sistema se apoya en una arquitectura relacional y de servicios orientada a separar con claridad los datos fuente, la formación de la Colección de Datos Funcionales de entrada, el cálculo técnico, la interpretación heurística y la persistencia del resultado.

Datos base: PerfilPersonal

Define el marco basal y estratégico del usuario. Dispone de CRUD completo.

  • Biometría basal: edad, sexo, altura y composición.
  • Contexto: profesión, clima, entorno y hábitos.
  • Dirección: objetivo principal del usuario.
Datos base: BiodataDiaria

Registra la evolución cotidiana del usuario. Dispone de CRUD completo.

  • Actividad: ejercicio, pasos, carga diaria.
  • Recuperación: HRV, sueño, fatiga, estrés.
  • Homeostasis: pulso, temperatura, SpO2, hidratación.
  • Capacidad cardiorrespiratoria: VO2 máx (ml/kg/min), último valor medido.
Datos base: AnaliticaClinica

Aporta validación bioquímica estructural. Dispone de CRUD completo.

  • Metabolismo: glucosa, HbA1c, lípidos.
  • Inflamación y reservas: ferritina, PCR, vitaminas.
  • Función orgánica: renal, hepática, endocrina.
Servicios funcionales principales
  • HealthDashDataService: construcción de la Colección de Datos Funcionales a partir de perfil, biometría diaria y analítica clínica.
  • HealthImportController: orquestación MVC del proceso de importación, lectura semanal, generación de XML reducido y agregación posterior.
  • HealthImportPathService: resolución centralizada de rutas internas para export.xml, XML reducidos semanales y directorios temporales.
  • HealthScoreService: fachada central del cálculo técnico de scores.
  • HardcodedHealthScoreEngine: implementación operativa estable de referencia.
  • DeclarativeHealthScoreEngine: implementación basada en configuración dinámica de bloques, inputs y reglas.
  • HealthScoreModeResolver: resolución del motor activo en tiempo de ejecución.
  • HealthRecommendationService: recomendación inmediata basada en estado actual.
  • HealthScoreHistoryService: persistencia del score técnico del cálculo.
  • DeclarativeHistoryService: persistencia histórica específica del motor declarativo.
  • HealthScoreBackfillService: reconstrucción retrospectiva del histórico técnico.
  • BioDataEvolutionService / BioDataTrendAnalysisService: análisis de tendencias sobre variables persistidas en BiodataDiaria.
  • EnergyDailyBalanceService: cálculo y persistencia del balance energético diario, macronutrientes, proteína/kg, alcohol, gasto total y clasificación oficial del estado energético.
  • EnergyDailyBalanceQueryService: construcción de modelos de consulta y visualización del análisis metabólico diario.
  • HealthTrendService: análisis y proyección de tendencia sobre histórico persistido.
  • HealthEvolutionService: evaluación evolutiva orientada a objetivo.
  • HealthEvolutionOrchestrator: coordinación del flujo heurístico completo.
  • PlanAdjustmentService: decisión sobre mantenimiento, ajuste, intensificación o sustitución de la pauta.
  • AnaliticaInterpretService: interpretación clínica asistida por IA de una AnaliticaClinica ya persistida, generando un informe HTML contextualizado.
  • AnaliticaPdfImportService: extracción automática de biomarcadores desde PDF de laboratorio mediante AnthropicClient con documento nativo base64; aplica reglas de conversión de escala (×10ⁿ, mg/dL↔mmol/L, pmol/L↔ng/dL) y retorna el resultado para revisión antes de persistir.
  • OllamaCompletionService / ClaudeCompletionService: motores de completado IA para Ollama Cloud y Claude API respectivamente.
  • AiBackgroundWorker: servicio hospedado de procesamiento asíncrono de tareas IA mediante cola de canal interno (Channel<T>).
  • IBackgroundTaskQueue / IAiResultStore / IAiTimingService: infraestructura de encadenamiento, almacenamiento de resultados y estimación de tiempos de respuesta IA.
  • AiProviderSettings: gestión en tiempo de ejecución del proveedor IA activo (global y por administrador), con consola de administración dedicada.
  • HistoriaPacienteService: generación del informe de revisión histórica parcial para el período seleccionado: compila perfil, evolución de scores SNC/MET/BIOE, balance energético por período, macronutrientes medios, analítica clínica con verificación de ~55 parámetros contra rangos de referencia (BuildParametros) y apéndice de registros diarios brutos; construye las variables de contexto para el prompt IA (HistoriaPacientePromptVars).
  • HistoriaPacientePromptSeeder: seeder que persiste en base de datos la plantilla versionada HISTORIA_PACIENTE con sus bloques de contenido (IaPromptBlock), incluyendo las reglas de interpretación clínica estáticas y los marcadores [FUERA DE RANGO] para guiar al modelo.
4.1. Subsistema de análisis de tendencias de BiodataDiaria

Junto al cálculo técnico de scores y a la evaluación heurística global, el sistema incorpora un subsistema específico de análisis de tendencias sobre la entidad BiodataDiaria, destinado a observar la dirección, estabilidad y magnitud del cambio de variables fisiológicas cotidianas.

Este subsistema opera sobre una ventana temporal seleccionable de registros diarios y permite obtener una lectura sintética de la evolución reciente de variables como peso, cintura, frecuencia cardiaca en reposo, HRV, sueño, distancia recorrida, pasos y energía activa.

Desde el punto de vista arquitectónico, este módulo constituye una capa analítica intermedia entre la persistencia diaria de biometría y la evaluación heurística global.

4.2. Subsistema de análisis metabólico diario y dinámica energética funcional

El sistema incorpora un subsistema específico de análisis energético-metabólico diario, destinado a interpretar la relación entre ingesta, gasto energético, distribución nutricional, proteína relativa al peso corporal, actividad física y balance energético final.

La fuente técnica principal de este subsistema es la entidad EnergyDailyBalance, generada a partir de BiodataDiaria y recalculada mediante servicios internos. Dicha entidad consolida la energía ingerida, el gasto basal ajustado, el gasto activo ajustado, el gasto total, el balance energético, los macronutrientes, el alcohol ingerido, la proteína por kilogramo de peso corporal y el estado energético resultante.

La clasificación oficial del estado energético se centraliza en EnergyDailyBalanceService.ResolveEnergyStatus, evitando que las vistas, prompts o módulos auxiliares reclasifiquen el balance mediante umbrales paralelos.

El estado se calcula como porcentaje del balance sobre un gasto energético de referencia, no en kcal absolutas: un mismo déficit en kcal representa un esfuerzo metabólico distinto según el gasto de cada persona. El gasto de referencia es la media personal de gasto total (basal + activo) de los últimos 15 días cerrados del propio usuario, excluyendo el día evaluado; si el usuario todavía no tiene ningún día cerrado con gasto registrado, se usa el gasto total de ese mismo día.

  • DeficitMuyAlto: balance ≤ -35% del gasto total.
  • DeficitSevero: balance ≤ -28% del gasto total.
  • DeficitAlto: balance ≤ -22% del gasto total.
  • DeficitModerado: balance ≤ -15% del gasto total.
  • DeficitLigero: balance ≤ -7% del gasto total.
  • Equilibrado: balance entre -7% y 7% del gasto total.
  • SuperavitModerado: balance ≤ 20% del gasto total.
  • SuperavitAlto: balance > 20% del gasto total.

Sobre esta base se articula el módulo de Análisis metabólico diario, que ofrece una lectura funcional del día evaluado: ingesta total, gasto estimado, balance calórico, distribución de hidratos, proteínas, grasas y alcohol, suficiencia proteica relativa y compatibilidad del patrón observado con preservación de masa magra o pérdida progresiva de grasa.

De forma complementaria, el módulo de Dinámica metabólica representa visualmente la evolución funcional del día mediante una curva por fases, integrando balance energético acumulado e índice relativo de disponibilidad energética. Este índice no constituye una magnitud física directa, sino una señal sintética orientativa que combina balance energético, proteína/kg, hidratos, grasas y alcohol.

Desde el punto de vista arquitectónico, ambos módulos forman una capa analítica específica dentro del sistema, conectada con BiodataDiaria y EnergyDailyBalance, pero separada del motor principal de scores.

Formación de la Colección de Datos Funcionales y regla temporal de cálculo

La unidad efectiva de cálculo del sistema es el HealthDashDataBundle, integrado por: PerfilPersonal, una colección acotada de BiodataDiaria y una AnaliticaClinica seleccionada por proximidad temporal anterior o igual a la fecha de cálculo.

La selección de biometría diaria se realiza mediante una ventana temporal natural de N días, siendo N un parámetro configurable del sistema. Cuando N = 7, equivale a [ FechaCalculo − 6 , FechaCalculo ].

La regla temporal aplicada queda reflejada en histórico a través de los campos FechaBioInicio, FechaBioFin, NumeroRegistrosBioUsados y ReglaAplicada.

Arquitectura de importación masiva y persistencia interna del XML

El sistema incorpora una arquitectura específica para la ingestión de exportaciones XML de gran tamaño, basada en la subida de un fichero ZIP fragmentado en bloques.

Una vez reconstruido el ZIP, el sistema realiza una extracción controlada del contenido, localiza el export.xml válido y lo copia a la ubicación interna definitiva del usuario.

Subsistema de importación estructurada y generación automática de BiodataDiaria

La arquitectura actual incorpora un subsistema específico de ingestión documental, reducción estructural, agregación funcional y transformación normalizada de datos externos en registros persistentes de BiodataDiaria.

Una vez persistido el export.xml, la aplicación genera archivos reducidos por semana natural, construidos mediante lectura secuencial en streaming y filtrado selectivo de nodos relevantes.

Sobre dichos XML reducidos semanales actúa un motor de agregación diaria diseñado para reconstruir, por fecha, la información fisiológica relevante contenida en el documento.

Esta cadena captura documental → persistencia interna → reducción semanal → agregación diaria → normalización → persistencia en BiodataDiaria constituye un componente técnico propio del sistema.

Validación y tipado: los modelos emplean DataAnnotations, precisión decimal y restricciones de persistencia mediante Entity Framework Core y SQL Server.

4.3. Calibración del factor de corrección de ingesta calórica (OllamaIntakeCorrectionFactor)

La IA estima la ingesta calórica a partir de los alimentos declarados por el usuario, aplicando el sistema Atwater general (4/4/9/7 kcal por gramo de hidratos/proteína/grasa/alcohol). El resultado se desvía del balance de masa real por varias causas mezcladas — imprecisión de la IA al interpretar la declaración, imprecisión del propio Atwater general (no distingue fibra, matriz alimentaria ni eficacia de absorción real del individuo) y hábitos de registro del usuario — que el sistema no puede aislar entre sí. El sistema incorpora un mecanismo de calibración empírica que calcula un factor multiplicador (OllamaIntakeCorrectionFactor) individual que absorbe el conjunto de esas desviaciones, no solo el error de la IA.

Principio físico del balance energético

La calibración se fundamenta en la identidad del balance energético:

IngestaReal = GastoTotal + (ΔPeso × 7700 kcal/kg)

Donde ΔPeso es la variación de peso corporal en el período analizado (negativo si hubo pérdida, positivo si hubo ganancia) y 7700 kcal/kg es el equivalente energético aproximado de un kilogramo de tejido adiposo. Esta igualdad permite deducir cuántas kilocalorías reales ingirió el usuario a partir de su gasto medido y la evolución de su peso, sin depender en absoluto de la estimación de la IA.

El factor de corrección se obtiene entonces como:

Factor = IngestaReal / SumaOllamaRawKcal

Si el factor resultante es mayor que 1, la ingesta real supera a la calculada por Atwater sobre lo declarado; si es menor que 1, es inferior. El factor es individual y mezcla todas las causas de desviación (IA, Atwater general, absorción real) sin distinguir cuál pesa más en cada caso. Aplicarlo a las estimaciones futuras produce una ingesta corregida más cercana a la realidad fisiológica del usuario en conjunto, no una corrección aislada del error de la IA.

Limitación conocida: la equivalencia de 7 700 kcal/kg solo es razonablemente válida a largo plazo (meses). A corto plazo, la mayor parte de la variación de peso refleja cambios en agua corporal, glucógeno muscular y contenido intestinal, no tejido adiposo real. Por eso el sistema exige un mínimo de 30 días para considerar el factor «Bueno» y 60 días para «Excelente», y prefiere la regresión OLS al endpoint cuando R² es suficiente: ambas medidas reducen el peso del ruido diario, pero no lo eliminan. El factor debe interpretarse como una estimación orientativa de la tendencia del usuario, no como una verdad fisiológica exacta.
Dos variantes del factor: Endpoint y Regresión OLS

El servicio EnergyCalibrationRegressionService calcula dos estimaciones independientes del ΔPeso:

  • ΔPeso Endpoint: diferencia simple entre el último peso registrado en el período y el primero. Es directo pero sensible a fluctuaciones puntuales (retención de líquidos, hora del pesaje, etc.).
  • ΔPeso Regresión (OLS): se ajusta una recta de mínimos cuadrados ordinarios sobre toda la serie de pesos del período. La pendiente de esa recta (kg/día) multiplicada por el número de días proporciona un ΔPeso suavizado, menos afectado por el ruido diario y más representativo de la tendencia real del tejido corporal.

A cada variante de ΔPeso le corresponde un factor independiente (FactorEndpoint y FactorRegresion).

Factor recomendado y criterio de ponderación

El factor final que se propone al usuario (FactorRecomendado) pondera las dos variantes en función del coeficiente de determinación R² de la regresión del peso:

Si R² ≥ 0,30 → FactorRecomendado = 0,70 × FactorRegresión + 0,30 × FactorEndpoint
Si R² < 0,30 → FactorRecomendado = FactorEndpoint

Un R² bajo indica alta variabilidad en el peso (ruido fisiológico, mediciones irregulares), por lo que la regresión no es fiable y se usa únicamente el endpoint. Con R² suficiente, la regresión recibe mayor peso (70%) por ser más robusta frente al ruido de corto plazo.

El factor resultante se limita al rango [0,80 – 2,50] como salvaguarda ante datos atípicos o períodos de registro incompleto.

Evaluación de la calidad de los datos

El servicio clasifica automáticamente la fiabilidad de la calibración en cuatro niveles:

Nivel Criterio
Excelente≥ 60 días con ingesta y R² ≥ 0,50
Buena≥ 30 días con ingesta y R² ≥ 0,30
Aceptable≥ 20 días con ingesta
Insuficiente< 15 días con ingesta o sin datos de peso

Con menos de 15 días de ingesta declarada el sistema devuelve calidad «Insuficiente» y no aplica el factor. Se recomienda un mínimo de 30 días para un factor fiable.

4.4. Automatización de Ingesta (IntakeAutomationService)

Módulo de siembra sintética que genera una semana completa de registros de ingesta plausibles a partir del historial fisiológico del usuario, sin intervención de IA generativa. El algoritmo es puramente estadístico:

Algoritmo de generación
  1. Se requieren al menos 30 días previos con ingesta registrada en el período de referencia. Sin ese mínimo el servicio rechaza la operación.
  2. Para cada macro (ProteinasIngeridasGr, HidratosIngeridosGr, GrasasIngeridasGr, AlcoholIngeridoGr) se calculan la media (μ) y la desviación típica (σ) por día de semana (lunes…domingo) sobre el período de referencia elegido. Si un día de semana no tiene historial, se usa la estadística global del período.
  3. La σ efectiva se limita: σ_ef = min(σ_hist, μ × 0,25). Así la variación máxima garantizada es el ±25 % de la media, evitando valores absurdos cuando el historial es ruidoso.
  4. Cada valor se muestrea como μ + N(0,1) × σ_ef (método Box-Muller) y se recorta al intervalo [0, μ + 2,5·σ_ef].
  5. Las calorías se calculan por coherencia a posteriori: Kcal = Prot×4 + HC×4 + Grasa×9 + Alcohol×7. No se reescalan los macros — la coherencia calórica es informativa, no forzada.
  6. Solo se siembran los días que no tienen ya ingesta en BiodataDiaria. Los días con datos previos se muestran como "ya existe" en la previsualización y no se modifican.
Limitación y advertencia de uso

La automatización introduce inexactitud estadística en los registros de ingesta. Los valores generados son plausibles con el patrón histórico del usuario pero no son reales. Afectan al balance energético, a la calibración del modelo y a los informes evolutivos. Se recomienda utilizar esta función únicamente cuando no hay datos reales disponibles y la brecha en el historial impide el funcionamiento de otros módulos. Es mejor que no tener nada, pero nunca equivale a la declaración real de ingesta.

Aviso de alérgenos en la estimación nutricional

El módulo de ingesta (tanto en modo IA pura como en el modo híbrido OFf+IA descrito en el apartado 8.14) incorpora una capa adicional de detección de alérgenos declarados en el perfil, desacoplada del cálculo de macros para no comprometer su fiabilidad numérica. Combina un diccionario determinista de los 14 alérgenos de declaración obligatoria de la UE con una capa complementaria de IA, cuyo resultado se filtra por el mismo diccionario antes de fusionarse. Descripción técnica completa en el apartado 8.23.

4.5. Asistente IA conversacional (AiAssistantController)

Panel de chat contextual accesible desde cualquier página de la aplicación mediante un botón flotante. No requiere tablas propias ni migraciones: el historial de conversación se mantiene íntegramente en el cliente (array JS) y se serializa en texto dentro del user prompt en cada petición.

Arquitectura y contexto
  • Reutiliza la interfaz IAiCompletionService existente, compatible con Claude API y Ollama Cloud, sin ningún cambio en los motores subyacentes.
  • El system prompt se construye dinámicamente en cada petición con: (a) manual de funcionalidades de Fenotipo, (b) perfil personal del usuario (PerfilesPersonales) y (c) los últimos 60 días (Ollama) o 180 días (Claude API) de BiodataDiaria en formato tabular compacto, incluyendo pasos, sueño, FC, HRV, SpO₂, peso, energía basal, energía activa, gasto total, ingesta calórica, balance y macros (P/HC/G).
  • El historial se limita a 10 turnos (20 entradas) para controlar el consumo de tokens.
  • Las respuestas se renderizan con marked.js. El contenedor del chat lleva class="tex2jax_ignore" para que MathJax (cargado globalmente) no procese los símbolos matemáticos de las respuestas del modelo.
  • El endpoint POST /AiAssistant/Chat está protegido con [Authorize] y token anti-CSRF.

4.6. Módulo de detección y medición de adaptación metabólica

La adaptación metabólica (adaptive thermogenesis) designa la reducción del gasto energético real por encima de la explicable por cambios de masa corporal, como respuesta homeostática del organismo a la restricción calórica prolongada. El sistema incorpora un módulo propio que cuantifica este fenómeno de forma automática y longitudinal.

Principio de cálculo

El módulo compara dos estimaciones del TDEE (Gasto Energético Total Diario) para el período analizado:

  • TDEE teórico: media del campo EnergyDailyBalance.EnergiaGastoTotalKcal, calculado por EnergyDailyBalanceService como suma directa de Basal Importado y Activa Importada de HealthKit, cada una multiplicada por su factor de calibración personal (UserEnergyCalibration).
  • TDEE real estimado: derivado por conservación de energía:
    TDEE_real = (ΣIngestaCorregida − ΔPeso_kg × 7 700) / n_días
    donde ΣIngestaCorregida es la suma de EnergyDailyBalance.EnergiaIngeridaKcal de los días válidos, calculada con el factor OllamaIntakeCorrectionFactor vigente cada día y solo sobre macros (el alcohol usa fórmula determinista, sin corrección).

La variación de peso se obtiene mediante regresión OLS sobre la serie histórica de peso si R² ≥ 0,25 (criterio de señal suficiente); en caso contrario se usa la diferencia entre endpoints, reduciendo el sesgo de retención hídrica.

Adaptación = TDEE_teórico − TDEE_real. Un valor positivo indica que el cuerpo gasta menos calorías de las que el modelo predice: es la medida operativa de la adaptación.

Tanto el panel principal como el histórico marcan en ámbar las ventanas con R² < 0,50: la tendencia de peso de esa ventana concreta está dominada por ruido no atribuible a cambio real de masa grasa/magra — típicamente retención hídrica por rachas de alcohol de varios días seguidos (que la mediana adaptativa no absorbe tan bien como un pico aislado de un solo día), sal, ciclo hormonal o variabilidad de la báscula. La cifra de Adaptación de esas ventanas no es incorrecta por diseño — sigue calculándose igual — pero debe leerse con cautela: parte del residuo puede reflejar esa fluctuación ajena a la grasa, no una reacción metabólica real.

Aviso de peso sin actualizar (racha)

Distinto del caso anterior: cuando el paciente no se pesa durante varios días, el pipeline de importación (HealthKit Auto Export) rellena BiodataDiaria.Peso con el fallback del último valor real conocido (hasta 30 días), para no dejar el campo vacío. Ese valor heredado es indistinguible de un peso real por su formato — solo se delata porque, en la práctica, dos pesadas reales prácticamente nunca coinciden a la décima: una racha de ≥3 días con el mismo valor exacto es, con altísima probabilidad, ausencia de pesaje, no una meseta fisiológica.

IPesoRachaService detecta esta racha sin depender de metadatos de origen (que solo dos de los cuatro pipelines de importación rellenan de forma fiable): compara el largo de la racha activa (la que toca el día más reciente) contra el máximo histórico de rachas ≥3 días de ese mismo paciente, con un margen — no un umbral fijo para todos, porque pesarse cada pocos días es una cadencia normal recomendada por muchos profesionales, no un hueco de datos. Con menos de 60 días de histórico o menos de 2 rachas previas, se usa un umbral fijo conservador (10 días) hasta que el paciente acumule su propio patrón.

Cuando la racha activa supera ese umbral, AvisoPesoDesactualizado se activa e inhibe la lectura de DeltaPesoKg/composición corporal/Nivel de esa ventana en los módulos que dependen de peso: Adaptación Metabólica, Calibración del modelo, Validación peso/balance, Evolución BioData e Historia Paciente (narrativa incluida). No es un simple aviso visual — las reglas de interpretación automática (p. ej. "pausa asociada a alcohol", "evolución estable") también se auto-inhiben para no atribuir a una causa fisiológica lo que en realidad es un vacío de báscula.

Niveles de adaptación y alertas
Nivel internoUmbral (% del TDEE teórico)Etiqueta en pantallaImplicación clínica
SinAdaptacion< −5 %Muy adaptableGasta más de lo teórico por encima del margen de ruido de la medición, sin freno; muy buena señal.
SinAdaptacion−5 a 10 %Sin adaptaciónGasto coherente con el modelo teórico; esperable en cualquier déficit prolongado.
Leve10–15 %LeveAjuste inicial; vigilar proteína y NEAT.
Moderada15–25 %ModeradaConsiderar reactivación metabólica.
Severa> 25 %SeveraAdaptación significativa; reevaluar pauta.

El umbral se aplica sobre el % de adaptación respecto al TDEE teórico (no kcal absolutas), para que sea comparable entre personas con gasto energético muy distinto; un residual inferior al −5 % (TDEE real por encima del teórico, fuera del margen de ruido de la medición) se etiqueta además como "Muy adaptable". Alertas automáticas: reactivación metabólica cuando la adaptación residual ≥ 15 % del TDEE teórico y período ≥ 20 días con datos; proteína insuficiente cuando cobertura proteica media < 80 % del objetivo.

Serie histórica y persistencia

El módulo calcula una serie histórica de 12 ventanas de 28 días, desplazadas 7 días entre sí, que permite visualizar la evolución de la adaptación a lo largo de los últimos meses. Los snapshots se persisten en MetabolicAdaptationSnapshots, tanto bajo demanda del usuario (recálculo manual de un día concreto) como de forma automática mediante MetabolicSnapshotWorker, servicio en segundo plano que cada noche a las 02:00 recalcula y guarda dos ventanas por usuario: la de D-1 (clave de cálculo el día actual) y la de D-2 (clave de cálculo el día anterior, sobrescribiendo su snapshot). Esta doble pasada compensa que los datos de un día pueden seguir completándose o corrigiéndose durante la mañana o tarde del día siguiente, después de que su primer snapshot ya se hubiera guardado. Los snapshots se representan visualmente con Chart.js (barras de adaptación y línea TDEE teórico vs real).

Causas documentadas en el modal de información del módulo: regulación hormonal (leptina, T3, cortisol, ghrelina), supresión del SNA, colapso del NEAT, aumento de eficiencia muscular (Rosenbaum: 20-25 % a −10 % de peso) y rol de la proteína (TEF, preservación de masa magra, efecto NEAT). Evidencia de reactivación metabólica prolongada: estudio MATADOR (Byrne 2017 — reducción de adaptación del 50 % con descansos de 2 semanas).

5. Persistencia funcional del sistema

HealthScoreHistory

Snapshot técnico persistido del cálculo funcional.

  • FechaCalculo: fecha efectiva del cálculo.
  • FechaAnaliticaUsada: analítica clínica tomada como referencia temporal.
  • FechaBioInicio / FechaBioFin: ventana real de biometría diaria utilizada.
  • NumeroRegistrosBioUsados: número real de registros diarios incluidos.
  • ScoreSNC / ScoreMetabolico / ScoreBioenergetico: resultado técnico del cálculo.
  • ReglaAplicada: regla temporal y técnica declarada para el cálculo.
  • EdadEnFechaCalculo: edad computada para la fecha evaluada.
  • FactorFiabilidadAnalitica: corrector aplicado por antigüedad de la analítica.
HealthEvolutionSnapshot

Persistencia del resultado heurístico completo de la evaluación operativa vigente.

  • Objetivo y dirección.
  • Alineación de pauta.
  • Score ponderado objetivo.
  • Decisión de pauta.
  • Justificación y recomendación ajustada.
  • Origen de pauta y versión asociada.
UserActivePlan / UserActivePlanVersion

Persistencia real de la pauta activa y de sus versiones.

Permite trazabilidad histórica y vinculación del snapshot heurístico con una versión concreta de pauta.

EnergyDailyBalance

Persistencia diaria del balance energético y nutricional calculado por el sistema.

  • Ingesta: energía total ingerida y macronutrientes.
  • Gasto: energía basal ajustada, energía activa ajustada y gasto total.
  • Balance: diferencia entre ingesta y gasto total.
  • Proteína/kg: indicador funcional de suficiencia proteica.
  • Alcohol: integración energética y moduladora del día.
  • EstadoEnergetico: clasificación oficial centralizada del balance diario.
  • ReglaAplicada: versión del modelo de gasto energético utilizado.
HealthScoreMasterParameters y modelo declarativo

Catálogo maestro técnico y estructura declarativa para la configuración progresiva del motor de cálculo.

Comprende parámetros scoreables, bloques, inputs, modelos y futuras reglas o modificadores.

Persistencia interna del import XML

El sistema conserva internamente una copia funcional del export.xml del usuario como fuente estable de lectura.

  • Origen: ZIP externo subido por bloques y recombinado en servidor.
  • Destino: ruta interna persistente por usuario.
  • Función: lectura completa, generación de reducidos semanales y reutilización funcional posterior.
MetabolicAdaptationSnapshot

Snapshot de adaptación metabólica, calculado bajo demanda o por el proceso nocturno.

  • FechaDesde / FechaHasta: ventana temporal analizada (14–90 días).
  • TdeePredichoMediaKcal: media de EnergyDailyBalance.EnergiaGastoTotalKcal.
  • TdeeRealEstimadoKcal: TDEE real derivado por balance de masa.
  • AdaptacionKcal / AdaptacionPorcentaje: gap TDEE teórico − real.
  • Nivel: SinAdaptacion / Leve / Moderada / Severa.
  • DeltaPesoKg / R2Peso: cambio de peso y bondad de ajuste OLS.
  • CoberturaProteicaMedia: ratio de suficiencia proteica en el período.
HealthScoreDeclarativeHistory

Persistencia técnica específica del motor declarativo.

  • Scores persistidos: SNC, Metabólico y Bioenergético.
  • Contexto temporal: fecha de cálculo, analítica usada y ventana biométrica aplicada.
  • Modelo persistido: serialización JSON completa del estado declarativo efectivo.
  • Integridad: hash SHA-256 del modelo declarativo utilizado.
  • Trazabilidad: versión del modelo, origen de configuración y existencia de parámetros de usuario.
Regla operativa vigente: el sistema distingue entre score técnico persistido, reconstrucción histórica recalculada, evaluación heurística vigente y balance energético diario persistido. Cada uno de estos planos cumple una función distinta dentro de la trazabilidad funcional del sistema.

7. Instrucciones y logística funcional

Pautas de suministro de datos, uso operativo y explotación funcional del sistema.

01Entrada: Perfil Personal

Datos básicos de marco: biometría basal, contexto personal, hábitos y objetivo funcional.

02Seguimiento diario

Registro manual o importación estructurada de biometría diaria y datos históricos de salud.

03Lectura semanal reducida

Generación de reducidos semanales, agregación diaria y transformación a BiodataDiaria.

04Verificación: Analítica Clínica

Registro analítico para auditoría de recomendaciones, trazabilidad temporal y confirmación bioquímica. Los valores pueden introducirse manualmente o mediante importación automática desde PDF: el informe de laboratorio se envía directamente a la IA, que extrae y convierte hasta 60 biomarcadores; el usuario revisa el resultado con semáforo de rangos y confirma antes de persistir.

8.22. Calibración del modelo energético — factores neutros y método de calibración

(El Índice de Eficiencia Locomotora — IEL, EnergiaActivaKcal del wearable frente a un modelo mecánico de la marcha — es puramente diagnóstico en Adaptación Metabólica: una deriva decreciente sostenida es una tercera línea de evidencia de adaptación metabólica activa, junto a la brecha TDEE_teórico/TDEE_real y la regresión OLS del peso. Nunca se usa para fijar ActiveEnergyCorrectionFactor, por el motivo que se explica a continuación.)

Factor de actividad neutro

ActiveEnergyCorrectionFactor vale 1,00 y solo se modifica con evidencia directa (comparativa contra calorimetría indirecta real). La aplicación no deriva este factor de modelos indirectos —factores bibliográficos de sobreestimación, estimación del gasto por frecuencia cardíaca o modelos mecánicos de la marcha—, porque todos comparan la energía activa contra una referencia idealizada y no contra el único dato objetivo disponible, el peso real. Además, el sesgo del sensor no tiene una dirección universal: depende del dispositivo, de la versión de firmware y de cada persona.

Circularidad de calibrar los tres factores a la vez

El factor de ingesta (OllamaIntakeCorrectionFactor) se deriva por regresión MCO de IngestaReal = GastoTotal + Δpeso × 7700, donde GastoTotal ya incluye Basal y Activa con sus factores vigentes aplicados. Por tanto, subir el factor Basal o el de Activa sube mecánicamente el factor de ingesta recomendado, sin que eso aporte ninguna evidencia nueva sobre la ingesta en sí.

Con tres factores libres y una sola ecuación de balance, el sistema está indeterminado: cualquier combinación que cuadre el balance es matemáticamente válida, pero solo una es correcta. Para romper la indeterminación sin introducir supuestos no verificados, se fijan dos de los tres factores en un valor de referencia y se calibra solo el tercero contra un dato independiente: el peso real medido.

Plausibilidad del basal del wearable

Antes de tomar Basal=1,00 como referencia neutra, se comprueba que el basal que entrega el wearable es plausible en términos absolutos para ese usuario, comparando su media con dos fórmulas independientes: Mifflin-St Jeor (edad, altura, peso) y Katch-McArdle (370 + 21,6 × masa magra, con la grasa corporal medida por bioimpedancia en BiodataDiaria.GrasaCorporalImpedanciaPct, no un porcentaje poblacional supuesto). Si ambas fórmulas coinciden entre sí y quedan claramente por encima o por debajo del basal del wearable, cualquier factor de ingesta calibrado contra ese basal absorbería el error; en ese caso se ajusta el factor Basal. La desviación, si existe, es individual y no se traslada a otros usuarios: el valor por defecto de BasalEnergyImportedCorrectionFactor es siempre 1,00.

Método de calibración (prueba y error contra el peso real)

Procedimiento que sigue el usuario en el panel de Calibración, en este orden:

  1. Fijar los tres factores en 1:1:1 (neutro) y guardar.
  2. En el Informe, filtrar Desde el primer registro Hasta hoy y pulsar Recalcular rango (confirmando el rango exacto en el modal) para propagar el neutro a todos los registros pasados (ver 8.35).
  3. Calibrar solo la ingesta con la calculadora MCO (≥30 días) y aplicar el factor recomendado.
  4. Repetir Recalcular rango sobre el periodo que corresponda (todo el histórico, o solo un tramo si distintas fechas deben conservar factores distintos) para propagar el nuevo factor de ingesta.
  5. Ejecutar la Validación: peso calculado vs. peso real (ver más abajo) sobre un periodo que incluya días no usados en la calibración, y comprobar si el Δ peso implícito coincide razonablemente con el Δ peso real.
  6. Si no coincide, antes de volver a tocar la ingesta (que solo desplazaría el error), comprobar la plausibilidad del basal como se describe arriba; solo si hay evidencia de desviación, ajustar Basal y repetir desde el paso 2.
Herramienta en la app: EnergyWeightValidationService

La tarjeta "Validación: peso calculado vs. peso real" del panel de Calibración suma EnergyDailyBalances.BalanceEnergeticoKcal exactamente en la misma ventana que tiene peso registrado en BiodataDiaria (primer y último día con peso > 0 dentro del periodo elegido, no el periodo completo solicitado si hay huecos en los extremos), la divide entre 7700 kcal/kg para obtener el Δ peso implícito, y lo compara contra el Δ peso real (último peso − primer peso). Muestra la diferencia en kg y en % relativo, y avisa explícitamente si el periodo solapa con el usado para calibrar la ingesta: en ese caso la coincidencia es en parte esperable (tautológica), no una predicción genuina, y la validación debe hacerse sobre un periodo distinto o más amplio que el de calibración.

8.23. Sistema híbrido de detección de alérgenos en estimación nutricional
Finalidad

El perfil personal recoge alergias/intolerancias alimentarias declaradas (PerfilPersonal.AlergiasAlimentarias, texto libre). El módulo de ingesta avisa cuando un alimento registrado puede contener alguno de esos alérgenos.

Arquitectura: dos capas desacopladas del cálculo

El prompt de cálculo de macros (NUTRITION_INTAKE_ANALYSIS) es un DSL minimalista tipo configuración (@rules / @task / @output) cuyo resultado numérico es sensible a cualquier texto añadido, por lo que se reserva exclusivamente al cálculo de macros. El aviso de alérgenos se resuelve en un paso posterior e independiente, una vez el JSON de macros ya está calculado y validado, de modo que un fallo en la detección de alérgenos nunca puede alterar el cálculo nutricional:

  • Capa determinista (AllergenKeywordMatcher): diccionario de palabras clave que cubre los 14 alérgenos de declaración obligatoria del Reglamento (UE) 1169/2011, con normalización de texto (sin tildes, minúsculas) y detección explícita de frases de negación ("sin gluten", "libre de lactosa", "deslactosado"...) para evitar falsos positivos por coincidencia de subcadena (p. ej. "sin gluten" contiene literalmente "gluten"). Es la única garantía de determinismo del sistema: mismo alimento, mismo resultado, siempre.
  • Capa complementaria por IA: llamada independiente que compara nombres de alimentos contra las alergias declaradas, con conocimiento semántico más amplio que el diccionario, pero sin garantía de determinismo: el mismo alimento y la misma alergia pueden producir respuestas distintas entre llamadas sucesivas (incluso con temperature=0), y la regla de negación del propio prompt no siempre se respeta. Por ello, el resultado de esta capa se filtra por el mismo diccionario de negación antes de fusionarse — el diccionario actúa como veto final sobre falsos positivos de la IA, no solo como fuente independiente.

Ambas capas se aplican de forma idéntica en los dos motores de cálculo de ingesta (IA pura y modo híbrido OFf+IA), mediante la clase compartida AllergenKeywordMatcher, invocada tanto desde NutritionAiService como desde NutritionOpenFoodFactsService — evitando que un alimento resuelto directamente por la etiqueta real de OpenFoodFacts quede fuera de la verificación.

Criterio de producto

El sistema solo emite aviso cuando existe certeza razonable a partir del propio nombre del alimento (p. ej. pan, pasta o cerveza para gluten). Categorías genuinamente ambiguas — p. ej. jamón cocido, donde la presencia de gluten depende del aditivo del fabricante concreto y no del tipo de alimento — se excluyen deliberadamente del diccionario, para no generar una falsa sensación de certeza ni de responsabilidad del sistema sobre una valoración que corresponde al etiquetado del producto real.

8.24. Oxigenación nocturna: SDS/SAP como cribado de apnea del sueño
Finalidad

La SpO₂ nocturna que registra el Apple Watch (leída del export.xml de HealthKit), combinada con datos ya presentes en el perfil y en Biodata (IMC, edad, tensión arterial, alcohol), permite un cribado orientativo de riesgo de apnea del sueño sin necesidad de ningún dispositivo o dato adicional.

Arquitectura de dos índices desacoplados

El panel Oxigenación nocturna (SleepOxygenationService) calcula deliberadamente dos índices distintos, para no confundir lo que el dispositivo observa con la probabilidad clínica que se infiere a partir de ello:

  • SDS (Índice de Desaturación del Sueño, 0-100): puramente objetivo. Mismo patrón de baseline personal que RecoveryService (media/desviación típica sobre una ventana rolling, z-score acotado a ±3σ), pero con la dirección invertida respecto a Recuperación: aquí un valor más alto es peor (más desaturación que el patrón habitual del propio usuario), no mejor. Cada noche se delimita con la ventana de sueño real de ese día (inicio y fin del sueño con 15 minutos de margen; si falta, una franja de respaldo de 22:00 a 07:00 hora de Madrid, de confianza baja) para separar SpO₂ de sueño de SpO₂ diurna, no descarta ninguna lectura, exige un mínimo de 5 lecturas esa noche para fiarse de su media (el Apple Watch muestrea SpO₂ de forma episódica, no continua) y un mínimo de 7 noches previas para fiarse del baseline.
  • SAP (Probabilidad de Apnea del Sueño, 0-100): mitad SDS, mitad puntuación de un cribado STOP-BANG simplificado de 7 factores (IMC>30, edad>65, ronquidos, apnea observada por terceros, hipertensión, somnolencia diurna, alcohol registrado ese día), sin pesos diferenciados por factor — igual que el cuestionario STOP-BANG real, que tampoco los tiene. Los factores sin dato disponible ese día se excluyen del cálculo y se renormaliza sobre los restantes, en vez de contarlos como ausentes/negativos; con menos de 4 de los 7 disponibles, el resultado se marca explícitamente como cribado incompleto.
Fuente de datos: muestras crudas de SpO₂

El cálculo lee las muestras de SpO₂ con su hora exacta guardadas en SaturacionOxigenoRaw, que se alimenta tanto desde la exportación automática (Health Auto Export) como desde la importación del export de Apple Health, de modo que el panel no depende de cuál de las dos vías aporte el dato. Solo se usan las lecturas del reloj; las tomas manuales se excluyen. Las medias nocturnas, los scores y el baseline se recalculan en cada visita a partir de esas muestras y de las ventanas de sueño de BiodataDiaria, sin ninguna caché intermedia.

Para contrastar las lecturas bajas con la fisiología, el sistema guarda también la frecuencia cardiaca y la frecuencia respiratoria con su hora exacta durante la ventana de sueño de cada noche (con 30 minutos de margen) en FrecuenciaVitalRaw, alimentada por las mismas dos vías (Health Auto Export e importación de Apple Health) y por un proceso puntual de relleno del histórico desde el export.xml ya subido. Cada lectura de SpO₂ ≤ 90 % se compara con la FC y la frecuencia respiratoria de ±15 minutos frente a la mediana de las noches de los 30 días previos en el mismo tramo de una hora desde el inicio del sueño (mínimo 7 noches), con escala robusta (1,4826 × desviación absoluta mediana, con mínimos de 2 ppm y 0,7 rpm). Esta comprobación es solo informativa: ninguna lectura se excluye de la media. Si ninguna de las dos supera 2 desviaciones, o no hay FC ni frecuencia respiratoria con que contrastarla, la nota de la noche lo indica y la noche se marca de confianza baja. La FC del reloj se mide con el mismo sensor óptico que la SpO₂, por lo que no constituye un testigo independiente. En las noches poco fiables el SDS pesa un 20 % del SAP en lugar de un 50 % (sin factores de cribado disponibles, el peso no se reduce).

Criterio de producto

SAP se presenta siempre como cribado orientativo, nunca como diagnóstico: la interfaz distingue explícitamente "esto observa tu dispositivo" (SDS) de "esto es una probabilidad de cribado, consulta con un profesional" (SAP), con ayuda visible junto a ambas siglas — no solo en un texto explicativo aparte — dado que la base de usuarios del sistema es predominantemente hispanohablante y las siglas se definen en inglés en la literatura clínica de referencia (STOP-BANG).

8.25. Generador de Informes: exploración dinámica sin persistir diseño ni resultado
Finalidad

El Generador de Informes permite investigar una hipótesis puntual (p. ej. "¿el consumo de alcohol de estos días explica esta subida de peso?") cruzando BiodataDiaria y EnergyDailyBalance dentro de la propia aplicación: el usuario elige qué campos y qué ventana temporal comparar, sin necesidad de conocimientos de SQL ni de un desarrollo a medida para cada pregunta.

Catálogo cerrado por tema clínico, no por tabla técnica

El selector de campos agrupa ~40 variables en 6 temas pensados para el usuario final (Peso y composición corporal, Actividad física, Cardio y variabilidad, Sueño, Nutrición e ingesta, Energía y balance) en vez de exponer los nombres reales de las tablas de origen — BiodataDiaria y EnergyDailyBalance se unen por UsuarioId+Fecha (InformeDinamicoService) de forma transparente para el usuario. Los cuatro macronutrientes muestran gramos junto con su aportación calórica entre paréntesis (factor Atwater: 4/4/9/7 kcal por gramo de hidratos/proteína/grasa/alcohol), para no obligar a hacer esa cuenta aparte.

Sin persistencia — ni diseño ni resultado

Ni la selección de campos ni el informe resultante se guardan en base de datos: cada petición recalcula desde cero a partir de las tablas fuente. La única "persistencia" es la propia URL, ya que la ventana temporal, los campos elegidos y el criterio de orden viajan como querystring de una petición GET — de modo que un informe concreto es reproducible o compartible con solo reenviar el enlace, sin necesidad de una tabla de "informes guardados" ni de exponer los datos de un paciente en un almacén adicional. El orden por cualquier campo (no solo por fecha) se resuelve por reflexión sobre un único catálogo (InformeDinamicoCampos), evitando un switch manual por cada una de las ~40 variables.

Doble eje del gráfico por magnitud, no por tema

Al permitir combinar libremente campos de escalas muy distintas (pasos en miles, IMC en decenas, sodio en miligramos), un único eje Y haría que el campo de mayor magnitud aplanase visualmente a los demás. El gráfico calcula el valor máximo absoluto de cada serie ya seleccionada sobre los propios datos del usuario y la asigna al eje izquierdo o derecho según supere o no un umbral fijo — una heurística basada en los datos reales, no en una clasificación temática rígida por campo.

Criterio de producto

La cabecera del informe (paciente, edad, sexo, altura, autor y momento de generación) está pensada para que el documento tenga sentido por sí solo si se imprime o exporta a PDF, sin depender del contexto de la aplicación. Para trasladarlo a un asistente de IA, un botón copia el informe en formato tabla al portapapeles; la interpretación se apoya en el asistente conversacional de la aplicación, sin una tubería de IA propia.

8.26. Correlación Termogénesis-Recuperación: convergencia entre dos baselines personales
Motivación

El indicador de Adaptación metabólica (TDEE_teórico − TDEE_real) arrastra un sesgo estructural: el "teórico" depende de una fórmula (Mifflin-St Jeor corregido con factores de calibración) que nunca acierta el basal real de un individuo concreto al milímetro — cada persona difiere de la predicción poblacional por su propio genotipo/fenotipo. Ese sesgo es aproximadamente constante en el tiempo para un mismo usuario (el metabolismo basal es fisiológicamente estable, cambia despacio — el propio estudio de seguimiento de "The Biggest Loser" muestra una supresión gradual y sostenida, no picos), así que el valor absoluto de Adaptación en un momento dado no es del todo fiable, pero su tendencia sí lo es: la resta entre dos snapshots cancela el sesgo constante.

Dos señales independientes, mismo principio de baseline personal

CorrelacionTermogenesisService cruza el histórico ya persistido de MetabolicAdaptationSnapshot (ventanas de 28 días rodantes) con RecoveryDailyScore (diario, HRV/FC reposo/temperatura/sueño), promediando Recovery sobre la misma ventana temporal de cada snapshot de Adaptación — no son directamente comparables fila a fila por su distinta granularidad. Ambos sistemas comparten ya el mismo criterio de cálculo (contra el baseline histórico del propio paciente, no contra baremos poblacionales), lo que los hace comparables por construcción sin necesidad de normalizarlos de ninguna forma especial.

Convergencia como señal, no un score nuevo

No se calcula ningún índice compuesto ni coeficiente de correlación estadística: se marca ConvergenciaNegativa cuando, respecto a la ventana anterior, la Adaptación empeora (% sube) y el Recovery medio empeora (score baja) a la vez. La justificación fisiológica es directa, no solo estadística: la restricción calórica crónica es un estresor real, asociado en la literatura a caída de HRV y subida de FC reposo (contextos de RED-S/infra-alimentación sostenida), y la propia reducción de actividad simpática es uno de los mecanismos propuestos de la termogénesis adaptativa — si ambas señales empeoran a la vez, es la misma fisiología vista desde dos ángulos independientes, no una coincidencia de dos métricas sin relación mecanística entre sí.

Criterio de producto

No introduce ningún cálculo nuevo de Adaptación ni de Recovery — solo los alinea temporalmente y señala cuándo coinciden. Presentado explícitamente como señal de alerta para valorar con el profesional, nunca como diagnóstico, igual que el resto de cribados del sistema.

8.27. Conservación de la Flexibilidad Metabólica: modelo jerárquico de referencia
Motivación y distinción conceptual

La Adaptación Metabólica (apartado 8.20) mide si el organismo está ahorrando energía, comparando TDEE teórico y TDEE real. La Flexibilidad Metabólica es un concepto distinto: la capacidad de alternar con facilidad entre sustratos energéticos (grasa/glucosa) según la disponibilidad de energía. Fenotipo no mide el cambio de sustrato directamente —requeriría calorimetría indirecta con cociente respiratorio, fuera del alcance de un wearable de consumo—. En su lugar, FlexibilidadMetabolicaService presenta un conjunto de señales conductuales con relación documentada con la flexibilidad metabólica, mostradas por separado, sin combinarlas en un único número: ponderarlas en un score compuesto exigiría pesos que hoy no están validados con datos propios del usuario.

Modelo jerárquico de referencia: baseline propio antes que umbral poblacional

Fenotipo aplica de forma consistente un principio de diseño: siempre que existe suficiente información longitudinal, la comparación se realiza contra el comportamiento histórico del propio paciente, no contra referencias poblacionales. Las recomendaciones poblacionales (p. ej. OMS/ACSM) solo se contemplan para inicializar el modelo o cuando la evidencia individual es todavía insuficiente. En este módulo, el entrenamiento de fuerza y de cardio (BiodataDiaria.MinutosEjercicioGym / MinutosEjercicioExterior) se comparan contra la propia media del paciente en los 90 días anteriores a la ventana analizada, no contra una cifra externa. Si el paciente no acumula al menos 14 días con datos en ese periodo previo, el criterio correspondiente se omite del checklist en vez de sustituirse por un valor poblacional por defecto — el sistema prefiere no opinar a comparar contra una referencia que no es la del propio individuo.

La variabilidad de peso (dispersión de los pesajes alrededor de la tendencia, en % del peso corporal) sigue el mismo principio que fuerza y cardio: MetabolicAdaptationService se calcula también sobre los 90 días anteriores a la ventana (mismo requisito de 14 días mínimos con datos) para obtener la variabilidad de peso residual habitual del paciente, y la etiqueta compara la ventana actual contra esa línea base propia (por debajo del 70% = Baja, 70-130% = Normal, por encima del 130% = Elevada) en vez de contra un umbral poblacional.

El déficit prolongado se juzga por adaptación medida, no por recuento de días

El tiempo en déficit no predice por sí solo la adaptación: dos pacientes con la misma racha de días en déficit pueden tener respuestas fisiológicas muy distintas. Por eso el criterio usa directamente el Nivel/AdaptacionResidualPorcentaje ya calculado por MetabolicAdaptationService sobre la misma ventana, es decir, el dato medido del propio paciente, y no el número de días transcurridos en déficit.

Checklist sin pesos y explicación por reglas

Los criterios que sí se combinan lo hacen por recuento simple (enfoque tipo test Apgar): cada criterio cumplido suma un punto, sin ponderación relativa entre ellos, y el total es variable (normalmente entre 3 y 6, según haya o no línea base de entrenamiento disponible y haya o no un evento de readaptación evaluable). La interpretación en lenguaje natural que acompaña al checklist se genera mediante plantillas condicionales deterministas a partir de los mismos datos ya calculados — no interviene ningún modelo de lenguaje, lo que garantiza reproducibilidad exacta del texto mostrado para una misma combinación de datos.

Recuperación tras readaptación: verificar el efecto, no solo el evento

Detectar un Refeed o Diet break (apartado 8.28) no implica que haya funcionado. El criterio "Recuperación tras tu última readaptación" compara la adaptación medida (MetabolicAdaptationSnapshot.AdaptacionResidualKcal / TdeePredichoMediaKcal) en el snapshot más cercano ANTES del evento frente al más cercano DESPUÉS, exigiendo al menos 7 días de margen tras el fin del evento para que exista un snapshot que ya refleje el efecto. Solo se marca como éxito si la adaptación medida bajó realmente; no se asume que "hacer una readaptación ya basta". Se omite del checklist si no hay ningún evento con margen suficiente en el histórico disponible, en vez de forzar una comparación sin margen.

Recomendaciones no prescriptivas con prioridad cualitativa

El panel "Qué podrías valorar" genera una tarjeta por cada criterio del checklist no cumplido, con una prioridad cualitativa (Alto/Medio), no un score numérico. La prioridad se apoya en la evidencia ya citada en el apartado 8.20 (estudio MATADOR para déficit/readaptación, mTOR y preservación muscular para proteína, ambos "Alto") frente a los criterios autorreferenciales de fuerza/cardio ("Medio"), que son un patrón conductual propio sin el mismo respaldo bibliográfico directo. Se presenta explícitamente como contexto para valorar con el profesional, no como prescripción.

Criterio de producto

Los nombres de las puntuaciones son deliberadamente no diagnósticos ("Contexto favorable/poco favorable", no "Flexibilidad alta/baja"): el sistema no mide la flexibilidad metabólica de forma directa, mide la probabilidad de conservarla dados los hábitos observados. Se presenta como contexto para valorar con el profesional, igual que el resto de cribados del sistema.

8.28. Detección retrospectiva de eventos de readaptación (Refeed y Diet break)

En pantalla, estos eventos se presentan castellanizados como "Readaptación ligera (Refeed)" y "Readaptación prolongada (Diet break)" — término distinto y deliberadamente separado de "Reactivación metabólica corta/prolongada" (apartado 8.20, la recomendación educativa del modal): "Readaptación" nombra el evento detectado automáticamente; "Reactivación metabólica" sigue siendo la recomendación de cuándo conviene aplicarlo.

Motivación

Un refeed (reactivación corta) y un diet break (pausa estructural) son estrategias documentadas para mitigar la adaptación metabólica (MATADOR, Byrne et al. 2017), pero Fenotipo no pedía al paciente que declarase cuándo las aplicaba. RefeedDietBreakDetectionService los infiere retrospectivamente a partir del balance energético diario ya registrado (EnergyDailyBalance.BalanceEnergeticoKcal), sin necesidad de que el usuario marque nada expresamente.

Clasificación por bandas y rachas

Cada día se clasifica en Déficit / Mantenimiento / Superávit agresivo según si el balance diario se aleja más o menos de ±10% de su TDEE teórico. Un Refeed es una racha de 1-2 días en banda de mantenimiento, precedida de al menos 5 días consecutivos en déficit; un Diet break es una racha de 7-14 días, precedida de al menos 14 días consecutivos en déficit. Rachas de 3-6 días quedan deliberadamente sin clasificar: la literatura no sostiene un límite tan fino como para forzar una etiqueta. Solo se clasifica una racha ya concluida (con datos posteriores que confirman su fin) — una racha todavía en curso al final del histórico disponible podría seguir creciendo y cambiar de categoría, así que se deja sin clasificar hasta que termine.

Confirmación informativa, no excluyente

Tras cada evento se registra si el paciente volvió a déficit en los 1-2 días siguientes (Confirmado / SinConfirmar / NoVolvioADeficit). Esta confirmación es puramente informativa: no reclasifica el tipo de evento ya detectado, porque una reactivación deliberada tiene valor aunque no derive en una vuelta inmediata a déficit (podría marcar, por ejemplo, un cambio de fase consciente).

Advertencia explícita: readaptación impulsada por alcohol, no por hidratos

Un refeed real busca reponer glucógeno y leptina principalmente vía hidratos; el alcohol no cumple esa función y puede además perjudicar sueño/HRV y frenar la oxidación de grasa. Como el detector solo mira el balance calórico total, un día podría alcanzar la banda de mantenimiento por alcohol sin que haya habido un aumento real de hidratos. Para cada evento se compara el salto de balance real (déficit previo → ventana del evento) frente a cuánto de ese salto explican el alcohol y los hidratos por separado; se marca ImpulsadoPorAlcohol solo si el alcohol explica al menos la mitad del salto y los hidratos no explican también al menos la mitad — así un refeed real con hidratos (aunque incluya alcohol) no se marca, solo el que depende del alcohol para llegar a mantenimiento. Deliberadamente no se excluye el día ni se oculta el evento: se advierte de forma explícita en pantalla, para no decidir en silencio de la omisión algo que el profesional debe poder valorar.

Naturaleza retrospectiva y ejecución

El sistema no distingue si el patrón fue deliberado o casual — el efecto fisiológico medido sobre el balance energético es el mismo en ambos casos. La detección se ejecuta automáticamente cada noche para todos los usuarios dentro de MetabolicSnapshotWorker, reescaneando el histórico completo de balance energético del paciente (operación idempotente vía upsert sobre RefeedDietBreakEvents, con clave única UsuarioId+FechaInicio+Tipo); también puede forzarse manualmente desde la interfaz sin esperar al ciclo nocturno.

Criterio de producto

Los eventos detectados se muestran como contexto en Adaptación Metabólica y en Conservación de la Flexibilidad Metabólica (apartado 8.27). En Flexibilidad Metabólica, el evento concluido más reciente con margen suficiente sí alimenta un criterio puntuado del checklist ("Recuperación tras tu última readaptación"); en Adaptación Metabólica sigue siendo puramente informativo.

8.29. Inspector de asuntos internos IA — observabilidad de llamadas a Ollama/Claude
Finalidad

IAiInspectorLogService registra cada llamada real a IA con datos objetivos, de modo que la lentitud o los fallos intermitentes de los proveedores (Ollama Cloud, Claude) se diagnostican con evidencia y no por deducción indirecta.

Qué se registra

Cada intento de llamada (incluidos los reintentos) genera una línea con: proveedor (Ollama/Claude), origen (tipo de análisis: NUTRITION_INTAKE_ANALYSIS, HISTORIA_PACIENTE_IA, DAILY_METABOLIC_ANALYSIS, etc.), email del usuario que originó la llamada, modelo concreto usado, número de intento, resultado, tipo de error si lo hay, código HTTP, motivo de parada del modelo (DoneReason de Ollama o StopReason de Claude — mismo campo, dos providers), tokens de prompt y de respuesta (PromptEvalCount/EvalCount de Ollama, Usage.InputTokens/Usage.OutputTokens de Claude), duración y detalle textual libre.

Persistencia sin depender de acceso a directorios de sistema

El hosting de despliegue no da acceso a directorios principales del servidor. El log se persiste en AppData/IAInspectorLogs dentro del propio árbol de la aplicación (raíz de contenido, no wwwroot) — como app.UseStaticFiles() se invoca sin parámetros, solo sirve el wwwroot por defecto, así que esta carpeta queda automáticamente inaccesible por HTTP sin configuración adicional. Un fichero por día (ia-inspector-yyyy-MM-dd.log), con la fecha y hora calculadas siempre en huso horario de Madrid (no UTC crudo), para que el corte de "día" coincida con el que percibe el usuario. Si la escritura falla por cualquier motivo, cae a un fichero de respaldo (nunca se propaga el fallo a la llamada real de IA).

Enrutado Ollama/Claude en tareas en segundo plano

La fábrica de IAiCompletionService decide el proveedor según AiProviderSettings.ResolveProvider(isAdmin). Los análisis que se procesan en segundo plano mediante IServiceScopeFactory.CreateScope() (Intake, Historia Paciente, Informes, BioData Evolution, HealthDash y los cuatro análisis de EnergyManagerController) se ejecutan en un scope sin petición HTTP activa, por lo que no pueden leer isAdmin de HttpContext. Para respetar el proveedor personal elegido por el administrador, un puente scoped (IAiProviderContext) transporta ese dato: el controlador captura isAdmin con el HttpContext real antes de encolar y lo fija en el scope de la tarea justo antes de resolver el servicio de IA; el mismo puente transporta también el origen del análisis y el email del usuario para el log del inspector.

Criterio de éxito (Ollama)

El streaming de /api/generate puede cortarse a mitad sin llegar el chunk final GenerateDoneResponseStream. Si no llega ese chunk final, la llamada se trata como fallo (ErrorTipo = "SinDone") y se reintenta con el mismo backoff que un timeout, aunque haya texto acumulado, para no devolver una respuesta potencialmente truncada como si fuera correcta.

Panel de administración

Accesible solo para el rol Admin (IaInspectorLogController): listado de ficheros por día con el de hoy resaltado, vista tabular filtrable de cada línea (proveedor, origen, usuario, modelo, resultado, tokens, duración), y borrado de ficheros antiguos con confirmación mediante el modal genérico del sistema (_ModalConfirmarAccion, no el confirm() nativo del navegador).

8.30. Módulo de entrenamiento de fuerza — catálogo, motor de propuesta adaptativo y ejecución guiada
Motivación

Completa la tercera pata de intervención activa junto a dieta y sueño: preservación y ganancia de masa muscular, especialmente relevante bajo restricción calórica sostenida y en el paciente mayor (referencias ACSM/NSCA de volumen, frecuencia, descanso y estimación de RM). El módulo separa deliberadamente lo propuesto de lo ejecutado, para poder medir adherencia real frente a prescripción, no solo si "hubo actividad".

Modelo de datos

Cuatro entidades: CatalogoEjercicio (catálogo de sistema, más de cien ejercicios de calistenia progresiva, banda elástica, mancuernas y barra de dominadas, además de calentamiento, movilidad y estiramientos, con cadenas de progresión encadenadas por EjercicioBaseId+NivelDificultad), EntrenamientoSesion (cabecera semanal por usuario/fecha), EntrenamientoSesionEjercicio (lo propuesto para esa sesión) y EntrenamientoEjecucion (lo realmente ejecutado — puede diferir del propuesto por sustitución manual). El índice (UsuarioId, Fecha) de EntrenamientoSesion es deliberadamente no único: una sesión recuperada de una semana anterior puede coincidir con la ya prevista para el día en curso.

Motor de propuesta semanal: split 2-de-3 y rotación anti-estancamiento

Genera 3 sesiones/semana (lunes/miércoles/viernes) con un split parcial 2-de-3: cada sesión cubre 2 de las 3 grandes zonas (empuje, tracción, pierna/cadera) y deja fuera la tercera, rotando cuál se omite cada día (PatronesPorDia en PropuestaEntrenamientoService). Así cada zona llega justo a las 2x/semana que exige la guía ACSM 2026 para grupos musculares grandes (más aún en mayores/sarcopenia según NSCA), con 48h o más entre sus dos apariciones, y al no compartir patrones las tres sesiones queda margen para sesiones extra fuera del calendario fijo. El core se reparte en las tres sesiones. Para cada patrón, ElegirEjercicioParaPatron mira la última ejecución del usuario en ese patrón dentro de una ventana de 14 días. Si las dos últimas ejecuciones del mismo ejercicio fueron limpias (Completo, sin Molestia), sube al siguiente nivel de la cadena de progresión. Si la última no fue limpia, en vez de repetir indefinidamente el mismo ejercicio, rota a otro del mismo patrón y nivel igual o inferior — priorizando el que lleve más tiempo sin hacerse — para evitar que una molestia puntual o una sesión incompleta bloquee al usuario en el mismo movimiento semana tras semana.

Sesión extra en día libre: clonado con guardia de 48h

EntrenamientoController.RepetirSesion deja crear una sesión extra, idéntica en ejercicios a una ya existente, en cualquier día sin sesión propia dentro de la semana actual o la anterior — para subir la frecuencia por encima de los 3 días fijos sin generar una propuesta nueva del motor. No mueve ni reescribe la sesión original (a diferencia de RetomarHoy, pensado para sesiones aún sin completar): la clona en una EntrenamientoSesion nueva, preservando el historial real de la original. Antes de crear, comprueba si el día elegido cae a menos de 48h (día anterior o posterior) de otra sesión del usuario que comparta algún PatronMovimiento con la que se va a copiar — si es así, bloquea la operación. Esta comprobación es independiente del split de PatronesPorDia: clonar reproduce el contenido exacto de la sesión origen, así que ni el reparto de zonas del calendario fijo garantiza por sí solo el descanso de una sesión repetida sobre él.

Sesión vencida sin tocar: Fallida vs. CompletadaExterna

PropuestaEntrenamientoService.MarcarPendientesVencidasComoFallidasAsync sella cada día las sesiones Pendiente/Rotada cuya fecha ya pasó. Antes de sellar, consulta BiodataDiaria.MinutosEjercicioGym para las fechas afectadas: si hay gimnasio real ese día registrado por el reloj fuera del flujo guiado (p. ej. una sesión de la app Entreno del Apple Watch, no de Entrenar), la sesión queda CompletadaExterna ("Entrenaste (reloj)" en el badge) en vez de Fallida. No se trata como Completada a secas porque no tiene EntrenamientoSesionEjercicio ejecutados detrás (sin detalle de qué se hizo realmente): cuenta como adherencia real en InspectorService y se reporta aparte (SesionesCompletadasExternamente) en ProgresoFuerzaService y en el prompt de ValoracionFuerzaIaService, sin sumar a SesionesCompletadas — así la IA no habla de progresión de fuerza (series/reps/peso) sobre una sesión sin ese detalle. Al igual que Fallida/Rotada, sigue siendo recuperable con RetomarHoy y aparece entre las sesiones abiertas de semanas anteriores en Plan semanal, por si el usuario quiere completar de todas formas el patrón concreto que el plan había propuesto ese día.

El chequeo en el instante exacto del vencimiento no basta por sí solo: BiodataDiaria puede recibir el import del wearable horas o incluso días después de que la sesión ya se haya sellado como Fallida. Por eso MarcarPendientesVencidasComoFallidasAsync también relee, cada vez que se llama, todas las sesiones ya Fallida de la semana ISO en curso (lunes a hoy) y las sube a CompletadaExterna si para entonces sí hay MinutosEjercicioGym. Como Progreso lee EntrenamientoSesion directamente sin pasar antes por AsegurarSemanaActualAsync (a diferencia de Plan semanal/Entrenar), el controlador la llama explícitamente al entrar en esa vista para no depender de que el usuario haya visitado antes el plan semanal.

Personalización de series, repeticiones y descanso

Los valores propuestos (SeriesPropuestas/RepsPropuestas/ DescansoPropuestoSegundos) no copian siempre el default del catálogo — usan la última ejecución real del usuario para ese ejercicio concreto dentro de la misma ventana de 14 días. El default del catálogo (editable solo por Admin) actúa exclusivamente de semilla la primera vez que el usuario hace ese ejercicio; a partir de ahí, sus propios ajustes en el momento de entrenar persisten y alimentan la siguiente propuesta automáticamente, sin tabla de preferencias adicional.

Exclusión personal de ejercicios

UsuarioEjercicioExcluido (UsuarioId + CatalogoEjercicioId) registra qué ejercicios del catálogo no quiere ver un usuario concreto en su plan semanal — sin equipamiento, no le gusta, o cualquier otro motivo; es deliberadamente genérica, no solo para ejercicios de suelo. Se gestiona desde CatalogoEjercicios/Index (columna "Para mí", accesible también desde Perfil Personal → "Excluir ejercicios") y afecta solo al usuario que la marca, respetando el contexto de paciente activo cuando un profesional gestiona a otro usuario (ResolveUserId()). ElegirEjercicioParaPatron filtra estos candidatos en los tres puntos donde elige un ejercicio nuevo — primera vez sin histórico, progresión de nivel y rotación anti-estancamiento —, pero conserva la lista completa (excluidos incluidos) para poder localizar en el histórico un ejercicio que el usuario excluyó después de haberlo hecho. Si excluir deja algún patrón de movimiento sin ningún candidato permitido, cae de vuelta a la lista completa para ese patrón en vez de dejar un hueco sin ejercicio esa semana, y lo anota en el motivo mostrado al usuario. Si el ejercicio que tocaría continuar (progresión o repetición) está excluido, no se le vuelve a proponer bajo ningún concepto: se trata como si no hubiera histórico válido y se elige uno nuevo permitido del mismo patrón. Relacionado pero independiente: CatalogoEjercicio.RequiereSuelo es un metadato descriptivo (visible en el CRUD, no editable por el usuario) que no está cableado al motor — la exclusión real y efectiva es siempre UsuarioEjercicioExcluido.

Ejecución guiada en tiempo real

Entrenar.cshtml: cronómetro de series y descanso con avisos sonoros, pausa/reanudación, y tratamiento especial para ejercicios EsPorTiempo (plancha, suspensión activa), que miden segundos de mantenimiento en vez de repeticiones. FinalizarSesion resuelve en servidor el CatalogoEjercicioId autoritativo de cada hueco de la sesión a partir de lo guardado tras una posible sustitución en Detalle — nunca confía en ese campo del payload del cliente, ya que la vista de ejecución no ofrece ningún control para cambiarlo a mitad de sesión.

Figuras del movimiento

CatalogoEjercicioFigura (relación 1:1 con el catálogo) almacena un par de SVG (postura inicial/final) por ejercicio, animados por alternancia CSS pura de opacity cada 700ms (sin JavaScript de temporización, por compatibilidad). Escape hatch MediaUrl para foto o vídeo real en casos excepcionales. CatalogoEjercicioFiguraSeeder mapea más de noventa posturas SVG reutilizables (compartidas entre variantes de una misma cadena solo cuando el apoyo/postura real coincide — rodillas, banco elevado o silla generan postura propia) a los ejercicios del catálogo, y es idempotente por ejercicio: solo rellena huecos, nunca sobrescribe una figura ya asignada, para poder corregirla desde el CRUD sin que un reseed la revierta.

Exportación e importación JSON del catálogo

CatalogoEjercicioJsonService exporta el catálogo completo (activos e inactivos, sin Id ni figura) a un JSON con cabecera formato (catalogo-ejercicios), version y exportadoUtc, y una lista ejercicios. La importación (solo rol Admin, archivo de hasta 5 MB) acepta ese mismo formato en dos modos: SoloTextos (actualiza Explicacion y Alertas de ejercicios existentes; los demás se omiten) y Completo (crea los ausentes y actualiza todos los campos presentes). La clave de correspondencia es el nombre, sin distinguir mayúsculas, porque los Id difieren entre entornos; ejercicioBase también se indica por nombre y se resuelve al final, por lo que puede apuntar a un ejercicio nuevo del mismo archivo (se guarda en dos pasos dentro de la misma transacción para conocer su Id). Un campo ausente deja el valor actual; un campo presente con null o vacío lo borra solo en los opcionales (grupo secundario, ejercicio base y alertas), y la explicación nunca puede quedar vacía.

La validación comprueba rangos (nivel 1–10, series 1–10, repeticiones o segundos 1–600, descanso 0–600 s), que grupo, patrón y equipamiento sean identificadores alfanuméricos de hasta 50 caracteres, nombres no repetidos en el archivo y bases existentes. Un grupo, patrón o equipamiento que coincide con uno existente salvo en mayúsculas adopta la grafía existente; uno desconocido se admite con un aviso por si es una errata. Las líneas de explicación y alertas se recortan, se descartan las vacías y se unen con salto de línea. Un único método (ProcesarAsync, con el indicador aplicar) calcula la vista previa y la aplicación, de modo que lo mostrado es lo que se guarda; la vista previa transporta el JSON en un campo oculto hasta la confirmación, y con cualquier error no se escribe nada (transacción única).

En pantalla, formatearTextoEjercicio (site.js, y su equivalente en Razor en el detalle de sesión) escapa el HTML y muestra cada línea como un apartado, con la etiqueta anterior a los dos puntos en negrita; un texto de una sola línea se muestra como un párrafo normal. Las alertas con varias líneas se muestran con viñetas.

Reconciliación con datos de wearable

ReconciliacionEjercicioHealthKitService corre tras cada import de HealthKit/Health Connect (iOS y Android, manual y automático) y rellena BiodataDiaria.MinutosEjercicioGym con lo ejecutado en sesión guiada, restando el mismo importe de MinutosEjercicioExterior — pero solo si el wearable no aportó nada explícito ese día (MinutosEjercicioGym == 0), para que el dato real del reloj gane siempre. Agrupa por fecha de sesión, no por sesión individual, así que varias sesiones el mismo día (recuperación de una sesión atrasada junto a la ya prevista) se suman correctamente sin duplicar ni perder minutos.

Informes y evolución

"Progreso de fuerza" compara días de entrenamiento (con su media semanal), sesiones completadas (con plan y detectadas por el reloj, que cuentan como entrenamiento real), minutos de gimnasio, series registradas en la app y progresión por patrón entre ventanas de 30/60/90 días, con valoración IA asíncrona (mismo patrón de cola en segundo plano y polling que el resto de análisis del sistema) que cruza el entrenamiento con Recovery, peso y composición corporal. La sección "Evolución por ejercicio" permite elegir un ejercicio con histórico real y un rango de fechas libre, y muestra tres gráficos Chart.js independientes (repeticiones, series, descanso real) por la distinta escala de cada magnitud.

Rutina fija de cuerpo completo — alternativa independiente

RutinaFijaController/RutinaFijaService añaden una segunda opción de entrenamiento, deliberadamente al margen de todo lo anterior: no crea EntrenamientoSesion ni EntrenamientoEjecucion, no guarda ninguna sesión — es una guía en pantalla que el usuario avanza a su ritmo con un clic (sin cuenta atrás), mostrando solo un cronómetro total informativo. El entrenamiento real lo registra el reloj, igual que una sesión CompletadaExterna del motor adaptativo.

Rutina personal por usuario. Cada usuario dispone de su propia tabla de ejercicios (UsuarioRutinaFijaEjercicio: usuario, ejercicio del catálogo y bloque), copiada la primera vez del banco inicial (RutinaFijaCuerpoCompleto: 16 de calentamiento, 7 de movilidad, 12 de estiramientos y 23 de fuerza en cuatro bloques corporales). Desde la ventana «Mi rutina» el usuario quita ejercicios y añade cualquiera del catálogo general (con buscador y un filtro de compatibilidad: sin suelo y con banda elástica, mancuernas o sin material), elige el bloque de los de fuerza y puede restablecer el banco inicial. La fase de un paso la identifica el patrón de movimiento del catálogo (Calentamiento, Movilidad, Estiramiento); los pasos de estas tres fases son ejercicios del catálogo, sembrados con su duración en segundos y con figura SVG. Los ejercicios excluidos por el usuario en su perfil no entran en ningún sorteo. La tabla no tiene clave foránea al catálogo ni a Identity y se limpia a mano al borrar un usuario o un ejercicio.

Sesión a la medida del tiempo. El usuario indica cuántos minutos tiene. El tiempo de cada ejercicio sale de los datos del catálogo: series × (trabajo + descanso), con el trabajo igual a los segundos si es «por tiempo» o a las repeticiones por unos 3 s, y un descanso de circuito de 20 s solo en fuerza. La duración se reparte entre calentamiento (≈10 %), vuelta a la calma (≈15 %, con una marcha suave fija) y parte principal, que se sortea por turnos entre los cuatro bloques con al menos un ejercicio de cada uno; si la rutina del usuario no da para el tiempo pedido, o el mínimo lo supera, se avisa. Todos los ejercicios tienen la misma probabilidad. Antes de empezar se muestra una vista previa con el tiempo de cada ejercicio: el sorteo es reproducible con una semilla (recargar no lo cambia), se puede sortear otro, cambiar un ejercicio solo en esa sesión o quitarlo de la rutina, y «Empezar» arranca exactamente esa lista. Los ejercicios propios de esta rutina y los pasos de calentamiento, movilidad y estiramiento forman parte del catálogo de sistema mediante RutinaFijaCatalogoAdicionalSeeder, idempotente por nombre y separado de CatalogoEjercicioSeeder, que no vuelve a sembrar una vez que el catálogo tiene contenido; sus figuras las siembra CatalogoEjercicioFiguraSeeder.

8.31. Detección de saltos de peso abruptos y aviso condicional en análisis IA
Motivación

Un cambio de peso día a día muy por encima de lo habitual en un paciente concreto puede tener causas muy distintas — ruido de báscula, retención hídrica (sodio, alcohol, inflamación), sobreentrenamiento/fatiga acumulada por un cambio de patrón de ejercicio, o incluso termogénesis adaptativa. El sistema señala este tipo de evento de forma explícita a los análisis IA, para que no comenten la tendencia de peso sin saber que el dato más reciente es un outlier respecto al propio histórico del paciente.

SaltoPesoDetector — z-score personal, no umbral poblacional

Mismo principio que HrvOutlierFilter (ver 8.18): compara el último salto absoluto de peso día a día contra la media/sigma de los saltos históricos de ESE paciente, no contra un umbral fijo igual para todos. Se dispara con z≥2,5 y exige un mínimo de 20 saltos históricos previos para calcular media/sigma con fiabilidad (arranque en frío: sin histórico suficiente, no se evalúa). Solo usa pesadas con PesoMedidoEnUtc real — nunca el fallback heredado de días sin báscula —, para no calcular la varianza histórica ni el salto evaluado sobre datos carried-forward.

Bloque de prompt SALTO_PESO_ABRUPTO

Bloque inyectado condicionalmente (solo si el detector dispara) que advierte a la IA de que un patrón fisiológico real es tan plausible como un artefacto de medición — explícitamente no descarta ninguna de las dos, y pide tratarlo como observación a vigilar, nunca como diagnóstico. Sembrado vía SaltoPesoPromptBlockSeeder (/Inicializer/SeedSaltoPesoPromptBlock).

Alcance: análisis que reciben el aviso

Integrado en BiodataAiSummaryService (evolución biométrica, vía BuildBlockAsync suelto, igual que el bloque de señal de fuerza) y en HistoriaPacienteService/HistoriaPacienteController (el bloque se añade tras BuildAsync porque el mecanismo de bloques enlazados a plantilla desde el admin no soporta "incluir solo si hay dato" — enlazarlo así lo haría aparecer siempre, vacío, cuando no hay salto). Adaptación Metabólica, Correlación Termogénesis-Recuperación y Flexibilidad Metabólica son puramente computacionales/basados en reglas — no generan ninguna narrativa IA en la que este aviso pudiera engancharse. ValoracionFuerzaIaService (entrenamiento de fuerza) sí genera narrativa IA que cruza con peso, pero solo recibe el peso inicial y final del periodo, no la serie diaria con PesoMedidoEnUtc, por lo que no incorpora este aviso.

Seguimiento de recuperación (DetectarRecuperacion) y comparación de HRV

Complementa al aviso anterior sin sustituirlo: mientras Detectar solo mira si el ÚLTIMO punto de la serie es un salto (evento recién ocurrido, sin datos posteriores todavía), DetectarRecuperacion busca hacia atrás (hasta 30 días) el salto más reciente que YA tenga alguna pesada real posterior, y compara esa pesada contra la mediana (no un punto único) de los 5 días previos al evento — pensado para cuando el detector opere sobre datos reales con su propio ruido día a día (agua, glucógeno), donde un punto suelto podría caer en un valle o un pico por pura casualidad. Si la lectura más reciente cae dentro del margen habitual de saltos de ese paciente respecto a esa mediana, se considera recuperado, con los días transcurridos. Cuando hay recuperación evaluable, EvaluarHrvConcomitante añade la mediana de HRV de los mismos 5 días previos al evento frente a la mediana de HRV alrededor de la fecha de recuperación — deliberadamente sin umbral de "normalizado sí/no": el HRV tiene demasiada variabilidad relativa natural para fijar una tolerancia defendible sin más estudio, así que se deja como dato descriptivo para que el bloque de prompt (y la IA) lo interprete, nunca como condición que dispare o bloquee el veredicto de recuperación del peso. Bloque separado (SALTO_PESO_RECUPERADO), inyectado en los mismos puntos que SALTO_PESO_ABRUPTO.

8.32. Captura cruda de SpO2, calibración y análisis por periodos (reposo/actividad)
Motivación

El promedio diario único de BiodataDiaria.SaturacionOxigeno oculta dropouts de sensor de hasta 20 puntos dentro de la misma noche (ej. una lectura aislada de 78% entre lecturas de 91%) y no distingue si una caída ocurrió durante el sueño o durante actividad física — dos contextos fisiológicos muy distintos que no deberían mezclarse en una sola media. A diferencia del cribado de apnea (ver 8.24), que lee export.xml bajo demanda sin persistir nada, este servicio sí guarda cada muestra cruda con su hora exacta, porque necesita reconstruir series por periodo (reposo vs. actividad) y una calibración frente a un dispositivo de referencia, ninguna de las dos cosas reconstruible desde un agregado diario ya colapsado.

Captura desde los dos pipelines de HealthKit, no solo uno

SaturacionOxigenoRaw se alimenta de forma independiente desde los dos caminos de entrada de HealthKit, sin que ninguno dependa del otro: HealthKitJsonPersistenceService (automatización de pago Health Auto Export, vía HealthAutoExportApiController) y HealthImportPreviewPersistenceService (exportación nativa export.xml de Apple, la vía que no exige comprar ninguna app adicional en iOS y por tanto la más probable para la mayoría de usuarios reales). Ambos extraen el mismo tipo de muestra (HKQuantityTypeIdentifierOxygenSaturation), normalizan el valor a escala 0-100 si llega como fracción 0-1, y comparten el mismo criterio de deduplicado por (usuario, instante, fuente) antes de insertar — best-effort: un fallo aquí nunca impide que BiodataDiaria se guarde igualmente.

Calibración manual frente a dispositivo de referencia

Spo2Controller/SpO2CalibracionMuestra permite registrar pares (lectura del reloj, lectura de un pulsioxímetro externo tomada casi al mismo instante). Antes de validar, la vista muestra cuatro estadísticos de las diferencias (Referencia − Reloj) para que el usuario decida con criterio: diferencia media (offset, la única cifra que se guarda al validar), diferencia mediana (una mediana muy alejada de la media delata una muestra suelta arrastrando el offset), desviación típica muestral (n-1) y MAE (media de |diferencia| sin compensar signos — a diferencia del offset, no se cancela si el reloj oscila por encima y por debajo a partes iguales, así que separa "sesgo fijo" de "ruido errático"). Se añade además un gráfico de Bland-Altman (eje X = media del par, eje Y = diferencia, con el sesgo y los límites de acuerdo al 95% — sesgo ± 1,96σ — como referencias horizontales): método estándar de comparación de instrumentos que no asume que ninguno de los dos sea un patrón absoluto, relevante aquí porque el pulsioxímetro de referencia típico en este uso (doméstico, ±2-3% de precisión declarada) tampoco lo es.

Bajo el gráfico, EvaluarCalibracion resume esa lectura en un aviso automático — heurística de sentido común, no un test estadístico formal: con menos de 10 pares avisa de que la desviación típica y los límites de acuerdo aún no son fiables; con una correlación de Pearson ≥0,5 entre la media del par y la diferencia avisa de que el sesgo podría no ser constante en todo el rango (candidato a regresión lineal futura, no a offset); si no hay indicios de tendencia, compara el semiancho de los límites de acuerdo (1,96×σ) contra 2 puntos — umbral orientativo, no clínico — para distinguir "diferencias consistentes" de "conviene revisar condiciones de medida o añadir más muestras". La decisión de validar sigue siendo siempre del usuario.

"Validar y activar" crea un SpO2FactorCorreccion: un offset constante (la diferencia media), con el campo Metodo como discriminador del método de corrección. Cada factor guarda su propia ventana de vigencia (FechaInicioVigencia/FechaFinVigencia): al validar uno nuevo, se desactiva el anterior y se le cierra la vigencia justo donde empieza el nuevo, de modo que la línea temporal de factores nunca tiene huecos ni solapes.

La corrección SÍ llega a BiodataDiaria.SaturacionOxigeno, por dos vías deliberadamente distintas: automática en cada importación nueva (AplicarCorreccionSpO2, duplicada en los dos servicios de persistencia de HealthKit, aplicada una vez que el mapper ya fijó el valor — usa siempre el factor Activo, sin mirar fechas de vigencia, porque una importación nueva siempre trae datos de "ahora") y manual para el histórico ya guardado (AplicarCorreccionHistorica, nunca se dispara sola: el usuario elige un rango de fechas y confirma). El backfill recalcula cada día desde SaturacionOxigenoRaw — no desde el valor ya guardado, para no arrastrar el método de agregación original — agrupando por día efectivo en hora de Madrid, y usando para cada día el factor cuya vigencia lo cubre (un rango que abarque varias calibraciones distintas usa el factor correcto en cada tramo). Al partir siempre del crudo intacto, repetir el backfill es idempotente. Ninguna de las dos vías toca SaturacionOxigenoRaw ni el placeholder 0 (= "sin dato") de SaturacionOxigeno.

Spo2AnalysisService — mediana/IQR por reposo y actividad

Clasifica cada muestra cruda como reposo o actividad reutilizando, sin recalcularla, la ventana de sueño real ya persistida en BiodataDiaria.SueñoInicioUtc/ SueñoFinUtc — el mismo criterio autorreferencial al hábito de sueño de cada usuario que ya se usa para el HRV nocturno, en vez de una franja horaria fija igual para todos. Por cada día calcula mediana e IQR (percentil por interpolación lineal, igual que numpy/Excel) por separado para reposo y actividad: la mediana amortigua los mismos dropouts de sensor que motivaron el servicio, y el IQR cuantifica cuánto varió la señal ese día sin que un solo valor extremo lo distorsione, algo que ni la media ni la desviación típica logran igual de bien con series tan escasas (2-8 muestras episódicas por noche).

Score de reposo — percentil contra el propio historial

CalcularScoreReposoAsync sitúa la mediana de reposo de un día dentro de la distribución histórica de medianas de reposo del propio paciente (0-100, 100 = mejor noche registrada) — nunca contra baremos poblacionales, mismo principio que el resto del sistema (ver 8.27). Exige al menos 5 días previos con dato de reposo; con menos, devuelve null en vez de un percentil que no significaría nada fiable sobre una muestra tan pequeña.

Vista de evolución — todo calculado al vuelo, nada persistido

/Spo2/Evolucion no persiste ningún resultado propio: el badge de "última sincronización", la serie diaria y el score se recalculan en cada carga de página directamente sobre SaturacionOxigenoRaw, así que nunca pueden quedar desactualizados frente a la última importación. La tabla y el gráfico incluyen además, como tercera serie, el valor tal cual guardado en BiodataDiaria.SaturacionOxigeno — contraste que permite comprobar que ambos caminos (agregado diario vs. mediana de reposo/actividad sobre el crudo) son coherentes entre sí, y para ver de un vistazo el efecto de un factor de corrección una vez activo (la serie de BiodataDiaria se desplazaría respecto a las de reposo/actividad, que siempre muestran el dato crudo sin corregir). Incluye descarga del detalle diario en JSON (generado en el propio navegador, sin ida y vuelta al servidor) para que el paciente pueda estudiar la serie con otra herramienta si lo desea.

Análisis IA de la evolución de SpO2 — carga de desaturación nocturna

La pestaña "Análisis IA" de /Spo2/Evolucion lanza, bajo demanda y sin caché, la interpretación del prompt SPO2_ANALISIS_EVOLUCION. Antes de llamar a la IA, Spo2AnalysisService.GetCargaDesaturacionAsync calcula sobre las lecturas crudas de reposo (solo las de la ventana de sueño real) las siguientes cifras, para la ventana elegida y para todo el histórico fiable del usuario (su basal propia): mediana y mínimo, porcentaje de lecturas por debajo de 92 % y de 90 %, noches con al menos una lectura por debajo de 90 %, rachas de 3 o más lecturas seguidas ≤ 90 % de una misma noche (un hueco de más de 45 minutos corta la racha), porcentaje de lecturas bajas en las primeras 3 horas de sueño frente al resto de la noche y mediana nocturna de la primera y la segunda mitad de la ventana. Spo2AnalysisDataBlock añade el perfil (edad, altura, peso, IMC) y los cortes de referencia de SpO2Classifier para ese perfil. La IA solo interpreta esas cifras ya calculadas: compara siempre con la basal propia antes que con referencias generales, considera llamativos únicamente los cambios que superan criterios heurísticos fijados en el prompt (2 puntos porcentuales más de lecturas <90 %, el doble de rachas por 30 noches con un mínimo de 2, o 1 punto menos de mediana), tiene prohibido nombrar enfermedades o dar causas, y adapta el texto a la audiencia (paciente o profesional; el profesional puede ver la versión de paciente). La respuesta se completa siempre con el aviso de contenido generado por IA.

Zona de SpO2 — corte ajustado por edad e IMC y filtrado por línea base personal

SpO2Classifier clasifica la saturación en tres zonas: "Baja" (<90 %, sin ajuste, umbral de hipoxemia independiente del perfil), "Límite" y "Normal". El corte de "Límite" parte de 95 % y se reduce 1 punto si la edad es ≥65 años y otro punto si el IMC (calculado con la altura del perfil y el último peso disponible) es ≥30. Son constantes heurísticas: la dirección del ajuste es coherente con los estudios poblacionales, pero el tamaño de cada punto es una decisión de diseño, no un valor publicado. En BioDataEvolutionAnalysisService la zona resultante se contrasta además con el z-score personal de la media del período frente al historial anterior (PersonalBaselineZScore): "Límite" solo se mantiene si ese z-score es ≤ −1 o si no hay baseline suficiente para calcularlo; en caso contrario se muestra "Normal". El resumen IA solo recibe la instrucción de mencionar el SpO2 en señales de atención cuando la zona final es "Baja" o "Límite". El marcador de HealthMarkerService (un solo día, sin baseline) aplica el corte ajustado pero no el filtro por z-score.

8.33. Objetivo Nutricional — kcal/macros personalizados y corrección por ventana
Referencia de proteína: ESPEN

La referencia de proteína de esta pantalla es ProteinRequirementService (consenso ESPEN 2022/2024), la misma que se usa en Biodata/Informes y en EnergyManager/Detail. Es una única referencia en toda la app, no dos distintas.

Objetivo diario de kcal y reparto de macros

NutritionTargetService parte del TDEE ya calibrado (media de 15 días de EnergyDailyBalance.EnergiaGastoTotalKcal, ya con los factores basal/activo aplicados — ver 8.16) y del ritmo de cambio de peso objetivo que el usuario fija en su perfil (PerfilPersonal.RitmoObjetivoKgSemana, kg/semana, negativo=pérdida): ObjetivoKcal = TDEE + RitmoObjetivoKgSemana × 7700 / 7. El reparto en macros se calcula tomando la proporción real proteína/grasa/hidratos que el usuario registró en la última semana (gramos de tabla/diccionario, sin el factor de calibración de ingesta) y aplicándola al kcal objetivo vía Atwater — el reparto propuesto refleja así los hábitos reales del usuario, reescalados al total de kcal correspondiente a su ritmo de peso objetivo. Si no hay ingesta registrada en la última semana, se marca SinDatosRecientes y solo se muestra el kcal objetivo. Si el kcal objetivo resultante no es positivo, se marca ObjetivoIncompatible — señal de que el ritmo pedido es demasiado agresivo para el TDEE. Junto a cada macro se muestra también su porcentaje sobre el total de kcal del objetivo (proteína×4, grasa×9, hidratos×4, cada uno dividido entre ObjetivoKcal) y, en una línea aparte, el mismo gramaje dividido por el factor de calibración de ingesta ("sin corregir") — para comparar de un vistazo con lo anotado literalmente en Asistencia Ingesta/BiodataDiaria, que nunca lleva ese factor aplicado. Un botón junto a la tabla abre un modal con los rangos de referencia AMDR (Institute of Medicine — proteína 10-35%, grasa 20-35%, hidratos 45-65%) junto al reparto propio del usuario, explicando que esos porcentajes no son una prescripción sino el reflejo de su ingesta real, y que el AMDR es una referencia poblacional general — sin juzgar si el reparto del usuario es adecuado para su caso.

Corrección por ventana de 5 días, con el alcohol siempre separado

IngestaCorreccionService compara la media de los últimos 5 días completos (excluye siempre el día en curso, cuya ingesta/gasto están a medias) en dos ejes independientes: cantidad total de comida real contra el objetivo de kcal, y reparto de proteína contra la referencia ESPEN. El alcohol se resta siempre de la ingesta antes de esta comparación (IngestaComidaKcal = EnergiaIngeridaKcal − AlcoholGr×7) porque no es un macronutriente y puede enmascarar un déficit real de comida si no se separa. El umbral de aviso (12%) no es tan estrecho que salte con el ruido normal de un día ni tan ancho que deje pasar una desviación sostenida real. Los gramos de proteína/grasa/ hidratos que se muestran en esta media (y en Progreso de hoy) se reescalan por día al mismo nivel del kcal neto/corregido (no del kcal de tabla), para que su Atwater cuadre exactamente con el kcal mostrado al lado; el aviso de reparto, en cambio, usa la media sin reescalar (gramos de tabla del diccionario), por ser lo comparable contra la referencia ESPEN, expresada en gramos reales. Cada fila de esta tabla muestra también, en una línea aparte, esa misma media sin reescalar ("sin corregir") — para comparar de un vistazo con lo anotado literalmente en Asistencia Ingesta. Al pie de esta tabla, y de la tabla de macros del Objetivo, se indica el factor de calibración de ingesta aplicado — un único valor en el Objetivo (cálculo hacia adelante con el factor actual), o el rango mín-máx en Seguimiento si la ventana de 5 días cruza una recalibración.

Para cada día de la ventana con alcohol registrado, se calcula además una nota de compensación (hidratos a restar = kcal de alcohol / 4) — puramente informativa y retrospectiva: solo sirve de guía para la próxima vez que el alcohol se anticipe, no corrige el histórico ya cerrado.

Progreso de HOY leído en vivo desde los slots de ingesta

A diferencia de la corrección por ventana (que solo mira días ya cerrados), IntakeDiaProgresoService lee los IntakePoolItem del día en curso y deserializa su NormalizedText — el mismo JSON de nutrición que usa IntakeExecutionService.ApplySlotToBiodataAsync al aplicar un slot a BiodataDiaria (clases NutritionJson/NutritionTotals, reutilizadas vía using static) — para sumar kcal/macros de cualquier slot que ya haya pasado por el cálculo de IA (Calculado/Confirmado), aunque todavía no esté "Aplicado". Los slots en Borrador (sin NormalizedText aún) se cuentan aparte y se avisan, sin contaminar la suma. Esto permite mostrar barras de progreso del día en curso sin esperar a que cada comida quede confirmada, algo que la vía de BiodataDiaria/EnergyDailyBalance (pensada para días ya cerrados) no puede dar. Bajo cada barra se muestra también el valor sin el factor de calibración de ingesta aplicado (kcal y gramos tal como se anotaron en Asistencia Ingesta), para que coincida con lo que el usuario ve al registrar la comida en esa otra pantalla.

Sugerencias de alimentos — los tres macros, ración fija, nunca un único alimento "exacto"

SugerenciaAlimentosService usa la biblioteca personal (IntakeFoodLibraryItem, no un catálogo genérico) para sugerir alimentos que cierren el hueco de HOY en los tres macros por separado (proteína, grasa e hidratos), ordenando por densidad del macro buscado sobre sodio (macroG100 / (1 + sodioMg100/1000), criterio "alto en el macro, bajo en sodio"). La ración mostrada es siempre una referencia fija de 100g, nunca la cantidad exacta que cerraría el hueco completo con un solo alimento: las sugerencias son opciones para combinar, no una ración única que resuelva el día con una sola cosa.

8.34. Transparencia de factores de calibración en las pantallas de Energía

Las pantallas Index, Report y Detail de Energía muestran, junto a cada valor de Ingerida/Basal/Activa, el factor de calibración que se aplicó ese día, como superíndice entre corchetes [factor]. EnergyBalanceDetailViewModel y EnergyBalanceRowViewModel exponen FactorIngesta/FactorBasal/ FactorActivo directamente desde EnergyDailyBalance, donde quedan congelados por fila (ver 8.16), de modo que el factor aplicado se lee directamente y no hay que deducirlo a partir de un porcentaje.

Detail se organiza en pestañas (Macronutrientes / Calidad del día, mismo patrón "carpetilla" que 8.21/8.33), y el objetivo de proteína de esa pantalla se etiqueta explícitamente como ESPEN 2022, la misma referencia que usa Objetivo Nutricional (8.33).

8.35. Recálculo del balance energético y uso del factor histórico
Recalcular es siempre una acción explícita

Consultar el Panel Energético o el Informe (incluido el filtro Desde/Hasta) no recalcula nada: las acciones de consulta de EnergyManagerController solo leen. Los valores de EnergyDailyBalance de días pasados, con su FactorIngesta congelado, solo cambian mediante Recalcular rango, una acción separada que pide confirmar el rango exacto y el factor antes de ejecutarse. Permite tratar por separado tramos que deben conservar factores distintos y, eligiendo Desde el primer registro Hasta hoy, cubre todo el histórico.

Adaptación Metabólica usa la ingesta calculada con el factor de cada día

MetabolicAdaptationService.ComputeWindowAsync obtiene la ingesta corregida de la ventana sumando EnergyDailyBalance.EnergiaIngeridaKcal de los días válidos, un valor ya calculado con el factor de calibración vigente el día en que se guardó cada fila (congelado, ver 8.16), en lugar de recalcularla desde los macros crudos de BiodataDiaria con el factor actual. Así, una ventana de Adaptación Metabólica o de Flexibilidad Metabólica que abarque un cambio de calibración respeta el factor de cada tramo. HistoricoAdaptacion lee los snapshots ya guardados.

8.36. Estado Corporal — adiposidad, capacidad física, ejes de contexto y evolución

Cuarta pestaña de Objetivo Nutricional. Distinto de la "lectura corporal" del Panel de Salud (HealthDash) (ILecturaCorporalService, IMC + índice cintura/talla): aquí la adiposidad se basa en la grasa corporal medida por bioimpedancia (BiodataDiaria.GrasaCorporalImpedanciaPct), no en el IMC, por ser un dato más directo de composición corporal cuando está disponible.

Tabla de referencia de grasa corporal por edad y sexo

BodyStatusService compara la última GrasaCorporalImpedanciaPct registrada con una tabla de cortes por sexo y tramo de edad (20-39/40-59/60-79), equivalentes a las categorías de IMC 18,5/25/30 — proporcionada directamente para esta función, no derivada de ninguna fórmula interna. Edades fuera de los tramos cubiertos se ajustan al extremo más cercano (<20 usa 20-39, ≥80 usa 60-79). Si no hay una medición reciente de grasa por impedancia, o el sexo del perfil (PerfilPersonal.Genero == 'O') no tiene banda propia en la tabla, se usa en su lugar el IMC estándar OMS (<18,5/25/30), indicándolo expresamente en la pantalla (BodyStatusResult.UsoFallbackImc).

Tres ejes de contexto, siempre visibles por separado

A diferencia de ILecturaCorporalService (que combina los ejes en un único resumen de texto), aquí los tres ejes se muestran siempre como indicadores independientes, sin fundirlos en un solo bloque: cardiovascular (media de TensionSistolica/TensionDiastolica de los últimos 7 días, clasificada con TensionArterialClassifier.Zona — mismos umbrales ESC/ESH que el resto de la app) y capacidad aeróbica (último BiodataDiaria.Vo2Max anterior al día en curso —el VO₂ máx conserva el último valor medido—, estimado por el reloj al caminar o correr al aire libre, no una prueba de esfuerzo), clasificada con la tabla de niveles por sexo y década de edad que muestra la app Salud (Alta, Por encima de la media, Por debajo de la media, Baja; solo desde los 20 años, con una única banda a partir de los 60; ambas tablas, mujeres y hombres, se muestran completas en el detalle del cálculo de la pestaña, leídas de CapacidadAerobicaReferencia.Tabla). De estos niveles solo "Baja" cuenta como factor de riesgo; "Por debajo de la media" se muestra pero no suma. El eje toma el más grave de las dos señales; metabólico-analítico (última AnaliticaClinica, limitado a dos bloques de la última AnaliticaClinica: glucosa >106 y HbA1c ≥5,7 — solo cuentan como señal si están alteradas a la vez, o si la glucosa lo está sola (valor directo, no promediado); una HbA1c algo alta en solitario con glucosa normal se muestra pero no suma a la severidad, por ser un hallazgo aislado sin confirmar (mismo criterio de convergencia aplicado un nivel más abajo, dentro del propio eje) — y triglicéridos ≥150 o índice aterogénico colesterol/HDL ≥4,5. A diferencia de LecturaCorporalService, este eje NO incluye enzimas hepáticas (AST/ALT/GGT) ni ácido úrico: son marcadores con causas frecuentes ajenas a la adiposidad (p. ej. el alcohol), y aquí el eje busca específicamente riesgo asociado al peso/composición corporal, no un cribado general; y recuperación (media de HrvMs de los últimos 7 días frente a la línea base personal del usuario en RecoveryBaseline, filtrada por HrvOrigen — nunca un umbral fijo de población, mismo criterio que ya usa RecoveryService/LecturaCorporalService). Si la última analítica tiene más de 6 meses, el eje metabólico se muestra igualmente pero como "sin dato reciente" — no cuenta ni como alterado ni como normal, para no dar una imagen desactualizada como si fuera vigente.

Categoría final: adiposidad × nº de ejes alterados

La categoría final combina la adiposidad (Bajopeso/Normal/Sobrepeso/Obesidad) con el nº de los tres ejes de contexto que están alterados (0/1/2+) — mismo criterio de "severidad = nº de ejes alterados" que ya usa LecturaCorporalService, aplicado aquí a un resultado nombrado en vez de un resumen narrativo. Dos combinaciones reciben un nombre propio de Fenotipo, inspirado en conceptos clínicos reales pero no una clasificación oficial (explicado siempre en el modal "¿Cómo se calcula?" de la propia pantalla): "Obesidad metabólicamente sana" (adiposidad de obesidad, pero ningún eje de contexto alterado) y "Normopeso con riesgo metabólico asociado" (adiposidad normal, pero 2 o más ejes alterados). El resto de combinaciones usan una descripción genérica: categoría de adiposidad + nº de factores de riesgo asociados. Cuando la adiposidad es Obesidad, se añade además el grado I/II/III de la OMS (IMC 30-34,9/35-39,9/≥40 — GradoObesidadImc), calculado siempre sobre el IMC aunque la categoría venga de la grasa corporal medida, porque la tabla de bandas por edad/sexo no distingue gravedad dentro de "Obesidad" (p. ej. "Obesidad I, con 1 factor de riesgo asociado").

Evolución — misma clasificación día a día, sin repetir consultas por día

IBodyStatusService.EvaluarEvolucionAsync reutiliza exactamente el mismo criterio de clasificación que el día de hoy (adiposidad, los tres ejes de contexto y su combinación están extraídos a métodos estáticos puros compartidos por ambos caminos, para que nunca diverjan), pero evaluado retrospectivamente día a día en un rango Desde/Hasta que el usuario elige (3 meses por defecto). En vez de repetir las mismas consultas por cada día del rango —potencialmente cientos si se pide todo el histórico—, carga una única vez toda la BiodataDiaria, AnaliticaClinica y RecoveryBaseline del periodo necesario, y evalúa cada día en memoria usando solo los datos vigentes hasta esa fecha (nunca datos posteriores a ella). El resultado se muestra como una línea de tiempo: los días consecutivos con la misma categoría final Y los mismos ejes de contexto alterados se agrupan en un único segmento coloreado, con sus fechas de inicio/fin y el nombre de los ejes concretos alterados en ese tramo (p. ej. "Cardiovascular, Recuperación (HRV)"), para visualizar los cambios de estado sin necesidad de leer una fila por día. Debajo, una segunda línea de tiempo independiente hace lo mismo para Capacidad Física (ICapacidadFisicaService.EvaluarEvolucionAsync, mismo patrón de carga única y clasificación compartida con EvaluarAsync) — cada día del rango recalcula sus propias ventanas de 14 días recientes y 90 días de línea base, todo resuelto en memoria sobre los datos ya cargados en bloque.

Capacidad Física — segunda categorización, independiente de la adiposidad

ICapacidadFisicaService añade, en la misma pestaña Estado Corporal, una categorización separada que no se fusiona con la de adiposidad — mide actividad real, no riesgo por peso/grasa. El eje principal compara los minutos de los últimos 14 días con la guía de actividad física de la OMS (2020): fuerza (mínimo 2 días/semana con ≥20 minutos de gimnasio, el mismo umbral que usa HistoriaPacienteService) y aeróbico (mínimo 150 minutos/semana de actividad exterior). Los minutos de gimnasio se toman tal cual de BiodataDiaria.MinutosEjercicioGym, sin distinguir si proceden del entrenamiento guiado (EntrenamientoEjecucion) o de la rutina fija libre. Categoría: "Activo" (cumple los dos), "Parcialmente activo" (cumple uno), "Sedentario" (ninguno) — nombres propios de Fenotipo, la guía citada es real pero esta categorización no lo es.

Junto a eso, tres ejes de contexto comparan el valor reciente (14 días) contra la propia línea base de los 90 días anteriores del usuario, vía PersonalBaselineZScore (mismo helper que ya usa BioDataEvolutionAnalysisService para tensión/SpO2, sin tocar RecoveryBaseline/SignalRecovery, reservado a HRV/FC/temperatura/sueño) — mismo criterio temporal de línea base (90 días anteriores, mínimo 14 días con datos) que ya usa FlexibilidadMetabolicaService para este mismo tipo de dato: (1) energía basal de EnergyDailyBalance (valor calibrado), como reflejo de NEAT — el basal de HealthKit no es un cálculo metabólico teórico fijo, varía con el movimiento incidental real del día; (2) minutos de ejercicio totales (gimnasio + exterior); (3) capacidad cardiorrespiratoria: último VO₂ máx (consumo máximo de oxígeno) —estimación del reloj al caminar o correr al aire libre, conservada hasta que haya una medida nueva—, con el z-score de ese último valor frente a la línea base de 90 días y una desviación típica mínima de 0,5 ml/kg/min; además se indica, solo como información y sin alterar el nivel del eje, el nivel de ese VO₂ máx frente a la población de la misma edad y sexo (misma tabla, CapacidadAerobicaReferencia, que usa el eje cardiovascular de Estado Corporal). La categoría final combina la etiqueta OMS con estos tres ejes, señalando expresamente cuando la tendencia personal contradice la etiqueta poblacional (p. ej. "Sedentario según la guía OMS, aunque por encima de tu propia media reciente"), para que una mejora real del usuario no quede oculta detrás de un umbral absoluto que aún no alcanza.

8.37. Análisis de sueño: duración, regularidad y despertar precoz autorreferenciados
Objeto

La página Sueño (SleepAnalysisService, SleepAnalysisController) analiza el patrón de sueño de una ventana de 30, 60 o 90 días a partir del episodio principal de cada noche ya presente en BiodataDiaria (SueñoInicioUtc, SueñoFinUtc, minutos por fase y minutos despierto). Se calcula al vuelo, sin tablas propias y sin modificar el dato de origen. Cada noche se atribuye al día en que termina. La hora de despertar corresponde al final del último tramo dormido registrado por el reloj, no a la hora de salir de la cama.

Tratamiento de las horas

Las horas se convierten a la zona horaria del perfil del usuario (PerfilPersonal.ZonaHoraria, con Europe/Madrid por defecto) antes de cualquier cálculo, de modo que el cambio de hora estacional no aparece como un desplazamiento del horario. Para promediar y calcular dispersiones, cada hora se expresa como minutos transcurridos desde un punto de corte alejado de los valores habituales (12:00 para la hora de acostarse, 18:00 para la de despertar), lo que mantiene contiguas horas a ambos lados de la medianoche.

Basal a largo plazo como referencia principal

Conforme al modelo jerárquico de referencia del sistema, cada métrica se compara en primer lugar con la basal del propio usuario: el mismo cálculo sobre todo el histórico disponible anterior al periodo analizado, hasta 365 días y nunca anterior al 1 de abril de 2026 (límite global de fiabilidad del registro de sueño), con un mínimo de 28 noches con horario (SleepBaselineLargoPlazo). Las referencias poblacionales (7 horas, bandas MESA) se presentan como contexto y solo actúan como referencia principal cuando la basal no alcanza ese mínimo. La comparación con el periodo anterior de igual longitud indica si un cambio es reciente.

Bloques
  • Calidad del dato: noches con ventana completa, noches con fases pero sin horario (cuentan para la duración y no para la regularidad) y días sin sueño registrado.
  • Duración: media de minutos dormidos (ligero + profundo + REM), porcentaje de noches con 7 horas o más (consenso AASM/SRS, Watson et al., 2015) y comparación con la ventana anterior de igual longitud.
  • Regularidad: desviación típica de la hora de acostarse, de la hora de despertar y de la duración, con un mínimo de 10 noches (Fischer et al., 2021). Las bandas de interpretación proceden del estudio MESA (Huang et al., 2020): ≤30, 31-60, 61-90 y >90 min para la hora de acostarse; ≤60, 61-90, 91-120 y >120 min para la duración.
  • Despertar precoz: una noche es de despertar precoz cuando termina al menos 90 minutos antes de la hora habitual de despertar del propio usuario, calculada para cada noche como el percentil 75 de las horas de despertar de las 28 noches anteriores (mínimo 7). El percentil 75 representa las noches no interrumpidas, de modo que una racha de despertares frecuentes no desplaza la propia referencia. Se informa la frecuencia en noches por semana y se marca como frecuente con 3 o más noches por semana de media en las últimas 4 semanas (mínimo 14 noches evaluables).
Factores asociados (vista del profesional)

Para los usuarios con rol Profesional o Administrador, la página añade una comparación entre noches con despertar precoz y noches normales en cuatro factores: alcohol y kilocalorías ingeridas el día anterior, minutos de ejercicio del día anterior y hora de acostarse. Cada noche precoz se empareja con la media de las noches normales situadas a ±14 días, y se promedian los pares; el emparejamiento local evita que diferencias entre periodos (por ejemplo, un mes con mayor déficit calórico y más despertares) se presenten como asociación noche a noche. Un factor se considera evaluable con al menos 5 pares y 3 noches normales cercanas por par. Los días sin ingesta registrada no aportan dato de alcohol ni de kilocalorías. Cuando menos del 20 % de las noches evaluables proceden de un día sin alcohol, se indica que la comparación refleja la cantidad y no la presencia o ausencia de consumo. El resultado se presenta como asociación con los datos registrados, no como causa. El paciente ve únicamente los bloques descriptivos.

Componente de sueño del panel de Recuperación

El mismo criterio de fiabilidad del dato rige el componente de sueño del Recovery Score (RecoveryService.CalcularZscoreComposicionSueno, 15 % del score): z-score compuesto frente a la basal personal de 30 días, con duración total 60 %, vigilia nocturna invertida 20 %, sueño profundo 10 % y REM 10 %, renormalizados si algún subcomponente carece de histórico. El peso recae en las magnitudes que el reloj de muñeca registra con más fiabilidad; las fases conservan un peso reducido.

Interpretación IA bajo demanda

La pestaña Interpretación IA envía al prompt SUENO_ANALISIS (catálogo de prompts programables) un bloque de texto con las cifras ya calculadas y la audiencia —paciente o profesional—; el bloque de factores solo se incluye para el profesional. El prompt limita la interpretación a los hallazgos que superan criterios explícitos frente a la basal propia (duración media al menos 30 minutos menor, desviación típica al menos 30 minutos mayor, al menos una noche semanal más de despertar precoz o despertar precoz frecuente). Las referencias poblacionales no generan hallazgos por sí solas: un patrón estable respecto a la basal no se comenta aunque difiera de ellas, y sin basal suficiente solo se tratan como hallazgo si las señales fisiológicas nocturnas están empeoradas. Todo cambio se contrasta con la VFC y la FC en reposo frente a su propia basal (z medio de RecoveryDailyScore en el periodo y en el anterior): con señales en lo habitual o mejor (z ≥ −0,5) el tono es descriptivo y sin alarma. El prompt admite como máximo dos sugerencias de una lista cerrada y excluye la denominación de entidades clínicas y la recomendación de productos o suplementos. La salida incluye un aviso de contenido generado por IA, además del aviso visible en pantalla.

Presentación

Tarjetas de resumen, gráfico de barras flotantes con la ventana de sueño de cada noche (despertares precoces resaltados y línea de hora habitual de despertar) y tabla noche a noche. El análisis describe el patrón con datos del reloj y no constituye un diagnóstico.

8.38. Evolución del Peso: línea de fondo, ritmo y rango normal autorreferenciados
Objeto

La pantalla Evolución del Peso (WeightTrendService, WeightTrendAnalyzer, WeightTrendTextos, acción InformesController.EvolucionPeso) separa el peso de fondo de las oscilaciones diarias y lo presenta en lenguaje llano a cualquier usuario. Se calcula al vuelo sobre BiodataDiaria, sin tablas propias y sin modificar el dato de origen. La parte de cálculo es pura (sin acceso a base de datos) y la de lectura es de solo lectura.

Datos admitidos

Ventana de los últimos 180 días. Solo cuentan las pesadas con hora de medición (PesoMedidoEnUtc), porque algunos caminos de importación arrastran el último peso conocido a los días sin pesada. Si el usuario tiene menos de 14 pesadas con hora, se aceptan las que cambian respecto al día anterior y se muestra un aviso. Con menos de 14 pesadas la pantalla indica que faltan datos y no muestra cifras ni gráfico.

Modelo

Modelo de estado lineal-gaussiano con tres componentes: nivel (peso de fondo), pendiente (que puede variar muy despacio, con un suelo de 0,002 kg/día²) y oscilación persistente (autorregresiva de orden 1: un día alto se arrastra unos días y vuelve), más el error de la báscula fijado en 0,10 kg. Se estima con un filtro de Kalman (estimador recursivo que actualiza la estimación con cada pesada) y un suavizado de Rauch-Tung-Striebel (segunda pasada hacia atrás que afina el pasado con lo que se sabe después). Tres parámetros por usuario (tamaño y persistencia de la oscilación y cuánto puede cambiar la pendiente) se eligen por máxima verosimilitud (los valores que mejor explican las pesadas del propio usuario) con una búsqueda de Nelder–Mead propia, sin dependencias externas. Los días sin pesada se saltan. Una pesada a más de 3,5 desviaciones esperadas de lo previsto se marca como inusual y no mueve la línea de fondo.

Resultado y criterios de estado

Peso de fondo, ritmo semanal con su intervalo del 90 % y rango en que cae el peso de hoy el 90 % de los días. El estado es Bajando cuando la probabilidad de que la pendiente sea negativa alcanza el 90 %, Subiendo cuando es del 10 % o menos, Estable cuando todo el intervalo del ritmo cabe en ±0,15 kg por semana con al menos tres semanas de datos, y sin conclusión en los demás casos. La probabilidad no se muestra al usuario. La posición de hoy (dentro de lo normal, algo alto, algo bajo o inusual) se obtiene comparando la pesada con el rango del día. Si el ritmo reciente cae claramente por debajo del medio, se informa de una desaceleración y de cuántas semanas más de datos hacen falta para concluir. La pantalla no ofrece cifras de proyección.

Pesadas esporádicas

Si hay menos de 14 días con pesada en 60 pero al menos 8 pesadas en 180 días repartidas en un tramo de 6 semanas o más, el modelo de estado no se aplica (exige pesadas casi diarias) y se ajusta una regresión lineal simple (una recta peso-fecha, una pesada por día). Se informa del cambio total en el tramo y del ritmo medio por semana con su intervalo del 90 % (t de Student). El cambio se considera claro solo si el intervalo no incluye el cero; si no, se indica que no se ve un cambio claro. En este modo no se muestran estado, rango normal de hoy ni la banda del gráfico, y se omite el aviso de «hace una semana sin pesada».

Mensajes de adherencia

La pantalla fomenta la pesada diaria matutina sin presión: los textos explican para qué sirve (el peso varía cada día por agua, sal y digestión, así que una pesada suelta engaña y muchas muestran el rumbo; las propias pesadas permiten conocer cuánto varía el peso de cada persona y detectar lecturas raras; a la misma hora y en las mismas condiciones las pesadas son comparables) y evitan reproches por los días sin pesada. Un bloque fijo, «¿Por qué pesarse cada mañana?», resume esas razones e incluye la asociación general entre pesarse con regularidad y mejores resultados en los programas de control de peso.

Punto de partida y cintura

El usuario puede elegir una fecha de partida; el peso se toma siempre de BiodataDiaria: el de ese día, si no el del día anterior, si no el del posterior, y si ninguno existe la fecha no se admite (nunca se crea ni se escribe un registro). La pantalla indica qué día se usó y si el dato procede de una edición manual. Sin fecha elegida se toma la primera pesada con hora del usuario. La fecha elegida se conserva únicamente en la sesión. La ventana de cálculo es de 180 días o, si el punto de partida queda más atrás, desde ese día, con un máximo de 730 días diarios. Si la historia desde el inicio supera ese máximo, lo anterior no entra en el modelo (el estado actual apenas depende de ello) y se resume en medias semanales (semanas que empiezan en lunes, solo con pesadas con hora de medición) únicamente para el gráfico; en ese caso la vista «desde el inicio» dibuja todo en medias semanales, con una nota, y los periodos de 30 y 90 días siguen siendo diarios. La bajada de cintura se muestra solo cuando hay al menos dos medidas reales (CinturaMedidaEnUtc) separadas 28 días o más.

Presentación

Etiqueta de estado, titular, progreso desde el punto de partida, situación de hoy, avisos y un bloque «Lo normal» con las oscilaciones esperables, junto a un gráfico de las pesadas, la línea de fondo y la banda de lo normal con periodos de 30 días, 90 días o todo. La pantalla se ajusta a la altura de la ventana y apila las dos columnas en pantallas estrechas.

8.39. Copias de seguridad de la ingesta y del diccionario de alimentos: copia comprimida, aviso de cambios y restauración con calibración de origen
Qué se protege y por qué

La ingesta es un dato manual que no puede releerse de ninguna exportación del reloj. Se copian el diccionario de alimentos del usuario (IntakeFoodLibraryItem) y, de cada día con ingesta, las columnas de BiodataDiaria HidratosIngeridosGr, GrasasIngeridasGr, ProteinasIngeridasGr, AlcoholIngeridoGr, SodioMg y AlimentosDiaJson. No se copian el resto de la fila de Biodata, que se recupera con sus propias copias e importaciones, ni EnergyDailyBalance, que se recalcula, ni los menús semanales, que se regeneran.

Formato y almacenamiento

Cada copia es un fichero JSON comprimido con gzip (intake_AAAAMMDD_HHMMSSfff_tipo.json.gz) en AppData/Backups/intake/{usuarioId}/, fuera de wwwroot y por tanto sin URL propia; la ruta base se puede cambiar con Backups:Directory. Lleva una cabecera (versión, tipo, fecha, huella SHA-256 del contenido, recuentos y factores de calibración del momento), el diccionario y los días; cada día incluye los tres factores con los que se calculó su EnergyDailyBalance. La huella excluye los factores, de modo que una recalibración sin cambios de ingesta no cuenta como cambio. La escritura es atómica (fichero temporal y renombrado) y el identificador de copia se valida con una expresión regular: el nombre de fichero nunca lo decide el cliente. Las copias en JSON sin comprimir anteriores a la compresión se siguen leyendo; al descargar, el usuario recibe siempre el JSON legible (se descomprime al vuelo).

Copia automática y retención

IntakeBackupWorker crea cada noche a las 03:30 una copia «diaria» por usuario con datos, a día vencido: recoge también los cambios retroactivos hechos hasta ese momento. Se omite si la huella coincide con la de la última copia. Cada comprobación, haya o no copia nueva, deja una marca por usuario (estado.json: fecha, resultado «creada», «sinCambios» o «error») que la pantalla muestra, para distinguir un «no había cambios» de un «no se ejecutó». Al arrancar la aplicación, pasados dos minutos, se repasa a quien no haya sido comprobado desde la última hora programada (o falló la última vez), lo que cubre un sitio inactivo a las 03:30 y una carpeta borrada. La retención conserva 30 días y, de los meses anteriores, la última de cada mes; las copias manuales no caducan. No se puede eliminar la copia más reciente ni las «previas» a una restauración.

Aviso de cambios y pantalla de gestión

La pantalla Alimentación → Copias de Seguridad compara en vivo la base de datos con la última copia y enumera los alimentos y días nuevos, modificados o desaparecidos: avisa de lo que aún no está protegido hasta la copia de esa noche. Permite crear una copia manual, ver el detalle, descargar, restaurar y eliminar, y lleva una ayuda detallada en un modal. Cada usuario ve y restaura solo lo suyo (siempre con el identificador del usuario autenticado, nunca el del paciente activo de un profesional).

Restauración como imagen fiel

Antes de aplicar nada se muestra una simulación exacta (la misma operación, deshecha al final) y se guarda automáticamente una copia «previa» del estado actual. La restauración es transaccional, admite un periodo (Desde/Hasta), restaurar o no el diccionario (que no tiene fecha) y una opción de «imagen exacta» que vacía lo que no esté en la copia dentro del periodo. Solo escribe las columnas de ingesta de filas de Biodata que ya existen: los días sin fila se omiten y se listan, para no crear filas a medias. Sin «imagen exacta» la restauración nunca degrada: un valor vacío de la copia (macros a 0, sodio nulo o AlimentosDiaJson vacío) no sustituye a un dato que hoy existe, porque el desglose de platos solo vive en esa columna y una copia anterior a su relleno lo borraría. Solo «imagen exacta» vacía datos, y la simulación informa de cuántos días han conservado su dato actual.

Persistencia de la energía al restaurar

Tras confirmar, el balance energético de los días afectados se recalcula con los factores de origen guardados en la copia, no con los vigentes: RecalculateAndSaveAsync admite un parámetro opcional EnergyFactorOverride que sustituye a la calibración actual del usuario. Así el histórico restaurado no cambia aunque la calibración haya evolucionado (ver 8.35). Los días que no traen factores propios usan los de la cabecera.

Supresión y despliegue

Al ejecutar una baja de usuario (UserDeleteService), una vez confirmado el borrado en la base de datos, se elimina la carpeta de copias del usuario. El perfil de publicación excluye AppData del paquete y protege AppData\Backups del borrado en el servidor.

8.40. Dieta y Evolución: grupos de alimentos frente a la evolución posterior de un parámetro, con retardo y sin tendencia temporal
Qué es y qué no es

Pantalla de Alimentación (DietaEvolucionController, DietaEvolucionService) que pone en relación lo que se come, por grupos de alimentos, con la evolución posterior de un parámetro elegido en un desplegable. Es un análisis exploratorio de solo lectura: se calcula al abrirla, no guarda nada, y muestra coincidencias en el tiempo, no causas. Con pocas semanas de datos casi ninguna asociación es concluyente, y la propia pantalla lo dice y marca como «no evaluable» lo que no puede analizarse.

Datos de entrada y diccionario común de grupos

Usa el desglose de platos de cada día (BiodataDiaria.AlimentosDiaJson, disponible desde el 18/05/2026). Cada plato se asigna a un grupo mediante un diccionario global (AlimentoGrupo) que relaciona el nombre normalizado del alimento (FoodNameNormalizer) con un alimento canónico y un grupo; no contiene datos personales, solo nombres de alimentos. Se siembra desde un fichero revisado a mano embebido en la aplicación (AlimentoGrupoSeeder, que solo añade los nombres que faltan y nunca pisa lo ya existente) y define 22 grupos (lácteos, carnes, embutidos, pescado azul, pescado blanco y marisco, alcohol por tipo, etc.). Los nombres que no están en el diccionario quedan como «Sin clasificar», no entran en la tabla y, si superan el 5 % de los gramos del periodo, se avisa con los principales. Café, bebidas sin calorías y zumos se excluyen del análisis y del gráfico por no tener interés nutricional (casi todo agua). El diccionario se mantiene desde Configuración → Catálogos y diccionarios → «Diccionario de grupos de alimentos» (AlimentoGrupoAdminController, solo administradores): listado editable por grupo y origen, alta de nombres, renombrado de un grupo entero y una lista agregada de los nombres sin clasificar de todos los usuarios (solo totales y número de usuarios distintos, sin identificar a nadie) para asignarles grupo. Lo que se guarda a mano queda como «Manual» y revisado, y el arranque no lo pisa.

Exposición, parámetros y retardo

Se agrega por semanas de lunes a domingo; una semana solo cuenta si tiene al menos 4 días con desglose. La exposición es la media por día, a elegir, de los gramos de cada grupo o de su aporte a un macro concreto (hidratos, proteínas, grasas, alcohol o sodio). Los parámetros son peso (solo pesadas con hora real), grasa corporal por impedancia, HRV, FC en reposo, sueño total y puntuación de recuperación, cada uno como media semanal de sus días válidos (mínimo 2, 1, 4, 4, 4 y 4 días respectivamente). El retardo es de 1 a 4 semanas (mínimo una semana): se contrasta la exposición de la semana t con el cambio del parámetro entre t y t+L.

Estadística

Para cada grupo se calcula la correlación de rangos de Spearman entre la exposición y el cambio del parámetro, como correlación parcial que descuenta la tendencia del tiempo (número de semana): si el grupo y el parámetro solo coinciden porque ambos evolucionan con las semanas, no cuenta como asociación. Se muestran además la correlación bruta (sin descontar) y la tendencia del propio grupo. El intervalo del 95 % y el valor p son aproximados (transformación de Fisher, error típico √(1,06/(n−4))) y los p se corrigen por comparaciones múltiples con Benjamini-Hochberg (q). Un grupo solo es evaluable con al menos 10 parejas de semanas, presencia en 6 semanas o más y 5 valores distintos. Lectura: «Pista sólida» si q < 0,10; «Coincidencia a seguir» si |ρ| ≥ 0,3; «Era tendencia temporal» si la bruta era ≥ 0,3 y la ajustada no; en otro caso, «Sin asociación clara». No se ajusta por las kcal totales ni por el alcohol, y la media semanal de HRV mezcla fuentes (SDNN y RMSSD).

Presentación

Tres pestañas a la altura de la ventana: Gráfico (barras apiladas con los ocho grupos de mayor exposición y «Otros», y el parámetro como línea sobre un segundo eje; las semanas sin dato del parámetro entre dos semanas con dato se sombrean en gris y cortan la línea), Tabla (asociaciones, con scroll vertical y cabecera fija y un botón «Cómo leer esta tabla» que abre un glosario en ventana modal con la explicación en lenguaje llano de ρ, ρ bruta, tendencia, intervalo de confianza, q y las cinco lecturas) e Interpretación IA. Un botón «Copiar datos JSON», común a todos los parámetros, copia los datos que alimentan el gráfico y la tabla, incluida la lectura de cada grupo.

Interpretación IA

Bajo demanda y sin caché, el botón de la pestaña repite en el servidor el cálculo con los ajustes elegidos en pantalla (DietaEvolucionController.GenerarInterpretacion) y construye un bloque de datos con cifras ya calculadas (DietaEvolucionDataBlock): contexto del análisis, calidad del dato, la lectura de cada grupo —la misma etiqueta que muestra la tabla, con una única fuente en AsociacionGrupo.Lectura— y las limitaciones del análisis, sin las series semanales. Se envía a una plantilla versionada gestionada por DietaEvolucionPromptSeeder (DIETA_EVOLUCION_ANALISIS), editable desde el mantenimiento de prompts. La tarea se ejecuta en segundo plano y la pantalla sondea el resultado. La audiencia es el paciente (segunda persona, sin siglas) o el profesional (tercera persona, con ρ y q explicadas). El modelo recibe la instrucción de respetar las lecturas de la tabla, comentar como máximo tres grupos con «Pista sólida» o «Coincidencia a seguir», no atribuir causas ni explicar mecanismos, no recomendar cambios de alimentos y recordar que probar varias combinaciones de parámetro, retardo y periodo aumenta la probabilidad de coincidencias por azar. Si ningún grupo es evaluable no se llama a la IA. El resultado se presenta siempre con el aviso de contenido generado por IA, que el servidor añade si el modelo lo omite.

9. Notas sobre el método heurístico de ensayo y ajuste progresivo

El término heurístico se emplea para designar un enfoque de decisión apoyado en reglas prácticas, comparación temporal y ajuste progresivo, sin pretensión de sustituir el juicio clínico.

El sistema observa resultados, detecta desviaciones, compara tendencias y reajusta recomendaciones en función del comportamiento real de las variables biométricas y clínicas.

12. Estado de protección y derechos de autor

Estado registral: Obra software inscrita en el Registro Territorial de la Propiedad Intelectual de Madrid, asiento registral 16/2026/8798, fecha de efecto 07/09/2026.

Derechos de autor:
© 2026 Ricardo M. Trigo Calonge.
Todos los derechos reservados.

Queda prohibida la reproducción, distribución, comunicación pública o transformación, total o parcial, del código fuente, modelos estructurales, arquitectura software, algoritmos, lógica funcional y documentación técnica asociada, sin autorización expresa del titular.

↑ Índice