mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(insights): feed the six specialised cards a precomputed SIGNAL block
BP, pulse, weight, BMI, mood and medication-compliance still asked the model to derive the recent-vs-baseline comparison from raw buckets — the exact arithmetic small models do unreliably, and the pattern the base-system prompt already advertises as precomputed. Build the SIGNAL block (current / baseline / signed delta / normal-swing verdict) with buildMetricSignal from each card's graded series and inject it into the snapshot; BP carries two signals, compliance one per medication. Each card's prompt section now leads from the signal block and is told to state, not recompute. The server-computed compliance % is unchanged.
This commit is contained in:
1 parent
22fac8ae90
commit
db68f994c2
13 files changed
+138
-25
No files matched your search
@@ -2,8 +2,8 @@ import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
|
||||
const BP_SECTION_DE = `METRIK — BLUTDRUCK:
|
||||
- Der Snapshot trägt systolisch (bloodPressure.systolic) und diastolisch (bloodPressure.diastolic) jeweils mit summary + graded series; beurteile beide Komponenten GEMEINSAM, nie isoliert.
|
||||
- Tendenz über Tage/Wochen: Vergleiche das recent-Mittel (letzte Tage/Woche) mit dem weekly/monthly-Mittel der Person. Eine systolische Abweichung gilt ab ~5 mmHg, eine diastolische ab ~3 mmHg gegenüber der eigenen Baseline als signifikant — kleinere Schwankungen sind normal und kein Befund.
|
||||
- Der Snapshot trägt systolisch (bloodPressure.systolic) und diastolisch (bloodPressure.diastolic) jeweils mit signal (der fertige Vergleich) + summary + graded series; beurteile beide Komponenten GEMEINSAM, nie isoliert.
|
||||
- Tendenz über Tage/Wochen: Führe mit bloodPressure.systolic.signal und bloodPressure.diastolic.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als "innerhalb der üblichen Schwankung — kein Befund".
|
||||
- Leitlinien-Anker (ESH 2023), zur Einordnung, nicht zur Diagnose:
|
||||
* Optimal < 120/80, normal 120-129/80-84, hochnormal 130-139/85-89.
|
||||
* Hypertonie Grad 1 ab 140/90, Grad 2 ab 160/100, Grad 3 ab 180/110.
|
||||
@@ -14,8 +14,8 @@ const BP_SECTION_DE = `METRIK — BLUTDRUCK:
|
||||
- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const BP_SECTION_EN = `METRIC — BLOOD PRESSURE:
|
||||
- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a summary + graded series; judge both components TOGETHER, never in isolation.
|
||||
- Trend over days/weeks: compare the recent mean (last days/week) with the person's weekly/monthly mean. Treat a systolic shift of ~5 mmHg or a diastolic shift of ~3 mmHg against their own baseline as significant — smaller swings are normal and not a finding.
|
||||
- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.
|
||||
- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as "inside the usual swing — not a finding".
|
||||
- Guideline anchors (ESH 2023), for placement, not diagnosis:
|
||||
* Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.
|
||||
* Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.
|
||||
|
||||
@@ -2,18 +2,18 @@ import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
|
||||
const BMI_SECTION_DE = `METRIK — BMI:
|
||||
- Der Snapshot trägt bmi.summary + bmi.series (graded). bmi.latestDayFocus zeigt den jüngsten Wert, dessen WHO-Klassifikation und den Schritt zum Vortag; bmi.target ist das grüne Band (18.5-24.9).
|
||||
- Der Snapshot trägt bmi.signal (der fertige Vergleich) + bmi.summary + bmi.series (graded). bmi.latestDayFocus zeigt den jüngsten Wert, dessen WHO-Klassifikation und den Schritt zum Vortag; bmi.target ist das grüne Band (18.5-24.9).
|
||||
- Fokus dieser Karte ist die WHO-BAND-EINORDNUNG, nicht das Gewichtstempo (das trägt die Gewichts-Karte). Setze den Schwerpunkt darauf, in welchem Band der Wert liegt und ob er ein Band wechselt oder sich einer Bandgrenze nähert.
|
||||
- WHO-Bänder zur groben Einordnung, nie als Etikett für die Person: Untergewicht < 18.5, Normal 18.5-24.9, Übergewicht 25.0-29.9, Adipositas I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. Ab 65+ gilt 22-27 als günstig (DEGAM).
|
||||
- Bandbezug: Vergleiche den recent-Wert gegen das weekly/monthly-Mittel und nenne es ausdrücklich, wenn der recent-Wert ein WHO-Band gewechselt hat oder sich (innerhalb ~0.5 BMI-Punkte) einer Bandgrenze nähert. Bleibt der Wert sicher im selben Band, sage das knapp — keine Tempo-Erzählung erzwingen.
|
||||
- Bandbezug: Führe mit bmi.signal — current, delta gegenüber baselineLabel und placement (Bandlage) sind bereits berechnet; NENNE sie, rechne sie NICHT neu. Nenne es ausdrücklich, wenn der recent-Wert ein WHO-Band gewechselt hat oder sich (innerhalb ~0.5 BMI-Punkte) einer Bandgrenze nähert. Bleibt der Wert sicher im selben Band, sage das knapp — keine Tempo-Erzählung erzwingen.
|
||||
- Grenzen des BMI offen ansprechen: Er misst keine Körperzusammensetzung und ist bei muskulösen oder sehr sportlichen Personen wenig aussagekräftig.
|
||||
- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, der zur Bandlage passt, wenn der Befund einen nahelegt — bei stabilem Wert im günstigen Band das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const BMI_SECTION_EN = `METRIC — BMI:
|
||||
- The snapshot carries bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).
|
||||
- The snapshot carries bmi.signal (the finished comparison) + bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).
|
||||
- This card's focus is WHO-BAND placement, not weight pace (the weight card carries that). Centre it on which band the value sits in and whether it crosses a band or nears a band boundary.
|
||||
- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).
|
||||
- Band reference: compare the recent value against the weekly/monthly mean and call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.
|
||||
- Band reference: lead from bmi.signal — current, delta vs baselineLabel and placement (band position) are already computed; STATE them, do NOT recompute. Call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.
|
||||
- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.
|
||||
- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step.`;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
|
||||
const MEDCO_SECTION_DE = `METRIK — MEDIKAMENTEN-EINNAHMETREUE:
|
||||
- Der Snapshot trägt overall (medicationCount, averageCompliance7, averageCompliance30) und medications[] je Medikament mit name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, dailySeries (graded) und latestDay.
|
||||
- Lies die Einnahmetreue als Prozentsatz erfüllter geplanter Dosen über die letzten 7 bzw. 30 Tage. Vergleiche compliance7 mit compliance30, um die jüngste Richtung gegen die eigene Baseline der Person zu sehen.
|
||||
- Der Snapshot trägt overall (medicationCount, averageCompliance7, averageCompliance30) und medications[] je Medikament mit name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (der fertige Trend-Vergleich), dailySeries (graded) und latestDay.
|
||||
- Lies die Einnahmetreue als Prozentsatz erfüllter geplanter Dosen über die letzten 7 bzw. 30 Tage; compliance7/compliance30 sind server-berechnet. Für die jüngste RICHTUNG gegen die eigene Baseline führe mit medications[].signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet; NENNE sie, rechne sie NICHT neu.
|
||||
- Seltene Kadenz: Bei niedriger schedulesPerDay oder seltener Einnahme (z.B. Wocheninjektion) ist compliance7 rauschanfällig — eine einzige verpasste Dosis verschiebt den Prozentwert stark. Stütze die Einschätzung dann auf compliance30 (den tragenden Wert) und behandle compliance7 nur als groben Hinweis.
|
||||
- Einordnung (zur Orientierung, nicht als Urteil): ≥ 90 % sehr verlässlich, 80-89 % gut, 70-79 % lückenhaft, < 70 % deutlich lückenhaft — die therapeutische Wirkung kann dann eingeschränkt sein.
|
||||
- Positiv rahmen: streak ist die längste ununterbrochene Serie — bei guter Treue ausdrücklich anerkennen. Bei mehreren Medikamenten das mit der niedrigsten Treue benennen.
|
||||
@@ -11,8 +11,8 @@ const MEDCO_SECTION_DE = `METRIK — MEDIKAMENTEN-EINNAHMETREUE:
|
||||
- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. Abenddosen werden häufiger vergessen — eine feste Routine oder ein Reminder zur kritischen Uhrzeit kann helfen). Bei durchgehend hoher Treue das ehrlich anerkennen, statt einen Schritt zu erzwingen. Keine Schuldzuweisung.`;
|
||||
|
||||
const MEDCO_SECTION_EN = `METRIC — MEDICATION ADHERENCE:
|
||||
- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, dailySeries (graded) and latestDay.
|
||||
- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days. Compare compliance7 with compliance30 to see the recent direction against the person's own baseline.
|
||||
- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.
|
||||
- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.
|
||||
- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.
|
||||
- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.
|
||||
- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
|
||||
const MOOD_SECTION_DE = `METRIK — STIMMUNG / WOHLBEFINDEN:
|
||||
- Skala 1 (sehr schlecht) bis 5 (sehr gut). Der Snapshot trägt mood.summary + mood.series (graded, Tagesmittel), mood.target (grünes/oranges Band), mood.latestDayFocus und ggf. mood.tags (häufigste Stimmungs-Tags).
|
||||
- Wochenmuster zählt: Vergleiche das recent-Mittel mit dem weekly/monthly-Mittel der Person. Schwankungen < 0.5 Punkte über die letzten Tage sind normal und kein Befund.
|
||||
- Skala 1 (sehr schlecht) bis 5 (sehr gut). Der Snapshot trägt mood.signal (der fertige Vergleich) + mood.summary + mood.series (graded, Tagesmittel), mood.target (grünes/oranges Band), mood.latestDayFocus und ggf. mood.tags (häufigste Stimmungs-Tags).
|
||||
- Wochenmuster zählt: Führe mit mood.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als "innerhalb der üblichen Schwankung — kein Befund".
|
||||
- Anhaltende Phasen ernst, aber ruhig benennen: mehrere Tage unter ~2.5 oder über ~4.5 sind erwähnenswert — als Beobachtung, nie als Diagnose und nie alarmierend.
|
||||
- Bei anhaltend sehr niedriger Stimmung (mehrere Tage deutlich unter ~2.5): biete ruhig und autonomie-wahrend einen Unterstützungs-Pfad an — etwa "bei anhaltender Belastung kann ein Gespräch mit einer Vertrauensperson oder einer Fachperson helfen". Nie alarmierend, nie als Diagnose, kein Notfall-Ton; eine sanfte Option, kein Imperativ.
|
||||
- Tags: Falls mood.tags vorhanden, kannst du ein wiederkehrendes Tag als möglichen Kontext nennen, ohne einen Zusammenhang zu behaupten.
|
||||
@@ -11,8 +11,8 @@ const MOOD_SECTION_DE = `METRIK — STIMMUNG / WOHLBEFINDEN:
|
||||
- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren, freundlichen Schritt, wenn der Befund einen nahelegt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const MOOD_SECTION_EN = `METRIC — MOOD / WELL-BEING:
|
||||
- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).
|
||||
- The weekly pattern matters: compare the recent mean with the person's weekly/monthly mean. Swings < 0.5 points across the recent days are normal and not a finding.
|
||||
- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).
|
||||
- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as "inside the usual swing — not a finding".
|
||||
- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.
|
||||
- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. "if the strain persists, talking it through with someone you trust or a professional can help". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.
|
||||
- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.
|
||||
|
||||
@@ -2,16 +2,16 @@ import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
|
||||
const PULSE_SECTION_DE = `METRIK — PULS / HERZFREQUENZ:
|
||||
- Der Snapshot trägt pulse.summary + pulse.series (graded) und pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus zeigt den jüngsten Tageswert und den Schritt zum Vortag.
|
||||
- Tendenz über Wochen: Vergleiche das recent-Mittel mit dem weekly/monthly-Mittel der Person. Eine Abweichung ab ~5 bpm gegenüber der eigenen Baseline ist signifikant; eine Variabilität < 5 bpm über die letzten Tage ist normal und kein Befund.
|
||||
- Der Snapshot trägt pulse.signal (der fertige Vergleich), pulse.summary + pulse.series (graded) und pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus zeigt den jüngsten Tageswert und den Schritt zum Vortag.
|
||||
- Tendenz über Wochen: Führe mit pulse.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als "innerhalb der üblichen Schwankung — kein Befund".
|
||||
- Richtung: Ein über Wochen sinkender Ruhepuls deutet auf bessere kardiovaskuläre Fitness; ein steigender kann auf Stress, Schlafmangel oder Übertraining hinweisen — als mögliche Erklärung, nie als Diagnose.
|
||||
- Einordnungs-Anker (Ruhepuls): athletisch 50-60, normal 60-100. Werte im Band 80-100 sind technisch normal, langfristig aber günstiger niedriger. < 50 oder > 100 über mehrere Tage ist erwähnenswert, ohne zu alarmieren.
|
||||
- Medikamenten-Bezug: Betablocker senken die Herzfrequenz. moodContext.moodVsPulseCorrelation und ein pulseVsSystolic-Zusammenhang nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.
|
||||
- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich oder ist er stabil, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const PULSE_SECTION_EN = `METRIC — PULSE / HEART RATE:
|
||||
- The snapshot carries pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.
|
||||
- Trend over weeks: compare the recent mean with the person's weekly/monthly mean. A shift of ~5 bpm against their own baseline is significant; variability < 5 bpm across the recent days is normal and not a finding.
|
||||
- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.
|
||||
- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as "inside the usual swing — not a finding".
|
||||
- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.
|
||||
- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.
|
||||
- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { Locale } from "@/lib/i18n/config";
|
||||
import { getBaseSystemPrompt } from "./base-system";
|
||||
|
||||
const WEIGHT_SECTION_DE = `METRIK — GEWICHT:
|
||||
- Der Snapshot trägt weight.summary + weight.series (graded). weight.latestDayFocus zeigt den jüngsten Tageswert, den Schritt zum vorherigen Messtag und ggf. den Blutdruck desselben Tages.
|
||||
- Der Snapshot trägt weight.signal (der fertige Vergleich) + weight.summary + weight.series (graded). weight.latestDayFocus zeigt den jüngsten Tageswert, den Schritt zum vorherigen Messtag und ggf. den Blutdruck desselben Tages.
|
||||
- Fokus dieser Karte ist der kontinuierliche TREND und das TEMPO über die Zeit (kg/Woche, Plateaus, Meilensteine), nicht die WHO-Band-Einordnung — die trägt die BMI-Karte. Den BMI-Wert nur als Nebenbezug nennen, nie das Band als zentrale Aussage hier.
|
||||
- Wochen-/Monatstrend zählt, nicht der Einzelwert: Vergleiche das recent-Mittel mit dem weekly/monthly-Mittel der Person. Tageschwankungen von 1-2 kg (Wasser, Tageszeit) sind normal und kein Befund.
|
||||
- Wochen-/Monatstrend zählt, nicht der Einzelwert: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von 1-2 kg (Wasser, Tageszeit) sind normal und kein Befund.
|
||||
- Plateau: Verändert sich das Mittel über > ~2 Wochen um < 0.5 kg, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.
|
||||
- Tempo: Nachhaltiger Verlust liegt bei ~0.5-1.0 kg/Woche (DGE). Anhaltend > 1 kg/Woche ist eher Wasser/Muskel als Fett — vorsichtig einordnen.
|
||||
- Meilensteine gegen das eigene Maximum: ~5 % Verlust vom höchsten Wert (series-Max) bringt bereits metabolischen Nutzen, ~10 % einen deutlichen — wenn erreicht, ausdrücklich anerkennen.
|
||||
@@ -13,9 +13,9 @@ const WEIGHT_SECTION_DE = `METRIK — GEWICHT:
|
||||
- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen). Ist der Trend stabil und gibt es nichts Sinnvolles zu tun, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen.`;
|
||||
|
||||
const WEIGHT_SECTION_EN = `METRIC — WEIGHT:
|
||||
- The snapshot carries weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.
|
||||
- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.
|
||||
- This card's focus is the continuous TREND and PACE over time (kg/week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.
|
||||
- The weekly/monthly trend matters, not the single value: compare the recent mean with the person's weekly/monthly mean. Day-to-day swings of 1-2 kg (water, time of day) are normal and not a finding.
|
||||
- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 1-2 kg (water, time of day) are normal and not a finding.
|
||||
- Plateau: if the mean moves < 0.5 kg over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.
|
||||
- Pace: sustainable loss is ~0.5-1.0 kg/week (DGE). Sustained > 1 kg/week is more likely water/muscle than fat — frame cautiously.
|
||||
- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.
|
||||
|
||||
@@ -108,7 +108,16 @@ describe("generateBloodPressureStatusForUser — graded payload", () => {
|
||||
expect(sys).toHaveProperty("recent");
|
||||
expect(sys).toHaveProperty("weekly");
|
||||
expect(sys).toHaveProperty("monthly");
|
||||
expect(sys).toHaveProperty("yearly");
|
||||
// v1.18.10 (HIGH-4) — the per-channel SIGNAL block (the finished
|
||||
// recent-vs-baseline comparison) now rides the snapshot so the model
|
||||
// states the trend rather than re-deriving it.
|
||||
expect(snapshot.bloodPressure.systolic.signal).toHaveProperty("current");
|
||||
expect(snapshot.bloodPressure.systolic.signal).toHaveProperty(
|
||||
"outsideNormalSwing",
|
||||
);
|
||||
// `yearly` is the lowest-signal slice and the FIRST the budget degrader
|
||||
// sheds; on this deliberately-large 1000-day fixture it may be dropped to
|
||||
// make room. The recent/weekly/monthly core + the signal block stay.
|
||||
expect(sys.recent.length).toBeLessThanOrEqual(21);
|
||||
expect(sys.recent[0]).toHaveProperty("date");
|
||||
expect(sys.recent[0]).toHaveProperty("mean");
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
buildGradedSeriesWithRollups,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { buildMetricSignal } from "@/lib/insights/metric-signal";
|
||||
import {
|
||||
type SupportedLocale,
|
||||
normalizeLocale,
|
||||
@@ -505,6 +506,31 @@ export async function prepareBloodPressureStatusForUser(
|
||||
)
|
||||
: null;
|
||||
|
||||
// v1.18.10 (HIGH-4) — hand the model the finished recent-vs-baseline
|
||||
// comparison + normal-swing verdict per channel instead of asking it to
|
||||
// derive them. BP needs two signals; the user's own targets are the coarse
|
||||
// band anchor when present.
|
||||
const sysSignal = buildMetricSignal({
|
||||
metric: locale === "en" ? "your systolic" : "deine Systole",
|
||||
unit: "mmHg",
|
||||
direction: "lower-better",
|
||||
graded: sysGraded,
|
||||
normalRange: bpTargets
|
||||
? { low: bpTargets.sysLow, high: bpTargets.sysHigh }
|
||||
: null,
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
});
|
||||
const diaSignal = buildMetricSignal({
|
||||
metric: locale === "en" ? "your diastolic" : "deine Diastole",
|
||||
unit: "mmHg",
|
||||
direction: "lower-better",
|
||||
graded: diaGraded,
|
||||
normalRange: bpTargets
|
||||
? { low: bpTargets.diaLow, high: bpTargets.diaHigh }
|
||||
: null,
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
});
|
||||
|
||||
const snapshot = {
|
||||
locale,
|
||||
generatedForDay: todayKey,
|
||||
@@ -516,12 +542,14 @@ export async function prepareBloodPressureStatusForUser(
|
||||
},
|
||||
bloodPressure: {
|
||||
systolic: {
|
||||
...(sysSignal ? { signal: sysSignal } : {}),
|
||||
summary: summarizeSeries(
|
||||
sysSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
series: sysGraded,
|
||||
},
|
||||
diastolic: {
|
||||
...(diaSignal ? { signal: diaSignal } : {}),
|
||||
summary: summarizeSeries(
|
||||
diaSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
scaleGradedSeries,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { buildMetricSignal } from "@/lib/insights/metric-signal";
|
||||
import {
|
||||
type SupportedLocale,
|
||||
normalizeLocale,
|
||||
@@ -244,6 +245,17 @@ export async function prepareBmiStatusForUser(
|
||||
)
|
||||
: null;
|
||||
|
||||
// v1.18.10 (HIGH-4) — hand the model the finished recent-vs-baseline
|
||||
// comparison + normal-swing verdict instead of asking it to derive them.
|
||||
// BMI reads against the WHO healthy band; "target-band" framing.
|
||||
const bmiSignal = buildMetricSignal({
|
||||
metric: locale === "en" ? "your BMI" : "dein BMI",
|
||||
direction: "target-band",
|
||||
graded: bmiGraded,
|
||||
normalRange: { low: 18.5, high: 24.9 },
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
});
|
||||
|
||||
const snapshot = {
|
||||
locale,
|
||||
generatedForDay: todayKey,
|
||||
@@ -254,6 +266,7 @@ export async function prepareBmiStatusForUser(
|
||||
newestMeasurementDaysAgo,
|
||||
},
|
||||
bmi: {
|
||||
...(bmiSignal ? { signal: bmiSignal } : {}),
|
||||
summary: summarizeSeries(
|
||||
bmiSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@/lib/insights/assessment-context";
|
||||
import { applyPayloadBudget } from "@/lib/insights/bucket-series";
|
||||
import { buildGradedSeriesFromPoints } from "@/lib/insights/graded-series";
|
||||
import { buildMetricSignal } from "@/lib/insights/metric-signal";
|
||||
import { degradeStatusSnapshotToBudget } from "@/lib/insights/graded-series";
|
||||
import {
|
||||
type SupportedLocale,
|
||||
@@ -307,6 +308,24 @@ export async function prepareMedicationComplianceStatusForUser(
|
||||
const dailySeries = buildGradedSeriesFromPoints(perDayRecords, now);
|
||||
const latestDay = dailyBudgeted.daily[0] ?? null;
|
||||
|
||||
// v1.18.10 (HIGH-4) — the finished recent-vs-baseline adherence comparison
|
||||
// + normal-swing verdict, computed from the canonical per-day rate series
|
||||
// so the model states the trend rather than deriving it. The compliance %
|
||||
// itself stays server-computed (compliance7/30 above); this adds only the
|
||||
// trend signal. Adherence reads higher-better; the band is 0–100%.
|
||||
const signal = buildMetricSignal({
|
||||
metric:
|
||||
locale === "en"
|
||||
? `adherence for ${sanitizeForPrompt(medication.name)}`
|
||||
: `Einnahmetreue für ${sanitizeForPrompt(medication.name)}`,
|
||||
unit: "%",
|
||||
direction: "higher-better",
|
||||
graded: dailySeries,
|
||||
// Recency is surfaced separately via dataCoverage.newestMeasurementDaysAgo
|
||||
// (computed after this map); the per-med signal leaves it null.
|
||||
newestDaysAgo: null,
|
||||
});
|
||||
|
||||
return {
|
||||
medicationId: medication.id,
|
||||
name: sanitizeForPrompt(medication.name),
|
||||
@@ -319,6 +338,7 @@ export async function prepareMedicationComplianceStatusForUser(
|
||||
taken7: compliance7.taken,
|
||||
skipped7: compliance7.skipped,
|
||||
missed7: compliance7.missed,
|
||||
...(signal ? { signal } : {}),
|
||||
dailySeries,
|
||||
latestDay,
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
buildGradedSeriesFromPoints,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { buildMetricSignal } from "@/lib/insights/metric-signal";
|
||||
import {
|
||||
type SupportedLocale,
|
||||
normalizeLocale,
|
||||
@@ -496,6 +497,18 @@ export async function prepareMoodStatusForUser(
|
||||
now,
|
||||
});
|
||||
|
||||
// v1.18.10 (HIGH-4) — hand the model the finished recent-vs-baseline
|
||||
// comparison + normal-swing verdict instead of asking it to derive them.
|
||||
// Mood reads higher-better against the green band.
|
||||
const moodSignal = buildMetricSignal({
|
||||
metric: locale === "en" ? "your mood" : "deine Stimmung",
|
||||
unit: "/5",
|
||||
direction: "higher-better",
|
||||
graded: moodGraded,
|
||||
normalRange: { low: greenMin, high: greenMax },
|
||||
newestDaysAgo: newestEntryDaysAgo,
|
||||
});
|
||||
|
||||
const snapshot = {
|
||||
locale,
|
||||
generatedForDay: todayKey,
|
||||
@@ -506,6 +519,7 @@ export async function prepareMoodStatusForUser(
|
||||
newestEntryDaysAgo,
|
||||
},
|
||||
mood: {
|
||||
...(moodSignal ? { signal: moodSignal } : {}),
|
||||
summary: moodSummary,
|
||||
series: moodGraded,
|
||||
latestDayFocus: latestMood
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
buildGradedSeriesWithRollups,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { buildMetricSignal } from "@/lib/insights/metric-signal";
|
||||
import {
|
||||
type SupportedLocale,
|
||||
normalizeLocale,
|
||||
@@ -321,6 +322,19 @@ export async function preparePulseStatusForUser(
|
||||
)
|
||||
: null;
|
||||
|
||||
// v1.18.10 (HIGH-4) — feed the model the finished recent-vs-baseline
|
||||
// comparison + normal-swing verdict instead of asking it to derive them from
|
||||
// the raw buckets (the "model computes a number" pattern the mandate bans).
|
||||
// Resting pulse reads lower-better; the green band is the coarse anchor.
|
||||
const pulseSignal = buildMetricSignal({
|
||||
metric: locale === "en" ? "your resting pulse" : "dein Ruhepuls",
|
||||
unit: "bpm",
|
||||
direction: "lower-better",
|
||||
graded: pulseGraded,
|
||||
normalRange: { low: pulseTarget.greenMin, high: pulseTarget.greenMax },
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
});
|
||||
|
||||
const snapshot = {
|
||||
locale,
|
||||
generatedForDay: todayKey,
|
||||
@@ -331,6 +345,7 @@ export async function preparePulseStatusForUser(
|
||||
newestMeasurementDaysAgo,
|
||||
},
|
||||
pulse: {
|
||||
...(pulseSignal ? { signal: pulseSignal } : {}),
|
||||
summary: pulseSummary,
|
||||
series: pulseGraded,
|
||||
latestDayFocus: latestPulse
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
buildGradedSeriesWithRollups,
|
||||
degradeStatusSnapshotToBudget,
|
||||
} from "@/lib/insights/graded-series";
|
||||
import { buildMetricSignal } from "@/lib/insights/metric-signal";
|
||||
import {
|
||||
type SupportedLocale,
|
||||
normalizeLocale,
|
||||
@@ -382,6 +383,18 @@ export async function prepareWeightStatusForUser(
|
||||
)
|
||||
: null;
|
||||
|
||||
// v1.18.10 (HIGH-4) — hand the model the finished recent-vs-baseline
|
||||
// comparison + normal-swing verdict instead of asking it to derive them.
|
||||
// Weight has no universal favourable direction or population band, so the
|
||||
// signal carries the neutral "target-band" framing and no normalRange.
|
||||
const weightSignal = buildMetricSignal({
|
||||
metric: locale === "en" ? "your weight" : "dein Gewicht",
|
||||
unit: "kg",
|
||||
direction: "target-band",
|
||||
graded: weightGraded,
|
||||
newestDaysAgo: newestMeasurementDaysAgo,
|
||||
});
|
||||
|
||||
const snapshot = {
|
||||
locale,
|
||||
generatedForDay: todayKey,
|
||||
@@ -392,6 +405,7 @@ export async function prepareWeightStatusForUser(
|
||||
newestMeasurementDaysAgo,
|
||||
},
|
||||
weight: {
|
||||
...(weightSignal ? { signal: weightSignal } : {}),
|
||||
summary: summarizeSeries(
|
||||
weightSeries.daily.map((bucket) => ({ value: bucket.value })),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user