fix(insights): write the weight note and its thresholds in the reader's mass unit

The weight status note had its own generator and never took the unit
preference: its signal, summary, graded series, latest-day figure,
target band and correlation pairs were all in kilograms, and the card
prompt stated the normal day-to-day swing, the plateau line and the
loss pace in kilograms too. A reader on pounds was handed both units.

The generator now converts every weight figure once after the canonical
arithmetic (the correlations are scale-free and stay as computed), the
card prompt states its thresholds in the reader's unit, and the unit
preference joins the input fingerprint so a switch is never answered
with the old note. The shared status-card examples and the briefing's
plateau pattern no longer show a weight in kilograms for the model to
copy.

Refs #1067
This commit is contained in:
Marc-André Bombeck committed 2026-10-01 13:23:17 +02:00
1 parent d11c165675
commit 7cb1b5ba05
5 files changed
+141 -43

No files matched your search

+2 -2
View File
@@ -259,12 +259,12 @@ Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zähl
id: "examples",
en: `EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:
- VERDICT-LED (meaning first, number as support, one step): "Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it."
- TREND-LED (direction first, then where it stands): "Your weight has been easing down steadily for three weeks now, sitting around 82.4 kg — about 1.1 kg under your 30-day average. Nothing dramatic, just a consistent direction."
- TREND-LED (direction first, then where it stands): "Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction."
- ONE-LINER (steady, NO forced step): "Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring."
- BAD (banned filler, ungrounded — do NOT write this): "Your numbers look good. Make sure to get enough sleep and keep up regular exercise."`,
de: `BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:
- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): "Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten."
- TREND-ZUERST (Richtung zuerst, dann der Stand): "Dein Gewicht geht seit drei Wochen ruhig nach unten, aktuell rund 82,4 kg — etwa 1,1 kg unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung."
- TREND-ZUERST (Richtung zuerst, dann der Stand): "Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung."
- EINZEILER (stabil, KEIN erzwungener Schritt): "Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig."
- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): "Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung."`,
},
+2 -2
View File
@@ -386,7 +386,7 @@ gib die obige Verweigerung zurück.`,
"consider increasing to Y mg", "stop at Z mg", or any
variation. A plateau finding ALWAYS frames the next decision
as a conversation with the prescribing clinician — pattern:
"Weight has settled around <kg> for three weeks at <dose> —
"Weight has settled around <weight> for three weeks at <dose> —
typical mid-titration. Worth mentioning at the next visit if
it persists." This is a SAFETY contract; treat any
dose-prescriptive instinct as a sign the response is
@@ -605,7 +605,7 @@ gib die obige Verweigerung zurück.`,
mg" oder eine Variante davon enthalten. Ein Plateau-Befund
rahmt die nächste Entscheidung IMMER als Gespräch mit der
behandelnden Ärztin — Muster: "Das Gewicht hat sich seit drei
Wochen bei <kg> auf <Dosis> eingependelt — typische
Wochen bei <weight> auf <Dosis> eingependelt — typische
mid-titration Phase. Lohnt sich beim nächsten Termin
anzusprechen, falls es darüber hinaus persistiert." Das ist
ein SICHERHEITS-Vertrag; behandle jeden dosis-präskriptiven
+36 -14
View File
@@ -2,36 +2,58 @@ import type { Locale } from "@/lib/i18n/config";
import { getBaseSystemPromptBody } from "./base-system";
import { withOutputLanguage } from "./output-language";
import { instructionLocale } from "./output-language";
import {
applyDisplayTransform,
DEFAULT_UNIT_PREFERENCE,
getDisplayTransform,
type UnitPreference,
} from "@/lib/measurements/display-transform";
const WEIGHT_SECTION_DE = `METRIK — GEWICHT:
function weightSectionDe(q: (kg: number) => string): string {
return `METRIK — GEWICHT:
- 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: 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.
- Fokus dieser Karte ist der kontinuierliche TREND und das TEMPO über die Zeit (Tempo pro 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: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von ${q(1)}-${q(2)} (Wasser, Tageszeit) sind normal und kein Befund.
- Eigenes Ziel: Trägt der Snapshot weight.target, ist das der Zielbereich, den die Person selbst gesetzt hat; weight.target.progress sagt, welche Richtung Fortschritt ist (gaining = zunehmen, losing = abnehmen, holding steady = halten). Beurteile jede Veränderung GEGEN dieses Ziel: Liegt die Person darunter, ist eine Zunahme Fortschritt und nie ein Rückschritt. Die Hinweise zu Tempo und Meilensteinen beim Abnehmen unten gelten nur, wenn Abnehmen die Fortschrittsrichtung ist oder kein Ziel vorliegt.
- 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.
- Plateau: Verändert sich das Mittel über > ~2 Wochen um < ${q(0.5)}, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.
- Tempo: Nachhaltiger Verlust liegt bei ~${q(0.5)}-${q(1)} pro Woche (DGE). Anhaltend > ${q(1)} pro 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.
- BMI-Bezug nur als knapper Nebensatz, wenn die Größe im Profil vorliegt (sonst nicht behaupten); die WHO-Band-Einordnung selbst gehört auf die BMI-Karte, nicht hierher.
- Zusammenhänge: weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation und moodContext.moodVsWeightCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.
- 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:
function weightSectionEn(q: (kg: number) => string): string {
return `METRIC — WEIGHT:
- 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: 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.
- This card's focus is the continuous TREND and PACE over time (pace per 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: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of ${q(1)}-${q(2)} (water, time of day) are normal and not a finding.
- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.
- 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.
- Plateau: if the mean moves < ${q(0.5)} over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.
- Pace: sustainable loss is ~${q(0.5)}-${q(1)} per week (DGE). Sustained > ${q(1)} per 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.
- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.
- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.
- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step.`;
}
export function getWeightSystemPrompt(locale: Locale): string {
export function getWeightSystemPrompt(
locale: Locale,
/**
* The reader's metric/imperial choice. The snapshot is in that unit, so
* the swing, plateau and pace thresholds are stated in it too.
*/
system: UnitPreference = DEFAULT_UNIT_PREFERENCE,
): string {
const mass = getDisplayTransform("WEIGHT", system);
const q = (kg: number) =>
`${applyDisplayTransform(kg, mass)} ${mass.displayUnit}`;
// fr/es/it/pl compose the ENGLISH body (the base prompt names their
// language and appends their own directive); only de takes the German one.
const section =
instructionLocale(locale) === "en" ? WEIGHT_SECTION_EN : WEIGHT_SECTION_DE;
instructionLocale(locale) === "en"
? weightSectionEn(q)
: weightSectionDe(q);
return withOutputLanguage(
`${getBaseSystemPromptBody(locale)}
@@ -64,12 +86,12 @@ export function getWeightUserPrompt(
: "";
if (instructionLocale(locale) === "en") {
return `Date: ${todayKey}${openerLine}
Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. "easing down steadily", "holding right where it's settled") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (kg/week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.${ctxBlock}${extraBlock}
Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. "easing down steadily", "holding right where it's settled") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.${ctxBlock}${extraBlock}
${snapshotJson}`;
}
return `Datum: ${todayKey}${openerLine}
Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. "geht ruhig nach unten", "hält sich genau da, wo es sich eingependelt hat") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (kg/Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. "geht ruhig nach unten", "hält sich genau da, wo es sich eingependelt hat") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
${snapshotJson}`;
}
@@ -438,3 +438,50 @@ describe("generateWeightStatusForUser — judged against the stored target (#100
expect(snapshot.weight.signal.direction).toBe("target-band");
});
});
describe("generateWeightStatusForUser — the reader's mass unit", () => {
it("hands an imperial reader every weight figure and threshold in pounds", async () => {
const now = new Date();
const records = Array.from({ length: 40 }, (_, day) => ({
type: "WEIGHT",
value: 80,
measuredAt: new Date(now.getTime() - day * dayMs),
}));
vi.mocked(prisma.user.findUnique).mockResolvedValue({
thresholdsJson: { WEIGHT: { min: 65, max: 70 } },
unitPreference: "imperial",
} as never);
vi.mocked(prisma.measurement.findMany).mockResolvedValue(records as never);
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
let sent: { systemPrompt: string; userPrompt: string } | null = null;
vi.mocked(runStatusCompletion).mockImplementation(async (args) => {
sent = args as never;
return {
kind: "ok",
content: '{"summary":"OK"}',
providerType: "anthropic",
model: "x",
tokensUsed: 1,
} as never;
});
await generateWeightStatusForUser("user-1", { locale: "en" });
const snapshot = JSON.parse(sent!.userPrompt.match(/\{[\s\S]*\}/)![0]);
expect(snapshot.weight.unit).toBe("lb");
expect(snapshot.weight.signal.unit).toBe("lb");
expect(snapshot.weight.latestDayFocus.value).toBe(176.4);
expect(snapshot.weight.summary.mean).toBe(176.4);
expect(snapshot.weight.series.recent[0].mean).toBe(176.4);
expect(snapshot.weight.target).toMatchObject({
min: 143.3,
max: 154.3,
unit: "lb",
});
expect(JSON.stringify(snapshot.weight)).not.toContain("kg");
// The card's own thresholds are stated in pounds as well.
expect(sent!.systemPrompt).toContain("< 1.1 lb over");
expect(sent!.systemPrompt).toContain("~1.1 lb-2.2 lb per week");
expect(sent!.systemPrompt).not.toMatch(/\d kg/);
});
});
+54 -25
View File
@@ -30,8 +30,16 @@ import {
import {
buildGradedSeriesFromPoints,
buildGradedSeriesWithRollups,
convertGradedSeries,
degradeStatusSnapshotToBudget,
} from "@/lib/insights/graded-series";
import {
applyDisplayTransform,
applyDisplayTransformDelta,
getReadingTransform,
resolveUnitPreferences,
transformRescales,
} from "@/lib/measurements/display-transform";
import { buildMetricSignal } from "@/lib/insights/metric-signal";
import {
type SupportedLocale,
@@ -236,20 +244,27 @@ export async function prepareWeightStatusForUser(
// v1.39 (#1006) — the person's own weight target decides which way is
// progress. Folded into the input fingerprint so editing the target flips
// the gate even when no weight row moved.
const weightTarget = resolveWeightTargetOverride(
(
await prisma.user.findUnique({
where: { id: userId },
select: { thresholdsJson: true },
})
)?.thresholdsJson,
);
const userRow = await prisma.user.findUnique({
where: { id: userId },
select: { thresholdsJson: true, unitPreference: true },
});
const weightTarget = resolveWeightTargetOverride(userRow?.thresholdsJson);
// The note is written in the reader's mass unit. Every weight figure the
// snapshot carries is converted once, after the canonical arithmetic; the
// correlations are scale-free and stay as computed.
const units = resolveUnitPreferences({
unitPreference: userRow?.unitPreference,
});
const mass = getReadingTransform("WEIGHT", units);
const toMass = (kg: number) => applyDisplayTransform(kg, mass);
const inputHash = await computeStatusInputFingerprint({
userId,
types: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"],
extra: {
weightTargetMin: weightTarget?.min ?? null,
weightTargetMax: weightTarget?.max ?? null,
// A unit switch changes every figure the note quotes.
unitPreference: units.system,
},
includeMood: true,
// v1.18.11 (P6-tighten) — WEIGHT is an FDR discovery OUTCOME channel and
@@ -370,11 +385,14 @@ export async function prepareWeightStatusForUser(
// the MONTH / YEAR rollup tier (with a full-history in-memory fallback
// on a cold-tier coverage miss). Mood has no rollup tier, so it stays
// an in-memory fold.
const [weightGraded, sysGraded, diaGraded] = await Promise.all([
const [canonicalWeightGraded, sysGraded, diaGraded] = await Promise.all([
buildGradedSeriesWithRollups(userId, "WEIGHT", now, userTz),
buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_SYS", now, userTz),
buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_DIA", now, userTz),
]);
const weightGraded = transformRescales(mass)
? convertGradedSeries(canonicalWeightGraded, mass, mass.decimals)
: canonicalWeightGraded;
const moodGraded = buildGradedSeriesFromPoints(
moodSeries.daily.map((b) => ({
measuredAt: canonicalDailyTimestamp(
@@ -473,17 +491,21 @@ export async function prepareWeightStatusForUser(
const recentWeights = weightSeries.daily
.filter((bucket) => bucket.dayOffset < 7)
.map((bucket) => bucket.value);
const weightTargetFeature = buildWeightTargetFeature(weightTarget, {
avg7:
recentWeights.length > 0
? recentWeights.reduce((sum, value) => sum + value, 0) /
recentWeights.length
: null,
latest: latestWeight?.value ?? null,
});
const weightTargetFeature = buildWeightTargetFeature(
weightTarget,
{
avg7:
recentWeights.length > 0
? recentWeights.reduce((sum, value) => sum + value, 0) /
recentWeights.length
: null,
latest: latestWeight?.value ?? null,
},
units.system,
);
const weightSignal = buildMetricSignal({
metric: locale === "en" ? "your weight" : "dein Gewicht",
unit: "kg",
unit: mass.displayUnit,
direction:
weightTargetFeature?.position === "below"
? "higher-better"
@@ -504,21 +526,25 @@ export async function prepareWeightStatusForUser(
newestMeasurementDaysAgo,
},
weight: {
unit: mass.displayUnit,
...(weightSignal ? { signal: weightSignal } : {}),
...(weightTargetFeature ? { target: weightTargetFeature } : {}),
summary: summarizeSeries(
weightSeries.daily.map((bucket) => ({ value: bucket.value })),
weightSeries.daily.map((bucket) => ({ value: toMass(bucket.value) })),
),
series: weightGraded,
latestDayFocus: latestWeight
? {
day: latestWeightDay,
dayOffset: latestWeight.dayOffset,
value: latestWeight.value,
value: toMass(latestWeight.value),
deltaToPreviousDailyPoint:
previousWeight == null
? null
: round(latestWeight.value - previousWeight.value, 2),
: applyDisplayTransformDelta(
round(latestWeight.value - previousWeight.value, 2),
mass,
),
sameDayBloodPressure: sameDayBp,
}
: null,
@@ -547,7 +573,7 @@ export async function prepareWeightStatusForUser(
.slice(-CORRELATION_PAIR_CAP)
.map((entry) => ({
day: entry.dayKey,
weight: round(entry.a, 2),
weight: toMass(round(entry.a, 2)),
systolic: round(entry.b, 2),
})),
},
@@ -555,7 +581,7 @@ export async function prepareWeightStatusForUser(
correlation: weightVsMeanBpCorrelation,
pairs: weightVsMeanBpPairs.slice(-CORRELATION_PAIR_CAP).map((entry) => ({
day: entry.dayKey,
weight: round(entry.a, 2),
weight: toMass(round(entry.a, 2)),
meanBloodPressure: round(entry.b, 2),
})),
},
@@ -626,7 +652,10 @@ export async function prepareWeightStatusForUser(
// v1.12.7 — diversity / anti-repetition block (see blood-pressure-status).
const varietyLead = pickVarietyLead(userId, "weight", todayKey);
const steadyRun = computeSteadyRun(weightGraded.weekly, weightGraded.monthly);
const steadyRun = computeSteadyRun(
canonicalWeightGraded.weekly,
canonicalWeightGraded.monthly,
);
const relations = await getRelevantCorrelationsForMetric(
userId,
"WEIGHT",
@@ -654,7 +683,7 @@ export async function prepareWeightStatusForUser(
metric: "weight",
userId,
cacheAction,
systemPrompt: getWeightSystemPrompt(locale),
systemPrompt: getWeightSystemPrompt(locale, units.system),
userPrompt: getWeightUserPrompt(
snapshotJson,
todayKey,